From 23cf8414b8de51d86719498c78b996b5ce203f1f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Aug 2019 16:20:08 -0700 Subject: [PATCH 0001/3953] 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 eebd10f4fb9089..aebcb95c3b1a5b 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 7c584daae7ea6f..db3f3ac273dbd5 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 0002/3953] 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 81bb1944bed2d0..d0cfc294db128f 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 0003/3953] 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 d0cfc294db128f..7409be5f98cd4a 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 0004/3953] 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 7409be5f98cd4a..44d910a8106903 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 0005/3953] 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 44d910a8106903..2ad13288e081fc 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 0006/3953] 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 2ad13288e081fc..6b986329291877 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 0007/3953] 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 648fc8b96dff09..8d6271183bd87e 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 b26e1c7e59faf6..0f4fb56970ba78 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 33d6be841cf041..f5484829de71a8 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 9d254f72e9bb38..b5d139719ef6de 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 0008/3953] 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 788da6a8d64321..715c06aca43c82 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -9,8 +9,10 @@ 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 python_script_service_handler(call): 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 fcf1519d4c76a2..d7732c00f94fe1 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -3,8 +3,11 @@ 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 0009/3953] 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 8ecbfeab01a17e..97746f3f472b43 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -38,7 +38,7 @@ 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 @@ def available(self) -> bool: @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 0fffad8e97eff8..b086eaa29c75f7 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -117,9 +117,10 @@ def icon(self) -> Optional[str]: 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 add03c6b644605..c15b3121d3a63e 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -34,6 +34,7 @@ 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 @@ def unit_of_measurement(self) -> str: """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 0010/3953] 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 409b4d382083e8..38b7018af6c6c2 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -24,14 +24,12 @@ 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 @@ 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 @@ 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_leaf(car_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 @@ def __init__(self, hass, leaf, car_config): 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 @@ async def async_refresh_data(self, now): 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 @@ async def async_get_battery(self): 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 @@ async def async_get_battery(self): ) 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 @@ async def async_set_climate(self, toggle): _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 11d18ee5a8e7cd..00000000000000 --- 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 ab94c01b7c1272..70aaa112414beb 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 f5484829de71a8..a2b91f7eb7d069 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 0011/3953] 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 | 19 ++++++++++-- tests/helpers/test_entity.py | 31 +++++++++++++++++-- tests/helpers/test_entity_platform.py | 2 +- 5 files changed, 62 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index aecdf45dde5f88..7de41415f080ec 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 @@ def async_write_ha_state(self): @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 74351ac50af89f..4a6a3038fd0c97 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -349,6 +349,9 @@ async def _async_add_entity( 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 @@ async def _async_add_entity( ) 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 f18abe9b0e2b0f..64328a0c8c5647 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,11 +151,24 @@ async def test_update_entity(hass, client): state = hass.states.get("test_domain.world") assert state.name == "after update" + # 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": 7, + "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 58f76d396c1def..94650592d8e1bb 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -7,13 +7,13 @@ 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 606a4c82096291..caf8bb702afe87 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 0012/3953] 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 57eb3f17584da5..8d47d8a0173bad 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 a2b91f7eb7d069..d3d60e6a43e66e 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 0013/3953] 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 aebcb95c3b1a5b..a2a79eee24949f 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 0014/3953] 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 6b986329291877..2e537fbb774567 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 0015/3953] 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 8d6271183bd87e..78f87639a9929a 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 0f4fb56970ba78..873a5aaf31deb9 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 d3d60e6a43e66e..c9475f4b65b67a 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 b5d139719ef6de..50f49296247805 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 0016/3953] 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 2e1fbea14d1392..c2da37943c1abb 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3,13 +3,7 @@ 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.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 @@ async def async_setup( 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 @@ async def async_setup( 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 @@ def __init__(self, hass: HomeAssistant, hass_config: dict) -> None: 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 @@ def update(self, *, disable_new_entities): 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 7de41415f080ec..bd96e1bafdb5f4 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -503,6 +503,10 @@ async def _async_registry_updated(self, event): 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 3d84313a5c650d..7d81f62fa1c051 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -302,7 +302,7 @@ def _async_update_entity( 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 64328a0c8c5647..9472d8882540c9 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 94650592d8e1bb..3c89a5c65379d6 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 aee6b6f19a3965..9debbdbcba7cda 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 ca6872a7a2cc1e..d9dd614c9a5e4a 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -20,6 +20,7 @@ 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 0017/3953] 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 7379d66777b01c..8a319c655f6f0d 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -7,7 +7,7 @@ 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 @@ 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 0018/3953] 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 24eb61d52b00e8..047eaaaf5db9c1 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 ef9293381fd491..db4ff9e851ec82 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -270,6 +270,9 @@ def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): 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 @@ def state(self): """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 @@ def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): 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 @@ def supported_features(self): """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 c9475f4b65b67a..70483d1f2e2134 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 0019/3953] 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 0887aa19bfba6a..2149e40e5045f5 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -52,7 +52,7 @@ def __init__(self, hass, config: ConfigType, see) -> None: 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 0020/3953] 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 028121a966354e..d44a1c7760aae4 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -170,7 +170,7 @@ async def async_update(self): """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 0021/3953] 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 a5450253be0c10..20ed7930a4fc68 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -4,7 +4,7 @@ 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 disconnect_controller(event): 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 @@ async def _heos_event(self, event): # 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 @@ async def get_sources(): 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 7c7f57a91d7851..1d56478ba3ac6f 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 @@ async def async_step_user(self, user_input=None): 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 09833bb729b416..eb9ef258a3cbd2 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 a4094a0c216de7..40f6113a80d794 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 decorator(func): 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 8f3521399e2f50..ee5df1b483b552 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 70483d1f2e2134..b467859343f4cc 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 50f49296247805..96b82caf968372 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 0d834ccc770f7a..df021fea55df5c 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 728e65b81f5b2e..7b2645cb8ecb0a 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 de062757803b0f..0f9bf2d8b3ee6a 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 set_signal(): 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 0e1cbc8ea2e576..5a835cf73036a4 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 0022/3953] 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 0e00489303322c..8e7de41e626b35 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 b467859343f4cc..42982b34134285 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 0023/3953] 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 a2a79eee24949f..4d2998b85b8ff3 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 0024/3953] 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 2e537fbb774567..fab10bfeee6927 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 0025/3953] 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 fab10bfeee6927..896aaa710c12f6 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 0026/3953] 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 896aaa710c12f6..7c88e615fa5baa 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 8e5d272b5fabb33cb1236ab3c82e16a71fcba38f Mon Sep 17 00:00:00 2001 From: presslab-us Date: Mon, 26 Aug 2019 23:16:54 -0400 Subject: [PATCH 0027/3953] Support formatting and scaling with ZHA Metering cluster (#26201) * Support formatting and scaling with Metering cluster * fix lint * run black formatter --- .../zha/core/channels/smartenergy.py | 83 +++++++++++++++++++ homeassistant/components/zha/sensor.py | 22 +++-- tests/components/zha/test_sensor.py | 4 +- 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index a182193cabafee..8e2fa7e3d5a3f2 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -8,6 +8,8 @@ import zigpy.zcl.clusters.smartenergy as smartenergy +from homeassistant.core import callback + from .. import registries from ..channels import AttributeListeningChannel, ZigbeeChannel from ..const import REPORT_CONFIG_DEFAULT @@ -77,6 +79,87 @@ class Metering(AttributeListeningChannel): REPORT_CONFIG = [{"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT}] + unit_of_measure_map = { + 0x00: "kW", + 0x01: "m³/h", + 0x02: "ft³/h", + 0x03: "ccf/h", + 0x04: "US gal/h", + 0x05: "IMP gal/h", + 0x06: "BTU/h", + 0x07: "l/h", + 0x08: "kPa", + 0x09: "kPa", + 0x0A: "mcf/h", + 0x0B: "unitless", + 0x0C: "MJ/s", + } + + def __init__(self, cluster, device): + """Initialize Metering.""" + super().__init__(cluster, device) + self._divisor = None + self._multiplier = None + self._unit_enum = None + self._format_spec = None + + async def async_configure(self): + """Configure channel.""" + await self.fetch_config(False) + await super().async_configure() + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.fetch_config(True) + await super().async_initialize(from_cache) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute update from Metering cluster.""" + super().attribute_updated(attrid, value * self._multiplier / self._divisor) + + @property + def unit_of_measurement(self): + """Return unit of measurement.""" + return self.unit_of_measure_map.get(self._unit_enum & 0x7F, "unknown") + + async def fetch_config(self, from_cache): + """Fetch config from device and updates format specifier.""" + self._divisor = await self.get_attribute_value("divisor", from_cache=from_cache) + self._multiplier = await self.get_attribute_value( + "multiplier", from_cache=from_cache + ) + self._unit_enum = await self.get_attribute_value( + "unit_of_measure", from_cache=from_cache + ) + fmting = await self.get_attribute_value( + "demand_formatting", from_cache=from_cache + ) + + if self._divisor is None or self._divisor == 0: + self._divisor = 1 + if self._multiplier is None or self._multiplier == 0: + self._multiplier = 1 + if self._unit_enum is None: + self._unit_enum = 0x7F # unknown + if fmting is None: + fmting = 0xF9 # 1 digit to the right, 15 digits to the left + + r_digits = fmting & 0x07 # digits to the right of decimal point + l_digits = (fmting >> 3) & 0x0F # digits to the left of decimal point + if l_digits == 0: + l_digits = 15 + width = r_digits + l_digits + (1 if r_digits > 0 else 0) + + if fmting & 0x80: + self._format_spec = "{:" + str(width) + "." + str(r_digits) + "f}" + else: + self._format_spec = "{:0" + str(width) + "." + str(r_digits) + "f}" + + def formatter_function(self, value): + """Return formatted value for display.""" + return self._format_spec.format(value).lstrip() + @registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Prepayment.cluster_id) class Prepayment(ZigbeeChannel): diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index e38acebb22c2f8..b260dfc5459bc5 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -136,7 +136,6 @@ async def async_battery_device_state_attr_provider(channel): SENSOR_TEMPERATURE: TEMP_CELSIUS, SENSOR_PRESSURE: "hPa", SENSOR_ILLUMINANCE: "lx", - SENSOR_METERING: POWER_WATT, SENSOR_ELECTRICAL_MEASUREMENT: POWER_WATT, SENSOR_GENERIC: None, SENSOR_BATTERY: "%", @@ -219,15 +218,19 @@ 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, SENSOR_GENERIC) - self._unit = UNIT_REGISTRY.get(self._sensor_type) - self._formatter_function = FORMATTER_FUNC_REGISTRY.get( - self._sensor_type, pass_through_formatter - ) - 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, CHANNEL_ATTRIBUTE) ) + if self._sensor_type == SENSOR_METERING: + self._unit = self._channel.unit_of_measurement + self._formatter_function = self._channel.formatter_function + else: + self._unit = UNIT_REGISTRY.get(self._sensor_type) + self._formatter_function = FORMATTER_FUNC_REGISTRY.get( + self._sensor_type, pass_through_formatter + ) + self._force_update = FORCE_UPDATE_REGISTRY.get(self._sensor_type, False) + self._should_poll = POLLING_REGISTRY.get(self._sensor_type, False) self._device_class = DEVICE_CLASS_REGISTRY.get(self._sensor_type, None) self.state_attr_provider = DEVICE_STATE_ATTR_PROVIDER_REGISTRY.get( self._sensor_type, None @@ -271,7 +274,10 @@ def async_set_state(self, state): # this is necessary because HA saves the unit based on what shows in # the UI and not based on what the sensor has configured so we need # to flip it back after state restoration - self._unit = UNIT_REGISTRY.get(self._sensor_type) + if self._sensor_type == SENSOR_METERING: + self._unit = self._channel.unit_of_measurement + else: + self._unit = UNIT_REGISTRY.get(self._sensor_type) self._state = self._formatter_function(state) self.async_schedule_update_ha_state() diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index bd5e2add68b5da..dc3ea35229f2b4 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -160,8 +160,8 @@ async def async_test_illuminance(hass, device_info): async def async_test_metering(hass, device_info): """Test metering sensor.""" - await send_attribute_report(hass, device_info["cluster"], 1024, 10) - assert_state(hass, device_info, "10", "W") + await send_attribute_report(hass, device_info["cluster"], 1024, 12345) + assert_state(hass, device_info, "12345.0", "unknown") async def async_test_electrical_measurement(hass, device_info): From c185c015ef7aca189a0110e500bf92119564d724 Mon Sep 17 00:00:00 2001 From: Florian Klien Date: Tue, 27 Aug 2019 07:34:58 +0200 Subject: [PATCH 0028/3953] 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 27b6a2da59ff66..153f6b5aea6972 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 4733cf666c85bc..bec3670cf2a32f 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 9dc40197e9d2ea44aa1a4ede501c13476de77e55 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Tue, 27 Aug 2019 03:30:41 -0500 Subject: [PATCH 0029/3953] Fix flaky updater tests (#26221) --- tests/components/updater/test_init.py | 118 +++++++++++++------------- 1 file changed, 58 insertions(+), 60 deletions(-) diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py index 014fb7b6f45f28..237b8125072af6 100644 --- a/tests/components/updater/test_init.py +++ b/tests/components/updater/test_init.py @@ -1,18 +1,19 @@ """The tests for the Updater component.""" import asyncio from datetime import timedelta -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import updater +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util + from tests.common import ( + MockDependency, async_fire_time_changed, - mock_coro, mock_component, - MockDependency, + mock_coro, ) NEW_VERSION = "10000.0" @@ -31,44 +32,44 @@ def mock_distro(): yield -@pytest.fixture -def mock_get_newest_version(): +@pytest.fixture(name="mock_get_newest_version") +def mock_get_newest_version_fixture(): """Fixture to mock get_newest_version.""" with patch("homeassistant.components.updater.get_newest_version") as mock: yield mock -@pytest.fixture -def mock_get_uuid(): +@pytest.fixture(name="mock_get_uuid") +def mock_get_uuid_fixture(): """Fixture to mock get_uuid.""" with patch("homeassistant.components.updater._load_uuid") as mock: yield mock -@pytest.fixture -def mock_utcnow(): +@pytest.fixture(name="mock_utcnow") +def mock_utcnow_fixture(): """Fixture to mock utcnow.""" - with patch("homeassistant.components.updater.dt_util.utcnow") as mock: - yield mock + with patch("homeassistant.components.updater.dt_util") as mock: + yield mock.utcnow -@asyncio.coroutine -def test_new_version_shows_entity_startup(hass, mock_get_uuid, mock_get_newest_version): +async 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, RELEASE_NOTES)) - res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) + res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) assert res, "Updater failed to set up" - yield from hass.async_block_till_done() + await 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_rename_entity(hass, mock_get_uuid, mock_get_newest_version): +async def test_rename_entity(hass, mock_get_uuid, mock_get_newest_version, mock_utcnow): """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)) @@ -77,32 +78,33 @@ def test_rename_entity(hass, mock_get_uuid, mock_get_newest_version): later = now + timedelta(hours=1) mock_utcnow.return_value = now - res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) + res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) assert res, "Updater failed to set up" - yield from hass.async_block_till_done() + await 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 = await 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() + await 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() + await 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): +async def test_new_version_shows_entity_true( + hass, mock_get_uuid, mock_get_newest_version, mock_utcnow +): """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)) @@ -111,13 +113,13 @@ def test_new_version_shows_entity_true(hass, mock_get_uuid, mock_get_newest_vers later = now + timedelta(hours=1) mock_utcnow.return_value = now - res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) + res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) assert res, "Updater failed to set up" - yield from hass.async_block_till_done() + await 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() + await hass.async_block_till_done() assert hass.states.is_state("binary_sensor.updater", "on") assert ( @@ -130,8 +132,9 @@ def test_new_version_shows_entity_true(hass, mock_get_uuid, mock_get_newest_vers ) -@asyncio.coroutine -def test_same_version_shows_entity_false(hass, mock_get_uuid, mock_get_newest_version): +async def test_same_version_shows_entity_false( + hass, mock_get_uuid, mock_get_newest_version, mock_utcnow +): """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, "")) @@ -140,13 +143,13 @@ def test_same_version_shows_entity_false(hass, mock_get_uuid, mock_get_newest_ve later = now + timedelta(hours=1) mock_utcnow.return_value = now - res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) + res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) assert res, "Updater failed to set up" - yield from hass.async_block_till_done() + await 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() + await hass.async_block_till_done() assert hass.states.is_state("binary_sensor.updater", "off") assert ( @@ -156,8 +159,9 @@ def test_same_version_shows_entity_false(hass, mock_get_uuid, mock_get_newest_ve 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): +async def test_disable_reporting( + hass, mock_get_uuid, mock_get_newest_version, mock_utcnow +): """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, "")) @@ -166,37 +170,35 @@ def test_disable_reporting(hass, mock_get_uuid, mock_get_newest_version): later = now + timedelta(hours=1) mock_utcnow.return_value = now - res = yield from async_setup_component( + res = await async_setup_component( hass, updater.DOMAIN, {updater.DOMAIN: {"reporting": False}} ) assert res, "Updater failed to set up" - yield from hass.async_block_till_done() + await 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() + await hass.async_block_till_done() assert hass.states.is_state("binary_sensor.updater", "off") - res = yield from updater.get_newest_version(hass, MOCK_HUUID, MOCK_CONFIG) + res = await updater.get_newest_version(hass, MOCK_HUUID, MOCK_CONFIG) call = mock_get_newest_version.mock_calls[0][1] assert call[0] is hass assert call[1] is None -@asyncio.coroutine -def test_get_newest_version_no_analytics_when_no_huuid(hass, aioclient_mock): +async def test_get_newest_version_no_analytics_when_no_huuid(hass, aioclient_mock): """Test we do not gather analytics when no huuid is passed in.""" aioclient_mock.post(updater.UPDATER_URL, json=MOCK_RESPONSE) with patch( "homeassistant.helpers.system_info.async_get_system_info", side_effect=Exception ): - res = yield from updater.get_newest_version(hass, None, False) + res = await updater.get_newest_version(hass, None, False) assert res == (MOCK_RESPONSE["version"], MOCK_RESPONSE["release-notes"]) -@asyncio.coroutine -def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock): +async def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock): """Test we gather analytics when huuid is passed in.""" aioclient_mock.post(updater.UPDATER_URL, json=MOCK_RESPONSE) @@ -204,23 +206,21 @@ def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock): "homeassistant.helpers.system_info.async_get_system_info", Mock(return_value=mock_coro({"fake": "bla"})), ): - res = yield from updater.get_newest_version(hass, MOCK_HUUID, False) + res = await updater.get_newest_version(hass, MOCK_HUUID, False) assert res == (MOCK_RESPONSE["version"], MOCK_RESPONSE["release-notes"]) -@asyncio.coroutine -def test_error_fetching_new_version_timeout(hass): +async def test_error_fetching_new_version_timeout(hass): """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"})), ), patch("async_timeout.timeout", side_effect=asyncio.TimeoutError): - res = yield from updater.get_newest_version(hass, MOCK_HUUID, False) + res = await updater.get_newest_version(hass, MOCK_HUUID, False) assert res is None -@asyncio.coroutine -def test_error_fetching_new_version_bad_json(hass, aioclient_mock): +async def test_error_fetching_new_version_bad_json(hass, aioclient_mock): """Test we handle json error while fetching new version.""" aioclient_mock.post(updater.UPDATER_URL, text="not json") @@ -228,12 +228,11 @@ def test_error_fetching_new_version_bad_json(hass, aioclient_mock): "homeassistant.helpers.system_info.async_get_system_info", Mock(return_value=mock_coro({"fake": "bla"})), ): - res = yield from updater.get_newest_version(hass, MOCK_HUUID, False) + res = await updater.get_newest_version(hass, MOCK_HUUID, False) assert res is None -@asyncio.coroutine -def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): +async def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): """Test we handle response error while fetching new version.""" aioclient_mock.post( updater.UPDATER_URL, @@ -247,13 +246,12 @@ def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): "homeassistant.helpers.system_info.async_get_system_info", Mock(return_value=mock_coro({"fake": "bla"})), ): - res = yield from updater.get_newest_version(hass, MOCK_HUUID, False) + res = await updater.get_newest_version(hass, MOCK_HUUID, False) assert res is None -@asyncio.coroutine -def test_new_version_shows_entity_after_hour_hassio( - hass, mock_get_uuid, mock_get_newest_version +async def test_new_version_shows_entity_after_hour_hassio( + hass, mock_get_uuid, mock_get_newest_version, mock_utcnow ): """Test if binary sensor gets updated if new version is available / hass.io.""" mock_get_uuid.return_value = MOCK_HUUID @@ -265,13 +263,13 @@ def test_new_version_shows_entity_after_hour_hassio( later = now + timedelta(hours=1) mock_utcnow.return_value = now - res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) + res = await async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) assert res, "Updater failed to set up" - yield from hass.async_block_till_done() + await 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() + await hass.async_block_till_done() assert hass.states.is_state("binary_sensor.updater", "on") assert ( From d9ae63e2395bdbd78953a1bbf5f6f4bb7f495b85 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Tue, 27 Aug 2019 10:00:54 -0400 Subject: [PATCH 0030/3953] Remove throttle from update (#26216) --- homeassistant/components/environment_canada/weather.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index ebb6b0cd51fc7e..a4fad083d2a6e0 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -20,7 +20,6 @@ WeatherEntity, ) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS -from homeassistant.util import Throttle import homeassistant.util.dt as dt import homeassistant.helpers.config_validation as cv @@ -30,8 +29,6 @@ CONF_ATTRIBUTION = "Data provided by Environment Canada" CONF_STATION = "station" -MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=10) - def validate_station(station): """Check that the station ID is well-formed.""" @@ -171,7 +168,6 @@ def forecast(self): """Return the forecast array.""" return get_forecast(self.ec_data, self.forecast_type) - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Environment Canada.""" self.ec_data.update() From 1f2e0d39498f5a3d025f1f84e19db5f51a360d54 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 27 Aug 2019 21:06:14 +0200 Subject: [PATCH 0031/3953] 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 caa46e10f99131..be4088a5c86592 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -14,8 +14,6 @@ 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 @@ def async_add_cover(lights): 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 @@ def __init__(self, device, gateway): @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 @@ async def async_set_cover_position(self, **kwargs): 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 @@ async def async_stop_cover(self, **kwargs): """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 f264877b77a5f8..7230ff4fb7bdff 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -16,7 +16,7 @@ "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 @@ "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 6525f8704a0255fe5a64978f4b5fe087724b66de Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Tue, 27 Aug 2019 15:30:19 -0400 Subject: [PATCH 0032/3953] Bump dependency to add PLAY_STATE_STOPPED (#26239) --- homeassistant/components/apple_tv/manifest.json | 2 +- homeassistant/components/apple_tv/media_player.py | 1 + requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index f21de7333767fc..c391fb0e14ba0e 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple tv", "documentation": "https://www.home-assistant.io/components/apple_tv", "requirements": [ - "pyatv==0.3.12" + "pyatv==0.3.13" ], "dependencies": ["configurator"], "codeowners": [] diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 8ecaeab424c8f2..3e77c7a1cfd8e3 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -127,6 +127,7 @@ def state(self): const.PLAY_STATE_PAUSED, const.PLAY_STATE_FAST_FORWARD, const.PLAY_STATE_FAST_BACKWARD, + const.PLAY_STATE_STOPPED, ): # Catch fast forward/backward here so "play" is default action return STATE_PAUSED diff --git a/requirements_all.txt b/requirements_all.txt index bec3670cf2a32f..147cbb3a8a99a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,7 @@ pyarlo==0.2.3 pyatmo==2.2.1 # homeassistant.components.apple_tv -pyatv==0.3.12 +pyatv==0.3.13 # homeassistant.components.bbox pybbox==0.0.5-alpha From 3c07a9b4c75bb3baaf132018ba322a8bc79b8103 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Tue, 27 Aug 2019 18:08:09 -0500 Subject: [PATCH 0033/3953] Cleanup strings (#26243) --- homeassistant/components/smartthings/__init__.py | 15 +++++---------- homeassistant/components/smartthings/climate.py | 2 +- homeassistant/components/smartthings/const.py | 5 +---- homeassistant/components/smartthings/smartapp.py | 8 +++----- tests/components/smartthings/test_init.py | 2 +- 5 files changed, 11 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 555fc6ec765bc6..93f7cbb8f32997 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -78,8 +78,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Initialize config entry which represents an installed SmartApp.""" if not validate_webhook_requirements(hass): _LOGGER.warning( - "The 'base_url' of the 'http' integration must be " - "configured and start with 'https://'" + "The 'base_url' of the 'http' integration must be configured and start with 'https://'" ) return False @@ -121,8 +120,7 @@ async def retrieve_device_status(device): await device.status.refresh() except ClientResponseError: _LOGGER.debug( - "Unable to update status for device: %s (%s), " - "the device will be excluded", + "Unable to update status for device: %s (%s), the device will be excluded", device.label, device.device_id, exc_info=True, @@ -148,8 +146,7 @@ async def retrieve_device_status(device): except ClientResponseError as ex: if ex.status in (401, 403): _LOGGER.exception( - "Unable to setup config entry '%s' - please " - "reconfigure the integration", + "Unable to setup config entry '%s' - please reconfigure the integration", entry.title, ) remove_entry = True @@ -186,9 +183,7 @@ async def async_get_entry_scenes(entry: ConfigEntry, api): except ClientResponseError as ex: if ex.status == 403: _LOGGER.exception( - "Unable to load scenes for config entry '%s' " - "because the access token does not have the " - "required access", + "Unable to load scenes for config entry '%s' because the access token does not have the required access", entry.title, ) else: @@ -235,7 +230,7 @@ async def async_remove_entry(hass: HomeAssistantType, entry: ConfigEntry) -> Non app_count = sum(1 for entry in all_entries if entry.data[CONF_APP_ID] == app_id) if app_count > 1: _LOGGER.debug( - "App %s was not removed because it is in use by other" "config entries", + "App %s was not removed because it is in use by other config entries", app_id, ) return diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index bb307523e97084..4f005a326cde52 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -406,7 +406,7 @@ async def async_update(self): modes.add(state) else: _LOGGER.debug( - "Device %s (%s) returned an invalid supported " "AC mode: %s", + "Device %s (%s) returned an invalid supported AC mode: %s", self._device.label, self._device.device_id, mode, diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index cd9fc1ccdf8ae9..c258101da70c9e 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -37,8 +37,5 @@ "scene", ] TOKEN_REFRESH_INTERVAL = timedelta(days=14) -VAL_UID = ( - "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" - "{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$" -) +VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$" VAL_UID_MATCHER = re.compile(VAL_UID) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index b152ba3328f6bb..b64ba690d41dbc 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -77,8 +77,7 @@ async def validate_installed_app(api, installed_app_id: str): installed_app = await api.installed_app(installed_app_id) if installed_app.installed_app_status != InstalledAppStatus.AUTHORIZED: raise RuntimeWarning( - "Installed SmartApp instance '{}' ({}) is not " - "AUTHORIZED but instead {}".format( + "Installed SmartApp instance '{}' ({}) is not AUTHORIZED but instead {}".format( installed_app.display_name, installed_app.installed_app_id, installed_app.installed_app_status, @@ -321,7 +320,7 @@ async def create_subscription(target: str): ) except Exception as error: # pylint:disable=broad-except _LOGGER.error( - "Failed to create subscription for '%s' under app " "'%s': %s", + "Failed to create subscription for '%s' under app '%s': %s", target, installed_app_id, error, @@ -331,8 +330,7 @@ async def delete_subscription(sub: SubscriptionEntity): try: await api.delete_subscription(installed_app_id, sub.subscription_id) _LOGGER.debug( - "Removed subscription for '%s' under app '%s' " - "because it was no longer needed", + "Removed subscription for '%s' under app '%s' because it was no longer needed", sub.capability, installed_app_id, ) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 15b556f1d83cb8..4e1ffce7e22119 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -371,7 +371,7 @@ def async_track_time_interval(hass, action, interval): stored_action = action with patch( - "homeassistant.components.smartthings" ".async_track_time_interval", + "homeassistant.components.smartthings.async_track_time_interval", new=async_track_time_interval, ): broker = smartthings.DeviceBroker(hass, config_entry, token, Mock(), [], []) From a28e644def4da4d2d7056e8029c1e17ece8e99ca Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Wed, 28 Aug 2019 09:21:21 +0200 Subject: [PATCH 0034/3953] 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 b2692a37059be0..34aed146cf088b 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 49ad527a371c743cc698175db313f3bb65429c67 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 29 Aug 2019 00:42:39 +1000 Subject: [PATCH 0035/3953] Fix WWLLN entity management (#26250) * added debug logging * fixed manager to keep track of managed external ids --- homeassistant/components/wwlln/geo_location.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/wwlln/geo_location.py b/homeassistant/components/wwlln/geo_location.py index 661972ff4371ff..e8dd7ec08c774c 100644 --- a/homeassistant/components/wwlln/geo_location.py +++ b/homeassistant/components/wwlln/geo_location.py @@ -88,6 +88,7 @@ def __init__( @callback def _create_events(self, ids_to_create): """Create new geo location events.""" + _LOGGER.debug("Going to create %s", ids_to_create) events = [] for strike_id in ids_to_create: strike = self._strikes[strike_id] @@ -106,6 +107,7 @@ def _create_events(self, ids_to_create): @callback def _remove_events(self, ids_to_remove): """Remove old geo location events.""" + _LOGGER.debug("Going to remove %s", ids_to_remove) for strike_id in ids_to_remove: async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(strike_id)) @@ -136,12 +138,17 @@ async def async_update(self): return new_strike_ids = set(self._strikes) + # Remove all managed entities that are not in the latest update anymore. ids_to_remove = self._managed_strike_ids.difference(new_strike_ids) self._remove_events(ids_to_remove) + # Create new entities for all strikes that are not managed entities yet. ids_to_create = new_strike_ids.difference(self._managed_strike_ids) self._create_events(ids_to_create) + # Store all external IDs of all managed strikes. + self._managed_strike_ids = new_strike_ids + class WWLLNEvent(GeolocationEvent): """Define a lightning strike event.""" From 33bd9c83fb782d3f99297fcff7e313d13def658e Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Wed, 28 Aug 2019 19:35:09 +0200 Subject: [PATCH 0036/3953] Enable cert_expiry config entries (#25624) * Enable cert_expiry config entries * add black * lint fixes * Rerun black * Black on json files is a bad idea * Work on comments * Forgot the lint * More comment work * Correctly set defaults * More comments * Add codeowner * Fix black * More comments implemented * Removed the catch * Add helper.py from cert_expiry to .coveragerc --- .coveragerc | 1 + CODEOWNERS | 1 + .../cert_expiry/.translations/en.json | 24 +++ .../components/cert_expiry/__init__.py | 24 +++ .../components/cert_expiry/config_flow.py | 98 +++++++++++++ homeassistant/components/cert_expiry/const.py | 6 + .../components/cert_expiry/helper.py | 15 ++ .../components/cert_expiry/manifest.json | 13 +- .../components/cert_expiry/sensor.py | 49 +++---- .../components/cert_expiry/strings.json | 24 +++ homeassistant/generated/config_flows.py | 1 + tests/components/cert_expiry/__init__.py | 1 + .../cert_expiry/test_config_flow.py | 137 ++++++++++++++++++ 13 files changed, 358 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/cert_expiry/.translations/en.json create mode 100644 homeassistant/components/cert_expiry/config_flow.py create mode 100644 homeassistant/components/cert_expiry/const.py create mode 100644 homeassistant/components/cert_expiry/helper.py create mode 100644 homeassistant/components/cert_expiry/strings.json create mode 100644 tests/components/cert_expiry/__init__.py create mode 100644 tests/components/cert_expiry/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 1d861d69c1dfe1..02d59b55f5f2a1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -93,6 +93,7 @@ omit = homeassistant/components/canary/camera.py homeassistant/components/cast/* homeassistant/components/cert_expiry/sensor.py + homeassistant/components/cert_expiry/helper.py homeassistant/components/channels/media_player.py homeassistant/components/cisco_ios/device_tracker.py homeassistant/components/cisco_mobility_express/device_tracker.py diff --git a/CODEOWNERS b/CODEOWNERS index 3ede39518c1874..d51031486ef7cc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -46,6 +46,7 @@ homeassistant/components/broadlink/* @danielhiversen homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties +homeassistant/components/cert_expiry/* @cereal2nd homeassistant/components/cisco_ios/* @fbradyirl homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json new file mode 100644 index 00000000000000..b6aa1cefb02aed --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "This host and port combination is already configured" + }, + "error": { + "certificate_fetch_failed": "Can not fetch certificate from this host and port combination", + "connection_timeout": "Timeout whemn connecting to this host", + "host_port_exists": "This host and port combination is already configured", + "resolve_failed": "This host can not be resolved" + }, + "step": { + "user": { + "data": { + "host": "The hostname of the certificate", + "name": "The name of the certificate", + "port": "The port of the certificate" + }, + "title": "Define the certificate to test" + } + }, + "title": "Certificate Expiry" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 78ceb60dd404b7..ab68d5ba08bc43 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -1 +1,25 @@ """The cert_expiry component.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import callback +from homeassistant.helpers.typing import HomeAssistantType + + +async def async_setup(hass, config): + """Platform setup, do nothing.""" + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Load the saved entities.""" + + @callback + def async_start(_): + """Load the entry after the start event.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start) + + return True diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py new file mode 100644 index 00000000000000..dd3463fff95c4e --- /dev/null +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -0,0 +1,98 @@ +"""Config flow for the Cert Expiry platform.""" +import socket +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import slugify + +from .const import DOMAIN, DEFAULT_PORT, DEFAULT_NAME +from .helper import get_cert + + +@callback +def certexpiry_entries(hass: HomeAssistant): + """Return the host,port tuples for the domain.""" + return set( + (entry.data[CONF_HOST], entry.data[CONF_PORT]) + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self) -> None: + """Initialize the config flow.""" + self._errors = {} + + def _prt_in_configuration_exists(self, user_input) -> bool: + """Return True if host, port combination exists in configuration.""" + host = user_input[CONF_HOST] + port = user_input.get(CONF_PORT, DEFAULT_PORT) + if (host, port) in certexpiry_entries(self.hass): + return True + return False + + def _test_connection(self, user_input=None): + """Test connection to the server and try to get the certtificate.""" + try: + get_cert(user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT)) + return True + except socket.gaierror: + self._errors[CONF_HOST] = "resolve_failed" + except socket.timeout: + self._errors[CONF_HOST] = "connection_timeout" + except OSError: + self._errors[CONF_HOST] = "certificate_fetch_failed" + return False + + async def async_step_user(self, user_input=None): + """Step when user intializes a integration.""" + self._errors = {} + if user_input is not None: + # set some defaults in case we need to return to the form + if self._prt_in_configuration_exists(user_input): + self._errors[CONF_HOST] = "host_port_exists" + else: + if self._test_connection(user_input): + host = user_input[CONF_HOST] + name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) + prt = user_input.get(CONF_PORT, DEFAULT_PORT) + return self.async_create_entry( + title=name, data={CONF_HOST: host, CONF_PORT: prt} + ) + else: + user_input = {} + user_input[CONF_NAME] = DEFAULT_NAME + user_input[CONF_HOST] = "" + user_input[CONF_PORT] = DEFAULT_PORT + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): str, + vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str, + vol.Required( + CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT) + ): int, + } + ), + errors=self._errors, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry. + + Only host was required in the yaml file all other fields are optional + """ + if self._prt_in_configuration_exists(user_input): + return self.async_abort(reason="host_port_exists") + return await self.async_step_user(user_input) diff --git a/homeassistant/components/cert_expiry/const.py b/homeassistant/components/cert_expiry/const.py new file mode 100644 index 00000000000000..4129781f2a0fb2 --- /dev/null +++ b/homeassistant/components/cert_expiry/const.py @@ -0,0 +1,6 @@ +"""Const for Cert Expiry.""" + +DOMAIN = "cert_expiry" +DEFAULT_NAME = "SSL Certificate Expiry" +DEFAULT_PORT = 443 +TIMEOUT = 10.0 diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py new file mode 100644 index 00000000000000..9c10887293adbd --- /dev/null +++ b/homeassistant/components/cert_expiry/helper.py @@ -0,0 +1,15 @@ +"""Helper functions for the Cert Expiry platform.""" +import socket +import ssl + +from .const import TIMEOUT + + +def get_cert(host, port): + """Get the ssl certificate for the host and port combination.""" + ctx = ssl.create_default_context() + address = (host, port) + with socket.create_connection(address, timeout=TIMEOUT) as sock: + with ctx.wrap_socket(sock, server_hostname=address[0]) as ssock: + cert = ssock.getpeercert() + return cert diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 7ef2e0b7d105d6..781f27afb5f115 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -1,8 +1,9 @@ { - "domain": "cert_expiry", - "name": "Cert expiry", - "documentation": "https://www.home-assistant.io/components/cert_expiry", - "requirements": [], - "dependencies": [], - "codeowners": [] + "domain": "cert_expiry", + "name": "Cert expiry", + "documentation": "https://www.home-assistant.io/components/cert_expiry", + "requirements": [], + "config_flow": true, + "dependencies": [], + "codeowners": ["@cereal2nd"] } diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index b1e0d819358e0d..fccfb295c0fff2 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -7,24 +7,18 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, - CONF_HOST, - CONF_PORT, - EVENT_HOMEASSISTANT_START, -) +from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT from homeassistant.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT +from .helper import get_cert -DEFAULT_NAME = "SSL Certificate Expiry" -DEFAULT_PORT = 443 +_LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(hours=12) -TIMEOUT = 10.0 - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, @@ -34,22 +28,22 @@ ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up certificate expiry sensor.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + ) + ) - def run_setup(event): - """Wait until Home Assistant is fully initialized before creating. - Delay the setup until Home Assistant is fully initialized. - """ - server_name = config.get(CONF_HOST) - server_port = config.get(CONF_PORT) - sensor_name = config.get(CONF_NAME) - - add_entities([SSLCertificate(sensor_name, server_name, server_port)], True) - - # To allow checking of the HA certificate we must first be running. - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) +async def async_setup_entry(hass, entry, async_add_entities): + """Add cert-expiry entry.""" + async_add_entities( + [SSLCertificate(entry.title, entry.data[CONF_HOST], entry.data[CONF_PORT])], + True, + ) + return True class SSLCertificate(Entity): @@ -90,13 +84,8 @@ def available(self): def update(self): """Fetch the certificate information.""" - ctx = ssl.create_default_context() try: - address = (self.server_name, self.server_port) - with socket.create_connection(address, timeout=TIMEOUT) as sock: - with ctx.wrap_socket(sock, server_hostname=address[0]) as ssock: - cert = ssock.getpeercert() - + cert = get_cert(self.server_name, self.server_port) except socket.gaierror: _LOGGER.error("Cannot resolve hostname: %s", self.server_name) self._available = False diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json new file mode 100644 index 00000000000000..8943643e8b392e --- /dev/null +++ b/homeassistant/components/cert_expiry/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "title": "Certificate Expiry", + "step": { + "user": { + "title": "Define the certificate to test", + "data": { + "name": "The name of the certificate", + "host": "The hostname of the certificate", + "port": "The port of the certificate" + } + } + }, + "error": { + "host_port_exists": "This host and port combination is already configured", + "resolve_failed": "This host can not be resolved", + "connection_timeout": "Timeout whemn connecting to this host", + "certificate_fetch_failed": "Can not fetch certificate from this host and port combination" + }, + "abort": { + "host_port_exists": "This host and port combination is already configured" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index de665ecf5a6e44..082e0f853f87ce 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -10,6 +10,7 @@ "ambient_station", "axis", "cast", + "cert_expiry", "daikin", "deconz", "dialogflow", diff --git a/tests/components/cert_expiry/__init__.py b/tests/components/cert_expiry/__init__.py new file mode 100644 index 00000000000000..5ef5adee2e2291 --- /dev/null +++ b/tests/components/cert_expiry/__init__.py @@ -0,0 +1 @@ +"""Tests for the Cert Expiry component.""" diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py new file mode 100644 index 00000000000000..f8c99496a563eb --- /dev/null +++ b/tests/components/cert_expiry/test_config_flow.py @@ -0,0 +1,137 @@ +"""Tests for the Cert Expiry config flow.""" +import pytest +import socket +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components.cert_expiry import config_flow +from homeassistant.components.cert_expiry.const import DEFAULT_PORT +from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST + +from tests.common import MockConfigEntry + +NAME = "Cert Expiry test 1 2 3" +PORT = 443 +HOST = "example.com" + + +@pytest.fixture(name="test_connect") +def mock_controller(): + """Mock a successfull _prt_in_configuration_exists.""" + with patch( + "homeassistant.components.cert_expiry.config_flow.CertexpiryConfigFlow._test_connection", + return_value=True, + ): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.CertexpiryConfigFlow() + flow.hass = hass + return flow + + +async def test_user(hass, test_connect): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # tets with all provided + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "cert_expiry_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + +async def test_import(hass, test_connect): + """Test import step.""" + flow = init_config_flow(hass) + + # import with only host + result = await flow.async_step_import({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "ssl_certificate_expiry" + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == DEFAULT_PORT + + # import with host and name + result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "cert_expiry_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == DEFAULT_PORT + + # improt with host and port + result = await flow.async_step_import({CONF_HOST: HOST, CONF_PORT: PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "ssl_certificate_expiry" + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + # import with all + result = await flow.async_step_import( + {CONF_HOST: HOST, CONF_PORT: PORT, CONF_NAME: NAME} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "cert_expiry_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + +async def test_abort_if_already_setup(hass, test_connect): + """Test we abort if the cert is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain="cert_expiry", + data={CONF_PORT: DEFAULT_PORT, CONF_NAME: NAME, CONF_HOST: HOST}, + ).add_to_hass(hass) + + # Should fail, same HOST and PORT (default) + result = await flow.async_step_import( + {CONF_HOST: HOST, CONF_NAME: NAME, CONF_PORT: DEFAULT_PORT} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "host_port_exists" + + # Should be the same HOST and PORT (default) + result = await flow.async_step_user( + {CONF_HOST: HOST, CONF_NAME: NAME, CONF_PORT: DEFAULT_PORT} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "host_port_exists"} + + # SHOULD pass, same Host diff PORT + result = await flow.async_step_import( + {CONF_HOST: HOST, CONF_NAME: NAME, CONF_PORT: 888} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "cert_expiry_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == 888 + + +async def test_abort_on_socket_failed(hass): + """Test we abort of we have errors during socket creation.""" + flow = init_config_flow(hass) + + with patch("socket.create_connection", side_effect=socket.gaierror()): + result = await flow.async_step_user({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "resolve_failed"} + + with patch("socket.create_connection", side_effect=socket.timeout()): + result = await flow.async_step_user({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "connection_timeout"} + + with patch("socket.create_connection", side_effect=OSError()): + result = await flow.async_step_user({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "certificate_fetch_failed"} From e69953fe2de5d162e11d6f0c9abfae33a1382cda Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Aug 2019 12:45:13 -0700 Subject: [PATCH 0037/3953] 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 | 39 ++++++------------- .../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, 615 insertions(+), 70 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 d62402f2eee403..ed8e0c3a35800a 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 00000000000000..869cc46ea106de --- /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 00000000000000..3548361e396bb7 --- /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 eaac252d605da5..607454f4402f02 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 852965596e073f..4ac97068905560 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 1e9301a19da684..c5404a173f653b 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 020cd8f5946ed5..e85fceaf463f1e 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 00000000000000..91dafab0201643 --- /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 34da602a6cee52..dd8f1cc4026edb 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 4ae633ef16573d..1a5d992ef7b914 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 00000000000000..2f2eb6df214b21 --- /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 3395b31b4c79ed..ee222c83b5180d 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 58dbba34fa8384..a0a2d77d48c841 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 951a30f18260a4..3a297eb15ea3dd 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 12ed8cc457a5d4..66c02884a7e257 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 b058e94e25ad23..9ddf336c0605c6 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 00000000000000..648dbfe73f98e7 --- /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 8efc9101d9a175..48a2ff233da84d 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 00000000000000..16a1b19ff8e0ff --- /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 00000000000000..077956287b3e14 --- /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 00000000000000..b107e1bb696df0 --- /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 00000000000000..a93f9aac26fc74 --- /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 00000000000000..3f9bfab3304728 --- /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 95df1c991a20c2..cb4682fc937103 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 00000000000000..5cf8cbef17f004 --- /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 00000000000000..2bb7a1cca688ee --- /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 b1523da188ce88..15aabaa6308424 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 00000000000000..2393908e281130 --- /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 00000000000000..d744de150d28a8 --- /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 00000000000000..6505229355cf50 --- /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 00000000000000..12854e4ed619ff --- /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 61b66b794e18c4..d44142213bf066 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 417d0627616093..271e38147c3323 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 71cfab736cb6be..89823dd2055900 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 00000000000000..b3c82fdd8db90f --- /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 00000000000000..b96a358f2f03d7 --- /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 00000000000000..ad2f19b0668ce1 --- /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 00000000000000..b20317a236a71b --- /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 0c1fe674887d2e..c35de9c535c1d5 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 00000000000000..747074436d7af8 --- /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 00000000000000..33e8a88a9ae0cd --- /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 00000000000000..835929d26dfe44 --- /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 00000000000000..d63802984efb21 --- /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 00000000000000..680571040b1ecd --- /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 aa7eb5f29bc2f8..aac48ee4774bae 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 c20e3350272d8c..7436513ba6f84d 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 093ee552951786..0f7066df007be2 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 65df6a58b10799..09ad22740cde75 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 00000000000000..ff36d1ca66bf87 --- /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 6afb01169cbec0..ba873c4f029786 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 00000000000000..3a9041020764b6 --- /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 00000000000000..c91f9a78c292da --- /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 1d9fb41fc8ce11..2832804113a6ad 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 00000000000000..a8804835278828 --- /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 55016606e2dbe6..4b3e1ed52d44a1 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 00000000000000..b9b9cc6c0eba6d --- /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 a61a028f3968d6..e3fcfc89c5bd4b 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 c484bfbf09fa5d..3686148fdb6452 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -1,41 +1,26 @@ { "config": { - "title": "UniFi Controller", + "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" + }, "step": { "user": { - "title": "Set up UniFi Controller", "data": { "host": "Host", - "username": "User name", "password": "Password", "port": "Port", "site": "Site ID", + "username": "User name", "verify_ssl": "Controller using proper certificate" - } + }, + "title": "Set up UniFi Controller" } }, - "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": { - "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" - } - } - } + "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 00000000000000..94a064f34b4ff1 --- /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 00000000000000..941f72f2e7da78 --- /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 00000000000000..df390e73dd10da --- /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 00000000000000..389bfbd3cb1d48 --- /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 00000000000000..d185410a4ef3d7 --- /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 00000000000000..09ca1a0273f2ad --- /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 d233b485bd0d2a..704c7baeecb3c2 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 00000000000000..8a9f543be0a0b3 --- /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 2e246fb9931a7f..f2ca1a19aa49bf 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 907ffdb762fee4e34cec21cd54733f32c610882c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Aug 2019 12:45:13 -0700 Subject: [PATCH 0038/3953] 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 | 39 ++++++------------- .../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, 615 insertions(+), 70 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 d62402f2eee403..ed8e0c3a35800a 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 00000000000000..869cc46ea106de --- /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 00000000000000..3548361e396bb7 --- /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 eaac252d605da5..607454f4402f02 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 852965596e073f..4ac97068905560 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 1e9301a19da684..c5404a173f653b 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 020cd8f5946ed5..e85fceaf463f1e 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 00000000000000..91dafab0201643 --- /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 34da602a6cee52..dd8f1cc4026edb 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 4ae633ef16573d..1a5d992ef7b914 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 00000000000000..2f2eb6df214b21 --- /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 3395b31b4c79ed..ee222c83b5180d 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 58dbba34fa8384..a0a2d77d48c841 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 951a30f18260a4..3a297eb15ea3dd 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 12ed8cc457a5d4..66c02884a7e257 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 b058e94e25ad23..9ddf336c0605c6 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 00000000000000..648dbfe73f98e7 --- /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 8efc9101d9a175..48a2ff233da84d 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 00000000000000..16a1b19ff8e0ff --- /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 00000000000000..077956287b3e14 --- /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 00000000000000..b107e1bb696df0 --- /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 00000000000000..a93f9aac26fc74 --- /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 00000000000000..3f9bfab3304728 --- /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 95df1c991a20c2..cb4682fc937103 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 00000000000000..5cf8cbef17f004 --- /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 00000000000000..2bb7a1cca688ee --- /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 b1523da188ce88..15aabaa6308424 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 00000000000000..2393908e281130 --- /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 00000000000000..d744de150d28a8 --- /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 00000000000000..6505229355cf50 --- /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 00000000000000..12854e4ed619ff --- /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 61b66b794e18c4..d44142213bf066 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 417d0627616093..271e38147c3323 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 71cfab736cb6be..89823dd2055900 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 00000000000000..b3c82fdd8db90f --- /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 00000000000000..b96a358f2f03d7 --- /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 00000000000000..ad2f19b0668ce1 --- /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 00000000000000..b20317a236a71b --- /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 0c1fe674887d2e..c35de9c535c1d5 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 00000000000000..747074436d7af8 --- /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 00000000000000..33e8a88a9ae0cd --- /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 00000000000000..835929d26dfe44 --- /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 00000000000000..d63802984efb21 --- /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 00000000000000..680571040b1ecd --- /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 aa7eb5f29bc2f8..aac48ee4774bae 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 c20e3350272d8c..7436513ba6f84d 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 093ee552951786..0f7066df007be2 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 65df6a58b10799..09ad22740cde75 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 00000000000000..ff36d1ca66bf87 --- /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 6afb01169cbec0..ba873c4f029786 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 00000000000000..3a9041020764b6 --- /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 00000000000000..c91f9a78c292da --- /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 1d9fb41fc8ce11..2832804113a6ad 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 00000000000000..a8804835278828 --- /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 55016606e2dbe6..4b3e1ed52d44a1 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 00000000000000..b9b9cc6c0eba6d --- /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 a61a028f3968d6..e3fcfc89c5bd4b 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 c484bfbf09fa5d..3686148fdb6452 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -1,41 +1,26 @@ { "config": { - "title": "UniFi Controller", + "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" + }, "step": { "user": { - "title": "Set up UniFi Controller", "data": { "host": "Host", - "username": "User name", "password": "Password", "port": "Port", "site": "Site ID", + "username": "User name", "verify_ssl": "Controller using proper certificate" - } + }, + "title": "Set up UniFi Controller" } }, - "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": { - "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" - } - } - } + "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 00000000000000..94a064f34b4ff1 --- /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 00000000000000..941f72f2e7da78 --- /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 00000000000000..df390e73dd10da --- /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 00000000000000..389bfbd3cb1d48 --- /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 00000000000000..d185410a4ef3d7 --- /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 00000000000000..09ca1a0273f2ad --- /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 d233b485bd0d2a..704c7baeecb3c2 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 00000000000000..8a9f543be0a0b3 --- /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 2e246fb9931a7f..f2ca1a19aa49bf 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 cf3bb300e69b53ba7fddd9409a8811d28b10cc44 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Wed, 28 Aug 2019 22:38:20 +0200 Subject: [PATCH 0039/3953] Fix for 0.98: Don't update disabled entities (Homematic IP Cloud) (#26236) * Homematic IP Cloud Fix: Don't update disabled entities * Added enabled to entity.py * Update test for enabled * Update entity.py --- homeassistant/components/homematicip_cloud/device.py | 12 ++++++++++-- homeassistant/helpers/entity.py | 5 +++++ tests/helpers/test_entity.py | 2 ++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index b086eaa29c75f7..71855d7c3f5a4a 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -76,8 +76,16 @@ async def async_added_to_hass(self): def _async_device_changed(self, *args, **kwargs): """Handle device state changes.""" - _LOGGER.debug("Event %s (%s)", self.name, self._device.modelType) - self.async_schedule_update_ha_state() + # Don't update disabled entities + if self.enabled: + _LOGGER.debug("Event %s (%s)", self.name, self._device.modelType) + self.async_schedule_update_ha_state() + else: + _LOGGER.debug( + "Device Changed Event for %s (%s) not fired. Entity is disabled.", + self.name, + self._device.modelType, + ) @property def name(self) -> str: diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index dc2e46cc6b22fe..1aa405326e565b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -229,6 +229,11 @@ def entity_registry_enabled_default(self): # are used to perform a very specific function. Overwriting these may # produce undesirable effects in the entity's operation. + @property + def enabled(self): + """Return if the entity is enabled in the entity registry.""" + return self.registry_entry is None or not self.registry_entry.disabled + @callback def async_set_context(self, context): """Set the context the entity currently operates under.""" diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 3c89a5c65379d6..18cedf1c46ac5a 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -552,8 +552,10 @@ async def test_disabled_in_entity_registry(hass): await hass.async_block_till_done() assert entry2 != entry assert ent.registry_entry == entry2 + assert ent.enabled is True entry3 = registry.async_update_entity("hello.world", disabled_by="user") await hass.async_block_till_done() assert entry3 != entry2 assert ent.registry_entry == entry3 + assert ent.enabled is False From 1e61d50fc52d6467565dde34b8d44905204a9093 Mon Sep 17 00:00:00 2001 From: Florian Klien Date: Tue, 27 Aug 2019 07:34:58 +0200 Subject: [PATCH 0040/3953] 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 27b6a2da59ff66..153f6b5aea6972 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 42982b34134285..10f3cb190c6632 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 0041/3953] 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 caa46e10f99131..be4088a5c86592 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -14,8 +14,6 @@ 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 @@ def async_add_cover(lights): 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 @@ def __init__(self, device, gateway): @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 @@ async def async_set_cover_position(self, **kwargs): 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 @@ async def async_stop_cover(self, **kwargs): """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 f264877b77a5f8..7230ff4fb7bdff 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -16,7 +16,7 @@ "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 @@ "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 0042/3953] 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 b2692a37059be0..34aed146cf088b 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 0043/3953] 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 4d2998b85b8ff3..9a9b098aabb31d 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 d652bb23de13299cc27b4a01ad1a3454e6326a72 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Aug 2019 13:43:45 -0700 Subject: [PATCH 0044/3953] 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 78f87639a9929a..fa6145a7af21de 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 873a5aaf31deb9..a1ffd515c5bfa4 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 147cbb3a8a99a3..cb489a0aa68199 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 2799e78ce596a9..b1caf72deedd9c 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 From 69ddca6f68415ebaa32f3e2e89f9d84d1d05b5e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Aug 2019 13:43:45 -0700 Subject: [PATCH 0045/3953] 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 78f87639a9929a..fa6145a7af21de 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 873a5aaf31deb9..a1ffd515c5bfa4 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 10f3cb190c6632..e749340a8cb2c1 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 96b82caf968372..1aad0450390740 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 From ec3d83c0cc09009d6c98abfcf3d7269d3186abe1 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 29 Aug 2019 08:45:01 +0200 Subject: [PATCH 0046/3953] Velbus config entries remove decorator (#26256) --- homeassistant/components/velbus/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index a67f8429db9f08..e9cbe14ce25be6 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -18,8 +18,7 @@ def velbus_entries(hass: HomeAssistant): ) -@config_entries.HANDLERS.register(DOMAIN) -class VelbusConfigFlow(config_entries.ConfigFlow): +class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 From 789ad38c38917c8a9ec730038c8c2e8e6164d889 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 29 Aug 2019 03:03:03 -0700 Subject: [PATCH 0047/3953] Bump androidtv to 0.0.25 and add tests (#26202) * Add tests for androidtv * Test that the error and reconnection attempts are logged correctly. > "Handles device/service unavailable. Log a warning once when > unavailable, log once when reconnected." https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html * Clarify comment * Add test for when the ADB shell command returns None * Bump androidtv to 0.0.25 --- .coveragerc | 1 - .../components/androidtv/manifest.json | 2 +- .../components/androidtv/media_player.py | 20 +- requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/androidtv/__init__.py | 1 + .../components/androidtv/test_media_player.py | 232 ++++++++++++++++++ 8 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 tests/components/androidtv/__init__.py create mode 100644 tests/components/androidtv/test_media_player.py diff --git a/.coveragerc b/.coveragerc index 02d59b55f5f2a1..6b239402cb187f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -31,7 +31,6 @@ omit = homeassistant/components/amcrest/* homeassistant/components/ampio/* homeassistant/components/android_ip_webcam/* - homeassistant/components/androidtv/* homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py homeassistant/components/apache_kafka/* diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 047eaaaf5db9c1..91ea4019c05f74 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.24" + "androidtv==0.0.25" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index db4ff9e851ec82..2db210b56f3e52 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -431,8 +431,10 @@ def update(self): # Try to connect self._available = self.aftv.connect(always_log_errors=False) - # To be safe, wait until the next update to run ADB commands. - return + # To be safe, wait until the next update to run ADB commands if + # using the Python ADB implementation. + if not self.aftv.adb_server_ip: + return # If the ADB connection is not intact, don't update. if not self._available: @@ -443,7 +445,9 @@ def update(self): self.aftv.update() ) - self._state = ANDROIDTV_STATES[state] + self._state = ANDROIDTV_STATES.get(state) + if self._state is None: + self._available = False @property def is_volume_muted(self): @@ -506,8 +510,10 @@ def update(self): # Try to connect self._available = self.aftv.connect(always_log_errors=False) - # To be safe, wait until the next update to run ADB commands. - return + # To be safe, wait until the next update to run ADB commands if + # using the Python ADB implementation. + if not self.aftv.adb_server_ip: + return # If the ADB connection is not intact, don't update. if not self._available: @@ -518,7 +524,9 @@ def update(self): self._get_sources ) - self._state = ANDROIDTV_STATES[state] + self._state = ANDROIDTV_STATES.get(state) + if self._state is None: + self._available = False @property def source(self): diff --git a/requirements_all.txt b/requirements_all.txt index cb489a0aa68199..c241f5fd4a268b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.24 +androidtv==0.0.25 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b1caf72deedd9c..ed0689654a691c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,6 +78,9 @@ aiowwlln==1.0.0 # homeassistant.components.ambiclimate ambiclimate==0.2.1 +# homeassistant.components.androidtv +androidtv==0.0.25 + # homeassistant.components.apns apns2==0.3.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ce0aa6721351f5..6a181ab6b00a68 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -55,6 +55,7 @@ "aiounifi", "aioswitcher", "aiowwlln", + "androidtv", "apns2", "aprslib", "av", diff --git a/tests/components/androidtv/__init__.py b/tests/components/androidtv/__init__.py new file mode 100644 index 00000000000000..34e8c745fdca65 --- /dev/null +++ b/tests/components/androidtv/__init__.py @@ -0,0 +1 @@ +"""Tests for the androidtv component.""" diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py new file mode 100644 index 00000000000000..e787fddd3bcb04 --- /dev/null +++ b/tests/components/androidtv/test_media_player.py @@ -0,0 +1,232 @@ +"""The tests for the androidtv platform.""" +import logging +from socket import error as socket_error +import unittest +from unittest.mock import patch + +from homeassistant.components.androidtv.media_player import ( + AndroidTVDevice, + FireTVDevice, + setup, +) + + +def connect_device_success(self, *args, **kwargs): + """Return `self`, which will result in the ADB connection being interpreted as available.""" + return self + + +def connect_device_fail(self, *args, **kwargs): + """Raise a socket error.""" + raise socket_error + + +def adb_shell_python_adb_error(self, cmd): + """Raise an error that is among those caught for the Python ADB implementation.""" + raise AttributeError + + +def adb_shell_adb_server_error(self, cmd): + """Raise an error that is among those caught for the ADB server implementation.""" + raise ConnectionResetError + + +class AdbAvailable: + """A class that indicates the ADB connection is available.""" + + def shell(self, cmd): + """Send an ADB shell command (ADB server implementation).""" + return "" + + +class AdbUnavailable: + """A class with ADB shell methods that raise errors.""" + + def __bool__(self): + """Return `False` to indicate that the ADB connection is unavailable.""" + return False + + def shell(self, cmd): + """Raise an error that pertains to the Python ADB implementation.""" + raise ConnectionResetError + + +PATCH_PYTHON_ADB_CONNECT_SUCCESS = patch( + "adb.adb_commands.AdbCommands.ConnectDevice", connect_device_success +) +PATCH_PYTHON_ADB_COMMAND_SUCCESS = patch( + "adb.adb_commands.AdbCommands.Shell", return_value="" +) +PATCH_PYTHON_ADB_CONNECT_FAIL = patch( + "adb.adb_commands.AdbCommands.ConnectDevice", connect_device_fail +) +PATCH_PYTHON_ADB_COMMAND_FAIL = patch( + "adb.adb_commands.AdbCommands.Shell", adb_shell_python_adb_error +) +PATCH_PYTHON_ADB_COMMAND_NONE = patch( + "adb.adb_commands.AdbCommands.Shell", return_value=None +) + +PATCH_ADB_SERVER_CONNECT_SUCCESS = patch( + "adb_messenger.client.Client.device", return_value=AdbAvailable() +) +PATCH_ADB_SERVER_AVAILABLE = patch( + "androidtv.basetv.BaseTV.available", return_value=True +) +PATCH_ADB_SERVER_CONNECT_FAIL = patch( + "adb_messenger.client.Client.device", return_value=AdbUnavailable() +) +PATCH_ADB_SERVER_COMMAND_FAIL = patch( + "{}.AdbAvailable.shell".format(__name__), adb_shell_adb_server_error +) +PATCH_ADB_SERVER_COMMAND_NONE = patch( + "{}.AdbAvailable.shell".format(__name__), return_value=None +) + + +class TestAndroidTVPythonImplementation(unittest.TestCase): + """Test the androidtv media player for an Android TV device.""" + + def setUp(self): + """Set up an `AndroidTVDevice` media player.""" + with PATCH_PYTHON_ADB_CONNECT_SUCCESS, PATCH_PYTHON_ADB_COMMAND_SUCCESS: + aftv = setup("IP:PORT", device_class="androidtv") + self.aftv = AndroidTVDevice(aftv, "Fake Android TV", {}, None, None) + + def test_reconnect(self): + """Test that the error and reconnection attempts are logged correctly. + + "Handles device/service unavailable. Log a warning once when + unavailable, log once when reconnected." + + https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html + """ + with self.assertLogs(level=logging.WARNING) as logs: + with PATCH_PYTHON_ADB_CONNECT_FAIL, PATCH_PYTHON_ADB_COMMAND_FAIL: + for _ in range(5): + self.aftv.update() + self.assertFalse(self.aftv.available) + self.assertIsNone(self.aftv.state) + + assert len(logs.output) == 2 + assert logs.output[0].startswith("ERROR") + assert logs.output[1].startswith("WARNING") + + with self.assertLogs(level=logging.DEBUG) as logs: + with PATCH_PYTHON_ADB_CONNECT_SUCCESS, PATCH_PYTHON_ADB_COMMAND_SUCCESS: + # Update 1 will reconnect + self.aftv.update() + self.assertTrue(self.aftv.available) + + # Update 2 will update the state + self.aftv.update() + self.assertTrue(self.aftv.available) + self.assertIsNotNone(self.aftv.state) + + assert ( + "ADB connection to {} successfully established".format(self.aftv.aftv.host) + in logs.output[0] + ) + + def test_adb_shell_returns_none(self): + """Test the case that the ADB shell command returns `None`. + + The state should be `None` and the device should be unavailable. + """ + with PATCH_PYTHON_ADB_COMMAND_NONE: + self.aftv.update() + self.assertFalse(self.aftv.available) + self.assertIsNone(self.aftv.state) + + with PATCH_PYTHON_ADB_CONNECT_SUCCESS, PATCH_PYTHON_ADB_COMMAND_SUCCESS: + # Update 1 will reconnect + self.aftv.update() + self.assertTrue(self.aftv.available) + + # Update 2 will update the state + self.aftv.update() + self.assertTrue(self.aftv.available) + self.assertIsNotNone(self.aftv.state) + + +class TestAndroidTVServerImplementation(unittest.TestCase): + """Test the androidtv media player for an Android TV device.""" + + def setUp(self): + """Set up an `AndroidTVDevice` media player.""" + with PATCH_ADB_SERVER_CONNECT_SUCCESS, PATCH_ADB_SERVER_AVAILABLE: + aftv = setup( + "IP:PORT", adb_server_ip="ADB_SERVER_IP", device_class="androidtv" + ) + self.aftv = AndroidTVDevice(aftv, "Fake Android TV", {}, None, None) + + def test_reconnect(self): + """Test that the error and reconnection attempts are logged correctly. + + "Handles device/service unavailable. Log a warning once when + unavailable, log once when reconnected." + + https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html + """ + with self.assertLogs(level=logging.WARNING) as logs: + with PATCH_ADB_SERVER_CONNECT_FAIL, PATCH_ADB_SERVER_COMMAND_FAIL: + for _ in range(5): + self.aftv.update() + self.assertFalse(self.aftv.available) + self.assertIsNone(self.aftv.state) + + assert len(logs.output) == 2 + assert logs.output[0].startswith("ERROR") + assert logs.output[1].startswith("WARNING") + + with self.assertLogs(level=logging.DEBUG) as logs: + with PATCH_ADB_SERVER_CONNECT_SUCCESS: + self.aftv.update() + self.assertTrue(self.aftv.available) + self.assertIsNotNone(self.aftv.state) + + assert ( + "ADB connection to {} via ADB server {}:{} successfully established".format( + self.aftv.aftv.host, + self.aftv.aftv.adb_server_ip, + self.aftv.aftv.adb_server_port, + ) + in logs.output[0] + ) + + def test_adb_shell_returns_none(self): + """Test the case that the ADB shell command returns `None`. + + The state should be `None` and the device should be unavailable. + """ + with PATCH_ADB_SERVER_COMMAND_NONE: + self.aftv.update() + self.assertFalse(self.aftv.available) + self.assertIsNone(self.aftv.state) + + with PATCH_ADB_SERVER_CONNECT_SUCCESS: + self.aftv.update() + self.assertTrue(self.aftv.available) + self.assertIsNotNone(self.aftv.state) + + +class TestFireTVPythonImplementation(TestAndroidTVPythonImplementation): + """Test the androidtv media player for a Fire TV device.""" + + def setUp(self): + """Set up a `FireTVDevice` media player.""" + with PATCH_PYTHON_ADB_CONNECT_SUCCESS, PATCH_PYTHON_ADB_COMMAND_SUCCESS: + aftv = setup("IP:PORT", device_class="firetv") + self.aftv = FireTVDevice(aftv, "Fake Fire TV", {}, True, None, None) + + +class TestFireTVServerImplementation(TestAndroidTVServerImplementation): + """Test the androidtv media player for a Fire TV device.""" + + def setUp(self): + """Set up a `FireTVDevice` media player.""" + with PATCH_ADB_SERVER_CONNECT_SUCCESS, PATCH_ADB_SERVER_AVAILABLE: + aftv = setup( + "IP:PORT", adb_server_ip="ADB_SERVER_IP", device_class="firetv" + ) + self.aftv = FireTVDevice(aftv, "Fake Fire TV", {}, True, None, None) From 16fff16082b42f3b1d672c005737d137a8360478 Mon Sep 17 00:00:00 2001 From: StephenWetzel Date: Thu, 29 Aug 2019 11:56:12 -0400 Subject: [PATCH 0048/3953] =?UTF-8?q?Add=20two=20new=20methods=20to=20the?= =?UTF-8?q?=20OpenUV=20component=20that=20consume=20only=20a=20singl?= =?UTF-8?q?=E2=80=A6=20(#26207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add two new methods to the OpenUV component that consume only a single API call * Two lines after class * Rename methods to better reflect what they do, and DRY copy and pasted code * More error handling down into methods, run api calls in parallel * Fix import order * Add new methods to services.yaml, and update error messages --- homeassistant/components/openuv/__init__.py | 68 +++++++++++++------ homeassistant/components/openuv/services.yaml | 8 ++- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 339b8900049575..c1a8873b9e0ce1 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -1,5 +1,6 @@ """Support for UV data from openuv.io.""" import logging +import asyncio import voluptuous as vol @@ -198,18 +199,32 @@ async def async_setup_entry(hass, config_entry): @_verify_domain_control async def update_data(service): - """Refresh OpenUV data.""" - _LOGGER.debug("Refreshing OpenUV data") + """Refresh all OpenUV data.""" + _LOGGER.debug("Refreshing all OpenUV data") + await openuv.async_update() + async_dispatcher_send(hass, TOPIC_UPDATE) - try: - await openuv.async_update() - except OpenUvError as err: - _LOGGER.error("Error during data update: %s", err) - return + hass.services.async_register(DOMAIN, "update_data", update_data) + @_verify_domain_control + async def update_uv_index_data(service): + """Refresh OpenUV UV index data.""" + _LOGGER.debug("Refreshing OpenUV UV index data") + await openuv.async_update_uv_index_data() async_dispatcher_send(hass, TOPIC_UPDATE) - hass.services.async_register(DOMAIN, "update_data", update_data) + hass.services.async_register(DOMAIN, "update_uv_index_data", update_uv_index_data) + + @_verify_domain_control + async def update_protection_data(service): + """Refresh OpenUV protection window data.""" + _LOGGER.debug("Refreshing OpenUV protection window data") + await openuv.async_update_protection_data() + async_dispatcher_send(hass, TOPIC_UPDATE) + + hass.services.async_register( + DOMAIN, "update_protection_data", update_protection_data + ) return True @@ -234,21 +249,36 @@ def __init__(self, client, binary_sensor_conditions, sensor_conditions): self.data = {} self.sensor_conditions = sensor_conditions - async def async_update(self): - """Update sensor/binary sensor data.""" - if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: - resp = await self.client.uv_protection_window() - data = resp["result"] + async def async_update_protection_data(self): + """Update binary sensor (protection window) data.""" + from pyopenuv.errors import OpenUvError - if data.get("from_time") and data.get("to_time"): - self.data[DATA_PROTECTION_WINDOW] = data - else: - _LOGGER.debug("No valid protection window data for this location") + if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: + try: + resp = await self.client.uv_protection_window() + self.data[DATA_PROTECTION_WINDOW] = resp["result"] + except OpenUvError as err: + _LOGGER.error("Error during protection data update: %s", err) self.data[DATA_PROTECTION_WINDOW] = {} + return + + async def async_update_uv_index_data(self): + """Update sensor (uv index, etc) data.""" + from pyopenuv.errors import OpenUvError if any(c in self.sensor_conditions for c in SENSORS): - data = await self.client.uv_index() - self.data[DATA_UV] = data + try: + data = await self.client.uv_index() + self.data[DATA_UV] = data + except OpenUvError as err: + _LOGGER.error("Error during uv index data update: %s", err) + self.data[DATA_UV] = {} + return + + async def async_update(self): + """Update sensor/binary sensor data.""" + tasks = [self.async_update_protection_data(), self.async_update_uv_index_data()] + await asyncio.gather(*tasks) class OpenUvEntity(Entity): diff --git a/homeassistant/components/openuv/services.yaml b/homeassistant/components/openuv/services.yaml index f353c7f4774e2f..be9a7ba522f638 100644 --- a/homeassistant/components/openuv/services.yaml +++ b/homeassistant/components/openuv/services.yaml @@ -2,4 +2,10 @@ --- update_data: - description: Request new data from OpenUV. + description: Request new data from OpenUV. Consumes two API calls. + +update_uv_index_data: + description: Request new UV index data from OpenUV. + +update_protection_data: + description: Request new protection window data from OpenUV. From 955bed8df4dc856c7faa16961036454671972dea Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Thu, 29 Aug 2019 14:23:42 -0500 Subject: [PATCH 0049/3953] Clean up HEOS strings (#26242) * Clean up strings * Shorten lines to ~ 88 --- homeassistant/components/heos/__init__.py | 7 +++---- tests/components/heos/test_init.py | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 20ed7930a4fc68..f7e1ce5bc58628 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -87,9 +87,8 @@ async def disconnect_controller(event): favorites = await controller.get_favorites() else: _LOGGER.warning( - "%s is not logged in to a HEOS account and will be unable " - "to retrieve HEOS favorites: Use the 'heos.sign_in' service " - "to sign-in to a HEOS account", + "%s is not logged in to a HEOS account and will be unable to retrieve " + "HEOS favorites: Use the 'heos.sign_in' service to sign-in to a HEOS account", host, ) inputs = await controller.get_input_sources() @@ -312,7 +311,7 @@ async def get_sources(): if retry_attempts < self.max_retry_attempts: retry_attempts += 1 _LOGGER.debug( - "Error retrieving sources and will " "retry: %s", error + "Error retrieving sources and will retry: %s", error ) await asyncio.sleep(self.retry_delay) else: diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index 7b2645cb8ecb0a..cfbdcb9198abdb 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -108,9 +108,9 @@ async def test_async_setup_entry_not_signed_in_loads_platforms( assert hass.data[DOMAIN][DATA_SOURCE_MANAGER].favorites == {} assert hass.data[DOMAIN][DATA_SOURCE_MANAGER].inputs == input_sources assert ( - "127.0.0.1 is not logged in to a HEOS account and will be unable " - "to retrieve HEOS favorites: Use the 'heos.sign_in' service to " - "sign-in to a HEOS account" in caplog.text + "127.0.0.1 is not logged in to a HEOS account and will be unable to retrieve " + "HEOS favorites: Use the 'heos.sign_in' service to sign-in to a HEOS account" + in caplog.text ) From 36312bdef19ee1659d7e790dacfcb98d81e3237c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Aug 2019 12:32:15 -0700 Subject: [PATCH 0050/3953] Add translations --- .../components/adguard/.translations/fr.json | 11 ++++++++--- homeassistant/components/axis/.translations/fr.json | 1 + .../components/deconz/.translations/en.json | 11 +++++++++++ .../components/deconz/.translations/fr.json | 2 ++ .../components/life360/.translations/en.json | 1 + .../components/plaato/.translations/fr.json | 13 ++++++++++++- .../components/unifi/.translations/en.json | 12 ++++++++++++ homeassistant/components/wemo/.translations/fr.json | 5 +++++ 8 files changed, 52 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/adguard/.translations/fr.json b/homeassistant/components/adguard/.translations/fr.json index 7a58c39634519f..6543ddd50bc4ec 100644 --- a/homeassistant/components/adguard/.translations/fr.json +++ b/homeassistant/components/adguard/.translations/fr.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour." + "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour.", + "single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e." }, "error": { "connection_error": "\u00c9chec de connexion." }, "step": { "hassio_confirm": { + "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 AdGuard Home fourni par le module compl\u00e9mentaire Hass.io: {addon} ?", "title": "AdGuard Home via le module compl\u00e9mentaire Hass.io" }, "user": { @@ -16,8 +18,11 @@ "password": "Mot de passe", "port": "Port", "ssl": "AdGuard Home utilise un certificat SSL", - "username": "Nom d'utilisateur" - } + "username": "Nom d'utilisateur", + "verify_ssl": "AdGuard Home utilise un certificat appropri\u00e9" + }, + "description": "Configurez votre instance AdGuard Home pour permettre la surveillance et le contr\u00f4le.", + "title": "Liez votre AdGuard Home." } }, "title": "AdGuard Home" diff --git a/homeassistant/components/axis/.translations/fr.json b/homeassistant/components/axis/.translations/fr.json index e85fceaf463f1e..24afb4a226ce7e 100644 --- a/homeassistant/components/axis/.translations/fr.json +++ b/homeassistant/components/axis/.translations/fr.json @@ -8,6 +8,7 @@ }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration de l'appareil est d\u00e9j\u00e0 en cours.", "device_unavailable": "L'appareil n'est pas disponible", "faulty_credentials": "Mauvaises informations d'identification de l'utilisateur" }, diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index dd8f1cc4026edb..57da3c706a035c 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": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Allow deCONZ CLIP sensors", + "allow_deconz_groups": "Allow deCONZ light groups" + }, + "description": "Configure visibility of deCONZ device types" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 3d658ca00b0049..9b98914314a749 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration pour le pont est d\u00e9j\u00e0 en cours.", "no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert", + "not_deconz_bridge": "Pas un pont deCONZ", "one_instance_only": "Le composant prend uniquement en charge une instance deCONZ", "updated_instance": "Instance deCONZ mise \u00e0 jour avec la nouvelle adresse d'h\u00f4te" }, diff --git a/homeassistant/components/life360/.translations/en.json b/homeassistant/components/life360/.translations/en.json index 2c187ba0470730..e6017339b732bb 100644 --- a/homeassistant/components/life360/.translations/en.json +++ b/homeassistant/components/life360/.translations/en.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Invalid credentials", "invalid_username": "Invalid username", + "unexpected": "Unexpected error communicating with Life360 server", "user_already_configured": "Account has already been configured" }, "step": { diff --git a/homeassistant/components/plaato/.translations/fr.json b/homeassistant/components/plaato/.translations/fr.json index 091c680be4c62d..d710886b84b32b 100644 --- a/homeassistant/components/plaato/.translations/fr.json +++ b/homeassistant/components/plaato/.translations/fr.json @@ -1,7 +1,18 @@ { "config": { "abort": { + "not_internet_accessible": "Votre instance de Home Assistant doit \u00eatre accessible depuis Internet pour recevoir les messages de Plaato Airlock.", "one_instance_allowed": "Une seule instance est n\u00e9cessaire." - } + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Plaato Airlock. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails." + }, + "step": { + "user": { + "description": "\u00cates-vous s\u00fbr de vouloir installer le Plaato Airlock ?", + "title": "Configurer le Webhook Plaato" + } + }, + "title": "Plaato Airlock" } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 3686148fdb6452..2025bad6246f1f 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -22,5 +22,17 @@ } }, "title": "UniFi Controller" + }, + "options": { + "step": { + "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/wemo/.translations/fr.json b/homeassistant/components/wemo/.translations/fr.json index c1c8830cb25618..08b55e2366ad96 100644 --- a/homeassistant/components/wemo/.translations/fr.json +++ b/homeassistant/components/wemo/.translations/fr.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique Wemo trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Une seule configuration de Wemo est possible." + }, "step": { "confirm": { + "description": "Voulez-vous configurer Wemo?", "title": "Wemo" } }, From 04d2dbb57366148cbf855bd224b7df390bd8c515 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Aug 2019 12:32:15 -0700 Subject: [PATCH 0051/3953] Add translations --- .../components/adguard/.translations/fr.json | 11 ++++++++--- homeassistant/components/axis/.translations/fr.json | 1 + .../components/deconz/.translations/en.json | 11 +++++++++++ .../components/deconz/.translations/fr.json | 2 ++ .../components/life360/.translations/en.json | 1 + .../components/plaato/.translations/fr.json | 13 ++++++++++++- .../components/unifi/.translations/en.json | 12 ++++++++++++ homeassistant/components/wemo/.translations/fr.json | 5 +++++ 8 files changed, 52 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/adguard/.translations/fr.json b/homeassistant/components/adguard/.translations/fr.json index 7a58c39634519f..6543ddd50bc4ec 100644 --- a/homeassistant/components/adguard/.translations/fr.json +++ b/homeassistant/components/adguard/.translations/fr.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour." + "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour.", + "single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e." }, "error": { "connection_error": "\u00c9chec de connexion." }, "step": { "hassio_confirm": { + "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 AdGuard Home fourni par le module compl\u00e9mentaire Hass.io: {addon} ?", "title": "AdGuard Home via le module compl\u00e9mentaire Hass.io" }, "user": { @@ -16,8 +18,11 @@ "password": "Mot de passe", "port": "Port", "ssl": "AdGuard Home utilise un certificat SSL", - "username": "Nom d'utilisateur" - } + "username": "Nom d'utilisateur", + "verify_ssl": "AdGuard Home utilise un certificat appropri\u00e9" + }, + "description": "Configurez votre instance AdGuard Home pour permettre la surveillance et le contr\u00f4le.", + "title": "Liez votre AdGuard Home." } }, "title": "AdGuard Home" diff --git a/homeassistant/components/axis/.translations/fr.json b/homeassistant/components/axis/.translations/fr.json index e85fceaf463f1e..24afb4a226ce7e 100644 --- a/homeassistant/components/axis/.translations/fr.json +++ b/homeassistant/components/axis/.translations/fr.json @@ -8,6 +8,7 @@ }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration de l'appareil est d\u00e9j\u00e0 en cours.", "device_unavailable": "L'appareil n'est pas disponible", "faulty_credentials": "Mauvaises informations d'identification de l'utilisateur" }, diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index dd8f1cc4026edb..57da3c706a035c 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": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Allow deCONZ CLIP sensors", + "allow_deconz_groups": "Allow deCONZ light groups" + }, + "description": "Configure visibility of deCONZ device types" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 3d658ca00b0049..9b98914314a749 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration pour le pont est d\u00e9j\u00e0 en cours.", "no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert", + "not_deconz_bridge": "Pas un pont deCONZ", "one_instance_only": "Le composant prend uniquement en charge une instance deCONZ", "updated_instance": "Instance deCONZ mise \u00e0 jour avec la nouvelle adresse d'h\u00f4te" }, diff --git a/homeassistant/components/life360/.translations/en.json b/homeassistant/components/life360/.translations/en.json index 2c187ba0470730..e6017339b732bb 100644 --- a/homeassistant/components/life360/.translations/en.json +++ b/homeassistant/components/life360/.translations/en.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Invalid credentials", "invalid_username": "Invalid username", + "unexpected": "Unexpected error communicating with Life360 server", "user_already_configured": "Account has already been configured" }, "step": { diff --git a/homeassistant/components/plaato/.translations/fr.json b/homeassistant/components/plaato/.translations/fr.json index 091c680be4c62d..d710886b84b32b 100644 --- a/homeassistant/components/plaato/.translations/fr.json +++ b/homeassistant/components/plaato/.translations/fr.json @@ -1,7 +1,18 @@ { "config": { "abort": { + "not_internet_accessible": "Votre instance de Home Assistant doit \u00eatre accessible depuis Internet pour recevoir les messages de Plaato Airlock.", "one_instance_allowed": "Une seule instance est n\u00e9cessaire." - } + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Plaato Airlock. \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails." + }, + "step": { + "user": { + "description": "\u00cates-vous s\u00fbr de vouloir installer le Plaato Airlock ?", + "title": "Configurer le Webhook Plaato" + } + }, + "title": "Plaato Airlock" } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 3686148fdb6452..2025bad6246f1f 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -22,5 +22,17 @@ } }, "title": "UniFi Controller" + }, + "options": { + "step": { + "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/wemo/.translations/fr.json b/homeassistant/components/wemo/.translations/fr.json index c1c8830cb25618..08b55e2366ad96 100644 --- a/homeassistant/components/wemo/.translations/fr.json +++ b/homeassistant/components/wemo/.translations/fr.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique Wemo trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Une seule configuration de Wemo est possible." + }, "step": { "confirm": { + "description": "Voulez-vous configurer Wemo?", "title": "Wemo" } }, From 24a4a42664e1b202c96620f90c5bb43851edc175 Mon Sep 17 00:00:00 2001 From: Eliseo Martelli Date: Thu, 29 Aug 2019 21:36:21 +0200 Subject: [PATCH 0052/3953] Update sensor.py (#26209) --- homeassistant/components/qbittorrent/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index a257b0e317a5da..2900496a01e408 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -41,7 +41,7 @@ ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the qBittorrent sensors.""" from qbittorrent.client import Client, LoginRequired @@ -62,7 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sensor = QBittorrentSensor(sensor_type, client, name, LoginRequired) dev.append(sensor) - async_add_entities(dev, True) + add_entities(dev, True) def format_speed(speed): @@ -105,7 +105,7 @@ def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - async def async_update(self): + def update(self): """Get the latest data from qBittorrent and updates the state.""" try: data = self.client.sync() @@ -113,7 +113,6 @@ async def async_update(self): except RequestException: _LOGGER.error("Connection lost") self._available = False - return except self._exception: _LOGGER.error("Invalid authentication") return From 25961df548a8588e9586e614f65220ef7b3c5f7a Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 29 Aug 2019 15:44:53 -0400 Subject: [PATCH 0053/3953] Fix ZHA state restore by always restoring last seen on devices (#26271) * fix state restore by always restoring last seen * cleanup --- homeassistant/components/zha/core/gateway.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 9cf93b565812dd..3d8c3e8fd9086b 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -304,6 +304,8 @@ def _async_get_or_create_device(self, zigpy_device): manufacturer=zha_device.manufacturer, model=zha_device.model, ) + entry = self.zha_storage.async_get_or_create(zha_device) + zha_device.async_update_last_seen(entry.last_seen) return zha_device @callback @@ -356,10 +358,6 @@ async def async_device_initialized(self, device): ) 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 ) From 5f850a7dc7b7452640d8d2d6ccb6f9377b18398b Mon Sep 17 00:00:00 2001 From: Eliseo Martelli Date: Thu, 29 Aug 2019 21:36:21 +0200 Subject: [PATCH 0054/3953] Update sensor.py (#26209) --- homeassistant/components/qbittorrent/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index a257b0e317a5da..2900496a01e408 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -41,7 +41,7 @@ ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the qBittorrent sensors.""" from qbittorrent.client import Client, LoginRequired @@ -62,7 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sensor = QBittorrentSensor(sensor_type, client, name, LoginRequired) dev.append(sensor) - async_add_entities(dev, True) + add_entities(dev, True) def format_speed(speed): @@ -105,7 +105,7 @@ def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - async def async_update(self): + def update(self): """Get the latest data from qBittorrent and updates the state.""" try: data = self.client.sync() @@ -113,7 +113,6 @@ async def async_update(self): except RequestException: _LOGGER.error("Connection lost") self._available = False - return except self._exception: _LOGGER.error("Invalid authentication") return From 069e762da0c81f3f7097156314968015f57300c2 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Wed, 28 Aug 2019 22:38:20 +0200 Subject: [PATCH 0055/3953] Fix for 0.98: Don't update disabled entities (Homematic IP Cloud) (#26236) * Homematic IP Cloud Fix: Don't update disabled entities * Added enabled to entity.py * Update test for enabled * Update entity.py --- homeassistant/components/homematicip_cloud/device.py | 12 ++++++++++-- homeassistant/helpers/entity.py | 5 +++++ tests/helpers/test_entity.py | 2 ++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index b086eaa29c75f7..71855d7c3f5a4a 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -76,8 +76,16 @@ async def async_added_to_hass(self): def _async_device_changed(self, *args, **kwargs): """Handle device state changes.""" - _LOGGER.debug("Event %s (%s)", self.name, self._device.modelType) - self.async_schedule_update_ha_state() + # Don't update disabled entities + if self.enabled: + _LOGGER.debug("Event %s (%s)", self.name, self._device.modelType) + self.async_schedule_update_ha_state() + else: + _LOGGER.debug( + "Device Changed Event for %s (%s) not fired. Entity is disabled.", + self.name, + self._device.modelType, + ) @property def name(self) -> str: diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index bd96e1bafdb5f4..91562b9046da7c 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -229,6 +229,11 @@ def entity_registry_enabled_default(self): # are used to perform a very specific function. Overwriting these may # produce undesirable effects in the entity's operation. + @property + def enabled(self): + """Return if the entity is enabled in the entity registry.""" + return self.registry_entry is None or not self.registry_entry.disabled + @callback def async_set_context(self, context): """Set the context the entity currently operates under.""" diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 3c89a5c65379d6..18cedf1c46ac5a 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -552,8 +552,10 @@ async def test_disabled_in_entity_registry(hass): await hass.async_block_till_done() assert entry2 != entry assert ent.registry_entry == entry2 + assert ent.enabled is True entry3 = registry.async_update_entity("hello.world", disabled_by="user") await hass.async_block_till_done() assert entry3 != entry2 assert ent.registry_entry == entry3 + assert ent.enabled is False From bb52e1736499fc995a5439fdd2b2a4d52511d261 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 29 Aug 2019 15:44:53 -0400 Subject: [PATCH 0056/3953] Fix ZHA state restore by always restoring last seen on devices (#26271) * fix state restore by always restoring last seen * cleanup --- homeassistant/components/zha/core/gateway.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 9cf93b565812dd..3d8c3e8fd9086b 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -304,6 +304,8 @@ def _async_get_or_create_device(self, zigpy_device): manufacturer=zha_device.manufacturer, model=zha_device.model, ) + entry = self.zha_storage.async_get_or_create(zha_device) + zha_device.async_update_last_seen(entry.last_seen) return zha_device @callback @@ -356,10 +358,6 @@ async def async_device_initialized(self, device): ) 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 ) From 5676f6fb86abbbaa6d34784cede1bc8b618a6d24 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Aug 2019 13:06:34 -0700 Subject: [PATCH 0057/3953] Bumped version to 0.98.1 --- homeassistant/components/tuya/switch.py | 7 ++++++- homeassistant/const.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 9c021766637507..a0d262aa08517e 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -26,11 +26,12 @@ def __init__(self, tuya): """Init Tuya switch device.""" super().__init__(tuya) self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) + self._is_on = False @property def is_on(self): """Return true if switch is on.""" - return self.tuya.state() + return self._is_on def turn_on(self, **kwargs): """Turn the switch on.""" @@ -39,3 +40,7 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn the device off.""" self.tuya.turn_off() + + def update(self): + """Update switch device.""" + self._is_on = self.tuya.state() diff --git a/homeassistant/const.py b/homeassistant/const.py index 9a9b098aabb31d..2f2546378dbb5e 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" +PATCH_VERSION = "1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 66b905776bbfd0082dc424afdbfe7b1db6a50cb5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Aug 2019 13:22:29 -0700 Subject: [PATCH 0058/3953] Fix partly cloudy (#26277) --- homeassistant/components/buienradar/sensor.py | 10 +++++----- homeassistant/components/mysensors/sensor.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 72da5164daba56..841cc428bac37a 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -108,11 +108,11 @@ "rainchance_3d": ["Rainchance 3d", "%", "mdi:weather-pouring"], "rainchance_4d": ["Rainchance 4d", "%", "mdi:weather-pouring"], "rainchance_5d": ["Rainchance 5d", "%", "mdi:weather-pouring"], - "sunchance_1d": ["Sunchance 1d", "%", "mdi:weather-partlycloudy"], - "sunchance_2d": ["Sunchance 2d", "%", "mdi:weather-partlycloudy"], - "sunchance_3d": ["Sunchance 3d", "%", "mdi:weather-partlycloudy"], - "sunchance_4d": ["Sunchance 4d", "%", "mdi:weather-partlycloudy"], - "sunchance_5d": ["Sunchance 5d", "%", "mdi:weather-partlycloudy"], + "sunchance_1d": ["Sunchance 1d", "%", "mdi:weather-partly-cloudy"], + "sunchance_2d": ["Sunchance 2d", "%", "mdi:weather-partly-cloudy"], + "sunchance_3d": ["Sunchance 3d", "%", "mdi:weather-partly-cloudy"], + "sunchance_4d": ["Sunchance 4d", "%", "mdi:weather-partly-cloudy"], + "sunchance_5d": ["Sunchance 5d", "%", "mdi:weather-partly-cloudy"], "windforce_1d": ["Wind force 1d", "Bft", "mdi:weather-windy"], "windforce_2d": ["Wind force 2d", "Bft", "mdi:weather-windy"], "windforce_3d": ["Wind force 3d", "Bft", "mdi:weather-windy"], diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 8eaf336e9ba825..a7d1cad98fa9b9 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -14,7 +14,7 @@ "V_DIMMER": ["%", "mdi:percent"], "V_PERCENTAGE": ["%", "mdi:percent"], "V_PRESSURE": [None, "mdi:gauge"], - "V_FORECAST": [None, "mdi:weather-partlycloudy"], + "V_FORECAST": [None, "mdi:weather-partly-cloudy"], "V_RAIN": [None, "mdi:weather-rainy"], "V_RAINRATE": [None, "mdi:weather-rainy"], "V_WIND": [None, "mdi:weather-windy"], From 6a02fd51b860f753c029cb094ad43e2874753fca Mon Sep 17 00:00:00 2001 From: mbo18 Date: Thu, 29 Aug 2019 22:22:52 +0200 Subject: [PATCH 0059/3953] Fix missing DarkSky mdi icon (#26274) * Fix missing DarkSky mdi icon Fix mdi icon for DarkSky * fix icon * Update weather.py --- homeassistant/components/darksky/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index e5886ae5e59cc0..0f33935c66c919 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -193,7 +193,7 @@ "%", "%", "%", - "mdi:weather-partlycloudy", + "mdi:weather-partly-cloudy", ["currently", "hourly", "daily"], ], "humidity": [ @@ -380,11 +380,11 @@ "cloudy": ["/static/images/darksky/weather-cloudy.svg", "mdi:weather-cloudy"], "partly-cloudy-day": [ "/static/images/darksky/weather-partlycloudy.svg", - "mdi:weather-partlycloudy", + "mdi:weather-partly-cloudy", ], "partly-cloudy-night": [ "/static/images/darksky/weather-cloudy.svg", - "mdi:weather-partlycloudy", + "mdi:weather-partly-cloudy", ], } From 015adbbac0539cdcd8ceb7c7d6bbf838c1b0a2cf Mon Sep 17 00:00:00 2001 From: mbo18 Date: Thu, 29 Aug 2019 22:22:52 +0200 Subject: [PATCH 0060/3953] Fix missing DarkSky mdi icon (#26274) * Fix missing DarkSky mdi icon Fix mdi icon for DarkSky * fix icon * Update weather.py --- homeassistant/components/darksky/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index e5886ae5e59cc0..0f33935c66c919 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -193,7 +193,7 @@ "%", "%", "%", - "mdi:weather-partlycloudy", + "mdi:weather-partly-cloudy", ["currently", "hourly", "daily"], ], "humidity": [ @@ -380,11 +380,11 @@ "cloudy": ["/static/images/darksky/weather-cloudy.svg", "mdi:weather-cloudy"], "partly-cloudy-day": [ "/static/images/darksky/weather-partlycloudy.svg", - "mdi:weather-partlycloudy", + "mdi:weather-partly-cloudy", ], "partly-cloudy-night": [ "/static/images/darksky/weather-cloudy.svg", - "mdi:weather-partlycloudy", + "mdi:weather-partly-cloudy", ], } From 5413cbd19572a5b1c0bf7cc124c23de6c50d5471 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Aug 2019 13:22:29 -0700 Subject: [PATCH 0061/3953] Fix partly cloudy (#26277) --- homeassistant/components/buienradar/sensor.py | 10 +++++----- homeassistant/components/mysensors/sensor.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 72da5164daba56..841cc428bac37a 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -108,11 +108,11 @@ "rainchance_3d": ["Rainchance 3d", "%", "mdi:weather-pouring"], "rainchance_4d": ["Rainchance 4d", "%", "mdi:weather-pouring"], "rainchance_5d": ["Rainchance 5d", "%", "mdi:weather-pouring"], - "sunchance_1d": ["Sunchance 1d", "%", "mdi:weather-partlycloudy"], - "sunchance_2d": ["Sunchance 2d", "%", "mdi:weather-partlycloudy"], - "sunchance_3d": ["Sunchance 3d", "%", "mdi:weather-partlycloudy"], - "sunchance_4d": ["Sunchance 4d", "%", "mdi:weather-partlycloudy"], - "sunchance_5d": ["Sunchance 5d", "%", "mdi:weather-partlycloudy"], + "sunchance_1d": ["Sunchance 1d", "%", "mdi:weather-partly-cloudy"], + "sunchance_2d": ["Sunchance 2d", "%", "mdi:weather-partly-cloudy"], + "sunchance_3d": ["Sunchance 3d", "%", "mdi:weather-partly-cloudy"], + "sunchance_4d": ["Sunchance 4d", "%", "mdi:weather-partly-cloudy"], + "sunchance_5d": ["Sunchance 5d", "%", "mdi:weather-partly-cloudy"], "windforce_1d": ["Wind force 1d", "Bft", "mdi:weather-windy"], "windforce_2d": ["Wind force 2d", "Bft", "mdi:weather-windy"], "windforce_3d": ["Wind force 3d", "Bft", "mdi:weather-windy"], diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 8eaf336e9ba825..a7d1cad98fa9b9 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -14,7 +14,7 @@ "V_DIMMER": ["%", "mdi:percent"], "V_PERCENTAGE": ["%", "mdi:percent"], "V_PRESSURE": [None, "mdi:gauge"], - "V_FORECAST": [None, "mdi:weather-partlycloudy"], + "V_FORECAST": [None, "mdi:weather-partly-cloudy"], "V_RAIN": [None, "mdi:weather-rainy"], "V_RAINRATE": [None, "mdi:weather-rainy"], "V_WIND": [None, "mdi:weather-windy"], From 0d7326168e3df031de0c77a9eefd68489d7d4eca Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 29 Aug 2019 23:04:01 +0200 Subject: [PATCH 0062/3953] UniFi - dont schedule updates on disabled entities (#26278) * Dont schedule updates on disabled entities * Use entity enabled since it is available --- homeassistant/components/unifi/device_tracker.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index c8024808e390e7..4845e9222ce551 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -150,7 +150,9 @@ def update_items(controller, async_add_entities, tracked): for client_id in controller.api.clients: - if client_id in tracked and tracked[client_id].entity_id: + if client_id in tracked: + if not tracked[client_id].enabled: + continue LOGGER.debug( "Updating UniFi tracked client %s (%s)", tracked[client_id].entity_id, @@ -183,7 +185,9 @@ def update_items(controller, async_add_entities, tracked): for device_id in controller.api.devices: - if device_id in tracked and tracked[device_id].entity_id: + if device_id in tracked: + if not tracked[device_id].enabled: + continue LOGGER.debug( "Updating UniFi tracked device %s (%s)", tracked[device_id].entity_id, From 1ca2f1906af7095a1f656761391187acb61130ee Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 29 Aug 2019 23:04:01 +0200 Subject: [PATCH 0063/3953] UniFi - dont schedule updates on disabled entities (#26278) * Dont schedule updates on disabled entities * Use entity enabled since it is available --- homeassistant/components/unifi/device_tracker.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index c8024808e390e7..4845e9222ce551 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -150,7 +150,9 @@ def update_items(controller, async_add_entities, tracked): for client_id in controller.api.clients: - if client_id in tracked and tracked[client_id].entity_id: + if client_id in tracked: + if not tracked[client_id].enabled: + continue LOGGER.debug( "Updating UniFi tracked client %s (%s)", tracked[client_id].entity_id, @@ -183,7 +185,9 @@ def update_items(controller, async_add_entities, tracked): for device_id in controller.api.devices: - if device_id in tracked and tracked[device_id].entity_id: + if device_id in tracked: + if not tracked[device_id].enabled: + continue LOGGER.debug( "Updating UniFi tracked device %s (%s)", tracked[device_id].entity_id, From 62338dd28ec2f328892353eed234f45411e99ffc Mon Sep 17 00:00:00 2001 From: 5mauggy <28944741+5mauggy@users.noreply.github.com> Date: Fri, 30 Aug 2019 14:28:39 +0200 Subject: [PATCH 0064/3953] Fix deConz thermostat integration (#26267) * Fixed logger name to allow selective logging * Fixed thermostat mode ('off' and 'heat' modes were not consistent with Eurotronic Spirit Zigbee Thermostat state) and added 'auto' to supported mode * Added required blank lines in code * Black formatting * Revert logging code added to each files. Instead, only replaced "." by __package__ in const.py * Added a test on self._device.state_on to determine hvac_mode * Black formatting * Added debug message when unsupported hvac_mode is encountered * Applied formatting recommandations * Updated tests for 'auto' hvac_mode --- homeassistant/components/deconz/climate.py | 11 ++++++++--- homeassistant/components/deconz/const.py | 2 +- tests/components/deconz/test_climate.py | 18 ++++++++++++++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index d20833d5a82400..a72c29019598e7 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -3,6 +3,7 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, @@ -15,7 +16,7 @@ from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF] +SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -68,7 +69,9 @@ def hvac_mode(self): Need to be one of HVAC_MODE_*. """ - if self._device.on: + if self._device.mode in SUPPORT_HVAC: + return self._device.mode + if self._device.state_on: return HVAC_MODE_HEAT return HVAC_MODE_OFF @@ -101,8 +104,10 @@ async def async_set_temperature(self, **kwargs): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" - if hvac_mode == HVAC_MODE_HEAT: + if hvac_mode == HVAC_MODE_AUTO: data = {"mode": "auto"} + elif hvac_mode == HVAC_MODE_HEAT: + data = {"mode": "heat"} elif hvac_mode == HVAC_MODE_OFF: data = {"mode": "off"} diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index ba6172120ecabb..ef152aa2b708f0 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -1,7 +1,7 @@ """Constants for the deCONZ component.""" import logging -_LOGGER = logging.getLogger(".") +_LOGGER = logging.getLogger(__package__) DOMAIN = "deconz" diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index f4972564a8ea60..1547f58a12b1bc 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -118,13 +118,23 @@ async def test_climate_devices(hass): await hass.services.async_call( "climate", "set_hvac_mode", - {"entity_id": "climate.climate_1_name", "hvac_mode": "heat"}, + {"entity_id": "climate.climate_1_name", "hvac_mode": "auto"}, blocking=True, ) gateway.api.session.put.assert_called_with( "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "auto"}' ) + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": "climate.climate_1_name", "hvac_mode": "heat"}, + blocking=True, + ) + gateway.api.session.put.assert_called_with( + "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "heat"}' + ) + await hass.services.async_call( "climate", "set_hvac_mode", @@ -145,7 +155,7 @@ async def test_climate_devices(hass): "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"heatsetpoint": 2000.0}' ) - assert len(gateway.api.session.put.mock_calls) == 3 + assert len(gateway.api.session.put.mock_calls) == 4 async def test_verify_state_update(hass): @@ -154,7 +164,7 @@ async def test_verify_state_update(hass): assert "climate.climate_1_name" in gateway.deconz_ids thermostat = hass.states.get("climate.climate_1_name") - assert thermostat.state == "off" + assert thermostat.state == "auto" state_update = { "t": "event", @@ -169,7 +179,7 @@ async def test_verify_state_update(hass): assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.climate_1_name") - assert thermostat.state == "off" + assert thermostat.state == "auto" assert gateway.api.sensors["1"].changed_keys == {"state", "r", "t", "on", "e", "id"} From 4d08e73e3eb5db0391ca86488648b8acce95521d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 30 Aug 2019 14:48:08 +0200 Subject: [PATCH 0065/3953] Enable py_noaa --- azure-pipelines-wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 4e2ecd73d2137f..362789ba72f449 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -65,5 +65,6 @@ jobs: 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} + sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} done displayName: 'Prepare requirements files for Hass.io' From b074337b9cc1b61e6f16ef9a6feca337f758fa00 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 30 Aug 2019 15:50:49 +0200 Subject: [PATCH 0066/3953] Update azure-pipelines-wheels.yml --- 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 362789ba72f449..24440ea7210da5 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -18,7 +18,7 @@ schedules: always: true variables: - name: versionWheels - value: '1.1-3.7-alpine3.10' + value: '1.3-3.7-alpine3.10' resources: repositories: - repository: azure From 299695ca24917f81eb68b37a0400d00a404025cb Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 30 Aug 2019 15:56:52 +0200 Subject: [PATCH 0067/3953] Update azure-pipelines-wheels.yml --- azure-pipelines-wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 24440ea7210da5..2a557d87ee7985 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -31,6 +31,7 @@ jobs: 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' + builderPip: 'Cython;numpy' wheelsRequirement: 'requirements_wheels.txt' wheelsRequirementDiff: 'requirements_diff.txt' preBuild: From 2f6bdc864375294cb42d17f008aca7483294378d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Aug 2019 16:41:07 -0700 Subject: [PATCH 0068/3953] Remove deprecated SMA config (#26306) --- homeassistant/components/sma/sensor.py | 25 ------------------------- tests/components/sma/test_sensor.py | 18 ------------------ 2 files changed, 43 deletions(-) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 34aed146cf088b..5b6bc57be9d6dc 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -19,7 +19,6 @@ 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__) @@ -31,7 +30,6 @@ CONF_UNIT = "unit" GROUPS = ["user", "installer"] -OLD_CONFIG_DEPRECATED = MAJOR_VERSION > 0 or MINOR_VERSION > 98 def _check_sensor_schema(conf): @@ -45,29 +43,6 @@ def _check_sensor_schema(conf): customs = list(conf[CONF_CUSTOM].keys()) - 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( diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index bee1743791c597..3caf889c522265 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -15,24 +15,6 @@ } -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"] From f01e106e6d8f3f7586edab0f955eb6710078d9fd Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Sat, 31 Aug 2019 03:30:18 +0300 Subject: [PATCH 0069/3953] bump tuyaha 0.0.4 (#26303) --- 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 8d47d8a0173bad..9c83056f6aca81 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.3" + "tuyaha==0.0.4" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index c241f5fd4a268b..d1eddd292063a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1860,7 +1860,7 @@ tplink==0.2.1 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.3 +tuyaha==0.0.4 # homeassistant.components.twentemilieu twentemilieu==0.1.0 From 37a3d5fd8568502c9b592a80a8f68a399257a893 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Aug 2019 18:32:38 -0700 Subject: [PATCH 0070/3953] Add HEAD and PUT support to webhooks (#26299) --- homeassistant/components/webhook/__init__.py | 6 ++- tests/components/webhook/test_init.py | 39 ++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 38540bbd307399..a12e55c771a61e 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -99,11 +99,15 @@ class WebhookView(HomeAssistantView): name = "api:webhook" requires_auth = False - async def post(self, request, webhook_id): + async def _handle(self, request, webhook_id): """Handle webhook call.""" hass = request.app["hass"] return await async_handle_webhook(hass, webhook_id, request) + head = _handle + post = _handle + put = _handle + @callback def websocket_list(hass, connection, msg): diff --git a/tests/components/webhook/test_init.py b/tests/components/webhook/test_init.py index 307437f8c056d2..5f07ca9abc9db6 100644 --- a/tests/components/webhook/test_init.py +++ b/tests/components/webhook/test_init.py @@ -99,9 +99,48 @@ async def handle(*args): assert len(hooks) == 1 assert hooks[0][0] is hass assert hooks[0][1] == webhook_id + assert hooks[0][2].method == "POST" assert await hooks[0][2].text() == "" +async def test_webhook_put(hass, mock_client): + """Test sending a put request to a webhook.""" + hooks = [] + webhook_id = hass.components.webhook.async_generate_id() + + async def handle(*args): + """Handle webhook.""" + hooks.append(args) + + hass.components.webhook.async_register("test", "Test hook", webhook_id, handle) + + resp = await mock_client.put("/api/webhook/{}".format(webhook_id)) + assert resp.status == 200 + assert len(hooks) == 1 + assert hooks[0][0] is hass + assert hooks[0][1] == webhook_id + assert hooks[0][2].method == "PUT" + + +async def test_webhook_head(hass, mock_client): + """Test sending a head request to a webhook.""" + hooks = [] + webhook_id = hass.components.webhook.async_generate_id() + + async def handle(*args): + """Handle webhook.""" + hooks.append(args) + + hass.components.webhook.async_register("test", "Test hook", webhook_id, handle) + + resp = await mock_client.head("/api/webhook/{}".format(webhook_id)) + assert resp.status == 200 + assert len(hooks) == 1 + assert hooks[0][0] is hass + assert hooks[0][1] == webhook_id + assert hooks[0][2].method == "HEAD" + + async def test_listing_webhook(hass, hass_ws_client, hass_access_token): """Test unregistering a webhook.""" assert await async_setup_component(hass, "webhook", {}) From 7b05ede2972abef57f0b442a6fce95c08b8bf00a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Aug 2019 18:34:40 -0700 Subject: [PATCH 0071/3953] Fix Alexa Report State (#26305) * Fix Alexa Report State * Forgot to save a file --- homeassistant/components/alexa/auth.py | 5 +++ homeassistant/components/alexa/config.py | 8 ++++ .../components/alexa/smart_home_http.py | 5 +++ .../components/alexa/state_report.py | 27 +++++++++---- .../components/cloud/alexa_config.py | 8 +++- tests/components/cloud/test_alexa_config.py | 38 ++++++++++++++++++- 6 files changed, 81 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index d4633d938ed505..9f87a6d954e95b 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -56,6 +56,11 @@ async def async_do_auth(self, accept_grant_code): return await self._async_request_new_token(lwa_params) + @callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + self._prefs[STORAGE_ACCESS_TOKEN] = None + async def async_get_access_token(self): """Perform access token or token refresh request.""" async with self._get_token_lock: diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index a22ebbcd30d431..f98337d71c56ae 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -1,4 +1,6 @@ """Config helpers for Alexa.""" +from homeassistant.core import callback + from .state_report import async_enable_proactive_mode @@ -55,11 +57,17 @@ async def async_disable_proactive_mode(self): unsub_func() self._unsub_proactive_report = None + @callback def should_expose(self, entity_id): """If an entity should be exposed.""" # pylint: disable=no-self-use return False + @callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + raise NotImplementedError + async def async_get_access_token(self): """Get an access token.""" raise NotImplementedError diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 7fdd4e3000a3ea..ada00e8a326820 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -57,6 +57,11 @@ def should_expose(self, entity_id): """If an entity should be exposed.""" return self._config[CONF_FILTER](entity_id) + @core.callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + self._auth.async_invalidate_access_token() + async def async_get_access_token(self): """Get an access token.""" return await self._auth.async_get_access_token() diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 7e8428899776e3..1e22d5fc09f23b 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -51,7 +51,9 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): ) -async def async_send_changereport_message(hass, config, alexa_entity): +async def async_send_changereport_message( + hass, config, alexa_entity, *, invalidate_access_token=True +): """Send a ChangeReport message for an Alexa entity. https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-with-changereport-events @@ -88,21 +90,30 @@ async def async_send_changereport_message(hass, config, alexa_entity): except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout sending report to Alexa.") - return None + return response_text = await response.text() _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) _LOGGER.debug("Received (%s): %s", response.status, response_text) - if response.status != 202: - response_json = json.loads(response_text) - _LOGGER.error( - "Error when sending ChangeReport to Alexa: %s: %s", - response_json["payload"]["code"], - response_json["payload"]["description"], + if response.status == 202 and not invalidate_access_token: + return + + response_json = json.loads(response_text) + + if response_json["payload"]["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION": + config.async_invalidate_access_token() + return await async_send_changereport_message( + hass, config, alexa_entity, invalidate_access_token=False ) + _LOGGER.error( + "Error when sending ChangeReport to Alexa: %s: %s", + response_json["payload"]["code"], + response_json["payload"]["description"], + ) + async def async_send_add_or_update_message(hass, config, entity_ids): """Send an AddOrUpdateReport message for entities. diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index d31bcfdfc40e61..a1432f196bf5d9 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -7,6 +7,7 @@ import async_timeout from hass_nabucasa import cloud_api +from homeassistant.core import callback from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers import entity_registry from homeassistant.helpers.event import async_call_later @@ -95,9 +96,14 @@ def should_expose(self, entity_id): entity_config = entity_configs.get(entity_id, {}) return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) + @callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + self._token_valid = None + async def async_get_access_token(self): """Get an access token.""" - if self._token_valid is not None and self._token_valid < utcnow(): + if self._token_valid is not None and self._token_valid > utcnow(): return self._token resp = await cloud_api.async_alexa_access_token(self._cloud) diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 22d8c64c3b0f85..c8e84016a28a9f 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -1,6 +1,6 @@ """Test Alexa config.""" import contextlib -from unittest.mock import patch +from unittest.mock import patch, Mock from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.util.dt import utcnow @@ -43,6 +43,42 @@ async def test_alexa_config_report_state(hass, cloud_prefs): assert conf.is_reporting_states is False +async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock): + """Test Alexa config should expose using prefs.""" + aioclient_mock.post( + "http://example/alexa_token", + json={ + "access_token": "mock-token", + "event_endpoint": "http://example.com/alexa_endpoint", + "expires_in": 30, + }, + ) + conf = alexa_config.AlexaConfig( + hass, + ALEXA_SCHEMA({}), + cloud_prefs, + Mock( + alexa_access_token_url="http://example/alexa_token", + run_executor=Mock(side_effect=mock_coro), + websession=hass.helpers.aiohttp_client.async_get_clientsession(), + ), + ) + + token = await conf.async_get_access_token() + assert token == "mock-token" + assert len(aioclient_mock.mock_calls) == 1 + + token = await conf.async_get_access_token() + assert token == "mock-token" + assert len(aioclient_mock.mock_calls) == 1 + assert conf._token_valid is not None + conf.async_invalidate_access_token() + assert conf._token_valid is None + token = await conf.async_get_access_token() + assert token == "mock-token" + assert len(aioclient_mock.mock_calls) == 2 + + @contextlib.contextmanager def patch_sync_helper(): """Patch sync helper. From 944b544b2eb79c9165ee2d24b74696a571a066e6 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 31 Aug 2019 13:18:18 +0100 Subject: [PATCH 0072/3953] Add support for Homekit accessory battery sensors (#26210) * Add simple battery sensor * Add test for battery sensor based on a real device * Vary icon based on battery state * Add test for battery sensory * Read other battery related states from accessory * Add a device class to the battery sensor * Respect the low battery flag from the device --- .../components/homekit_controller/const.py | 1 + .../components/homekit_controller/sensor.py | 84 +- .../specific_devices/test_hue_bridge.py | 36 + .../homekit_controller/test_sensor.py | 65 + .../homekit_controller/hue_bridge.json | 2249 +++++++++++++++++ 5 files changed, 2430 insertions(+), 5 deletions(-) create mode 100644 tests/components/homekit_controller/specific_devices/test_hue_bridge.py create mode 100644 tests/fixtures/homekit_controller/hue_bridge.json diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 6aa5dc93662dbc..ad12f3fdb71213 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -25,4 +25,5 @@ "humidity": "sensor", "light": "sensor", "temperature": "sensor", + "battery": "sensor", } diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 596b697bedecd9..f91dae26ba04f4 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -1,7 +1,7 @@ """Support for Homekit sensors.""" from homekit.model.characteristics import CharacteristicsTypes -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import DEVICE_CLASS_BATTERY, TEMP_CELSIUS from . import KNOWN_DEVICES, HomeKitEntity @@ -30,7 +30,7 @@ def get_characteristic_types(self): @property def name(self): """Return the name of the device.""" - return "{} {}".format(super().name, "Humidity") + return f"{super().name} Humidity" @property def icon(self): @@ -66,7 +66,7 @@ def get_characteristic_types(self): @property def name(self): """Return the name of the device.""" - return "{} {}".format(super().name, "Temperature") + return f"{super().name} Temperature" @property def icon(self): @@ -102,7 +102,7 @@ def get_characteristic_types(self): @property def name(self): """Return the name of the device.""" - return "{} {}".format(super().name, "Light Level") + return f"{super().name} Light Level" @property def icon(self): @@ -138,7 +138,7 @@ def get_characteristic_types(self): @property def name(self): """Return the name of the device.""" - return "{} {}".format(super().name, "CO2") + return f"{super().name} CO2" @property def icon(self): @@ -159,11 +159,85 @@ def state(self): return self._state +class HomeKitBatterySensor(HomeKitEntity): + """Representation of a Homekit battery sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + self._low_battery = False + self._charging = False + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [ + CharacteristicsTypes.BATTERY_LEVEL, + CharacteristicsTypes.STATUS_LO_BATT, + CharacteristicsTypes.CHARGING_STATE, + ] + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def name(self): + """Return the name of the device.""" + return f"{super().name} Battery" + + @property + def icon(self): + """Return the sensor icon.""" + if not self.available or self.state is None: + return "mdi:battery-unknown" + + # This is similar to the logic in helpers.icon, but we have delegated the + # decision about what mdi:battery-alert is to the device. + icon = "mdi:battery" + if self._charging and self.state > 10: + percentage = int(round(self.state / 20 - 0.01)) * 20 + icon += f"-charging-{percentage}" + elif self._charging: + icon += "-outline" + elif self._low_battery: + icon += "-alert" + elif self.state < 95: + percentage = max(int(round(self.state / 10 - 0.01)) * 10, 10) + icon += f"-{percentage}" + + return icon + + @property + def unit_of_measurement(self): + """Return units for the sensor.""" + return UNIT_PERCENT + + def _update_battery_level(self, value): + self._state = value + + def _update_status_lo_batt(self, value): + self._low_battery = value == 1 + + def _update_charging_state(self, value): + # 0 = not charging + # 1 = charging + # 2 = not chargeable + self._charging = value == 1 + + @property + def state(self): + """Return the current battery level percentage.""" + return self._state + + ENTITY_TYPES = { "humidity": HomeKitHumiditySensor, "temperature": HomeKitTemperatureSensor, "light": HomeKitLightSensor, "carbon-dioxide": HomeKitCarbonDioxideSensor, + "battery": HomeKitBatterySensor, } diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py new file mode 100644 index 00000000000000..f3e4baa4b1f3d1 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -0,0 +1,36 @@ +"""Tests for handling accessories on a Hue bridge via HomeKit.""" + +from tests.components.homekit_controller.common import ( + setup_accessories_from_file, + setup_test_accessories, + Helper, +) + + +async def test_hue_bridge_setup(hass): + """Test that a Hue hub can be correctly setup in HA via HomeKit.""" + accessories = await setup_accessories_from_file(hass, "hue_bridge.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # Check that the battery is correctly found and set up + battery_id = "sensor.hue_dimmer_switch_battery" + battery = entity_registry.async_get(battery_id) + assert battery.unique_id == "homekit-6623462389072572-644245094400" + + battery_helper = Helper( + hass, "sensor.hue_dimmer_switch_battery", pairing, accessories[0], config_entry + ) + battery_state = await battery_helper.poll_and_get_state() + assert battery_state.attributes["friendly_name"] == "Hue dimmer switch Battery" + assert battery_state.attributes["icon"] == "mdi:battery" + assert battery_state.state == "100" + + device_registry = await hass.helpers.device_registry.async_get_registry() + + device = device_registry.async_get(battery.device_id) + assert device.manufacturer == "Philips" + assert device.name == "Hue dimmer switch" + assert device.model == "RWL021" + assert device.sw_version == "45.1.17846" diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 13d844e016289b..f9d84b069962ee 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -5,6 +5,9 @@ HUMIDITY = ("humidity", "relative-humidity.current") LIGHT_LEVEL = ("light", "light-level.current") CARBON_DIOXIDE_LEVEL = ("carbon-dioxide", "carbon-dioxide.level") +BATTERY_LEVEL = ("battery", "battery-level") +CHARGING_STATE = ("battery", "charging-state") +LO_BATT = ("battery", "status-lo-batt") def create_temperature_sensor_service(): @@ -47,6 +50,22 @@ def create_carbon_dioxide_level_sensor_service(): return service +def create_battery_level_sensor(): + """Define battery level characteristics.""" + service = FakeService("public.hap.service.battery") + + cur_state = service.add_characteristic("battery-level") + cur_state.value = 100 + + low_battery = service.add_characteristic("status-lo-batt") + low_battery.value = 0 + + charging_state = service.add_characteristic("charging-state") + charging_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() @@ -101,3 +120,49 @@ async def test_carbon_dioxide_level_sensor_read_state(hass, utcnow): helper.characteristics[CARBON_DIOXIDE_LEVEL].value = 20 state = await helper.poll_and_get_state() assert state.state == "20" + + +async def test_battery_level_sensor(hass, utcnow): + """Test reading the state of a HomeKit battery level sensor.""" + sensor = create_battery_level_sensor() + helper = await setup_test_component(hass, [sensor], suffix="battery") + + helper.characteristics[BATTERY_LEVEL].value = 100 + state = await helper.poll_and_get_state() + assert state.state == "100" + assert state.attributes["icon"] == "mdi:battery" + + helper.characteristics[BATTERY_LEVEL].value = 20 + state = await helper.poll_and_get_state() + assert state.state == "20" + assert state.attributes["icon"] == "mdi:battery-20" + + +async def test_battery_charging(hass, utcnow): + """Test reading the state of a HomeKit battery's charging state.""" + sensor = create_battery_level_sensor() + helper = await setup_test_component(hass, [sensor], suffix="battery") + + helper.characteristics[BATTERY_LEVEL].value = 0 + helper.characteristics[CHARGING_STATE].value = 1 + state = await helper.poll_and_get_state() + assert state.attributes["icon"] == "mdi:battery-outline" + + helper.characteristics[BATTERY_LEVEL].value = 20 + state = await helper.poll_and_get_state() + assert state.attributes["icon"] == "mdi:battery-charging-20" + + +async def test_battery_low(hass, utcnow): + """Test reading the state of a HomeKit battery's low state.""" + sensor = create_battery_level_sensor() + helper = await setup_test_component(hass, [sensor], suffix="battery") + + helper.characteristics[LO_BATT].value = 0 + helper.characteristics[BATTERY_LEVEL].value = 1 + state = await helper.poll_and_get_state() + assert state.attributes["icon"] == "mdi:battery-10" + + helper.characteristics[LO_BATT].value = 1 + state = await helper.poll_and_get_state() + assert state.attributes["icon"] == "mdi:battery-alert" diff --git a/tests/fixtures/homekit_controller/hue_bridge.json b/tests/fixtures/homekit_controller/hue_bridge.json new file mode 100644 index 00000000000000..7ed3882be09fcd --- /dev/null +++ b/tests/fixtures/homekit_controller/hue_bridge.json @@ -0,0 +1,2249 @@ +[ + { + "aid": 6623462389072572, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 37, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch" + }, + { + "format": "string", + "iid": 35, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "RWL021" + }, + { + "format": "string", + "iid": 34, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 84, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "45.1.17846" + }, + { + "format": "string", + "iid": 50, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462389072572" + }, + { + "format": "bool", + "iid": 22, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 644245094436, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch battery" + }, + { + "format": "uint8", + "iid": 644245094505, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000068-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "format": "uint8", + "iid": 644245094522, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000079-0000-1000-8000-0026BB765291", + "value": 0 + }, + { + "format": "uint8", + "iid": 644245094544, + "maxValue": 2, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "0000008F-0000-1000-8000-0026BB765291", + "value": 2 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 644245149880, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462389072572" + } + ], + "iid": 644245094400, + "type": "00000096-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 588410585124, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch button 1" + }, + { + "format": "uint8", + "iid": 588410585204, + "maxValue": 0, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000073-0000-1000-8000-0026BB765291", + "value": null + }, + { + "format": "uint8", + "iid": 588410585292, + "minStep": 1, + "minValue": 1, + "perms": [ + "pr" + ], + "type": "000000CB-0000-1000-8000-0026BB765291", + "value": 1 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 588410640568, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462389072572" + } + ], + "iid": 588410585088, + "linked": [ + 256 + ], + "type": "00000089-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "uint8", + "iid": 462, + "maxValue": 1, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr" + ], + "type": "000000CD-0000-1000-8000-0026BB765291", + "value": 1 + } + ], + "iid": 256, + "type": "000000CC-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 588410650660, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch button 2" + }, + { + "format": "uint8", + "iid": 588410650740, + "maxValue": 0, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000073-0000-1000-8000-0026BB765291", + "value": null + }, + { + "format": "uint8", + "iid": 588410650828, + "minStep": 1, + "minValue": 1, + "perms": [ + "pr" + ], + "type": "000000CB-0000-1000-8000-0026BB765291", + "value": 2 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 588410706104, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462389072572" + } + ], + "iid": 588410650624, + "linked": [ + 256 + ], + "type": "00000089-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 588410716196, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch button 3" + }, + { + "format": "uint8", + "iid": 588410716276, + "maxValue": 0, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000073-0000-1000-8000-0026BB765291", + "value": null + }, + { + "format": "uint8", + "iid": 588410716364, + "minStep": 1, + "minValue": 1, + "perms": [ + "pr" + ], + "type": "000000CB-0000-1000-8000-0026BB765291", + "value": 3 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 588410771640, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462389072572" + } + ], + "iid": 588410716160, + "linked": [ + 256 + ], + "type": "00000089-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 588410781732, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue dimmer switch button 4" + }, + { + "format": "uint8", + "iid": 588410781812, + "maxValue": 0, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "ev" + ], + "type": "00000073-0000-1000-8000-0026BB765291", + "value": null + }, + { + "format": "uint8", + "iid": 588410781900, + "minStep": 1, + "minValue": 1, + "perms": [ + "pr" + ], + "type": "000000CB-0000-1000-8000-0026BB765291", + "value": 4 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 588410837176, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462389072572" + } + ], + "iid": 588410781696, + "linked": [ + 256 + ], + "type": "00000089-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Philips hue - 482544" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "BSB002" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips Lighting" + }, + { + "format": "string", + "iid": 8, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.32.1932126170" + }, + { + "format": "string", + "iid": 6, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "1" + }, + { + "format": "bool", + "iid": 7, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462378982941, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462378982941" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462378982941" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462378983942, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462378983942" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462378983942" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462379123707, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462379123707" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462379123707" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462379122122, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462379122122" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 70 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462379122122" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462385996792, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462385996792" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462385996792" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462383114193, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462383114193" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 20 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462383114193" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462383114163, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LWB010" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462383114163" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue white lamp" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462383114163" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462412413293, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance spot" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW013" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462412413293" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance spot" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": true + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462412413293" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462412411853, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance spot" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW013" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462412411853" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance spot" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": true + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462412411853" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462403233419, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW012" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462403233419" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462403233419" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462403113447, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW012" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462403113447" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 35 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462403113447" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462395276939, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW012" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462395276939" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462395276939" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + }, + { + "aid": 6623462395276914, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 5, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "string", + "iid": 4, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "LTW012" + }, + { + "format": "string", + "iid": 3, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Philips" + }, + { + "format": "string", + "iid": 112, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "1.46.13" + }, + { + "format": "string", + "iid": 11, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "6623462395276914" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + } + ], + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 65591, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 65535, + "type": "000000A2-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 2817, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Hue ambiance candle" + }, + { + "format": "bool", + "iid": 2822, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "format": "int", + "iid": 2823, + "maxValue": 100, + "minStep": 1, + "minValue": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000008-0000-1000-8000-0026BB765291", + "unit": "percentage", + "value": 100 + }, + { + "description": "Color Temperature", + "format": "uint32", + "iid": 3017, + "maxValue": 454, + "minStep": 1, + "minValue": 153, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "000000CE-0000-1000-8000-0026BB765291", + "value": 366 + }, + { + "description": "ID to uniquely identify service within a single accessory", + "format": "string", + "iid": 2827, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "D8B76298-42E7-5FFD-B1D6-1782D9A1F936", + "value": "6623462395276914" + } + ], + "iid": 2816, + "type": "00000043-0000-1000-8000-0026BB765291" + } + ] + } +] \ No newline at end of file From 614cf7422596569464f5fe2699c49721b2ae4de8 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Sat, 31 Aug 2019 05:30:59 -0700 Subject: [PATCH 0073/3953] Add Withings support (#25154) * Rebasing with a clean branch. Addressing PR feedback. Cleaning up some static code checks. Fixing bug with saving credentials. * Removing unecessary change. * Caching data manager. * Removing configurable measures. * Using import step in config flow. * Updating config flows. * Addressing PR feedback. * Formatting source. * Addressing PR feedback and rebasing. --- CODEOWNERS | 1 + homeassistant/components/withings/__init__.py | 99 ++++ homeassistant/components/withings/common.py | 308 ++++++++++++ .../components/withings/config_flow.py | 189 +++++++ homeassistant/components/withings/const.py | 103 ++++ .../components/withings/manifest.json | 17 + homeassistant/components/withings/sensor.py | 460 ++++++++++++++++++ .../components/withings/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/withings/__init__.py | 1 + tests/components/withings/common.py | 213 ++++++++ tests/components/withings/conftest.py | 345 +++++++++++++ tests/components/withings/test_common.py | 130 +++++ tests/components/withings/test_config_flow.py | 175 +++++++ tests/components/withings/test_init.py | 196 ++++++++ tests/components/withings/test_sensor.py | 304 ++++++++++++ 19 files changed, 2566 insertions(+) create mode 100644 homeassistant/components/withings/__init__.py create mode 100644 homeassistant/components/withings/common.py create mode 100644 homeassistant/components/withings/config_flow.py create mode 100644 homeassistant/components/withings/const.py create mode 100644 homeassistant/components/withings/manifest.json create mode 100644 homeassistant/components/withings/sensor.py create mode 100644 homeassistant/components/withings/strings.json create mode 100644 tests/components/withings/__init__.py create mode 100644 tests/components/withings/common.py create mode 100644 tests/components/withings/conftest.py create mode 100644 tests/components/withings/test_common.py create mode 100644 tests/components/withings/test_config_flow.py create mode 100644 tests/components/withings/test_init.py create mode 100644 tests/components/withings/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index d51031486ef7cc..623d6ca9eec328 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -301,6 +301,7 @@ homeassistant/components/weather/* @fabaff homeassistant/components/weblink/* @home-assistant/core homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @sqldiablo +homeassistant/components/withings/* @vangorra homeassistant/components/worldclock/* @fabaff homeassistant/components/wwlln/* @bachya homeassistant/components/xfinity/* @cisasteelersfan diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py new file mode 100644 index 00000000000000..ecefa681b87bbe --- /dev/null +++ b/homeassistant/components/withings/__init__.py @@ -0,0 +1,99 @@ +""" +Support for the Withings API. + +For more details about this platform, please refer to the documentation at +""" +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT, SOURCE_USER +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers import config_validation as cv + +from . import config_flow, const +from .common import _LOGGER, get_data_manager, NotAuthenticatedError + +DOMAIN = const.DOMAIN + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(const.CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), + vol.Required(const.CLIENT_SECRET): vol.All( + cv.string, vol.Length(min=1) + ), + vol.Optional(const.BASE_URL): cv.url, + vol.Required(const.PROFILES): vol.All( + cv.ensure_list, + vol.Unique(), + vol.Length(min=1), + [vol.All(cv.string, vol.Length(min=1))], + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Set up the Withings component.""" + conf = config.get(DOMAIN) + if not conf: + return True + + hass.data[DOMAIN] = {const.CONFIG: conf} + + base_url = conf.get(const.BASE_URL, hass.config.api.base_url).rstrip("/") + + hass.http.register_view(config_flow.WithingsAuthCallbackView) + + config_flow.register_flow_implementation( + hass, + conf[const.CLIENT_ID], + conf[const.CLIENT_SECRET], + base_url, + conf[const.PROFILES], + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={} + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up Withings from a config entry.""" + data_manager = get_data_manager(hass, entry) + + _LOGGER.debug("Confirming we're authenticated") + try: + await data_manager.check_authenticated() + except NotAuthenticatedError: + # Trigger new config flow. + hass.async_create_task( + hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": SOURCE_USER, const.PROFILE: data_manager.profile}, + data={}, + ) + ) + return False + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload Withings config entry.""" + await hass.async_create_task( + hass.config_entries.async_forward_entry_unload(entry, "sensor") + ) + + return True diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py new file mode 100644 index 00000000000000..f2be849cbc794b --- /dev/null +++ b/homeassistant/components/withings/common.py @@ -0,0 +1,308 @@ +"""Common code for Withings.""" +import datetime +import logging +import re +import time + +import nokia +from oauthlib.oauth2.rfc6749.errors import MissingTokenError +from requests_oauthlib import TokenUpdated + +from homeassistant.config_entries import ConfigEntry +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt, slugify + +from . import const + +_LOGGER = logging.getLogger(const.LOG_NAMESPACE) +NOT_AUTHENTICATED_ERROR = re.compile( + ".*(Error Code (100|101|102|200|401)|Missing access token parameter).*", + re.IGNORECASE, +) + + +class NotAuthenticatedError(HomeAssistantError): + """Raise when not authenticated with the service.""" + + pass + + +class ServiceError(HomeAssistantError): + """Raise when the service has an error.""" + + pass + + +class ThrottleData: + """Throttle data.""" + + def __init__(self, interval: int, data): + """Constructor.""" + self._time = int(time.time()) + self._interval = interval + self._data = data + + @property + def time(self): + """Get time created.""" + return self._time + + @property + def interval(self): + """Get interval.""" + return self._interval + + @property + def data(self): + """Get data.""" + return self._data + + def is_expired(self): + """Is this data expired.""" + return int(time.time()) - self.time > self.interval + + +class WithingsDataManager: + """A class representing an Withings cloud service connection.""" + + service_available = None + + def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): + """Constructor.""" + self._hass = hass + self._api = api + self._profile = profile + self._slug = slugify(profile) + + self._measures = None + self._sleep = None + self._sleep_summary = None + + self.sleep_summary_last_update_parameter = None + self.throttle_data = {} + + @property + def profile(self) -> str: + """Get the profile.""" + return self._profile + + @property + def slug(self) -> str: + """Get the slugified profile the data is for.""" + return self._slug + + @property + def api(self): + """Get the api object.""" + return self._api + + @property + def measures(self): + """Get the current measures data.""" + return self._measures + + @property + def sleep(self): + """Get the current sleep data.""" + return self._sleep + + @property + def sleep_summary(self): + """Get the current sleep summary data.""" + return self._sleep_summary + + @staticmethod + def get_throttle_interval(): + """Get the throttle interval.""" + return const.THROTTLE_INTERVAL + + def get_throttle_data(self, domain: str) -> ThrottleData: + """Get throttlel data.""" + return self.throttle_data.get(domain) + + def set_throttle_data(self, domain: str, throttle_data: ThrottleData): + """Set throttle data.""" + self.throttle_data[domain] = throttle_data + + @staticmethod + def print_service_unavailable(): + """Print the service is unavailable (once) to the log.""" + if WithingsDataManager.service_available is not False: + _LOGGER.error("Looks like the service is not available at the moment") + WithingsDataManager.service_available = False + return True + + @staticmethod + def print_service_available(): + """Print the service is available (once) to to the log.""" + if WithingsDataManager.service_available is not True: + _LOGGER.info("Looks like the service is available again") + WithingsDataManager.service_available = True + return True + + async def call(self, function, is_first_call=True, throttle_domain=None): + """Call an api method and handle the result.""" + throttle_data = self.get_throttle_data(throttle_domain) + + should_throttle = ( + throttle_domain and throttle_data and not throttle_data.is_expired() + ) + + try: + if should_throttle: + _LOGGER.debug("Throttling call for domain: %s", throttle_domain) + result = throttle_data.data + else: + _LOGGER.debug("Running call.") + result = await self._hass.async_add_executor_job(function) + + # Update throttle data. + self.set_throttle_data( + throttle_domain, ThrottleData(self.get_throttle_interval(), result) + ) + + WithingsDataManager.print_service_available() + return result + + except TokenUpdated: + WithingsDataManager.print_service_available() + if not is_first_call: + raise ServiceError( + "Stuck in a token update loop. This should never happen" + ) + + _LOGGER.info("Token updated, re-running call.") + return await self.call(function, False, throttle_domain) + + except MissingTokenError as ex: + raise NotAuthenticatedError(ex) + + except Exception as ex: # pylint: disable=broad-except + # Service error, probably not authenticated. + if NOT_AUTHENTICATED_ERROR.match(str(ex)): + raise NotAuthenticatedError(ex) + + # Probably a network error. + WithingsDataManager.print_service_unavailable() + raise PlatformNotReady(ex) + + async def check_authenticated(self): + """Check if the user is authenticated.""" + + def function(): + return self._api.request("user", "getdevice", version="v2") + + return await self.call(function) + + async def update_measures(self): + """Update the measures data.""" + + def function(): + return self._api.get_measures() + + self._measures = await self.call(function, throttle_domain="update_measures") + + return self._measures + + async def update_sleep(self): + """Update the sleep data.""" + end_date = int(time.time()) + start_date = end_date - (6 * 60 * 60) + + def function(): + return self._api.get_sleep(startdate=start_date, enddate=end_date) + + self._sleep = await self.call(function, throttle_domain="update_sleep") + + return self._sleep + + async def update_sleep_summary(self): + """Update the sleep summary data.""" + now = dt.utcnow() + yesterday = now - datetime.timedelta(days=1) + yesterday_noon = datetime.datetime( + yesterday.year, + yesterday.month, + yesterday.day, + 12, + 0, + 0, + 0, + datetime.timezone.utc, + ) + + _LOGGER.debug( + "Getting sleep summary data since: %s", + yesterday.strftime("%Y-%m-%d %H:%M:%S UTC"), + ) + + def function(): + return self._api.get_sleep_summary(lastupdate=yesterday_noon.timestamp()) + + self._sleep_summary = await self.call( + function, throttle_domain="update_sleep_summary" + ) + + return self._sleep_summary + + +def create_withings_data_manager( + hass: HomeAssistantType, entry: ConfigEntry +) -> WithingsDataManager: + """Set up the sensor config entry.""" + entry_creds = entry.data.get(const.CREDENTIALS) or {} + profile = entry.data[const.PROFILE] + credentials = nokia.NokiaCredentials( + entry_creds.get("access_token"), + entry_creds.get("token_expiry"), + entry_creds.get("token_type"), + entry_creds.get("refresh_token"), + entry_creds.get("user_id"), + entry_creds.get("client_id"), + entry_creds.get("consumer_secret"), + ) + + def credentials_saver(credentials_param): + _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) + + # Sanitizing the data as sometimes a NokiaCredentials object + # is passed through from the API. + cred_data = credentials_param + if not isinstance(credentials_param, dict): + cred_data = credentials_param.__dict__ + + entry.data[const.CREDENTIALS] = cred_data + hass.config_entries.async_update_entry(entry, data={**entry.data}) + + _LOGGER.debug("Creating nokia api instance") + api = nokia.NokiaApi( + credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) + ) + + _LOGGER.debug("Creating withings data manager for profile: %s", profile) + return WithingsDataManager(hass, profile, api) + + +def get_data_manager( + hass: HomeAssistantType, entry: ConfigEntry +) -> WithingsDataManager: + """Get a data manager for a config entry. + + If the data manager doesn't exist yet, it will be + created and cached for later use. + """ + profile = entry.data.get(const.PROFILE) + + if not hass.data.get(const.DOMAIN): + hass.data[const.DOMAIN] = {} + + if not hass.data[const.DOMAIN].get(const.DATA_MANAGER): + hass.data[const.DOMAIN][const.DATA_MANAGER] = {} + + if not hass.data[const.DOMAIN][const.DATA_MANAGER].get(profile): + hass.data[const.DOMAIN][const.DATA_MANAGER][ + profile + ] = create_withings_data_manager(hass, entry) + + return hass.data[const.DOMAIN][const.DATA_MANAGER][profile] diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py new file mode 100644 index 00000000000000..23cc74281e8b9b --- /dev/null +++ b/homeassistant/components/withings/config_flow.py @@ -0,0 +1,189 @@ +"""Config flow for Withings.""" +from collections import OrderedDict +import logging +from typing import Optional + +import aiohttp +import nokia +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.http import HomeAssistantView +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback + +from . import const + +DATA_FLOW_IMPL = "withings_flow_implementation" + +_LOGGER = logging.getLogger(__name__) + + +@callback +def register_flow_implementation(hass, client_id, client_secret, base_url, profiles): + """Register a flow implementation. + + hass: Home assistant object. + client_id: Client id. + client_secret: Client secret. + base_url: Base url of home assistant instance. + profiles: The profiles to work with. + """ + if DATA_FLOW_IMPL not in hass.data: + hass.data[DATA_FLOW_IMPL] = OrderedDict() + + hass.data[DATA_FLOW_IMPL] = { + const.CLIENT_ID: client_id, + const.CLIENT_SECRET: client_secret, + const.BASE_URL: base_url, + const.PROFILES: profiles, + } + + +@config_entries.HANDLERS.register(const.DOMAIN) +class WithingsFlowHandler(config_entries.ConfigFlow): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize flow.""" + self.flow_profile = None + self.data = None + + def async_profile_config_entry(self, profile: str) -> Optional[ConfigEntry]: + """Get a profile config entry.""" + entries = self.hass.config_entries.async_entries(const.DOMAIN) + for entry in entries: + if entry.data.get(const.PROFILE) == profile: + return entry + + return None + + def get_auth_client(self, profile: str): + """Get a new auth client.""" + flow = self.hass.data[DATA_FLOW_IMPL] + client_id = flow[const.CLIENT_ID] + client_secret = flow[const.CLIENT_SECRET] + base_url = flow[const.BASE_URL].rstrip("/") + + callback_uri = "{}/{}?flow_id={}&profile={}".format( + base_url.rstrip("/"), + const.AUTH_CALLBACK_PATH.lstrip("/"), + self.flow_id, + profile, + ) + + return nokia.NokiaAuth( + client_id, + client_secret, + callback_uri, + scope=",".join(["user.info", "user.metrics", "user.activity"]), + ) + + async def async_step_import(self, user_input=None): + """Create user step.""" + return await self.async_step_user(user_input) + + async def async_step_user(self, user_input=None): + """Create an entry for selecting a profile.""" + flow = self.hass.data.get(DATA_FLOW_IMPL, {}) + + if user_input: + return await self.async_step_auth(user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(const.PROFILE): vol.In(flow.get(const.PROFILES))} + ), + ) + + async def async_step_auth(self, user_input=None): + """Create an entry for auth.""" + if user_input.get(const.CODE): + self.data = user_input + return self.async_external_step_done(next_step_id="finish") + + profile = user_input.get(const.PROFILE) + + auth_client = self.get_auth_client(profile) + + url = auth_client.get_authorize_url() + + return self.async_external_step(step_id="auth", url=url) + + async def async_step_finish(self, user_input=None): + """Received code for authentication.""" + data = user_input or self.data or {} + + _LOGGER.debug( + "Should close all flows below %s", + self.hass.config_entries.flow.async_progress(), + ) + + profile = data[const.PROFILE] + code = data[const.CODE] + + return await self._async_create_session(profile, code) + + async def _async_create_session(self, profile, code): + """Create withings session and entries.""" + auth_client = self.get_auth_client(profile) + + _LOGGER.debug("Requesting credentials with code: %s.", code) + credentials = auth_client.get_credentials(code) + + return self.async_create_entry( + title=profile, + data={const.PROFILE: profile, const.CREDENTIALS: credentials.__dict__}, + ) + + +class WithingsAuthCallbackView(HomeAssistantView): + """Withings Authorization Callback View.""" + + requires_auth = False + url = const.AUTH_CALLBACK_PATH + name = const.AUTH_CALLBACK_NAME + + def __init__(self): + """Constructor.""" + + async def get(self, request): + """Receive authorization code.""" + hass = request.app["hass"] + + code = request.query.get("code") + profile = request.query.get("profile") + flow_id = request.query.get("flow_id") + + if not flow_id: + return aiohttp.web_response.Response( + status=400, text="'flow_id' argument not provided in url." + ) + + if not profile: + return aiohttp.web_response.Response( + status=400, text="'profile' argument not provided in url." + ) + + if not code: + return aiohttp.web_response.Response( + status=400, text="'code' argument not provided in url." + ) + + try: + await hass.config_entries.flow.async_configure( + flow_id, {const.PROFILE: profile, const.CODE: code} + ) + + return aiohttp.web_response.Response( + status=200, + headers={"content-type": "text/html"}, + text="", + ) + + except data_entry_flow.UnknownFlow: + return aiohttp.web_response.Response(status=400, text="Unknown flow") diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py new file mode 100644 index 00000000000000..79527d9d557be4 --- /dev/null +++ b/homeassistant/components/withings/const.py @@ -0,0 +1,103 @@ +"""Constants used by the Withings component.""" +import homeassistant.const as const + +DATA_MANAGER = "data_manager" + +BASE_URL = "base_url" +CLIENT_ID = "client_id" +CLIENT_SECRET = "client_secret" +CODE = "code" +CONFIG = "config" +CREDENTIALS = "credentials" +DOMAIN = "withings" +LOG_NAMESPACE = "homeassistant.components.withings" +MEASURES = "measures" +PROFILE = "profile" +PROFILES = "profiles" + +AUTH_CALLBACK_PATH = "/api/withings/authorize" +AUTH_CALLBACK_NAME = "withings:authorize" + +THROTTLE_INTERVAL = 60 + +STATE_UNKNOWN = const.STATE_UNKNOWN +STATE_AWAKE = "awake" +STATE_DEEP = "deep" +STATE_LIGHT = "light" +STATE_REM = "rem" + +MEASURE_TYPE_BODY_TEMP = 71 +MEASURE_TYPE_BONE_MASS = 88 +MEASURE_TYPE_DIASTOLIC_BP = 9 +MEASURE_TYPE_FAT_MASS = 8 +MEASURE_TYPE_FAT_MASS_FREE = 5 +MEASURE_TYPE_FAT_RATIO = 6 +MEASURE_TYPE_HEART_PULSE = 11 +MEASURE_TYPE_HEIGHT = 4 +MEASURE_TYPE_HYDRATION = 77 +MEASURE_TYPE_MUSCLE_MASS = 76 +MEASURE_TYPE_PWV = 91 +MEASURE_TYPE_SKIN_TEMP = 73 +MEASURE_TYPE_SLEEP_DEEP_DURATION = "deepsleepduration" +MEASURE_TYPE_SLEEP_HEART_RATE_AVERAGE = "hr_average" +MEASURE_TYPE_SLEEP_HEART_RATE_MAX = "hr_max" +MEASURE_TYPE_SLEEP_HEART_RATE_MIN = "hr_min" +MEASURE_TYPE_SLEEP_LIGHT_DURATION = "lightsleepduration" +MEASURE_TYPE_SLEEP_REM_DURATION = "remsleepduration" +MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_AVERAGE = "rr_average" +MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MAX = "rr_max" +MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MIN = "rr_min" +MEASURE_TYPE_SLEEP_STATE_AWAKE = 0 +MEASURE_TYPE_SLEEP_STATE_DEEP = 2 +MEASURE_TYPE_SLEEP_STATE_LIGHT = 1 +MEASURE_TYPE_SLEEP_STATE_REM = 3 +MEASURE_TYPE_SLEEP_TOSLEEP_DURATION = "durationtosleep" +MEASURE_TYPE_SLEEP_TOWAKEUP_DURATION = "durationtowakeup" +MEASURE_TYPE_SLEEP_WAKEUP_DURATION = "wakeupduration" +MEASURE_TYPE_SLEEP_WAKUP_COUNT = "wakeupcount" +MEASURE_TYPE_SPO2 = 54 +MEASURE_TYPE_SYSTOLIC_BP = 10 +MEASURE_TYPE_TEMP = 12 +MEASURE_TYPE_WEIGHT = 1 + +MEAS_BODY_TEMP_C = "body_temperature_c" +MEAS_BONE_MASS_KG = "bone_mass_kg" +MEAS_DIASTOLIC_MMHG = "diastolic_blood_pressure_mmhg" +MEAS_FAT_FREE_MASS_KG = "fat_free_mass_kg" +MEAS_FAT_MASS_KG = "fat_mass_kg" +MEAS_FAT_RATIO_PCT = "fat_ratio_pct" +MEAS_HEART_PULSE_BPM = "heart_pulse_bpm" +MEAS_HEIGHT_M = "height_m" +MEAS_HYDRATION = "hydration" +MEAS_MUSCLE_MASS_KG = "muscle_mass_kg" +MEAS_PWV = "pulse_wave_velocity" +MEAS_SKIN_TEMP_C = "skin_temperature_c" +MEAS_SLEEP_DEEP_DURATION_SECONDS = "sleep_deep_duration_seconds" +MEAS_SLEEP_HEART_RATE_AVERAGE = "sleep_heart_rate_average_bpm" +MEAS_SLEEP_HEART_RATE_MAX = "sleep_heart_rate_max_bpm" +MEAS_SLEEP_HEART_RATE_MIN = "sleep_heart_rate_min_bpm" +MEAS_SLEEP_LIGHT_DURATION_SECONDS = "sleep_light_duration_seconds" +MEAS_SLEEP_REM_DURATION_SECONDS = "sleep_rem_duration_seconds" +MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE = "sleep_respiratory_average_bpm" +MEAS_SLEEP_RESPIRATORY_RATE_MAX = "sleep_respiratory_max_bpm" +MEAS_SLEEP_RESPIRATORY_RATE_MIN = "sleep_respiratory_min_bpm" +MEAS_SLEEP_STATE = "sleep_state" +MEAS_SLEEP_TOSLEEP_DURATION_SECONDS = "sleep_tosleep_duration_seconds" +MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS = "sleep_towakeup_duration_seconds" +MEAS_SLEEP_WAKEUP_COUNT = "sleep_wakeup_count" +MEAS_SLEEP_WAKEUP_DURATION_SECONDS = "sleep_wakeup_duration_seconds" +MEAS_SPO2_PCT = "spo2_pct" +MEAS_SYSTOLIC_MMGH = "systolic_blood_pressure_mmhg" +MEAS_TEMP_C = "temperature_c" +MEAS_WEIGHT_KG = "weight_kg" + +UOM_BEATS_PER_MINUTE = "bpm" +UOM_BREATHS_PER_MINUTE = "br/m" +UOM_FREQUENCY = "times" +UOM_METERS_PER_SECOND = "m/s" +UOM_MMHG = "mmhg" +UOM_PERCENT = "%" +UOM_LENGTH_M = const.LENGTH_METERS +UOM_MASS_KG = const.MASS_KILOGRAMS +UOM_SECONDS = "seconds" +UOM_TEMP_C = const.TEMP_CELSIUS diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json new file mode 100644 index 00000000000000..726d9f13eda0b0 --- /dev/null +++ b/homeassistant/components/withings/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "withings", + "name": "Withings", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/withings", + "requirements": [ + "nokia==1.2.0" + ], + "dependencies": [ + "api", + "http", + "webhook" + ], + "codeowners": [ + "@vangorra" + ] +} \ No newline at end of file diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py new file mode 100644 index 00000000000000..3328808295f36e --- /dev/null +++ b/homeassistant/components/withings/sensor.py @@ -0,0 +1,460 @@ +"""Sensors flow for Withings.""" +import typing as types + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import slugify + +from . import const +from .common import _LOGGER, WithingsDataManager, get_data_manager + +# There's only 3 calls (per profile) made to the withings api every 5 +# minutes (see throttle values). This component wouldn't benefit +# much from parallel updates. +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: types.Callable[[types.List[Entity], bool], None], +): + """Set up the sensor config entry.""" + data_manager = get_data_manager(hass, entry) + entities = create_sensor_entities(data_manager) + async_add_entities(entities, True) + + +def get_measures(): + """Get all the measures. + + This function exists to be easily mockable so we can test + one measure at a time. This becomes necessary when integration + testing throttle functionality in the data manager. + """ + return list(WITHINGS_MEASUREMENTS_MAP) + + +def create_sensor_entities(data_manager: WithingsDataManager): + """Create sensor entities.""" + entities = [] + + measures = get_measures() + + for attribute in WITHINGS_ATTRIBUTES: + if attribute.measurement not in measures: + _LOGGER.debug( + "Skipping measurement %s as it is not in the" + "list of measurements to use", + attribute.measurement, + ) + continue + + _LOGGER.debug( + "Creating entity for measurement: %s, measure_type: %s," + "friendly_name: %s, unit_of_measurement: %s", + attribute.measurement, + attribute.measure_type, + attribute.friendly_name, + attribute.unit_of_measurement, + ) + + entity = WithingsHealthSensor(data_manager, attribute) + + entities.append(entity) + + return entities + + +class WithingsAttribute: + """Base class for modeling withing data.""" + + def __init__( + self, + measurement: str, + measure_type, + friendly_name: str, + unit_of_measurement: str, + icon: str, + ) -> None: + """Constructor.""" + self.measurement = measurement + self.measure_type = measure_type + self.friendly_name = friendly_name + self.unit_of_measurement = unit_of_measurement + self.icon = icon + + +class WithingsMeasureAttribute(WithingsAttribute): + """Model measure attributes.""" + + +class WithingsSleepStateAttribute(WithingsAttribute): + """Model sleep data attributes.""" + + def __init__( + self, measurement: str, friendly_name: str, unit_of_measurement: str, icon: str + ) -> None: + """Constructor.""" + super().__init__(measurement, None, friendly_name, unit_of_measurement, icon) + + +class WithingsSleepSummaryAttribute(WithingsAttribute): + """Models sleep summary attributes.""" + + +WITHINGS_ATTRIBUTES = [ + WithingsMeasureAttribute( + const.MEAS_WEIGHT_KG, + const.MEASURE_TYPE_WEIGHT, + "Weight", + const.UOM_MASS_KG, + "mdi:weight-kilogram", + ), + WithingsMeasureAttribute( + const.MEAS_FAT_MASS_KG, + const.MEASURE_TYPE_FAT_MASS, + "Fat Mass", + const.UOM_MASS_KG, + "mdi:weight-kilogram", + ), + WithingsMeasureAttribute( + const.MEAS_FAT_FREE_MASS_KG, + const.MEASURE_TYPE_FAT_MASS_FREE, + "Fat Free Mass", + const.UOM_MASS_KG, + "mdi:weight-kilogram", + ), + WithingsMeasureAttribute( + const.MEAS_MUSCLE_MASS_KG, + const.MEASURE_TYPE_MUSCLE_MASS, + "Muscle Mass", + const.UOM_MASS_KG, + "mdi:weight-kilogram", + ), + WithingsMeasureAttribute( + const.MEAS_BONE_MASS_KG, + const.MEASURE_TYPE_BONE_MASS, + "Bone Mass", + const.UOM_MASS_KG, + "mdi:weight-kilogram", + ), + WithingsMeasureAttribute( + const.MEAS_HEIGHT_M, + const.MEASURE_TYPE_HEIGHT, + "Height", + const.UOM_LENGTH_M, + "mdi:ruler", + ), + WithingsMeasureAttribute( + const.MEAS_TEMP_C, + const.MEASURE_TYPE_TEMP, + "Temperature", + const.UOM_TEMP_C, + "mdi:thermometer", + ), + WithingsMeasureAttribute( + const.MEAS_BODY_TEMP_C, + const.MEASURE_TYPE_BODY_TEMP, + "Body Temperature", + const.UOM_TEMP_C, + "mdi:thermometer", + ), + WithingsMeasureAttribute( + const.MEAS_SKIN_TEMP_C, + const.MEASURE_TYPE_SKIN_TEMP, + "Skin Temperature", + const.UOM_TEMP_C, + "mdi:thermometer", + ), + WithingsMeasureAttribute( + const.MEAS_FAT_RATIO_PCT, + const.MEASURE_TYPE_FAT_RATIO, + "Fat Ratio", + const.UOM_PERCENT, + None, + ), + WithingsMeasureAttribute( + const.MEAS_DIASTOLIC_MMHG, + const.MEASURE_TYPE_DIASTOLIC_BP, + "Diastolic Blood Pressure", + const.UOM_MMHG, + None, + ), + WithingsMeasureAttribute( + const.MEAS_SYSTOLIC_MMGH, + const.MEASURE_TYPE_SYSTOLIC_BP, + "Systolic Blood Pressure", + const.UOM_MMHG, + None, + ), + WithingsMeasureAttribute( + const.MEAS_HEART_PULSE_BPM, + const.MEASURE_TYPE_HEART_PULSE, + "Heart Pulse", + const.UOM_BEATS_PER_MINUTE, + "mdi:heart-pulse", + ), + WithingsMeasureAttribute( + const.MEAS_SPO2_PCT, const.MEASURE_TYPE_SPO2, "SP02", const.UOM_PERCENT, None + ), + WithingsMeasureAttribute( + const.MEAS_HYDRATION, const.MEASURE_TYPE_HYDRATION, "Hydration", "", "mdi:water" + ), + WithingsMeasureAttribute( + const.MEAS_PWV, + const.MEASURE_TYPE_PWV, + "Pulse Wave Velocity", + const.UOM_METERS_PER_SECOND, + None, + ), + WithingsSleepStateAttribute( + const.MEAS_SLEEP_STATE, "Sleep state", None, "mdi:sleep" + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_WAKEUP_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_WAKEUP_DURATION, + "Wakeup time", + const.UOM_SECONDS, + "mdi:sleep-off", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_LIGHT_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_LIGHT_DURATION, + "Light sleep", + const.UOM_SECONDS, + "mdi:sleep", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_DEEP_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_DEEP_DURATION, + "Deep sleep", + const.UOM_SECONDS, + "mdi:sleep", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_REM_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_REM_DURATION, + "REM sleep", + const.UOM_SECONDS, + "mdi:sleep", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_WAKEUP_COUNT, + const.MEASURE_TYPE_SLEEP_WAKUP_COUNT, + "Wakeup count", + const.UOM_FREQUENCY, + "mdi:sleep-off", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_TOSLEEP_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_TOSLEEP_DURATION, + "Time to sleep", + const.UOM_SECONDS, + "mdi:sleep", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_TOWAKEUP_DURATION, + "Time to wakeup", + const.UOM_SECONDS, + "mdi:sleep-off", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_HEART_RATE_AVERAGE, + const.MEASURE_TYPE_SLEEP_HEART_RATE_AVERAGE, + "Average heart rate", + const.UOM_BEATS_PER_MINUTE, + "mdi:heart-pulse", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_HEART_RATE_MIN, + const.MEASURE_TYPE_SLEEP_HEART_RATE_MIN, + "Minimum heart rate", + const.UOM_BEATS_PER_MINUTE, + "mdi:heart-pulse", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_HEART_RATE_MAX, + const.MEASURE_TYPE_SLEEP_HEART_RATE_MAX, + "Maximum heart rate", + const.UOM_BEATS_PER_MINUTE, + "mdi:heart-pulse", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE, + const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_AVERAGE, + "Average respiratory rate", + const.UOM_BREATHS_PER_MINUTE, + None, + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_RESPIRATORY_RATE_MIN, + const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MIN, + "Minimum respiratory rate", + const.UOM_BREATHS_PER_MINUTE, + None, + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, + const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MAX, + "Maximum respiratory rate", + const.UOM_BREATHS_PER_MINUTE, + None, + ), +] + +WITHINGS_MEASUREMENTS_MAP = {attr.measurement: attr for attr in WITHINGS_ATTRIBUTES} + + +class WithingsHealthSensor(Entity): + """Implementation of a Withings sensor.""" + + def __init__( + self, data_manager: WithingsDataManager, attribute: WithingsAttribute + ) -> None: + """Initialize the Withings sensor.""" + self._data_manager = data_manager + self._attribute = attribute + self._state = None + + self._slug = self._data_manager.slug + self._user_id = self._data_manager.api.get_credentials().user_id + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return "Withings {} {}".format(self._attribute.measurement, self._slug) + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return "withings_{}_{}_{}".format( + self._slug, self._user_id, slugify(self._attribute.measurement) + ) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of this entity, if any.""" + return self._attribute.unit_of_measurement + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return self._attribute.icon + + @property + def device_state_attributes(self): + """Get withings attributes.""" + return self._attribute.__dict__ + + async def async_update(self) -> None: + """Update the data.""" + _LOGGER.debug( + "Async update slug: %s, measurement: %s, user_id: %s", + self._slug, + self._attribute.measurement, + self._user_id, + ) + + if isinstance(self._attribute, WithingsMeasureAttribute): + _LOGGER.debug("Updating measures state") + await self._data_manager.update_measures() + await self.async_update_measure(self._data_manager.measures) + + elif isinstance(self._attribute, WithingsSleepStateAttribute): + _LOGGER.debug("Updating sleep state") + await self._data_manager.update_sleep() + await self.async_update_sleep_state(self._data_manager.sleep) + + elif isinstance(self._attribute, WithingsSleepSummaryAttribute): + _LOGGER.debug("Updating sleep summary state") + await self._data_manager.update_sleep_summary() + await self.async_update_sleep_summary(self._data_manager.sleep_summary) + + async def async_update_measure(self, data) -> None: + """Update the measures data.""" + if data is None: + _LOGGER.error("Provided data is None. Setting state to %s", None) + self._state = None + return + + measure_type = self._attribute.measure_type + + _LOGGER.debug( + "Finding the unambiguous measure group with measure_type: %s", measure_type + ) + measure_groups = [ + g + for g in data + if (not g.is_ambiguous() and g.get_measure(measure_type) is not None) + ] + + if not measure_groups: + _LOGGER.warning("No measure groups found, setting state to %s", None) + self._state = None + return + + _LOGGER.debug( + "Sorting list of %s measure groups by date created (DESC)", + len(measure_groups), + ) + measure_groups.sort(key=(lambda g: g.created), reverse=True) + + self._state = round(measure_groups[0].get_measure(measure_type), 4) + + async def async_update_sleep_state(self, data) -> None: + """Update the sleep state data.""" + if data is None: + _LOGGER.error("Provided data is None. Setting state to %s", None) + self._state = None + return + + if not data.series: + _LOGGER.warning("No sleep data, setting state to %s", None) + self._state = None + return + + series = sorted(data.series, key=lambda o: o.enddate, reverse=True) + + serie = series[0] + + if serie.state == const.MEASURE_TYPE_SLEEP_STATE_AWAKE: + self._state = const.STATE_AWAKE + elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_LIGHT: + self._state = const.STATE_LIGHT + elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_DEEP: + self._state = const.STATE_DEEP + elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_REM: + self._state = const.STATE_REM + else: + self._state = None + + async def async_update_sleep_summary(self, data) -> None: + """Update the sleep summary data.""" + if data is None: + _LOGGER.error("Provided data is None. Setting state to %s", None) + self._state = None + return + + if not data.series: + _LOGGER.warning("Sleep data has no series, setting state to %s", None) + self._state = None + return + + measurement = self._attribute.measurement + measure_type = self._attribute.measure_type + + _LOGGER.debug("Determining total value for: %s", measurement) + total = 0 + for serie in data.series: + if hasattr(serie, measure_type): + total += getattr(serie, measure_type) + + self._state = round(total, 4) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json new file mode 100644 index 00000000000000..88b8e6d5ea0944 --- /dev/null +++ b/homeassistant/components/withings/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "title": "Withings", + "step": { + "user": { + "title": "User Profile.", + "description": "Select a user profile to which you want Home Assistant to map with a Withings profile. On the withings page, be sure to select the same user or data will not be labeled correctly.", + "data": { + "profile": "Profile" + } + } + }, + "create_entry": { + "default": "Successfully authenticated with Withings for the selected profile." + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 082e0f853f87ce..dadb68642bcabd 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -62,6 +62,7 @@ "velbus", "vesync", "wemo", + "withings", "wwlln", "zha", "zone", diff --git a/requirements_all.txt b/requirements_all.txt index d1eddd292063a1..37e1e1f6474d1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -843,6 +843,9 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 +# homeassistant.components.withings +nokia==1.2.0 + # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ed0689654a691c..35125387e89e8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,6 +222,9 @@ minio==4.0.9 # homeassistant.components.ssdp netdisco==2.6.0 +# homeassistant.components.withings +nokia==1.2.0 + # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 6a181ab6b00a68..e99fd0a6c46b48 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -102,6 +102,7 @@ "mficlient", "minio", "netdisco", + "nokia", "numpy", "oauth2client", "paho-mqtt", diff --git a/tests/components/withings/__init__.py b/tests/components/withings/__init__.py new file mode 100644 index 00000000000000..c1caac222a5d93 --- /dev/null +++ b/tests/components/withings/__init__.py @@ -0,0 +1 @@ +"""Tests for the withings component.""" diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py new file mode 100644 index 00000000000000..b8406c39711e57 --- /dev/null +++ b/tests/components/withings/common.py @@ -0,0 +1,213 @@ +"""Common data for for the withings component tests.""" +import time + +import nokia + +import homeassistant.components.withings.const as const + + +def new_sleep_data(model, series): + """Create simple dict to simulate api data.""" + return {"series": series, "model": model} + + +def new_sleep_data_serie(startdate, enddate, state): + """Create simple dict to simulate api data.""" + return {"startdate": startdate, "enddate": enddate, "state": state} + + +def new_sleep_summary(timezone, model, startdate, enddate, date, modified, data): + """Create simple dict to simulate api data.""" + return { + "timezone": timezone, + "model": model, + "startdate": startdate, + "enddate": enddate, + "date": date, + "modified": modified, + "data": data, + } + + +def new_sleep_summary_detail( + wakeupduration, + lightsleepduration, + deepsleepduration, + remsleepduration, + wakeupcount, + durationtosleep, + durationtowakeup, + hr_average, + hr_min, + hr_max, + rr_average, + rr_min, + rr_max, +): + """Create simple dict to simulate api data.""" + return { + "wakeupduration": wakeupduration, + "lightsleepduration": lightsleepduration, + "deepsleepduration": deepsleepduration, + "remsleepduration": remsleepduration, + "wakeupcount": wakeupcount, + "durationtosleep": durationtosleep, + "durationtowakeup": durationtowakeup, + "hr_average": hr_average, + "hr_min": hr_min, + "hr_max": hr_max, + "rr_average": rr_average, + "rr_min": rr_min, + "rr_max": rr_max, + } + + +def new_measure_group( + grpid, attrib, date, created, category, deviceid, more, offset, measures +): + """Create simple dict to simulate api data.""" + return { + "grpid": grpid, + "attrib": attrib, + "date": date, + "created": created, + "category": category, + "deviceid": deviceid, + "measures": measures, + "more": more, + "offset": offset, + "comment": "blah", # deprecated + } + + +def new_measure(type_str, value, unit): + """Create simple dict to simulate api data.""" + return { + "value": value, + "type": type_str, + "unit": unit, + "algo": -1, # deprecated + "fm": -1, # deprecated + "fw": -1, # deprecated + } + + +def nokia_sleep_response(states): + """Create a sleep response based on states.""" + data = [] + for state in states: + data.append( + new_sleep_data_serie( + "2019-02-01 0{}:00:00".format(str(len(data))), + "2019-02-01 0{}:00:00".format(str(len(data) + 1)), + state, + ) + ) + + return nokia.NokiaSleep(new_sleep_data("aa", data)) + + +NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( + { + "updatetime": "", + "timezone": "", + "measuregrps": [ + # Un-ambiguous groups. + new_measure_group( + 1, + 0, + time.time(), + time.time(), + 1, + "DEV_ID", + False, + 0, + [ + new_measure(const.MEASURE_TYPE_WEIGHT, 70, 0), + new_measure(const.MEASURE_TYPE_FAT_MASS, 5, 0), + new_measure(const.MEASURE_TYPE_FAT_MASS_FREE, 60, 0), + new_measure(const.MEASURE_TYPE_MUSCLE_MASS, 50, 0), + new_measure(const.MEASURE_TYPE_BONE_MASS, 10, 0), + new_measure(const.MEASURE_TYPE_HEIGHT, 2, 0), + new_measure(const.MEASURE_TYPE_TEMP, 40, 0), + new_measure(const.MEASURE_TYPE_BODY_TEMP, 35, 0), + new_measure(const.MEASURE_TYPE_SKIN_TEMP, 20, 0), + new_measure(const.MEASURE_TYPE_FAT_RATIO, 70, -3), + new_measure(const.MEASURE_TYPE_DIASTOLIC_BP, 70, 0), + new_measure(const.MEASURE_TYPE_SYSTOLIC_BP, 100, 0), + new_measure(const.MEASURE_TYPE_HEART_PULSE, 60, 0), + new_measure(const.MEASURE_TYPE_SPO2, 95, -2), + new_measure(const.MEASURE_TYPE_HYDRATION, 95, -2), + new_measure(const.MEASURE_TYPE_PWV, 100, 0), + ], + ), + # Ambiguous groups (we ignore these) + new_measure_group( + 1, + 1, + time.time(), + time.time(), + 1, + "DEV_ID", + False, + 0, + [ + new_measure(const.MEASURE_TYPE_WEIGHT, 71, 0), + new_measure(const.MEASURE_TYPE_FAT_MASS, 4, 0), + new_measure(const.MEASURE_TYPE_MUSCLE_MASS, 51, 0), + new_measure(const.MEASURE_TYPE_BONE_MASS, 11, 0), + new_measure(const.MEASURE_TYPE_HEIGHT, 201, 0), + new_measure(const.MEASURE_TYPE_TEMP, 41, 0), + new_measure(const.MEASURE_TYPE_BODY_TEMP, 34, 0), + new_measure(const.MEASURE_TYPE_SKIN_TEMP, 21, 0), + new_measure(const.MEASURE_TYPE_FAT_RATIO, 71, -3), + new_measure(const.MEASURE_TYPE_DIASTOLIC_BP, 71, 0), + new_measure(const.MEASURE_TYPE_SYSTOLIC_BP, 101, 0), + new_measure(const.MEASURE_TYPE_HEART_PULSE, 61, 0), + new_measure(const.MEASURE_TYPE_SPO2, 98, -2), + new_measure(const.MEASURE_TYPE_HYDRATION, 96, -2), + new_measure(const.MEASURE_TYPE_PWV, 102, 0), + ], + ), + ], + } +) + + +NOKIA_SLEEP_RESPONSE = nokia_sleep_response( + [ + const.MEASURE_TYPE_SLEEP_STATE_AWAKE, + const.MEASURE_TYPE_SLEEP_STATE_LIGHT, + const.MEASURE_TYPE_SLEEP_STATE_REM, + const.MEASURE_TYPE_SLEEP_STATE_DEEP, + ] +) + +NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( + { + "series": [ + new_sleep_summary( + "UTC", + 32, + "2019-02-01", + "2019-02-02", + "2019-02-02", + "12345", + new_sleep_summary_detail( + 110, 210, 310, 410, 510, 610, 710, 810, 910, 1010, 1110, 1210, 1310 + ), + ), + new_sleep_summary( + "UTC", + 32, + "2019-02-01", + "2019-02-02", + "2019-02-02", + "12345", + new_sleep_summary_detail( + 210, 310, 410, 510, 610, 710, 810, 910, 1010, 1110, 1210, 1310, 1410 + ), + ), + ] + } +) diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py new file mode 100644 index 00000000000000..7cbe3dc1cd4cd0 --- /dev/null +++ b/tests/components/withings/conftest.py @@ -0,0 +1,345 @@ +"""Fixtures for withings tests.""" +import time +from typing import Awaitable, Callable, List + +import asynctest +import nokia +import pytest + +import homeassistant.components.api as api +import homeassistant.components.http as http +import homeassistant.components.withings.const as const +from homeassistant.components.withings import CONFIG_SCHEMA, DOMAIN +from homeassistant.config import async_process_ha_core_config +from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC +from homeassistant.setup import async_setup_component + +from .common import ( + NOKIA_MEASURES_RESPONSE, + NOKIA_SLEEP_RESPONSE, + NOKIA_SLEEP_SUMMARY_RESPONSE, +) + + +class WithingsFactoryConfig: + """Configuration for withings test fixture.""" + + PROFILE_1 = "Person 1" + PROFILE_2 = "Person 2" + + def __init__( + self, + api_config: dict = None, + http_config: dict = None, + measures: List[str] = None, + unit_system: str = None, + throttle_interval: int = const.THROTTLE_INTERVAL, + nokia_request_response="DATA", + nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, + nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, + nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, + ) -> None: + """Constructor.""" + self._throttle_interval = throttle_interval + self._nokia_request_response = nokia_request_response + self._nokia_measures_response = nokia_measures_response + self._nokia_sleep_response = nokia_sleep_response + self._nokia_sleep_summary_response = nokia_sleep_summary_response + self._withings_config = { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: [ + WithingsFactoryConfig.PROFILE_1, + WithingsFactoryConfig.PROFILE_2, + ], + } + + self._api_config = api_config or {"base_url": "http://localhost/"} + self._http_config = http_config or {} + self._measures = measures + + assert self._withings_config, "withings_config must be set." + assert isinstance( + self._withings_config, dict + ), "withings_config must be a dict." + assert isinstance(self._api_config, dict), "api_config must be a dict." + assert isinstance(self._http_config, dict), "http_config must be a dict." + + self._hass_config = { + "homeassistant": {CONF_UNIT_SYSTEM: unit_system or CONF_UNIT_SYSTEM_METRIC}, + api.DOMAIN: self._api_config, + http.DOMAIN: self._http_config, + DOMAIN: self._withings_config, + } + + @property + def withings_config(self): + """Get withings component config.""" + return self._withings_config + + @property + def api_config(self): + """Get api component config.""" + return self._api_config + + @property + def http_config(self): + """Get http component config.""" + return self._http_config + + @property + def measures(self): + """Get the measures.""" + return self._measures + + @property + def hass_config(self): + """Home assistant config.""" + return self._hass_config + + @property + def throttle_interval(self): + """Throttle interval.""" + return self._throttle_interval + + @property + def nokia_request_response(self): + """Request response.""" + return self._nokia_request_response + + @property + def nokia_measures_response(self) -> nokia.NokiaMeasures: + """Measures response.""" + return self._nokia_measures_response + + @property + def nokia_sleep_response(self) -> nokia.NokiaSleep: + """Sleep response.""" + return self._nokia_sleep_response + + @property + def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: + """Sleep summary response.""" + return self._nokia_sleep_summary_response + + +class WithingsFactoryData: + """Data about the configured withing test component.""" + + def __init__( + self, + hass, + flow_id, + nokia_auth_get_credentials_mock, + nokia_api_request_mock, + nokia_api_get_measures_mock, + nokia_api_get_sleep_mock, + nokia_api_get_sleep_summary_mock, + data_manager_get_throttle_interval_mock, + ): + """Constructor.""" + self._hass = hass + self._flow_id = flow_id + self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock + self._nokia_api_request_mock = nokia_api_request_mock + self._nokia_api_get_measures_mock = nokia_api_get_measures_mock + self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock + self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock + self._data_manager_get_throttle_interval_mock = ( + data_manager_get_throttle_interval_mock + ) + + @property + def hass(self): + """Get hass instance.""" + return self._hass + + @property + def flow_id(self): + """Get flow id.""" + return self._flow_id + + @property + def nokia_auth_get_credentials_mock(self): + """Get auth credentials mock.""" + return self._nokia_auth_get_credentials_mock + + @property + def nokia_api_request_mock(self): + """Get request mock.""" + return self._nokia_api_request_mock + + @property + def nokia_api_get_measures_mock(self): + """Get measures mock.""" + return self._nokia_api_get_measures_mock + + @property + def nokia_api_get_sleep_mock(self): + """Get sleep mock.""" + return self._nokia_api_get_sleep_mock + + @property + def nokia_api_get_sleep_summary_mock(self): + """Get sleep summary mock.""" + return self._nokia_api_get_sleep_summary_mock + + @property + def data_manager_get_throttle_interval_mock(self): + """Get throttle mock.""" + return self._data_manager_get_throttle_interval_mock + + async def configure_user(self): + """Present a form with user profiles.""" + step = await self.hass.config_entries.flow.async_configure(self.flow_id, None) + assert step["step_id"] == "user" + + async def configure_profile(self, profile: str): + """Select the user profile. Present a form with authorization link.""" + print("CONFIG_PROFILE:", profile) + step = await self.hass.config_entries.flow.async_configure( + self.flow_id, {const.PROFILE: profile} + ) + assert step["step_id"] == "auth" + + async def configure_code(self, profile: str, code: str): + """Handle authorization code. Create config entries.""" + step = await self.hass.config_entries.flow.async_configure( + self.flow_id, {const.PROFILE: profile, const.CODE: code} + ) + assert step["type"] == "external_done" + + await self.hass.async_block_till_done() + + step = await self.hass.config_entries.flow.async_configure( + self.flow_id, {const.PROFILE: profile, const.CODE: code} + ) + + assert step["type"] == "create_entry" + + await self.hass.async_block_till_done() + + async def configure_all(self, profile: str, code: str): + """Configure all flow steps.""" + await self.configure_user() + await self.configure_profile(profile) + await self.configure_code(profile, code) + + +WithingsFactory = Callable[[WithingsFactoryConfig], Awaitable[WithingsFactoryData]] + + +@pytest.fixture(name="withings_factory") +def withings_factory_fixture(request, hass) -> WithingsFactory: + """Home assistant platform fixture.""" + patches = [] + + async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: + CONFIG_SCHEMA(config.hass_config.get(DOMAIN)) + + await async_process_ha_core_config( + hass, config.hass_config.get("homeassistant") + ) + assert await async_setup_component(hass, http.DOMAIN, config.hass_config) + assert await async_setup_component(hass, api.DOMAIN, config.hass_config) + + nokia_auth_get_credentials_patch = asynctest.patch( + "nokia.NokiaAuth.get_credentials", + return_value=nokia.NokiaCredentials( + access_token="my_access_token", + token_expiry=time.time() + 600, + token_type="my_token_type", + refresh_token="my_refresh_token", + user_id="my_user_id", + client_id=config.withings_config.get(const.CLIENT_ID), + consumer_secret=config.withings_config.get(const.CLIENT_SECRET), + ), + ) + nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() + + nokia_api_request_patch = asynctest.patch( + "nokia.NokiaApi.request", return_value=config.nokia_request_response + ) + nokia_api_request_mock = nokia_api_request_patch.start() + + nokia_api_get_measures_patch = asynctest.patch( + "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response + ) + nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() + + nokia_api_get_sleep_patch = asynctest.patch( + "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response + ) + nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() + + nokia_api_get_sleep_summary_patch = asynctest.patch( + "nokia.NokiaApi.get_sleep_summary", + return_value=config.nokia_sleep_summary_response, + ) + nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() + + data_manager_get_throttle_interval_patch = asynctest.patch( + "homeassistant.components.withings.common.WithingsDataManager" + ".get_throttle_interval", + return_value=config.throttle_interval, + ) + data_manager_get_throttle_interval_mock = ( + data_manager_get_throttle_interval_patch.start() + ) + + get_measures_patch = asynctest.patch( + "homeassistant.components.withings.sensor.get_measures", + return_value=config.measures, + ) + get_measures_patch.start() + + patches.extend( + [ + nokia_auth_get_credentials_patch, + nokia_api_request_patch, + nokia_api_get_measures_patch, + nokia_api_get_sleep_patch, + nokia_api_get_sleep_summary_patch, + data_manager_get_throttle_interval_patch, + get_measures_patch, + ] + ) + + # Collect the flow id. + tasks = [] + + orig_async_create_task = hass.async_create_task + + def create_task(*args): + task = orig_async_create_task(*args) + tasks.append(task) + return task + + async_create_task_patch = asynctest.patch.object( + hass, "async_create_task", side_effect=create_task + ) + + with async_create_task_patch: + assert await async_setup_component(hass, DOMAIN, config.hass_config) + await hass.async_block_till_done() + + flow_id = tasks[2].result()["flow_id"] + + return WithingsFactoryData( + hass, + flow_id, + nokia_auth_get_credentials_mock, + nokia_api_request_mock, + nokia_api_get_measures_mock, + nokia_api_get_sleep_mock, + nokia_api_get_sleep_summary_mock, + data_manager_get_throttle_interval_mock, + ) + + def cleanup(): + for patch in patches: + patch.stop() + + request.addfinalizer(cleanup) + + return factory diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py new file mode 100644 index 00000000000000..a22689f92bb6b5 --- /dev/null +++ b/tests/components/withings/test_common.py @@ -0,0 +1,130 @@ +"""Tests for the Withings component.""" +from asynctest import MagicMock +import nokia +from oauthlib.oauth2.rfc6749.errors import MissingTokenError +import pytest +from requests_oauthlib import TokenUpdated + +from homeassistant.components.withings.common import ( + NotAuthenticatedError, + ServiceError, + WithingsDataManager, +) +from homeassistant.exceptions import PlatformNotReady + + +@pytest.fixture(name="nokia_api") +def nokia_api_fixture(): + """Provide nokia api.""" + nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) + nokia_api.get_measures = MagicMock() + nokia_api.get_sleep = MagicMock() + return nokia_api + + +@pytest.fixture(name="data_manager") +def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): + """Provide data manager.""" + return WithingsDataManager(hass, "My Profile", nokia_api) + + +def test_print_service(): + """Test method.""" + # Go from None to True + WithingsDataManager.service_available = None + assert WithingsDataManager.print_service_available() + assert WithingsDataManager.service_available is True + assert not WithingsDataManager.print_service_available() + assert not WithingsDataManager.print_service_available() + + # Go from True to False + assert WithingsDataManager.print_service_unavailable() + assert WithingsDataManager.service_available is False + assert not WithingsDataManager.print_service_unavailable() + assert not WithingsDataManager.print_service_unavailable() + + # Go from False to True + assert WithingsDataManager.print_service_available() + assert WithingsDataManager.service_available is True + assert not WithingsDataManager.print_service_available() + assert not WithingsDataManager.print_service_available() + + # Go from Non to False + WithingsDataManager.service_available = None + assert WithingsDataManager.print_service_unavailable() + assert WithingsDataManager.service_available is False + assert not WithingsDataManager.print_service_unavailable() + assert not WithingsDataManager.print_service_unavailable() + + +async def test_data_manager_call(data_manager): + """Test method.""" + # Token refreshed. + def hello_func(): + return "HELLO2" + + function = MagicMock(side_effect=[TokenUpdated("my_token"), hello_func()]) + result = await data_manager.call(function) + assert result == "HELLO2" + assert function.call_count == 2 + + # Too many token refreshes. + function = MagicMock( + side_effect=[TokenUpdated("my_token"), TokenUpdated("my_token")] + ) + try: + result = await data_manager.call(function) + assert False, "This should not have ran." + except ServiceError: + assert True + assert function.call_count == 2 + + # Not authenticated 1. + test_function = MagicMock(side_effect=MissingTokenError("Error Code 401")) + try: + result = await data_manager.call(test_function) + assert False, "An exception should have been thrown." + except NotAuthenticatedError: + assert True + + # Not authenticated 2. + test_function = MagicMock(side_effect=Exception("Error Code 401")) + try: + result = await data_manager.call(test_function) + assert False, "An exception should have been thrown." + except NotAuthenticatedError: + assert True + + # Service error. + test_function = MagicMock(side_effect=PlatformNotReady()) + try: + result = await data_manager.call(test_function) + assert False, "An exception should have been thrown." + except PlatformNotReady: + assert True + + +async def test_data_manager_call_throttle_enabled(data_manager): + """Test method.""" + hello_func = MagicMock(return_value="HELLO2") + + result = await data_manager.call(hello_func, throttle_domain="test") + assert result == "HELLO2" + + result = await data_manager.call(hello_func, throttle_domain="test") + assert result == "HELLO2" + + assert hello_func.call_count == 1 + + +async def test_data_manager_call_throttle_disabled(data_manager): + """Test method.""" + hello_func = MagicMock(return_value="HELLO2") + + result = await data_manager.call(hello_func) + assert result == "HELLO2" + + result = await data_manager.call(hello_func) + assert result == "HELLO2" + + assert hello_func.call_count == 2 diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py new file mode 100644 index 00000000000000..93b9a434b7f1ac --- /dev/null +++ b/tests/components/withings/test_config_flow.py @@ -0,0 +1,175 @@ +"""Tests for the Withings config flow.""" +from aiohttp.web_request import BaseRequest +from asynctest import CoroutineMock, MagicMock +import pytest + +from homeassistant import setup, data_entry_flow +import homeassistant.components.api as api +import homeassistant.components.http as http +from homeassistant.components.withings import const +from homeassistant.components.withings.config_flow import ( + register_flow_implementation, + WithingsFlowHandler, + WithingsAuthCallbackView, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + + +@pytest.fixture(name="flow_handler") +def flow_handler_fixture(hass: HomeAssistantType): + """Provide flow handler.""" + flow_handler = WithingsFlowHandler() + flow_handler.hass = hass + return flow_handler + + +@pytest.fixture(name="setup_hass") +async def setup_hass_fixture(hass: HomeAssistantType): + """Provide hass instance.""" + config = { + http.DOMAIN: {}, + api.DOMAIN: {"base_url": "http://localhost/"}, + const.DOMAIN: { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_secret", + const.PROFILES: ["Person 1", "Person 2"], + }, + } + + hass.data = {} + + await setup.async_setup_component(hass, "http", config) + await setup.async_setup_component(hass, "api", config) + + return hass + + +def test_flow_handler_init(flow_handler: WithingsFlowHandler): + """Test the init of the flow handler.""" + assert not flow_handler.flow_profile + + +def test_flow_handler_async_profile_config_entry( + hass: HomeAssistantType, flow_handler: WithingsFlowHandler +): + """Test profile config entry.""" + config_entries = [ + ConfigEntry( + version=1, + domain=const.DOMAIN, + title="AAA", + data={}, + source="source", + connection_class="connection_class", + system_options={}, + ), + ConfigEntry( + version=1, + domain=const.DOMAIN, + title="Person 1", + data={const.PROFILE: "Person 1"}, + source="source", + connection_class="connection_class", + system_options={}, + ), + ConfigEntry( + version=1, + domain=const.DOMAIN, + title="BBB", + data={}, + source="source", + connection_class="connection_class", + system_options={}, + ), + ] + + hass.config_entries.async_entries = MagicMock(return_value=config_entries) + + config_entry = flow_handler.async_profile_config_entry + + assert not config_entry("GGGG") + hass.config_entries.async_entries.assert_called_with(const.DOMAIN) + + assert not config_entry("CCC") + hass.config_entries.async_entries.assert_called_with(const.DOMAIN) + + assert config_entry("Person 1") == config_entries[1] + hass.config_entries.async_entries.assert_called_with(const.DOMAIN) + + +def test_flow_handler_get_auth_client( + hass: HomeAssistantType, flow_handler: WithingsFlowHandler +): + """Test creation of an auth client.""" + register_flow_implementation( + hass, "my_client_id", "my_client_secret", "http://localhost/", ["Person 1"] + ) + + client = flow_handler.get_auth_client("Person 1") + assert client.client_id == "my_client_id" + assert client.consumer_secret == "my_client_secret" + assert client.callback_uri.startswith( + "http://localhost/api/withings/authorize?flow_id=" + ) + assert client.callback_uri.endswith("&profile=Person 1") + assert client.scope == "user.info,user.metrics,user.activity" + + +async def test_auth_callback_view_get(hass: HomeAssistantType): + """Test get api path.""" + view = WithingsAuthCallbackView() + hass.config_entries.flow.async_configure = CoroutineMock(return_value="AAAA") + + request = MagicMock(spec=BaseRequest) + request.app = {"hass": hass} + + # No args + request.query = {} + response = await view.get(request) + assert response.status == 400 + hass.config_entries.flow.async_configure.assert_not_called() + hass.config_entries.flow.async_configure.reset_mock() + + # Checking flow_id + request.query = {"flow_id": "my_flow_id"} + response = await view.get(request) + assert response.status == 400 + hass.config_entries.flow.async_configure.assert_not_called() + hass.config_entries.flow.async_configure.reset_mock() + + # Checking flow_id and profile + request.query = {"flow_id": "my_flow_id", "profile": "my_profile"} + response = await view.get(request) + assert response.status == 400 + hass.config_entries.flow.async_configure.assert_not_called() + hass.config_entries.flow.async_configure.reset_mock() + + # Checking flow_id, profile, code + request.query = { + "flow_id": "my_flow_id", + "profile": "my_profile", + "code": "my_code", + } + response = await view.get(request) + assert response.status == 200 + hass.config_entries.flow.async_configure.assert_called_with( + "my_flow_id", {const.PROFILE: "my_profile", const.CODE: "my_code"} + ) + hass.config_entries.flow.async_configure.reset_mock() + + # Exception thrown + hass.config_entries.flow.async_configure = CoroutineMock( + side_effect=data_entry_flow.UnknownFlow() + ) + request.query = { + "flow_id": "my_flow_id", + "profile": "my_profile", + "code": "my_code", + } + response = await view.get(request) + assert response.status == 400 + hass.config_entries.flow.async_configure.assert_called_with( + "my_flow_id", {const.PROFILE: "my_profile", const.CODE: "my_code"} + ) + hass.config_entries.flow.async_configure.reset_mock() diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py new file mode 100644 index 00000000000000..609fc1678ea770 --- /dev/null +++ b/tests/components/withings/test_init.py @@ -0,0 +1,196 @@ +"""Tests for the Withings component.""" +from asynctest import MagicMock +import voluptuous as vol + +import homeassistant.components.api as api +import homeassistant.components.http as http +from homeassistant.components.withings import async_setup, const, CONFIG_SCHEMA + +from .conftest import WithingsFactory, WithingsFactoryConfig + +BASE_HASS_CONFIG = { + http.DOMAIN: {}, + api.DOMAIN: {"base_url": "http://localhost/"}, + const.DOMAIN: None, +} + + +def config_schema_validate(withings_config): + """Assert a schema config succeeds.""" + hass_config = BASE_HASS_CONFIG.copy() + hass_config[const.DOMAIN] = withings_config + + return CONFIG_SCHEMA(hass_config) + + +def config_schema_assert_fail(withings_config): + """Assert a schema config will fail.""" + try: + config_schema_validate(withings_config) + assert False, "This line should not have run." + except vol.error.MultipleInvalid: + assert True + + +def test_config_schema_basic_config(): + """Test schema.""" + config_schema_validate( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: ["Person 1", "Person 2"], + } + ) + + +def test_config_schema_client_id(): + """Test schema.""" + config_schema_assert_fail( + { + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: ["Person 1", "Person 2"], + } + ) + config_schema_assert_fail( + { + const.CLIENT_SECRET: "my_client_secret", + const.CLIENT_ID: "", + const.PROFILES: ["Person 1"], + } + ) + config_schema_validate( + { + const.CLIENT_SECRET: "my_client_secret", + const.CLIENT_ID: "my_client_id", + const.PROFILES: ["Person 1"], + } + ) + + +def test_config_schema_client_secret(): + """Test schema.""" + config_schema_assert_fail( + {const.CLIENT_ID: "my_client_id", const.PROFILES: ["Person 1"]} + ) + config_schema_assert_fail( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "", + const.PROFILES: ["Person 1"], + } + ) + config_schema_validate( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: ["Person 1"], + } + ) + + +def test_config_schema_profiles(): + """Test schema.""" + config_schema_assert_fail( + {const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret"} + ) + config_schema_assert_fail( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: "", + } + ) + config_schema_assert_fail( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: [], + } + ) + config_schema_assert_fail( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: ["Person 1", "Person 1"], + } + ) + config_schema_validate( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: ["Person 1"], + } + ) + config_schema_validate( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: ["Person 1", "Person 2"], + } + ) + + +def test_config_schema_base_url(): + """Test schema.""" + config_schema_validate( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: ["Person 1"], + } + ) + config_schema_assert_fail( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.BASE_URL: 123, + const.PROFILES: ["Person 1"], + } + ) + config_schema_assert_fail( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.BASE_URL: "", + const.PROFILES: ["Person 1"], + } + ) + config_schema_assert_fail( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.BASE_URL: "blah blah", + const.PROFILES: ["Person 1"], + } + ) + config_schema_validate( + { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.BASE_URL: "https://www.blah.blah.blah/blah/blah", + const.PROFILES: ["Person 1"], + } + ) + + +async def test_async_setup_no_config(hass): + """Test method.""" + hass.async_create_task = MagicMock() + + await async_setup(hass, {}) + + hass.async_create_task.assert_not_called() + + +async def test_async_setup_teardown(withings_factory: WithingsFactory, hass): + """Test method.""" + data = await withings_factory(WithingsFactoryConfig(measures=[const.MEAS_TEMP_C])) + + profile = WithingsFactoryConfig.PROFILE_1 + await data.configure_all(profile, "authorization_code") + + entries = hass.config_entries.async_entries(const.DOMAIN) + assert entries + + for entry in entries: + await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py new file mode 100644 index 00000000000000..da77910097be89 --- /dev/null +++ b/tests/components/withings/test_sensor.py @@ -0,0 +1,304 @@ +"""Tests for the Withings component.""" +from unittest.mock import MagicMock, patch + +import asynctest +from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary +import pytest + +from homeassistant.components.withings import DOMAIN +from homeassistant.components.withings.common import NotAuthenticatedError +import homeassistant.components.withings.const as const +from homeassistant.components.withings.sensor import async_setup_entry +from homeassistant.config_entries import ConfigEntry, SOURCE_USER +from homeassistant.const import STATE_UNKNOWN +from homeassistant.helpers.entity_component import async_update_entity +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import slugify + +from .common import nokia_sleep_response +from .conftest import WithingsFactory, WithingsFactoryConfig + + +def get_entity_id(measure, profile): + """Get an entity id for a measure and profile.""" + return "sensor.{}_{}_{}".format(DOMAIN, measure, slugify(profile)) + + +def assert_state_equals(hass: HomeAssistantType, profile: str, measure: str, expected): + """Assert the state of a withings sensor.""" + entity_id = get_entity_id(measure, profile) + state_obj = hass.states.get(entity_id) + + assert state_obj, "Expected entity {} to exist but it did not".format(entity_id) + + assert state_obj.state == str( + expected + ), "Expected {} but was {} for measure {}".format( + expected, state_obj.state, measure + ) + + +async def test_health_sensor_properties(withings_factory: WithingsFactory): + """Test method.""" + data = await withings_factory(WithingsFactoryConfig(measures=[const.MEAS_HEIGHT_M])) + + await data.configure_all(WithingsFactoryConfig.PROFILE_1, "authorization_code") + + state = data.hass.states.get("sensor.withings_height_m_person_1") + state_dict = state.as_dict() + assert state_dict.get("state") == "2" + assert state_dict.get("attributes") == { + "measurement": "height_m", + "measure_type": 4, + "friendly_name": "Withings height_m person_1", + "unit_of_measurement": "m", + "icon": "mdi:ruler", + } + + +SENSOR_TEST_DATA = [ + (const.MEAS_WEIGHT_KG, 70), + (const.MEAS_FAT_MASS_KG, 5), + (const.MEAS_FAT_FREE_MASS_KG, 60), + (const.MEAS_MUSCLE_MASS_KG, 50), + (const.MEAS_BONE_MASS_KG, 10), + (const.MEAS_HEIGHT_M, 2), + (const.MEAS_FAT_RATIO_PCT, 0.07), + (const.MEAS_DIASTOLIC_MMHG, 70), + (const.MEAS_SYSTOLIC_MMGH, 100), + (const.MEAS_HEART_PULSE_BPM, 60), + (const.MEAS_SPO2_PCT, 0.95), + (const.MEAS_HYDRATION, 0.95), + (const.MEAS_PWV, 100), + (const.MEAS_SLEEP_WAKEUP_DURATION_SECONDS, 320), + (const.MEAS_SLEEP_LIGHT_DURATION_SECONDS, 520), + (const.MEAS_SLEEP_DEEP_DURATION_SECONDS, 720), + (const.MEAS_SLEEP_REM_DURATION_SECONDS, 920), + (const.MEAS_SLEEP_WAKEUP_COUNT, 1120), + (const.MEAS_SLEEP_TOSLEEP_DURATION_SECONDS, 1320), + (const.MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS, 1520), + (const.MEAS_SLEEP_HEART_RATE_AVERAGE, 1720), + (const.MEAS_SLEEP_HEART_RATE_MIN, 1920), + (const.MEAS_SLEEP_HEART_RATE_MAX, 2120), + (const.MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE, 2320), + (const.MEAS_SLEEP_RESPIRATORY_RATE_MIN, 2520), + (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, 2720), +] + + +@pytest.mark.parametrize("measure,expected", SENSOR_TEST_DATA) +async def test_health_sensor_throttled( + withings_factory: WithingsFactory, measure, expected +): + """Test method.""" + data = await withings_factory(WithingsFactoryConfig(measures=measure)) + + profile = WithingsFactoryConfig.PROFILE_1 + await data.configure_all(profile, "authorization_code") + + # Checking initial data. + assert_state_equals(data.hass, profile, measure, expected) + + # Encountering a throttled data. + await async_update_entity(data.hass, get_entity_id(measure, profile)) + + assert_state_equals(data.hass, profile, measure, expected) + + +NONE_SENSOR_TEST_DATA = [ + (const.MEAS_WEIGHT_KG, STATE_UNKNOWN), + (const.MEAS_SLEEP_STATE, STATE_UNKNOWN), + (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, STATE_UNKNOWN), +] + + +@pytest.mark.parametrize("measure,expected", NONE_SENSOR_TEST_DATA) +async def test_health_sensor_state_none( + withings_factory: WithingsFactory, measure, expected +): + """Test method.""" + data = await withings_factory( + WithingsFactoryConfig( + measures=measure, + nokia_measures_response=None, + nokia_sleep_response=None, + nokia_sleep_summary_response=None, + ) + ) + + profile = WithingsFactoryConfig.PROFILE_1 + await data.configure_all(profile, "authorization_code") + + # Checking initial data. + assert_state_equals(data.hass, profile, measure, expected) + + # Encountering a throttled data. + await async_update_entity(data.hass, get_entity_id(measure, profile)) + + assert_state_equals(data.hass, profile, measure, expected) + + +EMPTY_SENSOR_TEST_DATA = [ + (const.MEAS_WEIGHT_KG, STATE_UNKNOWN), + (const.MEAS_SLEEP_STATE, STATE_UNKNOWN), + (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, STATE_UNKNOWN), +] + + +@pytest.mark.parametrize("measure,expected", EMPTY_SENSOR_TEST_DATA) +async def test_health_sensor_state_empty( + withings_factory: WithingsFactory, measure, expected +): + """Test method.""" + data = await withings_factory( + WithingsFactoryConfig( + measures=measure, + nokia_measures_response=NokiaMeasures({"measuregrps": []}), + nokia_sleep_response=NokiaSleep({"series": []}), + nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), + ) + ) + + profile = WithingsFactoryConfig.PROFILE_1 + await data.configure_all(profile, "authorization_code") + + # Checking initial data. + assert_state_equals(data.hass, profile, measure, expected) + + # Encountering a throttled data. + await async_update_entity(data.hass, get_entity_id(measure, profile)) + + assert_state_equals(data.hass, profile, measure, expected) + + +SLEEP_STATES_TEST_DATA = [ + ( + const.STATE_AWAKE, + [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_AWAKE], + ), + ( + const.STATE_LIGHT, + [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_LIGHT], + ), + ( + const.STATE_REM, + [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_REM], + ), + ( + const.STATE_DEEP, + [const.MEASURE_TYPE_SLEEP_STATE_LIGHT, const.MEASURE_TYPE_SLEEP_STATE_DEEP], + ), + (const.STATE_UNKNOWN, [const.MEASURE_TYPE_SLEEP_STATE_LIGHT, "blah,"]), +] + + +@pytest.mark.parametrize("expected,sleep_states", SLEEP_STATES_TEST_DATA) +async def test_sleep_state_throttled( + withings_factory: WithingsFactory, expected, sleep_states +): + """Test method.""" + measure = const.MEAS_SLEEP_STATE + + data = await withings_factory( + WithingsFactoryConfig( + measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) + ) + ) + + profile = WithingsFactoryConfig.PROFILE_1 + await data.configure_all(profile, "authorization_code") + + # Check initial data. + assert_state_equals(data.hass, profile, measure, expected) + + # Encountering a throttled data. + await async_update_entity(data.hass, get_entity_id(measure, profile)) + + assert_state_equals(data.hass, profile, measure, expected) + + +async def test_async_setup_check_credentials( + hass: HomeAssistantType, withings_factory: WithingsFactory +): + """Test method.""" + check_creds_patch = asynctest.patch( + "homeassistant.components.withings.common.WithingsDataManager" + ".check_authenticated", + side_effect=NotAuthenticatedError(), + ) + + async_init_patch = asynctest.patch.object( + hass.config_entries.flow, + "async_init", + wraps=hass.config_entries.flow.async_init, + ) + + with check_creds_patch, async_init_patch as async_init_mock: + data = await withings_factory( + WithingsFactoryConfig(measures=[const.MEAS_HEIGHT_M]) + ) + + profile = WithingsFactoryConfig.PROFILE_1 + await data.configure_all(profile, "authorization_code") + + async_init_mock.assert_called_with( + const.DOMAIN, + context={"source": SOURCE_USER, const.PROFILE: profile}, + data={}, + ) + + +async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): + """Test method.""" + expected_creds = { + "access_token": "my_access_token2", + "refresh_token": "my_refresh_token2", + "token_type": "my_token_type2", + "expires_in": "2", + } + + original_nokia_api = NokiaApi + nokia_api_instance = None + + def new_nokia_api(*args, **kwargs): + nonlocal nokia_api_instance + nokia_api_instance = original_nokia_api(*args, **kwargs) + nokia_api_instance.request = MagicMock() + return nokia_api_instance + + nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) + session_patch = patch("requests_oauthlib.OAuth2Session") + client_patch = patch("oauthlib.oauth2.WebApplicationClient") + update_entry_patch = patch.object( + hass.config_entries, + "async_update_entry", + wraps=hass.config_entries.async_update_entry, + ) + + with session_patch, client_patch, nokia_api_patch, update_entry_patch: + async_add_entities = MagicMock() + hass.config_entries.async_update_entry = MagicMock() + config_entry = ConfigEntry( + version=1, + domain=const.DOMAIN, + title="my title", + data={ + const.PROFILE: "Person 1", + const.CREDENTIALS: { + "access_token": "my_access_token", + "refresh_token": "my_refresh_token", + "token_type": "my_token_type", + "token_expiry": "9999999999", + }, + }, + source="source", + connection_class="conn_class", + system_options={}, + ) + + await async_setup_entry(hass, config_entry, async_add_entities) + + nokia_api_instance.set_token(expected_creds) + + new_creds = config_entry.data[const.CREDENTIALS] + assert new_creds["access_token"] == "my_access_token2" From d1874d148a2ee11961fc537948db36bd2789258b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 31 Aug 2019 15:56:43 +0200 Subject: [PATCH 0074/3953] deCONZ - Dont update entry if data is equal --- homeassistant/components/deconz/config_flow.py | 10 ++++++---- tests/components/deconz/test_config_flow.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 650c02857509da..306a4fbf839243 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -157,8 +157,12 @@ async def _create_entry(self): async def _update_entry(self, entry, host): """Update existing entry.""" + if entry.data[CONF_HOST] == host: + return self.async_abort(reason="already_configured") + entry.data[CONF_HOST] = host self.hass.config_entries.async_update_entry(entry) + return self.async_abort(reason="updated_instance") async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" @@ -175,8 +179,7 @@ async def async_step_ssdp(self, discovery_info): if uuid in gateways: entry = gateways[uuid].config_entry - await self._update_entry(entry, discovery_info[CONF_HOST]) - return self.async_abort(reason="updated_instance") + return await self._update_entry(entry, discovery_info[CONF_HOST]) bridgeid = discovery_info[ATTR_SERIAL] if any( @@ -224,8 +227,7 @@ async def async_step_hassio(self, user_input=None): if bridgeid in gateway_entries: entry = gateway_entries[bridgeid] - await self._update_entry(entry, user_input[CONF_HOST]) - return self.async_abort(reason="updated_instance") + return await self._update_entry(entry, user_input[CONF_HOST]) self._hassio_discovery = user_input diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 8165c9df080dcf..ea3abead02870e 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -336,6 +336,24 @@ async def test_hassio_update_instance(hass): assert entry.data[config_flow.CONF_HOST] == "mock-deconz" +async def test_hassio_dont_update_instance(hass): + """Test we can update an existing config entry.""" + entry = MockConfigEntry( + domain=config_flow.DOMAIN, + data={config_flow.CONF_BRIDGEID: "id", config_flow.CONF_HOST: "1.2.3.4"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + data={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_SERIAL: "id"}, + context={"source": "hassio"}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + async def test_hassio_confirm(hass): """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( From 46b5b0cac7f70a66199180a8a6cb99d88a68db50 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 31 Aug 2019 07:46:26 -0700 Subject: [PATCH 0075/3953] Fix alexa bad temp sensors (#26307) --- .../components/alexa/capabilities.py | 23 +++++++- tests/components/alexa/__init__.py | 6 +++ tests/components/alexa/test_capabilities.py | 54 ++++++++++++++++++- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index dfb97cd9db25f4..d769f797da1bfa 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -11,6 +11,7 @@ STATE_ON, STATE_UNAVAILABLE, STATE_UNLOCKED, + STATE_UNKNOWN, ) import homeassistant.components.climate.const as climate from homeassistant.components import light, fan, cover @@ -443,7 +444,17 @@ def get_property(self, name): if self.entity.domain == climate.DOMAIN: unit = self.hass.config.units.temperature_unit temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE) - return {"value": float(temp), "scale": API_TEMP_UNITS[unit]} + + if temp in (STATE_UNAVAILABLE, STATE_UNKNOWN): + return None + + try: + temp = float(temp) + except ValueError: + _LOGGER.warning("Invalid temp value %s for %s", temp, self.entity.entity_id) + return None + + return {"value": temp, "scale": API_TEMP_UNITS[unit]} class AlexaContactSensor(AlexaCapibility): @@ -591,4 +602,12 @@ def get_property(self, name): if temp is None: return None - return {"value": float(temp), "scale": API_TEMP_UNITS[unit]} + try: + temp = float(temp) + except ValueError: + _LOGGER.warning( + "Invalid temp value %s for %s in %s", temp, name, self.entity.entity_id + ) + return None + + return {"value": temp, "scale": API_TEMP_UNITS[unit]} diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index f853c4ef848cb0..48406a11aef1f2 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -171,6 +171,12 @@ def __init__(self, properties): """Initialize class.""" self.properties = properties + def assert_not_has_property(self, namespace, name): + """Assert a property does not exist.""" + for prop in self.properties: + if prop["namespace"] == namespace and prop["name"] == name: + assert False, "Property %s:%s exists" + def assert_equal(self, namespace, name, value): """Assert a property is equal to a given value.""" for prop in self.properties: diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index f8ad3f57c420e0..357e0e3026d47c 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -1,7 +1,15 @@ """Test Alexa capabilities.""" import pytest -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS, + STATE_LOCKED, + STATE_UNLOCKED, + STATE_UNKNOWN, + STATE_UNAVAILABLE, +) +from homeassistant.components import climate from homeassistant.components.alexa import smart_home from tests.common import async_mock_service @@ -368,3 +376,47 @@ async def test_report_cover_percentage_state(hass): properties = await reported_properties(hass, "cover.closed") properties.assert_equal("Alexa.PercentageController", "percentage", 0) + + +async def test_temperature_sensor_sensor(hass): + """Test TemperatureSensor reports sensor temperature correctly.""" + for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"): + hass.states.async_set( + "sensor.temp_living_room", + bad_value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + + properties = await reported_properties(hass, "sensor.temp_living_room") + properties.assert_not_has_property("Alexa.TemperatureSensor", "temperature") + + hass.states.async_set( + "sensor.temp_living_room", "34", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + ) + properties = await reported_properties(hass, "sensor.temp_living_room") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) + + +async def test_temperature_sensor_climate(hass): + """Test TemperatureSensor reports climate temperature correctly.""" + for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"): + hass.states.async_set( + "climate.downstairs", + climate.HVAC_MODE_HEAT, + {climate.ATTR_CURRENT_TEMPERATURE: bad_value}, + ) + + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_not_has_property("Alexa.TemperatureSensor", "temperature") + + hass.states.async_set( + "climate.downstairs", + climate.HVAC_MODE_HEAT, + {climate.ATTR_CURRENT_TEMPERATURE: 34}, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) From 9df2c3f8c9c34f1014dbb4c69362588e963eb9d5 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 31 Aug 2019 18:24:17 +0100 Subject: [PATCH 0076/3953] Add precision argument to the Range Filter (#25874) * add precision argument * add precision testing to range_filter --- homeassistant/components/filter/sensor.py | 5 +++-- tests/components/filter/test_sensor.py | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 2a8798d372909f..e8c532547c1466 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -89,7 +89,7 @@ } ) -FILTER_RANGE_SCHEMA = vol.Schema( +FILTER_RANGE_SCHEMA = FILTER_SCHEMA.extend( { vol.Required(CONF_FILTER_NAME): FILTER_NAME_RANGE, vol.Optional(CONF_FILTER_LOWER_BOUND): vol.Coerce(float), @@ -406,6 +406,7 @@ class RangeFilter(Filter): def __init__( self, entity, + precision: Optional[int] = DEFAULT_PRECISION, lower_bound: Optional[float] = None, upper_bound: Optional[float] = None, ): @@ -414,7 +415,7 @@ def __init__( :param upper_bound: band upper bound :param lower_bound: band lower bound """ - super().__init__(FILTER_NAME_RANGE, entity=entity) + super().__init__(FILTER_NAME_RANGE, precision=precision, entity=entity) self._lower_bound = lower_bound self._upper_bound = upper_bound self._stats_internal = Counter() diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 6288e0699fd6c7..e9c8f4c35e2ba5 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -217,7 +217,9 @@ def test_range(self): """Test if range filter works.""" lower = 10 upper = 20 - filt = RangeFilter(entity=None, lower_bound=lower, upper_bound=upper) + filt = RangeFilter( + entity=None, precision=2, lower_bound=lower, upper_bound=upper + ) for unf_state in self.values: unf = float(unf_state.state) filtered = filt.filter_state(unf_state) @@ -232,7 +234,9 @@ def test_range_zero(self): """Test if range filter works with zeroes as bounds.""" lower = 0 upper = 0 - filt = RangeFilter(entity=None, lower_bound=lower, upper_bound=upper) + filt = RangeFilter( + entity=None, precision=2, lower_bound=lower, upper_bound=upper + ) for unf_state in self.values: unf = float(unf_state.state) filtered = filt.filter_state(unf_state) From 922522b089873297c69066cb46361fd7d1273d72 Mon Sep 17 00:00:00 2001 From: Pawel Date: Sat, 31 Aug 2019 21:56:29 +0200 Subject: [PATCH 0077/3953] Fetch Onkyo current radio preset (#26211) * atribute to show which preset is currently on in radio * add attribute for onkyo zone * change format string to f-strings --- .../components/onkyo/media_player.py | 57 +++++++++++-------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 6865eb8c9f9e39..b5001a1f983801 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -85,7 +85,10 @@ TIMEOUT_MESSAGE = "Timeout waiting for response." + ATTR_HDMI_OUTPUT = "hdmi_output" +ATTR_PRESET = "preset" + ACCEPTED_VALUES = [ "no", "analog", @@ -177,7 +180,7 @@ def service_handle(service): "2", receiver, config.get(CONF_SOURCES), - name="{} Zone 2".format(config[CONF_NAME]), + name=f"{config[CONF_NAME]} Zone 2", ) ) # Add Zone3 if available @@ -188,7 +191,7 @@ def service_handle(service): "3", receiver, config.get(CONF_SOURCES), - name="{} Zone 3".format(config[CONF_NAME]), + name=f"{config[CONF_NAME]} Zone 3", ) ) except OSError: @@ -210,8 +213,8 @@ def __init__(self, receiver, sources, name=None, max_volume=SUPPORTED_MAX_VOLUME self._muted = False self._volume = 0 self._pwstate = STATE_OFF - self._name = name or "{}_{}".format( - receiver.info["model_name"], receiver.info["identifier"] + self._name = ( + name or f"{receiver.info['model_name']}_{receiver.info['identifier']}" ) self._max_volume = max_volume self._current_source = None @@ -249,7 +252,7 @@ def update(self): mute_raw = self.command("audio-muting query") current_source_raw = self.command("input-selector query") hdmi_out_raw = self.command("hdmi-output-selector query") - + preset_raw = self.command("preset query") if not (volume_raw and mute_raw and current_source_raw): return @@ -265,6 +268,11 @@ def update(self): break else: self._current_source = "_".join([i for i in current_source_tuples[1]]) + if preset_raw and self._current_source.lower() == "radio": + self._attributes[ATTR_PRESET] = preset_raw[1] + elif ATTR_PRESET in self._attributes: + del self._attributes[ATTR_PRESET] + self._muted = bool(mute_raw[1] == "on") self._volume = volume_raw[1] / self._max_volume @@ -323,7 +331,7 @@ def set_volume_level(self, volume): Onkyo ranges from 1-80 however 80 is usually far too loud so allow the user to specify the upper range with CONF_MAX_VOLUME """ - self.command("volume {}".format(int(volume * self._max_volume))) + self.command(f"volume {int(volume * self._max_volume)}") def volume_up(self): """Increase volume by 1 step.""" @@ -348,17 +356,17 @@ def select_source(self, source): """Set the input source.""" if source in self._source_list: source = self._reverse_mapping[source] - self.command("input-selector {}".format(source)) + self.command(f"input-selector {source}") def play_media(self, media_type, media_id, **kwargs): """Play radio station by preset number.""" source = self._reverse_mapping[self._current_source] if media_type.lower() == "radio" and source in DEFAULT_PLAYABLE_SOURCES: - self.command("preset {}".format(media_id)) + self.command(f"preset {media_id}") def select_output(self, output): """Set hdmi-out.""" - self.command("hdmi-output-selector={}".format(output)) + self.command(f"hdmi-output-selector={output}") class OnkyoDeviceZone(OnkyoDevice): @@ -372,7 +380,7 @@ def __init__(self, zone, receiver, sources, name=None): def update(self): """Get the latest state from the device.""" - status = self.command("zone{}.power=query".format(self._zone)) + status = self.command(f"zone{self._zone}.power=query") if not status: return @@ -382,10 +390,10 @@ def update(self): self._pwstate = STATE_OFF return - volume_raw = self.command("zone{}.volume=query".format(self._zone)) - mute_raw = self.command("zone{}.muting=query".format(self._zone)) - current_source_raw = self.command("zone{}.selector=query".format(self._zone)) - + volume_raw = self.command(f"zone{self._zone}.volume=query") + mute_raw = self.command(f"zone{self._zone}.muting=query") + current_source_raw = self.command(f"zone{self._zone}.selector=query") + preset_raw = self.command(f"zone{self._zone}.preset=query") # If we received a source value, but not a volume value # it's likely this zone permanently does not support volume. if current_source_raw and not volume_raw: @@ -411,7 +419,10 @@ def update(self): else: self._current_source = "_".join([i for i in current_source_tuples[1]]) self._muted = bool(mute_raw[1] == "on") - + if preset_raw and self._current_source.lower() == "radio": + self._attributes[ATTR_PRESET] = preset_raw[1] + elif ATTR_PRESET in self._attributes: + del self._attributes[ATTR_PRESET] if self._supports_volume: self._volume = volume_raw[1] / 80.0 @@ -424,33 +435,33 @@ def supported_features(self): def turn_off(self): """Turn the media player off.""" - self.command("zone{}.power=standby".format(self._zone)) + self.command(f"zone{self._zone}.power=standby") def set_volume_level(self, volume): """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" - self.command("zone{}.volume={}".format(self._zone, int(volume * 80))) + self.command(f"zone{self._zone}.volume={int(volume * 80)}") def volume_up(self): """Increase volume by 1 step.""" - self.command("zone{}.volume=level-up".format(self._zone)) + self.command(f"zone{self._zone}.volume=level-up") def volume_down(self): """Decrease volume by 1 step.""" - self.command("zone{}.volume=level-down".format(self._zone)) + self.command(f"zone{self._zone}.volume=level-down") def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" if mute: - self.command("zone{}.muting=on".format(self._zone)) + self.command(f"zone{self._zone}.muting=on") else: - self.command("zone{}.muting=off".format(self._zone)) + self.command(f"zone{self._zone}.muting=off") def turn_on(self): """Turn the media player on.""" - self.command("zone{}.power=on".format(self._zone)) + self.command(f"zone{self._zone}.power=on") def select_source(self, source): """Set the input source.""" if source in self._source_list: source = self._reverse_mapping[source] - self.command("zone{}.selector={}".format(self._zone, source)) + self.command(f"zone{self._zone}.selector={source}") From d9ef92f6d277f3289fb33e12213564e2324bc499 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 31 Aug 2019 22:04:04 +0200 Subject: [PATCH 0078/3953] UniFi - use entity registry disabled_by to control available entities (#26141) * Move ignoring logic to entity registry enabled default * Handle config to option import better * Properly enable and disable entity registry entries on changes from config entry options * Fix balloobs comments * Fix some tests * Fix tests * Simplify updating disable on entities * Simplify device tracker update function * Local entity disabled replaced on rebase * Only alter entities with changed options * Proper tracking of changed options * Back to straightforward updating of disabled --- homeassistant/components/unifi/__init__.py | 6 +- homeassistant/components/unifi/const.py | 4 + homeassistant/components/unifi/controller.py | 100 +++++++++----- .../components/unifi/device_tracker.py | 125 ++++++++++-------- homeassistant/components/unifi/switch.py | 4 +- tests/components/unifi/test_controller.py | 12 +- tests/components/unifi/test_device_tracker.py | 26 +++- 7 files changed, 172 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index da9bbb8e59e986..5ad60ddd835af8 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -11,6 +11,9 @@ 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, @@ -20,9 +23,6 @@ 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( { diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index ffa9a28818bf55..4522ac4254a0c9 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -18,6 +18,10 @@ CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" CONF_SSID_FILTER = "ssid_filter" +CONF_DONT_TRACK_CLIENTS = "dont_track_clients" +CONF_DONT_TRACK_DEVICES = "dont_track_devices" +CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" + DEFAULT_BLOCK_CLIENTS = [] DEFAULT_TRACK_CLIENTS = True DEFAULT_TRACK_DEVICES = True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 47c692b12b24a6..b29b088a815f51 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -18,6 +18,9 @@ CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_DONT_TRACK_CLIENTS, + CONF_DONT_TRACK_DEVICES, + CONF_DONT_TRACK_WIRED_CLIENTS, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, @@ -30,6 +33,7 @@ DEFAULT_TRACK_WIRED_CLIENTS, DEFAULT_DETECTION_TIME, DEFAULT_SSID_FILTER, + DOMAIN, LOGGER, UNIFI_CONFIG, ) @@ -49,7 +53,6 @@ def __init__(self, hass, config_entry): self._site_name = None self._site_role = None - self.unifi_config = {} @property def host(self): @@ -116,11 +119,14 @@ def mac(self): return None @property - def event_update(self): + def signal_update(self): """Event specific per UniFi entry to signal new data.""" - return "unifi-update-{}".format( - CONTROLLER_ID.format(host=self.host, site=self.site) - ) + return f"unifi-update-{CONTROLLER_ID.format(host=self.host, site=self.site)}" + + @property + def signal_options_update(self): + """Event specific per UniFi entry to signal new options.""" + return f"unifi-options-{CONTROLLER_ID.format(host=self.host, site=self.site)}" async def request_update(self): """Request an update.""" @@ -164,7 +170,7 @@ async def async_update(self): LOGGER.info("Reconnected to controller %s", self.host) self.available = True - async_dispatcher_send(self.hass, self.event_update) + async_dispatcher_send(self.hass, self.signal_update) async def async_setup(self): """Set up a UniFi controller.""" @@ -191,37 +197,9 @@ async def async_setup(self): LOGGER.error("Unknown error connecting with UniFi controller: %s", err) return False - for unifi_config in hass.data[UNIFI_CONFIG]: - if ( - self.host == unifi_config[CONF_HOST] - and self.site_name == unifi_config[CONF_SITE_ID] - ): - 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 - ] + self.import_configuration() - 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) + self.config_entry.add_update_listener(self.async_options_updated) for platform in ["device_tracker", "switch"]: hass.async_create_task( @@ -232,6 +210,56 @@ async def async_setup(self): return True + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + controller_id = CONTROLLER_ID.format( + host=entry.data[CONF_CONTROLLER][CONF_HOST], + site=entry.data[CONF_CONTROLLER][CONF_SITE_ID], + ) + controller = hass.data[DOMAIN][controller_id] + + async_dispatcher_send(hass, controller.signal_options_update) + + def import_configuration(self): + """Import configuration to config entry options.""" + unifi_config = {} + for config in self.hass.data[UNIFI_CONFIG]: + if ( + self.host == config[CONF_HOST] + and self.site_name == config[CONF_SITE_ID] + ): + unifi_config = config + break + + old_options = dict(self.config_entry.options) + new_options = {} + + for config, option in ( + (CONF_BLOCK_CLIENT, CONF_BLOCK_CLIENT), + (CONF_DONT_TRACK_CLIENTS, CONF_TRACK_CLIENTS), + (CONF_DONT_TRACK_WIRED_CLIENTS, CONF_TRACK_WIRED_CLIENTS), + (CONF_DONT_TRACK_DEVICES, CONF_TRACK_DEVICES), + (CONF_DETECTION_TIME, CONF_DETECTION_TIME), + (CONF_SSID_FILTER, CONF_SSID_FILTER), + ): + if config in unifi_config: + if config == option and unifi_config[ + config + ] != self.config_entry.options.get(option): + new_options[option] = unifi_config[config] + elif config != option and ( + option not in self.config_entry.options + or unifi_config[config] == self.config_entry.options.get(option) + ): + new_options[option] = not unifi_config[config] + + if new_options: + options = {**old_options, **new_options} + self.hass.config_entries.async_update_entry( + self.config_entry, options=options + ) + async def async_reset(self): """Reset this controller to default state. diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 4845e9222ce551..c4451546776b2c 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -20,6 +20,7 @@ from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util @@ -136,7 +137,24 @@ def update_controller(): """Update the values of the controller.""" update_items(controller, async_add_entities, tracked) - async_dispatcher_connect(hass, controller.event_update, update_controller) + async_dispatcher_connect(hass, controller.signal_update, update_controller) + + @callback + def update_disable_on_entities(): + """Update the values of the controller.""" + for entity in tracked.values(): + + disabled_by = None + if not entity.entity_registry_enabled_default and entity.enabled: + disabled_by = DISABLED_CONFIG_ENTRY + + registry.async_update_entity( + entity.registry_entry.entity_id, disabled_by=disabled_by + ) + + async_dispatcher_connect( + hass, controller.signal_options_update, update_disable_on_entities + ) update_controller() @@ -146,65 +164,20 @@ def update_items(controller, async_add_entities, tracked): """Update tracked device state from the controller.""" new_tracked = [] - if controller.option_track_clients: - - for client_id in controller.api.clients: + for items, tracker_class in ( + (controller.api.clients, UniFiClientTracker), + (controller.api.devices, UniFiDeviceTracker), + ): - if client_id in tracked: - if not tracked[client_id].enabled: - continue - 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] + for item_id in items: - if ( - not client.is_wired - and controller.option_ssid_filter - and client.essid not in controller.option_ssid_filter - ): + if item_id in tracked: + if tracked[item_id].enabled: + tracked[item_id].async_schedule_update_ha_state() continue - if not controller.option_track_wired_clients and client.is_wired: - continue - - tracked[client_id] = UniFiClientTracker(client, controller) - new_tracked.append(tracked[client_id]) - LOGGER.debug( - "New UniFi client tracker %s (%s)", - client.name or client.hostname, - client.mac, - ) - - if controller.option_track_devices: - - for device_id in controller.api.devices: - - if device_id in tracked: - if not tracked[device_id].enabled: - continue - 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 or device.model, - device.mac, - ) + tracked[item_id] = tracker_class(items[item_id], controller) + new_tracked.append(tracked[item_id]) if new_tracked: async_add_entities(new_tracked) @@ -218,8 +191,33 @@ def __init__(self, client, controller): self.client = client self.controller = controller + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + if not self.controller.option_track_clients: + return False + + if ( + not self.client.is_wired + and self.controller.option_ssid_filter + and self.client.essid not in self.controller.option_ssid_filter + ): + return False + + if not self.controller.option_track_wired_clients and self.client.is_wired: + return False + + return True + + async def async_added_to_hass(self): + """Client entity created.""" + LOGGER.debug("New UniFi client tracker %s (%s)", self.name, self.client.mac) + async def async_update(self): """Synchronize state with controller.""" + LOGGER.debug( + "Updating UniFi tracked client %s (%s)", self.entity_id, self.client.mac + ) await self.controller.request_update() @property @@ -277,8 +275,23 @@ def __init__(self, device, controller): self.device = device self.controller = controller + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + if not self.controller.option_track_devices: + return False + + return True + + async def async_added_to_hass(self): + """Subscribe to device events.""" + LOGGER.debug("New UniFi device tracker %s (%s)", self.name, self.device.mac) + async def async_update(self): """Synchronize state with controller.""" + LOGGER.debug( + "Updating UniFi tracked device %s (%s)", self.entity_id, self.device.mac + ) await self.controller.request_update() @property diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index b7bb9b730ada2f..ca4ae46f085c68 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -61,7 +61,7 @@ def update_controller(): """Update the values of the controller.""" update_items(controller, async_add_entities, switches, switches_off) - async_dispatcher_connect(hass, controller.event_update, update_controller) + async_dispatcher_connect(hass, controller.signal_update, update_controller) update_controller() switches_off.clear() @@ -220,7 +220,7 @@ def available(self): or self.client.sw_mac and ( self.controller.available - or self.client.sw_mac in self.controller.api.devices + and self.client.sw_mac in self.controller.api.devices ) ) diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 714db8604b278b..b28044bc3c7476 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -42,12 +42,12 @@ async def test_controller_setup(): { 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: [], + controller.CONF_BLOCK_CLIENT: ["mac"], + controller.CONF_DONT_TRACK_CLIENTS: True, + controller.CONF_DONT_TRACK_DEVICES: True, + controller.CONF_DONT_TRACK_WIRED_CLIENTS: True, + controller.CONF_DETECTION_TIME: 30, + controller.CONF_SSID_FILTER: ["ssid"], } ] } diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 30c2191625e10c..e099286de7d1aa 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -15,6 +15,8 @@ CONF_CONTROLLER, CONF_SITE_ID, CONF_SSID_FILTER, + CONF_TRACK_DEVICES, + CONF_TRACK_WIRED_CLIENTS, UNIFI_CONFIG, ) from homeassistant.const import ( @@ -69,10 +71,10 @@ "mac": "00:00:00:00:01:01", "model": "US16P150", "name": "device_1", - "overheating": False, + "overheating": True, "state": 1, "type": "usw", - "upgradable": False, + "upgradable": True, "version": "4.0.42.10433", } DEVICE_2 = { @@ -149,6 +151,7 @@ async def setup_controller(hass, mock_controller, options={}): system_options={}, options=options, ) + hass.config_entries._entries.append(config_entry) mock_controller.config_entry = config_entry await mock_controller.async_update() @@ -230,6 +233,25 @@ async def test_tracked_devices(hass, mock_controller): device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == STATE_UNAVAILABLE + mock_controller.config_entry.add_update_listener( + mock_controller.async_options_updated + ) + hass.config_entries.async_update_entry( + mock_controller.config_entry, + options={ + CONF_SSID_FILTER: [], + CONF_TRACK_WIRED_CLIENTS: False, + CONF_TRACK_DEVICES: False, + }, + ) + await hass.async_block_till_done() + client_1 = hass.states.get("device_tracker.client_1") + assert client_1 + client_2 = hass.states.get("device_tracker.wired_client") + assert client_2 is None + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 is None + async def test_restoring_client(hass, mock_controller): """Test the update_items function with some clients.""" From baa30aec9d903636c84b0acc41582e5197ee385a Mon Sep 17 00:00:00 2001 From: Balazs Sandor Date: Sat, 31 Aug 2019 22:29:42 +0200 Subject: [PATCH 0079/3953] Fix onvif camera setup error (#24585) * fix: onvif setup error * refactor: onvif camera init process * onvif/camera: review fixes * onvif/camera: review fixes * onvif/camera: fix pydoc * onvif: remove unrelated async-await * Onvif review fix * onvif/camera: remove log --- homeassistant/components/onvif/camera.py | 67 +++++++++++++----------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 44270e5e7e9e5e..6163453b4a527f 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -24,6 +24,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.service import extract_entity_ids +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -158,19 +159,38 @@ async def async_initialize(self): from aiohttp.client_exceptions import ClientConnectorError from homeassistant.exceptions import PlatformNotReady from zeep.exceptions import Fault - import homeassistant.util.dt as dt_util try: _LOGGER.debug("Updating service addresses") - await self._camera.update_xaddrs() - _LOGGER.debug("Setting up the ONVIF device management service") + await self.async_check_date_and_time() + await self.async_obtain_input_uri() + self.setup_ptz() + except ClientConnectorError as err: + _LOGGER.warning( + "Couldn't connect to camera '%s', but will " "retry later. Error: %s", + self._name, + err, + ) + raise PlatformNotReady + except Fault as err: + _LOGGER.error( + "Couldn't connect to camera '%s', please verify " + "that the credentials are correct. Error: %s", + self._name, + err, + ) - devicemgmt = self._camera.create_devicemgmt_service() + async def async_check_date_and_time(self): + """Warns if camera and system date not synced.""" + from aiohttp.client_exceptions import ServerDisconnectedError - _LOGGER.debug("Retrieving current camera date/time") + _LOGGER.debug("Setting up the ONVIF device management service") + devicemgmt = self._camera.create_devicemgmt_service() + _LOGGER.debug("Retrieving current camera date/time") + try: system_date = dt_util.utcnow() device_time = await devicemgmt.GetSystemDateAndTime() if device_time: @@ -201,33 +221,10 @@ async def async_initialize(self): cam_date, system_date, ) - - _LOGGER.debug("Obtaining input uri") - - await self.async_obtain_input_uri() - - _LOGGER.debug("Setting up the ONVIF PTZ service") - - if self._camera.get_service("ptz", create=False) is None: - _LOGGER.warning("PTZ is not available on this camera") - else: - self._ptz_service = self._camera.create_ptz_service() - _LOGGER.debug("Completed set up of the ONVIF camera component") - except ClientConnectorError as err: + except ServerDisconnectedError as err: _LOGGER.warning( - "Couldn't connect to camera '%s', but will " "retry later. Error: %s", - self._name, - err, - ) - raise PlatformNotReady - except Fault as err: - _LOGGER.error( - "Couldn't connect to camera '%s', please verify " - "that the credentials are correct. Error: %s", - self._name, - err, + "Couldn't get camera '%s' date/time. Error: %s", self._name, err ) - return async def async_obtain_input_uri(self): """Set the input uri for the camera.""" @@ -280,7 +277,15 @@ async def async_obtain_input_uri(self): ) except exceptions.ONVIFError as err: _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) - return + + def setup_ptz(self): + """Set up PTZ if available.""" + _LOGGER.debug("Setting up the ONVIF PTZ service") + if self._camera.get_service("ptz", create=False) is None: + _LOGGER.warning("PTZ is not available on this camera") + else: + self._ptz_service = self._camera.create_ptz_service() + _LOGGER.debug("Completed set up of the ONVIF camera component") async def async_perform_ptz(self, pan, tilt, zoom): """Perform a PTZ action on the camera.""" From b31fde6255b1ac9889af72623a5d30ad96f305c8 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Sun, 1 Sep 2019 10:20:08 +0200 Subject: [PATCH 0080/3953] Upgrade youtube_dl to 2019.09.01 (#26330) --- 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 f93fe5e77a9709..419d4b72864400 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.08.13" + "youtube_dl==2019.09.01" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 37e1e1f6474d1b..aa9daad808e43d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1971,7 +1971,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.08.13 +youtube_dl==2019.09.01 # homeassistant.components.zengge zengge==0.2 From fade2e991b9ec9666d4cffb4e9def07f60f8a07a Mon Sep 17 00:00:00 2001 From: tyjtyj Date: Sun, 1 Sep 2019 16:24:54 +0800 Subject: [PATCH 0081/3953] Fix google_maps scan interval (#26328) Reported on https://github.com/home-assistant/home-assistant/issues/26275 --- 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 2149e40e5045f5..2b5550860ee5b5 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -52,7 +52,7 @@ def __init__(self, hass, config: ConfigType, see) -> None: self.see = see self.username = config[CONF_USERNAME] self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] - self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(60) + self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=60) credfile = "{}.{}".format( hass.config.path(CREDENTIALS_FILE), slugify(self.username) From 5ba436e3d8392b17ddabb5ac34305c68df79534f Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Sun, 1 Sep 2019 01:38:58 -0700 Subject: [PATCH 0082/3953] Add a keypress service for AlarmDecoder (#26100) * Add a keypress service for AlarmDecoder (like Envisalink has) * Feedback * Import DOMAIN --- .../alarmdecoder/alarm_control_panel.py | 23 ++++++++++++++++++- .../components/alarmdecoder/services.yaml | 9 ++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 42f839bcd60d04..c05dfd30d21634 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -13,13 +13,17 @@ ) import homeassistant.helpers.config_validation as cv -from . import DATA_AD, SIGNAL_PANEL_MESSAGE +from . import DATA_AD, DOMAIN as DOMAIN_ALARMDECODER, SIGNAL_PANEL_MESSAGE _LOGGER = logging.getLogger(__name__) SERVICE_ALARM_TOGGLE_CHIME = "alarmdecoder_alarm_toggle_chime" ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string}) +SERVICE_ALARM_KEYPRESS = "alarm_keypress" +ATTR_KEYPRESS = "keypress" +ALARM_KEYPRESS_SCHEMA = vol.Schema({vol.Required(ATTR_KEYPRESS): cv.string}) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up for AlarmDecoder alarm panels.""" @@ -38,6 +42,18 @@ def alarm_toggle_chime_handler(service): schema=ALARM_TOGGLE_CHIME_SCHEMA, ) + def alarm_keypress_handler(service): + """Register keypress handler.""" + keypress = service.data[ATTR_KEYPRESS] + device.alarm_keypress(keypress) + + hass.services.register( + DOMAIN_ALARMDECODER, + SERVICE_ALARM_KEYPRESS, + alarm_keypress_handler, + schema=ALARM_KEYPRESS_SCHEMA, + ) + class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): """Representation of an AlarmDecoder-based alarm panel.""" @@ -145,3 +161,8 @@ def alarm_toggle_chime(self, code=None): """Send toggle chime command.""" if code: self.hass.data[DATA_AD].send("{!s}9".format(code)) + + def alarm_keypress(self, keypress): + """Send custom keypresses.""" + if keypress: + self.hass.data[DATA_AD].send(keypress) diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml index e69de29bb2d1d6..55451d42f13768 100644 --- a/homeassistant/components/alarmdecoder/services.yaml +++ b/homeassistant/components/alarmdecoder/services.yaml @@ -0,0 +1,9 @@ +alarm_keypress: + description: Send custom keypresses to the alarm. + fields: + entity_id: + description: Name of the alarm control panel to trigger. + example: 'alarm_control_panel.downstairs' + keypress: + description: 'String to send to the alarm panel.' + example: '*71' From 298aafc79d3aa99aaa0e00ff0520204db93b1275 Mon Sep 17 00:00:00 2001 From: Rocik Date: Sun, 1 Sep 2019 10:42:17 +0200 Subject: [PATCH 0083/3953] Add support for Supla switches (#26188) * add support for Supla switches * remove blank line at the end of file * Add comma on last element of a list * Remove unnecessary supla dependencies variable --- homeassistant/components/supla/__init__.py | 5 ++- homeassistant/components/supla/cover.py | 2 -- homeassistant/components/supla/switch.py | 38 ++++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/supla/switch.py diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 50b8e29f88e7a3..9eef9d989cb093 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -17,7 +17,10 @@ CONF_SERVER = "server" CONF_SERVERS = "servers" -SUPLA_FUNCTION_HA_CMP_MAP = {"CONTROLLINGTHEROLLERSHUTTER": "cover"} +SUPLA_FUNCTION_HA_CMP_MAP = { + "CONTROLLINGTHEROLLERSHUTTER": "cover", + "LIGHTSWITCH": "switch", +} SUPLA_CHANNELS = "supla_channels" SUPLA_SERVERS = "supla_servers" diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index 0b842bd181c5df..3182aa8c1363e7 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -5,8 +5,6 @@ from homeassistant.components.cover import ATTR_POSITION, CoverDevice from homeassistant.components.supla import SuplaChannel -DEPENDENCIES = ["supla"] - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py new file mode 100644 index 00000000000000..5e7a54699505b9 --- /dev/null +++ b/homeassistant/components/supla/switch.py @@ -0,0 +1,38 @@ +"""Support for Supla cover - curtains, rollershutters etc.""" +import logging +from pprint import pformat + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.supla import SuplaChannel + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Supla switches.""" + if discovery_info is None: + return + + _LOGGER.debug("Discovery: %s", pformat(discovery_info)) + + add_entities([SuplaSwitch(device) for device in discovery_info]) + + +class SuplaSwitch(SuplaChannel, SwitchDevice): + """Representation of a Supla Switch.""" + + def turn_on(self, **kwargs): + """Turn on the switch.""" + self.action("TURN_ON") + + def turn_off(self, **kwargs): + """Turn off the switch.""" + self.action("TURN_OFF") + + @property + def is_on(self): + """Return true if switch is on.""" + state = self.channel_data.get("state") + if state: + return state["on"] + return False From f91dd4f5f859ed6aafc7b78d41cdfcdb34347418 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 1 Sep 2019 11:45:41 +0100 Subject: [PATCH 0084/3953] Change evohome to asyncio client (#26042) * fully async now * add convergence (call update() 2 seconds after client API call) (issue#25400) * handle dead TRVs (e.g. flat battery) --- homeassistant/components/evohome/__init__.py | 146 +++++++++--------- homeassistant/components/evohome/climate.py | 94 +++++------ .../components/evohome/manifest.json | 2 +- .../components/evohome/water_heater.py | 31 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 3 - 6 files changed, 135 insertions(+), 143 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 0530878236236a..adb6e856984079 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -2,14 +2,13 @@ Such systems include evohome (multi-zone), and Round Thermostat (single zone). """ -import asyncio from datetime import datetime, timedelta import logging from typing import Any, Dict, Optional, Tuple -import requests.exceptions +import aiohttp.client_exceptions import voluptuous as vol -import evohomeclient2 +import evohomeasync2 from homeassistant.const import ( CONF_ACCESS_TOKEN, @@ -21,17 +20,10 @@ TEMP_CELSIUS, ) from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import ( - async_track_point_in_utc_time, - track_time_interval, -) from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime, utcnow @@ -81,55 +73,60 @@ def _handle_exception(err) -> bool: try: raise err - except evohomeclient2.AuthenticationError: + except evohomeasync2.AuthenticationError: _LOGGER.error( "Failed to (re)authenticate with the vendor's server. " + "Check your network and the vendor's service status page. " "Check that your username and password are correct. " "Message is: %s", err, ) return False - except requests.exceptions.ConnectionError: + except aiohttp.ClientConnectionError: # this appears to be common with Honeywell's servers _LOGGER.warning( "Unable to connect with the vendor's server. " - "Check your network and the vendor's status page." + "Check your network and the vendor's service status page. " "Message is: %s", err, ) return False - except requests.exceptions.HTTPError: - if err.response.status_code == HTTP_SERVICE_UNAVAILABLE: + except aiohttp.ClientResponseError: + if err.status == HTTP_SERVICE_UNAVAILABLE: _LOGGER.warning( - "Vendor says their server is currently unavailable. " - "Check the vendor's status page." + "The vendor says their server is currently unavailable. " + "Check the vendor's service status page." ) return False - if err.response.status_code == HTTP_TOO_MANY_REQUESTS: + if err.status == HTTP_TOO_MANY_REQUESTS: _LOGGER.warning( "The vendor's API rate limit has been exceeded. " - "Consider increasing the %s.", + "If this message persists, consider increasing the %s.", CONF_SCAN_INTERVAL, ) return False - raise # we don't expect/handle any other HTTPErrors + raise # we don't expect/handle any other ClientResponseError -def setup(hass: HomeAssistantType, hass_config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell evohome system.""" - broker = EvoBroker(hass, hass_config[DOMAIN]) - if not broker.init_client(): + broker = EvoBroker(hass, config[DOMAIN]) + if not await broker.init_client(): return False - load_platform(hass, "climate", DOMAIN, {}, hass_config) + hass.async_create_task(async_load_platform(hass, "climate", DOMAIN, {}, config)) if broker.tcs.hotwater: - load_platform(hass, "water_heater", DOMAIN, {}, hass_config) + hass.async_create_task( + async_load_platform(hass, "water_heater", DOMAIN, {}, config) + ) - track_time_interval(hass, broker.update, hass_config[DOMAIN][CONF_SCAN_INTERVAL]) + hass.helpers.event.async_track_time_interval( + broker.update, config[DOMAIN][CONF_SCAN_INTERVAL] + ) return True @@ -141,8 +138,7 @@ def __init__(self, hass, params) -> None: """Initialize the evohome client and data structure.""" self.hass = hass self.params = params - - self.config = self.status = self.timers = {} + self.config = {} self.client = self.tcs = None self._app_storage = {} @@ -150,32 +146,31 @@ def __init__(self, hass, params) -> None: hass.data[DOMAIN] = {} hass.data[DOMAIN]["broker"] = self - def init_client(self) -> bool: + async def init_client(self) -> bool: """Initialse the evohome data broker. Return True if this is successful, otherwise return False. """ - refresh_token, access_token, access_token_expires = asyncio.run_coroutine_threadsafe( - self._load_auth_tokens(), self.hass.loop - ).result() + refresh_token, access_token, access_token_expires = ( + await self._load_auth_tokens() + ) - # evohomeclient2 uses naive/local datetimes + # evohomeasync2 uses naive/local datetimes if access_token_expires is not None: access_token_expires = _utc_to_local_dt(access_token_expires) - try: - client = self.client = evohomeclient2.EvohomeClient( - self.params[CONF_USERNAME], - self.params[CONF_PASSWORD], - refresh_token=refresh_token, - access_token=access_token, - access_token_expires=access_token_expires, - ) + client = self.client = evohomeasync2.EvohomeClient( + self.params[CONF_USERNAME], + self.params[CONF_PASSWORD], + refresh_token=refresh_token, + access_token=access_token, + access_token_expires=access_token_expires, + session=async_get_clientsession(self.hass), + ) - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: + try: + await client.login() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: if not _handle_exception(err): return False @@ -200,17 +195,14 @@ def init_client(self) -> bool: return False self.tcs = ( - client.locations[loc_idx] # noqa: E501; pylint: disable=protected-access + client.locations[loc_idx] # pylint: disable=protected-access ._gateways[0] ._control_systems[0] ) _LOGGER.debug("Config = %s", self.config) - if _LOGGER.isEnabledFor(logging.DEBUG): - # don't do an I/O unless required - _LOGGER.debug( - "Status = %s", client.locations[loc_idx].status()[GWS][0][TCS][0] - ) + if _LOGGER.isEnabledFor(logging.DEBUG): # don't do an I/O unless required + await self.update() # includes: _LOGGER.debug("Status = %s"... return True @@ -237,7 +229,7 @@ async def _load_auth_tokens( return (None, None, None) # account switched: so tokens wont be valid async def _save_auth_tokens(self, *args) -> None: - # evohomeclient2 uses naive/local datetimes + # evohomeasync2 uses naive/local datetimes access_token_expires = _local_dt_to_utc(self.client.access_token_expires) self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME] @@ -248,13 +240,12 @@ async def _save_auth_tokens(self, *args) -> None: store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) await store.async_save(self._app_storage) - async_track_point_in_utc_time( - self.hass, + self.hass.helpers.event.async_track_point_in_utc_time( self._save_auth_tokens, access_token_expires + self.params[CONF_SCAN_INTERVAL], ) - def update(self, *args, **kwargs) -> None: + async def update(self, *args, **kwargs) -> None: """Get the latest state data of the entire evohome Location. This includes state data for the Controller and all its child devices, @@ -264,19 +255,16 @@ def update(self, *args, **kwargs) -> None: loc_idx = self.params[CONF_LOCATION_IDX] try: - status = self.client.locations[loc_idx].status()[GWS][0][TCS][0] - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: + status = await self.client.locations[loc_idx].status() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: _handle_exception(err) else: - self.timers["statusUpdated"] = utcnow() - - _LOGGER.debug("Status = %s", status) - # inform the evohome devices that state data has been updated - async_dispatcher_send(self.hass, DOMAIN, {"signal": "refresh"}) + self.hass.helpers.dispatcher.async_dispatcher_send( + DOMAIN, {"signal": "refresh"} + ) + + _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) class EvoDevice(Entity): @@ -289,6 +277,7 @@ class EvoDevice(Entity): def __init__(self, evo_broker, evo_device) -> None: """Initialize the evohome entity.""" self._evo_device = evo_device + self._evo_broker = evo_broker self._evo_tcs = evo_broker.tcs self._name = self._icon = self._precision = None @@ -387,7 +376,7 @@ def supported_features(self) -> int: async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + self.hass.helpers.dispatcher.async_dispatcher_connect(DOMAIN, self._refresh) @property def precision(self) -> float: @@ -399,14 +388,27 @@ def temperature_unit(self) -> str: """Return the temperature unit to use in the frontend UI.""" return TEMP_CELSIUS - def _update_schedule(self) -> None: + async def _call_client_api(self, api_function) -> None: + try: + await api_function + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: + _handle_exception(err) + + self.hass.helpers.event.async_call_later( + 2, self._evo_broker.update() + ) # call update() in 2 seconds + + async def _update_schedule(self) -> None: """Get the latest state data.""" if ( not self._schedule.get("DailySchedules") or parse_datetime(self.setpoints["next"]["from"]) < utcnow() ): - self._schedule = self._evo_device.schedule() + try: + self._schedule = await self._evo_device.schedule() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: + _handle_exception(err) - def update(self) -> None: + async def async_update(self) -> None: """Get the latest state data.""" - self._update_schedule() + await self._update_schedule() diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index d1b9d5f54c799a..0264f76f38f5db 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -3,9 +3,6 @@ import logging from typing import Any, Dict, Optional, List -import requests.exceptions -import evohomeclient2 - from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, @@ -25,7 +22,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import CONF_LOCATION_IDX, _handle_exception, EvoDevice +from . import CONF_LOCATION_IDX, EvoDevice from .const import ( DOMAIN, EVO_RESET, @@ -65,10 +62,13 @@ HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items()} -def setup_platform( - hass: HomeAssistantType, hass_config: ConfigType, add_entities, discovery_info=None +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Create the evohome Controller, and its Zones, if any.""" + if discovery_info is None: + return + broker = hass.data[DOMAIN]["broker"] loc_idx = broker.params[CONF_LOCATION_IDX] @@ -91,7 +91,7 @@ def setup_platform( zone.name, ) - add_entities([EvoThermostat(broker, zone)], update_before_add=True) + async_add_entities([EvoThermostat(broker, zone)], update_before_add=True) return controller = EvoController(broker, broker.tcs) @@ -107,7 +107,7 @@ def setup_platform( ) zones.append(EvoZone(broker, zone)) - add_entities([controller] + zones, update_before_add=True) + async_add_entities([controller] + zones, update_before_add=True) class EvoClimateDevice(EvoDevice, ClimateDevice): @@ -119,22 +119,18 @@ def __init__(self, evo_broker, evo_device) -> None: self._preset_modes = None - def _set_temperature( + async def _set_temperature( self, temperature: float, until: Optional[datetime] = None ) -> None: """Set a new target temperature for the Zone. until == None means indefinitely (i.e. PermanentOverride) """ - try: + await self._call_client_api( self._evo_device.set_temperature(temperature, until) - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: - _handle_exception(err) + ) - def _set_zone_mode(self, op_mode: str) -> None: + async def _set_zone_mode(self, op_mode: str) -> None: """Set a Zone to one of its native EVO_* operating modes. Zones inherit their _effective_ operating mode from the Controller. @@ -153,35 +149,24 @@ def _set_zone_mode(self, op_mode: str) -> None: (by default) 5C, and 'Away', Zones to (by default) 12C. """ if op_mode == EVO_FOLLOW: - try: - self._evo_device.cancel_temp_override() - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: - _handle_exception(err) + await self._call_client_api(self._evo_device.cancel_temp_override()) return temperature = self._evo_device.setpointStatus["targetHeatTemperature"] until = None # EVO_PERMOVER if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: - self._update_schedule() + await self._update_schedule() if self._schedule["DailySchedules"]: until = parse_datetime(self.setpoints["next"]["from"]) - self._set_temperature(temperature, until=until) + await self._set_temperature(temperature, until=until) - def _set_tcs_mode(self, op_mode: str) -> None: + async def _set_tcs_mode(self, op_mode: str) -> None: """Set the Controller to any of its native EVO_* operating modes.""" - try: - # noqa: E501; pylint: disable=protected-access - self._evo_tcs._set_status(op_mode) - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: - _handle_exception(err) + await self._call_client_api( + self._evo_tcs._set_status(op_mode) # pylint: disable=protected-access + ) @property def hvac_modes(self) -> List[str]: @@ -216,6 +201,11 @@ def __init__(self, evo_broker, evo_device) -> None: self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE self._preset_modes = list(HA_PRESET_TO_EVO) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._evo_device.temperatureStatus["isAvailable"] + @property def hvac_mode(self) -> str: """Return the current operating mode of the evohome Zone.""" @@ -276,28 +266,28 @@ def max_temp(self) -> float: """ return self._evo_device.setpointCapabilities["maxHeatSetpoint"] - def set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs) -> None: """Set a new target temperature.""" until = kwargs.get("until") if until: until = parse_datetime(until) - self._set_temperature(kwargs["temperature"], until) + await self._set_temperature(kwargs["temperature"], until) - def set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set an operating mode for the Zone.""" if hvac_mode == HVAC_MODE_OFF: - self._set_temperature(self.min_temp, until=None) + await self._set_temperature(self.min_temp, until=None) else: # HVAC_MODE_HEAT - self._set_zone_mode(EVO_FOLLOW) + await self._set_zone_mode(EVO_FOLLOW) - def set_preset_mode(self, preset_mode: Optional[str]) -> None: + async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: """Set a new preset mode. If preset_mode is None, then revert to following the schedule. """ - self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) + await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) class EvoController(EvoClimateDevice): @@ -344,25 +334,25 @@ def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - def set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs) -> None: """Do nothing. The evohome Controller doesn't have a target temperature. """ return - def set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set an operating mode for the Controller.""" - self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) + await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) - def set_preset_mode(self, preset_mode: Optional[str]) -> None: + async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: """Set a new preset mode. If preset_mode is None, then revert to 'Auto' mode. """ - self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) + await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) - def update(self) -> None: + async def async_update(self) -> None: """Get the latest state data.""" return @@ -409,16 +399,16 @@ def preset_mode(self) -> Optional[str]: return super().preset_mode - def set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set an operating mode.""" - self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) + await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) - def set_preset_mode(self, preset_mode: Optional[str]) -> None: + async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: """Set a new preset mode. If preset_mode is None, then revert to following the schedule. """ if preset_mode in list(HA_PRESET_TO_TCS): - self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode)) + await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode)) else: - self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) + await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 078d4ace776cf1..32a57cf20b1c77 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -3,7 +3,7 @@ "name": "Evohome", "documentation": "https://www.home-assistant.io/components/evohome", "requirements": [ - "evohomeclient==0.3.3" + "evohome-async==0.3.3b4" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 6309f07a000c74..1b37bc3b2b58c2 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -2,9 +2,6 @@ import logging from typing import List -import requests.exceptions -import evohomeclient2 - from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, WaterHeaterDevice, @@ -12,7 +9,7 @@ from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON from homeassistant.util.dt import parse_datetime -from . import _handle_exception, EvoDevice +from . import EvoDevice from .const import DOMAIN, EVO_STRFTIME, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER _LOGGER = logging.getLogger(__name__) @@ -23,8 +20,13 @@ HA_OPMODE_TO_DHW = {STATE_ON: EVO_FOLLOW, STATE_OFF: EVO_PERMOVER} -def setup_platform(hass, hass_config, add_entities, discovery_info=None) -> None: +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Create the DHW controller.""" + if discovery_info is None: + return + broker = hass.data[DOMAIN]["broker"] _LOGGER.debug( @@ -33,7 +35,7 @@ def setup_platform(hass, hass_config, add_entities, discovery_info=None) -> None evo_dhw = EvoDHW(broker, broker.tcs.hotwater) - add_entities([evo_dhw], update_before_add=True) + async_add_entities([evo_dhw], update_before_add=True) class EvoDHW(EvoDevice, WaterHeaterDevice): @@ -58,6 +60,11 @@ def __init__(self, evo_broker, evo_device) -> None: self._supported_features = SUPPORT_OPERATION_MODE self._operation_list = list(HA_OPMODE_TO_DHW) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._evo_device.temperatureStatus.get("isAvailable", False) + @property def current_operation(self) -> str: """Return the current operating mode (On, or Off).""" @@ -73,7 +80,7 @@ def current_temperature(self) -> float: """Return the current temperature.""" return self._evo_device.temperatureStatus["temperature"] - def set_operation_mode(self, operation_mode: str) -> None: + async def async_set_operation_mode(self, operation_mode: str) -> None: """Set new operation mode for a DHW controller.""" op_mode = HA_OPMODE_TO_DHW[operation_mode] @@ -81,17 +88,13 @@ def set_operation_mode(self, operation_mode: str) -> None: until = None # EVO_FOLLOW, EVO_PERMOVER if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: - self._update_schedule() + await self._update_schedule() if self._schedule["DailySchedules"]: until = parse_datetime(self.setpoints["next"]["from"]) until = until.strftime(EVO_STRFTIME) data = {"Mode": op_mode, "State": state, "UntilTime": until} - try: + await self._call_client_api( self._evo_device._set_dhw(data) # pylint: disable=protected-access - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: - _handle_exception(err) + ) diff --git a/requirements_all.txt b/requirements_all.txt index aa9daad808e43d..76ffb996fa59c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -461,7 +461,7 @@ eternalegypt==0.0.10 # evdev==0.6.1 # homeassistant.components.evohome -evohomeclient==0.3.3 +evohome-async==0.3.3b4 # homeassistant.components.dlib_face_detect # homeassistant.components.dlib_face_identify diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35125387e89e8b..2111149cb55bfc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -123,9 +123,6 @@ enocean==0.50 # homeassistant.components.season ephem==3.7.6.0 -# homeassistant.components.evohome -evohomeclient==0.3.3 - # homeassistant.components.feedreader feedparser-homeassistant==5.2.2.dev1 From a80d26f0dcd714156839e53cc485509aebc92599 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Sun, 1 Sep 2019 13:10:58 +0200 Subject: [PATCH 0085/3953] Upgrade sqlalchemy to 1.3.8 (#26331) --- 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 c91b910724c5ff..9ecfa88053fa69 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.7" + "sqlalchemy==1.3.8" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index a489e3fd736764..38a320543a9cca 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.7" + "sqlalchemy==1.3.8" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a1ffd515c5bfa4..5754e3fc391d6d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ pytz>=2019.02 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.7 +sqlalchemy==1.3.8 voluptuous-serialize==2.2.0 voluptuous==0.11.7 zeroconf==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index 76ffb996fa59c6..7ce5937ff13c8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1774,7 +1774,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.7 +sqlalchemy==1.3.8 # homeassistant.components.srp_energy srpenergy==1.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2111149cb55bfc..f7727bd459b532 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -382,7 +382,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.7 +sqlalchemy==1.3.8 # homeassistant.components.srp_energy srpenergy==1.0.6 From 6102eb9f1ce2acffbe10e9386567f55c4b537609 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 1 Sep 2019 13:15:48 +0200 Subject: [PATCH 0086/3953] Migrate Axis, deCONZ and UniFi to use config entry subclass (#26173) * Use init_subclass for Config Entries * Pylint cant handle subclass being the only user of imports --- homeassistant/components/axis/config_flow.py | 3 +-- homeassistant/components/deconz/config_flow.py | 3 +-- homeassistant/components/unifi/config_flow.py | 5 ++--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 4b54982244be65..f93e49d9818c25 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -56,8 +56,7 @@ def configured_devices(hass): } -@config_entries.HANDLERS.register(DOMAIN) -class AxisFlowHandler(config_entries.ConfigFlow): +class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Axis config flow.""" VERSION = 1 diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 306a4fbf839243..60d47a0a4e22b6 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -43,8 +43,7 @@ def get_master_gateway(hass): return gateway -@config_entries.HANDLERS.register(DOMAIN) -class DeconzFlowHandler(config_entries.ConfigFlow): +class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a deCONZ config flow.""" VERSION = 1 diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index e1f0a91c774eb1..c885f30af231e4 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -11,7 +11,7 @@ CONF_VERIFY_SSL, ) -from .const import ( +from .const import ( # pylint: disable=unused-import CONF_CONTROLLER, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, @@ -33,8 +33,7 @@ DEFAULT_VERIFY_SSL = False -@config_entries.HANDLERS.register(DOMAIN) -class UnifiFlowHandler(config_entries.ConfigFlow): +class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a UniFi config flow.""" VERSION = 1 From 597cd3e88643690ea72a04d344eda3119cdeeaba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 1 Sep 2019 15:22:50 +0300 Subject: [PATCH 0087/3953] Upgrade tibber library (#26332) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 1985a85999b14a..d0f358c5902bec 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "name": "Tibber", "documentation": "https://www.home-assistant.io/components/tibber", "requirements": [ - "pyTibber==0.11.6" + "pyTibber==0.11.7" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7ce5937ff13c8a..61a2d46482e72b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1029,7 +1029,7 @@ pyRFXtrx==0.23 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.11.6 +pyTibber==0.11.7 # homeassistant.components.dlink pyW215==0.6.0 From 5b77a357e6698fe15534772f420389d77c4c5dca Mon Sep 17 00:00:00 2001 From: fmartens <17504441+fmartens@users.noreply.github.com> Date: Sun, 1 Sep 2019 17:52:43 +0200 Subject: [PATCH 0088/3953] Inverted rflink cover (#26038) * Added InvertedRflinkCover class to support COCO/KAKU ASUN-650 devices * Rename TYPE_NORMAL to TYPE_STANDARD * Cleaning up code and removed unused imports * Added unit tests for InvertedRflinkCover * less if/else statements * Autoresolve type for newkaku * Updated tests for InvertedRflinkCover * Added unit test for standard cover without specifying type * Updated comments in unit tests * Updated unit test configuration and comments to be more explanatory * Restore variable names in first part of the unit test that have been changed during a search and replace * Reformated the code according to 4de97ab * remove blank lines at end of rflink test_cover.py * Replace single with double quote in test_cover.py * Replaced single quotes with double qoutes and fixed formatting * Black improvements * Reformated the code of the unit test. * entity_type_for_device_id should return 'TYPE_STANDARD' instead of 'None' --- homeassistant/components/rflink/cover.py | 56 ++- tests/components/rflink/test_cover.py | 412 +++++++++++++++++++++++ 2 files changed, 466 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index 7e6de0ec03b5dc..f41c4cde2f7d3f 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -4,7 +4,7 @@ import voluptuous as vol from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice -from homeassistant.const import CONF_NAME, STATE_OPEN +from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_OPEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -23,6 +23,8 @@ _LOGGER = logging.getLogger(__name__) +TYPE_STANDARD = "standard" +TYPE_INVERTED = "inverted" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -33,6 +35,7 @@ { cv.string: { vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_TYPE): vol.Any(TYPE_STANDARD, TYPE_INVERTED), vol.Optional(CONF_ALIASES, default=[]): vol.All( cv.ensure_list, [cv.string] ), @@ -52,12 +55,51 @@ ) +def entity_type_for_device_id(device_id): + """Return entity class for protocol of a given device_id. + + Async friendly. + """ + entity_type_mapping = { + # KlikAanKlikUit cover have the controls inverted + "newkaku": TYPE_INVERTED + } + protocol = device_id.split("_")[0] + return entity_type_mapping.get(protocol, TYPE_STANDARD) + + +def entity_class_for_type(entity_type): + """Translate entity type to entity class. + + Async friendly. + """ + entity_device_mapping = { + # default cover implementation + TYPE_STANDARD: RflinkCover, + # cover with open/close commands inverted + # like KAKU/COCO ASUN-650 + TYPE_INVERTED: InvertedRflinkCover, + } + + return entity_device_mapping.get(entity_type, RflinkCover) + + def devices_from_config(domain_config): """Parse configuration and add Rflink cover devices.""" devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): + # Determine what kind of entity to create, RflinkCover + # or InvertedRflinkCover + if CONF_TYPE in config: + # Remove type from config to not pass it as and argument + # to entity instantiation + entity_type = config.pop(CONF_TYPE) + else: + entity_type = entity_type_for_device_id(device_id) + + entity_class = entity_class_for_type(entity_type) device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) - device = RflinkCover(device_id, **device_config) + device = entity_class(device_id, **device_config) devices.append(device) return devices @@ -115,3 +157,13 @@ def async_open_cover(self, **kwargs): def async_stop_cover(self, **kwargs): """Turn the device stop.""" return self._async_handle_command("stop_cover") + + +class InvertedRflinkCover(RflinkCover): + """Rflink cover that has inverted open/close commands.""" + + async def _async_send_command(self, cmd, repetitions): + """Will invert only the UP/DOWN commands.""" + _LOGGER.debug("Getting command: %s for Rflink device: %s", cmd, self._device_id) + cmd_inv = {"UP": "DOWN", "DOWN": "UP"} + await super()._async_send_command(cmd_inv.get(cmd, cmd), repetitions) diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py index e4b3154a4c43c3..858258e7efd0c4 100644 --- a/tests/components/rflink/test_cover.py +++ b/tests/components/rflink/test_cover.py @@ -390,3 +390,415 @@ async def test_restore_state(hass, monkeypatch): assert state assert state.state == STATE_CLOSED assert state.attributes["assumed_state"] + + +# The code checks the ID, it will use the +# 'inverted' class when the name starts with +# 'newkaku' +async def test_inverted_cover(hass, monkeypatch): + """Ensure states are restored on startup.""" + config = { + "rflink": {"port": "/dev/ttyABC0"}, + DOMAIN: { + "platform": "rflink", + "devices": { + "nonkaku_device_1": { + "name": "nonkaku_type_standard", + "type": "standard", + }, + "nonkaku_device_2": {"name": "nonkaku_type_none"}, + "nonkaku_device_3": { + "name": "nonkaku_type_inverted", + "type": "inverted", + }, + "newkaku_device_4": { + "name": "newkaku_type_standard", + "type": "standard", + }, + "newkaku_device_5": {"name": "newkaku_type_none"}, + "newkaku_device_6": { + "name": "newkaku_type_inverted", + "type": "inverted", + }, + }, + }, + } + + # setup mocking rflink module + event_callback, _, protocol, _ = await mock_rflink( + hass, config, DOMAIN, monkeypatch + ) + + # test default state of cover loaded from config + standard_cover = hass.states.get(DOMAIN + ".nonkaku_type_standard") + assert standard_cover.state == STATE_CLOSED + assert standard_cover.attributes["assumed_state"] + + # mock incoming up command event for nonkaku_device_1 + event_callback({"id": "nonkaku_device_1", "command": "up"}) + await hass.async_block_till_done() + + standard_cover = hass.states.get(DOMAIN + ".nonkaku_type_standard") + assert standard_cover.state == STATE_OPEN + assert standard_cover.attributes.get("assumed_state") + + # mock incoming up command event for nonkaku_device_2 + event_callback({"id": "nonkaku_device_2", "command": "up"}) + await hass.async_block_till_done() + + standard_cover = hass.states.get(DOMAIN + ".nonkaku_type_none") + assert standard_cover.state == STATE_OPEN + assert standard_cover.attributes.get("assumed_state") + + # mock incoming up command event for nonkaku_device_3 + event_callback({"id": "nonkaku_device_3", "command": "up"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".nonkaku_type_inverted") + assert inverted_cover.state == STATE_OPEN + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming up command event for newkaku_device_4 + event_callback({"id": "newkaku_device_4", "command": "up"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_standard") + assert inverted_cover.state == STATE_OPEN + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming up command event for newkaku_device_5 + event_callback({"id": "newkaku_device_5", "command": "up"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_none") + assert inverted_cover.state == STATE_OPEN + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming up command event for newkaku_device_6 + event_callback({"id": "newkaku_device_6", "command": "up"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_inverted") + assert inverted_cover.state == STATE_OPEN + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming down command event for nonkaku_device_1 + event_callback({"id": "nonkaku_device_1", "command": "down"}) + + await hass.async_block_till_done() + + standard_cover = hass.states.get(DOMAIN + ".nonkaku_type_standard") + assert standard_cover.state == STATE_CLOSED + assert standard_cover.attributes.get("assumed_state") + + # mock incoming down command event for nonkaku_device_2 + event_callback({"id": "nonkaku_device_2", "command": "down"}) + + await hass.async_block_till_done() + + standard_cover = hass.states.get(DOMAIN + ".nonkaku_type_none") + assert standard_cover.state == STATE_CLOSED + assert standard_cover.attributes.get("assumed_state") + + # mock incoming down command event for nonkaku_device_3 + event_callback({"id": "nonkaku_device_3", "command": "down"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".nonkaku_type_inverted") + assert inverted_cover.state == STATE_CLOSED + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming down command event for newkaku_device_4 + event_callback({"id": "newkaku_device_4", "command": "down"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_standard") + assert inverted_cover.state == STATE_CLOSED + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming down command event for newkaku_device_5 + event_callback({"id": "newkaku_device_5", "command": "down"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_none") + assert inverted_cover.state == STATE_CLOSED + assert inverted_cover.attributes.get("assumed_state") + + # mock incoming down command event for newkaku_device_6 + event_callback({"id": "newkaku_device_6", "command": "down"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_inverted") + assert inverted_cover.state == STATE_CLOSED + assert inverted_cover.attributes.get("assumed_state") + + # We are only testing the 'inverted' devices, the 'standard' devices + # are already covered by other test cases. + + # should respond to group command + event_callback({"id": "nonkaku_device_3", "command": "alloff"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".nonkaku_type_inverted") + assert inverted_cover.state == STATE_CLOSED + + # should respond to group command + event_callback({"id": "nonkaku_device_3", "command": "allon"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".nonkaku_type_inverted") + assert inverted_cover.state == STATE_OPEN + + # should respond to group command + event_callback({"id": "newkaku_device_4", "command": "alloff"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_standard") + assert inverted_cover.state == STATE_CLOSED + + # should respond to group command + event_callback({"id": "newkaku_device_4", "command": "allon"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_standard") + assert inverted_cover.state == STATE_OPEN + + # should respond to group command + event_callback({"id": "newkaku_device_5", "command": "alloff"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_none") + assert inverted_cover.state == STATE_CLOSED + + # should respond to group command + event_callback({"id": "newkaku_device_5", "command": "allon"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_none") + assert inverted_cover.state == STATE_OPEN + + # should respond to group command + event_callback({"id": "newkaku_device_6", "command": "alloff"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_inverted") + assert inverted_cover.state == STATE_CLOSED + + # should respond to group command + event_callback({"id": "newkaku_device_6", "command": "allon"}) + + await hass.async_block_till_done() + + inverted_cover = hass.states.get(DOMAIN + ".newkaku_type_inverted") + assert inverted_cover.state == STATE_OPEN + + # Sending the close command from HA should result + # in an 'DOWN' command sent to a non-newkaku device + # that has its type set to 'standard'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_standard"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_standard").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[0][0][0] == "nonkaku_device_1" + assert protocol.send_command_ack.call_args_list[0][0][1] == "DOWN" + + # Sending the open command from HA should result + # in an 'UP' command sent to a non-newkaku device + # that has its type set to 'standard'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_standard"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_standard").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[1][0][0] == "nonkaku_device_1" + assert protocol.send_command_ack.call_args_list[1][0][1] == "UP" + + # Sending the close command from HA should result + # in an 'DOWN' command sent to a non-newkaku device + # that has its type not specified. + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_none"} + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_none").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[2][0][0] == "nonkaku_device_2" + assert protocol.send_command_ack.call_args_list[2][0][1] == "DOWN" + + # Sending the open command from HA should result + # in an 'UP' command sent to a non-newkaku device + # that has its type not specified. + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_none"} + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_none").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[3][0][0] == "nonkaku_device_2" + assert protocol.send_command_ack.call_args_list[3][0][1] == "UP" + + # Sending the close command from HA should result + # in an 'UP' command sent to a non-newkaku device + # that has its type set to 'inverted'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_inverted"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_inverted").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[4][0][0] == "nonkaku_device_3" + assert protocol.send_command_ack.call_args_list[4][0][1] == "UP" + + # Sending the open command from HA should result + # in an 'DOWN' command sent to a non-newkaku device + # that has its type set to 'inverted'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".nonkaku_type_inverted"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".nonkaku_type_inverted").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[5][0][0] == "nonkaku_device_3" + assert protocol.send_command_ack.call_args_list[5][0][1] == "DOWN" + + # Sending the close command from HA should result + # in an 'DOWN' command sent to a newkaku device + # that has its type set to 'standard'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_standard"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_standard").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[6][0][0] == "newkaku_device_4" + assert protocol.send_command_ack.call_args_list[6][0][1] == "DOWN" + + # Sending the open command from HA should result + # in an 'UP' command sent to a newkaku device + # that has its type set to 'standard'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_standard"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_standard").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[7][0][0] == "newkaku_device_4" + assert protocol.send_command_ack.call_args_list[7][0][1] == "UP" + + # Sending the close command from HA should result + # in an 'UP' command sent to a newkaku device + # that has its type not specified. + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_none"} + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_none").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[8][0][0] == "newkaku_device_5" + assert protocol.send_command_ack.call_args_list[8][0][1] == "UP" + + # Sending the open command from HA should result + # in an 'DOWN' command sent to a newkaku device + # that has its type not specified. + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_none"} + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_none").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[9][0][0] == "newkaku_device_5" + assert protocol.send_command_ack.call_args_list[9][0][1] == "DOWN" + + # Sending the close command from HA should result + # in an 'UP' command sent to a newkaku device + # that has its type set to 'inverted'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_inverted"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_inverted").state == STATE_CLOSED + assert protocol.send_command_ack.call_args_list[10][0][0] == "newkaku_device_6" + assert protocol.send_command_ack.call_args_list[10][0][1] == "UP" + + # Sending the open command from HA should result + # in an 'DOWN' command sent to a newkaku device + # that has its type set to 'inverted'. + hass.async_create_task( + hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: DOMAIN + ".newkaku_type_inverted"}, + ) + ) + + await hass.async_block_till_done() + + assert hass.states.get(DOMAIN + ".newkaku_type_inverted").state == STATE_OPEN + assert protocol.send_command_ack.call_args_list[11][0][0] == "newkaku_device_6" + assert protocol.send_command_ack.call_args_list[11][0][1] == "DOWN" From b5426761f46ea02d3eee30ba5c1b2ddc5343c2d6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 1 Sep 2019 17:57:25 +0200 Subject: [PATCH 0089/3953] UniFi - Simplify getting controller from config entry (#26335) * Simplify getting controller from config entry * Lint ignore no longer needed * Fix tests --- homeassistant/components/unifi/__init__.py | 15 +++++-------- homeassistant/components/unifi/config_flow.py | 22 ++++++++++++++++--- .../components/unifi/device_tracker.py | 9 ++------ homeassistant/components/unifi/switch.py | 11 ++-------- tests/components/unifi/test_device_tracker.py | 3 ++- tests/components/unifi/test_init.py | 8 +++++-- tests/components/unifi/test_switch.py | 3 ++- 7 files changed, 38 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 5ad60ddd835af8..db6358285296a3 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,6 +1,9 @@ """Support for devices connected to UniFi POE.""" import voluptuous as vol +from homeassistant.components.unifi.config_flow import ( + get_controller_id_from_config_entry, +) from homeassistant.const import CONF_HOST from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -9,14 +12,12 @@ from .const import ( ATTR_MANUFACTURER, 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, DOMAIN, UNIFI_CONFIG, ) @@ -70,10 +71,7 @@ async def async_setup_entry(hass, config_entry): controller = UniFiController(hass, config_entry) - controller_id = CONTROLLER_ID.format( - host=config_entry.data[CONF_CONTROLLER][CONF_HOST], - site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], - ) + controller_id = get_controller_id_from_config_entry(config_entry) hass.data[DOMAIN][controller_id] = controller @@ -98,9 +96,6 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - controller_id = CONTROLLER_ID.format( - host=config_entry.data[CONF_CONTROLLER][CONF_HOST], - site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], - ) + controller_id = get_controller_id_from_config_entry(config_entry) controller = hass.data[DOMAIN].pop(controller_id) return await controller.async_reset() diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index c885f30af231e4..00b003746a2568 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -11,13 +11,14 @@ CONF_VERIFY_SSL, ) -from .const import ( # pylint: disable=unused-import +from .const import ( CONF_CONTROLLER, + CONF_DETECTION_TIME, + CONF_SITE_ID, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, - CONF_DETECTION_TIME, - CONF_SITE_ID, + CONTROLLER_ID, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, @@ -33,6 +34,21 @@ DEFAULT_VERIFY_SSL = False +@callback +def get_controller_id_from_config_entry(config_entry): + """Return controller with a matching bridge id.""" + return CONTROLLER_ID.format( + host=config_entry.data[CONF_CONTROLLER][CONF_HOST], + site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], + ) + + +@callback +def get_controller_from_config_entry(hass, config_entry): + """Return controller with a matching bridge id.""" + return hass.data[DOMAIN][get_controller_id_from_config_entry(config_entry)] + + class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a UniFi config flow.""" diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index c4451546776b2c..1f4aef754adc0b 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import unifi +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER @@ -29,7 +29,6 @@ ATTR_MANUFACTURER, CONF_CONTROLLER, CONF_SITE_ID, - CONTROLLER_ID, DOMAIN as UNIFI_DOMAIN, ) @@ -106,11 +105,7 @@ async def async_setup_scanner(hass, config, sync_see, discovery_info): async def async_setup_entry(hass, config_entry, async_add_entities): """Set up device tracker for UniFi component.""" - controller_id = CONTROLLER_ID.format( - host=config_entry.data[CONF_CONTROLLER][CONF_HOST], - site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], - ) - controller = hass.data[unifi.DOMAIN][controller_id] + controller = get_controller_from_config_entry(hass, config_entry) tracked = {} registry = await entity_registry.async_get_registry(hass) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index ca4ae46f085c68..f46a55f671ea4f 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,17 +1,14 @@ """Support for devices connected to UniFi POE.""" import logging -from homeassistant.components import unifi +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_HOST from homeassistant.core import callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_CONTROLLER, CONF_SITE_ID, CONTROLLER_ID - LOGGER = logging.getLogger(__name__) @@ -25,11 +22,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Switches are controlling network switch ports with Poe. """ - controller_id = CONTROLLER_ID.format( - host=config_entry.data[CONF_CONTROLLER][CONF_HOST], - site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], - ) - controller = hass.data[unifi.DOMAIN][controller_id] + controller = get_controller_from_config_entry(hass, config_entry) if controller.site_role != "admin": return diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index e099286de7d1aa..437fabf9689469 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -17,6 +17,7 @@ CONF_SSID_FILTER, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, + CONTROLLER_ID as CONF_CONTROLLER_ID, UNIFI_CONFIG, ) from homeassistant.const import ( @@ -101,7 +102,7 @@ ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} -CONTROLLER_ID = unifi.CONTROLLER_ID.format(host="mock-host", site="mock-site") +CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") @pytest.fixture diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index b725e34f61dc65..ffd6d97e5b3bcd 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -4,7 +4,11 @@ from homeassistant.components import unifi from homeassistant.components.unifi import config_flow from homeassistant.setup import async_setup_component -from homeassistant.components.unifi.const import CONF_CONTROLLER, CONF_SITE_ID +from homeassistant.components.unifi.const import ( + CONF_CONTROLLER, + CONF_SITE_ID, + CONTROLLER_ID as CONF_CONTROLLER_ID, +) from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -113,7 +117,7 @@ async def test_controller_fail_setup(hass): mock_cntrlr.return_value.async_setup.return_value = mock_coro(False) assert await unifi.async_setup_entry(hass, entry) is False - controller_id = unifi.CONTROLLER_ID.format(host="0.0.0.0", site="default") + controller_id = CONF_CONTROLLER_ID.format(host="0.0.0.0", site="default") assert controller_id in hass.data[unifi.DOMAIN] diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 3ac9ddb17dc61b..e660e57fc671d5 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -15,6 +15,7 @@ from homeassistant.components.unifi.const import ( CONF_CONTROLLER, CONF_SITE_ID, + CONTROLLER_ID as CONF_CONTROLLER_ID, UNIFI_CONFIG, ) from homeassistant.helpers import entity_registry @@ -213,7 +214,7 @@ ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} -CONTROLLER_ID = unifi.CONTROLLER_ID.format(host="mock-host", site="mock-site") +CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") @pytest.fixture From 1617c2cd647d5d17eb7701e241ad1f80cfca6c1e Mon Sep 17 00:00:00 2001 From: Aleix Murtra Date: Sun, 1 Sep 2019 18:05:46 +0200 Subject: [PATCH 0090/3953] Add BeeWi SmartClim BLE sensors (#26174) * Add BeeWi SmartClim BLE temperature and humidity sensor * Update missing CODEOWNERS and .coveragerc files * Updated requirements file * Update documentation * Fixed requested changes and decoupled IO library * Add unique_id property * Improve unique_id --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/beewi_smartclim/__init__.py | 1 + .../components/beewi_smartclim/manifest.json | 12 ++ .../components/beewi_smartclim/sensor.py | 108 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 126 insertions(+) create mode 100644 homeassistant/components/beewi_smartclim/__init__.py create mode 100644 homeassistant/components/beewi_smartclim/manifest.json create mode 100644 homeassistant/components/beewi_smartclim/sensor.py diff --git a/.coveragerc b/.coveragerc index 6b239402cb187f..df87a5a1f71d70 100644 --- a/.coveragerc +++ b/.coveragerc @@ -57,6 +57,7 @@ omit = homeassistant/components/avion/light.py homeassistant/components/azure_event_hub/* homeassistant/components/baidu/tts.py + homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bbb_gpio/* homeassistant/components/bbox/device_tracker.py homeassistant/components/bbox/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 623d6ca9eec328..7c2e69c9af19d8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -37,6 +37,7 @@ homeassistant/components/awair/* @danielsjf homeassistant/components/aws/* @awarecan @robbiet480 homeassistant/components/axis/* @kane610 homeassistant/components/azure_event_hub/* @eavanvalkenburg +homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blink/* @fronzbot diff --git a/homeassistant/components/beewi_smartclim/__init__.py b/homeassistant/components/beewi_smartclim/__init__.py new file mode 100644 index 00000000000000..f907ce95ae6390 --- /dev/null +++ b/homeassistant/components/beewi_smartclim/__init__.py @@ -0,0 +1 @@ +"""The beewi_smartclim component.""" diff --git a/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant/components/beewi_smartclim/manifest.json new file mode 100644 index 00000000000000..3e9ad732b74342 --- /dev/null +++ b/homeassistant/components/beewi_smartclim/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "beewi_smartclim", + "name": "BeeWi SmartClim BLE sensor", + "documentation": "https://www.home-assistant.io/components/beewi_smartclim", + "requirements": [ + "beewi_smartclim==0.0.7" + ], + "dependencies": [], + "codeowners": [ + "@alemuro" + ] +} \ No newline at end of file diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py new file mode 100644 index 00000000000000..7bfa8883013724 --- /dev/null +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -0,0 +1,108 @@ +"""Platform for beewi_smartclim integration.""" +import logging + +from beewi_smartclim import BeewiSmartClimPoller +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_NAME, + CONF_MAC, + TEMP_CELSIUS, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_BATTERY, +) +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +# Default values +DEFAULT_NAME = "BeeWi SmartClim" + +# Sensor config +SENSOR_TYPES = [ + [DEVICE_CLASS_TEMPERATURE, "Temperature", TEMP_CELSIUS], + [DEVICE_CLASS_HUMIDITY, "Humidity", "%"], + [DEVICE_CLASS_BATTERY, "Battery", "%"], +] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the beewi_smartclim platform.""" + + mac = config[CONF_MAC] + prefix = config[CONF_NAME] + poller = BeewiSmartClimPoller(mac) + + sensors = [] + + for sensor_type in SENSOR_TYPES: + device = sensor_type[0] + name = sensor_type[1] + unit = sensor_type[2] + # `prefix` is the name configured by the user for the sensor, we're appending + # the device type at the end of the name (garden -> garden temperature) + if prefix: + name = f"{prefix} {name}" + + sensors.append(BeewiSmartclimSensor(poller, name, mac, device, unit)) + + add_entities(sensors) + + +class BeewiSmartclimSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, poller, name, mac, device, unit): + """Initialize the sensor.""" + self._poller = poller + self._name = name + self._mac = mac + self._device = device + self._unit = unit + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor. State is returned in Celsius.""" + return self._state + + @property + def device_class(self): + """Device class of this entity.""" + return self._device + + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return f"{self._mac}_{self._device}" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + def update(self): + """Fetch new state data from the poller.""" + self._poller.update_sensor() + self._state = None + if self._device == DEVICE_CLASS_TEMPERATURE: + self._state = self._poller.get_temperature() + if self._device == DEVICE_CLASS_HUMIDITY: + self._state = self._poller.get_humidity() + if self._device == DEVICE_CLASS_BATTERY: + self._state = self._poller.get_battery() diff --git a/requirements_all.txt b/requirements_all.txt index 61a2d46482e72b..b58f15ef2851b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -262,6 +262,9 @@ batinfo==0.4.2 # homeassistant.components.sytadin beautifulsoup4==4.8.0 +# homeassistant.components.beewi_smartclim +beewi_smartclim==0.0.7 + # homeassistant.components.zha bellows-homeassistant==0.9.1 From 79488daddfe5855b802d4e181b91cb0de15177a9 Mon Sep 17 00:00:00 2001 From: Steven Rollason <2099542+gadgetchnnel@users.noreply.github.com> Date: Sun, 1 Sep 2019 17:12:55 +0100 Subject: [PATCH 0091/3953] New template sensor attributes (#26127) * updated sensor and test files * Formatting fixes * Updated attribute template code * Black formatting * Code improvements based on feedback on binary_sensor pull request * Updated tests * Remove duplicated code and fix tests * Black formatting on tests * Remove link from docstring * Moved default to schema * Formatting fix and change to use dict[key] to retrieve attribute_templates --- homeassistant/components/template/sensor.py | 54 +++++++++++--- tests/components/template/test_sensor.py | 82 ++++++++++++++++++++- 2 files changed, 123 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a5397e0ea7d353..b77528e0c324a7 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -1,6 +1,7 @@ """Allows the creation of a sensor that breaks out state_attributes.""" import logging from typing import Optional +from itertools import chain import voluptuous as vol @@ -28,6 +29,8 @@ from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.event import async_track_state_change +CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" + _LOGGER = logging.getLogger(__name__) SENSOR_SCHEMA = vol.Schema( @@ -36,6 +39,9 @@ vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template, + vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( + {cv.string: cv.template} + ), vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, @@ -60,17 +66,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) device_class = device_config.get(CONF_DEVICE_CLASS) + attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES] entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) invalid_templates = [] - for tpl_name, template in ( - (CONF_VALUE_TEMPLATE, state_template), - (CONF_ICON_TEMPLATE, icon_template), - (CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template), - (CONF_FRIENDLY_NAME_TEMPLATE, friendly_name_template), - ): + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_FRIENDLY_NAME_TEMPLATE: friendly_name_template, + } + + for tpl_name, template in chain(templates.items(), attribute_templates.items()): if template is None: continue template.hass = hass @@ -82,7 +91,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) @@ -113,6 +122,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_picture_template, entity_ids, device_class, + attribute_templates, ) ) if not sensors: @@ -138,6 +148,7 @@ def __init__( entity_picture_template, entity_ids, device_class, + attribute_templates, ): """Initialize the sensor.""" self.hass = hass @@ -155,6 +166,8 @@ def __init__( self._entity_picture = None self._entities = entity_ids self._device_class = device_class + self._attribute_templates = attribute_templates + self._attributes = {} async def async_added_to_hass(self): """Register callbacks.""" @@ -209,6 +222,11 @@ def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" return self._unit_of_measurement + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + @property def should_poll(self): """No polling needed.""" @@ -229,11 +247,23 @@ async def async_update(self): else: self._state = None _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), - ("_name", self._friendly_name_template), - ): + + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + "_name": self._friendly_name_template, + } + + attrs = {} + 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_sensor.py b/tests/components/template/test_sensor.py index 298efc6bebbd43..9223399bee7af1 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -174,6 +174,38 @@ def test_friendly_name_template_with_unknown_state(self): state = self.hass.states.get("sensor.test_template_sensor") assert state.attributes["friendly_name"] == "It Works." + def test_attribute_templates(self): + """Test attribute_templates template.""" + with assert_setup_component(1): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.test_state.state }}", + "attribute_templates": { + "test_attribute": "It {{ states.sensor.test_state.state }}." + }, + } + }, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get("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("sensor.test_template_sensor") + assert state.attributes["test_attribute"] == "It Works." + def test_template_syntax_error(self): """Test templating syntax error.""" with assert_setup_component(0): @@ -345,6 +377,34 @@ def test_setup_valid_device_class(self): assert "device_class" not in state.attributes +async def test_invalid_attribute_template(hass, caplog): + """Test that errors are logged if rendering template fails.""" + hass.states.async_set("sensor.test_sensor", "startup") + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "invalid_template": { + "value_template": "{{ states.sensor.test_sensor.state }}", + "attribute_templates": { + "test_attribute": "{{ states.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("sensor.invalid_template") + + assert ("Error rendering attribute test_attribute") in caplog.text + + async def test_no_template_match_all(hass, caplog): """Test that we do not allow sensors that match on all.""" hass.states.async_set("sensor.test_sensor", "startup") @@ -369,12 +429,22 @@ async def test_no_template_match_all(hass, caplog): "value_template": "{{ states.sensor.test_sensor.state }}", "friendly_name_template": "{{ 1 + 1 }}", }, + "invalid_attribute": { + "value_template": "{{ states.sensor.test_sensor.state }}", + "attribute_templates": {"test_attribute": "{{ 1 + 1 }}"}, + }, }, } }, ) + + assert hass.states.get("sensor.invalid_state").state == "unknown" + assert hass.states.get("sensor.invalid_icon").state == "unknown" + assert hass.states.get("sensor.invalid_entity_picture").state == "unknown" + assert hass.states.get("sensor.invalid_friendly_name").state == "unknown" + await hass.async_block_till_done() - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 6 assert ( "Template sensor invalid_state has no entity ids " "configured to track nor were we able to extract the entities to " @@ -395,11 +465,17 @@ async def test_no_template_match_all(hass, caplog): "configured to track nor were we able to extract the entities to " "track from the friendly_name template" ) in caplog.text + assert ( + "Template sensor invalid_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("sensor.invalid_state").state == "unknown" assert hass.states.get("sensor.invalid_icon").state == "unknown" assert hass.states.get("sensor.invalid_entity_picture").state == "unknown" assert hass.states.get("sensor.invalid_friendly_name").state == "unknown" + assert hass.states.get("sensor.invalid_attribute").state == "unknown" hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -408,6 +484,7 @@ async def test_no_template_match_all(hass, caplog): assert hass.states.get("sensor.invalid_icon").state == "startup" assert hass.states.get("sensor.invalid_entity_picture").state == "startup" assert hass.states.get("sensor.invalid_friendly_name").state == "startup" + assert hass.states.get("sensor.invalid_attribute").state == "startup" hass.states.async_set("sensor.test_sensor", "hello") await hass.async_block_till_done() @@ -416,6 +493,7 @@ async def test_no_template_match_all(hass, caplog): assert hass.states.get("sensor.invalid_icon").state == "startup" assert hass.states.get("sensor.invalid_entity_picture").state == "startup" assert hass.states.get("sensor.invalid_friendly_name").state == "startup" + assert hass.states.get("sensor.invalid_attribute").state == "startup" await hass.helpers.entity_component.async_update_entity("sensor.invalid_state") await hass.helpers.entity_component.async_update_entity("sensor.invalid_icon") @@ -425,8 +503,10 @@ async def test_no_template_match_all(hass, caplog): await hass.helpers.entity_component.async_update_entity( "sensor.invalid_friendly_name" ) + await hass.helpers.entity_component.async_update_entity("sensor.invalid_attribute") assert hass.states.get("sensor.invalid_state").state == "2" assert hass.states.get("sensor.invalid_icon").state == "hello" assert hass.states.get("sensor.invalid_entity_picture").state == "hello" assert hass.states.get("sensor.invalid_friendly_name").state == "hello" + assert hass.states.get("sensor.invalid_attribute").state == "hello" From 3aa272971616d82d98456572559d307869e42701 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 Sep 2019 06:40:18 +0200 Subject: [PATCH 0092/3953] Add improvements of device_automation from frontend PR 3514 (#26295) * Add improvements from frontend PR 3514 * Fix test * Tweak --- homeassistant/components/automation/manifest.json | 1 + homeassistant/components/device_automation/__init__.py | 2 +- homeassistant/components/light/strings.json | 8 ++++++++ tests/components/device_automation/test_init.py | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/light/strings.json diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index ea63d4ff98a31c..935cc7a9175058 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -4,6 +4,7 @@ "documentation": "https://www.home-assistant.io/components/automation", "requirements": [], "dependencies": [ + "device_automation", "group", "webhook" ], diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index b1f319b0a6ae7c..018e1286d8bc53 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -75,7 +75,7 @@ async def async_get_device_automation_triggers(hass, device_id): @websocket_api.async_response @websocket_api.websocket_command( { - vol.Required("type"): "device_automation/list_triggers", + vol.Required("type"): "device_automation/trigger/list", vol.Required("device_id"): str, } ) diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json new file mode 100644 index 00000000000000..94954bb790b198 --- /dev/null +++ b/homeassistant/components/light/strings.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_on": "{name} turned on", + "turn_off": "{name} turned off" + } + } +} diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 55367e7696c480..16320257b40bc2 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -62,7 +62,7 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r await client.send_json( { "id": 1, - "type": "device_automation/list_triggers", + "type": "device_automation/trigger/list", "device_id": device_entry.id, } ) From aa7513bc5ce8b83762c3998ec4b2718763dd4d7a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Sep 2019 21:42:57 -0700 Subject: [PATCH 0093/3953] Expose current direction properly on state machine (#26298) * Expose current direction properly on state machine * Fix template fan --- homeassistant/components/demo/fan.py | 8 ++++---- homeassistant/components/fan/__init__.py | 2 +- homeassistant/components/template/fan.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index cdeed5dbfec692..ab8a6f3fae9dc6 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -34,13 +34,13 @@ def __init__(self, hass, name: str, supported_features: int) -> None: self._supported_features = supported_features self._speed = STATE_OFF self.oscillating = None - self.direction = None + self._direction = None self._name = name if supported_features & SUPPORT_OSCILLATE: self.oscillating = False if supported_features & SUPPORT_DIRECTION: - self.direction = "forward" + self._direction = "forward" @property def name(self) -> str: @@ -80,7 +80,7 @@ def set_speed(self, speed: str) -> None: def set_direction(self, direction: str) -> None: """Set the direction of the fan.""" - self.direction = direction + self._direction = direction self.schedule_update_ha_state() def oscillate(self, oscillating: bool) -> None: @@ -91,7 +91,7 @@ def oscillate(self, oscillating: bool) -> None: @property def current_direction(self) -> str: """Fan direction.""" - return self.direction + return self._direction @property def supported_features(self) -> int: diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index f5edfe5bb5996e..50d698f733656f 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -54,7 +54,7 @@ "speed": ATTR_SPEED, "speed_list": ATTR_SPEED_LIST, "oscillating": ATTR_OSCILLATING, - "direction": ATTR_DIRECTION, + "current_direction": ATTR_DIRECTION, } # type: dict FAN_SET_SPEED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index c3d5a4d878fd9e..7fd8c4d9b3cea6 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -243,7 +243,7 @@ def oscillating(self): return self._oscillating @property - def direction(self): + def current_direction(self): """Return the oscillation state.""" return self._direction From 1b13c4954149eec7e5211934114ba4e9bb265f43 Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Mon, 2 Sep 2019 06:44:50 +0200 Subject: [PATCH 0094/3953] added missing bluepy dependency for miflora (#26297) fixes dependency issue #19362 (only part of the solution) --- homeassistant/components/miflora/manifest.json | 1 + requirements_all.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index d4e7a333acf287..c7ef2b89611c43 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -3,6 +3,7 @@ "name": "Miflora", "documentation": "https://www.home-assistant.io/components/miflora", "requirements": [ + "bluepy==1.1.4", "miflora==0.4.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index b58f15ef2851b7..5a9860b7c6f2eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -287,6 +287,7 @@ blinkstick==1.1.8 blockchain==1.4.4 # homeassistant.components.decora +# homeassistant.components.miflora # bluepy==1.1.4 # homeassistant.components.bme680 From 385a4969449232c3b34b1cc8ecd4cfba5bb4b998 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Sep 2019 22:30:09 -0700 Subject: [PATCH 0095/3953] Update translations --- .../components/adguard/.translations/pl.json | 6 ++--- .../ambiclimate/.translations/pl.json | 4 ++-- .../arcam_fmj/.translations/ca.json | 5 ++++ .../arcam_fmj/.translations/da.json | 5 ++++ .../arcam_fmj/.translations/nl.json | 5 ++++ .../arcam_fmj/.translations/no.json | 5 ++++ .../arcam_fmj/.translations/pl.json | 5 ++++ .../arcam_fmj/.translations/ru.json | 5 ++++ .../arcam_fmj/.translations/zh-Hant.json | 5 ++++ .../cert_expiry/.translations/ca.json | 24 +++++++++++++++++++ .../cert_expiry/.translations/da.json | 24 +++++++++++++++++++ .../cert_expiry/.translations/hu.json | 12 ++++++++++ .../cert_expiry/.translations/nl.json | 24 +++++++++++++++++++ .../cert_expiry/.translations/no.json | 24 +++++++++++++++++++ .../cert_expiry/.translations/pl.json | 24 +++++++++++++++++++ .../cert_expiry/.translations/ru.json | 24 +++++++++++++++++++ .../cert_expiry/.translations/zh-Hant.json | 24 +++++++++++++++++++ .../components/deconz/.translations/ca.json | 11 +++++++++ .../components/deconz/.translations/da.json | 11 +++++++++ .../components/deconz/.translations/nl.json | 11 +++++++++ .../components/deconz/.translations/no.json | 11 +++++++++ .../components/deconz/.translations/pl.json | 15 ++++++++++-- .../components/deconz/.translations/ru.json | 11 +++++++++ .../deconz/.translations/zh-Hant.json | 11 +++++++++ .../emulated_roku/.translations/no.json | 2 +- .../esphome/.translations/zh-Hant.json | 2 +- .../geonetnz_quakes/.translations/ca.json | 17 +++++++++++++ .../geonetnz_quakes/.translations/da.json | 17 +++++++++++++ .../geonetnz_quakes/.translations/hu.json | 12 ++++++++++ .../geonetnz_quakes/.translations/nl.json | 17 +++++++++++++ .../geonetnz_quakes/.translations/pl.json | 17 +++++++++++++ .../geonetnz_quakes/.translations/ru.json | 17 +++++++++++++ .../.translations/zh-Hant.json | 17 +++++++++++++ .../components/heos/.translations/pl.json | 2 +- .../homekit_controller/.translations/pl.json | 4 ++-- .../components/life360/.translations/ca.json | 1 + .../components/life360/.translations/da.json | 1 + .../components/life360/.translations/hu.json | 7 ++++++ .../components/life360/.translations/nl.json | 1 + .../components/life360/.translations/pl.json | 1 + .../components/life360/.translations/ru.json | 1 + .../life360/.translations/zh-Hant.json | 1 + .../components/light/.translations/en.json | 8 +++++++ .../logi_circle/.translations/pl.json | 8 +++---- .../components/met/.translations/hu.json | 9 +++++++ .../components/nest/.translations/ca.json | 2 +- .../nest/.translations/zh-Hant.json | 2 +- .../components/notion/.translations/hu.json | 18 ++++++++++++++ .../point/.translations/zh-Hant.json | 2 +- .../components/ps4/.translations/pl.json | 2 +- .../toon/.translations/zh-Hant.json | 2 +- .../components/traccar/.translations/ca.json | 18 ++++++++++++++ .../components/traccar/.translations/da.json | 18 ++++++++++++++ .../components/traccar/.translations/nl.json | 18 ++++++++++++++ .../components/traccar/.translations/pl.json | 18 ++++++++++++++ .../components/traccar/.translations/ru.json | 18 ++++++++++++++ .../traccar/.translations/zh-Hant.json | 18 ++++++++++++++ .../twentemilieu/.translations/ca.json | 23 ++++++++++++++++++ .../twentemilieu/.translations/da.json | 23 ++++++++++++++++++ .../twentemilieu/.translations/hu.json | 18 ++++++++++++++ .../twentemilieu/.translations/nl.json | 23 ++++++++++++++++++ .../twentemilieu/.translations/pl.json | 23 ++++++++++++++++++ .../twentemilieu/.translations/ru.json | 23 ++++++++++++++++++ .../twentemilieu/.translations/zh-Hant.json | 23 ++++++++++++++++++ .../components/unifi/.translations/ca.json | 12 ++++++++++ .../components/unifi/.translations/da.json | 12 ++++++++++ .../components/unifi/.translations/nl.json | 12 ++++++++++ .../components/unifi/.translations/pl.json | 20 ++++++++++++++++ .../components/unifi/.translations/ru.json | 12 ++++++++++ .../unifi/.translations/zh-Hant.json | 12 ++++++++++ .../components/velbus/.translations/ca.json | 21 ++++++++++++++++ .../components/velbus/.translations/da.json | 21 ++++++++++++++++ .../components/velbus/.translations/hu.json | 10 ++++++++ .../components/velbus/.translations/nl.json | 21 ++++++++++++++++ .../components/velbus/.translations/pl.json | 21 ++++++++++++++++ .../components/velbus/.translations/ru.json | 21 ++++++++++++++++ .../velbus/.translations/zh-Hant.json | 21 ++++++++++++++++ .../components/vesync/.translations/ca.json | 20 ++++++++++++++++ .../components/vesync/.translations/da.json | 20 ++++++++++++++++ .../components/vesync/.translations/hu.json | 16 +++++++++++++ .../components/vesync/.translations/nl.json | 20 ++++++++++++++++ .../components/vesync/.translations/pl.json | 20 ++++++++++++++++ .../components/vesync/.translations/ru.json | 20 ++++++++++++++++ .../vesync/.translations/zh-Hant.json | 20 ++++++++++++++++ .../components/withings/.translations/en.json | 17 +++++++++++++ .../components/wwlln/.translations/hu.json | 12 ++++++++++ 86 files changed, 1120 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/ca.json create mode 100644 homeassistant/components/arcam_fmj/.translations/da.json create mode 100644 homeassistant/components/arcam_fmj/.translations/nl.json create mode 100644 homeassistant/components/arcam_fmj/.translations/no.json create mode 100644 homeassistant/components/arcam_fmj/.translations/pl.json create mode 100644 homeassistant/components/arcam_fmj/.translations/ru.json create mode 100644 homeassistant/components/arcam_fmj/.translations/zh-Hant.json create mode 100644 homeassistant/components/cert_expiry/.translations/ca.json create mode 100644 homeassistant/components/cert_expiry/.translations/da.json create mode 100644 homeassistant/components/cert_expiry/.translations/hu.json create mode 100644 homeassistant/components/cert_expiry/.translations/nl.json create mode 100644 homeassistant/components/cert_expiry/.translations/no.json create mode 100644 homeassistant/components/cert_expiry/.translations/pl.json create mode 100644 homeassistant/components/cert_expiry/.translations/ru.json create mode 100644 homeassistant/components/cert_expiry/.translations/zh-Hant.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/ca.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/da.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/hu.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/nl.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/pl.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/ru.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json create mode 100644 homeassistant/components/life360/.translations/hu.json create mode 100644 homeassistant/components/light/.translations/en.json create mode 100644 homeassistant/components/met/.translations/hu.json create mode 100644 homeassistant/components/notion/.translations/hu.json create mode 100644 homeassistant/components/traccar/.translations/ca.json create mode 100644 homeassistant/components/traccar/.translations/da.json create mode 100644 homeassistant/components/traccar/.translations/nl.json create mode 100644 homeassistant/components/traccar/.translations/pl.json create mode 100644 homeassistant/components/traccar/.translations/ru.json create mode 100644 homeassistant/components/traccar/.translations/zh-Hant.json create mode 100644 homeassistant/components/twentemilieu/.translations/ca.json create mode 100644 homeassistant/components/twentemilieu/.translations/da.json create mode 100644 homeassistant/components/twentemilieu/.translations/hu.json create mode 100644 homeassistant/components/twentemilieu/.translations/nl.json create mode 100644 homeassistant/components/twentemilieu/.translations/pl.json create mode 100644 homeassistant/components/twentemilieu/.translations/ru.json create mode 100644 homeassistant/components/twentemilieu/.translations/zh-Hant.json create mode 100644 homeassistant/components/velbus/.translations/ca.json create mode 100644 homeassistant/components/velbus/.translations/da.json create mode 100644 homeassistant/components/velbus/.translations/hu.json create mode 100644 homeassistant/components/velbus/.translations/nl.json create mode 100644 homeassistant/components/velbus/.translations/pl.json create mode 100644 homeassistant/components/velbus/.translations/ru.json create mode 100644 homeassistant/components/velbus/.translations/zh-Hant.json create mode 100644 homeassistant/components/vesync/.translations/ca.json create mode 100644 homeassistant/components/vesync/.translations/da.json create mode 100644 homeassistant/components/vesync/.translations/hu.json create mode 100644 homeassistant/components/vesync/.translations/nl.json create mode 100644 homeassistant/components/vesync/.translations/pl.json create mode 100644 homeassistant/components/vesync/.translations/ru.json create mode 100644 homeassistant/components/vesync/.translations/zh-Hant.json create mode 100644 homeassistant/components/withings/.translations/en.json create mode 100644 homeassistant/components/wwlln/.translations/hu.json diff --git a/homeassistant/components/adguard/.translations/pl.json b/homeassistant/components/adguard/.translations/pl.json index 199b621c81b7a0..e58c901f3643f4 100644 --- a/homeassistant/components/adguard/.translations/pl.json +++ b/homeassistant/components/adguard/.translations/pl.json @@ -5,11 +5,11 @@ "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home." }, "error": { - "connection_error": "Po\u0142\u0105czenie nieudane." + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." }, "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant'a, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?", + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?", "title": "AdGuard Home przez dodatek Hass.io" }, "user": { @@ -21,7 +21,7 @@ "username": "Nazwa u\u017cytkownika", "verify_ssl": "AdGuard Home u\u017cywa odpowiedniego certyfikatu." }, - "description": "Skonfiguruj swoj\u0105 instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i nadz\u00f3r sieci.", + "description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119.", "title": "Po\u0142\u0105cz sw\u00f3j AdGuard Home" } }, diff --git a/homeassistant/components/ambiclimate/.translations/pl.json b/homeassistant/components/ambiclimate/.translations/pl.json index dac6e52dda2af5..47e9c9f35b2897 100644 --- a/homeassistant/components/ambiclimate/.translations/pl.json +++ b/homeassistant/components/ambiclimate/.translations/pl.json @@ -3,14 +3,14 @@ "abort": { "access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu.", "already_setup": "Konto Ambiclimate jest skonfigurowane.", - "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 w nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Ambiclimate" }, "error": { "follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku Prze\u015blij", - "no_token": "Nie uwierzytelniony z Ambiclimate" + "no_token": "Nieuwierzytelniony z Ambiclimate" }, "step": { "auth": { diff --git a/homeassistant/components/arcam_fmj/.translations/ca.json b/homeassistant/components/arcam_fmj/.translations/ca.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/ca.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/da.json b/homeassistant/components/arcam_fmj/.translations/da.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/da.json @@ -0,0 +1,5 @@ +{ + "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 new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/nl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/no.json b/homeassistant/components/arcam_fmj/.translations/no.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/no.json @@ -0,0 +1,5 @@ +{ + "config": { + "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 new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/pl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/ru.json b/homeassistant/components/arcam_fmj/.translations/ru.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/ru.json @@ -0,0 +1,5 @@ +{ + "config": { + "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 new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/zh-Hant.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/ca.json b/homeassistant/components/cert_expiry/.translations/ca.json new file mode 100644 index 00000000000000..25c0b26fafc285 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada" + }, + "error": { + "certificate_fetch_failed": "No s'ha pogut obtenir el certificat des d'aquesta combinaci\u00f3 d'amfitri\u00f3 i port", + "connection_timeout": "S'ha acabat el temps d'espera durant la connexi\u00f3 amb l'amfitri\u00f3.", + "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada", + "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Nom d'amfitri\u00f3 del certificat", + "name": "Nom del certificat", + "port": "Port del certificat" + }, + "title": "Configuraci\u00f3 del certificat a provar" + } + }, + "title": "Caducitat del certificat" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/da.json b/homeassistant/components/cert_expiry/.translations/da.json new file mode 100644 index 00000000000000..667ab5fa4e3d01 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/da.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret" + }, + "error": { + "certificate_fetch_failed": "Kan ikke hente certifikat fra denne v\u00e6rt- og portkombination", + "connection_timeout": "Timeout ved tilslutning til denne v\u00e6rt", + "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret", + "resolve_failed": "V\u00e6rten kunne ikke findes" + }, + "step": { + "user": { + "data": { + "host": "Certifikatets v\u00e6rtsnavn", + "name": "Certifikatets navn", + "port": "Certifikatets port" + }, + "title": "Definer certifikatet, der skal testes" + } + }, + "title": "Certifikat udl\u00f8b" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/hu.json b/homeassistant/components/cert_expiry/.translations/hu.json new file mode 100644 index 00000000000000..584f4c2b75961b --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "A tan\u00fas\u00edtv\u00e1ny neve", + "port": "A tan\u00fas\u00edtv\u00e1ny portja" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/nl.json b/homeassistant/components/cert_expiry/.translations/nl.json new file mode 100644 index 00000000000000..d2fe3c76e85001 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd" + }, + "error": { + "certificate_fetch_failed": "Kan certificaat niet ophalen van deze combinatie van host en poort", + "connection_timeout": "Timeout bij verbinding maken met deze host", + "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd", + "resolve_failed": "Deze host kon niet gevonden worden" + }, + "step": { + "user": { + "data": { + "host": "De hostnaam van het certificaat", + "name": "De naam van het certificaat", + "port": "De poort van het certificaat" + }, + "title": "Het certificaat defini\u00ebren dat moet worden getest" + } + }, + "title": "Vervaldatum certificaat" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/no.json b/homeassistant/components/cert_expiry/.translations/no.json new file mode 100644 index 00000000000000..e095cc360a0f4e --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert" + }, + "error": { + "certificate_fetch_failed": "Kan ikke hente sertifikat fra denne verts- og portkombinasjonen", + "connection_timeout": "Timeout n\u00e5r det kobles til denne verten", + "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert", + "resolve_failed": "Denne verten kan ikke l\u00f8ses" + }, + "step": { + "user": { + "data": { + "host": "Sertifikatets vertsnavn", + "name": "Sertifikatets navn", + "port": "Sertifikatets port" + }, + "title": "Definer sertifikatet som skal testes" + } + }, + "title": "Sertifikat utl\u00f8p" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/pl.json b/homeassistant/components/cert_expiry/.translations/pl.json new file mode 100644 index 00000000000000..162c8bf8a0aae8 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana" + }, + "error": { + "certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu", + "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z tym hostem", + "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", + "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta certyfikatu", + "name": "Nazwa certyfikatu", + "port": "Port certyfikatu" + }, + "title": "Zdefiniuj certyfikat do przetestowania" + } + }, + "title": "Wa\u017cno\u015b\u0107 certyfikatu" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json new file mode 100644 index 00000000000000..6a795dee13e26b --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430" + }, + "error": { + "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430", + "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443", + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", + "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442" + }, + "step": { + "user": { + "data": { + "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", + "port": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + }, + "title": "C\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + } + }, + "title": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hant.json b/homeassistant/components/cert_expiry/.translations/zh-Hant.json new file mode 100644 index 00000000000000..9af730db969f12 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "certificate_fetch_failed": "\u7121\u6cd5\u81ea\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u7372\u5f97\u8a8d\u8b49", + "connection_timeout": "\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef\u903e\u6642", + "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "resolve_failed": "\u4e3b\u6a5f\u7aef\u7121\u6cd5\u89e3\u6790" + }, + "step": { + "user": { + "data": { + "host": "\u8a8d\u8b49\u4e3b\u6a5f\u7aef\u540d\u7a31", + "name": "\u8a8d\u8b49\u540d\u7a31", + "port": "\u8a8d\u8b49\u901a\u8a0a\u57e0" + }, + "title": "\u5b9a\u7fa9\u8a8d\u8b49\u9032\u884c\u6e2c\u8a66" + } + }, + "title": "\u8a8d\u8b49\u5df2\u904e\u671f" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 7b69b7477f59c2..56ae59c78ba031 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -40,5 +40,16 @@ } }, "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Permet sensors deCONZ CLIP", + "allow_deconz_groups": "Permet grups de llums deCONZ" + }, + "description": "Configura la visibilitat dels tipus de dispositius deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index f79538ffeb6935..3c8c0377880d75 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -40,5 +40,16 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Tillad deCONZ CLIP sensorer", + "allow_deconz_groups": "Tillad deCONZ lys grupper" + }, + "description": "Konfigurer synligheden af deCONZ-enhedstyper" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 19477bbed3f33c..785fba4ffc0e02 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -40,5 +40,16 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "DeCONZ CLIP sensoren toestaan", + "allow_deconz_groups": "DeCONZ-lichtgroepen toestaan" + }, + "description": "De zichtbaarheid van deCONZ-apparaattypen configureren" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 7c674c71022fc0..8798248224a582 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -40,5 +40,16 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Tillat deCONZ CLIP-sensorer", + "allow_deconz_groups": "Tillat deCONZ lys grupper" + }, + "description": "Konfigurere synlighet av deCONZ enhetstyper" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index a17835f79a3020..0f2009a46b687b 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -15,9 +15,9 @@ "hassio_confirm": { "data": { "allow_clip_sensor": "Zezwalaj na importowanie wirtualnych sensor\u00f3w", - "allow_deconz_groups": "Zezw\u00f3l na importowanie grup deCONZ" + "allow_deconz_groups": "Zezwalaj na importowanie grup deCONZ" }, - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant'a, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek Hass.io {addon}?", + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek Hass.io {addon}?", "title": "Bramka deCONZ Zigbee przez dodatek Hass.io" }, "init": { @@ -40,5 +40,16 @@ } }, "title": "Brama deCONZ Zigbee" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Zezwalaj na czujniki deCONZ CLIP", + "allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ" + }, + "description": "Skonfiguruj widoczno\u015b\u0107 urz\u0105dze\u0144 deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index ea701b3f759434..ee7208cdf1731a 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -40,5 +40,16 @@ } }, "title": "deCONZ" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 deCONZ CLIP", + "allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ" + } + } } } \ 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 0173c90c3b7e5b..53d6f76a60161a 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -40,5 +40,16 @@ } }, "title": "deCONZ Zigbee \u9598\u9053\u5668" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", + "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" + }, + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/no.json b/homeassistant/components/emulated_roku/.translations/no.json index e83497599ca4cd..b41da3ccde394a 100644 --- a/homeassistant/components/emulated_roku/.translations/no.json +++ b/homeassistant/components/emulated_roku/.translations/no.json @@ -11,7 +11,7 @@ "host_ip": "Vert IP", "listen_port": "Lytte port", "name": "Navn", - "upnp_bind_multicast": "Bind multicast (True/False)" + "upnp_bind_multicast": "Bind multicast (Sant/Usant)" }, "title": "Definer serverkonfigurasjon" } diff --git a/homeassistant/components/esphome/.translations/zh-Hant.json b/homeassistant/components/esphome/.translations/zh-Hant.json index 74d0b925fb2380..0386fd8c468572 100644 --- a/homeassistant/components/esphome/.translations/zh-Hant.json +++ b/homeassistant/components/esphome/.translations/zh-Hant.json @@ -18,7 +18,7 @@ "title": "\u8f38\u5165\u5bc6\u78bc" }, "discovery_confirm": { - "description": "\u662f\u5426\u8981\u5c07 ESPHome \u7bc0\u9ede\u300c{name}\u300d\u65b0\u589e\u81f3 Home Assistant\uff1f", + "description": "\u662f\u5426\u8981\u5c07 ESPHome \u7bc0\u9ede `{name}` \u65b0\u589e\u81f3 Home Assistant\uff1f", "title": "\u767c\u73fe\u5230 ESPHome \u7bc0\u9ede" }, "user": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/ca.json b/homeassistant/components/geonetnz_quakes/.translations/ca.json new file mode 100644 index 00000000000000..57ce2b4ee81bf3 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Ubicaci\u00f3 ja registrada" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radi" + }, + "title": "Introdueix els detalls del filtre." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/da.json b/homeassistant/components/geonetnz_quakes/.translations/da.json new file mode 100644 index 00000000000000..0d0e927bc4be4e --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/da.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Placering allerede registreret" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "Udfyld dine filteroplysninger." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/hu.json b/homeassistant/components/geonetnz_quakes/.translations/hu.json new file mode 100644 index 00000000000000..42de5a1314239b --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "radius": "Sug\u00e1r" + }, + "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/nl.json b/homeassistant/components/geonetnz_quakes/.translations/nl.json new file mode 100644 index 00000000000000..d6af28240eb3fa --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Locatie al geregistreerd" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Straal" + }, + "title": "Vul uw filtergegevens in." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/pl.json b/homeassistant/components/geonetnz_quakes/.translations/pl.json new file mode 100644 index 00000000000000..427c753f6c1a01 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/pl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokalizacja ju\u017c zarejestrowana" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Promie\u0144" + }, + "title": "Wype\u0142nij szczeg\u00f3\u0142y dotycz\u0105ce filtra." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json new file mode 100644 index 00000000000000..7d6583bc1d5b59 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "GeoNet" + } + }, + "title": "\u0417\u0435\u043c\u043b\u0435\u0442\u0440\u044f\u0441\u0435\u043d\u0438\u044f \u0432 \u041d\u043e\u0432\u043e\u0439 \u0417\u0435\u043b\u0430\u043d\u0434\u0438\u0438 (GeoNet)" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json new file mode 100644 index 00000000000000..59b4abf259a12d --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "\u5ea7\u6a19\u5df2\u8a3b\u518a" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "\u534a\u5f91" + }, + "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/pl.json b/homeassistant/components/heos/.translations/pl.json index 9b5f9844ddc9e5..d427acc3a986f5 100644 --- a/homeassistant/components/heos/.translations/pl.json +++ b/homeassistant/components/heos/.translations/pl.json @@ -12,7 +12,7 @@ "access_token": "Host", "host": "Host" }, - "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP urz\u0105dzenia Heos (preferowane po\u0142\u0105czenie kablowe, nie WiFi).", + "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP urz\u0105dzenia Heos (najlepiej pod\u0142\u0105czonego przewodowo do sieci).", "title": "Po\u0142\u0105cz si\u0119 z Heos" } }, diff --git a/homeassistant/components/homekit_controller/.translations/pl.json b/homeassistant/components/homekit_controller/.translations/pl.json index 031a7440ed0129..e66353c5000d75 100644 --- a/homeassistant/components/homekit_controller/.translations/pl.json +++ b/homeassistant/components/homekit_controller/.translations/pl.json @@ -13,8 +13,8 @@ "authentication_error": "Niepoprawny kod parowania HomeKit. Sprawd\u017a go i spr\u00f3buj ponownie.", "busy_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c jest ju\u017c powi\u0105zane z innym kontrolerem.", "max_peers_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c nie ma wolnej pami\u0119ci parowania.", - "max_tries_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c otrzyma\u0142o ponad 100 nieudanych pr\u00f3b uwierzytelnienia.", - "pairing_failed": "Wyst\u0105pi\u0142 nieobs\u0142ugiwany b\u0142\u0105d podczas pr\u00f3by sparowania z tym urz\u0105dzeniem. Mo\u017ce to by\u0107 tymczasowa awaria lub Twoje urz\u0105dzenie mo\u017ce nie by\u0107 obecnie obs\u0142ugiwane.", + "max_tries_error": "Urz\u0105dzenie odm\u00f3wi\u0142o dodania parowania, poniewa\u017c otrzyma\u0142o ponad 100 nieudanych pr\u00f3b uwierzytelnienia.", + "pairing_failed": "Wyst\u0105pi\u0142 nieobs\u0142ugiwany b\u0142\u0105d podczas pr\u00f3by sparowania z tym urz\u0105dzeniem. Mo\u017ce to by\u0107 tymczasowa awaria lub urz\u0105dzenie mo\u017ce nie by\u0107 obecnie obs\u0142ugiwane.", "unable_to_pair": "Nie mo\u017cna sparowa\u0107, spr\u00f3buj ponownie.", "unknown_error": "Urz\u0105dzenie zg\u0142osi\u0142o nieznany b\u0142\u0105d. Parowanie nie powiod\u0142o si\u0119." }, diff --git a/homeassistant/components/life360/.translations/ca.json b/homeassistant/components/life360/.translations/ca.json index a7189d69185220..58401a33d1460f 100644 --- a/homeassistant/components/life360/.translations/ca.json +++ b/homeassistant/components/life360/.translations/ca.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Credencials inv\u00e0lides", "invalid_username": "Nom d'usuari incorrecte", + "unexpected": "S'ha produ\u00eft un error inesperat en comunicar-se amb el servidor de Life360.", "user_already_configured": "El compte ja ha estat configurat" }, "step": { diff --git a/homeassistant/components/life360/.translations/da.json b/homeassistant/components/life360/.translations/da.json index 1870c3fdb51eaf..933fce4a4e8798 100644 --- a/homeassistant/components/life360/.translations/da.json +++ b/homeassistant/components/life360/.translations/da.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ugyldige legitimationsoplysninger", "invalid_username": "Ugyldigt brugernavn", + "unexpected": "Uventet fejl under kommunikation med Life360-serveren", "user_already_configured": "Kontoen er allerede konfigureret" }, "step": { diff --git a/homeassistant/components/life360/.translations/hu.json b/homeassistant/components/life360/.translations/hu.json new file mode 100644 index 00000000000000..227e784b0653eb --- /dev/null +++ b/homeassistant/components/life360/.translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unexpected": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt a kommunik\u00e1ci\u00f3ban a Life360 szerverrel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/nl.json b/homeassistant/components/life360/.translations/nl.json index ec7a53329503a0..08be66a89639f1 100644 --- a/homeassistant/components/life360/.translations/nl.json +++ b/homeassistant/components/life360/.translations/nl.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ongeldige gebruikersgegevens", "invalid_username": "Ongeldige gebruikersnaam", + "unexpected": "Onverwachte fout bij communicatie met Life360-server", "user_already_configured": "Account is al geconfigureerd" }, "step": { diff --git a/homeassistant/components/life360/.translations/pl.json b/homeassistant/components/life360/.translations/pl.json index 15aabaa6308424..cd5e61fc123608 100644 --- a/homeassistant/components/life360/.translations/pl.json +++ b/homeassistant/components/life360/.translations/pl.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", + "unexpected": "Nieoczekiwany b\u0142\u0105d komunikacji z serwerem Life360", "user_already_configured": "Konto zosta\u0142o ju\u017c skonfigurowane." }, "step": { diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index 0f698457bf799a..c03ad0f7e1f6a6 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d", + "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360", "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" }, "step": { diff --git a/homeassistant/components/life360/.translations/zh-Hant.json b/homeassistant/components/life360/.translations/zh-Hant.json index 8ab5dcf536979e..75081c62d41099 100644 --- a/homeassistant/components/life360/.translations/zh-Hant.json +++ b/homeassistant/components/life360/.translations/zh-Hant.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "\u6191\u8b49\u7121\u6548", "invalid_username": "\u4f7f\u7528\u8005\u540d\u7a31\u7121\u6548", + "unexpected": "\u8207 Life360 \u4f3a\u670d\u5668\u901a\u8a0a\u767c\u751f\u672a\u77e5\u932f\u8aa4", "user_already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "step": { diff --git a/homeassistant/components/light/.translations/en.json b/homeassistant/components/light/.translations/en.json new file mode 100644 index 00000000000000..9e5d1abddaf486 --- /dev/null +++ b/homeassistant/components/light/.translations/en.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} turned off", + "turn_on": "{name} turned on" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/pl.json b/homeassistant/components/logi_circle/.translations/pl.json index 2c155ffde61b35..f39df48ae5a483 100644 --- a/homeassistant/components/logi_circle/.translations/pl.json +++ b/homeassistant/components/logi_circle/.translations/pl.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Logi Circle.", - "external_error": "Wyst\u0105pi\u0142 wyj\u0105tek zewn\u0119trzny.", - "external_setup": "Logi Circle pomy\u015blnie skonfigurowano.", + "external_error": "Wyst\u0105pi\u0142 wyj\u0105tek z innego przep\u0142ywu.", + "external_setup": "Logi Circle zosta\u0142o pomy\u015blnie skonfigurowane z innego przep\u0142ywu.", "no_flows": "Musisz skonfigurowa\u0107 Logi Circle, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { @@ -12,12 +12,12 @@ "error": { "auth_error": "Autoryzacja API nie powiod\u0142a si\u0119.", "auth_timeout": "Up\u0142yn\u0105\u0142 limit czasu \u017c\u0105dania tokena dost\u0119pu.", - "follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku Prze\u015blij." + "follow_link": "Post\u0119puj zgodnie z linkiem i uwierzytelnij si\u0119 przed naci\u015bni\u0119ciem przycisku Prze\u015blij." }, "step": { "auth": { "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do swojego konta Logi Circle, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})", - "title": "Uwierzytelnienie Logi Circle" + "title": "Uwierzytelnij za pomoc\u0105 Logi Circle" }, "user": { "data": { diff --git a/homeassistant/components/met/.translations/hu.json b/homeassistant/components/met/.translations/hu.json new file mode 100644 index 00000000000000..3b34d8f6354d6a --- /dev/null +++ b/homeassistant/components/met/.translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Elhelyezked\u00e9s" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/ca.json b/homeassistant/components/nest/.translations/ca.json index b242208791b6ae..636568b96d350b 100644 --- a/homeassistant/components/nest/.translations/ca.json +++ b/homeassistant/components/nest/.translations/ca.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte Nest.", "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", - "authorize_url_timeout": "El temps d'espera m\u00e0xim per generar l'URL d'autoritzaci\u00f3 s'ha esgotat.", + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "no_flows": "Necessites configurar Nest abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/nest/)." }, "error": { diff --git a/homeassistant/components/nest/.translations/zh-Hant.json b/homeassistant/components/nest/.translations/zh-Hant.json index 6b9dbdb19b1148..c477557e7ba0c7 100644 --- a/homeassistant/components/nest/.translations/zh-Hant.json +++ b/homeassistant/components/nest/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Nest \u5e33\u865f\u3002", "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", - "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Nest \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/nest/\uff09\u3002" + "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Nest \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15](https://www.home-assistant.io/components/nest/)\u3002" }, "error": { "internal_error": "\u8a8d\u8b49\u78bc\u5167\u90e8\u932f\u8aa4", diff --git a/homeassistant/components/notion/.translations/hu.json b/homeassistant/components/notion/.translations/hu.json new file mode 100644 index 00000000000000..2f7664cf74eec1 --- /dev/null +++ b/homeassistant/components/notion/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Felhaszn\u00e1l\u00f3n\u00e9v m\u00e1r regisztr\u00e1lva van", + "invalid_credentials": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3", + "no_devices": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a fi\u00f3kban" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v/Email C\u00edm" + }, + "title": "T\u00f6ltse ki adatait" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/zh-Hant.json b/homeassistant/components/point/.translations/zh-Hant.json index 91a86f5e3dba1c..9f688b2e5f9d52 100644 --- a/homeassistant/components/point/.translations/zh-Hant.json +++ b/homeassistant/components/point/.translations/zh-Hant.json @@ -5,7 +5,7 @@ "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", "external_setup": "\u5df2\u7531\u5176\u4ed6\u6d41\u7a0b\u6210\u529f\u8a2d\u5b9a Point\u3002", - "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Point \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/point/\uff09\u3002" + "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Point \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15](https://www.home-assistant.io/components/point/)\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Minut Point \u88dd\u7f6e\u3002" diff --git a/homeassistant/components/ps4/.translations/pl.json b/homeassistant/components/ps4/.translations/pl.json index 3e36960b12c019..9fb4c73f1d056a 100644 --- a/homeassistant/components/ps4/.translations/pl.json +++ b/homeassistant/components/ps4/.translations/pl.json @@ -8,7 +8,7 @@ "port_997_bind_error": "Nie mo\u017cna powi\u0105za\u0107 z portem 997." }, "error": { - "credential_timeout": "Up\u0142yn\u0105\u0142 limit czasu us\u0142ugi po\u015bwiadcze\u0144. Naci\u015bnij przycisk Prze\u015blij, aby ponownie uruchomi\u0107.", + "credential_timeout": "Up\u0142yn\u0105\u0142 limit czasu us\u0142ugi po\u015bwiadcze\u0144. Naci\u015bnij przycisk Prze\u015blij, aby ponowi\u0107.", "login_failed": "Nie uda\u0142o si\u0119 sparowa\u0107 z PlayStation 4. Sprawd\u017a, czy PIN jest poprawny.", "no_ipaddress": "Wprowad\u017a adres IP PlayStation 4, kt\u00f3ry chcesz skonfigurowa\u0107.", "not_ready": "PlayStation 4 nie jest w\u0142\u0105czona lub po\u0142\u0105czona z sieci\u0105." diff --git a/homeassistant/components/toon/.translations/zh-Hant.json b/homeassistant/components/toon/.translations/zh-Hant.json index b09d921268cf11..0156b58c9ac049 100644 --- a/homeassistant/components/toon/.translations/zh-Hant.json +++ b/homeassistant/components/toon/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "client_id": "\u8a2d\u5b9a\u5167\u7528\u6236\u7aef ID \u7121\u6548\u3002", "client_secret": "\u8a2d\u5b9a\u5167\u5ba2\u6236\u7aef\u5bc6\u78bc\u7121\u6548\u3002", "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u88dd\u7f6e\u3002", - "no_app": "\u5fc5\u9808\u5148\u8a2d\u5b9a Toon \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/toon/\uff09\u3002", + "no_app": "\u5fc5\u9808\u5148\u8a2d\u5b9a Toon \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15](https://www.home-assistant.io/components/toon/(\u3002", "unknown_auth_fail": "\u9a57\u8b49\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, "error": { diff --git a/homeassistant/components/traccar/.translations/ca.json b/homeassistant/components/traccar/.translations/ca.json new file mode 100644 index 00000000000000..0cfb9738d5dd89 --- /dev/null +++ b/homeassistant/components/traccar/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Traccar.", + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "create_entry": { + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Traccar.\n\nUtilitza el seg\u00fcent enlla\u00e7: `{webhook_url}`\n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." + }, + "step": { + "user": { + "description": "Est\u00e0s segur que vols configurar Traccar?", + "title": "Configura Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/da.json b/homeassistant/components/traccar/.translations/da.json new file mode 100644 index 00000000000000..af3963f8c0f7b5 --- /dev/null +++ b/homeassistant/components/traccar/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Traccar meddelelser.", + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + }, + "create_entry": { + "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i Traccar.\n\n Brug f\u00f8lgende URL: `{webhook_url}`\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil konfigurere Traccar?", + "title": "Konfigurer Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/nl.json b/homeassistant/components/traccar/.translations/nl.json new file mode 100644 index 00000000000000..c4ee0544a2e716 --- /dev/null +++ b/homeassistant/components/traccar/.translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Uw Home Assistant-exemplaar moet toegankelijk zijn vanaf internet om berichten van Traccar te ontvangen.", + "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." + }, + "create_entry": { + "default": "Voor het verzenden van gebeurtenissen naar Home Assistant, moet u de webhook-functie in Traccar instellen.\n\nGebruik de volgende URL: ' {webhook_url} '\n\nZie [de documentatie] ({docs_url}) voor meer informatie." + }, + "step": { + "user": { + "description": "Weet u zeker dat u Traccar wilt instellen?", + "title": "Traccar instellen" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/pl.json b/homeassistant/components/traccar/.translations/pl.json new file mode 100644 index 00000000000000..66ddbaaa3fdb21 --- /dev/null +++ b/homeassistant/components/traccar/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Twoja instancja Home Assistant musi by\u0107 dost\u0119pna z Internetu, aby otrzymywa\u0107 wiadomo\u015bci z Traccar.", + "one_instance_allowed": "Niezb\u0119dna jest tylko jedna instancja." + }, + "create_entry": { + "default": "Aby wys\u0142a\u0107 wydarzenia do Home Assistant, musisz skonfigurowa\u0107 funkcj\u0119 webhook w Traccar. \n\n U\u017cyj nast\u0119puj\u0105cego URL: ` {webhook_url} ` \n\n Zobacz [dokumentacj\u0119] ( {docs_url} ) w celu uzyskania dalszych szczeg\u00f3\u0142\u00f3w." + }, + "step": { + "user": { + "description": "Czy na pewno chcesz skonfigurowa\u0107 Traccar?", + "title": "Skonfiguruj Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/ru.json b/homeassistant/components/traccar/.translations/ru.json new file mode 100644 index 00000000000000..afaab87efe4e8e --- /dev/null +++ b/homeassistant/components/traccar/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Traccar.", + "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Traccar.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Traccar?", + "title": "Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/zh-Hant.json b/homeassistant/components/traccar/.translations/zh-Hant.json new file mode 100644 index 00000000000000..f5402454294856 --- /dev/null +++ b/homeassistant/components/traccar/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Traccar \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "create_entry": { + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Traccar \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u4f7f\u7528 url: `{webhook_url}`\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Traccar\uff1f", + "title": "\u8a2d\u5b9a Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/ca.json b/homeassistant/components/twentemilieu/.translations/ca.json new file mode 100644 index 00000000000000..27ab8e8a8b2b0b --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adre\u00e7a ja configurada." + }, + "error": { + "connection_error": "No s'ha pogut connectar.", + "invalid_address": "No s'ha trobat l'adre\u00e7a a l'\u00e0rea de servei de Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Lletra/addicional de casa", + "house_number": "N\u00famero de casa", + "post_code": "Codi postal" + }, + "description": "Configura Twente Milieu amb informaci\u00f3 de la recollida de residus a la teva adre\u00e7a.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/da.json b/homeassistant/components/twentemilieu/.translations/da.json new file mode 100644 index 00000000000000..1e3ca933e3895a --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adresse er allerede konfigureret." + }, + "error": { + "connection_error": "Forbindelse mislykkedes.", + "invalid_address": "Adresse ikke fundet i Twente Milieu serviceomr\u00e5de." + }, + "step": { + "user": { + "data": { + "house_letter": "Hus nummer/yderligere", + "house_number": "Husnummer", + "post_code": "Postnummer" + }, + "description": "Konfigurer Twente Milieu, der leverer oplysninger om indsamling af affald p\u00e5 din adresse.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/hu.json b/homeassistant/components/twentemilieu/.translations/hu.json new file mode 100644 index 00000000000000..439e02d1027813 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "address_exists": "A c\u00edm m\u00e1r be lett \u00e1ll\u00edtva." + }, + "error": { + "connection_error": "Nem siker\u00fclt csatlakozni." + }, + "step": { + "user": { + "data": { + "house_number": "h\u00e1zsz\u00e1m", + "post_code": "ir\u00e1ny\u00edt\u00f3sz\u00e1m" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/nl.json b/homeassistant/components/twentemilieu/.translations/nl.json new file mode 100644 index 00000000000000..a420133f464c93 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adres al ingesteld." + }, + "error": { + "connection_error": "Kon niet verbinden.", + "invalid_address": "Adres niet gevonden in servicegebied Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Huisnummer / toevoeging", + "house_number": "Huisnummer", + "post_code": "Postcode" + }, + "description": "Stel Twente Milieu in voor het inzamelen van afval op uw adres.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/pl.json b/homeassistant/components/twentemilieu/.translations/pl.json new file mode 100644 index 00000000000000..042fcf0dda66d4 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adres ju\u017c skonfigurowany." + }, + "error": { + "connection_error": "Po\u0142\u0105czenie nieudane.", + "invalid_address": "Nie znaleziono adresu w obszarze us\u0142ugi Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "List domowy / dodatkowy", + "house_number": "Numer domu", + "post_code": "Kod pocztowy" + }, + "description": "Skonfiguruj Twente Milieu, dostarczaj\u0105c informacji o zbieraniu odpad\u00f3w pod swoim adresem.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/ru.json b/homeassistant/components/twentemilieu/.translations/ru.json new file mode 100644 index 00000000000000..5d964604a77312 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d." + }, + "error": { + "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "invalid_address": "\u0410\u0434\u0440\u0435\u0441 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 \u0437\u043e\u043d\u0435 \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u044f Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "\u041b\u0438\u0442\u0435\u0440 \u0434\u043e\u043c\u0430 / \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435", + "house_number": "\u041d\u043e\u043c\u0435\u0440 \u0434\u043e\u043c\u0430", + "post_code": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Twente Milieu \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u0432\u044b\u0432\u043e\u0437\u0435 \u043c\u0443\u0441\u043e\u0440\u0430 \u043f\u043e \u0412\u0430\u0448\u0435\u043c\u0443 \u0430\u0434\u0440\u0435\u0441\u0443.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/zh-Hant.json b/homeassistant/components/twentemilieu/.translations/zh-Hant.json new file mode 100644 index 00000000000000..0e0083ec5c1750 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "\u5730\u5740\u5df2\u8a2d\u5b9a\u3002" + }, + "error": { + "connection_error": "\u9023\u7dda\u5931\u6557\u3002", + "invalid_address": "Twente Milieu \u670d\u52d9\u5340\u57df\u5167\u627e\u4e0d\u5230\u6b64\u5730\u5740\u3002" + }, + "step": { + "user": { + "data": { + "house_letter": "\u9580\u724c\u5b57\u6bcd/\u9644\u52a0\u8cc7\u8a0a", + "house_number": "\u9580\u724c\u865f\u78bc", + "post_code": "\u90f5\u905e\u5340\u865f" + }, + "description": "\u8a2d\u5b9a Twente Milieu \u4ee5\u53d6\u5f97\u8a72\u5730\u5740\u5ee2\u68c4\u7269\u56de\u6536\u8cc7\u8a0a\u3002", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 442d82d9a3f35e..8a8d8b11f57661 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -22,5 +22,17 @@ } }, "title": "Controlador UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Temps (en segons) des de s'ha vist per \u00faltima vegada fins que es considera a fora", + "track_clients": "Segueix clients de la xarxa", + "track_devices": "Segueix dispositius de la xarxa (dispositius Ubiquiti)", + "track_wired_clients": "Inclou clients de xarxa per cable" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json index 4155658d7deae5..53b794ed4353ff 100644 --- a/homeassistant/components/unifi/.translations/da.json +++ b/homeassistant/components/unifi/.translations/da.json @@ -22,5 +22,17 @@ } }, "title": "UniFi Controller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tid i sekunder fra sidst set indtil betragtet som v\u00e6k", + "track_clients": "Spor netv\u00e6rksklienter", + "track_devices": "Spor netv\u00e6rksenheder (Ubiquiti-enheder)", + "track_wired_clients": "Inkluder kablede netv\u00e6rksklienter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json index 7a1eea546a2941..f907364327c958 100644 --- a/homeassistant/components/unifi/.translations/nl.json +++ b/homeassistant/components/unifi/.translations/nl.json @@ -22,5 +22,17 @@ } }, "title": "UniFi-controller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tijd in seconden vanaf laatst gezien tot beschouwd als weg", + "track_clients": "Volg netwerkclients", + "track_devices": "Netwerkapparaten volgen (Ubiquiti-apparaten)", + "track_wired_clients": "Inclusief bedrade netwerkcli\u00ebnten" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index 5382adcbf7d97b..6366f82b3da0a8 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -22,5 +22,25 @@ } }, "title": "Kontroler UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Czas w sekundach od momentu, kiedy ostatnio widziano, a\u017c do momentu, kiedy uznano go za nieobecny.", + "track_clients": "\u015aled\u017a klient\u00f3w sieciowych", + "track_devices": "\u015aled\u017a urz\u0105dzenia sieciowe (urz\u0105dzenia Ubiquiti)", + "track_wired_clients": "Uwzgl\u0119dnij klient\u00f3w sieci przewodowej" + } + }, + "init": { + "data": { + "few": "Kilka", + "many": "Wiele", + "one": "Jeden", + "other": "Inne" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index f4d86300acaed2..76802a96367677 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -22,5 +22,17 @@ } }, "title": "UniFi Controller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\".", + "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", + "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)", + "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index e506c582cb7177..2d5bd9027ac44d 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -22,5 +22,17 @@ } }, "title": "UniFi \u63a7\u5236\u5668" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\u6700\u7d42\u51fa\u73fe\u5f8c\u8996\u70ba\u96e2\u958b\u7684\u6642\u9593\uff08\u4ee5\u79d2\u70ba\u55ae\u4f4d\uff09", + "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", + "track_devices": "\u8ffd\u8e64\u7db2\u8def\u88dd\u7f6e\uff08Ubiquiti \u88dd\u7f6e\uff09", + "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/ca.json b/homeassistant/components/velbus/.translations/ca.json new file mode 100644 index 00000000000000..e38977a483fa5f --- /dev/null +++ b/homeassistant/components/velbus/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "El port ja est\u00e0 configurat" + }, + "error": { + "connection_failed": "Ha fallat la connexi\u00f3 Velbus", + "port_exists": "El port ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "name": "Nom de la connexi\u00f3 Velbus", + "port": "Cadena de connexi\u00f3" + }, + "title": "Tipus de connexi\u00f3 Velbus" + } + }, + "title": "Interf\u00edcie Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/da.json b/homeassistant/components/velbus/.translations/da.json new file mode 100644 index 00000000000000..5e636c8bcd77a3 --- /dev/null +++ b/homeassistant/components/velbus/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Denne port er allerede konfigureret" + }, + "error": { + "connection_failed": "Velbus forbindelsen mislykkedes", + "port_exists": "Denne port er allerede konfigureret" + }, + "step": { + "user": { + "data": { + "name": "Navnet p\u00e5 denne velbus forbindelse", + "port": "Forbindelsesstreng" + }, + "title": "Definer velbus forbindelsestypen" + } + }, + "title": "Velbus-interface" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/hu.json b/homeassistant/components/velbus/.translations/hu.json new file mode 100644 index 00000000000000..c836b414746de4 --- /dev/null +++ b/homeassistant/components/velbus/.translations/hu.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "port_exists": "Ez a port m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "port_exists": "Ez a port m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/nl.json b/homeassistant/components/velbus/.translations/nl.json new file mode 100644 index 00000000000000..b2908e8d2210cf --- /dev/null +++ b/homeassistant/components/velbus/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Deze poort is al geconfigureerd" + }, + "error": { + "connection_failed": "De velbus verbinding is mislukt.", + "port_exists": "Deze poort is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "name": "De naam voor deze velbus-verbinding", + "port": "Verbindingsreeks" + }, + "title": "Definieer de velbus-verbindingstype" + } + }, + "title": "Velbus interface" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/pl.json b/homeassistant/components/velbus/.translations/pl.json new file mode 100644 index 00000000000000..72e18b0e2c89fa --- /dev/null +++ b/homeassistant/components/velbus/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Ten port jest ju\u017c skonfigurowany" + }, + "error": { + "connection_failed": "Po\u0142\u0105czenie Velbus nie powiod\u0142o si\u0119", + "port_exists": "Ten port jest ju\u017c skonfigurowany" + }, + "step": { + "user": { + "data": { + "name": "Nazwa tego po\u0142\u0105czenia Velbus", + "port": "Parametry po\u0142\u0105czenia" + }, + "title": "Zdefiniuj typ po\u0142\u0105czenia Velbus" + } + }, + "title": "Interfejs Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/ru.json b/homeassistant/components/velbus/.translations/ru.json new file mode 100644 index 00000000000000..3434c584221455 --- /dev/null +++ b/homeassistant/components/velbus/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "\u042d\u0442\u043e\u0442 \u043f\u043e\u0440\u0442 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." + }, + "error": { + "connection_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441 Velbus.", + "port_exists": "\u042d\u0442\u043e\u0442 \u043f\u043e\u0440\u0442 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f Velbus", + "port": "\u0421\u0442\u0440\u043e\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "title": "Velbus" + } + }, + "title": "Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/zh-Hant.json b/homeassistant/components/velbus/.translations/zh-Hant.json new file mode 100644 index 00000000000000..33f9191e8a2163 --- /dev/null +++ b/homeassistant/components/velbus/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "\u6b64\u901a\u8a0a\u57e0\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "connection_failed": "Velbus \u9023\u7dda\u5931\u6557", + "port_exists": "\u6b64\u901a\u8a0a\u57e0\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "name": "Velbus \u9023\u7dda\u540d\u7a31", + "port": "\u9023\u7dda\u5b57\u4e32" + }, + "title": "\u5b9a\u7fa9 Velbus \u9023\u7dda\u985e\u578b" + } + }, + "title": "Velbus \u4ecb\u9762" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/ca.json b/homeassistant/components/vesync/.translations/ca.json new file mode 100644 index 00000000000000..0c253fd4812c11 --- /dev/null +++ b/homeassistant/components/vesync/.translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s es permet una \u00fanica inst\u00e0ncia de VeSync" + }, + "error": { + "invalid_login": "Nom d'usuari o contrasenya incorrectes" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Introdueix el nom d\u2019usuari i contrasenya" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/da.json b/homeassistant/components/vesync/.translations/da.json new file mode 100644 index 00000000000000..43e56328f99785 --- /dev/null +++ b/homeassistant/components/vesync/.translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Kun en Vesync-forekomst er tilladt" + }, + "error": { + "invalid_login": "Ugyldigt brugernavn eller adgangskode" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Email adresse" + }, + "title": "Indtast brugernavn og adgangskode" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/hu.json b/homeassistant/components/vesync/.translations/hu.json new file mode 100644 index 00000000000000..4735140216fd04 --- /dev/null +++ b/homeassistant/components/vesync/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_login": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Email c\u00edm" + }, + "title": "\u00cdrja be a felhaszn\u00e1l\u00f3nevet \u00e9s a jelsz\u00f3t" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/nl.json b/homeassistant/components/vesync/.translations/nl.json new file mode 100644 index 00000000000000..d19d528c61a32f --- /dev/null +++ b/homeassistant/components/vesync/.translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Er is slechts \u00e9\u00e9n Vesync instantie toegestaan." + }, + "error": { + "invalid_login": "Ongeldige gebruikersnaam of wachtwoord" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mailadres" + }, + "title": "Voer gebruikersnaam en wachtwoord in" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/pl.json b/homeassistant/components/vesync/.translations/pl.json new file mode 100644 index 00000000000000..d6584f11d29041 --- /dev/null +++ b/homeassistant/components/vesync/.translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Dozwolona jest tylko jedna instancja Vesync" + }, + "error": { + "invalid_login": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "title": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o." + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/ru.json b/homeassistant/components/vesync/.translations/ru.json new file mode 100644 index 00000000000000..38b86e9e29f479 --- /dev/null +++ b/homeassistant/components/vesync/.translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "title": "VeSync" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/zh-Hant.json b/homeassistant/components/vesync/.translations/zh-Hant.json new file mode 100644 index 00000000000000..05e4a1bbc7974a --- /dev/null +++ b/homeassistant/components/vesync/.translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 Vesync \u7269\u4ef6" + }, + "error": { + "invalid_login": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + }, + "title": "\u8acb\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/en.json b/homeassistant/components/withings/.translations/en.json new file mode 100644 index 00000000000000..2b906dd80030fb --- /dev/null +++ b/homeassistant/components/withings/.translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Successfully authenticated with Withings for the selected profile." + }, + "step": { + "user": { + "data": { + "profile": "Profile" + }, + "description": "Select a user profile to which you want Home Assistant to map with a Withings profile. On the withings page, be sure to select the same user or data will not be labeled correctly.", + "title": "User Profile." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/hu.json b/homeassistant/components/wwlln/.translations/hu.json new file mode 100644 index 00000000000000..740fc1a8179c96 --- /dev/null +++ b/homeassistant/components/wwlln/.translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + } + } + } + } +} \ No newline at end of file From 7a171dae330fe37651eeef0d9000be44c1e585ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Sep 2019 22:30:21 -0700 Subject: [PATCH 0096/3953] Updated frontend to 20190901.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 fa6145a7af21de..2b17091ba5cb94 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==20190828.0" + "home-assistant-frontend==20190901.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5754e3fc391d6d..2fa5a1cd41ae4b 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==20190828.0 +home-assistant-frontend==20190901.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5a9860b7c6f2eb..54e27f8397246f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -628,7 +628,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190828.0 +home-assistant-frontend==20190901.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7727bd459b532..c76473cd3287a0 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==20190828.0 +home-assistant-frontend==20190901.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 47e76fcd5e9fe2651f7346b4d39f8f0044aa8574 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Sep 2019 21:42:57 -0700 Subject: [PATCH 0097/3953] Expose current direction properly on state machine (#26298) * Expose current direction properly on state machine * Fix template fan --- homeassistant/components/demo/fan.py | 8 ++++---- homeassistant/components/fan/__init__.py | 2 +- homeassistant/components/template/fan.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index cdeed5dbfec692..ab8a6f3fae9dc6 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -34,13 +34,13 @@ def __init__(self, hass, name: str, supported_features: int) -> None: self._supported_features = supported_features self._speed = STATE_OFF self.oscillating = None - self.direction = None + self._direction = None self._name = name if supported_features & SUPPORT_OSCILLATE: self.oscillating = False if supported_features & SUPPORT_DIRECTION: - self.direction = "forward" + self._direction = "forward" @property def name(self) -> str: @@ -80,7 +80,7 @@ def set_speed(self, speed: str) -> None: def set_direction(self, direction: str) -> None: """Set the direction of the fan.""" - self.direction = direction + self._direction = direction self.schedule_update_ha_state() def oscillate(self, oscillating: bool) -> None: @@ -91,7 +91,7 @@ def oscillate(self, oscillating: bool) -> None: @property def current_direction(self) -> str: """Fan direction.""" - return self.direction + return self._direction @property def supported_features(self) -> int: diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index f5edfe5bb5996e..50d698f733656f 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -54,7 +54,7 @@ "speed": ATTR_SPEED, "speed_list": ATTR_SPEED_LIST, "oscillating": ATTR_OSCILLATING, - "direction": ATTR_DIRECTION, + "current_direction": ATTR_DIRECTION, } # type: dict FAN_SET_SPEED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index c3d5a4d878fd9e..7fd8c4d9b3cea6 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -243,7 +243,7 @@ def oscillating(self): return self._oscillating @property - def direction(self): + def current_direction(self): """Return the oscillation state.""" return self._direction From 732855e86c88c105151bd88b570c2bf8cd896530 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Sat, 31 Aug 2019 03:30:18 +0300 Subject: [PATCH 0098/3953] bump tuyaha 0.0.4 (#26303) --- 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 8d47d8a0173bad..9c83056f6aca81 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.3" + "tuyaha==0.0.4" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index e749340a8cb2c1..2bf7e6a841cc0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1857,7 +1857,7 @@ tplink==0.2.1 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.3 +tuyaha==0.0.4 # homeassistant.components.twentemilieu twentemilieu==0.1.0 From 795d5405db4bfa50a47f3d9b72dc83683831d3be Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Aug 2019 18:34:40 -0700 Subject: [PATCH 0099/3953] Fix Alexa Report State (#26305) * Fix Alexa Report State * Forgot to save a file --- homeassistant/components/alexa/auth.py | 5 +++ homeassistant/components/alexa/config.py | 8 ++++ .../components/alexa/smart_home_http.py | 5 +++ .../components/alexa/state_report.py | 27 +++++++++---- .../components/cloud/alexa_config.py | 8 +++- tests/components/cloud/test_alexa_config.py | 38 ++++++++++++++++++- 6 files changed, 81 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index d4633d938ed505..9f87a6d954e95b 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -56,6 +56,11 @@ async def async_do_auth(self, accept_grant_code): return await self._async_request_new_token(lwa_params) + @callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + self._prefs[STORAGE_ACCESS_TOKEN] = None + async def async_get_access_token(self): """Perform access token or token refresh request.""" async with self._get_token_lock: diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index a22ebbcd30d431..f98337d71c56ae 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -1,4 +1,6 @@ """Config helpers for Alexa.""" +from homeassistant.core import callback + from .state_report import async_enable_proactive_mode @@ -55,11 +57,17 @@ async def async_disable_proactive_mode(self): unsub_func() self._unsub_proactive_report = None + @callback def should_expose(self, entity_id): """If an entity should be exposed.""" # pylint: disable=no-self-use return False + @callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + raise NotImplementedError + async def async_get_access_token(self): """Get an access token.""" raise NotImplementedError diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 7fdd4e3000a3ea..ada00e8a326820 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -57,6 +57,11 @@ def should_expose(self, entity_id): """If an entity should be exposed.""" return self._config[CONF_FILTER](entity_id) + @core.callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + self._auth.async_invalidate_access_token() + async def async_get_access_token(self): """Get an access token.""" return await self._auth.async_get_access_token() diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 7e8428899776e3..1e22d5fc09f23b 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -51,7 +51,9 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): ) -async def async_send_changereport_message(hass, config, alexa_entity): +async def async_send_changereport_message( + hass, config, alexa_entity, *, invalidate_access_token=True +): """Send a ChangeReport message for an Alexa entity. https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-with-changereport-events @@ -88,21 +90,30 @@ async def async_send_changereport_message(hass, config, alexa_entity): except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout sending report to Alexa.") - return None + return response_text = await response.text() _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) _LOGGER.debug("Received (%s): %s", response.status, response_text) - if response.status != 202: - response_json = json.loads(response_text) - _LOGGER.error( - "Error when sending ChangeReport to Alexa: %s: %s", - response_json["payload"]["code"], - response_json["payload"]["description"], + if response.status == 202 and not invalidate_access_token: + return + + response_json = json.loads(response_text) + + if response_json["payload"]["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION": + config.async_invalidate_access_token() + return await async_send_changereport_message( + hass, config, alexa_entity, invalidate_access_token=False ) + _LOGGER.error( + "Error when sending ChangeReport to Alexa: %s: %s", + response_json["payload"]["code"], + response_json["payload"]["description"], + ) + async def async_send_add_or_update_message(hass, config, entity_ids): """Send an AddOrUpdateReport message for entities. diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index d31bcfdfc40e61..a1432f196bf5d9 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -7,6 +7,7 @@ import async_timeout from hass_nabucasa import cloud_api +from homeassistant.core import callback from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers import entity_registry from homeassistant.helpers.event import async_call_later @@ -95,9 +96,14 @@ def should_expose(self, entity_id): entity_config = entity_configs.get(entity_id, {}) return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) + @callback + def async_invalidate_access_token(self): + """Invalidate access token.""" + self._token_valid = None + async def async_get_access_token(self): """Get an access token.""" - if self._token_valid is not None and self._token_valid < utcnow(): + if self._token_valid is not None and self._token_valid > utcnow(): return self._token resp = await cloud_api.async_alexa_access_token(self._cloud) diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 22d8c64c3b0f85..c8e84016a28a9f 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -1,6 +1,6 @@ """Test Alexa config.""" import contextlib -from unittest.mock import patch +from unittest.mock import patch, Mock from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.util.dt import utcnow @@ -43,6 +43,42 @@ async def test_alexa_config_report_state(hass, cloud_prefs): assert conf.is_reporting_states is False +async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock): + """Test Alexa config should expose using prefs.""" + aioclient_mock.post( + "http://example/alexa_token", + json={ + "access_token": "mock-token", + "event_endpoint": "http://example.com/alexa_endpoint", + "expires_in": 30, + }, + ) + conf = alexa_config.AlexaConfig( + hass, + ALEXA_SCHEMA({}), + cloud_prefs, + Mock( + alexa_access_token_url="http://example/alexa_token", + run_executor=Mock(side_effect=mock_coro), + websession=hass.helpers.aiohttp_client.async_get_clientsession(), + ), + ) + + token = await conf.async_get_access_token() + assert token == "mock-token" + assert len(aioclient_mock.mock_calls) == 1 + + token = await conf.async_get_access_token() + assert token == "mock-token" + assert len(aioclient_mock.mock_calls) == 1 + assert conf._token_valid is not None + conf.async_invalidate_access_token() + assert conf._token_valid is None + token = await conf.async_get_access_token() + assert token == "mock-token" + assert len(aioclient_mock.mock_calls) == 2 + + @contextlib.contextmanager def patch_sync_helper(): """Patch sync helper. From 309d401e476fe0171085cb9561e24b498d02cbe9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 31 Aug 2019 07:46:26 -0700 Subject: [PATCH 0100/3953] Fix alexa bad temp sensors (#26307) --- .../components/alexa/capabilities.py | 23 +++++++- tests/components/alexa/__init__.py | 6 +++ tests/components/alexa/test_capabilities.py | 54 ++++++++++++++++++- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index dfb97cd9db25f4..d769f797da1bfa 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -11,6 +11,7 @@ STATE_ON, STATE_UNAVAILABLE, STATE_UNLOCKED, + STATE_UNKNOWN, ) import homeassistant.components.climate.const as climate from homeassistant.components import light, fan, cover @@ -443,7 +444,17 @@ def get_property(self, name): if self.entity.domain == climate.DOMAIN: unit = self.hass.config.units.temperature_unit temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE) - return {"value": float(temp), "scale": API_TEMP_UNITS[unit]} + + if temp in (STATE_UNAVAILABLE, STATE_UNKNOWN): + return None + + try: + temp = float(temp) + except ValueError: + _LOGGER.warning("Invalid temp value %s for %s", temp, self.entity.entity_id) + return None + + return {"value": temp, "scale": API_TEMP_UNITS[unit]} class AlexaContactSensor(AlexaCapibility): @@ -591,4 +602,12 @@ def get_property(self, name): if temp is None: return None - return {"value": float(temp), "scale": API_TEMP_UNITS[unit]} + try: + temp = float(temp) + except ValueError: + _LOGGER.warning( + "Invalid temp value %s for %s in %s", temp, name, self.entity.entity_id + ) + return None + + return {"value": temp, "scale": API_TEMP_UNITS[unit]} diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index f853c4ef848cb0..48406a11aef1f2 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -171,6 +171,12 @@ def __init__(self, properties): """Initialize class.""" self.properties = properties + def assert_not_has_property(self, namespace, name): + """Assert a property does not exist.""" + for prop in self.properties: + if prop["namespace"] == namespace and prop["name"] == name: + assert False, "Property %s:%s exists" + def assert_equal(self, namespace, name, value): """Assert a property is equal to a given value.""" for prop in self.properties: diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index f8ad3f57c420e0..357e0e3026d47c 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -1,7 +1,15 @@ """Test Alexa capabilities.""" import pytest -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS, + STATE_LOCKED, + STATE_UNLOCKED, + STATE_UNKNOWN, + STATE_UNAVAILABLE, +) +from homeassistant.components import climate from homeassistant.components.alexa import smart_home from tests.common import async_mock_service @@ -368,3 +376,47 @@ async def test_report_cover_percentage_state(hass): properties = await reported_properties(hass, "cover.closed") properties.assert_equal("Alexa.PercentageController", "percentage", 0) + + +async def test_temperature_sensor_sensor(hass): + """Test TemperatureSensor reports sensor temperature correctly.""" + for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"): + hass.states.async_set( + "sensor.temp_living_room", + bad_value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + + properties = await reported_properties(hass, "sensor.temp_living_room") + properties.assert_not_has_property("Alexa.TemperatureSensor", "temperature") + + hass.states.async_set( + "sensor.temp_living_room", "34", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + ) + properties = await reported_properties(hass, "sensor.temp_living_room") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) + + +async def test_temperature_sensor_climate(hass): + """Test TemperatureSensor reports climate temperature correctly.""" + for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"): + hass.states.async_set( + "climate.downstairs", + climate.HVAC_MODE_HEAT, + {climate.ATTR_CURRENT_TEMPERATURE: bad_value}, + ) + + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_not_has_property("Alexa.TemperatureSensor", "temperature") + + hass.states.async_set( + "climate.downstairs", + climate.HVAC_MODE_HEAT, + {climate.ATTR_CURRENT_TEMPERATURE: 34}, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) From d1e3fbd622ccc0fd94b1e5d5834d166db189d891 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 31 Aug 2019 15:56:43 +0200 Subject: [PATCH 0101/3953] deCONZ - Dont update entry if data is equal --- homeassistant/components/deconz/config_flow.py | 10 ++++++---- tests/components/deconz/test_config_flow.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 650c02857509da..306a4fbf839243 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -157,8 +157,12 @@ async def _create_entry(self): async def _update_entry(self, entry, host): """Update existing entry.""" + if entry.data[CONF_HOST] == host: + return self.async_abort(reason="already_configured") + entry.data[CONF_HOST] = host self.hass.config_entries.async_update_entry(entry) + return self.async_abort(reason="updated_instance") async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" @@ -175,8 +179,7 @@ async def async_step_ssdp(self, discovery_info): if uuid in gateways: entry = gateways[uuid].config_entry - await self._update_entry(entry, discovery_info[CONF_HOST]) - return self.async_abort(reason="updated_instance") + return await self._update_entry(entry, discovery_info[CONF_HOST]) bridgeid = discovery_info[ATTR_SERIAL] if any( @@ -224,8 +227,7 @@ async def async_step_hassio(self, user_input=None): if bridgeid in gateway_entries: entry = gateway_entries[bridgeid] - await self._update_entry(entry, user_input[CONF_HOST]) - return self.async_abort(reason="updated_instance") + return await self._update_entry(entry, user_input[CONF_HOST]) self._hassio_discovery = user_input diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 8165c9df080dcf..ea3abead02870e 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -336,6 +336,24 @@ async def test_hassio_update_instance(hass): assert entry.data[config_flow.CONF_HOST] == "mock-deconz" +async def test_hassio_dont_update_instance(hass): + """Test we can update an existing config entry.""" + entry = MockConfigEntry( + domain=config_flow.DOMAIN, + data={config_flow.CONF_BRIDGEID: "id", config_flow.CONF_HOST: "1.2.3.4"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + data={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_SERIAL: "id"}, + context={"source": "hassio"}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + async def test_hassio_confirm(hass): """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( From 85a1726e69d4aa55a9d88468969d1995d0425f7e Mon Sep 17 00:00:00 2001 From: tyjtyj Date: Sun, 1 Sep 2019 16:24:54 +0800 Subject: [PATCH 0102/3953] Fix google_maps scan interval (#26328) Reported on https://github.com/home-assistant/home-assistant/issues/26275 --- 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 2149e40e5045f5..2b5550860ee5b5 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -52,7 +52,7 @@ def __init__(self, hass, config: ConfigType, see) -> None: self.see = see self.username = config[CONF_USERNAME] self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] - self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(60) + self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=60) credfile = "{}.{}".format( hass.config.path(CREDENTIALS_FILE), slugify(self.username) From f6cf4c38e741091b00a6b09a432839db517fb5e2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Sep 2019 22:31:00 -0700 Subject: [PATCH 0103/3953] Bumped version to 0.98.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2f2546378dbb5e..6d195da991e905 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 = "1" +PATCH_VERSION = "2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From ecaadfed3a1b6eaf822ea576d7b2010a8df0537e Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 2 Sep 2019 21:37:12 +1000 Subject: [PATCH 0104/3953] USGS Earthquakes icon for geolocation entities (#26353) * define icon * updated tests * added codeowner * updated codeowners --- CODEOWNERS | 1 + .../components/usgs_earthquakes_feed/geo_location.py | 5 +++++ homeassistant/components/usgs_earthquakes_feed/manifest.json | 4 +++- tests/components/usgs_earthquakes_feed/test_geo_location.py | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7c2e69c9af19d8..27c4f03ae93720 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -289,6 +289,7 @@ homeassistant/components/upcloud/* @scop homeassistant/components/updater/* @home-assistant/core homeassistant/components/upnp/* @robbiet480 homeassistant/components/uptimerobot/* @ludeeus +homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes homeassistant/components/velbus/* @cereal2nd homeassistant/components/velux/* @Julius2342 diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 7e5d6f5ebfef47..7890243c1e0acc 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -243,6 +243,11 @@ def _update_from_feed(self, feed_entry): self._type = feed_entry.type self._alert = feed_entry.alert + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:pulse" + @property def source(self) -> str: """Return source value of this external event.""" diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index 00aa23c3d4d0d3..0d1c116786ab6b 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -6,5 +6,7 @@ "geojson_client==0.4" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@exxamalte" + ] } diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index 69037e3b5f57f1..65ceec4d425efc 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -26,6 +26,7 @@ ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, + ATTR_ICON, ) from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_fire_time_changed @@ -148,6 +149,7 @@ async def test_setup(hass): ATTR_MAGNITUDE: 5.7, ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "usgs_earthquakes_feed", + ATTR_ICON: "mdi:pulse", } assert round(abs(float(state.state) - 15.5), 7) == 0 @@ -161,6 +163,7 @@ async def test_setup(hass): ATTR_FRIENDLY_NAME: "Title 2", ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "usgs_earthquakes_feed", + ATTR_ICON: "mdi:pulse", } assert round(abs(float(state.state) - 20.5), 7) == 0 @@ -174,6 +177,7 @@ async def test_setup(hass): ATTR_FRIENDLY_NAME: "Title 3", ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "usgs_earthquakes_feed", + ATTR_ICON: "mdi:pulse", } assert round(abs(float(state.state) - 25.5), 7) == 0 From ed5d3dba0e259a9648da13093a72769a2e7874c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 2 Sep 2019 16:51:59 +0300 Subject: [PATCH 0105/3953] Test with 3.6.1 in Travis (#26347) https://github.com/home-assistant/architecture/issues/278 --- .travis.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3447571a3e8571..525a4c8e72c1f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,18 +16,14 @@ addons: matrix: fast_finish: true include: - - python: "3.6.0" + - python: "3.6.1" env: TOXENV=lint - dist: trusty - - python: "3.6.0" + - python: "3.6.1" env: TOXENV=pylint - dist: trusty - - python: "3.6.0" + - python: "3.6.1" env: TOXENV=typing - dist: trusty - - python: "3.6.0" + - python: "3.6.1" env: TOXENV=py36 - dist: trusty - python: "3.7" env: TOXENV=py37 From 64465f0fbea1a8644d62df9b2aeba1ba1102b385 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 2 Sep 2019 21:15:00 +0200 Subject: [PATCH 0106/3953] Push to version 0.7.10 of denonavr (#26362) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 5e40dbb89da102..34699d666ad02d 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denonavr", "documentation": "https://www.home-assistant.io/components/denonavr", "requirements": [ - "denonavr==0.7.9" + "denonavr==0.7.10" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 54e27f8397246f..a6d8b0f414f4c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -384,7 +384,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.7.9 +denonavr==0.7.10 # homeassistant.components.directv directpy==0.5 From 85473d2c989fe45cad0a2b3989604e965ab697ac Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Mon, 2 Sep 2019 13:08:01 -0700 Subject: [PATCH 0107/3953] Bump androidtv to 0.0.26 and update tests (#26340) * Move the patchers to a separate file * Got a pytest test working (mostly) * Checkpoint * Switch to pytest for all tests * Bump androidtv to 0.0.26 and update tests * More robust patching * Remove unused constants * Combine two lines * Add 2 additional checks * Check that state objects are not None; add more description to tests * Use f strings --- .../components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/androidtv/patchers.py | 129 ++++++ .../components/androidtv/test_media_player.py | 393 +++++++++--------- 5 files changed, 337 insertions(+), 191 deletions(-) create mode 100644 tests/components/androidtv/patchers.py diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 91ea4019c05f74..2c8fc98e24865a 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.25" + "androidtv==0.0.26" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index a6d8b0f414f4c0..cda3ed77bf0685 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.25 +androidtv==0.0.26 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c76473cd3287a0..37e45278e33124 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -79,7 +79,7 @@ aiowwlln==1.0.0 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.25 +androidtv==0.0.26 # homeassistant.components.apns apns2==0.3.0 diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py new file mode 100644 index 00000000000000..86d1c1c15bdc02 --- /dev/null +++ b/tests/components/androidtv/patchers.py @@ -0,0 +1,129 @@ +"""Define patches used for androidtv tests.""" + +from socket import error as socket_error +from unittest.mock import patch + + +class AdbCommandsFake: + """A fake of the `adb.adb_commands.AdbCommands` class.""" + + def ConnectDevice(self, *args, **kwargs): # pylint: disable=invalid-name + """Try to connect to a device.""" + raise NotImplementedError + + def Shell(self, cmd): # pylint: disable=invalid-name + """Send an ADB shell command.""" + raise NotImplementedError + + +class AdbCommandsFakeSuccess(AdbCommandsFake): + """A fake of the `adb.adb_commands.AdbCommands` class when the connection attempt succeeds.""" + + def ConnectDevice(self, *args, **kwargs): # pylint: disable=invalid-name + """Successfully connect to a device.""" + return self + + +class AdbCommandsFakeFail(AdbCommandsFake): + """A fake of the `adb.adb_commands.AdbCommands` class when the connection attempt fails.""" + + def ConnectDevice( + self, *args, **kwargs + ): # pylint: disable=invalid-name, no-self-use + """Fail to connect to a device.""" + raise socket_error + + +class ClientFakeSuccess: + """A fake of the `adb_messenger.client.Client` class when the connection and shell commands succeed.""" + + def __init__(self, host="127.0.0.1", port=5037): + """Initialize a `ClientFakeSuccess` instance.""" + self._devices = [] + + def devices(self): + """Get a list of the connected devices.""" + return self._devices + + def device(self, serial): + """Mock the `Client.device` method when the device is connected via ADB.""" + device = DeviceFake(serial) + self._devices.append(device) + return device + + +class ClientFakeFail: + """A fake of the `adb_messenger.client.Client` class when the connection and shell commands fail.""" + + def __init__(self, host="127.0.0.1", port=5037): + """Initialize a `ClientFakeFail` instance.""" + self._devices = [] + + def devices(self): + """Get a list of the connected devices.""" + return self._devices + + def device(self, serial): + """Mock the `Client.device` method when the device is not connected via ADB.""" + self._devices = [] + + +class DeviceFake: + """A fake of the `adb_messenger.device.Device` class.""" + + def __init__(self, host): + """Initialize a `DeviceFake` instance.""" + self.host = host + + def get_serial_no(self): + """Get the serial number for the device (IP:PORT).""" + return self.host + + def shell(self, cmd): + """Send an ADB shell command.""" + raise NotImplementedError + + +def patch_connect(success): + """Mock the `adb.adb_commands.AdbCommands` and `adb_messenger.client.Client` classes.""" + + if success: + return { + "python": patch( + "androidtv.adb_manager.AdbCommands", AdbCommandsFakeSuccess + ), + "server": patch("androidtv.adb_manager.Client", ClientFakeSuccess), + } + return { + "python": patch("androidtv.adb_manager.AdbCommands", AdbCommandsFakeFail), + "server": patch("androidtv.adb_manager.Client", ClientFakeFail), + } + + +def patch_shell(response=None, error=False): + """Mock the `AdbCommandsFake.Shell` and `DeviceFake.shell` methods.""" + + def shell_success(self, cmd): + """Mock the `AdbCommandsFake.Shell` and `DeviceFake.shell` methods when they are successful.""" + self.shell_cmd = cmd + return response + + def shell_fail_python(self, cmd): + """Mock the `AdbCommandsFake.Shell` method when it fails.""" + self.shell_cmd = cmd + raise AttributeError + + def shell_fail_server(self, cmd): + """Mock the `DeviceFake.shell` method when it fails.""" + self.shell_cmd = cmd + raise ConnectionResetError + + if not error: + return { + "python": patch(f"{__name__}.AdbCommandsFake.Shell", shell_success), + "server": patch(f"{__name__}.DeviceFake.shell", shell_success), + } + return { + "python": patch(f"{__name__}.AdbCommandsFake.Shell", shell_fail_python), + "server": patch(f"{__name__}.DeviceFake.shell", shell_fail_server), + } diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index e787fddd3bcb04..39b392c97ee306 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,232 +1,249 @@ """The tests for the androidtv platform.""" import logging -from socket import error as socket_error -import unittest -from unittest.mock import patch +from homeassistant.setup import async_setup_component from homeassistant.components.androidtv.media_player import ( - AndroidTVDevice, - FireTVDevice, - setup, + ANDROIDTV_DOMAIN, + CONF_ADB_SERVER_IP, +) +from homeassistant.components.media_player.const import DOMAIN +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, + CONF_PLATFORM, + STATE_IDLE, + STATE_OFF, + STATE_UNAVAILABLE, ) +from . import patchers + + +# Android TV device with Python ADB implementation +CONFIG_ANDROIDTV_PYTHON_ADB = { + DOMAIN: { + CONF_PLATFORM: ANDROIDTV_DOMAIN, + CONF_HOST: "127.0.0.1", + CONF_NAME: "Android TV", + } +} + +# Android TV device with ADB server +CONFIG_ANDROIDTV_ADB_SERVER = { + DOMAIN: { + CONF_PLATFORM: ANDROIDTV_DOMAIN, + CONF_HOST: "127.0.0.1", + CONF_NAME: "Android TV", + CONF_ADB_SERVER_IP: "127.0.0.1", + } +} + +# Fire TV device with Python ADB implementation +CONFIG_FIRETV_PYTHON_ADB = { + DOMAIN: { + CONF_PLATFORM: ANDROIDTV_DOMAIN, + CONF_HOST: "127.0.0.1", + CONF_NAME: "Fire TV", + CONF_DEVICE_CLASS: "firetv", + } +} + +# Fire TV device with ADB server +CONFIG_FIRETV_ADB_SERVER = { + DOMAIN: { + CONF_PLATFORM: ANDROIDTV_DOMAIN, + CONF_HOST: "127.0.0.1", + CONF_NAME: "Fire TV", + CONF_DEVICE_CLASS: "firetv", + CONF_ADB_SERVER_IP: "127.0.0.1", + } +} + + +async def _test_reconnect(hass, caplog, config): + """Test that the error and reconnection attempts are logged correctly. + + "Handles device/service unavailable. Log a warning once when + unavailable, log once when reconnected." + + https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html + """ + if CONF_ADB_SERVER_IP not in config[DOMAIN]: + patch_key = "python" + else: + patch_key = "server" + + if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + entity_id = "media_player.android_tv" + else: + entity_id = "media_player.fire_tv" + + with patchers.patch_connect(True)[patch_key], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + caplog.clear() + caplog.set_level(logging.WARNING) + + with patchers.patch_connect(False)[patch_key], patchers.patch_shell(error=True)[ + patch_key + ]: + for _ in range(5): + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + assert len(caplog.record_tuples) == 2 + assert caplog.record_tuples[0][1] == logging.ERROR + assert caplog.record_tuples[1][1] == logging.WARNING + + caplog.set_level(logging.DEBUG) + with patchers.patch_connect(True)[patch_key], patchers.patch_shell("1")[patch_key]: + # Update 1 will reconnect + await hass.helpers.entity_component.async_update_entity(entity_id) + + # If using an ADB server, the state will get updated; otherwise, the + # state will be the last known state + state = hass.states.get(entity_id) + if patch_key == "server": + assert state.state == STATE_IDLE + else: + assert state.state == STATE_OFF + + # Update 2 will update the state, regardless of which ADB connection + # method is used + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_IDLE + + if patch_key == "python": + assert ( + "ADB connection to 127.0.0.1:5555 successfully established" + in caplog.record_tuples[2] + ) + else: + assert ( + "ADB connection to 127.0.0.1:5555 via ADB server 127.0.0.1:5037 successfully established" + in caplog.record_tuples[2] + ) -def connect_device_success(self, *args, **kwargs): - """Return `self`, which will result in the ADB connection being interpreted as available.""" - return self + return True -def connect_device_fail(self, *args, **kwargs): - """Raise a socket error.""" - raise socket_error +async def _test_adb_shell_returns_none(hass, config): + """Test the case that the ADB shell command returns `None`. + The state should be `None` and the device should be unavailable. + """ + if CONF_ADB_SERVER_IP not in config[DOMAIN]: + patch_key = "python" + else: + patch_key = "server" -def adb_shell_python_adb_error(self, cmd): - """Raise an error that is among those caught for the Python ADB implementation.""" - raise AttributeError + if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + entity_id = "media_player.android_tv" + else: + entity_id = "media_player.fire_tv" + with patchers.patch_connect(True)[patch_key], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state != STATE_UNAVAILABLE -def adb_shell_adb_server_error(self, cmd): - """Raise an error that is among those caught for the ADB server implementation.""" - raise ConnectionResetError + with patchers.patch_shell(None)[patch_key], patchers.patch_shell(error=True)[ + patch_key + ]: + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNAVAILABLE + return True -class AdbAvailable: - """A class that indicates the ADB connection is available.""" - def shell(self, cmd): - """Send an ADB shell command (ADB server implementation).""" - return "" +async def test_reconnect_androidtv_python_adb(hass, caplog): + """Test that the error and reconnection attempts are logged correctly. + * Device type: Android TV + * ADB connection method: Python ADB implementation -class AdbUnavailable: - """A class with ADB shell methods that raise errors.""" + """ + assert await _test_reconnect(hass, caplog, CONFIG_ANDROIDTV_PYTHON_ADB) - def __bool__(self): - """Return `False` to indicate that the ADB connection is unavailable.""" - return False - def shell(self, cmd): - """Raise an error that pertains to the Python ADB implementation.""" - raise ConnectionResetError +async def test_adb_shell_returns_none_androidtv_python_adb(hass): + """Test the case that the ADB shell command returns `None`. + * Device type: Android TV + * ADB connection method: Python ADB implementation -PATCH_PYTHON_ADB_CONNECT_SUCCESS = patch( - "adb.adb_commands.AdbCommands.ConnectDevice", connect_device_success -) -PATCH_PYTHON_ADB_COMMAND_SUCCESS = patch( - "adb.adb_commands.AdbCommands.Shell", return_value="" -) -PATCH_PYTHON_ADB_CONNECT_FAIL = patch( - "adb.adb_commands.AdbCommands.ConnectDevice", connect_device_fail -) -PATCH_PYTHON_ADB_COMMAND_FAIL = patch( - "adb.adb_commands.AdbCommands.Shell", adb_shell_python_adb_error -) -PATCH_PYTHON_ADB_COMMAND_NONE = patch( - "adb.adb_commands.AdbCommands.Shell", return_value=None -) + """ + assert await _test_adb_shell_returns_none(hass, CONFIG_ANDROIDTV_PYTHON_ADB) -PATCH_ADB_SERVER_CONNECT_SUCCESS = patch( - "adb_messenger.client.Client.device", return_value=AdbAvailable() -) -PATCH_ADB_SERVER_AVAILABLE = patch( - "androidtv.basetv.BaseTV.available", return_value=True -) -PATCH_ADB_SERVER_CONNECT_FAIL = patch( - "adb_messenger.client.Client.device", return_value=AdbUnavailable() -) -PATCH_ADB_SERVER_COMMAND_FAIL = patch( - "{}.AdbAvailable.shell".format(__name__), adb_shell_adb_server_error -) -PATCH_ADB_SERVER_COMMAND_NONE = patch( - "{}.AdbAvailable.shell".format(__name__), return_value=None -) +async def test_reconnect_firetv_python_adb(hass, caplog): + """Test that the error and reconnection attempts are logged correctly. -class TestAndroidTVPythonImplementation(unittest.TestCase): - """Test the androidtv media player for an Android TV device.""" + * Device type: Fire TV + * ADB connection method: Python ADB implementation - def setUp(self): - """Set up an `AndroidTVDevice` media player.""" - with PATCH_PYTHON_ADB_CONNECT_SUCCESS, PATCH_PYTHON_ADB_COMMAND_SUCCESS: - aftv = setup("IP:PORT", device_class="androidtv") - self.aftv = AndroidTVDevice(aftv, "Fake Android TV", {}, None, None) + """ + assert await _test_reconnect(hass, caplog, CONFIG_FIRETV_PYTHON_ADB) - def test_reconnect(self): - """Test that the error and reconnection attempts are logged correctly. - "Handles device/service unavailable. Log a warning once when - unavailable, log once when reconnected." +async def test_adb_shell_returns_none_firetv_python_adb(hass): + """Test the case that the ADB shell command returns `None`. - https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html - """ - with self.assertLogs(level=logging.WARNING) as logs: - with PATCH_PYTHON_ADB_CONNECT_FAIL, PATCH_PYTHON_ADB_COMMAND_FAIL: - for _ in range(5): - self.aftv.update() - self.assertFalse(self.aftv.available) - self.assertIsNone(self.aftv.state) + * Device type: Fire TV + * ADB connection method: Python ADB implementation - assert len(logs.output) == 2 - assert logs.output[0].startswith("ERROR") - assert logs.output[1].startswith("WARNING") + """ + assert await _test_adb_shell_returns_none(hass, CONFIG_FIRETV_PYTHON_ADB) - with self.assertLogs(level=logging.DEBUG) as logs: - with PATCH_PYTHON_ADB_CONNECT_SUCCESS, PATCH_PYTHON_ADB_COMMAND_SUCCESS: - # Update 1 will reconnect - self.aftv.update() - self.assertTrue(self.aftv.available) - # Update 2 will update the state - self.aftv.update() - self.assertTrue(self.aftv.available) - self.assertIsNotNone(self.aftv.state) +async def test_reconnect_androidtv_adb_server(hass, caplog): + """Test that the error and reconnection attempts are logged correctly. - assert ( - "ADB connection to {} successfully established".format(self.aftv.aftv.host) - in logs.output[0] - ) + * Device type: Android TV + * ADB connection method: ADB server - def test_adb_shell_returns_none(self): - """Test the case that the ADB shell command returns `None`. - - The state should be `None` and the device should be unavailable. - """ - with PATCH_PYTHON_ADB_COMMAND_NONE: - self.aftv.update() - self.assertFalse(self.aftv.available) - self.assertIsNone(self.aftv.state) - - with PATCH_PYTHON_ADB_CONNECT_SUCCESS, PATCH_PYTHON_ADB_COMMAND_SUCCESS: - # Update 1 will reconnect - self.aftv.update() - self.assertTrue(self.aftv.available) - - # Update 2 will update the state - self.aftv.update() - self.assertTrue(self.aftv.available) - self.assertIsNotNone(self.aftv.state) - - -class TestAndroidTVServerImplementation(unittest.TestCase): - """Test the androidtv media player for an Android TV device.""" - - def setUp(self): - """Set up an `AndroidTVDevice` media player.""" - with PATCH_ADB_SERVER_CONNECT_SUCCESS, PATCH_ADB_SERVER_AVAILABLE: - aftv = setup( - "IP:PORT", adb_server_ip="ADB_SERVER_IP", device_class="androidtv" - ) - self.aftv = AndroidTVDevice(aftv, "Fake Android TV", {}, None, None) - - def test_reconnect(self): - """Test that the error and reconnection attempts are logged correctly. - - "Handles device/service unavailable. Log a warning once when - unavailable, log once when reconnected." - - https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html - """ - with self.assertLogs(level=logging.WARNING) as logs: - with PATCH_ADB_SERVER_CONNECT_FAIL, PATCH_ADB_SERVER_COMMAND_FAIL: - for _ in range(5): - self.aftv.update() - self.assertFalse(self.aftv.available) - self.assertIsNone(self.aftv.state) - - assert len(logs.output) == 2 - assert logs.output[0].startswith("ERROR") - assert logs.output[1].startswith("WARNING") - - with self.assertLogs(level=logging.DEBUG) as logs: - with PATCH_ADB_SERVER_CONNECT_SUCCESS: - self.aftv.update() - self.assertTrue(self.aftv.available) - self.assertIsNotNone(self.aftv.state) + """ + assert await _test_reconnect(hass, caplog, CONFIG_ANDROIDTV_ADB_SERVER) - assert ( - "ADB connection to {} via ADB server {}:{} successfully established".format( - self.aftv.aftv.host, - self.aftv.aftv.adb_server_ip, - self.aftv.aftv.adb_server_port, - ) - in logs.output[0] - ) - def test_adb_shell_returns_none(self): - """Test the case that the ADB shell command returns `None`. +async def test_adb_shell_returns_none_androidtv_adb_server(hass): + """Test the case that the ADB shell command returns `None`. + + * Device type: Android TV + * ADB connection method: ADB server + + """ + assert await _test_adb_shell_returns_none(hass, CONFIG_ANDROIDTV_ADB_SERVER) - The state should be `None` and the device should be unavailable. - """ - with PATCH_ADB_SERVER_COMMAND_NONE: - self.aftv.update() - self.assertFalse(self.aftv.available) - self.assertIsNone(self.aftv.state) - with PATCH_ADB_SERVER_CONNECT_SUCCESS: - self.aftv.update() - self.assertTrue(self.aftv.available) - self.assertIsNotNone(self.aftv.state) +async def test_reconnect_firetv_adb_server(hass, caplog): + """Test that the error and reconnection attempts are logged correctly. + * Device type: Fire TV + * ADB connection method: ADB server -class TestFireTVPythonImplementation(TestAndroidTVPythonImplementation): - """Test the androidtv media player for a Fire TV device.""" + """ + assert await _test_reconnect(hass, caplog, CONFIG_FIRETV_ADB_SERVER) - def setUp(self): - """Set up a `FireTVDevice` media player.""" - with PATCH_PYTHON_ADB_CONNECT_SUCCESS, PATCH_PYTHON_ADB_COMMAND_SUCCESS: - aftv = setup("IP:PORT", device_class="firetv") - self.aftv = FireTVDevice(aftv, "Fake Fire TV", {}, True, None, None) +async def test_adb_shell_returns_none_firetv_adb_server(hass): + """Test the case that the ADB shell command returns `None`. -class TestFireTVServerImplementation(TestAndroidTVServerImplementation): - """Test the androidtv media player for a Fire TV device.""" + * Device type: Fire TV + * ADB connection method: ADB server - def setUp(self): - """Set up a `FireTVDevice` media player.""" - with PATCH_ADB_SERVER_CONNECT_SUCCESS, PATCH_ADB_SERVER_AVAILABLE: - aftv = setup( - "IP:PORT", adb_server_ip="ADB_SERVER_IP", device_class="firetv" - ) - self.aftv = FireTVDevice(aftv, "Fake Fire TV", {}, True, None, None) + """ + assert await _test_adb_shell_returns_none(hass, CONFIG_FIRETV_ADB_SERVER) From df90e9c4fdcb1a9864583597b802a2e2ed62d5a0 Mon Sep 17 00:00:00 2001 From: Costas Date: Mon, 2 Sep 2019 22:17:34 +0200 Subject: [PATCH 0108/3953] Update google_maps dependency and improve error message (#26361) * updated dependency and made error message a bit more accurate. * updated dependency and made error message a bit more accurate. --- homeassistant/components/google_maps/device_tracker.py | 5 +---- homeassistant/components/google_maps/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 2b5550860ee5b5..75f370e502eafb 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -67,10 +67,7 @@ def __init__(self, hass, config: ConfigType, see) -> None: except InvalidCookies: _LOGGER.error( - "You have specified invalid login credentials. " - "Please make sure you have saved your credentials" - " in the following file: %s", - credfile, + "The cookie file provided does not provide a valid session. Please create another one and try again." ) self.success_init = False diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index 5f31f533a38788..ec48e5252a8ab2 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -3,7 +3,7 @@ "name": "Google maps", "documentation": "https://www.home-assistant.io/components/google_maps", "requirements": [ - "locationsharinglib==4.0.2" + "locationsharinglib==4.1.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index cda3ed77bf0685..d96a7a76d457c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -748,7 +748,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.google_maps -locationsharinglib==4.0.2 +locationsharinglib==4.1.0 # homeassistant.components.logi_circle logi_circle==0.2.2 From 6a5f7bd8e42ae35ce60377391596126aa69103ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Mon, 2 Sep 2019 22:21:09 +0200 Subject: [PATCH 0109/3953] Update zigpy_zigate to 0.2.0 (#26327) * Update zigpy_zigate to 0.2.0 * Update requirements_all.txt --- 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 8e7de41e626b35..bf97dca1c708eb 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.2.2", "zigpy-homeassistant==0.7.1", "zigpy-xbee-homeassistant==0.4.0", - "zigpy-zigate==0.1.0" + "zigpy-zigate==0.2.0" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index d96a7a76d457c4..7fdf063ea54258 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2002,7 +2002,7 @@ zigpy-homeassistant==0.7.1 zigpy-xbee-homeassistant==0.4.0 # homeassistant.components.zha -zigpy-zigate==0.1.0 +zigpy-zigate==0.2.0 # homeassistant.components.zoneminder zm-py==0.3.3 From c6d839f8ae67d6949de35ac0394b94e035fcbdb7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 3 Sep 2019 07:12:10 +0200 Subject: [PATCH 0110/3953] String has nothing to do with class method naming (#26368) --- homeassistant/components/deconz/.translations/en.json | 2 +- homeassistant/components/deconz/strings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 57da3c706a035c..3c6656d6ae696b 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -43,7 +43,7 @@ }, "options": { "step": { - "async_step_deconz_devices": { + "deconz_devices": { "data": { "allow_clip_sensor": "Allow deCONZ CLIP sensors", "allow_deconz_groups": "Allow deCONZ light groups" diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index ea9ea2805155c6..7081f816e6ae08 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -43,7 +43,7 @@ }, "options": { "step": { - "async_step_deconz_devices": { + "deconz_devices": { "description": "Configure visibility of deCONZ device types", "data": { "allow_clip_sensor": "Allow deCONZ CLIP sensors", From df9703d814f412099e677325cffebb1562163067 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Sep 2019 09:35:03 +0200 Subject: [PATCH 0111/3953] Add token support --- azure-pipelines-release.yml | 6 +++--- azure-pipelines-translation.yml | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 azure-pipelines-translation.yml diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 7c88e615fa5baa..510e16a351c14f 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -68,11 +68,11 @@ stages: - script: python setup.py sdist bdist_wheel displayName: 'Build package' - script: | - export TWINE_USERNAME="$(twineUser)" - export TWINE_PASSWORD="$(twinePassword)" - twine upload dist/* --skip-existing displayName: 'Upload pypi' + env: + TWINE_USERNAME: '$(twineUser)' + TWINE_PASSWORD: '$(twinePassword)' - job: 'ReleaseDocker' timeoutInMinutes: 240 pool: diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml new file mode 100644 index 00000000000000..ad536aa9068d1c --- /dev/null +++ b/azure-pipelines-translation.yml @@ -0,0 +1,27 @@ +# https://dev.azure.com/home-assistant + +trigger: + batch: true + branches: + include: + - dev +pr: none +variables: + - group: translation + + +jobs: + +- job: 'Upload' + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: '3.7' + - script: | + ./script/translations_upload + displayName: 'Upload Translation' + env: + LOKALISE_TOKEN: $(lokaliseToken) From 0cffd6148176a8ce73b33dd4ffaf6289d1feb43d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Sep 2019 00:50:24 -0700 Subject: [PATCH 0112/3953] Allow passing dictionaries to async_register_entity_service (#26370) --- homeassistant/components/light/__init__.py | 9 ++++++--- homeassistant/components/switch/__init__.py | 15 +++------------ homeassistant/helpers/entity_component.py | 3 +++ 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index c70a209a35ac9b..94ba43b85458d9 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -128,9 +128,12 @@ } ) -LIGHT_TURN_OFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {ATTR_TRANSITION: VALID_TRANSITION, ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG])} -) + +LIGHT_TURN_OFF_SCHEMA = { + ATTR_TRANSITION: VALID_TRANSITION, + ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), +} + LIGHT_TOGGLE_SCHEMA = LIGHT_TURN_ON_SCHEMA diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 348c2a8616bc6c..aa7459d1d3cec7 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -10,7 +10,6 @@ from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, - ENTITY_SERVICE_SCHEMA, ) from homeassistant.const import ( STATE_ON, @@ -68,17 +67,9 @@ async def async_setup(hass, config): ) await component.async_setup(config) - component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" - ) - - component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" - ) - - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" - ) + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") return True diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index a923763570285e..42b19da889ece1 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -15,6 +15,7 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery +from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.loader import bind_hass, async_get_integration from homeassistant.util import slugify @@ -202,6 +203,8 @@ async def async_extract_from_service(self, service, expand_group=True): @callback def async_register_entity_service(self, name, schema, func, required_features=None): """Register an entity service.""" + if isinstance(schema, dict): + schema = ENTITY_SERVICE_SCHEMA.extend(schema) async def handle_service(call): """Handle the service.""" From 245450a402979f29bb2d14034b8645db9eff807b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 3 Sep 2019 10:17:03 +0200 Subject: [PATCH 0113/3953] Upgrade pyhaversion to 3.1.0 (#26232) --- homeassistant/components/version/manifest.json | 2 +- homeassistant/components/version/sensor.py | 12 ++++++++++-- requirements_all.txt | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 2a48f91a6f8887..815e7ff9a25794 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -3,7 +3,7 @@ "name": "Version", "documentation": "https://www.home-assistant.io/components/version", "requirements": [ - "pyhaversion==3.0.2" + "pyhaversion==3.1.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 438ea8f690cda4..3e00b87e9840d2 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -28,7 +28,7 @@ "odroid-c2", "odroid-xu", ] -ALL_SOURCES = ["local", "pypi", "hassio", "docker"] +ALL_SOURCES = ["local", "pypi", "hassio", "docker", "haio"] CONF_BETA = "beta" CONF_IMAGE = "image" @@ -54,7 +54,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Version sensor platform.""" - from pyhaversion import LocalVersion, DockerVersion, HassioVersion, PyPiVersion + from pyhaversion import ( + LocalVersion, + DockerVersion, + HassioVersion, + PyPiVersion, + HaIoVersion, + ) beta = config.get(CONF_BETA) image = config.get(CONF_IMAGE) @@ -74,6 +80,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= haversion = VersionData(HassioVersion(hass.loop, session, branch, image)) elif source == "docker": haversion = VersionData(DockerVersion(hass.loop, session, branch, image)) + elif source == "haio": + haversion = VersionData(HaIoVersion(hass.loop, session)) else: haversion = VersionData(LocalVersion(hass.loop, session)) diff --git a/requirements_all.txt b/requirements_all.txt index 7fdf063ea54258..60e580296d0cec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1193,7 +1193,7 @@ pygtfs==0.1.5 pygtt==1.1.2 # homeassistant.components.version -pyhaversion==3.0.2 +pyhaversion==3.1.0 # homeassistant.components.heos pyheos==0.6.0 From c2a752e34fefa270d55aa42be7af1a0b75999e11 Mon Sep 17 00:00:00 2001 From: "ruohan.chen" Date: Tue, 3 Sep 2019 16:18:08 +0800 Subject: [PATCH 0114/3953] Support new climate arch for zhong_hong (#26309) * support new climate arch for zhong_hong * use black to format * mapping the states between lib and HA * Add zhong_hong mapping constant --- .../components/zhong_hong/climate.py | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 8514ec711cbc3a..f1a363cfedecd5 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -10,6 +10,7 @@ HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -46,7 +47,26 @@ } ) -SUPPORT_HVAC = [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY] +SUPPORT_HVAC = [ + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, +] + +ZHONG_HONG_MODE_COOL = "cool" +ZHONG_HONG_MODE_HEAT = "heat" +ZHONG_HONG_MODE_DRY = "dry" +ZHONG_HONG_MODE_FAN_ONLY = "fan_only" + + +MODE_TO_STATE = { + ZHONG_HONG_MODE_COOL: HVAC_MODE_COOL, + ZHONG_HONG_MODE_HEAT: HVAC_MODE_HEAT, + ZHONG_HONG_MODE_DRY: HVAC_MODE_DRY, + ZHONG_HONG_MODE_FAN_ONLY: HVAC_MODE_FAN_ONLY, +} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -117,7 +137,9 @@ def _after_update(self, climate): """Handle state update.""" _LOGGER.debug("async update ha state") if self._device.current_operation: - self._current_operation = self._device.current_operation.lower() + self._current_operation = MODE_TO_STATE[ + self._device.current_operation.lower() + ] if self._device.current_temperature: self._current_temperature = self._device.current_temperature if self._device.current_fan_mode: @@ -156,7 +178,9 @@ def temperature_unit(self): @property def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" - return self._current_operation + if self.is_on: + return self._current_operation + return HVAC_MODE_OFF @property def hvac_modes(self): @@ -223,6 +247,14 @@ def set_temperature(self, **kwargs): def set_hvac_mode(self, hvac_mode): """Set new target operation mode.""" + if hvac_mode == HVAC_MODE_OFF: + if self.is_on: + self.turn_off() + return + + if not self.is_on: + self.turn_on() + self._device.set_operation_mode(hvac_mode.upper()) def set_fan_mode(self, fan_mode): From 950f8343d39dffc01f799f001dbb61dc934fab9e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Sep 2019 10:39:23 +0200 Subject: [PATCH 0115/3953] Update azure-pipelines-wheels.yml --- 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 2a557d87ee7985..eec3f678981b53 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -30,7 +30,7 @@ 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' + 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;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev' builderPip: 'Cython;numpy' wheelsRequirement: 'requirements_wheels.txt' wheelsRequirementDiff: 'requirements_diff.txt' From 617133e465947f491fb5f96ecc703944632bc995 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Sep 2019 14:13:44 +0200 Subject: [PATCH 0116/3953] Correct file permissions, removing executable bits (#26376) --- homeassistant/components/bizkaibus/sensor.py | 0 homeassistant/components/cppm_tracker/device_tracker.py | 0 homeassistant/components/environment_canada/camera.py | 0 homeassistant/components/environment_canada/sensor.py | 0 homeassistant/components/fortios/device_tracker.py | 0 homeassistant/components/lcn/binary_sensor.py | 0 homeassistant/components/lcn/cover.py | 0 homeassistant/components/lcn/scene.py | 0 homeassistant/components/lcn/sensor.py | 0 homeassistant/components/lcn/services.py | 0 homeassistant/components/lcn/services.yaml | 0 homeassistant/components/lcn/switch.py | 0 homeassistant/components/rejseplanen/sensor.py | 0 homeassistant/components/repetier/__init__.py | 0 homeassistant/components/repetier/manifest.json | 0 homeassistant/components/repetier/sensor.py | 0 homeassistant/components/somfy_mylink/__init__.py | 0 homeassistant/components/somfy_mylink/cover.py | 0 homeassistant/components/yale_smart_alarm/alarm_control_panel.py | 0 homeassistant/components/zwave/lock.py | 0 20 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 homeassistant/components/bizkaibus/sensor.py mode change 100755 => 100644 homeassistant/components/cppm_tracker/device_tracker.py mode change 100755 => 100644 homeassistant/components/environment_canada/camera.py mode change 100755 => 100644 homeassistant/components/environment_canada/sensor.py mode change 100755 => 100644 homeassistant/components/fortios/device_tracker.py mode change 100755 => 100644 homeassistant/components/lcn/binary_sensor.py mode change 100755 => 100644 homeassistant/components/lcn/cover.py mode change 100755 => 100644 homeassistant/components/lcn/scene.py mode change 100755 => 100644 homeassistant/components/lcn/sensor.py mode change 100755 => 100644 homeassistant/components/lcn/services.py mode change 100755 => 100644 homeassistant/components/lcn/services.yaml mode change 100755 => 100644 homeassistant/components/lcn/switch.py mode change 100755 => 100644 homeassistant/components/rejseplanen/sensor.py mode change 100755 => 100644 homeassistant/components/repetier/__init__.py mode change 100755 => 100644 homeassistant/components/repetier/manifest.json mode change 100755 => 100644 homeassistant/components/repetier/sensor.py mode change 100755 => 100644 homeassistant/components/somfy_mylink/__init__.py mode change 100755 => 100644 homeassistant/components/somfy_mylink/cover.py mode change 100755 => 100644 homeassistant/components/yale_smart_alarm/alarm_control_panel.py mode change 100755 => 100644 homeassistant/components/zwave/lock.py diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json old mode 100755 new mode 100644 diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py old mode 100755 new mode 100644 From 3534b8a9772974a43ceff0d6954068fba51ea1e6 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 3 Sep 2019 14:14:33 +0200 Subject: [PATCH 0117/3953] Fix race during initial Sonos group construction (#26371) * Fix race during initial Sonos group construction * Update homeassistant/components/sonos/media_player.py --- homeassistant/components/sonos/media_player.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 86e30621334579..70461ad15d2fa6 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -337,8 +337,16 @@ def __init__(self, player): async def async_added_to_hass(self): """Subscribe sonos events.""" await self.async_seen() + self.hass.data[DATA_SONOS].entities.append(self) + def _rebuild_groups(): + """Build the current group topology.""" + for entity in self.hass.data[DATA_SONOS].entities: + entity.update_groups() + + self.hass.async_add_executor_job(_rebuild_groups) + @property def unique_id(self): """Return a unique ID.""" @@ -469,10 +477,6 @@ def _attach_player(self): self.update_volume() self._set_favorites() - # New player available, build the current group topology - for entity in self.hass.data[DATA_SONOS].entities: - entity.update_groups() - player = self.soco def subscribe(service, action): From ad516157187d19bbbd015338d952f996ed800130 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Sep 2019 16:11:36 +0200 Subject: [PATCH 0118/3953] Use literal string interpolation in integrations A (f-strings) (#26377) * Use literal string interpolation in integrations A (f-strings) * Black --- homeassistant/components/adguard/sensor.py | 4 +-- homeassistant/components/airvisual/sensor.py | 8 +++--- .../components/aladdin_connect/cover.py | 2 +- .../alarmdecoder/alarm_control_panel.py | 10 +++---- homeassistant/components/alexa/errors.py | 4 +-- homeassistant/components/alexa/handlers.py | 4 +-- homeassistant/components/alexa/intent.py | 2 +- .../components/alexa/state_report.py | 6 ++-- .../components/alpha_vantage/sensor.py | 2 +- .../components/ambiclimate/config_flow.py | 2 +- .../components/ambient_station/__init__.py | 4 +-- homeassistant/components/amcrest/camera.py | 4 +-- homeassistant/components/amcrest/helpers.py | 2 +- homeassistant/components/ampio/air_quality.py | 2 +- .../android_ip_webcam/binary_sensor.py | 2 +- .../components/android_ip_webcam/sensor.py | 2 +- .../components/android_ip_webcam/switch.py | 2 +- .../components/androidtv/media_player.py | 2 +- .../components/apache_kafka/__init__.py | 2 +- homeassistant/components/apns/notify.py | 8 +++--- .../components/apple_tv/media_player.py | 2 +- .../components/aprs/device_tracker.py | 5 ++-- homeassistant/components/arcam_fmj/const.py | 4 +-- .../components/arcam_fmj/media_player.py | 2 +- .../components/arest/binary_sensor.py | 8 ++---- homeassistant/components/arest/sensor.py | 6 ++-- homeassistant/components/arest/switch.py | 28 ++++++------------- homeassistant/components/august/camera.py | 2 +- homeassistant/components/august/lock.py | 2 +- .../components/aurora/binary_sensor.py | 2 +- .../components/aurora_abb_powerone/sensor.py | 2 +- .../components/auth/mfa_setup_flow.py | 8 ++---- homeassistant/components/awair/sensor.py | 4 +-- homeassistant/components/axis/axis_base.py | 4 +-- homeassistant/components/axis/camera.py | 2 +- homeassistant/components/axis/config_flow.py | 6 ++-- homeassistant/components/axis/device.py | 8 +++--- 37 files changed, 74 insertions(+), 95 deletions(-) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 17e53270f257be..e0c86e42d26677 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -132,7 +132,7 @@ def __init__(self, adguard): async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" percentage = await self.adguard.stats.blocked_percentage() - self._state = "{:.2f}".format(percentage) + self._state = f"{percentage:.2f}" class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): @@ -205,7 +205,7 @@ def __init__(self, adguard): async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" average = await self.adguard.stats.avg_processing_time() - self._state = "{:.2f}".format(average) + self._state = f"{average:.2f}" class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index ec2dea6903189b..20e5196c0f1fdd 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -194,7 +194,7 @@ def state(self): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return "{0}_{1}_{2}".format(self._location_id, self._locale, self._type) + return f"{self._location_id}_{self._locale}_{self._type}" @property def unit_of_measurement(self): @@ -210,7 +210,7 @@ async def async_update(self): return if self._type == SENSOR_TYPE_LEVEL: - aqi = data["aqi{0}".format(self._locale)] + aqi = data[f"aqi{self._locale}"] [level] = [ i for i in POLLUTANT_LEVEL_MAPPING @@ -219,9 +219,9 @@ async def async_update(self): self._state = level["label"] self._icon = level["icon"] elif self._type == SENSOR_TYPE_AQI: - self._state = data["aqi{0}".format(self._locale)] + self._state = data[f"aqi{self._locale}"] elif self._type == SENSOR_TYPE_POLLUTANT: - symbol = data["main{0}".format(self._locale)] + symbol = data[f"main{self._locale}"] self._state = POLLUTANT_MAPPING[symbol]["label"] self._attrs.update( { diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 770b663e8a3f21..b3da4fb4cbc3f0 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -85,7 +85,7 @@ def supported_features(self): @property def unique_id(self): """Return a unique ID.""" - return "{}-{}".format(self._device_id, self._number) + return f"{self._device_id}-{self._number}" @property def name(self): diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index c05dfd30d21634..288c1dfd1c75a8 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -140,27 +140,27 @@ def device_state_attributes(self): def alarm_disarm(self, code=None): """Send disarm command.""" if code: - self.hass.data[DATA_AD].send("{!s}1".format(code)) + self.hass.data[DATA_AD].send(f"{code!s}1") def alarm_arm_away(self, code=None): """Send arm away command.""" if code: - self.hass.data[DATA_AD].send("{!s}2".format(code)) + self.hass.data[DATA_AD].send(f"{code!s}2") def alarm_arm_home(self, code=None): """Send arm home command.""" if code: - self.hass.data[DATA_AD].send("{!s}3".format(code)) + self.hass.data[DATA_AD].send(f"{code!s}3") def alarm_arm_night(self, code=None): """Send arm night command.""" if code: - self.hass.data[DATA_AD].send("{!s}33".format(code)) + self.hass.data[DATA_AD].send(f"{code!s}33") def alarm_toggle_chime(self, code=None): """Send toggle chime command.""" if code: - self.hass.data[DATA_AD].send("{!s}9".format(code)) + self.hass.data[DATA_AD].send(f"{code!s}9") def alarm_keypress(self, keypress): """Send custom keypresses.""" diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 56202b23e20e0b..8c2fa692267fb0 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -40,7 +40,7 @@ class AlexaInvalidEndpointError(AlexaError): def __init__(self, endpoint_id): """Initialize invalid endpoint error.""" - msg = "The endpoint {} does not exist".format(endpoint_id) + msg = f"The endpoint {endpoint_id} does not exist" AlexaError.__init__(self, msg) self.endpoint_id = endpoint_id @@ -73,7 +73,7 @@ def __init__(self, hass, temp, min_temp, max_temp): "maximumValue": {"value": max_temp, "scale": API_TEMP_UNITS[unit]}, } payload = {"validRange": temp_range} - msg = "The requested temperature {} is out of range".format(temp) + msg = f"The requested temperature {temp} is out of range" AlexaError.__init__(self, msg, payload) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index cd5b56d60e2790..1e636b96ee5205 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -744,7 +744,7 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): presets = entity.attributes.get(climate.ATTR_PRESET_MODES, []) if ha_preset not in presets: - msg = "The requested thermostat mode {} is not supported".format(ha_preset) + msg = f"The requested thermostat mode {ha_preset} is not supported" raise AlexaUnsupportedThermostatModeError(msg) service = climate.SERVICE_SET_PRESET_MODE @@ -754,7 +754,7 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) ha_mode = next((k for k, v in API_THERMOSTAT_MODES.items() if v == mode), None) if ha_mode not in operation_list: - msg = "The requested thermostat mode {} is not supported".format(mode) + msg = f"The requested thermostat mode {mode} is not supported" raise AlexaUnsupportedThermostatModeError(msg) service = climate.SERVICE_SET_HVAC_MODE diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index edeb6865aad8f5..4cb75c65bc9758 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -113,7 +113,7 @@ async def async_handle_message(hass, message): handler = HANDLERS.get(req_type) if not handler: - raise UnknownRequest("Received unknown request {}".format(req_type)) + raise UnknownRequest(f"Received unknown request {req_type}") return await handler(hass, message) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 1e22d5fc09f23b..e956abec7ad997 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -60,7 +60,7 @@ async def async_send_changereport_message( """ token = await config.async_get_access_token() - headers = {"Authorization": "Bearer {}".format(token)} + headers = {"Authorization": f"Bearer {token}"} endpoint = alexa_entity.alexa_id() @@ -122,7 +122,7 @@ async def async_send_add_or_update_message(hass, config, entity_ids): """ token = await config.async_get_access_token() - headers = {"Authorization": "Bearer {}".format(token)} + headers = {"Authorization": f"Bearer {token}"} endpoints = [] @@ -152,7 +152,7 @@ async def async_send_delete_message(hass, config, entity_ids): """ token = await config.async_get_access_token() - headers = {"Authorization": "Bearer {}".format(token)} + headers = {"Authorization": f"Bearer {token}"} endpoints = [] diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 6d790e0719b61a..188567e4cf4276 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -168,7 +168,7 @@ def __init__(self, foreign_exchange, config): if CONF_NAME in config: self._name = config.get(CONF_NAME) else: - self._name = "{}/{}".format(self._to_currency, self._from_currency) + self._name = f"{self._to_currency}/{self._from_currency}" self._unit_of_measurement = self._to_currency self._icon = ICONS.get(self._from_currency, "USD") self.values = None diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index db6d42d1d5c611..99563dcb97de1a 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -130,7 +130,7 @@ def _generate_oauth(self): return oauth def _cb_url(self): - return "{}{}".format(self.hass.config.api.base_url, AUTH_CALLBACK_PATH) + return f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" async def _get_authorize_url(self): oauth = self._generate_oauth() diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 82c29f79983055..bff03eb422b231 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -492,7 +492,7 @@ def device_info(self): @property def name(self): """Return the name of the sensor.""" - return "{0}_{1}".format(self._station_name, self._sensor_name) + return f"{self._station_name}_{self._sensor_name}" @property def should_poll(self): @@ -502,7 +502,7 @@ def should_poll(self): @property def unique_id(self): """Return a unique, unchanging string that represents this sensor.""" - return "{0}_{1}".format(self._mac_address, self._sensor_type) + return f"{self._mac_address}_{self._sensor_type}" async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 483bdb2c7cf604..f75a5adbe9cd0a 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -490,7 +490,7 @@ def _goto_preset(self, preset): self._api.go_to_preset(action="start", preset_point_number=preset) except AmcrestError as error: log_update_error( - _LOGGER, "move", self.name, "camera to preset {}".format(preset), error + _LOGGER, "move", self.name, f"camera to preset {preset}", error ) def _set_color_bw(self, cbw): @@ -499,7 +499,7 @@ def _set_color_bw(self, cbw): self._api.day_night_color = _CBW.index(cbw) except AmcrestError as error: log_update_error( - _LOGGER, "set", self.name, "camera color mode to {}".format(cbw), error + _LOGGER, "set", self.name, f"camera color mode to {cbw}", error ) else: self._color_bw = cbw diff --git a/homeassistant/components/amcrest/helpers.py b/homeassistant/components/amcrest/helpers.py index d24d6e0e7071df..a40d6ace50a31d 100644 --- a/homeassistant/components/amcrest/helpers.py +++ b/homeassistant/components/amcrest/helpers.py @@ -4,7 +4,7 @@ def service_signal(service, ident=None): """Encode service and identifier into signal.""" - signal = "{}_{}".format(DOMAIN, service) + signal = f"{DOMAIN}_{service}" if ident: signal += "_{}".format(ident.replace(".", "_")) return signal diff --git a/homeassistant/components/ampio/air_quality.py b/homeassistant/components/ampio/air_quality.py index f55f20fc150259..e63f59839a80fd 100644 --- a/homeassistant/components/ampio/air_quality.py +++ b/homeassistant/components/ampio/air_quality.py @@ -57,7 +57,7 @@ def name(self): @property def unique_id(self): """Return unique_name.""" - return "ampio_smog_{}".format(self._station_id) + return f"ampio_smog_{self._station_id}" @property def particulate_matter_2_5(self): diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index d7bf009701d0ca..0e9cca46afbfd2 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -25,7 +25,7 @@ def __init__(self, name, host, ipcam, sensor): self._sensor = sensor self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._unit = None diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index 20f4acebca61db..05c1fe16c61c2d 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -39,7 +39,7 @@ def __init__(self, name, host, ipcam, sensor): self._sensor = sensor self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._unit = None diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index 5b2f5dad5e1052..2d5f2412d85285 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -39,7 +39,7 @@ def __init__(self, name, host, ipcam, setting): self._setting = setting self._mapped_name = KEY_MAP.get(self._setting, self._setting) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = False @property diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 2db210b56f3e52..d68f47b1b0a3b7 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -392,7 +392,7 @@ def adb_command(self, cmd): """Send an ADB command to an Android TV / Fire TV device.""" key = self._keys.get(cmd) if key: - self.aftv.adb_shell("input keyevent {}".format(key)) + self.aftv.adb_shell(f"input keyevent {key}") self._adb_response = None self.schedule_update_ha_state() return diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index caf96c61fb87b5..e0c8b824913e48 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -81,7 +81,7 @@ def __init__(self, hass, ip_address, port, topic, entities_filter): self._hass = hass self._producer = AIOKafkaProducer( loop=hass.loop, - bootstrap_servers="{0}:{1}".format(ip_address, port), + bootstrap_servers=f"{ip_address}:{port}", compression_type="gzip", ) self._topic = topic diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index 0b95cb9f0cb32a..dbd45013a3ce95 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -50,7 +50,7 @@ def get_service(hass, config, discovery_info=None): service = ApnsNotificationService(hass, name, topic, sandbox, cert_file) hass.services.register( - DOMAIN, "apns_{}".format(name), service.register, schema=REGISTER_SERVICE_SCHEMA + DOMAIN, f"apns_{name}", service.register, schema=REGISTER_SERVICE_SCHEMA ) return service @@ -98,7 +98,7 @@ def full_tracking_device_id(self): The full id of a device that is tracked by the device tracking component. """ - return "{}.{}".format(DEVICE_TRACKER_DOMAIN, self.tracking_id) + return f"{DEVICE_TRACKER_DOMAIN}.{self.tracking_id}" @property def disabled(self): @@ -124,9 +124,9 @@ def _write_device(out, device): """Write a single device to file.""" attributes = [] if device.name is not None: - attributes.append("name: {}".format(device.name)) + attributes.append(f"name: {device.name}") if device.tracking_device_id is not None: - attributes.append("tracking_device_id: {}".format(device.tracking_device_id)) + attributes.append(f"tracking_device_id: {device.tracking_device_id}") if device.disabled: attributes.append("disabled: True") diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 3e77c7a1cfd8e3..9ac5ba77f98d48 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -213,7 +213,7 @@ def media_title(self): title = self._playing.title return title if title else "No title" - return "Establishing a connection to {0}...".format(self._name) + return f"Establishing a connection to {self._name}..." @property def supported_features(self): diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index c5ae8ed8414554..86b0b6f48afd65 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -70,7 +70,7 @@ def gps_accuracy(gps, posambiguity: int) -> int: accuracy = round(dist_m) else: - message = "APRS position ambiguity must be 0-4, not '{0}'.".format(posambiguity) + message = f"APRS position ambiguity must be 0-4, not '{posambiguity}'." raise ValueError(message) return accuracy @@ -147,8 +147,7 @@ def run(self): ) self.ais.connect() self.start_complete( - True, - "Connected to {0} with callsign {1}.".format(self.host, self.callsign), + True, f"Connected to {self.host} with callsign {self.callsign}." ) self.ais.consumer(callback=self.rx_msg, immortal=True) except (AprsConnectionError, LoginError) as err: diff --git a/homeassistant/components/arcam_fmj/const.py b/homeassistant/components/arcam_fmj/const.py index b065e1a0833290..dc5a576acec067 100644 --- a/homeassistant/components/arcam_fmj/const.py +++ b/homeassistant/components/arcam_fmj/const.py @@ -9,5 +9,5 @@ DEFAULT_NAME = "Arcam FMJ" DEFAULT_SCAN_INTERVAL = 5 -DOMAIN_DATA_ENTRIES = "{}.entries".format(DOMAIN) -DOMAIN_DATA_CONFIG = "{}.config".format(DOMAIN) +DOMAIN_DATA_ENTRIES = f"{DOMAIN}.entries" +DOMAIN_DATA_CONFIG = f"{DOMAIN}.config" diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 971abc3e26d5ab..231e9821dc6ddc 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -319,7 +319,7 @@ def media_title(self): channel = self.media_channel if channel: - value = "{} - {}".format(source.name, channel) + value = f"{source.name} - {channel}" else: value = source.name return value diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 96ffa371864682..669a28b707800b 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -73,9 +73,7 @@ def __init__(self, arest, resource, name, device_class, pin): self._pin = pin if self._pin is not None: - request = requests.get( - "{}/mode/{}/i".format(self._resource, self._pin), timeout=10 - ) + request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10) if request.status_code != 200: _LOGGER.error("Can't set mode of %s", self._resource) @@ -112,9 +110,7 @@ def __init__(self, resource, pin): def update(self): """Get the latest data from aREST device.""" try: - response = requests.get( - "{}/digital/{}".format(self._resource, self._pin), timeout=10 - ) + response = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) self.data = {"state": response.json()["return_value"]} except requests.exceptions.ConnectionError: _LOGGER.error("No route to device '%s'", self._resource) diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 533adeccb5eb1b..2416eeb0ebb08f 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -148,9 +148,7 @@ def __init__( self._renderer = renderer if self._pin is not None: - request = requests.get( - "{}/mode/{}/i".format(self._resource, self._pin), timeout=10 - ) + request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10) if request.status_code != 200: _LOGGER.error("Can't set mode of %s", self._resource) @@ -212,7 +210,7 @@ def update(self): self.data = {"value": response.json()["return_value"]} except TypeError: response = requests.get( - "{}/digital/{}".format(self._resource, self._pin), timeout=10 + f"{self._resource}/digital/{self._pin}", timeout=10 ) self.data = {"value": response.json()["return_value"]} self.available = True diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index 558df89100eed7..e1a7edacb7e59b 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -114,7 +114,7 @@ def __init__(self, resource, location, name, func): super().__init__(resource, location, name) self._func = func - request = requests.get("{}/{}".format(self._resource, self._func), timeout=10) + request = requests.get(f"{self._resource}/{self._func}", timeout=10) if request.status_code != 200: _LOGGER.error("Can't find function") @@ -130,9 +130,7 @@ def __init__(self, resource, location, name, func): def turn_on(self, **kwargs): """Turn the device on.""" request = requests.get( - "{}/{}".format(self._resource, self._func), - timeout=10, - params={"params": "1"}, + f"{self._resource}/{self._func}", timeout=10, params={"params": "1"} ) if request.status_code == 200: @@ -143,9 +141,7 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn the device off.""" request = requests.get( - "{}/{}".format(self._resource, self._func), - timeout=10, - params={"params": "0"}, + f"{self._resource}/{self._func}", timeout=10, params={"params": "0"} ) if request.status_code == 200: @@ -158,9 +154,7 @@ def turn_off(self, **kwargs): def update(self): """Get the latest data from aREST API and update the state.""" try: - request = requests.get( - "{}/{}".format(self._resource, self._func), timeout=10 - ) + request = requests.get(f"{self._resource}/{self._func}", timeout=10) self._state = request.json()["return_value"] != 0 self._available = True except requests.exceptions.ConnectionError: @@ -177,9 +171,7 @@ def __init__(self, resource, location, name, pin, invert): self._pin = pin self.invert = invert - request = requests.get( - "{}/mode/{}/o".format(self._resource, self._pin), timeout=10 - ) + request = requests.get(f"{self._resource}/mode/{self._pin}/o", timeout=10) if request.status_code != 200: _LOGGER.error("Can't set mode") self._available = False @@ -188,8 +180,7 @@ def turn_on(self, **kwargs): """Turn the device on.""" turn_on_payload = int(not self.invert) request = requests.get( - "{}/digital/{}/{}".format(self._resource, self._pin, turn_on_payload), - timeout=10, + f"{self._resource}/digital/{self._pin}/{turn_on_payload}", timeout=10 ) if request.status_code == 200: self._state = True @@ -200,8 +191,7 @@ def turn_off(self, **kwargs): """Turn the device off.""" turn_off_payload = int(self.invert) request = requests.get( - "{}/digital/{}/{}".format(self._resource, self._pin, turn_off_payload), - timeout=10, + f"{self._resource}/digital/{self._pin}/{turn_off_payload}", timeout=10 ) if request.status_code == 200: self._state = False @@ -211,9 +201,7 @@ def turn_off(self, **kwargs): def update(self): """Get the latest data from aREST API and update the state.""" try: - request = requests.get( - "{}/digital/{}".format(self._resource, self._pin), timeout=10 - ) + request = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) status_value = int(self.invert) self._state = request.json()["return_value"] != status_value self._available = True diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index a8335d1aa52ffe..2492eb754181ab 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -73,4 +73,4 @@ def camera_image(self): @property def unique_id(self) -> str: """Get the unique id of the camera.""" - return "{:s}_camera".format(self._doorbell.device_id) + return f"{self._doorbell.device_id:s}_camera" diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index e919c47dd4c2e2..8b8c019eb2db90 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -93,4 +93,4 @@ def device_state_attributes(self): @property def unique_id(self) -> str: """Get the unique id of the lock.""" - return "{:s}_lock".format(self._lock.device_id) + return f"{self._lock.device_id:s}_lock" diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index 0d983f35e37ffa..a69433c418617f 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -64,7 +64,7 @@ def __init__(self, aurora_data, name): @property def name(self): """Return the name of the sensor.""" - return "{}".format(self._name) + return f"{self._name}" @property def is_on(self): diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 456b508048443f..05ed5fa99bf2b1 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -49,7 +49,7 @@ class AuroraABBSolarPVMonitorSensor(Entity): def __init__(self, client, name, typename): """Initialize the sensor.""" - self._name = "{} {}".format(name, typename) + self._name = f"{name} {typename}" self.client = client self._state = None diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index c18bc276a449cb..42dab7ebb5a0d6 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -34,7 +34,7 @@ async def _async_create_setup_flow(handler, context, data): """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)) + raise ValueError(f"Mfa module {handler} is not found") user_id = data.pop("user_id") return await mfa_module.async_setup_flow(user_id) @@ -80,9 +80,7 @@ async def async_setup_flow(msg): if mfa_module is None: connection.send_message( websocket_api.error_message( - msg["id"], - "no_module", - "MFA module {} is not found".format(mfa_module_id), + msg["id"], "no_module", f"MFA module {mfa_module_id} is not found" ) ) return @@ -117,7 +115,7 @@ async def async_depose(msg): websocket_api.error_message( msg["id"], "disable_failed", - "Cannot disable MFA Module {}: {}".format(mfa_module_id, err), + f"Cannot disable MFA Module {mfa_module_id}: {err}", ) ) return diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 85b5a0be191533..c899e0097964d9 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -150,7 +150,7 @@ def __init__(self, data, device, sensor_type, throttle): """Initialize the sensor.""" self._uuid = device[CONF_UUID] self._device_class = SENSOR_TYPES[sensor_type]["device_class"] - self._name = "Awair {}".format(self._device_class) + self._name = f"Awair {self._device_class}" unit = SENSOR_TYPES[sensor_type]["unit_of_measurement"] self._unit_of_measurement = unit self._data = data @@ -202,7 +202,7 @@ def available(self): @property def unique_id(self): """Return the unique id of this entity.""" - return "{}_{}".format(self._uuid, self._type) + return f"{self._uuid}_{self._type}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 3864ac344e1487..f22a169a1023de 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -72,7 +72,7 @@ def device_class(self): @property def name(self): """Return the name of the event.""" - return "{} {} {}".format(self.device.name, self.event.TYPE, self.event.id) + return f"{self.device.name} {self.event.TYPE} {self.event.id}" @property def should_poll(self): @@ -82,4 +82,4 @@ def should_poll(self): @property def unique_id(self): """Return a unique identifier for this device.""" - return "{}-{}-{}".format(self.device.serial, self.event.topic, self.event.id) + return f"{self.device.serial}-{self.event.topic}-{self.event.id}" diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index e7e0f7459f30e6..a55e45dd37468d 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -92,4 +92,4 @@ def _new_address(self): @property def unique_id(self): """Return a unique identifier for this device.""" - return "{}-camera".format(self.device.serial) + return f"{self.device.serial}-camera" diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index f93e49d9818c25..3b5efe96760efd 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -137,9 +137,9 @@ async def _create_entry(self): if entry.data[CONF_MODEL] == self.model ] - self.name = "{}".format(self.model) + self.name = f"{self.model}" for idx in range(len(same_model) + 1): - self.name = "{} {}".format(self.model, idx) + self.name = f"{self.model} {idx}" if self.name not in same_model: break @@ -150,7 +150,7 @@ async def _create_entry(self): CONF_MODEL: self.model, } - title = "{} - {}".format(self.model, self.serial_number) + title = f"{self.model} - {self.serial_number}" return self.async_create_entry(title=title, data=data) async def _update_entry(self, entry, host): diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 465d8c73b74205..3b91f7e147456b 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -65,7 +65,7 @@ async def async_update_device_registry(self): connections={(CONNECTION_NETWORK_MAC, self.serial)}, identifiers={(DOMAIN, self.serial)}, manufacturer="Axis Communications AB", - model="{} {}".format(self.model, self.product_type), + model=f"{self.model} {self.product_type}", name=self.name, sw_version=self.fw_version, ) @@ -115,7 +115,7 @@ async def async_setup(self): @property def event_new_address(self): """Device specific event to signal new device address.""" - return "axis_new_address_{}".format(self.serial) + return f"axis_new_address_{self.serial}" @staticmethod async def async_new_address_callback(hass, entry): @@ -131,7 +131,7 @@ async def async_new_address_callback(hass, entry): @property def event_reachable(self): """Device specific event to signal a change in connection status.""" - return "axis_reachable_{}".format(self.serial) + return f"axis_reachable_{self.serial}" @callback def async_connection_status_callback(self, status): @@ -149,7 +149,7 @@ def async_connection_status_callback(self, status): @property def event_new_sensor(self): """Device specific event to signal new sensor available.""" - return "axis_add_sensor_{}".format(self.serial) + return f"axis_add_sensor_{self.serial}" @callback def async_event_callback(self, action, event_id): From 105461edb52f19b9e38508993d3ba5216a7c61b6 Mon Sep 17 00:00:00 2001 From: ThaStealth Date: Tue, 3 Sep 2019 17:05:23 +0200 Subject: [PATCH 0119/3953] Remove solaredge_local duplicate code (#25941) * Removed duplicate code * Update sensor.py Splitted exceptions into two seperate ones * Update sensor.py * Update sensor.py * Update sensor.py Fixed linting errors * Update sensor.py --- .../components/solaredge_local/sensor.py | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 4bf015a74893bd..80bd8e1f61ed8c 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -149,25 +149,19 @@ def update(self): try: response = self.api.get_status() _LOGGER.debug("response from SolarEdge: %s", response) + except (ConnectTimeout): + _LOGGER.error("Connection timeout, skipping update") + return + except (HTTPError): + _LOGGER.error("Could not retrieve data, skipping update") + return + try: self.data["energyTotal"] = response.energy.total self.data["energyThisYear"] = response.energy.thisYear self.data["energyThisMonth"] = response.energy.thisMonth self.data["energyToday"] = response.energy.today self.data["currentPower"] = response.powerWatt - _LOGGER.debug("Updated SolarEdge overview data: %s", self.data) except AttributeError: - _LOGGER.error("Missing details data in solaredge response") - _LOGGER.debug("Response is: %s", response) - return - except (ConnectTimeout, HTTPError): - _LOGGER.error("Could not retrieve data, skipping update") - return - - self.data["energyTotal"] = response.energy.total - self.data["energyThisYear"] = response.energy.thisYear - self.data["energyThisMonth"] = response.energy.thisMonth - self.data["energyToday"] = response.energy.today - self.data["currentPower"] = response.powerWatt - _LOGGER.debug("Updated SolarEdge overview data: %s", self.data) + _LOGGER.error("Missing details data in SolarEdge response") From 6a24d893c81c8719f4267dfb5cd5decb8d09bf51 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Sep 2019 17:09:59 +0200 Subject: [PATCH 0120/3953] Use literal string interpolation in integrations B-D (f-strings) (#26378) --- homeassistant/components/bbox/sensor.py | 2 +- .../components/blackbird/media_player.py | 2 +- .../components/blink/alarm_control_panel.py | 2 +- .../components/blink/binary_sensor.py | 4 +-- homeassistant/components/blink/camera.py | 4 +-- homeassistant/components/blink/sensor.py | 4 +-- homeassistant/components/blinkt/light.py | 2 +- homeassistant/components/bloomsky/__init__.py | 2 +- .../components/bloomsky/binary_sensor.py | 2 +- homeassistant/components/bloomsky/sensor.py | 4 +-- .../components/bluesound/media_player.py | 16 +++++------ .../bluetooth_tracker/device_tracker.py | 2 +- homeassistant/components/bme280/sensor.py | 2 +- homeassistant/components/bme680/sensor.py | 2 +- .../bmw_connected_drive/binary_sensor.py | 12 ++++---- .../components/bmw_connected_drive/lock.py | 4 +-- .../components/bmw_connected_drive/sensor.py | 4 +-- homeassistant/components/bom/camera.py | 4 +-- homeassistant/components/bom/sensor.py | 2 +- .../components/broadlink/__init__.py | 2 +- homeassistant/components/broadlink/switch.py | 8 +++--- homeassistant/components/buienradar/sensor.py | 2 +- homeassistant/components/camera/__init__.py | 16 ++++------- homeassistant/components/canary/sensor.py | 2 +- .../components/cisco_webex_teams/notify.py | 4 +-- homeassistant/components/clicksend/notify.py | 4 +-- .../components/clicksend_tts/notify.py | 4 +-- homeassistant/components/cloud/http_api.py | 2 +- homeassistant/components/cmus/media_player.py | 2 +- homeassistant/components/co2signal/sensor.py | 2 +- homeassistant/components/coinbase/sensor.py | 4 +-- .../concord232/alarm_control_panel.py | 2 +- .../components/concord232/binary_sensor.py | 2 +- .../components/crimereports/sensor.py | 2 +- homeassistant/components/cups/sensor.py | 2 +- .../components/currencylayer/sensor.py | 2 +- homeassistant/components/daikin/sensor.py | 2 +- homeassistant/components/daikin/switch.py | 2 +- homeassistant/components/darksky/sensor.py | 8 +++--- homeassistant/components/datadog/__init__.py | 6 ++-- .../components/ddwrt/device_tracker.py | 6 ++-- homeassistant/components/deconz/gateway.py | 4 +-- homeassistant/components/deluge/sensor.py | 2 +- .../components/deutsche_bahn/sensor.py | 2 +- homeassistant/components/dht/sensor.py | 2 +- .../components/digitalloggers/switch.py | 2 +- homeassistant/components/doorbird/__init__.py | 14 ++++------ homeassistant/components/doorbird/switch.py | 4 +-- .../components/downloader/__init__.py | 8 +++--- .../components/duke_energy/sensor.py | 2 +- .../components/dwd_weather_warnings/sensor.py | 28 +++++++++---------- homeassistant/components/dyson/sensor.py | 12 ++++---- 52 files changed, 116 insertions(+), 126 deletions(-) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 76621b7792b5df..b59b166e41f145 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -91,7 +91,7 @@ def __init__(self, bbox_data, sensor_type, name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index a77fad69663f46..eca7fa84f504fe 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -99,7 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = [] for zone_id, extra in config[CONF_ZONES].items(): _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) - unique_id = "{}-{}".format(connection, zone_id) + unique_id = f"{connection}-{zone_id}" device = BlackbirdZone(blackbird, sources, zone_id, extra[CONF_NAME]) hass.data[DATA_BLACKBIRD][unique_id] = device devices.append(device) diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index adcefeddf239e8..b1c9f6a7ec07fd 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -55,7 +55,7 @@ def state(self): @property def name(self): """Return the name of the panel.""" - return "{} {}".format(BLINK_DATA, self._name) + return f"{BLINK_DATA} {self._name}" @property def device_state_attributes(self): diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 4c268989d32bac..e8c01953bffd62 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -26,11 +26,11 @@ def __init__(self, data, camera, sensor_type): self.data = data self._type = sensor_type name, icon = BINARY_SENSORS[sensor_type] - self._name = "{} {} {}".format(BLINK_DATA, camera, name) + self._name = f"{BLINK_DATA} {camera} {name}" self._icon = icon self._camera = data.cameras[camera] self._state = None - self._unique_id = "{}-{}".format(self._camera.serial, self._type) + self._unique_id = f"{self._camera.serial}-{self._type}" @property def name(self): diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 5e8b5323f896f0..52043324a40f99 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -30,9 +30,9 @@ def __init__(self, data, name, camera): """Initialize a camera.""" super().__init__() self.data = data - self._name = "{} {}".format(BLINK_DATA, name) + self._name = f"{BLINK_DATA} {name}" self._camera = camera - self._unique_id = "{}-camera".format(camera.serial) + self._unique_id = f"{camera.serial}-camera" self.response = None self.current_image = None self.last_image = None diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index fba2d0bd493b33..81616b463ecfbe 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -28,7 +28,7 @@ class BlinkSensor(Entity): def __init__(self, data, camera, sensor_type): """Initialize sensors from Blink camera.""" name, units, icon = SENSORS[sensor_type] - self._name = "{} {} {}".format(BLINK_DATA, camera, name) + self._name = f"{BLINK_DATA} {camera} {name}" self._camera_name = name self._type = sensor_type self.data = data @@ -36,7 +36,7 @@ def __init__(self, data, camera, sensor_type): self._state = None self._unit_of_measurement = units self._icon = icon - self._unique_id = "{}-{}".format(self._camera.serial, self._type) + self._unique_id = f"{self._camera.serial}-{self._type}" self._sensor_key = self._type if self._type == "temperature": self._sensor_key = "temperature_calibrated" diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py index 9fee72662c6449..e626a73d287c64 100644 --- a/homeassistant/components/blinkt/light.py +++ b/homeassistant/components/blinkt/light.py @@ -51,7 +51,7 @@ def __init__(self, blinkt, name, index): Default brightness and white color. """ self._blinkt = blinkt - self._name = "{}_{}".format(name, index) + self._name = f"{name}_{index}" self._index = index self._is_on = False self._brightness = 255 diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index dc0723730c4899..6373471fe7a6c6 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -63,7 +63,7 @@ def refresh_devices(self): """Use the API to retrieve a list of devices.""" _LOGGER.debug("Fetching BloomSky update") response = requests.get( - "{}?{}".format(self.API_URL, self._endpoint_argument), + f"{self.API_URL}?{self._endpoint_argument}", headers={AUTHORIZATION: self._api_key}, timeout=10, ) diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 3a8242929c5e56..99951fcf5c54e8 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -42,7 +42,7 @@ def __init__(self, bs, device, sensor_name): self._sensor_name = sensor_name self._name = "{} {}".format(device["DeviceName"], sensor_name) self._state = None - self._unique_id = "{}-{}".format(self._device_id, self._sensor_name) + self._unique_id = f"{self._device_id}-{self._sensor_name}" @property def unique_id(self): diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index cca57bcae82d70..18f60036397b9f 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -72,7 +72,7 @@ def __init__(self, bs, device, sensor_name): self._sensor_name = sensor_name self._name = "{} {}".format(device["DeviceName"], sensor_name) self._state = None - self._unique_id = "{}-{}".format(self._device_id, self._sensor_name) + self._unique_id = f"{self._device_id}-{self._sensor_name}" @property def unique_id(self): @@ -103,6 +103,6 @@ def update(self): state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] if self._sensor_name in FORMAT_NUMBERS: - self._state = "{0:.2f}".format(state) + self._state = f"{state:.2f}" else: self._state = state diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index e5f264b5f73c65..bf0568aed16c29 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -336,7 +336,7 @@ async def send_bluesound_command( if method[0] == "/": method = method[1:] - url = "http://{}:{}/{}".format(self.host, self.port, method) + url = f"http://{self.host}:{self.port}/{method}" _LOGGER.debug("Calling URL: %s", url) response = None @@ -380,8 +380,8 @@ async def async_update_status(self): etag = self._status.get("@etag", "") if etag != "": - url = "Status?etag={}&timeout=120.0".format(etag) - url = "http://{}:{}/{}".format(self.host, self.port, url) + url = f"Status?etag={etag}&timeout=120.0" + url = f"http://{self.host}:{self.port}/{url}" _LOGGER.debug("Calling URL: %s", url) @@ -595,7 +595,7 @@ def media_image_url(self): if not url: return if url[0] == "/": - url = "http://{}:{}{}".format(self.host, self.port, url) + url = f"http://{self.host}:{self.port}{url}" return url @@ -843,13 +843,13 @@ async def async_unjoin(self): async def async_add_slave(self, slave_device): """Add slave to master.""" return await self.send_bluesound_command( - "/AddSlave?slave={}&port={}".format(slave_device.host, slave_device.port) + f"/AddSlave?slave={slave_device.host}&port={slave_device.port}" ) async def async_remove_slave(self, slave_device): """Remove slave to master.""" return await self.send_bluesound_command( - "/RemoveSlave?slave={}&port={}".format(slave_device.host, slave_device.port) + f"/RemoveSlave?slave={slave_device.host}&port={slave_device.port}" ) async def async_increase_timer(self): @@ -870,7 +870,7 @@ async def async_clear_timer(self): async def async_set_shuffle(self, shuffle): """Enable or disable shuffle mode.""" value = "1" if shuffle else "0" - return await self.send_bluesound_command("/Shuffle?state={}".format(value)) + return await self.send_bluesound_command(f"/Shuffle?state={value}") async def async_select_source(self, source): """Select input source.""" @@ -967,7 +967,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): if self.is_grouped and not self.is_master: return - url = "Play?url={}".format(media_id) + url = f"Play?url={media_id}" if kwargs.get(ATTR_MEDIA_ENQUEUE): return await self.send_bluesound_command(url) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 65db87fa072aba..e760f91070a163 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -54,7 +54,7 @@ def see_device(mac, name, rssi=None): if rssi is not None: attributes["rssi"] = rssi see( - mac="{}{}".format(BT_PREFIX, mac), + mac=f"{BT_PREFIX}{mac}", host_name=name, attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH, diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index bdd91e6dfe1bdf..ee4e1731156c17 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -147,7 +147,7 @@ def __init__(self, bme280_client, sensor_type, temp_unit, name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 58b343b3de0ad6..20fdfc9ee79f6f 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -331,7 +331,7 @@ def __init__(self, bme680_client, sensor_type, temp_unit, name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 418ccbabffe17b..c9cc9b2d33373f 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -61,8 +61,8 @@ def __init__( self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = "{} {}".format(self._vehicle.name, self._attribute) - self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) + self._name = f"{self._vehicle.name} {self._attribute}" + self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._sensor_name = sensor_name self._device_class = device_class self._icon = icon @@ -177,16 +177,14 @@ def update(self): def _format_cbs_report(self, report): result = {} service_type = report.service_type.lower().replace("_", " ") - result["{} status".format(service_type)] = report.state.value + result[f"{service_type} status"] = report.state.value if report.due_date is not None: - result["{} date".format(service_type)] = report.due_date.strftime( - "%Y-%m-%d" - ) + result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d") if report.due_distance is not None: distance = round( self.hass.config.units.length(report.due_distance, LENGTH_KILOMETERS) ) - result["{} distance".format(service_type)] = "{} {}".format( + result[f"{service_type} distance"] = "{} {}".format( distance, self.hass.config.units.length_unit ) return result diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index a16dbc6b341b65..2055b442dcd17a 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -30,8 +30,8 @@ def __init__(self, account, vehicle, attribute: str, sensor_name): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = "{} {}".format(self._vehicle.name, self._attribute) - self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) + self._name = f"{self._vehicle.name} {self._attribute}" + self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._sensor_name = sensor_name self._state = None diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 8248ded4f8bcef..011908d54585e4 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -68,8 +68,8 @@ def __init__(self, account, vehicle, attribute: str, attribute_info): self._account = account self._attribute = attribute self._state = None - self._name = "{} {}".format(self._vehicle.name, self._attribute) - self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) + self._name = f"{self._vehicle.name} {self._attribute}" + self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._attribute_info = attribute_info @property diff --git a/homeassistant/components/bom/camera.py b/homeassistant/components/bom/camera.py index 3a5d6cdc503f32..f417cf769a40ac 100644 --- a/homeassistant/components/bom/camera.py +++ b/homeassistant/components/bom/camera.py @@ -84,7 +84,7 @@ def _validate_schema(config): LOCATIONS_MSG = "Set '{}' to one of: {}".format( CONF_LOCATION, ", ".join(sorted(LOCATIONS)) ) -XOR_MSG = "Specify exactly one of '{}' or '{}'".format(CONF_ID, CONF_LOCATION) +XOR_MSG = f"Specify exactly one of '{CONF_ID}' or '{CONF_LOCATION}'" PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA.extend( @@ -106,7 +106,7 @@ def _validate_schema(config): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up BOM radar-loop camera component.""" location = config.get(CONF_LOCATION) or "ID {}".format(config.get(CONF_ID)) - name = config.get(CONF_NAME) or "BOM Radar Loop - {}".format(location) + name = config.get(CONF_NAME) or f"BOM Radar Loop - {location}" args = [ config.get(x) for x in (CONF_LOCATION, CONF_ID, CONF_DELTA, CONF_FRAMES, CONF_OUTFILE) diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 790b2ddc74fef2..33444f1099652a 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -117,7 +117,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): CONF_WMO_ID, ) elif zone_id and wmo_id: - station = "{}.{}".format(zone_id, wmo_id) + station = f"{zone_id}.{wmo_id}" else: station = closest_station( config.get(CONF_LATITUDE), diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 5fb5af2732b01b..589da62feaa13b 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -64,7 +64,7 @@ async def _learn_command(call): packet = await hass.async_add_executor_job(device.check_data) if packet: data = b64encode(packet).decode("utf8") - log_msg = "Received packet is: {}".format(data) + log_msg = f"Received packet is: {data}" _LOGGER.info(log_msg) hass.components.persistent_notification.async_create( log_msg, title="Broadlink switch" diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 277260c0336e03..d60331aaa4407e 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -103,9 +103,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def _get_mp1_slot_name(switch_friendly_name, slot): """Get slot name.""" - if not slots["slot_{}".format(slot)]: - return "{} slot {}".format(switch_friendly_name, slot) - return slots["slot_{}".format(slot)] + if not slots[f"slot_{slot}"]: + return f"{switch_friendly_name} slot {slot}" + return slots[f"slot_{slot}"] if switch_type in RM_TYPES: broadlink_device = broadlink.rm((ip_addr, 80), mac_addr, None) @@ -371,7 +371,7 @@ def get_outlet_status(self, slot): """Get status of outlet from cached status list.""" if self._states is None: return None - return self._states["s{}".format(slot)] + return self._states[f"s{slot}"] @Throttle(TIME_BETWEEN_UPDATES) def update(self): diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 841cc428bac37a..ef65db74f165e7 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -401,7 +401,7 @@ def unique_id(self): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 597d67fcdeec63..68cd1f51dda198 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -134,7 +134,7 @@ async def async_request_stream(hass, entity_id, fmt): if not source: raise HomeAssistantError( - "{} does not support play stream service".format(camera.entity_id) + f"{camera.entity_id} does not support play stream service" ) return request_stream(hass, source, fmt=fmt, keepalive=camera_prefs.preload_stream) @@ -534,9 +534,7 @@ async def handle(self, request, camera): # Compose camera stream from stills interval = float(request.query.get("interval")) if interval < MIN_STREAM_INTERVAL: - raise ValueError( - "Stream interval must be be > {}".format(MIN_STREAM_INTERVAL) - ) + raise ValueError(f"Stream interval must be be > {MIN_STREAM_INTERVAL}") return await camera.handle_async_still_stream(request, interval) except ValueError: raise web.HTTPBadRequest() @@ -588,7 +586,7 @@ async def ws_camera_stream(hass, connection, msg): if not source: raise HomeAssistantError( - "{} does not support play stream service".format(camera.entity_id) + f"{camera.entity_id} does not support play stream service" ) fmt = msg["format"] @@ -670,7 +668,7 @@ async def async_handle_play_stream_service(camera, service_call): if not source: raise HomeAssistantError( - "{} does not support play stream service".format(camera.entity_id) + f"{camera.entity_id} does not support play stream service" ) hass = camera.hass @@ -681,7 +679,7 @@ async def async_handle_play_stream_service(camera, service_call): url = request_stream(hass, source, fmt=fmt, keepalive=camera_prefs.preload_stream) data = { ATTR_ENTITY_ID: entity_ids, - ATTR_MEDIA_CONTENT_ID: "{}{}".format(hass.config.api.base_url, url), + ATTR_MEDIA_CONTENT_ID: f"{hass.config.api.base_url}{url}", ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], } @@ -696,9 +694,7 @@ async def async_handle_record_service(camera, call): source = await camera.stream_source() if not source: - raise HomeAssistantError( - "{} does not support record service".format(camera.entity_id) - ) + raise HomeAssistantError(f"{camera.entity_id} does not support record service") hass = camera.hass filename = call.data[CONF_FILENAME] diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index dcb54a772a3c59..6bb01c9d114804 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -53,7 +53,7 @@ def __init__(self, data, sensor_type, location, device): self._sensor_value = None sensor_type_name = sensor_type[0].replace("_", " ").title() - self._name = "{} {} {}".format(location.name, device.name, sensor_type_name) + self._name = f"{location.name} {device.name} {sensor_type_name}" @property def name(self): diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index 9feac3207adf6d..a77f5673df723e 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -52,9 +52,7 @@ def send_message(self, message="", **kwargs): title = "{}{}".format(kwargs.get(ATTR_TITLE), "
") try: - self.client.messages.create( - roomId=self.room, html="{}{}".format(title, message) - ) + self.client.messages.create(roomId=self.room, html=f"{title}{message}") except ApiError as api_error: _LOGGER.error( "Could not send CiscoWebexTeams notification. " "Error: %s", api_error diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 1ec828b4a28bf8..87fc217ac42396 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -73,7 +73,7 @@ def send_message(self, message="", **kwargs): } ) - api_url = "{}/sms/send".format(BASE_API_URL) + api_url = f"{BASE_API_URL}/sms/send" resp = requests.post( api_url, data=json.dumps(data), @@ -94,7 +94,7 @@ def send_message(self, message="", **kwargs): def _authenticate(config): """Authenticate with ClickSend.""" - api_url = "{}/account".format(BASE_API_URL) + api_url = f"{BASE_API_URL}/account" resp = requests.get( api_url, headers=HEADERS, diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index 7c73c346a3317c..ba30c61e937f9a 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -79,7 +79,7 @@ def send_message(self, message="", **kwargs): } ] } - api_url = "{}/voice/send".format(BASE_API_URL) + api_url = f"{BASE_API_URL}/voice/send" resp = requests.post( api_url, data=json.dumps(data), @@ -100,7 +100,7 @@ def send_message(self, message="", **kwargs): def _authenticate(config): """Authenticate with ClickSend.""" - api_url = "{}/account".format(BASE_API_URL) + api_url = f"{BASE_API_URL}/account" resp = requests.get( api_url, headers=HEADERS, diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index d261c9e494c107..fce530ddce5dc7 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -157,7 +157,7 @@ def _process_cloud_exception(exc, where): err_info = _CLOUD_ERRORS.get(exc.__class__) if err_info is None: _LOGGER.exception("Unexpected error processing request for %s", where) - err_info = (502, "Unexpected error: {}".format(exc)) + err_info = (502, f"Unexpected error: {exc}") return err_info diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index a491bcc09ee149..dbaa763c46174b 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -82,7 +82,7 @@ def __init__(self, server, password, port, name): if server: self.cmus = remote.PyCmus(server=server, password=password, port=port) - auto_name = "cmus-{}".format(server) + auto_name = f"cmus-{server}" else: self.cmus = remote.PyCmus() auto_name = "cmus-local" diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index d881482ed1a2fd..9098a053fff2ae 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -68,7 +68,7 @@ def __init__(self, token, country_code, lat, lon): lat=round(self._latitude, 2), lon=round(self._longitude, 2) ) - self._friendly_name = "CO2 intensity - {}".format(device_name) + self._friendly_name = f"CO2 intensity - {device_name}" @property def name(self): diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index c5f53ef609d874..4a3e85d5e4313d 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -44,7 +44,7 @@ class AccountSensor(Entity): def __init__(self, coinbase_data, name, currency): """Initialize the sensor.""" self._coinbase_data = coinbase_data - self._name = "Coinbase {}".format(name) + self._name = f"Coinbase {name}" self._state = None self._unit_of_measurement = currency self._native_balance = None @@ -97,7 +97,7 @@ def __init__(self, coinbase_data, exchange_currency, native_currency): """Initialize the sensor.""" self._coinbase_data = coinbase_data self.currency = exchange_currency - self._name = "{} Exchange Rate".format(exchange_currency) + self._name = f"{exchange_currency} Exchange Rate" self._state = None self._unit_of_measurement = native_currency diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 2383700f42af14..68f0d77e307a82 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = "http://{}:{}".format(host, port) + url = f"http://{host}:{port}" try: add_entities([Concord232Alarm(url, name, code, mode)], True) diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 89b6ab6af97e5a..10643f134d70e8 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: _LOGGER.debug("Initializing client") - client = concord232_client.Client("http://{}:{}".format(host, port)) + client = concord232_client.Client(f"http://{host}:{port}") client.zones = client.list_zones() client.last_zone_update = datetime.datetime.now() diff --git a/homeassistant/components/crimereports/sensor.py b/homeassistant/components/crimereports/sensor.py index 2ad31e7513b6cd..6295125b7cabd0 100644 --- a/homeassistant/components/crimereports/sensor.py +++ b/homeassistant/components/crimereports/sensor.py @@ -29,7 +29,7 @@ DOMAIN = "crimereports" -EVENT_INCIDENT = "{}_incident".format(DOMAIN) +EVENT_INCIDENT = f"{DOMAIN}_incident" SCAN_INTERVAL = timedelta(minutes=30) diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 79bb050a617ad4..f6a5133d8a9851 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -333,7 +333,7 @@ def update(self): else: for ipp_printer in self._ipp_printers: self.attributes[ipp_printer] = conn.getPrinterAttributes( - uri="ipp://{}:{}/{}".format(self._host, self._port, ipp_printer) + uri=f"ipp://{self._host}:{self._port}/{ipp_printer}" ) self.available = True diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index dbafae551876a0..d4660d70286d8c 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -95,7 +95,7 @@ def update(self): self.rest.update() value = self.rest.data if value is not None: - self._state = round(value["{}{}".format(self._base, self._quote)], 4) + self._state = round(value[f"{self._base}{self._quote}"], 4) class CurrencylayerData: diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index c55988b8dc144e..f83566e66e8a7f 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -58,7 +58,7 @@ def __init__(self, api, monitored_state, units: UnitSystem, name=None) -> None: @property def unique_id(self): """Return a unique ID.""" - return "{}-{}".format(self._api.mac, self._device_attribute) + return f"{self._api.mac}-{self._device_attribute}" @property def icon(self): diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 6290e6fecef2f3..4d3b0d3eadea7f 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -44,7 +44,7 @@ def __init__(self, daikin_api, zone_id): @property def unique_id(self): """Return a unique ID.""" - return "{}-zone{}".format(self._api.mac, self._zone_id) + return f"{self._api.mac}-zone{self._zone_id}" @property def icon(self): diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 0f33935c66c919..4f000253245f1f 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -553,10 +553,10 @@ def __init__( def name(self): """Return the name of the sensor.""" if self.forecast_day is not None: - return "{} {} {}d".format(self.client_name, self._name, self.forecast_day) + return f"{self.client_name} {self._name} {self.forecast_day}d" if self.forecast_hour is not None: - return "{} {} {}h".format(self.client_name, self._name, self.forecast_hour) - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name} {self.forecast_hour}h" + return f"{self.client_name} {self._name}" @property def state(self): @@ -704,7 +704,7 @@ def __init__(self, forecast_data, sensor_type, name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/datadog/__init__.py b/homeassistant/components/datadog/__init__.py index 1ad4ed9aab8e77..5517e41d5c66df 100644 --- a/homeassistant/components/datadog/__init__.py +++ b/homeassistant/components/datadog/__init__.py @@ -59,7 +59,7 @@ def logbook_entry_listener(event): statsd.event( title="Home Assistant", - text="%%% \n **{}** {} \n %%%".format(name, message), + text=f"%%% \n **{name}** {message} \n %%%", tags=[ "entity:{}".format(event.data.get("entity_id")), "domain:{}".format(event.data.get("domain")), @@ -79,8 +79,8 @@ def state_changed_listener(event): return states = dict(state.attributes) - metric = "{}.{}".format(prefix, state.domain) - tags = ["entity:{}".format(state.entity_id)] + metric = f"{prefix}.{state.domain}" + tags = [f"entity:{state.entity_id}"] for key, value in states.items(): if isinstance(value, (float, int)): diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 4a40561b9e3485..4e661376719ec9 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -65,7 +65,7 @@ def __init__(self, config): self.mac2name = {} # Test the router is accessible - url = "{}://{}/Status_Wireless.live.asp".format(self.protocol, self.host) + url = f"{self.protocol}://{self.host}/Status_Wireless.live.asp" data = self.get_ddwrt_data(url) if not data: raise ConnectionError("Cannot connect to DD-Wrt router") @@ -80,7 +80,7 @@ def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" # If not initialised and not already scanned and not found. if device not in self.mac2name: - url = "{}://{}/Status_Lan.live.asp".format(self.protocol, self.host) + url = f"{self.protocol}://{self.host}/Status_Lan.live.asp" data = self.get_ddwrt_data(url) if not data: @@ -115,7 +115,7 @@ def _update_info(self): _LOGGER.info("Checking ARP") endpoint = "Wireless" if self.wireless_only else "Lan" - url = "{}://{}/Status_{}.live.asp".format(self.protocol, self.host, endpoint) + url = f"{self.protocol}://{self.host}/Status_{endpoint}.live.asp" data = self.get_ddwrt_data(url) if not data: diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 0ed3ffd2a564ca..2117f8dc6bb3cf 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -138,7 +138,7 @@ async def async_new_address_callback(hass, entry): @property def event_reachable(self): """Gateway specific event to signal a change in connection status.""" - return "deconz_reachable_{}".format(self.bridgeid) + return f"deconz_reachable_{self.bridgeid}" @callback def async_connection_status_callback(self, available): @@ -241,7 +241,7 @@ def __init__(self, hass, device): self._hass = hass self._device = device self._device.register_async_callback(self.async_update_callback) - self._event = "deconz_{}".format(CONF_EVENT) + self._event = f"deconz_{CONF_EVENT}" self._id = slugify(self._device.name) _LOGGER.debug("deCONZ event created: %s", self._id) diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 8b42b6175ce6d5..098484cf7ae6e2 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -84,7 +84,7 @@ def __init__(self, sensor_type, deluge_client, client_name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index db094bb9b12439..fbe0efa15ac7a2 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -47,7 +47,7 @@ class DeutscheBahnSensor(Entity): def __init__(self, start, goal, offset, only_direct): """Initialize the sensor.""" - self._name = "{} to {}".format(start, goal) + self._name = f"{start} to {goal}" self.data = SchieneData(start, goal, offset, only_direct) self._state = None diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 6ea5e7a46a28c4..aadb6b2d4cbf92 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -115,7 +115,7 @@ def __init__( @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py index d80385d0f542d3..9983ccc93fa355 100644 --- a/homeassistant/components/digitalloggers/switch.py +++ b/homeassistant/components/digitalloggers/switch.py @@ -88,7 +88,7 @@ def __init__(self, controller_name, parent_device, outlet): @property def name(self): """Return the display name of this relay.""" - return "{}_{}".format(self._controller_name, self._name) + return f"{self._controller_name}_{self._name}" @property def is_on(self): diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 3afa9c58e66ab7..ff0bbd7119407d 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -20,7 +20,7 @@ DOMAIN = "doorbird" -API_URL = "/api/{}".format(DOMAIN) +API_URL = f"/api/{DOMAIN}" CONF_CUSTOM_URL = "hass_url_override" CONF_EVENTS = "events" @@ -195,17 +195,15 @@ def slug(self): return slugify(self._name) def _get_event_name(self, event): - return "{}_{}".format(self.slug, event) + return f"{self.slug}_{event}" def _register_event(self, hass_url, event): """Add a schedule entry in the device for a sensor.""" - url = "{}{}/{}?token={}".format(hass_url, API_URL, event, self._token) + url = f"{hass_url}{API_URL}/{event}?token={self._token}" # Register HA URL as webhook if not already, then get the ID if not self.webhook_is_registered(url): - self.device.change_favorite( - "http", "Home Assistant ({})".format(event), url - ) + self.device.change_favorite("http", f"Home Assistant ({event})", url) fav_id = self.get_webhook_id(url) @@ -288,9 +286,9 @@ async def get(self, request, event): if event == "clear": hass.bus.async_fire(RESET_DEVICE_FAVORITES, {"token": token}) - message = "HTTP Favorites cleared for {}".format(device.slug) + message = f"HTTP Favorites cleared for {device.slug}" return web.Response(status=200, text=message) - hass.bus.async_fire("{}_{}".format(DOMAIN, event), event_data) + hass.bus.async_fire(f"{DOMAIN}_{event}", event_data) return web.Response(status=200, text="OK") diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py index a907099cba4c0f..643e006dfef0ed 100644 --- a/homeassistant/components/doorbird/switch.py +++ b/homeassistant/components/doorbird/switch.py @@ -45,9 +45,9 @@ def __init__(self, doorstation, relay): def name(self): """Return the name of the switch.""" if self._relay == IR_RELAY: - return "{} IR".format(self._doorstation.name) + return f"{self._doorstation.name} IR" - return "{} Relay {}".format(self._doorstation.name, self._relay) + return f"{self._doorstation.name} Relay {self._relay}" @property def icon(self): diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 0fe589f2765656..9c725d9b3a273b 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -81,7 +81,7 @@ def do_download(): "downloading '%s' failed, status_code=%d", url, req.status_code ) hass.bus.fire( - "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), + f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", {"url": url, "filename": filename}, ) @@ -126,7 +126,7 @@ def do_download(): while os.path.isfile(final_path): tries += 1 - final_path = "{}_{}.{}".format(path, tries, ext) + final_path = f"{path}_{tries}.{ext}" _LOGGER.debug("%s -> %s", url, final_path) @@ -136,14 +136,14 @@ def do_download(): _LOGGER.debug("Downloading of %s done", url) hass.bus.fire( - "{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), + f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}", {"url": url, "filename": filename}, ) except requests.exceptions.ConnectionError: _LOGGER.exception("ConnectionError occurred for %s", url) hass.bus.fire( - "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), + f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", {"url": url, "filename": filename}, ) diff --git a/homeassistant/components/duke_energy/sensor.py b/homeassistant/components/duke_energy/sensor.py index b8a9bec5db82df..998809decc02f4 100644 --- a/homeassistant/components/duke_energy/sensor.py +++ b/homeassistant/components/duke_energy/sensor.py @@ -44,7 +44,7 @@ def __init__(self, meter): @property def name(self): """Return the name.""" - return "duke_energy_{}".format(self.duke_meter.id) + return f"duke_energy_{self.duke_meter.id}" @property def unique_id(self): diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index a019a5c7b3ae5a..4d7ad04e38257d 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -92,7 +92,7 @@ def __init__(self, api, name, variable): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self._var_name) + return f"{self._name} {self._var_name}" @property def icon(self): @@ -140,23 +140,23 @@ def device_state_attributes(self): for event in self._api.data[prefix + "_warnings"]: i = i + 1 - data["warning_{}_name".format(i)] = event["event"] - data["warning_{}_level".format(i)] = event["level"] - data["warning_{}_type".format(i)] = event["type"] + data[f"warning_{i}_name"] = event["event"] + data[f"warning_{i}_level"] = event["level"] + data[f"warning_{i}_type"] = event["type"] if event["headline"]: - data["warning_{}_headline".format(i)] = event["headline"] + data[f"warning_{i}_headline"] = event["headline"] if event["description"]: - data["warning_{}_description".format(i)] = event["description"] + data[f"warning_{i}_description"] = event["description"] if event["instruction"]: - data["warning_{}_instruction".format(i)] = event["instruction"] + data[f"warning_{i}_instruction"] = event["instruction"] if event["start"] is not None: - data["warning_{}_start".format(i)] = dt_util.as_local( + data[f"warning_{i}_start"] = dt_util.as_local( dt_util.utc_from_timestamp(event["start"] / 1000) ) if event["end"] is not None: - data["warning_{}_end".format(i)] = dt_util.as_local( + data[f"warning_{i}_end"] = dt_util.as_local( dt_util.utc_from_timestamp(event["end"] / 1000) ) @@ -212,7 +212,7 @@ def update(self): "Found %d %s global DWD warnings", len(json_obj[myvalue]), mykey ) - data["{}_warning_level".format(mykey)] = 0 + data[f"{mykey}_warning_level"] = 0 my_warnings = [] if self.region_id is not None: @@ -234,13 +234,13 @@ def update(self): break # Get max warning level - maxlevel = data["{}_warning_level".format(mykey)] + maxlevel = data[f"{mykey}_warning_level"] for event in my_warnings: if event["level"] >= maxlevel: - data["{}_warning_level".format(mykey)] = event["level"] + data[f"{mykey}_warning_level"] = event["level"] - data["{}_warning_count".format(mykey)] = len(my_warnings) - data["{}_warnings".format(mykey)] = my_warnings + data[f"{mykey}_warning_count"] = len(my_warnings) + data[f"{mykey}_warnings"] = my_warnings _LOGGER.debug("Found %d %s local DWD warnings", len(my_warnings), mykey) diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index f89823b143fcb8..1eb2b79c0735ac 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -101,7 +101,7 @@ def icon(self): @property def unique_id(self): """Return the sensor's unique id.""" - return "{}-{}".format(self._device.serial, self._sensor_type) + return f"{self._device.serial}-{self._sensor_type}" class DysonFilterLifeSensor(DysonSensor): @@ -110,7 +110,7 @@ class DysonFilterLifeSensor(DysonSensor): def __init__(self, device): """Create a new Dyson Filter Life sensor.""" super().__init__(device, "filter_life") - self._name = "{} Filter Life".format(self._device.name) + self._name = f"{self._device.name} Filter Life" @property def state(self): @@ -126,7 +126,7 @@ class DysonDustSensor(DysonSensor): def __init__(self, device): """Create a new Dyson Dust sensor.""" super().__init__(device, "dust") - self._name = "{} Dust".format(self._device.name) + self._name = f"{self._device.name} Dust" @property def state(self): @@ -142,7 +142,7 @@ class DysonHumiditySensor(DysonSensor): def __init__(self, device): """Create a new Dyson Humidity sensor.""" super().__init__(device, "humidity") - self._name = "{} Humidity".format(self._device.name) + self._name = f"{self._device.name} Humidity" @property def state(self): @@ -160,7 +160,7 @@ class DysonTemperatureSensor(DysonSensor): def __init__(self, device, unit): """Create a new Dyson Temperature sensor.""" super().__init__(device, "temperature") - self._name = "{} Temperature".format(self._device.name) + self._name = f"{self._device.name} Temperature" self._unit = unit @property @@ -187,7 +187,7 @@ class DysonAirQualitySensor(DysonSensor): def __init__(self, device): """Create a new Dyson Air Quality sensor.""" super().__init__(device, "air_quality") - self._name = "{} AQI".format(self._device.name) + self._name = f"{self._device.name} AQI" @property def state(self): From fa79ef122084a19d7067b03d37f05ca599031f29 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Sep 2019 17:10:56 +0200 Subject: [PATCH 0121/3953] Use literal string interpolation in integrations E-G (f-strings) (#26379) --- homeassistant/components/ebox/sensor.py | 2 +- homeassistant/components/ebusd/sensor.py | 2 +- homeassistant/components/ecobee/weather.py | 2 +- homeassistant/components/efergy/sensor.py | 8 ++++---- homeassistant/components/eight_sleep/__init__.py | 4 ++-- homeassistant/components/eight_sleep/binary_sensor.py | 2 +- homeassistant/components/eight_sleep/sensor.py | 6 +++--- homeassistant/components/elkm1/__init__.py | 6 +++--- homeassistant/components/elkm1/alarm_control_panel.py | 8 +++----- homeassistant/components/emby/media_player.py | 3 +-- homeassistant/components/emoncms/sensor.py | 4 ++-- homeassistant/components/emoncms_history/__init__.py | 4 ++-- homeassistant/components/emulated_hue/hue_api.py | 2 +- .../components/entur_public_transport/sensor.py | 2 +- homeassistant/components/esphome/__init__.py | 8 ++++---- homeassistant/components/essent/sensor.py | 2 +- homeassistant/components/everlights/light.py | 4 ++-- homeassistant/components/facebox/image_processing.py | 8 ++++---- homeassistant/components/fail2ban/sensor.py | 2 +- homeassistant/components/fastdotcom/__init__.py | 2 +- homeassistant/components/feedreader/__init__.py | 2 +- homeassistant/components/fibaro/__init__.py | 8 ++++---- homeassistant/components/fibaro/climate.py | 2 +- homeassistant/components/fido/sensor.py | 2 +- homeassistant/components/file/notify.py | 2 +- homeassistant/components/filter/sensor.py | 2 +- homeassistant/components/fints/sensor.py | 10 +++++----- homeassistant/components/fitbit/sensor.py | 8 ++++---- homeassistant/components/flock/notify.py | 2 +- homeassistant/components/flunearyou/sensor.py | 2 +- homeassistant/components/foobot/sensor.py | 2 +- homeassistant/components/fritzdect/switch.py | 4 ++-- homeassistant/components/garadget/cover.py | 6 +++--- homeassistant/components/geniushub/binary_sensor.py | 4 ++-- homeassistant/components/geniushub/sensor.py | 4 ++-- homeassistant/components/geo_rss_events/sensor.py | 2 +- homeassistant/components/geofency/__init__.py | 4 ++-- homeassistant/components/google/__init__.py | 4 ++-- homeassistant/components/google_assistant/error.py | 4 +--- homeassistant/components/google_assistant/helpers.py | 2 +- homeassistant/components/google_wifi/sensor.py | 4 ++-- homeassistant/components/gpmdp/media_player.py | 2 +- homeassistant/components/gpslogger/__init__.py | 4 ++-- homeassistant/components/gtfs/sensor.py | 10 +++++----- homeassistant/components/gtt/sensor.py | 2 +- 45 files changed, 87 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index 1482ab34c682f4..66c58b828826cb 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -106,7 +106,7 @@ def __init__(self, ebox_data, sensor_type, name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index 37b7d2dd060c4e..ac156e040d7332 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -44,7 +44,7 @@ def __init__(self, data, sensor, name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._client_name, self._name) + return f"{self._client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 0680ef67f82ded..b09e06bd822dd4 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -123,7 +123,7 @@ def attribution(self): if self.weather: station = self.weather.get("weatherStation", "UNKNOWN") time = self.weather.get("timestamp", "UNKNOWN") - return "Ecobee weather provided by {} at {}".format(station, time) + return f"Ecobee weather provided by {station} at {time}" return None @property diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 53c89097a59eea..43c3b67457a755 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -99,7 +99,7 @@ def __init__(self, sensor_type, app_token, utc_offset, period, currency, sid=Non """Initialize the sensor.""" self.sid = sid if sid: - self._name = "efergy_{}".format(sid) + self._name = f"efergy_{sid}" else: self._name = SENSOR_TYPES[sensor_type][0] self.type = sensor_type @@ -109,7 +109,7 @@ def __init__(self, sensor_type, app_token, utc_offset, period, currency, sid=Non self.period = period self.currency = currency if self.type == "cost": - self._unit_of_measurement = "{}/{}".format(self.currency, self.period) + self._unit_of_measurement = f"{self.currency}/{self.period}" else: self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] @@ -132,7 +132,7 @@ def update(self): """Get the Efergy monitor data from the web service.""" try: if self.type == "instant_readings": - url_string = "{}getInstant?token={}".format(_RESOURCE, self.app_token) + url_string = f"{_RESOURCE}getInstant?token={self.app_token}" response = requests.get(url_string, timeout=10) self._state = response.json()["reading"] elif self.type == "amount": @@ -142,7 +142,7 @@ def update(self): response = requests.get(url_string, timeout=10) self._state = response.json()["sum"] elif self.type == "budget": - url_string = "{}getBudget?token={}".format(_RESOURCE, self.app_token) + url_string = f"{_RESOURCE}getBudget?token={self.app_token}" response = requests.get(url_string, timeout=10) self._state = response.json()["status"] elif self.type == "cost": diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 2479ea5440feba..923c3f7d309124 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -141,8 +141,8 @@ async def async_update_user_data(now): for user in eight.users: obj = eight.users[user] for sensor in SENSORS: - sensors.append("{}_{}".format(obj.side, sensor)) - binary_sensors.append("{}_presence".format(obj.side)) + sensors.append(f"{obj.side}_{sensor}") + binary_sensors.append(f"{obj.side}_presence") sensors.append("room_temp") else: # No users, cannot continue diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index 7d7ebecafee944..7b801578ccd4bf 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -34,7 +34,7 @@ def __init__(self, name, eight, sensor): self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._side = self._sensor.split("_")[0] diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index afc06986ea61e7..d3d54fd58caa92 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -68,7 +68,7 @@ def __init__(self, name, eight, sensor): self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._side = self._sensor.split("_")[0] @@ -122,7 +122,7 @@ def __init__(self, name, eight, sensor, units): self._sensor = sensor self._sensor_root = self._sensor.split("_", 1)[1] self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._attr = None self._units = units @@ -261,7 +261,7 @@ def __init__(self, name, eight, sensor, units): self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._attr = None self._units = units diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index e26749e6f6b069..d15399df67b15a 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -146,7 +146,7 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: def _included(ranges, set_to, values): for rng in ranges: if not rng[0] <= rng[1] <= len(values): - raise vol.Invalid("Invalid range {}".format(rng)) + raise vol.Invalid(f"Invalid range {rng}") values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1) for index, conf in enumerate(hass_config[DOMAIN]): @@ -250,7 +250,7 @@ def __init__(self, element, elk, elk_data): # we could have used elkm1__foo_bar for the latter, but that # would have been a breaking change if self._prefix != "": - uid_start = "elkm1m_{prefix}".format(prefix=self._prefix) + uid_start = f"elkm1m_{self._prefix}" else: uid_start = "elkm1" self._unique_id = "{uid_start}_{name}".format( @@ -260,7 +260,7 @@ def __init__(self, element, elk, elk_data): @property def name(self): """Name of the element.""" - return "{p}{n}".format(p=self._prefix, n=self._element.name) + return f"{self._prefix}{self._element.name}" @property def unique_id(self): diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 275d94efa66fd0..927ed53115e5e6 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -59,7 +59,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _dispatch(signal, entity_ids, *args): for entity_id in entity_ids: - async_dispatcher_send(hass, "{}_{}".format(signal, entity_id), *args) + async_dispatcher_send(hass, f"{signal}_{entity_id}", *args) def _arm_service(service): entity_ids = service.data.get(ATTR_ENTITY_ID, []) @@ -117,13 +117,11 @@ async def async_added_to_hass(self): for keypad in self._elk.keypads: keypad.add_callback(self._watch_keypad) async_dispatcher_connect( - self.hass, - "{}_{}".format(SIGNAL_ARM_ENTITY, self.entity_id), - self._arm_service, + self.hass, f"{SIGNAL_ARM_ENTITY}_{self.entity_id}", self._arm_service ) async_dispatcher_connect( self.hass, - "{}_{}".format(SIGNAL_DISPLAY_MESSAGE, self.entity_id), + f"{SIGNAL_DISPLAY_MESSAGE}_{self.entity_id}", self._display_message, ) diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 409dd8ec472d62..d8a98a96585903 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -209,8 +209,7 @@ def supports_remote_control(self): def name(self): """Return the name of the device.""" return ( - "Emby - {} - {}".format(self.device.client, self.device.name) - or DEVICE_DEFAULT_NAME + f"Emby - {self.device.client} - {self.device.name}" or DEVICE_DEFAULT_NAME ) @property diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 8d79b771fb93ff..5f9d31697b86f8 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -135,7 +135,7 @@ def __init__( id_for_name = "" if str(sensorid) == "1" else sensorid # Use the feed name assigned in EmonCMS or fall back to the feed ID feed_name = elem.get("name") or "Feed {}".format(elem["id"]) - self._name = "EmonCMS{} {}".format(id_for_name, feed_name) + self._name = f"EmonCMS{id_for_name} {feed_name}" else: self._name = name self._identifier = get_id( @@ -225,7 +225,7 @@ class EmonCmsData: def __init__(self, hass, url, apikey, interval): """Initialize the data object.""" self._apikey = apikey - self._url = "{}/feed/list.json".format(url) + self._url = f"{url}/feed/list.json" self._interval = interval self._hass = hass self.data = None diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index 779a25872f9df2..3b30a29960b3fd 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -47,7 +47,7 @@ def setup(hass, config): def send_data(url, apikey, node, payload): """Send payload data to Emoncms.""" try: - fullurl = "{}/input/post.json".format(url) + fullurl = f"{url}/input/post.json" data = {"apikey": apikey, "data": payload} parameters = {"node": node} req = requests.post( @@ -83,7 +83,7 @@ def update_emoncms(time): if payload_dict: payload = "{%s}" % ",".join( - "{}:{}".format(key, val) for key, val in payload_dict.items() + f"{key}:{val}" for key, val in payload_dict.items() ) send_data( diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index fc00746fc7f493..5e1c72618b6e43 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -590,5 +590,5 @@ def entity_to_json(config, entity, state): def create_hue_success_response(entity_id, attr, value): """Create a success response for an attribute set on a light.""" - success_key = "/lights/{}/state/{}".format(entity_id, attr) + success_key = f"/lights/{entity_id}/state/{attr}" return {"success": {success_key: value}} diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 46ba62ba3fa0c9..0f8324ded9e099 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -121,7 +121,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: given_name = "{} {}".format(name, data.get_stop_info(place).name) except KeyError: - given_name = "{} {}".format(name, place) + given_name = f"{name} {place}" entities.append( EnturPublicTransportSensor(proxy, given_name, place, show_on_map) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 8780d2b67aed2f..182d4003e30664 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool host, port, password, - client_info="Home Assistant {}".format(const.__version__), + client_info=f"Home Assistant {const.__version__}", ) # Store client in per-config-entry hass.data @@ -254,7 +254,7 @@ async def _async_setup_device_registry( """Set up device registry feature for a particular config entry.""" sw_version = device_info.esphome_version if device_info.compilation_time: - sw_version += " ({})".format(device_info.compilation_time) + sw_version += f" ({device_info.compilation_time})" device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -269,7 +269,7 @@ async def _async_setup_device_registry( async def _register_service( hass: HomeAssistantType, entry_data: RuntimeEntryData, service: UserService ): - service_name = "{}_{}".format(entry_data.device_info.name, service.name) + service_name = f"{entry_data.device_info.name}_{service.name}" schema = {} for arg in service.args: schema[vol.Required(arg.name)] = { @@ -315,7 +315,7 @@ async def _setup_services( entry_data.services = {serv.key: serv for serv in services} for service in to_unregister: - service_name = "{}_{}".format(entry_data.device_info.name, service.name) + service_name = f"{entry_data.device_info.name}_{service.name}" hass.services.async_remove(DOMAIN, service_name) for service in to_register: diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py index 83d3164e3ff875..b106d9d2ae660c 100644 --- a/homeassistant/components/essent/sensor.py +++ b/homeassistant/components/essent/sensor.py @@ -95,7 +95,7 @@ def __init__(self, essent_base, meter, meter_type, tariff, unit): @property def name(self): """Return the name of the sensor.""" - return "Essent {} ({})".format(self._type, self._tariff) + return f"Essent {self._type} ({self._tariff})" @property def state(self): diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 21629360ac792f..506617e4c6028d 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -87,7 +87,7 @@ def __init__(self, api, channel, status, effects): @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}-{}".format(self._mac, self._channel) + return f"{self._mac}-{self._channel}" @property def available(self) -> bool: @@ -102,7 +102,7 @@ def name(self): @property def is_on(self): """Return true if device is on.""" - return self._status["ch{}Active".format(self._channel)] == 1 + return self._status[f"ch{self._channel}Active"] == 1 @property def brightness(self): diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index a1e686bcbd02ad..228cae2f19d54f 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -168,7 +168,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = config[CONF_PORT] username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - url_health = "http://{}:{}/healthz".format(ip_address, port) + url_health = f"http://{ip_address}:{port}/healthz" hostname = check_box_health(url_health, username, password) if hostname is None: return @@ -214,8 +214,8 @@ def __init__( ): """Init with the API key and model id.""" super().__init__() - self._url_check = "http://{}:{}/{}/check".format(ip_address, port, CLASSIFIER) - self._url_teach = "http://{}:{}/{}/teach".format(ip_address, port, CLASSIFIER) + self._url_check = f"http://{ip_address}:{port}/{CLASSIFIER}/check" + self._url_teach = f"http://{ip_address}:{port}/{CLASSIFIER}/teach" self._username = username self._password = password self._hostname = hostname @@ -224,7 +224,7 @@ def __init__( self._name = name else: camera_name = split_entity_id(camera_entity)[1] - self._name = "{} {}".format(CLASSIFIER, camera_name) + self._name = f"{CLASSIFIER} {camera_name}" self._matched = {} def process_image(self, image): diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index d5e3d6064ea87f..2dc528b2cff776 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -57,7 +57,7 @@ class BanSensor(Entity): def __init__(self, name, jail, log_parser): """Initialize the sensor.""" - self._name = "{} {}".format(name, jail) + self._name = f"{name} {jail}" self.jail = jail self.ban_dict = {STATE_CURRENT_BANS: [], STATE_ALL_BANS: []} self.last_ban = None diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index a40c2597222ad6..b070eef031010c 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers.event import async_track_time_interval DOMAIN = "fastdotcom" -DATA_UPDATED = "{}_data_updated".format(DOMAIN) +DATA_UPDATED = f"{DOMAIN}_data_updated" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index cdd76a56e167f9..44ec95f8213154 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -44,7 +44,7 @@ def setup(hass, config): urls = config.get(DOMAIN)[CONF_URLS] scan_interval = config.get(DOMAIN).get(CONF_SCAN_INTERVAL) max_entries = config.get(DOMAIN).get(CONF_MAX_ENTRIES) - data_file = hass.config.path("{}.pickle".format(DOMAIN)) + data_file = hass.config.path(f"{DOMAIN}.pickle") storage = StoredData(data_file) feeds = [ FeedManager(url, scan_interval, max_entries, hass, storage) for url in urls diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index d47c2b0c2d2af9..f500b38664307c 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -247,11 +247,11 @@ def _read_scenes(self): else: room_name = self._room_map[device.roomID].name device.room_name = room_name - device.friendly_name = "{} {}".format(room_name, device.name) + device.friendly_name = f"{room_name} {device.name}" device.ha_id = "scene_{}_{}_{}".format( slugify(room_name), slugify(device.name), device.id ) - device.unique_id_str = "{}.scene.{}".format(self.hub_serial, device.id) + device.unique_id_str = f"{self.hub_serial}.scene.{device.id}" self._scene_map[device.id] = device self.fibaro_devices["scene"].append(device) @@ -287,7 +287,7 @@ def _read_devices(self): device.mapped_type = None dtype = device.mapped_type if dtype: - device.unique_id_str = "{}.{}".format(self.hub_serial, device.id) + device.unique_id_str = f"{self.hub_serial}.{device.id}" self._device_map[device.id] = device if dtype != "climate": self.fibaro_devices[dtype].append(device) @@ -414,7 +414,7 @@ def call_set_color(self, red, green, blue, white): green = int(max(0, min(255, green))) blue = int(max(0, min(255, blue))) white = int(max(0, min(255, white))) - color_str = "{},{},{},{}".format(red, green, blue, white) + color_str = f"{red},{green},{blue},{white}" self.fibaro_device.properties.color = color_str self.action("setColor", str(red), str(green), str(blue), str(white)) diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index ed399fac209458..71be289e27b3c2 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -115,7 +115,7 @@ def __init__(self, fibaro_device): self._op_mode_device = None self._fan_mode_device = None self._support_flags = 0 - self.entity_id = "climate.{}".format(self.ha_id) + self.entity_id = f"climate.{self.ha_id}" self._hvac_support = [] self._preset_support = [] self._fan_support = [] diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index e556903638c314..e85b45db4d3aa0 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -108,7 +108,7 @@ def __init__(self, fido_data, sensor_type, name, number): @property def name(self): """Return the name of the sensor.""" - return "{} {} {}".format(self.client_name, self._number, self._name) + return f"{self.client_name} {self._number} {self._name}" @property def state(self): diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index f4d31a5fd6fa2e..b190bf5d12187a 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -57,5 +57,5 @@ def send_message(self, message="", **kwargs): if self.add_timestamp: text = "{} {}\n".format(dt_util.utcnow().isoformat(), message) else: - text = "{}\n".format(message) + text = f"{message}\n" file.write(text) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index e8c532547c1466..81c4623c53f52a 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -332,7 +332,7 @@ def __str__(self): def __repr__(self): """Return timestamp and state as the representation of FilterState.""" - return "{} : {}".format(self.timestamp, self.state) + return f"{self.timestamp} : {self.state}" class Filter: diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 7a1760ea3d5a95..008337f88eb077 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -77,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): account_name = account_config.get(account.iban) if not account_name: - account_name = "{} - {}".format(fints_name, account.iban) + account_name = f"{fints_name} - {account.iban}" accounts.append(FinTsAccount(client, account, account_name)) _LOGGER.debug("Creating account %s for bank %s", account.iban, fints_name) @@ -90,7 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): account_name = holdings_config.get(account.accountnumber) if not account_name: - account_name = "{} - {}".format(fints_name, account.accountnumber) + account_name = f"{fints_name} - {account.accountnumber}" accounts.append(FinTsHoldingsAccount(client, account, account_name)) _LOGGER.debug( "Creating holdings %s for bank %s", account.accountnumber, fints_name @@ -265,11 +265,11 @@ def device_state_attributes(self) -> dict: if self._client.name: attributes[ATTR_BANK] = self._client.name for holding in self._holdings: - total_name = "{} total".format(holding.name) + total_name = f"{holding.name} total" attributes[total_name] = holding.total_value - pieces_name = "{} pieces".format(holding.name) + pieces_name = f"{holding.name} pieces" attributes[pieces_name] = holding.pieces - price_name = "{} price".format(holding.name) + price_name = f"{holding.name} price" attributes[price_name] = holding.market_value return attributes diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 830914ce113945..534477d88cf792 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -167,7 +167,7 @@ def fitbit_configuration_callback(callback_data): else: setup_platform(hass, config, add_entities, discovery_info) - start_url = "{}{}".format(hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH) + start_url = f"{hass.config.api.base_url}{FITBIT_AUTH_CALLBACK_PATH}" description = """Please create a Fitbit developer app at https://dev.fitbit.com/apps/new. @@ -204,9 +204,9 @@ def request_oauth_completion(hass): def fitbit_configuration_callback(callback_data): """Handle configuration updates.""" - start_url = "{}{}".format(hass.config.api.base_url, FITBIT_AUTH_START) + start_url = f"{hass.config.api.base_url}{FITBIT_AUTH_START}" - description = "Please authorize Fitbit by visiting {}".format(start_url) + description = f"Please authorize Fitbit by visiting {start_url}" _CONFIGURING["fitbit"] = configurator.request_config( "Fitbit", @@ -498,7 +498,7 @@ def update(self): hours -= 12 elif hours == 0: hours = 12 - self._state = "{}:{:02d} {}".format(hours, minutes, setting) + self._state = f"{hours}:{minutes:02d} {setting}" else: self._state = raw_state else: diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index 07abd097c87015..ba52d3b4beb03e 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -20,7 +20,7 @@ async def get_service(hass, config, discovery_info=None): """Get the Flock notification service.""" access_token = config.get(CONF_ACCESS_TOKEN) - url = "{}{}".format(_RESOURCE, access_token) + url = f"{_RESOURCE}{access_token}" session = async_get_clientsession(hass) return FlockNotificationService(url, session) diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 97453c41af074c..0df61fd24e114e 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -143,7 +143,7 @@ def state(self): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return "{0},{1}_{2}".format(self.fny.latitude, self.fny.longitude, self._kind) + return f"{self.fny.latitude},{self.fny.longitude}_{self._kind}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index 8ec3541d188c78..8d3cf6de27de2a 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -117,7 +117,7 @@ def state(self): @property def unique_id(self): """Return the unique id of this entity.""" - return "{}_{}".format(self._uuid, self.type) + return f"{self._uuid}_{self.type}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/fritzdect/switch.py b/homeassistant/components/fritzdect/switch.py index 22a44a11133cdd..dcb700d6636663 100644 --- a/homeassistant/components/fritzdect/switch.py +++ b/homeassistant/components/fritzdect/switch.py @@ -95,7 +95,7 @@ def device_state_attributes(self): attrs[ATTR_CURRENT_CONSUMPTION_UNIT] = "{}".format( ATTR_CURRENT_CONSUMPTION_UNIT_VALUE ) - attrs[ATTR_TOTAL_CONSUMPTION] = "{:.3f}".format(self.data.total_consumption) + attrs[ATTR_TOTAL_CONSUMPTION] = f"{self.data.total_consumption:.3f}" attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = "{}".format( ATTR_TOTAL_CONSUMPTION_UNIT_VALUE ) @@ -104,7 +104,7 @@ def device_state_attributes(self): attrs[ATTR_TEMPERATURE] = "{}".format( self.units.temperature(self.data.temperature, TEMP_CELSIUS) ) - attrs[ATTR_TEMPERATURE_UNIT] = "{}".format(self.units.temperature_unit) + attrs[ATTR_TEMPERATURE_UNIT] = f"{self.units.temperature_unit}" return attrs @property diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index 2e52b49c5f4b87..d487c39db6bfa5 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -177,7 +177,7 @@ def get_token(self): "username": self._username, "password": self._password, } - url = "{}/oauth/token".format(self.particle_url) + url = f"{self.particle_url}/oauth/token" ret = requests.post(url, auth=("particle", "particle"), data=args, timeout=10) try: @@ -187,7 +187,7 @@ def get_token(self): def remove_token(self): """Remove authorization token from API.""" - url = "{}/v1/access_tokens/{}".format(self.particle_url, self.access_token) + url = f"{self.particle_url}/v1/access_tokens/{self.access_token}" ret = requests.delete(url, auth=(self._username, self._password), timeout=10) return ret.text @@ -266,6 +266,6 @@ def _put_command(self, func, arg=None): params = {"access_token": self.access_token} if arg: params["command"] = arg - url = "{}/v1/devices/{}/{}".format(self.particle_url, self.device_id, func) + url = f"{self.particle_url}/v1/devices/{self.device_id}/{func}" ret = requests.post(url, data=params, timeout=10) return ret.json() diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index 1cc8cd3f4063b0..105a03bf757c68 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -29,9 +29,9 @@ def __init__(self, device) -> None: self._device = device if device.type[:21] == "Dual Channel Receiver": - self._name = "Dual Channel Receiver {}".format(device.id) + self._name = f"Dual Channel Receiver {device.id}" else: - self._name = "{} {}".format(device.type, device.id) + self._name = f"{device.type} {device.id}" @property def is_on(self) -> bool: diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 5e39be1620a983..e87a43fc1ae688 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -34,7 +34,7 @@ def __init__(self, device) -> None: super().__init__() self._device = device - self._name = "{} {}".format(device.type, device.id) + self._name = f"{device.type} {device.id}" @property def icon(self) -> str: @@ -112,7 +112,7 @@ def state(self) -> str: @property def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" - return {"{}_list".format(self._level): self._issues} + return {f"{self._level}_list": self._issues} async def async_update(self) -> Awaitable[None]: """Process the sensor's state data.""" diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index a4d13bdef9d065..9f336668142155 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -157,7 +157,7 @@ def update(self): # And now compute the attributes from the filtered events. matrix = {} for entry in feed_entries: - matrix[entry.title] = "{:.0f}km".format(entry.distance_to_home) + matrix[entry.title] = f"{entry.distance_to_home:.0f}km" self._state_attributes = matrix elif status == georss_client.UPDATE_OK_NO_DATA: _LOGGER.debug("Update successful, but no data received from %s", self._feed) diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 6835103968a83f..9d8e0b29f5d191 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -50,7 +50,7 @@ LOCATION_ENTRY = "1" LOCATION_EXIT = "0" -TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" def _address(value: str) -> str: @@ -131,7 +131,7 @@ def _set_location(hass, data, location_name): data, ) - return web.Response(text="Setting location for {}".format(device), status=HTTP_OK) + return web.Response(text=f"Setting location for {device}", status=HTTP_OK) async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 41901a71704aa8..62aa2212bb1b72 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -59,10 +59,10 @@ DATA_INDEX = "google_calendars" -YAML_DEVICES = "{}_calendars.yaml".format(DOMAIN) +YAML_DEVICES = f"{DOMAIN}_calendars.yaml" SCOPES = "https://www.googleapis.com/auth/calendar" -TOKEN_FILE = ".{}.token".format(DOMAIN) +TOKEN_FILE = f".{DOMAIN}.token" CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/google_assistant/error.py b/homeassistant/components/google_assistant/error.py index a2ff72511c71d4..82c256067eb80d 100644 --- a/homeassistant/components/google_assistant/error.py +++ b/homeassistant/components/google_assistant/error.py @@ -26,9 +26,7 @@ class ChallengeNeeded(SmartHomeError): def __init__(self, challenge_type): """Initialize challenge needed error.""" - super().__init__( - ERR_CHALLENGE_NEEDED, "Challenge needed: {}".format(challenge_type) - ) + super().__init__(ERR_CHALLENGE_NEEDED, f"Challenge needed: {challenge_type}") self.challenge_type = challenge_type def to_response(self): diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 066ed0057acb55..daaf790a0c1521 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -212,7 +212,7 @@ async def execute(self, data, command_payload): if not executed: raise SmartHomeError( ERR_FUNCTION_NOT_SUPPORTED, - "Unable to execute {} for {}".format(command, self.state.entity_id), + f"Unable to execute {command} for {self.state.entity_id}", ) @callback diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index a910e91b1641e3..1d4ed8d84f812c 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -88,7 +88,7 @@ def __init__(self, api, name, variable): @property def name(self): """Return the name of the sensor.""" - return "{}_{}".format(self._name, self._var_name) + return f"{self._name}_{self._var_name}" @property def icon(self): @@ -125,7 +125,7 @@ class GoogleWifiAPI: def __init__(self, host, conditions): """Initialize the data object.""" uri = "http://" - resource = "{}{}{}".format(uri, host, ENDPOINT) + resource = f"{uri}{host}{ENDPOINT}" self._request = requests.Request("GET", resource).prepare() self.raw_data = None self.conditions = conditions diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py index 90522a84ce51b5..e6df8b0fe8b1be 100644 --- a/homeassistant/components/gpmdp/media_player.py +++ b/homeassistant/components/gpmdp/media_player.py @@ -142,7 +142,7 @@ def setup_gpmdp(hass, config, code, add_entities): name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = "ws://{}:{}".format(host, port) + url = f"ws://{host}:{port}" if not code: request_configuration(hass, config, url, add_entities) diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 839adec2f5bff7..3ac09457d81541 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) -TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" DEFAULT_ACCURACY = 200 @@ -90,7 +90,7 @@ async def handle_webhook(hass, webhook_id, request): attrs, ) - return web.Response(text="Setting location for {}".format(device), status=HTTP_OK) + return web.Response(text=f"Setting location for {device}", status=HTTP_OK) async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index b5c1000681dd30..d70e1016f07e84 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -139,9 +139,9 @@ def get_next_departure( if include_tomorrow: limit = int(limit / 2 * 3) tomorrow_name = tomorrow.strftime("%A").lower() - tomorrow_select = "calendar.{} AS tomorrow,".format(tomorrow_name) - tomorrow_where = "OR calendar.{} = 1".format(tomorrow_name) - tomorrow_order = "calendar.{} DESC,".format(tomorrow_name) + tomorrow_select = f"calendar.{tomorrow_name} AS tomorrow," + tomorrow_where = f"OR calendar.{tomorrow_name} = 1" + tomorrow_order = f"calendar.{tomorrow_name} DESC," sql_query = """ SELECT trip.trip_id, trip.route_id, @@ -357,7 +357,7 @@ def setup_platform( (gtfs_root, _) = os.path.splitext(data) - sqlite_file = "{}.sqlite?check_same_thread=False".format(gtfs_root) + sqlite_file = f"{gtfs_root}.sqlite?check_same_thread=False" joined_path = os.path.join(gtfs_dir, sqlite_file) gtfs = pygtfs.Schedule(joined_path) @@ -673,7 +673,7 @@ def append_keys(self, resource: dict, prefix: Optional[str] = None) -> None: continue key = attr if prefix and not key.startswith(prefix): - key = "{} {}".format(prefix, key) + key = f"{prefix} {key}" key = slugify(key) self._attributes[key] = val diff --git a/homeassistant/components/gtt/sensor.py b/homeassistant/components/gtt/sensor.py index 43f13c9462020a..cd66a670696eb4 100644 --- a/homeassistant/components/gtt/sensor.py +++ b/homeassistant/components/gtt/sensor.py @@ -38,7 +38,7 @@ def __init__(self, stop, bus_name): """Initialize the Gtt sensor.""" self.data = GttData(stop, bus_name) self._state = None - self._name = "Stop {}".format(stop) + self._name = f"Stop {stop}" @property def name(self): From 13bb2ea35ac555102259dee7b6c32aa587a7fef9 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 4 Sep 2019 01:16:13 +1000 Subject: [PATCH 0122/3953] GeoNet NZ Quakes Sensor (#26078) * working version of status sensor * changed unit of measurement * align naming with feed source * simplified sensor name * fix potential issue during initialisation * fixed tests * changed icon to constant * added tests for new sensor * split tests for geolocation vs sensor * fixed lint * fixed pylint * fixed test * removed config entry id from attributes * moved entity manager to component * fix issue with multiple config entries overriding each other's data * creating async tasks instead of awaiting each unloading * moved manager to component * correctly triggering update only when this component is loaded * fixed tests after major code refactorings * fixed pylint * moved actual creation of new events to geolocation platform * changed all timestamps to utc * changed the way platforms are setup and manager is updated * simplify assert statement * changed the way waiting for unloading platforms --- .../components/geonetnz_quakes/__init__.py | 154 ++++++++++++++++-- .../components/geonetnz_quakes/const.py | 9 + .../geonetnz_quakes/geo_location.py | 131 ++------------- .../components/geonetnz_quakes/sensor.py | 139 ++++++++++++++++ tests/components/geonetnz_quakes/__init__.py | 30 ++++ .../geonetnz_quakes/test_geo_location.py | 61 ++----- .../components/geonetnz_quakes/test_sensor.py | 115 +++++++++++++ 7 files changed, 472 insertions(+), 167 deletions(-) create mode 100644 homeassistant/components/geonetnz_quakes/sensor.py create mode 100644 tests/components/geonetnz_quakes/test_sensor.py diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index e786b4130296c7..069c9ab7daa165 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -1,27 +1,47 @@ """The GeoNet NZ Quakes integration.""" +import asyncio +import logging +from datetime import timedelta + import voluptuous as vol +from aio_geojson_geonetnz_quakes import GeonetnzQuakesFeedManager +from homeassistant.core import callback +from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM, + LENGTH_MILES, ) -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, aiohttp_client +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval from .config_flow import configured_instances from .const import ( + PLATFORMS, CONF_MINIMUM_MAGNITUDE, CONF_MMI, + DEFAULT_FILTER_TIME_INTERVAL, DEFAULT_MINIMUM_MAGNITUDE, DEFAULT_MMI, DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, FEED, + SIGNAL_DELETE_ENTITY, + SIGNAL_NEW_GEOLOCATION, + SIGNAL_STATUS, + SIGNAL_UPDATE_ENTITY, ) +_LOGGER = logging.getLogger(__name__) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -81,13 +101,20 @@ async def async_setup(hass, config): 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") - ) - + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + if FEED not in hass.data[DOMAIN]: + hass.data[DOMAIN][FEED] = {} + + radius = config_entry.data[CONF_RADIUS] + unit_system = config_entry.data[CONF_UNIT_SYSTEM] + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + # Create feed entity manager for all platforms. + manager = GeonetnzQuakesFeedEntityManager(hass, config_entry, radius, unit_system) + hass.data[DOMAIN][FEED][config_entry.entry_id] = manager + _LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id) + await manager.async_init() return True @@ -95,7 +122,114 @@ 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 asyncio.wait( + [ + hass.config_entries.async_forward_entry_unload(config_entry, domain) + for domain in PLATFORMS + ] + ) + return True - await hass.config_entries.async_forward_entry_unload(config_entry, "geo_location") - return True +class GeonetnzQuakesFeedEntityManager: + """Feed Entity Manager for GeoNet NZ Quakes feed.""" + + def __init__(self, hass, config_entry, radius_in_km, unit_system): + """Initialize the Feed Entity Manager.""" + self._hass = hass + self._config_entry = config_entry + coordinates = ( + config_entry.data[CONF_LATITUDE], + config_entry.data[CONF_LONGITUDE], + ) + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = GeonetnzQuakesFeedManager( + websession, + self._generate_entity, + self._update_entity, + self._remove_entity, + coordinates, + mmi=config_entry.data[CONF_MMI], + filter_radius=radius_in_km, + filter_minimum_magnitude=config_entry.data[CONF_MINIMUM_MAGNITUDE], + filter_time=DEFAULT_FILTER_TIME_INTERVAL, + status_callback=self._status_update, + ) + self._config_entry_id = config_entry.entry_id + self._scan_interval = timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]) + self._unit_system = unit_system + self._track_time_remove_callback = None + self._status_info = None + self.listeners = [] + + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" + + for domain in PLATFORMS: + self._hass.async_create_task( + self._hass.config_entries.async_forward_entry_setup( + self._config_entry, domain + ) + ) + + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + 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.""" + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + self.listeners = [] + if self._track_time_remove_callback: + self._track_time_remove_callback() + _LOGGER.debug("Feed entity manager stopped") + + @callback + def async_event_new_entity(self): + """Return manager specific event to signal new entity.""" + return SIGNAL_NEW_GEOLOCATION.format(self._config_entry_id) + + def get_entry(self, external_id): + """Get feed entry by external id.""" + return self._feed_manager.feed_entries.get(external_id) + + def status_info(self): + """Return latest status update info received.""" + return self._status_info + + async def _generate_entity(self, external_id): + """Generate new entity.""" + async_dispatcher_send( + self._hass, + self.async_event_new_entity(), + self, + external_id, + self._unit_system, + ) + + 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)) + + async def _status_update(self, status_info): + """Propagate status update.""" + _LOGGER.debug("Status update received: %s", status_info) + self._status_info = status_info + async_dispatcher_send(self._hass, SIGNAL_STATUS.format(self._config_entry_id)) diff --git a/homeassistant/components/geonetnz_quakes/const.py b/homeassistant/components/geonetnz_quakes/const.py index d06e85ee2cb51b..d564d407f7c7e9 100644 --- a/homeassistant/components/geonetnz_quakes/const.py +++ b/homeassistant/components/geonetnz_quakes/const.py @@ -3,12 +3,21 @@ DOMAIN = "geonetnz_quakes" +PLATFORMS = ("sensor", "geo_location") + CONF_MINIMUM_MAGNITUDE = "minimum_magnitude" CONF_MMI = "mmi" FEED = "feed" +DEFAULT_FILTER_TIME_INTERVAL = timedelta(days=7) DEFAULT_MINIMUM_MAGNITUDE = 0.0 DEFAULT_MMI = 3 DEFAULT_RADIUS = 50.0 DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) + +SIGNAL_DELETE_ENTITY = "geonetnz_quakes_delete_{}" +SIGNAL_UPDATE_ENTITY = "geonetnz_quakes_update_{}" +SIGNAL_STATUS = "geonetnz_quakes_status_{}" + +SIGNAL_NEW_GEOLOCATION = "geonetnz_quakes_new_geolocation_{}" diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 9d4be94e3aa27d..1ee7c287c6170e 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -1,33 +1,20 @@ """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 homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from .const import CONF_MINIMUM_MAGNITUDE, CONF_MMI, DOMAIN, FEED +from .const import DOMAIN, FEED, SIGNAL_DELETE_ENTITY, SIGNAL_UPDATE_ENTITY _LOGGER = logging.getLogger(__name__) @@ -39,111 +26,27 @@ 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.""" + manager = hass.data[DOMAIN][FEED][entry.entry_id] - 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 + @callback + def async_add_geolocation(feed_manager, external_id, unit_system): + """Add gelocation entity from feed.""" + new_entity = GeonetnzQuakesEvent(feed_manager, external_id, unit_system) + _LOGGER.debug("Adding geolocation %s", new_entity) + async_add_entities([new_entity], True) + + manager.listeners.append( + async_dispatcher_connect( + hass, manager.async_event_new_entity(), async_add_geolocation ) - _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)) + ) + hass.async_create_task(manager.async_update()) + _LOGGER.debug("Geolocation setup done") class GeonetnzQuakesEvent(GeolocationEvent): diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py new file mode 100644 index 00000000000000..e0be94d1b261d4 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/sensor.py @@ -0,0 +1,139 @@ +"""Feed Entity Manager Sensor support for GeoNet NZ Quakes Feeds.""" +import logging +from typing import Optional + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.util import dt + +from .const import DOMAIN, FEED, SIGNAL_STATUS + +_LOGGER = logging.getLogger(__name__) + +ATTR_STATUS = "status" +ATTR_LAST_UPDATE = "last_update" +ATTR_LAST_UPDATE_SUCCESSFUL = "last_update_successful" +ATTR_LAST_TIMESTAMP = "last_timestamp" +ATTR_CREATED = "created" +ATTR_UPDATED = "updated" +ATTR_REMOVED = "removed" + +DEFAULT_ICON = "mdi:pulse" +DEFAULT_UNIT_OF_MEASUREMENT = "quakes" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the GeoNet NZ Quakes Feed platform.""" + manager = hass.data[DOMAIN][FEED][entry.entry_id] + sensor = GeonetnzQuakesSensor(entry.entry_id, entry.title, manager) + async_add_entities([sensor]) + _LOGGER.debug("Sensor setup done") + + +class GeonetnzQuakesSensor(Entity): + """This is a status sensor for the GeoNet NZ Quakes integration.""" + + def __init__(self, config_entry_id, config_title, manager): + """Initialize entity.""" + self._config_entry_id = config_entry_id + self._config_title = config_title + self._manager = manager + self._status = None + self._last_update = None + self._last_update_successful = None + self._last_timestamp = None + self._total = None + self._created = None + self._updated = None + self._removed = None + self._remove_signal_status = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self._remove_signal_status = async_dispatcher_connect( + self.hass, + SIGNAL_STATUS.format(self._config_entry_id), + self._update_status_callback, + ) + _LOGGER.debug("Waiting for updates %s", self._config_entry_id) + # First update is manual because of how the feed entity manager is updated. + await self.async_update() + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + if self._remove_signal_status: + self._remove_signal_status() + + @callback + def _update_status_callback(self): + """Call status update method.""" + _LOGGER.debug("Received status update for %s", self._config_entry_id) + self.async_schedule_update_ha_state(True) + + @property + def should_poll(self): + """No polling needed for GeoNet NZ Quakes status sensor.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + _LOGGER.debug("Updating %s", self._config_entry_id) + if self._manager: + status_info = self._manager.status_info() + if status_info: + self._update_from_status_info(status_info) + + def _update_from_status_info(self, status_info): + """Update the internal state from the provided information.""" + self._status = status_info.status + self._last_update = ( + dt.as_utc(status_info.last_update) if status_info.last_update else None + ) + self._last_update_successful = ( + dt.as_utc(status_info.last_update_successful) + if status_info.last_update_successful + else None + ) + self._last_timestamp = status_info.last_timestamp + self._total = status_info.total + self._created = status_info.created + self._updated = status_info.updated + self._removed = status_info.removed + + @property + def state(self): + """Return the state of the sensor.""" + return self._total + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return f"GeoNet NZ Quakes ({self._config_title})" + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEFAULT_ICON + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return DEFAULT_UNIT_OF_MEASUREMENT + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_STATUS, self._status), + (ATTR_LAST_UPDATE, self._last_update), + (ATTR_LAST_UPDATE_SUCCESSFUL, self._last_update_successful), + (ATTR_LAST_TIMESTAMP, self._last_timestamp), + (ATTR_CREATED, self._created), + (ATTR_UPDATED, self._updated), + (ATTR_REMOVED, self._removed), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/tests/components/geonetnz_quakes/__init__.py b/tests/components/geonetnz_quakes/__init__.py index 95c50679338bb3..424c6372ea8c6f 100644 --- a/tests/components/geonetnz_quakes/__init__.py +++ b/tests/components/geonetnz_quakes/__init__.py @@ -1 +1,31 @@ """Tests for the geonetnz_quakes component.""" +from unittest.mock import MagicMock + + +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 diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index c5b7282f320df2..04bbdc9dcf03c9 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -1,6 +1,5 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime -from unittest.mock import MagicMock from asynctest import patch, CoroutineMock @@ -30,39 +29,11 @@ from homeassistant.util.unit_system import IMPERIAL_SYSTEM from tests.common import async_fire_time_changed import homeassistant.util.dt as dt_util +from tests.components.geonetnz_quakes import _generate_mock_feed_entry 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. @@ -94,13 +65,13 @@ async def test_setup(hass): ) 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. + # Artificially trigger update and collect events. 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 + # 3 geolocation and 1 sensor entities + assert len(all_states) == 4 state = hass.states.get("geo_location.title_1") assert state is not None @@ -155,14 +126,13 @@ async def test_setup(hass): } assert float(state.state) == 25.5 - # Simulate an update - one existing, one new entry, - # one outdated entry + # Simulate an update - two 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 + assert len(all_states) == 4 # Simulate an update - empty data, but successful update, # so no changes to entities. @@ -171,7 +141,7 @@ async def test_setup(hass): await hass.async_block_till_done() all_states = hass.states.async_all() - assert len(all_states) == 3 + assert len(all_states) == 4 # Simulate an update - empty data, removes all entities mock_feed_update.return_value = "ERROR", None @@ -179,7 +149,7 @@ async def test_setup(hass): await hass.async_block_till_done() all_states = hass.states.async_all() - assert len(all_states) == 0 + assert len(all_states) == 1 async def test_setup_imperial(hass): @@ -193,17 +163,22 @@ async def test_setup_imperial(hass): 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: + "aio_geojson_client.feed.GeoJsonFeed.__init__", + new_callable=CoroutineMock, + create=True, + ) as mock_feed_init, patch( + "aio_geojson_client.feed.GeoJsonFeed.last_timestamp", + new_callable=CoroutineMock, + create=True, + ): mock_feed_update.return_value = "OK", [mock_entry_1] assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) - # Artificially trigger update. + # Artificially trigger update and collect events. 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 + assert len(all_states) == 2 # Test conversion of 200 miles to kilometers. assert mock_feed_init.call_args[1].get("filter_radius") == 321.8688 diff --git a/tests/components/geonetnz_quakes/test_sensor.py b/tests/components/geonetnz_quakes/test_sensor.py new file mode 100644 index 00000000000000..518e08f02bb7b1 --- /dev/null +++ b/tests/components/geonetnz_quakes/test_sensor.py @@ -0,0 +1,115 @@ +"""The tests for the GeoNet NZ Quakes Feed integration.""" +import datetime + +from asynctest import patch, CoroutineMock + +from homeassistant.components import geonetnz_quakes +from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL +from homeassistant.components.geonetnz_quakes.sensor import ( + ATTR_STATUS, + ATTR_LAST_UPDATE, + ATTR_CREATED, + ATTR_UPDATED, + ATTR_REMOVED, + ATTR_LAST_UPDATE_SUCCESSFUL, +) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, + CONF_RADIUS, + ATTR_UNIT_OF_MEASUREMENT, + ATTR_ICON, +) +from homeassistant.setup import async_setup_component +from tests.common import async_fire_time_changed +import homeassistant.util.dt as dt_util +from tests.components.geonetnz_quakes import _generate_mock_feed_entry + +CONFIG = {geonetnz_quakes.DOMAIN: {CONF_RADIUS: 200}} + + +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 and collect events. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + # 3 geolocation and 1 sensor entities + assert len(all_states) == 4 + + state = hass.states.get("sensor.geonet_nz_quakes_32_87336_117_22743") + assert state is not None + assert int(state.state) == 3 + assert state.name == "GeoNet NZ Quakes (32.87336, -117.22743)" + attributes = state.attributes + assert attributes[ATTR_STATUS] == "OK" + assert attributes[ATTR_CREATED] == 3 + assert attributes[ATTR_LAST_UPDATE].tzinfo == dt_util.UTC + assert attributes[ATTR_LAST_UPDATE_SUCCESSFUL].tzinfo == dt_util.UTC + assert attributes[ATTR_LAST_UPDATE] == attributes[ATTR_LAST_UPDATE_SUCCESSFUL] + assert attributes[ATTR_UNIT_OF_MEASUREMENT] == "quakes" + assert attributes[ATTR_ICON] == "mdi:pulse" + + # Simulate an update - two 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) == 4 + + state = hass.states.get("sensor.geonet_nz_quakes_32_87336_117_22743") + attributes = state.attributes + assert attributes[ATTR_CREATED] == 1 + assert attributes[ATTR_UPDATED] == 2 + assert attributes[ATTR_REMOVED] == 1 + + # 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) == 4 + + # 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) == 1 + + state = hass.states.get("sensor.geonet_nz_quakes_32_87336_117_22743") + attributes = state.attributes + assert attributes[ATTR_REMOVED] == 3 From f9edec19ada1b794a10fff8f58a003cacc7ec3b4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Sep 2019 17:27:14 +0200 Subject: [PATCH 0123/3953] Use literal string interpolation in integrations H-J (f-strings) (#26380) --- homeassistant/components/habitica/sensor.py | 2 +- .../components/hangouts/hangouts_bot.py | 2 +- homeassistant/components/hangouts/intents.py | 2 +- .../components/haveibeenpwned/sensor.py | 4 ++-- homeassistant/components/hddtemp/sensor.py | 2 +- homeassistant/components/hdmi_cec/__init__.py | 6 +++--- homeassistant/components/heos/config_flow.py | 2 +- homeassistant/components/heos/media_player.py | 8 ++++---- .../components/hikvision/binary_sensor.py | 8 ++++---- .../components/hitron_coda/device_tracker.py | 4 ++-- homeassistant/components/hive/binary_sensor.py | 6 +++--- homeassistant/components/hive/climate.py | 8 ++++---- homeassistant/components/hive/light.py | 6 +++--- homeassistant/components/hive/sensor.py | 6 +++--- homeassistant/components/hive/switch.py | 6 +++--- homeassistant/components/hive/water_heater.py | 6 +++--- .../components/homeassistant/__init__.py | 2 +- homeassistant/components/homekit/type_lights.py | 10 +++------- .../components/homekit/type_thermostats.py | 8 ++++---- homeassistant/components/homekit/util.py | 4 +--- .../components/homekit_controller/__init__.py | 6 +++--- .../components/homekit_controller/const.py | 6 +++--- .../components/homekit_controller/storage.py | 2 +- homeassistant/components/homematic/__init__.py | 6 +++--- .../components/homematicip_cloud/__init__.py | 2 +- .../homematicip_cloud/alarm_control_panel.py | 4 ++-- .../homematicip_cloud/binary_sensor.py | 2 +- .../components/homematicip_cloud/device.py | 6 +++--- .../components/homematicip_cloud/light.py | 2 +- .../components/homematicip_cloud/switch.py | 6 +++--- homeassistant/components/homeworks/__init__.py | 2 +- homeassistant/components/honeywell/climate.py | 17 ++++++----------- homeassistant/components/hp_ilo/sensor.py | 2 +- homeassistant/components/html5/notify.py | 4 ++-- homeassistant/components/htu21d/sensor.py | 2 +- homeassistant/components/huawei_lte/sensor.py | 2 +- .../components/huawei_router/device_tracker.py | 8 ++++---- homeassistant/components/hue/config_flow.py | 2 +- homeassistant/components/hydroquebec/sensor.py | 4 ++-- .../components/ialarm/alarm_control_panel.py | 4 ++-- .../components/icloud/device_tracker.py | 8 ++++---- .../components/ign_sismologia/geo_location.py | 4 ++-- homeassistant/components/ihc/__init__.py | 4 ++-- .../components/imap_email_content/sensor.py | 2 +- homeassistant/components/incomfort/climate.py | 2 +- .../components/incomfort/water_heater.py | 2 +- homeassistant/components/influxdb/__init__.py | 2 +- homeassistant/components/insteon/__init__.py | 10 +++++----- homeassistant/components/ios/sensor.py | 6 +++--- homeassistant/components/iota/sensor.py | 2 +- homeassistant/components/iperf3/__init__.py | 2 +- homeassistant/components/ipma/weather.py | 2 +- homeassistant/components/iqvia/__init__.py | 2 +- homeassistant/components/iqvia/sensor.py | 10 +++++----- homeassistant/components/isy994/__init__.py | 6 +++--- homeassistant/components/isy994/sensor.py | 2 +- homeassistant/components/itach/remote.py | 2 +- homeassistant/components/itunes/media_player.py | 6 +++--- .../components/jewish_calendar/sensor.py | 2 +- 59 files changed, 128 insertions(+), 139 deletions(-) diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index e70d0eb696a1fb..1fa4ad63b36bf4 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -66,7 +66,7 @@ def icon(self): @property def name(self): """Return the name of the sensor.""" - return "{0}_{1}_{2}".format(habitica.DOMAIN, self._name, self._sensor_name) + return f"{habitica.DOMAIN}_{self._name}_{self._sensor_name}" @property def state(self): diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index d444e852cca885..35f866b3d813aa 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -323,7 +323,7 @@ async def _async_list_conversations(self): } self.hass.states.async_set( - "{}.conversations".format(DOMAIN), + f"{DOMAIN}.conversations", len(self._conversation_list.get_all()), attributes=conversations, ) diff --git a/homeassistant/components/hangouts/intents.py b/homeassistant/components/hangouts/intents.py index a26da7a4872e9c..5e4c6ff206bfe5 100644 --- a/homeassistant/components/hangouts/intents.py +++ b/homeassistant/components/hangouts/intents.py @@ -25,7 +25,7 @@ async def async_handle(self, intent_obj): help_text = "I understand the following sentences:" for intent_data in intents.values(): for sentence in intent_data["sentences"]: - help_text += "\n'{}'".format(sentence) + help_text += f"\n'{sentence}'" response.async_set_speech(help_text) return response diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index ec43d9444a238c..7fa3f422300fb5 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -61,7 +61,7 @@ def __init__(self, data, email): @property def name(self): """Return the name of the sensor.""" - return "Breaches {}".format(self._email) + return f"Breaches {self._email}" @property def unit_of_measurement(self): @@ -151,7 +151,7 @@ def update_no_throttle(self): def update(self, **kwargs): """Get the latest data for current email from REST service.""" try: - url = "{}{}?truncateResponse=false".format(URL, self._email) + url = f"{URL}{self._email}?truncateResponse=false" 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=header, allow_redirects=True, timeout=5) diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index fa3b5fd256ce69..d0dd5018dca8b6 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -67,7 +67,7 @@ def __init__(self, name, disk, hddtemp): """Initialize a HDDTemp sensor.""" self.hddtemp = hddtemp self.disk = disk - self._name = "{} {}".format(name, disk) + self._name = f"{name} {disk}" self._state = None self._details = None self._unit = None diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 969925182fde23..d1637f96d95db8 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -264,7 +264,7 @@ def _tx(call): if isinstance(data[ATTR_ATT], (list,)): att = data[ATTR_ATT] else: - att = reduce(lambda x, y: "%s:%x" % (x, y), data[ATTR_ATT]) + att = reduce(lambda x, y: f"{x}:{y:x}", data[ATTR_ATT]) else: att = "" command = CecCommand(cmd, dst, src, att) @@ -312,7 +312,7 @@ def _update(call): def _new_device(device): """Handle new devices which are detected by HDMI network.""" - key = "{}.{}".format(DOMAIN, device.name) + key = f"{DOMAIN}.{device.name}" hass.data[key] = device ent_platform = base_config[DOMAIN][CONF_TYPES].get(key, platform) discovery.load_platform( @@ -399,7 +399,7 @@ def _update(self, device=None): def name(self): """Return the name of the device.""" return ( - "%s %s" % (self.vendor_name, self._device.osd_name) + f"{self.vendor_name} {self._device.osd_name}" if ( self._device.osd_name is not None and self.vendor_name is not None diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 1d56478ba3ac6f..4380cb4d8ba09a 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -10,7 +10,7 @@ def format_title(host: str) -> str: """Format the title for config entries.""" - return "Controller ({})".format(host) + return f"Controller ({host})" @config_entries.HANDLERS.register(DOMAIN) diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 40f6113a80d794..10ea28ca16cb19 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -183,7 +183,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): None, ) if index is None: - raise ValueError("Invalid quick select '{}'".format(media_id)) + raise ValueError(f"Invalid quick select '{media_id}'") await self._player.play_quick_select(index) return @@ -191,7 +191,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): playlists = await self._player.heos.get_playlists() playlist = next((p for p in playlists if p.name == media_id), None) if not playlist: - raise ValueError("Invalid playlist '{}'".format(media_id)) + raise ValueError(f"Invalid playlist '{media_id}'") add_queue_option = ( heos_const.ADD_QUEUE_ADD_TO_END if kwargs.get(ATTR_MEDIA_ENQUEUE) @@ -215,11 +215,11 @@ async def async_play_media(self, media_type, media_id, **kwargs): None, ) if index is None: - raise ValueError("Invalid favorite '{}'".format(media_id)) + raise ValueError(f"Invalid favorite '{media_id}'") await self._player.play_favorite(index) return - raise ValueError("Unsupported media type '{}'".format(media_type)) + raise ValueError(f"Unsupported media type '{media_type}'") @log_command_error("select source") async def async_select_source(self, source): diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index a9ab242c2fda0e..b898f5d860c2bb 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -93,7 +93,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: protocol = "http" - url = "{}://{}".format(protocol, host) + url = f"{protocol}://{host}" data = HikvisionData(hass, url, port, name, username, password) @@ -196,11 +196,11 @@ def __init__(self, hass, sensor, channel, cam, delay): self._channel = channel if self._cam.type == "NVR": - self._name = "{} {} {}".format(self._cam.name, sensor, channel) + self._name = f"{self._cam.name} {sensor} {channel}" else: - self._name = "{} {}".format(self._cam.name, sensor) + self._name = f"{self._cam.name} {sensor}" - self._id = "{}.{}.{}".format(self._cam.cam_id, sensor, channel) + self._id = f"{self._cam.cam_id}.{sensor}.{channel}" if delay is None: self._delay = 0 diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index e3e8975c1257c8..2f3526d45b6dbc 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -44,8 +44,8 @@ def __init__(self, config): """Initialize the scanner.""" self.last_results = [] host = config[CONF_HOST] - self._url = "http://{}/data/getConnectInfo.asp".format(host) - self._loginurl = "http://{}/goform/login".format(host) + self._url = f"http://{host}/data/getConnectInfo.asp" + self._loginurl = f"http://{host}/goform/login" self._username = config.get(CONF_USERNAME) self._password = config.get(CONF_PASSWORD) diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 80aaaf8646311a..50c8277302fbc7 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -26,8 +26,8 @@ def __init__(self, hivesession, hivedevice): self.node_device_type = hivedevice["Hive_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" self.session.entities.append(self) @property @@ -42,7 +42,7 @@ def device_info(self): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index d4a1c915518eff..861957e6ef0166 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -54,8 +54,8 @@ def __init__(self, hivesession, hivedevice): self.thermostat_node_id = hivedevice["Thermostat_NodeID"] self.session = hivesession self.attributes = {} - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" @property def unique_id(self): @@ -74,7 +74,7 @@ def supported_features(self): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property @@ -82,7 +82,7 @@ def name(self): """Return the name of the Climate device.""" friendly_name = "Heating" if self.node_name is not None: - friendly_name = "{} {}".format(self.node_name, friendly_name) + friendly_name = f"{self.node_name} {friendly_name}" return friendly_name @property diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 5892e304379ba4..a85c3a43992eb8 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -33,8 +33,8 @@ def __init__(self, hivesession, hivedevice): self.light_device_type = hivedevice["Hive_Light_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" self.session.entities.append(self) @property @@ -49,7 +49,7 @@ def device_info(self): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index dd3343633d8b47..c43fe461a8e6a1 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -37,8 +37,8 @@ def __init__(self, hivesession, hivedevice): self.device_type = hivedevice["HA_DeviceType"] self.node_device_type = hivedevice["Hive_DeviceType"] self.session = hivesession - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" self.session.entities.append(self) @property @@ -53,7 +53,7 @@ def device_info(self): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 4644ccaec00614..75efdfe3e5d461 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -23,8 +23,8 @@ def __init__(self, hivesession, hivedevice): self.device_type = hivedevice["HA_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" self.session.entities.append(self) @property @@ -39,7 +39,7 @@ def device_info(self): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index f186d804d34c89..1b009582c1aec7 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -42,8 +42,8 @@ def __init__(self, hivesession, hivedevice): self.node_name = hivedevice["Hive_NodeName"] self.device_type = hivedevice["HA_DeviceType"] self.session = hivesession - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" self._unit_of_measurement = TEMP_CELSIUS @property @@ -63,7 +63,7 @@ def supported_features(self): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 2bd0a62cebb1a9..02e53d1de108e8 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -110,7 +110,7 @@ async def async_handle_core_service(call): hass.components.persistent_notification.async_create( "Config error. See dev-info panel for details.", "Config validating", - "{0}.check_config".format(ha.DOMAIN), + f"{ha.DOMAIN}.check_config", ) return diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index fce81d0adf7366..8e1b07fbbfff34 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -127,9 +127,7 @@ def set_brightness(self, value): self.set_state(0) # Turn off light return params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} - self.call_service( - DOMAIN, SERVICE_TURN_ON, params, "brightness at {}%".format(value) - ) + self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"brightness at {value}%") def set_color_temperature(self, value): """Set color temperature if call came from HomeKit.""" @@ -137,7 +135,7 @@ def set_color_temperature(self, value): self._flag[CHAR_COLOR_TEMPERATURE] = True params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} self.call_service( - DOMAIN, SERVICE_TURN_ON, params, "color temperature at {}".format(value) + DOMAIN, SERVICE_TURN_ON, params, f"color temperature at {value}" ) def set_saturation(self, value): @@ -167,9 +165,7 @@ def set_color(self): {CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True} ) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} - self.call_service( - DOMAIN, SERVICE_TURN_ON, params, "set color at {}".format(color) - ) + self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"set color at {color}") def update_state(self, new_state): """Update light after state change.""" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index e00912d340eb26..63eb688a0c1866 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -209,7 +209,7 @@ def set_cooling_threshold(self, value): DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, params, - "cooling threshold {}{}".format(temperature, self._unit), + f"cooling threshold {temperature}{self._unit}", ) @debounce @@ -230,7 +230,7 @@ def set_heating_threshold(self, value): DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, params, - "heating threshold {}{}".format(temperature, self._unit), + f"heating threshold {temperature}{self._unit}", ) @debounce @@ -244,7 +244,7 @@ def set_target_temperature(self, value): DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, params, - "{}{}".format(temperature, self._unit), + f"{temperature}{self._unit}", ) def update_state(self, new_state): @@ -378,7 +378,7 @@ def set_target_temperature(self, value): DOMAIN_WATER_HEATER, SERVICE_SET_TEMPERATURE_WATER_HEATER, params, - "{}{}".format(temperature, self._unit), + f"{temperature}{self._unit}", ) def update_state(self, new_state): diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 3b5f3c814368d5..d60c94d420deb9 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -116,9 +116,7 @@ def validate_entity_config(values): params = MEDIA_PLAYER_SCHEMA(feature) key = params.pop(CONF_FEATURE) if key in feature_list: - raise vol.Invalid( - "A feature can be added only once for {}".format(entity) - ) + raise vol.Invalid(f"A feature can be added only once for {entity}") feature_list[key] = params config[CONF_FEATURE_LIST] = feature_list diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 5ae82d0f124af8..6a6492847225f1 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -106,7 +106,7 @@ def _setup_characteristic(self, char): # Callback to allow entity to configure itself based on this # characteristics metadata (valid values, value ranges, features, etc) setup_fn_name = escape_characteristic_name(short_name) - setup_fn = getattr(self, "_setup_{}".format(setup_fn_name), None) + setup_fn = getattr(self, f"_setup_{setup_fn_name}", None) if not setup_fn: return # pylint: disable=not-callable @@ -128,7 +128,7 @@ def async_state_changed(self): # 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) + update_fn = getattr(self, f"_update_{char_name}", None) if not update_fn: continue @@ -141,7 +141,7 @@ def async_state_changed(self): def unique_id(self): """Return the ID of this device.""" serial = self._accessory_info["serial-number"] - return "homekit-{}-{}".format(serial, self._iid) + return f"homekit-{serial}-{self._iid}" @property def name(self): diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index ad12f3fdb71213..09a7df2a2bfa67 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -1,9 +1,9 @@ """Constants for the homekit_controller component.""" DOMAIN = "homekit_controller" -KNOWN_DEVICES = "{}-devices".format(DOMAIN) -CONTROLLER = "{}-controller".format(DOMAIN) -ENTITY_MAP = "{}-entity-map".format(DOMAIN) +KNOWN_DEVICES = f"{DOMAIN}-devices" +CONTROLLER = f"{DOMAIN}-controller" +ENTITY_MAP = f"{DOMAIN}-entity-map" HOMEKIT_DIR = ".homekit" PAIRING_FILE = "pairing.json" diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index ec5a2e7cc4334e..46d095b5631823 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -5,7 +5,7 @@ from .const import DOMAIN -ENTITY_MAP_STORAGE_KEY = "{}-entity-map".format(DOMAIN) +ENTITY_MAP_STORAGE_KEY = f"{DOMAIN}-entity-map" ENTITY_MAP_STORAGE_VERSION = 1 ENTITY_MAP_SAVE_DELAY = 10 diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 0ab47247edc201..598e3765612420 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -711,15 +711,15 @@ def _create_ha_id(name, channel, param, count): # Has multiple elements/channels if count > 1 and param is None: - return "{} {}".format(name, channel) + return f"{name} {channel}" # With multiple parameters on first channel if count == 1 and param is not None: - return "{} {}".format(name, param) + return f"{name} {param}" # Multiple parameters with multiple channels if count > 1 and param is not None: - return "{} {} {}".format(name, channel, param) + return f"{name} {channel} {param}" def _hm_event_handler(hass, interface, device, caller, attribute, value): diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index f2d84095b19357..c8fb31998ef4ad 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -234,7 +234,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_registry = await dr.async_get_registry(hass) home = hap.home # Add the HAP name from configuration if set. - hapname = home.label if not home.name else "{} {}".format(home.label, home.name) + hapname = home.label if not home.name else f"{home.label} {home.name}" device_registry.async_get_or_create( config_entry_id=home.id, identifiers={(DOMAIN, home.id)}, diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 38097afc1b65f3..592d234225cff9 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -112,7 +112,7 @@ def name(self) -> str: """Return the name of the generic device.""" name = CONST_ALARM_CONTROL_PANEL_NAME if self._home.name: - name = "{} {}".format(self._home.name, name) + name = f"{self._home.name} {name}" return name @property @@ -131,7 +131,7 @@ def available(self) -> bool: @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}_{}".format(self.__class__.__name__, self._home.id) + return f"{self.__class__.__name__}_{self._home.id}" def _get_zone_alarm_state(security_zone) -> bool: diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 97746f3f472b43..d6bc24d21edf18 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -291,7 +291,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD def __init__(self, home: AsyncHome, device, post: str = "SecurityZone") -> None: """Initialize security zone group.""" - device.modelType = "HmIP-{}".format(post) + device.modelType = f"HmIP-{post}" super().__init__(home, device, post) @property diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 71855d7c3f5a4a..5eeb14b635946c 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -92,9 +92,9 @@ def name(self) -> str: """Return the name of the generic device.""" name = self._device.label if self._home.name is not None and self._home.name != "": - name = "{} {}".format(self._home.name, name) + name = f"{self._home.name} {name}" if self.post is not None and self.post != "": - name = "{} {}".format(name, self.post) + name = f"{name} {self.post}" return name @property @@ -110,7 +110,7 @@ def available(self) -> bool: @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}_{}".format(self.__class__.__name__, self._device.id) + return f"{self.__class__.__name__}_{self._device.id}" @property def icon(self) -> Optional[str]: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index c034b19bb3a860..bc7b12f9653ea5 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -205,7 +205,7 @@ def supported_features(self) -> int: @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}_{}_{}".format(self.__class__.__name__, self.post, self._device.id) + return f"{self.__class__.__name__}_{self.post}_{self._device.id}" async def async_turn_on(self, **kwargs): """Turn the light on.""" diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index a9535736d0f23a..6d19087781daef 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -93,7 +93,7 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): def __init__(self, home: AsyncHome, device, post: str = "Group") -> None: """Initialize switching group.""" - device.modelType = "HmIP-{}".format(post) + device.modelType = f"HmIP-{post}" super().__init__(home, device, post) @property @@ -149,12 +149,12 @@ class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): def __init__(self, home: AsyncHome, device, channel: int): """Initialize the multi switch device.""" self.channel = channel - super().__init__(home, device, "Channel{}".format(channel)) + super().__init__(home, device, f"Channel{channel}") @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}_{}_{}".format(self.__class__.__name__, self.post, self._device.id) + return f"{self.__class__.__name__}_{self.post}_{self._device.id}" @property def is_on(self) -> bool: diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index dcc2ce5dde62d1..bd40336b8ba894 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -106,7 +106,7 @@ def __init__(self, controller, addr, name): @property def unique_id(self): """Return a unique identifier.""" - return "homeworks.{}".format(self._addr) + return f"homeworks.{self._addr}" @property def name(self): diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 62a370f60faa8e..84a816628c0626 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -318,7 +318,7 @@ def _set_temperature(self, **kwargs) -> None: # Get current mode mode = self._device.system_mode # Set hold if this is not the case - if getattr(self._device, "hold_{}".format(mode)) is False: + if getattr(self._device, f"hold_{mode}") is False: # Get next period key next_period_key = "{}NextPeriod".format(mode.capitalize()) # Get next period raw value @@ -326,11 +326,9 @@ def _set_temperature(self, **kwargs) -> None: # Get next period time hour, minute = divmod(next_period * 15, 60) # Set hold time - setattr( - self._device, "hold_{}".format(mode), datetime.time(hour, minute) - ) + setattr(self._device, f"hold_{mode}", datetime.time(hour, minute)) # Set temperature - setattr(self._device, "setpoint_{}".format(mode), temperature) + setattr(self._device, f"setpoint_{mode}", temperature) except somecomfort.SomeComfortError: _LOGGER.error("Temperature %.1f out of range", temperature) @@ -375,17 +373,14 @@ def _turn_away_mode_on(self) -> None: try: # Set permanent hold - setattr(self._device, "hold_{}".format(mode), True) + setattr(self._device, f"hold_{mode}", True) # Set temperature setattr( - self._device, - "setpoint_{}".format(mode), - getattr(self, "_{}_away_temp".format(mode)), + self._device, f"setpoint_{mode}", getattr(self, f"_{mode}_away_temp") ) except somecomfort.SomeComfortError: _LOGGER.error( - "Temperature %.1f out of range", - getattr(self, "_{}_away_temp".format(mode)), + "Temperature %.1f out of range", getattr(self, f"_{mode}_away_temp") ) def _turn_away_mode_off(self) -> None: diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index 1ad70c06397d52..cf95c21a8d1e7e 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -194,4 +194,4 @@ def update(self): hpilo.IloCommunicationError, hpilo.IloLoginFailed, ) as error: - raise ValueError("Unable to init HP ILO, {}".format(error)) + raise ValueError(f"Unable to init HP ILO, {error}") diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 18882968cf900a..ac76911b9f63ea 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -570,8 +570,8 @@ def create_vapid_headers(vapid_email, subscription_info, vapid_private_key): if vapid_email and vapid_private_key and ATTR_ENDPOINT in subscription_info: url = urlparse(subscription_info.get(ATTR_ENDPOINT)) vapid_claims = { - "sub": "mailto:{}".format(vapid_email), - "aud": "{}://{}".format(url.scheme, url.netloc), + "sub": f"mailto:{vapid_email}", + "aud": f"{url.scheme}://{url.netloc}", } vapid = Vapid.from_string(private_key=vapid_private_key) return vapid.sign(vapid_claims) diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index c2223720eb5b2e..f94b11d5ada003 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -76,7 +76,7 @@ class HTU21DSensor(Entity): def __init__(self, htu21d_client, name, variable, unit): """Initialize the sensor.""" - self._name = "{}_{}".format(name, variable) + self._name = f"{name}_{variable}" self._variable = variable self._unit_of_measurement = unit self._client = htu21d_client diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index a2b52d1c164eb7..cb8f5fb5766aab 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -175,7 +175,7 @@ class HuaweiLteSensor(Entity): @property def unique_id(self) -> str: """Return unique ID for sensor.""" - return "{}-{}".format(self.data.mac, self.path) + return f"{self.data.mac}-{self.path}" @property def name(self) -> str: diff --git a/homeassistant/components/huawei_router/device_tracker.py b/homeassistant/components/huawei_router/device_tracker.py index 08b7c9ec859b9c..b7b5731dfd3b87 100644 --- a/homeassistant/components/huawei_router/device_tracker.py +++ b/homeassistant/components/huawei_router/device_tracker.py @@ -119,12 +119,12 @@ def _get_data(self): def _get_devices_response(self): """Get the raw string with the devices from the router.""" - cnt = requests.post("http://{}/asp/GetRandCount.asp".format(self.host)) + cnt = requests.post(f"http://{self.host}/asp/GetRandCount.asp") cnt_str = str(cnt.content, cnt.apparent_encoding, errors="replace") _LOGGER.debug("Logging in") cookie = requests.post( - "http://{}/login.cgi".format(self.host), + f"http://{self.host}/login.cgi", data=[ ("UserName", self.username), ("PassWord", self.password), @@ -136,13 +136,13 @@ def _get_devices_response(self): _LOGGER.debug("Requesting lan user info update") # this request is needed or else some devices' state won't be updated requests.get( - "http://{}/html/bbsp/common/lanuserinfo.asp".format(self.host), + f"http://{self.host}/html/bbsp/common/lanuserinfo.asp", cookies=cookie.cookies, ) _LOGGER.debug("Requesting lan user info data") devices = requests.get( - "http://{}/html/bbsp/common/GetLanUserDevInfo.asp".format(self.host), + f"http://{self.host}/html/bbsp/common/GetLanUserDevInfo.asp", cookies=cookie.cookies, ) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 0b0e3723b138a1..9c0e94bc3bdce0 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -160,7 +160,7 @@ async def async_step_ssdp(self, discovery_info): { "host": host, # This format is the legacy format that Hue used for discovery - "path": "phue-{}.conf".format(serial), + "path": f"phue-{serial}.conf", } ) diff --git a/homeassistant/components/hydroquebec/sensor.py b/homeassistant/components/hydroquebec/sensor.py index 79de222397b991..fd713e8b7a7937 100644 --- a/homeassistant/components/hydroquebec/sensor.py +++ b/homeassistant/components/hydroquebec/sensor.py @@ -104,7 +104,7 @@ ) HOST = "https://www.hydroquebec.com" -HOME_URL = "{}/portail/web/clientele/authentification".format(HOST) +HOME_URL = f"{HOST}/portail/web/clientele/authentification" PROFILE_URL = "{}/portail/fr/group/clientele/" "portrait-de-consommation".format(HOST) MONTHLY_MAP = ( ("period_total_bill", "montantFacturePeriode"), @@ -164,7 +164,7 @@ def __init__(self, hydroquebec_data, sensor_type, name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index e98e712bc6ff7a..845c6b9021f1e2 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -28,7 +28,7 @@ def no_application_protocol(value): """Validate that value is without the application protocol.""" protocol_separator = "://" if not value or protocol_separator in value: - raise vol.Invalid("Invalid host, {} is not allowed".format(protocol_separator)) + raise vol.Invalid(f"Invalid host, {protocol_separator} is not allowed") return value @@ -52,7 +52,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config.get(CONF_PASSWORD) host = config.get(CONF_HOST) - url = "http://{}".format(host) + url = f"http://{host}" ialarm = IAlarmPanel(name, code, username, password, url) add_entities([ialarm], True) diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index dbd4f54bac6022..2ecf904314fe4b 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -281,10 +281,10 @@ def icloud_need_trusted_device(self): devicename = device.get( "deviceName", "SMS to %s" % device.get("phoneNumber") ) - devicesstring += "{}: {};".format(i, devicename) + devicesstring += f"{i}: {devicename};" _CONFIGURING[self.accountname] = configurator.request_config( - "iCloud {}".format(self.accountname), + f"iCloud {self.accountname}", self.icloud_trusted_device_callback, description=( "Please choose your trusted device by entering" @@ -327,7 +327,7 @@ def icloud_need_verification_code(self): return _CONFIGURING[self.accountname] = configurator.request_config( - "iCloud {}".format(self.accountname), + f"iCloud {self.accountname}", self.icloud_verification_callback, description=("Please enter the validation code:"), entity_picture="/static/images/config_icloud.png", @@ -528,7 +528,7 @@ def setinterval(self, interval=None, devicename=None): """Set the interval of the given devices.""" devs = [devicename] if devicename else self.devices for device in devs: - devid = "{}.{}".format(DOMAIN, device) + devid = f"{DOMAIN}.{device}" devicestate = self.hass.states.get(devid) if interval is not None: if devicestate is not None: diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index abba8749663d1d..44ba41ea7835fe 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -210,9 +210,9 @@ def source(self) -> str: def name(self) -> Optional[str]: """Return the name of the entity.""" if self._magnitude and self._region: - return "M {:.1f} - {}".format(self._magnitude, self._region) + return f"M {self._magnitude:.1f} - {self._region}" if self._magnitude: - return "M {:.1f}".format(self._magnitude) + return f"M {self._magnitude:.1f}" if self._region: return self._region return self._title diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 3be40058fec13f..a55b94eb26a5eb 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -61,7 +61,7 @@ def validate_name(config): if CONF_NAME in config: return config ihcid = config[CONF_ID] - name = "ihc_{}".format(ihcid) + name = f"ihc_{ihcid}" config[CONF_NAME] = name return config @@ -312,7 +312,7 @@ def get_discovery_info(component_setup, groups, controller_id): if "setting" in node.attrib and node.attrib["setting"] == "yes": continue ihc_id = int(node.attrib["id"].strip("_"), 0) - name = "{}_{}".format(groupname, ihc_id) + name = f"{groupname}_{ihc_id}" device = { "ihc_id": ihc_id, "ctrl_id": controller_id, diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index b1bab09fdcbf79..c5171cde646f14 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -118,7 +118,7 @@ def read_next(self): if not self._unread_ids: search = "SINCE {0:%d-%b-%Y}".format(datetime.date.today()) if self._last_id is not None: - search = "UID {}:*".format(self._last_id) + search = f"UID {self._last_id}:*" _, data = self.connection.uid("search", None, search) self._unread_ids = deque(data[0].split()) diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 0d13caca3b7426..cccb9d256444dc 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -30,7 +30,7 @@ def __init__(self, client, room): """Initialize the climate device.""" self._client = client self._room = room - self._name = "Room {}".format(room.room_no) + self._name = f"Room {room.room_no}" async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 7eeb618c87488e..2449a1223ccc5f 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -96,7 +96,7 @@ def supported_features(self): def current_operation(self): """Return the current operation mode.""" if self._heater.is_failed: - return "Fault code: {}".format(self._heater.fault_code) + return f"Fault code: {self._heater.fault_code}" return self._heater.display_text diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index e8d2cc54bf1e1d..2bb5207aa85408 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -247,7 +247,7 @@ def event_to_json(event): try: json["fields"][key] = float(value) except (ValueError, TypeError): - new_key = "{}_str".format(key) + new_key = f"{key}_str" new_value = str(value) json["fields"][new_key] = new_value diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 2dda073aa182db..4015d472ce8d42 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -314,7 +314,7 @@ def load_aldb(service): def _send_load_aldb_signal(entity_id, reload): """Send the load All-Link database signal to INSTEON entity.""" - signal = "{}_{}".format(entity_id, SIGNAL_LOAD_ALDB) + signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}" dispatcher_send(hass, signal, reload) def print_aldb(service): @@ -322,7 +322,7 @@ def print_aldb(service): # For now this sends logs to the log file. # Furture direction is to create an INSTEON control panel. entity_id = service.data[CONF_ENTITY_ID] - signal = "{}_{}".format(entity_id, SIGNAL_PRINT_ALDB) + signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}" dispatcher_send(hass, signal) def print_im_aldb(service): @@ -652,9 +652,9 @@ async def async_added_to_hass(self): ) self._insteon_device_state.register_updates(self.async_entity_update) self.hass.data[DOMAIN][INSTEON_ENTITIES][self.entity_id] = self - load_signal = "{}_{}".format(self.entity_id, SIGNAL_LOAD_ALDB) + load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}" async_dispatcher_connect(self.hass, load_signal, self._load_aldb) - print_signal = "{}_{}".format(self.entity_id, SIGNAL_PRINT_ALDB) + print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}" async_dispatcher_connect(self.hass, print_signal, self._print_aldb) def _load_aldb(self, reload=False): @@ -679,7 +679,7 @@ def _get_label(self): if self._insteon_device_state.name in STATE_NAME_LABEL_MAP: label = STATE_NAME_LABEL_MAP[self._insteon_device_state.name] else: - label = "Group {:d}".format(self.group) + label = f"Group {self.group:d}" return label diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index 4da0d148f9c718..47c54c3faceecd 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -67,7 +67,7 @@ def state(self): def unique_id(self): """Return the unique ID of this sensor.""" device_id = self._device[ios.ATTR_DEVICE_ID] - return "{}_{}".format(self.type, device_id) + return f"{self.type}_{device_id}" @property def unit_of_measurement(self): @@ -100,11 +100,11 @@ def icon(self): ios.ATTR_BATTERY_STATE_UNPLUGGED, ): charging = False - icon_state = "{}-off".format(DEFAULT_ICON_STATE) + icon_state = f"{DEFAULT_ICON_STATE}-off" elif battery_state == ios.ATTR_BATTERY_STATE_UNKNOWN: battery_level = None charging = False - icon_state = "{}-unknown".format(DEFAULT_ICON_LEVEL) + icon_state = f"{DEFAULT_ICON_LEVEL}-unknown" if self.type == "state": return icon_state diff --git a/homeassistant/components/iota/sensor.py b/homeassistant/components/iota/sensor.py index a34d6ed021421f..8a0b17aa63bdab 100644 --- a/homeassistant/components/iota/sensor.py +++ b/homeassistant/components/iota/sensor.py @@ -46,7 +46,7 @@ def __init__(self, wallet_config, iota_config): @property def name(self): """Return the name of the sensor.""" - return "{} Balance".format(self._name) + return f"{self._name} Balance" @property def state(self): diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 1a68eccb312a1a..eda601b09de3f0 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.event import async_track_time_interval DOMAIN = "iperf3" -DATA_UPDATED = "{}_data_updated".format(DOMAIN) +DATA_UPDATED = f"{DOMAIN}_data_updated" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index fdaf5904aa1ea6..9f1836c73896fd 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -132,7 +132,7 @@ async def async_update(self): @property def unique_id(self) -> str: """Return a unique id.""" - return "{}, {}".format(self._station.latitude, self._station.longitude) + return f"{self._station.latitude}, {self._station.longitude}" @property def attribution(self): diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index b6930e1070f92d..e3add21c3a41d0 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -234,7 +234,7 @@ def state(self): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return "{0}_{1}".format(self._zip_code, self._type) + return f"{self._zip_code}_{self._type}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index f2fd1143b6ac95..90aa89f06d1699 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -174,9 +174,9 @@ async def async_update(self): index = idx + 1 self._attrs.update( { - "{0}_{1}".format(ATTR_ALLERGEN_GENUS, index): attrs["Genus"], - "{0}_{1}".format(ATTR_ALLERGEN_NAME, index): attrs["Name"], - "{0}_{1}".format(ATTR_ALLERGEN_TYPE, index): attrs["PlantType"], + f"{ATTR_ALLERGEN_GENUS}_{index}": attrs["Genus"], + f"{ATTR_ALLERGEN_NAME}_{index}": attrs["Name"], + f"{ATTR_ALLERGEN_TYPE}_{index}": attrs["PlantType"], } ) elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW): @@ -184,8 +184,8 @@ async def async_update(self): index = idx + 1 self._attrs.update( { - "{0}_{1}".format(ATTR_ALLERGEN_NAME, index): attrs["Name"], - "{0}_{1}".format(ATTR_ALLERGEN_AMOUNT, index): attrs["PPM"], + f"{ATTR_ALLERGEN_NAME}_{index}": attrs["Name"], + f"{ATTR_ALLERGEN_AMOUNT}_{index}": attrs["PPM"], } ) elif self._type == TYPE_DISEASE_TODAY: diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 9ad0f6beef154c..407196532ef7fd 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -343,7 +343,7 @@ def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: """Categorize the ISY994 programs.""" for domain in SUPPORTED_PROGRAM_DOMAINS: try: - folder = programs[KEY_MY_PROGRAMS]["HA.{}".format(domain)] + folder = programs[KEY_MY_PROGRAMS][f"HA.{domain}"] except KeyError: pass else: @@ -378,10 +378,10 @@ def _categorize_weather(hass: HomeAssistant, climate) -> None: WeatherNode( getattr(climate, attr), attr.replace("_", " "), - getattr(climate, "{}_units".format(attr)), + getattr(climate, f"{attr}_units"), ) for attr in climate_attrs - if "{}_units".format(attr) in climate_attrs + if f"{attr}_units" in climate_attrs ] hass.data[ISY994_WEATHER].extend(weather_nodes) diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index a382c2f0830499..a9746b004d0493 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -272,7 +272,7 @@ def state(self) -> str: int_prec = int(self._node.prec) decimal_part = str_val[-int_prec:] whole_part = str_val[: len(str_val) - int_prec] - val = float("{}.{}".format(whole_part, decimal_part)) + val = float(f"{whole_part}.{decimal_part}") raw_units = self.raw_unit_of_measurement if raw_units in (TEMP_CELSIUS, TEMP_FAHRENHEIT): val = self.hass.config.units.temperature(val, raw_units) diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py index db646338f409eb..9895b54a50dd43 100644 --- a/homeassistant/components/itach/remote.py +++ b/homeassistant/components/itach/remote.py @@ -78,7 +78,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): cmddata = cmd[CONF_DATA].strip() if not cmddata: cmddata = '""' - cmddatas += "{}\n{}\n".format(cmdname, cmddata) + cmddatas += f"{cmdname}\n{cmddata}\n" itachip2ir.addDevice(name, modaddr, connaddr, cmddatas) devices.append(ITachIP2IRRemote(itachip2ir, name)) add_entities(devices, True) diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index df8ae7bd556ba4..aebe16ffa26616 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -84,13 +84,13 @@ def _base_url(self): uri_scheme = "http://" if self.port: - return "{}{}:{}".format(uri_scheme, self.host, self.port) + return f"{uri_scheme}{self.host}:{self.port}" - return "{}{}".format(uri_scheme, self.host) + return f"{uri_scheme}{self.host}" def _request(self, method, path, params=None): """Make the actual request and return the parsed response.""" - url = "{}{}".format(self._base_url, path) + url = f"{self._base_url}{path}" try: if method == "GET": diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 7e119494a20ec7..d298aee91436b4 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -129,7 +129,7 @@ def __init__( @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def icon(self): From 09a350ba26f985d2546ee8731c7e2c8feaa3c232 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Sep 2019 17:28:47 +0200 Subject: [PATCH 0124/3953] Removes executable but from hassfest codeowners (#26381) --- script/hassfest/codeowners.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 script/hassfest/codeowners.py diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py old mode 100755 new mode 100644 From 12d470331ca39efdbbddef3c2d2bda1eadc234b4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Sep 2019 17:56:41 +0200 Subject: [PATCH 0125/3953] Update translations_upload --- script/translations_upload | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/translations_upload b/script/translations_upload index 52045e41d60c81..22a2bbceba202f 100755 --- a/script/translations_upload +++ b/script/translations_upload @@ -26,8 +26,8 @@ LANG_ISO=en CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) -# Check Travis and CircleCI environment as well -if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${TRAVIS_BRANCH-}" != "dev" ] && [ "${CIRCLE_BRANCH-}" != "dev" ]; then +# Check Travis and Azure environment as well +if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${TRAVIS_BRANCH-}" != "dev" ] && [ "${AZURE_BRANCH-}" != "dev" ]; then echo "Please only run the translations upload script from a clean checkout of dev." exit 1 fi From fcbc2fda49768da4a1ab7950f6ea8d04d4fc7d03 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Sep 2019 17:58:00 +0200 Subject: [PATCH 0126/3953] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index ad536aa9068d1c..6c82bd578035c6 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -25,3 +25,4 @@ jobs: displayName: 'Upload Translation' env: LOKALISE_TOKEN: $(lokaliseToken) + AZURE_BRANCH: $(Build.SourceBranch) From 09a4a81d0980ff4901d5e0175d90955faf3a5e40 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Sep 2019 18:05:59 +0200 Subject: [PATCH 0127/3953] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index 6c82bd578035c6..fe4f70c5bf60c0 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -21,8 +21,8 @@ jobs: inputs: versionSpec: '3.7' - script: | + export LOKALISE_TOKEN="$(lokaliseToken)" + export AZURE_BRANCH="$(Build.SourceBranch)" + ./script/translations_upload displayName: 'Upload Translation' - env: - LOKALISE_TOKEN: $(lokaliseToken) - AZURE_BRANCH: $(Build.SourceBranch) From 7d27b4d2ab978f8c73bc4437e36e457db9c958f9 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Sep 2019 18:07:23 +0200 Subject: [PATCH 0128/3953] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 510e16a351c14f..63ce5b707cf6b2 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -68,11 +68,11 @@ stages: - script: python setup.py sdist bdist_wheel displayName: 'Build package' - script: | + TWINE_USERNAME="$(twineUser)" + TWINE_PASSWORD="$(twinePassword)" + twine upload dist/* --skip-existing displayName: 'Upload pypi' - env: - TWINE_USERNAME: '$(twineUser)' - TWINE_PASSWORD: '$(twinePassword)' - job: 'ReleaseDocker' timeoutInMinutes: 240 pool: From 7d1e3af7013939bd32757bd1e5210d887cada86b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Sep 2019 18:09:08 +0200 Subject: [PATCH 0129/3953] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index fe4f70c5bf60c0..05514ae3273485 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -22,7 +22,7 @@ jobs: versionSpec: '3.7' - script: | export LOKALISE_TOKEN="$(lokaliseToken)" - export AZURE_BRANCH="$(Build.SourceBranch)" + export AZURE_BRANCH="$(Build.SourceBranchName)" ./script/translations_upload displayName: 'Upload Translation' From 330ae0d88548ba14d75faedd0adc36ec55c0240d Mon Sep 17 00:00:00 2001 From: Alexander Date: Tue, 3 Sep 2019 19:09:25 +0200 Subject: [PATCH 0130/3953] Add support Slide cover (#25913) * Add support GoSlide cover * Fixed Parameters differ from overridden Fixed Removed other pylint warnings * Renamed GoSlide to Slide, because of Innovation in Motion rebranding * Fixed codeowners file * Fixed requirements file * Removed pylint: disable=unused-argument Removed DOMAIN not exist check Changed if to min/max Changed 3rd party import to top of the module Removed timeout/retry parameters Removed unused constants Added check for discovery_info is none Changed pass slide object instead of full hass object Changed pass api object instead of full hass object Added unique_id functionality Removed entity_id/name properties Removed supported_features/state functions * Fixed unused variables * Changed Slide API uses snake names Changed Improved exception handling Changed Updated Slide API to 0.50.0 * Changed moved exceptions into goslide-api Changed retry setup into coroutine * Changed str(err) to err Changed invert if result to if not result --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/slide/__init__.py | 157 +++++++++++++++++++ homeassistant/components/slide/const.py | 7 + homeassistant/components/slide/cover.py | 124 +++++++++++++++ homeassistant/components/slide/manifest.json | 12 ++ requirements_all.txt | 3 + 7 files changed, 305 insertions(+) create mode 100755 homeassistant/components/slide/__init__.py create mode 100644 homeassistant/components/slide/const.py create mode 100644 homeassistant/components/slide/cover.py create mode 100644 homeassistant/components/slide/manifest.json diff --git a/.coveragerc b/.coveragerc index df87a5a1f71d70..6583f6e0ae5e1b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -564,6 +564,7 @@ omit = homeassistant/components/skybeacon/sensor.py homeassistant/components/skybell/* homeassistant/components/slack/notify.py + homeassistant/components/slide/* homeassistant/components/sma/sensor.py homeassistant/components/smappee/* homeassistant/components/smarty/* diff --git a/CODEOWNERS b/CODEOWNERS index 27c4f03ae93720..9c2c8673e6e365 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -236,6 +236,7 @@ homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/simplisafe/* @bachya +homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza homeassistant/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre diff --git a/homeassistant/components/slide/__init__.py b/homeassistant/components/slide/__init__.py new file mode 100755 index 00000000000000..54154ae863ee83 --- /dev/null +++ b/homeassistant/components/slide/__init__.py @@ -0,0 +1,157 @@ +"""Component for the Go Slide API.""" + +import logging +from datetime import timedelta + +import voluptuous as vol +from goslideapi import GoSlideCloud, goslideapi + +from homeassistant.const import ( + CONF_USERNAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + STATE_OPEN, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.event import async_track_time_interval, async_call_later +from .const import DOMAIN, SLIDES, API, COMPONENT, DEFAULT_RETRY + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + 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 Slide platform.""" + + async def update_slides(now=None): + """Update slide information.""" + result = await hass.data[DOMAIN][API].slides_overview() + + if result is None: + _LOGGER.error("Slide API does not work or returned an error") + return + + if result: + _LOGGER.debug("Slide API returned %d slide(s)", len(result)) + else: + _LOGGER.warning("Slide API returned 0 slides") + + for slide in result: + if "device_id" not in slide: + _LOGGER.error( + "Found invalid Slide entry, device_id is " "missing. Entry=%s", + slide, + ) + continue + + uid = slide["device_id"].replace("slide_", "") + slidenew = hass.data[DOMAIN][SLIDES].setdefault(uid, {}) + slidenew["mac"] = uid + slidenew["id"] = slide["id"] + slidenew["name"] = slide["device_name"] + slidenew["state"] = None + oldpos = slidenew.get("pos") + slidenew["pos"] = None + slidenew["online"] = False + + if "device_info" not in slide: + _LOGGER.error( + "Slide %s (%s) has no device_info Entry=%s", + slide["id"], + slidenew["mac"], + slide, + ) + continue + + # Check if we have pos (OK) or code (NOK) + if "pos" in slide["device_info"]: + slidenew["online"] = True + slidenew["pos"] = slide["device_info"]["pos"] + slidenew["pos"] = max(0, min(1, slidenew["pos"])) + + if oldpos is None or oldpos == slidenew["pos"]: + slidenew["state"] = ( + STATE_CLOSED if slidenew["pos"] > 0.95 else STATE_OPEN + ) + elif oldpos < slidenew["pos"]: + slidenew["state"] = ( + STATE_CLOSED if slidenew["pos"] >= 0.95 else STATE_CLOSING + ) + else: + slidenew["state"] = ( + STATE_OPEN if slidenew["pos"] <= 0.05 else STATE_OPENING + ) + elif "code" in slide["device_info"]: + _LOGGER.warning( + "Slide %s (%s) is offline with " "code=%s", + slide["id"], + slidenew["mac"], + slide["device_info"]["code"], + ) + else: + _LOGGER.error( + "Slide %s (%s) has invalid device_info %s", + slide["id"], + slidenew["mac"], + slide["device_info"], + ) + + _LOGGER.debug("Updated entry=%s", slidenew) + + async def retry_setup(now): + """Retry setup if a connection/timeout happens on Slide API.""" + await async_setup(hass, config) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][SLIDES] = {} + + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + scaninterval = config[DOMAIN][CONF_SCAN_INTERVAL] + + hass.data[DOMAIN][API] = GoSlideCloud(username, password) + + try: + result = await hass.data[DOMAIN][API].login() + except (goslideapi.ClientConnectionError, goslideapi.ClientTimeoutError) as err: + _LOGGER.error( + "Error connecting to Slide Cloud: %s, going to retry in %s seconds", + err, + DEFAULT_RETRY, + ) + async_call_later(hass, DEFAULT_RETRY, retry_setup) + return True + + if not result: + _LOGGER.error("Slide API returned unknown error during authentication") + return False + + _LOGGER.debug("Slide API successfully authenticated") + + await update_slides() + + hass.async_create_task(async_load_platform(hass, COMPONENT, DOMAIN, {}, config)) + + async_track_time_interval(hass, update_slides, scaninterval) + + return True diff --git a/homeassistant/components/slide/const.py b/homeassistant/components/slide/const.py new file mode 100644 index 00000000000000..de3d2e560c1f2d --- /dev/null +++ b/homeassistant/components/slide/const.py @@ -0,0 +1,7 @@ +"""Define constants for the Go Slide component.""" + +API = "api" +COMPONENT = "cover" +DOMAIN = "slide" +SLIDES = "slides" +DEFAULT_RETRY = 120 diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py new file mode 100644 index 00000000000000..1c4e6da5aaca6b --- /dev/null +++ b/homeassistant/components/slide/cover.py @@ -0,0 +1,124 @@ +"""Support for Go Slide slides.""" + +import logging + +from homeassistant.const import ATTR_ID +from homeassistant.components.cover import ( + ATTR_POSITION, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, + DEVICE_CLASS_CURTAIN, + CoverDevice, +) +from .const import API, DOMAIN, SLIDES + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up cover(s) for Go Slide platform.""" + + if discovery_info is None: + return + + entities = [] + + for slide in hass.data[DOMAIN][SLIDES].values(): + _LOGGER.debug("Setting up Slide entity: %s", slide) + entities.append(SlideCover(hass.data[DOMAIN][API], slide)) + + async_add_entities(entities) + + +class SlideCover(CoverDevice): + """Representation of a Go Slide cover.""" + + def __init__(self, api, slide): + """Initialize the cover.""" + self._api = api + self._slide = slide + self._id = slide["id"] + self._unique_id = slide["mac"] + self._name = slide["name"] + + @property + def unique_id(self): + """Return the device unique id.""" + return self._unique_id + + @property + def name(self): + """Return the device name.""" + return self._name + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return {ATTR_ID: self._id} + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._slide["state"] == STATE_OPENING + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._slide["state"] == STATE_CLOSING + + @property + def is_closed(self): + """Return None if status is unknown, True if closed, else False.""" + if self._slide["state"] is None: + return None + return self._slide["state"] == STATE_CLOSED + + @property + def available(self): + """Return False if state is not available.""" + return self._slide["online"] + + @property + def assumed_state(self): + """Let HA know the integration is assumed state.""" + return True + + @property + def device_class(self): + """Return the device class of the cover.""" + return DEVICE_CLASS_CURTAIN + + @property + def current_cover_position(self): + """Return the current position of cover shutter.""" + pos = self._slide["pos"] + if pos is not None: + pos = int(pos * 100) + return pos + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self._slide["state"] = STATE_OPENING + await self._api.slide_open(self._id) + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + self._slide["state"] = STATE_CLOSING + await self._api.slide_close(self._id) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self._api.slide_stop(self._id) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] / 100 + + if self._slide["pos"] is not None: + if position > self._slide["pos"]: + self._slide["state"] = STATE_CLOSING + else: + self._slide["state"] = STATE_OPENING + + await self._api.slide_set_position(self._id, position) diff --git a/homeassistant/components/slide/manifest.json b/homeassistant/components/slide/manifest.json new file mode 100644 index 00000000000000..f9fd7f242b6332 --- /dev/null +++ b/homeassistant/components/slide/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "slide", + "name": "Slide", + "documentation": "https://www.home-assistant.io/components/slide", + "requirements": [ + "goslide-api==0.5.1" + ], + "dependencies": [], + "codeowners": [ + "@ualex73" + ] +} diff --git a/requirements_all.txt b/requirements_all.txt index 60e580296d0cec..c22370e576d970 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -567,6 +567,9 @@ google-cloud-texttospeech==0.4.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 +# homeassistant.components.slide +goslide-api==0.5.1 + # homeassistant.components.remote_rpi_gpio gpiozero==1.4.1 From 1c5e0123c92650aae65acc304ec70501ad2734f7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Sep 2019 20:35:00 +0200 Subject: [PATCH 0131/3953] Use literal string interpolation in integrations N-Q (f-strings) (#26391) --- homeassistant/components/neato/vacuum.py | 2 +- homeassistant/components/nello/lock.py | 2 +- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/nest/binary_sensor.py | 4 ++-- homeassistant/components/nest/local_auth.py | 2 +- homeassistant/components/netatmo/binary_sensor.py | 2 +- homeassistant/components/netatmo/camera.py | 4 ++-- homeassistant/components/netatmo/climate.py | 2 +- homeassistant/components/netdata/sensor.py | 2 +- homeassistant/components/netgear_lte/__init__.py | 4 ++-- homeassistant/components/nextbus/sensor.py | 6 ++---- homeassistant/components/nfandroidtv/notify.py | 2 +- homeassistant/components/niko_home_control/light.py | 2 +- homeassistant/components/nilu/air_quality.py | 2 +- homeassistant/components/nmbs/sensor.py | 8 ++++---- homeassistant/components/no_ip/__init__.py | 4 ++-- homeassistant/components/noaa_tides/sensor.py | 4 ++-- homeassistant/components/notify/__init__.py | 4 ++-- homeassistant/components/nsw_fuel_station/sensor.py | 2 +- .../components/nx584/alarm_control_panel.py | 2 +- homeassistant/components/nx584/binary_sensor.py | 2 +- homeassistant/components/nzbget/sensor.py | 2 +- homeassistant/components/octoprint/binary_sensor.py | 4 ++-- homeassistant/components/octoprint/sensor.py | 2 +- homeassistant/components/onboarding/views.py | 2 +- homeassistant/components/onvif/camera.py | 2 +- homeassistant/components/opengarage/cover.py | 7 ++----- homeassistant/components/opensky/sensor.py | 4 ++-- homeassistant/components/opentherm_gw/__init__.py | 6 +++--- .../components/opentherm_gw/binary_sensor.py | 2 +- homeassistant/components/opentherm_gw/sensor.py | 4 ++-- homeassistant/components/openuv/__init__.py | 2 +- homeassistant/components/openuv/binary_sensor.py | 2 +- homeassistant/components/openuv/sensor.py | 2 +- homeassistant/components/openweathermap/sensor.py | 2 +- homeassistant/components/owlet/__init__.py | 2 +- homeassistant/components/owntracks/__init__.py | 6 +++--- homeassistant/components/owntracks/messages.py | 4 ++-- homeassistant/components/pandora/media_player.py | 2 +- homeassistant/components/panel_custom/__init__.py | 2 +- homeassistant/components/philips_js/media_player.py | 2 +- homeassistant/components/pi_hole/sensor.py | 2 +- homeassistant/components/pjlink/media_player.py | 4 ++-- homeassistant/components/plaato/__init__.py | 6 +++--- homeassistant/components/plaato/sensor.py | 13 +++++-------- homeassistant/components/plant/__init__.py | 12 ++++++------ homeassistant/components/plex/media_player.py | 6 +++--- homeassistant/components/plex/sensor.py | 6 +++--- homeassistant/components/plum_lightpad/light.py | 2 +- homeassistant/components/point/__init__.py | 6 +++--- .../components/point/alarm_control_panel.py | 2 +- homeassistant/components/prezzibenzina/sensor.py | 2 +- homeassistant/components/proximity/__init__.py | 2 +- homeassistant/components/ps4/__init__.py | 2 +- homeassistant/components/pushetta/notify.py | 4 +--- homeassistant/components/pushsafer/notify.py | 2 +- homeassistant/components/pyload/sensor.py | 2 +- homeassistant/components/qbittorrent/sensor.py | 2 +- homeassistant/components/qnap/sensor.py | 8 ++++---- homeassistant/components/qwikswitch/__init__.py | 2 +- .../components/qwikswitch/binary_sensor.py | 2 +- homeassistant/components/qwikswitch/sensor.py | 4 ++-- 62 files changed, 103 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index d7d824c244c07a..93fe285dcfdb69 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -127,7 +127,7 @@ def __init__(self, hass, robot): """Initialize the Neato Connected Vacuum.""" self.robot = robot self.neato = hass.data[NEATO_LOGIN] - self._name = "{}".format(self.robot.name) + self._name = f"{self.robot.name}" self._status_state = None self._clean_state = None self._state = None diff --git a/homeassistant/components/nello/lock.py b/homeassistant/components/nello/lock.py index 5ae8bb6196855f..3efe0a9cc5fc1b 100644 --- a/homeassistant/components/nello/lock.py +++ b/homeassistant/components/nello/lock.py @@ -59,7 +59,7 @@ def update(self): location_id = self._nello_lock.location_id short_id = self._nello_lock.short_id address = self._nello_lock.address - self._name = "Nello {}".format(short_id) + self._name = f"Nello {short_id}" self._device_attrs = {ATTR_ADDRESS: address, ATTR_LOCATION_ID: location_id} # Process recent activity activity = self._nello_lock.activity diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index b7033bbfd63b6c..cf1ba36aa89320 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -405,7 +405,7 @@ def should_poll(self): @property def unique_id(self): """Return unique id based on device serial and variable.""" - return "{}-{}".format(self.device.serial, self.variable) + return f"{self.device.serial}-{self.variable}" @property def device_info(self): diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index d335acc2bf107c..0f3ae7da710edf 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -143,12 +143,12 @@ def __init__(self, structure, device, zone): """Initialize the sensor.""" super(NestActivityZoneSensor, self).__init__(structure, device, "") self.zone = zone - self._name = "{} {} activity".format(self._name, self.zone.name) + self._name = f"{self._name} {self.zone.name} activity" @property def unique_id(self): """Return unique id based on camera serial and zone id.""" - return "{}-{}".format(self.device.serial, self.zone.zone_id) + return f"{self.device.serial}-{self.zone.zone_id}" @property def device_class(self): diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index c60d09a60028d4..51d826c242fbd0 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -45,5 +45,5 @@ async def resolve_auth_code(hass, client_id, client_secret, code): if err.response.status_code == 401: raise config_flow.CodeInvalid() raise config_flow.NestAuthError( - "Unknown error: {} ({})".format(err, err.response.status_code) + f"Unknown error: {err} ({err.response.status_code})" ) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 2f2f3f9e182d97..591cd790ecf5d7 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -152,7 +152,7 @@ def __init__( self._home = home self._timeout = timeout if home: - self._name = "{} / {}".format(home, camera_name) + self._name = f"{home} / {camera_name}" else: self._name = camera_name if module_name: diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index ec55394105c9ea..d18ff9fc46c800 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -88,11 +88,11 @@ def camera_image(self): try: if self._localurl: response = requests.get( - "{0}/live/snapshot_720.jpg".format(self._localurl), timeout=10 + f"{self._localurl}/live/snapshot_720.jpg", timeout=10 ) elif self._vpnurl: response = requests.get( - "{0}/live/snapshot_720.jpg".format(self._vpnurl), + f"{self._vpnurl}/live/snapshot_720.jpg", timeout=10, verify=self._verify_ssl, ) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 9656d4a37a451c..1465058652dea2 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -154,7 +154,7 @@ def __init__(self, data, room_id): self._state = None self._room_id = room_id self._room_name = self._data.homedata.rooms[self._data.home_id][room_id]["name"] - self._name = "netatmo_{}".format(self._room_name) + self._name = f"netatmo_{self._room_name}" self._current_temperature = None self._target_temperature = None self._preset = None diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index 8fa18a7f19c034..aab901506a8f40 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -112,7 +112,7 @@ def __init__(self, netdata, name, sensor, sensor_name, element, icon, unit, inve @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self._sensor_name) + return f"{self._name} {self._sensor_name}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index e4909ce68fc599..2514b37657fb05 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -350,7 +350,7 @@ class LTEEntity(Entity): @_unique_id.default def _init_unique_id(self): """Register unique_id while we know data is valid.""" - return "{}_{}".format(self.sensor_type, self.modem_data.data.serial_number) + return f"{self.sensor_type}_{self.modem_data.data.serial_number}" async def async_added_to_hass(self): """Register callback.""" @@ -380,4 +380,4 @@ def unique_id(self): @property def name(self): """Return the name of the sensor.""" - return "Netgear LTE {}".format(self.sensor_type) + return f"Netgear LTE {self.sensor_type}" diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 3f9a5e01817b83..661eb75b732663 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -62,9 +62,7 @@ def validate_value(value_name, value, value_list): "Invalid %s tag `%s`. Please use one of the following: %s", value_name, value, - ", ".join( - "{}: {}".format(title, tag) for tag, title in valid_values.items() - ), + ", ".join(f"{title}: {tag}" for tag, title in valid_values.items()), ) return False @@ -126,7 +124,7 @@ def __init__(self, client, agency, route, stop, name=None): self.stop = stop self._custom_name = name # Maybe pull a more user friendly name from the API here - self._name = "{} {}".format(agency, route) + self._name = f"{agency} {route}" self._client = client # set up default state attributes diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index d828ed9d98aad8..36eed0a11db8a1 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -137,7 +137,7 @@ def __init__( is_allowed_path, ): """Initialize the service.""" - self._target = "http://{}:7676".format(remoteip) + self._target = f"http://{remoteip}:7676" self._default_duration = duration self._default_fontsize = fontsize self._default_position = position diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index af93ee0da69952..4cb8495600232a 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -46,7 +46,7 @@ def __init__(self, light, data): """Set up the Niko Home Control light platform.""" self._data = data self._light = light - self._unique_id = "light-{}".format(light.id) + self._unique_id = f"light-{light.id}" self._name = light.name self._state = light.is_on self._brightness = None diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index 246ff10b11711f..8d3d61befd5076 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -147,7 +147,7 @@ class NiluSensor(AirQualityEntity): def __init__(self, api_data: NiluData, name: str, show_on_map: bool): """Initialize the sensor.""" self._api = api_data - self._name = "{} {}".format(name, api_data.data.name) + self._name = f"{name} {api_data.data.name}" self._max_aqi = None self._attrs = {} diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 59aa112622296e..8b2182665f64ef 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -124,7 +124,7 @@ def device_state_attributes(self): departure = get_time_until(self._attrs["time"]) attrs = { - "departure": "In {} minutes".format(departure), + "departure": f"In {departure} minutes", "extra_train": int(self._attrs["isExtra"]) > 0, "vehicle_id": self._attrs["vehicle"], "monitored_station": self._station, @@ -132,7 +132,7 @@ def device_state_attributes(self): } if delay > 0: - attrs["delay"] = "{} minutes".format(delay) + attrs["delay"] = f"{delay} minutes" return attrs @@ -194,7 +194,7 @@ def device_state_attributes(self): departure = get_time_until(self._attrs["departure"]["time"]) attrs = { - "departure": "In {} minutes".format(departure), + "departure": f"In {departure} minutes", "destination": self._station_to, "direction": self._attrs["departure"]["direction"]["name"], "platform_arriving": self._attrs["arrival"]["platform"], @@ -218,7 +218,7 @@ def device_state_attributes(self): ) + get_delay_in_minutes(via["departure"]["delay"]) if delay > 0: - attrs["delay"] = "{} minutes".format(delay) + attrs["delay"] = f"{delay} minutes" return attrs diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 2fa9d45a8b2e41..70ac7099d30727 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -34,7 +34,7 @@ } UPDATE_URL = "https://dynupdate.noip.com/nic/update" -HA_USER_AGENT = "{} {}".format(SERVER_SOFTWARE, EMAIL) +HA_USER_AGENT = f"{SERVER_SOFTWARE} {EMAIL}" CONFIG_SCHEMA = vol.Schema( { @@ -58,7 +58,7 @@ async def async_setup(hass, config): password = config[DOMAIN].get(CONF_PASSWORD) timeout = config[DOMAIN].get(CONF_TIMEOUT) - auth_str = base64.b64encode("{}:{}".format(user, password).encode("utf-8")) + auth_str = base64.b64encode(f"{user}:{password}".encode("utf-8")) session = hass.helpers.aiohttp_client.async_get_clientsession() diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index 2b3d2e42d4d5fe..e5f31dba1568b3 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -101,10 +101,10 @@ def state(self): api_time = self.data.index[0] if self.data["hi_lo"][0] == "H": tidetime = api_time.strftime("%-I:%M %p") - return "High tide at {}".format(tidetime) + return f"High tide at {tidetime}" if self.data["hi_lo"][0] == "L": tidetime = api_time.strftime("%-I:%M %p") - return "Low tide at {}".format(tidetime) + return f"Low tide at {tidetime}" return None def update(self): diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 773c08808c3056..6ede7f18da732b 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -124,7 +124,7 @@ async def async_notify_message(service): p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or p_type ) for name, target in notify_service.targets.items(): - target_name = slugify("{}_{}".format(platform_name, name)) + target_name = slugify(f"{platform_name}_{name}") targets[target_name] = target hass.services.async_register( DOMAIN, @@ -145,7 +145,7 @@ async def async_notify_message(service): schema=NOTIFY_SERVICE_SCHEMA, ) - hass.config.components.add("{}.{}".format(DOMAIN, p_type)) + hass.config.components.add(f"{DOMAIN}.{p_type}") return True diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 4c3258b6effc5e..a84aa554be910c 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -141,7 +141,7 @@ def get_station_name(self) -> str: None, ) - self._station_name = name or "station {}".format(self.station_id) + self._station_name = name or f"station {self.station_id}" return self._station_name diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index abe5f2a126e82b..d3d867ff378678 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = "http://{}:{}".format(host, port) + url = f"http://{host}:{port}" try: add_entities([NX584Alarm(hass, url, name)]) diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index e3af407a53dc1e..8b26a958a6ffa7 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): zone_types = config.get(CONF_ZONE_TYPES) try: - client = nx584_client.Client("http://{}:{}".format(host, port)) + client = nx584_client.Client(f"http://{host}:{port}") zones = client.list_zones() except requests.exceptions.ConnectionError as ex: _LOGGER.error("Unable to connect to NX584: %s", str(ex)) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 50fdf004739a1d..73643a5383cea1 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -65,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config.get(CONF_PASSWORD) monitored_types = config.get(CONF_MONITORED_VARIABLES) - url = "http{}://{}:{}/jsonrpc".format(ssl, host, port) + url = f"http{ssl}://{host}:{port}/jsonrpc" try: nzbgetapi = NZBGetAPI(api_url=url, username=username, password=password) diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index ea457ee19c79a8..7ed1170c6a0f22 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -45,9 +45,9 @@ def __init__( """Initialize a new OctoPrint sensor.""" self.sensor_name = sensor_name if tool is None: - self._name = "{} {}".format(sensor_name, condition) + self._name = f"{sensor_name} {condition}" else: - self._name = "{} {}".format(sensor_name, condition) + self._name = f"{sensor_name} {condition}" self.sensor_type = sensor_type self.api = api self._state = False diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 0233684c320b00..d21aac9ff650bc 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -89,7 +89,7 @@ def __init__( """Initialize a new OctoPrint sensor.""" self.sensor_name = sensor_name if tool is None: - self._name = "{} {}".format(sensor_name, condition) + self._name = f"{sensor_name} {condition}" else: self._name = "{} {} {} {}".format(sensor_name, condition, tool, "temp") self.sensor_type = sensor_type diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index df0f01bcff4d1f..2e79393fe4236d 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -122,7 +122,7 @@ async def post(self, request, data): for area in DEFAULT_AREAS: area_registry.async_create( - translations["component.onboarding.area.{}".format(area)] + translations[f"component.onboarding.area.{area}"] ) await self._async_mark_done(hass) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 6163453b4a527f..0635a2d1f11bb1 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -267,7 +267,7 @@ async def async_obtain_input_uri(self): uri_no_auth = stream_uri.Uri uri_for_log = uri_no_auth.replace("rtsp://", "rtsp://:@", 1) self._input = uri_no_auth.replace( - "rtsp://", "rtsp://{}:{}@".format(self._username, self._password), 1 + "rtsp://", f"rtsp://{self._username}:{self._password}@", 1 ) _LOGGER.debug( diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index ff85d182a22402..1243a9164fd26b 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -122,9 +122,7 @@ def open_cover(self, **kwargs): def update(self): """Get updated status from API.""" try: - status = requests.get( - "{}/jc".format(self.opengarage_url), timeout=10 - ).json() + status = requests.get(f"{self.opengarage_url}/jc", timeout=10).json() except requests.exceptions.RequestException as ex: _LOGGER.error( "Unable to connect to OpenGarage device: %(reason)s", dict(reason=ex) @@ -157,8 +155,7 @@ def _push_button(self): result = -1 try: result = requests.get( - "{}/cc?dkey={}&click=1".format(self.opengarage_url, self._device_key), - timeout=10, + f"{self.opengarage_url}/cc?dkey={self._device_key}&click=1", timeout=10 ).json()["result"] except requests.exceptions.RequestException as ex: _LOGGER.error( diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index a0cfaf5c2bef5d..0c17daa0ab4686 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -36,8 +36,8 @@ DEFAULT_ALTITUDE = 0 -EVENT_OPENSKY_ENTRY = "{}_entry".format(DOMAIN) -EVENT_OPENSKY_EXIT = "{}_exit".format(DOMAIN) +EVENT_OPENSKY_ENTRY = f"{DOMAIN}_entry" +EVENT_OPENSKY_EXIT = f"{DOMAIN}_exit" SCAN_INTERVAL = timedelta(seconds=12) # opensky public limit is 10 seconds OPENSKY_ATTRIBUTION = ( diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index b20d97dadce33a..0c145963653c3e 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -260,7 +260,7 @@ async def set_gpio_mode(call): gpio_id = call.data[ATTR_ID] gpio_mode = call.data[ATTR_MODE] mode = await gw_dev.gateway.set_gpio_mode(gpio_id, gpio_mode) - gpio_var = getattr(gw_vars, "OTGW_GPIO_{}".format(gpio_id)) + gpio_var = getattr(gw_vars, f"OTGW_GPIO_{gpio_id}") gw_dev.status.update({gpio_var: mode}) async_dispatcher_send(hass, gw_dev.update_signal, gw_dev.status) @@ -274,7 +274,7 @@ async def set_led_mode(call): led_id = call.data[ATTR_ID] led_mode = call.data[ATTR_MODE] mode = await gw_dev.gateway.set_led_mode(led_id, led_mode) - led_var = getattr(gw_vars, "OTGW_LED_{}".format(led_id)) + led_var = getattr(gw_vars, f"OTGW_LED_{led_id}") gw_dev.status.update({led_var: mode}) async_dispatcher_send(hass, gw_dev.update_signal, gw_dev.status) @@ -333,7 +333,7 @@ def __init__(self, hass, gw_id, config): self.name = config.get(CONF_NAME, gw_id) self.climate_config = config[CONF_CLIMATE] self.status = {} - self.update_signal = "{}_{}_update".format(DATA_OPENTHERM_GW, gw_id) + self.update_signal = f"{DATA_OPENTHERM_GW}_{gw_id}_update" self.gateway = pyotgw.pyotgw() async def connect_and_subscribe(self, device_path): diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 2f4206b8e09fb8..614829265e2dcc 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -33,7 +33,7 @@ class OpenThermBinarySensor(BinarySensorDevice): def __init__(self, gw_dev, var, device_class, friendly_name_format): """Initialize the binary sensor.""" self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, "{}_{}".format(var, gw_dev.gw_id), hass=gw_dev.hass + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass ) self._gateway = gw_dev self._var = var diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 3727d907c9ad2c..1449caf5defdb9 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -34,7 +34,7 @@ class OpenThermSensor(Entity): def __init__(self, gw_dev, var, device_class, unit, friendly_name_format): """Initialize the OpenTherm Gateway sensor.""" self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, "{}_{}".format(var, gw_dev.gw_id), hass=gw_dev.hass + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass ) self._gateway = gw_dev self._var = var @@ -55,7 +55,7 @@ def receive_report(self, status): """Handle status updates from the component.""" value = status.get(self._var) if isinstance(value, float): - value = "{:2.1f}".format(value) + value = f"{value:2.1f}" self._value = value self.async_schedule_update_ha_state() diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index c1a8873b9e0ce1..62a8c642bc8e4f 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -36,7 +36,7 @@ NOTIFICATION_ID = "openuv_notification" NOTIFICATION_TITLE = "OpenUV Component Setup" -TOPIC_UPDATE = "{0}_data_update".format(DOMAIN) +TOPIC_UPDATE = f"{DOMAIN}_data_update" TYPE_CURRENT_OZONE_LEVEL = "current_ozone_level" TYPE_CURRENT_UV_INDEX = "current_uv_index" diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index d081e09f853194..59f6e4d1c67e47 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -76,7 +76,7 @@ def should_poll(self): @property def unique_id(self) -> str: """Return a unique, HASS-friendly identifier for this entity.""" - return "{0}_{1}_{2}".format(self._latitude, self._longitude, self._sensor_type) + return f"{self._latitude}_{self._longitude}_{self._sensor_type}" async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index e86bfdac35f479..de2688ab121655 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -98,7 +98,7 @@ def state(self): @property def unique_id(self) -> str: """Return a unique, HASS-friendly identifier for this entity.""" - return "{0}_{1}_{2}".format(self._latitude, self._longitude, self._sensor_type) + return f"{self._latitude}_{self._longitude}_{self._sensor_type}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 85bd1ccb2c6eb0..51dc92623f3db1 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -108,7 +108,7 @@ def __init__(self, name, weather_data, sensor_type, temp_unit): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py index 1a49d92d1cfe89..f9543c7fa6e6d8 100644 --- a/homeassistant/components/owlet/__init__.py +++ b/homeassistant/components/owlet/__init__.py @@ -58,7 +58,7 @@ def setup(hass, config): device.update_properties() if not name: - name = "{}'s Owlet".format(device.baby_name) + name = f"{device.baby_name}'s Owlet" hass.data[DOMAIN] = OwletDevice(device, name, SENSOR_TYPES) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index df9ae27b5ffd76..7e65ff3d51dba6 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -169,7 +169,7 @@ async def handle_webhook(hass, webhook_id, request): if user: topic_base = re.sub("/#$", "", context.mqtt_topic) - message["topic"] = "{}/{}/{}".format(topic_base, user, device) + message["topic"] = f"{topic_base}/{user}/{device}" elif message["_type"] != "encrypted": _LOGGER.warning( @@ -264,7 +264,7 @@ def async_see_beacons(self, hass, dev_id, kwargs_param): # Mobile beacons should always be set to the location of the # tracking device. I get the device state and make the necessary # changes to kwargs. - device_tracker_state = hass.states.get("device_tracker.{}".format(dev_id)) + device_tracker_state = hass.states.get(f"device_tracker.{dev_id}") if device_tracker_state is not None: acc = device_tracker_state.attributes.get("gps_accuracy") @@ -282,6 +282,6 @@ def async_see_beacons(self, hass, dev_id, kwargs_param): # kwargs location is the beacon's configured lat/lon kwargs.pop("battery", None) for beacon in self.mobile_beacons_active[dev_id]: - kwargs["dev_id"] = "{}_{}".format(BEACON_DEV_ID, beacon) + kwargs["dev_id"] = f"{BEACON_DEV_ID}_{beacon}" kwargs["host_name"] = beacon self.async_see(**kwargs) diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 61cfb9e05f9b4f..7ef31be13272eb 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -60,7 +60,7 @@ def _parse_see_args(message, subscribe_topic): Async friendly. """ user, device = _parse_topic(message["topic"], subscribe_topic) - dev_id = slugify("{}_{}".format(user, device)) + dev_id = slugify(f"{user}_{device}") kwargs = {"dev_id": dev_id, "host_name": user, "attributes": {}} if message["lat"] is not None and message["lon"] is not None: kwargs["gps"] = (message["lat"], message["lon"]) @@ -253,7 +253,7 @@ async def async_handle_transition_message(hass, context, message): async def async_handle_waypoint(hass, name_base, waypoint): """Handle a waypoint.""" name = waypoint["desc"] - pretty_name = "{} - {}".format(name_base, name) + pretty_name = f"{name_base} - {name}" lat = waypoint["lat"] lon = waypoint["lon"] rad = waypoint["rad"] diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 9ced5fc6cf4cae..c242670ba48b37 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -220,7 +220,7 @@ def select_source(self, source): return _LOGGER.debug("Setting station %s, %d", source, station_index) self._send_station_list_command() - self._pianobar.sendline("{}".format(station_index)) + self._pianobar.sendline(f"{station_index}") self._pianobar.expect("\r\n") self._player_state = STATE_PLAYING diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index d18d00ef841167..cf861992bd6e08 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -165,7 +165,7 @@ async def async_setup(hass, config): panel_path = panel.get(CONF_WEBCOMPONENT_PATH) if panel_path is None: - panel_path = hass.config.path(PANEL_DIR, "{}.html".format(name)) + panel_path = hass.config.path(PANEL_DIR, f"{name}.html") if CONF_JS_URL in panel: kwargs["js_url"] = panel[CONF_JS_URL] diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 10d7fe8009d3b8..579dc253603263 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -286,7 +286,7 @@ def update(self): self._tv.update() self._sources = { - srcid: source["name"] or "Source {}".format(srcid) + srcid: source["name"] or f"Source {srcid}" for srcid, source in (self._tv.sources or {}).items() } diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index d60392373bc719..9c41c20fd637e9 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -117,7 +117,7 @@ def __init__(self, pi_hole, name, condition): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self._condition_name) + return f"{self._name} {self._condition_name}" @property def icon(self): diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 398e77ea5116fc..44b4055e032f92 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.data["pjlink"] = {} hass_data = hass.data["pjlink"] - device_label = "{}:{}".format(host, port) + device_label = f"{host}:{port}" if device_label in hass_data: return @@ -65,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def format_input_source(input_source_name, input_source_number): """Format input source for display in UI.""" - return "{} {}".format(input_source_name, input_source_number) + return f"{input_source_name} {input_source_number}" class PjLinkDevice(MediaPlayerDevice): diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 7ca5de419e055a..49b749b8de6f54 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -37,8 +37,8 @@ ATTR_CO2_VOLUME = "co2_volume" ATTR_BATCH_VOLUME = "batch_volume" -SENSOR_UPDATE = "{}_sensor_update".format(DOMAIN) -SENSOR_DATA_KEY = "{}.{}".format(DOMAIN, SENSOR) +SENSOR_UPDATE = f"{DOMAIN}_sensor_update" +SENSOR_DATA_KEY = f"{DOMAIN}.{SENSOR}" WEBHOOK_SCHEMA = vol.Schema( { @@ -121,7 +121,7 @@ async def handle_webhook(hass, webhook_id, request): async_dispatcher_send(hass, SENSOR_UPDATE, device_id) - return web.Response(text="Saving status for {}".format(device_id), status=HTTP_OK) + return web.Response(text=f"Saving status for {device_id}", status=HTTP_OK) def _device_id(data): diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index bf128af931a4dd..f8e6a3e9fa7b48 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -54,9 +54,7 @@ async def _update_sensor(device_id): async_add_entities(entities, True) else: for entity in devices[device_id]: - async_dispatcher_send( - hass, "{}_{}".format(PLAATO_DOMAIN, entity.unique_id) - ) + async_dispatcher_send(hass, f"{PLAATO_DOMAIN}_{entity.unique_id}") hass.data[SENSOR_DATA_KEY] = async_dispatcher_connect( hass, SENSOR_UPDATE, _update_sensor @@ -73,18 +71,18 @@ def __init__(self, device_id, sensor_type): self._device_id = device_id self._type = sensor_type self._state = 0 - self._name = "{} {}".format(device_id, sensor_type) + self._name = f"{device_id} {sensor_type}" self._attributes = None @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(PLAATO_DOMAIN, self._name) + return f"{PLAATO_DOMAIN} {self._name}" @property def unique_id(self): """Return the unique ID of this sensor.""" - return "{}_{}".format(self._device_id, self._type) + return f"{self._device_id}_{self._type}" @property def device_info(self): @@ -157,6 +155,5 @@ def should_poll(self): async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( - "{}_{}".format(PLAATO_DOMAIN, self.unique_id), - self.async_schedule_update_ha_state, + f"{PLAATO_DOMAIN}_{self.unique_id}", self.async_schedule_update_ha_state ) diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 5dff1d29e705a1..a516e06d55bce4 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -216,7 +216,7 @@ def state_changed(self, entity_id, _, new_state): ) else: raise HomeAssistantError( - "Unknown reading from sensor {}: {}".format(entity_id, value) + f"Unknown reading from sensor {entity_id}: {value}" ) if ATTR_UNIT_OF_MEASUREMENT in new_state.attributes: self._unit_of_measurement[reading] = new_state.attributes.get( @@ -229,10 +229,10 @@ def _update_state(self): result = [] for sensor_name in self._sensormap.values(): params = self.READINGS[sensor_name] - value = getattr(self, "_{}".format(sensor_name)) + value = getattr(self, f"_{sensor_name}") if value is not None: if value == STATE_UNAVAILABLE: - result.append("{} unavailable".format(sensor_name)) + result.append(f"{sensor_name} unavailable") else: if sensor_name == READING_BRIGHTNESS: result.append( @@ -260,14 +260,14 @@ def _check_min(self, sensor_name, value, params): if "min" in params and params["min"] in self._config: min_value = self._config[params["min"]] if value < min_value: - return "{} low".format(sensor_name) + return f"{sensor_name} low" def _check_max(self, sensor_name, value, params): """If configured, check the value against the defined maximum value.""" if "max" in params and params["max"] in self._config: max_value = self._config[params["max"]] if value > max_value: - return "{} high".format(sensor_name) + return f"{sensor_name} high" return None async def async_added_to_hass(self): @@ -352,7 +352,7 @@ def state_attributes(self): } for reading in self._sensormap.values(): - attrib[reading] = getattr(self, "_{}".format(reading)) + attrib[reading] = getattr(self, f"_{reading}") if self._brightness_history.max is not None: attrib[ATTR_MAX_BRIGHTNESS_HISTORY] = self._brightness_history.max diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 98137897149f19..39694a061c4839 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -82,7 +82,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): # Parse discovery data host = discovery_info.get("host") port = discovery_info.get("port") - host = "%s:%s" % (host, port) + host = f"{host}:{port}" _LOGGER.info("Discovered PLEX server: %s", host) if host in _CONFIGURING: @@ -113,7 +113,7 @@ def setup_plexserver( cert_session.verify = False try: plexserver = plexapi.server.PlexServer( - "%s://%s" % (http_prefix, host), token, cert_session + f"{http_prefix}://{host}", token, cert_session ) _LOGGER.info("Discovery configuration done (no token needed)") except ( @@ -847,7 +847,7 @@ def _get_tv_media(self, library_name, show_name, season_number, episode_number): show = self.device.server.library.section(library_name).get(show_name) if not season_number: - playlist_name = "{} - {} Episodes".format(self.entity_id, show_name) + playlist_name = f"{self.entity_id} - {show_name} Episodes" return self.device.server.createPlaylist(playlist_name, show.episodes()) for season in show.seasons(): diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index dbd0d9f8578167..d900b4de87c1d7 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -150,7 +150,7 @@ def update(self): for sess in sessions: user = sess.usernames[0] device = sess.players[0].title - now_playing_user = "{0} - {1}".format(user, device) + now_playing_user = f"{user} - {device}" now_playing_title = "" if sess.TYPE == "episode": @@ -161,7 +161,7 @@ def update(self): season_title += " ({0})".format(sess.show().year) season_episode = "S{0}".format(sess.parentIndex) if sess.index is not None: - season_episode += " · E{0}".format(sess.index) + season_episode += f" · E{sess.index}" episode_title = sess.title now_playing_title = "{0} - {1} - {2}".format( season_title, season_episode, episode_title @@ -181,7 +181,7 @@ def update(self): # "The Incredible Hulk (2008)" now_playing_title = sess.title if sess.year is not None: - now_playing_title += " ({0})".format(sess.year) + now_playing_title += f" ({sess.year})" now_playing.append((now_playing_user, now_playing_title)) self._state = len(sessions) diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index ecf423c500b0df..63fa67f4da54bb 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -94,7 +94,7 @@ class GlowRing(Light): def __init__(self, lightpad): """Initialize the light.""" self._lightpad = lightpad - self._name = "{} Glow Ring".format(lightpad.friendly_name) + self._name = f"{lightpad.friendly_name} Glow Ring" self._state = lightpad.glow_enabled self._brightness = lightpad.glow_intensity * 255.0 diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index f0931bc9e8fc24..e9885891553b9e 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -182,7 +182,7 @@ async def _sync(self): async def new_device(device_id, component): """Load new device.""" - config_entries_key = "{}.{}".format(component, DOMAIN) + config_entries_key = f"{component}.{DOMAIN}" async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]: if config_entries_key not in self._hass.data[CONFIG_ENTRY_IS_SETUP]: await self._hass.config_entries.async_forward_entry_setup( @@ -247,7 +247,7 @@ def __init__(self, point_client, device_id, device_class): def __str__(self): """Return string representation of device.""" - return "MinutPoint {}".format(self.name) + return f"MinutPoint {self.name}" async def async_added_to_hass(self): """Call when entity is added to hass.""" @@ -333,7 +333,7 @@ def should_poll(self): @property def unique_id(self): """Return the unique id of the sensor.""" - return "point.{}-{}".format(self._id, self.device_class) + return f"point.{self._id}-{self.device_class}" @property def value(self): diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index 4a0db111b7d51f..f9e725f6c8e5c7 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -108,7 +108,7 @@ def alarm_arm_away(self, code=None): @property def unique_id(self): """Return the unique id of the sensor.""" - return "point.{}".format(self._home_id) + return f"point.{self._home_id}" @property def device_info(self): diff --git a/homeassistant/components/prezzibenzina/sensor.py b/homeassistant/components/prezzibenzina/sensor.py index 420cd448c1999f..f1f41ba46bad0a 100644 --- a/homeassistant/components/prezzibenzina/sensor.py +++ b/homeassistant/components/prezzibenzina/sensor.py @@ -77,7 +77,7 @@ def __init__(self, index, client, station, name, ft, srv): self._index = index self._data = None self._station = station - self._name = "{} {} {}".format(name, ft, srv) + self._name = f"{name} {ft} {srv}" @property def name(self): diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index e9b85f790848dd..b5856b7f78e921 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -71,7 +71,7 @@ def setup_proximity_component(hass, name, config): zone_id, unit_of_measurement, ) - proximity.entity_id = "{}.{}".format(DOMAIN, proximity_zone) + proximity.entity_id = f"{DOMAIN}.{proximity_zone}" proximity.schedule_update_ha_state() diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 9baf1adbcc2c1b..60635bba525664 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -156,7 +156,7 @@ async def async_migrate_entry(hass, entry): def format_unique_id(creds, mac_address): """Use last 4 Chars of credential as suffix. Unique ID per PSN user.""" suffix = creds[-4:] - return "{}_{}".format(mac_address, suffix) + return f"{mac_address}_{suffix}" def load_games(hass: HomeAssistantType) -> dict: diff --git a/homeassistant/components/pushetta/notify.py b/homeassistant/components/pushetta/notify.py index 2bdd7d036ce71f..b8911039f3f050 100644 --- a/homeassistant/components/pushetta/notify.py +++ b/homeassistant/components/pushetta/notify.py @@ -61,9 +61,7 @@ def send_message(self, message="", **kwargs): title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) try: - self.pushetta.pushMessage( - self._channel_name, "{} {}".format(title, message) - ) + self.pushetta.pushMessage(self._channel_name, f"{title} {message}") except exceptions.TokenValidationError: _LOGGER.error("Please check your access token") self.is_valid = False diff --git a/homeassistant/components/pushsafer/notify.py b/homeassistant/components/pushsafer/notify.py index 461b2540beff45..758a3390286f65 100644 --- a/homeassistant/components/pushsafer/notify.py +++ b/homeassistant/components/pushsafer/notify.py @@ -132,7 +132,7 @@ def get_base64(cls, filebyte, mimetype): return None base64_image = base64.b64encode(filebyte).decode("utf8") - return "data:{};base64,{}".format(mimetype, base64_image) + return f"data:{mimetype};base64,{base64_image}" def load_from_url(self, url=None, username=None, password=None, auth=None): """Load image/document/etc from URL.""" diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 07c23cd9e80b66..8ffe1ece4a2ab1 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -55,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) monitored_types = config.get(CONF_MONITORED_VARIABLES) - url = "http{}://{}:{}/api/".format(ssl, host, port) + url = f"http{ssl}://{host}:{port}/api/" try: pyloadapi = PyLoadAPI(api_url=url, username=username, password=password) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 2900496a01e408..f00b392065cb7b 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -88,7 +88,7 @@ def __init__(self, sensor_type, qbittorrent_client, client_name, exception): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 8ab2ee575bfd01..efbb1ac26ca53a 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -215,8 +215,8 @@ def name(self): server_name = self._api.data["system_stats"]["system"]["name"] if self.monitor_device is not None: - return "{} {} ({})".format(server_name, self.var_name, self.monitor_device) - return "{} {}".format(server_name, self.var_name) + return f"{server_name} {self.var_name} ({self.monitor_device})" + return f"{server_name} {self.var_name}" @property def icon(self): @@ -270,7 +270,7 @@ def device_state_attributes(self): if self._api.data: data = self._api.data["system_stats"]["memory"] size = round_nicely(float(data["total"]) / 1024) - return {ATTR_MEMORY_SIZE: "{} GB".format(size)} + return {ATTR_MEMORY_SIZE: f"{size} GB"} class QNAPNetworkSensor(QNAPSensor): @@ -331,7 +331,7 @@ def device_state_attributes(self): ATTR_NAME: data["system"]["name"], ATTR_MODEL: data["system"]["model"], ATTR_SERIAL: data["system"]["serial_number"], - ATTR_UPTIME: "{:0>2d}d {:0>2d}h {:0>2d}m".format(days, hours, minutes), + ATTR_UPTIME: f"{days:0>2d}d {hours:0>2d}h {minutes:0>2d}m", } diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index 9e4c0658358e05..1ae92b0a18ad05 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -80,7 +80,7 @@ def poll(self): @property def unique_id(self): """Return a unique identifier for this sensor.""" - return "qs{}".format(self.qsid) + return f"qs{self.qsid}" @callback def update_packet(self, packet): diff --git a/homeassistant/components/qwikswitch/binary_sensor.py b/homeassistant/components/qwikswitch/binary_sensor.py index 36e8181cc47392..a5b142e19aede4 100644 --- a/homeassistant/components/qwikswitch/binary_sensor.py +++ b/homeassistant/components/qwikswitch/binary_sensor.py @@ -61,7 +61,7 @@ def is_on(self): @property def unique_id(self): """Return a unique identifier for this sensor.""" - return "qs{}:{}".format(self.qsid, self.channel) + return f"qs{self.qsid}:{self.channel}" @property def device_class(self): diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 8e9a755d6dab12..01964fc7831e04 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -34,7 +34,7 @@ def __init__(self, sensor): self._decode, self.unit = SENSORS[sensor_type] if isinstance(self.unit, type): - self.unit = "{}:{}".format(sensor_type, self.channel) + self.unit = f"{sensor_type}:{self.channel}" @callback def update_packet(self, packet): @@ -60,7 +60,7 @@ def state(self): @property def unique_id(self): """Return a unique identifier for this sensor.""" - return "qs{}:{}".format(self.qsid, self.channel) + return f"qs{self.qsid}:{self.channel}" @property def unit_of_measurement(self): From cde09062c42404c8890a79fdad3c6b65c62e5666 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Sep 2019 21:04:38 +0200 Subject: [PATCH 0132/3953] Update OpenCV 4.1.1 / Numpy 1.17.1 (#26387) --- homeassistant/components/iqvia/manifest.json | 4 ++-- homeassistant/components/opencv/manifest.json | 6 +++--- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 357bfca607aece..7392c931f48339 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/iqvia", "requirements": [ - "numpy==1.17.0", + "numpy==1.17.1", "pyiqvia==0.2.1" ], "dependencies": [], "codeowners": [ "@bachya" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 68f14846af7a1e..e8ebeb102e67b1 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -3,9 +3,9 @@ "name": "Opencv", "documentation": "https://www.home-assistant.io/components/opencv", "requirements": [ - "numpy==1.17.0", - "opencv-python-headless==4.1.0.25" + "numpy==1.17.1", + "opencv-python-headless==4.1.1.26" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index f5bd981bad1dc4..9419cbaaefbede 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": [ "tensorflow==1.13.2", - "numpy==1.17.0", + "numpy==1.17.1", "pillow==6.1.0", "protobuf==3.6.1" ], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index b9c01c15d2004a..8719138f3ac213 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -3,8 +3,8 @@ "name": "Trend", "documentation": "https://www.home-assistant.io/components/trend", "requirements": [ - "numpy==1.17.0" + "numpy==1.17.1" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index c22370e576d970..df3f6962788c67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -866,7 +866,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.0 +numpy==1.17.1 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -884,7 +884,7 @@ onkyo-eiscp==1.2.4 onvif-zeep-async==0.2.0 # homeassistant.components.opencv -# opencv-python-headless==4.1.0.25 +# opencv-python-headless==4.1.1.26 # homeassistant.components.openevse openevsewifi==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 37e45278e33124..2ef8f0ca672595 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -226,7 +226,7 @@ nokia==1.2.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.0 +numpy==1.17.1 # homeassistant.components.google oauth2client==4.0.0 From ef0e9431b6f7ed3b4081ba9c705d29393c08b70c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Sep 2019 21:12:51 +0200 Subject: [PATCH 0133/3953] Use literal string interpolation in integrations T-W (f-strings) (#26394) --- homeassistant/components/tado/climate.py | 2 +- homeassistant/components/tado/sensor.py | 8 ++-- .../components/tapsaff/binary_sensor.py | 2 +- homeassistant/components/ted5000/sensor.py | 2 +- homeassistant/components/teksavvy/sensor.py | 2 +- .../components/telegram_bot/__init__.py | 4 +- .../components/telegram_bot/webhooks.py | 2 +- .../components/tellduslive/__init__.py | 2 +- homeassistant/components/tellstick/sensor.py | 2 +- homeassistant/components/tesla/sensor.py | 4 +- .../components/thermoworks_smoke/sensor.py | 4 +- .../components/thethingsnetwork/sensor.py | 7 +--- homeassistant/components/tibber/sensor.py | 4 +- homeassistant/components/time_date/sensor.py | 8 ++-- .../components/tplink/device_tracker.py | 38 +++++++++---------- homeassistant/components/traccar/__init__.py | 4 +- homeassistant/components/tradfri/light.py | 4 +- homeassistant/components/tradfri/switch.py | 2 +- .../trafikverket_weatherstation/sensor.py | 2 +- .../components/transmission/sensor.py | 2 +- homeassistant/components/travisci/sensor.py | 2 +- .../components/twentemilieu/sensor.py | 10 ++--- homeassistant/components/twilio/__init__.py | 2 +- .../components/ubus/device_tracker.py | 2 +- .../components/uk_transport/sensor.py | 10 ++--- .../components/unifi/device_tracker.py | 2 +- homeassistant/components/unifi/switch.py | 8 ++-- .../components/upc_connect/device_tracker.py | 9 ++--- homeassistant/components/upnp/sensor.py | 8 ++-- homeassistant/components/usps/camera.py | 2 +- homeassistant/components/usps/sensor.py | 4 +- homeassistant/components/vallox/__init__.py | 2 +- homeassistant/components/vallox/sensor.py | 16 ++++---- homeassistant/components/velbus/__init__.py | 2 +- .../components/volumio/media_player.py | 4 +- .../components/volvooncall/__init__.py | 6 +-- homeassistant/components/waqi/sensor.py | 2 +- .../components/watson_iot/__init__.py | 2 +- .../components/waze_travel_time/sensor.py | 2 +- homeassistant/components/wemo/__init__.py | 6 +-- homeassistant/components/wink/__init__.py | 8 ++-- .../components/wink/binary_sensor.py | 2 +- .../components/wirelesstag/__init__.py | 18 ++++----- .../components/wirelesstag/binary_sensor.py | 2 +- .../components/wirelesstag/switch.py | 2 +- homeassistant/components/withings/sensor.py | 2 +- .../components/worldtidesinfo/sensor.py | 4 +- .../components/worxlandroid/sensor.py | 4 +- .../components/wunderground/sensor.py | 10 ++--- homeassistant/components/wwlln/__init__.py | 2 +- 50 files changed, 128 insertions(+), 132 deletions(-) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 15e01db4082ee4..1108b32af4e07f 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -124,7 +124,7 @@ def create_climate_device(tado, hass, zone, name, zone_id): max_temp = float(temperatures["celsius"]["max"]) step = temperatures["celsius"].get("step", PRECISION_TENTHS) - data_id = "zone {} {}".format(name, zone_id) + data_id = f"zone {name} {zone_id}" device = TadoClimate( tado, name, diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 5cfdbd1f30c06c..7b4bd643f3d123 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -80,7 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def create_zone_sensor(tado, zone, name, zone_id, variable): """Create a zone sensor.""" - data_id = "zone {} {}".format(name, zone_id) + data_id = f"zone {name} {zone_id}" tado.add_sensor( data_id, @@ -92,7 +92,7 @@ def create_zone_sensor(tado, zone, name, zone_id, variable): def create_device_sensor(tado, device, name, device_id, variable): """Create a device sensor.""" - data_id = "device {} {}".format(name, device_id) + data_id = f"device {name} {device_id}" tado.add_sensor( data_id, @@ -118,7 +118,7 @@ def __init__(self, store, zone_name, zone_id, zone_variable, data_id): self.zone_id = zone_id self.zone_variable = zone_variable - self._unique_id = "{} {}".format(zone_variable, zone_id) + self._unique_id = f"{zone_variable} {zone_id}" self._data_id = data_id self._state = None @@ -132,7 +132,7 @@ def unique_id(self): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.zone_name, self.zone_variable) + return f"{self.zone_name} {self.zone_variable}" @property def state(self): diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index 3b4bcfa8ea55a6..fe6b01ced4e170 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -45,7 +45,7 @@ def __init__(self, taps_aff_data, name): @property def name(self): """Return the name of the sensor.""" - return "{}".format(self._name) + return f"{self._name}" @property def is_on(self): diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index 922d88d44bf89a..ea0963a092e59a 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) port = config.get(CONF_PORT) name = config.get(CONF_NAME) - url = "http://{}:{}/api/LiveData.xml".format(host, port) + url = f"http://{host}:{port}/api/LiveData.xml" gateway = Ted5000Gateway(url) diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py index 51914d7a4fc49e..74c39a221ba0cf 100644 --- a/homeassistant/components/teksavvy/sensor.py +++ b/homeassistant/components/teksavvy/sensor.py @@ -90,7 +90,7 @@ def __init__(self, teksavvydata, sensor_type, name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index e73c25203e01a9..a36f41edf3b7d4 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -554,7 +554,7 @@ def _send_msg(self, func_send, msg_error, *args_msg, **kwargs_msg): def send_message(self, message="", target=None, **kwargs): """Send a message to one or multiple pre-allowed chat IDs.""" title = kwargs.get(ATTR_TITLE) - text = "{}\n{}".format(title, message) if title else message + text = f"{title}\n{message}" if title else message params = self._get_msg_kwargs(kwargs) for chat_id in self._get_target_chat_ids(target): _LOGGER.debug("Send message in chat ID %s with params: %s", chat_id, params) @@ -590,7 +590,7 @@ def edit_message(self, type_edit, chat_id=None, **kwargs): if type_edit == SERVICE_EDIT_MESSAGE: message = kwargs.get(ATTR_MESSAGE) title = kwargs.get(ATTR_TITLE) - text = "{}\n{}".format(title, message) if title else message + text = f"{title}\n{message}" if title else message _LOGGER.debug( "Editing message with ID %s.", message_id or inline_message_id ) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index 166f48c4961296..c71510eddd90df 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -45,7 +45,7 @@ async def async_setup_platform(hass, config): else: _LOGGER.debug("telegram webhook Status: %s", current_status) - handler_url = "{0}{1}".format(base_url, TELEGRAM_HANDLER_URL) + handler_url = f"{base_url}{TELEGRAM_HANDLER_URL}" if not handler_url.startswith("https"): _LOGGER.error("Invalid telegram webhook %s must be https", handler_url) return False diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 05662cc2b239cd..7234127a15231a 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -46,7 +46,7 @@ CONFIG_ENTRY_IS_SETUP = "telldus_config_entry_is_setup" NEW_CLIENT_TASK = "telldus_new_client_task" -INTERVAL_TRACKER = "{}_INTERVAL".format(DOMAIN) +INTERVAL_TRACKER = f"{DOMAIN}_INTERVAL" async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 24c038f870aaa8..83b56c2cf394d8 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -107,7 +107,7 @@ def __init__(self, name, tellcore_sensor, datatype, sensor_info): self._unit_of_measurement = sensor_info.unit or None self._value = None - self._name = "{} {}".format(name, sensor_info.name) + self._name = f"{name} {sensor_info.name}" @property def name(self): diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index 98cf5e47fd930f..c737b2f0bba576 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -43,13 +43,13 @@ def __init__(self, tesla_device, controller, sensor_type=None): super().__init__(tesla_device, controller) if self.type: - self._name = "{} ({})".format(self.tesla_device.name, self.type) + self._name = f"{self.tesla_device.name} ({self.type})" @property def unique_id(self) -> str: """Return a unique ID.""" if self.type: - return "{}_{}".format(self.tesla_id, self.type) + return f"{self.tesla_id}_{self.type}" return self.tesla_id @property diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 08e6afc3e568b6..70a16287fcc245 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -86,7 +86,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) except HTTPError as error: - msg = "{}".format(error.strerror) + msg = f"{error.strerror}" if "EMAIL_NOT_FOUND" in msg or "INVALID_PASSWORD" in msg: _LOGGER.error("Invalid email and password combination") else: @@ -105,7 +105,7 @@ def __init__(self, sensor_type, serial, mgr): self._state = None self._attributes = {} self._unit_of_measurement = TEMP_FAHRENHEIT - self._unique_id = "{serial}-{type}".format(serial=serial, type=sensor_type) + self._unique_id = f"{serial}-{sensor_type}" self.serial = serial self.mgr = mgr self.update_unit() diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index ccba2bc4b38399..3ba58a688fe179 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -65,7 +65,7 @@ def __init__(self, ttn_data_storage, device_id, value, unit_of_measurement): self._device_id = device_id self._unit_of_measurement = unit_of_measurement self._value = value - self._name = "{} {}".format(self._device_id, self._value) + self._name = f"{self._device_id} {self._value}" @property def name(self): @@ -116,10 +116,7 @@ def __init__(self, hass, app_id, device_id, access_key, values): self._url = TTN_DATA_STORAGE_URL.format( app_id=app_id, endpoint="api/v2/query", device_id=device_id ) - self._headers = { - ACCEPT: CONTENT_TYPE_JSON, - AUTHORIZATION: "key {}".format(access_key), - } + self._headers = {ACCEPT: CONTENT_TYPE_JSON, AUTHORIZATION: f"key {access_key}"} async def async_update(self): """Get the current state from The Things Network Data Storage.""" diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index aba6499ca6f336..3dfe0265bdeef5 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -149,7 +149,7 @@ def __init__(self, tibber_home): self._device_state_attributes = {} self._unit_of_measurement = "W" nickname = tibber_home.info["viewer"]["home"]["appNickname"] - self._name = "Real time consumption {}".format(nickname) + self._name = f"Real time consumption {nickname}" async def async_added_to_hass(self): """Start unavailability tracking.""" @@ -215,4 +215,4 @@ def unique_id(self): """Return a unique ID.""" home = self._tibber_home.info["viewer"]["home"] _id = home["meteringPointData"]["consumptionEan"] - return "{}_rt_consumption".format(_id) + return f"{_id}_rt_consumption" diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index 02cde06d76309f..cbe4c85ace31d1 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -118,15 +118,15 @@ def _update_internal_state(self, time_date): elif self.type == "date": self._state = date elif self.type == "date_time": - self._state = "{}, {}".format(date, time) + self._state = f"{date}, {time}" elif self.type == "time_date": - self._state = "{}, {}".format(time, date) + self._state = f"{time}, {date}" elif self.type == "time_utc": self._state = time_utc elif self.type == "beat": - self._state = "@{0:03d}".format(beat) + self._state = f"@{beat:03d}" elif self.type == "date_time_iso": - self._state = dt_util.parse_datetime("{} {}".format(date, time)).isoformat() + self._state = dt_util.parse_datetime(f"{date} {time}").isoformat() @callback def point_in_time_listener(self, time_date): diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py index 6f1d9761fdf37a..60d495738338e7 100644 --- a/homeassistant/components/tplink/device_tracker.py +++ b/homeassistant/components/tplink/device_tracker.py @@ -168,8 +168,8 @@ def _update_info(self): """ _LOGGER.info("Loading wireless clients...") - url = "http://{}/userRpm/WlanStationRpm.htm".format(self.host) - referer = "http://{}".format(self.host) + url = f"http://{self.host}/userRpm/WlanStationRpm.htm" + referer = f"http://{self.host}" page = requests.get( url, auth=(self.username, self.password), @@ -205,16 +205,16 @@ def _update_info(self): """ _LOGGER.info("Loading wireless clients...") - url = "http://{}/data/map_access_wireless_client_grid.json".format(self.host) - referer = "http://{}".format(self.host) + url = f"http://{self.host}/data/map_access_wireless_client_grid.json" + referer = f"http://{self.host}" # Router uses Authorization cookie instead of header # Let's create the cookie - username_password = "{}:{}".format(self.username, self.password) + username_password = f"{self.username}:{self.password}" b64_encoded_username_password = base64.b64encode( username_password.encode("ascii") ).decode("ascii") - cookie = "Authorization=Basic {}".format(b64_encoded_username_password) + cookie = f"Authorization=Basic {b64_encoded_username_password}" response = requests.post( url, headers={REFERER: referer, COOKIE: cookie}, timeout=4 @@ -264,8 +264,8 @@ def _get_auth_tokens(self): """Retrieve auth tokens from the router.""" _LOGGER.info("Retrieving auth tokens...") - url = "http://{}/cgi-bin/luci/;stok=/login?form=login".format(self.host) - referer = "http://{}/webpages/login.html".format(self.host) + url = f"http://{self.host}/cgi-bin/luci/;stok=/login?form=login" + referer = f"http://{self.host}/webpages/login.html" # If possible implement RSA encryption of password here. response = requests.post( @@ -303,7 +303,7 @@ def _update_info(self): url = ( "http://{}/cgi-bin/luci/;stok={}/admin/wireless?" "form=statistics" ).format(self.host, self.stok) - referer = "http://{}/webpages/index.html".format(self.host) + referer = f"http://{self.host}/webpages/index.html" response = requests.post( url, @@ -346,7 +346,7 @@ def _log_out(self): url = ("http://{}/cgi-bin/luci/;stok={}/admin/system?" "form=logout").format( self.host, self.stok ) - referer = "http://{}/webpages/index.html".format(self.host) + referer = f"http://{self.host}/webpages/index.html" requests.post( url, @@ -379,19 +379,19 @@ def get_device_name(self, device): def _get_auth_tokens(self): """Retrieve auth tokens from the router.""" _LOGGER.info("Retrieving auth tokens...") - url = "http://{}/userRpm/LoginRpm.htm?Save=Save".format(self.host) + url = f"http://{self.host}/userRpm/LoginRpm.htm?Save=Save" # Generate md5 hash of password. The C7 appears to use the first 15 # characters of the password only, so we truncate to remove additional # characters from being hashed. password = hashlib.md5(self.password.encode("utf")[:15]).hexdigest() - credentials = "{}:{}".format(self.username, password).encode("utf") + credentials = f"{self.username}:{password}".encode("utf") # Encode the credentials to be sent as a cookie. self.credentials = base64.b64encode(credentials).decode("utf") # Create the authorization cookie. - cookie = "Authorization=Basic {}".format(self.credentials) + cookie = f"Authorization=Basic {self.credentials}" response = requests.get(url, headers={COOKIE: cookie}) @@ -423,9 +423,9 @@ def _update_info(self): # Check both the 2.4GHz and 5GHz client list URLs for clients_url in ("WlanStationRpm.htm", "WlanStationRpm_5g.htm"): - url = "http://{}/{}/userRpm/{}".format(self.host, self.token, clients_url) - referer = "http://{}".format(self.host) - cookie = "Authorization=Basic {}".format(self.credentials) + url = f"http://{self.host}/{self.token}/userRpm/{clients_url}" + referer = f"http://{self.host}" + cookie = f"Authorization=Basic {self.credentials}" page = requests.get(url, headers={COOKIE: cookie, REFERER: referer}) mac_results.extend(self.parse_macs.findall(page.text)) @@ -456,7 +456,7 @@ def _update_info(self): """ _LOGGER.info("Loading wireless clients...") - base_url = "http://{}".format(self.host) + base_url = f"http://{self.host}" header = { USER_AGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;" @@ -466,7 +466,7 @@ def _update_info(self): ACCEPT_ENCODING: "gzip, deflate", CONTENT_TYPE: "application/x-www-form-urlencoded; charset=UTF-8", HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest", - REFERER: "http://{}/".format(self.host), + REFERER: f"http://{self.host}/", CONNECTION: KEEP_ALIVE, PRAGMA: HTTP_HEADER_NO_CACHE, CACHE_CONTROL: HTTP_HEADER_NO_CACHE, @@ -484,7 +484,7 @@ def _update_info(self): # A timestamp is required to be sent as get parameter timestamp = int(datetime.now().timestamp() * 1e3) - client_list_url = "{}/data/monitor.client.client.json".format(base_url) + client_list_url = f"{base_url}/data/monitor.client.client.json" get_params = {"operation": "load", "_": timestamp} diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 8e3f90fb66f666..5eb87de0db28f5 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) -TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" DEFAULT_ACCURACY = 200 @@ -83,7 +83,7 @@ async def handle_webhook(hass, webhook_id, request): attrs, ) - return web.Response(text="Setting location for {}".format(device), status=HTTP_OK) + return web.Response(text=f"Setting location for {device}", status=HTTP_OK) async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 7992bf459db78f..97fdfd9d36d885 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -57,7 +57,7 @@ class TradfriGroup(Light): def __init__(self, group, api, gateway_id): """Initialize a Group.""" self._api = api - self._unique_id = "group-{}-{}".format(gateway_id, group.id) + self._unique_id = f"group-{gateway_id}-{group.id}" self._group = group self._name = group.name @@ -152,7 +152,7 @@ class TradfriLight(Light): def __init__(self, light, api, gateway_id): """Initialize a Light.""" self._api = api - self._unique_id = "light-{}-{}".format(gateway_id, light.id) + self._unique_id = f"light-{gateway_id}-{light.id}" self._light = None self._light_control = None self._light_data = None diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 2b1bb0d5c548de..4be72eb7359e64 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -34,7 +34,7 @@ class TradfriSwitch(SwitchDevice): def __init__(self, switch, api, gateway_id): """Initialize a switch.""" self._api = api - self._unique_id = "{}-{}".format(gateway_id, switch.id) + self._unique_id = f"{gateway_id}-{switch.id}" self._switch = None self._socket_control = None self._switch_data = None diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 9c79aa5cda6054..cb80e8d441bfc4 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -147,7 +147,7 @@ def __init__(self, weather_api, name, sensor_type, sensor_station): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._client, self._name) + return f"{self._client} {self._name}" @property def icon(self): diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 9e5397dd9fbaee..ac2e64ce92f390 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -63,7 +63,7 @@ def __init__( @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 546fe7606e7569..b86b62fc1e9518 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -84,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for repo in repositories: if "/" not in repo: - repo = "{0}/{1}".format(user.login, repo) + repo = f"{user.login}/{repo}" for sensor_type in config.get(CONF_MONITORED_CONDITIONS): sensors.append(TravisCISensor(travis, repo, user, branch, sensor_type)) diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index 9dc109c98ded7e..b1be9a071e4b4a 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -40,28 +40,28 @@ async def async_setup_entry( TwenteMilieuSensor( twentemilieu, unique_id=entry.data[CONF_ID], - name="{} Waste Pickup".format(WASTE_TYPE_NON_RECYCLABLE), + name=f"{WASTE_TYPE_NON_RECYCLABLE} Waste Pickup", waste_type=WASTE_TYPE_NON_RECYCLABLE, icon="mdi:delete-empty", ), TwenteMilieuSensor( twentemilieu, unique_id=entry.data[CONF_ID], - name="{} Waste Pickup".format(WASTE_TYPE_ORGANIC), + name=f"{WASTE_TYPE_ORGANIC} Waste Pickup", waste_type=WASTE_TYPE_ORGANIC, icon="mdi:delete-empty", ), TwenteMilieuSensor( twentemilieu, unique_id=entry.data[CONF_ID], - name="{} Waste Pickup".format(WASTE_TYPE_PAPER), + name=f"{WASTE_TYPE_PAPER} Waste Pickup", waste_type=WASTE_TYPE_PAPER, icon="mdi:delete-empty", ), TwenteMilieuSensor( twentemilieu, unique_id=entry.data[CONF_ID], - name="{} Waste Pickup".format(WASTE_TYPE_PLASTIC), + name=f"{WASTE_TYPE_PLASTIC} Waste Pickup", waste_type=WASTE_TYPE_PLASTIC, icon="mdi:delete-empty", ), @@ -110,7 +110,7 @@ def available(self) -> bool: @property def unique_id(self) -> str: """Return the unique ID for this sensor.""" - return "{}_{}_{}".format(DOMAIN, self._unique_id, self._waste_type) + return f"{DOMAIN}_{self._unique_id}_{self._waste_type}" @property def should_poll(self) -> bool: diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index 74264a31f0631c..ea5629e7cab6d9 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -11,7 +11,7 @@ DATA_TWILIO = DOMAIN -RECEIVED_DATA = "{}_data_received".format(DOMAIN) +RECEIVED_DATA = f"{DOMAIN}_data_received" CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index 53cd900c0d499b..f14ea5af02cd43 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -80,7 +80,7 @@ def __init__(self, config): self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") self.last_results = {} - self.url = "http://{}/ubus".format(host) + self.url = f"http://{host}/ubus" self.session_id = _get_session_id(self.url, self.username, self.password) self.hostapd = [] diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 38183d23a0ebc7..8e6e46531e2f41 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -157,10 +157,10 @@ def __init__(self, api_app_id, api_app_key, stop_atcocode, bus_direction, interv self._stop_atcocode = stop_atcocode self._bus_direction = bus_direction self._next_buses = [] - self._destination_re = re.compile("{}".format(bus_direction), re.IGNORECASE) + self._destination_re = re.compile(f"{bus_direction}", re.IGNORECASE) - sensor_name = "Next bus to {}".format(bus_direction) - stop_url = "bus/stop/{}/live.json".format(stop_atcocode) + sensor_name = f"Next bus to {bus_direction}" + stop_url = f"bus/stop/{stop_atcocode}/live.json" UkTransportSensor.__init__(self, sensor_name, api_app_id, api_app_key, stop_url) self.update = Throttle(interval)(self._update) @@ -220,8 +220,8 @@ def __init__(self, api_app_id, api_app_key, station_code, calling_at, interval): self._calling_at = calling_at self._next_trains = [] - sensor_name = "Next train to {}".format(calling_at) - query_url = "train/station/{}/live.json".format(station_code) + sensor_name = f"Next train to {calling_at}" + query_url = f"train/station/{station_code}/live.json" UkTransportSensor.__init__( self, sensor_name, api_app_id, api_app_key, query_url diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 1f4aef754adc0b..ca6ddb6820660c 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -238,7 +238,7 @@ def name(self) -> str: @property def unique_id(self) -> str: """Return a unique identifier for this client.""" - return "{}-{}".format(self.client.mac, self.controller.site) + return f"{self.client.mac}-{self.controller.site}" @property def available(self) -> bool: diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index f46a55f671ea4f..4f757102d530e8 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -69,7 +69,7 @@ def update_items(controller, async_add_entities, switches, switches_off): # block client for client_id in controller.option_block_clients: - block_client_id = "block-{}".format(client_id) + block_client_id = f"block-{client_id}" if block_client_id in switches: LOGGER.debug( @@ -91,7 +91,7 @@ def update_items(controller, async_add_entities, switches, switches_off): # control poe for client_id in controller.api.clients: - poe_client_id = "poe-{}".format(client_id) + poe_client_id = f"poe-{client_id}" if poe_client_id in switches: LOGGER.debug( @@ -194,7 +194,7 @@ async def async_added_to_hass(self): @property def unique_id(self): """Return a unique identifier for this switch.""" - return "poe-{}".format(self.client.mac) + return f"poe-{self.client.mac}" @property def is_on(self): @@ -255,7 +255,7 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): @property def unique_id(self): """Return a unique identifier for this switch.""" - return "block-{}".format(self.client.mac) + return f"block-{self.client.mac}" @property def is_on(self): diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index 3355c33ab2a182..384b82d139564c 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -48,7 +48,7 @@ def __init__(self, hass, config): self.headers = { HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest", - REFERER: "http://{}/index.html".format(self.host), + REFERER: f"http://{self.host}/index.html", USER_AGENT: ( "Mozilla/5.0 (Windows NT 10.0; WOW64) " "AppleWebKit/537.36 (KHTML, like Gecko) " @@ -88,8 +88,7 @@ async def async_initialize_token(self): # get first token with async_timeout.timeout(10): response = await self.websession.get( - "http://{}/common_page/login.html".format(self.host), - headers=self.headers, + f"http://{self.host}/common_page/login.html", headers=self.headers ) await response.text() @@ -109,8 +108,8 @@ async def _async_ws_function(self, function): # The 'token' parameter has to be first, and 'fun' second # or the UPC firmware will return an error response = await self.websession.post( - "http://{}/xml/getter.xml".format(self.host), - data="token={}&fun={}".format(self.token, function), + f"http://{self.host}/xml/getter.xml", + data=f"token={self.token}&fun={function}", headers=self.headers, allow_redirects=False, ) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 33ffa4d478af5e..e5746e088f866b 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -118,7 +118,7 @@ def name(self) -> str: @property def unique_id(self) -> str: """Return an unique ID.""" - return "{}_{}".format(self._device.udn, self._type_name) + return f"{self._device.udn}_{self._type_name}" @property def state(self) -> str: @@ -172,12 +172,12 @@ def _async_fetch_value(self): @property def unique_id(self) -> str: """Return an unique ID.""" - return "{}_{}/sec_{}".format(self._device.udn, self.unit, self._direction) + return f"{self._device.udn}_{self.unit}/sec_{self._direction}" @property def name(self) -> str: """Return the name of the sensor.""" - return "{} {}/sec {}".format(self._device.name, self.unit, self._direction) + return f"{self._device.name} {self.unit}/sec {self._direction}" @property def icon(self) -> str: @@ -187,7 +187,7 @@ def icon(self) -> str: @property def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" - return "{}/sec".format(self.unit) + return f"{self.unit}/sec" def _is_overflowed(self, new_value) -> bool: """Check if value has overflowed.""" diff --git a/homeassistant/components/usps/camera.py b/homeassistant/components/usps/camera.py index 78af9c4feab0e2..3141314b049cb5 100644 --- a/homeassistant/components/usps/camera.py +++ b/homeassistant/components/usps/camera.py @@ -65,7 +65,7 @@ def camera_image(self): @property def name(self): """Return the name of this camera.""" - return "{} mail".format(self._name) + return f"{self._name} mail" @property def model(self): diff --git a/homeassistant/components/usps/sensor.py b/homeassistant/components/usps/sensor.py index a8aa6f6cc6f304..7e26e6c9e5c793 100644 --- a/homeassistant/components/usps/sensor.py +++ b/homeassistant/components/usps/sensor.py @@ -36,7 +36,7 @@ def __init__(self, usps): @property def name(self): """Return the name of the sensor.""" - return "{} packages".format(self._name) + return f"{self._name} packages" @property def state(self): @@ -85,7 +85,7 @@ def __init__(self, usps): @property def name(self): """Return the name of the sensor.""" - return "{} mail".format(self._name) + return f"{self._name} mail" @property def state(self): diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 40ae0a8348251d..c107e4f8894551 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -152,7 +152,7 @@ def fetch_metric(self, metric_key): raise OSError("Device state out of sync.") if metric_key not in vlxDevConstants.__dict__: - raise KeyError("Unknown metric key: {}".format(metric_key)) + raise KeyError(f"Unknown metric key: {metric_key}") return self._metric_cache[metric_key] diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 705ccd3103daf8..f7be502cecb18c 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -28,14 +28,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sensors = [ ValloxProfileSensor( - name="{} Current Profile".format(name), + name=f"{name} Current Profile", state_proxy=state_proxy, device_class=None, unit_of_measurement=None, icon="mdi:gauge", ), ValloxFanSpeedSensor( - name="{} Fan Speed".format(name), + name=f"{name} Fan Speed", state_proxy=state_proxy, metric_key="A_CYC_FAN_SPEED", device_class=None, @@ -43,7 +43,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon="mdi:fan", ), ValloxSensor( - name="{} Extract Air".format(name), + name=f"{name} Extract Air", state_proxy=state_proxy, metric_key="A_CYC_TEMP_EXTRACT_AIR", device_class=DEVICE_CLASS_TEMPERATURE, @@ -51,7 +51,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon=None, ), ValloxSensor( - name="{} Exhaust Air".format(name), + name=f"{name} Exhaust Air", state_proxy=state_proxy, metric_key="A_CYC_TEMP_EXHAUST_AIR", device_class=DEVICE_CLASS_TEMPERATURE, @@ -59,7 +59,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon=None, ), ValloxSensor( - name="{} Outdoor Air".format(name), + name=f"{name} Outdoor Air", state_proxy=state_proxy, metric_key="A_CYC_TEMP_OUTDOOR_AIR", device_class=DEVICE_CLASS_TEMPERATURE, @@ -67,7 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon=None, ), ValloxSensor( - name="{} Supply Air".format(name), + name=f"{name} Supply Air", state_proxy=state_proxy, metric_key="A_CYC_TEMP_SUPPLY_AIR", device_class=DEVICE_CLASS_TEMPERATURE, @@ -75,7 +75,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon=None, ), ValloxSensor( - name="{} Humidity".format(name), + name=f"{name} Humidity", state_proxy=state_proxy, metric_key="A_CYC_RH_VALUE", device_class=DEVICE_CLASS_HUMIDITY, @@ -83,7 +83,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon=None, ), ValloxFilterRemainingSensor( - name="{} Remaining Time For Filter".format(name), + name=f"{name} Remaining Time For Filter", state_proxy=state_proxy, metric_key="A_CYC_REMAINING_TIME_FOR_FILTER", device_class=DEVICE_CLASS_TIMESTAMP, diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 76018dcf54844d..9946f06446f059 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -119,7 +119,7 @@ def unique_id(self): serial = self._module.get_module_address() else: serial = self._module.serial - return "{}-{}".format(serial, self._channel) + return f"{serial}-{self._channel}" @property def name(self): diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 96e1d883646361..8bd1952a6507d0 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -125,7 +125,7 @@ def __init__(self, name, host, port, hass): async def send_volumio_msg(self, method, params=None): """Send message.""" - url = "http://{}:{}/api/v1/{}/".format(self.host, self.port, method) + url = f"http://{self.host}:{self.port}/api/v1/{method}/" _LOGGER.debug("URL: %s params: %s", url, params) @@ -202,7 +202,7 @@ def media_image_url(self): if str(url[0:2]).lower() == "ht": mediaurl = url else: - mediaurl = "http://{}:{}{}".format(self.host, self.port, url) + mediaurl = f"http://{self.host}:{self.port}{url}" return mediaurl @property diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index b007628dbd85b1..c41c72020c42aa 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -36,7 +36,7 @@ CONF_SCANDINAVIAN_MILES = "scandinavian_miles" CONF_MUTABLE = "mutable" -SIGNAL_STATE_UPDATED = "{}.updated".format(DOMAIN) +SIGNAL_STATE_UPDATED = f"{DOMAIN}.updated" COMPONENTS = { "sensor": "sensor", @@ -261,7 +261,7 @@ def _vehicle_name(self): @property def name(self): """Return full name of the entity.""" - return "{} {}".format(self._vehicle_name, self._entity_name) + return f"{self._vehicle_name} {self._entity_name}" @property def should_poll(self): @@ -278,5 +278,5 @@ def device_state_attributes(self): """Return device specific state attributes.""" return dict( self.instrument.attributes, - model="{}/{}".format(self.vehicle.vehicle_type, self.vehicle.model_year), + model=f"{self.vehicle.vehicle_type}/{self.vehicle.model_year}", ) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index a2b9e69e002460..dbfe6de1a60b46 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -113,7 +113,7 @@ def __init__(self, client, station): def name(self): """Return the name of the sensor.""" if self.station_name: - return "WAQI {}".format(self.station_name) + return f"WAQI {self.station_name}" return "WAQI {}".format(self.url if self.url else self.uid) @property diff --git a/homeassistant/components/watson_iot/__init__.py b/homeassistant/components/watson_iot/__init__.py index 54c11506b29a8e..aef2cc8ccce274 100644 --- a/homeassistant/components/watson_iot/__init__.py +++ b/homeassistant/components/watson_iot/__init__.py @@ -126,7 +126,7 @@ def event_to_json(event): if key != "unit_of_measurement": # If the key is already in fields if key in out_event["fields"]: - key = "{}_".format(key) + key = f"{key}_" # For each value we try to cast it as float # But if we can not do it we store the value # as string diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 3fc44f90d4268e..340c0adbc9705e 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -175,7 +175,7 @@ def _get_location_from_entity(self, entity_id): return _get_location_from_attributes(state) # Check if device is inside a zone. - zone_state = self.hass.states.get("zone.{}".format(state.state)) + zone_state = self.hass.states.get(f"zone.{state.state}") if location.has_location(zone_state): _LOGGER.debug( "%s is in %s, getting zone location", entity_id, zone_state.entity_id diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 3cdc5afd4a0063..9e479991d15548 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -108,7 +108,7 @@ def stop_wemo(event): def setup_url_for_device(device): """Determine setup.xml url for given device.""" - return "http://{}:{}/setup.xml".format(device.host, device.port) + return f"http://{device.host}:{device.port}/setup.xml" def setup_url_for_address(host, port): """Determine setup.xml url for given host and port pair.""" @@ -118,7 +118,7 @@ def setup_url_for_address(host, port): if not port: return None - return "http://{}:{}/setup.xml".format(host, port) + return f"http://{host}:{port}/setup.xml" def discovery_dispatch(service, discovery_info): """Dispatcher for incoming WeMo discovery events.""" @@ -150,7 +150,7 @@ def discover_wemo_devices(now): if not url: _LOGGER.error( "Unable to get description url for WeMo at: %s", - "{}:{}".format(host, port) if port else host, + f"{host}:{port}" if port else host, ) continue diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 710adfd734d294..5af784359d83db 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -52,7 +52,7 @@ WINK_AUTH_CALLBACK_PATH = "/auth/wink/callback" WINK_AUTH_START = "/auth/wink" WINK_CONFIG_FILE = ".wink.conf" -USER_AGENT = "Manufacturer/Home-Assistant{} python/3 Wink/3".format(__version__) +USER_AGENT = f"Manufacturer/Home-Assistant{__version__} python/3 Wink/3" DEFAULT_CONFIG = {"client_id": "CLIENT_ID_HERE", "client_secret": "CLIENT_SECRET_HERE"} @@ -228,7 +228,7 @@ def wink_configuration_callback(callback_data): _configurator = hass.data[DOMAIN]["configuring"][DOMAIN] configurator.notify_errors(_configurator, error_msg) - start_url = "{}{}".format(hass.config.api.base_url, WINK_AUTH_CALLBACK_PATH) + start_url = f"{hass.config.api.base_url}{WINK_AUTH_CALLBACK_PATH}" description = """Please create a Wink developer app at https://developer.wink.com. @@ -268,9 +268,9 @@ def wink_configuration_callback(callback_data): """Call setup again.""" setup(hass, config) - start_url = "{}{}".format(hass.config.api.base_url, WINK_AUTH_START) + start_url = f"{hass.config.api.base_url}{WINK_AUTH_START}" - description = "Please authorize Wink by visiting {}".format(start_url) + description = f"Please authorize Wink by visiting {start_url}" hass.data[DOMAIN]["configuring"][DOMAIN] = configurator.request_config( DOMAIN, wink_configuration_callback, description=description diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index ad1800b4223c9a..e82a767fde8315 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -140,7 +140,7 @@ def device_state_attributes(self): # The service call to set the Kidde code # takes a string of 1s and 0s so it makes # sense to display it to the user that way - _formatted_kidde_code = "{:b}".format(_kidde_code).zfill(8) + _formatted_kidde_code = f"{_kidde_code:b}".zfill(8) _attributes["kidde_radio_code"] = _formatted_kidde_code return _attributes diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 331b88894def67..5e0da881076f65 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -74,14 +74,14 @@ def load_tags(self): def arm(self, switch): """Arm entity sensor monitoring.""" - func_name = "arm_{}".format(switch.sensor_type) + func_name = f"arm_{switch.sensor_type}" arm_func = getattr(self.api, func_name) if arm_func is not None: arm_func(switch.tag_id, switch.tag_manager_mac) def disarm(self, switch): """Disarm entity sensor monitoring.""" - func_name = "disarm_{}".format(switch.sensor_type) + func_name = f"disarm_{switch.sensor_type}" disarm_func = getattr(self.api, func_name) if disarm_func is not None: disarm_func(switch.tag_id, switch.tag_manager_mac) @@ -132,18 +132,18 @@ def local_base_url(self): port = self.hass.config.api.port if port is not None: - self._local_base_url += ":{}".format(port) + self._local_base_url += f":{port}" return self._local_base_url @property def update_callback_url(self): """Return url for local push notifications(update event).""" - return "{}/api/events/wirelesstag_update_tags".format(self.local_base_url) + return f"{self.local_base_url}/api/events/wirelesstag_update_tags" @property def binary_event_callback_url(self): """Return url for local push notifications(binary event).""" - return "{}/api/events/wirelesstag_binary_event".format(self.local_base_url) + return f"{self.local_base_url}/api/events/wirelesstag_binary_event" def handle_update_tags_event(self, event): """Handle push event from wireless tag manager.""" @@ -254,7 +254,7 @@ def updated_state_value(self): # pylint: disable=no-self-use def decorate_value(self, value): """Decorate input value to be well presented for end user.""" - return "{:.1f}".format(value) + return f"{value:.1f}" @property def available(self): @@ -280,8 +280,8 @@ def device_state_attributes(self): """Return the state attributes.""" return { ATTR_BATTERY_LEVEL: int(self._tag.battery_remaining * 100), - ATTR_VOLTAGE: "{:.2f}V".format(self._tag.battery_volts), - ATTR_TAG_SIGNAL_STRENGTH: "{}dBm".format(self._tag.signal_strength), + ATTR_VOLTAGE: f"{self._tag.battery_volts:.2f}V", + ATTR_TAG_SIGNAL_STRENGTH: f"{self._tag.signal_strength}dBm", ATTR_TAG_OUT_OF_RANGE: not self._tag.is_in_range, - ATTR_TAG_POWER_CONSUMPTION: "{:.2f}%".format(self._tag.power_consumption), + ATTR_TAG_POWER_CONSUMPTION: f"{self._tag.power_consumption:.2f}%", } diff --git a/homeassistant/components/wirelesstag/binary_sensor.py b/homeassistant/components/wirelesstag/binary_sensor.py index 72b68a8762ff65..4fcebe73478c78 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -95,7 +95,7 @@ def __init__(self, api, tag, sensor_type): """Initialize a binary sensor for a Wireless Sensor Tags.""" super().__init__(api, tag) self._sensor_type = sensor_type - self._name = "{0} {1}".format(self._tag.name, self.event.human_readable_name) + self._name = f"{self._tag.name} {self.event.human_readable_name}" async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/wirelesstag/switch.py b/homeassistant/components/wirelesstag/switch.py index 37f97e3a1e695a..1bc806d9e32390 100644 --- a/homeassistant/components/wirelesstag/switch.py +++ b/homeassistant/components/wirelesstag/switch.py @@ -79,5 +79,5 @@ def updated_state_value(self): @property def principal_value(self): """Provide actual value of switch.""" - attr_name = "is_{}_sensor_armed".format(self.sensor_type) + attr_name = f"is_{self.sensor_type}_sensor_armed" return getattr(self._tag, attr_name, False) diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 3328808295f36e..67cf966c1bcd4a 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -325,7 +325,7 @@ def __init__( @property def name(self) -> str: """Return the name of the sensor.""" - return "Withings {} {}".format(self._attribute.measurement, self._slug) + return f"Withings {self._attribute.measurement} {self._slug}" @property def unique_id(self) -> str: diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index bd20431d70630c..aaa9f2d1585919 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -96,12 +96,12 @@ def state(self): tidetime = time.strftime( "%I:%M %p", time.localtime(self.data["extremes"][0]["dt"]) ) - return "High tide at {}".format(tidetime) + return f"High tide at {tidetime}" if "Low" in str(self.data["extremes"][0]["type"]): tidetime = time.strftime( "%I:%M %p", time.localtime(self.data["extremes"][0]["dt"]) ) - return "Low tide at {}".format(tidetime) + return f"Low tide at {tidetime}" return None return None diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 69e2813e7d1c16..4e9bf0a6a4a913 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -63,12 +63,12 @@ def __init__(self, sensor, config): self.pin = config.get(CONF_PIN) self.timeout = config.get(CONF_TIMEOUT) self.allow_unreachable = config.get(CONF_ALLOW_UNREACHABLE) - self.url = "http://{}/jsondata.cgi".format(self.host) + self.url = f"http://{self.host}/jsondata.cgi" @property def name(self): """Return the name of the sensor.""" - return "worxlandroid-{}".format(self.sensor) + return f"worxlandroid-{self.sensor}" @property def state(self): diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 21f87d9ce0b510..5272b33ccb5673 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -968,7 +968,7 @@ async def async_setup_platform( ) if pws_id is None: - unique_id_base = "@{:06f},{:06f}".format(longitude, latitude) + unique_id_base = f"@{longitude:06f},{latitude:06f}" else: # Manually specified weather station, use that for unique_id unique_id_base = pws_id @@ -999,7 +999,7 @@ def __init__(self, hass: HomeAssistantType, rest, condition, unique_id_base: str # This is only the suggested entity id, it might get changed by # the entity registry later. self.entity_id = sensor.ENTITY_ID_FORMAT.format("pws_" + condition) - self._unique_id = "{},{}".format(unique_id_base, condition) + self._unique_id = f"{unique_id_base},{condition}" self._device_class = self._cfg_expand("device_class") def _cfg_expand(self, what, default=None): @@ -1106,7 +1106,7 @@ def __init__(self, hass, api_key, pws_id, lang, latitude, longitude): self._hass = hass self._api_key = api_key self._pws_id = pws_id - self._lang = "lang:{}".format(lang) + self._lang = f"lang:{lang}" self._latitude = latitude self._longitude = longitude self._features = set() @@ -1122,9 +1122,9 @@ def _build_url(self, baseurl=_RESOURCE): self._api_key, "/".join(sorted(self._features)), self._lang ) if self._pws_id: - url = url + "pws:{}".format(self._pws_id) + url = url + f"pws:{self._pws_id}" else: - url = url + "{},{}".format(self._latitude, self._longitude) + url = url + f"{self._latitude},{self._longitude}" return url + ".json" diff --git a/homeassistant/components/wwlln/__init__.py b/homeassistant/components/wwlln/__init__.py index ca3711490e7fad..412efc904dbf52 100644 --- a/homeassistant/components/wwlln/__init__.py +++ b/homeassistant/components/wwlln/__init__.py @@ -47,7 +47,7 @@ async def async_setup(hass, config): latitude = conf.get(CONF_LATITUDE, hass.config.latitude) longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) - identifier = "{0}, {1}".format(latitude, longitude) + identifier = f"{latitude}, {longitude}" if identifier in configured_instances(hass): return True From 7203027cbf699877ccbe180cc66147036a5857fd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Sep 2019 21:14:00 +0200 Subject: [PATCH 0134/3953] Use literal string interpolation in integrations K-M (f-strings) (#26389) --- homeassistant/components/kankun/switch.py | 8 +++---- homeassistant/components/kira/__init__.py | 2 +- homeassistant/components/kodi/media_player.py | 12 +++++----- homeassistant/components/kodi/notify.py | 2 +- .../components/konnected/__init__.py | 2 +- homeassistant/components/kwb/sensor.py | 2 +- homeassistant/components/lastfm/sensor.py | 6 ++--- homeassistant/components/lcn/helpers.py | 2 +- .../components/life360/config_flow.py | 2 +- .../components/life360/device_tracker.py | 10 ++++----- homeassistant/components/lifx/light.py | 2 +- homeassistant/components/lifx_cloud/scene.py | 2 +- homeassistant/components/light/__init__.py | 10 ++++----- .../linksys_smart/device_tracker.py | 2 +- .../components/linux_battery/sensor.py | 2 +- .../components/liveboxplaytv/media_player.py | 2 +- homeassistant/components/locative/__init__.py | 10 ++++----- homeassistant/components/logbook/__init__.py | 14 ++++++------ .../components/logentries/__init__.py | 2 +- .../components/logi_circle/__init__.py | 2 +- .../components/logi_circle/config_flow.py | 2 +- .../components/logi_circle/sensor.py | 2 +- .../components/luftdaten/__init__.py | 2 +- homeassistant/components/lutron/__init__.py | 4 ++-- homeassistant/components/lyft/sensor.py | 2 +- .../components/magicseaweed/sensor.py | 6 ++--- homeassistant/components/mailgun/__init__.py | 4 ++-- homeassistant/components/marytts/tts.py | 2 +- homeassistant/components/metoffice/weather.py | 2 +- homeassistant/components/microsoft/tts.py | 4 ++-- .../components/microsoft_face/__init__.py | 22 ++++++++----------- .../microsoft_face_detect/image_processing.py | 2 +- homeassistant/components/miflora/sensor.py | 2 +- homeassistant/components/minio/__init__.py | 2 +- homeassistant/components/mitemp_bt/sensor.py | 2 +- .../components/mobile_app/binary_sensor.py | 2 +- homeassistant/components/mobile_app/entity.py | 2 +- homeassistant/components/mobile_app/sensor.py | 2 +- .../components/mobile_app/webhook.py | 8 +++---- homeassistant/components/mochad/light.py | 8 +++---- homeassistant/components/modbus/climate.py | 2 +- homeassistant/components/modbus/sensor.py | 2 +- homeassistant/components/mopar/__init__.py | 2 +- .../components/mpchc/media_player.py | 8 +++---- homeassistant/components/mpd/media_player.py | 2 +- homeassistant/components/mqtt/discovery.py | 6 ++--- .../components/mqtt/vacuum/schema_legacy.py | 8 +++---- .../components/mysensors/__init__.py | 4 ++-- homeassistant/components/mysensors/gateway.py | 2 +- homeassistant/components/mysensors/notify.py | 2 +- .../components/mystrom/binary_sensor.py | 9 +++----- 51 files changed, 104 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py index 5d41b2360e93e1..63f289862f636e 100644 --- a/homeassistant/components/kankun/switch.py +++ b/homeassistant/components/kankun/switch.py @@ -66,7 +66,7 @@ def __init__(self, hass, name, host, port, path, user, passwd): self._hass = hass self._name = name self._state = False - self._url = "http://{}:{}{}".format(host, port, path) + self._url = f"http://{host}:{port}{path}" if user is not None: self._auth = (user, passwd) else: @@ -78,7 +78,7 @@ def _switch(self, newstate): try: req = requests.get( - "{}?set={}".format(self._url, newstate), auth=self._auth, timeout=5 + f"{self._url}?set={newstate}", auth=self._auth, timeout=5 ) return req.json()["ok"] except requests.RequestException: @@ -89,9 +89,7 @@ def _query_state(self): _LOGGER.info("Querying state from: %s", self._url) try: - req = requests.get( - "{}?get=state".format(self._url), auth=self._auth, timeout=5 - ) + req = requests.get(f"{self._url}?get=state", auth=self._auth, timeout=5) return req.json()["state"] == "on" except requests.RequestException: _LOGGER.error("State query failed") diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py index 24eb8b06aef620..77f91a50dfad04 100644 --- a/homeassistant/components/kira/__init__.py +++ b/homeassistant/components/kira/__init__.py @@ -32,7 +32,7 @@ CONF_SENSOR = "sensor" CONF_REMOTE = "remote" -CODES_YAML = "{}_codes.yaml".format(DOMAIN) +CODES_YAML = f"{DOMAIN}_codes.yaml" CODE_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 14ef0292eccfc6..9f0aab6c00c7d8 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -140,7 +140,7 @@ def _check_deprecated_turn_off(hass, turn_off_action): method = DEPRECATED_TURN_OFF_ACTIONS[turn_off_action] new_config = OrderedDict( [ - ("service", "{}.{}".format(DOMAIN, SERVICE_CALL_METHOD)), + ("service", f"{DOMAIN}.{SERVICE_CALL_METHOD}"), ( "data_template", OrderedDict([("entity_id", "{{ entity_id }}"), ("method", method)]), @@ -281,18 +281,18 @@ def __init__( if username is not None: kwargs["auth"] = aiohttp.BasicAuth(username, password) - image_auth_string = "{}:{}@".format(username, password) + image_auth_string = f"{username}:{password}@" else: image_auth_string = "" http_protocol = "https" if encryption else "http" ws_protocol = "wss" if encryption else "ws" - self._http_url = "{}://{}:{}/jsonrpc".format(http_protocol, host, port) + self._http_url = f"{http_protocol}://{host}:{port}/jsonrpc" self._image_url = "{}://{}{}:{}/image".format( http_protocol, image_auth_string, host, port ) - self._ws_url = "{}://{}:{}/jsonrpc".format(ws_protocol, host, tcp_port) + self._ws_url = f"{ws_protocol}://{host}:{tcp_port}/jsonrpc" self._http_server = jsonrpc_async.Server(self._http_url, **kwargs) if websocket: @@ -326,14 +326,14 @@ def on_hass_stop(event): turn_on_action = script.Script( self.hass, turn_on_action, - "{} turn ON script".format(self.name), + f"{self.name} turn ON script", self.async_update_ha_state(True), ) if turn_off_action is not None: turn_off_action = script.Script( self.hass, _check_deprecated_turn_off(hass, turn_off_action), - "{} turn OFF script".format(self.name), + f"{self.name} turn OFF script", ) self._turn_on_action = turn_on_action self._turn_off_action = turn_off_action diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index 70c8669019cd85..41dfc42b5deda5 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -62,7 +62,7 @@ async def async_get_service(hass, config, discovery_info=None): ) http_protocol = "https" if encryption else "http" - url = "{}://{}:{}/jsonrpc".format(http_protocol, host, port) + url = f"{http_protocol}://{host}:{port}/jsonrpc" if username is not None: auth = aiohttp.BasicAuth(username, password) diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index d98f44bd3d9f4d..4cc872fb78b2a2 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -538,7 +538,7 @@ async def put(self, request: Request, device_id) -> Response: ) auth = request.headers.get(AUTHORIZATION, None) - if not hmac.compare_digest("Bearer {}".format(self.auth_token), auth): + if not hmac.compare_digest(f"Bearer {self.auth_token}", auth): return self.json_message("unauthorized", status_code=HTTP_UNAUTHORIZED) pin_num = int(pin_num) device = data[CONF_DEVICES].get(device_id) diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index 911884d2a9e3ad..49815faf7aed44 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -92,7 +92,7 @@ def __init__(self, easyfire, sensor, client_name): @property def name(self): """Return the name.""" - return "{} {}".format(self._client_name, self._name) + return f"{self._client_name} {self._name}" @property def available(self) -> bool: diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index ce92cffee53d39..736792aefd8123 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -72,7 +72,7 @@ def name(self): @property def entity_id(self): """Return the entity ID.""" - return "sensor.lastfm_{}".format(self._name) + return f"sensor.lastfm_{self._name}" @property def state(self): @@ -84,7 +84,7 @@ def update(self): self._cover = self._user.get_image() self._playcount = self._user.get_playcount() last = self._user.get_recent_tracks(limit=2)[0] - self._lastplayed = "{} - {}".format(last.track.artist, last.track.title) + self._lastplayed = f"{last.track.artist} - {last.track.title}" top = self._user.get_top_tracks(limit=1)[0] toptitle = re.search("', '(.+?)',", str(top)) topartist = re.search("'(.+?)',", str(top)) @@ -93,7 +93,7 @@ def update(self): self._state = "Not Scrobbling" return now = self._user.get_now_playing() - self._state = "{} - {}".format(now.artist, now.title) + self._state = f"{now.artist} - {now.title}" @property def device_state_attributes(self): diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 92d515127269c7..236035b0400eb7 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -38,7 +38,7 @@ def has_unique_connection_names(connections): if suffix == 0: connection[CONF_NAME] = DEFAULT_NAME else: - connection[CONF_NAME] = "{}{:d}".format(DEFAULT_NAME, suffix) + connection[CONF_NAME] = f"{DEFAULT_NAME}{suffix:d}" schema = vol.Schema(vol.Unique()) schema([connection.get(CONF_NAME) for connection in connections]) diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index be84d276422109..6af999a575d5c8 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -99,7 +99,7 @@ async def async_step_import(self, user_input): ) return self.async_abort(reason="unexpected") return self.async_create_entry( - title="{} (from configuration)".format(username), + title=f"{username} (from configuration)", data={ CONF_USERNAME: username, CONF_PASSWORD: password, diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index dc5645a5216770..ddd562ebfac8e9 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -159,7 +159,7 @@ def _err(self, key, err_msg): _errs = self._errs.get(key, 0) if _errs < self._max_errs: self._errs[key] = _errs = _errs + 1 - msg = "{}: {}".format(key, err_msg) + msg = f"{key}: {err_msg}" if _errs >= self._error_threshold: if _errs == self._max_errs: msg = "Suppressing further errors until OK: " + msg @@ -233,14 +233,12 @@ def _update_member(self, member, dev_id): convert(float(gps_accuracy), LENGTH_FEET, LENGTH_METERS) ) except (TypeError, ValueError): - self._err( - dev_id, "GPS data invalid: {}, {}, {}".format(lat, lon, gps_accuracy) - ) + self._err(dev_id, f"GPS data invalid: {lat}, {lon}, {gps_accuracy}") return self._ok(dev_id) - msg = "Updating {}".format(dev_id) + msg = f"Updating {dev_id}" if prev_seen: msg += "; Time since last update: {}".format(last_seen - prev_seen) _LOGGER.debug(msg) @@ -401,7 +399,7 @@ def _update_life360(self, now=None): except (Life360Error, KeyError): pass if incl_circle: - err_key = 'get_circle_members "{}"'.format(circle_name) + err_key = f'get_circle_members "{circle_name}"' try: members = api.get_circle_members(circle_id) except Life360Error as exc: diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index b3ec9ed288ff16..ed26db3d49e535 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -509,7 +509,7 @@ def name(self): @property def who(self): """Return a string identifying the bulb.""" - return "%s (%s)" % (self.bulb.ip_addr, self.name) + return f"{self.bulb.ip_addr} ({self.name})" @property def min_mireds(self): diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index 6fc9fac126762a..ac4e0201fb87f5 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -31,7 +31,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= token = config.get(CONF_TOKEN) timeout = config.get(CONF_TIMEOUT) - headers = {AUTHORIZATION: "Bearer {}".format(token)} + headers = {AUTHORIZATION: f"Bearer {token}"} url = LIFX_API_URL.format("scenes") diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 94ba43b85458d9..ed61d961d881df 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -237,16 +237,16 @@ async def async_handle(self, intent_obj): response = intent_obj.create_response() if not speech_parts: # No attributes changed - speech = "Turned on {}".format(state.name) + speech = f"Turned on {state.name}" else: - parts = ["Changed {} to".format(state.name)] + parts = [f"Changed {state.name} to"] for index, part in enumerate(speech_parts): if index == 0: - parts.append(" {}".format(part)) + parts.append(f" {part}") elif index != len(speech_parts) - 1: - parts.append(", {}".format(part)) + parts.append(f", {part}") else: - parts.append(" and {}".format(part)) + parts.append(f" and {part}") speech = "".join(parts) response.async_set_speech(speech) diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index 9877f6ed09174e..1af84a4c4ab216 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -100,7 +100,7 @@ def _make_request(self): ] headers = {"X-JNAP-Action": "http://linksys.com/jnap/core/Transaction"} return requests.post( - "http://{}/JNAP/".format(self.host), + f"http://{self.host}/JNAP/", timeout=DEFAULT_TIMEOUT, headers=headers, json=data, diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index 9eb7090e957f74..9256c3ad18dc47 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if system == "android": os.listdir(os.path.join(DEFAULT_PATH, "battery")) else: - os.listdir(os.path.join(DEFAULT_PATH, "BAT{}".format(battery_id))) + os.listdir(os.path.join(DEFAULT_PATH, f"BAT{battery_id}")) except FileNotFoundError: _LOGGER.error("No battery found") return False diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py index 3c3c8bb9b38ce0..98418d6be81895 100644 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ b/homeassistant/components/liveboxplaytv/media_player.py @@ -178,7 +178,7 @@ def media_title(self): """Title of current playing media.""" if self._current_channel: if self._current_program: - return "{}: {}".format(self._current_channel, self._current_program) + return f"{self._current_channel}: {self._current_program}" return self._current_channel @property diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index e1df5b980a9e39..61e0b1f7474949 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "locative" -TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" ATTR_DEVICE_ID = "device" @@ -76,12 +76,10 @@ async def handle_webhook(hass, webhook_id, request): if direction == "enter": async_dispatcher_send(hass, TRACKER_UPDATE, device, gps_location, location_name) - return web.Response( - text="Setting location to {}".format(location_name), status=HTTP_OK - ) + return web.Response(text=f"Setting location to {location_name}", status=HTTP_OK) if direction == "exit": - current_state = hass.states.get("{}.{}".format(DEVICE_TRACKER, device)) + current_state = hass.states.get(f"{DEVICE_TRACKER}.{device}") if current_state is None or current_state.state == location_name: location_name = STATE_NOT_HOME @@ -108,7 +106,7 @@ async def handle_webhook(hass, webhook_id, request): _LOGGER.error("Received unidentified message from Locative: %s", direction) return web.Response( - text="Received unidentified message: {}".format(direction), + text=f"Received unidentified message: {direction}", status=HTTP_UNPROCESSABLE_ENTITY, ) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 0383e73105b714..3c5e828765c98d 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -188,7 +188,7 @@ def humanify(hass, events): - if 2+ sensor updates in GROUP_BY_MINUTES, show last - if home assistant stop and start happen in same minute call it restarted """ - domain_prefixes = tuple("{}.".format(dom) for dom in CONTINUOUS_DOMAINS) + domain_prefixes = tuple(f"{dom}." for dom in CONTINUOUS_DOMAINS) # Group events in batches of GROUP_BY_MINUTES for _, g_events in groupby( @@ -332,7 +332,7 @@ def humanify(hass, events): entity_id = data.get(ATTR_ENTITY_ID) value = data.get(ATTR_VALUE) - value_msg = " to {}".format(value) if value else "" + value_msg = f" to {value}" if value else "" message = "send command {}{} for {}".format( data[ATTR_SERVICE], value_msg, data[ATTR_DISPLAY_NAME] ) @@ -519,7 +519,7 @@ def _keep_event(event, entities_filter): domain = DOMAIN_HOMEKIT if not entity_id and domain: - entity_id = "%s." % (domain,) + entity_id = f"{domain}." return not entity_id or entities_filter(entity_id) @@ -530,7 +530,7 @@ def _entry_message_from_state(domain, state): if domain in ["device_tracker", "person"]: if state.state == STATE_NOT_HOME: return "is away" - return "is at {}".format(state.state) + return f"is at {state.state}" if domain == "sun": if state.state == sun.STATE_ABOVE_HORIZON: @@ -596,9 +596,9 @@ def _entry_message_from_state(domain, state): "vibration", ]: if state.state == STATE_ON: - return "detected {}".format(device_class) + return f"detected {device_class}" if state.state == STATE_OFF: - return "cleared (no {} detected)".format(device_class) + return f"cleared (no {device_class} detected)" if state.state == STATE_ON: # Future: combine groups and its entity entries ? @@ -607,4 +607,4 @@ def _entry_message_from_state(domain, state): if state.state == STATE_OFF: return "turned off" - return "changed to {}".format(state.state) + return f"changed to {state.state}" diff --git a/homeassistant/components/logentries/__init__.py b/homeassistant/components/logentries/__init__.py index ba92fb8a6726b7..3601ee275b852b 100644 --- a/homeassistant/components/logentries/__init__.py +++ b/homeassistant/components/logentries/__init__.py @@ -24,7 +24,7 @@ def setup(hass, config): """Set up the Logentries component.""" conf = config[DOMAIN] token = conf.get(CONF_TOKEN) - le_wh = "{}{}".format(DEFAULT_HOST, token) + le_wh = f"{DEFAULT_HOST}{token}" def logentries_event_listener(event): """Listen for new messages on the bus and sends them to Logentries.""" diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 6f073a064f16f6..12484a655d635f 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -156,7 +156,7 @@ async def async_setup_entry(hass, entry): except asyncio.TimeoutError: # The TimeoutError exception object returns nothing when casted to a # string, so we'll handle it separately. - err = "{}s timeout exceeded when connecting to Logi Circle API".format(_TIMEOUT) + err = f"{_TIMEOUT}s timeout exceeded when connecting to Logi Circle API" hass.components.persistent_notification.create( "Error: {}
" "You will need to restart hass after fixing." diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index f81db9ba17139d..2a25c5f00a49fd 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -179,7 +179,7 @@ async def _async_create_session(self, code): account_id = (await logi_session.account)["accountId"] await logi_session.close() return self.async_create_entry( - title="Logi Circle ({})".format(account_id), + title=f"Logi Circle ({account_id})", data={ CONF_CLIENT_ID: client_id, CONF_CLIENT_SECRET: client_secret, diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index f229250ea09e0c..fc5ad7155b44ca 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -49,7 +49,7 @@ def __init__(self, camera, time_zone, sensor_type): """Initialize a sensor for Logi Circle camera.""" self._sensor_type = sensor_type self._camera = camera - self._id = "{}-{}".format(self._camera.mac_address, self._sensor_type) + self._id = f"{self._camera.mac_address}-{self._sensor_type}" self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[2]) self._name = "{0} {1}".format( self._camera.name, SENSOR_TYPES.get(self._sensor_type)[0] diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index e05967a91fd958..86129eafc0295e 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -34,7 +34,7 @@ SENSOR_PRESSURE = "pressure" SENSOR_TEMPERATURE = "temperature" -TOPIC_UPDATE = "{0}_data_update".format(DOMAIN) +TOPIC_UPDATE = f"{DOMAIN}_data_update" VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3" diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 9f4d81df044d47..de3ca40fd1d9c3 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -114,7 +114,7 @@ def _update_callback(self, _device, _context, _event, _params): @property def name(self): """Return the name of the device.""" - return "{} {}".format(self._area_name, self._lutron_device.name) + return f"{self._area_name} {self._lutron_device.name}" @property def should_poll(self): @@ -132,7 +132,7 @@ class LutronButton: def __init__(self, hass, keypad, button): """Register callback for activity on the button.""" - name = "{}: {}".format(keypad.name, button.name) + name = f"{keypad.name}: {button.name}" self._hass = hass self._has_release_event = ( button.button_type is not None and "RaiseLower" in button.button_type diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py index 0bf739b040c32c..339b996c5d847b 100644 --- a/homeassistant/components/lyft/sensor.py +++ b/homeassistant/components/lyft/sensor.py @@ -85,7 +85,7 @@ def __init__(self, sensorType, products, product_id, product): self._sensortype = sensorType self._name = "{} {}".format(self._product["display_name"], self._sensortype) if "lyft" not in self._name.lower(): - self._name = "Lyft{}".format(self._name) + self._name = f"Lyft{self._name}" if self._sensortype == "time": self._unit_of_measurement = "min" elif self._sensortype == "price": diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 61abb9788c53d5..66ab87a6569ffa 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -108,10 +108,10 @@ def __init__(self, forecast_data, sensor_type, name, unit_system, hour=None): def name(self): """Return the name of the sensor.""" if self.hour is None and "forecast" in self.type: - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" if self.hour is None: - return "Current {} {}".format(self.client_name, self._name) - return "{} {} {}".format(self.hour, self.client_name, self._name) + return f"Current {self.client_name} {self._name}" + return f"{self.hour} {self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py index 87bbe6bee076c7..4bcca0848f43f9 100644 --- a/homeassistant/components/mailgun/__init__.py +++ b/homeassistant/components/mailgun/__init__.py @@ -19,7 +19,7 @@ DEFAULT_SANDBOX = False -MESSAGE_RECEIVED = "{}_message_received".format(DOMAIN) +MESSAGE_RECEIVED = f"{DOMAIN}_message_received" CONFIG_SCHEMA = vol.Schema( { @@ -75,7 +75,7 @@ async def verify_webhook(hass, token=None, timestamp=None, signature=None): hmac_digest = hmac.new( key=bytes(hass.data[DOMAIN][CONF_API_KEY], "utf-8"), - msg=bytes("{}{}".format(timestamp, token), "utf-8"), + msg=bytes(f"{timestamp}{token}", "utf-8"), digestmod=hashlib.sha256, ).hexdigest() diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index 9f64c088b4a0b8..e5088c5b2df3c3 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -74,7 +74,7 @@ async def async_get_tts_audio(self, message, language, options=None): try: with async_timeout.timeout(10): - url = "http://{}:{}/process?".format(self._host, self._port) + url = f"http://{self._host}:{self._port}/process?" audio = self._codec.upper() if audio == "WAV": diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index e90d6fdc4c2e82..bb7a64005ce287 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -83,7 +83,7 @@ def update(self): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self.site.name) + return f"{self._name} {self.site.name}" @property def condition(self): diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index ff06e8815ed1d7..3536c788bb9d4e 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -115,8 +115,8 @@ def __init__(self, apikey, lang, gender, ttype, rate, volume, pitch, contour): self._gender = gender self._type = ttype self._output = DEFAULT_OUTPUT - self._rate = "{}%".format(rate) - self._volume = "{}%".format(volume) + self._rate = f"{rate}%" + self._volume = f"{volume}%" self._pitch = pitch self._contour = contour self.name = "Microsoft" diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 6c2fc5ae6bf2ad..5d0c50e536a554 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -92,7 +92,7 @@ async def async_create_group(service): g_id = slugify(name) try: - await face.call_api("put", "persongroups/{0}".format(g_id), {"name": name}) + await face.call_api("put", f"persongroups/{g_id}", {"name": name}) face.store[g_id] = {} entities[g_id] = MicrosoftFaceGroupEntity(hass, face, g_id, name) @@ -109,7 +109,7 @@ async def async_delete_group(service): g_id = slugify(service.data[ATTR_NAME]) try: - await face.call_api("delete", "persongroups/{0}".format(g_id)) + await face.call_api("delete", f"persongroups/{g_id}") face.store.pop(g_id) entity = entities.pop(g_id) @@ -126,7 +126,7 @@ async def async_train_group(service): g_id = service.data[ATTR_GROUP] try: - await face.call_api("post", "persongroups/{0}/train".format(g_id)) + await face.call_api("post", f"persongroups/{g_id}/train") except HomeAssistantError as err: _LOGGER.error("Can't train group '%s' with error: %s", g_id, err) @@ -141,7 +141,7 @@ async def async_create_person(service): try: user_data = await face.call_api( - "post", "persongroups/{0}/persons".format(g_id), {"name": name} + "post", f"persongroups/{g_id}/persons", {"name": name} ) face.store[g_id][name] = user_data["personId"] @@ -160,9 +160,7 @@ async def async_delete_person(service): p_id = face.store[g_id].get(name) try: - await face.call_api( - "delete", "persongroups/{0}/persons/{1}".format(g_id, p_id) - ) + await face.call_api("delete", f"persongroups/{g_id}/persons/{p_id}") face.store[g_id].pop(name) await entities[g_id].async_update_ha_state() @@ -186,7 +184,7 @@ async def async_face_person(service): await face.call_api( "post", - "persongroups/{0}/persons/{1}/persistedFaces".format(g_id, p_id), + f"persongroups/{g_id}/persons/{p_id}/persistedFaces", image.content, binary=True, ) @@ -218,7 +216,7 @@ def name(self): @property def entity_id(self): """Return entity id.""" - return "{0}.{1}".format(DOMAIN, self._id) + return f"{DOMAIN}.{self._id}" @property def state(self): @@ -249,7 +247,7 @@ def __init__(self, hass, server_loc, api_key, timeout, entities): self.websession = async_get_clientsession(hass) self.timeout = timeout self._api_key = api_key - self._server_url = "https://{0}.{1}".format(server_loc, FACE_API_URL) + self._server_url = f"https://{server_loc}.{FACE_API_URL}" self._store = {} self._entities = entities @@ -270,9 +268,7 @@ async def update_store(self): self.hass, self, g_id, group["name"] ) - persons = await self.call_api( - "get", "persongroups/{0}/persons".format(g_id) - ) + persons = await self.call_api("get", f"persongroups/{g_id}/persons") for person in persons: self._store[g_id][person["name"]] = person["personId"] diff --git a/homeassistant/components/microsoft_face_detect/image_processing.py b/homeassistant/components/microsoft_face_detect/image_processing.py index 243b4533938191..c10f7edf9dbe3d 100644 --- a/homeassistant/components/microsoft_face_detect/image_processing.py +++ b/homeassistant/components/microsoft_face_detect/image_processing.py @@ -30,7 +30,7 @@ def validate_attributes(list_attributes): """Validate face attributes.""" for attr in list_attributes: if attr not in SUPPORTED_ATTRIBUTES: - raise vol.Invalid("Invalid attribute {0}".format(attr)) + raise vol.Invalid(f"Invalid attribute {attr}") return list_attributes diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 999dc473c604a7..86f1462e2cca55 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -85,7 +85,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= prefix = config.get(CONF_NAME) if prefix: - name = "{} {}".format(prefix, name) + name = f"{prefix} {name}" devs.append( MiFloraSensor(poller, parameter, name, unit, icon, force_update, median) diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index cede3a7aad53c2..d411d913082f89 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -166,7 +166,7 @@ def remove_file(service): def get_minio_endpoint(host: str, port: int) -> str: """Create minio endpoint from host and port.""" - return "{}:{}".format(host, port) + return f"{host}:{port}" class QueueListener(threading.Thread): diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index c9b7837c683710..9cd1f1cebc29be 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -94,7 +94,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): prefix = config.get(CONF_NAME) if prefix: - name = "{} {}".format(prefix, name) + name = f"{prefix} {name}" devs.append( MiTempBtSensor(poller, parameter, device, name, unit, force_update, median) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index c0ea297edc1b1a..975c4c16c32cd9 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -53,7 +53,7 @@ def handle_sensor_registration(webhook_id, data): async_dispatcher_connect( hass, - "{}_{}_register".format(DOMAIN, ENTITY_TYPE), + f"{DOMAIN}_{ENTITY_TYPE}_register", partial(handle_sensor_registration, webhook_id), ) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index c207475dc3c842..27cb9934b18ef8 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -21,7 +21,7 @@ def sensor_id(webhook_id, unique_id): """Return a unique sensor ID.""" - return "{}_{}".format(webhook_id, unique_id) + return f"{webhook_id}_{unique_id}" class MobileAppEntity(Entity): diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index d6d2247736c3cc..b96a6f1e2f0496 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -53,7 +53,7 @@ def handle_sensor_registration(webhook_id, data): async_dispatcher_connect( hass, - "{}_{}_register".format(DOMAIN, ENTITY_TYPE), + f"{DOMAIN}_{ENTITY_TYPE}_register", partial(handle_sensor_registration, webhook_id), ) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index ecbd08375e0c1e..f95d5b993f083a 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -220,13 +220,13 @@ async def handle_webhook( unique_id = data[ATTR_SENSOR_UNIQUE_ID] - unique_store_key = "{}_{}".format(webhook_id, unique_id) + unique_store_key = f"{webhook_id}_{unique_id}" if unique_store_key in hass.data[DOMAIN][entity_type]: _LOGGER.error("Refusing to re-register existing sensor %s!", unique_id) return error_response( ERR_SENSOR_DUPLICATE_UNIQUE_ID, - "{} {} already exists!".format(entity_type, unique_id), + f"{entity_type} {unique_id} already exists!", status=409, ) @@ -257,13 +257,13 @@ async def handle_webhook( unique_id = sensor[ATTR_SENSOR_UNIQUE_ID] - unique_store_key = "{}_{}".format(webhook_id, unique_id) + unique_store_key = f"{webhook_id}_{unique_id}" if unique_store_key not in hass.data[DOMAIN][entity_type]: _LOGGER.error( "Refusing to update non-registered sensor: %s", unique_store_key ) - err_msg = "{} {} is not registered".format(entity_type, unique_id) + err_msg = f"{entity_type} {unique_id} is not registered" resp[unique_id] = { "success": False, "error": {"code": ERR_SENSOR_NOT_REGISTERED, "message": err_msg}, diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index a7b0b8c2d0faa2..899908c34bdfc4 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -50,7 +50,7 @@ def __init__(self, hass, ctrl, dev): self._controller = ctrl self._address = dev[CONF_ADDRESS] - self._name = dev.get(CONF_NAME, "x10_light_dev_{}".format(self._address)) + self._name = dev.get(CONF_NAME, f"x10_light_dev_{self._address}") self._comm_type = dev.get(mochad.CONF_COMM_TYPE, "pl") self.light = device.Device(ctrl, self._address, comm_type=self._comm_type) self._brightness = 0 @@ -95,12 +95,12 @@ def _adjust_brightness(self, brightness): if self._brightness > brightness: bdelta = self._brightness - brightness mochad_brightness = self._calculate_brightness_value(bdelta) - self.light.send_cmd("dim {}".format(mochad_brightness)) + self.light.send_cmd(f"dim {mochad_brightness}") self._controller.read_data() elif self._brightness < brightness: bdelta = brightness - self._brightness mochad_brightness = self._calculate_brightness_value(bdelta) - self.light.send_cmd("bright {}".format(mochad_brightness)) + self.light.send_cmd(f"bright {mochad_brightness}") self._controller.read_data() def turn_on(self, **kwargs): @@ -109,7 +109,7 @@ def turn_on(self, **kwargs): with mochad.REQ_LOCK: if self._brightness_levels > 32: out_brightness = self._calculate_brightness_value(brightness) - self.light.send_cmd("xdim {}".format(out_brightness)) + self.light.send_cmd(f"xdim {out_brightness}") self._controller.read_data() else: self.light.send_cmd("on") diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 22b871cea20f65..64b45b03c95994 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -170,7 +170,7 @@ def read_register(self, register): [x.to_bytes(2, byteorder="big") for x in result.registers] ) val = struct.unpack(self._structure, byte_string)[0] - register_value = format(val, ".{}f".format(self._precision)) + register_value = format(val, f".{self._precision}f") return register_value def write_register(self, register, value): diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 4fc9fb808c6c53..1a5c71812d610b 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -54,7 +54,7 @@ def number(value: Any) -> Union[int, float]: value = float(value) return value except (TypeError, ValueError): - raise vol.Invalid("invalid number {}".format(value)) + raise vol.Invalid(f"invalid number {value}") PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/mopar/__init__.py b/homeassistant/components/mopar/__init__.py index 686b927b515f7d..857dbab2a3b08a 100644 --- a/homeassistant/components/mopar/__init__.py +++ b/homeassistant/components/mopar/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.event import track_time_interval DOMAIN = "mopar" -DATA_UPDATED = "{}_data_updated".format(DOMAIN) +DATA_UPDATED = f"{DOMAIN}_data_updated" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py index d616ec6d1e842f..ae96704be589e9 100644 --- a/homeassistant/components/mpchc/media_player.py +++ b/homeassistant/components/mpchc/media_player.py @@ -56,7 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = "{}:{}".format(host, port) + url = f"{host}:{port}" add_entities([MpcHcDevice(name, url)], True) @@ -73,9 +73,7 @@ def __init__(self, name, url): def update(self): """Get the latest details.""" try: - response = requests.get( - "{}/variables.html".format(self._url), data=None, timeout=3 - ) + response = requests.get(f"{self._url}/variables.html", data=None, timeout=3) mpchc_variables = re.findall(r'

(.+?)

', response.text) @@ -88,7 +86,7 @@ def _send_command(self, command_id): """Send a command to MPC-HC via its window message ID.""" try: params = {"wm_command": command_id} - requests.get("{}/command.html".format(self._url), params=params, timeout=3) + requests.get(f"{self._url}/command.html", params=params, timeout=3) except requests.exceptions.RequestException: _LOGGER.error( "Could not send command %d to MPC-HC at: %s", command_id, self._url diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 0d924cdd1d29f8..c19f8f49226d9f 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -211,7 +211,7 @@ def media_title(self): if title is None: return name - return "{}: {}".format(name, title) + return f"{name}: {title}" @property def media_artist(self): diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d611b8db13e430..f393c3157932a5 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -86,7 +86,7 @@ async def async_device_message_received(msg): """Process the received message.""" payload = msg.payload topic = msg.topic - topic_trimmed = topic.replace("{}/".format(discovery_topic), "", 1) + topic_trimmed = topic.replace(f"{discovery_topic}/", "", 1) match = TOPIC_MATCHER.match(topic_trimmed) if not match: @@ -134,9 +134,7 @@ async def async_device_message_received(msg): if payload: # Attach MQTT topic to the payload, used for debug prints - setattr( - payload, "__configuration_source__", "MQTT (topic: '{}')".format(topic) - ) + setattr(payload, "__configuration_source__", f"MQTT (topic: '{topic}')") if CONF_PLATFORM in payload and "schema" not in payload: platform = payload[CONF_PLATFORM] diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 20be0dcf89cfb7..f2fa8f8da6622f 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -339,7 +339,7 @@ def message_received(msg): elif self._cleaning: self._status = "Cleaning" elif self._error: - self._status = "Error: {}".format(self._error) + self._status = f"Error: {self._error}" else: self._status = "Stopped" @@ -360,7 +360,7 @@ def message_received(msg): self.hass, self._sub_state, { - "topic{}".format(i): { + f"topic{i}": { "topic": topic, "msg_callback": message_received, "qos": self._qos, @@ -550,7 +550,7 @@ async def async_set_fan_speed(self, fan_speed, **kwargs): mqtt.async_publish( self.hass, self._set_fan_speed_topic, fan_speed, self._qos, self._retain ) - self._status = "Setting fan to {}...".format(fan_speed) + self._status = f"Setting fan to {fan_speed}..." self.async_write_ha_state() async def async_send_command(self, command, params=None, **kwargs): @@ -566,5 +566,5 @@ async def async_send_command(self, command, params=None, **kwargs): mqtt.async_publish( self.hass, self._send_command_topic, message, self._qos, self._retain ) - self._status = "Sending command {}...".format(message) + self._status = f"Sending command {message}..." self.async_write_ha_state() diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 2bde87976bc222..cbedd947843c38 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -56,7 +56,7 @@ def is_persistence_file(value): """Validate that persistence file path ends in either .pickle or .json.""" if value.endswith((".json", ".pickle")): return value - raise vol.Invalid("{} does not end in either `.json` or `.pickle`".format(value)) + raise vol.Invalid(f"{value} does not end in either `.json` or `.pickle`") def deprecated(key): @@ -138,7 +138,7 @@ def _get_mysensors_name(gateway, node_id, child_id): ), node_name, ) - return "{} {}".format(node_name, child_id) + return f"{node_name} {child_id}" @callback diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 28d49303835aab..366692205a761a 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -44,7 +44,7 @@ def is_serial_port(value): ports = ("COM{}".format(idx + 1) for idx in range(256)) if value in ports: return value - raise vol.Invalid("{} is not a serial port".format(value)) + raise vol.Invalid(f"{value} is not a serial port") return cv.isdevice(value) diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py index ac94a8559a19a0..99e731762df5c8 100644 --- a/homeassistant/components/mysensors/notify.py +++ b/homeassistant/components/mysensors/notify.py @@ -26,7 +26,7 @@ def send_msg(self, msg): def __repr__(self): """Return the representation.""" - return "".format(self.name) + return f"" class MySensorsNotificationService(BaseNotificationService): diff --git a/homeassistant/components/mystrom/binary_sensor.py b/homeassistant/components/mystrom/binary_sensor.py index 20d32be199edde..ff0063a380ed7d 100644 --- a/homeassistant/components/mystrom/binary_sensor.py +++ b/homeassistant/components/mystrom/binary_sensor.py @@ -41,19 +41,16 @@ async def _handle(self, hass, data): if button_action is None: _LOGGER.error("Received unidentified message from myStrom button: %s", data) - return ( - "Received unidentified message: {}".format(data), - HTTP_UNPROCESSABLE_ENTITY, - ) + return (f"Received unidentified message: {data}", HTTP_UNPROCESSABLE_ENTITY) button_id = data[button_action] - entity_id = "{}.{}_{}".format(DOMAIN, button_id, button_action) + entity_id = f"{DOMAIN}.{button_id}_{button_action}" if entity_id not in self.buttons: _LOGGER.info( "New myStrom button/action detected: %s/%s", button_id, button_action ) self.buttons[entity_id] = MyStromBinarySensor( - "{}_{}".format(button_id, button_action) + f"{button_id}_{button_action}" ) self.add_entities([self.buttons[entity_id]]) else: From 445c741b30cd5a40b5914e42351ec8d20c63243b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Sep 2019 21:14:39 +0200 Subject: [PATCH 0135/3953] Use literal string interpolation in integrations R-S (f-strings) (#26392) --- homeassistant/components/rachio/__init__.py | 2 +- .../components/rachio/binary_sensor.py | 4 +-- homeassistant/components/rachio/switch.py | 8 ++--- homeassistant/components/rainbird/switch.py | 2 +- .../components/rainmachine/__init__.py | 6 ++-- .../components/recollect_waste/sensor.py | 2 +- homeassistant/components/reddit/sensor.py | 2 +- .../components/rejseplanen/sensor.py | 2 +- .../components/remember_the_milk/__init__.py | 6 ++-- homeassistant/components/repetier/__init__.py | 2 +- homeassistant/components/rflink/light.py | 2 +- homeassistant/components/rfxtrx/__init__.py | 4 +-- .../components/rfxtrx/binary_sensor.py | 2 +- homeassistant/components/rfxtrx/sensor.py | 4 +-- .../components/ring/binary_sensor.py | 2 +- homeassistant/components/ring/light.py | 2 +- homeassistant/components/ring/sensor.py | 2 +- homeassistant/components/ring/switch.py | 4 +-- homeassistant/components/roku/__init__.py | 2 +- homeassistant/components/roku/media_player.py | 2 +- homeassistant/components/roku/remote.py | 2 +- homeassistant/components/roomba/vacuum.py | 2 +- homeassistant/components/route53/__init__.py | 2 +- homeassistant/components/rova/sensor.py | 2 +- homeassistant/components/rtorrent/sensor.py | 2 +- homeassistant/components/sabnzbd/sensor.py | 2 +- .../components/samsungtv/media_player.py | 2 +- homeassistant/components/scsgate/__init__.py | 6 ++-- homeassistant/components/sense/sensor.py | 2 +- homeassistant/components/serial_pm/sensor.py | 2 +- .../components/seventeentrack/sensor.py | 4 +-- homeassistant/components/shiftr/__init__.py | 5 +-- .../components/shopping_list/__init__.py | 2 +- homeassistant/components/sigfox/sensor.py | 6 ++-- .../components/sky_hub/device_tracker.py | 2 +- homeassistant/components/skybell/camera.py | 2 +- homeassistant/components/sma/sensor.py | 6 ++-- homeassistant/components/smappee/sensor.py | 2 +- .../components/smartthings/binary_sensor.py | 4 +-- .../components/smartthings/sensor.py | 4 +-- .../components/smartthings/smartapp.py | 2 +- .../components/smarty/binary_sensor.py | 12 ++----- homeassistant/components/smarty/sensor.py | 12 +++---- homeassistant/components/smhi/weather.py | 2 +- homeassistant/components/smtp/notify.py | 12 +++---- .../components/snapcast/media_player.py | 14 ++++---- .../components/solaredge_local/sensor.py | 2 +- homeassistant/components/solax/sensor.py | 4 +-- homeassistant/components/somfy/config_flow.py | 4 +-- .../components/sonos/media_player.py | 2 +- .../components/speedtestdotnet/const.py | 2 +- homeassistant/components/spotcrime/sensor.py | 2 +- .../components/spotify/media_player.py | 2 +- .../components/squeezebox/media_player.py | 6 ++-- homeassistant/components/srp_energy/sensor.py | 2 +- homeassistant/components/startca/sensor.py | 2 +- .../components/steam_online/sensor.py | 2 +- .../components/stiebel_eltron/climate.py | 2 +- homeassistant/components/stream/__init__.py | 8 ++--- homeassistant/components/stream/hls.py | 7 ++-- .../streamlabswater/binary_sensor.py | 2 +- .../components/streamlabswater/sensor.py | 6 ++-- .../swiss_hydrological_data/sensor.py | 2 +- .../swiss_public_transport/sensor.py | 2 +- .../components/swisscom/device_tracker.py | 2 +- .../components/switcher_kis/switch.py | 2 +- homeassistant/components/syncthru/sensor.py | 34 +++++++++---------- .../components/synologydsm/sensor.py | 2 +- homeassistant/components/sytadin/sensor.py | 2 +- 69 files changed, 129 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 0d582b0c2e99f1..2030512ab3136e 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -226,7 +226,7 @@ def _deinit_webhooks(event) -> None: def __str__(self) -> str: """Display the controller as a string.""" - return 'Rachio controller "{}"'.format(self.name) + return f'Rachio controller "{self.name}"' @property def controller_id(self) -> str: diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 01d38a931c474d..f74e3ca1802181 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -87,12 +87,12 @@ def __init__(self, hass, controller): @property def name(self) -> str: """Return the name of this sensor including the controller name.""" - return "{} online".format(self._controller.name) + return f"{self._controller.name} online" @property def unique_id(self) -> str: """Return a unique id for this entity.""" - return "{}-online".format(self._controller.controller_id) + return f"{self._controller.controller_id}-online" @property def device_class(self) -> str: diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index b65e6bf6044295..80c227a6df6977 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -72,7 +72,7 @@ def should_poll(self) -> bool: @property def name(self) -> str: """Get a name for this switch.""" - return "Switch on {}".format(self._controller.name) + return f"Switch on {self._controller.name}" @property def is_on(self) -> bool: @@ -113,12 +113,12 @@ def __init__(self, hass, controller): @property def name(self) -> str: """Return the name of the standby switch.""" - return "{} in standby mode".format(self._controller.name) + return f"{self._controller.name} in standby mode" @property def unique_id(self) -> str: """Return a unique id by combining controller id and purpose.""" - return "{}-standby".format(self._controller.controller_id) + return f"{self._controller.controller_id}-standby" @property def icon(self) -> str: @@ -183,7 +183,7 @@ def name(self) -> str: @property def unique_id(self) -> str: """Return a unique id by combining controller id and zone number.""" - return "{}-zone-{}".format(self._controller.controller_id, self.zone_id) + return f"{self._controller.controller_id}-zone-{self.zone_id}" @property def icon(self) -> str: diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index a1b82bc1af7453..868e8ff4c7d65b 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -53,7 +53,7 @@ def __init__(self, rb, dev, dev_id): self._rainbird = rb self._devid = dev_id self._zone = int(dev.get(CONF_ZONE)) - self._name = dev.get(CONF_FRIENDLY_NAME, "Sprinkler {}".format(self._zone)) + self._name = dev.get(CONF_FRIENDLY_NAME, f"Sprinkler {self._zone}") self._state = None self._duration = dev.get(CONF_TRIGGER_TIME) self._attributes = {"duration": self._duration, "zone": self._zone} diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index b04384dc81dda3..183872087a7240 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -41,9 +41,9 @@ DATA_LISTENER = "listener" -PROGRAM_UPDATE_TOPIC = "{0}_program_update".format(DOMAIN) -SENSOR_UPDATE_TOPIC = "{0}_data_update".format(DOMAIN) -ZONE_UPDATE_TOPIC = "{0}_zone_update".format(DOMAIN) +PROGRAM_UPDATE_TOPIC = f"{DOMAIN}_program_update" +SENSOR_UPDATE_TOPIC = f"{DOMAIN}_data_update" +ZONE_UPDATE_TOPIC = f"{DOMAIN}_zone_update" CONF_CONTROLLERS = "controllers" CONF_PROGRAM_ID = "program_id" diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 0c3833528dc6f4..118b6fb370979a 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -66,7 +66,7 @@ def name(self): @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}{}".format(self.client.place_id, self.client.service_id) + return f"{self.client.place_id}{self.client.service_id}" @property def state(self): diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 0e95bea9091d7e..f9c8140f60db71 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -94,7 +94,7 @@ def __init__(self, reddit, subreddit: str, limit: int, sort_by: str): @property def name(self): """Return the name of the sensor.""" - return "reddit_{}".format(self._subreddit) + return f"reddit_{self._subreddit}" @property def state(self): diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 3172e614166bec..61cb319fd11ed6 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -220,7 +220,7 @@ def intersection(lst1, lst2): and due_at_time is not None and route is not None ): - due_at = "{} {}".format(due_at_date, due_at_time) + due_at = f"{due_at_date} {due_at_time}" departure_data = { ATTR_DUE_IN: due_in_minutes(due_at), diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index 3d340d9c07e612..c92a246da14789 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -87,13 +87,13 @@ def _create_instance( component.add_entities([entity]) hass.services.register( DOMAIN, - "{}_create_task".format(account_name), + f"{account_name}_create_task", entity.create_task, schema=SERVICE_SCHEMA_CREATE_TASK, ) hass.services.register( DOMAIN, - "{}_complete_task".format(account_name), + f"{account_name}_complete_task", entity.complete_task, schema=SERVICE_SCHEMA_COMPLETE_TASK, ) @@ -137,7 +137,7 @@ def register_account_callback(_): configurator.request_done(request_id) request_id = configurator.async_request_config( - "{} - {}".format(DOMAIN, account_name), + f"{DOMAIN} - {account_name}", callback=register_account_callback, description="You need to log in to Remember The Milk to" + "connect your account. \n\n" diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 1643966b33eef6..6f72a6b7ddc545 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -239,7 +239,7 @@ def _load_entities(self): info["name"] = printer.slug info["printer_name"] = self.conf_name - known = "{}-{}".format(printer.slug, sensor_type) + known = f"{printer.slug}-{sensor_type}" if known in self._known_entities: continue diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index 56ae6f8675fb3a..682d45f8f420b2 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -300,7 +300,7 @@ class ToggleRflinkLight(SwitchableRflinkDevice, Light): @property def entity_id(self): """Return entity id.""" - return "light.{}".format(self.name) + return f"light.{self.name}" def _handle_event(self, event): """Adjust state if Rflink picks up a remote command for this device.""" diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 0c9a98143c800c..79b3054ecf2750 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -106,7 +106,7 @@ def handle_receive(event): slugify(event.device.id_string.lower()), event.device.__class__.__name__, event.device.subtype, - "".join("{0:02x}".format(x) for x in event.data), + "".join(f"{x:02x}" for x in event.data), ) # Callback to HA registered components. @@ -270,7 +270,7 @@ def get_new_device(event, config, device): if not config[ATTR_AUTOMATIC_ADD]: return - pkt_id = "".join("{0:02x}".format(x) for x in event.data) + pkt_id = "".join(f"{x:02x}" for x in event.data) _LOGGER.debug( "Automatic add %s rfxtrx device (Class: %s Sub: %s Packet_id: %s)", device_id, diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index d4ed874156e9a1..8f1c7e6fa55f3d 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -116,7 +116,7 @@ def binary_sensor_update(event): poss_id = slugify(poss_dev.event.device.id_string.lower()) _LOGGER.debug("Found possible matching device ID: %s", poss_id) - pkt_id = "".join("{0:02x}".format(x) for x in event.data) + pkt_id = "".join(f"{x:02x}" for x in event.data) sensor = RfxtrxBinarySensor(event, pkt_id) sensor.hass = hass rfxtrx.RFX_DEVICES[device_id] = sensor diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 69397263a627e4..5941b00764b8ce 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -98,7 +98,7 @@ def sensor_update(event): if not config[CONF_AUTOMATIC_ADD]: return - pkt_id = "".join("{0:02x}".format(x) for x in event.data) + pkt_id = "".join(f"{x:02x}" for x in event.data) _LOGGER.info("Automatic add rfxtrx.sensor: %s", pkt_id) data_type = "" @@ -141,7 +141,7 @@ def state(self): @property def name(self): """Get the name of the sensor.""" - return "{} {}".format(self._name, self.data_type) + return f"{self._name} {self.data_type}" @property def device_state_attributes(self): diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 1b06a1d47d1086..6806df0408fb78 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -73,7 +73,7 @@ def __init__(self, hass, data, sensor_type): ) self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] self._state = None - self._unique_id = "{}-{}".format(self._data.id, self._sensor_type) + self._unique_id = f"{self._data.id}-{self._sensor_type}" @property def name(self): diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index bd7ea3a36797a1..5805114252e09a 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -56,7 +56,7 @@ def _update_callback(self): @property def name(self): """Name of the light.""" - return "{} light".format(self._device.name) + return f"{self._device.name} light" @property def unique_id(self): diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 9950609c10fddf..af661f4571c210 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -121,7 +121,7 @@ def __init__(self, hass, data, sensor_type): ) self._state = None self._tz = str(hass.config.time_zone) - self._unique_id = "{}-{}".format(self._data.id, self._sensor_type) + self._unique_id = f"{self._data.id}-{self._sensor_type}" async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 3b6bd4ea0243b5..cbbecb1a40398b 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -38,7 +38,7 @@ 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) + self._unique_id = f"{self._device.id}-{self._device_type}" async def async_added_to_hass(self): """Register callbacks.""" @@ -53,7 +53,7 @@ def _update_callback(self): @property def name(self): """Name of the device.""" - return "{} {}".format(self._device.name, self._device_type) + return f"{self._device.name} {self._device_type}" @property def unique_id(self): diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index e6dd05b9328647..aa13814ee6b7a3 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -78,7 +78,7 @@ def scan_for_rokus(hass): "Name: {0}
Host: {1}
".format( r_info.userdevicename if r_info.userdevicename - else "{} {}".format(r_info.modelname, r_info.serial_num), + else f"{r_info.modelname} {r_info.serial_num}", roku.host, ) ) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 03060361020c1d..d69b0eddb71d52 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -92,7 +92,7 @@ def name(self): """Return the name of the device.""" if self._device_info.user_device_name: return self._device_info.user_device_name - return "Roku {}".format(self._device_info.serial_num) + return f"Roku {self._device_info.serial_num}" @property def state(self): diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 0bb840e953135a..f443b7e8e74edc 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -36,7 +36,7 @@ def name(self): """Return the name of the device.""" if self._device_info.user_device_name: return self._device_info.user_device_name - return "Roku {}".format(self._device_info.serial_num) + return f"Roku {self._device_info.serial_num}" @property def unique_id(self): diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index 766fd72cdbab15..291658e19f4b2a 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -327,7 +327,7 @@ async def async_update(self): pos_y = pos_state.get("point", {}).get("y") theta = pos_state.get("theta") if all(item is not None for item in [pos_x, pos_y, theta]): - position = "({}, {}, {})".format(pos_x, pos_y, theta) + position = f"({pos_x}, {pos_y}, {theta})" self._state_attrs[ATTR_POSITION] = position # Not all Roombas have a bin full sensor diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py index 906f09a56491f1..3dffc3ffd9e381 100644 --- a/homeassistant/components/route53/__init__.py +++ b/homeassistant/components/route53/__init__.py @@ -104,7 +104,7 @@ def _update_route53( { "Action": "UPSERT", "ResourceRecordSet": { - "Name": "{}.{}".format(record, domain), + "Name": f"{record}.{domain}", "Type": "A", "TTL": ttl, "ResourceRecords": [{"Value": ipaddress}], diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index c39bf5ca4f3f18..fe0b5dead84d15 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -96,7 +96,7 @@ def __init__(self, platform_name, sensor_key, data_service): @property def name(self): """Return the name.""" - return "{}_{}".format(self.platform_name, self.sensor_key) + return f"{self.platform_name}_{self.sensor_key}" @property def icon(self): diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index f2533e3dc86261..ed16331e91288b 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -79,7 +79,7 @@ def __init__(self, sensor_type, rtorrent_client, client_name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 642dd27b1d82e3..58624c758d9c83 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -42,7 +42,7 @@ async def async_added_to_hass(self): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._client_name, self._name) + return f"{self._client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 934ee94e65df3b..2821a05261b337 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -84,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): tv_name = discovery_info.get("name") model = discovery_info.get("model_name") host = discovery_info.get("host") - name = "{} ({})".format(tv_name, model) + name = f"{tv_name} ({model})" port = DEFAULT_PORT timeout = DEFAULT_TIMEOUT mac = None diff --git a/homeassistant/components/scsgate/__init__.py b/homeassistant/components/scsgate/__init__.py index acb7f78a2aafc2..739a2949d17a5d 100644 --- a/homeassistant/components/scsgate/__init__.py +++ b/homeassistant/components/scsgate/__init__.py @@ -77,11 +77,11 @@ def handle_message(self, message): """Handle a messages seen on the bus.""" from scsgate.messages import StateMessage, ScenarioTriggeredMessage - self._logger.debug("Received message {}".format(message)) + self._logger.debug(f"Received message {message}") if not isinstance(message, StateMessage) and not isinstance( message, ScenarioTriggeredMessage ): - msg = "Ignored message {} - not relevant type".format(message) + msg = f"Ignored message {message} - not relevant type" self._logger.debug(msg) return @@ -97,7 +97,7 @@ def handle_message(self, message): try: self._devices[message.entity].process_event(message) except Exception as exception: # pylint: disable=broad-except - msg = "Exception while processing event: {}".format(exception) + msg = f"Exception while processing event: {exception}" self._logger.error(msg) else: self._logger.info( diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 8ad289b92000df..36474620b03f2b 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -80,7 +80,7 @@ class Sense(Entity): def __init__(self, data, name, sensor_type, is_production, update_call): """Initialize the Sense sensor.""" name_type = PRODUCTION_NAME if is_production else CONSUMPTION_NAME - self._name = "{} {}".format(name, name_type) + self._name = f"{name} {name_type}" self._data = data self._sensor_type = sensor_type self.update_sensor = update_call diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index 80952672487783..1d46b05d46e0d6 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if config.get(CONF_NAME) is not None: name = "{} PM{}".format(config.get(CONF_NAME), pmname) else: - name = "PM{}".format(pmname) + name = f"PM{pmname}" dev.append(ParticulateMatterSensor(coll, name, pmname)) add_entities(dev) diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 14539e342f18c4..33abe2f1f861df 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -120,7 +120,7 @@ def icon(self): @property def name(self): """Return the name.""" - return "Seventeentrack Packages {0}".format(self._status) + return f"Seventeentrack Packages {self._status}" @property def state(self): @@ -203,7 +203,7 @@ def name(self): name = self._friendly_name if not name: name = self._tracking_number - return "Seventeentrack Package: {0}".format(name) + return f"Seventeentrack Package: {name}" @property def state(self): diff --git a/homeassistant/components/shiftr/__init__.py b/homeassistant/components/shiftr/__init__.py index a7e82ef66cf623..8e698d283cfcaa 100644 --- a/homeassistant/components/shiftr/__init__.py +++ b/homeassistant/components/shiftr/__init__.py @@ -69,10 +69,7 @@ def shiftr_event_listener(event): if state.attributes: for attribute, data in state.attributes.items(): mqttc.publish( - "/{}/{}".format(topic, attribute), - str(data), - qos=0, - retain=False, + f"/{topic}/{attribute}", str(data), qos=0, retain=False ) except RuntimeError: pass diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 075d819655b395..3c9cb4391a73af 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -196,7 +196,7 @@ def async_handle(self, intent_obj): intent_obj.hass.data[DOMAIN].async_add(item) response = intent_obj.create_response() - response.async_set_speech("I've added {} to your shopping list".format(item)) + response.async_set_speech(f"I've added {item} to your shopping list") intent_obj.hass.bus.async_fire(EVENT) return response diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index 0961017b65c153..b890880389c232 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -90,7 +90,7 @@ def get_devices(self, device_types): """Get the device_id of each device registered.""" devices = [] for unique_type in device_types: - location_url = "devicetypes/{}/devices".format(unique_type) + location_url = f"devicetypes/{unique_type}/devices" url = urljoin(API_URL, location_url) response = requests.get(url, auth=self._auth, timeout=10) devices_data = json.loads(response.text)["data"] @@ -117,12 +117,12 @@ def __init__(self, device_id, auth, name): self._device_id = device_id self._auth = auth self._message_data = {} - self._name = "{}_{}".format(name, device_id) + self._name = f"{name}_{device_id}" self._state = None def get_last_message(self): """Return the last message from a device.""" - device_url = "devices/{}/messages?limit=1".format(self._device_id) + device_url = f"devices/{self._device_id}/messages?limit=1" url = urljoin(API_URL, device_url) response = requests.get(url, auth=self._auth, timeout=10) data = json.loads(response.text)["data"][0] diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index eea97fb37fb021..c8969add244a19 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -34,7 +34,7 @@ def __init__(self, config): _LOGGER.info("Initialising Sky Hub") self.host = config.get(CONF_HOST, "192.168.1.254") self.last_results = {} - self.url = "http://{}/".format(self.host) + self.url = f"http://{self.host}/" # Test the router is accessible data = _get_skyhub_data(self.url) diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index 87cfdce6dfcaca..87dc3c0bf8d75b 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -57,7 +57,7 @@ def __init__(self, device, camera_type, name=None): SkybellDevice.__init__(self, device) Camera.__init__(self) if name is not None: - self._name = "{} {}".format(self._device.name, name) + self._name = f"{self._device.name} {name}" else: self._name = self._device.name self._url = None diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 5b6bc57be9d6dc..56e10b03d2adca 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -50,7 +50,7 @@ def _check_sensor_schema(conf): sensor, ) elif sensor not in valid: - raise vol.Invalid("{} does not exist".format(sensor)) + raise vol.Invalid(f"{sensor} does not exist") return conf @@ -217,7 +217,7 @@ def async_update_values(self): update = False for sens in self._sub_sensors: # Can be remove from 0.99 - newval = "{} {}".format(sens.value, sens.unit) + newval = f"{sens.value} {sens.unit}" if self._attr[sens.name] != newval: update = True self._attr[sens.name] = newval @@ -231,4 +231,4 @@ def async_update_values(self): @property def unique_id(self): """Return a unique identifier for this sensor.""" - return "sma-{}-{}".format(self._sensor.key, self._sensor.name) + return f"sma-{self._sensor.key}-{self._sensor.name}" diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index cdbd1d18c2990a..28abf759d098f6 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -139,7 +139,7 @@ def name(self): else: location_name = "Local" - return "{} {} {}".format(SENSOR_PREFIX, location_name, self._name) + return f"{SENSOR_PREFIX} {location_name} {self._name}" @property def icon(self): diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 1ddbee6b827ca5..1e90709fc82c43 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -66,12 +66,12 @@ def __init__(self, device, attribute): @property def name(self) -> str: """Return the name of the binary sensor.""" - return "{} {}".format(self._device.label, self._attribute) + return f"{self._device.label} {self._attribute}" @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}.{}".format(self._device.device_id, self._attribute) + return f"{self._device.device_id}.{self._attribute}" @property def is_on(self): diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 423c141e4dab0b..3a6f9167054a04 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -283,12 +283,12 @@ def __init__( @property def name(self) -> str: """Return the name of the binary sensor.""" - return "{} {}".format(self._device.label, self._name) + return f"{self._device.label} {self._name}" @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}.{}".format(self._device.device_id, self._attribute) + return f"{self._device.device_id}.{self._attribute}" @property def state(self): diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index b64ba690d41dbc..d205c1d245cb1b 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -112,7 +112,7 @@ def _get_app_template(hass: HomeAssistantType): cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] if cloudhook_url is not None: endpoint = "via Nabu Casa" - description = "{} {}".format(hass.config.location_name, endpoint) + description = f"{hass.config.location_name} {endpoint}" return { "app_name": APP_NAME_PREFIX + str(uuid4()), diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index 2d79700db78507..8723f0248d34a8 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -69,9 +69,7 @@ class BoostSensor(SmartyBinarySensor): def __init__(self, name, smarty): """Alarm Sensor Init.""" - super().__init__( - name="{} Boost State".format(name), device_class=None, smarty=smarty - ) + super().__init__(name=f"{name} Boost State", device_class=None, smarty=smarty) def update(self) -> None: """Update state.""" @@ -84,9 +82,7 @@ class AlarmSensor(SmartyBinarySensor): def __init__(self, name, smarty): """Alarm Sensor Init.""" - super().__init__( - name="{} Alarm".format(name), device_class="problem", smarty=smarty - ) + super().__init__(name=f"{name} Alarm", device_class="problem", smarty=smarty) def update(self) -> None: """Update state.""" @@ -99,9 +95,7 @@ class WarningSensor(SmartyBinarySensor): def __init__(self, name, smarty): """Warning Sensor Init.""" - super().__init__( - name="{} Warning".format(name), device_class="problem", smarty=smarty - ) + super().__init__(name=f"{name} Warning", device_class="problem", smarty=smarty) def update(self) -> None: """Update state.""" diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index 16d910beeb5b57..bf647777b52801 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -88,7 +88,7 @@ class SupplyAirTemperatureSensor(SmartySensor): def __init__(self, name, smarty): """Supply Air Temperature Init.""" super().__init__( - name="{} Supply Air Temperature".format(name), + name=f"{name} Supply Air Temperature", device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, smarty=smarty, @@ -106,7 +106,7 @@ class ExtractAirTemperatureSensor(SmartySensor): def __init__(self, name, smarty): """Supply Air Temperature Init.""" super().__init__( - name="{} Extract Air Temperature".format(name), + name=f"{name} Extract Air Temperature", device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, smarty=smarty, @@ -124,7 +124,7 @@ class OutdoorAirTemperatureSensor(SmartySensor): def __init__(self, name, smarty): """Outdoor Air Temperature Init.""" super().__init__( - name="{} Outdoor Air Temperature".format(name), + name=f"{name} Outdoor Air Temperature", device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, smarty=smarty, @@ -142,7 +142,7 @@ class SupplyFanSpeedSensor(SmartySensor): def __init__(self, name, smarty): """Supply Fan Speed RPM Init.""" super().__init__( - name="{} Supply Fan Speed".format(name), + name=f"{name} Supply Fan Speed", device_class=None, unit_of_measurement=None, smarty=smarty, @@ -160,7 +160,7 @@ class ExtractFanSpeedSensor(SmartySensor): def __init__(self, name, smarty): """Extract Fan Speed RPM Init.""" super().__init__( - name="{} Extract Fan Speed".format(name), + name=f"{name} Extract Fan Speed", device_class=None, unit_of_measurement=None, smarty=smarty, @@ -178,7 +178,7 @@ class FilterDaysLeftSensor(SmartySensor): def __init__(self, name, smarty): """Filter Days Left Init.""" super().__init__( - name="{} Filter Days Left".format(name), + name=f"{name} Filter Days Left", device_class=DEVICE_CLASS_TIMESTAMP, unit_of_measurement=None, smarty=smarty, diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 29a8c300944585..5f6722b72a6df3 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -102,7 +102,7 @@ def __init__( @property def unique_id(self) -> str: """Return a unique id.""" - return "{}, {}".format(self._latitude, self._longitude) + return f"{self._latitude}, {self._longitude}" @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 7336b4577fa3d8..8a96865ab8db63 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -182,7 +182,7 @@ def send_message(self, message="", **kwargs): msg["Subject"] = subject msg["To"] = ",".join(self.recipients) if self._sender_name: - msg["From"] = "{} <{}>".format(self._sender_name, self._sender) + msg["From"] = f"{self._sender_name} <{self._sender}>" else: msg["From"] = self._sender msg["X-Mailer"] = "HomeAssistant" @@ -225,18 +225,18 @@ def _build_multipart_msg(message, images): msg.attach(msg_alt) body_txt = MIMEText(message) msg_alt.attach(body_txt) - body_text = ["

{}


".format(message)] + body_text = [f"

{message}


"] for atch_num, atch_name in enumerate(images): - cid = "image{}".format(atch_num) - body_text.append('
'.format(cid)) + cid = f"image{atch_num}" + body_text.append(f'
') try: with open(atch_name, "rb") as attachment_file: file_bytes = attachment_file.read() try: attachment = MIMEImage(file_bytes) msg.attach(attachment) - attachment.add_header("Content-ID", "<{}>".format(cid)) + attachment.add_header("Content-ID", f"<{cid}>") except TypeError: _LOGGER.warning( "Attachment %s has an unknown MIME type. " @@ -271,7 +271,7 @@ def _build_html_msg(text, html, images): with open(atch_name, "rb") as attachment_file: attachment = MIMEImage(attachment_file.read(), filename=name) msg.attach(attachment) - attachment.add_header("Content-ID", "<{}>".format(name)) + attachment.add_header("Content-ID", f"<{name}>") except FileNotFoundError: _LOGGER.warning( "Attachment %s [#%s] not found. Skipping", atch_name, atch_num diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 454201319adfd3..81cd6538578c0b 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -98,7 +98,7 @@ async def async_service_handle(service_event, service, data): return # Note: Host part is needed, when using multiple snapservers - hpid = "{}:{}".format(host, port) + hpid = f"{host}:{port}" groups = [SnapcastGroupDevice(group, hpid) for group in server.groups] clients = [SnapcastClientDevice(client, hpid) for client in server.clients] @@ -114,7 +114,7 @@ def __init__(self, group, uid_part): """Initialize the Snapcast group device.""" group.set_callback(self.schedule_update_ha_state) self._group = group - self._uid = "{}{}_{}".format(GROUP_PREFIX, uid_part, self._group.identifier) + self._uid = f"{GROUP_PREFIX}{uid_part}_{self._group.identifier}" @property def state(self): @@ -133,7 +133,7 @@ def unique_id(self): @property def name(self): """Return the name of the device.""" - return "{}{}".format(GROUP_PREFIX, self._group.identifier) + return f"{GROUP_PREFIX}{self._group.identifier}" @property def source(self): @@ -163,7 +163,7 @@ def source_list(self): @property def device_state_attributes(self): """Return the state attributes.""" - name = "{} {}".format(self._group.friendly_name, GROUP_SUFFIX) + name = f"{self._group.friendly_name} {GROUP_SUFFIX}" return {"friendly_name": name} @property @@ -204,7 +204,7 @@ def __init__(self, client, uid_part): """Initialize the Snapcast client device.""" client.set_callback(self.schedule_update_ha_state) self._client = client - self._uid = "{}{}_{}".format(CLIENT_PREFIX, uid_part, self._client.identifier) + self._uid = f"{CLIENT_PREFIX}{uid_part}_{self._client.identifier}" @property def unique_id(self): @@ -223,7 +223,7 @@ def identifier(self): @property def name(self): """Return the name of the device.""" - return "{}{}".format(CLIENT_PREFIX, self._client.identifier) + return f"{CLIENT_PREFIX}{self._client.identifier}" @property def source(self): @@ -260,7 +260,7 @@ def state(self): @property def device_state_attributes(self): """Return the state attributes.""" - name = "{} {}".format(self._client.friendly_name, CLIENT_SUFFIX) + name = f"{self._client.friendly_name} {CLIENT_SUFFIX}" return {"friendly_name": name} @property diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 80bd8e1f61ed8c..8586d950e39cdb 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -67,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): platform_name = config[CONF_NAME] # Create new SolarEdge object to retrieve data - api = SolarEdge("http://{}/".format(ip_address)) + api = SolarEdge(f"http://{ip_address}/") # Check if api can be reached and site is active try: diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 62c6a2a3a514b8..0c1cfcf21da32a 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -35,7 +35,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= idx, unit = solax.INVERTER_SENSORS[sensor] if unit == "C": unit = TEMP_CELSIUS - uid = "{}-{}".format(serial, idx) + uid = f"{serial}-{idx}" devices.append(Inverter(uid, serial, sensor, unit)) endpoint.sensors = devices async_add_entities(devices) @@ -97,7 +97,7 @@ def unique_id(self): @property def name(self): """Name of this inverter attribute.""" - return "Solax {} {}".format(self.serial, self.key) + return f"Solax {self.serial} {self.key}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index 7e6645c31e2f52..9f3c58c8ffba70 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -73,7 +73,7 @@ async def _get_authorization_url(self): client_id = self.hass.data[DOMAIN][CLIENT_ID] client_secret = self.hass.data[DOMAIN][CLIENT_SECRET] - redirect_uri = "{}{}".format(self.hass.config.api.base_url, AUTH_CALLBACK_PATH) + redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" api = SomfyApi(client_id, client_secret, redirect_uri) self.hass.http.register_view(SomfyAuthCallbackView()) @@ -95,7 +95,7 @@ async def async_step_creation(self, user_input=None): code = self.code from pymfy.api.somfy_api import SomfyApi - redirect_uri = "{}{}".format(self.hass.config.api.base_url, AUTH_CALLBACK_PATH) + redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" api = SomfyApi(client_id, client_secret, redirect_uri) token = await self.hass.async_add_executor_job(api.request_token, None, code) _LOGGER.info("Successfully authenticated Somfy") diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 70461ad15d2fa6..41472413a077b6 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -606,7 +606,7 @@ def update_media_radio(self, variables, track_info): # media_artist = "Station - Artist - Title" # detect this case and trim from the front of # media_artist for cosmetics - trim = "{title} - ".format(title=self._media_title) + trim = f"{self._media_title} - " chars = min(len(self._media_artist), len(trim)) if self._media_artist[:chars].upper() == trim[:chars].upper(): diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index 90a89fcda2c60a..69aadb7ac6c8d2 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -1,7 +1,7 @@ """Consts used by Speedtest.net.""" DOMAIN = "speedtestdotnet" -DATA_UPDATED = "{}_data_updated".format(DOMAIN) +DATA_UPDATED = f"{DOMAIN}_data_updated" SENSOR_TYPES = { "ping": ["Ping", "ms"], diff --git a/homeassistant/components/spotcrime/sensor.py b/homeassistant/components/spotcrime/sensor.py index 4498fd47e694e8..fc3a7592af3130 100644 --- a/homeassistant/components/spotcrime/sensor.py +++ b/homeassistant/components/spotcrime/sensor.py @@ -29,7 +29,7 @@ DEFAULT_DAYS = 1 NAME = "spotcrime" -EVENT_INCIDENT = "{}_incident".format(NAME) +EVENT_INCIDENT = f"{NAME}_incident" SCAN_INTERVAL = timedelta(minutes=30) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 74a0dc0c9c0cca..31fdc09af80371 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -99,7 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Spotify platform.""" import spotipy.oauth2 - callback_url = "{}{}".format(hass.config.api.base_url, AUTH_CALLBACK_PATH) + callback_url = f"{hass.config.api.base_url}{AUTH_CALLBACK_PATH}" cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH)) oauth = spotipy.oauth2.SpotifyOAuth( config.get(CONF_CLIENT_ID), diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 5f3a91bcf2f9d2..9e62e7ee0dbc83 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -208,7 +208,7 @@ async def async_query(self, *command, player=""): if self._username is None else aiohttp.BasicAuth(self._username, self._password) ) - url = "http://{}:{}/jsonrpc.js".format(self.host, self.port) + url = f"http://{self.host}:{self.port}/jsonrpc.js" data = json.dumps( {"id": "1", "method": "slim.request", "params": [player, command]} ) @@ -288,9 +288,7 @@ def async_query(self, *parameters): async def async_update(self): """Retrieve the current state of the player.""" tags = "adKl" - response = await self.async_query( - "status", "-", "1", "tags:{tags}".format(tags=tags) - ) + response = await self.async_query("status", "-", "1", f"tags:{tags}") if response is False: return diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index a9873c76afe79f..f1d1787b7b48c5 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -87,7 +87,7 @@ def state(self): if self._state is None: return None - return "{0:.2f}".format(self._state) + return f"{self._state:.2f}" @property def name(self): diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index d4104dd3dcf47c..5e370ed7b63290 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -86,7 +86,7 @@ def __init__(self, startcadata, sensor_type, name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 924afedfbd5f3b..6c9c5ac6079eb9 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -80,7 +80,7 @@ def name(self): @property def entity_id(self): """Return the entity ID.""" - return "sensor.steam_{}".format(self._account) + return f"sensor.steam_{self._account}" @property def state(self): diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py index f2e6209d21db70..ce16b10f548856 100644 --- a/homeassistant/components/stiebel_eltron/climate.py +++ b/homeassistant/components/stiebel_eltron/climate.py @@ -139,7 +139,7 @@ def max_temp(self): @property def current_humidity(self): """Return the current humidity.""" - return float("{0:.1f}".format(self._current_humidity)) + return float(f"{self._current_humidity:.1f}") @property def hvac_modes(self): diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 50cc1d8169d6e4..2ae8dd5f71405b 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -217,9 +217,7 @@ async def async_handle_record_service(hass, call): # Check for file access if not hass.config.is_allowed_path(video_path): - raise HomeAssistantError( - "Can't write {}, no access to path!".format(video_path) - ) + raise HomeAssistantError(f"Can't write {video_path}, no access to path!") # Check for active stream streams = hass.data[DOMAIN][ATTR_STREAMS] @@ -231,9 +229,7 @@ async def async_handle_record_service(hass, call): # Add recorder recorder = stream.outputs.get("recorder") if recorder: - raise HomeAssistantError( - "Stream already recording to {}!".format(recorder.video_path) - ) + raise HomeAssistantError(f"Stream already recording to {recorder.video_path}!") recorder = stream.add_provider("recorder") recorder.video_path = video_path diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index ab8779151588a3..c9e62f53a5788b 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -64,10 +64,7 @@ def __init__(self, stream): @staticmethod def render_preamble(track): """Render preamble.""" - return [ - "#EXT-X-VERSION:3", - "#EXT-X-TARGETDURATION:{}".format(track.target_duration), - ] + return ["#EXT-X-VERSION:3", f"#EXT-X-TARGETDURATION:{track.target_duration}"] @staticmethod def render_playlist(track, start_time): @@ -84,7 +81,7 @@ def render_playlist(track, start_time): playlist.extend( [ "#EXTINF:{:.04f},".format(float(segment.duration)), - "./segment/{}.ts".format(segment.sequence), + f"./segment/{segment.sequence}.ts", ] ) diff --git a/homeassistant/components/streamlabswater/binary_sensor.py b/homeassistant/components/streamlabswater/binary_sensor.py index fd0ccb57aa6018..78b2ceb40441d0 100644 --- a/homeassistant/components/streamlabswater/binary_sensor.py +++ b/homeassistant/components/streamlabswater/binary_sensor.py @@ -58,7 +58,7 @@ def __init__(self, location_name, streamlabs_location_data): @property def name(self): """Return the name for away mode.""" - return "{} {}".format(self._location_name, NAME_AWAY_MODE) + return f"{self._location_name} {NAME_AWAY_MODE}" @property def is_on(self): diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index 69196c288f6077..e7168f8ec0b536 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -79,7 +79,7 @@ def __init__(self, location_name, streamlabs_usage_data): @property def name(self): """Return the name for daily usage.""" - return "{} {}".format(self._location_name, NAME_DAILY_USAGE) + return f"{self._location_name} {NAME_DAILY_USAGE}" @property def icon(self): @@ -107,7 +107,7 @@ class StreamLabsMonthlyUsage(StreamLabsDailyUsage): @property def name(self): """Return the name for monthly usage.""" - return "{} {}".format(self._location_name, NAME_MONTHLY_USAGE) + return f"{self._location_name} {NAME_MONTHLY_USAGE}" @property def state(self): @@ -121,7 +121,7 @@ class StreamLabsYearlyUsage(StreamLabsDailyUsage): @property def name(self): """Return the name for yearly usage.""" - return "{} {}".format(self._location_name, NAME_YEARLY_USAGE) + return f"{self._location_name} {NAME_YEARLY_USAGE}" @property def state(self): diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index bbac046d62ef97..2d5d0e8de3f3af 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -101,7 +101,7 @@ def name(self): @property def unique_id(self) -> str: """Return a unique, friendly identifier for this entity.""" - return "{0}_{1}".format(self._station, self._condition) + return f"{self._station}_{self._condition}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index fedcb3003b05c5..3cf8babf554889 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -110,7 +110,7 @@ def device_state_attributes(self): ATTR_DEPARTURE_TIME2: self._opendata.connections[2]["departure"], ATTR_START: self._opendata.from_name, ATTR_TARGET: self._opendata.to_name, - ATTR_REMAINING_TIME: "{}".format(self._remaining_time), + ATTR_REMAINING_TIME: f"{self._remaining_time}", ATTR_ATTRIBUTION: ATTRIBUTION, } return attr diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index 3775854fade01e..98965af15136b8 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -74,7 +74,7 @@ def _update_info(self): def get_swisscom_data(self): """Retrieve data from Swisscom and return parsed result.""" - url = "http://{}/ws".format(self.host) + url = f"http://{self.host}/ws" headers = {CONTENT_TYPE: "application/x-sah-ws-4-call+json"} data = """ {"service":"Devices", "method":"get", diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index f52935c02ec8f6..a758a5843472cb 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -65,7 +65,7 @@ def should_poll(self) -> bool: @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}-{}".format(self._device_data.device_id, self._device_data.mac_addr) + return f"{self._device_data.device_id}-{self._device_data.mac_addr}" @property def is_on(self) -> bool: diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 0a50eec75c2696..1258732223b01c 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -18,12 +18,10 @@ TRAYS = range(1, 6) OUTPUT_TRAYS = range(0, 6) DEFAULT_MONITORED_CONDITIONS = [] -DEFAULT_MONITORED_CONDITIONS.extend(["toner_{}".format(key) for key in TONER_COLORS]) -DEFAULT_MONITORED_CONDITIONS.extend(["drum_{}".format(key) for key in DRUM_COLORS]) -DEFAULT_MONITORED_CONDITIONS.extend(["tray_{}".format(key) for key in TRAYS]) -DEFAULT_MONITORED_CONDITIONS.extend( - ["output_tray_{}".format(key) for key in OUTPUT_TRAYS] -) +DEFAULT_MONITORED_CONDITIONS.extend([f"toner_{key}" for key in TONER_COLORS]) +DEFAULT_MONITORED_CONDITIONS.extend([f"drum_{key}" for key in DRUM_COLORS]) +DEFAULT_MONITORED_CONDITIONS.extend([f"tray_{key}" for key in TRAYS]) +DEFAULT_MONITORED_CONDITIONS.extend([f"output_tray_{key}" for key in OUTPUT_TRAYS]) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -81,16 +79,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= devices = [SyncThruMainSensor(printer, name)] for key in supp_toner: - if "toner_{}".format(key) in monitored: + if f"toner_{key}" in monitored: devices.append(SyncThruTonerSensor(printer, name, key)) for key in supp_drum: - if "drum_{}".format(key) in monitored: + if f"drum_{key}" in monitored: devices.append(SyncThruDrumSensor(printer, name, key)) for key in supp_tray: - if "tray_{}".format(key) in monitored: + if f"tray_{key}" in monitored: devices.append(SyncThruInputTraySensor(printer, name, key)) for key in supp_output_tray: - if "output_tray_{}".format(key) in monitored: + if f"output_tray_{key}" in monitored: devices.append(SyncThruOutputTraySensor(printer, name, key)) async_add_entities(devices, True) @@ -173,10 +171,10 @@ class SyncThruTonerSensor(SyncThruSensor): def __init__(self, syncthru, name, color): """Initialize the sensor.""" super().__init__(syncthru, name) - self._name = "{} Toner {}".format(name, color) + self._name = f"{name} Toner {color}" self._color = color self._unit_of_measurement = "%" - self._id_suffix = "_toner_{}".format(color) + self._id_suffix = f"_toner_{color}" def update(self): """Get the latest data from SyncThru and update the state.""" @@ -193,10 +191,10 @@ class SyncThruDrumSensor(SyncThruSensor): def __init__(self, syncthru, name, color): """Initialize the sensor.""" super().__init__(syncthru, name) - self._name = "{} Drum {}".format(name, color) + self._name = f"{name} Drum {color}" self._color = color self._unit_of_measurement = "%" - self._id_suffix = "_drum_{}".format(color) + self._id_suffix = f"_drum_{color}" def update(self): """Get the latest data from SyncThru and update the state.""" @@ -213,9 +211,9 @@ class SyncThruInputTraySensor(SyncThruSensor): def __init__(self, syncthru, name, number): """Initialize the sensor.""" super().__init__(syncthru, name) - self._name = "{} Tray {}".format(name, number) + self._name = f"{name} Tray {number}" self._number = number - self._id_suffix = "_tray_{}".format(number) + self._id_suffix = f"_tray_{number}" def update(self): """Get the latest data from SyncThru and update the state.""" @@ -234,9 +232,9 @@ class SyncThruOutputTraySensor(SyncThruSensor): def __init__(self, syncthru, name, number): """Initialize the sensor.""" super().__init__(syncthru, name) - self._name = "{} Output Tray {}".format(name, number) + self._name = f"{name} Output Tray {number}" self._number = number - self._id_suffix = "_output_tray_{}".format(number) + self._id_suffix = f"_output_tray_{number}" def update(self): """Get the latest data from SyncThru and update the state.""" diff --git a/homeassistant/components/synologydsm/sensor.py b/homeassistant/components/synologydsm/sensor.py index 17295f15250619..e19f6ada809706 100644 --- a/homeassistant/components/synologydsm/sensor.py +++ b/homeassistant/components/synologydsm/sensor.py @@ -184,7 +184,7 @@ def __init__(self, api, name, variable, variable_info, monitor_device=None): def name(self): """Return the name of the sensor, if any.""" if self.monitor_device is not None: - return "{} ({})".format(self.var_name, self.monitor_device) + return f"{self.var_name} ({self.monitor_device})" return self.var_name @property diff --git a/homeassistant/components/sytadin/sensor.py b/homeassistant/components/sytadin/sensor.py index 4296f2d5b05b39..b7c94933a39974 100644 --- a/homeassistant/components/sytadin/sensor.py +++ b/homeassistant/components/sytadin/sensor.py @@ -86,7 +86,7 @@ def __init__(self, data, name, sensor_type, option, unit): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self._option) + return f"{self._name} {self._option}" @property def state(self): From dae6895a95659629a607f27d8da46d1f4323be29 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Sep 2019 21:15:31 +0200 Subject: [PATCH 0136/3953] Use literal string interpolation in integrations X-Z (f-strings) (#26395) --- .../components/xiaomi/device_tracker.py | 2 +- .../components/xiaomi_aqara/__init__.py | 6 +- homeassistant/components/xiaomi_miio/fan.py | 2 +- homeassistant/components/xiaomi_miio/light.py | 4 +- .../components/xiaomi_miio/remote.py | 2 +- .../components/xiaomi_miio/sensor.py | 2 +- .../components/xiaomi_miio/switch.py | 4 +- homeassistant/components/xmpp/notify.py | 4 +- .../components/yamaha/media_player.py | 6 +- .../yamaha_musiccast/media_player.py | 2 +- homeassistant/components/yeelight/__init__.py | 2 +- .../components/yeelight/binary_sensor.py | 2 +- homeassistant/components/yeelight/light.py | 4 +- .../components/yeelightsunflower/light.py | 2 +- homeassistant/components/yr/sensor.py | 4 +- homeassistant/components/yweather/sensor.py | 2 +- homeassistant/components/zamg/sensor.py | 4 +- homeassistant/components/zestimate/sensor.py | 4 +- homeassistant/components/zha/api.py | 70 +++++++++---------- .../components/zha/core/channels/__init__.py | 6 +- .../components/zha/core/channels/closures.py | 8 +-- .../components/zha/core/channels/general.py | 10 +-- .../zha/core/channels/homeautomation.py | 4 +- .../components/zha/core/channels/hvac.py | 8 +-- .../components/zha/core/channels/security.py | 8 +-- homeassistant/components/zha/core/device.py | 24 +++---- .../components/zha/core/discovery.py | 6 +- homeassistant/components/zha/core/gateway.py | 12 ++-- homeassistant/components/zha/entity.py | 2 +- .../ziggo_mediabox_xl/media_player.py | 2 +- .../components/zoneminder/__init__.py | 2 +- homeassistant/components/zoneminder/sensor.py | 4 +- homeassistant/components/zoneminder/switch.py | 2 +- homeassistant/components/zwave/__init__.py | 18 +++-- homeassistant/components/zwave/node_entity.py | 2 +- homeassistant/components/zwave/util.py | 6 +- 36 files changed, 115 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index 36ce4589396f7b..dbc647f49827d1 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -143,7 +143,7 @@ def _retrieve_list(host, token, **kwargs): def _get_token(host, username, password): """Get authentication token for the given host+username+password.""" - url = "http://{}/cgi-bin/luci/api/xqsystem/login".format(host) + url = f"http://{host}/cgi-bin/luci/api/xqsystem/login" data = {"username": username, "password": password} try: res = requests.post(url, data=data, timeout=5) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index cf2411ccda50ac..6e2298e05b9782 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -232,7 +232,7 @@ def __init__(self, device, device_type, xiaomi_hub): self._state = None self._is_available = True self._sid = device["sid"] - self._name = "{}_{}".format(device_type, self._sid) + self._name = f"{device_type}_{self._sid}" self._type = device_type self._write_to_hub = xiaomi_hub.write_to_hub self._get_from_hub = xiaomi_hub.get_from_hub @@ -247,7 +247,7 @@ def __init__(self, device, device_type, xiaomi_hub): self._data_key, self._sid # pylint: disable=no-member ) else: - self._unique_id = "{}{}".format(self._type, self._sid) + self._unique_id = f"{self._type}{self._sid}" def _add_push_data_job(self, *args): self.hass.add_job(self.push_data, *args) @@ -345,7 +345,7 @@ def gateway(sid): if gateway.sid == sid: return gateway - raise vol.Invalid("Unknown gateway sid {}".format(sid)) + raise vol.Invalid(f"Unknown gateway sid {sid}") gateways = list(xiaomi.gateways.values()) kwargs = {} diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 93ca7e4bde0c6b..c6ca6db32fbce6 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -440,7 +440,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) + unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( "%s %s %s detected", model, diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index ebb5be2cc06219..3d23f1dfc98c48 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -136,7 +136,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) + unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( "%s %s %s detected", model, @@ -731,7 +731,7 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): def __init__(self, name, light, model, unique_id): """Initialize the light device.""" - name = "{} Ambient Light".format(name) + name = f"{name} Ambient Light" if unique_id is not None: unique_id = "{}-{}".format(unique_id, "ambient") super().__init__(name, light, model, unique_id) diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index d66d8ce39b1cf2..311a356870c828 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -90,7 +90,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: device_info = device.info() model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) + unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( "%s %s %s detected", model, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index ffbdf281843b08..0ebffb06fcd87e 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -52,7 +52,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= air_quality_monitor = AirQualityMonitor(host, token) device_info = air_quality_monitor.info() model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) + unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( "%s %s %s detected", model, diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 8188d7911889ca..5f79652621bab3 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -117,7 +117,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) + unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( "%s %s %s detected", model, @@ -426,7 +426,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): def __init__(self, name, plug, model, unique_id, channel_usb): """Initialize the plug switch.""" - name = "{} USB".format(name) if channel_usb else name + name = f"{name} USB" if channel_usb else name if unique_id is not None and channel_usb: unique_id = "{}-{}".format(unique_id, "usb") diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index ce22bf7a953d8b..3719113f7c9d18 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -87,12 +87,12 @@ def __init__(self, sender, resource, password, recipient, tls, verify, room, has async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - text = "{}: {}".format(title, message) if title else message + text = f"{title}: {message}" if title else message data = kwargs.get(ATTR_DATA) timeout = data.get(ATTR_TIMEOUT, XEP_0363_TIMEOUT) if data else None await async_send_message( - "{}/{}".format(self._sender, self._resource), + f"{self._sender}/{self._resource}", self._password, self._recipient, self._tls, diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index ff976c6b12fe59..e699ab74e680d9 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -114,7 +114,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for recv in rxv.find(): receivers.extend(recv.zone_controllers()) else: - ctrl_url = "http://{}:80/YamahaRemoteControl/ctrl".format(host) + ctrl_url = f"http://{host}:80/YamahaRemoteControl/ctrl" receivers = rxv.RXV(ctrl_url, name).zone_controllers() devices = [] @@ -276,7 +276,7 @@ def source_list(self): @property def zone_id(self): """Return a zone_id to ensure 1 media player per zone.""" - return "{0}:{1}".format(self.receiver.ctrl_url, self._zone) + return f"{self.receiver.ctrl_url}:{self._zone}" @property def supported_features(self): @@ -410,6 +410,6 @@ def media_title(self): # If both song and station is available, print both, otherwise # just the one we have. if song and station: - return "{}: {}".format(station, song) + return f"{station}: {song}" return song or station diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index d82b093ca7e8e0..38e606a0962cd1 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -128,7 +128,7 @@ def __init__(self, recv, zone): @property def name(self): """Return the name of the device.""" - return "{} ({})".format(self._name, self._zone.zone_id) + return f"{self._name} ({self._zone.zone_id})" @property def state(self): diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 172d66f9bf5c0d..431c34aa06ef80 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -26,7 +26,7 @@ DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = "yeelight_{}_data_updated" -DEVICE_INITIALIZED = "{}_device_initialized".format(DOMAIN) +DEVICE_INITIALIZED = f"{DOMAIN}_device_initialized" DEFAULT_NAME = "Yeelight" DEFAULT_TRANSITION = 350 diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index 0a6e021df94aa0..da39152e9cae63 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -48,7 +48,7 @@ def should_poll(self): @property def name(self): """Return the name of the sensor.""" - return "{} nightlight".format(self._device.name) + return f"{self._device.name} nightlight" @property def is_on(self): diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index a3d5d2dec2e863..8601e0e16322c1 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -165,7 +165,7 @@ def _wrap(self, *args, **kwargs): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight bulbs.""" - data_key = "{}_lights".format(DATA_YEELIGHT) + data_key = f"{DATA_YEELIGHT}_lights" if not discovery_info: return @@ -673,7 +673,7 @@ def __init__(self, *args, **kwargs): @property def name(self) -> str: """Return the name of the device if any.""" - return "{} ambilight".format(self.device.name) + return f"{self.device.name} ambilight" def _get_property(self, prop, default=None): bg_prop = self.PROPERTIES_MAPPING.get(prop) diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 896daac96c40de..fa836f2776ffa5 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -50,7 +50,7 @@ def __init__(self, light): @property def name(self): """Return the display name of this light.""" - return "sunflower_{}".format(self._light.zid) + return f"sunflower_{self._light.zid}" @property def available(self): diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index 15d966d1354f41..3d8c63621be977 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -106,7 +106,7 @@ def __init__(self, name, sensor_type): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): @@ -168,7 +168,7 @@ def try_again(err: str): with async_timeout.timeout(10): resp = await websession.get(self._url, params=self._urlparams) if resp.status != 200: - try_again("{} returned {}".format(resp.url, resp.status)) + try_again(f"{resp.url} returned {resp.status}") return text = await resp.text() diff --git a/homeassistant/components/yweather/sensor.py b/homeassistant/components/yweather/sensor.py index d23b49a0230624..4dc236998724ee 100644 --- a/homeassistant/components/yweather/sensor.py +++ b/homeassistant/components/yweather/sensor.py @@ -108,7 +108,7 @@ def __init__(self, weather_data, name, forecast, sensor_type): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._client, self._name) + return f"{self._client} {self._name}" @property def state(self): diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 52f6617c397a76..9eea1f6612c8d1 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -124,7 +124,7 @@ def __init__(self, probe, variable, name): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self.variable) + return f"{self.client_name} {self.variable}" @property def state(self): @@ -212,7 +212,7 @@ def update(self): } break else: - raise ValueError("No weather data for station {}".format(self._station_id)) + raise ValueError(f"No weather data for station {self._station_id}") def get_data(self, variable): """Get the data.""" diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 0b5c75934b6bdb..703e3bf25a0cee 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -19,7 +19,7 @@ DEFAULT_NAME = "Zestimate" NAME = "zestimate" -ZESTIMATE = "{}:{}".format(DEFAULT_NAME, NAME) +ZESTIMATE = f"{DEFAULT_NAME}:{NAME}" ICON = "mdi:home-variant" @@ -74,7 +74,7 @@ def unique_id(self): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self.address) + return f"{self._name} {self.address}" @property def state(self): diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 77b1b36fa36d6c..be079e83fa6bb0 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -283,10 +283,10 @@ async def websocket_device_cluster_attributes(hass, connection, msg): ) _LOGGER.debug( "Requested attributes for: %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), - "{}: [{}]".format(RESPONSE, cluster_attributes), + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", + f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", + f"{RESPONSE}: [{cluster_attributes}]", ) connection.send_result(msg[ID], cluster_attributes) @@ -337,10 +337,10 @@ async def websocket_device_cluster_commands(hass, connection, msg): ) _LOGGER.debug( "Requested commands for: %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), - "{}: [{}]".format(RESPONSE, cluster_commands), + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", + f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", + f"{RESPONSE}: [{cluster_commands}]", ) connection.send_result(msg[ID], cluster_commands) @@ -381,11 +381,11 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): ) _LOGGER.debug( "Read attribute for: %s %s %s %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), - "{}: [{}]".format(ATTR_ATTRIBUTE, attribute), - "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", + f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", + f"{ATTR_ATTRIBUTE}: [{attribute}]", + f"{ATTR_MANUFACTURER}: [{manufacturer}]", "{}: [{}]".format(RESPONSE, str(success.get(attribute))), "{}: [{}]".format("failure", failure), ) @@ -411,7 +411,7 @@ async def websocket_get_bindable_devices(hass, connection, msg): _LOGGER.debug( "Get bindable devices: %s %s", - "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee), + f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", "{}: [{}]".format("bindable devices:", devices), ) @@ -435,8 +435,8 @@ async def websocket_bind_devices(hass, connection, msg): await async_binding_operation(zha_gateway, source_ieee, target_ieee, BIND_REQUEST) _LOGGER.info( "Issue bind devices: %s %s", - "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee), - "{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee), + f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", + f"{ATTR_TARGET_IEEE}: [{target_ieee}]", ) @@ -457,8 +457,8 @@ async def websocket_unbind_devices(hass, connection, msg): await async_binding_operation(zha_gateway, source_ieee, target_ieee, UNBIND_REQUEST) _LOGGER.info( "Issue unbind devices: %s %s", - "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee), - "{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee), + f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", + f"{ATTR_TARGET_IEEE}: [{target_ieee}]", ) @@ -482,8 +482,8 @@ async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operati _LOGGER.debug( "processing binding operation for: %s %s %s", - "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee), - "{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee), + f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", + f"{ATTR_TARGET_IEEE}: [{target_ieee}]", "{}: {}".format("cluster", cluster_pair.source_cluster.cluster_id), ) bind_tasks.append( @@ -551,13 +551,13 @@ async def set_zigbee_cluster_attributes(service): ) _LOGGER.debug( "Set attribute for: %s %s %s %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), - "{}: [{}]".format(ATTR_ATTRIBUTE, attribute), - "{}: [{}]".format(ATTR_VALUE, value), - "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), - "{}: [{}]".format(RESPONSE, response), + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", + f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", + f"{ATTR_ATTRIBUTE}: [{attribute}]", + f"{ATTR_VALUE}: [{value}]", + f"{ATTR_MANUFACTURER}: [{manufacturer}]", + f"{RESPONSE}: [{response}]", ) hass.helpers.service.async_register_admin_service( @@ -593,14 +593,14 @@ async def issue_zigbee_cluster_command(service): ) _LOGGER.debug( "Issue command for: %s %s %s %s %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), - "{}: [{}]".format(ATTR_COMMAND, command), - "{}: [{}]".format(ATTR_COMMAND_TYPE, command_type), - "{}: [{}]".format(ATTR_ARGS, args), - "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), - "{}: [{}]".format(RESPONSE, response), + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", + f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", + f"{ATTR_COMMAND}: [{command}]", + f"{ATTR_COMMAND_TYPE}: [{command_type}]", + f"{ATTR_ARGS}: [{args}]", + f"{ATTR_MANUFACTURER}: [{manufacturer}]", + f"{RESPONSE}: [{response}]", ) hass.helpers.service.async_register_admin_service( diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 20756f26b729f7..9e3b69a80df07c 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -87,7 +87,7 @@ def __init__(self, cluster, device): self._channel_name = cluster.ep_attribute if self.CHANNEL_NAME: self._channel_name = self.CHANNEL_NAME - self._generic_id = "channel_0x{:04x}".format(cluster.cluster_id) + self._generic_id = f"channel_0x{cluster.cluster_id:04x}" self._cluster = cluster self._zha_device = device self._unique_id = "{}:{}:0x{:04x}".format( @@ -299,9 +299,7 @@ def attribute_updated(self, attrid, value): """Handle attribute updates on this cluster.""" if attrid == self.value_attribute: async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 0559c4a1f76056..378be778e6f565 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -30,9 +30,7 @@ async def async_update(self): result = await self.get_attribute_value("lock_state", from_cache=True) async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - result, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", result ) @callback @@ -44,9 +42,7 @@ def attribute_updated(self, attrid, value): ) if attrid == self._value_attribute: async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 6a828ef1ad8a94..f67ee2fb75ac77 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -198,7 +198,7 @@ def attribute_updated(self, attrid, value): def dispatch_level_change(self, command, level): """Dispatch level change.""" async_dispatcher_send( - self._zha_device.hass, "{}_{}".format(self.unique_id, command), level + self._zha_device.hass, f"{self.unique_id}_{command}", level ) async def async_initialize(self, from_cache): @@ -284,9 +284,7 @@ def attribute_updated(self, attrid, value): """Handle attribute updates on this cluster.""" if attrid == self.ON_OFF: async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) self._state = bool(value) @@ -355,9 +353,7 @@ def attribute_updated(self, attrid, value): attr_id = attr if attrid == attr_id: async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 198eec67a469ca..7a5f0161fb4385 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -72,9 +72,7 @@ async def async_update(self): # This is a polling channel. Don't allow cache. result = await self.get_attribute_value("active_power", from_cache=False) async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - result, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", result ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 46d9ffb52e5e7f..2f6e6c1b3e8390 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -48,9 +48,7 @@ async def async_update(self): result = await self.get_attribute_value("fan_mode", from_cache=True) async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - result, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", result ) @callback @@ -62,9 +60,7 @@ def attribute_updated(self, attrid, value): ) if attrid == self._value_attribute: async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index cac93ea7214b22..cd407cfc416b68 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -43,9 +43,7 @@ def cluster_command(self, tsn, command_id, args): if command_id == 0: state = args[0] & 3 async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - state, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", state ) self.debug("Updated alarm state: %s", state) elif command_id == 1: @@ -91,9 +89,7 @@ def attribute_updated(self, attrid, value): if attrid == 2: value = value & 3 async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1c22b41ce86af7..1db4aafeeb9a80 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -102,7 +102,7 @@ def __init__(self, hass, zigpy_device, zha_gateway): @property def name(self): """Return device name.""" - return "{} {}".format(self.manufacturer, self.model) + return f"{self.manufacturer} {self.model}" @property def ieee(self): @@ -461,10 +461,10 @@ async def write_zigbee_attribute( except DeliveryError as exc: self.debug( "failed to set attribute: %s %s %s %s %s", - "{}: {}".format(ATTR_VALUE, value), - "{}: {}".format(ATTR_ATTRIBUTE, attribute), - "{}: {}".format(ATTR_CLUSTER_ID, cluster_id), - "{}: {}".format(ATTR_ENDPOINT_ID, endpoint_id), + f"{ATTR_VALUE}: {value}", + f"{ATTR_ATTRIBUTE}: {attribute}", + f"{ATTR_CLUSTER_ID}: {cluster_id}", + f"{ATTR_ENDPOINT_ID}: {endpoint_id}", exc, ) return None @@ -493,13 +493,13 @@ async def issue_cluster_command( self.debug( "Issued cluster command: %s %s %s %s %s %s %s", - "{}: {}".format(ATTR_CLUSTER_ID, cluster_id), - "{}: {}".format(ATTR_COMMAND, command), - "{}: {}".format(ATTR_COMMAND_TYPE, command_type), - "{}: {}".format(ATTR_ARGS, args), - "{}: {}".format(ATTR_CLUSTER_ID, cluster_type), - "{}: {}".format(ATTR_MANUFACTURER, manufacturer), - "{}: {}".format(ATTR_ENDPOINT_ID, endpoint_id), + f"{ATTR_CLUSTER_ID}: {cluster_id}", + f"{ATTR_COMMAND}: {command}", + f"{ATTR_COMMAND_TYPE}: {command_type}", + f"{ATTR_ARGS}: {args}", + f"{ATTR_CLUSTER_ID}: {cluster_type}", + f"{ATTR_MANUFACTURER}: {manufacturer}", + f"{ATTR_ENDPOINT_ID}: {endpoint_id}", ) return response diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index c4489164b0c481..5a5ffb34ab13ea 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -62,7 +62,7 @@ def async_process_endpoint( component = None profile_clusters = [] - device_key = "{}-{}".format(device.ieee, endpoint_id) + device_key = f"{device.ieee}-{endpoint_id}" node_config = {} if CONF_DEVICE_CONFIG in config: node_config = config[CONF_DEVICE_CONFIG].get(device_key, {}) @@ -281,12 +281,12 @@ def _async_handle_single_cluster_match( channels = [] _async_create_cluster_channel(cluster, zha_device, is_new_join, channels=channels) - cluster_key = "{}-{}".format(device_key, cluster.cluster_id) + cluster_key = f"{device_key}-{cluster.cluster_id}" discovery_info = { "unique_id": cluster_key, "zha_device": zha_device, "channels": channels, - "entity_suffix": "_{}".format(cluster.cluster_id), + "entity_suffix": f"_{cluster.cluster_id}", "component": component, } diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 3d8c3e8fd9086b..be09312f6931cb 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -339,7 +339,7 @@ async def async_device_initialized(self, device): _LOGGER.debug( "device - %s entering async_device_initialized - is_new_join: %s", - "0x{:04x}:{}".format(device.nwk, device.ieee), + f"0x{device.nwk:04x}:{device.ieee}", zha_device.status is not DeviceStatus.INITIALIZED, ) @@ -348,13 +348,13 @@ async def async_device_initialized(self, device): # new nwk or device was physically reset and added again without being removed _LOGGER.debug( "device - %s has been reset and readded or its nwk address changed", - "0x{:04x}:{}".format(device.nwk, device.ieee), + f"0x{device.nwk:04x}:{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), + f"0x{device.nwk:04x}:{device.ieee}", ) await self._async_device_joined(device, zha_device) @@ -413,9 +413,9 @@ async def async_device_restored(self, device): # to update it now _LOGGER.debug( "attempting to request fresh state for device - %s %s %s", - "0x{:04x}:{}".format(zha_device.nwk, zha_device.ieee), + f"0x{zha_device.nwk:04x}:{zha_device.ieee}", zha_device.name, - "with power source: {}".format(zha_device.power_source), + f"with power source: {zha_device.power_source}", ) await zha_device.async_initialize(from_cache=False) else: @@ -427,7 +427,7 @@ async def async_device_restored(self, device): 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), + f"0x{zha_device.nwk:04x}:{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() diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 694f7b25695258..00c3942358e1fe 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -189,7 +189,7 @@ async def async_accept_signal(self, channel, signal, func, signal_override=False unsub = async_dispatcher_connect(self.hass, signal, func) else: unsub = async_dispatcher_connect( - self.hass, "{}_{}".format(channel.unique_id, signal), func + self.hass, f"{channel.unique_id}_{signal}", func ) self._unsubs.append(unsub) diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index f9e4e1ac49dbee..a5f8b38ac3775a 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -206,5 +206,5 @@ def select_source(self, source): if digits is None: return - self.send_keys(["NUM_{}".format(digit) for digit in str(digits)]) + self.send_keys([f"NUM_{digit}" for digit in str(digits)]) self._state = STATE_PLAYING diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index 1ce6b87a88f912..a116cc31891e5d 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -64,7 +64,7 @@ def setup(hass, config): schema = "http" host_name = conf[CONF_HOST] - server_origin = "{}://{}".format(schema, host_name) + server_origin = f"{schema}://{host_name}" zm_client = ZoneMinder( server_origin, conf.get(CONF_USERNAME), diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index e2ab4b0905fc20..bfcfcb8f907a8e 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -68,7 +68,7 @@ def __init__(self, monitor): @property def name(self): """Return the name of the sensor.""" - return "{} Status".format(self._monitor.name) + return f"{self._monitor.name} Status" @property def state(self): @@ -105,7 +105,7 @@ def __init__(self, monitor, include_archived, sensor_type): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._monitor.name, self.time_period.title) + return f"{self._monitor.name} {self.time_period.title}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index d22ef611b35303..d2d761aab1e60c 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -53,7 +53,7 @@ def __init__(self, monitor, on_state, off_state): @property def name(self): """Return the name of the switch.""" - return "{} State".format(self._monitor.name) + return f"{self._monitor.name} State" def update(self): """Update the switch value.""" diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index bc40d46b8ba6ef..223ce810d7cefe 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -478,10 +478,10 @@ def _on_timeout(sec): def node_removed(node): node_id = node.node_id - node_key = "node-{}".format(node_id) + node_key = f"node-{node_id}" _LOGGER.info("Node Removed: %s", hass.data[DATA_DEVICES][node_key]) for key in list(hass.data[DATA_DEVICES]): - if not key.startswith("{}-".format(node_id)): + if not key.startswith(f"{node_id}-"): continue entity = hass.data[DATA_DEVICES][key] @@ -586,11 +586,11 @@ async def rename_node(service): update_ids = service.data.get(const.ATTR_UPDATE_IDS) # We want to rename the device, the node entity, # and all the contained entities - node_key = "node-{}".format(node_id) + node_key = f"node-{node_id}" entity = hass.data[DATA_DEVICES][node_key] await entity.node_renamed(update_ids) for key in list(hass.data[DATA_DEVICES]): - if not key.startswith("{}-".format(node_id)): + if not key.startswith(f"{node_id}-"): continue entity = hass.data[DATA_DEVICES][key] await entity.value_renamed(update_ids) @@ -607,7 +607,7 @@ async def rename_value(service): "Renamed Z-Wave value (Node %d Value %d) to %s", node_id, value_id, name ) update_ids = service.data.get(const.ATTR_UPDATE_IDS) - value_key = "{}-{}".format(node_id, value_id) + value_key = f"{node_id}-{value_id}" entity = hass.data[DATA_DEVICES][value_key] await entity.value_renamed(update_ids) @@ -1109,7 +1109,7 @@ def _check_entity_ready(self): if polling_intensity: self.primary.enable_poll(polling_intensity) - platform = import_module(".{}".format(component), __name__) + platform = import_module(f".{component}", __name__) device = platform.get_device( node=self._node, values=self, node_config=node_config, hass=self._hass @@ -1149,9 +1149,7 @@ async def discover_device(component, device): self._hass.data[DATA_DEVICES][device.unique_id] = device if component in SUPPORTED_PLATFORMS: - async_dispatcher_send( - self._hass, "zwave_new_{}".format(component), device - ) + async_dispatcher_send(self._hass, f"zwave_new_{component}", device) else: await discovery.async_load_platform( self._hass, @@ -1316,4 +1314,4 @@ def _compute_unique_id(self): def compute_value_unique_id(node, value): """Compute unique_id a value would get if it were to get one.""" - return "{}-{}".format(node.node_id, value.object_id) + return f"{node.node_id}-{value.object_id}" diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index c60314d35798a5..66c3452f7c881d 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -348,5 +348,5 @@ def device_state_attributes(self): def _compute_unique_id(self): if is_node_parsed(self.node) or self.node.is_ready: - return "node-{}".format(self.node_id) + return f"node-{self.node_id}" return None diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index 1e7b77d2b38b02..da8fa37f44fbdc 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -91,8 +91,8 @@ def check_value_schema(value, schema): def node_name(node): """Return the name of the node.""" if is_node_parsed(node): - return node.name or "{} {}".format(node.manufacturer_name, node.product_name) - return "Unknown Node {}".format(node.node_id) + return node.name or f"{node.manufacturer_name} {node.product_name}" + return f"Unknown Node {node.node_id}" def node_device_id_and_name(node, instance=1): @@ -100,7 +100,7 @@ def node_device_id_and_name(node, instance=1): name = node_name(node) if instance == 1: return ((const.DOMAIN, node.node_id), name) - name = "{} ({})".format(name, instance) + name = f"{name} ({instance})" return ((const.DOMAIN, node.node_id, instance), name) From e7ccb6f0473721c8a598c233daac184a1194d09e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Sep 2019 22:09:21 +0200 Subject: [PATCH 0137/3953] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 36 ++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index 05514ae3273485..d10d562c2b571c 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -6,8 +6,21 @@ trigger: include: - dev pr: none +schedules: + - cron: "0 1 * * *" + displayName: "translation update" + branches: + include: + - dev + always: true variables: - - group: translation +- group: translation +resources: + repositories: + - repository: azure + type: github + name: 'home-assistant/ci-azure' + endpoint: 'home-assistant' jobs: @@ -26,3 +39,24 @@ jobs: ./script/translations_upload displayName: 'Upload Translation' + +- job: 'Download' + condition: eq(variables['Build.Reason'], 'Schedule') + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: '3.7' + - template: templates/azp-step-git-init.yaml@azure + - script: | + export LOKALISE_TOKEN="$(lokaliseToken)" + export AZURE_BRANCH="$(Build.SourceBranchName)" + + ./script/translations_download + displayName: 'Download Translation' + - script: | + commit -am "[CI] Translation update" + commit push + displayName: 'Update translation' From b6cd5ab27b90425771ee0685146c342989b9a1e0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Sep 2019 22:15:25 +0200 Subject: [PATCH 0138/3953] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index d10d562c2b571c..a183589795e176 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -57,6 +57,6 @@ jobs: ./script/translations_download displayName: 'Download Translation' - script: | - commit -am "[CI] Translation update" - commit push + git commit -am "[CI] Translation update" + git commit push displayName: 'Update translation' From 9035efee10909312072a4f9831060b522054ee6c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Sep 2019 01:02:42 +0200 Subject: [PATCH 0139/3953] Fixes invalid JSON files and whitespace corrections in YAML files (#26396) --- .devcontainer/devcontainer.json | 63 +++---- .vscode/tasks.json | 178 +++++++++--------- .../components/ihc/ihc_auto_setup.yaml | 2 +- homeassistant/components/knx/services.yaml | 4 +- homeassistant/components/lcn/services.yaml | 15 +- .../persistent_notification/services.yaml | 2 +- homeassistant/components/point/strings.json | 2 +- homeassistant/components/solax/manifest.json | 1 - .../components/utility_meter/services.yaml | 6 +- homeassistant/components/zha/services.yaml | 14 +- tests/fixtures/ring_oauth.json | 4 +- 11 files changed, 142 insertions(+), 149 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 22bd4384b23e5e..e747e8fdb9858e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,35 +1,32 @@ // See https://aka.ms/vscode-remote/devcontainer.json for format details. { - "name": "Home Assistant Dev", - "context": "..", - "dockerFile": "../Dockerfile.dev", - "postCreateCommand": "pip3 install -e .", - "appPort": 8123, - "runArgs": [ - "-e", - "GIT_EDITOR=\"code --wait\"" - ], - "extensions": [ - "ms-python.python", - "ms-azure-devops.azure-pipelines", - "redhat.vscode-yaml" - ], - "settings": { - "python.pythonPath": "/usr/local/bin/python", - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true, - "terminal.integrated.shell.linux": "/bin/bash", - "yaml.customTags": [ - "!secret scalar", - "!include_dir_named scalar", - "!include_dir_list scalar", - "!include_dir_merge_list scalar", - "!include_dir_merge_named scalar" - ] - } -} \ No newline at end of file + "name": "Home Assistant Dev", + "context": "..", + "dockerFile": "../Dockerfile.dev", + "postCreateCommand": "pip3 install -e .", + "appPort": 8123, + "runArgs": ["-e", "GIT_EDITOR=\"code --wait\""], + "extensions": [ + "ms-python.python", + "ms-azure-devops.azure-pipelines", + "redhat.vscode-yaml" + ], + "settings": { + "python.pythonPath": "/usr/local/bin/python", + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true, + "terminal.integrated.shell.linux": "/bin/bash", + "yaml.customTags": [ + "!secret scalar", + "!include_dir_named scalar", + "!include_dir_list scalar", + "!include_dir_merge_list scalar", + "!include_dir_merge_named scalar" + ] + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e6f38920d7d77e..f57c182809b93e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,92 +1,90 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "Preview", - "type": "shell", - "command": "hass -c ./config", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Pytest", - "type": "shell", - "command": "pytest --timeout=10 tests", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Flake8", - "type": "shell", - "command": "flake8 homeassistant tests", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Pylint", - "type": "shell", - "command": "pylint homeassistant", - "dependsOn": [ - "Install all Requirements" - ], - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Generate Requirements", - "type": "shell", - "command": "./script/gen_requirements_all.py", - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Install all Requirements", - "type": "shell", - "command": "pip3 install -r requirements_all.txt -c homeassistant/package_constraints.txt", - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - } - ] + "version": "2.0.0", + "tasks": [ + { + "label": "Preview", + "type": "shell", + "command": "hass -c ./config", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Pytest", + "type": "shell", + "command": "pytest --timeout=10 tests", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Flake8", + "type": "shell", + "command": "flake8 homeassistant tests", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Pylint", + "type": "shell", + "command": "pylint homeassistant", + "dependsOn": ["Install all Requirements"], + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Generate Requirements", + "type": "shell", + "command": "./script/gen_requirements_all.py", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Install all Requirements", + "type": "shell", + "command": "pip3 install -r requirements_all.txt -c homeassistant/package_constraints.txt", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + } + ] } diff --git a/homeassistant/components/ihc/ihc_auto_setup.yaml b/homeassistant/components/ihc/ihc_auto_setup.yaml index 81d5bf37977dff..0495ed58458416 100644 --- a/homeassistant/components/ihc/ihc_auto_setup.yaml +++ b/homeassistant/components/ihc/ihc_auto_setup.yaml @@ -1,6 +1,6 @@ # IHC auto setup configuration. # To customize this, copy this file to the home assistant configuration -# folder and make your changes. +# folder and make your changes. binary_sensor: # Magnet contact diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 5b751bac17c15d..5faaf0678d1497 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -1,9 +1,9 @@ send: description: "Send arbitrary data directly to the KNX bus." fields: - address: + address: description: "Group address(es) to write to." example: "1/1/0" - payload: + 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]" diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml index b8f4fbb20a7dd9..80c636577f8cc4 100644 --- a/homeassistant/components/lcn/services.yaml +++ b/homeassistant/components/lcn/services.yaml @@ -14,7 +14,7 @@ output_abs: example: 50 transition: description: Transition time in seconds - example: 5 + example: 5 output_rel: description: Set relative brightness of output port in percent. @@ -30,7 +30,7 @@ output_rel: example: 50 transition: description: Transition time in seconds - example: 5 + example: 5 output_toggle: description: Toggle output port. @@ -43,7 +43,7 @@ output_toggle: example: "output1" transition: description: Transition time in seconds - example: 5 + example: 5 relays: description: Set the relays status. @@ -72,7 +72,7 @@ led: - off - blink - flicker - + var_abs: description: Set absolute value of a variable or setpoint. fields: @@ -88,7 +88,7 @@ var_abs: unit_of_measurement: description: Unit of value example: 'celsius' - + var_reset: description: Reset value of variable or setpoint. fields: @@ -98,7 +98,7 @@ var_reset: variable: description: Variable or setpoint name example: 'var1' - + var_rel: description: Shift value of a variable, setpoint or threshold. fields: @@ -188,7 +188,7 @@ dyn_text: text: description: Text to send (up to 60 characters encoded as UTF-8) example: 'text up to 60 characters' - + pck: description: Send arbitrary PCK command. fields: @@ -198,4 +198,3 @@ pck: pck: description: PCK command (without address header) example: 'PIN4' - \ No newline at end of file diff --git a/homeassistant/components/persistent_notification/services.yaml b/homeassistant/components/persistent_notification/services.yaml index 496ab9199c3487..d026896a7c5d0c 100644 --- a/homeassistant/components/persistent_notification/services.yaml +++ b/homeassistant/components/persistent_notification/services.yaml @@ -17,7 +17,7 @@ dismiss: notification_id: description: Target ID of the notification, which should be removed. [Required] example: 1234 - + mark_read: description: Mark a notification read. fields: diff --git a/homeassistant/components/point/strings.json b/homeassistant/components/point/strings.json index 642a61a5f9d3fe..e5491a8bbee733 100644 --- a/homeassistant/components/point/strings.json +++ b/homeassistant/components/point/strings.json @@ -29,4 +29,4 @@ "authorize_url_fail": "Unknown error generating an authorize url." } } -} +} diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 14598607adacad..52e50ab47998a5 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -8,4 +8,3 @@ "dependencies": [], "codeowners": ["@squishykid"] } - \ No newline at end of file diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index 7c09117d48f4a6..5437f4b83a69b8 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -4,21 +4,21 @@ reset: description: Resets the counter of an utility meter. fields: entity_id: - description: Name(s) of the utility meter to reset + description: Name(s) of the utility meter to reset example: 'utility_meter.energy' next_tariff: description: Changes the tariff to the next one. fields: entity_id: - description: Name(s) of entities to reset + description: Name(s) of entities to reset example: 'utility_meter.energy' select_tariff: description: selects the current tariff of an utility meter. fields: entity_id: - description: Name of the entity to set the tariff for + description: Name of the entity to set the tariff for example: 'utility_meter.energy' tariff: description: Name of the tariff to switch to diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index 048054077f84db..ffd5aa21472c83 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -18,19 +18,19 @@ remove: example: "00:0d:6f:00:05:7d:2d:34" reconfigure_device: - description: >- - Reconfigure ZHA device (heal device). Use this if you are having issues + description: >- + Reconfigure ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery powered device please ensure it is awake and accepting commands when you use this - service. + service. fields: ieee_address: description: IEEE address of the device to reconfigure example: "00:0d:6f:00:05:7d:2d:34" set_zigbee_cluster_attribute: - description: >- - Set attribute value for the specified cluster on the specified entity. + description: >- + Set attribute value for the specified cluster on the specified entity. fields: ieee: description: IEEE address for the device @@ -55,8 +55,8 @@ set_zigbee_cluster_attribute: example: 0x00FC issue_zigbee_cluster_command: - description: >- - Issue command on the specified cluster on the specified entity. + description: >- + Issue command on the specified cluster on the specified entity. fields: ieee: description: IEEE address for the device diff --git a/tests/fixtures/ring_oauth.json b/tests/fixtures/ring_oauth.json index 5e69ddde065272..2dbc78c48d2eae 100644 --- a/tests/fixtures/ring_oauth.json +++ b/tests/fixtures/ring_oauth.json @@ -1,8 +1,8 @@ { - "access_token": "eyJ0eWfvEQwqfJNKyQ9999", + "access_token": "eyJ0eWfvEQwqfJNKyQ9999", "token_type": "bearer", "expires_in": 3600, "refresh_token": "67695a26bdefc1ac8999", - "scope": "client", + "scope": "client", "created_at": 1529099870 } From 757482ee853fc22b39e05696167ecdead98aba99 Mon Sep 17 00:00:00 2001 From: John Luetke Date: Tue, 3 Sep 2019 16:18:06 -0700 Subject: [PATCH 0140/3953] Refactor pihole integration (#25837) * Adds tests for pi_hole integration * Refactor pi_hole component to an integration supporting multiple platforms * Adds mock of Hole dependency * Aborts platform setup when discovery_info is none * Removes use of monitored_conditions * Adds integration setup test * Removes PlatformNotReady check * Adds sensor test * Code review updates * Refactor tests to assert state through hass * Reorder imports --- homeassistant/components/pi_hole/__init__.py | 95 ++++++++++++++ homeassistant/components/pi_hole/const.py | 43 ++++++ homeassistant/components/pi_hole/sensor.py | 130 +++---------------- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/pi_hole/__init__.py | 1 + tests/components/pi_hole/test_init.py | 99 ++++++++++++++ 7 files changed, 260 insertions(+), 112 deletions(-) create mode 100644 homeassistant/components/pi_hole/const.py create mode 100644 tests/components/pi_hole/__init__.py create mode 100644 tests/components/pi_hole/test_init.py diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 432e0f3fa1163f..ffc9827eed41fe 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -1 +1,96 @@ """The pi_hole component.""" +import logging + +import voluptuous as vol +from hole import Hole +from hole.exceptions import HoleError + +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +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.util import Throttle + +from .const import ( + DOMAIN, + CONF_LOCATION, + DEFAULT_HOST, + DEFAULT_LOCATION, + DEFAULT_NAME, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, + MIN_TIME_BETWEEN_UPDATES, +) + +LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the pi_hole integration.""" + + conf = config[DOMAIN] + name = conf[CONF_NAME] + host = conf[CONF_HOST] + use_tls = conf[CONF_SSL] + verify_tls = conf[CONF_VERIFY_SSL] + location = conf[CONF_LOCATION] + + LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) + + session = async_get_clientsession(hass, True) + pi_hole = PiHoleData( + Hole( + host, + hass.loop, + session, + location=location, + tls=use_tls, + verify_tls=verify_tls, + ), + name, + ) + + await pi_hole.async_update() + + hass.data[DOMAIN] = pi_hole + + hass.async_create_task(async_load_platform(hass, SENSOR_DOMAIN, DOMAIN, {}, config)) + + return True + + +class PiHoleData: + """Get the latest data and update the states.""" + + def __init__(self, api, name): + """Initialize the data object.""" + self.api = api + self.name = name + self.available = True + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Get the latest data from the Pi-hole.""" + + try: + await self.api.get_data() + self.available = True + except HoleError: + LOGGER.error("Unable to fetch data from Pi-hole") + self.available = False diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py new file mode 100644 index 00000000000000..ba83bf1d805334 --- /dev/null +++ b/homeassistant/components/pi_hole/const.py @@ -0,0 +1,43 @@ +"""Constants for the pi_hole intergration.""" +from datetime import timedelta + +DOMAIN = "pi_hole" + +CONF_LOCATION = "location" + +DEFAULT_HOST = "pi.hole" +DEFAULT_LOCATION = "admin" +DEFAULT_METHOD = "GET" +DEFAULT_NAME = "Pi-Hole" +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True + +ATTR_BLOCKED_DOMAINS = "domains_blocked" + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) + +SENSOR_DICT = { + "ads_blocked_today": ["Ads Blocked Today", "ads", "mdi:close-octagon-outline"], + "ads_percentage_today": [ + "Ads Percentage Blocked Today", + "%", + "mdi:close-octagon-outline", + ], + "clients_ever_seen": ["Seen Clients", "clients", "mdi:account-outline"], + "dns_queries_today": [ + "DNS Queries Today", + "queries", + "mdi:comment-question-outline", + ], + "domains_being_blocked": ["Domains Blocked", "domains", "mdi:block-helper"], + "queries_cached": ["DNS Queries Cached", "queries", "mdi:comment-question-outline"], + "queries_forwarded": [ + "DNS Queries Forwarded", + "queries", + "mdi:comment-question-outline", + ], + "unique_clients": ["DNS Unique Clients", "clients", "mdi:account-outline"], + "unique_domains": ["DNS Unique Domains", "domains", "mdi:domain"], +} + +SENSOR_LIST = list(SENSOR_DICT) diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 9c41c20fd637e9..4e80e9767a6fb1 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -1,100 +1,27 @@ """Support for getting statistical data from a Pi-hole system.""" -from datetime import timedelta import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_SSL, - CONF_VERIFY_SSL, -) -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -ATTR_BLOCKED_DOMAINS = "domains_blocked" -ATTR_PERCENTAGE_TODAY = "percentage_today" -ATTR_QUERIES_TODAY = "queries_today" - -CONF_LOCATION = "location" -DEFAULT_HOST = "localhost" - -DEFAULT_LOCATION = "admin" -DEFAULT_METHOD = "GET" -DEFAULT_NAME = "Pi-Hole" -DEFAULT_SSL = False -DEFAULT_VERIFY_SSL = True - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) - -MONITORED_CONDITIONS = { - "ads_blocked_today": ["Ads Blocked Today", "ads", "mdi:close-octagon-outline"], - "ads_percentage_today": [ - "Ads Percentage Blocked Today", - "%", - "mdi:close-octagon-outline", - ], - "clients_ever_seen": ["Seen Clients", "clients", "mdi:account-outline"], - "dns_queries_today": [ - "DNS Queries Today", - "queries", - "mdi:comment-question-outline", - ], - "domains_being_blocked": ["Domains Blocked", "domains", "mdi:block-helper"], - "queries_cached": ["DNS Queries Cached", "queries", "mdi:comment-question-outline"], - "queries_forwarded": [ - "DNS Queries Forwarded", - "queries", - "mdi:comment-question-outline", - ], - "unique_clients": ["DNS Unique Clients", "clients", "mdi:account-outline"], - "unique_domains": ["DNS Unique Domains", "domains", "mdi:domain"], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(CONF_MONITORED_CONDITIONS, default=["ads_blocked_today"]): vol.All( - cv.ensure_list, [vol.In(MONITORED_CONDITIONS)] - ), - } -) - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Pi-hole sensor.""" - from hole import Hole +from .const import ( + DOMAIN as PIHOLE_DOMAIN, + ATTR_BLOCKED_DOMAINS, + SENSOR_LIST, + SENSOR_DICT, +) - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - use_tls = config.get(CONF_SSL) - location = config.get(CONF_LOCATION) - verify_tls = config.get(CONF_VERIFY_SSL) +LOGGER = logging.getLogger(__name__) - session = async_get_clientsession(hass, verify_tls) - pi_hole = PiHoleData(Hole(host, hass.loop, session, location=location, tls=use_tls)) - await pi_hole.async_update() +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the pi-hole sensor.""" + if discovery_info is None: + return - if pi_hole.api.data is None: - raise PlatformNotReady + pi_hole = hass.data[PIHOLE_DOMAIN] - sensors = [ - PiHoleSensor(pi_hole, name, condition) - for condition in config[CONF_MONITORED_CONDITIONS] - ] + sensors = [] + sensors = [PiHoleSensor(pi_hole, sensor_name) for sensor_name in SENSOR_LIST] async_add_entities(sensors, True) @@ -102,13 +29,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class PiHoleSensor(Entity): """Representation of a Pi-hole sensor.""" - def __init__(self, pi_hole, name, condition): + def __init__(self, pi_hole, sensor_name): """Initialize a Pi-hole sensor.""" self.pi_hole = pi_hole - self._name = name - self._condition = condition + self._name = pi_hole.name + self._condition = sensor_name - variable_info = MONITORED_CONDITIONS[condition] + variable_info = SENSOR_DICT[sensor_name] self._condition_name = variable_info[0] self._unit_of_measurement = variable_info[1] self._icon = variable_info[2] @@ -151,24 +78,3 @@ async def async_update(self): """Get the latest data from the Pi-hole API.""" await self.pi_hole.async_update() self.data = self.pi_hole.api.data - - -class PiHoleData: - """Get the latest data and update the states.""" - - def __init__(self, api): - """Initialize the data object.""" - self.api = api - self.available = True - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """Get the latest data from the Pi-hole.""" - from hole.exceptions import HoleError - - try: - await self.api.get_data() - self.available = True - except HoleError: - _LOGGER.error("Unable to fetch data from Pi-hole") - self.available = False diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ef8f0ca672595..f6013efee06ddb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -172,6 +172,9 @@ hbmqtt==0.9.4 # homeassistant.components.jewish_calendar hdate==0.9.0 +# homeassistant.components.pi_hole +hole==0.5.0 + # homeassistant.components.workday holidays==0.9.11 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e99fd0a6c46b48..39e5de3e2b0828 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -86,6 +86,7 @@ "haversine", "hbmqtt", "hdate", + "hole", "holidays", "home-assistant-frontend", "homekit[IP]", diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py new file mode 100644 index 00000000000000..7eea15b79c87bf --- /dev/null +++ b/tests/components/pi_hole/__init__.py @@ -0,0 +1 @@ +"""Tests for the pi_hole component.""" diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py new file mode 100644 index 00000000000000..f30422bfea99f7 --- /dev/null +++ b/tests/components/pi_hole/test_init.py @@ -0,0 +1,99 @@ +"""Test pi_hole component.""" + +from asynctest import CoroutineMock +from hole import Hole + +from homeassistant.components import pi_hole +from tests.common import async_setup_component +from unittest.mock import patch + + +def mock_pihole_data_call(Hole): + """Need to override so as to allow mocked data.""" + Hole.__init__ = ( + lambda self, host, loop, session, location, tls, verify_tls=True, api_token=None: None + ) + Hole.data = { + "ads_blocked_today": 0, + "ads_percentage_today": 0, + "clients_ever_seen": 0, + "dns_queries_today": 0, + "domains_being_blocked": 0, + "queries_cached": 0, + "queries_forwarded": 0, + "status": 0, + "unique_clients": 0, + "unique_domains": 0, + } + pass + + +async def test_setup_no_config(hass): + """Tests component setup with no config.""" + with patch.object( + Hole, "get_data", new=CoroutineMock(side_effect=mock_pihole_data_call(Hole)) + ): + assert await async_setup_component(hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: {}}) + + await hass.async_block_till_done() + + assert ( + hass.states.get("sensor.pi_hole_ads_blocked_today").name + == "Pi-Hole Ads Blocked Today" + ) + assert ( + hass.states.get("sensor.pi_hole_ads_percentage_blocked_today").name + == "Pi-Hole Ads Percentage Blocked Today" + ) + assert ( + hass.states.get("sensor.pi_hole_dns_queries_cached").name + == "Pi-Hole DNS Queries Cached" + ) + assert ( + hass.states.get("sensor.pi_hole_dns_queries_forwarded").name + == "Pi-Hole DNS Queries Forwarded" + ) + assert ( + hass.states.get("sensor.pi_hole_dns_queries_today").name + == "Pi-Hole DNS Queries Today" + ) + assert ( + hass.states.get("sensor.pi_hole_dns_unique_clients").name + == "Pi-Hole DNS Unique Clients" + ) + assert ( + hass.states.get("sensor.pi_hole_dns_unique_domains").name + == "Pi-Hole DNS Unique Domains" + ) + assert ( + hass.states.get("sensor.pi_hole_domains_blocked").name + == "Pi-Hole Domains Blocked" + ) + assert hass.states.get("sensor.pi_hole_seen_clients").name == "Pi-Hole Seen Clients" + + assert hass.states.get("sensor.pi_hole_ads_blocked_today").state == "0" + assert hass.states.get("sensor.pi_hole_ads_percentage_blocked_today").state == "0" + assert hass.states.get("sensor.pi_hole_dns_queries_cached").state == "0" + assert hass.states.get("sensor.pi_hole_dns_queries_forwarded").state == "0" + assert hass.states.get("sensor.pi_hole_dns_queries_today").state == "0" + assert hass.states.get("sensor.pi_hole_dns_unique_clients").state == "0" + assert hass.states.get("sensor.pi_hole_dns_unique_domains").state == "0" + assert hass.states.get("sensor.pi_hole_domains_blocked").state == "0" + assert hass.states.get("sensor.pi_hole_seen_clients").state == "0" + + +async def test_setup_custom_config(hass): + """Tests component setup with custom config.""" + with patch.object( + Hole, "get_data", new=CoroutineMock(side_effect=mock_pihole_data_call(Hole)) + ): + assert await async_setup_component( + hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: {"name": "Custom"}} + ) + + await hass.async_block_till_done() + + assert ( + hass.states.get("sensor.custom_ads_blocked_today").name + == "Custom Ads Blocked Today" + ) From f7a58cc19e1850639b59472ca991950dcc41f72f Mon Sep 17 00:00:00 2001 From: Sriram Vaidyanathan Date: Tue, 3 Sep 2019 20:06:11 -0400 Subject: [PATCH 0141/3953] Change xiaomi camera conf_host to template (#25799) * changed conf_host to template Changed conf_host to template to accommodate dynamic ips * Update camera.py * Updated per comment * update for black formatter * black changes --- homeassistant/components/xiaomi/camera.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 905b19c622bd5e..363c17fe4a9cc5 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.exceptions import TemplateError from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ( CONF_HOST, @@ -34,7 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_HOST): cv.template, vol.Required(CONF_MODEL): vol.Any(MODEL_YI, MODEL_XIAOFANG), vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, @@ -63,6 +64,7 @@ def __init__(self, hass, config): self._manager = hass.data[DATA_FFMPEG] self._name = config[CONF_NAME] self.host = config[CONF_HOST] + self.host.hass = hass self._model = config[CONF_MODEL] self.port = config[CONF_PORT] self.path = config[CONF_PATH] @@ -84,11 +86,11 @@ def model(self): """Return the camera model.""" return self._model - def get_latest_video_url(self): + def get_latest_video_url(self, host): """Retrieve the latest video file from the Xiaomi Camera FTP server.""" from ftplib import FTP, error_perm - ftp = FTP(self.host) + ftp = FTP(host) try: ftp.login(self.user, self.passwd) except error_perm as exc: @@ -133,14 +135,20 @@ def get_latest_video_url(self): video = videos[-1] return "ftp://{0}:{1}@{2}:{3}{4}/{5}".format( - self.user, self.passwd, self.host, self.port, ftp.pwd(), video + self.user, self.passwd, host, self.port, ftp.pwd(), video ) async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg.tools import ImageFrame, IMAGE_JPEG - url = await self.hass.async_add_job(self.get_latest_video_url) + try: + host = self.host.async_render() + except TemplateError as exc: + _LOGGER.error("Error parsing template %s: %s", self.host, exc) + return self._last_image + + url = await self.hass.async_add_executor_job(self.get_latest_video_url, host) if url != self._last_url: ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) self._last_image = await asyncio.shield( From b968b53e38b68c7a7f1959ab9fc539f3bf225e46 Mon Sep 17 00:00:00 2001 From: croghostrider Date: Wed, 4 Sep 2019 02:33:48 +0200 Subject: [PATCH 0142/3953] =?UTF-8?q?Fix=20Emulated=20Hue=20AttributeError?= =?UTF-8?q?:=20'NoneType'=20object=20has=20no=20attribute=20'=E2=80=A6=20(?= =?UTF-8?q?#26018)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix Emulated Hue AttributeError: 'NoneType' object has no attribute 'lower' * Fix debug * Update error message --- homeassistant/components/emulated_hue/hue_api.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 5e1c72618b6e43..5d08af6c5eeb75 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -197,11 +197,19 @@ def get(self, request, username, entity_id): return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) hass = request.app["hass"] - entity_id = self.config.number_to_entity_id(entity_id) - entity = hass.states.get(entity_id) + hass_entity_id = self.config.number_to_entity_id(entity_id) + + if hass_entity_id is None: + _LOGGER.error( + "Unknown entity number: %s not found in emulated_hue_ids.json", + entity_id, + ) + return web.Response(text="Entity not found", status=404) + + entity = hass.states.get(hass_entity_id) if entity is None: - _LOGGER.error("Entity not found: %s", entity_id) + _LOGGER.error("Entity not found: %s", hass_entity_id) return web.Response(text="Entity not found", status=404) if not self.config.is_entity_exposed(entity): From 53720c5c48fb4dd3fb4029521c8d6856c0438083 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Sep 2019 18:55:58 -0700 Subject: [PATCH 0143/3953] Allow passing None as input_text config (#26409) --- .../components/input_text/__init__.py | 33 +++++++++++-------- tests/components/input_text/test_init.py | 9 +++++ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index fc49bd65ced92f..41d78e6e7c540c 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -58,20 +58,23 @@ def _cv_input_text(cfg): CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( - vol.All( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MIN, default=0): vol.Coerce(int), - vol.Optional(CONF_MAX, default=100): vol.Coerce(int), - vol.Optional(CONF_INITIAL, ""): cv.string, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(ATTR_PATTERN): cv.string, - vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In( - [MODE_TEXT, MODE_PASSWORD] - ), - }, - _cv_input_text, + vol.Any( + vol.All( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MIN, default=0): vol.Coerce(int), + vol.Optional(CONF_MAX, default=100): vol.Coerce(int), + vol.Optional(CONF_INITIAL, ""): cv.string, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(ATTR_PATTERN): cv.string, + vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In( + [MODE_TEXT, MODE_PASSWORD] + ), + }, + _cv_input_text, + ), + None, ) ) }, @@ -87,6 +90,8 @@ async def async_setup(hass, config): entities = [] for object_id, cfg in config[DOMAIN].items(): + if cfg is None: + cfg = {} name = cfg.get(CONF_NAME) minimum = cfg.get(CONF_MIN) maximum = cfg.get(CONF_MAX) diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index a13bc4470c390d..4888994d78868b 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -186,3 +186,12 @@ async def test_input_text_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_config_none(hass): + """Set up input_text without any config.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {"b1": None}}) + + state = hass.states.get("input_text.b1") + assert state + assert str(state.state) == "unknown" From 4e2fcdb9a3e6e4722d873d4029f970486ea61bd0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Sep 2019 18:57:32 -0700 Subject: [PATCH 0144/3953] Fix state report (#26406) * Fix state report * Update test --- homeassistant/components/alexa/state_report.py | 7 +++++-- tests/components/alexa/test_state_report.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index e956abec7ad997..b7ff9d17fe8214 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -97,12 +97,15 @@ async def async_send_changereport_message( _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) _LOGGER.debug("Received (%s): %s", response.status, response_text) - if response.status == 202 and not invalidate_access_token: + if response.status == 202: return response_json = json.loads(response_text) - if response_json["payload"]["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION": + if ( + response_json["payload"]["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION" + and not invalidate_access_token + ): config.async_invalidate_access_token() return await async_send_changereport_message( hass, config, alexa_entity, invalidate_access_token=False diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index f6bb4c9cc29f45..2b3f9f34adf57d 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -5,7 +5,7 @@ async def test_report_state(hass, aioclient_mock): """Test proactive state reports.""" - aioclient_mock.post(TEST_URL, json={"data": "is irrelevant"}, status=202) + aioclient_mock.post(TEST_URL, text="", status=202) hass.states.async_set( "binary_sensor.test_contact", @@ -39,7 +39,7 @@ async def test_report_state(hass, aioclient_mock): async def test_send_add_or_update_message(hass, aioclient_mock): """Test sending an AddOrUpdateReport message.""" - aioclient_mock.post(TEST_URL, json={"data": "is irrelevant"}) + aioclient_mock.post(TEST_URL, text="") hass.states.async_set( "binary_sensor.test_contact", From 2f9de2a5a50e69123a0e32e9a2130ba2985a2ec5 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 4 Sep 2019 11:58:40 +1000 Subject: [PATCH 0145/3953] IGN Sismologia icon for geolocation entities (#26408) * define icon * fixed tests --- homeassistant/components/ign_sismologia/geo_location.py | 5 +++++ tests/components/ign_sismologia/test_geo_location.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index 44ba41ea7835fe..057d832b4faad8 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -201,6 +201,11 @@ def _update_from_feed(self, feed_entry): self._publication_date = feed_entry.published self._image_url = feed_entry.image_url + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:pulse" + @property def source(self) -> str: """Return source value of this external event.""" diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index fa3042e9f7a8d4..4babbb6a4254a4 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -23,6 +23,7 @@ ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, + ATTR_ICON, ) from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_fire_time_changed @@ -126,6 +127,7 @@ async def test_setup(hass): ATTR_MAGNITUDE: 5.7, ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "ign_sismologia", + ATTR_ICON: "mdi:pulse", } assert float(state.state) == 15.5 @@ -141,6 +143,7 @@ async def test_setup(hass): ATTR_MAGNITUDE: 4.6, ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "ign_sismologia", + ATTR_ICON: "mdi:pulse", } assert float(state.state) == 20.5 @@ -156,6 +159,7 @@ async def test_setup(hass): ATTR_REGION: "Region 3", ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "ign_sismologia", + ATTR_ICON: "mdi:pulse", } assert float(state.state) == 25.5 From 2dc90be94f85e8c6d665a2f1ddff587d2b048814 Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Tue, 3 Sep 2019 20:00:05 -0600 Subject: [PATCH 0146/3953] Update to 0.1.13 (#26402) Update to 0.1.13 --- homeassistant/components/harmony/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index b2f9e69e014627..a957db0675fb94 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -3,7 +3,7 @@ "name": "Harmony", "documentation": "https://www.home-assistant.io/components/harmony", "requirements": [ - "aioharmony==0.1.11" + "aioharmony==0.1.13" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index df3f6962788c67..367de8014f91f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -142,7 +142,7 @@ aiofreepybox==0.0.8 aioftp==0.12.0 # homeassistant.components.harmony -aioharmony==0.1.11 +aioharmony==0.1.13 # homeassistant.components.emulated_hue # homeassistant.components.http From 2f0eb0762421c7287e69f2afcc4555a28d7792c5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Sep 2019 05:36:04 +0200 Subject: [PATCH 0147/3953] Migrate legacy typehints in core to PEP-526 (#26403) * Migrate legacy typehints in core to PEP-526 * Fix one type --- homeassistant/__main__.py | 4 +- homeassistant/auth/__init__.py | 12 ++-- homeassistant/auth/auth_store.py | 30 ++++----- homeassistant/auth/mfa_modules/__init__.py | 2 +- homeassistant/auth/mfa_modules/notify.py | 18 +++--- homeassistant/auth/mfa_modules/totp.py | 12 ++-- homeassistant/auth/models.py | 6 +- homeassistant/auth/permissions/entities.py | 7 +- homeassistant/auth/permissions/merge.py | 10 +-- homeassistant/auth/permissions/util.py | 6 +- homeassistant/auth/providers/__init__.py | 12 ++-- homeassistant/auth/providers/command_line.py | 6 +- homeassistant/auth/providers/homeassistant.py | 10 +-- .../auth/providers/insecure_example.py | 2 +- homeassistant/bootstrap.py | 6 +- .../components/device_tracker/legacy.py | 2 +- homeassistant/config.py | 18 ++---- homeassistant/config_entries.py | 8 +-- homeassistant/const.py | 64 +++++++++---------- homeassistant/core.py | 37 ++++++----- homeassistant/data_entry_flow.py | 13 +--- homeassistant/helpers/aiohttp_client.py | 6 +- homeassistant/helpers/area_registry.py | 6 +- homeassistant/helpers/check_config.py | 2 +- homeassistant/helpers/config_validation.py | 4 +- homeassistant/helpers/entity.py | 6 +- homeassistant/helpers/entity_values.py | 6 +- homeassistant/helpers/intent.py | 12 ++-- homeassistant/helpers/restore_state.py | 14 ++-- homeassistant/helpers/script.py | 8 +-- homeassistant/helpers/state.py | 19 ++---- homeassistant/helpers/storage.py | 4 +- homeassistant/helpers/sun.py | 4 +- homeassistant/helpers/translation.py | 2 +- homeassistant/loader.py | 22 +++---- homeassistant/scripts/benchmark/__init__.py | 2 +- homeassistant/scripts/check_config.py | 12 ++-- homeassistant/util/aiohttp.py | 2 +- homeassistant/util/async_.py | 8 +-- homeassistant/util/dt.py | 24 +++---- homeassistant/util/logging.py | 2 +- homeassistant/util/ruamel_yaml.py | 6 +- homeassistant/util/unit_system.py | 4 +- homeassistant/util/yaml/dumper.py | 2 +- homeassistant/util/yaml/loader.py | 22 +++---- 45 files changed, 221 insertions(+), 263 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 8765ee6c82260f..9fe501078c2a45 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -7,7 +7,7 @@ import subprocess import sys import threading -from typing import List, Dict, Any, TYPE_CHECKING # noqa pylint: disable=unused-import +from typing import List, Dict, Any, TYPE_CHECKING from homeassistant import monkey_patch from homeassistant.const import __version__, REQUIRED_PYTHON_VER, RESTART_EXIT_CODE @@ -280,7 +280,7 @@ async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: hass = core.HomeAssistant() if args.demo_mode: - config = {"frontend": {}, "demo": {}} # type: Dict[str, Any] + config: Dict[str, Any] = {"frontend": {}, "demo": {}} bootstrap.async_from_config_dict( config, hass, diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index e2778e9f45b58f..ee0d6c08441caa 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -47,7 +47,7 @@ async def auth_manager_from_config( else: providers = () # So returned auth providers are in same order as config - provider_hash = OrderedDict() # type: _ProviderDict + provider_hash: _ProviderDict = OrderedDict() for provider in providers: key = (provider.type, provider.id) provider_hash[key] = provider @@ -59,7 +59,7 @@ async def auth_manager_from_config( else: modules = () # So returned auth modules are in same order as config - module_hash = OrderedDict() # type: _MfaModuleDict + module_hash: _MfaModuleDict = OrderedDict() for module in modules: module_hash[module.id] = module @@ -168,11 +168,11 @@ async def async_create_system_user( async def async_create_user(self, name: str) -> models.User: """Create a user.""" - kwargs = { + kwargs: Dict[str, Any] = { "name": name, "is_active": True, "group_ids": [GROUP_ID_ADMIN], - } # type: Dict[str, Any] + } if await self._user_should_be_owner(): kwargs["is_owner"] = True @@ -238,7 +238,7 @@ async def async_update_user( group_ids: Optional[List[str]] = None, ) -> None: """Update a user.""" - kwargs = {} # type: Dict[str,Any] + kwargs: Dict[str, Any] = {} if name is not None: kwargs["name"] = name if group_ids is not None: @@ -299,7 +299,7 @@ async def async_disable_user_mfa( async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]: """List enabled mfa modules for user.""" - modules = OrderedDict() # type: Dict[str, str] + modules: Dict[str, str] = OrderedDict() for module_id, module in self._mfa_modules.items(): if await module.async_is_user_setup(user.id): modules[module_id] = module.name diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 894819fb3c7bf6..4c64730edda646 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -4,7 +4,7 @@ from datetime import timedelta import hmac from logging import getLogger -from typing import Any, Dict, List, Optional # noqa: F401 +from typing import Any, Dict, List, Optional from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback @@ -13,7 +13,7 @@ from . import models from .const import GROUP_ID_ADMIN, GROUP_ID_USER, GROUP_ID_READ_ONLY from .permissions import PermissionLookup, system_policies -from .permissions.types import PolicyType # noqa: F401 +from .permissions.types import PolicyType STORAGE_VERSION = 1 STORAGE_KEY = "auth" @@ -34,9 +34,9 @@ class AuthStore: def __init__(self, hass: HomeAssistant) -> None: """Initialize the auth store.""" self.hass = hass - self._users = None # type: Optional[Dict[str, models.User]] - self._groups = None # type: Optional[Dict[str, models.Group]] - self._perm_lookup = None # type: Optional[PermissionLookup] + self._users: Optional[Dict[str, models.User]] = None + self._groups: Optional[Dict[str, models.Group]] = None + self._perm_lookup: Optional[PermissionLookup] = None self._store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) @@ -97,13 +97,13 @@ async def async_create_user( raise ValueError(f"Invalid group specified {group_id}") groups.append(group) - kwargs = { + kwargs: Dict[str, Any] = { "name": name, # Until we get group management, we just put everyone in the # same group. "groups": groups, "perm_lookup": self._perm_lookup, - } # type: Dict[str, Any] + } if is_owner is not None: kwargs["is_owner"] = is_owner @@ -210,12 +210,12 @@ async def async_create_refresh_token( access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, ) -> models.RefreshToken: """Create a new token for a user.""" - kwargs = { + kwargs: Dict[str, Any] = { "user": user, "client_id": client_id, "token_type": token_type, "access_token_expiration": access_token_expiration, - } # type: Dict[str, Any] + } if client_name: kwargs["client_name"] = client_name if client_icon: @@ -307,8 +307,8 @@ async def _async_load_task(self) -> None: self._set_defaults() return - users = OrderedDict() # type: Dict[str, models.User] - groups = OrderedDict() # type: Dict[str, models.Group] + users: Dict[str, models.User] = OrderedDict() + groups: Dict[str, models.Group] = OrderedDict() # Soft-migrating data as we load. We are going to make sure we have a # read only group and an admin group. There are two states that we can @@ -325,7 +325,7 @@ async def _async_load_task(self) -> None: # was added. for group_dict in data.get("groups", []): - policy = None # type: Optional[PolicyType] + policy: Optional[PolicyType] = None if group_dict["id"] == GROUP_ID_ADMIN: has_admin_group = True @@ -503,11 +503,11 @@ def _data_to_save(self) -> Dict: groups = [] for group in self._groups.values(): - g_dict = { + g_dict: Dict[str, Any] = { "id": group.id, # Name not read for sys groups. Kept here for backwards compat "name": group.name, - } # type: Dict[str, Any] + } if not group.system_generated: g_dict["policy"] = group.policy @@ -558,7 +558,7 @@ def _set_defaults(self) -> None: """Set default values for auth store.""" self._users = OrderedDict() - groups = OrderedDict() # type: Dict[str, models.Group] + groups: Dict[str, models.Group] = OrderedDict() admin_group = _system_admin_group() groups[admin_group.id] = admin_group user_group = _system_user_group() diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index baccedeabbff23..9d49f67df82d7d 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -109,7 +109,7 @@ async def async_step_init( Return self.async_show_form(step_id='init') if user_input is None. Return self.async_create_entry(data={'result': result}) if finish. """ - errors = {} # type: Dict[str, str] + errors: Dict[str, str] = {} if user_input: result = await self._auth_module.async_setup_user(self._user_id, user_input) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 4a41ff03ef6a2c..a6a754fc2a6f3f 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -95,7 +95,7 @@ class NotifyAuthModule(MultiFactorAuthModule): def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: """Initialize the user data store.""" super().__init__(hass, config) - self._user_settings = None # type: Optional[_UsersDict] + self._user_settings: Optional[_UsersDict] = None self._user_store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) @@ -279,18 +279,18 @@ def __init__( """Initialize the setup flow.""" super().__init__(auth_module, setup_schema, user_id) # to fix typing complaint - self._auth_module = auth_module # type: NotifyAuthModule + self._auth_module: NotifyAuthModule = auth_module self._available_notify_services = available_notify_services - self._secret = None # type: Optional[str] - self._count = None # type: Optional[int] - self._notify_service = None # type: Optional[str] - self._target = None # type: Optional[str] + self._secret: Optional[str] = None + self._count: Optional[int] = None + self._notify_service: Optional[str] = None + self._target: Optional[str] = None async def async_step_init( self, user_input: Optional[Dict[str, str]] = None ) -> Dict[str, Any]: """Let user select available notify services.""" - errors = {} # type: Dict[str, str] + errors: Dict[str, str] = {} hass = self._auth_module.hass if user_input: @@ -304,7 +304,7 @@ async def async_step_init( if not self._available_notify_services: return self.async_abort(reason="no_available_service") - schema = OrderedDict() # type: Dict[str, Any] + schema: Dict[str, Any] = OrderedDict() schema["notify_service"] = vol.In(self._available_notify_services) schema["target"] = vol.Optional(str) @@ -316,7 +316,7 @@ async def async_step_setup( self, user_input: Optional[Dict[str, str]] = None ) -> Dict[str, Any]: """Verify user can recevie one-time password.""" - errors = {} # type: Dict[str, str] + errors: Dict[str, str] = {} hass = self._auth_module.hass if user_input: diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 22d153e3420c91..d6d901ac3b1fe5 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -2,7 +2,7 @@ import asyncio import logging from io import BytesIO -from typing import Any, Dict, Optional, Tuple # noqa: F401 +from typing import Any, Dict, Optional, Tuple import voluptuous as vol @@ -75,7 +75,7 @@ class TotpAuthModule(MultiFactorAuthModule): def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: """Initialize the user data store.""" super().__init__(hass, config) - self._users = None # type: Optional[Dict[str, str]] + self._users: Optional[Dict[str, str]] = None self._user_store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) @@ -107,7 +107,7 @@ def _add_ota_secret(self, user_id: str, secret: Optional[str] = None) -> str: """Create a ota_secret for user.""" import pyotp - ota_secret = secret or pyotp.random_base32() # type: str + ota_secret: str = secret or pyotp.random_base32() self._users[user_id] = ota_secret # type: ignore return ota_secret @@ -181,9 +181,9 @@ def __init__( """Initialize the setup flow.""" super().__init__(auth_module, setup_schema, user.id) # to fix typing complaint - self._auth_module = auth_module # type: TotpAuthModule + self._auth_module: TotpAuthModule = auth_module self._user = user - self._ota_secret = None # type: Optional[str] + self._ota_secret: Optional[str] = None self._url = None # type Optional[str] self._image = None # type Optional[str] @@ -197,7 +197,7 @@ async def async_step_init( """ import pyotp - errors = {} # type: Dict[str, str] + errors: Dict[str, str] = {} if user_input: verified = await self.hass.async_add_executor_job( # type: ignore diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 533d7672ee4de3..26055032422f0c 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -1,6 +1,6 @@ """Auth models.""" from datetime import datetime, timedelta -from typing import Dict, List, NamedTuple, Optional # noqa: F401 +from typing import Dict, List, NamedTuple, Optional import uuid import attr @@ -31,9 +31,7 @@ class User: """A user.""" name = attr.ib(type=str) # type: Optional[str] - perm_lookup = attr.ib( - type=perm_mdl.PermissionLookup, cmp=False - ) # type: perm_mdl.PermissionLookup + perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, cmp=False) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) is_owner = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False) diff --git a/homeassistant/auth/permissions/entities.py b/homeassistant/auth/permissions/entities.py index 2708693743afb4..add9913abf3370 100644 --- a/homeassistant/auth/permissions/entities.py +++ b/homeassistant/auth/permissions/entities.py @@ -1,6 +1,6 @@ """Entity permissions.""" from collections import OrderedDict -from typing import Callable, Optional # noqa: F401 +from typing import Callable, Optional import voluptuous as vol @@ -8,8 +8,7 @@ from .models import PermissionLookup from .types import CategoryType, SubCategoryDict, ValueType -# pylint: disable=unused-import -from .util import SubCatLookupType, lookup_all, compile_policy # noqa +from .util import SubCatLookupType, lookup_all, compile_policy SINGLE_ENTITY_SCHEMA = vol.Any( True, @@ -90,7 +89,7 @@ def compile_entities( policy: CategoryType, perm_lookup: PermissionLookup ) -> Callable[[str, str], bool]: """Compile policy into a function that tests policy.""" - subcategories = OrderedDict() # type: SubCatLookupType + subcategories: SubCatLookupType = OrderedDict() subcategories[ENTITY_ENTITY_IDS] = _lookup_entity_id subcategories[ENTITY_DEVICE_IDS] = _lookup_device subcategories[ENTITY_AREAS] = _lookup_area diff --git a/homeassistant/auth/permissions/merge.py b/homeassistant/auth/permissions/merge.py index f8b3639ad5ae91..3cf02e0577134f 100644 --- a/homeassistant/auth/permissions/merge.py +++ b/homeassistant/auth/permissions/merge.py @@ -1,13 +1,13 @@ """Merging of policies.""" -from typing import cast, Dict, List, Set # noqa: F401 +from typing import cast, Dict, List, Set from .types import PolicyType, CategoryType def merge_policies(policies: List[PolicyType]) -> PolicyType: """Merge policies.""" - new_policy = {} # type: Dict[str, CategoryType] - seen = set() # type: Set[str] + new_policy: Dict[str, CategoryType] = {} + seen: Set[str] = set() for policy in policies: for category in policy: if category in seen: @@ -33,8 +33,8 @@ def _merge_policies(sources: List[CategoryType]) -> CategoryType: # If there are multiple sources with a dict as policy, we recursively # merge each key in the source. - policy = None # type: CategoryType - seen = set() # type: Set[str] + policy: CategoryType = None + seen: Set[str] = set() for source in sources: if source is None: continue diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 6b44cbf61d4d38..109a5dc04ae252 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -1,7 +1,7 @@ """Helpers to deal with permissions.""" from functools import wraps -from typing import Callable, Dict, List, Optional, cast # noqa: F401 +from typing import Callable, Dict, List, Optional, cast from .const import SUBCAT_ALL from .models import PermissionLookup @@ -45,7 +45,7 @@ def apply_policy_allow_all(entity_id: str, key: str) -> bool: assert isinstance(policy, dict) - funcs = [] # type: List[Callable[[str, str], Optional[bool]]] + funcs: List[Callable[[str, str], Optional[bool]]] = [] for key, lookup_func in subcategories.items(): lookup_value = policy.get(key) @@ -85,7 +85,7 @@ def _gen_dict_test_func( def test_value(object_id: str, key: str) -> Optional[bool]: """Test if permission is allowed based on the keys.""" - schema = lookup_func(perm_lookup, lookup_dict, object_id) # type: ValueType + schema: ValueType = lookup_func(perm_lookup, lookup_dict, object_id) if schema is None or isinstance(schema, bool): return schema diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index ee9ef8f94cd0ca..3e25003ad00eda 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -16,7 +16,7 @@ from ..auth_store import AuthStore from ..const import MFA_SESSION_EXPIRATION -from ..models import Credentials, User, UserMeta # noqa: F401 +from ..models import Credentials, User, UserMeta _LOGGER = logging.getLogger(__name__) DATA_REQS = "auth_prov_reqs_processed" @@ -175,12 +175,12 @@ class LoginFlow(data_entry_flow.FlowHandler): def __init__(self, auth_provider: AuthProvider) -> None: """Initialize the login flow.""" self._auth_provider = auth_provider - self._auth_module_id = None # type: Optional[str] + self._auth_module_id: Optional[str] = None self._auth_manager = auth_provider.hass.auth # type: ignore - self.available_mfa_modules = {} # type: Dict[str, str] + self.available_mfa_modules: Dict[str, str] = {} self.created_at = dt_util.utcnow() self.invalid_mfa_times = 0 - self.user = None # type: Optional[User] + self.user: Optional[User] = None async def async_step_init( self, user_input: Optional[Dict[str, str]] = None @@ -255,10 +255,10 @@ async def async_step_mfa( if not errors: return await self.async_finish(self.user) - description_placeholders = { + description_placeholders: Dict[str, Optional[str]] = { "mfa_module_name": auth_module.name, "mfa_module_id": auth_module.id, - } # type: Dict[str, Optional[str]] + } return self.async_show_form( step_id="mfa", diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index cdf1a533412c21..58a2cac1fc532c 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -53,7 +53,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: attributes provided by external programs. """ super().__init__(*args, **kwargs) - self._user_meta = {} # type: Dict[str, Dict[str, Any]] + self._user_meta: Dict[str, Dict[str, Any]] = {} async def async_login_flow(self, context: Optional[dict]) -> LoginFlow: """Return a flow to login.""" @@ -85,7 +85,7 @@ async def async_validate_login(self, username: str, password: str) -> None: raise InvalidAuthError if self.config[CONF_META]: - meta = {} # type: Dict[str, str] + meta: Dict[str, str] = {} for _line in stdout.splitlines(): try: line = _line.decode().lstrip() @@ -146,7 +146,7 @@ async def async_step_init( user_input.pop("password") return await self.async_finish(user_input) - schema = collections.OrderedDict() # type: Dict[str, type] + schema: Dict[str, type] = collections.OrderedDict() schema["username"] = str schema["password"] = str diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index df38810fc29bcd..265a24a4b288c3 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -4,7 +4,7 @@ from collections import OrderedDict import logging -from typing import Any, Dict, List, Optional, Set, cast # noqa: F401 +from typing import Any, Dict, List, Optional, Set, cast import bcrypt import voluptuous as vol @@ -53,7 +53,7 @@ def __init__(self, hass: HomeAssistant) -> None: self._store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) - self._data = None # type: Optional[Dict[str, Any]] + self._data: Optional[Dict[str, Any]] = None # Legacy mode will allow usernames to start/end with whitespace # and will compare usernames case-insensitive. # Remove in 2020 or when we launch 1.0. @@ -74,7 +74,7 @@ async def async_load(self) -> None: if data is None: data = {"users": []} - seen = set() # type: Set[str] + seen: Set[str] = set() for user in data["users"]: username = user["username"] @@ -210,7 +210,7 @@ class HassAuthProvider(AuthProvider): def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize an Home Assistant auth provider.""" super().__init__(*args, **kwargs) - self.data = None # type: Optional[Data] + self.data: Optional[Data] = None self._init_lock = asyncio.Lock() async def async_initialize(self) -> None: @@ -296,7 +296,7 @@ async def async_step_init( user_input.pop("password") return await self.async_finish(user_input) - schema = OrderedDict() # type: Dict[str, type] + schema: Dict[str, type] = OrderedDict() schema["username"] = str schema["password"] = str diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index 35524c3f5fc298..37859f5ed0ec07 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -112,7 +112,7 @@ async def async_step_init( user_input.pop("password") return await self.async_finish(user_input) - schema = OrderedDict() # type: Dict[str, type] + schema: Dict[str, type] = OrderedDict() schema["username"] = str schema["password"] = str diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 3e71a588af0c09..ef294491141042 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -206,9 +206,9 @@ def async_enable_logging( ): if log_rotate_days: - err_handler = logging.handlers.TimedRotatingFileHandler( + err_handler: logging.FileHandler = logging.handlers.TimedRotatingFileHandler( err_log_path, when="midnight", backupCount=log_rotate_days - ) # type: logging.FileHandler + ) else: err_handler = logging.FileHandler(err_log_path, mode="w", delay=True) @@ -335,7 +335,7 @@ async def _async_set_up_integrations( ) # Load all integrations - after_dependencies = {} # type: Dict[str, Set[str]] + after_dependencies: Dict[str, Set[str]] = {} for int_or_exc in await asyncio.gather( *(loader.async_get_integration(hass, domain) for domain in stage_2_domains), diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 67e35df00a1773..2bfd0c41a47bfd 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -532,7 +532,7 @@ async def async_added_to_hass(self): class DeviceScanner: """Device scanner object.""" - hass = None # type: HomeAssistantType + hass: HomeAssistantType = None def scan_devices(self) -> List[str]: """Scan for devices.""" diff --git a/homeassistant/config.py b/homeassistant/config.py index f4775e718054ee..4b7efed00e4082 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,17 +7,7 @@ import os import re import shutil -from typing import ( # noqa: F401 pylint: disable=unused-import - Any, - Tuple, - Optional, - Dict, - List, - Union, - Callable, - Sequence, - Set, -) +from typing import Any, Tuple, Optional, Dict, Union, Callable, Sequence, Set from types import ModuleType import voluptuous as vol from voluptuous.humanize import humanize_error @@ -118,7 +108,7 @@ def _no_duplicate_auth_provider( Each type of auth provider can only have one config without optional id. Unique id is required if same type of auth provider used multiple times. """ - config_keys = set() # type: Set[Tuple[str, Optional[str]]] + config_keys: Set[Tuple[str, Optional[str]]] = set() for config in configs: key = (config[CONF_TYPE], config.get(CONF_ID)) if key in config_keys: @@ -142,7 +132,7 @@ def _no_duplicate_auth_mfa_module( times. Note: this is different than auth provider """ - config_keys = set() # type: Set[str] + config_keys: Set[str] = set() for config in configs: key = config.get(CONF_ID, config[CONF_TYPE]) if key in config_keys: @@ -623,7 +613,7 @@ def _identify_config_schema(module: ModuleType) -> Tuple[Optional[str], Optional def _recursive_merge(conf: Dict[str, Any], package: Dict[str, Any]) -> Union[bool, str]: """Merge package into conf, recursively.""" - error = False # type: Union[bool, str] + error: Union[bool, str] = False for key, pack_conf in package.items(): if isinstance(pack_conf, dict): if not pack_conf: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c2da37943c1abb..8a40cff1bd5258 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -138,10 +138,10 @@ def __init__( self.state = state # Listeners to call on update - self.update_listeners = [] # type: list + self.update_listeners: List = [] # Function to cancel a scheduled retry - self._async_cancel_retry_setup = None # type: Optional[Callable[[], Any]] + self._async_cancel_retry_setup: Optional[Callable[[], Any]] = None async def async_setup( self, @@ -386,14 +386,14 @@ def __init__(self, hass: HomeAssistant, hass_config: dict) -> None: ) self.options = OptionsFlowManager(hass) self._hass_config = hass_config - self._entries = [] # type: List[ConfigEntry] + self._entries: List[ConfigEntry] = [] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) EntityRegistryDisabledHandler(hass).async_setup() @callback def async_domains(self) -> List[str]: """Return domains for which we have entries.""" - seen = set() # type: Set[str] + seen: Set[str] = set() result = [] for entry in self._entries: diff --git a/homeassistant/const.py b/homeassistant/const.py index c986c39dcb0825..4cfd16b8c9f0cc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -260,8 +260,8 @@ # The unit of measurement if applicable ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement" -CONF_UNIT_SYSTEM_METRIC = "metric" # type: str -CONF_UNIT_SYSTEM_IMPERIAL = "imperial" # type: str +CONF_UNIT_SYSTEM_METRIC: str = "metric" +CONF_UNIT_SYSTEM_IMPERIAL: str = "imperial" # Electrical attributes ATTR_VOLTAGE = "voltage" @@ -334,39 +334,39 @@ TEMP_FAHRENHEIT = "°F" # Length units -LENGTH_CENTIMETERS = "cm" # type: str -LENGTH_METERS = "m" # type: str -LENGTH_KILOMETERS = "km" # type: str +LENGTH_CENTIMETERS: str = "cm" +LENGTH_METERS: str = "m" +LENGTH_KILOMETERS: str = "km" -LENGTH_INCHES = "in" # type: str -LENGTH_FEET = "ft" # type: str -LENGTH_YARD = "yd" # type: str -LENGTH_MILES = "mi" # type: str +LENGTH_INCHES: str = "in" +LENGTH_FEET: str = "ft" +LENGTH_YARD: str = "yd" +LENGTH_MILES: str = "mi" # Pressure units -PRESSURE_PA = "Pa" # type: str -PRESSURE_HPA = "hPa" # type: str -PRESSURE_BAR = "bar" # type: str -PRESSURE_MBAR = "mbar" # type: str -PRESSURE_INHG = "inHg" # type: str -PRESSURE_PSI = "psi" # type: str +PRESSURE_PA: str = "Pa" +PRESSURE_HPA: str = "hPa" +PRESSURE_BAR: str = "bar" +PRESSURE_MBAR: str = "mbar" +PRESSURE_INHG: str = "inHg" +PRESSURE_PSI: str = "psi" # Volume units -VOLUME_LITERS = "L" # type: str -VOLUME_MILLILITERS = "mL" # type: str +VOLUME_LITERS: str = "L" +VOLUME_MILLILITERS: str = "mL" -VOLUME_GALLONS = "gal" # type: str -VOLUME_FLUID_OUNCE = "fl. oz." # type: str +VOLUME_GALLONS: str = "gal" +VOLUME_FLUID_OUNCE: str = "fl. oz." # Mass units -MASS_GRAMS = "g" # type: str -MASS_KILOGRAMS = "kg" # type: str +MASS_GRAMS: str = "g" +MASS_KILOGRAMS: str = "kg" -MASS_OUNCES = "oz" # type: str -MASS_POUNDS = "lb" # type: str +MASS_OUNCES: str = "oz" +MASS_POUNDS: str = "lb" # UV Index units -UNIT_UV_INDEX = "UV index" # type: str +UNIT_UV_INDEX: str = "UV index" # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP = "stop" @@ -460,15 +460,15 @@ # The exit code to send to request a restart RESTART_EXIT_CODE = 100 -UNIT_NOT_RECOGNIZED_TEMPLATE = "{} is not a recognized {} unit." # type: str +UNIT_NOT_RECOGNIZED_TEMPLATE: str = "{} is not a recognized {} unit." -LENGTH = "length" # type: str -MASS = "mass" # type: str -PRESSURE = "pressure" # type: str -VOLUME = "volume" # type: str -TEMPERATURE = "temperature" # type: str -SPEED_MS = "speed_ms" # type: str -ILLUMINANCE = "illuminance" # type: str +LENGTH: str = "length" +MASS: str = "mass" +PRESSURE: str = "pressure" +VOLUME: str = "volume" +TEMPERATURE: str = "temperature" +SPEED_MS: str = "speed_ms" +ILLUMINANCE: str = "illuminance" WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] diff --git a/homeassistant/core.py b/homeassistant/core.py index 4d7596d667b106..c29d41ace9a126 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -17,7 +17,7 @@ import uuid from types import MappingProxyType -from typing import ( # noqa: F401 pylint: disable=unused-import +from typing import ( Optional, Any, Callable, @@ -28,7 +28,6 @@ Set, TYPE_CHECKING, Awaitable, - Iterator, ) from async_timeout import timeout @@ -170,10 +169,10 @@ def __init__(self, loop: Optional[asyncio.events.AbstractEventLoop] = None) -> N """Initialize new Home Assistant object.""" self.loop: asyncio.events.AbstractEventLoop = (loop or asyncio.get_event_loop()) - executor_opts = { + executor_opts: Dict[str, Any] = { "max_workers": None, "thread_name_prefix": "SyncWorker", - } # type: Dict[str, Any] + } self.executor = ThreadPoolExecutor(**executor_opts) self.loop.set_default_executor(self.executor) @@ -733,7 +732,7 @@ def __init__( ) self.entity_id = entity_id.lower() - self.state = state # type: str + self.state: str = state self.attributes = MappingProxyType(attributes or {}) self.last_updated = last_updated or dt_util.utcnow() self.last_changed = last_changed or self.last_updated @@ -836,7 +835,7 @@ class StateMachine: def __init__(self, bus: EventBus, loop: asyncio.events.AbstractEventLoop) -> None: """Initialize state machine.""" - self._states = {} # type: Dict[str, State] + self._states: Dict[str, State] = {} self._bus = bus self._loop = loop @@ -1050,7 +1049,7 @@ class ServiceRegistry: def __init__(self, hass: HomeAssistant) -> None: """Initialize a service registry.""" - self._services = {} # type: Dict[str, Dict[str, Service]] + self._services: Dict[str, Dict[str, Service]] = {} self._hass = hass @property @@ -1269,29 +1268,29 @@ def __init__(self, hass: HomeAssistant) -> None: """Initialize a new config object.""" self.hass = hass - self.latitude = 0 # type: float - self.longitude = 0 # type: float - self.elevation = 0 # type: int - self.location_name = "Home" # type: str - self.time_zone = dt_util.UTC # type: datetime.tzinfo - self.units = METRIC_SYSTEM # type: UnitSystem + self.latitude: float = 0 + self.longitude: float = 0 + self.elevation: int = 0 + self.location_name: str = "Home" + self.time_zone: datetime.tzinfo = dt_util.UTC + self.units: UnitSystem = METRIC_SYSTEM - self.config_source = "default" # type: str + self.config_source: str = "default" # If True, pip install is skipped for requirements on startup - self.skip_pip = False # type: bool + self.skip_pip: bool = False # List of loaded components - self.components = set() # type: set + self.components: set = set() # API (HTTP) server configuration, see components.http.ApiConfig - self.api = None # type: Optional[Any] + self.api: Optional[Any] = None # Directory that holds the configuration - self.config_dir = None # type: Optional[str] + self.config_dir: Optional[str] = None # List of allowed external dirs to access - self.whitelist_external_dirs = set() # type: Set[str] + self.whitelist_external_dirs: Set[str] = set() def distance(self, lat: float, lon: float) -> Optional[float]: """Calculate distance from Home Assistant. diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 6bbd757fca63a6..3b128646219db2 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,13 +1,6 @@ """Classes to help gather user submissions.""" import logging -from typing import ( - Dict, - Any, - Callable, - Hashable, - List, - Optional, -) # noqa pylint: disable=unused-import +from typing import Dict, Any, Callable, Hashable, List, Optional import uuid import voluptuous as vol from .core import callback, HomeAssistant @@ -52,7 +45,7 @@ def __init__( ) -> None: """Initialize the flow manager.""" self.hass = hass - self._progress = {} # type: Dict[str, Any] + self._progress: Dict[str, Any] = {} self._async_create_flow = async_create_flow self._async_finish_flow = async_finish_flow @@ -136,7 +129,7 @@ async def _async_handle_step( ) ) - result = await getattr(flow, method)(user_input) # type: Dict + result: Dict = await getattr(flow, method)(user_input) if result["type"] not in ( RESULT_TYPE_FORM, diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 89fc9e5488a10f..7f1579cd2c67aa 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -1,9 +1,9 @@ """Helper for aiohttp webclient stuff.""" import asyncio import sys -from ssl import SSLContext # noqa: F401 +from ssl import SSLContext from typing import Any, Awaitable, Optional, cast -from typing import Union # noqa: F401 +from typing import Union import aiohttp from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE @@ -171,7 +171,7 @@ def _async_get_connector( return cast(aiohttp.BaseConnector, hass.data[key]) if verify_ssl: - ssl_context = ssl_util.client_context() # type: Union[bool, SSLContext] + ssl_context: Union[bool, SSLContext] = ssl_util.client_context() else: ssl_context = False diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 04a1858782df98..e75b195d386519 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -3,7 +3,7 @@ import uuid from asyncio import Event from collections import OrderedDict -from typing import MutableMapping # noqa: F401 +from typing import MutableMapping from typing import Iterable, Optional, cast import attr @@ -36,7 +36,7 @@ class AreaRegistry: def __init__(self, hass: HomeAssistantType) -> None: """Initialize the area registry.""" self.hass = hass - self.areas = {} # type: MutableMapping[str, AreaEntry] + self.areas: MutableMapping[str, AreaEntry] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback @@ -119,7 +119,7 @@ async def async_load(self) -> None: """Load the area registry.""" data = await self._store.async_load() - areas = OrderedDict() # type: OrderedDict[str, AreaEntry] + areas: MutableMapping[str, AreaEntry] = OrderedDict() if data is not None: for area in data["areas"]: diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index f49ae9768272ad..4052a94b9de9b7 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -36,7 +36,7 @@ class HomeAssistantConfig(OrderedDict): """Configuration result with errors attribute.""" - errors = attr.ib(default=attr.Factory(list)) # type: List[CheckConfigError] + errors: List[CheckConfigError] = attr.ib(default=attr.Factory(list)) def add_error(self, message, domain=None, config=None): """Add a single error.""" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index db96f4a2d0292d..471c6d50360698 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -823,7 +823,7 @@ def validator(value): } ) -CONDITION_SCHEMA = vol.Any( +CONDITION_SCHEMA: vol.Schema = vol.Any( NUMERIC_STATE_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA, SUN_CONDITION_SCHEMA, @@ -832,7 +832,7 @@ def validator(value): ZONE_CONDITION_SCHEMA, AND_CONDITION_SCHEMA, OR_CONDITION_SCHEMA, -) # type: vol.Schema +) _SCRIPT_DELAY_SCHEMA = vol.Schema( { diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 1aa405326e565b..af8d5589c8a85a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -91,7 +91,7 @@ class Entity: entity_id = None # type: str # Owning hass instance. Will be set by EntityPlatform - hass = None # type: Optional[HomeAssistant] + hass: Optional[HomeAssistant] = None # Owning platform instance. Will be set by EntityPlatform platform = None @@ -109,10 +109,10 @@ class Entity: parallel_updates = None # Entry in the entity registry - registry_entry = None # type: Optional[RegistryEntry] + registry_entry: Optional[RegistryEntry] = None # Hold list for functions to call on remove. - _on_remove = None # type: Optional[List[CALLBACK_TYPE]] + _on_remove: Optional[List[CALLBACK_TYPE]] = None # Context _context = None diff --git a/homeassistant/helpers/entity_values.py b/homeassistant/helpers/entity_values.py index 1b8e3c2fbec3f0..de48219a8d1c65 100644 --- a/homeassistant/helpers/entity_values.py +++ b/homeassistant/helpers/entity_values.py @@ -2,7 +2,7 @@ from collections import OrderedDict import fnmatch import re -from typing import Any, Dict, Optional, Pattern # noqa: F401 +from typing import Any, Dict, Optional, Pattern from homeassistant.core import split_entity_id @@ -17,12 +17,12 @@ def __init__( glob: Optional[Dict] = None, ) -> None: """Initialize an EntityConfigDict.""" - self._cache = {} # type: Dict[str, Dict] + self._cache: Dict[str, Dict] = {} self._exact = exact self._domain = domain if glob is None: - compiled = None # type: Optional[Dict[Pattern[str], Any]] + compiled: Optional[Dict[Pattern[str], Any]] = None else: compiled = OrderedDict() for key, value in glob.items(): diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 4fb0d94287c453..1fa0ec76a67c1b 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -55,7 +55,7 @@ async def async_handle( text_input: Optional[str] = None, ) -> "IntentResponse": """Handle an intent.""" - handler = hass.data.get(DATA_KEY, {}).get(intent_type) # type: IntentHandler + handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type) if handler is None: raise UnknownIntent(f"Unknown intent {intent_type}") @@ -122,10 +122,10 @@ def async_test_feature(state: State, feature: int, feature_name: str) -> None: class IntentHandler: """Intent handler registration.""" - intent_type = None # type: Optional[str] - slot_schema = None # type: Optional[vol.Schema] + intent_type: Optional[str] = None + slot_schema: Optional[vol.Schema] = None _slot_schema = None - platforms = [] # type: Optional[Iterable[str]] + platforms: Optional[Iterable[str]] = [] @callback def async_can_handle(self, intent_obj: "Intent") -> bool: @@ -236,8 +236,8 @@ class IntentResponse: def __init__(self, intent: Optional[Intent] = None) -> None: """Initialize an IntentResponse.""" self.intent = intent - self.speech = {} # type: Dict[str, Dict[str, Any]] - self.card = {} # type: Dict[str, Dict[str, str]] + self.speech: Dict[str, Dict[str, Any]] = {} + self.card: Dict[str, Dict[str, str]] = {} @callback def async_set_speech( diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index cf17186fc98013..fdf52c99075a54 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -2,7 +2,7 @@ import asyncio import logging from datetime import timedelta, datetime -from typing import Any, Dict, List, Set, Optional # noqa pylint_disable=unused-import +from typing import Any, Dict, List, Set, Optional from homeassistant.core import ( HomeAssistant, @@ -17,7 +17,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.json import JSONEncoder -from homeassistant.helpers.storage import Store # noqa pylint_disable=unused-import +from homeassistant.helpers.storage import Store # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -108,12 +108,12 @@ async def load_instance(hass: HomeAssistant) -> "RestoreStateData": def __init__(self, hass: HomeAssistant) -> None: """Initialize the restore state data class.""" - self.hass = hass # type: HomeAssistant - self.store = Store( + self.hass: HomeAssistant = hass + self.store: Store = Store( hass, STORAGE_VERSION, STORAGE_KEY, encoder=JSONEncoder - ) # type: Store - self.last_states = {} # type: Dict[str, StoredState] - self.entity_ids = set() # type: Set[str] + ) + self.last_states: Dict[str, StoredState] = {} + self.entity_ids: Set[str] = set() def async_get_stored_states(self) -> List[StoredState]: """Get the set of states which should be stored. diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1fd5bb673d7949..43ef156ef09f3f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -102,15 +102,15 @@ def __init__( self.name = name self._change_listener = change_listener self._cur = -1 - self._exception_step = None # type: Optional[int] + self._exception_step: Optional[int] = None self.last_action = None - self.last_triggered = None # type: Optional[datetime] + self.last_triggered: Optional[datetime] = None self.can_cancel = any( CONF_DELAY in action or CONF_WAIT_TEMPLATE in action for action in self.sequence ) - self._async_listener = [] # type: List[CALLBACK_TYPE] - self._config_cache = {} # type: Dict[Set[Tuple], Callable[..., bool]] + self._async_listener: List[CALLBACK_TYPE] = [] + self._config_cache: Dict[Set[Tuple], Callable[..., bool]] = {} self._actions = { ACTION_DELAY: self._async_delay, ACTION_WAIT_TEMPLATE: self._async_wait_template, diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 60aceee110f5c0..7f9692b3380264 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -5,16 +5,7 @@ import logging from collections import defaultdict from types import ModuleType, TracebackType -from typing import ( # noqa: F401 pylint: disable=unused-import - Awaitable, - Dict, - Iterable, - List, - Optional, - Tuple, - Type, - Union, -) +from typing import Awaitable, Dict, Iterable, List, Optional, Tuple, Type, Union from homeassistant.loader import bind_hass, async_get_integration, IntegrationNotFound import homeassistant.util.dt as dt_util @@ -99,7 +90,7 @@ class AsyncTrackStates: def __init__(self, hass: HomeAssistantType) -> None: """Initialize a TrackStates block.""" self.hass = hass - self.states = [] # type: List[State] + self.states: List[State] = [] # pylint: disable=attribute-defined-outside-init def __enter__(self) -> List[State]: @@ -147,7 +138,7 @@ async def async_reproduce_state( if isinstance(states, State): states = [states] - to_call = defaultdict(list) # type: Dict[str, List[State]] + to_call: Dict[str, List[State]] = defaultdict(list) for state in states: to_call[state.domain].append(state) @@ -191,7 +182,7 @@ async def async_reproduce_state_legacy( context: Optional[Context] = None, ) -> None: """Reproduce given state.""" - to_call = defaultdict(list) # type: Dict[Tuple[str, str], List[str]] + to_call: Dict[Tuple[str, str], List[str]] = defaultdict(list) if domain == GROUP_DOMAIN: service_domain = HASS_DOMAIN @@ -238,7 +229,7 @@ async def async_reproduce_state_legacy( key = (service, json.dumps(dict(state.attributes), sort_keys=True)) to_call[key].append(state.entity_id) - domain_tasks = [] # type: List[Awaitable[Optional[bool]]] + domain_tasks: List[Awaitable[Optional[bool]]] = [] for (service, service_data), entity_ids in to_call.items(): data = json.loads(service_data) data[ATTR_ENTITY_ID] = entity_ids diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 368753cd626eae..cd99a47cf57223 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -70,11 +70,11 @@ def __init__( self.key = key self.hass = hass self._private = private - self._data = None # type: Optional[Dict[str, Any]] + self._data: Optional[Dict[str, Any]] = None self._unsub_delay_listener = None self._unsub_stop_listener = None self._write_lock = asyncio.Lock() - self._load_task = None # type: Optional[asyncio.Future] + self._load_task: Optional[asyncio.Future] = None self._encoder = encoder @property diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index d6c7496317d5b2..9fa6e074bdd945 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -68,14 +68,14 @@ def get_location_astral_event_next( mod = -1 while True: try: - next_dt = ( + next_dt: datetime.datetime = ( getattr(location, event)( dt_util.as_local(utc_point_in_time).date() + datetime.timedelta(days=mod), local=False, ) + offset - ) # type: datetime.datetime + ) if next_dt > utc_point_in_time: return next_dt except AstralError: diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 531695c64da5ff..b9fd24c95e0908 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -82,7 +82,7 @@ def build_resources( ) -> Dict[str, Dict[str, Any]]: """Build the resources response for the given components.""" # Build response - resources = {} # type: Dict[str, Dict[str, Any]] + resources: Dict[str, Dict[str, Any]] = {} for component in components: if "." not in component: domain = component diff --git a/homeassistant/loader.py b/homeassistant/loader.py index f7c59ab0bf0af8..70284348157e76 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -127,7 +127,7 @@ async def async_get_config_flows(hass: "HomeAssistant") -> Set[str]: """Return cached list of config flows.""" from homeassistant.generated.config_flows import FLOWS - flows = set() # type: Set[str] + flows: Set[str] = set() flows.update(FLOWS) integrations = await async_get_custom_components(hass) @@ -201,14 +201,14 @@ def __init__( self.hass = hass self.pkg_path = pkg_path self.file_path = file_path - self.name = manifest["name"] # type: str - self.domain = manifest["domain"] # type: str - self.dependencies = manifest["dependencies"] # type: List[str] - self.after_dependencies = manifest.get( + self.name: str = manifest["name"] + self.domain: str = manifest["domain"] + self.dependencies: List[str] = manifest["dependencies"] + self.after_dependencies: Optional[List[str]] = manifest.get( "after_dependencies" - ) # type: Optional[List[str]] - self.requirements = manifest["requirements"] # type: List[str] - self.config_flow = manifest.get("config_flow", False) # type: bool + ) + self.requirements: List[str] = manifest["requirements"] + self.config_flow: bool = manifest.get("config_flow", False) _LOGGER.info("Loaded %s from %s", self.domain, pkg_path) @property @@ -246,9 +246,7 @@ async def async_get_integration(hass: "HomeAssistant", domain: str) -> Integrati raise IntegrationNotFound(domain) cache = hass.data[DATA_INTEGRATIONS] = {} - int_or_evt = cache.get( - domain, _UNDEF - ) # type: Union[Integration, asyncio.Event, None] + int_or_evt: Union[Integration, asyncio.Event, None] = cache.get(domain, _UNDEF) if isinstance(int_or_evt, asyncio.Event): await int_or_evt.wait() @@ -428,7 +426,7 @@ def __getattr__(self, comp_name: str) -> ModuleWrapper: integration = self._hass.data.get(DATA_INTEGRATIONS, {}).get(comp_name) if isinstance(integration, Integration): - component = integration.get_component() # type: Optional[ModuleType] + component: Optional[ModuleType] = integration.get_component() else: # Fallback to importing old-school component = _load_file(self._hass, comp_name, LOOKUP_PATHS) diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 6bfac4976771dc..480f6fc9fdeefa 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -15,7 +15,7 @@ # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any -BENCHMARKS = {} # type: Dict[str, Callable] +BENCHMARKS: Dict[str, Callable] = {} def run(args): diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 28734b30fccf06..3ac023115a102a 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -21,11 +21,11 @@ _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access -MOCKS = { +MOCKS: Dict[str, Tuple[str, Callable]] = { "load": ("homeassistant.util.yaml.loader.load_yaml", yaml_loader.load_yaml), "load*": ("homeassistant.config.load_yaml", yaml_loader.load_yaml), "secrets": ("homeassistant.util.yaml.loader.secret_yaml", yaml_loader.secret_yaml), -} # type: Dict[str, Tuple[str, Callable]] +} SILENCE = ("homeassistant.scripts.check_config.yaml_loader.clear_secret_cache",) PATCHES: Dict[str, Any] = {} @@ -82,7 +82,7 @@ def run(script_args: List) -> int: res = check(config_dir, args.secrets) - domain_info = [] # type: List[str] + domain_info: List[str] = [] if args.info: domain_info = args.info.split(",") @@ -122,7 +122,7 @@ def run(script_args: List) -> int: dump_dict(res["components"].get(domain, None)) if args.secrets: - flatsecret = {} # type: Dict[str, str] + flatsecret: Dict[str, str] = {} for sfn, sdict in res["secret_cache"].items(): sss = [] @@ -153,13 +153,13 @@ def run(script_args: List) -> int: def check(config_dir, secrets=False): """Perform a check by mocking hass load functions.""" logging.getLogger("homeassistant.loader").setLevel(logging.CRITICAL) - res = { + res: Dict[str, Any] = { "yaml_files": OrderedDict(), # yaml_files loaded "secrets": OrderedDict(), # secret cache and secrets loaded "except": OrderedDict(), # exceptions raised (with config) #'components' is a HomeAssistantConfig # noqa: E265 "secret_cache": None, - } # type: Dict[str, Any] + } # pylint: disable=possibly-unused-variable def mock_load(filename): diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index 6d37eb1ac46c4e..1e36d2d4875615 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -22,7 +22,7 @@ def __init__( self.method = method self.url = url self.status = status - self.headers = CIMultiDict(headers or {}) # type: CIMultiDict[str] + self.headers: CIMultiDict[str] = CIMultiDict(headers or {}) self.query_string = query_string or "" self._content = content diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index c658c9dfdfe67d..271b9caa62ae59 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -103,11 +103,11 @@ def _chain_future( raise TypeError("A future is required for destination argument") # pylint: disable=protected-access if isinstance(source, Future): - source_loop = source._loop # type: Optional[AbstractEventLoop] + source_loop: Optional[AbstractEventLoop] = source._loop else: source_loop = None if isinstance(destination, Future): - dest_loop = destination._loop # type: Optional[AbstractEventLoop] + dest_loop: Optional[AbstractEventLoop] = destination._loop else: dest_loop = None @@ -152,7 +152,7 @@ def run_coroutine_threadsafe( if not coroutines.iscoroutine(coro): raise TypeError("A coroutine object is required") - future = concurrent.futures.Future() # type: concurrent.futures.Future + future: concurrent.futures.Future = concurrent.futures.Future() def callback() -> None: """Handle the call to the coroutine.""" @@ -200,7 +200,7 @@ def run_callback_threadsafe( if ident is not None and ident == threading.get_ident(): raise RuntimeError("Cannot be called from within the event loop") - future = concurrent.futures.Future() # type: concurrent.futures.Future + future: concurrent.futures.Future = concurrent.futures.Future() def run_callback() -> None: """Run callback and store result.""" diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 9a456e6f4008b1..a948c4407ae6e7 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -1,25 +1,17 @@ """Helper methods to handle the time in Home Assistant.""" import datetime as dt import re -from typing import ( - Any, - Union, - Optional, # noqa pylint: disable=unused-import - Tuple, - List, - cast, - Dict, -) +from typing import Any, Union, Optional, Tuple, List, cast, Dict import pytz import pytz.exceptions as pytzexceptions -import pytz.tzinfo as pytzinfo # noqa pylint: disable=unused-import +import pytz.tzinfo as pytzinfo from homeassistant.const import MATCH_ALL DATE_STR_FORMAT = "%Y-%m-%d" UTC = pytz.utc -DEFAULT_TIME_ZONE = pytz.utc # type: dt.tzinfo +DEFAULT_TIME_ZONE: dt.tzinfo = pytz.utc # Copyright (c) Django Software Foundation and individual contributors. @@ -83,7 +75,7 @@ def as_utc(dattim: dt.datetime) -> dt.datetime: def as_timestamp(dt_value: dt.datetime) -> float: """Convert a date/time into a unix time (seconds since 1970).""" if hasattr(dt_value, "timestamp"): - parsed_dt = dt_value # type: Optional[dt.datetime] + parsed_dt: Optional[dt.datetime] = dt_value else: parsed_dt = parse_datetime(str(dt_value)) if parsed_dt is None: @@ -111,7 +103,7 @@ def start_of_local_day( ) -> dt.datetime: """Return local datetime object of start of day from date or datetime.""" if dt_or_d is None: - date = now().date() # type: dt.date + date: dt.date = now().date() elif isinstance(dt_or_d, dt.datetime): date = dt_or_d.date() return DEFAULT_TIME_ZONE.localize( # type: ignore @@ -133,12 +125,12 @@ def parse_datetime(dt_str: str) -> Optional[dt.datetime]: match = DATETIME_RE.match(dt_str) if not match: return None - kws = match.groupdict() # type: Dict[str, Any] + kws: Dict[str, Any] = match.groupdict() if kws["microsecond"]: kws["microsecond"] = kws["microsecond"].ljust(6, "0") tzinfo_str = kws.pop("tzinfo") - tzinfo = None # type: Optional[dt.tzinfo] + tzinfo: Optional[dt.tzinfo] = None if tzinfo_str == "Z": tzinfo = UTC elif tzinfo_str is not None: @@ -324,7 +316,7 @@ def _lower_bound(arr: List[int], cmp: int) -> Optional[int]: # Now we need to handle timezones. We will make this datetime object # "naive" first and then re-convert it to the target timezone. # This is so that we can call pytz's localize and handle DST changes. - tzinfo = result.tzinfo # type: pytzinfo.DstTzInfo + tzinfo: pytzinfo.DstTzInfo = result.tzinfo result = result.replace(tzinfo=None) try: diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index c5927c0ce45a26..236e2fc1aa24de 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -34,7 +34,7 @@ def __init__(self, loop: AbstractEventLoop, handler: logging.Handler) -> None: """Initialize async logging handler wrapper.""" self.handler = handler self.loop = loop - self._queue = asyncio.Queue(loop=loop) # type: asyncio.Queue + self._queue: asyncio.Queue = asyncio.Queue(loop=loop) self._thread = threading.Thread(target=self._process) # Delegate from handler diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index 70f447a3b0d618..b7e8927888c734 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -22,7 +22,7 @@ class ExtSafeConstructor(SafeConstructor): """Extended SafeConstructor.""" - name = None # type: Optional[str] + name: Optional[str] = None class UnsupportedYamlError(HomeAssistantError): @@ -67,7 +67,7 @@ def object_to_yaml(data: JSON_TYPE) -> str: stream = StringIO() try: yaml.dump(data, stream) - result = stream.getvalue() # type: str + result: str = stream.getvalue() return result except YAMLError as exc: _LOGGER.error("YAML error: %s", exc) @@ -78,7 +78,7 @@ def yaml_to_object(data: str) -> JSON_TYPE: """Create object from yaml string.""" yaml = YAML(typ="rt") try: - result = yaml.load(data) # type: Union[List, Dict, str] + result: Union[List, Dict, str] = yaml.load(data) return result except YAMLError as exc: _LOGGER.error("YAML error: %s", exc) diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index d79a9da192296b..23ac8f05025617 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -75,7 +75,7 @@ def __init__( pressure: str, ) -> None: """Initialize the unit system object.""" - errors = ", ".join( + errors: str = ", ".join( UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit, unit_type) for unit, unit_type in [ (temperature, TEMPERATURE), @@ -85,7 +85,7 @@ def __init__( (pressure, PRESSURE), ] if not is_valid_unit(unit, unit_type) - ) # type: str + ) if errors: raise ValueError(errors) diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index a6fba4d04d84a1..a53dc0cdd02030 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -29,7 +29,7 @@ def represent_odict( # type: ignore dump, tag, mapping, flow_style=None ) -> yaml.MappingNode: """Like BaseRepresenter.represent_mapping but does not issue the sort().""" - value = [] # type: list + value: list = [] node = yaml.MappingNode(tag, value, flow_style=flow_style) if dump.alias_key is not None: dump.represented_objects[dump.alias_key] = node diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index f7e44178acb0f2..ccc55691ee1b74 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -26,12 +26,12 @@ # mypy: allow-untyped-calls, no-warn-return-any -_LOGGER = logging.getLogger(__name__) -__SECRET_CACHE = {} # type: Dict[str, JSON_TYPE] - JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name DICT_T = TypeVar("DICT_T", bound=Dict) # pylint: disable=invalid-name +_LOGGER = logging.getLogger(__name__) +__SECRET_CACHE: Dict[str, JSON_TYPE] = {} + def clear_secret_cache() -> None: """Clear the secret cache. @@ -47,10 +47,8 @@ class SafeLineLoader(yaml.SafeLoader): def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node: """Annotate a node with the first line it was seen.""" - last_line = self.line # type: int - node = super(SafeLineLoader, self).compose_node( - parent, index - ) # type: yaml.nodes.Node + last_line: int = self.line + node: yaml.nodes.Node = super(SafeLineLoader, self).compose_node(parent, index) node.__line__ = last_line + 1 # type: ignore return node @@ -141,7 +139,7 @@ def _include_dir_named_yaml( loader: SafeLineLoader, node: yaml.nodes.Node ) -> OrderedDict: """Load multiple files from directory as a dictionary.""" - mapping = OrderedDict() # type: OrderedDict + mapping: OrderedDict = OrderedDict() loc = os.path.join(os.path.dirname(loader.name), node.value) for fname in _find_files(loc, "*.yaml"): filename = os.path.splitext(os.path.basename(fname))[0] @@ -155,7 +153,7 @@ def _include_dir_merge_named_yaml( loader: SafeLineLoader, node: yaml.nodes.Node ) -> OrderedDict: """Load multiple files from directory as a merged dictionary.""" - mapping = OrderedDict() # type: OrderedDict + mapping: OrderedDict = OrderedDict() loc = os.path.join(os.path.dirname(loader.name), node.value) for fname in _find_files(loc, "*.yaml"): if os.path.basename(fname) == SECRET_YAML: @@ -182,8 +180,8 @@ def _include_dir_merge_list_yaml( loader: SafeLineLoader, node: yaml.nodes.Node ) -> JSON_TYPE: """Load multiple files from directory as a merged list.""" - loc = os.path.join(os.path.dirname(loader.name), node.value) # type: str - merged_list = [] # type: List[JSON_TYPE] + loc: str = os.path.join(os.path.dirname(loader.name), node.value) + merged_list: List[JSON_TYPE] = [] for fname in _find_files(loc, "*.yaml"): if os.path.basename(fname) == SECRET_YAML: continue @@ -198,7 +196,7 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order loader.flatten_mapping(node) nodes = loader.construct_pairs(node) - seen = {} # type: Dict + seen: Dict = {} for (key, _), (child_node, _) in zip(nodes, node.value): line = child_node.start_mark.line From ca97bba4b4e3d3523caf70ead6641f661fae1b59 Mon Sep 17 00:00:00 2001 From: Taylor Silva Date: Wed, 4 Sep 2019 01:00:20 -0400 Subject: [PATCH 0148/3953] Add X10 devices as ISY994 switches (#26342) * Add X10 devices as switches Signed-off-by: Taylor Silva * Add logging message for when unsupported device is found. This will help a user figure out why they aren't seeing all devices. Signed-off-by: Taylor Silva * Update homeassistant/components/isy994/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/isy994/__init__.py 100 is too generic of an ISY uom. We don't want to accidentally add non-switch devices. --- homeassistant/components/isy994/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 407196532ef7fd..e1d24fa5551041 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -138,7 +138,7 @@ "Siren", "Siren_ADV", ], - "insteon_type": ["2.", "9.10.", "9.11."], + "insteon_type": ["2.", "9.10.", "9.11.", "113."], }, } @@ -182,6 +182,7 @@ def _check_for_node_def(hass: HomeAssistant, node, single_domain: str = None) -> hass.data[ISY994_NODES][domain].append(node) return True + _LOGGER.warning("Unsupported node: %s, type: %s", node.name, node.type) return False From b4058b5c7f86f30e7935a27d4de3cdd9808e2e19 Mon Sep 17 00:00:00 2001 From: Quentame Date: Wed, 4 Sep 2019 07:04:26 +0200 Subject: [PATCH 0149/3953] Add config flow to linky (#26076) * Linky: setup ConfigFlow * async_track_time_interval * Review from @MartinHjelmare 1 * Review from @MartinHjelmare 2 * Review from @MartinHjelmare 3 * Review from @MartinHjelmare 4 * black --fast homeassistant tests * Bump pylinky to 0.4.0 and add error user feedback * Fix .coveragerc * Linky platform moved to integration in config.yml and with multiple accounts * Remove useless logs * Review from @MartinHjelmare 5 * Add config flow tests * Add config flow tests : login + fetch on failed --- .coveragerc | 1 + CODEOWNERS | 2 +- .../components/linky/.translations/en.json | 25 +++ homeassistant/components/linky/__init__.py | 54 ++++++ homeassistant/components/linky/config_flow.py | 118 +++++++++++++ homeassistant/components/linky/const.py | 5 + homeassistant/components/linky/manifest.json | 4 +- homeassistant/components/linky/sensor.py | 101 +++++------ homeassistant/components/linky/strings.json | 25 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/linky/__init__.py | 1 + tests/components/linky/test_config_flow.py | 167 ++++++++++++++++++ 15 files changed, 452 insertions(+), 58 deletions(-) create mode 100644 homeassistant/components/linky/.translations/en.json create mode 100644 homeassistant/components/linky/config_flow.py create mode 100644 homeassistant/components/linky/const.py create mode 100644 homeassistant/components/linky/strings.json create mode 100644 tests/components/linky/__init__.py create mode 100644 tests/components/linky/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 6583f6e0ae5e1b..001729ace5bc6c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -339,6 +339,7 @@ omit = homeassistant/components/limitlessled/light.py homeassistant/components/linksys_ap/device_tracker.py homeassistant/components/linksys_smart/device_tracker.py + homeassistant/components/linky/__init__.py homeassistant/components/linky/sensor.py homeassistant/components/linode/* homeassistant/components/linux_battery/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 9c2c8673e6e365..32c2f5c2930eb4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -153,7 +153,7 @@ homeassistant/components/life360/* @pnbruckner homeassistant/components/lifx/* @amelchio homeassistant/components/lifx_cloud/* @amelchio homeassistant/components/lifx_legacy/* @amelchio -homeassistant/components/linky/* @tiste @Quentame +homeassistant/components/linky/* @Quentame homeassistant/components/linux_battery/* @fabaff homeassistant/components/liveboxplaytv/* @pschmitt homeassistant/components/logger/* @home-assistant/core diff --git a/homeassistant/components/linky/.translations/en.json b/homeassistant/components/linky/.translations/en.json new file mode 100644 index 00000000000000..6c655b835811cd --- /dev/null +++ b/homeassistant/components/linky/.translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Account already configured" + }, + "error": { + "access": "Could not access to Enedis.fr, please check your internet connection", + "enedis": "Enedis.fr answered with an error: please retry later (usually not between 11PM and 2AM)", + "unknown": "Unknown error: please retry later (usually not between 11PM and 2AM)", + "username_exists": "Account already configured", + "wrong_login": "Login error: please check your email & password" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email" + }, + "description": "Enter your credentials", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/__init__.py b/homeassistant/components/linky/__init__.py index 345f13e8a57af2..a7f3d7bb03e9f5 100644 --- a/homeassistant/components/linky/__init__.py +++ b/homeassistant/components/linky/__init__.py @@ -1 +1,55 @@ """The linky component.""" +import logging + +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DEFAULT_TIMEOUT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +ACCOUNT_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]))}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up Linky sensors from legacy config file.""" + + conf = config.get(DOMAIN) + if conf is None: + return True + + for linky_account_conf in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=linky_account_conf.copy(), + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up Linky sensors.""" + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + + return True diff --git a/homeassistant/components/linky/config_flow.py b/homeassistant/components/linky/config_flow.py new file mode 100644 index 00000000000000..3b882eed2ad9a3 --- /dev/null +++ b/homeassistant/components/linky/config_flow.py @@ -0,0 +1,118 @@ +"""Config flow to configure the Linky integration.""" +import logging + +import voluptuous as vol +from pylinky.client import LinkyClient +from pylinky.exceptions import ( + PyLinkyAccessException, + PyLinkyEnedisException, + PyLinkyException, + PyLinkyWrongLoginException, +) + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.core import callback + +from .const import DEFAULT_TIMEOUT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class LinkyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize Linky config flow.""" + self._username = None + self._password = None + self._timeout = None + + def _configuration_exists(self, username: str) -> bool: + """Return True if username exists in configuration.""" + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_USERNAME] == username: + return True + return False + + @callback + def _show_setup_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ): str, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + } + ), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is None: + return self._show_setup_form(user_input, None) + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + self._timeout = user_input.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) + + if self._configuration_exists(self._username): + errors[CONF_USERNAME] = "username_exists" + return self._show_setup_form(user_input, errors) + + client = LinkyClient(self._username, self._password, None, self._timeout) + try: + await self.hass.async_add_executor_job(client.login) + await self.hass.async_add_executor_job(client.fetch_data) + except PyLinkyAccessException as exp: + _LOGGER.error(exp) + errors["base"] = "access" + return self._show_setup_form(user_input, errors) + except PyLinkyEnedisException as exp: + _LOGGER.error(exp) + errors["base"] = "enedis" + return self._show_setup_form(user_input, errors) + except PyLinkyWrongLoginException as exp: + _LOGGER.error(exp) + errors["base"] = "wrong_login" + return self._show_setup_form(user_input, errors) + except PyLinkyException as exp: + _LOGGER.error(exp) + errors["base"] = "unknown" + return self._show_setup_form(user_input, errors) + finally: + client.close_session() + + return self.async_create_entry( + title=self._username, + data={ + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_TIMEOUT: self._timeout, + }, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry. + + Only host was required in the yaml file all other fields are optional + """ + if self._configuration_exists(user_input[CONF_USERNAME]): + return self.async_abort(reason="username_exists") + + return await self.async_step_user(user_input) diff --git a/homeassistant/components/linky/const.py b/homeassistant/components/linky/const.py new file mode 100644 index 00000000000000..e8e6886752864b --- /dev/null +++ b/homeassistant/components/linky/const.py @@ -0,0 +1,5 @@ +"""Linky component constants.""" + +DOMAIN = "linky" + +DEFAULT_TIMEOUT = 10 diff --git a/homeassistant/components/linky/manifest.json b/homeassistant/components/linky/manifest.json index cd4ac4665e2801..10a5bbcf86498c 100644 --- a/homeassistant/components/linky/manifest.json +++ b/homeassistant/components/linky/manifest.json @@ -1,13 +1,13 @@ { "domain": "linky", "name": "Linky", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/linky", "requirements": [ - "pylinky==0.3.3" + "pylinky==0.4.0" ], "dependencies": [], "codeowners": [ - "@tiste", "@Quentame" ] } diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index 98aca67d8ea972..bd2d38735d6192 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -1,12 +1,12 @@ """Support for Linky.""" -from datetime import timedelta import json import logging +from datetime import timedelta -from pylinky.client import DAILY, MONTHLY, YEARLY, LinkyClient, PyLinkyError -import voluptuous as vol +from pylinky.client import DAILY, MONTHLY, YEARLY, LinkyClient +from pylinky.client import PyLinkyException -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_PASSWORD, @@ -14,10 +14,9 @@ CONF_USERNAME, ENERGY_KILO_WATT_HOUR, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval -import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -29,7 +28,6 @@ INDEX_LAST = -2 ATTRIBUTION = "Data provided by Enedis" -DEFAULT_TIMEOUT = 10 SENSORS = { "yesterday": ("Linky yesterday", DAILY, INDEX_LAST), "current_month": ("Linky current month", MONTHLY, INDEX_CURRENT), @@ -41,59 +39,54 @@ SENSORS_INDEX_SCALE = 1 SENSORS_INDEX_WHEN = 2 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - } -) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up the Linky platform.""" + pass + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Add Linky entries.""" + account = LinkyAccount( + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], entry.data[CONF_TIMEOUT] + ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Configure the platform and add the Linky sensor.""" - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - timeout = config[CONF_TIMEOUT] + await hass.async_add_executor_job(account.update_linky_data) - account = LinkyAccount(hass, add_entities, username, password, timeout) - add_entities(account.sensors, True) + sensors = [ + LinkySensor("Linky yesterday", account, DAILY, INDEX_LAST), + LinkySensor("Linky current month", account, MONTHLY, INDEX_CURRENT), + LinkySensor("Linky last month", account, MONTHLY, INDEX_LAST), + LinkySensor("Linky current year", account, YEARLY, INDEX_CURRENT), + LinkySensor("Linky last year", account, YEARLY, INDEX_LAST), + ] + + async_track_time_interval(hass, account.update_linky_data, SCAN_INTERVAL) + + async_add_entities(sensors, True) class LinkyAccount: """Representation of a Linky account.""" - def __init__(self, hass, add_entities, username, password, timeout): + def __init__(self, username, password, timeout): """Initialise the Linky account.""" self._username = username - self.__password = password + self._password = password self._timeout = timeout self._data = None - self.sensors = [] - - self.update_linky_data(dt_util.utcnow()) - - self.sensors.append(LinkySensor("Linky yesterday", self, DAILY, INDEX_LAST)) - self.sensors.append( - LinkySensor("Linky current month", self, MONTHLY, INDEX_CURRENT) - ) - self.sensors.append(LinkySensor("Linky last month", self, MONTHLY, INDEX_LAST)) - self.sensors.append( - LinkySensor("Linky current year", self, YEARLY, INDEX_CURRENT) - ) - self.sensors.append(LinkySensor("Linky last year", self, YEARLY, INDEX_LAST)) - - track_time_interval(hass, self.update_linky_data, SCAN_INTERVAL) - def update_linky_data(self, event_time): + def update_linky_data(self, event_time=None): """Fetch new state data for the sensor.""" - client = LinkyClient(self._username, self.__password, None, self._timeout) + client = LinkyClient(self._username, self._password, None, self._timeout) try: client.login() client.fetch_data() self._data = client.get_data() _LOGGER.debug(json.dumps(self._data, indent=2)) - except PyLinkyError as exp: + except PyLinkyException as exp: _LOGGER.error(exp) finally: client.close_session() @@ -115,12 +108,12 @@ class LinkySensor(Entity): def __init__(self, name, account: LinkyAccount, scale, when): """Initialize the sensor.""" self._name = name - self.__account = account + self._account = account self._scale = scale - self.__when = when + self._when = when self._username = account.username - self.__time = None - self.__consumption = None + self._time = None + self._consumption = None @property def name(self): @@ -130,7 +123,7 @@ def name(self): @property def state(self): """Return the state of the sensor.""" - return self.__consumption + return self._consumption @property def unit_of_measurement(self): @@ -147,18 +140,18 @@ def device_state_attributes(self): """Return the state attributes of the sensor.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - "time": self.__time, + "time": self._time, CONF_USERNAME: self._username, } - def update(self): + async def async_update(self) -> None: """Retrieve the new data for the sensor.""" - data = self.__account.data[self._scale][self.__when] - self.__consumption = data[CONSUMPTION] - self.__time = data[TIME] + data = self._account.data[self._scale][self._when] + self._consumption = data[CONSUMPTION] + self._time = data[TIME] if self._scale is not YEARLY: year_index = INDEX_CURRENT - if self.__time.endswith("Dec"): + if self._time.endswith("Dec"): year_index = INDEX_LAST - self.__time += " " + self.__account.data[YEARLY][year_index][TIME] + self._time += " " + self._account.data[YEARLY][year_index][TIME] diff --git a/homeassistant/components/linky/strings.json b/homeassistant/components/linky/strings.json new file mode 100644 index 00000000000000..e5aa04cad1f367 --- /dev/null +++ b/homeassistant/components/linky/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "title": "Linky", + "step": { + "user": { + "title": "Linky", + "description": "Enter your credentials", + "data": { + "username": "Email", + "password": "Password" + } + } + }, + "error":{ + "username_exists": "Account already configured", + "access": "Could not access to Enedis.fr, please check your internet connection", + "enedis": "Enedis.fr answered with an error: please retry later (usually not between 11PM and 2AM)", + "wrong_login": "Login error: please check your email & password", + "unknown": "Unknown error: please retry later (usually not between 11PM and 2AM)" + }, + "abort":{ + "username_exists": "Account already configured" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index dadb68642bcabd..32690153221b65 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -30,6 +30,7 @@ "iqvia", "life360", "lifx", + "linky", "locative", "logi_circle", "luftdaten", diff --git a/requirements_all.txt b/requirements_all.txt index 367de8014f91f9..fe468084aad903 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1259,7 +1259,7 @@ pylgnetcast-homeassistant==0.2.0.dev0 pylgtv==0.1.9 # homeassistant.components.linky -pylinky==0.3.3 +pylinky==0.4.0 # homeassistant.components.litejet pylitejet==0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6013efee06ddb..a23bc7ce610ee8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -291,6 +291,9 @@ pyhomematic==0.1.60 # homeassistant.components.iqvia pyiqvia==0.2.1 +# homeassistant.components.linky +pylinky==0.4.0 + # homeassistant.components.litejet pylitejet==0.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 39e5de3e2b0828..1468969d9dd719 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -120,6 +120,7 @@ "pyheos", "pyhomematic", "pyiqvia", + "pylinky", "pylitejet", "pymfy", "pymonoprice", diff --git a/tests/components/linky/__init__.py b/tests/components/linky/__init__.py new file mode 100644 index 00000000000000..f461885e384e23 --- /dev/null +++ b/tests/components/linky/__init__.py @@ -0,0 +1 @@ +"""Tests for the Linky component.""" diff --git a/tests/components/linky/test_config_flow.py b/tests/components/linky/test_config_flow.py new file mode 100644 index 00000000000000..f18ce72c1c37d6 --- /dev/null +++ b/tests/components/linky/test_config_flow.py @@ -0,0 +1,167 @@ +"""Tests for the Linky config flow.""" +import pytest +from unittest.mock import patch +from pylinky.exceptions import ( + PyLinkyAccessException, + PyLinkyEnedisException, + PyLinkyException, + PyLinkyWrongLoginException, +) + +from homeassistant import data_entry_flow +from homeassistant.components.linky import config_flow +from homeassistant.components.linky.const import DOMAIN, DEFAULT_TIMEOUT +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME + +from tests.common import MockConfigEntry + +USERNAME = "username" +PASSWORD = "password" +TIMEOUT = 20 + + +@pytest.fixture(name="login") +def mock_controller_login(): + """Mock a successful login.""" + with patch("pylinky.client.LinkyClient.login", return_value=True): + yield + + +@pytest.fixture(name="fetch_data") +def mock_controller_fetch_data(): + """Mock a successful get data.""" + with patch("pylinky.client.LinkyClient.fetch_data", return_value={}): + yield + + +@pytest.fixture(name="close_session") +def mock_controller_close_session(): + """Mock a successful closing session.""" + with patch("pylinky.client.LinkyClient.close_session", return_value=None): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.LinkyFlowHandler() + flow.hass = hass + return flow + + +async def test_user(hass, login, fetch_data, close_session): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # test with all provided + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_TIMEOUT] == DEFAULT_TIMEOUT + + +async def test_import(hass, login, fetch_data, close_session): + """Test import step.""" + flow = init_config_flow(hass) + + # import with username and password + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_TIMEOUT] == DEFAULT_TIMEOUT + + # import with all + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_TIMEOUT: TIMEOUT} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_TIMEOUT] == TIMEOUT + + +async def test_abort_if_already_setup(hass, login, fetch_data, close_session): + """Test we abort if Linky is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain=DOMAIN, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ).add_to_hass(hass) + + # Should fail, same USERNAME (import) + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "username_exists" + + # Should fail, same USERNAME (flow) + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_USERNAME: "username_exists"} + + +async def test_abort_on_login_failed(hass, close_session): + """Test when we have errors during login.""" + flow = init_config_flow(hass) + + with patch( + "pylinky.client.LinkyClient.login", side_effect=PyLinkyAccessException() + ): + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "access"} + + with patch( + "pylinky.client.LinkyClient.login", side_effect=PyLinkyWrongLoginException() + ): + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "wrong_login"} + + +async def test_abort_on_fetch_failed(hass, login, close_session): + """Test when we have errors during fetch.""" + flow = init_config_flow(hass) + + with patch( + "pylinky.client.LinkyClient.fetch_data", side_effect=PyLinkyAccessException() + ): + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "access"} + + with patch( + "pylinky.client.LinkyClient.fetch_data", side_effect=PyLinkyEnedisException() + ): + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "enedis"} + + with patch("pylinky.client.LinkyClient.fetch_data", side_effect=PyLinkyException()): + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} From 22d3cf41171cba1dc86189a6f9a6fff171d7ea0e Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Tue, 3 Sep 2019 23:11:30 -0700 Subject: [PATCH 0150/3953] Bump ISY994's PyISY dependency to 1.1.2 (#26413) Fixed a major bug that was responsible for ISY events getting seemingly random delays up to 24 seconds --- homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 7860c080b2fe05..0dd0f1eae80a0b 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Isy994", "documentation": "https://www.home-assistant.io/components/isy994", "requirements": [ - "PyISY==1.1.1" + "PyISY==1.1.2" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index fe468084aad903..852788e1be305f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -50,7 +50,7 @@ PyEssent==0.13 PyGithub==1.43.5 # homeassistant.components.isy994 -PyISY==1.1.1 +PyISY==1.1.2 # homeassistant.components.mvglive PyMVGLive==1.1.4 From b9923ca109c337eafed67627cd1fb6204abffb33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 4 Sep 2019 09:13:17 +0300 Subject: [PATCH 0151/3953] Met, check for existing location (#26400) --- .../components/met/.translations/en.json | 2 +- homeassistant/components/met/config_flow.py | 18 +++++++++++++----- homeassistant/components/met/strings.json | 2 +- tests/components/met/test_config_flow.py | 4 +++- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/met/.translations/en.json b/homeassistant/components/met/.translations/en.json index 21ae7cb78fa4f9..93d028b06261ba 100644 --- a/homeassistant/components/met/.translations/en.json +++ b/homeassistant/components/met/.translations/en.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Name already exists" + "name_exists": "Location already exists" }, "step": { "user": { diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 795ba57d9887b2..c7ff4973c7d2aa 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -12,9 +12,15 @@ @callback def configured_instances(hass): """Return a set of configured SimpliSafe instances.""" - return set( - entry.data[CONF_NAME] for entry in hass.config_entries.async_entries(DOMAIN) - ) + entites = [] + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data.get("track_home"): + entites.append("home") + continue + entites.append( + f"{entry.data.get(CONF_LATITUDE)}-{entry.data.get(CONF_LONGITUDE)}" + ) + return set(entites) class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -32,11 +38,13 @@ async def async_step_user(self, user_input=None): self._errors = {} if user_input is not None: - if user_input[CONF_NAME] not in configured_instances(self.hass): + if ( + f"{user_input.get(CONF_LATITUDE)}-{user_input.get(CONF_LONGITUDE)}" + not in configured_instances(self.hass) + ): return self.async_create_entry( title=user_input[CONF_NAME], data=user_input ) - self._errors[CONF_NAME] = "name_exists" return await self._show_config_form( diff --git a/homeassistant/components/met/strings.json b/homeassistant/components/met/strings.json index f5c49bac3c4420..0c52e624418a29 100644 --- a/homeassistant/components/met/strings.json +++ b/homeassistant/components/met/strings.json @@ -14,7 +14,7 @@ } }, "error": { - "name_exists": "Name already exists" + "name_exists": "Location already exists" } } } diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 22061386b93c56..32f3be676e0514 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -102,7 +102,7 @@ async def test_flow_entry_created_from_user_input(): async def test_flow_entry_config_entry_already_exists(): """Test that create data from user input and config_entry already exists. - Test when the form should show when user puts existing name + Test when the form should show when user puts existing location in the config gui. Then the form should show with error """ hass = Mock() @@ -112,6 +112,8 @@ async def test_flow_entry_config_entry_already_exists(): first_entry = MockConfigEntry(domain="met") first_entry.data["name"] = "home" + first_entry.data[CONF_LONGITUDE] = "0" + first_entry.data[CONF_LATITUDE] = "0" first_entry.add_to_hass(hass) test_data = { From 92f83628832eb70dfbf085f1c8159a7df25835f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Sep 2019 23:13:34 -0700 Subject: [PATCH 0152/3953] Allow core config updated (#26398) --- homeassistant/components/websocket_api/permissions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index 7aa845a298ddb2..ffbb80fa19ec7b 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -8,6 +8,7 @@ EVENT_SERVICE_REMOVED, EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, + EVENT_CORE_CONFIG_UPDATE, ) from homeassistant.components.persistent_notification import ( EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, @@ -22,6 +23,7 @@ # Except for state_changed, which is handled accordingly. SUBSCRIBE_WHITELIST = { EVENT_COMPONENT_LOADED, + EVENT_CORE_CONFIG_UPDATE, EVENT_PANELS_UPDATED, EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, EVENT_SERVICE_REGISTERED, From 60ef41cc69a563b98600240dc5b44f333121bd6b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Sep 2019 08:13:53 +0200 Subject: [PATCH 0153/3953] Correct file permissions in slide integration (#26390) --- homeassistant/components/slide/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 homeassistant/components/slide/__init__.py diff --git a/homeassistant/components/slide/__init__.py b/homeassistant/components/slide/__init__.py old mode 100755 new mode 100644 From 525a434511468f1190db3873d3ddca0be2a9fd85 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 4 Sep 2019 07:14:30 +0100 Subject: [PATCH 0154/3953] Use literal string interpolation in honeywell (#26386) * Initial commit * fix lint hints --- homeassistant/components/honeywell/climate.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 84a816628c0626..4b73cf4f2b5f47 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -161,9 +161,7 @@ def __init__( self._password = password _LOGGER.debug( - # noqa; pylint: disable=protected-access - "latestData = %s ", - device._data, + "latestData = %s ", device._data # pylint: disable=protected-access ) # not all honeywell HVACs support all modes @@ -176,8 +174,7 @@ def __init__( | SUPPORT_TARGET_TEMPERATURE_RANGE ) - # noqa; pylint: disable=protected-access - if device._data["canControlHumidification"]: + if device._data["canControlHumidification"]: # pylint: disable=protected-access self._supported_features |= SUPPORT_TARGET_HUMIDITY if device.raw_ui_data["SwitchEmergencyHeatAllowed"]: @@ -320,7 +317,7 @@ def _set_temperature(self, **kwargs) -> None: # Set hold if this is not the case if getattr(self._device, f"hold_{mode}") is False: # Get next period key - next_period_key = "{}NextPeriod".format(mode.capitalize()) + next_period_key = f"{mode.capitalize()}NextPeriod" # Get next period raw value next_period = self._device.raw_ui_data.get(next_period_key) # Get next period time @@ -460,7 +457,5 @@ def update(self) -> None: _LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp) _LOGGER.debug( - # noqa; pylint: disable=protected-access - "latestData = %s ", - self._device._data, + "latestData = %s ", self._device._data # pylint: disable=protected-access ) From d5c61be6518bf705e5297649cdc9868fdd56b2db Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 4 Sep 2019 07:15:40 +0100 Subject: [PATCH 0155/3953] Initial commit (#26385) --- homeassistant/components/geniushub/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index e87a43fc1ae688..82db3d4224e45a 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -59,7 +59,7 @@ def icon(self) -> str: icon = "mdi:battery" if battery_level <= 95: - icon += "-{}".format(int(round(battery_level / 10 - 0.01)) * 10) + icon += f"-{int(round(battery_level / 10 - 0.01)) * 10}" return icon From 4661f2a6df2e353b104c42277bd9d7ae779fdc20 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 4 Sep 2019 07:16:31 +0100 Subject: [PATCH 0156/3953] Initial commit (#26383) --- homeassistant/components/evohome/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index adb6e856984079..ba7a72024ed16b 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -330,7 +330,7 @@ def setpoints(self) -> Dict[str, Any]: switchpoint = day["Switchpoints"][idx] dt_naive = datetime.strptime( - "{}T{}".format(sp_date, switchpoint["TimeOfDay"]), "%Y-%m-%dT%H:%M:%S" + f"{sp_date}T{switchpoint['TimeOfDay']}", "%Y-%m-%dT%H:%M:%S" ) spt["from"] = _local_dt_to_utc(dt_naive).isoformat() From b50ac6f4869081cb930adbbadcec9532d9b1e239 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 3 Sep 2019 10:17:03 +0200 Subject: [PATCH 0157/3953] Upgrade pyhaversion to 3.1.0 (#26232) --- homeassistant/components/version/manifest.json | 2 +- homeassistant/components/version/sensor.py | 12 ++++++++++-- requirements_all.txt | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 2a48f91a6f8887..815e7ff9a25794 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -3,7 +3,7 @@ "name": "Version", "documentation": "https://www.home-assistant.io/components/version", "requirements": [ - "pyhaversion==3.0.2" + "pyhaversion==3.1.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 438ea8f690cda4..3e00b87e9840d2 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -28,7 +28,7 @@ "odroid-c2", "odroid-xu", ] -ALL_SOURCES = ["local", "pypi", "hassio", "docker"] +ALL_SOURCES = ["local", "pypi", "hassio", "docker", "haio"] CONF_BETA = "beta" CONF_IMAGE = "image" @@ -54,7 +54,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Version sensor platform.""" - from pyhaversion import LocalVersion, DockerVersion, HassioVersion, PyPiVersion + from pyhaversion import ( + LocalVersion, + DockerVersion, + HassioVersion, + PyPiVersion, + HaIoVersion, + ) beta = config.get(CONF_BETA) image = config.get(CONF_IMAGE) @@ -74,6 +80,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= haversion = VersionData(HassioVersion(hass.loop, session, branch, image)) elif source == "docker": haversion = VersionData(DockerVersion(hass.loop, session, branch, image)) + elif source == "haio": + haversion = VersionData(HaIoVersion(hass.loop, session)) else: haversion = VersionData(LocalVersion(hass.loop, session)) diff --git a/requirements_all.txt b/requirements_all.txt index 2bf7e6a841cc0a..42272a0ccbb186 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1186,7 +1186,7 @@ pygtfs==0.1.5 pygtt==1.1.2 # homeassistant.components.version -pyhaversion==3.0.2 +pyhaversion==3.1.0 # homeassistant.components.heos pyheos==0.6.0 From a74bb3fd5e7b66bf6afb24cc4d831a5303fb797f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 3 Sep 2019 07:12:10 +0200 Subject: [PATCH 0158/3953] String has nothing to do with class method naming (#26368) --- homeassistant/components/deconz/.translations/en.json | 2 +- homeassistant/components/deconz/strings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 57da3c706a035c..3c6656d6ae696b 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -43,7 +43,7 @@ }, "options": { "step": { - "async_step_deconz_devices": { + "deconz_devices": { "data": { "allow_clip_sensor": "Allow deCONZ CLIP sensors", "allow_deconz_groups": "Allow deCONZ light groups" diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index ea9ea2805155c6..7081f816e6ae08 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -43,7 +43,7 @@ }, "options": { "step": { - "async_step_deconz_devices": { + "deconz_devices": { "description": "Configure visibility of deCONZ device types", "data": { "allow_clip_sensor": "Allow deCONZ CLIP sensors", From a980eedd226321732ff2a6889dc23a49363f217b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 3 Sep 2019 14:14:33 +0200 Subject: [PATCH 0159/3953] Fix race during initial Sonos group construction (#26371) * Fix race during initial Sonos group construction * Update homeassistant/components/sonos/media_player.py --- homeassistant/components/sonos/media_player.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 86e30621334579..70461ad15d2fa6 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -337,8 +337,16 @@ def __init__(self, player): async def async_added_to_hass(self): """Subscribe sonos events.""" await self.async_seen() + self.hass.data[DATA_SONOS].entities.append(self) + def _rebuild_groups(): + """Build the current group topology.""" + for entity in self.hass.data[DATA_SONOS].entities: + entity.update_groups() + + self.hass.async_add_executor_job(_rebuild_groups) + @property def unique_id(self): """Return a unique ID.""" @@ -469,10 +477,6 @@ def _attach_player(self): self.update_volume() self._set_favorites() - # New player available, build the current group topology - for entity in self.hass.data[DATA_SONOS].entities: - entity.update_groups() - player = self.soco def subscribe(service, action): From d4905477b812213da2333103c9fdd789151ae2d8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Sep 2019 23:13:34 -0700 Subject: [PATCH 0160/3953] Allow core config updated (#26398) --- homeassistant/components/websocket_api/permissions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index 7aa845a298ddb2..ffbb80fa19ec7b 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -8,6 +8,7 @@ EVENT_SERVICE_REMOVED, EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, + EVENT_CORE_CONFIG_UPDATE, ) from homeassistant.components.persistent_notification import ( EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, @@ -22,6 +23,7 @@ # Except for state_changed, which is handled accordingly. SUBSCRIBE_WHITELIST = { EVENT_COMPONENT_LOADED, + EVENT_CORE_CONFIG_UPDATE, EVENT_PANELS_UPDATED, EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, EVENT_SERVICE_REGISTERED, From 93e4cd6bb22ab8295d32b53651bb2f21aa76b2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 4 Sep 2019 09:13:17 +0300 Subject: [PATCH 0161/3953] Met, check for existing location (#26400) --- .../components/met/.translations/en.json | 2 +- homeassistant/components/met/config_flow.py | 18 +++++++++++++----- homeassistant/components/met/strings.json | 2 +- tests/components/met/test_config_flow.py | 4 +++- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/met/.translations/en.json b/homeassistant/components/met/.translations/en.json index 21ae7cb78fa4f9..93d028b06261ba 100644 --- a/homeassistant/components/met/.translations/en.json +++ b/homeassistant/components/met/.translations/en.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Name already exists" + "name_exists": "Location already exists" }, "step": { "user": { diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 795ba57d9887b2..c7ff4973c7d2aa 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -12,9 +12,15 @@ @callback def configured_instances(hass): """Return a set of configured SimpliSafe instances.""" - return set( - entry.data[CONF_NAME] for entry in hass.config_entries.async_entries(DOMAIN) - ) + entites = [] + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data.get("track_home"): + entites.append("home") + continue + entites.append( + f"{entry.data.get(CONF_LATITUDE)}-{entry.data.get(CONF_LONGITUDE)}" + ) + return set(entites) class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -32,11 +38,13 @@ async def async_step_user(self, user_input=None): self._errors = {} if user_input is not None: - if user_input[CONF_NAME] not in configured_instances(self.hass): + if ( + f"{user_input.get(CONF_LATITUDE)}-{user_input.get(CONF_LONGITUDE)}" + not in configured_instances(self.hass) + ): return self.async_create_entry( title=user_input[CONF_NAME], data=user_input ) - self._errors[CONF_NAME] = "name_exists" return await self._show_config_form( diff --git a/homeassistant/components/met/strings.json b/homeassistant/components/met/strings.json index f5c49bac3c4420..0c52e624418a29 100644 --- a/homeassistant/components/met/strings.json +++ b/homeassistant/components/met/strings.json @@ -14,7 +14,7 @@ } }, "error": { - "name_exists": "Name already exists" + "name_exists": "Location already exists" } } } diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 22061386b93c56..32f3be676e0514 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -102,7 +102,7 @@ async def test_flow_entry_created_from_user_input(): async def test_flow_entry_config_entry_already_exists(): """Test that create data from user input and config_entry already exists. - Test when the form should show when user puts existing name + Test when the form should show when user puts existing location in the config gui. Then the form should show with error """ hass = Mock() @@ -112,6 +112,8 @@ async def test_flow_entry_config_entry_already_exists(): first_entry = MockConfigEntry(domain="met") first_entry.data["name"] = "home" + first_entry.data[CONF_LONGITUDE] = "0" + first_entry.data[CONF_LATITUDE] = "0" first_entry.add_to_hass(hass) test_data = { From 8cf02e0b223572604e07d6e79e05ddac90aee7ab Mon Sep 17 00:00:00 2001 From: ehendrix23 Date: Tue, 3 Sep 2019 20:00:05 -0600 Subject: [PATCH 0162/3953] Update to 0.1.13 (#26402) Update to 0.1.13 --- homeassistant/components/harmony/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index b2f9e69e014627..a957db0675fb94 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -3,7 +3,7 @@ "name": "Harmony", "documentation": "https://www.home-assistant.io/components/harmony", "requirements": [ - "aioharmony==0.1.11" + "aioharmony==0.1.13" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 42272a0ccbb186..bb6035b01e1e8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -142,7 +142,7 @@ aiofreepybox==0.0.8 aioftp==0.12.0 # homeassistant.components.harmony -aioharmony==0.1.11 +aioharmony==0.1.13 # homeassistant.components.emulated_hue # homeassistant.components.http From 7bccbcbcc3737cf8076a91d6f68017bb4bfef150 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Sep 2019 18:57:32 -0700 Subject: [PATCH 0163/3953] Fix state report (#26406) * Fix state report * Update test --- homeassistant/components/alexa/state_report.py | 7 +++++-- tests/components/alexa/test_state_report.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 1e22d5fc09f23b..fbf928fd23e9dd 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -97,12 +97,15 @@ async def async_send_changereport_message( _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) _LOGGER.debug("Received (%s): %s", response.status, response_text) - if response.status == 202 and not invalidate_access_token: + if response.status == 202: return response_json = json.loads(response_text) - if response_json["payload"]["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION": + if ( + response_json["payload"]["code"] == "INVALID_ACCESS_TOKEN_EXCEPTION" + and not invalidate_access_token + ): config.async_invalidate_access_token() return await async_send_changereport_message( hass, config, alexa_entity, invalidate_access_token=False diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index f6bb4c9cc29f45..2b3f9f34adf57d 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -5,7 +5,7 @@ async def test_report_state(hass, aioclient_mock): """Test proactive state reports.""" - aioclient_mock.post(TEST_URL, json={"data": "is irrelevant"}, status=202) + aioclient_mock.post(TEST_URL, text="", status=202) hass.states.async_set( "binary_sensor.test_contact", @@ -39,7 +39,7 @@ async def test_report_state(hass, aioclient_mock): async def test_send_add_or_update_message(hass, aioclient_mock): """Test sending an AddOrUpdateReport message.""" - aioclient_mock.post(TEST_URL, json={"data": "is irrelevant"}) + aioclient_mock.post(TEST_URL, text="") hass.states.async_set( "binary_sensor.test_contact", From 860843ada1690dcf7c968834109012f8a23899fa Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Tue, 3 Sep 2019 23:11:30 -0700 Subject: [PATCH 0164/3953] Bump ISY994's PyISY dependency to 1.1.2 (#26413) Fixed a major bug that was responsible for ISY events getting seemingly random delays up to 24 seconds --- homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 7860c080b2fe05..0dd0f1eae80a0b 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,7 +3,7 @@ "name": "Isy994", "documentation": "https://www.home-assistant.io/components/isy994", "requirements": [ - "PyISY==1.1.1" + "PyISY==1.1.2" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index bb6035b01e1e8d..3660514e355b66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -50,7 +50,7 @@ PyEssent==0.13 PyGithub==1.43.5 # homeassistant.components.isy994 -PyISY==1.1.1 +PyISY==1.1.2 # homeassistant.components.mvglive PyMVGLive==1.1.4 From b8f9319cb0cbeee2c4058e5339f1a6a32da2b45c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Sep 2019 23:20:25 -0700 Subject: [PATCH 0165/3953] Bumped version to 0.98.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6d195da991e905..2a20917b3be4d2 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 = "2" +PATCH_VERSION = "3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 7995bf9e6669489aa75446b2a0d0f82063f131f0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 4 Sep 2019 08:36:49 +0200 Subject: [PATCH 0166/3953] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index a183589795e176..fea7572e33aa8f 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -41,7 +41,7 @@ jobs: displayName: 'Upload Translation' - job: 'Download' - condition: eq(variables['Build.Reason'], 'Schedule') + condition: or(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual')) pool: vmImage: 'ubuntu-latest' steps: @@ -58,5 +58,5 @@ jobs: displayName: 'Download Translation' - script: | git commit -am "[CI] Translation update" - git commit push + git push displayName: 'Update translation' From 90aaa3620623d5b6e8d2b1af80f721f90c4a3d35 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 4 Sep 2019 08:49:34 +0200 Subject: [PATCH 0167/3953] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index fea7572e33aa8f..35b1ea672ea500 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -41,6 +41,8 @@ jobs: displayName: 'Upload Translation' - job: 'Download' + dependsOn: + - 'Upload' condition: or(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual')) pool: vmImage: 'ubuntu-latest' @@ -57,6 +59,6 @@ jobs: ./script/translations_download displayName: 'Download Translation' - script: | - git commit -am "[CI] Translation update" - git push + git commit -am "[ci skip] Translation update" + git push origin displayName: 'Update translation' From 6ae2aacdb25e630d90c17bec7877f7c9e7633c07 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 4 Sep 2019 08:57:09 +0200 Subject: [PATCH 0168/3953] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index 35b1ea672ea500..12a3a43c49cd9a 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -60,5 +60,5 @@ jobs: displayName: 'Download Translation' - script: | git commit -am "[ci skip] Translation update" - git push origin + git push origin HEAD:dev displayName: 'Update translation' From 9e9859a959c33bb56096d7bf4cada70df1f2a234 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 4 Sep 2019 09:02:35 +0200 Subject: [PATCH 0169/3953] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index 12a3a43c49cd9a..b1597bbeb8ed7c 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -59,6 +59,7 @@ jobs: ./script/translations_download displayName: 'Download Translation' - script: | + git checkout dev git commit -am "[ci skip] Translation update" - git push origin HEAD:dev + git push displayName: 'Update translation' From 3b1a4a52e9f87dc7a1daf47463c8c1fcab4d1474 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 4 Sep 2019 07:41:09 +0000 Subject: [PATCH 0170/3953] [ci skip] Translation update --- .../components/deconz/.translations/da.json | 7 +++++++ .../components/deconz/.translations/de.json | 11 ++++++++++ .../components/deconz/.translations/en.json | 7 +++++++ .../components/deconz/.translations/es.json | 16 +++++++++++++++ .../components/deconz/.translations/sl.json | 11 ++++++++++ .../components/life360/.translations/de.json | 1 + .../components/life360/.translations/no.json | 1 + .../components/life360/.translations/sl.json | 1 + .../smartthings/.translations/es.json | 2 +- .../components/unifi/.translations/de.json | 12 +++++++++++ .../components/unifi/.translations/es.json | 11 ++++++++++ .../components/unifi/.translations/no.json | 12 +++++++++++ .../components/unifi/.translations/sl.json | 20 +++++++++++++++++++ .../components/zwave/.translations/pl.json | 2 +- 14 files changed, 112 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index 3c8c0377880d75..1b595924106cfd 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -49,6 +49,13 @@ "allow_deconz_groups": "Tillad deCONZ lys grupper" }, "description": "Konfigurer synligheden af deCONZ-enhedstyper" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Tillad deCONZ CLIP sensorer", + "allow_deconz_groups": "Tillad deCONZ lys grupper" + }, + "description": "Konfigurer synligheden af deCONZ-enhedstyper" } } } diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index b7cba820daaccc..5902d2a3bf3619 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -40,5 +40,16 @@ } }, "title": "deCONZ Zigbee Gateway" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen", + "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen" + }, + "description": "Konfigurieren der Sichtbarkeit von deCONZ-Ger\u00e4tetypen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 3c6656d6ae696b..272a6f5d1be2ed 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -43,6 +43,13 @@ }, "options": { "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Allow deCONZ CLIP sensors", + "allow_deconz_groups": "Allow deCONZ light groups" + }, + "description": "Configure visibility of deCONZ device types" + }, "deconz_devices": { "data": { "allow_clip_sensor": "Allow deCONZ CLIP sensors", diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index ca38deb28fe5ba..8bcf03914cee84 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -38,5 +38,21 @@ } }, "title": "Pasarela Zigbee deCONZ" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + } + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + }, + "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 58ecde32a8484d..4a30e9d34d113a 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -40,5 +40,16 @@ } }, "title": "deCONZ Zigbee prehod" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Dovoli deCONZ CLIP senzorje", + "allow_deconz_groups": "Dovolite deCONZ skupine lu\u010di" + }, + "description": "Konfiguracija vidnosti tipov naprav deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/de.json b/homeassistant/components/life360/.translations/de.json index 27dfbaed2bcdae..08a55d26caec42 100644 --- a/homeassistant/components/life360/.translations/de.json +++ b/homeassistant/components/life360/.translations/de.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen", "invalid_username": "Ung\u00fcltiger Benutzername", + "unexpected": "Unerwarteter Fehler bei der Kommunikation mit dem Life360-Server", "user_already_configured": "Konto wurde bereits konfiguriert" }, "step": { diff --git a/homeassistant/components/life360/.translations/no.json b/homeassistant/components/life360/.translations/no.json index 1a1e98c526e275..032dd606cbd855 100644 --- a/homeassistant/components/life360/.translations/no.json +++ b/homeassistant/components/life360/.translations/no.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ugyldig legitimasjon", "invalid_username": "Ugyldig brukernavn", + "unexpected": "Uventet feil under kommunikasjon med Life360-servern", "user_already_configured": "Kontoen er allerede konfigurert" }, "step": { diff --git a/homeassistant/components/life360/.translations/sl.json b/homeassistant/components/life360/.translations/sl.json index 36e4917256bc91..2bb3bb4833e817 100644 --- a/homeassistant/components/life360/.translations/sl.json +++ b/homeassistant/components/life360/.translations/sl.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Napa\u010dno geslo", "invalid_username": "Napa\u010dno uporabni\u0161ko ime", + "unexpected": "Nepri\u010dakovana napaka pri komunikaciji s stre\u017enikom Life360", "user_already_configured": "Ra\u010dun \u017ee nastavljen" }, "step": { diff --git a/homeassistant/components/smartthings/.translations/es.json b/homeassistant/components/smartthings/.translations/es.json index 9ae98bcb9f1b33..513b8ba3ffe1a2 100644 --- a/homeassistant/components/smartthings/.translations/es.json +++ b/homeassistant/components/smartthings/.translations/es.json @@ -5,7 +5,7 @@ "app_setup_error": "No se pudo configurar el SmartApp. Por favor, int\u00e9ntelo de nuevo.", "base_url_not_https": "La 'base_url' del componente 'http' debe empezar por 'https://'.", "token_already_setup": "El token ya ha sido configurado.", - "token_forbidden": "El token no tiene los alcances necesarios de OAuth.", + "token_forbidden": "El token no tiene los \u00e1mbitos de OAuth necesarios.", "token_invalid_format": "El token debe estar en formato UID/GUID", "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", "webhook_error": "SmartThings no ha podido validar el endpoint configurado en 'base_url'. Por favor, revisa los requisitos del componente." diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index 2b71d01417bd6f..0c44871f583c97 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -22,5 +22,17 @@ } }, "title": "UniFi-Controller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Zeit in Sekunden vom letzten Gesehenen bis zur Entfernung", + "track_clients": "Nachverfolgen von Netzwerkclients", + "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)", + "track_wired_clients": "Einbinden von kabelgebundenen Netzwerk-Clients" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 4f570fe1386146..8b0eb56203700d 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -22,5 +22,16 @@ } }, "title": "Controlador UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado", + "track_clients": "Seguimiento de los clientes de red", + "track_wired_clients": "Incluir clientes de red cableada" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index 541b0f60d175c9..068f4341544900 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -22,5 +22,17 @@ } }, "title": "UniFi kontroller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tid i sekunder fra sist sett til den ble ansett borte", + "track_clients": "Spor nettverksklienter", + "track_devices": "Spore nettverksenheter (Ubiquiti-enheter)", + "track_wired_clients": "Inkluder kablede nettverksklienter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/sl.json b/homeassistant/components/unifi/.translations/sl.json index 7543542abbfe37..35000bf4e1f28d 100644 --- a/homeassistant/components/unifi/.translations/sl.json +++ b/homeassistant/components/unifi/.translations/sl.json @@ -22,5 +22,25 @@ } }, "title": "UniFi Krmilnik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\u010cas v sekundah od zadnjega videnja na omre\u017eju do odsotnosti", + "track_clients": "Sledite odjemalcem omre\u017eja", + "track_devices": "Sledite omre\u017enim napravam (naprave Ubiquiti)", + "track_wired_clients": "Vklju\u010dite kliente iz o\u017ei\u010denega omre\u017eja" + } + }, + "init": { + "data": { + "few": "NEKAJ", + "one": "ENA", + "other": "OSTALO", + "two": "DVA" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/pl.json b/homeassistant/components/zwave/.translations/pl.json index c392f0093a0d35..254008ddb4c642 100644 --- a/homeassistant/components/zwave/.translations/pl.json +++ b/homeassistant/components/zwave/.translations/pl.json @@ -13,7 +13,7 @@ "network_key": "Klucz sieciowy (pozostaw pusty, by generowa\u0107 automatycznie)", "usb_path": "\u015acie\u017cka do kontrolera Z-Wave USB" }, - "description": "Zobacz https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych", + "description": "Przejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych", "title": "Konfiguracja Z-Wave" } }, From c19155109191ac93e26ad28378be598eaaef6ff5 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 4 Sep 2019 09:56:25 +0200 Subject: [PATCH 0171/3953] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index b1597bbeb8ed7c..83ed75da256336 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -7,7 +7,7 @@ trigger: - dev pr: none schedules: - - cron: "0 1 * * *" + - cron: "30 0 * * *" displayName: "translation update" branches: include: From d1bc0c1dd90fe1b25bcc719baeee46a1096aea9f Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 4 Sep 2019 19:33:29 +1000 Subject: [PATCH 0172/3953] NSW Rural Fire Service icon for geolocation entities (#26416) * define icon * add myself as codeowner --- CODEOWNERS | 1 + .../components/nsw_rural_fire_service_feed/geo_location.py | 7 +++++++ .../components/nsw_rural_fire_service_feed/manifest.json | 4 +++- .../nsw_rural_fire_service_feed/test_geo_location.py | 4 ++++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 32c2f5c2930eb4..7f60243097e239 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -191,6 +191,7 @@ homeassistant/components/no_ip/* @fabaff homeassistant/components/notify/* @home-assistant/core homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 +homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuki/* @pschmitt homeassistant/components/nws/* @MatthewFlamm homeassistant/components/ohmconnect/* @robbiet480 diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index 16ef2c10bbd567..9a9679f957511f 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -210,6 +210,13 @@ def _update_from_feed(self, feed_entry): self._size = feed_entry.size self._responsible_agency = feed_entry.responsible_agency + @property + def icon(self): + """Return the icon to use in the frontend.""" + if self._fire: + return "mdi:fire" + return "mdi:alarm-light" + @property def source(self) -> str: """Return source value of this external event.""" diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index b2bc6aaab249bc..4542eb45c8222e 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -6,5 +6,7 @@ "geojson_client==0.4" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@exxamalte" + ] } diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index 8913c0ffa48733..f5f88087010a66 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -27,6 +27,7 @@ CONF_LONGITUDE, CONF_RADIUS, EVENT_HOMEASSISTANT_START, + ATTR_ICON, ) from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_fire_time_changed @@ -150,6 +151,7 @@ async def test_setup(hass): ATTR_RESPONSIBLE_AGENCY: "Agency 1", ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "nsw_rural_fire_service_feed", + ATTR_ICON: "mdi:fire", } assert round(abs(float(state.state) - 15.5), 7) == 0 @@ -164,6 +166,7 @@ async def test_setup(hass): ATTR_FIRE: False, ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "nsw_rural_fire_service_feed", + ATTR_ICON: "mdi:alarm-light", } assert round(abs(float(state.state) - 20.5), 7) == 0 @@ -178,6 +181,7 @@ async def test_setup(hass): ATTR_FIRE: True, ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "nsw_rural_fire_service_feed", + ATTR_ICON: "mdi:fire", } assert round(abs(float(state.state) - 25.5), 7) == 0 From 6bef5a98feded30e27bf780fd9df6cb2e0cbf82d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 4 Sep 2019 11:47:13 +0200 Subject: [PATCH 0173/3953] Add prettier to vscode (#26417) --- .devcontainer/devcontainer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e747e8fdb9858e..a025a52e849c1c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,7 +9,8 @@ "extensions": [ "ms-python.python", "ms-azure-devops.azure-pipelines", - "redhat.vscode-yaml" + "redhat.vscode-yaml", + "esbenp.prettier-vscode" ], "settings": { "python.pythonPath": "/usr/local/bin/python", From 2c65e024910a413f63363af93934da28c83a731b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Sep 2019 15:51:15 +0200 Subject: [PATCH 0174/3953] Updated frontend to 20190904.0 (#26421) * Updated frontend to 20190904.0 * Updated frontend to 20190904.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 2b17091ba5cb94..3659b40b7b01b7 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==20190901.0" + "home-assistant-frontend==20190904.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2fa5a1cd41ae4b..6ec2ed358d5797 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==20190901.0 +home-assistant-frontend==20190904.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 852788e1be305f..41d474808bef3b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -631,7 +631,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190901.0 +home-assistant-frontend==20190904.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a23bc7ce610ee8..5e0e0d2a3ea9d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -179,7 +179,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190901.0 +home-assistant-frontend==20190904.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From c4c21d3e99fa1a84ca3d9a5be1e02a06bcd2e2c7 Mon Sep 17 00:00:00 2001 From: zewelor Date: Wed, 4 Sep 2019 16:15:40 +0200 Subject: [PATCH 0175/3953] Add device to mqtt camera (#26238) * Add device to mqtt camera * Support discovery device info update and add tests --- homeassistant/components/mqtt/camera.py | 29 +++++++--- tests/components/mqtt/test_camera.py | 77 +++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 1df635bbde4ab3..f3ae36c5746855 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -7,13 +7,19 @@ from homeassistant.components import camera, mqtt from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_DEVICE from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ATTR_DISCOVERY_HASH, CONF_UNIQUE_ID, MqttDiscoveryUpdate, subscription +from . import ( + ATTR_DISCOVERY_HASH, + CONF_UNIQUE_ID, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -26,6 +32,7 @@ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, } ) @@ -45,7 +52,9 @@ async def async_discover(discovery_payload): try: discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, discovery_hash) + await _async_setup_entity( + config, async_add_entities, config_entry, discovery_hash + ) except Exception: if discovery_hash: clear_discovery_hash(hass, discovery_hash) @@ -56,15 +65,17 @@ async def async_discover(discovery_payload): ) -async def _async_setup_entity(config, async_add_entities, discovery_hash=None): +async def _async_setup_entity( + config, async_add_entities, config_entry=None, discovery_hash=None +): """Set up the MQTT Camera.""" - async_add_entities([MqttCamera(config, discovery_hash)]) + async_add_entities([MqttCamera(config, config_entry, discovery_hash)]) -class MqttCamera(MqttDiscoveryUpdate, Camera): +class MqttCamera(MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera): """representation of a MQTT camera.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize the MQTT Camera.""" self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) @@ -73,8 +84,11 @@ def __init__(self, config, discovery_hash): self._qos = 0 self._last_image = None + device_config = config.get(CONF_DEVICE) + Camera.__init__(self) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe MQTT events.""" @@ -85,6 +99,7 @@ async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) self._config = config + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_write_ha_state() diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index ecc54e0e209b0c..70b5e941fe3868 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,5 +1,6 @@ """The tests for mqtt camera component.""" from unittest.mock import ANY +import json from homeassistant.components import camera, mqtt from homeassistant.components.mqtt.discovery import async_start @@ -167,3 +168,79 @@ async def test_entity_id_update(hass, mqtt_mock): assert state is not None assert mock_mqtt.async_subscribe.call_count == 1 mock_mqtt.async_subscribe.assert_any_call("test-topic", ANY, 0, None) + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT camera device registry integration.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps( + { + "platform": "mqtt", + "name": "Test 1", + "topic": "test-topic", + "device": { + "identifiers": ["helloworld"], + "connections": [["mac", "02:5b:26:a8:dc:12"]], + "manufacturer": "Whatever", + "name": "Beer", + "model": "Glass", + "sw_version": "0.1-beta", + }, + "unique_id": "veryunique", + } + ) + async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + assert device.identifiers == {("mqtt", "helloworld")} + assert device.connections == {("mac", "02:5b:26:a8:dc:12")} + assert device.manufacturer == "Whatever" + assert device.name == "Beer" + assert device.model == "Glass" + assert device.sw_version == "0.1-beta" + + +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + "platform": "mqtt", + "name": "Test 1", + "topic": "test-topic", + "device": { + "identifiers": ["helloworld"], + "connections": [["mac", "02:5b:26:a8:dc:12"]], + "manufacturer": "Whatever", + "name": "Beer", + "model": "Glass", + "sw_version": "0.1-beta", + }, + "unique_id": "veryunique", + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + assert device.name == "Beer" + + config["device"]["name"] = "Milk" + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + assert device.name == "Milk" From 79045f2da1694bb75a72385de0746426bd56eae9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Sep 2019 09:23:56 -0700 Subject: [PATCH 0176/3953] Undo accidental Tuya change --- homeassistant/components/tuya/switch.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index a0d262aa08517e..9c021766637507 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -26,12 +26,11 @@ def __init__(self, tuya): """Init Tuya switch device.""" super().__init__(tuya) self.entity_id = ENTITY_ID_FORMAT.format(tuya.object_id()) - self._is_on = False @property def is_on(self): """Return true if switch is on.""" - return self._is_on + return self.tuya.state() def turn_on(self, **kwargs): """Turn the switch on.""" @@ -40,7 +39,3 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn the device off.""" self.tuya.turn_off() - - def update(self): - """Update switch device.""" - self._is_on = self.tuya.state() From 4004879ae0b673e77f25e5df99aed069a5d4c1f7 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Thu, 5 Sep 2019 00:49:22 +0800 Subject: [PATCH 0177/3953] Entity registry doesn't overwrite with None (#24275) --- homeassistant/helpers/entity_registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 3be00c859a7a41..4b97aff19a8983 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -154,8 +154,8 @@ def async_get_or_create( if entity_id: return self._async_update_entity( entity_id, - config_entry_id=config_entry_id, - device_id=device_id, + config_entry_id=config_entry_id or _UNDEF, + device_id=device_id or _UNDEF, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be From 0df1b4c7a18d131df758daec3676a36690ffb27f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Sep 2019 19:09:24 +0200 Subject: [PATCH 0178/3953] Replaces IOError by OSError (#26428) --- homeassistant/__main__.py | 6 +++--- homeassistant/components/bme680/sensor.py | 2 +- homeassistant/components/bmw_connected_drive/__init__.py | 2 +- homeassistant/components/egardia/__init__.py | 4 ++-- homeassistant/components/hangouts/hangouts_bot.py | 2 +- homeassistant/components/keyboard_remote/__init__.py | 2 +- homeassistant/components/liveboxplaytv/media_player.py | 2 +- homeassistant/components/miflora/sensor.py | 2 +- homeassistant/components/mitemp_bt/sensor.py | 2 +- homeassistant/components/pilight/__init__.py | 2 +- homeassistant/components/proxy/camera.py | 2 +- homeassistant/components/remote_rpi_gpio/__init__.py | 2 +- homeassistant/components/remote_rpi_gpio/binary_sensor.py | 2 +- homeassistant/components/remote_rpi_gpio/switch.py | 2 +- homeassistant/components/sky_hub/device_tracker.py | 2 +- homeassistant/components/supla/__init__.py | 2 +- homeassistant/components/telnet/switch.py | 2 +- homeassistant/components/temper/sensor.py | 2 +- homeassistant/config.py | 4 ++-- homeassistant/scripts/macos/__init__.py | 2 +- 20 files changed, 24 insertions(+), 24 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 9fe501078c2a45..f7e24d69884975 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -216,7 +216,7 @@ def check_pid(pid_file: str) -> None: try: with open(pid_file, "r") as file: pid = int(file.readline()) - except IOError: + except OSError: # PID File does not exist return @@ -239,7 +239,7 @@ def write_pid(pid_file: str) -> None: try: with open(pid_file, "w") as file: file.write(str(pid)) - except IOError: + except OSError: print(f"Fatal Error: Unable to write pid file {pid_file}") sys.exit(1) @@ -258,7 +258,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None: val = fcntl(_fd, F_GETFD) if not val & FD_CLOEXEC: fcntl(_fd, F_SETFD, val | FD_CLOEXEC) - except IOError: + except OSError: pass diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 20fdfc9ee79f6f..a36b35ea9d4be2 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -171,7 +171,7 @@ def _setup_bme680(config): sensor.select_gas_heater_profile(0) else: sensor.set_gas_status(bme680.DISABLE_GAS_MEAS) - except (RuntimeError, IOError): + except (RuntimeError, OSError): _LOGGER.error("BME680 sensor not detected at 0x%02x", i2c_address) return None diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index c257470bb2d0e8..160c8a5e4551c9 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -142,7 +142,7 @@ def update(self, *_): self.account.update_vehicle_states() for listener in self._update_listeners: listener() - except IOError as exception: + except OSError as exception: _LOGGER.error( "Could not connect to the BMW Connected Drive portal. " "The vehicle state could not be updated." diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py index e17ea8f065d10a..9e11f522dd53d0 100644 --- a/homeassistant/components/egardia/__init__.py +++ b/homeassistant/components/egardia/__init__.py @@ -110,7 +110,7 @@ def setup(hass, config): server = egardiaserver.EgardiaServer("", rs_port) bound = server.bind() if not bound: - raise IOError( + raise OSError( "Binding error occurred while " + "starting EgardiaServer." ) hass.data[EGARDIA_SERVER] = server @@ -123,7 +123,7 @@ def handle_stop_event(event): # listen to home assistant stop event hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event) - except IOError: + except OSError: _LOGGER.error("Binding error occurred while starting EgardiaServer") return False diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 35f866b3d813aa..9fc3e2fa58e8fb 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -293,7 +293,7 @@ async def _async_send_message(self, message, targets, data): if self.hass.config.is_allowed_path(uri): try: image_file = open(uri, "rb") - except IOError as error: + except OSError as error: _LOGGER.error( "Image file I/O error(%s): %s", error.errno, error.strerror ) diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index 1a3b41f74bd993..8b901dcc61e930 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -157,7 +157,7 @@ def run(self): try: event = self.dev.read_one() - except IOError: # Keyboard Disconnected + except OSError: # Keyboard Disconnected self.dev = None self.hass.bus.fire( KEYBOARD_REMOTE_DISCONNECTED, diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py index 98418d6be81895..c466d71c4c5fd3 100644 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ b/homeassistant/components/liveboxplaytv/media_player.py @@ -70,7 +70,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: device = LiveboxPlayTvDevice(host, port, name) livebox_devices.append(device) - except IOError: + except OSError: _LOGGER.error( "Failed to connect to Livebox Play TV at %s:%s. " "Please check your configuration", diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 86f1462e2cca55..28020a801750f3 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -157,7 +157,7 @@ def update(self): try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) - except IOError as ioerr: + except OSError as ioerr: _LOGGER.info("Polling error %s", ioerr) return except BluetoothBackendException as bterror: diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index 9cd1f1cebc29be..adeba48dbc8517 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -157,7 +157,7 @@ def update(self): try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) - except IOError as ioerr: + except OSError as ioerr: _LOGGER.warning("Polling error %s", ioerr) return except BluetoothBackendException as bterror: diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 5d4f5dd25b52f8..2688b15e837c1d 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -92,7 +92,7 @@ def send_code(call): try: pilight_client.send_code(message_data) - except IOError: + except OSError: _LOGGER.error("Pilight send failed for %s", str(message_data)) hass.services.register(DOMAIN, SERVICE_NAME, send_code, schema=RF_CODE_SCHEMA) diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 7d145315748417..53a4f620dcc3d4 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -66,7 +66,7 @@ def _precheck_image(image, opts): raise ValueError() try: img = Image.open(io.BytesIO(image)) - except IOError: + except OSError: _LOGGER.warning("Failed to open image") raise ValueError() imgfmt = str(img.format) diff --git a/homeassistant/components/remote_rpi_gpio/__init__.py b/homeassistant/components/remote_rpi_gpio/__init__.py index ccefd00c723ac3..33356d0e3b82cc 100644 --- a/homeassistant/components/remote_rpi_gpio/__init__.py +++ b/homeassistant/components/remote_rpi_gpio/__init__.py @@ -47,7 +47,7 @@ def setup_input(address, port, pull_mode, bouncetime): bounce_time=bouncetime, pin_factory=PiGPIOFactory(address), ) - except (ValueError, IndexError, KeyError, IOError): + except (ValueError, IndexError, KeyError, OSError): return None diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index 8c7d7b7d023b1d..e12d83324fd756 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): button = remote_rpi_gpio.setup_input( address, port_num, pull_mode, bouncetime ) - except (ValueError, IndexError, KeyError, IOError): + except (ValueError, IndexError, KeyError, OSError): return new_sensor = RemoteRPiGPIOBinarySensor(port_name, button, invert_logic) devices.append(new_sensor) diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index aa20a2909d2ffd..8240de7951d710 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for port, name in ports.items(): try: led = remote_rpi_gpio.setup_output(address, port, invert_logic) - except (ValueError, IndexError, KeyError, IOError): + except (ValueError, IndexError, KeyError, OSError): return new_switch = RemoteRPiGPIOSwitch(name, led, invert_logic) devices.append(new_switch) diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index c8969add244a19..109c410c16d0b8 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -94,7 +94,7 @@ def _parse_skyhub_response(data_str): """Parse the Sky Hub data format.""" pattmatch = re.search("attach_dev = '(.*)'", data_str) if pattmatch is None: - raise IOError( + raise OSError( "Error: Impossible to fetch data from" + " Sky Hub. Try to reboot the router." ) diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 9eef9d989cb093..86e763142e6191 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -65,7 +65,7 @@ def setup(hass, base_config): srv_info, ) return False - except IOError: + except OSError: _LOGGER.exception( "Server: %s not configured. Error on Supla API access: ", server_address ) diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index a4777af54578bb..87fb70bb8886a6 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -117,7 +117,7 @@ def _telnet_command(self, command): response = telnet.read_until(b"\r", timeout=self._timeout) _LOGGER.debug("telnet response: %s", response.decode("ASCII").strip()) return response.decode("ASCII").strip() - except IOError as error: + except OSError as error: _LOGGER.error( 'Command "%s" failed with exception: %s', command, repr(error) ) diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index c5e5c4af978773..a32de3da10fb62 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -96,7 +96,7 @@ def update(self): ) sensor_value = self.temper_device.get_temperature(format_str) self.current_value = round(sensor_value, 1) - except IOError: + except OSError: _LOGGER.error( "Failed to get temperature. The device address may" "have changed. Attempting to reset device" diff --git a/homeassistant/config.py b/homeassistant/config.py index 4b7efed00e4082..d3bd97dad8f777 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -289,7 +289,7 @@ def _write_default_config(config_dir: str) -> Optional[str]: return config_path - except IOError: + except OSError: print("Unable to create default configuration file", config_path) return None @@ -393,7 +393,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: try: with open(config_path, "wt", encoding="utf-8") as config_file: config_file.write(config_raw) - except IOError: + except OSError: _LOGGER.exception("Migrating to google_translate tts failed") pass diff --git a/homeassistant/scripts/macos/__init__.py b/homeassistant/scripts/macos/__init__.py index e8d8306c8ce038..ceb3609dbdb1b7 100644 --- a/homeassistant/scripts/macos/__init__.py +++ b/homeassistant/scripts/macos/__init__.py @@ -27,7 +27,7 @@ def install_osx(): try: with open(path, "w", encoding="utf-8") as outp: outp.write(plist) - except IOError as err: + except OSError as err: print("Unable to write to " + path, err) return From 6acfede512dcb2c0f41b5d591a3a2794e4299192 Mon Sep 17 00:00:00 2001 From: Pierre <3458055+BaQs@users.noreply.github.com> Date: Wed, 4 Sep 2019 20:20:20 +0200 Subject: [PATCH 0179/3953] Add atome sensor platform (#26197) * Atome sensor platform - provides live data from Linky energy meters with Atome device from Total/Direct energie * Proper requirements, added code ownership * Do not cover atome component * Proper PEP8 import, proper use of const, added missing docstring etc * Proper PEP8 import, proper use of const, added missing docstring etc * Integrate recommendations from MartinHjelmare * Init shall remain as clean as possible, we don't want side effect * Add daily,weekly,monthly,yearly sensors. Now depends on pyatome 0.1 * Requirements regenerated for atome component * Refactored the way we update sensors * Removed some un-necessary returns and unused variable --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/atome/__init__.py | 1 + homeassistant/components/atome/manifest.json | 8 + homeassistant/components/atome/sensor.py | 279 +++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 293 insertions(+) create mode 100644 homeassistant/components/atome/__init__.py create mode 100644 homeassistant/components/atome/manifest.json create mode 100644 homeassistant/components/atome/sensor.py diff --git a/.coveragerc b/.coveragerc index 001729ace5bc6c..e9b58c87baadef 100644 --- a/.coveragerc +++ b/.coveragerc @@ -50,6 +50,7 @@ omit = homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* homeassistant/components/asuswrt/device_tracker.py + homeassistant/components/atome/* homeassistant/components/august/* homeassistant/components/aurora_abb_powerone/sensor.py homeassistant/components/automatic/device_tracker.py diff --git a/CODEOWNERS b/CODEOWNERS index 7f60243097e239..0408bcc80320b6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,6 +28,7 @@ homeassistant/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff homeassistant/components/arest/* @fabaff homeassistant/components/asuswrt/* @kennedyshead +homeassistant/components/atome/* @baqs homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core homeassistant/components/automatic/* @armills diff --git a/homeassistant/components/atome/__init__.py b/homeassistant/components/atome/__init__.py new file mode 100644 index 00000000000000..6f524606a817bf --- /dev/null +++ b/homeassistant/components/atome/__init__.py @@ -0,0 +1 @@ +"""Support for Atome devices connected to a Linky Energy Meter.""" diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json new file mode 100644 index 00000000000000..621faba4fc0a33 --- /dev/null +++ b/homeassistant/components/atome/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "atome", + "name": "Atome", + "documentation": "https://www.home-assistant.io/components/atome", + "dependencies": [], + "codeowners": ["@baqs"], + "requirements": ["pyatome==0.1.1"] +} diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py new file mode 100644 index 00000000000000..c98b634bb2111d --- /dev/null +++ b/homeassistant/components/atome/sensor.py @@ -0,0 +1,279 @@ +"""Linky Atome.""" +import logging +from datetime import timedelta + +import voluptuous as vol +from pyatome.client import AtomeClient +from pyatome.client import PyAtomeError + +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + CONF_NAME, + DEVICE_CLASS_POWER, + POWER_WATT, + ENERGY_KILO_WATT_HOUR, +) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "atome" + +LIVE_SCAN_INTERVAL = timedelta(seconds=30) +DAILY_SCAN_INTERVAL = timedelta(seconds=150) +WEEKLY_SCAN_INTERVAL = timedelta(hours=1) +MONTHLY_SCAN_INTERVAL = timedelta(hours=1) +YEARLY_SCAN_INTERVAL = timedelta(days=1) + +LIVE_NAME = "Atome Live Power" +DAILY_NAME = "Atome Daily" +WEEKLY_NAME = "Atome Weekly" +MONTHLY_NAME = "Atome Monthly" +YEARLY_NAME = "Atome Yearly" + +LIVE_TYPE = "live" +DAILY_TYPE = "day" +WEEKLY_TYPE = "week" +MONTHLY_TYPE = "month" +YEARLY_TYPE = "year" + +ICON = "mdi:flash" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Atome sensor.""" + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + try: + atome_client = AtomeClient(username, password) + atome_client.login() + except PyAtomeError as exp: + _LOGGER.error(exp) + return + + data = AtomeData(atome_client) + + sensors = [] + sensors.append(AtomeSensor(data, LIVE_NAME, LIVE_TYPE)) + sensors.append(AtomeSensor(data, DAILY_NAME, DAILY_TYPE)) + sensors.append(AtomeSensor(data, WEEKLY_NAME, WEEKLY_TYPE)) + sensors.append(AtomeSensor(data, MONTHLY_NAME, MONTHLY_TYPE)) + sensors.append(AtomeSensor(data, YEARLY_NAME, YEARLY_TYPE)) + + add_entities(sensors, True) + + +class AtomeData: + """Stores data retrieved from Neurio sensor.""" + + def __init__(self, client: AtomeClient): + """Initialize the data.""" + self.atome_client = client + self._live_power = None + self._subscribed_power = None + self._is_connected = None + self._day_usage = None + self._day_price = None + self._week_usage = None + self._week_price = None + self._month_usage = None + self._month_price = None + self._year_usage = None + self._year_price = None + + @property + def live_power(self): + """Return latest active power value.""" + return self._live_power + + @property + def subscribed_power(self): + """Return latest active power value.""" + return self._subscribed_power + + @property + def is_connected(self): + """Return latest active power value.""" + return self._is_connected + + @Throttle(LIVE_SCAN_INTERVAL) + def update_live_usage(self): + """Return current power value.""" + try: + values = self.atome_client.get_live() + self._live_power = values["last"] + self._subscribed_power = values["subscribed"] + self._is_connected = values["isConnected"] + _LOGGER.debug( + "Updating Atome live data. Got: %d, isConnected: %s, subscribed: %d", + self._live_power, + self._is_connected, + self._subscribed_power, + ) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def day_usage(self): + """Return latest daily usage value.""" + return self._day_usage + + @property + def day_price(self): + """Return latest daily usage value.""" + return self._day_price + + @Throttle(DAILY_SCAN_INTERVAL) + def update_day_usage(self): + """Return current daily power usage.""" + try: + values = self.atome_client.get_consumption(DAILY_TYPE) + self._day_usage = values["total"] / 1000 + self._day_price = values["price"] + _LOGGER.debug("Updating Atome daily data. Got: %d.", self._day_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def week_usage(self): + """Return latest weekly usage value.""" + return self._week_usage + + @property + def week_price(self): + """Return latest weekly usage value.""" + return self._week_price + + @Throttle(WEEKLY_SCAN_INTERVAL) + def update_week_usage(self): + """Return current weekly power usage.""" + try: + values = self.atome_client.get_consumption(WEEKLY_TYPE) + self._week_usage = values["total"] / 1000 + self._week_price = values["price"] + _LOGGER.debug("Updating Atome weekly data. Got: %d.", self._week_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def month_usage(self): + """Return latest monthly usage value.""" + return self._month_usage + + @property + def month_price(self): + """Return latest monthly usage value.""" + return self._month_price + + @Throttle(MONTHLY_SCAN_INTERVAL) + def update_month_usage(self): + """Return current monthly power usage.""" + try: + values = self.atome_client.get_consumption(MONTHLY_TYPE) + self._month_usage = values["total"] / 1000 + self._month_price = values["price"] + _LOGGER.debug("Updating Atome monthly data. Got: %d.", self._month_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def year_usage(self): + """Return latest yearly usage value.""" + return self._year_usage + + @property + def year_price(self): + """Return latest yearly usage value.""" + return self._year_price + + @Throttle(YEARLY_SCAN_INTERVAL) + def update_year_usage(self): + """Return current yearly power usage.""" + try: + values = self.atome_client.get_consumption(YEARLY_TYPE) + self._year_usage = values["total"] / 1000 + self._year_price = values["price"] + _LOGGER.debug("Updating Atome yearly data. Got: %d.", self._year_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + +class AtomeSensor(Entity): + """Representation of a sensor entity for Atome.""" + + def __init__(self, data, name, sensor_type): + """Initialize the sensor.""" + self._name = name + self._data = data + self._state = None + self._attributes = {} + + self._sensor_type = sensor_type + + if sensor_type == LIVE_TYPE: + self._unit_of_measurement = POWER_WATT + else: + self._unit_of_measurement = ENERGY_KILO_WATT_HOUR + + @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 device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_POWER + + def update(self): + """Update device state.""" + update_function = getattr(self._data, f"update_{self._sensor_type}_usage") + update_function() + + if self._sensor_type == LIVE_TYPE: + self._state = self._data.live_power + self._attributes["subscribed_power"] = self._data.subscribed_power + self._attributes["is_connected"] = self._data.is_connected + else: + self._state = getattr(self._data, f"{self._sensor_type}_usage") + self._attributes["price"] = getattr( + self._data, f"{self._sensor_type}_price" + ) diff --git a/requirements_all.txt b/requirements_all.txt index 41d474808bef3b..7f0e861fc5dc45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1068,6 +1068,9 @@ pyarlo==0.2.3 # homeassistant.components.netatmo pyatmo==2.2.1 +# homeassistant.components.atome +pyatome==0.1.1 + # homeassistant.components.apple_tv pyatv==0.3.13 From 1bd22a129bc7db7babf2e9c2b85ef1af0885b7f0 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 5 Sep 2019 00:32:29 +0000 Subject: [PATCH 0180/3953] [ci skip] Translation update --- .../components/auth/.translations/ko.json | 2 +- .../components/cast/.translations/ko.json | 10 +++++----- .../components/daikin/.translations/ko.json | 6 +++--- .../components/deconz/.translations/ca.json | 9 ++++++++- .../components/deconz/.translations/en.json | 2 +- .../components/deconz/.translations/ko.json | 18 ++++++++++++++++++ .../components/deconz/.translations/sl.json | 7 +++++++ .../components/hangouts/.translations/ko.json | 6 +++--- .../components/life360/.translations/ko.json | 1 + .../components/met/.translations/ko.json | 2 +- .../components/met/.translations/sl.json | 2 +- .../components/point/.translations/ko.json | 2 +- .../components/ps4/.translations/ko.json | 2 +- .../components/unifi/.translations/ko.json | 12 ++++++++++++ .../components/upnp/.translations/ko.json | 4 ++-- 15 files changed, 65 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 6c2e8988d83c58..1cb70519b20f45 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [\uad6c\uae00 OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json index 32c744c8f20b0e..71dee3afec5ec1 100644 --- a/homeassistant/components/cast/.translations/ko.json +++ b/homeassistant/components/cast/.translations/ko.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "no_devices_found": "Googgle Cast \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 Google Cast \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "\uad6c\uae00 \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 \uad6c\uae00 \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "step": { "confirm": { - "description": "Google Cast\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Google Cast" + "description": "\uad6c\uae00 \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8" } }, - "title": "Google Cast" + "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8" } } \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/ko.json b/homeassistant/components/daikin/.translations/ko.json index 2291d46800d849..4b1d1bd86e5b8a 100644 --- a/homeassistant/components/daikin/.translations/ko.json +++ b/homeassistant/components/daikin/.translations/ko.json @@ -10,10 +10,10 @@ "data": { "host": "\ud638\uc2a4\ud2b8" }, - "description": "Daikin AC \uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "Daikin AC \uad6c\uc131" + "description": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8 \uad6c\uc131" } }, - "title": "Daikin AC" + "title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 56ae59c78ba031..263730ba5837c2 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -48,7 +48,14 @@ "allow_clip_sensor": "Permet sensors deCONZ CLIP", "allow_deconz_groups": "Permet grups de llums deCONZ" }, - "description": "Configura la visibilitat dels tipus de dispositius deCONZ" + "description": "Configura la visibilitat dels tipus dels dispositius deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Permet sensors deCONZ CLIP", + "allow_deconz_groups": "Permet grups de llums deCONZ" + }, + "description": "Configura la visibilitat dels tipus dels dispositius deCONZ" } } } diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 570de0a239cf8c..272a6f5d1be2ed 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -43,7 +43,7 @@ }, "options": { "step": { - "deconz_devices": { + "async_step_deconz_devices": { "data": { "allow_clip_sensor": "Allow deCONZ CLIP sensors", "allow_deconz_groups": "Allow deCONZ light groups" diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 4bf845d50e5fd2..0ddff8557ec14c 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -40,5 +40,23 @@ } }, "title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9", + "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9" + }, + "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9", + "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9" + }, + "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 4a30e9d34d113a..86210b2e6c1065 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -49,6 +49,13 @@ "allow_deconz_groups": "Dovolite deCONZ skupine lu\u010di" }, "description": "Konfiguracija vidnosti tipov naprav deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Dovoli deCONZ CLIP senzorje", + "allow_deconz_groups": "Dovolite deCONZ skupine lu\u010di" + }, + "description": "Konfiguracija vidnosti tipov naprav deCONZ" } } } diff --git a/homeassistant/components/hangouts/.translations/ko.json b/homeassistant/components/hangouts/.translations/ko.json index e045f3359d1542..3b1c755b3588c1 100644 --- a/homeassistant/components/hangouts/.translations/ko.json +++ b/homeassistant/components/hangouts/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Google Hangouts \uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "already_configured": "\uad6c\uae00 \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -24,9 +24,9 @@ "password": "\ube44\ubc00\ubc88\ud638" }, "description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.", - "title": "Google Hangouts \ub85c\uadf8\uc778" + "title": "\uad6c\uae00 \ud589\uc544\uc6c3 \ub85c\uadf8\uc778" } }, - "title": "Google Hangouts" + "title": "\uad6c\uae00 \ud589\uc544\uc6c3" } } \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/ko.json b/homeassistant/components/life360/.translations/ko.json index b81a6fd059f5ce..067b305b80c426 100644 --- a/homeassistant/components/life360/.translations/ko.json +++ b/homeassistant/components/life360/.translations/ko.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "\ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unexpected": "Life360 \uc11c\ubc84 \uc5f0\uacb0\uc911 \uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", "user_already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { diff --git a/homeassistant/components/met/.translations/ko.json b/homeassistant/components/met/.translations/ko.json index 6900458ba60d15..81a98b9754fe81 100644 --- a/homeassistant/components/met/.translations/ko.json +++ b/homeassistant/components/met/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" + "name_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/sl.json b/homeassistant/components/met/.translations/sl.json index 5dffbe133e7db6..71ffdaf8509583 100644 --- a/homeassistant/components/met/.translations/sl.json +++ b/homeassistant/components/met/.translations/sl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Ime \u017ee obstaja" + "name_exists": "Lokacija \u017ee obstaja" }, "step": { "user": { diff --git a/homeassistant/components/point/.translations/ko.json b/homeassistant/components/point/.translations/ko.json index d70859c8bde0d6..0dd9cd43adadc1 100644 --- a/homeassistant/components/point/.translations/ko.json +++ b/homeassistant/components/point/.translations/ko.json @@ -8,7 +8,7 @@ "no_flows": "Point \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Point \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/point/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." }, "create_entry": { - "default": "Point \uae30\uae30\ub294 Minut \ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "default": "Point \uae30\uae30\ub85c Minut \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "follow_link": "Submit \ubc84\ud2bc\uc744 \ub204\ub974\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694", diff --git a/homeassistant/components/ps4/.translations/ko.json b/homeassistant/components/ps4/.translations/ko.json index f13a66d5e8a039..25f64cd21e9db2 100644 --- a/homeassistant/components/ps4/.translations/ko.json +++ b/homeassistant/components/ps4/.translations/ko.json @@ -3,7 +3,7 @@ "abort": { "credential_error": "\uc790\uaca9 \uc99d\uba85\uc744 \uac00\uc838\uc624\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "devices_configured": "\ubc1c\uacac \ub41c \ubaa8\ub4e0 \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "no_devices_found": "PlayStation 4 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "no_devices_found": "PlayStation 4 \uae30\uae30\ub97c \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "port_987_bind_error": "\ud3ec\ud2b8 987 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "port_997_bind_error": "\ud3ec\ud2b8 997 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json index 431d6bbf5e6d73..1fff9887906b53 100644 --- a/homeassistant/components/unifi/.translations/ko.json +++ b/homeassistant/components/unifi/.translations/ko.json @@ -22,5 +22,17 @@ } }, "title": "UniFi \ucee8\ud2b8\ub864\ub7ec" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ud655\uc778\ub41c \uc2dc\uac04\ubd80\ud130 \uc678\ucd9c \uc0c1\ud0dc\ub85c \uac04\uc8fc\ub418\ub294 \uc2dc\uac04 (\ucd08)", + "track_clients": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uc801 \ub300\uc0c1", + "track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)", + "track_wired_clients": "\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ud3ec\ud568" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json index 9fa37e1236dd32..d846a5e38ce342 100644 --- a/homeassistant/components/upnp/.translations/ko.json +++ b/homeassistant/components/upnp/.translations/ko.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "UPnP/IGD \uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", - "incomplete_device": "\ubd88\uc644\uc804\ud55c UPnP \uc7a5\uce58 \ubb34\uc2dc\ud558\uae30", + "incomplete_device": "\ubd88\uc644\uc804\ud55c UPnP \uae30\uae30 \ubb34\uc2dc\ud558\uae30", "no_devices_discovered": "\ubc1c\uacac\ub41c UPnP/IGD \uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "no_devices_found": "UPnP/IGD \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "no_devices_found": "UPnP/IGD \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", "no_sensors_or_port_mapping": "\ucd5c\uc18c\ud55c \uc13c\uc11c \ud639\uc740 \ud3ec\ud2b8 \ub9e4\ud551\uc744 \ud65c\uc131\ud654 \ud574\uc57c \ud569\ub2c8\ub2e4", "single_instance_allowed": "\ud558\ub098\uc758 UPnP/IGD \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, From a85f89c5a6be453f7e00f8eb95c4107af7f5849d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 5 Sep 2019 12:09:21 +0200 Subject: [PATCH 0181/3953] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index 83ed75da256336..2fd49c056f7fd6 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -60,6 +60,7 @@ jobs: displayName: 'Download Translation' - script: | git checkout dev + git add homeassistant git commit -am "[ci skip] Translation update" git push displayName: 'Update translation' From 1cbb895d2066e5360736004b361fe222b83aeb69 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 5 Sep 2019 10:11:41 +0000 Subject: [PATCH 0182/3953] [ci skip] Translation update --- .../ambiclimate/.translations/pl.json | 2 +- .../ambient_station/.translations/pl.json | 2 +- .../arcam_fmj/.translations/de.json | 5 ++++ .../arcam_fmj/.translations/ko.json | 5 ++++ .../arcam_fmj/.translations/sl.json | 5 ++++ .../components/auth/.translations/pl.json | 2 +- .../cert_expiry/.translations/de.json | 24 ++++++++++++++++++ .../cert_expiry/.translations/es.json | 22 ++++++++++++++++ .../cert_expiry/.translations/ko.json | 24 ++++++++++++++++++ .../cert_expiry/.translations/sl.json | 24 ++++++++++++++++++ .../components/deconz/.translations/de.json | 7 ++++++ .../components/deconz/.translations/pl.json | 7 ++++++ .../components/deconz/.translations/ru.json | 7 ++++++ .../components/esphome/.translations/pl.json | 2 +- .../geonetnz_quakes/.translations/de.json | 17 +++++++++++++ .../geonetnz_quakes/.translations/es.json | 14 +++++++++++ .../geonetnz_quakes/.translations/ko.json | 17 +++++++++++++ .../geonetnz_quakes/.translations/no.json | 17 +++++++++++++ .../geonetnz_quakes/.translations/sl.json | 17 +++++++++++++ .../components/life360/.translations/pl.json | 4 +-- .../components/light/.translations/ca.json | 8 ++++++ .../components/light/.translations/da.json | 8 ++++++ .../components/light/.translations/de.json | 8 ++++++ .../components/light/.translations/es.json | 8 ++++++ .../components/light/.translations/fr.json | 8 ++++++ .../components/light/.translations/ko.json | 8 ++++++ .../components/light/.translations/no.json | 8 ++++++ .../components/light/.translations/pl.json | 8 ++++++ .../components/light/.translations/ru.json | 8 ++++++ .../components/light/.translations/sl.json | 8 ++++++ .../light/.translations/zh-Hant.json | 8 ++++++ .../components/linky/.translations/ca.json | 25 +++++++++++++++++++ .../components/linky/.translations/da.json | 25 +++++++++++++++++++ .../components/linky/.translations/de.json | 25 +++++++++++++++++++ .../components/linky/.translations/fr.json | 25 +++++++++++++++++++ .../components/linky/.translations/ko.json | 25 +++++++++++++++++++ .../components/linky/.translations/pl.json | 25 +++++++++++++++++++ .../components/linky/.translations/ru.json | 25 +++++++++++++++++++ .../components/linky/.translations/sl.json | 25 +++++++++++++++++++ .../logi_circle/.translations/pl.json | 2 +- .../components/met/.translations/ru.json | 2 +- .../components/notion/.translations/pl.json | 2 +- .../components/openuv/.translations/pl.json | 2 +- .../components/point/.translations/pl.json | 2 +- .../rainmachine/.translations/pl.json | 4 +-- .../simplisafe/.translations/pl.json | 4 +-- .../components/toon/.translations/pl.json | 4 +-- .../components/traccar/.translations/de.json | 18 +++++++++++++ .../components/traccar/.translations/es.json | 7 ++++++ .../components/traccar/.translations/ko.json | 18 +++++++++++++ .../components/traccar/.translations/no.json | 18 +++++++++++++ .../components/traccar/.translations/sl.json | 18 +++++++++++++ .../twentemilieu/.translations/de.json | 23 +++++++++++++++++ .../twentemilieu/.translations/es.json | 16 ++++++++++++ .../twentemilieu/.translations/ko.json | 23 +++++++++++++++++ .../twentemilieu/.translations/no.json | 23 +++++++++++++++++ .../twentemilieu/.translations/sl.json | 23 +++++++++++++++++ .../components/unifi/.translations/de.json | 6 +++++ .../components/velbus/.translations/de.json | 21 ++++++++++++++++ .../components/velbus/.translations/es.json | 14 +++++++++++ .../components/velbus/.translations/ko.json | 21 ++++++++++++++++ .../components/velbus/.translations/no.json | 21 ++++++++++++++++ .../components/velbus/.translations/sl.json | 21 ++++++++++++++++ .../components/vesync/.translations/de.json | 20 +++++++++++++++ .../components/vesync/.translations/es.json | 16 ++++++++++++ .../components/vesync/.translations/fr.json | 20 +++++++++++++++ .../components/vesync/.translations/ko.json | 20 +++++++++++++++ .../components/vesync/.translations/no.json | 20 +++++++++++++++ .../components/vesync/.translations/sl.json | 20 +++++++++++++++ .../components/withings/.translations/ca.json | 17 +++++++++++++ .../components/withings/.translations/da.json | 16 ++++++++++++ .../components/withings/.translations/de.json | 17 +++++++++++++ .../components/withings/.translations/es.json | 17 +++++++++++++ .../components/withings/.translations/fr.json | 17 +++++++++++++ .../components/withings/.translations/ko.json | 17 +++++++++++++ .../components/withings/.translations/no.json | 17 +++++++++++++ .../components/withings/.translations/pl.json | 17 +++++++++++++ .../components/withings/.translations/ru.json | 17 +++++++++++++ .../components/withings/.translations/sl.json | 17 +++++++++++++ .../withings/.translations/zh-Hant.json | 17 +++++++++++++ .../components/wwlln/.translations/pl.json | 2 +- 81 files changed, 1111 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/de.json create mode 100644 homeassistant/components/arcam_fmj/.translations/ko.json create mode 100644 homeassistant/components/arcam_fmj/.translations/sl.json create mode 100644 homeassistant/components/cert_expiry/.translations/de.json create mode 100644 homeassistant/components/cert_expiry/.translations/es.json create mode 100644 homeassistant/components/cert_expiry/.translations/ko.json create mode 100644 homeassistant/components/cert_expiry/.translations/sl.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/de.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/es.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/ko.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/no.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/sl.json create mode 100644 homeassistant/components/light/.translations/ca.json create mode 100644 homeassistant/components/light/.translations/da.json create mode 100644 homeassistant/components/light/.translations/de.json create mode 100644 homeassistant/components/light/.translations/es.json create mode 100644 homeassistant/components/light/.translations/fr.json create mode 100644 homeassistant/components/light/.translations/ko.json create mode 100644 homeassistant/components/light/.translations/no.json create mode 100644 homeassistant/components/light/.translations/pl.json create mode 100644 homeassistant/components/light/.translations/ru.json create mode 100644 homeassistant/components/light/.translations/sl.json create mode 100644 homeassistant/components/light/.translations/zh-Hant.json create mode 100644 homeassistant/components/linky/.translations/ca.json create mode 100644 homeassistant/components/linky/.translations/da.json create mode 100644 homeassistant/components/linky/.translations/de.json create mode 100644 homeassistant/components/linky/.translations/fr.json create mode 100644 homeassistant/components/linky/.translations/ko.json create mode 100644 homeassistant/components/linky/.translations/pl.json create mode 100644 homeassistant/components/linky/.translations/ru.json create mode 100644 homeassistant/components/linky/.translations/sl.json create mode 100644 homeassistant/components/traccar/.translations/de.json create mode 100644 homeassistant/components/traccar/.translations/es.json create mode 100644 homeassistant/components/traccar/.translations/ko.json create mode 100644 homeassistant/components/traccar/.translations/no.json create mode 100644 homeassistant/components/traccar/.translations/sl.json create mode 100644 homeassistant/components/twentemilieu/.translations/de.json create mode 100644 homeassistant/components/twentemilieu/.translations/es.json create mode 100644 homeassistant/components/twentemilieu/.translations/ko.json create mode 100644 homeassistant/components/twentemilieu/.translations/no.json create mode 100644 homeassistant/components/twentemilieu/.translations/sl.json create mode 100644 homeassistant/components/velbus/.translations/de.json create mode 100644 homeassistant/components/velbus/.translations/es.json create mode 100644 homeassistant/components/velbus/.translations/ko.json create mode 100644 homeassistant/components/velbus/.translations/no.json create mode 100644 homeassistant/components/velbus/.translations/sl.json create mode 100644 homeassistant/components/vesync/.translations/de.json create mode 100644 homeassistant/components/vesync/.translations/es.json create mode 100644 homeassistant/components/vesync/.translations/fr.json create mode 100644 homeassistant/components/vesync/.translations/ko.json create mode 100644 homeassistant/components/vesync/.translations/no.json create mode 100644 homeassistant/components/vesync/.translations/sl.json create mode 100644 homeassistant/components/withings/.translations/ca.json create mode 100644 homeassistant/components/withings/.translations/da.json create mode 100644 homeassistant/components/withings/.translations/de.json create mode 100644 homeassistant/components/withings/.translations/es.json create mode 100644 homeassistant/components/withings/.translations/fr.json create mode 100644 homeassistant/components/withings/.translations/ko.json create mode 100644 homeassistant/components/withings/.translations/no.json create mode 100644 homeassistant/components/withings/.translations/pl.json create mode 100644 homeassistant/components/withings/.translations/ru.json create mode 100644 homeassistant/components/withings/.translations/sl.json create mode 100644 homeassistant/components/withings/.translations/zh-Hant.json diff --git a/homeassistant/components/ambiclimate/.translations/pl.json b/homeassistant/components/ambiclimate/.translations/pl.json index 47e9c9f35b2897..7ba95b007c995a 100644 --- a/homeassistant/components/ambiclimate/.translations/pl.json +++ b/homeassistant/components/ambiclimate/.translations/pl.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Kliknij poni\u017cszy [link]({authorization_url}) i Zezw\u00f3l na dost\u0119p do swojego konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n(Upewnij si\u0119, \u017ce podany adres URL to {cb_url})", + "description": "Kliknij poni\u017cszy [link]({authorization_url}) i Zezw\u00f3l na dost\u0119p do konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n(Upewnij si\u0119, \u017ce podany adres URL to {cb_url})", "title": "Uwierzytelnienie Ambiclimate" } }, diff --git a/homeassistant/components/ambient_station/.translations/pl.json b/homeassistant/components/ambient_station/.translations/pl.json index 2140b4e29fe27c..6ebd0848a632fe 100644 --- a/homeassistant/components/ambient_station/.translations/pl.json +++ b/homeassistant/components/ambient_station/.translations/pl.json @@ -11,7 +11,7 @@ "api_key": "Klucz API", "app_key": "Klucz aplikacji" }, - "title": "Wprowad\u017a swoje dane" + "title": "Wprowad\u017a dane" } }, "title": "Ambient PWS" diff --git a/homeassistant/components/arcam_fmj/.translations/de.json b/homeassistant/components/arcam_fmj/.translations/de.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/de.json @@ -0,0 +1,5 @@ +{ + "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 new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/ko.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/sl.json b/homeassistant/components/arcam_fmj/.translations/sl.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/sl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/pl.json b/homeassistant/components/auth/.translations/pl.json index f0e9f7b71ea449..78610a5324fe39 100644 --- a/homeassistant/components/auth/.translations/pl.json +++ b/homeassistant/components/auth/.translations/pl.json @@ -13,7 +13,7 @@ "title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144" }, "setup": { - "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez **notify.{notify_service}**. Wpisz je poni\u017cej:", + "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez **notify.{notify_service}**. Wprowad\u017a je poni\u017cej:", "title": "Sprawd\u017a konfiguracj\u0119" } }, diff --git a/homeassistant/components/cert_expiry/.translations/de.json b/homeassistant/components/cert_expiry/.translations/de.json new file mode 100644 index 00000000000000..344abe13067c42 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert." + }, + "error": { + "certificate_fetch_failed": "Zertifikat kann von dieser Kombination aus Host und Port nicht abgerufen werden", + "connection_timeout": "Zeit\u00fcberschreitung beim Herstellen einer Verbindung mit diesem Host", + "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert.", + "resolve_failed": "Dieser Host kann nicht aufgel\u00f6st werden" + }, + "step": { + "user": { + "data": { + "host": "Der Hostname des Zertifikats", + "name": "Der Name des Zertifikats", + "port": "Der Port des Zertifikats" + }, + "title": "Definieren Sie das zu testende Zertifikat" + } + }, + "title": "Zertifikatsablauf" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/es.json b/homeassistant/components/cert_expiry/.translations/es.json new file mode 100644 index 00000000000000..2cb0bd9af166ed --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" + }, + "error": { + "certificate_fetch_failed": "No se puede obtener el certificado de esta combinaci\u00f3n de host y puerto", + "connection_timeout": "Tiempo de espera agotado al conectar con el dispositivo.", + "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "host": "El nombre de host del certificado", + "name": "El nombre del certificado", + "port": "El puerto del certificado" + } + } + }, + "title": "Caducidad del certificado" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/ko.json b/homeassistant/components/cert_expiry/.translations/ko.json new file mode 100644 index 00000000000000..a807d32a6fbaaa --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "certificate_fetch_failed": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uc5d0\uc11c \uc778\uc99d\uc11c\ub97c \uac00\uc838 \uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "connection_timeout": "\ud638\uc2a4\ud2b8 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", + "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\uc778\uc99d\uc11c\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984", + "name": "\uc778\uc99d\uc11c\uc758 \uc774\ub984", + "port": "\uc778\uc99d\uc11c\uc758 \ud3ec\ud2b8" + }, + "title": "\uc778\uc99d\uc11c \uc815\uc758 \ud14c\uc2a4\ud2b8 \ub300\uc0c1" + } + }, + "title": "\uc778\uc99d\uc11c \ub9cc\ub8cc" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/sl.json b/homeassistant/components/cert_expiry/.translations/sl.json new file mode 100644 index 00000000000000..c088e414c73dbd --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana" + }, + "error": { + "certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila", + "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem", + "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", + "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti" + }, + "step": { + "user": { + "data": { + "host": "Ime gostitelja potrdila", + "name": "Ime potrdila", + "port": "Vrata potrdila" + }, + "title": "Dolo\u010dite potrdilo za testiranje" + } + }, + "title": "Veljavnost certifikata" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 5902d2a3bf3619..97e25e28965c5f 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -49,6 +49,13 @@ "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen" }, "description": "Konfigurieren der Sichtbarkeit von deCONZ-Ger\u00e4tetypen" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen", + "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen" + }, + "description": "Sichtbarkeit der deCONZ-Ger\u00e4tetypen konfigurieren" } } } diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 0f2009a46b687b..67af24ceb943d1 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -49,6 +49,13 @@ "allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ" }, "description": "Skonfiguruj widoczno\u015b\u0107 urz\u0105dze\u0144 deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Zezwalaj na czujniki deCONZ CLIP", + "allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ" + }, + "description": "Skonfiguruj widoczno\u015b\u0107 typ\u00f3w urz\u0105dze\u0144 deCONZ" } } } diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index ee7208cdf1731a..23e98919bb8076 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -49,6 +49,13 @@ "allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 deCONZ CLIP", + "allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ" } } } diff --git a/homeassistant/components/esphome/.translations/pl.json b/homeassistant/components/esphome/.translations/pl.json index c8e6012ea94582..9394b5af543cb2 100644 --- a/homeassistant/components/esphome/.translations/pl.json +++ b/homeassistant/components/esphome/.translations/pl.json @@ -26,7 +26,7 @@ "host": "Host", "port": "Port" }, - "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia swojego [ESPHome](https://esphomelib.com/) w\u0119z\u0142a.", + "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia [ESPHome](https://esphomelib.com/) w\u0119z\u0142a.", "title": "ESPHome" } }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/de.json b/homeassistant/components/geonetnz_quakes/.translations/de.json new file mode 100644 index 00000000000000..7c6fd08af96c8d --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Standort bereits registriert" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "F\u00fcllen Sie Ihre Filterdaten aus." + } + }, + "title": "GeoNet NZ Erdbeben" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/es.json b/homeassistant/components/geonetnz_quakes/.translations/es.json new file mode 100644 index 00000000000000..41404822dd8289 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "identifier_exists": "Ubicaci\u00f3n ya registrada" + }, + "step": { + "user": { + "data": { + "radius": "Radio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/ko.json b/homeassistant/components/geonetnz_quakes/.translations/ko.json new file mode 100644 index 00000000000000..26caa2ebe54cad --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/ko.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "\ubc18\uacbd" + }, + "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/no.json b/homeassistant/components/geonetnz_quakes/.translations/no.json new file mode 100644 index 00000000000000..40b695d6f51488 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Beliggenhet allerede er registrert" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "Fyll ut filterdetaljene." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/sl.json b/homeassistant/components/geonetnz_quakes/.translations/sl.json new file mode 100644 index 00000000000000..bdd05d339535b2 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/sl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokacija je \u017ee registrirana" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radij" + }, + "title": "Izpolnite podrobnosti filtra." + } + }, + "title": "GeoNet NZ Potresi" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/pl.json b/homeassistant/components/life360/.translations/pl.json index cd5e61fc123608..e9cd992030442b 100644 --- a/homeassistant/components/life360/.translations/pl.json +++ b/homeassistant/components/life360/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", - "user_already_configured": "Konto zosta\u0142o ju\u017c skonfigurowane." + "user_already_configured": "Konto jest ju\u017c skonfigurowane" }, "create_entry": { "default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})." @@ -11,7 +11,7 @@ "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", "unexpected": "Nieoczekiwany b\u0142\u0105d komunikacji z serwerem Life360", - "user_already_configured": "Konto zosta\u0142o ju\u017c skonfigurowane." + "user_already_configured": "Konto jest ju\u017c skonfigurowane" }, "step": { "user": { diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json new file mode 100644 index 00000000000000..edafcd7eff071c --- /dev/null +++ b/homeassistant/components/light/.translations/ca.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} apagat", + "turn_on": "{name} enc\u00e8s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json new file mode 100644 index 00000000000000..7b266ba74125c9 --- /dev/null +++ b/homeassistant/components/light/.translations/da.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} slukket", + "turn_on": "{name} t\u00e6ndt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json new file mode 100644 index 00000000000000..fcfc2773ed8941 --- /dev/null +++ b/homeassistant/components/light/.translations/de.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} ausgeschaltet", + "turn_on": "{name} eingeschaltet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json new file mode 100644 index 00000000000000..b56875453dd67e --- /dev/null +++ b/homeassistant/components/light/.translations/es.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{nombre} desactivado", + "turn_on": "{nombre} activado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json new file mode 100644 index 00000000000000..00d03b12d0130a --- /dev/null +++ b/homeassistant/components/light/.translations/fr.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} d\u00e9sactiv\u00e9", + "turn_on": "{name} activ\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json new file mode 100644 index 00000000000000..3d0e9630bd3687 --- /dev/null +++ b/homeassistant/components/light/.translations/ko.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turn_on": "{name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/no.json b/homeassistant/components/light/.translations/no.json new file mode 100644 index 00000000000000..39c391eff3356a --- /dev/null +++ b/homeassistant/components/light/.translations/no.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} sl\u00e5tt av", + "turn_on": "{name} sl\u00e5tt p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json new file mode 100644 index 00000000000000..a3df3639c22a8b --- /dev/null +++ b/homeassistant/components/light/.translations/pl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{nazwa} wy\u0142\u0105czone", + "turn_on": "{name} w\u0142\u0105czone" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json new file mode 100644 index 00000000000000..56a579b576e2a3 --- /dev/null +++ b/homeassistant/components/light/.translations/ru.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turn_on": "{name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/sl.json b/homeassistant/components/light/.translations/sl.json new file mode 100644 index 00000000000000..68e770e88731b8 --- /dev/null +++ b/homeassistant/components/light/.translations/sl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} izklopljeno", + "turn_on": "{name} vklopljeno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/zh-Hant.json b/homeassistant/components/light/.translations/zh-Hant.json new file mode 100644 index 00000000000000..66ed83aeb9b7ba --- /dev/null +++ b/homeassistant/components/light/.translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "\u7531 {name} \u95dc\u9589", + "turn_on": "\u7531 {name} \u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/ca.json b/homeassistant/components/linky/.translations/ca.json new file mode 100644 index 00000000000000..ca437417f590db --- /dev/null +++ b/homeassistant/components/linky/.translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "El compte ja ha estat configurat" + }, + "error": { + "access": "No s'ha pogut accedir a Enedis.fr, comprova la teva connexi\u00f3 a Internet", + "enedis": "Enedis.fr ha respost amb un error: torna-ho a provar m\u00e9s tard (millo no entre les 23:00 i les 14:00)", + "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard (millor no entre les 23:00 i les 14:00)", + "username_exists": "El compte ja ha estat configurat", + "wrong_login": "Error d\u2019inici de sessi\u00f3: comprova el teu correu electr\u00f2nic i la contrasenya" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "description": "Introdueix les teves credencials", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/da.json b/homeassistant/components/linky/.translations/da.json new file mode 100644 index 00000000000000..cacad99de584bb --- /dev/null +++ b/homeassistant/components/linky/.translations/da.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Kontoen er allerede konfigureret" + }, + "error": { + "access": "Kunne ikke f\u00e5 adgang til Enedis.fr, kontroller din internetforbindelse", + "enedis": "Enedis.fr svarede med en fejl: Pr\u00f8v igen senere (normalt ikke mellem 23:00 og 02:00)", + "unknown": "Ukendt fejl: Pr\u00f8v igen senere (normalt ikke mellem 23:00 og 02:00)", + "username_exists": "Kontoen er allerede konfigureret", + "wrong_login": "Loginfejl: Kontroller din e-mail og adgangskode" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "E-mail" + }, + "description": "Indtast dine legitimationsoplysninger", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/de.json b/homeassistant/components/linky/.translations/de.json new file mode 100644 index 00000000000000..3fc13126270c66 --- /dev/null +++ b/homeassistant/components/linky/.translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Konto bereits konfiguriert" + }, + "error": { + "access": "Konnte nicht auf Enedis.fr zugreifen, \u00fcberpr\u00fcfe bitte die Internetverbindung", + "enedis": "Enedis.fr antwortete mit einem Fehler: wiederhole den Vorgang sp\u00e4ter (in der Regel nicht zwischen 23 Uhr und 2 Uhr morgens)", + "unknown": "Unbekannter Fehler: Wiederhole den Vorgang sp\u00e4ter (in der Regel nicht zwischen 23 Uhr und 2 Uhr morgens)", + "username_exists": "Konto bereits konfiguriert", + "wrong_login": "Login-Fehler: Pr\u00fcfe bitte E-Mail & Passwort" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail" + }, + "description": "Gib deine Zugangsdaten ein", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/fr.json b/homeassistant/components/linky/.translations/fr.json new file mode 100644 index 00000000000000..af12c2b654d8ff --- /dev/null +++ b/homeassistant/components/linky/.translations/fr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "access": "Impossible d'acc\u00e9der \u00e0 Enedis.fr, merci de v\u00e9rifier votre connexion internet", + "enedis": "Erreur d'Enedis.fr: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", + "unknown": "Erreur inconnue: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", + "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9", + "wrong_login": "Impossible de vous identifier: merci de v\u00e9rifier vos identifiants" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Email" + }, + "description": "Entrez vos identifiants", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/ko.json b/homeassistant/components/linky/.translations/ko.json new file mode 100644 index 00000000000000..45172e70097596 --- /dev/null +++ b/homeassistant/components/linky/.translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "access": "Enedis.fr \uc5d0 \uc811\uc18d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc778\ud130\ub137 \uc5f0\uacb0\uc744 \ud655\uc778\ud574\ubcf4\uc138\uc694", + "enedis": "Enedis.fr \uc774 \uc624\ub958\ub85c \uc751\ub2f5\ud588\uc2b5\ub2c8\ub2e4: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694 (\uc800\ub141 11\uc2dc \ubd80\ud130 \uc0c8\ubcbd 2\uc2dc\ub294 \ud53c\ud574\uc8fc\uc138\uc694)", + "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694 (\uc800\ub141 11\uc2dc \ubd80\ud130 \uc0c8\ubcbd 2\uc2dc\ub294 \ud53c\ud574\uc8fc\uc138\uc694)", + "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "wrong_login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + }, + "description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json new file mode 100644 index 00000000000000..a4f68fa8687f0a --- /dev/null +++ b/homeassistant/components/linky/.translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "access": "Nie mo\u017cna uzyska\u0107 dost\u0119pu do Enedis.fr, sprawd\u017a po\u0142\u0105czenie internetowe", + "enedis": "Enedis.fr odpowiedzia\u0142 b\u0142\u0119dem: spr\u00f3buj ponownie p\u00f3\u017aniej (zwykle nie mi\u0119dzy 23:00, a 2:00)", + "unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej (zwykle nie mi\u0119dzy godzin\u0105 23:00, a 2:00)", + "username_exists": "Konto jest ju\u017c skonfigurowane", + "wrong_login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "E-mail" + }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json new file mode 100644 index 00000000000000..498b5b2f12f29b --- /dev/null +++ b/homeassistant/components/linky/.translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + }, + "error": { + "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443", + "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430", + "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/sl.json b/homeassistant/components/linky/.translations/sl.json new file mode 100644 index 00000000000000..9e9d6668fcb8fb --- /dev/null +++ b/homeassistant/components/linky/.translations/sl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Ra\u010dun \u017ee nastavljen" + }, + "error": { + "access": "Do Enedis.fr ni bilo mogo\u010de dostopati, preverite internetno povezavo", + "enedis": "Enedis.fr je odgovoril z napako: poskusite pozneje (ponavadi med 23. in 2. uro)", + "unknown": "Neznana napaka: Prosimo, poskusite pozneje (obi\u010dajno ne med 23. in 2. uro)", + "username_exists": "Ra\u010dun \u017ee nastavljen", + "wrong_login": "Napaka pri prijavi: preverite svoj e-po\u0161tni naslov in geslo" + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "description": "Vnesite svoje poverilnice", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/pl.json b/homeassistant/components/logi_circle/.translations/pl.json index f39df48ae5a483..5d8e6a0607df4a 100644 --- a/homeassistant/components/logi_circle/.translations/pl.json +++ b/homeassistant/components/logi_circle/.translations/pl.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do swojego konta Logi Circle, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})", + "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do konta Logi Circle, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})", "title": "Uwierzytelnij za pomoc\u0105 Logi Circle" }, "user": { diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json index d298b1e3b07a5c..d92d28d948419d 100644 --- a/homeassistant/components/met/.translations/ru.json +++ b/homeassistant/components/met/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" }, "step": { "user": { diff --git a/homeassistant/components/notion/.translations/pl.json b/homeassistant/components/notion/.translations/pl.json index c35de9c535c1d5..279290bff1e679 100644 --- a/homeassistant/components/notion/.translations/pl.json +++ b/homeassistant/components/notion/.translations/pl.json @@ -11,7 +11,7 @@ "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika/adres e-mail" }, - "title": "Wprowad\u017a swoje dane" + "title": "Wprowad\u017a dane" } }, "title": "Poj\u0119cie" diff --git a/homeassistant/components/openuv/.translations/pl.json b/homeassistant/components/openuv/.translations/pl.json index 2c4c47e8da44ed..ee3875c2903c42 100644 --- a/homeassistant/components/openuv/.translations/pl.json +++ b/homeassistant/components/openuv/.translations/pl.json @@ -12,7 +12,7 @@ "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna" }, - "title": "Wprowad\u017a swoje dane" + "title": "Wprowad\u017a dane" } }, "title": "OpenUV" diff --git a/homeassistant/components/point/.translations/pl.json b/homeassistant/components/point/.translations/pl.json index 66b454e47ff1b7..ca36001cc1ade8 100644 --- a/homeassistant/components/point/.translations/pl.json +++ b/homeassistant/components/point/.translations/pl.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do swojego konta Minut, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})", + "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do konta Minut, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})", "title": "Uwierzytelnienie Point" }, "user": { diff --git a/homeassistant/components/rainmachine/.translations/pl.json b/homeassistant/components/rainmachine/.translations/pl.json index 9891ac50f4811f..9ab6156549d5a1 100644 --- a/homeassistant/components/rainmachine/.translations/pl.json +++ b/homeassistant/components/rainmachine/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", + "identifier_exists": "Konto jest ju\u017c zarejestrowane", "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" }, "step": { @@ -11,7 +11,7 @@ "password": "Has\u0142o", "port": "Port" }, - "title": "Wprowad\u017a swoje dane" + "title": "Wprowad\u017a dane" } }, "title": "RainMachine" diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json index 0b83ba8cbedd10..c4d616600f56a7 100644 --- a/homeassistant/components/simplisafe/.translations/pl.json +++ b/homeassistant/components/simplisafe/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", + "identifier_exists": "Konto jest ju\u017c zarejestrowane", "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" }, "step": { @@ -11,7 +11,7 @@ "password": "Has\u0142o", "username": "Adres e-mail" }, - "title": "Wprowad\u017a swoje dane" + "title": "Wprowad\u017a dane" } }, "title": "SimpliSafe" diff --git a/homeassistant/components/toon/.translations/pl.json b/homeassistant/components/toon/.translations/pl.json index 26627389ddd592..403be9bc067a54 100644 --- a/homeassistant/components/toon/.translations/pl.json +++ b/homeassistant/components/toon/.translations/pl.json @@ -18,8 +18,8 @@ "tenant": "Najemca", "username": "Nazwa u\u017cytkownika" }, - "description": "Uwierzytelnij swoje konto Eneco Toon (nie konto programisty).", - "title": "Po\u0142\u0105cz swoje konto Toon" + "description": "Uwierzytelnij konto Eneco Toon (nie konto programisty).", + "title": "Po\u0142\u0105cz konto Toon" }, "display": { "data": { diff --git a/homeassistant/components/traccar/.translations/de.json b/homeassistant/components/traccar/.translations/de.json new file mode 100644 index 00000000000000..c835ddf76b2481 --- /dev/null +++ b/homeassistant/components/traccar/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Ihre Home Assistant-Instanz muss \u00fcber das Internet zug\u00e4nglich sein, um Nachrichten von Traccar zu empfangen.", + "one_instance_allowed": "Es ist nur eine einzelne Instanz erforderlich." + }, + "create_entry": { + "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]({docs_url}) f\u00fcr weitere Details." + }, + "step": { + "user": { + "description": "M\u00f6chten Sie Traccar wirklich einrichten?", + "title": "Traccar einrichten" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/es.json b/homeassistant/components/traccar/.translations/es.json new file mode 100644 index 00000000000000..ab8c0e70cd42e4 --- /dev/null +++ b/homeassistant/components/traccar/.translations/es.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Traccar." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/ko.json b/homeassistant/components/traccar/.translations/ko.json new file mode 100644 index 00000000000000..d9f31967e68b9e --- /dev/null +++ b/homeassistant/components/traccar/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Traccar \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.", + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "create_entry": { + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Traccar \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c URL \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4: `{webhook_url}`\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + }, + "step": { + "user": { + "description": "Traccar \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Traccar \uc124\uc815" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/no.json b/homeassistant/components/traccar/.translations/no.json new file mode 100644 index 00000000000000..dea146b649aace --- /dev/null +++ b/homeassistant/components/traccar/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant-forekomst m\u00e5 v\u00e6re tilgjengelig fra Internett for \u00e5 motta meldinger fra Traccar.", + "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + }, + "create_entry": { + "default": "Hvis du vil sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar.\n\nBruk f\u00f8lgende URL-adresse: ' {webhook_url} '\n\nSe [dokumentasjonen] ({docs_url}) for mer informasjon." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil sette opp Traccar?", + "title": "Sett opp Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/sl.json b/homeassistant/components/traccar/.translations/sl.json new file mode 100644 index 00000000000000..95aaca7e67df2f --- /dev/null +++ b/homeassistant/components/traccar/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Va\u0161 Home Assistant mora biti dostopen prek interneta, da boste lahko prejemali Traccar sporo\u010dila.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "\u010ce \u017eelite poslati dogodke v Home Assistant, boste morali nastaviti funkcijo \"webhook\" v traccar.\n\nUporabite naslednji URL: ' {webhook_url} '\n\nZa podrobnej\u0161e informacije glejte [dokumentacijo] ({docs_url})." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Traccar?", + "title": "Nastavite Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/de.json b/homeassistant/components/twentemilieu/.translations/de.json new file mode 100644 index 00000000000000..502a54a8a3d7ee --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adresse bereits eingerichtet." + }, + "error": { + "connection_error": "Fehler beim Herstellen einer Verbindung.", + "invalid_address": "Adresse nicht im Einzugsgebiet von Twente Milieu gefunden." + }, + "step": { + "user": { + "data": { + "house_letter": "Hausbrief/zusatz", + "house_number": "Hausnummer", + "post_code": "Postleitzahl" + }, + "description": "Richten Sie Twente Milieu mit Informationen zur Abfallsammlung unter Ihrer Adresse ein.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/es.json b/homeassistant/components/twentemilieu/.translations/es.json new file mode 100644 index 00000000000000..02dcb71f54e5ff --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "connection_error": "No se conect\u00f3." + }, + "step": { + "user": { + "data": { + "house_letter": "Letra de la casa/adicional", + "house_number": "N\u00famero de casa", + "post_code": "C\u00f3digo postal" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/ko.json b/homeassistant/components/twentemilieu/.translations/ko.json new file mode 100644 index 00000000000000..a78867d86a8ba2 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "\uc8fc\uc18c\uac00 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "invalid_address": "Twente Milieu \uc11c\ube44\uc2a4 \uc9c0\uc5ed\uc5d0\uc11c \uc8fc\uc18c\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "house_letter": "\uc9d1 \uc8fc\uc18c/\ucd94\uac00\uc815\ubcf4", + "house_number": "\uc9d1 \ubc88\ud638", + "post_code": "\uc6b0\ud3b8\ubc88\ud638" + }, + "description": "\uc8fc\uc18c\uc5d0 \uc4f0\ub808\uae30 \uc218\uac70 \uc815\ubcf4\ub97c \ub123\uc5b4 Twente Milieu \ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/no.json b/homeassistant/components/twentemilieu/.translations/no.json new file mode 100644 index 00000000000000..1d4395bb2c80fd --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adressen er allerede konfigurert." + }, + "error": { + "connection_error": "Tilkobling mislyktes.", + "invalid_address": "Adresse ble ikke funnet i Twente Milieu tjenesteomr\u00e5de." + }, + "step": { + "user": { + "data": { + "house_letter": "Hus brev/ekstra", + "house_number": "Husnummer", + "post_code": "Postnummer" + }, + "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/sl.json b/homeassistant/components/twentemilieu/.translations/sl.json new file mode 100644 index 00000000000000..7b74b96d0574a6 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Naslov je \u017ee nastavljen." + }, + "error": { + "connection_error": "Povezava ni uspela.", + "invalid_address": "V storitvenem obmo\u010dju Twente Milieu ni mogo\u010de najti naslova." + }, + "step": { + "user": { + "data": { + "house_letter": "Hi\u0161na \u0161tevilka -\u010drka/dodatno", + "house_number": "Hi\u0161na \u0161tevilka", + "post_code": "Po\u0161tna \u0161tevilka" + }, + "description": "Nastavite Twente milieu, ki zagotavlja informacije o zbiranju odpadkov na va\u0161em naslovu.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index 0c44871f583c97..e447e89644f5b5 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -32,6 +32,12 @@ "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)", "track_wired_clients": "Einbinden von kabelgebundenen Netzwerk-Clients" } + }, + "init": { + "data": { + "one": "eins", + "other": "andere" + } } } } diff --git a/homeassistant/components/velbus/.translations/de.json b/homeassistant/components/velbus/.translations/de.json new file mode 100644 index 00000000000000..72af917e12ed29 --- /dev/null +++ b/homeassistant/components/velbus/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Dieser Port ist bereits konfiguriert" + }, + "error": { + "connection_failed": "Die Velbus-Verbindung ist fehlgeschlagen", + "port_exists": "Dieser Port ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "name": "Der Name f\u00fcr diese Velbus-Verbindung", + "port": "Verbindungs details" + }, + "title": "Definieren des Velbus-Verbindungstyps" + } + }, + "title": "Velbus-Schnittstelle" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/es.json b/homeassistant/components/velbus/.translations/es.json new file mode 100644 index 00000000000000..e60ef7b4c676f1 --- /dev/null +++ b/homeassistant/components/velbus/.translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "port_exists": "Este puerto ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "port": "Cadena de conexi\u00f3n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/ko.json b/homeassistant/components/velbus/.translations/ko.json new file mode 100644 index 00000000000000..6e218afc97c10a --- /dev/null +++ b/homeassistant/components/velbus/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "\ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "connection_failed": "Velbus \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "port_exists": "\ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "name": "Velbus \uc5f0\uacb0 \uc774\ub984", + "port": "\uc5f0\uacb0 \ubb38\uc790\uc5f4" + }, + "title": "Velbus \uc5f0\uacb0 \uc720\ud615 \uc815\uc758" + } + }, + "title": "Velbus \uc778\ud130\ud398\uc774\uc2a4" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/no.json b/homeassistant/components/velbus/.translations/no.json new file mode 100644 index 00000000000000..c6b16170877edf --- /dev/null +++ b/homeassistant/components/velbus/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Denne porten er allerede konfigurert" + }, + "error": { + "connection_failed": "Velbus-tilkoblingen mislyktes", + "port_exists": "Denne porten er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "name": "Navnet p\u00e5 denne velbus tilkoblingen", + "port": "Tilkoblingsstreng" + }, + "title": "Definer tilkoblingstype for velbus" + } + }, + "title": "Velbus-grensesnitt" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/sl.json b/homeassistant/components/velbus/.translations/sl.json new file mode 100644 index 00000000000000..2fa1ccadcea616 --- /dev/null +++ b/homeassistant/components/velbus/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Ta vrata so \u017ee nastavljena" + }, + "error": { + "connection_failed": "Povezava z velbusom ni uspela", + "port_exists": "Ta vrata so \u017ee nastavljena" + }, + "step": { + "user": { + "data": { + "name": "Ime za to velbus povezavo", + "port": "Povezovalni niz" + }, + "title": "Dolo\u010dite vrsto povezave z velbusom" + } + }, + "title": "Velbus vmesnik" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/de.json b/homeassistant/components/vesync/.translations/de.json new file mode 100644 index 00000000000000..44b3ea86c5509d --- /dev/null +++ b/homeassistant/components/vesync/.translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Nur eine Vesync-Instanz ist zul\u00e4ssig" + }, + "error": { + "invalid_login": "Ung\u00fcltiger Benutzername oder Kennwort" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail-Adresse" + }, + "title": "Benutzername und Passwort eingeben" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/es.json b/homeassistant/components/vesync/.translations/es.json new file mode 100644 index 00000000000000..99611c5f9bfa79 --- /dev/null +++ b/homeassistant/components/vesync/.translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_login": "Nombre de usuario o contrase\u00f1a no v\u00e1lidos" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Introduzca el nombre de usuario y la contrase\u00f1a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/fr.json b/homeassistant/components/vesync/.translations/fr.json new file mode 100644 index 00000000000000..4928ea4f0be508 --- /dev/null +++ b/homeassistant/components/vesync/.translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Une seule instance de Vesync est autoris\u00e9e" + }, + "error": { + "invalid_login": "Nom d'utilisateur ou mot de passe invalide" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Adresse e-mail" + }, + "title": "Entrez vos identifiants" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/ko.json b/homeassistant/components/vesync/.translations/ko.json new file mode 100644 index 00000000000000..ca43b90acc9632 --- /dev/null +++ b/homeassistant/components/vesync/.translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Vesync \uc778\uc2a4\ud134\uc2a4\ub9cc \ud5c8\uc6a9\ub429\ub2c8\ub2e4" + }, + "error": { + "invalid_login": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + }, + "title": "\uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/no.json b/homeassistant/components/vesync/.translations/no.json new file mode 100644 index 00000000000000..be5f27b7a0f0e0 --- /dev/null +++ b/homeassistant/components/vesync/.translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Bare en Vesync-forekomst er tillatt" + }, + "error": { + "invalid_login": "Ugyldig brukernavn eller passord" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-postadresse" + }, + "title": "Skriv inn brukernavn og passord" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/sl.json b/homeassistant/components/vesync/.translations/sl.json new file mode 100644 index 00000000000000..636237dcfc1946 --- /dev/null +++ b/homeassistant/components/vesync/.translations/sl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Dovoljen je samo ena instanca Vesync" + }, + "error": { + "invalid_login": "Neveljavno uporabni\u0161ko ime ali geslo" + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "title": "Vnesite uporabni\u0161ko Ime in Geslo" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json new file mode 100644 index 00000000000000..a96f8cff523fdd --- /dev/null +++ b/homeassistant/components/withings/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Autenticaci\u00f3 exitosa amb Withings per al perfil seleccionat." + }, + "step": { + "user": { + "data": { + "profile": "Perfil" + }, + "description": "Selecciona un perfil d'usuari amb el qual vols que Home Assistant s'uneixi amb un perfil de Withings. A la p\u00e0gina de Withings, assegura't de seleccionar el mateix usuari o, les dades no seran les correctes.", + "title": "Perfil d'usuari." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/da.json b/homeassistant/components/withings/.translations/da.json new file mode 100644 index 00000000000000..8b7fbcb3bb4a0f --- /dev/null +++ b/homeassistant/components/withings/.translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "create_entry": { + "default": "Godkendt med Withings for den valgte profil." + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "title": "Brugerprofil." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json new file mode 100644 index 00000000000000..15b6f4e3b01f59 --- /dev/null +++ b/homeassistant/components/withings/.translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Erfolgreiche Authentifizierung mit Withings f\u00fcr das ausgew\u00e4hlte Profil." + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "description": "W\u00e4hlen Sie ein Benutzerprofil aus, dem Home Assistant ein Withings-Profil zuordnen soll. Stellen Sie sicher, dass Sie auf der Withings-Seite denselben Benutzer ausw\u00e4hlen, da sonst die Daten nicht korrekt gekennzeichnet werden.", + "title": "Benutzerprofil." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/es.json b/homeassistant/components/withings/.translations/es.json new file mode 100644 index 00000000000000..fac325a7097645 --- /dev/null +++ b/homeassistant/components/withings/.translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Autenticado correctamente con Withings para el perfil seleccionado." + }, + "step": { + "user": { + "data": { + "profile": "Perfil" + }, + "description": "Seleccione un perfil de usuario para el cual desea que Home Assistant se conecte con el perfil de Withings. En la p\u00e1gina de Withings, aseg\u00farese de seleccionar el mismo usuario o los datos no se identificar\u00e1n correctamente.", + "title": "Perfil de usuario." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json new file mode 100644 index 00000000000000..b66786cc9e0e18 --- /dev/null +++ b/homeassistant/components/withings/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "description": "S\u00e9lectionnez l'utilisateur que vous souhaitez associer \u00e0 Withings. Sur la page withings, veillez \u00e0 s\u00e9lectionner le m\u00eame utilisateur, sinon les donn\u00e9es ne seront pas \u00e9tiquet\u00e9es correctement.", + "title": "Profil utilisateur" + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/ko.json b/homeassistant/components/withings/.translations/ko.json new file mode 100644 index 00000000000000..3c2f00ba4aef76 --- /dev/null +++ b/homeassistant/components/withings/.translations/ko.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "\uc120\ud0dd\ud55c \ud504\ub85c\ud544\ub85c Withings \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "profile": "\ud504\ub85c\ud544" + }, + "description": "Home Assistant \uac00 Withings \ud504\ub85c\ud544\uacfc \ub9f5\ud551\ud560 \uc0ac\uc6a9\uc790 \ud504\ub85c\ud544\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. Withings \ud398\uc774\uc9c0\uc5d0\uc11c \ub3d9\uc77c\ud55c \uc0ac\uc6a9\uc790\ub97c \uc120\ud0dd\ud574\uc57c\ud569\ub2c8\ub2e4. \uadf8\ub807\uc9c0 \uc54a\uc73c\uba74 \ub370\uc774\ud130\uc5d0 \uc62c\ubc14\ub978 \ub808\uc774\ube14\uc774 \uc9c0\uc815\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "title": "\uc0ac\uc6a9\uc790 \ud504\ub85c\ud544." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/no.json b/homeassistant/components/withings/.translations/no.json new file mode 100644 index 00000000000000..22d8884d66a573 --- /dev/null +++ b/homeassistant/components/withings/.translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Vellykket autentisering for Withings og den valgte profilen." + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "description": "Velg en brukerprofil som du vil at Home Assistant skal kartlegge med en Withings-profil. P\u00e5 Withings-siden m\u00e5 du passe p\u00e5 at du velger samme bruker ellers vil ikke dataen bli merket riktig.", + "title": "Brukerprofil." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json new file mode 100644 index 00000000000000..1643ecb148012b --- /dev/null +++ b/homeassistant/components/withings/.translations/pl.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu" + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "description": "Wybierz profil u\u017cytkownika Withings, na kt\u00f3ry chcesz po\u0142\u0105czy\u0107 z Home Assistant'em. Na stronie Withings wybierz ten sam profil u\u017cytkownika by dane by\u0142y poprawnie oznaczone.", + "title": "Profil u\u017cytkownika" + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/ru.json b/homeassistant/components/withings/.translations/ru.json new file mode 100644 index 00000000000000..d9d5e14208f05b --- /dev/null +++ b/homeassistant/components/withings/.translations/ru.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "user": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u041d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 Withings \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u043e\u0433\u043e \u0436\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u0438\u043d\u0430\u0447\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u044b \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e.", + "title": "Withings" + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json new file mode 100644 index 00000000000000..d0fcb6a5276b61 --- /dev/null +++ b/homeassistant/components/withings/.translations/sl.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Uspe\u0161no overjen z Withings za izbrani profil." + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "description": "Izberite uporabni\u0161ki profil, za katerega \u017eelite, da se Home Assistant prika\u017ee s profilom Withings. Na Withings strani ne pozabite izbrati istega uporabnika sicer podatki ne bodo pravilno ozna\u010deni.", + "title": "Uporabni\u0161ki profil." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json new file mode 100644 index 00000000000000..30a77102d04a31 --- /dev/null +++ b/homeassistant/components/withings/.translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "data": { + "profile": "\u500b\u4eba\u8a2d\u5b9a" + }, + "description": "\u9078\u64c7 Home Assistant \u6240\u8981\u5c0d\u61c9\u4f7f\u7528\u7684 Withings \u500b\u4eba\u8a2d\u5b9a\u3002\u65bc Withings \u9801\u9762\u3001\u78ba\u5b9a\u9078\u53d6\u76f8\u540c\u7684\u4f7f\u7528\u8005\uff0c\u5426\u5247\u8cc7\u6599\u5c07\u7121\u6cd5\u6b63\u78ba\u6a19\u793a\u3002", + "title": "\u500b\u4eba\u8a2d\u5b9a\u3002" + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/pl.json b/homeassistant/components/wwlln/.translations/pl.json index 704c7baeecb3c2..652d580644fce6 100644 --- a/homeassistant/components/wwlln/.translations/pl.json +++ b/homeassistant/components/wwlln/.translations/pl.json @@ -10,7 +10,7 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "radius": "Promie\u0144 (przy u\u017cyciu systemu jednostki bazowej)" }, - "title": "Wpisz informacje o swojej lokalizacji." + "title": "Wprowad\u017a informacje o lokalizacji." } }, "title": "\u015awiatowa sie\u0107 lokalizacji wy\u0142adowa\u0144 atmosferycznych (WWLLN)" From c50faaef3cbfb21d16e02c365503cf854dbc55f6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 5 Sep 2019 14:20:08 +0200 Subject: [PATCH 0183/3953] Cleanup Dockerfile.dev (#26451) --- Dockerfile.dev | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index 00f5576bdbb0fc..457dc7b3d0fcd2 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -23,9 +23,10 @@ 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 /workspaces/ -RUN pip3 install -r requirements_test_all.txt -c package_constraints.txt +# Install Python dependencies from requirements +COPY requirements_test_all.txt homeassistant/package_constraints.txt ./ +RUN pip3 install -r requirements_test_all.txt -c package_constraints.txt \ + && rm -f requirements_test_all.txt package_constraints.txt # Set the default shell to bash instead of sh ENV SHELL /bin/bash From f7dc537275aa12c18bbdbec5b4b1a3d4eb3b5260 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 Sep 2019 16:49:32 +0200 Subject: [PATCH 0184/3953] Add device automation condition (#26313) * Add support for device conditions * Lint * Update test case * Make and+or conditions async, adjust tests * Cleanup tests * Remove non callback versions of conditions, correct typing * Correct typing * Update light/strings.json * Address review comments * Make device automation lists simple lists, not dicts * Add device_automation/const.py * Use IS_ON/IS_OFF everywhere for conditions --- .../components/automation/__init__.py | 6 +- .../components/device_automation/__init__.py | 71 +++- .../components/device_automation/const.py | 5 + .../components/light/device_automation.py | 89 ++++- homeassistant/components/light/strings.json | 4 + homeassistant/helpers/condition.py | 87 ++--- homeassistant/helpers/config_validation.py | 13 +- homeassistant/helpers/script.py | 2 +- .../components/device_automation/test_init.py | 50 ++- .../light/test_device_automation.py | 115 +++++- tests/helpers/test_condition.py | 351 +++++++++--------- 11 files changed, 505 insertions(+), 288 deletions(-) create mode 100644 homeassistant/components/device_automation/const.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 1cffd361b19210..3849188c6b3b9b 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -386,7 +386,7 @@ async def _async_process_config(hass, config, component): action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name) if CONF_CONDITION in config_block: - cond_func = _async_process_if(hass, config, config_block) + cond_func = await _async_process_if(hass, config, config_block) if cond_func is None: continue @@ -437,14 +437,14 @@ async def action(entity_id, variables, context): return action -def _async_process_if(hass, config, p_config): +async def _async_process_if(hass, config, p_config): """Process if checks.""" if_configs = p_config.get(CONF_CONDITION) checks = [] for if_config in if_configs: try: - checks.append(condition.async_from_config(if_config, False)) + checks.append(await condition.async_from_config(hass, if_config, False)) except HomeAssistantError as ex: _LOGGER.warning("Invalid condition: %s", ex) return None diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 018e1286d8bc53..cc0e8c25979642 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,12 +1,16 @@ """Helpers for device automations.""" import asyncio import logging +from typing import Callable, cast import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.core import split_entity_id +from homeassistant.const import CONF_DOMAIN +from homeassistant.core import split_entity_id, HomeAssistant +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration, IntegrationNotFound DOMAIN = "device_automation" @@ -16,14 +20,31 @@ async def async_setup(hass, config): """Set up device automation.""" + hass.components.websocket_api.async_register_command( + websocket_device_automation_list_conditions + ) hass.components.websocket_api.async_register_command( websocket_device_automation_list_triggers ) return True -async def _async_get_device_automation_triggers(hass, domain, device_id): - """List device triggers.""" +async def async_device_condition_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True +) -> Callable[..., bool]: + """Wrap action method with state based condition.""" + if config_validation: + config = cv.DEVICE_CONDITION_SCHEMA(config) + integration = await async_get_integration(hass, config[CONF_DOMAIN]) + platform = integration.get_platform("device_automation") + return cast( + Callable[..., bool], + platform.async_condition_from_config(config, config_validation), # type: ignore + ) + + +async def _async_get_device_automations_from_domain(hass, domain, fname, device_id): + """List device automations.""" integration = None try: integration = await async_get_integration(hass, domain) @@ -37,19 +58,19 @@ async def _async_get_device_automation_triggers(hass, domain, device_id): # The domain does not have device automations return None - if hasattr(platform, "async_get_triggers"): - return await platform.async_get_triggers(hass, device_id) + if hasattr(platform, fname): + return await getattr(platform, fname)(hass, device_id) -async def async_get_device_automation_triggers(hass, device_id): - """List device triggers.""" +async def _async_get_device_automations(hass, fname, device_id): + """List device automations.""" device_registry, entity_registry = await asyncio.gather( hass.helpers.device_registry.async_get_registry(), hass.helpers.entity_registry.async_get_registry(), ) domains = set() - triggers = [] + automations = [] device = device_registry.async_get(device_id) for entry_id in device.config_entries: config_entry = hass.config_entries.async_get_entry(entry_id) @@ -59,17 +80,33 @@ async def async_get_device_automation_triggers(hass, device_id): for entity in entities: domains.add(split_entity_id(entity.entity_id)[0]) - device_triggers = await asyncio.gather( + device_automations = await asyncio.gather( *( - _async_get_device_automation_triggers(hass, domain, device_id) + _async_get_device_automations_from_domain(hass, domain, fname, device_id) for domain in domains ) ) - for device_trigger in device_triggers: - if device_trigger is not None: - triggers.extend(device_trigger) + for device_automation in device_automations: + if device_automation is not None: + automations.extend(device_automation) + + return automations - return triggers + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/condition/list", + vol.Required("device_id"): str, + } +) +async def websocket_device_automation_list_conditions(hass, connection, msg): + """Handle request for device conditions.""" + device_id = msg["device_id"] + conditions = await _async_get_device_automations( + hass, "async_get_conditions", device_id + ) + connection.send_result(msg["id"], conditions) @websocket_api.async_response @@ -82,5 +119,7 @@ async def async_get_device_automation_triggers(hass, device_id): async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] - triggers = await async_get_device_automation_triggers(hass, device_id) - connection.send_result(msg["id"], {"triggers": triggers}) + triggers = await _async_get_device_automations( + hass, "async_get_triggers", device_id + ) + connection.send_result(msg["id"], triggers) diff --git a/homeassistant/components/device_automation/const.py b/homeassistant/components/device_automation/const.py new file mode 100644 index 00000000000000..b846718e96e7df --- /dev/null +++ b/homeassistant/components/device_automation/const.py @@ -0,0 +1,5 @@ +"""Constants for device automations.""" +CONF_IS_OFF = "is_off" +CONF_IS_ON = "is_on" +CONF_TURN_OFF = "turn_off" +CONF_TURN_ON = "turn_on" diff --git a/homeassistant/components/light/device_automation.py b/homeassistant/components/light/device_automation.py index ed75b5f906f5a8..66bad135659b3c 100644 --- a/homeassistant/components/light/device_automation.py +++ b/homeassistant/components/light/device_automation.py @@ -2,39 +2,70 @@ import voluptuous as vol import homeassistant.components.automation.state as state +from homeassistant.components.device_automation.const import ( + CONF_IS_OFF, + CONF_IS_ON, + CONF_TURN_OFF, + CONF_TURN_ON, +) from homeassistant.core import split_entity_id from homeassistant.const import ( + CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_PLATFORM, CONF_TYPE, ) -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import condition, 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" +ENTITY_CONDITIONS = [ + { + # True when light is turned off + CONF_CONDITION: "device", + CONF_DOMAIN: DOMAIN, + CONF_TYPE: CONF_IS_OFF, + }, + { + # True when light is turned on + CONF_CONDITION: "device", + CONF_DOMAIN: DOMAIN, + CONF_TYPE: CONF_IS_ON, + }, +] ENTITY_TRIGGERS = [ { - # Trigger when light is turned on + # Trigger when light is turned off CONF_PLATFORM: "device", CONF_DOMAIN: DOMAIN, CONF_TYPE: CONF_TURN_OFF, }, { - # Trigger when light is turned off + # Trigger when light is turned on CONF_PLATFORM: "device", CONF_DOMAIN: DOMAIN, CONF_TYPE: CONF_TURN_ON, }, ] +CONDITION_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_CONDITION): "device", + vol.Optional(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), + } + ) +) + TRIGGER_SCHEMA = vol.All( vol.Schema( { @@ -42,7 +73,7 @@ vol.Optional(CONF_DEVICE_ID): str, vol.Required(CONF_DOMAIN): DOMAIN, vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): str, + vol.Required(CONF_TYPE): vol.In([CONF_TURN_OFF, CONF_TURN_ON]), } ) ) @@ -52,9 +83,27 @@ def _is_domain(entity, domain): return split_entity_id(entity.entity_id)[0] == domain +def async_condition_from_config(config, config_validation): + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + condition_type = config[CONF_TYPE] + if condition_type == CONF_IS_ON: + stat = "on" + else: + stat = "off" + state_config = { + condition.CONF_CONDITION: "state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + condition.CONF_STATE: stat, + } + + return condition.state_from_config(state_config, config_validation) + + async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - trigger_type = config.get(CONF_TYPE) + config = TRIGGER_SCHEMA(config) + trigger_type = config[CONF_TYPE] if trigger_type == CONF_TURN_ON: from_state = "off" to_state = "on" @@ -75,17 +124,27 @@ async def async_trigger(hass, config, action, automation_info): return await async_attach_trigger(hass, config, action, automation_info) -async def async_get_triggers(hass, device_id): - """List device triggers.""" - triggers = [] +async def _async_get_automations(hass, device_id, automation_templates): + """List device automations.""" + automations = [] entity_registry = await hass.helpers.entity_registry.async_get_registry() entities = async_entries_for_device(entity_registry, device_id) domain_entities = [x for x in entities if _is_domain(x, DOMAIN)] for entity in domain_entities: - for trigger in ENTITY_TRIGGERS: - trigger = dict(trigger) - trigger.update(device_id=device_id, entity_id=entity.entity_id) - triggers.append(trigger) + for automation in automation_templates: + automation = dict(automation) + automation.update(device_id=device_id, entity_id=entity.entity_id) + automations.append(automation) + + return automations + - return triggers +async def async_get_conditions(hass, device_id): + """List device conditions.""" + return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS) diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 94954bb790b198..6f002d9ed8cbea 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -1,5 +1,9 @@ { "device_automation": { + "condition_type": { + "is_on": "{name} is on", + "is_off": "{name} is off" + }, "trigger_type": { "turn_on": "{name} turned on", "turn_off": "{name} turned off" diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 40465f83728c0a..133251e779d1db 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -1,4 +1,5 @@ """Offer reusable conditions.""" +import asyncio from datetime import datetime, timedelta import functools as ft import logging @@ -10,6 +11,9 @@ from homeassistant.core import HomeAssistant, State from homeassistant.components import zone as zone_cmp +from homeassistant.components.device_automation import ( # noqa: F401 pylint: disable=unused-import + async_device_condition_from_config as async_device_from_config, +) from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, @@ -41,40 +45,9 @@ _LOGGER = logging.getLogger(__name__) -# PyLint does not like the use of _threaded_factory -# pylint: disable=invalid-name - - -def _threaded_factory( - async_factory: Callable[[ConfigType, bool], Callable[..., bool]] -) -> Callable[[ConfigType, bool], Callable[..., bool]]: - """Create threaded versions of async factories.""" - - @ft.wraps(async_factory) - def factory( - config: ConfigType, config_validation: bool = True - ) -> Callable[..., bool]: - """Threaded factory.""" - async_check = async_factory(config, config_validation) - - def condition_if( - hass: HomeAssistant, variables: TemplateVarsType = None - ) -> bool: - """Validate condition.""" - return cast( - bool, - run_callback_threadsafe( - hass.loop, async_check, hass, variables - ).result(), - ) - - return condition_if - return factory - - -def async_from_config( - config: ConfigType, config_validation: bool = True +async def async_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True ) -> Callable[..., bool]: """Turn a condition configuration into a method. @@ -95,29 +68,30 @@ def async_from_config( ) ) - return cast(Callable[..., bool], factory(config, config_validation)) - + # Check for partials to properly determine if coroutine function + check_factory = factory + while isinstance(check_factory, ft.partial): + check_factory = check_factory.func -from_config = _threaded_factory(async_from_config) + if asyncio.iscoroutinefunction(check_factory): + return cast(Callable[..., bool], await factory(hass, config, config_validation)) + return cast(Callable[..., bool], factory(config, config_validation)) -def async_and_from_config( - config: ConfigType, config_validation: bool = True +async def async_and_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True ) -> Callable[..., bool]: """Create multi condition matcher using 'AND'.""" if config_validation: config = cv.AND_CONDITION_SCHEMA(config) - checks = None + checks = [ + await async_from_config(hass, entry, False) for entry in config["conditions"] + ] def if_and_condition( hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: """Test and condition.""" - nonlocal checks - - if checks is None: - checks = [async_from_config(entry, False) for entry in config["conditions"]] - try: for check in checks: if not check(hass, variables): @@ -131,26 +105,20 @@ def if_and_condition( return if_and_condition -and_from_config = _threaded_factory(async_and_from_config) - - -def async_or_from_config( - config: ConfigType, config_validation: bool = True +async def async_or_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True ) -> Callable[..., bool]: """Create multi condition matcher using 'OR'.""" if config_validation: config = cv.OR_CONDITION_SCHEMA(config) - checks = None + checks = [ + await async_from_config(hass, entry, False) for entry in config["conditions"] + ] def if_or_condition( hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: """Test and condition.""" - nonlocal checks - - if checks is None: - checks = [async_from_config(entry, False) for entry in config["conditions"]] - try: for check in checks: if check(hass, variables): @@ -163,9 +131,6 @@ def if_or_condition( return if_or_condition -or_from_config = _threaded_factory(async_or_from_config) - - def numeric_state( hass: HomeAssistant, entity: Union[None, str, State], @@ -263,9 +228,6 @@ def if_numeric_state( return if_numeric_state -numeric_state_from_config = _threaded_factory(async_numeric_state_from_config) - - def state( hass: HomeAssistant, entity: Union[None, str, State], @@ -423,9 +385,6 @@ def template_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool return template_if -template_from_config = _threaded_factory(async_template_from_config) - - def time( before: Optional[dt_util.dt.time] = None, after: Optional[dt_util.dt.time] = None, diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 471c6d50360698..3aa17befd484d0 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -24,10 +24,13 @@ CONF_ALIAS, CONF_BELOW, CONF_CONDITION, + CONF_DOMAIN, CONF_ENTITY_ID, CONF_ENTITY_NAMESPACE, + CONF_FOR, CONF_PLATFORM, CONF_SCAN_INTERVAL, + CONF_STATE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, CONF_VALUE_TEMPLATE, @@ -746,8 +749,8 @@ def validator(value): { vol.Required(CONF_CONDITION): "state", vol.Required(CONF_ENTITY_ID): entity_id, - vol.Required("state"): str, - vol.Optional("for"): vol.All(time_period, positive_timedelta), + vol.Required(CONF_STATE): str, + vol.Optional(CONF_FOR): vol.All(time_period, positive_timedelta), # To support use_trigger_value in automation # Deprecated 2016/04/25 vol.Optional("from"): str, @@ -823,6 +826,11 @@ def validator(value): } ) +DEVICE_CONDITION_SCHEMA = vol.Schema( + {vol.Required(CONF_CONDITION): "device", vol.Required(CONF_DOMAIN): str}, + extra=vol.ALLOW_EXTRA, +) + CONDITION_SCHEMA: vol.Schema = vol.Any( NUMERIC_STATE_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA, @@ -832,6 +840,7 @@ def validator(value): ZONE_CONDITION_SCHEMA, AND_CONDITION_SCHEMA, OR_CONDITION_SCHEMA, + DEVICE_CONDITION_SCHEMA, ) _SCRIPT_DELAY_SCHEMA = vol.Schema( diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 43ef156ef09f3f..da173efcba6b9c 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -338,7 +338,7 @@ async def _async_check_condition(self, action, variables, context): config_cache_key = frozenset((k, str(v)) for k, v in action.items()) config = self._config_cache.get(config_cache_key) if not config: - config = condition.async_from_config(action, False) + config = await condition.async_from_config(self.hass, action, False) self._config_cache[config_cache_key] = config self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION]) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 16320257b40bc2..4bcb68f119b80f 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -21,7 +21,7 @@ def entity_reg(hass): return mock_registry(hass) -def _same_triggers(a, b): +def _same_lists(a, b): if len(a) != len(b): return False @@ -31,6 +31,50 @@ def _same_triggers(a, b): return True +async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity_reg): + """Test we get the expected conditions from a light through websocket.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": "light", + "type": "is_off", + "device_id": device_entry.id, + "entity_id": "light.test_5678", + }, + { + "condition": "device", + "domain": "light", + "type": "is_on", + "device_id": device_entry.id, + "entity_id": "light.test_5678", + }, + ] + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/list", + "device_id": device_entry.id, + } + ) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + conditions = msg["result"] + assert _same_lists(conditions, expected_conditions) + + async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_reg): """Test we get the expected triggers from a light through websocket.""" await async_setup_component(hass, "device_automation", {}) @@ -71,5 +115,5 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert msg["id"] == 1 assert msg["type"] == TYPE_RESULT assert msg["success"] - triggers = msg["result"]["triggers"] - assert _same_triggers(triggers, expected_triggers) + triggers = msg["result"] + assert _same_lists(triggers, expected_triggers) diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py index 3e92c15ee06d2d..0e356ae13aa0ab 100644 --- a/tests/components/light/test_device_automation.py +++ b/tests/components/light/test_device_automation.py @@ -6,11 +6,10 @@ from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.components.device_automation import ( - async_get_device_automation_triggers, + _async_get_device_automations as async_get_device_automations, ) from homeassistant.helpers import device_registry - from tests.common import ( MockConfigEntry, async_mock_service, @@ -37,7 +36,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -def _same_triggers(a, b): +def _same_lists(a, b): if len(a) != len(b): return False @@ -47,6 +46,37 @@ def _same_triggers(a, b): return True +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a light.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": "light", + "type": "is_off", + "device_id": device_entry.id, + "entity_id": "light.test_5678", + }, + { + "condition": "device", + "domain": "light", + "type": "is_on", + "device_id": device_entry.id, + "entity_id": "light.test_5678", + }, + ] + conditions = await async_get_device_automations( + hass, "async_get_conditions", device_entry.id + ) + assert _same_lists(conditions, expected_conditions) + + async def test_get_triggers(hass, device_reg, entity_reg): """Test we get the expected triggers from a light.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -72,8 +102,10 @@ async def test_get_triggers(hass, device_reg, entity_reg): "entity_id": "light.test_5678", }, ] - triggers = await async_get_device_automation_triggers(hass, device_entry.id) - assert _same_triggers(triggers, expected_triggers) + triggers = await async_get_device_automations( + hass, "async_get_triggers", device_entry.id + ) + assert _same_lists(triggers, expected_triggers) async def test_if_fires_on_state_change(hass, calls): @@ -158,3 +190,76 @@ async def test_if_fires_on_state_change(hass, calls): assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( dev1.entity_id ) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, "test.light") + + platform.init() + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + + dev1, dev2, dev3 = platform.DEVICES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": "light", + "entity_id": dev1.entity_id, + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": "light", + "entity_id": dev1.entity_id, + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index ddd22107fa08df..b603f98bb04b93 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -4,182 +4,175 @@ from homeassistant.helpers import condition from homeassistant.util import dt -from tests.common import get_test_home_assistant - - -class TestConditionHelper: - """Test condition helpers.""" - - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() - - def test_and_condition(self): - """Test the 'and' condition.""" - test = condition.from_config( - { - "condition": "and", - "conditions": [ - { - "condition": "state", - "entity_id": "sensor.temperature", - "state": "100", - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": 110, - }, - ], - } - ) - - self.hass.states.set("sensor.temperature", 120) - assert not test(self.hass) - - self.hass.states.set("sensor.temperature", 105) - assert not test(self.hass) - - self.hass.states.set("sensor.temperature", 100) - assert test(self.hass) - - def test_and_condition_with_template(self): - """Test the 'and' condition.""" - test = condition.from_config( - { - "condition": "and", - "conditions": [ - { - "condition": "template", - "value_template": '{{ states.sensor.temperature.state == "100" }}', - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": 110, - }, - ], - } - ) - - self.hass.states.set("sensor.temperature", 120) - assert not test(self.hass) - - self.hass.states.set("sensor.temperature", 105) - assert not test(self.hass) - - self.hass.states.set("sensor.temperature", 100) - assert test(self.hass) - - def test_or_condition(self): - """Test the 'or' condition.""" - test = condition.from_config( - { - "condition": "or", - "conditions": [ - { - "condition": "state", - "entity_id": "sensor.temperature", - "state": "100", - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": 110, - }, - ], - } - ) - - self.hass.states.set("sensor.temperature", 120) - assert not test(self.hass) - - self.hass.states.set("sensor.temperature", 105) - assert test(self.hass) - - self.hass.states.set("sensor.temperature", 100) - assert test(self.hass) - - def test_or_condition_with_template(self): - """Test the 'or' condition.""" - test = condition.from_config( - { - "condition": "or", - "conditions": [ - { - "condition": "template", - "value_template": '{{ states.sensor.temperature.state == "100" }}', - }, - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": 110, - }, - ], - } - ) - - self.hass.states.set("sensor.temperature", 120) - assert not test(self.hass) - - self.hass.states.set("sensor.temperature", 105) - assert test(self.hass) - - self.hass.states.set("sensor.temperature", 100) - assert test(self.hass) - - def test_time_window(self): - """Test time condition windows.""" - sixam = dt.parse_time("06:00:00") - sixpm = dt.parse_time("18:00:00") - - with patch( - "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=3), - ): - assert not condition.time(after=sixam, before=sixpm) - assert condition.time(after=sixpm, before=sixam) - - with patch( - "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=9), - ): - assert condition.time(after=sixam, before=sixpm) - assert not condition.time(after=sixpm, before=sixam) - - with patch( - "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=15), - ): - assert condition.time(after=sixam, before=sixpm) - assert not condition.time(after=sixpm, before=sixam) - - with patch( - "homeassistant.helpers.condition.dt_util.now", - return_value=dt.now().replace(hour=21), - ): - assert not condition.time(after=sixam, before=sixpm) - assert condition.time(after=sixpm, before=sixam) - - def test_if_numeric_state_not_raise_on_unavailable(self): - """Test numeric_state doesn't raise on unavailable/unknown state.""" - test = condition.from_config( - { - "condition": "numeric_state", - "entity_id": "sensor.temperature", - "below": 42, - } - ) - - with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn: - self.hass.states.set("sensor.temperature", "unavailable") - assert not test(self.hass) - assert len(logwarn.mock_calls) == 0 - - self.hass.states.set("sensor.temperature", "unknown") - assert not test(self.hass) - assert len(logwarn.mock_calls) == 0 + +async def test_and_condition(hass): + """Test the 'and' condition.""" + test = await condition.async_from_config( + hass, + { + "condition": "and", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + }, + ) + + hass.states.async_set("sensor.temperature", 120) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 105) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 100) + assert test(hass) + + +async def test_and_condition_with_template(hass): + """Test the 'and' condition.""" + test = await condition.async_from_config( + hass, + { + "condition": "and", + "conditions": [ + { + "condition": "template", + "value_template": '{{ states.sensor.temperature.state == "100" }}', + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + }, + ) + + hass.states.async_set("sensor.temperature", 120) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 105) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 100) + assert test(hass) + + +async def test_or_condition(hass): + """Test the 'or' condition.""" + test = await condition.async_from_config( + hass, + { + "condition": "or", + "conditions": [ + { + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + }, + ) + + hass.states.async_set("sensor.temperature", 120) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 105) + assert test(hass) + + hass.states.async_set("sensor.temperature", 100) + assert test(hass) + + +async def test_or_condition_with_template(hass): + """Test the 'or' condition.""" + test = await condition.async_from_config( + hass, + { + "condition": "or", + "conditions": [ + { + "condition": "template", + "value_template": '{{ states.sensor.temperature.state == "100" }}', + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + }, + ) + + hass.states.async_set("sensor.temperature", 120) + assert not test(hass) + + hass.states.async_set("sensor.temperature", 105) + assert test(hass) + + hass.states.async_set("sensor.temperature", 100) + assert test(hass) + + +async def test_time_window(hass): + """Test time condition windows.""" + sixam = dt.parse_time("06:00:00") + sixpm = dt.parse_time("18:00:00") + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt.now().replace(hour=3), + ): + assert not condition.time(after=sixam, before=sixpm) + assert condition.time(after=sixpm, before=sixam) + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt.now().replace(hour=9), + ): + assert condition.time(after=sixam, before=sixpm) + assert not condition.time(after=sixpm, before=sixam) + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt.now().replace(hour=15), + ): + assert condition.time(after=sixam, before=sixpm) + assert not condition.time(after=sixpm, before=sixam) + + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt.now().replace(hour=21), + ): + assert not condition.time(after=sixam, before=sixpm) + assert condition.time(after=sixpm, before=sixam) + + +async def test_if_numeric_state_not_raise_on_unavailable(hass): + """Test numeric_state doesn't raise on unavailable/unknown state.""" + test = await condition.async_from_config( + hass, + {"condition": "numeric_state", "entity_id": "sensor.temperature", "below": 42}, + ) + + with patch("homeassistant.helpers.condition._LOGGER.warning") as logwarn: + hass.states.async_set("sensor.temperature", "unavailable") + assert not test(hass) + assert len(logwarn.mock_calls) == 0 + + hass.states.async_set("sensor.temperature", "unknown") + assert not test(hass) + assert len(logwarn.mock_calls) == 0 From a00012572989058c3772a35973ef161e95df21bb Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 6 Sep 2019 01:11:48 +1000 Subject: [PATCH 0185/3953] Queensland Bushfire Alert icon for geolocation entities (#26439) * define icon * reordered const imports --- .../components/qld_bushfire/geo_location.py | 5 +++++ .../qld_bushfire/test_geo_location.py | 18 +++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index e8d32c036d561f..8ae80ca9027a43 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -198,6 +198,11 @@ def _update_from_feed(self, feed_entry): self._updated_date = feed_entry.updated self._status = feed_entry.status + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:fire" + @property def source(self) -> str: """Return source value of this external event.""" diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index c74630f7cd2274..59de4643cb8e8b 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -5,23 +5,24 @@ from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.qld_bushfire.geo_location import ( - ATTR_EXTERNAL_ID, - SCAN_INTERVAL, ATTR_CATEGORY, - ATTR_STATUS, + ATTR_EXTERNAL_ID, ATTR_PUBLICATION_DATE, + ATTR_STATUS, ATTR_UPDATED_DATE, + SCAN_INTERVAL, ) from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - CONF_RADIUS, + ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_fire_time_changed @@ -122,6 +123,7 @@ async def test_setup(hass): ATTR_STATUS: "Status 1", ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "qld_bushfire", + ATTR_ICON: "mdi:fire", } assert float(state.state) == 15.5 @@ -135,6 +137,7 @@ async def test_setup(hass): ATTR_FRIENDLY_NAME: "Title 2", ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "qld_bushfire", + ATTR_ICON: "mdi:fire", } assert float(state.state) == 20.5 @@ -148,6 +151,7 @@ async def test_setup(hass): ATTR_FRIENDLY_NAME: "Title 3", ATTR_UNIT_OF_MEASUREMENT: "km", ATTR_SOURCE: "qld_bushfire", + ATTR_ICON: "mdi:fire", } assert float(state.state) == 25.5 From 2cd845fb253c54715f5674489e2d4e88cfd23294 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 5 Sep 2019 12:50:26 -0500 Subject: [PATCH 0186/3953] Standardize Plex server connections (#26444) * Common connection class * Omit tests for new Plex files * Oops * Add missing properties * Remove redundant log message * Stopgap to avoid duplicate setups * Cleaner check for server setup Co-Authored-By: Martin Hjelmare * Cleaner check for server setup Co-Authored-By: Martin Hjelmare * Not needed with previous setup check * Remove username/password support * Reduce log level Co-Authored-By: Martin Hjelmare * Don't do setup in __init__ * Oops * Committing too fast... * Connect after init * Catch update exceptions like media_player * Pass in validated PlexServer instance * Remove unnecessary check * Counter should be unknown on init * Remove servername config option --- .coveragerc | 3 +- homeassistant/components/plex/const.py | 15 ++++ homeassistant/components/plex/media_player.py | 59 +++++++------ homeassistant/components/plex/sensor.py | 88 ++++++------------- homeassistant/components/plex/server.py | 59 +++++++++++++ 5 files changed, 136 insertions(+), 88 deletions(-) create mode 100644 homeassistant/components/plex/const.py create mode 100644 homeassistant/components/plex/server.py diff --git a/.coveragerc b/.coveragerc index e9b58c87baadef..e75c4180ef6e04 100644 --- a/.coveragerc +++ b/.coveragerc @@ -470,8 +470,7 @@ omit = homeassistant/components/pioneer/media_player.py homeassistant/components/pjlink/media_player.py homeassistant/components/plaato/* - homeassistant/components/plex/media_player.py - homeassistant/components/plex/sensor.py + homeassistant/components/plex/* homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py new file mode 100644 index 00000000000000..1c93ff24c400ff --- /dev/null +++ b/homeassistant/components/plex/const.py @@ -0,0 +1,15 @@ +"""Constants for the Plex component.""" +DOMAIN = "plex" +NAME_FORMAT = "Plex {}" + +DEFAULT_PORT = 32400 +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True + +PLEX_CONFIG_FILE = "plex.conf" +PLEX_SERVER_CONFIG = "server_config" + +CONF_USE_EPISODE_ART = "use_episode_art" +CONF_SHOW_ALL_CONTROLS = "show_all_controls" +CONF_REMOVE_UNAVAILABLE_CLIENTS = "remove_unavailable_clients" +CONF_CLIENT_REMOVE_INTERVAL = "client_remove_interval" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 39694a061c4839..5b427e6a353fbc 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -2,8 +2,7 @@ from datetime import timedelta import json import logging - -import requests +import requests.exceptions import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -21,6 +20,9 @@ SUPPORT_VOLUME_SET, ) from homeassistant.const import ( + CONF_URL, + CONF_TOKEN, + CONF_VERIFY_SSL, DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, @@ -32,17 +34,21 @@ from homeassistant.util import dt as dt_util from homeassistant.util.json import load_json, save_json -_CONFIGURING = {} -_LOGGER = logging.getLogger(__name__) +from .const import ( + CONF_USE_EPISODE_ART, + CONF_SHOW_ALL_CONTROLS, + CONF_REMOVE_UNAVAILABLE_CLIENTS, + CONF_CLIENT_REMOVE_INTERVAL, + DOMAIN as PLEX_DOMAIN, + NAME_FORMAT, + PLEX_CONFIG_FILE, +) +from .server import PlexServer -NAME_FORMAT = "Plex {}" -PLEX_CONFIG_FILE = "plex.conf" -PLEX_DATA = "plex" +SERVER_SETUP = "server_setup" -CONF_USE_EPISODE_ART = "use_episode_art" -CONF_SHOW_ALL_CONTROLS = "show_all_controls" -CONF_REMOVE_UNAVAILABLE_CLIENTS = "remove_unavailable_clients" -CONF_CLIENT_REMOVE_INTERVAL = "client_remove_interval" +_CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -58,8 +64,10 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the Plex platform.""" - if PLEX_DATA not in hass.data: - hass.data[PLEX_DATA] = {} + plex_data = hass.data.setdefault(PLEX_DOMAIN, {}) + server_setup = plex_data.setdefault(SERVER_SETUP, False) + if server_setup: + return # get config from plex.conf file_config = load_json(hass.config.path(PLEX_CONFIG_FILE)) @@ -102,20 +110,19 @@ def setup_plexserver( host, token, has_ssl, verify_ssl, hass, config, add_entities_callback ): """Set up a plexserver based on host parameter.""" - import plexapi.server import plexapi.exceptions - cert_session = None http_prefix = "https" if has_ssl else "http" - if has_ssl and (verify_ssl is False): - _LOGGER.info("Ignoring SSL verification") - cert_session = requests.Session() - cert_session.verify = False + + server_config = { + CONF_URL: f"{http_prefix}://{host}", + CONF_TOKEN: token, + CONF_VERIFY_SSL: verify_ssl, + } + try: - plexserver = plexapi.server.PlexServer( - f"{http_prefix}://{host}", token, cert_session - ) - _LOGGER.info("Discovery configuration done (no token needed)") + plexserver = PlexServer(server_config) + plexserver.connect() except ( plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized, @@ -125,6 +132,8 @@ def setup_plexserver( # No token or wrong token request_configuration(host, hass, config, add_entities_callback) return + else: + hass.data[PLEX_DOMAIN][SERVER_SETUP] = True # If we came here and configuring this host, mark as done if host in _CONFIGURING: @@ -139,9 +148,7 @@ def setup_plexserver( {host: {"token": token, "ssl": has_ssl, "verify": verify_ssl}}, ) - _LOGGER.info("Connected to: %s://%s", http_prefix, host) - - plex_clients = hass.data[PLEX_DATA] + plex_clients = {} plex_sessions = {} track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10)) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index d900b4de87c1d7..d18e9506837692 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,32 +1,30 @@ """Support for Plex media server monitoring.""" from datetime import timedelta import logging +import plexapi.exceptions +import requests.exceptions import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import ( CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_SSL, + CONF_URL, CONF_VERIFY_SSL, ) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) - -CONF_SERVER = "server" +from .const import DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL +from .server import PlexServer DEFAULT_HOST = "localhost" DEFAULT_NAME = "Plex" -DEFAULT_PORT = 32400 -DEFAULT_SSL = False -DEFAULT_VERIFY_SSL = True +_LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) @@ -34,11 +32,8 @@ { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_TOKEN): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SERVER): cv.string, - vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, } @@ -48,34 +43,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Plex sensor.""" name = config.get(CONF_NAME) - plex_user = config.get(CONF_USERNAME) - plex_password = config.get(CONF_PASSWORD) - plex_server = config.get(CONF_SERVER) plex_host = config.get(CONF_HOST) plex_port = config.get(CONF_PORT) plex_token = config.get(CONF_TOKEN) + verify_ssl = config.get(CONF_VERIFY_SSL) plex_url = "{}://{}:{}".format( "https" if config.get(CONF_SSL) else "http", plex_host, plex_port ) - import plexapi.exceptions - try: - add_entities( - [ - PlexSensor( - name, - plex_url, - plex_user, - plex_password, - plex_server, - plex_token, - config.get(CONF_VERIFY_SSL), - ) - ], - True, + plex_server = PlexServer( + {CONF_URL: plex_url, CONF_TOKEN: plex_token, CONF_VERIFY_SSL: verify_ssl} ) + plex_server.connect() except ( plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized, @@ -84,43 +65,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error(error) return + add_entities([PlexSensor(name, plex_server)], True) + class PlexSensor(Entity): """Representation of a Plex now playing sensor.""" - def __init__( - self, - name, - plex_url, - plex_user, - plex_password, - plex_server, - plex_token, - verify_ssl, - ): + def __init__(self, name, plex_server): """Initialize the sensor.""" - from plexapi.myplex import MyPlexAccount - from plexapi.server import PlexServer - from requests import Session - self._name = name - self._state = 0 + self._state = None self._now_playing = [] - - cert_session = None - if not verify_ssl: - _LOGGER.info("Ignoring SSL verification") - cert_session = Session() - cert_session.verify = False - - if plex_token: - self._server = PlexServer(plex_url, plex_token, cert_session) - elif plex_user and plex_password: - user = MyPlexAccount(plex_user, plex_password) - server = plex_server if plex_server else user.resources()[0].name - self._server = user.resource(server).connect() - else: - self._server = PlexServer(plex_url, None, cert_session) + self._server = plex_server @property def name(self): @@ -145,7 +101,19 @@ def device_state_attributes(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update method for Plex sensor.""" - sessions = self._server.sessions() + try: + sessions = self._server.sessions() + except plexapi.exceptions.BadRequest: + _LOGGER.error( + "Error listing current Plex sessions on %s", self._server.friendly_name + ) + return + except requests.exceptions.RequestException as ex: + _LOGGER.warning( + "Temporary error connecting to %s (%s)", self._server.friendly_name, ex + ) + return + now_playing = [] for sess in sessions: user = sess.usernames[0] diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py new file mode 100644 index 00000000000000..6647b81714f3d5 --- /dev/null +++ b/homeassistant/components/plex/server.py @@ -0,0 +1,59 @@ +"""Shared class to maintain Plex server instances.""" +import logging +import plexapi.server +from requests import Session + +from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL + +from .const import DEFAULT_VERIFY_SSL + +_LOGGER = logging.getLogger(__package__) + + +class PlexServer: + """Manages a single Plex server connection.""" + + def __init__(self, server_config): + """Initialize a Plex server instance.""" + self._plex_server = None + self._url = server_config.get(CONF_URL) + self._token = server_config.get(CONF_TOKEN) + self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) + + def connect(self): + """Connect to a Plex server directly, obtaining direct URL if necessary.""" + + def _connect_with_url(): + session = None + if self._url.startswith("https") and not self._verify_ssl: + session = Session() + session.verify = False + self._plex_server = plexapi.server.PlexServer( + self._url, self._token, session + ) + _LOGGER.debug("Connected to: %s (%s)", self.friendly_name, self.url_in_use) + + _connect_with_url() + + def clients(self): + """Pass through clients call to plexapi.""" + return self._plex_server.clients() + + def sessions(self): + """Pass through sessions call to plexapi.""" + return self._plex_server.sessions() + + @property + def friendly_name(self): + """Return name of connected Plex server.""" + return self._plex_server.friendlyName + + @property + def machine_identifier(self): + """Return unique identifier of connected Plex server.""" + return self._plex_server.machineIdentifier + + @property + def url_in_use(self): + """Return URL used for connected Plex server.""" + return self._plex_server._baseurl # pylint: disable=W0212 From 23fdc0455436cc8bb1fade00ac2e2635e1eadfe6 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 5 Sep 2019 15:20:58 -0500 Subject: [PATCH 0187/3953] Add plex server config options to media_player platform (#26458) * Add server config options to media_player platform * Unnecessary else * Default host * No need to try for default values * Use const --- homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/media_player.py | 19 ++++++++++++++++++- homeassistant/components/plex/sensor.py | 3 +-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 1c93ff24c400ff..4495b9a8c838ba 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -2,6 +2,7 @@ DOMAIN = "plex" NAME_FORMAT = "Plex {}" +DEFAULT_HOST = "localhost" DEFAULT_PORT = 32400 DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 5b427e6a353fbc..6005321310d79a 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -20,6 +20,9 @@ SUPPORT_VOLUME_SET, ) from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_SSL, CONF_URL, CONF_TOKEN, CONF_VERIFY_SSL, @@ -39,6 +42,10 @@ CONF_SHOW_ALL_CONTROLS, CONF_REMOVE_UNAVAILABLE_CLIENTS, CONF_CLIENT_REMOVE_INTERVAL, + DEFAULT_HOST, + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, PLEX_CONFIG_FILE, @@ -52,6 +59,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_TOKEN): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean, vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): cv.boolean, @@ -99,7 +111,12 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): has_ssl = False verify_ssl = True else: - return + host = config[CONF_HOST] + port = config[CONF_PORT] + host = f"{host}:{port}" + token = config.get(CONF_TOKEN) + has_ssl = config[CONF_SSL] + verify_ssl = config[CONF_VERIFY_SSL] setup_plexserver( host, token, has_ssl, verify_ssl, hass, config, add_entities_callback diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index d18e9506837692..bece6274af69dc 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -19,10 +19,9 @@ from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -from .const import DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL +from .const import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL from .server import PlexServer -DEFAULT_HOST = "localhost" DEFAULT_NAME = "Plex" _LOGGER = logging.getLogger(__name__) From b1c2a5fa08b5da302d445ca41e2d3b5c84c18150 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 6 Sep 2019 01:26:22 +0200 Subject: [PATCH 0188/3953] Add device automation action (#26455) * Add support for device actions, with light as example. * Add translation; return list --- .../components/device_automation/__init__.py | 17 +++ .../components/device_automation/const.py | 1 + .../components/light/device_automation.py | 67 ++++++++++- homeassistant/components/light/strings.json | 5 + homeassistant/helpers/config_validation.py | 7 ++ homeassistant/helpers/script.py | 19 ++- .../components/device_automation/test_init.py | 47 ++++++++ .../light/test_device_automation.py | 113 ++++++++++++++++++ 8 files changed, 274 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index cc0e8c25979642..9508dd9c849a93 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -20,6 +20,9 @@ async def async_setup(hass, config): """Set up device automation.""" + hass.components.websocket_api.async_register_command( + websocket_device_automation_list_actions + ) hass.components.websocket_api.async_register_command( websocket_device_automation_list_conditions ) @@ -93,6 +96,20 @@ async def _async_get_device_automations(hass, fname, device_id): return automations +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/action/list", + vol.Required("device_id"): str, + } +) +async def websocket_device_automation_list_actions(hass, connection, msg): + """Handle request for device actions.""" + device_id = msg["device_id"] + actions = await _async_get_device_automations(hass, "async_get_actions", device_id) + connection.send_result(msg["id"], actions) + + @websocket_api.async_response @websocket_api.websocket_command( { diff --git a/homeassistant/components/device_automation/const.py b/homeassistant/components/device_automation/const.py index b846718e96e7df..a668c78598a289 100644 --- a/homeassistant/components/device_automation/const.py +++ b/homeassistant/components/device_automation/const.py @@ -1,5 +1,6 @@ """Constants for device automations.""" CONF_IS_OFF = "is_off" CONF_IS_ON = "is_on" +CONF_TOGGLE = "toggle" CONF_TURN_OFF = "turn_off" CONF_TURN_ON = "turn_on" diff --git a/homeassistant/components/light/device_automation.py b/homeassistant/components/light/device_automation.py index 66bad135659b3c..4ddba8b9423dde 100644 --- a/homeassistant/components/light/device_automation.py +++ b/homeassistant/components/light/device_automation.py @@ -5,25 +5,48 @@ from homeassistant.components.device_automation.const import ( CONF_IS_OFF, CONF_IS_ON, + CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON, ) from homeassistant.core import split_entity_id from homeassistant.const import ( CONF_CONDITION, + CONF_DEVICE, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_PLATFORM, CONF_TYPE, ) -from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers import condition, config_validation as cv, service from homeassistant.helpers.entity_registry import async_entries_for_device from . import DOMAIN # mypy: allow-untyped-defs, no-check-untyped-defs +ENTITY_ACTIONS = [ + { + # Turn light off + CONF_DEVICE: None, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: CONF_TURN_OFF, + }, + { + # Turn light on + CONF_DEVICE: None, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: CONF_TURN_ON, + }, + { + # Toggle light + CONF_DEVICE: None, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: CONF_TOGGLE, + }, +] + ENTITY_CONDITIONS = [ { # True when light is turned off @@ -54,6 +77,18 @@ }, ] +ACTION_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_DEVICE): None, + vol.Optional(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]), + } + ) +) + CONDITION_SCHEMA = vol.All( vol.Schema( { @@ -83,6 +118,31 @@ def _is_domain(entity, domain): return split_entity_id(entity.entity_id)[0] == domain +async def async_action_from_config(hass, config, variables, context): + """Change state based on configuration.""" + config = ACTION_SCHEMA(config) + action_type = config[CONF_TYPE] + if action_type == CONF_TURN_ON: + action = "turn_on" + elif action_type == CONF_TURN_OFF: + action = "turn_off" + else: + action = "toggle" + service_action = { + service.CONF_SERVICE: "light.{}".format(action), + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + } + + await service.async_call_from_config( + hass, + service_action, + blocking=True, + variables=variables, + # validate_config=False, + context=context, + ) + + def async_condition_from_config(config, config_validation): """Evaluate state based on configuration.""" config = CONDITION_SCHEMA(config) @@ -140,6 +200,11 @@ async def _async_get_automations(hass, device_id, automation_templates): return automations +async def async_get_actions(hass, device_id): + """List device actions.""" + return await _async_get_automations(hass, device_id, ENTITY_ACTIONS) + + async def async_get_conditions(hass, device_id): """List device conditions.""" return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS) diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 6f002d9ed8cbea..6b9e5d7d6493fe 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "toggle": "Toggle {name}", + "turn_on": "Turn on {name}", + "turn_off": "Turn off {name}" + }, "condition_type": { "is_on": "{name} is on", "is_off": "{name} is off" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 3aa17befd484d0..743a848ff93818 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -24,6 +24,7 @@ CONF_ALIAS, CONF_BELOW, CONF_CONDITION, + CONF_DEVICE, CONF_DOMAIN, CONF_ENTITY_ID, CONF_ENTITY_NAMESPACE, @@ -861,6 +862,11 @@ def validator(value): } ) +DEVICE_ACTION_SCHEMA = vol.Schema( + {vol.Required(CONF_DEVICE): None, vol.Required(CONF_DOMAIN): str}, + extra=vol.ALLOW_EXTRA, +) + SCRIPT_SCHEMA = vol.All( ensure_list, [ @@ -870,6 +876,7 @@ def validator(value): _SCRIPT_WAIT_TEMPLATE_SCHEMA, EVENT_SCHEMA, CONDITION_SCHEMA, + DEVICE_ACTION_SCHEMA, ) ], ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index da173efcba6b9c..1d8f915543f47b 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE -from homeassistant.const import CONF_CONDITION, CONF_TIMEOUT +from homeassistant.const import CONF_CONDITION, CONF_DEVICE, CONF_DOMAIN, CONF_TIMEOUT from homeassistant import exceptions from homeassistant.helpers import ( service, @@ -22,6 +22,7 @@ async_track_template, ) from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import async_get_integration import homeassistant.util.dt as date_util from homeassistant.util.async_ import run_coroutine_threadsafe, run_callback_threadsafe @@ -48,6 +49,7 @@ ACTION_CHECK_CONDITION = "condition" ACTION_FIRE_EVENT = "event" ACTION_CALL_SERVICE = "call_service" +ACTION_DEVICE_AUTOMATION = "device" def _determine_action(action): @@ -64,6 +66,9 @@ def _determine_action(action): if CONF_EVENT in action: return ACTION_FIRE_EVENT + if CONF_DEVICE in action: + return ACTION_DEVICE_AUTOMATION + return ACTION_CALL_SERVICE @@ -117,6 +122,7 @@ def __init__( ACTION_CHECK_CONDITION: self._async_check_condition, ACTION_FIRE_EVENT: self._async_fire_event, ACTION_CALL_SERVICE: self._async_call_service, + ACTION_DEVICE_AUTOMATION: self._async_device_automation, } @property @@ -318,6 +324,17 @@ async def _async_call_service(self, action, variables, context): context=context, ) + async def _async_device_automation(self, action, variables, context): + """Perform the device automation specified in the action. + + This method is a coroutine. + """ + self.last_action = action.get(CONF_ALIAS, "device automation") + self._log("Executing step %s" % self.last_action) + integration = await async_get_integration(self.hass, action[CONF_DOMAIN]) + platform = integration.get_platform("device_automation") + await platform.async_action_from_config(self.hass, action, variables, context) + async def _async_fire_event(self, action, variables, context): """Fire an event.""" self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT]) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 4bcb68f119b80f..b084c64182b455 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -31,6 +31,53 @@ def _same_lists(a, b): return True +async def test_websocket_get_actions(hass, hass_ws_client, device_reg, entity_reg): + """Test we get the expected conditions from a light through websocket.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "device": None, + "domain": "light", + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": "light.test_5678", + }, + { + "device": None, + "domain": "light", + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": "light.test_5678", + }, + { + "device": None, + "domain": "light", + "type": "toggle", + "device_id": device_entry.id, + "entity_id": "light.test_5678", + }, + ] + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 1, "type": "device_automation/action/list", "device_id": device_entry.id} + ) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + actions = msg["result"] + assert _same_lists(actions, expected_actions) + + async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity_reg): """Test we get the expected conditions from a light through websocket.""" await async_setup_component(hass, "device_automation", {}) diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py index 0e356ae13aa0ab..38c573b514fb78 100644 --- a/tests/components/light/test_device_automation.py +++ b/tests/components/light/test_device_automation.py @@ -46,6 +46,44 @@ def _same_lists(a, b): return True +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a light.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "device": None, + "domain": "light", + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": "light.test_5678", + }, + { + "device": None, + "domain": "light", + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": "light.test_5678", + }, + { + "device": None, + "domain": "light", + "type": "toggle", + "device_id": device_entry.id, + "entity_id": "light.test_5678", + }, + ] + actions = await async_get_device_automations( + hass, "async_get_actions", device_entry.id + ) + assert _same_lists(actions, expected_actions) + + async def test_get_conditions(hass, device_reg, entity_reg): """Test we get the expected conditions from a light.""" config_entry = MockConfigEntry(domain="test", data={}) @@ -263,3 +301,78 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_action(hass, calls): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, "test.light") + + platform.init() + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + + dev1, dev2, dev3 = platform.DEVICES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": { + "device": None, + "domain": "light", + "entity_id": dev1.entity_id, + "type": "turn_off", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": { + "device": None, + "domain": "light", + "entity_id": dev1.entity_id, + "type": "turn_on", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "action": { + "device": None, + "domain": "light", + "entity_id": dev1.entity_id, + "type": "toggle", + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_ON From 518d2c31bb2b68d4842cd24aa15b2ba7e412851e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 6 Sep 2019 01:38:00 +0200 Subject: [PATCH 0189/3953] deCONZ - use entity registry disabled_by to control available entities (#26219) * First draft * Support enabling disabled entities * Clean up * Move import * Local entity enabled replaced during rebase * Add option flow test * Mark options properties with option --- homeassistant/components/deconz/__init__.py | 30 +++--- .../components/deconz/binary_sensor.py | 15 +-- homeassistant/components/deconz/climate.py | 7 +- .../components/deconz/config_flow.py | 13 ++- homeassistant/components/deconz/const.py | 2 +- homeassistant/components/deconz/cover.py | 2 +- .../components/deconz/deconz_device.py | 27 +++++- homeassistant/components/deconz/gateway.py | 95 +++++++++++++++---- homeassistant/components/deconz/light.py | 14 ++- homeassistant/components/deconz/scene.py | 3 +- homeassistant/components/deconz/sensor.py | 14 +-- homeassistant/components/deconz/switch.py | 2 +- tests/components/deconz/test_binary_sensor.py | 4 +- tests/components/deconz/test_climate.py | 4 +- tests/components/deconz/test_config_flow.py | 26 +++++ tests/components/deconz/test_cover.py | 2 +- tests/components/deconz/test_light.py | 7 +- tests/components/deconz/test_sensor.py | 4 +- tests/components/deconz/test_switch.py | 2 +- 19 files changed, 191 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 68974d12253f6e..56663c6b2dab0c 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -12,15 +12,7 @@ # Loading the config flow file will register the flow from .config_flow import get_master_gateway -from .const import ( - CONF_ALLOW_CLIP_SENSOR, - CONF_ALLOW_DECONZ_GROUPS, - CONF_BRIDGEID, - CONF_MASTER_GATEWAY, - DEFAULT_PORT, - DOMAIN, - _LOGGER, -) +from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN, _LOGGER from .gateway import DeconzGateway CONFIG_SCHEMA = vol.Schema( @@ -86,7 +78,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = {} if not config_entry.options: - await async_populate_options(hass, config_entry) + await async_update_master_gateway(hass, config_entry) gateway = DeconzGateway(hass, config_entry) @@ -203,25 +195,25 @@ async def async_unload_entry(hass, config_entry): hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) elif gateway.master: - await async_populate_options(hass, config_entry) + await async_update_master_gateway(hass, config_entry) new_master_gateway = next(iter(hass.data[DOMAIN].values())) - await async_populate_options(hass, new_master_gateway.config_entry) + await async_update_master_gateway(hass, new_master_gateway.config_entry) return await gateway.async_reset() -async def async_populate_options(hass, config_entry): - """Populate default options for gateway. +async def async_update_master_gateway(hass, config_entry): + """Update master gateway boolean. Called by setup_entry and unload_entry. Makes sure there is always one master available. """ master = not get_master_gateway(hass) - options = { - CONF_MASTER_GATEWAY: master, - CONF_ALLOW_CLIP_SENSOR: config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, False), - CONF_ALLOW_DECONZ_GROUPS: config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True), - } + old_options = dict(config_entry.options) + + new_options = {CONF_MASTER_GATEWAY: master} + + options = {**old_options, **new_options} hass.config_entries.async_update_entry(config_entry, options=options) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 0b5d3173812ba7..492b16a603a5bb 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -8,7 +8,7 @@ from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry, DeconzEntityHandler ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" @@ -24,6 +24,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ binary sensor.""" gateway = get_gateway_from_config_entry(hass, config_entry) + entity_handler = DeconzEntityHandler(gateway) + @callback def async_add_sensor(sensors): """Add binary sensor from deCONZ.""" @@ -31,17 +33,16 @@ def async_add_sensor(sensors): for sensor in sensors: - if sensor.BINARY and not ( - not gateway.allow_clip_sensor and sensor.type.startswith("CLIP") - ): - - entities.append(DeconzBinarySensor(sensor, gateway)) + if sensor.BINARY: + new_sensor = DeconzBinarySensor(sensor, gateway) + entity_handler.add_entity(new_sensor) + entities.append(new_sensor) async_add_entities(entities, True) gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor + hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor ) ) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index a72c29019598e7..1844cb2c97c73f 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -38,17 +38,14 @@ def async_add_climate(sensors): for sensor in sensors: - if sensor.type in Thermostat.ZHATYPE and not ( - not gateway.allow_clip_sensor and sensor.type.startswith("CLIP") - ): - + if sensor.type in Thermostat.ZHATYPE: entities.append(DeconzThermostat(sensor, gateway)) async_add_entities(entities, True) gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SENSOR), async_add_climate + hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_climate ) ) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 60d47a0a4e22b6..12e2e092f67848 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,6 +1,5 @@ """Config flow to configure deCONZ component.""" import asyncio -from copy import copy import async_timeout import voluptuous as vol @@ -17,6 +16,8 @@ CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, + DEFAULT_ALLOW_CLIP_SENSOR, + DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, DOMAIN, ) @@ -256,7 +257,7 @@ class DeconzOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry): """Initialize deCONZ options flow.""" self.config_entry = config_entry - self.options = copy(config_entry.options) + self.options = dict(config_entry.options) async def async_step_init(self, user_input=None): """Manage the deCONZ options.""" @@ -277,11 +278,15 @@ async def async_step_deconz_devices(self, user_input=None): { vol.Optional( CONF_ALLOW_CLIP_SENSOR, - default=self.config_entry.options[CONF_ALLOW_CLIP_SENSOR], + default=self.config_entry.options.get( + CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR + ), ): bool, vol.Optional( CONF_ALLOW_DECONZ_GROUPS, - default=self.config_entry.options[CONF_ALLOW_DECONZ_GROUPS], + default=self.config_entry.options.get( + CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS + ), ): bool, } ), diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index ef152aa2b708f0..62879a82724b03 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -7,7 +7,7 @@ DEFAULT_PORT = 80 DEFAULT_ALLOW_CLIP_SENSOR = False -DEFAULT_ALLOW_DECONZ_GROUPS = False +DEFAULT_ALLOW_DECONZ_GROUPS = True CONF_ALLOW_CLIP_SENSOR = "allow_clip_sensor" CONF_ALLOW_DECONZ_GROUPS = "allow_deconz_groups" diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index be4088a5c86592..b82144d37c739a 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -40,7 +40,7 @@ def async_add_cover(lights): gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_LIGHT), async_add_cover + hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_cover ) ) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 389ed11e437126..ad621db86ce730 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -14,21 +14,40 @@ def __init__(self, device, gateway): """Set up device and add update callback to get data from websocket.""" self._device = device self.gateway = gateway - self.unsub_dispatcher = None + self.listeners = [] + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + if not self.gateway.option_allow_clip_sensor and self._device.type.startswith( + "CLIP" + ): + return False + + if ( + not self.gateway.option_allow_deconz_groups + and self._device.type == "LightGroup" + ): + return False + + return True async def async_added_to_hass(self): """Subscribe to device events.""" self._device.register_async_callback(self.async_update_callback) self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, self.gateway.event_reachable, self.async_update_callback + self.listeners.append( + async_dispatcher_connect( + self.hass, self.gateway.signal_reachable, self.async_update_callback + ) ) async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self._device.remove_callback(self.async_update_callback) del self.gateway.deconz_ids[self.entity_id] - self.unsub_dispatcher() + for unsub_dispatcher in self.listeners: + unsub_dispatcher() @callback def async_update_callback(self, force_update=False): diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 2117f8dc6bb3cf..73cdeb74884b4e 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -14,6 +14,10 @@ async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.entity_registry import ( + async_get_registry, + DISABLED_CONFIG_ENTRY, +) from homeassistant.util import slugify from .const import ( @@ -22,6 +26,8 @@ CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, CONF_MASTER_GATEWAY, + DEFAULT_ALLOW_CLIP_SENSOR, + DEFAULT_ALLOW_DECONZ_GROUPS, DOMAIN, NEW_DEVICE, NEW_SENSOR, @@ -61,14 +67,18 @@ def master(self) -> bool: return self.config_entry.options[CONF_MASTER_GATEWAY] @property - def allow_clip_sensor(self) -> bool: + def option_allow_clip_sensor(self) -> bool: """Allow loading clip sensor from gateway.""" - return self.config_entry.options.get(CONF_ALLOW_CLIP_SENSOR, True) + return self.config_entry.options.get( + CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR + ) @property - def allow_deconz_groups(self) -> bool: + def option_allow_deconz_groups(self) -> bool: """Allow loading deCONZ groups from gateway.""" - return self.config_entry.options.get(CONF_ALLOW_DECONZ_GROUPS, True) + return self.config_entry.options.get( + CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS + ) async def async_update_device_registry(self): """Update device registry.""" @@ -111,7 +121,7 @@ async def async_setup(self): self.listeners.append( async_dispatcher_connect( - hass, self.async_event_new_device(NEW_SENSOR), self.async_add_remote + hass, self.async_signal_new_device(NEW_SENSOR), self.async_add_remote ) ) @@ -119,35 +129,50 @@ async def async_setup(self): self.api.start() - self.config_entry.add_update_listener(self.async_new_address_callback) + self.config_entry.add_update_listener(self.async_new_address) + self.config_entry.add_update_listener(self.async_options_updated) return True @staticmethod - async def async_new_address_callback(hass, entry): + async def async_new_address(hass, entry): """Handle signals of gateway getting new address. This is a static method because a class method (bound method), can not be used with weak references. """ - gateway = hass.data[DOMAIN][entry.data[CONF_BRIDGEID]] - gateway.api.close() - gateway.api.host = entry.data[CONF_HOST] - gateway.api.start() + gateway = get_gateway_from_config_entry(hass, entry) + if gateway.api.host != entry.data[CONF_HOST]: + gateway.api.close() + gateway.api.host = entry.data[CONF_HOST] + gateway.api.start() @property - def event_reachable(self): + def signal_reachable(self): """Gateway specific event to signal a change in connection status.""" - return f"deconz_reachable_{self.bridgeid}" + return f"deconz-reachable-{self.bridgeid}" @callback def async_connection_status_callback(self, available): """Handle signals of gateway connection status.""" self.available = available - async_dispatcher_send(self.hass, self.event_reachable, True) + async_dispatcher_send(self.hass, self.signal_reachable, True) + + @property + def signal_options_update(self): + """Event specific per deCONZ entry to signal new options.""" + return f"deconz-options-{self.bridgeid}" + + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + gateway = get_gateway_from_config_entry(hass, entry) + + registry = await async_get_registry(hass) + async_dispatcher_send(hass, gateway.signal_options_update, registry) @callback - def async_event_new_device(self, device_type): + def async_signal_new_device(self, device_type): """Gateway specific event to signal new device.""" return NEW_DEVICE[device_type].format(self.bridgeid) @@ -157,7 +182,7 @@ def async_add_device_callback(self, device_type, device): if not isinstance(device, list): device = [device] async_dispatcher_send( - self.hass, self.async_event_new_device(device_type), device + self.hass, self.async_signal_new_device(device_type), device ) @callback @@ -165,7 +190,7 @@ def async_add_remote(self, sensors): """Set up remote from deCONZ.""" for sensor in sensors: if sensor.type in Switch.ZHATYPE and not ( - not self.allow_clip_sensor and sensor.type.startswith("CLIP") + not self.option_allow_clip_sensor and sensor.type.startswith("CLIP") ): self.events.append(DeconzEvent(self.hass, sensor)) @@ -183,6 +208,7 @@ async def async_reset(self): Will cancel any scheduled setup retry and will unload the config entry. """ + self.api.async_connection_status_callback = None self.api.close() for component in SUPPORTED_PLATFORMS: @@ -229,6 +255,41 @@ async def get_gateway( raise CannotConnect +class DeconzEntityHandler: + """Platform entity handler to help with updating disabled by.""" + + def __init__(self, gateway): + """Create an entity handler.""" + self.gateway = gateway + self._entities = [] + + gateway.listeners.append( + async_dispatcher_connect( + gateway.hass, gateway.signal_options_update, self.update_entity_registry + ) + ) + + @callback + def add_entity(self, entity): + """Add a new entity to handler.""" + self._entities.append(entity) + + @callback + def update_entity_registry(self, entity_registry): + """Update entity registry disabled by status.""" + for entity in self._entities: + + if entity.entity_registry_enabled_default != entity.enabled: + disabled_by = None + + if entity.enabled: + disabled_by = DISABLED_CONFIG_ENTRY + + entity_registry.async_update_entity( + entity.registry_entry.entity_id, disabled_by=disabled_by + ) + + class DeconzEvent: """When you want signals instead of entities. diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index b68aa6f07796d6..ec1dfd2bcb1901 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -29,7 +29,7 @@ SWITCH_TYPES, ) from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry, DeconzEntityHandler async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -41,6 +41,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ lights and groups from a config entry.""" gateway = get_gateway_from_config_entry(hass, config_entry) + entity_handler = DeconzEntityHandler(gateway) + @callback def async_add_light(lights): """Add light from deCONZ.""" @@ -54,7 +56,7 @@ def async_add_light(lights): gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_LIGHT), async_add_light + hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_light ) ) @@ -64,14 +66,16 @@ def async_add_group(groups): entities = [] for group in groups: - if group.lights and gateway.allow_deconz_groups: - entities.append(DeconzGroup(group, gateway)) + if group.lights: + new_group = DeconzGroup(group, gateway) + entity_handler.add_entity(new_group) + entities.append(new_group) async_add_entities(entities, True) gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_GROUP), async_add_group + hass, gateway.async_signal_new_device(NEW_GROUP), async_add_group ) ) diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index ede60e3ef453ed..8d27d386da266b 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -28,7 +28,7 @@ def async_add_scene(scenes): gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SCENE), async_add_scene + hass, gateway.async_signal_new_device(NEW_SCENE), async_add_scene ) ) @@ -49,6 +49,7 @@ async def async_added_to_hass(self): async def async_will_remove_from_hass(self) -> None: """Disconnect scene object when removed.""" + del self.gateway.deconz_ids[self.entity_id] self._scene = None async def async_activate(self): diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index dad3c25cc38d0a..d84a47c6aaf8e3 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -13,7 +13,7 @@ from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry, DeconzEntityHandler ATTR_CURRENT = "current" ATTR_POWER = "power" @@ -30,6 +30,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ sensors.""" gateway = get_gateway_from_config_entry(hass, config_entry) + entity_handler = DeconzEntityHandler(gateway) + @callback def async_add_sensor(sensors): """Add sensors from deCONZ.""" @@ -37,22 +39,22 @@ def async_add_sensor(sensors): for sensor in sensors: - if not sensor.BINARY and not ( - not gateway.allow_clip_sensor and sensor.type.startswith("CLIP") - ): + if not sensor.BINARY: if sensor.type in Switch.ZHATYPE: if sensor.battery: entities.append(DeconzBattery(sensor, gateway)) else: - entities.append(DeconzSensor(sensor, gateway)) + new_sensor = DeconzSensor(sensor, gateway) + entity_handler.add_entity(new_sensor) + entities.append(new_sensor) async_add_entities(entities, True) gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor + hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor ) ) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 7ce40789802f3d..b1fd4b10f46065 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -37,7 +37,7 @@ def async_add_switch(lights): gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_LIGHT), async_add_switch + hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_switch ) ) diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index acf06728d0d0d5..b6745e1a971312 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -118,7 +118,7 @@ async def test_add_new_sensor(hass): sensor.BINARY = True sensor.uniqueid = "1" sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) + async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) await hass.async_block_till_done() assert "binary_sensor.name" in gateway.deconz_ids @@ -131,7 +131,7 @@ async def test_do_not_allow_clip_sensor(hass): sensor.name = "name" sensor.type = "CLIPPresence" sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) + async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) await hass.async_block_till_done() assert len(gateway.deconz_ids) == 0 diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 1547f58a12b1bc..b76b3511a090bc 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -191,7 +191,7 @@ async def test_add_new_climate_device(hass): sensor.type = "ZHAThermostat" sensor.uniqueid = "1" sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) + async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) await hass.async_block_till_done() assert "climate.name" in gateway.deconz_ids @@ -203,7 +203,7 @@ async def test_do_not_allow_clipsensor(hass): sensor.name = "name" sensor.type = "CLIPThermostat" sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) + async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) await hass.async_block_till_done() assert len(gateway.deconz_ids) == 0 diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index ea3abead02870e..3f00c31c7e8746 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -382,3 +382,29 @@ async def test_hassio_confirm(hass): config_flow.CONF_BRIDGEID: "id", config_flow.CONF_API_KEY: "1234567890ABCDEF", } + + +async def test_option_flow(hass): + """Test config flow selection of one of two bridges.""" + entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None) + hass.config_entries._entries.append(entry) + + flow = await hass.config_entries.options._async_create_flow( + entry.entry_id, context={"source": "test"}, data=None + ) + + result = await flow.async_step_init() + assert result["type"] == "form" + assert result["step_id"] == "deconz_devices" + + result = await flow.async_step_deconz_devices( + user_input={ + config_flow.CONF_ALLOW_CLIP_SENSOR: False, + config_flow.CONF_ALLOW_DECONZ_GROUPS: False, + } + ) + assert result["type"] == "create_entry" + assert result["data"] == { + config_flow.CONF_ALLOW_CLIP_SENSOR: False, + config_flow.CONF_ALLOW_DECONZ_GROUPS: False, + } diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 7230ff4fb7bdff..2de70f6d247c30 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -135,7 +135,7 @@ async def test_add_new_cover(hass): cover.type = "Level controllable output" cover.uniqueid = "1" cover.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_event_new_device("light"), [cover]) + async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [cover]) await hass.async_block_till_done() assert "cover.name" in gateway.deconz_ids diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index afe7ca445e5738..ecce762f51c4f3 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -193,7 +193,7 @@ async def test_add_new_light(hass): light.name = "name" light.uniqueid = "1" light.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_event_new_device("light"), [light]) + async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [light]) await hass.async_block_till_done() assert "light.name" in gateway.deconz_ids @@ -204,7 +204,7 @@ async def test_add_new_group(hass): group = Mock() group.name = "name" group.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_event_new_device("group"), [group]) + async_dispatcher_send(hass, gateway.async_signal_new_device("group"), [group]) await hass.async_block_till_done() assert "light.name" in gateway.deconz_ids @@ -214,8 +214,9 @@ async def test_do_not_add_deconz_groups(hass): gateway = await setup_gateway(hass, {}, allow_deconz_groups=False) group = Mock() group.name = "name" + group.type = "LightGroup" group.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_event_new_device("group"), [group]) + async_dispatcher_send(hass, gateway.async_signal_new_device("group"), [group]) await hass.async_block_till_done() assert len(gateway.deconz_ids) == 0 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index fa1ba175ed5762..eb391cc563d378 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -162,7 +162,7 @@ async def test_add_new_sensor(hass): sensor.uniqueid = "1" sensor.BINARY = False sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) + async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) await hass.async_block_till_done() assert "sensor.name" in gateway.deconz_ids @@ -174,7 +174,7 @@ async def test_do_not_allow_clipsensor(hass): sensor.name = "name" sensor.type = "CLIPTemperature" sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) + async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) await hass.async_block_till_done() assert len(gateway.deconz_ids) == 0 diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 746d1b6342c4f8..6b691bcab8e038 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -143,7 +143,7 @@ async def test_add_new_switch(hass): switch.type = "Smart plug" switch.uniqueid = "1" switch.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_event_new_device("light"), [switch]) + async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [switch]) await hass.async_block_till_done() assert "switch.name" in gateway.deconz_ids From 3714cdaa6dc0475b76f50a1aa6c4fab237225f32 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 6 Sep 2019 00:33:40 +0000 Subject: [PATCH 0190/3953] [ci skip] Translation update --- .../deconz/.translations/zh-Hant.json | 7 ++++++ .../components/light/.translations/en.json | 9 +++++++ .../linky/.translations/zh-Hant.json | 25 +++++++++++++++++++ .../components/met/.translations/zh-Hant.json | 2 +- 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/linky/.translations/zh-Hant.json diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 53d6f76a60161a..75dcac93dd9eb0 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -49,6 +49,13 @@ "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" }, "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", + "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" + }, + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b" } } } diff --git a/homeassistant/components/light/.translations/en.json b/homeassistant/components/light/.translations/en.json index 9e5d1abddaf486..3d5290ca6ce719 100644 --- a/homeassistant/components/light/.translations/en.json +++ b/homeassistant/components/light/.translations/en.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Toggle {name}", + "turn_off": "Turn off {name}", + "turn_on": "Turn on {name}" + }, + "condition_type": { + "is_off": "{name} is off", + "is_on": "{name} is on" + }, "trigger_type": { "turn_off": "{name} turned off", "turn_on": "{name} turned on" diff --git a/homeassistant/components/linky/.translations/zh-Hant.json b/homeassistant/components/linky/.translations/zh-Hant.json new file mode 100644 index 00000000000000..bcfac6643c8e6f --- /dev/null +++ b/homeassistant/components/linky/.translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "access": "\u7121\u6cd5\u8a2a\u554f Enedis.fr\uff0c\u8acb\u6aa2\u67e5\u60a8\u7684\u7db2\u969b\u7db2\u8def\u9023\u7dda", + "enedis": "Endis.fr \u56de\u5831\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66\uff08\u901a\u5e38\u907f\u958b\u591c\u9593 11 - \u51cc\u6668 2 \u9ede\u4e4b\u9593\uff09", + "unknown": "\u672a\u77e5\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66\uff08\u901a\u5e38\u907f\u958b\u591c\u9593 11 - \u51cc\u6668 2 \u9ede\u4e4b\u9593\uff09", + "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "wrong_login": "\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u78ba\u8a8d\u96fb\u5b50\u90f5\u4ef6\u8207\u79d8\u5bc6\u6b63\u78ba\u6027" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u8f38\u5165\u6191\u8b49", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/zh-Hant.json b/homeassistant/components/met/.translations/zh-Hant.json index c49c90ee6e422c..de7c34ffc87969 100644 --- a/homeassistant/components/met/.translations/zh-Hant.json +++ b/homeassistant/components/met/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" + "name_exists": "\u8a72\u5ea7\u6a19\u5df2\u5b58\u5728" }, "step": { "user": { From 5994f82fc5d73822bd0516d7fb340c8a293105e0 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 6 Sep 2019 05:41:57 +0200 Subject: [PATCH 0191/3953] Add person to device_sun_light_trigger (#25877) * Add person to device_sun_light_trigger * tests * fix test --- .../device_sun_light_trigger/__init__.py | 2 + .../device_sun_light_trigger/manifest.json | 3 +- .../device_sun_light_trigger/test_init.py | 64 ++++++++++++++++++- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index 1b71b44369da14..9a058cfacc10bb 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -63,12 +63,14 @@ async def async_setup(hass, config): device_tracker = hass.components.device_tracker group = hass.components.group light = hass.components.light + person = hass.components.person conf = config[DOMAIN] disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF) light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS) light_profile = conf.get(CONF_LIGHT_PROFILE) device_group = conf.get(CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES) device_entity_ids = group.get_entity_ids(device_group, device_tracker.DOMAIN) + device_entity_ids.extend(group.get_entity_ids(device_group, person.DOMAIN)) if not device_entity_ids: logger.error("No devices found to track") diff --git a/homeassistant/components/device_sun_light_trigger/manifest.json b/homeassistant/components/device_sun_light_trigger/manifest.json index abe5a1d500cb81..40ab85bc1e5fbb 100644 --- a/homeassistant/components/device_sun_light_trigger/manifest.json +++ b/homeassistant/components/device_sun_light_trigger/manifest.json @@ -6,7 +6,8 @@ "dependencies": [ "device_tracker", "group", - "light" + "light", + "person" ], "codeowners": [] } diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index dd23bf9cff6da0..70681a6d1504d4 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -6,7 +6,12 @@ from homeassistant.setup import async_setup_component from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME -from homeassistant.components import device_tracker, light, device_sun_light_trigger +from homeassistant.components import ( + device_tracker, + light, + device_sun_light_trigger, + group, +) from homeassistant.components.device_tracker.const import ( ENTITY_ID_FORMAT as DT_ENTITY_ID_FORMAT, ) @@ -90,6 +95,8 @@ async def test_lights_turn_off_when_everyone_leaves(hass, scanner): hass, device_sun_light_trigger.DOMAIN, {device_sun_light_trigger.DOMAIN: {}} ) + assert light.is_on(hass) + hass.states.async_set(device_tracker.ENTITY_ID_ALL_DEVICES, STATE_NOT_HOME) await hass.async_block_till_done() @@ -111,3 +118,58 @@ async def test_lights_turn_on_when_coming_home_after_sun_set(hass, scanner): await hass.async_block_till_done() assert light.is_on(hass) + + +async def test_lights_turn_on_when_coming_home_after_sun_set_person(hass, scanner): + """Test lights turn on when coming home after sun set.""" + device_1 = DT_ENTITY_ID_FORMAT.format("device_1") + device_2 = DT_ENTITY_ID_FORMAT.format("device_2") + + test_time = datetime(2017, 4, 5, 3, 2, 3, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=test_time): + await common_light.async_turn_off(hass) + hass.states.async_set(device_1, STATE_NOT_HOME) + hass.states.async_set(device_2, STATE_NOT_HOME) + await hass.async_block_till_done() + + assert not light.is_on(hass) + assert hass.states.get(device_tracker.ENTITY_ID_ALL_DEVICES).state == "not_home" + assert hass.states.get(device_1).state == "not_home" + assert hass.states.get(device_2).state == "not_home" + + assert await async_setup_component( + hass, + "person", + {"person": [{"id": "me", "name": "Me", "device_trackers": [device_1]}]}, + ) + + await group.Group.async_create_group(hass, "person_me", ["person.me"]) + + assert await async_setup_component( + hass, + device_sun_light_trigger.DOMAIN, + {device_sun_light_trigger.DOMAIN: {"device_group": "group.person_me"}}, + ) + + assert not light.is_on(hass) + assert hass.states.get(device_1).state == "not_home" + assert hass.states.get(device_2).state == "not_home" + assert hass.states.get("person.me").state == "not_home" + + # Unrelated device has no impact + hass.states.async_set(device_2, STATE_HOME) + await hass.async_block_till_done() + + assert not light.is_on(hass) + assert hass.states.get(device_1).state == "not_home" + assert hass.states.get(device_2).state == "home" + assert hass.states.get("person.me").state == "not_home" + + # person home switches on + hass.states.async_set(device_1, STATE_HOME) + await hass.async_block_till_done() + + assert light.is_on(hass) + assert hass.states.get(device_1).state == "home" + assert hass.states.get(device_2).state == "home" + assert hass.states.get("person.me").state == "home" From f23ab2af8c80a4c1a876e4ad7491d7e8b20760b9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Sep 2019 22:10:37 -0700 Subject: [PATCH 0192/3953] Bumped version to 0.98.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2a20917b3be4d2..db6580727eab6a 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 = "3" +PATCH_VERSION = "4" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 50cec91cf018bdb6a094bc1a993b80836543e96d Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Fri, 6 Sep 2019 11:19:31 +0200 Subject: [PATCH 0193/3953] Change darksky icon for clear night (#26452) * change icon for clear night change mdi:weather-sunny to mdi:night for condition clear night * changed erroneous mdi:night to mdi:weather-night --- homeassistant/components/darksky/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 4f000253245f1f..d4e7e7ec63a97c 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -371,7 +371,7 @@ CONDITION_PICTURES = { "clear-day": ["/static/images/darksky/weather-sunny.svg", "mdi:weather-sunny"], - "clear-night": ["/static/images/darksky/weather-night.svg", "mdi:weather-sunny"], + "clear-night": ["/static/images/darksky/weather-night.svg", "mdi:weather-night"], "rain": ["/static/images/darksky/weather-pouring.svg", "mdi:weather-pouring"], "snow": ["/static/images/darksky/weather-snowy.svg", "mdi:weather-snowy"], "sleet": ["/static/images/darksky/weather-hail.svg", "mdi:weather-snowy-rainy"], From 815e7a70e9429f4df296ad1b777434ae666d4691 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Fri, 6 Sep 2019 14:24:10 +0300 Subject: [PATCH 0194/3953] Jewish calendar binary sensor (#26200) * Move jewish calendar to its own platform * Fix tests for Jewish Calendar platform As part of this, move tests to use async_setup_component instead of testing JewishCalendarSensor as suggested by @MartinHjelmare here: https://github.com/home-assistant/home-assistant/pull/24958#pullrequestreview-259394226 * Get sensors to update during test * Use hass.config.set_time_zone instead of directly calling set_default_time_zone in tests * Cleanup log messages * Rename result from weekly_portion to parshat_hashavua * Fix english/hebrew tests * Fix updating of issue melacha binary sensor * Fix docstrings of binary sensor * Reset timezones before and after each test * Use correct entity_id for day of the omer tests * Fix omer tests * Cleanup and rearrange tests * Remove the old issur_melacha_in_effect sensor * Rename variables to make the code clearer Instead of using lagging_date, use after_tzais and after_shkia * Use dt_util.set_default_time_zone instead of hass.config.set_time_zone so as not to break other tests * Remove should_poll set to false (accidental copy/paste) * Remove _LOGGER messaging during init and impossible cases * Move binary tests to standalone test functions Move sensor tests to standalone test functions * Collect entities before calling add_entities * Fix pylint errors * Simplify logic in binary sensor until a future a PR adds more sensors * Rename test_id holyness to holiday_type * Fix time zone for binary sensor tests Fix time zone for sensor tests * Don't use unnecessary alter_time in sensors Don't use unnecessary alter time in binary sensor Remove unused alter_time * Simply set hass.config.time_zone instead of murking around with global values * Use async_fire_time_changed instead of directly calling async_update_entity * Removing debug messaging during init of integration * Capitalize constants * Collect all Entities before calling async_add_entities * Revert "Don't use unnecessary alter_time in sensors" This reverts commit 74371740eaeb6e73c1a374725b05207071648ee1. * Use test time instead of utc_now * Remove superfluous testing * Fix triggering of time changed * Fix failing tests due to side-effects * Use dt_util.as_utc instead of reimplementing it's functionality * Use dict[key] for default values * Move 3rd party imports to the top of the module * Fix imports --- .../components/jewish_calendar/__init__.py | 108 ++ .../jewish_calendar/binary_sensor.py | 66 + .../components/jewish_calendar/sensor.py | 218 +-- tests/components/jewish_calendar/__init__.py | 71 + .../jewish_calendar/test_binary_sensor.py | 97 ++ .../components/jewish_calendar/test_sensor.py | 1254 +++++++---------- 6 files changed, 952 insertions(+), 862 deletions(-) create mode 100644 homeassistant/components/jewish_calendar/binary_sensor.py create mode 100644 tests/components/jewish_calendar/test_binary_sensor.py diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 93a60e363e1aa2..c7bbbdb2d907a9 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -1 +1,109 @@ """The jewish_calendar component.""" +import logging + +import voluptuous as vol +import hdate + +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.helpers.discovery import async_load_platform +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "jewish_calendar" + +SENSOR_TYPES = { + "binary": { + "issur_melacha_in_effect": ["Issur Melacha in Effect", "mdi:power-plug-off"] + }, + "data": { + "date": ["Date", "mdi:judaism"], + "weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"], + "holiday_name": ["Holiday name", "mdi:calendar-star"], + "holiday_type": ["Holiday type", "mdi:counter"], + "omer_count": ["Day of the Omer", "mdi:counter"], + }, + "time": { + "first_light": ["Alot Hashachar", "mdi:weather-sunset-up"], + "gra_end_shma": ['Latest time for Shm"a GR"A', "mdi:calendar-clock"], + "mga_end_shma": ['Latest time for Shm"a MG"A', "mdi:calendar-clock"], + "plag_mincha": ["Plag Hamincha", "mdi:weather-sunset-down"], + "first_stars": ["T'set Hakochavim", "mdi:weather-night"], + "upcoming_shabbat_candle_lighting": [ + "Upcoming Shabbat Candle Lighting", + "mdi:candle", + ], + "upcoming_shabbat_havdalah": ["Upcoming Shabbat Havdalah", "mdi:weather-night"], + "upcoming_candle_lighting": ["Upcoming Candle Lighting", "mdi:candle"], + "upcoming_havdalah": ["Upcoming Havdalah", "mdi:weather-night"], + }, +} + +CONF_DIASPORA = "diaspora" +CONF_LANGUAGE = "language" +CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset" +CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset" + +CANDLE_LIGHT_DEFAULT = 18 + +DEFAULT_NAME = "Jewish Calendar" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DIASPORA, default=False): cv.boolean, + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Optional(CONF_LANGUAGE, default="english"): vol.In( + ["hebrew", "english"] + ), + vol.Optional( + CONF_CANDLE_LIGHT_MINUTES, default=CANDLE_LIGHT_DEFAULT + ): int, + # Default of 0 means use 8.5 degrees / 'three_stars' time. + vol.Optional(CONF_HAVDALAH_OFFSET_MINUTES, default=0): int, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Jewish Calendar component.""" + name = config[DOMAIN][CONF_NAME] + language = config[DOMAIN][CONF_LANGUAGE] + + latitude = config[DOMAIN].get(CONF_LATITUDE, hass.config.latitude) + longitude = config[DOMAIN].get(CONF_LONGITUDE, hass.config.longitude) + diaspora = config[DOMAIN][CONF_DIASPORA] + + candle_lighting_offset = config[DOMAIN][CONF_CANDLE_LIGHT_MINUTES] + havdalah_offset = config[DOMAIN][CONF_HAVDALAH_OFFSET_MINUTES] + + location = hdate.Location( + latitude=latitude, + longitude=longitude, + timezone=hass.config.time_zone, + diaspora=diaspora, + ) + + hass.data[DOMAIN] = { + "location": location, + "name": name, + "language": language, + "candle_lighting_offset": candle_lighting_offset, + "havdalah_offset": havdalah_offset, + "diaspora": diaspora, + } + + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) + + hass.async_create_task( + async_load_platform(hass, "binary_sensor", DOMAIN, {}, config) + ) + + return True diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py new file mode 100644 index 00000000000000..7362fce3cd0301 --- /dev/null +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -0,0 +1,66 @@ +"""Support for Jewish Calendar binary sensors.""" +import logging + +import hdate + +from homeassistant.components.binary_sensor import BinarySensorDevice +import homeassistant.util.dt as dt_util + +from . import DOMAIN, SENSOR_TYPES + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Jewish Calendar binary sensor devices.""" + if discovery_info is None: + return + + async_add_entities( + [ + JewishCalendarBinarySensor(hass.data[DOMAIN], sensor, sensor_info) + for sensor, sensor_info in SENSOR_TYPES["binary"].items() + ] + ) + + +class JewishCalendarBinarySensor(BinarySensorDevice): + """Representation of an Jewish Calendar binary sensor.""" + + def __init__(self, data, sensor, sensor_info): + """Initialize the binary sensor.""" + self._location = data["location"] + self._type = sensor + self._name = f"{data['name']} {sensor_info[0]}" + self._icon = sensor_info[1] + self._hebrew = data["language"] == "hebrew" + self._candle_lighting_offset = data["candle_lighting_offset"] + self._havdalah_offset = data["havdalah_offset"] + self._state = False + + @property + def icon(self): + """Return the icon of the entity.""" + return self._icon + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state + + async def async_update(self): + """Update the state of the sensor.""" + zmanim = hdate.Zmanim( + date=dt_util.now(), + location=self._location, + candle_lighting_offset=self._candle_lighting_offset, + havdalah_offset=self._havdalah_offset, + hebrew=self._hebrew, + ) + + self._state = zmanim.issur_melacha_in_effect diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index d298aee91436b4..405838b1fb10f9 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -1,140 +1,59 @@ """Platform to retrieve Jewish calendar information for Home Assistant.""" import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - SUN_EVENT_SUNSET, -) -import homeassistant.helpers.config_validation as cv +import hdate + +from homeassistant.const import SUN_EVENT_SUNSET from homeassistant.helpers.entity import Entity from homeassistant.helpers.sun import get_astral_event_date import homeassistant.util.dt as dt_util -_LOGGER = logging.getLogger(__name__) +from . import DOMAIN, SENSOR_TYPES -SENSOR_TYPES = { - "date": ["Date", "mdi:judaism"], - "weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"], - "holiday_name": ["Holiday", "mdi:calendar-star"], - "holyness": ["Holyness", "mdi:counter"], - "first_light": ["Alot Hashachar", "mdi:weather-sunset-up"], - "gra_end_shma": ['Latest time for Shm"a GR"A', "mdi:calendar-clock"], - "mga_end_shma": ['Latest time for Shm"a MG"A', "mdi:calendar-clock"], - "plag_mincha": ["Plag Hamincha", "mdi:weather-sunset-down"], - "first_stars": ["T'set Hakochavim", "mdi:weather-night"], - "upcoming_shabbat_candle_lighting": [ - "Upcoming Shabbat Candle Lighting", - "mdi:candle", - ], - "upcoming_shabbat_havdalah": ["Upcoming Shabbat Havdalah", "mdi:weather-night"], - "upcoming_candle_lighting": ["Upcoming Candle Lighting", "mdi:candle"], - "upcoming_havdalah": ["Upcoming Havdalah", "mdi:weather-night"], - "issur_melacha_in_effect": ["Issur Melacha in Effect", "mdi:power-plug-off"], - "omer_count": ["Day of the Omer", "mdi:counter"], -} - -CONF_DIASPORA = "diaspora" -CONF_LANGUAGE = "language" -CONF_SENSORS = "sensors" -CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset" -CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset" - -CANDLE_LIGHT_DEFAULT = 18 - -DEFAULT_NAME = "Jewish Calendar" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DIASPORA, default=False): cv.boolean, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_LANGUAGE, default="english"): vol.In(["hebrew", "english"]), - vol.Optional(CONF_CANDLE_LIGHT_MINUTES, default=CANDLE_LIGHT_DEFAULT): int, - # Default of 0 means use 8.5 degrees / 'three_stars' time. - vol.Optional(CONF_HAVDALAH_OFFSET_MINUTES, default=0): int, - vol.Optional(CONF_SENSORS, default=["date"]): vol.All( - cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)] - ), - } -) +_LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Jewish calendar sensor platform.""" - language = config.get(CONF_LANGUAGE) - name = config.get(CONF_NAME) - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - diaspora = config.get(CONF_DIASPORA) - candle_lighting_offset = config.get(CONF_CANDLE_LIGHT_MINUTES) - havdalah_offset = config.get(CONF_HAVDALAH_OFFSET_MINUTES) - - if None in (latitude, longitude): - _LOGGER.error("Latitude or longitude not set in Home Assistant config") + if discovery_info is None: return - dev = [] - for sensor_type in config[CONF_SENSORS]: - dev.append( - JewishCalSensor( - name, - language, - sensor_type, - latitude, - longitude, - hass.config.time_zone, - diaspora, - candle_lighting_offset, - havdalah_offset, - ) - ) - async_add_entities(dev, True) + sensors = [ + JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info) + for sensor, sensor_info in SENSOR_TYPES["data"].items() + ] + sensors.extend( + JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info) + for sensor, sensor_info in SENSOR_TYPES["time"].items() + ) + + async_add_entities(sensors) -class JewishCalSensor(Entity): +class JewishCalendarSensor(Entity): """Representation of an Jewish calendar sensor.""" - def __init__( - self, - name, - language, - sensor_type, - latitude, - longitude, - timezone, - diaspora, - candle_lighting_offset=CANDLE_LIGHT_DEFAULT, - havdalah_offset=0, - ): + def __init__(self, data, sensor, sensor_info): """Initialize the Jewish calendar sensor.""" - self.client_name = name - self._name = SENSOR_TYPES[sensor_type][0] - self.type = sensor_type - self._hebrew = language == "hebrew" + self._location = data["location"] + self._type = sensor + self._name = f"{data['name']} {sensor_info[0]}" + self._icon = sensor_info[1] + self._hebrew = data["language"] == "hebrew" + self._candle_lighting_offset = data["candle_lighting_offset"] + self._havdalah_offset = data["havdalah_offset"] + self._diaspora = data["diaspora"] self._state = None - self.latitude = latitude - self.longitude = longitude - self.timezone = timezone - self.diaspora = diaspora - self.candle_lighting_offset = candle_lighting_offset - self.havdalah_offset = havdalah_offset - _LOGGER.debug("Sensor %s initialized", self.type) @property def name(self): """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" + return self._name @property def icon(self): """Icon to display in the front end.""" - return SENSOR_TYPES[self.type][1] + return self._icon @property def state(self): @@ -143,9 +62,7 @@ def state(self): async def async_update(self): """Update the state of the sensor.""" - import hdate - - now = dt_util.as_local(dt_util.now()) + now = dt_util.now() _LOGGER.debug("Now: %s Timezone = %s", now, now.tzinfo) today = now.date() @@ -155,66 +72,65 @@ async def async_update(self): _LOGGER.debug("Now: %s Sunset: %s", now, sunset) - location = hdate.Location( - latitude=self.latitude, - longitude=self.longitude, - timezone=self.timezone, - diaspora=self.diaspora, - ) - def make_zmanim(date): """Create a Zmanim object.""" return hdate.Zmanim( date=date, - location=location, - candle_lighting_offset=self.candle_lighting_offset, - havdalah_offset=self.havdalah_offset, + location=self._location, + candle_lighting_offset=self._candle_lighting_offset, + havdalah_offset=self._havdalah_offset, hebrew=self._hebrew, ) - date = hdate.HDate(today, diaspora=self.diaspora, hebrew=self._hebrew) - lagging_date = date + date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew) - # Advance Hebrew date if sunset has passed. - # Not all sensors should advance immediately when the Hebrew date - # officially changes (i.e. after sunset), hence lagging_date. - if now > sunset: - date = date.next_day + # The Jewish day starts after darkness (called "tzais") and finishes at + # sunset ("shkia"). The time in between is a gray area (aka "Bein + # Hashmashot" - literally: "in between the sun and the moon"). + + # For some sensors, it is more interesting to consider the date to be + # tomorrow based on sunset ("shkia"), for others based on "tzais". + # Hence the following variables. + after_tzais_date = after_shkia_date = date today_times = make_zmanim(today) + + if now > sunset: + after_shkia_date = date.next_day + if today_times.havdalah and now > today_times.havdalah: - lagging_date = lagging_date.next_day + after_tzais_date = date.next_day # Terminology note: by convention in py-libhdate library, "upcoming" # refers to "current" or "upcoming" dates. - if self.type == "date": - self._state = date.hebrew_date - elif self.type == "weekly_portion": + if self._type == "date": + self._state = after_shkia_date.hebrew_date + elif self._type == "weekly_portion": # Compute the weekly portion based on the upcoming shabbat. - self._state = lagging_date.upcoming_shabbat.parasha - elif self.type == "holiday_name": - self._state = date.holiday_description - elif self.type == "holyness": - self._state = date.holiday_type - elif self.type == "upcoming_shabbat_candle_lighting": - times = make_zmanim(lagging_date.upcoming_shabbat.previous_day.gdate) + self._state = after_tzais_date.upcoming_shabbat.parasha + elif self._type == "holiday_name": + self._state = after_shkia_date.holiday_description + elif self._type == "holiday_type": + self._state = after_shkia_date.holiday_type + elif self._type == "upcoming_shabbat_candle_lighting": + times = make_zmanim(after_tzais_date.upcoming_shabbat.previous_day.gdate) self._state = times.candle_lighting - elif self.type == "upcoming_candle_lighting": + elif self._type == "upcoming_candle_lighting": times = make_zmanim( - lagging_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate + after_tzais_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate ) self._state = times.candle_lighting - elif self.type == "upcoming_shabbat_havdalah": - times = make_zmanim(lagging_date.upcoming_shabbat.gdate) + elif self._type == "upcoming_shabbat_havdalah": + times = make_zmanim(after_tzais_date.upcoming_shabbat.gdate) self._state = times.havdalah - elif self.type == "upcoming_havdalah": - times = make_zmanim(lagging_date.upcoming_shabbat_or_yom_tov.last_day.gdate) + elif self._type == "upcoming_havdalah": + times = make_zmanim( + after_tzais_date.upcoming_shabbat_or_yom_tov.last_day.gdate + ) self._state = times.havdalah - elif self.type == "issur_melacha_in_effect": - self._state = make_zmanim(now).issur_melacha_in_effect - elif self.type == "omer_count": - self._state = date.omer_day + elif self._type == "omer_count": + self._state = after_shkia_date.omer_day else: times = make_zmanim(today).zmanim - self._state = times[self.type].time() + self._state = times[self._type].time() _LOGGER.debug("New value: %s", self._state) diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index d6928c189e8c57..54589a640cc0cd 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -1 +1,72 @@ """Tests for the jewish_calendar component.""" +from datetime import datetime +from collections import namedtuple +from contextlib import contextmanager +from unittest.mock import patch + +from homeassistant.components import jewish_calendar +import homeassistant.util.dt as dt_util + + +_LatLng = namedtuple("_LatLng", ["lat", "lng"]) + +NYC_LATLNG = _LatLng(40.7128, -74.0060) +JERUSALEM_LATLNG = _LatLng(31.778, 35.235) + +ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE + + +def teardown_module(): + """Reset time zone.""" + dt_util.set_default_time_zone(ORIG_TIME_ZONE) + + +def make_nyc_test_params(dtime, results, havdalah_offset=0): + """Make test params for NYC.""" + if isinstance(results, dict): + time_zone = dt_util.get_time_zone("America/New_York") + results = { + key: time_zone.localize(value) if isinstance(value, datetime) else value + for key, value in results.items() + } + return ( + dtime, + jewish_calendar.CANDLE_LIGHT_DEFAULT, + havdalah_offset, + True, + "America/New_York", + NYC_LATLNG.lat, + NYC_LATLNG.lng, + results, + ) + + +def make_jerusalem_test_params(dtime, results, havdalah_offset=0): + """Make test params for Jerusalem.""" + if isinstance(results, dict): + time_zone = dt_util.get_time_zone("Asia/Jerusalem") + results = { + key: time_zone.localize(value) if isinstance(value, datetime) else value + for key, value in results.items() + } + return ( + dtime, + jewish_calendar.CANDLE_LIGHT_DEFAULT, + havdalah_offset, + False, + "Asia/Jerusalem", + JERUSALEM_LATLNG.lat, + JERUSALEM_LATLNG.lng, + results, + ) + + +@contextmanager +def alter_time(local_time): + """Manage multiple time mocks.""" + utc_time = dt_util.as_utc(local_time) + patch1 = patch("homeassistant.util.dt.utcnow", return_value=utc_time) + patch2 = patch("homeassistant.util.dt.now", return_value=local_time) + + with patch1, patch2: + yield diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py new file mode 100644 index 00000000000000..64745d8929f7e7 --- /dev/null +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -0,0 +1,97 @@ +"""The tests for the Jewish calendar binary sensors.""" +from datetime import timedelta +from datetime import datetime as dt + +import pytest + +from homeassistant.const import STATE_ON, STATE_OFF +import homeassistant.util.dt as dt_util +from homeassistant.setup import async_setup_component +from homeassistant.components import jewish_calendar + +from tests.common import async_fire_time_changed +from . import alter_time, make_nyc_test_params, make_jerusalem_test_params + + +MELACHA_PARAMS = [ + make_nyc_test_params(dt(2018, 9, 1, 16, 0), STATE_ON), + make_nyc_test_params(dt(2018, 9, 1, 20, 21), STATE_OFF), + make_nyc_test_params(dt(2018, 9, 7, 13, 1), STATE_OFF), + make_nyc_test_params(dt(2018, 9, 8, 21, 25), STATE_OFF), + make_nyc_test_params(dt(2018, 9, 9, 21, 25), STATE_ON), + make_nyc_test_params(dt(2018, 9, 10, 21, 25), STATE_ON), + make_nyc_test_params(dt(2018, 9, 28, 21, 25), STATE_ON), + make_nyc_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF), + make_nyc_test_params(dt(2018, 9, 30, 21, 25), STATE_ON), + make_nyc_test_params(dt(2018, 10, 1, 21, 25), STATE_ON), + make_jerusalem_test_params(dt(2018, 9, 29, 21, 25), STATE_OFF), + make_jerusalem_test_params(dt(2018, 9, 30, 21, 25), STATE_ON), + make_jerusalem_test_params(dt(2018, 10, 1, 21, 25), STATE_OFF), +] + +MELACHA_TEST_IDS = [ + "currently_first_shabbat", + "after_first_shabbat", + "friday_upcoming_shabbat", + "upcoming_rosh_hashana", + "currently_rosh_hashana", + "second_day_rosh_hashana", + "currently_shabbat_chol_hamoed", + "upcoming_two_day_yomtov_in_diaspora", + "currently_first_day_of_two_day_yomtov_in_diaspora", + "currently_second_day_of_two_day_yomtov_in_diaspora", + "upcoming_one_day_yom_tov_in_israel", + "currently_one_day_yom_tov_in_israel", + "after_one_day_yom_tov_in_israel", +] + + +@pytest.mark.parametrize( + [ + "now", + "candle_lighting", + "havdalah", + "diaspora", + "tzname", + "latitude", + "longitude", + "result", + ], + MELACHA_PARAMS, + ids=MELACHA_TEST_IDS, +) +async def test_issur_melacha_sensor( + hass, now, candle_lighting, havdalah, diaspora, tzname, latitude, longitude, result +): + """Test Issur Melacha sensor output.""" + time_zone = dt_util.get_time_zone(tzname) + test_time = time_zone.localize(now) + + hass.config.time_zone = time_zone + hass.config.latitude = latitude + hass.config.longitude = longitude + + with alter_time(test_time): + assert await async_setup_component( + hass, + jewish_calendar.DOMAIN, + { + "jewish_calendar": { + "name": "test", + "language": "english", + "diaspora": diaspora, + "candle_lighting_minutes_before_sunset": candle_lighting, + "havdalah_minutes_after_sunset": havdalah, + } + }, + ) + await hass.async_block_till_done() + + future = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert ( + hass.states.get("binary_sensor.test_issur_melacha_in_effect").state + == result + ) diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index f8c214f9800007..8d72830b3698ab 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -1,733 +1,565 @@ -"""The tests for the Jewish calendar sensor platform.""" -from collections import namedtuple -from datetime import time +"""The tests for the Jewish calendar sensors.""" +from datetime import time, timedelta from datetime import datetime as dt -from unittest.mock import patch import pytest -from homeassistant.util.async_ import run_coroutine_threadsafe -from homeassistant.util.dt import get_time_zone, set_default_time_zone -from homeassistant.setup import setup_component -from homeassistant.components.jewish_calendar.sensor import ( - JewishCalSensor, - CANDLE_LIGHT_DEFAULT, -) -from tests.common import get_test_home_assistant +import homeassistant.util.dt as dt_util +from homeassistant.setup import async_setup_component +from homeassistant.components import jewish_calendar +from tests.common import async_fire_time_changed +from . import alter_time, make_nyc_test_params, make_jerusalem_test_params -_LatLng = namedtuple("_LatLng", ["lat", "lng"]) -NYC_LATLNG = _LatLng(40.7128, -74.0060) -JERUSALEM_LATLNG = _LatLng(31.778, 35.235) +async def test_jewish_calendar_min_config(hass): + """Test minimum jewish calendar configuration.""" + assert await async_setup_component( + hass, jewish_calendar.DOMAIN, {"jewish_calendar": {}} + ) + await hass.async_block_till_done() + assert hass.states.get("sensor.jewish_calendar_date") is not None -def make_nyc_test_params(dtime, results, havdalah_offset=0): - """Make test params for NYC.""" - return ( - dtime, - CANDLE_LIGHT_DEFAULT, - havdalah_offset, - True, - "America/New_York", - NYC_LATLNG.lat, - NYC_LATLNG.lng, - results, +async def test_jewish_calendar_hebrew(hass): + """Test jewish calendar sensor with language set to hebrew.""" + assert await async_setup_component( + hass, jewish_calendar.DOMAIN, {"jewish_calendar": {"language": "hebrew"}} ) + await hass.async_block_till_done() + assert hass.states.get("sensor.jewish_calendar_date") is not None -def make_jerusalem_test_params(dtime, results, havdalah_offset=0): - """Make test params for Jerusalem.""" - return ( - dtime, - CANDLE_LIGHT_DEFAULT, - havdalah_offset, +TEST_PARAMS = [ + (dt(2018, 9, 3), "UTC", 31.778, 35.235, "english", "date", False, "23 Elul 5778"), + ( + dt(2018, 9, 3), + "UTC", + 31.778, + 35.235, + "hebrew", + "date", + False, + 'כ"ג אלול ה\' תשע"ח', + ), + ( + dt(2018, 9, 10), + "UTC", + 31.778, + 35.235, + "hebrew", + "holiday_name", + False, + "א' ראש השנה", + ), + ( + dt(2018, 9, 10), + "UTC", + 31.778, + 35.235, + "english", + "holiday_name", + False, + "Rosh Hashana I", + ), + (dt(2018, 9, 10), "UTC", 31.778, 35.235, "english", "holiday_type", False, 1), + ( + dt(2018, 9, 8), + "UTC", + 31.778, + 35.235, + "hebrew", + "parshat_hashavua", + False, + "נצבים", + ), + ( + dt(2018, 9, 8), + "America/New_York", + 40.7128, + -74.0060, + "hebrew", + "t_set_hakochavim", + True, + time(19, 48), + ), + ( + dt(2018, 9, 8), + "Asia/Jerusalem", + 31.778, + 35.235, + "hebrew", + "t_set_hakochavim", False, + time(19, 21), + ), + ( + dt(2018, 10, 14), "Asia/Jerusalem", - JERUSALEM_LATLNG.lat, - JERUSALEM_LATLNG.lng, - results, - ) + 31.778, + 35.235, + "hebrew", + "parshat_hashavua", + False, + "לך לך", + ), + ( + dt(2018, 10, 14, 17, 0, 0), + "Asia/Jerusalem", + 31.778, + 35.235, + "hebrew", + "date", + False, + "ה' מרחשוון ה' תשע\"ט", + ), + ( + dt(2018, 10, 14, 19, 0, 0), + "Asia/Jerusalem", + 31.778, + 35.235, + "hebrew", + "date", + False, + "ו' מרחשוון ה' תשע\"ט", + ), +] +TEST_IDS = [ + "date_output", + "date_output_hebrew", + "holiday_name", + "holiday_name_english", + "holiday_type", + "torah_reading", + "first_stars_ny", + "first_stars_jerusalem", + "torah_reading_weekday", + "date_before_sunset", + "date_after_sunset", +] -class TestJewishCalenderSensor: - """Test the Jewish Calendar sensor.""" - - # pylint: disable=attribute-defined-outside-init - def setup_method(self, method): - """Set up things to run when tests begin.""" - self.hass = get_test_home_assistant() - - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() - # Reset the default timezone, so we don't affect other tests - set_default_time_zone(get_time_zone("UTC")) - - def test_jewish_calendar_min_config(self): - """Test minimum jewish calendar configuration.""" - config = {"sensor": {"platform": "jewish_calendar"}} - assert setup_component(self.hass, "sensor", config) - - def test_jewish_calendar_hebrew(self): - """Test jewish calendar sensor with language set to hebrew.""" - config = {"sensor": {"platform": "jewish_calendar", "language": "hebrew"}} - - assert setup_component(self.hass, "sensor", config) - - def test_jewish_calendar_multiple_sensors(self): - """Test jewish calendar sensor with multiple sensors setup.""" - config = { - "sensor": { - "platform": "jewish_calendar", - "sensors": [ - "date", - "weekly_portion", - "holiday_name", - "holyness", - "first_light", - "gra_end_shma", - "mga_end_shma", - "plag_mincha", - "first_stars", - ], - } - } - - assert setup_component(self.hass, "sensor", config) - - test_params = [ - ( - dt(2018, 9, 3), - "UTC", - 31.778, - 35.235, - "english", - "date", - False, - "23 Elul 5778", - ), - ( - dt(2018, 9, 3), - "UTC", - 31.778, - 35.235, - "hebrew", - "date", - False, - 'כ"ג אלול ה\' תשע"ח', - ), - ( - dt(2018, 9, 10), - "UTC", - 31.778, - 35.235, - "hebrew", - "holiday_name", - False, - "א' ראש השנה", - ), - ( - dt(2018, 9, 10), - "UTC", - 31.778, - 35.235, - "english", - "holiday_name", - False, - "Rosh Hashana I", - ), - (dt(2018, 9, 10), "UTC", 31.778, 35.235, "english", "holyness", False, 1), - ( - dt(2018, 9, 8), - "UTC", - 31.778, - 35.235, - "hebrew", - "weekly_portion", - False, - "נצבים", - ), - ( - dt(2018, 9, 8), - "America/New_York", - 40.7128, - -74.0060, - "hebrew", - "first_stars", - True, - time(19, 48), - ), - ( - dt(2018, 9, 8), - "Asia/Jerusalem", - 31.778, - 35.235, - "hebrew", - "first_stars", - False, - time(19, 21), - ), - ( - dt(2018, 10, 14), - "Asia/Jerusalem", - 31.778, - 35.235, - "hebrew", - "weekly_portion", - False, - "לך לך", - ), - ( - dt(2018, 10, 14, 17, 0, 0), - "Asia/Jerusalem", - 31.778, - 35.235, - "hebrew", - "date", - False, - "ה' מרחשוון ה' תשע\"ט", - ), - ( - dt(2018, 10, 14, 19, 0, 0), - "Asia/Jerusalem", - 31.778, - 35.235, - "hebrew", - "date", - False, - "ו' מרחשוון ה' תשע\"ט", - ), - ] - - test_ids = [ - "date_output", - "date_output_hebrew", - "holiday_name", - "holiday_name_english", - "holyness", - "torah_reading", - "first_stars_ny", - "first_stars_jerusalem", - "torah_reading_weekday", - "date_before_sunset", - "date_after_sunset", - ] - - @pytest.mark.parametrize( - [ - "cur_time", - "tzname", - "latitude", - "longitude", - "language", - "sensor", - "diaspora", - "result", - ], - test_params, - ids=test_ids, - ) - def test_jewish_calendar_sensor( - self, cur_time, tzname, latitude, longitude, language, sensor, diaspora, result - ): - """Test Jewish calendar sensor output.""" - time_zone = get_time_zone(tzname) - set_default_time_zone(time_zone) - test_time = time_zone.localize(cur_time) - self.hass.config.latitude = latitude - self.hass.config.longitude = longitude - sensor = JewishCalSensor( - name="test", - language=language, - sensor_type=sensor, - latitude=latitude, - longitude=longitude, - timezone=time_zone, - diaspora=diaspora, - ) - sensor.hass = self.hass - with patch("homeassistant.util.dt.now", return_value=test_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() - assert sensor.state == result - - shabbat_params = [ - make_nyc_test_params( - dt(2018, 9, 1, 16, 0), - { - "upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15), - "upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 14), - "weekly_portion": "Ki Tavo", - "hebrew_weekly_portion": "כי תבוא", - }, - ), - make_nyc_test_params( - dt(2018, 9, 1, 16, 0), - { - "upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15), - "upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 22), - "weekly_portion": "Ki Tavo", - "hebrew_weekly_portion": "כי תבוא", - }, - havdalah_offset=50, - ), - make_nyc_test_params( - dt(2018, 9, 1, 20, 0), - { - "upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15), - "upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 14), - "upcoming_candle_lighting": dt(2018, 8, 31, 19, 15), - "upcoming_havdalah": dt(2018, 9, 1, 20, 14), - "weekly_portion": "Ki Tavo", - "hebrew_weekly_portion": "כי תבוא", - }, - ), - make_nyc_test_params( - dt(2018, 9, 1, 20, 21), - { - "upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19, 4), - "upcoming_shabbat_havdalah": dt(2018, 9, 8, 20, 2), - "weekly_portion": "Nitzavim", - "hebrew_weekly_portion": "נצבים", - }, - ), - make_nyc_test_params( - dt(2018, 9, 7, 13, 1), - { - "upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19, 4), - "upcoming_shabbat_havdalah": dt(2018, 9, 8, 20, 2), - "weekly_portion": "Nitzavim", - "hebrew_weekly_portion": "נצבים", - }, - ), - make_nyc_test_params( - dt(2018, 9, 8, 21, 25), - { - "upcoming_candle_lighting": dt(2018, 9, 9, 19, 1), - "upcoming_havdalah": dt(2018, 9, 11, 19, 57), - "upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52), - "upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), - "weekly_portion": "Vayeilech", - "hebrew_weekly_portion": "וילך", - "holiday_name": "Erev Rosh Hashana", - "hebrew_holiday_name": "ערב ראש השנה", - }, - ), - make_nyc_test_params( - dt(2018, 9, 9, 21, 25), - { - "upcoming_candle_lighting": dt(2018, 9, 9, 19, 1), - "upcoming_havdalah": dt(2018, 9, 11, 19, 57), - "upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52), - "upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), - "weekly_portion": "Vayeilech", - "hebrew_weekly_portion": "וילך", - "holiday_name": "Rosh Hashana I", - "hebrew_holiday_name": "א' ראש השנה", - }, - ), - make_nyc_test_params( - dt(2018, 9, 10, 21, 25), - { - "upcoming_candle_lighting": dt(2018, 9, 9, 19, 1), - "upcoming_havdalah": dt(2018, 9, 11, 19, 57), - "upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52), - "upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), - "weekly_portion": "Vayeilech", - "hebrew_weekly_portion": "וילך", - "holiday_name": "Rosh Hashana II", - "hebrew_holiday_name": "ב' ראש השנה", - }, - ), - make_nyc_test_params( - dt(2018, 9, 28, 21, 25), - { - "upcoming_shabbat_candle_lighting": dt(2018, 9, 28, 18, 28), - "upcoming_shabbat_havdalah": dt(2018, 9, 29, 19, 25), - "weekly_portion": "none", - "hebrew_weekly_portion": "none", - }, - ), - make_nyc_test_params( - dt(2018, 9, 29, 21, 25), - { - "upcoming_candle_lighting": dt(2018, 9, 30, 18, 25), - "upcoming_havdalah": dt(2018, 10, 2, 19, 20), - "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17), - "upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), - "weekly_portion": "Bereshit", - "hebrew_weekly_portion": "בראשית", - "holiday_name": "Hoshana Raba", - "hebrew_holiday_name": "הושענא רבה", - }, - ), - make_nyc_test_params( - dt(2018, 9, 30, 21, 25), - { - "upcoming_candle_lighting": dt(2018, 9, 30, 18, 25), - "upcoming_havdalah": dt(2018, 10, 2, 19, 20), - "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17), - "upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), - "weekly_portion": "Bereshit", - "hebrew_weekly_portion": "בראשית", - "holiday_name": "Shmini Atzeret", - "hebrew_holiday_name": "שמיני עצרת", - }, - ), - make_nyc_test_params( - dt(2018, 10, 1, 21, 25), - { - "upcoming_candle_lighting": dt(2018, 9, 30, 18, 25), - "upcoming_havdalah": dt(2018, 10, 2, 19, 20), - "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17), - "upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), - "weekly_portion": "Bereshit", - "hebrew_weekly_portion": "בראשית", - "holiday_name": "Simchat Torah", - "hebrew_holiday_name": "שמחת תורה", - }, - ), - make_jerusalem_test_params( - dt(2018, 9, 29, 21, 25), - { - "upcoming_candle_lighting": dt(2018, 9, 30, 18, 10), - "upcoming_havdalah": dt(2018, 10, 1, 19, 2), - "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3), - "upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), - "weekly_portion": "Bereshit", - "hebrew_weekly_portion": "בראשית", - "holiday_name": "Hoshana Raba", - "hebrew_holiday_name": "הושענא רבה", - }, - ), - make_jerusalem_test_params( - dt(2018, 9, 30, 21, 25), - { - "upcoming_candle_lighting": dt(2018, 9, 30, 18, 10), - "upcoming_havdalah": dt(2018, 10, 1, 19, 2), - "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3), - "upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), - "weekly_portion": "Bereshit", - "hebrew_weekly_portion": "בראשית", - "holiday_name": "Shmini Atzeret", - "hebrew_holiday_name": "שמיני עצרת", - }, - ), - make_jerusalem_test_params( - dt(2018, 10, 1, 21, 25), - { - "upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3), - "upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), - "weekly_portion": "Bereshit", - "hebrew_weekly_portion": "בראשית", - }, - ), - make_nyc_test_params( - dt(2016, 6, 11, 8, 25), - { - "upcoming_candle_lighting": dt(2016, 6, 10, 20, 7), - "upcoming_havdalah": dt(2016, 6, 13, 21, 17), - "upcoming_shabbat_candle_lighting": dt(2016, 6, 10, 20, 7), - "upcoming_shabbat_havdalah": None, - "weekly_portion": "Bamidbar", - "hebrew_weekly_portion": "במדבר", - "holiday_name": "Erev Shavuot", - "hebrew_holiday_name": "ערב שבועות", - }, - ), - make_nyc_test_params( - dt(2016, 6, 12, 8, 25), - { - "upcoming_candle_lighting": dt(2016, 6, 10, 20, 7), - "upcoming_havdalah": dt(2016, 6, 13, 21, 17), - "upcoming_shabbat_candle_lighting": dt(2016, 6, 17, 20, 10), - "upcoming_shabbat_havdalah": dt(2016, 6, 18, 21, 19), - "weekly_portion": "Nasso", - "hebrew_weekly_portion": "נשא", - "holiday_name": "Shavuot", - "hebrew_holiday_name": "שבועות", - }, - ), - make_jerusalem_test_params( - dt(2017, 9, 21, 8, 25), - { - "upcoming_candle_lighting": dt(2017, 9, 20, 18, 23), - "upcoming_havdalah": dt(2017, 9, 23, 19, 13), - "upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14), - "upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), - "weekly_portion": "Ha'Azinu", - "hebrew_weekly_portion": "האזינו", - "holiday_name": "Rosh Hashana I", - "hebrew_holiday_name": "א' ראש השנה", - }, - ), - make_jerusalem_test_params( - dt(2017, 9, 22, 8, 25), + +@pytest.mark.parametrize( + [ + "now", + "tzname", + "latitude", + "longitude", + "language", + "sensor", + "diaspora", + "result", + ], + TEST_PARAMS, + ids=TEST_IDS, +) +async def test_jewish_calendar_sensor( + hass, now, tzname, latitude, longitude, language, sensor, diaspora, result +): + """Test Jewish calendar sensor output.""" + time_zone = dt_util.get_time_zone(tzname) + test_time = time_zone.localize(now) + + hass.config.time_zone = time_zone + hass.config.latitude = latitude + hass.config.longitude = longitude + + with alter_time(test_time): + assert await async_setup_component( + hass, + jewish_calendar.DOMAIN, { - "upcoming_candle_lighting": dt(2017, 9, 20, 18, 23), - "upcoming_havdalah": dt(2017, 9, 23, 19, 13), - "upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14), - "upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), - "weekly_portion": "Ha'Azinu", - "hebrew_weekly_portion": "האזינו", - "holiday_name": "Rosh Hashana II", - "hebrew_holiday_name": "ב' ראש השנה", + "jewish_calendar": { + "name": "test", + "language": language, + "diaspora": diaspora, + } }, - ), - make_jerusalem_test_params( - dt(2017, 9, 23, 8, 25), + ) + await hass.async_block_till_done() + + future = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert hass.states.get(f"sensor.test_{sensor}").state == str(result) + + +SHABBAT_PARAMS = [ + make_nyc_test_params( + dt(2018, 9, 1, 16, 0), + { + "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 15), + "english_upcoming_havdalah": dt(2018, 9, 1, 20, 14), + "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 14), + "english_parshat_hashavua": "Ki Tavo", + "hebrew_parshat_hashavua": "כי תבוא", + }, + ), + make_nyc_test_params( + dt(2018, 9, 1, 16, 0), + { + "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 15), + "english_upcoming_havdalah": dt(2018, 9, 1, 20, 22), + "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 22), + "english_parshat_hashavua": "Ki Tavo", + "hebrew_parshat_hashavua": "כי תבוא", + }, + havdalah_offset=50, + ), + make_nyc_test_params( + dt(2018, 9, 1, 20, 0), + { + "english_upcoming_shabbat_candle_lighting": dt(2018, 8, 31, 19, 15), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 1, 20, 14), + "english_upcoming_candle_lighting": dt(2018, 8, 31, 19, 15), + "english_upcoming_havdalah": dt(2018, 9, 1, 20, 14), + "english_parshat_hashavua": "Ki Tavo", + "hebrew_parshat_hashavua": "כי תבוא", + }, + ), + make_nyc_test_params( + dt(2018, 9, 1, 20, 21), + { + "english_upcoming_candle_lighting": dt(2018, 9, 7, 19, 4), + "english_upcoming_havdalah": dt(2018, 9, 8, 20, 2), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19, 4), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 8, 20, 2), + "english_parshat_hashavua": "Nitzavim", + "hebrew_parshat_hashavua": "נצבים", + }, + ), + make_nyc_test_params( + dt(2018, 9, 7, 13, 1), + { + "english_upcoming_candle_lighting": dt(2018, 9, 7, 19, 4), + "english_upcoming_havdalah": dt(2018, 9, 8, 20, 2), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 7, 19, 4), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 8, 20, 2), + "english_parshat_hashavua": "Nitzavim", + "hebrew_parshat_hashavua": "נצבים", + }, + ), + make_nyc_test_params( + dt(2018, 9, 8, 21, 25), + { + "english_upcoming_candle_lighting": dt(2018, 9, 9, 19, 1), + "english_upcoming_havdalah": dt(2018, 9, 11, 19, 57), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), + "english_parshat_hashavua": "Vayeilech", + "hebrew_parshat_hashavua": "וילך", + "english_holiday_name": "Erev Rosh Hashana", + "hebrew_holiday_name": "ערב ראש השנה", + }, + ), + make_nyc_test_params( + dt(2018, 9, 9, 21, 25), + { + "english_upcoming_candle_lighting": dt(2018, 9, 9, 19, 1), + "english_upcoming_havdalah": dt(2018, 9, 11, 19, 57), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), + "english_parshat_hashavua": "Vayeilech", + "hebrew_parshat_hashavua": "וילך", + "english_holiday_name": "Rosh Hashana I", + "hebrew_holiday_name": "א' ראש השנה", + }, + ), + make_nyc_test_params( + dt(2018, 9, 10, 21, 25), + { + "english_upcoming_candle_lighting": dt(2018, 9, 9, 19, 1), + "english_upcoming_havdalah": dt(2018, 9, 11, 19, 57), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 14, 18, 52), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), + "english_parshat_hashavua": "Vayeilech", + "hebrew_parshat_hashavua": "וילך", + "english_holiday_name": "Rosh Hashana II", + "hebrew_holiday_name": "ב' ראש השנה", + }, + ), + make_nyc_test_params( + dt(2018, 9, 28, 21, 25), + { + "english_upcoming_candle_lighting": dt(2018, 9, 28, 18, 28), + "english_upcoming_havdalah": dt(2018, 9, 29, 19, 25), + "english_upcoming_shabbat_candle_lighting": dt(2018, 9, 28, 18, 28), + "english_upcoming_shabbat_havdalah": dt(2018, 9, 29, 19, 25), + "english_parshat_hashavua": "none", + "hebrew_parshat_hashavua": "none", + }, + ), + make_nyc_test_params( + dt(2018, 9, 29, 21, 25), + { + "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 25), + "english_upcoming_havdalah": dt(2018, 10, 2, 19, 20), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), + "english_parshat_hashavua": "Bereshit", + "hebrew_parshat_hashavua": "בראשית", + "english_holiday_name": "Hoshana Raba", + "hebrew_holiday_name": "הושענא רבה", + }, + ), + make_nyc_test_params( + dt(2018, 9, 30, 21, 25), + { + "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 25), + "english_upcoming_havdalah": dt(2018, 10, 2, 19, 20), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), + "english_parshat_hashavua": "Bereshit", + "hebrew_parshat_hashavua": "בראשית", + "english_holiday_name": "Shmini Atzeret", + "hebrew_holiday_name": "שמיני עצרת", + }, + ), + make_nyc_test_params( + dt(2018, 10, 1, 21, 25), + { + "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 25), + "english_upcoming_havdalah": dt(2018, 10, 2, 19, 20), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 17), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), + "english_parshat_hashavua": "Bereshit", + "hebrew_parshat_hashavua": "בראשית", + "english_holiday_name": "Simchat Torah", + "hebrew_holiday_name": "שמחת תורה", + }, + ), + make_jerusalem_test_params( + dt(2018, 9, 29, 21, 25), + { + "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 10), + "english_upcoming_havdalah": dt(2018, 10, 1, 19, 2), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), + "english_parshat_hashavua": "Bereshit", + "hebrew_parshat_hashavua": "בראשית", + "english_holiday_name": "Hoshana Raba", + "hebrew_holiday_name": "הושענא רבה", + }, + ), + make_jerusalem_test_params( + dt(2018, 9, 30, 21, 25), + { + "english_upcoming_candle_lighting": dt(2018, 9, 30, 18, 10), + "english_upcoming_havdalah": dt(2018, 10, 1, 19, 2), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), + "english_parshat_hashavua": "Bereshit", + "hebrew_parshat_hashavua": "בראשית", + "english_holiday_name": "Shmini Atzeret", + "hebrew_holiday_name": "שמיני עצרת", + }, + ), + make_jerusalem_test_params( + dt(2018, 10, 1, 21, 25), + { + "english_upcoming_candle_lighting": dt(2018, 10, 5, 18, 3), + "english_upcoming_havdalah": dt(2018, 10, 6, 18, 56), + "english_upcoming_shabbat_candle_lighting": dt(2018, 10, 5, 18, 3), + "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), + "english_parshat_hashavua": "Bereshit", + "hebrew_parshat_hashavua": "בראשית", + }, + ), + make_nyc_test_params( + dt(2016, 6, 11, 8, 25), + { + "english_upcoming_candle_lighting": dt(2016, 6, 10, 20, 7), + "english_upcoming_havdalah": dt(2016, 6, 13, 21, 17), + "english_upcoming_shabbat_candle_lighting": dt(2016, 6, 10, 20, 7), + "english_upcoming_shabbat_havdalah": "unknown", + "english_parshat_hashavua": "Bamidbar", + "hebrew_parshat_hashavua": "במדבר", + "english_holiday_name": "Erev Shavuot", + "hebrew_holiday_name": "ערב שבועות", + }, + ), + make_nyc_test_params( + dt(2016, 6, 12, 8, 25), + { + "english_upcoming_candle_lighting": dt(2016, 6, 10, 20, 7), + "english_upcoming_havdalah": dt(2016, 6, 13, 21, 17), + "english_upcoming_shabbat_candle_lighting": dt(2016, 6, 17, 20, 10), + "english_upcoming_shabbat_havdalah": dt(2016, 6, 18, 21, 19), + "english_parshat_hashavua": "Nasso", + "hebrew_parshat_hashavua": "נשא", + "english_holiday_name": "Shavuot", + "hebrew_holiday_name": "שבועות", + }, + ), + make_jerusalem_test_params( + dt(2017, 9, 21, 8, 25), + { + "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 23), + "english_upcoming_havdalah": dt(2017, 9, 23, 19, 13), + "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14), + "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), + "english_parshat_hashavua": "Ha'Azinu", + "hebrew_parshat_hashavua": "האזינו", + "english_holiday_name": "Rosh Hashana I", + "hebrew_holiday_name": "א' ראש השנה", + }, + ), + make_jerusalem_test_params( + dt(2017, 9, 22, 8, 25), + { + "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 23), + "english_upcoming_havdalah": dt(2017, 9, 23, 19, 13), + "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14), + "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), + "english_parshat_hashavua": "Ha'Azinu", + "hebrew_parshat_hashavua": "האזינו", + "english_holiday_name": "Rosh Hashana II", + "hebrew_holiday_name": "ב' ראש השנה", + }, + ), + make_jerusalem_test_params( + dt(2017, 9, 23, 8, 25), + { + "english_upcoming_candle_lighting": dt(2017, 9, 20, 18, 23), + "english_upcoming_havdalah": dt(2017, 9, 23, 19, 13), + "english_upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14), + "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), + "english_parshat_hashavua": "Ha'Azinu", + "hebrew_parshat_hashavua": "האזינו", + "english_holiday_name": "", + "hebrew_holiday_name": "", + }, + ), +] + +SHABBAT_TEST_IDS = [ + "currently_first_shabbat", + "currently_first_shabbat_with_havdalah_offset", + "currently_first_shabbat_bein_hashmashot_lagging_date", + "after_first_shabbat", + "friday_upcoming_shabbat", + "upcoming_rosh_hashana", + "currently_rosh_hashana", + "second_day_rosh_hashana", + "currently_shabbat_chol_hamoed", + "upcoming_two_day_yomtov_in_diaspora", + "currently_first_day_of_two_day_yomtov_in_diaspora", + "currently_second_day_of_two_day_yomtov_in_diaspora", + "upcoming_one_day_yom_tov_in_israel", + "currently_one_day_yom_tov_in_israel", + "after_one_day_yom_tov_in_israel", + # Type 1 = Sat/Sun/Mon + "currently_first_day_of_three_day_type1_yomtov_in_diaspora", + "currently_second_day_of_three_day_type1_yomtov_in_diaspora", + # Type 2 = Thurs/Fri/Sat + "currently_first_day_of_three_day_type2_yomtov_in_israel", + "currently_second_day_of_three_day_type2_yomtov_in_israel", + "currently_third_day_of_three_day_type2_yomtov_in_israel", +] + + +@pytest.mark.parametrize("language", ["english", "hebrew"]) +@pytest.mark.parametrize( + [ + "now", + "candle_lighting", + "havdalah", + "diaspora", + "tzname", + "latitude", + "longitude", + "result", + ], + SHABBAT_PARAMS, + ids=SHABBAT_TEST_IDS, +) +async def test_shabbat_times_sensor( + hass, + language, + now, + candle_lighting, + havdalah, + diaspora, + tzname, + latitude, + longitude, + result, +): + """Test sensor output for upcoming shabbat/yomtov times.""" + time_zone = dt_util.get_time_zone(tzname) + test_time = time_zone.localize(now) + + hass.config.time_zone = time_zone + hass.config.latitude = latitude + hass.config.longitude = longitude + + with alter_time(test_time): + assert await async_setup_component( + hass, + jewish_calendar.DOMAIN, { - "upcoming_candle_lighting": dt(2017, 9, 20, 18, 23), - "upcoming_havdalah": dt(2017, 9, 23, 19, 13), - "upcoming_shabbat_candle_lighting": dt(2017, 9, 22, 19, 14), - "upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), - "weekly_portion": "Ha'Azinu", - "hebrew_weekly_portion": "האזינו", - "holiday_name": "", - "hebrew_holiday_name": "", + "jewish_calendar": { + "name": "test", + "language": language, + "diaspora": diaspora, + "candle_lighting_minutes_before_sunset": candle_lighting, + "havdalah_minutes_after_sunset": havdalah, + } }, - ), - ] - - shabbat_test_ids = [ - "currently_first_shabbat", - "currently_first_shabbat_with_havdalah_offset", - "currently_first_shabbat_bein_hashmashot_lagging_date", - "after_first_shabbat", - "friday_upcoming_shabbat", - "upcoming_rosh_hashana", - "currently_rosh_hashana", - "second_day_rosh_hashana", - "currently_shabbat_chol_hamoed", - "upcoming_two_day_yomtov_in_diaspora", - "currently_first_day_of_two_day_yomtov_in_diaspora", - "currently_second_day_of_two_day_yomtov_in_diaspora", - "upcoming_one_day_yom_tov_in_israel", - "currently_one_day_yom_tov_in_israel", - "after_one_day_yom_tov_in_israel", - # Type 1 = Sat/Sun/Mon - "currently_first_day_of_three_day_type1_yomtov_in_diaspora", - "currently_second_day_of_three_day_type1_yomtov_in_diaspora", - # Type 2 = Thurs/Fri/Sat - "currently_first_day_of_three_day_type2_yomtov_in_israel", - "currently_second_day_of_three_day_type2_yomtov_in_israel", - "currently_third_day_of_three_day_type2_yomtov_in_israel", - ] - - @pytest.mark.parametrize( - [ - "now", - "candle_lighting", - "havdalah", - "diaspora", - "tzname", - "latitude", - "longitude", - "result", - ], - shabbat_params, - ids=shabbat_test_ids, - ) - def test_shabbat_times_sensor( - self, - now, - candle_lighting, - havdalah, - diaspora, - tzname, - latitude, - longitude, - result, - ): - """Test sensor output for upcoming shabbat/yomtov times.""" - time_zone = get_time_zone(tzname) - set_default_time_zone(time_zone) - test_time = time_zone.localize(now) - for sensor_type, value in result.items(): - if isinstance(value, dt): - result[sensor_type] = time_zone.localize(value) - self.hass.config.latitude = latitude - self.hass.config.longitude = longitude - - if ( - "upcoming_shabbat_candle_lighting" in result - and "upcoming_candle_lighting" not in result - ): - result["upcoming_candle_lighting"] = result[ - "upcoming_shabbat_candle_lighting" - ] - if "upcoming_shabbat_havdalah" in result and "upcoming_havdalah" not in result: - result["upcoming_havdalah"] = result["upcoming_shabbat_havdalah"] - - for sensor_type, result_value in result.items(): - language = "english" - if sensor_type.startswith("hebrew_"): - language = "hebrew" - sensor_type = sensor_type.replace("hebrew_", "") - sensor = JewishCalSensor( - name="test", - language=language, - sensor_type=sensor_type, - latitude=latitude, - longitude=longitude, - timezone=time_zone, - diaspora=diaspora, - havdalah_offset=havdalah, - candle_lighting_offset=candle_lighting, - ) - sensor.hass = self.hass - with patch("homeassistant.util.dt.now", return_value=test_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() - assert sensor.state == result_value, "Value for {}".format(sensor_type) - - melacha_params = [ - make_nyc_test_params(dt(2018, 9, 1, 16, 0), True), - make_nyc_test_params(dt(2018, 9, 1, 20, 21), False), - make_nyc_test_params(dt(2018, 9, 7, 13, 1), False), - make_nyc_test_params(dt(2018, 9, 8, 21, 25), False), - make_nyc_test_params(dt(2018, 9, 9, 21, 25), True), - make_nyc_test_params(dt(2018, 9, 10, 21, 25), True), - make_nyc_test_params(dt(2018, 9, 28, 21, 25), True), - make_nyc_test_params(dt(2018, 9, 29, 21, 25), False), - make_nyc_test_params(dt(2018, 9, 30, 21, 25), True), - make_nyc_test_params(dt(2018, 10, 1, 21, 25), True), - make_jerusalem_test_params(dt(2018, 9, 29, 21, 25), False), - make_jerusalem_test_params(dt(2018, 9, 30, 21, 25), True), - make_jerusalem_test_params(dt(2018, 10, 1, 21, 25), False), - ] - melacha_test_ids = [ - "currently_first_shabbat", - "after_first_shabbat", - "friday_upcoming_shabbat", - "upcoming_rosh_hashana", - "currently_rosh_hashana", - "second_day_rosh_hashana", - "currently_shabbat_chol_hamoed", - "upcoming_two_day_yomtov_in_diaspora", - "currently_first_day_of_two_day_yomtov_in_diaspora", - "currently_second_day_of_two_day_yomtov_in_diaspora", - "upcoming_one_day_yom_tov_in_israel", - "currently_one_day_yom_tov_in_israel", - "after_one_day_yom_tov_in_israel", - ] - - @pytest.mark.parametrize( - [ - "now", - "candle_lighting", - "havdalah", - "diaspora", - "tzname", - "latitude", - "longitude", - "result", - ], - melacha_params, - ids=melacha_test_ids, - ) - def test_issur_melacha_sensor( - self, - now, - candle_lighting, - havdalah, - diaspora, - tzname, - latitude, - longitude, - result, - ): - """Test Issur Melacha sensor output.""" - time_zone = get_time_zone(tzname) - set_default_time_zone(time_zone) - test_time = time_zone.localize(now) - self.hass.config.latitude = latitude - self.hass.config.longitude = longitude - sensor = JewishCalSensor( - name="test", - language="english", - sensor_type="issur_melacha_in_effect", - latitude=latitude, - longitude=longitude, - timezone=time_zone, - diaspora=diaspora, - havdalah_offset=havdalah, - candle_lighting_offset=candle_lighting, ) - sensor.hass = self.hass - with patch("homeassistant.util.dt.now", return_value=test_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() - assert sensor.state == result - - omer_params = [ - make_nyc_test_params(dt(2019, 4, 21, 0, 0), 1), - make_jerusalem_test_params(dt(2019, 4, 21, 0, 0), 1), - make_nyc_test_params(dt(2019, 4, 21, 23, 0), 2), - make_jerusalem_test_params(dt(2019, 4, 21, 23, 0), 2), - make_nyc_test_params(dt(2019, 5, 23, 0, 0), 33), - make_jerusalem_test_params(dt(2019, 5, 23, 0, 0), 33), - make_nyc_test_params(dt(2019, 6, 8, 0, 0), 49), - make_jerusalem_test_params(dt(2019, 6, 8, 0, 0), 49), - make_nyc_test_params(dt(2019, 6, 9, 0, 0), 0), - make_jerusalem_test_params(dt(2019, 6, 9, 0, 0), 0), - make_nyc_test_params(dt(2019, 1, 1, 0, 0), 0), - make_jerusalem_test_params(dt(2019, 1, 1, 0, 0), 0), - ] - omer_test_ids = [ - "nyc_first_day_of_omer", - "israel_first_day_of_omer", - "nyc_first_day_of_omer_after_tzeit", - "israel_first_day_of_omer_after_tzeit", - "nyc_lag_baomer", - "israel_lag_baomer", - "nyc_last_day_of_omer", - "israel_last_day_of_omer", - "nyc_shavuot_no_omer", - "israel_shavuot_no_omer", - "nyc_jan_1st_no_omer", - "israel_jan_1st_no_omer", - ] - - @pytest.mark.parametrize( - [ - "now", - "candle_lighting", - "havdalah", - "diaspora", - "tzname", - "latitude", - "longitude", - "result", - ], - omer_params, - ids=omer_test_ids, - ) - def test_omer_sensor( - self, - now, - candle_lighting, - havdalah, - diaspora, - tzname, - latitude, - longitude, - result, - ): - """Test Omer Count sensor output.""" - time_zone = get_time_zone(tzname) - set_default_time_zone(time_zone) - test_time = time_zone.localize(now) - self.hass.config.latitude = latitude - self.hass.config.longitude = longitude - sensor = JewishCalSensor( - name="test", - language="english", - sensor_type="omer_count", - latitude=latitude, - longitude=longitude, - timezone=time_zone, - diaspora=diaspora, + await hass.async_block_till_done() + + future = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + for sensor_type, result_value in result.items(): + if not sensor_type.startswith(language): + print(f"Not checking {sensor_type} for {language}") + continue + + sensor_type = sensor_type.replace(f"{language}_", "") + + assert hass.states.get(f"sensor.test_{sensor_type}").state == str( + result_value + ), f"Value for {sensor_type}" + + +OMER_PARAMS = [ + (dt(2019, 4, 21, 0), "1"), + (dt(2019, 4, 21, 23), "2"), + (dt(2019, 5, 23, 0), "33"), + (dt(2019, 6, 8, 0), "49"), + (dt(2019, 6, 9, 0), "0"), + (dt(2019, 1, 1, 0), "0"), +] +OMER_TEST_IDS = [ + "first_day_of_omer", + "first_day_of_omer_after_tzeit", + "lag_baomer", + "last_day_of_omer", + "shavuot_no_omer", + "jan_1st_no_omer", +] + + +@pytest.mark.parametrize(["test_time", "result"], OMER_PARAMS, ids=OMER_TEST_IDS) +async def test_omer_sensor(hass, test_time, result): + """Test Omer Count sensor output.""" + test_time = hass.config.time_zone.localize(test_time) + + with alter_time(test_time): + assert await async_setup_component( + hass, jewish_calendar.DOMAIN, {"jewish_calendar": {"name": "test"}} ) - sensor.hass = self.hass - with patch("homeassistant.util.dt.now", return_value=test_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() - assert sensor.state == result + await hass.async_block_till_done() + + future = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert hass.states.get("sensor.test_day_of_the_omer").state == result From a202afcac2558de59f76d2728e2d0835d79383d7 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 6 Sep 2019 12:25:46 +0100 Subject: [PATCH 0195/3953] bump geniushub client (#26476) fixes #26440 --- homeassistant/components/geniushub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 12f7c266840bf6..0c10f7010cfc55 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.6.5" + "geniushub-client==0.6.7" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/requirements_all.txt b/requirements_all.txt index 7f0e861fc5dc45..63cd24741250e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -522,7 +522,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.6.5 +geniushub-client==0.6.7 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From f540d74b654700f349303cca7df0e13ba66a824a Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 6 Sep 2019 15:28:24 +0200 Subject: [PATCH 0196/3953] Unify device_state_attributes handling for Homematic IP Cloud (#26449) * unifi DSA for Homematic IP Cloud * sabotage is not relevant for state * TODAY_SUNSHINE_DURATION is not a group attribute * Separated the words as requested * add missing underscores --- .../homematicip_cloud/binary_sensor.py | 92 +++++++++---------- .../components/homematicip_cloud/light.py | 26 +++--- .../components/homematicip_cloud/sensor.py | 34 +++---- .../components/homematicip_cloud/switch.py | 6 +- .../components/homematicip_cloud/weather.py | 2 +- 5 files changed, 78 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index d6bc24d21edf18..594f4f6c54aa4e 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -43,14 +43,25 @@ _LOGGER = logging.getLogger(__name__) ATTR_LOW_BATTERY = "low_battery" -ATTR_MOTIONDETECTED = "motion detected" -ATTR_PRESENCEDETECTED = "presence detected" -ATTR_POWERMAINSFAILURE = "power mains failure" -ATTR_WINDOWSTATE = "window state" -ATTR_MOISTUREDETECTED = "moisture detected" -ATTR_WATERLEVELDETECTED = "water level detected" -ATTR_SMOKEDETECTORALARM = "smoke detector alarm" +ATTR_MOISTURE_DETECTED = "moisture_detected" +ATTR_MOTION_DETECTED = "motion_detected" +ATTR_POWER_MAINS_FAILURE = "power_mains_failure" +ATTR_PRESENCE_DETECTED = "presence_detected" +ATTR_SMOKE_DETECTOR_ALARM = "smoke_detector_alarm" ATTR_TODAY_SUNSHINE_DURATION = "today_sunshine_duration_in_minutes" +ATTR_WATER_LEVEL_DETECTED = "water_level_detected" +ATTR_WINDOW_STATE = "window_state" + +GROUP_ATTRIBUTES = { + "lowBat": ATTR_LOW_BATTERY, + "modelType": ATTR_MODEL_TYPE, + "moistureDetected": ATTR_MOISTURE_DETECTED, + "motionDetected": ATTR_MOTION_DETECTED, + "powerMainsFailure": ATTR_POWER_MAINS_FAILURE, + "presenceDetected": ATTR_PRESENCE_DETECTED, + "unreach": ATTR_GROUP_MEMBER_UNREACHABLE, + "waterlevelDetected": ATTR_WATER_LEVEL_DETECTED, +} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -118,8 +129,6 @@ def device_class(self) -> str: @property def is_on(self) -> bool: """Return true if the contact interface is on/open.""" - if hasattr(self._device, "sabotage") and self._device.sabotage: - return True if self._device.windowState is None: return None return self._device.windowState != WindowState.CLOSED @@ -136,8 +145,6 @@ def device_class(self) -> str: @property def is_on(self) -> bool: """Return true if the shutter contact is on/open.""" - if hasattr(self._device, "sabotage") and self._device.sabotage: - return True if self._device.windowState is None: return None return self._device.windowState != WindowState.CLOSED @@ -154,8 +161,6 @@ def device_class(self) -> str: @property def is_on(self) -> bool: """Return true if motion is detected.""" - if hasattr(self._device, "sabotage") and self._device.sabotage: - return True return self._device.motionDetected @@ -170,8 +175,6 @@ def device_class(self) -> str: @property def is_on(self) -> bool: """Return true if presence is detected.""" - if hasattr(self._device, "sabotage") and self._device.sabotage: - return True return self._device.presenceDetected @@ -259,13 +262,13 @@ def is_on(self) -> bool: @property def device_state_attributes(self): """Return the state attributes of the illuminance sensor.""" - attr = super().device_state_attributes - if ( - hasattr(self._device, "todaySunshineDuration") - and self._device.todaySunshineDuration - ): - attr[ATTR_TODAY_SUNSHINE_DURATION] = self._device.todaySunshineDuration - return attr + state_attr = super().device_state_attributes + + today_sunshine_duration = getattr(self._device, "todaySunshineDuration", None) + if today_sunshine_duration: + state_attr[ATTR_TODAY_SUNSHINE_DURATION] = today_sunshine_duration + + return state_attr class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): @@ -309,21 +312,18 @@ def available(self) -> bool: @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" - attr = {ATTR_MODEL_TYPE: self._device.modelType} + state_attr = {ATTR_MODEL_TYPE: self._device.modelType} - if self._device.motionDetected: - attr[ATTR_MOTIONDETECTED] = True - if self._device.presenceDetected: - attr[ATTR_PRESENCEDETECTED] = True + for attr, attr_key in GROUP_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value - if ( - self._device.windowState is not None - and self._device.windowState != WindowState.CLOSED - ): - attr[ATTR_WINDOWSTATE] = str(self._device.windowState) - if self._device.unreach: - attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True - return attr + window_state = getattr(self._device, "windowState", None) + if window_state and window_state != WindowState.CLOSED: + state_attr[ATTR_WINDOW_STATE] = str(window_state) + + return state_attr @property def is_on(self) -> bool: @@ -356,23 +356,13 @@ def __init__(self, home: AsyncHome, device) -> None: @property def device_state_attributes(self): """Return the state attributes of the security group.""" - attr = super().device_state_attributes - - if self._device.powerMainsFailure: - attr[ATTR_POWERMAINSFAILURE] = True - if self._device.moistureDetected: - attr[ATTR_MOISTUREDETECTED] = True - if self._device.waterlevelDetected: - attr[ATTR_WATERLEVELDETECTED] = True - if self._device.lowBat: - attr[ATTR_LOW_BATTERY] = True - if ( - self._device.smokeDetectorAlarmType is not None - and self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF - ): - attr[ATTR_SMOKEDETECTORALARM] = str(self._device.smokeDetectorAlarmType) + state_attr = super().device_state_attributes + + smoke_detector_at = getattr(self._device, "smokeDetectorAlarmType", None) + if smoke_detector_at and smoke_detector_at != SmokeDetectorAlarmType.IDLE_OFF: + state_attr[ATTR_SMOKE_DETECTOR_ALARM] = str(smoke_detector_at) - return attr + return state_attr @property def is_on(self) -> bool: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index bc7b12f9653ea5..42ff6d30478fb4 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -93,13 +93,15 @@ class HomematicipLightMeasuring(HomematicipLight): @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - attr = super().device_state_attributes - if self._device.currentPowerConsumption > 0.05: - attr[ATTR_POWER_CONSUMPTION] = round( - self._device.currentPowerConsumption, 2 - ) - attr[ATTR_ENERGY_COUNTER] = round(self._device.energyCounter, 2) - return attr + state_attr = super().device_state_attributes + + current_power_consumption = self._device.currentPowerConsumption + if current_power_consumption > 0.05: + state_attr[ATTR_POWER_CONSUMPTION] = round(current_power_consumption, 2) + + state_attr[ATTR_ENERGY_COUNTER] = round(self._device.energyCounter, 2) + + return state_attr class HomematicipDimmer(HomematicipGenericDevice, Light): @@ -187,15 +189,17 @@ def hs_color(self) -> tuple: @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - attr = super().device_state_attributes + state_attr = super().device_state_attributes + if self.is_on: - attr[ATTR_COLOR_NAME] = self._func_channel.simpleRGBColorState - return attr + state_attr[ATTR_COLOR_NAME] = self._func_channel.simpleRGBColorState + + return state_attr @property def name(self) -> str: """Return the name of the generic device.""" - return "{} {}".format(super().name, "Notification") + return f"{super().name} Notification" @property def supported_features(self) -> int: diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index c15b3121d3a63e..b396a8d9defade 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -229,13 +229,13 @@ def unit_of_measurement(self) -> str: @property def device_state_attributes(self): """Return the state attributes of the windspeed sensor.""" - attr = super().device_state_attributes - if ( - hasattr(self._device, "temperatureOffset") - and self._device.temperatureOffset - ): - attr[ATTR_TEMPERATURE_OFFSET] = self._device.temperatureOffset - return attr + state_attr = super().device_state_attributes + + temperature_offset = getattr(self._device, "temperatureOffset", None) + if temperature_offset: + state_attr[ATTR_TEMPERATURE_OFFSET] = temperature_offset + + return state_attr class HomematicipIlluminanceSensor(HomematicipGenericDevice): @@ -307,15 +307,17 @@ def unit_of_measurement(self) -> str: @property def device_state_attributes(self): """Return the state attributes of the wind speed sensor.""" - attr = super().device_state_attributes - if hasattr(self._device, "windDirection") and self._device.windDirection: - attr[ATTR_WIND_DIRECTION] = _get_wind_direction(self._device.windDirection) - if ( - hasattr(self._device, "windDirectionVariation") - and self._device.windDirectionVariation - ): - attr[ATTR_WIND_DIRECTION_VARIATION] = self._device.windDirectionVariation - return attr + state_attr = super().device_state_attributes + + wind_direction = getattr(self._device, "windDirection", None) + if wind_direction: + state_attr[ATTR_WIND_DIRECTION] = _get_wind_direction(wind_direction) + + wind_direction_variation = getattr(self._device, "windDirectionVariation", None) + if wind_direction_variation: + state_attr[ATTR_WIND_DIRECTION_VARIATION] = wind_direction_variation + + return state_attr class HomematicipTodayRainSensor(HomematicipGenericDevice): diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 6d19087781daef..058e21262e3e88 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -113,10 +113,10 @@ def available(self) -> bool: @property def device_state_attributes(self): """Return the state attributes of the switch-group.""" - attr = {} + state_attr = {} if self._device.unreach: - attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True - return attr + state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True + return state_attr async def async_turn_on(self, **kwargs): """Turn the group on.""" diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 463e1bfb7410f1..2d0a69d7d06320 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -79,7 +79,7 @@ def attribution(self) -> str: @property def condition(self) -> str: """Return the current condition.""" - if hasattr(self._device, "raining") and self._device.raining: + if getattr(self._device, "raining", None): return "rainy" if self._device.storm: return "windy" From c847cc20fc17b374370a4fd3e8f4425a30ac56c1 Mon Sep 17 00:00:00 2001 From: zewelor Date: Fri, 6 Sep 2019 20:33:03 +0200 Subject: [PATCH 0197/3953] Add yeelight nightlight support via separate light entity (#26224) * Add yeelight nightligh support via separate light entity * Fix lint too many ancestors * PR fixes --- homeassistant/components/yeelight/__init__.py | 12 ++- homeassistant/components/yeelight/light.py | 100 ++++++++++++++++-- 2 files changed, 104 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 431c34aa06ef80..c899c811a47d35 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -37,6 +37,7 @@ CONF_MODE_MUSIC = "use_music_mode" CONF_FLOW_PARAMS = "flow_params" CONF_CUSTOM_EFFECTS = "custom_effects" +CONF_NIGHTLIGHT_SWITCH_TYPE = "nightlight_switch_type" ATTR_COUNT = "count" ATTR_ACTION = "action" @@ -48,6 +49,8 @@ ACTIVE_MODE_NIGHTLIGHT = "1" +NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" + SCAN_INTERVAL = timedelta(seconds=30) YEELIGHT_RGB_TRANSITION = "RGBTransition" @@ -84,6 +87,9 @@ vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int, vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean, vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean, + vol.Optional(CONF_NIGHTLIGHT_SWITCH_TYPE): vol.Any( + NIGHTLIGHT_SWITCH_TYPE_LIGHT + ), vol.Optional(CONF_MODEL): cv.string, } ) @@ -256,10 +262,12 @@ def type(self): return self._device_type - def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None): + def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None): """Turn on device.""" try: - self.bulb.turn_on(duration=duration, light_type=light_type) + self.bulb.turn_on( + duration=duration, light_type=light_type, power_mode=power_mode + ) except BulbException as ex: _LOGGER.error("Unable to turn the bulb on: %s", ex) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 8601e0e16322c1..8065ff2ae1170f 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -45,6 +45,8 @@ CONF_FLOW_PARAMS, ATTR_ACTION, ATTR_COUNT, + NIGHTLIGHT_SWITCH_TYPE_LIGHT, + CONF_NIGHTLIGHT_SWITCH_TYPE, ) _LOGGER = logging.getLogger(__name__) @@ -177,6 +179,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("Adding %s", device.name) custom_effects = _parse_custom_effects(discovery_info[CONF_CUSTOM_EFFECTS]) + nl_switch_light = ( + discovery_info.get(CONF_NIGHTLIGHT_SWITCH_TYPE) == NIGHTLIGHT_SWITCH_TYPE_LIGHT + ) lights = [] @@ -193,9 +198,17 @@ def _lights_setup_helper(klass): elif device_type == BulbType.Color: _lights_setup_helper(YeelightColorLight) elif device_type == BulbType.WhiteTemp: - _lights_setup_helper(YeelightWhiteTempLight) + if nl_switch_light and device.is_nightlight_supported: + _lights_setup_helper(YeelightWithNightLight) + _lights_setup_helper(YeelightNightLightMode) + else: + _lights_setup_helper(YeelightWhiteTempWithoutNightlightSwitch) elif device_type == BulbType.WhiteTempMood: - _lights_setup_helper(YeelightWithAmbientLight) + if nl_switch_light and device.is_nightlight_supported: + _lights_setup_helper(YeelightNightLightMode) + _lights_setup_helper(YeelightWithAmbientAndNightlight) + else: + _lights_setup_helper(YeelightWithAmbientWithoutNightlight) _lights_setup_helper(YeelightAmbientLight) else: _lights_setup_helper(YeelightGenericLight) @@ -376,6 +389,10 @@ def _brightness_property(self): def _power_property(self): return "power" + @property + def _turn_on_power_mode(self): + return PowerMode.LAST + @property def _predefined_effects(self): return YEELIGHT_MONO_EFFECT_LIST @@ -559,7 +576,11 @@ def turn_on(self, **kwargs) -> None: if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - self.device.turn_on(duration=duration, light_type=self.light_type) + self.device.turn_on( + duration=duration, + light_type=self.light_type, + power_mode=self._turn_on_power_mode, + ) if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: try: @@ -632,7 +653,7 @@ def _predefined_effects(self): return YEELIGHT_COLOR_EFFECT_LIST -class YeelightWhiteTempLight(YeelightGenericLight): +class YeelightWhiteTempLightsupport: """Representation of a Color Yeelight light.""" @property @@ -640,17 +661,84 @@ def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_YEELIGHT_WHITE_TEMP + @property + def _predefined_effects(self): + return YEELIGHT_TEMP_ONLY_EFFECT_LIST + + +class YeelightWhiteTempWithoutNightlightSwitch( + YeelightGenericLight, YeelightWhiteTempLightsupport +): + """White temp light, when nightlight switch is not set to light.""" + @property def _brightness_property(self): return "current_brightness" + +class YeelightWithNightLight(YeelightGenericLight, YeelightWhiteTempLightsupport): + """Representation of a Yeelight with nightlight support. + + It represents case when nightlight switch is set to light. + """ + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return super().is_on and not self.device.is_nightlight_enabled + + @property + def _turn_on_power_mode(self): + return PowerMode.NORMAL + + +class YeelightNightLightMode(YeelightGenericLight): + """Representation of a Yeelight when in nightlight mode.""" + + @property + def name(self) -> str: + """Return the name of the device if any.""" + return f"{self.device.name} nightlight" + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:weather-night" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return super().is_on and self.device.is_nightlight_enabled + + @property + def _brightness_property(self): + return "nl_br" + + @property + def _turn_on_power_mode(self): + return PowerMode.MOONLIGHT + @property def _predefined_effects(self): return YEELIGHT_TEMP_ONLY_EFFECT_LIST -class YeelightWithAmbientLight(YeelightWhiteTempLight): - """Representation of a Yeelight which has ambilight support.""" +class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwitch): + """Representation of a Yeelight which has ambilight support. + + And nightlight switch type is none. + """ + + @property + def _power_property(self): + return "main_power" + + +class YeelightWithAmbientAndNightlight(YeelightWithNightLight): + """Representation of a Yeelight which has ambilight support. + + And nightlight switch type is set to light. + """ @property def _power_property(self): From 9e8f4a589f80ec00532daa09b20ce9edbf61c1fd Mon Sep 17 00:00:00 2001 From: zewelor Date: Fri, 6 Sep 2019 20:46:14 +0200 Subject: [PATCH 0198/3953] Add set scene service calls to yeelight (#26255) * Add set scene service calls to yeelight * Simplify code * DRY valid brightness validation * Fix services description * PR fixes --- homeassistant/components/yeelight/light.py | 215 +++++++++++++++--- .../components/yeelight/services.yaml | 64 +++++- 2 files changed, 248 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 8065ff2ae1170f..171f25128c6640 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -3,9 +3,10 @@ import voluptuous as vol from yeelight import RGBTransition, SleepTransition, Flow, BulbException -from yeelight.enums import PowerMode, LightType, BulbType +from yeelight.enums import PowerMode, LightType, BulbType, SceneClass from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.service import extract_entity_ids +import homeassistant.helpers.config_validation as cv from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, color_temperature_kelvin_to_mired as kelvin_to_mired, @@ -28,6 +29,8 @@ SUPPORT_FLASH, SUPPORT_EFFECT, Light, + ATTR_RGB_COLOR, + ATTR_KELVIN, ) import homeassistant.util.color as color_util from . import ( @@ -51,6 +54,8 @@ _LOGGER = logging.getLogger(__name__) +PLATFORM_DATA_KEY = f"{DATA_YEELIGHT}_lights" + SUPPORT_YEELIGHT = ( SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_EFFECT ) @@ -60,9 +65,15 @@ SUPPORT_YEELIGHT_RGB = SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR ATTR_MODE = "mode" +ATTR_MINUTES = "minutes" SERVICE_SET_MODE = "set_mode" SERVICE_START_FLOW = "start_flow" +SERVICE_SET_COLOR_SCENE = "set_color_scene" +SERVICE_SET_HSV_SCENE = "set_hsv_scene" +SERVICE_SET_COLOR_TEMP_SCENE = "set_color_temp_scene" +SERVICE_SET_COLOR_FLOW_SCENE = "set_color_flow_scene" +SERVICE_SET_AUTO_DELAY_OFF_SCENE = "set_auto_delay_off_scene" EFFECT_DISCO = "Disco" EFFECT_TEMP = "Slow Temp" @@ -123,6 +134,60 @@ "ceiling4": BulbType.WhiteTempMood, } +VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100)) + +SERVICE_SCHEMA_SET_MODE = YEELIGHT_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])} +) + +SERVICE_SCHEMA_START_FLOW = YEELIGHT_SERVICE_SCHEMA.extend( + YEELIGHT_FLOW_TRANSITION_SCHEMA +) + +SERVICE_SCHEMA_SET_COLOR_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( + { + vol.Required(ATTR_RGB_COLOR): vol.All( + vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + ), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + } +) + +SERVICE_SCHEMA_SET_HSV_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( + { + vol.Required(ATTR_HS_COLOR): vol.All( + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=359)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + ) + ), + vol.Coerce(tuple), + ), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + } +) + +SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( + { + vol.Required(ATTR_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1700, max=6500) + ), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + } +) + +SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( + YEELIGHT_FLOW_TRANSITION_SCHEMA +) + +SERVICE_SCHEMA_SET_AUTO_DELAY_OFF = YEELIGHT_SERVICE_SCHEMA.extend( + { + vol.Required(ATTR_MINUTES): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + } +) + def _transitions_config_parser(transitions): """Parse transitions config into initialized objects.""" @@ -167,13 +232,12 @@ def _wrap(self, *args, **kwargs): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight bulbs.""" - data_key = f"{DATA_YEELIGHT}_lights" if not discovery_info: return - if data_key not in hass.data: - hass.data[data_key] = [] + if PLATFORM_DATA_KEY not in hass.data: + hass.data[PLATFORM_DATA_KEY] = [] device = hass.data[DATA_YEELIGHT][discovery_info[CONF_HOST]] _LOGGER.debug("Adding %s", device.name) @@ -218,41 +282,120 @@ def _lights_setup_helper(klass): device.name, ) - hass.data[data_key] += lights + hass.data[PLATFORM_DATA_KEY] += lights add_entities(lights, True) + setup_services(hass) - def service_handler(service): - """Dispatch service calls to target entities.""" - params = { - key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID - } - entity_ids = extract_entity_ids(hass, service) - target_devices = [ - light for light in hass.data[data_key] if light.entity_id in entity_ids - ] - - for target_device in target_devices: - if service.service == SERVICE_SET_MODE: - target_device.set_mode(**params) - elif service.service == SERVICE_START_FLOW: - params[ATTR_TRANSITIONS] = _transitions_config_parser( - params[ATTR_TRANSITIONS] - ) - target_device.start_flow(**params) +def setup_services(hass): + """Set up the service listeners.""" + + def service_call(func): + def service_to_entities(service): + """Return the known entities that a service call mentions.""" + + entity_ids = extract_entity_ids(hass, service) + target_devices = [ + light + for light in hass.data[PLATFORM_DATA_KEY] + if light.entity_id in entity_ids + ] + + return target_devices + + def service_to_params(service): + """Return service call params, without entity_id.""" + return { + key: value + for key, value in service.data.items() + if key != ATTR_ENTITY_ID + } + + def wrapper(service): + params = service_to_params(service) + target_devices = service_to_entities(service) + for device in target_devices: + func(device, params) + + return wrapper - service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])} + @service_call + def service_set_mode(target_device, params): + target_device.set_mode(**params) + + @service_call + def service_start_flow(target_devices, params): + params[ATTR_TRANSITIONS] = _transitions_config_parser(params[ATTR_TRANSITIONS]) + target_devices.start_flow(**params) + + @service_call + def service_set_color_scene(target_device, params): + target_device.set_scene( + SceneClass.COLOR, *[*params[ATTR_RGB_COLOR], params[ATTR_BRIGHTNESS]] + ) + + @service_call + def service_set_hsv_scene(target_device, params): + target_device.set_scene( + SceneClass.HSV, *[*params[ATTR_HS_COLOR], params[ATTR_BRIGHTNESS]] + ) + + @service_call + def service_set_color_temp_scene(target_device, params): + target_device.set_scene( + SceneClass.CT, params[ATTR_KELVIN], params[ATTR_BRIGHTNESS] + ) + + @service_call + def service_set_color_flow_scene(target_device, params): + flow = Flow( + count=params[ATTR_COUNT], + action=Flow.actions[params[ATTR_ACTION]], + transitions=_transitions_config_parser(params[ATTR_TRANSITIONS]), + ) + target_device.set_scene(SceneClass.CF, flow) + + @service_call + def service_set_auto_delay_off_scene(target_device, params): + target_device.set_scene( + SceneClass.AUTO_DELAY_OFF, params[ATTR_BRIGHTNESS], params[ATTR_MINUTES] + ) + + hass.services.register( + DOMAIN, SERVICE_SET_MODE, service_set_mode, schema=SERVICE_SCHEMA_SET_MODE ) hass.services.register( - DOMAIN, SERVICE_SET_MODE, service_handler, schema=service_schema_set_mode + DOMAIN, SERVICE_START_FLOW, service_start_flow, schema=SERVICE_SCHEMA_START_FLOW ) - - service_schema_start_flow = YEELIGHT_SERVICE_SCHEMA.extend( - YEELIGHT_FLOW_TRANSITION_SCHEMA + hass.services.register( + DOMAIN, + SERVICE_SET_COLOR_SCENE, + service_set_color_scene, + schema=SERVICE_SCHEMA_SET_COLOR_SCENE, + ) + hass.services.register( + DOMAIN, + SERVICE_SET_HSV_SCENE, + service_set_hsv_scene, + schema=SERVICE_SCHEMA_SET_HSV_SCENE, ) hass.services.register( - DOMAIN, SERVICE_START_FLOW, service_handler, schema=service_schema_start_flow + DOMAIN, + SERVICE_SET_COLOR_TEMP_SCENE, + service_set_color_temp_scene, + schema=SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE, + ) + hass.services.register( + DOMAIN, + SERVICE_SET_COLOR_FLOW_SCENE, + service_set_color_flow_scene, + schema=SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE, + ) + hass.services.register( + DOMAIN, + SERVICE_SET_AUTO_DELAY_OFF_SCENE, + service_set_auto_delay_off_scene, + schema=SERVICE_SCHEMA_SET_AUTO_DELAY_OFF, ) @@ -639,6 +782,18 @@ def start_flow(self, transitions, count=0, action=ACTION_RECOVER): except BulbException as ex: _LOGGER.error("Unable to set effect: %s", ex) + def set_scene(self, scene_class, *args): + """ + Set the light directly to the specified state. + + If the light is off, it will first be turned on. + """ + try: + self._bulb.set_scene(scene_class, *args) + self.device.update() + except BulbException as ex: + _LOGGER.error("Unable to set scene: %s", ex) + class YeelightColorLight(YeelightGenericLight): """Representation of a Color Yeelight light.""" diff --git a/homeassistant/components/yeelight/services.yaml b/homeassistant/components/yeelight/services.yaml index 14dcfb27a4d54a..52106a42063545 100644 --- a/homeassistant/components/yeelight/services.yaml +++ b/homeassistant/components/yeelight/services.yaml @@ -7,7 +7,69 @@ set_mode: mode: description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'. example: 'moonlight' - +set_color_scene: + description: Changes the light to the specified RGB color and brightness. If the light is off, it will be turned on. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + rgb_color: + description: Color for the light in RGB-format. + example: '[255, 100, 100]' + brightness: + description: The brightness value to set (1-100). + example: 50 +set_hsv_scene: + description: Changes the light to the specified HSV color and brightness. If the light is off, it will be turned on. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + hs_color: + description: Color for the light in hue/sat format. Hue is 0-359 and Sat is 0-100. + example: '[300, 70]' + brightness: + description: The brightness value to set (1-100). + example: 50 +set_color_temp_scene: + description: Changes the light to the specified color temperature. If the light is off, it will be turned on. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + kelvin: + description: Color temperature for the light in Kelvin. + example: 4000 + brightness: + description: The brightness value to set (1-100). + example: 50 +set_color_flow_scene: + description: starts a color flow. If the light is off, it will be turned on. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + count: + description: The number of times to run this flow (0 to run forever). + example: 0 + action: + description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover') + example: 'stay' + transitions: + description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html + example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' +set_auto_delay_off_scene: + description: Turns the light on to the specified brightness and sets a timer to turn it back off after the given number of minutes. If the light is off, Set a color scene, if light is off, it will be turned on. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + minutes: + description: The minutes to wait before automatically turning the light off. + example: 5 + brightness: + description: The brightness value to set (1-100). + example: 50 start_flow: description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects fields: From b3e574d5b2680889f2bd2d64aa2cca2dbd4bb158 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 6 Sep 2019 22:01:23 +0200 Subject: [PATCH 0199/3953] Add device_info to Linky integration (#26477) * Add device_info to Linky integration * Remove useless SENSORS const * Review: remove DOMAIN from the unique_id --- homeassistant/components/linky/sensor.py | 28 ++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index bd2d38735d6192..5ff04c5ee70a38 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -18,6 +18,8 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(hours=4) @@ -28,17 +30,6 @@ INDEX_LAST = -2 ATTRIBUTION = "Data provided by Enedis" -SENSORS = { - "yesterday": ("Linky yesterday", DAILY, INDEX_LAST), - "current_month": ("Linky current month", MONTHLY, INDEX_CURRENT), - "last_month": ("Linky last month", MONTHLY, INDEX_LAST), - "current_year": ("Linky current year", YEARLY, INDEX_CURRENT), - "last_year": ("Linky last year", YEARLY, INDEX_LAST), -} -SENSORS_INDEX_LABEL = 0 -SENSORS_INDEX_SCALE = 1 -SENSORS_INDEX_WHEN = 2 - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up the Linky platform.""" @@ -114,6 +105,12 @@ def __init__(self, name, account: LinkyAccount, scale, when): self._username = account.username self._time = None self._consumption = None + self._unique_id = f"{self._username}_{scale}_{when}" + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id @property def name(self): @@ -144,6 +141,15 @@ def device_state_attributes(self): CONF_USERNAME: self._username, } + @property + def device_info(self): + """Return device information.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Enedis", + } + async def async_update(self) -> None: """Retrieve the new data for the sensor.""" data = self._account.data[self._scale][self._when] From f9445c948824509288f9a6649ca4a6727bd51a0b Mon Sep 17 00:00:00 2001 From: dieselrabbit <37058192+dieselrabbit@users.noreply.github.com> Date: Fri, 6 Sep 2019 13:05:46 -0700 Subject: [PATCH 0200/3953] Update radiotherm climate attributes (#26465) * Update radiotherm for lovelace -Adds hvac_action property to better support the Lovelace Climate card. -Clean up "fan" attribute. Now called "fan_action", as "mode" is supported via hvac_action. -Update current operation label from "Off" to "Idle". * black formatting --- .../components/radiotherm/climate.py | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index f2c3e229c9586a..2585dc0b00bcc4 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -11,6 +11,11 @@ HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, + FAN_ON, + FAN_OFF, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, ) @@ -25,8 +30,7 @@ _LOGGER = logging.getLogger(__name__) -ATTR_FAN = "fan" -ATTR_MODE = "mode" +ATTR_FAN_ACTION = "fan_action" CONF_HOLD_TEMP = "hold_temp" @@ -55,11 +59,11 @@ # Active thermostat state (is it heating or cooling?). In the future # this should probably made into heat and cool binary sensors. -CODE_TO_TEMP_STATE = {0: HVAC_MODE_OFF, 1: HVAC_MODE_HEAT, 2: HVAC_MODE_COOL} +CODE_TO_TEMP_STATE = {0: CURRENT_HVAC_IDLE, 1: CURRENT_HVAC_HEAT, 2: CURRENT_HVAC_COOL} # Active fan state. This is if the fan is actually on or not. In the # future this should probably made into a binary sensor for the fan. -CODE_TO_FAN_STATE = {0: HVAC_MODE_OFF, 1: STATE_ON} +CODE_TO_FAN_STATE = {0: FAN_OFF, 1: FAN_ON} def round_temp(temperature): @@ -160,7 +164,7 @@ def precision(self): @property def device_state_attributes(self): """Return the device specific state attributes.""" - return {ATTR_FAN: self._fstate, ATTR_MODE: self._tstate} + return {ATTR_FAN_ACTION: self._fstate} @property def fan_modes(self): @@ -200,6 +204,13 @@ def hvac_modes(self): """Return the operation modes list.""" return OPERATION_LIST + @property + def hvac_action(self): + """Return the current running hvac operation if supported.""" + if self.hvac_mode == HVAC_MODE_OFF: + return None + return self._tstate + @property def target_temperature(self): """Return the temperature we try to reach.""" @@ -261,9 +272,9 @@ def update(self): # This doesn't really work - tstate is only set if the HVAC is # active. If it's idle, we don't know what to do with the target # temperature. - if self._tstate == HVAC_MODE_COOL: + if self._tstate == CURRENT_HVAC_COOL: self._target_temperature = data["t_cool"] - elif self._tstate == HVAC_MODE_HEAT: + elif self._tstate == CURRENT_HVAC_HEAT: self._target_temperature = data["t_heat"] else: self._current_operation = HVAC_MODE_OFF @@ -281,9 +292,9 @@ def set_temperature(self, **kwargs): elif self._current_operation == HVAC_MODE_HEAT: self.device.t_heat = temperature elif self._current_operation == HVAC_MODE_AUTO: - if self._tstate == HVAC_MODE_COOL: + if self._tstate == CURRENT_HVAC_COOL: self.device.t_cool = temperature - elif self._tstate == HVAC_MODE_HEAT: + elif self._tstate == CURRENT_HVAC_HEAT: self.device.t_heat = temperature # Only change the hold if requested or if hold mode was turned From a72d9da9f44942f90403d56ed3b4807e688a9ff8 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Fri, 6 Sep 2019 22:09:03 +0200 Subject: [PATCH 0201/3953] Add Viessmann ViCare Climate platform (#26151) * Add Viessmann ViCare Climate platform * Add water_heater and fix review comments Update to latest PyVicare (0.1.0) * Move PyVicare API creation to component * More review fixes * Return false if api creation fails * Fix logging format * Update PyVicare 0.1.1 to fix json issues * Formatting fixes --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/vicare/__init__.py | 58 +++++ homeassistant/components/vicare/climate.py | 207 ++++++++++++++++++ homeassistant/components/vicare/manifest.json | 9 + .../components/vicare/water_heater.py | 132 +++++++++++ requirements_all.txt | 3 + 7 files changed, 411 insertions(+) create mode 100644 homeassistant/components/vicare/__init__.py create mode 100644 homeassistant/components/vicare/climate.py create mode 100644 homeassistant/components/vicare/manifest.json create mode 100644 homeassistant/components/vicare/water_heater.py diff --git a/.coveragerc b/.coveragerc index e75c4180ef6e04..e7f155f8923ab5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -692,6 +692,7 @@ omit = homeassistant/components/vesync/const.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py + homeassistant/components/vicare/* homeassistant/components/vizio/media_player.py homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index 0408bcc80320b6..5c9673b0838bac 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -298,6 +298,7 @@ homeassistant/components/velbus/* @cereal2nd homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe +homeassistant/components/vicare/* @oischinger homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf homeassistant/components/waqi/* @andrey-git diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py new file mode 100644 index 00000000000000..9fec04f23283f3 --- /dev/null +++ b/homeassistant/components/vicare/__init__.py @@ -0,0 +1,58 @@ +"""The ViCare integration.""" +import logging + +import voluptuous as vol +from PyViCare.PyViCareDevice import Device + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME +from homeassistant.helpers import discovery + +_LOGGER = logging.getLogger(__name__) + +VICARE_PLATFORMS = ["climate", "water_heater"] + +DOMAIN = "vicare" +VICARE_API = "api" +VICARE_NAME = "name" + +CONF_CIRCUIT = "circuit" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CIRCUIT): int, + vol.Optional(CONF_NAME, default="ViCare"): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Create the ViCare component.""" + conf = config[DOMAIN] + params = {"token_file": "/tmp/vicare_token.save"} + if conf.get(CONF_CIRCUIT) is not None: + params["circuit"] = conf[CONF_CIRCUIT] + + try: + vicare_api = Device(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) + except AttributeError: + _LOGGER.error( + "Failed to create PyViCare API client. Please check your credentials." + ) + return False + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][VICARE_API] = vicare_api + hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME] + + for platform in VICARE_PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py new file mode 100644 index 00000000000000..5727508deb4ac0 --- /dev/null +++ b/homeassistant/components/vicare/climate.py @@ -0,0 +1,207 @@ +"""Viessmann ViCare climate device.""" +import logging + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + PRESET_ECO, + PRESET_COMFORT, + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_AUTO, +) +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE + +from . import DOMAIN as VICARE_DOMAIN +from . import VICARE_API +from . import VICARE_NAME + +_LOGGER = logging.getLogger(__name__) + +VICARE_MODE_DHW = "dhw" +VICARE_MODE_DHWANDHEATING = "dhwAndHeating" +VICARE_MODE_FORCEDREDUCED = "forcedReduced" +VICARE_MODE_FORCEDNORMAL = "forcedNormal" +VICARE_MODE_OFF = "standby" + +VICARE_PROGRAM_ACTIVE = "active" +VICARE_PROGRAM_COMFORT = "comfort" +VICARE_PROGRAM_ECO = "eco" +VICARE_PROGRAM_EXTERNAL = "external" +VICARE_PROGRAM_HOLIDAY = "holiday" +VICARE_PROGRAM_NORMAL = "normal" +VICARE_PROGRAM_REDUCED = "reduced" +VICARE_PROGRAM_STANDBY = "standby" + +VICARE_HOLD_MODE_AWAY = "away" +VICARE_HOLD_MODE_HOME = "home" +VICARE_HOLD_MODE_OFF = "off" + +VICARE_TEMP_HEATING_MIN = 3 +VICARE_TEMP_HEATING_MAX = 37 + +SUPPORT_FLAGS_HEATING = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + +VICARE_TO_HA_HVAC_HEATING = { + VICARE_MODE_DHW: HVAC_MODE_OFF, + VICARE_MODE_DHWANDHEATING: HVAC_MODE_AUTO, + VICARE_MODE_FORCEDREDUCED: HVAC_MODE_OFF, + VICARE_MODE_FORCEDNORMAL: HVAC_MODE_HEAT, + VICARE_MODE_OFF: HVAC_MODE_OFF, +} + +HA_TO_VICARE_HVAC_HEATING = { + HVAC_MODE_HEAT: VICARE_MODE_FORCEDNORMAL, + HVAC_MODE_OFF: VICARE_MODE_FORCEDREDUCED, + HVAC_MODE_AUTO: VICARE_MODE_DHWANDHEATING, +} + +VICARE_TO_HA_PRESET_HEATING = { + VICARE_PROGRAM_COMFORT: PRESET_COMFORT, + VICARE_PROGRAM_ECO: PRESET_ECO, +} + +HA_TO_VICARE_PRESET_HEATING = { + PRESET_COMFORT: VICARE_PROGRAM_COMFORT, + PRESET_ECO: VICARE_PROGRAM_ECO, +} + +PYVICARE_ERROR = "error" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the ViCare climate devices.""" + if discovery_info is None: + return + vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] + add_entities( + [ViCareClimate(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", vicare_api)] + ) + + +class ViCareClimate(ClimateDevice): + """Representation of the ViCare heating climate device.""" + + def __init__(self, name, api): + """Initialize the climate device.""" + self._name = name + self._state = None + self._api = api + self._target_temperature = None + self._current_mode = None + self._current_temperature = None + self._current_program = None + + def update(self): + """Let HA know there has been an update from the ViCare API.""" + _room_temperature = self._api.getRoomTemperature() + if _room_temperature is not None and _room_temperature != "error": + self._current_temperature = _room_temperature + else: + self._current_temperature = self._api.getSupplyTemperature() + self._current_program = self._api.getActiveProgram() + + # The getCurrentDesiredTemperature call can yield 'error' (str) when the system is in standby + desired_temperature = self._api.getCurrentDesiredTemperature() + if desired_temperature == PYVICARE_ERROR: + desired_temperature = None + + self._target_temperature = desired_temperature + + self._current_mode = self._api.getActiveMode() + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS_HEATING + + @property + def name(self): + """Return the name of the climate device.""" + return self._name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def hvac_mode(self): + """Return current hvac mode.""" + return VICARE_TO_HA_HVAC_HEATING.get(self._current_mode) + + def set_hvac_mode(self, hvac_mode): + """Set a new hvac mode on the ViCare API.""" + vicare_mode = HA_TO_VICARE_HVAC_HEATING.get(hvac_mode) + if vicare_mode is None: + _LOGGER.error( + "Cannot set invalid vicare mode: %s / %s", hvac_mode, vicare_mode + ) + return + + _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode) + self._api.setMode(vicare_mode) + + @property + def hvac_modes(self): + """Return the list of available hvac modes.""" + return list(HA_TO_VICARE_HVAC_HEATING) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return VICARE_TEMP_HEATING_MIN + + @property + def max_temp(self): + """Return the maximum temperature.""" + return VICARE_TEMP_HEATING_MAX + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + self._api.setProgramTemperature( + self._current_program, self._target_temperature + ) + + @property + def preset_mode(self): + """Return the current preset mode, e.g., home, away, temp.""" + return VICARE_TO_HA_PRESET_HEATING.get(self._current_program) + + @property + def preset_modes(self): + """Return the available preset mode.""" + return list(VICARE_TO_HA_PRESET_HEATING) + + def set_preset_mode(self, preset_mode): + """Set new preset mode and deactivate any existing programs.""" + vicare_program = HA_TO_VICARE_PRESET_HEATING.get(preset_mode) + if vicare_program is None: + _LOGGER.error( + "Cannot set invalid vicare program: %s / %s", + preset_mode, + vicare_program, + ) + return + + _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) + self._api.deactivateProgram(self._current_program) + self._api.activateProgram(vicare_program) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json new file mode 100644 index 00000000000000..e5f55b20ddaf8d --- /dev/null +++ b/homeassistant/components/vicare/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "vicare", + "name": "Viessmann ViCare", + "documentation": "https://www.home-assistant.io/components/vicare", + "dependencies": [], + "codeowners": ["@oischinger"], + "requirements": ["PyViCare==0.1.1"] +} + diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py new file mode 100644 index 00000000000000..71c0f6c2aefe7a --- /dev/null +++ b/homeassistant/components/vicare/water_heater.py @@ -0,0 +1,132 @@ +"""Viessmann ViCare water_heater device.""" +import logging + +from homeassistant.components.water_heater import ( + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterDevice, +) +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE + +from . import DOMAIN as VICARE_DOMAIN +from . import VICARE_API +from . import VICARE_NAME + +_LOGGER = logging.getLogger(__name__) + +VICARE_MODE_DHW = "dhw" +VICARE_MODE_DHWANDHEATING = "dhwAndHeating" +VICARE_MODE_FORCEDREDUCED = "forcedReduced" +VICARE_MODE_FORCEDNORMAL = "forcedNormal" +VICARE_MODE_OFF = "standby" + +VICARE_TEMP_WATER_MIN = 10 +VICARE_TEMP_WATER_MAX = 60 + +OPERATION_MODE_ON = "on" +OPERATION_MODE_OFF = "off" + +SUPPORT_FLAGS_HEATER = SUPPORT_TARGET_TEMPERATURE + +VICARE_TO_HA_HVAC_DHW = { + VICARE_MODE_DHW: OPERATION_MODE_ON, + VICARE_MODE_DHWANDHEATING: OPERATION_MODE_ON, + VICARE_MODE_FORCEDREDUCED: OPERATION_MODE_OFF, + VICARE_MODE_FORCEDNORMAL: OPERATION_MODE_ON, + VICARE_MODE_OFF: OPERATION_MODE_OFF, +} + +HA_TO_VICARE_HVAC_DHW = { + OPERATION_MODE_OFF: VICARE_MODE_OFF, + OPERATION_MODE_ON: VICARE_MODE_DHW, +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the ViCare water_heater devices.""" + if discovery_info is None: + return + vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] + add_entities( + [ViCareWater(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Water", vicare_api)] + ) + + +class ViCareWater(WaterHeaterDevice): + """Representation of the ViCare domestic hot water device.""" + + def __init__(self, name, api): + """Initialize the DHW water_heater device.""" + self._name = name + self._state = None + self._api = api + self._target_temperature = None + self._current_temperature = None + self._current_mode = None + + def update(self): + """Let HA know there has been an update from the ViCare API.""" + current_temperature = self._api.getDomesticHotWaterStorageTemperature() + if current_temperature is not None and current_temperature != "error": + self._current_temperature = current_temperature + else: + self._current_temperature = None + + self._target_temperature = self._api.getDomesticHotWaterConfiguredTemperature() + + self._current_mode = self._api.getActiveMode() + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS_HEATER + + @property + def name(self): + """Return the name of the water_heater device.""" + return self._name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + self._api.setDomesticHotWaterTemperature(self._target_temperature) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return VICARE_TEMP_WATER_MIN + + @property + def max_temp(self): + """Return the maximum temperature.""" + return VICARE_TEMP_WATER_MAX + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return VICARE_TO_HA_HVAC_DHW.get(self._current_mode) + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return list(HA_TO_VICARE_HVAC_DHW) diff --git a/requirements_all.txt b/requirements_all.txt index 63cd24741250e0..fc07e144b3d017 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -74,6 +74,9 @@ PyRMVtransport==0.1.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 +# homeassistant.components.vicare +PyViCare==0.1.1 + # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.12.4 From 0abb2f3eb8b538bfb150053277310b6426669ec2 Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Fri, 6 Sep 2019 13:21:56 -0700 Subject: [PATCH 0202/3953] Add new integration for Jandy iAqualink pool control (#26034) * Import new iaqualink component with climate platform. * Style and unittest changes, fix async_step_import. * Reorder imports. * Fix stale doctstrings and add unittest. --- .coveragerc | 1 + CODEOWNERS | 1 + .../iaqualink/.translations/en.json | 21 +++ .../components/iaqualink/__init__.py | 103 ++++++++++++ homeassistant/components/iaqualink/climate.py | 150 ++++++++++++++++++ .../components/iaqualink/config_flow.py | 52 ++++++ homeassistant/components/iaqualink/const.py | 5 + .../components/iaqualink/manifest.json | 13 ++ .../components/iaqualink/strings.json | 21 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/iaqualink/__init__.py | 1 + .../components/iaqualink/test_config_flow.py | 77 +++++++++ 15 files changed, 453 insertions(+) create mode 100644 homeassistant/components/iaqualink/.translations/en.json create mode 100644 homeassistant/components/iaqualink/__init__.py create mode 100644 homeassistant/components/iaqualink/climate.py create mode 100644 homeassistant/components/iaqualink/config_flow.py create mode 100644 homeassistant/components/iaqualink/const.py create mode 100644 homeassistant/components/iaqualink/manifest.json create mode 100644 homeassistant/components/iaqualink/strings.json create mode 100644 tests/components/iaqualink/__init__.py create mode 100644 tests/components/iaqualink/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index e7f155f8923ab5..03ee2e4038ca9c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -287,6 +287,7 @@ omit = homeassistant/components/hydrawise/* homeassistant/components/hyperion/light.py homeassistant/components/ialarm/alarm_control_panel.py + homeassistant/components/iaqualink/climate.py homeassistant/components/icloud/device_tracker.py homeassistant/components/idteck_prox/* homeassistant/components/ifttt/* diff --git a/CODEOWNERS b/CODEOWNERS index 5c9673b0838bac..386deb107290de 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -129,6 +129,7 @@ homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop homeassistant/components/huawei_router/* @abmantis homeassistant/components/hue/* @balloob +homeassistant/components/iaqualink/* @flz homeassistant/components/ign_sismologia/* @exxamalte homeassistant/components/incomfort/* @zxdavb homeassistant/components/influxdb/* @fabaff diff --git a/homeassistant/components/iaqualink/.translations/en.json b/homeassistant/components/iaqualink/.translations/en.json new file mode 100644 index 00000000000000..4c706522198c9f --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Jandy iAqualink", + "step": { + "user": { + "title": "Connect to iAqualink", + "description": "Please enter the username and password for your iAqualink account.", + "data": { + "username": "Username / Email Address", + "password": "Password" + } + } + }, + "error": { + "connection_failure": "Unable to connect to iAqualink. Check your username and password." + }, + "abort": { + "already_setup": "You can only configure a single iAqualink connection." + } + } +} diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py new file mode 100644 index 00000000000000..3f171715c57684 --- /dev/null +++ b/homeassistant/components/iaqualink/__init__.py @@ -0,0 +1,103 @@ +"""Component to embed Aqualink devices.""" +import asyncio +import logging + +from aiohttp import CookieJar +import voluptuous as vol + +from iaqualink import AqualinkClient, AqualinkLoginException, AqualinkThermostat + +from homeassistant import config_entries +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_create_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import DOMAIN + + +_LOGGER = logging.getLogger(__name__) + +ATTR_CONFIG = "config" +PARALLEL_UPDATES = 0 + +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: HomeAssistantType, config: ConfigType) -> None: + """Set up the Aqualink component.""" + conf = config.get(DOMAIN) + + hass.data[DOMAIN] = {} + + if conf is not None: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None: + """Set up Aqualink from a config entry.""" + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + + # These will contain the initialized devices + climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = [] + + session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) + aqualink = AqualinkClient(username, password, session) + try: + await aqualink.login() + except AqualinkLoginException as login_exception: + _LOGGER.error("Exception raised while attempting to login: %s", login_exception) + return False + + systems = await aqualink.get_systems() + systems = list(systems.values()) + if not systems: + _LOGGER.error("No systems detected or supported") + return False + + # Only supporting the first system for now. + devices = await systems[0].get_devices() + + for dev in devices.values(): + if isinstance(dev, AqualinkThermostat): + climates += [dev] + + forward_setup = hass.config_entries.async_forward_entry_setup + if climates: + _LOGGER.debug("Got %s climates: %s", len(climates), climates) + hass.async_create_task(forward_setup(entry, CLIMATE_DOMAIN)) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + forward_unload = hass.config_entries.async_forward_entry_unload + + tasks = [] + + if hass.data[DOMAIN][CLIMATE_DOMAIN]: + tasks += [forward_unload(entry, CLIMATE_DOMAIN)] + + hass.data[DOMAIN].clear() + + return all(await asyncio.gather(*tasks)) diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py new file mode 100644 index 00000000000000..0a30517623d46b --- /dev/null +++ b/homeassistant/components/iaqualink/climate.py @@ -0,0 +1,150 @@ +"""Support for Aqualink Thermostats.""" +import logging +from typing import List, Optional + +from iaqualink import ( + AqualinkState, + AqualinkHeater, + AqualinkPump, + AqualinkSensor, + AqualinkThermostat, +) +from iaqualink.const import ( + AQUALINK_TEMP_CELSIUS_HIGH, + AQUALINK_TEMP_CELSIUS_LOW, + AQUALINK_TEMP_FAHRENHEIT_HIGH, + AQUALINK_TEMP_FAHRENHEIT_LOW, +) + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + DOMAIN, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DOMAIN as AQUALINK_DOMAIN, CLIMATE_SUPPORTED_MODES + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up discovered switches.""" + devs = [] + for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: + devs.append(HassAqualinkThermostat(dev)) + async_add_entities(devs, True) + + +class HassAqualinkThermostat(ClimateDevice): + """Representation of a thermostat.""" + + def __init__(self, dev: AqualinkThermostat): + """Initialize the thermostat.""" + self.dev = dev + + @property + def name(self) -> str: + """Return the name of the thermostat.""" + return self.dev.label.split(" ")[0] + + async def async_update(self) -> None: + """Update the internal state of the thermostat. + + The API update() command refreshes the state of all devices so we + only update if this is the main thermostat to avoid unnecessary + calls. + """ + if self.name != "Pool": + return + await self.dev.system.update() + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def hvac_modes(self) -> List[str]: + """Return the list of supported HVAC modes.""" + return CLIMATE_SUPPORTED_MODES + + @property + def pump(self) -> AqualinkPump: + """Return the pump device for the current thermostat.""" + pump = f"{self.name.lower()}_pump" + return self.dev.system.devices[pump] + + @property + def hvac_mode(self) -> str: + """Return the current HVAC mode.""" + state = AqualinkState(self.heater.state) + if state == AqualinkState.ON: + return HVAC_MODE_HEAT + return HVAC_MODE_OFF + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Turn the underlying heater switch on or off.""" + if hvac_mode == HVAC_MODE_HEAT: + await self.heater.turn_on() + elif hvac_mode == HVAC_MODE_OFF: + await self.heater.turn_off() + else: + _LOGGER.warning("Unknown operation mode: %s", hvac_mode) + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + if self.dev.system.temp_unit == "F": + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + @property + def min_temp(self) -> int: + """Return the minimum temperature supported by the thermostat.""" + if self.temperature_unit == TEMP_FAHRENHEIT: + return AQUALINK_TEMP_FAHRENHEIT_LOW + return AQUALINK_TEMP_CELSIUS_LOW + + @property + def max_temp(self) -> int: + """Return the minimum temperature supported by the thermostat.""" + if self.temperature_unit == TEMP_FAHRENHEIT: + return AQUALINK_TEMP_FAHRENHEIT_HIGH + return AQUALINK_TEMP_CELSIUS_HIGH + + @property + def target_temperature(self) -> float: + """Return the current target temperature.""" + return float(self.dev.state) + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + await self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE])) + + @property + def sensor(self) -> AqualinkSensor: + """Return the sensor device for the current thermostat.""" + sensor = f"{self.name.lower()}_temp" + return self.dev.system.devices[sensor] + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + if self.sensor.state != "": + return float(self.sensor.state) + return None + + @property + def heater(self) -> AqualinkHeater: + """Return the heater device for the current thermostat.""" + heater = f"{self.name.lower()}_heater" + return self.dev.system.devices[heater] diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py new file mode 100644 index 00000000000000..ec83477d253a04 --- /dev/null +++ b/homeassistant/components/iaqualink/config_flow.py @@ -0,0 +1,52 @@ +"""Config flow to configure zone component.""" +from typing import Optional + +import voluptuous as vol + +from iaqualink import AqualinkClient, AqualinkLoginException + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import ConfigType + +from .const import DOMAIN + + +@config_entries.HANDLERS.register(DOMAIN) +class AqualinkFlowHandler(config_entries.ConfigFlow): + """Aqualink config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input: Optional[ConfigType] = None): + """Handle a flow start.""" + # Supporting a single account. + entries = self.hass.config_entries.async_entries(DOMAIN) + if entries: + return self.async_abort(reason="already_setup") + + errors = {} + + if user_input is not None: + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + try: + aqualink = AqualinkClient(username, password) + await aqualink.login() + return self.async_create_entry(title=username, data=user_input) + except AqualinkLoginException: + errors["base"] = "connection_failure" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ), + errors=errors, + ) + + async def async_step_import(self, user_input: Optional[ConfigType] = None): + """Occurs when an entry is setup through config.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/iaqualink/const.py b/homeassistant/components/iaqualink/const.py new file mode 100644 index 00000000000000..ebdcd365194e6d --- /dev/null +++ b/homeassistant/components/iaqualink/const.py @@ -0,0 +1,5 @@ +"""Constants for the the iaqualink component.""" +from homeassistant.components.climate.const import HVAC_MODE_HEAT, HVAC_MODE_OFF + +DOMAIN = "iaqualink" +CLIMATE_SUPPORTED_MODES = [HVAC_MODE_HEAT, HVAC_MODE_OFF] diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json new file mode 100644 index 00000000000000..25e02536897915 --- /dev/null +++ b/homeassistant/components/iaqualink/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "iaqualink", + "name": "Jandy iAqualink", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/iaqualink/", + "dependencies": [], + "codeowners": [ + "@flz" + ], + "requirements": [ + "iaqualink==0.2.9" + ] +} diff --git a/homeassistant/components/iaqualink/strings.json b/homeassistant/components/iaqualink/strings.json new file mode 100644 index 00000000000000..4c706522198c9f --- /dev/null +++ b/homeassistant/components/iaqualink/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Jandy iAqualink", + "step": { + "user": { + "title": "Connect to iAqualink", + "description": "Please enter the username and password for your iAqualink account.", + "data": { + "username": "Username / Email Address", + "password": "Password" + } + } + }, + "error": { + "connection_failure": "Unable to connect to iAqualink. Check your username and password." + }, + "abort": { + "already_setup": "You can only configure a single iAqualink connection." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 32690153221b65..1dffe2d8e6bd82 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -24,6 +24,7 @@ "homekit_controller", "homematicip_cloud", "hue", + "iaqualink", "ifttt", "ios", "ipma", diff --git a/requirements_all.txt b/requirements_all.txt index fc07e144b3d017..08641c38b8e5f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -663,6 +663,9 @@ hydrawiser==0.1.1 # homeassistant.components.htu21d # i2csense==0.0.4 +# homeassistant.components.iaqualink +iaqualink==0.2.9 + # homeassistant.components.watson_tts ibm-watson==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e0e0d2a3ea9d4..de5cdaa9da4f9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -194,6 +194,9 @@ httplib2==0.10.3 # homeassistant.components.huawei_lte huawei-lte-api==1.3.0 +# homeassistant.components.iaqualink +iaqualink==0.2.9 + # homeassistant.components.influxdb influxdb==5.2.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1468969d9dd719..7c49055131b7d4 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -93,6 +93,7 @@ "homematicip", "httplib2", "huawei-lte-api", + "iaqualink", "influxdb", "jsonpath", "libpurecool", diff --git a/tests/components/iaqualink/__init__.py b/tests/components/iaqualink/__init__.py new file mode 100644 index 00000000000000..c4e3b75d0ae6b2 --- /dev/null +++ b/tests/components/iaqualink/__init__.py @@ -0,0 +1 @@ +"""Tests for the iAqualink component.""" diff --git a/tests/components/iaqualink/test_config_flow.py b/tests/components/iaqualink/test_config_flow.py new file mode 100644 index 00000000000000..5c4d75ee3c155f --- /dev/null +++ b/tests/components/iaqualink/test_config_flow.py @@ -0,0 +1,77 @@ +"""Tests for iAqualink config flow.""" +from unittest.mock import patch + +import iaqualink +import pytest + +from homeassistant.components.iaqualink import config_flow +from tests.common import MockConfigEntry, mock_coro + +DATA = {"username": "test@example.com", "password": "pass"} + + +@pytest.mark.parametrize("step", ["import", "user"]) +async def test_already_configured(hass, step): + """Test config flow when iaqualink component is already setup.""" + MockConfigEntry(domain="iaqualink", data=DATA).add_to_hass(hass) + + flow = config_flow.AqualinkFlowHandler() + flow.hass = hass + flow.context = {} + + fname = f"async_step_{step}" + func = getattr(flow, fname) + result = await func(DATA) + + assert result["type"] == "abort" + + +@pytest.mark.parametrize("step", ["import", "user"]) +async def test_without_config(hass, step): + """Test with no configuration.""" + flow = config_flow.AqualinkFlowHandler() + flow.hass = hass + flow.context = {} + + fname = f"async_step_{step}" + func = getattr(flow, fname) + result = await func() + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + +@pytest.mark.parametrize("step", ["import", "user"]) +async def test_with_invalid_credentials(hass, step): + """Test config flow with invalid username and/or password.""" + flow = config_flow.AqualinkFlowHandler() + flow.hass = hass + + fname = f"async_step_{step}" + func = getattr(flow, fname) + with patch( + "iaqualink.AqualinkClient.login", side_effect=iaqualink.AqualinkLoginException + ): + result = await func(DATA) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "connection_failure"} + + +@pytest.mark.parametrize("step", ["import", "user"]) +async def test_with_existing_config(hass, step): + """Test with existing configuration.""" + flow = config_flow.AqualinkFlowHandler() + flow.hass = hass + flow.context = {} + + fname = f"async_step_{step}" + func = getattr(flow, fname) + with patch("iaqualink.AqualinkClient.login", return_value=mock_coro(None)): + result = await func(DATA) + + assert result["type"] == "create_entry" + assert result["title"] == DATA["username"] + assert result["data"] == DATA From 48dea595178d63b9d42d39be7cb9d2d02aadcfe1 Mon Sep 17 00:00:00 2001 From: Magnus Brange Date: Fri, 6 Sep 2019 22:28:31 +0200 Subject: [PATCH 0203/3953] Add protocol and model as an optional restriction for tellstick sensors (#26026) * Add protocl and model as restriction for tellstick sensors A tellstick sensors is uniq identified by id, protocol and model, not just the id. This will add an optional restriction for "named sensors" for protocol and model. * Don't default config to empty string * Compare None with 'is not' --- homeassistant/components/tellstick/sensor.py | 35 ++++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 83b56c2cf394d8..98d162d6d81b5d 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_ID, CONF_NAME +from homeassistant.const import TEMP_CELSIUS, CONF_ID, CONF_NAME, CONF_PROTOCOL from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -16,6 +16,7 @@ CONF_DATATYPE_MASK = "datatype_mask" CONF_ONLY_NAMED = "only_named" CONF_TEMPERATURE_SCALE = "temperature_scale" +CONF_MODEL = "model" DEFAULT_DATATYPE_MASK = 127 DEFAULT_TEMPERATURE_SCALE = TEMP_CELSIUS @@ -35,6 +36,8 @@ { vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_PROTOCOL): cv.string, + vol.Optional(CONF_MODEL): cv.string, } ) ], @@ -74,18 +77,36 @@ def setup_platform(hass, config, add_entities, discovery_info=None): datatype_mask = config.get(CONF_DATATYPE_MASK) if config[CONF_ONLY_NAMED]: - named_sensors = { - named_sensor[CONF_ID]: named_sensor[CONF_NAME] - for named_sensor in config[CONF_ONLY_NAMED] - } + named_sensors = {} + for named_sensor in config[CONF_ONLY_NAMED]: + name = named_sensor[CONF_NAME] + proto = named_sensor.get(CONF_PROTOCOL) + model = named_sensor.get(CONF_MODEL) + id_ = named_sensor[CONF_ID] + if proto is not None: + if model is not None: + named_sensors["{}{}{}".format(proto, model, id_)] = name + else: + named_sensors["{}{}".format(proto, id_)] = name + else: + named_sensors[id_] = name for tellcore_sensor in tellcore_lib.sensors(): if not config[CONF_ONLY_NAMED]: sensor_name = str(tellcore_sensor.id) else: - if tellcore_sensor.id not in named_sensors: + proto_id = "{}{}".format(tellcore_sensor.protocol, tellcore_sensor.id) + proto_model_id = "{}{}{}".format( + tellcore_sensor.protocol, tellcore_sensor.model, tellcore_sensor.id + ) + if tellcore_sensor.id in named_sensors: + sensor_name = named_sensors[tellcore_sensor.id] + elif proto_id in named_sensors: + sensor_name = named_sensors[proto_id] + elif proto_model_id in named_sensors: + sensor_name = named_sensors[proto_model_id] + else: continue - sensor_name = named_sensors[tellcore_sensor.id] for datatype in sensor_value_descriptions: if datatype & datatype_mask and tellcore_sensor.has_value(datatype): From a5ccb03e2eb8dad11ea2366919bff917530ddb4a Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 7 Sep 2019 01:38:09 +0200 Subject: [PATCH 0204/3953] Fix KNX light tunable white rounding error (#26364) * KNX light - tunable white rounding error mitigate rounding errors in kelvin - mired conversion for lights with relative color temperature fixes https://github.com/home-assistant/home-assistant/issues/26357 * typo _min_kelvin <> _max_kelvin * black --- homeassistant/components/knx/light.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index c04feed23374fa..71a82c6df2a6eb 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -305,7 +305,10 @@ async def async_turn_on(self, **kwargs): await self.device.set_color_temperature(kelvin) elif self.device.supports_tunable_white and update_color_temp: # calculate relative_ct from Kelvin to fit typical KNX devices - kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) + kelvin = min( + self._max_kelvin, + int(color_util.color_temperature_mired_to_kelvin(mireds)), + ) relative_ct = int( 255 * (kelvin - self._min_kelvin) From 0b1f389c76df841baa1571bbacb2b5d3c1f5aa7e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 7 Sep 2019 00:32:45 +0000 Subject: [PATCH 0205/3953] [ci skip] Translation update --- .../components/adguard/.translations/it.json | 15 ++++++-- .../ambiclimate/.translations/it.json | 17 ++++++++- .../ambient_station/.translations/it.json | 3 +- .../arcam_fmj/.translations/it.json | 5 +++ .../components/auth/.translations/it.json | 4 +- .../components/axis/.translations/it.json | 1 + .../cert_expiry/.translations/it.json | 24 ++++++++++++ .../components/deconz/.translations/it.json | 22 ++++++++++- .../components/deconz/.translations/nl.json | 7 ++++ .../emulated_roku/.translations/it.json | 6 ++- .../components/esphome/.translations/it.json | 2 +- .../geonetnz_quakes/.translations/it.json | 17 +++++++++ .../components/hangouts/.translations/it.json | 2 + .../homekit_controller/.translations/it.json | 5 +++ .../homematicip_cloud/.translations/it.json | 2 +- .../components/hue/.translations/it.json | 10 +++-- .../iaqualink/.translations/en.json | 38 +++++++++---------- .../components/ifttt/.translations/it.json | 2 +- .../components/iqvia/.translations/it.json | 1 + .../components/life360/.translations/it.json | 4 +- .../components/light/.translations/it.json | 17 +++++++++ .../components/light/.translations/nl.json | 17 +++++++++ .../components/light/.translations/ru.json | 9 +++++ .../components/linky/.translations/it.json | 25 ++++++++++++ .../components/linky/.translations/nl.json | 25 ++++++++++++ .../logi_circle/.translations/it.json | 3 +- .../components/met/.translations/it.json | 20 ++++++++++ .../mobile_app/.translations/it.json | 4 +- .../components/notion/.translations/it.json | 19 ++++++++++ .../owntracks/.translations/it.json | 3 ++ .../components/plaato/.translations/it.json | 18 +++++++++ .../components/point/.translations/it.json | 1 + .../components/ps4/.translations/it.json | 9 +++-- .../smartthings/.translations/it.json | 2 + .../components/somfy/.translations/it.json | 13 +++++++ .../tellduslive/.translations/it.json | 2 + .../components/toon/.translations/it.json | 2 + .../components/traccar/.translations/it.json | 18 +++++++++ .../components/tradfri/.translations/it.json | 3 +- .../twentemilieu/.translations/it.json | 23 +++++++++++ .../components/unifi/.translations/it.json | 18 +++++++++ .../components/upnp/.translations/it.json | 4 ++ .../components/velbus/.translations/it.json | 21 ++++++++++ .../components/vesync/.translations/it.json | 20 ++++++++++ .../components/withings/.translations/it.json | 17 +++++++++ .../components/withings/.translations/nl.json | 17 +++++++++ .../components/wwlln/.translations/it.json | 18 +++++++++ 47 files changed, 491 insertions(+), 44 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/it.json create mode 100644 homeassistant/components/cert_expiry/.translations/it.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/it.json create mode 100644 homeassistant/components/light/.translations/it.json create mode 100644 homeassistant/components/light/.translations/nl.json create mode 100644 homeassistant/components/linky/.translations/it.json create mode 100644 homeassistant/components/linky/.translations/nl.json create mode 100644 homeassistant/components/met/.translations/it.json create mode 100644 homeassistant/components/notion/.translations/it.json create mode 100644 homeassistant/components/plaato/.translations/it.json create mode 100644 homeassistant/components/somfy/.translations/it.json create mode 100644 homeassistant/components/traccar/.translations/it.json create mode 100644 homeassistant/components/twentemilieu/.translations/it.json create mode 100644 homeassistant/components/velbus/.translations/it.json create mode 100644 homeassistant/components/vesync/.translations/it.json create mode 100644 homeassistant/components/withings/.translations/it.json create mode 100644 homeassistant/components/withings/.translations/nl.json create mode 100644 homeassistant/components/wwlln/.translations/it.json diff --git a/homeassistant/components/adguard/.translations/it.json b/homeassistant/components/adguard/.translations/it.json index 6cd8767334dc9f..57f81dc1d99ad5 100644 --- a/homeassistant/components/adguard/.translations/it.json +++ b/homeassistant/components/adguard/.translations/it.json @@ -1,21 +1,30 @@ { "config": { "abort": { + "existing_instance_updated": "Configurazione esistente aggiornata.", "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home." }, "error": { "connection_error": "Impossibile connettersi." }, "step": { + "hassio_confirm": { + "description": "Vuoi configurare Home Assistant per connettersi alla AdGuard Home fornita dal componente aggiuntivo di Hass.io: {addon} ?", + "title": "AdGuard Home tramite il componente aggiuntivo di Hass.io" + }, "user": { "data": { "host": "Host", "password": "Password", "port": "Porta", "ssl": "AdGuard Home utilizza un certificato SSL", - "username": "Nome utente" - } + "username": "Nome utente", + "verify_ssl": "AdGuard Home utilizza un certificato appropriato" + }, + "description": "Configura l'istanza di AdGuard Home per consentire il monitoraggio e il controllo.", + "title": "Collega la tua AdGuard Home." } - } + }, + "title": "AdGuard Home" } } \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/it.json b/homeassistant/components/ambiclimate/.translations/it.json index b062eb67c1ffea..a13874b36764d5 100644 --- a/homeassistant/components/ambiclimate/.translations/it.json +++ b/homeassistant/components/ambiclimate/.translations/it.json @@ -1,7 +1,22 @@ { "config": { "abort": { - "already_setup": "L'account Ambiclimate \u00e8 configurato." + "access_token": "Errore sconosciuto durante la generazione di un token di accesso.", + "already_setup": "L'account Ambiclimate \u00e8 configurato.", + "no_config": "\u00c8 necessario configurare Ambiclimate prima di poter eseguire l'autenticazione con esso. [Leggere le istruzioni] (https://www.home-assistant.io/components/ambiclimate/)." + }, + "create_entry": { + "default": "Autenticato con successo con Ambiclimate" + }, + "error": { + "follow_link": "Si prega di seguire il link e di autenticarsi prima di premere Invia", + "no_token": "Non autenticato con Ambiclimate" + }, + "step": { + "auth": { + "description": "Segui questo [link]({authorization_url}) e Consenti accesso al tuo account Ambiclimate, quindi torna indietro e premi Invia qui sotto. \n (Assicurati che l'URL di richiamata specificato sia {cb_url})", + "title": "Autenticare Ambiclimate" + } }, "title": "Ambiclimate" } diff --git a/homeassistant/components/ambient_station/.translations/it.json b/homeassistant/components/ambient_station/.translations/it.json index f87c987a79fba4..b468ba3673cd5d 100644 --- a/homeassistant/components/ambient_station/.translations/it.json +++ b/homeassistant/components/ambient_station/.translations/it.json @@ -13,6 +13,7 @@ }, "title": "Inserisci i tuoi dati" } - } + }, + "title": "PWS ambientale" } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/it.json b/homeassistant/components/arcam_fmj/.translations/it.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/it.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/it.json b/homeassistant/components/auth/.translations/it.json index be06f0209c409d..dbfe4acd6156ab 100644 --- a/homeassistant/components/auth/.translations/it.json +++ b/homeassistant/components/auth/.translations/it.json @@ -10,7 +10,7 @@ "step": { "init": { "description": "Selezionare uno dei servizi di notifica:", - "title": "Imposta la password one-time fornita dal componente di notifica" + "title": "Imposta la password monouso fornita dal componente di notifica" }, "setup": { "description": "\u00c8 stata inviata una password monouso tramite **notify.{notify_service}**. Per favore, inseriscila qui sotto:", @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "Per attivare l'autenticazione a due fattori utilizzando password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Dopo aver scansionato il codice, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con codice ** ` {code} ` **.", + "description": "Per attivare l'autenticazione a due fattori utilizzando le password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \nDopo la scansione, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con il codice ** ` {code} ` **.", "title": "Imposta l'autenticazione a due fattori usando TOTP" } }, diff --git a/homeassistant/components/axis/.translations/it.json b/homeassistant/components/axis/.translations/it.json index 2498c28ec33aca..e979af0883656f 100644 --- a/homeassistant/components/axis/.translations/it.json +++ b/homeassistant/components/axis/.translations/it.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "bad_config_file": "Dati errati dal file di configurazione", + "link_local_address": "Gli indirizzi locali di collegamento non sono supportati", "not_axis_device": "Il dispositivo rilevato non \u00e8 un dispositivo Axis" }, "error": { diff --git a/homeassistant/components/cert_expiry/.translations/it.json b/homeassistant/components/cert_expiry/.translations/it.json new file mode 100644 index 00000000000000..9135ed3b478592 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata" + }, + "error": { + "certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta", + "connection_timeout": "Tempo scaduto durante la connessione a questo host", + "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", + "resolve_failed": "Questo host non pu\u00f2 essere risolto" + }, + "step": { + "user": { + "data": { + "host": "L'hostname del certificato", + "name": "Il nome del certificato", + "port": "La porta del certificato" + }, + "title": "Definire il certificato da testare" + } + }, + "title": "Scadenza certificato" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index f15a2ddf26536a..9836a43f8a84b7 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione per bridge \u00e8 gi\u00e0 in corso.", "no_bridges": "Nessun bridge deCONZ rilevato", + "not_deconz_bridge": "Non \u00e8 un bridge deCONZ", "one_instance_only": "Il componente supporto solo un'istanza di deCONZ", "updated_instance": "Istanza deCONZ aggiornata con nuovo indirizzo host" }, @@ -26,7 +28,7 @@ "title": "Definisci il gateway deCONZ" }, "link": { - "description": "Sblocca il tuo gateway deCONZ per registrarlo in Home Assistant.\n\n1. Vai nelle impostazioni di sistema di deCONZ\n2. Premi il bottone \"Unlock Gateway\"", + "description": "Sblocca il tuo gateway deCONZ per registrarti con Home Assistant.\n\n1. Vai a Impostazioni deCONZ -> Gateway -> Avanzate\n2. Premere il pulsante \"Autentica app\"", "title": "Collega con deCONZ" }, "options": { @@ -38,5 +40,23 @@ } }, "title": "Gateway Zigbee deCONZ" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Consentire sensori CLIP deCONZ", + "allow_deconz_groups": "Consentire gruppi luce deCONZ" + }, + "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Consentire sensori CLIP deCONZ", + "allow_deconz_groups": "Consentire gruppi luce deCONZ" + }, + "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 785fba4ffc0e02..f9f2d40488f6a4 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -49,6 +49,13 @@ "allow_deconz_groups": "DeCONZ-lichtgroepen toestaan" }, "description": "De zichtbaarheid van deCONZ-apparaattypen configureren" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "DeCONZ CLIP sensoren toestaan", + "allow_deconz_groups": "Sta deCONZ-lichtgroepen toe" + }, + "description": "Configureer de zichtbaarheid van deCONZ-apparaattypen" } } } diff --git a/homeassistant/components/emulated_roku/.translations/it.json b/homeassistant/components/emulated_roku/.translations/it.json index cba89add799481..8f39309264a6ca 100644 --- a/homeassistant/components/emulated_roku/.translations/it.json +++ b/homeassistant/components/emulated_roku/.translations/it.json @@ -6,8 +6,12 @@ "step": { "user": { "data": { + "advertise_ip": "Pubblicizza IP", + "advertise_port": "Pubblicizza porta", "host_ip": "Indirizzo IP dell'host", - "name": "Nome" + "listen_port": "Porta di ascolto", + "name": "Nome", + "upnp_bind_multicast": "Associa multicast (Vero / Falso)" }, "title": "Definisci la configurazione del server" } diff --git a/homeassistant/components/esphome/.translations/it.json b/homeassistant/components/esphome/.translations/it.json index b9088c2eadc34e..bb77e87f6a1c38 100644 --- a/homeassistant/components/esphome/.translations/it.json +++ b/homeassistant/components/esphome/.translations/it.json @@ -18,7 +18,7 @@ "title": "Inserisci la password" }, "discovery_confirm": { - "description": "Vuoi aggiungere il nodo ESPHome ` {name} ` a Home Assistant?", + "description": "Vuoi aggiungere il nodo ESPHome `{name}` a Home Assistant?", "title": "Trovato nodo ESPHome" }, "user": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/it.json b/homeassistant/components/geonetnz_quakes/.translations/it.json new file mode 100644 index 00000000000000..a6fd4660a1551d --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Localit\u00e0 gi\u00e0 registrata" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Raggio" + }, + "title": "Inserisci i tuoi dettagli del filtro." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/it.json b/homeassistant/components/hangouts/.translations/it.json index ad8dafd17ec74a..6983d190097c4b 100644 --- a/homeassistant/components/hangouts/.translations/it.json +++ b/homeassistant/components/hangouts/.translations/it.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA Pin" }, + "description": "Vuoto", "title": "Autenticazione a due fattori" }, "user": { @@ -22,6 +23,7 @@ "email": "Indirizzo email", "password": "Password" }, + "description": "Vuoto", "title": "Accesso a Google Hangouts" } }, diff --git a/homeassistant/components/homekit_controller/.translations/it.json b/homeassistant/components/homekit_controller/.translations/it.json index a1d460d12dcfa8..a1e9f82144ef62 100644 --- a/homeassistant/components/homekit_controller/.translations/it.json +++ b/homeassistant/components/homekit_controller/.translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "accessory_not_found_error": "Impossibile aggiungere l'associazione in quanto non \u00e8 pi\u00f9 possibile trovare il dispositivo.", "already_configured": "L'accessorio \u00e8 gi\u00e0 configurato con questo controller.", "already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.", "already_paired": "Questo accessorio \u00e8 gi\u00e0 associato a un altro dispositivo. Si prega di resettare l'accessorio e riprovare.", @@ -10,6 +11,10 @@ }, "error": { "authentication_error": "Codice HomeKit errato. Per favore, controllate e riprovate.", + "busy_error": "Il dispositivo ha rifiutato di aggiungere l'associazione in quanto \u00e8 gi\u00e0 associato a un altro controller.", + "max_peers_error": "Il dispositivo ha rifiutato di aggiungere l'associazione in quanto non dispone di una memoria libera per l'associazione.", + "max_tries_error": "Il dispositivo ha rifiutato di aggiungere l'associazione poich\u00e9 ha ricevuto pi\u00f9 di 100 tentativi di autenticazione non riusciti.", + "pairing_failed": "Si \u00e8 verificato un errore non gestito durante il tentativo di associazione con questo dispositivo. Potrebbe trattarsi di un errore temporaneo o il dispositivo potrebbe non essere attualmente supportato.", "unable_to_pair": "Impossibile abbinare, per favore riprova.", "unknown_error": "Il dispositivo ha riportato un errore sconosciuto. L'abbinamento non \u00e8 riuscito." }, diff --git a/homeassistant/components/homematicip_cloud/.translations/it.json b/homeassistant/components/homematicip_cloud/.translations/it.json index 6e6d7c8a59fe06..c7f1af21f2270a 100644 --- a/homeassistant/components/homematicip_cloud/.translations/it.json +++ b/homeassistant/components/homematicip_cloud/.translations/it.json @@ -15,7 +15,7 @@ "init": { "data": { "hapid": "ID del punto di accesso (SGTIN)", - "name": "Nome (facoltativo, utilizzato come prefisso del nome per tutti i dispositivi)", + "name": "Nome (opzionale, usato come prefisso del nome per tutti i dispositivi)", "pin": "Codice Pin (opzionale)" }, "title": "Scegli punto di accesso HomematicIP" diff --git a/homeassistant/components/hue/.translations/it.json b/homeassistant/components/hue/.translations/it.json index 72b2fd6445bf37..5dd64364c1080f 100644 --- a/homeassistant/components/hue/.translations/it.json +++ b/homeassistant/components/hue/.translations/it.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati", - "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato", + "all_configured": "Tutti i bridge di Philips Hue sono gi\u00e0 configurati", + "already_configured": "Il bridge \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione per bridge \u00e8 gi\u00e0 in corso.", "cannot_connect": "Impossibile connettersi al bridge", "discover_timeout": "Impossibile trovare i bridge Hue", - "no_bridges": "Nessun bridge Hue di Philips trovato", + "no_bridges": "Nessun bridge di Philips Hue trovato", + "not_hue_bridge": "Non \u00e8 un bridge Hue", "unknown": "Si \u00e8 verificato un errore" }, "error": { @@ -24,6 +26,6 @@ "title": "Collega Hub" } }, - "title": "Philips Hue Bridge" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/en.json b/homeassistant/components/iaqualink/.translations/en.json index 4c706522198c9f..4972c3d3ff7d06 100644 --- a/homeassistant/components/iaqualink/.translations/en.json +++ b/homeassistant/components/iaqualink/.translations/en.json @@ -1,21 +1,21 @@ { - "config": { - "title": "Jandy iAqualink", - "step": { - "user": { - "title": "Connect to iAqualink", - "description": "Please enter the username and password for your iAqualink account.", - "data": { - "username": "Username / Email Address", - "password": "Password" - } - } - }, - "error": { - "connection_failure": "Unable to connect to iAqualink. Check your username and password." - }, - "abort": { - "already_setup": "You can only configure a single iAqualink connection." + "config": { + "abort": { + "already_setup": "You can only configure a single iAqualink connection." + }, + "error": { + "connection_failure": "Unable to connect to iAqualink. Check your username and password." + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username / Email Address" + }, + "description": "Please enter the username and password for your iAqualink account.", + "title": "Connect to iAqualink" + } + }, + "title": "Jandy iAqualink" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/it.json b/homeassistant/components/ifttt/.translations/it.json index e5dc76b7923cb5..d6faf60d618ed7 100644 --- a/homeassistant/components/ifttt/.translations/it.json +++ b/homeassistant/components/ifttt/.translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant deve essere accessibile da internet per ricevere messaggi IFTTT", + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi IFTTT.", "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { diff --git a/homeassistant/components/iqvia/.translations/it.json b/homeassistant/components/iqvia/.translations/it.json index 37079cf571dbbf..492654c660c5b0 100644 --- a/homeassistant/components/iqvia/.translations/it.json +++ b/homeassistant/components/iqvia/.translations/it.json @@ -9,6 +9,7 @@ "data": { "zip_code": "CAP" }, + "description": "Compila il tuo CAP americano o canadese.", "title": "IQVIA" } }, diff --git a/homeassistant/components/life360/.translations/it.json b/homeassistant/components/life360/.translations/it.json index 9c4cb1cc4cb15f..b7d2d6c8f1b211 100644 --- a/homeassistant/components/life360/.translations/it.json +++ b/homeassistant/components/life360/.translations/it.json @@ -5,11 +5,12 @@ "user_already_configured": "L'account \u00e8 gi\u00e0 stato configurato" }, "create_entry": { - "default": "Per impostare le opzioni avanzate, consultare la [Documentazione Life360] ( {docs_url} )." + "default": "Per impostare le opzioni avanzate, consultare la [Documentazione Life360]({docs_url})." }, "error": { "invalid_credentials": "Credenziali non valide", "invalid_username": "Nome utente non valido", + "unexpected": "Errore imprevisto durante la comunicazione con il server di Life360", "user_already_configured": "L'account \u00e8 gi\u00e0 stato configurato" }, "step": { @@ -18,6 +19,7 @@ "password": "Password", "username": "Nome utente" }, + "description": "Per impostare le opzioni avanzate, vedere [Documentazione di Life360]({docs_url}).\n\u00c8 consigliabile eseguire questa operazione prima di aggiungere gli account.", "title": "Informazioni sull'account Life360" } }, diff --git a/homeassistant/components/light/.translations/it.json b/homeassistant/components/light/.translations/it.json new file mode 100644 index 00000000000000..b8d1c95750a0d0 --- /dev/null +++ b/homeassistant/components/light/.translations/it.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Commuta {nome}", + "turn_off": "Spegnere {nome}", + "turn_on": "Accendere {nome}" + }, + "condition_type": { + "is_off": "{name} \u00e8 disattivato", + "is_on": "{name} \u00e8 attivo" + }, + "trigger_type": { + "turn_off": "{name} disattivato", + "turn_on": "{name} attivato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/nl.json b/homeassistant/components/light/.translations/nl.json new file mode 100644 index 00000000000000..546fea78b6d5e9 --- /dev/null +++ b/homeassistant/components/light/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Omschakelen {naam}", + "turn_off": "{Naam} uitschakelen", + "turn_on": "{Naam} inschakelen" + }, + "condition_type": { + "is_off": "{name} is uitgeschakeld", + "is_on": "{name} is ingeschakeld" + }, + "trigger_type": { + "turn_off": "{name} is uitgeschakeld", + "turn_on": "{name} is ingeschakeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json index 56a579b576e2a3..c0a5e09d24431e 100644 --- a/homeassistant/components/light/.translations/ru.json +++ b/homeassistant/components/light/.translations/ru.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {name}", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {name}" + }, + "condition_type": { + "is_off": "{name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "is_on": "{name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, "trigger_type": { "turn_off": "{name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "turn_on": "{name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" diff --git a/homeassistant/components/linky/.translations/it.json b/homeassistant/components/linky/.translations/it.json new file mode 100644 index 00000000000000..bc9890bed2c760 --- /dev/null +++ b/homeassistant/components/linky/.translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Account gi\u00e0 configurato" + }, + "error": { + "access": "Impossibile accedere a Enedis.fr, si prega di controllare la connessione internet", + "enedis": "Enedis.fr ha risposto con un errore: si prega di riprovare pi\u00f9 tardi (di solito non tra le 23:00 e le 02:00).", + "unknown": "Errore sconosciuto: riprova pi\u00f9 tardi (in genere non tra le 23:00 e le 02:00)", + "username_exists": "Account gi\u00e0 configurato", + "wrong_login": "Errore di accesso: si prega di controllare la tua e-mail e la password" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "E-mail" + }, + "description": "Inserisci le tue credenziali", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/nl.json b/homeassistant/components/linky/.translations/nl.json new file mode 100644 index 00000000000000..89759fdf21630e --- /dev/null +++ b/homeassistant/components/linky/.translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Account reeds geconfigureerd" + }, + "error": { + "access": "Geen toegang tot Enedis.fr, controleer uw internetverbinding", + "enedis": "Enedis.fr antwoordde met een fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)", + "unknown": "Onbekende fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)", + "username_exists": "Account reeds geconfigureerd", + "wrong_login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mail" + }, + "description": "Voer uw gegevens in", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/it.json b/homeassistant/components/logi_circle/.translations/it.json index 568bf79a40d207..d7c1d9ba9de7e4 100644 --- a/homeassistant/components/logi_circle/.translations/it.json +++ b/homeassistant/components/logi_circle/.translations/it.json @@ -12,10 +12,11 @@ "error": { "auth_error": "Autorizzazione API fallita.", "auth_timeout": "Timeout dell'autorizzazione durante la richiesta del token di accesso.", - "follow_link": "Segui il link e autenticati prima di premere Invio" + "follow_link": "Segui il link e autenticati prima di premere Invia" }, "step": { "auth": { + "description": "Segui il link qui sotto e Accetta l'accesso al tuo account Logi Circle, quindi torna indietro e premi Invia qui sotto. \n\n [Link]({authorization_url})", "title": "Autenticarsi con Logi Circle" }, "user": { diff --git a/homeassistant/components/met/.translations/it.json b/homeassistant/components/met/.translations/it.json new file mode 100644 index 00000000000000..a1cfd12e8cda7c --- /dev/null +++ b/homeassistant/components/met/.translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "name_exists": "La posizione esiste gi\u00e0" + }, + "step": { + "user": { + "data": { + "elevation": "Altitudine", + "latitude": "Latitudine", + "longitude": "Longitudine", + "name": "Nome" + }, + "description": "Meteorologisk institutt", + "title": "Posizione" + } + }, + "title": "Met.no" + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/.translations/it.json b/homeassistant/components/mobile_app/.translations/it.json index 049e551d19bdfe..37c0deb9c2d8e3 100644 --- a/homeassistant/components/mobile_app/.translations/it.json +++ b/homeassistant/components/mobile_app/.translations/it.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "install_app": "Apri l'app per dispositivi mobili per configurare l'integrazione con Home Assistant. Vedi [i documenti] ( {apps_url} ) per un elenco di app compatibili." + "install_app": "Apri l'App per dispositivi mobili per configurare l'integrazione con Home Assistant. Vedi [i documenti]({apps_url}) per un elenco di app compatibili." }, "step": { "confirm": { - "description": "Vuoi configurare il componente Mobile App?", + "description": "Si desidera configurare il componente App per dispositivi mobili?", "title": "App per dispositivi mobili" } }, diff --git a/homeassistant/components/notion/.translations/it.json b/homeassistant/components/notion/.translations/it.json new file mode 100644 index 00000000000000..36f3f5fc7bcad3 --- /dev/null +++ b/homeassistant/components/notion/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Nome utente gi\u00e0 registrato", + "invalid_credentials": "Nome utente o password non validi", + "no_devices": "Nessun dispositivo trovato nell'account" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente / indirizzo Email" + }, + "title": "Inserisci le tue informazioni" + } + }, + "title": "Nozione" + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/it.json b/homeassistant/components/owntracks/.translations/it.json index 9b66b693c333a1..03b0c84744f75d 100644 --- a/homeassistant/components/owntracks/.translations/it.json +++ b/homeassistant/components/owntracks/.translations/it.json @@ -3,6 +3,9 @@ "abort": { "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, + "create_entry": { + "default": "\n\nSu Android, apri l'[app OwnTracks]({android_url}), vai su preferenze -> connessione. Modifica le seguenti impostazioni: \n - Modalit\u00e0: HTTP privato \n - Host: {webhook_url} \n - Identificazione: \n - Nome utente: `` \n - ID dispositivo: ``\n\nSu iOS, apri l'[app OwnTracks]({ios_url}), tocca l'icona (i) in alto a sinistra -> impostazioni. Modifica le seguenti impostazioni: \n - Modalit\u00e0: HTTP \n - URL: {webhook_url} \n - Attiva autenticazione \n - UserID: `` \n\n {secret} \n \n Vedi [la documentazione]({docs_url}) per maggiori informazioni." + }, "step": { "user": { "description": "Sei sicuro di voler configurare OwnTracks?", diff --git a/homeassistant/components/plaato/.translations/it.json b/homeassistant/components/plaato/.translations/it.json new file mode 100644 index 00000000000000..7e7697a339bc20 --- /dev/null +++ b/homeassistant/components/plaato/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Plaato Airlook.", + "one_instance_allowed": "\u00c8 necessaria solo una singola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai impostare la funzione webhook in Plaato Airlock. \n\n Inserisci le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Metodo: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare Plaato Airlock?", + "title": "Configura il webhook di Plaato" + } + }, + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/it.json b/homeassistant/components/point/.translations/it.json index 324801009ca5aa..3c0ef8306e0cb6 100644 --- a/homeassistant/components/point/.translations/it.json +++ b/homeassistant/components/point/.translations/it.json @@ -16,6 +16,7 @@ }, "step": { "auth": { + "description": "Segui il link qui sotto e Accetta l'accesso al tuo account Minut, quindi torna indietro e premi Invia qui sotto. \n\n [Link] ( {authorization_url} )", "title": "Autenticare Point" }, "user": { diff --git a/homeassistant/components/ps4/.translations/it.json b/homeassistant/components/ps4/.translations/it.json index afa32056757ccb..de5eb4e5e6f30f 100644 --- a/homeassistant/components/ps4/.translations/it.json +++ b/homeassistant/components/ps4/.translations/it.json @@ -4,11 +4,12 @@ "credential_error": "Errore nel recupero delle credenziali.", "devices_configured": "Tutti i dispositivi trovati sono gi\u00e0 configurati.", "no_devices_found": "Nessun dispositivo PlayStation 4 trovato in rete.", - "port_987_bind_error": "Impossibile connettersi alla porta 987.", - "port_997_bind_error": "Impossibile connettersi alla porta 997." + "port_987_bind_error": "Impossibile collegarsi alla porta 987. Per ulteriori informazioni, consultare la [documentazione] (https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", + "port_997_bind_error": "Impossibile collegarsi alla porta 997. Consultare la [documentazione] (https://www.home-assistant.io/components/ps4/) per ulteriori informazioni." }, "error": { - "login_failed": "Accoppiamento alla PlayStation 4 fallito. Verifica che il PIN sia corretto.", + "credential_timeout": "Servizio credenziali scaduto. Premi Invia per riavviare.", + "login_failed": "Impossibile eseguire l'associazione a PlayStation 4. Verificare che il PIN sia corretto.", "no_ipaddress": "Inserisci l'indirizzo IP della PlayStation 4 che desideri configurare.", "not_ready": "La PlayStation 4 non \u00e8 accesa o non \u00e8 collegata alla rete." }, @@ -24,7 +25,7 @@ "name": "Nome", "region": "Area geografica" }, - "description": "Inserisci le informazioni della tua PlayStation 4. Per il \"PIN\", vai su \"Impostazioni\" sulla tua console PlayStation 4. Quindi accedi a \"Impostazioni connessione app mobile\" e seleziona \"Aggiungi dispositivo\". Inserisci il PIN che viene visualizzato.", + "description": "Inserisci le tue informazioni su PlayStation 4. Per \"PIN\", vai a \"Impostazioni\" sulla console PlayStation 4. Quindi vai a 'Impostazioni di connessione app mobile' e seleziona 'Aggiungi dispositivo'. Immettere il PIN visualizzato. Fare riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/smartthings/.translations/it.json b/homeassistant/components/smartthings/.translations/it.json index 486a61847a71a0..c2b17eed04d3fe 100644 --- a/homeassistant/components/smartthings/.translations/it.json +++ b/homeassistant/components/smartthings/.translations/it.json @@ -5,6 +5,7 @@ "app_setup_error": "Impossibile configurare SmartApp. Riprovare.", "base_url_not_https": "Il `base_url` per il componente `http` deve essere configurato e deve iniziare con `https://`.", "token_already_setup": "Il token \u00e8 gi\u00e0 stato configurato.", + "token_forbidden": "Il token non dispone degli ambiti OAuth necessari.", "token_invalid_format": "Il token deve essere nel formato UID/GUID", "token_unauthorized": "Il token non \u00e8 valido o non \u00e8 pi\u00f9 autorizzato.", "webhook_error": "SmartThings non ha potuto convalidare l'endpoint configurato in `base_url`. Si prega di rivedere i requisiti del componente." @@ -18,6 +19,7 @@ "title": "Inserisci il Token di Accesso Personale" }, "wait_install": { + "description": "Si prega di installare l'Home Assistant SmartApp in almeno una posizione e fare clic su Invia.", "title": "Installa SmartApp" } }, diff --git a/homeassistant/components/somfy/.translations/it.json b/homeassistant/components/somfy/.translations/it.json new file mode 100644 index 00000000000000..06fc8bed40f409 --- /dev/null +++ b/homeassistant/components/somfy/.translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account Somfy.", + "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", + "missing_configuration": "Il componente Somfy non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticato con successo con Somfy." + }, + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/it.json b/homeassistant/components/tellduslive/.translations/it.json index 3baa307de51f7b..ce152285e757d3 100644 --- a/homeassistant/components/tellduslive/.translations/it.json +++ b/homeassistant/components/tellduslive/.translations/it.json @@ -11,12 +11,14 @@ }, "step": { "auth": { + "description": "Per collegare il tuo account TelldusLive:\n 1. Clicca sul link sottostante\n 2. Accedi a Telldus Live\n 3. Autorizzare **{app_name}**** (cliccare **S\u00ec**).\n 4. Torna qui e clicca su **SUBMIT**.\n\n [Collega account TelldusLive]({auth_url})", "title": "Autenticati con TelldusLive" }, "user": { "data": { "host": "Host" }, + "description": "Vuoto", "title": "Scegli l'endpoint." } }, diff --git a/homeassistant/components/toon/.translations/it.json b/homeassistant/components/toon/.translations/it.json index 696c770f130952..7934913558114f 100644 --- a/homeassistant/components/toon/.translations/it.json +++ b/homeassistant/components/toon/.translations/it.json @@ -2,6 +2,7 @@ "config": { "abort": { "client_id": "L'ID client dalla configurazione non \u00e8 valido.", + "client_secret": "Il client segreto della configurazione non \u00e8 valido.", "no_agreements": "Questo account non ha display Toon.", "no_app": "\u00c8 necessario configurare Toon prima di poter eseguire l'autenticazione con esso. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Si \u00e8 verificato un errore imprevisto durante l'autenticazione." @@ -14,6 +15,7 @@ "authenticate": { "data": { "password": "Password", + "tenant": "Inquilino", "username": "Nome utente" }, "description": "Autenticati con il tuo account Eneco Toon (non l'account sviluppatore).", diff --git a/homeassistant/components/traccar/.translations/it.json b/homeassistant/components/traccar/.translations/it.json new file mode 100644 index 00000000000000..a0980644a71a27 --- /dev/null +++ b/homeassistant/components/traccar/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Traccar.", + "one_instance_allowed": "\u00c8 necessaria solo una singola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, \u00e8 necessario configurare la funzionalit\u00e0 webhook in Traccar.\n\nUtilizzare l'URL seguente: `{webhook_url}`\n\nPer ulteriori dettagli, vedere [la documentazione]({docs_url}) ." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare Traccar?", + "title": "Imposta Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/it.json b/homeassistant/components/tradfri/.translations/it.json index 4c114492336492..e58e296743fcc4 100644 --- a/homeassistant/components/tradfri/.translations/it.json +++ b/homeassistant/components/tradfri/.translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il bridge \u00e8 gi\u00e0 configurato" + "already_configured": "Il bridge \u00e8 gi\u00e0 configurato", + "already_in_progress": "La configurazione del Bridge \u00e8 gi\u00e0 in corso." }, "error": { "cannot_connect": "Impossibile connettersi al gateway.", diff --git a/homeassistant/components/twentemilieu/.translations/it.json b/homeassistant/components/twentemilieu/.translations/it.json new file mode 100644 index 00000000000000..cfdb594f120de3 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Indirizzo gi\u00e0 impostato." + }, + "error": { + "connection_error": "Impossibile connettersi.", + "invalid_address": "Indirizzo non trovato nell'area di servizio di Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Edificio, Scala, Interno, ecc. / Informazioni aggiuntive", + "house_number": "Numero civico", + "post_code": "Codice di avviamento postale" + }, + "description": "Imposta Twente Milieu fornendo le informazioni sulla raccolta dei rifiuti al tuo indirizzo.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json index 407371bf89f198..ebf6b0c49323c6 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -22,5 +22,23 @@ } }, "title": "UniFi Controller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tempo in secondi dall'ultima volta che viene visto fino a quando non \u00e8 considerato lontano", + "track_clients": "Monitorare i client di rete", + "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)", + "track_wired_clients": "Includi i client di rete cablata" + } + }, + "init": { + "data": { + "one": "Vuoto", + "other": "Vuoto" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/it.json b/homeassistant/components/upnp/.translations/it.json index 798f6578093950..e822895a6cfaab 100644 --- a/homeassistant/components/upnp/.translations/it.json +++ b/homeassistant/components/upnp/.translations/it.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Abilita almeno i sensori o la mappatura delle porte", "single_instance_allowed": "\u00c8 necessaria una sola configurazione di UPnP/IGD." }, + "error": { + "one": "Vuoto", + "other": "Vuoto" + }, "step": { "confirm": { "description": "Vuoi configurare UPnP/IGD?", diff --git a/homeassistant/components/velbus/.translations/it.json b/homeassistant/components/velbus/.translations/it.json new file mode 100644 index 00000000000000..e4f1fbf9c6b7b3 --- /dev/null +++ b/homeassistant/components/velbus/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Questa porta \u00e8 gi\u00e0 configurata" + }, + "error": { + "connection_failed": "La connessione Velbus non \u00e8 riuscita", + "port_exists": "Questa porta \u00e8 gi\u00e0 configurata" + }, + "step": { + "user": { + "data": { + "name": "Il nome per questa connessione Velbus", + "port": "Stringa di connessione" + }, + "title": "Definire il tipo di connessione Velbus" + } + }, + "title": "Interfaccia Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/it.json b/homeassistant/components/vesync/.translations/it.json new file mode 100644 index 00000000000000..d3e53547559a7e --- /dev/null +++ b/homeassistant/components/vesync/.translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 consentita una sola istanza di Vesync" + }, + "error": { + "invalid_login": "Nome utente o password non validi" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Indirizzo E-mail" + }, + "title": "Immettere nome utente e password" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json new file mode 100644 index 00000000000000..5bf342836ce61a --- /dev/null +++ b/homeassistant/components/withings/.translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Autenticazione completata con Withings per il profilo selezionato." + }, + "step": { + "user": { + "data": { + "profile": "Profilo" + }, + "description": "Seleziona un profilo utente a cui desideri associare Home Assistant con un profilo Withings. Nella pagina Withings, assicurati di selezionare lo stesso utente o i dati non saranno etichettati correttamente.", + "title": "Profilo utente." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json new file mode 100644 index 00000000000000..1729879a154d8d --- /dev/null +++ b/homeassistant/components/withings/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." + }, + "step": { + "user": { + "data": { + "profile": "Profiel" + }, + "description": "Selecteer een gebruikersprofiel waaraan u Home Assistant wilt toewijzen met een Withings-profiel. Zorg ervoor dat u op de pagina Withings dezelfde gebruiker selecteert, anders worden de gegevens niet correct gelabeld.", + "title": "Gebruikersprofiel." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/it.json b/homeassistant/components/wwlln/.translations/it.json new file mode 100644 index 00000000000000..f0fc32636072fa --- /dev/null +++ b/homeassistant/components/wwlln/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Localit\u00e0 gi\u00e0 registrata" + }, + "step": { + "user": { + "data": { + "latitude": "Latitudine", + "longitude": "Longitudine", + "radius": "Raggio (utilizzando il tuo sistema di unit\u00e0 di misura di base)" + }, + "title": "Inserisci le informazioni sulla tua posizione." + } + }, + "title": "Rete mondiale di localizzazione dei fulmini (WWLLN)" + } +} \ No newline at end of file From 078a72d102d8922e38441d0a1af74161cd5a08d2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 6 Sep 2019 18:41:19 -0600 Subject: [PATCH 0206/3953] Bump aiowwlln to 2.0.1 (#26486) --- homeassistant/components/wwlln/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wwlln/manifest.json b/homeassistant/components/wwlln/manifest.json index ef9295341c065b..6d13f7adbfd497 100644 --- a/homeassistant/components/wwlln/manifest.json +++ b/homeassistant/components/wwlln/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/wwlln", "requirements": [ - "aiowwlln==1.0.0" + "aiowwlln==2.0.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 08641c38b8e5f5..0b41bf319b8598 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aioswitcher==2019.4.26 aiounifi==11 # homeassistant.components.wwlln -aiowwlln==1.0.0 +aiowwlln==2.0.1 # homeassistant.components.aladdin_connect aladdin_connect==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de5cdaa9da4f9d..5084472a68d32d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -73,7 +73,7 @@ aioswitcher==2019.4.26 aiounifi==11 # homeassistant.components.wwlln -aiowwlln==1.0.0 +aiowwlln==2.0.1 # homeassistant.components.ambiclimate ambiclimate==0.2.1 From 5b3004c7b08a05f7027c68769f1d1ba41f1103fb Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Sat, 7 Sep 2019 02:41:34 +0200 Subject: [PATCH 0207/3953] Vicare: Avoid invalid temperature values (#26485) The PyVicare API can return the string "error" in case of connection or authentication errors. The current_temperature value could be set to "error" instead of a nueric value or None which breaks the climate component. This commit sets the current_temperature to None instead. --- homeassistant/components/vicare/climate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 5727508deb4ac0..1a5098360cfab2 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -96,10 +96,13 @@ def __init__(self, name, api): def update(self): """Let HA know there has been an update from the ViCare API.""" _room_temperature = self._api.getRoomTemperature() - if _room_temperature is not None and _room_temperature != "error": + _supply_temperature = self._api.getSupplyTemperature() + if _room_temperature is not None and _room_temperature != PYVICARE_ERROR: self._current_temperature = _room_temperature + elif _supply_temperature != PYVICARE_ERROR: + self._current_temperature = _supply_temperature else: - self._current_temperature = self._api.getSupplyTemperature() + self._current_temperature = None self._current_program = self._api.getActiveProgram() # The getCurrentDesiredTemperature call can yield 'error' (str) when the system is in standby From c1671bbb28dc4f44ce5a0de03e61fb54dffbd7d9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 Sep 2019 17:56:18 -0700 Subject: [PATCH 0208/3953] Updated frontend to 20190828.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 fa6145a7af21de..1366269061ec6f 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==20190828.0" + "home-assistant-frontend==20190828.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a1ffd515c5bfa4..54e5cd17252e9a 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==20190828.0 +home-assistant-frontend==20190828.1 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3660514e355b66..875e321fd30336 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==20190828.0 +home-assistant-frontend==20190828.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1aad0450390740..949b027f419671 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==20190828.0 +home-assistant-frontend==20190828.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 6ad87e52a8ba24f1ca19eae8500d2735e5cf5625 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 Sep 2019 18:01:30 -0700 Subject: [PATCH 0209/3953] Bumped version to 0.98.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index db6580727eab6a..81870cf924dcc2 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 = "4" +PATCH_VERSION = "5" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 33e1b44b3a5724e254a4d60691c881ef3d687a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 7 Sep 2019 09:48:58 +0300 Subject: [PATCH 0210/3953] Use PEP 526 type annotations, add some type hints (#26464) * Add some more type hints to helpers.event * Change most type comments to variable types * Remove some superfluous type hints --- homeassistant/auth/models.py | 12 +++---- .../components/bbox/device_tracker.py | 4 +-- homeassistant/components/bbox/sensor.py | 2 +- homeassistant/components/buienradar/camera.py | 6 ++-- homeassistant/components/cast/media_player.py | 25 ++++++------- .../components/device_tracker/config_entry.py | 2 +- .../components/device_tracker/legacy.py | 18 +++++----- .../components/device_tracker/setup.py | 2 +- homeassistant/components/ebox/sensor.py | 8 ++--- homeassistant/components/esphome/__init__.py | 8 ++--- .../components/esphome/config_flow.py | 10 +++--- homeassistant/components/fan/__init__.py | 14 ++++---- homeassistant/components/fido/sensor.py | 8 ++--- homeassistant/components/fints/sensor.py | 14 ++++---- .../components/google_assistant/http.py | 2 +- .../components/google_assistant/smart_home.py | 4 +-- homeassistant/components/group/light.py | 26 +++++++------- homeassistant/components/gtfs/sensor.py | 8 ++--- .../components/hydroquebec/sensor.py | 6 ++-- homeassistant/components/isy994/__init__.py | 2 +- homeassistant/components/mqtt/__init__.py | 36 +++++++++---------- .../components/onkyo/media_player.py | 6 ++-- homeassistant/components/person/__init__.py | 8 ++--- .../components/proximity/__init__.py | 4 +-- homeassistant/components/recorder/__init__.py | 8 ++--- homeassistant/components/startca/sensor.py | 4 +-- homeassistant/components/switch/light.py | 8 ++--- .../components/switcher_kis/switch.py | 2 +- homeassistant/components/teksavvy/sensor.py | 4 +-- .../components/webostv/media_player.py | 4 +-- homeassistant/components/zha/core/store.py | 6 ++-- homeassistant/helpers/entity_registry.py | 4 +-- homeassistant/helpers/event.py | 17 +++++---- homeassistant/loader.py | 32 ++++------------- tests/components/switcher_kis/conftest.py | 2 +- 35 files changed, 152 insertions(+), 174 deletions(-) diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 26055032422f0c..6889d17a25fe6d 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -20,7 +20,7 @@ class Group: """A group.""" - name = attr.ib(type=str) # type: Optional[str] + name = attr.ib(type=Optional[str]) policy = attr.ib(type=perm_mdl.PolicyType) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) system_generated = attr.ib(type=bool, default=False) @@ -30,22 +30,20 @@ class Group: class User: """A user.""" - name = attr.ib(type=str) # type: Optional[str] + name = attr.ib(type=Optional[str]) perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, cmp=False) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) is_owner = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False) system_generated = attr.ib(type=bool, default=False) - groups = attr.ib(type=List, factory=list, cmp=False) # type: List[Group] + groups = attr.ib(type=List[Group], factory=list, cmp=False) # List of credentials of a user. - credentials = attr.ib(type=list, factory=list, cmp=False) # type: List[Credentials] + credentials = attr.ib(type=List["Credentials"], factory=list, cmp=False) # Tokens associated with a user. - refresh_tokens = attr.ib( - type=dict, factory=dict, cmp=False - ) # type: Dict[str, RefreshToken] + refresh_tokens = attr.ib(type=Dict[str, "RefreshToken"], factory=dict, cmp=False) _permissions = attr.ib( type=Optional[perm_mdl.PolicyPermissions], init=False, cmp=False, default=None diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index b565d05685f979..89449aeab4592f 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -2,6 +2,7 @@ from collections import namedtuple from datetime import timedelta import logging +from typing import List import voluptuous as vol @@ -41,12 +42,11 @@ class BboxDeviceScanner(DeviceScanner): def __init__(self, config): """Get host from config.""" - from typing import List # noqa: pylint: disable=unused-import self.host = config[CONF_HOST] """Initialize the scanner.""" - self.last_results = [] # type: List[Device] + self.last_results: List[Device] = [] self.success_init = self._update_info() _LOGGER.info("Scanner initialized") diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index b59b166e41f145..ba38f8d2607dc3 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) -BANDWIDTH_MEGABITS_SECONDS = "Mb/s" # type: str +BANDWIDTH_MEGABITS_SECONDS = "Mb/s" ATTRIBUTION = "Powered by Bouygues Telecom" diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 1db9e2beaf9376..cdf202bbafd685 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -81,13 +81,13 @@ def __init__(self, name: str, dimension: int, delta: float): # invariant: this condition is private to and owned by this instance. self._condition = asyncio.Condition() - self._last_image = None # type: Optional[bytes] + self._last_image: Optional[bytes] = None # value of the last seen last modified header - self._last_modified = None # type: Optional[str] + self._last_modified: Optional[str] = None # loading status self._loading = False # deadline for image refresh - self.delta after last successful load - self._deadline = None # type: Optional[datetime] + self._deadline: Optional[datetime] = None @property def name(self) -> str: diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index af9f39f8ed446f..26bcaccc247625 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -525,32 +525,33 @@ class CastDevice(MediaPlayerDevice): "elected leader" itself. """ - def __init__(self, cast_info): + def __init__(self, cast_info: ChromecastInfo): """Initialize the cast device.""" - import pychromecast # noqa: pylint: disable=unused-import + import pychromecast - self._cast_info = cast_info # type: ChromecastInfo + self._cast_info = cast_info self.services = None if cast_info.service: self.services = set() self.services.add(cast_info.service) - self._chromecast = None # type: Optional[pychromecast.Chromecast] + self._chromecast: Optional[pychromecast.Chromecast] = None self.cast_status = None self.media_status = None self.media_status_received = None - self._dynamic_group_cast_info = None # type: ChromecastInfo - self._dynamic_group_cast = None # type: Optional[pychromecast.Chromecast] + self._dynamic_group_cast_info: ChromecastInfo = None + self._dynamic_group_cast: Optional[pychromecast.Chromecast] = None self.dynamic_group_media_status = None self.dynamic_group_media_status_received = None self.mz_media_status = {} self.mz_media_status_received = {} self.mz_mgr = None - self._available = False # type: bool - self._dynamic_group_available = False # type: bool - self._status_listener = None # type: Optional[CastStatusListener] - self._dynamic_group_status_listener = ( - None - ) # type: Optional[DynamicGroupCastStatusListener] + self._available = False + self._dynamic_group_available = False + self._status_listener: Optional[CastStatusListener] = None + self._dynamic_group_status_listener: Optional[ + DynamicGroupCastStatusListener + ] = None + self._add_remove_handler = None self._del_remove_handler = None diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 460f11984096ca..9e53c2e0cea24e 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -18,7 +18,7 @@ async def async_setup_entry(hass, entry): """Set up an entry.""" - component = hass.data.get(DOMAIN) # type: Optional[EntityComponent] + component: Optional[EntityComponent] = hass.data.get(DOMAIN) if component is None: component = hass.data[DOMAIN] = EntityComponent(LOGGER, DOMAIN, hass) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 2bfd0c41a47bfd..5c186cc12a1182 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -327,15 +327,15 @@ async def async_init_single_device(dev): class Device(RestoreEntity): """Represent a tracked device.""" - host_name = None # type: str - location_name = None # type: str - gps = None # type: GPSType - gps_accuracy = 0 # type: int - last_seen = None # type: dt_util.dt.datetime - consider_home = None # type: dt_util.dt.timedelta - battery = None # type: int - attributes = None # type: dict - icon = None # type: str + host_name: str = None + location_name: str = None + gps: GPSType = None + gps_accuracy: int = 0 + last_seen: dt_util.dt.datetime = None + consider_home: dt_util.dt.timedelta = None + battery: int = None + attributes: dict = None + icon: str = None # Track if the last update of this device was HOME. last_update_home = False diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py index e6edb5f63ac6a0..6c9f05dead7349 100644 --- a/homeassistant/components/device_tracker/setup.py +++ b/homeassistant/components/device_tracker/setup.py @@ -147,7 +147,7 @@ def async_setup_scanner_platform( scanner.hass = hass # Initial scan of each mac we also tell about host name for config - seen = set() # type: Any + seen: Any = set() async def async_device_tracker_scan(now: dt_util.dt.datetime): """Handle interval matches.""" diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index 66c58b828826cb..95c5513ecaf94e 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -26,10 +26,10 @@ _LOGGER = logging.getLogger(__name__) -GIGABITS = "Gb" # type: str -PRICE = "CAD" # type: str -DAYS = "days" # type: str -PERCENT = "%" # type: str +GIGABITS = "Gb" +PRICE = "CAD" +DAYS = "days" +PERCENT = "%" DEFAULT_NAME = "EBox" diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 182d4003e30664..bc06aba94ead87 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -203,7 +203,7 @@ async def try_connect(tries: int = 0, is_disconnect: bool = True) -> None: # When removing/disconnecting manually return - data = hass.data[DOMAIN][entry.entry_id] # type: RuntimeEntryData + data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] for disconnect_cb in data.disconnect_callbacks: disconnect_cb() data.disconnect_callbacks = [] @@ -326,7 +326,7 @@ async def _cleanup_instance( hass: HomeAssistantType, entry: ConfigEntry ) -> RuntimeEntryData: """Cleanup the esphome client if it exists.""" - data = hass.data[DATA_KEY].pop(entry.entry_id) # type: RuntimeEntryData + data: RuntimeEntryData = hass.data[DATA_KEY].pop(entry.entry_id) if data.reconnect_task is not None: data.reconnect_task.cancel() for disconnect_cb in data.disconnect_callbacks: @@ -363,7 +363,7 @@ async def platform_async_setup_entry( This method is in charge of receiving, distributing and storing info and state updates. """ - entry_data = hass.data[DOMAIN][entry.entry_id] # type: RuntimeEntryData + entry_data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] entry_data.info[component_key] = {} entry_data.state[component_key] = {} @@ -468,7 +468,7 @@ def __init__(self, entry_id: str, component_key: str, key: int): self._entry_id = entry_id self._component_key = component_key self._key = key - self._remove_callbacks = [] # type: List[Callable[[], None]] + self._remove_callbacks: List[Callable[[], None]] = [] async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 35389d055d6708..9680ed46acdf5d 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -19,9 +19,9 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): def __init__(self): """Initialize flow.""" - self._host = None # type: Optional[str] - self._port = None # type: Optional[int] - self._password = None # type: Optional[str] + self._host: Optional[str] = None + self._port: Optional[int] = None + self._password: Optional[str] = None async def async_step_user( self, user_input: Optional[ConfigType] = None, error: Optional[str] = None @@ -94,9 +94,7 @@ async def async_step_zeroconf(self, user_input: ConfigType): already_configured = True elif entry.entry_id in self.hass.data.get(DATA_KEY, {}): # Does a config entry with this name already exist? - data = self.hass.data[DATA_KEY][ - entry.entry_id - ] # type: RuntimeEntryData + data: RuntimeEntryData = self.hass.data[DATA_KEY][entry.entry_id] # Node names are unique in the network if data.device_info is not None: already_configured = data.device_info.name == node_name diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 50d698f733656f..82f4d37938c7d8 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -55,23 +55,21 @@ "speed_list": ATTR_SPEED_LIST, "oscillating": ATTR_OSCILLATING, "current_direction": ATTR_DIRECTION, -} # type: dict +} FAN_SET_SPEED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( {vol.Required(ATTR_SPEED): cv.string} -) # type: dict +) -FAN_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Optional(ATTR_SPEED): cv.string} -) # type: dict +FAN_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({vol.Optional(ATTR_SPEED): cv.string}) FAN_OSCILLATE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( {vol.Required(ATTR_OSCILLATING): cv.boolean} -) # type: dict +) FAN_SET_DIRECTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( {vol.Optional(ATTR_DIRECTION): cv.string} -) # type: dict +) @bind_hass @@ -198,7 +196,7 @@ def current_direction(self) -> Optional[str]: @property def state_attributes(self) -> dict: """Return optional state attributes.""" - data = {} # type: dict + data = {} for prop, attr in PROP_TO_ATTR.items(): if not hasattr(self, prop): diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index e85b45db4d3aa0..8814a2406c52a9 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -25,10 +25,10 @@ _LOGGER = logging.getLogger(__name__) -KILOBITS = "Kb" # type: str -PRICE = "CAD" # type: str -MESSAGES = "messages" # type: str -MINUTES = "minutes" # type: str +KILOBITS = "Kb" +PRICE = "CAD" +MESSAGES = "messages" +MINUTES = "minutes" DEFAULT_NAME = "Fido" diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 008337f88eb077..376ea2c0f9d4fc 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -162,11 +162,11 @@ class FinTsAccount(Entity): def __init__(self, client: FinTsClient, account, name: str) -> None: """Initialize a FinTs balance account.""" - self._client = client # type: FinTsClient + self._client = client self._account = account - self._name = name # type: str - self._balance = None # type: float - self._currency = None # type: str + self._name = name + self._balance: float = None + self._currency: str = None @property def should_poll(self) -> bool: @@ -222,11 +222,11 @@ class FinTsHoldingsAccount(Entity): def __init__(self, client: FinTsClient, account, name: str) -> None: """Initialize a FinTs holdings account.""" - self._client = client # type: FinTsClient - self._name = name # type: str + self._client = client + self._name = name self._account = account self._holdings = [] - self._total = None # type: float + self._total: float = None @property def should_poll(self) -> bool: diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 24502462512fba..d68650fb6384c3 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -93,7 +93,7 @@ def __init__(self, config): async def post(self, request: Request) -> Response: """Handle Google Assistant requests.""" - message = await request.json() # type: dict + message: dict = await request.json() result = await async_handle_message( request.app["hass"], self.config, request["hass_user"].id, message ) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 2cb440f9181fea..6ab6d937b51e84 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -24,7 +24,7 @@ async def async_handle_message(hass, config, user_id, message): """Handle incoming API messages.""" - request_id = message.get("requestId") # type: str + request_id: str = message.get("requestId") data = RequestData(config, user_id, request_id) @@ -38,7 +38,7 @@ async def async_handle_message(hass, config, user_id, message): async def _process(hass, data, message): """Process a message.""" - inputs = message.get("inputs") # type: list + inputs: list = message.get("inputs") if len(inputs) != 1: return { diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index f0d29d923c8ed1..87d8134ccbf6b3 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -75,19 +75,19 @@ class LightGroup(light.Light): def __init__(self, name: str, entity_ids: List[str]) -> None: """Initialize a light group.""" - self._name = name # type: str - self._entity_ids = entity_ids # type: List[str] - self._is_on = False # type: bool - self._available = False # type: bool - self._brightness = None # type: Optional[int] - self._hs_color = None # type: Optional[Tuple[float, float]] - self._color_temp = None # type: Optional[int] - self._min_mireds = 154 # type: Optional[int] - self._max_mireds = 500 # type: Optional[int] - self._white_value = None # type: Optional[int] - self._effect_list = None # type: Optional[List[str]] - self._effect = None # type: Optional[str] - self._supported_features = 0 # type: int + self._name = name + self._entity_ids = entity_ids + self._is_on = False + self._available = False + self._brightness: Optional[int] = None + self._hs_color: Optional[Tuple[float, float]] = None + self._color_temp: Optional[int] = None + self._min_mireds: Optional[int] = 154 + self._max_mireds: Optional[int] = 500 + self._white_value: Optional[int] = None + self._effect_list: Optional[List[str]] = None + self._effect: Optional[str] = None + self._supported_features: int = 0 self._async_unsub_state_changed = None async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index d70e1016f07e84..eb662d8b2c93a8 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -256,7 +256,7 @@ def get_next_departure( _LOGGER.debug("Timetable: %s", sorted(timetable.keys())) - item = {} # type: dict + item = {} for key in sorted(timetable.keys()): if dt_util.parse_datetime(key) > now: item = timetable[key] @@ -393,11 +393,11 @@ def __init__( self._available = False self._icon = ICON self._name = "" - self._state = None # type: Optional[str] - self._attributes = {} # type: dict + self._state: Optional[str] = None + self._attributes = {} self._agency = None - self._departure = {} # type: dict + self._departure = {} self._destination = None self._origin = None self._route = None diff --git a/homeassistant/components/hydroquebec/sensor.py b/homeassistant/components/hydroquebec/sensor.py index fd713e8b7a7937..c3ad79c1c9866e 100644 --- a/homeassistant/components/hydroquebec/sensor.py +++ b/homeassistant/components/hydroquebec/sensor.py @@ -28,9 +28,9 @@ _LOGGER = logging.getLogger(__name__) KILOWATT_HOUR = ENERGY_KILO_WATT_HOUR -PRICE = "CAD" # type: str -DAYS = "days" # type: str -CONF_CONTRACT = "contract" # type: str +PRICE = "CAD" +DAYS = "days" +CONF_CONTRACT = "contract" DEFAULT_NAME = "HydroQuebec" diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index e1d24fa5551041..727ec91dc37cd3 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -459,7 +459,7 @@ class ISYDevice(Entity): """Representation of an ISY994 device.""" _attrs = {} - _name = None # type: str + _name: str = None def __init__(self, node) -> None: """Initialize the insteon device.""" diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 75552d1d14b8d7..8d83cd0cc2ba0f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -10,7 +10,7 @@ import socket import ssl import time -from typing import Any, Callable, List, Optional, Union, cast # noqa: F401 +from typing import Any, Callable, List, Optional, Union import attr import requests.certs @@ -479,7 +479,7 @@ async def _async_setup_server(hass: HomeAssistantType, config: ConfigType): This method is a coroutine. """ - conf = config.get(DOMAIN, {}) # type: ConfigType + conf: ConfigType = config.get(DOMAIN, {}) success, broker_config = await server.async_start( hass, conf.get(CONF_PASSWORD), conf.get(CONF_EMBEDDED) @@ -502,16 +502,16 @@ async def _async_setup_discovery( _LOGGER.error("Unable to load MQTT discovery") return False - success = await discovery.async_start( + success: bool = await discovery.async_start( hass, conf[CONF_DISCOVERY_PREFIX], hass_config, config_entry - ) # type: bool + ) return success async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Start the MQTT protocol service.""" - conf = config.get(DOMAIN) # type: Optional[ConfigType] + conf: Optional[ConfigType] = config.get(DOMAIN) # We need this because discovery can cause components to be set up and # otherwise it will not load the users config. @@ -621,7 +621,7 @@ async def async_setup_entry(hass, entry): birth_message = None # Be able to override versions other than TLSv1.0 under Python3.6 - conf_tls_version = conf.get(CONF_TLS_VERSION) # type: str + conf_tls_version: str = conf.get(CONF_TLS_VERSION) if conf_tls_version == "1.2": tls_version = ssl.PROTOCOL_TLSv1_2 elif conf_tls_version == "1.1": @@ -655,7 +655,7 @@ async def async_setup_entry(hass, entry): tls_version=tls_version, ) - result = await hass.data[DATA_MQTT].async_connect() # type: str + result: str = await hass.data[DATA_MQTT].async_connect() if result == CONNECTION_FAILED: return False @@ -671,11 +671,11 @@ async def async_stop_mqtt(event: Event): async def async_publish_service(call: ServiceCall): """Handle MQTT publish service calls.""" - msg_topic = call.data[ATTR_TOPIC] # type: str + msg_topic: str = call.data[ATTR_TOPIC] payload = call.data.get(ATTR_PAYLOAD) payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE) - qos = call.data[ATTR_QOS] # type: int - retain = call.data[ATTR_RETAIN] # type: bool + qos: int = call.data[ATTR_QOS] + retain: bool = call.data[ATTR_RETAIN] if payload_template is not None: try: payload = template.Template(payload_template, hass).async_render() @@ -741,14 +741,14 @@ def __init__( self.broker = broker self.port = port self.keepalive = keepalive - self.subscriptions = [] # type: List[Subscription] + self.subscriptions: List[Subscription] = [] self.birth_message = birth_message self.connected = False - self._mqttc = None # type: mqtt.Client + self._mqttc: mqtt.Client = None self._paho_lock = asyncio.Lock() if protocol == PROTOCOL_31: - proto = mqtt.MQTTv31 # type: int + proto: int = mqtt.MQTTv31 else: proto = mqtt.MQTTv311 @@ -796,7 +796,7 @@ async def async_connect(self) -> str: This method is a coroutine. """ - result = None # type: int + result: int = None try: result = await self.hass.async_add_job( self._mqttc.connect, self.broker, self.port, self.keepalive @@ -870,7 +870,7 @@ async def _async_unsubscribe(self, topic: str) -> None: This method is a coroutine. """ async with self._paho_lock: - result = None # type: int + result: int = None result, _ = await self.hass.async_add_job(self._mqttc.unsubscribe, topic) _raise_on_error(result) @@ -879,7 +879,7 @@ async def _async_perform_subscription(self, topic: str, qos: int) -> None: _LOGGER.debug("Subscribing to %s", topic) async with self._paho_lock: - result = None # type: int + result: int = None result, _ = await self.hass.async_add_job(self._mqttc.subscribe, topic, qos) _raise_on_error(result) @@ -928,7 +928,7 @@ def _mqtt_handle_message(self, msg) -> None: if not _match_topic(subscription.topic, msg.topic): continue - payload = msg.payload # type: SubscribePayloadType + payload: SubscribePayloadType = msg.payload if subscription.encoding is not None: try: payload = msg.payload.decode(subscription.encoding) @@ -1077,7 +1077,7 @@ class MqttAvailability(Entity): def __init__(self, config: dict) -> None: """Initialize the availability mixin.""" self._availability_sub_state = None - self._available = False # type: bool + self._available = False self._avail_config = config diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index b5001a1f983801..023fb32e6e40da 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -1,8 +1,6 @@ """Support for Onkyo Receivers.""" import logging - -# pylint: disable=unused-import -from typing import List # noqa: F401 +from typing import List import voluptuous as vol @@ -54,7 +52,7 @@ | SUPPORT_PLAY_MEDIA ) -KNOWN_HOSTS = [] # type: List[str] +KNOWN_HOSTS: List[str] = [] DEFAULT_SOURCES = { "tv": "TV", "bd": "Bluray", diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index c6a2f91bab3977..832853c670d827 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -441,7 +441,7 @@ def ws_list_person( hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg ): """List persons.""" - manager = hass.data[DOMAIN] # type: PersonManager + manager: PersonManager = hass.data[DOMAIN] connection.send_result( msg["id"], {"storage": manager.storage_persons, "config": manager.config_persons}, @@ -464,7 +464,7 @@ async def ws_create_person( hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg ): """Create a person.""" - manager = hass.data[DOMAIN] # type: PersonManager + manager: PersonManager = hass.data[DOMAIN] try: person = await manager.async_create_person( name=msg["name"], @@ -495,7 +495,7 @@ async def ws_update_person( hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg ): """Update a person.""" - manager = hass.data[DOMAIN] # type: PersonManager + manager: PersonManager = hass.data[DOMAIN] changes = {} for key in ("name", "user_id", "device_trackers"): if key in msg: @@ -519,7 +519,7 @@ async def ws_delete_person( hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg ): """Delete a person.""" - manager = hass.data[DOMAIN] # type: PersonManager + manager: PersonManager = hass.data[DOMAIN] await manager.async_delete_person(msg["person_id"]) connection.send_result(msg["id"]) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index b5856b7f78e921..1f86958d08e065 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -211,8 +211,8 @@ def check_proximity_state_change(self, entity, old_state, new_state): # Loop through each of the distances collected and work out the # closest. - closest_device = None # type: str - dist_to_zone = None # type: float + closest_device: str = None + dist_to_zone: float = None for device in distances_to_zone: if not dist_to_zone or distances_to_zone[device] < dist_to_zone: diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 0d814a5d74baac..9d34cc6fb79f27 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -7,7 +7,7 @@ import queue import threading import time -from typing import Any, Dict, Optional # noqa: F401 +from typing import Any, Dict, Optional import voluptuous as vol @@ -177,12 +177,12 @@ def __init__( self.hass = hass self.keep_days = keep_days self.purge_interval = purge_interval - self.queue = queue.Queue() # type: Any + self.queue: Any = queue.Queue() self.recording_start = dt_util.utcnow() self.db_url = uri self.async_db_ready = asyncio.Future() - self.engine = None # type: Any - self.run_info = None # type: Any + self.engine: Any = None + self.run_info: Any = None self.entity_filter = generate_filter( include.get(CONF_DOMAINS, []), diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 5e370ed7b63290..1b567c58b45b87 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -18,8 +18,8 @@ DEFAULT_NAME = "Start.ca" CONF_TOTAL_BANDWIDTH = "total_bandwidth" -GIGABYTES = "GB" # type: str -PERCENT = "%" # type: str +GIGABYTES = "GB" +PERCENT = "%" MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) REQUEST_TIMEOUT = 5 # seconds diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 0b1094c0dd9d71..2027a8fc458234 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -48,10 +48,10 @@ class LightSwitch(Light): def __init__(self, name: str, switch_entity_id: str) -> None: """Initialize Light Switch.""" - self._name = name # type: str - self._switch_entity_id = switch_entity_id # type: str - self._is_on = False # type: bool - self._available = False # type: bool + self._name = name + self._switch_entity_id = switch_entity_id + self._is_on = False + self._available = False self._async_unsub_state_changed = None @property diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index a758a5843472cb..454baca4eef849 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -143,7 +143,7 @@ async def _control_device(self, send_on: bool) -> None: STATE_ON as SWITCHER_STATE_ON, ) - response = None # type: SwitcherV2ControlResponseMSG + response: "SwitcherV2ControlResponseMSG" = None async with SwitcherV2Api( self.hass.loop, self._device_data.ip_addr, diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py index 74c39a221ba0cf..dc8b16b8ce18fc 100644 --- a/homeassistant/components/teksavvy/sensor.py +++ b/homeassistant/components/teksavvy/sensor.py @@ -17,8 +17,8 @@ DEFAULT_NAME = "TekSavvy" CONF_TOTAL_BANDWIDTH = "total_bandwidth" -GIGABYTES = "GB" # type: str -PERCENT = "%" # type: str +GIGABYTES = "GB" +PERCENT = "%" MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) REQUEST_TIMEOUT = 5 # seconds diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 0b5696709fd212..1da70bc60ec255 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging from urllib.parse import urlparse -from typing import Dict # noqa: F401 pylint: disable=unused-import +from typing import Dict import voluptuous as vol @@ -36,7 +36,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -_CONFIGURING = {} # type: Dict[str, str] +_CONFIGURING: Dict[str, str] = {} _LOGGER = logging.getLogger(__name__) CONF_SOURCES = "sources" diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 85b4261e4ecf3c..cea38517767c85 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -2,7 +2,7 @@ # pylint: disable=W0611 from collections import OrderedDict import logging -from typing import MutableMapping # noqa: F401 +from typing import MutableMapping from typing import cast import attr @@ -35,7 +35,7 @@ class ZhaDeviceStorage: def __init__(self, hass: HomeAssistantType) -> None: """Initialize the zha device storage.""" self.hass = hass - self.devices = {} # type: MutableMapping[str, ZhaDeviceEntry] + self.devices: MutableMapping[str, ZhaDeviceEntry] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback @@ -88,7 +88,7 @@ async def async_load(self) -> None: """Load the registry of zha device entries.""" data = await self._store.async_load() - devices = OrderedDict() # type: OrderedDict[str, ZhaDeviceEntry] + devices: "OrderedDict[str, ZhaDeviceEntry]" = OrderedDict() if data is not None: for device in data["devices"]: diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4b97aff19a8983..00671e9c7763ae 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -53,7 +53,7 @@ class RegistryEntry: device_id = attr.ib(type=str, default=None) config_entry_id = attr.ib(type=str, default=None) disabled_by = attr.ib( - type=str, + type=Optional[str], default=None, validator=attr.validators.in_( ( @@ -64,7 +64,7 @@ class RegistryEntry: None, ) ), - ) # type: Optional[str] + ) domain = attr.ib(type=str, init=False, repr=False) @domain.default diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 3afb5cb88e46dc..b7707b844d417a 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,5 +1,5 @@ """Helpers for listening to events.""" -from datetime import timedelta +from datetime import datetime, timedelta import functools as ft from typing import Callable @@ -21,8 +21,7 @@ from homeassistant.util.async_ import run_callback_threadsafe -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name @@ -187,7 +186,9 @@ def state_for_cancel_listener(entity, from_state, to_state): @callback @bind_hass -def async_track_point_in_time(hass, action, point_in_time) -> CALLBACK_TYPE: +def async_track_point_in_time( + hass: HomeAssistant, action: Callable[..., None], point_in_time: datetime +) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in time.""" utc_point_in_time = dt_util.as_utc(point_in_time) @@ -204,7 +205,9 @@ def utc_converter(utc_now): @callback @bind_hass -def async_track_point_in_utc_time(hass, action, point_in_time) -> CALLBACK_TYPE: +def async_track_point_in_utc_time( + hass: HomeAssistant, action: Callable[..., None], point_in_time: datetime +) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in UTC time.""" # Ensure point_in_time is UTC point_in_time = dt_util.as_utc(point_in_time) @@ -284,8 +287,8 @@ class SunListener: action = attr.ib(type=Callable) event = attr.ib(type=str) offset = attr.ib(type=timedelta) - _unsub_sun = attr.ib(default=None) - _unsub_config = attr.ib(default=None) + _unsub_sun: CALLBACK_TYPE = attr.ib(default=None) + _unsub_config: CALLBACK_TYPE = attr.ib(default=None) @callback def async_attach(self): diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 70284348157e76..1a9a3d256acb2d 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -322,9 +322,7 @@ def __init__(self, from_domain: str, to_domain: str) -> None: def _load_file( - hass, # type: HomeAssistant - comp_or_platform: str, - base_paths: List[str], + hass: "HomeAssistant", comp_or_platform: str, base_paths: List[str] ) -> Optional[ModuleType]: """Try to load specified file. @@ -391,11 +389,7 @@ def _load_file( class ModuleWrapper: """Class to wrap a Python module and auto fill in hass argument.""" - def __init__( - self, - hass, # type: HomeAssistant - module: ModuleType, - ) -> None: + def __init__(self, hass: "HomeAssistant", module: ModuleType) -> None: """Initialize the module wrapper.""" self._hass = hass self._module = module @@ -414,9 +408,7 @@ def __getattr__(self, attr: str) -> Any: class Components: """Helper to load components.""" - def __init__( - self, hass # type: HomeAssistant - ) -> None: + def __init__(self, hass: "HomeAssistant") -> None: """Initialize the Components class.""" self._hass = hass @@ -442,9 +434,7 @@ def __getattr__(self, comp_name: str) -> ModuleWrapper: class Helpers: """Helper to load helpers.""" - def __init__( - self, hass # type: HomeAssistant - ) -> None: + def __init__(self, hass: "HomeAssistant") -> None: """Initialize the Helpers class.""" self._hass = hass @@ -462,10 +452,7 @@ def bind_hass(func: CALLABLE_T) -> CALLABLE_T: return func -async def async_component_dependencies( - hass, # type: HomeAssistant - domain: str, -) -> Set[str]: +async def async_component_dependencies(hass: "HomeAssistant", domain: str) -> Set[str]: """Return all dependencies and subdependencies of components. Raises CircularDependency if a circular dependency is found. @@ -474,10 +461,7 @@ async def async_component_dependencies( async def _async_component_dependencies( - hass, # type: HomeAssistant - domain: str, - loaded: Set[str], - loading: Set, + hass: "HomeAssistant", domain: str, loaded: Set[str], loading: Set ) -> Set[str]: """Recursive function to get component dependencies. @@ -508,9 +492,7 @@ async def _async_component_dependencies( return loaded -def _async_mount_config_dir( - hass, # type: HomeAssistant -) -> bool: +def _async_mount_config_dir(hass: "HomeAssistant") -> bool: """Mount config dir in order to load custom_component. Async friendly but not a coroutine. diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index e06746915335b9..888ffd46c3b136 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -93,7 +93,7 @@ def last_state_change(self) -> datetime: @fixture(name="mock_bridge") def mock_bridge_fixture() -> Generator[None, Any, None]: """Fixture for mocking aioswitcher.bridge.SwitcherV2Bridge.""" - queue = Queue() # type: Queue + queue = Queue() async def mock_queue(): """Mock asyncio's Queue.""" From ece023bfee3a9d73bc42bbbe7654104712d4a793 Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 7 Sep 2019 15:35:51 +0200 Subject: [PATCH 0211/3953] Restructure Z-Wave Climate (#25724) --- homeassistant/components/zwave/climate.py | 45 +++++++++++++++++++---- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 2c7ce4b18a457c..b40fff669589c6 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -171,6 +171,28 @@ def supported_features(self): def update_properties(self): """Handle the data changes for node values.""" # Operation Mode + self._update_operation_mode() + + # Current Temp + self._update_current_temp() + + # Fan Mode + self._update_fan_mode() + + # Swing mode + self._update_swing_mode() + + # Set point + self._update_target_temp() + + # Operating state + self._update_operating_state() + + # Fan operating state + self._update_fan_state() + + def _update_operation_mode(self): + """Update hvac and preset modes.""" if self.values.mode: self._hvac_list = [] self._hvac_mapping = {} @@ -259,22 +281,27 @@ def update_properties(self): _LOGGER.debug("self._preset_list=%s", self._preset_list) _LOGGER.debug("self._preset_mode=%s", self._preset_mode) - # Current Temp + def _update_current_temp(self): + """Update current temperature.""" if self.values.temperature: self._current_temperature = self.values.temperature.data device_unit = self.values.temperature.units if device_unit is not None: self._unit = device_unit - # Fan Mode + def _update_fan_mode(self): + """Update fan mode.""" if self.values.fan_mode: self._current_fan_mode = self.values.fan_mode.data fan_modes = self.values.fan_mode.data_items if fan_modes: self._fan_modes = list(fan_modes) + _LOGGER.debug("self._fan_modes=%s", self._fan_modes) _LOGGER.debug("self._current_fan_mode=%s", self._current_fan_mode) - # Swing mode + + def _update_swing_mode(self): + """Update swing mode.""" if self._zxt_120 == 1: if self.values.zxt_120_swing_mode: self._current_swing_mode = self.values.zxt_120_swing_mode.data @@ -283,7 +310,9 @@ def update_properties(self): self._swing_modes = list(swing_modes) _LOGGER.debug("self._swing_modes=%s", self._swing_modes) _LOGGER.debug("self._current_swing_mode=%s", self._current_swing_mode) - # Set point + + def _update_target_temp(self): + """Update target temperature.""" if self.values.primary.data == 0: _LOGGER.debug( "Setpoint is 0, setting default to " "current_temperature=%s", @@ -294,12 +323,14 @@ def update_properties(self): else: self._target_temperature = round((float(self.values.primary.data)), 1) - # Operating state + def _update_operating_state(self): + """Update operating state.""" if self.values.operating_state: mode = self.values.operating_state.data self._hvac_action = HVAC_CURRENT_MAPPINGS.get(str(mode).lower(), mode) - # Fan operating state + def _update_fan_state(self): + """Update fan state.""" if self.values.fan_action: self._fan_action = self.values.fan_action.data @@ -448,7 +479,7 @@ def set_preset_mode(self, preset_mode): return if preset_mode == PRESET_NONE: # Activate the current hvac mode - self.update_properties() + self._update_operation_mode() operation_mode = self._hvac_mapping.get(self.hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) self.values.mode.data = operation_mode From 5237bd3fd136d661402cc7592c41a3b4baadc22c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 7 Sep 2019 10:39:56 -0400 Subject: [PATCH 0212/3953] fix cluster configuration (#26494) --- homeassistant/components/zha/core/channels/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 9e3b69a80df07c..c703f339e4337a 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -202,7 +202,7 @@ async def async_configure(self): # Xiaomi devices don't need this and it disrupts pairing if self._zha_device.manufacturer != "LUMI": await self.bind() - if self.cluster.cluster_id not in self.cluster.endpoint.out_clusters: + if self.cluster.cluster_id in self.cluster.endpoint.in_clusters: for report_config in self._report_config: await self.configure_reporting( report_config["attr"], report_config["config"] From da88be3827624982b25d2b79d84d91cb6a762b2c Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 7 Sep 2019 11:47:24 -0700 Subject: [PATCH 0213/3953] Bump androidtv to 0.0.27 (#26497) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_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 2c8fc98e24865a..6643faa85bdeb3 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.26" + "androidtv==0.0.27" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index 0b41bf319b8598..54c9f4efb7b5e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -197,7 +197,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.26 +androidtv==0.0.27 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5084472a68d32d..7654ea3e849132 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -79,7 +79,7 @@ aiowwlln==2.0.1 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.26 +androidtv==0.0.27 # homeassistant.components.apns apns2==0.3.0 From 28b9416b0cd7280ee0958006122f067f42167961 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 8 Sep 2019 02:07:11 +0200 Subject: [PATCH 0214/3953] Device automations: Rename name to entity_name in translations (#26491) --- homeassistant/components/light/strings.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 6b9e5d7d6493fe..460114b143cc78 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Toggle {name}", - "turn_on": "Turn on {name}", - "turn_off": "Turn off {name}" + "toggle": "Toggle {entity_name}", + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}" }, "condition_type": { - "is_on": "{name} is on", - "is_off": "{name} is off" + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" }, "trigger_type": { - "turn_on": "{name} turned on", - "turn_off": "{name} turned off" + "turn_on": "{entity_name} turned on", + "turn_off": "{entity_name} turned off" } } } From e204d22a9e4a37812ca783bf19f31cd1f8e62f5f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 8 Sep 2019 00:32:16 +0000 Subject: [PATCH 0215/3953] [ci skip] Translation update --- .../components/deconz/.translations/it.json | 2 +- .../geonetnz_quakes/.translations/it.json | 2 +- .../components/hangouts/.translations/it.json | 2 +- .../components/heos/.translations/ca.json | 2 +- .../components/heos/.translations/it.json | 2 +- .../homekit_controller/.translations/it.json | 12 +++++------ .../iaqualink/.translations/ca.json | 21 +++++++++++++++++++ .../iaqualink/.translations/it.json | 21 +++++++++++++++++++ .../iaqualink/.translations/ko.json | 21 +++++++++++++++++++ .../iaqualink/.translations/ru.json | 21 +++++++++++++++++++ .../iaqualink/.translations/zh-Hant.json | 21 +++++++++++++++++++ .../components/light/.translations/ca.json | 9 ++++++++ .../components/light/.translations/en.json | 14 ++++++------- .../components/light/.translations/ko.json | 9 ++++++++ .../light/.translations/zh-Hant.json | 9 ++++++++ .../components/linky/.translations/it.json | 2 +- .../components/notion/.translations/it.json | 2 +- .../components/notion/.translations/ko.json | 2 +- .../simplisafe/.translations/it.json | 2 +- .../components/tradfri/.translations/it.json | 2 +- .../twentemilieu/.translations/it.json | 2 +- .../components/unifi/.translations/it.json | 2 +- .../components/wwlln/.translations/ko.json | 2 +- 23 files changed, 158 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/iaqualink/.translations/ca.json create mode 100644 homeassistant/components/iaqualink/.translations/it.json create mode 100644 homeassistant/components/iaqualink/.translations/ko.json create mode 100644 homeassistant/components/iaqualink/.translations/ru.json create mode 100644 homeassistant/components/iaqualink/.translations/zh-Hant.json diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 9836a43f8a84b7..90b85aaeba5f01 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -23,7 +23,7 @@ "init": { "data": { "host": "Host", - "port": "Porta (valore di default: '80')" + "port": "Porta" }, "title": "Definisci il gateway deCONZ" }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/it.json b/homeassistant/components/geonetnz_quakes/.translations/it.json index a6fd4660a1551d..2a019aa39d94ae 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/it.json +++ b/homeassistant/components/geonetnz_quakes/.translations/it.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "mmi": "MMI", + "mmi": "Intensit\u00e0 in Scala Mercalli Modificata", "radius": "Raggio" }, "title": "Inserisci i tuoi dettagli del filtro." diff --git a/homeassistant/components/hangouts/.translations/it.json b/homeassistant/components/hangouts/.translations/it.json index 6983d190097c4b..ff0a8238d49caa 100644 --- a/homeassistant/components/hangouts/.translations/it.json +++ b/homeassistant/components/hangouts/.translations/it.json @@ -20,7 +20,7 @@ "user": { "data": { "authorization_code": "Codice di autorizzazione (necessario per l'autenticazione manuale)", - "email": "Indirizzo email", + "email": "Indirizzo E-mail", "password": "Password" }, "description": "Vuoto", diff --git a/homeassistant/components/heos/.translations/ca.json b/homeassistant/components/heos/.translations/ca.json index 05d95116b10f6b..60bd780547c8cc 100644 --- a/homeassistant/components/heos/.translations/ca.json +++ b/homeassistant/components/heos/.translations/ca.json @@ -4,7 +4,7 @@ "already_setup": "Nom\u00e9s pots configurar una \u00fanica connexi\u00f3 de Heos tot i que aquesta ja pot controlar tots els dispositius de la xarxa." }, "error": { - "connection_failure": "No es pot connectar amb l'amfitri\u00f3 especificat." + "connection_failure": "No s'ha pogut connectar amb l'amfitri\u00f3 especificat." }, "step": { "user": { diff --git a/homeassistant/components/heos/.translations/it.json b/homeassistant/components/heos/.translations/it.json index 20a4060add4ba5..824f7c3fb50d83 100644 --- a/homeassistant/components/heos/.translations/it.json +++ b/homeassistant/components/heos/.translations/it.json @@ -16,6 +16,6 @@ "title": "Connetti a Heos" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/it.json b/homeassistant/components/homekit_controller/.translations/it.json index a1e9f82144ef62..7ed026a529c2fd 100644 --- a/homeassistant/components/homekit_controller/.translations/it.json +++ b/homeassistant/components/homekit_controller/.translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "accessory_not_found_error": "Impossibile aggiungere l'associazione in quanto non \u00e8 pi\u00f9 possibile trovare il dispositivo.", + "accessory_not_found_error": "Impossibile aggiungere l'abbinamento in quanto non \u00e8 pi\u00f9 possibile trovare il dispositivo.", "already_configured": "L'accessorio \u00e8 gi\u00e0 configurato con questo controller.", "already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.", "already_paired": "Questo accessorio \u00e8 gi\u00e0 associato a un altro dispositivo. Si prega di resettare l'accessorio e riprovare.", @@ -11,10 +11,10 @@ }, "error": { "authentication_error": "Codice HomeKit errato. Per favore, controllate e riprovate.", - "busy_error": "Il dispositivo ha rifiutato di aggiungere l'associazione in quanto \u00e8 gi\u00e0 associato a un altro controller.", - "max_peers_error": "Il dispositivo ha rifiutato di aggiungere l'associazione in quanto non dispone di una memoria libera per l'associazione.", - "max_tries_error": "Il dispositivo ha rifiutato di aggiungere l'associazione poich\u00e9 ha ricevuto pi\u00f9 di 100 tentativi di autenticazione non riusciti.", - "pairing_failed": "Si \u00e8 verificato un errore non gestito durante il tentativo di associazione con questo dispositivo. Potrebbe trattarsi di un errore temporaneo o il dispositivo potrebbe non essere attualmente supportato.", + "busy_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento in quanto \u00e8 gi\u00e0 associato a un altro controller.", + "max_peers_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento in quanto non dispone di una memoria libera per esso.", + "max_tries_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento poich\u00e9 ha ricevuto pi\u00f9 di 100 tentativi di autenticazione non riusciti.", + "pairing_failed": "Si \u00e8 verificato un errore non gestito durante il tentativo di abbinamento con questo dispositivo. Potrebbe trattarsi di un errore temporaneo o il dispositivo potrebbe non essere attualmente supportato.", "unable_to_pair": "Impossibile abbinare, per favore riprova.", "unknown_error": "Il dispositivo ha riportato un errore sconosciuto. L'abbinamento non \u00e8 riuscito." }, @@ -24,7 +24,7 @@ "data": { "pairing_code": "Codice di abbinamento" }, - "description": "Inserisci il codice di abbinamento HomeKit per usare questo accessorio", + "description": "Immettere il codice di abbinamento HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio", "title": "Abbina con accessorio HomeKit" }, "user": { diff --git a/homeassistant/components/iaqualink/.translations/ca.json b/homeassistant/components/iaqualink/.translations/ca.json new file mode 100644 index 00000000000000..a5456c7b0cd0a9 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s pots configurar una \u00fanica connexi\u00f3 d'iAqualink." + }, + "error": { + "connection_failure": "No s'ha pogut connectar amb iAqualink. Comprova el nom d'usuari i la contrasenya." + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari / Correu electr\u00f2nic" + }, + "description": "Introdueix el nom d'usuari i la contrasenya del teu compte d'iAqualink.", + "title": "Connexi\u00f3 amb iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/it.json b/homeassistant/components/iaqualink/.translations/it.json new file mode 100644 index 00000000000000..73d840bdbd376c --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare una sola connessione iAqualink." + }, + "error": { + "connection_failure": "Impossibile connettersi a iAqualink. Controllare il nome utente e la password." + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome Utente / Indirizzo E-mail" + }, + "description": "Inserisci il nome utente e la password del tuo account iAqualink.", + "title": "Collegati a iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/ko.json b/homeassistant/components/iaqualink/.translations/ko.json new file mode 100644 index 00000000000000..9b2519077e26ce --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 iAqualink \uc5f0\uacb0\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_failure": "iAqualink \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984 / \uc774\uba54\uc77c \uc8fc\uc18c" + }, + "description": "iAqualink \uacc4\uc815\uc758 \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "iAqualink \uc5f0\uacb0" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/ru.json b/homeassistant/components/iaqualink/.translations/ru.json new file mode 100644 index 00000000000000..35444dd422b379 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a iAqualink. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0412\u0430\u0448 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", + "title": "Jandy iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/zh-Hant.json b/homeassistant/components/iaqualink/.translations/zh-Hant.json new file mode 100644 index 00000000000000..146088b4eff9c0 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 iAqualink \u9023\u7dda\u3002" + }, + "error": { + "connection_failure": "\u7121\u6cd5\u9023\u7dda\u81f3 iAqualink\uff0c\u8acb\u78ba\u8a8d\u60a8\u7684\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31 / \u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u8acb\u8f38\u5165 iAqualink \u5e33\u865f\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002", + "title": "\u9023\u7dda\u81f3 iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json index edafcd7eff071c..5017af8e576f64 100644 --- a/homeassistant/components/light/.translations/ca.json +++ b/homeassistant/components/light/.translations/ca.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Commuta {name}", + "turn_off": "Apaga {name}", + "turn_on": "Enc\u00e9n {name}" + }, + "condition_type": { + "is_off": "{name} est\u00e0 apagat", + "is_on": "{name} est\u00e0 enc\u00e8s" + }, "trigger_type": { "turn_off": "{name} apagat", "turn_on": "{name} enc\u00e8s" diff --git a/homeassistant/components/light/.translations/en.json b/homeassistant/components/light/.translations/en.json index 3d5290ca6ce719..60ccbd99348d7d 100644 --- a/homeassistant/components/light/.translations/en.json +++ b/homeassistant/components/light/.translations/en.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Toggle {name}", - "turn_off": "Turn off {name}", - "turn_on": "Turn on {name}" + "toggle": "Toggle {entity_name}", + "turn_off": "Turn off {entity_name}", + "turn_on": "Turn on {entity_name}" }, "condition_type": { - "is_off": "{name} is off", - "is_on": "{name} is on" + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on" }, "trigger_type": { - "turn_off": "{name} turned off", - "turn_on": "{name} turned on" + "turn_off": "{entity_name} turned off", + "turn_on": "{entity_name} turned on" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json index 3d0e9630bd3687..d62717a0725a4b 100644 --- a/homeassistant/components/light/.translations/ko.json +++ b/homeassistant/components/light/.translations/ko.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "{name} \ud1a0\uae00", + "turn_off": "{name} \ub044\uae30", + "turn_on": "{name} \ucf1c\uae30" + }, + "condition_type": { + "is_off": "{name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "is_on": "{name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + }, "trigger_type": { "turn_off": "{name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", "turn_on": "{name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/light/.translations/zh-Hant.json b/homeassistant/components/light/.translations/zh-Hant.json index 66ed83aeb9b7ba..269715b7cc33fe 100644 --- a/homeassistant/components/light/.translations/zh-Hant.json +++ b/homeassistant/components/light/.translations/zh-Hant.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "\u5207\u63db {name}", + "turn_off": "\u95dc\u9589 {name}", + "turn_on": "\u958b\u555f {name}" + }, + "condition_type": { + "is_off": "{name} \u5df2\u95dc\u9589", + "is_on": "{name} \u5df2\u958b\u555f" + }, "trigger_type": { "turn_off": "\u7531 {name} \u95dc\u9589", "turn_on": "\u7531 {name} \u958b\u555f" diff --git a/homeassistant/components/linky/.translations/it.json b/homeassistant/components/linky/.translations/it.json index bc9890bed2c760..09d5f7e2d2bcb4 100644 --- a/homeassistant/components/linky/.translations/it.json +++ b/homeassistant/components/linky/.translations/it.json @@ -8,7 +8,7 @@ "enedis": "Enedis.fr ha risposto con un errore: si prega di riprovare pi\u00f9 tardi (di solito non tra le 23:00 e le 02:00).", "unknown": "Errore sconosciuto: riprova pi\u00f9 tardi (in genere non tra le 23:00 e le 02:00)", "username_exists": "Account gi\u00e0 configurato", - "wrong_login": "Errore di accesso: si prega di controllare la tua e-mail e la password" + "wrong_login": "Errore di accesso: si prega di controllare la tua E-mail e la password" }, "step": { "user": { diff --git a/homeassistant/components/notion/.translations/it.json b/homeassistant/components/notion/.translations/it.json index 36f3f5fc7bcad3..035c0c38952707 100644 --- a/homeassistant/components/notion/.translations/it.json +++ b/homeassistant/components/notion/.translations/it.json @@ -9,7 +9,7 @@ "user": { "data": { "password": "Password", - "username": "Nome utente / indirizzo Email" + "username": "Nome utente / indirizzo E-mail" }, "title": "Inserisci le tue informazioni" } diff --git a/homeassistant/components/notion/.translations/ko.json b/homeassistant/components/notion/.translations/ko.json index 32eb4b688558a0..76dc91cf46b0a1 100644 --- a/homeassistant/components/notion/.translations/ko.json +++ b/homeassistant/components/notion/.translations/ko.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "no_devices": "\uacc4\uc815\uc5d0 \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + "no_devices": "\uacc4\uc815\uc5d0 \ub4f1\ub85d\ub41c \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/simplisafe/.translations/it.json b/homeassistant/components/simplisafe/.translations/it.json index 134bfae366821c..6f0e403a356548 100644 --- a/homeassistant/components/simplisafe/.translations/it.json +++ b/homeassistant/components/simplisafe/.translations/it.json @@ -9,7 +9,7 @@ "data": { "code": "Codice (Home Assistant)", "password": "Password", - "username": "Indirizzo email" + "username": "Indirizzo E-mail" }, "title": "Inserisci i tuoi dati" } diff --git a/homeassistant/components/tradfri/.translations/it.json b/homeassistant/components/tradfri/.translations/it.json index e58e296743fcc4..99ba9053d79792 100644 --- a/homeassistant/components/tradfri/.translations/it.json +++ b/homeassistant/components/tradfri/.translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Il bridge \u00e8 gi\u00e0 configurato", + "already_configured": "Bridge gi\u00e0 configurato.", "already_in_progress": "La configurazione del Bridge \u00e8 gi\u00e0 in corso." }, "error": { diff --git a/homeassistant/components/twentemilieu/.translations/it.json b/homeassistant/components/twentemilieu/.translations/it.json index cfdb594f120de3..27850d207b0591 100644 --- a/homeassistant/components/twentemilieu/.translations/it.json +++ b/homeassistant/components/twentemilieu/.translations/it.json @@ -12,7 +12,7 @@ "data": { "house_letter": "Edificio, Scala, Interno, ecc. / Informazioni aggiuntive", "house_number": "Numero civico", - "post_code": "Codice di avviamento postale" + "post_code": "Codice di Avviamento Postale" }, "description": "Imposta Twente Milieu fornendo le informazioni sulla raccolta dei rifiuti al tuo indirizzo.", "title": "Twente Milieu" diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json index ebf6b0c49323c6..c4c76c4232d2e7 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -28,7 +28,7 @@ "device_tracker": { "data": { "detection_time": "Tempo in secondi dall'ultima volta che viene visto fino a quando non \u00e8 considerato lontano", - "track_clients": "Monitorare i client di rete", + "track_clients": "Traccia i client di rete", "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)", "track_wired_clients": "Includi i client di rete cablata" } diff --git a/homeassistant/components/wwlln/.translations/ko.json b/homeassistant/components/wwlln/.translations/ko.json index 5e879cd7330311..e5831f5af29224 100644 --- a/homeassistant/components/wwlln/.translations/ko.json +++ b/homeassistant/components/wwlln/.translations/ko.json @@ -10,7 +10,7 @@ "longitude": "\uacbd\ub3c4", "radius": "\ubc18\uacbd (\uae30\ubcf8 \ub2e8\uc704 \uc2dc\uc2a4\ud15c \uc0ac\uc6a9)" }, - "title": "\uc704\uce58 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + "title": "\uc704\uce58 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." } }, "title": "\uc138\uacc4 \ub099\ub8b0 \uc704\uce58\ub9dd (WWLLN)" From 3544f3d7e03b52b8da605f63da45c886179fbc50 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 7 Sep 2019 22:14:32 -0400 Subject: [PATCH 0216/3953] Bump ZHA dependencies. (#26504) --- 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 bf97dca1c708eb..3fc31ce720ec80 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,8 +6,8 @@ "requirements": [ "bellows-homeassistant==0.9.1", "zha-quirks==0.0.22", - "zigpy-deconz==0.2.2", - "zigpy-homeassistant==0.7.1", + "zigpy-deconz==0.3.0", + "zigpy-homeassistant==0.7.2", "zigpy-xbee-homeassistant==0.4.0", "zigpy-zigate==0.2.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index 54c9f4efb7b5e7..097f1ccaad3b05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2005,10 +2005,10 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.2.2 +zigpy-deconz==0.3.0 # homeassistant.components.zha -zigpy-homeassistant==0.7.1 +zigpy-homeassistant==0.7.2 # homeassistant.components.zha zigpy-xbee-homeassistant==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7654ea3e849132..77cc25b03fd988 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -423,4 +423,4 @@ wakeonlan==1.1.6 zeroconf==0.23.0 # homeassistant.components.zha -zigpy-homeassistant==0.7.1 +zigpy-homeassistant==0.7.2 From 7614f9f3fbdd5a6ff138d5daa427eb7daa8c2769 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 8 Sep 2019 14:11:40 +0100 Subject: [PATCH 0217/3953] Bump geniushubclient (#26519) * bump client * bump again --- homeassistant/components/geniushub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 0c10f7010cfc55..b007dfdd7844b7 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.6.7" + "geniushub-client==0.6.11" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/requirements_all.txt b/requirements_all.txt index 097f1ccaad3b05..d40073c1ba3004 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -525,7 +525,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.6.7 +geniushub-client==0.6.11 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From 4390ccfd4d7384e616db3e65bf95ba64e2962b97 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 8 Sep 2019 16:02:31 +0200 Subject: [PATCH 0218/3953] Update to async_upnp_client==0.14.11 (#26515) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 4e7b11767beb5b..bf05d5c7f63fdd 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "Dlna dmr", "documentation": "https://www.home-assistant.io/components/dlna_dmr", "requirements": [ - "async-upnp-client==0.14.10" + "async-upnp-client==0.14.11" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 6120b6b3ca6d9e..9aec23a687ce0d 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/upnp", "requirements": [ - "async-upnp-client==0.14.10" + "async-upnp-client==0.14.11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d40073c1ba3004..33718fb8a3daf9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -228,7 +228,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.upnp -async-upnp-client==0.14.10 +async-upnp-client==0.14.11 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 From 036e0ade1f295b7e5043ec2970f99d075f5d487c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 8 Sep 2019 20:59:09 +0200 Subject: [PATCH 0219/3953] Updated frontend to 20190908.0 (#26524) --- 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 3659b40b7b01b7..bf5e062e434d8f 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==20190904.0" + "home-assistant-frontend==20190908.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6ec2ed358d5797..504f6516f84527 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==20190904.0 +home-assistant-frontend==20190908.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 33718fb8a3daf9..225227cc5b55ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -634,7 +634,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190904.0 +home-assistant-frontend==20190908.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77cc25b03fd988..db81eee7e186dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -179,7 +179,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190904.0 +home-assistant-frontend==20190908.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 0983367abe4e8efda0b82be61d8917ae04d9c7f6 Mon Sep 17 00:00:00 2001 From: Kevin McCormack Date: Sun, 8 Sep 2019 15:36:48 -0400 Subject: [PATCH 0220/3953] Add vivotek camera component (#26071) * Add vivotek camera component * Update vivotek camera compontent Use async request to enable/disable motion detection * Update Vivotek camera - Use HTTPS - Use IP address for still and stream * Update vivotek component - Add brand and model properties - Add state property - Use attribute to save motion detection status * Add vivotek camera to .coveragerc * Update vivotek camera Fix lint errors * Update vivotek camera Remove unused method * Update Vivotek integration to use libpyvivotek Use libpyvivotek instead of directly making HTTPS API calls. * Update Vivotek component Address code review. - Remove unused code - Replace async methods with synchronous methods - Update docstrings * Linter fixes for Vivotek component * Update Vivotek camera component - Add SSL option - Remove authentication options as only basic authentication is currently working * Update Vivotek camera component - Make frame rate configurable - Require username and password * Remove unused constants in Vivotek component * Update Vivotek camera integration - Use libpyvivotek v0.2.1 with better response parsing - Use add_entities instead of async_add_entities * Update Vivotek camera component - Build camera and stream source ouside VivotekCam - Remove unnecessary _stream_source attribute * Update Vivotek camera component - Move brand to constant - Move _supported_features to property * Update Vivotek camera compontent to remove unused property --- .coveragerc | 1 + homeassistant/components/vivotek/__init__.py | 1 + homeassistant/components/vivotek/camera.py | 120 ++++++++++++++++++ .../components/vivotek/manifest.json | 10 ++ requirements_all.txt | 3 + 5 files changed, 135 insertions(+) create mode 100644 homeassistant/components/vivotek/__init__.py create mode 100644 homeassistant/components/vivotek/camera.py create mode 100644 homeassistant/components/vivotek/manifest.json diff --git a/.coveragerc b/.coveragerc index 03ee2e4038ca9c..e96340dde1c43e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -694,6 +694,7 @@ omit = homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vicare/* + homeassistant/components/vivotek/camera.py homeassistant/components/vizio/media_player.py homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/media_player.py diff --git a/homeassistant/components/vivotek/__init__.py b/homeassistant/components/vivotek/__init__.py new file mode 100644 index 00000000000000..b5220b12a9b68a --- /dev/null +++ b/homeassistant/components/vivotek/__init__.py @@ -0,0 +1 @@ +"""The Vivotek camera component.""" diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py new file mode 100644 index 00000000000000..bf136731cb6a51 --- /dev/null +++ b/homeassistant/components/vivotek/camera.py @@ -0,0 +1,120 @@ +"""Support for Vivotek IP Cameras.""" + +import logging + +import voluptuous as vol +from libpyvivotek import VivotekCamera + +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_FRAMERATE = "framerate" + +DEFAULT_CAMERA_BRAND = "Vivotek" +DEFAULT_NAME = "Vivotek Camera" +DEFAULT_EVENT_0_KEY = "event_i0_enable" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a Vivotek IP Camera.""" + args = dict( + config=config, + cam=VivotekCamera( + host=config[CONF_IP_ADDRESS], + port=(443 if config[CONF_SSL] else 80), + verify_ssl=config[CONF_VERIFY_SSL], + usr=config[CONF_USERNAME], + pwd=config[CONF_PASSWORD], + ), + stream_source=( + "rtsp://%s:%s@%s:554/live.sdp", + config[CONF_USERNAME], + config[CONF_PASSWORD], + config[CONF_IP_ADDRESS], + ), + ) + add_entities([VivotekCam(**args)]) + + +class VivotekCam(Camera): + """A Vivotek IP camera.""" + + def __init__(self, config, cam, stream_source): + """Initialize a Vivotek camera.""" + super().__init__() + + self._cam = cam + self._frame_interval = 1 / config[CONF_FRAMERATE] + self._motion_detection_enabled = False + self._name = config[CONF_NAME] + self._stream_source = stream_source + + @property + def supported_features(self): + """Return supported features for this camera.""" + return SUPPORT_STREAM + + @property + def frame_interval(self): + """Return the interval between frames of the mjpeg stream.""" + return self._frame_interval + + def camera_image(self): + """Return bytes of camera image.""" + return self._cam.snapshot() + + @property + def name(self): + """Return the name of this device.""" + return self._name + + async def stream_source(self): + """Return the source of the stream.""" + return self._stream_source + + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return self._motion_detection_enabled + + def disable_motion_detection(self): + """Disable motion detection in camera.""" + response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 0) + self._motion_detection_enabled = int(response) == 1 + + def enable_motion_detection(self): + """Enable motion detection in camera.""" + response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 1) + self._motion_detection_enabled = int(response) == 1 + + @property + def brand(self): + """Return the camera brand.""" + return DEFAULT_CAMERA_BRAND + + @property + def model(self): + """Return the camera model.""" + return self._cam.model_name diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json new file mode 100644 index 00000000000000..8a6a37762d4fea --- /dev/null +++ b/homeassistant/components/vivotek/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "vivotek", + "name": "Vivotek", + "documentation": "https://www.home-assistant.io/components/vivotek", + "requirements": [ + "libpyvivotek==0.2.1" + ], + "dependencies": [], + "codeowners": [] +} diff --git a/requirements_all.txt b/requirements_all.txt index 225227cc5b55ab..e467f5ab2babf0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -726,6 +726,9 @@ libpurecool==0.5.0 # homeassistant.components.foscam libpyfoscam==1.0 +# homeassistant.components.vivotek +libpyvivotek==0.2.1 + # homeassistant.components.mikrotik librouteros==2.3.0 From 28beebac6108f9185a37704c3416b849d9d4417c Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sun, 8 Sep 2019 21:49:20 +0200 Subject: [PATCH 0221/3953] Enable SolarEdge config entries (#26282) * Initial commit for the solaredge configflow * rerun the hassfest script * Adding testcases * Rerun hassfest, problem with black? * Requirements for the tests * Remove CONF_MONITORED_CONDITIONS from configuration.yaml * Remove the options flow strings * Resolve some comments * Comments * More comments * Move the config from the sensor platform to the component itself * More comments * More comments * Added solaredge __init__ * Added more test to increase coverage --- .coveragerc | 1 + .../solaredge/.translations/en.json | 21 +++ .../components/solaredge/__init__.py | 42 ++++++ .../components/solaredge/config_flow.py | 98 +++++++++++++ homeassistant/components/solaredge/const.py | 68 +++++++++ .../components/solaredge/manifest.json | 1 + homeassistant/components/solaredge/sensor.py | 111 +++------------ .../components/solaredge/strings.json | 21 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/solaredge/__init__.py | 1 + .../components/solaredge/test_config_flow.py | 132 ++++++++++++++++++ 13 files changed, 412 insertions(+), 89 deletions(-) create mode 100644 homeassistant/components/solaredge/.translations/en.json create mode 100644 homeassistant/components/solaredge/config_flow.py create mode 100644 homeassistant/components/solaredge/const.py create mode 100644 homeassistant/components/solaredge/strings.json create mode 100644 tests/components/solaredge/__init__.py create mode 100644 tests/components/solaredge/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index e96340dde1c43e..c30c78ddf65bec 100644 --- a/.coveragerc +++ b/.coveragerc @@ -576,6 +576,7 @@ omit = homeassistant/components/snmp/* homeassistant/components/sochain/sensor.py homeassistant/components/socialblade/sensor.py + homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py homeassistant/components/solax/sensor.py diff --git a/homeassistant/components/solaredge/.translations/en.json b/homeassistant/components/solaredge/.translations/en.json new file mode 100644 index 00000000000000..3265e3bb1b0a86 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "SolarEdge", + "step": { + "user": { + "title": "Define the API parameters for this installation", + "data": { + "name": "The name of this installation", + "site_id": "The SolarEdge site-id", + "api_key": "The API key for this site" + } + } + }, + "error": { + "site_exists": "This site_id is already configured" + }, + "abort": { + "site_exists": "This site_id is already configured" + } + } +} diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index b675126c5fd69b..8909b970aafd2b 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -1 +1,43 @@ """The solaredge component.""" +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DEFAULT_NAME, DOMAIN, CONF_SITE_ID + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_SITE_ID): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Platform setup, do nothing.""" + if DOMAIN not in config: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config[DOMAIN]) + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Load the saved entities.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + return True diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py new file mode 100644 index 00000000000000..67f05d83aa0f17 --- /dev/null +++ b/homeassistant/components/solaredge/config_flow.py @@ -0,0 +1,98 @@ +"""Config flow for the SolarEdge platform.""" +import solaredge +import voluptuous as vol +from requests.exceptions import HTTPError, ConnectTimeout + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import slugify + +from .const import DOMAIN, DEFAULT_NAME, CONF_SITE_ID + + +@callback +def solaredge_entries(hass: HomeAssistant): + """Return the site_ids for the domain.""" + return set( + (entry.data[CONF_SITE_ID]) + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self) -> None: + """Initialize the config flow.""" + self._errors = {} + + def _site_in_configuration_exists(self, site_id) -> bool: + """Return True if site_id exists in configuration.""" + if site_id in solaredge_entries(self.hass): + return True + return False + + def _check_site(self, site_id, api_key) -> bool: + """Check if we can connect to the soleredge api service.""" + api = solaredge.Solaredge(api_key) + try: + response = api.get_details(site_id) + except (ConnectTimeout, HTTPError): + self._errors[CONF_SITE_ID] = "could_not_connect" + return False + try: + if response["details"]["status"].lower() != "active": + self._errors[CONF_SITE_ID] = "site_not_active" + return False + except KeyError: + self._errors[CONF_SITE_ID] = "api_failure" + return False + return True + + async def async_step_user(self, user_input=None): + """Step when user intializes a integration.""" + self._errors = {} + if user_input is not None: + name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) + if self._site_in_configuration_exists(user_input[CONF_SITE_ID]): + self._errors[CONF_SITE_ID] = "site_exists" + else: + site = user_input[CONF_SITE_ID] + api = user_input[CONF_API_KEY] + can_connect = await self.hass.async_add_executor_job( + self._check_site, site, api + ) + if can_connect: + return self.async_create_entry( + title=name, data={CONF_SITE_ID: site, CONF_API_KEY: api} + ) + + else: + user_input = {} + user_input[CONF_NAME] = DEFAULT_NAME + user_input[CONF_SITE_ID] = "" + user_input[CONF_API_KEY] = "" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): str, + vol.Required(CONF_SITE_ID, default=user_input[CONF_SITE_ID]): str, + vol.Required(CONF_API_KEY, default=user_input[CONF_API_KEY]): str, + } + ), + errors=self._errors, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + if self._site_in_configuration_exists(user_input[CONF_SITE_ID]): + return self.async_abort(reason="site_exists") + return await self.async_step_user(user_input) diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py new file mode 100644 index 00000000000000..0d3d1a0cb5fff7 --- /dev/null +++ b/homeassistant/components/solaredge/const.py @@ -0,0 +1,68 @@ +"""Constants for the SolarEdge Monitoring API.""" +from datetime import timedelta + +from homeassistant.const import POWER_WATT, ENERGY_WATT_HOUR + +DOMAIN = "solaredge" + +# Config for solaredge monitoring api requests. +CONF_SITE_ID = "site_id" + +DEFAULT_NAME = "SolarEdge" + +OVERVIEW_UPDATE_DELAY = timedelta(minutes=10) +DETAILS_UPDATE_DELAY = timedelta(hours=12) +INVENTORY_UPDATE_DELAY = timedelta(hours=12) +POWER_FLOW_UPDATE_DELAY = timedelta(minutes=10) + +SCAN_INTERVAL = timedelta(minutes=10) + +# Supported overview sensor types: +# Key: ['json_key', 'name', unit, icon, default] +SENSOR_TYPES = { + "lifetime_energy": [ + "lifeTimeData", + "Lifetime energy", + ENERGY_WATT_HOUR, + "mdi:solar-power", + False, + ], + "energy_this_year": [ + "lastYearData", + "Energy this year", + ENERGY_WATT_HOUR, + "mdi:solar-power", + False, + ], + "energy_this_month": [ + "lastMonthData", + "Energy this month", + ENERGY_WATT_HOUR, + "mdi:solar-power", + False, + ], + "energy_today": [ + "lastDayData", + "Energy today", + ENERGY_WATT_HOUR, + "mdi:solar-power", + False, + ], + "current_power": [ + "currentPower", + "Current Power", + POWER_WATT, + "mdi:solar-power", + True, + ], + "site_details": [None, "Site details", None, None, False], + "meters": ["meters", "Meters", None, None, False], + "sensors": ["sensors", "Sensors", None, None, False], + "gateways": ["gateways", "Gateways", None, None, False], + "batteries": ["batteries", "Batteries", None, None, False], + "inverters": ["inverters", "Inverters", None, None, False], + "power_consumption": ["LOAD", "Power Consumption", None, "mdi:flash", False], + "solar_power": ["PV", "Solar Power", None, "mdi:solar-power", False], + "grid_power": ["GRID", "Grid Power", None, "mdi:power-plug", False], + "storage_power": ["STORAGE", "Storage Power", None, "mdi:car-battery", False], +} diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index b2707a0a9372de..7452790cd6043f 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -6,6 +6,7 @@ "solaredge==0.0.2", "stringcase==1.2.0" ], + "config_flow": true, "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index cad81c3c3381eb..896596a2a34d01 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -1,102 +1,39 @@ """Support for SolarEdge Monitoring API.""" - -from datetime import timedelta import logging - -import voluptuous as vol +import solaredge from requests.exceptions import HTTPError, ConnectTimeout -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_API_KEY, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - POWER_WATT, - ENERGY_WATT_HOUR, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_API_KEY from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -# Config for solaredge monitoring api requests. -CONF_SITE_ID = "site_id" - -OVERVIEW_UPDATE_DELAY = timedelta(minutes=10) -DETAILS_UPDATE_DELAY = timedelta(hours=12) -INVENTORY_UPDATE_DELAY = timedelta(hours=12) -POWER_FLOW_UPDATE_DELAY = timedelta(minutes=10) - -SCAN_INTERVAL = timedelta(minutes=10) - -# Supported overview sensor types: -# Key: ['json_key', 'name', unit, icon] -SENSOR_TYPES = { - "lifetime_energy": [ - "lifeTimeData", - "Lifetime energy", - ENERGY_WATT_HOUR, - "mdi:solar-power", - ], - "energy_this_year": [ - "lastYearData", - "Energy this year", - ENERGY_WATT_HOUR, - "mdi:solar-power", - ], - "energy_this_month": [ - "lastMonthData", - "Energy this month", - ENERGY_WATT_HOUR, - "mdi:solar-power", - ], - "energy_today": [ - "lastDayData", - "Energy today", - ENERGY_WATT_HOUR, - "mdi:solar-power", - ], - "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"], - "site_details": [None, "Site details", None, None], - "meters": ["meters", "Meters", None, None], - "sensors": ["sensors", "Sensors", None, None], - "gateways": ["gateways", "Gateways", None, None], - "batteries": ["batteries", "Batteries", None, None], - "inverters": ["inverters", "Inverters", None, None], - "power_consumption": ["LOAD", "Power Consumption", None, "mdi:flash"], - "solar_power": ["PV", "Solar Power", None, "mdi:solar-power"], - "grid_power": ["GRID", "Grid Power", None, "mdi:power-plug"], - "storage_power": ["STORAGE", "Storage Power", None, "mdi:car-battery"], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_SITE_ID): cv.string, - vol.Optional(CONF_NAME, default="SolarEdge"): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=["current_power"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - } +from .const import ( + CONF_SITE_ID, + OVERVIEW_UPDATE_DELAY, + DETAILS_UPDATE_DELAY, + INVENTORY_UPDATE_DELAY, + POWER_FLOW_UPDATE_DELAY, + SENSOR_TYPES, ) _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Create the SolarEdge Monitoring API sensor.""" - import solaredge +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old configuration.""" + pass - api_key = config[CONF_API_KEY] - site_id = config[CONF_SITE_ID] - platform_name = config[CONF_NAME] - # Create new SolarEdge object to retrieve data - api = solaredge.Solaredge(api_key) +async def async_setup_entry(hass, entry, async_add_entities): + """Add an solarEdge entry.""" + # Add the needed sensors to hass + api = solaredge.Solaredge(entry.data[CONF_API_KEY]) # Check if api can be reached and site is active try: - response = api.get_details(site_id) - + response = await hass.async_add_executor_job( + api.get_details, entry.data[CONF_SITE_ID] + ) if response["details"]["status"].lower() != "active": _LOGGER.error("SolarEdge site is not active") return @@ -108,17 +45,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Could not retrieve details from SolarEdge API") return - # Create sensor factory that will create sensors based on sensor_key. - sensor_factory = SolarEdgeSensorFactory(platform_name, site_id, api) - - # Create a new sensor for each sensor type. + sensor_factory = SolarEdgeSensorFactory(entry.title, entry.data[CONF_SITE_ID], api) entities = [] - for sensor_key in config[CONF_MONITORED_CONDITIONS]: + for sensor_key in SENSOR_TYPES: sensor = sensor_factory.create_sensor(sensor_key) if sensor is not None: entities.append(sensor) - - add_entities(entities, True) + async_add_entities(entities) class SolarEdgeSensorFactory: diff --git a/homeassistant/components/solaredge/strings.json b/homeassistant/components/solaredge/strings.json new file mode 100644 index 00000000000000..3265e3bb1b0a86 --- /dev/null +++ b/homeassistant/components/solaredge/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "SolarEdge", + "step": { + "user": { + "title": "Define the API parameters for this installation", + "data": { + "name": "The name of this installation", + "site_id": "The SolarEdge site-id", + "api_key": "The API key for this site" + } + } + }, + "error": { + "site_exists": "This site_id is already configured" + }, + "abort": { + "site_exists": "This site_id is already configured" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1dffe2d8e6bd82..a2f9c26949a706 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -50,6 +50,7 @@ "simplisafe", "smartthings", "smhi", + "solaredge", "somfy", "sonos", "tellduslive", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db81eee7e186dd..8582683d5f2241 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -386,6 +386,9 @@ sleepyq==0.7 # homeassistant.components.smhi smhi-pkg==1.0.10 +# homeassistant.components.solaredge +solaredge==0.0.2 + # homeassistant.components.honeywell somecomfort==0.5.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7c49055131b7d4..ad6507b4e9ea60 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -157,6 +157,7 @@ "simplisafe-python", "sleepyq", "smhi-pkg", + "solaredge", "somecomfort", "sqlalchemy", "srpenergy", diff --git a/tests/components/solaredge/__init__.py b/tests/components/solaredge/__init__.py new file mode 100644 index 00000000000000..c2a54cfafb623c --- /dev/null +++ b/tests/components/solaredge/__init__.py @@ -0,0 +1 @@ +"""Tests for the SolarEdge component.""" diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py new file mode 100644 index 00000000000000..c1183147bac5e2 --- /dev/null +++ b/tests/components/solaredge/test_config_flow.py @@ -0,0 +1,132 @@ +"""Tests for the SolarEdge config flow.""" +import pytest +from requests.exceptions import HTTPError, ConnectTimeout +from unittest.mock import patch, Mock + +from homeassistant import data_entry_flow +from homeassistant.components.solaredge import config_flow +from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME +from homeassistant.const import CONF_NAME, CONF_API_KEY + +from tests.common import MockConfigEntry + +NAME = "solaredge site 1 2 3" +SITE_ID = "1a2b3c4d5e6f7g8h" +API_KEY = "a1b2c3d4e5f6g7h8" + + +@pytest.fixture(name="test_api") +def mock_controller(): + """Mock a successfull Solaredge API.""" + api = Mock() + api.get_details.return_value = {"details": {"status": "active"}} + with patch("solaredge.Solaredge", return_value=api): + yield api + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.SolarEdgeConfigFlow() + flow.hass = hass + return flow + + +async def test_user(hass, test_api): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # tets with all provided + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solaredge_site_1_2_3" + assert result["data"][CONF_SITE_ID] == SITE_ID + assert result["data"][CONF_API_KEY] == API_KEY + + +async def test_import(hass, test_api): + """Test import step.""" + flow = init_config_flow(hass) + + # import with site_id and api_key + result = await flow.async_step_import( + {CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solaredge" + assert result["data"][CONF_SITE_ID] == SITE_ID + assert result["data"][CONF_API_KEY] == API_KEY + + # import with all + result = await flow.async_step_import( + {CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID, CONF_NAME: NAME} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solaredge_site_1_2_3" + assert result["data"][CONF_SITE_ID] == SITE_ID + assert result["data"][CONF_API_KEY] == API_KEY + + +async def test_abort_if_already_setup(hass, test_api): + """Test we abort if the site_id is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain="solaredge", + data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}, + ).add_to_hass(hass) + + # import: Should fail, same SITE_ID + result = await flow.async_step_import( + {CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "site_exists" + + # user: Should fail, same SITE_ID + result = await flow.async_step_user( + {CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_SITE_ID: "site_exists"} + + +async def test_asserts(hass, test_api): + """Test the _site_in_configuration_exists method.""" + flow = init_config_flow(hass) + + # test with inactive site + test_api.get_details.return_value = {"details": {"status": "NOK"}} + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_SITE_ID: "site_not_active"} + + # test with api_failure + test_api.get_details.return_value = {} + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_SITE_ID: "api_failure"} + + # test with ConnectionTimeout + test_api.get_details.side_effect = ConnectTimeout() + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_SITE_ID: "could_not_connect"} + + # test with HTTPError + test_api.get_details.side_effect = HTTPError() + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_SITE_ID: "could_not_connect"} From fec6706bf7dbeb4919c732d5b091d9868d21695b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 9 Sep 2019 09:21:34 -0400 Subject: [PATCH 0222/3953] use newly added is_server for cluster reporting (#26533) --- homeassistant/components/zha/core/channels/__init__.py | 2 +- tests/components/zha/common.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index c703f339e4337a..aed12bc65a5495 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -202,7 +202,7 @@ async def async_configure(self): # Xiaomi devices don't need this and it disrupts pairing if self._zha_device.manufacturer != "LUMI": await self.bind() - if self.cluster.cluster_id in self.cluster.endpoint.in_clusters: + if self.cluster.is_server: for report_config in self._report_config: await self.configure_reporting( report_config["attr"], report_config["config"] diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index d34c6983528515..fc29e4012cd6f8 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -50,7 +50,7 @@ def add_input_cluster(self, cluster_id): """Add an input cluster.""" from zigpy.zcl import Cluster - cluster = Cluster.from_id(self, cluster_id) + cluster = Cluster.from_id(self, cluster_id, is_server=True) patch_cluster(cluster) self.in_clusters[cluster_id] = cluster if hasattr(cluster, "ep_attribute"): @@ -60,7 +60,7 @@ def add_output_cluster(self, cluster_id): """Add an output cluster.""" from zigpy.zcl import Cluster - cluster = Cluster.from_id(self, cluster_id) + cluster = Cluster.from_id(self, cluster_id, is_server=False) patch_cluster(cluster) self.out_clusters[cluster_id] = cluster From 5f13cdf760c7609376a9938bf91e16d80ade5968 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 9 Sep 2019 16:24:24 +0200 Subject: [PATCH 0223/3953] 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 77e9cbb95f11a8..b1e6ff6a0a524b 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -18,7 +18,7 @@ schedules: always: true variables: - name: versionWheels - value: '1.1-3.7-alpine3.10' + value: '1.3-3.7-alpine3.10' resources: repositories: - repository: azure From b14b14c3c95d8bf474ffd5ce0f37d1cce748429d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 Sep 2019 16:55:47 +0200 Subject: [PATCH 0224/3953] Add device automation support to switch entities (#26466) * Add device automation support to switch entities * Update switch translations * Address review comments --- .../device_automation/toggle_entity.py | 189 +++++++++ .../components/light/device_automation.py | 195 +-------- .../components/switch/device_automation.py | 56 +++ homeassistant/components/switch/strings.json | 17 + homeassistant/helpers/script.py | 4 +- .../light/test_device_automation.py | 68 ++-- .../switch/test_device_automation.py | 372 ++++++++++++++++++ 7 files changed, 686 insertions(+), 215 deletions(-) create mode 100644 homeassistant/components/device_automation/toggle_entity.py create mode 100644 homeassistant/components/switch/device_automation.py create mode 100644 homeassistant/components/switch/strings.json create mode 100644 tests/components/switch/test_device_automation.py diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py new file mode 100644 index 00000000000000..2a2a7aaca18866 --- /dev/null +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -0,0 +1,189 @@ +"""Device automation helpers for toggle entity.""" +import voluptuous as vol + +import homeassistant.components.automation.state as state +from homeassistant.components.device_automation.const import ( + CONF_IS_OFF, + CONF_IS_ON, + CONF_TOGGLE, + CONF_TURN_OFF, + CONF_TURN_ON, +) +from homeassistant.core import split_entity_id +from homeassistant.const import ( + CONF_CONDITION, + CONF_DEVICE, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import condition, config_validation as cv, service + +ENTITY_ACTIONS = [ + { + # Turn entity off + CONF_DEVICE: None, + CONF_TYPE: CONF_TURN_OFF, + }, + { + # Turn entity on + CONF_DEVICE: None, + CONF_TYPE: CONF_TURN_ON, + }, + { + # Toggle entity + CONF_DEVICE: None, + CONF_TYPE: CONF_TOGGLE, + }, +] + +ENTITY_CONDITIONS = [ + { + # True when entity is turned off + CONF_CONDITION: "device", + CONF_TYPE: CONF_IS_OFF, + }, + { + # True when entity is turned on + CONF_CONDITION: "device", + CONF_TYPE: CONF_IS_ON, + }, +] + +ENTITY_TRIGGERS = [ + { + # Trigger when entity is turned off + CONF_PLATFORM: "device", + CONF_TYPE: CONF_TURN_OFF, + }, + { + # Trigger when entity is turned on + CONF_PLATFORM: "device", + CONF_TYPE: CONF_TURN_ON, + }, +] + +ACTION_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE): None, + vol.Optional(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): str, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]), + } +) + +CONDITION_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONDITION): "device", + vol.Optional(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): str, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), + } +) + +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "device", + vol.Optional(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): str, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In([CONF_TURN_OFF, CONF_TURN_ON]), + } +) + + +def _is_domain(entity, domain): + return split_entity_id(entity.entity_id)[0] == domain + + +async def async_call_action_from_config(hass, config, variables, context, domain): + """Change state based on configuration.""" + config = ACTION_SCHEMA(config) + action_type = config[CONF_TYPE] + if action_type == CONF_TURN_ON: + action = "turn_on" + elif action_type == CONF_TURN_OFF: + action = "turn_off" + else: + action = "toggle" + + service_action = { + service.CONF_SERVICE: "{}.{}".format(domain, action), + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + } + + await service.async_call_from_config( + hass, service_action, blocking=True, variables=variables, context=context + ) + + +def async_condition_from_config(config, config_validation): + """Evaluate state based on configuration.""" + condition_type = config[CONF_TYPE] + if condition_type == CONF_IS_ON: + stat = "on" + else: + stat = "off" + state_config = { + condition.CONF_CONDITION: "state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + condition.CONF_STATE: stat, + } + + return condition.state_from_config(state_config, config_validation) + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + trigger_type = config[CONF_TYPE] + if trigger_type == CONF_TURN_ON: + from_state = "off" + to_state = "on" + else: + from_state = "on" + to_state = "off" + state_config = { + state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + + return await state.async_trigger(hass, state_config, action, automation_info) + + +async def _async_get_automations(hass, device_id, automation_templates, domain): + """List device automations.""" + automations = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entities = async_entries_for_device(entity_registry, device_id) + domain_entities = [x for x in entities if _is_domain(x, domain)] + for entity in domain_entities: + for automation in automation_templates: + automation = dict(automation) + automation.update( + device_id=device_id, entity_id=entity.entity_id, domain=domain + ) + automations.append(automation) + + return automations + + +async def async_get_actions(hass, device_id, domain): + """List device actions.""" + return await _async_get_automations(hass, device_id, ENTITY_ACTIONS, domain) + + +async def async_get_conditions(hass, device_id, domain): + """List device conditions.""" + return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain) + + +async def async_get_triggers(hass, device_id, domain): + """List device triggers.""" + return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) diff --git a/homeassistant/components/light/device_automation.py b/homeassistant/components/light/device_automation.py index 4ddba8b9423dde..61292d47449adf 100644 --- a/homeassistant/components/light/device_automation.py +++ b/homeassistant/components/light/device_automation.py @@ -1,215 +1,56 @@ """Provides device automations for lights.""" import voluptuous as vol -import homeassistant.components.automation.state as state -from homeassistant.components.device_automation.const import ( - CONF_IS_OFF, - CONF_IS_ON, - CONF_TOGGLE, - CONF_TURN_OFF, - CONF_TURN_ON, -) -from homeassistant.core import split_entity_id -from homeassistant.const import ( - CONF_CONDITION, - CONF_DEVICE, - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_ENTITY_ID, - CONF_PLATFORM, - CONF_TYPE, -) -from homeassistant.helpers import condition, config_validation as cv, service -from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN from . import DOMAIN # mypy: allow-untyped-defs, no-check-untyped-defs -ENTITY_ACTIONS = [ - { - # Turn light off - CONF_DEVICE: None, - CONF_DOMAIN: DOMAIN, - CONF_TYPE: CONF_TURN_OFF, - }, - { - # Turn light on - CONF_DEVICE: None, - CONF_DOMAIN: DOMAIN, - CONF_TYPE: CONF_TURN_ON, - }, - { - # Toggle light - CONF_DEVICE: None, - CONF_DOMAIN: DOMAIN, - CONF_TYPE: CONF_TOGGLE, - }, -] - -ENTITY_CONDITIONS = [ - { - # True when light is turned off - CONF_CONDITION: "device", - CONF_DOMAIN: DOMAIN, - CONF_TYPE: CONF_IS_OFF, - }, - { - # True when light is turned on - CONF_CONDITION: "device", - CONF_DOMAIN: DOMAIN, - CONF_TYPE: CONF_IS_ON, - }, -] - -ENTITY_TRIGGERS = [ - { - # Trigger when light is turned off - CONF_PLATFORM: "device", - CONF_DOMAIN: DOMAIN, - CONF_TYPE: CONF_TURN_OFF, - }, - { - # Trigger when light is turned on - CONF_PLATFORM: "device", - CONF_DOMAIN: DOMAIN, - CONF_TYPE: CONF_TURN_ON, - }, -] - -ACTION_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_DEVICE): None, - vol.Optional(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]), - } - ) -) +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) -CONDITION_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_CONDITION): "device", - vol.Optional(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), - } - ) +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} ) -TRIGGER_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_PLATFORM): "device", - vol.Optional(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In([CONF_TURN_OFF, CONF_TURN_ON]), - } - ) +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} ) -def _is_domain(entity, domain): - return split_entity_id(entity.entity_id)[0] == domain - - -async def async_action_from_config(hass, config, variables, context): +async def async_call_action_from_config(hass, config, variables, context): """Change state based on configuration.""" config = ACTION_SCHEMA(config) - action_type = config[CONF_TYPE] - if action_type == CONF_TURN_ON: - action = "turn_on" - elif action_type == CONF_TURN_OFF: - action = "turn_off" - else: - action = "toggle" - service_action = { - service.CONF_SERVICE: "light.{}".format(action), - CONF_ENTITY_ID: config[CONF_ENTITY_ID], - } - - await service.async_call_from_config( - hass, - service_action, - blocking=True, - variables=variables, - # validate_config=False, - context=context, + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN ) def async_condition_from_config(config, config_validation): """Evaluate state based on configuration.""" config = CONDITION_SCHEMA(config) - condition_type = config[CONF_TYPE] - if condition_type == CONF_IS_ON: - stat = "on" - else: - stat = "off" - state_config = { - condition.CONF_CONDITION: "state", - condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - condition.CONF_STATE: stat, - } - - return condition.state_from_config(state_config, config_validation) + return toggle_entity.async_condition_from_config(config, config_validation) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" config = TRIGGER_SCHEMA(config) - trigger_type = config[CONF_TYPE] - if trigger_type == CONF_TURN_ON: - from_state = "off" - to_state = "on" - else: - from_state = "on" - to_state = "off" - state_config = { - state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, - state.CONF_TO: to_state, - } - - return await state.async_trigger(hass, state_config, action, automation_info) - - -async def async_trigger(hass, config, action, automation_info): - """Temporary so existing automation framework can be used for testing.""" - return await async_attach_trigger(hass, config, action, automation_info) - - -async def _async_get_automations(hass, device_id, automation_templates): - """List device automations.""" - automations = [] - entity_registry = await hass.helpers.entity_registry.async_get_registry() - - entities = async_entries_for_device(entity_registry, device_id) - domain_entities = [x for x in entities if _is_domain(x, DOMAIN)] - for entity in domain_entities: - for automation in automation_templates: - automation = dict(automation) - automation.update(device_id=device_id, entity_id=entity.entity_id) - automations.append(automation) - - return automations + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info + ) async def async_get_actions(hass, device_id): """List device actions.""" - return await _async_get_automations(hass, device_id, ENTITY_ACTIONS) + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) async def async_get_conditions(hass, device_id): """List device conditions.""" - return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS) + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) async def async_get_triggers(hass, device_id): """List device triggers.""" - return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS) + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_automation.py b/homeassistant/components/switch/device_automation.py new file mode 100644 index 00000000000000..61292d47449adf --- /dev/null +++ b/homeassistant/components/switch/device_automation.py @@ -0,0 +1,56 @@ +"""Provides device automations for lights.""" +import voluptuous as vol + +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from . import DOMAIN + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +async def async_call_action_from_config(hass, config, variables, context): + """Change state based on configuration.""" + config = ACTION_SCHEMA(config) + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) + + +def async_condition_from_config(config, config_validation): + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config, config_validation) + + +async def async_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info + ) + + +async def async_get_actions(hass, device_id): + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) + + +async def async_get_conditions(hass, device_id): + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json new file mode 100644 index 00000000000000..857b3763076143 --- /dev/null +++ b/homeassistant/components/switch/strings.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Toggle {entity_name}", + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}" + }, + "condition_type": { + "turn_on": "{entity_name} turned on", + "turn_off": "{entity_name} turned off" + }, + "trigger_type": { + "turn_on": "{entity_name} turned on", + "turn_off": "{entity_name} turned off" + } + } +} diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1d8f915543f47b..81a6cd059bb74d 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -333,7 +333,9 @@ async def _async_device_automation(self, action, variables, context): self._log("Executing step %s" % self.last_action) integration = await async_get_integration(self.hass, action[CONF_DOMAIN]) platform = integration.get_platform("device_automation") - await platform.async_action_from_config(self.hass, action, variables, context) + await platform.async_call_action_from_config( + self.hass, action, variables, context + ) async def _async_fire_event(self, action, variables, context): """Fire an event.""" diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py index 38c573b514fb78..6bbd8b5c31931e 100644 --- a/tests/components/light/test_device_automation.py +++ b/tests/components/light/test_device_automation.py @@ -1,7 +1,7 @@ """The test for light device automation.""" import pytest -from homeassistant.components import light +from homeassistant.components.light import DOMAIN from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation @@ -54,28 +54,28 @@ async def test_get_actions(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_actions = [ { "device": None, - "domain": "light", + "domain": DOMAIN, "type": "turn_off", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": f"{DOMAIN}.test_5678", }, { "device": None, - "domain": "light", + "domain": DOMAIN, "type": "turn_on", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": f"{DOMAIN}.test_5678", }, { "device": None, - "domain": "light", + "domain": DOMAIN, "type": "toggle", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": f"{DOMAIN}.test_5678", }, ] actions = await async_get_device_automations( @@ -92,21 +92,21 @@ async def test_get_conditions(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_conditions = [ { "condition": "device", - "domain": "light", + "domain": DOMAIN, "type": "is_off", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": f"{DOMAIN}.test_5678", }, { "condition": "device", - "domain": "light", + "domain": DOMAIN, "type": "is_on", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": f"{DOMAIN}.test_5678", }, ] conditions = await async_get_device_automations( @@ -123,21 +123,21 @@ async def test_get_triggers(hass, device_reg, entity_reg): config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_triggers = [ { "platform": "device", - "domain": "light", + "domain": DOMAIN, "type": "turn_off", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": f"{DOMAIN}.test_5678", }, { "platform": "device", - "domain": "light", + "domain": DOMAIN, "type": "turn_on", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": f"{DOMAIN}.test_5678", }, ] triggers = await async_get_device_automations( @@ -148,12 +148,10 @@ async def test_get_triggers(hass, device_reg, entity_reg): async def test_if_fires_on_state_change(hass, calls): """Test for turn_on and turn_off triggers firing.""" - platform = getattr(hass.components, "test.light") + platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - assert await async_setup_component( - hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} - ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) dev1, dev2, dev3 = platform.DEVICES @@ -165,7 +163,7 @@ async def test_if_fires_on_state_change(hass, calls): { "trigger": { "platform": "device", - "domain": light.DOMAIN, + "domain": DOMAIN, "entity_id": dev1.entity_id, "type": "turn_on", }, @@ -188,7 +186,7 @@ async def test_if_fires_on_state_change(hass, calls): { "trigger": { "platform": "device", - "domain": light.DOMAIN, + "domain": DOMAIN, "entity_id": dev1.entity_id, "type": "turn_off", }, @@ -232,12 +230,10 @@ async def test_if_fires_on_state_change(hass, calls): async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" - platform = getattr(hass.components, "test.light") + platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - assert await async_setup_component( - hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} - ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) dev1, dev2, dev3 = platform.DEVICES @@ -251,7 +247,7 @@ async def test_if_state(hass, calls): "condition": [ { "condition": "device", - "domain": "light", + "domain": DOMAIN, "entity_id": dev1.entity_id, "type": "is_on", } @@ -269,7 +265,7 @@ async def test_if_state(hass, calls): "condition": [ { "condition": "device", - "domain": "light", + "domain": DOMAIN, "entity_id": dev1.entity_id, "type": "is_off", } @@ -305,12 +301,10 @@ async def test_if_state(hass, calls): async def test_action(hass, calls): """Test for turn_on and turn_off actions.""" - platform = getattr(hass.components, "test.light") + platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() - assert await async_setup_component( - hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} - ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) dev1, dev2, dev3 = platform.DEVICES @@ -323,7 +317,7 @@ async def test_action(hass, calls): "trigger": {"platform": "event", "event_type": "test_event1"}, "action": { "device": None, - "domain": "light", + "domain": DOMAIN, "entity_id": dev1.entity_id, "type": "turn_off", }, @@ -332,7 +326,7 @@ async def test_action(hass, calls): "trigger": {"platform": "event", "event_type": "test_event2"}, "action": { "device": None, - "domain": "light", + "domain": DOMAIN, "entity_id": dev1.entity_id, "type": "turn_on", }, @@ -341,7 +335,7 @@ async def test_action(hass, calls): "trigger": {"platform": "event", "event_type": "test_event3"}, "action": { "device": None, - "domain": "light", + "domain": DOMAIN, "entity_id": dev1.entity_id, "type": "toggle", }, diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py new file mode 100644 index 00000000000000..56da6767c1dd3d --- /dev/null +++ b/tests/components/switch/test_device_automation.py @@ -0,0 +1,372 @@ +"""The test for switch device automation.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +def _same_lists(a, b): + if len(a) != len(b): + return False + + for d in a: + if d not in b: + return False + return True + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "device": None, + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "device": None, + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "device": None, + "domain": DOMAIN, + "type": "toggle", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + actions = await async_get_device_automations( + hass, "async_get_actions", device_entry.id + ) + assert _same_lists(actions, expected_actions) + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations( + hass, "async_get_conditions", device_entry.id + ) + assert _same_lists(conditions, expected_conditions) + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations( + hass, "async_get_triggers", device_entry.id + ) + assert _same_lists(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + dev1, dev2, dev3 = platform.DEVICES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "entity_id": dev1.entity_id, + "type": "turn_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "entity_id": dev1.entity_id, + "type": "turn_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(dev1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( + dev1.entity_id + ) + + hass.states.async_set(dev1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( + dev1.entity_id + ) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + dev1, dev2, dev3 = platform.DEVICES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "entity_id": dev1.entity_id, + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "entity_id": dev1.entity_id, + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_action(hass, calls): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + dev1, dev2, dev3 = platform.DEVICES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": { + "device": None, + "domain": DOMAIN, + "entity_id": dev1.entity_id, + "type": "turn_off", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": { + "device": None, + "domain": DOMAIN, + "entity_id": dev1.entity_id, + "type": "turn_on", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "action": { + "device": None, + "domain": DOMAIN, + "entity_id": dev1.entity_id, + "type": "toggle", + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(dev1.entity_id).state == STATE_ON From 02ded7a4a81c2d033b8f60aa2f4ca2b0a6c05454 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 10 Sep 2019 01:40:22 +0800 Subject: [PATCH 0225/3953] Remove device from device action schema (#26536) --- .../device_automation/toggle_entity.py | 17 ++++++----------- homeassistant/helpers/config_validation.py | 4 ++-- homeassistant/helpers/script.py | 9 +++++++-- tests/components/device_automation/test_init.py | 3 --- .../components/light/test_device_automation.py | 13 +++++++------ .../components/switch/test_device_automation.py | 13 +++++++------ 6 files changed, 29 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 2a2a7aaca18866..722b92e33f2603 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -12,7 +12,6 @@ from homeassistant.core import split_entity_id from homeassistant.const import ( CONF_CONDITION, - CONF_DEVICE, CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, @@ -25,18 +24,15 @@ ENTITY_ACTIONS = [ { # Turn entity off - CONF_DEVICE: None, - CONF_TYPE: CONF_TURN_OFF, + CONF_TYPE: CONF_TURN_OFF }, { # Turn entity on - CONF_DEVICE: None, - CONF_TYPE: CONF_TURN_ON, + CONF_TYPE: CONF_TURN_ON }, { # Toggle entity - CONF_DEVICE: None, - CONF_TYPE: CONF_TOGGLE, + CONF_TYPE: CONF_TOGGLE }, ] @@ -68,8 +64,7 @@ ACTION_SCHEMA = vol.Schema( { - vol.Required(CONF_DEVICE): None, - vol.Optional(CONF_DEVICE_ID): str, + vol.Required(CONF_DEVICE_ID): str, vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]), @@ -79,7 +74,7 @@ CONDITION_SCHEMA = vol.Schema( { vol.Required(CONF_CONDITION): "device", - vol.Optional(CONF_DEVICE_ID): str, + vol.Required(CONF_DEVICE_ID): str, vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), @@ -89,7 +84,7 @@ TRIGGER_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): "device", - vol.Optional(CONF_DEVICE_ID): str, + vol.Required(CONF_DEVICE_ID): str, vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TURN_OFF, CONF_TURN_ON]), diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 743a848ff93818..e53954a65ddb0d 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -24,7 +24,7 @@ CONF_ALIAS, CONF_BELOW, CONF_CONDITION, - CONF_DEVICE, + CONF_DEVICE_ID, CONF_DOMAIN, CONF_ENTITY_ID, CONF_ENTITY_NAMESPACE, @@ -863,7 +863,7 @@ def validator(value): ) DEVICE_ACTION_SCHEMA = vol.Schema( - {vol.Required(CONF_DEVICE): None, vol.Required(CONF_DOMAIN): str}, + {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str}, extra=vol.ALLOW_EXTRA, ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 81a6cd059bb74d..0b569e2d4ad1d0 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -9,7 +9,12 @@ import voluptuous as vol from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE -from homeassistant.const import CONF_CONDITION, CONF_DEVICE, CONF_DOMAIN, CONF_TIMEOUT +from homeassistant.const import ( + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_TIMEOUT, +) from homeassistant import exceptions from homeassistant.helpers import ( service, @@ -66,7 +71,7 @@ def _determine_action(action): if CONF_EVENT in action: return ACTION_FIRE_EVENT - if CONF_DEVICE in action: + if CONF_DEVICE_ID in action: return ACTION_DEVICE_AUTOMATION return ACTION_CALL_SERVICE diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index b084c64182b455..a01dad03d46276 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -43,21 +43,18 @@ async def test_websocket_get_actions(hass, hass_ws_client, device_reg, entity_re entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) expected_actions = [ { - "device": None, "domain": "light", "type": "turn_off", "device_id": device_entry.id, "entity_id": "light.test_5678", }, { - "device": None, "domain": "light", "type": "turn_on", "device_id": device_entry.id, "entity_id": "light.test_5678", }, { - "device": None, "domain": "light", "type": "toggle", "device_id": device_entry.id, diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py index 6bbd8b5c31931e..40fa08856c5684 100644 --- a/tests/components/light/test_device_automation.py +++ b/tests/components/light/test_device_automation.py @@ -57,21 +57,18 @@ async def test_get_actions(hass, device_reg, entity_reg): entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_actions = [ { - "device": None, "domain": DOMAIN, "type": "turn_off", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { - "device": None, "domain": DOMAIN, "type": "turn_on", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { - "device": None, "domain": DOMAIN, "type": "toggle", "device_id": device_entry.id, @@ -164,6 +161,7 @@ async def test_if_fires_on_state_change(hass, calls): "trigger": { "platform": "device", "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "turn_on", }, @@ -187,6 +185,7 @@ async def test_if_fires_on_state_change(hass, calls): "trigger": { "platform": "device", "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "turn_off", }, @@ -248,6 +247,7 @@ async def test_if_state(hass, calls): { "condition": "device", "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "is_on", } @@ -266,6 +266,7 @@ async def test_if_state(hass, calls): { "condition": "device", "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "is_off", } @@ -316,8 +317,8 @@ async def test_action(hass, calls): { "trigger": {"platform": "event", "event_type": "test_event1"}, "action": { - "device": None, "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "turn_off", }, @@ -325,8 +326,8 @@ async def test_action(hass, calls): { "trigger": {"platform": "event", "event_type": "test_event2"}, "action": { - "device": None, "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "turn_on", }, @@ -334,8 +335,8 @@ async def test_action(hass, calls): { "trigger": {"platform": "event", "event_type": "test_event3"}, "action": { - "device": None, "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "toggle", }, diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py index 56da6767c1dd3d..7dba73476514f0 100644 --- a/tests/components/switch/test_device_automation.py +++ b/tests/components/switch/test_device_automation.py @@ -57,21 +57,18 @@ async def test_get_actions(hass, device_reg, entity_reg): entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_actions = [ { - "device": None, "domain": DOMAIN, "type": "turn_off", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { - "device": None, "domain": DOMAIN, "type": "turn_on", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { - "device": None, "domain": DOMAIN, "type": "toggle", "device_id": device_entry.id, @@ -164,6 +161,7 @@ async def test_if_fires_on_state_change(hass, calls): "trigger": { "platform": "device", "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "turn_on", }, @@ -187,6 +185,7 @@ async def test_if_fires_on_state_change(hass, calls): "trigger": { "platform": "device", "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "turn_off", }, @@ -248,6 +247,7 @@ async def test_if_state(hass, calls): { "condition": "device", "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "is_on", } @@ -266,6 +266,7 @@ async def test_if_state(hass, calls): { "condition": "device", "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "is_off", } @@ -316,8 +317,8 @@ async def test_action(hass, calls): { "trigger": {"platform": "event", "event_type": "test_event1"}, "action": { - "device": None, "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "turn_off", }, @@ -325,8 +326,8 @@ async def test_action(hass, calls): { "trigger": {"platform": "event", "event_type": "test_event2"}, "action": { - "device": None, "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "turn_on", }, @@ -334,8 +335,8 @@ async def test_action(hass, calls): { "trigger": {"platform": "event", "event_type": "test_event3"}, "action": { - "device": None, "domain": DOMAIN, + "device_id": "", "entity_id": dev1.entity_id, "type": "toggle", }, From f167914951c93f26f9f4305380150166215cccbd Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Mon, 9 Sep 2019 11:06:06 -0700 Subject: [PATCH 0226/3953] Move iaqualink update from climate to component (#26505) * Move iaqualink update from climate to component * Typing fix --- .../components/iaqualink/__init__.py | 50 ++++++++++++++++++- homeassistant/components/iaqualink/climate.py | 16 ++---- homeassistant/components/iaqualink/const.py | 3 ++ 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 3f171715c57684..95c6f6895fccd1 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -1,5 +1,6 @@ """Component to embed Aqualink devices.""" import asyncio +from functools import wraps import logging from aiohttp import CookieJar @@ -11,11 +12,18 @@ from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import DOMAIN +from .const import DOMAIN, UPDATE_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -86,6 +94,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None _LOGGER.debug("Got %s climates: %s", len(climates), climates) hass.async_create_task(forward_setup(entry, CLIMATE_DOMAIN)) + async def _async_systems_update(now): + """Refresh internal state for all systems.""" + await systems[0].update() + async_dispatcher_send(hass, DOMAIN) + + async_track_time_interval(hass, _async_systems_update, UPDATE_INTERVAL) + return True @@ -101,3 +116,36 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo hass.data[DOMAIN].clear() return all(await asyncio.gather(*tasks)) + + +def refresh_system(func): + """Force update all entities after state change.""" + + @wraps(func) + async def wrapper(self, *args, **kwargs): + """Call decorated function and send update signal to all entities.""" + await func(self, *args, **kwargs) + async_dispatcher_send(self.hass, DOMAIN) + + return wrapper + + +class AqualinkEntity(Entity): + """Abstract class for all Aqualink platforms.""" + + async def async_added_to_hass(self) -> None: + """Set up a listener when this entity is added to HA.""" + async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) + + @callback + def _update_callback(self) -> None: + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def should_poll(self) -> bool: + """Return False as entities shouldn't be polled. + + Entities are checked periodically as the integration runs periodic + updates on a timer. + """ + return False diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 0a30517623d46b..321c54329a2878 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -27,6 +27,7 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.typing import HomeAssistantType +from . import AqualinkEntity, refresh_system from .const import DOMAIN as AQUALINK_DOMAIN, CLIMATE_SUPPORTED_MODES _LOGGER = logging.getLogger(__name__) @@ -44,7 +45,7 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkThermostat(ClimateDevice): +class HassAqualinkThermostat(ClimateDevice, AqualinkEntity): """Representation of a thermostat.""" def __init__(self, dev: AqualinkThermostat): @@ -56,17 +57,6 @@ def name(self) -> str: """Return the name of the thermostat.""" return self.dev.label.split(" ")[0] - async def async_update(self) -> None: - """Update the internal state of the thermostat. - - The API update() command refreshes the state of all devices so we - only update if this is the main thermostat to avoid unnecessary - calls. - """ - if self.name != "Pool": - return - await self.dev.system.update() - @property def supported_features(self) -> int: """Return the list of supported features.""" @@ -91,6 +81,7 @@ def hvac_mode(self) -> str: return HVAC_MODE_HEAT return HVAC_MODE_OFF + @refresh_system async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Turn the underlying heater switch on or off.""" if hvac_mode == HVAC_MODE_HEAT: @@ -126,6 +117,7 @@ def target_temperature(self) -> float: """Return the current target temperature.""" return float(self.dev.state) + @refresh_system async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" await self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE])) diff --git a/homeassistant/components/iaqualink/const.py b/homeassistant/components/iaqualink/const.py index ebdcd365194e6d..219eb9129944f2 100644 --- a/homeassistant/components/iaqualink/const.py +++ b/homeassistant/components/iaqualink/const.py @@ -1,5 +1,8 @@ """Constants for the the iaqualink component.""" +from datetime import timedelta + from homeassistant.components.climate.const import HVAC_MODE_HEAT, HVAC_MODE_OFF DOMAIN = "iaqualink" CLIMATE_SUPPORTED_MODES = [HVAC_MODE_HEAT, HVAC_MODE_OFF] +UPDATE_INTERVAL = timedelta(seconds=30) From 7d71976e01abb816ed05c983758e9d94a46d77b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Sep 2019 12:01:49 -0700 Subject: [PATCH 0227/3953] Do not complain about automatic generated files (#26540) * Do not complain about automatic generated files * Update generated files --- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 1 + homeassistant/generated/zeroconf.py | 1 + script/hassfest/config_flow.py | 1 + script/hassfest/ssdp.py | 1 + script/hassfest/zeroconf.py | 1 + 6 files changed, 6 insertions(+) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a2f9c26949a706..7f3f5c1f20d23b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -3,6 +3,7 @@ To update, run python3 -m script.hassfest """ +# fmt: off FLOWS = [ "adguard", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 28df05a872cfb0..6d62c47110b2d7 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -3,6 +3,7 @@ To update, run python3 -m script.hassfest """ +# fmt: off SSDP = { "device_type": {}, diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 09c1712c061d52..6200e2facb0c60 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -3,6 +3,7 @@ To update, run python3 -m script.hassfest """ +# fmt: off ZEROCONF = { "_axis-video._tcp.local.": [ diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 5376f21db9eb81..4384399f4db393 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -10,6 +10,7 @@ To update, run python3 -m script.hassfest \"\"\" +# fmt: off FLOWS = {} """.strip() diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index 82068af6a7acbf..3b02ea181514df 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -11,6 +11,7 @@ To update, run python3 -m script.hassfest \"\"\" +# fmt: off SSDP = {} """.strip() diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index bdd765e315ecd1..3d93d363086162 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -11,6 +11,7 @@ To update, run python3 -m script.hassfest \"\"\" +# fmt: off ZEROCONF = {} From 702e63e6e8707841b4e3c232154c8c35143cf4d4 Mon Sep 17 00:00:00 2001 From: tyjtyj Date: Tue, 10 Sep 2019 03:44:00 +0800 Subject: [PATCH 0228/3953] Fix Tuya Light without brightness (#26534) Return none if there is brightness data from tuya https://github.com/home-assistant/home-assistant/issues/25896 --- homeassistant/components/tuya/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 2c72dd60490899..9ac72419612aa3 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -40,6 +40,8 @@ def __init__(self, tuya): @property def brightness(self): """Return the brightness of the light.""" + if self.tuya.brightness() is None: + return None return int(self.tuya.brightness()) @property From ff136a19d9864e3d72c4d0774f65ba5170f1f236 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 9 Sep 2019 22:02:53 +0200 Subject: [PATCH 0229/3953] Add Delta Counter of HmIP-SPDR to Homematic IP Cloud (#26538) --- .../components/homematicip_cloud/sensor.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index b396a8d9defade..43812df94d2283 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -10,6 +10,7 @@ AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, AsyncMotionDetectorPushButton, + AsyncPassageDetector, AsyncPlugableSwitchMeasuring, AsyncPresenceDetectorIndoor, AsyncTemperatureHumiditySensorDisplay, @@ -38,6 +39,8 @@ _LOGGER = logging.getLogger(__name__) +ATTR_LEFT_COUNTER = "left_counter" +ATTR_RIGHT_COUNTER = "right_counter" ATTR_TEMPERATURE_OFFSET = "temperature_offset" ATTR_WIND_DIRECTION = "wind_direction" ATTR_WIND_DIRECTION_VARIATION = "wind_direction_variation_in_degree" @@ -100,6 +103,8 @@ async def async_setup_entry( devices.append(HomematicipWindspeedSensor(home, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): devices.append(HomematicipTodayRainSensor(home, device)) + if isinstance(device, AsyncPassageDetector): + devices.append(HomematicipPassageDetectorDeltaCounter(home, device)) if devices: async_add_entities(devices) @@ -338,6 +343,29 @@ def unit_of_measurement(self) -> str: return "mm" +class HomematicipPassageDetectorDeltaCounter(HomematicipGenericDevice): + """Representation of a HomematicIP passage detector delta counter.""" + + def __init__(self, home: AsyncHome, device) -> None: + """Initialize the device.""" + super().__init__(home, device) + + @property + def state(self) -> int: + """Representation of the HomematicIP passage detector delta counter value.""" + return self._device.leftRightCounterDelta + + @property + def device_state_attributes(self): + """Return the state attributes of the delta counter.""" + state_attr = super().device_state_attributes + + state_attr[ATTR_LEFT_COUNTER] = self._device.leftCounter + state_attr[ATTR_RIGHT_COUNTER] = self._device.rightCounter + + return state_attr + + def _get_wind_direction(wind_direction_degree: float) -> str: """Convert wind direction degree to named direction.""" if 11.25 <= wind_direction_degree < 33.75: From 3c629db096ec77c6dc549e171d4af777d3c9fd1a Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Mon, 9 Sep 2019 14:10:25 -0700 Subject: [PATCH 0230/3953] Add light platform to iaqualink integration (#26484) * Add light platform to iaqualink component. * Style changes. * Polling moved to timer in the component --- .coveragerc | 1 + .../components/iaqualink/__init__.py | 16 ++- homeassistant/components/iaqualink/light.py | 105 ++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/iaqualink/light.py diff --git a/.coveragerc b/.coveragerc index c30c78ddf65bec..7a51a591780878 100644 --- a/.coveragerc +++ b/.coveragerc @@ -288,6 +288,7 @@ omit = homeassistant/components/hyperion/light.py homeassistant/components/ialarm/alarm_control_panel.py homeassistant/components/iaqualink/climate.py + homeassistant/components/iaqualink/light.py homeassistant/components/icloud/device_tracker.py homeassistant/components/idteck_prox/* homeassistant/components/ifttt/* diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 95c6f6895fccd1..c7b7bc472dd544 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -6,10 +6,16 @@ from aiohttp import CookieJar import voluptuous as vol -from iaqualink import AqualinkClient, AqualinkLoginException, AqualinkThermostat +from iaqualink import ( + AqualinkClient, + AqualinkLight, + AqualinkLoginException, + AqualinkThermostat, +) from homeassistant import config_entries from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback @@ -67,6 +73,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None # These will contain the initialized devices climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = [] + lights = hass.data[DOMAIN][LIGHT_DOMAIN] = [] session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) aqualink = AqualinkClient(username, password, session) @@ -88,11 +95,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None for dev in devices.values(): if isinstance(dev, AqualinkThermostat): climates += [dev] + elif isinstance(dev, AqualinkLight): + lights += [dev] forward_setup = hass.config_entries.async_forward_entry_setup if climates: _LOGGER.debug("Got %s climates: %s", len(climates), climates) hass.async_create_task(forward_setup(entry, CLIMATE_DOMAIN)) + if lights: + _LOGGER.debug("Got %s lights: %s", len(lights), lights) + hass.async_create_task(forward_setup(entry, LIGHT_DOMAIN)) async def _async_systems_update(now): """Refresh internal state for all systems.""" @@ -112,6 +124,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo if hass.data[DOMAIN][CLIMATE_DOMAIN]: tasks += [forward_unload(entry, CLIMATE_DOMAIN)] + if hass.data[DOMAIN][LIGHT_DOMAIN]: + tasks += [forward_unload(entry, LIGHT_DOMAIN)] hass.data[DOMAIN].clear() diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py new file mode 100644 index 00000000000000..fbfb10783ee4a1 --- /dev/null +++ b/homeassistant/components/iaqualink/light.py @@ -0,0 +1,105 @@ +"""Support for Aqualink pool lights.""" +import logging + +from iaqualink import AqualinkLight, AqualinkLightEffect + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_EFFECT, + DOMAIN, + SUPPORT_BRIGHTNESS, + SUPPORT_EFFECT, + Light, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import AqualinkEntity, refresh_system +from .const import DOMAIN as AQUALINK_DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up discovered lights.""" + devs = [] + for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: + devs.append(HassAqualinkLight(dev)) + async_add_entities(devs, True) + + +class HassAqualinkLight(Light, AqualinkEntity): + """Representation of a light.""" + + def __init__(self, dev: AqualinkLight): + """Initialize the light.""" + self.dev = dev + + @property + def name(self) -> str: + """Return the name of the light.""" + return self.dev.label + + @property + def is_on(self) -> bool: + """Return whether the light is on or off.""" + return self.dev.is_on + + @refresh_system + async def async_turn_on(self, **kwargs) -> None: + """Turn on the light. + + This handles brightness and light effects for lights that do support + them. + """ + brightness = kwargs.get(ATTR_BRIGHTNESS) + effect = kwargs.get(ATTR_EFFECT) + + # For now I'm assuming lights support either effects or brightness. + if effect: + effect = AqualinkLightEffect[effect].value + await self.dev.set_effect(effect) + elif brightness: + # Aqualink supports percentages in 25% increments. + pct = int(round(brightness * 4.0 / 255)) * 25 + await self.dev.set_brightness(pct) + else: + await self.dev.turn_on() + + @refresh_system + async def async_turn_off(self, **kwargs) -> None: + """Turn off the light.""" + await self.dev.turn_off() + + @property + def brightness(self) -> int: + """Return current brightness of the light. + + The scale needs converting between 0-100 and 0-255. + """ + return self.dev.brightness * 255 / 100 + + @property + def effect(self) -> str: + """Return the current light effect if supported.""" + return AqualinkLightEffect(self.dev.effect).name + + @property + def effect_list(self) -> list: + """Return supported light effects.""" + return list(AqualinkLightEffect.__members__) + + @property + def supported_features(self) -> int: + """Return the list of features supported by the light.""" + if self.dev.is_dimmer: + return SUPPORT_BRIGHTNESS + + if self.dev.is_color: + return SUPPORT_EFFECT + + return 0 From 30fb4ddc9888bdb1c2e4588aaed782577810a817 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 9 Sep 2019 16:28:20 -0500 Subject: [PATCH 0231/3953] Move config and connections to Plex component (#26488) * Move config and connections to component * Separate imports * Set a unique_id on sensor * Set a platforms const * Add SERVERS dict, hardcode to single server * Move to debug * Return false * More debug * Import at top to fix lint * Guard against legacy setup attempts * Refactor to add setup callback * Review comments * Log levels * Return result of callback * Store CONFIGURING in hass.data * Set up discovery if no config data * Use schema to set defaults * Remove media_player options to remove entities * Improve error handling --- .../components/discovery/__init__.py | 3 +- homeassistant/components/plex/__init__.py | 202 +++++++++++++++++- homeassistant/components/plex/const.py | 4 + homeassistant/components/plex/media_player.py | 195 ++--------------- homeassistant/components/plex/sensor.py | 64 ++---- homeassistant/components/plex/server.py | 1 + 6 files changed, 238 insertions(+), 231 deletions(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 5f1fd335d45d89..827e05a424be85 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -36,6 +36,7 @@ SERVICE_MOBILE_APP = "hass_mobile_app" SERVICE_NETGEAR = "netgear_router" SERVICE_OCTOPRINT = "octoprint" +SERVICE_PLEX = "plex_mediaserver" SERVICE_ROKU = "roku" SERVICE_SABNZBD = "sabnzbd" SERVICE_SAMSUNG_PRINTER = "samsung_printer" @@ -68,7 +69,7 @@ SERVICE_FREEBOX: ("freebox", None), SERVICE_YEELIGHT: ("yeelight", None), "panasonic_viera": ("media_player", "panasonic_viera"), - "plex_mediaserver": ("media_player", "plex"), + SERVICE_PLEX: ("plex", None), "yamaha": ("media_player", "yamaha"), "logitech_mediaserver": ("media_player", "squeezebox"), "directv": ("media_player", "directv"), diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 6e4e02026abff8..846f3e3f53c325 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1 +1,201 @@ -"""The plex component.""" +"""Support to embed Plex.""" +import logging + +import plexapi.exceptions +import requests.exceptions +import voluptuous as vol + +from homeassistant.components.discovery import SERVICE_PLEX +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_TOKEN, + CONF_URL, + CONF_VERIFY_SSL, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.util.json import load_json, save_json + +from .const import ( + CONF_USE_EPISODE_ART, + CONF_SHOW_ALL_CONTROLS, + DEFAULT_HOST, + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, + DOMAIN as PLEX_DOMAIN, + PLATFORMS, + PLEX_CONFIG_FILE, + PLEX_MEDIA_PLAYER_OPTIONS, + SERVERS, +) +from .server import PlexServer + +MEDIA_PLAYER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, + vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean, + } +) + +SERVER_CONFIG_SCHEMA = vol.Schema( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_TOKEN): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(MP_DOMAIN, default={}): MEDIA_PLAYER_SCHEMA, + } +) + +CONFIG_SCHEMA = vol.Schema({PLEX_DOMAIN: SERVER_CONFIG_SCHEMA}, extra=vol.ALLOW_EXTRA) + +CONFIGURING = "configuring" +_LOGGER = logging.getLogger(__package__) + + +def setup(hass, config): + """Set up the Plex component.""" + + def server_discovered(service, info): + """Pass back discovered Plex server details.""" + if hass.data[PLEX_DOMAIN][SERVERS]: + _LOGGER.debug("Plex server already configured, ignoring discovery.") + return + _LOGGER.debug("Discovered Plex server: %s:%s", info["host"], info["port"]) + setup_plex(discovery_info=info) + + def setup_plex(config=None, discovery_info=None, configurator_info=None): + """Return assembled server_config dict.""" + json_file = hass.config.path(PLEX_CONFIG_FILE) + file_config = load_json(json_file) + + if config: + server_config = config + host_and_port = ( + f"{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" + ) + if MP_DOMAIN in server_config: + hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN) + elif file_config: + _LOGGER.debug("Loading config from %s", json_file) + host_and_port, server_config = file_config.popitem() + server_config[CONF_VERIFY_SSL] = server_config.pop("verify") + elif discovery_info: + server_config = {} + host_and_port = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" + elif configurator_info: + server_config = configurator_info + host_and_port = server_config["host_and_port"] + else: + discovery.listen(hass, SERVICE_PLEX, server_discovered) + return True + + use_ssl = server_config.get(CONF_SSL, DEFAULT_SSL) + http_prefix = "https" if use_ssl else "http" + server_config[CONF_URL] = f"{http_prefix}://{host_and_port}" + + plex_server = PlexServer(server_config) + try: + plex_server.connect() + except requests.exceptions.ConnectionError as error: + _LOGGER.error( + "Plex server could not be reached, please verify host and port: [%s]", + error, + ) + return False + except ( + plexapi.exceptions.BadRequest, + plexapi.exceptions.Unauthorized, + plexapi.exceptions.NotFound, + ) as error: + _LOGGER.error( + "Connection to Plex server failed, please verify token and SSL settings: [%s]", + error, + ) + request_configuration(host_and_port) + return False + else: + hass.data[PLEX_DOMAIN][SERVERS][ + plex_server.machine_identifier + ] = plex_server + + if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: + request_id = hass.data[PLEX_DOMAIN][CONFIGURING].pop(host_and_port) + configurator = hass.components.configurator + configurator.request_done(request_id) + _LOGGER.debug("Discovery configuration done") + if configurator_info: + # Write plex.conf if created via discovery/configurator + save_json( + hass.config.path(PLEX_CONFIG_FILE), + { + host_and_port: { + CONF_TOKEN: server_config[CONF_TOKEN], + CONF_SSL: use_ssl, + "verify": server_config[CONF_VERIFY_SSL], + } + }, + ) + + if not hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS): + hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = MEDIA_PLAYER_SCHEMA({}) + + for platform in PLATFORMS: + hass.helpers.discovery.load_platform( + platform, PLEX_DOMAIN, {}, original_config + ) + + return True + + def request_configuration(host_and_port): + """Request configuration steps from the user.""" + configurator = hass.components.configurator + if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: + configurator.notify_errors( + hass.data[PLEX_DOMAIN][CONFIGURING][host_and_port], + "Failed to register, please try again.", + ) + return + + def plex_configuration_callback(data): + """Handle configuration changes.""" + config = { + "host_and_port": host_and_port, + CONF_TOKEN: data.get("token"), + CONF_SSL: cv.boolean(data.get("ssl")), + CONF_VERIFY_SSL: cv.boolean(data.get("verify_ssl")), + } + setup_plex(configurator_info=config) + + hass.data[PLEX_DOMAIN][CONFIGURING][ + host_and_port + ] = configurator.request_config( + "Plex Media Server", + plex_configuration_callback, + description="Enter the X-Plex-Token", + entity_picture="/static/images/logo_plex_mediaserver.png", + submit_caption="Confirm", + fields=[ + {"id": "token", "name": "X-Plex-Token", "type": ""}, + {"id": "ssl", "name": "Use SSL", "type": ""}, + {"id": "verify_ssl", "name": "Verify SSL", "type": ""}, + ], + ) + + # End of inner functions. + + original_config = config + + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, CONFIGURING: {}}) + + if hass.data[PLEX_DOMAIN][SERVERS]: + _LOGGER.debug("Plex server already configured") + return False + + plex_config = config.get(PLEX_DOMAIN, {}) + return setup_plex(config=plex_config) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 4495b9a8c838ba..bf8c2387e4de3a 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -7,7 +7,11 @@ DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True +PLATFORMS = ["media_player", "sensor"] +SERVERS = "servers" + PLEX_CONFIG_FILE = "plex.conf" +PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" PLEX_SERVER_CONFIG = "server_config" CONF_USE_EPISODE_ART = "use_episode_art" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 6005321310d79a..cfc63948bee80c 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -2,10 +2,13 @@ from datetime import timedelta import json import logging + +import plexapi.exceptions +import plexapi.playlist +import plexapi.playqueue import requests.exceptions -import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -20,150 +23,37 @@ SUPPORT_VOLUME_SET, ) from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_URL, - CONF_TOKEN, - CONF_VERIFY_SSL, DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_time_interval from homeassistant.util import dt as dt_util -from homeassistant.util.json import load_json, save_json from .const import ( CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, - CONF_REMOVE_UNAVAILABLE_CLIENTS, - CONF_CLIENT_REMOVE_INTERVAL, - DEFAULT_HOST, - DEFAULT_PORT, - DEFAULT_SSL, - DEFAULT_VERIFY_SSL, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, - PLEX_CONFIG_FILE, + PLEX_MEDIA_PLAYER_OPTIONS, + SERVERS, ) -from .server import PlexServer SERVER_SETUP = "server_setup" _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_TOKEN): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, - vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean, - vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): cv.boolean, - vol.Optional( - CONF_CLIENT_REMOVE_INTERVAL, default=timedelta(seconds=600) - ): vol.All(cv.time_period, cv.positive_timedelta), - } -) - def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the Plex platform.""" - plex_data = hass.data.setdefault(PLEX_DOMAIN, {}) - server_setup = plex_data.setdefault(SERVER_SETUP, False) - if server_setup: + if discovery_info is None: return - # get config from plex.conf - file_config = load_json(hass.config.path(PLEX_CONFIG_FILE)) - - if file_config: - # Setup a configured PlexServer - host, host_config = file_config.popitem() - token = host_config["token"] - try: - has_ssl = host_config["ssl"] - except KeyError: - has_ssl = False - try: - verify_ssl = host_config["verify"] - except KeyError: - verify_ssl = True - - # Via discovery - elif discovery_info is not None: - # Parse discovery data - host = discovery_info.get("host") - port = discovery_info.get("port") - host = f"{host}:{port}" - _LOGGER.info("Discovered PLEX server: %s", host) - - if host in _CONFIGURING: - return - token = None - has_ssl = False - verify_ssl = True - else: - host = config[CONF_HOST] - port = config[CONF_PORT] - host = f"{host}:{port}" - token = config.get(CONF_TOKEN) - has_ssl = config[CONF_SSL] - verify_ssl = config[CONF_VERIFY_SSL] - - setup_plexserver( - host, token, has_ssl, verify_ssl, hass, config, add_entities_callback - ) - - -def setup_plexserver( - host, token, has_ssl, verify_ssl, hass, config, add_entities_callback -): - """Set up a plexserver based on host parameter.""" - import plexapi.exceptions - - http_prefix = "https" if has_ssl else "http" - - server_config = { - CONF_URL: f"{http_prefix}://{host}", - CONF_TOKEN: token, - CONF_VERIFY_SSL: verify_ssl, - } - - try: - plexserver = PlexServer(server_config) - plexserver.connect() - except ( - plexapi.exceptions.BadRequest, - plexapi.exceptions.Unauthorized, - plexapi.exceptions.NotFound, - ) as error: - _LOGGER.info(error) - # No token or wrong token - request_configuration(host, hass, config, add_entities_callback) - return - else: - hass.data[PLEX_DOMAIN][SERVER_SETUP] = True - - # If we came here and configuring this host, mark as done - if host in _CONFIGURING: - request_id = _CONFIGURING.pop(host) - configurator = hass.components.configurator - configurator.request_done(request_id) - _LOGGER.info("Discovery configuration done") - - # Save config - save_json( - hass.config.path(PLEX_CONFIG_FILE), - {host: {"token": token, "ssl": has_ssl, "verify": verify_ssl}}, - ) + plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] + config = hass.data[PLEX_MEDIA_PLAYER_OPTIONS] plex_clients = {} plex_sessions = {} @@ -178,7 +68,9 @@ def update_devices(): return except requests.exceptions.RequestException as ex: _LOGGER.warning( - "Could not connect to plex server at http://%s (%s)", host, ex + "Could not connect to Plex server: %s (%s)", + plexserver.friendly_name, + ex, ) return @@ -210,7 +102,9 @@ def update_devices(): return except requests.exceptions.RequestException as ex: _LOGGER.warning( - "Could not connect to plex server at http://%s (%s)", host, ex + "Could not connect to Plex server: %s (%s)", + plexserver.friendly_name, + ex, ) return @@ -239,7 +133,6 @@ def update_devices(): _LOGGER.debug("Refreshing session: %s", machine_identifier) plex_clients[machine_identifier].refresh(None, session) - clients_to_remove = [] for client in plex_clients.values(): # force devices to idle that do not have a valid session if client.session is None: @@ -253,59 +146,10 @@ def update_devices(): if client not in new_plex_clients: client.schedule_update_ha_state() - if not config.get(CONF_REMOVE_UNAVAILABLE_CLIENTS) or client.available: - continue - - if (dt_util.utcnow() - client.marked_unavailable) >= ( - config.get(CONF_CLIENT_REMOVE_INTERVAL) - ): - hass.add_job(client.async_remove()) - clients_to_remove.append(client.machine_identifier) - - while clients_to_remove: - del plex_clients[clients_to_remove.pop()] - if new_plex_clients: add_entities_callback(new_plex_clients) -def request_configuration(host, hass, config, add_entities_callback): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - # We got an error if this method is called while we are configuring - if host in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING[host], "Failed to register, please try again." - ) - - return - - def plex_configuration_callback(data): - """Handle configuration changes.""" - setup_plexserver( - host, - data.get("token"), - cv.boolean(data.get("has_ssl")), - cv.boolean(data.get("do_not_verify_ssl")), - hass, - config, - add_entities_callback, - ) - - _CONFIGURING[host] = configurator.request_config( - "Plex Media Server", - plex_configuration_callback, - description="Enter the X-Plex-Token", - entity_picture="/static/images/logo_plex_mediaserver.png", - submit_caption="Confirm", - fields=[ - {"id": "token", "name": "X-Plex-Token", "type": ""}, - {"id": "has_ssl", "name": "Use SSL", "type": ""}, - {"id": "do_not_verify_ssl", "name": "Do not verify SSL", "type": ""}, - ], - ) - - class PlexClient(MediaPlayerDevice): """Representation of a Plex device.""" @@ -378,9 +222,6 @@ def _clear_media_details(self): def refresh(self, device, session): """Refresh key device data.""" - import plexapi.exceptions - - # new data refresh self._clear_media_details() if session: # Not being triggered by Chrome or FireTablet Plex App @@ -851,8 +692,6 @@ def play_media(self, media_type, media_id, **kwargs): src["video_name"] ) - import plexapi.playlist - if ( media and media_type == "EPISODE" @@ -918,8 +757,6 @@ def _client_play_media(self, media, delete=False, **params): _LOGGER.error("Client cannot play media: %s", self.entity_id) return - import plexapi.playqueue - playqueue = plexapi.playqueue.PlayQueue.create( self.device.server, media, **params ) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index bece6274af69dc..f469e95da808e0 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,87 +1,51 @@ """Support for Plex media server monitoring.""" from datetime import timedelta import logging + import plexapi.exceptions import requests.exceptions -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, - CONF_HOST, - CONF_PORT, - CONF_TOKEN, - CONF_SSL, - CONF_URL, - CONF_VERIFY_SSL, -) + from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv -from .const import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL -from .server import PlexServer +from .const import DOMAIN as PLEX_DOMAIN, SERVERS DEFAULT_NAME = "Plex" _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_TOKEN): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Plex sensor.""" - name = config.get(CONF_NAME) - plex_host = config.get(CONF_HOST) - plex_port = config.get(CONF_PORT) - plex_token = config.get(CONF_TOKEN) - verify_ssl = config.get(CONF_VERIFY_SSL) - - plex_url = "{}://{}:{}".format( - "https" if config.get(CONF_SSL) else "http", plex_host, plex_port - ) - - try: - plex_server = PlexServer( - {CONF_URL: plex_url, CONF_TOKEN: plex_token, CONF_VERIFY_SSL: verify_ssl} - ) - plex_server.connect() - except ( - plexapi.exceptions.BadRequest, - plexapi.exceptions.Unauthorized, - plexapi.exceptions.NotFound, - ) as error: - _LOGGER.error(error) + if discovery_info is None: return - add_entities([PlexSensor(name, plex_server)], True) + plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] + add_entities([PlexSensor(plexserver)], True) class PlexSensor(Entity): """Representation of a Plex now playing sensor.""" - def __init__(self, name, plex_server): + def __init__(self, plex_server): """Initialize the sensor.""" - self._name = name + self._name = DEFAULT_NAME self._state = None self._now_playing = [] self._server = plex_server + self._unique_id = f"sensor-{plex_server.machine_identifier}" @property def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return the id of this plex client.""" + return self._unique_id + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 6647b81714f3d5..c778588752a3ad 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,5 +1,6 @@ """Shared class to maintain Plex server instances.""" import logging + import plexapi.server from requests import Session From f3123ee0ca21da7fc02c0f870672d1c29b322961 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Tue, 10 Sep 2019 00:41:47 +0300 Subject: [PATCH 0232/3953] Fix radiotherm local time (#26526) We want to use our own dt_util.now() which takes into consideration the globally set DEFAULT_TIME_ZONE --- homeassistant/components/radiotherm/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index 2585dc0b00bcc4..a007dd673ace3a 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -1,5 +1,4 @@ """Support for Radio Thermostat wifi-enabled home thermostats.""" -import datetime import logging import voluptuous as vol @@ -26,6 +25,7 @@ TEMP_FAHRENHEIT, STATE_ON, ) +from homeassistant.util import dt as dt_util import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -310,7 +310,7 @@ def set_time(self): """Set device time.""" # Calling this clears any local temperature override and # reverts to the scheduled temperature. - now = datetime.datetime.now() + now = dt_util.now() self.device.time = { "day": now.weekday(), "hour": now.hour, From 051639b6ad5113b431c7887d8a2e756bb57171b8 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Mon, 9 Sep 2019 23:46:11 +0200 Subject: [PATCH 0233/3953] Add more attributes to vicare climate entity (#26521) Add some device information as attributes to the climate device: - room_temperature - supply_temperature - outside_temperature - active_vicare_program - active_vicare_mode - heating_curve_slope - heating_curve_shift - month_since_last_service - date_last_service - error_history - active_error - circulationpump_active --- homeassistant/components/vicare/climate.py | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 1a5098360cfab2..7010f943707303 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -88,6 +88,7 @@ def __init__(self, name, api): self._name = name self._state = None self._api = api + self._attributes = {} self._target_temperature = None self._current_mode = None self._current_temperature = None @@ -114,6 +115,25 @@ def update(self): self._current_mode = self._api.getActiveMode() + # Update the device attributes + self._attributes = {} + self._attributes["room_temperature"] = _room_temperature + self._attributes["supply_temperature"] = _supply_temperature + self._attributes["outside_temperature"] = self._api.getOutsideTemperature() + self._attributes["active_vicare_program"] = self._current_program + self._attributes["active_vicare_mode"] = self._current_mode + self._attributes["heating_curve_slope"] = self._api.getHeatingCurveSlope() + self._attributes["heating_curve_shift"] = self._api.getHeatingCurveShift() + self._attributes[ + "month_since_last_service" + ] = self._api.getMonthSinceLastService() + self._attributes["date_last_service"] = self._api.getLastServiceDate() + self._attributes["error_history"] = self._api.getErrorHistory() + self._attributes["active_error"] = self._api.getActiveError() + self._attributes[ + "circulationpump_active" + ] = self._api.getCirculationPumpActive() + @property def supported_features(self): """Return the list of supported features.""" @@ -208,3 +228,8 @@ def set_preset_mode(self, preset_mode): _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) self._api.deactivateProgram(self._current_program) self._api.activateProgram(vicare_program) + + @property + def device_state_attributes(self): + """Show Device Attributes.""" + return self._attributes From a4fd991ab557834dd484e30570251f32e47abfd3 Mon Sep 17 00:00:00 2001 From: indykoning <15870933+indykoning@users.noreply.github.com> Date: Mon, 9 Sep 2019 23:47:44 +0200 Subject: [PATCH 0234/3953] Add growatt server integration (#25635) * Added growatt server integration * Ran black formatter * Processed feedback. * Made attributes more readable. * Create a sensor for each property. * Added unique_ids * Accidentally flipped functions * Added dynamic device classes. * Fixed stale session. --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/growatt_server/__init__.py | 1 + .../components/growatt_server/manifest.json | 12 ++ .../components/growatt_server/sensor.py | 189 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 207 insertions(+) create mode 100644 homeassistant/components/growatt_server/__init__.py create mode 100644 homeassistant/components/growatt_server/manifest.json create mode 100644 homeassistant/components/growatt_server/sensor.py diff --git a/.coveragerc b/.coveragerc index 7a51a591780878..6ade2d1f703150 100644 --- a/.coveragerc +++ b/.coveragerc @@ -249,6 +249,7 @@ omit = homeassistant/components/greeneye_monitor/sensor.py homeassistant/components/greenwave/light.py homeassistant/components/group/notify.py + homeassistant/components/growatt_server/sensor.py homeassistant/components/gstreamer/media_player.py homeassistant/components/gtfs/sensor.py homeassistant/components/gtt/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 386deb107290de..f82523d8c8d8ed 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -110,6 +110,7 @@ homeassistant/components/google_translate/* @awarecan homeassistant/components/google_travel_time/* @robbiet480 homeassistant/components/gpsd/* @fabaff homeassistant/components/group/* @home-assistant/core +homeassistant/components/growatt_server/* @indykoning homeassistant/components/gtfs/* @robbiet480 homeassistant/components/harmony/* @ehendrix23 homeassistant/components/hassio/* @home-assistant/hass-io diff --git a/homeassistant/components/growatt_server/__init__.py b/homeassistant/components/growatt_server/__init__.py new file mode 100644 index 00000000000000..14205e8d9ba3ce --- /dev/null +++ b/homeassistant/components/growatt_server/__init__.py @@ -0,0 +1 @@ +"""The Growatt server PV inverter sensor integration.""" diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json new file mode 100644 index 00000000000000..a6a1d2b8aebb7a --- /dev/null +++ b/homeassistant/components/growatt_server/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "growatt_server", + "name": "Growatt Server", + "documentation": "https://www.home-assistant.io/components/growatt_server/", + "requirements": [ + "growattServer==0.0.1" + ], + "dependencies": [], + "codeowners": [ + "@indykoning" + ] +} diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py new file mode 100644 index 00000000000000..3b7109222a4abb --- /dev/null +++ b/homeassistant/components/growatt_server/sensor.py @@ -0,0 +1,189 @@ +"""Read status of growatt inverters.""" +import re +import json +import logging +import datetime + +import growattServer +import voluptuous as vol + +from homeassistant.util import Throttle +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD + +_LOGGER = logging.getLogger(__name__) + +CONF_PLANT_ID = "plant_id" +DEFAULT_PLANT_ID = "0" +DEFAULT_NAME = "Growatt" +SCAN_INTERVAL = datetime.timedelta(minutes=5) + +TOTAL_SENSOR_TYPES = { + "total_money_today": ("Total money today", "€", "plantMoneyText", None), + "total_money_total": ("Money lifetime", "€", "totalMoneyText", None), + "total_energy_today": ("Energy Today", "kWh", "todayEnergy", "power"), + "total_output_power": ("Output Power", "W", "invTodayPpv", "power"), + "total_energy_output": ("Lifetime energy output", "kWh", "totalEnergy", "power"), + "total_maximum_output": ("Maximum power", "W", "nominalPower", "power"), +} + +INVERTER_SENSOR_TYPES = { + "inverter_energy_today": ("Energy today", "kWh", "e_today", "power"), + "inverter_energy_total": ("Lifetime energy output", "kWh", "e_total", "power"), + "inverter_voltage_input_1": ("Input 1 voltage", "V", "vpv1", None), + "inverter_amperage_input_1": ("Input 1 Amperage", "A", "ipv1", None), + "inverter_wattage_input_1": ("Input 1 Wattage", "W", "ppv1", "power"), + "inverter_voltage_input_2": ("Input 2 voltage", "V", "vpv2", None), + "inverter_amperage_input_2": ("Input 2 Amperage", "A", "ipv2", None), + "inverter_wattage_input_2": ("Input 2 Wattage", "W", "ppv2", "power"), + "inverter_voltage_input_3": ("Input 3 voltage", "V", "vpv3", None), + "inverter_amperage_input_3": ("Input 3 Amperage", "A", "ipv3", None), + "inverter_wattage_input_3": ("Input 3 Wattage", "W", "ppv3", "power"), + "inverter_internal_wattage": ("Internal wattage", "W", "ppv", "power"), + "inverter_reactive_voltage": ("Reactive voltage", "V", "vacr", None), + "inverter_inverter_reactive_amperage": ("Reactive amperage", "A", "iacr", None), + "inverter_frequency": ("AC frequency", "Hz", "fac", None), + "inverter_current_wattage": ("Output power", "W", "pac", "power"), + "inverter_current_reactive_wattage": ("Reactive wattage", "W", "pacr", "power"), +} + +SENSOR_TYPES = {**TOTAL_SENSOR_TYPES, **INVERTER_SENSOR_TYPES} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PLANT_ID, default=DEFAULT_PLANT_ID): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Growatt sensor.""" + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + plant_id = config[CONF_PLANT_ID] + name = config[CONF_NAME] + + api = growattServer.GrowattApi() + + # Log in to api and fetch first plant if no plant id is defined. + login_response = api.login(username, password) + if not login_response["success"] and login_response["errCode"] == "102": + _LOGGER.error("Username or Password may be incorrect!") + return + user_id = login_response["userId"] + if plant_id == DEFAULT_PLANT_ID: + plant_info = api.plant_list(user_id) + plant_id = plant_info["data"][0]["plantId"] + + # Get a list of inverters for specified plant to add sensors for. + inverters = api.inverter_list(plant_id) + entities = [] + probe = GrowattData(api, username, password, plant_id, True) + for sensor in TOTAL_SENSOR_TYPES: + entities.append( + GrowattInverter(probe, f"{name} Total", sensor, f"{plant_id}-{sensor}") + ) + + # Add sensors for each inverter in the specified plant. + for inverter in inverters: + probe = GrowattData(api, username, password, inverter["deviceSn"], False) + for sensor in INVERTER_SENSOR_TYPES: + entities.append( + GrowattInverter( + probe, + f"{inverter['deviceAilas']}", + sensor, + f"{inverter['deviceSn']}-{sensor}", + ) + ) + + add_entities(entities, True) + + +class GrowattInverter(Entity): + """Representation of a Growatt Sensor.""" + + def __init__(self, probe, name, sensor, unique_id): + """Initialize a PVOutput sensor.""" + self.sensor = sensor + self.probe = probe + self._name = name + self._state = None + self._unique_id = unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._name} {SENSOR_TYPES[self.sensor][0]}" + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self._unique_id + + @property + def icon(self): + """Return the icon of the sensor.""" + return "mdi:solar-power" + + @property + def state(self): + """Return the state of the sensor.""" + return self.probe.get_data(SENSOR_TYPES[self.sensor][2]) + + @property + def device_class(self): + """Return the device class of the sensor.""" + return SENSOR_TYPES[self.sensor][3] + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return SENSOR_TYPES[self.sensor][1] + + def update(self): + """Get the latest data from the Growat API and updates the state.""" + self.probe.update() + + +class GrowattData: + """The class for handling data retrieval.""" + + def __init__(self, api, username, password, inverter_id, is_total=False): + """Initialize the probe.""" + + self.is_total = is_total + self.api = api + self.inverter_id = inverter_id + self.data = {} + self.username = username + self.password = password + + @Throttle(SCAN_INTERVAL) + def update(self): + """Update probe data.""" + self.api.login(self.username, self.password) + _LOGGER.debug("Updating data for %s", self.inverter_id) + try: + if self.is_total: + total_info = self.api.plant_info(self.inverter_id) + del total_info["deviceList"] + # PlantMoneyText comes in as "3.1/€" remove anything that isn't part of the number + total_info["plantMoneyText"] = re.sub( + r"[^\d.,]", "", total_info["plantMoneyText"] + ) + self.data = total_info + else: + inverter_info = self.api.inverter_detail(self.inverter_id) + self.data = inverter_info["data"] + except json.decoder.JSONDecodeError: + _LOGGER.error("Unable to fetch data from Growatt server") + + def get_data(self, variable): + """Get the data.""" + return self.data.get(variable) diff --git a/requirements_all.txt b/requirements_all.txt index e467f5ab2babf0..503d14245f488f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -585,6 +585,9 @@ greeneye_monitor==1.0 # homeassistant.components.greenwave greenwavereality==0.5.1 +# homeassistant.components.growatt_server +growattServer==0.0.1 + # homeassistant.components.gstreamer gstreamer-player==1.1.2 From 5d5102e1a2d263e75937635100e82a2dbc71dc6f Mon Sep 17 00:00:00 2001 From: lifeisafractal Date: Mon, 9 Sep 2019 17:59:40 -0400 Subject: [PATCH 0235/3953] Add zwave application version (#26205) * Set application version in zwave * update tests for more coverage --- homeassistant/components/zwave/node_entity.py | 28 ++++++++ tests/components/zwave/test_node_entity.py | 67 +++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 66c3452f7c881d..44241e91daf942 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -17,6 +17,7 @@ EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, COMMAND_CLASS_CENTRAL_SCENE, + COMMAND_CLASS_VERSION, DOMAIN, ) from .util import node_name, is_node_parsed, node_device_id_and_name @@ -30,6 +31,7 @@ ATTR_PRODUCT_NAME = "product_name" ATTR_MANUFACTURER_NAME = "manufacturer_name" ATTR_NODE_NAME = "node_name" +ATTR_APPLICATION_VERSION = "application_version" STAGE_COMPLETE = "Complete" @@ -130,10 +132,14 @@ def __init__(self, node, network): self._product_name = node.product_name self._manufacturer_name = node.manufacturer_name self._unique_id = self._compute_unique_id() + self._application_version = None self._attributes = {} self.wakeup_interval = None self.location = None self.battery_level = None + dispatcher.connect( + self.network_node_value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED + ) dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_NODE) dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_NOTIFICATION) @@ -161,6 +167,24 @@ def device_info(self): info["via_device"] = (DOMAIN, 1) return info + def maybe_update_application_version(self, value): + """Update application version if value is a Command Class Version, Application Value.""" + if ( + value + and value.command_class == COMMAND_CLASS_VERSION + and value.label == "Application Version" + ): + self._application_version = value.data + + def network_node_value_added(self, node=None, value=None, args=None): + """Handle a added value to a none on the network.""" + if node and node.node_id != self.node_id: + return + if args is not None and "nodeId" in args and args["nodeId"] != self.node_id: + return + + self.maybe_update_application_version(value) + def network_node_changed(self, node=None, value=None, args=None): """Handle a changed node on the network.""" if node and node.node_id != self.node_id: @@ -172,6 +196,8 @@ def network_node_changed(self, node=None, value=None, args=None): if value is not None and value.command_class == COMMAND_CLASS_CENTRAL_SCENE: self.central_scene_activated(value.index, value.data) + self.maybe_update_application_version(value) + self.node_changed() def get_node_statistics(self): @@ -343,6 +369,8 @@ def device_state_attributes(self): attrs[ATTR_BATTERY_LEVEL] = self.battery_level if self.wakeup_interval is not None: attrs[ATTR_WAKEUP] = self.wakeup_interval + if self._application_version is not None: + attrs[ATTR_APPLICATION_VERSION] = self._application_version return attrs diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 1e5ec615088b09..dba187d7b966a0 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -164,6 +164,73 @@ def listener(event): assert events[0].data[const.ATTR_SCENE_DATA] == scene_data +async def test_application_version(hass, mock_openzwave): + """Test application version.""" + mock_receivers = {} + + signal_mocks = [ + mock_zwave.MockNetwork.SIGNAL_VALUE_CHANGED, + mock_zwave.MockNetwork.SIGNAL_VALUE_ADDED, + ] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal in signal_mocks: + mock_receivers[signal] = receiver + + node = mock_zwave.MockNode(node_id=11) + + with patch("pydispatch.dispatcher.connect", new=mock_connect): + entity = node_entity.ZWaveNodeEntity(node, mock_openzwave) + + for signal_mock in signal_mocks: + assert signal_mock in mock_receivers.keys() + + events = [] + + def listener(event): + events.append(event) + + # Make sure application version isn't set before + assert ( + node_entity.ATTR_APPLICATION_VERSION + not in entity.device_state_attributes.keys() + ) + + # Add entity to hass + entity.hass = hass + entity.entity_id = "zwave.mock_node" + + # Fire off an added value + value = mock_zwave.MockValue( + command_class=const.COMMAND_CLASS_VERSION, + label="Application Version", + data="5.10", + ) + hass.async_add_job( + mock_receivers[mock_zwave.MockNetwork.SIGNAL_VALUE_ADDED], node, value + ) + await hass.async_block_till_done() + + assert ( + entity.device_state_attributes[node_entity.ATTR_APPLICATION_VERSION] == "5.10" + ) + + # Fire off a changed + value = mock_zwave.MockValue( + command_class=const.COMMAND_CLASS_VERSION, + label="Application Version", + data="4.14", + ) + hass.async_add_job( + mock_receivers[mock_zwave.MockNetwork.SIGNAL_VALUE_CHANGED], node, value + ) + await hass.async_block_till_done() + + assert ( + entity.device_state_attributes[node_entity.ATTR_APPLICATION_VERSION] == "4.14" + ) + + @pytest.mark.usefixtures("mock_openzwave") class TestZWaveNodeEntity(unittest.TestCase): """Class to test ZWaveNodeEntity.""" From 9df5c0ab86c2e58fa9b750a8624af4370000ac49 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 10 Sep 2019 00:32:37 +0000 Subject: [PATCH 0236/3953] [ci skip] Translation update --- .../components/deconz/.translations/pl.json | 2 +- .../iaqualink/.translations/da.json | 21 ++++++++++++++++ .../iaqualink/.translations/pl.json | 21 ++++++++++++++++ .../components/light/.translations/it.json | 14 +++++------ .../components/light/.translations/ko.json | 14 +++++------ .../components/light/.translations/pl.json | 9 +++++++ .../components/light/.translations/ru.json | 14 +++++------ .../components/notion/.translations/pl.json | 2 +- .../solaredge/.translations/ca.json | 21 ++++++++++++++++ .../solaredge/.translations/da.json | 21 ++++++++++++++++ .../solaredge/.translations/en.json | 24 +++++++++---------- .../solaredge/.translations/it.json | 21 ++++++++++++++++ .../solaredge/.translations/ko.json | 21 ++++++++++++++++ .../solaredge/.translations/pl.json | 21 ++++++++++++++++ .../solaredge/.translations/ru.json | 21 ++++++++++++++++ .../components/switch/.translations/en.json | 17 +++++++++++++ .../components/switch/.translations/it.json | 17 +++++++++++++ .../components/switch/.translations/ko.json | 17 +++++++++++++ .../components/switch/.translations/ru.json | 17 +++++++++++++ .../components/tradfri/.translations/pl.json | 2 +- .../components/unifi/.translations/it.json | 6 ----- 21 files changed, 281 insertions(+), 42 deletions(-) create mode 100644 homeassistant/components/iaqualink/.translations/da.json create mode 100644 homeassistant/components/iaqualink/.translations/pl.json create mode 100644 homeassistant/components/solaredge/.translations/ca.json create mode 100644 homeassistant/components/solaredge/.translations/da.json create mode 100644 homeassistant/components/solaredge/.translations/it.json create mode 100644 homeassistant/components/solaredge/.translations/ko.json create mode 100644 homeassistant/components/solaredge/.translations/pl.json create mode 100644 homeassistant/components/solaredge/.translations/ru.json create mode 100644 homeassistant/components/switch/.translations/en.json create mode 100644 homeassistant/components/switch/.translations/it.json create mode 100644 homeassistant/components/switch/.translations/ko.json create mode 100644 homeassistant/components/switch/.translations/ru.json diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 67af24ceb943d1..506461ea50e8e0 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -28,7 +28,7 @@ "title": "Zdefiniuj bramk\u0119 deCONZ" }, "link": { - "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"", + "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawienia deCONZ > bramka > Zaawansowane\n 2. Naci\u015bnij przycisk \"Uwierzytelnij aplikacj\u0119\"", "title": "Po\u0142\u0105cz z deCONZ" }, "options": { diff --git a/homeassistant/components/iaqualink/.translations/da.json b/homeassistant/components/iaqualink/.translations/da.json new file mode 100644 index 00000000000000..a1e1c20cbc5e5f --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en enkelt iAqualink-forbindelse." + }, + "error": { + "connection_failure": "Kan ikke oprette forbindelse til iAqualink. Kontroller dit brugernavn og din adgangskode." + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn / e-mail-adresse" + }, + "description": "Indtast brugernavn og adgangskode til din iAqualink-konto.", + "title": "Opret forbindelse til iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/pl.json b/homeassistant/components/iaqualink/.translations/pl.json new file mode 100644 index 00000000000000..211a65f5ccb4fb --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno po\u0142\u0105czenie iAqualink." + }, + "error": { + "connection_failure": "Nie mo\u017cna po\u0142\u0105czy\u0107 z iAqualink. Sprawd\u017a nazw\u0119 u\u017cytkownika i has\u0142o." + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika / adres e-mail" + }, + "description": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o do konta iAqualink.", + "title": "Po\u0142\u0105cz z iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/it.json b/homeassistant/components/light/.translations/it.json index b8d1c95750a0d0..85a117f0b53f2b 100644 --- a/homeassistant/components/light/.translations/it.json +++ b/homeassistant/components/light/.translations/it.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Commuta {nome}", - "turn_off": "Spegnere {nome}", - "turn_on": "Accendere {nome}" + "toggle": "Commuta {entity_name}", + "turn_off": "Spegnere {entity_name}", + "turn_on": "Accendere {entity_name}" }, "condition_type": { - "is_off": "{name} \u00e8 disattivato", - "is_on": "{name} \u00e8 attivo" + "is_off": "{entity_name} \u00e8 disattivato", + "is_on": "{entity_name} \u00e8 attivo" }, "trigger_type": { - "turn_off": "{name} disattivato", - "turn_on": "{name} attivato" + "turn_off": "{entity_name} disattivato", + "turn_on": "{entity_name} attivato" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json index d62717a0725a4b..7277ef5900f02a 100644 --- a/homeassistant/components/light/.translations/ko.json +++ b/homeassistant/components/light/.translations/ko.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "{name} \ud1a0\uae00", - "turn_off": "{name} \ub044\uae30", - "turn_on": "{name} \ucf1c\uae30" + "toggle": "{entity_name} \ud1a0\uae00", + "turn_off": "{entity_name} \ub044\uae30", + "turn_on": "{entity_name} \ucf1c\uae30" }, "condition_type": { - "is_off": "{name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "is_on": "{name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" }, "trigger_type": { - "turn_off": "{name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turn_on": "{name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index a3df3639c22a8b..9debeaf4169d4c 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Prze\u0142\u0105cz {entity_name}", + "turn_off": "Wy\u0142\u0105cz {entity_name}", + "turn_on": "W\u0142\u0105cz {entity_name}" + }, + "condition_type": { + "is_off": "(entity_name} jest wy\u0142\u0105czony.", + "is_on": "(entity_name} jest w\u0142\u0105czony." + }, "trigger_type": { "turn_off": "{nazwa} wy\u0142\u0105czone", "turn_on": "{name} w\u0142\u0105czone" diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json index c0a5e09d24431e..3154e17a509c7f 100644 --- a/homeassistant/components/light/.translations/ru.json +++ b/homeassistant/components/light/.translations/ru.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {name}", - "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {name}", - "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {name}" + "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, "condition_type": { - "is_off": "{name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "is_on": "{name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turn_off": "{name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "{name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } } } \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/pl.json b/homeassistant/components/notion/.translations/pl.json index 279290bff1e679..380d4ad151e6d6 100644 --- a/homeassistant/components/notion/.translations/pl.json +++ b/homeassistant/components/notion/.translations/pl.json @@ -9,7 +9,7 @@ "user": { "data": { "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika/adres e-mail" + "username": "Nazwa u\u017cytkownika / adres e-mail" }, "title": "Wprowad\u017a dane" } diff --git a/homeassistant/components/solaredge/.translations/ca.json b/homeassistant/components/solaredge/.translations/ca.json new file mode 100644 index 00000000000000..fd3707af3ddd88 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Aquest site_id ja est\u00e0 configurat" + }, + "error": { + "site_exists": "Aquest site_id ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API d\u2019aquest lloc", + "name": "Nom d\u2019aquesta instal\u00b7laci\u00f3", + "site_id": "SolarEdge site_id" + }, + "title": "Configuraci\u00f3 dels par\u00e0metres de l'API per aquesta instal\u00b7laci\u00f3" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/da.json b/homeassistant/components/solaredge/.translations/da.json new file mode 100644 index 00000000000000..7ed64f51083d16 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Dette site_id er allerede konfigureret" + }, + "error": { + "site_exists": "Dette site_id er allerede konfigureret" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8glen til dette websted", + "name": "Navnet p\u00e5 denne installation", + "site_id": "SolarEdge site-id" + }, + "title": "Definer API-parametre til denne installation" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/en.json b/homeassistant/components/solaredge/.translations/en.json index 3265e3bb1b0a86..7b06c110397f04 100644 --- a/homeassistant/components/solaredge/.translations/en.json +++ b/homeassistant/components/solaredge/.translations/en.json @@ -1,21 +1,21 @@ { "config": { - "title": "SolarEdge", + "abort": { + "site_exists": "This site_id is already configured" + }, + "error": { + "site_exists": "This site_id is already configured" + }, "step": { "user": { - "title": "Define the API parameters for this installation", "data": { + "api_key": "The API key for this site", "name": "The name of this installation", - "site_id": "The SolarEdge site-id", - "api_key": "The API key for this site" - } + "site_id": "The SolarEdge site-id" + }, + "title": "Define the API parameters for this installation" } }, - "error": { - "site_exists": "This site_id is already configured" - }, - "abort": { - "site_exists": "This site_id is already configured" - } + "title": "SolarEdge" } -} +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/it.json b/homeassistant/components/solaredge/.translations/it.json new file mode 100644 index 00000000000000..6523f393628f27 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato" + }, + "error": { + "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato" + }, + "step": { + "user": { + "data": { + "api_key": "La chiave API per questo sito", + "name": "Il nome di questa installazione", + "site_id": "Il sito-id di SolarEdge" + }, + "title": "Definire i parametri API per questa installazione" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/ko.json b/homeassistant/components/solaredge/.translations/ko.json new file mode 100644 index 00000000000000..3d4b344825289a --- /dev/null +++ b/homeassistant/components/solaredge/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "\uc774 \uc0ac\uc774\ud2b8\uc758 API \ud0a4", + "name": "\uc774 \uc124\uce58\uc758 \uc774\ub984", + "site_id": "SolarEdge site-id" + }, + "title": "\uc774 \uc124\uce58\uc5d0 \ub300\ud55c API \ub9e4\uac1c\ubcc0\uc218\ub97c \uc815\uc758\ud574\uc8fc\uc138\uc694" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/pl.json b/homeassistant/components/solaredge/.translations/pl.json new file mode 100644 index 00000000000000..376a81219b0c81 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Ten site_id jest ju\u017c skonfigurowany" + }, + "error": { + "site_exists": "Ten site_id jest ju\u017c skonfigurowany" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API dla tej strony", + "name": "Nazwa tej instalacji", + "site_id": "SolarEdge site-id" + }, + "title": "Zdefiniuj parametry API dla tej instalacji" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json new file mode 100644 index 00000000000000..fe36e4296feb92 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + }, + "error": { + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u0430\u0439\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "site_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0430\u0439\u0442\u0430 SolarEdge" + }, + "title": "SolarEdge" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/en.json b/homeassistant/components/switch/.translations/en.json new file mode 100644 index 00000000000000..5be333cbf13bf7 --- /dev/null +++ b/homeassistant/components/switch/.translations/en.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Toggle {entity_name}", + "turn_off": "Turn off {entity_name}", + "turn_on": "Turn on {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} turned off", + "turn_on": "{entity_name} turned on" + }, + "trigger_type": { + "turn_off": "{entity_name} turned off", + "turn_on": "{entity_name} turned on" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/it.json b/homeassistant/components/switch/.translations/it.json new file mode 100644 index 00000000000000..c51ce8c6ee5934 --- /dev/null +++ b/homeassistant/components/switch/.translations/it.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Attivare / Disattivare {entity_name}", + "turn_off": "Disattivare {entity_name}", + "turn_on": "Attivare {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} disattivato", + "turn_on": "{entity_name} attivato" + }, + "trigger_type": { + "turn_off": "{entity_name} disattivato", + "turn_on": "{entity_name} attivato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ko.json b/homeassistant/components/switch/.translations/ko.json new file mode 100644 index 00000000000000..2156ea04e01d68 --- /dev/null +++ b/homeassistant/components/switch/.translations/ko.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} \ud1a0\uae00", + "turn_off": "{entity_name} \ub044\uae30", + "turn_on": "{entity_name} \ucf1c\uae30" + }, + "condition_type": { + "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + }, + "trigger_type": { + "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json new file mode 100644 index 00000000000000..1b0658cd174102 --- /dev/null +++ b/homeassistant/components/switch/.translations/ru.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "trigger_type": { + "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json index e3fcfc89c5bd4b..3a1798e66d995b 100644 --- a/homeassistant/components/tradfri/.translations/pl.json +++ b/homeassistant/components/tradfri/.translations/pl.json @@ -15,7 +15,7 @@ "host": "Host", "security_code": "Kod bezpiecze\u0144stwa" }, - "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramy.", + "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramki.", "title": "Wprowad\u017a kod bezpiecze\u0144stwa" } }, diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json index c4c76c4232d2e7..499c77bfb69c23 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -32,12 +32,6 @@ "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)", "track_wired_clients": "Includi i client di rete cablata" } - }, - "init": { - "data": { - "one": "Vuoto", - "other": "Vuoto" - } } } } From adf6852acc47cf41e5fe9670519b5b8b6a998fbb Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Tue, 10 Sep 2019 04:12:20 -0700 Subject: [PATCH 0237/3953] Remove unnecessary force_refresh=True, clarify system behavior (#26543) * Remove unnecessary force_refresh=True, clarify system behavior * Fix lint. --- homeassistant/components/iaqualink/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index c7b7bc472dd544..906d9cd6260c90 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -145,7 +145,14 @@ async def wrapper(self, *args, **kwargs): class AqualinkEntity(Entity): - """Abstract class for all Aqualink platforms.""" + """Abstract class for all Aqualink platforms. + + Entity state is updated via the interval timer within the integration. + Any entity state change via the iaqualink library triggers an internal + state refresh which is then propagated to all the entities in the system + via the refresh_system decorator above to the _update_callback in this + class. + """ async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" @@ -153,7 +160,7 @@ async def async_added_to_hass(self) -> None: @callback def _update_callback(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) + self.async_schedule_update_ha_state() @property def should_poll(self) -> bool: From d746035a9177062bc4e893089c4607dc96ffafd2 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 10 Sep 2019 13:17:10 +0200 Subject: [PATCH 0238/3953] =?UTF-8?q?Add=20M=C3=A9t=C3=A9o=20France=20icon?= =?UTF-8?q?s=20+=20device=5Fclass=20(#26441)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Météo France icons * Add Météo France device_class (temperature) + use constants * Fix weather alert info log * Use new f"{...} {...}" format for sensor name --- .../components/meteo_france/__init__.py | 59 +-------- .../components/meteo_france/const.py | 112 ++++++++++++++++++ .../components/meteo_france/sensor.py | 27 ++++- .../components/meteo_france/weather.py | 2 +- 4 files changed, 139 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/meteo_france/const.py diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index d6460fd6e5aa01..cfcd78400bd5e8 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -4,70 +4,17 @@ import voluptuous as vol -from homeassistant.const import CONF_MONITORED_CONDITIONS, TEMP_CELSIUS +from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.util import Throttle -_LOGGER = logging.getLogger(__name__) - -ATTRIBUTION = "Data provided by Météo-France" - -CONF_CITY = "city" +from .const import DOMAIN, CONF_CITY, SENSOR_TYPES, DATA_METEO_FRANCE -DATA_METEO_FRANCE = "data_meteo_france" -DEFAULT_WEATHER_CARD = True -DOMAIN = "meteo_france" +_LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(minutes=5) -SENSOR_TYPES = { - "rain_chance": ["Rain chance", "%"], - "freeze_chance": ["Freeze chance", "%"], - "thunder_chance": ["Thunder chance", "%"], - "snow_chance": ["Snow chance", "%"], - "weather": ["Weather", None], - "wind_speed": ["Wind Speed", "km/h"], - "next_rain": ["Next rain", "min"], - "temperature": ["Temperature", TEMP_CELSIUS], - "uv": ["UV", None], - "weather_alert": ["Weather Alert", None], -} - -CONDITION_CLASSES = { - "clear-night": ["Nuit Claire"], - "cloudy": ["Très nuageux"], - "fog": ["Brume ou bancs de brouillard", "Brouillard", "Brouillard givrant"], - "hail": ["Risque de grêle"], - "lightning": ["Risque d'orages", "Orages"], - "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"], - "partlycloudy": ["Ciel voilé", "Ciel voilé nuit", "Éclaircies"], - "pouring": ["Pluie forte"], - "rainy": [ - "Bruine / Pluie faible", - "Bruine", - "Pluie faible", - "Pluies éparses / Rares averses", - "Pluies éparses", - "Rares averses", - "Pluie / Averses", - "Averses", - "Pluie", - ], - "snowy": [ - "Neige / Averses de neige", - "Neige", - "Averses de neige", - "Neige forte", - "Quelques flocons", - ], - "snowy-rainy": ["Pluie et neige", "Pluie verglaçante"], - "sunny": ["Ensoleillé"], - "windy": [], - "windy-variant": [], - "exceptional": [], -} - def has_all_unique_cities(value): """Validate that all cities are unique.""" diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py new file mode 100644 index 00000000000000..223aca20bac240 --- /dev/null +++ b/homeassistant/components/meteo_france/const.py @@ -0,0 +1,112 @@ +"""Meteo-France component constants.""" + +from homeassistant.const import TEMP_CELSIUS + +DOMAIN = "meteo_france" +DATA_METEO_FRANCE = "data_meteo_france" +ATTRIBUTION = "Data provided by Météo-France" + +CONF_CITY = "city" + +DEFAULT_WEATHER_CARD = True + +SENSOR_TYPE_NAME = "name" +SENSOR_TYPE_UNIT = "unit" +SENSOR_TYPE_ICON = "icon" +SENSOR_TYPE_CLASS = "device_class" +SENSOR_TYPES = { + "rain_chance": { + SENSOR_TYPE_NAME: "Rain chance", + SENSOR_TYPE_UNIT: "%", + SENSOR_TYPE_ICON: "mdi:weather-rainy", + SENSOR_TYPE_CLASS: None, + }, + "freeze_chance": { + SENSOR_TYPE_NAME: "Freeze chance", + SENSOR_TYPE_UNIT: "%", + SENSOR_TYPE_ICON: "mdi:snowflake", + SENSOR_TYPE_CLASS: None, + }, + "thunder_chance": { + SENSOR_TYPE_NAME: "Thunder chance", + SENSOR_TYPE_UNIT: "%", + SENSOR_TYPE_ICON: "mdi:weather-lightning", + SENSOR_TYPE_CLASS: None, + }, + "snow_chance": { + SENSOR_TYPE_NAME: "Snow chance", + SENSOR_TYPE_UNIT: "%", + SENSOR_TYPE_ICON: "mdi:weather-snowy", + SENSOR_TYPE_CLASS: None, + }, + "weather": { + SENSOR_TYPE_NAME: "Weather", + SENSOR_TYPE_UNIT: None, + SENSOR_TYPE_ICON: "mdi:weather-partly-cloudy", + SENSOR_TYPE_CLASS: None, + }, + "wind_speed": { + SENSOR_TYPE_NAME: "Wind Speed", + SENSOR_TYPE_UNIT: "km/h", + SENSOR_TYPE_ICON: "mdi:weather-windy", + SENSOR_TYPE_CLASS: None, + }, + "next_rain": { + SENSOR_TYPE_NAME: "Next rain", + SENSOR_TYPE_UNIT: "min", + SENSOR_TYPE_ICON: "mdi:weather-rainy", + SENSOR_TYPE_CLASS: None, + }, + "temperature": { + SENSOR_TYPE_NAME: "Temperature", + SENSOR_TYPE_UNIT: TEMP_CELSIUS, + SENSOR_TYPE_ICON: "mdi:thermometer", + SENSOR_TYPE_CLASS: "temperature", + }, + "uv": { + SENSOR_TYPE_NAME: "UV", + SENSOR_TYPE_UNIT: None, + SENSOR_TYPE_ICON: "mdi:sunglasses", + SENSOR_TYPE_CLASS: None, + }, + "weather_alert": { + SENSOR_TYPE_NAME: "Weather Alert", + SENSOR_TYPE_UNIT: None, + SENSOR_TYPE_ICON: "mdi:weather-cloudy-alert", + SENSOR_TYPE_CLASS: None, + }, +} + +CONDITION_CLASSES = { + "clear-night": ["Nuit Claire"], + "cloudy": ["Très nuageux"], + "fog": ["Brume ou bancs de brouillard", "Brouillard", "Brouillard givrant"], + "hail": ["Risque de grêle"], + "lightning": ["Risque d'orages", "Orages"], + "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"], + "partlycloudy": ["Ciel voilé", "Ciel voilé nuit", "Éclaircies"], + "pouring": ["Pluie forte"], + "rainy": [ + "Bruine / Pluie faible", + "Bruine", + "Pluie faible", + "Pluies éparses / Rares averses", + "Pluies éparses", + "Rares averses", + "Pluie / Averses", + "Averses", + "Pluie", + ], + "snowy": [ + "Neige / Averses de neige", + "Neige", + "Averses de neige", + "Neige forte", + "Quelques flocons", + ], + "snowy-rainy": ["Pluie et neige", "Pluie verglaçante"], + "sunny": ["Ensoleillé"], + "windy": [], + "windy-variant": [], + "exceptional": [], +} diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 95113a60cd38ee..8c2bd32048fc00 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -4,7 +4,16 @@ from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity -from . import ATTRIBUTION, CONF_CITY, DATA_METEO_FRANCE, SENSOR_TYPES +from .const import ( + ATTRIBUTION, + CONF_CITY, + DATA_METEO_FRANCE, + SENSOR_TYPES, + SENSOR_TYPE_ICON, + SENSOR_TYPE_NAME, + SENSOR_TYPE_UNIT, + SENSOR_TYPE_CLASS, +) _LOGGER = logging.getLogger(__name__) @@ -44,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 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"], ) @@ -79,7 +88,7 @@ def __init__(self, condition, client, alert_watcher): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._data["name"], SENSOR_TYPES[self._condition][0]) + return f"{self._data['name']} {SENSOR_TYPES[self._condition][SENSOR_TYPE_NAME]}" @property def state(self): @@ -111,7 +120,17 @@ def device_state_attributes(self): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return SENSOR_TYPES[self._condition][1] + return SENSOR_TYPES[self._condition][SENSOR_TYPE_UNIT] + + @property + def icon(self): + """Return the icon.""" + return SENSOR_TYPES[self._condition][SENSOR_TYPE_ICON] + + @property + def device_class(self): + """Return the device class of the sensor.""" + return SENSOR_TYPES[self._condition][SENSOR_TYPE_CLASS] def update(self): """Fetch new state data for the sensor.""" diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 9a861d13c2eab4..00da55809ffb64 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -12,7 +12,7 @@ import homeassistant.util.dt as dt_util from homeassistant.const import TEMP_CELSIUS -from . import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE +from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE _LOGGER = logging.getLogger(__name__) From b321ed2fdb49f3fc5ede9d15eef5f33f20b4f500 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 10 Sep 2019 17:15:35 +0200 Subject: [PATCH 0239/3953] Update azure-pipelines-ci.yml for Azure Pipelines --- azure-pipelines-ci.yml | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 0ee272f900daa7..558c0c39f663b9 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -113,7 +113,7 @@ stages: 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 + pytest --timeout=9 --durations=10 -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'])) @@ -121,22 +121,11 @@ stages: set -e . 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 + pytest --timeout=9 --durations=10 --cov homeassistant --cov-report html -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 - condition: succeededOrFailed() - inputs: - testResultsFiles: 'test-results.xml' - testRunTitle: 'Publish test results for Python $(python.container)' - - task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: cobertura - summaryFileLocation: coverage.xml - displayName: 'publish coverage artifact' - condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain'])) - stage: 'FullCheck' dependsOn: From fbc3376c326c414ac291fe8ff4713efadd463455 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 10 Sep 2019 16:19:08 +0100 Subject: [PATCH 0240/3953] Bump geniushub-client to 0.6.13 (#26554) * bump geniushub client --- homeassistant/components/geniushub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index b007dfdd7844b7..f2110ffb2f0220 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.6.11" + "geniushub-client==0.6.13" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/requirements_all.txt b/requirements_all.txt index 503d14245f488f..a5670313692bbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -525,7 +525,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.6.11 +geniushub-client==0.6.13 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From a7830bc2d2c7e57d4ce083be7dc7bfe334d2d7f7 Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Tue, 10 Sep 2019 13:01:13 -0700 Subject: [PATCH 0241/3953] Add sensor platform to iaqualink component (#26544) * Add sensor platform to iaqualink component * Remove unnecessary icon, fix case where value is 0 --- .coveragerc | 1 + .../components/iaqualink/__init__.py | 10 ++++ homeassistant/components/iaqualink/sensor.py | 59 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 homeassistant/components/iaqualink/sensor.py diff --git a/.coveragerc b/.coveragerc index 6ade2d1f703150..e5848b8f4615ea 100644 --- a/.coveragerc +++ b/.coveragerc @@ -290,6 +290,7 @@ omit = homeassistant/components/ialarm/alarm_control_panel.py homeassistant/components/iaqualink/climate.py homeassistant/components/iaqualink/light.py + homeassistant/components/iaqualink/sensor.py homeassistant/components/icloud/device_tracker.py homeassistant/components/idteck_prox/* homeassistant/components/ifttt/* diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 906d9cd6260c90..925fd0a7906d5e 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -10,12 +10,14 @@ AqualinkClient, AqualinkLight, AqualinkLoginException, + AqualinkSensor, AqualinkThermostat, ) from homeassistant import config_entries from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback @@ -74,6 +76,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None # These will contain the initialized devices climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = [] lights = hass.data[DOMAIN][LIGHT_DOMAIN] = [] + sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) aqualink = AqualinkClient(username, password, session) @@ -97,6 +100,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None climates += [dev] elif isinstance(dev, AqualinkLight): lights += [dev] + elif isinstance(dev, AqualinkSensor): + sensors += [dev] forward_setup = hass.config_entries.async_forward_entry_setup if climates: @@ -105,6 +110,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None if lights: _LOGGER.debug("Got %s lights: %s", len(lights), lights) hass.async_create_task(forward_setup(entry, LIGHT_DOMAIN)) + if sensors: + _LOGGER.debug("Got %s sensors: %s", len(sensors), sensors) + hass.async_create_task(forward_setup(entry, SENSOR_DOMAIN)) async def _async_systems_update(now): """Refresh internal state for all systems.""" @@ -126,6 +134,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo tasks += [forward_unload(entry, CLIMATE_DOMAIN)] if hass.data[DOMAIN][LIGHT_DOMAIN]: tasks += [forward_unload(entry, LIGHT_DOMAIN)] + if hass.data[DOMAIN][SENSOR_DOMAIN]: + tasks += [forward_unload(entry, SENSOR_DOMAIN)] hass.data[DOMAIN].clear() diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py new file mode 100644 index 00000000000000..4a1691e0314caa --- /dev/null +++ b/homeassistant/components/iaqualink/sensor.py @@ -0,0 +1,59 @@ +"""Support for Aqualink temperature sensors.""" +import logging +from typing import Optional + +from iaqualink import AqualinkSensor + +from homeassistant.components.sensor import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.typing import HomeAssistantType + +from . import AqualinkEntity +from .const import DOMAIN as AQUALINK_DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up discovered sensors.""" + devs = [] + for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: + devs.append(HassAqualinkSensor(dev)) + async_add_entities(devs, True) + + +class HassAqualinkSensor(AqualinkEntity): + """Representation of a sensor.""" + + def __init__(self, dev: AqualinkSensor): + """Initialize the sensor.""" + self.dev = dev + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return self.dev.label + + @property + def unit_of_measurement(self) -> str: + """Return the measurement unit for the sensor.""" + if self.dev.system.temp_unit == "F": + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + @property + def state(self) -> str: + """Return the state of the sensor.""" + return int(self.dev.state) if self.dev.state != "" else None + + @property + def device_class(self) -> Optional[str]: + """Return the class of the sensor.""" + if self.dev.name.endswith("_temp"): + return DEVICE_CLASS_TEMPERATURE + return None From 7468cc21bed278e8abc79c8ad28ed1e251806d81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 10 Sep 2019 13:05:46 -0700 Subject: [PATCH 0242/3953] Refactor Cast (#26550) * Refactor Cast * Fix tests & address comments * Update reqs --- homeassistant/components/cast/const.py | 20 + homeassistant/components/cast/discovery.py | 99 ++++ homeassistant/components/cast/helpers.py | 246 +++++++++ homeassistant/components/cast/media_player.py | 519 +++--------------- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 35 +- tests/components/cast/test_media_player.py | 75 ++- 7 files changed, 526 insertions(+), 471 deletions(-) create mode 100644 homeassistant/components/cast/discovery.py create mode 100644 homeassistant/components/cast/helpers.py diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index e9f9ba4c39deac..a493c322f14100 100644 --- a/homeassistant/components/cast/const.py +++ b/homeassistant/components/cast/const.py @@ -1,3 +1,23 @@ """Consts for Cast integration.""" DOMAIN = "cast" +DEFAULT_PORT = 8009 + +# Stores a threading.Lock that is held by the internal pychromecast discovery. +INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running" +# Stores all ChromecastInfo we encountered through discovery or config as a set +# If we find a chromecast with a new host, the old one will be removed again. +KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts" +# Stores UUIDs of cast devices that were added as entities. Doesn't store +# None UUIDs. +ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices" +# Stores an audio group manager. +CAST_MULTIZONE_MANAGER_KEY = "cast_multizone_manager" + +# Dispatcher signal fired with a ChromecastInfo every time we discover a new +# Chromecast or receive it through configuration +SIGNAL_CAST_DISCOVERED = "cast_discovered" + +# Dispatcher signal fired with a ChromecastInfo every time a Chromecast is +# removed +SIGNAL_CAST_REMOVED = "cast_removed" diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py new file mode 100644 index 00000000000000..d3097b3cc29fb3 --- /dev/null +++ b/homeassistant/components/cast/discovery.py @@ -0,0 +1,99 @@ +"""Deal with Cast discovery.""" +import logging +import threading + +import pychromecast + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import dispatcher_send + +from .const import ( + KNOWN_CHROMECAST_INFO_KEY, + SIGNAL_CAST_DISCOVERED, + INTERNAL_DISCOVERY_RUNNING_KEY, + SIGNAL_CAST_REMOVED, +) +from .helpers import ChromecastInfo, ChromeCastZeroconf + +_LOGGER = logging.getLogger(__name__) + + +def discover_chromecast(hass: HomeAssistant, info: ChromecastInfo): + """Discover a Chromecast.""" + if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]: + _LOGGER.debug("Discovered previous chromecast %s", info) + + # Either discovered completely new chromecast or a "moved" one. + info = info.fill_out_missing_chromecast_info() + _LOGGER.debug("Discovered chromecast %s", info) + + if info.uuid is not None: + # Remove previous cast infos with same uuid from known chromecasts. + same_uuid = set( + x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] if info.uuid == x.uuid + ) + hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid + + hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) + dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) + + +def _remove_chromecast(hass: HomeAssistant, info: ChromecastInfo): + # Removed chromecast + _LOGGER.debug("Removed chromecast %s", info) + + dispatcher_send(hass, SIGNAL_CAST_REMOVED, info) + + +def setup_internal_discovery(hass: HomeAssistant) -> None: + """Set up the pychromecast internal discovery.""" + if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: + hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() + + if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False): + # Internal discovery is already running + return + + def internal_add_callback(name): + """Handle zeroconf discovery of a new chromecast.""" + mdns = listener.services[name] + discover_chromecast( + hass, + ChromecastInfo( + service=name, + host=mdns[0], + port=mdns[1], + uuid=mdns[2], + model_name=mdns[3], + friendly_name=mdns[4], + ), + ) + + def internal_remove_callback(name, mdns): + """Handle zeroconf discovery of a removed chromecast.""" + _remove_chromecast( + hass, + ChromecastInfo( + service=name, + host=mdns[0], + port=mdns[1], + uuid=mdns[2], + model_name=mdns[3], + friendly_name=mdns[4], + ), + ) + + _LOGGER.debug("Starting internal pychromecast discovery.") + listener, browser = pychromecast.start_discovery( + internal_add_callback, internal_remove_callback + ) + ChromeCastZeroconf.set_zeroconf(browser.zc) + + def stop_discovery(event): + """Stop discovery of new chromecasts.""" + _LOGGER.debug("Stopping internal pychromecast discovery.") + pychromecast.stop_discovery(browser) + hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py new file mode 100644 index 00000000000000..ea5c77ebc1afa3 --- /dev/null +++ b/homeassistant/components/cast/helpers.py @@ -0,0 +1,246 @@ +"""Helpers to deal with Cast devices.""" +from typing import Optional, Tuple + +import attr +from pychromecast import dial + +from .const import DEFAULT_PORT + + +@attr.s(slots=True, frozen=True) +class ChromecastInfo: + """Class to hold all data about a chromecast for creating connections. + + This also has the same attributes as the mDNS fields by zeroconf. + """ + + host = attr.ib(type=str) + port = attr.ib(type=int) + service = attr.ib(type=Optional[str], default=None) + uuid = attr.ib( + type=Optional[str], converter=attr.converters.optional(str), default=None + ) # always convert UUID to string if not None + manufacturer = attr.ib(type=str, default="") + model_name = attr.ib(type=str, default="") + friendly_name = attr.ib(type=Optional[str], default=None) + is_dynamic_group = attr.ib(type=Optional[bool], default=None) + + @property + def is_audio_group(self) -> bool: + """Return if this is an audio group.""" + return self.port != DEFAULT_PORT + + @property + def is_information_complete(self) -> bool: + """Return if all information is filled out.""" + want_dynamic_group = self.is_audio_group + have_dynamic_group = self.is_dynamic_group is not None + have_all_except_dynamic_group = all( + attr.astuple( + self, + filter=attr.filters.exclude( + attr.fields(ChromecastInfo).is_dynamic_group + ), + ) + ) + return have_all_except_dynamic_group and ( + not want_dynamic_group or have_dynamic_group + ) + + @property + def host_port(self) -> Tuple[str, int]: + """Return the host+port tuple.""" + return self.host, self.port + + def fill_out_missing_chromecast_info(self) -> "ChromecastInfo": + """Return a new ChromecastInfo object with missing attributes filled in. + + Uses blocking HTTP. + """ + if self.is_information_complete: + # We have all information, no need to check HTTP API. Or this is an + # audio group, so checking via HTTP won't give us any new information. + return self + + # Fill out missing information via HTTP dial. + if self.is_audio_group: + is_dynamic_group = False + http_group_status = None + dynamic_groups = [] + if self.uuid: + http_group_status = dial.get_multizone_status( + self.host, + services=[self.service], + zconf=ChromeCastZeroconf.get_zeroconf(), + ) + if http_group_status is not None: + dynamic_groups = [ + str(g.uuid) for g in http_group_status.dynamic_groups + ] + is_dynamic_group = self.uuid in dynamic_groups + + return ChromecastInfo( + service=self.service, + host=self.host, + port=self.port, + uuid=self.uuid, + friendly_name=self.friendly_name, + manufacturer=self.manufacturer, + model_name=self.model_name, + is_dynamic_group=is_dynamic_group, + ) + + http_device_status = dial.get_device_status( + self.host, services=[self.service], zconf=ChromeCastZeroconf.get_zeroconf() + ) + if http_device_status is None: + # HTTP dial didn't give us any new information. + return self + + return ChromecastInfo( + service=self.service, + host=self.host, + port=self.port, + uuid=(self.uuid or http_device_status.uuid), + friendly_name=(self.friendly_name or http_device_status.friendly_name), + manufacturer=(self.manufacturer or http_device_status.manufacturer), + model_name=(self.model_name or http_device_status.model_name), + ) + + def same_dynamic_group(self, other: "ChromecastInfo") -> bool: + """Test chromecast info is same dynamic group.""" + return ( + self.is_audio_group + and other.is_dynamic_group + and self.friendly_name == other.friendly_name + ) + + +class ChromeCastZeroconf: + """Class to hold a zeroconf instance.""" + + __zconf = None + + @classmethod + def set_zeroconf(cls, zconf): + """Set zeroconf.""" + cls.__zconf = zconf + + @classmethod + def get_zeroconf(cls): + """Get zeroconf.""" + return cls.__zconf + + +class CastStatusListener: + """Helper class to handle pychromecast status callbacks. + + Necessary because a CastDevice entity can create a new socket client + and therefore callbacks from multiple chromecast connections can + potentially arrive. This class allows invalidating past chromecast objects. + """ + + def __init__(self, cast_device, chromecast, mz_mgr): + """Initialize the status listener.""" + self._cast_device = cast_device + self._uuid = chromecast.uuid + self._valid = True + self._mz_mgr = mz_mgr + + chromecast.register_status_listener(self) + chromecast.socket_client.media_controller.register_status_listener(self) + chromecast.register_connection_listener(self) + # pylint: disable=protected-access + if cast_device._cast_info.is_audio_group: + self._mz_mgr.add_multizone(chromecast) + else: + self._mz_mgr.register_listener(chromecast.uuid, self) + + def new_cast_status(self, cast_status): + """Handle reception of a new CastStatus.""" + if self._valid: + self._cast_device.new_cast_status(cast_status) + + def new_media_status(self, media_status): + """Handle reception of a new MediaStatus.""" + if self._valid: + self._cast_device.new_media_status(media_status) + + def new_connection_status(self, connection_status): + """Handle reception of a new ConnectionStatus.""" + if self._valid: + self._cast_device.new_connection_status(connection_status) + + @staticmethod + def added_to_multizone(group_uuid): + """Handle the cast added to a group.""" + pass + + def removed_from_multizone(self, group_uuid): + """Handle the cast removed from a group.""" + if self._valid: + self._cast_device.multizone_new_media_status(group_uuid, None) + + def multizone_new_cast_status(self, group_uuid, cast_status): + """Handle reception of a new CastStatus for a group.""" + pass + + def multizone_new_media_status(self, group_uuid, media_status): + """Handle reception of a new MediaStatus for a group.""" + if self._valid: + self._cast_device.multizone_new_media_status(group_uuid, media_status) + + def invalidate(self): + """Invalidate this status listener. + + All following callbacks won't be forwarded. + """ + # pylint: disable=protected-access + if self._cast_device._cast_info.is_audio_group: + self._mz_mgr.remove_multizone(self._uuid) + else: + self._mz_mgr.deregister_listener(self._uuid, self) + self._valid = False + + +class DynamicGroupCastStatusListener: + """Helper class to handle pychromecast status callbacks. + + Necessary because a CastDevice entity can create a new socket client + and therefore callbacks from multiple chromecast connections can + potentially arrive. This class allows invalidating past chromecast objects. + """ + + def __init__(self, cast_device, chromecast, mz_mgr): + """Initialize the status listener.""" + self._cast_device = cast_device + self._uuid = chromecast.uuid + self._valid = True + self._mz_mgr = mz_mgr + + chromecast.register_status_listener(self) + chromecast.socket_client.media_controller.register_status_listener(self) + chromecast.register_connection_listener(self) + self._mz_mgr.add_multizone(chromecast) + + def new_cast_status(self, cast_status): + """Handle reception of a new CastStatus.""" + pass + + def new_media_status(self, media_status): + """Handle reception of a new MediaStatus.""" + if self._valid: + self._cast_device.new_dynamic_group_media_status(media_status) + + def new_connection_status(self, connection_status): + """Handle reception of a new ConnectionStatus.""" + if self._valid: + self._cast_device.new_dynamic_group_connection_status(connection_status) + + def invalidate(self): + """Invalidate this status listener. + + All following callbacks won't be forwarded. + """ + self._mz_mgr.remove_multizone(self._uuid) + self._valid = False diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 26bcaccc247625..7b750c3fe0cbfd 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -1,10 +1,14 @@ """Provide functionality to interact with Cast devices on the network.""" import asyncio import logging -import threading -from typing import Optional, Tuple +from typing import Optional -import attr +import pychromecast +from pychromecast.socket_client import ( + CONNECTION_STATUS_CONNECTED, + CONNECTION_STATUS_DISCONNECTED, +) +from pychromecast.controllers.multizone import MultizoneManager import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice @@ -35,22 +39,33 @@ from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro -from . import DOMAIN as CAST_DOMAIN - -DEPENDENCIES = ("cast",) +from .const import ( + DOMAIN as CAST_DOMAIN, + ADDED_CAST_DEVICES_KEY, + SIGNAL_CAST_DISCOVERED, + KNOWN_CHROMECAST_INFO_KEY, + CAST_MULTIZONE_MANAGER_KEY, + DEFAULT_PORT, + SIGNAL_CAST_REMOVED, +) +from .helpers import ( + ChromecastInfo, + CastStatusListener, + DynamicGroupCastStatusListener, + ChromeCastZeroconf, +) +from .discovery import setup_internal_discovery, discover_chromecast _LOGGER = logging.getLogger(__name__) CONF_IGNORE_CEC = "ignore_cec" CAST_SPLASH = "https://home-assistant.io/images/cast/splash.png" -DEFAULT_PORT = 8009 - SUPPORT_CAST = ( SUPPORT_PAUSE | SUPPORT_PLAY @@ -62,24 +77,6 @@ | SUPPORT_VOLUME_SET ) -# Stores a threading.Lock that is held by the internal pychromecast discovery. -INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running" -# Stores all ChromecastInfo we encountered through discovery or config as a set -# If we find a chromecast with a new host, the old one will be removed again. -KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts" -# Stores UUIDs of cast devices that were added as entities. Doesn't store -# None UUIDs. -ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices" -# Stores an audio group manager. -CAST_MULTIZONE_MANAGER_KEY = "cast_multizone_manager" - -# Dispatcher signal fired with a ChromecastInfo every time we discover a new -# Chromecast or receive it through configuration -SIGNAL_CAST_DISCOVERED = "cast_discovered" - -# Dispatcher signal fired with a ChromecastInfo every time a Chromecast is -# removed -SIGNAL_CAST_REMOVED = "cast_removed" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -89,212 +86,6 @@ ) -@attr.s(slots=True, frozen=True) -class ChromecastInfo: - """Class to hold all data about a chromecast for creating connections. - - This also has the same attributes as the mDNS fields by zeroconf. - """ - - host = attr.ib(type=str) - port = attr.ib(type=int) - service = attr.ib(type=Optional[str], default=None) - uuid = attr.ib( - type=Optional[str], converter=attr.converters.optional(str), default=None - ) # always convert UUID to string if not None - manufacturer = attr.ib(type=str, default="") - model_name = attr.ib(type=str, default="") - friendly_name = attr.ib(type=Optional[str], default=None) - is_dynamic_group = attr.ib(type=Optional[bool], default=None) - - @property - def is_audio_group(self) -> bool: - """Return if this is an audio group.""" - return self.port != DEFAULT_PORT - - @property - def is_information_complete(self) -> bool: - """Return if all information is filled out.""" - want_dynamic_group = self.is_audio_group - have_dynamic_group = self.is_dynamic_group is not None - have_all_except_dynamic_group = all( - attr.astuple( - self, - filter=attr.filters.exclude( - attr.fields(ChromecastInfo).is_dynamic_group - ), - ) - ) - return have_all_except_dynamic_group and ( - not want_dynamic_group or have_dynamic_group - ) - - @property - def host_port(self) -> Tuple[str, int]: - """Return the host+port tuple.""" - return self.host, self.port - - -def _is_matching_dynamic_group( - our_info: ChromecastInfo, new_info: ChromecastInfo -) -> bool: - return ( - our_info.is_audio_group - and new_info.is_dynamic_group - and our_info.friendly_name == new_info.friendly_name - ) - - -def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: - """Fill out missing attributes of ChromecastInfo using blocking HTTP.""" - if info.is_information_complete: - # We have all information, no need to check HTTP API. Or this is an - # audio group, so checking via HTTP won't give us any new information. - return info - - # Fill out missing information via HTTP dial. - from pychromecast import dial - - if info.is_audio_group: - is_dynamic_group = False - http_group_status = None - dynamic_groups = [] - if info.uuid: - http_group_status = dial.get_multizone_status( - info.host, - services=[info.service], - zconf=ChromeCastZeroconf.get_zeroconf(), - ) - if http_group_status is not None: - dynamic_groups = [str(g.uuid) for g in http_group_status.dynamic_groups] - is_dynamic_group = info.uuid in dynamic_groups - - return ChromecastInfo( - service=info.service, - host=info.host, - port=info.port, - uuid=info.uuid, - friendly_name=info.friendly_name, - manufacturer=info.manufacturer, - model_name=info.model_name, - is_dynamic_group=is_dynamic_group, - ) - - http_device_status = dial.get_device_status( - info.host, services=[info.service], zconf=ChromeCastZeroconf.get_zeroconf() - ) - if http_device_status is None: - # HTTP dial didn't give us any new information. - return info - - return ChromecastInfo( - service=info.service, - host=info.host, - port=info.port, - uuid=(info.uuid or http_device_status.uuid), - friendly_name=(info.friendly_name or http_device_status.friendly_name), - manufacturer=(info.manufacturer or http_device_status.manufacturer), - model_name=(info.model_name or http_device_status.model_name), - ) - - -def _discover_chromecast(hass: HomeAssistantType, info: ChromecastInfo): - if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]: - _LOGGER.debug("Discovered previous chromecast %s", info) - - # Either discovered completely new chromecast or a "moved" one. - info = _fill_out_missing_chromecast_info(info) - _LOGGER.debug("Discovered chromecast %s", info) - - if info.uuid is not None: - # Remove previous cast infos with same uuid from known chromecasts. - same_uuid = set( - x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] if info.uuid == x.uuid - ) - hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid - - hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) - dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) - - -def _remove_chromecast(hass: HomeAssistantType, info: ChromecastInfo): - # Removed chromecast - _LOGGER.debug("Removed chromecast %s", info) - - dispatcher_send(hass, SIGNAL_CAST_REMOVED, info) - - -class ChromeCastZeroconf: - """Class to hold a zeroconf instance.""" - - __zconf = None - - @classmethod - def set_zeroconf(cls, zconf): - """Set zeroconf.""" - cls.__zconf = zconf - - @classmethod - def get_zeroconf(cls): - """Get zeroconf.""" - return cls.__zconf - - -def _setup_internal_discovery(hass: HomeAssistantType) -> None: - """Set up the pychromecast internal discovery.""" - if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: - hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() - - if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False): - # Internal discovery is already running - return - - import pychromecast - - def internal_add_callback(name): - """Handle zeroconf discovery of a new chromecast.""" - mdns = listener.services[name] - _discover_chromecast( - hass, - ChromecastInfo( - service=name, - host=mdns[0], - port=mdns[1], - uuid=mdns[2], - model_name=mdns[3], - friendly_name=mdns[4], - ), - ) - - def internal_remove_callback(name, mdns): - """Handle zeroconf discovery of a removed chromecast.""" - _remove_chromecast( - hass, - ChromecastInfo( - service=name, - host=mdns[0], - port=mdns[1], - uuid=mdns[2], - model_name=mdns[3], - friendly_name=mdns[4], - ), - ) - - _LOGGER.debug("Starting internal pychromecast discovery.") - listener, browser = pychromecast.start_discovery( - internal_add_callback, internal_remove_callback - ) - ChromeCastZeroconf.set_zeroconf(browser.zc) - - def stop_discovery(event): - """Stop discovery of new chromecasts.""" - _LOGGER.debug("Stopping internal pychromecast discovery.") - pychromecast.stop_discovery(browser) - hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) - - @callback def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo): """Create a CastDevice Entity from the chromecast object. @@ -357,8 +148,6 @@ async def _async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info ): """Set up the cast platform.""" - import pychromecast - # Import CEC IGNORE attributes pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set()) @@ -390,9 +179,9 @@ def async_cast_discovered(discover: ChromecastInfo) -> None: if info is None or info.is_audio_group: # If we were a) explicitly told to enable discovery or # b) have an audio group cast device, we need internal discovery. - hass.async_add_job(_setup_internal_discovery, hass) + hass.async_add_executor_job(setup_internal_discovery, hass) else: - info = await hass.async_add_job(_fill_out_missing_chromecast_info, info) + info = await hass.async_add_executor_job(info.fill_out_missing_chromecast_info) if info.friendly_name is None: _LOGGER.debug( "Cannot retrieve detail information for chromecast" @@ -400,121 +189,7 @@ def async_cast_discovered(discover: ChromecastInfo) -> None: info, ) - hass.async_add_job(_discover_chromecast, hass, info) - - -class CastStatusListener: - """Helper class to handle pychromecast status callbacks. - - Necessary because a CastDevice entity can create a new socket client - and therefore callbacks from multiple chromecast connections can - potentially arrive. This class allows invalidating past chromecast objects. - """ - - def __init__(self, cast_device, chromecast, mz_mgr): - """Initialize the status listener.""" - self._cast_device = cast_device - self._uuid = chromecast.uuid - self._valid = True - self._mz_mgr = mz_mgr - - chromecast.register_status_listener(self) - chromecast.socket_client.media_controller.register_status_listener(self) - chromecast.register_connection_listener(self) - # pylint: disable=protected-access - if cast_device._cast_info.is_audio_group: - self._mz_mgr.add_multizone(chromecast) - else: - self._mz_mgr.register_listener(chromecast.uuid, self) - - def new_cast_status(self, cast_status): - """Handle reception of a new CastStatus.""" - if self._valid: - self._cast_device.new_cast_status(cast_status) - - def new_media_status(self, media_status): - """Handle reception of a new MediaStatus.""" - if self._valid: - self._cast_device.new_media_status(media_status) - - def new_connection_status(self, connection_status): - """Handle reception of a new ConnectionStatus.""" - if self._valid: - self._cast_device.new_connection_status(connection_status) - - @staticmethod - def added_to_multizone(group_uuid): - """Handle the cast added to a group.""" - pass - - def removed_from_multizone(self, group_uuid): - """Handle the cast removed from a group.""" - if self._valid: - self._cast_device.multizone_new_media_status(group_uuid, None) - - def multizone_new_cast_status(self, group_uuid, cast_status): - """Handle reception of a new CastStatus for a group.""" - pass - - def multizone_new_media_status(self, group_uuid, media_status): - """Handle reception of a new MediaStatus for a group.""" - if self._valid: - self._cast_device.multizone_new_media_status(group_uuid, media_status) - - def invalidate(self): - """Invalidate this status listener. - - All following callbacks won't be forwarded. - """ - # pylint: disable=protected-access - if self._cast_device._cast_info.is_audio_group: - self._mz_mgr.remove_multizone(self._uuid) - else: - self._mz_mgr.deregister_listener(self._uuid, self) - self._valid = False - - -class DynamicGroupCastStatusListener: - """Helper class to handle pychromecast status callbacks. - - Necessary because a CastDevice entity can create a new socket client - and therefore callbacks from multiple chromecast connections can - potentially arrive. This class allows invalidating past chromecast objects. - """ - - def __init__(self, cast_device, chromecast, mz_mgr): - """Initialize the status listener.""" - self._cast_device = cast_device - self._uuid = chromecast.uuid - self._valid = True - self._mz_mgr = mz_mgr - - chromecast.register_status_listener(self) - chromecast.socket_client.media_controller.register_status_listener(self) - chromecast.register_connection_listener(self) - self._mz_mgr.add_multizone(chromecast) - - def new_cast_status(self, cast_status): - """Handle reception of a new CastStatus.""" - pass - - def new_media_status(self, media_status): - """Handle reception of a new MediaStatus.""" - if self._valid: - self._cast_device.new_dynamic_group_media_status(media_status) - - def new_connection_status(self, connection_status): - """Handle reception of a new ConnectionStatus.""" - if self._valid: - self._cast_device.new_dynamic_group_connection_status(connection_status) - - def invalidate(self): - """Invalidate this status listener. - - All following callbacks won't be forwarded. - """ - self._mz_mgr.remove_multizone(self._uuid) - self._valid = False + hass.async_add_executor_job(discover_chromecast, hass, info) class CastDevice(MediaPlayerDevice): @@ -527,7 +202,6 @@ class CastDevice(MediaPlayerDevice): def __init__(self, cast_info: ChromecastInfo): """Initialize the cast device.""" - import pychromecast self._cast_info = cast_info self.services = None @@ -557,75 +231,18 @@ def __init__(self, cast_info: ChromecastInfo): async def async_added_to_hass(self): """Create chromecast object when added to hass.""" - - @callback - def async_cast_discovered(discover: ChromecastInfo): - """Handle discovery of new Chromecast.""" - if self._cast_info.uuid is None: - # We can't handle empty UUIDs - return - if _is_matching_dynamic_group(self._cast_info, discover): - _LOGGER.debug("Discovered matching dynamic group: %s", discover) - self.hass.async_create_task( - async_create_catching_coro(self.async_set_dynamic_group(discover)) - ) - return - - if self._cast_info.uuid != discover.uuid: - # Discovered is not our device. - return - if self.services is None: - _LOGGER.warning( - "[%s %s (%s:%s)] Received update for manually added Cast", - self.entity_id, - self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, - ) - return - _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) - self.hass.async_create_task( - async_create_catching_coro(self.async_set_cast_info(discover)) - ) - - def async_cast_removed(discover: ChromecastInfo): - """Handle removal of Chromecast.""" - if self._cast_info.uuid is None: - # We can't handle empty UUIDs - return - if ( - self._dynamic_group_cast_info is not None - and self._dynamic_group_cast_info.uuid == discover.uuid - ): - _LOGGER.debug("Removed matching dynamic group: %s", discover) - self.hass.async_create_task( - async_create_catching_coro(self.async_del_dynamic_group()) - ) - return - if self._cast_info.uuid != discover.uuid: - # Removed is not our device. - return - _LOGGER.debug("Removed chromecast with same UUID: %s", discover) - self.hass.async_create_task( - async_create_catching_coro(self.async_del_cast_info(discover)) - ) - - async def async_stop(event): - """Disconnect socket on Home Assistant stop.""" - await self._async_disconnect() - self._add_remove_handler = async_dispatcher_connect( - self.hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered + self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered ) self._del_remove_handler = async_dispatcher_connect( - self.hass, SIGNAL_CAST_REMOVED, async_cast_removed + self.hass, SIGNAL_CAST_REMOVED, self._async_cast_removed ) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) self.hass.async_create_task( async_create_catching_coro(self.async_set_cast_info(self._cast_info)) ) for info in self.hass.data[KNOWN_CHROMECAST_INFO_KEY]: - if _is_matching_dynamic_group(self._cast_info, info): + if self._cast_info.same_dynamic_group(info): _LOGGER.debug( "[%s %s (%s:%s)] Found dynamic group: %s", self.entity_id, @@ -653,7 +270,6 @@ async def async_will_remove_from_hass(self) -> None: async def async_set_cast_info(self, cast_info): """Set the cast information and set up the chromecast object.""" - import pychromecast self._cast_info = cast_info @@ -718,9 +334,8 @@ async def async_set_cast_info(self, cast_info): self._chromecast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: - from pychromecast.controllers.multizone import MultizoneManager - self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager() + self.mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr) @@ -745,7 +360,6 @@ async def async_del_cast_info(self, cast_info): async def async_set_dynamic_group(self, cast_info): """Set the cast information and set up the chromecast object.""" - import pychromecast _LOGGER.debug( "[%s %s (%s:%s)] Connecting to dynamic group by host %s", @@ -774,9 +388,8 @@ async def async_set_dynamic_group(self, cast_info): self._dynamic_group_cast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: - from pychromecast.controllers.multizone import MultizoneManager - self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager() + mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] self._dynamic_group_status_listener = DynamicGroupCastStatusListener( @@ -867,11 +480,6 @@ def new_media_status(self, media_status): def new_connection_status(self, connection_status): """Handle updates of connection status.""" - from pychromecast.socket_client import ( - CONNECTION_STATUS_CONNECTED, - CONNECTION_STATUS_DISCONNECTED, - ) - _LOGGER.debug( "[%s %s (%s:%s)] Received cast device connection status: %s", self.entity_id, @@ -902,7 +510,7 @@ def new_connection_status(self, connection_status): info = self._cast_info if info.friendly_name is None and not info.is_audio_group: # We couldn't find friendly_name when the cast was added, retry - self._cast_info = _fill_out_missing_chromecast_info(info) + self._cast_info = info.fill_out_missing_chromecast_info() self._available = new_available self.schedule_update_ha_state() @@ -914,11 +522,6 @@ def new_dynamic_group_media_status(self, media_status): def new_dynamic_group_connection_status(self, connection_status): """Handle updates of connection status.""" - from pychromecast.socket_client import ( - CONNECTION_STATUS_CONNECTED, - CONNECTION_STATUS_DISCONNECTED, - ) - _LOGGER.debug( "[%s %s (%s:%s)] Received dynamic group connection status: %s", self.entity_id, @@ -992,7 +595,6 @@ def _media_controller(self): def turn_on(self): """Turn on the cast device.""" - import pychromecast if not self._chromecast.is_idle: # Already turned on @@ -1277,3 +879,56 @@ def media_position_updated_at(self): def unique_id(self) -> Optional[str]: """Return a unique ID.""" return self._cast_info.uuid + + async def _async_cast_discovered(self, discover: ChromecastInfo): + """Handle discovery of new Chromecast.""" + if self._cast_info.uuid is None: + # We can't handle empty UUIDs + return + + if self._cast_info.same_dynamic_group(discover): + _LOGGER.debug("Discovered matching dynamic group: %s", discover) + await self.async_set_dynamic_group(discover) + return + + if self._cast_info.uuid != discover.uuid: + # Discovered is not our device. + return + + if self.services is None: + _LOGGER.warning( + "[%s %s (%s:%s)] Received update for manually added Cast", + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + ) + return + + _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) + await self.async_set_cast_info(discover) + + async def _async_cast_removed(self, discover: ChromecastInfo): + """Handle removal of Chromecast.""" + if self._cast_info.uuid is None: + # We can't handle empty UUIDs + return + + if ( + self._dynamic_group_cast_info is not None + and self._dynamic_group_cast_info.uuid == discover.uuid + ): + _LOGGER.debug("Removed matching dynamic group: %s", discover) + await self.async_del_dynamic_group() + return + + if self._cast_info.uuid != discover.uuid: + # Removed is not our device. + return + + _LOGGER.debug("Removed chromecast with same UUID: %s", discover) + await self.async_del_cast_info(discover) + + async def _async_stop(self, event): + """Disconnect socket on Home Assistant stop.""" + await self._async_disconnect() diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8582683d5f2241..1fc8ba3ebfe799 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,6 +279,9 @@ pyMetno==0.4.6 # homeassistant.components.blackbird pyblackbird==0.5 +# homeassistant.components.cast +pychromecast==3.2.2 + # homeassistant.components.deconz pydeconz==62 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ad6507b4e9ea60..ff2943a583b3f8 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -10,8 +10,8 @@ from script.hassfest.model import Integration COMMENT_REQUIREMENTS = ( - "Adafruit-DHT", "Adafruit_BBIO", + "Adafruit-DHT", "avion", "beacontools", "blinkt", @@ -26,7 +26,6 @@ "i2csense", "opencv-python-headless", "py_noaa", - "VL53L1X2", "pybluez", "pycups", "PySwitchbot", @@ -39,11 +38,11 @@ "RPi.GPIO", "smbus-cffi", "tensorflow", + "VL53L1X2", ) TEST_REQUIREMENTS = ( "adguardhome", - "ambiclimate", "aio_geojson_geonetnz_quakes", "aioambient", "aioautomatic", @@ -52,14 +51,16 @@ "aiohttp_cors", "aiohue", "aionotion", - "aiounifi", "aioswitcher", + "aiounifi", "aiowwlln", + "ambiclimate", "androidtv", "apns2", "aprslib", "av", "axis", + "bellows-homeassistant", "caldav", "coinmarketcap", "defusedxml", @@ -99,7 +100,6 @@ "libpurecool", "libsoundtouch", "luftdaten", - "pyMetno", "mbddns", "mficlient", "minio", @@ -115,44 +115,49 @@ "ptvsd", "pushbullet.py", "py-canary", + "py17track", "pyblackbird", + "pychromecast", "pydeconz", "pydispatcher", "pyheos", "pyhomematic", + "pyHS100", "pyiqvia", "pylinky", "pylitejet", + "pyMetno", "pymfy", "pymonoprice", + "PyNaCl", "pynws", "pynx584", "pyopenuv", "pyotp", "pyps4-homeassistant", + "pyqwikswitch", + "PyRMVtransport", "pysma", "pysmartapp", "pysmartthings", "pysonos", - "pyqwikswitch", - "PyRMVtransport", - "PyTransportNSW", "pyspcwebgw", + "python_awair", "python-forecastio", "python-nest", - "python_awair", "python-velbus", + "pythonwhois", "pytradfri[async]", + "PyTransportNSW", "pyunifi", "pyupnp-async", "pyvesync", "pywebpush", - "pyHS100", - "PyNaCl", "regenmaschine", "restrictedpython", "rflink", "ring_doorbell", + "ruamel.yaml", "rxv", "simplisafe-python", "sleepyq", @@ -166,16 +171,12 @@ "twentemilieu", "uvcclient", "vsure", - "warrant", - "pythonwhois", - "wakeonlan", "vultr", + "wakeonlan", + "warrant", "YesssSMS", - "ruamel.yaml", "zeroconf", "zigpy-homeassistant", - "bellows-homeassistant", - "py17track", ) IGNORE_PIN = ("colorlog>2.1,<3", "keyring>=9.3,<10.0", "urllib3") diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 7995ba8f7817e0..8f33709fb2d1a4 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -22,12 +22,16 @@ @pytest.fixture(autouse=True) def cast_mock(): """Mock pychromecast.""" - with patch.dict( - "sys.modules", - { - "pychromecast": MagicMock(), - "pychromecast.controllers.multizone": MagicMock(), - }, + pycast_mock = MagicMock() + + with patch( + "homeassistant.components.cast.media_player.pychromecast", pycast_mock + ), patch( + "homeassistant.components.cast.discovery.pychromecast", pycast_mock + ), patch( + "homeassistant.components.cast.helpers.dial", MagicMock() + ), patch( + "homeassistant.components.cast.media_player.MultizoneManager", MagicMock() ): yield @@ -73,7 +77,8 @@ async def async_setup_cast_internal_discovery(hass, config=None, discovery_info= browser = MagicMock(zc={}) with patch( - "pychromecast.start_discovery", return_value=(listener, browser) + "homeassistant.components.cast.discovery.pychromecast.start_discovery", + return_value=(listener, browser), ) as start_discovery: add_entities = await async_setup_cast(hass, config, discovery_info) await hass.async_block_till_done() @@ -104,7 +109,8 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas cast.CastStatusListener = MagicMock() with patch( - "pychromecast._get_chromecast_from_host", return_value=chromecast + "homeassistant.components.cast.discovery.pychromecast._get_chromecast_from_host", + return_value=chromecast, ) as get_chromecast: await async_setup_component( hass, @@ -122,7 +128,8 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas def test_start_discovery_called_once(hass): """Test pychromecast.start_discovery called exactly once.""" with patch( - "pychromecast.start_discovery", return_value=(None, None) + "homeassistant.components.cast.discovery.pychromecast.start_discovery", + return_value=(None, None), ) as start_discovery: yield from async_setup_cast(hass) @@ -138,14 +145,17 @@ def test_stop_discovery_called_on_stop(hass): browser = MagicMock(zc={}) with patch( - "pychromecast.start_discovery", return_value=(None, browser) + "homeassistant.components.cast.discovery.pychromecast.start_discovery", + return_value=(None, browser), ) as start_discovery: # start_discovery should be called with empty config yield from async_setup_cast(hass, {}) assert start_discovery.call_count == 1 - with patch("pychromecast.stop_discovery") as stop_discovery: + with patch( + "homeassistant.components.cast.discovery.pychromecast.stop_discovery" + ) as stop_discovery: # stop discovery should be called on shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) yield from hass.async_block_till_done() @@ -153,7 +163,8 @@ def test_stop_discovery_called_on_stop(hass): stop_discovery.assert_called_once_with(browser) with patch( - "pychromecast.start_discovery", return_value=(None, browser) + "homeassistant.components.cast.discovery.pychromecast.start_discovery", + return_value=(None, browser), ) as start_discovery: # start_discovery should be called again on re-startup yield from async_setup_cast(hass) @@ -173,7 +184,10 @@ async def test_internal_discovery_callback_fill_out(hass): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - with patch("pychromecast.dial.get_device_status", return_value=full_info): + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ): signal = MagicMock() async_dispatcher_connect(hass, "cast_discovered", signal) @@ -210,7 +224,7 @@ async def test_normal_chromecast_not_starting_discovery(hass): """Test cast platform not starting discovery when not required.""" # pylint: disable=no-member with patch( - "homeassistant.components.cast.media_player." "_setup_internal_discovery" + "homeassistant.components.cast.media_player.setup_internal_discovery" ) as setup_discovery: # normal (non-group) chromecast shouldn't start discovery. add_entities = await async_setup_cast(hass, {"host": "host1"}) @@ -275,7 +289,10 @@ async def test_entity_media_states(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - with patch("pychromecast.dial.get_device_status", return_value=full_info): + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ): chromecast, entity = await async_setup_media_player_cast(hass, info) entity._available = True @@ -330,7 +347,10 @@ async def test_group_media_states(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - with patch("pychromecast.dial.get_device_status", return_value=full_info): + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ): chromecast, entity = await async_setup_media_player_cast(hass, info) entity._available = True @@ -377,7 +397,10 @@ async def test_dynamic_group_media_states(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - with patch("pychromecast.dial.get_device_status", return_value=full_info): + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ): chromecast, entity = await async_setup_media_player_cast(hass, info) entity._available = True @@ -426,12 +449,14 @@ async def test_group_media_control(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - with patch("pychromecast.dial.get_device_status", return_value=full_info): + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ): chromecast, entity = await async_setup_media_player_cast(hass, info) entity._available = True - entity.schedule_update_ha_state() - await hass.async_block_till_done() + entity.async_write_ha_state() state = hass.states.get("media_player.speaker") assert state is not None @@ -480,7 +505,10 @@ async def test_dynamic_group_media_control(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - with patch("pychromecast.dial.get_device_status", return_value=full_info): + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ): chromecast, entity = await async_setup_media_player_cast(hass, info) entity._available = True @@ -529,7 +557,10 @@ async def test_disconnect_on_stop(hass: HomeAssistantType): """Test cast device disconnects socket on stop.""" info = get_fake_chromecast_info() - with patch("pychromecast.dial.get_device_status", return_value=info): + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=info, + ): chromecast, _ = await async_setup_media_player_cast(hass, info) hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) From 6f27c5ae46f772dabeccd7b775b9b1c9e7df754c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 10 Sep 2019 13:07:55 -0700 Subject: [PATCH 0243/3953] Fix tests --- tests/helpers/test_translation.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index f4218fb1a7e52a..1c3748250a5fdb 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -95,30 +95,23 @@ async def test_get_translations(hass, mock_config_flows): assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}}) translations = await translation.async_get_translations(hass, "en") - assert translations == { - "component.switch.state.string1": "Value 1", - "component.switch.state.string2": "Value 2", - } + + assert translations["component.switch.state.string1"] == "Value 1" + assert translations["component.switch.state.string2"] == "Value 2" translations = await translation.async_get_translations(hass, "de") - assert translations == { - "component.switch.state.string1": "German Value 1", - "component.switch.state.string2": "German Value 2", - } + assert translations["component.switch.state.string1"] == "German Value 1" + assert translations["component.switch.state.string2"] == "German Value 2" # Test a partial translation translations = await translation.async_get_translations(hass, "es") - assert translations == { - "component.switch.state.string1": "Spanish Value 1", - "component.switch.state.string2": "Value 2", - } + assert translations["component.switch.state.string1"] == "Spanish Value 1" + assert translations["component.switch.state.string2"] == "Value 2" # Test that an untranslated language falls back to English. translations = await translation.async_get_translations(hass, "invalid-language") - assert translations == { - "component.switch.state.string1": "Value 1", - "component.switch.state.string2": "Value 2", - } + assert translations["component.switch.state.string1"] == "Value 1" + assert translations["component.switch.state.string2"] == "Value 2" async def test_get_translations_loads_config_flows(hass, mock_config_flows): From 4f3a2c0443c880e4547ea31c5d8345f9202788fa Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 10 Sep 2019 16:14:22 -0400 Subject: [PATCH 0244/3953] fix events for smartthings acceleration cluster (#26557) --- .../zha/core/channels/manufacturerspecific.py | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 6ed9de9b30313c..6b4d2e2c08c0a6 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -6,9 +6,17 @@ """ import logging +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send + from . import AttributeListeningChannel from .. import registries -from ..const import REPORT_CONFIG_ASAP, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT +from ..const import ( + REPORT_CONFIG_ASAP, + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_MIN_INT, + SIGNAL_ATTR_UPDATED, +) _LOGGER = logging.getLogger(__name__) @@ -38,3 +46,23 @@ class SmartThingsAcceleration(AttributeListeningChannel): {"attr": "y_axis", "config": REPORT_CONFIG_ASAP}, {"attr": "z_axis", "config": REPORT_CONFIG_ASAP}, ] + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == self.value_attribute: + async_dispatcher_send( + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value + ) + else: + self.zha_send_event( + self._cluster, + SIGNAL_ATTR_UPDATED, + { + "attribute_id": attrid, + "attribute_name": self._cluster.attributes.get(attrid, ["Unknown"])[ + 0 + ], + "value": value, + }, + ) From 1cea3a6abc9c94cbe8ae97b8781e1cf5d99ed87d Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 10 Sep 2019 16:44:41 -0400 Subject: [PATCH 0245/3953] osram cluster (#26555) --- .../zha/core/channels/manufacturerspecific.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 6b4d2e2c08c0a6..e15acdaf5e31bd 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -9,7 +9,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import AttributeListeningChannel +from . import AttributeListeningChannel, ZigbeeChannel from .. import registries from ..const import ( REPORT_CONFIG_ASAP, @@ -34,6 +34,14 @@ class SmartThingsHumidity(AttributeListeningChannel): ] +@registries.CHANNEL_ONLY_CLUSTERS.register(0xFD00) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFD00) +class OsramButton(ZigbeeChannel): + """Osram button channel.""" + + REPORT_CONFIG = [] + + @registries.ZIGBEE_CHANNEL_REGISTRY.register( registries.SMARTTHINGS_ACCELERATION_CLUSTER ) From bee566f89321086768ef4db910e2777f818e833f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 10 Sep 2019 23:23:27 +0200 Subject: [PATCH 0246/3953] Nuki less strict (#26542) --- homeassistant/components/nuki/lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 31a655dfeddd93..dc0ae1f22495db 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -146,7 +146,7 @@ def update(self): self._available = False return - self._available = self._nuki_lock.state != 255 + self._available = True self._name = self._nuki_lock.name self._locked = self._nuki_lock.is_locked self._battery_critical = self._nuki_lock.battery_critical From c680c07c659e76712dc810bc50e8b11ad0a94df5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 11 Sep 2019 01:56:28 +0200 Subject: [PATCH 0247/3953] deCONZ device automations (#26366) * Early draft * Getting there * Working fully with Hue dimmer remote * Fix Balloobs comments * No side effects in constructor * Improve hue dimmer * Add Ikea remote control * Add xiaomi button support * Refactor getting deconz event * Added xiaomi devices and tradfri wireless dimmer * Resolve unique id from device id * Add Hue Tap and Tradfri on off switch * More triggers for ikea on off switch and Aqara double wall switch * Add support for Tradfri open close remote * Fix changes after rebase * Initial test * Change id to event_id * Fix translations and add subtypes * Try if tests pass without the new tests * Revert disabling tests Add new exception InvalidDeviceAutomationConfig * Ignore places calling remotes * Enable all gateway tests * Found the issue, now to identify which test creates it * Remove block till done * See if device automation test passes in azure * Register event to device registry * Enable test sensors * Skip deconz event tests currently failing * Added reason why skipping tests --- .../components/automation/__init__.py | 8 +- .../components/deconz/.translations/en.json | 29 ++ .../components/deconz/deconz_device.py | 79 +++--- .../components/deconz/deconz_event.py | 56 ++++ .../components/deconz/device_automation.py | 254 ++++++++++++++++++ homeassistant/components/deconz/gateway.py | 40 +-- homeassistant/components/deconz/strings.json | 29 ++ .../device_automation/exceptions.py | 6 + .../deconz/test_device_automation.py | 138 ++++++++++ tests/components/deconz/test_gateway.py | 53 ++-- 10 files changed, 607 insertions(+), 85 deletions(-) create mode 100644 homeassistant/components/deconz/deconz_event.py create mode 100644 homeassistant/components/deconz/device_automation.py create mode 100644 homeassistant/components/device_automation/exceptions.py create mode 100644 tests/components/deconz/test_device_automation.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 3849188c6b3b9b..03eedd6d16213e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,6 +6,9 @@ import voluptuous as vol +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -467,7 +470,10 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): for conf in trigger_configs: platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) - remove = await platform.async_trigger(hass, conf, action, info) + try: + remove = await platform.async_trigger(hass, conf, action, info) + except InvalidDeviceAutomationConfig: + remove = False if not remove: _LOGGER.error("Error setting up trigger %s", name) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 272a6f5d1be2ed..afe70a30193f01 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -58,5 +58,34 @@ "description": "Configure visibility of deCONZ device types" } } + }, + "device_automation": { + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_gyro_activated": "Device shaken" + }, + "trigger_subtype": { + "turn_on": "Turn on", + "turn_off": "Turn off", + "dim_up": "Dim up", + "dim_down": "Dim down", + "left": "Left", + "right": "Right", + "open": "Open", + "close": "Close", + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index ad621db86ce730..e6249b2304cfee 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -7,8 +7,8 @@ from .const import DOMAIN as DECONZ_DOMAIN -class DeconzDevice(Entity): - """Representation of a deCONZ device.""" +class DeconzBase: + """Common base for deconz entities and events.""" def __init__(self, device, gateway): """Set up device and add update callback to get data from websocket.""" @@ -16,6 +16,47 @@ def __init__(self, device, gateway): self.gateway = gateway self.listeners = [] + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return self._device.uniqueid + + @property + def serial(self): + """Return a serial number for this device.""" + if self.unique_id is None or self.unique_id.count(":") != 7: + return None + + return self.unique_id.split("-", 1)[0] + + @property + def device_info(self): + """Return a device description for device registry.""" + if self.serial is None: + return None + + bridgeid = self.gateway.api.config.bridgeid + + return { + "connections": {(CONNECTION_ZIGBEE, self.serial)}, + "identifiers": {(DECONZ_DOMAIN, self.serial)}, + "manufacturer": self._device.manufacturer, + "model": self._device.modelid, + "name": self._device.name, + "sw_version": self._device.swversion, + "via_device": (DECONZ_DOMAIN, bridgeid), + } + + +class DeconzDevice(DeconzBase, Entity): + """Representation of a deCONZ device.""" + + def __init__(self, device, gateway): + """Set up device and add update callback to get data from websocket.""" + super().__init__(device, gateway) + + self.unsub_dispatcher = None + @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" @@ -54,41 +95,17 @@ def async_update_callback(self, force_update=False): """Update the device's state.""" self.async_schedule_update_ha_state() - @property - def name(self): - """Return the name of the device.""" - return self._device.name - - @property - def unique_id(self): - """Return a unique identifier for this device.""" - return self._device.uniqueid - @property def available(self): """Return True if device is available.""" return self.gateway.available and self._device.reachable + @property + def name(self): + """Return the name of the device.""" + return self._device.name + @property def should_poll(self): """No polling needed.""" return False - - @property - def device_info(self): - """Return a device description for device registry.""" - if self._device.uniqueid is None or self._device.uniqueid.count(":") != 7: - return None - - serial = self._device.uniqueid.split("-", 1)[0] - bridgeid = self.gateway.api.config.bridgeid - - return { - "connections": {(CONNECTION_ZIGBEE, serial)}, - "identifiers": {(DECONZ_DOMAIN, serial)}, - "manufacturer": self._device.manufacturer, - "model": self._device.modelid, - "name": self._device.name, - "sw_version": self._device.swversion, - "via_device": (DECONZ_DOMAIN, bridgeid), - } diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py new file mode 100644 index 00000000000000..f6c2d471bbf0a1 --- /dev/null +++ b/homeassistant/components/deconz/deconz_event.py @@ -0,0 +1,56 @@ +"""Representation of a deCONZ remote.""" +from homeassistant.const import CONF_EVENT, CONF_ID +from homeassistant.core import callback +from homeassistant.util import slugify + +from .const import _LOGGER +from .deconz_device import DeconzBase + +CONF_DECONZ_EVENT = "deconz_event" +CONF_UNIQUE_ID = "unique_id" + + +class DeconzEvent(DeconzBase): + """When you want signals instead of entities. + + Stateless sensors such as remotes are expected to generate an event + instead of a sensor entity in hass. + """ + + def __init__(self, device, gateway): + """Register callback that will be used for signals.""" + super().__init__(device, gateway) + + self._device.register_async_callback(self.async_update_callback) + + self.device_id = None + self.event_id = slugify(self._device.name) + _LOGGER.debug("deCONZ event created: %s", self.event_id) + + @callback + def async_will_remove_from_hass(self) -> None: + """Disconnect event object when removed.""" + self._device.remove_callback(self.async_update_callback) + self._device = None + + @callback + def async_update_callback(self, force_update=False): + """Fire the event if reason is that state is updated.""" + if "state" in self._device.changed_keys: + data = { + CONF_ID: self.event_id, + CONF_UNIQUE_ID: self.serial, + CONF_EVENT: self._device.state, + } + self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) + + async def async_update_device_registry(self): + """Update device registry.""" + device_registry = ( + await self.gateway.hass.helpers.device_registry.async_get_registry() + ) + + entry = device_registry.async_get_or_create( + config_entry_id=self.gateway.config_entry.entry_id, **self.device_info + ) + self.device_id = entry.id diff --git a/homeassistant/components/deconz/device_automation.py b/homeassistant/components/deconz/device_automation.py new file mode 100644 index 00000000000000..28f36b8f431ea8 --- /dev/null +++ b/homeassistant/components/deconz/device_automation.py @@ -0,0 +1,254 @@ +"""Provides device automations for deconz events.""" +import voluptuous as vol + +import homeassistant.components.automation.event as event + +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_EVENT, + CONF_PLATFORM, + CONF_TYPE, +) + +from . import DOMAIN +from .config_flow import configured_gateways +from .deconz_event import CONF_DECONZ_EVENT, CONF_UNIQUE_ID +from .gateway import get_gateway_from_config_entry + +CONF_SUBTYPE = "subtype" + +CONF_SHORT_PRESS = "remote_button_short_press" +CONF_SHORT_RELEASE = "remote_button_short_release" +CONF_LONG_PRESS = "remote_button_long_press" +CONF_LONG_RELEASE = "remote_button_long_release" +CONF_DOUBLE_PRESS = "remote_button_double_press" +CONF_TRIPLE_PRESS = "remote_button_triple_press" +CONF_QUADRUPLE_PRESS = "remote_button_quadruple_press" +CONF_QUINTUPLE_PRESS = "remote_button_quintuple_press" +CONF_ROTATED = "remote_button_rotated" +CONF_SHAKE = "remote_gyro_activated" + +CONF_TURN_ON = "turn_on" +CONF_TURN_OFF = "turn_off" +CONF_DIM_UP = "dim_up" +CONF_DIM_DOWN = "dim_down" +CONF_LEFT = "left" +CONF_RIGHT = "right" +CONF_OPEN = "open" +CONF_CLOSE = "close" +CONF_BOTH_BUTTONS = "both_buttons" +CONF_BUTTON_1 = "button_1" +CONF_BUTTON_2 = "button_2" +CONF_BUTTON_3 = "button_3" +CONF_BUTTON_4 = "button_4" + +HUE_DIMMER_REMOTE_MODEL = "RWL021" +HUE_DIMMER_REMOTE = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1000, + (CONF_SHORT_RELEASE, CONF_TURN_ON): 1002, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_LONG_RELEASE, CONF_TURN_ON): 1003, + (CONF_SHORT_PRESS, CONF_DIM_UP): 2000, + (CONF_SHORT_RELEASE, CONF_DIM_UP): 2002, + (CONF_LONG_PRESS, CONF_DIM_UP): 2001, + (CONF_LONG_RELEASE, CONF_DIM_UP): 2003, + (CONF_SHORT_PRESS, CONF_DIM_DOWN): 3000, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): 3002, + (CONF_LONG_PRESS, CONF_DIM_DOWN): 3001, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): 3003, + (CONF_SHORT_PRESS, CONF_TURN_OFF): 4000, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): 4002, + (CONF_LONG_PRESS, CONF_TURN_OFF): 4001, + (CONF_LONG_RELEASE, CONF_TURN_OFF): 4003, +} + +HUE_TAP_REMOTE_MODEL = "ZGPSWITCH" +HUE_TAP_REMOTE = { + (CONF_SHORT_PRESS, CONF_BUTTON_1): 34, + (CONF_SHORT_PRESS, CONF_BUTTON_2): 16, + (CONF_SHORT_PRESS, CONF_BUTTON_3): 17, + (CONF_SHORT_PRESS, CONF_BUTTON_4): 18, +} + +TRADFRI_ON_OFF_SWITCH_MODEL = "TRADFRI on/off switch" +TRADFRI_ON_OFF_SWITCH = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_LONG_RELEASE, CONF_TURN_ON): 1003, + (CONF_SHORT_PRESS, CONF_TURN_OFF): 2002, + (CONF_LONG_PRESS, CONF_TURN_OFF): 2001, + (CONF_LONG_RELEASE, CONF_TURN_OFF): 2003, +} + +TRADFRI_OPEN_CLOSE_REMOTE_MODEL = "TRADFRI open/close remote" +TRADFRI_OPEN_CLOSE_REMOTE = { + (CONF_SHORT_PRESS, CONF_OPEN): 1002, + (CONF_LONG_PRESS, CONF_OPEN): 1003, + (CONF_SHORT_PRESS, CONF_CLOSE): 2002, + (CONF_LONG_PRESS, CONF_CLOSE): 2003, +} + +TRADFRI_REMOTE_MODEL = "TRADFRI remote control" +TRADFRI_REMOTE = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_SHORT_PRESS, CONF_DIM_UP): 2002, + (CONF_LONG_PRESS, CONF_DIM_UP): 2001, + (CONF_LONG_RELEASE, CONF_DIM_UP): 2003, + (CONF_SHORT_PRESS, CONF_DIM_DOWN): 3002, + (CONF_LONG_PRESS, CONF_DIM_DOWN): 3001, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): 3003, + (CONF_SHORT_PRESS, CONF_LEFT): 4002, + (CONF_LONG_PRESS, CONF_LEFT): 4001, + (CONF_LONG_RELEASE, CONF_LEFT): 4003, + (CONF_SHORT_PRESS, CONF_RIGHT): 5002, + (CONF_LONG_PRESS, CONF_RIGHT): 5001, + (CONF_LONG_RELEASE, CONF_RIGHT): 5003, +} + +TRADFRI_WIRELESS_DIMMER_MODEL = "TRADFRI wireless dimmer" +TRADFRI_WIRELESS_DIMMER = { + (CONF_ROTATED, CONF_LEFT): 3002, + (CONF_ROTATED, CONF_RIGHT): 2002, +} + +AQARA_DOUBLE_WALL_SWITCH_MODEL = "lumi.remote.b286acn01" +AQARA_DOUBLE_WALL_SWITCH = { + (CONF_SHORT_PRESS, CONF_LEFT): 1002, + (CONF_LONG_PRESS, CONF_LEFT): 1001, + (CONF_DOUBLE_PRESS, CONF_LEFT): 1004, + (CONF_SHORT_PRESS, CONF_RIGHT): 2002, + (CONF_LONG_PRESS, CONF_RIGHT): 2001, + (CONF_DOUBLE_PRESS, CONF_RIGHT): 2004, + (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): 3002, + (CONF_LONG_PRESS, CONF_BOTH_BUTTONS): 3001, + (CONF_DOUBLE_PRESS, CONF_BOTH_BUTTONS): 3004, +} + +AQARA_MINI_SWITCH_MODEL = "lumi.remote.b1acn01" +AQARA_MINI_SWITCH = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_LONG_RELEASE, CONF_TURN_ON): 1003, +} + +AQARA_ROUND_SWITCH_MODEL = "lumi.sensor_switch" +AQARA_ROUND_SWITCH = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1000, + (CONF_SHORT_RELEASE, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): 1005, + (CONF_QUADRUPLE_PRESS, CONF_TURN_ON): 1006, + (CONF_QUINTUPLE_PRESS, CONF_TURN_ON): 1010, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_LONG_RELEASE, CONF_TURN_ON): 1003, +} + +AQARA_SQUARE_SWITCH_MODEL = "lumi.sensor_switch.aq3" +AQARA_SQUARE_SWITCH = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_LONG_RELEASE, CONF_TURN_ON): 1003, + (CONF_SHAKE, ""): 1007, +} + +REMOTES = { + HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, + HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, + TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH, + TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE, + TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE, + TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER, + AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, + AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, + AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH, + AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, +} + +TRIGGER_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_PLATFORM): "device", + vol.Required(CONF_TYPE): str, + vol.Required(CONF_SUBTYPE): str, + } + ) +) + + +def _get_deconz_event_from_device_id(hass, device_id): + """Resolve deconz event from device id.""" + deconz_config_entries = configured_gateways(hass) + for config_entry in deconz_config_entries.values(): + + gateway = get_gateway_from_config_entry(hass, config_entry) + for deconz_event in gateway.events: + + if device_id == deconz_event.device_id: + return deconz_event + + return None + + +async def async_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(config[CONF_DEVICE_ID]) + + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + + if device.model not in REMOTES and trigger not in REMOTES[device.model]: + raise InvalidDeviceAutomationConfig + + trigger = REMOTES[device.model][trigger] + + deconz_event = _get_deconz_event_from_device_id(hass, device.id) + if deconz_event is None: + raise InvalidDeviceAutomationConfig + + event_id = deconz_event.serial + + state_config = { + event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, + event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, + } + + return await event.async_trigger(hass, state_config, action, automation_info) + + +async def async_get_triggers(hass, device_id): + """List device triggers. + + Make sure device is a supported remote model. + Retrieve the deconz event object matching device entry. + Generate device trigger list. + """ + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(device_id) + + if device.model not in REMOTES: + return + + triggers = [] + for trigger, subtype in REMOTES[device.model].keys(): + triggers.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 73cdeb74884b4e..35cf63fc3d228a 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -6,8 +6,8 @@ from pydeconz.sensor import Switch from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.const import CONF_EVENT, CONF_HOST, CONF_ID -from homeassistant.core import EventOrigin, callback +from homeassistant.const import CONF_HOST +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import ( @@ -18,7 +18,6 @@ async_get_registry, DISABLED_CONFIG_ENTRY, ) -from homeassistant.util import slugify from .const import ( _LOGGER, @@ -33,6 +32,7 @@ NEW_SENSOR, SUPPORTED_PLATFORMS, ) +from .deconz_event import DeconzEvent from .errors import AuthenticationRequired, CannotConnect @@ -192,7 +192,9 @@ def async_add_remote(self, sensors): if sensor.type in Switch.ZHATYPE and not ( not self.option_allow_clip_sensor and sensor.type.startswith("CLIP") ): - self.events.append(DeconzEvent(self.hass, sensor)) + event = DeconzEvent(sensor, self) + self.hass.async_create_task(event.async_update_device_registry()) + self.events.append(event) @callback def shutdown(self, event): @@ -288,33 +290,3 @@ def update_entity_registry(self, entity_registry): entity_registry.async_update_entity( entity.registry_entry.entity_id, disabled_by=disabled_by ) - - -class DeconzEvent: - """When you want signals instead of entities. - - Stateless sensors such as remotes are expected to generate an event - instead of a sensor entity in hass. - """ - - def __init__(self, hass, device): - """Register callback that will be used for signals.""" - self._hass = hass - self._device = device - self._device.register_async_callback(self.async_update_callback) - self._event = f"deconz_{CONF_EVENT}" - self._id = slugify(self._device.name) - _LOGGER.debug("deCONZ event created: %s", self._id) - - @callback - def async_will_remove_from_hass(self) -> None: - """Disconnect event object when removed.""" - self._device.remove_callback(self.async_update_callback) - self._device = None - - @callback - def async_update_callback(self, force_update=False): - """Fire the event if reason is that state is updated.""" - if "state" in self._device.changed_keys: - data = {CONF_ID: self._id, CONF_EVENT: self._device.state} - self._hass.bus.async_fire(self._event, data, EventOrigin.remote) diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 7081f816e6ae08..00aa463349cf68 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -51,5 +51,34 @@ } } } + }, + "device_automation": { + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_gyro_activated": "Device shaken" + }, + "trigger_subtype": { + "turn_on": "Turn on", + "turn_off": "Turn off", + "dim_up": "Dim up", + "dim_down": "Dim down", + "left": "Left", + "right": "Right", + "open": "Open", + "close": "Close", + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button" + } } } diff --git a/homeassistant/components/device_automation/exceptions.py b/homeassistant/components/device_automation/exceptions.py new file mode 100644 index 00000000000000..2f7c0df01876f7 --- /dev/null +++ b/homeassistant/components/device_automation/exceptions.py @@ -0,0 +1,6 @@ +"""Device automation exceptions.""" +from homeassistant.exceptions import HomeAssistantError + + +class InvalidDeviceAutomationConfig(HomeAssistantError): + """When device automation config is invalid.""" diff --git a/tests/components/deconz/test_device_automation.py b/tests/components/deconz/test_device_automation.py new file mode 100644 index 00000000000000..0be566d4b52e43 --- /dev/null +++ b/tests/components/deconz/test_device_automation.py @@ -0,0 +1,138 @@ +"""deCONZ device automation tests.""" +from asynctest import patch + +from homeassistant import config_entries +from homeassistant.components import deconz +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) + +BRIDGEID = "0123456789" + +ENTRY_CONFIG = { + deconz.config_flow.CONF_API_KEY: "ABCDEF", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, + deconz.config_flow.CONF_HOST: "1.2.3.4", + deconz.config_flow.CONF_PORT: 80, +} + +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, +} + +DECONZ_SENSOR = { + "1": { + "config": { + "alert": "none", + "battery": 60, + "group": "10", + "on": True, + "reachable": True, + }, + "ep": 1, + "etag": "1b355c0b6d2af28febd7ca9165881952", + "manufacturername": "IKEA of Sweden", + "mode": 1, + "modelid": "TRADFRI on/off switch", + "name": "TRADFRI on/off switch ", + "state": {"buttonevent": 2002, "lastupdated": "2019-09-07T07:39:39"}, + "swversion": "1.4.018", + "type": "ZHASwitch", + "uniqueid": "d0:cf:5e:ff:fe:71:a4:3a-01-1000", + } +} + +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG, "sensors": DECONZ_SENSOR} + + +def _same_lists(a, b): + if len(a) != len(b): + return False + + for d in a: + if d not in b: + return False + return True + + +async def setup_deconz(hass, options): + """Create the deCONZ gateway.""" + config_entry = config_entries.ConfigEntry( + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, + options=options, + entry_id="1", + ) + + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=DECONZ_WEB_REQUEST + ): + await deconz.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][BRIDGEID] + + +async def test_get_triggers(hass): + """Test triggers work.""" + gateway = await setup_deconz(hass, options={}) + device_id = gateway.events[0].device_id + triggers = await async_get_device_automations(hass, "async_get_triggers", device_id) + + expected_triggers = [ + { + "device_id": device_id, + "domain": "deconz", + "platform": "device", + "type": deconz.device_automation.CONF_SHORT_PRESS, + "subtype": deconz.device_automation.CONF_TURN_ON, + }, + { + "device_id": device_id, + "domain": "deconz", + "platform": "device", + "type": deconz.device_automation.CONF_LONG_PRESS, + "subtype": deconz.device_automation.CONF_TURN_ON, + }, + { + "device_id": device_id, + "domain": "deconz", + "platform": "device", + "type": deconz.device_automation.CONF_LONG_RELEASE, + "subtype": deconz.device_automation.CONF_TURN_ON, + }, + { + "device_id": device_id, + "domain": "deconz", + "platform": "device", + "type": deconz.device_automation.CONF_SHORT_PRESS, + "subtype": deconz.device_automation.CONF_TURN_OFF, + }, + { + "device_id": device_id, + "domain": "deconz", + "platform": "device", + "type": deconz.device_automation.CONF_LONG_PRESS, + "subtype": deconz.device_automation.CONF_TURN_OFF, + }, + { + "device_id": device_id, + "domain": "deconz", + "platform": "device", + "type": deconz.device_automation.CONF_LONG_RELEASE, + "subtype": deconz.device_automation.CONF_TURN_OFF, + }, + ] + + assert _same_lists(triggers, expected_triggers) diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 3750d14cd342a0..c17aa0b66390ca 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -126,9 +126,9 @@ async def test_add_device(hass): assert len(mock_dispatch_send.mock_calls[0]) == 3 -async def test_add_remote(): +@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") +async def test_add_remote(hass): """Successful add remote.""" - hass = Mock() entry = Mock() entry.data = ENTRY_CONFIG @@ -139,6 +139,7 @@ async def test_add_remote(): deconz_gateway = gateway.DeconzGateway(hass, entry) deconz_gateway.async_add_remote([remote]) + await hass.async_block_till_done() assert len(deconz_gateway.events) == 1 @@ -219,37 +220,51 @@ async def test_get_gateway_fails_cannot_connect(hass): assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False -async def test_create_event(): +@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") +async def test_create_event(hass): """Successfully created a deCONZ event.""" - hass = Mock() - remote = Mock() - remote.name = "Name" + mock_remote = Mock() + mock_remote.name = "Name" - event = gateway.DeconzEvent(hass, remote) + mock_gateway = Mock() + mock_gateway.hass = hass - assert event._id == "name" + event = gateway.DeconzEvent(mock_remote, mock_gateway) + await hass.async_block_till_done() + assert event.event_id == "name" -async def test_update_event(): + +@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") +async def test_update_event(hass): """Successfully update a deCONZ event.""" - hass = Mock() - remote = Mock() - remote.name = "Name" + hass.bus.async_fire = Mock() - event = gateway.DeconzEvent(hass, remote) - remote.changed_keys = {"state": True} + mock_remote = Mock() + mock_remote.name = "Name" + + mock_gateway = Mock() + mock_gateway.hass = hass + + event = gateway.DeconzEvent(mock_remote, mock_gateway) + await hass.async_block_till_done() + mock_remote.changed_keys = {"state": True} event.async_update_callback() assert len(hass.bus.async_fire.mock_calls) == 1 -async def test_remove_event(): +@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") +async def test_remove_event(hass): """Successfully update a deCONZ event.""" - hass = Mock() - remote = Mock() - remote.name = "Name" + mock_remote = Mock() + mock_remote.name = "Name" + + mock_gateway = Mock() + mock_gateway.hass = hass - event = gateway.DeconzEvent(hass, remote) + event = gateway.DeconzEvent(mock_remote, mock_gateway) + await hass.async_block_till_done() event.async_will_remove_from_hass() assert event._device is None From 53a3f2e83d88e15575e05a3b850415e7d3436bfe Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 11 Sep 2019 00:33:50 +0000 Subject: [PATCH 0248/3953] [ci skip] Translation update --- .../components/deconz/.translations/en.json | 58 +++++++++---------- .../iaqualink/.translations/nl.json | 21 +++++++ .../solaredge/.translations/nl.json | 21 +++++++ .../components/switch/.translations/ca.json | 17 ++++++ .../components/switch/.translations/nl.json | 17 ++++++ .../components/switch/.translations/pl.json | 17 ++++++ .../components/unifi/.translations/it.json | 6 ++ .../components/withings/.translations/da.json | 1 + 8 files changed, 129 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/iaqualink/.translations/nl.json create mode 100644 homeassistant/components/solaredge/.translations/nl.json create mode 100644 homeassistant/components/switch/.translations/ca.json create mode 100644 homeassistant/components/switch/.translations/nl.json create mode 100644 homeassistant/components/switch/.translations/pl.json diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index afe70a30193f01..ead71db8c27d89 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee gateway" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "close": "Close", + "dim_down": "Dim down", + "dim_up": "Dim up", + "left": "Left", + "open": "Open", + "right": "Right", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_gyro_activated": "Device shaken" + } + }, "options": { "step": { "async_step_deconz_devices": { @@ -58,34 +87,5 @@ "description": "Configure visibility of deCONZ device types" } } - }, - "device_automation": { - "trigger_type": { - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "remote_button_rotated": "Button rotated \"{subtype}\"", - "remote_gyro_activated": "Device shaken" - }, - "trigger_subtype": { - "turn_on": "Turn on", - "turn_off": "Turn off", - "dim_up": "Dim up", - "dim_down": "Dim down", - "left": "Left", - "right": "Right", - "open": "Open", - "close": "Close", - "both_buttons": "Both buttons", - "button_1": "First button", - "button_2": "Second button", - "button_3": "Third button", - "button_4": "Fourth button" - } } } \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/nl.json b/homeassistant/components/iaqualink/.translations/nl.json new file mode 100644 index 00000000000000..c0a515bb741e00 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "U kunt slechts \u00e9\u00e9n iAqualink-verbinding configureren." + }, + "error": { + "connection_failure": "Kan geen verbinding maken met iAqualink. Controleer je gebruikersnaam en wachtwoord." + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam/E-mailadres" + }, + "description": "Voer de gebruikersnaam en het wachtwoord voor uw iAqualink-account in.", + "title": "Verbinding maken met iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/nl.json b/homeassistant/components/solaredge/.translations/nl.json new file mode 100644 index 00000000000000..3cc52b43a63126 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Deze site_id is al geconfigureerd" + }, + "error": { + "site_exists": "Deze site_id is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "api_key": "De API-sleutel voor deze site", + "name": "De naam van deze installatie", + "site_id": "De SolarEdge site-id" + }, + "title": "Definieer de API-parameters voor deze installatie" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ca.json b/homeassistant/components/switch/.translations/ca.json new file mode 100644 index 00000000000000..c97565ddfe6a7e --- /dev/null +++ b/homeassistant/components/switch/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Commuta {entity_name}", + "turn_off": "Desactiva {entity_name}", + "turn_on": "Activa {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} desactivat", + "turn_on": "{entity_name} activat" + }, + "trigger_type": { + "turn_off": "{entity_name} desactivat", + "turn_on": "{entity_name} activat" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/nl.json b/homeassistant/components/switch/.translations/nl.json new file mode 100644 index 00000000000000..1d8355d21581bf --- /dev/null +++ b/homeassistant/components/switch/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Omschakelen {entity_name}", + "turn_off": "Zet {entity_name} uit.", + "turn_on": "Zet {entity_name} aan." + }, + "condition_type": { + "turn_off": "{entity_name} uitgeschakeld", + "turn_on": "{entity_name} ingeschakeld" + }, + "trigger_type": { + "turn_off": "{entity_name} uitgeschakeld", + "turn_on": "{entity_name} ingeschakeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json new file mode 100644 index 00000000000000..f564d1424eac61 --- /dev/null +++ b/homeassistant/components/switch/.translations/pl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Prze\u0142\u0105cz {entity_name}", + "turn_off": "Wy\u0142\u0105cz {entity_name}", + "turn_on": "W\u0142\u0105cz {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} wy\u0142\u0105czone", + "turn_on": "{entity_name} w\u0142\u0105czone" + }, + "trigger_type": { + "turn_off": "{entity_name} wy\u0142\u0105czone", + "turn_on": "{entity_name} w\u0142\u0105czone" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json index 499c77bfb69c23..5285ed21873529 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -32,6 +32,12 @@ "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)", "track_wired_clients": "Includi i client di rete cablata" } + }, + "init": { + "data": { + "one": "uno", + "other": "altro" + } } } } diff --git a/homeassistant/components/withings/.translations/da.json b/homeassistant/components/withings/.translations/da.json index 8b7fbcb3bb4a0f..d2dddbbd204bfb 100644 --- a/homeassistant/components/withings/.translations/da.json +++ b/homeassistant/components/withings/.translations/da.json @@ -8,6 +8,7 @@ "data": { "profile": "Profil" }, + "description": "V\u00e6lg en brugerprofil, som du vil have Home Assistant til at tilknytte med en Withings-profil. P\u00e5 siden Withings skal du s\u00f8rge for at v\u00e6lge den samme bruger eller data vil ikke blive m\u00e6rket korrekt.", "title": "Brugerprofil." } }, From 02466ed8abae978af83d3720d5088092e2d7f767 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 11 Sep 2019 08:28:57 +0200 Subject: [PATCH 0249/3953] Ignore test output --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5389954ca59578..15f0896975da36 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ nosetests.xml htmlcov/ test-reports/ test-results.xml +test-output.xml # Translations *.mo From 702a524b5597885e91dacbc8f50ea8d0607e37fc Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 11 Sep 2019 11:20:21 +0200 Subject: [PATCH 0250/3953] Improve startup of devcontainer (#26572) --- .devcontainer/devcontainer.json | 2 +- .vscode/tasks.json | 15 +++++++++++++++ Dockerfile.dev | 6 +++--- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a025a52e849c1c..e78a8e6851c728 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ "name": "Home Assistant Dev", "context": "..", "dockerFile": "../Dockerfile.dev", - "postCreateCommand": "pip3 install -e .", + "postCreateCommand": "mkdir -p config && pip3 install -e .", "appPort": 8123, "runArgs": ["-e", "GIT_EDITOR=\"code --wait\""], "extensions": [ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f57c182809b93e..151868a1663981 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -19,6 +19,7 @@ "label": "Pytest", "type": "shell", "command": "pytest --timeout=10 tests", + "dependsOn": ["Install all Test Requirements"], "group": { "kind": "test", "isDefault": true @@ -85,6 +86,20 @@ "panel": "new" }, "problemMatcher": [] + }, + { + "label": "Install all Test Requirements", + "type": "shell", + "command": "pip3 install -r requirements_test_all.txt -c homeassistant/package_constraints.txt", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] } ] } diff --git a/Dockerfile.dev b/Dockerfile.dev index 457dc7b3d0fcd2..eb76fe5b16b038 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -24,9 +24,9 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ WORKDIR /workspaces # Install Python dependencies from requirements -COPY requirements_test_all.txt homeassistant/package_constraints.txt ./ -RUN pip3 install -r requirements_test_all.txt -c package_constraints.txt \ - && rm -f requirements_test_all.txt package_constraints.txt +COPY requirements_test.txt homeassistant/package_constraints.txt ./ +RUN pip3 install -r requirements_test.txt -c package_constraints.txt \ + && rm -f requirements_test.txt package_constraints.txt # Set the default shell to bash instead of sh ENV SHELL /bin/bash From e6ecabd6e1c4544d59e414ad0e77cd77a92d5686 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 11 Sep 2019 11:33:35 +0200 Subject: [PATCH 0251/3953] Cleanup stale script stuff (#26573) --- script/dev_docker | 33 --------------------------------- script/dev_openzwave_docker | 16 ---------------- script/lint_docker | 13 ------------- script/test_docker | 16 ---------------- script/translations_upload | 2 +- script/travis_deploy | 11 ----------- 6 files changed, 1 insertion(+), 90 deletions(-) delete mode 100755 script/dev_docker delete mode 100755 script/dev_openzwave_docker delete mode 100755 script/lint_docker delete mode 100755 script/test_docker delete mode 100755 script/travis_deploy diff --git a/script/dev_docker b/script/dev_docker deleted file mode 100755 index 514fce734777e9..00000000000000 --- a/script/dev_docker +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh -# Build and run Home Assinstant in Docker. - -# Optional: pass in a timezone as first argument -# If not given will attempt to mount /etc/localtime - -# Stop on errors -set -e - -cd "$(dirname "$0")/.." - -docker build -t home-assistant-dev -f virtualization/Docker/Dockerfile.dev . - -if [ $# -gt 0 ] -then - docker run \ - --net=host \ - --device=/dev/ttyUSB0:/zwaveusbstick:rwm \ - -e "TZ=$1" \ - -v `pwd`:/usr/src/app \ - -v `pwd`/config:/config \ - -t -i home-assistant-dev - -else - docker run \ - --net=host \ - -v /etc/localtime:/etc/localtime:ro \ - -v `pwd`:/usr/src/app \ - -v `pwd`/config:/config \ - --rm \ - -t -i home-assistant-dev - -fi diff --git a/script/dev_openzwave_docker b/script/dev_openzwave_docker deleted file mode 100755 index 7304995f3e18eb..00000000000000 --- a/script/dev_openzwave_docker +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -# Open a docker that can be used to debug/dev python-openzwave. Pass in a command line argument to build - -cd "$(dirname "$0")/.." - -if [ $# -gt 0 ] -then - docker build -t home-assistant-dev . -fi - -docker run \ - --device=/dev/ttyUSB0:/zwaveusbstick:rwm \ - -v `pwd`:/usr/src/app \ - -p 8123:8123 \ - -t -i home-assistant-dev \ - /bin/bash diff --git a/script/lint_docker b/script/lint_docker deleted file mode 100755 index 7e6ff42e074c9e..00000000000000 --- a/script/lint_docker +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# Execute lint in a docker container to spot code mistakes. - -# Stop on errors -set -e - -cd "$(dirname "$0")/.." - -docker build -t home-assistant-test -f virtualization/Docker/Dockerfile.dev . -docker run --rm \ - -v `pwd`/.tox/:/usr/src/app/.tox/ \ - -t -i home-assistant-test \ - tox -e lint diff --git a/script/test_docker b/script/test_docker deleted file mode 100755 index bbea52a3a0bd61..00000000000000 --- a/script/test_docker +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -# Executes the tests with tox in a docker container. -# Every argument is passed to tox to allow running only a subset of tests. -# The following example will only run media_player tests: -# ./test_docker -- tests/components/media_player/ - -# Stop on errors -set -e - -cd "$(dirname "$0")/.." - -docker build -t home-assistant-test -f virtualization/Docker/Dockerfile.dev . -docker run --rm \ - -v `pwd`/.tox/:/usr/src/app/.tox/ \ - -t -i home-assistant-test \ - tox -e py36 ${@:2} diff --git a/script/translations_upload b/script/translations_upload index 22a2bbceba202f..fec8a3387c1dae 100755 --- a/script/translations_upload +++ b/script/translations_upload @@ -27,7 +27,7 @@ LANG_ISO=en CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) # Check Travis and Azure environment as well -if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${TRAVIS_BRANCH-}" != "dev" ] && [ "${AZURE_BRANCH-}" != "dev" ]; then +if [ "${CURRENT_BRANCH-}" != "dev" ] && [ "${AZURE_BRANCH-}" != "dev" ]; then echo "Please only run the translations upload script from a clean checkout of dev." exit 1 fi diff --git a/script/travis_deploy b/script/travis_deploy deleted file mode 100755 index 359f6a46077654..00000000000000 --- a/script/travis_deploy +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -# Safe bash settings -# -e Exit on command fail -# -u Exit on unset variable -# -o pipefail Exit if piped command has error code -set -eu -o pipefail - -cd "$(dirname "$0")/.." - -script/translations_upload From f3fa073045a39845e638aacf48e658d1f04e4801 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 11 Sep 2019 13:17:07 +0200 Subject: [PATCH 0252/3953] Bump UPC connect / fix auth issue (#26570) * Bump UPC connect / fix auth issue * Fix lint * Fix platform schema * Fix config value * Address comment / add session cleanup * Fix device handling --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/upc_connect/device_tracker.py | 133 ++++------- .../components/upc_connect/manifest.json | 6 +- requirements_all.txt | 4 +- requirements_test_all.txt | 1 - tests/components/upc_connect/__init__.py | 1 - .../upc_connect/test_device_tracker.py | 221 ------------------ 8 files changed, 47 insertions(+), 321 deletions(-) delete mode 100644 tests/components/upc_connect/__init__.py delete mode 100644 tests/components/upc_connect/test_device_tracker.py diff --git a/.coveragerc b/.coveragerc index e5848b8f4615ea..d635aea6c67c6a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -675,6 +675,7 @@ omit = homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/upcloud/* homeassistant/components/upnp/* + homeassistant/components/upc_connect/* homeassistant/components/ups/sensor.py homeassistant/components/uptimerobot/binary_sensor.py homeassistant/components/uscis/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index f82523d8c8d8ed..2a04189dfba8a9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -290,6 +290,7 @@ homeassistant/components/twentemilieu/* @frenck homeassistant/components/twilio_call/* @robbiet480 homeassistant/components/twilio_sms/* @robbiet480 homeassistant/components/unifi/* @kane610 +homeassistant/components/upc_connect/* @pvizeli homeassistant/components/upcloud/* @scop homeassistant/components/updater/* @home-assistant/core homeassistant/components/upnp/* @robbiet480 diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index 384b82d139564c..fc9225c6ef4256 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -1,10 +1,9 @@ """Support for UPC ConnectBox router.""" -import asyncio import logging +from typing import List, Optional -import aiohttp -from aiohttp.hdrs import REFERER, USER_AGENT -import async_timeout +from connect_box import ConnectBox +from connect_box.exceptions import ConnectBoxError, ConnectBoxLoginError import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -12,118 +11,66 @@ PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, HTTP_HEADER_X_REQUESTED_WITH -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CMD_DEVICES = 123 - DEFAULT_IP = "192.168.0.1" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string} + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string, + } ) async def async_get_scanner(hass, config): """Return the UPC device scanner.""" - scanner = UPCDeviceScanner(hass, config[DOMAIN]) - success_init = await scanner.async_initialize_token() + conf = config[DOMAIN] + session = hass.helpers.aiohttp_client.async_get_clientsession() + connect_box = ConnectBox(session, conf[CONF_PASSWORD], host=conf[CONF_HOST]) + + # Check login data + try: + await connect_box.async_initialize_token() + except ConnectBoxLoginError: + _LOGGER.error("ConnectBox login data error!") + return None + except ConnectBoxError: + pass + + async def _shutdown(event): + """Shutdown event.""" + await connect_box.async_close_session() - return scanner if success_init else None + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) + + return UPCDeviceScanner(connect_box) class UPCDeviceScanner(DeviceScanner): """This class queries a router running UPC ConnectBox firmware.""" - def __init__(self, hass, config): + def __init__(self, connect_box: ConnectBox): """Initialize the scanner.""" - self.hass = hass - self.host = config[CONF_HOST] - - self.data = {} - self.token = None + self.connect_box: ConnectBox = connect_box - self.headers = { - HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest", - REFERER: f"http://{self.host}/index.html", - USER_AGENT: ( - "Mozilla/5.0 (Windows NT 10.0; WOW64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/47.0.2526.106 Safari/537.36" - ), - } - - self.websession = async_get_clientsession(hass) - - async def async_scan_devices(self): + async def async_scan_devices(self) -> List[str]: """Scan for new devices and return a list with found device IDs.""" - import defusedxml.ElementTree as ET - - if self.token is None: - token_initialized = await self.async_initialize_token() - if not token_initialized: - _LOGGER.error("Not connected to %s", self.host) - return [] - - raw = await self._async_ws_function(CMD_DEVICES) - try: - xml_root = ET.fromstring(raw) - return [mac.text for mac in xml_root.iter("MACAddr")] - except (ET.ParseError, TypeError): - _LOGGER.warning("Can't read device from %s", self.host) - self.token = None + await self.connect_box.async_get_devices() + except ConnectBoxError: return [] - async def async_get_device_name(self, device): - """Get the device name (the name of the wireless device not used).""" - return None - - async def async_initialize_token(self): - """Get first token.""" - try: - # get first token - with async_timeout.timeout(10): - response = await self.websession.get( - f"http://{self.host}/common_page/login.html", headers=self.headers - ) - - await response.text() + return [device.mac for device in self.connect_box.devices] - self.token = response.cookies["sessionToken"].value - - return True - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Can not load login page from %s", self.host) - return False + async def async_get_device_name(self, device: str) -> Optional[str]: + """Get the device name (the name of the wireless device not used).""" + for connected_device in self.connect_box.devices: + if connected_device != device: + continue + return connected_device.hostname - async def _async_ws_function(self, function): - """Execute a command on UPC firmware webservice.""" - try: - with async_timeout.timeout(10): - # The 'token' parameter has to be first, and 'fun' second - # or the UPC firmware will return an error - response = await self.websession.post( - f"http://{self.host}/xml/getter.xml", - data=f"token={self.token}&fun={function}", - headers=self.headers, - allow_redirects=False, - ) - - # Error? - if response.status != 200: - _LOGGER.warning("Receive http code %d", response.status) - self.token = None - return - - # Load data, store token for next request - self.token = response.cookies["sessionToken"].value - return await response.text() - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Error on %s", function) - self.token = None + return None diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 36a06ac3204223..53bd7fc5820a5b 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -2,9 +2,7 @@ "domain": "upc_connect", "name": "Upc connect", "documentation": "https://www.home-assistant.io/components/upc_connect", - "requirements": [ - "defusedxml==0.6.0" - ], + "requirements": ["connect-box==0.2.3"], "dependencies": [], - "codeowners": [] + "codeowners": ["@pvizeli"] } diff --git a/requirements_all.txt b/requirements_all.txt index a5670313692bbf..d1b03636bab57c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -354,6 +354,9 @@ colorlog==4.0.2 # homeassistant.components.concord232 concord232==0.15 +# homeassistant.components.upc_connect +connect-box==0.2.3 + # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart # homeassistant.components.xiaomi_miio @@ -380,7 +383,6 @@ datapoint==0.4.3 # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect -# homeassistant.components.upc_connect defusedxml==0.6.0 # homeassistant.components.deluge diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1fc8ba3ebfe799..f54886c4957889 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -105,7 +105,6 @@ coinmarketcap==5.0.3 # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect -# homeassistant.components.upc_connect defusedxml==0.6.0 # homeassistant.components.dsmr diff --git a/tests/components/upc_connect/__init__.py b/tests/components/upc_connect/__init__.py deleted file mode 100644 index d491190d111a25..00000000000000 --- a/tests/components/upc_connect/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the upc_connect component.""" diff --git a/tests/components/upc_connect/test_device_tracker.py b/tests/components/upc_connect/test_device_tracker.py deleted file mode 100644 index d04219eb884eee..00000000000000 --- a/tests/components/upc_connect/test_device_tracker.py +++ /dev/null @@ -1,221 +0,0 @@ -"""The tests for the UPC ConnextBox device tracker platform.""" -import asyncio - -from asynctest import patch -import pytest - -from homeassistant.components.device_tracker import DOMAIN -import homeassistant.components.upc_connect.device_tracker as platform -from homeassistant.const import CONF_HOST, CONF_PLATFORM -from homeassistant.setup import async_setup_component - -from tests.common import assert_setup_component, load_fixture, mock_component - -HOST = "127.0.0.1" - - -async def async_scan_devices_mock(scanner): - """Mock async_scan_devices.""" - return [] - - -@pytest.fixture(autouse=True) -def setup_comp_deps(hass, mock_device_tracker_conf): - """Set up component dependencies.""" - mock_component(hass, "zone") - mock_component(hass, "group") - yield - - -async def test_setup_platform_timeout_loginpage(hass, caplog, aioclient_mock): - """Set up a platform with timeout on loginpage.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(HOST), exc=asyncio.TimeoutError() - ) - aioclient_mock.post("http://{}/xml/getter.xml".format(HOST), content=b"successful") - - assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}} - ) - - assert len(aioclient_mock.mock_calls) == 1 - - assert "Error setting up platform" in caplog.text - - -async def test_setup_platform_timeout_webservice(hass, caplog, aioclient_mock): - """Set up a platform with api timeout.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(HOST), - cookies={"sessionToken": "654321"}, - content=b"successful", - exc=asyncio.TimeoutError(), - ) - - assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}} - ) - - assert len(aioclient_mock.mock_calls) == 1 - - assert "Error setting up platform" in caplog.text - - -@patch( - "homeassistant.components.upc_connect.device_tracker." - "UPCDeviceScanner.async_scan_devices", - return_value=async_scan_devices_mock, -) -async def test_setup_platform(scan_mock, hass, aioclient_mock): - """Set up a platform.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(HOST), - cookies={"sessionToken": "654321"}, - ) - aioclient_mock.post("http://{}/xml/getter.xml".format(HOST), content=b"successful") - - with assert_setup_component(1, DOMAIN): - assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}} - ) - - assert len(aioclient_mock.mock_calls) == 1 - - -async def test_scan_devices(hass, aioclient_mock): - """Set up a upc platform and scan device.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(HOST), - cookies={"sessionToken": "654321"}, - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(HOST), - content=b"successful", - cookies={"sessionToken": "654321"}, - ) - - scanner = await platform.async_get_scanner( - hass, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}} - ) - - assert len(aioclient_mock.mock_calls) == 1 - - aioclient_mock.clear_requests() - aioclient_mock.post( - "http://{}/xml/getter.xml".format(HOST), - text=load_fixture("upc_connect.xml"), - cookies={"sessionToken": "1235678"}, - ) - - mac_list = await scanner.async_scan_devices() - - assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[0][2] == "token=654321&fun=123" - assert mac_list == ["30:D3:2D:0:69:21", "5C:AA:FD:25:32:02", "70:EE:50:27:A1:38"] - - -async def test_scan_devices_without_session(hass, aioclient_mock): - """Set up a upc platform and scan device with no token.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(HOST), - cookies={"sessionToken": "654321"}, - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(HOST), - content=b"successful", - cookies={"sessionToken": "654321"}, - ) - - scanner = await platform.async_get_scanner( - hass, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}} - ) - - assert len(aioclient_mock.mock_calls) == 1 - - aioclient_mock.clear_requests() - aioclient_mock.get( - "http://{}/common_page/login.html".format(HOST), - cookies={"sessionToken": "654321"}, - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(HOST), - text=load_fixture("upc_connect.xml"), - cookies={"sessionToken": "1235678"}, - ) - - scanner.token = None - mac_list = await scanner.async_scan_devices() - - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2] == "token=654321&fun=123" - assert mac_list == ["30:D3:2D:0:69:21", "5C:AA:FD:25:32:02", "70:EE:50:27:A1:38"] - - -async def test_scan_devices_without_session_wrong_re(hass, aioclient_mock): - """Set up a upc platform and scan device with no token and wrong.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(HOST), - cookies={"sessionToken": "654321"}, - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(HOST), - content=b"successful", - cookies={"sessionToken": "654321"}, - ) - - scanner = await platform.async_get_scanner( - hass, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}} - ) - - assert len(aioclient_mock.mock_calls) == 1 - - aioclient_mock.clear_requests() - aioclient_mock.get( - "http://{}/common_page/login.html".format(HOST), - cookies={"sessionToken": "654321"}, - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(HOST), - status=400, - cookies={"sessionToken": "1235678"}, - ) - - scanner.token = None - mac_list = await scanner.async_scan_devices() - - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2] == "token=654321&fun=123" - assert mac_list == [] - - -async def test_scan_devices_parse_error(hass, aioclient_mock): - """Set up a upc platform and scan device with parse error.""" - aioclient_mock.get( - "http://{}/common_page/login.html".format(HOST), - cookies={"sessionToken": "654321"}, - ) - aioclient_mock.post( - "http://{}/xml/getter.xml".format(HOST), - content=b"successful", - cookies={"sessionToken": "654321"}, - ) - - scanner = await platform.async_get_scanner( - hass, {DOMAIN: {CONF_PLATFORM: "upc_connect", CONF_HOST: HOST}} - ) - - assert len(aioclient_mock.mock_calls) == 1 - - aioclient_mock.clear_requests() - aioclient_mock.post( - "http://{}/xml/getter.xml".format(HOST), - text="Blablebla blabalble", - cookies={"sessionToken": "1235678"}, - ) - - mac_list = await scanner.async_scan_devices() - - assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[0][2] == "token=654321&fun=123" - assert scanner.token is None - assert mac_list == [] From c31efe50ca0727ddc500df284448a165615c4686 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 11 Sep 2019 09:18:31 -0400 Subject: [PATCH 0253/3953] bump dependencies (#26576) --- 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 3fc31ce720ec80..3095d14061919e 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,9 +5,9 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.9.1", - "zha-quirks==0.0.22", + "zha-quirks==0.0.23", "zigpy-deconz==0.3.0", - "zigpy-homeassistant==0.7.2", + "zigpy-homeassistant==0.8.0", "zigpy-xbee-homeassistant==0.4.0", "zigpy-zigate==0.2.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index d1b03636bab57c..bd4efdafb9e44c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2004,7 +2004,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.22 +zha-quirks==0.0.23 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2016,7 +2016,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.3.0 # homeassistant.components.zha -zigpy-homeassistant==0.7.2 +zigpy-homeassistant==0.8.0 # homeassistant.components.zha zigpy-xbee-homeassistant==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f54886c4957889..b3c6514936a9e0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -428,4 +428,4 @@ wakeonlan==1.1.6 zeroconf==0.23.0 # homeassistant.components.zha -zigpy-homeassistant==0.7.2 +zigpy-homeassistant==0.8.0 From faeb95581aa0c13ea820edc33eda318979eaa902 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 11 Sep 2019 15:28:06 +0200 Subject: [PATCH 0254/3953] Updated frontend to 20190911.0 (#26578) --- 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 bf5e062e434d8f..8880d763611e9a 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==20190908.0" + "home-assistant-frontend==20190911.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 504f6516f84527..e111a974d1d85d 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==20190908.0 +home-assistant-frontend==20190911.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index bd4efdafb9e44c..0cbb76f78d6ac4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190908.0 +home-assistant-frontend==20190911.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3c6514936a9e0..1e80567f217c82 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190908.0 +home-assistant-frontend==20190911.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From f53fcacf492dd1a61a74aa41b531c60be834b9ed Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Wed, 11 Sep 2019 17:37:09 +0300 Subject: [PATCH 0255/3953] Make uk_transport sensor timezone/DST aware (#26577) * Make uk_transport sensor timezone/DST aware * Fix offset-naive and offset-aware datetime comparison --- homeassistant/components/uk_transport/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 8e6e46531e2f41..eb325d32212e11 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -11,6 +11,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_MODE from homeassistant.helpers.entity import Entity @@ -277,12 +278,11 @@ def device_state_attributes(self): def _delta_mins(hhmm_time_str): """Calculate time delta in minutes to a time in hh:mm format.""" - now = datetime.now() + now = dt_util.now() hhmm_time = datetime.strptime(hhmm_time_str, "%H:%M") - hhmm_datetime = datetime( - now.year, now.month, now.day, hour=hhmm_time.hour, minute=hhmm_time.minute - ) + hhmm_datetime = now.replace(hour=hhmm_time.hour, minute=hhmm_time.minute) + if hhmm_datetime < now: hhmm_datetime += timedelta(days=1) From 7dfdec531cfe840bfeece1afddaf6ec608a77466 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Wed, 11 Sep 2019 19:00:18 +0300 Subject: [PATCH 0256/3953] Fix GTFS sensor wrong timezone (#26580) --- homeassistant/components/gtfs/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index eb662d8b2c93a8..086545f0c761d5 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -122,7 +122,7 @@ def get_next_departure( include_tomorrow: bool = False, ) -> dict: """Get the next departure for the given schedule.""" - now = datetime.datetime.now() + offset + now = dt_util.now().replace(tzinfo=None) + offset now_date = now.strftime(dt_util.DATE_STR_FORMAT) yesterday = now - datetime.timedelta(days=1) yesterday_date = yesterday.strftime(dt_util.DATE_STR_FORMAT) From 1a73e6b44e27571e69e90a98968253fa2bff372c Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Wed, 11 Sep 2019 10:24:41 -0700 Subject: [PATCH 0257/3953] Add switch platform to iaqualink integration (#26545) * Add switch platform to iaqualink component * Remove unnecessary call to constructor --- .coveragerc | 1 + .../components/iaqualink/__init__.py | 10 +++ homeassistant/components/iaqualink/switch.py | 65 +++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 homeassistant/components/iaqualink/switch.py diff --git a/.coveragerc b/.coveragerc index d635aea6c67c6a..1e5cb41c463a85 100644 --- a/.coveragerc +++ b/.coveragerc @@ -291,6 +291,7 @@ omit = homeassistant/components/iaqualink/climate.py homeassistant/components/iaqualink/light.py homeassistant/components/iaqualink/sensor.py + homeassistant/components/iaqualink/switch.py homeassistant/components/icloud/device_tracker.py homeassistant/components/idteck_prox/* homeassistant/components/ifttt/* diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 925fd0a7906d5e..56a39df64c9dea 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -12,12 +12,14 @@ AqualinkLoginException, AqualinkSensor, AqualinkThermostat, + AqualinkToggle, ) from homeassistant import config_entries from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback @@ -77,6 +79,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = [] lights = hass.data[DOMAIN][LIGHT_DOMAIN] = [] sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] + switches = hass.data[DOMAIN][SWITCH_DOMAIN] = [] session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) aqualink = AqualinkClient(username, password, session) @@ -102,6 +105,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None lights += [dev] elif isinstance(dev, AqualinkSensor): sensors += [dev] + elif isinstance(dev, AqualinkToggle): + switches += [dev] forward_setup = hass.config_entries.async_forward_entry_setup if climates: @@ -113,6 +118,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None if sensors: _LOGGER.debug("Got %s sensors: %s", len(sensors), sensors) hass.async_create_task(forward_setup(entry, SENSOR_DOMAIN)) + if switches: + _LOGGER.debug("Got %s switches: %s", len(switches), switches) + hass.async_create_task(forward_setup(entry, SWITCH_DOMAIN)) async def _async_systems_update(now): """Refresh internal state for all systems.""" @@ -136,6 +144,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo tasks += [forward_unload(entry, LIGHT_DOMAIN)] if hass.data[DOMAIN][SENSOR_DOMAIN]: tasks += [forward_unload(entry, SENSOR_DOMAIN)] + if hass.data[DOMAIN][SWITCH_DOMAIN]: + tasks += [forward_unload(entry, SWITCH_DOMAIN)] hass.data[DOMAIN].clear() diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py new file mode 100644 index 00000000000000..f2fc51ce713136 --- /dev/null +++ b/homeassistant/components/iaqualink/switch.py @@ -0,0 +1,65 @@ +"""Support for Aqualink pool feature switches.""" +import logging + +from iaqualink import AqualinkToggle + +from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import AqualinkEntity, refresh_system +from .const import DOMAIN as AQUALINK_DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up discovered switches.""" + devs = [] + for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: + devs.append(HassAqualinkSwitch(dev)) + async_add_entities(devs, True) + + +class HassAqualinkSwitch(SwitchDevice, AqualinkEntity): + """Representation of a switch.""" + + def __init__(self, dev: AqualinkToggle): + """Initialize the switch.""" + self.dev = dev + + @property + def name(self) -> str: + """Return the name of the switch.""" + return self.dev.label + + @property + def icon(self) -> str: + """Return an icon based on the switch type.""" + if self.name == "Cleaner": + return "mdi:robot-vacuum" + if self.name == "Waterfall" or self.name.endswith("Dscnt"): + return "mdi:fountain" + if self.name.endswith("Pump") or self.name.endswith("Blower"): + return "mdi:fan" + if self.name.endswith("Heater"): + return "mdi:radiator" + + @property + def is_on(self) -> bool: + """Return whether the switch is on or not.""" + return self.dev.is_on + + @refresh_system + async def async_turn_on(self, **kwargs) -> None: + """Turn on the switch.""" + await self.dev.turn_on() + + @refresh_system + async def async_turn_off(self, **kwargs) -> None: + """Turn off the switch.""" + await self.dev.turn_off() From 2b30f47f4bbc870a18da59a4f7495454dd48db4f Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Wed, 11 Sep 2019 10:26:50 -0700 Subject: [PATCH 0258/3953] Add Obihai integration (#26537) * Add Obihai integration * Lint * Lint and bump library for multiple ports fix * Review comments * Review comments * Correct errors * Review comments * Review comments --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/obihai/__init__.py | 1 + homeassistant/components/obihai/manifest.json | 11 ++ homeassistant/components/obihai/sensor.py | 104 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 121 insertions(+) create mode 100644 homeassistant/components/obihai/__init__.py create mode 100644 homeassistant/components/obihai/manifest.json create mode 100644 homeassistant/components/obihai/sensor.py diff --git a/.coveragerc b/.coveragerc index 1e5cb41c463a85..ad001e5604839e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -435,6 +435,7 @@ omit = homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/sensor.py + homeassistant/components/obihai/* homeassistant/components/octoprint/* homeassistant/components/oem/climate.py homeassistant/components/oasa_telematics/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 2a04189dfba8a9..7a84eeac13b757 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -197,6 +197,7 @@ homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuki/* @pschmitt homeassistant/components/nws/* @MatthewFlamm +homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/opentherm_gw/* @mvn23 diff --git a/homeassistant/components/obihai/__init__.py b/homeassistant/components/obihai/__init__.py new file mode 100644 index 00000000000000..8e65423b73bb3c --- /dev/null +++ b/homeassistant/components/obihai/__init__.py @@ -0,0 +1 @@ +"""The Obihai integration.""" diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json new file mode 100644 index 00000000000000..b6bad10d608096 --- /dev/null +++ b/homeassistant/components/obihai/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "obihai", + "name": "Obihai", + "documentation": "https://www.home-assistant.io/components/obihai", + "requirements": [ + "pyobihai==1.0.2" + ], + "dependencies": [], + "codeowners": ["@dshokouhi"] + } + \ No newline at end of file diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py new file mode 100644 index 00000000000000..4eb3881e95bf37 --- /dev/null +++ b/homeassistant/components/obihai/sensor.py @@ -0,0 +1,104 @@ +"""Support for Obihai Sensors.""" +import logging + +from datetime import timedelta +import voluptuous as vol + +from pyobihai import PyObihai + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + DEVICE_CLASS_TIMESTAMP, +) + +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=5) + +OBIHAI = "Obihai" +DEFAULT_USERNAME = "admin" +DEFAULT_PASSWORD = "admin" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Obihai sensor platform.""" + + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + host = config[CONF_HOST] + + sensors = [] + + pyobihai = PyObihai() + + services = pyobihai.get_state(host, username, password) + + line_services = pyobihai.get_line_state(host, username, password) + + for key in services: + sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + + for key in line_services: + sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + + add_entities(sensors) + + +class ObihaiServiceSensors(Entity): + """Get the status of each Obihai Lines.""" + + def __init__(self, pyobihai, host, username, password, service_name): + """Initialize monitor sensor.""" + self._host = host + self._username = username + self._password = password + self._service_name = service_name + self._state = None + self._name = f"{OBIHAI} {self._service_name}" + self._pyobihai = pyobihai + + @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 device_class(self): + """Return the device class for uptime sensor.""" + if self._service_name == "Last Reboot": + return DEVICE_CLASS_TIMESTAMP + return None + + def update(self): + """Update the sensor.""" + services = self._pyobihai.get_state(self._host, self._username, self._password) + + if self._service_name in services: + self._state = services.get(self._service_name) + + services = self._pyobihai.get_line_state( + self._host, self._username, self._password + ) + + if self._service_name in services: + self._state = services.get(self._service_name) diff --git a/requirements_all.txt b/requirements_all.txt index 0cbb76f78d6ac4..173e203a892d85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1344,6 +1344,9 @@ pynws==0.7.4 # homeassistant.components.nx584 pynx584==0.4 +# homeassistant.components.obihai +pyobihai==1.0.2 + # homeassistant.components.openuv pyopenuv==1.0.9 From 6eeb01edc4549da4c525886b4d161d966c919062 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 11 Sep 2019 13:21:08 -0500 Subject: [PATCH 0259/3953] Remove default host for Plex config (#26583) * Remove default host, allow config with token(+server) * Require one of host or token * Oops * Adjust schema * Fix schema * Add self as codeowner * Update CODEOWNERS --- CODEOWNERS | 1 + homeassistant/components/plex/__init__.py | 37 ++++++++++++--------- homeassistant/components/plex/const.py | 4 +-- homeassistant/components/plex/manifest.json | 4 ++- homeassistant/components/plex/server.py | 20 ++++++++++- 5 files changed, 46 insertions(+), 20 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7a84eeac13b757..18218bbf68e0f9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -212,6 +212,7 @@ homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel +homeassistant/components/plex/* @jjlawren homeassistant/components/plugwise/* @laetificat @CoMPaTech homeassistant/components/point/* @fredrike homeassistant/components/ps4/* @ktnrg45 diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 846f3e3f53c325..69e77c8854fef1 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -20,9 +20,9 @@ from homeassistant.util.json import load_json, save_json from .const import ( + CONF_SERVER, CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, - DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, @@ -42,14 +42,18 @@ ) SERVER_CONFIG_SCHEMA = vol.Schema( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_TOKEN): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(MP_DOMAIN, default={}): MEDIA_PLAYER_SCHEMA, - } + vol.All( + { + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_TOKEN): cv.string, + vol.Optional(CONF_SERVER): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(MP_DOMAIN, default={}): MEDIA_PLAYER_SCHEMA, + }, + cv.has_at_least_one_key(CONF_HOST, CONF_TOKEN), + ) ) CONFIG_SCHEMA = vol.Schema({PLEX_DOMAIN: SERVER_CONFIG_SCHEMA}, extra=vol.ALLOW_EXTRA) @@ -73,12 +77,14 @@ def setup_plex(config=None, discovery_info=None, configurator_info=None): """Return assembled server_config dict.""" json_file = hass.config.path(PLEX_CONFIG_FILE) file_config = load_json(json_file) + host_and_port = None if config: server_config = config - host_and_port = ( - f"{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" - ) + if CONF_HOST in server_config: + host_and_port = ( + f"{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" + ) if MP_DOMAIN in server_config: hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN) elif file_config: @@ -95,9 +101,10 @@ def setup_plex(config=None, discovery_info=None, configurator_info=None): discovery.listen(hass, SERVICE_PLEX, server_discovered) return True - use_ssl = server_config.get(CONF_SSL, DEFAULT_SSL) - http_prefix = "https" if use_ssl else "http" - server_config[CONF_URL] = f"{http_prefix}://{host_and_port}" + if host_and_port: + use_ssl = server_config.get(CONF_SSL, DEFAULT_SSL) + http_prefix = "https" if use_ssl else "http" + server_config[CONF_URL] = f"{http_prefix}://{host_and_port}" plex_server = PlexServer(server_config) try: diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index bf8c2387e4de3a..6f19623c809e86 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -2,7 +2,6 @@ DOMAIN = "plex" NAME_FORMAT = "Plex {}" -DEFAULT_HOST = "localhost" DEFAULT_PORT = 32400 DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True @@ -14,7 +13,6 @@ PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" PLEX_SERVER_CONFIG = "server_config" +CONF_SERVER = "server" CONF_USE_EPISODE_ART = "use_episode_art" CONF_SHOW_ALL_CONTROLS = "show_all_controls" -CONF_REMOVE_UNAVAILABLE_CLIENTS = "remove_unavailable_clients" -CONF_CLIENT_REMOVE_INTERVAL = "client_remove_interval" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 32ddb83476c81e..4269400dc2456e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,5 +6,7 @@ "plexapi==3.0.6" ], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": [ + "@jjlawren" + ] } diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index c778588752a3ad..962e074996f5e5 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,12 +1,13 @@ """Shared class to maintain Plex server instances.""" import logging +import plexapi.myplex import plexapi.server from requests import Session from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL -from .const import DEFAULT_VERIFY_SSL +from .const import CONF_SERVER, DEFAULT_VERIFY_SSL _LOGGER = logging.getLogger(__package__) @@ -19,11 +20,25 @@ def __init__(self, server_config): self._plex_server = None self._url = server_config.get(CONF_URL) self._token = server_config.get(CONF_TOKEN) + self._server_name = server_config.get(CONF_SERVER) self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" + def _set_missing_url(): + account = plexapi.myplex.MyPlexAccount(token=self._token) + available_servers = [ + x.name for x in account.resources() if "server" in x.provides + ] + server_choice = ( + self._server_name if self._server_name else available_servers[0] + ) + connections = account.resource(server_choice).connections + local_url = [x.httpuri for x in connections if x.local] + remote_url = [x.uri for x in connections if not x.local] + self._url = local_url[0] if local_url else remote_url[0] + def _connect_with_url(): session = None if self._url.startswith("https") and not self._verify_ssl: @@ -34,6 +49,9 @@ def _connect_with_url(): ) _LOGGER.debug("Connected to: %s (%s)", self.friendly_name, self.url_in_use) + if self._token and not self._url: + _set_missing_url() + _connect_with_url() def clients(self): From adaa200935051b48a4c0f3d10d0e00714f8c5138 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Sep 2019 12:34:10 -0600 Subject: [PATCH 0260/3953] Home Assistant Cast (#26566) * Add backend support for Home Assistant Cast * Update test reqs --- homeassistant/components/cast/__init__.py | 5 +- homeassistant/components/cast/const.py | 3 + .../components/cast/home_assistant_cast.py | 64 +++++++++++++++++++ homeassistant/components/cast/manifest.json | 4 +- homeassistant/components/cast/media_player.py | 27 ++++++++ homeassistant/components/cast/services.yaml | 9 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/common.py | 15 +++++ .../cast/test_home_assistant_cast.py | 28 ++++++++ 10 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/cast/home_assistant_cast.py create mode 100644 homeassistant/components/cast/services.yaml create mode 100644 tests/components/cast/test_home_assistant_cast.py diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index cc112984f888ae..4dfb58ef3b7d6e 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -1,6 +1,7 @@ """Component to embed Google Cast.""" from homeassistant import config_entries +from . import home_assistant_cast from .const import DOMAIN @@ -20,8 +21,10 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass, entry: config_entries.ConfigEntry): """Set up Cast from a config entry.""" + await home_assistant_cast.async_setup_ha_cast(hass, entry) + hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "media_player") ) diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index a493c322f14100..c6164484dbbb14 100644 --- a/homeassistant/components/cast/const.py +++ b/homeassistant/components/cast/const.py @@ -21,3 +21,6 @@ # Dispatcher signal fired with a ChromecastInfo every time a Chromecast is # removed SIGNAL_CAST_REMOVED = "cast_removed" + +# Dispatcher signal fired when a Chromecast should show a Home Assistant Cast view. +SIGNAL_HASS_CAST_SHOW_VIEW = "cast_show_view" diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py new file mode 100644 index 00000000000000..f604594bfc5887 --- /dev/null +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -0,0 +1,64 @@ +"""Home Assistant Cast integration for Cast.""" +from typing import Optional + +import voluptuous as vol + +from pychromecast.controllers.homeassistant import HomeAssistantController + +from homeassistant import auth, config_entries, core +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers import config_validation as cv, dispatcher + +from .const import DOMAIN, SIGNAL_HASS_CAST_SHOW_VIEW + +SERVICE_SHOW_VIEW = "show_lovelace_view" +ATTR_VIEW_PATH = "view_path" + + +async def async_setup_ha_cast( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): + """Set up Home Assistant Cast.""" + user_id: Optional[str] = entry.data.get("user_id") + user: Optional[auth.models.User] = None + + if user_id is not None: + user = await hass.auth.async_get_user(user_id) + + if user is None: + user = await hass.auth.async_create_system_user( + "Home Assistant Cast", [auth.GROUP_ID_ADMIN] + ) + hass.config_entries.async_update_entry( + entry, data={**entry.data, "user_id": user.id} + ) + + if user.refresh_tokens: + refresh_token: auth.models.RefreshToken = list(user.refresh_tokens.values())[0] + else: + refresh_token = await hass.auth.async_create_refresh_token(user) + + async def handle_show_view(call: core.ServiceCall): + """Handle a Show View service call.""" + controller = HomeAssistantController( + # If you are developing Home Assistant Cast, uncomment and set to your dev app id. + # app_id="5FE44367", + hass_url=hass.config.api.base_url, + client_id=None, + refresh_token=refresh_token.token, + ) + + dispatcher.async_dispatcher_send( + hass, + SIGNAL_HASS_CAST_SHOW_VIEW, + controller, + call.data[ATTR_ENTITY_ID], + call.data[ATTR_VIEW_PATH], + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_SHOW_VIEW, + handle_show_view, + vol.Schema({ATTR_ENTITY_ID: cv.entity_id, ATTR_VIEW_PATH: str}), + ) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index ff9e8907ec5aeb..4fb1c67a56e440 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,9 +3,7 @@ "name": "Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/components/cast", - "requirements": [ - "pychromecast==3.2.2" - ], + "requirements": ["pychromecast==4.0.0"], "dependencies": [], "zeroconf": ["_googlecast._tcp.local."], "codeowners": [] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 7b750c3fe0cbfd..c2d847fd09bb2b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -9,6 +9,7 @@ CONNECTION_STATUS_DISCONNECTED, ) from pychromecast.controllers.multizone import MultizoneManager +from pychromecast.controllers.homeassistant import HomeAssistantController import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice @@ -52,6 +53,7 @@ CAST_MULTIZONE_MANAGER_KEY, DEFAULT_PORT, SIGNAL_CAST_REMOVED, + SIGNAL_HASS_CAST_SHOW_VIEW, ) from .helpers import ( ChromecastInfo, @@ -225,9 +227,11 @@ def __init__(self, cast_info: ChromecastInfo): self._dynamic_group_status_listener: Optional[ DynamicGroupCastStatusListener ] = None + self._hass_cast_controller: Optional[HomeAssistantController] = None self._add_remove_handler = None self._del_remove_handler = None + self._cast_view_remove_handler = None async def async_added_to_hass(self): """Create chromecast object when added to hass.""" @@ -256,6 +260,10 @@ async def async_added_to_hass(self): ) break + self._cast_view_remove_handler = async_dispatcher_connect( + self.hass, SIGNAL_HASS_CAST_SHOW_VIEW, self._handle_signal_show_view + ) + async def async_will_remove_from_hass(self) -> None: """Disconnect Chromecast object when removed.""" await self._async_disconnect() @@ -265,8 +273,13 @@ async def async_will_remove_from_hass(self) -> None: self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid) if self._add_remove_handler: self._add_remove_handler() + self._add_remove_handler = None if self._del_remove_handler: self._del_remove_handler() + self._del_remove_handler = None + if self._cast_view_remove_handler: + self._cast_view_remove_handler() + self._cast_view_remove_handler = None async def async_set_cast_info(self, cast_info): """Set the cast information and set up the chromecast object.""" @@ -453,6 +466,7 @@ def _invalidate(self): self.mz_media_status = {} self.mz_media_status_received = {} self.mz_mgr = None + self._hass_cast_controller = None if self._status_listener is not None: self._status_listener.invalidate() self._status_listener = None @@ -932,3 +946,16 @@ async def _async_cast_removed(self, discover: ChromecastInfo): async def _async_stop(self, event): """Disconnect socket on Home Assistant stop.""" await self._async_disconnect() + + def _handle_signal_show_view( + self, controller: HomeAssistantController, entity_id: str, view_path: str + ): + """Handle a show view signal.""" + if entity_id != self.entity_id: + return + + if self._hass_cast_controller is None: + self._hass_cast_controller = controller + self._chromecast.register_handler(controller) + + self._hass_cast_controller.show_lovelace_view(view_path) diff --git a/homeassistant/components/cast/services.yaml b/homeassistant/components/cast/services.yaml new file mode 100644 index 00000000000000..24bc7b16a903c7 --- /dev/null +++ b/homeassistant/components/cast/services.yaml @@ -0,0 +1,9 @@ +show_lovelace_view: + description: Show a Lovelace view on a Chromecast. + fields: + entity_id: + description: Media Player entity to show the Lovelace view on. + example: "media_player.kitchen" + view_path: + description: The path of the Lovelace view to show. + example: downstairs diff --git a/requirements_all.txt b/requirements_all.txt index 173e203a892d85..80868fa05b17b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1110,7 +1110,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==3.2.2 +pychromecast==4.0.0 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e80567f217c82..c9c32d596bbeb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,7 +279,7 @@ pyMetno==0.4.6 pyblackbird==0.5 # homeassistant.components.cast -pychromecast==3.2.2 +pychromecast==4.0.0 # homeassistant.components.deconz pydeconz==62 diff --git a/tests/common.py b/tests/common.py index 0e2f701c210a75..847635d4dad67e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1030,3 +1030,18 @@ def capture_events(event): hass.bus.async_listen(event_name, capture_events) return events + + +@ha.callback +def async_mock_signal(hass, signal): + """Catch all dispatches to a signal.""" + calls = [] + + @ha.callback + def mock_signal_handler(*args): + """Mock service call.""" + calls.append(args) + + hass.helpers.dispatcher.async_dispatcher_connect(signal, mock_signal_handler) + + return calls diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py new file mode 100644 index 00000000000000..e67b8f70160472 --- /dev/null +++ b/tests/components/cast/test_home_assistant_cast.py @@ -0,0 +1,28 @@ +"""Test Home Assistant Cast.""" +from unittest.mock import Mock +from homeassistant.components.cast import home_assistant_cast + +from tests.common import MockConfigEntry, async_mock_signal + + +async def test_service_show_view(hass): + """Test we don't set app id in prod.""" + hass.config.api = Mock(base_url="http://example.com") + await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) + calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) + + await hass.services.async_call( + "cast", + "show_lovelace_view", + {"entity_id": "media_player.kitchen", "view_path": "mock_path"}, + blocking=True, + ) + + assert len(calls) == 1 + controller, entity_id, view_path = calls[0] + assert controller.hass_url == "http://example.com" + assert controller.client_id is None + # Verify user did not accidentally submit their dev app id + assert controller.supporting_app_id == "B12CE3CA" + assert entity_id == "media_player.kitchen" + assert view_path == "mock_path" From 182bf1edef9004d2301d6ffee0156dc3b44e6ce6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 11 Sep 2019 20:42:54 +0200 Subject: [PATCH 0261/3953] Deprecate Python 3.6.0 (#26575) * Deprecate Python 3.6.1 * Update msg --- homeassistant/bootstrap.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index ef294491141042..7c4ec731b49b93 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -97,6 +97,17 @@ async def async_from_config_dict( stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) + if sys.version_info[:3] < (3, 6, 1): + msg = ( + "Python 3.6.0 support is deprecated and will " + "be removed in the first release after October 2. Please " + "upgrade Python to 3.6.1 or higher." + ) + _LOGGER.warning(msg) + hass.components.persistent_notification.async_create( + msg, "Python version", "python_version" + ) + return hass From 3fbdc89db1eaaf963f242a79adc31aa55f6c9788 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Sep 2019 13:21:22 -0600 Subject: [PATCH 0262/3953] Updated frontend to 20190911.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 8880d763611e9a..f9cca789f5040e 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==20190911.0" + "home-assistant-frontend==20190911.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e111a974d1d85d..d635d178940e3e 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==20190911.0 +home-assistant-frontend==20190911.1 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 80868fa05b17b0..da36928d6ba834 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190911.0 +home-assistant-frontend==20190911.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9c32d596bbeb4..07adcfc79f6933 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190911.0 +home-assistant-frontend==20190911.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From df390bc9ab45f1e34d1a0aa6ef02ee7d53184a82 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Sep 2019 13:30:51 -0600 Subject: [PATCH 0263/3953] Check if git is dirty before committing (#26588) --- script/version_bump.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/script/version_bump.py b/script/version_bump.py index d5df8e66902f0d..de6638df30ba8c 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -108,7 +108,7 @@ def write_version(version): content = re.sub("MAJOR_VERSION = .*\n", f"MAJOR_VERSION = {major}\n", content) content = re.sub("MINOR_VERSION = .*\n", f"MINOR_VERSION = {minor}\n", content) - content = re.sub("PATCH_VERSION = .*\n", f"PATCH_VERSION = '{patch}'\n", content) + content = re.sub("PATCH_VERSION = .*\n", f'PATCH_VERSION = "{patch}"\n', content) with open("homeassistant/const.py", "wt") as fil: content = fil.write(content) @@ -126,9 +126,15 @@ def main(): "--commit", action="store_true", help="Create a version bump commit." ) arguments = parser.parse_args() + + if arguments.commit and subprocess.run(["git", "diff", "--quiet"]).returncode == 1: + print("Cannot use --commit because git is dirty.") + return + current = Version(const.__version__) bumped = bump_version(current, arguments.type) assert bumped > current, "BUG! New version is not newer than old version" + write_version(bumped) if not arguments.commit: From 0221d136de82d36d6ca33e132248eda9e27bf3d0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 11 Sep 2019 21:35:30 +0200 Subject: [PATCH 0264/3953] Remove support of UniFi device tracker configuration import (#26587) --- homeassistant/components/unifi/config_flow.py | 16 +---- .../components/unifi/device_tracker.py | 62 +------------------ tests/components/unifi/test_device_tracker.py | 7 +-- 3 files changed, 7 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 00b003746a2568..fdb75d09194ef9 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Unifi.""" +"""Config flow for UniFi.""" import voluptuous as vol from homeassistant import config_entries @@ -164,20 +164,6 @@ async def async_step_site(self, user_input=None): errors=errors, ) - async def async_step_import(self, import_config): - """Import from UniFi device tracker config.""" - config = { - CONF_HOST: import_config[CONF_HOST], - CONF_USERNAME: import_config[CONF_USERNAME], - CONF_PASSWORD: import_config[CONF_PASSWORD], - CONF_PORT: import_config.get(CONF_PORT), - CONF_VERIFY_SSL: import_config.get(CONF_VERIFY_SSL), - } - - self.desc = import_config[CONF_SITE_ID] - - return await self.async_step_user(user_input=config) - class UnifiOptionsFlowHandler(config_entries.OptionsFlow): """Handle Unifi options.""" diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index ca6ddb6820660c..b3982e7327d42d 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,36 +1,20 @@ -"""Support for Unifi WAP controllers.""" -from datetime import timedelta - +"""Track devices using UniFi controllers.""" import logging import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER from homeassistant.core import callback -from homeassistant.const import ( - CONF_HOST, - CONF_USERNAME, - CONF_PASSWORD, - CONF_PORT, - CONF_VERIFY_SSL, -) from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY -import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -from .const import ( - ATTR_MANUFACTURER, - CONF_CONTROLLER, - CONF_SITE_ID, - DOMAIN as UNIFI_DOMAIN, -) +from .const import ATTR_MANUFACTURER LOGGER = logging.getLogger(__name__) @@ -55,51 +39,11 @@ "vlan", ] -CONF_DT_SITE_ID = "site_id" - -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 8443 -DEFAULT_VERIFY_SSL = True -DEFAULT_DETECTION_TIME = timedelta(seconds=300) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_DT_SITE_ID, default="default"): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): vol.Any( - cv.boolean, cv.isfile - ), - }, - extra=vol.ALLOW_EXTRA, -) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) async def async_setup_scanner(hass, config, sync_see, discovery_info): """Set up the Unifi integration.""" - config[CONF_SITE_ID] = config.pop(CONF_DT_SITE_ID) # Current from legacy - - exist = False - - for entry in hass.config_entries.async_entries(UNIFI_DOMAIN): - if ( - config[CONF_HOST] == entry.data[CONF_CONTROLLER][CONF_HOST] - and config[CONF_SITE_ID] == entry.data[CONF_CONTROLLER][CONF_SITE_ID] - ): - exist = True - break - - if not exist: - hass.async_create_task( - hass.config_entries.flow.async_init( - UNIFI_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=config, - ) - ) - return True diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 437fabf9689469..969c2a734d3448 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,4 +1,4 @@ -"""The tests for the Unifi WAP device tracker platform.""" +"""The tests for the UniFi device tracker platform.""" from collections import deque from copy import copy from unittest.mock import Mock @@ -32,7 +32,6 @@ from homeassistant.setup import async_setup_component import homeassistant.components.device_tracker as device_tracker -import homeassistant.components.unifi.device_tracker as unifi_dt import homeassistant.util.dt as dt_util DEFAULT_DETECTION_TIME = timedelta(seconds=300) @@ -275,14 +274,14 @@ async def test_restoring_client(hass, mock_controller): registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( device_tracker.DOMAIN, - unifi_dt.UNIFI_DOMAIN, + unifi.DOMAIN, "{}-mock-site".format(CLIENT_1["mac"]), suggested_object_id=CLIENT_1["hostname"], config_entry=config_entry, ) registry.async_get_or_create( device_tracker.DOMAIN, - unifi_dt.UNIFI_DOMAIN, + unifi.DOMAIN, "{}-mock-site".format(CLIENT_2["mac"]), suggested_object_id=CLIENT_2["hostname"], config_entry=config_entry, From 2ec86a23497320cc708abb7af57e917631a9508b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Sep 2019 13:36:12 -0600 Subject: [PATCH 0265/3953] Bumped version to 0.99.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4cfd16b8c9f0cc..3a474d6245b05c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 99 -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) From 1eef758ecc6a59ea983e953d775a7eac30fbe542 Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 11 Sep 2019 16:51:05 -0400 Subject: [PATCH 0266/3953] Add support for DOODS Image Processing (#26208) * Add support for doods * Move connection to external module * Fix for CI * Another update for CI * Reformatted via black * Updated linting stuff * Updated per code review * Removed none check for something with a default * Updated config parsing * Updated if statements, need to disable lint check * Fixed formatting and bug that should make linter happy * Fixed one more issue with box drawing for areas * removed extra imports * Reworked per suggestion * Changed output to debug for informational detection message --- .coveragerc | 1 + homeassistant/components/doods/__init__.py | 1 + .../components/doods/image_processing.py | 360 ++++++++++++++++++ homeassistant/components/doods/manifest.json | 10 + requirements_all.txt | 3 + 5 files changed, 375 insertions(+) create mode 100644 homeassistant/components/doods/__init__.py create mode 100644 homeassistant/components/doods/image_processing.py create mode 100644 homeassistant/components/doods/manifest.json diff --git a/.coveragerc b/.coveragerc index ad001e5604839e..0c6ac82894a6f4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -143,6 +143,7 @@ omit = homeassistant/components/dlna_dmr/media_player.py homeassistant/components/dnsip/sensor.py homeassistant/components/dominos/* + homeassistant/components/doods/* homeassistant/components/doorbird/* homeassistant/components/dovado/* homeassistant/components/downloader/* diff --git a/homeassistant/components/doods/__init__.py b/homeassistant/components/doods/__init__.py new file mode 100644 index 00000000000000..b6edb9be87bda0 --- /dev/null +++ b/homeassistant/components/doods/__init__.py @@ -0,0 +1 @@ +"""The doods component.""" diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py new file mode 100644 index 00000000000000..ba44d86c2e4083 --- /dev/null +++ b/homeassistant/components/doods/image_processing.py @@ -0,0 +1,360 @@ +"""Support for the DOODS service.""" +import io +import logging +import time + +import voluptuous as vol +from PIL import Image, ImageDraw +from pydoods import PyDOODS + +from homeassistant.components.image_processing import ( + CONF_CONFIDENCE, + CONF_ENTITY_ID, + CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingEntity, +) +from homeassistant.core import split_entity_id +from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +ATTR_MATCHES = "matches" +ATTR_SUMMARY = "summary" +ATTR_TOTAL_MATCHES = "total_matches" + +CONF_URL = "url" +CONF_AUTH_KEY = "auth_key" +CONF_DETECTOR = "detector" +CONF_LABELS = "labels" +CONF_AREA = "area" +CONF_TOP = "top" +CONF_BOTTOM = "bottom" +CONF_RIGHT = "right" +CONF_LEFT = "left" +CONF_FILE_OUT = "file_out" + +AREA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_BOTTOM, default=1): cv.small_float, + vol.Optional(CONF_LEFT, default=0): cv.small_float, + vol.Optional(CONF_RIGHT, default=1): cv.small_float, + vol.Optional(CONF_TOP, default=0): cv.small_float, + } +) + +LABEL_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_AREA): AREA_SCHEMA, + vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_URL): cv.string, + vol.Required(CONF_DETECTOR): cv.string, + vol.Optional(CONF_AUTH_KEY, default=""): cv.string, + vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]), + vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), + vol.Optional(CONF_LABELS, default=[]): vol.All( + cv.ensure_list, [vol.Any(cv.string, LABEL_SCHEMA)] + ), + vol.Optional(CONF_AREA): AREA_SCHEMA, + } +) + + +def draw_box(draw, box, img_width, img_height, text="", color=(255, 255, 0)): + """Draw bounding box on image.""" + ymin, xmin, ymax, xmax = box + (left, right, top, bottom) = ( + xmin * img_width, + xmax * img_width, + ymin * img_height, + ymax * img_height, + ) + draw.line( + [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], + width=5, + fill=color, + ) + if text: + draw.text((left, abs(top - 15)), text, fill=color) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Doods client.""" + url = config[CONF_URL] + auth_key = config[CONF_AUTH_KEY] + detector_name = config[CONF_DETECTOR] + + doods = PyDOODS(url, auth_key) + response = doods.get_detectors() + if not isinstance(response, dict): + _LOGGER.warning("Could not connect to doods server: %s", url) + return + + detector = {} + for server_detector in response["detectors"]: + if server_detector["name"] == detector_name: + detector = server_detector + break + + if not detector: + _LOGGER.warning( + "Detector %s is not supported by doods server %s", detector_name, url + ) + return + + entities = [] + for camera in config[CONF_SOURCE]: + entities.append( + Doods( + hass, + camera[CONF_ENTITY_ID], + camera.get(CONF_NAME), + doods, + detector, + config, + ) + ) + add_entities(entities) + + +class Doods(ImageProcessingEntity): + """Doods image processing service client.""" + + def __init__(self, hass, camera_entity, name, doods, detector, config): + """Initialize the DOODS entity.""" + self.hass = hass + self._camera_entity = camera_entity + if name: + self._name = name + else: + name = split_entity_id(camera_entity)[1] + self._name = f"Doods {name}" + self._doods = doods + self._file_out = config[CONF_FILE_OUT] + + # detector config and aspect ratio + self._width = None + self._height = None + self._aspect = None + if detector["width"] and detector["height"]: + self._width = detector["width"] + self._height = detector["height"] + self._aspect = self._width / self._height + + # the base confidence + dconfig = {} + confidence = config[CONF_CONFIDENCE] + + # handle labels and specific detection areas + labels = config[CONF_LABELS] + self._label_areas = {} + for label in labels: + if isinstance(label, dict): + label_name = label[CONF_NAME] + if label_name not in detector["labels"] and label_name != "*": + _LOGGER.warning("Detector does not support label %s", label_name) + continue + + # Label Confidence + label_confidence = label[CONF_CONFIDENCE] + if label_name not in dconfig or dconfig[label_name] > label_confidence: + dconfig[label_name] = label_confidence + + # Label area + label_area = label.get(CONF_AREA) + self._label_areas[label_name] = [0, 0, 1, 1] + if label_area: + self._label_areas[label_name] = [ + label_area[CONF_TOP], + label_area[CONF_LEFT], + label_area[CONF_BOTTOM], + label_area[CONF_RIGHT], + ] + else: + if label not in detector["labels"] and label != "*": + _LOGGER.warning("Detector does not support label %s", label) + continue + self._label_areas[label] = [0, 0, 1, 1] + if label not in dconfig or dconfig[label] > confidence: + dconfig[label] = confidence + + if not dconfig: + dconfig["*"] = confidence + + # Handle global detection area + self._area = [0, 0, 1, 1] + area_config = config.get(CONF_AREA) + if area_config: + self._area = [ + area_config[CONF_TOP], + area_config[CONF_LEFT], + area_config[CONF_BOTTOM], + area_config[CONF_RIGHT], + ] + + template.attach(hass, self._file_out) + + self._dconfig = dconfig + self._matches = {} + self._total_matches = 0 + self._last_image = None + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera_entity + + @property + def name(self): + """Return the name of the image processor.""" + return self._name + + @property + def state(self): + """Return the state of the entity.""" + return self._total_matches + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return { + ATTR_MATCHES: self._matches, + ATTR_SUMMARY: { + label: len(values) for label, values in self._matches.items() + }, + ATTR_TOTAL_MATCHES: self._total_matches, + } + + def _save_image(self, image, matches, paths): + img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") + img_width, img_height = img.size + draw = ImageDraw.Draw(img) + + # Draw custom global region/area + if self._area != [0, 0, 1, 1]: + draw_box( + draw, self._area, img_width, img_height, "Detection Area", (0, 255, 255) + ) + + for label, values in matches.items(): + + # Draw custom label regions/areas + if label in self._label_areas and self._label_areas[label] != [0, 0, 1, 1]: + box_label = f"{label.capitalize()} Detection Area" + draw_box( + draw, + self._label_areas[label], + img_width, + img_height, + box_label, + (0, 255, 0), + ) + + # Draw detected objects + for instance in values: + box_label = f'{label} {instance["score"]:.1f}%' + # Already scaled, use 1 for width and height + draw_box( + draw, + instance["box"], + img_width, + img_height, + box_label, + (255, 255, 0), + ) + + for path in paths: + _LOGGER.info("Saving results image to %s", path) + img.save(path) + + def process_image(self, image): + """Process the image.""" + img = Image.open(io.BytesIO(bytearray(image))) + img_width, img_height = img.size + + if self._aspect and abs((img_width / img_height) - self._aspect) > 0.1: + _LOGGER.debug( + "The image aspect: %s and the detector aspect: %s differ by more than 0.1", + (img_width / img_height), + self._aspect, + ) + + # Run detection + start = time.time() + response = self._doods.detect(image, self._dconfig) + _LOGGER.debug( + "doods detect: %s response: %s duration: %s", + self._dconfig, + response, + time.time() - start, + ) + + matches = {} + total_matches = 0 + + if not response or "error" in response: + if "error" in response: + _LOGGER.error(response["error"]) + self._matches = matches + self._total_matches = total_matches + return + + for detection in response["detections"]: + score = detection["confidence"] + boxes = [ + detection["top"], + detection["left"], + detection["bottom"], + detection["right"], + ] + label = detection["label"] + + # Exclude unlisted labels + if "*" not in self._dconfig and label not in self._dconfig: + continue + + # Exclude matches outside global area definition + if ( + boxes[0] < self._area[0] + or boxes[1] < self._area[1] + or boxes[2] > self._area[2] + or boxes[3] > self._area[3] + ): + continue + + # Exclude matches outside label specific area definition + if self._label_areas and ( + boxes[0] < self._label_areas[label][0] + or boxes[1] < self._label_areas[label][1] + or boxes[2] > self._label_areas[label][2] + or boxes[3] > self._label_areas[label][3] + ): + continue + + if label not in matches: + matches[label] = [] + matches[label].append({"score": float(score), "box": boxes}) + total_matches += 1 + + # Save Images + if total_matches and self._file_out: + paths = [] + for path_template in self._file_out: + if isinstance(path_template, template.Template): + paths.append( + path_template.render(camera_entity=self._camera_entity) + ) + else: + paths.append(path_template) + self._save_image(image, matches, paths) + + self._matches = matches + self._total_matches = total_matches diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json new file mode 100644 index 00000000000000..3e1ce22a230b03 --- /dev/null +++ b/homeassistant/components/doods/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "doods", + "name": "DOODS - Distributed Outside Object Detection Service", + "documentation": "https://www.home-assistant.io/components/doods", + "requirements": [ + "pydoods==1.0.1" + ], + "dependencies": [], + "codeowners": [] +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index da36928d6ba834..8a4bb391da452f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1142,6 +1142,9 @@ pydelijn==0.5.1 # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.doods +pydoods==1.0.1 + # homeassistant.components.android_ip_webcam pydroid-ipcam==0.8 From fc21bdbe273fd15860ff749b824bad78f56177e8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Sep 2019 17:27:56 -0600 Subject: [PATCH 0267/3953] Update PyChromecast (#26594) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 4fb1c67a56e440..84a6a6e2934fd6 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/components/cast", - "requirements": ["pychromecast==4.0.0"], + "requirements": ["pychromecast==4.0.1"], "dependencies": [], "zeroconf": ["_googlecast._tcp.local."], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 8a4bb391da452f..ee42e82f97150a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1110,7 +1110,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==4.0.0 +pychromecast==4.0.1 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07adcfc79f6933..7125d3e9ed5a02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,7 +279,7 @@ pyMetno==0.4.6 pyblackbird==0.5 # homeassistant.components.cast -pychromecast==4.0.0 +pychromecast==4.0.1 # homeassistant.components.deconz pydeconz==62 From 3fda07a4eac66feba04c12fc50365007d7241fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Thu, 12 Sep 2019 02:03:02 +0200 Subject: [PATCH 0268/3953] Bump zigate to 0.3.0 (#26586) * Bump zigate to 0.3.0 --- 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 3095d14061919e..6a2543e8b24533 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.3.0", "zigpy-homeassistant==0.8.0", "zigpy-xbee-homeassistant==0.4.0", - "zigpy-zigate==0.2.0" + "zigpy-zigate==0.3.0" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index ee42e82f97150a..48fe33d9e58527 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2028,7 +2028,7 @@ zigpy-homeassistant==0.8.0 zigpy-xbee-homeassistant==0.4.0 # homeassistant.components.zha -zigpy-zigate==0.2.0 +zigpy-zigate==0.3.0 # homeassistant.components.zoneminder zm-py==0.3.3 From d4c5cf396790e89aedc4693a91cc984e7a335bac Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 12 Sep 2019 00:33:41 +0000 Subject: [PATCH 0269/3953] [ci skip] Translation update --- .../components/deconz/.translations/da.json | 18 +++++++++ .../components/deconz/.translations/fr.json | 40 +++++++++++++++++++ .../components/deconz/.translations/it.json | 29 ++++++++++++++ .../deconz/.translations/zh-Hant.json | 29 ++++++++++++++ .../iaqualink/.translations/fr.json | 7 ++++ .../components/light/.translations/fr.json | 9 +++++ .../light/.translations/zh-Hant.json | 14 +++---- .../solaredge/.translations/zh-Hant.json | 21 ++++++++++ .../components/switch/.translations/fr.json | 17 ++++++++ .../switch/.translations/zh-Hant.json | 17 ++++++++ .../components/velbus/.translations/fr.json | 13 ++++++ 11 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/iaqualink/.translations/fr.json create mode 100644 homeassistant/components/solaredge/.translations/zh-Hant.json create mode 100644 homeassistant/components/switch/.translations/fr.json create mode 100644 homeassistant/components/switch/.translations/zh-Hant.json create mode 100644 homeassistant/components/velbus/.translations/fr.json diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index 1b595924106cfd..6b74c09107a098 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -41,6 +41,24 @@ }, "title": "deCONZ Zigbee gateway" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Begge knapper", + "button_1": "F\u00f8rste knap", + "button_2": "Anden knap", + "button_3": "Tredje knap", + "button_4": "Fjerde knap", + "close": "Luk", + "dim_down": "D\u00e6mp ned", + "dim_up": "D\u00e6mp op", + "left": "Venstre", + "open": "\u00c5ben", + "right": "H\u00f8jre" + }, + "trigger_type": { + "remote_gyro_activated": "Enhed rystet" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 9b98914314a749..0f1277e0b0584b 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -40,5 +40,45 @@ } }, "title": "Passerelle deCONZ Zigbee" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Les deux boutons", + "button_1": "Premier bouton", + "button_2": "Deuxi\u00e8me bouton", + "button_3": "Troisi\u00e8me bouton", + "button_4": "Quatri\u00e8me bouton", + "close": "Ferm\u00e9", + "dim_down": "Assombrir", + "dim_up": "\u00c9claircir", + "left": "Gauche", + "open": "Ouvert", + "right": "Droite", + "turn_off": "\u00c9teint", + "turn_on": "Allum\u00e9" + }, + "trigger_type": { + "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9", + "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", + "remote_button_long_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9 apr\u00e8s appui long", + "remote_button_quadruple_press": "Bouton \"{subtype}\" quadruple cliqu\u00e9", + "remote_button_quintuple_press": "Bouton \"{subtype}\" quintuple cliqu\u00e9", + "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9", + "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", + "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", + "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", + "remote_gyro_activated": "Appareil secou\u00e9" + } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP", + "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ" + }, + "description": "Configurer la visibilit\u00e9 des appareils de type deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 90b85aaeba5f01..f14e7b4c66797b 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -41,6 +41,35 @@ }, "title": "Gateway Zigbee deCONZ" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Entrambi i pulsanti", + "button_1": "Primo pulsante", + "button_2": "Secondo pulsante", + "button_3": "Terzo pulsante", + "button_4": "Quarto pulsante", + "close": "Chiudere", + "dim_down": "Diminuire luminosit\u00e0", + "dim_up": "Aumentare luminosit\u00e0", + "left": "Sinistra", + "open": "Aperto", + "right": "Destra", + "turn_off": "Spegnere", + "turn_on": "Accendere" + }, + "trigger_type": { + "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", + "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", + "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", + "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", + "remote_button_rotated": "Pulsante ruotato \"{subtype}\"", + "remote_button_short_press": "Pulsante \"{subtype}\" premuto", + "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", + "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", + "remote_gyro_activated": "Dispositivo in vibrazione" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 75dcac93dd9eb0..f024386aa0f873 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee \u9598\u9053\u5668" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u5169\u500b\u6309\u9215", + "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "close": "\u95dc\u9589", + "dim_down": "\u8abf\u6697", + "dim_up": "\u8abf\u4eae", + "left": "\u5de6", + "open": "\u958b\u555f", + "right": "\u53f3", + "turn_off": "\u95dc\u9589", + "turn_on": "\u958b\u555f" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", + "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", + "remote_button_long_release": "\u9577\u6309\u5f8c\u91cb\u653e \"{subtype}\" \u6309\u9215", + "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u9ede\u64ca", + "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u9ede\u64ca", + "remote_button_rotated": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215", + "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", + "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", + "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/iaqualink/.translations/fr.json b/homeassistant/components/iaqualink/.translations/fr.json new file mode 100644 index 00000000000000..cf449ebb358cbc --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'une seule connexion iAqualink." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json index 00d03b12d0130a..6ab87274409320 100644 --- a/homeassistant/components/light/.translations/fr.json +++ b/homeassistant/components/light/.translations/fr.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Basculer {entity_name}", + "turn_off": "\u00c9teindre {entity_name}", + "turn_on": "Allumer {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est \u00e9teint", + "is_on": "{entity_name} est allum\u00e9" + }, "trigger_type": { "turn_off": "{name} d\u00e9sactiv\u00e9", "turn_on": "{name} activ\u00e9" diff --git a/homeassistant/components/light/.translations/zh-Hant.json b/homeassistant/components/light/.translations/zh-Hant.json index 269715b7cc33fe..8f5fec9b309e82 100644 --- a/homeassistant/components/light/.translations/zh-Hant.json +++ b/homeassistant/components/light/.translations/zh-Hant.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "\u5207\u63db {name}", - "turn_off": "\u95dc\u9589 {name}", - "turn_on": "\u958b\u555f {name}" + "toggle": "\u5207\u63db {entity_name}", + "turn_off": "\u95dc\u9589 {entity_name}", + "turn_on": "\u958b\u555f {entity_name}" }, "condition_type": { - "is_off": "{name} \u5df2\u95dc\u9589", - "is_on": "{name} \u5df2\u958b\u555f" + "is_off": "{entity_name} \u5df2\u95dc\u9589", + "is_on": "{entity_name} \u5df2\u958b\u555f" }, "trigger_type": { - "turn_off": "\u7531 {name} \u95dc\u9589", - "turn_on": "\u7531 {name} \u958b\u555f" + "turn_off": "{entity_name} \u5df2\u95dc\u9589", + "turn_on": "{entity_name} \u5df2\u958b\u555f" } } } \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/zh-Hant.json b/homeassistant/components/solaredge/.translations/zh-Hant.json new file mode 100644 index 00000000000000..698c28d99bf72a --- /dev/null +++ b/homeassistant/components/solaredge/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "name": "\u5b89\u88dd\u540d\u7a31", + "site_id": "SolarEdge site-id" + }, + "title": "\u8a2d\u5b9a API \u53c3\u6578" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/fr.json b/homeassistant/components/switch/.translations/fr.json new file mode 100644 index 00000000000000..eeffc9262e56ea --- /dev/null +++ b/homeassistant/components/switch/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Basculer {entity_name}", + "turn_off": "\u00c9teindre {entity_name}", + "turn_on": "Allumer {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} \u00e9teint", + "turn_on": "{entity_name} allum\u00e9" + }, + "trigger_type": { + "turn_off": "{entity_name} \u00e9teint", + "turn_on": "{entity_name} allum\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/zh-Hant.json b/homeassistant/components/switch/.translations/zh-Hant.json new file mode 100644 index 00000000000000..0607f4ab08ee3c --- /dev/null +++ b/homeassistant/components/switch/.translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u5207\u63db {entity_name}", + "turn_off": "\u95dc\u9589 {entity_name}", + "turn_on": "\u958b\u555f {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} \u5df2\u95dc\u9589", + "turn_on": "{entity_name} \u5df2\u958b\u555f" + }, + "trigger_type": { + "turn_off": "{entity_name} \u5df2\u95dc\u9589", + "turn_on": "{entity_name} \u5df2\u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/fr.json b/homeassistant/components/velbus/.translations/fr.json new file mode 100644 index 00000000000000..f930df12861abe --- /dev/null +++ b/homeassistant/components/velbus/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Le nom pour cette connexion velbus" + }, + "title": "D\u00e9finir le type de connexion velbus" + } + }, + "title": "Interface Velbus" + } +} \ No newline at end of file From c06487fa5e1b0c2b1d203201bb97b4396ead5113 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Thu, 12 Sep 2019 08:30:04 +0200 Subject: [PATCH 0270/3953] Upgrade youtube_dl to 2019.09.12.1 (#26593) --- 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 419d4b72864400..4e253741b051f8 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.09.01" + "youtube_dl==2019.09.12.1" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 48fe33d9e58527..09b8ffb746a524 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2001,7 +2001,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.09.01 +youtube_dl==2019.09.12.1 # homeassistant.components.zengge zengge==0.2 From 63cf21296ccf303c5ada6b6d2c837744e4ac804d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 12 Sep 2019 08:30:28 +0200 Subject: [PATCH 0271/3953] Update azure-pipelines-release.yml --- 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 63ce5b707cf6b2..7c88e615fa5baa 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -68,8 +68,8 @@ stages: - script: python setup.py sdist bdist_wheel displayName: 'Build package' - script: | - TWINE_USERNAME="$(twineUser)" - TWINE_PASSWORD="$(twinePassword)" + export TWINE_USERNAME="$(twineUser)" + export TWINE_PASSWORD="$(twinePassword)" twine upload dist/* --skip-existing displayName: 'Upload pypi' From dfcffa14fba25ef0e67d02e5c5f45ee4c1202493 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 12 Sep 2019 08:30:57 +0200 Subject: [PATCH 0272/3953] Update azure-pipelines-release.yml --- 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 63ce5b707cf6b2..7c88e615fa5baa 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -68,8 +68,8 @@ stages: - script: python setup.py sdist bdist_wheel displayName: 'Build package' - script: | - TWINE_USERNAME="$(twineUser)" - TWINE_PASSWORD="$(twinePassword)" + export TWINE_USERNAME="$(twineUser)" + export TWINE_PASSWORD="$(twinePassword)" twine upload dist/* --skip-existing displayName: 'Upload pypi' From 41f96a315ee2a0be3c866da2bb10fff49f21a139 Mon Sep 17 00:00:00 2001 From: Gerard Date: Thu, 12 Sep 2019 09:50:02 +0200 Subject: [PATCH 0273/3953] Fix CCM messages (#26589) --- .../bmw_connected_drive/binary_sensor.py | 13 +++++++------ .../components/bmw_connected_drive/sensor.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index c9cc9b2d33373f..c13de4559847ef 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -9,7 +9,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "lids": ["Doors", "opening", "mdi:car-door"], + "lids": ["Doors", "opening", "mdi:car-door-lock"], "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"], @@ -122,8 +122,9 @@ def device_state_attributes(self): 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.has_check_control_messages - if check_control_messages: + check_control_messages = vehicle_state.check_control_messages + has_check_control_messages = vehicle_state.has_check_control_messages + if has_check_control_messages: cbs_list = [] for message in check_control_messages: cbs_list.append(message["ccmDescriptionShort"]) @@ -184,9 +185,9 @@ def _format_cbs_report(self, report): distance = round( self.hass.config.units.length(report.due_distance, LENGTH_KILOMETERS) ) - result[f"{service_type} distance"] = "{} {}".format( - distance, self.hass.config.units.length_unit - ) + result[ + f"{service_type} distance" + ] = f"{distance} {self.hass.config.units.length_unit}" return result def update_callback(self): diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 011908d54585e4..96d541b1955337 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -17,10 +17,10 @@ ATTR_TO_HA_METRIC = { "mileage": ["mdi:speedometer", LENGTH_KILOMETERS], - "remaining_range_total": ["mdi:ruler", LENGTH_KILOMETERS], - "remaining_range_electric": ["mdi:ruler", LENGTH_KILOMETERS], - "remaining_range_fuel": ["mdi:ruler", LENGTH_KILOMETERS], - "max_range_electric": ["mdi:ruler", LENGTH_KILOMETERS], + "remaining_range_total": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "max_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS], "remaining_fuel": ["mdi:gas-station", VOLUME_LITERS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], @@ -28,10 +28,10 @@ ATTR_TO_HA_IMPERIAL = { "mileage": ["mdi:speedometer", LENGTH_MILES], - "remaining_range_total": ["mdi:ruler", LENGTH_MILES], - "remaining_range_electric": ["mdi:ruler", LENGTH_MILES], - "remaining_range_fuel": ["mdi:ruler", LENGTH_MILES], - "max_range_electric": ["mdi:ruler", LENGTH_MILES], + "remaining_range_total": ["mdi:map-marker-distance", LENGTH_MILES], + "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_MILES], + "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_MILES], + "max_range_electric": ["mdi:map-marker-distance", LENGTH_MILES], "remaining_fuel": ["mdi:gas-station", VOLUME_GALLONS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], From c6a73e9ef7f7ea6c70924059a5ce305efd8f3ba8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 12 Sep 2019 13:28:48 +0200 Subject: [PATCH 0274/3953] Update azure-pipelines-wheels.yml --- azure-pipelines-wheels.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index eec3f678981b53..8c534a88d30457 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -45,7 +45,6 @@ jobs: 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} @@ -63,9 +62,12 @@ jobs: 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|# avion|avion|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} sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} + sed -i "s|# VL53L1X|VL53L1X|g" ${requirement_file} + sed -i "s|# bme680|bme680|g" ${requirement_file} done displayName: 'Prepare requirements files for Hass.io' From 284ae015603795445ed352cb4de53103df891d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Thu, 12 Sep 2019 14:00:58 +0200 Subject: [PATCH 0275/3953] Bump zigpy-zigate to 0.3.1 (#26600) * Bump zigpy-zigate to 0.3.1 Bump zigpy-zigate to 0.3.1 (fix Rpi.GPIO dependency) * Bump zigpy-zigate to 0.3.1 Bump zigpy-zigate to 0.3.1 (fix Rpi.GPIO dependency) --- 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 6a2543e8b24533..e78661a04e534d 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.3.0", "zigpy-homeassistant==0.8.0", "zigpy-xbee-homeassistant==0.4.0", - "zigpy-zigate==0.3.0" + "zigpy-zigate==0.3.1" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index 09b8ffb746a524..2075dab9a371e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2028,7 +2028,7 @@ zigpy-homeassistant==0.8.0 zigpy-xbee-homeassistant==0.4.0 # homeassistant.components.zha -zigpy-zigate==0.3.0 +zigpy-zigate==0.3.1 # homeassistant.components.zoneminder zm-py==0.3.3 From 25ef4a156f8584b7bd69d71d4ec9efcb5533f916 Mon Sep 17 00:00:00 2001 From: Gilad Peleg Date: Thu, 12 Sep 2019 19:01:55 +0300 Subject: [PATCH 0276/3953] Improve bluetooth tracker device code (#26067) * Improve bluetooth device tracker code * Don't use set operations * Fix logging template interpolation * Warn if not tracking new devices and not devices to track * Updates due to CR * Fix pylint warning * Fix pylint import warning * Merge with dev --- .../bluetooth_tracker/device_tracker.py | 158 ++++++++++-------- 1 file changed, 92 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index e760f91070a163..8f01036da75911 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -1,25 +1,30 @@ """Tracking for bluetooth devices.""" import logging +from typing import List, Set, Tuple +# pylint: disable=import-error +import bluetooth +from bt_proximity import BluetoothRSSI import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.components.device_tracker.legacy import ( - YAML_DEVICES, - async_load_config, -) from homeassistant.components.device_tracker.const import ( - CONF_TRACK_NEW, CONF_SCAN_INTERVAL, - SCAN_INTERVAL, + CONF_TRACK_NEW, DEFAULT_TRACK_NEW, - SOURCE_TYPE_BLUETOOTH, DOMAIN, + SCAN_INTERVAL, + SOURCE_TYPE_BLUETOOTH, ) -import homeassistant.util.dt as dt_util +from homeassistant.components.device_tracker.legacy import ( + YAML_DEVICES, + async_load_config, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.async_ import run_coroutine_threadsafe +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -42,66 +47,86 @@ ) -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the Bluetooth Scanner.""" - # pylint: disable=import-error - import bluetooth - from bt_proximity import BluetoothRSSI - - def see_device(mac, name, rssi=None): - """Mark a device as seen.""" - attributes = {} - if rssi is not None: - attributes["rssi"] = rssi - see( - mac=f"{BT_PREFIX}{mac}", - host_name=name, - attributes=attributes, - source_type=SOURCE_TYPE_BLUETOOTH, - ) - - device_id = config.get(CONF_DEVICE_ID) - - def discover_devices(): - """Discover Bluetooth devices.""" - result = bluetooth.discover_devices( - duration=8, - lookup_names=True, - flush_cache=True, - lookup_class=False, - device_id=device_id, - ) - _LOGGER.debug("Bluetooth devices discovered = %d", len(result)) - return result - - yaml_path = hass.config.path(YAML_DEVICES) - devs_to_track = [] - devs_donot_track = [] - - # Load all known devices. - # We just need the devices so set consider_home and home range - # to 0 +def is_bluetooth_device(device) -> bool: + """Check whether a device is a bluetooth device by its mac.""" + return device.mac and device.mac[:3].upper() == BT_PREFIX + + +def discover_devices(device_id: int) -> List[Tuple[str, str]]: + """Discover Bluetooth devices.""" + result = bluetooth.discover_devices( + duration=8, + lookup_names=True, + flush_cache=True, + lookup_class=False, + device_id=device_id, + ) + _LOGGER.debug("Bluetooth devices discovered = %d", len(result)) + return result + + +def see_device(see, mac: str, device_name: str, rssi=None) -> None: + """Mark a device as seen.""" + attributes = {} + if rssi is not None: + attributes["rssi"] = rssi + see( + mac=f"{BT_PREFIX}{mac}", + host_name=device_name, + attributes=attributes, + source_type=SOURCE_TYPE_BLUETOOTH, + ) + + +def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[str]]: + """ + Load all known devices. + + We just need the devices so set consider_home and home range to 0 + """ + yaml_path: str = hass.config.path(YAML_DEVICES) + devices_to_track: Set[str] = set() + devices_to_not_track: Set[str] = set() + for device in run_coroutine_threadsafe( async_load_config(yaml_path, hass, 0), hass.loop ).result(): # Check if device is a valid bluetooth device - if device.mac and device.mac[:3].upper() == BT_PREFIX: - if device.track: - devs_to_track.append(device.mac[3:]) - else: - devs_donot_track.append(device.mac[3:]) + if not is_bluetooth_device(device): + continue + + normalized_mac: str = device.mac[3:] + if device.track: + devices_to_track.add(normalized_mac) + else: + devices_to_not_track.add(normalized_mac) + + return devices_to_track, devices_to_not_track + + +def setup_scanner(hass: HomeAssistantType, config: dict, see, discovery_info=None): + """Set up the Bluetooth Scanner.""" + device_id: int = config.get(CONF_DEVICE_ID) + devices_to_track, devices_to_not_track = get_tracking_devices(hass) # If track new devices is true discover new devices on startup. - track_new = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) + track_new: bool = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) + _LOGGER.debug("Tracking new devices = %s", track_new) + + if not devices_to_track and not track_new: + _LOGGER.debug("No Bluetooth devices to track and not tracking new devices") + if track_new: - for dev in discover_devices(): - if dev[0] not in devs_to_track and dev[0] not in devs_donot_track: - devs_to_track.append(dev[0]) - see_device(dev[0], dev[1]) + for mac, device_name in discover_devices(device_id): + if mac not in devices_to_track and mac not in devices_to_not_track: + devices_to_track.add(mac) + see_device(see, mac, device_name) interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) request_rssi = config.get(CONF_REQUEST_RSSI, False) + if request_rssi: + _LOGGER.debug("Detecting RSSI for devices") def update_bluetooth(_): """Update Bluetooth and set timer for the next update.""" @@ -112,21 +137,22 @@ def update_bluetooth_once(): """Lookup Bluetooth device and update status.""" try: if track_new: - for dev in discover_devices(): - if dev[0] not in devs_to_track and dev[0] not in devs_donot_track: - devs_to_track.append(dev[0]) - for mac in devs_to_track: + for mac, device_name in discover_devices(device_id): + if mac not in devices_to_track and mac not in devices_to_not_track: + devices_to_track.add(mac) + + for mac in devices_to_track: _LOGGER.debug("Scanning %s", mac) - result = bluetooth.lookup_name(mac, timeout=5) + device_name = bluetooth.lookup_name(mac, timeout=5) rssi = None if request_rssi: client = BluetoothRSSI(mac) rssi = client.request_rssi() client.close() - if result is None: + if device_name is None: # Could not lookup device name continue - see_device(mac, result, rssi) + see_device(see, mac, device_name, rssi) except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") From 32a6a76d6ac64ba221b6c37344667e97f9920ffb Mon Sep 17 00:00:00 2001 From: PoofyTeddy <33599733+poofyteddy@users.noreply.github.com> Date: Thu, 12 Sep 2019 21:50:24 +0200 Subject: [PATCH 0277/3953] Disable Watson TTS Telemetry (#26253) --- homeassistant/components/watson_tts/tts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 0b7228fb568bcf..a30d08f31f3d9e 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -99,6 +99,7 @@ def get_engine(hass, config): supported_languages = list({s[:5] for s in SUPPORTED_VOICES}) default_voice = config[CONF_VOICE] output_format = config[CONF_OUTPUT_FORMAT] + service.set_default_headers({"x-watson-learning-opt-out": "true"}) return WatsonTTSProvider(service, supported_languages, default_voice, output_format) From 10f742d55258971c1ea5181aefe532b89b15523e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 13 Sep 2019 00:33:08 +0000 Subject: [PATCH 0278/3953] [ci skip] Translation update --- .../arcam_fmj/.translations/fr.json | 5 ++++ .../cert_expiry/.translations/fr.json | 24 +++++++++++++++ .../components/deconz/.translations/ca.json | 29 +++++++++++++++++++ .../components/deconz/.translations/fr.json | 7 +++++ .../components/deconz/.translations/it.json | 4 +-- .../components/deconz/.translations/ko.json | 29 +++++++++++++++++++ .../components/deconz/.translations/pl.json | 17 +++++++++++ .../components/deconz/.translations/ru.json | 28 ++++++++++++++++-- .../geonetnz_quakes/.translations/fr.json | 17 +++++++++++ .../iaqualink/.translations/fr.json | 16 +++++++++- .../components/life360/.translations/fr.json | 1 + .../solaredge/.translations/fr.json | 21 ++++++++++++++ .../components/traccar/.translations/fr.json | 18 ++++++++++++ .../twentemilieu/.translations/fr.json | 23 +++++++++++++++ .../components/unifi/.translations/fr.json | 12 ++++++++ .../components/velbus/.translations/fr.json | 10 ++++++- 16 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/fr.json create mode 100644 homeassistant/components/cert_expiry/.translations/fr.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/fr.json create mode 100644 homeassistant/components/solaredge/.translations/fr.json create mode 100644 homeassistant/components/traccar/.translations/fr.json create mode 100644 homeassistant/components/twentemilieu/.translations/fr.json diff --git a/homeassistant/components/arcam_fmj/.translations/fr.json b/homeassistant/components/arcam_fmj/.translations/fr.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/fr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/fr.json b/homeassistant/components/cert_expiry/.translations/fr.json new file mode 100644 index 00000000000000..a3536902c76d26 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e" + }, + "error": { + "certificate_fetch_failed": "Impossible de r\u00e9cup\u00e9rer le certificat de cette combinaison h\u00f4te / port", + "connection_timeout": "D\u00e9lai d'attente lors de la connexion \u00e0 cet h\u00f4te", + "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e", + "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu" + }, + "step": { + "user": { + "data": { + "host": "Le nom d'h\u00f4te du certificat", + "name": "Le nom du certificat", + "port": "Le port du certificat" + }, + "title": "D\u00e9finir le certificat \u00e0 tester" + } + }, + "title": "Expiration du certificat" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 263730ba5837c2..d36de4acc1e96c 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -41,6 +41,35 @@ }, "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Ambd\u00f3s botons", + "button_1": "Primer bot\u00f3", + "button_2": "Segon bot\u00f3", + "button_3": "Tercer bot\u00f3", + "button_4": "Quart bot\u00f3", + "close": "Tanca", + "dim_down": "Atenua la brillantor", + "dim_up": "Augmenta la brillantor", + "left": "Esquerra", + "open": "Obert", + "right": "Dreta", + "turn_off": "Desactiva", + "turn_on": "Activa" + }, + "trigger_type": { + "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives", + "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament", + "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives", + "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives", + "remote_button_rotated": "Bot\u00f3 \"{subtype}\" girat", + "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", + "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", + "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives", + "remote_gyro_activated": "Dispositiu sacsejat" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 0f1277e0b0584b..cc6d22945dcb79 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -72,6 +72,13 @@ }, "options": { "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP", + "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ" + }, + "description": "Configurer la visibilit\u00e9 des appareils de type deCONZ" + }, "deconz_devices": { "data": { "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP", diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index f14e7b4c66797b..7a2b8832864e2b 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -43,8 +43,8 @@ }, "device_automation": { "trigger_subtype": { - "both_buttons": "Entrambi i pulsanti", - "button_1": "Primo pulsante", + "both_buttons": "Entrambi", + "button_1": "Primo", "button_2": "Secondo pulsante", "button_3": "Terzo pulsante", "button_4": "Quarto pulsante", diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 0ddff8557ec14c..923a2beb2ffba2 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\ub450 \uac1c", + "button_1": "\uccab \ubc88\uc9f8", + "button_2": "\ub450 \ubc88\uc9f8", + "button_3": "\uc138 \ubc88\uc9f8", + "button_4": "\ub124 \ubc88\uc9f8", + "close": "\ub2eb\uae30", + "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30", + "dim_up": "\ubc1d\uac8c \ud558\uae30", + "left": "\uc67c\ucabd", + "open": "\uc5f4\uae30", + "right": "\uc624\ub978\ucabd", + "turn_off": "\ub044\uae30", + "turn_on": "\ucf1c\uae30" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", + "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", + "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", + "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", + "remote_button_rotated": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", + "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984", + "remote_gyro_activated": "\uae30\uae30 \ud754\ub4e6" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 506461ea50e8e0..994e13f567474c 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -41,6 +41,23 @@ }, "title": "Brama deCONZ Zigbee" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Oba przyciski", + "button_1": "Pierwszy przycisk", + "button_2": "Drugi przycisk", + "button_3": "Trzeci przycisk", + "button_4": "Czwarty przycisk", + "close": "Zamknij", + "dim_down": "Przyciemnienie", + "dim_up": "Przyciemnienie", + "left": "Lewo", + "open": "Otw\u00f3rz", + "right": "Prawo", + "turn_off": "Wy\u0142\u0105cz", + "turn_on": "W\u0142\u0105cz" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 23e98919bb8076..92fd1e3e7490c6 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ", @@ -25,7 +25,7 @@ "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" }, - "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ" + "title": "deCONZ" }, "link": { "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb", @@ -41,6 +41,30 @@ }, "title": "deCONZ" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u041e\u0431\u0435 \u043a\u043d\u043e\u043f\u043a\u0438", + "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", + "left": "\u041d\u0430\u043b\u0435\u0432\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", + "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e" + }, + "trigger_type": { + "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", + "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430", + "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/fr.json b/homeassistant/components/geonetnz_quakes/.translations/fr.json new file mode 100644 index 00000000000000..74ae5541754ef7 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Emplacement d\u00e9j\u00e0 enregistr\u00e9" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Rayon" + }, + "title": "Remplissez les d\u00e9tails de votre filtre." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/fr.json b/homeassistant/components/iaqualink/.translations/fr.json index cf449ebb358cbc..97971b99e9f7ab 100644 --- a/homeassistant/components/iaqualink/.translations/fr.json +++ b/homeassistant/components/iaqualink/.translations/fr.json @@ -2,6 +2,20 @@ "config": { "abort": { "already_setup": "Vous ne pouvez configurer qu'une seule connexion iAqualink." - } + }, + "error": { + "connection_failure": "Impossible de se connecter \u00e0 iAqualink. V\u00e9rifiez votre nom d'utilisateur et votre mot de passe." + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur / adresse e-mail" + }, + "description": "Veuillez saisir le nom d'utilisateur et le mot de passe de votre compte iAqualink.", + "title": "Se connecter \u00e0 iAqualink" + } + }, + "title": "Jandy iAqualink" } } \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/fr.json b/homeassistant/components/life360/.translations/fr.json index cb4682fc937103..947425e4807f91 100644 --- a/homeassistant/components/life360/.translations/fr.json +++ b/homeassistant/components/life360/.translations/fr.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Informations d'identification invalides", "invalid_username": "Nom d'utilisateur invalide", + "unexpected": "Erreur inattendue lors de la communication avec le serveur Life360", "user_already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" }, "step": { diff --git a/homeassistant/components/solaredge/.translations/fr.json b/homeassistant/components/solaredge/.translations/fr.json new file mode 100644 index 00000000000000..201e3ff49c6105 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "api_key": "La cl\u00e9 API pour ce site", + "name": "Le nom de cette installation", + "site_id": "L'identifiant de site SolarEdge" + }, + "title": "D\u00e9finir les param\u00e8tres de l'API pour cette installation" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/fr.json b/homeassistant/components/traccar/.translations/fr.json new file mode 100644 index 00000000000000..0948a31739fcea --- /dev/null +++ b/homeassistant/components/traccar/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Votre instance de Home Assistant doit \u00eatre accessible depuis Internet pour recevoir les messages de Traccar.", + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Traccar. \n\n Utilisez l'URL suivante: ` {webhook_url} ` \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails." + }, + "step": { + "user": { + "description": "\u00cates-vous s\u00fbr de vouloir configurer Traccar?", + "title": "Configurer Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/fr.json b/homeassistant/components/twentemilieu/.translations/fr.json new file mode 100644 index 00000000000000..0321a6b73cec35 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adresse d\u00e9j\u00e0 configur\u00e9e." + }, + "error": { + "connection_error": "\u00c9chec de connexion.", + "invalid_address": "Adresse introuvable dans la zone de service de Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Lettre de la maison / suppl\u00e9mentaire", + "house_number": "Num\u00e9ro de maison", + "post_code": "Code postal" + }, + "description": "Configurez Twente Milieu en fournissant des informations sur la collecte des d\u00e9chets sur votre adresse.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index 9e567fcc394a75..8c2526f8a1565a 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -22,5 +22,17 @@ } }, "title": "Contr\u00f4leur UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Temps en secondes depuis la derni\u00e8re vue avant de consid\u00e9rer comme absent", + "track_clients": "Suivre les clients du r\u00e9seau", + "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)", + "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/fr.json b/homeassistant/components/velbus/.translations/fr.json index f930df12861abe..8d93adbf4a92dd 100644 --- a/homeassistant/components/velbus/.translations/fr.json +++ b/homeassistant/components/velbus/.translations/fr.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "port_exists": "Ce port est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "connection_failed": "La connexion velbus a \u00e9chou\u00e9", + "port_exists": "Ce port est d\u00e9j\u00e0 configur\u00e9" + }, "step": { "user": { "data": { - "name": "Le nom pour cette connexion velbus" + "name": "Le nom pour cette connexion velbus", + "port": "Cha\u00eene de connexion" }, "title": "D\u00e9finir le type de connexion velbus" } From 7e7ec498cac6ad34ef6a2a6315bd1c6480cdc8c0 Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Fri, 13 Sep 2019 07:33:14 +0200 Subject: [PATCH 0279/3953] Fix Typo (#26612) --- homeassistant/components/cert_expiry/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json index 8943643e8b392e..3e2fea2342e260 100644 --- a/homeassistant/components/cert_expiry/strings.json +++ b/homeassistant/components/cert_expiry/strings.json @@ -14,7 +14,7 @@ "error": { "host_port_exists": "This host and port combination is already configured", "resolve_failed": "This host can not be resolved", - "connection_timeout": "Timeout whemn connecting to this host", + "connection_timeout": "Timeout when connecting to this host", "certificate_fetch_failed": "Can not fetch certificate from this host and port combination" }, "abort": { From 2f6d567657cbfa564080679a6336331c77416318 Mon Sep 17 00:00:00 2001 From: Gilad Peleg Date: Fri, 13 Sep 2019 22:09:45 +0300 Subject: [PATCH 0280/3953] Refactor Bluetooth Tracker to async (#26614) * Convert bluetooth device tracker to async * WIP * WIP * Fix callback * Fix tracked devices * Perform synchornized updates * Add doc * Run in executor * Improve execution * Improve execution * Don't create a redundant task * Optimize see_device to run concurrently * Remove redundant initialization scan --- .../bluetooth_tracker/device_tracker.py | 128 +++++++++++------- 1 file changed, 76 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 8f01036da75911..6a26775b0a8ac7 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -1,6 +1,7 @@ """Tracking for bluetooth devices.""" +import asyncio import logging -from typing import List, Set, Tuple +from typing import List, Set, Tuple, Optional # pylint: disable=import-error import bluetooth @@ -21,10 +22,9 @@ async_load_config, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.util.async_ import run_coroutine_threadsafe -import homeassistant.util.dt as dt_util + _LOGGER = logging.getLogger(__name__) @@ -65,12 +65,15 @@ def discover_devices(device_id: int) -> List[Tuple[str, str]]: return result -def see_device(see, mac: str, device_name: str, rssi=None) -> None: +async def see_device( + hass: HomeAssistantType, async_see, mac: str, device_name: str, rssi=None +) -> None: """Mark a device as seen.""" attributes = {} if rssi is not None: attributes["rssi"] = rssi - see( + + await async_see( mac=f"{BT_PREFIX}{mac}", host_name=device_name, attributes=attributes, @@ -78,90 +81,111 @@ def see_device(see, mac: str, device_name: str, rssi=None) -> None: ) -def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[str]]: +async def get_tracking_devices(hass: HomeAssistantType) -> Tuple[Set[str], Set[str]]: """ Load all known devices. We just need the devices so set consider_home and home range to 0 """ yaml_path: str = hass.config.path(YAML_DEVICES) - devices_to_track: Set[str] = set() - devices_to_not_track: Set[str] = set() - - for device in run_coroutine_threadsafe( - async_load_config(yaml_path, hass, 0), hass.loop - ).result(): - # Check if device is a valid bluetooth device - if not is_bluetooth_device(device): - continue - - normalized_mac: str = device.mac[3:] - if device.track: - devices_to_track.add(normalized_mac) - else: - devices_to_not_track.add(normalized_mac) + + devices = await async_load_config(yaml_path, hass, 0) + bluetooth_devices = [device for device in devices if is_bluetooth_device(device)] + + devices_to_track: Set[str] = { + device.mac[3:] for device in bluetooth_devices if device.track + } + devices_to_not_track: Set[str] = { + device.mac[3:] for device in bluetooth_devices if not device.track + } return devices_to_track, devices_to_not_track -def setup_scanner(hass: HomeAssistantType, config: dict, see, discovery_info=None): +def lookup_name(mac: str) -> Optional[str]: + """Lookup a Bluetooth device name.""" + _LOGGER.debug("Scanning %s", mac) + return bluetooth.lookup_name(mac, timeout=5) + + +async def async_setup_scanner( + hass: HomeAssistantType, config: dict, async_see, discovery_info=None +): """Set up the Bluetooth Scanner.""" device_id: int = config.get(CONF_DEVICE_ID) - devices_to_track, devices_to_not_track = get_tracking_devices(hass) + interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + request_rssi = config.get(CONF_REQUEST_RSSI, False) + update_bluetooth_lock = asyncio.Lock() # If track new devices is true discover new devices on startup. track_new: bool = config.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) - _LOGGER.debug("Tracking new devices = %s", track_new) + _LOGGER.debug("Tracking new devices is set to %s", track_new) + + devices_to_track, devices_to_not_track = await get_tracking_devices(hass) if not devices_to_track and not track_new: _LOGGER.debug("No Bluetooth devices to track and not tracking new devices") - if track_new: - for mac, device_name in discover_devices(device_id): - if mac not in devices_to_track and mac not in devices_to_not_track: - devices_to_track.add(mac) - see_device(see, mac, device_name) - - interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - - request_rssi = config.get(CONF_REQUEST_RSSI, False) if request_rssi: _LOGGER.debug("Detecting RSSI for devices") - def update_bluetooth(_): - """Update Bluetooth and set timer for the next update.""" - update_bluetooth_once() - track_point_in_utc_time(hass, update_bluetooth, dt_util.utcnow() + interval) + async def perform_bluetooth_update(): + """Discover Bluetooth devices and update status.""" + + _LOGGER.debug("Performing Bluetooth devices discovery and update") + tasks = [] - def update_bluetooth_once(): - """Lookup Bluetooth device and update status.""" try: if track_new: - for mac, device_name in discover_devices(device_id): + devices = await hass.async_add_executor_job(discover_devices, device_id) + for mac, device_name in devices: if mac not in devices_to_track and mac not in devices_to_not_track: devices_to_track.add(mac) for mac in devices_to_track: - _LOGGER.debug("Scanning %s", mac) - device_name = bluetooth.lookup_name(mac, timeout=5) + device_name = await hass.async_add_executor_job(lookup_name, mac) + if device_name is None: + # Could not lookup device name + continue + rssi = None if request_rssi: client = BluetoothRSSI(mac) - rssi = client.request_rssi() + rssi = await hass.async_add_executor_job(client.request_rssi) client.close() - if device_name is None: - # Could not lookup device name - continue - see_device(see, mac, device_name, rssi) + + tasks.append(see_device(hass, async_see, mac, device_name, rssi)) + + if tasks: + await asyncio.wait(tasks) + except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") - def handle_update_bluetooth(call): + async def update_bluetooth(now=None): + """Lookup Bluetooth devices and update status.""" + + # If an update is in progress, we don't do anything + if update_bluetooth_lock.locked(): + _LOGGER.debug( + "Previous execution of update_bluetooth is taking longer than the scheduled update of interval %s", + interval, + ) + return + + async with update_bluetooth_lock: + await perform_bluetooth_update() + + async def handle_manual_update_bluetooth(call): """Update bluetooth devices on demand.""" - update_bluetooth_once() - update_bluetooth(dt_util.utcnow()) + await update_bluetooth() + + hass.async_create_task(update_bluetooth()) + async_track_time_interval(hass, update_bluetooth, interval) - hass.services.register(DOMAIN, "bluetooth_tracker_update", handle_update_bluetooth) + hass.services.async_register( + DOMAIN, "bluetooth_tracker_update", handle_manual_update_bluetooth + ) return True From e4bf2c47168889ca6339524fa66bd1a194b4250d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 13 Sep 2019 22:04:02 +0200 Subject: [PATCH 0281/3953] Update azure-pipelines-wheels.yml --- azure-pipelines-wheels.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 8c534a88d30457..0c614a9dab2561 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -67,7 +67,6 @@ jobs: sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} - sed -i "s|# VL53L1X|VL53L1X|g" ${requirement_file} sed -i "s|# bme680|bme680|g" ${requirement_file} done displayName: 'Prepare requirements files for Hass.io' From 357f2421c80c92f1de857b71d43bfd6a1add1a96 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 13 Sep 2019 22:29:39 +0200 Subject: [PATCH 0282/3953] Update azure-pipelines-wheels.yml for Azure Pipelines --- azure-pipelines-wheels.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 0c614a9dab2561..42815d8c8ae779 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -68,5 +68,9 @@ jobs: sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} sed -i "s|# bme680|bme680|g" ${requirement_file} + + if [[ "$(buildArch)" =~ arm ]]; then + sed -i "s|# VL53L1X|VL53L1X|g" ${requirement_file} + fi done displayName: 'Prepare requirements files for Hass.io' From fb1acfccc93063d0e479265af69d6564ea192415 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 14 Sep 2019 00:16:37 +0200 Subject: [PATCH 0283/3953] deCONZ - create deconz_events through sensor platform (#26592) * Move event creation into sensor platform where it belongs * Fixed the weird failing test observed during device automation PR --- homeassistant/components/deconz/gateway.py | 23 +------ homeassistant/components/deconz/sensor.py | 9 +++ tests/components/deconz/test_deconz_event.py | 60 +++++++++++++++++ tests/components/deconz/test_gateway.py | 68 -------------------- 4 files changed, 70 insertions(+), 90 deletions(-) create mode 100644 tests/components/deconz/test_deconz_event.py diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 35cf63fc3d228a..a090dca0d0c632 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -3,7 +3,6 @@ import async_timeout from pydeconz import DeconzSession, errors -from pydeconz.sensor import Switch from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_HOST @@ -29,10 +28,9 @@ DEFAULT_ALLOW_DECONZ_GROUPS, DOMAIN, NEW_DEVICE, - NEW_SENSOR, SUPPORTED_PLATFORMS, ) -from .deconz_event import DeconzEvent + from .errors import AuthenticationRequired, CannotConnect @@ -119,14 +117,6 @@ async def async_setup(self): ) ) - self.listeners.append( - async_dispatcher_connect( - hass, self.async_signal_new_device(NEW_SENSOR), self.async_add_remote - ) - ) - - self.async_add_remote(self.api.sensors.values()) - self.api.start() self.config_entry.add_update_listener(self.async_new_address) @@ -185,17 +175,6 @@ def async_add_device_callback(self, device_type, device): self.hass, self.async_signal_new_device(device_type), device ) - @callback - def async_add_remote(self, sensors): - """Set up remote from deCONZ.""" - for sensor in sensors: - if sensor.type in Switch.ZHATYPE and not ( - not self.option_allow_clip_sensor and sensor.type.startswith("CLIP") - ): - event = DeconzEvent(sensor, self) - self.hass.async_create_task(event.async_update_device_registry()) - self.events.append(event) - @callback def shutdown(self, event): """Wrap the call to deconz.close. diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index d84a47c6aaf8e3..a6138087f1ce40 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -13,6 +13,7 @@ from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice +from .deconz_event import DeconzEvent from .gateway import get_gateway_from_config_entry, DeconzEntityHandler ATTR_CURRENT = "current" @@ -42,6 +43,14 @@ def async_add_sensor(sensors): if not sensor.BINARY: if sensor.type in Switch.ZHATYPE: + + if gateway.option_allow_clip_sensor or not sensor.type.startswith( + "CLIP" + ): + event = DeconzEvent(sensor, gateway) + hass.async_create_task(event.async_update_device_registry()) + gateway.events.append(event) + if sensor.battery: entities.append(DeconzBattery(sensor, gateway)) diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py new file mode 100644 index 00000000000000..72966ba6c66b09 --- /dev/null +++ b/tests/components/deconz/test_deconz_event.py @@ -0,0 +1,60 @@ +"""Test deCONZ remote events.""" +from unittest.mock import Mock + +from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT, DeconzEvent +from homeassistant.core import callback + + +async def test_create_event(hass): + """Successfully created a deCONZ event.""" + mock_remote = Mock() + mock_remote.name = "Name" + + mock_gateway = Mock() + mock_gateway.hass = hass + + event = DeconzEvent(mock_remote, mock_gateway) + + assert event.event_id == "name" + + +async def test_update_event(hass): + """Successfully update a deCONZ event.""" + mock_remote = Mock() + mock_remote.name = "Name" + + mock_gateway = Mock() + mock_gateway.hass = hass + + event = DeconzEvent(mock_remote, mock_gateway) + mock_remote.changed_keys = {"state": True} + + calls = [] + + @callback + def listener(event): + """Mock listener.""" + calls.append(event) + + unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, listener) + + event.async_update_callback() + await hass.async_block_till_done() + + assert len(calls) == 1 + + unsub() + + +async def test_remove_event(hass): + """Successfully update a deCONZ event.""" + mock_remote = Mock() + mock_remote.name = "Name" + + mock_gateway = Mock() + mock_gateway.hass = hass + + event = DeconzEvent(mock_remote, mock_gateway) + event.async_will_remove_from_hass() + + assert event._device is None diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index c17aa0b66390ca..d84706430f465b 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -126,24 +126,6 @@ async def test_add_device(hass): assert len(mock_dispatch_send.mock_calls[0]) == 3 -@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") -async def test_add_remote(hass): - """Successful add remote.""" - entry = Mock() - entry.data = ENTRY_CONFIG - - remote = Mock() - remote.name = "name" - remote.type = "ZHASwitch" - remote.register_async_callback = Mock() - - deconz_gateway = gateway.DeconzGateway(hass, entry) - deconz_gateway.async_add_remote([remote]) - await hass.async_block_till_done() - - assert len(deconz_gateway.events) == 1 - - async def test_shutdown(): """Successful shutdown.""" hass = Mock() @@ -218,53 +200,3 @@ async def test_get_gateway_fails_cannot_connect(hass): side_effect=pydeconz.errors.RequestError, ), pytest.raises(errors.CannotConnect): assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False - - -@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") -async def test_create_event(hass): - """Successfully created a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = gateway.DeconzEvent(mock_remote, mock_gateway) - await hass.async_block_till_done() - - assert event.event_id == "name" - - -@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") -async def test_update_event(hass): - """Successfully update a deCONZ event.""" - hass.bus.async_fire = Mock() - - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = gateway.DeconzEvent(mock_remote, mock_gateway) - await hass.async_block_till_done() - mock_remote.changed_keys = {"state": True} - event.async_update_callback() - - assert len(hass.bus.async_fire.mock_calls) == 1 - - -@pytest.mark.skip(reason="fails for unkown reason, will refactor in a separate PR") -async def test_remove_event(hass): - """Successfully update a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = gateway.DeconzEvent(mock_remote, mock_gateway) - await hass.async_block_till_done() - event.async_will_remove_from_hass() - - assert event._device is None From 6a9ecf00154d51dbd530c2d63ed70aa1440e052f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 14 Sep 2019 00:32:15 +0000 Subject: [PATCH 0284/3953] [ci skip] Translation update --- .../components/adguard/.translations/es.json | 13 +++++++++ .../cert_expiry/.translations/en.json | 2 +- .../cert_expiry/.translations/it.json | 2 +- .../components/deconz/.translations/es.json | 11 +++++++ .../components/deconz/.translations/pl.json | 12 ++++++++ .../components/deconz/.translations/sl.json | 29 +++++++++++++++++++ .../iaqualink/.translations/es.json | 12 ++++++++ .../iaqualink/.translations/sl.json | 21 ++++++++++++++ .../components/life360/.translations/es.json | 3 +- .../components/light/.translations/es.json | 12 ++++++-- .../components/light/.translations/sl.json | 9 ++++++ .../components/linky/.translations/es.json | 15 ++++++++++ .../solaredge/.translations/es.json | 13 +++++++++ .../solaredge/.translations/sl.json | 21 ++++++++++++++ .../components/switch/.translations/es.json | 16 ++++++++++ .../components/switch/.translations/sl.json | 17 +++++++++++ .../components/traccar/.translations/es.json | 12 +++++++- .../twentemilieu/.translations/es.json | 3 ++ .../components/velbus/.translations/es.json | 3 ++ 19 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/iaqualink/.translations/es.json create mode 100644 homeassistant/components/iaqualink/.translations/sl.json create mode 100644 homeassistant/components/linky/.translations/es.json create mode 100644 homeassistant/components/solaredge/.translations/es.json create mode 100644 homeassistant/components/solaredge/.translations/sl.json create mode 100644 homeassistant/components/switch/.translations/es.json create mode 100644 homeassistant/components/switch/.translations/sl.json diff --git a/homeassistant/components/adguard/.translations/es.json b/homeassistant/components/adguard/.translations/es.json index 971d38f9ab2d26..46f21d96195465 100644 --- a/homeassistant/components/adguard/.translations/es.json +++ b/homeassistant/components/adguard/.translations/es.json @@ -2,6 +2,19 @@ "config": { "abort": { "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente." + }, + "error": { + "connection_error": "No se conect\u00f3." + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index b6aa1cefb02aed..873dfee9a92bb9 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Can not fetch certificate from this host and port combination", - "connection_timeout": "Timeout whemn connecting to this host", + "connection_timeout": "Timeout when connecting to this host", "host_port_exists": "This host and port combination is already configured", "resolve_failed": "This host can not be resolved" }, diff --git a/homeassistant/components/cert_expiry/.translations/it.json b/homeassistant/components/cert_expiry/.translations/it.json index 9135ed3b478592..73749382dd9bca 100644 --- a/homeassistant/components/cert_expiry/.translations/it.json +++ b/homeassistant/components/cert_expiry/.translations/it.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta", - "connection_timeout": "Tempo scaduto durante la connessione a questo host", + "connection_timeout": "Tempo scaduto collegandosi a questo host", "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", "resolve_failed": "Questo host non pu\u00f2 essere risolto" }, diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 8bcf03914cee84..3d2b3f17814a49 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -39,6 +39,17 @@ }, "title": "Pasarela Zigbee deCONZ" }, + "device_automation": { + "trigger_subtype": { + "close": "Cerrar", + "dim_down": "Bajar la intensidad", + "dim_up": "Subir la intensidad", + "left": "Izquierda", + "right": "Derecha", + "turn_off": "Apagar", + "turn_on": "Encender" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 994e13f567474c..70c33cf3c02f4f 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -56,6 +56,18 @@ "right": "Prawo", "turn_off": "Wy\u0142\u0105cz", "turn_on": "W\u0142\u0105cz" + }, + "trigger_type": { + "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "Przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "Przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_rotated": "Przycisk obr\u00f3cony \"{subtype}\"", + "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", + "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", + "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", + "remote_gyro_activated": "Urz\u0105dzenie potrz\u0105\u015bni\u0119te" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 86210b2e6c1065..9aebb2a556f6cd 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee prehod" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Oba gumba", + "button_1": "Prvi gumb", + "button_2": "Drugi gumb", + "button_3": "Tretji gumb", + "button_4": "\u010cetrti gumb", + "close": "Zapri", + "dim_down": "Zatemnite", + "dim_up": "pove\u010dajte mo\u010d", + "left": "Levo", + "open": "Odprto", + "right": "Desno", + "turn_off": "Ugasni", + "turn_on": "Pri\u017egi" + }, + "trigger_type": { + "remote_button_double_press": "Dvakrat kliknete gumb \"{subtype}\"", + "remote_button_long_press": "\"{subtype}\" gumb neprekinjeno pritisnjen", + "remote_button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku", + "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", + "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", + "remote_button_rotated": "Gumb \"{subtype}\" zasukan", + "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", + "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", + "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", + "remote_gyro_activated": "Naprava se je pretresla" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/iaqualink/.translations/es.json b/homeassistant/components/iaqualink/.translations/es.json new file mode 100644 index 00000000000000..7326d80497be4a --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/es.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario / correo electr\u00f3nico" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/sl.json b/homeassistant/components/iaqualink/.translations/sl.json new file mode 100644 index 00000000000000..e2a7f94b3d8a70 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Konfigurirate lahko samo eno povezavo iAqualink." + }, + "error": { + "connection_failure": "Ne morete vzpostaviti povezave z iAqualink. Preverite va\u0161e uporabni\u0161ko ime in geslo." + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime / e-po\u0161tni naslov" + }, + "description": "Prosimo, vnesite uporabni\u0161ko ime in geslo za iAqualink ra\u010dun.", + "title": "Pove\u017eite se z iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/es.json b/homeassistant/components/life360/.translations/es.json index 8fc70a60a052e6..28999de5e81b94 100644 --- a/homeassistant/components/life360/.translations/es.json +++ b/homeassistant/components/life360/.translations/es.json @@ -9,7 +9,8 @@ }, "error": { "invalid_credentials": "Credenciales no v\u00e1lidas", - "invalid_username": "Nombre de usuario no v\u00e1lido" + "invalid_username": "Nombre de usuario no v\u00e1lido", + "user_already_configured": "La cuenta ya ha sido configurada" }, "step": { "user": { diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json index b56875453dd67e..93dfc65bbe1c0a 100644 --- a/homeassistant/components/light/.translations/es.json +++ b/homeassistant/components/light/.translations/es.json @@ -1,8 +1,16 @@ { "device_automation": { + "action_type": { + "turn_off": "Apagar {entity_name}", + "turn_on": "Encender {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagada", + "is_on": "{entity_name} est\u00e1 encendida" + }, "trigger_type": { - "turn_off": "{nombre} desactivado", - "turn_on": "{nombre} activado" + "turn_off": "{entity_name} apagada", + "turn_on": "{entity_name} encendida" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/sl.json b/homeassistant/components/light/.translations/sl.json index 68e770e88731b8..afd59d619e0cdc 100644 --- a/homeassistant/components/light/.translations/sl.json +++ b/homeassistant/components/light/.translations/sl.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Preklopite {entity_name}", + "turn_off": "Izklopite {entity_name}", + "turn_on": "Vklopite {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen" + }, "trigger_type": { "turn_off": "{name} izklopljeno", "turn_on": "{name} vklopljeno" diff --git a/homeassistant/components/linky/.translations/es.json b/homeassistant/components/linky/.translations/es.json new file mode 100644 index 00000000000000..7c0d17c8a8f1e6 --- /dev/null +++ b/homeassistant/components/linky/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "username_exists": "Cuenta ya configurada" + }, + "error": { + "wrong_login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" + }, + "step": { + "user": { + "description": "Introduzca sus credenciales" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/es.json b/homeassistant/components/solaredge/.translations/es.json new file mode 100644 index 00000000000000..9f52511a165cff --- /dev/null +++ b/homeassistant/components/solaredge/.translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "La clave de la API para este sitio", + "name": "El nombre de esta instalaci\u00f3n" + }, + "title": "Definir los par\u00e1metros de la API para esta instalaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/sl.json b/homeassistant/components/solaredge/.translations/sl.json new file mode 100644 index 00000000000000..ebfefe40b0e54d --- /dev/null +++ b/homeassistant/components/solaredge/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Ta site_id je \u017ee nastavljen" + }, + "error": { + "site_exists": "Ta site_id je \u017ee nastavljen" + }, + "step": { + "user": { + "data": { + "api_key": "API klju\u010d za to stran", + "name": "Ime te namestitve", + "site_id": "SolarEdge site-ID" + }, + "title": "Dolo\u010dite parametre API za to namestitev" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/es.json b/homeassistant/components/switch/.translations/es.json new file mode 100644 index 00000000000000..6749eab129331a --- /dev/null +++ b/homeassistant/components/switch/.translations/es.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Apagar {entity_name}", + "turn_on": "Encender {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} apagado", + "turn_on": "{entity_name} encendido" + }, + "trigger_type": { + "turn_off": "{entity_name} apagado", + "turn_on": "{entity_name} encendido" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/sl.json b/homeassistant/components/switch/.translations/sl.json new file mode 100644 index 00000000000000..38edfe5a1953d7 --- /dev/null +++ b/homeassistant/components/switch/.translations/sl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Preklopite {entity_name}", + "turn_off": "Izklopite {entity_name}", + "turn_on": "Vklopite {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} izklopljen", + "turn_on": "{entity_name} vklopljen" + }, + "trigger_type": { + "turn_off": "{entity_name} izklopljen", + "turn_on": "{entity_name} vklopljen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/es.json b/homeassistant/components/traccar/.translations/es.json index ab8c0e70cd42e4..b0b65a10c83cc9 100644 --- a/homeassistant/components/traccar/.translations/es.json +++ b/homeassistant/components/traccar/.translations/es.json @@ -1,7 +1,17 @@ { "config": { "abort": { - "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Traccar." + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Traccar.", + "one_instance_allowed": "S\u00f3lo se necesita una \u00fanica instancia." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, necesitar\u00e1 configurar la funci\u00f3n de webhook en Traccar.\n\nUtilice la siguiente url: ``{webhook_url}``\n\nConsulte la [documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1 seguro de querer configurar Traccar?", + "title": "Configurar Traccar" + } } } } \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/es.json b/homeassistant/components/twentemilieu/.translations/es.json index 02dcb71f54e5ff..902e28b2080d87 100644 --- a/homeassistant/components/twentemilieu/.translations/es.json +++ b/homeassistant/components/twentemilieu/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "address_exists": "Direcci\u00f3n ya configurada." + }, "error": { "connection_error": "No se conect\u00f3." }, diff --git a/homeassistant/components/velbus/.translations/es.json b/homeassistant/components/velbus/.translations/es.json index e60ef7b4c676f1..1acaaa53ab2f11 100644 --- a/homeassistant/components/velbus/.translations/es.json +++ b/homeassistant/components/velbus/.translations/es.json @@ -3,6 +3,9 @@ "abort": { "port_exists": "Este puerto ya est\u00e1 configurado" }, + "error": { + "port_exists": "Este puerto ya est\u00e1 configurado" + }, "step": { "user": { "data": { From bca7363a80f5f0bf664a4196333e973c9dd6c348 Mon Sep 17 00:00:00 2001 From: Dan Ponte Date: Fri, 13 Sep 2019 22:06:09 -0400 Subject: [PATCH 0285/3953] zha ZCL color loop effect (#26549) * Initial implementation of ZCL color loop effect * Fix linter complaints * Use const for action * Reformat with Black * Cleanup after review. * Handle effect being None --- homeassistant/components/zha/light.py | 64 +++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 379f69febbb82f..27257e5039aee2 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -27,9 +27,15 @@ DEFAULT_DURATION = 5 +CAPABILITIES_COLOR_LOOP = 0x4 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 +UPDATE_COLORLOOP_ACTION = 0x1 +UPDATE_COLORLOOP_DIRECTION = 0x2 +UPDATE_COLORLOOP_TIME = 0x4 +UPDATE_COLORLOOP_HUE = 0x8 + UNSUPPORTED_ATTRIBUTE = 0x86 SCAN_INTERVAL = timedelta(minutes=60) PARALLEL_UPDATES = 5 @@ -85,6 +91,8 @@ def __init__(self, unique_id, zha_device, channels, **kwargs): self._color_temp = None self._hs_color = None self._brightness = None + self._effect_list = [] + self._effect = None 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) @@ -103,6 +111,10 @@ def __init__(self, unique_id, zha_device, channels, **kwargs): self._supported_features |= light.SUPPORT_COLOR self._hs_color = (0, 0) + if color_capabilities & CAPABILITIES_COLOR_LOOP: + self._supported_features |= light.SUPPORT_EFFECT + self._effect_list.append(light.EFFECT_COLORLOOP) + @property def is_on(self) -> bool: """Return true if entity is on.""" @@ -141,6 +153,16 @@ def color_temp(self): """Return the CT color value in mireds.""" return self._color_temp + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effect_list + + @property + def effect(self): + """Return the current effect.""" + return self._effect + @property def supported_features(self): """Flag supported features.""" @@ -173,12 +195,15 @@ def async_restore_last_state(self, last_state): self._color_temp = last_state.attributes["color_temp"] if "hs_color" in last_state.attributes: self._hs_color = last_state.attributes["hs_color"] + if "effect" in last_state.attributes: + self._effect = last_state.attributes["effect"] async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) duration = transition * 10 if transition else DEFAULT_DURATION brightness = kwargs.get(light.ATTR_BRIGHTNESS) + effect = kwargs.get(light.ATTR_EFFECT) t_log = {} if ( @@ -234,6 +259,36 @@ async def async_turn_on(self, **kwargs): return self._hs_color = hs_color + if ( + effect == light.EFFECT_COLORLOOP + and self.supported_features & light.SUPPORT_EFFECT + ): + result = await self._color_channel.color_loop_set( + UPDATE_COLORLOOP_ACTION + | UPDATE_COLORLOOP_DIRECTION + | UPDATE_COLORLOOP_TIME, + 0x2, # start from current hue + 0x1, # only support up + transition if transition else 7, # transition + 0, # no hue + ) + t_log["color_loop_set"] = result + self._effect = light.EFFECT_COLORLOOP + elif ( + self._effect == light.EFFECT_COLORLOOP + and effect != light.EFFECT_COLORLOOP + and self.supported_features & light.SUPPORT_EFFECT + ): + result = await self._color_channel.color_loop_set( + UPDATE_COLORLOOP_ACTION, + 0x0, + 0x0, + 0x0, + 0x0, # update action only, action off, no dir,time,hue + ) + t_log["color_loop_set"] = result + self._effect = None + self.debug("turned on: %s", t_log) self.async_schedule_update_ha_state() @@ -292,6 +347,15 @@ async def async_get_state(self, from_cache=True): self._hs_color = color_util.color_xy_to_hs( float(color_x / 65535), float(color_y / 65535) ) + if ( + color_capabilities is not None + and color_capabilities & CAPABILITIES_COLOR_LOOP + ): + color_loop_active = await self._color_channel.get_attribute_value( + "color_loop_active", from_cache=from_cache + ) + if color_loop_active is not None and color_loop_active == 1: + self._effect = light.EFFECT_COLORLOOP async def refresh(self, time): """Call async_get_state at an interval.""" From a71cd6e90e54c2411dc04a689a7c7a3f170899ee Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Fri, 13 Sep 2019 22:05:47 -0700 Subject: [PATCH 0286/3953] Add iaqualink binary sensor and unique_id (#26616) * Add binary_platform to iaqualink integration, add unique_id * Revert Mixin changes, move self.dev to AqualinkEntity * Style fixes --- .coveragerc | 1 + .../components/iaqualink/__init__.py | 20 ++++++++ .../components/iaqualink/binary_sensor.py | 48 +++++++++++++++++++ homeassistant/components/iaqualink/climate.py | 14 +----- homeassistant/components/iaqualink/light.py | 8 +--- homeassistant/components/iaqualink/sensor.py | 6 --- homeassistant/components/iaqualink/switch.py | 8 +--- 7 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/iaqualink/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 0c6ac82894a6f4..fef77c8a247bba 100644 --- a/.coveragerc +++ b/.coveragerc @@ -289,6 +289,7 @@ omit = homeassistant/components/hydrawise/* homeassistant/components/hyperion/light.py homeassistant/components/ialarm/alarm_control_panel.py + homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py homeassistant/components/iaqualink/light.py homeassistant/components/iaqualink/sensor.py diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 56a39df64c9dea..dec91186be2869 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -7,7 +7,9 @@ import voluptuous as vol from iaqualink import ( + AqualinkBinarySensor, AqualinkClient, + AqualinkDevice, AqualinkLight, AqualinkLoginException, AqualinkSensor, @@ -16,6 +18,7 @@ ) from homeassistant import config_entries +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -76,6 +79,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None password = entry.data[CONF_PASSWORD] # These will contain the initialized devices + binary_sensors = hass.data[DOMAIN][BINARY_SENSOR_DOMAIN] = [] climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = [] lights = hass.data[DOMAIN][LIGHT_DOMAIN] = [] sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] @@ -103,12 +107,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None climates += [dev] elif isinstance(dev, AqualinkLight): lights += [dev] + elif isinstance(dev, AqualinkBinarySensor): + binary_sensors += [dev] elif isinstance(dev, AqualinkSensor): sensors += [dev] elif isinstance(dev, AqualinkToggle): switches += [dev] forward_setup = hass.config_entries.async_forward_entry_setup + if binary_sensors: + _LOGGER.debug("Got %s binary sensors: %s", len(binary_sensors), binary_sensors) + hass.async_create_task(forward_setup(entry, BINARY_SENSOR_DOMAIN)) if climates: _LOGGER.debug("Got %s climates: %s", len(climates), climates) hass.async_create_task(forward_setup(entry, CLIMATE_DOMAIN)) @@ -138,6 +147,8 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo tasks = [] + if hass.data[DOMAIN][BINARY_SENSOR_DOMAIN]: + tasks += [forward_unload(entry, BINARY_SENSOR_DOMAIN)] if hass.data[DOMAIN][CLIMATE_DOMAIN]: tasks += [forward_unload(entry, CLIMATE_DOMAIN)] if hass.data[DOMAIN][LIGHT_DOMAIN]: @@ -174,6 +185,10 @@ class AqualinkEntity(Entity): class. """ + def __init__(self, dev: AqualinkDevice): + """Initialize the entity.""" + self.dev = dev + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) @@ -190,3 +205,8 @@ def should_poll(self) -> bool: updates on a timer. """ return False + + @property + def unique_id(self) -> str: + """Return a unique identifier for this entity.""" + return f"{self.dev.system.serial}_{self.dev.name}" diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py new file mode 100644 index 00000000000000..09c9322a58764b --- /dev/null +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -0,0 +1,48 @@ +"""Support for Aqualink temperature sensors.""" +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_COLD, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import AqualinkEntity +from .const import DOMAIN as AQUALINK_DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up discovered binary sensors.""" + devs = [] + for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: + devs.append(HassAqualinkBinarySensor(dev)) + async_add_entities(devs, True) + + +class HassAqualinkBinarySensor(AqualinkEntity, BinarySensorDevice): + """Representation of a binary sensor.""" + + @property + def name(self) -> str: + """Return the name of the binary sensor.""" + return self.dev.label + + @property + def is_on(self) -> bool: + """Return whether the binary sensor is on or not.""" + return self.dev.is_on + + @property + def device_class(self) -> str: + """Return the class of the binary sensor.""" + if self.name == "Freeze Protection": + return DEVICE_CLASS_COLD + return None diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 321c54329a2878..f41d17837c2f6c 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -2,13 +2,7 @@ import logging from typing import List, Optional -from iaqualink import ( - AqualinkState, - AqualinkHeater, - AqualinkPump, - AqualinkSensor, - AqualinkThermostat, -) +from iaqualink import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState from iaqualink.const import ( AQUALINK_TEMP_CELSIUS_HIGH, AQUALINK_TEMP_CELSIUS_LOW, @@ -45,13 +39,9 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkThermostat(ClimateDevice, AqualinkEntity): +class HassAqualinkThermostat(AqualinkEntity, ClimateDevice): """Representation of a thermostat.""" - def __init__(self, dev: AqualinkThermostat): - """Initialize the thermostat.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the thermostat.""" diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index fbfb10783ee4a1..813af7863f1700 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -1,7 +1,7 @@ """Support for Aqualink pool lights.""" import logging -from iaqualink import AqualinkLight, AqualinkLightEffect +from iaqualink import AqualinkLightEffect from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -32,13 +32,9 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkLight(Light, AqualinkEntity): +class HassAqualinkLight(AqualinkEntity, Light): """Representation of a light.""" - def __init__(self, dev: AqualinkLight): - """Initialize the light.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the light.""" diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py index 4a1691e0314caa..81021d0b4471d4 100644 --- a/homeassistant/components/iaqualink/sensor.py +++ b/homeassistant/components/iaqualink/sensor.py @@ -2,8 +2,6 @@ import logging from typing import Optional -from iaqualink import AqualinkSensor - from homeassistant.components.sensor import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT @@ -30,10 +28,6 @@ async def async_setup_entry( class HassAqualinkSensor(AqualinkEntity): """Representation of a sensor.""" - def __init__(self, dev: AqualinkSensor): - """Initialize the sensor.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the sensor.""" diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py index f2fc51ce713136..8efb473cf54d35 100644 --- a/homeassistant/components/iaqualink/switch.py +++ b/homeassistant/components/iaqualink/switch.py @@ -1,8 +1,6 @@ """Support for Aqualink pool feature switches.""" import logging -from iaqualink import AqualinkToggle - from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -25,13 +23,9 @@ async def async_setup_entry( async_add_entities(devs, True) -class HassAqualinkSwitch(SwitchDevice, AqualinkEntity): +class HassAqualinkSwitch(AqualinkEntity, SwitchDevice): """Representation of a switch.""" - def __init__(self, dev: AqualinkToggle): - """Initialize the switch.""" - self.dev = dev - @property def name(self) -> str: """Return the name of the switch.""" From 1d3f2d20d2eb2d12d02cde93d7d79e66f8157a1c Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 14 Sep 2019 07:12:19 +0200 Subject: [PATCH 0287/3953] Add group attribute to Homematic IP Cloud (#26618) * Add group attribute to Homematic IP Cloud * Fix docstring --- homeassistant/components/homematicip_cloud/binary_sensor.py | 4 ++-- homeassistant/components/homematicip_cloud/device.py | 3 +++ homeassistant/components/homematicip_cloud/sensor.py | 6 +++--- homeassistant/components/homematicip_cloud/switch.py | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 594f4f6c54aa4e..4ac4614379b905 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -38,7 +38,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_MODEL_TYPE +from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP, ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -312,7 +312,7 @@ def available(self) -> bool: @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" - state_attr = {ATTR_MODEL_TYPE: self._device.modelType} + state_attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: True} for attr, attr_key in GROUP_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 5eeb14b635946c..05853d4b260bca 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -12,6 +12,7 @@ ATTR_MODEL_TYPE = "model_type" ATTR_ID = "id" +ATTR_IS_GROUP = "is_group" # RSSI HAP -> Device ATTR_RSSI_DEVICE = "rssi_device" # RSSI Device -> HAP @@ -131,4 +132,6 @@ def device_state_attributes(self): if attr_value: state_attr[attr_key] = attr_value + state_attr[ATTR_IS_GROUP] = False + return state_attr diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 43812df94d2283..770921288b9341 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -35,7 +35,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_MODEL_TYPE +from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -150,8 +150,8 @@ def unit_of_measurement(self) -> str: @property def device_state_attributes(self): - """Return the state attributes of the security zone group.""" - return {ATTR_MODEL_TYPE: self._device.modelType} + """Return the state attributes of the access point.""" + return {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: False} class HomematicipHeatingThermostat(HomematicipGenericDevice): diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 058e21262e3e88..ababf793f0ce6e 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -19,7 +19,7 @@ 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_IS_GROUP _LOGGER = logging.getLogger(__name__) @@ -113,7 +113,7 @@ def available(self) -> bool: @property def device_state_attributes(self): """Return the state attributes of the switch-group.""" - state_attr = {} + state_attr = {ATTR_IS_GROUP: True} if self._device.unreach: state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True return state_attr From ffee50bd7a629cc48cc177e4dcf66906985c0dd3 Mon Sep 17 00:00:00 2001 From: Gerard Date: Thu, 12 Sep 2019 09:50:02 +0200 Subject: [PATCH 0288/3953] Fix CCM messages (#26589) --- .../bmw_connected_drive/binary_sensor.py | 13 +++++++------ .../components/bmw_connected_drive/sensor.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index c9cc9b2d33373f..c13de4559847ef 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -9,7 +9,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "lids": ["Doors", "opening", "mdi:car-door"], + "lids": ["Doors", "opening", "mdi:car-door-lock"], "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"], @@ -122,8 +122,9 @@ def device_state_attributes(self): 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.has_check_control_messages - if check_control_messages: + check_control_messages = vehicle_state.check_control_messages + has_check_control_messages = vehicle_state.has_check_control_messages + if has_check_control_messages: cbs_list = [] for message in check_control_messages: cbs_list.append(message["ccmDescriptionShort"]) @@ -184,9 +185,9 @@ def _format_cbs_report(self, report): distance = round( self.hass.config.units.length(report.due_distance, LENGTH_KILOMETERS) ) - result[f"{service_type} distance"] = "{} {}".format( - distance, self.hass.config.units.length_unit - ) + result[ + f"{service_type} distance" + ] = f"{distance} {self.hass.config.units.length_unit}" return result def update_callback(self): diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 011908d54585e4..96d541b1955337 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -17,10 +17,10 @@ ATTR_TO_HA_METRIC = { "mileage": ["mdi:speedometer", LENGTH_KILOMETERS], - "remaining_range_total": ["mdi:ruler", LENGTH_KILOMETERS], - "remaining_range_electric": ["mdi:ruler", LENGTH_KILOMETERS], - "remaining_range_fuel": ["mdi:ruler", LENGTH_KILOMETERS], - "max_range_electric": ["mdi:ruler", LENGTH_KILOMETERS], + "remaining_range_total": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "max_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS], "remaining_fuel": ["mdi:gas-station", VOLUME_LITERS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], @@ -28,10 +28,10 @@ ATTR_TO_HA_IMPERIAL = { "mileage": ["mdi:speedometer", LENGTH_MILES], - "remaining_range_total": ["mdi:ruler", LENGTH_MILES], - "remaining_range_electric": ["mdi:ruler", LENGTH_MILES], - "remaining_range_fuel": ["mdi:ruler", LENGTH_MILES], - "max_range_electric": ["mdi:ruler", LENGTH_MILES], + "remaining_range_total": ["mdi:map-marker-distance", LENGTH_MILES], + "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_MILES], + "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_MILES], + "max_range_electric": ["mdi:map-marker-distance", LENGTH_MILES], "remaining_fuel": ["mdi:gas-station", VOLUME_GALLONS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], From a1a44d47b93947032369bfe36208146c1822d025 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Sep 2019 17:27:56 -0600 Subject: [PATCH 0289/3953] Update PyChromecast (#26594) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 4fb1c67a56e440..84a6a6e2934fd6 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/components/cast", - "requirements": ["pychromecast==4.0.0"], + "requirements": ["pychromecast==4.0.1"], "dependencies": [], "zeroconf": ["_googlecast._tcp.local."], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index da36928d6ba834..2e2e51587dcdaa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1110,7 +1110,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==4.0.0 +pychromecast==4.0.1 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07adcfc79f6933..7125d3e9ed5a02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,7 +279,7 @@ pyMetno==0.4.6 pyblackbird==0.5 # homeassistant.components.cast -pychromecast==4.0.0 +pychromecast==4.0.1 # homeassistant.components.deconz pydeconz==62 From 36ab3d342188cf25c227baffabab582aeef0aa76 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 14 Sep 2019 11:53:52 +0000 Subject: [PATCH 0290/3953] Bumped version to 0.99.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3a474d6245b05c..eafe547bb7c509 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 99 -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 5885c3f3535f8124df949c957e7406ddcdf43e23 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 14 Sep 2019 15:15:06 +0200 Subject: [PATCH 0291/3953] Move deCONZ services to their own file (#26645) --- homeassistant/components/deconz/__init__.py | 123 +-------------- homeassistant/components/deconz/services.py | 157 ++++++++++++++++++++ 2 files changed, 162 insertions(+), 118 deletions(-) create mode 100644 homeassistant/components/deconz/services.py diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 56663c6b2dab0c..af9f619cb3e1bd 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -10,10 +10,10 @@ ) from homeassistant.helpers import config_validation as cv -# Loading the config flow file will register the flow from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN, _LOGGER +from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN from .gateway import DeconzGateway +from .services import async_setup_services, async_unload_services CONFIG_SCHEMA = vol.Schema( { @@ -28,28 +28,6 @@ extra=vol.ALLOW_EXTRA, ) -SERVICE_DECONZ = "configure" - -SERVICE_FIELD = "field" -SERVICE_ENTITY = "entity" -SERVICE_DATA = "data" - -SERVICE_SCHEMA = vol.All( - vol.Schema( - { - vol.Optional(SERVICE_ENTITY): cv.entity_id, - vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"), - vol.Required(SERVICE_DATA): dict, - vol.Optional(CONF_BRIDGEID): str, - } - ), - cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD), -) - -SERVICE_DEVICE_REFRESH = "device_refresh" - -SERVICE_DEVICE_REFRESCH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str})) - async def async_setup(hass, config): """Load configuration for deCONZ component. @@ -89,100 +67,10 @@ async def async_setup_entry(hass, config_entry): await gateway.async_update_device_registry() - async def async_configure(call): - """Set attribute of device in deCONZ. - - Entity is used to resolve to a device path (e.g. '/lights/1'). - Field is a string representing either a full path - (e.g. '/lights/1/state') when entity is not specified, or a - subpath (e.g. '/state') when used together with entity. - Data is a json object with what data you want to alter - e.g. data={'on': true}. - { - "field": "/lights/1/state", - "data": {"on": true} - } - See Dresden Elektroniks REST API documentation for details: - http://dresden-elektronik.github.io/deconz-rest-doc/rest/ - """ - field = call.data.get(SERVICE_FIELD, "") - entity_id = call.data.get(SERVICE_ENTITY) - data = call.data[SERVICE_DATA] - - gateway = get_master_gateway(hass) - if CONF_BRIDGEID in call.data: - gateway = hass.data[DOMAIN][call.data[CONF_BRIDGEID]] - - if entity_id: - try: - field = gateway.deconz_ids[entity_id] + field - except KeyError: - _LOGGER.error("Could not find the entity %s", entity_id) - return - - await gateway.api.async_put_state(field, data) - - hass.services.async_register( - DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA - ) - - async def async_refresh_devices(call): - """Refresh available devices from deCONZ.""" - gateway = get_master_gateway(hass) - if CONF_BRIDGEID in call.data: - gateway = hass.data[DOMAIN][call.data[CONF_BRIDGEID]] - - groups = set(gateway.api.groups.keys()) - lights = set(gateway.api.lights.keys()) - scenes = set(gateway.api.scenes.keys()) - sensors = set(gateway.api.sensors.keys()) - - await gateway.api.async_load_parameters() - - gateway.async_add_device_callback( - "group", - [ - group - for group_id, group in gateway.api.groups.items() - if group_id not in groups - ], - ) - - gateway.async_add_device_callback( - "light", - [ - light - for light_id, light in gateway.api.lights.items() - if light_id not in lights - ], - ) - - gateway.async_add_device_callback( - "scene", - [ - scene - for scene_id, scene in gateway.api.scenes.items() - if scene_id not in scenes - ], - ) - - gateway.async_add_device_callback( - "sensor", - [ - sensor - for sensor_id, sensor in gateway.api.sensors.items() - if sensor_id not in sensors - ], - ) - - hass.services.async_register( - DOMAIN, - SERVICE_DEVICE_REFRESH, - async_refresh_devices, - schema=SERVICE_DEVICE_REFRESCH_SCHEMA, - ) + await async_setup_services(hass) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) + return True @@ -191,8 +79,7 @@ async def async_unload_entry(hass, config_entry): gateway = hass.data[DOMAIN].pop(config_entry.data[CONF_BRIDGEID]) if not hass.data[DOMAIN]: - hass.services.async_remove(DOMAIN, SERVICE_DECONZ) - hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) + await async_unload_services(hass) elif gateway.master: await async_update_master_gateway(hass, config_entry) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py new file mode 100644 index 00000000000000..31ba0ff3581260 --- /dev/null +++ b/homeassistant/components/deconz/services.py @@ -0,0 +1,157 @@ +"""deCONZ services.""" +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv + +from .config_flow import get_master_gateway +from .const import CONF_BRIDGEID, DOMAIN, _LOGGER + +DECONZ_SERVICES = "deconz_services" + +SERVICE_FIELD = "field" +SERVICE_ENTITY = "entity" +SERVICE_DATA = "data" + +SERVICE_CONFIGURE_DEVICE = "configure" +SERVICE_CONFIGURE_DEVICE_SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(SERVICE_ENTITY): cv.entity_id, + vol.Optional(SERVICE_FIELD): cv.matches_regex("/.*"), + vol.Required(SERVICE_DATA): dict, + vol.Optional(CONF_BRIDGEID): str, + } + ), + cv.has_at_least_one_key(SERVICE_ENTITY, SERVICE_FIELD), +) + +SERVICE_DEVICE_REFRESH = "device_refresh" +SERVICE_DEVICE_REFRESH_SCHEMA = vol.All(vol.Schema({vol.Optional(CONF_BRIDGEID): str})) + + +async def async_setup_services(hass): + """Set up services for deCONZ integration.""" + if hass.data.get(DECONZ_SERVICES, False): + return + + hass.data[DECONZ_SERVICES] = True + + async def async_call_deconz_service(service_call): + """Call correct deCONZ service.""" + service = service_call.service + service_data = service_call.data + + if service == SERVICE_CONFIGURE_DEVICE: + await async_configure_service(hass, service_data) + + elif service == SERVICE_DEVICE_REFRESH: + await async_refresh_devices_service(hass, service_data) + + hass.services.async_register( + DOMAIN, + SERVICE_CONFIGURE_DEVICE, + async_call_deconz_service, + schema=SERVICE_CONFIGURE_DEVICE_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_DEVICE_REFRESH, + async_call_deconz_service, + schema=SERVICE_DEVICE_REFRESH_SCHEMA, + ) + + +async def async_unload_services(hass): + """Unload deCONZ services.""" + if not hass.data.get(DECONZ_SERVICES): + return + + hass.data[DECONZ_SERVICES] = False + + hass.services.async_remove(DOMAIN, SERVICE_CONFIGURE_DEVICE) + hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) + + +async def async_configure_service(hass, data): + """Set attribute of device in deCONZ. + + Entity is used to resolve to a device path (e.g. '/lights/1'). + Field is a string representing either a full path + (e.g. '/lights/1/state') when entity is not specified, or a + subpath (e.g. '/state') when used together with entity. + Data is a json object with what data you want to alter + e.g. data={'on': true}. + { + "field": "/lights/1/state", + "data": {"on": true} + } + See Dresden Elektroniks REST API documentation for details: + http://dresden-elektronik.github.io/deconz-rest-doc/rest/ + """ + field = data.get(SERVICE_FIELD, "") + entity_id = data.get(SERVICE_ENTITY) + data = data[SERVICE_DATA] + + gateway = get_master_gateway(hass) + if CONF_BRIDGEID in data: + gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]] + + if entity_id: + try: + field = gateway.deconz_ids[entity_id] + field + except KeyError: + _LOGGER.error("Could not find the entity %s", entity_id) + return + + await gateway.api.async_put_state(field, data) + + +async def async_refresh_devices_service(hass, data): + """Refresh available devices from deCONZ.""" + gateway = get_master_gateway(hass) + if CONF_BRIDGEID in data: + gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]] + + groups = set(gateway.api.groups.keys()) + lights = set(gateway.api.lights.keys()) + scenes = set(gateway.api.scenes.keys()) + sensors = set(gateway.api.sensors.keys()) + + await gateway.api.async_load_parameters() + + gateway.async_add_device_callback( + "group", + [ + group + for group_id, group in gateway.api.groups.items() + if group_id not in groups + ], + ) + + gateway.async_add_device_callback( + "light", + [ + light + for light_id, light in gateway.api.lights.items() + if light_id not in lights + ], + ) + + gateway.async_add_device_callback( + "scene", + [ + scene + for scene_id, scene in gateway.api.scenes.items() + if scene_id not in scenes + ], + ) + + gateway.async_add_device_callback( + "sensor", + [ + sensor + for sensor_id, sensor in gateway.api.sensors.items() + if sensor_id not in sensors + ], + ) From 24f1ff0aefef3695bf5318a69c1a08820d00dce0 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 14 Sep 2019 17:23:23 +0200 Subject: [PATCH 0292/3953] Add built in weather to Homematic IP Cloud (#26642) --- .../components/homematicip_cloud/weather.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 2d0a69d7d06320..ed9098559a344c 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -7,6 +7,7 @@ AsyncWeatherSensorPro, ) from homematicip.aio.home import AsyncHome +from homematicip.base.enums import WeatherCondition from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry @@ -17,6 +18,24 @@ _LOGGER = logging.getLogger(__name__) +HOME_WEATHER_CONDITION = { + WeatherCondition.CLEAR: "sunny", + WeatherCondition.LIGHT_CLOUDY: "partlycloudy", + WeatherCondition.CLOUDY: "cloudy", + WeatherCondition.CLOUDY_WITH_RAIN: "rainy", + WeatherCondition.CLOUDY_WITH_SNOW_RAIN: "snowy-rainy", + WeatherCondition.HEAVILY_CLOUDY: "cloudy", + WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN: "rainy", + WeatherCondition.HEAVILY_CLOUDY_WITH_STRONG_RAIN: "snowy-rainy", + WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW: "snowy", + WeatherCondition.HEAVILY_CLOUDY_WITH_SNOW_RAIN: "snowy-rainy", + WeatherCondition.HEAVILY_CLOUDY_WITH_THUNDER: "lightning", + WeatherCondition.HEAVILY_CLOUDY_WITH_RAIN_AND_THUNDER: "lightning-rainy", + WeatherCondition.FOGGY: "fog", + WeatherCondition.STRONG_WIND: "windy", + WeatherCondition.UNKNOWN: "", +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud weather sensor.""" @@ -35,6 +54,8 @@ async def async_setup_entry( elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)): devices.append(HomematicipWeatherSensor(home, device)) + devices.append(HomematicipHomeWeather(home)) + if devices: async_add_entities(devices) @@ -95,3 +116,57 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor): def wind_bearing(self) -> float: """Return the wind bearing.""" return self._device.windDirection + + +class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity): + """representation of a HomematicIP Cloud home weather.""" + + def __init__(self, home: AsyncHome) -> None: + """Initialize the home weather.""" + home.weather.modelType = "HmIP-Home-Weather" + super().__init__(home, home) + + @property + def available(self) -> bool: + """Device available.""" + return self._home.connected + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return f"Weather {self._home.location.city}" + + @property + def temperature(self) -> float: + """Return the platform temperature.""" + return self._device.weather.temperature + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def humidity(self) -> int: + """Return the humidity.""" + return self._device.weather.humidity + + @property + def wind_speed(self) -> float: + """Return the wind speed.""" + return round(self._device.weather.windSpeed, 1) + + @property + def wind_bearing(self) -> float: + """Return the wind bearing.""" + return self._device.weather.windDirection + + @property + def attribution(self) -> str: + """Return the attribution.""" + return "Powered by Homematic IP" + + @property + def condition(self) -> str: + """Return the current condition.""" + return HOME_WEATHER_CONDITION.get(self._device.weather.weatherCondition) From 41c9ed5d5133fe19515959b890f8c748bff0bfb8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 14 Sep 2019 19:15:18 +0200 Subject: [PATCH 0293/3953] deCONZ - battery sensor instead of battery attribute (#26591) * Allow all sensors to create battery sensors * Neither binary sensor, climate nor sensor will have battery attributes --- .../components/deconz/binary_sensor.py | 7 +- homeassistant/components/deconz/climate.py | 6 +- .../components/deconz/deconz_device.py | 4 +- .../components/deconz/deconz_event.py | 5 ++ homeassistant/components/deconz/sensor.py | 75 ++++++++++--------- 5 files changed, 49 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 492b16a603a5bb..b81ecdc5164a75 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -2,7 +2,7 @@ from pydeconz.sensor import Presence, Vibration from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -17,7 +17,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -56,7 +55,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): def async_update_callback(self, force_update=False): """Update the sensor's state.""" changed = set(self._device.changed_keys) - keys = {"battery", "on", "reachable", "state"} + keys = {"on", "reachable", "state"} if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @@ -79,8 +78,6 @@ def icon(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" attr = {} - if self._device.battery: - attr[ATTR_BATTERY_LEVEL] = self._device.battery if self._device.on is not None: attr[ATTR_ON] = self._device.on diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 1844cb2c97c73f..b7a1ebce22ad48 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -8,7 +8,7 @@ HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -21,7 +21,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -120,9 +119,6 @@ def device_state_attributes(self): """Return the state attributes of the thermostat.""" attr = {} - if self._device.battery: - attr[ATTR_BATTERY_LEVEL] = self._device.battery - if self._device.offset: attr[ATTR_OFFSET] = self._device.offset diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index e6249b2304cfee..68daee6cf260ca 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -24,10 +24,10 @@ def unique_id(self): @property def serial(self): """Return a serial number for this device.""" - if self.unique_id is None or self.unique_id.count(":") != 7: + if self._device.uniqueid is None or self._device.uniqueid.count(":") != 7: return None - return self.unique_id.split("-", 1)[0] + return self._device.uniqueid.split("-", 1)[0] @property def device_info(self): diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index f6c2d471bbf0a1..31588db1f23833 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -27,6 +27,11 @@ def __init__(self, device, gateway): self.event_id = slugify(self._device.name) _LOGGER.debug("deCONZ event created: %s", self.event_id) + @property + def device(self): + """Return Event device.""" + return self._device + @callback def async_will_remove_from_hass(self) -> None: """Disconnect event object when removed.""" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index a6138087f1ce40..001721d4f00035 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,15 +1,9 @@ """Support for deCONZ sensors.""" from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, - ATTR_TEMPERATURE, - ATTR_VOLTAGE, - DEVICE_CLASS_BATTERY, -) +from homeassistant.const import ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.util import slugify from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice @@ -24,40 +18,47 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ sensors.""" gateway = get_gateway_from_config_entry(hass, config_entry) + batteries = set() entity_handler = DeconzEntityHandler(gateway) @callback def async_add_sensor(sensors): - """Add sensors from deCONZ.""" + """Add sensors from deCONZ. + + Create DeconzEvent if part of ZHAType list. + Create DeconzSensor if not a ZHAType and not a binary sensor. + Create DeconzBattery if sensor has a battery attribute. + """ entities = [] for sensor in sensors: - if not sensor.BINARY: + if sensor.type in Switch.ZHATYPE: - if sensor.type in Switch.ZHATYPE: + if gateway.option_allow_clip_sensor or not sensor.type.startswith( + "CLIP" + ): + new_event = DeconzEvent(sensor, gateway) + hass.async_create_task(new_event.async_update_device_registry()) + gateway.events.append(new_event) - if gateway.option_allow_clip_sensor or not sensor.type.startswith( - "CLIP" - ): - event = DeconzEvent(sensor, gateway) - hass.async_create_task(event.async_update_device_registry()) - gateway.events.append(event) + elif not sensor.BINARY: - if sensor.battery: - entities.append(DeconzBattery(sensor, gateway)) + new_sensor = DeconzSensor(sensor, gateway) + entity_handler.add_entity(new_sensor) + entities.append(new_sensor) - else: - new_sensor = DeconzSensor(sensor, gateway) - entity_handler.add_entity(new_sensor) - entities.append(new_sensor) + if sensor.battery: + new_battery = DeconzBattery(sensor, gateway) + if new_battery.unique_id not in batteries: + batteries.add(new_battery.unique_id) + entities.append(new_battery) async_add_entities(entities, True) @@ -77,7 +78,7 @@ class DeconzSensor(DeconzDevice): def async_update_callback(self, force_update=False): """Update the sensor's state.""" changed = set(self._device.changed_keys) - keys = {"battery", "on", "reachable", "state"} + keys = {"on", "reachable", "state"} if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() @@ -105,8 +106,6 @@ def unit_of_measurement(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" attr = {} - if self._device.battery: - attr[ATTR_BATTERY_LEVEL] = self._device.battery if self._device.on is not None: attr[ATTR_ON] = self._device.on @@ -133,13 +132,6 @@ def device_state_attributes(self): class DeconzBattery(DeconzDevice): """Battery class for when a device is only represented as an event.""" - def __init__(self, device, gateway): - """Register dispatcher callback for update of battery state.""" - super().__init__(device, gateway) - - self._name = "{} {}".format(self._device.name, "Battery Level") - self._unit_of_measurement = "%" - @callback def async_update_callback(self, force_update=False): """Update the battery's state, if needed.""" @@ -148,6 +140,11 @@ def async_update_callback(self, force_update=False): if force_update or any(key in changed for key in keys): self.async_schedule_update_ha_state() + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return f"{self.serial}-battery" + @property def state(self): """Return the state of the battery.""" @@ -156,7 +153,7 @@ def state(self): @property def name(self): """Return the name of the battery.""" - return self._name + return f"{self._device.name} Battery Level" @property def device_class(self): @@ -166,10 +163,16 @@ def device_class(self): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return self._unit_of_measurement + return "%" @property def device_state_attributes(self): """Return the state attributes of the battery.""" - attr = {ATTR_EVENT_ID: slugify(self._device.name)} + attr = {} + + if self._device.type in Switch.ZHATYPE: + for event in self.gateway.events: + if self._device == event.device: + attr[ATTR_EVENT_ID] = event.event_id + return attr From 9c2053a251c88999ae215171c568a5c2e27c32f2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 14 Sep 2019 22:53:59 +0200 Subject: [PATCH 0294/3953] deCONZ - Remove mechanisms to import a configuration from configuration.yaml (#26648) --- homeassistant/components/deconz/__init__.py | 36 +----- .../components/deconz/config_flow.py | 23 +--- tests/components/deconz/test_config_flow.py | 35 ----- tests/components/deconz/test_init.py | 122 +++++------------- 4 files changed, 36 insertions(+), 180 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index af9f619cb3e1bd..558b0fe4205147 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -1,48 +1,20 @@ """Support for deCONZ devices.""" import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import ( - CONF_API_KEY, - CONF_HOST, - CONF_PORT, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.helpers import config_validation as cv +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN +from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DOMAIN from .gateway import DeconzGateway from .services import async_setup_services, async_unload_services CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.Schema({}, extra=vol.ALLOW_EXTRA)}, extra=vol.ALLOW_EXTRA ) async def async_setup(hass, config): - """Load configuration for deCONZ component. - - Discovery has loaded the component if DOMAIN is not present in config. - """ - if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: - deconz_config = config[DOMAIN] - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=deconz_config, - ) - ) + """Old way of setting up deCONZ integrations.""" return True diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 12e2e092f67848..c63b1721393222 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -191,31 +191,12 @@ async def async_step_ssdp(self, discovery_info): # pylint: disable=unsupported-assignment-operation self.context[CONF_BRIDGEID] = bridgeid - deconz_config = { + self.deconz_config = { CONF_HOST: discovery_info[CONF_HOST], CONF_PORT: discovery_info[CONF_PORT], } - return await self.async_step_import(deconz_config) - - async def async_step_import(self, import_config): - """Import a deCONZ bridge as a config entry. - - This flow is triggered by `async_setup` for configured bridges. - This flow is also triggered by `async_step_discovery`. - - This will execute for any bridge that does not have a - config entry yet (based on host). - - If an API key is provided, we will create an entry. - Otherwise we will delegate to `link` step which - will ask user to link the bridge. - """ - self.deconz_config = import_config - if CONF_API_KEY not in import_config: - return await self.async_step_link() - - return await self._create_entry() + return await self.async_step_link() async def async_step_hassio(self, user_input=None): """Prepare configuration for a Hass.io deCONZ bridge. diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 3f00c31c7e8746..d7071d6daef08c 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -234,41 +234,6 @@ async def test_bridge_discovery_update_existing_entry(hass): assert entry.data[config_flow.CONF_HOST] == "mock-deconz" -async def test_import_without_api_key(hass): - """Test importing a host without an API key.""" - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - data={config_flow.CONF_HOST: "1.2.3.4"}, - context={"source": "import"}, - ) - - assert result["type"] == "form" - assert result["step_id"] == "link" - - -async def test_import_with_api_key(hass): - """Test importing a host with an API key.""" - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - data={ - config_flow.CONF_BRIDGEID: "id", - config_flow.CONF_HOST: "mock-deconz", - config_flow.CONF_PORT: 80, - config_flow.CONF_API_KEY: "1234567890ABCDEF", - }, - context={"source": "import"}, - ) - - assert result["type"] == "create_entry" - assert result["title"] == "deCONZ-id" - assert result["data"] == { - config_flow.CONF_BRIDGEID: "id", - config_flow.CONF_HOST: "mock-deconz", - config_flow.CONF_PORT: 80, - config_flow.CONF_API_KEY: "1234567890ABCDEF", - } - - async def test_create_entry(hass, aioclient_mock): """Test that _create_entry work and that bridgeid can be requested.""" aioclient_mock.get( diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index b0456e0b6248bb..d0586565521716 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -6,7 +6,6 @@ import voluptuous as vol from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.setup import async_setup_component from homeassistant.components import deconz from tests.common import mock_coro, MockConfigEntry @@ -34,74 +33,13 @@ async def setup_entry(hass, entry): assert await deconz.async_setup_entry(hass, entry) is True -async def test_config_with_host_passed_to_config_entry(hass): - """Test that configured options for a host are loaded via config entry.""" - with patch.object(hass.config_entries, "flow") as mock_config_flow: - assert ( - await async_setup_component( - hass, - deconz.DOMAIN, - { - deconz.DOMAIN: { - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - } - }, - ) - is True - ) - # Import flow started - assert len(mock_config_flow.mock_calls) == 1 - - -async def test_config_without_host_not_passed_to_config_entry(hass): - """Test that a configuration without a host does not initiate an import.""" - MockConfigEntry(domain=deconz.DOMAIN, data={}).add_to_hass(hass) - with patch.object(hass.config_entries, "flow") as mock_config_flow: - assert ( - await async_setup_component(hass, deconz.DOMAIN, {deconz.DOMAIN: {}}) - is True - ) - # No flow started - assert len(mock_config_flow.mock_calls) == 0 - - -async def test_config_import_entry_fails_when_entries_exist(hass): - """Test that an already registered host does not initiate an import.""" - MockConfigEntry(domain=deconz.DOMAIN, data={}).add_to_hass(hass) - with patch.object(hass.config_entries, "flow") as mock_config_flow: - assert ( - await async_setup_component( - hass, - deconz.DOMAIN, - { - deconz.DOMAIN: { - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - } - }, - ) - is True - ) - # No flow started - assert len(mock_config_flow.mock_calls) == 0 - - -async def test_config_discovery(hass): - """Test that a discovered bridge does not initiate an import.""" - with patch.object(hass, "config_entries") as mock_config_entries: - assert await async_setup_component(hass, deconz.DOMAIN, {}) is True - # No flow started - assert len(mock_config_entries.flow.mock_calls) == 0 - - async def test_setup_entry_fails(hass): """Test setup entry fails if deCONZ is not available.""" entry = Mock() entry.data = { - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, } with patch("pydeconz.DeconzSession.async_load_parameters", side_effect=Exception): await deconz.async_setup_entry(hass, entry) @@ -111,9 +49,9 @@ async def test_setup_entry_no_available_bridge(hass): """Test setup entry fails if deCONZ is not available.""" entry = Mock() entry.data = { - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, } with patch( "pydeconz.DeconzSession.async_load_parameters", side_effect=asyncio.TimeoutError @@ -126,9 +64,9 @@ async def test_setup_entry_successful(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) @@ -145,9 +83,9 @@ async def test_setup_entry_multiple_gateways(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) @@ -156,9 +94,9 @@ async def test_setup_entry_multiple_gateways(hass): entry2 = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY2_HOST, - deconz.CONF_PORT: ENTRY2_PORT, - deconz.CONF_API_KEY: ENTRY2_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY2_HOST, + deconz.config_flow.CONF_PORT: ENTRY2_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY, deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID, }, ) @@ -178,9 +116,9 @@ async def test_unload_entry(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) @@ -201,9 +139,9 @@ async def test_unload_entry_multiple_gateways(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) @@ -212,9 +150,9 @@ async def test_unload_entry_multiple_gateways(hass): entry2 = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY2_HOST, - deconz.CONF_PORT: ENTRY2_PORT, - deconz.CONF_API_KEY: ENTRY2_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY2_HOST, + deconz.config_flow.CONF_PORT: ENTRY2_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY, deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID, }, ) @@ -237,9 +175,9 @@ async def test_service_configure(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) @@ -304,9 +242,9 @@ async def test_service_refresh_devices(hass): entry = MockConfigEntry( domain=deconz.DOMAIN, data={ - deconz.CONF_HOST: ENTRY1_HOST, - deconz.CONF_PORT: ENTRY1_PORT, - deconz.CONF_API_KEY: ENTRY1_API_KEY, + deconz.config_flow.CONF_HOST: ENTRY1_HOST, + deconz.config_flow.CONF_PORT: ENTRY1_PORT, + deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, }, ) From 57833f5b1e6e6e508c14d4db2635c203f0c2545a Mon Sep 17 00:00:00 2001 From: chriscla Date: Sat, 14 Sep 2019 17:44:19 -0700 Subject: [PATCH 0295/3953] Refactor nzbget to support future platform changes (#26462) * Re-factor nzbget platform to enable future features. * Re-factor nzbget platform to enable future features. * Re-factor nzbget platform to enable future features. * Re-factor nzbget platform to enable future features. * Use pynzbgetapi instead of raw HTTP requests * Using pynzbgetapi * Pinning pynzbgetapi version. * Requiring pynzbgetapi 0.2.0 * Addressing review comments * Refreshing requirements (adding pynzbgetapi) * Remove period from logging message * Updating requirements file * Add nzbget init to .coveragerc * Adding nzbget codeowner * Updating codeowners file --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/nzbget/__init__.py | 105 ++++++++++++ homeassistant/components/nzbget/manifest.json | 4 +- homeassistant/components/nzbget/sensor.py | 157 +++++------------- requirements_all.txt | 3 + 6 files changed, 151 insertions(+), 120 deletions(-) diff --git a/.coveragerc b/.coveragerc index fef77c8a247bba..824fb3828f2d32 100644 --- a/.coveragerc +++ b/.coveragerc @@ -436,6 +436,7 @@ omit = homeassistant/components/nuki/lock.py homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py + homeassistant/components/nzbget/__init__.py homeassistant/components/nzbget/sensor.py homeassistant/components/obihai/* homeassistant/components/octoprint/* diff --git a/CODEOWNERS b/CODEOWNERS index 18218bbf68e0f9..fe5e19f9115f37 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -197,6 +197,7 @@ homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuki/* @pschmitt homeassistant/components/nws/* @MatthewFlamm +homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/onboarding/* @home-assistant/core diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 2480daf2ead090..563fe2610932c0 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -1 +1,106 @@ """The nzbget component.""" +from datetime import timedelta +import logging + +import pynzbgetapi +import requests +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "nzbget" +DATA_NZBGET = "data_nzbget" +DATA_UPDATED = "nzbget_data_updated" + +DEFAULT_NAME = "NZBGet" +DEFAULT_PORT = 6789 + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=5) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + vol.Optional(CONF_SSL, default=False): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the NZBGet sensors.""" + host = config[DOMAIN][CONF_HOST] + port = config[DOMAIN][CONF_PORT] + ssl = "s" if config[DOMAIN][CONF_SSL] else "" + name = config[DOMAIN][CONF_NAME] + username = config[DOMAIN].get(CONF_USERNAME) + password = config[DOMAIN].get(CONF_PASSWORD) + scan_interval = config[DOMAIN][CONF_SCAN_INTERVAL] + + try: + nzbget_api = pynzbgetapi.NZBGetAPI(host, username, password, ssl, ssl, port) + nzbget_api.version() + except pynzbgetapi.NZBGetAPIException as conn_err: + _LOGGER.error("Error setting up NZBGet API: %s", conn_err) + return False + + _LOGGER.debug("Successfully validated NZBGet API connection") + + nzbget_data = hass.data[DATA_NZBGET] = NZBGetData(hass, nzbget_api) + nzbget_data.update() + + def refresh(event_time): + """Get the latest data from NZBGet.""" + nzbget_data.update() + + track_time_interval(hass, refresh, scan_interval) + + sensorconfig = {"client_name": name} + + hass.helpers.discovery.load_platform("sensor", DOMAIN, sensorconfig, config) + + return True + + +class NZBGetData: + """Get the latest data and update the states.""" + + def __init__(self, hass, api): + """Initialize the NZBGet RPC API.""" + self.hass = hass + self.status = None + self.available = True + self._api = api + + def update(self): + """Get the latest data from NZBGet instance.""" + try: + self.status = self._api.status() + self.available = True + dispatcher_send(self.hass, DATA_UPDATED) + except requests.exceptions.ConnectionError: + self.available = False + _LOGGER.error("Unable to refresh NZBGet data") diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json index 69293ede516aee..17b11d6aef9fdc 100644 --- a/homeassistant/components/nzbget/manifest.json +++ b/homeassistant/components/nzbget/manifest.json @@ -2,7 +2,7 @@ "domain": "nzbget", "name": "Nzbget", "documentation": "https://www.home-assistant.io/components/nzbget", - "requirements": [], + "requirements": ["pynzbgetapi==0.2.0"], "dependencies": [], - "codeowners": [] + "codeowners": ["@chriscla"] } diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 73643a5383cea1..ce1fda0839e10b 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -1,32 +1,15 @@ -"""Support for monitoring NZBGet NZB client.""" -from datetime import timedelta +"""Monitor the NZBGet API.""" import logging -from aiohttp.hdrs import CONTENT_TYPE -import requests -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_SSL, - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_PASSWORD, - CONF_USERNAME, - CONTENT_TYPE_JSON, - CONF_MONITORED_VARIABLES, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle + +from . import DATA_NZBGET, DATA_UPDATED _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "NZBGet" -DEFAULT_PORT = 6789 - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) SENSOR_TYPES = { "article_cache": ["ArticleCacheMB", "Article Cache", "MB"], @@ -40,66 +23,39 @@ "uptime": ["UpTimeSec", "Uptime", "min"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=["download_rate"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_USERNAME): cv.string, - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the NZBGet sensors.""" - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - ssl = "s" if config.get(CONF_SSL) else "" - name = config.get(CONF_NAME) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - monitored_types = config.get(CONF_MONITORED_VARIABLES) - - url = f"http{ssl}://{host}:{port}/jsonrpc" - - try: - nzbgetapi = NZBGetAPI(api_url=url, username=username, password=password) - nzbgetapi.update() - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as conn_err: - _LOGGER.error("Error setting up NZBGet API: %s", conn_err) - return False + """Create NZBGet sensors.""" + + if discovery_info is None: + return + + nzbget_data = hass.data[DATA_NZBGET] + name = discovery_info["client_name"] devices = [] - for ng_type in monitored_types: + for sensor_type, sensor_config in SENSOR_TYPES.items(): new_sensor = NZBGetSensor( - api=nzbgetapi, sensor_type=SENSOR_TYPES.get(ng_type), client_name=name + nzbget_data, sensor_type, name, sensor_config[0], sensor_config[1] ) devices.append(new_sensor) - add_entities(devices) + add_entities(devices, True) class NZBGetSensor(Entity): """Representation of a NZBGet sensor.""" - def __init__(self, api, sensor_type, client_name): + def __init__( + self, nzbget_data, sensor_type, client_name, sensor_name, unit_of_measurement + ): """Initialize a new NZBGet sensor.""" - self._name = "{} {}".format(client_name, sensor_type[1]) - self.type = sensor_type[0] + self._name = f"{client_name} {sensor_type}" + self.type = sensor_name self.client_name = client_name - self.api = api + self.nzbget_data = nzbget_data self._state = None - self._unit_of_measurement = sensor_type[2] - self.update() - _LOGGER.debug("Created NZBGet sensor: %s", self.type) + self._unit_of_measurement = unit_of_measurement @property def name(self): @@ -116,21 +72,31 @@ def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + @property + def available(self): + """Return whether the sensor is available.""" + return self.nzbget_data.available + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) + def update(self): """Update state of sensor.""" - try: - self.api.update() - except requests.exceptions.ConnectionError: - # Error calling the API, already logged in api.update() - return - if self.api.status is None: + if self.nzbget_data.status is None: _LOGGER.debug( "Update of %s requested, but no status is available", self._name ) return - value = self.api.status.get(self.type) + value = self.nzbget_data.status.get(self.type) if value is None: _LOGGER.warning("Unable to locate value for %s", self.type) return @@ -143,48 +109,3 @@ def update(self): self._state = round(value / 60, 2) else: self._state = value - - -class NZBGetAPI: - """Simple JSON-RPC wrapper for NZBGet's API.""" - - def __init__(self, api_url, username=None, password=None): - """Initialize NZBGet API and set headers needed later.""" - self.api_url = api_url - self.status = None - self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON} - - if username is not None and password is not None: - self.auth = (username, password) - else: - self.auth = None - self.update() - - def post(self, method, params=None): - """Send a POST request and return the response as a dict.""" - payload = {"method": method} - - if params: - payload["params"] = params - try: - response = requests.post( - self.api_url, - json=payload, - auth=self.auth, - headers=self.headers, - timeout=5, - ) - response.raise_for_status() - return response.json() - except requests.exceptions.ConnectionError as conn_exc: - _LOGGER.error( - "Failed to update NZBGet status from %s. Error: %s", - self.api_url, - conn_exc, - ) - raise - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Update cached response.""" - self.status = self.post("status")["result"] diff --git a/requirements_all.txt b/requirements_all.txt index 2075dab9a371e3..e5ecd69ee2469a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1347,6 +1347,9 @@ pynws==0.7.4 # homeassistant.components.nx584 pynx584==0.4 +# homeassistant.components.nzbget +pynzbgetapi==0.2.0 + # homeassistant.components.obihai pyobihai==1.0.2 From 6a60ebdb30d164de7eca9ad4dccef5f6d4955c02 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 15 Sep 2019 09:50:17 +0200 Subject: [PATCH 0296/3953] Rename MockToggleDevice to MockToggleEntity (#26644) * Rename MockToggleDevice to MockToggleEntity * Fix tests --- tests/common.py | 16 +-- tests/components/flux/test_switch.py | 110 +++++++-------- .../generic_thermostat/test_climate.py | 2 +- .../light/test_device_automation.py | 48 +++---- tests/components/light/test_init.py | 130 +++++++++--------- tests/components/scene/test_init.py | 2 +- .../switch/test_device_automation.py | 48 +++---- tests/components/switch/test_init.py | 2 +- .../custom_components/test/light.py | 20 +-- .../custom_components/test/switch.py | 20 +-- 10 files changed, 200 insertions(+), 198 deletions(-) diff --git a/tests/common.py b/tests/common.py index 847635d4dad67e..fda5c743222703 100644 --- a/tests/common.py +++ b/tests/common.py @@ -602,40 +602,40 @@ def __init__( ) -class MockToggleDevice(entity.ToggleEntity): +class MockToggleEntity(entity.ToggleEntity): """Provide a mock toggle device.""" - def __init__(self, name, state): - """Initialize the mock device.""" + def __init__(self, name, state, unique_id=None): + """Initialize the mock entity.""" self._name = name or DEVICE_DEFAULT_NAME self._state = state self.calls = [] @property def name(self): - """Return the name of the device if any.""" + """Return the name of the entity if any.""" self.calls.append(("name", {})) return self._name @property def state(self): - """Return the name of the device if any.""" + """Return the state of the entity if any.""" self.calls.append(("state", {})) return self._state @property def is_on(self): - """Return true if device is on.""" + """Return true if entity is on.""" self.calls.append(("is_on", {})) return self._state == STATE_ON def turn_on(self, **kwargs): - """Turn the device on.""" + """Turn the entity on.""" self.calls.append(("turn_on", kwargs)) self._state = STATE_ON def turn_off(self, **kwargs): - """Turn the device off.""" + """Turn the entity off.""" self.calls.append(("turn_off", kwargs)) self._state = STATE_OFF diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index 08a49c4a6670d4..fb35485f5c9685 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -82,10 +82,10 @@ async def test_flux_when_switch_is_off(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -113,7 +113,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -131,10 +131,10 @@ async def test_flux_before_sunrise(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -162,7 +162,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -184,10 +184,10 @@ async def test_flux_before_sunrise_known_location(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -210,7 +210,7 @@ async def test_flux_before_sunrise_known_location(hass): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], # 'brightness': 255, # 'disable_brightness_adjust': True, # 'mode': 'rgb', @@ -237,10 +237,10 @@ async def test_flux_after_sunrise_before_sunset(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -267,7 +267,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -290,10 +290,10 @@ async def test_flux_after_sunset_before_stop(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -320,7 +320,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "22:00", } }, @@ -344,10 +344,10 @@ async def test_flux_after_stop_before_sunrise(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -374,7 +374,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -397,10 +397,10 @@ async def test_flux_with_custom_start_stop_times(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -427,7 +427,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "start_time": "6:00", "stop_time": "23:30", } @@ -454,10 +454,10 @@ async def test_flux_before_sunrise_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -484,7 +484,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -512,10 +512,10 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -542,7 +542,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -570,10 +570,10 @@ async def test_flux_after_sunset_before_midnight_stop_next_day(hass, x): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -600,7 +600,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -627,10 +627,10 @@ async def test_flux_after_sunset_after_midnight_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -657,7 +657,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -684,10 +684,10 @@ async def test_flux_after_stop_before_sunrise_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -714,7 +714,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -738,10 +738,10 @@ async def test_flux_with_custom_colortemps(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -768,7 +768,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "start_colortemp": "1000", "stop_colortemp": "6000", "stop_time": "22:00", @@ -794,10 +794,10 @@ async def test_flux_with_custom_brightness(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -824,7 +824,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "brightness": 255, "stop_time": "22:00", } @@ -848,23 +848,23 @@ async def test_flux_with_multiple_lights(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1, dev2, dev3 = platform.DEVICES - common_light.turn_on(hass, entity_id=dev2.entity_id) + ent1, ent2, ent3 = platform.ENTITIES + common_light.turn_on(hass, entity_id=ent2.entity_id) await hass.async_block_till_done() - common_light.turn_on(hass, entity_id=dev3.entity_id) + common_light.turn_on(hass, entity_id=ent3.entity_id) await hass.async_block_till_done() - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None - state = hass.states.get(dev2.entity_id) + state = hass.states.get(ent2.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None - state = hass.states.get(dev3.entity_id) + state = hass.states.get(ent3.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -893,7 +893,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id, dev2.entity_id, dev3.entity_id], + "lights": [ent1.entity_id, ent2.entity_id, ent3.entity_id], } }, ) @@ -921,10 +921,10 @@ async def test_flux_with_mired(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("color_temp") is None @@ -950,7 +950,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "mode": "mired", } }, @@ -972,10 +972,10 @@ async def test_flux_with_rgb(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("color_temp") is None @@ -1001,7 +1001,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "mode": "rgb", } }, diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 367ea52b3a2bcd..776d8f39f69613 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -116,7 +116,7 @@ async def test_heater_switch(hass, setup_comp_1): """Test heater switching test switch.""" platform = getattr(hass.components, "test.switch") platform.init() - switch_1 = platform.DEVICES[1] + switch_1 = platform.ENTITIES[1] assert await async_setup_component( hass, switch.DOMAIN, {"switch": {"platform": "test"}} ) diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py index 40fa08856c5684..3525f1121c08e2 100644 --- a/tests/components/light/test_device_automation.py +++ b/tests/components/light/test_device_automation.py @@ -150,7 +150,7 @@ async def test_if_fires_on_state_change(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -162,7 +162,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, "action": { @@ -186,7 +186,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, "action": { @@ -209,21 +209,21 @@ async def test_if_fires_on_state_change(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - dev1.entity_id + ent1.entity_id ) - hass.states.async_set(dev1.entity_id, STATE_ON) + hass.states.async_set(ent1.entity_id, STATE_ON) await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - dev1.entity_id + ent1.entity_id ) @@ -234,7 +234,7 @@ async def test_if_state(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -248,7 +248,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_on", } ], @@ -267,7 +267,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_off", } ], @@ -283,7 +283,7 @@ async def test_if_state(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") @@ -292,7 +292,7 @@ async def test_if_state(hass, calls): assert len(calls) == 1 assert calls[0].data["some"] == "is_on event - test_event1" - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() @@ -307,7 +307,7 @@ async def test_action(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -319,7 +319,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, }, @@ -328,7 +328,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, }, @@ -337,7 +337,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "toggle", }, }, @@ -345,29 +345,29 @@ async def test_action(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index dc4cb7502c5765..8ceda6cbd3efa7 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -137,39 +137,39 @@ def test_services(self): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES # Test init - assert light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # Test basic turn_on, turn_off, toggle services - common.turn_off(self.hass, entity_id=dev1.entity_id) - common.turn_on(self.hass, entity_id=dev2.entity_id) + common.turn_off(self.hass, entity_id=ent1.entity_id) + common.turn_on(self.hass, entity_id=ent2.entity_id) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert light.is_on(self.hass, dev2.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert light.is_on(self.hass, ent2.entity_id) # turn on all lights common.turn_on(self.hass) self.hass.block_till_done() - assert light.is_on(self.hass, dev1.entity_id) - assert light.is_on(self.hass, dev2.entity_id) - assert light.is_on(self.hass, dev3.entity_id) + assert light.is_on(self.hass, ent1.entity_id) + assert light.is_on(self.hass, ent2.entity_id) + assert light.is_on(self.hass, ent3.entity_id) # turn off all lights common.turn_off(self.hass) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # turn off all lights by setting brightness to 0 common.turn_on(self.hass) @@ -180,97 +180,97 @@ def test_services(self): self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # toggle all lights common.toggle(self.hass) self.hass.block_till_done() - assert light.is_on(self.hass, dev1.entity_id) - assert light.is_on(self.hass, dev2.entity_id) - assert light.is_on(self.hass, dev3.entity_id) + assert light.is_on(self.hass, ent1.entity_id) + assert light.is_on(self.hass, ent2.entity_id) + assert light.is_on(self.hass, ent3.entity_id) # toggle all lights common.toggle(self.hass) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # Ensure all attributes process correctly common.turn_on( - self.hass, dev1.entity_id, transition=10, brightness=20, color_name="blue" + self.hass, ent1.entity_id, transition=10, brightness=20, color_name="blue" ) common.turn_on( - self.hass, dev2.entity_id, rgb_color=(255, 255, 255), white_value=255 + self.hass, ent2.entity_id, rgb_color=(255, 255, 255), white_value=255 ) - common.turn_on(self.hass, dev3.entity_id, xy_color=(0.4, 0.6)) + common.turn_on(self.hass, ent3.entity_id, xy_color=(0.4, 0.6)) self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert { light.ATTR_TRANSITION: 10, light.ATTR_BRIGHTNESS: 20, light.ATTR_HS_COLOR: (240, 100), } == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} == data - _, data = dev3.last_call("turn_on") + _, data = ent3.last_call("turn_on") assert {light.ATTR_HS_COLOR: (71.059, 100)} == data # Ensure attributes are filtered when light is turned off common.turn_on( - self.hass, dev1.entity_id, transition=10, brightness=0, color_name="blue" + self.hass, ent1.entity_id, transition=10, brightness=0, color_name="blue" ) common.turn_on( self.hass, - dev2.entity_id, + ent2.entity_id, brightness=0, rgb_color=(255, 255, 255), white_value=0, ) - common.turn_on(self.hass, dev3.entity_id, brightness=0, xy_color=(0.4, 0.6)) + common.turn_on(self.hass, ent3.entity_id, brightness=0, xy_color=(0.4, 0.6)) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) - _, data = dev1.last_call("turn_off") + _, data = ent1.last_call("turn_off") assert {light.ATTR_TRANSITION: 10} == data - _, data = dev2.last_call("turn_off") + _, data = ent2.last_call("turn_off") assert {} == data - _, data = dev3.last_call("turn_off") + _, data = ent3.last_call("turn_off") assert {} == data # One of the light profiles prof_name, prof_h, prof_s, prof_bri = "relax", 35.932, 69.412, 144 # Test light profiles - common.turn_on(self.hass, dev1.entity_id, profile=prof_name) + common.turn_on(self.hass, ent1.entity_id, profile=prof_name) # Specify a profile and a brightness attribute to overwrite it - common.turn_on(self.hass, dev2.entity_id, profile=prof_name, brightness=100) + common.turn_on(self.hass, ent2.entity_id, profile=prof_name, brightness=100) self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert { light.ATTR_BRIGHTNESS: prof_bri, light.ATTR_HS_COLOR: (prof_h, prof_s), } == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert { light.ATTR_BRIGHTNESS: 100, light.ATTR_HS_COLOR: (prof_h, prof_s), @@ -278,34 +278,34 @@ def test_services(self): # Test bad data common.turn_on(self.hass) - common.turn_on(self.hass, dev1.entity_id, profile="nonexisting") - common.turn_on(self.hass, dev2.entity_id, xy_color=["bla-di-bla", 5]) - common.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2]) + common.turn_on(self.hass, ent1.entity_id, profile="nonexisting") + common.turn_on(self.hass, ent2.entity_id, xy_color=["bla-di-bla", 5]) + common.turn_on(self.hass, ent3.entity_id, rgb_color=[255, None, 2]) self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert {} == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert {} == data - _, data = dev3.last_call("turn_on") + _, data = ent3.last_call("turn_on") assert {} == data # faulty attributes will not trigger a service call common.turn_on( - self.hass, dev1.entity_id, profile=prof_name, brightness="bright" + self.hass, ent1.entity_id, profile=prof_name, brightness="bright" ) - common.turn_on(self.hass, dev1.entity_id, rgb_color="yellowish") - common.turn_on(self.hass, dev2.entity_id, white_value="high") + common.turn_on(self.hass, ent1.entity_id, rgb_color="yellowish") + common.turn_on(self.hass, ent2.entity_id, white_value="high") self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert {} == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert {} == data def test_broken_light_profiles(self): @@ -340,24 +340,24 @@ def test_light_profiles(self): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1, _, _ = platform.DEVICES + ent1, _, _ = platform.ENTITIES - common.turn_on(self.hass, dev1.entity_id, profile="test") + common.turn_on(self.hass, ent1.entity_id, profile="test") self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") - assert light.is_on(self.hass, dev1.entity_id) + assert light.is_on(self.hass, ent1.entity_id) assert {light.ATTR_HS_COLOR: (71.059, 100), light.ATTR_BRIGHTNESS: 100} == data - common.turn_on(self.hass, dev1.entity_id, profile="test_off") + common.turn_on(self.hass, ent1.entity_id, profile="test_off") self.hass.block_till_done() - _, data = dev1.last_call("turn_off") + _, data = ent1.last_call("turn_off") - assert not light.is_on(self.hass, dev1.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) assert {} == data def test_default_profiles_group(self): @@ -387,10 +387,10 @@ def _mock_open(path, *args, **kwargs): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev, _, _ = platform.DEVICES - common.turn_on(self.hass, dev.entity_id) + ent, _, _ = platform.ENTITIES + common.turn_on(self.hass, ent.entity_id) self.hass.block_till_done() - _, data = dev.last_call("turn_on") + _, data = ent.last_call("turn_on") assert {light.ATTR_HS_COLOR: (71.059, 100), light.ATTR_BRIGHTNESS: 99} == data def test_default_profiles_light(self): @@ -424,7 +424,9 @@ def _mock_open(path, *args, **kwargs): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.DEVICES)) + dev = next( + filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES) + ) common.turn_on(self.hass, dev.entity_id) self.hass.block_till_done() _, data = dev.last_call("turn_on") diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 7047e6e8d92f3c..5c8d46cb727753 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -24,7 +24,7 @@ def setUp(self): # pylint: disable=invalid-name self.hass, light.DOMAIN, {light.DOMAIN: {"platform": "test"}} ) - self.light_1, self.light_2 = test_light.DEVICES[0:2] + self.light_1, self.light_2 = test_light.ENTITIES[0:2] common_light.turn_off( self.hass, [self.light_1.entity_id, self.light_2.entity_id] diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py index 7dba73476514f0..3bd29a72a12549 100644 --- a/tests/components/switch/test_device_automation.py +++ b/tests/components/switch/test_device_automation.py @@ -150,7 +150,7 @@ async def test_if_fires_on_state_change(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -162,7 +162,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, "action": { @@ -186,7 +186,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, "action": { @@ -209,21 +209,21 @@ async def test_if_fires_on_state_change(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - dev1.entity_id + ent1.entity_id ) - hass.states.async_set(dev1.entity_id, STATE_ON) + hass.states.async_set(ent1.entity_id, STATE_ON) await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - dev1.entity_id + ent1.entity_id ) @@ -234,7 +234,7 @@ async def test_if_state(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -248,7 +248,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_on", } ], @@ -267,7 +267,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_off", } ], @@ -283,7 +283,7 @@ async def test_if_state(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") @@ -292,7 +292,7 @@ async def test_if_state(hass, calls): assert len(calls) == 1 assert calls[0].data["some"] == "is_on event - test_event1" - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() @@ -307,7 +307,7 @@ async def test_action(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -319,7 +319,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, }, @@ -328,7 +328,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, }, @@ -337,7 +337,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "toggle", }, }, @@ -345,29 +345,29 @@ async def test_action(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index c04a30589edd8f..a9463cb78f4fea 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -21,7 +21,7 @@ def setUp(self): platform = getattr(self.hass.components, "test.switch") platform.init() # Switch 1 is ON, switch 2 is OFF - self.switch_1, self.switch_2, self.switch_3 = platform.DEVICES + self.switch_1, self.switch_2, self.switch_3 = platform.ENTITIES # pylint: disable=invalid-name def tearDown(self): diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 43338c9e14ed1f..0a48388b718b1e 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -4,23 +4,23 @@ Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from tests.common import MockToggleDevice +from tests.common import MockToggleEntity -DEVICES = [] +ENTITIES = [] def init(empty=False): - """Initialize the platform with devices.""" - global DEVICES + """Initialize the platform with entities.""" + global ENTITIES - DEVICES = ( + ENTITIES = ( [] if empty else [ - MockToggleDevice("Ceiling", STATE_ON), - MockToggleDevice("Ceiling", STATE_OFF), - MockToggleDevice(None, STATE_OFF), + MockToggleEntity("Ceiling", STATE_ON), + MockToggleEntity("Ceiling", STATE_OFF), + MockToggleEntity(None, STATE_OFF), ] ) @@ -28,5 +28,5 @@ def init(empty=False): async def async_setup_platform( hass, config, async_add_entities_callback, discovery_info=None ): - """Return mock devices.""" - async_add_entities_callback(DEVICES) + """Return mock entities.""" + async_add_entities_callback(ENTITIES) diff --git a/tests/testing_config/custom_components/test/switch.py b/tests/testing_config/custom_components/test/switch.py index f4226ecc63014f..484c47d1190e3c 100644 --- a/tests/testing_config/custom_components/test/switch.py +++ b/tests/testing_config/custom_components/test/switch.py @@ -4,23 +4,23 @@ Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from tests.common import MockToggleDevice +from tests.common import MockToggleEntity -DEVICES = [] +ENTITIES = [] def init(empty=False): - """Initialize the platform with devices.""" - global DEVICES + """Initialize the platform with entities.""" + global ENTITIES - DEVICES = ( + ENTITIES = ( [] if empty else [ - MockToggleDevice("AC", STATE_ON), - MockToggleDevice("AC", STATE_OFF), - MockToggleDevice(None, STATE_OFF), + MockToggleEntity("AC", STATE_ON), + MockToggleEntity("AC", STATE_OFF), + MockToggleEntity(None, STATE_OFF), ] ) @@ -28,5 +28,5 @@ def init(empty=False): async def async_setup_platform( hass, config, async_add_entities_callback, discovery_info=None ): - """Find and return test switches.""" - async_add_entities_callback(DEVICES) + """Return mock entities.""" + async_add_entities_callback(ENTITIES) From fd359c622241a0ce39005e40f5a88e9c6db9ac88 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Sun, 15 Sep 2019 05:55:20 -0400 Subject: [PATCH 0297/3953] Fix Environment Canada weather forecast, retain icon_code sensor (#26646) * Bump env_canada to 0.0.25 * Keep icon_code --- homeassistant/components/environment_canada/manifest.json | 2 +- homeassistant/components/environment_canada/sensor.py | 1 - requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 0625fd4c27f6d9..2ae2006512b03a 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.24" + "env_canada==0.0.25" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 2413edaebce060..244fda61656f88 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -68,7 +68,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ec_data = ECData(coordinates=(lat, lon), language=config.get(CONF_LANGUAGE)) sensor_list = list(ec_data.conditions.keys()) + list(ec_data.alerts.keys()) - sensor_list.remove("icon_code") add_entities([ECSensor(sensor_type, ec_data) for sensor_type in sensor_list], True) diff --git a/requirements_all.txt b/requirements_all.txt index e5ecd69ee2469a..056b51c345774c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -446,7 +446,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.24 +env_canada==0.0.25 # homeassistant.components.envirophat # envirophat==0.0.6 From f45f8f2f3dd8f4587ea0e419c78ac0dbf81289f0 Mon Sep 17 00:00:00 2001 From: Bryan York Date: Sun, 15 Sep 2019 11:53:05 -0700 Subject: [PATCH 0298/3953] Emulate color temperature for non-ct lights in light groups (#23495) * Emulate color temperature for non-ct lights in light groups * fix tests * Address review comments * Fix black formatting * Fix for pylint * Address comments * Fix black formatting * Address comments --- .../components/google_assistant/trait.py | 2 +- homeassistant/components/group/light.py | 48 +++++++++++++++++- tests/components/group/test_light.py | 50 +++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 5fa7d49b885b1c..2afa18af32e6a7 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -308,7 +308,7 @@ def sync_attributes(self): if features & light.SUPPORT_COLOR_TEMP: # Max Kelvin is Min Mireds K = 1000000 / mireds - # Min Kevin is Max Mireds K = 1000000 / mireds + # Min Kelvin is Max Mireds K = 1000000 / mireds response["colorTemperatureRange"] = { "temperatureMaxK": color_util.color_temperature_mired_to_kelvin( attrs.get(light.ATTR_MIN_MIREDS) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 87d8134ccbf6b3..0b1291d4045f42 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -1,4 +1,5 @@ """This platform allows several lights to be grouped into one light.""" +import asyncio from collections import Counter import itertools import logging @@ -19,6 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -179,6 +181,7 @@ def should_poll(self) -> bool: async def async_turn_on(self, **kwargs): """Forward the turn_on command to all lights in the light group.""" data = {ATTR_ENTITY_ID: self._entity_ids} + emulate_color_temp_entity_ids = [] if ATTR_BRIGHTNESS in kwargs: data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] @@ -189,6 +192,23 @@ async def async_turn_on(self, **kwargs): if ATTR_COLOR_TEMP in kwargs: data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP] + # Create a new entity list to mutate + updated_entities = list(self._entity_ids) + + # Walk through initial entity ids, split entity lists by support + for entity_id in self._entity_ids: + state = self.hass.states.get(entity_id) + if not state: + continue + support = state.attributes.get(ATTR_SUPPORTED_FEATURES) + # Only pass color temperature to supported entity_ids + if bool(support & SUPPORT_COLOR) and not bool( + support & SUPPORT_COLOR_TEMP + ): + emulate_color_temp_entity_ids.append(entity_id) + updated_entities.remove(entity_id) + data[ATTR_ENTITY_ID] = updated_entities + if ATTR_WHITE_VALUE in kwargs: data[ATTR_WHITE_VALUE] = kwargs[ATTR_WHITE_VALUE] @@ -201,8 +221,32 @@ async def async_turn_on(self, **kwargs): if ATTR_FLASH in kwargs: data[ATTR_FLASH] = kwargs[ATTR_FLASH] - await self.hass.services.async_call( - light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + if not emulate_color_temp_entity_ids: + await self.hass.services.async_call( + light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + ) + return + + emulate_color_temp_data = data.copy() + temp_k = color_util.color_temperature_mired_to_kelvin( + emulate_color_temp_data[ATTR_COLOR_TEMP] + ) + hs_color = color_util.color_temperature_to_hs(temp_k) + emulate_color_temp_data[ATTR_HS_COLOR] = hs_color + del emulate_color_temp_data[ATTR_COLOR_TEMP] + + emulate_color_temp_data[ATTR_ENTITY_ID] = emulate_color_temp_entity_ids + + await asyncio.gather( + self.hass.services.async_call( + light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True + ), + self.hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_ON, + emulate_color_temp_data, + blocking=True, + ), ) async def async_turn_off(self, **kwargs): diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index d3b0d8dd3010d3..87898e42d593e9 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -186,6 +186,56 @@ async def test_color_temp(hass): assert state.attributes["color_temp"] == 1000 +async def test_emulated_color_temp_group(hass): + """Test emulated color temperature in a group.""" + await async_setup_component( + hass, + "light", + { + "light": [ + {"platform": "demo"}, + { + "platform": "group", + "entities": [ + "light.bed_light", + "light.ceiling_lights", + "light.kitchen_lights", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + + hass.states.async_set("light.bed_light", "on", {"supported_features": 2}) + await hass.async_block_till_done() + hass.states.async_set("light.ceiling_lights", "on", {"supported_features": 63}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen_lights", "on", {"supported_features": 61}) + await hass.async_block_till_done() + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.light_group", "color_temp": 200}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.bed_light") + assert state.state == "on" + assert state.attributes["color_temp"] == 200 + assert "hs_color" not in state.attributes.keys() + + state = hass.states.get("light.ceiling_lights") + assert state.state == "on" + assert state.attributes["color_temp"] == 200 + assert "hs_color" in state.attributes.keys() + + state = hass.states.get("light.kitchen_lights") + assert state.state == "on" + assert state.attributes["hs_color"] == (27.001, 19.243) + + async def test_min_max_mireds(hass): """Test min/max mireds reporting.""" await async_setup_component( From 719a6018805c41fac99fa262a40e126cd7f4d8c8 Mon Sep 17 00:00:00 2001 From: chriscla Date: Sun, 15 Sep 2019 22:06:21 -0700 Subject: [PATCH 0299/3953] Use pynzbgetapi exceptions consistently (#26667) --- homeassistant/components/nzbget/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 563fe2610932c0..37744dce180342 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -3,7 +3,6 @@ import logging import pynzbgetapi -import requests import voluptuous as vol from homeassistant.const import ( @@ -101,6 +100,6 @@ def update(self): self.status = self._api.status() self.available = True dispatcher_send(self.hass, DATA_UPDATED) - except requests.exceptions.ConnectionError: + except pynzbgetapi.NZBGetAPIException: self.available = False _LOGGER.error("Unable to refresh NZBGet data") From 5116d02747ae3f549966c4ba5789ec051679f7cb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 16 Sep 2019 10:08:13 +0200 Subject: [PATCH 0300/3953] deCONZ - Improve service tests (#26663) * Improve configure service tests * Add refresh device service test * Add tests for setup and unload services * Remove refresh device test from test_init * Extra verification of deconz services existance in hass.data --- homeassistant/components/deconz/services.py | 5 +- tests/components/deconz/test_init.py | 96 -------- tests/components/deconz/test_services.py | 245 ++++++++++++++++++++ 3 files changed, 248 insertions(+), 98 deletions(-) create mode 100644 tests/components/deconz/test_services.py diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 31ba0ff3581260..3498b46d879c8f 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -89,13 +89,14 @@ async def async_configure_service(hass, data): See Dresden Elektroniks REST API documentation for details: http://dresden-elektronik.github.io/deconz-rest-doc/rest/ """ + bridgeid = data.get(CONF_BRIDGEID) field = data.get(SERVICE_FIELD, "") entity_id = data.get(SERVICE_ENTITY) data = data[SERVICE_DATA] gateway = get_master_gateway(hass) - if CONF_BRIDGEID in data: - gateway = hass.data[DOMAIN][data[CONF_BRIDGEID]] + if bridgeid: + gateway = hass.data[DOMAIN][bridgeid] if entity_id: try: diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index d0586565521716..7d630498cde14e 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -3,7 +3,6 @@ import asyncio import pytest -import voluptuous as vol from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components import deconz @@ -168,98 +167,3 @@ async def test_unload_entry_multiple_gateways(hass): assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN] assert hass.data[deconz.DOMAIN][ENTRY2_BRIDGEID].master - - -async def test_service_configure(hass): - """Test that service invokes pydeconz with the correct path and data.""" - entry = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, - }, - ) - entry.add_to_hass(hass) - - await setup_entry(hass, entry) - - hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].deconz_ids = {"light.test": "/light/1"} - data = {"on": True, "attr1": 10, "attr2": 20} - - # only field - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", "configure", service_data={"field": "/light/42", "data": data} - ) - await hass.async_block_till_done() - - # only entity - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", "configure", service_data={"entity": "light.test", "data": data} - ) - await hass.async_block_till_done() - - # entity + field - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", - "configure", - service_data={"entity": "light.test", "field": "/state", "data": data}, - ) - await hass.async_block_till_done() - - # non-existing entity (or not from deCONZ) - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - await hass.services.async_call( - "deconz", - "configure", - service_data={ - "entity": "light.nonexisting", - "field": "/state", - "data": data, - }, - ) - await hass.async_block_till_done() - - # field does not start with / - with pytest.raises(vol.Invalid): - with patch( - "pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True) - ): - await hass.services.async_call( - "deconz", - "configure", - service_data={"entity": "light.test", "field": "state", "data": data}, - ) - await hass.async_block_till_done() - - -async def test_service_refresh_devices(hass): - """Test that service can refresh devices.""" - entry = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, - }, - ) - entry.add_to_hass(hass) - - await setup_entry(hass, entry) - - with patch( - "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(True) - ): - await hass.services.async_call("deconz", "device_refresh", service_data={}) - await hass.async_block_till_done() - - with patch( - "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(False) - ): - await hass.services.async_call("deconz", "device_refresh", service_data={}) - await hass.async_block_till_done() diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py new file mode 100644 index 00000000000000..63934871fcbf39 --- /dev/null +++ b/tests/components/deconz/test_services.py @@ -0,0 +1,245 @@ +"""deCONZ service tests.""" +from asynctest import Mock, patch + +import pytest +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import deconz + +BRIDGEID = "0123456789" + +ENTRY_CONFIG = { + deconz.config_flow.CONF_API_KEY: "ABCDEF", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, + deconz.config_flow.CONF_HOST: "1.2.3.4", + deconz.config_flow.CONF_PORT: 80, +} + +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, +} + +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} + +GROUP = { + "1": { + "id": "Group 1 id", + "name": "Group 1 name", + "type": "LightGroup", + "state": {}, + "action": {}, + "scenes": [{"id": "1", "name": "Scene 1"}], + "lights": ["1"], + } +} + +LIGHT = { + "1": { + "id": "Light 1 id", + "name": "Light 1 name", + "state": {"reachable": True}, + "type": "Light", + "uniqueid": "00:00:00:00:00:00:00:01-00", + } +} + +SENSOR = { + "1": { + "id": "Sensor 1 id", + "name": "Sensor 1 name", + "type": "ZHALightLevel", + "state": {"lightlevel": 30000, "dark": False}, + "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + } +} + + +async def setup_deconz_integration(hass, options): + """Create the deCONZ gateway.""" + config_entry = config_entries.ConfigEntry( + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, + options=options, + entry_id="1", + ) + + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=DECONZ_WEB_REQUEST + ): + await deconz.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][BRIDGEID] + + +async def test_service_setup(hass): + """Verify service setup works.""" + assert deconz.services.DECONZ_SERVICES not in hass.data + with patch( + "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True) + ) as async_register: + await deconz.services.async_setup_services(hass) + assert hass.data[deconz.services.DECONZ_SERVICES] is True + assert async_register.call_count == 2 + + +async def test_service_setup_already_registered(hass): + """Make sure that services are only registered once.""" + hass.data[deconz.services.DECONZ_SERVICES] = True + with patch( + "homeassistant.core.ServiceRegistry.async_register", return_value=Mock(True) + ) as async_register: + await deconz.services.async_setup_services(hass) + async_register.assert_not_called() + + +async def test_service_unload(hass): + """Verify service unload works.""" + hass.data[deconz.services.DECONZ_SERVICES] = True + with patch( + "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True) + ) as async_remove: + await deconz.services.async_unload_services(hass) + assert hass.data[deconz.services.DECONZ_SERVICES] is False + assert async_remove.call_count == 2 + + +async def test_service_unload_not_registered(hass): + """Make sure that services can only be unloaded once.""" + with patch( + "homeassistant.core.ServiceRegistry.async_remove", return_value=Mock(True) + ) as async_remove: + await deconz.services.async_unload_services(hass) + assert deconz.services.DECONZ_SERVICES not in hass.data + async_remove.assert_not_called() + + +async def test_configure_service_with_field(hass): + """Test that service invokes pydeconz with the correct path and data.""" + await setup_deconz_integration(hass, options={}) + + data = { + deconz.services.SERVICE_FIELD: "/light/2", + deconz.CONF_BRIDGEID: BRIDGEID, + deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_called_with("/light/2", {"on": True, "attr1": 10, "attr2": 20}) + + +async def test_configure_service_with_entity(hass): + """Test that service invokes pydeconz with the correct path and data.""" + gateway = await setup_deconz_integration(hass, options={}) + + gateway.deconz_ids["light.test"] = "/light/1" + data = { + deconz.services.SERVICE_ENTITY: "light.test", + deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_called_with("/light/1", {"on": True, "attr1": 10, "attr2": 20}) + + +async def test_configure_service_with_entity_and_field(hass): + """Test that service invokes pydeconz with the correct path and data.""" + gateway = await setup_deconz_integration(hass, options={}) + + gateway.deconz_ids["light.test"] = "/light/1" + data = { + deconz.services.SERVICE_ENTITY: "light.test", + deconz.services.SERVICE_FIELD: "/state", + deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_called_with( + "/light/1/state", {"on": True, "attr1": 10, "attr2": 20} + ) + + +async def test_configure_service_with_faulty_field(hass): + """Test that service invokes pydeconz with the correct path and data.""" + await setup_deconz_integration(hass, options={}) + + data = {deconz.services.SERVICE_FIELD: "light/2", deconz.services.SERVICE_DATA: {}} + + with pytest.raises(vol.Invalid): + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + + +async def test_configure_service_with_faulty_entity(hass): + """Test that service invokes pydeconz with the correct path and data.""" + await setup_deconz_integration(hass, options={}) + + data = { + deconz.services.SERVICE_ENTITY: "light.nonexisting", + deconz.services.SERVICE_DATA: {}, + } + + with patch( + "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) + ) as put_state: + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data + ) + await hass.async_block_till_done() + put_state.assert_not_called() + + +async def test_service_refresh_devices(hass): + """Test that service can refresh devices.""" + gateway = await setup_deconz_integration(hass, options={}) + + data = {deconz.CONF_BRIDGEID: BRIDGEID} + + with patch( + "pydeconz.DeconzSession.async_get_state", + return_value={"groups": GROUP, "lights": LIGHT, "sensors": SENSOR}, + ): + await hass.services.async_call( + deconz.DOMAIN, deconz.services.SERVICE_DEVICE_REFRESH, service_data=data + ) + await hass.async_block_till_done() + + assert gateway.deconz_ids == { + "light.group_1_name": "/groups/1", + "light.light_1_name": "/lights/1", + "scene.group_1_name_scene_1": "/groups/1/scenes/1", + "sensor.sensor_1_name": "/sensors/1", + } From db48d5effdfc40d1ade8e54ea9fc52801e1e300a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 16 Sep 2019 10:34:31 +0200 Subject: [PATCH 0301/3953] Update azure-pipelines-ci.yml for Azure Pipelines --- azure-pipelines-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 558c0c39f663b9..13f0915bc56f12 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -112,6 +112,8 @@ stages: # Find offending deps with `pipdeptree -r -p typing` pip uninstall -y typing - script: | + set -e + . venv/bin/activate pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar tests script/check_dirty From 8de84c53a14f42e5ebc9e231cd60616ca4d73197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 16 Sep 2019 12:14:05 +0100 Subject: [PATCH 0302/3953] zha: fix 0 second transitions being ignored. (#26654) Allow turning a light on instantly, with no transition time. This is actually required for IKEA lights to be able to set brightness and color temp in a single call. --- homeassistant/components/zha/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 27257e5039aee2..c2273c54073ac0 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -201,7 +201,7 @@ def async_restore_last_state(self, last_state): async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) - duration = transition * 10 if transition else DEFAULT_DURATION + duration = transition * 10 if transition is not None else DEFAULT_DURATION brightness = kwargs.get(light.ATTR_BRIGHTNESS) effect = kwargs.get(light.ATTR_EFFECT) From c088e8fd956783c67defbfe2dd6c706fb5c9c446 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Mon, 16 Sep 2019 21:20:48 +0200 Subject: [PATCH 0303/3953] pytfiac version bump to 0.4 (#26669) --- homeassistant/components/tfiac/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tfiac/manifest.json b/homeassistant/components/tfiac/manifest.json index 9997ae00f0a4f4..d7282317d95dfb 100644 --- a/homeassistant/components/tfiac/manifest.json +++ b/homeassistant/components/tfiac/manifest.json @@ -3,7 +3,7 @@ "name": "Tfiac", "documentation": "https://www.home-assistant.io/components/tfiac", "requirements": [ - "pytfiac==0.3" + "pytfiac==0.4" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 056b51c345774c..049e3a423c24f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1470,7 +1470,7 @@ pytautulli==0.5.0 pyteleloisirs==3.5 # homeassistant.components.tfiac -pytfiac==0.3 +pytfiac==0.4 # homeassistant.components.thinkingcleaner pythinkingcleaner==0.0.3 From 771c674e90845a4eb15f20cbc8608e2f1a29824d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 17 Sep 2019 00:32:14 +0000 Subject: [PATCH 0304/3953] [ci skip] Translation update --- .../components/adguard/.translations/es.json | 18 +++++++--- .../arcam_fmj/.translations/es.json | 5 +++ .../arcam_fmj/.translations/lb.json | 5 +++ .../components/axis/.translations/es.json | 1 + .../cert_expiry/.translations/es.json | 8 +++-- .../cert_expiry/.translations/sl.json | 2 +- .../components/deconz/.translations/bg.json | 29 +++++++++++++++ .../components/deconz/.translations/es.json | 24 ++++++++++++- .../components/deconz/.translations/lb.json | 29 ++++++++++++++- .../components/deconz/.translations/nl.json | 29 +++++++++++++++ .../components/deconz/.translations/no.json | 36 +++++++++++++++++++ .../components/esphome/.translations/es.json | 1 + .../components/esphome/.translations/lb.json | 2 +- .../geonetnz_quakes/.translations/es.json | 7 ++-- .../components/heos/.translations/lb.json | 2 +- .../homekit_controller/.translations/es.json | 3 +- .../homekit_controller/.translations/lb.json | 2 +- .../homematicip_cloud/.translations/lb.json | 2 +- .../components/hue/.translations/es.json | 2 ++ .../iaqualink/.translations/bg.json | 21 +++++++++++ .../iaqualink/.translations/es.json | 13 +++++-- .../iaqualink/.translations/lb.json | 21 +++++++++++ .../iaqualink/.translations/no.json | 21 +++++++++++ .../components/life360/.translations/es.json | 1 + .../components/light/.translations/bg.json | 13 +++++++ .../components/light/.translations/es.json | 1 + .../components/light/.translations/lb.json | 17 +++++++++ .../components/light/.translations/no.json | 9 +++++ .../components/light/.translations/sl.json | 4 +-- .../components/linky/.translations/bg.json | 25 +++++++++++++ .../components/linky/.translations/es.json | 14 ++++++-- .../components/linky/.translations/lb.json | 22 ++++++++++++ .../components/linky/.translations/no.json | 25 +++++++++++++ .../components/met/.translations/es.json | 5 +-- .../components/met/.translations/lb.json | 2 +- .../components/plaato/.translations/es.json | 3 +- .../components/point/.translations/es.json | 2 +- .../components/ps4/.translations/lb.json | 6 ++-- .../solaredge/.translations/bg.json | 20 +++++++++++ .../solaredge/.translations/es.json | 12 +++++-- .../solaredge/.translations/lb.json | 19 ++++++++++ .../solaredge/.translations/no.json | 21 +++++++++++ .../components/switch/.translations/bg.json | 17 +++++++++ .../components/switch/.translations/es.json | 1 + .../components/switch/.translations/lb.json | 17 +++++++++ .../components/switch/.translations/no.json | 17 +++++++++ .../tellduslive/.translations/es.json | 1 + .../components/traccar/.translations/es.json | 3 +- .../twentemilieu/.translations/es.json | 10 ++++-- .../components/unifi/.translations/ca.json | 6 ++++ .../components/unifi/.translations/es.json | 7 ++++ .../components/velbus/.translations/es.json | 8 +++-- .../components/vesync/.translations/es.json | 6 +++- .../components/vesync/.translations/lb.json | 17 +++++++++ .../components/withings/.translations/lb.json | 12 +++++++ 55 files changed, 586 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/es.json create mode 100644 homeassistant/components/arcam_fmj/.translations/lb.json create mode 100644 homeassistant/components/iaqualink/.translations/bg.json create mode 100644 homeassistant/components/iaqualink/.translations/lb.json create mode 100644 homeassistant/components/iaqualink/.translations/no.json create mode 100644 homeassistant/components/light/.translations/bg.json create mode 100644 homeassistant/components/light/.translations/lb.json create mode 100644 homeassistant/components/linky/.translations/bg.json create mode 100644 homeassistant/components/linky/.translations/lb.json create mode 100644 homeassistant/components/linky/.translations/no.json create mode 100644 homeassistant/components/solaredge/.translations/bg.json create mode 100644 homeassistant/components/solaredge/.translations/lb.json create mode 100644 homeassistant/components/solaredge/.translations/no.json create mode 100644 homeassistant/components/switch/.translations/bg.json create mode 100644 homeassistant/components/switch/.translations/lb.json create mode 100644 homeassistant/components/switch/.translations/no.json create mode 100644 homeassistant/components/vesync/.translations/lb.json create mode 100644 homeassistant/components/withings/.translations/lb.json diff --git a/homeassistant/components/adguard/.translations/es.json b/homeassistant/components/adguard/.translations/es.json index 46f21d96195465..5886d8e5c5b6fc 100644 --- a/homeassistant/components/adguard/.translations/es.json +++ b/homeassistant/components/adguard/.translations/es.json @@ -1,20 +1,30 @@ { "config": { "abort": { - "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente." + "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.", + "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, "error": { "connection_error": "No se conect\u00f3." }, "step": { + "hassio_confirm": { + "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Hass.io: {addon} ?", + "title": "AdGuard Home a trav\u00e9s del complemento Hass.io" + }, "user": { "data": { "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Nombre de usuario" - } + "ssl": "AdGuard Home utiliza un certificado SSL", + "username": "Nombre de usuario", + "verify_ssl": "AdGuard Home utiliza un certificado apropiado" + }, + "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control.", + "title": "Enlace su AdGuard Home." } - } + }, + "title": "AdGuard Home" } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/es.json b/homeassistant/components/arcam_fmj/.translations/es.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/es.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/lb.json b/homeassistant/components/arcam_fmj/.translations/lb.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/lb.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/es.json b/homeassistant/components/axis/.translations/es.json index 817737eee04d88..d29481a3be96f8 100644 --- a/homeassistant/components/axis/.translations/es.json +++ b/homeassistant/components/axis/.translations/es.json @@ -8,6 +8,7 @@ }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en curso.", "device_unavailable": "El dispositivo no est\u00e1 disponible", "faulty_credentials": "Credenciales de usuario incorrectas" }, diff --git a/homeassistant/components/cert_expiry/.translations/es.json b/homeassistant/components/cert_expiry/.translations/es.json index 2cb0bd9af166ed..b10518646ac907 100644 --- a/homeassistant/components/cert_expiry/.translations/es.json +++ b/homeassistant/components/cert_expiry/.translations/es.json @@ -5,8 +5,9 @@ }, "error": { "certificate_fetch_failed": "No se puede obtener el certificado de esta combinaci\u00f3n de host y puerto", - "connection_timeout": "Tiempo de espera agotado al conectar con el dispositivo.", - "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" + "connection_timeout": "Tiempo de espera agotado al conectar a este host", + "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", + "resolve_failed": "Este host no se puede resolver" }, "step": { "user": { @@ -14,7 +15,8 @@ "host": "El nombre de host del certificado", "name": "El nombre del certificado", "port": "El puerto del certificado" - } + }, + "title": "Defina el certificado para probar" } }, "title": "Caducidad del certificado" diff --git a/homeassistant/components/cert_expiry/.translations/sl.json b/homeassistant/components/cert_expiry/.translations/sl.json index c088e414c73dbd..3774956330a52a 100644 --- a/homeassistant/components/cert_expiry/.translations/sl.json +++ b/homeassistant/components/cert_expiry/.translations/sl.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila", - "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem", + "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem je potekla", "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti" }, diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index a02a6ff422338f..f3eead4aae023a 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -40,5 +40,34 @@ } }, "title": "deCONZ" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", + "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "close": "\u0417\u0430\u0442\u0432\u0430\u0440\u044f\u043d\u0435", + "dim_down": "\u0417\u0430\u0442\u044a\u043c\u043d\u044f\u0432\u0430\u043d\u0435", + "dim_up": "\u041e\u0441\u0432\u0435\u0442\u044f\u0432\u0430\u043d\u0435", + "left": "\u041b\u044f\u0432\u043e", + "open": "\u041e\u0442\u0432\u0430\u0440\u044f\u043d\u0435", + "right": "\u0414\u044f\u0441\u043d\u043e", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", + "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_rotated": "\u0417\u0430\u0432\u044a\u0440\u0442\u044f\u043d \u0431\u0443\u0442\u043e\u043d \"{subtype}\"", + "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", + "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", + "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 3d2b3f17814a49..1bc6c8211a268d 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured": "El puente ya esta configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.", "no_bridges": "No se han descubierto puentes deCONZ", + "not_deconz_bridge": "No es un puente deCONZ", "one_instance_only": "El componente solo admite una instancia de deCONZ", "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, @@ -41,21 +43,41 @@ }, "device_automation": { "trigger_subtype": { + "both_buttons": "Ambos botones", + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", "close": "Cerrar", "dim_down": "Bajar la intensidad", "dim_up": "Subir la intensidad", "left": "Izquierda", + "open": "Abierto", "right": "Derecha", "turn_off": "Apagar", "turn_on": "Encender" + }, + "trigger_type": { + "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces consecutivas", + "remote_button_long_press": "bot\u00f3n \"{subtype}\" pulsado continuamente", + "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de un rato pulsado", + "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", + "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", + "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", + "remote_button_short_press": "bot\u00f3n \"{subtype}\" pulsado", + "remote_button_short_release": "bot\u00f3n \"{subtype}\" liberado", + "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", + "remote_gyro_activated": "Dispositivo sacudido" } }, "options": { "step": { "async_step_deconz_devices": { "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", "allow_deconz_groups": "Permitir grupos de luz deCONZ" - } + }, + "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ" }, "deconz_devices": { "data": { diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 60a27304d78437..41c75ec4aab242 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -23,7 +23,7 @@ "init": { "data": { "host": "Host", - "port": "Port (Standard Wert: '80')" + "port": "Port" }, "title": "deCONZ gateway d\u00e9fin\u00e9ieren" }, @@ -40,5 +40,32 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "B\u00e9id Kn\u00e4ppchen", + "button_1": "\u00c9ischte Kn\u00e4ppchen", + "button_2": "Zweete Kn\u00e4ppchen", + "button_3": "Dr\u00ebtte Kn\u00e4ppchen", + "button_4": "V\u00e9ierte Kn\u00e4ppchen", + "close": "Zoumaachen", + "left": "L\u00e9nks", + "open": "Op", + "right": "Riets", + "turn_off": "Ausschalten", + "turn_on": "Uschalten" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" Kn\u00e4ppche zwee mol gedr\u00e9ckt", + "remote_button_long_press": "\"{subtype}\" Kn\u00e4ppche permanent gedr\u00e9ckt", + "remote_button_long_release": "\"{subtype}\" Kn\u00e4ppche no laangem unhalen lassgelooss", + "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt", + "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt", + "remote_button_rotated": "Kn\u00e4ppche gedr\u00e9int \"{subtype}\"", + "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", + "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", + "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", + "remote_gyro_activated": "Apparat ger\u00ebselt" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index f9f2d40488f6a4..116f6254b37213 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee gateway" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Beide knoppen", + "button_1": "Eerste knop", + "button_2": "Tweede knop", + "button_3": "Derde knop", + "button_4": "Vierde knop", + "close": "Sluiten", + "dim_down": "Dim omlaag", + "dim_up": "Dim omhoog", + "left": "Links", + "open": "Open", + "right": "Rechts", + "turn_off": "Uitschakelen", + "turn_on": "Inschakelen" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" knop dubbel geklikt", + "remote_button_long_press": "\" {subtype} \" knop continu ingedrukt", + "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang indrukken van de knop", + "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt", + "remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt", + "remote_button_rotated": "Knop gedraaid \" {subtype} \"", + "remote_button_short_press": "\" {subtype} \" knop ingedrukt", + "remote_button_short_release": "\"{subtype}\" knop losgelaten", + "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt", + "remote_gyro_activated": "Apparaat geschud" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 8798248224a582..7a93c6ff9cf850 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee gateway" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Begge knappene", + "button_1": "F\u00f8rste knapp", + "button_2": "Andre knapp", + "button_3": "Tredje knapp", + "button_4": "Fjerde knapp", + "close": "Lukk", + "dim_down": "Dimm ned", + "dim_up": "Dimm opp", + "left": "Venstre", + "open": "\u00c5pen", + "right": "H\u00f8yre", + "turn_off": "Skru av", + "turn_on": "Sl\u00e5 p\u00e5" + }, + "trigger_type": { + "remote_button_double_press": "\"{under type}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{undertype}\" - knappen ble kontinuerlig trykket", + "remote_button_long_release": "\" {subtype} \" -knapp sluppet etter langt trykk", + "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{undertype}\" - knappen femdobbelt klikket", + "remote_button_rotated": "Knappen roterte \" {subtype} \"", + "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", + "remote_button_short_release": "\" {subtype} \" -knappen ble utgitt", + "remote_button_triple_press": "\"{under type}\"-knappen trippel klikket", + "remote_gyro_activated": "Enhet er ristet" + } + }, "options": { "step": { "async_step_deconz_devices": { @@ -49,6 +78,13 @@ "allow_deconz_groups": "Tillat deCONZ lys grupper" }, "description": "Konfigurere synlighet av deCONZ enhetstyper" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Tillat deCONZ CLIP-sensorer", + "allow_deconz_groups": "Tillat deCONZ lys grupper" + }, + "description": "Konfigurere synlighet av deCONZ enhetstyper" } } } diff --git a/homeassistant/components/esphome/.translations/es.json b/homeassistant/components/esphome/.translations/es.json index 88730a18554e9a..70d766cf4c0544 100644 --- a/homeassistant/components/esphome/.translations/es.json +++ b/homeassistant/components/esphome/.translations/es.json @@ -8,6 +8,7 @@ "invalid_password": "\u00a1Contrase\u00f1a incorrecta!", "resolve_error": "No se puede resolver la direcci\u00f3n de ESP. Si el error persiste, configura una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "Desplom\u00e9: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/lb.json b/homeassistant/components/esphome/.translations/lb.json index 955b050bc5b19d..882b67823ba288 100644 --- a/homeassistant/components/esphome/.translations/lb.json +++ b/homeassistant/components/esphome/.translations/lb.json @@ -14,7 +14,7 @@ "data": { "password": "Passwuert" }, - "description": "Gitt d'Passwuert vun \u00e4rer Konfiguratioun an.", + "description": "Gitt d'Passwuert vun \u00e4rer Konfiguratioun an fir {name}.", "title": "Passwuert aginn" }, "discovery_confirm": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/es.json b/homeassistant/components/geonetnz_quakes/.translations/es.json index 41404822dd8289..f6f592675ab33e 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/es.json +++ b/homeassistant/components/geonetnz_quakes/.translations/es.json @@ -6,9 +6,12 @@ "step": { "user": { "data": { + "mmi": "MMI", "radius": "Radio" - } + }, + "title": "Complete todos los campos requeridos" } - } + }, + "title": "GeoNet NZ Quakes" } } \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/lb.json b/homeassistant/components/heos/.translations/lb.json index 416f0878de46a3..cfe1d347b0cc20 100644 --- a/homeassistant/components/heos/.translations/lb.json +++ b/homeassistant/components/heos/.translations/lb.json @@ -16,6 +16,6 @@ "title": "Mat Heos verbannen" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/es.json b/homeassistant/components/homekit_controller/.translations/es.json index 642e76fd1dd0d3..67f6daa8469d8e 100644 --- a/homeassistant/components/homekit_controller/.translations/es.json +++ b/homeassistant/components/homekit_controller/.translations/es.json @@ -3,6 +3,7 @@ "abort": { "accessory_not_found_error": "No se puede a\u00f1adir el emparejamiento porque ya no se puede encontrar el dispositivo.", "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", + "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en curso.", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicia el accesorio e int\u00e9ntalo de nuevo.", "ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.", "invalid_config_entry": "Este dispositivo se muestra como listo para vincular, pero ya existe una entrada que causa conflicto en Home Assistant y se debe eliminar primero.", @@ -23,7 +24,7 @@ "data": { "pairing_code": "C\u00f3digo de vinculaci\u00f3n" }, - "description": "Introduce tu c\u00f3digo de vinculaci\u00f3n de HomeKit para usar este accesorio", + "description": "Introduce tu c\u00f3digo de vinculaci\u00f3n de HomeKit (en este formato XXX-XX-XXX) para usar este accesorio", "title": "Vincular con accesorio HomeKit" }, "user": { diff --git a/homeassistant/components/homekit_controller/.translations/lb.json b/homeassistant/components/homekit_controller/.translations/lb.json index 97efd428a0469e..ca7bce44508243 100644 --- a/homeassistant/components/homekit_controller/.translations/lb.json +++ b/homeassistant/components/homekit_controller/.translations/lb.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "Pairing Code" }, - "description": "Gitt \u00e4ren HomeKit pairing Code an fir d\u00ebsen Accessoire ze benotzen", + "description": "Gitt \u00e4ren HomeKit pairing Code (am Format XXX-XX-XXX) an fir d\u00ebsen Accessoire ze benotzen", "title": "Mam HomeKit Accessoire verbannen" }, "user": { diff --git a/homeassistant/components/homematicip_cloud/.translations/lb.json b/homeassistant/components/homematicip_cloud/.translations/lb.json index 2cad909a7ee54e..f8ae990d36442a 100644 --- a/homeassistant/components/homematicip_cloud/.translations/lb.json +++ b/homeassistant/components/homematicip_cloud/.translations/lb.json @@ -21,7 +21,7 @@ "title": "HomematicIP Accesspoint auswielen" }, "link": { - "description": "Dr\u00e9ckt de bloen Kn\u00e4ppchen um Accesspoint an den Submit Kn\u00e4ppchen fir d'HomematicIP mam Home Assistant ze registr\u00e9ieren.", + "description": "Dr\u00e9ckt de bloen Kn\u00e4ppchen um Accesspoint an den Submit Kn\u00e4ppchen fir d'HomematicIP mam Home Assistant ze registr\u00e9ieren.\n\n![Standuert vum Kn\u00e4ppchen op der Bridge](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Accesspoint verbannen" } }, diff --git a/homeassistant/components/hue/.translations/es.json b/homeassistant/components/hue/.translations/es.json index 56e7ed62e9dc8a..3ec9ed871d3dca 100644 --- a/homeassistant/components/hue/.translations/es.json +++ b/homeassistant/components/hue/.translations/es.json @@ -3,9 +3,11 @@ "abort": { "all_configured": "Todos los puentes Philips Hue ya est\u00e1n configurados", "already_configured": "El puente ya esta configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.", "cannot_connect": "No se puede conectar al puente", "discover_timeout": "No se han descubierto puentes Philips Hue", "no_bridges": "No se han descubierto puentes Philips Hue.", + "not_hue_bridge": "No es un puente Hue", "unknown": "Se produjo un error desconocido" }, "error": { diff --git a/homeassistant/components/iaqualink/.translations/bg.json b/homeassistant/components/iaqualink/.translations/bg.json new file mode 100644 index 00000000000000..5b37bde3ee3a81 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 iAqualink." + }, + "error": { + "connection_failure": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 iAqualink. \u041f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 / \u0438\u043c\u0435\u0439\u043b \u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u041c\u043e\u043b\u044f \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f iAqualink \u0430\u043a\u0430\u0443\u043d\u0442.", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/es.json b/homeassistant/components/iaqualink/.translations/es.json index 7326d80497be4a..698be68bd78e9c 100644 --- a/homeassistant/components/iaqualink/.translations/es.json +++ b/homeassistant/components/iaqualink/.translations/es.json @@ -1,12 +1,21 @@ { "config": { + "abort": { + "already_setup": "Solo puede configurar una \u00fanica conexi\u00f3n iAqualink." + }, + "error": { + "connection_failure": "No se puede conectar a iAqualink. Verifica tu nombre de usuario y contrase\u00f1a." + }, "step": { "user": { "data": { "password": "Contrase\u00f1a", "username": "Usuario / correo electr\u00f3nico" - } + }, + "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a de su cuenta de iAqualink.", + "title": "Con\u00e9ctese a iAqualink" } - } + }, + "title": "Jandy iAqualink" } } \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/lb.json b/homeassistant/components/iaqualink/.translations/lb.json new file mode 100644 index 00000000000000..4beb11214bc2c8 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen eng eenzeg iAqualink Verbindung konfigur\u00e9ieren." + }, + "error": { + "connection_failure": "Kann sech net mat iAqualink verbannen. Iwwerpr\u00e9ift \u00e4ren Benotzernumm an Passwuert" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm / E-Mail Adresse" + }, + "description": "Gitt den Benotznumm an d'Passwuert fir \u00e4ren iAqualink Kont un.", + "title": "Mat iAqualink verbannen" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/no.json b/homeassistant/components/iaqualink/.translations/no.json new file mode 100644 index 00000000000000..9d464a6d516c55 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en enkel iAqualink-tilkobling." + }, + "error": { + "connection_failure": "Kan ikke koble til iAqualink. Sjekk brukernavnet og passordet ditt." + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn / E-postadresse" + }, + "description": "Vennligst skriv inn brukernavn og passord for iAqualink-kontoen din.", + "title": "Koble til iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/es.json b/homeassistant/components/life360/.translations/es.json index 28999de5e81b94..2b185cb1b6c476 100644 --- a/homeassistant/components/life360/.translations/es.json +++ b/homeassistant/components/life360/.translations/es.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Credenciales no v\u00e1lidas", "invalid_username": "Nombre de usuario no v\u00e1lido", + "unexpected": "Error inesperado al comunicarse con el servidor Life360", "user_already_configured": "La cuenta ya ha sido configurada" }, "step": { diff --git a/homeassistant/components/light/.translations/bg.json b/homeassistant/components/light/.translations/bg.json new file mode 100644 index 00000000000000..533ba76b6a7613 --- /dev/null +++ b/homeassistant/components/light/.translations/bg.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u041f\u0440\u0435\u0432\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b.", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json index 93dfc65bbe1c0a..fcbe835e4c2a9b 100644 --- a/homeassistant/components/light/.translations/es.json +++ b/homeassistant/components/light/.translations/es.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Alternar {entity_name}", "turn_off": "Apagar {entity_name}", "turn_on": "Encender {entity_name}" }, diff --git a/homeassistant/components/light/.translations/lb.json b/homeassistant/components/light/.translations/lb.json new file mode 100644 index 00000000000000..fdd76cda7e62fc --- /dev/null +++ b/homeassistant/components/light/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} \u00ebmschalten", + "turn_off": "{entity_name} ausschalten", + "turn_on": "{entity_name} uschalten" + }, + "condition_type": { + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un" + }, + "trigger_type": { + "turn_off": "{entity_name} gouf ausgeschalt", + "turn_on": "{entity_name} gouf ugeschalt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/no.json b/homeassistant/components/light/.translations/no.json index 39c391eff3356a..2241ca6644e9c0 100644 --- a/homeassistant/components/light/.translations/no.json +++ b/homeassistant/components/light/.translations/no.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Veksle {entity_name}", + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er av", + "is_on": "{entity_name} er p\u00e5" + }, "trigger_type": { "turn_off": "{name} sl\u00e5tt av", "turn_on": "{name} sl\u00e5tt p\u00e5" diff --git a/homeassistant/components/light/.translations/sl.json b/homeassistant/components/light/.translations/sl.json index afd59d619e0cdc..432c8ae37d15fb 100644 --- a/homeassistant/components/light/.translations/sl.json +++ b/homeassistant/components/light/.translations/sl.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} je vklopljen" }, "trigger_type": { - "turn_off": "{name} izklopljeno", - "turn_on": "{name} vklopljeno" + "turn_off": "{entity_name} izklopljen", + "turn_on": "{entity_name} vklopljen" } } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/bg.json b/homeassistant/components/linky/.translations/bg.json new file mode 100644 index 00000000000000..6eeb898ee1ffc8 --- /dev/null +++ b/homeassistant/components/linky/.translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b" + }, + "error": { + "access": "\u041d\u044f\u043c\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e Enedis.fr, \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0438", + "enedis": "Enedis.fr \u043e\u0442\u0433\u043e\u0432\u043e\u0440\u0438 \u0441 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)", + "username_exists": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b", + "wrong_login": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0432\u043b\u0438\u0437\u0430\u043d\u0435: \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0438\u043c\u0435\u0439\u043b\u0430 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "E-mail" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043d\u0434\u0435\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u0441\u0438 \u0434\u0430\u043d\u043d\u0438", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/es.json b/homeassistant/components/linky/.translations/es.json index 7c0d17c8a8f1e6..511f3c9d8e56f8 100644 --- a/homeassistant/components/linky/.translations/es.json +++ b/homeassistant/components/linky/.translations/es.json @@ -4,12 +4,22 @@ "username_exists": "Cuenta ya configurada" }, "error": { + "access": "No se pudo acceder a Enedis.fr, compruebe su conexi\u00f3n a Internet", + "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11:00 y las 2 de la ma\u00f1ana)", + "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 23:00 y las 02:00 horas).", + "username_exists": "Cuenta ya configurada", "wrong_login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" }, "step": { "user": { - "description": "Introduzca sus credenciales" + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "description": "Introduzca sus credenciales", + "title": "Linky" } - } + }, + "title": "Linky" } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/lb.json b/homeassistant/components/linky/.translations/lb.json new file mode 100644 index 00000000000000..d38002385590fa --- /dev/null +++ b/homeassistant/components/linky/.translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "username_exists": "Kont ass scho konfigur\u00e9iert" + }, + "error": { + "username_exists": "Kont ass scho konfigur\u00e9iert", + "wrong_login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + }, + "description": "F\u00ebllt \u00e4r Login Informatiounen aus", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/no.json b/homeassistant/components/linky/.translations/no.json new file mode 100644 index 00000000000000..c43f434562c152 --- /dev/null +++ b/homeassistant/components/linky/.translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Kontoen er allerede konfigurert" + }, + "error": { + "access": "Kunne ikke f\u00e5 tilgang til Enedis.fr, vennligst sjekk internettforbindelsen din", + "enedis": "Enedis.fr svarte med en feil: vennligst pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)", + "unknown": "Ukjent feil: pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)", + "username_exists": "Kontoen er allerede konfigurert", + "wrong_login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-post" + }, + "description": "Skriv inn legitimasjonen din", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/es.json b/homeassistant/components/met/.translations/es.json index 7659ab4d2962a8..a475518bd851cb 100644 --- a/homeassistant/components/met/.translations/es.json +++ b/homeassistant/components/met/.translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "El nombre ya existe" + "name_exists": "La ubicaci\u00f3n ya existe" }, "step": { "user": { @@ -14,6 +14,7 @@ "description": "Instituto de meteorolog\u00eda", "title": "Ubicaci\u00f3n" } - } + }, + "title": "Met.no" } } \ No newline at end of file diff --git a/homeassistant/components/met/.translations/lb.json b/homeassistant/components/met/.translations/lb.json index 660f639d859718..9f91d37c23360c 100644 --- a/homeassistant/components/met/.translations/lb.json +++ b/homeassistant/components/met/.translations/lb.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Numm g\u00ebtt et schonn" + "name_exists": "Standuert g\u00ebtt et schonn" }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/es.json b/homeassistant/components/plaato/.translations/es.json index e52a80be986370..ecb061e91c9c44 100644 --- a/homeassistant/components/plaato/.translations/es.json +++ b/homeassistant/components/plaato/.translations/es.json @@ -12,6 +12,7 @@ "description": "\u00bfEst\u00e1s seguro de que quieres configurar el Airlock de Plaato?", "title": "Configurar el webhook de Plaato" } - } + }, + "title": "Plaato Airlock" } } \ No newline at end of file diff --git a/homeassistant/components/point/.translations/es.json b/homeassistant/components/point/.translations/es.json index 33b6b1d38271a9..9a94e54dd5fa30 100644 --- a/homeassistant/components/point/.translations/es.json +++ b/homeassistant/components/point/.translations/es.json @@ -27,6 +27,6 @@ "title": "Proveedor de autenticaci\u00f3n" } }, - "title": "Point de Minut" + "title": "Minut Point" } } \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/lb.json b/homeassistant/components/ps4/.translations/lb.json index 17757cb9d20317..0986b0e0240afb 100644 --- a/homeassistant/components/ps4/.translations/lb.json +++ b/homeassistant/components/ps4/.translations/lb.json @@ -4,8 +4,8 @@ "credential_error": "Feeler beim ausliesen vun den Umeldungs Informatiounen.", "devices_configured": "All Apparater sinn schonn konfigur\u00e9iert", "no_devices_found": "Keng Playstation 4 am Netzwierk fonnt.", - "port_987_bind_error": "Konnt sech net mam Port 987 verbannen.", - "port_997_bind_error": "Konnt sech net mam Port 997 verbannen." + "port_987_bind_error": "Konnt sech net mam Port 987 verbannen. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen.", + "port_997_bind_error": "Konnt sech net mam Port 997 verbannen. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen." }, "error": { "credential_timeout": "Z\u00e4it Iwwerschreidung beim Service vun den Umeldungsinformatiounen. Dr\u00e9ck op ofsch\u00e9cke fir nach emol ze starten.", @@ -25,7 +25,7 @@ "name": "Numm", "region": "Regioun" }, - "description": "Gitt \u00e4r Playstation 4 Informatiounen an. Fir 'PIN', gitt an d'Astellunge vun der Playstation 4 Konsole. Dann op 'Mobile App Verbindungs Astellungen' a wielt \"Apparat dob\u00e4isetzen' aus. Gitt de PIN an deen ugewise g\u00ebtt.", + "description": "Gitt \u00e4r Playstation 4 Informatiounen an. Fir 'PIN', gitt an d'Astellunge vun der Playstation 4 Konsole. Dann op 'Mobile App Verbindungs Astellungen' a wielt \"Apparat dob\u00e4isetzen' aus. Gitt de PIN an deen ugewise g\u00ebtt. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/solaredge/.translations/bg.json b/homeassistant/components/solaredge/.translations/bg.json new file mode 100644 index 00000000000000..72f1ad2a4c758f --- /dev/null +++ b/homeassistant/components/solaredge/.translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "site_exists": "\u0422\u043e\u0432\u0430 site_id \u0432\u0435\u0447\u0435 \u0435 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u043e" + }, + "error": { + "site_exists": "\u0422\u043e\u0432\u0430 site_id \u0432\u0435\u0447\u0435 \u0435 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u043e" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447\u0430 \u0437\u0430 \u0442\u043e\u0437\u0438 \u0441\u0430\u0439\u0442", + "name": "\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435\u0442\u043e \u043d\u0430 \u0442\u0430\u0437\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f", + "site_id": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u044a\u0440\u044a\u0442 site-id \u043d\u0430 SolarEdge" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (API) \u0437\u0430 \u0442\u0430\u0437\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/es.json b/homeassistant/components/solaredge/.translations/es.json index 9f52511a165cff..8708729bf4aebd 100644 --- a/homeassistant/components/solaredge/.translations/es.json +++ b/homeassistant/components/solaredge/.translations/es.json @@ -1,13 +1,21 @@ { "config": { + "abort": { + "site_exists": "Este site_id ya est\u00e1 configurado" + }, + "error": { + "site_exists": "Este site_id ya est\u00e1 configurado" + }, "step": { "user": { "data": { "api_key": "La clave de la API para este sitio", - "name": "El nombre de esta instalaci\u00f3n" + "name": "El nombre de esta instalaci\u00f3n", + "site_id": "La identificaci\u00f3n del sitio de SolarEdge" }, "title": "Definir los par\u00e1metros de la API para esta instalaci\u00f3n" } - } + }, + "title": "SolarEdge" } } \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/lb.json b/homeassistant/components/solaredge/.translations/lb.json new file mode 100644 index 00000000000000..957a0187c1d6de --- /dev/null +++ b/homeassistant/components/solaredge/.translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert" + }, + "error": { + "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel fir d\u00ebsen Site", + "name": "Numm vun d\u00ebser Installatioun" + } + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/no.json b/homeassistant/components/solaredge/.translations/no.json new file mode 100644 index 00000000000000..ad7cb55316b696 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Denne site_id er allerede konfigurert" + }, + "error": { + "site_exists": "Denne site_id er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkelen for dette nettstedet", + "name": "Navnet p\u00e5 denne installasjonen", + "site_id": "SolarEdge nettsted-id" + }, + "title": "Definer API-parametrene for denne installasjonen" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/bg.json b/homeassistant/components/switch/.translations/bg.json new file mode 100644 index 00000000000000..31e41d3f504dea --- /dev/null +++ b/homeassistant/components/switch/.translations/bg.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u041f\u0440\u0435\u0432\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" + }, + "condition_type": { + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + }, + "trigger_type": { + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/es.json b/homeassistant/components/switch/.translations/es.json index 6749eab129331a..987d13a39403e8 100644 --- a/homeassistant/components/switch/.translations/es.json +++ b/homeassistant/components/switch/.translations/es.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Alternar {entity_name}", "turn_off": "Apagar {entity_name}", "turn_on": "Encender {entity_name}" }, diff --git a/homeassistant/components/switch/.translations/lb.json b/homeassistant/components/switch/.translations/lb.json new file mode 100644 index 00000000000000..291d8cb478f251 --- /dev/null +++ b/homeassistant/components/switch/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} \u00ebmschalten", + "turn_off": "{entity_name} ausschalten", + "turn_on": "{entity_name} uschalten" + }, + "condition_type": { + "turn_off": "{entity_name} gouf ausgeschalt", + "turn_on": "{entity_name} gouf ugeschalt" + }, + "trigger_type": { + "turn_off": "{entity_name} gouf ausgeschalt", + "turn_on": "{entity_name} gouf ugeschalt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/no.json b/homeassistant/components/switch/.translations/no.json new file mode 100644 index 00000000000000..8a00ac09549771 --- /dev/null +++ b/homeassistant/components/switch/.translations/no.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Veksle {entity_name}", + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} sl\u00e5tt av", + "turn_on": "{entity_name} sl\u00e5tt p\u00e5" + }, + "trigger_type": { + "turn_off": "{entity_name} sl\u00e5tt av", + "turn_on": "{entity_name} sl\u00e5tt p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json index b0313a1eee357a..677e0389d45b71 100644 --- a/homeassistant/components/tellduslive/.translations/es.json +++ b/homeassistant/components/tellduslive/.translations/es.json @@ -18,6 +18,7 @@ "data": { "host": "Host" }, + "description": "Vac\u00edo", "title": "Elige el punto final." } }, diff --git a/homeassistant/components/traccar/.translations/es.json b/homeassistant/components/traccar/.translations/es.json index b0b65a10c83cc9..dedaf02971c79f 100644 --- a/homeassistant/components/traccar/.translations/es.json +++ b/homeassistant/components/traccar/.translations/es.json @@ -12,6 +12,7 @@ "description": "\u00bfEst\u00e1 seguro de querer configurar Traccar?", "title": "Configurar Traccar" } - } + }, + "title": "Traccar" } } \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/es.json b/homeassistant/components/twentemilieu/.translations/es.json index 902e28b2080d87..60a412684f775b 100644 --- a/homeassistant/components/twentemilieu/.translations/es.json +++ b/homeassistant/components/twentemilieu/.translations/es.json @@ -4,7 +4,8 @@ "address_exists": "Direcci\u00f3n ya configurada." }, "error": { - "connection_error": "No se conect\u00f3." + "connection_error": "No se conect\u00f3.", + "invalid_address": "Direcci\u00f3n no encontrada en el \u00e1rea de servicio de Twente Milieu." }, "step": { "user": { @@ -12,8 +13,11 @@ "house_letter": "Letra de la casa/adicional", "house_number": "N\u00famero de casa", "post_code": "C\u00f3digo postal" - } + }, + "description": "Configure Twente Milieu proporcionando informaci\u00f3n sobre la recolecci\u00f3n de residuos en su direcci\u00f3n.", + "title": "Twente Milieu" } - } + }, + "title": "Twente Milieu" } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 8a8d8b11f57661..3741b035d7a9d6 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -32,6 +32,12 @@ "track_devices": "Segueix dispositius de la xarxa (dispositius Ubiquiti)", "track_wired_clients": "Inclou clients de xarxa per cable" } + }, + "init": { + "data": { + "one": "un", + "other": "altre" + } } } } diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 8b0eb56203700d..0539f5607b4c36 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -29,8 +29,15 @@ "data": { "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado", "track_clients": "Seguimiento de los clientes de red", + "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de red cableada" } + }, + "init": { + "data": { + "one": "uno", + "other": "otro" + } } } } diff --git a/homeassistant/components/velbus/.translations/es.json b/homeassistant/components/velbus/.translations/es.json index 1acaaa53ab2f11..1e1e8897c30155 100644 --- a/homeassistant/components/velbus/.translations/es.json +++ b/homeassistant/components/velbus/.translations/es.json @@ -4,14 +4,18 @@ "port_exists": "Este puerto ya est\u00e1 configurado" }, "error": { + "connection_failed": "La conexi\u00f3n velbus fall\u00f3", "port_exists": "Este puerto ya est\u00e1 configurado" }, "step": { "user": { "data": { + "name": "El nombre de esta conexi\u00f3n velbus", "port": "Cadena de conexi\u00f3n" - } + }, + "title": "Definir el tipo de conexi\u00f3n velbus" } - } + }, + "title": "Interfaz Velbus" } } \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/es.json b/homeassistant/components/vesync/.translations/es.json index 99611c5f9bfa79..856dc77a52c48d 100644 --- a/homeassistant/components/vesync/.translations/es.json +++ b/homeassistant/components/vesync/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_setup": "Solo se permite una instancia de Vesync" + }, "error": { "invalid_login": "Nombre de usuario o contrase\u00f1a no v\u00e1lidos" }, @@ -11,6 +14,7 @@ }, "title": "Introduzca el nombre de usuario y la contrase\u00f1a" } - } + }, + "title": "VeSync" } } \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/lb.json b/homeassistant/components/vesync/.translations/lb.json new file mode 100644 index 00000000000000..7d1dbad19f3bd8 --- /dev/null +++ b/homeassistant/components/vesync/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_login": "Ong\u00ebltege Benotzernumm oder Passwuert" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail Adresse" + }, + "title": "Benotznumm a Passwuert aginn" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json new file mode 100644 index 00000000000000..994d02aa7f5936 --- /dev/null +++ b/homeassistant/components/withings/.translations/lb.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "title": "Benotzer Profil." + } + } + } +} \ No newline at end of file From 0ef79da281ff0ad48a69b600a39920d13f8a36c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Sep 2019 01:23:31 -0600 Subject: [PATCH 0305/3953] Use Nabu Casa url if no https url set (#26682) * Use Nabu Casa url if no https url set * Update test_home_assistant_cast.py --- .../components/cast/home_assistant_cast.py | 12 +++++++++- .../cast/test_home_assistant_cast.py | 24 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index f604594bfc5887..d5d35ba7c9f933 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -40,10 +40,20 @@ async def async_setup_ha_cast( async def handle_show_view(call: core.ServiceCall): """Handle a Show View service call.""" + hass_url = hass.config.api.base_url + + # Home Assistant Cast only works with https urls. If user has no configured + # base url, use their remote url. + if not hass_url.lower().startswith("https://"): + try: + hass_url = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass + controller = HomeAssistantController( # If you are developing Home Assistant Cast, uncomment and set to your dev app id. # app_id="5FE44367", - hass_url=hass.config.api.base_url, + hass_url=hass_url, client_id=None, refresh_token=refresh_token.token, ) diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index e67b8f70160472..8db6fd4609e850 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,5 +1,5 @@ """Test Home Assistant Cast.""" -from unittest.mock import Mock +from unittest.mock import Mock, patch from homeassistant.components.cast import home_assistant_cast from tests.common import MockConfigEntry, async_mock_signal @@ -26,3 +26,25 @@ async def test_service_show_view(hass): assert controller.supporting_app_id == "B12CE3CA" assert entity_id == "media_player.kitchen" assert view_path == "mock_path" + + +async def test_use_cloud_url(hass): + """Test that we fall back to cloud url.""" + hass.config.api = Mock(base_url="http://example.com") + await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) + calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) + + with patch( + "homeassistant.components.cloud.async_remote_ui_url", + return_value="https://something.nabu.acas", + ): + await hass.services.async_call( + "cast", + "show_lovelace_view", + {"entity_id": "media_player.kitchen", "view_path": "mock_path"}, + blocking=True, + ) + + assert len(calls) == 1 + controller = calls[0][0] + assert controller.hass_url == "https://something.nabu.acas" From e0f1677296e6b7f06e393b4d341bc247de169d53 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 17 Sep 2019 09:34:34 +0200 Subject: [PATCH 0306/3953] Updated frontend to 20190917.0 (#26686) --- homeassistant/components/frontend/manifest.json | 4 +++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 7052ebfc15ad2e..955c4b90cc6440 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", - "requirements": ["home-assistant-frontend==20190911.1"], + "requirements": [ + "home-assistant-frontend==20190917.0" + ], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d635d178940e3e..b01efba8f0466e 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==20190911.1 +home-assistant-frontend==20190917.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 049e3a423c24f7..f59f0ba19ab185 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190911.1 +home-assistant-frontend==20190917.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7125d3e9ed5a02..297ed6bb866a46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190911.1 +home-assistant-frontend==20190917.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From fe8ff200bccfa289b6c262ffd73191206e6e8975 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Sep 2019 01:23:31 -0600 Subject: [PATCH 0307/3953] Use Nabu Casa url if no https url set (#26682) * Use Nabu Casa url if no https url set * Update test_home_assistant_cast.py --- .../components/cast/home_assistant_cast.py | 12 +++++++++- .../cast/test_home_assistant_cast.py | 24 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index f604594bfc5887..d5d35ba7c9f933 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -40,10 +40,20 @@ async def async_setup_ha_cast( async def handle_show_view(call: core.ServiceCall): """Handle a Show View service call.""" + hass_url = hass.config.api.base_url + + # Home Assistant Cast only works with https urls. If user has no configured + # base url, use their remote url. + if not hass_url.lower().startswith("https://"): + try: + hass_url = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass + controller = HomeAssistantController( # If you are developing Home Assistant Cast, uncomment and set to your dev app id. # app_id="5FE44367", - hass_url=hass.config.api.base_url, + hass_url=hass_url, client_id=None, refresh_token=refresh_token.token, ) diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index e67b8f70160472..8db6fd4609e850 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,5 +1,5 @@ """Test Home Assistant Cast.""" -from unittest.mock import Mock +from unittest.mock import Mock, patch from homeassistant.components.cast import home_assistant_cast from tests.common import MockConfigEntry, async_mock_signal @@ -26,3 +26,25 @@ async def test_service_show_view(hass): assert controller.supporting_app_id == "B12CE3CA" assert entity_id == "media_player.kitchen" assert view_path == "mock_path" + + +async def test_use_cloud_url(hass): + """Test that we fall back to cloud url.""" + hass.config.api = Mock(base_url="http://example.com") + await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) + calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) + + with patch( + "homeassistant.components.cloud.async_remote_ui_url", + return_value="https://something.nabu.acas", + ): + await hass.services.async_call( + "cast", + "show_lovelace_view", + {"entity_id": "media_player.kitchen", "view_path": "mock_path"}, + blocking=True, + ) + + assert len(calls) == 1 + controller = calls[0][0] + assert controller.hass_url == "https://something.nabu.acas" From 8d44e0cc0c6645a43959f5116ce04a36d7f03283 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 17 Sep 2019 09:34:34 +0200 Subject: [PATCH 0308/3953] Updated frontend to 20190917.0 (#26686) --- homeassistant/components/frontend/manifest.json | 4 +++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 7052ebfc15ad2e..955c4b90cc6440 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", - "requirements": ["home-assistant-frontend==20190911.1"], + "requirements": [ + "home-assistant-frontend==20190917.0" + ], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d635d178940e3e..b01efba8f0466e 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==20190911.1 +home-assistant-frontend==20190917.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2e2e51587dcdaa..a656cd80fffab8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190911.1 +home-assistant-frontend==20190917.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7125d3e9ed5a02..297ed6bb866a46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190911.1 +home-assistant-frontend==20190917.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 1b57ea51be762680da88e7919971a75b98e678ed Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 17 Sep 2019 07:43:09 +0000 Subject: [PATCH 0309/3953] Bump version to 0.99.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index eafe547bb7c509..3cc75e9dc2beed 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 99 -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 4be0c057d29d0750208a0716b0e2add9190ff20d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 17 Sep 2019 11:44:43 +0200 Subject: [PATCH 0310/3953] Fix Nuki issues (#26689) * Fix Nuki issues * remove stale code * Add comments * Fix lint --- CODEOWNERS | 2 +- homeassistant/components/nuki/__init__.py | 2 + homeassistant/components/nuki/lock.py | 90 +++++++-------------- homeassistant/components/nuki/manifest.json | 8 +- 4 files changed, 35 insertions(+), 67 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index fe5e19f9115f37..c454514912ca51 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -195,7 +195,7 @@ homeassistant/components/notify/* @home-assistant/core homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte -homeassistant/components/nuki/* @pschmitt +homeassistant/components/nuki/* @pvizeli homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 2e15ac8a68d052..c8b19082585c12 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1 +1,3 @@ """The nuki component.""" + +DOMAIN = "nuki" diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index dc0ae1f22495db..7fda26b290041d 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,20 +1,18 @@ """Nuki.io lock platform.""" from datetime import timedelta import logging -import requests +from pynuki import NukiBridge +from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.components.lock import ( - DOMAIN, - PLATFORM_SCHEMA, - LockDevice, - SUPPORT_OPEN, -) +from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockDevice from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids +from . import DOMAIN + _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 8080 @@ -30,7 +28,8 @@ NUKI_DATA = "nuki" SERVICE_LOCK_N_GO = "lock_n_go" -SERVICE_CHECK_CONNECTION = "check_connection" + +ERROR_STATES = (0, 254, 255) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -47,48 +46,30 @@ } ) -CHECK_CONNECTION_SERVICE_SCHEMA = vol.Schema( - {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids} -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nuki lock platform.""" - from pynuki import NukiBridge - bridge = NukiBridge( config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], DEFAULT_TIMEOUT ) - add_entities([NukiLock(lock) for lock in bridge.locks]) + devices = [NukiLock(lock) for lock in bridge.locks] def service_handler(service): """Service handler for nuki services.""" entity_ids = extract_entity_ids(hass, service) - all_locks = hass.data[NUKI_DATA][DOMAIN] - target_locks = [] - if not entity_ids: - target_locks = all_locks - else: - for lock in all_locks: - if lock.entity_id in entity_ids: - target_locks.append(lock) - for lock in target_locks: - if service.service == SERVICE_LOCK_N_GO: - unlatch = service.data[ATTR_UNLATCH] - lock.lock_n_go(unlatch=unlatch) - elif service.service == SERVICE_CHECK_CONNECTION: - lock.check_connection() + unlatch = service.data[ATTR_UNLATCH] + + for lock in devices: + if lock.entity_id not in entity_ids: + continue + lock.lock_n_go(unlatch=unlatch) hass.services.register( - "nuki", SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA - ) - hass.services.register( - "nuki", - SERVICE_CHECK_CONNECTION, - service_handler, - schema=CHECK_CONNECTION_SERVICE_SCHEMA, + DOMAIN, SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA ) + add_entities(devices) + class NukiLock(LockDevice): """Representation of a Nuki lock.""" @@ -99,15 +80,7 @@ def __init__(self, nuki_lock): self._locked = nuki_lock.is_locked self._name = nuki_lock.name self._battery_critical = nuki_lock.battery_critical - self._available = nuki_lock.state != 255 - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - if NUKI_DATA not in self.hass.data: - self.hass.data[NUKI_DATA] = {} - if DOMAIN not in self.hass.data[NUKI_DATA]: - self.hass.data[NUKI_DATA][DOMAIN] = [] - self.hass.data[NUKI_DATA][DOMAIN].append(self) + self._available = nuki_lock.state not in ERROR_STATES @property def name(self): @@ -140,13 +113,19 @@ def available(self) -> bool: def update(self): """Update the nuki lock properties.""" - try: - self._nuki_lock.update(aggressive=False) - except requests.exceptions.RequestException: - self._available = False - return + for level in (False, True): + try: + self._nuki_lock.update(aggressive=level) + except RequestException: + _LOGGER.warning("Network issues detect with %s", self.name) + self._available = False + return + + # If in error state, we force an update and repoll data + self._available = self._nuki_lock.state not in ERROR_STATES + if self._available: + break - self._available = True self._name = self._nuki_lock.name self._locked = self._nuki_lock.is_locked self._battery_critical = self._nuki_lock.battery_critical @@ -170,12 +149,3 @@ def lock_n_go(self, unlatch=False, **kwargs): amount of time depending on the lock settings) and relock. """ self._nuki_lock.lock_n_go(unlatch, kwargs) - - def check_connection(self, **kwargs): - """Update the nuki lock properties.""" - try: - self._nuki_lock.update(aggressive=True) - except requests.exceptions.RequestException: - self._available = False - else: - self._available = self._nuki_lock.state != 255 diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 932b80690c4d2c..e7f078a1a0594a 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -2,11 +2,7 @@ "domain": "nuki", "name": "Nuki", "documentation": "https://www.home-assistant.io/components/nuki", - "requirements": [ - "pynuki==1.3.3" - ], + "requirements": ["pynuki==1.3.3"], "dependencies": [], - "codeowners": [ - "@pschmitt" - ] + "codeowners": ["@pvizeli"] } From b7f7d545d173759ac2c1282143847621c418ee58 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 17 Sep 2019 13:48:01 +0200 Subject: [PATCH 0311/3953] Bump connect-box library to fix logging (#26690) --- homeassistant/components/upc_connect/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 53bd7fc5820a5b..efa38286e7e2f6 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "upc_connect", "name": "Upc connect", "documentation": "https://www.home-assistant.io/components/upc_connect", - "requirements": ["connect-box==0.2.3"], + "requirements": ["connect-box==0.2.4"], "dependencies": [], "codeowners": ["@pvizeli"] } diff --git a/requirements_all.txt b/requirements_all.txt index f59f0ba19ab185..0e432bff8bb7be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -355,7 +355,7 @@ colorlog==4.0.2 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.3 +connect-box==0.2.4 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart From a3bdbf3188a6dfd6cd77294a519213ea5bf881be Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 17 Sep 2019 15:41:49 +0200 Subject: [PATCH 0312/3953] Updated frontend to 20190917.1 (#26691) --- 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 955c4b90cc6440..01823882f9d371 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==20190917.0" + "home-assistant-frontend==20190917.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b01efba8f0466e..43a22cb980c079 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==20190917.0 +home-assistant-frontend==20190917.1 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0e432bff8bb7be..c2be62c2325666 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.0 +home-assistant-frontend==20190917.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 297ed6bb866a46..b00194b0d91be8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.0 +home-assistant-frontend==20190917.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 6853f99e3040dbb237088195946190b24f0316e8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 17 Sep 2019 11:44:43 +0200 Subject: [PATCH 0313/3953] Fix Nuki issues (#26689) * Fix Nuki issues * remove stale code * Add comments * Fix lint --- CODEOWNERS | 2 +- homeassistant/components/nuki/__init__.py | 2 + homeassistant/components/nuki/lock.py | 90 +++++++-------------- homeassistant/components/nuki/manifest.json | 8 +- 4 files changed, 35 insertions(+), 67 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 18218bbf68e0f9..1e45bcee4ecff9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -195,7 +195,7 @@ homeassistant/components/notify/* @home-assistant/core homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte -homeassistant/components/nuki/* @pschmitt +homeassistant/components/nuki/* @pvizeli homeassistant/components/nws/* @MatthewFlamm homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 2e15ac8a68d052..c8b19082585c12 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1 +1,3 @@ """The nuki component.""" + +DOMAIN = "nuki" diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index dc0ae1f22495db..7fda26b290041d 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,20 +1,18 @@ """Nuki.io lock platform.""" from datetime import timedelta import logging -import requests +from pynuki import NukiBridge +from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.components.lock import ( - DOMAIN, - PLATFORM_SCHEMA, - LockDevice, - SUPPORT_OPEN, -) +from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockDevice from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids +from . import DOMAIN + _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 8080 @@ -30,7 +28,8 @@ NUKI_DATA = "nuki" SERVICE_LOCK_N_GO = "lock_n_go" -SERVICE_CHECK_CONNECTION = "check_connection" + +ERROR_STATES = (0, 254, 255) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -47,48 +46,30 @@ } ) -CHECK_CONNECTION_SERVICE_SCHEMA = vol.Schema( - {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids} -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nuki lock platform.""" - from pynuki import NukiBridge - bridge = NukiBridge( config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], DEFAULT_TIMEOUT ) - add_entities([NukiLock(lock) for lock in bridge.locks]) + devices = [NukiLock(lock) for lock in bridge.locks] def service_handler(service): """Service handler for nuki services.""" entity_ids = extract_entity_ids(hass, service) - all_locks = hass.data[NUKI_DATA][DOMAIN] - target_locks = [] - if not entity_ids: - target_locks = all_locks - else: - for lock in all_locks: - if lock.entity_id in entity_ids: - target_locks.append(lock) - for lock in target_locks: - if service.service == SERVICE_LOCK_N_GO: - unlatch = service.data[ATTR_UNLATCH] - lock.lock_n_go(unlatch=unlatch) - elif service.service == SERVICE_CHECK_CONNECTION: - lock.check_connection() + unlatch = service.data[ATTR_UNLATCH] + + for lock in devices: + if lock.entity_id not in entity_ids: + continue + lock.lock_n_go(unlatch=unlatch) hass.services.register( - "nuki", SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA - ) - hass.services.register( - "nuki", - SERVICE_CHECK_CONNECTION, - service_handler, - schema=CHECK_CONNECTION_SERVICE_SCHEMA, + DOMAIN, SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA ) + add_entities(devices) + class NukiLock(LockDevice): """Representation of a Nuki lock.""" @@ -99,15 +80,7 @@ def __init__(self, nuki_lock): self._locked = nuki_lock.is_locked self._name = nuki_lock.name self._battery_critical = nuki_lock.battery_critical - self._available = nuki_lock.state != 255 - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - if NUKI_DATA not in self.hass.data: - self.hass.data[NUKI_DATA] = {} - if DOMAIN not in self.hass.data[NUKI_DATA]: - self.hass.data[NUKI_DATA][DOMAIN] = [] - self.hass.data[NUKI_DATA][DOMAIN].append(self) + self._available = nuki_lock.state not in ERROR_STATES @property def name(self): @@ -140,13 +113,19 @@ def available(self) -> bool: def update(self): """Update the nuki lock properties.""" - try: - self._nuki_lock.update(aggressive=False) - except requests.exceptions.RequestException: - self._available = False - return + for level in (False, True): + try: + self._nuki_lock.update(aggressive=level) + except RequestException: + _LOGGER.warning("Network issues detect with %s", self.name) + self._available = False + return + + # If in error state, we force an update and repoll data + self._available = self._nuki_lock.state not in ERROR_STATES + if self._available: + break - self._available = True self._name = self._nuki_lock.name self._locked = self._nuki_lock.is_locked self._battery_critical = self._nuki_lock.battery_critical @@ -170,12 +149,3 @@ def lock_n_go(self, unlatch=False, **kwargs): amount of time depending on the lock settings) and relock. """ self._nuki_lock.lock_n_go(unlatch, kwargs) - - def check_connection(self, **kwargs): - """Update the nuki lock properties.""" - try: - self._nuki_lock.update(aggressive=True) - except requests.exceptions.RequestException: - self._available = False - else: - self._available = self._nuki_lock.state != 255 diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 932b80690c4d2c..e7f078a1a0594a 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -2,11 +2,7 @@ "domain": "nuki", "name": "Nuki", "documentation": "https://www.home-assistant.io/components/nuki", - "requirements": [ - "pynuki==1.3.3" - ], + "requirements": ["pynuki==1.3.3"], "dependencies": [], - "codeowners": [ - "@pschmitt" - ] + "codeowners": ["@pvizeli"] } From d8ccc7751f45e3ae8767c88b988fb652848ea4ec Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 17 Sep 2019 13:48:01 +0200 Subject: [PATCH 0314/3953] Bump connect-box library to fix logging (#26690) --- homeassistant/components/upc_connect/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 53bd7fc5820a5b..efa38286e7e2f6 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "upc_connect", "name": "Upc connect", "documentation": "https://www.home-assistant.io/components/upc_connect", - "requirements": ["connect-box==0.2.3"], + "requirements": ["connect-box==0.2.4"], "dependencies": [], "codeowners": ["@pvizeli"] } diff --git a/requirements_all.txt b/requirements_all.txt index a656cd80fffab8..2bb709be2d038a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -355,7 +355,7 @@ colorlog==4.0.2 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.3 +connect-box==0.2.4 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart From 7d79d281c19c5a26f7255c2a96fe60c0e1042dfc Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 17 Sep 2019 15:41:49 +0200 Subject: [PATCH 0315/3953] Updated frontend to 20190917.1 (#26691) --- 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 955c4b90cc6440..01823882f9d371 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==20190917.0" + "home-assistant-frontend==20190917.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b01efba8f0466e..43a22cb980c079 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==20190917.0 +home-assistant-frontend==20190917.1 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2bb709be2d038a..e260d7237a5473 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.0 +home-assistant-frontend==20190917.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 297ed6bb866a46..b00194b0d91be8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.0 +home-assistant-frontend==20190917.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From e9fe90a8731488c193e83eac6daeb98ae99c347f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 17 Sep 2019 15:47:39 +0200 Subject: [PATCH 0316/3953] Bump version to 0.99.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3cc75e9dc2beed..c18f0ea0fd0a06 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 99 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 15bb12f48eddf2416519f6568da1f1b118aeb533 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 17 Sep 2019 15:59:12 +0200 Subject: [PATCH 0317/3953] Fix release access for bram (#26693) --- 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 7c88e615fa5baa..29e68a5d7acc16 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -43,7 +43,7 @@ stages: release="$(Build.SourceBranchName)" created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')" - if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480)$ ]]; then + if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten)$ ]]; then exit 0 fi From 12f68af1076f4dd1ae41b5b4006317f9a8679d5e Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 17 Sep 2019 08:53:12 -0700 Subject: [PATCH 0318/3953] Switch py_nextbus to py_nextbusnext (#26681) The orignal package maintainer seems to be unresponsive. I've forked the package and added the bug fixes to the new fork Fixes #24561 --- homeassistant/components/nextbus/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 63bdbf8a928e35..5c5a095c8f4cef 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/components/nextbus", "dependencies": [], "codeowners": ["@vividboarder"], - "requirements": ["py_nextbus==0.1.2"] + "requirements": ["py_nextbusnext==0.1.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index c2be62c2325666..80b79563e85bcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,7 @@ pyW215==0.6.0 pyW800rf32==0.1 # homeassistant.components.nextbus -py_nextbus==0.1.2 +py_nextbusnext==0.1.4 # homeassistant.components.noaa_tides # py_noaa==0.3.0 From ed13cab8d66b76582b51e1ef49c465134f918fe7 Mon Sep 17 00:00:00 2001 From: gibman Date: Tue, 17 Sep 2019 20:22:39 +0200 Subject: [PATCH 0319/3953] Disconnect velux on hass stop (#26266) * velux KLF200 device did not disconnect properly when rebooting the hass device. disconnect is now being called on the 'EVENT_HOMEASSISTANT_STOP' event * removed comment * removed comment * trigger bot * trigger bot * trigger bot * logging casing fixed. code moved from init. * logger level debug logger level moved from info to debug only config[DOMAIN] exposed to module imports moved to top * DOMAIN part of config passed to module. * removed trailing whitespaces etc. * black --fast changes * added missing docstring * D400 First line should end with a period * black formatting --- homeassistant/components/velux/__init__.py | 30 +++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 4c21bb7fdefbf5..51f615e68aaf5a 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -1,11 +1,12 @@ """Support for VELUX KLF 200 devices.""" import logging - import voluptuous as vol +from pyvlx import PyVLX +from pyvlx import PyVLXException from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP DOMAIN = "velux" DATA_VELUX = "data_velux" @@ -24,10 +25,9 @@ async def async_setup(hass, config): """Set up the velux component.""" - from pyvlx import PyVLXException - try: - hass.data[DATA_VELUX] = VeluxModule(hass, config) + hass.data[DATA_VELUX] = VeluxModule(hass, config[DOMAIN]) + hass.data[DATA_VELUX].setup() await hass.data[DATA_VELUX].async_start() except PyVLXException as ex: @@ -44,15 +44,27 @@ async def async_setup(hass, config): class VeluxModule: """Abstraction for velux component.""" - def __init__(self, hass, config): + def __init__(self, hass, domain_config): """Initialize for velux component.""" - from pyvlx import PyVLX + self.pyvlx = None + self._hass = hass + self._domain_config = domain_config + + def setup(self): + """Velux component setup.""" + + async def on_hass_stop(event): + """Close connection when hass stops.""" + _LOGGER.debug("Velux interface terminated") + await self.pyvlx.disconnect() - host = config[DOMAIN].get(CONF_HOST) - password = config[DOMAIN].get(CONF_PASSWORD) + self._hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + host = self._domain_config.get(CONF_HOST) + password = self._domain_config.get(CONF_PASSWORD) self.pyvlx = PyVLX(host=host, password=password) async def async_start(self): """Start velux component.""" + _LOGGER.debug("Velux interface started") await self.pyvlx.load_scenes() await self.pyvlx.load_nodes() From 4060f1346a2576c9e0799cbb7908c59182f6c88e Mon Sep 17 00:00:00 2001 From: Jesse Rizzo <32472573+jesserizzo@users.noreply.github.com> Date: Tue, 17 Sep 2019 13:24:03 -0500 Subject: [PATCH 0320/3953] Improve Envoy detection and support multiple Envoys (#26665) * Bump envoy_reader to 0.8.6, fix missing dependency * Support for optional name in config * Replace str.format with f-strings --- homeassistant/components/enphase_envoy/sensor.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 2cc46632ddaf58..13784e24d77fc9 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, + CONF_NAME, POWER_WATT, ENERGY_WATT_HOUR, ) @@ -44,6 +45,7 @@ vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( cv.ensure_list, [vol.In(list(SENSORS))] ), + vol.Optional(CONF_NAME, default=""): cv.string, } ) @@ -54,6 +56,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] + name = config[CONF_NAME] entities = [] # Iterate through the list of sensors @@ -66,14 +69,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= Envoy( ip_address, condition, - "{} {}".format(SENSORS[condition][0], inverter), + f"{name}{SENSORS[condition][0]} {inverter}", SENSORS[condition][1], ) ) else: entities.append( Envoy( - ip_address, condition, SENSORS[condition][0], SENSORS[condition][1] + ip_address, + condition, + f"{name}{SENSORS[condition][0]}", + SENSORS[condition][1], ) ) async_add_entities(entities) From 39edc45e4e82a10d1cbc6ffeccb8e83a3460a121 Mon Sep 17 00:00:00 2001 From: zewelor Date: Tue, 17 Sep 2019 20:29:46 +0200 Subject: [PATCH 0321/3953] Fix volumio set shuffle (#26660) --- homeassistant/components/volumio/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 8bd1952a6507d0..7c13488c3f573e 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -306,7 +306,7 @@ def async_mute_volume(self, mute): def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" return self.send_volumio_msg( - "commands", params={"cmd": "random", "value": str(shuffle)} + "commands", params={"cmd": "random", "value": str(shuffle).lower()} ) def async_select_source(self, source): From c17057de4b3b14792b9646d82e0a39bb27e5327d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 17 Sep 2019 21:00:17 +0200 Subject: [PATCH 0322/3953] Fix mysensors validation for composite entities (#26666) * Composite entities require multiple value types to be present in child values to function. Any of those value types should trigger an entity update if updated. * Always write platform v names as sets. * Run black. --- homeassistant/components/mysensors/const.py | 106 +++++++++--------- homeassistant/components/mysensors/helpers.py | 17 +-- 2 files changed, 63 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index d12ecd9d3a61a9..45f603a2cb44f8 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -26,72 +26,72 @@ UPDATE_DELAY = 0.1 BINARY_SENSOR_TYPES = { - "S_DOOR": "V_TRIPPED", - "S_MOTION": "V_TRIPPED", - "S_SMOKE": "V_TRIPPED", - "S_SPRINKLER": "V_TRIPPED", - "S_WATER_LEAK": "V_TRIPPED", - "S_SOUND": "V_TRIPPED", - "S_VIBRATION": "V_TRIPPED", - "S_MOISTURE": "V_TRIPPED", + "S_DOOR": {"V_TRIPPED"}, + "S_MOTION": {"V_TRIPPED"}, + "S_SMOKE": {"V_TRIPPED"}, + "S_SPRINKLER": {"V_TRIPPED"}, + "S_WATER_LEAK": {"V_TRIPPED"}, + "S_SOUND": {"V_TRIPPED"}, + "S_VIBRATION": {"V_TRIPPED"}, + "S_MOISTURE": {"V_TRIPPED"}, } -CLIMATE_TYPES = {"S_HVAC": "V_HVAC_FLOW_STATE"} +CLIMATE_TYPES = {"S_HVAC": {"V_HVAC_FLOW_STATE"}} -COVER_TYPES = {"S_COVER": ["V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"]} +COVER_TYPES = {"S_COVER": {"V_DIMMER", "V_PERCENTAGE", "V_LIGHT", "V_STATUS"}} -DEVICE_TRACKER_TYPES = {"S_GPS": "V_POSITION"} +DEVICE_TRACKER_TYPES = {"S_GPS": {"V_POSITION"}} LIGHT_TYPES = { - "S_DIMMER": ["V_DIMMER", "V_PERCENTAGE"], - "S_RGB_LIGHT": "V_RGB", - "S_RGBW_LIGHT": "V_RGBW", + "S_DIMMER": {"V_DIMMER", "V_PERCENTAGE"}, + "S_RGB_LIGHT": {"V_RGB"}, + "S_RGBW_LIGHT": {"V_RGBW"}, } -NOTIFY_TYPES = {"S_INFO": "V_TEXT"} +NOTIFY_TYPES = {"S_INFO": {"V_TEXT"}} SENSOR_TYPES = { - "S_SOUND": "V_LEVEL", - "S_VIBRATION": "V_LEVEL", - "S_MOISTURE": "V_LEVEL", - "S_INFO": "V_TEXT", - "S_GPS": "V_POSITION", - "S_TEMP": "V_TEMP", - "S_HUM": "V_HUM", - "S_BARO": ["V_PRESSURE", "V_FORECAST"], - "S_WIND": ["V_WIND", "V_GUST", "V_DIRECTION"], - "S_RAIN": ["V_RAIN", "V_RAINRATE"], - "S_UV": "V_UV", - "S_WEIGHT": ["V_WEIGHT", "V_IMPEDANCE"], - "S_POWER": ["V_WATT", "V_KWH", "V_VAR", "V_VA", "V_POWER_FACTOR"], - "S_DISTANCE": "V_DISTANCE", - "S_LIGHT_LEVEL": ["V_LIGHT_LEVEL", "V_LEVEL"], - "S_IR": "V_IR_RECEIVE", - "S_WATER": ["V_FLOW", "V_VOLUME"], - "S_CUSTOM": ["V_VAR1", "V_VAR2", "V_VAR3", "V_VAR4", "V_VAR5", "V_CUSTOM"], - "S_SCENE_CONTROLLER": ["V_SCENE_ON", "V_SCENE_OFF"], - "S_COLOR_SENSOR": "V_RGB", - "S_MULTIMETER": ["V_VOLTAGE", "V_CURRENT", "V_IMPEDANCE"], - "S_GAS": ["V_FLOW", "V_VOLUME"], - "S_WATER_QUALITY": ["V_TEMP", "V_PH", "V_ORP", "V_EC"], - "S_AIR_QUALITY": ["V_DUST_LEVEL", "V_LEVEL"], - "S_DUST": ["V_DUST_LEVEL", "V_LEVEL"], + "S_SOUND": {"V_LEVEL"}, + "S_VIBRATION": {"V_LEVEL"}, + "S_MOISTURE": {"V_LEVEL"}, + "S_INFO": {"V_TEXT"}, + "S_GPS": {"V_POSITION"}, + "S_TEMP": {"V_TEMP"}, + "S_HUM": {"V_HUM"}, + "S_BARO": {"V_PRESSURE", "V_FORECAST"}, + "S_WIND": {"V_WIND", "V_GUST", "V_DIRECTION"}, + "S_RAIN": {"V_RAIN", "V_RAINRATE"}, + "S_UV": {"V_UV"}, + "S_WEIGHT": {"V_WEIGHT", "V_IMPEDANCE"}, + "S_POWER": {"V_WATT", "V_KWH", "V_VAR", "V_VA", "V_POWER_FACTOR"}, + "S_DISTANCE": {"V_DISTANCE"}, + "S_LIGHT_LEVEL": {"V_LIGHT_LEVEL", "V_LEVEL"}, + "S_IR": {"V_IR_RECEIVE"}, + "S_WATER": {"V_FLOW", "V_VOLUME"}, + "S_CUSTOM": {"V_VAR1", "V_VAR2", "V_VAR3", "V_VAR4", "V_VAR5", "V_CUSTOM"}, + "S_SCENE_CONTROLLER": {"V_SCENE_ON", "V_SCENE_OFF"}, + "S_COLOR_SENSOR": {"V_RGB"}, + "S_MULTIMETER": {"V_VOLTAGE", "V_CURRENT", "V_IMPEDANCE"}, + "S_GAS": {"V_FLOW", "V_VOLUME"}, + "S_WATER_QUALITY": {"V_TEMP", "V_PH", "V_ORP", "V_EC"}, + "S_AIR_QUALITY": {"V_DUST_LEVEL", "V_LEVEL"}, + "S_DUST": {"V_DUST_LEVEL", "V_LEVEL"}, } SWITCH_TYPES = { - "S_LIGHT": "V_LIGHT", - "S_BINARY": "V_STATUS", - "S_DOOR": "V_ARMED", - "S_MOTION": "V_ARMED", - "S_SMOKE": "V_ARMED", - "S_SPRINKLER": "V_STATUS", - "S_WATER_LEAK": "V_ARMED", - "S_SOUND": "V_ARMED", - "S_VIBRATION": "V_ARMED", - "S_MOISTURE": "V_ARMED", - "S_IR": "V_IR_SEND", - "S_LOCK": "V_LOCK_STATUS", - "S_WATER_QUALITY": "V_STATUS", + "S_LIGHT": {"V_LIGHT"}, + "S_BINARY": {"V_STATUS"}, + "S_DOOR": {"V_ARMED"}, + "S_MOTION": {"V_ARMED"}, + "S_SMOKE": {"V_ARMED"}, + "S_SPRINKLER": {"V_STATUS"}, + "S_WATER_LEAK": {"V_ARMED"}, + "S_SOUND": {"V_ARMED"}, + "S_VIBRATION": {"V_ARMED"}, + "S_MOISTURE": {"V_ARMED"}, + "S_IR": {"V_IR_SEND"}, + "S_LOCK": {"V_LOCK_STATUS"}, + "S_WATER_QUALITY": {"V_STATUS"}, } diff --git a/homeassistant/components/mysensors/helpers.py b/homeassistant/components/mysensors/helpers.py index fda89158293bb2..f0e9b06b762a4c 100644 --- a/homeassistant/components/mysensors/helpers.py +++ b/homeassistant/components/mysensors/helpers.py @@ -121,20 +121,23 @@ def validate_child(gateway, node_id, child, value_type=None): child_type_name = next( (member.name for member in pres if member.value == child.type), None ) - value_types = [value_type] if value_type else [*child.values] - value_type_names = [ + value_types = {value_type} if value_type else {*child.values} + value_type_names = { member.name for member in set_req if member.value in value_types - ] + } platforms = TYPE_TO_PLATFORMS.get(child_type_name, []) if not platforms: _LOGGER.warning("Child type %s is not supported", child.type) return validated for platform in platforms: - v_names = FLAT_PLATFORM_TYPES[platform, child_type_name] - if not isinstance(v_names, list): - v_names = [v_names] - v_names = [v_name for v_name in v_names if v_name in value_type_names] + platform_v_names = FLAT_PLATFORM_TYPES[platform, child_type_name] + v_names = platform_v_names & value_type_names + if not v_names: + child_value_names = { + member.name for member in set_req if member.value in child.values + } + v_names = platform_v_names & child_value_names for v_name in v_names: child_schema_gen = SCHEMAS.get((platform, v_name), default_schema) From 10572a62b1d87c33386791c019d5fecebf5ceb0c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Sep 2019 21:12:54 +0200 Subject: [PATCH 0323/3953] Add support for automation description (#26662) * Add support for automation annotation * Rename annotation to description --- homeassistant/components/automation/__init__.py | 2 ++ homeassistant/components/config/automation.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 03eedd6d16213e..9e08a9cff1fc40 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -43,6 +43,7 @@ GROUP_NAME_ALL_AUTOMATIONS = "all automations" CONF_ALIAS = "alias" +CONF_DESCRIPTION = "description" CONF_HIDE_ENTITY = "hide_entity" CONF_CONDITION = "condition" @@ -95,6 +96,7 @@ def _platform_validator(config): # str on purpose CONF_ID: str, CONF_ALIAS: cv.string, + vol.Optional(CONF_DESCRIPTION): cv.string, vol.Optional(CONF_INITIAL_STATE): cv.boolean, vol.Optional(CONF_HIDE_ENTITY, default=DEFAULT_HIDE_ENTITY): cv.boolean, vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 7ca71fc4f93dfb..17efdba3fb573f 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -53,7 +53,7 @@ def _write_value(self, hass, data, config_key, new_value): # Iterate through some keys that we want to have ordered in the output updated_value = OrderedDict() - for key in ("id", "alias", "trigger", "condition", "action"): + for key in ("id", "alias", "description", "trigger", "condition", "action"): if key in cur_value: updated_value[key] = cur_value[key] if key in new_value: From 504b8c7685089e13ed8ac0545622faab7b1b6a36 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Sep 2019 21:55:01 +0200 Subject: [PATCH 0324/3953] Fix translation, adjust trigger names (#26635) --- homeassistant/components/device_automation/const.py | 2 ++ .../components/device_automation/toggle_entity.py | 10 ++++++---- homeassistant/components/light/strings.json | 4 ++-- homeassistant/components/switch/strings.json | 8 ++++---- tests/components/device_automation/test_init.py | 4 ++-- tests/components/light/test_device_automation.py | 8 ++++---- tests/components/switch/test_device_automation.py | 8 ++++---- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/device_automation/const.py b/homeassistant/components/device_automation/const.py index a668c78598a289..40bfc4ca0a13e4 100644 --- a/homeassistant/components/device_automation/const.py +++ b/homeassistant/components/device_automation/const.py @@ -4,3 +4,5 @@ CONF_TOGGLE = "toggle" CONF_TURN_OFF = "turn_off" CONF_TURN_ON = "turn_on" +CONF_TURNED_OFF = "turned_off" +CONF_TURNED_ON = "turned_on" diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 722b92e33f2603..1593e70771aea3 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -8,6 +8,8 @@ CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON, + CONF_TURNED_OFF, + CONF_TURNED_ON, ) from homeassistant.core import split_entity_id from homeassistant.const import ( @@ -53,12 +55,12 @@ { # Trigger when entity is turned off CONF_PLATFORM: "device", - CONF_TYPE: CONF_TURN_OFF, + CONF_TYPE: CONF_TURNED_OFF, }, { # Trigger when entity is turned on CONF_PLATFORM: "device", - CONF_TYPE: CONF_TURN_ON, + CONF_TYPE: CONF_TURNED_ON, }, ] @@ -87,7 +89,7 @@ vol.Required(CONF_DEVICE_ID): str, vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In([CONF_TURN_OFF, CONF_TURN_ON]), + vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), } ) @@ -136,7 +138,7 @@ def async_condition_from_config(config, config_validation): async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] - if trigger_type == CONF_TURN_ON: + if trigger_type == CONF_TURNED_ON: from_state = "off" to_state = "on" else: diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 460114b143cc78..77b842ba07833a 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -10,8 +10,8 @@ "is_off": "{entity_name} is off" }, "trigger_type": { - "turn_on": "{entity_name} turned on", - "turn_off": "{entity_name} turned off" + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" } } } diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index 857b3763076143..77b842ba07833a 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -6,12 +6,12 @@ "turn_off": "Turn off {entity_name}" }, "condition_type": { - "turn_on": "{entity_name} turned on", - "turn_off": "{entity_name} turned off" + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" }, "trigger_type": { - "turn_on": "{entity_name} turned on", - "turn_off": "{entity_name} turned off" + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" } } } diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index a01dad03d46276..b05c04a16f1dc5 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -133,14 +133,14 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r { "platform": "device", "domain": "light", - "type": "turn_off", + "type": "turned_off", "device_id": device_entry.id, "entity_id": "light.test_5678", }, { "platform": "device", "domain": "light", - "type": "turn_on", + "type": "turned_on", "device_id": device_entry.id, "entity_id": "light.test_5678", }, diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py index 3525f1121c08e2..27b8b860d7227c 100644 --- a/tests/components/light/test_device_automation.py +++ b/tests/components/light/test_device_automation.py @@ -125,14 +125,14 @@ async def test_get_triggers(hass, device_reg, entity_reg): { "platform": "device", "domain": DOMAIN, - "type": "turn_off", + "type": "turned_off", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { "platform": "device", "domain": DOMAIN, - "type": "turn_on", + "type": "turned_on", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, @@ -163,7 +163,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_on", + "type": "turned_on", }, "action": { "service": "test.automation", @@ -187,7 +187,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_off", + "type": "turned_off", }, "action": { "service": "test.automation", diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py index 3bd29a72a12549..1ebe4785761aa5 100644 --- a/tests/components/switch/test_device_automation.py +++ b/tests/components/switch/test_device_automation.py @@ -125,14 +125,14 @@ async def test_get_triggers(hass, device_reg, entity_reg): { "platform": "device", "domain": DOMAIN, - "type": "turn_off", + "type": "turned_off", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { "platform": "device", "domain": DOMAIN, - "type": "turn_on", + "type": "turned_on", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, @@ -163,7 +163,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_on", + "type": "turned_on", }, "action": { "service": "test.automation", @@ -187,7 +187,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_off", + "type": "turned_off", }, "action": { "service": "test.automation", From 9114ed36cd106f167d455f286b1d56c3be23b5de Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 17 Sep 2019 22:39:46 +0200 Subject: [PATCH 0325/3953] Fix cert expiry config flow check and update (#26638) * Fix typo in translations * Work on bug #26619 * readd the homeassistant.start event * Remove the callback * Added the executor_job for _test_connection * Update test_config_flow.py --- .../components/cert_expiry/.translations/en.json | 2 +- homeassistant/components/cert_expiry/__init__.py | 14 +++----------- .../components/cert_expiry/config_flow.py | 8 +++++--- homeassistant/components/cert_expiry/sensor.py | 16 +++++++++++++++- tests/components/cert_expiry/test_config_flow.py | 4 ++-- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index 873dfee9a92bb9..51bd522f2c4491 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -21,4 +21,4 @@ }, "title": "Certificate Expiry" } -} \ No newline at end of file +} diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index ab68d5ba08bc43..7c7efea7333120 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -1,7 +1,5 @@ """The cert_expiry component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType @@ -13,13 +11,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Load the saved entities.""" - @callback - def async_start(_): - """Load the entry after the start event.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start) - + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) return True diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index dd3463fff95c4e..d73762ce882647 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -38,10 +38,12 @@ def _prt_in_configuration_exists(self, user_input) -> bool: return True return False - def _test_connection(self, user_input=None): + async def _test_connection(self, user_input=None): """Test connection to the server and try to get the certtificate.""" try: - get_cert(user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT)) + await self.hass.async_add_executor_job( + get_cert, user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT) + ) return True except socket.gaierror: self._errors[CONF_HOST] = "resolve_failed" @@ -59,7 +61,7 @@ async def async_step_user(self, user_input=None): if self._prt_in_configuration_exists(user_input): self._errors[CONF_HOST] = "host_port_exists" else: - if self._test_connection(user_input): + if await self._test_connection(user_input): host = user_input[CONF_HOST] name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) prt = user_input.get(CONF_PORT, DEFAULT_PORT) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index fccfb295c0fff2..b564cff7338584 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -9,7 +9,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT +from homeassistant.const import ( + CONF_NAME, + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_START, +) from homeassistant.helpers.entity import Entity from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT @@ -82,6 +87,15 @@ def available(self): """Icon to use in the frontend, if any.""" return self._available + async def async_added_to_hass(self): + """Once the entity is added we should update to get the initial data loaded.""" + + def do_update(_): + """Run the update method when the start event was fired.""" + self.update() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) + def update(self): """Fetch the certificate information.""" try: diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index f8c99496a563eb..f44e65512e35e5 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.cert_expiry.const import DEFAULT_PORT from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_coro NAME = "Cert Expiry test 1 2 3" PORT = 443 @@ -20,7 +20,7 @@ def mock_controller(): """Mock a successfull _prt_in_configuration_exists.""" with patch( "homeassistant.components.cert_expiry.config_flow.CertexpiryConfigFlow._test_connection", - return_value=True, + side_effect=lambda *_: mock_coro(True), ): yield From c6fc677f5b96c7fe63e4048a98cf45ac05293dd0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Sep 2019 13:45:48 -0700 Subject: [PATCH 0326/3953] Verify withings config (#26698) --- .../components/withings/config_flow.py | 5 ++- .../components/withings/strings.json | 5 ++- tests/components/withings/test_config_flow.py | 35 ++++++------------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index 23cc74281e8b9b..f28a4f59d80195 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -88,7 +88,10 @@ async def async_step_import(self, user_input=None): async def async_step_user(self, user_input=None): """Create an entry for selecting a profile.""" - flow = self.hass.data.get(DATA_FLOW_IMPL, {}) + flow = self.hass.data.get(DATA_FLOW_IMPL) + + if not flow: + return self.async_abort(reason="no_flows") if user_input: return await self.async_step_auth(user_input) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 88b8e6d5ea0944..1a99abc7255651 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -12,6 +12,9 @@ }, "create_entry": { "default": "Successfully authenticated with Withings for the selected profile." + }, + "abort": { + "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation." } } -} \ No newline at end of file +} diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 93b9a434b7f1ac..3ae9d11c3b6555 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -3,9 +3,7 @@ from asynctest import CoroutineMock, MagicMock import pytest -from homeassistant import setup, data_entry_flow -import homeassistant.components.api as api -import homeassistant.components.http as http +from homeassistant import data_entry_flow from homeassistant.components.withings import const from homeassistant.components.withings.config_flow import ( register_flow_implementation, @@ -24,27 +22,6 @@ def flow_handler_fixture(hass: HomeAssistantType): return flow_handler -@pytest.fixture(name="setup_hass") -async def setup_hass_fixture(hass: HomeAssistantType): - """Provide hass instance.""" - config = { - http.DOMAIN: {}, - api.DOMAIN: {"base_url": "http://localhost/"}, - const.DOMAIN: { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_secret", - const.PROFILES: ["Person 1", "Person 2"], - }, - } - - hass.data = {} - - await setup.async_setup_component(hass, "http", config) - await setup.async_setup_component(hass, "api", config) - - return hass - - def test_flow_handler_init(flow_handler: WithingsFlowHandler): """Test the init of the flow handler.""" assert not flow_handler.flow_profile @@ -173,3 +150,13 @@ async def test_auth_callback_view_get(hass: HomeAssistantType): "my_flow_id", {const.PROFILE: "my_profile", const.CODE: "my_code"} ) hass.config_entries.flow.async_configure.reset_mock() + + +async def test_init_without_config(hass): + """Try initializin a configg flow without it being configured.""" + result = await hass.config_entries.flow.async_init( + "withings", context={"source": "user"} + ) + + assert result["type"] == "abort" + assert result["reason"] == "no_flows" From d33ecbb5bed47e014637b31e5d5c3c87d33f3cd0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 17 Sep 2019 22:46:11 +0200 Subject: [PATCH 0327/3953] Updated frontend to 20190917.2 (#26696) --- 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 01823882f9d371..3d5860d0a4350c 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==20190917.1" + "home-assistant-frontend==20190917.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 43a22cb980c079..de5c823d999c28 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==20190917.1 +home-assistant-frontend==20190917.2 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 80b79563e85bcc..05a3e89b6757bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.1 +home-assistant-frontend==20190917.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b00194b0d91be8..07fc31ec6efd4e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.1 +home-assistant-frontend==20190917.2 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From a390cf7c6a0e36f13b7d551a7d9b4b752db96d78 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 18 Sep 2019 00:32:12 +0000 Subject: [PATCH 0328/3953] [ci skip] Translation update --- homeassistant/components/cert_expiry/.translations/en.json | 2 +- .../components/homekit_controller/.translations/fr.json | 2 +- homeassistant/components/light/.translations/ca.json | 4 +++- homeassistant/components/light/.translations/en.json | 4 +++- homeassistant/components/light/.translations/es.json | 4 +++- homeassistant/components/light/.translations/fr.json | 4 ++-- homeassistant/components/ps4/.translations/fr.json | 4 ++-- homeassistant/components/switch/.translations/ca.json | 6 +++++- homeassistant/components/switch/.translations/en.json | 6 +++++- homeassistant/components/switch/.translations/es.json | 6 +++++- homeassistant/components/upnp/.translations/ca.json | 4 ++++ homeassistant/components/withings/.translations/en.json | 3 +++ 12 files changed, 37 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index 51bd522f2c4491..873dfee9a92bb9 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -21,4 +21,4 @@ }, "title": "Certificate Expiry" } -} +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/fr.json b/homeassistant/components/homekit_controller/.translations/fr.json index 15e50a4012701c..7f0566ddd42365 100644 --- a/homeassistant/components/homekit_controller/.translations/fr.json +++ b/homeassistant/components/homekit_controller/.translations/fr.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "Code d\u2019appairage" }, - "description": "Entrez votre code de jumelage HomeKit pour utiliser cet accessoire.", + "description": "Entrez votre code de jumelage HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire.", "title": "Appairer avec l'accessoire HomeKit" }, "user": { diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json index 5017af8e576f64..4cdf3d9042cde2 100644 --- a/homeassistant/components/light/.translations/ca.json +++ b/homeassistant/components/light/.translations/ca.json @@ -11,7 +11,9 @@ }, "trigger_type": { "turn_off": "{name} apagat", - "turn_on": "{name} enc\u00e8s" + "turn_on": "{name} enc\u00e8s", + "turned_off": "{entity_name} apagat", + "turned_on": "{entity_name} enc\u00e8s" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/en.json b/homeassistant/components/light/.translations/en.json index 60ccbd99348d7d..225d64be231ea6 100644 --- a/homeassistant/components/light/.translations/en.json +++ b/homeassistant/components/light/.translations/en.json @@ -11,7 +11,9 @@ }, "trigger_type": { "turn_off": "{entity_name} turned off", - "turn_on": "{entity_name} turned on" + "turn_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json index fcbe835e4c2a9b..533c4b3c0f3486 100644 --- a/homeassistant/components/light/.translations/es.json +++ b/homeassistant/components/light/.translations/es.json @@ -11,7 +11,9 @@ }, "trigger_type": { "turn_off": "{entity_name} apagada", - "turn_on": "{entity_name} encendida" + "turn_on": "{entity_name} encendida", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json index 6ab87274409320..f0aabef2ae742d 100644 --- a/homeassistant/components/light/.translations/fr.json +++ b/homeassistant/components/light/.translations/fr.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} est allum\u00e9" }, "trigger_type": { - "turn_off": "{name} d\u00e9sactiv\u00e9", - "turn_on": "{name} activ\u00e9" + "turn_off": "{entity_name} d\u00e9sactiv\u00e9", + "turn_on": "{entity_name} activ\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/fr.json b/homeassistant/components/ps4/.translations/fr.json index 03baf0c032e3a3..991222d45be78b 100644 --- a/homeassistant/components/ps4/.translations/fr.json +++ b/homeassistant/components/ps4/.translations/fr.json @@ -4,8 +4,8 @@ "credential_error": "Erreur lors de l'extraction des informations d'identification.", "devices_configured": "Tous les p\u00e9riph\u00e9riques trouv\u00e9s sont d\u00e9j\u00e0 configur\u00e9s.", "no_devices_found": "Aucun appareil PlayStation 4 trouv\u00e9 sur le r\u00e9seau.", - "port_987_bind_error": "Impossible de se connecter au port 997.", - "port_997_bind_error": "Impossible de se connecter au port 997." + "port_987_bind_error": "Impossible de se connecter au port 997. Reportez-vous \u00e0 la [documentation] (https://www.home-assistant.io/components/ps4/) pour plus d'informations.", + "port_997_bind_error": "Impossible de se connecter au port 997. Reportez-vous \u00e0 la [documentation] (https://www.home-assistant.io/components/ps4/) pour plus d'informations." }, "error": { "credential_timeout": "Le service d'informations d'identification a expir\u00e9. Appuyez sur soumettre pour red\u00e9marrer.", diff --git a/homeassistant/components/switch/.translations/ca.json b/homeassistant/components/switch/.translations/ca.json index c97565ddfe6a7e..6fea704f756d49 100644 --- a/homeassistant/components/switch/.translations/ca.json +++ b/homeassistant/components/switch/.translations/ca.json @@ -6,12 +6,16 @@ "turn_on": "Activa {entity_name}" }, "condition_type": { + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s", "turn_off": "{entity_name} desactivat", "turn_on": "{entity_name} activat" }, "trigger_type": { "turn_off": "{entity_name} desactivat", - "turn_on": "{entity_name} activat" + "turn_on": "{entity_name} activat", + "turned_off": "{entity_name} apagat", + "turned_on": "{entity_name} enc\u00e8s" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/en.json b/homeassistant/components/switch/.translations/en.json index 5be333cbf13bf7..ed036023755c40 100644 --- a/homeassistant/components/switch/.translations/en.json +++ b/homeassistant/components/switch/.translations/en.json @@ -6,12 +6,16 @@ "turn_on": "Turn on {entity_name}" }, "condition_type": { + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on", "turn_off": "{entity_name} turned off", "turn_on": "{entity_name} turned on" }, "trigger_type": { "turn_off": "{entity_name} turned off", - "turn_on": "{entity_name} turned on" + "turn_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/es.json b/homeassistant/components/switch/.translations/es.json index 987d13a39403e8..b38928fa753dce 100644 --- a/homeassistant/components/switch/.translations/es.json +++ b/homeassistant/components/switch/.translations/es.json @@ -6,12 +6,16 @@ "turn_on": "Encender {entity_name}" }, "condition_type": { + "is_off": "{entity_name} est\u00e1 apagada", + "is_on": "{entity_name} est\u00e1 encendida", "turn_off": "{entity_name} apagado", "turn_on": "{entity_name} encendido" }, "trigger_type": { "turn_off": "{entity_name} apagado", - "turn_on": "{entity_name} encendido" + "turn_on": "{entity_name} encendido", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ca.json b/homeassistant/components/upnp/.translations/ca.json index 161b5d85599147..28ad9ce954d995 100644 --- a/homeassistant/components/upnp/.translations/ca.json +++ b/homeassistant/components/upnp/.translations/ca.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Activa, com a m\u00ednim, els sensors o l'assignaci\u00f3 de ports", "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de UPnP/IGD." }, + "error": { + "one": "un", + "other": "altre" + }, "step": { "confirm": { "description": "Vols configurar UPnP/IGD?", diff --git a/homeassistant/components/withings/.translations/en.json b/homeassistant/components/withings/.translations/en.json index 2b906dd80030fb..16ce491e776d02 100644 --- a/homeassistant/components/withings/.translations/en.json +++ b/homeassistant/components/withings/.translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation." + }, "create_entry": { "default": "Successfully authenticated with Withings for the selected profile." }, From 72baf563fa040150a24b560c3052831475faf3a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 18 Sep 2019 08:30:59 +0300 Subject: [PATCH 0329/3953] Add alternative name for Tibber sensors (#26685) * Add alternative name for Tibber sensors * refactor tibber sensor --- homeassistant/components/tibber/sensor.py | 68 +++++++++-------------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 3dfe0265bdeef5..a5a7f320d93315 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -44,8 +44,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(dev, True) -class TibberSensorElPrice(Entity): - """Representation of an Tibber sensor for el price.""" +class TibberSensor(Entity): + """Representation of a generic Tibber sensor.""" def __init__(self, tibber_home): """Initialize the sensor.""" @@ -54,10 +54,25 @@ def __init__(self, tibber_home): self._state = None self._is_available = False self._device_state_attributes = {} - self._unit_of_measurement = self._tibber_home.price_unit - self._name = "Electricity price {}".format( - tibber_home.info["viewer"]["home"]["appNickname"] - ) + self._name = tibber_home.info["viewer"]["home"]["appNickname"] + if self._name is None: + self._name = tibber_home.info["viewer"]["home"]["address"].get( + "address1", "" + ) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._device_state_attributes + + @property + def state(self): + """Return the state of the device.""" + return self._state + + +class TibberSensorElPrice(TibberSensor): + """Representation of a Tibber sensor for el price.""" async def async_update(self): """Get the latest data and updates the states.""" @@ -86,11 +101,6 @@ async def async_update(self): self._device_state_attributes.update(attrs) self._is_available = self._state is not None - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._device_state_attributes - @property def available(self): """Return True if entity is available.""" @@ -99,12 +109,7 @@ def available(self): @property def name(self): """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state + return "Electricity price {}".format(self._name) @property def icon(self): @@ -114,7 +119,7 @@ def icon(self): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return self._unit_of_measurement + return self._tibber_home.price_unit @property def unique_id(self): @@ -139,17 +144,8 @@ async def _fetch_data(self): ]["estimatedAnnualConsumption"] -class TibberSensorRT(Entity): - """Representation of an Tibber sensor for real time consumption.""" - - def __init__(self, tibber_home): - """Initialize the sensor.""" - self._tibber_home = tibber_home - self._state = None - self._device_state_attributes = {} - self._unit_of_measurement = "W" - nickname = tibber_home.info["viewer"]["home"]["appNickname"] - self._name = f"Real time consumption {nickname}" +class TibberSensorRT(TibberSensor): + """Representation of a Tibber sensor for real time consumption.""" async def async_added_to_hass(self): """Start unavailability tracking.""" @@ -175,11 +171,6 @@ async def _async_callback(self, payload): self.async_schedule_update_ha_state() - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._device_state_attributes - @property def available(self): """Return True if entity is available.""" @@ -188,18 +179,13 @@ def available(self): @property def name(self): """Return the name of the sensor.""" - return self._name + return "Real time consumption {}".format(self._name) @property def should_poll(self): """Return the polling state.""" return False - @property - def state(self): - """Return the state of the device.""" - return self._state - @property def icon(self): """Return the icon to use in the frontend.""" @@ -208,7 +194,7 @@ def icon(self): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return self._unit_of_measurement + return "W" @property def unique_id(self): From 8a39924b37486bf806467eafefaa654c33ada45b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 18 Sep 2019 12:47:26 +0200 Subject: [PATCH 0330/3953] deCONZ improve light tests (#26697) * Improve light tests * Small improvements on light and group classes --- homeassistant/components/deconz/cover.py | 1 - homeassistant/components/deconz/light.py | 16 +- homeassistant/components/deconz/switch.py | 1 - tests/components/deconz/test_cover.py | 190 +++++------ tests/components/deconz/test_light.py | 365 ++++++++++++---------- tests/components/deconz/test_switch.py | 218 +++++++------ 6 files changed, 431 insertions(+), 360 deletions(-) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index b82144d37c739a..bcd408c25a7591 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -17,7 +17,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index ec1dfd2bcb1901..bf4b05089a84c0 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -34,7 +34,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -194,9 +193,6 @@ def device_state_attributes(self): attributes = {} attributes["is_deconz_group"] = self._device.type == "LightGroup" - if self._device.type == "LightGroup": - attributes["all_on"] = self._device.all_on - return attributes @@ -207,9 +203,7 @@ def __init__(self, device, gateway): """Set up group and create an unique id.""" super().__init__(device, gateway) - self._unique_id = "{}-{}".format( - self.gateway.api.config.bridgeid, self._device.deconz_id - ) + self._unique_id = f"{self.gateway.api.config.bridgeid}-{self._device.deconz_id}" @property def unique_id(self): @@ -228,3 +222,11 @@ def device_info(self): "name": self._device.name, "via_device": (DECONZ_DOMAIN, bridgeid), } + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = dict(super().device_state_attributes) + attributes["all_on"] = self._device.all_on + + return attributes diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index b1fd4b10f46065..1b51256580aeb7 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -10,7 +10,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 2de70f6d247c30..246c2bae7c561c 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -1,82 +1,83 @@ """deCONZ cover platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy + +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.components.deconz.const import COVER_TYPES -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.components.cover as cover -from tests.common import mock_coro - -SUPPORTED_COVERS = { +COVERS = { "1": { - "id": "Cover 1 id", - "name": "Cover 1 name", + "id": "Level controllable cover id", + "name": "Level controllable cover", "type": "Level controllable output", "state": {"bri": 255, "on": False, "reachable": True}, "modelid": "Not zigbee spec", "uniqueid": "00:00:00:00:00:00:00:00-00", }, "2": { - "id": "Cover 2 id", - "name": "Cover 2 name", + "id": "Window covering device id", + "name": "Window covering device", "type": "Window covering device", "state": {"bri": 255, "on": True, "reachable": True}, "modelid": "lumi.curtain", + "uniqueid": "00:00:00:00:00:00:00:01-00", }, -} - -UNSUPPORTED_COVER = { - "1": { - "id": "Cover id", - "name": "Unsupported switch", + "3": { + "id": "Unsupported cover id", + "name": "Unsupported cover", "type": "Not a cover", - "state": {}, - } + "state": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, } +BRIDGEID = "0123456789" 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_BRIDGEID: BRIDGEID, deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, +} -async def setup_gateway(hass, data): - """Load the deCONZ cover platform.""" - from pydeconz import DeconzSession +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - loop = Mock() - session = Mock() +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - - await hass.config_entries.async_forward_entry_setup(config_entry, "cover") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -92,64 +93,69 @@ async def test_platform_manually_configured(hass): async def test_no_covers(hass): """Test that no cover entities are created.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_cover(hass): """Test that all supported cover entities are created.""" - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - gateway = await setup_gateway(hass, {"lights": SUPPORTED_COVERS}) - assert "cover.cover_1_name" in gateway.deconz_ids - assert len(SUPPORTED_COVERS) == len(COVER_TYPES) - assert len(hass.states.async_all()) == 3 - - cover_1 = hass.states.get("cover.cover_1_name") - assert cover_1 is not None - assert cover_1.state == "open" - - gateway.api.lights["1"].async_update({}) - - await hass.services.async_call( - "cover", "open_cover", {"entity_id": "cover.cover_1_name"}, blocking=True - ) - await hass.services.async_call( - "cover", "close_cover", {"entity_id": "cover.cover_1_name"}, blocking=True - ) - await hass.services.async_call( - "cover", "stop_cover", {"entity_id": "cover.cover_1_name"}, blocking=True + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = deepcopy(COVERS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) + assert "cover.level_controllable_cover" in gateway.deconz_ids + assert "cover.window_covering_device" in gateway.deconz_ids + assert "cover.unsupported_cover" not in gateway.deconz_ids + assert len(hass.states.async_all()) == 5 - await hass.services.async_call( - "cover", "close_cover", {"entity_id": "cover.cover_2_name"}, blocking=True - ) + level_controllable_cover = hass.states.get("cover.level_controllable_cover") + assert level_controllable_cover.state == "open" + level_controllable_cover_device = gateway.api.lights["1"] -async def test_add_new_cover(hass): - """Test successful creation of cover entity.""" - data = {} - gateway = await setup_gateway(hass, data) - cover = Mock() - cover.name = "name" - cover.type = "Level controllable output" - cover.uniqueid = "1" - cover.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [cover]) + level_controllable_cover_device.async_update({"state": {"on": True}}) await hass.async_block_till_done() - assert "cover.name" in gateway.deconz_ids - - -async def test_unsupported_cover(hass): - """Test that unsupported covers are not created.""" - await setup_gateway(hass, {"lights": UNSUPPORTED_COVER}) - assert len(hass.states.async_all()) == 0 - -async def test_unload_cover(hass): - """Test that it works to unload switch entities.""" - gateway = await setup_gateway(hass, {"lights": SUPPORTED_COVERS}) - - await gateway.async_reset() - - assert len(hass.states.async_all()) == 1 + level_controllable_cover = hass.states.get("cover.level_controllable_cover") + assert level_controllable_cover.state == "closed" + + with patch.object( + level_controllable_cover_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + cover.DOMAIN, + cover.SERVICE_OPEN_COVER, + {"entity_id": "cover.level_controllable_cover"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"on": False}) + + with patch.object( + level_controllable_cover_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + cover.DOMAIN, + cover.SERVICE_CLOSE_COVER, + {"entity_id": "cover.level_controllable_cover"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"on": True, "bri": 255}) + + with patch.object( + level_controllable_cover_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + cover.DOMAIN, + cover.SERVICE_STOP_COVER, + {"entity_id": "cover.level_controllable_cover"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"bri_inc": 0}) diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index ecce762f51c4f3..50a5b2adacabcb 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -1,49 +1,28 @@ """deCONZ light platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy + +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.components.light as light -from tests.common import mock_coro - - -LIGHT = { +GROUPS = { "1": { - "id": "Light 1 id", - "name": "Light 1 name", - "state": { - "on": True, - "bri": 255, - "colormode": "xy", - "xy": (500, 500), - "reachable": True, - }, - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, - "2": { - "id": "Light 2 id", - "name": "Light 2 name", - "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, - }, -} - -GROUP = { - "1": { - "id": "Group 1 id", - "name": "Group 1 name", + "id": "Light group id", + "name": "Light group", "type": "LightGroup", - "state": {}, + "state": {"all_on": False, "any_on": True}, "action": {}, "scenes": [], "lights": ["1", "2"], }, "2": { - "id": "Group 2 id", - "name": "Group 2 name", + "id": "Empty group id", + "name": "Empty group", + "type": "LightGroup", "state": {}, "action": {}, "scenes": [], @@ -51,60 +30,80 @@ }, } -SWITCH = { +LIGHTS = { "1": { - "id": "Switch 1 id", - "name": "Switch 1 name", + "id": "RGB light id", + "name": "RGB light", + "state": { + "on": True, + "bri": 255, + "colormode": "xy", + "effect": "colorloop", + "xy": (500, 500), + "reachable": True, + }, + "type": "Extended color light", + "uniqueid": "00:00:00:00:00:00:00:00-00", + }, + "2": { + "id": "Tunable white light id", + "name": "Tunable white light", + "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, + "type": "Tunable white light", + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "3": { + "id": "On off switch id", + "name": "On off switch", "type": "On/Off plug-in unit", - "state": {}, - } + "state": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, } +BRIDGEID = "0123456789" ENTRY_CONFIG = { deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, 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, +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, } +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} -async def setup_gateway(hass, data, allow_deconz_groups=True): - """Load the deCONZ light platform.""" - from pydeconz import DeconzSession - - loop = Mock() - session = Mock() - - ENTRY_OPTIONS[deconz.const.CONF_ALLOW_DECONZ_GROUPS] = allow_deconz_groups +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, - options=ENTRY_OPTIONS, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - await hass.config_entries.async_forward_entry_setup(config_entry, "light") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -120,119 +119,157 @@ async def test_platform_manually_configured(hass): async def test_no_lights_or_groups(hass): """Test that no lights or groups entities are created.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_lights_and_groups(hass): """Test that lights or groups entities are created.""" - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - gateway = await setup_gateway(hass, {"lights": LIGHT, "groups": GROUP}) - assert "light.light_1_name" in gateway.deconz_ids - assert "light.light_2_name" in gateway.deconz_ids - assert "light.group_1_name" in gateway.deconz_ids - assert "light.group_2_name" not in gateway.deconz_ids - assert len(hass.states.async_all()) == 4 - - lamp_1 = hass.states.get("light.light_1_name") - assert lamp_1 is not None - assert lamp_1.state == "on" - assert lamp_1.attributes["brightness"] == 255 - assert lamp_1.attributes["hs_color"] == (224.235, 100.0) - - light_2 = hass.states.get("light.light_2_name") - assert light_2 is not None - assert light_2.state == "on" - assert light_2.attributes["color_temp"] == 2500 - - gateway.api.lights["1"].async_update({}) - - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.light_1_name", - "color_temp": 2500, - "brightness": 200, - "transition": 5, - "flash": "short", - "effect": "colorloop", - }, - blocking=True, - ) - await hass.services.async_call( - "light", - "turn_on", - { - "entity_id": "light.light_1_name", - "hs_color": (20, 30), - "flash": "long", - "effect": "None", - }, - blocking=True, - ) - await hass.services.async_call( - "light", - "turn_off", - {"entity_id": "light.light_1_name", "transition": 5, "flash": "short"}, - blocking=True, - ) - await hass.services.async_call( - "light", - "turn_off", - {"entity_id": "light.light_1_name", "flash": "long"}, - blocking=True, + data = deepcopy(DECONZ_WEB_REQUEST) + data["groups"] = deepcopy(GROUPS) + data["lights"] = deepcopy(LIGHTS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) - - -async def test_add_new_light(hass): - """Test successful creation of light entities.""" - gateway = await setup_gateway(hass, {}) - light = Mock() - light.name = "name" - light.uniqueid = "1" - light.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [light]) + assert "light.rgb_light" in gateway.deconz_ids + assert "light.tunable_white_light" in gateway.deconz_ids + assert "light.light_group" in gateway.deconz_ids + assert "light.empty_group" not in gateway.deconz_ids + assert "light.on_off_switch" not in gateway.deconz_ids + # 4 entities + 2 groups (one for switches and one for lights) + assert len(hass.states.async_all()) == 6 + + rgb_light = hass.states.get("light.rgb_light") + assert rgb_light.state == "on" + assert rgb_light.attributes["brightness"] == 255 + assert rgb_light.attributes["hs_color"] == (224.235, 100.0) + assert rgb_light.attributes["is_deconz_group"] is False + + tunable_white_light = hass.states.get("light.tunable_white_light") + assert tunable_white_light.state == "on" + assert tunable_white_light.attributes["color_temp"] == 2500 + + light_group = hass.states.get("light.light_group") + assert light_group.state == "on" + assert light_group.attributes["all_on"] is False + + empty_group = hass.states.get("light.empty_group") + assert empty_group is None + + rgb_light_device = gateway.api.lights["1"] + + rgb_light_device.async_update({"state": {"on": False}}) await hass.async_block_till_done() - assert "light.name" in gateway.deconz_ids + rgb_light = hass.states.get("light.rgb_light") + assert rgb_light.state == "off" + + with patch.object( + rgb_light_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_ON, + { + "entity_id": "light.rgb_light", + "color_temp": 2500, + "brightness": 200, + "transition": 5, + "flash": "short", + "effect": "colorloop", + }, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "/lights/1/state", + { + "ct": 2500, + "bri": 200, + "transitiontime": 50, + "alert": "select", + "effect": "colorloop", + }, + ) -async def test_add_new_group(hass): - """Test successful creation of group entities.""" - gateway = await setup_gateway(hass, {}) - group = Mock() - group.name = "name" - group.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("group"), [group]) - await hass.async_block_till_done() - assert "light.name" in gateway.deconz_ids + with patch.object( + rgb_light_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_ON, + { + "entity_id": "light.rgb_light", + "hs_color": (20, 30), + "flash": "long", + "effect": "None", + }, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "/lights/1/state", + {"xy": (0.411, 0.351), "alert": "lselect", "effect": "none"}, + ) + with patch.object( + rgb_light_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_OFF, + {"entity_id": "light.rgb_light", "transition": 5, "flash": "short"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with( + "/lights/1/state", {"bri": 0, "transitiontime": 50, "alert": "select"} + ) -async def test_do_not_add_deconz_groups(hass): - """Test that clip sensors can be ignored.""" - gateway = await setup_gateway(hass, {}, allow_deconz_groups=False) - group = Mock() - group.name = "name" - group.type = "LightGroup" - group.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("group"), [group]) - await hass.async_block_till_done() - assert len(gateway.deconz_ids) == 0 + with patch.object( + rgb_light_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + light.DOMAIN, + light.SERVICE_TURN_OFF, + {"entity_id": "light.rgb_light", "flash": "long"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"alert": "lselect"}) -async def test_no_switch(hass): - """Test that a switch doesn't get created as a light entity.""" - gateway = await setup_gateway(hass, {"lights": SWITCH}) - assert len(gateway.deconz_ids) == 0 - assert len(hass.states.async_all()) == 0 +async def test_disable_light_groups(hass): + """Test successful creation of sensor entities.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["groups"] = deepcopy(GROUPS) + data["lights"] = deepcopy(LIGHTS) + gateway = await setup_deconz_integration( + hass, + ENTRY_CONFIG, + options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False}, + get_state_response=data, + ) + assert "light.rgb_light" in gateway.deconz_ids + assert "light.tunable_white_light" in gateway.deconz_ids + assert "light.light_group" not in gateway.deconz_ids + assert "light.empty_group" not in gateway.deconz_ids + assert "light.on_off_switch" not in gateway.deconz_ids + # 4 entities + 2 groups (one for switches and one for lights) + assert len(hass.states.async_all()) == 5 + rgb_light = hass.states.get("light.rgb_light") + assert rgb_light is not None -async def test_unload_light(hass): - """Test that it works to unload switch entities.""" - gateway = await setup_gateway(hass, {"lights": LIGHT, "groups": GROUP}) + tunable_white_light = hass.states.get("light.tunable_white_light") + assert tunable_white_light is not None - await gateway.async_reset() + light_group = hass.states.get("light.light_group") + assert light_group is None - # Group.all_lights will not be removed - assert len(hass.states.async_all()) == 1 + empty_group = hass.states.get("light.empty_group") + assert empty_group is None diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 6b691bcab8e038..c574ed8911e063 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -1,86 +1,89 @@ """deCONZ switch platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy + +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.components.deconz.const import SWITCH_TYPES -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component -import homeassistant.components.switch as switch -from tests.common import mock_coro +import homeassistant.components.switch as switch -SUPPORTED_SWITCHES = { +SWITCHES = { "1": { - "id": "Switch 1 id", - "name": "Switch 1 name", + "id": "On off switch id", + "name": "On off switch", "type": "On/Off plug-in unit", "state": {"on": True, "reachable": True}, "uniqueid": "00:00:00:00:00:00:00:00-00", }, "2": { - "id": "Switch 2 id", - "name": "Switch 2 name", + "id": "Smart plug id", + "name": "Smart plug", "type": "Smart plug", - "state": {"on": True, "reachable": True}, + "state": {"on": False, "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:01-00", }, "3": { - "id": "Switch 3 id", - "name": "Switch 3 name", + "id": "Warning device id", + "name": "Warning device", "type": "Warning device", "state": {"alert": "lselect", "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", }, -} - -UNSUPPORTED_SWITCH = { - "1": { - "id": "Switch id", + "4": { + "id": "Unsupported switch id", "name": "Unsupported switch", "type": "Not a smart plug", - "state": {}, - } + "state": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:03-00", + }, } +BRIDGEID = "0123456789" 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_BRIDGEID: BRIDGEID, deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, } +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, +} -async def setup_gateway(hass, data): - """Load the deCONZ switch platform.""" - from pydeconz import DeconzSession +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - loop = Mock() - session = Mock() +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - - await hass.config_entries.async_forward_entry_setup(config_entry, "switch") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -96,68 +99,93 @@ async def test_platform_manually_configured(hass): async def test_no_switches(hass): """Test that no switch entities are created.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_switches(hass): """Test that all supported switch entities are created.""" - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - gateway = await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES}) - assert "switch.switch_1_name" in gateway.deconz_ids - assert "switch.switch_2_name" in gateway.deconz_ids - assert "switch.switch_3_name" in gateway.deconz_ids - assert len(SUPPORTED_SWITCHES) == len(SWITCH_TYPES) - assert len(hass.states.async_all()) == 4 - - switch_1 = hass.states.get("switch.switch_1_name") - assert switch_1 is not None - assert switch_1.state == "on" - switch_3 = hass.states.get("switch.switch_3_name") - assert switch_3 is not None - assert switch_3.state == "on" - - gateway.api.lights["1"].async_update({}) - - await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.switch_1_name"}, blocking=True - ) - await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.switch_1_name"}, blocking=True - ) - - await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.switch_3_name"}, blocking=True - ) - await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.switch_3_name"}, blocking=True + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = deepcopy(SWITCHES) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) + assert "switch.on_off_switch" in gateway.deconz_ids + assert "switch.smart_plug" in gateway.deconz_ids + assert "switch.warning_device" in gateway.deconz_ids + assert "switch.unsupported_switch" not in gateway.deconz_ids + assert len(hass.states.async_all()) == 6 + on_off_switch = hass.states.get("switch.on_off_switch") + assert on_off_switch.state == "on" -async def test_add_new_switch(hass): - """Test successful creation of switch entity.""" - gateway = await setup_gateway(hass, {}) - switch = Mock() - switch.name = "name" - switch.type = "Smart plug" - switch.uniqueid = "1" - switch.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [switch]) - await hass.async_block_till_done() - assert "switch.name" in gateway.deconz_ids + smart_plug = hass.states.get("switch.smart_plug") + assert smart_plug.state == "off" + warning_device = hass.states.get("switch.warning_device") + assert warning_device.state == "on" -async def test_unsupported_switch(hass): - """Test that unsupported switches are not created.""" - await setup_gateway(hass, {"lights": UNSUPPORTED_SWITCH}) - assert len(hass.states.async_all()) == 0 + on_off_switch_device = gateway.api.lights["1"] + warning_device_device = gateway.api.lights["3"] + on_off_switch_device.async_update({"state": {"on": False}}) + warning_device_device.async_update({"state": {"alert": None}}) + await hass.async_block_till_done() -async def test_unload_switch(hass): - """Test that it works to unload switch entities.""" - gateway = await setup_gateway(hass, {"lights": SUPPORTED_SWITCHES}) + on_off_switch = hass.states.get("switch.on_off_switch") + assert on_off_switch.state == "off" - await gateway.async_reset() + warning_device = hass.states.get("switch.warning_device") + assert warning_device.state == "off" - assert len(hass.states.async_all()) == 1 + with patch.object( + on_off_switch_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_ON, + {"entity_id": "switch.on_off_switch"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"on": True}) + + with patch.object( + on_off_switch_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_OFF, + {"entity_id": "switch.on_off_switch"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/1/state", {"on": False}) + + with patch.object( + warning_device_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_ON, + {"entity_id": "switch.warning_device"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/3/state", {"alert": "lselect"}) + + with patch.object( + warning_device_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + switch.DOMAIN, + switch.SERVICE_TURN_OFF, + {"entity_id": "switch.warning_device"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/lights/3/state", {"alert": "none"}) From fe5a4cef7f7d93fb55b6a4cc339e54ba93ea00ec Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 18 Sep 2019 15:37:04 +0200 Subject: [PATCH 0331/3953] Updated frontend to 20190918.0 (#26704) --- 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 3d5860d0a4350c..628099a47d2c15 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==20190917.2" + "home-assistant-frontend==20190918.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index de5c823d999c28..bbeb228f3459e0 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==20190917.2 +home-assistant-frontend==20190918.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 05a3e89b6757bf..415c8e514f58b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.2 +home-assistant-frontend==20190918.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07fc31ec6efd4e..5ff52cf2255c4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.2 +home-assistant-frontend==20190918.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 9cd5c5471df061f1a032944dd4f38b193a86d047 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Wed, 18 Sep 2019 20:00:12 +0400 Subject: [PATCH 0332/3953] Hide "PTZ is not available on this camera" warning (#26649) * Hide "PTZ is not available" warning * Change log level to "debug" --- homeassistant/components/onvif/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 0635a2d1f11bb1..4fdd513f840f28 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -282,7 +282,7 @@ def setup_ptz(self): """Set up PTZ if available.""" _LOGGER.debug("Setting up the ONVIF PTZ service") if self._camera.get_service("ptz", create=False) is None: - _LOGGER.warning("PTZ is not available on this camera") + _LOGGER.debug("PTZ is not available") else: self._ptz_service = self._camera.create_ptz_service() _LOGGER.debug("Completed set up of the ONVIF camera component") From 4a30c1023c1794b2f101f22e010094b18a131921 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 15 Sep 2019 09:50:17 +0200 Subject: [PATCH 0333/3953] Rename MockToggleDevice to MockToggleEntity (#26644) * Rename MockToggleDevice to MockToggleEntity * Fix tests --- tests/common.py | 16 +-- tests/components/flux/test_switch.py | 110 +++++++-------- .../generic_thermostat/test_climate.py | 2 +- .../light/test_device_automation.py | 48 +++---- tests/components/light/test_init.py | 130 +++++++++--------- tests/components/scene/test_init.py | 2 +- .../switch/test_device_automation.py | 48 +++---- tests/components/switch/test_init.py | 2 +- .../custom_components/test/light.py | 20 +-- .../custom_components/test/switch.py | 20 +-- 10 files changed, 200 insertions(+), 198 deletions(-) diff --git a/tests/common.py b/tests/common.py index 847635d4dad67e..fda5c743222703 100644 --- a/tests/common.py +++ b/tests/common.py @@ -602,40 +602,40 @@ def __init__( ) -class MockToggleDevice(entity.ToggleEntity): +class MockToggleEntity(entity.ToggleEntity): """Provide a mock toggle device.""" - def __init__(self, name, state): - """Initialize the mock device.""" + def __init__(self, name, state, unique_id=None): + """Initialize the mock entity.""" self._name = name or DEVICE_DEFAULT_NAME self._state = state self.calls = [] @property def name(self): - """Return the name of the device if any.""" + """Return the name of the entity if any.""" self.calls.append(("name", {})) return self._name @property def state(self): - """Return the name of the device if any.""" + """Return the state of the entity if any.""" self.calls.append(("state", {})) return self._state @property def is_on(self): - """Return true if device is on.""" + """Return true if entity is on.""" self.calls.append(("is_on", {})) return self._state == STATE_ON def turn_on(self, **kwargs): - """Turn the device on.""" + """Turn the entity on.""" self.calls.append(("turn_on", kwargs)) self._state = STATE_ON def turn_off(self, **kwargs): - """Turn the device off.""" + """Turn the entity off.""" self.calls.append(("turn_off", kwargs)) self._state = STATE_OFF diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index 08a49c4a6670d4..fb35485f5c9685 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -82,10 +82,10 @@ async def test_flux_when_switch_is_off(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -113,7 +113,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -131,10 +131,10 @@ async def test_flux_before_sunrise(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -162,7 +162,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -184,10 +184,10 @@ async def test_flux_before_sunrise_known_location(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -210,7 +210,7 @@ async def test_flux_before_sunrise_known_location(hass): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], # 'brightness': 255, # 'disable_brightness_adjust': True, # 'mode': 'rgb', @@ -237,10 +237,10 @@ async def test_flux_after_sunrise_before_sunset(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -267,7 +267,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -290,10 +290,10 @@ async def test_flux_after_sunset_before_stop(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -320,7 +320,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "22:00", } }, @@ -344,10 +344,10 @@ async def test_flux_after_stop_before_sunrise(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -374,7 +374,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], } }, ) @@ -397,10 +397,10 @@ async def test_flux_with_custom_start_stop_times(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -427,7 +427,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "start_time": "6:00", "stop_time": "23:30", } @@ -454,10 +454,10 @@ async def test_flux_before_sunrise_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -484,7 +484,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -512,10 +512,10 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -542,7 +542,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -570,10 +570,10 @@ async def test_flux_after_sunset_before_midnight_stop_next_day(hass, x): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -600,7 +600,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -627,10 +627,10 @@ async def test_flux_after_sunset_after_midnight_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -657,7 +657,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -684,10 +684,10 @@ async def test_flux_after_stop_before_sunrise_stop_next_day(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -714,7 +714,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "stop_time": "01:00", } }, @@ -738,10 +738,10 @@ async def test_flux_with_custom_colortemps(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -768,7 +768,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "start_colortemp": "1000", "stop_colortemp": "6000", "stop_time": "22:00", @@ -794,10 +794,10 @@ async def test_flux_with_custom_brightness(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -824,7 +824,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "brightness": 255, "stop_time": "22:00", } @@ -848,23 +848,23 @@ async def test_flux_with_multiple_lights(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1, dev2, dev3 = platform.DEVICES - common_light.turn_on(hass, entity_id=dev2.entity_id) + ent1, ent2, ent3 = platform.ENTITIES + common_light.turn_on(hass, entity_id=ent2.entity_id) await hass.async_block_till_done() - common_light.turn_on(hass, entity_id=dev3.entity_id) + common_light.turn_on(hass, entity_id=ent3.entity_id) await hass.async_block_till_done() - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None - state = hass.states.get(dev2.entity_id) + state = hass.states.get(ent2.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None - state = hass.states.get(dev3.entity_id) + state = hass.states.get(ent3.entity_id) assert STATE_ON == state.state assert state.attributes.get("xy_color") is None assert state.attributes.get("brightness") is None @@ -893,7 +893,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id, dev2.entity_id, dev3.entity_id], + "lights": [ent1.entity_id, ent2.entity_id, ent3.entity_id], } }, ) @@ -921,10 +921,10 @@ async def test_flux_with_mired(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("color_temp") is None @@ -950,7 +950,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "mode": "mired", } }, @@ -972,10 +972,10 @@ async def test_flux_with_rgb(hass): hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1 = platform.DEVICES[0] + ent1 = platform.ENTITIES[0] # Verify initial state of light - state = hass.states.get(dev1.entity_id) + state = hass.states.get(ent1.entity_id) assert STATE_ON == state.state assert state.attributes.get("color_temp") is None @@ -1001,7 +1001,7 @@ def event_date(hass, event, now=None): switch.DOMAIN: { "platform": "flux", "name": "flux", - "lights": [dev1.entity_id], + "lights": [ent1.entity_id], "mode": "rgb", } }, diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 367ea52b3a2bcd..776d8f39f69613 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -116,7 +116,7 @@ async def test_heater_switch(hass, setup_comp_1): """Test heater switching test switch.""" platform = getattr(hass.components, "test.switch") platform.init() - switch_1 = platform.DEVICES[1] + switch_1 = platform.ENTITIES[1] assert await async_setup_component( hass, switch.DOMAIN, {"switch": {"platform": "test"}} ) diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py index 40fa08856c5684..3525f1121c08e2 100644 --- a/tests/components/light/test_device_automation.py +++ b/tests/components/light/test_device_automation.py @@ -150,7 +150,7 @@ async def test_if_fires_on_state_change(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -162,7 +162,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, "action": { @@ -186,7 +186,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, "action": { @@ -209,21 +209,21 @@ async def test_if_fires_on_state_change(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - dev1.entity_id + ent1.entity_id ) - hass.states.async_set(dev1.entity_id, STATE_ON) + hass.states.async_set(ent1.entity_id, STATE_ON) await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - dev1.entity_id + ent1.entity_id ) @@ -234,7 +234,7 @@ async def test_if_state(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -248,7 +248,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_on", } ], @@ -267,7 +267,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_off", } ], @@ -283,7 +283,7 @@ async def test_if_state(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") @@ -292,7 +292,7 @@ async def test_if_state(hass, calls): assert len(calls) == 1 assert calls[0].data["some"] == "is_on event - test_event1" - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() @@ -307,7 +307,7 @@ async def test_action(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -319,7 +319,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, }, @@ -328,7 +328,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, }, @@ -337,7 +337,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "toggle", }, }, @@ -345,29 +345,29 @@ async def test_action(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index dc4cb7502c5765..8ceda6cbd3efa7 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -137,39 +137,39 @@ def test_services(self): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES # Test init - assert light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # Test basic turn_on, turn_off, toggle services - common.turn_off(self.hass, entity_id=dev1.entity_id) - common.turn_on(self.hass, entity_id=dev2.entity_id) + common.turn_off(self.hass, entity_id=ent1.entity_id) + common.turn_on(self.hass, entity_id=ent2.entity_id) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert light.is_on(self.hass, dev2.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert light.is_on(self.hass, ent2.entity_id) # turn on all lights common.turn_on(self.hass) self.hass.block_till_done() - assert light.is_on(self.hass, dev1.entity_id) - assert light.is_on(self.hass, dev2.entity_id) - assert light.is_on(self.hass, dev3.entity_id) + assert light.is_on(self.hass, ent1.entity_id) + assert light.is_on(self.hass, ent2.entity_id) + assert light.is_on(self.hass, ent3.entity_id) # turn off all lights common.turn_off(self.hass) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # turn off all lights by setting brightness to 0 common.turn_on(self.hass) @@ -180,97 +180,97 @@ def test_services(self): self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # toggle all lights common.toggle(self.hass) self.hass.block_till_done() - assert light.is_on(self.hass, dev1.entity_id) - assert light.is_on(self.hass, dev2.entity_id) - assert light.is_on(self.hass, dev3.entity_id) + assert light.is_on(self.hass, ent1.entity_id) + assert light.is_on(self.hass, ent2.entity_id) + assert light.is_on(self.hass, ent3.entity_id) # toggle all lights common.toggle(self.hass) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) # Ensure all attributes process correctly common.turn_on( - self.hass, dev1.entity_id, transition=10, brightness=20, color_name="blue" + self.hass, ent1.entity_id, transition=10, brightness=20, color_name="blue" ) common.turn_on( - self.hass, dev2.entity_id, rgb_color=(255, 255, 255), white_value=255 + self.hass, ent2.entity_id, rgb_color=(255, 255, 255), white_value=255 ) - common.turn_on(self.hass, dev3.entity_id, xy_color=(0.4, 0.6)) + common.turn_on(self.hass, ent3.entity_id, xy_color=(0.4, 0.6)) self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert { light.ATTR_TRANSITION: 10, light.ATTR_BRIGHTNESS: 20, light.ATTR_HS_COLOR: (240, 100), } == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} == data - _, data = dev3.last_call("turn_on") + _, data = ent3.last_call("turn_on") assert {light.ATTR_HS_COLOR: (71.059, 100)} == data # Ensure attributes are filtered when light is turned off common.turn_on( - self.hass, dev1.entity_id, transition=10, brightness=0, color_name="blue" + self.hass, ent1.entity_id, transition=10, brightness=0, color_name="blue" ) common.turn_on( self.hass, - dev2.entity_id, + ent2.entity_id, brightness=0, rgb_color=(255, 255, 255), white_value=0, ) - common.turn_on(self.hass, dev3.entity_id, brightness=0, xy_color=(0.4, 0.6)) + common.turn_on(self.hass, ent3.entity_id, brightness=0, xy_color=(0.4, 0.6)) self.hass.block_till_done() - assert not light.is_on(self.hass, dev1.entity_id) - assert not light.is_on(self.hass, dev2.entity_id) - assert not light.is_on(self.hass, dev3.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) + assert not light.is_on(self.hass, ent2.entity_id) + assert not light.is_on(self.hass, ent3.entity_id) - _, data = dev1.last_call("turn_off") + _, data = ent1.last_call("turn_off") assert {light.ATTR_TRANSITION: 10} == data - _, data = dev2.last_call("turn_off") + _, data = ent2.last_call("turn_off") assert {} == data - _, data = dev3.last_call("turn_off") + _, data = ent3.last_call("turn_off") assert {} == data # One of the light profiles prof_name, prof_h, prof_s, prof_bri = "relax", 35.932, 69.412, 144 # Test light profiles - common.turn_on(self.hass, dev1.entity_id, profile=prof_name) + common.turn_on(self.hass, ent1.entity_id, profile=prof_name) # Specify a profile and a brightness attribute to overwrite it - common.turn_on(self.hass, dev2.entity_id, profile=prof_name, brightness=100) + common.turn_on(self.hass, ent2.entity_id, profile=prof_name, brightness=100) self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert { light.ATTR_BRIGHTNESS: prof_bri, light.ATTR_HS_COLOR: (prof_h, prof_s), } == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert { light.ATTR_BRIGHTNESS: 100, light.ATTR_HS_COLOR: (prof_h, prof_s), @@ -278,34 +278,34 @@ def test_services(self): # Test bad data common.turn_on(self.hass) - common.turn_on(self.hass, dev1.entity_id, profile="nonexisting") - common.turn_on(self.hass, dev2.entity_id, xy_color=["bla-di-bla", 5]) - common.turn_on(self.hass, dev3.entity_id, rgb_color=[255, None, 2]) + common.turn_on(self.hass, ent1.entity_id, profile="nonexisting") + common.turn_on(self.hass, ent2.entity_id, xy_color=["bla-di-bla", 5]) + common.turn_on(self.hass, ent3.entity_id, rgb_color=[255, None, 2]) self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert {} == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert {} == data - _, data = dev3.last_call("turn_on") + _, data = ent3.last_call("turn_on") assert {} == data # faulty attributes will not trigger a service call common.turn_on( - self.hass, dev1.entity_id, profile=prof_name, brightness="bright" + self.hass, ent1.entity_id, profile=prof_name, brightness="bright" ) - common.turn_on(self.hass, dev1.entity_id, rgb_color="yellowish") - common.turn_on(self.hass, dev2.entity_id, white_value="high") + common.turn_on(self.hass, ent1.entity_id, rgb_color="yellowish") + common.turn_on(self.hass, ent2.entity_id, white_value="high") self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") assert {} == data - _, data = dev2.last_call("turn_on") + _, data = ent2.last_call("turn_on") assert {} == data def test_broken_light_profiles(self): @@ -340,24 +340,24 @@ def test_light_profiles(self): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev1, _, _ = platform.DEVICES + ent1, _, _ = platform.ENTITIES - common.turn_on(self.hass, dev1.entity_id, profile="test") + common.turn_on(self.hass, ent1.entity_id, profile="test") self.hass.block_till_done() - _, data = dev1.last_call("turn_on") + _, data = ent1.last_call("turn_on") - assert light.is_on(self.hass, dev1.entity_id) + assert light.is_on(self.hass, ent1.entity_id) assert {light.ATTR_HS_COLOR: (71.059, 100), light.ATTR_BRIGHTNESS: 100} == data - common.turn_on(self.hass, dev1.entity_id, profile="test_off") + common.turn_on(self.hass, ent1.entity_id, profile="test_off") self.hass.block_till_done() - _, data = dev1.last_call("turn_off") + _, data = ent1.last_call("turn_off") - assert not light.is_on(self.hass, dev1.entity_id) + assert not light.is_on(self.hass, ent1.entity_id) assert {} == data def test_default_profiles_group(self): @@ -387,10 +387,10 @@ def _mock_open(path, *args, **kwargs): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev, _, _ = platform.DEVICES - common.turn_on(self.hass, dev.entity_id) + ent, _, _ = platform.ENTITIES + common.turn_on(self.hass, ent.entity_id) self.hass.block_till_done() - _, data = dev.last_call("turn_on") + _, data = ent.last_call("turn_on") assert {light.ATTR_HS_COLOR: (71.059, 100), light.ATTR_BRIGHTNESS: 99} == data def test_default_profiles_light(self): @@ -424,7 +424,9 @@ def _mock_open(path, *args, **kwargs): self.hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} ) - dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.DEVICES)) + dev = next( + filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES) + ) common.turn_on(self.hass, dev.entity_id) self.hass.block_till_done() _, data = dev.last_call("turn_on") diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 7047e6e8d92f3c..5c8d46cb727753 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -24,7 +24,7 @@ def setUp(self): # pylint: disable=invalid-name self.hass, light.DOMAIN, {light.DOMAIN: {"platform": "test"}} ) - self.light_1, self.light_2 = test_light.DEVICES[0:2] + self.light_1, self.light_2 = test_light.ENTITIES[0:2] common_light.turn_off( self.hass, [self.light_1.entity_id, self.light_2.entity_id] diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py index 7dba73476514f0..3bd29a72a12549 100644 --- a/tests/components/switch/test_device_automation.py +++ b/tests/components/switch/test_device_automation.py @@ -150,7 +150,7 @@ async def test_if_fires_on_state_change(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -162,7 +162,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, "action": { @@ -186,7 +186,7 @@ async def test_if_fires_on_state_change(hass, calls): "platform": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, "action": { @@ -209,21 +209,21 @@ async def test_if_fires_on_state_change(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - dev1.entity_id + ent1.entity_id ) - hass.states.async_set(dev1.entity_id, STATE_ON) + hass.states.async_set(ent1.entity_id, STATE_ON) await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - dev1.entity_id + ent1.entity_id ) @@ -234,7 +234,7 @@ async def test_if_state(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -248,7 +248,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_on", } ], @@ -267,7 +267,7 @@ async def test_if_state(hass, calls): "condition": "device", "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "is_off", } ], @@ -283,7 +283,7 @@ async def test_if_state(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") @@ -292,7 +292,7 @@ async def test_if_state(hass, calls): assert len(calls) == 1 assert calls[0].data["some"] == "is_on event - test_event1" - hass.states.async_set(dev1.entity_id, STATE_OFF) + hass.states.async_set(ent1.entity_id, STATE_OFF) hass.bus.async_fire("test_event1") hass.bus.async_fire("test_event2") await hass.async_block_till_done() @@ -307,7 +307,7 @@ async def test_action(hass, calls): platform.init() assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - dev1, dev2, dev3 = platform.DEVICES + ent1, ent2, ent3 = platform.ENTITIES assert await async_setup_component( hass, @@ -319,7 +319,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_off", }, }, @@ -328,7 +328,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "turn_on", }, }, @@ -337,7 +337,7 @@ async def test_action(hass, calls): "action": { "domain": DOMAIN, "device_id": "", - "entity_id": dev1.entity_id, + "entity_id": ent1.entity_id, "type": "toggle", }, }, @@ -345,29 +345,29 @@ async def test_action(hass, calls): }, ) await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON assert len(calls) == 0 hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event1") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event2") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_OFF + assert hass.states.get(ent1.entity_id).state == STATE_OFF hass.bus.async_fire("test_event3") await hass.async_block_till_done() - assert hass.states.get(dev1.entity_id).state == STATE_ON + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index c04a30589edd8f..a9463cb78f4fea 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -21,7 +21,7 @@ def setUp(self): platform = getattr(self.hass.components, "test.switch") platform.init() # Switch 1 is ON, switch 2 is OFF - self.switch_1, self.switch_2, self.switch_3 = platform.DEVICES + self.switch_1, self.switch_2, self.switch_3 = platform.ENTITIES # pylint: disable=invalid-name def tearDown(self): diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 43338c9e14ed1f..0a48388b718b1e 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -4,23 +4,23 @@ Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from tests.common import MockToggleDevice +from tests.common import MockToggleEntity -DEVICES = [] +ENTITIES = [] def init(empty=False): - """Initialize the platform with devices.""" - global DEVICES + """Initialize the platform with entities.""" + global ENTITIES - DEVICES = ( + ENTITIES = ( [] if empty else [ - MockToggleDevice("Ceiling", STATE_ON), - MockToggleDevice("Ceiling", STATE_OFF), - MockToggleDevice(None, STATE_OFF), + MockToggleEntity("Ceiling", STATE_ON), + MockToggleEntity("Ceiling", STATE_OFF), + MockToggleEntity(None, STATE_OFF), ] ) @@ -28,5 +28,5 @@ def init(empty=False): async def async_setup_platform( hass, config, async_add_entities_callback, discovery_info=None ): - """Return mock devices.""" - async_add_entities_callback(DEVICES) + """Return mock entities.""" + async_add_entities_callback(ENTITIES) diff --git a/tests/testing_config/custom_components/test/switch.py b/tests/testing_config/custom_components/test/switch.py index f4226ecc63014f..484c47d1190e3c 100644 --- a/tests/testing_config/custom_components/test/switch.py +++ b/tests/testing_config/custom_components/test/switch.py @@ -4,23 +4,23 @@ Call init before using it in your tests to ensure clean test data. """ from homeassistant.const import STATE_ON, STATE_OFF -from tests.common import MockToggleDevice +from tests.common import MockToggleEntity -DEVICES = [] +ENTITIES = [] def init(empty=False): - """Initialize the platform with devices.""" - global DEVICES + """Initialize the platform with entities.""" + global ENTITIES - DEVICES = ( + ENTITIES = ( [] if empty else [ - MockToggleDevice("AC", STATE_ON), - MockToggleDevice("AC", STATE_OFF), - MockToggleDevice(None, STATE_OFF), + MockToggleEntity("AC", STATE_ON), + MockToggleEntity("AC", STATE_OFF), + MockToggleEntity(None, STATE_OFF), ] ) @@ -28,5 +28,5 @@ def init(empty=False): async def async_setup_platform( hass, config, async_add_entities_callback, discovery_info=None ): - """Find and return test switches.""" - async_add_entities_callback(DEVICES) + """Return mock entities.""" + async_add_entities_callback(ENTITIES) From bc7ff8323cc585c2a8871f86b73e760182372f21 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Sep 2019 21:55:01 +0200 Subject: [PATCH 0334/3953] Fix translation, adjust trigger names (#26635) --- homeassistant/components/device_automation/const.py | 2 ++ .../components/device_automation/toggle_entity.py | 10 ++++++---- homeassistant/components/light/strings.json | 4 ++-- homeassistant/components/switch/strings.json | 8 ++++---- tests/components/device_automation/test_init.py | 4 ++-- tests/components/light/test_device_automation.py | 8 ++++---- tests/components/switch/test_device_automation.py | 8 ++++---- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/device_automation/const.py b/homeassistant/components/device_automation/const.py index a668c78598a289..40bfc4ca0a13e4 100644 --- a/homeassistant/components/device_automation/const.py +++ b/homeassistant/components/device_automation/const.py @@ -4,3 +4,5 @@ CONF_TOGGLE = "toggle" CONF_TURN_OFF = "turn_off" CONF_TURN_ON = "turn_on" +CONF_TURNED_OFF = "turned_off" +CONF_TURNED_ON = "turned_on" diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 722b92e33f2603..1593e70771aea3 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -8,6 +8,8 @@ CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON, + CONF_TURNED_OFF, + CONF_TURNED_ON, ) from homeassistant.core import split_entity_id from homeassistant.const import ( @@ -53,12 +55,12 @@ { # Trigger when entity is turned off CONF_PLATFORM: "device", - CONF_TYPE: CONF_TURN_OFF, + CONF_TYPE: CONF_TURNED_OFF, }, { # Trigger when entity is turned on CONF_PLATFORM: "device", - CONF_TYPE: CONF_TURN_ON, + CONF_TYPE: CONF_TURNED_ON, }, ] @@ -87,7 +89,7 @@ vol.Required(CONF_DEVICE_ID): str, vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In([CONF_TURN_OFF, CONF_TURN_ON]), + vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), } ) @@ -136,7 +138,7 @@ def async_condition_from_config(config, config_validation): async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] - if trigger_type == CONF_TURN_ON: + if trigger_type == CONF_TURNED_ON: from_state = "off" to_state = "on" else: diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json index 460114b143cc78..77b842ba07833a 100644 --- a/homeassistant/components/light/strings.json +++ b/homeassistant/components/light/strings.json @@ -10,8 +10,8 @@ "is_off": "{entity_name} is off" }, "trigger_type": { - "turn_on": "{entity_name} turned on", - "turn_off": "{entity_name} turned off" + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" } } } diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index 857b3763076143..77b842ba07833a 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -6,12 +6,12 @@ "turn_off": "Turn off {entity_name}" }, "condition_type": { - "turn_on": "{entity_name} turned on", - "turn_off": "{entity_name} turned off" + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" }, "trigger_type": { - "turn_on": "{entity_name} turned on", - "turn_off": "{entity_name} turned off" + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" } } } diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index a01dad03d46276..b05c04a16f1dc5 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -133,14 +133,14 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r { "platform": "device", "domain": "light", - "type": "turn_off", + "type": "turned_off", "device_id": device_entry.id, "entity_id": "light.test_5678", }, { "platform": "device", "domain": "light", - "type": "turn_on", + "type": "turned_on", "device_id": device_entry.id, "entity_id": "light.test_5678", }, diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py index 3525f1121c08e2..27b8b860d7227c 100644 --- a/tests/components/light/test_device_automation.py +++ b/tests/components/light/test_device_automation.py @@ -125,14 +125,14 @@ async def test_get_triggers(hass, device_reg, entity_reg): { "platform": "device", "domain": DOMAIN, - "type": "turn_off", + "type": "turned_off", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { "platform": "device", "domain": DOMAIN, - "type": "turn_on", + "type": "turned_on", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, @@ -163,7 +163,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_on", + "type": "turned_on", }, "action": { "service": "test.automation", @@ -187,7 +187,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_off", + "type": "turned_off", }, "action": { "service": "test.automation", diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py index 3bd29a72a12549..1ebe4785761aa5 100644 --- a/tests/components/switch/test_device_automation.py +++ b/tests/components/switch/test_device_automation.py @@ -125,14 +125,14 @@ async def test_get_triggers(hass, device_reg, entity_reg): { "platform": "device", "domain": DOMAIN, - "type": "turn_off", + "type": "turned_off", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { "platform": "device", "domain": DOMAIN, - "type": "turn_on", + "type": "turned_on", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, @@ -163,7 +163,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_on", + "type": "turned_on", }, "action": { "service": "test.automation", @@ -187,7 +187,7 @@ async def test_if_fires_on_state_change(hass, calls): "domain": DOMAIN, "device_id": "", "entity_id": ent1.entity_id, - "type": "turn_off", + "type": "turned_off", }, "action": { "service": "test.automation", From 5b0cbad953090e3a787d9308862d41b33426ad14 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 17 Sep 2019 22:39:46 +0200 Subject: [PATCH 0335/3953] Fix cert expiry config flow check and update (#26638) * Fix typo in translations * Work on bug #26619 * readd the homeassistant.start event * Remove the callback * Added the executor_job for _test_connection * Update test_config_flow.py --- .../components/cert_expiry/.translations/en.json | 2 +- homeassistant/components/cert_expiry/__init__.py | 14 +++----------- .../components/cert_expiry/config_flow.py | 8 +++++--- homeassistant/components/cert_expiry/sensor.py | 16 +++++++++++++++- tests/components/cert_expiry/test_config_flow.py | 4 ++-- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index b6aa1cefb02aed..85575df62914a1 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -21,4 +21,4 @@ }, "title": "Certificate Expiry" } -} \ No newline at end of file +} diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index ab68d5ba08bc43..7c7efea7333120 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -1,7 +1,5 @@ """The cert_expiry component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType @@ -13,13 +11,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Load the saved entities.""" - @callback - def async_start(_): - """Load the entry after the start event.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start) - + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) return True diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index dd3463fff95c4e..d73762ce882647 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -38,10 +38,12 @@ def _prt_in_configuration_exists(self, user_input) -> bool: return True return False - def _test_connection(self, user_input=None): + async def _test_connection(self, user_input=None): """Test connection to the server and try to get the certtificate.""" try: - get_cert(user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT)) + await self.hass.async_add_executor_job( + get_cert, user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT) + ) return True except socket.gaierror: self._errors[CONF_HOST] = "resolve_failed" @@ -59,7 +61,7 @@ async def async_step_user(self, user_input=None): if self._prt_in_configuration_exists(user_input): self._errors[CONF_HOST] = "host_port_exists" else: - if self._test_connection(user_input): + if await self._test_connection(user_input): host = user_input[CONF_HOST] name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) prt = user_input.get(CONF_PORT, DEFAULT_PORT) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index fccfb295c0fff2..b564cff7338584 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -9,7 +9,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT +from homeassistant.const import ( + CONF_NAME, + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_START, +) from homeassistant.helpers.entity import Entity from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT @@ -82,6 +87,15 @@ def available(self): """Icon to use in the frontend, if any.""" return self._available + async def async_added_to_hass(self): + """Once the entity is added we should update to get the initial data loaded.""" + + def do_update(_): + """Run the update method when the start event was fired.""" + self.update() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) + def update(self): """Fetch the certificate information.""" try: diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index f8c99496a563eb..f44e65512e35e5 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.cert_expiry.const import DEFAULT_PORT from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_coro NAME = "Cert Expiry test 1 2 3" PORT = 443 @@ -20,7 +20,7 @@ def mock_controller(): """Mock a successfull _prt_in_configuration_exists.""" with patch( "homeassistant.components.cert_expiry.config_flow.CertexpiryConfigFlow._test_connection", - return_value=True, + side_effect=lambda *_: mock_coro(True), ): yield From 7d525ff2f3fe7193933e4e747b641680c1a41a32 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 17 Sep 2019 15:59:12 +0200 Subject: [PATCH 0336/3953] Fix release access for bram (#26693) --- 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 7c88e615fa5baa..29e68a5d7acc16 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -43,7 +43,7 @@ stages: release="$(Build.SourceBranchName)" created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')" - if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480)$ ]]; then + if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten)$ ]]; then exit 0 fi From 46a55ed723a18751dcdf8e49341a426b96cab1aa Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 17 Sep 2019 22:46:11 +0200 Subject: [PATCH 0337/3953] Updated frontend to 20190917.2 (#26696) --- 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 01823882f9d371..3d5860d0a4350c 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==20190917.1" + "home-assistant-frontend==20190917.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 43a22cb980c079..de5c823d999c28 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==20190917.1 +home-assistant-frontend==20190917.2 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index e260d7237a5473..b92f38dd8baf46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.1 +home-assistant-frontend==20190917.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b00194b0d91be8..07fc31ec6efd4e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.1 +home-assistant-frontend==20190917.2 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From ef9b3321c1b5eb3b6ba4fc29c58789f4fcfffc3c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Sep 2019 13:45:48 -0700 Subject: [PATCH 0338/3953] Verify withings config (#26698) --- .../components/withings/config_flow.py | 5 ++- .../components/withings/strings.json | 5 ++- tests/components/withings/test_config_flow.py | 35 ++++++------------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index 23cc74281e8b9b..f28a4f59d80195 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -88,7 +88,10 @@ async def async_step_import(self, user_input=None): async def async_step_user(self, user_input=None): """Create an entry for selecting a profile.""" - flow = self.hass.data.get(DATA_FLOW_IMPL, {}) + flow = self.hass.data.get(DATA_FLOW_IMPL) + + if not flow: + return self.async_abort(reason="no_flows") if user_input: return await self.async_step_auth(user_input) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 88b8e6d5ea0944..1a99abc7255651 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -12,6 +12,9 @@ }, "create_entry": { "default": "Successfully authenticated with Withings for the selected profile." + }, + "abort": { + "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation." } } -} \ No newline at end of file +} diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 93b9a434b7f1ac..3ae9d11c3b6555 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -3,9 +3,7 @@ from asynctest import CoroutineMock, MagicMock import pytest -from homeassistant import setup, data_entry_flow -import homeassistant.components.api as api -import homeassistant.components.http as http +from homeassistant import data_entry_flow from homeassistant.components.withings import const from homeassistant.components.withings.config_flow import ( register_flow_implementation, @@ -24,27 +22,6 @@ def flow_handler_fixture(hass: HomeAssistantType): return flow_handler -@pytest.fixture(name="setup_hass") -async def setup_hass_fixture(hass: HomeAssistantType): - """Provide hass instance.""" - config = { - http.DOMAIN: {}, - api.DOMAIN: {"base_url": "http://localhost/"}, - const.DOMAIN: { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_secret", - const.PROFILES: ["Person 1", "Person 2"], - }, - } - - hass.data = {} - - await setup.async_setup_component(hass, "http", config) - await setup.async_setup_component(hass, "api", config) - - return hass - - def test_flow_handler_init(flow_handler: WithingsFlowHandler): """Test the init of the flow handler.""" assert not flow_handler.flow_profile @@ -173,3 +150,13 @@ async def test_auth_callback_view_get(hass: HomeAssistantType): "my_flow_id", {const.PROFILE: "my_profile", const.CODE: "my_code"} ) hass.config_entries.flow.async_configure.reset_mock() + + +async def test_init_without_config(hass): + """Try initializin a configg flow without it being configured.""" + result = await hass.config_entries.flow.async_init( + "withings", context={"source": "user"} + ) + + assert result["type"] == "abort" + assert result["reason"] == "no_flows" From e7d5e08780b31da13b517693427de9898349543f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 18 Sep 2019 15:37:04 +0200 Subject: [PATCH 0339/3953] Updated frontend to 20190918.0 (#26704) --- 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 3d5860d0a4350c..628099a47d2c15 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==20190917.2" + "home-assistant-frontend==20190918.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index de5c823d999c28..bbeb228f3459e0 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==20190917.2 +home-assistant-frontend==20190918.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index b92f38dd8baf46..55f59104ba547a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.2 +home-assistant-frontend==20190918.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07fc31ec6efd4e..5ff52cf2255c4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190917.2 +home-assistant-frontend==20190918.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 1a9b4b82f5b8c7fe033d124073fff5defa0eb646 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 18 Sep 2019 18:44:57 +0200 Subject: [PATCH 0340/3953] Bump version to 0.99.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c18f0ea0fd0a06..6eab8b4cd4078e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 99 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0b4" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From ce42b46ccd68897080a539623889b8cac12de324 Mon Sep 17 00:00:00 2001 From: zewelor Date: Wed, 18 Sep 2019 19:07:07 +0200 Subject: [PATCH 0341/3953] Fix yeelight inheritance order (#26706) --- homeassistant/components/yeelight/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 171f25128c6640..b47cdb981612e4 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -822,7 +822,7 @@ def _predefined_effects(self): class YeelightWhiteTempWithoutNightlightSwitch( - YeelightGenericLight, YeelightWhiteTempLightsupport + YeelightWhiteTempLightsupport, YeelightGenericLight ): """White temp light, when nightlight switch is not set to light.""" @@ -831,7 +831,7 @@ def _brightness_property(self): return "current_brightness" -class YeelightWithNightLight(YeelightGenericLight, YeelightWhiteTempLightsupport): +class YeelightWithNightLight(YeelightWhiteTempLightsupport, YeelightGenericLight): """Representation of a Yeelight with nightlight support. It represents case when nightlight switch is set to light. From 886d8bd6e27122a074739bcb8b155fa22ce53633 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 18 Sep 2019 19:07:32 +0200 Subject: [PATCH 0342/3953] deCONZ rewrite sensor tests (#26679) * Improve binary sensor tests * Fix sensor tests * Improve readability of binary sensor * Fix climate tests Fix sensor platform not loading climate devices as sensors * Add test to verify adding new sensor after start up --- homeassistant/components/deconz/sensor.py | 4 +- tests/components/deconz/test_binary_sensor.py | 217 +++++++----- tests/components/deconz/test_climate.py | 319 +++++++++++------- tests/components/deconz/test_sensor.py | 283 ++++++++++------ 4 files changed, 520 insertions(+), 303 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 001721d4f00035..cc3f3de3170c66 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,5 +1,5 @@ """Support for deCONZ sensors.""" -from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch +from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch, Thermostat from homeassistant.const import ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY from homeassistant.core import callback @@ -48,7 +48,7 @@ def async_add_sensor(sensors): hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) - elif not sensor.BINARY: + elif not sensor.BINARY and sensor.type not in Thermostat.ZHATYPE: new_sensor = DeconzSensor(sensor, gateway) entity_handler.add_entity(new_sensor) diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index b6745e1a971312..c5c35f108296b0 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -1,79 +1,98 @@ """deCONZ binary sensor platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy -from tests.common import mock_coro +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.components.binary_sensor as binary_sensor -SENSOR = { +SENSORS = { "1": { - "id": "Sensor 1 id", - "name": "Sensor 1 name", + "id": "Presence sensor id", + "name": "Presence sensor", "type": "ZHAPresence", - "state": {"presence": False}, - "config": {}, + "state": {"dark": False, "presence": False}, + "config": {"on": True, "reachable": True, "temperature": 10}, "uniqueid": "00:00:00:00:00:00:00:00-00", }, "2": { - "id": "Sensor 2 id", - "name": "Sensor 2 name", + "id": "Temperature sensor id", + "name": "Temperature sensor", "type": "ZHATemperature", "state": {"temperature": False}, "config": {}, + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "3": { + "id": "CLIP presence sensor id", + "name": "CLIP presence sensor", + "type": "CLIPPresence", + "state": {}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, + "4": { + "id": "Vibration sensor id", + "name": "Vibration sensor", + "type": "ZHAVibration", + "state": { + "orientation": [1, 2, 3], + "tiltangle": 36, + "vibration": True, + "vibrationstrength": 10, + }, + "config": {"on": True, "reachable": True, "temperature": 10}, + "uniqueid": "00:00:00:00:00:00:00:03-00", }, } +BRIDGEID = "0123456789" ENTRY_CONFIG = { deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, 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, +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, } +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} -async def setup_gateway(hass, data, allow_clip_sensor=True): - """Load the deCONZ binary sensor platform.""" - from pydeconz import DeconzSession - - loop = Mock() - session = Mock() - - ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, - options=ENTRY_OPTIONS, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -89,58 +108,94 @@ async def test_platform_manually_configured(hass): async def test_no_binary_sensors(hass): """Test that no sensors in deconz results in no sensor entities.""" - data = {} - gateway = await setup_gateway(hass, data) - assert len(hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids) == 0 + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_binary_sensors(hass): """Test successful creation of binary sensor entities.""" - data = {"sensors": SENSOR} - gateway = await setup_gateway(hass, data) - assert "binary_sensor.sensor_1_name" in gateway.deconz_ids - assert "binary_sensor.sensor_2_name" not in gateway.deconz_ids - assert len(hass.states.async_all()) == 1 - - hass.data[deconz.DOMAIN][gateway.bridgeid].api.sensors["1"].async_update( - {"state": {"on": False}} + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) + assert "binary_sensor.presence_sensor" in gateway.deconz_ids + assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids + assert "binary_sensor.clip_presence_sensor" not in gateway.deconz_ids + assert "binary_sensor.vibration_sensor" in gateway.deconz_ids + assert len(hass.states.async_all()) == 3 + presence_sensor = hass.states.get("binary_sensor.presence_sensor") + assert presence_sensor.state == "off" -async def test_add_new_sensor(hass): - """Test successful creation of sensor entities.""" - data = {} - gateway = await setup_gateway(hass, data) - sensor = Mock() - sensor.name = "name" - sensor.type = "ZHAPresence" - sensor.BINARY = True - sensor.uniqueid = "1" - sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) - await hass.async_block_till_done() - assert "binary_sensor.name" in gateway.deconz_ids - - -async def test_do_not_allow_clip_sensor(hass): - """Test that clip sensors can be ignored.""" - data = {} - gateway = await setup_gateway(hass, data, allow_clip_sensor=False) - sensor = Mock() - sensor.name = "name" - sensor.type = "CLIPPresence" - sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) + temperature_sensor = hass.states.get("binary_sensor.temperature_sensor") + assert temperature_sensor is None + + clip_presence_sensor = hass.states.get("binary_sensor.clip_presence_sensor") + assert clip_presence_sensor is None + + vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") + assert vibration_sensor.state == "on" + + gateway.api.sensors["1"].async_update({"state": {"presence": True}}) await hass.async_block_till_done() - assert len(gateway.deconz_ids) == 0 + presence_sensor = hass.states.get("binary_sensor.presence_sensor") + assert presence_sensor.state == "on" -async def test_unload_switch(hass): - """Test that it works to unload switch entities.""" - data = {"sensors": SENSOR} - gateway = await setup_gateway(hass, data) - await gateway.async_reset() +async def test_allow_clip_sensor(hass): + """Test that CLIP sensors can be allowed.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, + ENTRY_CONFIG, + options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}, + get_state_response=data, + ) + assert "binary_sensor.presence_sensor" in gateway.deconz_ids + assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids + assert "binary_sensor.clip_presence_sensor" in gateway.deconz_ids + assert "binary_sensor.vibration_sensor" in gateway.deconz_ids + assert len(hass.states.async_all()) == 4 - assert len(hass.states.async_all()) == 0 + presence_sensor = hass.states.get("binary_sensor.presence_sensor") + assert presence_sensor.state == "off" + + temperature_sensor = hass.states.get("binary_sensor.temperature_sensor") + assert temperature_sensor is None + + clip_presence_sensor = hass.states.get("binary_sensor.clip_presence_sensor") + assert clip_presence_sensor.state == "off" + + vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") + assert vibration_sensor.state == "on" + + +async def test_add_new_binary_sensor(hass): + """Test that adding a new binary sensor works.""" + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 + + state_added = { + "t": "event", + "e": "added", + "r": "sensors", + "id": "1", + "sensor": deepcopy(SENSORS["1"]), + } + gateway.api.async_event_handler(state_added) + await hass.async_block_till_done() + + assert "binary_sensor.presence_sensor" in gateway.deconz_ids + + presence_sensor = hass.states.get("binary_sensor.presence_sensor") + assert presence_sensor.state == "off" diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index b76b3511a090bc..1211188d3db3fa 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,23 +1,18 @@ """deCONZ climate platform tests.""" from copy import deepcopy -from unittest.mock import Mock, patch -import asynctest +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.components.climate as climate -from tests.common import mock_coro - - -SENSOR = { +SENSORS = { "1": { - "id": "Climate 1 id", - "name": "Climate 1 name", + "id": "Thermostat id", + "name": "Thermostat", "type": "ZHAThermostat", "state": {"on": True, "temperature": 2260, "valve": 30}, "config": { @@ -30,62 +25,66 @@ "uniqueid": "00:00:00:00:00:00:00:00-00", }, "2": { - "id": "Sensor 2 id", - "name": "Sensor 2 name", + "id": "Presence sensor id", + "name": "Presence sensor", "type": "ZHAPresence", "state": {"presence": False}, - "config": {}, + "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "3": { + "id": "CLIP thermostat id", + "name": "CLIP thermostat", + "type": "CLIPThermostat", + "state": {"on": True, "temperature": 2260, "valve": 30}, + "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:02-00", }, } +BRIDGEID = "0123456789" + ENTRY_CONFIG = { deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, 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, +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, } +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} -async def setup_gateway(hass, data, allow_clip_sensor=True): - """Load the deCONZ sensor platform.""" - from pydeconz import DeconzSession - - response = Mock( - status=200, json=asynctest.CoroutineMock(), text=asynctest.CoroutineMock() - ) - response.content_type = "application/json" - - session = Mock(put=asynctest.CoroutineMock(return_value=response)) - - ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, - options=ENTRY_OPTIONS, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(hass.loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - - await hass.config_entries.async_forward_entry_setup(config_entry, "climate") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -101,69 +100,155 @@ async def test_platform_manually_configured(hass): async def test_no_sensors(hass): """Test that no sensors in deconz results in no climate entities.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids - assert not hass.states.async_all() + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 + assert len(hass.states.async_all()) == 0 async def test_climate_devices(hass): """Test successful creation of sensor entities.""" - gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)}) - assert "climate.climate_1_name" in gateway.deconz_ids - assert "sensor.sensor_2_name" not in gateway.deconz_ids - assert len(hass.states.async_all()) == 1 - - gateway.api.sensors["1"].async_update({"state": {"on": False}}) - - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.climate_1_name", "hvac_mode": "auto"}, - blocking=True, - ) - gateway.api.session.put.assert_called_with( - "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "auto"}' + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) + assert "climate.thermostat" in gateway.deconz_ids + assert "sensor.thermostat" not in gateway.deconz_ids + assert "sensor.thermostat_battery_level" in gateway.deconz_ids + assert "climate.presence_sensor" not in gateway.deconz_ids + assert "climate.clip_thermostat" not in gateway.deconz_ids + assert len(hass.states.async_all()) == 3 + + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "auto" - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.climate_1_name", "hvac_mode": "heat"}, - blocking=True, - ) - gateway.api.session.put.assert_called_with( - "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "heat"}' - ) + thermostat = hass.states.get("sensor.thermostat") + assert thermostat is None - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.climate_1_name", "hvac_mode": "off"}, - blocking=True, - ) - gateway.api.session.put.assert_called_with( - "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"mode": "off"}' - ) + thermostat_battery_level = hass.states.get("sensor.thermostat_battery_level") + assert thermostat_battery_level.state == "100" - await hass.services.async_call( - "climate", - "set_temperature", - {"entity_id": "climate.climate_1_name", "temperature": 20}, - blocking=True, - ) - gateway.api.session.put.assert_called_with( - "http://1.2.3.4:80/api/ABCDEF/sensors/1/config", data='{"heatsetpoint": 2000.0}' + presence_sensor = hass.states.get("climate.presence_sensor") + assert presence_sensor is None + + clip_thermostat = hass.states.get("climate.clip_thermostat") + assert clip_thermostat is None + + thermostat_device = gateway.api.sensors["1"] + + thermostat_device.async_update({"config": {"mode": "off"}}) + await hass.async_block_till_done() + + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "off" + + thermostat_device.async_update({"config": {"mode": "other"}, "state": {"on": True}}) + await hass.async_block_till_done() + + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "heat" + + thermostat_device.async_update({"state": {"on": False}}) + await hass.async_block_till_done() + + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "off" + + # Verify service calls + + with patch.object( + thermostat_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + climate.DOMAIN, + climate.SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.thermostat", "hvac_mode": "auto"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/sensors/1/config", {"mode": "auto"}) + + with patch.object( + thermostat_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + climate.DOMAIN, + climate.SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.thermostat", "hvac_mode": "heat"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/sensors/1/config", {"mode": "heat"}) + + with patch.object( + thermostat_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + climate.DOMAIN, + climate.SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.thermostat", "hvac_mode": "off"}, + blocking=True, + ) + set_callback.assert_called_with("/sensors/1/config", {"mode": "off"}) + + with patch.object( + thermostat_device, "_async_set_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + climate.DOMAIN, + climate.SERVICE_SET_TEMPERATURE, + {"entity_id": "climate.thermostat", "temperature": 20}, + blocking=True, + ) + set_callback.assert_called_with("/sensors/1/config", {"heatsetpoint": 2000.0}) + + +async def test_clip_climate_device(hass): + """Test successful creation of sensor entities.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, + ENTRY_CONFIG, + options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}, + get_state_response=data, ) + assert "climate.thermostat" in gateway.deconz_ids + assert "sensor.thermostat" not in gateway.deconz_ids + assert "sensor.thermostat_battery_level" in gateway.deconz_ids + assert "climate.presence_sensor" not in gateway.deconz_ids + assert "climate.clip_thermostat" in gateway.deconz_ids + assert len(hass.states.async_all()) == 4 + + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "auto" + + thermostat = hass.states.get("sensor.thermostat") + assert thermostat is None + + thermostat_battery_level = hass.states.get("sensor.thermostat_battery_level") + assert thermostat_battery_level.state == "100" + + presence_sensor = hass.states.get("climate.presence_sensor") + assert presence_sensor is None - assert len(gateway.api.session.put.mock_calls) == 4 + clip_thermostat = hass.states.get("climate.clip_thermostat") + assert clip_thermostat.state == "heat" async def test_verify_state_update(hass): """Test that state update properly.""" - gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)}) - assert "climate.climate_1_name" in gateway.deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert "climate.thermostat" in gateway.deconz_ids - thermostat = hass.states.get("climate.climate_1_name") + thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" state_update = { @@ -174,44 +259,32 @@ async def test_verify_state_update(hass): "state": {"on": False}, } gateway.api.async_event_handler(state_update) - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 - thermostat = hass.states.get("climate.climate_1_name") + thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" assert gateway.api.sensors["1"].changed_keys == {"state", "r", "t", "on", "e", "id"} async def test_add_new_climate_device(hass): - """Test successful creation of climate entities.""" - gateway = await setup_gateway(hass, {}) - sensor = Mock() - sensor.name = "name" - sensor.type = "ZHAThermostat" - sensor.uniqueid = "1" - sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) - await hass.async_block_till_done() - assert "climate.name" in gateway.deconz_ids - - -async def test_do_not_allow_clipsensor(hass): - """Test that clip sensors can be ignored.""" - gateway = await setup_gateway(hass, {}, allow_clip_sensor=False) - sensor = Mock() - sensor.name = "name" - sensor.type = "CLIPThermostat" - sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) - await hass.async_block_till_done() + """Test that adding a new climate device works.""" + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) assert len(gateway.deconz_ids) == 0 + state_added = { + "t": "event", + "e": "added", + "r": "sensors", + "id": "1", + "sensor": deepcopy(SENSORS["1"]), + } + gateway.api.async_event_handler(state_added) + await hass.async_block_till_done() -async def test_unload_sensor(hass): - """Test that it works to unload sensor entities.""" - gateway = await setup_gateway(hass, {"sensors": SENSOR}) - - await gateway.async_reset() + assert "climate.thermostat" in gateway.deconz_ids - assert len(hass.states.async_all()) == 0 + thermostat = hass.states.get("climate.thermostat") + assert thermostat.state == "auto" diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index eb391cc563d378..947c42e6949834 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -1,123 +1,125 @@ """deCONZ sensor platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy -from tests.common import mock_coro +from asynctest import patch from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.components.sensor as sensor -SENSOR = { +SENSORS = { "1": { - "id": "Sensor 1 id", - "name": "Sensor 1 name", + "id": "Light sensor id", + "name": "Light level sensor", "type": "ZHALightLevel", "state": {"lightlevel": 30000, "dark": False}, - "config": {"reachable": True}, + "config": {"on": True, "reachable": True, "temperature": 10}, "uniqueid": "00:00:00:00:00:00:00:00-00", }, "2": { - "id": "Sensor 2 id", - "name": "Sensor 2 name", + "id": "Presence sensor id", + "name": "Presence sensor", "type": "ZHAPresence", "state": {"presence": False}, "config": {}, + "uniqueid": "00:00:00:00:00:00:00:01-00", }, "3": { - "id": "Sensor 3 id", - "name": "Sensor 3 name", + "id": "Switch 1 id", + "name": "Switch 1", "type": "ZHASwitch", "state": {"buttonevent": 1000}, "config": {}, + "uniqueid": "00:00:00:00:00:00:00:02-00", }, "4": { - "id": "Sensor 4 id", - "name": "Sensor 4 name", + "id": "Switch 2 id", + "name": "Switch 2", "type": "ZHASwitch", "state": {"buttonevent": 1000}, "config": {"battery": 100}, - "uniqueid": "00:00:00:00:00:00:00:01-00", + "uniqueid": "00:00:00:00:00:00:00:03-00", }, "5": { - "id": "Sensor 5 id", - "name": "Sensor 5 name", - "type": "ZHASwitch", - "state": {"buttonevent": 1000}, - "config": {"battery": 100}, - "uniqueid": "00:00:00:00:00:00:00:02:00-00", - }, - "6": { - "id": "Sensor 6 id", - "name": "Sensor 6 name", + "id": "Daylight sensor id", + "name": "Daylight sensor", "type": "Daylight", - "state": {"daylight": True}, + "state": {"daylight": True, "status": 130}, "config": {}, + "uniqueid": "00:00:00:00:00:00:00:04-00", }, - "7": { - "id": "Sensor 7 id", - "name": "Sensor 7 name", + "6": { + "id": "Power sensor id", + "name": "Power sensor", "type": "ZHAPower", "state": {"current": 2, "power": 6, "voltage": 3}, "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:05-00", }, - "8": { - "id": "Sensor 8 id", - "name": "Sensor 8 name", + "7": { + "id": "Consumption id", + "name": "Consumption sensor", "type": "ZHAConsumption", "state": {"consumption": 2, "power": 6}, "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:06-00", + }, + "8": { + "id": "CLIP light sensor id", + "name": "CLIP light level sensor", + "type": "CLIPLightLevel", + "state": {"lightlevel": 30000}, + "config": {"reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:07-00", }, } +BRIDGEID = "0123456789" ENTRY_CONFIG = { deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: "0123456789", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, 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, +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "mac": "00:11:22:33:44:55", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "websocketport": 1234, } +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} -async def setup_gateway(hass, data, allow_clip_sensor=True): - """Load the deCONZ sensor platform.""" - from pydeconz import DeconzSession - - loop = Mock() - session = Mock() - - ENTRY_OPTIONS[deconz.const.CONF_ALLOW_CLIP_SENSOR] = allow_clip_sensor +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, system_options={}, - options=ENTRY_OPTIONS, + options=options, + entry_id="1", ) - gateway = deconz.DeconzGateway(hass, config_entry) - gateway.api = DeconzSession(loop, session, **config_entry.data) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - # To flush out the service call to update the group + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() - return gateway + + hass.config_entries._entries.append(config_entry) + + return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] async def test_platform_manually_configured(hass): @@ -133,56 +135,143 @@ async def test_platform_manually_configured(hass): async def test_no_sensors(hass): """Test that no sensors in deconz results in no sensor entities.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_sensors(hass): """Test successful creation of sensor entities.""" - gateway = await setup_gateway(hass, {"sensors": SENSOR}) - assert "sensor.sensor_1_name" in gateway.deconz_ids - assert "sensor.sensor_2_name" not in gateway.deconz_ids - assert "sensor.sensor_3_name" not in gateway.deconz_ids - assert "sensor.sensor_3_name_battery_level" not in gateway.deconz_ids - assert "sensor.sensor_4_name" not in gateway.deconz_ids - assert "sensor.sensor_4_name_battery_level" in gateway.deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert "sensor.light_level_sensor" in gateway.deconz_ids + assert "sensor.presence_sensor" not in gateway.deconz_ids + assert "sensor.switch_1" not in gateway.deconz_ids + assert "sensor.switch_1_battery_level" not in gateway.deconz_ids + assert "sensor.switch_2" not in gateway.deconz_ids + assert "sensor.switch_2_battery_level" in gateway.deconz_ids + assert "sensor.daylight_sensor" in gateway.deconz_ids + assert "sensor.power_sensor" in gateway.deconz_ids + assert "sensor.consumption_sensor" in gateway.deconz_ids + assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids assert len(hass.states.async_all()) == 6 - gateway.api.sensors["1"].async_update({"state": {"on": False}}) - gateway.api.sensors["4"].async_update({"config": {"battery": 75}}) + light_level_sensor = hass.states.get("sensor.light_level_sensor") + assert light_level_sensor.state == "999.8" + presence_sensor = hass.states.get("sensor.presence_sensor") + assert presence_sensor is None -async def test_add_new_sensor(hass): - """Test successful creation of sensor entities.""" - gateway = await setup_gateway(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_signal_new_device("sensor"), [sensor]) - await hass.async_block_till_done() - assert "sensor.name" in gateway.deconz_ids + switch_1 = hass.states.get("sensor.switch_1") + assert switch_1 is None + + switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level") + assert switch_1_battery_level is None + switch_2 = hass.states.get("sensor.switch_2") + assert switch_2 is None -async def test_do_not_allow_clipsensor(hass): - """Test that clip sensors can be ignored.""" - gateway = await setup_gateway(hass, {}, allow_clip_sensor=False) - sensor = Mock() - sensor.name = "name" - sensor.type = "CLIPTemperature" - sensor.register_async_callback = Mock() - async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor]) + switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") + assert switch_2_battery_level.state == "100" + + daylight_sensor = hass.states.get("sensor.daylight_sensor") + assert daylight_sensor.state == "dawn" + + power_sensor = hass.states.get("sensor.power_sensor") + assert power_sensor.state == "6" + + consumption_sensor = hass.states.get("sensor.consumption_sensor") + assert consumption_sensor.state == "0.002" + + gateway.api.sensors["1"].async_update({"state": {"lightlevel": 2000}}) + gateway.api.sensors["4"].async_update({"config": {"battery": 75}}) await hass.async_block_till_done() - assert len(gateway.deconz_ids) == 0 + light_level_sensor = hass.states.get("sensor.light_level_sensor") + assert light_level_sensor.state == "1.6" -async def test_unload_sensor(hass): - """Test that it works to unload sensor entities.""" - gateway = await setup_gateway(hass, {"sensors": SENSOR}) + switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") + assert switch_2_battery_level.state == "75" - await gateway.async_reset() - assert len(hass.states.async_all()) == 0 +async def test_allow_clip_sensors(hass): + """Test that CLIP sensors can be allowed.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, + ENTRY_CONFIG, + options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True}, + get_state_response=data, + ) + assert "sensor.light_level_sensor" in gateway.deconz_ids + assert "sensor.presence_sensor" not in gateway.deconz_ids + assert "sensor.switch_1" not in gateway.deconz_ids + assert "sensor.switch_1_battery_level" not in gateway.deconz_ids + assert "sensor.switch_2" not in gateway.deconz_ids + assert "sensor.switch_2_battery_level" in gateway.deconz_ids + assert "sensor.daylight_sensor" in gateway.deconz_ids + assert "sensor.power_sensor" in gateway.deconz_ids + assert "sensor.consumption_sensor" in gateway.deconz_ids + assert "sensor.clip_light_level_sensor" in gateway.deconz_ids + assert len(hass.states.async_all()) == 7 + + light_level_sensor = hass.states.get("sensor.light_level_sensor") + assert light_level_sensor.state == "999.8" + + presence_sensor = hass.states.get("sensor.presence_sensor") + assert presence_sensor is None + + switch_1 = hass.states.get("sensor.switch_1") + assert switch_1 is None + + switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level") + assert switch_1_battery_level is None + + switch_2 = hass.states.get("sensor.switch_2") + assert switch_2 is None + + switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") + assert switch_2_battery_level.state == "100" + + daylight_sensor = hass.states.get("sensor.daylight_sensor") + assert daylight_sensor.state == "dawn" + + power_sensor = hass.states.get("sensor.power_sensor") + assert power_sensor.state == "6" + + consumption_sensor = hass.states.get("sensor.consumption_sensor") + assert consumption_sensor.state == "0.002" + + clip_light_level_sensor = hass.states.get("sensor.clip_light_level_sensor") + assert clip_light_level_sensor.state == "999.8" + + +async def test_add_new_sensor(hass): + """Test that adding a new sensor works.""" + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 + + state_added = { + "t": "event", + "e": "added", + "r": "sensors", + "id": "1", + "sensor": deepcopy(SENSORS["1"]), + } + gateway.api.async_event_handler(state_added) + await hass.async_block_till_done() + + assert "sensor.light_level_sensor" in gateway.deconz_ids + + light_level_sensor = hass.states.get("sensor.light_level_sensor") + assert light_level_sensor.state == "999.8" From 873d331ee3e1408e673cb1c7bb6851e4b4e1ba1c Mon Sep 17 00:00:00 2001 From: roblandry Date: Wed, 18 Sep 2019 14:11:26 -0400 Subject: [PATCH 0343/3953] Fix torque degree char (#26183) * Fix for \xC2\xB0 char instead of degree symbol * Remove comment * Black --- homeassistant/components/torque/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 0806ba0799c452..10161856a47a71 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -88,7 +88,12 @@ def get(self, request): names[pid] = data[key] elif is_unit: pid = convert_pid(is_unit.group(1)) - units[pid] = data[key] + + temp_unit = data[key] + if "\\xC2\\xB0" in temp_unit: + temp_unit = temp_unit.replace("\\xC2\\xB0", "°") + + units[pid] = temp_unit elif is_value: pid = convert_pid(is_value.group(1)) if pid in self.sensors: From f66a42d521c3e465f7c8064d2cc432944386cf6b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Sep 2019 13:40:17 -0700 Subject: [PATCH 0344/3953] Updated frontend to 20190918.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 628099a47d2c15..978127c6342028 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==20190918.0" + "home-assistant-frontend==20190918.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bbeb228f3459e0..5eeec405e7da3d 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==20190918.0 +home-assistant-frontend==20190918.1 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 415c8e514f58b8..9848716d895e32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.0 +home-assistant-frontend==20190918.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ff52cf2255c4c..69ca7eefe03a52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.0 +home-assistant-frontend==20190918.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 1af5d206012786fd3166b971886e0a2001c0810d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Sep 2019 13:40:17 -0700 Subject: [PATCH 0345/3953] Updated frontend to 20190918.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 628099a47d2c15..978127c6342028 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==20190918.0" + "home-assistant-frontend==20190918.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bbeb228f3459e0..5eeec405e7da3d 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==20190918.0 +home-assistant-frontend==20190918.1 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 55f59104ba547a..d880b672d723f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.0 +home-assistant-frontend==20190918.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ff52cf2255c4c..69ca7eefe03a52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.0 +home-assistant-frontend==20190918.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From e3f25eb730ff3934b2a03ae3631e3872749076c1 Mon Sep 17 00:00:00 2001 From: zewelor Date: Wed, 18 Sep 2019 19:07:07 +0200 Subject: [PATCH 0346/3953] Fix yeelight inheritance order (#26706) --- homeassistant/components/yeelight/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 171f25128c6640..b47cdb981612e4 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -822,7 +822,7 @@ def _predefined_effects(self): class YeelightWhiteTempWithoutNightlightSwitch( - YeelightGenericLight, YeelightWhiteTempLightsupport + YeelightWhiteTempLightsupport, YeelightGenericLight ): """White temp light, when nightlight switch is not set to light.""" @@ -831,7 +831,7 @@ def _brightness_property(self): return "current_brightness" -class YeelightWithNightLight(YeelightGenericLight, YeelightWhiteTempLightsupport): +class YeelightWithNightLight(YeelightWhiteTempLightsupport, YeelightGenericLight): """Representation of a Yeelight with nightlight support. It represents case when nightlight switch is set to light. From 9f08e2b718d5e604e9416d8fde85b8f683509e51 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Sep 2019 14:47:50 -0700 Subject: [PATCH 0347/3953] Bumped version to 0.99.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6eab8b4cd4078e..7013242676df70 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 99 -PATCH_VERSION = "0b4" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 88154074c126a2f6c3a98df3f64d9b8c43a09d99 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Sep 2019 17:29:34 -0700 Subject: [PATCH 0348/3953] Update translations --- .../components/adguard/.translations/es.json | 27 ++++++++++- .../ambiclimate/.translations/ru.json | 2 +- .../arcam_fmj/.translations/es.json | 5 ++ .../arcam_fmj/.translations/fr.json | 5 ++ .../arcam_fmj/.translations/lb.json | 5 ++ .../components/axis/.translations/es.json | 1 + .../cert_expiry/.translations/en.json | 4 +- .../cert_expiry/.translations/es.json | 8 ++-- .../cert_expiry/.translations/fr.json | 24 ++++++++++ .../cert_expiry/.translations/it.json | 2 +- .../cert_expiry/.translations/lb.json | 7 +++ .../cert_expiry/.translations/sl.json | 2 +- .../components/deconz/.translations/bg.json | 29 ++++++++++++ .../components/deconz/.translations/ca.json | 29 ++++++++++++ .../components/deconz/.translations/da.json | 18 +++++++ .../components/deconz/.translations/es.json | 35 +++++++++++++- .../components/deconz/.translations/fr.json | 47 +++++++++++++++++++ .../components/deconz/.translations/it.json | 29 ++++++++++++ .../components/deconz/.translations/ko.json | 29 ++++++++++++ .../components/deconz/.translations/lb.json | 40 +++++++++++++++- .../components/deconz/.translations/nl.json | 29 ++++++++++++ .../components/deconz/.translations/no.json | 36 ++++++++++++++ .../components/deconz/.translations/pl.json | 29 ++++++++++++ .../components/deconz/.translations/ru.json | 28 ++++++++++- .../components/deconz/.translations/sl.json | 29 ++++++++++++ .../deconz/.translations/zh-Hant.json | 29 ++++++++++++ .../components/esphome/.translations/es.json | 1 + .../components/esphome/.translations/lb.json | 2 +- .../geonetnz_quakes/.translations/es.json | 7 ++- .../geonetnz_quakes/.translations/fr.json | 17 +++++++ .../components/heos/.translations/lb.json | 2 +- .../homekit_controller/.translations/es.json | 3 +- .../homekit_controller/.translations/fr.json | 2 +- .../homekit_controller/.translations/lb.json | 2 +- .../homematicip_cloud/.translations/lb.json | 2 +- .../components/hue/.translations/es.json | 2 + .../iaqualink/.translations/bg.json | 21 +++++++++ .../iaqualink/.translations/es.json | 21 +++++++++ .../iaqualink/.translations/fr.json | 21 +++++++++ .../iaqualink/.translations/lb.json | 21 +++++++++ .../iaqualink/.translations/no.json | 21 +++++++++ .../iaqualink/.translations/sl.json | 21 +++++++++ .../components/life360/.translations/es.json | 4 +- .../components/life360/.translations/fr.json | 1 + .../components/life360/.translations/ru.json | 4 +- .../components/light/.translations/bg.json | 13 +++++ .../components/light/.translations/ca.json | 4 +- .../components/light/.translations/da.json | 4 +- .../components/light/.translations/de.json | 4 +- .../components/light/.translations/en.json | 4 +- .../components/light/.translations/es.json | 13 ++++- .../components/light/.translations/fr.json | 13 ++++- .../components/light/.translations/it.json | 4 +- .../components/light/.translations/ko.json | 4 +- .../components/light/.translations/lb.json | 17 +++++++ .../components/light/.translations/nl.json | 8 ++-- .../components/light/.translations/no.json | 13 ++++- .../components/light/.translations/pl.json | 4 +- .../components/light/.translations/ru.json | 4 +- .../components/light/.translations/sl.json | 13 ++++- .../light/.translations/zh-Hant.json | 14 +++--- .../components/linky/.translations/bg.json | 25 ++++++++++ .../components/linky/.translations/es.json | 25 ++++++++++ .../components/linky/.translations/lb.json | 25 ++++++++++ .../components/linky/.translations/no.json | 25 ++++++++++ .../logi_circle/.translations/ru.json | 2 +- .../components/met/.translations/es.json | 5 +- .../components/met/.translations/lb.json | 2 +- .../components/nest/.translations/ru.json | 2 +- .../components/plaato/.translations/es.json | 3 +- .../components/point/.translations/es.json | 2 +- .../components/point/.translations/ru.json | 2 +- .../components/ps4/.translations/fr.json | 4 +- .../components/ps4/.translations/lb.json | 6 +-- .../solaredge/.translations/bg.json | 20 ++++++++ .../solaredge/.translations/es.json | 21 +++++++++ .../solaredge/.translations/fr.json | 21 +++++++++ .../solaredge/.translations/lb.json | 21 +++++++++ .../solaredge/.translations/no.json | 21 +++++++++ .../solaredge/.translations/sl.json | 21 +++++++++ .../solaredge/.translations/zh-Hant.json | 21 +++++++++ .../components/switch/.translations/bg.json | 17 +++++++ .../components/switch/.translations/ca.json | 6 ++- .../components/switch/.translations/en.json | 6 ++- .../components/switch/.translations/es.json | 19 ++++++++ .../components/switch/.translations/fr.json | 17 +++++++ .../components/switch/.translations/it.json | 4 +- .../components/switch/.translations/ko.json | 6 ++- .../components/switch/.translations/lb.json | 19 ++++++++ .../components/switch/.translations/nl.json | 6 ++- .../components/switch/.translations/no.json | 17 +++++++ .../components/switch/.translations/pl.json | 4 +- .../components/switch/.translations/ru.json | 4 +- .../components/switch/.translations/sl.json | 17 +++++++ .../switch/.translations/zh-Hant.json | 17 +++++++ .../tellduslive/.translations/es.json | 1 + .../components/toon/.translations/ru.json | 2 +- .../components/traccar/.translations/es.json | 15 +++++- .../components/traccar/.translations/fr.json | 18 +++++++ .../components/traccar/.translations/lb.json | 18 +++++++ .../twentemilieu/.translations/es.json | 13 +++-- .../twentemilieu/.translations/fr.json | 23 +++++++++ .../twentemilieu/.translations/lb.json | 21 +++++++++ .../components/unifi/.translations/ca.json | 6 +++ .../components/unifi/.translations/es.json | 7 +++ .../components/unifi/.translations/fr.json | 12 +++++ .../components/unifi/.translations/nl.json | 6 +++ .../components/upnp/.translations/ca.json | 4 ++ .../components/velbus/.translations/es.json | 11 ++++- .../components/velbus/.translations/fr.json | 21 +++++++++ .../components/velbus/.translations/lb.json | 20 ++++++++ .../components/vesync/.translations/es.json | 6 ++- .../components/vesync/.translations/lb.json | 20 ++++++++ .../components/withings/.translations/ca.json | 3 ++ .../components/withings/.translations/en.json | 3 ++ .../components/withings/.translations/ko.json | 3 ++ .../components/withings/.translations/lb.json | 13 +++++ .../components/withings/.translations/nl.json | 3 ++ .../components/withings/.translations/ru.json | 3 ++ 119 files changed, 1428 insertions(+), 96 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/es.json create mode 100644 homeassistant/components/arcam_fmj/.translations/fr.json create mode 100644 homeassistant/components/arcam_fmj/.translations/lb.json create mode 100644 homeassistant/components/cert_expiry/.translations/fr.json create mode 100644 homeassistant/components/cert_expiry/.translations/lb.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/fr.json create mode 100644 homeassistant/components/iaqualink/.translations/bg.json create mode 100644 homeassistant/components/iaqualink/.translations/es.json create mode 100644 homeassistant/components/iaqualink/.translations/fr.json create mode 100644 homeassistant/components/iaqualink/.translations/lb.json create mode 100644 homeassistant/components/iaqualink/.translations/no.json create mode 100644 homeassistant/components/iaqualink/.translations/sl.json create mode 100644 homeassistant/components/light/.translations/bg.json create mode 100644 homeassistant/components/light/.translations/lb.json create mode 100644 homeassistant/components/linky/.translations/bg.json create mode 100644 homeassistant/components/linky/.translations/es.json create mode 100644 homeassistant/components/linky/.translations/lb.json create mode 100644 homeassistant/components/linky/.translations/no.json create mode 100644 homeassistant/components/solaredge/.translations/bg.json create mode 100644 homeassistant/components/solaredge/.translations/es.json create mode 100644 homeassistant/components/solaredge/.translations/fr.json create mode 100644 homeassistant/components/solaredge/.translations/lb.json create mode 100644 homeassistant/components/solaredge/.translations/no.json create mode 100644 homeassistant/components/solaredge/.translations/sl.json create mode 100644 homeassistant/components/solaredge/.translations/zh-Hant.json create mode 100644 homeassistant/components/switch/.translations/bg.json create mode 100644 homeassistant/components/switch/.translations/es.json create mode 100644 homeassistant/components/switch/.translations/fr.json create mode 100644 homeassistant/components/switch/.translations/lb.json create mode 100644 homeassistant/components/switch/.translations/no.json create mode 100644 homeassistant/components/switch/.translations/sl.json create mode 100644 homeassistant/components/switch/.translations/zh-Hant.json create mode 100644 homeassistant/components/traccar/.translations/fr.json create mode 100644 homeassistant/components/traccar/.translations/lb.json create mode 100644 homeassistant/components/twentemilieu/.translations/fr.json create mode 100644 homeassistant/components/twentemilieu/.translations/lb.json create mode 100644 homeassistant/components/velbus/.translations/fr.json create mode 100644 homeassistant/components/velbus/.translations/lb.json create mode 100644 homeassistant/components/vesync/.translations/lb.json create mode 100644 homeassistant/components/withings/.translations/lb.json diff --git a/homeassistant/components/adguard/.translations/es.json b/homeassistant/components/adguard/.translations/es.json index 971d38f9ab2d26..5886d8e5c5b6fc 100644 --- a/homeassistant/components/adguard/.translations/es.json +++ b/homeassistant/components/adguard/.translations/es.json @@ -1,7 +1,30 @@ { "config": { "abort": { - "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente." - } + "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.", + "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." + }, + "error": { + "connection_error": "No se conect\u00f3." + }, + "step": { + "hassio_confirm": { + "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Hass.io: {addon} ?", + "title": "AdGuard Home a trav\u00e9s del complemento Hass.io" + }, + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "ssl": "AdGuard Home utiliza un certificado SSL", + "username": "Nombre de usuario", + "verify_ssl": "AdGuard Home utiliza un certificado apropiado" + }, + "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control.", + "title": "Enlace su AdGuard Home." + } + }, + "title": "AdGuard Home" } } \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json index 129579315a29de..a4300e1e5306c5 100644 --- a/homeassistant/components/ambiclimate/.translations/ru.json +++ b/homeassistant/components/ambiclimate/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "no_config": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/arcam_fmj/.translations/es.json b/homeassistant/components/arcam_fmj/.translations/es.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/es.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/fr.json b/homeassistant/components/arcam_fmj/.translations/fr.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/fr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/lb.json b/homeassistant/components/arcam_fmj/.translations/lb.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/lb.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/es.json b/homeassistant/components/axis/.translations/es.json index 817737eee04d88..d29481a3be96f8 100644 --- a/homeassistant/components/axis/.translations/es.json +++ b/homeassistant/components/axis/.translations/es.json @@ -8,6 +8,7 @@ }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en curso.", "device_unavailable": "El dispositivo no est\u00e1 disponible", "faulty_credentials": "Credenciales de usuario incorrectas" }, diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index 85575df62914a1..873dfee9a92bb9 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Can not fetch certificate from this host and port combination", - "connection_timeout": "Timeout whemn connecting to this host", + "connection_timeout": "Timeout when connecting to this host", "host_port_exists": "This host and port combination is already configured", "resolve_failed": "This host can not be resolved" }, @@ -21,4 +21,4 @@ }, "title": "Certificate Expiry" } -} +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/es.json b/homeassistant/components/cert_expiry/.translations/es.json index 2cb0bd9af166ed..b10518646ac907 100644 --- a/homeassistant/components/cert_expiry/.translations/es.json +++ b/homeassistant/components/cert_expiry/.translations/es.json @@ -5,8 +5,9 @@ }, "error": { "certificate_fetch_failed": "No se puede obtener el certificado de esta combinaci\u00f3n de host y puerto", - "connection_timeout": "Tiempo de espera agotado al conectar con el dispositivo.", - "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" + "connection_timeout": "Tiempo de espera agotado al conectar a este host", + "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", + "resolve_failed": "Este host no se puede resolver" }, "step": { "user": { @@ -14,7 +15,8 @@ "host": "El nombre de host del certificado", "name": "El nombre del certificado", "port": "El puerto del certificado" - } + }, + "title": "Defina el certificado para probar" } }, "title": "Caducidad del certificado" diff --git a/homeassistant/components/cert_expiry/.translations/fr.json b/homeassistant/components/cert_expiry/.translations/fr.json new file mode 100644 index 00000000000000..a3536902c76d26 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e" + }, + "error": { + "certificate_fetch_failed": "Impossible de r\u00e9cup\u00e9rer le certificat de cette combinaison h\u00f4te / port", + "connection_timeout": "D\u00e9lai d'attente lors de la connexion \u00e0 cet h\u00f4te", + "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e", + "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu" + }, + "step": { + "user": { + "data": { + "host": "Le nom d'h\u00f4te du certificat", + "name": "Le nom du certificat", + "port": "Le port du certificat" + }, + "title": "D\u00e9finir le certificat \u00e0 tester" + } + }, + "title": "Expiration du certificat" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/it.json b/homeassistant/components/cert_expiry/.translations/it.json index 9135ed3b478592..73749382dd9bca 100644 --- a/homeassistant/components/cert_expiry/.translations/it.json +++ b/homeassistant/components/cert_expiry/.translations/it.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta", - "connection_timeout": "Tempo scaduto durante la connessione a questo host", + "connection_timeout": "Tempo scaduto collegandosi a questo host", "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", "resolve_failed": "Questo host non pu\u00f2 essere risolto" }, diff --git a/homeassistant/components/cert_expiry/.translations/lb.json b/homeassistant/components/cert_expiry/.translations/lb.json new file mode 100644 index 00000000000000..d6811728a22402 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/lb.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/sl.json b/homeassistant/components/cert_expiry/.translations/sl.json index c088e414c73dbd..3774956330a52a 100644 --- a/homeassistant/components/cert_expiry/.translations/sl.json +++ b/homeassistant/components/cert_expiry/.translations/sl.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila", - "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem", + "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem je potekla", "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti" }, diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index a02a6ff422338f..f3eead4aae023a 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -40,5 +40,34 @@ } }, "title": "deCONZ" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", + "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "close": "\u0417\u0430\u0442\u0432\u0430\u0440\u044f\u043d\u0435", + "dim_down": "\u0417\u0430\u0442\u044a\u043c\u043d\u044f\u0432\u0430\u043d\u0435", + "dim_up": "\u041e\u0441\u0432\u0435\u0442\u044f\u0432\u0430\u043d\u0435", + "left": "\u041b\u044f\u0432\u043e", + "open": "\u041e\u0442\u0432\u0430\u0440\u044f\u043d\u0435", + "right": "\u0414\u044f\u0441\u043d\u043e", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", + "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_rotated": "\u0417\u0430\u0432\u044a\u0440\u0442\u044f\u043d \u0431\u0443\u0442\u043e\u043d \"{subtype}\"", + "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", + "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", + "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 263730ba5837c2..d36de4acc1e96c 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -41,6 +41,35 @@ }, "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Ambd\u00f3s botons", + "button_1": "Primer bot\u00f3", + "button_2": "Segon bot\u00f3", + "button_3": "Tercer bot\u00f3", + "button_4": "Quart bot\u00f3", + "close": "Tanca", + "dim_down": "Atenua la brillantor", + "dim_up": "Augmenta la brillantor", + "left": "Esquerra", + "open": "Obert", + "right": "Dreta", + "turn_off": "Desactiva", + "turn_on": "Activa" + }, + "trigger_type": { + "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives", + "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament", + "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives", + "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives", + "remote_button_rotated": "Bot\u00f3 \"{subtype}\" girat", + "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", + "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", + "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives", + "remote_gyro_activated": "Dispositiu sacsejat" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index 1b595924106cfd..6b74c09107a098 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -41,6 +41,24 @@ }, "title": "deCONZ Zigbee gateway" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Begge knapper", + "button_1": "F\u00f8rste knap", + "button_2": "Anden knap", + "button_3": "Tredje knap", + "button_4": "Fjerde knap", + "close": "Luk", + "dim_down": "D\u00e6mp ned", + "dim_up": "D\u00e6mp op", + "left": "Venstre", + "open": "\u00c5ben", + "right": "H\u00f8jre" + }, + "trigger_type": { + "remote_gyro_activated": "Enhed rystet" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 8bcf03914cee84..1bc6c8211a268d 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured": "El puente ya esta configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.", "no_bridges": "No se han descubierto puentes deCONZ", + "not_deconz_bridge": "No es un puente deCONZ", "one_instance_only": "El componente solo admite una instancia de deCONZ", "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, @@ -39,12 +41,43 @@ }, "title": "Pasarela Zigbee deCONZ" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Ambos botones", + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "close": "Cerrar", + "dim_down": "Bajar la intensidad", + "dim_up": "Subir la intensidad", + "left": "Izquierda", + "open": "Abierto", + "right": "Derecha", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces consecutivas", + "remote_button_long_press": "bot\u00f3n \"{subtype}\" pulsado continuamente", + "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de un rato pulsado", + "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", + "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", + "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", + "remote_button_short_press": "bot\u00f3n \"{subtype}\" pulsado", + "remote_button_short_release": "bot\u00f3n \"{subtype}\" liberado", + "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", + "remote_gyro_activated": "Dispositivo sacudido" + } + }, "options": { "step": { "async_step_deconz_devices": { "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", "allow_deconz_groups": "Permitir grupos de luz deCONZ" - } + }, + "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ" }, "deconz_devices": { "data": { diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 9b98914314a749..cc6d22945dcb79 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -40,5 +40,52 @@ } }, "title": "Passerelle deCONZ Zigbee" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Les deux boutons", + "button_1": "Premier bouton", + "button_2": "Deuxi\u00e8me bouton", + "button_3": "Troisi\u00e8me bouton", + "button_4": "Quatri\u00e8me bouton", + "close": "Ferm\u00e9", + "dim_down": "Assombrir", + "dim_up": "\u00c9claircir", + "left": "Gauche", + "open": "Ouvert", + "right": "Droite", + "turn_off": "\u00c9teint", + "turn_on": "Allum\u00e9" + }, + "trigger_type": { + "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9", + "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", + "remote_button_long_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9 apr\u00e8s appui long", + "remote_button_quadruple_press": "Bouton \"{subtype}\" quadruple cliqu\u00e9", + "remote_button_quintuple_press": "Bouton \"{subtype}\" quintuple cliqu\u00e9", + "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9", + "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", + "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", + "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", + "remote_gyro_activated": "Appareil secou\u00e9" + } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP", + "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ" + }, + "description": "Configurer la visibilit\u00e9 des appareils de type deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP", + "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ" + }, + "description": "Configurer la visibilit\u00e9 des appareils de type deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 90b85aaeba5f01..7a2b8832864e2b 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -41,6 +41,35 @@ }, "title": "Gateway Zigbee deCONZ" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Entrambi", + "button_1": "Primo", + "button_2": "Secondo pulsante", + "button_3": "Terzo pulsante", + "button_4": "Quarto pulsante", + "close": "Chiudere", + "dim_down": "Diminuire luminosit\u00e0", + "dim_up": "Aumentare luminosit\u00e0", + "left": "Sinistra", + "open": "Aperto", + "right": "Destra", + "turn_off": "Spegnere", + "turn_on": "Accendere" + }, + "trigger_type": { + "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", + "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", + "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", + "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", + "remote_button_rotated": "Pulsante ruotato \"{subtype}\"", + "remote_button_short_press": "Pulsante \"{subtype}\" premuto", + "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", + "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", + "remote_gyro_activated": "Dispositivo in vibrazione" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 0ddff8557ec14c..923a2beb2ffba2 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\ub450 \uac1c", + "button_1": "\uccab \ubc88\uc9f8", + "button_2": "\ub450 \ubc88\uc9f8", + "button_3": "\uc138 \ubc88\uc9f8", + "button_4": "\ub124 \ubc88\uc9f8", + "close": "\ub2eb\uae30", + "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30", + "dim_up": "\ubc1d\uac8c \ud558\uae30", + "left": "\uc67c\ucabd", + "open": "\uc5f4\uae30", + "right": "\uc624\ub978\ucabd", + "turn_off": "\ub044\uae30", + "turn_on": "\ucf1c\uae30" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", + "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", + "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", + "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", + "remote_button_rotated": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", + "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984", + "remote_gyro_activated": "\uae30\uae30 \ud754\ub4e6" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 60a27304d78437..c536e5771411f9 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -23,7 +23,7 @@ "init": { "data": { "host": "Host", - "port": "Port (Standard Wert: '80')" + "port": "Port" }, "title": "deCONZ gateway d\u00e9fin\u00e9ieren" }, @@ -40,5 +40,43 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "B\u00e9id Kn\u00e4ppchen", + "button_1": "\u00c9ischte Kn\u00e4ppchen", + "button_2": "Zweete Kn\u00e4ppchen", + "button_3": "Dr\u00ebtte Kn\u00e4ppchen", + "button_4": "V\u00e9ierte Kn\u00e4ppchen", + "close": "Zoumaachen", + "left": "L\u00e9nks", + "open": "Op", + "right": "Riets", + "turn_off": "Ausschalten", + "turn_on": "Uschalten" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" Kn\u00e4ppche zwee mol gedr\u00e9ckt", + "remote_button_long_press": "\"{subtype}\" Kn\u00e4ppche permanent gedr\u00e9ckt", + "remote_button_long_release": "\"{subtype}\" Kn\u00e4ppche no laangem unhalen lassgelooss", + "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt", + "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt", + "remote_button_rotated": "Kn\u00e4ppche gedr\u00e9int \"{subtype}\"", + "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", + "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", + "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", + "remote_gyro_activated": "Apparat ger\u00ebselt" + } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", + "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben" + }, + "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index f9f2d40488f6a4..116f6254b37213 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee gateway" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Beide knoppen", + "button_1": "Eerste knop", + "button_2": "Tweede knop", + "button_3": "Derde knop", + "button_4": "Vierde knop", + "close": "Sluiten", + "dim_down": "Dim omlaag", + "dim_up": "Dim omhoog", + "left": "Links", + "open": "Open", + "right": "Rechts", + "turn_off": "Uitschakelen", + "turn_on": "Inschakelen" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" knop dubbel geklikt", + "remote_button_long_press": "\" {subtype} \" knop continu ingedrukt", + "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang indrukken van de knop", + "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt", + "remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt", + "remote_button_rotated": "Knop gedraaid \" {subtype} \"", + "remote_button_short_press": "\" {subtype} \" knop ingedrukt", + "remote_button_short_release": "\"{subtype}\" knop losgelaten", + "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt", + "remote_gyro_activated": "Apparaat geschud" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 8798248224a582..7a93c6ff9cf850 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee gateway" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Begge knappene", + "button_1": "F\u00f8rste knapp", + "button_2": "Andre knapp", + "button_3": "Tredje knapp", + "button_4": "Fjerde knapp", + "close": "Lukk", + "dim_down": "Dimm ned", + "dim_up": "Dimm opp", + "left": "Venstre", + "open": "\u00c5pen", + "right": "H\u00f8yre", + "turn_off": "Skru av", + "turn_on": "Sl\u00e5 p\u00e5" + }, + "trigger_type": { + "remote_button_double_press": "\"{under type}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{undertype}\" - knappen ble kontinuerlig trykket", + "remote_button_long_release": "\" {subtype} \" -knapp sluppet etter langt trykk", + "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{undertype}\" - knappen femdobbelt klikket", + "remote_button_rotated": "Knappen roterte \" {subtype} \"", + "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", + "remote_button_short_release": "\" {subtype} \" -knappen ble utgitt", + "remote_button_triple_press": "\"{under type}\"-knappen trippel klikket", + "remote_gyro_activated": "Enhet er ristet" + } + }, "options": { "step": { "async_step_deconz_devices": { @@ -49,6 +78,13 @@ "allow_deconz_groups": "Tillat deCONZ lys grupper" }, "description": "Konfigurere synlighet av deCONZ enhetstyper" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Tillat deCONZ CLIP-sensorer", + "allow_deconz_groups": "Tillat deCONZ lys grupper" + }, + "description": "Konfigurere synlighet av deCONZ enhetstyper" } } } diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 506461ea50e8e0..70c33cf3c02f4f 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -41,6 +41,35 @@ }, "title": "Brama deCONZ Zigbee" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Oba przyciski", + "button_1": "Pierwszy przycisk", + "button_2": "Drugi przycisk", + "button_3": "Trzeci przycisk", + "button_4": "Czwarty przycisk", + "close": "Zamknij", + "dim_down": "Przyciemnienie", + "dim_up": "Przyciemnienie", + "left": "Lewo", + "open": "Otw\u00f3rz", + "right": "Prawo", + "turn_off": "Wy\u0142\u0105cz", + "turn_on": "W\u0142\u0105cz" + }, + "trigger_type": { + "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "Przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "Przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_rotated": "Przycisk obr\u00f3cony \"{subtype}\"", + "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", + "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", + "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", + "remote_gyro_activated": "Urz\u0105dzenie potrz\u0105\u015bni\u0119te" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 23e98919bb8076..92fd1e3e7490c6 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ", @@ -25,7 +25,7 @@ "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" }, - "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ" + "title": "deCONZ" }, "link": { "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb", @@ -41,6 +41,30 @@ }, "title": "deCONZ" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u041e\u0431\u0435 \u043a\u043d\u043e\u043f\u043a\u0438", + "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", + "left": "\u041d\u0430\u043b\u0435\u0432\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", + "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e" + }, + "trigger_type": { + "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", + "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430", + "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 86210b2e6c1065..9aebb2a556f6cd 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee prehod" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Oba gumba", + "button_1": "Prvi gumb", + "button_2": "Drugi gumb", + "button_3": "Tretji gumb", + "button_4": "\u010cetrti gumb", + "close": "Zapri", + "dim_down": "Zatemnite", + "dim_up": "pove\u010dajte mo\u010d", + "left": "Levo", + "open": "Odprto", + "right": "Desno", + "turn_off": "Ugasni", + "turn_on": "Pri\u017egi" + }, + "trigger_type": { + "remote_button_double_press": "Dvakrat kliknete gumb \"{subtype}\"", + "remote_button_long_press": "\"{subtype}\" gumb neprekinjeno pritisnjen", + "remote_button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku", + "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", + "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", + "remote_button_rotated": "Gumb \"{subtype}\" zasukan", + "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", + "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", + "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", + "remote_gyro_activated": "Naprava se je pretresla" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 75dcac93dd9eb0..f024386aa0f873 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -41,6 +41,35 @@ }, "title": "deCONZ Zigbee \u9598\u9053\u5668" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u5169\u500b\u6309\u9215", + "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "close": "\u95dc\u9589", + "dim_down": "\u8abf\u6697", + "dim_up": "\u8abf\u4eae", + "left": "\u5de6", + "open": "\u958b\u555f", + "right": "\u53f3", + "turn_off": "\u95dc\u9589", + "turn_on": "\u958b\u555f" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", + "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", + "remote_button_long_release": "\u9577\u6309\u5f8c\u91cb\u653e \"{subtype}\" \u6309\u9215", + "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u9ede\u64ca", + "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u9ede\u64ca", + "remote_button_rotated": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215", + "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", + "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", + "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/esphome/.translations/es.json b/homeassistant/components/esphome/.translations/es.json index 88730a18554e9a..70d766cf4c0544 100644 --- a/homeassistant/components/esphome/.translations/es.json +++ b/homeassistant/components/esphome/.translations/es.json @@ -8,6 +8,7 @@ "invalid_password": "\u00a1Contrase\u00f1a incorrecta!", "resolve_error": "No se puede resolver la direcci\u00f3n de ESP. Si el error persiste, configura una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "Desplom\u00e9: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/.translations/lb.json b/homeassistant/components/esphome/.translations/lb.json index 955b050bc5b19d..882b67823ba288 100644 --- a/homeassistant/components/esphome/.translations/lb.json +++ b/homeassistant/components/esphome/.translations/lb.json @@ -14,7 +14,7 @@ "data": { "password": "Passwuert" }, - "description": "Gitt d'Passwuert vun \u00e4rer Konfiguratioun an.", + "description": "Gitt d'Passwuert vun \u00e4rer Konfiguratioun an fir {name}.", "title": "Passwuert aginn" }, "discovery_confirm": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/es.json b/homeassistant/components/geonetnz_quakes/.translations/es.json index 41404822dd8289..f6f592675ab33e 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/es.json +++ b/homeassistant/components/geonetnz_quakes/.translations/es.json @@ -6,9 +6,12 @@ "step": { "user": { "data": { + "mmi": "MMI", "radius": "Radio" - } + }, + "title": "Complete todos los campos requeridos" } - } + }, + "title": "GeoNet NZ Quakes" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/fr.json b/homeassistant/components/geonetnz_quakes/.translations/fr.json new file mode 100644 index 00000000000000..74ae5541754ef7 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Emplacement d\u00e9j\u00e0 enregistr\u00e9" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Rayon" + }, + "title": "Remplissez les d\u00e9tails de votre filtre." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/lb.json b/homeassistant/components/heos/.translations/lb.json index 416f0878de46a3..cfe1d347b0cc20 100644 --- a/homeassistant/components/heos/.translations/lb.json +++ b/homeassistant/components/heos/.translations/lb.json @@ -16,6 +16,6 @@ "title": "Mat Heos verbannen" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/es.json b/homeassistant/components/homekit_controller/.translations/es.json index 642e76fd1dd0d3..67f6daa8469d8e 100644 --- a/homeassistant/components/homekit_controller/.translations/es.json +++ b/homeassistant/components/homekit_controller/.translations/es.json @@ -3,6 +3,7 @@ "abort": { "accessory_not_found_error": "No se puede a\u00f1adir el emparejamiento porque ya no se puede encontrar el dispositivo.", "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", + "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en curso.", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicia el accesorio e int\u00e9ntalo de nuevo.", "ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.", "invalid_config_entry": "Este dispositivo se muestra como listo para vincular, pero ya existe una entrada que causa conflicto en Home Assistant y se debe eliminar primero.", @@ -23,7 +24,7 @@ "data": { "pairing_code": "C\u00f3digo de vinculaci\u00f3n" }, - "description": "Introduce tu c\u00f3digo de vinculaci\u00f3n de HomeKit para usar este accesorio", + "description": "Introduce tu c\u00f3digo de vinculaci\u00f3n de HomeKit (en este formato XXX-XX-XXX) para usar este accesorio", "title": "Vincular con accesorio HomeKit" }, "user": { diff --git a/homeassistant/components/homekit_controller/.translations/fr.json b/homeassistant/components/homekit_controller/.translations/fr.json index 15e50a4012701c..7f0566ddd42365 100644 --- a/homeassistant/components/homekit_controller/.translations/fr.json +++ b/homeassistant/components/homekit_controller/.translations/fr.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "Code d\u2019appairage" }, - "description": "Entrez votre code de jumelage HomeKit pour utiliser cet accessoire.", + "description": "Entrez votre code de jumelage HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire.", "title": "Appairer avec l'accessoire HomeKit" }, "user": { diff --git a/homeassistant/components/homekit_controller/.translations/lb.json b/homeassistant/components/homekit_controller/.translations/lb.json index 97efd428a0469e..ca7bce44508243 100644 --- a/homeassistant/components/homekit_controller/.translations/lb.json +++ b/homeassistant/components/homekit_controller/.translations/lb.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "Pairing Code" }, - "description": "Gitt \u00e4ren HomeKit pairing Code an fir d\u00ebsen Accessoire ze benotzen", + "description": "Gitt \u00e4ren HomeKit pairing Code (am Format XXX-XX-XXX) an fir d\u00ebsen Accessoire ze benotzen", "title": "Mam HomeKit Accessoire verbannen" }, "user": { diff --git a/homeassistant/components/homematicip_cloud/.translations/lb.json b/homeassistant/components/homematicip_cloud/.translations/lb.json index 2cad909a7ee54e..f8ae990d36442a 100644 --- a/homeassistant/components/homematicip_cloud/.translations/lb.json +++ b/homeassistant/components/homematicip_cloud/.translations/lb.json @@ -21,7 +21,7 @@ "title": "HomematicIP Accesspoint auswielen" }, "link": { - "description": "Dr\u00e9ckt de bloen Kn\u00e4ppchen um Accesspoint an den Submit Kn\u00e4ppchen fir d'HomematicIP mam Home Assistant ze registr\u00e9ieren.", + "description": "Dr\u00e9ckt de bloen Kn\u00e4ppchen um Accesspoint an den Submit Kn\u00e4ppchen fir d'HomematicIP mam Home Assistant ze registr\u00e9ieren.\n\n![Standuert vum Kn\u00e4ppchen op der Bridge](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Accesspoint verbannen" } }, diff --git a/homeassistant/components/hue/.translations/es.json b/homeassistant/components/hue/.translations/es.json index 56e7ed62e9dc8a..3ec9ed871d3dca 100644 --- a/homeassistant/components/hue/.translations/es.json +++ b/homeassistant/components/hue/.translations/es.json @@ -3,9 +3,11 @@ "abort": { "all_configured": "Todos los puentes Philips Hue ya est\u00e1n configurados", "already_configured": "El puente ya esta configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.", "cannot_connect": "No se puede conectar al puente", "discover_timeout": "No se han descubierto puentes Philips Hue", "no_bridges": "No se han descubierto puentes Philips Hue.", + "not_hue_bridge": "No es un puente Hue", "unknown": "Se produjo un error desconocido" }, "error": { diff --git a/homeassistant/components/iaqualink/.translations/bg.json b/homeassistant/components/iaqualink/.translations/bg.json new file mode 100644 index 00000000000000..5b37bde3ee3a81 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 iAqualink." + }, + "error": { + "connection_failure": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 iAqualink. \u041f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 / \u0438\u043c\u0435\u0439\u043b \u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u041c\u043e\u043b\u044f \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f iAqualink \u0430\u043a\u0430\u0443\u043d\u0442.", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/es.json b/homeassistant/components/iaqualink/.translations/es.json new file mode 100644 index 00000000000000..698be68bd78e9c --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Solo puede configurar una \u00fanica conexi\u00f3n iAqualink." + }, + "error": { + "connection_failure": "No se puede conectar a iAqualink. Verifica tu nombre de usuario y contrase\u00f1a." + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario / correo electr\u00f3nico" + }, + "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a de su cuenta de iAqualink.", + "title": "Con\u00e9ctese a iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/fr.json b/homeassistant/components/iaqualink/.translations/fr.json new file mode 100644 index 00000000000000..97971b99e9f7ab --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'une seule connexion iAqualink." + }, + "error": { + "connection_failure": "Impossible de se connecter \u00e0 iAqualink. V\u00e9rifiez votre nom d'utilisateur et votre mot de passe." + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur / adresse e-mail" + }, + "description": "Veuillez saisir le nom d'utilisateur et le mot de passe de votre compte iAqualink.", + "title": "Se connecter \u00e0 iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/lb.json b/homeassistant/components/iaqualink/.translations/lb.json new file mode 100644 index 00000000000000..4beb11214bc2c8 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen eng eenzeg iAqualink Verbindung konfigur\u00e9ieren." + }, + "error": { + "connection_failure": "Kann sech net mat iAqualink verbannen. Iwwerpr\u00e9ift \u00e4ren Benotzernumm an Passwuert" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm / E-Mail Adresse" + }, + "description": "Gitt den Benotznumm an d'Passwuert fir \u00e4ren iAqualink Kont un.", + "title": "Mat iAqualink verbannen" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/no.json b/homeassistant/components/iaqualink/.translations/no.json new file mode 100644 index 00000000000000..9d464a6d516c55 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en enkel iAqualink-tilkobling." + }, + "error": { + "connection_failure": "Kan ikke koble til iAqualink. Sjekk brukernavnet og passordet ditt." + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn / E-postadresse" + }, + "description": "Vennligst skriv inn brukernavn og passord for iAqualink-kontoen din.", + "title": "Koble til iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/sl.json b/homeassistant/components/iaqualink/.translations/sl.json new file mode 100644 index 00000000000000..e2a7f94b3d8a70 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Konfigurirate lahko samo eno povezavo iAqualink." + }, + "error": { + "connection_failure": "Ne morete vzpostaviti povezave z iAqualink. Preverite va\u0161e uporabni\u0161ko ime in geslo." + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime / e-po\u0161tni naslov" + }, + "description": "Prosimo, vnesite uporabni\u0161ko ime in geslo za iAqualink ra\u010dun.", + "title": "Pove\u017eite se z iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/es.json b/homeassistant/components/life360/.translations/es.json index 8fc70a60a052e6..2b185cb1b6c476 100644 --- a/homeassistant/components/life360/.translations/es.json +++ b/homeassistant/components/life360/.translations/es.json @@ -9,7 +9,9 @@ }, "error": { "invalid_credentials": "Credenciales no v\u00e1lidas", - "invalid_username": "Nombre de usuario no v\u00e1lido" + "invalid_username": "Nombre de usuario no v\u00e1lido", + "unexpected": "Error inesperado al comunicarse con el servidor Life360", + "user_already_configured": "La cuenta ya ha sido configurada" }, "step": { "user": { diff --git a/homeassistant/components/life360/.translations/fr.json b/homeassistant/components/life360/.translations/fr.json index cb4682fc937103..947425e4807f91 100644 --- a/homeassistant/components/life360/.translations/fr.json +++ b/homeassistant/components/life360/.translations/fr.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Informations d'identification invalides", "invalid_username": "Nom d'utilisateur invalide", + "unexpected": "Erreur inattendue lors de la communication avec le serveur Life360", "user_already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" }, "step": { diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index c03ad0f7e1f6a6..1e962142373f89 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -5,7 +5,7 @@ "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" }, "create_entry": { - "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 Life360]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "error": { "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", @@ -19,7 +19,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 Life360]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", "title": "Life360" } }, diff --git a/homeassistant/components/light/.translations/bg.json b/homeassistant/components/light/.translations/bg.json new file mode 100644 index 00000000000000..533ba76b6a7613 --- /dev/null +++ b/homeassistant/components/light/.translations/bg.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u041f\u0440\u0435\u0432\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b.", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json index 5017af8e576f64..c9b727088ab180 100644 --- a/homeassistant/components/light/.translations/ca.json +++ b/homeassistant/components/light/.translations/ca.json @@ -10,8 +10,8 @@ "is_on": "{name} est\u00e0 enc\u00e8s" }, "trigger_type": { - "turn_off": "{name} apagat", - "turn_on": "{name} enc\u00e8s" + "turned_off": "{name} apagat", + "turned_on": "{name} enc\u00e8s" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json index 7b266ba74125c9..4ea4a94014ea83 100644 --- a/homeassistant/components/light/.translations/da.json +++ b/homeassistant/components/light/.translations/da.json @@ -1,8 +1,8 @@ { "device_automation": { "trigger_type": { - "turn_off": "{name} slukket", - "turn_on": "{name} t\u00e6ndt" + "turned_off": "{name} slukket", + "turned_on": "{name} t\u00e6ndt" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json index fcfc2773ed8941..2fe1c6b42dcd42 100644 --- a/homeassistant/components/light/.translations/de.json +++ b/homeassistant/components/light/.translations/de.json @@ -1,8 +1,8 @@ { "device_automation": { "trigger_type": { - "turn_off": "{name} ausgeschaltet", - "turn_on": "{name} eingeschaltet" + "turned_off": "{name} ausgeschaltet", + "turned_on": "{name} eingeschaltet" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/en.json b/homeassistant/components/light/.translations/en.json index 60ccbd99348d7d..3f37de5331e3ea 100644 --- a/homeassistant/components/light/.translations/en.json +++ b/homeassistant/components/light/.translations/en.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} is on" }, "trigger_type": { - "turn_off": "{entity_name} turned off", - "turn_on": "{entity_name} turned on" + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json index b56875453dd67e..6bf91651d2e148 100644 --- a/homeassistant/components/light/.translations/es.json +++ b/homeassistant/components/light/.translations/es.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "toggle": "Alternar {entity_name}", + "turn_off": "Apagar {entity_name}", + "turn_on": "Encender {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagada", + "is_on": "{entity_name} est\u00e1 encendida" + }, "trigger_type": { - "turn_off": "{nombre} desactivado", - "turn_on": "{nombre} activado" + "turned_off": "{entity_name} apagada", + "turned_on": "{entity_name} encendida" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json index 00d03b12d0130a..fd30e9317180ec 100644 --- a/homeassistant/components/light/.translations/fr.json +++ b/homeassistant/components/light/.translations/fr.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "toggle": "Basculer {entity_name}", + "turn_off": "\u00c9teindre {entity_name}", + "turn_on": "Allumer {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est \u00e9teint", + "is_on": "{entity_name} est allum\u00e9" + }, "trigger_type": { - "turn_off": "{name} d\u00e9sactiv\u00e9", - "turn_on": "{name} activ\u00e9" + "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} activ\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/it.json b/homeassistant/components/light/.translations/it.json index 85a117f0b53f2b..2f4d2ca121f512 100644 --- a/homeassistant/components/light/.translations/it.json +++ b/homeassistant/components/light/.translations/it.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \u00e8 attivo" }, "trigger_type": { - "turn_off": "{entity_name} disattivato", - "turn_on": "{entity_name} attivato" + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json index 7277ef5900f02a..e055f67421ef53 100644 --- a/homeassistant/components/light/.translations/ko.json +++ b/homeassistant/components/light/.translations/ko.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" }, "trigger_type": { - "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/lb.json b/homeassistant/components/light/.translations/lb.json new file mode 100644 index 00000000000000..a7f807e8dcda54 --- /dev/null +++ b/homeassistant/components/light/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} \u00ebmschalten", + "turn_off": "{entity_name} ausschalten", + "turn_on": "{entity_name} uschalten" + }, + "condition_type": { + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un" + }, + "trigger_type": { + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/nl.json b/homeassistant/components/light/.translations/nl.json index 546fea78b6d5e9..63954ca83a9fa1 100644 --- a/homeassistant/components/light/.translations/nl.json +++ b/homeassistant/components/light/.translations/nl.json @@ -2,16 +2,16 @@ "device_automation": { "action_type": { "toggle": "Omschakelen {naam}", - "turn_off": "{Naam} uitschakelen", - "turn_on": "{Naam} inschakelen" + "turn_off": "{entity_name} uitschakelen", + "turn_on": "{entity_name} inschakelen" }, "condition_type": { "is_off": "{name} is uitgeschakeld", "is_on": "{name} is ingeschakeld" }, "trigger_type": { - "turn_off": "{name} is uitgeschakeld", - "turn_on": "{name} is ingeschakeld" + "turned_off": "{name} is uitgeschakeld", + "turned_on": "{name} is ingeschakeld" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/no.json b/homeassistant/components/light/.translations/no.json index 39c391eff3356a..008123739d9138 100644 --- a/homeassistant/components/light/.translations/no.json +++ b/homeassistant/components/light/.translations/no.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "toggle": "Veksle {entity_name}", + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er av", + "is_on": "{entity_name} er p\u00e5" + }, "trigger_type": { - "turn_off": "{name} sl\u00e5tt av", - "turn_on": "{name} sl\u00e5tt p\u00e5" + "turned_off": "{name} sl\u00e5tt av", + "turned_on": "{name} sl\u00e5tt p\u00e5" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 9debeaf4169d4c..22a93909578608 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -10,8 +10,8 @@ "is_on": "(entity_name} jest w\u0142\u0105czony." }, "trigger_type": { - "turn_off": "{nazwa} wy\u0142\u0105czone", - "turn_on": "{name} w\u0142\u0105czone" + "turned_off": "{nazwa} wy\u0142\u0105czone", + "turned_on": "{name} w\u0142\u0105czone" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json index 3154e17a509c7f..ba9339c1a944e8 100644 --- a/homeassistant/components/light/.translations/ru.json +++ b/homeassistant/components/light/.translations/ru.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/sl.json b/homeassistant/components/light/.translations/sl.json index 68e770e88731b8..bef4f1583b6b19 100644 --- a/homeassistant/components/light/.translations/sl.json +++ b/homeassistant/components/light/.translations/sl.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "toggle": "Preklopite {entity_name}", + "turn_off": "Izklopite {entity_name}", + "turn_on": "Vklopite {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen" + }, "trigger_type": { - "turn_off": "{name} izklopljeno", - "turn_on": "{name} vklopljeno" + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/zh-Hant.json b/homeassistant/components/light/.translations/zh-Hant.json index 269715b7cc33fe..5ac06129463b35 100644 --- a/homeassistant/components/light/.translations/zh-Hant.json +++ b/homeassistant/components/light/.translations/zh-Hant.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "\u5207\u63db {name}", - "turn_off": "\u95dc\u9589 {name}", - "turn_on": "\u958b\u555f {name}" + "toggle": "\u5207\u63db {entity_name}", + "turn_off": "\u95dc\u9589 {entity_name}", + "turn_on": "\u958b\u555f {entity_name}" }, "condition_type": { - "is_off": "{name} \u5df2\u95dc\u9589", - "is_on": "{name} \u5df2\u958b\u555f" + "is_off": "{entity_name} \u5df2\u95dc\u9589", + "is_on": "{entity_name} \u5df2\u958b\u555f" }, "trigger_type": { - "turn_off": "\u7531 {name} \u95dc\u9589", - "turn_on": "\u7531 {name} \u958b\u555f" + "turned_off": "{entity_name} \u5df2\u95dc\u9589", + "turned_on": "{entity_name} \u5df2\u958b\u555f" } } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/bg.json b/homeassistant/components/linky/.translations/bg.json new file mode 100644 index 00000000000000..6eeb898ee1ffc8 --- /dev/null +++ b/homeassistant/components/linky/.translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b" + }, + "error": { + "access": "\u041d\u044f\u043c\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e Enedis.fr, \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0438", + "enedis": "Enedis.fr \u043e\u0442\u0433\u043e\u0432\u043e\u0440\u0438 \u0441 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430: \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e (\u043e\u0431\u0438\u043a\u043d\u043e\u0432\u0435\u043d\u043e \u043d\u0435 \u043c\u0435\u0436\u0434\u0443 23:00 \u0438 02:00)", + "username_exists": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b", + "wrong_login": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0432\u043b\u0438\u0437\u0430\u043d\u0435: \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0438\u043c\u0435\u0439\u043b\u0430 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0441\u0438" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "E-mail" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0438\u043d\u0434\u0435\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u0441\u0438 \u0434\u0430\u043d\u043d\u0438", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/es.json b/homeassistant/components/linky/.translations/es.json new file mode 100644 index 00000000000000..511f3c9d8e56f8 --- /dev/null +++ b/homeassistant/components/linky/.translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Cuenta ya configurada" + }, + "error": { + "access": "No se pudo acceder a Enedis.fr, compruebe su conexi\u00f3n a Internet", + "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11:00 y las 2 de la ma\u00f1ana)", + "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 23:00 y las 02:00 horas).", + "username_exists": "Cuenta ya configurada", + "wrong_login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "description": "Introduzca sus credenciales", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/lb.json b/homeassistant/components/linky/.translations/lb.json new file mode 100644 index 00000000000000..cd3c7152c89e7d --- /dev/null +++ b/homeassistant/components/linky/.translations/lb.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Kont ass scho konfigur\u00e9iert" + }, + "error": { + "access": "Keng Verbindung zu Enedis.fr, iwwerpr\u00e9ift d'Internet Verbindung", + "enedis": "Enedis.fr huet mat engem Feeler ge\u00e4ntwert: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)", + "unknown": "Onbekannte Feeler: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)", + "username_exists": "Kont ass scho konfigur\u00e9iert", + "wrong_login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + }, + "description": "F\u00ebllt \u00e4r Login Informatiounen aus", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/no.json b/homeassistant/components/linky/.translations/no.json new file mode 100644 index 00000000000000..c43f434562c152 --- /dev/null +++ b/homeassistant/components/linky/.translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Kontoen er allerede konfigurert" + }, + "error": { + "access": "Kunne ikke f\u00e5 tilgang til Enedis.fr, vennligst sjekk internettforbindelsen din", + "enedis": "Enedis.fr svarte med en feil: vennligst pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)", + "unknown": "Ukjent feil: pr\u00f8v p\u00e5 nytt senere (vanligvis ikke mellom 23:00 og 02:00)", + "username_exists": "Kontoen er allerede konfigurert", + "wrong_login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-post" + }, + "description": "Skriv inn legitimasjonen din", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/ru.json b/homeassistant/components/logi_circle/.translations/ru.json index 1e9c089828fe92..40c7c8853daeb4 100644 --- a/homeassistant/components/logi_circle/.translations/ru.json +++ b/homeassistant/components/logi_circle/.translations/ru.json @@ -4,7 +4,7 @@ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "external_error": "\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.", "external_setup": "Logi Circle \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.", - "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Logi Circle \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/logi_circle/)." + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Logi Circle \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/met/.translations/es.json b/homeassistant/components/met/.translations/es.json index 7659ab4d2962a8..a475518bd851cb 100644 --- a/homeassistant/components/met/.translations/es.json +++ b/homeassistant/components/met/.translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "El nombre ya existe" + "name_exists": "La ubicaci\u00f3n ya existe" }, "step": { "user": { @@ -14,6 +14,7 @@ "description": "Instituto de meteorolog\u00eda", "title": "Ubicaci\u00f3n" } - } + }, + "title": "Met.no" } } \ No newline at end of file diff --git a/homeassistant/components/met/.translations/lb.json b/homeassistant/components/met/.translations/lb.json index 660f639d859718..9f91d37c23360c 100644 --- a/homeassistant/components/met/.translations/lb.json +++ b/homeassistant/components/met/.translations/lb.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Numm g\u00ebtt et schonn" + "name_exists": "Standuert g\u00ebtt et schonn" }, "step": { "user": { diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index 1c24acd96e427a..ac88ed224edb26 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -4,7 +4,7 @@ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\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.", - "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Nest \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Nest \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430", diff --git a/homeassistant/components/plaato/.translations/es.json b/homeassistant/components/plaato/.translations/es.json index e52a80be986370..ecb061e91c9c44 100644 --- a/homeassistant/components/plaato/.translations/es.json +++ b/homeassistant/components/plaato/.translations/es.json @@ -12,6 +12,7 @@ "description": "\u00bfEst\u00e1s seguro de que quieres configurar el Airlock de Plaato?", "title": "Configurar el webhook de Plaato" } - } + }, + "title": "Plaato Airlock" } } \ No newline at end of file diff --git a/homeassistant/components/point/.translations/es.json b/homeassistant/components/point/.translations/es.json index 33b6b1d38271a9..9a94e54dd5fa30 100644 --- a/homeassistant/components/point/.translations/es.json +++ b/homeassistant/components/point/.translations/es.json @@ -27,6 +27,6 @@ "title": "Proveedor de autenticaci\u00f3n" } }, - "title": "Point de Minut" + "title": "Minut Point" } } \ No newline at end of file diff --git a/homeassistant/components/point/.translations/ru.json b/homeassistant/components/point/.translations/ru.json index 2a10b234e99e21..487510969481f6 100644 --- a/homeassistant/components/point/.translations/ru.json +++ b/homeassistant/components/point/.translations/ru.json @@ -5,7 +5,7 @@ "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.", "external_setup": "Point \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.", - "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Point \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/point/)." + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Point \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/point/)." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/ps4/.translations/fr.json b/homeassistant/components/ps4/.translations/fr.json index 03baf0c032e3a3..991222d45be78b 100644 --- a/homeassistant/components/ps4/.translations/fr.json +++ b/homeassistant/components/ps4/.translations/fr.json @@ -4,8 +4,8 @@ "credential_error": "Erreur lors de l'extraction des informations d'identification.", "devices_configured": "Tous les p\u00e9riph\u00e9riques trouv\u00e9s sont d\u00e9j\u00e0 configur\u00e9s.", "no_devices_found": "Aucun appareil PlayStation 4 trouv\u00e9 sur le r\u00e9seau.", - "port_987_bind_error": "Impossible de se connecter au port 997.", - "port_997_bind_error": "Impossible de se connecter au port 997." + "port_987_bind_error": "Impossible de se connecter au port 997. Reportez-vous \u00e0 la [documentation] (https://www.home-assistant.io/components/ps4/) pour plus d'informations.", + "port_997_bind_error": "Impossible de se connecter au port 997. Reportez-vous \u00e0 la [documentation] (https://www.home-assistant.io/components/ps4/) pour plus d'informations." }, "error": { "credential_timeout": "Le service d'informations d'identification a expir\u00e9. Appuyez sur soumettre pour red\u00e9marrer.", diff --git a/homeassistant/components/ps4/.translations/lb.json b/homeassistant/components/ps4/.translations/lb.json index 17757cb9d20317..0986b0e0240afb 100644 --- a/homeassistant/components/ps4/.translations/lb.json +++ b/homeassistant/components/ps4/.translations/lb.json @@ -4,8 +4,8 @@ "credential_error": "Feeler beim ausliesen vun den Umeldungs Informatiounen.", "devices_configured": "All Apparater sinn schonn konfigur\u00e9iert", "no_devices_found": "Keng Playstation 4 am Netzwierk fonnt.", - "port_987_bind_error": "Konnt sech net mam Port 987 verbannen.", - "port_997_bind_error": "Konnt sech net mam Port 997 verbannen." + "port_987_bind_error": "Konnt sech net mam Port 987 verbannen. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen.", + "port_997_bind_error": "Konnt sech net mam Port 997 verbannen. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen." }, "error": { "credential_timeout": "Z\u00e4it Iwwerschreidung beim Service vun den Umeldungsinformatiounen. Dr\u00e9ck op ofsch\u00e9cke fir nach emol ze starten.", @@ -25,7 +25,7 @@ "name": "Numm", "region": "Regioun" }, - "description": "Gitt \u00e4r Playstation 4 Informatiounen an. Fir 'PIN', gitt an d'Astellunge vun der Playstation 4 Konsole. Dann op 'Mobile App Verbindungs Astellungen' a wielt \"Apparat dob\u00e4isetzen' aus. Gitt de PIN an deen ugewise g\u00ebtt.", + "description": "Gitt \u00e4r Playstation 4 Informatiounen an. Fir 'PIN', gitt an d'Astellunge vun der Playstation 4 Konsole. Dann op 'Mobile App Verbindungs Astellungen' a wielt \"Apparat dob\u00e4isetzen' aus. Gitt de PIN an deen ugewise g\u00ebtt. Liest [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/solaredge/.translations/bg.json b/homeassistant/components/solaredge/.translations/bg.json new file mode 100644 index 00000000000000..72f1ad2a4c758f --- /dev/null +++ b/homeassistant/components/solaredge/.translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "site_exists": "\u0422\u043e\u0432\u0430 site_id \u0432\u0435\u0447\u0435 \u0435 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u043e" + }, + "error": { + "site_exists": "\u0422\u043e\u0432\u0430 site_id \u0432\u0435\u0447\u0435 \u0435 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u043e" + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447\u0430 \u0437\u0430 \u0442\u043e\u0437\u0438 \u0441\u0430\u0439\u0442", + "name": "\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435\u0442\u043e \u043d\u0430 \u0442\u0430\u0437\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f", + "site_id": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u044a\u0440\u044a\u0442 site-id \u043d\u0430 SolarEdge" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (API) \u0437\u0430 \u0442\u0430\u0437\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/es.json b/homeassistant/components/solaredge/.translations/es.json new file mode 100644 index 00000000000000..8708729bf4aebd --- /dev/null +++ b/homeassistant/components/solaredge/.translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Este site_id ya est\u00e1 configurado" + }, + "error": { + "site_exists": "Este site_id ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "api_key": "La clave de la API para este sitio", + "name": "El nombre de esta instalaci\u00f3n", + "site_id": "La identificaci\u00f3n del sitio de SolarEdge" + }, + "title": "Definir los par\u00e1metros de la API para esta instalaci\u00f3n" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/fr.json b/homeassistant/components/solaredge/.translations/fr.json new file mode 100644 index 00000000000000..201e3ff49c6105 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "api_key": "La cl\u00e9 API pour ce site", + "name": "Le nom de cette installation", + "site_id": "L'identifiant de site SolarEdge" + }, + "title": "D\u00e9finir les param\u00e8tres de l'API pour cette installation" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/lb.json b/homeassistant/components/solaredge/.translations/lb.json new file mode 100644 index 00000000000000..afc558ca80cd84 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert" + }, + "error": { + "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel fir d\u00ebsen Site", + "name": "Numm vun d\u00ebser Installatioun", + "site_id": "SolarEdge site-ID" + }, + "title": "API Parameter fir d\u00ebs Installatioun d\u00e9fin\u00e9ieren" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/no.json b/homeassistant/components/solaredge/.translations/no.json new file mode 100644 index 00000000000000..ad7cb55316b696 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Denne site_id er allerede konfigurert" + }, + "error": { + "site_exists": "Denne site_id er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkelen for dette nettstedet", + "name": "Navnet p\u00e5 denne installasjonen", + "site_id": "SolarEdge nettsted-id" + }, + "title": "Definer API-parametrene for denne installasjonen" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/sl.json b/homeassistant/components/solaredge/.translations/sl.json new file mode 100644 index 00000000000000..ebfefe40b0e54d --- /dev/null +++ b/homeassistant/components/solaredge/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Ta site_id je \u017ee nastavljen" + }, + "error": { + "site_exists": "Ta site_id je \u017ee nastavljen" + }, + "step": { + "user": { + "data": { + "api_key": "API klju\u010d za to stran", + "name": "Ime te namestitve", + "site_id": "SolarEdge site-ID" + }, + "title": "Dolo\u010dite parametre API za to namestitev" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/zh-Hant.json b/homeassistant/components/solaredge/.translations/zh-Hant.json new file mode 100644 index 00000000000000..698c28d99bf72a --- /dev/null +++ b/homeassistant/components/solaredge/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "name": "\u5b89\u88dd\u540d\u7a31", + "site_id": "SolarEdge site-id" + }, + "title": "\u8a2d\u5b9a API \u53c3\u6578" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/bg.json b/homeassistant/components/switch/.translations/bg.json new file mode 100644 index 00000000000000..efccc652d5beb7 --- /dev/null +++ b/homeassistant/components/switch/.translations/bg.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u041f\u0440\u0435\u0432\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" + }, + "condition_type": { + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + }, + "trigger_type": { + "turned_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "turned_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ca.json b/homeassistant/components/switch/.translations/ca.json index c97565ddfe6a7e..dbf5e152656497 100644 --- a/homeassistant/components/switch/.translations/ca.json +++ b/homeassistant/components/switch/.translations/ca.json @@ -6,12 +6,14 @@ "turn_on": "Activa {entity_name}" }, "condition_type": { + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s", "turn_off": "{entity_name} desactivat", "turn_on": "{entity_name} activat" }, "trigger_type": { - "turn_off": "{entity_name} desactivat", - "turn_on": "{entity_name} activat" + "turned_off": "{entity_name} desactivat", + "turned_on": "{entity_name} activat" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/en.json b/homeassistant/components/switch/.translations/en.json index 5be333cbf13bf7..391a071cb8f4fb 100644 --- a/homeassistant/components/switch/.translations/en.json +++ b/homeassistant/components/switch/.translations/en.json @@ -6,12 +6,14 @@ "turn_on": "Turn on {entity_name}" }, "condition_type": { + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on", "turn_off": "{entity_name} turned off", "turn_on": "{entity_name} turned on" }, "trigger_type": { - "turn_off": "{entity_name} turned off", - "turn_on": "{entity_name} turned on" + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/es.json b/homeassistant/components/switch/.translations/es.json new file mode 100644 index 00000000000000..24dbc2cdc1fff0 --- /dev/null +++ b/homeassistant/components/switch/.translations/es.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Alternar {entity_name}", + "turn_off": "Apagar {entity_name}", + "turn_on": "Encender {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagada", + "is_on": "{entity_name} est\u00e1 encendida", + "turn_off": "{entity_name} apagado", + "turn_on": "{entity_name} encendido" + }, + "trigger_type": { + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/fr.json b/homeassistant/components/switch/.translations/fr.json new file mode 100644 index 00000000000000..4775d62bce3758 --- /dev/null +++ b/homeassistant/components/switch/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Basculer {entity_name}", + "turn_off": "\u00c9teindre {entity_name}", + "turn_on": "Allumer {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} \u00e9teint", + "turn_on": "{entity_name} allum\u00e9" + }, + "trigger_type": { + "turned_off": "{entity_name} \u00e9teint", + "turned_on": "{entity_name} allum\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/it.json b/homeassistant/components/switch/.translations/it.json index c51ce8c6ee5934..254c09380c1d1d 100644 --- a/homeassistant/components/switch/.translations/it.json +++ b/homeassistant/components/switch/.translations/it.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} attivato" }, "trigger_type": { - "turn_off": "{entity_name} disattivato", - "turn_on": "{entity_name} attivato" + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ko.json b/homeassistant/components/switch/.translations/ko.json index 2156ea04e01d68..02c303f932987b 100644 --- a/homeassistant/components/switch/.translations/ko.json +++ b/homeassistant/components/switch/.translations/ko.json @@ -6,12 +6,14 @@ "turn_on": "{entity_name} \ucf1c\uae30" }, "condition_type": { + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" }, "trigger_type": { - "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/lb.json b/homeassistant/components/switch/.translations/lb.json new file mode 100644 index 00000000000000..8e974a0a8de137 --- /dev/null +++ b/homeassistant/components/switch/.translations/lb.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} \u00ebmschalten", + "turn_off": "{entity_name} ausschalten", + "turn_on": "{entity_name} uschalten" + }, + "condition_type": { + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un", + "turn_off": "{entity_name} gouf ausgeschalt", + "turn_on": "{entity_name} gouf ugeschalt" + }, + "trigger_type": { + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/nl.json b/homeassistant/components/switch/.translations/nl.json index 1d8355d21581bf..5e2aa6747a4c32 100644 --- a/homeassistant/components/switch/.translations/nl.json +++ b/homeassistant/components/switch/.translations/nl.json @@ -6,12 +6,14 @@ "turn_on": "Zet {entity_name} aan." }, "condition_type": { + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld", "turn_off": "{entity_name} uitgeschakeld", "turn_on": "{entity_name} ingeschakeld" }, "trigger_type": { - "turn_off": "{entity_name} uitgeschakeld", - "turn_on": "{entity_name} ingeschakeld" + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/no.json b/homeassistant/components/switch/.translations/no.json new file mode 100644 index 00000000000000..adc128991c5efc --- /dev/null +++ b/homeassistant/components/switch/.translations/no.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Veksle {entity_name}", + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} sl\u00e5tt av", + "turn_on": "{entity_name} sl\u00e5tt p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index f564d1424eac61..c63799bf783923 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} w\u0142\u0105czone" }, "trigger_type": { - "turn_off": "{entity_name} wy\u0142\u0105czone", - "turn_on": "{entity_name} w\u0142\u0105czone" + "turned_off": "{entity_name} wy\u0142\u0105czone", + "turned_on": "{entity_name} w\u0142\u0105czone" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index 1b0658cd174102..45a941b665de03 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/sl.json b/homeassistant/components/switch/.translations/sl.json new file mode 100644 index 00000000000000..89423e071fdc8b --- /dev/null +++ b/homeassistant/components/switch/.translations/sl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Preklopite {entity_name}", + "turn_off": "Izklopite {entity_name}", + "turn_on": "Vklopite {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} izklopljen", + "turn_on": "{entity_name} vklopljen" + }, + "trigger_type": { + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/zh-Hant.json b/homeassistant/components/switch/.translations/zh-Hant.json new file mode 100644 index 00000000000000..c1a67897b16c84 --- /dev/null +++ b/homeassistant/components/switch/.translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u5207\u63db {entity_name}", + "turn_off": "\u95dc\u9589 {entity_name}", + "turn_on": "\u958b\u555f {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} \u5df2\u95dc\u9589", + "turn_on": "{entity_name} \u5df2\u958b\u555f" + }, + "trigger_type": { + "turned_off": "{entity_name} \u5df2\u95dc\u9589", + "turned_on": "{entity_name} \u5df2\u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json index b0313a1eee357a..677e0389d45b71 100644 --- a/homeassistant/components/tellduslive/.translations/es.json +++ b/homeassistant/components/tellduslive/.translations/es.json @@ -18,6 +18,7 @@ "data": { "host": "Host" }, + "description": "Vac\u00edo", "title": "Elige el punto final." } }, diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 012aa65187c28b..0eddbe2a151d79 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -4,7 +4,7 @@ "client_id": "Client ID \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "client_secret": "Client secret \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", - "no_app": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Toon \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/toon/).", + "no_app": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Toon \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { diff --git a/homeassistant/components/traccar/.translations/es.json b/homeassistant/components/traccar/.translations/es.json index ab8c0e70cd42e4..dedaf02971c79f 100644 --- a/homeassistant/components/traccar/.translations/es.json +++ b/homeassistant/components/traccar/.translations/es.json @@ -1,7 +1,18 @@ { "config": { "abort": { - "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Traccar." - } + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Traccar.", + "one_instance_allowed": "S\u00f3lo se necesita una \u00fanica instancia." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, necesitar\u00e1 configurar la funci\u00f3n de webhook en Traccar.\n\nUtilice la siguiente url: ``{webhook_url}``\n\nConsulte la [documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1 seguro de querer configurar Traccar?", + "title": "Configurar Traccar" + } + }, + "title": "Traccar" } } \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/fr.json b/homeassistant/components/traccar/.translations/fr.json new file mode 100644 index 00000000000000..0948a31739fcea --- /dev/null +++ b/homeassistant/components/traccar/.translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Votre instance de Home Assistant doit \u00eatre accessible depuis Internet pour recevoir les messages de Traccar.", + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "create_entry": { + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer la fonction Webhook dans Traccar. \n\n Utilisez l'URL suivante: ` {webhook_url} ` \n\n Voir [la documentation] ( {docs_url} ) pour plus de d\u00e9tails." + }, + "step": { + "user": { + "description": "\u00cates-vous s\u00fbr de vouloir configurer Traccar?", + "title": "Configurer Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/lb.json b/homeassistant/components/traccar/.translations/lb.json new file mode 100644 index 00000000000000..8808d85a1d6db2 --- /dev/null +++ b/homeassistant/components/traccar/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Traccar Noriichten z'empf\u00e4nken.", + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "create_entry": { + "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Traccar ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider Informatiounen." + }, + "step": { + "user": { + "description": "S\u00e9cher fir Traccar anzeriichten?", + "title": "Traccar ariichten" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/es.json b/homeassistant/components/twentemilieu/.translations/es.json index 02dcb71f54e5ff..60a412684f775b 100644 --- a/homeassistant/components/twentemilieu/.translations/es.json +++ b/homeassistant/components/twentemilieu/.translations/es.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "address_exists": "Direcci\u00f3n ya configurada." + }, "error": { - "connection_error": "No se conect\u00f3." + "connection_error": "No se conect\u00f3.", + "invalid_address": "Direcci\u00f3n no encontrada en el \u00e1rea de servicio de Twente Milieu." }, "step": { "user": { @@ -9,8 +13,11 @@ "house_letter": "Letra de la casa/adicional", "house_number": "N\u00famero de casa", "post_code": "C\u00f3digo postal" - } + }, + "description": "Configure Twente Milieu proporcionando informaci\u00f3n sobre la recolecci\u00f3n de residuos en su direcci\u00f3n.", + "title": "Twente Milieu" } - } + }, + "title": "Twente Milieu" } } \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/fr.json b/homeassistant/components/twentemilieu/.translations/fr.json new file mode 100644 index 00000000000000..0321a6b73cec35 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adresse d\u00e9j\u00e0 configur\u00e9e." + }, + "error": { + "connection_error": "\u00c9chec de connexion.", + "invalid_address": "Adresse introuvable dans la zone de service de Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Lettre de la maison / suppl\u00e9mentaire", + "house_number": "Num\u00e9ro de maison", + "post_code": "Code postal" + }, + "description": "Configurez Twente Milieu en fournissant des informations sur la collecte des d\u00e9chets sur votre adresse.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/lb.json b/homeassistant/components/twentemilieu/.translations/lb.json new file mode 100644 index 00000000000000..0b07c5003ef2ff --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "address_exists": "Adresse ass scho ageriicht." + }, + "error": { + "connection_error": "Feeler beim verbannen." + }, + "step": { + "user": { + "data": { + "house_letter": "Haus Buschtaf/zous\u00e4tzlech", + "house_number": "Haus Nummer", + "post_code": "Postleitzuel" + }, + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 8a8d8b11f57661..3741b035d7a9d6 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -32,6 +32,12 @@ "track_devices": "Segueix dispositius de la xarxa (dispositius Ubiquiti)", "track_wired_clients": "Inclou clients de xarxa per cable" } + }, + "init": { + "data": { + "one": "un", + "other": "altre" + } } } } diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 8b0eb56203700d..0539f5607b4c36 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -29,8 +29,15 @@ "data": { "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado", "track_clients": "Seguimiento de los clientes de red", + "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de red cableada" } + }, + "init": { + "data": { + "one": "uno", + "other": "otro" + } } } } diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index 9e567fcc394a75..8c2526f8a1565a 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -22,5 +22,17 @@ } }, "title": "Contr\u00f4leur UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Temps en secondes depuis la derni\u00e8re vue avant de consid\u00e9rer comme absent", + "track_clients": "Suivre les clients du r\u00e9seau", + "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)", + "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json index f907364327c958..518f0066534411 100644 --- a/homeassistant/components/unifi/.translations/nl.json +++ b/homeassistant/components/unifi/.translations/nl.json @@ -32,6 +32,12 @@ "track_devices": "Netwerkapparaten volgen (Ubiquiti-apparaten)", "track_wired_clients": "Inclusief bedrade netwerkcli\u00ebnten" } + }, + "init": { + "data": { + "one": "Leeg", + "other": "Leeg" + } } } } diff --git a/homeassistant/components/upnp/.translations/ca.json b/homeassistant/components/upnp/.translations/ca.json index 161b5d85599147..28ad9ce954d995 100644 --- a/homeassistant/components/upnp/.translations/ca.json +++ b/homeassistant/components/upnp/.translations/ca.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Activa, com a m\u00ednim, els sensors o l'assignaci\u00f3 de ports", "single_instance_allowed": "Nom\u00e9s cal una sola configuraci\u00f3 de UPnP/IGD." }, + "error": { + "one": "un", + "other": "altre" + }, "step": { "confirm": { "description": "Vols configurar UPnP/IGD?", diff --git a/homeassistant/components/velbus/.translations/es.json b/homeassistant/components/velbus/.translations/es.json index e60ef7b4c676f1..1e1e8897c30155 100644 --- a/homeassistant/components/velbus/.translations/es.json +++ b/homeassistant/components/velbus/.translations/es.json @@ -3,12 +3,19 @@ "abort": { "port_exists": "Este puerto ya est\u00e1 configurado" }, + "error": { + "connection_failed": "La conexi\u00f3n velbus fall\u00f3", + "port_exists": "Este puerto ya est\u00e1 configurado" + }, "step": { "user": { "data": { + "name": "El nombre de esta conexi\u00f3n velbus", "port": "Cadena de conexi\u00f3n" - } + }, + "title": "Definir el tipo de conexi\u00f3n velbus" } - } + }, + "title": "Interfaz Velbus" } } \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/fr.json b/homeassistant/components/velbus/.translations/fr.json new file mode 100644 index 00000000000000..8d93adbf4a92dd --- /dev/null +++ b/homeassistant/components/velbus/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Ce port est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "connection_failed": "La connexion velbus a \u00e9chou\u00e9", + "port_exists": "Ce port est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "name": "Le nom pour cette connexion velbus", + "port": "Cha\u00eene de connexion" + }, + "title": "D\u00e9finir le type de connexion velbus" + } + }, + "title": "Interface Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/lb.json b/homeassistant/components/velbus/.translations/lb.json new file mode 100644 index 00000000000000..89e0bd818d277c --- /dev/null +++ b/homeassistant/components/velbus/.translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "port_exists": "D\u00ebse Port ass scho konfigur\u00e9iert" + }, + "error": { + "connection_failed": "Feeler bei der velbus Verbindung", + "port_exists": "D\u00ebse Port ass scho konfigur\u00e9iert" + }, + "step": { + "user": { + "data": { + "name": "Numm fir d\u00ebs velbus Verbindung", + "port": "Verbindungs zeeche-folleg" + } + } + }, + "title": "Velbus Interface" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/es.json b/homeassistant/components/vesync/.translations/es.json index 99611c5f9bfa79..856dc77a52c48d 100644 --- a/homeassistant/components/vesync/.translations/es.json +++ b/homeassistant/components/vesync/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_setup": "Solo se permite una instancia de Vesync" + }, "error": { "invalid_login": "Nombre de usuario o contrase\u00f1a no v\u00e1lidos" }, @@ -11,6 +14,7 @@ }, "title": "Introduzca el nombre de usuario y la contrase\u00f1a" } - } + }, + "title": "VeSync" } } \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/lb.json b/homeassistant/components/vesync/.translations/lb.json new file mode 100644 index 00000000000000..cfccd8b1dbb49a --- /dev/null +++ b/homeassistant/components/vesync/.translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "N\u00ebmmen eng eenzeg Instanz vu Vesync ass erlaabt." + }, + "error": { + "invalid_login": "Ong\u00ebltege Benotzernumm oder Passwuert" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail Adresse" + }, + "title": "Benotznumm a Passwuert aginn" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json index a96f8cff523fdd..2f2fdbe9b3f742 100644 --- a/homeassistant/components/withings/.translations/ca.json +++ b/homeassistant/components/withings/.translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Necessites configurar Withings abans de poder autenticar't-hi. Llegeix la documentaci\u00f3." + }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Withings per al perfil seleccionat." }, diff --git a/homeassistant/components/withings/.translations/en.json b/homeassistant/components/withings/.translations/en.json index 2b906dd80030fb..16ce491e776d02 100644 --- a/homeassistant/components/withings/.translations/en.json +++ b/homeassistant/components/withings/.translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation." + }, "create_entry": { "default": "Successfully authenticated with Withings for the selected profile." }, diff --git a/homeassistant/components/withings/.translations/ko.json b/homeassistant/components/withings/.translations/ko.json index 3c2f00ba4aef76..617964e0596aba 100644 --- a/homeassistant/components/withings/.translations/ko.json +++ b/homeassistant/components/withings/.translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Withings \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Withings \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/withings/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + }, "create_entry": { "default": "\uc120\ud0dd\ud55c \ud504\ub85c\ud544\ub85c Withings \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json new file mode 100644 index 00000000000000..9015f4908308d1 --- /dev/null +++ b/homeassistant/components/withings/.translations/lb.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "title": "Benotzer Profil." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json index 1729879a154d8d..3776621bec208e 100644 --- a/homeassistant/components/withings/.translations/nl.json +++ b/homeassistant/components/withings/.translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "U moet Withings configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de documentatie te lezen]" + }, "create_entry": { "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." }, diff --git a/homeassistant/components/withings/.translations/ru.json b/homeassistant/components/withings/.translations/ru.json index d9d5e14208f05b..c6c621fbdf33ee 100644 --- a/homeassistant/components/withings/.translations/ru.json +++ b/homeassistant/components/withings/.translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Withings \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, From fccbaf38050a3678d9dbf5369ffe8382e2c0700e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 19 Sep 2019 00:32:15 +0000 Subject: [PATCH 0349/3953] [ci skip] Translation update --- .../ambiclimate/.translations/ru.json | 2 +- .../cert_expiry/.translations/lb.json | 7 +++++++ .../components/deconz/.translations/lb.json | 11 ++++++++++ .../components/life360/.translations/ru.json | 4 ++-- .../components/light/.translations/ca.json | 6 ++---- .../components/light/.translations/da.json | 4 ++-- .../components/light/.translations/de.json | 4 ++-- .../components/light/.translations/en.json | 2 -- .../components/light/.translations/es.json | 6 ++---- .../components/light/.translations/fr.json | 4 ++-- .../components/light/.translations/it.json | 4 ++-- .../components/light/.translations/ko.json | 4 ++-- .../components/light/.translations/lb.json | 4 ++-- .../components/light/.translations/nl.json | 8 +++---- .../components/light/.translations/no.json | 4 ++-- .../components/light/.translations/pl.json | 4 ++-- .../components/light/.translations/ru.json | 4 ++-- .../components/light/.translations/sl.json | 4 ++-- .../light/.translations/zh-Hant.json | 4 ++-- .../components/linky/.translations/lb.json | 3 +++ .../logi_circle/.translations/ru.json | 2 +- .../components/nest/.translations/ru.json | 2 +- .../components/point/.translations/ru.json | 2 +- .../solaredge/.translations/lb.json | 6 ++++-- .../components/switch/.translations/bg.json | 4 ++-- .../components/switch/.translations/ca.json | 6 ++---- .../components/switch/.translations/en.json | 2 -- .../components/switch/.translations/es.json | 2 -- .../components/switch/.translations/fr.json | 4 ++-- .../components/switch/.translations/it.json | 4 ++-- .../components/switch/.translations/ko.json | 6 ++++-- .../components/switch/.translations/lb.json | 6 ++++-- .../components/switch/.translations/nl.json | 6 ++++-- .../components/switch/.translations/no.json | 4 ++-- .../components/switch/.translations/pl.json | 4 ++-- .../components/switch/.translations/ru.json | 4 ++-- .../components/switch/.translations/sl.json | 4 ++-- .../switch/.translations/zh-Hant.json | 4 ++-- .../components/toon/.translations/ru.json | 2 +- .../components/traccar/.translations/lb.json | 18 ++++++++++++++++ .../twentemilieu/.translations/lb.json | 21 +++++++++++++++++++ .../components/unifi/.translations/nl.json | 6 ++++++ .../components/velbus/.translations/lb.json | 20 ++++++++++++++++++ .../components/vesync/.translations/lb.json | 3 +++ .../components/withings/.translations/ca.json | 3 +++ .../components/withings/.translations/ko.json | 3 +++ .../components/withings/.translations/lb.json | 3 ++- .../components/withings/.translations/nl.json | 3 +++ .../components/withings/.translations/ru.json | 3 +++ 49 files changed, 174 insertions(+), 76 deletions(-) create mode 100644 homeassistant/components/cert_expiry/.translations/lb.json create mode 100644 homeassistant/components/traccar/.translations/lb.json create mode 100644 homeassistant/components/twentemilieu/.translations/lb.json create mode 100644 homeassistant/components/velbus/.translations/lb.json diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json index 129579315a29de..a4300e1e5306c5 100644 --- a/homeassistant/components/ambiclimate/.translations/ru.json +++ b/homeassistant/components/ambiclimate/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "no_config": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/cert_expiry/.translations/lb.json b/homeassistant/components/cert_expiry/.translations/lb.json new file mode 100644 index 00000000000000..d6811728a22402 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/lb.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 41c75ec4aab242..c536e5771411f9 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -67,5 +67,16 @@ "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", "remote_gyro_activated": "Apparat ger\u00ebselt" } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", + "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben" + }, + "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index c03ad0f7e1f6a6..1e962142373f89 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -5,7 +5,7 @@ "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" }, "create_entry": { - "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 Life360]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "error": { "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", @@ -19,7 +19,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 Life360]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", "title": "Life360" } }, diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json index 4cdf3d9042cde2..c9b727088ab180 100644 --- a/homeassistant/components/light/.translations/ca.json +++ b/homeassistant/components/light/.translations/ca.json @@ -10,10 +10,8 @@ "is_on": "{name} est\u00e0 enc\u00e8s" }, "trigger_type": { - "turn_off": "{name} apagat", - "turn_on": "{name} enc\u00e8s", - "turned_off": "{entity_name} apagat", - "turned_on": "{entity_name} enc\u00e8s" + "turned_off": "{name} apagat", + "turned_on": "{name} enc\u00e8s" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json index 7b266ba74125c9..4ea4a94014ea83 100644 --- a/homeassistant/components/light/.translations/da.json +++ b/homeassistant/components/light/.translations/da.json @@ -1,8 +1,8 @@ { "device_automation": { "trigger_type": { - "turn_off": "{name} slukket", - "turn_on": "{name} t\u00e6ndt" + "turned_off": "{name} slukket", + "turned_on": "{name} t\u00e6ndt" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json index fcfc2773ed8941..2fe1c6b42dcd42 100644 --- a/homeassistant/components/light/.translations/de.json +++ b/homeassistant/components/light/.translations/de.json @@ -1,8 +1,8 @@ { "device_automation": { "trigger_type": { - "turn_off": "{name} ausgeschaltet", - "turn_on": "{name} eingeschaltet" + "turned_off": "{name} ausgeschaltet", + "turned_on": "{name} eingeschaltet" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/en.json b/homeassistant/components/light/.translations/en.json index 225d64be231ea6..3f37de5331e3ea 100644 --- a/homeassistant/components/light/.translations/en.json +++ b/homeassistant/components/light/.translations/en.json @@ -10,8 +10,6 @@ "is_on": "{entity_name} is on" }, "trigger_type": { - "turn_off": "{entity_name} turned off", - "turn_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on" } diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json index 533c4b3c0f3486..6bf91651d2e148 100644 --- a/homeassistant/components/light/.translations/es.json +++ b/homeassistant/components/light/.translations/es.json @@ -10,10 +10,8 @@ "is_on": "{entity_name} est\u00e1 encendida" }, "trigger_type": { - "turn_off": "{entity_name} apagada", - "turn_on": "{entity_name} encendida", - "turned_off": "{entity_name} apagado", - "turned_on": "{entity_name} encendido" + "turned_off": "{entity_name} apagada", + "turned_on": "{entity_name} encendida" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json index f0aabef2ae742d..fd30e9317180ec 100644 --- a/homeassistant/components/light/.translations/fr.json +++ b/homeassistant/components/light/.translations/fr.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} est allum\u00e9" }, "trigger_type": { - "turn_off": "{entity_name} d\u00e9sactiv\u00e9", - "turn_on": "{entity_name} activ\u00e9" + "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} activ\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/it.json b/homeassistant/components/light/.translations/it.json index 85a117f0b53f2b..2f4d2ca121f512 100644 --- a/homeassistant/components/light/.translations/it.json +++ b/homeassistant/components/light/.translations/it.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \u00e8 attivo" }, "trigger_type": { - "turn_off": "{entity_name} disattivato", - "turn_on": "{entity_name} attivato" + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json index 7277ef5900f02a..e055f67421ef53 100644 --- a/homeassistant/components/light/.translations/ko.json +++ b/homeassistant/components/light/.translations/ko.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" }, "trigger_type": { - "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/lb.json b/homeassistant/components/light/.translations/lb.json index fdd76cda7e62fc..a7f807e8dcda54 100644 --- a/homeassistant/components/light/.translations/lb.json +++ b/homeassistant/components/light/.translations/lb.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} ass un" }, "trigger_type": { - "turn_off": "{entity_name} gouf ausgeschalt", - "turn_on": "{entity_name} gouf ugeschalt" + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/nl.json b/homeassistant/components/light/.translations/nl.json index 546fea78b6d5e9..63954ca83a9fa1 100644 --- a/homeassistant/components/light/.translations/nl.json +++ b/homeassistant/components/light/.translations/nl.json @@ -2,16 +2,16 @@ "device_automation": { "action_type": { "toggle": "Omschakelen {naam}", - "turn_off": "{Naam} uitschakelen", - "turn_on": "{Naam} inschakelen" + "turn_off": "{entity_name} uitschakelen", + "turn_on": "{entity_name} inschakelen" }, "condition_type": { "is_off": "{name} is uitgeschakeld", "is_on": "{name} is ingeschakeld" }, "trigger_type": { - "turn_off": "{name} is uitgeschakeld", - "turn_on": "{name} is ingeschakeld" + "turned_off": "{name} is uitgeschakeld", + "turned_on": "{name} is ingeschakeld" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/no.json b/homeassistant/components/light/.translations/no.json index 2241ca6644e9c0..008123739d9138 100644 --- a/homeassistant/components/light/.translations/no.json +++ b/homeassistant/components/light/.translations/no.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} er p\u00e5" }, "trigger_type": { - "turn_off": "{name} sl\u00e5tt av", - "turn_on": "{name} sl\u00e5tt p\u00e5" + "turned_off": "{name} sl\u00e5tt av", + "turned_on": "{name} sl\u00e5tt p\u00e5" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 9debeaf4169d4c..22a93909578608 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -10,8 +10,8 @@ "is_on": "(entity_name} jest w\u0142\u0105czony." }, "trigger_type": { - "turn_off": "{nazwa} wy\u0142\u0105czone", - "turn_on": "{name} w\u0142\u0105czone" + "turned_off": "{nazwa} wy\u0142\u0105czone", + "turned_on": "{name} w\u0142\u0105czone" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json index 3154e17a509c7f..ba9339c1a944e8 100644 --- a/homeassistant/components/light/.translations/ru.json +++ b/homeassistant/components/light/.translations/ru.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/sl.json b/homeassistant/components/light/.translations/sl.json index 432c8ae37d15fb..bef4f1583b6b19 100644 --- a/homeassistant/components/light/.translations/sl.json +++ b/homeassistant/components/light/.translations/sl.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} je vklopljen" }, "trigger_type": { - "turn_off": "{entity_name} izklopljen", - "turn_on": "{entity_name} vklopljen" + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/zh-Hant.json b/homeassistant/components/light/.translations/zh-Hant.json index 8f5fec9b309e82..5ac06129463b35 100644 --- a/homeassistant/components/light/.translations/zh-Hant.json +++ b/homeassistant/components/light/.translations/zh-Hant.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \u5df2\u958b\u555f" }, "trigger_type": { - "turn_off": "{entity_name} \u5df2\u95dc\u9589", - "turn_on": "{entity_name} \u5df2\u958b\u555f" + "turned_off": "{entity_name} \u5df2\u95dc\u9589", + "turned_on": "{entity_name} \u5df2\u958b\u555f" } } } \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/lb.json b/homeassistant/components/linky/.translations/lb.json index d38002385590fa..cd3c7152c89e7d 100644 --- a/homeassistant/components/linky/.translations/lb.json +++ b/homeassistant/components/linky/.translations/lb.json @@ -4,6 +4,9 @@ "username_exists": "Kont ass scho konfigur\u00e9iert" }, "error": { + "access": "Keng Verbindung zu Enedis.fr, iwwerpr\u00e9ift d'Internet Verbindung", + "enedis": "Enedis.fr huet mat engem Feeler ge\u00e4ntwert: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)", + "unknown": "Onbekannte Feeler: prob\u00e9iert sp\u00e9ider nach emol (normalerweis net t\u00ebscht 23h00 an 2h00)", "username_exists": "Kont ass scho konfigur\u00e9iert", "wrong_login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert" }, diff --git a/homeassistant/components/logi_circle/.translations/ru.json b/homeassistant/components/logi_circle/.translations/ru.json index 1e9c089828fe92..40c7c8853daeb4 100644 --- a/homeassistant/components/logi_circle/.translations/ru.json +++ b/homeassistant/components/logi_circle/.translations/ru.json @@ -4,7 +4,7 @@ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "external_error": "\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u043e \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.", "external_setup": "Logi Circle \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.", - "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Logi Circle \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/logi_circle/)." + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Logi Circle \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index 1c24acd96e427a..ac88ed224edb26 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -4,7 +4,7 @@ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\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.", - "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Nest \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Nest \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430", diff --git a/homeassistant/components/point/.translations/ru.json b/homeassistant/components/point/.translations/ru.json index 2a10b234e99e21..487510969481f6 100644 --- a/homeassistant/components/point/.translations/ru.json +++ b/homeassistant/components/point/.translations/ru.json @@ -5,7 +5,7 @@ "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.", "external_setup": "Point \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0438\u0437 \u0434\u0440\u0443\u0433\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0430.", - "no_flows": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Point \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. [\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/point/)." + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Point \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/point/)." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/solaredge/.translations/lb.json b/homeassistant/components/solaredge/.translations/lb.json index 957a0187c1d6de..afc558ca80cd84 100644 --- a/homeassistant/components/solaredge/.translations/lb.json +++ b/homeassistant/components/solaredge/.translations/lb.json @@ -10,8 +10,10 @@ "user": { "data": { "api_key": "API Schl\u00ebssel fir d\u00ebsen Site", - "name": "Numm vun d\u00ebser Installatioun" - } + "name": "Numm vun d\u00ebser Installatioun", + "site_id": "SolarEdge site-ID" + }, + "title": "API Parameter fir d\u00ebs Installatioun d\u00e9fin\u00e9ieren" } }, "title": "SolarEdge" diff --git a/homeassistant/components/switch/.translations/bg.json b/homeassistant/components/switch/.translations/bg.json index 31e41d3f504dea..efccc652d5beb7 100644 --- a/homeassistant/components/switch/.translations/bg.json +++ b/homeassistant/components/switch/.translations/bg.json @@ -10,8 +10,8 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" }, "trigger_type": { - "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", - "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + "turned_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "turned_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ca.json b/homeassistant/components/switch/.translations/ca.json index 6fea704f756d49..dbf5e152656497 100644 --- a/homeassistant/components/switch/.translations/ca.json +++ b/homeassistant/components/switch/.translations/ca.json @@ -12,10 +12,8 @@ "turn_on": "{entity_name} activat" }, "trigger_type": { - "turn_off": "{entity_name} desactivat", - "turn_on": "{entity_name} activat", - "turned_off": "{entity_name} apagat", - "turned_on": "{entity_name} enc\u00e8s" + "turned_off": "{entity_name} desactivat", + "turned_on": "{entity_name} activat" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/en.json b/homeassistant/components/switch/.translations/en.json index ed036023755c40..391a071cb8f4fb 100644 --- a/homeassistant/components/switch/.translations/en.json +++ b/homeassistant/components/switch/.translations/en.json @@ -12,8 +12,6 @@ "turn_on": "{entity_name} turned on" }, "trigger_type": { - "turn_off": "{entity_name} turned off", - "turn_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on" } diff --git a/homeassistant/components/switch/.translations/es.json b/homeassistant/components/switch/.translations/es.json index b38928fa753dce..24dbc2cdc1fff0 100644 --- a/homeassistant/components/switch/.translations/es.json +++ b/homeassistant/components/switch/.translations/es.json @@ -12,8 +12,6 @@ "turn_on": "{entity_name} encendido" }, "trigger_type": { - "turn_off": "{entity_name} apagado", - "turn_on": "{entity_name} encendido", "turned_off": "{entity_name} apagado", "turned_on": "{entity_name} encendido" } diff --git a/homeassistant/components/switch/.translations/fr.json b/homeassistant/components/switch/.translations/fr.json index eeffc9262e56ea..4775d62bce3758 100644 --- a/homeassistant/components/switch/.translations/fr.json +++ b/homeassistant/components/switch/.translations/fr.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} allum\u00e9" }, "trigger_type": { - "turn_off": "{entity_name} \u00e9teint", - "turn_on": "{entity_name} allum\u00e9" + "turned_off": "{entity_name} \u00e9teint", + "turned_on": "{entity_name} allum\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/it.json b/homeassistant/components/switch/.translations/it.json index c51ce8c6ee5934..254c09380c1d1d 100644 --- a/homeassistant/components/switch/.translations/it.json +++ b/homeassistant/components/switch/.translations/it.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} attivato" }, "trigger_type": { - "turn_off": "{entity_name} disattivato", - "turn_on": "{entity_name} attivato" + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ko.json b/homeassistant/components/switch/.translations/ko.json index 2156ea04e01d68..02c303f932987b 100644 --- a/homeassistant/components/switch/.translations/ko.json +++ b/homeassistant/components/switch/.translations/ko.json @@ -6,12 +6,14 @@ "turn_on": "{entity_name} \ucf1c\uae30" }, "condition_type": { + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" }, "trigger_type": { - "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/lb.json b/homeassistant/components/switch/.translations/lb.json index 291d8cb478f251..8e974a0a8de137 100644 --- a/homeassistant/components/switch/.translations/lb.json +++ b/homeassistant/components/switch/.translations/lb.json @@ -6,12 +6,14 @@ "turn_on": "{entity_name} uschalten" }, "condition_type": { + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un", "turn_off": "{entity_name} gouf ausgeschalt", "turn_on": "{entity_name} gouf ugeschalt" }, "trigger_type": { - "turn_off": "{entity_name} gouf ausgeschalt", - "turn_on": "{entity_name} gouf ugeschalt" + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/nl.json b/homeassistant/components/switch/.translations/nl.json index 1d8355d21581bf..5e2aa6747a4c32 100644 --- a/homeassistant/components/switch/.translations/nl.json +++ b/homeassistant/components/switch/.translations/nl.json @@ -6,12 +6,14 @@ "turn_on": "Zet {entity_name} aan." }, "condition_type": { + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld", "turn_off": "{entity_name} uitgeschakeld", "turn_on": "{entity_name} ingeschakeld" }, "trigger_type": { - "turn_off": "{entity_name} uitgeschakeld", - "turn_on": "{entity_name} ingeschakeld" + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/no.json b/homeassistant/components/switch/.translations/no.json index 8a00ac09549771..adc128991c5efc 100644 --- a/homeassistant/components/switch/.translations/no.json +++ b/homeassistant/components/switch/.translations/no.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} sl\u00e5tt p\u00e5" }, "trigger_type": { - "turn_off": "{entity_name} sl\u00e5tt av", - "turn_on": "{entity_name} sl\u00e5tt p\u00e5" + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index f564d1424eac61..c63799bf783923 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} w\u0142\u0105czone" }, "trigger_type": { - "turn_off": "{entity_name} wy\u0142\u0105czone", - "turn_on": "{entity_name} w\u0142\u0105czone" + "turned_off": "{entity_name} wy\u0142\u0105czone", + "turned_on": "{entity_name} w\u0142\u0105czone" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index 1b0658cd174102..45a941b665de03 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/sl.json b/homeassistant/components/switch/.translations/sl.json index 38edfe5a1953d7..89423e071fdc8b 100644 --- a/homeassistant/components/switch/.translations/sl.json +++ b/homeassistant/components/switch/.translations/sl.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} vklopljen" }, "trigger_type": { - "turn_off": "{entity_name} izklopljen", - "turn_on": "{entity_name} vklopljen" + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/zh-Hant.json b/homeassistant/components/switch/.translations/zh-Hant.json index 0607f4ab08ee3c..c1a67897b16c84 100644 --- a/homeassistant/components/switch/.translations/zh-Hant.json +++ b/homeassistant/components/switch/.translations/zh-Hant.json @@ -10,8 +10,8 @@ "turn_on": "{entity_name} \u5df2\u958b\u555f" }, "trigger_type": { - "turn_off": "{entity_name} \u5df2\u95dc\u9589", - "turn_on": "{entity_name} \u5df2\u958b\u555f" + "turned_off": "{entity_name} \u5df2\u95dc\u9589", + "turned_on": "{entity_name} \u5df2\u958b\u555f" } } } \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 012aa65187c28b..0eddbe2a151d79 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -4,7 +4,7 @@ "client_id": "Client ID \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "client_secret": "Client secret \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", - "no_app": "\u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Toon \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/toon/).", + "no_app": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Toon \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { diff --git a/homeassistant/components/traccar/.translations/lb.json b/homeassistant/components/traccar/.translations/lb.json new file mode 100644 index 00000000000000..8808d85a1d6db2 --- /dev/null +++ b/homeassistant/components/traccar/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u00c4r Home Assistant Instanz muss iwwert Internet accessibel si fir Traccar Noriichten z'empf\u00e4nken.", + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "create_entry": { + "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Traccar ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider Informatiounen." + }, + "step": { + "user": { + "description": "S\u00e9cher fir Traccar anzeriichten?", + "title": "Traccar ariichten" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/lb.json b/homeassistant/components/twentemilieu/.translations/lb.json new file mode 100644 index 00000000000000..0b07c5003ef2ff --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "address_exists": "Adresse ass scho ageriicht." + }, + "error": { + "connection_error": "Feeler beim verbannen." + }, + "step": { + "user": { + "data": { + "house_letter": "Haus Buschtaf/zous\u00e4tzlech", + "house_number": "Haus Nummer", + "post_code": "Postleitzuel" + }, + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json index f907364327c958..518f0066534411 100644 --- a/homeassistant/components/unifi/.translations/nl.json +++ b/homeassistant/components/unifi/.translations/nl.json @@ -32,6 +32,12 @@ "track_devices": "Netwerkapparaten volgen (Ubiquiti-apparaten)", "track_wired_clients": "Inclusief bedrade netwerkcli\u00ebnten" } + }, + "init": { + "data": { + "one": "Leeg", + "other": "Leeg" + } } } } diff --git a/homeassistant/components/velbus/.translations/lb.json b/homeassistant/components/velbus/.translations/lb.json new file mode 100644 index 00000000000000..89e0bd818d277c --- /dev/null +++ b/homeassistant/components/velbus/.translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "port_exists": "D\u00ebse Port ass scho konfigur\u00e9iert" + }, + "error": { + "connection_failed": "Feeler bei der velbus Verbindung", + "port_exists": "D\u00ebse Port ass scho konfigur\u00e9iert" + }, + "step": { + "user": { + "data": { + "name": "Numm fir d\u00ebs velbus Verbindung", + "port": "Verbindungs zeeche-folleg" + } + } + }, + "title": "Velbus Interface" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/lb.json b/homeassistant/components/vesync/.translations/lb.json index 7d1dbad19f3bd8..cfccd8b1dbb49a 100644 --- a/homeassistant/components/vesync/.translations/lb.json +++ b/homeassistant/components/vesync/.translations/lb.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_setup": "N\u00ebmmen eng eenzeg Instanz vu Vesync ass erlaabt." + }, "error": { "invalid_login": "Ong\u00ebltege Benotzernumm oder Passwuert" }, diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json index a96f8cff523fdd..2f2fdbe9b3f742 100644 --- a/homeassistant/components/withings/.translations/ca.json +++ b/homeassistant/components/withings/.translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Necessites configurar Withings abans de poder autenticar't-hi. Llegeix la documentaci\u00f3." + }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Withings per al perfil seleccionat." }, diff --git a/homeassistant/components/withings/.translations/ko.json b/homeassistant/components/withings/.translations/ko.json index 3c2f00ba4aef76..617964e0596aba 100644 --- a/homeassistant/components/withings/.translations/ko.json +++ b/homeassistant/components/withings/.translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Withings \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Withings \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/withings/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + }, "create_entry": { "default": "\uc120\ud0dd\ud55c \ud504\ub85c\ud544\ub85c Withings \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json index 994d02aa7f5936..9015f4908308d1 100644 --- a/homeassistant/components/withings/.translations/lb.json +++ b/homeassistant/components/withings/.translations/lb.json @@ -7,6 +7,7 @@ }, "title": "Benotzer Profil." } - } + }, + "title": "Withings" } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json index 1729879a154d8d..3776621bec208e 100644 --- a/homeassistant/components/withings/.translations/nl.json +++ b/homeassistant/components/withings/.translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "U moet Withings configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de documentatie te lezen]" + }, "create_entry": { "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." }, diff --git a/homeassistant/components/withings/.translations/ru.json b/homeassistant/components/withings/.translations/ru.json index d9d5e14208f05b..c6c621fbdf33ee 100644 --- a/homeassistant/components/withings/.translations/ru.json +++ b/homeassistant/components/withings/.translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Withings \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, From 80136f3591b0580a38ac9ef2babf2f79fd444272 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 19 Sep 2019 09:39:09 +0300 Subject: [PATCH 0350/3953] Change datetime.now() to dt_util.now() (#26582) * Change datetime.now() to dt_util.now() in cases where the functionality should stay the same These changes should not affect the functionality, rather cleanup our codebase. In general we would like integrations to not to use datetime.now() unless there's a very good reason for it, rather use our own dt_util.now() which makes the code aware of our current time zone. * Use datetime.utcnow() for season sensor to get offset-naive utc time * Revert "Use datetime.utcnow() for season sensor to get offset-naive utc time" This reverts commit 5f36463d9c7d52f8e11ffcec7e57dfbc7b21bdd1. * BOM sensor last_updated should be UTC as well * Run black * Remove unused last_partition_update variable --- .../components/bmw_connected_drive/__init__.py | 4 ++-- homeassistant/components/bom/sensor.py | 13 ++++++++----- .../components/concord232/alarm_control_panel.py | 1 - .../components/concord232/binary_sensor.py | 7 ++++--- homeassistant/components/demo/weather.py | 5 +++-- homeassistant/components/dlna_dmr/media_player.py | 6 +++--- homeassistant/components/doorbird/camera.py | 3 ++- homeassistant/components/doorbird/switch.py | 5 +++-- homeassistant/components/ebusd/sensor.py | 5 ++--- homeassistant/components/ios/notify.py | 3 +-- homeassistant/components/mobile_app/notify.py | 3 +-- homeassistant/components/ring/light.py | 9 +++++---- homeassistant/components/ring/switch.py | 9 +++++---- homeassistant/components/season/sensor.py | 5 +++-- homeassistant/components/systemmonitor/sensor.py | 3 +-- homeassistant/components/upnp/sensor.py | 6 +++--- 16 files changed, 46 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 160c8a5e4551c9..8e67da86dc3014 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,5 +1,4 @@ """Reads vehicle status from BMW connected drive portal.""" -import datetime import logging import voluptuous as vol @@ -8,6 +7,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -100,7 +100,7 @@ def execute_service(call): # update every UPDATE_INTERVAL minutes, starting now # this should even out the load on the servers - now = datetime.datetime.now() + now = dt_util.utcnow() track_utc_time_change( hass, cd_account.update, diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 33444f1099652a..ed22be003ad4fa 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -13,6 +13,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, @@ -240,7 +241,7 @@ def should_update(self): # Never updated before, therefore an update should occur. return True - now = datetime.datetime.now() + now = dt_util.utcnow() update_due_at = self.last_updated + datetime.timedelta(minutes=35) return now > update_due_at @@ -251,8 +252,8 @@ def update(self): _LOGGER.debug( "BOM was updated %s minutes ago, skipping update as" " < 35 minutes, Now: %s, LastUpdate: %s", - (datetime.datetime.now() - self.last_updated), - datetime.datetime.now(), + (dt_util.utcnow() - self.last_updated), + dt_util.utcnow(), self.last_updated, ) return @@ -263,8 +264,10 @@ def update(self): # set lastupdate using self._data[0] as the first element in the # array is the latest date in the json - self.last_updated = datetime.datetime.strptime( - str(self._data[0]["local_date_time_full"]), "%Y%m%d%H%M%S" + self.last_updated = dt_util.as_utc( + datetime.datetime.strptime( + str(self._data[0]["local_date_time_full"]), "%Y%m%d%H%M%S" + ) ) return diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 68f0d77e307a82..e86ec02040e3b3 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -69,7 +69,6 @@ def __init__(self, url, name, code, mode): self._url = url self._alarm = concord232_client.Client(self._url) self._alarm.partitions = self._alarm.list_partitions() - self._alarm.last_partition_update = datetime.datetime.now() @property def name(self): diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 10643f134d70e8..1a406d743b7cfa 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -12,6 +12,7 @@ ) from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -53,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.debug("Initializing client") client = concord232_client.Client(f"http://{host}:{port}") client.zones = client.list_zones() - client.last_zone_update = datetime.datetime.now() + client.last_zone_update = dt_util.utcnow() except requests.exceptions.ConnectionError as ex: _LOGGER.error("Unable to connect to Concord232: %s", str(ex)) @@ -128,11 +129,11 @@ def is_on(self): def update(self): """Get updated stats from API.""" - last_update = datetime.datetime.now() - self._client.last_zone_update + last_update = dt_util.utcnow() - self._client.last_zone_update _LOGGER.debug("Zone: %s ", self._zone) if last_update > datetime.timedelta(seconds=1): self._client.zones = self._client.list_zones() - self._client.last_zone_update = datetime.datetime.now() + self._client.last_zone_update = dt_util.utcnow() _LOGGER.debug("Updated from zone: %s", self._zone["name"]) if hasattr(self._client, "zones"): diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index b81a2193bb54ab..2253f261ad2cef 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -1,5 +1,5 @@ """Demo platform that offers fake meteorological data.""" -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -10,6 +10,7 @@ WeatherEntity, ) from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +import homeassistant.util.dt as dt_util CONDITION_CLASSES = { "cloudy": [], @@ -147,7 +148,7 @@ def attribution(self): @property def forecast(self): """Return the forecast.""" - reftime = datetime.now().replace(hour=16, minute=00) + reftime = dt_util.now().replace(hour=16, minute=00) forecast_data = [] for entry in self._forecast: diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index c7c488950cc747..5dd7ab7a88a74d 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -1,6 +1,5 @@ """Support for DLNA DMR (Device Media Renderer).""" import asyncio -from datetime import datetime from datetime import timedelta import functools import logging @@ -43,6 +42,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util from homeassistant.util import get_local_ip _LOGGER = logging.getLogger(__name__) @@ -241,14 +241,14 @@ async def async_update(self): return # do we need to (re-)subscribe? - now = datetime.now() + now = dt_util.utcnow() should_renew = ( self._subscription_renew_time and now >= self._subscription_renew_time ) if should_renew or not was_available and self._available: try: timeout = await self._device.async_subscribe_services() - self._subscription_renew_time = datetime.now() + timeout / 2 + self._subscription_renew_time = dt_util.utcnow() + timeout / 2 except (asyncio.TimeoutError, aiohttp.ClientError): self._available = False _LOGGER.debug("Could not (re)subscribe") diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index eaae3f1923636d..457c319d9e1654 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -8,6 +8,7 @@ from homeassistant.components.camera import Camera, SUPPORT_STREAM from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.util.dt as dt_util from . import DOMAIN as DOORBIRD_DOMAIN @@ -77,7 +78,7 @@ def name(self): async def async_camera_image(self): """Pull a still image from the camera.""" - now = datetime.datetime.now() + now = dt_util.utcnow() if self._last_image and now - self._last_update < self._interval: return self._last_image diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py index 643e006dfef0ed..7a0dfa82e76eef 100644 --- a/homeassistant/components/doorbird/switch.py +++ b/homeassistant/components/doorbird/switch.py @@ -3,6 +3,7 @@ import logging from homeassistant.components.switch import SwitchDevice +import homeassistant.util.dt as dt_util from . import DOMAIN as DOORBIRD_DOMAIN @@ -66,7 +67,7 @@ def turn_on(self, **kwargs): else: self._state = self._doorstation.device.energize_relay(self._relay) - now = datetime.datetime.now() + now = dt_util.utcnow() self._assume_off = now + self._time def turn_off(self, **kwargs): @@ -75,6 +76,6 @@ def turn_off(self, **kwargs): def update(self): """Wait for the correct amount of assumed time to pass.""" - if self._state and self._assume_off <= datetime.datetime.now(): + if self._state and self._assume_off <= dt_util.utcnow(): self._state = False self._assume_off = datetime.datetime.min diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index ac156e040d7332..4bc79e7bd39c19 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -3,6 +3,7 @@ import datetime from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -68,9 +69,7 @@ def device_state_attributes(self): if index < len(time_frame): parsed = datetime.datetime.strptime(time_frame[index], "%H:%M") parsed = parsed.replace( - datetime.datetime.now().year, - datetime.datetime.now().month, - datetime.datetime.now().day, + dt_util.now().year, dt_util.now().month, dt_util.now().day ) schedule[item[0]] = parsed.isoformat() return schedule diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index 83e4c089b9a541..ee74b369629b75 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -1,5 +1,4 @@ """Support for iOS push notifications.""" -from datetime import datetime, timezone import logging import requests @@ -25,7 +24,7 @@ def log_rate_limits(hass, target, resp, level=20): """Output rate limit log line at given level.""" rate_limits = resp["rateLimits"] resetsAt = dt_util.parse_datetime(rate_limits["resetsAt"]) - resetsAtTime = resetsAt - datetime.now(timezone.utc) + resetsAtTime = resetsAt - dt_util.utcnow() rate_limit_msg = ( "iOS push notification rate limits for %s: " "%d sent, %d allowed, %d errors, " diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index d16ed23266a412..1e6a0517026255 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -1,6 +1,5 @@ """Support for mobile_app push notifications.""" import asyncio -from datetime import datetime, timezone import logging import async_timeout @@ -60,7 +59,7 @@ def log_rate_limits(hass, device_name, resp, level=logging.INFO): rate_limits = resp[ATTR_PUSH_RATE_LIMITS] resetsAt = rate_limits[ATTR_PUSH_RATE_LIMITS_RESETS_AT] - resetsAtTime = dt_util.parse_datetime(resetsAt) - datetime.now(timezone.utc) + resetsAtTime = dt_util.parse_datetime(resetsAt) - dt_util.utcnow() rate_limit_msg = ( "mobile_app push notification rate limits for %s: " "%d sent, %d allowed, %d errors, " diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 5805114252e09a..697be4d1579220 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -1,9 +1,10 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" import logging -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.components.light import Light from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING @@ -41,7 +42,7 @@ def __init__(self, device): self._device = device self._unique_id = self._device.id self._light_on = False - self._no_updates_until = datetime.now() + self._no_updates_until = dt_util.utcnow() async def async_added_to_hass(self): """Register callbacks.""" @@ -77,7 +78,7 @@ 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._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.async_schedule_update_ha_state(True) def turn_on(self, **kwargs): @@ -90,7 +91,7 @@ def turn_off(self, **kwargs): def update(self): """Update current state of the light.""" - if self._no_updates_until > datetime.now(): + if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") return diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index cbbecb1a40398b..413d2a70aae2ba 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -1,9 +1,10 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" import logging -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.components.switch import SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING @@ -72,14 +73,14 @@ class SirenSwitch(BaseRingSwitch): def __init__(self, device): """Initialize the switch for a device with a siren.""" super().__init__(device, "siren") - self._no_updates_until = datetime.now() + self._no_updates_until = dt_util.utcnow() 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._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.schedule_update_ha_state() @property @@ -102,7 +103,7 @@ def icon(self): def update(self): """Update state of the siren.""" - if self._no_updates_until > datetime.now(): + if self._no_updates_until > dt_util.utcnow(): _LOGGER.debug("Skipping update...") return self._siren_on = self._device.siren > 0 diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index f2e0ccac2b0008..cdd6af57617e46 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -8,6 +8,7 @@ from homeassistant.const import CONF_TYPE from homeassistant.helpers.entity import Entity from homeassistant import util +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -104,7 +105,7 @@ def __init__(self, hass, hemisphere, season_tracking_type): """Initialize the season.""" self.hass = hass self.hemisphere = hemisphere - self.datetime = datetime.now() + self.datetime = dt_util.utcnow().replace(tzinfo=None) self.type = season_tracking_type self.season = get_season(self.datetime, self.hemisphere, self.type) @@ -125,5 +126,5 @@ def icon(self): def update(self): """Update season.""" - self.datetime = datetime.utcnow() + self.datetime = dt_util.utcnow().replace(tzinfo=None) self.season = get_season(self.datetime, self.hemisphere, self.type) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 446a36ec350f88..ad2072baaa5230 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -1,5 +1,4 @@ """Support for monitoring the local system.""" -from datetime import datetime import logging import os import socket @@ -193,7 +192,7 @@ def update(self): counters = psutil.net_io_counters(pernic=True) if self.argument in counters: counter = counters[self.argument][IO_COUNTER[self.type]] - now = datetime.now() + now = dt_util.utcnow() if self._last_value and self._last_value < counter: self._state = round( (counter - self._last_value) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index e5746e088f866b..b721fa29cdd860 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -1,11 +1,11 @@ """Support for UPnP/IGD Sensors.""" -from datetime import datetime import logging from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.dt as dt_util from .const import DOMAIN as DOMAIN_UPNP, SIGNAL_REMOVE_SENSOR @@ -199,10 +199,10 @@ async def async_update(self): if self._last_value is None: self._last_value = new_value - self._last_update_time = datetime.now() + self._last_update_time = dt_util.utcnow() return - now = datetime.now() + now = dt_util.utcnow() if self._is_overflowed(new_value): self._state = None # temporarily report nothing else: From 5e15675593ba94a2c11f9f929cdad317e27ce190 Mon Sep 17 00:00:00 2001 From: Martin Brooksbank Date: Thu, 19 Sep 2019 08:55:07 +0100 Subject: [PATCH 0351/3953] Add additional needles to glances cpu_temp attribute (#22311) * Added additional needles to the cpu_temp attribute * Fix conflict --- homeassistant/components/glances/sensor.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 1385d5e59a768b..90b4b386f37e52 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -197,18 +197,20 @@ async def async_update(self): elif self.type == "cpu_temp": for sensor in value["sensors"]: if sensor["label"] in [ - "CPU", + "amdgpu 1", + "aml_thermal", + "Core 0", + "Core 1", "CPU Temperature", - "Package id 0", - "Physical id 0", - "cpu_thermal 1", + "CPU", "cpu-thermal 1", + "cpu_thermal 1", "exynos-therm 1", - "soc_thermal 1", + "Package id 0", + "Physical id 0", + "radeon 1", "soc-thermal 1", - "aml_thermal", - "Core 0", - "Core 1", + "soc_thermal 1", ]: self._state = sensor["value"] elif self.type == "docker_active": From 770eeaf82f4800127e977f980f5438d0fa8edbc9 Mon Sep 17 00:00:00 2001 From: Andrew Rowson Date: Thu, 19 Sep 2019 11:51:49 +0100 Subject: [PATCH 0352/3953] Encode prometheus metric names per the prom spec (#26639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Referencing issue #26418. Prometheus metric names can only contain chars a-zA-Z0-9, : and _ (https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels). HA currently generates invalid prometheus names, e.g. if the unit for a sensor is a non-ASCII character containing ° or μ. To resolve, we need to sanitize the name before creating, replacing non-valid characters with a valid representation. In this case, I've used "u{unicode-hex-code}". Also updated the test case to make sure that the ° case is handled. --- homeassistant/components/prometheus/__init__.py | 16 +++++++++++++++- tests/components/prometheus/test_init.py | 11 +++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 1ba2c4809b6618..82db5f6725f512 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -1,5 +1,6 @@ """Support for Prometheus metrics export.""" import logging +import string from aiohttp import web import voluptuous as vol @@ -159,10 +160,23 @@ def _metric(self, metric, factory, documentation, labels=None): try: return self._metrics[metric] except KeyError: - full_metric_name = f"{self.metrics_prefix}{metric}" + full_metric_name = self._sanitize_metric_name( + f"{self.metrics_prefix}{metric}" + ) self._metrics[metric] = factory(full_metric_name, documentation, labels) return self._metrics[metric] + @staticmethod + def _sanitize_metric_name(metric: str) -> str: + return "".join( + [ + c + if c in string.ascii_letters or c.isdigit() or c == "_" or c == ":" + else f"u{hex(ord(c))}" + for c in metric + ] + ) + @staticmethod def state_as_number(state): """Return a state casted to a float.""" diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 9e313fd3694583..4ec40731c5d206 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -41,6 +41,11 @@ async def prometheus_client(loop, hass, hass_client): sensor3.entity_id = "sensor.electricity_price" await sensor3.async_update_ha_state() + sensor4 = DemoSensor("Wind Direction", 25, None, "°", None) + sensor4.hass = hass + sensor4.entity_id = "sensor.wind_direction" + await sensor4.async_update_ha_state() + return await hass_client() @@ -103,3 +108,9 @@ def test_view(prometheus_client): # pylint: disable=redefined-outer-name 'entity="sensor.electricity_price",' 'friendly_name="Electricity price"} 0.123' in body ) + + assert ( + 'sensor_unit_u0xb0{domain="sensor",' + 'entity="sensor.wind_direction",' + 'friendly_name="Wind Direction"} 25.0' in body + ) From 1041b106168fe00934c721c1764d53de912808d9 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 19 Sep 2019 19:19:27 +0300 Subject: [PATCH 0353/3953] Move alexa integration to use dt_util (#26723) --- homeassistant/components/alexa/capabilities.py | 4 ++-- homeassistant/components/alexa/flash_briefings.py | 4 ++-- homeassistant/components/alexa/handlers.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index d769f797da1bfa..aeaa0a62c4bdbb 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,5 +1,4 @@ """Alexa capabilities.""" -from datetime import datetime import logging from homeassistant.const import ( @@ -16,6 +15,7 @@ import homeassistant.components.climate.const as climate from homeassistant.components import light, fan, cover import homeassistant.util.color as color_util +import homeassistant.util.dt as dt_util from .const import ( API_TEMP_UNITS, @@ -109,7 +109,7 @@ def serialize_properties(self): "name": prop_name, "namespace": self.name(), "value": prop_value, - "timeOfSample": datetime.now().strftime(DATE_FORMAT), + "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT), "uncertaintyInMilliseconds": 0, } diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 708d1592e4c0fb..0b5c1243764ce2 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -1,9 +1,9 @@ """Support for Alexa skill service end point.""" import copy -from datetime import datetime import logging import uuid +import homeassistant.util.dt as dt_util from homeassistant.components import http from homeassistant.core import callback from homeassistant.helpers import template @@ -89,7 +89,7 @@ def get(self, request, briefing_id): else: output[ATTR_REDIRECTION_URL] = item.get(CONF_DISPLAY_URL) - output[ATTR_UPDATE_DATE] = datetime.now().strftime(DATE_FORMAT) + output[ATTR_UPDATE_DATE] = dt_util.utcnow().strftime(DATE_FORMAT) briefing.append(output) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 1e636b96ee5205..c72101460c4819 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1,5 +1,4 @@ """Alexa message handlers.""" -from datetime import datetime import logging import math @@ -28,6 +27,7 @@ TEMP_FAHRENHEIT, ) import homeassistant.util.color as color_util +import homeassistant.util.dt as dt_util from homeassistant.util.decorator import Registry from homeassistant.util.temperature import convert as convert_temperature @@ -275,7 +275,7 @@ async def async_api_activate(hass, config, directive, context): payload = { "cause": {"type": Cause.VOICE_INTERACTION}, - "timestamp": "%sZ" % (datetime.utcnow().isoformat(),), + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", } return directive.response( @@ -299,7 +299,7 @@ async def async_api_deactivate(hass, config, directive, context): payload = { "cause": {"type": Cause.VOICE_INTERACTION}, - "timestamp": "%sZ" % (datetime.utcnow().isoformat(),), + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", } return directive.response( From 468deef32674b40a140886ef3b8ecbc370611004 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 20:02:21 +0200 Subject: [PATCH 0354/3953] Bumps pytest to 5.1.2 (#26718) --- 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 bfe459b0cfb8d3..0d96492e3aadbe 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -17,5 +17,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.1 +pytest==5.1.2 requests_mock==1.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69ca7eefe03a52..87f02e53b79677 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -18,7 +18,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.1 +pytest==5.1.2 requests_mock==1.6.0 From a8a485abf7ba95fe6b744c24cf80cf45453ca09b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 20:34:41 +0200 Subject: [PATCH 0355/3953] Bumps aiohttp to 3.6.0 (#26728) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- .../smartthings/test_config_flow.py | 25 ++++++++++++--- tests/components/smartthings/test_init.py | 31 ++++++++++++++----- tests/test_util/aiohttp.py | 6 +++- 6 files changed, 51 insertions(+), 17 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5eeec405e7da3d..0a5cfbfc67d15a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiohttp==3.5.4 +aiohttp==3.6.0 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9848716d895e32..a00441518a54fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.5.4 +aiohttp==3.6.0 astral==1.10.1 async_timeout==3.0.1 attrs==19.1.0 diff --git a/setup.py b/setup.py index 5ab8d74c64cf00..8be458fc449c16 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.5.4", + "aiohttp==3.6.0", "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.1.0", diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 5724d7a3bac0be..fce0129a7bf399 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -84,7 +84,10 @@ async def test_token_unauthorized(hass, smartthings_mock): flow = SmartThingsFlowHandler() flow.hass = hass - smartthings_mock.apps.side_effect = ClientResponseError(None, None, status=401) + request_info = Mock(real_url="http://example.com") + smartthings_mock.apps.side_effect = ClientResponseError( + request_info=request_info, history=None, status=401 + ) result = await flow.async_step_user({"access_token": str(uuid4())}) @@ -98,7 +101,10 @@ async def test_token_forbidden(hass, smartthings_mock): flow = SmartThingsFlowHandler() flow.hass = hass - smartthings_mock.apps.side_effect = ClientResponseError(None, None, status=403) + request_info = Mock(real_url="http://example.com") + smartthings_mock.apps.side_effect = ClientResponseError( + request_info=request_info, history=None, status=403 + ) result = await flow.async_step_user({"access_token": str(uuid4())}) @@ -113,7 +119,10 @@ async def test_webhook_error(hass, smartthings_mock): flow.hass = hass data = {"error": {}} - error = APIResponseError(None, None, data=data, status=422) + request_info = Mock(real_url="http://example.com") + error = APIResponseError( + request_info=request_info, history=None, data=data, status=422 + ) error.is_target_error = Mock(return_value=True) smartthings_mock.apps.side_effect = error @@ -131,7 +140,10 @@ async def test_api_error(hass, smartthings_mock): flow.hass = hass data = {"error": {}} - error = APIResponseError(None, None, data=data, status=400) + request_info = Mock(real_url="http://example.com") + error = APIResponseError( + request_info=request_info, history=None, data=data, status=400 + ) smartthings_mock.apps.side_effect = error @@ -147,7 +159,10 @@ async def test_unknown_api_error(hass, smartthings_mock): flow = SmartThingsFlowHandler() flow.hass = hass - smartthings_mock.apps.side_effect = ClientResponseError(None, None, status=404) + request_info = Mock(real_url="http://example.com") + smartthings_mock.apps.side_effect = ClientResponseError( + request_info=request_info, history=None, status=404 + ) result = await flow.async_step_user({"access_token": str(uuid4())}) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 4e1ffce7e22119..9749ab9bb71e3d 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -54,7 +54,10 @@ async def test_unrecoverable_api_errors_create_new_flow( """ assert await async_setup_component(hass, "persistent_notification", {}) config_entry.add_to_hass(hass) - smartthings_mock.app.side_effect = ClientResponseError(None, None, status=401) + request_info = Mock(real_url="http://example.com") + smartthings_mock.app.side_effect = ClientResponseError( + request_info=request_info, history=None, status=401 + ) # Assert setup returns false result = await smartthings.async_setup_entry(hass, config_entry) @@ -75,7 +78,10 @@ async def test_recoverable_api_errors_raise_not_ready( ): """Test config entry not ready raised for recoverable API errors.""" config_entry.add_to_hass(hass) - smartthings_mock.app.side_effect = ClientResponseError(None, None, status=500) + request_info = Mock(real_url="http://example.com") + smartthings_mock.app.side_effect = ClientResponseError( + request_info=request_info, history=None, status=500 + ) with pytest.raises(ConfigEntryNotReady): await smartthings.async_setup_entry(hass, config_entry) @@ -86,9 +92,12 @@ async def test_scenes_api_errors_raise_not_ready( ): """Test if scenes are unauthorized we continue to load platforms.""" config_entry.add_to_hass(hass) + request_info = Mock(real_url="http://example.com") smartthings_mock.app.return_value = app smartthings_mock.installed_app.return_value = installed_app - smartthings_mock.scenes.side_effect = ClientResponseError(None, None, status=500) + smartthings_mock.scenes.side_effect = ClientResponseError( + request_info=request_info, history=None, status=500 + ) with pytest.raises(ConfigEntryNotReady): await smartthings.async_setup_entry(hass, config_entry) @@ -140,10 +149,13 @@ async def test_scenes_unauthorized_loads_platforms( ): """Test if scenes are unauthorized we continue to load platforms.""" config_entry.add_to_hass(hass) + request_info = Mock(real_url="http://example.com") smartthings_mock.app.return_value = app smartthings_mock.installed_app.return_value = installed_app smartthings_mock.devices.return_value = [device] - smartthings_mock.scenes.side_effect = ClientResponseError(None, None, status=403) + smartthings_mock.scenes.side_effect = ClientResponseError( + request_info=request_info, history=None, status=403 + ) mock_token = Mock() mock_token.access_token.return_value = str(uuid4()) mock_token.refresh_token.return_value = str(uuid4()) @@ -290,12 +302,13 @@ async def test_remove_entry_app_in_use(hass, config_entry, smartthings_mock): async def test_remove_entry_already_deleted(hass, config_entry, smartthings_mock): """Test handles when the apps have already been removed.""" + request_info = Mock(real_url="http://example.com") # Arrange smartthings_mock.delete_installed_app.side_effect = ClientResponseError( - None, None, status=403 + request_info=request_info, history=None, status=403 ) smartthings_mock.delete_app.side_effect = ClientResponseError( - None, None, status=403 + request_info=request_info, history=None, status=403 ) # Act await smartthings.async_remove_entry(hass, config_entry) @@ -308,9 +321,10 @@ async def test_remove_entry_installedapp_api_error( hass, config_entry, smartthings_mock ): """Test raises exceptions removing the installed app.""" + request_info = Mock(real_url="http://example.com") # Arrange smartthings_mock.delete_installed_app.side_effect = ClientResponseError( - None, None, status=500 + request_info=request_info, history=None, status=500 ) # Act with pytest.raises(ClientResponseError): @@ -337,8 +351,9 @@ async def test_remove_entry_installedapp_unknown_error( async def test_remove_entry_app_api_error(hass, config_entry, smartthings_mock): """Test raises exceptions removing the app.""" # Arrange + request_info = Mock(real_url="http://example.com") smartthings_mock.delete_app.side_effect = ClientResponseError( - None, None, status=500 + request_info=request_info, history=None, status=500 ) # Act with pytest.raises(ClientResponseError): diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 98fc70f3bf51f3..ce13ca5a594aa5 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -244,8 +244,12 @@ def release(self): def raise_for_status(self): """Raise error if status is 400 or higher.""" if self.status >= 400: + request_info = mock.Mock(real_url="http://example.com") raise ClientResponseError( - None, None, code=self.status, headers=self.headers + request_info=request_info, + history=None, + code=self.status, + headers=self.headers, ) def close(self): From 1e5de9e9e4e9806d7a0b2a3b691b225bb379a12e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Sep 2019 11:49:47 -0700 Subject: [PATCH 0356/3953] Bump TRADFRI (#26731) * Bump TRADFRI * Fix test --- homeassistant/components/tradfri/manifest.json | 12 +++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tradfri/test_light.py | 4 +++- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index ba6b21e00283ab..d847c6df24f7cb 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,17 +3,11 @@ "name": "Tradfri", "config_flow": true, "documentation": "https://www.home-assistant.io/components/tradfri", - "requirements": [ - "pytradfri[async]==6.0.1" - ], + "requirements": ["pytradfri[async]==6.3.1"], "homekit": { - "models": [ - "TRADFRI" - ] + "models": ["TRADFRI"] }, "dependencies": [], "zeroconf": ["_coap._udp.local."], - "codeowners": [ - "@ggravlingen" - ] + "codeowners": ["@ggravlingen"] } diff --git a/requirements_all.txt b/requirements_all.txt index a00441518a54fc..99e94ec438abc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1596,7 +1596,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==6.0.1 +pytradfri[async]==6.3.1 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87f02e53b79677..0ac705bf2e1d72 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ python-velbus==2.0.27 python_awair==0.0.4 # homeassistant.components.tradfri -pytradfri[async]==6.0.1 +pytradfri[async]==6.3.1 # homeassistant.components.vesync pyvesync==1.1.0 diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 69fe3bae618dab..4c691f66af872c 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -4,7 +4,9 @@ from unittest.mock import Mock, MagicMock, patch, PropertyMock import pytest -from pytradfri.device import Device, LightControl, Light +from pytradfri.device import Device +from pytradfri.device.light import Light +from pytradfri.device.light_control import LightControl from homeassistant.components import tradfri From 44cde5fb733f0ef8d7a992919946e8a9586291df Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 20:50:45 +0200 Subject: [PATCH 0357/3953] Bumps pre-commit to 1.18.3 (#26717) --- 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 0d96492e3aadbe..44b27d8e13e9c5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 mypy==0.720 -pre-commit==1.18.2 +pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0ac705bf2e1d72..83ec2d1d2c1023 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -11,7 +11,7 @@ flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 mypy==0.720 -pre-commit==1.18.2 +pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 pytest-aiohttp==0.3.0 From 0e201fd85936339208d0e10d4ca22db7c9e04fc6 Mon Sep 17 00:00:00 2001 From: Robin Wohlers-Reichel Date: Fri, 20 Sep 2019 04:52:15 +1000 Subject: [PATCH 0358/3953] Update Solax Library to 0.2.2 (#26705) * bump version and adjust * Address review comments * Fix import order * bump solax version * Trigger Azure * default port --- homeassistant/components/solax/manifest.json | 2 +- homeassistant/components/solax/sensor.py | 28 +++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 52e50ab47998a5..3a154b857fe878 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -3,7 +3,7 @@ "name": "Solax Inverter", "documentation": "https://www.home-assistant.io/components/solax", "requirements": [ - "solax==0.1.2" + "solax==0.2.2" ], "dependencies": [], "codeowners": ["@squishykid"] diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 0c1cfcf21da32a..a5b4547b344894 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -4,9 +4,11 @@ from datetime import timedelta import logging +from solax import real_time_api +from solax.inverter import InverterError import voluptuous as vol -from homeassistant.const import TEMP_CELSIUS, CONF_IP_ADDRESS +from homeassistant.const import TEMP_CELSIUS, CONF_IP_ADDRESS, CONF_PORT from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -15,24 +17,28 @@ _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_IP_ADDRESS): cv.string}) +DEFAULT_PORT = 80 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + } +) SCAN_INTERVAL = timedelta(seconds=30) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Platform setup.""" - import solax - - api = solax.RealTimeAPI(config[CONF_IP_ADDRESS]) + api = await real_time_api(config[CONF_IP_ADDRESS], config[CONF_PORT]) endpoint = RealTimeDataEndpoint(hass, api) resp = await api.get_data() serial = resp.serial_number hass.async_add_job(endpoint.async_refresh) async_track_time_interval(hass, endpoint.async_refresh, SCAN_INTERVAL) devices = [] - for sensor in solax.INVERTER_SENSORS: - idx, unit = solax.INVERTER_SENSORS[sensor] + for sensor, (idx, unit) in api.inverter.sensor_map().items(): if unit == "C": unit = TEMP_CELSIUS uid = f"{serial}-{idx}" @@ -56,16 +62,14 @@ async def async_refresh(self, now=None): This is the only method that should fetch new data for Home Assistant. """ - from solax import SolaxRequestError - try: api_response = await self.api.get_data() self.ready.set() - except SolaxRequestError: + except InverterError: if now is not None: self.ready.clear() - else: - raise PlatformNotReady + return + raise PlatformNotReady data = api_response.data for sensor in self.sensors: if sensor.key in data: diff --git a/requirements_all.txt b/requirements_all.txt index 99e94ec438abc4..e6ac06e9199fc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1785,7 +1785,7 @@ solaredge-local==0.1.4 solaredge==0.0.2 # homeassistant.components.solax -solax==0.1.2 +solax==0.2.2 # homeassistant.components.honeywell somecomfort==0.5.2 From 94192ecd7d7d60c9b4517d4dd0efd3f2ee770d3d Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Thu, 19 Sep 2019 13:27:18 -0700 Subject: [PATCH 0359/3953] Bump pyobihai to fix issue with user account (#26736) --- homeassistant/components/obihai/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index b6bad10d608096..e7706b0435ceec 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -3,7 +3,7 @@ "name": "Obihai", "documentation": "https://www.home-assistant.io/components/obihai", "requirements": [ - "pyobihai==1.0.2" + "pyobihai==1.1.0" ], "dependencies": [], "codeowners": ["@dshokouhi"] diff --git a/requirements_all.txt b/requirements_all.txt index e6ac06e9199fc5..e67a7a00c79d88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1351,7 +1351,7 @@ pynx584==0.4 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.0.2 +pyobihai==1.1.0 # homeassistant.components.openuv pyopenuv==1.0.9 From 246a611a7c7c39c3f386a1c10daa14f513429362 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 23:05:02 +0200 Subject: [PATCH 0360/3953] Bump aiohttp to 3.6.1 (#26739) --- 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 0a5cfbfc67d15a..746485f2ece2da 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiohttp==3.6.0 +aiohttp==3.6.1 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index e67a7a00c79d88..8cbf9230ec0fc6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.6.0 +aiohttp==3.6.1 astral==1.10.1 async_timeout==3.0.1 attrs==19.1.0 diff --git a/setup.py b/setup.py index 8be458fc449c16..e6776d8a1a0811 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.6.0", + "aiohttp==3.6.1", "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.1.0", From 120c8bad502a25ea5b2fcd4a7b98a6bb32a63958 Mon Sep 17 00:00:00 2001 From: Andrew Rowson Date: Thu, 19 Sep 2019 11:51:49 +0100 Subject: [PATCH 0361/3953] Encode prometheus metric names per the prom spec (#26639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Referencing issue #26418. Prometheus metric names can only contain chars a-zA-Z0-9, : and _ (https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels). HA currently generates invalid prometheus names, e.g. if the unit for a sensor is a non-ASCII character containing ° or μ. To resolve, we need to sanitize the name before creating, replacing non-valid characters with a valid representation. In this case, I've used "u{unicode-hex-code}". Also updated the test case to make sure that the ° case is handled. --- homeassistant/components/prometheus/__init__.py | 16 +++++++++++++++- tests/components/prometheus/test_init.py | 11 +++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 1ba2c4809b6618..82db5f6725f512 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -1,5 +1,6 @@ """Support for Prometheus metrics export.""" import logging +import string from aiohttp import web import voluptuous as vol @@ -159,10 +160,23 @@ def _metric(self, metric, factory, documentation, labels=None): try: return self._metrics[metric] except KeyError: - full_metric_name = f"{self.metrics_prefix}{metric}" + full_metric_name = self._sanitize_metric_name( + f"{self.metrics_prefix}{metric}" + ) self._metrics[metric] = factory(full_metric_name, documentation, labels) return self._metrics[metric] + @staticmethod + def _sanitize_metric_name(metric: str) -> str: + return "".join( + [ + c + if c in string.ascii_letters or c.isdigit() or c == "_" or c == ":" + else f"u{hex(ord(c))}" + for c in metric + ] + ) + @staticmethod def state_as_number(state): """Return a state casted to a float.""" diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 9e313fd3694583..4ec40731c5d206 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -41,6 +41,11 @@ async def prometheus_client(loop, hass, hass_client): sensor3.entity_id = "sensor.electricity_price" await sensor3.async_update_ha_state() + sensor4 = DemoSensor("Wind Direction", 25, None, "°", None) + sensor4.hass = hass + sensor4.entity_id = "sensor.wind_direction" + await sensor4.async_update_ha_state() + return await hass_client() @@ -103,3 +108,9 @@ def test_view(prometheus_client): # pylint: disable=redefined-outer-name 'entity="sensor.electricity_price",' 'friendly_name="Electricity price"} 0.123' in body ) + + assert ( + 'sensor_unit_u0xb0{domain="sensor",' + 'entity="sensor.wind_direction",' + 'friendly_name="Wind Direction"} 25.0' in body + ) From 66405d5651c18f3ad0be52c4ec2ed79d0b33048b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Sep 2019 11:49:47 -0700 Subject: [PATCH 0362/3953] Bump TRADFRI (#26731) * Bump TRADFRI * Fix test --- homeassistant/components/tradfri/manifest.json | 12 +++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tradfri/test_light.py | 4 +++- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index ba6b21e00283ab..d847c6df24f7cb 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,17 +3,11 @@ "name": "Tradfri", "config_flow": true, "documentation": "https://www.home-assistant.io/components/tradfri", - "requirements": [ - "pytradfri[async]==6.0.1" - ], + "requirements": ["pytradfri[async]==6.3.1"], "homekit": { - "models": [ - "TRADFRI" - ] + "models": ["TRADFRI"] }, "dependencies": [], "zeroconf": ["_coap._udp.local."], - "codeowners": [ - "@ggravlingen" - ] + "codeowners": ["@ggravlingen"] } diff --git a/requirements_all.txt b/requirements_all.txt index d880b672d723f4..ffe2514af6085d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1590,7 +1590,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==6.0.1 +pytradfri[async]==6.3.1 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69ca7eefe03a52..049ff0dbd2c144 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ python-velbus==2.0.27 python_awair==0.0.4 # homeassistant.components.tradfri -pytradfri[async]==6.0.1 +pytradfri[async]==6.3.1 # homeassistant.components.vesync pyvesync==1.1.0 diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 69fe3bae618dab..4c691f66af872c 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -4,7 +4,9 @@ from unittest.mock import Mock, MagicMock, patch, PropertyMock import pytest -from pytradfri.device import Device, LightControl, Light +from pytradfri.device import Device +from pytradfri.device.light import Light +from pytradfri.device.light_control import LightControl from homeassistant.components import tradfri From 3551c39bad2ad9179b010353a1b697330950de34 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Thu, 19 Sep 2019 13:27:18 -0700 Subject: [PATCH 0363/3953] Bump pyobihai to fix issue with user account (#26736) --- homeassistant/components/obihai/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index b6bad10d608096..e7706b0435ceec 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -3,7 +3,7 @@ "name": "Obihai", "documentation": "https://www.home-assistant.io/components/obihai", "requirements": [ - "pyobihai==1.0.2" + "pyobihai==1.1.0" ], "dependencies": [], "codeowners": ["@dshokouhi"] diff --git a/requirements_all.txt b/requirements_all.txt index ffe2514af6085d..4eb496ae8840fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1345,7 +1345,7 @@ pynws==0.7.4 pynx584==0.4 # homeassistant.components.obihai -pyobihai==1.0.2 +pyobihai==1.1.0 # homeassistant.components.openuv pyopenuv==1.0.9 From 2d12bac0e239c654077d8ab02c51f0a455703624 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 19 Sep 2019 16:29:26 -0500 Subject: [PATCH 0364/3953] Add Plex config flow support (#26548) * Add config flow support * Log error on failed connection * Review comments * Unused errors * Move form to step * Use instance var instead of passing argument * Only share servers created by component * Return errors early to avoid try:else * Separate debug for validation vs setup * Unnecessary * Unnecessary checks * Combine import flows, move logic to component * Use config entry discovery handler * Temporary lint fix * Filter out servers already configured * Remove manual config flow * Skip discovery if a config exists * Swap conditional to reduce indenting * Only discover when no configs created or creating * Un-nest function * Proper async use * Move legacy file import to discovery * Fix, bad else * Separate validate step * Unused without manual setup step * Async oops * First attempt at tests * Test cleanup * Full test coverage for config_flow, enable tests * Lint * Fix lint vs black * Add test init * Add test package requirement * Actually run script * Use 'not None' convention * Group exceptions by result * Improve logic, add new error and test * Test cleanup * Add more asserts --- .coveragerc | 5 +- .../components/discovery/__init__.py | 2 +- homeassistant/components/plex/__init__.py | 205 +++----- homeassistant/components/plex/config_flow.py | 171 +++++++ homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/errors.py | 14 + homeassistant/components/plex/manifest.json | 3 +- homeassistant/components/plex/media_player.py | 30 +- homeassistant/components/plex/sensor.py | 23 +- homeassistant/components/plex/server.py | 16 +- homeassistant/components/plex/strings.json | 33 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/plex/__init__.py | 1 + tests/components/plex/mock_classes.py | 35 ++ tests/components/plex/test_config_flow.py | 454 ++++++++++++++++++ 17 files changed, 831 insertions(+), 167 deletions(-) create mode 100644 homeassistant/components/plex/config_flow.py create mode 100644 homeassistant/components/plex/errors.py create mode 100644 homeassistant/components/plex/strings.json create mode 100644 tests/components/plex/__init__.py create mode 100644 tests/components/plex/mock_classes.py create mode 100644 tests/components/plex/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 824fb3828f2d32..0e4199bb09770e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -479,7 +479,10 @@ omit = homeassistant/components/pioneer/media_player.py homeassistant/components/pjlink/media_player.py homeassistant/components/plaato/* - homeassistant/components/plex/* + homeassistant/components/plex/__init__.py + homeassistant/components/plex/media_player.py + homeassistant/components/plex/sensor.py + homeassistant/components/plex/server.py homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 827e05a424be85..15fcfc15338da2 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -50,6 +50,7 @@ SERVICE_DAIKIN: "daikin", SERVICE_TELLDUSLIVE: "tellduslive", SERVICE_IGD: "upnp", + SERVICE_PLEX: "plex", } SERVICE_HANDLERS = { @@ -69,7 +70,6 @@ SERVICE_FREEBOX: ("freebox", None), SERVICE_YEELIGHT: ("yeelight", None), "panasonic_viera": ("media_player", "panasonic_viera"), - SERVICE_PLEX: ("plex", None), "yamaha": ("media_player", "yamaha"), "logitech_mediaserver": ("media_player", "squeezebox"), "directv": ("media_player", "directv"), diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 69e77c8854fef1..665091d69b9b1b 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -5,7 +5,7 @@ import requests.exceptions import voluptuous as vol -from homeassistant.components.discovery import SERVICE_PLEX +from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( CONF_HOST, @@ -16,20 +16,18 @@ CONF_VERIFY_SSL, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.util.json import load_json, save_json from .const import ( - CONF_SERVER, CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, + CONF_SERVER, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN as PLEX_DOMAIN, PLATFORMS, - PLEX_CONFIG_FILE, PLEX_MEDIA_PLAYER_OPTIONS, + PLEX_SERVER_CONFIG, SERVERS, ) from .server import PlexServer @@ -58,151 +56,76 @@ CONFIG_SCHEMA = vol.Schema({PLEX_DOMAIN: SERVER_CONFIG_SCHEMA}, extra=vol.ALLOW_EXTRA) -CONFIGURING = "configuring" _LOGGER = logging.getLogger(__package__) def setup(hass, config): """Set up the Plex component.""" + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}}) - def server_discovered(service, info): - """Pass back discovered Plex server details.""" - if hass.data[PLEX_DOMAIN][SERVERS]: - _LOGGER.debug("Plex server already configured, ignoring discovery.") - return - _LOGGER.debug("Discovered Plex server: %s:%s", info["host"], info["port"]) - setup_plex(discovery_info=info) - - def setup_plex(config=None, discovery_info=None, configurator_info=None): - """Return assembled server_config dict.""" - json_file = hass.config.path(PLEX_CONFIG_FILE) - file_config = load_json(json_file) - host_and_port = None - - if config: - server_config = config - if CONF_HOST in server_config: - host_and_port = ( - f"{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" - ) - if MP_DOMAIN in server_config: - hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN) - elif file_config: - _LOGGER.debug("Loading config from %s", json_file) - host_and_port, server_config = file_config.popitem() - server_config[CONF_VERIFY_SSL] = server_config.pop("verify") - elif discovery_info: - server_config = {} - host_and_port = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" - elif configurator_info: - server_config = configurator_info - host_and_port = server_config["host_and_port"] - else: - discovery.listen(hass, SERVICE_PLEX, server_discovered) - return True - - if host_and_port: - use_ssl = server_config.get(CONF_SSL, DEFAULT_SSL) - http_prefix = "https" if use_ssl else "http" - server_config[CONF_URL] = f"{http_prefix}://{host_and_port}" - - plex_server = PlexServer(server_config) - try: - plex_server.connect() - except requests.exceptions.ConnectionError as error: - _LOGGER.error( - "Plex server could not be reached, please verify host and port: [%s]", - error, - ) - return False - except ( - plexapi.exceptions.BadRequest, - plexapi.exceptions.Unauthorized, - plexapi.exceptions.NotFound, - ) as error: - _LOGGER.error( - "Connection to Plex server failed, please verify token and SSL settings: [%s]", - error, - ) - request_configuration(host_and_port) - return False - else: - hass.data[PLEX_DOMAIN][SERVERS][ - plex_server.machine_identifier - ] = plex_server - - if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: - request_id = hass.data[PLEX_DOMAIN][CONFIGURING].pop(host_and_port) - configurator = hass.components.configurator - configurator.request_done(request_id) - _LOGGER.debug("Discovery configuration done") - if configurator_info: - # Write plex.conf if created via discovery/configurator - save_json( - hass.config.path(PLEX_CONFIG_FILE), - { - host_and_port: { - CONF_TOKEN: server_config[CONF_TOKEN], - CONF_SSL: use_ssl, - "verify": server_config[CONF_VERIFY_SSL], - } - }, - ) - - if not hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS): - hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = MEDIA_PLAYER_SCHEMA({}) - - for platform in PLATFORMS: - hass.helpers.discovery.load_platform( - platform, PLEX_DOMAIN, {}, original_config - ) - - return True - - def request_configuration(host_and_port): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: - configurator.notify_errors( - hass.data[PLEX_DOMAIN][CONFIGURING][host_and_port], - "Failed to register, please try again.", - ) - return - - def plex_configuration_callback(data): - """Handle configuration changes.""" - config = { - "host_and_port": host_and_port, - CONF_TOKEN: data.get("token"), - CONF_SSL: cv.boolean(data.get("ssl")), - CONF_VERIFY_SSL: cv.boolean(data.get("verify_ssl")), - } - setup_plex(configurator_info=config) - - hass.data[PLEX_DOMAIN][CONFIGURING][ - host_and_port - ] = configurator.request_config( - "Plex Media Server", - plex_configuration_callback, - description="Enter the X-Plex-Token", - entity_picture="/static/images/logo_plex_mediaserver.png", - submit_caption="Confirm", - fields=[ - {"id": "token", "name": "X-Plex-Token", "type": ""}, - {"id": "ssl", "name": "Use SSL", "type": ""}, - {"id": "verify_ssl", "name": "Verify SSL", "type": ""}, - ], + plex_config = config.get(PLEX_DOMAIN, {}) + if plex_config: + _setup_plex(hass, plex_config) + + return True + + +def _setup_plex(hass, config): + """Pass configuration to a config flow.""" + server_config = dict(config) + if MP_DOMAIN in server_config: + hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN) + if CONF_HOST in server_config: + prefix = "https" if server_config.pop(CONF_SSL) else "http" + server_config[ + CONF_URL + ] = f"{prefix}://{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" + hass.async_create_task( + hass.config_entries.flow.async_init( + PLEX_DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=server_config, ) + ) - # End of inner functions. - - original_config = config - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, CONFIGURING: {}}) +async def async_setup_entry(hass, entry): + """Set up Plex from a config entry.""" + server_config = entry.data[PLEX_SERVER_CONFIG] - if hass.data[PLEX_DOMAIN][SERVERS]: - _LOGGER.debug("Plex server already configured") + plex_server = PlexServer(server_config) + try: + await hass.async_add_executor_job(plex_server.connect) + except requests.exceptions.ConnectionError as error: + _LOGGER.error( + "Plex server (%s) could not be reached: [%s]", + server_config[CONF_URL], + error, + ) + return False + except ( + plexapi.exceptions.BadRequest, + plexapi.exceptions.Unauthorized, + plexapi.exceptions.NotFound, + ) as error: + _LOGGER.error( + "Login to %s failed, verify token and SSL settings: [%s]", + server_config[CONF_SERVER], + error, + ) return False - plex_config = config.get(PLEX_DOMAIN, {}) - return setup_plex(config=plex_config) + _LOGGER.debug( + "Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use + ) + hass.data[PLEX_DOMAIN][SERVERS][plex_server.machine_identifier] = plex_server + + if not hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS): + hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = MEDIA_PLAYER_SCHEMA({}) + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py new file mode 100644 index 00000000000000..3c683c802f5ccf --- /dev/null +++ b/homeassistant/components/plex/config_flow.py @@ -0,0 +1,171 @@ +"""Config flow for Plex.""" +import logging + +import plexapi.exceptions +import requests.exceptions +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.core import callback +from homeassistant.util.json import load_json + +from .const import ( # pylint: disable=unused-import + CONF_SERVER, + CONF_SERVER_IDENTIFIER, + DEFAULT_VERIFY_SSL, + DOMAIN, + PLEX_CONFIG_FILE, + PLEX_SERVER_CONFIG, +) +from .errors import NoServersFound, ServerNotSpecified +from .server import PlexServer + +USER_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): str}) + +_LOGGER = logging.getLogger(__package__) + + +@callback +def configured_servers(hass): + """Return a set of the configured Plex servers.""" + return set( + entry.data[CONF_SERVER_IDENTIFIER] + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Plex config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the Plex flow.""" + self.current_login = {} + self.available_servers = None + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is not None: + return await self.async_step_server_validate(user_input) + + return self.async_show_form(step_id="user", data_schema=USER_SCHEMA, errors={}) + + async def async_step_server_validate(self, server_config): + """Validate a provided configuration.""" + errors = {} + self.current_login = server_config + + plex_server = PlexServer(server_config) + try: + await self.hass.async_add_executor_job(plex_server.connect) + + except NoServersFound: + errors["base"] = "no_servers" + except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): + _LOGGER.error("Invalid credentials provided, config not created") + errors["base"] = "faulty_credentials" + except (plexapi.exceptions.NotFound, requests.exceptions.ConnectionError): + _LOGGER.error( + "Plex server could not be reached: %s", server_config[CONF_URL] + ) + errors["base"] = "not_found" + + except ServerNotSpecified as available_servers: + self.available_servers = available_servers.args[0] + return await self.async_step_select_server() + + except Exception as error: # pylint: disable=broad-except + _LOGGER.error("Unknown error connecting to Plex server: %s", error) + return self.async_abort(reason="unknown") + + if errors: + return self.async_show_form( + step_id="user", data_schema=USER_SCHEMA, errors=errors + ) + + server_id = plex_server.machine_identifier + + for entry in self._async_current_entries(): + if entry.data[CONF_SERVER_IDENTIFIER] == server_id: + return self.async_abort(reason="already_configured") + + url = plex_server.url_in_use + token = server_config.get(CONF_TOKEN) + + entry_config = {CONF_URL: url} + if token: + entry_config[CONF_TOKEN] = token + if url.startswith("https"): + entry_config[CONF_VERIFY_SSL] = server_config.get( + CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL + ) + + _LOGGER.debug("Valid config created for %s", plex_server.friendly_name) + + return self.async_create_entry( + title=plex_server.friendly_name, + data={ + CONF_SERVER: plex_server.friendly_name, + CONF_SERVER_IDENTIFIER: server_id, + PLEX_SERVER_CONFIG: entry_config, + }, + ) + + async def async_step_select_server(self, user_input=None): + """Use selected Plex server.""" + config = dict(self.current_login) + if user_input is not None: + config[CONF_SERVER] = user_input[CONF_SERVER] + return await self.async_step_server_validate(config) + + configured = configured_servers(self.hass) + available_servers = [ + name + for (name, server_id) in self.available_servers + if server_id not in configured + ] + + if not available_servers: + return self.async_abort(reason="all_configured") + if len(available_servers) == 1: + config[CONF_SERVER] = available_servers[0] + return await self.async_step_server_validate(config) + + return self.async_show_form( + step_id="select_server", + data_schema=vol.Schema( + {vol.Required(CONF_SERVER): vol.In(available_servers)} + ), + errors={}, + ) + + async def async_step_discovery(self, discovery_info): + """Set default host and port from discovery.""" + if self._async_current_entries() or self._async_in_progress(): + # Skip discovery if a config already exists or is in progress. + return self.async_abort(reason="already_configured") + + json_file = self.hass.config.path(PLEX_CONFIG_FILE) + file_config = await self.hass.async_add_executor_job(load_json, json_file) + + if file_config: + host_and_port, host_config = file_config.popitem() + prefix = "https" if host_config[CONF_SSL] else "http" + + server_config = { + CONF_URL: f"{prefix}://{host_and_port}", + CONF_TOKEN: host_config[CONF_TOKEN], + CONF_VERIFY_SSL: host_config["verify"], + } + _LOGGER.info("Imported legacy config, file can be removed: %s", json_file) + return await self.async_step_server_validate(server_config) + + return await self.async_step_user() + + async def async_step_import(self, import_config): + """Import from Plex configuration.""" + _LOGGER.debug("Imported Plex configuration") + return await self.async_step_server_validate(import_config) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 6f19623c809e86..e77ac303bf1a25 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -14,5 +14,6 @@ PLEX_SERVER_CONFIG = "server_config" CONF_SERVER = "server" +CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" CONF_SHOW_ALL_CONTROLS = "show_all_controls" diff --git a/homeassistant/components/plex/errors.py b/homeassistant/components/plex/errors.py new file mode 100644 index 00000000000000..11c15404f4505c --- /dev/null +++ b/homeassistant/components/plex/errors.py @@ -0,0 +1,14 @@ +"""Errors for the Plex component.""" +from homeassistant.exceptions import HomeAssistantError + + +class PlexException(HomeAssistantError): + """Base class for Plex exceptions.""" + + +class NoServersFound(PlexException): + """No servers found on Plex account.""" + + +class ServerNotSpecified(PlexException): + """Multiple servers linked to account without choice provided.""" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 4269400dc2456e..94d990952a684e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -1,11 +1,12 @@ { "domain": "plex", "name": "Plex", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/plex", "requirements": [ "plexapi==3.0.6" ], - "dependencies": ["configurator"], + "dependencies": [], "codeowners": [ "@jjlawren" ] diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index cfc63948bee80c..bc19ff41dfedff 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -35,26 +35,40 @@ from .const import ( CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, + CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, PLEX_MEDIA_PLAYER_OPTIONS, SERVERS, ) -SERVER_SETUP = "server_setup" - -_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities_callback, discovery_info=None): - """Set up the Plex platform.""" - if discovery_info is None: - return +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Plex media_player platform. + + Deprecated. + """ + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Plex media_player from a config entry.""" + + def add_entities(entities, update_before_add=False): + """Sync version of async add entities.""" + hass.add_job(async_add_entities, entities, update_before_add) + + hass.async_add_executor_job(_setup_platform, hass, config_entry, add_entities) + - plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] +def _setup_platform(hass, config_entry, add_entities_callback): + """Set up the Plex media_player platform.""" + server_id = config_entry.data[CONF_SERVER_IDENTIFIER] config = hass.data[PLEX_MEDIA_PLAYER_OPTIONS] + plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] plex_clients = {} plex_sessions = {} track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10)) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index f469e95da808e0..7d5b54356a0c82 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -8,21 +8,26 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import DOMAIN as PLEX_DOMAIN, SERVERS +from .const import CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, SERVERS -DEFAULT_NAME = "Plex" _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Plex sensor.""" - if discovery_info is None: - return +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Plex sensor platform. - plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] - add_entities([PlexSensor(plexserver)], True) + Deprecated. + """ + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Plex sensor from a config entry.""" + server_id = config_entry.data[CONF_SERVER_IDENTIFIER] + sensor = PlexSensor(hass.data[PLEX_DOMAIN][SERVERS][server_id]) + async_add_entities([sensor], True) class PlexSensor(Entity): @@ -30,10 +35,10 @@ class PlexSensor(Entity): def __init__(self, plex_server): """Initialize the sensor.""" - self._name = DEFAULT_NAME self._state = None self._now_playing = [] self._server = plex_server + self._name = f"Plex ({plex_server.friendly_name})" self._unique_id = f"sensor-{plex_server.machine_identifier}" @property diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 962e074996f5e5..f41a9bdabae183 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,6 +1,4 @@ """Shared class to maintain Plex server instances.""" -import logging - import plexapi.myplex import plexapi.server from requests import Session @@ -8,8 +6,7 @@ from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from .const import CONF_SERVER, DEFAULT_VERIFY_SSL - -_LOGGER = logging.getLogger(__package__) +from .errors import NoServersFound, ServerNotSpecified class PlexServer: @@ -29,8 +26,16 @@ def connect(self): def _set_missing_url(): account = plexapi.myplex.MyPlexAccount(token=self._token) available_servers = [ - x.name for x in account.resources() if "server" in x.provides + (x.name, x.clientIdentifier) + for x in account.resources() + if "server" in x.provides ] + + if not available_servers: + raise NoServersFound + if not self._server_name and len(available_servers) > 1: + raise ServerNotSpecified(available_servers) + server_choice = ( self._server_name if self._server_name else available_servers[0] ) @@ -47,7 +52,6 @@ def _connect_with_url(): self._plex_server = plexapi.server.PlexServer( self._url, self._token, session ) - _LOGGER.debug("Connected to: %s (%s)", self.friendly_name, self.url_in_use) if self._token and not self._url: _set_missing_url() diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json new file mode 100644 index 00000000000000..396a3387fee295 --- /dev/null +++ b/homeassistant/components/plex/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "title": "Plex", + "step": { + "select_server": { + "title": "Select Plex server", + "description": "Multiple servers available, select one:", + "data": { + "server": "Server" + } + }, + "user": { + "title": "Connect Plex server", + "description": "Enter a Plex token for automatic setup.", + "data": { + "token": "Plex token" + } + } + }, + "error": { + "faulty_credentials": "Authorization failed", + "no_servers": "No servers linked to account", + "not_found": "Plex server not found" + }, + "abort": { + "all_configured": "All linked servers already configured", + "already_configured": "This Plex server is already configured", + "already_in_progress": "Plex is being configured", + "invalid_import": "Imported configuration is invalid", + "unknown": "Failed for unknown reason" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7f3f5c1f20d23b..9ddae5acdb9941 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -45,6 +45,7 @@ "openuv", "owntracks", "plaato", + "plex", "point", "ps4", "rainmachine", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83ec2d1d2c1023..ef8618f146bbb3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -249,6 +249,9 @@ pexpect==4.6.0 # homeassistant.components.pilight pilight==0.1.1 +# homeassistant.components.plex +plexapi==3.0.6 + # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ff2943a583b3f8..72fb9ff5a44233 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -110,6 +110,7 @@ "paho-mqtt", "pexpect", "pilight", + "plexapi", "pmsensor", "prometheus_client", "ptvsd", diff --git a/tests/components/plex/__init__.py b/tests/components/plex/__init__.py new file mode 100644 index 00000000000000..9c9c00d87ace68 --- /dev/null +++ b/tests/components/plex/__init__.py @@ -0,0 +1 @@ +"""Tests for the Plex component.""" diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py new file mode 100644 index 00000000000000..d027087828073c --- /dev/null +++ b/tests/components/plex/mock_classes.py @@ -0,0 +1,35 @@ +"""Mock classes used in tests.""" + +MOCK_HOST_1 = "1.2.3.4" +MOCK_PORT_1 = "32400" +MOCK_HOST_2 = "4.3.2.1" +MOCK_PORT_2 = "32400" + + +class MockAvailableServer: # pylint: disable=too-few-public-methods + """Mock avilable server objects.""" + + def __init__(self, name, client_id): + """Initialize the object.""" + self.name = name + self.clientIdentifier = client_id # pylint: disable=invalid-name + self.provides = ["server"] + + +class MockConnection: # pylint: disable=too-few-public-methods + """Mock a single account resource connection object.""" + + def __init__(self, ssl): + """Initialize the object.""" + prefix = "https" if ssl else "http" + self.httpuri = f"{prefix}://{MOCK_HOST_1}:{MOCK_PORT_1}" + self.uri = "{prefix}://{MOCK_HOST_2}:{MOCK_PORT_2}" + self.local = True + + +class MockConnections: # pylint: disable=too-few-public-methods + """Mock a list of resource connections.""" + + def __init__(self, ssl=False): + """Initialize the object.""" + self.connections = [MockConnection(ssl)] diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py new file mode 100644 index 00000000000000..9c9c1b625259f9 --- /dev/null +++ b/tests/components/plex/test_config_flow.py @@ -0,0 +1,454 @@ +"""Tests for Plex config flow.""" +from unittest.mock import MagicMock, Mock, patch, PropertyMock +import plexapi.exceptions +import requests.exceptions + +from homeassistant.components.plex import config_flow +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL + +from tests.common import MockConfigEntry + +from .mock_classes import MOCK_HOST_1, MOCK_PORT_1, MockAvailableServer, MockConnections + +MOCK_NAME_1 = "Plex Server 1" +MOCK_ID_1 = "unique_id_123" +MOCK_NAME_2 = "Plex Server 2" +MOCK_ID_2 = "unique_id_456" +MOCK_TOKEN = "secret_token" +MOCK_FILE_CONTENTS = { + f"{MOCK_HOST_1}:{MOCK_PORT_1}": {"ssl": False, "token": MOCK_TOKEN, "verify": True} +} +MOCK_SERVER_1 = MockAvailableServer(MOCK_NAME_1, MOCK_ID_1) +MOCK_SERVER_2 = MockAvailableServer(MOCK_NAME_2, MOCK_ID_2) + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.PlexFlowHandler() + flow.hass = hass + return flow + + +async def test_bad_credentials(hass): + """Test when provided credentials are rejected.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch( + "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized + ): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"]["base"] == "faulty_credentials" + + +async def test_import_file_from_discovery(hass): + """Test importing a legacy file during discovery.""" + + file_host_and_port, file_config = list(MOCK_FILE_CONTENTS.items())[0] + used_url = f"http://{file_host_and_port}" + + with patch("plexapi.server.PlexServer") as mock_plex_server, patch( + "homeassistant.components.plex.config_flow.load_json", + return_value=MOCK_FILE_CONTENTS, + ): + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_ID_1 + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_NAME_1 + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=used_url) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "discovery"}, + data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_NAME_1 + assert result["data"][config_flow.CONF_SERVER] == MOCK_NAME_1 + assert result["data"][config_flow.CONF_SERVER_IDENTIFIER] == MOCK_ID_1 + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] == used_url + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] + == file_config[CONF_TOKEN] + ) + + +async def test_discovery(hass): + """Test starting a flow from discovery.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "discovery"}, + data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + +async def test_discovery_while_in_progress(hass): + """Test starting a flow from discovery.""" + + await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "discovery"}, + data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_import_success(hass): + """Test a successful configuration import.""" + + mock_connections = MockConnections(ssl=True) + + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.server.PlexServer") as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "import"}, + data={ + CONF_TOKEN: MOCK_TOKEN, + CONF_URL: f"https://{MOCK_HOST_1}:{MOCK_PORT_1}", + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_SERVER_1.name + assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == MOCK_SERVER_1.clientIdentifier + ) + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] + == mock_connections.connections[0].httpuri + ) + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_import_bad_hostname(hass): + """Test when an invalid address is provided.""" + + with patch( + "plexapi.server.PlexServer", side_effect=requests.exceptions.ConnectionError + ): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "import"}, + data={ + CONF_TOKEN: MOCK_TOKEN, + CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"]["base"] == "not_found" + + +async def test_unknown_exception(hass): + """Test when an unknown exception is encountered.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "user"}, + data={CONF_TOKEN: MOCK_TOKEN}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "unknown" + + +async def test_no_servers_found(hass): + """Test when no servers are on an account.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[]) + + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"]["base"] == "no_servers" + + +async def test_single_available_server(hass): + """Test creating an entry with one server available.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + mock_connections = MockConnections() + + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( + "plexapi.server.PlexServer" + ) as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_SERVER_1.name + assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == MOCK_SERVER_1.clientIdentifier + ) + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] + == mock_connections.connections[0].httpuri + ) + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_multiple_servers_with_selection(hass): + """Test creating an entry with multiple servers available.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( + "plexapi.server.PlexServer" + ) as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "form" + assert result["step_id"] == "select_server" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={config_flow.CONF_SERVER: MOCK_SERVER_1.name} + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_SERVER_1.name + assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == MOCK_SERVER_1.clientIdentifier + ) + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] + == mock_connections.connections[0].httpuri + ) + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_adding_last_unconfigured_server(hass): + """Test automatically adding last unconfigured server when multiple servers on account.""" + + MockConfigEntry( + domain=config_flow.DOMAIN, + data={ + config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2, + config_flow.CONF_SERVER: MOCK_NAME_2, + }, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( + "plexapi.server.PlexServer" + ) as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_SERVER_1.name + assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == MOCK_SERVER_1.clientIdentifier + ) + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] + == mock_connections.connections[0].httpuri + ) + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_already_configured(hass): + """Test a duplicated successful flow.""" + + flow = init_config_flow(hass) + MockConfigEntry( + domain=config_flow.DOMAIN, data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1} + ).add_to_hass(hass) + + mock_connections = MockConnections() + + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.server.PlexServer") as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + result = await flow.async_step_import( + {CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}"} + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_all_available_servers_configured(hass): + """Test when all available servers are already configured.""" + + MockConfigEntry( + domain=config_flow.DOMAIN, + data={ + config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1, + config_flow.CONF_SERVER: MOCK_NAME_1, + }, + ).add_to_hass(hass) + + MockConfigEntry( + domain=config_flow.DOMAIN, + data={ + config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2, + config_flow.CONF_SERVER: MOCK_NAME_2, + }, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) + mm_plex_account.resource = Mock(return_value=mock_connections) + + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + ) + + assert result["type"] == "abort" + assert result["reason"] == "all_configured" From 8439329b04ee8284236d72e3aaa78b454f0a6fba Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Sep 2019 14:29:43 -0700 Subject: [PATCH 0365/3953] Bumped version to 0.99.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7013242676df70..c2b5a0d375b411 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 99 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From c8fb7ce98b0cabaccfd62135652998c5bb434622 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 23:30:25 +0200 Subject: [PATCH 0366/3953] Bump restrictedpython to 5.0 (#26741) --- homeassistant/components/python_script/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 610ec92a2b3939..83d70830b11a43 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -3,8 +3,8 @@ "name": "Python script", "documentation": "https://www.home-assistant.io/components/python_script", "requirements": [ - "restrictedpython==4.0" + "restrictedpython==5.0" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 8cbf9230ec0fc6..99d81158edbae5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1666,7 +1666,7 @@ recollect-waste==1.0.1 regenmaschine==1.5.1 # homeassistant.components.python_script -restrictedpython==4.0 +restrictedpython==5.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef8618f146bbb3..3d0d42cc8472bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -371,7 +371,7 @@ pywebpush==1.9.2 regenmaschine==1.5.1 # homeassistant.components.python_script -restrictedpython==4.0 +restrictedpython==5.0 # homeassistant.components.rflink rflink==0.0.46 From b68b8430a4220a2b408fdbae55d9e7f8e97fcd84 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Fri, 20 Sep 2019 05:31:54 +0800 Subject: [PATCH 0367/3953] Izone component (#24550) * iZone component * Rename constants to const. * Changes as per code review. * Stop listening if discovery times out. * Unload properly * Changes as per code review * Climate 1.0 * Use dispatcher instead of listener * Free air settings * Test case for config flow. * Changes as per code review * Fix error on shutdown * Changes as per code review * Lint fix * Black formatting * Black on test * Fix test * Lint fix * Formatting * Updated requirements * Remaining patches * Per code r/v --- .coveragerc | 3 + CODEOWNERS | 1 + .../components/izone/.translations/en.json | 15 + homeassistant/components/izone/__init__.py | 67 +++ homeassistant/components/izone/climate.py | 546 ++++++++++++++++++ homeassistant/components/izone/config_flow.py | 45 ++ homeassistant/components/izone/const.py | 14 + homeassistant/components/izone/discovery.py | 87 +++ homeassistant/components/izone/manifest.json | 9 + homeassistant/components/izone/strings.json | 15 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/izone/__init__.py | 1 + tests/components/izone/test_config_flow.py | 83 +++ 16 files changed, 894 insertions(+) create mode 100644 homeassistant/components/izone/.translations/en.json create mode 100644 homeassistant/components/izone/__init__.py create mode 100644 homeassistant/components/izone/climate.py create mode 100644 homeassistant/components/izone/config_flow.py create mode 100644 homeassistant/components/izone/const.py create mode 100644 homeassistant/components/izone/discovery.py create mode 100644 homeassistant/components/izone/manifest.json create mode 100644 homeassistant/components/izone/strings.json create mode 100644 tests/components/izone/__init__.py create mode 100644 tests/components/izone/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0e4199bb09770e..5d5b0f6c81b09b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -295,6 +295,9 @@ omit = homeassistant/components/iaqualink/sensor.py homeassistant/components/iaqualink/switch.py homeassistant/components/icloud/device_tracker.py + homeassistant/components/izone/climate.py + homeassistant/components/izone/discovery.py + homeassistant/components/izone/__init__.py homeassistant/components/idteck_prox/* homeassistant/components/ifttt/* homeassistant/components/iglo/light.py diff --git a/CODEOWNERS b/CODEOWNERS index c454514912ca51..19dd0d5c8b69f3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -144,6 +144,7 @@ homeassistant/components/ios/* @robbiet480 homeassistant/components/ipma/* @dgomes homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 +homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/keba/* @dannerph homeassistant/components/knx/* @Julius2342 diff --git a/homeassistant/components/izone/.translations/en.json b/homeassistant/components/izone/.translations/en.json new file mode 100644 index 00000000000000..5293ad2a1fec34 --- /dev/null +++ b/homeassistant/components/izone/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No iZone devices found on the network.", + "single_instance_allowed": "Only a single configuration of iZone is necessary." + }, + "step": { + "confirm": { + "description": "Do you want to set up iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py new file mode 100644 index 00000000000000..7f80fb077cf92c --- /dev/null +++ b/homeassistant/components/izone/__init__.py @@ -0,0 +1,67 @@ +""" +Platform for the iZone AC. + +For more details about this component, please refer to the documentation +https://home-assistant.io/components/izone/ +""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EXCLUDE +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import IZONE, DATA_CONFIG +from .discovery import async_start_discovery_service, async_stop_discovery_service + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + IZONE: vol.Schema( + { + vol.Optional(CONF_EXCLUDE, default=[]): vol.All( + cv.ensure_list, [cv.string] + ) + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Register the iZone component config.""" + conf = config.get(IZONE) + if not conf: + return True + + hass.data[DATA_CONFIG] = conf + + # Explicitly added in the config file, create a config entry. + hass.async_create_task( + hass.config_entries.flow.async_init( + IZONE, context={"source": config_entries.SOURCE_IMPORT} + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up from a config entry.""" + await async_start_discovery_service(hass) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "climate") + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload the config entry and stop discovery process.""" + await async_stop_discovery_service(hass) + await hass.config_entries.async_forward_entry_unload(entry, "climate") + return True diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py new file mode 100644 index 00000000000000..c932c66627bcae --- /dev/null +++ b/homeassistant/components/izone/climate.py @@ -0,0 +1,546 @@ +"""Support for the iZone HVAC.""" +import logging +from typing import Optional, List + +from pizone import Zone, Controller + +from homeassistant.core import callback +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + FAN_AUTO, + PRESET_ECO, + PRESET_NONE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_HALVES, + TEMP_CELSIUS, + CONF_EXCLUDE, +) +from homeassistant.helpers.temperature import display_temp as show_temp +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import ( + DATA_DISCOVERY_SERVICE, + IZONE, + DISPATCH_CONTROLLER_DISCOVERED, + DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_RECONNECTED, + DISPATCH_CONTROLLER_UPDATE, + DISPATCH_ZONE_UPDATE, + DATA_CONFIG, +) + +_LOGGER = logging.getLogger(__name__) + +_IZONE_FAN_TO_HA = { + Controller.Fan.LOW: FAN_LOW, + Controller.Fan.MED: FAN_MEDIUM, + Controller.Fan.HIGH: FAN_HIGH, + Controller.Fan.AUTO: FAN_AUTO, +} + + +async def async_setup_entry( + hass: HomeAssistantType, config: ConfigType, async_add_entities +): + """Initialize an IZone Controller.""" + disco = hass.data[DATA_DISCOVERY_SERVICE] + + @callback + def init_controller(ctrl: Controller): + """Register the controller device and the containing zones.""" + conf = hass.data.get(DATA_CONFIG) # type: ConfigType + + # Filter out any entities excluded in the config file + if conf and ctrl.device_uid in conf[CONF_EXCLUDE]: + _LOGGER.info("Controller UID=%s ignored as excluded", ctrl.device_uid) + return + _LOGGER.info("Controller UID=%s discovered", ctrl.device_uid) + + device = ControllerDevice(ctrl) + async_add_entities([device]) + async_add_entities(device.zones.values()) + + # create any components not yet created + for controller in disco.pi_disco.controllers.values(): + init_controller(controller) + + # connect to register any further components + async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, init_controller) + + return True + + +class ControllerDevice(ClimateDevice): + """Representation of iZone Controller.""" + + def __init__(self, controller: Controller) -> None: + """Initialise ControllerDevice.""" + self._controller = controller + + self._supported_features = SUPPORT_FAN_MODE + + if ( + controller.ras_mode == "master" and controller.zone_ctrl == 13 + ) or controller.ras_mode == "RAS": + self._supported_features |= SUPPORT_TARGET_TEMPERATURE + + self._state_to_pizone = { + HVAC_MODE_COOL: Controller.Mode.COOL, + HVAC_MODE_HEAT: Controller.Mode.HEAT, + HVAC_MODE_HEAT_COOL: Controller.Mode.AUTO, + HVAC_MODE_FAN_ONLY: Controller.Mode.VENT, + HVAC_MODE_DRY: Controller.Mode.DRY, + } + if controller.free_air_enabled: + self._supported_features |= SUPPORT_PRESET_MODE + + self._fan_to_pizone = {} + for fan in controller.fan_modes: + self._fan_to_pizone[_IZONE_FAN_TO_HA[fan]] = fan + self._available = True + + self._device_info = { + "identifiers": {(IZONE, self.unique_id)}, + "name": self.name, + "manufacturer": "IZone", + "model": self._controller.sys_type, + } + + # Create the zones + self.zones = {} + for zone in controller.zones: + self.zones[zone] = ZoneDevice(self, zone) + + async def async_added_to_hass(self): + """Call on adding to hass.""" + # Register for connect/disconnect/update events + @callback + def controller_disconnected(ctrl: Controller, ex: Exception) -> None: + """Disconnected from controller.""" + if ctrl is not self._controller: + return + self.set_available(False, ex) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_DISCONNECTED, controller_disconnected + ) + ) + + @callback + def controller_reconnected(ctrl: Controller) -> None: + """Reconnected to controller.""" + if ctrl is not self._controller: + return + self.set_available(True) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_RECONNECTED, controller_reconnected + ) + ) + + @callback + def controller_update(ctrl: Controller) -> None: + """Handle controller data updates.""" + if ctrl is not self._controller: + return + self.async_schedule_update_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_UPDATE, controller_update + ) + ) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @callback + def set_available(self, available: bool, ex: Exception = None) -> None: + """ + Set availability for the controller. + + Also sets zone availability as they follow the same availability. + """ + if self.available == available: + return + + if available: + _LOGGER.info("Reconnected controller %s ", self._controller.device_uid) + else: + _LOGGER.info( + "Controller %s disconnected due to exception: %s", + self._controller.device_uid, + ex, + ) + + self._available = available + self.async_schedule_update_ha_state() + for zone in self.zones.values(): + zone.async_schedule_update_ha_state() + + @property + def device_info(self): + """Return the device info for the iZone system.""" + return self._device_info + + @property + def unique_id(self): + """Return the ID of the controller device.""" + return self._controller.device_uid + + @property + def name(self) -> str: + """Return the name of the entity.""" + return f"iZone Controller {self._controller.device_uid}" + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return self._supported_features + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def precision(self) -> float: + """Return the precision of the system.""" + return PRECISION_HALVES + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + return { + "supply_temperature": show_temp( + self.hass, + self.supply_temperature, + self.temperature_unit, + self.precision, + ), + "temp_setpoint": show_temp( + self.hass, + self._controller.temp_setpoint, + self.temperature_unit, + self.precision, + ), + } + + @property + def hvac_mode(self) -> str: + """Return current operation ie. heat, cool, idle.""" + if not self._controller.is_on: + return HVAC_MODE_OFF + mode = self._controller.mode + for (key, value) in self._state_to_pizone.items(): + if value == mode: + return key + assert False, "Should be unreachable" + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available operation modes.""" + if self._controller.free_air: + return [HVAC_MODE_OFF, HVAC_MODE_FAN_ONLY] + return [HVAC_MODE_OFF, *self._state_to_pizone] + + @property + def preset_mode(self): + """Eco mode is external air.""" + return PRESET_ECO if self._controller.free_air else PRESET_NONE + + @property + def preset_modes(self): + """Available preset modes, normal or eco.""" + if self._controller.free_air_enabled: + return [PRESET_NONE, PRESET_ECO] + return [PRESET_NONE] + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + if self._controller.mode == Controller.Mode.FREE_AIR: + return self._controller.temp_supply + return self._controller.temp_return + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + if not self._supported_features & SUPPORT_TARGET_TEMPERATURE: + return None + return self._controller.temp_setpoint + + @property + def supply_temperature(self) -> float: + """Return the current supply, or in duct, temperature.""" + return self._controller.temp_supply + + @property + def target_temperature_step(self) -> Optional[float]: + """Return the supported step of target temperature.""" + return 0.5 + + @property + def fan_mode(self) -> Optional[str]: + """Return the fan setting.""" + return _IZONE_FAN_TO_HA[self._controller.fan] + + @property + def fan_modes(self) -> Optional[List[str]]: + """Return the list of available fan modes.""" + return list(self._fan_to_pizone) + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return self._controller.temp_min + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return self._controller.temp_max + + async def wrap_and_catch(self, coro): + """Catch any connection errors and set unavailable.""" + try: + await coro + except ConnectionError as ex: + self.set_available(False, ex) + else: + self.set_available(True) + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + if not self.supported_features & SUPPORT_TARGET_TEMPERATURE: + self.async_schedule_update_ha_state(True) + return + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + await self.wrap_and_catch(self._controller.set_temp_setpoint(temp)) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + fan = self._fan_to_pizone[fan_mode] + await self.wrap_and_catch(self._controller.set_fan(fan)) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target operation mode.""" + if hvac_mode == HVAC_MODE_OFF: + await self.wrap_and_catch(self._controller.set_on(False)) + return + if not self._controller.is_on: + await self.wrap_and_catch(self._controller.set_on(True)) + if self._controller.free_air: + return + mode = self._state_to_pizone[hvac_mode] + await self.wrap_and_catch(self._controller.set_mode(mode)) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode.""" + await self.wrap_and_catch( + self._controller.set_free_air(preset_mode == PRESET_ECO) + ) + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + await self.wrap_and_catch(self._controller.set_on(True)) + + +class ZoneDevice(ClimateDevice): + """Representation of iZone Zone.""" + + def __init__(self, controller: ControllerDevice, zone: Zone) -> None: + """Initialise ZoneDevice.""" + self._controller = controller + self._zone = zone + self._name = zone.name.title() + + self._supported_features = 0 + if zone.type != Zone.Type.AUTO: + self._state_to_pizone = { + HVAC_MODE_OFF: Zone.Mode.CLOSE, + HVAC_MODE_FAN_ONLY: Zone.Mode.OPEN, + } + else: + self._state_to_pizone = { + HVAC_MODE_OFF: Zone.Mode.CLOSE, + HVAC_MODE_FAN_ONLY: Zone.Mode.OPEN, + HVAC_MODE_HEAT_COOL: Zone.Mode.AUTO, + } + self._supported_features |= SUPPORT_TARGET_TEMPERATURE + + self._device_info = { + "identifiers": {(IZONE, controller.unique_id, zone.index)}, + "name": self.name, + "manufacturer": "IZone", + "via_device": (IZONE, controller.unique_id), + "model": zone.type.name.title(), + } + + async def async_added_to_hass(self): + """Call on adding to hass.""" + + @callback + def zone_update(ctrl: Controller, zone: Zone) -> None: + """Handle zone data updates.""" + if zone is not self._zone: + return + self._name = zone.name.title() + self.async_schedule_update_ha_state() + + self.async_on_remove( + async_dispatcher_connect(self.hass, DISPATCH_ZONE_UPDATE, zone_update) + ) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._controller.available + + @property + def assumed_state(self) -> bool: + """Return True if unable to access real state of the entity.""" + return self._controller.assumed_state + + @property + def device_info(self): + """Return the device info for the iZone system.""" + return self._device_info + + @property + def unique_id(self): + """Return the ID of the controller device.""" + return "{}_z{}".format(self._controller.unique_id, self._zone.index + 1) + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False + + @property + def supported_features(self): + """Return the list of supported features.""" + try: + if self._zone.mode == Zone.Mode.AUTO: + return self._supported_features + return self._supported_features & ~SUPPORT_TARGET_TEMPERATURE + except ConnectionError: + return None + + @property + def temperature_unit(self): + """Return the unit of measurement which this thermostat uses.""" + return TEMP_CELSIUS + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_HALVES + + @property + def hvac_mode(self): + """Return current operation ie. heat, cool, idle.""" + mode = self._zone.mode + for (key, value) in self._state_to_pizone.items(): + if value == mode: + return key + return None + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + return list(self._state_to_pizone.keys()) + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._zone.temp_current + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + if self._zone.type != Zone.Type.AUTO: + return None + return self._zone.temp_setpoint + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 0.5 + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._controller.min_temp + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._controller.max_temp + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + if self._zone.mode != Zone.Mode.AUTO: + return + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + await self._controller.wrap_and_catch(self._zone.set_temp_setpoint(temp)) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target operation mode.""" + mode = self._state_to_pizone[hvac_mode] + await self._controller.wrap_and_catch(self._zone.set_mode(mode)) + self.async_schedule_update_ha_state() + + @property + def is_on(self): + """Return true if on.""" + return self._zone.mode != Zone.Mode.CLOSE + + async def async_turn_on(self): + """Turn device on (open zone).""" + if self._zone.type == Zone.Type.AUTO: + await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.AUTO)) + else: + await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.OPEN)) + self.async_schedule_update_ha_state() + + async def async_turn_off(self): + """Turn device off (close zone).""" + await self._controller.wrap_and_catch(self._zone.set_mode(Zone.Mode.CLOSE)) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py new file mode 100644 index 00000000000000..eb57a36a2bb5f7 --- /dev/null +++ b/homeassistant/components/izone/config_flow.py @@ -0,0 +1,45 @@ +"""Config flow for izone.""" + +import logging +import asyncio + +from async_timeout import timeout + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import IZONE, TIMEOUT_DISCOVERY, DISPATCH_CONTROLLER_DISCOVERED + + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(hass): + from .discovery import async_start_discovery_service, async_stop_discovery_service + + controller_ready = asyncio.Event() + async_dispatcher_connect( + hass, DISPATCH_CONTROLLER_DISCOVERED, lambda x: controller_ready.set() + ) + + disco = await async_start_discovery_service(hass) + + try: + async with timeout(TIMEOUT_DISCOVERY): + await controller_ready.wait() + except asyncio.TimeoutError: + pass + + if not disco.pi_disco.controllers: + await async_stop_discovery_service(hass) + _LOGGER.debug("No controllers found") + return False + + _LOGGER.debug("Controllers %s", disco.pi_disco.controllers) + return True + + +config_entry_flow.register_discovery_flow( + IZONE, "iZone Aircon", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL +) diff --git a/homeassistant/components/izone/const.py b/homeassistant/components/izone/const.py new file mode 100644 index 00000000000000..4da7bc9e4afdb5 --- /dev/null +++ b/homeassistant/components/izone/const.py @@ -0,0 +1,14 @@ +"""Constants used by the izone component.""" + +IZONE = "izone" + +DATA_DISCOVERY_SERVICE = "izone_discovery" +DATA_CONFIG = "izone_config" + +DISPATCH_CONTROLLER_DISCOVERED = "izone_controller_discovered" +DISPATCH_CONTROLLER_DISCONNECTED = "izone_controller_disconnected" +DISPATCH_CONTROLLER_RECONNECTED = "izone_controller_disconnected" +DISPATCH_CONTROLLER_UPDATE = "izone_controller_update" +DISPATCH_ZONE_UPDATE = "izone_zone_update" + +TIMEOUT_DISCOVERY = 20 diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py new file mode 100644 index 00000000000000..3630c28605bb7f --- /dev/null +++ b/homeassistant/components/izone/discovery.py @@ -0,0 +1,87 @@ +"""Internal discovery service for iZone AC.""" + +import logging +import pizone + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ( + DATA_DISCOVERY_SERVICE, + DISPATCH_CONTROLLER_DISCOVERED, + DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_RECONNECTED, + DISPATCH_CONTROLLER_UPDATE, + DISPATCH_ZONE_UPDATE, +) + +_LOGGER = logging.getLogger(__name__) + + +class DiscoveryService(pizone.Listener): + """Discovery data and interfacing with pizone library.""" + + def __init__(self, hass): + """Initialise discovery service.""" + super().__init__() + self.hass = hass + self.pi_disco = None + + # Listener interface + def controller_discovered(self, ctrl: pizone.Controller) -> None: + """Handle new controller discoverery.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCOVERED, ctrl) + + def controller_disconnected(self, ctrl: pizone.Controller, ex: Exception) -> None: + """On disconnect from controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCONNECTED, ctrl, ex) + + def controller_reconnected(self, ctrl: pizone.Controller) -> None: + """On reconnect to controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_RECONNECTED, ctrl) + + def controller_update(self, ctrl: pizone.Controller) -> None: + """System update message is recieved from the controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_UPDATE, ctrl) + + def zone_update(self, ctrl: pizone.Controller, zone: pizone.Zone) -> None: + """Zone update message is recieved from the controller.""" + async_dispatcher_send(self.hass, DISPATCH_ZONE_UPDATE, ctrl, zone) + + +async def async_start_discovery_service(hass: HomeAssistantType): + """Set up the pizone internal discovery.""" + disco = hass.data.get(DATA_DISCOVERY_SERVICE) + if disco: + # Already started + return disco + + # discovery local services + disco = DiscoveryService(hass) + hass.data[DATA_DISCOVERY_SERVICE] = disco + + # Start the pizone discovery service, disco is the listener + session = aiohttp_client.async_get_clientsession(hass) + loop = hass.loop + + disco.pi_disco = pizone.discovery(disco, loop=loop, session=session) + await disco.pi_disco.start_discovery() + + async def shutdown_event(event): + await async_stop_discovery_service(hass) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown_event) + + return disco + + +async def async_stop_discovery_service(hass: HomeAssistantType): + """Stop the discovery service.""" + disco = hass.data.get(DATA_DISCOVERY_SERVICE) + if not disco: + return + + await disco.pi_disco.close() + del hass.data[DATA_DISCOVERY_SERVICE] diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json new file mode 100644 index 00000000000000..2f6747ab4cc51d --- /dev/null +++ b/homeassistant/components/izone/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "izone", + "name": "izone", + "documentation": "https://www.home-assistant.io/components/izone", + "requirements": [ "python-izone==1.1.1" ], + "dependencies": [], + "codeowners": [ "@Swamp-Ig" ], + "config_flow": true +} diff --git a/homeassistant/components/izone/strings.json b/homeassistant/components/izone/strings.json new file mode 100644 index 00000000000000..7cb14b03c6c593 --- /dev/null +++ b/homeassistant/components/izone/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "iZone", + "step": { + "confirm": { + "title": "iZone", + "description": "Do you want to set up iZone?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of iZone is necessary.", + "no_devices_found": "No iZone devices found on the network." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9ddae5acdb9941..9a534c01bbf2c6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -30,6 +30,7 @@ "ios", "ipma", "iqvia", + "izone", "life360", "lifx", "linky", diff --git a/requirements_all.txt b/requirements_all.txt index 99d81158edbae5..ae4007ee770a1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1508,6 +1508,9 @@ python-gitlab==1.6.0 # homeassistant.components.hp_ilo python-hpilo==4.3 +# homeassistant.components.izone +python-izone==1.1.1 + # homeassistant.components.joaoapps_join python-join-api==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d0d42cc8472bb..f9644058580088 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,6 +349,9 @@ pyspcwebgw==0.4.0 # homeassistant.components.darksky python-forecastio==1.4.0 +# homeassistant.components.izone +python-izone==1.1.1 + # homeassistant.components.nest python-nest==4.1.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 72fb9ff5a44233..384d50bccef729 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -145,6 +145,7 @@ "pyspcwebgw", "python_awair", "python-forecastio", + "python-izone", "python-nest", "python-velbus", "pythonwhois", diff --git a/tests/components/izone/__init__.py b/tests/components/izone/__init__.py new file mode 100644 index 00000000000000..1baeb3fee82702 --- /dev/null +++ b/tests/components/izone/__init__.py @@ -0,0 +1 @@ +"""IZone tests.""" diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py new file mode 100644 index 00000000000000..faa920271e385b --- /dev/null +++ b/tests/components/izone/test_config_flow.py @@ -0,0 +1,83 @@ +"""Tests for iZone.""" + +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.izone.const import IZONE, DISPATCH_CONTROLLER_DISCOVERED + +from tests.common import mock_coro + + +@pytest.fixture +def mock_disco(): + """Mock discovery service.""" + disco = Mock() + disco.pi_disco = Mock() + disco.pi_disco.controllers = {} + yield disco + + +def _mock_start_discovery(hass, mock_disco): + from homeassistant.helpers.dispatcher import async_dispatcher_send + + def do_disovered(*args): + async_dispatcher_send(hass, DISPATCH_CONTROLLER_DISCOVERED, True) + return mock_coro(mock_disco) + + return do_disovered + + +async def test_not_found(hass, mock_disco): + """Test not finding iZone controller.""" + + with patch( + "homeassistant.components.izone.discovery.async_start_discovery_service" + ) as start_disco, patch( + "homeassistant.components.izone.discovery.async_stop_discovery_service", + return_value=mock_coro(), + ) as stop_disco: + start_disco.side_effect = _mock_start_discovery(hass, mock_disco) + result = await hass.config_entries.flow.async_init( + IZONE, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + await hass.async_block_till_done() + + stop_disco.assert_called_once() + + +async def test_found(hass, mock_disco): + """Test not finding iZone controller.""" + mock_disco.pi_disco.controllers["blah"] = object() + + with patch( + "homeassistant.components.izone.climate.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.izone.discovery.async_start_discovery_service" + ) as start_disco, patch( + "homeassistant.components.izone.async_start_discovery_service", + return_value=mock_coro(), + ): + start_disco.side_effect = _mock_start_discovery(hass, mock_disco) + result = await hass.config_entries.flow.async_init( + IZONE, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + 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() + + mock_setup.assert_called_once() From d26273a9e9392bd81e3e469feb5edc39683f082d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 Sep 2019 23:38:58 +0200 Subject: [PATCH 0368/3953] Bump influxdb to 5.2.3 (#26743) --- homeassistant/components/influxdb/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index 20652ddd046375..feda5da732cab7 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -3,10 +3,10 @@ "name": "Influxdb", "documentation": "https://www.home-assistant.io/components/influxdb", "requirements": [ - "influxdb==5.2.0" + "influxdb==5.2.3" ], "dependencies": [], "codeowners": [ "@fabaff" ] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index ae4007ee770a1d..064762591db9a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -687,7 +687,7 @@ ihcsdk==2.3.0 incomfort-client==0.3.1 # homeassistant.components.influxdb -influxdb==5.2.0 +influxdb==5.2.3 # homeassistant.components.insteon insteonplm==0.16.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9644058580088..2f5a1bef6e469a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -197,7 +197,7 @@ huawei-lte-api==1.3.0 iaqualink==0.2.9 # homeassistant.components.influxdb -influxdb==5.2.0 +influxdb==5.2.3 # homeassistant.components.verisure jsonpath==0.75 From aac2c3e91cc9e9d15feea90a758d205819d8dfa0 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 19 Sep 2019 23:39:57 +0200 Subject: [PATCH 0369/3953] Update codeowners (#26733) --- CODEOWNERS | 5 ----- homeassistant/components/lifx/manifest.json | 4 +--- homeassistant/components/lifx_cloud/manifest.json | 4 +--- homeassistant/components/lifx_legacy/manifest.json | 4 +--- homeassistant/components/netgear_lte/manifest.json | 4 +--- homeassistant/components/sonos/manifest.json | 4 +--- 6 files changed, 5 insertions(+), 20 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 19dd0d5c8b69f3..9766f011889fe9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -154,9 +154,6 @@ homeassistant/components/lametric/* @robbiet480 homeassistant/components/launch_library/* @ludeeus homeassistant/components/lcn/* @alengwenus homeassistant/components/life360/* @pnbruckner -homeassistant/components/lifx/* @amelchio -homeassistant/components/lifx_cloud/* @amelchio -homeassistant/components/lifx_legacy/* @amelchio homeassistant/components/linky/* @Quentame homeassistant/components/linux_battery/* @fabaff homeassistant/components/liveboxplaytv/* @pschmitt @@ -187,7 +184,6 @@ 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 @@ -254,7 +250,6 @@ homeassistant/components/solaredge_local/* @drobtravels homeassistant/components/solax/* @squishykid homeassistant/components/somfy/* @tetienne homeassistant/components/songpal/* @rytilahti -homeassistant/components/sonos/* @amelchio homeassistant/components/spaceapi/* @fabaff homeassistant/components/spider/* @peternijssen homeassistant/components/sql/* @dgomes diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index fd74d9831fca0a..131d1a23b6a5f3 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -13,7 +13,5 @@ ] }, "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/lifx_cloud/manifest.json b/homeassistant/components/lifx_cloud/manifest.json index c2834fbc788b63..83805692e4d246 100644 --- a/homeassistant/components/lifx_cloud/manifest.json +++ b/homeassistant/components/lifx_cloud/manifest.json @@ -4,7 +4,5 @@ "documentation": "https://www.home-assistant.io/components/lifx_cloud", "requirements": [], "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/lifx_legacy/manifest.json b/homeassistant/components/lifx_legacy/manifest.json index 4ff59ac17703df..fb38b41f314c4a 100644 --- a/homeassistant/components/lifx_legacy/manifest.json +++ b/homeassistant/components/lifx_legacy/manifest.json @@ -6,7 +6,5 @@ "liffylights==0.9.4" ], "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 609ea72cc699c3..99ca3cb1ccfb82 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -6,7 +6,5 @@ "eternalegypt==0.0.10" ], "dependencies": [], - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 8c231ec63e0914..a08c0a59c07fd6 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -12,7 +12,5 @@ "urn:schemas-upnp-org:device:ZonePlayer:1" ] }, - "codeowners": [ - "@amelchio" - ] + "codeowners": [] } From 9e2cd5116acee97cc09453e564e419371e8e3e9c Mon Sep 17 00:00:00 2001 From: Askarov Rishat Date: Fri, 20 Sep 2019 00:41:44 +0300 Subject: [PATCH 0370/3953] Add transport data from maps.yandex.ru api (#26252) * adding feature obtaining Moscow transport data from maps.yandex.ru api * extracting the YandexMapsRequester to pypi * fix code review comments * fix stop_name, state in datetime, logger formating * fix comments * add docstring to init * rename, because it works not only Moscow, but many another big cities in Russia * fix comments * Try to solve relative view in sensor timestamp * back to isoformat * add tests, update external library version * flake8 and black tests for sensor.py * fix manifest.json * update tests, migrate to pytest, async, Using MockDependency * move json to tests/fixtures * script/lint fixes * fix comments * removing check_filter function * fix typo --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/yandex_transport/__init__.py | 1 + .../components/yandex_transport/manifest.json | 12 + .../components/yandex_transport/sensor.py | 128 + requirements_all.txt | 3 + tests/components/yandex_transport/__init__.py | 1 + .../test_yandex_transport_sensor.py | 88 + tests/fixtures/yandex_transport_reply.json | 2106 +++++++++++++++++ 9 files changed, 2341 insertions(+) create mode 100644 homeassistant/components/yandex_transport/__init__.py create mode 100644 homeassistant/components/yandex_transport/manifest.json create mode 100644 homeassistant/components/yandex_transport/sensor.py create mode 100644 tests/components/yandex_transport/__init__.py create mode 100644 tests/components/yandex_transport/test_yandex_transport_sensor.py create mode 100644 tests/fixtures/yandex_transport_reply.json diff --git a/.coveragerc b/.coveragerc index 5d5b0f6c81b09b..a29586c7b6e1cd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -747,6 +747,7 @@ omit = homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yamaha/media_player.py homeassistant/components/yamaha_musiccast/media_player.py + homeassistant/components/yandex_transport/* homeassistant/components/yeelight/* homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py diff --git a/CODEOWNERS b/CODEOWNERS index 9766f011889fe9..9bcd475d5d4788 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -318,6 +318,7 @@ homeassistant/components/xiaomi_miio/* @rytilahti @syssi homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yamaha_musiccast/* @jalmeroth +homeassistant/components/yandex_transport/* @rishatik92 homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yessssms/* @flowolf diff --git a/homeassistant/components/yandex_transport/__init__.py b/homeassistant/components/yandex_transport/__init__.py new file mode 100644 index 00000000000000..d007b2d3df8581 --- /dev/null +++ b/homeassistant/components/yandex_transport/__init__.py @@ -0,0 +1 @@ +"""Service for obtaining information about closer bus from Transport Yandex Service.""" diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json new file mode 100644 index 00000000000000..54837b2eb0914d --- /dev/null +++ b/homeassistant/components/yandex_transport/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "yandex_transport", + "name": "Yandex Transport", + "documentation": "https://www.home-assistant.io/components/yandex_transport", + "requirements": [ + "ya_ma==0.3.4" + ], + "dependencies": [], + "codeowners": [ + "@rishatik92" + ] +} diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py new file mode 100644 index 00000000000000..340291807ead98 --- /dev/null +++ b/homeassistant/components/yandex_transport/sensor.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +"""Service for obtaining information about closer bus from Transport Yandex Service.""" + +import logging +from datetime import timedelta + +import voluptuous as vol +from ya_ma import YandexMapsRequester + +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +STOP_NAME = "stop_name" +USER_AGENT = "Home Assistant" +ATTRIBUTION = "Data provided by maps.yandex.ru" + +CONF_STOP_ID = "stop_id" +CONF_ROUTE = "routes" + +DEFAULT_NAME = "Yandex Transport" +ICON = "mdi:bus" + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STOP_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ROUTE, default=[]): vol.All(cv.ensure_list, [cv.string]), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Yandex transport sensor.""" + stop_id = config[CONF_STOP_ID] + name = config[CONF_NAME] + routes = config[CONF_ROUTE] + + data = YandexMapsRequester(user_agent=USER_AGENT) + add_entities([DiscoverMoscowYandexTransport(data, stop_id, routes, name)], True) + + +class DiscoverMoscowYandexTransport(Entity): + """Implementation of yandex_transport sensor.""" + + def __init__(self, requester, stop_id, routes, name): + """Initialize sensor.""" + self.requester = requester + self._stop_id = stop_id + self._routes = [] + self._routes = routes + self._state = None + self._name = name + self._attrs = None + + def update(self): + """Get the latest data from maps.yandex.ru and update the states.""" + attrs = {} + closer_time = None + try: + yandex_reply = self.requester.get_stop_info(self._stop_id) + data = yandex_reply["data"] + stop_metadata = data["properties"]["StopMetaData"] + except KeyError as key_error: + _LOGGER.warning( + "Exception KeyError was captured, missing key is %s. Yandex returned: %s", + key_error, + yandex_reply, + ) + self.requester.set_new_session() + data = self.requester.get_stop_info(self._stop_id)["data"] + stop_metadata = data["properties"]["StopMetaData"] + stop_name = data["properties"]["name"] + transport_list = stop_metadata["Transport"] + for transport in transport_list: + route = transport["name"] + if self._routes and route not in self._routes: + # skip unnecessary route info + continue + if "Events" in transport["BriefSchedule"]: + for event in transport["BriefSchedule"]["Events"]: + if "Estimated" in event: + posix_time_next = int(event["Estimated"]["value"]) + if closer_time is None or closer_time > posix_time_next: + closer_time = posix_time_next + if route not in attrs: + attrs[route] = [] + attrs[route].append(event["Estimated"]["text"]) + attrs[STOP_NAME] = stop_name + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION + if closer_time is None: + self._state = None + else: + self._state = dt_util.utc_from_timestamp(closer_time).isoformat( + timespec="seconds" + ) + self._attrs = attrs + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 064762591db9a2..437aa60cf96d15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1994,6 +1994,9 @@ xmltodict==0.12.0 # homeassistant.components.xs1 xs1-api-client==2.3.5 +# homeassistant.components.yandex_transport +ya_ma==0.3.4 + # homeassistant.components.yweather yahooweather==0.10 diff --git a/tests/components/yandex_transport/__init__.py b/tests/components/yandex_transport/__init__.py new file mode 100644 index 00000000000000..fe6b0db52d3e05 --- /dev/null +++ b/tests/components/yandex_transport/__init__.py @@ -0,0 +1 @@ +"""Tests for the yandex transport platform.""" diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py new file mode 100644 index 00000000000000..50d945e7fae371 --- /dev/null +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -0,0 +1,88 @@ +"""Tests for the yandex transport platform.""" + +import json +import pytest + +import homeassistant.components.sensor as sensor +import homeassistant.util.dt as dt_util +from homeassistant.const import CONF_NAME +from tests.common import ( + assert_setup_component, + async_setup_component, + MockDependency, + load_fixture, +) + +REPLY = json.loads(load_fixture("yandex_transport_reply.json")) + + +@pytest.fixture +def mock_requester(): + """Create a mock ya_ma module and YandexMapsRequester.""" + with MockDependency("ya_ma") as ya_ma: + instance = ya_ma.YandexMapsRequester.return_value + instance.get_stop_info.return_value = REPLY + yield instance + + +STOP_ID = 9639579 +ROUTES = ["194", "т36", "т47", "м10"] +NAME = "test_name" +TEST_CONFIG = { + "sensor": { + "platform": "yandex_transport", + "stop_id": 9639579, + "routes": ROUTES, + "name": NAME, + } +} + +FILTERED_ATTRS = { + "т36": ["21:43", "21:47", "22:02"], + "т47": ["21:40", "22:01"], + "м10": ["21:48", "22:00"], + "stop_name": "7-й автобусный парк", + "attribution": "Data provided by maps.yandex.ru", +} + +RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds") + + +async def assert_setup_sensor(hass, config, count=1): + """Set up the sensor and assert it's been created.""" + with assert_setup_component(count): + assert await async_setup_component(hass, sensor.DOMAIN, config) + + +async def test_setup_platform_valid_config(hass, mock_requester): + """Test that sensor is set up properly with valid config.""" + await assert_setup_sensor(hass, TEST_CONFIG) + + +async def test_setup_platform_invalid_config(hass, mock_requester): + """Check an invalid configuration.""" + await assert_setup_sensor( + hass, {"sensor": {"platform": "yandex_transport", "stopid": 1234}}, count=0 + ) + + +async def test_name(hass, mock_requester): + """Return the name if set in the configuration.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + assert state.name == TEST_CONFIG["sensor"][CONF_NAME] + + +async def test_state(hass, mock_requester): + """Return the contents of _state.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + assert state.state == RESULT_STATE + + +async def test_filtered_attributes(hass, mock_requester): + """Return the contents of attributes.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + state_attrs = {key: state.attributes[key] for key in FILTERED_ATTRS} + assert state_attrs == FILTERED_ATTRS diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json new file mode 100644 index 00000000000000..c5e4857297aa93 --- /dev/null +++ b/tests/fixtures/yandex_transport_reply.json @@ -0,0 +1,2106 @@ +{ + "data": { + "geometries": [ + { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + } + ], + "geometry": { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + }, + "properties": { + "name": "7-й автобусный парк", + "description": "7-й автобусный парк", + "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)", + "StopMetaData": { + "id": "stop__9639579", + "name": "7-й автобусный парк", + "type": "urban", + "region": { + "id": 213, + "type": 6, + "parent_id": 1, + "capital_id": 0, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow", + "native_name": "", + "iso_name": "RU MOW", + "is_main": true, + "en_name": "Moscow", + "short_en_name": "MSK", + "phone_code": "495 499", + "phone_code_old": "095", + "zip_code": "", + "population": 12506468, + "synonyms": "Moskau, Moskva", + "latitude": 55.753215, + "longitude": 37.622504, + "latitude_size": 0.878654, + "longitude_size": 1.164423, + "zoom": 10, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "weather", + "afisha", + "maps", + "tv", + "ad", + "etrain", + "subway", + "delivery", + "route" + ], + "ename": "moscow", + "bounds": [ + [ + 37.0402925, + 55.31141404514547 + ], + [ + 38.2047155, + 56.190068045145466 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву", + "dative": "Москве", + "directional": "", + "genitive": "Москвы", + "instrumental": "Москвой", + "locative": "", + "nominative": "Москва", + "preposition": "в", + "prepositional": "Москве" + }, + "parent": { + "id": 1, + "type": 5, + "parent_id": 3, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow-and-moscow-oblast", + "native_name": "", + "iso_name": "RU-MOS", + "is_main": true, + "en_name": "Moscow and Moscow Oblast", + "short_en_name": "RU-MOS", + "phone_code": "495 496 498 499", + "phone_code_old": "", + "zip_code": "", + "population": 7503385, + "synonyms": "Московская область, Подмосковье, Podmoskovye", + "latitude": 55.815792, + "longitude": 37.380031, + "latitude_size": 2.705659, + "longitude_size": 5.060749, + "zoom": 8, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 10716, + 10747, + 10758, + 20728, + 10740, + 10738, + 20523, + 10735, + 10734, + 10743, + 21622 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "moscow-and-moscow-oblast", + "bounds": [ + [ + 34.8496565, + 54.439456064325434 + ], + [ + 39.9104055, + 57.14511506432543 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву и Московскую область", + "dative": "Москве и Московской области", + "directional": "", + "genitive": "Москвы и Московской области", + "instrumental": "Москвой и Московской областью", + "locative": "", + "nominative": "Москва и Московская область", + "preposition": "в", + "prepositional": "Москве и Московской области" + }, + "parent": { + "id": 225, + "type": 3, + "parent_id": 10001, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "russia", + "native_name": "", + "iso_name": "RU", + "is_main": false, + "en_name": "Russia", + "short_en_name": "RU", + "phone_code": "7", + "phone_code_old": "", + "zip_code": "", + "population": 146880432, + "synonyms": "Russian Federation,Российская Федерация", + "latitude": 61.698653, + "longitude": 99.505405, + "latitude_size": 40.700127, + "longitude_size": 171.643239, + "zoom": 3, + "tzname": "", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 2, + 65, + 54, + 47, + 43, + 66, + 51, + 56, + 172, + 39, + 62 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "russia", + "bounds": [ + [ + 13.683785499999999, + 35.290400699917846 + ], + [ + -174.6729755, + 75.99052769991785 + ] + ], + "names": { + "ablative": "", + "accusative": "Россию", + "dative": "России", + "directional": "", + "genitive": "России", + "instrumental": "Россией", + "locative": "", + "nominative": "Россия", + "preposition": "в", + "prepositional": "России" + } + } + } + }, + "Transport": [ + { + "lineId": "2036925416", + "name": "194", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659860", + "tzOffset": 10800, + "text": "21:51" + } + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661840", + "tzOffset": 10800, + "text": "22:24" + } + } + ], + "departureTime": "21:51" + } + } + ], + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659860", + "tzOffset": 10800, + "text": "21:51" + } + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661840", + "tzOffset": 10800, + "text": "22:24" + } + } + ], + "departureTime": "21:51" + } + }, + { + "lineId": "213_114_bus_mosgortrans", + "name": "114", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568603405", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1568672165", + "tzOffset": 10800, + "text": "1:16" + } + } + } + } + ], + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568603405", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1568672165", + "tzOffset": 10800, + "text": "1:16" + } + } + } + }, + { + "lineId": "213_154_bus_mosgortrans", + "name": "154", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659260", + "tzOffset": 10800, + "text": "21:41" + }, + "Estimated": { + "value": "1568659252", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1054764%5F191500" + }, + { + "Scheduled": { + "value": "1568660580", + "tzOffset": 10800, + "text": "22:03" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:41" + } + } + ], + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659260", + "tzOffset": 10800, + "text": "21:41" + }, + "Estimated": { + "value": "1568659252", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1054764%5F191500" + }, + { + "Scheduled": { + "value": "1568660580", + "tzOffset": 10800, + "text": "22:03" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:41" + } + }, + { + "lineId": "213_179_bus_mosgortrans", + "name": "179", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568659351", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|59832%5F31359" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661660", + "tzOffset": 10800, + "text": "22:21" + } + } + ], + "departureTime": "21:52" + } + } + ], + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568659351", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|59832%5F31359" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661660", + "tzOffset": 10800, + "text": "22:21" + } + } + ], + "departureTime": "21:52" + } + }, + { + "lineId": "213_191m_minibus_default", + "name": "591", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660525", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|38278%5F9345312" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568602033", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1568672233", + "tzOffset": 10800, + "text": "1:17" + } + } + } + } + ], + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660525", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|38278%5F9345312" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568602033", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1568672233", + "tzOffset": 10800, + "text": "1:17" + } + } + } + }, + { + "lineId": "213_206m_minibus_default", + "name": "206к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568601239", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1568671439", + "tzOffset": 10800, + "text": "1:03" + } + } + } + } + ], + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568601239", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1568671439", + "tzOffset": 10800, + "text": "1:03" + } + } + } + }, + { + "lineId": "213_215_bus_mosgortrans", + "name": "215", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1568601276", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1568671476", + "tzOffset": 10800, + "text": "1:04" + } + } + } + } + ], + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1568601276", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1568671476", + "tzOffset": 10800, + "text": "1:04" + } + } + } + }, + { + "lineId": "213_282_bus_mosgortrans", + "name": "282", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659888", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|34874%5F9345408" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568602180", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1568673460", + "tzOffset": 10800, + "text": "1:37" + } + } + } + } + ], + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659888", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|34874%5F9345408" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568602180", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1568673460", + "tzOffset": 10800, + "text": "1:37" + } + } + } + }, + { + "lineId": "213_294m_minibus_default", + "name": "994", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1568601527", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1568671727", + "tzOffset": 10800, + "text": "1:08" + } + } + } + } + ], + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1568601527", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1568671727", + "tzOffset": 10800, + "text": "1:08" + } + } + } + }, + { + "lineId": "213_36_trolleybus_mosgortrans", + "name": "т36", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + }, + "Estimated": { + "value": "1568659426", + "tzOffset": 10800, + "text": "21:43" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Scheduled": { + "value": "1568660520", + "tzOffset": 10800, + "text": "22:02" + }, + "Estimated": { + "value": "1568659656", + "tzOffset": 10800, + "text": "21:47" + }, + "vehicleId": "codd%5Fnew|1117016%5F430280" + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + }, + "Estimated": { + "value": "1568660538", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|1054576%5F430226" + } + ], + "departureTime": "21:48" + } + } + ], + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + }, + "Estimated": { + "value": "1568659426", + "tzOffset": 10800, + "text": "21:43" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Scheduled": { + "value": "1568660520", + "tzOffset": 10800, + "text": "22:02" + }, + "Estimated": { + "value": "1568659656", + "tzOffset": 10800, + "text": "21:47" + }, + "vehicleId": "codd%5Fnew|1117016%5F430280" + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + }, + "Estimated": { + "value": "1568660538", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|1054576%5F430226" + } + ], + "departureTime": "21:48" + } + }, + { + "lineId": "213_47_trolleybus_mosgortrans", + "name": "т47", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + }, + "Estimated": { + "value": "1568659253", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1112219%5F430329" + }, + { + "Scheduled": { + "value": "1568660940", + "tzOffset": 10800, + "text": "22:09" + }, + "Estimated": { + "value": "1568660519", + "tzOffset": 10800, + "text": "22:01" + }, + "vehicleId": "codd%5Fnew|1139620%5F430382" + }, + { + "Scheduled": { + "value": "1568663580", + "tzOffset": 10800, + "text": "22:53" + } + } + ], + "departureTime": "21:53" + } + } + ], + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + }, + "Estimated": { + "value": "1568659253", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1112219%5F430329" + }, + { + "Scheduled": { + "value": "1568660940", + "tzOffset": 10800, + "text": "22:09" + }, + "Estimated": { + "value": "1568660519", + "tzOffset": 10800, + "text": "22:01" + }, + "vehicleId": "codd%5Fnew|1139620%5F430382" + }, + { + "Scheduled": { + "value": "1568663580", + "tzOffset": 10800, + "text": "22:53" + } + } + ], + "departureTime": "21:53" + } + }, + { + "lineId": "213_56_trolleybus_mosgortrans", + "name": "т56", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660675", + "tzOffset": 10800, + "text": "22:04" + }, + "vehicleId": "codd%5Fnew|146304%5F31207" + } + ], + "Frequency": { + "text": "8 мин", + "value": 480, + "begin": { + "value": "1568606244", + "tzOffset": 10800, + "text": "6:57" + }, + "end": { + "value": "1568670144", + "tzOffset": 10800, + "text": "0:42" + } + } + } + } + ], + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660675", + "tzOffset": 10800, + "text": "22:04" + }, + "vehicleId": "codd%5Fnew|146304%5F31207" + } + ], + "Frequency": { + "text": "8 мин", + "value": 480, + "begin": { + "value": "1568606244", + "tzOffset": 10800, + "text": "6:57" + }, + "end": { + "value": "1568670144", + "tzOffset": 10800, + "text": "0:42" + } + } + } + }, + { + "lineId": "213_63_bus_mosgortrans", + "name": "63", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|38921%5F9215306" + }, + { + "Estimated": { + "value": "1568660136", + "tzOffset": 10800, + "text": "21:55" + }, + "vehicleId": "codd%5Fnew|38918%5F9215303" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1568600987", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568670227", + "tzOffset": 10800, + "text": "0:43" + } + } + } + } + ], + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|38921%5F9215306" + }, + { + "Estimated": { + "value": "1568660136", + "tzOffset": 10800, + "text": "21:55" + }, + "vehicleId": "codd%5Fnew|38918%5F9215303" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1568600987", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568670227", + "tzOffset": 10800, + "text": "0:43" + } + } + } + }, + { + "lineId": "213_677_bus_mosgortrans", + "name": "677", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|11731%5F31376" + } + ], + "Frequency": { + "text": "4 мин", + "value": 240, + "begin": { + "value": "1568600940", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568672640", + "tzOffset": 10800, + "text": "1:24" + } + } + } + } + ], + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|11731%5F31376" + } + ], + "Frequency": { + "text": "4 мин", + "value": 240, + "begin": { + "value": "1568600940", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568672640", + "tzOffset": 10800, + "text": "1:24" + } + } + } + }, + { + "lineId": "213_692_bus_mosgortrans", + "name": "692", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568660280", + "tzOffset": 10800, + "text": "21:58" + }, + "Estimated": { + "value": "1568660255", + "tzOffset": 10800, + "text": "21:57" + }, + "vehicleId": "codd%5Fnew|63029%5F31485" + }, + { + "Scheduled": { + "value": "1568693340", + "tzOffset": 10800, + "text": "7:09" + } + }, + { + "Scheduled": { + "value": "1568696940", + "tzOffset": 10800, + "text": "8:09" + } + } + ], + "departureTime": "21:58" + } + } + ], + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568660280", + "tzOffset": 10800, + "text": "21:58" + }, + "Estimated": { + "value": "1568660255", + "tzOffset": 10800, + "text": "21:57" + }, + "vehicleId": "codd%5Fnew|63029%5F31485" + }, + { + "Scheduled": { + "value": "1568693340", + "tzOffset": 10800, + "text": "7:09" + } + }, + { + "Scheduled": { + "value": "1568696940", + "tzOffset": 10800, + "text": "8:09" + } + } + ], + "departureTime": "21:58" + } + }, + { + "lineId": "213_78_trolleybus_mosgortrans", + "name": "т78", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659620", + "tzOffset": 10800, + "text": "21:47" + }, + "Estimated": { + "value": "1568659898", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|147522%5F31184" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:47" + } + } + ], + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659620", + "tzOffset": 10800, + "text": "21:47" + }, + "Estimated": { + "value": "1568659898", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|147522%5F31184" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:47" + } + }, + { + "lineId": "213_82_bus_mosgortrans", + "name": "82", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + } + }, + { + "Scheduled": { + "value": "1568661780", + "tzOffset": 10800, + "text": "22:23" + } + }, + { + "Scheduled": { + "value": "1568663760", + "tzOffset": 10800, + "text": "22:56" + } + } + ], + "departureTime": "21:48" + } + } + ], + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + } + }, + { + "Scheduled": { + "value": "1568661780", + "tzOffset": 10800, + "text": "22:23" + } + }, + { + "Scheduled": { + "value": "1568663760", + "tzOffset": 10800, + "text": "22:56" + } + } + ], + "departureTime": "21:48" + } + }, + { + "lineId": "2465131598", + "name": "179к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659500", + "tzOffset": 10800, + "text": "21:45" + } + }, + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + } + }, + { + "Scheduled": { + "value": "1568660880", + "tzOffset": 10800, + "text": "22:08" + } + } + ], + "departureTime": "21:45" + } + } + ], + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659500", + "tzOffset": 10800, + "text": "21:45" + } + }, + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + } + }, + { + "Scheduled": { + "value": "1568660880", + "tzOffset": 10800, + "text": "22:08" + } + } + ], + "departureTime": "21:45" + } + }, + { + "lineId": "466_bus_default", + "name": "466", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568604647", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1568675447", + "tzOffset": 10800, + "text": "2:10" + } + } + } + } + ], + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568604647", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1568675447", + "tzOffset": 10800, + "text": "2:10" + } + } + } + }, + { + "lineId": "677k_bus_default", + "name": "677к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568660003", + "tzOffset": 10800, + "text": "21:53" + }, + "vehicleId": "codd%5Fnew|130308%5F31319" + }, + { + "Scheduled": { + "value": "1568661240", + "tzOffset": 10800, + "text": "22:14" + } + }, + { + "Scheduled": { + "value": "1568662500", + "tzOffset": 10800, + "text": "22:35" + } + } + ], + "departureTime": "21:52" + } + } + ], + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568660003", + "tzOffset": 10800, + "text": "21:53" + }, + "vehicleId": "codd%5Fnew|130308%5F31319" + }, + { + "Scheduled": { + "value": "1568661240", + "tzOffset": 10800, + "text": "22:14" + } + }, + { + "Scheduled": { + "value": "1568662500", + "tzOffset": 10800, + "text": "22:35" + } + } + ], + "departureTime": "21:52" + } + }, + { + "lineId": "m10_bus_default", + "name": "м10", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659718", + "tzOffset": 10800, + "text": "21:48" + }, + "vehicleId": "codd%5Fnew|146260%5F31212" + }, + { + "Estimated": { + "value": "1568660422", + "tzOffset": 10800, + "text": "22:00" + }, + "vehicleId": "codd%5Fnew|13997%5F31247" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568606903", + "tzOffset": 10800, + "text": "7:08" + }, + "end": { + "value": "1568675183", + "tzOffset": 10800, + "text": "2:06" + } + } + } + } + ], + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659718", + "tzOffset": 10800, + "text": "21:48" + }, + "vehicleId": "codd%5Fnew|146260%5F31212" + }, + { + "Estimated": { + "value": "1568660422", + "tzOffset": 10800, + "text": "22:00" + }, + "vehicleId": "codd%5Fnew|13997%5F31247" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568606903", + "tzOffset": 10800, + "text": "7:08" + }, + "end": { + "value": "1568675183", + "tzOffset": 10800, + "text": "2:06" + } + } + } + } + ] + } + }, + "toponymSeoname": "dmitrovskoye_shosse" + } +} From f5d12669a504bf0ca8b5038e4644c853642581a8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 19 Sep 2019 23:44:09 +0200 Subject: [PATCH 0371/3953] deCONZ improve gateway tests (#26709) * Improve gateway tests * Harmonize all tests to use the same gateway initialization method * Improve scene tests * Add gateway resync call to platform tests * Forgot to change switch tests to use common gateway method * Improve event tests --- homeassistant/components/deconz/gateway.py | 8 +- homeassistant/components/deconz/scene.py | 1 - tests/components/deconz/test_binary_sensor.py | 52 +--- tests/components/deconz/test_climate.py | 51 +-- tests/components/deconz/test_cover.py | 51 +-- tests/components/deconz/test_deconz_event.py | 120 +++---- tests/components/deconz/test_gateway.py | 293 +++++++++--------- tests/components/deconz/test_light.py | 51 +-- tests/components/deconz/test_scene.py | 93 ++---- tests/components/deconz/test_sensor.py | 52 +--- tests/components/deconz/test_services.py | 82 ++--- tests/components/deconz/test_switch.py | 52 +--- 12 files changed, 313 insertions(+), 593 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index a090dca0d0c632..75898b0fdab819 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -184,11 +184,7 @@ def shutdown(self, event): self.api.close() async def async_reset(self): - """Reset this gateway to default state. - - Will cancel any scheduled setup retry and will unload - the config entry. - """ + """Reset this gateway to default state.""" self.api.async_connection_status_callback = None self.api.close() @@ -203,7 +199,7 @@ async def async_reset(self): for event in self.events: event.async_will_remove_from_hass() - self.events.remove(event) + self.events.clear() self.deconz_ids = {} return True diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 8d27d386da266b..a84e799d44d47b 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -9,7 +9,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index c5c35f108296b0..2f42193291c8dd 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -1,14 +1,12 @@ """deCONZ binary sensor platform tests.""" from copy import deepcopy -from asynctest import patch - -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.binary_sensor as binary_sensor +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration SENSORS = { "1": { @@ -50,50 +48,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -147,6 +101,10 @@ async def test_binary_sensors(hass): presence_sensor = hass.states.get("binary_sensor.presence_sensor") assert presence_sensor.state == "on" + await gateway.async_reset() + + assert len(hass.states.async_all()) == 0 + async def test_allow_clip_sensor(hass): """Test that CLIP sensors can be allowed.""" diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 1211188d3db3fa..cee91f00c4283b 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -3,12 +3,13 @@ from asynctest import patch -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.climate as climate +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration + SENSORS = { "1": { "id": "Thermostat id", @@ -42,50 +43,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -205,6 +162,10 @@ async def test_climate_devices(hass): ) set_callback.assert_called_with("/sensors/1/config", {"heatsetpoint": 2000.0}) + await gateway.async_reset() + + assert len(hass.states.async_all()) == 0 + async def test_clip_climate_device(hass): """Test successful creation of sensor entities.""" diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 246c2bae7c561c..5c7ee48a78a2eb 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -3,12 +3,13 @@ from asynctest import patch -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.cover as cover +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration + COVERS = { "1": { "id": "Level controllable cover id", @@ -35,50 +36,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -159,3 +116,7 @@ async def test_cover(hass): ) await hass.async_block_till_done() set_callback.assert_called_with("/lights/1/state", {"bri_inc": 0}) + + await gateway.async_reset() + + assert len(hass.states.async_all()) == 2 diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 72966ba6c66b09..ade9aa02ad4afb 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -1,60 +1,74 @@ """Test deCONZ remote events.""" -from unittest.mock import Mock - -from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT, DeconzEvent -from homeassistant.core import callback - - -async def test_create_event(hass): - """Successfully created a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = DeconzEvent(mock_remote, mock_gateway) - - assert event.event_id == "name" - - -async def test_update_event(hass): - """Successfully update a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = DeconzEvent(mock_remote, mock_gateway) - mock_remote.changed_keys = {"state": True} - - calls = [] - - @callback - def listener(event): - """Mock listener.""" - calls.append(event) - - unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, listener) - - event.async_update_callback() +from copy import deepcopy + +from asynctest import Mock + +from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT + +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration + +SENSORS = { + "1": { + "id": "Switch 1 id", + "name": "Switch 1", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "2": { + "id": "Switch 2 id", + "name": "Switch 2", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {"battery": 100}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, +} + + +async def test_deconz_events(hass): + """Test successful creation of deconz events.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert "sensor.switch_1" not in gateway.deconz_ids + assert "sensor.switch_1_battery_level" not in gateway.deconz_ids + assert "sensor.switch_2" not in gateway.deconz_ids + assert "sensor.switch_2_battery_level" in gateway.deconz_ids + assert len(hass.states.async_all()) == 1 + assert len(gateway.events) == 2 + + switch_1 = hass.states.get("sensor.switch_1") + assert switch_1 is None + + switch_1_battery_level = hass.states.get("sensor.switch_1_battery_level") + assert switch_1_battery_level is None + + switch_2 = hass.states.get("sensor.switch_2") + assert switch_2 is None + + switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") + assert switch_2_battery_level.state == "100" + + mock_listener = Mock() + unsub = hass.bus.async_listen(CONF_DECONZ_EVENT, mock_listener) + + gateway.api.sensors["1"].async_update({"state": {"buttonevent": 2000}}) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(mock_listener.mock_calls) == 1 + assert mock_listener.mock_calls[0][1][0].data == { + "id": "switch_1", + "unique_id": "00:00:00:00:00:00:00:01", + "event": 2000, + } unsub() + await gateway.async_reset() -async def test_remove_event(hass): - """Successfully update a deCONZ event.""" - mock_remote = Mock() - mock_remote.name = "Name" - - mock_gateway = Mock() - mock_gateway.hass = hass - - event = DeconzEvent(mock_remote, mock_gateway) - event.async_will_remove_from_hass() - - assert event._device is None + assert len(hass.states.async_all()) == 0 + assert len(gateway.events) == 0 diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index d84706430f465b..25a1cd465c510a 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -1,187 +1,178 @@ """Test deCONZ gateway.""" -from unittest.mock import Mock, patch +from copy import deepcopy + +from asynctest import Mock, patch import pytest +from homeassistant import config_entries +from homeassistant.components import deconz +from homeassistant.components import ssdp from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.components.deconz import errors, gateway - -from tests.common import mock_coro +from homeassistant.helpers.dispatcher import async_dispatcher_connect import pydeconz +BRIDGEID = "0123456789" ENTRY_CONFIG = { - "host": "1.2.3.4", - "port": 80, - "api_key": "1234567890ABCDEF", - "bridgeid": "0123456789ABCDEF", - "allow_clip_sensor": True, - "allow_deconz_groups": True, + deconz.config_flow.CONF_API_KEY: "ABCDEF", + deconz.config_flow.CONF_BRIDGEID: BRIDGEID, + deconz.config_flow.CONF_HOST: "1.2.3.4", + deconz.config_flow.CONF_PORT: 80, } +DECONZ_CONFIG = { + "bridgeid": BRIDGEID, + "ipaddress": "1.2.3.4", + "mac": "00:11:22:33:44:55", + "modelid": "deCONZ", + "name": "deCONZ mock gateway", + "sw_version": "2.05.69", + "uuid": "1234", + "websocketport": 1234, +} -async def test_gateway_setup(): - """Successful setup.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - api = Mock() - api.async_add_remote.return_value = Mock() - api.sensors = {} - - deconz_gateway = gateway.DeconzGateway(hass, entry) - - with patch.object( - gateway, "get_gateway", return_value=mock_coro(api) - ), patch.object(gateway, "async_dispatcher_connect", return_value=Mock()): - assert await deconz_gateway.async_setup() is True - - assert deconz_gateway.api is api - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 7 - assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == ( - entry, - "binary_sensor", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == ( - entry, - "climate", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == ( - entry, - "cover", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[3][1] == ( - entry, - "light", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[4][1] == ( - entry, - "scene", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[5][1] == ( - entry, - "sensor", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[6][1] == ( - entry, - "switch", +DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} + + +async def setup_deconz_integration(hass, config, options, get_state_response): + """Create the deCONZ gateway.""" + config_entry = config_entries.ConfigEntry( + version=1, + domain=deconz.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, + options=options, + entry_id="1", ) - assert len(api.start.mock_calls) == 1 + with patch( + "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + ), patch("pydeconz.DeconzSession.start", return_value=True): + await deconz.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() -async def test_gateway_retry(): - """Retry setup.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG + hass.config_entries._entries.append(config_entry) - deconz_gateway = gateway.DeconzGateway(hass, entry) + return hass.data[deconz.DOMAIN].get(config[deconz.CONF_BRIDGEID]) - with patch.object( - gateway, "get_gateway", side_effect=errors.CannotConnect - ), pytest.raises(ConfigEntryNotReady): - await deconz_gateway.async_setup() - -async def test_gateway_setup_fails(): +async def test_gateway_setup(hass): + """Successful setup.""" + data = deepcopy(DECONZ_WEB_REQUEST) + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ) as forward_entry_setup: + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert gateway.bridgeid == BRIDGEID + assert gateway.master is True + assert gateway.option_allow_clip_sensor is False + assert gateway.option_allow_deconz_groups is True + + assert len(gateway.deconz_ids) == 0 + assert len(hass.states.async_all()) == 0 + + entry = gateway.config_entry + assert forward_entry_setup.mock_calls[0][1] == (entry, "binary_sensor") + assert forward_entry_setup.mock_calls[1][1] == (entry, "climate") + assert forward_entry_setup.mock_calls[2][1] == (entry, "cover") + assert forward_entry_setup.mock_calls[3][1] == (entry, "light") + assert forward_entry_setup.mock_calls[4][1] == (entry, "scene") + assert forward_entry_setup.mock_calls[5][1] == (entry, "sensor") + assert forward_entry_setup.mock_calls[6][1] == (entry, "switch") + + +async def test_gateway_retry(hass): """Retry setup.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - - deconz_gateway = gateway.DeconzGateway(hass, entry) + data = deepcopy(DECONZ_WEB_REQUEST) + with patch( + "homeassistant.components.deconz.gateway.get_gateway", + side_effect=deconz.errors.CannotConnect, + ), pytest.raises(ConfigEntryNotReady): + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) - with patch.object(gateway, "get_gateway", side_effect=Exception): - result = await deconz_gateway.async_setup() - assert not result +async def test_gateway_setup_fails(hass): + """Retry setup.""" + data = deepcopy(DECONZ_WEB_REQUEST) + with patch( + "homeassistant.components.deconz.gateway.get_gateway", side_effect=Exception + ): + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert gateway is None -async def test_connection_status(hass): +async def test_connection_status_signalling(hass): """Make sure that connection status triggers a dispatcher send.""" - entry = Mock() - entry.data = ENTRY_CONFIG - - deconz_gateway = gateway.DeconzGateway(hass, entry) - with patch.object(gateway, "async_dispatcher_send") as mock_dispatch_send: - deconz_gateway.async_connection_status_callback(True) - - await hass.async_block_till_done() - assert len(mock_dispatch_send.mock_calls) == 1 - assert len(mock_dispatch_send.mock_calls[0]) == 3 - - -async def test_add_device(hass): - """Successful retry setup.""" - entry = Mock() - entry.data = ENTRY_CONFIG - - deconz_gateway = gateway.DeconzGateway(hass, entry) - with patch.object(gateway, "async_dispatcher_send") as mock_dispatch_send: - deconz_gateway.async_add_device_callback("sensor", Mock()) - - await hass.async_block_till_done() - assert len(mock_dispatch_send.mock_calls) == 1 - assert len(mock_dispatch_send.mock_calls[0]) == 3 - - -async def test_shutdown(): - """Successful shutdown.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - - deconz_gateway = gateway.DeconzGateway(hass, entry) - deconz_gateway.api = Mock() - deconz_gateway.shutdown(None) + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) - assert len(deconz_gateway.api.close.mock_calls) == 1 + event_call = Mock() + unsub = async_dispatcher_connect(hass, gateway.signal_reachable, event_call) + gateway.async_connection_status_callback(False) + await hass.async_block_till_done() -async def test_reset_after_successful_setup(): - """Verify that reset works on a setup component.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - api = Mock() - api.async_add_remote.return_value = Mock() - api.sensors = {} + assert gateway.available is False + assert len(event_call.mock_calls) == 1 - deconz_gateway = gateway.DeconzGateway(hass, entry) + unsub() - with patch.object( - gateway, "get_gateway", return_value=mock_coro(api) - ), patch.object(gateway, "async_dispatcher_connect", return_value=Mock()): - assert await deconz_gateway.async_setup() is True - listener = Mock() - deconz_gateway.listeners = [listener] - event = Mock() - event.async_will_remove_from_hass = Mock() - deconz_gateway.events = [event] - deconz_gateway.deconz_ids = {"key": "value"} +async def test_update_address(hass): + """Make sure that connection status triggers a dispatcher send.""" + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert gateway.api.host == "1.2.3.4" + + await hass.config_entries.flow.async_init( + deconz.config_flow.DOMAIN, + data={ + deconz.config_flow.CONF_HOST: "2.3.4.5", + deconz.config_flow.CONF_PORT: 80, + ssdp.ATTR_SERIAL: BRIDGEID, + ssdp.ATTR_MANUFACTURERURL: deconz.config_flow.DECONZ_MANUFACTURERURL, + deconz.config_flow.ATTR_UUID: "uuid:1234", + }, + context={"source": "ssdp"}, + ) + await hass.async_block_till_done() - hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) - assert await deconz_gateway.async_reset() is True + assert gateway.api.host == "2.3.4.5" - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 7 - assert len(listener.mock_calls) == 1 - assert len(deconz_gateway.listeners) == 0 +async def test_reset_after_successful_setup(hass): + """Make sure that connection status triggers a dispatcher send.""" + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) - assert len(event.async_will_remove_from_hass.mock_calls) == 1 - assert len(deconz_gateway.events) == 0 + result = await gateway.async_reset() + await hass.async_block_till_done() - assert len(deconz_gateway.deconz_ids) == 0 + assert result is True async def test_get_gateway(hass): """Successful call.""" - with patch( - "pydeconz.DeconzSession.async_load_parameters", return_value=mock_coro(True) - ): - assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) + with patch("pydeconz.DeconzSession.async_load_parameters", return_value=True): + assert await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) async def test_get_gateway_fails_unauthorized(hass): @@ -189,8 +180,11 @@ async def test_get_gateway_fails_unauthorized(hass): with patch( "pydeconz.DeconzSession.async_load_parameters", side_effect=pydeconz.errors.Unauthorized, - ), pytest.raises(errors.AuthenticationRequired): - assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False + ), pytest.raises(deconz.errors.AuthenticationRequired): + assert ( + await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) + is False + ) async def test_get_gateway_fails_cannot_connect(hass): @@ -198,5 +192,8 @@ async def test_get_gateway_fails_cannot_connect(hass): with patch( "pydeconz.DeconzSession.async_load_parameters", side_effect=pydeconz.errors.RequestError, - ), pytest.raises(errors.CannotConnect): - assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) is False + ), pytest.raises(deconz.errors.CannotConnect): + assert ( + await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) + is False + ) diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 50a5b2adacabcb..14dc5cc8eac1c7 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -3,12 +3,13 @@ from asynctest import patch -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.light as light +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration + GROUPS = { "1": { "id": "Light group id", @@ -61,50 +62,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -242,6 +199,10 @@ async def test_lights_and_groups(hass): await hass.async_block_till_done() set_callback.assert_called_with("/lights/1/state", {"alert": "lselect"}) + await gateway.async_reset() + + assert len(hass.states.async_all()) == 2 + async def test_disable_light_groups(hass): """Test successful creation of sensor entities.""" diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 074e943548de40..dcc8ba500c32f7 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -1,67 +1,28 @@ """deCONZ scene platform tests.""" -from unittest.mock import Mock, patch +from copy import deepcopy + +from asynctest import patch -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.scene as scene -from tests.common import mock_coro - +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration -GROUP = { +GROUPS = { "1": { - "id": "Group 1 id", - "name": "Group 1 name", - "state": {}, + "id": "Light group id", + "name": "Light group", + "type": "LightGroup", + "state": {"all_on": False, "any_on": True}, "action": {}, - "scenes": [{"id": "1", "name": "Scene 1"}], + "scenes": [{"id": "1", "name": "Scene"}], "lights": [], } } -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, -} - - -async def setup_gateway(hass, data): - """Load the deCONZ scene platform.""" - from pydeconz import DeconzSession - - loop = Mock() - session = Mock() - - config_entry = config_entries.ConfigEntry( - 1, - deconz.DOMAIN, - "Mock Title", - 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) - gateway.api.config = Mock() - hass.data[deconz.DOMAIN] = {gateway.bridgeid: gateway} - - with patch("pydeconz.DeconzSession.async_get_state", return_value=mock_coro(data)): - await gateway.api.async_load_parameters() - - await hass.config_entries.async_forward_entry_setup(config_entry, "scene") - # To flush out the service call to update the group - await hass.async_block_till_done() - return gateway - - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" assert ( @@ -75,26 +36,38 @@ async def test_platform_manually_configured(hass): async def test_no_scenes(hass): """Test that scenes can be loaded without scenes being available.""" - gateway = await setup_gateway(hass, {}) - assert not hass.data[deconz.DOMAIN][gateway.bridgeid].deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert len(gateway.deconz_ids) == 0 assert len(hass.states.async_all()) == 0 async def test_scenes(hass): """Test that scenes works.""" - with patch("pydeconz.DeconzSession.async_put_state", return_value=mock_coro(True)): - gateway = await setup_gateway(hass, {"groups": GROUP}) - assert "scene.group_1_name_scene_1" in gateway.deconz_ids + data = deepcopy(DECONZ_WEB_REQUEST) + data["groups"] = deepcopy(GROUPS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + + assert "scene.light_group_scene" in gateway.deconz_ids assert len(hass.states.async_all()) == 1 - await hass.services.async_call( - "scene", "turn_on", {"entity_id": "scene.group_1_name_scene_1"}, blocking=True - ) + light_group_scene = hass.states.get("scene.light_group_scene") + assert light_group_scene + group_scene = gateway.api.groups["1"].scenes["1"] -async def test_unload_scene(hass): - """Test that it works to unload scene entities.""" - gateway = await setup_gateway(hass, {"groups": GROUP}) + with patch.object( + group_scene, "_async_set_state_callback", return_value=True + ) as set_callback: + await hass.services.async_call( + "scene", "turn_on", {"entity_id": "scene.light_group_scene"}, blocking=True + ) + await hass.async_block_till_done() + set_callback.assert_called_with("/groups/1/scenes/1/recall", {}) await gateway.async_reset() diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 947c42e6949834..928e527dd0706e 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -1,14 +1,12 @@ """deCONZ sensor platform tests.""" from copy import deepcopy -from asynctest import patch - -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component import homeassistant.components.sensor as sensor +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration SENSORS = { "1": { @@ -77,50 +75,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -199,6 +153,10 @@ async def test_sensors(hass): switch_2_battery_level = hass.states.get("sensor.switch_2_battery_level") assert switch_2_battery_level.state == "75" + await gateway.async_reset() + + assert len(hass.states.async_all()) == 0 + async def test_allow_clip_sensors(hass): """Test that CLIP sensors can be allowed.""" diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 63934871fcbf39..533d85eef7cbe3 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -1,30 +1,19 @@ """deCONZ service tests.""" +from copy import deepcopy + from asynctest import Mock, patch import pytest import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import deconz -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} +from .test_gateway import ( + BRIDGEID, + ENTRY_CONFIG, + DECONZ_WEB_REQUEST, + setup_deconz_integration, +) GROUP = { "1": { @@ -60,31 +49,6 @@ } -async def setup_deconz_integration(hass, options): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=ENTRY_CONFIG, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=DECONZ_WEB_REQUEST - ): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][BRIDGEID] - - async def test_service_setup(hass): """Verify service setup works.""" assert deconz.services.DECONZ_SERVICES not in hass.data @@ -129,7 +93,10 @@ async def test_service_unload_not_registered(hass): async def test_configure_service_with_field(hass): """Test that service invokes pydeconz with the correct path and data.""" - await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) data = { deconz.services.SERVICE_FIELD: "/light/2", @@ -149,7 +116,10 @@ async def test_configure_service_with_field(hass): async def test_configure_service_with_entity(hass): """Test that service invokes pydeconz with the correct path and data.""" - gateway = await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) gateway.deconz_ids["light.test"] = "/light/1" data = { @@ -169,7 +139,10 @@ async def test_configure_service_with_entity(hass): async def test_configure_service_with_entity_and_field(hass): """Test that service invokes pydeconz with the correct path and data.""" - gateway = await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) gateway.deconz_ids["light.test"] = "/light/1" data = { @@ -192,7 +165,10 @@ async def test_configure_service_with_entity_and_field(hass): async def test_configure_service_with_faulty_field(hass): """Test that service invokes pydeconz with the correct path and data.""" - await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) data = {deconz.services.SERVICE_FIELD: "light/2", deconz.services.SERVICE_DATA: {}} @@ -205,7 +181,10 @@ async def test_configure_service_with_faulty_field(hass): async def test_configure_service_with_faulty_entity(hass): """Test that service invokes pydeconz with the correct path and data.""" - await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) data = { deconz.services.SERVICE_ENTITY: "light.nonexisting", @@ -224,7 +203,10 @@ async def test_configure_service_with_faulty_entity(hass): async def test_service_refresh_devices(hass): """Test that service can refresh devices.""" - gateway = await setup_deconz_integration(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) data = {deconz.CONF_BRIDGEID: BRIDGEID} diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index c574ed8911e063..262bd7001f5846 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -3,13 +3,13 @@ from asynctest import patch -from homeassistant import config_entries from homeassistant.components import deconz from homeassistant.setup import async_setup_component - import homeassistant.components.switch as switch +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration + SWITCHES = { "1": { "id": "On off switch id", @@ -41,50 +41,6 @@ }, } -BRIDGEID = "0123456789" - -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG} - - -async def setup_deconz_integration(hass, config, options, get_state_response): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response - ), patch("pydeconz.DeconzSession.start", return_value=True): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][config[deconz.CONF_BRIDGEID]] - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a gateway.""" @@ -189,3 +145,7 @@ async def test_switches(hass): ) await hass.async_block_till_done() set_callback.assert_called_with("/lights/3/state", {"alert": "none"}) + + await gateway.async_reset() + + assert len(hass.states.async_all()) == 2 From 7a1bfa7a1fab30a10b72bfd5fa8500288ecedf5c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Sep 2019 15:23:29 -0700 Subject: [PATCH 0372/3953] Updated frontend to 20190919.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 978127c6342028..18c19a9012a7da 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==20190918.1" + "home-assistant-frontend==20190919.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 746485f2ece2da..900bfddda2ec10 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==20190918.1 +home-assistant-frontend==20190919.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 437aa60cf96d15..29897b562270e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.1 +home-assistant-frontend==20190919.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f5a1bef6e469a..122ff317c0e511 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.1 +home-assistant-frontend==20190919.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 794c26a66e0beefd03168dc1be205f8ad4fa66a1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Sep 2019 15:23:29 -0700 Subject: [PATCH 0373/3953] Updated frontend to 20190919.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 978127c6342028..18c19a9012a7da 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==20190918.1" + "home-assistant-frontend==20190919.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5eeec405e7da3d..72aebd96196c5d 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==20190918.1 +home-assistant-frontend==20190919.0 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4eb496ae8840fe..3691ce2c3791c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.1 +home-assistant-frontend==20190919.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 049ff0dbd2c144..d40ed239a09f21 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190918.1 +home-assistant-frontend==20190919.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 616b36527b715adbe653d6d285d4b5e925a72b11 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Sep 2019 15:26:49 -0700 Subject: [PATCH 0374/3953] Bumped version to 0.99.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c2b5a0d375b411..8441d9bb9e07a0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 99 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 20e61fb41b9eb6d7b79862ee39aa202dc344dc7f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 20 Sep 2019 00:32:11 +0000 Subject: [PATCH 0375/3953] [ci skip] Translation update --- .../components/izone/.translations/it.json | 15 +++++++++ .../components/plex/.translations/en.json | 33 +++++++++++++++++++ .../components/plex/.translations/it.json | 33 +++++++++++++++++++ .../components/switch/.translations/it.json | 2 ++ .../components/switch/.translations/pl.json | 2 ++ .../components/switch/.translations/sl.json | 2 ++ .../switch/.translations/zh-Hant.json | 2 ++ .../components/withings/.translations/it.json | 3 ++ .../components/withings/.translations/pl.json | 3 ++ .../components/withings/.translations/sl.json | 3 ++ .../withings/.translations/zh-Hant.json | 3 ++ 11 files changed, 101 insertions(+) create mode 100644 homeassistant/components/izone/.translations/it.json create mode 100644 homeassistant/components/plex/.translations/en.json create mode 100644 homeassistant/components/plex/.translations/it.json diff --git a/homeassistant/components/izone/.translations/it.json b/homeassistant/components/izone/.translations/it.json new file mode 100644 index 00000000000000..5498624a061ed4 --- /dev/null +++ b/homeassistant/components/izone/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo iZone trovato in rete.", + "single_instance_allowed": "\u00c8 necessaria una sola configurazione di iZone." + }, + "step": { + "confirm": { + "description": "Vuoi configurare iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json new file mode 100644 index 00000000000000..2ada5e810ecbaa --- /dev/null +++ b/homeassistant/components/plex/.translations/en.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "All linked servers already configured", + "already_configured": "This Plex server is already configured", + "already_in_progress": "Plex is being configured", + "invalid_import": "Imported configuration is invalid", + "unknown": "Failed for unknown reason" + }, + "error": { + "faulty_credentials": "Authorization failed", + "no_servers": "No servers linked to account", + "not_found": "Plex server not found" + }, + "step": { + "select_server": { + "data": { + "server": "Server" + }, + "description": "Multiple servers available, select one:", + "title": "Select Plex server" + }, + "user": { + "data": { + "token": "Plex token" + }, + "description": "Enter a Plex token for automatic setup.", + "title": "Connect Plex server" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json new file mode 100644 index 00000000000000..2e77b4ba9768b5 --- /dev/null +++ b/homeassistant/components/plex/.translations/it.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "Tutti i server collegati sono gi\u00e0 configurati", + "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", + "already_in_progress": "Plex \u00e8 in fase di configurazione", + "invalid_import": "La configurazione importata non \u00e8 valida", + "unknown": "Non riuscito per motivo sconosciuto" + }, + "error": { + "faulty_credentials": "Autorizzazione non riuscita", + "no_servers": "Nessun server collegato all'account", + "not_found": "Server Plex non trovato" + }, + "step": { + "select_server": { + "data": { + "server": "Server" + }, + "description": "Sono disponibili pi\u00f9 server, selezionarne uno:", + "title": "Selezionare il server Plex" + }, + "user": { + "data": { + "token": "Token Plex" + }, + "description": "Immettere un token Plex per la configurazione automatica.", + "title": "Collegare il server Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/it.json b/homeassistant/components/switch/.translations/it.json index 254c09380c1d1d..ec742e4113bd7c 100644 --- a/homeassistant/components/switch/.translations/it.json +++ b/homeassistant/components/switch/.translations/it.json @@ -6,6 +6,8 @@ "turn_on": "Attivare {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u00e8 disattivato", + "is_on": "{entity_name} \u00e8 attivo", "turn_off": "{entity_name} disattivato", "turn_on": "{entity_name} attivato" }, diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index c63799bf783923..921048286b6a82 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -6,6 +6,8 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { + "is_off": "(entity_name} jest wy\u0142\u0105czony.", + "is_on": "{entity_name} jest w\u0142\u0105czony", "turn_off": "{entity_name} wy\u0142\u0105czone", "turn_on": "{entity_name} w\u0142\u0105czone" }, diff --git a/homeassistant/components/switch/.translations/sl.json b/homeassistant/components/switch/.translations/sl.json index 89423e071fdc8b..f1b851b05b6f4b 100644 --- a/homeassistant/components/switch/.translations/sl.json +++ b/homeassistant/components/switch/.translations/sl.json @@ -6,6 +6,8 @@ "turn_on": "Vklopite {entity_name}" }, "condition_type": { + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen", "turn_off": "{entity_name} izklopljen", "turn_on": "{entity_name} vklopljen" }, diff --git a/homeassistant/components/switch/.translations/zh-Hant.json b/homeassistant/components/switch/.translations/zh-Hant.json index c1a67897b16c84..517d48354dcc2d 100644 --- a/homeassistant/components/switch/.translations/zh-Hant.json +++ b/homeassistant/components/switch/.translations/zh-Hant.json @@ -6,6 +6,8 @@ "turn_on": "\u958b\u555f {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u5df2\u95dc\u9589", + "is_on": "{entity_name} \u5df2\u958b\u555f", "turn_off": "{entity_name} \u5df2\u95dc\u9589", "turn_on": "{entity_name} \u5df2\u958b\u555f" }, diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json index 5bf342836ce61a..51276869ec605c 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "\u00c8 necessario configurare Withings prima di potersi autenticare con esso. Si prega di leggere la documentazione." + }, "create_entry": { "default": "Autenticazione completata con Withings per il profilo selezionato." }, diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json index 1643ecb148012b..3c345a1a788bd6 100644 --- a/homeassistant/components/withings/.translations/pl.json +++ b/homeassistant/components/withings/.translations/pl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Musisz skonfigurowa\u0107 Withings, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Przeczytaj prosz\u0119 dokumentacj\u0119." + }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu" }, diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json index d0fcb6a5276b61..71934516ea7f1b 100644 --- a/homeassistant/components/withings/.translations/sl.json +++ b/homeassistant/components/withings/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo." + }, "create_entry": { "default": "Uspe\u0161no overjen z Withings za izbrani profil." }, diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json index 30a77102d04a31..9e408eb0d5c19e 100644 --- a/homeassistant/components/withings/.translations/zh-Hant.json +++ b/homeassistant/components/withings/.translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Withings \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002\u8acb\u53c3\u95b1\u6587\u4ef6\u3002" + }, "create_entry": { "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u88dd\u7f6e\u3002" }, From 9e44d1af1991dd3822b4664c94c1fe7e5410fd7e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 20 Sep 2019 15:55:43 +0200 Subject: [PATCH 0376/3953] Revert "Add transport data from maps.yandex.ru api (#26252)" (#26762) This reverts commit 9e2cd5116acee97cc09453e564e419371e8e3e9c. --- .coveragerc | 1 - CODEOWNERS | 1 - .../components/yandex_transport/__init__.py | 1 - .../components/yandex_transport/manifest.json | 12 - .../components/yandex_transport/sensor.py | 128 - requirements_all.txt | 3 - tests/components/yandex_transport/__init__.py | 1 - .../test_yandex_transport_sensor.py | 88 - tests/fixtures/yandex_transport_reply.json | 2106 ----------------- 9 files changed, 2341 deletions(-) delete mode 100644 homeassistant/components/yandex_transport/__init__.py delete mode 100644 homeassistant/components/yandex_transport/manifest.json delete mode 100644 homeassistant/components/yandex_transport/sensor.py delete mode 100644 tests/components/yandex_transport/__init__.py delete mode 100644 tests/components/yandex_transport/test_yandex_transport_sensor.py delete mode 100644 tests/fixtures/yandex_transport_reply.json diff --git a/.coveragerc b/.coveragerc index a29586c7b6e1cd..5d5b0f6c81b09b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -747,7 +747,6 @@ omit = homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yamaha/media_player.py homeassistant/components/yamaha_musiccast/media_player.py - homeassistant/components/yandex_transport/* homeassistant/components/yeelight/* homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py diff --git a/CODEOWNERS b/CODEOWNERS index 9bcd475d5d4788..9766f011889fe9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -318,7 +318,6 @@ homeassistant/components/xiaomi_miio/* @rytilahti @syssi homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yamaha_musiccast/* @jalmeroth -homeassistant/components/yandex_transport/* @rishatik92 homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yessssms/* @flowolf diff --git a/homeassistant/components/yandex_transport/__init__.py b/homeassistant/components/yandex_transport/__init__.py deleted file mode 100644 index d007b2d3df8581..00000000000000 --- a/homeassistant/components/yandex_transport/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Service for obtaining information about closer bus from Transport Yandex Service.""" diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json deleted file mode 100644 index 54837b2eb0914d..00000000000000 --- a/homeassistant/components/yandex_transport/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "yandex_transport", - "name": "Yandex Transport", - "documentation": "https://www.home-assistant.io/components/yandex_transport", - "requirements": [ - "ya_ma==0.3.4" - ], - "dependencies": [], - "codeowners": [ - "@rishatik92" - ] -} diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py deleted file mode 100644 index 340291807ead98..00000000000000 --- a/homeassistant/components/yandex_transport/sensor.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- -"""Service for obtaining information about closer bus from Transport Yandex Service.""" - -import logging -from datetime import timedelta - -import voluptuous as vol -from ya_ma import YandexMapsRequester - -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) - -STOP_NAME = "stop_name" -USER_AGENT = "Home Assistant" -ATTRIBUTION = "Data provided by maps.yandex.ru" - -CONF_STOP_ID = "stop_id" -CONF_ROUTE = "routes" - -DEFAULT_NAME = "Yandex Transport" -ICON = "mdi:bus" - -SCAN_INTERVAL = timedelta(minutes=1) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_STOP_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ROUTE, default=[]): vol.All(cv.ensure_list, [cv.string]), - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Yandex transport sensor.""" - stop_id = config[CONF_STOP_ID] - name = config[CONF_NAME] - routes = config[CONF_ROUTE] - - data = YandexMapsRequester(user_agent=USER_AGENT) - add_entities([DiscoverMoscowYandexTransport(data, stop_id, routes, name)], True) - - -class DiscoverMoscowYandexTransport(Entity): - """Implementation of yandex_transport sensor.""" - - def __init__(self, requester, stop_id, routes, name): - """Initialize sensor.""" - self.requester = requester - self._stop_id = stop_id - self._routes = [] - self._routes = routes - self._state = None - self._name = name - self._attrs = None - - def update(self): - """Get the latest data from maps.yandex.ru and update the states.""" - attrs = {} - closer_time = None - try: - yandex_reply = self.requester.get_stop_info(self._stop_id) - data = yandex_reply["data"] - stop_metadata = data["properties"]["StopMetaData"] - except KeyError as key_error: - _LOGGER.warning( - "Exception KeyError was captured, missing key is %s. Yandex returned: %s", - key_error, - yandex_reply, - ) - self.requester.set_new_session() - data = self.requester.get_stop_info(self._stop_id)["data"] - stop_metadata = data["properties"]["StopMetaData"] - stop_name = data["properties"]["name"] - transport_list = stop_metadata["Transport"] - for transport in transport_list: - route = transport["name"] - if self._routes and route not in self._routes: - # skip unnecessary route info - continue - if "Events" in transport["BriefSchedule"]: - for event in transport["BriefSchedule"]["Events"]: - if "Estimated" in event: - posix_time_next = int(event["Estimated"]["value"]) - if closer_time is None or closer_time > posix_time_next: - closer_time = posix_time_next - if route not in attrs: - attrs[route] = [] - attrs[route].append(event["Estimated"]["text"]) - attrs[STOP_NAME] = stop_name - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - if closer_time is None: - self._state = None - else: - self._state = dt_util.utc_from_timestamp(closer_time).isoformat( - timespec="seconds" - ) - self._attrs = attrs - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attrs - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 29897b562270e7..ac49c415398022 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1994,9 +1994,6 @@ xmltodict==0.12.0 # homeassistant.components.xs1 xs1-api-client==2.3.5 -# homeassistant.components.yandex_transport -ya_ma==0.3.4 - # homeassistant.components.yweather yahooweather==0.10 diff --git a/tests/components/yandex_transport/__init__.py b/tests/components/yandex_transport/__init__.py deleted file mode 100644 index fe6b0db52d3e05..00000000000000 --- a/tests/components/yandex_transport/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the yandex transport platform.""" diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py deleted file mode 100644 index 50d945e7fae371..00000000000000 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Tests for the yandex transport platform.""" - -import json -import pytest - -import homeassistant.components.sensor as sensor -import homeassistant.util.dt as dt_util -from homeassistant.const import CONF_NAME -from tests.common import ( - assert_setup_component, - async_setup_component, - MockDependency, - load_fixture, -) - -REPLY = json.loads(load_fixture("yandex_transport_reply.json")) - - -@pytest.fixture -def mock_requester(): - """Create a mock ya_ma module and YandexMapsRequester.""" - with MockDependency("ya_ma") as ya_ma: - instance = ya_ma.YandexMapsRequester.return_value - instance.get_stop_info.return_value = REPLY - yield instance - - -STOP_ID = 9639579 -ROUTES = ["194", "т36", "т47", "м10"] -NAME = "test_name" -TEST_CONFIG = { - "sensor": { - "platform": "yandex_transport", - "stop_id": 9639579, - "routes": ROUTES, - "name": NAME, - } -} - -FILTERED_ATTRS = { - "т36": ["21:43", "21:47", "22:02"], - "т47": ["21:40", "22:01"], - "м10": ["21:48", "22:00"], - "stop_name": "7-й автобусный парк", - "attribution": "Data provided by maps.yandex.ru", -} - -RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds") - - -async def assert_setup_sensor(hass, config, count=1): - """Set up the sensor and assert it's been created.""" - with assert_setup_component(count): - assert await async_setup_component(hass, sensor.DOMAIN, config) - - -async def test_setup_platform_valid_config(hass, mock_requester): - """Test that sensor is set up properly with valid config.""" - await assert_setup_sensor(hass, TEST_CONFIG) - - -async def test_setup_platform_invalid_config(hass, mock_requester): - """Check an invalid configuration.""" - await assert_setup_sensor( - hass, {"sensor": {"platform": "yandex_transport", "stopid": 1234}}, count=0 - ) - - -async def test_name(hass, mock_requester): - """Return the name if set in the configuration.""" - await assert_setup_sensor(hass, TEST_CONFIG) - state = hass.states.get("sensor.test_name") - assert state.name == TEST_CONFIG["sensor"][CONF_NAME] - - -async def test_state(hass, mock_requester): - """Return the contents of _state.""" - await assert_setup_sensor(hass, TEST_CONFIG) - state = hass.states.get("sensor.test_name") - assert state.state == RESULT_STATE - - -async def test_filtered_attributes(hass, mock_requester): - """Return the contents of attributes.""" - await assert_setup_sensor(hass, TEST_CONFIG) - state = hass.states.get("sensor.test_name") - state_attrs = {key: state.attributes[key] for key in FILTERED_ATTRS} - assert state_attrs == FILTERED_ATTRS diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json deleted file mode 100644 index c5e4857297aa93..00000000000000 --- a/tests/fixtures/yandex_transport_reply.json +++ /dev/null @@ -1,2106 +0,0 @@ -{ - "data": { - "geometries": [ - { - "type": "Point", - "coordinates": [ - 37.565280044, - 55.851959656 - ] - } - ], - "geometry": { - "type": "Point", - "coordinates": [ - 37.565280044, - 55.851959656 - ] - }, - "properties": { - "name": "7-й автобусный парк", - "description": "7-й автобусный парк", - "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)", - "StopMetaData": { - "id": "stop__9639579", - "name": "7-й автобусный парк", - "type": "urban", - "region": { - "id": 213, - "type": 6, - "parent_id": 1, - "capital_id": 0, - "geo_parent_id": 0, - "city_id": 213, - "name": "moscow", - "native_name": "", - "iso_name": "RU MOW", - "is_main": true, - "en_name": "Moscow", - "short_en_name": "MSK", - "phone_code": "495 499", - "phone_code_old": "095", - "zip_code": "", - "population": 12506468, - "synonyms": "Moskau, Moskva", - "latitude": 55.753215, - "longitude": 37.622504, - "latitude_size": 0.878654, - "longitude_size": 1.164423, - "zoom": 10, - "tzname": "Europe/Moscow", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "weather", - "afisha", - "maps", - "tv", - "ad", - "etrain", - "subway", - "delivery", - "route" - ], - "ename": "moscow", - "bounds": [ - [ - 37.0402925, - 55.31141404514547 - ], - [ - 38.2047155, - 56.190068045145466 - ] - ], - "names": { - "ablative": "", - "accusative": "Москву", - "dative": "Москве", - "directional": "", - "genitive": "Москвы", - "instrumental": "Москвой", - "locative": "", - "nominative": "Москва", - "preposition": "в", - "prepositional": "Москве" - }, - "parent": { - "id": 1, - "type": 5, - "parent_id": 3, - "capital_id": 213, - "geo_parent_id": 0, - "city_id": 213, - "name": "moscow-and-moscow-oblast", - "native_name": "", - "iso_name": "RU-MOS", - "is_main": true, - "en_name": "Moscow and Moscow Oblast", - "short_en_name": "RU-MOS", - "phone_code": "495 496 498 499", - "phone_code_old": "", - "zip_code": "", - "population": 7503385, - "synonyms": "Московская область, Подмосковье, Podmoskovye", - "latitude": 55.815792, - "longitude": 37.380031, - "latitude_size": 2.705659, - "longitude_size": 5.060749, - "zoom": 8, - "tzname": "Europe/Moscow", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [ - 213, - 10716, - 10747, - 10758, - 20728, - 10740, - 10738, - 20523, - 10735, - 10734, - 10743, - 21622 - ], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "ad" - ], - "ename": "moscow-and-moscow-oblast", - "bounds": [ - [ - 34.8496565, - 54.439456064325434 - ], - [ - 39.9104055, - 57.14511506432543 - ] - ], - "names": { - "ablative": "", - "accusative": "Москву и Московскую область", - "dative": "Москве и Московской области", - "directional": "", - "genitive": "Москвы и Московской области", - "instrumental": "Москвой и Московской областью", - "locative": "", - "nominative": "Москва и Московская область", - "preposition": "в", - "prepositional": "Москве и Московской области" - }, - "parent": { - "id": 225, - "type": 3, - "parent_id": 10001, - "capital_id": 213, - "geo_parent_id": 0, - "city_id": 213, - "name": "russia", - "native_name": "", - "iso_name": "RU", - "is_main": false, - "en_name": "Russia", - "short_en_name": "RU", - "phone_code": "7", - "phone_code_old": "", - "zip_code": "", - "population": 146880432, - "synonyms": "Russian Federation,Российская Федерация", - "latitude": 61.698653, - "longitude": 99.505405, - "latitude_size": 40.700127, - "longitude_size": 171.643239, - "zoom": 3, - "tzname": "", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [ - 213, - 2, - 65, - 54, - 47, - 43, - 66, - 51, - 56, - 172, - 39, - 62 - ], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "ad" - ], - "ename": "russia", - "bounds": [ - [ - 13.683785499999999, - 35.290400699917846 - ], - [ - -174.6729755, - 75.99052769991785 - ] - ], - "names": { - "ablative": "", - "accusative": "Россию", - "dative": "России", - "directional": "", - "genitive": "России", - "instrumental": "Россией", - "locative": "", - "nominative": "Россия", - "preposition": "в", - "prepositional": "России" - } - } - } - }, - "Transport": [ - { - "lineId": "2036925416", - "name": "194", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036927196", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9648742", - "name": "Коровино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659860", - "tzOffset": 10800, - "text": "21:51" - } - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661840", - "tzOffset": 10800, - "text": "22:24" - } - } - ], - "departureTime": "21:51" - } - } - ], - "threadId": "2036927196", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9648742", - "name": "Коровино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659860", - "tzOffset": 10800, - "text": "21:51" - } - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661840", - "tzOffset": 10800, - "text": "22:24" - } - } - ], - "departureTime": "21:51" - } - }, - { - "lineId": "213_114_bus_mosgortrans", - "name": "114", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_114_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568603405", - "tzOffset": 10800, - "text": "6:10" - }, - "end": { - "value": "1568672165", - "tzOffset": 10800, - "text": "1:16" - } - } - } - } - ], - "threadId": "213B_114_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568603405", - "tzOffset": 10800, - "text": "6:10" - }, - "end": { - "value": "1568672165", - "tzOffset": 10800, - "text": "1:16" - } - } - } - }, - { - "lineId": "213_154_bus_mosgortrans", - "name": "154", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_154_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642548", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659260", - "tzOffset": 10800, - "text": "21:41" - }, - "Estimated": { - "value": "1568659252", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1054764%5F191500" - }, - { - "Scheduled": { - "value": "1568660580", - "tzOffset": 10800, - "text": "22:03" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:41" - } - } - ], - "threadId": "213B_154_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642548", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659260", - "tzOffset": 10800, - "text": "21:41" - }, - "Estimated": { - "value": "1568659252", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1054764%5F191500" - }, - { - "Scheduled": { - "value": "1568660580", - "tzOffset": 10800, - "text": "22:03" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:41" - } - }, - { - "lineId": "213_179_bus_mosgortrans", - "name": "179", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_179_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568659351", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|59832%5F31359" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661660", - "tzOffset": 10800, - "text": "22:21" - } - } - ], - "departureTime": "21:52" - } - } - ], - "threadId": "213B_179_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568659351", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|59832%5F31359" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661660", - "tzOffset": 10800, - "text": "22:21" - } - } - ], - "departureTime": "21:52" - } - }, - { - "lineId": "213_191m_minibus_default", - "name": "591", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_191m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660525", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|38278%5F9345312" - } - ], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568602033", - "tzOffset": 10800, - "text": "5:47" - }, - "end": { - "value": "1568672233", - "tzOffset": 10800, - "text": "1:17" - } - } - } - } - ], - "threadId": "213A_191m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660525", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|38278%5F9345312" - } - ], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568602033", - "tzOffset": 10800, - "text": "5:47" - }, - "end": { - "value": "1568672233", - "tzOffset": 10800, - "text": "1:17" - } - } - } - }, - { - "lineId": "213_206m_minibus_default", - "name": "206к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_206m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568601239", - "tzOffset": 10800, - "text": "5:33" - }, - "end": { - "value": "1568671439", - "tzOffset": 10800, - "text": "1:03" - } - } - } - } - ], - "threadId": "213A_206m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568601239", - "tzOffset": 10800, - "text": "5:33" - }, - "end": { - "value": "1568671439", - "tzOffset": 10800, - "text": "1:03" - } - } - } - }, - { - "lineId": "213_215_bus_mosgortrans", - "name": "215", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_215_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "27 мин", - "value": 1620, - "begin": { - "value": "1568601276", - "tzOffset": 10800, - "text": "5:34" - }, - "end": { - "value": "1568671476", - "tzOffset": 10800, - "text": "1:04" - } - } - } - } - ], - "threadId": "213B_215_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "27 мин", - "value": 1620, - "begin": { - "value": "1568601276", - "tzOffset": 10800, - "text": "5:34" - }, - "end": { - "value": "1568671476", - "tzOffset": 10800, - "text": "1:04" - } - } - } - }, - { - "lineId": "213_282_bus_mosgortrans", - "name": "282", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_282_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9641102", - "name": "Улица Корнейчука" - }, - { - "id": "2532226085", - "name": "Метро Войковская" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659888", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|34874%5F9345408" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568602180", - "tzOffset": 10800, - "text": "5:49" - }, - "end": { - "value": "1568673460", - "tzOffset": 10800, - "text": "1:37" - } - } - } - } - ], - "threadId": "213A_282_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9641102", - "name": "Улица Корнейчука" - }, - { - "id": "2532226085", - "name": "Метро Войковская" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659888", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|34874%5F9345408" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568602180", - "tzOffset": 10800, - "text": "5:49" - }, - "end": { - "value": "1568673460", - "tzOffset": 10800, - "text": "1:37" - } - } - } - }, - { - "lineId": "213_294m_minibus_default", - "name": "994", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_294m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9649459", - "name": "Метро Алтуфьево" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "30 мин", - "value": 1800, - "begin": { - "value": "1568601527", - "tzOffset": 10800, - "text": "5:38" - }, - "end": { - "value": "1568671727", - "tzOffset": 10800, - "text": "1:08" - } - } - } - } - ], - "threadId": "213A_294m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9649459", - "name": "Метро Алтуфьево" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "30 мин", - "value": 1800, - "begin": { - "value": "1568601527", - "tzOffset": 10800, - "text": "5:38" - }, - "end": { - "value": "1568671727", - "tzOffset": 10800, - "text": "1:08" - } - } - } - }, - { - "lineId": "213_36_trolleybus_mosgortrans", - "name": "т36", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_36_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642550", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9640641", - "name": "Дмитровское шоссе, 155" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - }, - "Estimated": { - "value": "1568659426", - "tzOffset": 10800, - "text": "21:43" - }, - "vehicleId": "codd%5Fnew|1084829%5F430260" - }, - { - "Scheduled": { - "value": "1568660520", - "tzOffset": 10800, - "text": "22:02" - }, - "Estimated": { - "value": "1568659656", - "tzOffset": 10800, - "text": "21:47" - }, - "vehicleId": "codd%5Fnew|1117016%5F430280" - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - }, - "Estimated": { - "value": "1568660538", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|1054576%5F430226" - } - ], - "departureTime": "21:48" - } - } - ], - "threadId": "213A_36_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642550", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9640641", - "name": "Дмитровское шоссе, 155" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - }, - "Estimated": { - "value": "1568659426", - "tzOffset": 10800, - "text": "21:43" - }, - "vehicleId": "codd%5Fnew|1084829%5F430260" - }, - { - "Scheduled": { - "value": "1568660520", - "tzOffset": 10800, - "text": "22:02" - }, - "Estimated": { - "value": "1568659656", - "tzOffset": 10800, - "text": "21:47" - }, - "vehicleId": "codd%5Fnew|1117016%5F430280" - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - }, - "Estimated": { - "value": "1568660538", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|1054576%5F430226" - } - ], - "departureTime": "21:48" - } - }, - { - "lineId": "213_47_trolleybus_mosgortrans", - "name": "т47", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_47_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639568", - "name": "Бескудниковский переулок" - }, - { - "id": "stop__9641903", - "name": "Бескудниковский переулок" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - }, - "Estimated": { - "value": "1568659253", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1112219%5F430329" - }, - { - "Scheduled": { - "value": "1568660940", - "tzOffset": 10800, - "text": "22:09" - }, - "Estimated": { - "value": "1568660519", - "tzOffset": 10800, - "text": "22:01" - }, - "vehicleId": "codd%5Fnew|1139620%5F430382" - }, - { - "Scheduled": { - "value": "1568663580", - "tzOffset": 10800, - "text": "22:53" - } - } - ], - "departureTime": "21:53" - } - } - ], - "threadId": "213B_47_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639568", - "name": "Бескудниковский переулок" - }, - { - "id": "stop__9641903", - "name": "Бескудниковский переулок" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - }, - "Estimated": { - "value": "1568659253", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1112219%5F430329" - }, - { - "Scheduled": { - "value": "1568660940", - "tzOffset": 10800, - "text": "22:09" - }, - "Estimated": { - "value": "1568660519", - "tzOffset": 10800, - "text": "22:01" - }, - "vehicleId": "codd%5Fnew|1139620%5F430382" - }, - { - "Scheduled": { - "value": "1568663580", - "tzOffset": 10800, - "text": "22:53" - } - } - ], - "departureTime": "21:53" - } - }, - { - "lineId": "213_56_trolleybus_mosgortrans", - "name": "т56", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_56_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639561", - "name": "Коровинское шоссе" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660675", - "tzOffset": 10800, - "text": "22:04" - }, - "vehicleId": "codd%5Fnew|146304%5F31207" - } - ], - "Frequency": { - "text": "8 мин", - "value": 480, - "begin": { - "value": "1568606244", - "tzOffset": 10800, - "text": "6:57" - }, - "end": { - "value": "1568670144", - "tzOffset": 10800, - "text": "0:42" - } - } - } - } - ], - "threadId": "213A_56_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639561", - "name": "Коровинское шоссе" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660675", - "tzOffset": 10800, - "text": "22:04" - }, - "vehicleId": "codd%5Fnew|146304%5F31207" - } - ], - "Frequency": { - "text": "8 мин", - "value": 480, - "begin": { - "value": "1568606244", - "tzOffset": 10800, - "text": "6:57" - }, - "end": { - "value": "1568670144", - "tzOffset": 10800, - "text": "0:42" - } - } - } - }, - { - "lineId": "213_63_bus_mosgortrans", - "name": "63", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_63_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|38921%5F9215306" - }, - { - "Estimated": { - "value": "1568660136", - "tzOffset": 10800, - "text": "21:55" - }, - "vehicleId": "codd%5Fnew|38918%5F9215303" - } - ], - "Frequency": { - "text": "17 мин", - "value": 1020, - "begin": { - "value": "1568600987", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568670227", - "tzOffset": 10800, - "text": "0:43" - } - } - } - } - ], - "threadId": "213A_63_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|38921%5F9215306" - }, - { - "Estimated": { - "value": "1568660136", - "tzOffset": 10800, - "text": "21:55" - }, - "vehicleId": "codd%5Fnew|38918%5F9215303" - } - ], - "Frequency": { - "text": "17 мин", - "value": 1020, - "begin": { - "value": "1568600987", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568670227", - "tzOffset": 10800, - "text": "0:43" - } - } - } - }, - { - "lineId": "213_677_bus_mosgortrans", - "name": "677", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_677_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639495", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|11731%5F31376" - } - ], - "Frequency": { - "text": "4 мин", - "value": 240, - "begin": { - "value": "1568600940", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568672640", - "tzOffset": 10800, - "text": "1:24" - } - } - } - } - ], - "threadId": "213B_677_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639495", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|11731%5F31376" - } - ], - "Frequency": { - "text": "4 мин", - "value": 240, - "begin": { - "value": "1568600940", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568672640", - "tzOffset": 10800, - "text": "1:24" - } - } - } - }, - { - "lineId": "213_692_bus_mosgortrans", - "name": "692", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036928706", - "EssentialStops": [ - { - "id": "3163417967", - "name": "Платформа Дегунино" - }, - { - "id": "3163417967", - "name": "Платформа Дегунино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568660280", - "tzOffset": 10800, - "text": "21:58" - }, - "Estimated": { - "value": "1568660255", - "tzOffset": 10800, - "text": "21:57" - }, - "vehicleId": "codd%5Fnew|63029%5F31485" - }, - { - "Scheduled": { - "value": "1568693340", - "tzOffset": 10800, - "text": "7:09" - } - }, - { - "Scheduled": { - "value": "1568696940", - "tzOffset": 10800, - "text": "8:09" - } - } - ], - "departureTime": "21:58" - } - } - ], - "threadId": "2036928706", - "EssentialStops": [ - { - "id": "3163417967", - "name": "Платформа Дегунино" - }, - { - "id": "3163417967", - "name": "Платформа Дегунино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568660280", - "tzOffset": 10800, - "text": "21:58" - }, - "Estimated": { - "value": "1568660255", - "tzOffset": 10800, - "text": "21:57" - }, - "vehicleId": "codd%5Fnew|63029%5F31485" - }, - { - "Scheduled": { - "value": "1568693340", - "tzOffset": 10800, - "text": "7:09" - } - }, - { - "Scheduled": { - "value": "1568696940", - "tzOffset": 10800, - "text": "8:09" - } - } - ], - "departureTime": "21:58" - } - }, - { - "lineId": "213_78_trolleybus_mosgortrans", - "name": "т78", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_78_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9887464", - "name": "9-я Северная линия" - }, - { - "id": "stop__9887464", - "name": "9-я Северная линия" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659620", - "tzOffset": 10800, - "text": "21:47" - }, - "Estimated": { - "value": "1568659898", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|147522%5F31184" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:47" - } - } - ], - "threadId": "213A_78_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9887464", - "name": "9-я Северная линия" - }, - { - "id": "stop__9887464", - "name": "9-я Северная линия" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659620", - "tzOffset": 10800, - "text": "21:47" - }, - "Estimated": { - "value": "1568659898", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|147522%5F31184" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:47" - } - }, - { - "lineId": "213_82_bus_mosgortrans", - "name": "82", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036925244", - "EssentialStops": [ - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - }, - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - } - }, - { - "Scheduled": { - "value": "1568661780", - "tzOffset": 10800, - "text": "22:23" - } - }, - { - "Scheduled": { - "value": "1568663760", - "tzOffset": 10800, - "text": "22:56" - } - } - ], - "departureTime": "21:48" - } - } - ], - "threadId": "2036925244", - "EssentialStops": [ - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - }, - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - } - }, - { - "Scheduled": { - "value": "1568661780", - "tzOffset": 10800, - "text": "22:23" - } - }, - { - "Scheduled": { - "value": "1568663760", - "tzOffset": 10800, - "text": "22:56" - } - } - ], - "departureTime": "21:48" - } - }, - { - "lineId": "2465131598", - "name": "179к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2465131758", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659500", - "tzOffset": 10800, - "text": "21:45" - } - }, - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - } - }, - { - "Scheduled": { - "value": "1568660880", - "tzOffset": 10800, - "text": "22:08" - } - } - ], - "departureTime": "21:45" - } - } - ], - "threadId": "2465131758", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659500", - "tzOffset": 10800, - "text": "21:45" - } - }, - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - } - }, - { - "Scheduled": { - "value": "1568660880", - "tzOffset": 10800, - "text": "22:08" - } - } - ], - "departureTime": "21:45" - } - }, - { - "lineId": "466_bus_default", - "name": "466", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "466B_bus_default", - "EssentialStops": [ - { - "id": "stop__9640546", - "name": "Станция Бескудниково" - }, - { - "id": "stop__9640545", - "name": "Станция Бескудниково" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568604647", - "tzOffset": 10800, - "text": "6:30" - }, - "end": { - "value": "1568675447", - "tzOffset": 10800, - "text": "2:10" - } - } - } - } - ], - "threadId": "466B_bus_default", - "EssentialStops": [ - { - "id": "stop__9640546", - "name": "Станция Бескудниково" - }, - { - "id": "stop__9640545", - "name": "Станция Бескудниково" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568604647", - "tzOffset": 10800, - "text": "6:30" - }, - "end": { - "value": "1568675447", - "tzOffset": 10800, - "text": "2:10" - } - } - } - }, - { - "lineId": "677k_bus_default", - "name": "677к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "677kA_bus_default", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568660003", - "tzOffset": 10800, - "text": "21:53" - }, - "vehicleId": "codd%5Fnew|130308%5F31319" - }, - { - "Scheduled": { - "value": "1568661240", - "tzOffset": 10800, - "text": "22:14" - } - }, - { - "Scheduled": { - "value": "1568662500", - "tzOffset": 10800, - "text": "22:35" - } - } - ], - "departureTime": "21:52" - } - } - ], - "threadId": "677kA_bus_default", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568660003", - "tzOffset": 10800, - "text": "21:53" - }, - "vehicleId": "codd%5Fnew|130308%5F31319" - }, - { - "Scheduled": { - "value": "1568661240", - "tzOffset": 10800, - "text": "22:14" - } - }, - { - "Scheduled": { - "value": "1568662500", - "tzOffset": 10800, - "text": "22:35" - } - } - ], - "departureTime": "21:52" - } - }, - { - "lineId": "m10_bus_default", - "name": "м10", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036926048", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659718", - "tzOffset": 10800, - "text": "21:48" - }, - "vehicleId": "codd%5Fnew|146260%5F31212" - }, - { - "Estimated": { - "value": "1568660422", - "tzOffset": 10800, - "text": "22:00" - }, - "vehicleId": "codd%5Fnew|13997%5F31247" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568606903", - "tzOffset": 10800, - "text": "7:08" - }, - "end": { - "value": "1568675183", - "tzOffset": 10800, - "text": "2:06" - } - } - } - } - ], - "threadId": "2036926048", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659718", - "tzOffset": 10800, - "text": "21:48" - }, - "vehicleId": "codd%5Fnew|146260%5F31212" - }, - { - "Estimated": { - "value": "1568660422", - "tzOffset": 10800, - "text": "22:00" - }, - "vehicleId": "codd%5Fnew|13997%5F31247" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568606903", - "tzOffset": 10800, - "text": "7:08" - }, - "end": { - "value": "1568675183", - "tzOffset": 10800, - "text": "2:06" - } - } - } - } - ] - } - }, - "toponymSeoname": "dmitrovskoye_shosse" - } -} From 6a3132344c8644f11bde973a18b73dcaac8421d7 Mon Sep 17 00:00:00 2001 From: Florian Klien Date: Fri, 20 Sep 2019 15:58:46 +0200 Subject: [PATCH 0377/3953] Bump openwrt-luci-rpc to version 1.1.1 (#26759) * Revert "luci device-tracker dependency fix (#26215)" This reverts commit 1e61d50fc52d6467565dde34b8d44905204a9093. * bump openwrt-luci-rpc to 1.1.1 fix missing dependency permanent fix for #26215 --- homeassistant/components/luci/manifest.json | 3 +-- requirements_all.txt | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 153f6b5aea6972..dffb4b52667369 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -3,8 +3,7 @@ "name": "Luci", "documentation": "https://www.home-assistant.io/components/luci", "requirements": [ - "openwrt-luci-rpc==1.1.0", - "packaging==19.1" + "openwrt-luci-rpc==1.1.1" ], "dependencies": [], "codeowners": ["@fbradyirl"] diff --git a/requirements_all.txt b/requirements_all.txt index ac49c415398022..522f06bca021fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -913,14 +913,11 @@ opensensemap-api==0.1.5 openwebifpy==3.1.1 # homeassistant.components.luci -openwrt-luci-rpc==1.1.0 +openwrt-luci-rpc==1.1.1 # 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 54242cd65c1d69d88ec366f545313cf003e9cbbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 20 Sep 2019 18:23:34 +0300 Subject: [PATCH 0378/3953] Type hint additions (#26765) --- .../components/automation/__init__.py | 7 ++++--- homeassistant/components/cover/__init__.py | 15 +++++++------- homeassistant/components/frontend/__init__.py | 9 ++++++--- homeassistant/components/http/ban.py | 6 +++--- .../media_player/reproduce_state.py | 4 ++-- homeassistant/components/switch/light.py | 12 +++++++---- homeassistant/helpers/config_validation.py | 20 +++++++++---------- homeassistant/helpers/script.py | 9 ++++----- homeassistant/helpers/template.py | 18 ++++++++--------- homeassistant/scripts/__init__.py | 12 +++++------ 10 files changed, 60 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 9e08a9cff1fc40..f0529f126f1e73 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -3,6 +3,7 @@ from functools import partial import importlib import logging +from typing import Any import voluptuous as vol @@ -34,7 +35,7 @@ from homeassistant.util.dt import parse_datetime, utcnow -# 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 DOMAIN = "automation" @@ -281,11 +282,11 @@ async def async_added_to_hass(self) -> None: if enable_automation: await self.async_enable() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on and update the state.""" await self.async_enable() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self.async_disable() diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index d491765bb00f57..8d2b4430fe110c 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import functools as ft import logging +from typing import Any import voluptuous as vol @@ -33,7 +34,7 @@ ) -# mypy: allow-untyped-calls, allow-incomplete-defs, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -263,7 +264,7 @@ def is_closed(self): """Return if the cover is closed or not.""" raise NotImplementedError() - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" raise NotImplementedError() @@ -274,7 +275,7 @@ def async_open_cover(self, **kwargs): """ return self.hass.async_add_job(ft.partial(self.open_cover, **kwargs)) - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close cover.""" raise NotImplementedError() @@ -285,7 +286,7 @@ def async_close_cover(self, **kwargs): """ return self.hass.async_add_job(ft.partial(self.close_cover, **kwargs)) - def toggle(self, **kwargs) -> None: + def toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" if self.is_closed: self.open_cover(**kwargs) @@ -323,7 +324,7 @@ def async_stop_cover(self, **kwargs): """ return self.hass.async_add_job(ft.partial(self.stop_cover, **kwargs)) - def open_cover_tilt(self, **kwargs): + def open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" pass @@ -334,7 +335,7 @@ def async_open_cover_tilt(self, **kwargs): """ return self.hass.async_add_job(ft.partial(self.open_cover_tilt, **kwargs)) - def close_cover_tilt(self, **kwargs): + def close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" pass @@ -369,7 +370,7 @@ def async_stop_cover_tilt(self, **kwargs): """ return self.hass.async_add_job(ft.partial(self.stop_cover_tilt, **kwargs)) - def toggle_tilt(self, **kwargs) -> None: + def toggle_tilt(self, **kwargs: Any) -> None: """Toggle the entity.""" if self.current_cover_tilt_position == 0: self.open_cover_tilt(**kwargs) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7298ce8c1d086a..8ef662ec878f90 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -4,6 +4,7 @@ import mimetypes import os import pathlib +from typing import Optional, Set, Tuple from aiohttp import web, web_urldispatcher, hdrs import voluptuous as vol @@ -22,7 +23,7 @@ from .storage import async_setup_frontend_storage -# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs # Fix mimetypes for borked Windows machines # https://github.com/home-assistant/home-assistant-polymer/issues/3336 @@ -400,7 +401,9 @@ def url_for(self, **kwargs: str) -> URL: """Construct url for resource with additional params.""" return URL("/") - async def resolve(self, request: web.Request): + async def resolve( + self, request: web.Request + ) -> Tuple[Optional[web_urldispatcher.UrlMappingMatchInfo], Set[str]]: """Resolve resource. Return (UrlMappingMatchInfo, allowed_methods) pair. @@ -447,7 +450,7 @@ def get_template(self): return tpl - async def get(self, request: web.Request): + async def get(self, request: web.Request) -> web.Response: """Serve the index page for panel pages.""" hass = request.app["hass"] diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index d8fa8853c7f18c..7d1e24f369800f 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -18,7 +18,7 @@ from .const import KEY_REAL_IP -# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -165,7 +165,7 @@ def __init__(self, ip_ban: str, banned_at: Optional[datetime] = None) -> None: self.banned_at = banned_at or datetime.utcnow() -async def async_load_ip_bans_config(hass: HomeAssistant, path: str): +async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> List[IpBan]: """Load list of banned IPs from config file.""" ip_list: List[IpBan] = [] @@ -188,7 +188,7 @@ async def async_load_ip_bans_config(hass: HomeAssistant, path: str): return ip_list -def update_ip_bans_config(path: str, ip_ban: IpBan): +def update_ip_bans_config(path: str, ip_ban: IpBan) -> None: """Update config file with new banned IP address.""" with open(path, "a") as out: ip_ = { diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 4eba4657d9554d..dac08afe471dea 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -36,7 +36,7 @@ ) -# mypy: allow-incomplete-defs, allow-untyped-defs +# mypy: allow-untyped-defs async def _async_reproduce_states( @@ -44,7 +44,7 @@ async def _async_reproduce_states( ) -> None: """Reproduce component states.""" - async def call_service(service: str, keys: Iterable): + async def call_service(service: str, keys: Iterable) -> None: """Call service with set of attributes given.""" data = {} data["entity_id"] = state.entity_id diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 2027a8fc458234..8f3b5d87f8c202 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,6 +1,6 @@ """Light support for switch entities.""" import logging -from typing import cast +from typing import cast, Callable, Dict, Optional, Sequence import voluptuous as vol @@ -14,13 +14,14 @@ ) from homeassistant.core import State, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change 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 +# mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,10 @@ async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistantType, + config: ConfigType, + async_add_entities: Callable[[Sequence[Entity], bool], None], + discovery_info: Optional[Dict] = None, ) -> None: """Initialize Light Switch platform.""" async_add_entities( @@ -105,7 +109,7 @@ async def async_added_to_hass(self) -> None: @callback def async_state_changed_listener( entity_id: str, old_state: State, new_state: State - ): + ) -> None: """Handle child updates.""" self.async_schedule_update_ha_state(True) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index e53954a65ddb0d..952fa41c42c1ce 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -52,7 +52,7 @@ from homeassistant.util import slugify as util_slugify -# 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 # pylint: disable=invalid-name @@ -95,7 +95,7 @@ def validate(obj: Dict) -> Dict: return validate -def has_at_most_one_key(*keys: str) -> Callable: +def has_at_most_one_key(*keys: str) -> Callable[[Dict], Dict]: """Validate that zero keys exist or one key exists.""" def validate(obj: Dict) -> Dict: @@ -224,7 +224,7 @@ def entity_ids(value: Union[str, List]) -> List[str]: comp_entity_ids = vol.Any(vol.All(vol.Lower, ENTITY_MATCH_ALL), entity_ids) -def entity_domain(domain: str): +def entity_domain(domain: str) -> Callable[[Any], str]: """Validate that entity belong to domain.""" def validate(value: Any) -> str: @@ -235,7 +235,7 @@ def validate(value: Any) -> str: return validate -def entities_domain(domain: str): +def entities_domain(domain: str) -> Callable[[Union[str, List]], List[str]]: """Validate that entities belong to domain.""" def validate(values: Union[str, List]) -> List[str]: @@ -284,7 +284,7 @@ def icon(value): ) -def time(value) -> time_sys: +def time(value: Any) -> time_sys: """Validate and transform a time.""" if isinstance(value, time_sys): return value @@ -300,7 +300,7 @@ def time(value) -> time_sys: return time_val -def date(value) -> date_sys: +def date(value: Any) -> date_sys: """Validate and transform a date.""" if isinstance(value, date_sys): return value @@ -439,7 +439,7 @@ def string(value: Any) -> str: return str(value) -def temperature_unit(value) -> str: +def temperature_unit(value: Any) -> str: """Validate and transform temperature unit.""" value = str(value).upper() if value == "C": @@ -578,7 +578,7 @@ def deprecated( replacement_key: Optional[str] = None, invalidation_version: Optional[str] = None, default: Optional[Any] = None, -): +) -> Callable[[Dict], Dict]: """ Log key as deprecated and provide a replacement (if exists). @@ -626,7 +626,7 @@ def deprecated( " deprecated, please remove it from your configuration" ) - def check_for_invalid_version(value: Optional[Any]): + def check_for_invalid_version(value: Optional[Any]) -> None: """Raise error if current version has reached invalidation.""" if not invalidation_version: return @@ -641,7 +641,7 @@ def check_for_invalid_version(value: Optional[Any]): ) ) - def validator(config: Dict): + def validator(config: Dict) -> Dict: """Check if key is in config and log warning.""" if key in config: value = config[key] diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 0b569e2d4ad1d0..23728b651098aa 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -4,7 +4,7 @@ from contextlib import suppress from datetime import datetime from itertools import islice -from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple +from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple, Any import voluptuous as vol @@ -32,8 +32,7 @@ from homeassistant.util.async_ import run_coroutine_threadsafe, run_callback_threadsafe -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -101,9 +100,9 @@ class Script: def __init__( self, hass: HomeAssistant, - sequence, + sequence: Sequence[Dict[str, Any]], name: Optional[str] = None, - change_listener=None, + change_listener: Optional[Callable[..., Any]] = None, ) -> None: """Initialize the script.""" self.hass = hass diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 98e3849bfb6332..9af1998e894ebe 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -7,7 +7,7 @@ import re from datetime import datetime from functools import wraps -from typing import Iterable +from typing import Any, Iterable import jinja2 from jinja2 import contextfilter, contextfunction @@ -25,13 +25,13 @@ from homeassistant.core import State, callback, split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper -from homeassistant.helpers.typing import TemplateVarsType +from homeassistant.helpers.typing import HomeAssistantType, TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import convert, dt as dt_util, location as loc_util from homeassistant.util.async_ import run_callback_threadsafe -# 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__) @@ -106,7 +106,7 @@ def extract_entities(template, variables=None): return MATCH_ALL -def _true(arg) -> bool: +def _true(arg: Any) -> bool: return True @@ -191,7 +191,7 @@ def extract_entities(self, variables=None): """Extract all entities for state_changed listener.""" return extract_entities(self.template, variables) - def render(self, variables: TemplateVarsType = None, **kwargs): + def render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str: """Render given template.""" if variables is not None: kwargs.update(variables) @@ -201,7 +201,7 @@ def render(self, variables: TemplateVarsType = None, **kwargs): ).result() @callback - def async_render(self, variables: TemplateVarsType = None, **kwargs) -> str: + def async_render(self, variables: TemplateVarsType = None, **kwargs: Any) -> str: """Render given template. This method must be run in the event loop. @@ -218,7 +218,7 @@ def async_render(self, variables: TemplateVarsType = None, **kwargs) -> str: @callback def async_render_to_info( - self, variables: TemplateVarsType = None, **kwargs + self, variables: TemplateVarsType = None, **kwargs: Any ) -> RenderInfo: """Render the template and collect an entity filter.""" assert self.hass and _RENDER_INFO not in self.hass.data @@ -479,7 +479,7 @@ def _resolve_state(hass, entity_id_or_state): return None -def expand(hass, *args) -> Iterable[State]: +def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]: """Expand out any groups into entity states.""" search = list(args) found = {} @@ -635,7 +635,7 @@ def distance(hass, *args): ) -def is_state(hass, entity_id: str, state: State) -> bool: +def is_state(hass: HomeAssistantType, entity_id: str, state: State) -> bool: """Test if a state is a specific value.""" state_obj = _get_state(hass, entity_id) return state_obj is not None and state_obj.state == state diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 0a9bac301883b4..00f5984c58ba43 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -5,7 +5,7 @@ import logging import os import sys -from typing import List +from typing import List, Optional, Sequence, Text from homeassistant.bootstrap import async_mount_local_lib_path from homeassistant.config import get_default_config_dir @@ -13,7 +13,7 @@ from homeassistant.util.package import install_package, is_virtual_env, is_installed -# mypy: allow-untyped-defs, allow-incomplete-defs, no-warn-return-any +# mypy: allow-untyped-defs, no-warn-return-any def run(args: List) -> int: @@ -62,13 +62,13 @@ def run(args: List) -> int: return script.run(args[1:]) # type: ignore -def extract_config_dir(args=None) -> str: +def extract_config_dir(args: Optional[Sequence[Text]] = None) -> str: """Extract the config dir from the arguments or get the default.""" parser = argparse.ArgumentParser(add_help=False) parser.add_argument("-c", "--config", default=None) - args = parser.parse_known_args(args)[0] + parsed_args = parser.parse_known_args(args)[0] return ( - os.path.join(os.getcwd(), args.config) - if args.config + os.path.join(os.getcwd(), parsed_args.config) + if parsed_args.config else get_default_config_dir() ) From aaf0f9890d635f1ef9a696af107c935b26e78cc3 Mon Sep 17 00:00:00 2001 From: Askarov Rishat Date: Fri, 20 Sep 2019 19:12:36 +0300 Subject: [PATCH 0379/3953] Add transport data from maps.yandex.ru api (#26766) * adding feature obtaining Moscow transport data from maps.yandex.ru api * extracting the YandexMapsRequester to pypi * fix code review comments * fix stop_name, state in datetime, logger formating * fix comments * add docstring to init * rename, because it works not only Moscow, but many another big cities in Russia * fix comments * Try to solve relative view in sensor timestamp * back to isoformat * add tests, update external library version * flake8 and black tests for sensor.py * fix manifest.json * update tests, migrate to pytest, async, Using MockDependency * move json to tests/fixtures * script/lint fixes * fix comments * removing check_filter function * fix typo * up version on manifest.json * up version to 0.3.7 in requirements_all.txt --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/yandex_transport/__init__.py | 1 + .../components/yandex_transport/manifest.json | 12 + .../components/yandex_transport/sensor.py | 128 + requirements_all.txt | 3 + tests/components/yandex_transport/__init__.py | 1 + .../test_yandex_transport_sensor.py | 88 + tests/fixtures/yandex_transport_reply.json | 2106 +++++++++++++++++ 9 files changed, 2341 insertions(+) create mode 100644 homeassistant/components/yandex_transport/__init__.py create mode 100644 homeassistant/components/yandex_transport/manifest.json create mode 100644 homeassistant/components/yandex_transport/sensor.py create mode 100644 tests/components/yandex_transport/__init__.py create mode 100644 tests/components/yandex_transport/test_yandex_transport_sensor.py create mode 100644 tests/fixtures/yandex_transport_reply.json diff --git a/.coveragerc b/.coveragerc index 5d5b0f6c81b09b..a29586c7b6e1cd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -747,6 +747,7 @@ omit = homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yamaha/media_player.py homeassistant/components/yamaha_musiccast/media_player.py + homeassistant/components/yandex_transport/* homeassistant/components/yeelight/* homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py diff --git a/CODEOWNERS b/CODEOWNERS index 9766f011889fe9..9bcd475d5d4788 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -318,6 +318,7 @@ homeassistant/components/xiaomi_miio/* @rytilahti @syssi homeassistant/components/xiaomi_tv/* @simse homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yamaha_musiccast/* @jalmeroth +homeassistant/components/yandex_transport/* @rishatik92 homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yessssms/* @flowolf diff --git a/homeassistant/components/yandex_transport/__init__.py b/homeassistant/components/yandex_transport/__init__.py new file mode 100644 index 00000000000000..d007b2d3df8581 --- /dev/null +++ b/homeassistant/components/yandex_transport/__init__.py @@ -0,0 +1 @@ +"""Service for obtaining information about closer bus from Transport Yandex Service.""" diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json new file mode 100644 index 00000000000000..6c633f848c0c68 --- /dev/null +++ b/homeassistant/components/yandex_transport/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "yandex_transport", + "name": "Yandex Transport", + "documentation": "https://www.home-assistant.io/components/yandex_transport", + "requirements": [ + "ya_ma==0.3.7" + ], + "dependencies": [], + "codeowners": [ + "@rishatik92" + ] +} diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py new file mode 100644 index 00000000000000..340291807ead98 --- /dev/null +++ b/homeassistant/components/yandex_transport/sensor.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +"""Service for obtaining information about closer bus from Transport Yandex Service.""" + +import logging +from datetime import timedelta + +import voluptuous as vol +from ya_ma import YandexMapsRequester + +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +STOP_NAME = "stop_name" +USER_AGENT = "Home Assistant" +ATTRIBUTION = "Data provided by maps.yandex.ru" + +CONF_STOP_ID = "stop_id" +CONF_ROUTE = "routes" + +DEFAULT_NAME = "Yandex Transport" +ICON = "mdi:bus" + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STOP_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ROUTE, default=[]): vol.All(cv.ensure_list, [cv.string]), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Yandex transport sensor.""" + stop_id = config[CONF_STOP_ID] + name = config[CONF_NAME] + routes = config[CONF_ROUTE] + + data = YandexMapsRequester(user_agent=USER_AGENT) + add_entities([DiscoverMoscowYandexTransport(data, stop_id, routes, name)], True) + + +class DiscoverMoscowYandexTransport(Entity): + """Implementation of yandex_transport sensor.""" + + def __init__(self, requester, stop_id, routes, name): + """Initialize sensor.""" + self.requester = requester + self._stop_id = stop_id + self._routes = [] + self._routes = routes + self._state = None + self._name = name + self._attrs = None + + def update(self): + """Get the latest data from maps.yandex.ru and update the states.""" + attrs = {} + closer_time = None + try: + yandex_reply = self.requester.get_stop_info(self._stop_id) + data = yandex_reply["data"] + stop_metadata = data["properties"]["StopMetaData"] + except KeyError as key_error: + _LOGGER.warning( + "Exception KeyError was captured, missing key is %s. Yandex returned: %s", + key_error, + yandex_reply, + ) + self.requester.set_new_session() + data = self.requester.get_stop_info(self._stop_id)["data"] + stop_metadata = data["properties"]["StopMetaData"] + stop_name = data["properties"]["name"] + transport_list = stop_metadata["Transport"] + for transport in transport_list: + route = transport["name"] + if self._routes and route not in self._routes: + # skip unnecessary route info + continue + if "Events" in transport["BriefSchedule"]: + for event in transport["BriefSchedule"]["Events"]: + if "Estimated" in event: + posix_time_next = int(event["Estimated"]["value"]) + if closer_time is None or closer_time > posix_time_next: + closer_time = posix_time_next + if route not in attrs: + attrs[route] = [] + attrs[route].append(event["Estimated"]["text"]) + attrs[STOP_NAME] = stop_name + attrs[ATTR_ATTRIBUTION] = ATTRIBUTION + if closer_time is None: + self._state = None + else: + self._state = dt_util.utc_from_timestamp(closer_time).isoformat( + timespec="seconds" + ) + self._attrs = attrs + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 522f06bca021fc..f59ed13bda389b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1991,6 +1991,9 @@ xmltodict==0.12.0 # homeassistant.components.xs1 xs1-api-client==2.3.5 +# homeassistant.components.yandex_transport +ya_ma==0.3.7 + # homeassistant.components.yweather yahooweather==0.10 diff --git a/tests/components/yandex_transport/__init__.py b/tests/components/yandex_transport/__init__.py new file mode 100644 index 00000000000000..fe6b0db52d3e05 --- /dev/null +++ b/tests/components/yandex_transport/__init__.py @@ -0,0 +1 @@ +"""Tests for the yandex transport platform.""" diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py new file mode 100644 index 00000000000000..50d945e7fae371 --- /dev/null +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -0,0 +1,88 @@ +"""Tests for the yandex transport platform.""" + +import json +import pytest + +import homeassistant.components.sensor as sensor +import homeassistant.util.dt as dt_util +from homeassistant.const import CONF_NAME +from tests.common import ( + assert_setup_component, + async_setup_component, + MockDependency, + load_fixture, +) + +REPLY = json.loads(load_fixture("yandex_transport_reply.json")) + + +@pytest.fixture +def mock_requester(): + """Create a mock ya_ma module and YandexMapsRequester.""" + with MockDependency("ya_ma") as ya_ma: + instance = ya_ma.YandexMapsRequester.return_value + instance.get_stop_info.return_value = REPLY + yield instance + + +STOP_ID = 9639579 +ROUTES = ["194", "т36", "т47", "м10"] +NAME = "test_name" +TEST_CONFIG = { + "sensor": { + "platform": "yandex_transport", + "stop_id": 9639579, + "routes": ROUTES, + "name": NAME, + } +} + +FILTERED_ATTRS = { + "т36": ["21:43", "21:47", "22:02"], + "т47": ["21:40", "22:01"], + "м10": ["21:48", "22:00"], + "stop_name": "7-й автобусный парк", + "attribution": "Data provided by maps.yandex.ru", +} + +RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds") + + +async def assert_setup_sensor(hass, config, count=1): + """Set up the sensor and assert it's been created.""" + with assert_setup_component(count): + assert await async_setup_component(hass, sensor.DOMAIN, config) + + +async def test_setup_platform_valid_config(hass, mock_requester): + """Test that sensor is set up properly with valid config.""" + await assert_setup_sensor(hass, TEST_CONFIG) + + +async def test_setup_platform_invalid_config(hass, mock_requester): + """Check an invalid configuration.""" + await assert_setup_sensor( + hass, {"sensor": {"platform": "yandex_transport", "stopid": 1234}}, count=0 + ) + + +async def test_name(hass, mock_requester): + """Return the name if set in the configuration.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + assert state.name == TEST_CONFIG["sensor"][CONF_NAME] + + +async def test_state(hass, mock_requester): + """Return the contents of _state.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + assert state.state == RESULT_STATE + + +async def test_filtered_attributes(hass, mock_requester): + """Return the contents of attributes.""" + await assert_setup_sensor(hass, TEST_CONFIG) + state = hass.states.get("sensor.test_name") + state_attrs = {key: state.attributes[key] for key in FILTERED_ATTRS} + assert state_attrs == FILTERED_ATTRS diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json new file mode 100644 index 00000000000000..c5e4857297aa93 --- /dev/null +++ b/tests/fixtures/yandex_transport_reply.json @@ -0,0 +1,2106 @@ +{ + "data": { + "geometries": [ + { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + } + ], + "geometry": { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + }, + "properties": { + "name": "7-й автобусный парк", + "description": "7-й автобусный парк", + "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)", + "StopMetaData": { + "id": "stop__9639579", + "name": "7-й автобусный парк", + "type": "urban", + "region": { + "id": 213, + "type": 6, + "parent_id": 1, + "capital_id": 0, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow", + "native_name": "", + "iso_name": "RU MOW", + "is_main": true, + "en_name": "Moscow", + "short_en_name": "MSK", + "phone_code": "495 499", + "phone_code_old": "095", + "zip_code": "", + "population": 12506468, + "synonyms": "Moskau, Moskva", + "latitude": 55.753215, + "longitude": 37.622504, + "latitude_size": 0.878654, + "longitude_size": 1.164423, + "zoom": 10, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "weather", + "afisha", + "maps", + "tv", + "ad", + "etrain", + "subway", + "delivery", + "route" + ], + "ename": "moscow", + "bounds": [ + [ + 37.0402925, + 55.31141404514547 + ], + [ + 38.2047155, + 56.190068045145466 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву", + "dative": "Москве", + "directional": "", + "genitive": "Москвы", + "instrumental": "Москвой", + "locative": "", + "nominative": "Москва", + "preposition": "в", + "prepositional": "Москве" + }, + "parent": { + "id": 1, + "type": 5, + "parent_id": 3, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow-and-moscow-oblast", + "native_name": "", + "iso_name": "RU-MOS", + "is_main": true, + "en_name": "Moscow and Moscow Oblast", + "short_en_name": "RU-MOS", + "phone_code": "495 496 498 499", + "phone_code_old": "", + "zip_code": "", + "population": 7503385, + "synonyms": "Московская область, Подмосковье, Podmoskovye", + "latitude": 55.815792, + "longitude": 37.380031, + "latitude_size": 2.705659, + "longitude_size": 5.060749, + "zoom": 8, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 10716, + 10747, + 10758, + 20728, + 10740, + 10738, + 20523, + 10735, + 10734, + 10743, + 21622 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "moscow-and-moscow-oblast", + "bounds": [ + [ + 34.8496565, + 54.439456064325434 + ], + [ + 39.9104055, + 57.14511506432543 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву и Московскую область", + "dative": "Москве и Московской области", + "directional": "", + "genitive": "Москвы и Московской области", + "instrumental": "Москвой и Московской областью", + "locative": "", + "nominative": "Москва и Московская область", + "preposition": "в", + "prepositional": "Москве и Московской области" + }, + "parent": { + "id": 225, + "type": 3, + "parent_id": 10001, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "russia", + "native_name": "", + "iso_name": "RU", + "is_main": false, + "en_name": "Russia", + "short_en_name": "RU", + "phone_code": "7", + "phone_code_old": "", + "zip_code": "", + "population": 146880432, + "synonyms": "Russian Federation,Российская Федерация", + "latitude": 61.698653, + "longitude": 99.505405, + "latitude_size": 40.700127, + "longitude_size": 171.643239, + "zoom": 3, + "tzname": "", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 2, + 65, + 54, + 47, + 43, + 66, + 51, + 56, + 172, + 39, + 62 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "russia", + "bounds": [ + [ + 13.683785499999999, + 35.290400699917846 + ], + [ + -174.6729755, + 75.99052769991785 + ] + ], + "names": { + "ablative": "", + "accusative": "Россию", + "dative": "России", + "directional": "", + "genitive": "России", + "instrumental": "Россией", + "locative": "", + "nominative": "Россия", + "preposition": "в", + "prepositional": "России" + } + } + } + }, + "Transport": [ + { + "lineId": "2036925416", + "name": "194", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659860", + "tzOffset": 10800, + "text": "21:51" + } + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661840", + "tzOffset": 10800, + "text": "22:24" + } + } + ], + "departureTime": "21:51" + } + } + ], + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659860", + "tzOffset": 10800, + "text": "21:51" + } + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661840", + "tzOffset": 10800, + "text": "22:24" + } + } + ], + "departureTime": "21:51" + } + }, + { + "lineId": "213_114_bus_mosgortrans", + "name": "114", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568603405", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1568672165", + "tzOffset": 10800, + "text": "1:16" + } + } + } + } + ], + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568603405", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1568672165", + "tzOffset": 10800, + "text": "1:16" + } + } + } + }, + { + "lineId": "213_154_bus_mosgortrans", + "name": "154", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659260", + "tzOffset": 10800, + "text": "21:41" + }, + "Estimated": { + "value": "1568659252", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1054764%5F191500" + }, + { + "Scheduled": { + "value": "1568660580", + "tzOffset": 10800, + "text": "22:03" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:41" + } + } + ], + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659260", + "tzOffset": 10800, + "text": "21:41" + }, + "Estimated": { + "value": "1568659252", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1054764%5F191500" + }, + { + "Scheduled": { + "value": "1568660580", + "tzOffset": 10800, + "text": "22:03" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:41" + } + }, + { + "lineId": "213_179_bus_mosgortrans", + "name": "179", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568659351", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|59832%5F31359" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661660", + "tzOffset": 10800, + "text": "22:21" + } + } + ], + "departureTime": "21:52" + } + } + ], + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568659351", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|59832%5F31359" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661660", + "tzOffset": 10800, + "text": "22:21" + } + } + ], + "departureTime": "21:52" + } + }, + { + "lineId": "213_191m_minibus_default", + "name": "591", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660525", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|38278%5F9345312" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568602033", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1568672233", + "tzOffset": 10800, + "text": "1:17" + } + } + } + } + ], + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660525", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|38278%5F9345312" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568602033", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1568672233", + "tzOffset": 10800, + "text": "1:17" + } + } + } + }, + { + "lineId": "213_206m_minibus_default", + "name": "206к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568601239", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1568671439", + "tzOffset": 10800, + "text": "1:03" + } + } + } + } + ], + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568601239", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1568671439", + "tzOffset": 10800, + "text": "1:03" + } + } + } + }, + { + "lineId": "213_215_bus_mosgortrans", + "name": "215", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1568601276", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1568671476", + "tzOffset": 10800, + "text": "1:04" + } + } + } + } + ], + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1568601276", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1568671476", + "tzOffset": 10800, + "text": "1:04" + } + } + } + }, + { + "lineId": "213_282_bus_mosgortrans", + "name": "282", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659888", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|34874%5F9345408" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568602180", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1568673460", + "tzOffset": 10800, + "text": "1:37" + } + } + } + } + ], + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659888", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|34874%5F9345408" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568602180", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1568673460", + "tzOffset": 10800, + "text": "1:37" + } + } + } + }, + { + "lineId": "213_294m_minibus_default", + "name": "994", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1568601527", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1568671727", + "tzOffset": 10800, + "text": "1:08" + } + } + } + } + ], + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1568601527", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1568671727", + "tzOffset": 10800, + "text": "1:08" + } + } + } + }, + { + "lineId": "213_36_trolleybus_mosgortrans", + "name": "т36", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + }, + "Estimated": { + "value": "1568659426", + "tzOffset": 10800, + "text": "21:43" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Scheduled": { + "value": "1568660520", + "tzOffset": 10800, + "text": "22:02" + }, + "Estimated": { + "value": "1568659656", + "tzOffset": 10800, + "text": "21:47" + }, + "vehicleId": "codd%5Fnew|1117016%5F430280" + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + }, + "Estimated": { + "value": "1568660538", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|1054576%5F430226" + } + ], + "departureTime": "21:48" + } + } + ], + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + }, + "Estimated": { + "value": "1568659426", + "tzOffset": 10800, + "text": "21:43" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Scheduled": { + "value": "1568660520", + "tzOffset": 10800, + "text": "22:02" + }, + "Estimated": { + "value": "1568659656", + "tzOffset": 10800, + "text": "21:47" + }, + "vehicleId": "codd%5Fnew|1117016%5F430280" + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + }, + "Estimated": { + "value": "1568660538", + "tzOffset": 10800, + "text": "22:02" + }, + "vehicleId": "codd%5Fnew|1054576%5F430226" + } + ], + "departureTime": "21:48" + } + }, + { + "lineId": "213_47_trolleybus_mosgortrans", + "name": "т47", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + }, + "Estimated": { + "value": "1568659253", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1112219%5F430329" + }, + { + "Scheduled": { + "value": "1568660940", + "tzOffset": 10800, + "text": "22:09" + }, + "Estimated": { + "value": "1568660519", + "tzOffset": 10800, + "text": "22:01" + }, + "vehicleId": "codd%5Fnew|1139620%5F430382" + }, + { + "Scheduled": { + "value": "1568663580", + "tzOffset": 10800, + "text": "22:53" + } + } + ], + "departureTime": "21:53" + } + } + ], + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + }, + "Estimated": { + "value": "1568659253", + "tzOffset": 10800, + "text": "21:40" + }, + "vehicleId": "codd%5Fnew|1112219%5F430329" + }, + { + "Scheduled": { + "value": "1568660940", + "tzOffset": 10800, + "text": "22:09" + }, + "Estimated": { + "value": "1568660519", + "tzOffset": 10800, + "text": "22:01" + }, + "vehicleId": "codd%5Fnew|1139620%5F430382" + }, + { + "Scheduled": { + "value": "1568663580", + "tzOffset": 10800, + "text": "22:53" + } + } + ], + "departureTime": "21:53" + } + }, + { + "lineId": "213_56_trolleybus_mosgortrans", + "name": "т56", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660675", + "tzOffset": 10800, + "text": "22:04" + }, + "vehicleId": "codd%5Fnew|146304%5F31207" + } + ], + "Frequency": { + "text": "8 мин", + "value": 480, + "begin": { + "value": "1568606244", + "tzOffset": 10800, + "text": "6:57" + }, + "end": { + "value": "1568670144", + "tzOffset": 10800, + "text": "0:42" + } + } + } + } + ], + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568660675", + "tzOffset": 10800, + "text": "22:04" + }, + "vehicleId": "codd%5Fnew|146304%5F31207" + } + ], + "Frequency": { + "text": "8 мин", + "value": 480, + "begin": { + "value": "1568606244", + "tzOffset": 10800, + "text": "6:57" + }, + "end": { + "value": "1568670144", + "tzOffset": 10800, + "text": "0:42" + } + } + } + }, + { + "lineId": "213_63_bus_mosgortrans", + "name": "63", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|38921%5F9215306" + }, + { + "Estimated": { + "value": "1568660136", + "tzOffset": 10800, + "text": "21:55" + }, + "vehicleId": "codd%5Fnew|38918%5F9215303" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1568600987", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568670227", + "tzOffset": 10800, + "text": "0:43" + } + } + } + } + ], + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|38921%5F9215306" + }, + { + "Estimated": { + "value": "1568660136", + "tzOffset": 10800, + "text": "21:55" + }, + "vehicleId": "codd%5Fnew|38918%5F9215303" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1568600987", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568670227", + "tzOffset": 10800, + "text": "0:43" + } + } + } + }, + { + "lineId": "213_677_bus_mosgortrans", + "name": "677", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|11731%5F31376" + } + ], + "Frequency": { + "text": "4 мин", + "value": 240, + "begin": { + "value": "1568600940", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568672640", + "tzOffset": 10800, + "text": "1:24" + } + } + } + } + ], + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659369", + "tzOffset": 10800, + "text": "21:42" + }, + "vehicleId": "codd%5Fnew|11731%5F31376" + } + ], + "Frequency": { + "text": "4 мин", + "value": 240, + "begin": { + "value": "1568600940", + "tzOffset": 10800, + "text": "5:29" + }, + "end": { + "value": "1568672640", + "tzOffset": 10800, + "text": "1:24" + } + } + } + }, + { + "lineId": "213_692_bus_mosgortrans", + "name": "692", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568660280", + "tzOffset": 10800, + "text": "21:58" + }, + "Estimated": { + "value": "1568660255", + "tzOffset": 10800, + "text": "21:57" + }, + "vehicleId": "codd%5Fnew|63029%5F31485" + }, + { + "Scheduled": { + "value": "1568693340", + "tzOffset": 10800, + "text": "7:09" + } + }, + { + "Scheduled": { + "value": "1568696940", + "tzOffset": 10800, + "text": "8:09" + } + } + ], + "departureTime": "21:58" + } + } + ], + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568660280", + "tzOffset": 10800, + "text": "21:58" + }, + "Estimated": { + "value": "1568660255", + "tzOffset": 10800, + "text": "21:57" + }, + "vehicleId": "codd%5Fnew|63029%5F31485" + }, + { + "Scheduled": { + "value": "1568693340", + "tzOffset": 10800, + "text": "7:09" + } + }, + { + "Scheduled": { + "value": "1568696940", + "tzOffset": 10800, + "text": "8:09" + } + } + ], + "departureTime": "21:58" + } + }, + { + "lineId": "213_78_trolleybus_mosgortrans", + "name": "т78", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659620", + "tzOffset": 10800, + "text": "21:47" + }, + "Estimated": { + "value": "1568659898", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|147522%5F31184" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:47" + } + } + ], + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659620", + "tzOffset": 10800, + "text": "21:47" + }, + "Estimated": { + "value": "1568659898", + "tzOffset": 10800, + "text": "21:51" + }, + "vehicleId": "codd%5Fnew|147522%5F31184" + }, + { + "Scheduled": { + "value": "1568660760", + "tzOffset": 10800, + "text": "22:06" + } + }, + { + "Scheduled": { + "value": "1568661900", + "tzOffset": 10800, + "text": "22:25" + } + } + ], + "departureTime": "21:47" + } + }, + { + "lineId": "213_82_bus_mosgortrans", + "name": "82", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + } + }, + { + "Scheduled": { + "value": "1568661780", + "tzOffset": 10800, + "text": "22:23" + } + }, + { + "Scheduled": { + "value": "1568663760", + "tzOffset": 10800, + "text": "22:56" + } + } + ], + "departureTime": "21:48" + } + } + ], + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659680", + "tzOffset": 10800, + "text": "21:48" + } + }, + { + "Scheduled": { + "value": "1568661780", + "tzOffset": 10800, + "text": "22:23" + } + }, + { + "Scheduled": { + "value": "1568663760", + "tzOffset": 10800, + "text": "22:56" + } + } + ], + "departureTime": "21:48" + } + }, + { + "lineId": "2465131598", + "name": "179к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659500", + "tzOffset": 10800, + "text": "21:45" + } + }, + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + } + }, + { + "Scheduled": { + "value": "1568660880", + "tzOffset": 10800, + "text": "22:08" + } + } + ], + "departureTime": "21:45" + } + } + ], + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659500", + "tzOffset": 10800, + "text": "21:45" + } + }, + { + "Scheduled": { + "value": "1568659980", + "tzOffset": 10800, + "text": "21:53" + } + }, + { + "Scheduled": { + "value": "1568660880", + "tzOffset": 10800, + "text": "22:08" + } + } + ], + "departureTime": "21:45" + } + }, + { + "lineId": "466_bus_default", + "name": "466", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568604647", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1568675447", + "tzOffset": 10800, + "text": "2:10" + } + } + } + } + ], + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1568604647", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1568675447", + "tzOffset": 10800, + "text": "2:10" + } + } + } + }, + { + "lineId": "677k_bus_default", + "name": "677к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568660003", + "tzOffset": 10800, + "text": "21:53" + }, + "vehicleId": "codd%5Fnew|130308%5F31319" + }, + { + "Scheduled": { + "value": "1568661240", + "tzOffset": 10800, + "text": "22:14" + } + }, + { + "Scheduled": { + "value": "1568662500", + "tzOffset": 10800, + "text": "22:35" + } + } + ], + "departureTime": "21:52" + } + } + ], + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1568659920", + "tzOffset": 10800, + "text": "21:52" + }, + "Estimated": { + "value": "1568660003", + "tzOffset": 10800, + "text": "21:53" + }, + "vehicleId": "codd%5Fnew|130308%5F31319" + }, + { + "Scheduled": { + "value": "1568661240", + "tzOffset": 10800, + "text": "22:14" + } + }, + { + "Scheduled": { + "value": "1568662500", + "tzOffset": 10800, + "text": "22:35" + } + } + ], + "departureTime": "21:52" + } + }, + { + "lineId": "m10_bus_default", + "name": "м10", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659718", + "tzOffset": 10800, + "text": "21:48" + }, + "vehicleId": "codd%5Fnew|146260%5F31212" + }, + { + "Estimated": { + "value": "1568660422", + "tzOffset": 10800, + "text": "22:00" + }, + "vehicleId": "codd%5Fnew|13997%5F31247" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568606903", + "tzOffset": 10800, + "text": "7:08" + }, + "end": { + "value": "1568675183", + "tzOffset": 10800, + "text": "2:06" + } + } + } + } + ], + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1568659718", + "tzOffset": 10800, + "text": "21:48" + }, + "vehicleId": "codd%5Fnew|146260%5F31212" + }, + { + "Estimated": { + "value": "1568660422", + "tzOffset": 10800, + "text": "22:00" + }, + "vehicleId": "codd%5Fnew|13997%5F31247" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1568606903", + "tzOffset": 10800, + "text": "7:08" + }, + "end": { + "value": "1568675183", + "tzOffset": 10800, + "text": "2:06" + } + } + } + } + ] + } + }, + "toponymSeoname": "dmitrovskoye_shosse" + } +} From 62adff23f935cbb644c8dfb17e8e109f407374a8 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 20 Sep 2019 15:11:24 -0400 Subject: [PATCH 0380/3953] ZHA siren and warning device support (#26046) * add ias warning device support * use channel only clusters for warning devices * squawk service * add warning device warning service * update services.yaml * remove debugging statement * update required attr access * fix constant * add error logging to IASWD services --- homeassistant/components/zha/api.py | 130 ++++++++++++++++++ .../components/zha/core/channels/security.py | 96 ++++++++++++- homeassistant/components/zha/core/const.py | 30 ++++ homeassistant/components/zha/services.yaml | 52 +++++++ 4 files changed, 306 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index be079e83fa6bb0..ff9f27d4843c20 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -19,9 +19,16 @@ ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ENDPOINT_ID, + ATTR_LEVEL, ATTR_MANUFACTURER, ATTR_NAME, ATTR_VALUE, + ATTR_WARNING_DEVICE_DURATION, + ATTR_WARNING_DEVICE_MODE, + ATTR_WARNING_DEVICE_STROBE, + ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, + ATTR_WARNING_DEVICE_STROBE_INTENSITY, + CHANNEL_IAS_WD, CLUSTER_COMMAND_SERVER, CLUSTER_COMMANDS_CLIENT, CLUSTER_COMMANDS_SERVER, @@ -31,6 +38,11 @@ DATA_ZHA_GATEWAY, DOMAIN, MFG_CLUSTER_ID_START, + WARNING_DEVICE_MODE_EMERGENCY, + WARNING_DEVICE_SOUND_HIGH, + WARNING_DEVICE_SQUAWK_MODE_ARMED, + WARNING_DEVICE_STROBE_HIGH, + WARNING_DEVICE_STROBE_YES, ) from .core.helpers import async_is_bindable_target, convert_ieee, get_matched_clusters @@ -56,6 +68,8 @@ SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = "issue_zigbee_cluster_command" SERVICE_DIRECT_ZIGBEE_BIND = "issue_direct_zigbee_bind" SERVICE_DIRECT_ZIGBEE_UNBIND = "issue_direct_zigbee_unbind" +SERVICE_WARNING_DEVICE_SQUAWK = "warning_device_squawk" +SERVICE_WARNING_DEVICE_WARN = "warning_device_warn" SERVICE_ZIGBEE_BIND = "service_zigbee_bind" IEEE_SERVICE = "ieee_based_service" @@ -80,6 +94,41 @@ vol.Optional(ATTR_MANUFACTURER): cv.positive_int, } ), + SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema( + { + vol.Required(ATTR_IEEE): convert_ieee, + vol.Optional( + ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED + ): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES + ): cv.positive_int, + vol.Optional( + ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH + ): cv.positive_int, + } + ), + SERVICE_WARNING_DEVICE_WARN: vol.Schema( + { + vol.Required(ATTR_IEEE): convert_ieee, + vol.Optional( + ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY + ): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE, default=WARNING_DEVICE_STROBE_YES + ): cv.positive_int, + vol.Optional( + ATTR_LEVEL, default=WARNING_DEVICE_SOUND_HIGH + ): cv.positive_int, + vol.Optional(ATTR_WARNING_DEVICE_DURATION, default=5): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, default=0x00 + ): cv.positive_int, + vol.Optional( + ATTR_WARNING_DEVICE_STROBE_INTENSITY, default=WARNING_DEVICE_STROBE_HIGH + ): cv.positive_int, + } + ), SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema( { vol.Required(ATTR_IEEE): convert_ieee, @@ -610,6 +659,85 @@ async def issue_zigbee_cluster_command(service): schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND], ) + async def warning_device_squawk(service): + """Issue the squawk command for an IAS warning device.""" + ieee = service.data[ATTR_IEEE] + mode = service.data.get(ATTR_WARNING_DEVICE_MODE) + strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE) + level = service.data.get(ATTR_LEVEL) + + zha_device = zha_gateway.get_device(ieee) + if zha_device is not None: + channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD) + if channel: + await channel.squawk(mode, strobe, level) + else: + _LOGGER.error( + "Squawking IASWD: %s is missing the required IASWD channel!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + else: + _LOGGER.error( + "Squawking IASWD: %s could not be found!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + _LOGGER.debug( + "Squawking IASWD: %s %s %s %s", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), + "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), + "{}: [{}]".format(ATTR_LEVEL, level), + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_WARNING_DEVICE_SQUAWK, + warning_device_squawk, + schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_SQUAWK], + ) + + async def warning_device_warn(service): + """Issue the warning command for an IAS warning device.""" + ieee = service.data[ATTR_IEEE] + mode = service.data.get(ATTR_WARNING_DEVICE_MODE) + strobe = service.data.get(ATTR_WARNING_DEVICE_STROBE) + level = service.data.get(ATTR_LEVEL) + duration = service.data.get(ATTR_WARNING_DEVICE_DURATION) + duty_mode = service.data.get(ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE) + intensity = service.data.get(ATTR_WARNING_DEVICE_STROBE_INTENSITY) + + zha_device = zha_gateway.get_device(ieee) + if zha_device is not None: + channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD) + if channel: + await channel.start_warning( + mode, strobe, level, duration, duty_mode, intensity + ) + else: + _LOGGER.error( + "Warning IASWD: %s is missing the required IASWD channel!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + else: + _LOGGER.error( + "Warning IASWD: %s could not be found!", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + ) + _LOGGER.debug( + "Warning IASWD: %s %s %s %s", + "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), + "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), + "{}: [{}]".format(ATTR_LEVEL, level), + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_WARNING_DEVICE_WARN, + warning_device_warn, + schema=SERVICE_SCHEMAS[SERVICE_WARNING_DEVICE_WARN], + ) + websocket_api.async_register_command(hass, websocket_permit_devices) websocket_api.async_register_command(hass, websocket_get_devices) websocket_api.async_register_command(hass, websocket_get_device) @@ -629,3 +757,5 @@ def async_unload_api(hass): hass.services.async_remove(DOMAIN, SERVICE_REMOVE) hass.services.async_remove(DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE) hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND) + hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_SQUAWK) + hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_WARN) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index cd407cfc416b68..25c11a9fd4f1cd 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -13,7 +13,15 @@ from . import ZigbeeChannel from .. import registries -from ..const import SIGNAL_ATTR_UPDATED +from ..const import ( + CLUSTER_COMMAND_SERVER, + SIGNAL_ATTR_UPDATED, + WARNING_DEVICE_MODE_EMERGENCY, + WARNING_DEVICE_SOUND_HIGH, + WARNING_DEVICE_SQUAWK_MODE_ARMED, + WARNING_DEVICE_STROBE_HIGH, + WARNING_DEVICE_STROBE_YES, +) _LOGGER = logging.getLogger(__name__) @@ -25,11 +33,95 @@ class IasAce(ZigbeeChannel): pass +@registries.CHANNEL_ONLY_CLUSTERS.register(security.IasWd.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasWd.cluster_id) class IasWd(ZigbeeChannel): """IAS Warning Device channel.""" - pass + @staticmethod + def set_bit(destination_value, destination_bit, source_value, source_bit): + """Set the specified bit in the value.""" + + if IasWd.get_bit(source_value, source_bit): + return destination_value | (1 << destination_bit) + return destination_value + + @staticmethod + def get_bit(value, bit): + """Get the specified bit from the value.""" + return (value & (1 << bit)) != 0 + + async def squawk( + self, + mode=WARNING_DEVICE_SQUAWK_MODE_ARMED, + strobe=WARNING_DEVICE_STROBE_YES, + squawk_level=WARNING_DEVICE_SOUND_HIGH, + ): + """Issue a squawk command. + + This command uses the WD capabilities to emit a quick audible/visible pulse called a + "squawk". The squawk command has no effect if the WD is currently active + (warning in progress). + """ + value = 0 + value = IasWd.set_bit(value, 0, squawk_level, 0) + value = IasWd.set_bit(value, 1, squawk_level, 1) + + value = IasWd.set_bit(value, 3, strobe, 0) + + value = IasWd.set_bit(value, 4, mode, 0) + value = IasWd.set_bit(value, 5, mode, 1) + value = IasWd.set_bit(value, 6, mode, 2) + value = IasWd.set_bit(value, 7, mode, 3) + + await self.device.issue_cluster_command( + self.cluster.endpoint.endpoint_id, + self.cluster.cluster_id, + 0x0001, + CLUSTER_COMMAND_SERVER, + [value], + ) + + async def start_warning( + self, + mode=WARNING_DEVICE_MODE_EMERGENCY, + strobe=WARNING_DEVICE_STROBE_YES, + siren_level=WARNING_DEVICE_SOUND_HIGH, + warning_duration=5, # seconds + strobe_duty_cycle=0x00, + strobe_intensity=WARNING_DEVICE_STROBE_HIGH, + ): + """Issue a start warning command. + + This command starts the WD operation. The WD alerts the surrounding area by audible + (siren) and visual (strobe) signals. + + strobe_duty_cycle indicates the length of the flash cycle. This provides a means + of varying the flash duration for different alarm types (e.g., fire, police, burglar). + Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the + nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. + The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies + “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for + 6/10ths of a second. + """ + value = 0 + value = IasWd.set_bit(value, 0, siren_level, 0) + value = IasWd.set_bit(value, 1, siren_level, 1) + + value = IasWd.set_bit(value, 2, strobe, 0) + + value = IasWd.set_bit(value, 4, mode, 0) + value = IasWd.set_bit(value, 5, mode, 1) + value = IasWd.set_bit(value, 6, mode, 2) + value = IasWd.set_bit(value, 7, mode, 3) + + await self.device.issue_cluster_command( + self.cluster.endpoint.endpoint_id, + self.cluster.cluster_id, + 0x0000, + CLUSTER_COMMAND_SERVER, + [value, warning_duration, strobe_duty_cycle, strobe_intensity], + ) @registries.BINARY_SENSOR_CLUSTERS.register(security.IasZone.cluster_id) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c35cb168fdff31..ac83c2cdcd8f41 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -34,6 +34,11 @@ ATTR_SIGNATURE = "signature" ATTR_TYPE = "type" ATTR_VALUE = "value" +ATTR_WARNING_DEVICE_DURATION = "duration" +ATTR_WARNING_DEVICE_MODE = "mode" +ATTR_WARNING_DEVICE_STROBE = "strobe" +ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE = "duty_cycle" +ATTR_WARNING_DEVICE_STROBE_INTENSITY = "intensity" BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000] @@ -44,6 +49,7 @@ CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement" CHANNEL_EVENT_RELAY = "event_relay" CHANNEL_FAN = "fan" +CHANNEL_IAS_WD = "ias_wd" CHANNEL_LEVEL = ATTR_LEVEL CHANNEL_ON_OFF = "on_off" CHANNEL_POWER_CONFIGURATION = "power" @@ -177,6 +183,30 @@ def list(cls): UNKNOWN_MANUFACTURER = "unk_manufacturer" UNKNOWN_MODEL = "unk_model" +WARNING_DEVICE_MODE_STOP = 0 +WARNING_DEVICE_MODE_BURGLAR = 1 +WARNING_DEVICE_MODE_FIRE = 2 +WARNING_DEVICE_MODE_EMERGENCY = 3 +WARNING_DEVICE_MODE_POLICE_PANIC = 4 +WARNING_DEVICE_MODE_FIRE_PANIC = 5 +WARNING_DEVICE_MODE_EMERGENCY_PANIC = 6 + +WARNING_DEVICE_STROBE_NO = 0 +WARNING_DEVICE_STROBE_YES = 1 + +WARNING_DEVICE_SOUND_LOW = 0 +WARNING_DEVICE_SOUND_MEDIUM = 1 +WARNING_DEVICE_SOUND_HIGH = 2 +WARNING_DEVICE_SOUND_VERY_HIGH = 3 + +WARNING_DEVICE_STROBE_LOW = 0x00 +WARNING_DEVICE_STROBE_MEDIUM = 0x01 +WARNING_DEVICE_STROBE_HIGH = 0x02 +WARNING_DEVICE_STROBE_VERY_HIGH = 0x03 + +WARNING_DEVICE_SQUAWK_MODE_ARMED = 0 +WARNING_DEVICE_SQUAWK_MODE_DISARMED = 1 + ZHA_DISCOVERY_NEW = "zha_discovery_new_{}" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" ZHA_GW_MSG = "zha_gateway_message" diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index ffd5aa21472c83..d279af46335fe1 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -82,3 +82,55 @@ issue_zigbee_cluster_command: manufacturer: description: manufacturer code example: 0x00FC + +warning_device_squawk: + description: >- + This service uses the WD capabilities to emit a quick audible/visible pulse called a "squawk". The squawk command has no effect if the WD is currently active (warning in progress). + fields: + ieee: + description: IEEE address for the device + example: "00:0d:6f:00:05:7d:2d:34" + mode: + description: >- + The Squawk Mode field is used as a 4-bit enumeration, and can have one of the values shown in Table 8-24 of the ZCL spec - Squawk Mode Field. The exact operation of each mode (how the WD “squawks”) is implementation specific. + example: 1 + strobe: + description: >- + The strobe field is used as a Boolean, and determines if the visual indication is also required in addition to the audible squawk, as shown in Table 8-25 of the ZCL spec - Strobe Bit. + example: 1 + level: + description: >- + The squawk level field is used as a 2-bit enumeration, and determines the intensity of audible squawk sound as shown in Table 8-26 of the ZCL spec - Squawk Level Field Values. + example: 2 + +warning_device_warn: + description: >- + This service starts the WD operation. The WD alerts the surrounding area by audible (siren) and visual (strobe) signals. + fields: + ieee: + description: IEEE address for the device + example: "00:0d:6f:00:05:7d:2d:34" + mode: + description: >- + The Warning Mode field is used as an 4-bit enumeration, can have one of the values defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards. + example: 1 + strobe: + description: >- + The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated. + example: 1 + level: + description: >- + The Siren Level field is used as a 2-bit enumeration, and indicates the intensity of audible squawk sound as shown in Table 8-22 of the ZCL spec. + example: 2 + duration: + description: >- + Requested duration of warning, in seconds. If both Strobe and Warning Mode are "0" this field SHALL be ignored. + example: 2 + duty_cycle: + description: >- + Indicates the length of the flash cycle. This provides a means of varying the flash duration for different alarm types (e.g., fire, police, burglar). Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for 6/10ths of a second. + example: 2 + intensity: + description: >- + Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec. + example: 2 From f6be61c6b75a860384fe75e1b0ef9b801139c2c8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 20 Sep 2019 13:32:41 -0600 Subject: [PATCH 0381/3953] Bump aiowwlln to 2.0.2 (#26769) --- homeassistant/components/wwlln/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wwlln/manifest.json b/homeassistant/components/wwlln/manifest.json index 6d13f7adbfd497..189b9365105159 100644 --- a/homeassistant/components/wwlln/manifest.json +++ b/homeassistant/components/wwlln/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/wwlln", "requirements": [ - "aiowwlln==2.0.1" + "aiowwlln==2.0.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index f59ed13bda389b..e66f248cbba06f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -179,7 +179,7 @@ aioswitcher==2019.4.26 aiounifi==11 # homeassistant.components.wwlln -aiowwlln==2.0.1 +aiowwlln==2.0.2 # homeassistant.components.aladdin_connect aladdin_connect==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 122ff317c0e511..c254da1f9d288f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -73,7 +73,7 @@ aioswitcher==2019.4.26 aiounifi==11 # homeassistant.components.wwlln -aiowwlln==2.0.1 +aiowwlln==2.0.2 # homeassistant.components.ambiclimate ambiclimate==0.2.1 From 5cf9ba51dff7e46c34d2a4dcc02cfabc6d45c2a5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 20 Sep 2019 14:41:46 -0600 Subject: [PATCH 0382/3953] Bump simplisafe-python to 5.0.1 (#26775) --- 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 8a03ac47402bae..cf26955b207b5e 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.3.0" + "simplisafe-python==5.0.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index e66f248cbba06f..9d7e8b645b1f5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1738,7 +1738,7 @@ shodan==1.15.0 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==4.3.0 +simplisafe-python==5.0.1 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c254da1f9d288f..fdc9ad8dc3f265 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -386,7 +386,7 @@ ring_doorbell==0.2.3 rxv==0.6.0 # homeassistant.components.simplisafe -simplisafe-python==4.3.0 +simplisafe-python==5.0.1 # homeassistant.components.sleepiq sleepyq==0.7 From 8502f7c7d448cad63eff0a4d2dbc8d5c8d582732 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Sep 2019 17:02:18 -0700 Subject: [PATCH 0383/3953] Add integration scaffolding script (#26777) * Add integration scaffolding script * Make easier to develop * Update py.test -> pytest --- script/scaffold/__init__.py | 1 + script/scaffold/__main__.py | 56 +++++++++++ script/scaffold/const.py | 5 + script/scaffold/error.py | 10 ++ script/scaffold/gather_info.py | 79 ++++++++++++++++ script/scaffold/generate.py | 47 ++++++++++ script/scaffold/model.py | 12 +++ .../templates/integration/__init__.py | 19 ++++ .../templates/integration/config_flow.py | 57 ++++++++++++ .../scaffold/templates/integration/const.py | 3 + .../scaffold/templates/integration/error.py | 10 ++ .../templates/integration/manifest.json | 11 +++ .../templates/integration/strings.json | 21 +++++ script/scaffold/templates/tests/__init__.py | 1 + .../templates/tests/test_config_flow.py | 93 +++++++++++++++++++ 15 files changed, 425 insertions(+) create mode 100644 script/scaffold/__init__.py create mode 100644 script/scaffold/__main__.py create mode 100644 script/scaffold/const.py create mode 100644 script/scaffold/error.py create mode 100644 script/scaffold/gather_info.py create mode 100644 script/scaffold/generate.py create mode 100644 script/scaffold/model.py create mode 100644 script/scaffold/templates/integration/__init__.py create mode 100644 script/scaffold/templates/integration/config_flow.py create mode 100644 script/scaffold/templates/integration/const.py create mode 100644 script/scaffold/templates/integration/error.py create mode 100644 script/scaffold/templates/integration/manifest.json create mode 100644 script/scaffold/templates/integration/strings.json create mode 100644 script/scaffold/templates/tests/__init__.py create mode 100644 script/scaffold/templates/tests/test_config_flow.py diff --git a/script/scaffold/__init__.py b/script/scaffold/__init__.py new file mode 100644 index 00000000000000..2eca398d998245 --- /dev/null +++ b/script/scaffold/__init__.py @@ -0,0 +1 @@ +"""Scaffold new integration.""" diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py new file mode 100644 index 00000000000000..d1b514ea934a2c --- /dev/null +++ b/script/scaffold/__main__.py @@ -0,0 +1,56 @@ +"""Validate manifests.""" +from pathlib import Path +import subprocess +import sys + +from . import gather_info, generate, error, model + + +def main(): + """Scaffold an integration.""" + if not Path("requirements_all.txt").is_file(): + print("Run from project root") + return 1 + + print("Creating a new integration for Home Assistant.") + + if "--develop" in sys.argv: + print("Running in developer mode. Automatically filling in info.") + print() + + info = model.Info( + domain="develop", + name="Develop Hub", + codeowner="@developer", + requirement="aiodevelop==1.2.3", + ) + else: + try: + info = gather_info.gather_info() + except error.ExitApp as err: + print() + print(err.reason) + return err.exit_code + + generate.generate(info) + + print("Running hassfest to pick up new codeowner and config flow.") + subprocess.run("python -m script.hassfest", shell=True) + print() + + print("Running tests") + print(f"$ pytest tests/components/{info.domain}") + if ( + subprocess.run(f"pytest tests/components/{info.domain}", shell=True).returncode + != 0 + ): + return 1 + print() + + print(f"Successfully created the {info.domain} integration!") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/script/scaffold/const.py b/script/scaffold/const.py new file mode 100644 index 00000000000000..cf66bb4e2ae4d7 --- /dev/null +++ b/script/scaffold/const.py @@ -0,0 +1,5 @@ +"""Constants for scaffolding.""" +from pathlib import Path + +COMPONENT_DIR = Path("homeassistant/components") +TESTS_DIR = Path("tests/components") diff --git a/script/scaffold/error.py b/script/scaffold/error.py new file mode 100644 index 00000000000000..d99cbe8026aff2 --- /dev/null +++ b/script/scaffold/error.py @@ -0,0 +1,10 @@ +"""Errors for scaffolding.""" + + +class ExitApp(Exception): + """Exception to indicate app should exit.""" + + def __init__(self, reason, exit_code): + """Initialize the exit app exception.""" + self.reason = reason + self.exit_code = exit_code diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py new file mode 100644 index 00000000000000..352d1da206c12b --- /dev/null +++ b/script/scaffold/gather_info.py @@ -0,0 +1,79 @@ +"""Gather info for scaffolding.""" +from homeassistant.util import slugify + +from .const import COMPONENT_DIR +from .model import Info +from .error import ExitApp + + +CHECK_EMPTY = ["Cannot be empty", lambda value: value] + + +FIELDS = { + "domain": { + "prompt": "What is the domain?", + "validators": [ + CHECK_EMPTY, + [ + "Domains cannot contain spaces or special characters.", + lambda value: value == slugify(value), + ], + [ + "There already is an integration with this domain.", + lambda value: not (COMPONENT_DIR / value).exists(), + ], + ], + }, + "name": { + "prompt": "What is the name of your integration?", + "validators": [CHECK_EMPTY], + }, + "codeowner": { + "prompt": "What is your GitHub handle?", + "validators": [ + CHECK_EMPTY, + [ + 'GitHub handles need to start with an "@"', + lambda value: value.startswith("@"), + ], + ], + }, + "requirement": { + "prompt": "What PyPI package and version do you depend on? Leave blank for none.", + "validators": [ + ["Versions should be pinned using '=='.", lambda value: "==" in value] + ], + }, +} + + +def gather_info() -> Info: + """Gather info from user.""" + answers = {} + + for key, info in FIELDS.items(): + hint = None + while key not in answers: + if hint is not None: + print() + print(f"Error: {hint}") + + try: + print() + value = input(info["prompt"] + "\n> ") + except (KeyboardInterrupt, EOFError): + raise ExitApp("Interrupted!", 1) + + value = value.strip() + hint = None + + for validator_hint, validator in info["validators"]: + if not validator(value): + hint = validator_hint + break + + if hint is None: + answers[key] = value + + print() + return Info(**answers) diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py new file mode 100644 index 00000000000000..f7b3f56f2e6623 --- /dev/null +++ b/script/scaffold/generate.py @@ -0,0 +1,47 @@ +"""Generate an integration.""" +import json +from pathlib import Path + +from .const import COMPONENT_DIR, TESTS_DIR +from .model import Info + +TEMPLATE_DIR = Path(__file__).parent / "templates" +TEMPLATE_INTEGRATION = TEMPLATE_DIR / "integration" +TEMPLATE_TESTS = TEMPLATE_DIR / "tests" + + +def generate(info: Info) -> None: + """Generate an integration.""" + print(f"Generating the {info.domain} integration...") + integration_dir = COMPONENT_DIR / info.domain + test_dir = TESTS_DIR / info.domain + + replaces = { + "NEW_DOMAIN": info.domain, + "NEW_NAME": info.name, + "NEW_CODEOWNER": info.codeowner, + # Special case because we need to keep the list empty if there is none. + '"MANIFEST_NEW_REQUIREMENT"': ( + json.dumps(info.requirement) if info.requirement else "" + ), + } + + for src_dir, target_dir in ( + (TEMPLATE_INTEGRATION, integration_dir), + (TEMPLATE_TESTS, test_dir), + ): + # Guard making it for test purposes. + if not target_dir.exists(): + target_dir.mkdir() + + for source_file in src_dir.glob("**/*"): + content = source_file.read_text() + + for to_search, to_replace in replaces.items(): + content = content.replace(to_search, to_replace) + + target_file = target_dir / source_file.relative_to(src_dir) + print(f"Writing {target_file}") + target_file.write_text(content) + + print() diff --git a/script/scaffold/model.py b/script/scaffold/model.py new file mode 100644 index 00000000000000..83fe922d8c4473 --- /dev/null +++ b/script/scaffold/model.py @@ -0,0 +1,12 @@ +"""Models for scaffolding.""" +import attr + + +@attr.s +class Info: + """Info about new integration.""" + + domain: str = attr.ib() + name: str = attr.ib() + codeowner: str = attr.ib() + requirement: str = attr.ib() diff --git a/script/scaffold/templates/integration/__init__.py b/script/scaffold/templates/integration/__init__.py new file mode 100644 index 00000000000000..356c7857d92bdb --- /dev/null +++ b/script/scaffold/templates/integration/__init__.py @@ -0,0 +1,19 @@ +"""The NEW_NAME integration.""" + +from .const import DOMAIN + + +async def async_setup(hass, config): + """Set up the NEW_NAME integration.""" + hass.data[DOMAIN] = config.get(DOMAIN, {}) + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry for NEW_NAME.""" + # TODO forward the entry for each platform that you want to set up. + # hass.async_create_task( + # hass.config_entries.async_forward_entry_setup(entry, "media_player") + # ) + + return True diff --git a/script/scaffold/templates/integration/config_flow.py b/script/scaffold/templates/integration/config_flow.py new file mode 100644 index 00000000000000..c05141ff0b009a --- /dev/null +++ b/script/scaffold/templates/integration/config_flow.py @@ -0,0 +1,57 @@ +"""Config flow for NEW_NAME integration.""" +import logging + +import voluptuous as vol + +from homeassistant import core, config_entries + +from .const import DOMAIN # pylint:disable=unused-import +from .error import CannotConnect, InvalidAuth + +_LOGGER = logging.getLogger(__name__) + +# TODO adjust the data schema to the data that you need +DATA_SCHEMA = vol.Schema({"host": str, "username": str, "password": str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + # TODO validate the data can be used to set up a connection. + # If you cannot connect: + # throw CannotConnect + # If the authentication is wrong: + # InvalidAuth + + # Return some info we want to store in the config entry. + return {"title": "Name of the device"} + + +class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for NEW_NAME.""" + + VERSION = 1 + # TODO pick one of the available connection classes + CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + + return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/script/scaffold/templates/integration/const.py b/script/scaffold/templates/integration/const.py new file mode 100644 index 00000000000000..e8a1c494d49729 --- /dev/null +++ b/script/scaffold/templates/integration/const.py @@ -0,0 +1,3 @@ +"""Constants for the NEW_NAME integration.""" + +DOMAIN = "NEW_DOMAIN" diff --git a/script/scaffold/templates/integration/error.py b/script/scaffold/templates/integration/error.py new file mode 100644 index 00000000000000..a99a32bb9501ab --- /dev/null +++ b/script/scaffold/templates/integration/error.py @@ -0,0 +1,10 @@ +"""Errors for the NEW_NAME integration.""" +from homeassistant.exceptions import HomeAssistantError + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/script/scaffold/templates/integration/manifest.json b/script/scaffold/templates/integration/manifest.json new file mode 100644 index 00000000000000..7c1e141eef07be --- /dev/null +++ b/script/scaffold/templates/integration/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "NEW_DOMAIN", + "name": "NEW_NAME", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/NEW_DOMAIN", + "requirements": ["MANIFEST_NEW_REQUIREMENT"], + "ssdp": {}, + "homekit": {}, + "dependencies": [], + "codeowners": ["NEW_CODEOWNER"] +} diff --git a/script/scaffold/templates/integration/strings.json b/script/scaffold/templates/integration/strings.json new file mode 100644 index 00000000000000..0f29967b286547 --- /dev/null +++ b/script/scaffold/templates/integration/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "NEW_NAME", + "step": { + "user": { + "title": "Connect to the device", + "data": { + "host": "Host" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/script/scaffold/templates/tests/__init__.py b/script/scaffold/templates/tests/__init__.py new file mode 100644 index 00000000000000..081b6d86600012 --- /dev/null +++ b/script/scaffold/templates/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the NEW_NAME integration.""" diff --git a/script/scaffold/templates/tests/test_config_flow.py b/script/scaffold/templates/tests/test_config_flow.py new file mode 100644 index 00000000000000..7735f497f80176 --- /dev/null +++ b/script/scaffold/templates/tests/test_config_flow.py @@ -0,0 +1,93 @@ +"""Test the NEW_NAME config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.components.NEW_DOMAIN.const import DOMAIN +from homeassistant.components.NEW_DOMAIN.error import CannotConnect, InvalidAuth + +from tests.common import mock_coro + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.NEW_DOMAIN.config_flow.validate_input", + return_value=mock_coro({"title": "Test Title"}), + ), patch( + "homeassistant.components.NEW_DOMAIN.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.NEW_DOMAIN.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Test Title" + assert result2["data"] == { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.NEW_DOMAIN.config_flow.validate_input", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.NEW_DOMAIN.config_flow.validate_input", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From 24cbae6ec3bc57e956ca5d52efd56f7e2b023f71 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 21 Sep 2019 00:32:16 +0000 Subject: [PATCH 0384/3953] [ci skip] Translation update --- .../components/adguard/.translations/pl.json | 2 +- .../cert_expiry/.translations/lb.json | 21 ++++++++++-- .../cert_expiry/.translations/no.json | 2 +- .../cert_expiry/.translations/zh-Hans.json | 16 +++++++++ .../components/deconz/.translations/lb.json | 9 +++++ .../components/deconz/.translations/ru.json | 9 +++-- .../geonetnz_quakes/.translations/lb.json | 17 ++++++++++ .../.translations/zh-Hans.json | 9 +++++ .../.translations/zh-Hans.json | 2 +- .../components/iqvia/.translations/pl.json | 2 +- .../components/izone/.translations/ca.json | 15 +++++++++ .../components/izone/.translations/fr.json | 15 +++++++++ .../components/izone/.translations/ko.json | 15 +++++++++ .../components/izone/.translations/lb.json | 15 +++++++++ .../components/izone/.translations/no.json | 15 +++++++++ .../components/izone/.translations/pl.json | 15 +++++++++ .../components/izone/.translations/ru.json | 15 +++++++++ .../components/life360/.translations/lb.json | 1 + .../linky/.translations/zh-Hans.json | 16 +++++++++ .../components/met/.translations/no.json | 2 +- .../components/met/.translations/zh-Hans.json | 7 +++- .../components/notion/.translations/ru.json | 2 +- .../plaato/.translations/zh-Hans.json | 13 ++++++++ .../components/plex/.translations/ca.json | 33 +++++++++++++++++++ .../components/plex/.translations/fr.json | 33 +++++++++++++++++++ .../components/plex/.translations/ko.json | 33 +++++++++++++++++++ .../components/plex/.translations/lb.json | 33 +++++++++++++++++++ .../components/plex/.translations/no.json | 33 +++++++++++++++++++ .../components/plex/.translations/pl.json | 33 +++++++++++++++++++ .../components/plex/.translations/ru.json | 33 +++++++++++++++++++ .../components/switch/.translations/fr.json | 2 ++ .../components/switch/.translations/no.json | 2 ++ .../components/switch/.translations/pl.json | 2 +- .../components/switch/.translations/ru.json | 2 ++ .../traccar/.translations/zh-Hans.json | 8 +++++ .../twentemilieu/.translations/lb.json | 4 ++- .../twentemilieu/.translations/zh-Hans.json | 7 ++++ .../components/unifi/.translations/lb.json | 18 ++++++++++ .../components/velbus/.translations/lb.json | 3 +- .../velbus/.translations/zh-Hans.json | 14 ++++++++ .../vesync/.translations/zh-Hans.json | 7 ++++ .../components/withings/.translations/fr.json | 3 ++ .../components/withings/.translations/lb.json | 7 ++++ .../components/withings/.translations/no.json | 3 ++ .../withings/.translations/zh-Hans.json | 10 ++++++ 45 files changed, 544 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/cert_expiry/.translations/zh-Hans.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/lb.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json create mode 100644 homeassistant/components/izone/.translations/ca.json create mode 100644 homeassistant/components/izone/.translations/fr.json create mode 100644 homeassistant/components/izone/.translations/ko.json create mode 100644 homeassistant/components/izone/.translations/lb.json create mode 100644 homeassistant/components/izone/.translations/no.json create mode 100644 homeassistant/components/izone/.translations/pl.json create mode 100644 homeassistant/components/izone/.translations/ru.json create mode 100644 homeassistant/components/linky/.translations/zh-Hans.json create mode 100644 homeassistant/components/plaato/.translations/zh-Hans.json create mode 100644 homeassistant/components/plex/.translations/ca.json create mode 100644 homeassistant/components/plex/.translations/fr.json create mode 100644 homeassistant/components/plex/.translations/ko.json create mode 100644 homeassistant/components/plex/.translations/lb.json create mode 100644 homeassistant/components/plex/.translations/no.json create mode 100644 homeassistant/components/plex/.translations/pl.json create mode 100644 homeassistant/components/plex/.translations/ru.json create mode 100644 homeassistant/components/traccar/.translations/zh-Hans.json create mode 100644 homeassistant/components/twentemilieu/.translations/zh-Hans.json create mode 100644 homeassistant/components/velbus/.translations/zh-Hans.json create mode 100644 homeassistant/components/vesync/.translations/zh-Hans.json create mode 100644 homeassistant/components/withings/.translations/zh-Hans.json diff --git a/homeassistant/components/adguard/.translations/pl.json b/homeassistant/components/adguard/.translations/pl.json index e58c901f3643f4..f8f64d542608fe 100644 --- a/homeassistant/components/adguard/.translations/pl.json +++ b/homeassistant/components/adguard/.translations/pl.json @@ -22,7 +22,7 @@ "verify_ssl": "AdGuard Home u\u017cywa odpowiedniego certyfikatu." }, "description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119.", - "title": "Po\u0142\u0105cz sw\u00f3j AdGuard Home" + "title": "Po\u0142\u0105cz AdGuard Home" } }, "title": "AdGuard Home" diff --git a/homeassistant/components/cert_expiry/.translations/lb.json b/homeassistant/components/cert_expiry/.translations/lb.json index d6811728a22402..9620526e363d9d 100644 --- a/homeassistant/components/cert_expiry/.translations/lb.json +++ b/homeassistant/components/cert_expiry/.translations/lb.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert" + }, "error": { - "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen." - } + "certificate_fetch_failed": "Kann keen Zertifikat vun d\u00ebsen Host a Port recuper\u00e9ieren", + "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen.", + "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert", + "resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn" + }, + "step": { + "user": { + "data": { + "host": "Den Hostnumm vum Zertifikat", + "name": "De Numm vum Zertifikat", + "port": "De Port vum Zertifikat" + }, + "title": "W\u00e9ieen Zertifikat soll getest ginn" + } + }, + "title": "Zertifikat Verfallsdatum" } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/no.json b/homeassistant/components/cert_expiry/.translations/no.json index e095cc360a0f4e..73e899106c1063 100644 --- a/homeassistant/components/cert_expiry/.translations/no.json +++ b/homeassistant/components/cert_expiry/.translations/no.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Kan ikke hente sertifikat fra denne verts- og portkombinasjonen", - "connection_timeout": "Timeout n\u00e5r det kobles til denne verten", + "connection_timeout": "Tidsavbrudd n\u00e5r du kobler til denne verten", "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert", "resolve_failed": "Denne verten kan ikke l\u00f8ses" }, diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hans.json b/homeassistant/components/cert_expiry/.translations/zh-Hans.json new file mode 100644 index 00000000000000..07affc990a8173 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "connection_timeout": "\u8fde\u63a5\u5230\u6b64\u4e3b\u673a\u65f6\u7684\u8d85\u65f6" + }, + "step": { + "user": { + "data": { + "host": "\u8bc1\u4e66\u7684\u4e3b\u673a\u540d", + "name": "\u8bc1\u4e66\u7684\u540d\u79f0", + "port": "\u8bc1\u4e66\u7684\u7aef\u53e3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index c536e5771411f9..1a03143f11edfd 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -49,6 +49,8 @@ "button_3": "Dr\u00ebtte Kn\u00e4ppchen", "button_4": "V\u00e9ierte Kn\u00e4ppchen", "close": "Zoumaachen", + "dim_down": "Erhellen", + "dim_up": "Verd\u00e4ischteren", "left": "L\u00e9nks", "open": "Op", "right": "Riets", @@ -70,6 +72,13 @@ }, "options": { "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", + "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben" + }, + "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren" + }, "deconz_devices": { "data": { "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 92fd1e3e7490c6..612c5afd033b79 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -49,9 +49,13 @@ "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", + "dim_down": "\u0423\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", + "dim_up": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", "left": "\u041d\u0430\u043b\u0435\u0432\u043e", "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", - "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e" + "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c" }, "trigger_type": { "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", @@ -62,7 +66,8 @@ "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430", "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", - "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b" + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438" } }, "options": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/lb.json b/homeassistant/components/geonetnz_quakes/.translations/lb.json new file mode 100644 index 00000000000000..2499befecbb822 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Standuert ass scho registr\u00e9iert" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "F\u00ebllt \u00e4r Filter D\u00e9tailer aus." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json b/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json new file mode 100644 index 00000000000000..3786b03f41fc3e --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "\u586b\u5199\u60a8\u7684filter\u8be6\u7ec6\u4fe1\u606f\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hans.json b/homeassistant/components/homekit_controller/.translations/zh-Hans.json index 8d064622f7e468..d9fdc8f91c2e1f 100644 --- a/homeassistant/components/homekit_controller/.translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/.translations/zh-Hans.json @@ -23,7 +23,7 @@ "data": { "pairing_code": "\u914d\u5bf9\u4ee3\u7801" }, - "description": "\u8f93\u5165\u60a8\u7684 HomeKit \u914d\u5bf9\u4ee3\u7801\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", + "description": "\u8f93\u5165\u60a8\u7684HomeKit\u914d\u5bf9\u4ee3\u7801\uff08\u683c\u5f0f\u4e3aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6", "title": "\u4e0e HomeKit \u914d\u4ef6\u914d\u5bf9" }, "user": { diff --git a/homeassistant/components/iqvia/.translations/pl.json b/homeassistant/components/iqvia/.translations/pl.json index 7a6e9a8a915634..b528cdeb70f3b0 100644 --- a/homeassistant/components/iqvia/.translations/pl.json +++ b/homeassistant/components/iqvia/.translations/pl.json @@ -9,7 +9,7 @@ "data": { "zip_code": "Kod pocztowy" }, - "description": "Wprowad\u017a sw\u00f3j ameryka\u0144ski lub kanadyjski kod pocztowy.", + "description": "Wprowad\u017a ameryka\u0144ski lub kanadyjski kod pocztowy.", "title": "IQVIA" } }, diff --git a/homeassistant/components/izone/.translations/ca.json b/homeassistant/components/izone/.translations/ca.json new file mode 100644 index 00000000000000..b80d9bee4e271a --- /dev/null +++ b/homeassistant/components/izone/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius iZone a la xarxa.", + "single_instance_allowed": "Nom\u00e9s cal una \u00fanica configuraci\u00f3 de iZone." + }, + "step": { + "confirm": { + "description": "Vols configurar iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/fr.json b/homeassistant/components/izone/.translations/fr.json new file mode 100644 index 00000000000000..c90416b0619746 --- /dev/null +++ b/homeassistant/components/izone/.translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique iZone trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Une seule configuration d'iZone est n\u00e9cessaire." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/ko.json b/homeassistant/components/izone/.translations/ko.json new file mode 100644 index 00000000000000..69b8ce8a31ea35 --- /dev/null +++ b/homeassistant/components/izone/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "iZone \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 iZone \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "iZone \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/lb.json b/homeassistant/components/izone/.translations/lb.json new file mode 100644 index 00000000000000..c6e075683ad159 --- /dev/null +++ b/homeassistant/components/izone/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng iZone Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun iZone ass n\u00e9ideg." + }, + "step": { + "confirm": { + "description": "Soll iZone konfigur\u00e9iert ginn?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/no.json b/homeassistant/components/izone/.translations/no.json new file mode 100644 index 00000000000000..fcd5c1df019a6b --- /dev/null +++ b/homeassistant/components/izone/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Finner ingen iZone-enheter p\u00e5 nettverket.", + "single_instance_allowed": "Bare en enkelt konfigurasjon av iZone er n\u00f8dvendig." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/pl.json b/homeassistant/components/izone/.translations/pl.json new file mode 100644 index 00000000000000..4f90cf71cbcb04 --- /dev/null +++ b/homeassistant/components/izone/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 iZone.", + "single_instance_allowed": "Wymagana jest tylko jedna konfiguracja iZone." + }, + "step": { + "confirm": { + "description": "Chcesz skonfigurowa\u0107 iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/ru.json b/homeassistant/components/izone/.translations/ru.json new file mode 100644 index 00000000000000..7e632c8dd62119 --- /dev/null +++ b/homeassistant/components/izone/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 iZone \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/lb.json b/homeassistant/components/life360/.translations/lb.json index bfed5937e24bea..3af9ab00728e5d 100644 --- a/homeassistant/components/life360/.translations/lb.json +++ b/homeassistant/components/life360/.translations/lb.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ong\u00eblteg Login Informatioune", "invalid_username": "Ong\u00ebltege Benotzernumm", + "unexpected": "Onerwaarte Feeler bei der Kommunikatioun mam Life360 Server", "user_already_configured": "Kont ass scho konfigur\u00e9iert" }, "step": { diff --git a/homeassistant/components/linky/.translations/zh-Hans.json b/homeassistant/components/linky/.translations/zh-Hans.json new file mode 100644 index 00000000000000..b450a3cbdb08c7 --- /dev/null +++ b/homeassistant/components/linky/.translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "username_exists": "\u8d26\u6237\u5df2\u914d\u7f6e\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7535\u5b50\u90ae\u7bb1" + }, + "description": "\u8f93\u5165\u60a8\u7684\u8eab\u4efd\u8ba4\u8bc1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/no.json b/homeassistant/components/met/.translations/no.json index 6ebaa08457f653..9a3ef350ab1082 100644 --- a/homeassistant/components/met/.translations/no.json +++ b/homeassistant/components/met/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Navnet eksisterer allerede" + "name_exists": "Lokasjonen finnes allerede" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/zh-Hans.json b/homeassistant/components/met/.translations/zh-Hans.json index 9565bb666181d4..9027347174d3e7 100644 --- a/homeassistant/components/met/.translations/zh-Hans.json +++ b/homeassistant/components/met/.translations/zh-Hans.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "name_exists": "\u4f4d\u7f6e\u5df2\u5b58\u5728" + }, "step": { "user": { "data": { + "elevation": "\u6d77\u62d4", "latitude": "\u7eac\u5ea6", - "longitude": "\u7ecf\u5ea6" + "longitude": "\u7ecf\u5ea6", + "name": "\u540d\u79f0" }, "title": "\u4f4d\u7f6e" } diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index f43fbeb58b7b0e..c7e89c368c178c 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c", - "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b" + "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e" }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/zh-Hans.json b/homeassistant/components/plaato/.translations/zh-Hans.json new file mode 100644 index 00000000000000..8d5c25babfab46 --- /dev/null +++ b/homeassistant/components/plaato/.translations/zh-Hans.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u53ef\u4ece\u4e92\u8054\u7f51\u8bbf\u95ee\u4ee5\u63a5\u6536Plaato Airlock\u6d88\u606f\u3002" + }, + "step": { + "user": { + "description": "\u4f60\u786e\u5b9a\u8981\u8bbe\u7f6ePlaato Airlock\u5417\uff1f", + "title": "\u8bbe\u7f6ePlaato Webhook" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json new file mode 100644 index 00000000000000..eb4f6459f4dcbe --- /dev/null +++ b/homeassistant/components/plex/.translations/ca.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats", + "already_configured": "Aquest servidor Plex ja est\u00e0 configurat", + "already_in_progress": "S\u2019est\u00e0 configurant Plex", + "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", + "unknown": "Ha fallat per motiu desconegut" + }, + "error": { + "faulty_credentials": "Ha fallat l'autoritzaci\u00f3", + "no_servers": "No hi ha servidors enlla\u00e7ats amb el compte", + "not_found": "No s'ha trobat el servidor Plex" + }, + "step": { + "select_server": { + "data": { + "server": "Servidor" + }, + "description": "Hi ha diversos servidors disponibles, selecciona'n un:", + "title": "Selecciona servidor Plex" + }, + "user": { + "data": { + "token": "Testimoni d'autenticaci\u00f3 Plex" + }, + "description": "Introdueix un testimoni d'autenticaci\u00f3 Plex per configurar-ho autom\u00e0ticament.", + "title": "Connexi\u00f3 amb el servidor Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json new file mode 100644 index 00000000000000..58a5169ac02701 --- /dev/null +++ b/homeassistant/components/plex/.translations/fr.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "Tous les serveurs li\u00e9s sont d\u00e9j\u00e0 configur\u00e9s", + "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Plex en cours de configuration", + "invalid_import": "La configuration import\u00e9e est invalide", + "unknown": "\u00c9chec pour une raison inconnue" + }, + "error": { + "faulty_credentials": "L'autorisation \u00e0 \u00e9chou\u00e9e", + "no_servers": "Aucun serveur li\u00e9 au compte", + "not_found": "Serveur Plex introuvable" + }, + "step": { + "select_server": { + "data": { + "server": "Serveur" + }, + "description": "Plusieurs serveurs disponibles, s\u00e9lectionnez-en un:", + "title": "S\u00e9lectionnez le serveur Plex" + }, + "user": { + "data": { + "token": "Jeton plex" + }, + "description": "Entrez un jeton Plex pour la configuration automatique.", + "title": "Connecter un serveur Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json new file mode 100644 index 00000000000000..d2610c68aed74f --- /dev/null +++ b/homeassistant/components/plex/.translations/ko.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84", + "already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", + "invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4", + "no_servers": "\uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc11c\ubc84\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "not_found": "Plex \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "select_server": { + "data": { + "server": "\uc11c\ubc84" + }, + "description": "\uc5ec\ub7ec \uc11c\ubc84\uac00 \uc0ac\uc6a9 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", + "title": "Plex \uc11c\ubc84 \uc120\ud0dd" + }, + "user": { + "data": { + "token": "Plex \ud1a0\ud070" + }, + "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json new file mode 100644 index 00000000000000..130cf2067abe72 --- /dev/null +++ b/homeassistant/components/plex/.translations/lb.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "All verbonne Server sinn scho konfigur\u00e9iert", + "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert", + "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", + "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", + "unknown": "Onbekannte Feeler opgetrueden" + }, + "error": { + "faulty_credentials": "Feeler beider Autorisatioun", + "no_servers": "Kee Server as mam Kont verbonnen", + "not_found": "Kee Plex Server fonnt" + }, + "step": { + "select_server": { + "data": { + "server": "Server" + }, + "description": "M\u00e9i Server disponibel, wielt een aus:", + "title": "Plex Server auswielen" + }, + "user": { + "data": { + "token": "Jeton fir de Plex" + }, + "description": "Gitt een Jeton fir de Plex un fir eng automatesch Konfiguratioun", + "title": "Plex Server verbannen" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json new file mode 100644 index 00000000000000..8ac90efe3d1474 --- /dev/null +++ b/homeassistant/components/plex/.translations/no.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "Alle knyttet servere som allerede er konfigurert", + "already_configured": "Denne Plex-serveren er allerede konfigurert", + "already_in_progress": "Plex blir konfigurert", + "invalid_import": "Den importerte konfigurasjonen er ugyldig", + "unknown": "Mislyktes av ukjent \u00e5rsak" + }, + "error": { + "faulty_credentials": "Autorisasjonen mislyktes", + "no_servers": "Ingen servere koblet til kontoen", + "not_found": "Plex-server ikke funnet" + }, + "step": { + "select_server": { + "data": { + "server": "Server" + }, + "description": "Flere servere tilgjengelig, velg en:", + "title": "Velg Plex-server" + }, + "user": { + "data": { + "token": "Plex token" + }, + "description": "Legg inn et Plex-token for automatisk oppsett.", + "title": "Koble til Plex-server" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json new file mode 100644 index 00000000000000..606f97d6965c60 --- /dev/null +++ b/homeassistant/components/plex/.translations/pl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.", + "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", + "already_in_progress": "Plex jest konfigurowany", + "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", + "unknown": "Nieznany b\u0142\u0105d" + }, + "error": { + "faulty_credentials": "Autoryzacja nie powiod\u0142a si\u0119", + "no_servers": "Brak serwer\u00f3w po\u0142\u0105czonych z kontem", + "not_found": "Nie znaleziono serwera Plex" + }, + "step": { + "select_server": { + "data": { + "server": "Serwer" + }, + "description": "Dost\u0119pnych jest wiele serwer\u00f3w, wybierz jeden:", + "title": "Wybierz serwer Plex" + }, + "user": { + "data": { + "token": "Token Plex" + }, + "description": "Wprowad\u017a token Plex do automatycznej konfiguracji.", + "title": "Po\u0142\u0105cz z serwerem Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json new file mode 100644 index 00000000000000..46cd613df4ac70 --- /dev/null +++ b/homeassistant/components/plex/.translations/ru.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", + "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", + "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" + }, + "error": { + "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", + "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e", + "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d" + }, + "step": { + "select_server": { + "data": { + "server": "\u0421\u0435\u0440\u0432\u0435\u0440" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u0438\u043d \u0438\u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432:", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" + }, + "user": { + "data": { + "token": "\u0422\u043e\u043a\u0435\u043d" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d Plex \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "title": "Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/fr.json b/homeassistant/components/switch/.translations/fr.json index 4775d62bce3758..807b85c5fb56d6 100644 --- a/homeassistant/components/switch/.translations/fr.json +++ b/homeassistant/components/switch/.translations/fr.json @@ -6,6 +6,8 @@ "turn_on": "Allumer {entity_name}" }, "condition_type": { + "is_off": "{entity_name} est \u00e9teint", + "is_on": "{entity_name} est allum\u00e9", "turn_off": "{entity_name} \u00e9teint", "turn_on": "{entity_name} allum\u00e9" }, diff --git a/homeassistant/components/switch/.translations/no.json b/homeassistant/components/switch/.translations/no.json index adc128991c5efc..3469079f230b43 100644 --- a/homeassistant/components/switch/.translations/no.json +++ b/homeassistant/components/switch/.translations/no.json @@ -6,6 +6,8 @@ "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" }, "condition_type": { + "is_off": "{entity_name} er av", + "is_on": "{entity_name} er p\u00e5", "turn_off": "{entity_name} sl\u00e5tt av", "turn_on": "{entity_name} sl\u00e5tt p\u00e5" }, diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 921048286b6a82..199b150f68ee6d 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -6,7 +6,7 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "(entity_name} jest wy\u0142\u0105czony.", + "is_off": "{entity_name} jest wy\u0142\u0105czony.", "is_on": "{entity_name} jest w\u0142\u0105czony", "turn_off": "{entity_name} wy\u0142\u0105czone", "turn_on": "{entity_name} w\u0142\u0105czone" diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index 45a941b665de03..b769e56c97446a 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -6,6 +6,8 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, diff --git a/homeassistant/components/traccar/.translations/zh-Hans.json b/homeassistant/components/traccar/.translations/zh-Hans.json new file mode 100644 index 00000000000000..248e8f9f44ed89 --- /dev/null +++ b/homeassistant/components/traccar/.translations/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u60a8\u7684 Home Assistant \u5b9e\u4f8b\u9700\u8981\u53ef\u4ece\u4e92\u8054\u7f51\u8bbf\u95ee\u4ee5\u63a5\u6536Traccar\u6d88\u606f\u3002", + "one_instance_allowed": "\u53ea\u6709\u4e00\u4e2a\u5b9e\u4f8b\u662f\u5fc5\u9700\u7684\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/lb.json b/homeassistant/components/twentemilieu/.translations/lb.json index 0b07c5003ef2ff..b6f10842b4d1c7 100644 --- a/homeassistant/components/twentemilieu/.translations/lb.json +++ b/homeassistant/components/twentemilieu/.translations/lb.json @@ -4,7 +4,8 @@ "address_exists": "Adresse ass scho ageriicht." }, "error": { - "connection_error": "Feeler beim verbannen." + "connection_error": "Feeler beim verbannen.", + "invalid_address": "Adresse net am Twente Milieu Service Ber\u00e4ich fonnt" }, "step": { "user": { @@ -13,6 +14,7 @@ "house_number": "Haus Nummer", "post_code": "Postleitzuel" }, + "description": "Offallsammlung Informatiounen vun Twente Milieu zu \u00e4erer Adresse ariichten.", "title": "Twente Milieu" } }, diff --git a/homeassistant/components/twentemilieu/.translations/zh-Hans.json b/homeassistant/components/twentemilieu/.translations/zh-Hans.json new file mode 100644 index 00000000000000..80301cfd57b808 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "address_exists": "\u5730\u5740\u5df2\u7ecf\u8bbe\u7f6e\u597d\u4e86\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json index 3bef273b83e53f..05b0ffc0c44cdf 100644 --- a/homeassistant/components/unifi/.translations/lb.json +++ b/homeassistant/components/unifi/.translations/lb.json @@ -22,5 +22,23 @@ } }, "title": "Unifi Kontroller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Z\u00e4it a Sekonne vum leschten Z\u00e4itpunkt un bis den Apparat als \u00ebnnerwee consider\u00e9iert g\u00ebtt", + "track_clients": "Netzwierk Cliente verfollegen", + "track_devices": "Netzwierk Apparater (Ubiquiti Apparater) verfollegen", + "track_wired_clients": "Kabel Netzwierk Cliente abez\u00e9ien" + } + }, + "init": { + "data": { + "one": "Een", + "other": "M\u00e9i" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/lb.json b/homeassistant/components/velbus/.translations/lb.json index 89e0bd818d277c..f38a74e5c1fd19 100644 --- a/homeassistant/components/velbus/.translations/lb.json +++ b/homeassistant/components/velbus/.translations/lb.json @@ -12,7 +12,8 @@ "data": { "name": "Numm fir d\u00ebs velbus Verbindung", "port": "Verbindungs zeeche-folleg" - } + }, + "title": "D\u00e9fin\u00e9iert den Typ vun der Velbus Verbindung" } }, "title": "Velbus Interface" diff --git a/homeassistant/components/velbus/.translations/zh-Hans.json b/homeassistant/components/velbus/.translations/zh-Hans.json new file mode 100644 index 00000000000000..7b2bc3b028b78d --- /dev/null +++ b/homeassistant/components/velbus/.translations/zh-Hans.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "port_exists": "\u6b64\u7aef\u53e3\u5df2\u914d\u7f6e\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "name": "\u8fd9\u4e2avelbus\u8fde\u63a5\u7684\u540d\u79f0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/zh-Hans.json b/homeassistant/components/vesync/.translations/zh-Hans.json new file mode 100644 index 00000000000000..caa00f36c89435 --- /dev/null +++ b/homeassistant/components/vesync/.translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_setup": "\u53ea\u5141\u8bb8\u4e00\u4e2aVesync\u5b9e\u4f8b" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json index b66786cc9e0e18..ad715d54eb1df5 100644 --- a/homeassistant/components/withings/.translations/fr.json +++ b/homeassistant/components/withings/.translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Vous devez configurer Withings avant de pouvoir vous authentifier avec celui-ci. Veuillez lire la documentation." + }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." }, diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json index 9015f4908308d1..5ca969f039102b 100644 --- a/homeassistant/components/withings/.translations/lb.json +++ b/homeassistant/components/withings/.translations/lb.json @@ -1,10 +1,17 @@ { "config": { + "abort": { + "no_flows": "Dir musst Withingss konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen. Liest w.e.g. d'Instruktioune." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mam ausgewielte Profile mat Withings authentifiz\u00e9iert." + }, "step": { "user": { "data": { "profile": "Profil" }, + "description": "Wielt ee Benotzer Profile aus dee mam Withings Profile soll verbonne ginn. Stellt s\u00e9cher dass dir op der Withings S\u00e4it deeselwechte Benotzer auswielt, soss ginn d'Donn\u00e9e net richteg ugewisen.", "title": "Benotzer Profil." } }, diff --git a/homeassistant/components/withings/.translations/no.json b/homeassistant/components/withings/.translations/no.json index 22d8884d66a573..d32c9640fd7636 100644 --- a/homeassistant/components/withings/.translations/no.json +++ b/homeassistant/components/withings/.translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Du m\u00e5 konfigurere Withings f\u00f8r du kan godkjenne med den. Vennligst les dokumentasjonen." + }, "create_entry": { "default": "Vellykket autentisering for Withings og den valgte profilen." }, diff --git a/homeassistant/components/withings/.translations/zh-Hans.json b/homeassistant/components/withings/.translations/zh-Hans.json new file mode 100644 index 00000000000000..c7485b09248ccd --- /dev/null +++ b/homeassistant/components/withings/.translations/zh-Hans.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "description": "\u8bf7\u9009\u62e9\u4f60\u60f3\u8981Home Assistant\u548cWithings\u5bf9\u5e94\u7684\u7528\u6237\u914d\u7f6e\u6587\u4ef6\u3002\u5728Withings\u9875\u9762\u4e0a\uff0c\u8bf7\u52a1\u5fc5\u9009\u62e9\u76f8\u540c\u7684\u7528\u6237\uff0c\u5426\u5219\u6570\u636e\u5c06\u65e0\u6cd5\u6b63\u786e\u6807\u8bb0\u3002", + "title": "\u7528\u6237\u8d44\u6599" + } + } + } +} \ No newline at end of file From ed21019b7a4c746b443b7d1192db11880da06c90 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 21 Sep 2019 14:34:08 +0100 Subject: [PATCH 0385/3953] Bump HAP-python to 2.6.0 for homekit (#26783) --- homeassistant/components/homekit/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/manifest.json b/homeassistant/components/homekit/manifest.json index ea3e801ac536f4..ebb0895bd7a100 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "Homekit", "documentation": "https://www.home-assistant.io/components/homekit", "requirements": [ - "HAP-python==2.5.0" + "HAP-python==2.6.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 9d7e8b645b1f5c..18ca056584653f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.0.0 # homeassistant.components.homekit -HAP-python==2.5.0 +HAP-python==2.6.0 # homeassistant.components.mastodon Mastodon.py==1.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdc9ad8dc3f265..621863c8efdc5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -23,7 +23,7 @@ requests_mock==1.6.0 # homeassistant.components.homekit -HAP-python==2.5.0 +HAP-python==2.6.0 # homeassistant.components.mobile_app # homeassistant.components.owntracks From 0e157856027d854e4183e00d91c830bfcdad877f Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Sat, 21 Sep 2019 09:56:40 -0400 Subject: [PATCH 0386/3953] Bump pynws version to 0.8.1 (#26770) * Bump to version 0.8.1 Fixes #26753. * gen_requirements.py changes * fix default params change in tests --- homeassistant/components/nws/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nws/test_weather.py | 21 +++++---------------- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index b0e5fdb208844a..bad90d9e827d77 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/components/nws", "dependencies": [], "codeowners": ["@MatthewFlamm"], - "requirements": ["pynws==0.7.4"] + "requirements": ["pynws==0.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 18ca056584653f..ef036c99c70f27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1339,7 +1339,7 @@ pynuki==1.3.3 pynut2==2.1.2 # homeassistant.components.nws -pynws==0.7.4 +pynws==0.8.1 # homeassistant.components.nx584 pynx584==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 621863c8efdc5f..a8aa92c81ddbbc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -312,7 +312,7 @@ pymfy==0.5.2 pymonoprice==0.3 # homeassistant.components.nws -pynws==0.7.4 +pynws==0.8.1 # homeassistant.components.nx584 pynx584==0.4 diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 436d25750fc518..0e450f06238d94 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -110,9 +110,7 @@ async def test_imperial(hass, aioclient_mock): STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") ) aioclient_mock.get( - OBSURL.format("KMIE"), - text=load_fixture("nws-weather-obs-valid.json"), - params={"limit": 1}, + OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") ) aioclient_mock.get( FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") @@ -142,9 +140,7 @@ async def test_metric(hass, aioclient_mock): STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") ) aioclient_mock.get( - OBSURL.format("KMIE"), - text=load_fixture("nws-weather-obs-valid.json"), - params={"limit": 1}, + OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") ) aioclient_mock.get( FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") @@ -174,9 +170,7 @@ async def test_none(hass, aioclient_mock): STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") ) aioclient_mock.get( - OBSURL.format("KMIE"), - text=load_fixture("nws-weather-obs-null.json"), - params={"limit": 1}, + OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-null.json") ) aioclient_mock.get( FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-null.json") @@ -208,7 +202,6 @@ async def test_fail_obs(hass, aioclient_mock): aioclient_mock.get( OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json"), - params={"limit": 1}, status=400, ) aioclient_mock.get( @@ -234,9 +227,7 @@ async def test_fail_stn(hass, aioclient_mock): status=400, ) aioclient_mock.get( - OBSURL.format("KMIE"), - text=load_fixture("nws-weather-obs-valid.json"), - params={"limit": 1}, + OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") ) aioclient_mock.get( FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") @@ -257,9 +248,7 @@ async def test_invalid_config(hass, aioclient_mock): STAURL.format(40.0, -85.0), text=load_fixture("nws-weather-sta-valid.json") ) aioclient_mock.get( - OBSURL.format("KMIE"), - text=load_fixture("nws-weather-obs-valid.json"), - params={"limit": 1}, + OBSURL.format("KMIE"), text=load_fixture("nws-weather-obs-valid.json") ) aioclient_mock.get( FORCURL.format(40.0, -85.0), text=load_fixture("nws-weather-fore-valid.json") From 9d0cb899ec74744ee669b722c237b32c2f3a29a5 Mon Sep 17 00:00:00 2001 From: scheric <38077357+scheric@users.noreply.github.com> Date: Sat, 21 Sep 2019 20:07:53 +0200 Subject: [PATCH 0387/3953] Add optimizer data to solaredge_local (#26708) * making SENSOR_TYPES universal * bump solaredge-local version 0.2.0 * add maintenance data * add calculations for optimizer data * add new sensors * add statistics lib * update sensor data setting * making api requests universal * fix Cl * Update requirements_all.txt * fix temperature * fix f-strings * Style guidelines * shortening line length * PEP8 test * flake8 * Black test * revert line length to 80 * Repair line 12 * black * style change * Black * black using pip * fix for pylint * added proper variable name * for loop cleanup * fix capitals * Update units * black * add check for good connection to inverter * Roundig large numbers * Add myself to codeowners * Fix layout manifest * Fix layout manifest * Update manifest.json * repair manifest layout * remove newline in manifest * add myself to CODEOWNERS --- CODEOWNERS | 2 +- .../components/solaredge_local/manifest.json | 19 ++- .../components/solaredge_local/sensor.py | 139 +++++++++++++----- requirements_all.txt | 2 +- 4 files changed, 115 insertions(+), 47 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9bcd475d5d4788..700e7145838f0c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -246,7 +246,7 @@ homeassistant/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/smtp/* @fabaff -homeassistant/components/solaredge_local/* @drobtravels +homeassistant/components/solaredge_local/* @drobtravels @scheric homeassistant/components/solax/* @squishykid homeassistant/components/somfy/* @tetienne homeassistant/components/songpal/* @rytilahti diff --git a/homeassistant/components/solaredge_local/manifest.json b/homeassistant/components/solaredge_local/manifest.json index 5fb07011983edd..291c774c383d56 100644 --- a/homeassistant/components/solaredge_local/manifest.json +++ b/homeassistant/components/solaredge_local/manifest.json @@ -1,8 +1,13 @@ { - "domain": "solaredge_local", - "name": "Solar Edge Local", - "documentation": "", - "dependencies": [], - "codeowners": ["@drobtravels"], - "requirements": ["solaredge-local==0.1.4"] - } \ No newline at end of file + "domain": "solaredge_local", + "name": "Solar Edge Local", + "documentation": "https://www.home-assistant.io/components/solaredge_local", + "requirements": [ + "solaredge-local==0.2.0" + ], + "dependencies": [], + "codeowners": [ + "@drobtravels", + "@scheric" + ] +} diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 8586d950e39cdb..4fc62e44921448 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -1,19 +1,20 @@ -""" -Support for SolarEdge Monitoring API. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.solaredge_local/ -""" +"""Support for SolarEdge-local Monitoring API.""" import logging from datetime import timedelta +import statistics from requests.exceptions import HTTPError, ConnectTimeout from solaredge_local import SolarEdge import voluptuous as vol - from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME, POWER_WATT, ENERGY_WATT_HOUR +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_NAME, + POWER_WATT, + ENERGY_WATT_HOUR, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -24,9 +25,10 @@ # Supported sensor types: # Key: ['json_key', 'name', unit, icon] SENSOR_TYPES = { - "lifetime_energy": [ - "energyTotal", - "Lifetime energy", + "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"], + "energy_this_month": [ + "energyThisMonth", + "Energy this month", ENERGY_WATT_HOUR, "mdi:solar-power", ], @@ -36,19 +38,48 @@ ENERGY_WATT_HOUR, "mdi:solar-power", ], - "energy_this_month": [ - "energyThisMonth", - "Energy this month", - ENERGY_WATT_HOUR, - "mdi:solar-power", - ], "energy_today": [ "energyToday", "Energy today", ENERGY_WATT_HOUR, "mdi:solar-power", ], - "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"], + "inverter_temperature": [ + "invertertemperature", + "Inverter Temperature", + TEMP_CELSIUS, + "mdi:thermometer", + ], + "lifetime_energy": [ + "energyTotal", + "Lifetime energy", + ENERGY_WATT_HOUR, + "mdi:solar-power", + ], + "optimizer_current": [ + "optimizercurrent", + "Avrage Optimizer Current", + "A", + "mdi:solar-panel", + ], + "optimizer_power": [ + "optimizerpower", + "Avrage Optimizer Power", + POWER_WATT, + "mdi:solar-panel", + ], + "optimizer_temperature": [ + "optimizertemperature", + "Avrage Optimizer Temperature", + TEMP_CELSIUS, + "mdi:solar-panel", + ], + "optimizer_voltage": [ + "optimizervoltage", + "Avrage Optimizer Voltage", + "V", + "mdi:solar-panel", + ], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -66,18 +97,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ip_address = config[CONF_IP_ADDRESS] platform_name = config[CONF_NAME] - # Create new SolarEdge object to retrieve data + # Create new SolarEdge object to retrieve data. api = SolarEdge(f"http://{ip_address}/") - # Check if api can be reached and site is active + # Check if api can be reached and site is active. try: status = api.get_status() - - status.energy # pylint: disable=pointless-statement _LOGGER.debug("Credentials correct and site is active") except AttributeError: - _LOGGER.error("Missing details data in solaredge response") - _LOGGER.debug("Response is: %s", status) + _LOGGER.error("Missing details data in solaredge status") + _LOGGER.debug("Status is: %s", status) return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve details from SolarEdge API") @@ -111,7 +140,7 @@ def __init__(self, platform_name, sensor_key, data): @property def name(self): """Return the name.""" - return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1]) + return f"{self.platform_name} ({SENSOR_TYPES[self.sensor_key][1]})" @property def unit_of_measurement(self): @@ -147,21 +176,55 @@ def __init__(self, hass, api): def update(self): """Update the data from the SolarEdge Monitoring API.""" try: - response = self.api.get_status() - _LOGGER.debug("response from SolarEdge: %s", response) - except (ConnectTimeout): + status = self.api.get_status() + _LOGGER.debug("Status from SolarEdge: %s", status) + except ConnectTimeout: _LOGGER.error("Connection timeout, skipping update") return - except (HTTPError): - _LOGGER.error("Could not retrieve data, skipping update") + except HTTPError: + _LOGGER.error("Could not retrieve status, skipping update") return try: - self.data["energyTotal"] = response.energy.total - self.data["energyThisYear"] = response.energy.thisYear - self.data["energyThisMonth"] = response.energy.thisMonth - self.data["energyToday"] = response.energy.today - self.data["currentPower"] = response.powerWatt - _LOGGER.debug("Updated SolarEdge overview data: %s", self.data) - except AttributeError: - _LOGGER.error("Missing details data in SolarEdge response") + maintenance = self.api.get_maintenance() + _LOGGER.debug("Maintenance from SolarEdge: %s", maintenance) + except ConnectTimeout: + _LOGGER.error("Connection timeout, skipping update") + return + except HTTPError: + _LOGGER.error("Could not retrieve maintenance, skipping update") + return + + temperature = [] + voltage = [] + current = [] + power = 0 + + for optimizer in maintenance.diagnostics.inverters.primary.optimizer: + if not optimizer.online: + continue + temperature.append(optimizer.temperature.value) + voltage.append(optimizer.inputV) + current.append(optimizer.inputC) + + if not voltage: + temperature.append(0) + voltage.append(0) + current.append(0) + else: + power = statistics.mean(voltage) * statistics.mean(current) + + if status.sn: + self.data["energyTotal"] = round(status.energy.total, 2) + self.data["energyThisYear"] = round(status.energy.thisYear, 2) + self.data["energyThisMonth"] = round(status.energy.thisMonth, 2) + self.data["energyToday"] = round(status.energy.today, 2) + self.data["currentPower"] = round(status.powerWatt, 2) + self.data[ + "invertertemperature" + ] = status.inverters.primary.temperature.value + if maintenance.system.name: + self.data["optimizertemperature"] = statistics.mean(temperature) + self.data["optimizervoltage"] = statistics.mean(voltage) + self.data["optimizercurrent"] = statistics.mean(current) + self.data["optimizerpower"] = power diff --git a/requirements_all.txt b/requirements_all.txt index ef036c99c70f27..58c59a1c04c38e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1779,7 +1779,7 @@ snapcast==2.0.10 socialbladeclient==0.2 # homeassistant.components.solaredge_local -solaredge-local==0.1.4 +solaredge-local==0.2.0 # homeassistant.components.solaredge solaredge==0.0.2 From e394be73374574b11e1a7111165dbc577755b245 Mon Sep 17 00:00:00 2001 From: Albert Gouws Date: Sun, 22 Sep 2019 06:42:03 +1200 Subject: [PATCH 0388/3953] Mqtt binary sensor expire after (#26058) * Added expire_after to mqtt binary_sensor. Updated mqtt test_binary_sensor test. * Cleanup MQTT Binary Sensor and tests after suggestions * Updated to not alter state at all * Change to include custom expired variable, and override available property to check expired * Added # pylint: disable=no-member --- .../components/mqtt/binary_sensor.py | 46 +++++++- tests/components/mqtt/test_binary_sensor.py | 107 +++++++++++++++++- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 4617fcf054a232..bcf398464bc7c0 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,4 +1,5 @@ """Support for MQTT binary sensors.""" +from datetime import timedelta import logging import voluptuous as vol @@ -21,7 +22,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.helpers.event as evt +from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import dt as dt_util from . import ( ATTR_DISCOVERY_HASH, @@ -43,12 +46,14 @@ DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_FORCE_UPDATE = False +CONF_EXPIRE_AFTER = "expire_after" PLATFORM_SCHEMA = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OFF_DELAY): vol.All(vol.Coerce(int), vol.Range(min=0)), @@ -112,8 +117,9 @@ def __init__(self, config, config_entry, discovery_hash): self._unique_id = config.get(CONF_UNIQUE_ID) self._state = None self._sub_state = None + self._expiration_trigger = None self._delay_listener = None - + self._expired = None device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) @@ -153,6 +159,26 @@ def off_delay_listener(now): def state_message_received(msg): """Handle a new received MQTT state message.""" payload = msg.payload + # auto-expire enabled? + expire_after = self._config.get(CONF_EXPIRE_AFTER) + + if expire_after is not None and expire_after > 0: + + # When expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message + self._expired = False + + # Reset old trigger + if self._expiration_trigger: + self._expiration_trigger() + self._expiration_trigger = None + + # Set new trigger + expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after) + + self._expiration_trigger = async_track_point_in_utc_time( + self.hass, self.value_is_expired, expiration_at + ) + value_template = self._config.get(CONF_VALUE_TEMPLATE) if value_template is not None: payload = value_template.async_render_with_possible_json_value( @@ -202,6 +228,15 @@ async def async_will_remove_from_hass(self): await MqttAttributes.async_will_remove_from_hass(self) await MqttAvailability.async_will_remove_from_hass(self) + @callback + def value_is_expired(self, *_): + """Triggered when value is expired.""" + + self._expiration_trigger = None + self._expired = True + + self.async_write_ha_state() + @property def should_poll(self): """Return the polling state.""" @@ -231,3 +266,12 @@ def force_update(self): def unique_id(self): """Return a unique ID.""" return self._unique_id + + @property + def available(self) -> bool: + """Return true if the device is available and value has not expired.""" + expire_after = self._config.get(CONF_EXPIRE_AFTER) + # pylint: disable=no-member + return MqttAvailability.available.fget(self) and ( + expire_after is None or not self._expired + ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index af27ff8c7d1c3d..28f1a7e9720091 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1,7 +1,8 @@ """The tests for the MQTT binary sensor platform.""" -from datetime import timedelta +from datetime import datetime, timedelta import json -from unittest.mock import ANY + +from unittest.mock import ANY, patch from homeassistant.components import binary_sensor, mqtt from homeassistant.components.mqtt.discovery import async_start @@ -24,6 +25,107 @@ ) +async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog): + """Test the expiration of the value.""" + assert await async_setup_component( + hass, + binary_sensor.DOMAIN, + { + binary_sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "expire_after": 4, + "force_update": True, + "availability_topic": "availability-topic", + } + }, + ) + + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic", "online") + + state = hass.states.get("binary_sensor.test") + assert state.state != STATE_UNAVAILABLE + + await expires_helper(hass, mqtt_mock, caplog) + + +async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): + """Test the expiration of the value.""" + assert await async_setup_component( + hass, + binary_sensor.DOMAIN, + { + binary_sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "expire_after": 4, + "force_update": True, + } + }, + ) + + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + + await expires_helper(hass, mqtt_mock, caplog) + + +async def expires_helper(hass, mqtt_mock, caplog): + """Run the basic expiry code.""" + + now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) + with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + async_fire_time_changed(hass, now) + async_fire_mqtt_message(hass, "test-topic", "ON") + await hass.async_block_till_done() + + # Value was set correctly. + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + + # Time jump +3s + now = now + timedelta(seconds=3) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Value is not yet expired + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_ON + + # Next message resets timer + with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + async_fire_time_changed(hass, now) + async_fire_mqtt_message(hass, "test-topic", "OFF") + await hass.async_block_till_done() + + # Value was updated correctly. + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + + # Time jump +3s + now = now + timedelta(seconds=3) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Value is not yet expired + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF + + # Time jump +2s + now = now + timedelta(seconds=2) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Value is expired now + state = hass.states.get("binary_sensor.test") + assert state.state == STATE_UNAVAILABLE + + async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -41,6 +143,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): ) state = hass.states.get("binary_sensor.test") + assert state.state == STATE_OFF async_fire_mqtt_message(hass, "test-topic", "ON") From 8c580209a65c288bdc7716887e87f0f026933c1e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 21 Sep 2019 20:52:35 +0200 Subject: [PATCH 0389/3953] Upgrade importlib-metadata to 0.23 (#26787) --- 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 900bfddda2ec10..32804d790411f2 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.17 home-assistant-frontend==20190919.0 -importlib-metadata==0.19 +importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 58c59a1c04c38e..8af0da567da609 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.19 +importlib-metadata==0.23 jinja2>=2.10.1 PyJWT==1.7.1 cryptography==2.7 diff --git a/setup.py b/setup.py index e6776d8a1a0811..87704a3c6a96b4 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ "bcrypt==3.1.7", "certifi>=2019.6.16", 'contextvars==2.4;python_version<"3.7"', - "importlib-metadata==0.19", + "importlib-metadata==0.23", "jinja2>=2.10.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. From a9ff15077c8e8ea4fef7b8bdefe543bc59b1a467 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 21 Sep 2019 20:52:46 +0200 Subject: [PATCH 0390/3953] Upgrade python-whois to 0.7.2 (#26788) --- homeassistant/components/whois/manifest.json | 2 +- homeassistant/components/whois/sensor.py | 11 ++++------- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json index dec3e78a50362b..6040c8655b9f25 100644 --- a/homeassistant/components/whois/manifest.json +++ b/homeassistant/components/whois/manifest.json @@ -3,7 +3,7 @@ "name": "Whois", "documentation": "https://www.home-assistant.io/components/whois", "requirements": [ - "python-whois==0.7.1" + "python-whois==0.7.2" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 313a6337a11b5b..09cf40f193fe87 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -3,6 +3,7 @@ import logging import voluptuous as vol +import whois from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME @@ -32,8 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the WHOIS sensor.""" - import whois - domain = config.get(CONF_DOMAIN) name = config.get(CONF_NAME) @@ -41,7 +40,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if "expiration_date" in whois.whois(domain): add_entities([WhoisSensor(name, domain)], True) else: - _LOGGER.error("WHOIS lookup for %s didn't contain expiration_date", domain) + _LOGGER.error( + "WHOIS lookup for %s didn't contain an expiration date", domain + ) return except whois.BaseException as ex: _LOGGER.error("Exception %s occurred during WHOIS lookup for %s", ex, domain) @@ -53,8 +54,6 @@ class WhoisSensor(Entity): def __init__(self, name, domain): """Initialize the sensor.""" - import whois - self.whois = whois.whois self._name = name @@ -95,8 +94,6 @@ def _empty_state_and_attributes(self): def update(self): """Get the current WHOIS data for the domain.""" - import whois - try: response = self.whois(self._domain) except whois.BaseException as ex: diff --git a/requirements_all.txt b/requirements_all.txt index 8af0da567da609..c67928093c37cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1569,7 +1569,7 @@ python-velbus==2.0.27 python-vlc==1.1.2 # homeassistant.components.whois -python-whois==0.7.1 +python-whois==0.7.2 # homeassistant.components.wink python-wink==1.10.5 From 9e79920c9c65bc343ccb3a15fa59588c1eba304c Mon Sep 17 00:00:00 2001 From: Zach Date: Sat, 21 Sep 2019 14:53:19 -0400 Subject: [PATCH 0391/3953] Fix doods missing detector name kwarg (#26784) * Fix missing detector name kwarg * Updated requirements_all.txt * Reformatted --- homeassistant/components/doods/image_processing.py | 5 ++++- homeassistant/components/doods/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index ba44d86c2e4083..850eae76040f2a 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -139,6 +139,7 @@ def __init__(self, hass, camera_entity, name, doods, detector, config): self._name = f"Doods {name}" self._doods = doods self._file_out = config[CONF_FILE_OUT] + self._detector_name = detector["name"] # detector config and aspect ratio self._width = None @@ -289,7 +290,9 @@ def process_image(self, image): # Run detection start = time.time() - response = self._doods.detect(image, self._dconfig) + response = self._doods.detect( + image, dconfig=self._dconfig, detector_name=self._detector_name + ) _LOGGER.debug( "doods detect: %s response: %s duration: %s", self._dconfig, diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 3e1ce22a230b03..75c1bd3dcd3814 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -3,8 +3,8 @@ "name": "DOODS - Distributed Outside Object Detection Service", "documentation": "https://www.home-assistant.io/components/doods", "requirements": [ - "pydoods==1.0.1" + "pydoods==1.0.2" ], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index c67928093c37cf..a17c201a9528d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ pydelijn==0.5.1 pydispatcher==2.0.5 # homeassistant.components.doods -pydoods==1.0.1 +pydoods==1.0.2 # homeassistant.components.android_ip_webcam pydroid-ipcam==0.8 From 88dcecab397c69a3e2257cc1cb4116367a8b2d8f Mon Sep 17 00:00:00 2001 From: John Luetke Date: Sat, 21 Sep 2019 12:39:49 -0700 Subject: [PATCH 0392/3953] Add myself as a pi_hole codeowner (#26796) --- CODEOWNERS | 2 +- homeassistant/components/pi_hole/manifest.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 700e7145838f0c..abd3379221a9a8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -207,7 +207,7 @@ homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus -homeassistant/components/pi_hole/* @fabaff +homeassistant/components/pi_hole/* @fabaff @johnluetke homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 2d19ab25fe78dd..7fe8bba6873913 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -7,6 +7,7 @@ ], "dependencies": [], "codeowners": [ - "@fabaff" + "@fabaff", + "@johnluetke" ] } From dc52b858a40905c129684a8e964ce182c4ff00df Mon Sep 17 00:00:00 2001 From: bouni Date: Sun, 22 Sep 2019 01:22:33 +0200 Subject: [PATCH 0393/3953] Fix spaceapi (#26453) * fixed latitude/longitude keys to be conform with spaceapi specification * version is now a string as required by the spaceapi specification * add spacefed * fixed lat/lon in spaceapi tests * extended tests * add feeds * extended tests * add cache * add more tests * add projects * more tests * add radio_show * more tests * add additional contact attributes * corrected valid issue_repoer_channel options * validate min length of contact/keymasters * fixed location as address is not required by spec * Update homeassistant/components/spaceapi/__init__.py Co-Authored-By: Fabian Affolter * Update homeassistant/components/spaceapi/__init__.py Co-Authored-By: Fabian Affolter * fixed issue with name change for longitude/latitude --- homeassistant/components/spaceapi/__init__.py | 187 ++++++++++++++++-- tests/components/spaceapi/test_init.py | 58 +++++- 2 files changed, 229 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/spaceapi/__init__.py b/homeassistant/components/spaceapi/__init__.py index 607d9c45538142..ea5a64d97e7cf2 100644 --- a/homeassistant/components/spaceapi/__init__.py +++ b/homeassistant/components/spaceapi/__init__.py @@ -7,9 +7,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, - ATTR_LATITUDE, ATTR_LOCATION, - ATTR_LONGITUDE, ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, CONF_ADDRESS, @@ -26,6 +24,15 @@ _LOGGER = logging.getLogger(__name__) ATTR_ADDRESS = "address" +ATTR_SPACEFED = "spacefed" +ATTR_CAM = "cam" +ATTR_STREAM = "stream" +ATTR_FEEDS = "feeds" +ATTR_CACHE = "cache" +ATTR_PROJECTS = "projects" +ATTR_RADIO_SHOW = "radio_show" +ATTR_LAT = "lat" +ATTR_LON = "lon" ATTR_API = "api" ATTR_CLOSE = "close" ATTR_CONTACT = "contact" @@ -49,32 +56,135 @@ CONF_IRC = "irc" CONF_ISSUE_REPORT_CHANNELS = "issue_report_channels" CONF_LOCATION = "location" +CONF_SPACEFED = "spacefed" +CONF_SPACENET = "spacenet" +CONF_SPACESAML = "spacesaml" +CONF_SPACEPHONE = "spacephone" +CONF_CAM = "cam" +CONF_STREAM = "stream" +CONF_M4 = "m4" +CONF_MJPEG = "mjpeg" +CONF_USTREAM = "ustream" +CONF_FEEDS = "feeds" +CONF_FEED_BLOG = "blog" +CONF_FEED_WIKI = "wiki" +CONF_FEED_CALENDAR = "calendar" +CONF_FEED_FLICKER = "flicker" +CONF_FEED_TYPE = "type" +CONF_FEED_URL = "url" +CONF_CACHE = "cache" +CONF_CACHE_SCHEDULE = "schedule" +CONF_PROJECTS = "projects" +CONF_RADIO_SHOW = "radio_show" +CONF_RADIO_SHOW_NAME = "name" +CONF_RADIO_SHOW_URL = "url" +CONF_RADIO_SHOW_TYPE = "type" +CONF_RADIO_SHOW_START = "start" +CONF_RADIO_SHOW_END = "end" CONF_LOGO = "logo" -CONF_MAILING_LIST = "mailing_list" CONF_PHONE = "phone" +CONF_SIP = "sip" +CONF_KEYMASTERS = "keymasters" +CONF_KEYMASTER_NAME = "name" +CONF_KEYMASTER_IRC_NICK = "irc_nick" +CONF_KEYMASTER_PHONE = "phone" +CONF_KEYMASTER_EMAIL = "email" +CONF_KEYMASTER_TWITTER = "twitter" +CONF_TWITTER = "twitter" +CONF_FACEBOOK = "facebook" +CONF_IDENTICA = "identica" +CONF_FOURSQUARE = "foursquare" +CONF_ML = "ml" +CONF_JABBER = "jabber" +CONF_ISSUE_MAIL = "issue_mail" CONF_SPACE = "space" CONF_TEMPERATURE = "temperature" -CONF_TWITTER = "twitter" DATA_SPACEAPI = "data_spaceapi" DOMAIN = "spaceapi" -ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_IRC, CONF_MAILING_LIST, CONF_TWITTER] +ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_ISSUE_MAIL, CONF_ML, CONF_TWITTER] SENSOR_TYPES = [CONF_HUMIDITY, CONF_TEMPERATURE] -SPACEAPI_VERSION = 0.13 +SPACEAPI_VERSION = "0.13" URL_API_SPACEAPI = "/api/spaceapi" -LOCATION_SCHEMA = vol.Schema({vol.Optional(CONF_ADDRESS): cv.string}, required=True) +LOCATION_SCHEMA = vol.Schema({vol.Optional(CONF_ADDRESS): cv.string}) + +SPACEFED_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SPACENET): cv.boolean, + vol.Optional(CONF_SPACESAML): cv.boolean, + vol.Optional(CONF_SPACEPHONE): cv.boolean, + } +) + +STREAM_SCHEMA = vol.Schema( + { + vol.Optional(CONF_M4): cv.url, + vol.Optional(CONF_MJPEG): cv.url, + vol.Optional(CONF_USTREAM): cv.url, + } +) + +FEED_SCHEMA = vol.Schema( + {vol.Optional(CONF_FEED_TYPE): cv.string, vol.Required(CONF_FEED_URL): cv.url} +) + +FEEDS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_FEED_BLOG): FEED_SCHEMA, + vol.Optional(CONF_FEED_WIKI): FEED_SCHEMA, + vol.Optional(CONF_FEED_CALENDAR): FEED_SCHEMA, + vol.Optional(CONF_FEED_FLICKER): FEED_SCHEMA, + } +) + +CACHE_SCHEMA = vol.Schema( + { + vol.Required(CONF_CACHE_SCHEDULE): cv.matches_regex( + r"(m.02|m.05|m.10|m.15|m.30|h.01|h.02|h.04|h.08|h.12|d.01)" + ) + } +) + +RADIO_SHOW_SCHEMA = vol.Schema( + { + vol.Required(CONF_RADIO_SHOW_NAME): cv.string, + vol.Required(CONF_RADIO_SHOW_URL): cv.url, + vol.Required(CONF_RADIO_SHOW_TYPE): cv.matches_regex(r"(mp3|ogg)"), + vol.Required(CONF_RADIO_SHOW_START): cv.string, + vol.Required(CONF_RADIO_SHOW_END): cv.string, + } +) + +KEYMASTER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_KEYMASTER_NAME): cv.string, + vol.Optional(CONF_KEYMASTER_IRC_NICK): cv.string, + vol.Optional(CONF_KEYMASTER_PHONE): cv.string, + vol.Optional(CONF_KEYMASTER_EMAIL): cv.string, + vol.Optional(CONF_KEYMASTER_TWITTER): cv.string, + } +) CONTACT_SCHEMA = vol.Schema( { vol.Optional(CONF_EMAIL): cv.string, vol.Optional(CONF_IRC): cv.string, - vol.Optional(CONF_MAILING_LIST): cv.string, + vol.Optional(CONF_ML): cv.string, vol.Optional(CONF_PHONE): cv.string, vol.Optional(CONF_TWITTER): cv.string, + vol.Optional(CONF_SIP): cv.string, + vol.Optional(CONF_FACEBOOK): cv.string, + vol.Optional(CONF_IDENTICA): cv.string, + vol.Optional(CONF_FOURSQUARE): cv.string, + vol.Optional(CONF_JABBER): cv.string, + vol.Optional(CONF_ISSUE_MAIL): cv.string, + vol.Optional(CONF_KEYMASTERS): vol.All( + cv.ensure_list, [KEYMASTER_SCHEMA], vol.Length(min=1) + ), }, required=False, ) @@ -100,12 +210,23 @@ vol.Required(CONF_ISSUE_REPORT_CHANNELS): vol.All( cv.ensure_list, [vol.In(ISSUE_REPORT_CHANNELS)] ), - vol.Required(CONF_LOCATION): LOCATION_SCHEMA, + vol.Optional(CONF_LOCATION): LOCATION_SCHEMA, vol.Required(CONF_LOGO): cv.url, vol.Required(CONF_SPACE): cv.string, vol.Required(CONF_STATE): STATE_SCHEMA, vol.Required(CONF_URL): cv.string, vol.Optional(CONF_SENSORS): SENSOR_SCHEMA, + vol.Optional(CONF_SPACEFED): SPACEFED_SCHEMA, + vol.Optional(CONF_CAM): vol.All( + cv.ensure_list, [cv.url], vol.Length(min=1) + ), + vol.Optional(CONF_STREAM): STREAM_SCHEMA, + vol.Optional(CONF_FEEDS): FEEDS_SCHEMA, + vol.Optional(CONF_CACHE): CACHE_SCHEMA, + vol.Optional(CONF_PROJECTS): vol.All(cv.ensure_list, [cv.url]), + vol.Optional(CONF_RADIO_SHOW): vol.All( + cv.ensure_list, [RADIO_SHOW_SCHEMA] + ), } ) }, @@ -150,11 +271,14 @@ def get(self, request): spaceapi = dict(hass.data[DATA_SPACEAPI]) is_sensors = spaceapi.get("sensors") - location = { - ATTR_ADDRESS: spaceapi[ATTR_LOCATION][CONF_ADDRESS], - ATTR_LATITUDE: hass.config.latitude, - ATTR_LONGITUDE: hass.config.longitude, - } + location = {ATTR_LAT: hass.config.latitude, ATTR_LON: hass.config.longitude} + + try: + location[ATTR_ADDRESS] = spaceapi[ATTR_LOCATION][CONF_ADDRESS] + except KeyError: + pass + except TypeError: + pass state_entity = spaceapi["state"][ATTR_ENTITY_ID] space_state = hass.states.get(state_entity) @@ -186,6 +310,41 @@ def get(self, request): ATTR_URL: spaceapi[CONF_URL], } + try: + data[ATTR_CAM] = spaceapi[CONF_CAM] + except KeyError: + pass + + try: + data[ATTR_SPACEFED] = spaceapi[CONF_SPACEFED] + except KeyError: + pass + + try: + data[ATTR_STREAM] = spaceapi[CONF_STREAM] + except KeyError: + pass + + try: + data[ATTR_FEEDS] = spaceapi[CONF_FEEDS] + except KeyError: + pass + + try: + data[ATTR_CACHE] = spaceapi[CONF_CACHE] + except KeyError: + pass + + try: + data[ATTR_PROJECTS] = spaceapi[CONF_PROJECTS] + except KeyError: + pass + + try: + data[ATTR_RADIO_SHOW] = spaceapi[CONF_RADIO_SHOW] + except KeyError: + pass + if is_sensors is not None: sensors = {} for sensor_type in is_sensors: diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index 02a6eccc285804..58c417831a966f 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -25,6 +25,34 @@ "temperature": ["test.temp1", "test.temp2"], "humidity": ["test.hum1"], }, + "spacefed": {"spacenet": True, "spacesaml": False, "spacephone": True}, + "cam": ["https://home-assistant.io/cam1", "https://home-assistant.io/cam2"], + "stream": { + "m4": "https://home-assistant.io/m4", + "mjpeg": "https://home-assistant.io/mjpeg", + "ustream": "https://home-assistant.io/ustream", + }, + "feeds": { + "blog": {"url": "https://home-assistant.io/blog"}, + "wiki": {"type": "mediawiki", "url": "https://home-assistant.io/wiki"}, + "calendar": {"type": "ical", "url": "https://home-assistant.io/calendar"}, + "flicker": {"url": "https://www.flickr.com/photos/home-assistant"}, + }, + "cache": {"schedule": "m.02"}, + "projects": [ + "https://home-assistant.io/projects/1", + "https://home-assistant.io/projects/2", + "https://home-assistant.io/projects/3", + ], + "radio_show": [ + { + "name": "Radioshow", + "url": "https://home-assistant.io/radio", + "type": "ogg", + "start": "2019-09-02T10:00Z", + "end": "2019-09-02T12:00Z", + } + ], } } @@ -61,11 +89,37 @@ async def test_spaceapi_get(hass, mock_client): assert data["space"] == "Home" assert data["contact"]["email"] == "hello@home-assistant.io" assert data["location"]["address"] == "In your Home" - assert data["location"]["latitude"] == 32.87336 - assert data["location"]["longitude"] == -117.22743 + assert data["location"]["lat"] == 32.87336 + assert data["location"]["lon"] == -117.22743 assert data["state"]["open"] == "null" assert data["state"]["icon"]["open"] == "https://home-assistant.io/open.png" assert data["state"]["icon"]["close"] == "https://home-assistant.io/close.png" + assert data["spacefed"]["spacenet"] == bool(1) + assert data["spacefed"]["spacesaml"] == bool(0) + assert data["spacefed"]["spacephone"] == bool(1) + assert data["cam"][0] == "https://home-assistant.io/cam1" + assert data["cam"][1] == "https://home-assistant.io/cam2" + assert data["stream"]["m4"] == "https://home-assistant.io/m4" + assert data["stream"]["mjpeg"] == "https://home-assistant.io/mjpeg" + assert data["stream"]["ustream"] == "https://home-assistant.io/ustream" + assert data["feeds"]["blog"]["url"] == "https://home-assistant.io/blog" + assert data["feeds"]["wiki"]["type"] == "mediawiki" + assert data["feeds"]["wiki"]["url"] == "https://home-assistant.io/wiki" + assert data["feeds"]["calendar"]["type"] == "ical" + assert data["feeds"]["calendar"]["url"] == "https://home-assistant.io/calendar" + assert ( + data["feeds"]["flicker"]["url"] + == "https://www.flickr.com/photos/home-assistant" + ) + assert data["cache"]["schedule"] == "m.02" + assert data["projects"][0] == "https://home-assistant.io/projects/1" + assert data["projects"][1] == "https://home-assistant.io/projects/2" + assert data["projects"][2] == "https://home-assistant.io/projects/3" + assert data["radio_show"][0]["name"] == "Radioshow" + assert data["radio_show"][0]["url"] == "https://home-assistant.io/radio" + assert data["radio_show"][0]["type"] == "ogg" + assert data["radio_show"][0]["start"] == "2019-09-02T10:00Z" + assert data["radio_show"][0]["end"] == "2019-09-02T12:00Z" async def test_spaceapi_state_get(hass, mock_client): From 544cdae67c3acce9105b2d334fa924beedb52b80 Mon Sep 17 00:00:00 2001 From: CQoute Date: Sun, 22 Sep 2019 08:59:52 +0930 Subject: [PATCH 0394/3953] Update light.py (#26703) Fix for esphome lights to use the flash feature --- homeassistant/components/esphome/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index e455d5581d1e3d..1205521706eb21 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -74,7 +74,7 @@ async def async_turn_on(self, **kwargs) -> None: red, green, blue = color_util.color_hsv_to_RGB(hue, sat, 100) data["rgb"] = (red / 255, green / 255, blue / 255) if ATTR_FLASH in kwargs: - data["flash"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] + data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] if ATTR_TRANSITION in kwargs: data["transition_length"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs: From 6135b862ba697b2f4967982234423ca3354d3adb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 07:10:21 +0200 Subject: [PATCH 0395/3953] Bump hbmqtt to 0.9.5 (#26804) --- homeassistant/components/mqtt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index d63d1707fac2eb..2df50699a9d8d3 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/mqtt", "requirements": [ - "hbmqtt==0.9.4", + "hbmqtt==0.9.5", "paho-mqtt==1.4.0" ], "dependencies": [ diff --git a/requirements_all.txt b/requirements_all.txt index a17c201a9528d6..b4ea5b509d34d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -612,7 +612,7 @@ hangups==0.4.9 hass-nabucasa==0.17 # homeassistant.components.mqtt -hbmqtt==0.9.4 +hbmqtt==0.9.5 # homeassistant.components.jewish_calendar hdate==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a8aa92c81ddbbc..214f2ee30ed1d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -166,7 +166,7 @@ hangups==0.4.9 hass-nabucasa==0.17 # homeassistant.components.mqtt -hbmqtt==0.9.4 +hbmqtt==0.9.5 # homeassistant.components.jewish_calendar hdate==0.9.0 From ef0dd689fab68ca16fb9edded37353f39470e20e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 07:10:34 +0200 Subject: [PATCH 0396/3953] Bump python-slugify to 3.0.4 (#26801) --- 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 32804d790411f2..842cf4840c832a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 -python-slugify==3.0.3 +python-slugify==3.0.4 pytz>=2019.02 pyyaml==5.1.2 requests==2.22.0 diff --git a/requirements_all.txt b/requirements_all.txt index b4ea5b509d34d0..42082b76ae0604 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.3 +python-slugify==3.0.4 pytz>=2019.02 pyyaml==5.1.2 requests==2.22.0 diff --git a/setup.py b/setup.py index 87704a3c6a96b4..26f112bb008207 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ # PyJWT has loose dependency. We want the latest one. "cryptography==2.7", "pip>=8.0.3", - "python-slugify==3.0.3", + "python-slugify==3.0.4", "pytz>=2019.02", "pyyaml==5.1.2", "requests==2.22.0", From 6bdfab1124d9d2494069e9df6d24be980db07f56 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 09:56:43 +0200 Subject: [PATCH 0397/3953] Bump pytest to 5.1.3 (#26794) --- 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 44b27d8e13e9c5..e697164a35a24b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -17,5 +17,5 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.2 +pytest==5.1.3 requests_mock==1.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 214f2ee30ed1d4..e3df0099032178 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -18,7 +18,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.1.2 +pytest==5.1.3 requests_mock==1.6.0 From a5ebf9f38d492e9b226bf2df40df2c70caf2adeb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 09:57:02 +0200 Subject: [PATCH 0398/3953] Bump iperf3 to 0.1.11 (#26795) --- homeassistant/components/iperf3/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index e35be24fc8089f..0547628b4bfd2c 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -3,7 +3,7 @@ "name": "Iperf3", "documentation": "https://www.home-assistant.io/components/iperf3", "requirements": [ - "iperf3==0.1.10" + "iperf3==0.1.11" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 42082b76ae0604..1e2c466a754ac5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -693,7 +693,7 @@ influxdb==5.2.3 insteonplm==0.16.5 # homeassistant.components.iperf3 -iperf3==0.1.10 +iperf3==0.1.11 # homeassistant.components.route53 ipify==1.0.0 From 48369ad08ff996fd6821ca527f4da188e6755ac6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 09:57:11 +0200 Subject: [PATCH 0399/3953] Bump shodan to 1.17.0 (#26797) * Bump shodan to 1.16.0 * Bump shodan to 1.17.0 --- 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 7ecc298e3f6f17..be7f0a524dcd0b 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.15.0" + "shodan==1.17.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 1e2c466a754ac5..4baf0de152ed5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1732,7 +1732,7 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.15.0 +shodan==1.17.0 # homeassistant.components.simplepush simplepush==1.1.4 From 4f7a4b93da91d2792a8ac8600b23571f06dba15b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 10:02:23 +0200 Subject: [PATCH 0400/3953] Bump request_mock to 1.7.0 (#26799) --- 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 e697164a35a24b..b9b919c4bfd077 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,4 +18,4 @@ pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==5.1.3 -requests_mock==1.6.0 +requests_mock==1.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3df0099032178..6b4d7fbc089a0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==5.1.3 -requests_mock==1.6.0 +requests_mock==1.7.0 # homeassistant.components.homekit From 04cae0818dae9a22d76deed483b73af8c85a3403 Mon Sep 17 00:00:00 2001 From: Dima Zavin Date: Sun, 22 Sep 2019 02:42:00 -0700 Subject: [PATCH 0401/3953] Bump pylutron to 0.2.5 (#26815) --- homeassistant/components/lutron/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index bece55ae09d819..451a6f3e33d822 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.2" + "pylutron==0.2.5" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 4baf0de152ed5c..08ed0d4476f143 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1288,7 +1288,7 @@ pyloopenergy==0.1.3 pylutron-caseta==0.5.0 # homeassistant.components.lutron -pylutron==0.2.2 +pylutron==0.2.5 # homeassistant.components.mailgun pymailgunner==1.4 From 5624e3efd47046a8fe773d7def0caf439fd04003 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 22 Sep 2019 11:43:41 +0200 Subject: [PATCH 0402/3953] Upgrade sendgrid to 6.1.0 (#26809) * Upgrade sendgrid to 6.1.0 * Move import (could to be a Black issue) --- homeassistant/components/sendgrid/manifest.json | 2 +- homeassistant/components/sendgrid/notify.py | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index eb006f408bcbd8..1ffbe69888f329 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -3,7 +3,7 @@ "name": "Sendgrid", "documentation": "https://www.home-assistant.io/components/sendgrid", "requirements": [ - "sendgrid==6.0.5" + "sendgrid==6.1.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index ac334587b89035..f16758a53559c2 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -3,6 +3,8 @@ import voluptuous as vol +from sendgrid import SendGridAPIClient + from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -45,8 +47,6 @@ class SendgridNotificationService(BaseNotificationService): def __init__(self, config): """Initialize the service.""" - from sendgrid import SendGridAPIClient - self.api_key = config[CONF_API_KEY] self.sender = config[CONF_SENDER] self.sender_name = config[CONF_SENDER_NAME] diff --git a/requirements_all.txt b/requirements_all.txt index 08ed0d4476f143..ad7ba89e4ca618 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1720,7 +1720,7 @@ schiene==0.23 scsgate==0.1.0 # homeassistant.components.sendgrid -sendgrid==6.0.5 +sendgrid==6.1.0 # homeassistant.components.sensehat sense-hat==2.2.0 From 14647f539138b88751d56a38e7eb813fae5b9fb6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Sep 2019 17:31:01 +0200 Subject: [PATCH 0403/3953] Exempt 'Help wanted' issue from stale bot (#26829) --- .github/stale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/stale.yml b/.github/stale.yml index a1a35e9f3b1ada..44cd95e1f5d710 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -13,6 +13,7 @@ onlyLabels: [] # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - under investigation + - Help wanted # Set to true to ignore issues in a project (defaults to false) exemptProjects: true From e5f6f33340a4ef13e5b2de9b94c791a18ce7d5a9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 22 Sep 2019 20:13:17 +0200 Subject: [PATCH 0404/3953] Add device automation support to binary_sensor entities (#26643) * Add device automation support to binary_sensor entities * turn_on -> turned_on * Correct spelling of present * Improve tests * Fix strings * Fix stale comment --- .../binary_sensor/device_automation.py | 423 ++++++++++++++++++ .../components/binary_sensor/strings.json | 93 ++++ .../binary_sensor/test_device_automation.py | 309 +++++++++++++ .../custom_components/test/binary_sensor.py | 50 +++ 4 files changed, 875 insertions(+) create mode 100644 homeassistant/components/binary_sensor/device_automation.py create mode 100644 homeassistant/components/binary_sensor/strings.json create mode 100644 tests/components/binary_sensor/test_device_automation.py create mode 100644 tests/testing_config/custom_components/test/binary_sensor.py diff --git a/homeassistant/components/binary_sensor/device_automation.py b/homeassistant/components/binary_sensor/device_automation.py new file mode 100644 index 00000000000000..c609c2eb5da4c8 --- /dev/null +++ b/homeassistant/components/binary_sensor/device_automation.py @@ -0,0 +1,423 @@ +"""Provides device automations for lights.""" +import voluptuous as vol + +import homeassistant.components.automation.state as state +from homeassistant.components.device_automation.const import ( + CONF_IS_OFF, + CONF_IS_ON, + CONF_TURNED_OFF, + CONF_TURNED_ON, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import split_entity_id +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import condition, config_validation as cv + +from . import ( + DOMAIN, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +) + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_IS_BAT_LOW = "is_bat_low" +CONF_IS_NOT_BAT_LOW = "is_not_bat_low" +CONF_IS_COLD = "is_cold" +CONF_IS_NOT_COLD = "is_not_cold" +CONF_IS_CONNECTED = "is_connected" +CONF_IS_NOT_CONNECTED = "is_not_connected" +CONF_IS_GAS = "is_gas" +CONF_IS_NO_GAS = "is_no_gas" +CONF_IS_HOT = "is_hot" +CONF_IS_NOT_HOT = "is_not_hot" +CONF_IS_LIGHT = "is_light" +CONF_IS_NO_LIGHT = "is_no_light" +CONF_IS_LOCKED = "is_locked" +CONF_IS_NOT_LOCKED = "is_not_locked" +CONF_IS_MOIST = "is_moist" +CONF_IS_NOT_MOIST = "is_not_moist" +CONF_IS_MOTION = "is_motion" +CONF_IS_NO_MOTION = "is_no_motion" +CONF_IS_MOVING = "is_moving" +CONF_IS_NOT_MOVING = "is_not_moving" +CONF_IS_OCCUPIED = "is_occupied" +CONF_IS_NOT_OCCUPIED = "is_not_occupied" +CONF_IS_PLUGGED_IN = "is_plugged_in" +CONF_IS_NOT_PLUGGED_IN = "is_not_plugged_in" +CONF_IS_POWERED = "is_powered" +CONF_IS_NOT_POWERED = "is_not_powered" +CONF_IS_PRESENT = "is_present" +CONF_IS_NOT_PRESENT = "is_not_present" +CONF_IS_PROBLEM = "is_problem" +CONF_IS_NO_PROBLEM = "is_no_problem" +CONF_IS_UNSAFE = "is_unsafe" +CONF_IS_NOT_UNSAFE = "is_not_unsafe" +CONF_IS_SMOKE = "is_smoke" +CONF_IS_NO_SMOKE = "is_no_smoke" +CONF_IS_SOUND = "is_sound" +CONF_IS_NO_SOUND = "is_no_sound" +CONF_IS_VIBRATION = "is_vibration" +CONF_IS_NO_VIBRATION = "is_no_vibration" +CONF_IS_OPEN = "is_open" +CONF_IS_NOT_OPEN = "is_not_open" + +CONF_BAT_LOW = "bat_low" +CONF_NOT_BAT_LOW = "not_bat_low" +CONF_COLD = "cold" +CONF_NOT_COLD = "not_cold" +CONF_CONNECTED = "connected" +CONF_NOT_CONNECTED = "not_connected" +CONF_GAS = "gas" +CONF_NO_GAS = "no_gas" +CONF_HOT = "hot" +CONF_NOT_HOT = "not_hot" +CONF_LIGHT = "light" +CONF_NO_LIGHT = "no_light" +CONF_LOCKED = "locked" +CONF_NOT_LOCKED = "not_locked" +CONF_MOIST = "moist" +CONF_NOT_MOIST = "not_moist" +CONF_MOTION = "motion" +CONF_NO_MOTION = "no_motion" +CONF_MOVING = "moving" +CONF_NOT_MOVING = "not_moving" +CONF_OCCUPIED = "occupied" +CONF_NOT_OCCUPIED = "not_occupied" +CONF_PLUGGED_IN = "plugged_in" +CONF_NOT_PLUGGED_IN = "not_plugged_in" +CONF_POWERED = "powered" +CONF_NOT_POWERED = "not_powered" +CONF_PRESENT = "present" +CONF_NOT_PRESENT = "not_present" +CONF_PROBLEM = "problem" +CONF_NO_PROBLEM = "no_problem" +CONF_UNSAFE = "unsafe" +CONF_NOT_UNSAFE = "not_unsafe" +CONF_SMOKE = "smoke" +CONF_NO_SMOKE = "no_smoke" +CONF_SOUND = "sound" +CONF_NO_SOUND = "no_sound" +CONF_VIBRATION = "vibration" +CONF_NO_VIBRATION = "no_vibration" +CONF_OPEN = "open" +CONF_NOT_OPEN = "not_open" + +IS_ON = [ + CONF_IS_BAT_LOW, + CONF_IS_COLD, + CONF_IS_CONNECTED, + CONF_IS_GAS, + CONF_IS_HOT, + CONF_IS_LIGHT, + CONF_IS_LOCKED, + CONF_IS_MOIST, + CONF_IS_MOTION, + CONF_IS_MOVING, + CONF_IS_OCCUPIED, + CONF_IS_OPEN, + CONF_IS_PLUGGED_IN, + CONF_IS_POWERED, + CONF_IS_PRESENT, + CONF_IS_PROBLEM, + CONF_IS_SMOKE, + CONF_IS_SOUND, + CONF_IS_UNSAFE, + CONF_IS_VIBRATION, + CONF_IS_ON, +] + +IS_OFF = [ + CONF_IS_NOT_BAT_LOW, + CONF_IS_NOT_COLD, + CONF_IS_NOT_CONNECTED, + CONF_IS_NOT_HOT, + CONF_IS_NOT_LOCKED, + CONF_IS_NOT_MOIST, + CONF_IS_NOT_MOVING, + CONF_IS_NOT_OCCUPIED, + CONF_IS_NOT_OPEN, + CONF_IS_NOT_PLUGGED_IN, + CONF_IS_NOT_POWERED, + CONF_IS_NOT_PRESENT, + CONF_IS_NOT_UNSAFE, + CONF_IS_NO_GAS, + CONF_IS_NO_LIGHT, + CONF_IS_NO_MOTION, + CONF_IS_NO_PROBLEM, + CONF_IS_NO_SMOKE, + CONF_IS_NO_SOUND, + CONF_IS_NO_VIBRATION, + CONF_IS_OFF, +] + +TURNED_ON = [ + CONF_BAT_LOW, + CONF_COLD, + CONF_CONNECTED, + CONF_GAS, + CONF_HOT, + CONF_LIGHT, + CONF_LOCKED, + CONF_MOIST, + CONF_MOTION, + CONF_MOVING, + CONF_OCCUPIED, + CONF_OPEN, + CONF_PLUGGED_IN, + CONF_POWERED, + CONF_PRESENT, + CONF_PROBLEM, + CONF_SMOKE, + CONF_SOUND, + CONF_UNSAFE, + CONF_VIBRATION, + CONF_TURNED_ON, +] + +TURNED_OFF = [ + CONF_NOT_BAT_LOW, + CONF_NOT_COLD, + CONF_NOT_CONNECTED, + CONF_NOT_HOT, + CONF_NOT_LOCKED, + CONF_NOT_MOIST, + CONF_NOT_MOVING, + CONF_NOT_OCCUPIED, + CONF_NOT_OPEN, + CONF_NOT_PLUGGED_IN, + CONF_NOT_POWERED, + CONF_NOT_PRESENT, + CONF_NOT_UNSAFE, + CONF_NO_GAS, + CONF_NO_LIGHT, + CONF_NO_MOTION, + CONF_NO_PROBLEM, + CONF_NO_SMOKE, + CONF_NO_SOUND, + CONF_NO_VIBRATION, + CONF_TURNED_OFF, +] + +ENTITY_CONDITIONS = { + DEVICE_CLASS_BATTERY: [ + {CONF_TYPE: CONF_IS_BAT_LOW}, + {CONF_TYPE: CONF_IS_NOT_BAT_LOW}, + ], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_IS_COLD}, {CONF_TYPE: CONF_IS_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_IS_CONNECTED}, + {CONF_TYPE: CONF_IS_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [ + {CONF_TYPE: CONF_IS_OPEN}, + {CONF_TYPE: CONF_IS_NOT_OPEN}, + ], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}, {CONF_TYPE: CONF_IS_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_IS_HOT}, {CONF_TYPE: CONF_IS_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_IS_LIGHT}, {CONF_TYPE: CONF_IS_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_IS_LOCKED}, {CONF_TYPE: CONF_IS_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_IS_MOIST}, {CONF_TYPE: CONF_IS_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_IS_MOTION}, {CONF_TYPE: CONF_IS_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_IS_MOVING}, {CONF_TYPE: CONF_IS_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_IS_OCCUPIED}, + {CONF_TYPE: CONF_IS_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_PLUG: [ + {CONF_TYPE: CONF_IS_PLUGGED_IN}, + {CONF_TYPE: CONF_IS_NOT_PLUGGED_IN}, + ], + DEVICE_CLASS_POWER: [ + {CONF_TYPE: CONF_IS_POWERED}, + {CONF_TYPE: CONF_IS_NOT_POWERED}, + ], + DEVICE_CLASS_PRESENCE: [ + {CONF_TYPE: CONF_IS_PRESENT}, + {CONF_TYPE: CONF_IS_NOT_PRESENT}, + ], + DEVICE_CLASS_PROBLEM: [ + {CONF_TYPE: CONF_IS_PROBLEM}, + {CONF_TYPE: CONF_IS_NO_PROBLEM}, + ], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_IS_VIBRATION}, + {CONF_TYPE: CONF_IS_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_ON}, {CONF_TYPE: CONF_IS_OFF}], +} + +ENTITY_TRIGGERS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BAT_LOW}, {CONF_TYPE: CONF_NOT_BAT_LOW}], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_COLD}, {CONF_TYPE: CONF_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_CONNECTED}, + {CONF_TYPE: CONF_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_MOIST}, {CONF_TYPE: CONF_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_MOTION}, {CONF_TYPE: CONF_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_MOVING}, {CONF_TYPE: CONF_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_OCCUPIED}, + {CONF_TYPE: CONF_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], + DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], + DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_VIBRATION}, + {CONF_TYPE: CONF_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], +} + +CONDITION_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONDITION): "device", + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), + } +) + +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "device", + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), + } +) + + +def async_condition_from_config(config, config_validation): + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + condition_type = config[CONF_TYPE] + if condition_type in IS_ON: + stat = "on" + else: + stat = "off" + state_config = { + condition.CONF_CONDITION: "state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + condition.CONF_STATE: stat, + } + + return condition.state_from_config(state_config, config_validation) + + +async def async_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + trigger_type = config[CONF_TYPE] + if trigger_type in TURNED_ON: + from_state = "off" + to_state = "on" + else: + from_state = "on" + to_state = "off" + state_config = { + state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + + return await state.async_trigger(hass, state_config, action, automation_info) + + +def _is_domain(entity, domain): + return split_entity_id(entity.entity_id)[0] == domain + + +async def _async_get_automations(hass, device_id, automation_templates, domain): + """List device automations.""" + automations = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entities = async_entries_for_device(entity_registry, device_id) + domain_entities = [x for x in entities if _is_domain(x, domain)] + for entity in domain_entities: + device_class = DEVICE_CLASS_NONE + entity_id = entity.entity_id + entity = hass.states.get(entity_id) + if entity and ATTR_DEVICE_CLASS in entity.attributes: + device_class = entity.attributes[ATTR_DEVICE_CLASS] + automation_template = automation_templates[device_class] + + for automation in automation_template: + automation = dict(automation) + automation.update(device_id=device_id, entity_id=entity_id, domain=domain) + automations.append(automation) + + return automations + + +async def async_get_conditions(hass, device_id): + """List device conditions.""" + automations = await _async_get_automations( + hass, device_id, ENTITY_CONDITIONS, DOMAIN + ) + for automation in automations: + automation.update(condition="device") + return automations + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + automations = await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, DOMAIN) + for automation in automations: + automation.update(platform="device") + return automations diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json new file mode 100644 index 00000000000000..109a2b1fd45f61 --- /dev/null +++ b/homeassistant/components/binary_sensor/strings.json @@ -0,0 +1,93 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} battery is low", + "is_not_bat_low": "{entity_name} battery is normal", + "is_cold": "{entity_name} is cold", + "is_not_cold": "{entity_name} is not cold", + "is_connected": "{entity_name} is connected", + "is_not_connected": "{entity_name} is disconnected", + "is_gas": "{entity_name} is detecting gas", + "is_no_gas": "{entity_name} is not detecting gas", + "is_hot": "{entity_name} is hot", + "is_not_hot": "{entity_name} is not hot", + "is_light": "{entity_name} is detecting light", + "is_no_light": "{entity_name} is not detecting light", + "is_locked": "{entity_name} is locked", + "is_not_locked": "{entity_name} is unlocked", + "is_moist": "{entity_name} is moist", + "is_not_moist": "{entity_name} is dry", + "is_motion": "{entity_name} is detecting motion", + "is_no_motion": "{entity_name} is not detecting motion", + "is_moving": "{entity_name} is moving", + "is_not_moving": "{entity_name} is not moving", + "is_occupied": "{entity_name} is occupied", + "is_not_occupied": "{entity_name} is not occupied", + "is_plugged_in": "{entity_name} is plugged in", + "is_not_plugged_in": "{entity_name} is unplugged", + "is_powered": "{entity_name} is powered", + "is_not_powered": "{entity_name} is not powered", + "is_present": "{entity_name} is present", + "is_not_present": "{entity_name} is not present", + "is_problem": "{entity_name} is detecting problem", + "is_no_problem": "{entity_name} is not detecting problem", + "is_unsafe": "{entity_name} is unsafe", + "is_not_unsafe": "{entity_name} is safe", + "is_smoke": "{entity_name} is detecting smoke", + "is_no_smoke": "{entity_name} is not detecting smoke", + "is_sound": "{entity_name} is detecting sound", + "is_no_sound": "{entity_name} is not detecting sound", + "is_vibration": "{entity_name} is detecting vibration", + "is_no_vibration": "{entity_name} is not detecting vibration", + "is_open": "{entity_name} is open", + "is_not_open": "{entity_name} is closed", + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" + }, + "trigger_type": { + "bat_low": "{entity_name} battery low", + "not_bat_low": "{entity_name} battery normal", + "cold": "{entity_name} became cold", + "not_cold": "{entity_name} became not cold", + "connected": "{entity_name} connected", + "not_connected": "{entity_name} disconnected", + "gas": "{entity_name} started detecting gas", + "no_gas": "{entity_name} stopped detecting gas", + "hot": "{entity_name} became hot", + "not_hot": "{entity_name} became not hot", + "light": "{entity_name} started detecting light", + "no_light": "{entity_name} stopped detecting light", + "locked": "{entity_name} locked", + "not_locked": "{entity_name} unlocked", + "moist§": "{entity_name} became moist", + "not_moist": "{entity_name} became dry", + "motion": "{entity_name} started detecting motion", + "no_motion": "{entity_name} stopped detecting motion", + "moving": "{entity_name} started moving", + "not_moving": "{entity_name} stopped moving", + "occupied": "{entity_name} became occupied", + "not_occupied": "{entity_name} became not occupied", + "plugged_in": "{entity_name} plugged in", + "not_plugged_in": "{entity_name} unplugged", + "powered": "{entity_name} powered", + "not_powered": "{entity_name} not powered", + "present": "{entity_name} present", + "not_present": "{entity_name} not present", + "problem": "{entity_name} started detecting problem", + "no_problem": "{entity_name} stopped detecting problem", + "unsafe": "{entity_name} became unsafe", + "not_unsafe": "{entity_name} became safe", + "smoke": "{entity_name} started detecting smoke", + "no_smoke": "{entity_name} stopped detecting smoke", + "sound": "{entity_name} started detecting sound", + "no_sound": "{entity_name} stopped detecting sound", + "vibration": "{entity_name} started detecting vibration", + "no_vibration": "{entity_name} stopped detecting vibration", + "opened": "{entity_name} opened", + "closed": "{entity_name} closed", + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" + + } + } +} diff --git a/tests/components/binary_sensor/test_device_automation.py b/tests/components/binary_sensor/test_device_automation.py new file mode 100644 index 00000000000000..91124d47f4e4ef --- /dev/null +++ b/tests/components/binary_sensor/test_device_automation.py @@ -0,0 +1,309 @@ +"""The test for binary_sensor device automation.""" +import pytest + +from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.binary_sensor.device_automation import ( + ENTITY_CONDITIONS, + ENTITY_TRIGGERS, +) +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +def _same_lists(a, b): + if len(a) != len(b): + return False + + for d in a: + if d not in b: + return False + return True + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["battery"].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_actions = [] + actions = await async_get_device_automations( + hass, "async_get_actions", device_entry.id + ) + assert _same_lists(actions, expected_actions) + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": condition["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for condition in ENTITY_CONDITIONS[device_class] + ] + conditions = await async_get_device_automations( + hass, "async_get_conditions", device_entry.id + ) + assert _same_lists(conditions, expected_conditions) + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": trigger["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for trigger in ENTITY_TRIGGERS[device_class] + ] + triggers = await async_get_device_automations( + hass, "async_get_triggers", device_entry.id + ) + assert _same_lists(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for on and off triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "bat_low", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "not_bat_low", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "not_bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "not_bat_low state - {} - on - off - None".format( + sensor1.entity_id + ) + + hass.states.async_set(sensor1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "bat_low state - {} - off - on - None".format( + sensor1.entity_id + ) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_bat_low", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_not_bat_low", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/testing_config/custom_components/test/binary_sensor.py b/tests/testing_config/custom_components/test/binary_sensor.py new file mode 100644 index 00000000000000..5052b8e47f10b2 --- /dev/null +++ b/tests/testing_config/custom_components/test/binary_sensor.py @@ -0,0 +1,50 @@ +""" +Provide a mock binary sensor platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.binary_sensor import BinarySensorDevice, DEVICE_CLASSES +from tests.common import MockEntity + + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + device_class: MockBinarySensor( + name=f"{device_class} sensor", + is_on=True, + unique_id=f"unique_{device_class}", + device_class=device_class, + ) + for device_class in DEVICE_CLASSES + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockBinarySensor(MockEntity, BinarySensorDevice): + """Mock Binary Sensor class.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._handle("is_on") + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._handle("device_class") From 2d906b111a932fe74ac30fd79d0633fc7ab8d5eb Mon Sep 17 00:00:00 2001 From: Kevin McCormack Date: Sun, 22 Sep 2019 13:06:02 -0700 Subject: [PATCH 0405/3953] Update Vivotek camera component (#26754) * Update Vivotek camera component Load model name to instance attribute * Update Vivotek camera component Ensure `update` is called when the entity is added. * Update libpyvivotek to 0.2.2 https://pypi.org/project/libpyvivotek/0.2.2/ - Retrieve camera model name anonymously - Raise a more helpful error for invalid creds --- homeassistant/components/vivotek/camera.py | 9 +++++++-- homeassistant/components/vivotek/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index bf136731cb6a51..012c1e1df34755 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -55,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config[CONF_IP_ADDRESS], ), ) - add_entities([VivotekCam(**args)]) + add_entities([VivotekCam(**args)], True) class VivotekCam(Camera): @@ -68,6 +68,7 @@ def __init__(self, config, cam, stream_source): self._cam = cam self._frame_interval = 1 / config[CONF_FRAMERATE] self._motion_detection_enabled = False + self._model_name = None self._name = config[CONF_NAME] self._stream_source = stream_source @@ -117,4 +118,8 @@ def brand(self): @property def model(self): """Return the camera model.""" - return self._cam.model_name + return self._model_name + + def update(self): + """Update entity status.""" + self._model_name = self._cam.model_name diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index 8a6a37762d4fea..cce2307bc4b554 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -3,7 +3,7 @@ "name": "Vivotek", "documentation": "https://www.home-assistant.io/components/vivotek", "requirements": [ - "libpyvivotek==0.2.1" + "libpyvivotek==0.2.2" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index ad7ba89e4ca618..939fcc27978c75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -732,7 +732,7 @@ libpurecool==0.5.0 libpyfoscam==1.0 # homeassistant.components.vivotek -libpyvivotek==0.2.1 +libpyvivotek==0.2.2 # homeassistant.components.mikrotik librouteros==2.3.0 From 49fef9a6a024790c885704710e18247d0ea63363 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sun, 22 Sep 2019 23:01:32 +0200 Subject: [PATCH 0406/3953] Add basic support for IKEA Fyrtur blinds (#26659) * Add basic support for IKEA Fyrtur blinds * Update coveragerc * Fix typo * Fix typos * Update following review * Fix incorrect rebase * Fix error * Update to new format of unique id * Add cover * Remove reference to cover in unique id --- .coveragerc | 1 + homeassistant/components/tradfri/__init__.py | 3 + homeassistant/components/tradfri/cover.py | 149 +++++++++++++++++++ homeassistant/components/tradfri/light.py | 11 +- homeassistant/components/tradfri/sensor.py | 8 +- homeassistant/components/tradfri/switch.py | 6 +- 6 files changed, 161 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/tradfri/cover.py diff --git a/.coveragerc b/.coveragerc index a29586c7b6e1cd..302ff9465549e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -669,6 +669,7 @@ omit = homeassistant/components/trackr/device_tracker.py homeassistant/components/tradfri/* homeassistant/components/tradfri/light.py + homeassistant/components/tradfri/cover.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/transmission/* diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 87b073db052001..bca91134bedf47 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -131,6 +131,9 @@ async def on_hass_stop(event): sw_version=gateway_info.firmware_version, ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "cover") + ) hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "light") ) diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py new file mode 100644 index 00000000000000..3dea978044fcae --- /dev/null +++ b/homeassistant/components/tradfri/cover.py @@ -0,0 +1,149 @@ +"""Support for IKEA Tradfri covers.""" +import logging + +from pytradfri.error import PytradfriError + +from homeassistant.components.cover import ( + CoverDevice, + ATTR_POSITION, + SUPPORT_OPEN, + SUPPORT_CLOSE, + SUPPORT_SET_POSITION, +) +from homeassistant.core import callback +from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY +from .const import CONF_GATEWAY_ID + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Load Tradfri covers based on a config entry.""" + gateway_id = config_entry.data[CONF_GATEWAY_ID] + api = hass.data[KEY_API][config_entry.entry_id] + gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] + + devices_commands = await api(gateway.get_devices()) + devices = await api(devices_commands) + covers = [dev for dev in devices if dev.has_blind_control] + if covers: + async_add_entities(TradfriCover(cover, api, gateway_id) for cover in covers) + + +class TradfriCover(CoverDevice): + """The platform class required by Home Assistant.""" + + def __init__(self, cover, api, gateway_id): + """Initialize a cover.""" + self._api = api + self._unique_id = f"{gateway_id}-{cover.id}" + self._cover = None + self._cover_control = None + self._cover_data = None + self._name = None + self._available = True + self._gateway_id = gateway_id + + self._refresh(cover) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + @property + def unique_id(self): + """Return unique ID for cover.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info.""" + info = self._cover.device_info + + return { + "identifiers": {(TRADFRI_DOMAIN, self._cover.id)}, + "name": self._name, + "manufacturer": info.manufacturer, + "model": info.model_number, + "sw_version": info.firmware_version, + "via_device": (TRADFRI_DOMAIN, self._gateway_id), + } + + async def async_added_to_hass(self): + """Start thread when added to hass.""" + self._async_start_observe() + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def should_poll(self): + """No polling needed for tradfri cover.""" + return False + + @property + def name(self): + """Return the display name of this cover.""" + return self._name + + @property + def current_cover_position(self): + """Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return 100 - self._cover_data.current_cover_position + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + await self._api(self._cover_control.set_state(100 - kwargs[ATTR_POSITION])) + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + await self._api(self._cover_control.set_state(0)) + + async def async_close_cover(self, **kwargs): + """Close cover.""" + await self._api(self._cover_control.set_state(100)) + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + return self.current_cover_position == 0 + + @callback + def _async_start_observe(self, exc=None): + """Start observation of cover.""" + if exc: + self._available = False + self.async_schedule_update_ha_state() + _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) + try: + cmd = self._cover.observe( + callback=self._observe_update, + err_callback=self._async_start_observe, + duration=0, + ) + self.hass.async_create_task(self._api(cmd)) + except PytradfriError as err: + _LOGGER.warning("Observation failed, trying again", exc_info=err) + self._async_start_observe() + + def _refresh(self, cover): + """Refresh the cover data.""" + self._cover = cover + + # Caching of BlindControl and cover object + self._available = cover.reachable + self._cover_control = cover.blind_control + self._cover_data = cover.blind_control.blinds[0] + self._name = cover.name + + @callback + def _observe_update(self, tradfri_device): + """Receive new state data for this cover.""" + self._refresh(tradfri_device) + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 97fdfd9d36d885..615899a98c8d22 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -1,6 +1,9 @@ """Support for IKEA Tradfri lights.""" import logging +from pytradfri.error import PytradfriError + +import homeassistant.util.color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -14,8 +17,6 @@ Light, ) from homeassistant.core import callback -import homeassistant.util.color as color_util - from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY from .const import CONF_GATEWAY_ID, CONF_IMPORT_GROUPS @@ -26,7 +27,6 @@ ATTR_SAT = "saturation" ATTR_TRANSITION_TIME = "transition_time" PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA -IKEA = "IKEA of Sweden" TRADFRI_LIGHT_MANAGER = "Tradfri Light Manager" SUPPORTED_FEATURES = SUPPORT_TRANSITION SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION @@ -113,9 +113,6 @@ async def async_turn_on(self, **kwargs): @callback def _async_start_observe(self, exc=None): """Start observation of light.""" - # pylint: disable=import-error - from pytradfri.error import PytradfriError - if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) @@ -339,8 +336,6 @@ async def async_turn_on(self, **kwargs): @callback def _async_start_observe(self, exc=None): """Start observation of light.""" - # pylint: disable=import-error - from pytradfri.error import PytradfriError if exc: self._available = False diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 627a98821549ce..4877dbbb541f1a 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,10 +1,11 @@ """Support for IKEA Tradfri sensors.""" -from datetime import timedelta import logging +from datetime import timedelta + +from pytradfri.error import PytradfriError from homeassistant.core import callback from homeassistant.helpers.entity import Entity - from . import KEY_API, KEY_GATEWAY _LOGGER = logging.getLogger(__name__) @@ -79,9 +80,6 @@ def state(self): @callback def _async_start_observe(self, exc=None): """Start observation of light.""" - # pylint: disable=import-error - from pytradfri.error import PytradfriError - if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 4be72eb7359e64..545c1ad93cec17 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,15 +1,15 @@ """Support for IKEA Tradfri switches.""" import logging +from pytradfri.error import PytradfriError + from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback - from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY from .const import CONF_GATEWAY_ID _LOGGER = logging.getLogger(__name__) -IKEA = "IKEA of Sweden" TRADFRI_SWITCH_MANAGER = "Tradfri Switch Manager" @@ -98,8 +98,6 @@ async def async_turn_on(self, **kwargs): @callback def _async_start_observe(self, exc=None): """Start observation of switch.""" - from pytradfri.error import PytradfriError - if exc: self._available = False self.async_schedule_update_ha_state() From f82f30dc6247b21a4db304140d4a603d018e09b2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 22 Sep 2019 16:47:41 -0500 Subject: [PATCH 0407/3953] Unload Plex config entries (#26771) * Unload config entries * Await coroutines * Unnecessary ensure --- homeassistant/components/plex/__init__.py | 23 ++++++++++++++++++- homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/media_player.py | 5 +++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 665091d69b9b1b..dd458dda07880a 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,4 +1,5 @@ """Support to embed Plex.""" +import asyncio import logging import plexapi.exceptions @@ -21,6 +22,7 @@ CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, CONF_SERVER, + CONF_SERVER_IDENTIFIER, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, @@ -28,6 +30,7 @@ PLATFORMS, PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, + REFRESH_LISTENERS, SERVERS, ) from .server import PlexServer @@ -61,7 +64,7 @@ def setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}}) + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}}) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: @@ -129,3 +132,21 @@ async def async_setup_entry(hass, entry): ) return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + server_id = entry.data[CONF_SERVER_IDENTIFIER] + + cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id) + await hass.async_add_executor_job(cancel) + + tasks = [ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + await asyncio.gather(*tasks) + + hass.data[PLEX_DOMAIN][SERVERS].pop(server_id) + + return True diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index e77ac303bf1a25..478dd3754e7d8f 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -7,6 +7,7 @@ DEFAULT_VERIFY_SSL = True PLATFORMS = ["media_player", "sensor"] +REFRESH_LISTENERS = "refresh_listeners" SERVERS = "servers" PLEX_CONFIG_FILE = "plex.conf" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index bc19ff41dfedff..4d097253ea1a93 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -39,6 +39,7 @@ DOMAIN as PLEX_DOMAIN, NAME_FORMAT, PLEX_MEDIA_PLAYER_OPTIONS, + REFRESH_LISTENERS, SERVERS, ) @@ -71,7 +72,9 @@ def _setup_platform(hass, config_entry, add_entities_callback): plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] plex_clients = {} plex_sessions = {} - track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10)) + hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = track_time_interval( + hass, lambda now: update_devices(), timedelta(seconds=10) + ) def update_devices(): """Update the devices objects.""" From fbe85a2eb29054abaeb1679211a3a8899e7bfc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 22 Sep 2019 23:49:09 +0200 Subject: [PATCH 0408/3953] Add Kaiterra integration (#26661) * add Kaiterra integration * fix: split to multiple platforms * fix lint issues * fix formmating * fix: docstrings * fix: pylint issues * Apply suggestions from code review Co-Authored-By: Martin Hjelmare * Adjust code based on suggestions * Update homeassistant/components/kaiterra/sensor.py Co-Authored-By: Martin Hjelmare --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/kaiterra/__init__.py | 92 ++++++++++++++ .../components/kaiterra/air_quality.py | 115 ++++++++++++++++++ homeassistant/components/kaiterra/api_data.py | 109 +++++++++++++++++ homeassistant/components/kaiterra/const.py | 57 +++++++++ .../components/kaiterra/manifest.json | 8 ++ homeassistant/components/kaiterra/sensor.py | 95 +++++++++++++++ requirements_all.txt | 3 + 9 files changed, 481 insertions(+) create mode 100644 homeassistant/components/kaiterra/__init__.py create mode 100644 homeassistant/components/kaiterra/air_quality.py create mode 100644 homeassistant/components/kaiterra/api_data.py create mode 100644 homeassistant/components/kaiterra/const.py create mode 100644 homeassistant/components/kaiterra/manifest.json create mode 100644 homeassistant/components/kaiterra/sensor.py diff --git a/.coveragerc b/.coveragerc index 302ff9465549e2..65ee6e9e25958a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -318,6 +318,7 @@ omit = homeassistant/components/itunes/media_player.py homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/* + homeassistant/components/kaiterra/* homeassistant/components/kankun/switch.py homeassistant/components/keba/* homeassistant/components/keenetic_ndms2/device_tracker.py diff --git a/CODEOWNERS b/CODEOWNERS index abd3379221a9a8..640a9a7bcc0ae4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -146,6 +146,7 @@ homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi +homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills diff --git a/homeassistant/components/kaiterra/__init__.py b/homeassistant/components/kaiterra/__init__.py new file mode 100644 index 00000000000000..8c61ad5418481a --- /dev/null +++ b/homeassistant/components/kaiterra/__init__.py @@ -0,0 +1,92 @@ +"""Support for Kaiterra devices.""" +import voluptuous as vol + +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers import config_validation as cv + +from homeassistant.const import ( + CONF_API_KEY, + CONF_DEVICES, + CONF_DEVICE_ID, + CONF_SCAN_INTERVAL, + CONF_TYPE, + CONF_NAME, +) + +from .const import ( + AVAILABLE_AQI_STANDARDS, + AVAILABLE_UNITS, + AVAILABLE_DEVICE_TYPES, + CONF_AQI_STANDARD, + CONF_PREFERRED_UNITS, + DOMAIN, + DEFAULT_AQI_STANDARD, + DEFAULT_PREFERRED_UNIT, + DEFAULT_SCAN_INTERVAL, + KAITERRA_COMPONENTS, +) + +from .api_data import KaiterraApiData + +KAITERRA_DEVICE_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_TYPE): vol.In(AVAILABLE_DEVICE_TYPES), + vol.Optional(CONF_NAME): cv.string, + } +) + +KAITERRA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [KAITERRA_DEVICE_SCHEMA]), + vol.Optional(CONF_AQI_STANDARD, default=DEFAULT_AQI_STANDARD): vol.In( + AVAILABLE_AQI_STANDARDS + ), + vol.Optional(CONF_PREFERRED_UNITS, default=DEFAULT_PREFERRED_UNIT): vol.All( + cv.ensure_list, [vol.In(AVAILABLE_UNITS)] + ), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): cv.time_period, + } +) + +CONFIG_SCHEMA = vol.Schema({DOMAIN: KAITERRA_SCHEMA}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Kaiterra components.""" + + conf = config[DOMAIN] + scan_interval = conf[CONF_SCAN_INTERVAL] + devices = conf[CONF_DEVICES] + session = async_get_clientsession(hass) + api = hass.data[DOMAIN] = KaiterraApiData(hass, conf, session) + + await api.async_update() + + async def _update(now=None): + """Periodic update.""" + await api.async_update() + + async_track_time_interval(hass, _update, scan_interval) + + # Load platforms for each device + for device in devices: + device_name, device_id = ( + device.get(CONF_NAME) or device[CONF_TYPE], + device[CONF_DEVICE_ID], + ) + for component in KAITERRA_COMPONENTS: + hass.async_create_task( + async_load_platform( + hass, + component, + DOMAIN, + {CONF_NAME: device_name, CONF_DEVICE_ID: device_id}, + config, + ) + ) + + return True diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py new file mode 100644 index 00000000000000..4dfe04f9c2e12d --- /dev/null +++ b/homeassistant/components/kaiterra/air_quality.py @@ -0,0 +1,115 @@ +"""Support for Kaiterra Air Quality Sensors.""" +from homeassistant.components.air_quality import AirQualityEntity + +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from homeassistant.const import CONF_DEVICE_ID, CONF_NAME + +from .const import ( + DOMAIN, + ATTR_VOC, + ATTR_AQI_LEVEL, + ATTR_AQI_POLLUTANT, + DISPATCHER_KAITERRA, +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the air_quality kaiterra sensor.""" + if discovery_info is None: + return + + api = hass.data[DOMAIN] + name = discovery_info[CONF_NAME] + device_id = discovery_info[CONF_DEVICE_ID] + + async_add_entities([KaiterraAirQuality(api, name, device_id)]) + + +class KaiterraAirQuality(AirQualityEntity): + """Implementation of a Kaittera air quality sensor.""" + + def __init__(self, api, name, device_id): + """Initialize the sensor.""" + self._api = api + self._name = f"{name} Air Quality" + self._device_id = device_id + + def _data(self, key): + return self._device.get(key, {}).get("value") + + @property + def _device(self): + return self._api.data.get(self._device_id, {}) + + @property + def should_poll(self): + """Return that the sensor should not be polled.""" + return False + + @property + def available(self): + """Return the availability of the sensor.""" + return self._api.data.get(self._device_id) is not None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def air_quality_index(self): + """Return the Air Quality Index (AQI).""" + return self._data("aqi") + + @property + def air_quality_index_level(self): + """Return the Air Quality Index level.""" + return self._data("aqi_level") + + @property + def air_quality_index_pollutant(self): + """Return the Air Quality Index level.""" + return self._data("aqi_pollutant") + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._data("rpm25c") + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._data("rpm10c") + + @property + def volatile_organic_compounds(self): + """Return the VOC (Volatile Organic Compounds) level.""" + return self._data("rtvoc") + + @property + def unique_id(self): + """Return the sensor's unique id.""" + return f"{self._device_id}_air_quality" + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + data = {} + attributes = [ + (ATTR_VOC, self.volatile_organic_compounds), + (ATTR_AQI_LEVEL, self.air_quality_index_level), + (ATTR_AQI_POLLUTANT, self.air_quality_index_pollutant), + ] + + for attr, value in attributes: + if value is not None: + data[attr] = value + + return data + + async def async_added_to_hass(self): + """Register callback.""" + async_dispatcher_connect( + self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state + ) diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py new file mode 100644 index 00000000000000..0c2d6d9366147d --- /dev/null +++ b/homeassistant/components/kaiterra/api_data.py @@ -0,0 +1,109 @@ +"""Data for all Kaiterra devices.""" +from logging import getLogger + +import asyncio + +import async_timeout + +from aiohttp.client_exceptions import ClientResponseError + +from kaiterra_async_client import KaiterraAPIClient, AQIStandard, Units + +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_DEVICE_ID, CONF_TYPE + +from .const import ( + AQI_SCALE, + AQI_LEVEL, + CONF_AQI_STANDARD, + CONF_PREFERRED_UNITS, + DISPATCHER_KAITERRA, +) + +_LOGGER = getLogger(__name__) + +POLLUTANTS = {"rpm25c": "PM2.5", "rpm10c": "PM10", "rtvoc": "TVOC"} + + +class KaiterraApiData: + """Get data from Kaiterra API.""" + + def __init__(self, hass, config, session): + """Initialize the API data object.""" + + api_key = config[CONF_API_KEY] + aqi_standard = config[CONF_AQI_STANDARD] + devices = config[CONF_DEVICES] + units = config[CONF_PREFERRED_UNITS] + + self._hass = hass + self._api = KaiterraAPIClient( + session, + api_key=api_key, + aqi_standard=AQIStandard.from_str(aqi_standard), + preferred_units=[Units.from_str(unit) for unit in units], + ) + self._devices_ids = [device[CONF_DEVICE_ID] for device in devices] + self._devices = [ + f"/{device[CONF_TYPE]}s/{device[CONF_DEVICE_ID]}" for device in devices + ] + self._scale = AQI_SCALE[aqi_standard] + self._level = AQI_LEVEL[aqi_standard] + self._update_listeners = [] + self.data = {} + + async def async_update(self) -> None: + """Get the data from Kaiterra API.""" + + try: + with async_timeout.timeout(10): + data = await self._api.get_latest_sensor_readings(self._devices) + except (ClientResponseError, asyncio.TimeoutError): + _LOGGER.debug("Couldn't fetch data") + self.data = {} + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + + _LOGGER.debug("New data retrieved: %s", data) + + try: + self.data = {} + for i, device in enumerate(data): + if not device: + self.data[self._devices_ids[i]] = {} + continue + + aqi, main_pollutant = None, None + for sensor_name, sensor in device.items(): + points = sensor.get("points") + + if not points: + continue + + point = points[0] + sensor["value"] = point.get("value") + + if "aqi" not in point: + continue + + sensor["aqi"] = point["aqi"] + if not aqi or aqi < point["aqi"]: + aqi = point["aqi"] + main_pollutant = POLLUTANTS.get(sensor_name) + + level = None + for j in range(1, len(self._scale)): + if aqi <= self._scale[j]: + level = self._level[j - 1] + break + + device["aqi"] = {"value": aqi} + device["aqi_level"] = {"value": level} + device["aqi_pollutant"] = {"value": main_pollutant} + + self.data[self._devices_ids[i]] = device + + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + except IndexError as err: + _LOGGER.error("Parsing error %s", err) + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) diff --git a/homeassistant/components/kaiterra/const.py b/homeassistant/components/kaiterra/const.py new file mode 100644 index 00000000000000..7e23edb1259fa9 --- /dev/null +++ b/homeassistant/components/kaiterra/const.py @@ -0,0 +1,57 @@ +"""Consts for Kaiterra integration.""" + +from datetime import timedelta + +DOMAIN = "kaiterra" + +DISPATCHER_KAITERRA = "kaiterra_update" + +AQI_SCALE = { + "cn": [0, 50, 100, 150, 200, 300, 400, 500], + "in": [0, 50, 100, 200, 300, 400, 500], + "us": [0, 50, 100, 150, 200, 300, 500], +} +AQI_LEVEL = { + "cn": [ + "Good", + "Satisfactory", + "Moderate", + "Unhealthy for sensitive groups", + "Unhealthy", + "Very unhealthy", + "Hazardous", + ], + "in": [ + "Good", + "Satisfactory", + "Moderately polluted", + "Poor", + "Very poor", + "Severe", + ], + "us": [ + "Good", + "Moderate", + "Unhealthy for sensitive groups", + "Unhealthy", + "Very unhealthy", + "Hazardous", + ], +} + +ATTR_VOC = "volatile_organic_compounds" +ATTR_AQI_LEVEL = "air_quality_index_level" +ATTR_AQI_POLLUTANT = "air_quality_index_pollutant" + +AVAILABLE_AQI_STANDARDS = ["us", "cn", "in"] +AVAILABLE_UNITS = ["x", "%", "C", "F", "mg/m³", "µg/m³", "ppm", "ppb"] +AVAILABLE_DEVICE_TYPES = ["laseregg", "sensedge"] + +CONF_AQI_STANDARD = "aqi_standard" +CONF_PREFERRED_UNITS = "preferred_units" + +DEFAULT_AQI_STANDARD = "us" +DEFAULT_PREFERRED_UNIT = [] +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +KAITERRA_COMPONENTS = ["sensor", "air_quality"] diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json new file mode 100644 index 00000000000000..926f73fa4dbea9 --- /dev/null +++ b/homeassistant/components/kaiterra/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "kaiterra", + "name": "Kaiterra", + "documentation": "https://www.home-assistant.io/components/kaiterra", + "requirements": ["kaiterra-async-client==0.0.2"], + "codeowners": ["@Michsior14"], + "dependencies": [] +} \ No newline at end of file diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py new file mode 100644 index 00000000000000..4ff6435b64d8b0 --- /dev/null +++ b/homeassistant/components/kaiterra/sensor.py @@ -0,0 +1,95 @@ +"""Support for Kaiterra Temperature ahn Humidity Sensors.""" +from homeassistant.helpers.entity import Entity + +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT + +from .const import DOMAIN, DISPATCHER_KAITERRA + +SENSORS = [ + {"name": "Temperature", "prop": "rtemp", "device_class": "temperature"}, + {"name": "Humidity", "prop": "rhumid", "device_class": "humidity"}, +] + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the kaiterra temperature and humidity sensor.""" + if discovery_info is None: + return + + api = hass.data[DOMAIN] + name = discovery_info[CONF_NAME] + device_id = discovery_info[CONF_DEVICE_ID] + + async_add_entities( + [KaiterraSensor(api, name, device_id, sensor) for sensor in SENSORS] + ) + + +class KaiterraSensor(Entity): + """Implementation of a Kaittera sensor.""" + + def __init__(self, api, name, device_id, sensor): + """Initialize the sensor.""" + self._api = api + self._name = f'{name} {sensor["name"]}' + self._device_id = device_id + self._kind = sensor["name"].lower() + self._property = sensor["prop"] + self._device_class = sensor["device_class"] + + @property + def _sensor(self): + """Return the sensor data.""" + return self._api.data.get(self._device_id, {}).get(self._property, {}) + + @property + def should_poll(self): + """Return that the sensor should not be polled.""" + return False + + @property + def available(self): + """Return the availability of the sensor.""" + return self._api.data.get(self._device_id) is not None + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def state(self): + """Return the state.""" + return self._sensor.get("value") + + @property + def unique_id(self): + """Return the sensor's unique id.""" + return f"{self._device_id}_{self._kind}" + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + if not self._sensor.get("units"): + return None + + value = self._sensor["units"].value + + if value == "F": + return TEMP_FAHRENHEIT + if value == "C": + return TEMP_CELSIUS + return value + + async def async_added_to_hass(self): + """Register callback.""" + async_dispatcher_connect( + self.hass, DISPATCHER_KAITERRA, self.async_write_ha_state + ) diff --git a/requirements_all.txt b/requirements_all.txt index 939fcc27978c75..250643f745d293 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -707,6 +707,9 @@ jsonrpc-async==0.6 # homeassistant.components.kodi jsonrpc-websocket==0.6 +# homeassistant.components.kaiterra +kaiterra-async-client==0.0.2 + # homeassistant.components.keba keba-kecontact==0.2.0 From 5914475fe5f97d08e9d721cbf328727df2f3af5e Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 22 Sep 2019 17:23:14 -0500 Subject: [PATCH 0409/3953] Add manual step to Plex config flow (#26773) * Add manual config step * Pass token to manual step * Fix for no token * Show error * Specify key location * Cast discovery port to int --- homeassistant/components/plex/config_flow.py | 53 +++++++++++- homeassistant/components/plex/strings.json | 18 ++++- tests/components/plex/test_config_flow.py | 84 ++++++++++++++++++-- 3 files changed, 140 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 3c683c802f5ccf..e620e4869e5083 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -6,13 +6,22 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_URL, + CONF_TOKEN, + CONF_SSL, + CONF_VERIFY_SSL, +) from homeassistant.core import callback from homeassistant.util.json import load_json from .const import ( # pylint: disable=unused-import CONF_SERVER, CONF_SERVER_IDENTIFIER, + DEFAULT_PORT, + DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, PLEX_CONFIG_FILE, @@ -21,7 +30,9 @@ from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer -USER_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): str}) +USER_SCHEMA = vol.Schema( + {vol.Optional(CONF_TOKEN): str, vol.Optional("manual_setup"): bool} +) _LOGGER = logging.getLogger(__package__) @@ -44,14 +55,22 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Plex flow.""" self.current_login = {} + self.discovery_info = {} self.available_servers = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" + errors = {} if user_input is not None: - return await self.async_step_server_validate(user_input) + if user_input.pop("manual_setup", False): + return await self.async_step_manual_setup(user_input) + if CONF_TOKEN in user_input: + return await self.async_step_server_validate(user_input) + errors[CONF_TOKEN] = "no_token" - return self.async_show_form(step_id="user", data_schema=USER_SCHEMA, errors={}) + return self.async_show_form( + step_id="user", data_schema=USER_SCHEMA, errors=errors + ) async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" @@ -114,6 +133,30 @@ async def async_step_server_validate(self, server_config): }, ) + async def async_step_manual_setup(self, user_input=None): + """Begin manual configuration.""" + if len(user_input) > 1: + host = user_input.pop(CONF_HOST) + port = user_input.pop(CONF_PORT) + prefix = "https" if user_input.pop(CONF_SSL) else "http" + user_input[CONF_URL] = f"{prefix}://{host}:{port}" + return await self.async_step_server_validate(user_input) + + data_schema = vol.Schema( + { + vol.Required( + CONF_HOST, default=self.discovery_info.get(CONF_HOST) + ): str, + vol.Required( + CONF_PORT, default=self.discovery_info.get(CONF_PORT, DEFAULT_PORT) + ): int, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, + vol.Optional(CONF_TOKEN, default=user_input.get(CONF_TOKEN, "")): str, + } + ) + return self.async_show_form(step_id="manual_setup", data_schema=data_schema) + async def async_step_select_server(self, user_input=None): """Use selected Plex server.""" config = dict(self.current_login) @@ -148,6 +191,8 @@ async def async_step_discovery(self, discovery_info): # Skip discovery if a config already exists or is in progress. return self.async_abort(reason="already_configured") + discovery_info[CONF_PORT] = int(discovery_info[CONF_PORT]) + self.discovery_info = discovery_info json_file = self.hass.config.path(PLEX_CONFIG_FILE) file_config = await self.hass.async_add_executor_job(load_json, json_file) diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 396a3387fee295..c093d4fe0cec1e 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -2,6 +2,16 @@ "config": { "title": "Plex", "step": { + "manual_setup": { + "title": "Plex server", + "data": { + "host": "Host", + "port": "Port", + "ssl": "Use SSL", + "verify_ssl": "Verify SSL certificate", + "token": "Token (if required)" + } + }, "select_server": { "title": "Select Plex server", "description": "Multiple servers available, select one:", @@ -11,16 +21,18 @@ }, "user": { "title": "Connect Plex server", - "description": "Enter a Plex token for automatic setup.", + "description": "Enter a Plex token for automatic setup or manually configure a server.", "data": { - "token": "Plex token" + "token": "Plex token", + "manual_setup": "Manual setup" } } }, "error": { "faulty_credentials": "Authorization failed", "no_servers": "No servers linked to account", - "not_found": "Plex server not found" + "not_found": "Plex server not found", + "no_token": "Provide a token or select manual setup" }, "abort": { "all_configured": "All linked servers already configured", diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 9c9c1b625259f9..e98aed793cfdca 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -4,7 +4,14 @@ import requests.exceptions from homeassistant.components.plex import config_flow -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, + CONF_TOKEN, + CONF_URL, +) from tests.common import MockConfigEntry @@ -44,7 +51,8 @@ async def test_bad_credentials(hass): ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "form" @@ -196,7 +204,7 @@ async def test_unknown_exception(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"}, - data={CONF_TOKEN: MOCK_TOKEN}, + data={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "abort" @@ -219,7 +227,8 @@ async def test_no_servers_found(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "form" @@ -257,7 +266,8 @@ async def test_single_available_server(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "create_entry" @@ -303,7 +313,8 @@ async def test_multiple_servers_with_selection(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "form" @@ -364,7 +375,8 @@ async def test_adding_last_unconfigured_server(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "create_entry" @@ -447,8 +459,64 @@ async def test_all_available_servers_configured(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} + result["flow_id"], + user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, ) assert result["type"] == "abort" assert result["reason"] == "all_configured" + + +async def test_manual_config(hass): + """Test creating via manual configuration.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_TOKEN: "", "manual_setup": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + + mock_connections = MockConnections(ssl=True) + + with patch("plexapi.server.PlexServer") as mock_plex_server: + type(mock_plex_server.return_value).machineIdentifier = PropertyMock( + return_value=MOCK_SERVER_1.clientIdentifier + ) + type(mock_plex_server.return_value).friendlyName = PropertyMock( + return_value=MOCK_SERVER_1.name + ) + type( # pylint: disable=protected-access + mock_plex_server.return_value + )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: MOCK_HOST_1, + CONF_PORT: int(MOCK_PORT_1), + CONF_SSL: True, + CONF_VERIFY_SSL: True, + CONF_TOKEN: MOCK_TOKEN, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == MOCK_SERVER_1.name + assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == MOCK_SERVER_1.clientIdentifier + ) + assert ( + result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] + == mock_connections.connections[0].httpuri + ) + assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN From 60f0988435ac104f83ace36fa762bb27cb093509 Mon Sep 17 00:00:00 2001 From: Tommy Larsson <45052383+larssont@users.noreply.github.com> Date: Mon, 23 Sep 2019 00:57:39 +0200 Subject: [PATCH 0410/3953] Add Ombi integration (#26755) * Add Ombi integration * Black * Remove trailing comma * Change dict.get to dict[key] for known keys * Remove monitored conditions from config * Define SCAN_INTERVAL for default scan interval * Adjust requests syntax and add music_requests * Remove Ombi object initialization * Move constants to const.py * Fix imports * Set pyombi requirement to version 0.1.3 * Add services.yaml * Add config schema and setup for integration * Set pyombi requirement to version 0.1.3 * Fix syntax for scan interval * Fix datetime import * Add all files from ombi component * Move imports around * Move imports around * Move pyombi import to top of module * Move scan_interval to sensor module * Add custom validator for urlbase * Add guard clause for discovery_info * Add service validation schemas and constants * Bump pyombi version * Adjust urlbase validation * Add exception warnings for irretrievable media * Bump pyombi * Change from .get to dict[key] * Change schema and conf variable names * Set return to return false * Remove unneeded return --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/ombi/__init__.py | 149 ++++++++++++++++++++ homeassistant/components/ombi/const.py | 24 ++++ homeassistant/components/ombi/manifest.json | 8 ++ homeassistant/components/ombi/sensor.py | 77 ++++++++++ homeassistant/components/ombi/services.yaml | 27 ++++ requirements_all.txt | 3 + 8 files changed, 290 insertions(+) create mode 100644 homeassistant/components/ombi/__init__.py create mode 100644 homeassistant/components/ombi/const.py create mode 100644 homeassistant/components/ombi/manifest.json create mode 100644 homeassistant/components/ombi/sensor.py create mode 100644 homeassistant/components/ombi/services.yaml diff --git a/.coveragerc b/.coveragerc index 65ee6e9e25958a..a4f52af76cefb1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -447,6 +447,7 @@ omit = homeassistant/components/oem/climate.py homeassistant/components/oasa_telematics/sensor.py homeassistant/components/ohmconnect/sensor.py + homeassistant/components/ombi/* homeassistant/components/onewire/sensor.py homeassistant/components/onkyo/media_player.py homeassistant/components/onvif/camera.py diff --git a/CODEOWNERS b/CODEOWNERS index 640a9a7bcc0ae4..8fe47035912d07 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -198,6 +198,7 @@ homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 +homeassistant/components/ombi/* @larssont homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py new file mode 100644 index 00000000000000..860c7d4dcb4df0 --- /dev/null +++ b/homeassistant/components/ombi/__init__.py @@ -0,0 +1,149 @@ +"""Support for Ombi.""" +import logging + +import pyombi +import voluptuous as vol + +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, +) +import homeassistant.helpers.config_validation as cv + +from .const import ( + ATTR_NAME, + ATTR_SEASON, + CONF_URLBASE, + DEFAULT_PORT, + DEFAULT_SEASON, + DEFAULT_SSL, + DEFAULT_URLBASE, + DOMAIN, + SERVICE_MOVIE_REQUEST, + SERVICE_MUSIC_REQUEST, + SERVICE_TV_REQUEST, +) + +_LOGGER = logging.getLogger(__name__) + + +def urlbase(value) -> str: + """Validate and transform urlbase.""" + if value is None: + raise vol.Invalid("string value is None") + value = str(value).strip("/") + if not value: + return value + return value + "/" + + +SUBMIT_MOVIE_REQUEST_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string}) + +SUBMIT_MUSIC_REQUEST_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string}) + +SUBMIT_TV_REQUEST_SERVICE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_NAME): cv.string, + vol.Optional(ATTR_SEASON, default=DEFAULT_SEASON): vol.In( + ["first", "latest", "all"] + ), + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): urlbase, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the Ombi component platform.""" + + ombi = pyombi.Ombi( + ssl=config[DOMAIN][CONF_SSL], + host=config[DOMAIN][CONF_HOST], + port=config[DOMAIN][CONF_PORT], + api_key=config[DOMAIN][CONF_API_KEY], + username=config[DOMAIN][CONF_USERNAME], + urlbase=config[DOMAIN][CONF_URLBASE], + ) + + try: + ombi.test_connection() + except pyombi.OmbiError as err: + _LOGGER.warning("Unable to setup Ombi: %s", err) + return False + + hass.data[DOMAIN] = {"instance": ombi} + + def submit_movie_request(call): + """Submit request for movie.""" + name = call.data[ATTR_NAME] + movies = ombi.search_movie(name) + if movies: + movie = movies[0] + ombi.request_movie(movie["theMovieDbId"]) + else: + raise Warning("No movie found.") + + def submit_tv_request(call): + """Submit request for TV show.""" + name = call.data[ATTR_NAME] + tv_shows = ombi.search_tv(name) + + if tv_shows: + season = call.data[ATTR_SEASON] + show = tv_shows[0]["id"] + if season == "first": + ombi.request_tv(show, request_first=True) + elif season == "latest": + ombi.request_tv(show, request_latest=True) + elif season == "all": + ombi.request_tv(show, request_all=True) + else: + raise Warning("No TV show found.") + + def submit_music_request(call): + """Submit request for music album.""" + name = call.data[ATTR_NAME] + music = ombi.search_music_album(name) + if music: + ombi.request_music(music[0]["foreignAlbumId"]) + else: + raise Warning("No music album found.") + + hass.services.register( + DOMAIN, + SERVICE_MOVIE_REQUEST, + submit_movie_request, + schema=SUBMIT_MOVIE_REQUEST_SERVICE_SCHEMA, + ) + hass.services.register( + DOMAIN, + SERVICE_MUSIC_REQUEST, + submit_music_request, + schema=SUBMIT_MUSIC_REQUEST_SERVICE_SCHEMA, + ) + hass.services.register( + DOMAIN, + SERVICE_TV_REQUEST, + submit_tv_request, + schema=SUBMIT_TV_REQUEST_SERVICE_SCHEMA, + ) + hass.helpers.discovery.load_platform("sensor", DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/ombi/const.py b/homeassistant/components/ombi/const.py new file mode 100644 index 00000000000000..42b58e7f50d631 --- /dev/null +++ b/homeassistant/components/ombi/const.py @@ -0,0 +1,24 @@ +"""Support for Ombi.""" +ATTR_NAME = "name" +ATTR_SEASON = "season" + +CONF_URLBASE = "urlbase" + +DEFAULT_NAME = DOMAIN = "ombi" +DEFAULT_PORT = 5000 +DEFAULT_SEASON = "latest" +DEFAULT_SSL = False +DEFAULT_URLBASE = "" + +SERVICE_MOVIE_REQUEST = "submit_movie_request" +SERVICE_MUSIC_REQUEST = "submit_music_request" +SERVICE_TV_REQUEST = "submit_tv_request" + +SENSOR_TYPES = { + "movies": {"type": "Movie requests", "icon": "mdi:movie"}, + "tv": {"type": "TV show requests", "icon": "mdi:television-classic"}, + "music": {"type": "Music album requests", "icon": "mdi:album"}, + "pending": {"type": "Pending requests", "icon": "mdi:clock-alert-outline"}, + "approved": {"type": "Approved requests", "icon": "mdi:check"}, + "available": {"type": "Available requests", "icon": "mdi:download"}, +} diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json new file mode 100644 index 00000000000000..066f3270ccdd5b --- /dev/null +++ b/homeassistant/components/ombi/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "ombi", + "name": "Ombi", + "documentation": "https://www.home-assistant.io/components/ombi/", + "dependencies": [], + "codeowners": ["@larssont"], + "requirements": ["pyombi==0.1.5"] +} diff --git a/homeassistant/components/ombi/sensor.py b/homeassistant/components/ombi/sensor.py new file mode 100644 index 00000000000000..2a2f50532b4e23 --- /dev/null +++ b/homeassistant/components/ombi/sensor.py @@ -0,0 +1,77 @@ +"""Support for Ombi.""" +from datetime import timedelta +import logging + +from pyombi import OmbiError + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN, SENSOR_TYPES + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=60) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Ombi sensor platform.""" + if discovery_info is None: + return + + sensors = [] + + ombi = hass.data[DOMAIN]["instance"] + + for sensor in SENSOR_TYPES: + sensor_label = sensor + sensor_type = SENSOR_TYPES[sensor]["type"] + sensor_icon = SENSOR_TYPES[sensor]["icon"] + sensors.append(OmbiSensor(sensor_label, sensor_type, ombi, sensor_icon)) + + add_entities(sensors, True) + + +class OmbiSensor(Entity): + """Representation of an Ombi sensor.""" + + def __init__(self, label, sensor_type, ombi, icon): + """Initialize the sensor.""" + self._state = None + self._label = label + self._type = sensor_type + self._ombi = ombi + self._icon = icon + + @property + def name(self): + """Return the name of the sensor.""" + return f"Ombi {self._type}" + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Update the sensor.""" + try: + if self._label == "movies": + self._state = self._ombi.movie_requests + elif self._label == "tv": + self._state = self._ombi.tv_requests + elif self._label == "music": + self._state = self._ombi.music_requests + elif self._label == "pending": + self._state = self._ombi.total_requests["pending"] + elif self._label == "approved": + self._state = self._ombi.total_requests["approved"] + elif self._label == "available": + self._state = self._ombi.total_requests["available"] + except OmbiError as err: + _LOGGER.warning("Unable to update Ombi sensor: %s", err) + self._state = None diff --git a/homeassistant/components/ombi/services.yaml b/homeassistant/components/ombi/services.yaml new file mode 100644 index 00000000000000..5f4f2defe32097 --- /dev/null +++ b/homeassistant/components/ombi/services.yaml @@ -0,0 +1,27 @@ +# Ombi services.yaml entries + +submit_movie_request: + description: Searches for a movie and requests the first result. + fields: + name: + description: Search parameter + example: "beverly hills cop" + + +submit_tv_request: + description: Searches for a TV show and requests the first result. + fields: + name: + description: Search parameter + example: "breaking bad" + season: + description: Which season(s) to request (first, latest or all) + example: "latest" + + +submit_music_request: + description: Searches for a music album and requests the first result. + fields: + name: + description: Search parameter + example: "nevermind" \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 250643f745d293..614362578e0085 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1353,6 +1353,9 @@ pynzbgetapi==0.2.0 # homeassistant.components.obihai pyobihai==1.1.0 +# homeassistant.components.ombi +pyombi==0.1.5 + # homeassistant.components.openuv pyopenuv==1.0.9 From d162e39ec917922de883813798b99a029c4658a8 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 23 Sep 2019 00:32:13 +0000 Subject: [PATCH 0411/3953] [ci skip] Translation update --- .../ambiclimate/.translations/no.json | 2 +- .../binary_sensor/.translations/en.json | 92 +++++++++++++++++++ .../binary_sensor/.translations/no.json | 92 +++++++++++++++++++ .../components/deconz/.translations/no.json | 10 +- .../components/deconz/.translations/ru.json | 4 +- .../izone/.translations/zh-Hant.json | 15 +++ .../components/light/.translations/no.json | 4 +- .../components/light/.translations/ru.json | 4 +- .../components/locative/.translations/no.json | 4 +- .../components/plex/.translations/en.json | 14 ++- .../plex/.translations/zh-Hant.json | 33 +++++++ .../components/point/.translations/no.json | 6 +- .../components/switch/.translations/ru.json | 4 +- .../tellduslive/.translations/no.json | 2 +- .../components/toon/.translations/no.json | 4 +- .../components/unifi/.translations/no.json | 6 ++ 16 files changed, 273 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/en.json create mode 100644 homeassistant/components/binary_sensor/.translations/no.json create mode 100644 homeassistant/components/izone/.translations/zh-Hant.json create mode 100644 homeassistant/components/plex/.translations/zh-Hant.json diff --git a/homeassistant/components/ambiclimate/.translations/no.json b/homeassistant/components/ambiclimate/.translations/no.json index 567d0b95ff38cb..7bb124ae5433e1 100644 --- a/homeassistant/components/ambiclimate/.translations/no.json +++ b/homeassistant/components/ambiclimate/.translations/no.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og Tillat tilgang til din Ambiclimate konto, og kom s\u00e5 tilbake og trykk Send nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})", + "description": "Vennligst f\u00f8lg denne [linken]({authorization_url}) og Tillat tilgang til din Ambiclimate konto, kom deretter tilbake og trykk Send nedenfor.\n(Kontroller at den angitte URL-adressen for tilbakeringing er {cb_url})", "title": "Autensiere Ambiclimate" } }, diff --git a/homeassistant/components/binary_sensor/.translations/en.json b/homeassistant/components/binary_sensor/.translations/en.json new file mode 100644 index 00000000000000..6379df936b898d --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/en.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} battery is low", + "is_cold": "{entity_name} is cold", + "is_connected": "{entity_name} is connected", + "is_gas": "{entity_name} is detecting gas", + "is_hot": "{entity_name} is hot", + "is_light": "{entity_name} is detecting light", + "is_locked": "{entity_name} is locked", + "is_moist": "{entity_name} is moist", + "is_motion": "{entity_name} is detecting motion", + "is_moving": "{entity_name} is moving", + "is_no_gas": "{entity_name} is not detecting gas", + "is_no_light": "{entity_name} is not detecting light", + "is_no_motion": "{entity_name} is not detecting motion", + "is_no_problem": "{entity_name} is not detecting problem", + "is_no_smoke": "{entity_name} is not detecting smoke", + "is_no_sound": "{entity_name} is not detecting sound", + "is_no_vibration": "{entity_name} is not detecting vibration", + "is_not_bat_low": "{entity_name} battery is normal", + "is_not_cold": "{entity_name} is not cold", + "is_not_connected": "{entity_name} is disconnected", + "is_not_hot": "{entity_name} is not hot", + "is_not_locked": "{entity_name} is unlocked", + "is_not_moist": "{entity_name} is dry", + "is_not_moving": "{entity_name} is not moving", + "is_not_occupied": "{entity_name} is not occupied", + "is_not_open": "{entity_name} is closed", + "is_not_plugged_in": "{entity_name} is unplugged", + "is_not_powered": "{entity_name} is not powered", + "is_not_present": "{entity_name} is not present", + "is_not_unsafe": "{entity_name} is safe", + "is_occupied": "{entity_name} is occupied", + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on", + "is_open": "{entity_name} is open", + "is_plugged_in": "{entity_name} is plugged in", + "is_powered": "{entity_name} is powered", + "is_present": "{entity_name} is present", + "is_problem": "{entity_name} is detecting problem", + "is_smoke": "{entity_name} is detecting smoke", + "is_sound": "{entity_name} is detecting sound", + "is_unsafe": "{entity_name} is unsafe", + "is_vibration": "{entity_name} is detecting vibration" + }, + "trigger_type": { + "bat_low": "{entity_name} battery low", + "closed": "{entity_name} closed", + "cold": "{entity_name} became cold", + "connected": "{entity_name} connected", + "gas": "{entity_name} started detecting gas", + "hot": "{entity_name} became hot", + "light": "{entity_name} started detecting light", + "locked": "{entity_name} locked", + "moist\u00a7": "{entity_name} became moist", + "motion": "{entity_name} started detecting motion", + "moving": "{entity_name} started moving", + "no_gas": "{entity_name} stopped detecting gas", + "no_light": "{entity_name} stopped detecting light", + "no_motion": "{entity_name} stopped detecting motion", + "no_problem": "{entity_name} stopped detecting problem", + "no_smoke": "{entity_name} stopped detecting smoke", + "no_sound": "{entity_name} stopped detecting sound", + "no_vibration": "{entity_name} stopped detecting vibration", + "not_bat_low": "{entity_name} battery normal", + "not_cold": "{entity_name} became not cold", + "not_connected": "{entity_name} disconnected", + "not_hot": "{entity_name} became not hot", + "not_locked": "{entity_name} unlocked", + "not_moist": "{entity_name} became dry", + "not_moving": "{entity_name} stopped moving", + "not_occupied": "{entity_name} became not occupied", + "not_plugged_in": "{entity_name} unplugged", + "not_powered": "{entity_name} not powered", + "not_present": "{entity_name} not present", + "not_unsafe": "{entity_name} became safe", + "occupied": "{entity_name} became occupied", + "opened": "{entity_name} opened", + "plugged_in": "{entity_name} plugged in", + "powered": "{entity_name} powered", + "present": "{entity_name} present", + "problem": "{entity_name} started detecting problem", + "smoke": "{entity_name} started detecting smoke", + "sound": "{entity_name} started detecting sound", + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on", + "unsafe": "{entity_name} became unsafe", + "vibration": "{entity_name} started detecting vibration" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/no.json b/homeassistant/components/binary_sensor/.translations/no.json new file mode 100644 index 00000000000000..5a1916bce59c33 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/no.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batteriniv\u00e5et er lavt", + "is_cold": "{entity_name} er kald", + "is_connected": "{entity_name} er tilkoblet", + "is_gas": "{entity_name} registrerer gass", + "is_hot": "{entity_name} er varm", + "is_light": "{entity_name} registrerer lys", + "is_locked": "{entity_name} er l\u00e5st", + "is_moist": "{entity_name} er fuktig", + "is_motion": "{entity_name} registrerer bevegelse", + "is_moving": "{entity_name} er i bevegelse", + "is_no_gas": "{entity_name} registrerer ikke gass", + "is_no_light": "{entity_name} registrerer ikke lys", + "is_no_motion": "{entity_name} registrerer ikke bevegelse", + "is_no_problem": "{entity_name} registrerer ikke et problem", + "is_no_smoke": "{entity_name} registrerer ikke r\u00f8yk", + "is_no_sound": "{entity_name} registrerer ikke lyd", + "is_no_vibration": "{entity_name} registrerer ikke bevegelse", + "is_not_bat_low": "{entity_name} batteri er normalt", + "is_not_cold": "{entity_name} er ikke kald", + "is_not_connected": "{entity_name} er frakoblet", + "is_not_hot": "{entity_name} er ikke varm", + "is_not_locked": "{entity_name} er ul\u00e5st", + "is_not_moist": "{entity_name} er t\u00f8rr", + "is_not_moving": "{entity_name} er ikke i bevegelse", + "is_not_occupied": "{entity_name} er ledig", + "is_not_open": "{entity_name} er lukket", + "is_not_plugged_in": "{entity_name} er koblet fra", + "is_not_powered": "{entity_name} er spenningsl\u00f8s", + "is_not_present": "{entity_name} er ikke tilstede", + "is_not_unsafe": "{entity_name} er trygg", + "is_occupied": "{entity_name} er opptatt", + "is_off": "{entity_name} er sl\u00e5tt av", + "is_on": "{entity_name} er sl\u00e5tt p\u00e5", + "is_open": "{entity_name} er \u00e5pen", + "is_plugged_in": "{entity_name} er koblet til", + "is_powered": "{entity_name} er spenningssatt", + "is_present": "{entity_name} er tilstede", + "is_problem": "{entity_name} registrerer et problem", + "is_smoke": "{entity_name} registrerer r\u00f8yk", + "is_sound": "{entity_name} registrerer lyd", + "is_unsafe": "{entity_name} er utrygg", + "is_vibration": "{entity_name} registrerer vibrasjon" + }, + "trigger_type": { + "bat_low": "{entity_name} lavt batteri", + "closed": "{entity_name} stengt", + "cold": "{entity_name} ble kald", + "connected": "{entity_name} tilkoblet", + "gas": "{entity_name} begynte \u00e5 registrere gass", + "hot": "{entity_name} ble varm", + "light": "{entity_name} begynte \u00e5 registrere lys", + "locked": "{entity_name} l\u00e5st", + "moist\u00a7": "{entity_name} ble fuktig", + "motion": "{entity_name} begynte \u00e5 registrere bevegelse", + "moving": "{entity_name} begynte \u00e5 bevege seg", + "no_gas": "{entity_name} sluttet \u00e5 registrere gass", + "no_light": "{entity_name} sluttet \u00e5 registrere lys", + "no_motion": "{entity_name} sluttet \u00e5 registrere bevegelse", + "no_problem": "{entity_name} sluttet \u00e5 registrere problem", + "no_smoke": "{entity_name} sluttet \u00e5 registrere r\u00f8yk", + "no_sound": "{entity_name} sluttet \u00e5 registrere lyd", + "no_vibration": "{entity_name} sluttet \u00e5 registrere vibrasjon", + "not_bat_low": "{entity_name} batteri normalt", + "not_cold": "{entity_name} ble ikke lenger kald", + "not_connected": "{entity_name} koblet fra", + "not_hot": "{entity_name} ble ikke lenger varm", + "not_locked": "{entity_name} l\u00e5st opp", + "not_moist": "{entity_name} ble t\u00f8rr", + "not_moving": "{entity_name} sluttet \u00e5 bevege seg", + "not_occupied": "{entity_name} ble ledig", + "not_plugged_in": "{entity_name} koblet fra", + "not_powered": "{entity_name} spenningsl\u00f8s", + "not_present": "{entity_name} ikke til stede", + "not_unsafe": "{entity_name} ble trygg", + "occupied": "{entity_name} ble opptatt", + "opened": "{entity_name} \u00e5pnet", + "plugged_in": "{entity_name} koblet til", + "powered": "{entity_name} spenningssatt", + "present": "{entity_name} tilstede", + "problem": "{entity_name} begynte \u00e5 registrere et problem", + "smoke": "{entity_name} begynte \u00e5 registrere r\u00f8yk", + "sound": "{entity_name} begynte \u00e5 registrere lyd", + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5", + "unsafe": "{entity_name} ble usikker", + "vibration": "{entity_name} begynte \u00e5 oppdage vibrasjon" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 7a93c6ff9cf850..3968c1f00c58d2 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -58,15 +58,15 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { - "remote_button_double_press": "\"{under type}\"-knappen ble dobbeltklikket", - "remote_button_long_press": "\"{undertype}\" - knappen ble kontinuerlig trykket", - "remote_button_long_release": "\" {subtype} \" -knapp sluppet etter langt trykk", + "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", + "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", + "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\"{undertype}\" - knappen femdobbelt klikket", + "remote_button_quintuple_press": "\" {subtype} \" - knappen femdobbelt klikket", "remote_button_rotated": "Knappen roterte \" {subtype} \"", "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", "remote_button_short_release": "\" {subtype} \" -knappen ble utgitt", - "remote_button_triple_press": "\"{under type}\"-knappen trippel klikket", + "remote_button_triple_press": "\" {subtype} \"-knappen trippel klikket", "remote_gyro_activated": "Enhet er ristet" } }, diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 612c5afd033b79..558fd9e5897e7f 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -54,8 +54,8 @@ "left": "\u041d\u0430\u043b\u0435\u0432\u043e", "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", - "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c", - "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c" + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", diff --git a/homeassistant/components/izone/.translations/zh-Hant.json b/homeassistant/components/izone/.translations/zh-Hant.json new file mode 100644 index 00000000000000..7448100158e8ee --- /dev/null +++ b/homeassistant/components/izone/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 iZone \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 iZone \u5373\u53ef\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a iZone\uff1f", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/no.json b/homeassistant/components/light/.translations/no.json index 008123739d9138..785e9ca2912e00 100644 --- a/homeassistant/components/light/.translations/no.json +++ b/homeassistant/components/light/.translations/no.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} er p\u00e5" }, "trigger_type": { - "turned_off": "{name} sl\u00e5tt av", - "turned_on": "{name} sl\u00e5tt p\u00e5" + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json index ba9339c1a944e8..a6a7994b7c3603 100644 --- a/homeassistant/components/light/.translations/ru.json +++ b/homeassistant/components/light/.translations/ru.json @@ -10,8 +10,8 @@ "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" } } } \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/no.json b/homeassistant/components/locative/.translations/no.json index 00e3337dfe1eeb..8e9b3272f947b3 100644 --- a/homeassistant/components/locative/.translations/no.json +++ b/homeassistant/components/locative/.translations/no.json @@ -10,9 +10,9 @@ "step": { "user": { "description": "Er du sikker p\u00e5 at du vil sette opp Locative Webhook?", - "title": "Sett opp Lokative Webhook" + "title": "Sett opp Locative Webhook" } }, - "title": "Lokative Webhook" + "title": "Locative Webhook" } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index 2ada5e810ecbaa..7fa9f62be07763 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Authorization failed", "no_servers": "No servers linked to account", + "no_token": "Provide a token or select manual setup", "not_found": "Plex server not found" }, "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "Use SSL", + "token": "Token (if required)", + "verify_ssl": "Verify SSL certificate" + }, + "title": "Plex server" + }, "select_server": { "data": { "server": "Server" @@ -22,9 +33,10 @@ }, "user": { "data": { + "manual_setup": "Manual setup", "token": "Plex token" }, - "description": "Enter a Plex token for automatic setup.", + "description": "Enter a Plex token for automatic setup or manually configure a server.", "title": "Connect Plex server" } }, diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json new file mode 100644 index 00000000000000..c79a49470e000d --- /dev/null +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "all_configured": "\u6240\u6709\u7d81\u5b9a\u4f3a\u670d\u5668\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", + "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", + "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" + }, + "error": { + "faulty_credentials": "\u9a57\u8b49\u5931\u6557", + "no_servers": "\u6b64\u5e33\u865f\u672a\u7d81\u5b9a\u4f3a\u670d\u5668", + "not_found": "\u627e\u4e0d\u5230 Plex \u4f3a\u670d\u5668" + }, + "step": { + "select_server": { + "data": { + "server": "\u4f3a\u670d\u5668" + }, + "description": "\u627e\u5230\u591a\u500b\u4f3a\u670d\u5668\uff0c\u8acb\u9078\u64c7\u4e00\u7d44\uff1a", + "title": "\u9078\u64c7 Plex \u4f3a\u670d\u5668" + }, + "user": { + "data": { + "token": "Plex \u5bc6\u9470" + }, + "description": "\u8acb\u8f38\u5165 Plex \u5bc6\u9470\u4ee5\u9032\u884c\u81ea\u52d5\u8a2d\u5b9a\u3002", + "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/point/.translations/no.json b/homeassistant/components/point/.translations/no.json index 58b6e1e63fd311..c87c1a702c8d39 100644 --- a/homeassistant/components/point/.translations/no.json +++ b/homeassistant/components/point/.translations/no.json @@ -8,11 +8,11 @@ "no_flows": "Du m\u00e5 konfigurere Point f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/point/)." }, "create_entry": { - "default": "Vellykket godkjenning med Minut for din(e) Point enhet(er)" + "default": "Vellykket autentisering med Minut for din(e) Point enhet(er)" }, "error": { - "follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send", - "no_token": "Ikke godkjent med Minut" + "follow_link": "Vennligst f\u00f8lg lenken og autentiser f\u00f8r du trykker p\u00e5 Send", + "no_token": "Ikke autentisert med Minut" }, "step": { "auth": { diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index b769e56c97446a..cd5cbc0d6a174b 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -12,8 +12,8 @@ "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" } } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/no.json b/homeassistant/components/tellduslive/.translations/no.json index d311b3b0d38068..090de51703654f 100644 --- a/homeassistant/components/tellduslive/.translations/no.json +++ b/homeassistant/components/tellduslive/.translations/no.json @@ -12,7 +12,7 @@ "step": { "auth": { "description": "For \u00e5 koble TelldusLive-kontoen din:\n 1. Klikk p\u00e5 linken under\n 2. Logg inn p\u00e5 Telldus Live \n 3. Tillat **{app_name}** (klikk**Ja**). \n 4. Kom tilbake hit og klikk **SUBMIT**. \n\n [Link TelldusLive-konto]({auth_url})", - "title": "Godkjen mot TelldusLive" + "title": "Godkjenn mot TelldusLive" }, "user": { "data": { diff --git a/homeassistant/components/toon/.translations/no.json b/homeassistant/components/toon/.translations/no.json index 37dcd8ac22f9f5..a033d2954d98d1 100644 --- a/homeassistant/components/toon/.translations/no.json +++ b/homeassistant/components/toon/.translations/no.json @@ -8,7 +8,7 @@ "unknown_auth_fail": "Uventet feil oppstod under autentisering." }, "error": { - "credentials": "De oppgitte legitimasjonene er ugyldige.", + "credentials": "Den oppgitte kontoinformasjonen er ugyldig.", "display_exists": "Den valgte skjermen er allerede konfigurert." }, "step": { @@ -18,7 +18,7 @@ "tenant": "Leietaker", "username": "Brukernavn" }, - "description": "Godkjen med Eneco Toon kontoen din (ikke utviklerkontoen).", + "description": "Godkjenn med Eneco Toon kontoen din (ikke utviklerkontoen).", "title": "Linken din Toon konto" }, "display": { diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index 068f4341544900..c21a47c7ea2c8b 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -32,6 +32,12 @@ "track_devices": "Spore nettverksenheter (Ubiquiti-enheter)", "track_wired_clients": "Inkluder kablede nettverksklienter" } + }, + "init": { + "data": { + "one": "en", + "other": "andre" + } } } } From 2e4cc7e5a018b5fc3eb81522b7bf893abf94e8e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Sep 2019 20:46:32 -0700 Subject: [PATCH 0412/3953] Prevent Wemo doing I/O in event loop (#26835) * Prevent Wemo doing I/O in event loop * Update config_flow.py --- homeassistant/components/wemo/config_flow.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py index a1614eb1ce30c2..21c911a66ce944 100644 --- a/homeassistant/components/wemo/config_flow.py +++ b/homeassistant/components/wemo/config_flow.py @@ -1,14 +1,16 @@ """Config flow for Wemo.""" + +import pywemo + from homeassistant.helpers import config_entry_flow from homeassistant import config_entries + from . import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - import pywemo - - return bool(pywemo.discover_devices()) + return bool(await hass.async_add_executor_job(pywemo.discover_devices)) config_entry_flow.register_discovery_flow( From 5a4a3e17cc820d28520c47d478b19182527547a2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Sep 2019 20:46:50 -0700 Subject: [PATCH 0413/3953] Split scaffolding script (#26832) * Add scaffolding split * Add second config flow method --- script/scaffold/__main__.py | 75 +++++-- script/scaffold/docs.py | 22 ++ script/scaffold/error.py | 2 +- script/scaffold/gather_info.py | 188 ++++++++++++++---- script/scaffold/generate.py | 144 ++++++++++---- script/scaffold/model.py | 53 ++++- .../integration/config_flow.py | 13 +- .../tests/test_config_flow.py | 2 +- .../integration/config_flow.py | 18 ++ .../templates/integration/__init__.py | 19 -- .../scaffold/templates/integration/error.py | 10 - .../integration/integration/__init__.py | 12 ++ .../integration/{ => integration}/const.py | 0 .../{ => integration}/manifest.json | 6 +- .../templates/integration/strings.json | 21 -- script/scaffold/templates/tests/__init__.py | 1 - 16 files changed, 426 insertions(+), 160 deletions(-) create mode 100644 script/scaffold/docs.py rename script/scaffold/templates/{ => config_flow}/integration/config_flow.py (83%) rename script/scaffold/templates/{ => config_flow}/tests/test_config_flow.py (97%) create mode 100644 script/scaffold/templates/config_flow_discovery/integration/config_flow.py delete mode 100644 script/scaffold/templates/integration/__init__.py delete mode 100644 script/scaffold/templates/integration/error.py create mode 100644 script/scaffold/templates/integration/integration/__init__.py rename script/scaffold/templates/integration/{ => integration}/const.py (100%) rename script/scaffold/templates/integration/{ => integration}/manifest.json (63%) delete mode 100644 script/scaffold/templates/integration/strings.json delete mode 100644 script/scaffold/templates/tests/__init__.py diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index d1b514ea934a2c..93bcc5aba4172c 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -1,9 +1,42 @@ """Validate manifests.""" +import argparse from pathlib import Path import subprocess import sys -from . import gather_info, generate, error, model +from . import gather_info, generate, error +from .const import COMPONENT_DIR + + +TEMPLATES = [ + p.name for p in (Path(__file__).parent / "templates").glob("*") if p.is_dir() +] + + +def valid_integration(integration): + """Test if it's a valid integration.""" + if not (COMPONENT_DIR / integration).exists(): + raise argparse.ArgumentTypeError( + f"The integration {integration} does not exist." + ) + + return integration + + +def get_arguments() -> argparse.Namespace: + """Get parsed passed in arguments.""" + parser = argparse.ArgumentParser(description="Home Assistant Scaffolder") + parser.add_argument("template", type=str, choices=TEMPLATES) + parser.add_argument( + "--develop", action="store_true", help="Automatically fill in info" + ) + parser.add_argument( + "--integration", type=valid_integration, help="Integration to target." + ) + + arguments = parser.parse_args() + + return arguments def main(): @@ -12,29 +45,22 @@ def main(): print("Run from project root") return 1 - print("Creating a new integration for Home Assistant.") + args = get_arguments() - if "--develop" in sys.argv: - print("Running in developer mode. Automatically filling in info.") - print() + info = gather_info.gather_info(args) - info = model.Info( - domain="develop", - name="Develop Hub", - codeowner="@developer", - requirement="aiodevelop==1.2.3", - ) - else: - try: - info = gather_info.gather_info() - except error.ExitApp as err: - print() - print(err.reason) - return err.exit_code + generate.generate(args.template, info) + + # If creating new integration, create config flow too + if args.template == "integration": + if info.authentication or not info.discoverable: + template = "config_flow" + else: + template = "config_flow_discovery" - generate.generate(info) + generate.generate(template, info) - print("Running hassfest to pick up new codeowner and config flow.") + print("Running hassfest to pick up new information.") subprocess.run("python -m script.hassfest", shell=True) print() @@ -47,10 +73,15 @@ def main(): return 1 print() - print(f"Successfully created the {info.domain} integration!") + print(f"Done!") return 0 if __name__ == "__main__": - sys.exit(main()) + try: + sys.exit(main()) + except error.ExitApp as err: + print() + print(f"Fatal Error: {err.reason}") + sys.exit(err.exit_code) diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py new file mode 100644 index 00000000000000..54a182be31bd63 --- /dev/null +++ b/script/scaffold/docs.py @@ -0,0 +1,22 @@ +"""Print links to relevant docs.""" +from .model import Info + + +def print_relevant_docs(template: str, info: Info) -> None: + """Print relevant docs.""" + if template == "integration": + print( + f""" +Your integration has been created at {info.integration_dir} . Next step is to fill in the blanks for the code marked with TODO. + +For a breakdown of each file, check the developer documentation at: +https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html +""" + ) + + elif template == "config_flow": + print( + f""" +The config flow has been added to the {info.domain} integration. Next step is to fill in the blanks for the code marked with TODO. +""" + ) diff --git a/script/scaffold/error.py b/script/scaffold/error.py index d99cbe8026aff2..75a869572fd7fc 100644 --- a/script/scaffold/error.py +++ b/script/scaffold/error.py @@ -4,7 +4,7 @@ class ExitApp(Exception): """Exception to indicate app should exit.""" - def __init__(self, reason, exit_code): + def __init__(self, reason, exit_code=1): """Initialize the exit app exception.""" self.reason = reason self.exit_code = exit_code diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index 352d1da206c12b..a7263daaf41982 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -1,4 +1,6 @@ """Gather info for scaffolding.""" +import json + from homeassistant.util import slugify from .const import COMPONENT_DIR @@ -9,49 +11,142 @@ CHECK_EMPTY = ["Cannot be empty", lambda value: value] -FIELDS = { - "domain": { - "prompt": "What is the domain?", - "validators": [ - CHECK_EMPTY, - [ - "Domains cannot contain spaces or special characters.", - lambda value: value == slugify(value), - ], - [ - "There already is an integration with this domain.", - lambda value: not (COMPONENT_DIR / value).exists(), - ], - ], - }, - "name": { - "prompt": "What is the name of your integration?", - "validators": [CHECK_EMPTY], - }, - "codeowner": { - "prompt": "What is your GitHub handle?", - "validators": [ - CHECK_EMPTY, - [ - 'GitHub handles need to start with an "@"', - lambda value: value.startswith("@"), - ], - ], - }, - "requirement": { - "prompt": "What PyPI package and version do you depend on? Leave blank for none.", - "validators": [ - ["Versions should be pinned using '=='.", lambda value: "==" in value] - ], - }, -} - - -def gather_info() -> Info: +def gather_info(arguments) -> Info: + """Gather info.""" + existing = arguments.template != "integration" + + if arguments.develop: + print("Running in developer mode. Automatically filling in info.") + print() + + if existing: + if arguments.develop: + return _load_existing_integration("develop") + + if arguments.integration: + return _load_existing_integration(arguments.integration) + + return gather_existing_integration() + + if arguments.develop: + return Info( + domain="develop", + name="Develop Hub", + codeowner="@developer", + requirement="aiodevelop==1.2.3", + ) + + return gather_new_integration() + + +def gather_new_integration() -> Info: + """Gather info about new integration from user.""" + return Info( + **_gather_info( + { + "domain": { + "prompt": "What is the domain?", + "validators": [ + CHECK_EMPTY, + [ + "Domains cannot contain spaces or special characters.", + lambda value: value == slugify(value), + ], + [ + "There already is an integration with this domain.", + lambda value: not (COMPONENT_DIR / value).exists(), + ], + ], + }, + "name": { + "prompt": "What is the name of your integration?", + "validators": [CHECK_EMPTY], + }, + "codeowner": { + "prompt": "What is your GitHub handle?", + "validators": [ + CHECK_EMPTY, + [ + 'GitHub handles need to start with an "@"', + lambda value: value.startswith("@"), + ], + ], + }, + "requirement": { + "prompt": "What PyPI package and version do you depend on? Leave blank for none.", + "validators": [ + [ + "Versions should be pinned using '=='.", + lambda value: not value or "==" in value, + ] + ], + }, + "authentication": { + "prompt": "Does Home Assistant need the user to authenticate to control the device/service? (yes/no)", + "default": "yes", + "validators": [ + [ + "Type either 'yes' or 'no'", + lambda value: value in ("yes", "no"), + ] + ], + "convertor": lambda value: value == "yes", + }, + "discoverable": { + "prompt": "Is the device/service discoverable on the local network? (yes/no)", + "default": "no", + "validators": [ + [ + "Type either 'yes' or 'no'", + lambda value: value in ("yes", "no"), + ] + ], + "convertor": lambda value: value == "yes", + }, + } + ) + ) + + +def gather_existing_integration() -> Info: + """Gather info about existing integration from user.""" + answers = _gather_info( + { + "domain": { + "prompt": "What is the domain?", + "validators": [ + CHECK_EMPTY, + [ + "Domains cannot contain spaces or special characters.", + lambda value: value == slugify(value), + ], + [ + "This integration does not exist.", + lambda value: (COMPONENT_DIR / value).exists(), + ], + ], + } + } + ) + + return _load_existing_integration(answers["domain"]) + + +def _load_existing_integration(domain) -> Info: + """Load an existing integration.""" + if not (COMPONENT_DIR / domain).exists(): + raise ExitApp("Integration does not exist", 1) + + manifest = json.loads((COMPONENT_DIR / domain / "manifest.json").read_text()) + + return Info(domain=domain, name=manifest["name"]) + + +def _gather_info(fields) -> dict: """Gather info from user.""" answers = {} - for key, info in FIELDS.items(): + for key, info in fields.items(): hint = None while key not in answers: if hint is not None: @@ -60,11 +155,18 @@ def gather_info() -> Info: try: print() - value = input(info["prompt"] + "\n> ") + msg = info["prompt"] + if "default" in info: + msg += f" [{info['default']}]" + value = input(f"{msg}\n> ") except (KeyboardInterrupt, EOFError): raise ExitApp("Interrupted!", 1) value = value.strip() + + if value == "" and "default" in info: + value = info["default"] + hint = None for validator_hint, validator in info["validators"]: @@ -73,7 +175,9 @@ def gather_info() -> Info: break if hint is None: + if "convertor" in info: + value = info["convertor"](value) answers[key] = value print() - return Info(**answers) + return answers diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index f7b3f56f2e6623..6bccf6529feeea 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -1,8 +1,7 @@ """Generate an integration.""" -import json from pathlib import Path -from .const import COMPONENT_DIR, TESTS_DIR +from .error import ExitApp from .model import Info TEMPLATE_DIR = Path(__file__).parent / "templates" @@ -10,38 +9,113 @@ TEMPLATE_TESTS = TEMPLATE_DIR / "tests" -def generate(info: Info) -> None: - """Generate an integration.""" - print(f"Generating the {info.domain} integration...") - integration_dir = COMPONENT_DIR / info.domain - test_dir = TESTS_DIR / info.domain - - replaces = { - "NEW_DOMAIN": info.domain, - "NEW_NAME": info.name, - "NEW_CODEOWNER": info.codeowner, - # Special case because we need to keep the list empty if there is none. - '"MANIFEST_NEW_REQUIREMENT"': ( - json.dumps(info.requirement) if info.requirement else "" - ), - } - - for src_dir, target_dir in ( - (TEMPLATE_INTEGRATION, integration_dir), - (TEMPLATE_TESTS, test_dir), - ): - # Guard making it for test purposes. - if not target_dir.exists(): - target_dir.mkdir() - - for source_file in src_dir.glob("**/*"): - content = source_file.read_text() - - for to_search, to_replace in replaces.items(): - content = content.replace(to_search, to_replace) - - target_file = target_dir / source_file.relative_to(src_dir) - print(f"Writing {target_file}") - target_file.write_text(content) +def generate(template: str, info: Info) -> None: + """Generate a template.""" + _validate(template, info) + print(f"Scaffolding {template} for the {info.domain} integration...") + _ensure_tests_dir_exists(info) + _generate(TEMPLATE_DIR / template / "integration", info.integration_dir, info) + _generate(TEMPLATE_DIR / template / "tests", info.tests_dir, info) + _custom_tasks(template, info) print() + + +def _validate(template, info): + """Validate we can run this task.""" + if template == "config_flow": + if (info.integration_dir / "config_flow.py").exists(): + raise ExitApp(f"Integration {info.domain} already has a config flow.") + + +def _generate(src_dir, target_dir, info: Info) -> None: + """Generate an integration.""" + replaces = {"NEW_DOMAIN": info.domain, "NEW_NAME": info.name} + + if not target_dir.exists(): + target_dir.mkdir() + + for source_file in src_dir.glob("**/*"): + content = source_file.read_text() + + for to_search, to_replace in replaces.items(): + content = content.replace(to_search, to_replace) + + target_file = target_dir / source_file.relative_to(src_dir) + print(f"Writing {target_file}") + target_file.write_text(content) + + +def _ensure_tests_dir_exists(info: Info) -> None: + """Ensure a test dir exists.""" + if info.tests_dir.exists(): + return + + info.tests_dir.mkdir() + print(f"Writing {info.tests_dir / '__init__.py'}") + (info.tests_dir / "__init__.py").write_text( + f'"""Tests for the {info.name} integration."""\n' + ) + + +def _custom_tasks(template, info) -> None: + """Handle custom tasks for templates.""" + if template == "integration": + changes = {"codeowners": [info.codeowner]} + + if info.requirement: + changes["requirements"] = [info.requirement] + + info.update_manifest(**changes) + + if template == "config_flow": + info.update_manifest(config_flow=True) + info.update_strings( + config={ + "title": info.name, + "step": { + "user": {"title": "Connect to the device", "data": {"host": "Host"}} + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error", + }, + "abort": {"already_configured": "Device is already configured"}, + } + ) + + if template == "config_flow_discovery": + info.update_manifest(config_flow=True) + info.update_strings( + config={ + "title": info.name, + "step": { + "confirm": { + "title": info.name, + "description": f"Do you want to set up {info.name}?", + } + }, + "abort": { + "single_instance_allowed": f"Only a single configuration of {info.name} is possible.", + "no_devices_found": f"No {info.name} devices found on the network.", + }, + } + ) + + if template in ("config_flow", "config_flow_discovery"): + init_file = info.integration_dir / "__init__.py" + init_file.write_text( + init_file.read_text() + + """ + +async def async_setup_entry(hass, entry): + \"\"\"Set up a config entry for NEW_NAME.\"\"\" + # TODO forward the entry for each platform that you want to set up. + # hass.async_create_task( + # hass.config_entries.async_forward_entry_setup(entry, "media_player") + # ) + + return True +""" + ) diff --git a/script/scaffold/model.py b/script/scaffold/model.py index 83fe922d8c4473..68ab771122e0ab 100644 --- a/script/scaffold/model.py +++ b/script/scaffold/model.py @@ -1,6 +1,11 @@ """Models for scaffolding.""" +import json +from pathlib import Path + import attr +from .const import COMPONENT_DIR, TESTS_DIR + @attr.s class Info: @@ -8,5 +13,49 @@ class Info: domain: str = attr.ib() name: str = attr.ib() - codeowner: str = attr.ib() - requirement: str = attr.ib() + codeowner: str = attr.ib(default=None) + requirement: str = attr.ib(default=None) + authentication: str = attr.ib(default=None) + discoverable: str = attr.ib(default=None) + + @property + def integration_dir(self) -> Path: + """Return directory if integration.""" + return COMPONENT_DIR / self.domain + + @property + def tests_dir(self) -> Path: + """Return test directory.""" + return TESTS_DIR / self.domain + + @property + def manifest_path(self) -> Path: + """Path to the manifest.""" + return COMPONENT_DIR / self.domain / "manifest.json" + + def manifest(self) -> dict: + """Return integration manifest.""" + return json.loads(self.manifest_path.read_text()) + + def update_manifest(self, **kwargs) -> None: + """Update the integration manifest.""" + print(f"Updating {self.domain} manifest: {kwargs}") + self.manifest_path.write_text( + json.dumps({**self.manifest(), **kwargs}, indent=2) + ) + + @property + def strings_path(self) -> Path: + """Path to the strings.""" + return COMPONENT_DIR / self.domain / "strings.json" + + def strings(self) -> dict: + """Return integration strings.""" + if not self.strings_path.exists(): + return {} + return json.loads(self.strings_path.read_text()) + + def update_strings(self, **kwargs) -> None: + """Update the integration strings.""" + print(f"Updating {self.domain} strings: {list(kwargs)}") + self.strings_path.write_text(json.dumps({**self.strings(), **kwargs}, indent=2)) diff --git a/script/scaffold/templates/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py similarity index 83% rename from script/scaffold/templates/integration/config_flow.py rename to script/scaffold/templates/config_flow/integration/config_flow.py index c05141ff0b009a..e08851f47a0358 100644 --- a/script/scaffold/templates/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -3,10 +3,9 @@ import voluptuous as vol -from homeassistant import core, config_entries +from homeassistant import core, config_entries, exceptions from .const import DOMAIN # pylint:disable=unused-import -from .error import CannotConnect, InvalidAuth _LOGGER = logging.getLogger(__name__) @@ -33,7 +32,7 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for NEW_NAME.""" VERSION = 1 - # TODO pick one of the available connection classes + # TODO pick one of the available connection classes in homeassistant/config_entries.py CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN async def async_step_user(self, user_input=None): @@ -55,3 +54,11 @@ async def async_step_user(self, user_input=None): return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/script/scaffold/templates/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py similarity index 97% rename from script/scaffold/templates/tests/test_config_flow.py rename to script/scaffold/templates/config_flow/tests/test_config_flow.py index 7735f497f80176..35d8a96ab2b796 100644 --- a/script/scaffold/templates/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -3,7 +3,7 @@ from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.const import DOMAIN -from homeassistant.components.NEW_DOMAIN.error import CannotConnect, InvalidAuth +from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth from tests.common import mock_coro diff --git a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py new file mode 100644 index 00000000000000..16d13aaa99ffc0 --- /dev/null +++ b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py @@ -0,0 +1,18 @@ +"""Config flow for NEW_NAME.""" +import my_pypi_dependency + +from homeassistant.helpers import config_entry_flow +from homeassistant import config_entries +from .const import DOMAIN + + +async def _async_has_devices(hass) -> bool: + """Return if there are devices that can be discovered.""" + # TODO Check if there are any devices that can be discovered in the network. + devices = await hass.async_add_executor_job(my_pypi_dependency.discover) + return len(devices) > 0 + + +config_entry_flow.register_discovery_flow( + DOMAIN, "NEW_NAME", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN +) diff --git a/script/scaffold/templates/integration/__init__.py b/script/scaffold/templates/integration/__init__.py deleted file mode 100644 index 356c7857d92bdb..00000000000000 --- a/script/scaffold/templates/integration/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -"""The NEW_NAME integration.""" - -from .const import DOMAIN - - -async def async_setup(hass, config): - """Set up the NEW_NAME integration.""" - hass.data[DOMAIN] = config.get(DOMAIN, {}) - return True - - -async def async_setup_entry(hass, entry): - """Set up a config entry for NEW_NAME.""" - # TODO forward the entry for each platform that you want to set up. - # hass.async_create_task( - # hass.config_entries.async_forward_entry_setup(entry, "media_player") - # ) - - return True diff --git a/script/scaffold/templates/integration/error.py b/script/scaffold/templates/integration/error.py deleted file mode 100644 index a99a32bb9501ab..00000000000000 --- a/script/scaffold/templates/integration/error.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Errors for the NEW_NAME integration.""" -from homeassistant.exceptions import HomeAssistantError - - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(HomeAssistantError): - """Error to indicate there is invalid auth.""" diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py new file mode 100644 index 00000000000000..7ab8b736782f62 --- /dev/null +++ b/script/scaffold/templates/integration/integration/__init__.py @@ -0,0 +1,12 @@ +"""The NEW_NAME integration.""" +import voluptuous as vol + +from .const import DOMAIN + + +CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}) + + +async def async_setup(hass, config): + """Set up the NEW_NAME integration.""" + return True diff --git a/script/scaffold/templates/integration/const.py b/script/scaffold/templates/integration/integration/const.py similarity index 100% rename from script/scaffold/templates/integration/const.py rename to script/scaffold/templates/integration/integration/const.py diff --git a/script/scaffold/templates/integration/manifest.json b/script/scaffold/templates/integration/integration/manifest.json similarity index 63% rename from script/scaffold/templates/integration/manifest.json rename to script/scaffold/templates/integration/integration/manifest.json index 7c1e141eef07be..cb4ecac61fb76c 100644 --- a/script/scaffold/templates/integration/manifest.json +++ b/script/scaffold/templates/integration/integration/manifest.json @@ -1,11 +1,11 @@ { "domain": "NEW_DOMAIN", "name": "NEW_NAME", - "config_flow": true, + "config_flow": false, "documentation": "https://www.home-assistant.io/components/NEW_DOMAIN", - "requirements": ["MANIFEST_NEW_REQUIREMENT"], + "requirements": [], "ssdp": {}, "homekit": {}, "dependencies": [], - "codeowners": ["NEW_CODEOWNER"] + "codeowners": [] } diff --git a/script/scaffold/templates/integration/strings.json b/script/scaffold/templates/integration/strings.json deleted file mode 100644 index 0f29967b286547..00000000000000 --- a/script/scaffold/templates/integration/strings.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "title": "NEW_NAME", - "step": { - "user": { - "title": "Connect to the device", - "data": { - "host": "Host" - } - } - }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "Device is already configured" - } - } -} diff --git a/script/scaffold/templates/tests/__init__.py b/script/scaffold/templates/tests/__init__.py deleted file mode 100644 index 081b6d86600012..00000000000000 --- a/script/scaffold/templates/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the NEW_NAME integration.""" From 2f277c4ea7b50ab9624d182bb729cdc2771d0706 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 08:43:55 +0200 Subject: [PATCH 0414/3953] Remove deprecated ups integration (ADR-0004) (#26824) --- .coveragerc | 1 - homeassistant/components/ups/__init__.py | 1 - homeassistant/components/ups/manifest.json | 10 -- homeassistant/components/ups/sensor.py | 126 --------------------- requirements_all.txt | 3 - 5 files changed, 141 deletions(-) delete mode 100644 homeassistant/components/ups/__init__.py delete mode 100644 homeassistant/components/ups/manifest.json delete mode 100644 homeassistant/components/ups/sensor.py diff --git a/.coveragerc b/.coveragerc index a4f52af76cefb1..38e3917734889d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -690,7 +690,6 @@ omit = homeassistant/components/upcloud/* homeassistant/components/upnp/* homeassistant/components/upc_connect/* - homeassistant/components/ups/sensor.py homeassistant/components/uptimerobot/binary_sensor.py homeassistant/components/uscis/sensor.py homeassistant/components/usps/* diff --git a/homeassistant/components/ups/__init__.py b/homeassistant/components/ups/__init__.py deleted file mode 100644 index 690d3102f9c5bf..00000000000000 --- a/homeassistant/components/ups/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The ups component.""" diff --git a/homeassistant/components/ups/manifest.json b/homeassistant/components/ups/manifest.json deleted file mode 100644 index 98db00c30948e1..00000000000000 --- a/homeassistant/components/ups/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "ups", - "name": "Ups", - "documentation": "https://www.home-assistant.io/components/ups", - "requirements": [ - "upsmychoice==1.0.6" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/ups/sensor.py b/homeassistant/components/ups/sensor.py deleted file mode 100644 index cfe35a9a63fc0c..00000000000000 --- a/homeassistant/components/ups/sensor.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Sensor for UPS packages.""" -import logging -from collections import defaultdict -from datetime import timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_NAME, - CONF_PASSWORD, - CONF_SCAN_INTERVAL, - CONF_USERNAME, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle, slugify -from homeassistant.util.dt import now, parse_date - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "ups" -COOKIE = "upsmychoice_cookies.pickle" -ICON = "mdi:package-variant-closed" -STATUS_DELIVERED = "delivered" - -SCAN_INTERVAL = timedelta(seconds=1800) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - } -) - - -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( - config.get(CONF_USERNAME), config.get(CONF_PASSWORD), cookie_path=cookie - ) - except upsmychoice.UPSError: - _LOGGER.exception("Could not connect to UPS My Choice") - return False - - add_entities( - [ - UPSSensor( - session, - config.get(CONF_NAME), - config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL), - ) - ], - True, - ) - - -class UPSSensor(Entity): - """UPS Sensor.""" - - def __init__(self, session, name, interval): - """Initialize the sensor.""" - self._session = session - self._name = name - self._attributes = None - self._state = None - self.update = Throttle(interval)(self._update) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name or DOMAIN - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "packages" - - def _update(self): - """Update device state.""" - import upsmychoice - - status_counts = defaultdict(int) - try: - for package in upsmychoice.get_packages(self._session): - status = slugify(package["status"]) - skip = ( - status == STATUS_DELIVERED - and parse_date(package["delivery_date"]) < now().date() - ) - if skip: - continue - status_counts[status] += 1 - except upsmychoice.UPSError: - _LOGGER.error("Could not connect to UPS My Choice account") - - self._attributes = {ATTR_ATTRIBUTION: upsmychoice.ATTRIBUTION} - self._attributes.update(status_counts) - self._state = sum(status_counts.values()) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def icon(self): - """Icon to use in the frontend.""" - return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 614362578e0085..0ca24c8e9d4df8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1913,9 +1913,6 @@ twilio==6.19.1 # homeassistant.components.upcloud upcloud-api==0.4.3 -# homeassistant.components.ups -upsmychoice==1.0.6 - # homeassistant.components.uscis uscisstatus==0.1.1 From 38ad573b962aeca7ffec9c226c6524111ff88d5b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 08:44:17 +0200 Subject: [PATCH 0415/3953] Remove deprecated usps integration (ADR-0004) (#26823) --- .coveragerc | 1 - homeassistant/components/usps/__init__.py | 96 --------------- homeassistant/components/usps/camera.py | 88 -------------- homeassistant/components/usps/manifest.json | 10 -- homeassistant/components/usps/sensor.py | 122 -------------------- requirements_all.txt | 3 - 6 files changed, 320 deletions(-) delete mode 100644 homeassistant/components/usps/__init__.py delete mode 100644 homeassistant/components/usps/camera.py delete mode 100644 homeassistant/components/usps/manifest.json delete mode 100644 homeassistant/components/usps/sensor.py diff --git a/.coveragerc b/.coveragerc index 38e3917734889d..06177f069c0799 100644 --- a/.coveragerc +++ b/.coveragerc @@ -692,7 +692,6 @@ omit = homeassistant/components/upc_connect/* homeassistant/components/uptimerobot/binary_sensor.py homeassistant/components/uscis/sensor.py - homeassistant/components/usps/* homeassistant/components/vallox/* homeassistant/components/vasttrafik/sensor.py homeassistant/components/velbus/__init__.py diff --git a/homeassistant/components/usps/__init__.py b/homeassistant/components/usps/__init__.py deleted file mode 100644 index 61da78fa6d7194..00000000000000 --- a/homeassistant/components/usps/__init__.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Support for USPS packages and mail.""" -from datetime import timedelta -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.util import Throttle -from homeassistant.util.dt import now - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "usps" -DATA_USPS = "data_usps" -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) -COOKIE = "usps_cookies.pickle" -CACHE = "usps_cache" -CONF_DRIVER = "driver" - -USPS_TYPE = ["sensor", "camera"] - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DOMAIN): cv.string, - vol.Optional(CONF_DRIVER): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -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) - name = conf.get(CONF_NAME) - driver = conf.get(CONF_DRIVER) - - import myusps - - try: - cookie = hass.config.path(COOKIE) - cache = hass.config.path(CACHE) - session = myusps.get_session( - username, password, cookie_path=cookie, cache_path=cache, driver=driver - ) - except myusps.USPSError: - _LOGGER.exception("Could not connect to My USPS") - return False - - hass.data[DATA_USPS] = USPSData(session, name) - - for component in USPS_TYPE: - discovery.load_platform(hass, component, DOMAIN, {}, config) - - return True - - -class USPSData: - """Stores the data retrieved from USPS. - - For each entity to use, acts as the single point responsible for fetching - updates from the server. - """ - - def __init__(self, session, name): - """Initialize the data object.""" - self.session = session - self.name = name - self.packages = [] - self.mail = [] - self.attribution = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self, **kwargs): - """Fetch the latest info from USPS.""" - import myusps - - self.packages = myusps.get_packages(self.session) - self.mail = myusps.get_mail(self.session, now().date()) - self.attribution = myusps.ATTRIBUTION - _LOGGER.debug("Mail, request date: %s, list: %s", now().date(), self.mail) - _LOGGER.debug("Package list: %s", self.packages) diff --git a/homeassistant/components/usps/camera.py b/homeassistant/components/usps/camera.py deleted file mode 100644 index 3141314b049cb5..00000000000000 --- a/homeassistant/components/usps/camera.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Support for a camera made up of USPS mail images.""" -from datetime import timedelta -import logging - -from homeassistant.components.camera import Camera - -from . import DATA_USPS - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=10) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up USPS mail camera.""" - if discovery_info is None: - return - - usps = hass.data[DATA_USPS] - add_entities([USPSCamera(usps)]) - - -class USPSCamera(Camera): - """Representation of the images available from USPS.""" - - def __init__(self, usps): - """Initialize the USPS camera images.""" - super().__init__() - - self._usps = usps - self._name = self._usps.name - self._session = self._usps.session - - self._mail_img = [] - self._last_mail = None - self._mail_index = 0 - self._mail_count = 0 - - self._timer = None - - def camera_image(self): - """Update the camera's image if it has changed.""" - self._usps.update() - try: - self._mail_count = len(self._usps.mail) - except TypeError: - # No mail - return None - - if self._usps.mail != self._last_mail: - # Mail items must have changed - self._mail_img = [] - if len(self._usps.mail) >= 1: - self._last_mail = self._usps.mail - for article in self._usps.mail: - _LOGGER.debug("Fetching article image: %s", article) - img = self._session.get(article["image"]).content - self._mail_img.append(img) - - try: - return self._mail_img[self._mail_index] - except IndexError: - return None - - @property - def name(self): - """Return the name of this camera.""" - return f"{self._name} mail" - - @property - def model(self): - """Return date of mail as model.""" - try: - return "Date: {}".format(str(self._usps.mail[0]["date"])) - except IndexError: - return None - - @property - def should_poll(self): - """Update the mail image index periodically.""" - return True - - def update(self): - """Update mail image index.""" - if self._mail_index < (self._mail_count - 1): - self._mail_index += 1 - else: - self._mail_index = 0 diff --git a/homeassistant/components/usps/manifest.json b/homeassistant/components/usps/manifest.json deleted file mode 100644 index 9e2f8886d3acbd..00000000000000 --- a/homeassistant/components/usps/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "usps", - "name": "Usps", - "documentation": "https://www.home-assistant.io/components/usps", - "requirements": [ - "myusps==1.3.2" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/usps/sensor.py b/homeassistant/components/usps/sensor.py deleted file mode 100644 index 7e26e6c9e5c793..00000000000000 --- a/homeassistant/components/usps/sensor.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Sensor for USPS packages.""" -from collections import defaultdict -import logging - -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DATE -from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify -from homeassistant.util.dt import now - -from . import DATA_USPS - -_LOGGER = logging.getLogger(__name__) - -STATUS_DELIVERED = "delivered" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the USPS platform.""" - if discovery_info is None: - return - - usps = hass.data[DATA_USPS] - add_entities([USPSPackageSensor(usps), USPSMailSensor(usps)], True) - - -class USPSPackageSensor(Entity): - """USPS Package Sensor.""" - - def __init__(self, usps): - """Initialize the sensor.""" - self._usps = usps - self._name = self._usps.name - self._attributes = None - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} packages" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Update device state.""" - self._usps.update() - status_counts = defaultdict(int) - for package in self._usps.packages: - status = slugify(package["primary_status"]) - if status == STATUS_DELIVERED and package["delivery_date"] < now().date(): - continue - status_counts[status] += 1 - self._attributes = {ATTR_ATTRIBUTION: self._usps.attribution} - self._attributes.update(status_counts) - self._state = sum(status_counts.values()) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:package-variant-closed" - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "packages" - - -class USPSMailSensor(Entity): - """USPS Mail Sensor.""" - - def __init__(self, usps): - """Initialize the sensor.""" - self._usps = usps - self._name = self._usps.name - self._attributes = None - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} mail" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Update device state.""" - self._usps.update() - if self._usps.mail is not None: - self._state = len(self._usps.mail) - else: - self._state = 0 - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attr = {} - attr[ATTR_ATTRIBUTION] = self._usps.attribution - try: - attr[ATTR_DATE] = str(self._usps.mail[0]["date"]) - except IndexError: - pass - return attr - - @property - def icon(self): - """Icon to use in the frontend.""" - return "mdi:mailbox" - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "pieces" diff --git a/requirements_all.txt b/requirements_all.txt index 0ca24c8e9d4df8..0a6f1979a91cd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -836,9 +836,6 @@ mychevy==1.2.0 # homeassistant.components.mycroft mycroftapi==2.0 -# homeassistant.components.usps -myusps==1.3.2 - # homeassistant.components.n26 n26==0.2.7 From 5c7f869f9b22e7afdbae70363972e87d427a5b1b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 08:44:38 +0200 Subject: [PATCH 0416/3953] Remove deprecated sytadin integration (ADR-0004) (#26819) * Remove deprecated sytadin integration (ADR-0004) * Update code owners file * Cleanup coveragerc --- .coveragerc | 1 - CODEOWNERS | 1 - homeassistant/components/sytadin/__init__.py | 1 - .../components/sytadin/manifest.json | 12 -- homeassistant/components/sytadin/sensor.py | 151 ------------------ requirements_all.txt | 1 - 6 files changed, 167 deletions(-) delete mode 100644 homeassistant/components/sytadin/__init__.py delete mode 100644 homeassistant/components/sytadin/manifest.json delete mode 100644 homeassistant/components/sytadin/sensor.py diff --git a/.coveragerc b/.coveragerc index 06177f069c0799..b6bdcc2704372f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -629,7 +629,6 @@ omit = homeassistant/components/synologydsm/sensor.py homeassistant/components/syslog/notify.py homeassistant/components/systemmonitor/sensor.py - homeassistant/components/sytadin/sensor.py homeassistant/components/tado/* homeassistant/components/tado/device_tracker.py homeassistant/components/tahoma/* diff --git a/CODEOWNERS b/CODEOWNERS index 8fe47035912d07..ae072cd092ccb4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -269,7 +269,6 @@ homeassistant/components/switchmate/* @danielhiversen homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff -homeassistant/components/sytadin/* @gautric homeassistant/components/tahoma/* @philklei homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike diff --git a/homeassistant/components/sytadin/__init__.py b/homeassistant/components/sytadin/__init__.py deleted file mode 100644 index 5243fe379a774e..00000000000000 --- a/homeassistant/components/sytadin/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The sytadin component.""" diff --git a/homeassistant/components/sytadin/manifest.json b/homeassistant/components/sytadin/manifest.json deleted file mode 100644 index c1453d88d81448..00000000000000 --- a/homeassistant/components/sytadin/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "sytadin", - "name": "Sytadin", - "documentation": "https://www.home-assistant.io/components/sytadin", - "requirements": [ - "beautifulsoup4==4.8.0" - ], - "dependencies": [], - "codeowners": [ - "@gautric" - ] -} diff --git a/homeassistant/components/sytadin/sensor.py b/homeassistant/components/sytadin/sensor.py deleted file mode 100644 index b7c94933a39974..00000000000000 --- a/homeassistant/components/sytadin/sensor.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Support for Sytadin Traffic, French Traffic Supervision.""" -import logging -import re -from datetime import timedelta - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - LENGTH_KILOMETERS, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - ATTR_ATTRIBUTION, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -URL = "http://www.sytadin.fr/sys/barometres_de_la_circulation.jsp.html" - -ATTRIBUTION = "Data provided by Direction des routes Île-de-France (DiRIF)" - -DEFAULT_NAME = "Sytadin" -REGEX = r"(\d*\.\d+|\d+)" - -OPTION_TRAFFIC_JAM = "traffic_jam" -OPTION_MEAN_VELOCITY = "mean_velocity" -OPTION_CONGESTION = "congestion" - -SENSOR_TYPES = { - OPTION_CONGESTION: ["Congestion", ""], - OPTION_MEAN_VELOCITY: ["Mean Velocity", LENGTH_KILOMETERS + "/h"], - OPTION_TRAFFIC_JAM: ["Traffic Jam", LENGTH_KILOMETERS], -} - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=[OPTION_TRAFFIC_JAM]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -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) - - dev = [] - for option in config.get(CONF_MONITORED_CONDITIONS): - _LOGGER.debug("Sensor device - %s", option) - dev.append( - SytadinSensor( - sytadin, name, option, SENSOR_TYPES[option][0], SENSOR_TYPES[option][1] - ) - ) - add_entities(dev, True) - - -class SytadinSensor(Entity): - """Representation of a Sytadin Sensor.""" - - def __init__(self, data, name, sensor_type, option, unit): - """Initialize the sensor.""" - self.data = data - self._state = None - self._name = name - self._option = option - self._type = sensor_type - self._unit = unit - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} {self._option}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - def update(self): - """Fetch new state data for the sensor.""" - self.data.update() - - if self.data is None: - return - - if self._type == OPTION_TRAFFIC_JAM: - self._state = self.data.traffic_jam - elif self._type == OPTION_MEAN_VELOCITY: - self._state = self.data.mean_velocity - elif self._type == OPTION_CONGESTION: - self._state = self.data.congestion - - -class SytadinData: - """The class for handling the data retrieval.""" - - def __init__(self, resource): - """Initialize the data object.""" - self._resource = resource - self.data = None - self.traffic_jam = self.mean_velocity = self.congestion = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from the Sytadin.""" - from bs4 import BeautifulSoup - - try: - raw_html = requests.get(self._resource, timeout=10).text - data = BeautifulSoup(raw_html, "html.parser") - - values = data.select(".barometre_valeur") - parse_traffic_jam = re.search(REGEX, values[0].text) - if parse_traffic_jam: - self.traffic_jam = parse_traffic_jam.group() - parse_mean_velocity = re.search(REGEX, values[1].text) - if parse_mean_velocity: - self.mean_velocity = parse_mean_velocity.group() - parse_congestion = re.search(REGEX, values[2].text) - if parse_congestion: - self.congestion = parse_congestion.group() - except requests.exceptions.ConnectionError: - _LOGGER.error("Connection error") - self.data = None diff --git a/requirements_all.txt b/requirements_all.txt index 0a6f1979a91cd0..a3c1548bc1ac04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -262,7 +262,6 @@ batinfo==0.4.2 # homeassistant.components.linksys_ap # homeassistant.components.scrape -# homeassistant.components.sytadin beautifulsoup4==4.8.0 # homeassistant.components.beewi_smartclim From 5c0fa35d4a6a8733d800884a6e8bd4f1273d0381 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Mon, 23 Sep 2019 11:50:18 +0200 Subject: [PATCH 0417/3953] Add here_travel_time (#24603) * Add here_travel_time * Bump herepy version to 0.6.2 * Update requirements_all.txt * Disable pylint and catch errors * Add herepy to requirements_test_all * Correctly place test req for herepy * use homeassistant.const.LENGTH_METERS * Implemented Requested Changes * Better error message for cryptic error code * add requested changes * add_entities instead of async * Add route attr and distance in km instead of m * fix linting errors * attribute duration in minutes instead of seconds * Correct pattern for longitude * dont split attribute but rather local var * move strings to const and use travelTime * Add tests * Add route for pedestrian and public * fix public transport route generation * remove print statement * Standalone pytest * Use hass fixture and increase test cov _resolve_zone is redundant * Clean up redundant code * Add type annotations * Readd _resolve_zone and add a test for it * Full test cov * use caplog * Add origin/destination attributes According to https://github.com/home-assistant/home-assistant/pull/24956 * Add mode: bicycle * black * Add mode: publicTransportTimeTable * Fix error for publicTransportTimeTable Switch route_mode and travel_mode in api request. * split up config options * More type hints * implement *_entity_id * align attributes with google_travel_time * route in lib apply requested changes * Update requirements_all.txt * remove DATA_KEY * Use ATTR_MODE * add attribution * Only add attribution if not none * Add debug log for raw response * Add _build_hass_attribution * clearer var names in credentials check * async _are_valid_client_credentials --- CODEOWNERS | 1 + .../components/here_travel_time/__init__.py | 1 + .../components/here_travel_time/manifest.json | 12 + .../components/here_travel_time/sensor.py | 431 ++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/here_travel_time/__init__.py | 1 + .../here_travel_time/test_sensor.py | 947 ++++++++++++++++++ .../attribution_response.json | 276 +++++ .../here_travel_time/bike_response.json | 274 +++++ .../car_enabled_response.json | 298 ++++++ .../here_travel_time/car_response.json | 299 ++++++ .../car_shortest_response.json | 231 +++++ .../here_travel_time/pedestrian_response.json | 308 ++++++ .../here_travel_time/public_response.json | 294 ++++++ .../public_time_table_response.json | 308 ++++++ .../routing_error_invalid_credentials.json | 15 + .../routing_error_no_route_found.json | 21 + .../here_travel_time/truck_response.json | 187 ++++ 20 files changed, 3911 insertions(+) create mode 100755 homeassistant/components/here_travel_time/__init__.py create mode 100755 homeassistant/components/here_travel_time/manifest.json create mode 100755 homeassistant/components/here_travel_time/sensor.py create mode 100644 tests/components/here_travel_time/__init__.py create mode 100644 tests/components/here_travel_time/test_sensor.py create mode 100644 tests/fixtures/here_travel_time/attribution_response.json create mode 100644 tests/fixtures/here_travel_time/bike_response.json create mode 100644 tests/fixtures/here_travel_time/car_enabled_response.json create mode 100644 tests/fixtures/here_travel_time/car_response.json create mode 100644 tests/fixtures/here_travel_time/car_shortest_response.json create mode 100644 tests/fixtures/here_travel_time/pedestrian_response.json create mode 100644 tests/fixtures/here_travel_time/public_response.json create mode 100644 tests/fixtures/here_travel_time/public_time_table_response.json create mode 100644 tests/fixtures/here_travel_time/routing_error_invalid_credentials.json create mode 100644 tests/fixtures/here_travel_time/routing_error_no_route_found.json create mode 100644 tests/fixtures/here_travel_time/truck_response.json diff --git a/CODEOWNERS b/CODEOWNERS index ae072cd092ccb4..7e05cdf0b399e7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -115,6 +115,7 @@ homeassistant/components/gtfs/* @robbiet480 homeassistant/components/harmony/* @ehendrix23 homeassistant/components/hassio/* @home-assistant/hass-io homeassistant/components/heos/* @andrewsayre +homeassistant/components/here_travel_time/* @eifinger homeassistant/components/hikvision/* @mezz64 homeassistant/components/hikvisioncam/* @fbradyirl homeassistant/components/history/* @home-assistant/core diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py new file mode 100755 index 00000000000000..9a5c8ec32aca43 --- /dev/null +++ b/homeassistant/components/here_travel_time/__init__.py @@ -0,0 +1 @@ +"""The here_travel_time component.""" diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json new file mode 100755 index 00000000000000..e26e2e1d6ea57c --- /dev/null +++ b/homeassistant/components/here_travel_time/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "here_travel_time", + "name": "HERE travel time", + "documentation": "https://www.home-assistant.io/components/here_travel_time", + "requirements": [ + "herepy==0.6.3.1" + ], + "dependencies": [], + "codeowners": [ + "@eifinger" + ] + } diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py new file mode 100755 index 00000000000000..ba4908fe85c3ac --- /dev/null +++ b/homeassistant/components/here_travel_time/sensor.py @@ -0,0 +1,431 @@ +"""Support for HERE travel time sensors.""" +from datetime import timedelta +import logging +from typing import Callable, Dict, Optional, Union + +import herepy +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_MODE, + CONF_NAME, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import location +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +CONF_DESTINATION_LATITUDE = "destination_latitude" +CONF_DESTINATION_LONGITUDE = "destination_longitude" +CONF_DESTINATION_ENTITY_ID = "destination_entity_id" +CONF_ORIGIN_LATITUDE = "origin_latitude" +CONF_ORIGIN_LONGITUDE = "origin_longitude" +CONF_ORIGIN_ENTITY_ID = "origin_entity_id" +CONF_APP_ID = "app_id" +CONF_APP_CODE = "app_code" +CONF_TRAFFIC_MODE = "traffic_mode" +CONF_ROUTE_MODE = "route_mode" + +DEFAULT_NAME = "HERE Travel Time" + +TRAVEL_MODE_BICYCLE = "bicycle" +TRAVEL_MODE_CAR = "car" +TRAVEL_MODE_PEDESTRIAN = "pedestrian" +TRAVEL_MODE_PUBLIC = "publicTransport" +TRAVEL_MODE_PUBLIC_TIME_TABLE = "publicTransportTimeTable" +TRAVEL_MODE_TRUCK = "truck" +TRAVEL_MODE = [ + TRAVEL_MODE_BICYCLE, + TRAVEL_MODE_CAR, + TRAVEL_MODE_PEDESTRIAN, + TRAVEL_MODE_PUBLIC, + TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_TRUCK, +] + +TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE] +TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] +TRAVEL_MODES_NON_VEHICLE = [TRAVEL_MODE_BICYCLE, TRAVEL_MODE_PEDESTRIAN] + +TRAFFIC_MODE_ENABLED = "traffic_enabled" +TRAFFIC_MODE_DISABLED = "traffic_disabled" + +ROUTE_MODE_FASTEST = "fastest" +ROUTE_MODE_SHORTEST = "shortest" +ROUTE_MODE = [ROUTE_MODE_FASTEST, ROUTE_MODE_SHORTEST] + +ICON_BICYCLE = "mdi:bike" +ICON_CAR = "mdi:car" +ICON_PEDESTRIAN = "mdi:walk" +ICON_PUBLIC = "mdi:bus" +ICON_TRUCK = "mdi:truck" + +UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] + +ATTR_DURATION = "duration" +ATTR_DISTANCE = "distance" +ATTR_ROUTE = "route" +ATTR_ORIGIN = "origin" +ATTR_DESTINATION = "destination" + +ATTR_MODE = "mode" +ATTR_UNIT_SYSTEM = CONF_UNIT_SYSTEM +ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE + +ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic" +ATTR_ORIGIN_NAME = "origin_name" +ATTR_DESTINATION_NAME = "destination_name" + +UNIT_OF_MEASUREMENT = "min" + +SCAN_INTERVAL = timedelta(minutes=5) + +TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] + +NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" + +COORDINATE_SCHEMA = vol.Schema( + { + vol.Inclusive(CONF_DESTINATION_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_DESTINATION_LONGITUDE, "coordinates"): cv.longitude, + } +) + +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_DESTINATION_LATITUDE, CONF_DESTINATION_ENTITY_ID), + cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_APP_ID): cv.string, + vol.Required(CONF_APP_CODE): cv.string, + vol.Inclusive( + CONF_DESTINATION_LATITUDE, "destination_coordinates" + ): cv.latitude, + vol.Inclusive( + CONF_DESTINATION_LONGITUDE, "destination_coordinates" + ): cv.longitude, + vol.Exclusive(CONF_DESTINATION_LATITUDE, "destination"): cv.latitude, + vol.Exclusive(CONF_DESTINATION_ENTITY_ID, "destination"): cv.entity_id, + vol.Inclusive(CONF_ORIGIN_LATITUDE, "origin_coordinates"): cv.latitude, + vol.Inclusive(CONF_ORIGIN_LONGITUDE, "origin_coordinates"): cv.longitude, + vol.Exclusive(CONF_ORIGIN_LATITUDE, "origin"): cv.latitude, + vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODE), + vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In( + ROUTE_MODE + ), + vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean, + vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), + } + ), +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: Dict[str, Union[str, bool]], + async_add_entities: Callable, + discovery_info: None = None, +) -> None: + """Set up the HERE travel time platform.""" + + app_id = config[CONF_APP_ID] + app_code = config[CONF_APP_CODE] + here_client = herepy.RoutingApi(app_id, app_code) + + if not await hass.async_add_executor_job( + _are_valid_client_credentials, here_client + ): + _LOGGER.error( + "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." + ) + return + + if config.get(CONF_ORIGIN_LATITUDE) is not None: + origin = f"{config[CONF_ORIGIN_LATITUDE]},{config[CONF_ORIGIN_LONGITUDE]}" + else: + origin = config[CONF_ORIGIN_ENTITY_ID] + + if config.get(CONF_DESTINATION_LATITUDE) is not None: + destination = ( + f"{config[CONF_DESTINATION_LATITUDE]},{config[CONF_DESTINATION_LONGITUDE]}" + ) + else: + destination = config[CONF_DESTINATION_ENTITY_ID] + + travel_mode = config[CONF_MODE] + traffic_mode = config[CONF_TRAFFIC_MODE] + route_mode = config[CONF_ROUTE_MODE] + name = config[CONF_NAME] + units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name) + + here_data = HERETravelTimeData( + here_client, travel_mode, traffic_mode, route_mode, units + ) + + sensor = HERETravelTimeSensor(name, origin, destination, here_data) + + async_add_entities([sensor], True) + + +def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool: + """Check if the provided credentials are correct using defaults.""" + known_working_origin = [38.9, -77.04833] + known_working_destination = [39.0, -77.1] + try: + here_client.car_route( + known_working_origin, + known_working_destination, + [ + herepy.RouteMode[ROUTE_MODE_FASTEST], + herepy.RouteMode[TRAVEL_MODE_CAR], + herepy.RouteMode[TRAFFIC_MODE_DISABLED], + ], + ) + except herepy.InvalidCredentialsError: + return False + return True + + +class HERETravelTimeSensor(Entity): + """Representation of a HERE travel time sensor.""" + + def __init__( + self, name: str, origin: str, destination: str, here_data: "HERETravelTimeData" + ) -> None: + """Initialize the sensor.""" + self._name = name + self._here_data = here_data + self._unit_of_measurement = UNIT_OF_MEASUREMENT + self._origin_entity_id = None + self._destination_entity_id = None + self._attrs = { + ATTR_UNIT_SYSTEM: self._here_data.units, + ATTR_MODE: self._here_data.travel_mode, + ATTR_TRAFFIC_MODE: self._here_data.traffic_mode, + } + + # Check if location is a trackable entity + if origin.split(".", 1)[0] in TRACKABLE_DOMAINS: + self._origin_entity_id = origin + else: + self._here_data.origin = origin + + if destination.split(".", 1)[0] in TRACKABLE_DOMAINS: + self._destination_entity_id = destination + else: + self._here_data.destination = destination + + @property + def state(self) -> Optional[str]: + """Return the state of the sensor.""" + if self._here_data.traffic_mode: + if self._here_data.traffic_time is not None: + return str(round(self._here_data.traffic_time / 60)) + if self._here_data.base_time is not None: + return str(round(self._here_data.base_time / 60)) + + return None + + @property + def name(self) -> str: + """Get the name of the sensor.""" + return self._name + + @property + def device_state_attributes( + self + ) -> Optional[Dict[str, Union[None, float, str, bool]]]: + """Return the state attributes.""" + if self._here_data.base_time is None: + return None + + res = self._attrs + if self._here_data.attribution is not None: + res[ATTR_ATTRIBUTION] = self._here_data.attribution + res[ATTR_DURATION] = self._here_data.base_time / 60 + res[ATTR_DISTANCE] = self._here_data.distance + res[ATTR_ROUTE] = self._here_data.route + res[ATTR_DURATION_IN_TRAFFIC] = self._here_data.traffic_time / 60 + res[ATTR_ORIGIN] = self._here_data.origin + res[ATTR_DESTINATION] = self._here_data.destination + res[ATTR_ORIGIN_NAME] = self._here_data.origin_name + res[ATTR_DESTINATION_NAME] = self._here_data.destination_name + return res + + @property + def unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + @property + def icon(self) -> str: + """Icon to use in the frontend depending on travel_mode.""" + if self._here_data.travel_mode == TRAVEL_MODE_BICYCLE: + return ICON_BICYCLE + if self._here_data.travel_mode == TRAVEL_MODE_PEDESTRIAN: + return ICON_PEDESTRIAN + if self._here_data.travel_mode in TRAVEL_MODES_PUBLIC: + return ICON_PUBLIC + if self._here_data.travel_mode == TRAVEL_MODE_TRUCK: + return ICON_TRUCK + return ICON_CAR + + async def async_update(self) -> None: + """Update Sensor Information.""" + # Convert device_trackers to HERE friendly location + if self._origin_entity_id is not None: + self._here_data.origin = await self._get_location_from_entity( + self._origin_entity_id + ) + + if self._destination_entity_id is not None: + self._here_data.destination = await self._get_location_from_entity( + self._destination_entity_id + ) + + await self.hass.async_add_executor_job(self._here_data.update) + + async def _get_location_from_entity(self, entity_id: str) -> Optional[str]: + """Get the location from the entity state or attributes.""" + entity = self.hass.states.get(entity_id) + + if entity is None: + _LOGGER.error("Unable to find entity %s", entity_id) + return None + + # Check if the entity has location attributes + if location.has_location(entity): + return self._get_location_from_attributes(entity) + + # Check if device is in a zone + zone_entity = self.hass.states.get("zone.{}".format(entity.state)) + if location.has_location(zone_entity): + _LOGGER.debug( + "%s is in %s, getting zone location", entity_id, zone_entity.entity_id + ) + return self._get_location_from_attributes(zone_entity) + + # If zone was not found in state then use the state as the location + if entity_id.startswith("sensor."): + return entity.state + + @staticmethod + def _get_location_from_attributes(entity: State) -> str: + """Get the lat/long string from an entities attributes.""" + attr = entity.attributes + return "{},{}".format(attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)) + + +class HERETravelTimeData: + """HERETravelTime data object.""" + + def __init__( + self, + here_client: herepy.RoutingApi, + travel_mode: str, + traffic_mode: bool, + route_mode: str, + units: str, + ) -> None: + """Initialize herepy.""" + self.origin = None + self.destination = None + self.travel_mode = travel_mode + self.traffic_mode = traffic_mode + self.route_mode = route_mode + self.attribution = None + self.traffic_time = None + self.distance = None + self.route = None + self.base_time = None + self.origin_name = None + self.destination_name = None + self.units = units + self._client = here_client + + def update(self) -> None: + """Get the latest data from HERE.""" + if self.traffic_mode: + traffic_mode = TRAFFIC_MODE_ENABLED + else: + traffic_mode = TRAFFIC_MODE_DISABLED + + if self.destination is not None and self.origin is not None: + # Convert location to HERE friendly location + destination = self.destination.split(",") + origin = self.origin.split(",") + + _LOGGER.debug( + "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s", + origin, + destination, + herepy.RouteMode[self.route_mode], + herepy.RouteMode[self.travel_mode], + herepy.RouteMode[traffic_mode], + ) + try: + response = self._client.car_route( + origin, + destination, + [ + herepy.RouteMode[self.route_mode], + herepy.RouteMode[self.travel_mode], + herepy.RouteMode[traffic_mode], + ], + ) + except herepy.NoRouteFoundError: + # Better error message for cryptic no route error codes + _LOGGER.error(NO_ROUTE_ERROR_MESSAGE) + return + + _LOGGER.debug("Raw response is: %s", response.response) + + # pylint: disable=no-member + source_attribution = response.response.get("sourceAttribution") + if source_attribution is not None: + self.attribution = self._build_hass_attribution(source_attribution) + # pylint: disable=no-member + route = response.response["route"] + summary = route[0]["summary"] + waypoint = route[0]["waypoint"] + self.base_time = summary["baseTime"] + if self.travel_mode in TRAVEL_MODES_VEHICLE: + self.traffic_time = summary["trafficTime"] + else: + self.traffic_time = self.base_time + distance = summary["distance"] + if self.units == CONF_UNIT_SYSTEM_IMPERIAL: + # Convert to miles. + self.distance = distance / 1609.344 + else: + # Convert to kilometers + self.distance = distance / 1000 + # pylint: disable=no-member + self.route = response.route_short + self.origin_name = waypoint[0]["mappedRoadName"] + self.destination_name = waypoint[1]["mappedRoadName"] + + @staticmethod + def _build_hass_attribution(source_attribution: Dict) -> Optional[str]: + """Build a hass frontend ready string out of the sourceAttribution.""" + suppliers = source_attribution.get("supplier") + if suppliers is not None: + supplier_titles = [] + for supplier in suppliers: + title = supplier.get("title") + if title is not None: + supplier_titles.append(title) + joined_supplier_titles = ",".join(supplier_titles) + attribution = f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." + return attribution diff --git a/requirements_all.txt b/requirements_all.txt index a3c1548bc1ac04..bf217688d266e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -619,6 +619,9 @@ hdate==0.9.0 # homeassistant.components.heatmiser heatmiserV3==0.9.1 +# homeassistant.components.here_travel_time +herepy==0.6.3.1 + # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b4d7fbc089a0d..9e846c9c416ac1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -171,6 +171,9 @@ hbmqtt==0.9.5 # homeassistant.components.jewish_calendar hdate==0.9.0 +# homeassistant.components.here_travel_time +herepy==0.6.3.1 + # homeassistant.components.pi_hole hole==0.5.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 384d50bccef729..d74a57d678d494 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -87,6 +87,7 @@ "haversine", "hbmqtt", "hdate", + "herepy", "hole", "holidays", "home-assistant-frontend", diff --git a/tests/components/here_travel_time/__init__.py b/tests/components/here_travel_time/__init__.py new file mode 100644 index 00000000000000..ac0ec709654e10 --- /dev/null +++ b/tests/components/here_travel_time/__init__.py @@ -0,0 +1 @@ +"""Tests for here_travel_time component.""" diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py new file mode 100644 index 00000000000000..783209690a389c --- /dev/null +++ b/tests/components/here_travel_time/test_sensor.py @@ -0,0 +1,947 @@ +"""The test for the here_travel_time sensor platform.""" +import logging +from unittest.mock import patch +import urllib + +import herepy +import pytest + +from homeassistant.components.here_travel_time.sensor import ( + ATTR_ATTRIBUTION, + ATTR_DESTINATION, + ATTR_DESTINATION_NAME, + ATTR_DISTANCE, + ATTR_DURATION, + ATTR_DURATION_IN_TRAFFIC, + ATTR_ORIGIN, + ATTR_ORIGIN_NAME, + ATTR_ROUTE, + CONF_MODE, + CONF_TRAFFIC_MODE, + CONF_UNIT_SYSTEM, + ICON_BICYCLE, + ICON_CAR, + ICON_PEDESTRIAN, + ICON_PUBLIC, + ICON_TRUCK, + NO_ROUTE_ERROR_MESSAGE, + ROUTE_MODE_FASTEST, + ROUTE_MODE_SHORTEST, + SCAN_INTERVAL, + TRAFFIC_MODE_DISABLED, + TRAFFIC_MODE_ENABLED, + TRAVEL_MODE_BICYCLE, + TRAVEL_MODE_CAR, + TRAVEL_MODE_PEDESTRIAN, + TRAVEL_MODE_PUBLIC, + TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODE_TRUCK, + UNIT_OF_MEASUREMENT, +) +from homeassistant.const import ATTR_ICON +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed, load_fixture + +DOMAIN = "sensor" + +PLATFORM = "here_travel_time" + +APP_ID = "test" +APP_CODE = "test" + +TRUCK_ORIGIN_LATITUDE = "41.9798" +TRUCK_ORIGIN_LONGITUDE = "-87.8801" +TRUCK_DESTINATION_LATITUDE = "41.9043" +TRUCK_DESTINATION_LONGITUDE = "-87.9216" + +BIKE_ORIGIN_LATITUDE = "41.9798" +BIKE_ORIGIN_LONGITUDE = "-87.8801" +BIKE_DESTINATION_LATITUDE = "41.9043" +BIKE_DESTINATION_LONGITUDE = "-87.9216" + +CAR_ORIGIN_LATITUDE = "38.9" +CAR_ORIGIN_LONGITUDE = "-77.04833" +CAR_DESTINATION_LATITUDE = "39.0" +CAR_DESTINATION_LONGITUDE = "-77.1" + + +def _build_mock_url(origin, destination, modes, app_id, app_code, departure): + """Construct a url for HERE.""" + base_url = "https://route.cit.api.here.com/routing/7.2/calculateroute.json?" + parameters = { + "waypoint0": origin, + "waypoint1": destination, + "mode": ";".join(str(herepy.RouteMode[mode]) for mode in modes), + "app_id": app_id, + "app_code": app_code, + "departure": departure, + } + url = base_url + urllib.parse.urlencode(parameters) + return url + + +def _assert_truck_sensor(sensor): + """Assert that states and attributes are correct for truck_response.""" + assert sensor.state == "14" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 13.533333333333333 + assert sensor.attributes.get(ATTR_DISTANCE) == 13.049 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "I-190; I-294 S - Tri-State Tollway; I-290 W - Eisenhower Expy W; " + "IL-64 W - E North Ave; I-290 E - Eisenhower Expy E; I-290" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 13.533333333333333 + assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( + [TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE] + ) + assert sensor.attributes.get(ATTR_DESTINATION) == ",".join( + [TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE] + ) + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Eisenhower Expy E" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_TRUCK + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_TRUCK + + +@pytest.fixture +def requests_mock_credentials_check(requests_mock): + """Add the url used in the api validation to all requests mock.""" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url( + ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), + ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), + modes, + APP_ID, + APP_CODE, + "now", + ) + requests_mock.get( + response_url, text=load_fixture("here_travel_time/car_response.json") + ) + return requests_mock + + +@pytest.fixture +def requests_mock_truck_response(requests_mock_credentials_check): + """Return a requests_mock for truck respones.""" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_TRUCK, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url( + ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]), + ",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]), + modes, + APP_ID, + APP_CODE, + "now", + ) + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/truck_response.json") + ) + + +@pytest.fixture +def requests_mock_car_disabled_response(requests_mock_credentials_check): + """Return a requests_mock for truck respones.""" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url( + ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), + ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), + modes, + APP_ID, + APP_CODE, + "now", + ) + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/car_response.json") + ) + + +async def test_car(hass, requests_mock_car_disabled_response): + """Test that car works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.state == "30" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 30.05 + assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "US-29 - K St NW; US-29 - Whitehurst Fwy; " + "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 31.016666666666666 + assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( + [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE] + ) + assert sensor.attributes.get(ATTR_DESTINATION) == ",".join( + [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE] + ) + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "22nd St NW" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Service Rd S" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_CAR + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_CAR + + # Test traffic mode disabled + assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( + ATTR_DURATION_IN_TRAFFIC + ) + + +async def test_traffic_mode_enabled(hass, requests_mock_credentials_check): + """Test that traffic mode enabled works.""" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_ENABLED] + response_url = _build_mock_url( + ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), + ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), + modes, + APP_ID, + APP_CODE, + "now", + ) + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/car_enabled_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "traffic_mode": True, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + + # Test traffic mode enabled + assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( + ATTR_DURATION_IN_TRAFFIC + ) + + +async def test_imperial(hass, requests_mock_car_disabled_response): + """Test that imperial units work.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "unit_system": "imperial", + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.attributes.get(ATTR_DISTANCE) == 14.852635608048994 + + +async def test_route_mode_shortest(hass, requests_mock_credentials_check): + """Test that route mode shortest works.""" + origin = "38.902981,-77.048338" + destination = "39.042158,-77.119116" + modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/car_shortest_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "route_mode": ROUTE_MODE_SHORTEST, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.attributes.get(ATTR_DISTANCE) == 18.388 + + +async def test_route_mode_fastest(hass, requests_mock_credentials_check): + """Test that route mode fastest works.""" + origin = "38.902981,-77.048338" + destination = "39.042158,-77.119116" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_ENABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/car_enabled_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "traffic_mode": True, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.attributes.get(ATTR_DISTANCE) == 23.381 + + +async def test_truck(hass, requests_mock_truck_response): + """Test that truck works.""" + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": TRUCK_ORIGIN_LATITUDE, + "origin_longitude": TRUCK_ORIGIN_LONGITUDE, + "destination_latitude": TRUCK_DESTINATION_LATITUDE, + "destination_longitude": TRUCK_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + +async def test_public_transport(hass, requests_mock_credentials_check): + """Test that publicTransport works.""" + origin = "41.9798,-87.8801" + destination = "41.9043,-87.9216" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/public_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_PUBLIC, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.state == "89" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 89.16666666666667 + assert sensor.attributes.get(ATTR_DISTANCE) == 22.325 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "332 - Palmer/Schiller; 332 - Cargo Rd./Delta Cargo; " "332 - Palmer/Schiller" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 89.16666666666667 + assert sensor.attributes.get(ATTR_ORIGIN) == origin + assert sensor.attributes.get(ATTR_DESTINATION) == destination + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PUBLIC + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_PUBLIC + + +async def test_public_transport_time_table(hass, requests_mock_credentials_check): + """Test that publicTransportTimeTable works.""" + origin = "41.9798,-87.8801" + destination = "41.9043,-87.9216" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_travel_time/public_time_table_response.json"), + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.state == "80" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 79.73333333333333 + assert sensor.attributes.get(ATTR_DISTANCE) == 14.775 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "330 - Archer/Harlem (Terminal); 309 - Elmhurst Metra Station" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 79.73333333333333 + assert sensor.attributes.get(ATTR_ORIGIN) == origin + assert sensor.attributes.get(ATTR_DESTINATION) == destination + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PUBLIC_TIME_TABLE + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_PUBLIC + + +async def test_pedestrian(hass, requests_mock_credentials_check): + """Test that pedestrian works.""" + origin = "41.9798,-87.8801" + destination = "41.9043,-87.9216" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PEDESTRIAN, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/pedestrian_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_PEDESTRIAN, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.state == "211" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 210.51666666666668 + assert sensor.attributes.get(ATTR_DISTANCE) == 12.533 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "Mannheim Rd; W Belmont Ave; Cullerton St; E Fullerton Ave; " + "La Porte Ave; E Palmer Ave; N Railroad Ave; W North Ave; " + "E North Ave; E Third St" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 210.51666666666668 + assert sensor.attributes.get(ATTR_ORIGIN) == origin + assert sensor.attributes.get(ATTR_DESTINATION) == destination + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_PEDESTRIAN + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_PEDESTRIAN + + +async def test_bicycle(hass, requests_mock_credentials_check): + """Test that bicycle works.""" + origin = "41.9798,-87.8801" + destination = "41.9043,-87.9216" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_BICYCLE, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/bike_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_BICYCLE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert sensor.state == "55" + assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT + + assert sensor.attributes.get(ATTR_ATTRIBUTION) is None + assert sensor.attributes.get(ATTR_DURATION) == 54.86666666666667 + assert sensor.attributes.get(ATTR_DISTANCE) == 12.613 + assert sensor.attributes.get(ATTR_ROUTE) == ( + "Mannheim Rd; W Belmont Ave; Cullerton St; N Landen Dr; " + "E Fullerton Ave; N Wolf Rd; W North Ave; N Clinton Ave; " + "E Third St; N Caroline Ave" + ) + assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" + assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 54.86666666666667 + assert sensor.attributes.get(ATTR_ORIGIN) == origin + assert sensor.attributes.get(ATTR_DESTINATION) == destination + assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "Mannheim Rd" + assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "" + assert sensor.attributes.get(CONF_MODE) == TRAVEL_MODE_BICYCLE + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is False + + assert sensor.attributes.get(ATTR_ICON) == ICON_BICYCLE + + +async def test_location_zone(hass, requests_mock_truck_response): + """Test that origin/destination supplied by a zone works.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + zone_config = { + "zone": [ + { + "name": "Destination", + "latitude": TRUCK_DESTINATION_LATITUDE, + "longitude": TRUCK_DESTINATION_LONGITUDE, + "radius": 250, + "passive": False, + }, + { + "name": "Origin", + "latitude": TRUCK_ORIGIN_LATITUDE, + "longitude": TRUCK_ORIGIN_LONGITUDE, + "radius": 250, + "passive": False, + }, + ] + } + assert await async_setup_component(hass, "zone", zone_config) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "zone.origin", + "destination_entity_id": "zone.destination", + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + # Test that update works more than once + async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + +async def test_location_sensor(hass, requests_mock_truck_response): + """Test that origin/destination supplied by a sensor works.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + hass.states.async_set( + "sensor.origin", ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]) + ) + hass.states.async_set( + "sensor.destination", + ",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]), + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "sensor.origin", + "destination_entity_id": "sensor.destination", + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + # Test that update works more than once + async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + +async def test_location_person(hass, requests_mock_truck_response): + """Test that origin/destination supplied by a person works.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + hass.states.async_set( + "person.origin", + "unknown", + { + "latitude": float(TRUCK_ORIGIN_LATITUDE), + "longitude": float(TRUCK_ORIGIN_LONGITUDE), + }, + ) + hass.states.async_set( + "person.destination", + "unknown", + { + "latitude": float(TRUCK_DESTINATION_LATITUDE), + "longitude": float(TRUCK_DESTINATION_LONGITUDE), + }, + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "person.origin", + "destination_entity_id": "person.destination", + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + # Test that update works more than once + async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + +async def test_location_device_tracker(hass, requests_mock_truck_response): + """Test that origin/destination supplied by a device_tracker works.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + hass.states.async_set( + "device_tracker.origin", + "unknown", + { + "latitude": float(TRUCK_ORIGIN_LATITUDE), + "longitude": float(TRUCK_ORIGIN_LONGITUDE), + }, + ) + hass.states.async_set( + "device_tracker.destination", + "unknown", + { + "latitude": float(TRUCK_DESTINATION_LATITUDE), + "longitude": float(TRUCK_DESTINATION_LONGITUDE), + }, + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "device_tracker.origin", + "destination_entity_id": "device_tracker.destination", + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + # Test that update works more than once + async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + + +async def test_location_device_tracker_added_after_update( + hass, requests_mock_truck_response, caplog +): + """Test that device_tracker added after first update works.""" + caplog.set_level(logging.ERROR) + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow): + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "device_tracker.origin", + "destination_entity_id": "device_tracker.destination", + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + assert len(caplog.records) == 2 + assert "Unable to find entity" in caplog.text + caplog.clear() + + # Device tracker appear after first update + hass.states.async_set( + "device_tracker.origin", + "unknown", + { + "latitude": float(TRUCK_ORIGIN_LATITUDE), + "longitude": float(TRUCK_ORIGIN_LONGITUDE), + }, + ) + hass.states.async_set( + "device_tracker.destination", + "unknown", + { + "latitude": float(TRUCK_DESTINATION_LATITUDE), + "longitude": float(TRUCK_DESTINATION_LONGITUDE), + }, + ) + + # Test that update works more than once + async_fire_time_changed(hass, utcnow + SCAN_INTERVAL) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + assert len(caplog.records) == 0 + + +async def test_location_device_tracker_in_zone( + hass, requests_mock_truck_response, caplog +): + """Test that device_tracker in zone uses device_tracker state works.""" + caplog.set_level(logging.DEBUG) + zone_config = { + "zone": [ + { + "name": "Origin", + "latitude": TRUCK_ORIGIN_LATITUDE, + "longitude": TRUCK_ORIGIN_LONGITUDE, + "radius": 250, + "passive": False, + } + ] + } + assert await async_setup_component(hass, "zone", zone_config) + hass.states.async_set( + "device_tracker.origin", "origin", {"latitude": None, "longitude": None} + ) + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "device_tracker.origin", + "destination_latitude": TRUCK_DESTINATION_LATITUDE, + "destination_longitude": TRUCK_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + sensor = hass.states.get("sensor.test") + _assert_truck_sensor(sensor) + assert ", getting zone location" in caplog.text + + +async def test_route_not_found(hass, requests_mock_credentials_check, caplog): + """Test that route not found error is correctly handled.""" + caplog.set_level(logging.ERROR) + origin = "52.516,13.3779" + destination = "47.013399,-10.171986" + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, + text=load_fixture("here_travel_time/routing_error_no_route_found.json"), + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert NO_ROUTE_ERROR_MESSAGE in caplog.text + + +async def test_pattern_origin(hass, caplog): + """Test that pattern matching the origin works.""" + caplog.set_level(logging.ERROR) + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": "138.90", + "origin_longitude": "-77.04833", + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "invalid latitude" in caplog.text + + +async def test_pattern_destination(hass, caplog): + """Test that pattern matching the destination works.""" + caplog.set_level(logging.ERROR) + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": "139.0", + "destination_longitude": "-77.1", + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "invalid latitude" in caplog.text + + +async def test_invalid_credentials(hass, requests_mock, caplog): + """Test that invalid credentials error is correctly handled.""" + caplog.set_level(logging.ERROR) + modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] + response_url = _build_mock_url( + ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), + ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), + modes, + APP_ID, + APP_CODE, + "now", + ) + requests_mock.get( + response_url, + text=load_fixture("here_travel_time/routing_error_invalid_credentials.json"), + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "Invalid credentials" in caplog.text + + +async def test_attribution(hass, requests_mock_credentials_check): + """Test that attributions are correctly displayed.""" + origin = "50.037751372637686,14.39233448220898" + destination = "50.07993838201255,14.42582157361062" + modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_ENABLED] + response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + requests_mock_credentials_check.get( + response_url, text=load_fixture("here_travel_time/attribution_response.json") + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_latitude": origin.split(",")[0], + "origin_longitude": origin.split(",")[1], + "destination_latitude": destination.split(",")[0], + "destination_longitude": destination.split(",")[1], + "app_id": APP_ID, + "app_code": APP_CODE, + "traffic_mode": True, + "route_mode": ROUTE_MODE_SHORTEST, + "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, + } + } + assert await async_setup_component(hass, DOMAIN, config) + sensor = hass.states.get("sensor.test") + assert ( + sensor.attributes.get(ATTR_ATTRIBUTION) + == "With the support of HERE Technologies. All information is provided without warranty of any kind." + ) diff --git a/tests/fixtures/here_travel_time/attribution_response.json b/tests/fixtures/here_travel_time/attribution_response.json new file mode 100644 index 00000000000000..9b682f6c51fb14 --- /dev/null +++ b/tests/fixtures/here_travel_time/attribution_response.json @@ -0,0 +1,276 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-09-21T15:17:31Z", + "mapVersion": "8.30.100.154", + "moduleVersion": "7.2.201937-5251", + "interfaceVersion": "2.6.70", + "availableMapVersion": [ + "8.30.100.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "+565790671", + "mappedPosition": { + "latitude": 50.0378591, + "longitude": 14.3924721 + }, + "originalPosition": { + "latitude": 50.0377513, + "longitude": 14.3923344 + }, + "type": "stopOver", + "spot": 0.3, + "sideOfStreet": "left", + "mappedRoadName": "V Bokách III", + "label": "V Bokách III", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+748931502", + "mappedPosition": { + "latitude": 50.0798786, + "longitude": 14.4260037 + }, + "originalPosition": { + "latitude": 50.0799383, + "longitude": 14.4258216 + }, + "type": "stopOver", + "spot": 1.0, + "sideOfStreet": "left", + "mappedRoadName": "Štěpánská", + "label": "Štěpánská", + "shapeIndex": 116, + "source": "user" + } + ], + "mode": { + "type": "shortest", + "transportModes": [ + "publicTransportTimeTable" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "+565790671", + "mappedPosition": { + "latitude": 50.0378591, + "longitude": 14.3924721 + }, + "originalPosition": { + "latitude": 50.0377513, + "longitude": 14.3923344 + }, + "type": "stopOver", + "spot": 0.3, + "sideOfStreet": "left", + "mappedRoadName": "V Bokách III", + "label": "V Bokách III", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+748931502", + "mappedPosition": { + "latitude": 50.0798786, + "longitude": 14.4260037 + }, + "originalPosition": { + "latitude": 50.0799383, + "longitude": 14.4258216 + }, + "type": "stopOver", + "spot": 1.0, + "sideOfStreet": "left", + "mappedRoadName": "Štěpánská", + "label": "Štěpánská", + "shapeIndex": 116, + "source": "user" + }, + "length": 7835, + "travelTime": 2413, + "maneuver": [ + { + "position": { + "latitude": 50.0378591, + "longitude": 14.3924721 + }, + "instruction": "Head northwest on Kosořská. Go for 28 m.", + "travelTime": 32, + "length": 28, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0380039, + "longitude": 14.3921542 + }, + "instruction": "Turn left onto Kosořská. Go for 24 m.", + "travelTime": 24, + "length": 24, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0380039, + "longitude": 14.3918109 + }, + "instruction": "Take the street on the left, Slivenecká. Go for 343 m.", + "travelTime": 354, + "length": 343, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0376499, + "longitude": 14.3871975 + }, + "instruction": "Turn left onto Slivenecká. Go for 64 m.", + "travelTime": 72, + "length": 64, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0373602, + "longitude": 14.3879807 + }, + "instruction": "Turn right onto Slivenecká. Go for 91 m.", + "travelTime": 95, + "length": 91, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0365448, + "longitude": 14.3878305 + }, + "instruction": "Turn left onto K Barrandovu. Go for 124 m.", + "travelTime": 126, + "length": 124, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0363168, + "longitude": 14.3894618 + }, + "instruction": "Go to the Tram station Geologicka and take the rail 5 toward Ústřední dílny DP. Follow for 13 stations.", + "travelTime": 1440, + "length": 6911, + "id": "M7", + "stopName": "Geologicka", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 50.0800508, + "longitude": 14.423403 + }, + "instruction": "Get off at Vodickova.", + "travelTime": 0, + "length": 0, + "id": "M8", + "stopName": "Vodickova", + "nextRoadName": "Vodičkova", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 50.0800508, + "longitude": 14.423403 + }, + "instruction": "Head northeast on Vodičkova. Go for 65 m.", + "travelTime": 74, + "length": 65, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0804901, + "longitude": 14.4239759 + }, + "instruction": "Turn right onto V Jámě. Go for 163 m.", + "travelTime": 174, + "length": 163, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0796962, + "longitude": 14.4258857 + }, + "instruction": "Turn left onto Štěpánská. Go for 22 m.", + "travelTime": 22, + "length": 22, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 50.0798786, + "longitude": 14.4260037 + }, + "instruction": "Arrive at Štěpánská. Your destination is on the left.", + "travelTime": 0, + "length": 0, + "id": "M12", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "publicTransportLine": [ + { + "lineName": "5", + "lineForeground": "#F5ADCE", + "lineBackground": "#F5ADCE", + "companyName": "HERE Technologies", + "destination": "Ústřední dílny DP", + "type": "railLight", + "id": "L1" + } + ], + "summary": { + "distance": 7835, + "baseTime": 2413, + "flags": [ + "noThroughRoad", + "builtUpArea" + ], + "text": "The trip takes 7.8 km and 40 mins.", + "travelTime": 2413, + "departure": "2019-09-21T17:16:17+02:00", + "timetableExpiration": "2019-09-21T00:00:00Z", + "_type": "PublicTransportRouteSummaryType" + } + } + ], + "language": "en-us", + "sourceAttribution": { + "attribution": "With the support of HERE Technologies. All information is provided without warranty of any kind.", + "supplier": [ + { + "title": "HERE Technologies", + "href": "https://transit.api.here.com/r?appId=Mt1bOYh3m9uxE7r3wuUx&u=https://wego.here.com" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/bike_response.json b/tests/fixtures/here_travel_time/bike_response.json new file mode 100644 index 00000000000000..a3af39129d01fe --- /dev/null +++ b/tests/fixtures/here_travel_time/bike_response.json @@ -0,0 +1,274 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-24T10:17:40Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201929-4522", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 87, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "bicycle" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 87, + "source": "user" + }, + "length": 12613, + "travelTime": 3292, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd (US-12/US-45). Go for 2.6 km.", + "travelTime": 646, + "length": 2648, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9579244, + "longitude": -87.8838551 + }, + "instruction": "Keep left onto Mannheim Rd (US-12/US-45). Go for 2.4 km.", + "travelTime": 621, + "length": 2427, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9364238, + "longitude": -87.8849387 + }, + "instruction": "Turn right onto W Belmont Ave. Go for 595 m.", + "travelTime": 158, + "length": 595, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9362521, + "longitude": -87.8921163 + }, + "instruction": "Turn left onto Cullerton St. Go for 669 m.", + "travelTime": 180, + "length": 669, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9305658, + "longitude": -87.8932428 + }, + "instruction": "Continue on N Landen Dr. Go for 976 m.", + "travelTime": 246, + "length": 976, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9217896, + "longitude": -87.8928781 + }, + "instruction": "Turn right onto E Fullerton Ave. Go for 904 m.", + "travelTime": 238, + "length": 904, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.921618, + "longitude": -87.9038107 + }, + "instruction": "Turn left onto N Wolf Rd. Go for 1.6 km.", + "travelTime": 417, + "length": 1604, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.907177, + "longitude": -87.9032314 + }, + "instruction": "Turn right onto W North Ave (IL-64). Go for 2.0 km.", + "travelTime": 574, + "length": 2031, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065225, + "longitude": -87.9277039 + }, + "instruction": "Turn left onto N Clinton Ave. Go for 275 m.", + "travelTime": 78, + "length": 275, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040549, + "longitude": -87.9277253 + }, + "instruction": "Turn left onto E Third St. Go for 249 m.", + "travelTime": 63, + "length": 249, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040334, + "longitude": -87.9247105 + }, + "instruction": "Continue on N Caroline Ave. Go for 96 m.", + "travelTime": 37, + "length": 96, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn slightly left. Go for 113 m.", + "travelTime": 28, + "length": 113, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 6, + "length": 26, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M14", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 12613, + "baseTime": 3292, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 12.6 km and 55 mins.", + "travelTime": 3292, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/car_enabled_response.json b/tests/fixtures/here_travel_time/car_enabled_response.json new file mode 100644 index 00000000000000..08da738f0464a9 --- /dev/null +++ b/tests/fixtures/here_travel_time/car_enabled_response.json @@ -0,0 +1,298 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T21:21:31Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 283, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "car" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 283, + "source": "user" + }, + "length": 23381, + "travelTime": 1817, + "maneuver": [ + { + "position": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "instruction": "Head toward 22nd St NW on K St NW. Go for 140 m.", + "travelTime": 36, + "length": 140, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9027703, + "longitude": -77.0494902 + }, + "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 325 m.", + "travelTime": 81, + "length": 325, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9026523, + "longitude": -77.0529449 + }, + "instruction": "Keep left onto K St NW (US-29). Go for 201 m.", + "travelTime": 29, + "length": 201, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9025235, + "longitude": -77.0552516 + }, + "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.", + "travelTime": 143, + "length": 1381, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9050448, + "longitude": -77.0701969 + }, + "instruction": "Turn left onto M St NW. Go for 784 m.", + "travelTime": 80, + "length": 784, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9060318, + "longitude": -77.0790696 + }, + "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.", + "travelTime": 287, + "length": 4230, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9303219, + "longitude": -77.1117926 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.", + "travelTime": 55, + "length": 844, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9368558, + "longitude": -77.1166742 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.", + "travelTime": 294, + "length": 4652, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9706838, + "longitude": -77.1461463 + }, + "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.", + "travelTime": 90, + "length": 2069, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9858222, + "longitude": -77.1571326 + }, + "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 2.9 km.", + "travelTime": 129, + "length": 2890, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0104449, + "longitude": -77.1508026 + }, + "instruction": "Keep left onto I-270-SPUR toward I-270/Rockville/Frederick. Go for 1.1 km.", + "travelTime": 48, + "length": 1136, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0192747, + "longitude": -77.144773 + }, + "instruction": "Take exit 1 toward Democracy Blvd/Old Georgetown Rd/MD-187 onto Democracy Blvd. Go for 1.8 km.", + "travelTime": 205, + "length": 1818, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0247464, + "longitude": -77.1253431 + }, + "instruction": "Turn left onto Old Georgetown Rd (MD-187). Go for 2.3 km.", + "travelTime": 230, + "length": 2340, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0447772, + "longitude": -77.1203649 + }, + "instruction": "Turn right onto Nicholson Ln. Go for 208 m.", + "travelTime": 31, + "length": 208, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0448952, + "longitude": -77.1179724 + }, + "instruction": "Turn right onto Commonwealth Dr. Go for 341 m.", + "travelTime": 75, + "length": 341, + "id": "M15", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "instruction": "Arrive at Commonwealth Dr. Your destination is on the left.", + "travelTime": 4, + "length": 22, + "id": "M16", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 23381, + "trafficTime": 1782, + "baseTime": 1712, + "flags": [ + "noThroughRoad", + "motorway", + "builtUpArea", + "park" + ], + "text": "The trip takes 23.4 km and 30 mins.", + "travelTime": 1782, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/car_response.json b/tests/fixtures/here_travel_time/car_response.json new file mode 100644 index 00000000000000..bda8454f3f3780 --- /dev/null +++ b/tests/fixtures/here_travel_time/car_response.json @@ -0,0 +1,299 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-19T07:38:39Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4446", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "+732182239", + "mappedPosition": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "originalPosition": { + "latitude": 38.9, + "longitude": -77.0483301 + }, + "type": "stopOver", + "spot": 0.4946237, + "sideOfStreet": "right", + "mappedRoadName": "22nd St NW", + "label": "22nd St NW", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+942865877", + "mappedPosition": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "originalPosition": { + "latitude": 38.9999999, + "longitude": -77.1000001 + }, + "type": "stopOver", + "spot": 1, + "sideOfStreet": "left", + "mappedRoadName": "Service Rd S", + "label": "Service Rd S", + "shapeIndex": 279, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "car" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "+732182239", + "mappedPosition": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "originalPosition": { + "latitude": 38.9, + "longitude": -77.0483301 + }, + "type": "stopOver", + "spot": 0.4946237, + "sideOfStreet": "right", + "mappedRoadName": "22nd St NW", + "label": "22nd St NW", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+942865877", + "mappedPosition": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "originalPosition": { + "latitude": 38.9999999, + "longitude": -77.1000001 + }, + "type": "stopOver", + "spot": 1, + "sideOfStreet": "left", + "mappedRoadName": "Service Rd S", + "label": "Service Rd S", + "shapeIndex": 279, + "source": "user" + }, + "length": 23903, + "travelTime": 1884, + "maneuver": [ + { + "position": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "instruction": "Head toward I St NW on 22nd St NW. Go for 279 m.", + "travelTime": 95, + "length": 279, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9021051, + "longitude": -77.048825 + }, + "instruction": "Turn left toward Pennsylvania Ave NW. Go for 71 m.", + "travelTime": 21, + "length": 71, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.902545, + "longitude": -77.0494151 + }, + "instruction": "Take the 3rd exit from Washington Cir NW roundabout onto K St NW. Go for 352 m.", + "travelTime": 90, + "length": 352, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9026523, + "longitude": -77.0529449 + }, + "instruction": "Keep left onto K St NW (US-29). Go for 201 m.", + "travelTime": 30, + "length": 201, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9025235, + "longitude": -77.0552516 + }, + "instruction": "Keep right onto Whitehurst Fwy (US-29). Go for 1.4 km.", + "travelTime": 131, + "length": 1381, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9050448, + "longitude": -77.0701969 + }, + "instruction": "Turn left onto M St NW. Go for 784 m.", + "travelTime": 78, + "length": 784, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9060318, + "longitude": -77.0790696 + }, + "instruction": "Turn slightly left onto Canal Rd NW. Go for 4.2 km.", + "travelTime": 277, + "length": 4230, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9303219, + "longitude": -77.1117926 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 844 m.", + "travelTime": 55, + "length": 844, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9368558, + "longitude": -77.1166742 + }, + "instruction": "Continue on Clara Barton Pkwy. Go for 4.7 km.", + "travelTime": 298, + "length": 4652, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9706838, + "longitude": -77.1461463 + }, + "instruction": "Keep right onto Cabin John Pkwy N toward I-495 N. Go for 2.1 km.", + "travelTime": 91, + "length": 2069, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9858222, + "longitude": -77.1571326 + }, + "instruction": "Take left ramp onto I-495 N (Capital Beltway). Go for 5.5 km.", + "travelTime": 238, + "length": 5538, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0153587, + "longitude": -77.1221781 + }, + "instruction": "Take exit 36 toward Bethesda onto MD-187 S (Old Georgetown Rd). Go for 2.4 km.", + "travelTime": 211, + "length": 2365, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9981818, + "longitude": -77.1093571 + }, + "instruction": "Turn left onto Lincoln Dr. Go for 506 m.", + "travelTime": 127, + "length": 506, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9987397, + "longitude": -77.1037138 + }, + "instruction": "Turn right onto Service Rd W. Go for 121 m.", + "travelTime": 36, + "length": 121, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9976454, + "longitude": -77.1036172 + }, + "instruction": "Turn left onto Service Rd S. Go for 510 m.", + "travelTime": 106, + "length": 510, + "id": "M15", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "instruction": "Arrive at Service Rd S. Your destination is on the left.", + "travelTime": 0, + "length": 0, + "id": "M16", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 23903, + "trafficTime": 1861, + "baseTime": 1803, + "flags": [ + "noThroughRoad", + "motorway", + "builtUpArea", + "park", + "privateRoad" + ], + "text": "The trip takes 23.9 km and 31 mins.", + "travelTime": 1861, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/car_shortest_response.json b/tests/fixtures/here_travel_time/car_shortest_response.json new file mode 100644 index 00000000000000..765c438c1cd14e --- /dev/null +++ b/tests/fixtures/here_travel_time/car_shortest_response.json @@ -0,0 +1,231 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T21:05:28Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 162, + "source": "user" + } + ], + "mode": { + "type": "shortest", + "transportModes": [ + "car" + ], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1128310200", + "mappedPosition": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "originalPosition": { + "latitude": 38.9029809, + "longitude": -77.048338 + }, + "type": "stopOver", + "spot": 0.3538462, + "sideOfStreet": "right", + "mappedRoadName": "K St NW", + "label": "K St NW", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "-18459081", + "mappedPosition": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "originalPosition": { + "latitude": 39.042158, + "longitude": -77.119116 + }, + "type": "stopOver", + "spot": 0.7253521, + "sideOfStreet": "left", + "mappedRoadName": "Commonwealth Dr", + "label": "Commonwealth Dr", + "shapeIndex": 162, + "source": "user" + }, + "length": 18388, + "travelTime": 2493, + "maneuver": [ + { + "position": { + "latitude": 38.9026523, + "longitude": -77.048338 + }, + "instruction": "Head west on K St NW. Go for 79 m.", + "travelTime": 22, + "length": 79, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9026523, + "longitude": -77.048825 + }, + "instruction": "Turn right onto 22nd St NW. Go for 141 m.", + "travelTime": 79, + "length": 141, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9039075, + "longitude": -77.048825 + }, + "instruction": "Keep left onto 22nd St NW. Go for 841 m.", + "travelTime": 256, + "length": 841, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9114928, + "longitude": -77.0487821 + }, + "instruction": "Turn left onto Massachusetts Ave NW. Go for 145 m.", + "travelTime": 22, + "length": 145, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9120293, + "longitude": -77.0502949 + }, + "instruction": "Take the 1st exit from Massachusetts Ave NW roundabout onto Massachusetts Ave NW. Go for 2.8 km.", + "travelTime": 301, + "length": 2773, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9286053, + "longitude": -77.073158 + }, + "instruction": "Turn right onto Wisconsin Ave NW. Go for 3.8 km.", + "travelTime": 610, + "length": 3801, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 38.9607918, + "longitude": -77.0857322 + }, + "instruction": "Continue on Wisconsin Ave (MD-355). Go for 9.7 km.", + "travelTime": 1013, + "length": 9686, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0447664, + "longitude": -77.1116638 + }, + "instruction": "Turn left onto Nicholson Ln. Go for 559 m.", + "travelTime": 111, + "length": 559, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0448952, + "longitude": -77.1179724 + }, + "instruction": "Turn left onto Commonwealth Dr. Go for 341 m.", + "travelTime": 75, + "length": 341, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 39.0422511, + "longitude": -77.1193526 + }, + "instruction": "Arrive at Commonwealth Dr. Your destination is on the left.", + "travelTime": 4, + "length": 22, + "id": "M10", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 18388, + "trafficTime": 2427, + "baseTime": 2150, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 18.4 km and 40 mins.", + "travelTime": 2427, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/pedestrian_response.json b/tests/fixtures/here_travel_time/pedestrian_response.json new file mode 100644 index 00000000000000..07881e8bd3d06b --- /dev/null +++ b/tests/fixtures/here_travel_time/pedestrian_response.json @@ -0,0 +1,308 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T18:40:10Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 122, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "pedestrian" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 122, + "source": "user" + }, + "length": 12533, + "travelTime": 12631, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd. Go for 848 m.", + "travelTime": 848, + "length": 848, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9722581, + "longitude": -87.8776109 + }, + "instruction": "Take the street on the left, Mannheim Rd. Go for 4.2 km.", + "travelTime": 4239, + "length": 4227, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9364238, + "longitude": -87.8849387 + }, + "instruction": "Turn right onto W Belmont Ave. Go for 595 m.", + "travelTime": 605, + "length": 595, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9362521, + "longitude": -87.8921163 + }, + "instruction": "Turn left onto Cullerton St. Go for 406 m.", + "travelTime": 411, + "length": 406, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9326043, + "longitude": -87.8919983 + }, + "instruction": "Turn right onto Cullerton St. Go for 1.2 km.", + "travelTime": 1249, + "length": 1239, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9217896, + "longitude": -87.8928781 + }, + "instruction": "Turn right onto E Fullerton Ave. Go for 786 m.", + "travelTime": 796, + "length": 786, + "id": "M6", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9216394, + "longitude": -87.9023838 + }, + "instruction": "Turn left onto La Porte Ave. Go for 424 m.", + "travelTime": 430, + "length": 424, + "id": "M7", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9180024, + "longitude": -87.9028559 + }, + "instruction": "Turn right onto E Palmer Ave. Go for 864 m.", + "travelTime": 875, + "length": 864, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9175196, + "longitude": -87.9132199 + }, + "instruction": "Turn left onto N Railroad Ave. Go for 1.2 km.", + "travelTime": 1180, + "length": 1170, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9070268, + "longitude": -87.9130161 + }, + "instruction": "Turn right onto W North Ave. Go for 638 m.", + "travelTime": 638, + "length": 638, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9068551, + "longitude": -87.9207087 + }, + "instruction": "Take the street on the left, E North Ave. Go for 354 m.", + "travelTime": 354, + "length": 354, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065869, + "longitude": -87.9249573 + }, + "instruction": "Take the street on the left, E North Ave. Go for 228 m.", + "travelTime": 242, + "length": 228, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065225, + "longitude": -87.9277039 + }, + "instruction": "Turn left. Go for 409 m.", + "travelTime": 419, + "length": 409, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040334, + "longitude": -87.9260409 + }, + "instruction": "Turn left onto E Third St. Go for 206 m.", + "travelTime": 206, + "length": 206, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn left. Go for 113 m.", + "travelTime": 113, + "length": 113, + "id": "M15", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 26, + "length": 26, + "id": "M16", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M17", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 12533, + "baseTime": 12631, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park", + "privateRoad" + ], + "text": "The trip takes 12.5 km and 3:31 h.", + "travelTime": 12631, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/public_response.json b/tests/fixtures/here_travel_time/public_response.json new file mode 100644 index 00000000000000..149b4d06c3975e --- /dev/null +++ b/tests/fixtures/here_travel_time/public_response.json @@ -0,0 +1,294 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T18:40:37Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 191, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "publicTransport" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 191, + "source": "user" + }, + "length": 22325, + "travelTime": 5350, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd. Go for 848 m.", + "travelTime": 848, + "length": 848, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9722581, + "longitude": -87.8776109 + }, + "instruction": "Take the street on the left, Mannheim Rd. Go for 825 m.", + "travelTime": 825, + "length": 825, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9650483, + "longitude": -87.8769565 + }, + "instruction": "Go to the stop Mannheim/Lawrence and take the bus 332 toward Palmer/Schiller. Follow for 7 stops.", + "travelTime": 475, + "length": 4360, + "id": "M3", + "stopName": "Mannheim/Lawrence", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9541478, + "longitude": -87.9133594 + }, + "instruction": "Get off at Irving Park/Taft.", + "travelTime": 0, + "length": 0, + "id": "M4", + "stopName": "Irving Park/Taft", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9541478, + "longitude": -87.9133594 + }, + "instruction": "Take the bus 332 toward Cargo Rd./Delta Cargo. Follow for 1 stop.", + "travelTime": 155, + "length": 3505, + "id": "M5", + "stopName": "Irving Park/Taft", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9599199, + "longitude": -87.9162776 + }, + "instruction": "Get off at Cargo Rd./S. Access Rd./Lufthansa.", + "travelTime": 0, + "length": 0, + "id": "M6", + "stopName": "Cargo Rd./S. Access Rd./Lufthansa", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9599199, + "longitude": -87.9162776 + }, + "instruction": "Take the bus 332 toward Palmer/Schiller. Follow for 41 stops.", + "travelTime": 1510, + "length": 11261, + "id": "M7", + "stopName": "Cargo Rd./S. Access Rd./Lufthansa", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9041729, + "longitude": -87.9399669 + }, + "instruction": "Get off at York/Third.", + "travelTime": 0, + "length": 0, + "id": "M8", + "stopName": "York/Third", + "nextRoadName": "N York St", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9041729, + "longitude": -87.9399669 + }, + "instruction": "Head east on N York St. Go for 33 m.", + "travelTime": 43, + "length": 33, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039476, + "longitude": -87.9398811 + }, + "instruction": "Turn left onto E Third St. Go for 1.4 km.", + "travelTime": 1355, + "length": 1354, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn left. Go for 113 m.", + "travelTime": 113, + "length": 113, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 26, + "length": 26, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M13", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "publicTransportLine": [ + { + "lineName": "332", + "companyName": "", + "destination": "Palmer/Schiller", + "type": "busPublic", + "id": "L1" + }, + { + "lineName": "332", + "companyName": "", + "destination": "Cargo Rd./Delta Cargo", + "type": "busPublic", + "id": "L2" + }, + { + "lineName": "332", + "companyName": "", + "destination": "Palmer/Schiller", + "type": "busPublic", + "id": "L3" + } + ], + "summary": { + "distance": 22325, + "baseTime": 5350, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 22.3 km and 1:29 h.", + "travelTime": 5350, + "departure": "1970-01-01T00:00:00Z", + "_type": "PublicTransportRouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/public_time_table_response.json b/tests/fixtures/here_travel_time/public_time_table_response.json new file mode 100644 index 00000000000000..52df0d4eb35ad5 --- /dev/null +++ b/tests/fixtures/here_travel_time/public_time_table_response.json @@ -0,0 +1,308 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-08-06T06:43:24Z", + "mapVersion": "8.30.99.152", + "moduleVersion": "7.2.201931-4739", + "interfaceVersion": "2.6.66", + "availableMapVersion": [ + "8.30.99.152" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 111, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "publicTransportTimeTable" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "-1230414527", + "mappedPosition": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5079365, + "sideOfStreet": "right", + "mappedRoadName": "Mannheim Rd", + "label": "Mannheim Rd - US-12", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+924115108", + "mappedPosition": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0.1925926, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 111, + "source": "user" + }, + "length": 14775, + "travelTime": 4784, + "maneuver": [ + { + "position": { + "latitude": 41.9797859, + "longitude": -87.8790879 + }, + "instruction": "Head south on Mannheim Rd. Go for 848 m.", + "travelTime": 848, + "length": 848, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9722581, + "longitude": -87.8776109 + }, + "instruction": "Take the street on the left, Mannheim Rd. Go for 812 m.", + "travelTime": 812, + "length": 812, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.965051, + "longitude": -87.8769591 + }, + "instruction": "Go to the Bus stop Mannheim/Lawrence and take the bus 330 toward Archer/Harlem (Terminal). Follow for 33 stops.", + "travelTime": 900, + "length": 7815, + "id": "M3", + "stopName": "Mannheim/Lawrence", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.896836, + "longitude": -87.883771 + }, + "instruction": "Get off at Mannheim/Lake.", + "travelTime": 0, + "length": 0, + "id": "M4", + "stopName": "Mannheim/Lake", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.896836, + "longitude": -87.883771 + }, + "instruction": "Walk to Bus Lake/Mannheim.", + "travelTime": 300, + "length": 72, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.897263, + "longitude": -87.8842648 + }, + "instruction": "Take the bus 309 toward Elmhurst Metra Station. Follow for 18 stops.", + "travelTime": 1020, + "length": 4362, + "id": "M6", + "stopName": "Lake/Mannheim", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9066347, + "longitude": -87.928671 + }, + "instruction": "Get off at North/Berteau.", + "travelTime": 0, + "length": 0, + "id": "M7", + "stopName": "North/Berteau", + "nextRoadName": "E Berteau Ave", + "_type": "PublicTransportManeuverType" + }, + { + "position": { + "latitude": 41.9066347, + "longitude": -87.928671 + }, + "instruction": "Head north on E Berteau Ave. Go for 23 m.", + "travelTime": 40, + "length": 23, + "id": "M8", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9067693, + "longitude": -87.9284549 + }, + "instruction": "Turn right onto E Berteau Ave. Go for 40 m.", + "travelTime": 44, + "length": 40, + "id": "M9", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065011, + "longitude": -87.9282939 + }, + "instruction": "Turn left onto E North Ave. Go for 49 m.", + "travelTime": 56, + "length": 49, + "id": "M10", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065225, + "longitude": -87.9277039 + }, + "instruction": "Turn slightly right. Go for 409 m.", + "travelTime": 419, + "length": 409, + "id": "M11", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9040334, + "longitude": -87.9260409 + }, + "instruction": "Turn left onto E Third St. Go for 206 m.", + "travelTime": 206, + "length": 206, + "id": "M12", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9038832, + "longitude": -87.9236054 + }, + "instruction": "Turn left. Go for 113 m.", + "travelTime": 113, + "length": 113, + "id": "M13", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9039047, + "longitude": -87.9222536 + }, + "instruction": "Turn left. Go for 26 m.", + "travelTime": 26, + "length": 26, + "id": "M14", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.90413, + "longitude": -87.9223502 + }, + "instruction": "Arrive at your destination on the right.", + "travelTime": 0, + "length": 0, + "id": "M15", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "publicTransportLine": [ + { + "lineName": "330", + "companyName": "PACE", + "destination": "Archer/Harlem (Terminal)", + "type": "busPublic", + "id": "L1" + }, + { + "lineName": "309", + "companyName": "PACE", + "destination": "Elmhurst Metra Station", + "type": "busPublic", + "id": "L2" + } + ], + "summary": { + "distance": 14775, + "baseTime": 4784, + "flags": [ + "noThroughRoad", + "builtUpArea", + "park" + ], + "text": "The trip takes 14.8 km and 1:20 h.", + "travelTime": 4784, + "departure": "2019-08-06T05:09:20-05:00", + "timetableExpiration": "2019-08-04T00:00:00Z", + "_type": "PublicTransportRouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json b/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json new file mode 100644 index 00000000000000..81fb246178c938 --- /dev/null +++ b/tests/fixtures/here_travel_time/routing_error_invalid_credentials.json @@ -0,0 +1,15 @@ +{ + "_type": "ns2:RoutingServiceErrorType", + "type": "PermissionError", + "subtype": "InvalidCredentials", + "details": "This is not a valid app_id and app_code pair. Please verify that the values are not swapped between the app_id and app_code and the values provisioned by HERE (either by your customer representative or via http://developer.here.com/myapps) were copied correctly into the request.", + "metaInfo": { + "timestamp": "2019-07-10T09:43:14Z", + "mapVersion": "8.30.98.152", + "moduleVersion": "7.2.201927-4307", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.152" + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/routing_error_no_route_found.json b/tests/fixtures/here_travel_time/routing_error_no_route_found.json new file mode 100644 index 00000000000000..a776fa91c43b2a --- /dev/null +++ b/tests/fixtures/here_travel_time/routing_error_no_route_found.json @@ -0,0 +1,21 @@ +{ + "_type": "ns2:RoutingServiceErrorType", + "type": "ApplicationError", + "subtype": "NoRouteFound", + "details": "Error is NGEO_ERROR_ROUTE_NO_END_POINT", + "additionalData": [ + { + "key": "error_code", + "value": "NGEO_ERROR_ROUTE_NO_END_POINT" + } + ], + "metaInfo": { + "timestamp": "2019-07-10T09:51:04Z", + "mapVersion": "8.30.98.152", + "moduleVersion": "7.2.201927-4307", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.152" + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/here_travel_time/truck_response.json b/tests/fixtures/here_travel_time/truck_response.json new file mode 100644 index 00000000000000..a302d564902e81 --- /dev/null +++ b/tests/fixtures/here_travel_time/truck_response.json @@ -0,0 +1,187 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-21T14:25:00Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4478", + "interfaceVersion": "2.6.64", + "availableMapVersion": [ + "8.30.98.154" + ] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "+930461269", + "mappedPosition": { + "latitude": 41.9800687, + "longitude": -87.8805614 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5555556, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "-1035319462", + "mappedPosition": { + "latitude": 41.9042909, + "longitude": -87.9216528 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0, + "sideOfStreet": "left", + "mappedRoadName": "Eisenhower Expy E", + "label": "Eisenhower Expy E - I-290", + "shapeIndex": 135, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": [ + "truck" + ], + "trafficMode": "disabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "+930461269", + "mappedPosition": { + "latitude": 41.9800687, + "longitude": -87.8805614 + }, + "originalPosition": { + "latitude": 41.9798, + "longitude": -87.8801 + }, + "type": "stopOver", + "spot": 0.5555556, + "sideOfStreet": "right", + "mappedRoadName": "", + "label": "", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "-1035319462", + "mappedPosition": { + "latitude": 41.9042909, + "longitude": -87.9216528 + }, + "originalPosition": { + "latitude": 41.9043, + "longitude": -87.9216001 + }, + "type": "stopOver", + "spot": 0, + "sideOfStreet": "left", + "mappedRoadName": "Eisenhower Expy E", + "label": "Eisenhower Expy E - I-290", + "shapeIndex": 135, + "source": "user" + }, + "length": 13049, + "travelTime": 812, + "maneuver": [ + { + "position": { + "latitude": 41.9800687, + "longitude": -87.8805614 + }, + "instruction": "Take ramp onto I-190. Go for 631 m.", + "travelTime": 53, + "length": 631, + "id": "M1", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.98259, + "longitude": -87.8744352 + }, + "instruction": "Take exit 1D toward Indiana onto I-294 S (Tri-State Tollway). Go for 10.9 km.", + "travelTime": 573, + "length": 10872, + "id": "M2", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9059324, + "longitude": -87.9199362 + }, + "instruction": "Take exit 33 toward Rockford/US-20/IL-64 onto I-290 W (Eisenhower Expy W). Go for 475 m.", + "travelTime": 54, + "length": 475, + "id": "M3", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9067156, + "longitude": -87.9237771 + }, + "instruction": "Take exit 13B toward North Ave onto IL-64 W (E North Ave). Go for 435 m.", + "travelTime": 51, + "length": 435, + "id": "M4", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9065869, + "longitude": -87.9249573 + }, + "instruction": "Take ramp onto I-290 E (Eisenhower Expy E) toward Chicago/I-294 S. Go for 636 m.", + "travelTime": 81, + "length": 636, + "id": "M5", + "_type": "PrivateTransportManeuverType" + }, + { + "position": { + "latitude": 41.9042909, + "longitude": -87.9216528 + }, + "instruction": "Arrive at Eisenhower Expy E (I-290). Your destination is on the left.", + "travelTime": 0, + "length": 0, + "id": "M6", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 13049, + "trafficTime": 812, + "baseTime": 812, + "flags": [ + "tollroad", + "motorway", + "builtUpArea" + ], + "text": "The trip takes 13.0 km and 14 mins.", + "travelTime": 812, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us" + } +} \ No newline at end of file From 9fd9ccc2a322dfd973e16165a61e972dccdaffc8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 12:13:12 +0200 Subject: [PATCH 0418/3953] Remove deprecated srp_energy integration (ADR-0004) (#26826) --- .coveragerc | 1 - .../components/srp_energy/__init__.py | 1 - .../components/srp_energy/manifest.json | 10 -- homeassistant/components/srp_energy/sensor.py | 156 ------------------ requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/srp_energy/__init__.py | 1 - tests/components/srp_energy/test_sensor.py | 62 ------- 8 files changed, 237 deletions(-) delete mode 100644 homeassistant/components/srp_energy/__init__.py delete mode 100644 homeassistant/components/srp_energy/manifest.json delete mode 100644 homeassistant/components/srp_energy/sensor.py delete mode 100644 tests/components/srp_energy/__init__.py delete mode 100644 tests/components/srp_energy/test_sensor.py diff --git a/.coveragerc b/.coveragerc index b6bdcc2704372f..256a6b3e41eb6d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -608,7 +608,6 @@ omit = homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/media_player.py homeassistant/components/squeezebox/media_player.py - homeassistant/components/srp_energy/sensor.py homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py deleted file mode 100644 index 71e04d7b8c99aa..00000000000000 --- a/homeassistant/components/srp_energy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The srp_energy component.""" diff --git a/homeassistant/components/srp_energy/manifest.json b/homeassistant/components/srp_energy/manifest.json deleted file mode 100644 index 050a78223c17f5..00000000000000 --- a/homeassistant/components/srp_energy/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "srp_energy", - "name": "Srp energy", - "documentation": "https://www.home-assistant.io/components/srp_energy", - "requirements": [ - "srpenergy==1.0.6" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py deleted file mode 100644 index f1d1787b7b48c5..00000000000000 --- a/homeassistant/components/srp_energy/sensor.py +++ /dev/null @@ -1,156 +0,0 @@ -"""Platform for retrieving energy data from SRP.""" -from datetime import datetime, timedelta -import logging - -from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout -import voluptuous as vol - -from homeassistant.const import ( - CONF_NAME, - CONF_PASSWORD, - ENERGY_KILO_WATT_HOUR, - CONF_USERNAME, - CONF_ID, -) -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) - -ATTRIBUTION = "Powered by SRP Energy" - -DEFAULT_NAME = "SRP Energy" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1440) -ENERGY_KWH = ENERGY_KILO_WATT_HOUR - -ATTR_READING_COST = "reading_cost" -ATTR_READING_TIME = "datetime" -ATTR_READING_USAGE = "reading_usage" -ATTR_DAILY_USAGE = "daily_usage" -ATTR_USAGE_HISTORY = "usage_history" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -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] - account_id = config[CONF_ID] - - from srpenergy.client import SrpEnergyClient - - srp_client = SrpEnergyClient(account_id, username, password) - - if not srp_client.validate(): - _LOGGER.error("Couldn't connect to %s. Check credentials", name) - return - - add_entities([SrpEnergy(name, srp_client)], True) - - -class SrpEnergy(Entity): - """Representation of an srp usage.""" - - def __init__(self, name, client): - """Initialize SRP Usage.""" - self._state = None - self._name = name - self._client = client - self._history = None - self._usage = None - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION - - @property - def state(self): - """Return the current state.""" - if self._state is None: - return None - - return f"{self._state:.2f}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return ENERGY_KWH - - @property - def history(self): - """Return the energy usage history of this entity, if any.""" - if self._usage is None: - return None - - history = [ - { - ATTR_READING_TIME: isodate, - ATTR_READING_USAGE: kwh, - ATTR_READING_COST: cost, - } - for _, _, isodate, kwh, cost in self._usage - ] - - return history - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attributes = {ATTR_USAGE_HISTORY: self.history} - - return attributes - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest usage from SRP Energy.""" - start_date = datetime.now() + timedelta(days=-1) - end_date = datetime.now() - - try: - - usage = self._client.usage(start_date, end_date) - - daily_usage = 0.0 - for _, _, _, kwh, _ in usage: - daily_usage += float(kwh) - - if usage: - - self._state = daily_usage - self._usage = usage - - else: - _LOGGER.error("Unable to fetch data from SRP. No data") - - except (ConnectError, HTTPError, Timeout) as error: - _LOGGER.error("Unable to connect to SRP. %s", error) - except ValueError as error: - _LOGGER.error("Value error connecting to SRP. %s", error) - except TypeError as error: - _LOGGER.error( - "Type error connecting to SRP. " "Check username and password. %s", - error, - ) diff --git a/requirements_all.txt b/requirements_all.txt index bf217688d266e6..011e8b7a096100 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1814,9 +1814,6 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.sql sqlalchemy==1.3.8 -# homeassistant.components.srp_energy -srpenergy==1.0.6 - # homeassistant.components.starlingbank starlingbank==3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e846c9c416ac1..c0e94a5afe58e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -407,9 +407,6 @@ somecomfort==0.5.2 # homeassistant.components.sql sqlalchemy==1.3.8 -# homeassistant.components.srp_energy -srpenergy==1.0.6 - # homeassistant.components.statsd statsd==3.2.1 diff --git a/tests/components/srp_energy/__init__.py b/tests/components/srp_energy/__init__.py deleted file mode 100644 index 2a278cf1d38de2..00000000000000 --- a/tests/components/srp_energy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the srp_energy component.""" diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py deleted file mode 100644 index e33902c3fd8c6e..00000000000000 --- a/tests/components/srp_energy/test_sensor.py +++ /dev/null @@ -1,62 +0,0 @@ -"""The tests for the Srp Energy Platform.""" -from unittest.mock import patch -import logging -from homeassistant.setup import async_setup_component - -_LOGGER = logging.getLogger(__name__) - -VALID_CONFIG_MINIMAL = { - "sensor": { - "platform": "srp_energy", - "username": "foo", - "password": "bar", - "id": 1234, - } -} - -PATCH_INIT = "srpenergy.client.SrpEnergyClient.__init__" -PATCH_VALIDATE = "srpenergy.client.SrpEnergyClient.validate" -PATCH_USAGE = "srpenergy.client.SrpEnergyClient.usage" - - -def mock_usage(self, startdate, enddate): # pylint: disable=invalid-name - """Mock srpusage usage.""" - _LOGGER.log(logging.INFO, "Calling mock usage") - usage = [ - ("9/19/2018", "12:00 AM", "2018-09-19T00:00:00-7:00", "1.2", "0.17"), - ("9/19/2018", "1:00 AM", "2018-09-19T01:00:00-7:00", "2.1", "0.30"), - ("9/19/2018", "2:00 AM", "2018-09-19T02:00:00-7:00", "1.5", "0.23"), - ("9/19/2018", "9:00 PM", "2018-09-19T21:00:00-7:00", "1.2", "0.19"), - ("9/19/2018", "10:00 PM", "2018-09-19T22:00:00-7:00", "1.1", "0.18"), - ("9/19/2018", "11:00 PM", "2018-09-19T23:00:00-7:00", "0.4", "0.09"), - ] - return usage - - -async def test_setup_with_config(hass): - """Test the platform setup with configuration.""" - with patch(PATCH_INIT, return_value=None), patch( - PATCH_VALIDATE, return_value=True - ), patch(PATCH_USAGE, new=mock_usage): - - await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) - - state = hass.states.get("sensor.srp_energy") - assert state is not None - - -async def test_daily_usage(hass): - """Test the platform daily usage.""" - with patch(PATCH_INIT, return_value=None), patch( - PATCH_VALIDATE, return_value=True - ), patch(PATCH_USAGE, new=mock_usage): - - await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) - - state = hass.states.get("sensor.srp_energy") - - assert state - assert state.state == "7.50" - - assert state.attributes - assert state.attributes.get("unit_of_measurement") From ad9daa922b7826bb40cce144c0d4a348378c7a95 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 12:16:41 +0200 Subject: [PATCH 0419/3953] Remove deprecated fedex integration (ADR-0004) (#26822) --- .coveragerc | 1 - homeassistant/components/fedex/__init__.py | 1 - homeassistant/components/fedex/manifest.json | 10 -- homeassistant/components/fedex/sensor.py | 120 ------------------- requirements_all.txt | 3 - 5 files changed, 135 deletions(-) delete mode 100644 homeassistant/components/fedex/__init__.py delete mode 100644 homeassistant/components/fedex/manifest.json delete mode 100644 homeassistant/components/fedex/sensor.py diff --git a/.coveragerc b/.coveragerc index 256a6b3e41eb6d..9fe3e10c8bc27a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -198,7 +198,6 @@ omit = homeassistant/components/evohome/* homeassistant/components/familyhub/camera.py homeassistant/components/fastdotcom/* - homeassistant/components/fedex/sensor.py homeassistant/components/ffmpeg/camera.py homeassistant/components/fibaro/* homeassistant/components/filesize/sensor.py diff --git a/homeassistant/components/fedex/__init__.py b/homeassistant/components/fedex/__init__.py deleted file mode 100644 index d685ab50372de5..00000000000000 --- a/homeassistant/components/fedex/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The fedex component.""" diff --git a/homeassistant/components/fedex/manifest.json b/homeassistant/components/fedex/manifest.json deleted file mode 100644 index b34a8b8383ef85..00000000000000 --- a/homeassistant/components/fedex/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "fedex", - "name": "Fedex", - "documentation": "https://www.home-assistant.io/components/fedex", - "requirements": [ - "fedexdeliverymanager==1.0.6" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/fedex/sensor.py b/homeassistant/components/fedex/sensor.py deleted file mode 100644 index 2f499e52e234e5..00000000000000 --- a/homeassistant/components/fedex/sensor.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Sensor for Fedex packages.""" -import logging -from collections import defaultdict -from datetime import timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, - ATTR_ATTRIBUTION, - CONF_SCAN_INTERVAL, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from homeassistant.util import slugify -from homeassistant.util.dt import now, parse_date - -_LOGGER = logging.getLogger(__name__) - -COOKIE = "fedexdeliverymanager_cookies.pickle" - -DOMAIN = "fedex" - -ICON = "mdi:package-variant-closed" - -STATUS_DELIVERED = "delivered" - -SCAN_INTERVAL = timedelta(seconds=1800) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, - } -) - - -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) - - try: - cookie = hass.config.path(COOKIE) - session = fedexdeliverymanager.get_session( - config.get(CONF_USERNAME), config.get(CONF_PASSWORD), cookie_path=cookie - ) - except fedexdeliverymanager.FedexError: - _LOGGER.exception("Could not connect to Fedex Delivery Manager") - return False - - add_entities([FedexSensor(session, name, update_interval)], True) - - -class FedexSensor(Entity): - """Fedex Sensor.""" - - def __init__(self, session, name, interval): - """Initialize the sensor.""" - self._session = session - self._name = name - self._attributes = None - self._state = None - self.update = Throttle(interval)(self._update) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name or DOMAIN - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return "packages" - - def _update(self): - """Update device state.""" - import fedexdeliverymanager - - status_counts = defaultdict(int) - for package in fedexdeliverymanager.get_packages(self._session): - status = slugify(package["primary_status"]) - skip = ( - status == STATUS_DELIVERED - and parse_date(package["delivery_date"]) < now().date() - ) - if skip: - continue - status_counts[status] += 1 - self._attributes = {ATTR_ATTRIBUTION: fedexdeliverymanager.ATTRIBUTION} - self._attributes.update(status_counts) - self._state = sum(status_counts.values()) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - @property - def icon(self): - """Icon to use in the frontend.""" - return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 011e8b7a096100..3214c5e43ab38a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -478,9 +478,6 @@ evohome-async==0.3.3b4 # homeassistant.components.fastdotcom fastdotcom==0.0.3 -# homeassistant.components.fedex -fedexdeliverymanager==1.0.6 - # homeassistant.components.feedreader feedparser-homeassistant==5.2.2.dev1 From 2c80ce3195e4e8d20e0fd40c918a9881dea84506 Mon Sep 17 00:00:00 2001 From: MajestyIV Date: Mon, 23 Sep 2019 14:39:10 +0200 Subject: [PATCH 0420/3953] HM-CC-TC was not recognized (#26623) * HM-CC-TC was not recognized * guard instead of exception --- homeassistant/components/homematic/climate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 1a2f642f91ccc5..935ebb9b497620 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -94,7 +94,9 @@ def preset_mode(self): if self._data.get("BOOST_MODE", False): return "boost" - # Get the name of the mode + if not self._hm_control_mode: + return None + mode = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][self._hm_control_mode] mode = mode.lower() @@ -177,8 +179,9 @@ def _hm_control_mode(self): """Return Control mode.""" if HMIP_CONTROL_MODE in self._data: return self._data[HMIP_CONTROL_MODE] + # Homematic - return self._data["CONTROL_MODE"] + return self._data.get("CONTROL_MODE") def _init_data_struct(self): """Generate a data dict (self._data) from the Homematic metadata.""" From 61634d0a645f939b46772ca43a18f723287918f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 23 Sep 2019 14:08:44 +0100 Subject: [PATCH 0421/3953] Store ZHA light brightness when fading off to turn on at the correct brightness (#26680) * Use light's on_level in ZHA to turn on at the correct brightness Previously, if the light is turned off with a time transition, the brightness level stored in the light will be 1. The next time the light is turned on with no explicit brightness, it will be at 1. This is solved by storing the current brightness in on_level before turning off, and then using that when turning on (by calling the onOff cluster 'on' command). * store off light level locally to avoid wearing device's flash memory * store off brightness in HA attributes * improve set/clear of off_brightness * fix device_state_attributes; clear off_brightness when light goes on * fix tests --- homeassistant/components/zha/light.py | 25 ++++++++++++++++++++----- tests/components/zha/test_light.py | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index c2273c54073ac0..fb388afac0ffc9 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -25,8 +25,6 @@ _LOGGER = logging.getLogger(__name__) -DEFAULT_DURATION = 5 - CAPABILITIES_COLOR_LOOP = 0x4 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 @@ -91,6 +89,7 @@ def __init__(self, unique_id, zha_device, channels, **kwargs): self._color_temp = None self._hs_color = None self._brightness = None + self._off_brightness = None self._effect_list = [] self._effect = None self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) @@ -130,7 +129,8 @@ def brightness(self): @property def device_state_attributes(self): """Return state attributes.""" - return self.state_attributes + attributes = {"off_brightness": self._off_brightness} + return attributes def set_level(self, value): """Set the brightness of this light between 0..254. @@ -171,6 +171,8 @@ def supported_features(self): def async_set_state(self, state): """Set the state.""" self._state = bool(state) + if state: + self._off_brightness = None self.async_schedule_update_ha_state() async def async_added_to_hass(self): @@ -191,6 +193,8 @@ def async_restore_last_state(self, last_state): self._state = last_state.state == STATE_ON if "brightness" in last_state.attributes: self._brightness = last_state.attributes["brightness"] + if "off_brightness" in last_state.attributes: + self._off_brightness = last_state.attributes["off_brightness"] if "color_temp" in last_state.attributes: self._color_temp = last_state.attributes["color_temp"] if "hs_color" in last_state.attributes: @@ -201,10 +205,13 @@ def async_restore_last_state(self, last_state): async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) - duration = transition * 10 if transition is not None else DEFAULT_DURATION + duration = transition * 10 if transition else 0 brightness = kwargs.get(light.ATTR_BRIGHTNESS) effect = kwargs.get(light.ATTR_EFFECT) + if brightness is None and self._off_brightness is not None: + brightness = self._off_brightness + t_log = {} if ( brightness is not None or transition @@ -225,13 +232,14 @@ async def async_turn_on(self, **kwargs): self._brightness = level if brightness is None or brightness: + # since some lights don't always turn on with move_to_level_with_on_off, + # we should call the on command on the on_off cluster if brightness is not 0. result = await self._on_off_channel.on() t_log["on_off"] = result if not isinstance(result, list) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return self._state = True - if ( light.ATTR_COLOR_TEMP in kwargs and self.supported_features & light.SUPPORT_COLOR_TEMP @@ -289,6 +297,7 @@ async def async_turn_on(self, **kwargs): t_log["color_loop_set"] = result self._effect = None + self._off_brightness = None self.debug("turned on: %s", t_log) self.async_schedule_update_ha_state() @@ -296,6 +305,7 @@ async def async_turn_off(self, **kwargs): """Turn the entity off.""" duration = kwargs.get(light.ATTR_TRANSITION) supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS + if duration and supports_level: result = await self._level_channel.move_to_level_with_on_off( 0, duration * 10 @@ -306,6 +316,11 @@ async def async_turn_off(self, **kwargs): if not isinstance(result, list) or result[1] is not Status.SUCCESS: return self._state = False + + if duration and supports_level: + # store current brightness so that the next turn_on uses it. + self._off_brightness = self._brightness + self.async_schedule_update_ha_state() async def async_update(self): diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 2b167d52ac65d1..3101abc52649fa 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -230,7 +230,7 @@ async def async_test_level_on_off_from_hass( 4, (types.uint8_t, types.uint16_t), 10, - 5.0, + 0, expect_reply=True, manufacturer=None, ) From 8a9e47d3c7b95d00af6ca9b8be596c4601eef9e7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 15:43:48 +0200 Subject: [PATCH 0422/3953] Bump pyotp to 2.3.0 (#26849) --- homeassistant/auth/mfa_modules/notify.py | 2 +- homeassistant/auth/mfa_modules/totp.py | 2 +- homeassistant/components/otp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index a6a754fc2a6f3f..01c5c12efb7588 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -22,7 +22,7 @@ SetupFlow, ) -REQUIREMENTS = ["pyotp==2.2.7"] +REQUIREMENTS = ["pyotp==2.3.0"] CONF_MESSAGE = "message" diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index d6d901ac3b1fe5..4e417fca2198b1 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -16,7 +16,7 @@ SetupFlow, ) -REQUIREMENTS = ["pyotp==2.2.7", "PyQRCode==1.2.1"] +REQUIREMENTS = ["pyotp==2.3.0", "PyQRCode==1.2.1"] CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA) diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index cea246af328d26..112fca24194a8e 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -3,7 +3,7 @@ "name": "Otp", "documentation": "https://www.home-assistant.io/components/otp", "requirements": [ - "pyotp==2.2.7" + "pyotp==2.3.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 3214c5e43ab38a..07dd2c3965193c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1367,7 +1367,7 @@ pyotgw==0.4b4 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp -pyotp==2.2.7 +pyotp==2.3.0 # homeassistant.components.owlet pyowlet==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0e94a5afe58e6..7ece1a030aae6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ pyopenuv==1.0.9 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp -pyotp==2.2.7 +pyotp==2.3.0 # homeassistant.components.ps4 pyps4-homeassistant==0.8.7 From 911d2893339c59fc817912a3fc171e8d219f0bfc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Sep 2019 16:05:02 +0200 Subject: [PATCH 0423/3953] Remove deprecated linksys_ap integration (ADR-0004) (#26847) --- .coveragerc | 1 - .../components/linksys_ap/__init__.py | 1 - .../components/linksys_ap/device_tracker.py | 107 ------------------ .../components/linksys_ap/manifest.json | 10 -- requirements_all.txt | 1 - 5 files changed, 120 deletions(-) delete mode 100644 homeassistant/components/linksys_ap/__init__.py delete mode 100644 homeassistant/components/linksys_ap/device_tracker.py delete mode 100644 homeassistant/components/linksys_ap/manifest.json diff --git a/.coveragerc b/.coveragerc index 9fe3e10c8bc27a..f8c632f7053572 100644 --- a/.coveragerc +++ b/.coveragerc @@ -348,7 +348,6 @@ omit = homeassistant/components/lifx_legacy/light.py homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py - homeassistant/components/linksys_ap/device_tracker.py homeassistant/components/linksys_smart/device_tracker.py homeassistant/components/linky/__init__.py homeassistant/components/linky/sensor.py diff --git a/homeassistant/components/linksys_ap/__init__.py b/homeassistant/components/linksys_ap/__init__.py deleted file mode 100644 index 5898aa36e98524..00000000000000 --- a/homeassistant/components/linksys_ap/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The linksys_ap component.""" diff --git a/homeassistant/components/linksys_ap/device_tracker.py b/homeassistant/components/linksys_ap/device_tracker.py deleted file mode 100644 index d40de718f902ca..00000000000000 --- a/homeassistant/components/linksys_ap/device_tracker.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Support for Linksys Access Points.""" -import base64 -import logging - -import requests -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA, - DeviceScanner, -) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL - -INTERFACES = 2 -DEFAULT_TIMEOUT = 10 - -_LOGGER = logging.getLogger(__name__) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - } -) - - -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: - return None - - -class LinksysAPDeviceScanner(DeviceScanner): - """This class queries a Linksys Access Point.""" - - def __init__(self, config): - """Initialize the scanner.""" - self.host = config[CONF_HOST] - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] - self.verify_ssl = config[CONF_VERIFY_SSL] - self.last_results = [] - - # Check if the access point is accessible - response = self._make_request() - if not response.status_code == 200: - raise ConnectionError("Cannot connect to Linksys Access Point") - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - - return self.last_results - - def get_device_name(self, device): - """ - Return the name (if known) of the device. - - Linksys does not provide an API to get a name for a device, - so we just return None - """ - return None - - def _update_info(self): - """Check for connected devices.""" - from bs4 import BeautifulSoup as BS - - _LOGGER.info("Checking Linksys AP") - - self.last_results = [] - for interface in range(INTERFACES): - request = self._make_request(interface) - self.last_results.extend( - [ - x.find_all("td")[1].text - for x in BS(request.content, "html.parser").find_all( - class_="section-row" - ) - ] - ) - - return True - - def _make_request(self, unit=0): - """Create a request to get the data.""" - # No, the '&&' is not a typo - this is expected by the web interface. - login = base64.b64encode(bytes(self.username, "utf8")).decode("ascii") - pwd = base64.b64encode(bytes(self.password, "utf8")).decode("ascii") - url = "https://{}/StatusClients.htm&&unit={}&vap=0".format(self.host, unit) - return requests.get( - url, - timeout=DEFAULT_TIMEOUT, - verify=self.verify_ssl, - cookies={"LoginName": login, "LoginPWD": pwd}, - ) diff --git a/homeassistant/components/linksys_ap/manifest.json b/homeassistant/components/linksys_ap/manifest.json deleted file mode 100644 index 31fafe17edd67f..00000000000000 --- a/homeassistant/components/linksys_ap/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "linksys_ap", - "name": "Linksys ap", - "documentation": "https://www.home-assistant.io/components/linksys_ap", - "requirements": [ - "beautifulsoup4==4.8.0" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/requirements_all.txt b/requirements_all.txt index 07dd2c3965193c..c8f95e29703fdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -260,7 +260,6 @@ batinfo==0.4.2 # homeassistant.components.eddystone_temperature # beacontools[scan]==1.2.3 -# homeassistant.components.linksys_ap # homeassistant.components.scrape beautifulsoup4==4.8.0 From 9c0fbfd101210e27ad162bce355c32ada0b33ab1 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 23 Sep 2019 13:35:27 -0400 Subject: [PATCH 0424/3953] Bump up ZHA dependencies (#26746) * Update ZHA to track zigpy changes. * Update ZHA dependencies. * Update tests. * Make coverage happy again. --- .coveragerc | 1 + homeassistant/components/zha/core/gateway.py | 2 +- homeassistant/components/zha/core/patches.py | 18 +++--------------- homeassistant/components/zha/manifest.json | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 4 ++-- tests/components/zha/test_binary_sensor.py | 4 ++-- tests/components/zha/test_device_tracker.py | 4 ++-- tests/components/zha/test_fan.py | 4 ++-- tests/components/zha/test_light.py | 8 ++++---- tests/components/zha/test_lock.py | 4 ++-- tests/components/zha/test_sensor.py | 2 +- tests/components/zha/test_switch.py | 4 ++-- 13 files changed, 30 insertions(+), 41 deletions(-) diff --git a/.coveragerc b/.coveragerc index f8c632f7053572..88fdbd45f9a017 100644 --- a/.coveragerc +++ b/.coveragerc @@ -762,6 +762,7 @@ omit = homeassistant/components/zha/core/device.py homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py + homeassistant/components/zha/core/patches.py homeassistant/components/zha/core/registries.py homeassistant/components/zha/device_entity.py homeassistant/components/zha/entity.py diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index be09312f6931cb..d2f842956dabef 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -310,7 +310,7 @@ def _async_get_or_create_device(self, zigpy_device): @callback def async_device_became_available( - self, sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args + self, sender, profile, cluster, src_ep, dst_ep, message ): """Handle tasks when a device becomes available.""" self.async_update_device(sender) diff --git a/homeassistant/components/zha/core/patches.py b/homeassistant/components/zha/core/patches.py index 75ef8cce19d18d..d64839026025ea 100644 --- a/homeassistant/components/zha/core/patches.py +++ b/homeassistant/components/zha/core/patches.py @@ -9,9 +9,7 @@ def apply_application_controller_patch(zha_gateway): """Apply patches to ZHA objects.""" # Patch handle_message until zigpy can provide an event here - def handle_message( - sender, is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args - ): + def handle_message(sender, profile, cluster, src_ep, dst_ep, message): """Handle message from a device.""" if ( not sender.initializing @@ -19,18 +17,8 @@ def handle_message( and not zha_gateway.devices[sender.ieee].available ): zha_gateway.async_device_became_available( - sender, - is_reply, - profile, - cluster, - src_ep, - dst_ep, - tsn, - command_id, - args, + sender, profile, cluster, src_ep, dst_ep, message ) - return sender.handle_message( - is_reply, profile, cluster, src_ep, dst_ep, tsn, command_id, args - ) + return sender.handle_message(profile, cluster, src_ep, dst_ep, message) zha_gateway.application_controller.handle_message = handle_message diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e78661a04e534d..7744b9f223339f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ - "bellows-homeassistant==0.9.1", + "bellows-homeassistant==0.10.0", "zha-quirks==0.0.23", - "zigpy-deconz==0.3.0", - "zigpy-homeassistant==0.8.0", - "zigpy-xbee-homeassistant==0.4.0", + "zigpy-deconz==0.4.0", + "zigpy-homeassistant==0.9.0", + "zigpy-xbee-homeassistant==0.5.0", "zigpy-zigate==0.3.1" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index c8f95e29703fdb..c9b28ae1a14062 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ beautifulsoup4==4.8.0 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.9.1 +bellows-homeassistant==0.10.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.6.0 @@ -2020,13 +2020,13 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.3.0 +zigpy-deconz==0.4.0 # homeassistant.components.zha -zigpy-homeassistant==0.8.0 +zigpy-homeassistant==0.9.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.4.0 +zigpy-xbee-homeassistant==0.5.0 # homeassistant.components.zha zigpy-zigate==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ece1a030aae6a..333c644ebf6be0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.9.1 +bellows-homeassistant==0.10.0 # homeassistant.components.caldav caldav==0.6.1 @@ -434,4 +434,4 @@ wakeonlan==1.1.6 zeroconf==0.23.0 # homeassistant.components.zha -zigpy-homeassistant==0.8.0 +zigpy-homeassistant==0.9.0 diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 7ba7d94d251855..47f81787acd9a0 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -74,13 +74,13 @@ async def async_test_binary_sensor_on_off(hass, cluster, entity_id): """Test getting on and off messages for binary sensors.""" # binary sensor on attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # binary sensor off attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index b4c313041f3ea5..6a7638d9f868c0 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -67,10 +67,10 @@ async def test_device_tracker(hass, config_entry, zha_gateway): # turn state flip attr = make_attribute(0x0020, 23) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) attr = make_attribute(0x0021, 200) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) zigpy_device.last_seen = time.time() + 10 next_update = dt_util.utcnow() + timedelta(seconds=30) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index a8c55890acf018..3fe5e7937c86e2 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -44,13 +44,13 @@ async def test_fan(hass, config_entry, zha_gateway): # turn on at fan attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at fan attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 3101abc52649fa..08c6cfe18cf211 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -123,13 +123,13 @@ async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at light attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -138,7 +138,7 @@ async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -243,7 +243,7 @@ async def async_test_level_on_off_from_hass( async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): """Test dimmer functionality from the light.""" attr = make_attribute(0, level) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == expected_state # hass uses None for brightness of 0 in state attributes diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 49a60a0f760028..7381b557310b7b 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -43,13 +43,13 @@ async def test_lock(hass, config_entry, zha_gateway): # set state to locked attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_LOCKED # set state to unlocked attr.value.value = 2 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNLOCKED diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index dc3ea35229f2b4..faa44f349272a0 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -177,7 +177,7 @@ async def send_attribute_report(hass, cluster, attrid, value): device is paired to the zigbee network. """ attr = make_attribute(attrid, value) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 07d5bd8ca7c7c5..ac6bc73b809db6 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -44,13 +44,13 @@ async def test_switch(hass, config_entry, zha_gateway): # turn on at switch attr = make_attribute(0, 1) - cluster.handle_message(False, 1, 0x0A, [[attr]]) + cluster.handle_message(1, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at switch attr.value.value = 0 - cluster.handle_message(False, 0, 0x0A, [[attr]]) + cluster.handle_message(0, 0x0A, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF From 5e6840d8f47730a5d0538a1201179f44ec8b0585 Mon Sep 17 00:00:00 2001 From: Balazs Sandor Date: Mon, 23 Sep 2019 19:41:35 +0200 Subject: [PATCH 0425/3953] fix onvif/camera setting up error (#26825) --- homeassistant/components/onvif/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 4fdd513f840f28..0c116568780a50 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -156,7 +156,7 @@ async def async_initialize(self): Initializes the camera by obtaining the input uri and connecting to the camera. Also retrieves the ONVIF profiles. """ - from aiohttp.client_exceptions import ClientConnectorError + from aiohttp.client_exceptions import ClientConnectionError from homeassistant.exceptions import PlatformNotReady from zeep.exceptions import Fault @@ -167,7 +167,7 @@ async def async_initialize(self): await self.async_check_date_and_time() await self.async_obtain_input_uri() self.setup_ptz() - except ClientConnectorError as err: + except ClientConnectionError as err: _LOGGER.warning( "Couldn't connect to camera '%s', but will " "retry later. Error: %s", self._name, From a7579924098b1e79bf070380fd54da4abe30e722 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 23 Sep 2019 19:42:09 +0200 Subject: [PATCH 0426/3953] Bump homematicip_cloud to 0.10.11 (#26852) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 2a041ce6689e77..b83358822b9f3f 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.10" + "homematicip==0.10.11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c9b28ae1a14062..62218b14780b03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.10 +homematicip==0.10.11 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 333c644ebf6be0..b824054a93afb9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -187,7 +187,7 @@ home-assistant-frontend==20190919.0 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.10 +homematicip==0.10.11 # homeassistant.components.google # homeassistant.components.remember_the_milk From 0c78f9e20c645bbf961d5438b5a5d1abe03c77ec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 Sep 2019 13:18:28 -0700 Subject: [PATCH 0427/3953] Updated frontend to 20190919.1 --- homeassistant/components/frontend/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 896867fcb172eb..605c7f9d7e3464 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", - "requirements": ["home-assistant-frontend==20190919.0"], + "home-assistant-frontend==20190919.1" "dependencies": [ "api", "auth", From 7bed79255ea0d681a77a18dc56bfcc9ce57ebab1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 Sep 2019 13:18:28 -0700 Subject: [PATCH 0428/3953] Updated frontend to 20190919.1 --- homeassistant/components/frontend/manifest.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 18c19a9012a7da..e50989a15df3d4 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,9 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", - "requirements": [ - "home-assistant-frontend==20190919.0" - ], + "requirements": ["home-assistant-frontend==20190919.1"], "dependencies": [ "api", "auth", From 9919f5f924a9f2357b6e4b8aaaa988edcf8a58c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Thu, 12 Sep 2019 14:00:58 +0200 Subject: [PATCH 0429/3953] Bump zigpy-zigate to 0.3.1 (#26600) * Bump zigpy-zigate to 0.3.1 Bump zigpy-zigate to 0.3.1 (fix Rpi.GPIO dependency) * Bump zigpy-zigate to 0.3.1 Bump zigpy-zigate to 0.3.1 (fix Rpi.GPIO dependency) --- homeassistant/components/zha/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 3095d14061919e..e78661a04e534d 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.3.0", "zigpy-homeassistant==0.8.0", "zigpy-xbee-homeassistant==0.4.0", - "zigpy-zigate==0.2.0" + "zigpy-zigate==0.3.1" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 72aebd96196c5d..c0a79b33ad331d 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==20190919.0 +home-assistant-frontend==20190919.1 importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3691ce2c3791c6..07b6599cd69fee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.0 +home-assistant-frontend==20190919.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 @@ -2025,7 +2025,7 @@ zigpy-homeassistant==0.8.0 zigpy-xbee-homeassistant==0.4.0 # homeassistant.components.zha -zigpy-zigate==0.2.0 +zigpy-zigate==0.3.1 # homeassistant.components.zoneminder zm-py==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d40ed239a09f21..f879f546c07144 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.0 +home-assistant-frontend==20190919.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 9401d9e28677f57d729b02cb6cd22b545caa6bf7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 Sep 2019 14:20:27 -0700 Subject: [PATCH 0430/3953] Fix frontend --- 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 605c7f9d7e3464..e50989a15df3d4 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", - "home-assistant-frontend==20190919.1" + "requirements": ["home-assistant-frontend==20190919.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 842cf4840c832a..47325a6c930e61 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==20190919.0 +home-assistant-frontend==20190919.1 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 62218b14780b03..04b6eaeb3e9db3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -637,7 +637,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.0 +home-assistant-frontend==20190919.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b824054a93afb9..b962ca1280b5d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -181,7 +181,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.0 +home-assistant-frontend==20190919.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 5d49d9c951943ddf13bc9cb56cee21700054b7fd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 24 Sep 2019 00:32:13 +0000 Subject: [PATCH 0431/3953] [ci skip] Translation update --- .../ambiclimate/.translations/no.json | 2 +- .../binary_sensor/.translations/ca.json | 65 +++++++++++++++++++ .../binary_sensor/.translations/ro.json | 45 +++++++++++++ .../binary_sensor/.translations/ru.json | 15 +++++ .../components/izone/.translations/da.json | 15 +++++ .../logi_circle/.translations/no.json | 2 +- .../components/plex/.translations/ca.json | 12 ++++ .../components/plex/.translations/da.json | 45 +++++++++++++ .../components/plex/.translations/no.json | 14 +++- .../components/plex/.translations/ro.json | 24 +++++++ .../components/plex/.translations/ru.json | 12 ++++ .../smartthings/.translations/no.json | 2 +- 12 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/ca.json create mode 100644 homeassistant/components/binary_sensor/.translations/ro.json create mode 100644 homeassistant/components/binary_sensor/.translations/ru.json create mode 100644 homeassistant/components/izone/.translations/da.json create mode 100644 homeassistant/components/plex/.translations/da.json create mode 100644 homeassistant/components/plex/.translations/ro.json diff --git a/homeassistant/components/ambiclimate/.translations/no.json b/homeassistant/components/ambiclimate/.translations/no.json index 7bb124ae5433e1..e84de4ffc2261f 100644 --- a/homeassistant/components/ambiclimate/.translations/no.json +++ b/homeassistant/components/ambiclimate/.translations/no.json @@ -9,7 +9,7 @@ "default": "Vellykket autentisering med Ambiclimate" }, "error": { - "follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker p\u00e5 Send", + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker p\u00e5 Send", "no_token": "Ikke autentisert med Ambiclimate" }, "step": { diff --git a/homeassistant/components/binary_sensor/.translations/ca.json b/homeassistant/components/binary_sensor/.translations/ca.json new file mode 100644 index 00000000000000..434c236418b37d --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ca.json @@ -0,0 +1,65 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "Bateria de {entity_name} baixa", + "is_connected": "{entity_name} est\u00e0 connectat", + "is_gas": "{entity_name} est\u00e0 detectant gas", + "is_light": "{entity_name} est\u00e0 detectant llum", + "is_locked": "{entity_name} est\u00e0 bloquejat", + "is_motion": "{entity_name} est\u00e0 detectant moviment", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta llum", + "is_no_motion": "{entity_name} no detecta moviment", + "is_no_smoke": "{entity_name} no detecta fum", + "is_no_sound": "{entity_name} no detecta so", + "is_no_vibration": "{entity_name} no detecta vibraci\u00f3", + "is_not_bat_low": "Bateria de {entity_name} normal", + "is_not_connected": "{entity_name} est\u00e0 desconnectat", + "is_not_locked": "{entity_name} est\u00e0 desbloquejat", + "is_not_occupied": "{entity_name} no est\u00e0 ocupat", + "is_not_powered": "{entity_name} no est\u00e0 alimentat", + "is_not_present": "{entity_name} no est\u00e0 present", + "is_occupied": "{entity_name} est\u00e0 ocupat", + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s", + "is_open": "{entity_name} est\u00e0 obert", + "is_powered": "{entity_name} est\u00e0 alimentat", + "is_present": "{entity_name} est\u00e0 present", + "is_smoke": "{entity_name} est\u00e0 detectant fum", + "is_sound": "{entity_name} est\u00e0 detectant so", + "is_unsafe": "{entity_name} \u00e9s insegur", + "is_vibration": "{entity_name} est\u00e0 detectant vibraci\u00f3" + }, + "trigger_type": { + "bat_low": "Bateria de {entity_name} baixa", + "closed": "{entity_name} est\u00e0 tancat", + "connected": "{entity_name} est\u00e0 connectat", + "gas": "{entity_name} ha comen\u00e7at a detectar gas", + "light": "{entity_name} ha comen\u00e7at a detectar llum", + "locked": "{entity_name} est\u00e0 bloquejat", + "motion": "{entity_name} ha comen\u00e7at a detectar moviment", + "moving": "{entity_name} ha comen\u00e7at a moure's", + "no_gas": "{entity_name} ha deixat de detectar gas", + "no_light": "{entity_name} ha deixat de detectar llum", + "no_motion": "{entity_name} ha deixat de detectar moviment", + "no_problem": "{entity_name} ha deixat de detectar un problema", + "no_smoke": "{entity_name} ha deixat de detectar fum", + "no_sound": "{entity_name} ha deixat de detectar so", + "no_vibration": "{entity_name} ha deixat de detectar vibraci\u00f3", + "not_bat_low": "Bateria de {entity_name} normal", + "not_connected": "{entity_name} est\u00e0 desconnectat", + "not_locked": "{entity_name} est\u00e0 desbloquejat", + "not_moving": "{entity_name} ha parat de moure's", + "not_powered": "{entity_name} no est\u00e0 alimentat", + "not_present": "{entity_name} no est\u00e0 present", + "powered": "{entity_name} alimentat", + "present": "{entity_name} present", + "problem": "{entity_name} ha comen\u00e7at a detectar un problema", + "smoke": "{entity_name} ha comen\u00e7at a detectar fum", + "sound": "{entity_name} ha comen\u00e7at a detectar so", + "turned_off": "{entity_name} apagat", + "turned_on": "{entity_name} enc\u00e8s", + "vibration": "{entity_name} ha comen\u00e7at a detectar vibraci\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ro.json b/homeassistant/components/binary_sensor/.translations/ro.json new file mode 100644 index 00000000000000..438822a97f5bf5 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ro.json @@ -0,0 +1,45 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} oprit", + "is_on": "{entity_name} pornit" + }, + "trigger_type": { + "gas": "{entity_name} a \u00eenceput s\u0103 detecteze gaz", + "hot": "{entity_name} a devenit fierbinte", + "locked": "{entity_name} blocat", + "motion": "{entity_name} a \u00eenceput s\u0103 detecteze mi\u0219care", + "moving": "{entity_name} a \u00eenceput s\u0103 se mi\u0219te", + "no_light": "{entity_name} a oprit detectarea luminii", + "no_motion": "{entity_name} a oprit detectarea mi\u0219c\u0103rii", + "no_problem": "{entity_name} a oprit detectarea problemei", + "no_smoke": "{entity_name} a oprit detectarea fumului", + "no_sound": "{entity_name} a oprit detectarea de sunet", + "no_vibration": "{entity_name} a oprit detectarea vibra\u021biilor", + "not_bat_low": "{entity_name} baterie normal\u0103", + "not_cold": "{entity_name} nu mai este rece", + "not_connected": "{entity_name} deconectat", + "not_hot": "{entity_name} nu mai este fierbinte", + "not_locked": "{entity_name} deblocat", + "not_moist": "{entity_name} a devenit uscat", + "not_moving": "{entity_name} a \u00eencetat mi\u0219carea", + "not_occupied": "{entity_name} a devenit neocupat", + "not_plugged_in": "{entity_name} deconectat", + "not_powered": "{entity_name} nu este alimentat", + "not_present": "{entity_name} nu este prezent", + "not_unsafe": "{entity_name} a devenit sigur", + "occupied": "{entity_name} a devenit ocupat", + "opened": "{entity_name} deschis", + "plugged_in": "{entity_name} conectat", + "powered": "{entity_name} alimentat", + "present": "{entity_name} prezent", + "problem": "{entity_name} a \u00eenceput detectarea unei probleme", + "smoke": "{entity_name} a \u00eenceput s\u0103 detecteze fum", + "sound": "{entity_name} a \u00eenceput s\u0103 detecteze sunetul", + "turned_off": "{entity_name} oprit", + "turned_on": "{entity_name} pornit", + "unsafe": "{entity_name} a devenit nesigur", + "vibration": "{entity_name} a \u00eenceput s\u0103 detecteze vibra\u021biile" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json new file mode 100644 index 00000000000000..7d73cb8d4aa489 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name}: \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "is_cold": "{entity_name}: \u0445\u043e\u043b\u043e\u0434\u043d\u043e", + "is_connected": "{entity_name}: \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "is_gas": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0433\u0430\u0437", + "is_hot": "{entity_name}: \u0433\u043e\u0440\u044f\u0447\u043e", + "is_light": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0441\u0432\u0435\u0442", + "is_locked": "{entity_name}: \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e", + "is_moist": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0432\u043b\u0430\u0433\u0430", + "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/da.json b/homeassistant/components/izone/.translations/da.json new file mode 100644 index 00000000000000..9dc3d88322c25e --- /dev/null +++ b/homeassistant/components/izone/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Der blev ikke fundet nogen iZone-enheder p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af iZone" + }, + "step": { + "confirm": { + "description": "Vil du konfigurere iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/no.json b/homeassistant/components/logi_circle/.translations/no.json index 03c128f636ca15..c68f49509baae8 100644 --- a/homeassistant/components/logi_circle/.translations/no.json +++ b/homeassistant/components/logi_circle/.translations/no.json @@ -12,7 +12,7 @@ "error": { "auth_error": "API-autorisasjonen mislyktes.", "auth_timeout": "Autorisasjon ble tidsavbrutt da du ba om token.", - "follow_link": "Vennligst f\u00f8lg lenken og godkjen f\u00f8r du trykker send." + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker send." }, "step": { "auth": { diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index eb4f6459f4dcbe..4c24dddbe87917 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Ha fallat l'autoritzaci\u00f3", "no_servers": "No hi ha servidors enlla\u00e7ats amb el compte", + "no_token": "Proporciona un testimoni d'autenticaci\u00f3 o selecciona configuraci\u00f3 manual", "not_found": "No s'ha trobat el servidor Plex" }, "step": { + "manual_setup": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port", + "ssl": "Utilitza SSL", + "token": "Testimoni d'autenticaci\u00f3 (si \u00e9s necessari)", + "verify_ssl": "Verifica el certificat SSL" + }, + "title": "Servidor Plex" + }, "select_server": { "data": { "server": "Servidor" @@ -22,6 +33,7 @@ }, "user": { "data": { + "manual_setup": "Configuraci\u00f3 manual", "token": "Testimoni d'autenticaci\u00f3 Plex" }, "description": "Introdueix un testimoni d'autenticaci\u00f3 Plex per configurar-ho autom\u00e0ticament.", diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json new file mode 100644 index 00000000000000..ea680a638eb471 --- /dev/null +++ b/homeassistant/components/plex/.translations/da.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "all_configured": "Alle linkede servere er allerede konfigureret", + "already_configured": "Denne Plex-server er allerede konfigureret", + "already_in_progress": "Plex konfigureres", + "invalid_import": "Importeret konfiguration er ugyldig", + "unknown": "Mislykkedes af ukendt \u00e5rsag" + }, + "error": { + "faulty_credentials": "Godkendelse mislykkedes", + "no_servers": "Ingen servere knyttet til konto", + "no_token": "Angiv et token eller v\u00e6lg manuel ops\u00e6tning", + "not_found": "Plex-server ikke fundet" + }, + "step": { + "manual_setup": { + "data": { + "host": "V\u00e6rt", + "port": "Port", + "ssl": "Brug SSL", + "token": "Token (hvis n\u00f8dvendigt)", + "verify_ssl": "Bekr\u00e6ft SSL-certifikat" + }, + "title": "Plex-server" + }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Flere servere til r\u00e5dighed, v\u00e6lg en:", + "title": "V\u00e6lg Plex-server" + }, + "user": { + "data": { + "manual_setup": "Manuel ops\u00e6tning", + "token": "Plex token" + }, + "description": "Indtast et Plex-token til automatisk ops\u00e6tning eller konfigurerer en server manuelt.", + "title": "Tilslut Plex-server" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index 8ac90efe3d1474..b58cdfe728e043 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Autorisasjonen mislyktes", "no_servers": "Ingen servere koblet til kontoen", + "no_token": "Angi et token eller velg manuelt oppsett", "not_found": "Plex-server ikke funnet" }, "step": { + "manual_setup": { + "data": { + "host": "Vert", + "port": "Port", + "ssl": "Bruk SSL", + "token": "Token (hvis n\u00f8dvendig)", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "title": "Plex server" + }, "select_server": { "data": { "server": "Server" @@ -22,9 +33,10 @@ }, "user": { "data": { + "manual_setup": "Manuelt oppsett", "token": "Plex token" }, - "description": "Legg inn et Plex-token for automatisk oppsett.", + "description": "Angi et Plex-token for automatisk oppsett eller Konfigurer en servern manuelt.", "title": "Koble til Plex-server" } }, diff --git a/homeassistant/components/plex/.translations/ro.json b/homeassistant/components/plex/.translations/ro.json new file mode 100644 index 00000000000000..537bd5e3fac49f --- /dev/null +++ b/homeassistant/components/plex/.translations/ro.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "no_token": "Furniza\u021bi un token sau selecta\u021bi configurarea manual\u0103" + }, + "step": { + "manual_setup": { + "data": { + "host": "Gazd\u0103", + "port": "Port", + "ssl": "Folosi\u021bi SSL", + "token": "Token-ul (dac\u0103 este necesar)", + "verify_ssl": "Verifica\u021bi certificatul SSL" + }, + "title": "Server Plex" + }, + "user": { + "data": { + "manual_setup": "Configurare manual\u0103" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 46cd613df4ac70..b906d0d8dc6c19 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e", + "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443", "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d" }, "step": { + "manual_setup": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL", + "token": "\u0422\u043e\u043a\u0435\u043d (\u0435\u0441\u043b\u0438 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f)", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "title": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex" + }, "select_server": { "data": { "server": "\u0421\u0435\u0440\u0432\u0435\u0440" @@ -22,6 +33,7 @@ }, "user": { "data": { + "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "token": "\u0422\u043e\u043a\u0435\u043d" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d Plex \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", diff --git a/homeassistant/components/smartthings/.translations/no.json b/homeassistant/components/smartthings/.translations/no.json index fe93407b429445..b539e315ea3beb 100644 --- a/homeassistant/components/smartthings/.translations/no.json +++ b/homeassistant/components/smartthings/.translations/no.json @@ -5,7 +5,7 @@ "app_setup_error": "Kan ikke konfigurere SmartApp. Vennligst pr\u00f8v p\u00e5 nytt.", "base_url_not_https": "`base_url` for `http` komponenten m\u00e5 konfigureres og starte med `https://`.", "token_already_setup": "Token har allerede blitt satt opp.", - "token_forbidden": "Tollet har ikke de n\u00f8dvendige OAuth m\u00e5lene.", + "token_forbidden": "Tokenet har ikke de n\u00f8dvendige OAuth-omfangene.", "token_invalid_format": "Token m\u00e5 v\u00e6re i UID/GUID format", "token_unauthorized": "Tollet er ugyldig eller ikke lenger autorisert.", "webhook_error": "SmartThings kunne ikke validere endepunktet konfigurert i `base_url`. Vennligst se komponent krav." From 44082869b42ba1c9b5a3849136d68d5fddc27984 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 24 Sep 2019 02:55:11 +0200 Subject: [PATCH 0432/3953] Group Linky sensors to Linky meter device (#26738) * Group Linky sensors to Llnky meter device * Fix Linky meter identifiers --- homeassistant/components/linky/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index 5ff04c5ee70a38..489e66c2b12482 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -145,8 +145,8 @@ def device_state_attributes(self): def device_info(self): """Return device information.""" return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, + "identifiers": {(DOMAIN, self._username)}, + "name": "Linky meter", "manufacturer": "Enedis", } From 6fe5582c6a4318a202d27919b4aa9e18e6b27528 Mon Sep 17 00:00:00 2001 From: Tim McCormick Date: Tue, 24 Sep 2019 02:45:14 +0100 Subject: [PATCH 0433/3953] Add unit to 'charging_level_hv' bwm_connected_drive sensor (#26861) * Update sensor.py Add unit to charging_level_hv, which allows correct graphing in UI * Update sensor.py Add space after # --- homeassistant/components/bmw_connected_drive/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 96d541b1955337..28a4e853f2cb6f 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -24,6 +24,8 @@ "remaining_fuel": ["mdi:gas-station", VOLUME_LITERS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], + # No icon as this is dealt with directly as a special case in icon() + "charging_level_hv": [None, "%"], } ATTR_TO_HA_IMPERIAL = { @@ -35,6 +37,8 @@ "remaining_fuel": ["mdi:gas-station", VOLUME_GALLONS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], + # No icon as this is dealt with directly as a special case in icon() + "charging_level_hv": [None, "%"], } From 53e6b8ade6846475adfc9f752826df9e36925e09 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 Sep 2019 23:23:53 -0700 Subject: [PATCH 0434/3953] Add reproduce state template (#26866) * Add reproduce state template * Handle invalid state --- script/scaffold/__main__.py | 10 ++- script/scaffold/docs.py | 11 +++ .../integration/reproduce_state.py | 78 +++++++++++++++++++ .../tests/test_reproduce_state.py | 56 +++++++++++++ setup.cfg | 4 +- 5 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 script/scaffold/templates/reproduce_state/integration/reproduce_state.py create mode 100644 script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 93bcc5aba4172c..22cdee8f69e0e5 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -4,7 +4,7 @@ import subprocess import sys -from . import gather_info, generate, error +from . import gather_info, generate, error, docs from .const import COMPONENT_DIR @@ -65,9 +65,11 @@ def main(): print() print("Running tests") - print(f"$ pytest tests/components/{info.domain}") + print(f"$ pytest -v tests/components/{info.domain}") if ( - subprocess.run(f"pytest tests/components/{info.domain}", shell=True).returncode + subprocess.run( + f"pytest -v tests/components/{info.domain}", shell=True + ).returncode != 0 ): return 1 @@ -75,6 +77,8 @@ def main(): print(f"Done!") + docs.print_relevant_docs(args.template, info) + return 0 diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 54a182be31bd63..801b8ebb5fd8b0 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -18,5 +18,16 @@ def print_relevant_docs(template: str, info: Info) -> None: print( f""" The config flow has been added to the {info.domain} integration. Next step is to fill in the blanks for the code marked with TODO. +""" + ) + + elif template == "reproduce_state": + print( + f""" +Reproduce state code has been added to the {info.domain} integration: + - {info.integration_dir / "reproduce_state.py"} + - {info.tests_dir / "test_reproduce_state.py"} + +Please update the relevant items marked as TODO before submitting a pull request. """ ) diff --git a/script/scaffold/templates/reproduce_state/integration/reproduce_state.py b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py new file mode 100644 index 00000000000000..3449009818b6ab --- /dev/null +++ b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py @@ -0,0 +1,78 @@ +"""Reproduce an NEW_NAME state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +# TODO add valid states here +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if ( + cur_state.state == state.state + and + # TODO this is an example attribute + cur_state.attributes.get("color") == state.attributes.get("color") + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + # TODO determine the services to call to achieve desired state + if state.state == STATE_ON: + service = SERVICE_TURN_ON + if "color" in state.attributes: + service_data["color"] = state.attributes["color"] + + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce NEW_NAME states.""" + # TODO pick one and remove other one + + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + # Alternative: Reproduce states in sequence + # for state in states: + # await _async_reproduce_state(hass, state, context) diff --git a/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py new file mode 100644 index 00000000000000..ff15625ad7c4fb --- /dev/null +++ b/script/scaffold/templates/reproduce_state/tests/test_reproduce_state.py @@ -0,0 +1,56 @@ +"""Test reproduce state for NEW_NAME.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing NEW_NAME states.""" + hass.states.async_set("NEW_DOMAIN.entity_off", "off", {}) + hass.states.async_set("NEW_DOMAIN.entity_on", "on", {"color": "red"}) + + turn_on_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_on") + turn_off_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("NEW_DOMAIN.entity_off", "off"), + State("NEW_DOMAIN.entity_on", "on", {"color": "red"}), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("NEW_DOMAIN.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("NEW_DOMAIN.entity_on", "off"), + State("NEW_DOMAIN.entity_off", "on", {"color": "red"}), + # Should not raise + State("NEW_DOMAIN.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "NEW_DOMAIN" + assert turn_on_calls[0].data == { + "entity_id": "NEW_DOMAIN.entity_off", + "color": "red", + } + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "NEW_DOMAIN" + assert turn_off_calls[0].data == {"entity_id": "NEW_DOMAIN.entity_on"} diff --git a/setup.cfg b/setup.cfg index 49f738cf969eb7..4c9c892b93fbb4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,11 +27,13 @@ max-line-length = 88 # W503: Line break occurred before a binary operator # E203: Whitespace before ':' # D202 No blank lines allowed after function docstring +# W504 line break after binary operator ignore = E501, W503, E203, - D202 + D202, + W504 [isort] # https://github.com/timothycrosley/isort From 1d60cccc2183cec1b36bb81bdea576006c1c7dba Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 24 Sep 2019 11:09:16 +0100 Subject: [PATCH 0435/3953] Put draw_box in image_processing (#26712) * Put draw_box in image_processing * Update draw_box * Update __init__.py * run isort * Fix lints * Update __init__.py * Update requirements_all.txt * Adds type hints * Update gen_requirements_all.py * Update requirements_test_all.txt * rerun script/gen_requirements_all.py * Change Pillow to pillow * Update manifest.json * Run script/gen_requirements_all.py --- .../components/doods/image_processing.py | 19 +-------- .../components/image_processing/__init__.py | 40 ++++++++++++++++++- .../components/image_processing/manifest.json | 4 +- .../components/tensorflow/image_processing.py | 19 +-------- .../components/tensorflow/manifest.json | 1 - requirements_all.txt | 2 +- requirements_test_all.txt | 5 +++ script/gen_requirements_all.py | 1 + 8 files changed, 51 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 850eae76040f2a..3eec85b3e5304f 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -14,6 +14,7 @@ CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, + draw_box, ) from homeassistant.core import split_entity_id from homeassistant.helpers import template @@ -68,24 +69,6 @@ ) -def draw_box(draw, box, img_width, img_height, text="", color=(255, 255, 0)): - """Draw bounding box on image.""" - ymin, xmin, ymax, xmax = box - (left, right, top, bottom) = ( - xmin * img_width, - xmax * img_width, - ymin * img_height, - ymax * img_height, - ) - draw.line( - [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], - width=5, - fill=color, - ) - if text: - draw.text((left, abs(top - 15)), text, fill=color) - - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Doods client.""" url = config[CONF_URL] diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index b1c167a4175072..e9621fe6bbe719 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -2,7 +2,9 @@ import asyncio from datetime import timedelta import logging +from typing import Tuple +from PIL import ImageDraw import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME @@ -14,7 +16,6 @@ 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__) @@ -64,6 +65,43 @@ PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) +def draw_box( + draw: ImageDraw, + box: Tuple[float, float, float, float], + img_width: int, + img_height: int, + text: str = "", + color: Tuple[int, int, int] = (255, 255, 0), +) -> None: + """ + Draw a bounding box on and image. + + The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) + where the coordinates are floats in the range [0.0, 1.0] and + relative to the width and height of the image. + + For example, if an image is 100 x 200 pixels (height x width) and the bounding + box is `(0.1, 0.2, 0.5, 0.9)`, the upper-left and bottom-right coordinates of + the bounding box will be `(40, 10)` to `(180, 50)` (in (x,y) coordinates). + """ + + line_width = 5 + y_min, x_min, y_max, x_max = box + (left, right, top, bottom) = ( + x_min * img_width, + x_max * img_width, + y_min * img_height, + y_max * img_height, + ) + draw.line( + [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], + width=line_width, + fill=color, + ) + if text: + draw.text((left + line_width, abs(top - line_width)), text, fill=color) + + async def async_setup(hass, config): """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index e675d18a00b7d6..f3a7121c0b4ff3 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -2,7 +2,9 @@ "domain": "image_processing", "name": "Image processing", "documentation": "https://www.home-assistant.io/components/image_processing", - "requirements": [], + "requirements": [ + "pillow==6.1.0" + ], "dependencies": [ "camera" ], diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index b977a087eaeb54..65e20f558a787d 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -12,6 +12,7 @@ CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, + draw_box, ) from homeassistant.core import split_entity_id from homeassistant.helpers import template @@ -67,24 +68,6 @@ ) -def draw_box(draw, box, img_width, img_height, text="", color=(255, 255, 0)): - """Draw bounding box on image.""" - ymin, xmin, ymax, xmax = box - (left, right, top, bottom) = ( - xmin * img_width, - xmax * img_width, - ymin * img_height, - ymax * img_height, - ) - draw.line( - [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], - width=5, - fill=color, - ) - if text: - draw.text((left, abs(top - 15)), text, fill=color) - - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the TensorFlow image processing platform.""" model_config = config.get(CONF_MODEL) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 9419cbaaefbede..279ac3b103cba8 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -5,7 +5,6 @@ "requirements": [ "tensorflow==1.13.2", "numpy==1.17.1", - "pillow==6.1.0", "protobuf==3.6.1" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 04b6eaeb3e9db3..037fb1fcd2afdb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -950,9 +950,9 @@ piglow==1.2.4 # homeassistant.components.pilight pilight==0.1.1 +# homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -# homeassistant.components.tensorflow pillow==6.1.0 # homeassistant.components.dominos diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b962ca1280b5d8..63ad27a654caa7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -252,6 +252,11 @@ pexpect==4.6.0 # homeassistant.components.pilight pilight==0.1.1 +# homeassistant.components.image_processing +# homeassistant.components.proxy +# homeassistant.components.qrcode +pillow==6.1.0 + # homeassistant.components.plex plexapi==3.0.6 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d74a57d678d494..649c48e1b7d282 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -111,6 +111,7 @@ "paho-mqtt", "pexpect", "pilight", + "pillow", "plexapi", "pmsensor", "prometheus_client", From 930dadb7226c111658d691c13f2a46d9019d9ea7 Mon Sep 17 00:00:00 2001 From: majuss Date: Tue, 24 Sep 2019 13:10:03 +0200 Subject: [PATCH 0436/3953] Move elv integration to component and bump pypca (#26552) * fixing elv integration * black formatting * linting * rebase * removed logger warning for failed conf * rebase; coverage --- .coveragerc | 2 +- homeassistant/components/elv/__init__.py | 36 ++++++++++++++++++++++ homeassistant/components/elv/manifest.json | 4 +-- homeassistant/components/elv/switch.py | 36 ++++++++-------------- requirements_all.txt | 2 +- 5 files changed, 53 insertions(+), 27 deletions(-) diff --git a/.coveragerc b/.coveragerc index 88fdbd45f9a017..a4d6d0d201e980 100644 --- a/.coveragerc +++ b/.coveragerc @@ -165,7 +165,7 @@ omit = homeassistant/components/eight_sleep/* homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/* - homeassistant/components/elv/switch.py + homeassistant/components/elv/* homeassistant/components/emby/media_player.py homeassistant/components/emoncms/sensor.py homeassistant/components/emoncms_history/* diff --git a/homeassistant/components/elv/__init__.py b/homeassistant/components/elv/__init__.py index 13ade253ff659a..b6097737414e91 100644 --- a/homeassistant/components/elv/__init__.py +++ b/homeassistant/components/elv/__init__.py @@ -1 +1,37 @@ """The Elv integration.""" + +import logging + +import voluptuous as vol + +from homeassistant.helpers import discovery +from homeassistant.const import CONF_DEVICE +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "elv" + +DEFAULT_DEVICE = "/dev/ttyUSB0" + +ELV_PLATFORMS = ["switch"] + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string} + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the PCA switch platform.""" + + for platform in ELV_PLATFORMS: + discovery.load_platform( + hass, platform, DOMAIN, {"device": config[DOMAIN][CONF_DEVICE]}, config + ) + + return True diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index 4c9ed56352ecd8..04d384416217a8 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/components/pca", "dependencies": [], "codeowners": ["@majuss"], - "requirements": ["pypca==0.0.4"] - } \ No newline at end of file + "requirements": ["pypca==0.0.5"] + } diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index c6258e244e9c3c..362424c7faca48 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -1,15 +1,11 @@ """Support for PCA 301 smart switch.""" import logging -import voluptuous as vol +import pypca +from serial import SerialException -from homeassistant.components.switch import ( - SwitchDevice, - PLATFORM_SCHEMA, - ATTR_CURRENT_POWER_W, -) -from homeassistant.const import CONF_NAME, CONF_DEVICE, EVENT_HOMEASSISTANT_STOP -import homeassistant.helpers.config_validation as cv +from homeassistant.components.switch import SwitchDevice, ATTR_CURRENT_POWER_W +from homeassistant.const import EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -17,26 +13,20 @@ DEFAULT_NAME = "PCA 301" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_DEVICE): cv.string, - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PCA switch platform.""" - import pypca - from serial import SerialException - name = config[CONF_NAME] - usb_device = config[CONF_DEVICE] + if discovery_info is None: + return + + serial_device = discovery_info["device"] try: - pca = pypca.PCA(usb_device) + pca = pypca.PCA(serial_device) pca.open() - entities = [SmartPlugSwitch(pca, device, name) for device in pca.get_devices()] + + entities = [SmartPlugSwitch(pca, device) for device in pca.get_devices()] add_entities(entities, True) except SerialException as exc: @@ -51,10 +41,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SmartPlugSwitch(SwitchDevice): """Representation of a PCA Smart Plug switch.""" - def __init__(self, pca, device_id, name): + def __init__(self, pca, device_id): """Initialize the switch.""" self._device_id = device_id - self._name = name + self._name = "PCA 301" self._state = None self._available = True self._emeter_params = {} diff --git a/requirements_all.txt b/requirements_all.txt index 037fb1fcd2afdb..b8d4a158655af8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1375,7 +1375,7 @@ pyowlet==1.0.2 pyowm==2.10.0 # homeassistant.components.elv -pypca==0.0.4 +pypca==0.0.5 # homeassistant.components.lcn pypck==0.6.3 From 161c8aada6fa5b7b6341a583c0cc8a7ea982f563 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Wed, 25 Sep 2019 00:05:19 +1000 Subject: [PATCH 0437/3953] Add availability_template to Template Sensor platform (#26516) * Added availability_template to Template Sensor platform * Added to test for invalid values in availability_template * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Moved availability_template rendering to common loop * Removed 'Magic' string * Cleaned up const and compare lowercase result to 'true' * Device is unavailbale if value template render fails * Converted test ot Async and fixed states * Comverted back to using boolean for _availability * Fixed state check in test --- homeassistant/components/template/const.py | 3 + homeassistant/components/template/sensor.py | 38 ++++++++---- tests/components/template/test_sensor.py | 67 ++++++++++++++++++++- 3 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/template/const.py diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py new file mode 100644 index 00000000000000..e6cf69341f9de8 --- /dev/null +++ b/homeassistant/components/template/const.py @@ -0,0 +1,3 @@ +"""Constants for the Template Platform Components.""" + +CONF_AVAILABILITY_TEMPLATE = "availability_template" diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index b77528e0c324a7..a876819373652f 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -24,10 +24,12 @@ MATCH_ALL, CONF_DEVICE_CLASS, ) + from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.event import async_track_state_change +from .const import CONF_AVAILABILITY_TEMPLATE CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" @@ -39,6 +41,7 @@ vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( {cv.string: cv.template} ), @@ -62,6 +65,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config[CONF_VALUE_TEMPLATE] icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) @@ -77,6 +81,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_ICON_TEMPLATE: icon_template, CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, CONF_FRIENDLY_NAME_TEMPLATE: friendly_name_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, } for tpl_name, template in chain(templates.items(), attribute_templates.items()): @@ -120,15 +125,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, entity_ids, device_class, attribute_templates, ) ) - if not sensors: - _LOGGER.error("No sensors added") - return False - async_add_entities(sensors) return True @@ -146,6 +148,7 @@ def __init__( state_template, icon_template, entity_picture_template, + availability_template, entity_ids, device_class, attribute_templates, @@ -162,10 +165,12 @@ def __init__( self._state = None self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._icon = None self._entity_picture = None self._entities = entity_ids self._device_class = device_class + self._available = True self._attribute_templates = attribute_templates self._attributes = {} @@ -222,6 +227,11 @@ def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" return self._unit_of_measurement + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + @property def device_state_attributes(self): """Return the state attributes.""" @@ -236,7 +246,9 @@ async def async_update(self): """Update the state from the template.""" try: self._state = self._template.async_render() + self._available = True except TemplateError as ex: + self._available = False if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute" ): @@ -248,12 +260,6 @@ async def async_update(self): self._state = None _LOGGER.error("Could not render template %s: %s", self._name, ex) - templates = { - "_icon": self._icon_template, - "_entity_picture": self._entity_picture_template, - "_name": self._friendly_name_template, - } - attrs = {} for key, value in self._attribute_templates.items(): try: @@ -263,12 +269,22 @@ async def async_update(self): self._attributes = attrs + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + "_name": self._friendly_name_template, + "_available": self._availability_template, + } + for property_name, template in templates.items(): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 9223399bee7af1..b3813da176633c 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -3,6 +3,7 @@ from homeassistant.setup import setup_component, async_setup_component from tests.common import get_test_home_assistant, assert_setup_component +from homeassistant.const import STATE_UNAVAILABLE, STATE_ON, STATE_OFF class TestTemplateSensor: @@ -251,7 +252,7 @@ def test_template_attribute_missing(self): self.hass.block_till_done() state = self.hass.states.get("sensor.test_template_sensor") - assert state.state == "unknown" + assert state.state == STATE_UNAVAILABLE def test_invalid_name_does_not_create(self): """Test invalid name.""" @@ -377,6 +378,44 @@ def test_setup_valid_device_class(self): assert "device_class" not in state.attributes +async def test_available_template_with_entities(hass): + """Test availability tempalates with values from other entities.""" + hass.states.async_set("sensor.availability_sensor", STATE_OFF) + with assert_setup_component(1, "sensor"): + assert await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.test_sensor.state }}", + "availability_template": "{{ is_state('sensor.availability_sensor', 'on') }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set("sensor.availability_sensor", STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("sensor.test_template_sensor").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set("sensor.availability_sensor", STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("sensor.test_template_sensor").state == STATE_UNAVAILABLE + + async def test_invalid_attribute_template(hass, caplog): """Test that errors are logged if rendering template fails.""" hass.states.async_set("sensor.test_sensor", "startup") @@ -405,6 +444,32 @@ async def test_invalid_attribute_template(hass, caplog): assert ("Error rendering attribute test_attribute") in caplog.text +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "my_sensor": { + "value_template": "{{ states.sensor.test_state.state }}", + "availability_template": "{{ x - 12 }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("sensor.my_sensor").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + async def test_no_template_match_all(hass, caplog): """Test that we do not allow sensors that match on all.""" hass.states.async_set("sensor.test_sensor", "startup") From 18873d202d58fa41ec57ffa01860b7670d6e224e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 24 Sep 2019 11:54:41 -0400 Subject: [PATCH 0438/3953] Add device automation support to ZHA (#26821) * beginning ZHA device automations * add cube * load triggers from zigpy devices * cleanup * add face_any for aqara cube * add endpoint id to events * add cluster id to events * cleanup * add device tilt * add test * fix copy paste error * add event trigger test * add test for no triggers for device * add exception test * better exception tests --- .../components/zha/.translations/en.json | 75 ++++- .../components/zha/core/channels/__init__.py | 2 + homeassistant/components/zha/core/device.py | 7 + .../components/zha/device_automation.py | 89 +++++ homeassistant/components/zha/strings.json | 75 ++++- .../components/zha/test_device_automation.py | 308 ++++++++++++++++++ 6 files changed, 524 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/zha/device_automation.py create mode 100644 tests/components/zha/test_device_automation.py diff --git a/homeassistant/components/zha/.translations/en.json b/homeassistant/components/zha/.translations/en.json index f0da251f5eb643..6a819fbc16f8e2 100644 --- a/homeassistant/components/zha/.translations/en.json +++ b/homeassistant/components/zha/.translations/en.json @@ -1,20 +1,63 @@ { - "config": { - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." - }, - "step": { - "user": { - "data": { - "radio_type": "Radio Type", - "usb_path": "USB Device Path" - }, - "title": "ZHA" - } + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "step": { + "user": { + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" }, "title": "ZHA" + } + }, + "title": "ZHA" + }, + "device_automation": { + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "device_knocked": "Device knocked \"{subtype}\"", + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"" + }, + "trigger_subtype": { + "turn_on": "Turn on", + "turn_off": "Turn off", + "dim_up": "Dim up", + "dim_down": "Dim down", + "left": "Left", + "right": "Right", + "open": "Open", + "close": "Close", + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "face_any": "With any/specified face(s) activated", + "face_1": "With face 1 activated", + "face_2": "With face 2 activated", + "face_3": "With face 3 activated", + "face_4": "With face 4 activated", + "face_5": "With face 5 activated", + "face_6": "With face 6 activated" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index aed12bc65a5495..3d4a03fb0acac0 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -240,6 +240,8 @@ def zha_send_event(self, cluster, command, args): { "unique_id": self._unique_id, "device_ieee": str(self._zha_device.ieee), + "endpoint_id": cluster.endpoint.endpoint_id, + "cluster_id": cluster.cluster_id, "command": command, "args": args, }, diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1db4aafeeb9a80..82d20ff78c207e 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -187,6 +187,13 @@ def all_channels(self): """Return cluster channels and relay channels for device.""" return self._all_channels + @property + def device_automation_triggers(self): + """Return the device automation triggers for this device.""" + if hasattr(self._zigpy_device, "device_automation_triggers"): + return self._zigpy_device.device_automation_triggers + return None + @property def available_signal(self): """Signal to use to subscribe to device availability changes.""" diff --git a/homeassistant/components/zha/device_automation.py b/homeassistant/components/zha/device_automation.py new file mode 100644 index 00000000000000..6a96ce5aa3e405 --- /dev/null +++ b/homeassistant/components/zha/device_automation.py @@ -0,0 +1,89 @@ +"""Provides device automations for ZHA devices that emit events.""" +import voluptuous as vol + +import homeassistant.components.automation.event as event +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE + +from . import DOMAIN +from .core.const import DATA_ZHA, DATA_ZHA_GATEWAY +from .core.helpers import convert_ieee + +CONF_SUBTYPE = "subtype" +DEVICE = "device" +DEVICE_IEEE = "device_ieee" +ZHA_EVENT = "zha_event" + +TRIGGER_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_PLATFORM): DEVICE, + vol.Required(CONF_TYPE): str, + vol.Required(CONF_SUBTYPE): str, + } + ) +) + + +async def async_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + zha_device = await _async_get_zha_device(hass, config[CONF_DEVICE_ID]) + + if ( + zha_device.device_automation_triggers is None + or trigger not in zha_device.device_automation_triggers + ): + raise InvalidDeviceAutomationConfig + + trigger = zha_device.device_automation_triggers[trigger] + + state_config = { + event.CONF_EVENT_TYPE: ZHA_EVENT, + event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, + } + + return await event.async_trigger(hass, state_config, action, automation_info) + + +async def async_get_triggers(hass, device_id): + """List device triggers. + + Make sure the device supports device automations and + if it does return the trigger list. + """ + zha_device = await _async_get_zha_device(hass, device_id) + + if not zha_device.device_automation_triggers: + return + + triggers = [] + for trigger, subtype in zha_device.device_automation_triggers.keys(): + triggers.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: DEVICE, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers + + +async def _async_get_zha_device(hass, device_id): + device_registry = await hass.helpers.device_registry.async_get_registry() + registry_device = device_registry.async_get(device_id) + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee = convert_ieee(ieee_address) + zha_device = zha_gateway.devices[ieee] + if not zha_device: + raise InvalidDeviceAutomationConfig + return zha_device diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index e1ed6a678e3e95..cfc32a020c6dd5 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -1,20 +1,63 @@ { - "config": { + "config": { + "title": "ZHA", + "step": { + "user": { "title": "ZHA", - "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." + "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." } -} \ No newline at end of file + }, + "device_automation": { + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "device_knocked": "Device knocked \"{subtype}\"", + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"" + }, + "trigger_subtype": { + "turn_on": "Turn on", + "turn_off": "Turn off", + "dim_up": "Dim up", + "dim_down": "Dim down", + "left": "Left", + "right": "Right", + "open": "Open", + "close": "Close", + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "face_any": "With any/specified face(s) activated", + "face_1": "with face 1 activated", + "face_2": "with face 2 activated", + "face_3": "with face 3 activated", + "face_4": "with face 4 activated", + "face_5": "with face 5 activated", + "face_6": "with face 6 activated" + } + } +} diff --git a/tests/components/zha/test_device_automation.py b/tests/components/zha/test_device_automation.py new file mode 100644 index 00000000000000..9de04ae8e662e8 --- /dev/null +++ b/tests/components/zha/test_device_automation.py @@ -0,0 +1,308 @@ +"""ZHA device automation tests.""" +from unittest.mock import patch + +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.components.switch import DOMAIN +from homeassistant.components.zha.core.const import CHANNEL_ON_OFF +from homeassistant.helpers.device_registry import async_get_registry +from homeassistant.setup import async_setup_component + +from .common import async_enable_traffic, async_init_zigpy_device + +from tests.common import async_mock_service + +ON = 1 +OFF = 0 +SHAKEN = "device_shaken" +COMMAND = "command" +COMMAND_SHAKE = "shake" +COMMAND_HOLD = "hold" +COMMAND_SINGLE = "single" +COMMAND_DOUBLE = "double" +DOUBLE_PRESS = "remote_button_double_press" +SHORT_PRESS = "remote_button_short_press" +LONG_PRESS = "remote_button_long_press" +LONG_RELEASE = "remote_button_long_release" + + +def _same_lists(list_a, list_b): + if len(list_a) != len(list_b): + return False + + for item in list_a: + if item not in list_b: + return False + return True + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_triggers(hass, config_entry, zha_gateway): + """Test zha device triggers.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + triggers = await async_get_device_automations( + hass, "async_get_triggers", reg_device.id + ) + + expected_triggers = [ + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHAKEN, + "subtype": SHAKEN, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": DOUBLE_PRESS, + "subtype": DOUBLE_PRESS, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHORT_PRESS, + "subtype": SHORT_PRESS, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": LONG_PRESS, + "subtype": LONG_PRESS, + }, + { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": LONG_RELEASE, + "subtype": LONG_RELEASE, + }, + ] + assert _same_lists(triggers, expected_triggers) + + +async def test_no_triggers(hass, config_entry, zha_gateway): + """Test zha device with no triggers.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + triggers = await async_get_device_automations( + hass, "async_get_triggers", reg_device.id + ) + assert triggers == [] + + +async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): + """Test for remote triggers firing.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + ieee_address = str(zha_device.ieee) + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHORT_PRESS, + "subtype": SHORT_PRESS, + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + + await hass.async_block_till_done() + + on_off_channel = zha_device.cluster_channels[CHANNEL_ON_OFF] + on_off_channel.zha_send_event(on_off_channel.cluster, COMMAND_SINGLE, []) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["message"] == "service called" + + +async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls): + """Test for exception on event triggers firing.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + ieee_address = str(zha_device.ieee) + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + with patch("logging.Logger.error") as mock: + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + mock.assert_called_with("Error setting up trigger %s", "automation 0") + + +async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls): + """Test for exception on event triggers firing.""" + from zigpy.zcl.clusters.general import OnOff, Basic + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + ) + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + ieee_address = str(zha_device.ieee) + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + + with patch("logging.Logger.error") as mock: + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + mock.assert_called_with("Error setting up trigger %s", "automation 0") From b1118cb8ffa4c21a955ae569697a3f81eae94e8a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Sep 2019 22:53:03 +0200 Subject: [PATCH 0439/3953] Removes unnecessary else/elif blocks (#26884) --- .../eddystone_temperature/sensor.py | 4 +-- homeassistant/components/filesize/sensor.py | 3 +- homeassistant/components/isy994/__init__.py | 6 ++-- homeassistant/components/mvglive/sensor.py | 12 ++++--- .../components/onkyo/media_player.py | 6 ++-- homeassistant/components/recorder/__init__.py | 4 +-- homeassistant/components/todoist/calendar.py | 35 +++++++++++-------- .../components/webostv/media_player.py | 6 ++-- .../components/websocket_api/http.py | 2 +- .../components/zha/core/discovery.py | 4 ++- homeassistant/components/zwave/__init__.py | 7 ++-- homeassistant/helpers/__init__.py | 3 +- homeassistant/scripts/__init__.py | 3 +- 13 files changed, 53 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 5492582ebed27d..67724e9fcf344e 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -60,8 +60,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if instance is None or namespace is None: _LOGGER.error("Skipping %s", dev_name) continue - else: - devices.append(EddystoneTemp(name, namespace, instance)) + + devices.append(EddystoneTemp(name, namespace, instance)) if devices: mon = Monitor(hass, devices, bt_device_id) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index a4b9bc5cd76c8f..af9375aad053d4 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -27,8 +27,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if not hass.config.is_allowed_path(path): _LOGGER.error("Filepath %s is not valid or allowed", path) continue - else: - sensors.append(Filesize(path)) + sensors.append(Filesize(path)) if sensors: add_entities(sensors, True) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 727ec91dc37cd3..324dcb019b3915 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -323,9 +323,9 @@ def _categorize_nodes( # determine if it should be a binary_sensor. if _is_sensor_a_binary_sensor(hass, node): continue - else: - hass.data[ISY994_NODES]["sensor"].append(node) - continue + + hass.data[ISY994_NODES]["sensor"].append(node) + continue # We have a bunch of different methods for determining the device type, # each of which works with different ISY firmware versions or device diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 6da26784d9c5d0..3c753d832e097e 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -189,17 +189,19 @@ def update(self): and _departure["destination"] not in self._destinations ): continue - elif ( + + if ( "" not in self._directions[:1] and _departure["direction"] not in self._directions ): continue - elif ( - "" not in self._lines[:1] and _departure["linename"] not in self._lines - ): + + if "" not in self._lines[:1] and _departure["linename"] not in self._lines: continue - elif _departure["time"] < self._timeoffset: + + if _departure["time"] < self._timeoffset: continue + # now select the relevant data _nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION} for k in ["destination", "linename", "time", "direction", "product"]: diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 023fb32e6e40da..9ec8c56d770a00 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -264,8 +264,7 @@ def update(self): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - else: - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join([i for i in current_source_tuples[1]]) if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] elif ATTR_PRESET in self._attributes: @@ -414,8 +413,7 @@ def update(self): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - else: - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join([i for i in current_source_tuples[1]]) self._muted = bool(mute_raw[1] == "on") if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 9d34cc6fb79f27..b36e0a34fa482a 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -320,10 +320,10 @@ def async_purge(now): purge.purge_old_data(self, event.keep_days, event.repack) self.queue.task_done() continue - elif event.event_type == EVENT_TIME_CHANGED: + if event.event_type == EVENT_TIME_CHANGED: self.queue.task_done() continue - elif event.event_type in self.exclude_t: + if event.event_type in self.exclude_t: self.queue.task_done() continue diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 7d2f51f29af34c..75aec037a25ed5 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -484,39 +484,44 @@ def select_best_task(project_tasks): for proposed_event in project_tasks: if event == proposed_event: continue + if proposed_event[COMPLETED]: # Event is complete! continue + if proposed_event[END] is None: # No end time: if event[END] is None and (proposed_event[PRIORITY] < event[PRIORITY]): # They also have no end time, # but we have a higher priority. event = proposed_event - continue - else: - continue - elif event[END] is None: + continue + + if event[END] is None: # We have an end time, they do not. event = proposed_event continue + if proposed_event[END].date() > event[END].date(): # Event is too late. continue - elif proposed_event[END].date() < event[END].date(): + + if proposed_event[END].date() < event[END].date(): # Event is earlier than current, select it. event = proposed_event continue - else: - if proposed_event[PRIORITY] > event[PRIORITY]: - # Proposed event has a higher priority. - event = proposed_event - continue - elif proposed_event[PRIORITY] == event[PRIORITY] and ( - proposed_event[END] < event[END] - ): - event = proposed_event - continue + + if proposed_event[PRIORITY] > event[PRIORITY]: + # Proposed event has a higher priority. + event = proposed_event + continue + + if proposed_event[PRIORITY] == event[PRIORITY] and ( + proposed_event[END] < event[END] + ): + event = proposed_event + continue + return event async def async_get_events(self, hass, start_date, end_date): diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 1da70bc60ec255..913d193845fb01 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -396,10 +396,12 @@ def play_media(self, media_type, media_id, **kwargs): if media_id == channel["channelNumber"]: perfect_match_channel_id = channel["channelId"] continue - elif media_id.lower() == channel["channelName"].lower(): + + if media_id.lower() == channel["channelName"].lower(): perfect_match_channel_id = channel["channelId"] continue - elif media_id.lower() in channel["channelName"].lower(): + + if media_id.lower() in channel["channelName"].lower(): partial_match_channel_id = channel["channelId"] if perfect_match_channel_id is not None: diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 108fcbc7740d6a..9a1f375fdfda20 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -165,7 +165,7 @@ def handle_hass_stop(event): if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING): break - elif msg.type != WSMsgType.TEXT: + if msg.type != WSMsgType.TEXT: disconnect_warn = "Received non-Text message." break diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 5a5ffb34ab13ea..80642a373da7e4 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -199,11 +199,13 @@ def _async_handle_single_cluster_matches( zha_device.is_mains_powered or matched_power_configuration ): continue - elif ( + + if ( cluster.cluster_id == PowerConfiguration.cluster_id and not zha_device.is_mains_powered ): matched_power_configuration = True + cluster_match_results.append( _async_handle_single_cluster_match( hass, diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 223ce810d7cefe..841b283a98dd1f 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -851,7 +851,8 @@ async def _check_awaked(): # Need to be in STATE_AWAKED before talking to nodes. _LOGGER.info("Z-Wave ready after %d seconds", waited) break - elif waited >= const.NETWORK_READY_WAIT_SECS: + + if waited >= const.NETWORK_READY_WAIT_SECS: # Wait up to NETWORK_READY_WAIT_SECS seconds for the Z-Wave # network to be ready. _LOGGER.warning( @@ -861,8 +862,8 @@ async def _check_awaked(): "final network state: %d %s", network.state, network.state_str ) break - else: - await asyncio.sleep(1) + + await asyncio.sleep(1) hass.async_add_job(_finalize_start) diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index dbfb9ae1864f10..4c1a9803d754a4 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -19,7 +19,8 @@ def config_per_platform(config: ConfigType, domain: str) -> Iterable[Tuple[Any, if not platform_config: continue - elif not isinstance(platform_config, list): + + if not isinstance(platform_config, list): platform_config = [platform_config] for item in platform_config: diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index 00f5984c58ba43..ecac61895c53de 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -23,7 +23,8 @@ def run(args: List) -> int: for fil in os.listdir(path): if fil == "__pycache__": continue - elif os.path.isdir(os.path.join(path, fil)): + + if os.path.isdir(os.path.join(path, fil)): scripts.append(fil) elif fil != "__init__.py" and fil.endswith(".py"): scripts.append(fil[:-3]) From 6f9ccb54342fdd723c769724da0f1e9ea98894a1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Sep 2019 23:20:04 +0200 Subject: [PATCH 0440/3953] Add and corrects typehints in Entity helper & core class (#26805) * Add and corrects typehints in Entity class * Adjust state type based on comments --- homeassistant/core.py | 8 ++++---- homeassistant/helpers/entity.py | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index c29d41ace9a126..31761f2560faf4 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -703,7 +703,7 @@ class State: def __init__( self, entity_id: str, - state: Any, + state: str, attributes: Optional[Dict] = None, last_changed: Optional[datetime.datetime] = None, last_updated: Optional[datetime.datetime] = None, @@ -732,7 +732,7 @@ def __init__( ) self.entity_id = entity_id.lower() - self.state: str = state + self.state = state self.attributes = MappingProxyType(attributes or {}) self.last_updated = last_updated or dt_util.utcnow() self.last_changed = last_changed or self.last_updated @@ -924,7 +924,7 @@ def async_remove(self, entity_id: str) -> bool: def set( self, entity_id: str, - new_state: Any, + new_state: str, attributes: Optional[Dict] = None, force_update: bool = False, context: Optional[Context] = None, @@ -950,7 +950,7 @@ def set( def async_set( self, entity_id: str, - new_state: Any, + new_state: str, attributes: Optional[Dict] = None, force_update: bool = False, context: Optional[Context] = None, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index af8d5589c8a85a..4911c5d5fb9157 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -3,7 +3,7 @@ import logging import functools as ft from timeit import default_timer as timer -from typing import Any, Optional, List, Iterable +from typing import Any, Dict, Iterable, List, Optional, Union from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -26,7 +26,7 @@ EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, ) -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE +from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Context from homeassistant.config import DATA_CUSTOMIZE from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.util import ensure_unique_string, slugify @@ -137,12 +137,12 @@ def name(self) -> Optional[str]: return None @property - def state(self) -> str: + def state(self) -> Union[None, str, int, float]: """Return the state of the entity.""" return STATE_UNKNOWN @property - def state_attributes(self): + def state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes. Implemented by component base class. @@ -150,7 +150,7 @@ def state_attributes(self): return None @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return device specific state attributes. Implemented by platform classes. @@ -158,7 +158,7 @@ def device_state_attributes(self): return None @property - def device_info(self): + def device_info(self) -> Optional[Dict[str, Any]]: """Return device specific attributes. Implemented by platform classes. @@ -171,17 +171,17 @@ def device_class(self) -> Optional[str]: return None @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> Optional[str]: """Return the unit of measurement of this entity, if any.""" return None @property - def icon(self): + def icon(self) -> Optional[str]: """Return the icon to use in the frontend, if any.""" return None @property - def entity_picture(self): + def entity_picture(self) -> Optional[str]: """Return the entity picture to use in the frontend, if any.""" return None @@ -215,12 +215,12 @@ def supported_features(self) -> Optional[int]: return None @property - def context_recent_time(self): + def context_recent_time(self) -> timedelta: """Time that a context is considered recent.""" return timedelta(seconds=5) @property - def entity_registry_enabled_default(self): + def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return True @@ -230,12 +230,12 @@ def entity_registry_enabled_default(self): # produce undesirable effects in the entity's operation. @property - def enabled(self): + def enabled(self) -> bool: """Return if the entity is enabled in the entity registry.""" return self.registry_entry is None or not self.registry_entry.disabled @callback - def async_set_context(self, context): + def async_set_context(self, context: Context) -> None: """Set the context the entity currently operates under.""" self._context = context self._context_set = dt_util.utcnow() @@ -540,7 +540,7 @@ def __eq__(self, other): return self.unique_id == other.unique_id - def __repr__(self): + def __repr__(self) -> str: """Return the representation.""" return "".format(self.name, self.state) From b52cfd34098ef3c271a5a8a67047493f1860f3db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Sep 2019 23:21:00 +0200 Subject: [PATCH 0441/3953] Add comment for clarity to helper.entity.enabled() (#26793) * Fixes entity enabled expression * Ensure True is returned when there is no registry_entity * Add comment for clarity to helper.entity.enabled() --- homeassistant/helpers/entity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 4911c5d5fb9157..fad02dee075839 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -231,7 +231,11 @@ def entity_registry_enabled_default(self) -> bool: @property def enabled(self) -> bool: - """Return if the entity is enabled in the entity registry.""" + """Return if the entity is enabled in the entity registry. + + If an entity is not part of the registry, it cannot be disabled + and will therefore always be enabled. + """ return self.registry_entry is None or not self.registry_entry.disabled @callback From 6fdff9ffab9618fcec3b4d364a3faab86f768440 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 Sep 2019 14:57:05 -0700 Subject: [PATCH 0442/3953] Reorg device automation (#26880) * async_trigger -> async_attach_trigger * Reorg device automations * Update docstrings * Fix types * Fix extending schemas --- .../components/automation/__init__.py | 9 +- homeassistant/components/automation/device.py | 6 +- homeassistant/components/automation/event.py | 6 +- .../components/automation/geo_location.py | 2 +- .../components/automation/homeassistant.py | 2 +- .../components/automation/litejet.py | 2 +- homeassistant/components/automation/mqtt.py | 2 +- .../components/automation/numeric_state.py | 2 +- homeassistant/components/automation/state.py | 6 +- homeassistant/components/automation/sun.py | 2 +- .../components/automation/template.py | 2 +- homeassistant/components/automation/time.py | 2 +- .../components/automation/time_pattern.py | 2 +- .../components/automation/webhook.py | 2 +- homeassistant/components/automation/zone.py | 2 +- .../binary_sensor/device_automation.py | 423 ------------------ .../binary_sensor/device_condition.py | 247 ++++++++++ .../binary_sensor/device_trigger.py | 238 ++++++++++ ...device_automation.py => device_trigger.py} | 19 +- .../components/device_automation/__init__.py | 68 ++- .../device_automation/toggle_entity.py | 98 ++-- .../components/light/device_action.py | 30 ++ .../components/light/device_automation.py | 56 --- .../components/light/device_condition.py | 28 ++ .../components/light/device_trigger.py | 33 ++ .../components/switch/device_action.py | 30 ++ .../components/switch/device_automation.py | 56 --- .../components/switch/device_condition.py | 28 ++ .../components/switch/device_trigger.py | 33 ++ ...device_automation.py => device_trigger.py} | 19 +- homeassistant/helpers/condition.py | 46 +- homeassistant/helpers/config_validation.py | 18 +- homeassistant/helpers/script.py | 2 +- tests/common.py | 4 +- .../binary_sensor/test_device_automation.py | 309 ------------- .../binary_sensor/test_device_condition.py | 144 ++++++ .../binary_sensor/test_device_trigger.py | 154 +++++++ .../{test_binary_sensor.py => test_init.py} | 0 ...e_automation.py => test_device_trigger.py} | 44 +- tests/components/light/test_device_action.py | 140 ++++++ .../light/test_device_automation.py | 373 --------------- .../components/light/test_device_condition.py | 136 ++++++ tests/components/light/test_device_trigger.py | 147 ++++++ tests/components/switch/test_device_action.py | 142 ++++++ .../switch/test_device_automation.py | 373 --------------- .../switch/test_device_condition.py | 138 ++++++ .../components/switch/test_device_trigger.py | 147 ++++++ .../components/zha/test_device_automation.py | 13 +- 48 files changed, 2014 insertions(+), 1771 deletions(-) delete mode 100644 homeassistant/components/binary_sensor/device_automation.py create mode 100644 homeassistant/components/binary_sensor/device_condition.py create mode 100644 homeassistant/components/binary_sensor/device_trigger.py rename homeassistant/components/deconz/{device_automation.py => device_trigger.py} (94%) create mode 100644 homeassistant/components/light/device_action.py delete mode 100644 homeassistant/components/light/device_automation.py create mode 100644 homeassistant/components/light/device_condition.py create mode 100644 homeassistant/components/light/device_trigger.py create mode 100644 homeassistant/components/switch/device_action.py delete mode 100644 homeassistant/components/switch/device_automation.py create mode 100644 homeassistant/components/switch/device_condition.py create mode 100644 homeassistant/components/switch/device_trigger.py rename homeassistant/components/zha/{device_automation.py => device_trigger.py} (84%) delete mode 100644 tests/components/binary_sensor/test_device_automation.py create mode 100644 tests/components/binary_sensor/test_device_condition.py create mode 100644 tests/components/binary_sensor/test_device_trigger.py rename tests/components/binary_sensor/{test_binary_sensor.py => test_init.py} (100%) rename tests/components/deconz/{test_device_automation.py => test_device_trigger.py} (71%) create mode 100644 tests/components/light/test_device_action.py delete mode 100644 tests/components/light/test_device_automation.py create mode 100644 tests/components/light/test_device_condition.py create mode 100644 tests/components/light/test_device_trigger.py create mode 100644 tests/components/switch/test_device_action.py delete mode 100644 tests/components/switch/test_device_automation.py create mode 100644 tests/components/switch/test_device_condition.py create mode 100644 tests/components/switch/test_device_trigger.py diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f0529f126f1e73..f669d415854b9d 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -3,7 +3,7 @@ from functools import partial import importlib import logging -from typing import Any +from typing import Any, Awaitable, Callable import voluptuous as vol @@ -23,7 +23,7 @@ SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import Context, CoreState +from homeassistant.core import Context, CoreState, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, extract_domain_configs, script import homeassistant.helpers.config_validation as cv @@ -31,6 +31,7 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime, utcnow @@ -67,6 +68,8 @@ _LOGGER = logging.getLogger(__name__) +AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]] + def _platform_validator(config): """Validate it is a valid platform.""" @@ -474,7 +477,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) try: - remove = await platform.async_trigger(hass, conf, action, info) + remove = await platform.async_attach_trigger(hass, conf, action, info) except InvalidDeviceAutomationConfig: remove = False diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index b090484ab6785d..fe2d65edef616b 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -13,8 +13,8 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") - return await platform.async_trigger(hass, config, action, automation_info) + platform = integration.get_platform("device_trigger") + return await platform.async_attach_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index d372aedd1d74f3..26dacac974d559 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -24,7 +24,9 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="event" +): """Listen for events based on configuration.""" event_type = config.get(CONF_EVENT_TYPE) event_data_schema = ( @@ -47,7 +49,7 @@ def handle_event(event): hass.async_run_job( action( - {"trigger": {"platform": "event", "event": event}}, + {"trigger": {"platform": platform_type, "event": event}}, context=event.context, ) ) diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 3f2aa1c00d794a..0ef0884d329e64 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -37,7 +37,7 @@ def source_match(state, source): return state and state.attributes.get("source") == source -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" source = config.get(CONF_SOURCE).lower() zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index bd1da7e7e1f35a..e4eb029d5aa778 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -21,7 +21,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 7bc4c9377656c5..9512db8261dbbf 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -32,7 +32,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index fd9a778dbfc454..135a421f72e1fb 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -25,7 +25,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" topic = config[CONF_TOPIC] payload = config.get(CONF_PAYLOAD) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index b33d724d7700fe..9dd4657291dc21 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -40,7 +40,7 @@ _LOGGER = logging.getLogger(__name__) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 5fbe97185a7fa5..184b9ea302b11c 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -37,7 +37,9 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="state" +): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) @@ -59,7 +61,7 @@ def call_action(): action( { "trigger": { - "platform": "state", + "platform": platform_type, "entity_id": entity, "from_state": from_s, "to_state": to_s, diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 7cbbe56f326b97..66892784a54d1d 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -28,7 +28,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for events based on configuration.""" event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index c83d660912cf53..f2b4134de42282 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -28,7 +28,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = hass diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 3942d0efadbb9d..231bc346e141c0 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -18,7 +18,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" at_time = config.get(CONF_AT) hours, minutes, seconds = at_time.hour, at_time.minute, at_time.second diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py index f749a308bf7a31..ee092916112024 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -30,7 +30,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" hours = config.get(CONF_HOURS) minutes = config.get(CONF_MINUTES) diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index 706afbe90421f2..bbcf9bd9ddcad2 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -36,7 +36,7 @@ async def _handle_webhook(action, hass, webhook_id, request): hass.async_run_job(action, {"trigger": result}) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Trigger based on incoming webhooks.""" webhook_id = config.get(CONF_WEBHOOK_ID) hass.components.webhook.async_register( diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 35b110060244f7..535ef298a2a78a 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -31,7 +31,7 @@ ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) zone_entity_id = config.get(CONF_ZONE) diff --git a/homeassistant/components/binary_sensor/device_automation.py b/homeassistant/components/binary_sensor/device_automation.py deleted file mode 100644 index c609c2eb5da4c8..00000000000000 --- a/homeassistant/components/binary_sensor/device_automation.py +++ /dev/null @@ -1,423 +0,0 @@ -"""Provides device automations for lights.""" -import voluptuous as vol - -import homeassistant.components.automation.state as state -from homeassistant.components.device_automation.const import ( - CONF_IS_OFF, - CONF_IS_ON, - CONF_TURNED_OFF, - CONF_TURNED_ON, -) -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - CONF_CONDITION, - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_ENTITY_ID, - CONF_PLATFORM, - CONF_TYPE, -) -from homeassistant.core import split_entity_id -from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers import condition, config_validation as cv - -from . import ( - DOMAIN, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_COLD, - DEVICE_CLASS_CONNECTIVITY, - DEVICE_CLASS_DOOR, - DEVICE_CLASS_GARAGE_DOOR, - DEVICE_CLASS_GAS, - DEVICE_CLASS_HEAT, - DEVICE_CLASS_LIGHT, - DEVICE_CLASS_LOCK, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_MOVING, - DEVICE_CLASS_OCCUPANCY, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PLUG, - DEVICE_CLASS_POWER, - DEVICE_CLASS_PRESENCE, - DEVICE_CLASS_PROBLEM, - DEVICE_CLASS_SAFETY, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_SOUND, - DEVICE_CLASS_VIBRATION, - DEVICE_CLASS_WINDOW, -) - - -# mypy: allow-untyped-defs, no-check-untyped-defs - -DEVICE_CLASS_NONE = "none" - -CONF_IS_BAT_LOW = "is_bat_low" -CONF_IS_NOT_BAT_LOW = "is_not_bat_low" -CONF_IS_COLD = "is_cold" -CONF_IS_NOT_COLD = "is_not_cold" -CONF_IS_CONNECTED = "is_connected" -CONF_IS_NOT_CONNECTED = "is_not_connected" -CONF_IS_GAS = "is_gas" -CONF_IS_NO_GAS = "is_no_gas" -CONF_IS_HOT = "is_hot" -CONF_IS_NOT_HOT = "is_not_hot" -CONF_IS_LIGHT = "is_light" -CONF_IS_NO_LIGHT = "is_no_light" -CONF_IS_LOCKED = "is_locked" -CONF_IS_NOT_LOCKED = "is_not_locked" -CONF_IS_MOIST = "is_moist" -CONF_IS_NOT_MOIST = "is_not_moist" -CONF_IS_MOTION = "is_motion" -CONF_IS_NO_MOTION = "is_no_motion" -CONF_IS_MOVING = "is_moving" -CONF_IS_NOT_MOVING = "is_not_moving" -CONF_IS_OCCUPIED = "is_occupied" -CONF_IS_NOT_OCCUPIED = "is_not_occupied" -CONF_IS_PLUGGED_IN = "is_plugged_in" -CONF_IS_NOT_PLUGGED_IN = "is_not_plugged_in" -CONF_IS_POWERED = "is_powered" -CONF_IS_NOT_POWERED = "is_not_powered" -CONF_IS_PRESENT = "is_present" -CONF_IS_NOT_PRESENT = "is_not_present" -CONF_IS_PROBLEM = "is_problem" -CONF_IS_NO_PROBLEM = "is_no_problem" -CONF_IS_UNSAFE = "is_unsafe" -CONF_IS_NOT_UNSAFE = "is_not_unsafe" -CONF_IS_SMOKE = "is_smoke" -CONF_IS_NO_SMOKE = "is_no_smoke" -CONF_IS_SOUND = "is_sound" -CONF_IS_NO_SOUND = "is_no_sound" -CONF_IS_VIBRATION = "is_vibration" -CONF_IS_NO_VIBRATION = "is_no_vibration" -CONF_IS_OPEN = "is_open" -CONF_IS_NOT_OPEN = "is_not_open" - -CONF_BAT_LOW = "bat_low" -CONF_NOT_BAT_LOW = "not_bat_low" -CONF_COLD = "cold" -CONF_NOT_COLD = "not_cold" -CONF_CONNECTED = "connected" -CONF_NOT_CONNECTED = "not_connected" -CONF_GAS = "gas" -CONF_NO_GAS = "no_gas" -CONF_HOT = "hot" -CONF_NOT_HOT = "not_hot" -CONF_LIGHT = "light" -CONF_NO_LIGHT = "no_light" -CONF_LOCKED = "locked" -CONF_NOT_LOCKED = "not_locked" -CONF_MOIST = "moist" -CONF_NOT_MOIST = "not_moist" -CONF_MOTION = "motion" -CONF_NO_MOTION = "no_motion" -CONF_MOVING = "moving" -CONF_NOT_MOVING = "not_moving" -CONF_OCCUPIED = "occupied" -CONF_NOT_OCCUPIED = "not_occupied" -CONF_PLUGGED_IN = "plugged_in" -CONF_NOT_PLUGGED_IN = "not_plugged_in" -CONF_POWERED = "powered" -CONF_NOT_POWERED = "not_powered" -CONF_PRESENT = "present" -CONF_NOT_PRESENT = "not_present" -CONF_PROBLEM = "problem" -CONF_NO_PROBLEM = "no_problem" -CONF_UNSAFE = "unsafe" -CONF_NOT_UNSAFE = "not_unsafe" -CONF_SMOKE = "smoke" -CONF_NO_SMOKE = "no_smoke" -CONF_SOUND = "sound" -CONF_NO_SOUND = "no_sound" -CONF_VIBRATION = "vibration" -CONF_NO_VIBRATION = "no_vibration" -CONF_OPEN = "open" -CONF_NOT_OPEN = "not_open" - -IS_ON = [ - CONF_IS_BAT_LOW, - CONF_IS_COLD, - CONF_IS_CONNECTED, - CONF_IS_GAS, - CONF_IS_HOT, - CONF_IS_LIGHT, - CONF_IS_LOCKED, - CONF_IS_MOIST, - CONF_IS_MOTION, - CONF_IS_MOVING, - CONF_IS_OCCUPIED, - CONF_IS_OPEN, - CONF_IS_PLUGGED_IN, - CONF_IS_POWERED, - CONF_IS_PRESENT, - CONF_IS_PROBLEM, - CONF_IS_SMOKE, - CONF_IS_SOUND, - CONF_IS_UNSAFE, - CONF_IS_VIBRATION, - CONF_IS_ON, -] - -IS_OFF = [ - CONF_IS_NOT_BAT_LOW, - CONF_IS_NOT_COLD, - CONF_IS_NOT_CONNECTED, - CONF_IS_NOT_HOT, - CONF_IS_NOT_LOCKED, - CONF_IS_NOT_MOIST, - CONF_IS_NOT_MOVING, - CONF_IS_NOT_OCCUPIED, - CONF_IS_NOT_OPEN, - CONF_IS_NOT_PLUGGED_IN, - CONF_IS_NOT_POWERED, - CONF_IS_NOT_PRESENT, - CONF_IS_NOT_UNSAFE, - CONF_IS_NO_GAS, - CONF_IS_NO_LIGHT, - CONF_IS_NO_MOTION, - CONF_IS_NO_PROBLEM, - CONF_IS_NO_SMOKE, - CONF_IS_NO_SOUND, - CONF_IS_NO_VIBRATION, - CONF_IS_OFF, -] - -TURNED_ON = [ - CONF_BAT_LOW, - CONF_COLD, - CONF_CONNECTED, - CONF_GAS, - CONF_HOT, - CONF_LIGHT, - CONF_LOCKED, - CONF_MOIST, - CONF_MOTION, - CONF_MOVING, - CONF_OCCUPIED, - CONF_OPEN, - CONF_PLUGGED_IN, - CONF_POWERED, - CONF_PRESENT, - CONF_PROBLEM, - CONF_SMOKE, - CONF_SOUND, - CONF_UNSAFE, - CONF_VIBRATION, - CONF_TURNED_ON, -] - -TURNED_OFF = [ - CONF_NOT_BAT_LOW, - CONF_NOT_COLD, - CONF_NOT_CONNECTED, - CONF_NOT_HOT, - CONF_NOT_LOCKED, - CONF_NOT_MOIST, - CONF_NOT_MOVING, - CONF_NOT_OCCUPIED, - CONF_NOT_OPEN, - CONF_NOT_PLUGGED_IN, - CONF_NOT_POWERED, - CONF_NOT_PRESENT, - CONF_NOT_UNSAFE, - CONF_NO_GAS, - CONF_NO_LIGHT, - CONF_NO_MOTION, - CONF_NO_PROBLEM, - CONF_NO_SMOKE, - CONF_NO_SOUND, - CONF_NO_VIBRATION, - CONF_TURNED_OFF, -] - -ENTITY_CONDITIONS = { - DEVICE_CLASS_BATTERY: [ - {CONF_TYPE: CONF_IS_BAT_LOW}, - {CONF_TYPE: CONF_IS_NOT_BAT_LOW}, - ], - DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_IS_COLD}, {CONF_TYPE: CONF_IS_NOT_COLD}], - DEVICE_CLASS_CONNECTIVITY: [ - {CONF_TYPE: CONF_IS_CONNECTED}, - {CONF_TYPE: CONF_IS_NOT_CONNECTED}, - ], - DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], - DEVICE_CLASS_GARAGE_DOOR: [ - {CONF_TYPE: CONF_IS_OPEN}, - {CONF_TYPE: CONF_IS_NOT_OPEN}, - ], - DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}, {CONF_TYPE: CONF_IS_NO_GAS}], - DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_IS_HOT}, {CONF_TYPE: CONF_IS_NOT_HOT}], - DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_IS_LIGHT}, {CONF_TYPE: CONF_IS_NO_LIGHT}], - DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_IS_LOCKED}, {CONF_TYPE: CONF_IS_NOT_LOCKED}], - DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_IS_MOIST}, {CONF_TYPE: CONF_IS_NOT_MOIST}], - DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_IS_MOTION}, {CONF_TYPE: CONF_IS_NO_MOTION}], - DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_IS_MOVING}, {CONF_TYPE: CONF_IS_NOT_MOVING}], - DEVICE_CLASS_OCCUPANCY: [ - {CONF_TYPE: CONF_IS_OCCUPIED}, - {CONF_TYPE: CONF_IS_NOT_OCCUPIED}, - ], - DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], - DEVICE_CLASS_PLUG: [ - {CONF_TYPE: CONF_IS_PLUGGED_IN}, - {CONF_TYPE: CONF_IS_NOT_PLUGGED_IN}, - ], - DEVICE_CLASS_POWER: [ - {CONF_TYPE: CONF_IS_POWERED}, - {CONF_TYPE: CONF_IS_NOT_POWERED}, - ], - DEVICE_CLASS_PRESENCE: [ - {CONF_TYPE: CONF_IS_PRESENT}, - {CONF_TYPE: CONF_IS_NOT_PRESENT}, - ], - DEVICE_CLASS_PROBLEM: [ - {CONF_TYPE: CONF_IS_PROBLEM}, - {CONF_TYPE: CONF_IS_NO_PROBLEM}, - ], - DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], - DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], - DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], - DEVICE_CLASS_VIBRATION: [ - {CONF_TYPE: CONF_IS_VIBRATION}, - {CONF_TYPE: CONF_IS_NO_VIBRATION}, - ], - DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], - DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_ON}, {CONF_TYPE: CONF_IS_OFF}], -} - -ENTITY_TRIGGERS = { - DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BAT_LOW}, {CONF_TYPE: CONF_NOT_BAT_LOW}], - DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_COLD}, {CONF_TYPE: CONF_NOT_COLD}], - DEVICE_CLASS_CONNECTIVITY: [ - {CONF_TYPE: CONF_CONNECTED}, - {CONF_TYPE: CONF_NOT_CONNECTED}, - ], - DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], - DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], - DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], - DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}], - DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_MOIST}, {CONF_TYPE: CONF_NOT_MOIST}], - DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_MOTION}, {CONF_TYPE: CONF_NO_MOTION}], - DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_MOVING}, {CONF_TYPE: CONF_NOT_MOVING}], - DEVICE_CLASS_OCCUPANCY: [ - {CONF_TYPE: CONF_OCCUPIED}, - {CONF_TYPE: CONF_NOT_OCCUPIED}, - ], - DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], - DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], - DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], - DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], - DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], - DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], - DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], - DEVICE_CLASS_VIBRATION: [ - {CONF_TYPE: CONF_VIBRATION}, - {CONF_TYPE: CONF_NO_VIBRATION}, - ], - DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], -} - -CONDITION_SCHEMA = vol.Schema( - { - vol.Required(CONF_CONDITION): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), - } -) - -TRIGGER_SCHEMA = vol.Schema( - { - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), - } -) - - -def async_condition_from_config(config, config_validation): - """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) - condition_type = config[CONF_TYPE] - if condition_type in IS_ON: - stat = "on" - else: - stat = "off" - state_config = { - condition.CONF_CONDITION: "state", - condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - condition.CONF_STATE: stat, - } - - return condition.state_from_config(state_config, config_validation) - - -async def async_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - trigger_type = config[CONF_TYPE] - if trigger_type in TURNED_ON: - from_state = "off" - to_state = "on" - else: - from_state = "on" - to_state = "off" - state_config = { - state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, - state.CONF_TO: to_state, - } - - return await state.async_trigger(hass, state_config, action, automation_info) - - -def _is_domain(entity, domain): - return split_entity_id(entity.entity_id)[0] == domain - - -async def _async_get_automations(hass, device_id, automation_templates, domain): - """List device automations.""" - automations = [] - entity_registry = await hass.helpers.entity_registry.async_get_registry() - - entities = async_entries_for_device(entity_registry, device_id) - domain_entities = [x for x in entities if _is_domain(x, domain)] - for entity in domain_entities: - device_class = DEVICE_CLASS_NONE - entity_id = entity.entity_id - entity = hass.states.get(entity_id) - if entity and ATTR_DEVICE_CLASS in entity.attributes: - device_class = entity.attributes[ATTR_DEVICE_CLASS] - automation_template = automation_templates[device_class] - - for automation in automation_template: - automation = dict(automation) - automation.update(device_id=device_id, entity_id=entity_id, domain=domain) - automations.append(automation) - - return automations - - -async def async_get_conditions(hass, device_id): - """List device conditions.""" - automations = await _async_get_automations( - hass, device_id, ENTITY_CONDITIONS, DOMAIN - ) - for automation in automations: - automation.update(condition="device") - return automations - - -async def async_get_triggers(hass, device_id): - """List device triggers.""" - automations = await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, DOMAIN) - for automation in automations: - automation.update(platform="device") - return automations diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py new file mode 100644 index 00000000000000..70b79becb8b12d --- /dev/null +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -0,0 +1,247 @@ +"""Implemenet device conditions for binary sensor.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.entity_registry import ( + async_entries_for_device, + async_get_registry, +) +from homeassistant.helpers.typing import ConfigType + +from . import ( + DOMAIN, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +) + +DEVICE_CLASS_NONE = "none" + +CONF_IS_BAT_LOW = "is_bat_low" +CONF_IS_NOT_BAT_LOW = "is_not_bat_low" +CONF_IS_COLD = "is_cold" +CONF_IS_NOT_COLD = "is_not_cold" +CONF_IS_CONNECTED = "is_connected" +CONF_IS_NOT_CONNECTED = "is_not_connected" +CONF_IS_GAS = "is_gas" +CONF_IS_NO_GAS = "is_no_gas" +CONF_IS_HOT = "is_hot" +CONF_IS_NOT_HOT = "is_not_hot" +CONF_IS_LIGHT = "is_light" +CONF_IS_NO_LIGHT = "is_no_light" +CONF_IS_LOCKED = "is_locked" +CONF_IS_NOT_LOCKED = "is_not_locked" +CONF_IS_MOIST = "is_moist" +CONF_IS_NOT_MOIST = "is_not_moist" +CONF_IS_MOTION = "is_motion" +CONF_IS_NO_MOTION = "is_no_motion" +CONF_IS_MOVING = "is_moving" +CONF_IS_NOT_MOVING = "is_not_moving" +CONF_IS_OCCUPIED = "is_occupied" +CONF_IS_NOT_OCCUPIED = "is_not_occupied" +CONF_IS_PLUGGED_IN = "is_plugged_in" +CONF_IS_NOT_PLUGGED_IN = "is_not_plugged_in" +CONF_IS_POWERED = "is_powered" +CONF_IS_NOT_POWERED = "is_not_powered" +CONF_IS_PRESENT = "is_present" +CONF_IS_NOT_PRESENT = "is_not_present" +CONF_IS_PROBLEM = "is_problem" +CONF_IS_NO_PROBLEM = "is_no_problem" +CONF_IS_UNSAFE = "is_unsafe" +CONF_IS_NOT_UNSAFE = "is_not_unsafe" +CONF_IS_SMOKE = "is_smoke" +CONF_IS_NO_SMOKE = "is_no_smoke" +CONF_IS_SOUND = "is_sound" +CONF_IS_NO_SOUND = "is_no_sound" +CONF_IS_VIBRATION = "is_vibration" +CONF_IS_NO_VIBRATION = "is_no_vibration" +CONF_IS_OPEN = "is_open" +CONF_IS_NOT_OPEN = "is_not_open" + +IS_ON = [ + CONF_IS_BAT_LOW, + CONF_IS_COLD, + CONF_IS_CONNECTED, + CONF_IS_GAS, + CONF_IS_HOT, + CONF_IS_LIGHT, + CONF_IS_LOCKED, + CONF_IS_MOIST, + CONF_IS_MOTION, + CONF_IS_MOVING, + CONF_IS_OCCUPIED, + CONF_IS_OPEN, + CONF_IS_PLUGGED_IN, + CONF_IS_POWERED, + CONF_IS_PRESENT, + CONF_IS_PROBLEM, + CONF_IS_SMOKE, + CONF_IS_SOUND, + CONF_IS_UNSAFE, + CONF_IS_VIBRATION, + CONF_IS_ON, +] + +IS_OFF = [ + CONF_IS_NOT_BAT_LOW, + CONF_IS_NOT_COLD, + CONF_IS_NOT_CONNECTED, + CONF_IS_NOT_HOT, + CONF_IS_NOT_LOCKED, + CONF_IS_NOT_MOIST, + CONF_IS_NOT_MOVING, + CONF_IS_NOT_OCCUPIED, + CONF_IS_NOT_OPEN, + CONF_IS_NOT_PLUGGED_IN, + CONF_IS_NOT_POWERED, + CONF_IS_NOT_PRESENT, + CONF_IS_NOT_UNSAFE, + CONF_IS_NO_GAS, + CONF_IS_NO_LIGHT, + CONF_IS_NO_MOTION, + CONF_IS_NO_PROBLEM, + CONF_IS_NO_SMOKE, + CONF_IS_NO_SOUND, + CONF_IS_NO_VIBRATION, + CONF_IS_OFF, +] + +ENTITY_CONDITIONS = { + DEVICE_CLASS_BATTERY: [ + {CONF_TYPE: CONF_IS_BAT_LOW}, + {CONF_TYPE: CONF_IS_NOT_BAT_LOW}, + ], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_IS_COLD}, {CONF_TYPE: CONF_IS_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_IS_CONNECTED}, + {CONF_TYPE: CONF_IS_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [ + {CONF_TYPE: CONF_IS_OPEN}, + {CONF_TYPE: CONF_IS_NOT_OPEN}, + ], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_IS_GAS}, {CONF_TYPE: CONF_IS_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_IS_HOT}, {CONF_TYPE: CONF_IS_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_IS_LIGHT}, {CONF_TYPE: CONF_IS_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_IS_LOCKED}, {CONF_TYPE: CONF_IS_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_IS_MOIST}, {CONF_TYPE: CONF_IS_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_IS_MOTION}, {CONF_TYPE: CONF_IS_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_IS_MOVING}, {CONF_TYPE: CONF_IS_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_IS_OCCUPIED}, + {CONF_TYPE: CONF_IS_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_PLUG: [ + {CONF_TYPE: CONF_IS_PLUGGED_IN}, + {CONF_TYPE: CONF_IS_NOT_PLUGGED_IN}, + ], + DEVICE_CLASS_POWER: [ + {CONF_TYPE: CONF_IS_POWERED}, + {CONF_TYPE: CONF_IS_NOT_POWERED}, + ], + DEVICE_CLASS_PRESENCE: [ + {CONF_TYPE: CONF_IS_PRESENT}, + {CONF_TYPE: CONF_IS_NOT_PRESENT}, + ], + DEVICE_CLASS_PROBLEM: [ + {CONF_TYPE: CONF_IS_PROBLEM}, + {CONF_TYPE: CONF_IS_NO_PROBLEM}, + ], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_IS_UNSAFE}, {CONF_TYPE: CONF_IS_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_IS_SMOKE}, {CONF_TYPE: CONF_IS_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_IS_SOUND}, {CONF_TYPE: CONF_IS_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_IS_VIBRATION}, + {CONF_TYPE: CONF_IS_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_IS_OPEN}, {CONF_TYPE: CONF_IS_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_ON}, {CONF_TYPE: CONF_IS_OFF}], +} + +CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), + } +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + conditions: List[dict] = [] + entity_registry = await async_get_registry(hass) + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + if state and ATTR_DEVICE_CLASS in state.attributes: + device_class = state.attributes[ATTR_DEVICE_CLASS] + + templates = ENTITY_CONDITIONS.get( + device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE] + ) + + conditions.extend( + ( + { + **template, + "condition": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for template in templates + ) + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + condition_type = config[CONF_TYPE] + if condition_type in IS_ON: + stat = "on" + else: + stat = "off" + state_config = { + condition.CONF_CONDITION: "state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + condition.CONF_STATE: stat, + } + + return condition.state_from_config(state_config, config_validation) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py new file mode 100644 index 00000000000000..2211b3001045ed --- /dev/null +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -0,0 +1,238 @@ +"""Provides device triggers for binary sensors.""" +import voluptuous as vol + +from homeassistant.components.automation import state as state_automation +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.const import ( + CONF_TURNED_OFF, + CONF_TURNED_ON, +) +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import config_validation as cv + +from . import ( + DOMAIN, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_COLD, + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PLUG, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DEVICE_CLASS_VIBRATION, + DEVICE_CLASS_WINDOW, +) + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_BAT_LOW = "bat_low" +CONF_NOT_BAT_LOW = "not_bat_low" +CONF_COLD = "cold" +CONF_NOT_COLD = "not_cold" +CONF_CONNECTED = "connected" +CONF_NOT_CONNECTED = "not_connected" +CONF_GAS = "gas" +CONF_NO_GAS = "no_gas" +CONF_HOT = "hot" +CONF_NOT_HOT = "not_hot" +CONF_LIGHT = "light" +CONF_NO_LIGHT = "no_light" +CONF_LOCKED = "locked" +CONF_NOT_LOCKED = "not_locked" +CONF_MOIST = "moist" +CONF_NOT_MOIST = "not_moist" +CONF_MOTION = "motion" +CONF_NO_MOTION = "no_motion" +CONF_MOVING = "moving" +CONF_NOT_MOVING = "not_moving" +CONF_OCCUPIED = "occupied" +CONF_NOT_OCCUPIED = "not_occupied" +CONF_PLUGGED_IN = "plugged_in" +CONF_NOT_PLUGGED_IN = "not_plugged_in" +CONF_POWERED = "powered" +CONF_NOT_POWERED = "not_powered" +CONF_PRESENT = "present" +CONF_NOT_PRESENT = "not_present" +CONF_PROBLEM = "problem" +CONF_NO_PROBLEM = "no_problem" +CONF_UNSAFE = "unsafe" +CONF_NOT_UNSAFE = "not_unsafe" +CONF_SMOKE = "smoke" +CONF_NO_SMOKE = "no_smoke" +CONF_SOUND = "sound" +CONF_NO_SOUND = "no_sound" +CONF_VIBRATION = "vibration" +CONF_NO_VIBRATION = "no_vibration" +CONF_OPEN = "open" +CONF_NOT_OPEN = "not_open" + + +TURNED_ON = [ + CONF_BAT_LOW, + CONF_COLD, + CONF_CONNECTED, + CONF_GAS, + CONF_HOT, + CONF_LIGHT, + CONF_LOCKED, + CONF_MOIST, + CONF_MOTION, + CONF_MOVING, + CONF_OCCUPIED, + CONF_OPEN, + CONF_PLUGGED_IN, + CONF_POWERED, + CONF_PRESENT, + CONF_PROBLEM, + CONF_SMOKE, + CONF_SOUND, + CONF_UNSAFE, + CONF_VIBRATION, + CONF_TURNED_ON, +] + +TURNED_OFF = [ + CONF_NOT_BAT_LOW, + CONF_NOT_COLD, + CONF_NOT_CONNECTED, + CONF_NOT_HOT, + CONF_NOT_LOCKED, + CONF_NOT_MOIST, + CONF_NOT_MOVING, + CONF_NOT_OCCUPIED, + CONF_NOT_OPEN, + CONF_NOT_PLUGGED_IN, + CONF_NOT_POWERED, + CONF_NOT_PRESENT, + CONF_NOT_UNSAFE, + CONF_NO_GAS, + CONF_NO_LIGHT, + CONF_NO_MOTION, + CONF_NO_PROBLEM, + CONF_NO_SMOKE, + CONF_NO_SOUND, + CONF_NO_VIBRATION, + CONF_TURNED_OFF, +] + + +ENTITY_TRIGGERS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BAT_LOW}, {CONF_TYPE: CONF_NOT_BAT_LOW}], + DEVICE_CLASS_COLD: [{CONF_TYPE: CONF_COLD}, {CONF_TYPE: CONF_NOT_COLD}], + DEVICE_CLASS_CONNECTIVITY: [ + {CONF_TYPE: CONF_CONNECTED}, + {CONF_TYPE: CONF_NOT_CONNECTED}, + ], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], + DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], + DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], + DEVICE_CLASS_LOCK: [{CONF_TYPE: CONF_LOCKED}, {CONF_TYPE: CONF_NOT_LOCKED}], + DEVICE_CLASS_MOISTURE: [{CONF_TYPE: CONF_MOIST}, {CONF_TYPE: CONF_NOT_MOIST}], + DEVICE_CLASS_MOTION: [{CONF_TYPE: CONF_MOTION}, {CONF_TYPE: CONF_NO_MOTION}], + DEVICE_CLASS_MOVING: [{CONF_TYPE: CONF_MOVING}, {CONF_TYPE: CONF_NOT_MOVING}], + DEVICE_CLASS_OCCUPANCY: [ + {CONF_TYPE: CONF_OCCUPIED}, + {CONF_TYPE: CONF_NOT_OCCUPIED}, + ], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], + DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], + DEVICE_CLASS_PROBLEM: [{CONF_TYPE: CONF_PROBLEM}, {CONF_TYPE: CONF_NO_PROBLEM}], + DEVICE_CLASS_SAFETY: [{CONF_TYPE: CONF_UNSAFE}, {CONF_TYPE: CONF_NOT_UNSAFE}], + DEVICE_CLASS_SMOKE: [{CONF_TYPE: CONF_SMOKE}, {CONF_TYPE: CONF_NO_SMOKE}], + DEVICE_CLASS_SOUND: [{CONF_TYPE: CONF_SOUND}, {CONF_TYPE: CONF_NO_SOUND}], + DEVICE_CLASS_VIBRATION: [ + {CONF_TYPE: CONF_VIBRATION}, + {CONF_TYPE: CONF_NO_VIBRATION}, + ], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], +} + + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), + } +) + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + trigger_type = config[CONF_TYPE] + if trigger_type in TURNED_ON: + from_state = "off" + to_state = "on" + else: + from_state = "on" + to_state = "off" + + state_config = { + state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_automation.CONF_FROM: from_state, + state_automation.CONF_TO: to_state, + } + + return await state_automation.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + triggers = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = None + state = hass.states.get(entry.entity_id) + if state: + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + templates = ENTITY_TRIGGERS.get( + device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] + ) + + triggers.extend( + ( + { + **automation, + "platform": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for automation in templates + ) + ) + + return triggers diff --git a/homeassistant/components/deconz/device_automation.py b/homeassistant/components/deconz/device_trigger.py similarity index 94% rename from homeassistant/components/deconz/device_automation.py rename to homeassistant/components/deconz/device_trigger.py index 28f36b8f431ea8..77efc78562a924 100644 --- a/homeassistant/components/deconz/device_automation.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -3,6 +3,7 @@ import homeassistant.components.automation.event as event +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -171,16 +172,8 @@ AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, } -TRIGGER_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_TYPE): str, - vol.Required(CONF_SUBTYPE): str, - } - ) +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} ) @@ -198,7 +191,7 @@ def _get_deconz_event_from_device_id(hass, device_id): return None -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" config = TRIGGER_SCHEMA(config) @@ -223,7 +216,9 @@ async def async_trigger(hass, config, action, automation_info): event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, } - return await event.async_trigger(hass, state_config, action, automation_info) + return await event.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) async def async_get_triggers(hass, device_id): diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 9508dd9c849a93..b444abd5238db4 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,16 +1,12 @@ """Helpers for device automations.""" import asyncio import logging -from typing import Callable, cast import voluptuous as vol +from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.components import websocket_api -from homeassistant.const import CONF_DOMAIN -from homeassistant.core import split_entity_id, HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration, IntegrationNotFound DOMAIN = "device_automation" @@ -18,6 +14,21 @@ _LOGGER = logging.getLogger(__name__) +TRIGGER_BASE_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "device", + vol.Required(CONF_DOMAIN): str, + vol.Required(CONF_DEVICE_ID): str, + } +) + +TYPES = { + "trigger": ("device_trigger", "async_get_triggers"), + "condition": ("device_condition", "async_get_conditions"), + "action": ("device_action", "async_get_actions"), +} + + async def async_setup(hass, config): """Set up device automation.""" hass.components.websocket_api.async_register_command( @@ -32,21 +43,9 @@ async def async_setup(hass, config): return True -async def async_device_condition_from_config( - hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: - """Wrap action method with state based condition.""" - if config_validation: - config = cv.DEVICE_CONDITION_SCHEMA(config) - integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") - return cast( - Callable[..., bool], - platform.async_condition_from_config(config, config_validation), # type: ignore - ) - - -async def _async_get_device_automations_from_domain(hass, domain, fname, device_id): +async def _async_get_device_automations_from_domain( + hass, domain, automation_type, device_id +): """List device automations.""" integration = None try: @@ -55,17 +54,18 @@ async def _async_get_device_automations_from_domain(hass, domain, fname, device_ _LOGGER.warning("Integration %s not found", domain) return None + platform_name, function_name = TYPES[automation_type] + try: - platform = integration.get_platform("device_automation") + platform = integration.get_platform(platform_name) except ImportError: # The domain does not have device automations return None - if hasattr(platform, fname): - return await getattr(platform, fname)(hass, device_id) + return await getattr(platform, function_name)(hass, device_id) -async def _async_get_device_automations(hass, fname, device_id): +async def _async_get_device_automations(hass, automation_type, device_id): """List device automations.""" device_registry, entity_registry = await asyncio.gather( hass.helpers.device_registry.async_get_registry(), @@ -79,13 +79,15 @@ async def _async_get_device_automations(hass, fname, device_id): config_entry = hass.config_entries.async_get_entry(entry_id) domains.add(config_entry.domain) - entities = async_entries_for_device(entity_registry, device_id) - for entity in entities: - domains.add(split_entity_id(entity.entity_id)[0]) + entity_entries = async_entries_for_device(entity_registry, device_id) + for entity_entry in entity_entries: + domains.add(entity_entry.domain) device_automations = await asyncio.gather( *( - _async_get_device_automations_from_domain(hass, domain, fname, device_id) + _async_get_device_automations_from_domain( + hass, domain, automation_type, device_id + ) for domain in domains ) ) @@ -106,7 +108,7 @@ async def _async_get_device_automations(hass, fname, device_id): async def websocket_device_automation_list_actions(hass, connection, msg): """Handle request for device actions.""" device_id = msg["device_id"] - actions = await _async_get_device_automations(hass, "async_get_actions", device_id) + actions = await _async_get_device_automations(hass, "action", device_id) connection.send_result(msg["id"], actions) @@ -120,9 +122,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg): async def websocket_device_automation_list_conditions(hass, connection, msg): """Handle request for device conditions.""" device_id = msg["device_id"] - conditions = await _async_get_device_automations( - hass, "async_get_conditions", device_id - ) + conditions = await _async_get_device_automations(hass, "condition", device_id) connection.send_result(msg["id"], conditions) @@ -136,7 +136,5 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] - triggers = await _async_get_device_automations( - hass, "async_get_triggers", device_id - ) + triggers = await _async_get_device_automations(hass, "trigger", device_id) connection.send_result(msg["id"], triggers) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 1593e70771aea3..b7cadd1349a3eb 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,7 +1,9 @@ """Device automation helpers for toggle entity.""" +from typing import List import voluptuous as vol -import homeassistant.components.automation.state as state +from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import state, AutomationActionType from homeassistant.components.device_automation.const import ( CONF_IS_OFF, CONF_IS_ON, @@ -11,17 +13,11 @@ CONF_TURNED_OFF, CONF_TURNED_ON, ) -from homeassistant.core import split_entity_id -from homeassistant.const import ( - CONF_CONDITION, - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_ENTITY_ID, - CONF_PLATFORM, - CONF_TYPE, -) +from homeassistant.const import CONF_CONDITION, CONF_ENTITY_ID, CONF_PLATFORM, CONF_TYPE from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import condition, config_validation as cv, service +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from . import TRIGGER_BASE_SCHEMA ENTITY_ACTIONS = [ { @@ -64,41 +60,35 @@ }, ] -ACTION_SCHEMA = vol.Schema( +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( { - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]), } ) -CONDITION_SCHEMA = vol.Schema( +CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( { - vol.Required(CONF_CONDITION): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), } ) -TRIGGER_SCHEMA = vol.Schema( +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( { - vol.Required(CONF_PLATFORM): "device", - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): str, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), } ) -def _is_domain(entity, domain): - return split_entity_id(entity.entity_id)[0] == domain - - -async def async_call_action_from_config(hass, config, variables, context, domain): +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, + domain: str, +): """Change state based on configuration.""" config = ACTION_SCHEMA(config) action_type = config[CONF_TYPE] @@ -119,7 +109,9 @@ async def async_call_action_from_config(hass, config, variables, context, domain ) -def async_condition_from_config(config, config_validation): +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" condition_type = config[CONF_TYPE] if condition_type == CONF_IS_ON: @@ -135,7 +127,12 @@ def async_condition_from_config(config, config_validation): return condition.state_from_config(state_config, config_validation) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] if trigger_type == CONF_TURNED_ON: @@ -150,37 +147,56 @@ async def async_attach_trigger(hass, config, action, automation_info): state.CONF_TO: to_state, } - return await state.async_trigger(hass, state_config, action, automation_info) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) -async def _async_get_automations(hass, device_id, automation_templates, domain): +async def _async_get_automations( + hass: HomeAssistant, device_id: str, automation_templates: List[dict], domain: str +) -> List[dict]: """List device automations.""" automations = [] entity_registry = await hass.helpers.entity_registry.async_get_registry() - entities = async_entries_for_device(entity_registry, device_id) - domain_entities = [x for x in entities if _is_domain(x, domain)] - for entity in domain_entities: - for automation in automation_templates: - automation = dict(automation) - automation.update( - device_id=device_id, entity_id=entity.entity_id, domain=domain + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == domain + ] + + for entry in entries: + automations.extend( + ( + { + **template, + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": domain, + } + for template in automation_templates ) - automations.append(automation) + ) return automations -async def async_get_actions(hass, device_id, domain): +async def async_get_actions( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device actions.""" return await _async_get_automations(hass, device_id, ENTITY_ACTIONS, domain) -async def async_get_conditions(hass, device_id, domain): +async def async_get_conditions( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device conditions.""" return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain) -async def async_get_triggers(hass, device_id, domain): +async def async_get_triggers( + hass: HomeAssistant, device_id: str, domain: str +) -> List[dict]: """List device triggers.""" return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py new file mode 100644 index 00000000000000..ea37b8e9470534 --- /dev/null +++ b/homeassistant/components/light/device_action.py @@ -0,0 +1,30 @@ +"""Provides device actions for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, Context +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import TemplateVarsType, ConfigType +from . import DOMAIN + + +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, +) -> None: + """Change state based on configuration.""" + config = ACTION_SCHEMA(config) + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_automation.py b/homeassistant/components/light/device_automation.py deleted file mode 100644 index 61292d47449adf..00000000000000 --- a/homeassistant/components/light/device_automation.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Provides device automations for lights.""" -import voluptuous as vol - -from homeassistant.components.device_automation import toggle_entity -from homeassistant.const import CONF_DOMAIN -from . import DOMAIN - - -# mypy: allow-untyped-defs, no-check-untyped-defs - -ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) - -CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - - -async def async_call_action_from_config(hass, config, variables, context): - """Change state based on configuration.""" - config = ACTION_SCHEMA(config) - await toggle_entity.async_call_action_from_config( - hass, config, variables, context, DOMAIN - ) - - -def async_condition_from_config(config, config_validation): - """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) - - -async def async_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) - - -async def async_get_actions(hass, device_id): - """List device actions.""" - return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) - - -async def async_get_conditions(hass, device_id): - """List device conditions.""" - return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) - - -async def async_get_triggers(hass, device_id): - """List device triggers.""" - return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py new file mode 100644 index 00000000000000..a69ca7ab8f2f2f --- /dev/null +++ b/homeassistant/components/light/device_condition.py @@ -0,0 +1,28 @@ +"""Provides device conditions for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.condition import ConditionCheckerType +from . import DOMAIN + + +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> ConditionCheckerType: + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config, config_validation) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py new file mode 100644 index 00000000000000..f2a82afdc2d41c --- /dev/null +++ b/homeassistant/components/light/device_trigger.py @@ -0,0 +1,33 @@ +"""Provides device trigger for lights.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN + + +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info + ) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers.""" + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py new file mode 100644 index 00000000000000..ca91cc70512be5 --- /dev/null +++ b/homeassistant/components/switch/device_action.py @@ -0,0 +1,30 @@ +"""Provides device actions for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, Context +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import TemplateVarsType, ConfigType +from . import DOMAIN + + +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, +) -> None: + """Change state based on configuration.""" + config = ACTION_SCHEMA(config) + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_automation.py b/homeassistant/components/switch/device_automation.py deleted file mode 100644 index 61292d47449adf..00000000000000 --- a/homeassistant/components/switch/device_automation.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Provides device automations for lights.""" -import voluptuous as vol - -from homeassistant.components.device_automation import toggle_entity -from homeassistant.const import CONF_DOMAIN -from . import DOMAIN - - -# mypy: allow-untyped-defs, no-check-untyped-defs - -ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) - -CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - -TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( - {vol.Required(CONF_DOMAIN): DOMAIN} -) - - -async def async_call_action_from_config(hass, config, variables, context): - """Change state based on configuration.""" - config = ACTION_SCHEMA(config) - await toggle_entity.async_call_action_from_config( - hass, config, variables, context, DOMAIN - ) - - -def async_condition_from_config(config, config_validation): - """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) - - -async def async_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) - - -async def async_get_actions(hass, device_id): - """List device actions.""" - return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) - - -async def async_get_conditions(hass, device_id): - """List device conditions.""" - return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) - - -async def async_get_triggers(hass, device_id): - """List device triggers.""" - return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py new file mode 100644 index 00000000000000..032c765bf5907c --- /dev/null +++ b/homeassistant/components/switch/device_condition.py @@ -0,0 +1,28 @@ +"""Provides device conditions for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.condition import ConditionCheckerType +from . import DOMAIN + + +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> ConditionCheckerType: + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config, config_validation) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py new file mode 100644 index 00000000000000..9be294d5460c99 --- /dev/null +++ b/homeassistant/components/switch/device_trigger.py @@ -0,0 +1,33 @@ +"""Provides device triggers for switches.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN + + +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info + ) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers.""" + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/zha/device_automation.py b/homeassistant/components/zha/device_trigger.py similarity index 84% rename from homeassistant/components/zha/device_automation.py rename to homeassistant/components/zha/device_trigger.py index 6a96ce5aa3e405..46e3beafcaedfd 100644 --- a/homeassistant/components/zha/device_automation.py +++ b/homeassistant/components/zha/device_trigger.py @@ -6,6 +6,7 @@ InvalidDeviceAutomationConfig, ) from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from . import DOMAIN from .core.const import DATA_ZHA, DATA_ZHA_GATEWAY @@ -16,20 +17,12 @@ DEVICE_IEEE = "device_ieee" ZHA_EVENT = "zha_event" -TRIGGER_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_PLATFORM): DEVICE, - vol.Required(CONF_TYPE): str, - vol.Required(CONF_SUBTYPE): str, - } - ) +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} ) -async def async_trigger(hass, config, action, automation_info): +async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" config = TRIGGER_SCHEMA(config) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) @@ -48,7 +41,9 @@ async def async_trigger(hass, config, action, automation_info): event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, } - return await event.async_trigger(hass, state_config, action, automation_info) + return await event.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) async def async_get_triggers(hass, device_id): diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 133251e779d1db..afb8c3934a7442 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -8,16 +8,14 @@ from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, TemplateVarsType - +from homeassistant.loader import async_get_integration from homeassistant.core import HomeAssistant, State from homeassistant.components import zone as zone_cmp -from homeassistant.components.device_automation import ( # noqa: F401 pylint: disable=unused-import - async_device_condition_from_config as async_device_from_config, -) from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_DOMAIN, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, CONF_CONDITION, @@ -45,10 +43,12 @@ _LOGGER = logging.getLogger(__name__) +ConditionCheckerType = Callable[[HomeAssistant, TemplateVarsType], bool] + async def async_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Turn a condition configuration into a method. Should be run on the event loop. @@ -74,13 +74,15 @@ async def async_from_config( check_factory = check_factory.func if asyncio.iscoroutinefunction(check_factory): - return cast(Callable[..., bool], await factory(hass, config, config_validation)) - return cast(Callable[..., bool], factory(config, config_validation)) + return cast( + ConditionCheckerType, await factory(hass, config, config_validation) + ) + return cast(ConditionCheckerType, factory(config, config_validation)) async def async_and_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Create multi condition matcher using 'AND'.""" if config_validation: config = cv.AND_CONDITION_SCHEMA(config) @@ -107,7 +109,7 @@ def if_and_condition( async def async_or_from_config( hass: HomeAssistant, config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Create multi condition matcher using 'OR'.""" if config_validation: config = cv.OR_CONDITION_SCHEMA(config) @@ -205,7 +207,7 @@ def async_numeric_state( def async_numeric_state_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.NUMERIC_STATE_CONDITION_SCHEMA(config) @@ -255,7 +257,7 @@ def state( def state_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.STATE_CONDITION_SCHEMA(config) @@ -327,7 +329,7 @@ def sun( def sun_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with sun based condition.""" if config_validation: config = cv.SUN_CONDITION_SCHEMA(config) @@ -370,7 +372,7 @@ def async_template( def async_template_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with state based condition.""" if config_validation: config = cv.TEMPLATE_CONDITION_SCHEMA(config) @@ -427,7 +429,7 @@ def time( def time_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with time based condition.""" if config_validation: config = cv.TIME_CONDITION_SCHEMA(config) @@ -476,7 +478,7 @@ def zone( def zone_from_config( config: ConfigType, config_validation: bool = True -) -> Callable[..., bool]: +) -> ConditionCheckerType: """Wrap action method with zone based condition.""" if config_validation: config = cv.ZONE_CONDITION_SCHEMA(config) @@ -488,3 +490,17 @@ def if_in_zone(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: return zone(hass, zone_entity_id, entity_id) return if_in_zone + + +async def async_device_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True +) -> ConditionCheckerType: + """Test a device condition.""" + if config_validation: + config = cv.DEVICE_CONDITION_SCHEMA(config) + integration = await async_get_integration(hass, config[CONF_DOMAIN]) + platform = integration.get_platform("device_condition") + return cast( + ConditionCheckerType, + platform.async_condition_from_config(config, config_validation), # type: ignore + ) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 952fa41c42c1ce..113f2437ce8cea 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -827,11 +827,16 @@ def validator(value): } ) -DEVICE_CONDITION_SCHEMA = vol.Schema( - {vol.Required(CONF_CONDITION): "device", vol.Required(CONF_DOMAIN): str}, - extra=vol.ALLOW_EXTRA, +DEVICE_CONDITION_BASE_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONDITION): "device", + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): str, + } ) +DEVICE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + CONDITION_SCHEMA: vol.Schema = vol.Any( NUMERIC_STATE_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA, @@ -862,11 +867,12 @@ def validator(value): } ) -DEVICE_ACTION_SCHEMA = vol.Schema( - {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str}, - extra=vol.ALLOW_EXTRA, +DEVICE_ACTION_BASE_SCHEMA = vol.Schema( + {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str} ) +DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + SCRIPT_SCHEMA = vol.All( ensure_list, [ diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 23728b651098aa..14ff873d4d173b 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -336,7 +336,7 @@ async def _async_device_automation(self, action, variables, context): self.last_action = action.get(CONF_ALIAS, "device automation") self._log("Executing step %s" % self.last_action) integration = await async_get_integration(self.hass, action[CONF_DOMAIN]) - platform = integration.get_platform("device_automation") + platform = integration.get_platform("device_action") await platform.async_call_action_from_config( self.hass, action, variables, context ) diff --git a/tests/common.py b/tests/common.py index fda5c743222703..bc39b1f5e0b62a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -54,7 +54,9 @@ from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe - +from homeassistant.components.device_automation import ( # noqa + _async_get_device_automations as async_get_device_automations, +) _TEST_INSTANCE_PORT = SERVER_PORT _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/binary_sensor/test_device_automation.py b/tests/components/binary_sensor/test_device_automation.py deleted file mode 100644 index 91124d47f4e4ef..00000000000000 --- a/tests/components/binary_sensor/test_device_automation.py +++ /dev/null @@ -1,309 +0,0 @@ -"""The test for binary_sensor device automation.""" -import pytest - -from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES -from homeassistant.components.binary_sensor.device_automation import ( - ENTITY_CONDITIONS, - ENTITY_TRIGGERS, -) -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) -from homeassistant.helpers import device_registry - -from tests.common import ( - MockConfigEntry, - async_mock_service, - mock_device_registry, - mock_registry, -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -def entity_reg(hass): - """Return an empty, loaded, registry.""" - return mock_registry(hass) - - -@pytest.fixture -def calls(hass): - """Track calls to a mock serivce.""" - return async_mock_service(hass, "test", "automation") - - -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - -async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a binary_sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create( - DOMAIN, - "test", - platform.ENTITIES["battery"].unique_id, - device_id=device_entry.id, - ) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_actions = [] - actions = await async_get_device_automations( - hass, "async_get_actions", device_entry.id - ) - assert _same_lists(actions, expected_actions) - - -async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a binary_sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - for device_class in DEVICE_CLASSES: - entity_reg.async_get_or_create( - DOMAIN, - "test", - platform.ENTITIES[device_class].unique_id, - device_id=device_entry.id, - ) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": condition["type"], - "device_id": device_entry.id, - "entity_id": platform.ENTITIES[device_class].entity_id, - } - for device_class in DEVICE_CLASSES - for condition in ENTITY_CONDITIONS[device_class] - ] - conditions = await async_get_device_automations( - hass, "async_get_conditions", device_entry.id - ) - assert _same_lists(conditions, expected_conditions) - - -async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a binary_sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - for device_class in DEVICE_CLASSES: - entity_reg.async_get_or_create( - DOMAIN, - "test", - platform.ENTITIES[device_class].unique_id, - device_id=device_entry.id, - ) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - expected_triggers = [ - { - "platform": "device", - "domain": DOMAIN, - "type": trigger["type"], - "device_id": device_entry.id, - "entity_id": platform.ENTITIES[device_class].entity_id, - } - for device_class in DEVICE_CLASSES - for trigger in ENTITY_TRIGGERS[device_class] - ] - triggers = await async_get_device_automations( - hass, "async_get_triggers", device_entry.id - ) - assert _same_lists(triggers, expected_triggers) - - -async def test_if_fires_on_state_change(hass, calls): - """Test for on and off triggers firing.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - sensor1 = platform.ENTITIES["battery"] - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": sensor1.entity_id, - "type": "bat_low", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": sensor1.entity_id, - "type": "not_bat_low", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "not_bat_low {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(sensor1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.states.async_set(sensor1.entity_id, STATE_OFF) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "not_bat_low state - {} - on - off - None".format( - sensor1.entity_id - ) - - hass.states.async_set(sensor1.entity_id, STATE_ON) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "bat_low state - {} - off - on - None".format( - sensor1.entity_id - ) - - -async def test_if_state(hass, calls): - """Test for turn_on and turn_off conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - sensor1 = platform.ENTITIES["battery"] - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": sensor1.entity_id, - "type": "is_bat_low", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": sensor1.entity_id, - "type": "is_not_bat_low", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(sensor1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" - - hass.states.async_set(sensor1.entity_id, STATE_OFF) - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py new file mode 100644 index 00000000000000..b5502d8fe3da39 --- /dev/null +++ b/tests/components/binary_sensor/test_device_condition.py @@ -0,0 +1,144 @@ +"""The test for binary_sensor device automation.""" +import pytest + +from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": condition["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for condition in ENTITY_CONDITIONS[device_class] + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_bat_low", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_not_bat_low", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py new file mode 100644 index 00000000000000..5be354c78fc5cf --- /dev/null +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -0,0 +1,154 @@ +"""The test for binary_sensor device automation.""" +import pytest + +from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.binary_sensor.device_trigger import ENTITY_TRIGGERS +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a binary_sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": trigger["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for trigger in ENTITY_TRIGGERS[device_class] + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_if_fires_on_state_change(hass, calls): + """Test for on and off triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "bat_low", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "not_bat_low", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "not_bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "not_bat_low device - {} - on - off - None".format( + sensor1.entity_id + ) + + hass.states.async_set(sensor1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "bat_low device - {} - off - on - None".format( + sensor1.entity_id + ) diff --git a/tests/components/binary_sensor/test_binary_sensor.py b/tests/components/binary_sensor/test_init.py similarity index 100% rename from tests/components/binary_sensor/test_binary_sensor.py rename to tests/components/binary_sensor/test_init.py diff --git a/tests/components/deconz/test_device_automation.py b/tests/components/deconz/test_device_trigger.py similarity index 71% rename from tests/components/deconz/test_device_automation.py rename to tests/components/deconz/test_device_trigger.py index 0be566d4b52e43..6590028d76638a 100644 --- a/tests/components/deconz/test_device_automation.py +++ b/tests/components/deconz/test_device_trigger.py @@ -3,9 +3,9 @@ from homeassistant import config_entries from homeassistant.components import deconz -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) +from homeassistant.components.deconz import device_trigger + +from tests.common import async_get_device_automations BRIDGEID = "0123456789" @@ -49,16 +49,6 @@ DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG, "sensors": DECONZ_SENSOR} -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - async def setup_deconz(hass, options): """Create the deCONZ gateway.""" config_entry = config_entries.ConfigEntry( @@ -88,51 +78,51 @@ async def test_get_triggers(hass): """Test triggers work.""" gateway = await setup_deconz(hass, options={}) device_id = gateway.events[0].device_id - triggers = await async_get_device_automations(hass, "async_get_triggers", device_id) + triggers = await async_get_device_automations(hass, "trigger", device_id) expected_triggers = [ { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_SHORT_PRESS, - "subtype": deconz.device_automation.CONF_TURN_ON, + "type": device_trigger.CONF_SHORT_PRESS, + "subtype": device_trigger.CONF_TURN_ON, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_PRESS, - "subtype": deconz.device_automation.CONF_TURN_ON, + "type": device_trigger.CONF_LONG_PRESS, + "subtype": device_trigger.CONF_TURN_ON, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_RELEASE, - "subtype": deconz.device_automation.CONF_TURN_ON, + "type": device_trigger.CONF_LONG_RELEASE, + "subtype": device_trigger.CONF_TURN_ON, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_SHORT_PRESS, - "subtype": deconz.device_automation.CONF_TURN_OFF, + "type": device_trigger.CONF_SHORT_PRESS, + "subtype": device_trigger.CONF_TURN_OFF, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_PRESS, - "subtype": deconz.device_automation.CONF_TURN_OFF, + "type": device_trigger.CONF_LONG_PRESS, + "subtype": device_trigger.CONF_TURN_OFF, }, { "device_id": device_id, "domain": "deconz", "platform": "device", - "type": deconz.device_automation.CONF_LONG_RELEASE, - "subtype": deconz.device_automation.CONF_TURN_OFF, + "type": device_trigger.CONF_LONG_RELEASE, + "subtype": device_trigger.CONF_TURN_OFF, }, ] - assert _same_lists(triggers, expected_triggers) + assert triggers == expected_triggers diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py new file mode 100644 index 00000000000000..bb50778db52f70 --- /dev/null +++ b/tests/components/light/test_device_action.py @@ -0,0 +1,140 @@ +"""The test for light device automation.""" +import pytest + +from homeassistant.components.light import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a light.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "toggle", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert actions == expected_actions + + +async def test_action(hass, calls): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_off", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_on", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "toggle", + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/light/test_device_automation.py b/tests/components/light/test_device_automation.py deleted file mode 100644 index 27b8b860d7227c..00000000000000 --- a/tests/components/light/test_device_automation.py +++ /dev/null @@ -1,373 +0,0 @@ -"""The test for light device automation.""" -import pytest - -from homeassistant.components.light import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) -from homeassistant.helpers import device_registry - -from tests.common import ( - MockConfigEntry, - async_mock_service, - mock_device_registry, - mock_registry, -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -def entity_reg(hass): - """Return an empty, loaded, registry.""" - return mock_registry(hass) - - -@pytest.fixture -def calls(hass): - """Track calls to a mock serivce.""" - return async_mock_service(hass, "test", "automation") - - -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - -async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a light.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_actions = [ - { - "domain": DOMAIN, - "type": "turn_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "turn_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "toggle", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - actions = await async_get_device_automations( - hass, "async_get_actions", device_entry.id - ) - assert _same_lists(actions, expected_actions) - - -async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a light.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": "is_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - conditions = await async_get_device_automations( - hass, "async_get_conditions", device_entry.id - ) - assert _same_lists(conditions, expected_conditions) - - -async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a light.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_triggers = [ - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - triggers = await async_get_device_automations( - hass, "async_get_triggers", device_entry.id - ) - assert _same_lists(triggers, expected_triggers) - - -async def test_if_fires_on_state_change(hass, calls): - """Test for turn_on and turn_off triggers firing.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turned_on", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turned_off", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.states.async_set(ent1.entity_id, STATE_OFF) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - ent1.entity_id - ) - - hass.states.async_set(ent1.entity_id, STATE_ON) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - ent1.entity_id - ) - - -async def test_if_state(hass, calls): - """Test for turn_on and turn_off conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_on", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_off", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" - - hass.states.async_set(ent1.entity_id, STATE_OFF) - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" - - -async def test_action(hass, calls): - """Test for turn_on and turn_off actions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_off", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_on", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event3"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "toggle", - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py new file mode 100644 index 00000000000000..8009fbd633798a --- /dev/null +++ b/tests/components/light/test_device_condition.py @@ -0,0 +1,136 @@ +"""The test for light device automation.""" +import pytest + +from homeassistant.components.light import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a light.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py new file mode 100644 index 00000000000000..9b540c7aa15af7 --- /dev/null +++ b/tests/components/light/test_device_trigger.py @@ -0,0 +1,147 @@ +"""The test for light device automation.""" +import pytest + +from homeassistant.components.light import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a light.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "turn_off device - {} - on - off - None".format( + ent1.entity_id + ) + + hass.states.async_set(ent1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( + ent1.entity_id + ) diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py new file mode 100644 index 00000000000000..888e06e0214db8 --- /dev/null +++ b/tests/components/switch/test_device_action.py @@ -0,0 +1,142 @@ +"""The test for switch device automation.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "domain": DOMAIN, + "type": "toggle", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert actions == expected_actions + + +async def test_action(hass, calls): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_off", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turn_on", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "action": { + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "toggle", + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_OFF + + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/switch/test_device_automation.py b/tests/components/switch/test_device_automation.py deleted file mode 100644 index 1ebe4785761aa5..00000000000000 --- a/tests/components/switch/test_device_automation.py +++ /dev/null @@ -1,373 +0,0 @@ -"""The test for switch device automation.""" -import pytest - -from homeassistant.components.switch import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) -from homeassistant.helpers import device_registry - -from tests.common import ( - MockConfigEntry, - async_mock_service, - mock_device_registry, - mock_registry, -) - - -@pytest.fixture -def device_reg(hass): - """Return an empty, loaded, registry.""" - return mock_device_registry(hass) - - -@pytest.fixture -def entity_reg(hass): - """Return an empty, loaded, registry.""" - return mock_registry(hass) - - -@pytest.fixture -def calls(hass): - """Track calls to a mock serivce.""" - return async_mock_service(hass, "test", "automation") - - -def _same_lists(a, b): - if len(a) != len(b): - return False - - for d in a: - if d not in b: - return False - return True - - -async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a switch.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_actions = [ - { - "domain": DOMAIN, - "type": "turn_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "turn_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "domain": DOMAIN, - "type": "toggle", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - actions = await async_get_device_automations( - hass, "async_get_actions", device_entry.id - ) - assert _same_lists(actions, expected_actions) - - -async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a switch.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_conditions = [ - { - "condition": "device", - "domain": DOMAIN, - "type": "is_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "condition": "device", - "domain": DOMAIN, - "type": "is_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - conditions = await async_get_device_automations( - hass, "async_get_conditions", device_entry.id - ) - assert _same_lists(conditions, expected_conditions) - - -async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a switch.""" - config_entry = MockConfigEntry(domain="test", data={}) - config_entry.add_to_hass(hass) - device_entry = device_reg.async_get_or_create( - config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) - expected_triggers = [ - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_off", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - { - "platform": "device", - "domain": DOMAIN, - "type": "turned_on", - "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", - }, - ] - triggers = await async_get_device_automations( - hass, "async_get_triggers", device_entry.id - ) - assert _same_lists(triggers, expected_triggers) - - -async def test_if_fires_on_state_change(hass, calls): - """Test for turn_on and turn_off triggers firing.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turned_on", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "turn_on {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - { - "trigger": { - "platform": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turned_off", - }, - "action": { - "service": "test.automation", - "data_template": { - "some": "turn_off {{ trigger.%s }}" - % "}} - {{ trigger.".join( - ( - "platform", - "entity_id", - "from_state.state", - "to_state.state", - "for", - ) - ) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.states.async_set(ent1.entity_id, STATE_OFF) - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "turn_off state - {} - on - off - None".format( - ent1.entity_id - ) - - hass.states.async_set(ent1.entity_id, STATE_ON) - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "turn_on state - {} - off - on - None".format( - ent1.entity_id - ) - - -async def test_if_state(hass, calls): - """Test for turn_on and turn_off conditions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_on", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_on {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "condition": [ - { - "condition": "device", - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "is_off", - } - ], - "action": { - "service": "test.automation", - "data_template": { - "some": "is_off {{ trigger.%s }}" - % "}} - {{ trigger.".join(("platform", "event.event_type")) - }, - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].data["some"] == "is_on event - test_event1" - - hass.states.async_set(ent1.entity_id, STATE_OFF) - hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert len(calls) == 2 - assert calls[1].data["some"] == "is_off event - test_event2" - - -async def test_action(hass, calls): - """Test for turn_on and turn_off actions.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - - platform.init() - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) - - ent1, ent2, ent3 = platform.ENTITIES - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": {"platform": "event", "event_type": "test_event1"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_off", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event2"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "turn_on", - }, - }, - { - "trigger": {"platform": "event", "event_type": "test_event3"}, - "action": { - "domain": DOMAIN, - "device_id": "", - "entity_id": ent1.entity_id, - "type": "toggle", - }, - }, - ] - }, - ) - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - assert len(calls) == 0 - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event1") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event2") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_OFF - - hass.bus.async_fire("test_event3") - await hass.async_block_till_done() - assert hass.states.get(ent1.entity_id).state == STATE_ON diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py new file mode 100644 index 00000000000000..e2ce5a373d227f --- /dev/null +++ b/tests/components/switch/test_device_condition.py @@ -0,0 +1,138 @@ +"""The test for switch device automation.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on event - test_event1" + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off event - test_event2" diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py new file mode 100644 index 00000000000000..43af9fe3df34b2 --- /dev/null +++ b/tests/components/switch/test_device_trigger.py @@ -0,0 +1,147 @@ +"""The test for switch device automation.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_on {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "turn_off device - {} - on - off - None".format( + ent1.entity_id + ) + + hass.states.async_set(ent1.entity_id, STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( + ent1.entity_id + ) diff --git a/tests/components/zha/test_device_automation.py b/tests/components/zha/test_device_automation.py index 9de04ae8e662e8..5a4b9d5616e8fe 100644 --- a/tests/components/zha/test_device_automation.py +++ b/tests/components/zha/test_device_automation.py @@ -4,9 +4,6 @@ import pytest import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.components.switch import DOMAIN from homeassistant.components.zha.core.const import CHANNEL_ON_OFF from homeassistant.helpers.device_registry import async_get_registry @@ -14,7 +11,7 @@ from .common import async_enable_traffic, async_init_zigpy_device -from tests.common import async_mock_service +from tests.common import async_mock_service, async_get_device_automations ON = 1 OFF = 0 @@ -73,9 +70,7 @@ async def test_triggers(hass, config_entry, zha_gateway): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - triggers = await async_get_device_automations( - hass, "async_get_triggers", reg_device.id - ) + triggers = await async_get_device_automations(hass, "trigger", reg_device.id) expected_triggers = [ { @@ -136,9 +131,7 @@ async def test_no_triggers(hass, config_entry, zha_gateway): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - triggers = await async_get_device_automations( - hass, "async_get_triggers", reg_device.id - ) + triggers = await async_get_device_automations(hass, "trigger", reg_device.id) assert triggers == [] From 9c9c921922876949fae384a1aea937ef55114168 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Sep 2019 00:38:20 +0200 Subject: [PATCH 0443/3953] Use Python3 new super syntax sugar (#26890) --- homeassistant/components/bloomsky/camera.py | 2 +- homeassistant/components/foscam/camera.py | 2 +- homeassistant/components/graphite/__init__.py | 2 +- homeassistant/components/loopenergy/sensor.py | 4 ++-- homeassistant/components/mochad/__init__.py | 2 +- homeassistant/components/nest/binary_sensor.py | 2 +- homeassistant/components/nest/camera.py | 2 +- homeassistant/components/netatmo/camera.py | 2 +- homeassistant/components/nuimo_controller/__init__.py | 2 +- homeassistant/components/nx584/binary_sensor.py | 2 +- homeassistant/components/onkyo/media_player.py | 2 +- homeassistant/components/ring/binary_sensor.py | 2 +- homeassistant/components/ring/camera.py | 2 +- homeassistant/components/ring/sensor.py | 2 +- homeassistant/components/squeezebox/media_player.py | 2 +- homeassistant/components/tcp/sensor.py | 2 +- homeassistant/components/tplink/device_tracker.py | 4 ++-- homeassistant/components/ubus/device_tracker.py | 2 +- homeassistant/components/uvc/camera.py | 2 +- homeassistant/components/wink/__init__.py | 4 ++-- homeassistant/components/wink/switch.py | 2 +- homeassistant/components/zigbee/__init__.py | 4 ++-- homeassistant/components/zwave/binary_sensor.py | 2 +- homeassistant/helpers/logging.py | 2 +- homeassistant/util/yaml/loader.py | 2 +- tests/components/smhi/common.py | 2 +- 26 files changed, 30 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/bloomsky/camera.py b/homeassistant/components/bloomsky/camera.py index 9b8c4ab283f3b2..d62dede5abdab4 100644 --- a/homeassistant/components/bloomsky/camera.py +++ b/homeassistant/components/bloomsky/camera.py @@ -19,7 +19,7 @@ class BloomSkyCamera(Camera): def __init__(self, bs, device): """Initialize access to the BloomSky camera images.""" - super(BloomSkyCamera, self).__init__() + super().__init__() self._name = device["DeviceName"] self._id = device["DeviceID"] self._bloomsky = bs diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 37f792cec45b83..63e9956d0dfaf6 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -41,7 +41,7 @@ def __init__(self, device_info): """Initialize a Foscam camera.""" from libpyfoscam import FoscamCamera - super(FoscamCam, self).__init__() + super().__init__() ip_address = device_info.get(CONF_IP) port = device_info.get(CONF_PORT) diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 550a0ce1d1303e..3809249bea698f 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -64,7 +64,7 @@ class GraphiteFeeder(threading.Thread): def __init__(self, hass, host, port, prefix): """Initialize the feeder.""" - super(GraphiteFeeder, self).__init__(daemon=True) + super().__init__(daemon=True) self._hass = hass self._host = host self._port = port diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py index 56a87ce4345867..994c3e2fd8952a 100644 --- a/homeassistant/components/loopenergy/sensor.py +++ b/homeassistant/components/loopenergy/sensor.py @@ -122,7 +122,7 @@ class LoopEnergyElec(LoopEnergyDevice): def __init__(self, controller): """Initialize the sensor.""" - super(LoopEnergyElec, self).__init__(controller) + super().__init__(controller) self._name = "Power Usage" self._controller.subscribe_elecricity(self._callback) @@ -136,7 +136,7 @@ class LoopEnergyGas(LoopEnergyDevice): def __init__(self, controller): """Initialize the sensor.""" - super(LoopEnergyGas, self).__init__(controller) + super().__init__(controller) self._name = "Gas Usage" self._controller.subscribe_gas(self._callback) diff --git a/homeassistant/components/mochad/__init__.py b/homeassistant/components/mochad/__init__.py index 07028de0d42419..77426e8ae2c911 100644 --- a/homeassistant/components/mochad/__init__.py +++ b/homeassistant/components/mochad/__init__.py @@ -64,7 +64,7 @@ class MochadCtrl: def __init__(self, host, port): """Initialize a PyMochad controller.""" - super(MochadCtrl, self).__init__() + super().__init__() self._host = host self._port = port diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index 0f3ae7da710edf..05170a54ed1b7a 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -141,7 +141,7 @@ class NestActivityZoneSensor(NestBinarySensor): def __init__(self, structure, device, zone): """Initialize the sensor.""" - super(NestActivityZoneSensor, self).__init__(structure, device, "") + super().__init__(structure, device, "") self.zone = zone self._name = f"{self._name} {self.zone.name} activity" diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 37fd18efb6d5e9..efc0bfbc8227e1 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -34,7 +34,7 @@ class NestCamera(Camera): def __init__(self, structure, device): """Initialize a Nest Camera.""" - super(NestCamera, self).__init__() + super().__init__() self.structure = structure self.device = device self._location = None diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index d18ff9fc46c800..60428961cb95be 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -69,7 +69,7 @@ class NetatmoCamera(Camera): def __init__(self, data, camera_name, home, camera_type, verify_ssl, quality): """Set up for access to the Netatmo camera images.""" - super(NetatmoCamera, self).__init__() + super().__init__() self._data = data self._camera_name = camera_name self._verify_ssl = verify_ssl diff --git a/homeassistant/components/nuimo_controller/__init__.py b/homeassistant/components/nuimo_controller/__init__.py index 42d35b60b250b3..8fa3897b735776 100644 --- a/homeassistant/components/nuimo_controller/__init__.py +++ b/homeassistant/components/nuimo_controller/__init__.py @@ -76,7 +76,7 @@ class NuimoThread(threading.Thread): def __init__(self, hass, mac, name): """Initialize thread object.""" - super(NuimoThread, self).__init__() + super().__init__() self._hass = hass self._mac = mac self._name = name diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 8b26a958a6ffa7..6f1c66d8d879b6 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -107,7 +107,7 @@ class NX584Watcher(threading.Thread): def __init__(self, client, zone_sensors): """Initialize NX584 watcher thread.""" - super(NX584Watcher, self).__init__() + super().__init__() self.daemon = True self._client = client self._zone_sensors = zone_sensors diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 9ec8c56d770a00..af92f6c5f0510a 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -373,7 +373,7 @@ def __init__(self, zone, receiver, sources, name=None): """Initialize the Zone with the zone identifier.""" self._zone = zone self._supports_volume = True - super(OnkyoDeviceZone, self).__init__(receiver, sources, name) + super().__init__(receiver, sources, name) def update(self): """Get the latest state from the device.""" diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 6806df0408fb78..86d26ec25b4ea6 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -65,7 +65,7 @@ class RingBinarySensor(BinarySensorDevice): def __init__(self, hass, data, sensor_type): """Initialize a sensor for Ring device.""" - super(RingBinarySensor, self).__init__() + super().__init__() self._sensor_type = sensor_type self._data = data self._name = "{0} {1}".format( diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index ef86ea6734cf6a..461b3a199d7721 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -75,7 +75,7 @@ class RingCam(Camera): def __init__(self, hass, camera, device_info): """Initialize a Ring Door Bell camera.""" - super(RingCam, self).__init__() + super().__init__() self._camera = camera self._hass = hass self._name = self._camera.name diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index af661f4571c210..6a64226a053e97 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -110,7 +110,7 @@ class RingSensor(Entity): def __init__(self, hass, data, sensor_type): """Initialize a sensor for Ring device.""" - super(RingSensor, self).__init__() + super().__init__() self._sensor_type = sensor_type self._data = data self._extra = None diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 9e62e7ee0dbc83..6540fca1405871 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -246,7 +246,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): def __init__(self, lms, player_id, name): """Initialize the SqueezeBox device.""" - super(SqueezeBoxDevice, self).__init__() + super().__init__() self._lms = lms self._id = player_id self._status = {} diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index 70301f203f8ccb..a387b3fc0bb7f7 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -81,7 +81,7 @@ def name(self): name = self._config[CONF_NAME] if name is not None: return name - return super(TcpSensor, self).name + return super().name @property def state(self): diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py index 60d495738338e7..e7f87074cb4c00 100644 --- a/homeassistant/components/tplink/device_tracker.py +++ b/homeassistant/components/tplink/device_tracker.py @@ -245,7 +245,7 @@ def __init__(self, config): """Initialize the scanner.""" self.stok = "" self.sysauth = "" - super(Tplink3DeviceScanner, self).__init__(config) + super().__init__(config) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -365,7 +365,7 @@ def __init__(self, config): """Initialize the scanner.""" self.credentials = "" self.token = "" - super(Tplink4DeviceScanner, self).__init__(config) + super().__init__(config) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index f14ea5af02cd43..8c83de202a468c 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -146,7 +146,7 @@ class DnsmasqUbusDeviceScanner(UbusDeviceScanner): def __init__(self, config): """Initialize the scanner.""" - super(DnsmasqUbusDeviceScanner, self).__init__(config) + super().__init__(config) self.leasefile = None def _generate_mac2name(self): diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 01a3338e540760..20aae3849ab354 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -78,7 +78,7 @@ class UnifiVideoCamera(Camera): def __init__(self, nvr, uuid, name, password): """Initialize an Unifi camera.""" - super(UnifiVideoCamera, self).__init__() + super().__init__() self._nvr = nvr self._uuid = uuid self._name = name diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 5af784359d83db..d0bb27c06e11b9 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -863,7 +863,7 @@ def icon(self): @property def device_state_attributes(self): """Return the device state attributes.""" - attributes = super(WinkSirenDevice, self).device_state_attributes + attributes = super().device_state_attributes auto_shutoff = self.wink.auto_shutoff() if auto_shutoff is not None: @@ -921,7 +921,7 @@ def name(self): @property def device_state_attributes(self): """Return the device state attributes.""" - attributes = super(WinkNimbusDialDevice, self).device_state_attributes + attributes = super().device_state_attributes dial_attributes = self.dial_attributes() return {**attributes, **dial_attributes} diff --git a/homeassistant/components/wink/switch.py b/homeassistant/components/wink/switch.py index d888fb82528f67..07d3ff4becc04c 100644 --- a/homeassistant/components/wink/switch.py +++ b/homeassistant/components/wink/switch.py @@ -53,7 +53,7 @@ def turn_off(self, **kwargs): @property def device_state_attributes(self): """Return the state attributes.""" - attributes = super(WinkToggleDevice, self).device_state_attributes + attributes = super().device_state_attributes try: event = self.wink.last_event() if event is not None: diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/zigbee/__init__.py index 7c1979f3ab9542..31cbc0c65b6aa7 100644 --- a/homeassistant/components/zigbee/__init__.py +++ b/homeassistant/components/zigbee/__init__.py @@ -172,7 +172,7 @@ class ZigBeeDigitalInConfig(ZigBeePinConfig): def __init__(self, config): """Initialise the Zigbee Digital input config.""" - super(ZigBeeDigitalInConfig, self).__init__(config) + super().__init__(config) self._bool2state, self._state2bool = self.boolean_maps @property @@ -216,7 +216,7 @@ class ZigBeeDigitalOutConfig(ZigBeePinConfig): def __init__(self, config): """Initialize the Zigbee Digital out.""" - super(ZigBeeDigitalOutConfig, self).__init__(config) + super().__init__(config) self._bool2state, self._state2bool = self.boolean_maps self._should_poll = config.get("poll", False) diff --git a/homeassistant/components/zwave/binary_sensor.py b/homeassistant/components/zwave/binary_sensor.py index fd18772edbf5ef..a28f53f93d463a 100644 --- a/homeassistant/components/zwave/binary_sensor.py +++ b/homeassistant/components/zwave/binary_sensor.py @@ -71,7 +71,7 @@ class ZWaveTriggerSensor(ZWaveBinarySensor): def __init__(self, values, device_class): """Initialize the sensor.""" - super(ZWaveTriggerSensor, self).__init__(values, device_class) + super().__init__(values, device_class) # Set default off delay to 60 sec self.re_arm_sec = 60 self.invalidate_after = None diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index e69564453fada0..dd9e383380191c 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -29,7 +29,7 @@ class KeywordStyleAdapter(logging.LoggerAdapter): def __init__(self, logger, extra=None): """Initialize a new StyleAdapter for the provided logger.""" - super(KeywordStyleAdapter, self).__init__(logger, extra or {}) + super().__init__(logger, extra or {}) def log(self, level, msg, *args, **kwargs): """Log the message provided at the appropriate level.""" diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index ccc55691ee1b74..9822b7c63c2124 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -48,7 +48,7 @@ class SafeLineLoader(yaml.SafeLoader): def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node: """Annotate a node with the first line it was seen.""" last_line: int = self.line - node: yaml.nodes.Node = super(SafeLineLoader, self).compose_node(parent, index) + node: yaml.nodes.Node = super().compose_node(parent, index) node.__line__ = last_line + 1 # type: ignore return node diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py index ecf904ac9c9f41..74c8a99b51fcf7 100644 --- a/tests/components/smhi/common.py +++ b/tests/components/smhi/common.py @@ -8,4 +8,4 @@ class AsyncMock(Mock): # pylint: disable=W0235 async def __call__(self, *args, **kwargs): """Hack for async support for Mock.""" - return super(AsyncMock, self).__call__(*args, **kwargs) + return super().__call__(*args, **kwargs) From 24c8db0121fee8dd5f2a0ad9e04bfa2f116b5d1d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 25 Sep 2019 00:32:16 +0000 Subject: [PATCH 0444/3953] [ci skip] Translation update --- .../binary_sensor/.translations/sl.json | 92 ++++++++++++++ .../components/izone/.translations/sl.json | 15 +++ .../moon/.translations/sensor.no.json | 2 +- .../components/plex/.translations/ko.json | 2 +- .../components/plex/.translations/ru.json | 2 +- .../components/plex/.translations/sl.json | 45 +++++++ .../components/zha/.translations/en.json | 116 +++++++++--------- .../components/zha/.translations/no.json | 21 ++++ 8 files changed, 234 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/sl.json create mode 100644 homeassistant/components/izone/.translations/sl.json create mode 100644 homeassistant/components/plex/.translations/sl.json diff --git a/homeassistant/components/binary_sensor/.translations/sl.json b/homeassistant/components/binary_sensor/.translations/sl.json new file mode 100644 index 00000000000000..6b4e144d9a6ac2 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/sl.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} ima prazno baterijo", + "is_cold": "{entity_name} je hladen", + "is_connected": "{entity_name} je povezan", + "is_gas": "{entity_name} zaznava plin", + "is_hot": "{entity_name} je vro\u010d", + "is_light": "{entity_name} zaznava svetlobo", + "is_locked": "{entity_name} je zaklenjen", + "is_moist": "{entity_name} je vla\u017een", + "is_motion": "{entity_name} zaznava gibanje", + "is_moving": "{entity_name} se premika", + "is_no_gas": "{entity_name} ne zaznava plina", + "is_no_light": "{entity_name} ne zaznava svetlobe", + "is_no_motion": "{entity_name} ne zaznava gibanja", + "is_no_problem": "{entity_name} ne zaznava te\u017eav", + "is_no_smoke": "{entity_name} ne zaznava dima", + "is_no_sound": "{entity_name} ne zaznava zvoka", + "is_no_vibration": "{entity_name} ne zazna vibracij", + "is_not_bat_low": "{entity_name} baterija je polna", + "is_not_cold": "{entity_name} ni hladen", + "is_not_connected": "{entity_name} ni povezan", + "is_not_hot": "{entity_name} ni vro\u010d", + "is_not_locked": "{entity_name} je odklenjen", + "is_not_moist": "{entity_name} je suh", + "is_not_moving": "{entity_name} se ne premika", + "is_not_occupied": "{entity_name} ni zaseden", + "is_not_open": "{entity_name} je zaprt", + "is_not_plugged_in": "{entity_name} je odklopljen", + "is_not_powered": "{entity_name} ni napajan", + "is_not_present": "{entity_name} ni prisoten", + "is_not_unsafe": "{entity_name} je varen", + "is_occupied": "{entity_name} je zaseden", + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen", + "is_open": "{entity_name} je odprt", + "is_plugged_in": "{entity_name} je priklju\u010den", + "is_powered": "{entity_name} je vklopljen", + "is_present": "{entity_name} je prisoten", + "is_problem": "{entity_name} zaznava te\u017eavo", + "is_smoke": "{entity_name} zaznava dim", + "is_sound": "{entity_name} zaznava zvok", + "is_unsafe": "{entity_name} ni varen", + "is_vibration": "{entity_name} zaznava vibracije" + }, + "trigger_type": { + "bat_low": "{entity_name} ima prazno baterijo", + "closed": "{entity_name} zaprto", + "cold": "{entity_name} je postal hladen", + "connected": "{entity_name} povezan", + "gas": "{entity_name} za\u010del zaznavati plin", + "hot": "{entity_name} je postal vro\u010d", + "light": "{entity_name} za\u010del zaznavati svetlobo", + "locked": "{entity_name} zaklenjen", + "moist\u00a7": "{entity_name} postal vla\u017een", + "motion": "{entity_name} za\u010del zaznavati gibanje", + "moving": "{entity_name} se je za\u010del premikati", + "no_gas": "{entity_name} prenehal zaznavati plin", + "no_light": "{entity_name} prenehal zaznavati svetlobo", + "no_motion": "{entity_name} prenehal zaznavati gibanje", + "no_problem": "{entity_name} prenehal odkrivati te\u017eavo", + "no_smoke": "{entity_name} prenehal zaznavati dim", + "no_sound": "{entity_name} prenehal zaznavati zvok", + "no_vibration": "{entity_name} prenehal zaznavati vibracije", + "not_bat_low": "{entity_name} ima polno baterijo", + "not_cold": "{entity_name} ni ve\u010d hladen", + "not_connected": "{entity_name} prekinjen", + "not_hot": "{entity_name} ni ve\u010d vro\u010d", + "not_locked": "{entity_name} odklenjen", + "not_moist": "{entity_name} je postalo suh", + "not_moving": "{entity_name} se je prenehal premikati", + "not_occupied": "{entity_name} ni zaseden", + "not_plugged_in": "{entity_name} odklopljen", + "not_powered": "{entity_name} ni napajan", + "not_present": "{entity_name} ni prisoten", + "not_unsafe": "{entity_name} je postal varen", + "occupied": "{entity_name} postal zaseden", + "opened": "{entity_name} odprl", + "plugged_in": "{entity_name} priklju\u010den", + "powered": "{entity_name} priklopljen", + "present": "{entity_name} prisoten", + "problem": "{entity_name} za\u010del odkrivati te\u017eavo", + "smoke": "{entity_name} za\u010del zaznavati dim", + "sound": "{entity_name} za\u010del zaznavati zvok", + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen", + "unsafe": "{entity_name} je postal nevaren", + "vibration": "{entity_name} je za\u010del odkrivat vibracije" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/sl.json b/homeassistant/components/izone/.translations/sl.json new file mode 100644 index 00000000000000..cb2fe82129741d --- /dev/null +++ b/homeassistant/components/izone/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni najdenih naprav iZone.", + "single_instance_allowed": "Potrebna je samo ena konfiguracija iZone." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/.translations/sensor.no.json b/homeassistant/components/moon/.translations/sensor.no.json index a440fdde4f27ca..3998494606f8de 100644 --- a/homeassistant/components/moon/.translations/sensor.no.json +++ b/homeassistant/components/moon/.translations/sensor.no.json @@ -2,7 +2,7 @@ "state": { "first_quarter": "F\u00f8rste kvartal", "full_moon": "Fullm\u00e5ne", - "last_quarter": "Siste kvarter", + "last_quarter": "Siste kvartal", "new_moon": "Nym\u00e5ne", "waning_crescent": "Minkende m\u00e5nesigd", "waning_gibbous": "Minkende m\u00e5ne", diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json index d2610c68aed74f..171c656566d279 100644 --- a/homeassistant/components/plex/.translations/ko.json +++ b/homeassistant/components/plex/.translations/ko.json @@ -24,7 +24,7 @@ "data": { "token": "Plex \ud1a0\ud070" }, - "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ud574\uc8fc\uc138\uc694.", "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" } }, diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index b906d0d8dc6c19..b424be059f9a5c 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -36,7 +36,7 @@ "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "token": "\u0422\u043e\u043a\u0435\u043d" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d Plex \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", "title": "Plex" } }, diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json new file mode 100644 index 00000000000000..2a03b7d0e8cd3d --- /dev/null +++ b/homeassistant/components/plex/.translations/sl.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "all_configured": "Vsi povezani stre\u017eniki so \u017ee konfigurirani", + "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", + "already_in_progress": "Plex se konfigurira", + "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", + "unknown": "Ni uspelo iz neznanega razloga" + }, + "error": { + "faulty_credentials": "Avtorizacija ni uspela", + "no_servers": "Ni stre\u017enikov povezanih z ra\u010dunom", + "no_token": "Vnesite \u017eeton ali izberite ro\u010dno nastavitev", + "not_found": "Plex stre\u017enika ni mogo\u010de najti" + }, + "step": { + "manual_setup": { + "data": { + "host": "Gostitelj", + "port": "Vrata", + "ssl": "Uporaba SSL", + "token": "\u017deton (po potrebi)", + "verify_ssl": "Preverite SSL potrdilo" + }, + "title": "Plex stre\u017enik" + }, + "select_server": { + "data": { + "server": "Stre\u017enik" + }, + "description": "Na voljo je ve\u010d stre\u017enikov, izberite enega:", + "title": "Izberite stre\u017enik Plex" + }, + "user": { + "data": { + "manual_setup": "Ro\u010dna nastavitev", + "token": "Plex \u017eeton" + }, + "description": "Vnesite \u017eeton Plex za samodejno nastavitev ali ro\u010dno konfigurirajte stre\u017enik.", + "title": "Pove\u017eite stre\u017enik Plex" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/en.json b/homeassistant/components/zha/.translations/en.json index 6a819fbc16f8e2..84b335bdeaac95 100644 --- a/homeassistant/components/zha/.translations/en.json +++ b/homeassistant/components/zha/.translations/en.json @@ -1,63 +1,63 @@ { - "config": { - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." - }, - "step": { - "user": { - "data": { - "radio_type": "Radio Type", - "usb_path": "USB Device Path" + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "step": { + "user": { + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" + }, + "title": "ZHA" + } }, "title": "ZHA" - } - }, - "title": "ZHA" - }, - "device_automation": { - "trigger_type": { - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "device_rotated": "Device rotated \"{subtype}\"", - "device_shaken": "Device shaken", - "device_slid": "Device slid \"{subtype}\"", - "device_tilted": "Device tilted", - "device_knocked": "Device knocked \"{subtype}\"", - "device_dropped": "Device dropped", - "device_flipped": "Device flipped \"{subtype}\"" }, - "trigger_subtype": { - "turn_on": "Turn on", - "turn_off": "Turn off", - "dim_up": "Dim up", - "dim_down": "Dim down", - "left": "Left", - "right": "Right", - "open": "Open", - "close": "Close", - "both_buttons": "Both buttons", - "button_1": "First button", - "button_2": "Second button", - "button_3": "Third button", - "button_4": "Fourth button", - "button_5": "Fifth button", - "button_6": "Sixth button", - "face_any": "With any/specified face(s) activated", - "face_1": "With face 1 activated", - "face_2": "With face 2 activated", - "face_3": "With face 3 activated", - "face_4": "With face 4 activated", - "face_5": "With face 5 activated", - "face_6": "With face 6 activated" + "device_automation": { + "trigger_subtype": { + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "close": "Close", + "dim_down": "Dim down", + "dim_up": "Dim up", + "face_1": "with face 1 activated", + "face_2": "with face 2 activated", + "face_3": "with face 3 activated", + "face_4": "with face 4 activated", + "face_5": "with face 5 activated", + "face_6": "with face 6 activated", + "face_any": "With any/specified face(s) activated", + "left": "Left", + "open": "Open", + "right": "Right", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"", + "device_knocked": "Device knocked \"{subtype}\"", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_triple_press": "\"{subtype}\" button triple clicked" + } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 9db55494ba4ac2..f639c85c682612 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -16,5 +16,26 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Begge knapper", + "button_1": "F\u00f8rste knapp", + "button_2": "Andre knapp", + "button_3": "Tredje knapp", + "button_4": "Fjerde knapp", + "close": "Lukk", + "dim_down": "Dimm ned", + "dim_up": "Dimm opp", + "left": "Venstre", + "open": "\u00c5pen", + "right": "H\u00f8yre", + "turn_off": "Skru av", + "turn_on": "Sl\u00e5 p\u00e5" + }, + "trigger_type": { + "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", + "remote_button_short_release": "\"{subtype}\"-knappen sluppet" + } } } \ No newline at end of file From 51a090cfdc99ea2502f9fb976b8cc1b58189cce4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 Sep 2019 20:47:24 -0700 Subject: [PATCH 0445/3953] Fix CI --- requirements_test.txt | 1 + requirements_test_all.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements_test.txt b/requirements_test.txt index b9b919c4bfd077..ae4401178b1809 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,6 +13,7 @@ mypy==0.720 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 +astroid==2.2.5 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63ad27a654caa7..c92b16b78fe566 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -14,6 +14,7 @@ mypy==0.720 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 +astroid==2.2.5 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 From 31964d0f49467e99e7d9a87811c7d50ce3fda63b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 25 Sep 2019 00:01:39 -0400 Subject: [PATCH 0446/3953] bump quirks (#26885) --- 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 7744b9f223339f..f46904f326721b 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.10.0", - "zha-quirks==0.0.23", + "zha-quirks==0.0.25", "zigpy-deconz==0.4.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", diff --git a/requirements_all.txt b/requirements_all.txt index b8d4a158655af8..3a40af2b1afd6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2011,7 +2011,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.23 +zha-quirks==0.0.25 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From dc229ed56970a438fb4a24573366f18925a7d3c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Wed, 25 Sep 2019 06:02:23 +0200 Subject: [PATCH 0447/3953] Update zigpy_zigate to 0.4.0 (#26883) * zigpy-zigate==0.4.0 * zigpy-zigate==0.4.0 --- 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 f46904f326721b..c4de1d66e838b7 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.4.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", - "zigpy-zigate==0.3.1" + "zigpy-zigate==0.4.0" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index 3a40af2b1afd6e..2b9fc631994dd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2029,7 +2029,7 @@ zigpy-homeassistant==0.9.0 zigpy-xbee-homeassistant==0.5.0 # homeassistant.components.zha -zigpy-zigate==0.3.1 +zigpy-zigate==0.4.0 # homeassistant.components.zoneminder zm-py==0.3.3 From 2ffbe5b99f5565aeade75276994fb537a8f48039 Mon Sep 17 00:00:00 2001 From: tleegaard Date: Wed, 25 Sep 2019 06:16:08 +0200 Subject: [PATCH 0448/3953] Inverting states for opening/closing Homekit covers (#26872) * Update cover.py * Update test_cover.py --- homeassistant/components/homekit_controller/cover.py | 2 +- tests/components/homekit_controller/test_cover.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 7f70b0cfac0f92..0606778acb5768 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -33,7 +33,7 @@ TARGET_GARAGE_STATE_MAP = {STATE_OPEN: 0, STATE_CLOSED: 1, STATE_STOPPED: 2} -CURRENT_WINDOW_STATE_MAP = {0: STATE_OPENING, 1: STATE_CLOSING, 2: STATE_STOPPED} +CURRENT_WINDOW_STATE_MAP = {0: STATE_CLOSING, 1: STATE_OPENING, 2: STATE_STOPPED} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/tests/components/homekit_controller/test_cover.py b/tests/components/homekit_controller/test_cover.py index afbb55e03f3230..53245176a04e34 100644 --- a/tests/components/homekit_controller/test_cover.py +++ b/tests/components/homekit_controller/test_cover.py @@ -93,11 +93,11 @@ async def test_read_window_cover_state(hass, utcnow): helper.characteristics[POSITION_STATE].value = 0 state = await helper.poll_and_get_state() - assert state.state == "opening" + assert state.state == "closing" helper.characteristics[POSITION_STATE].value = 1 state = await helper.poll_and_get_state() - assert state.state == "closing" + assert state.state == "opening" helper.characteristics[POSITION_STATE].value = 2 state = await helper.poll_and_get_state() From 87f1f6cc9fde6b1628dae3073e615e1aa8ebff4d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Sep 2019 06:28:50 +0200 Subject: [PATCH 0449/3953] Removes unnecessary utf8 source encoding declarations (#26887) --- homeassistant/components/lcn/const.py | 1 - homeassistant/components/yandex_transport/sensor.py | 1 - homeassistant/const.py | 1 - 3 files changed, 3 deletions(-) diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py index ecef388c0d4aa5..c49319abf420cf 100644 --- a/homeassistant/components/lcn/const.py +++ b/homeassistant/components/lcn/const.py @@ -1,4 +1,3 @@ -# coding: utf-8 """Constants for the LCN component.""" from itertools import product diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 340291807ead98..26311a4c72e58c 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Service for obtaining information about closer bus from Transport Yandex Service.""" import logging diff --git a/homeassistant/const.py b/homeassistant/const.py index 26cb3c20942666..9aa4544f5c34e7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,4 +1,3 @@ -# coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 From 1f03508bfe92602267933fa2afcdb9f936fafbc0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Sep 2019 06:29:57 +0200 Subject: [PATCH 0450/3953] Removes unnecessary print_function future import (#26888) --- homeassistant/__main__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index f7e24d69884975..b416b1f98d3d1b 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,6 +1,4 @@ """Start Home Assistant.""" -from __future__ import print_function - import argparse import os import platform From cd976b65ae2dba92556da0211563972f34c99645 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Wed, 25 Sep 2019 14:30:48 +1000 Subject: [PATCH 0451/3953] Add availability_template to Template Switch platform (#26513) * Added availability_template to Template Switch platform * Fixed Entity discovery big and coverage * flake8 * Cleaned template setup * I'll remember to run black every time one of these days... * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Refactored availability_tempalte rendering to common loop * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (async, magic values and state checks) * Fixed Enity Extraction --- homeassistant/components/template/switch.py | 73 +++++++++++++++----- tests/components/template/test_switch.py | 75 ++++++++++++++++++++- 2 files changed, 130 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index c77a90c1f8b6ee..2d4dda032ca857 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -19,12 +19,14 @@ ATTR_ENTITY_ID, CONF_SWITCHES, EVENT_HOMEASSISTANT_START, + MATCH_ALL, ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] @@ -37,6 +39,7 @@ 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_AVAILABILITY_TEMPLATE): cv.template, vol.Required(ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, @@ -58,19 +61,47 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config[CONF_VALUE_TEMPLATE] icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[ON_ACTION] off_action = device_config[OFF_ACTION] - entity_ids = ( - device_config.get(ATTR_ENTITY_ID) or state_template.extract_entities() - ) - - state_template.hass = hass - - if icon_template is not None: - icon_template.hass = hass - - if entity_picture_template is not None: - entity_picture_template.hass = hass + manual_entity_ids = device_config.get(ATTR_ENTITY_ID) + entity_ids = set() + + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + } + invalid_templates = [] + + for template_name, template in templates.items(): + if template is not None: + template.hass = hass + + if manual_entity_ids is not None: + continue + + template_entity_ids = template.extract_entities() + if template_entity_ids == MATCH_ALL: + invalid_templates.append(template_name.replace("_template", "")) + entity_ids = MATCH_ALL + elif entity_ids != MATCH_ALL: + entity_ids |= set(template_entity_ids) + if invalid_templates: + _LOGGER.warning( + "Template sensor %s has no entity ids configured to track nor" + " were we able to extract the entities to track from the %s " + "template(s). This entity will only be able to be updated " + "manually.", + device, + ", ".join(invalid_templates), + ) + else: + if manual_entity_ids is None: + entity_ids = list(entity_ids) + else: + entity_ids = manual_entity_ids switches.append( SwitchTemplate( @@ -80,6 +111,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, entity_ids, @@ -104,6 +136,7 @@ def __init__( state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, entity_ids, @@ -120,9 +153,11 @@ def __init__( self._state = False self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._icon = None self._entity_picture = None self._entities = entity_ids + self._available = True async def async_added_to_hass(self): """Register callbacks.""" @@ -160,11 +195,6 @@ def should_poll(self): """Return the polling state.""" return False - @property - def available(self): - """If switch is available.""" - return self._state is not None - @property def icon(self): """Return the icon to use in the frontend, if any.""" @@ -175,6 +205,11 @@ def entity_picture(self): """Return the entity_picture to use in the frontend, if any.""" return self._entity_picture + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_turn_on(self, **kwargs): """Fire the on action.""" await self._on_script.async_run(context=self._context) @@ -205,12 +240,16 @@ async def async_update(self): for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), + ("_available", self._availability_template), ): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index 9a07a935d12546..3adc5dcad46db9 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -1,7 +1,7 @@ """The tests for the Template switch platform.""" from homeassistant.core import callback from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component from tests.components.switch import common @@ -474,3 +474,76 @@ def test_off_action(self): self.hass.block_till_done() assert len(self.calls) == 1 + + +async def test_available_template_with_entities(hass): + """Test availability templates with values from other entities.""" + await setup.async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "template", + "switches": { + "test_template_switch": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "turn_off": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + "availability_template": "{{ is_state('availability_state.state', 'on') }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_template_switch").state != STATE_UNAVAILABLE + + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_template_switch").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + await setup.async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "template", + "switches": { + "test_template_switch": { + "value_template": "{{ true }}", + "turn_on": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "turn_off": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + "availability_template": "{{ x - 12 }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("switch.test_template_switch").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text From aaf013da6e99be41ad63f584162f22936cc304d2 Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Wed, 25 Sep 2019 15:20:46 +0700 Subject: [PATCH 0452/3953] Bump ndms2-client to 0.0.9 (#26899) --- homeassistant/components/keenetic_ndms2/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 42d8d89a021152..417616161e56ea 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,7 +3,7 @@ "name": "Keenetic ndms2", "documentation": "https://www.home-assistant.io/components/keenetic_ndms2", "requirements": [ - "ndms2_client==0.0.8" + "ndms2_client==0.0.9" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 2b9fc631994dd1..2f8ae35125d807 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -841,7 +841,7 @@ n26==0.2.7 nad_receiver==0.0.11 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.0.8 +ndms2_client==0.0.9 # homeassistant.components.ness_alarm nessclient==0.9.15 From f5018e91b586381a0035c17330faa25cd3ba1d7b Mon Sep 17 00:00:00 2001 From: zhumuht <40521367+zhumuht@users.noreply.github.com> Date: Wed, 25 Sep 2019 21:04:27 +0800 Subject: [PATCH 0453/3953] Add voltage attribute to Xiaomi Aqara devices (#26876) --- homeassistant/components/xiaomi_aqara/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 6e2298e05b9782..26d0f351fa298b 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -8,6 +8,7 @@ from homeassistant.components.discovery import SERVICE_XIAOMI_GW from homeassistant.const import ( ATTR_BATTERY_LEVEL, + ATTR_VOLTAGE, CONF_HOST, CONF_MAC, CONF_PORT, @@ -323,6 +324,7 @@ def parse_voltage(self, data): max_volt = 3300 min_volt = 2800 voltage = data[voltage_key] + self._device_state_attributes[ATTR_VOLTAGE] = round(voltage/1000.0, 2) voltage = min(voltage, max_volt) voltage = max(voltage, min_volt) percent = ((voltage - min_volt) / (max_volt - min_volt)) * 100 From 626b61b58f97bf3b7b15009d110e0dcf28c33e31 Mon Sep 17 00:00:00 2001 From: zhumuht <40521367+zhumuht@users.noreply.github.com> Date: Wed, 25 Sep 2019 21:39:01 +0800 Subject: [PATCH 0454/3953] Fix bed_activity history chart of the Xiaomi Aqara vibration sensor (#26875) --- homeassistant/components/xiaomi_aqara/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index b3f29e93c639ce..5ad29af0aaf399 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -19,6 +19,7 @@ "illumination": ["lm", None, DEVICE_CLASS_ILLUMINANCE], "lux": ["lx", None, DEVICE_CLASS_ILLUMINANCE], "pressure": ["hPa", None, DEVICE_CLASS_PRESSURE], + "bed_activity": ["μm", None, None], } From cc611615aa4e5bd34c5514ab7f7484e2a5f11a47 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 25 Sep 2019 09:04:34 -0700 Subject: [PATCH 0455/3953] Fix missing whitespace around arithmetic operator (#26908) --- homeassistant/components/xiaomi_aqara/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 26d0f351fa298b..7a337dcc497a81 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -324,7 +324,7 @@ def parse_voltage(self, data): max_volt = 3300 min_volt = 2800 voltage = data[voltage_key] - self._device_state_attributes[ATTR_VOLTAGE] = round(voltage/1000.0, 2) + self._device_state_attributes[ATTR_VOLTAGE] = round(voltage / 1000.0, 2) voltage = min(voltage, max_volt) voltage = max(voltage, min_volt) percent = ((voltage - min_volt) / (max_volt - min_volt)) * 100 From 4582b6e668b4d347fe1d96ed1d3d454243840299 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 25 Sep 2019 18:56:31 +0200 Subject: [PATCH 0456/3953] deCONZ - Improve ssdp discovery by storing uuid in config entry (#26882) * Improve ssdp discovery by storing uuid in config entry so discovery can update any deconz entry, loaded or not --- homeassistant/components/deconz/__init__.py | 18 ++++++++---- .../components/deconz/config_flow.py | 17 +++++------ homeassistant/components/deconz/const.py | 4 ++- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_config_flow.py | 22 ++++++++------ tests/components/deconz/test_gateway.py | 3 +- tests/components/deconz/test_init.py | 29 ++++++++++--------- 9 files changed, 57 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 558b0fe4205147..0ea91d10b191bd 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -4,8 +4,8 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DOMAIN -from .gateway import DeconzGateway +from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, CONF_UUID, DOMAIN +from .gateway import DeconzGateway, get_gateway_from_config_entry from .services import async_setup_services, async_unload_services CONFIG_SCHEMA = vol.Schema( @@ -39,6 +39,9 @@ async def async_setup_entry(hass, config_entry): await gateway.async_update_device_registry() + if CONF_UUID not in config_entry.data: + await async_add_uuid_to_config_entry(hass, config_entry) + await async_setup_services(hass) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) @@ -68,11 +71,14 @@ async def async_update_master_gateway(hass, config_entry): Makes sure there is always one master available. """ master = not get_master_gateway(hass) + options = {**config_entry.options, CONF_MASTER_GATEWAY: master} - old_options = dict(config_entry.options) + hass.config_entries.async_update_entry(config_entry, options=options) - new_options = {CONF_MASTER_GATEWAY: master} - options = {**old_options, **new_options} +async def async_add_uuid_to_config_entry(hass, config_entry): + """Add UUID to config entry to help discovery identify entries.""" + gateway = get_gateway_from_config_entry(hass, config_entry) + config = {**config_entry.data, CONF_UUID: gateway.api.config.uuid} - hass.config_entries.async_update_entry(config_entry, options=options) + hass.config_entries.async_update_entry(config_entry, data=config) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index c63b1721393222..488d48bb7409d8 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -5,7 +5,7 @@ import voluptuous as vol from pydeconz.errors import ResponseError, RequestError -from pydeconz.utils import async_discovery, async_get_api_key, async_get_bridgeid +from pydeconz.utils import async_discovery, async_get_api_key, async_get_gateway_config from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT @@ -16,6 +16,7 @@ CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, + CONF_UUID, DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, @@ -144,9 +145,11 @@ async def _create_entry(self): try: with async_timeout.timeout(10): - self.deconz_config[CONF_BRIDGEID] = await async_get_bridgeid( + gateway_config = await async_get_gateway_config( session, **self.deconz_config ) + self.deconz_config[CONF_BRIDGEID] = gateway_config.bridgeid + self.deconz_config[CONF_UUID] = gateway_config.uuid except asyncio.TimeoutError: return self.async_abort(reason="no_bridges") @@ -172,14 +175,10 @@ async def async_step_ssdp(self, discovery_info): return self.async_abort(reason="not_deconz_bridge") uuid = discovery_info[ATTR_UUID].replace("uuid:", "") - gateways = { - gateway.api.config.uuid: gateway - for gateway in self.hass.data.get(DOMAIN, {}).values() - } - if uuid in gateways: - entry = gateways[uuid].config_entry - return await self._update_entry(entry, discovery_info[CONF_HOST]) + for entry in self.hass.config_entries.async_entries(DOMAIN): + if uuid == entry.data.get(CONF_UUID): + return await self._update_entry(entry, discovery_info[CONF_HOST]) bridgeid = discovery_info[ATTR_SERIAL] if any( diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 62879a82724b03..ad23a564272d9c 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -5,13 +5,15 @@ DOMAIN = "deconz" +CONF_BRIDGEID = "bridgeid" +CONF_UUID = "uuid" + DEFAULT_PORT = 80 DEFAULT_ALLOW_CLIP_SENSOR = False DEFAULT_ALLOW_DECONZ_GROUPS = True CONF_ALLOW_CLIP_SENSOR = "allow_clip_sensor" CONF_ALLOW_DECONZ_GROUPS = "allow_deconz_groups" -CONF_BRIDGEID = "bridgeid" CONF_MASTER_GATEWAY = "master" SUPPORTED_PLATFORMS = [ diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index fe8f49d260a93f..4aec29008dee80 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/deconz", "requirements": [ - "pydeconz==62" + "pydeconz==63" ], "ssdp": { "manufacturer": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2f8ae35125d807..73ececb0517b6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1129,7 +1129,7 @@ pydaikin==1.6.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==62 +pydeconz==63 # homeassistant.components.delijn pydelijn==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c92b16b78fe566..6f542147363915 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -294,7 +294,7 @@ pyblackbird==0.5 pychromecast==4.0.1 # homeassistant.components.deconz -pydeconz==62 +pydeconz==63 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d7071d6daef08c..4d8d3a312582a1 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -209,22 +209,25 @@ async def test_bridge_discovery_update_existing_entry(hass): """Test if a discovered bridge has already been configured.""" entry = MockConfigEntry( domain=config_flow.DOMAIN, - data={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_BRIDGEID: "id"}, + data={ + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_BRIDGEID: "123ABC", + config_flow.CONF_UUID: "456DEF", + }, ) entry.add_to_hass(hass) gateway = Mock() gateway.config_entry = entry - gateway.api.config.uuid = "1234" - hass.data[config_flow.DOMAIN] = {"id": gateway} + hass.data[config_flow.DOMAIN] = {"123ABC": gateway} result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ config_flow.CONF_HOST: "mock-deconz", - ATTR_SERIAL: "id", + ATTR_SERIAL: "123ABC", ATTR_MANUFACTURERURL: config_flow.DECONZ_MANUFACTURERURL, - config_flow.ATTR_UUID: "uuid:1234", + config_flow.ATTR_UUID: "uuid:456DEF", }, context={"source": "ssdp"}, ) @@ -238,7 +241,7 @@ async def test_create_entry(hass, aioclient_mock): """Test that _create_entry work and that bridgeid can be requested.""" aioclient_mock.get( "http://1.2.3.4:80/api/1234567890ABCDEF/config", - json={"bridgeid": "id"}, + json={"bridgeid": "123ABC", "uuid": "456DEF"}, headers={"content-type": "application/json"}, ) @@ -253,12 +256,13 @@ async def test_create_entry(hass, aioclient_mock): result = await flow._create_entry() assert result["type"] == "create_entry" - assert result["title"] == "deCONZ-id" + assert result["title"] == "deCONZ-123ABC" assert result["data"] == { - config_flow.CONF_BRIDGEID: "id", + config_flow.CONF_BRIDGEID: "123ABC", config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80, config_flow.CONF_API_KEY: "1234567890ABCDEF", + config_flow.CONF_UUID: "456DEF", } @@ -273,7 +277,7 @@ async def test_create_entry_timeout(hass, aioclient_mock): } with patch( - "homeassistant.components.deconz.config_flow.async_get_bridgeid", + "homeassistant.components.deconz.config_flow.async_get_gateway_config", side_effect=asyncio.TimeoutError, ): result = await flow._create_entry() diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 25a1cd465c510a..b98681b6fc9697 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -20,6 +20,7 @@ deconz.config_flow.CONF_BRIDGEID: BRIDGEID, deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, + deconz.config_flow.CONF_UUID: "456DEF", } DECONZ_CONFIG = { @@ -147,7 +148,7 @@ async def test_update_address(hass): deconz.config_flow.CONF_PORT: 80, ssdp.ATTR_SERIAL: BRIDGEID, ssdp.ATTR_MANUFACTURERURL: deconz.config_flow.DECONZ_MANUFACTURERURL, - deconz.config_flow.ATTR_UUID: "uuid:1234", + deconz.config_flow.ATTR_UUID: "uuid:456DEF", }, context={"source": "ssdp"}, ) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 7d630498cde14e..986e01a1599bc9 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,33 +1,34 @@ """Test deCONZ component setup process.""" -from unittest.mock import Mock, patch - import asyncio + +from asynctest import Mock, patch + import pytest from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components import deconz -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry ENTRY1_HOST = "1.2.3.4" ENTRY1_PORT = 80 ENTRY1_API_KEY = "1234567890ABCDEF" ENTRY1_BRIDGEID = "12345ABC" +ENTRY1_UUID = "456DEF" ENTRY2_HOST = "2.3.4.5" ENTRY2_PORT = 80 ENTRY2_API_KEY = "1234567890ABCDEF" ENTRY2_BRIDGEID = "23456DEF" +ENTRY2_UUID = "789ACE" async def setup_entry(hass, entry): """Test that setup entry works.""" with patch.object( - deconz.DeconzGateway, "async_setup", return_value=mock_coro(True) + deconz.DeconzGateway, "async_setup", return_value=True ), patch.object( - deconz.DeconzGateway, - "async_update_device_registry", - return_value=mock_coro(True), + deconz.DeconzGateway, "async_update_device_registry", return_value=True ): assert await deconz.async_setup_entry(hass, entry) is True @@ -67,6 +68,7 @@ async def test_setup_entry_successful(hass): deconz.config_flow.CONF_PORT: ENTRY1_PORT, deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, + deconz.CONF_UUID: ENTRY1_UUID, }, ) entry.add_to_hass(hass) @@ -86,6 +88,7 @@ async def test_setup_entry_multiple_gateways(hass): deconz.config_flow.CONF_PORT: ENTRY1_PORT, deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, + deconz.CONF_UUID: ENTRY1_UUID, }, ) entry.add_to_hass(hass) @@ -97,6 +100,7 @@ async def test_setup_entry_multiple_gateways(hass): deconz.config_flow.CONF_PORT: ENTRY2_PORT, deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY, deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID, + deconz.CONF_UUID: ENTRY2_UUID, }, ) entry2.add_to_hass(hass) @@ -119,15 +123,14 @@ async def test_unload_entry(hass): deconz.config_flow.CONF_PORT: ENTRY1_PORT, deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, + deconz.CONF_UUID: ENTRY1_UUID, }, ) entry.add_to_hass(hass) await setup_entry(hass, entry) - with patch.object( - deconz.DeconzGateway, "async_reset", return_value=mock_coro(True) - ): + with patch.object(deconz.DeconzGateway, "async_reset", return_value=True): assert await deconz.async_unload_entry(hass, entry) assert not hass.data[deconz.DOMAIN] @@ -142,6 +145,7 @@ async def test_unload_entry_multiple_gateways(hass): deconz.config_flow.CONF_PORT: ENTRY1_PORT, deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, + deconz.CONF_UUID: ENTRY1_UUID, }, ) entry.add_to_hass(hass) @@ -153,6 +157,7 @@ async def test_unload_entry_multiple_gateways(hass): deconz.config_flow.CONF_PORT: ENTRY2_PORT, deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY, deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID, + deconz.CONF_UUID: ENTRY2_UUID, }, ) entry2.add_to_hass(hass) @@ -160,9 +165,7 @@ async def test_unload_entry_multiple_gateways(hass): await setup_entry(hass, entry) await setup_entry(hass, entry2) - with patch.object( - deconz.DeconzGateway, "async_reset", return_value=mock_coro(True) - ): + with patch.object(deconz.DeconzGateway, "async_reset", return_value=True): assert await deconz.async_unload_entry(hass, entry) assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN] From d1adb28c6b1f08e61f07fbb4b45addcc8ee3152f Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Wed, 25 Sep 2019 20:13:31 +0300 Subject: [PATCH 0457/3953] Add google_assistant alarm_control_panel (#26249) * add alarm_control_panel to google_assistant * add cancel arming option * raise error if requested state is same as current * rework executing command logic * Add tests * fixed tests * fixed level synonyms --- .../components/google_assistant/const.py | 7 + .../components/google_assistant/trait.py | 112 +++++- tests/components/google_assistant/__init__.py | 7 + .../google_assistant/test_google_assistant.py | 18 +- .../components/google_assistant/test_trait.py | 337 +++++++++++++++++- 5 files changed, 478 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 1d266d23d3fccd..54abd54caaffc6 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -15,6 +15,7 @@ sensor, switch, vacuum, + alarm_control_panel, ) DOMAIN = "google_assistant" @@ -48,6 +49,7 @@ "lock", "binary_sensor", "sensor", + "alarm_control_panel", ] PREFIX_TYPES = "action.devices.types." @@ -66,6 +68,7 @@ TYPE_DOOR = PREFIX_TYPES + "DOOR" TYPE_TV = PREFIX_TYPES + "TV" TYPE_SPEAKER = PREFIX_TYPES + "SPEAKER" +TYPE_ALARM = PREFIX_TYPES + "SECURITYSYSTEM" SERVICE_REQUEST_SYNC = "request_sync" HOMEGRAPH_URL = "https://homegraph.googleapis.com/" @@ -81,6 +84,9 @@ ERR_UNKNOWN_ERROR = "unknownError" ERR_FUNCTION_NOT_SUPPORTED = "functionNotSupported" +ERR_ALREADY_DISARMED = "alreadyDisarmed" +ERR_ALREADY_ARMED = "alreadyArmed" + ERR_CHALLENGE_NEEDED = "challengeNeeded" ERR_CHALLENGE_NOT_SETUP = "challengeFailedNotSetup" ERR_TOO_MANY_FAILED_ATTEMPTS = "tooManyFailedAttempts" @@ -106,6 +112,7 @@ script.DOMAIN: TYPE_SCENE, switch.DOMAIN: TYPE_SWITCH, vacuum.DOMAIN: TYPE_VACUUM, + alarm_control_panel.DOMAIN: TYPE_ALARM, } DEVICE_CLASS_TO_GOOGLE_TYPES = { diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 2afa18af32e6a7..7d6e79a82372be 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -16,6 +16,7 @@ sensor, switch, vacuum, + alarm_control_panel, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( @@ -31,6 +32,20 @@ ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ATTR_ASSUMED_STATE, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_TRIGGER, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + STATE_ALARM_PENDING, + ATTR_CODE, STATE_UNKNOWN, ) from homeassistant.core import DOMAIN as HA_DOMAIN @@ -43,6 +58,8 @@ CHALLENGE_ACK_NEEDED, CHALLENGE_PIN_NEEDED, CHALLENGE_FAILED_PIN_NEEDED, + ERR_ALREADY_DISARMED, + ERR_ALREADY_ARMED, ) from .error import SmartHomeError, ChallengeNeeded @@ -62,6 +79,7 @@ TRAIT_MODES = PREFIX_TRAITS + "Modes" TRAIT_OPENCLOSE = PREFIX_TRAITS + "OpenClose" TRAIT_VOLUME = PREFIX_TRAITS + "Volume" +TRAIT_ARMDISARM = PREFIX_TRAITS + "ArmDisarm" PREFIX_COMMANDS = "action.devices.commands." COMMAND_ONOFF = PREFIX_COMMANDS + "OnOff" @@ -85,6 +103,7 @@ COMMAND_OPENCLOSE = PREFIX_COMMANDS + "OpenClose" COMMAND_SET_VOLUME = PREFIX_COMMANDS + "setVolume" COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + "volumeRelative" +COMMAND_ARMDISARM = PREFIX_COMMANDS + "ArmDisarm" TRAITS = [] @@ -873,6 +892,98 @@ async def execute(self, command, data, params, challenge): ) +@register_trait +class ArmDisArmTrait(_Trait): + """Trait to Arm or Disarm a Security System. + + https://developers.google.com/actions/smarthome/traits/armdisarm + """ + + name = TRAIT_ARMDISARM + commands = [COMMAND_ARMDISARM] + + state_to_service = { + STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME, + STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY, + STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT, + STATE_ALARM_ARMED_CUSTOM_BYPASS: SERVICE_ALARM_ARM_CUSTOM_BYPASS, + STATE_ALARM_TRIGGERED: SERVICE_ALARM_TRIGGER, + } + + @staticmethod + def supported(domain, features, device_class): + """Test if state is supported.""" + return domain == alarm_control_panel.DOMAIN + + @staticmethod + def might_2fa(domain, features, device_class): + """Return if the trait might ask for 2FA.""" + return True + + def sync_attributes(self): + """Return ArmDisarm attributes for a sync request.""" + response = {} + levels = [] + for state in self.state_to_service: + # level synonyms are generated from state names + # 'armed_away' becomes 'armed away' or 'away' + level_synonym = [state.replace("_", " ")] + if state != STATE_ALARM_TRIGGERED: + level_synonym.append(state.split("_")[1]) + + level = { + "level_name": state, + "level_values": [{"level_synonym": level_synonym, "lang": "en"}], + } + levels.append(level) + response["availableArmLevels"] = {"levels": levels, "ordered": False} + return response + + def query_attributes(self): + """Return ArmDisarm query attributes.""" + if "post_pending_state" in self.state.attributes: + armed_state = self.state.attributes["post_pending_state"] + else: + armed_state = self.state.state + response = {"isArmed": armed_state in self.state_to_service} + if response["isArmed"]: + response.update({"currentArmLevel": armed_state}) + return response + + async def execute(self, command, data, params, challenge): + """Execute an ArmDisarm command.""" + if params["arm"] and not params.get("cancel"): + if self.state.state == params["armLevel"]: + raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed") + if self.state.attributes["code_arm_required"]: + _verify_pin_challenge(data, self.state, challenge) + service = self.state_to_service[params["armLevel"]] + # disarm the system without asking for code when + # 'cancel' arming action is received while current status is pending + elif ( + params["arm"] + and params.get("cancel") + and self.state.state == STATE_ALARM_PENDING + ): + service = SERVICE_ALARM_DISARM + else: + if self.state.state == STATE_ALARM_DISARMED: + raise SmartHomeError(ERR_ALREADY_DISARMED, "System is already disarmed") + _verify_pin_challenge(data, self.state, challenge) + service = SERVICE_ALARM_DISARM + + await self.hass.services.async_call( + alarm_control_panel.DOMAIN, + service, + { + ATTR_ENTITY_ID: self.state.entity_id, + ATTR_CODE: data.config.secure_devices_pin, + }, + blocking=True, + context=data.context, + ) + + @register_trait class FanSpeedTrait(_Trait): """Trait to control speed of Fan. @@ -1343,7 +1454,6 @@ def _verify_pin_challenge(data, state, challenge): """Verify a pin challenge.""" if not data.config.should_2fa(state): return - if not data.config.secure_devices_pin: raise SmartHomeError(ERR_CHALLENGE_NOT_SETUP, "Challenge is not set up") diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index ccb74e88e37329..12de2eaba1cdc9 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -230,4 +230,11 @@ def should_expose(self, state): "type": "action.devices.types.LOCK", "willReportState": False, }, + { + "id": "alarm_control_panel.alarm", + "name": {"name": "Alarm"}, + "traits": ["action.devices.traits.ArmDisarm"], + "type": "action.devices.types.SECURITYSYSTEM", + "willReportState": False, + }, ] diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 6a7b69daabb43e..6473e8964b874c 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -7,7 +7,15 @@ import pytest from homeassistant import core, const, setup -from homeassistant.components import fan, cover, light, switch, lock, media_player +from homeassistant.components import ( + fan, + cover, + light, + switch, + lock, + media_player, + alarm_control_panel, +) from homeassistant.components.climate import const as climate from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.components import google_assistant as ga @@ -98,6 +106,14 @@ def hass_fixture(loop, hass): setup.async_setup_component(hass, lock.DOMAIN, {"lock": [{"platform": "demo"}]}) ) + loop.run_until_complete( + setup.async_setup_component( + hass, + alarm_control_panel.DOMAIN, + {"alarm_control_panel": [{"platform": "demo"}]}, + ) + ) + return hass diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 0288aa87572e96..a5c527dacfe6e2 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,6 +1,6 @@ """Tests for the Google Assistant traits.""" from unittest.mock import patch, Mock - +import logging import pytest from homeassistant.components import ( @@ -18,12 +18,16 @@ switch, vacuum, group, + alarm_control_panel, ) from homeassistant.components.climate import const as climate from homeassistant.components.google_assistant import trait, helpers, const, error from homeassistant.const import ( STATE_ON, STATE_OFF, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, @@ -40,6 +44,7 @@ from tests.common import async_mock_service, mock_coro from . import BASIC_CONFIG, MockConfig +_LOGGER = logging.getLogger(__name__) REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" @@ -816,6 +821,336 @@ async def test_lock_unlock_unlock(hass): assert len(calls) == 2 +async def test_arm_disarm_arm_away(hass): + """Test ArmDisarm trait Arming support for alarm_control_panel domain.""" + assert helpers.get_google_type(alarm_control_panel.DOMAIN, None) is not None + assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None) + assert trait.ArmDisArmTrait.might_2fa(alarm_control_panel.DOMAIN, 0, None) + + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + assert trt.sync_attributes() == { + "availableArmLevels": { + "levels": [ + { + "level_name": "armed_home", + "level_values": [ + {"level_synonym": ["armed home", "home"], "lang": "en"} + ], + }, + { + "level_name": "armed_away", + "level_values": [ + {"level_synonym": ["armed away", "away"], "lang": "en"} + ], + }, + { + "level_name": "armed_night", + "level_values": [ + {"level_synonym": ["armed night", "night"], "lang": "en"} + ], + }, + { + "level_name": "armed_custom_bypass", + "level_values": [ + { + "level_synonym": ["armed custom bypass", "custom"], + "lang": "en", + } + ], + }, + { + "level_name": "triggered", + "level_values": [{"level_synonym": ["triggered"], "lang": "en"}], + }, + ], + "ordered": False, + } + } + + assert trt.query_attributes() == { + "isArmed": True, + "currentArmLevel": STATE_ALARM_ARMED_AWAY, + } + + assert trt.can_execute( + trait.COMMAND_ARMDISARM, {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY} + ) + + calls = async_mock_service( + hass, alarm_control_panel.DOMAIN, alarm_control_panel.SERVICE_ALARM_ARM_AWAY + ) + + # Test with no secure_pin configured + + with pytest.raises(error.SmartHomeError) as err: + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + BASIC_CONFIG, + ) + await trt.execute( + trait.COMMAND_ARMDISARM, + BASIC_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {}, + ) + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP + + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + # No challenge data + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_ARMDISARM, + PIN_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {}, + ) + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NEEDED + assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED + + # invalid pin + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_ARMDISARM, + PIN_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {"pin": 9999}, + ) + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NEEDED + assert err.value.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED + + # correct pin + await trt.execute( + trait.COMMAND_ARMDISARM, + PIN_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {"pin": "1234"}, + ) + + assert len(calls) == 1 + + # Test already armed + with pytest.raises(error.SmartHomeError) as err: + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + await trt.execute( + trait.COMMAND_ARMDISARM, + PIN_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {}, + ) + assert len(calls) == 1 + assert err.value.code == const.ERR_ALREADY_ARMED + + # Test with code_arm_required False + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False}, + ), + PIN_CONFIG, + ) + await trt.execute( + trait.COMMAND_ARMDISARM, + PIN_DATA, + {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}, + {}, + ) + assert len(calls) == 2 + + +async def test_arm_disarm_disarm(hass): + """Test ArmDisarm trait Disarming support for alarm_control_panel domain.""" + assert helpers.get_google_type(alarm_control_panel.DOMAIN, None) is not None + assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None) + assert trait.ArmDisArmTrait.might_2fa(alarm_control_panel.DOMAIN, 0, None) + + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + assert trt.sync_attributes() == { + "availableArmLevels": { + "levels": [ + { + "level_name": "armed_home", + "level_values": [ + {"level_synonym": ["armed home", "home"], "lang": "en"} + ], + }, + { + "level_name": "armed_away", + "level_values": [ + {"level_synonym": ["armed away", "away"], "lang": "en"} + ], + }, + { + "level_name": "armed_night", + "level_values": [ + {"level_synonym": ["armed night", "night"], "lang": "en"} + ], + }, + { + "level_name": "armed_custom_bypass", + "level_values": [ + { + "level_synonym": ["armed custom bypass", "custom"], + "lang": "en", + } + ], + }, + { + "level_name": "triggered", + "level_values": [{"level_synonym": ["triggered"], "lang": "en"}], + }, + ], + "ordered": False, + } + } + + assert trt.query_attributes() == {"isArmed": False} + + assert trt.can_execute(trait.COMMAND_ARMDISARM, {"arm": False}) + + calls = async_mock_service( + hass, alarm_control_panel.DOMAIN, alarm_control_panel.SERVICE_ALARM_DISARM + ) + + # Test without secure_pin configured + with pytest.raises(error.SmartHomeError) as err: + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + BASIC_CONFIG, + ) + await trt.execute(trait.COMMAND_ARMDISARM, BASIC_DATA, {"arm": False}, {}) + + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP + + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + + # No challenge data + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute(trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {}) + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NEEDED + assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED + + # invalid pin + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {"pin": 9999} + ) + assert len(calls) == 0 + assert err.value.code == const.ERR_CHALLENGE_NEEDED + assert err.value.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED + + # correct pin + await trt.execute( + trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {"pin": "1234"} + ) + + assert len(calls) == 1 + + # Test already disarmed + with pytest.raises(error.SmartHomeError) as err: + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_DISARMED, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, + ), + PIN_CONFIG, + ) + await trt.execute(trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {}) + assert len(calls) == 1 + assert err.value.code == const.ERR_ALREADY_DISARMED + + # Cancel arming after already armed will require pin + with pytest.raises(error.SmartHomeError) as err: + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_ARMED_AWAY, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False}, + ), + PIN_CONFIG, + ) + await trt.execute( + trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": True, "cancel": True}, {} + ) + assert len(calls) == 1 + assert err.value.code == const.ERR_CHALLENGE_NEEDED + assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED + + # Cancel arming while pending to arm doesn't require pin + trt = trait.ArmDisArmTrait( + hass, + State( + "alarm_control_panel.alarm", + STATE_ALARM_PENDING, + {alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False}, + ), + PIN_CONFIG, + ) + await trt.execute( + trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": True, "cancel": True}, {} + ) + assert len(calls) == 2 + + async def test_fan_speed(hass): """Test FanSpeed trait speed control support for fan domain.""" assert helpers.get_google_type(fan.DOMAIN, None) is not None From 7566a38f017a4cf1c19d1476033af2121b125908 Mon Sep 17 00:00:00 2001 From: MajestyIV Date: Mon, 23 Sep 2019 14:39:10 +0200 Subject: [PATCH 0458/3953] HM-CC-TC was not recognized (#26623) * HM-CC-TC was not recognized * guard instead of exception --- homeassistant/components/homematic/climate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 1a2f642f91ccc5..935ebb9b497620 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -94,7 +94,9 @@ def preset_mode(self): if self._data.get("BOOST_MODE", False): return "boost" - # Get the name of the mode + if not self._hm_control_mode: + return None + mode = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][self._hm_control_mode] mode = mode.lower() @@ -177,8 +179,9 @@ def _hm_control_mode(self): """Return Control mode.""" if HMIP_CONTROL_MODE in self._data: return self._data[HMIP_CONTROL_MODE] + # Homematic - return self._data["CONTROL_MODE"] + return self._data.get("CONTROL_MODE") def _init_data_struct(self): """Generate a data dict (self._data) from the Homematic metadata.""" From 4bab1612a38a3eb0865e2aa699d0c6854cf968f5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Sep 2019 10:28:20 -0700 Subject: [PATCH 0459/3953] Bumped version to 0.99.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8441d9bb9e07a0..3e4bf1154ba738 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 99 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 36f604f79d5ae38f289899fbb600079b43aced72 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Wed, 25 Sep 2019 11:26:15 -0700 Subject: [PATCH 0460/3953] Add call direction sensor for Obihai (#26867) * Add call direction sensor for obihai * Check user credentials * Review comments * Fix return --- homeassistant/components/obihai/manifest.json | 2 +- homeassistant/components/obihai/sensor.py | 17 +++++++++++++++++ requirements_all.txt | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index e7706b0435ceec..dd4df479af431c 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -3,7 +3,7 @@ "name": "Obihai", "documentation": "https://www.home-assistant.io/components/obihai", "requirements": [ - "pyobihai==1.1.0" + "pyobihai==1.1.1" ], "dependencies": [], "codeowners": ["@dshokouhi"] diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 4eb3881e95bf37..fbf4fffb17f807 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -46,16 +46,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): pyobihai = PyObihai() + login = pyobihai.check_account(host, username, password) + if not login: + _LOGGER.error("Invalid credentials") + return + services = pyobihai.get_state(host, username, password) line_services = pyobihai.get_line_state(host, username, password) + call_direction = pyobihai.get_call_direction(host, username, password) + for key in services: sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) for key in line_services: sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + for key in call_direction: + sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + add_entities(sensors) @@ -102,3 +112,10 @@ def update(self): if self._service_name in services: self._state = services.get(self._service_name) + + call_direction = self._pyobihai.get_call_direction( + self._host, self._username, self._password + ) + + if self._service_name in call_direction: + self._state = call_direction.get(self._service_name) diff --git a/requirements_all.txt b/requirements_all.txt index 73ececb0517b6d..26fdfc38fa3565 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1346,7 +1346,7 @@ pynx584==0.4 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.1.0 +pyobihai==1.1.1 # homeassistant.components.ombi pyombi==0.1.5 From cff7fd0ef3bb314455455b0fca07420a8c9c2fb6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 25 Sep 2019 21:50:14 +0200 Subject: [PATCH 0461/3953] deCONZ - Increase bridge discovery robustness in config flow (#26911) --- .../components/deconz/config_flow.py | 2 +- tests/components/deconz/test_config_flow.py | 35 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 488d48bb7409d8..66df687047f209 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -90,7 +90,7 @@ async def async_step_user(self, user_input=None): with async_timeout.timeout(10): self.bridges = await async_discovery(session) - except asyncio.TimeoutError: + except (asyncio.TimeoutError, ResponseError): self.bridges = [] if len(self.bridges) == 1: diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 4d8d3a312582a1..d0423c394a6975 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -134,7 +134,9 @@ async def test_user_step_two_bridges_selection(hass, aioclient_mock): assert flow.deconz_config[config_flow.CONF_HOST] == "1.2.3.4" -async def test_user_step_manual_configuration(hass, aioclient_mock): +async def test_user_step_manual_configuration_no_bridges_discovered( + hass, aioclient_mock +): """Test config flow with manual input.""" aioclient_mock.get( pydeconz.utils.URL_DISCOVER, @@ -148,6 +150,7 @@ async def test_user_step_manual_configuration(hass, aioclient_mock): assert result["type"] == "form" assert result["step_id"] == "init" + assert not hass.config_entries.flow._progress[result["flow_id"]].bridges result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -158,6 +161,36 @@ async def test_user_step_manual_configuration(hass, aioclient_mock): assert result["step_id"] == "link" +async def test_user_step_manual_configuration_after_timeout(hass): + """Test config flow with manual input.""" + with patch( + "homeassistant.components.deconz.config_flow.async_discovery", + side_effect=asyncio.TimeoutError, + ): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "init" + assert not hass.config_entries.flow._progress[result["flow_id"]].bridges + + +async def test_user_step_manual_configuration_after_ResponseError(hass): + """Test config flow with manual input.""" + with patch( + "homeassistant.components.deconz.config_flow.async_discovery", + side_effect=config_flow.ResponseError, + ): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "init" + assert not hass.config_entries.flow._progress[result["flow_id"]].bridges + + async def test_link_no_api_key(hass): """Test config flow should abort if no API key was possible to retrieve.""" flow = config_flow.DeconzFlowHandler() From f6995b8d17ca116d994e123b19180af77d9c1eb4 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Wed, 25 Sep 2019 16:38:21 -0400 Subject: [PATCH 0462/3953] Add config flow to ecobee (#26634) * Add basic config flow * Fix json files * Update __init__.py * Fix json errors * Move constants to const.py * Add ecobee to generated config flows * Update config_flow for updated API * Update manifest to include new dependencies Bump pyecobee, add aiofiles. * Update constants for ecobee * Modify ecobee setup to use config flow * Bump dependency * Update binary_sensor to use config_entry * Update sensor to use config_entry * Update __init__.py * Update weather to use config_entry * Update notify.py * Update ecobee constants * Update climate to use config_entry * Avoid a breaking change on ecobee services * Store api key from old config entry * Allow unloading of config entry * Show user a form before import * Refine import flow * Update strings.json to remove import step Not needed. * Move third party imports to top of module * Remove periods from end of log messages * Make configuration.yaml config optional * Remove unused strings * Reorganize config flow * Remove unneeded requirement * No need to store API key * Update async_unload_entry * Clean up if/else statements * Update requirements_all.txt * Fix config schema * Update __init__.py * Remove check for DATA_ECOBEE_CONFIG * Remove redundant check * Add check for DATA_ECOBEE_CONFIG * Change setup_platform to async * Fix state unknown and imports * Change init step to user * Have import step raise specific exceptions * Rearrange try/except block in import flow * Convert update() and refresh() to coroutines ...and update platforms to use async_update coroutine. * Finish converting init to async * Preliminary tests * Test full implementation * Update test_config_flow.py * Update test_config_flow.py * Add self to codeowners * Update test_config_flow.py * Use MockConfigEntry * Update test_config_flow.py * Update CODEOWNERS * pylint fixes * Register services under ecobee domain Breaking change! * Pylint fixes * Pylint fixes * Pylint fixes * Move service strings to ecobee domain * Fix log message capitalization * Fix import formatting * Update .coveragerc * Add __init__ to coveragerc * Add option flow test * Update .coveragerc * Act on updated options * Revert "Act on updated options" This reverts commit 56b0a859f2e3e80b6f4c77a8f784a2b29ee2cce9. * Remove hold_temp from climate * Remove hold_temp and options from init * Remove options handler from config flow * Remove options strings * Remove options flow test * Remove hold_temp constants * Fix climate tests * Pass api key to user step in import flow * Update test_config_flow.py Ensure that the import step calls the user step with the user's api key as user input if importing from ecobee.conf/validating imported keys fails. --- .coveragerc | 7 +- CODEOWNERS | 1 + .../components/climate/services.yaml | 20 -- .../components/ecobee/.translations/en.json | 23 ++ homeassistant/components/ecobee/__init__.py | 191 ++++++++-------- .../components/ecobee/binary_sensor.py | 38 ++-- homeassistant/components/ecobee/climate.py | 51 ++--- .../components/ecobee/config_flow.py | 120 ++++++++++ homeassistant/components/ecobee/const.py | 12 + homeassistant/components/ecobee/manifest.json | 15 +- homeassistant/components/ecobee/notify.py | 17 +- homeassistant/components/ecobee/sensor.py | 43 ++-- homeassistant/components/ecobee/services.yaml | 19 ++ homeassistant/components/ecobee/strings.json | 23 ++ homeassistant/components/ecobee/weather.py | 46 ++-- homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/ecobee/test_climate.py | 2 +- tests/components/ecobee/test_config_flow.py | 206 ++++++++++++++++++ 21 files changed, 623 insertions(+), 218 deletions(-) create mode 100644 homeassistant/components/ecobee/.translations/en.json create mode 100644 homeassistant/components/ecobee/config_flow.py create mode 100644 homeassistant/components/ecobee/const.py create mode 100644 homeassistant/components/ecobee/strings.json create mode 100644 tests/components/ecobee/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index a4d6d0d201e980..a8932f54a54725 100644 --- a/.coveragerc +++ b/.coveragerc @@ -156,7 +156,12 @@ omit = homeassistant/components/ebox/sensor.py homeassistant/components/ebusd/* homeassistant/components/ecoal_boiler/* - homeassistant/components/ecobee/* + homeassistant/components/ecobee/__init__.py + homeassistant/components/ecobee/binary_sensor.py + homeassistant/components/ecobee/climate.py + homeassistant/components/ecobee/notify.py + homeassistant/components/ecobee/sensor.py + homeassistant/components/ecobee/weather.py homeassistant/components/econet/water_heater.py homeassistant/components/ecovacs/* homeassistant/components/eddystone_temperature/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 7e05cdf0b399e7..cb9180d717daa8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -73,6 +73,7 @@ homeassistant/components/digital_ocean/* @fabaff homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 homeassistant/components/dweet/* @fabaff +homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 4e9a4a3a4f46f5..f10e1b4bd69d71 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -72,26 +72,6 @@ set_swing_mode: swing_mode: description: New value of swing mode. -ecobee_set_fan_min_on_time: - description: Set the minimum fan on time. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.kitchen' - fan_min_on_time: - description: New value of fan min on time. - example: 5 - -ecobee_resume_program: - description: Resume the programmed schedule. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.kitchen' - resume_all: - description: Resume all events and return to the scheduled program. This default to false which removes only the top event. - example: true - mill_set_room_temperature: description: Set Mill room temperatures. fields: diff --git a/homeassistant/components/ecobee/.translations/en.json b/homeassistant/components/ecobee/.translations/en.json new file mode 100644 index 00000000000000..9e7e9fed39659a --- /dev/null +++ b/homeassistant/components/ecobee/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "ecobee", + "step": { + "user": { + "title": "ecobee API key", + "description": "Please enter the API key obtained from ecobee.com.", + "data": {"api_key": "API Key"} + }, + "authorize": { + "title": "Authorize app on ecobee.com", + "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit." + } + }, + "error": { + "pin_request_failed": "Error requesting PIN from ecobee; please verify API key is correct.", + "token_request_failed": "Error requesting tokens from ecobee; please try again." + }, + "abort": { + "one_instance_only": "This integration currently supports only one ecobee instance." + } + } +} diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index cb8b7436b51089..eb65a7ed4269bc 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -1,123 +1,130 @@ -"""Support for Ecobee devices.""" -import logging -import os +"""Support for ecobee.""" +import asyncio from datetime import timedelta - import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery +from pyecobee import Ecobee, ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, ExpiredTokenError + +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY +from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle -from homeassistant.util.json import save_json - -_CONFIGURING = {} -_LOGGER = logging.getLogger(__name__) - -CONF_HOLD_TEMP = "hold_temp" -DOMAIN = "ecobee" - -ECOBEE_CONFIG_FILE = "ecobee.conf" +from .const import ( + CONF_REFRESH_TOKEN, + DATA_ECOBEE_CONFIG, + DOMAIN, + ECOBEE_PLATFORMS, + _LOGGER, +) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) -NETWORK = None - CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_HOLD_TEMP, default=False): cv.boolean, - } - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.Schema({vol.Optional(CONF_API_KEY): cv.string})}, extra=vol.ALLOW_EXTRA ) -def request_configuration(network, hass, config): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - if "ecobee" in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING["ecobee"], "Failed to register, please try again." - ) - - return - - def ecobee_configuration_callback(callback_data): - """Handle configuration callbacks.""" - network.request_tokens() - network.update() - setup_ecobee(hass, network, config) - - _CONFIGURING["ecobee"] = configurator.request_config( - "Ecobee", - ecobee_configuration_callback, - description=( - "Please authorize this app at https://www.ecobee.com/consumer" - "portal/index.html with pin code: " + network.pin - ), - description_image="/static/images/config_ecobee_thermostat.png", - submit_caption="I have authorized the app.", - ) +async def async_setup(hass, config): + """ + Ecobee uses config flow for configuration. + But, an "ecobee:" entry in configuration.yaml will trigger an import flow + if a config entry doesn't already exist. If ecobee.conf exists, the import + flow will attempt to import it and create a config entry, to assist users + migrating from the old ecobee component. Otherwise, the user will have to + continue setting up the integration via the config flow. + """ + hass.data[DATA_ECOBEE_CONFIG] = config.get(DOMAIN, {}) + + if not hass.config_entries.async_entries(DOMAIN) and hass.data[DATA_ECOBEE_CONFIG]: + # No config entry exists and configuration.yaml config exists, trigger the import flow. + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT} + ) + ) -def setup_ecobee(hass, network, config): - """Set up the Ecobee thermostat.""" - # If ecobee has a PIN then it needs to be configured. - if network.pin is not None: - request_configuration(network, hass, config) - return + return True - if "ecobee" in _CONFIGURING: - configurator = hass.components.configurator - configurator.request_done(_CONFIGURING.pop("ecobee")) - hold_temp = config[DOMAIN].get(CONF_HOLD_TEMP) +async def async_setup_entry(hass, entry): + """Set up ecobee via a config entry.""" + api_key = entry.data[CONF_API_KEY] + refresh_token = entry.data[CONF_REFRESH_TOKEN] - discovery.load_platform(hass, "climate", DOMAIN, {"hold_temp": hold_temp}, config) - discovery.load_platform(hass, "sensor", DOMAIN, {}, config) - discovery.load_platform(hass, "binary_sensor", DOMAIN, {}, config) - discovery.load_platform(hass, "weather", DOMAIN, {}, config) + data = EcobeeData(hass, entry, api_key=api_key, refresh_token=refresh_token) + if not await data.refresh(): + return False -class EcobeeData: - """Get the latest data and update the states.""" + await data.update() - def __init__(self, config_file): - """Init the Ecobee data object.""" - from pyecobee import Ecobee + if data.ecobee.thermostats is None: + _LOGGER.error("No ecobee devices found to set up") + return False - self.ecobee = Ecobee(config_file) + hass.data[DOMAIN] = data - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Get the latest data from pyecobee.""" - self.ecobee.update() - _LOGGER.debug("Ecobee data updated successfully") + for component in ECOBEE_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + return True -def setup(hass, config): - """Set up the Ecobee. - Will automatically load thermostat and sensor components to support - devices discovered on the network. +class EcobeeData: """ - global NETWORK + Handle getting the latest data from ecobee.com so platforms can use it. - if "ecobee" in _CONFIGURING: - return - - # Create ecobee.conf if it doesn't exist - if not os.path.isfile(hass.config.path(ECOBEE_CONFIG_FILE)): - jsonconfig = {"API_KEY": config[DOMAIN].get(CONF_API_KEY)} - save_json(hass.config.path(ECOBEE_CONFIG_FILE), jsonconfig) + Also handle refreshing tokens and updating config entry with refreshed tokens. + """ - NETWORK = EcobeeData(hass.config.path(ECOBEE_CONFIG_FILE)) + def __init__(self, hass, entry, api_key, refresh_token): + """Initialize the Ecobee data object.""" + self._hass = hass + self._entry = entry + self.ecobee = Ecobee( + config={ECOBEE_API_KEY: api_key, ECOBEE_REFRESH_TOKEN: refresh_token} + ) - setup_ecobee(hass, NETWORK.ecobee, config) + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def update(self): + """Get the latest data from ecobee.com.""" + try: + await self._hass.async_add_executor_job(self.ecobee.update) + _LOGGER.debug("Updating ecobee") + except ExpiredTokenError: + _LOGGER.warning( + "Ecobee update failed; attempting to refresh expired tokens" + ) + await self.refresh() + + async def refresh(self) -> bool: + """Refresh ecobee tokens and update config entry.""" + _LOGGER.debug("Refreshing ecobee tokens and updating config entry") + if await self._hass.async_add_executor_job(self.ecobee.refresh_tokens): + self._hass.config_entries.async_update_entry( + self._entry, + data={ + CONF_API_KEY: self.ecobee.config[ECOBEE_API_KEY], + CONF_REFRESH_TOKEN: self.ecobee.config[ECOBEE_REFRESH_TOKEN], + }, + ) + return True + _LOGGER.error("Error updating ecobee tokens") + return False + + +async def async_unload_entry(hass, config_entry): + """Unload the config entry and platforms.""" + hass.data.pop(DOMAIN) + + tasks = [] + for platform in ECOBEE_PLATFORMS: + tasks.append( + hass.config_entries.async_forward_entry_unload(config_entry, platform) + ) - return True + return all(await asyncio.gather(*tasks)) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index a3cd49ff45871e..8b7b819cfc7141 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -1,15 +1,20 @@ """Support for Ecobee binary sensors.""" -from homeassistant.components import ecobee -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_OCCUPANCY, +) -ECOBEE_CONFIG_FILE = "ecobee.conf" +from .const import DOMAIN -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee sensors.""" - if discovery_info is None: - return - data = ecobee.NETWORK +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up ecobee binary sensors.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up ecobee binary (occupancy) sensors.""" + data = hass.data[DOMAIN] dev = list() for index in range(len(data.ecobee.thermostats)): for sensor in data.ecobee.get_remote_sensors(index): @@ -17,21 +22,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if item["type"] != "occupancy": continue - dev.append(EcobeeBinarySensor(sensor["name"], index)) + dev.append(EcobeeBinarySensor(data, sensor["name"], index)) - add_entities(dev, True) + async_add_entities(dev, True) class EcobeeBinarySensor(BinarySensorDevice): """Representation of an Ecobee sensor.""" - def __init__(self, sensor_name, sensor_index): + def __init__(self, data, sensor_name, sensor_index): """Initialize the Ecobee sensor.""" + self.data = data self._name = sensor_name + " Occupancy" self.sensor_name = sensor_name self.index = sensor_index self._state = None - self._device_class = "occupancy" @property def name(self): @@ -46,13 +51,12 @@ def is_on(self): @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - return self._device_class + return DEVICE_CLASS_OCCUPANCY - def update(self): + async def async_update(self): """Get the latest state of the sensor.""" - data = ecobee.NETWORK - data.update() - for sensor in data.ecobee.get_remote_sensors(self.index): + await self.data.update() + for sensor in self.data.ecobee.get_remote_sensors(self.index): for item in sensor["capability"]: if item["type"] == "occupancy" and self.sensor_name == sensor["name"]: self._state = item["value"] diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 181f1561eba0ca..9eb8e8f26bc6ef 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -1,14 +1,11 @@ """Support for Ecobee Thermostats.""" import collections -import logging from typing import Optional import voluptuous as vol -from homeassistant.components import ecobee from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - DOMAIN, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_AUTO, @@ -38,8 +35,7 @@ ) import homeassistant.helpers.config_validation as cv -_CONFIGURING = {} -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, _LOGGER ATTR_FAN_MIN_ON_TIME = "fan_min_on_time" ATTR_RESUME_ALL = "resume_all" @@ -88,8 +84,8 @@ PRESET_HOLD_INDEFINITE: "indefinite", } -SERVICE_SET_FAN_MIN_ON_TIME = "ecobee_set_fan_min_on_time" -SERVICE_RESUME_PROGRAM = "ecobee_resume_program" +SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time" +SERVICE_RESUME_PROGRAM = "resume_program" SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema( { @@ -114,20 +110,19 @@ ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee Thermostat Platform.""" - if discovery_info is None: - return - data = ecobee.NETWORK - hold_temp = discovery_info["hold_temp"] - _LOGGER.info( - "Loading ecobee thermostat component with hold_temp set to %s", hold_temp - ) - devices = [ - Thermostat(data, index, hold_temp) - for index in range(len(data.ecobee.thermostats)) - ] - add_entities(devices) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up ecobee thermostat.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the ecobee thermostat.""" + + data = hass.data[DOMAIN] + + devices = [Thermostat(data, index) for index in range(len(data.ecobee.thermostats))] + + async_add_entities(devices, True) def fan_min_on_time_set_service(service): """Set the minimum fan on time on the target thermostats.""" @@ -163,14 +158,14 @@ def resume_program_set_service(service): thermostat.schedule_update_ha_state(True) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, fan_min_on_time_set_service, schema=SET_FAN_MIN_ON_TIME_SCHEMA, ) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, @@ -181,13 +176,12 @@ def resume_program_set_service(service): class Thermostat(ClimateDevice): """A thermostat class for Ecobee.""" - def __init__(self, data, thermostat_index, hold_temp): + def __init__(self, data, thermostat_index): """Initialize the thermostat.""" self.data = data self.thermostat_index = thermostat_index self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) self._name = self.thermostat["name"] - self.hold_temp = hold_temp self.vacation = None self._operation_list = [] @@ -206,14 +200,13 @@ def __init__(self, data, thermostat_index, hold_temp): self._fan_modes = [FAN_AUTO, FAN_ON] self.update_without_throttle = False - def update(self): + async def async_update(self): """Get the latest state from the thermostat.""" if self.update_without_throttle: - self.data.update(no_throttle=True) + await self.data.update(no_throttle=True) self.update_without_throttle = False else: - self.data.update() - + await self.data.update() self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) @property diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py new file mode 100644 index 00000000000000..f4cd4fc5bf086d --- /dev/null +++ b/homeassistant/components/ecobee/config_flow.py @@ -0,0 +1,120 @@ +"""Config flow to configure ecobee.""" +import voluptuous as vol + +from pyecobee import ( + Ecobee, + ECOBEE_CONFIG_FILENAME, + ECOBEE_API_KEY, + ECOBEE_REFRESH_TOKEN, +) + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistantError +from homeassistant.util.json import load_json + +from .const import CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, _LOGGER + + +class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle an ecobee config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize the ecobee flow.""" + self._ecobee = None + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if self._async_current_entries(): + # Config entry already exists, only one allowed. + return self.async_abort(reason="one_instance_only") + + errors = {} + stored_api_key = self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + + if user_input is not None: + # Use the user-supplied API key to attempt to obtain a PIN from ecobee. + self._ecobee = Ecobee(config={ECOBEE_API_KEY: user_input[CONF_API_KEY]}) + + if await self.hass.async_add_executor_job(self._ecobee.request_pin): + # We have a PIN; move to the next step of the flow. + return await self.async_step_authorize() + errors["base"] = "pin_request_failed" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_API_KEY, default=stored_api_key): str} + ), + errors=errors, + ) + + async def async_step_authorize(self, user_input=None): + """Present the user with the PIN so that the app can be authorized on ecobee.com.""" + errors = {} + + if user_input is not None: + # Attempt to obtain tokens from ecobee and finish the flow. + if await self.hass.async_add_executor_job(self._ecobee.request_tokens): + # Refresh token obtained; create the config entry. + config = { + CONF_API_KEY: self._ecobee.api_key, + CONF_REFRESH_TOKEN: self._ecobee.refresh_token, + } + return self.async_create_entry(title=DOMAIN, data=config) + errors["base"] = "token_request_failed" + + return self.async_show_form( + step_id="authorize", + errors=errors, + description_placeholders={"pin": self._ecobee.pin}, + ) + + async def async_step_import(self, import_data): + """ + Import ecobee config from configuration.yaml. + + Triggered by async_setup only if a config entry doesn't already exist. + If ecobee.conf exists, we will attempt to validate the credentials + and create an entry if valid. Otherwise, we will delegate to the user + step so that the user can continue the config flow. + """ + try: + legacy_config = await self.hass.async_add_executor_job( + load_json, self.hass.config.path(ECOBEE_CONFIG_FILENAME) + ) + config = { + ECOBEE_API_KEY: legacy_config[ECOBEE_API_KEY], + ECOBEE_REFRESH_TOKEN: legacy_config[ECOBEE_REFRESH_TOKEN], + } + except (HomeAssistantError, KeyError): + _LOGGER.debug( + "No valid ecobee.conf configuration found for import, delegating to user step" + ) + return await self.async_step_user( + user_input={ + CONF_API_KEY: self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + } + ) + + ecobee = Ecobee(config=config) + if await self.hass.async_add_executor_job(ecobee.refresh_tokens): + # Credentials found and validated; create the entry. + _LOGGER.debug( + "Valid ecobee configuration found for import, creating config entry" + ) + return self.async_create_entry( + title=DOMAIN, + data={ + CONF_API_KEY: ecobee.api_key, + CONF_REFRESH_TOKEN: ecobee.refresh_token, + }, + ) + return await self.async_step_user( + user_input={ + CONF_API_KEY: self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + } + ) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py new file mode 100644 index 00000000000000..c3a23099b8abcd --- /dev/null +++ b/homeassistant/components/ecobee/const.py @@ -0,0 +1,12 @@ +"""Constants for the ecobee integration.""" +import logging + +_LOGGER = logging.getLogger(__package__) + +DOMAIN = "ecobee" +DATA_ECOBEE_CONFIG = "ecobee_config" + +CONF_INDEX = "index" +CONF_REFRESH_TOKEN = "refresh_token" + +ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 31cca1e676fafd..092594c41fc0d4 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -1,10 +1,9 @@ { - "domain": "ecobee", - "name": "Ecobee", - "documentation": "https://www.home-assistant.io/components/ecobee", - "requirements": [ - "python-ecobee-api==0.0.21" - ], - "dependencies": ["configurator"], - "codeowners": [] + "domain": "ecobee", + "name": "Ecobee", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/ecobee", + "dependencies": [], + "requirements": ["python-ecobee-api==0.1.2"], + "codeowners": ["@marthoc"] } diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index bb6861a1492a97..c7b3f47d29c17f 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -1,15 +1,10 @@ """Support for Ecobee Send Message service.""" -import logging - import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components import ecobee from homeassistant.components.notify import BaseNotificationService, PLATFORM_SCHEMA -_LOGGER = logging.getLogger(__name__) - -CONF_INDEX = "index" +from .const import CONF_INDEX, DOMAIN PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_INDEX, default=0): cv.positive_int} @@ -18,17 +13,19 @@ def get_service(hass, config, discovery_info=None): """Get the Ecobee notification service.""" + data = hass.data[DOMAIN] index = config.get(CONF_INDEX) - return EcobeeNotificationService(index) + return EcobeeNotificationService(data, index) class EcobeeNotificationService(BaseNotificationService): """Implement the notification service for the Ecobee thermostat.""" - def __init__(self, thermostat_index): + def __init__(self, data, thermostat_index): """Initialize the service.""" + self.data = data self.thermostat_index = thermostat_index def send_message(self, message="", **kwargs): - """Send a message to a command line.""" - ecobee.NETWORK.ecobee.send_message(self.thermostat_index, message) + """Send a message.""" + self.data.ecobee.send_message(self.thermostat_index, message) diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index d21f937dd2062f..e62d68dc9bcfcd 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -1,5 +1,6 @@ """Support for Ecobee sensors.""" -from homeassistant.components import ecobee +from pyecobee.const import ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN + from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -7,7 +8,7 @@ ) from homeassistant.helpers.entity import Entity -ECOBEE_CONFIG_FILE = "ecobee.conf" +from .const import DOMAIN SENSOR_TYPES = { "temperature": ["Temperature", TEMP_FAHRENHEIT], @@ -15,11 +16,14 @@ } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee sensors.""" - if discovery_info is None: - return - data = ecobee.NETWORK +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up ecobee sensors.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up ecobee (temperature and humidity) sensors.""" + data = hass.data[DOMAIN] dev = list() for index in range(len(data.ecobee.thermostats)): for sensor in data.ecobee.get_remote_sensors(index): @@ -27,16 +31,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if item["type"] not in ("temperature", "humidity"): continue - dev.append(EcobeeSensor(sensor["name"], item["type"], index)) + dev.append(EcobeeSensor(data, sensor["name"], item["type"], index)) - add_entities(dev, True) + async_add_entities(dev, True) class EcobeeSensor(Entity): """Representation of an Ecobee sensor.""" - def __init__(self, sensor_name, sensor_type, sensor_index): + def __init__(self, data, sensor_name, sensor_type, sensor_index): """Initialize the sensor.""" + self.data = data self._name = "{} {}".format(sensor_name, SENSOR_TYPES[sensor_type][0]) self.sensor_name = sensor_name self.type = sensor_type @@ -59,6 +64,12 @@ def device_class(self): @property def state(self): """Return the state of the sensor.""" + if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN]: + return None + + if self.type == "temperature": + return float(self._state) / 10 + return self._state @property @@ -66,14 +77,10 @@ def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement - def update(self): + async def async_update(self): """Get the latest state of the sensor.""" - data = ecobee.NETWORK - data.update() - for sensor in data.ecobee.get_remote_sensors(self.index): + await self.data.update() + for sensor in self.data.ecobee.get_remote_sensors(self.index): for item in sensor["capability"]: if item["type"] == self.type and self.sensor_name == sensor["name"]: - if self.type == "temperature" and item["value"] != "unknown": - self._state = float(item["value"]) / 10 - else: - self._state = item["value"] + self._state = item["value"] diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml index e69de29bb2d1d6..87eefed9eaaa43 100644 --- a/homeassistant/components/ecobee/services.yaml +++ b/homeassistant/components/ecobee/services.yaml @@ -0,0 +1,19 @@ +resume_program: + description: Resume the programmed schedule. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' + resume_all: + description: Resume all events and return to the scheduled program. This default to false which removes only the top event. + example: true + +set_fan_min_on_time: + description: Set the minimum fan on time. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' + fan_min_on_time: + description: New value of fan min on time. + example: 5 diff --git a/homeassistant/components/ecobee/strings.json b/homeassistant/components/ecobee/strings.json new file mode 100644 index 00000000000000..9e7e9fed39659a --- /dev/null +++ b/homeassistant/components/ecobee/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "ecobee", + "step": { + "user": { + "title": "ecobee API key", + "description": "Please enter the API key obtained from ecobee.com.", + "data": {"api_key": "API Key"} + }, + "authorize": { + "title": "Authorize app on ecobee.com", + "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit." + } + }, + "error": { + "pin_request_failed": "Error requesting PIN from ecobee; please verify API key is correct.", + "token_request_failed": "Error requesting tokens from ecobee; please try again." + }, + "abort": { + "one_instance_only": "This integration currently supports only one ecobee instance." + } + } +} diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index b09e06bd822dd4..dd3112b636e0c3 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -1,7 +1,8 @@ """Support for displaying weather info from Ecobee API.""" from datetime import datetime -from homeassistant.components import ecobee +from pyecobee.const import ECOBEE_STATE_UNKNOWN + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, @@ -12,33 +13,37 @@ ) from homeassistant.const import TEMP_FAHRENHEIT +from .const import DOMAIN + ATTR_FORECAST_TEMP_HIGH = "temphigh" ATTR_FORECAST_PRESSURE = "pressure" ATTR_FORECAST_VISIBILITY = "visibility" ATTR_FORECAST_HUMIDITY = "humidity" -MISSING_DATA = -5002 + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up the ecobee weather platform.""" + pass -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Ecobee weather platform.""" - if discovery_info is None: - return +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the ecobee weather platform.""" + data = hass.data[DOMAIN] dev = list() - data = ecobee.NETWORK for index in range(len(data.ecobee.thermostats)): thermostat = data.ecobee.get_thermostat(index) if "weather" in thermostat: - dev.append(EcobeeWeather(thermostat["name"], index)) + dev.append(EcobeeWeather(data, thermostat["name"], index)) - add_entities(dev, True) + async_add_entities(dev, True) class EcobeeWeather(WeatherEntity): """Representation of Ecobee weather data.""" - def __init__(self, name, index): + def __init__(self, data, name, index): """Initialize the Ecobee weather platform.""" + self.data = data self._name = name self._index = index self.weather = None @@ -140,26 +145,25 @@ def forecast(self): ATTR_FORECAST_CONDITION: day["condition"], ATTR_FORECAST_TEMP: float(day["tempHigh"]) / 10, } - if day["tempHigh"] == MISSING_DATA: + if day["tempHigh"] == ECOBEE_STATE_UNKNOWN: break - if day["tempLow"] != MISSING_DATA: + if day["tempLow"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_TEMP_LOW] = float(day["tempLow"]) / 10 - if day["pressure"] != MISSING_DATA: + if day["pressure"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_PRESSURE] = int(day["pressure"]) - if day["windSpeed"] != MISSING_DATA: + if day["windSpeed"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_WIND_SPEED] = int(day["windSpeed"]) - if day["visibility"] != MISSING_DATA: + if day["visibility"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_WIND_SPEED] = int(day["visibility"]) - if day["relativeHumidity"] != MISSING_DATA: + if day["relativeHumidity"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_HUMIDITY] = int(day["relativeHumidity"]) forecasts.append(forecast) return forecasts except (ValueError, IndexError, KeyError): return None - def update(self): - """Get the latest state of the sensor.""" - data = ecobee.NETWORK - data.update() - thermostat = data.ecobee.get_thermostat(self._index) + async def async_update(self): + """Get the latest weather data.""" + await self.data.update() + thermostat = self.data.ecobee.get_thermostat(self._index) self.weather = thermostat.get("weather", None) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9a534c01bbf2c6..b6865f9e86a16e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -15,6 +15,7 @@ "daikin", "deconz", "dialogflow", + "ecobee", "emulated_roku", "esphome", "geofency", diff --git a/requirements_all.txt b/requirements_all.txt index 26fdfc38fa3565..5ca7a7c8160bd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1483,7 +1483,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.0.21 +python-ecobee-api==0.1.2 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f542147363915..d6c0d8dbbb2905 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -355,6 +355,9 @@ pysonos==0.0.23 # homeassistant.components.spc pyspcwebgw==0.4.0 +# homeassistant.components.ecobee +python-ecobee-api==0.1.2 + # homeassistant.components.darksky python-forecastio==1.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 649c48e1b7d282..fcb265bbc97b6b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -146,6 +146,7 @@ "pysonos", "pyspcwebgw", "python_awair", + "python-ecobee-api", "python-forecastio", "python-izone", "python-nest", diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index d6c40ddf9ab389..90a9a6417765a7 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -54,7 +54,7 @@ def setUp(self): self.data = mock.Mock() self.data.ecobee.get_thermostat.return_value = self.ecobee - self.thermostat = ecobee.Thermostat(self.data, 1, False) + self.thermostat = ecobee.Thermostat(self.data, 1) def test_name(self): """Test name property.""" diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py new file mode 100644 index 00000000000000..7b4d1f96a3761c --- /dev/null +++ b/tests/components/ecobee/test_config_flow.py @@ -0,0 +1,206 @@ +"""Tests for the ecobee config flow.""" +from unittest.mock import patch + +from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN + +from homeassistant import data_entry_flow +from homeassistant.components.ecobee import config_flow +from homeassistant.components.ecobee.const import ( + CONF_REFRESH_TOKEN, + DATA_ECOBEE_CONFIG, + DOMAIN, +) +from homeassistant.const import CONF_API_KEY +from tests.common import MockConfigEntry, mock_coro + + +async def test_abort_if_already_setup(hass): + """Test we abort if ecobee is already setup.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await flow.async_step_user() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "one_instance_only" + + +async def test_user_step_without_user_input(hass): + """Test expected result if user step is called.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_pin_request_succeeds(hass): + """Test expected result if pin request succeeds.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: + mock_ecobee = MockEcobee.return_value + mock_ecobee.request_pin.return_value = True + mock_ecobee.pin = "test-pin" + + result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authorize" + assert result["description_placeholders"] == {"pin": "test-pin"} + + +async def test_pin_request_fails(hass): + """Test expected result if pin request fails.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: + mock_ecobee = MockEcobee.return_value + mock_ecobee.request_pin.return_value = False + + result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "pin_request_failed" + + +async def test_token_request_succeeds(hass): + """Test expected result if token request succeeds.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: + mock_ecobee = MockEcobee.return_value + mock_ecobee.request_tokens.return_value = True + mock_ecobee.api_key = "test-api-key" + mock_ecobee.refresh_token = "test-token" + flow._ecobee = mock_ecobee + + result = await flow.async_step_authorize(user_input={}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DOMAIN + assert result["data"] == { + CONF_API_KEY: "test-api-key", + CONF_REFRESH_TOKEN: "test-token", + } + + +async def test_token_request_fails(hass): + """Test expected result if token request fails.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + with patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: + mock_ecobee = MockEcobee.return_value + mock_ecobee.request_tokens.return_value = False + mock_ecobee.pin = "test-pin" + flow._ecobee = mock_ecobee + + result = await flow.async_step_authorize(user_input={}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authorize" + assert result["errors"]["base"] == "token_request_failed" + assert result["description_placeholders"] == {"pin": "test-pin"} + + +async def test_import_flow_triggered_but_no_ecobee_conf(hass): + """Test expected result if import flow triggers but ecobee.conf doesn't exist.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {} + + result = await flow.async_step_import(import_data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_tokens( + hass +): + """Test expected result if import flow triggers and ecobee.conf exists with valid tokens.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + + MOCK_ECOBEE_CONF = {ECOBEE_API_KEY: None, ECOBEE_REFRESH_TOKEN: None} + + with patch( + "homeassistant.components.ecobee.config_flow.load_json", + return_value=MOCK_ECOBEE_CONF, + ), patch("homeassistant.components.ecobee.config_flow.Ecobee") as MockEcobee: + mock_ecobee = MockEcobee.return_value + mock_ecobee.refresh_tokens.return_value = True + mock_ecobee.api_key = "test-api-key" + mock_ecobee.refresh_token = "test-token" + + result = await flow.async_step_import(import_data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DOMAIN + assert result["data"] == { + CONF_API_KEY: "test-api-key", + CONF_REFRESH_TOKEN: "test-token", + } + + +async def test_import_flow_triggered_with_ecobee_conf_and_invalid_data(hass): + """Test expected result if import flow triggers and ecobee.conf exists with invalid data.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {CONF_API_KEY: "test-api-key"} + + MOCK_ECOBEE_CONF = {} + + with patch( + "homeassistant.components.ecobee.config_flow.load_json", + return_value=MOCK_ECOBEE_CONF, + ), patch.object( + flow, "async_step_user", return_value=mock_coro() + ) as mock_async_step_user: + + await flow.async_step_import(import_data=None) + + mock_async_step_user.assert_called_once_with( + user_input={CONF_API_KEY: "test-api-key"} + ) + + +async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_stale_tokens( + hass +): + """Test expected result if import flow triggers and ecobee.conf exists with stale tokens.""" + flow = config_flow.EcobeeFlowHandler() + flow.hass = hass + flow.hass.data[DATA_ECOBEE_CONFIG] = {CONF_API_KEY: "test-api-key"} + + MOCK_ECOBEE_CONF = {ECOBEE_API_KEY: None, ECOBEE_REFRESH_TOKEN: None} + + with patch( + "homeassistant.components.ecobee.config_flow.load_json", + return_value=MOCK_ECOBEE_CONF, + ), patch( + "homeassistant.components.ecobee.config_flow.Ecobee" + ) as MockEcobee, patch.object( + flow, "async_step_user", return_value=mock_coro() + ) as mock_async_step_user: + mock_ecobee = MockEcobee.return_value + mock_ecobee.refresh_tokens.return_value = False + + await flow.async_step_import(import_data=None) + + mock_async_step_user.assert_called_once_with( + user_input={CONF_API_KEY: "test-api-key"} + ) From b75639d9d13ed2032df8aaa5c9ae34e0e5d1d6b6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 25 Sep 2019 23:00:18 +0200 Subject: [PATCH 0463/3953] Remove lamps and groups from ha when removed from Hue (#26881) * Remove light when removed from hue * add remove_config_entry_id * Review + bump aiohue * lint * Add tests --- homeassistant/components/hue/light.py | 43 +++++++++++++--- homeassistant/components/hue/manifest.json | 2 +- homeassistant/helpers/device_registry.py | 6 +++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/test_light.py | 56 +++++++++++++++++++++ tests/helpers/test_device_registry.py | 58 ++++++++++++++++++++++ 7 files changed, 159 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 96c749a3413e18..5a3379f71ce880 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -8,6 +8,9 @@ import aiohue import async_timeout +from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg + from homeassistant.components import hue from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -147,6 +150,7 @@ async def update_bridge(): tasks.append( async_update_items( hass, + config_entry, bridge, async_add_entities, request_update, @@ -160,6 +164,7 @@ async def update_bridge(): tasks.append( async_update_items( hass, + config_entry, bridge, async_add_entities, request_update, @@ -176,6 +181,7 @@ async def update_bridge(): async def async_update_items( hass, + config_entry, bridge, async_add_entities, request_bridge_update, @@ -204,9 +210,9 @@ async def async_update_items( _LOGGER.error("Unable to reach bridge %s (%s)", bridge.host, err) bridge.available = False - for light_id, light in current.items(): - if light_id not in progress_waiting: - light.async_schedule_update_ha_state() + for item_id, item in current.items(): + if item_id not in progress_waiting: + item.async_schedule_update_ha_state() return @@ -219,7 +225,8 @@ async def async_update_items( _LOGGER.info("Reconnected to bridge %s", bridge.host) bridge.available = True - new_lights = [] + new_items = [] + removed_items = [] for item_id in api: if item_id not in current: @@ -227,12 +234,34 @@ async def async_update_items( api[item_id], request_bridge_update, bridge, is_group ) - new_lights.append(current[item_id]) + new_items.append(current[item_id]) elif item_id not in progress_waiting: current[item_id].async_schedule_update_ha_state() - if new_lights: - async_add_entities(new_lights) + for item_id in current: + if item_id in api: + continue + + # Device is removed from Hue, so we remove it from Home Assistant + entity = current[item_id] + removed_items.append(item_id) + await entity.async_remove() + ent_registry = await get_ent_reg(hass) + if entity.entity_id in ent_registry.entities: + ent_registry.async_remove(entity.entity_id) + dev_registry = await get_dev_reg(hass) + device = dev_registry.async_get_device( + identifiers={(hue.DOMAIN, entity.unique_id)}, connections=set() + ) + dev_registry.async_update_device( + device.id, remove_config_entry_id=config_entry.entry_id + ) + + if new_items: + async_add_entities(new_items) + + for item_id in removed_items: + del current[item_id] class HueLight(Light): diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index c0c7c462f905a7..cb37dd3036f245 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/hue", "requirements": [ - "aiohue==1.9.1" + "aiohue==1.9.2" ], "ssdp": { "manufacturer": [ diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 19b4a1333b6861..456678edac779a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -157,6 +157,7 @@ def async_update_device( name_by_user=_UNDEF, new_identifiers=_UNDEF, via_device_id=_UNDEF, + remove_config_entry_id=_UNDEF, ): """Update properties of a device.""" return self._async_update_device( @@ -166,6 +167,7 @@ def async_update_device( name_by_user=name_by_user, new_identifiers=new_identifiers, via_device_id=via_device_id, + remove_config_entry_id=remove_config_entry_id, ) @callback @@ -203,6 +205,10 @@ def _async_update_device( remove_config_entry_id is not _UNDEF and remove_config_entry_id in config_entries ): + if config_entries == {remove_config_entry_id}: + self.async_remove_device(device_id) + return + config_entries = config_entries - {remove_config_entry_id} if config_entries is not old.config_entries: diff --git a/requirements_all.txt b/requirements_all.txt index 5ca7a7c8160bd1..6481137f3e6a51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -152,7 +152,7 @@ aioharmony==0.1.13 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.1 +aiohue==1.9.2 # homeassistant.components.imap aioimaplib==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d6c0d8dbbb2905..d42120c339ea27 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -62,7 +62,7 @@ aioesphomeapi==2.2.0 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.1 +aiohue==1.9.2 # homeassistant.components.notion aionotion==1.1.0 diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 1c891b9c84012b..582cc185bc81d6 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -420,6 +420,62 @@ async def test_new_light_discovered(hass, mock_bridge): assert light.state == "off" +async def test_group_removed(hass, mock_bridge): + """Test if 2nd update has removed group.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append({"1": GROUP_RESPONSE["1"]}) + + # Calling a service will trigger the updates to run + await hass.services.async_call( + "light", "turn_on", {"entity_id": "light.group_1"}, blocking=True + ) + + # 2x group update, 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 5 + assert len(hass.states.async_all()) == 2 + + group = hass.states.get("light.group_1") + assert group is not None + + removed_group = hass.states.get("light.group_2") + assert removed_group is None + + +async def test_light_removed(hass, mock_bridge): + """Test if 2nd update has removed light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 3 + + mock_bridge.mock_light_responses.clear() + mock_bridge.mock_light_responses.append({"1": LIGHT_RESPONSE.get("1")}) + + # Calling a service will trigger the updates to run + await hass.services.async_call( + "light", "turn_on", {"entity_id": "light.hue_lamp_1"}, blocking=True + ) + + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 + + light = hass.states.get("light.hue_lamp_1") + assert light is not None + + removed_light = hass.states.get("light.hue_lamp_2") + assert removed_light is None + + async def test_other_group_update(hass, mock_bridge): """Test changing one group that will impact the state of other light.""" mock_bridge.allow_groups = True diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index b854b62853c1d0..1b146e9cb129c0 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -409,6 +409,64 @@ async def test_update(registry): assert updated_entry.via_device_id == "98765B" +async def test_update_remove_config_entries(hass, registry, update_events): + """Make sure we do not get duplicate entries.""" + entry = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry2 = registry.async_get_or_create( + config_entry_id="456", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + entry3 = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "34:56:78:CD:EF:12")}, + identifiers={("bridgeid", "4567")}, + manufacturer="manufacturer", + model="model", + ) + + assert len(registry.devices) == 2 + assert entry.id == entry2.id + assert entry.id != entry3.id + assert entry2.config_entries == {"123", "456"} + + updated_entry = registry.async_update_device( + entry2.id, remove_config_entry_id="123" + ) + removed_entry = registry.async_update_device( + entry3.id, remove_config_entry_id="123" + ) + + assert updated_entry.config_entries == {"456"} + assert removed_entry is None + + removed_entry = registry.async_get_device({("bridgeid", "4567")}, set()) + + assert removed_entry is None + + await hass.async_block_till_done() + + assert len(update_events) == 5 + assert update_events[0]["action"] == "create" + assert update_events[0]["device_id"] == entry.id + assert update_events[1]["action"] == "update" + assert update_events[1]["device_id"] == entry2.id + assert update_events[2]["action"] == "create" + assert update_events[2]["device_id"] == entry3.id + assert update_events[3]["action"] == "update" + assert update_events[3]["device_id"] == entry.id + assert update_events[4]["action"] == "remove" + assert update_events[4]["device_id"] == entry3.id + + async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" with asynctest.patch( From d1b4bd22ce9d499a4fdd950694e444fdddf391d0 Mon Sep 17 00:00:00 2001 From: petewill Date: Wed, 25 Sep 2019 15:20:02 -0600 Subject: [PATCH 0464/3953] Add MySensors ACK (#26894) * Add MySensors ACK The addition of the ACK will ask sensors to respond to commands sent to them which will update the MySensors device in Home Assistant. Currently, if a default MySensors sketch is used the device will update but Home Assistant does not reflect the update and custom code has to be added to tell Home Assistant the command was received. With the ACK set to 1 in the message all this is taken care of by MySensors. * Run black --- homeassistant/components/mysensors/climate.py | 12 +++++++++--- homeassistant/components/mysensors/cover.py | 14 ++++++++++---- homeassistant/components/mysensors/light.py | 10 ++++++---- homeassistant/components/mysensors/switch.py | 16 ++++++++++++---- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index f534f96b7804ac..dc053e60de15e7 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -156,7 +156,9 @@ async def async_set_temperature(self, **kwargs): (set_req.V_HVAC_SETPOINT_COOL, high), ] for value_type, value in updates: - self.gateway.set_child_value(self.node_id, self.child_id, value_type, value) + self.gateway.set_child_value( + self.node_id, self.child_id, value_type, value, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[value_type] = value @@ -166,7 +168,7 @@ async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode + self.node_id, self.child_id, set_req.V_HVAC_SPEED, fan_mode, ack=1 ) if self.gateway.optimistic: # Optimistically assume that device has changed state @@ -176,7 +178,11 @@ async def async_set_fan_mode(self, fan_mode): async def async_set_hvac_mode(self, hvac_mode): """Set new target temperature.""" self.gateway.set_child_value( - self.node_id, self.child_id, self.value_type, DICT_HA_TO_MYS[hvac_mode] + self.node_id, + self.child_id, + self.value_type, + DICT_HA_TO_MYS[hvac_mode], + ack=1, ) if self.gateway.optimistic: # Optimistically assume that device has changed state diff --git a/homeassistant/components/mysensors/cover.py b/homeassistant/components/mysensors/cover.py index bb764e375f36c1..6c02e430ba460b 100644 --- a/homeassistant/components/mysensors/cover.py +++ b/homeassistant/components/mysensors/cover.py @@ -43,7 +43,9 @@ def current_cover_position(self): async def async_open_cover(self, **kwargs): """Move the cover up.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_UP, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_UP, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that cover has changed state. if set_req.V_DIMMER in self._values: @@ -55,7 +57,9 @@ async def async_open_cover(self, **kwargs): async def async_close_cover(self, **kwargs): """Move the cover down.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_DOWN, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_DOWN, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that cover has changed state. if set_req.V_DIMMER in self._values: @@ -69,7 +73,7 @@ async def async_set_cover_position(self, **kwargs): position = kwargs.get(ATTR_POSITION) set_req = self.gateway.const.SetReq self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_DIMMER, position + self.node_id, self.child_id, set_req.V_DIMMER, position, ack=1 ) if self.gateway.optimistic: # Optimistically assume that cover has changed state. @@ -79,4 +83,6 @@ async def async_set_cover_position(self, **kwargs): async def async_stop_cover(self, **kwargs): """Stop the device.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_STOP, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_STOP, 1, ack=1 + ) diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 3936aefab0c033..8f0d0906311c68 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -75,7 +75,9 @@ def _turn_on_light(self): if self._state: return - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_LIGHT, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1 + ) if self.gateway.optimistic: # optimistically assume that light has changed state @@ -96,7 +98,7 @@ def _turn_on_dimmer(self, **kwargs): brightness = kwargs[ATTR_BRIGHTNESS] percent = round(100 * brightness / 255) self.gateway.set_child_value( - self.node_id, self.child_id, set_req.V_DIMMER, percent + self.node_id, self.child_id, set_req.V_DIMMER, percent, ack=1 ) if self.gateway.optimistic: @@ -129,7 +131,7 @@ def _turn_on_rgb_and_w(self, hex_template, **kwargs): if len(rgb) > 3: white = rgb.pop() self.gateway.set_child_value( - self.node_id, self.child_id, self.value_type, hex_color + self.node_id, self.child_id, self.value_type, hex_color, ack=1 ) if self.gateway.optimistic: @@ -141,7 +143,7 @@ def _turn_on_rgb_and_w(self, hex_template, **kwargs): async def async_turn_off(self, **kwargs): """Turn the device off.""" value_type = self.gateway.const.SetReq.V_LIGHT - self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0) + self.gateway.set_child_value(self.node_id, self.child_id, value_type, 0, ack=1) if self.gateway.optimistic: # optimistically assume that light has changed state self._state = False diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index df429460541dce..c624aaafa343f4 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -92,7 +92,9 @@ def is_on(self): async def async_turn_on(self, **kwargs): """Turn the switch on.""" - self.gateway.set_child_value(self.node_id, self.child_id, self.value_type, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_ON @@ -100,7 +102,9 @@ async def async_turn_on(self, **kwargs): async def async_turn_off(self, **kwargs): """Turn the switch off.""" - self.gateway.set_child_value(self.node_id, self.child_id, self.value_type, 0) + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, 0, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[self.value_type] = STATE_OFF @@ -129,7 +133,9 @@ async def async_turn_on(self, **kwargs): self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, self._ir_code ) - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_LIGHT, 1) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[self.value_type] = self._ir_code @@ -141,7 +147,9 @@ async def async_turn_on(self, **kwargs): async def async_turn_off(self, **kwargs): """Turn the IR switch off.""" set_req = self.gateway.const.SetReq - self.gateway.set_child_value(self.node_id, self.child_id, set_req.V_LIGHT, 0) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1 + ) if self.gateway.optimistic: # Optimistically assume that switch has changed state self._values[set_req.V_LIGHT] = STATE_OFF From ba92d781b4ed074492e1ece96163921b92b1839f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 26 Sep 2019 00:32:13 +0000 Subject: [PATCH 0465/3953] [ci skip] Translation update --- .../arcam_fmj/.translations/bg.json | 5 + .../binary_sensor/.translations/bg.json | 92 +++++++++++++++++++ .../binary_sensor/.translations/da.json | 65 +++++++++++++ .../binary_sensor/.translations/lb.json | 92 +++++++++++++++++++ .../binary_sensor/.translations/pl.json | 21 +++++ .../cert_expiry/.translations/bg.json | 24 +++++ .../components/deconz/.translations/bg.json | 18 ++++ .../components/deconz/.translations/lb.json | 4 +- .../components/ecobee/.translations/bg.json | 25 +++++ .../components/ecobee/.translations/en.json | 32 ++++--- .../geonetnz_quakes/.translations/bg.json | 17 ++++ .../components/izone/.translations/bg.json | 15 +++ .../components/life360/.translations/bg.json | 1 + .../components/light/.translations/bg.json | 4 + .../components/light/.translations/pl.json | 2 +- .../components/plex/.translations/bg.json | 45 +++++++++ .../components/plex/.translations/fr.json | 5 + .../components/plex/.translations/lb.json | 12 +++ .../solaredge/.translations/bg.json | 3 +- .../components/switch/.translations/bg.json | 2 + .../components/switch/.translations/pl.json | 4 +- .../components/toon/.translations/bg.json | 1 + .../components/traccar/.translations/bg.json | 18 ++++ .../twentemilieu/.translations/bg.json | 23 +++++ .../components/unifi/.translations/bg.json | 12 +++ .../components/velbus/.translations/bg.json | 21 +++++ .../components/vesync/.translations/bg.json | 20 ++++ .../components/withings/.translations/bg.json | 20 ++++ .../components/zha/.translations/bg.json | 43 +++++++++ .../components/zha/.translations/da.json | 17 ++++ .../components/zha/.translations/lb.json | 43 +++++++++ .../components/zha/.translations/nl.json | 26 ++++++ .../components/zha/.translations/no.json | 24 ++++- 33 files changed, 734 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/bg.json create mode 100644 homeassistant/components/binary_sensor/.translations/bg.json create mode 100644 homeassistant/components/binary_sensor/.translations/da.json create mode 100644 homeassistant/components/binary_sensor/.translations/lb.json create mode 100644 homeassistant/components/binary_sensor/.translations/pl.json create mode 100644 homeassistant/components/cert_expiry/.translations/bg.json create mode 100644 homeassistant/components/ecobee/.translations/bg.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/bg.json create mode 100644 homeassistant/components/izone/.translations/bg.json create mode 100644 homeassistant/components/plex/.translations/bg.json create mode 100644 homeassistant/components/traccar/.translations/bg.json create mode 100644 homeassistant/components/twentemilieu/.translations/bg.json create mode 100644 homeassistant/components/velbus/.translations/bg.json create mode 100644 homeassistant/components/vesync/.translations/bg.json create mode 100644 homeassistant/components/withings/.translations/bg.json diff --git a/homeassistant/components/arcam_fmj/.translations/bg.json b/homeassistant/components/arcam_fmj/.translations/bg.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/bg.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/bg.json b/homeassistant/components/binary_sensor/.translations/bg.json new file mode 100644 index 00000000000000..9b9741b960196c --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/bg.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0435 \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430", + "is_cold": "{entity_name} \u0435 \u0441\u0442\u0443\u0434\u0435\u043d", + "is_connected": "{entity_name} \u0435 \u0441\u0432\u044a\u0440\u0437\u0430\u043d", + "is_gas": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "is_hot": "{entity_name} \u0435 \u0433\u043e\u0440\u0435\u0449", + "is_light": "{entity_name} \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "is_locked": "{entity_name} \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "is_moist": "{entity_name} \u0435 \u0432\u043b\u0430\u0436\u0435\u043d", + "is_motion": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_moving": "{entity_name} \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "is_no_gas": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "is_no_light": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "is_no_motion": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "is_no_sound": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0437\u0432\u0443\u043a", + "is_no_vibration": "{entity_name} \u043d\u0435 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", + "is_not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0435 \u0437\u0430\u0440\u0435\u0434\u0435\u043d\u0430", + "is_not_cold": "{entity_name} \u043d\u0435 \u0435 \u0441\u0442\u0443\u0434\u0435\u043d", + "is_not_connected": "{entity_name} \u0435 \u0440\u0430\u0437\u043a\u0430\u0447\u0435\u043d", + "is_not_hot": "{entity_name} \u043d\u0435 \u0435 \u0433\u043e\u0440\u0435\u0449", + "is_not_locked": "{entity_name} \u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d", + "is_not_moist": "{entity_name} \u0435 \u0441\u0443\u0445", + "is_not_moving": "{entity_name} \u043d\u0435 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "is_not_occupied": "{entity_name} \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442", + "is_not_open": "{entity_name} \u0435 \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "is_not_plugged_in": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "is_not_present": "{entity_name} \u043d\u0435 \u0435 \u043d\u0430\u043b\u0438\u0446\u0435", + "is_not_unsafe": "{entity_name} \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "is_occupied": "{entity_name} \u0435 \u0437\u0430\u0435\u0442", + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "is_open": "{entity_name} \u0435 \u043e\u0442\u0432\u043e\u0440\u0435\u043d", + "is_plugged_in": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "is_powered": "{entity_name} \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "is_present": "{entity_name} \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "is_problem": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "is_smoke": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "is_sound": "{entity_name} \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", + "is_unsafe": "{entity_name} \u043d\u0435 \u0435 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "is_vibration": "{entity_name} \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" + }, + "trigger_type": { + "bat_low": "{entity_name} \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f", + "closed": "{entity_name} \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "cold": "{entity_name} \u0441\u0435 \u0438\u0437\u0441\u0442\u0443\u0434\u0438", + "connected": "{entity_name} \u0441\u0432\u044a\u0440\u0437\u0430\u043d", + "gas": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "hot": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", + "light": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "moist\u00a7": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0432\u043b\u0430\u0436\u0435\u043d", + "motion": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "moving": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "no_gas": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", + "no_light": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", + "no_motion": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "no_problem": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "no_smoke": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "no_sound": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0437\u0432\u0443\u043a", + "no_vibration": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438", + "not_bat_low": "{entity_name} \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u043d\u0435 \u0435 \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430", + "not_cold": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", + "not_connected": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "not_hot": "{entity_name} \u043e\u0445\u043b\u0430\u0434\u043d\u044f", + "not_locked": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d", + "not_moist": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0441\u0443\u0445", + "not_moving": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", + "not_occupied": "{entity_name} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442", + "not_plugged_in": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "not_present": "{entity_name} \u043d\u0435 \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "not_unsafe": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u0435\u043d", + "occupied": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0437\u0430\u0435\u0442", + "opened": "{entity_name} \u0441\u0435 \u043e\u0442\u0432\u043e\u0440\u0438", + "plugged_in": "{entity_name} \u0441\u0435 \u0432\u043a\u043b\u044e\u0447\u0438", + "powered": "{entity_name} \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", + "present": "{entity_name} \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", + "problem": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "smoke": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0438\u043c", + "sound": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0437\u0432\u0443\u043a", + "turned_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "turned_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "unsafe": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u043e\u043f\u0430\u0441\u0435\u043d", + "vibration": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u0437\u0430\u0441\u0438\u0447\u0430 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/da.json b/homeassistant/components/binary_sensor/.translations/da.json new file mode 100644 index 00000000000000..56822c2365ce71 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/da.json @@ -0,0 +1,65 @@ +{ + "device_automation": { + "condition_type": { + "is_cold": "{entity_name} er kold", + "is_connected": "{entity_name} er tilsluttet", + "is_gas": "{entity_name} registrerer gas", + "is_hot": "{entity_name} er varm", + "is_light": "{entity_name} registrerer lys", + "is_locked": "{entity_name} er l\u00e5st", + "is_moist": "{entity_name} er fugtig", + "is_motion": "{entity_name} registrerer bev\u00e6gelse", + "is_moving": "{entity_name} bev\u00e6ger sig", + "is_no_gas": "{entity_name} registrerer ikke gas", + "is_no_light": "{entity_name} registrerer ikke lys", + "is_no_motion": "{entity_name} registrerer ikke bev\u00e6gelse", + "is_no_problem": "{entity_name} registrerer ikke noget problem", + "is_no_smoke": "{entity_name} registrerer ikke r\u00f8g", + "is_no_sound": "{entity_name} registrerer ikke lyd", + "is_no_vibration": "{entity_name} registrerer ikke vibration", + "is_not_cold": "{entity_name} er ikke kold", + "is_not_connected": "{entity_name} er afbrudt", + "is_not_hot": "{entity_name} er ikke varm", + "is_not_locked": "{entity_name} er l\u00e5st op", + "is_not_moist": "{entity_name} er t\u00f8r", + "is_not_moving": "{entity_name} bev\u00e6ger sig ikke", + "is_not_occupied": "{entity_name} er ikke optaget", + "is_not_open": "{entity_name} er lukket", + "is_not_present": "{entity_name} er ikke til stede", + "is_not_unsafe": "{entity_name} er sikker", + "is_occupied": "{entity_name} er optaget", + "is_open": "{entity_name} er \u00e5ben", + "is_problem": "{entity_name} registrerer problem", + "is_smoke": "{entity_name} registrerer r\u00f8g", + "is_sound": "{entity_name} registrerer lyd", + "is_unsafe": "{entity_name} er usikker", + "is_vibration": "{entity_name} registrerer vibration" + }, + "trigger_type": { + "closed": "{entity_name} lukket", + "cold": "{entity_name} blev kold", + "connected": "{entity_name} tilsluttet", + "moist\u00a7": "{entity_name} blev fugtig", + "motion": "{entity_name} begyndte at registrere bev\u00e6gelse", + "moving": "{entity_name} begyndte at bev\u00e6ge sig", + "no_gas": "{entity_name} stoppede med at registrere gas", + "no_light": "{entity_name} stoppede med at registrere lys", + "no_motion": "{entity_name} stoppede med at registrere bev\u00e6gelse", + "no_problem": "{entity_name} stoppede med at registrere problem", + "no_smoke": "{entity_name} stoppede med at registrere r\u00f8g", + "no_sound": "{entity_name} stoppede med at registrere lyd", + "no_vibration": "{entity_name} stoppede med at registrere vibration", + "not_connected": "{entity_name} afbrudt", + "not_hot": "{entity_name} blev ikke varm", + "not_locked": "{entity_name} l\u00e5st op", + "not_moist": "{entity_name} blev t\u00f8r", + "not_present": "{entity_name} ikke til stede", + "not_unsafe": "{entity_name} blev sikker", + "occupied": "{entity_name} blev optaget", + "present": "{entity_name} til stede", + "problem": "{entity_name} begyndte at registrere problem", + "smoke": "{entity_name} begyndte at registrere r\u00f8g", + "sound": "{entity_name} begyndte at registrere lyd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/lb.json b/homeassistant/components/binary_sensor/.translations/lb.json new file mode 100644 index 00000000000000..0b10e1f51a52c5 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/lb.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} Batterie ass niddereg", + "is_cold": "{entity_name} ass kal", + "is_connected": "{entity_name} ass verbonnen", + "is_gas": "{entity_name} entdeckt Gas", + "is_hot": "{entity_name} ass waarm", + "is_light": "{entity_name} entdeckt Luucht", + "is_locked": "{entity_name} ass gespaart", + "is_moist": "{entity_name} ass fiicht", + "is_motion": "{entity_name} entdeckt Beweegung", + "is_moving": "{entity_name} beweegt sech", + "is_no_gas": "{entity_name} entdeckt kee Gas", + "is_no_light": "{entity_name} entdeckt keng Luucht", + "is_no_motion": "{entity_name} entdeckt keng Beweegung", + "is_no_problem": "{entity_name} entdeckt keng Problemer", + "is_no_smoke": "{entity_name} entdeckt keen Damp", + "is_no_sound": "{entity_name} entdeckt keen Toun", + "is_no_vibration": "{entity_name} entdeckt keng Vibratiounen", + "is_not_bat_low": "{entity_name} Batterie ass normal", + "is_not_cold": "{entity_name} ass net kal", + "is_not_connected": "{entity_name} ass d\u00e9connect\u00e9iert", + "is_not_hot": "{entity_name} ass net waarm", + "is_not_locked": "{entity_name} ass entspaart", + "is_not_moist": "{entity_name} ass dr\u00e9chen", + "is_not_moving": "{entity_name} beweegt sech net", + "is_not_occupied": "{entity_name} ass fr\u00e4i", + "is_not_open": "{entity_name} ass zou", + "is_not_plugged_in": "{entity_name} ass net ugeschloss", + "is_not_powered": "{entity_name} ass net aliment\u00e9iert", + "is_not_present": "{entity_name} ass net pr\u00e4sent", + "is_not_unsafe": "{entity_name} ass s\u00e9cher", + "is_occupied": "{entity_name} ass besat", + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un", + "is_open": "{entity_name} ass op", + "is_plugged_in": "{entity_name} ass ugeschloss", + "is_powered": "{entity_name} ass aliment\u00e9iert", + "is_present": "{entity_name} ass pr\u00e4sent", + "is_problem": "{entity_name} entdeckt Problemer", + "is_smoke": "{entity_name} entdeckt Damp", + "is_sound": "{entity_name} entdeckt Toun", + "is_unsafe": "{entity_name} ass ons\u00e9cher", + "is_vibration": "{entity_name} entdeckt Vibratiounen" + }, + "trigger_type": { + "bat_low": "{entity_name} Batterie niddereg", + "closed": "{entity_name} gouf zougemaach", + "cold": "{entity_name} gouf kal", + "connected": "{entity_name} ass verbonnen", + "gas": "{entity_name} huet ugefaangen Gas z'entdecken", + "hot": "{entity_name} gouf waarm", + "light": "{entity_name} huet ugefange Luucht z'entdecken", + "locked": "{entity_name} gespaart", + "moist\u00a7": "{entity_name} gouf fiicht", + "motion": "{entity_name} huet ugefaange Beweegung z'entdecken", + "moving": "{entity_name} huet ugefaangen sech ze beweegen", + "no_gas": "{entity_name} huet opgehale Gas z'entdecken", + "no_light": "{entity_name} huet opgehale Luucht z'entdecken", + "no_motion": "{entity_name} huet opgehale Beweegung z'entdecken", + "no_problem": "{entity_name} huet opgehale Problemer z'entdecken", + "no_smoke": "{entity_name} huet opgehale Damp z'entdecken", + "no_sound": "{entity_name} huet opgehale Toun z'entdecken", + "no_vibration": "{entity_name} huet opgehale Vibratiounen z'entdecken", + "not_bat_low": "{entity_name} Batterie normal", + "not_cold": "{entity_name} gouf net kal", + "not_connected": "{entity_name} d\u00e9connect\u00e9iert", + "not_hot": "{entity_name} gouf net waarm", + "not_locked": "{entity_name} entspaart", + "not_moist": "{entity_name} gouf dr\u00e9chen", + "not_moving": "{entity_name} huet opgehale sech ze beweegen", + "not_occupied": "{entity_name} gouf fr\u00e4i", + "not_plugged_in": "{entity_name} net ugeschloss", + "not_powered": "{entity_name} net aliment\u00e9iert", + "not_present": "{entity_name} net pr\u00e4sent", + "not_unsafe": "{entity_name} gouf s\u00e9cher", + "occupied": "{entity_name} gouf besat", + "opened": "{entity_name} gouf opgemaach", + "plugged_in": "{entity_name} ugeschloss", + "powered": "{entity_name} aliment\u00e9iert", + "present": "{entity_name} pr\u00e4sent", + "problem": "{entity_name} huet ugefaange Problemer z'entdecken", + "smoke": "{entity_name} huet ugefaangen Damp z'entdecken", + "sound": "{entity_name} huet ugefaangen Toun z'entdecken", + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt", + "unsafe": "{entity_name} gouf ons\u00e9cher", + "vibration": "{entity_name} huet ugefaange Vibratiounen z'entdecken" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json new file mode 100644 index 00000000000000..139cff2187fcfb --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "Bateria {entity_name} jest roz\u0142adowana", + "is_cold": "{entity_name} wykrywa zimno", + "is_connected": "{entity_name} jest po\u0142\u0105czone", + "is_gas": "{entity_name} wykrywa gaz", + "is_hot": "{entity_name} wykrywa gor\u0105co", + "is_light": "{entity_name} wykrywa \u015bwiat\u0142o", + "is_locked": "{entity_name} jest zamkni\u0119te", + "is_moist": "{entity_name} wykrywa wilgo\u0107", + "is_motion": "{entity_name} wykrywa ruch", + "is_moving": "{entity_name} porusza si\u0119", + "is_no_gas": "{entity_name} nie wykrywa gazu", + "is_no_light": "{entity_name} nie wykrywa \u015bwiat\u0142a", + "is_no_motion": "{entity_name} nie wykrywa ruchu", + "is_off": "{entity_name} jest wy\u0142\u0105czone", + "is_on": "{entity_name} jest w\u0142\u0105czone" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/bg.json b/homeassistant/components/cert_expiry/.translations/bg.json new file mode 100644 index 00000000000000..7c82ef8b9ba8f0 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/bg.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "certificate_fetch_failed": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043c\u0438\u0437\u0432\u043b\u0435\u0447\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442", + "connection_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0442\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441", + "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "resolve_failed": "\u0422\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" + }, + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441 \u0432 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", + "port": "\u041f\u043e\u0440\u0442 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + }, + "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 \u0437\u0430 \u0442\u0435\u0441\u0442\u0432\u0430\u043d\u0435" + } + }, + "title": "\u0421\u0440\u043e\u043a \u043d\u0430 \u0432\u0430\u043b\u0438\u0434\u043d\u043e\u0441\u0442 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index f3eead4aae023a..c9963e496238fd 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -69,5 +69,23 @@ "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e" } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ CLIP \u0441\u0435\u043d\u0437\u043e\u0440\u0438", + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u043d\u0438 \u0433\u0440\u0443\u043f\u0438" + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 \u0442\u0438\u043f\u043e\u0432\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ CLIP \u0441\u0435\u043d\u0437\u043e\u0440\u0438", + "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 deCONZ \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u043d\u0438 \u0433\u0440\u0443\u043f\u0438" + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 \u0442\u0438\u043f\u043e\u0432\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 1a03143f11edfd..840bc8929a7366 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -49,8 +49,8 @@ "button_3": "Dr\u00ebtte Kn\u00e4ppchen", "button_4": "V\u00e9ierte Kn\u00e4ppchen", "close": "Zoumaachen", - "dim_down": "Erhellen", - "dim_up": "Verd\u00e4ischteren", + "dim_down": "Verd\u00e4ischteren", + "dim_up": "Erhellen", "left": "L\u00e9nks", "open": "Op", "right": "Riets", diff --git a/homeassistant/components/ecobee/.translations/bg.json b/homeassistant/components/ecobee/.translations/bg.json new file mode 100644 index 00000000000000..bd8503fabd8b49 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\u0422\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 ecobee." + }, + "error": { + "pin_request_failed": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0438\u0441\u043a\u0430\u043d\u0435 \u043d\u0430 \u041f\u0418\u041d \u043e\u0442 ecobee; \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u0430\u043b\u0438 API \u043a\u043b\u044e\u0447\u044a\u0442 \u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d.", + "token_request_failed": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0438\u0441\u043a\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434\u043e\u0432\u0435 \u043e\u0442 ecobee; \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + }, + "step": { + "authorize": { + "description": "\u041c\u043e\u043b\u044f, \u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0442\u043e\u0432\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 https://www.ecobee.com/consumerportal/index.html \u0441 \u043f\u0438\u043d \u043a\u043e\u0434: \n\n {pin} \n \n \u0421\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435.", + "title": "\u041e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 ecobee.com" + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 API \u043a\u043b\u044e\u0447\u0430, \u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043e\u0442 ecobee.com.", + "title": "ecobee API \u043a\u043b\u044e\u0447" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/en.json b/homeassistant/components/ecobee/.translations/en.json index 9e7e9fed39659a..39072f70d8202c 100644 --- a/homeassistant/components/ecobee/.translations/en.json +++ b/homeassistant/components/ecobee/.translations/en.json @@ -1,23 +1,25 @@ { "config": { - "title": "ecobee", - "step": { - "user": { - "title": "ecobee API key", - "description": "Please enter the API key obtained from ecobee.com.", - "data": {"api_key": "API Key"} - }, - "authorize": { - "title": "Authorize app on ecobee.com", - "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit." - } + "abort": { + "one_instance_only": "This integration currently supports only one ecobee instance." }, "error": { "pin_request_failed": "Error requesting PIN from ecobee; please verify API key is correct.", "token_request_failed": "Error requesting tokens from ecobee; please try again." }, - "abort": { - "one_instance_only": "This integration currently supports only one ecobee instance." - } + "step": { + "authorize": { + "description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with pin code:\n\n{pin}\n\nThen, press Submit.", + "title": "Authorize app on ecobee.com" + }, + "user": { + "data": { + "api_key": "API Key" + }, + "description": "Please enter the API key obtained from ecobee.com.", + "title": "ecobee API key" + } + }, + "title": "ecobee" } -} +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/bg.json b/homeassistant/components/geonetnz_quakes/.translations/bg.json new file mode 100644 index 00000000000000..48d6eacda917d9 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0437\u0430 \u0444\u0438\u043b\u0442\u044a\u0440\u0430 \u0441\u0438." + } + }, + "title": "GeoNet NZ \u0417\u0435\u043c\u0435\u0442\u0440\u0435\u0441\u0435\u043d\u0438\u044f" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/bg.json b/homeassistant/components/izone/.translations/bg.json new file mode 100644 index 00000000000000..26a55aa4ed8e79 --- /dev/null +++ b/homeassistant/components/izone/.translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 iZone \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430.", + "single_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 iZone." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/bg.json b/homeassistant/components/life360/.translations/bg.json index 4fae0249fd004a..02354204f2406f 100644 --- a/homeassistant/components/life360/.translations/bg.json +++ b/homeassistant/components/life360/.translations/bg.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438", "invalid_username": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "unexpected": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u043a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u044f \u0441\u044a\u0441 \u0441\u044a\u0440\u0432\u044a\u0440\u0430 Life360", "user_already_configured": "\u0412\u0435\u0447\u0435 \u0438\u043c\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d \u043f\u0440\u043e\u0444\u0438\u043b" }, "step": { diff --git a/homeassistant/components/light/.translations/bg.json b/homeassistant/components/light/.translations/bg.json index 533ba76b6a7613..33b57d9e7cd1c1 100644 --- a/homeassistant/components/light/.translations/bg.json +++ b/homeassistant/components/light/.translations/bg.json @@ -8,6 +8,10 @@ "condition_type": { "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b.", "is_on": "{entity_name} \u0435 \u0432\u043a\u043b." + }, + "trigger_type": { + "turned_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "turned_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 22a93909578608..f8f4a2761d4624 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -6,7 +6,7 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "(entity_name} jest wy\u0142\u0105czony.", + "is_off": "{entity_name} jest wy\u0142\u0105czone", "is_on": "(entity_name} jest w\u0142\u0105czony." }, "trigger_type": { diff --git a/homeassistant/components/plex/.translations/bg.json b/homeassistant/components/plex/.translations/bg.json new file mode 100644 index 00000000000000..fb77e5da8cb666 --- /dev/null +++ b/homeassistant/components/plex/.translations/bg.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0432\u0435\u0447\u0435 \u0441\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", + "already_configured": "\u0422\u043e\u0437\u0438 Plex \u0441\u044a\u0440\u0432\u044a\u0440 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", + "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430", + "unknown": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0440\u0430\u0434\u0438 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "faulty_credentials": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", + "no_servers": "\u041d\u044f\u043c\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0438, \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441 \u0442\u043e\u0437\u0438 \u0430\u043a\u0430\u0443\u043d\u0442", + "no_token": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u043a\u043e\u0434 \u0438\u043b\u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "not_found": "Plex \u0441\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" + }, + "step": { + "manual_setup": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 SSL", + "token": "\u041a\u043e\u0434 (\u0430\u043a\u043e \u0441\u0435 \u0438\u0437\u0438\u0441\u043a\u0432\u0430)", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" + }, + "title": "Plex \u0441\u044a\u0440\u0432\u044a\u0440" + }, + "select_server": { + "data": { + "server": "\u0421\u044a\u0440\u0432\u044a\u0440" + }, + "description": "\u041d\u0430\u043b\u0438\u0447\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u044a\u0440\u0432\u044a\u0440\u0430, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u0438\u043d:", + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Plex \u0441\u044a\u0440\u0432\u044a\u0440" + }, + "user": { + "data": { + "manual_setup": "\u0420\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "token": "Plex \u043a\u043e\u0434" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043a\u043e\u0434 \u0437\u0430 Plex \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043b\u0438 \u0440\u044a\u0447\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440.", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 Plex \u0441\u044a\u0440\u0432\u044a\u0440" + } + }, + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 58a5169ac02701..812de425ef49f2 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -13,6 +13,11 @@ "not_found": "Serveur Plex introuvable" }, "step": { + "manual_setup": { + "data": { + "port": "Port" + } + }, "select_server": { "data": { "server": "Serveur" diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 130cf2067abe72..244044b2f67a80 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Feeler beider Autorisatioun", "no_servers": "Kee Server as mam Kont verbonnen", + "no_token": "Gitt en Token un oder wielt manuelle Setup", "not_found": "Kee Plex Server fonnt" }, "step": { + "manual_setup": { + "data": { + "host": "Apparat", + "port": "Port", + "ssl": "SSL benotzen", + "token": "Jeton (falls n\u00e9ideg)", + "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" + }, + "title": "Plex Server" + }, "select_server": { "data": { "server": "Server" @@ -22,6 +33,7 @@ }, "user": { "data": { + "manual_setup": "Manuell Konfiguratioun", "token": "Jeton fir de Plex" }, "description": "Gitt een Jeton fir de Plex un fir eng automatesch Konfiguratioun", diff --git a/homeassistant/components/solaredge/.translations/bg.json b/homeassistant/components/solaredge/.translations/bg.json index 72f1ad2a4c758f..e4223e373fd537 100644 --- a/homeassistant/components/solaredge/.translations/bg.json +++ b/homeassistant/components/solaredge/.translations/bg.json @@ -15,6 +15,7 @@ }, "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438\u0442\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043d\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 (API) \u0437\u0430 \u0442\u0430\u0437\u0438 \u0438\u043d\u0441\u0442\u0430\u043b\u0430\u0446\u0438\u044f" } - } + }, + "title": "SolarEdge" } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/bg.json b/homeassistant/components/switch/.translations/bg.json index efccc652d5beb7..64a3ea94e1b543 100644 --- a/homeassistant/components/switch/.translations/bg.json +++ b/homeassistant/components/switch/.translations/bg.json @@ -6,6 +6,8 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" }, "condition_type": { + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" }, diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 199b150f68ee6d..31187aaa1b7cc4 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -6,8 +6,8 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "{entity_name} jest wy\u0142\u0105czony.", - "is_on": "{entity_name} jest w\u0142\u0105czony", + "is_off": "{entity_name} jest wy\u0142\u0105czone", + "is_on": "{entity_name} jest w\u0142\u0105czone", "turn_off": "{entity_name} wy\u0142\u0105czone", "turn_on": "{entity_name} w\u0142\u0105czone" }, diff --git a/homeassistant/components/toon/.translations/bg.json b/homeassistant/components/toon/.translations/bg.json index e4aa0d8c08842a..0de9452b3cd226 100644 --- a/homeassistant/components/toon/.translations/bg.json +++ b/homeassistant/components/toon/.translations/bg.json @@ -15,6 +15,7 @@ "authenticate": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "tenant": "\u041d\u0430\u0435\u043c\u0430\u0442\u0435\u043b", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" }, "description": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0441 \u0412\u0430\u0448\u0438\u044f Eneco Toon \u043f\u0440\u043e\u0444\u0438\u043b (\u043d\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0430 \u0437\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u0446\u0438).", diff --git a/homeassistant/components/traccar/.translations/bg.json b/homeassistant/components/traccar/.translations/bg.json new file mode 100644 index 00000000000000..7fe89d491c9552 --- /dev/null +++ b/homeassistant/components/traccar/.translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0435 \u0434\u043e\u0441\u0442\u044a\u043f\u0435\u043d \u043e\u0442 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0437\u0430 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430 \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043e\u0442 Traccar.", + "one_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "create_entry": { + "default": "\u0417\u0430 \u0434\u0430 \u0438\u0437\u043f\u0440\u0430\u0449\u0430\u0442\u0435 \u0441\u044a\u0431\u0438\u0442\u0438\u044f \u0434\u043e Home Assistant, \u0449\u0435 \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u044f\u0442\u0430 webhook \u0432 Traccar. \n\n \u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u043d\u0438\u044f \u0430\u0434\u0440\u0435\u0441: ` {webhook_url} ` \n\n \u0412\u0438\u0436\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430]({docs_url}) \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0441\u0442\u0438." + }, + "step": { + "user": { + "description": "\u0421\u0438\u0433\u0443\u0440\u043d\u0438 \u043b\u0438 \u0441\u0442\u0435, \u0447\u0435 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Traccar?", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/bg.json b/homeassistant/components/twentemilieu/.translations/bg.json new file mode 100644 index 00000000000000..df36ab070d7991 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." + }, + "error": { + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435.", + "invalid_address": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u0432 \u0437\u043e\u043d\u0430 \u0437\u0430 \u043e\u0431\u0441\u043b\u0443\u0436\u0432\u0430\u043d\u0435 \u043d\u0430 Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "\u0414\u043e\u043c\u0430\u0448\u043d\u043e \u043f\u0438\u0441\u043c\u043e/\u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u043e", + "house_number": "\u041d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043a\u044a\u0449\u0430", + "post_code": "\u041f\u043e\u0449\u0435\u043d\u0441\u043a\u0438 \u043a\u043e\u0434" + }, + "description": "\u0421\u044a\u0437\u0434\u0430\u0439\u0442\u0435 Twente Milieu, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u044f\u0449\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0441\u044a\u0431\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u0442\u043f\u0430\u0434\u044a\u0446\u0438 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0430\u0434\u0440\u0435\u0441.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/bg.json b/homeassistant/components/unifi/.translations/bg.json index d8f571c968e01c..df5654ff78be5b 100644 --- a/homeassistant/components/unifi/.translations/bg.json +++ b/homeassistant/components/unifi/.translations/bg.json @@ -22,5 +22,17 @@ } }, "title": "UniFi \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\u0412\u0440\u0435\u043c\u0435 \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0438 \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u043e\u0442\u043e \u0432\u0438\u0436\u0434\u0430\u043d\u0435 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0447\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u043a\u0430\u0442\u043e \u043e\u0442\u0441\u044a\u0441\u0442\u0432\u0430\u0449\u043e", + "track_clients": "\u041f\u0440\u043e\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0438", + "track_devices": "\u041f\u0440\u043e\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (Ubiquiti \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430)", + "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0435\u0442\u0435 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441 \u043a\u0430\u0431\u0435\u043b" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/bg.json b/homeassistant/components/velbus/.translations/bg.json new file mode 100644 index 00000000000000..e769f83d28e2f6 --- /dev/null +++ b/homeassistant/components/velbus/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "\u0422\u043e\u0437\u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "error": { + "connection_failed": "\u0412\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0441 velbus \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "port_exists": "\u0422\u043e\u0437\u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d" + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0430 \u0442\u0430\u0437\u0438 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 velbus", + "port": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u0449 \u043d\u0438\u0437" + }, + "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0442\u0438\u043f\u0430 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0441 velbus" + } + }, + "title": "Velbus \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/bg.json b/homeassistant/components/vesync/.translations/bg.json new file mode 100644 index 00000000000000..a12436936e6960 --- /dev/null +++ b/homeassistant/components/vesync/.translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Vesync" + }, + "error": { + "invalid_login": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "E-mail \u0430\u0434\u0440\u0435\u0441" + }, + "title": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/bg.json b/homeassistant/components/withings/.translations/bg.json new file mode 100644 index 00000000000000..e75860d0e16b6f --- /dev/null +++ b/homeassistant/components/withings/.translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "no_flows": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Withings, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0441\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0442\u0435. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Withings \u0437\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b." + }, + "step": { + "user": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b, \u043a\u044a\u043c \u043a\u043e\u0439\u0442\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 Home Assistant \u0441 Withings. \u041d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u043d\u0430 Withings \u043d\u0435 \u0437\u0430\u0431\u0440\u0430\u0432\u044f\u0439\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u0438\u043d \u0438 \u0441\u044a\u0449 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b \u0438\u043b\u0438 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u043d\u044f\u043c\u0430 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e.", + "title": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/bg.json b/homeassistant/components/zha/.translations/bg.json index 642a2c0af134a6..2715ef46dc8504 100644 --- a/homeassistant/components/zha/.translations/bg.json +++ b/homeassistant/components/zha/.translations/bg.json @@ -16,5 +16,48 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", + "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_5": "\u041f\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_6": "\u0428\u0435\u0441\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "close": "\u0417\u0430\u0442\u0432\u043e\u0440\u0438", + "dim_down": "\u0417\u0430\u0442\u044a\u043c\u043d\u044f\u0432\u0430\u043d\u0435", + "dim_up": "\u041e\u0441\u0432\u0435\u0442\u044f\u0432\u0430\u043d\u0435", + "face_1": "\u0441 \u043b\u0438\u0446\u0435 1 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_2": "\u0441 \u043b\u0438\u0446\u0435 2 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_3": "\u0441 \u043b\u0438\u0446\u0435 3 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_4": "\u0441 \u043b\u0438\u0446\u0435 4 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_5": "\u0441 \u043b\u0438\u0446\u0435 5 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_6": "\u0441 \u043b\u0438\u0446\u0435 6 \u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u043e", + "face_any": "\u0421 \u043d\u044f\u043a\u043e\u0438/\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438 \u043b\u0438\u0446\u0435(\u0430) \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0438", + "left": "\u041b\u044f\u0432\u043e", + "open": "\u041e\u0442\u0432\u043e\u0440\u0435\u043d", + "right": "\u0414\u044f\u0441\u043d\u043e", + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438" + }, + "trigger_type": { + "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0438\u0437\u0442\u044a\u0440\u0432\u0430\u043d\u043e", + "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043e\u0431\u044a\u0440\u043d\u0430\u0442\u043e \"{subtype}\"", + "device_knocked": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043f\u043e\u0447\u0443\u043a\u0430\u043d\u043e \"{subtype}\"", + "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \"{subtype}\"", + "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e", + "device_slid": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0433\u043e \u0435 \u043f\u043b\u044a\u0437\u043d\u0430\u0442\u043e \"{subtype}\"", + "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043d\u0430\u043a\u043b\u043e\u043d\u0435\u043d\u043e", + "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", + "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", + "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", + "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", + "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", + "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 3140648f57adf6..8d99b6eebc9db8 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -16,5 +16,22 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Begge knapper", + "button_1": "F\u00f8rste knap", + "button_2": "Anden knap", + "button_3": "Tredje knap", + "button_4": "Fjerde knap", + "button_5": "Femte knap", + "close": "Luk", + "left": "Venstre", + "open": "\u00c5ben", + "right": "H\u00f8jre" + }, + "trigger_type": { + "device_shaken": "Enhed rystet" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/lb.json b/homeassistant/components/zha/.translations/lb.json index f3e7053ca11bc6..49a754f1da587f 100644 --- a/homeassistant/components/zha/.translations/lb.json +++ b/homeassistant/components/zha/.translations/lb.json @@ -16,5 +16,48 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "B\u00e9id Kn\u00e4ppchen", + "button_1": "\u00c9ischte Kn\u00e4ppchen", + "button_2": "Zweete Kn\u00e4ppchen", + "button_3": "Dr\u00ebtte Kn\u00e4ppchen", + "button_4": "V\u00e9ierte Kn\u00e4ppchen", + "button_5": "F\u00ebnnefte Kn\u00e4ppchen", + "button_6": "Sechste Kn\u00e4ppchen", + "close": "Zoumaachen", + "dim_down": "Verd\u00e4ischteren", + "dim_up": "Erhellen", + "face_1": "mat S\u00e4it 1 aktiv\u00e9iert", + "face_2": "mat S\u00e4it 2 aktiv\u00e9iert", + "face_3": "mat S\u00e4it 3 aktiv\u00e9iert", + "face_4": "mat S\u00e4it 4 aktiv\u00e9iert", + "face_5": "mat S\u00e4it 5 aktiv\u00e9iert", + "face_6": "mat S\u00e4it 6 aktiv\u00e9iert", + "face_any": "Mat iergendenger/spezifiz\u00e9ierter S\u00e4it(en) aktiv\u00e9iert", + "left": "L\u00e9nks", + "open": "Op", + "right": "Riets", + "turn_off": "Ausschalten", + "turn_on": "Uschalten" + }, + "trigger_type": { + "device_dropped": "Apparat gefall", + "device_flipped": "Apparat \u00ebmgedr\u00e9int \"{subtype}\"", + "device_knocked": "Apparat geklappt \"{subtype}\"", + "device_rotated": "Apparat gedr\u00e9int \"{subtype}\"", + "device_shaken": "Apparat ger\u00ebselt", + "device_slid": "Apparat gerutscht \"{subtype}\"", + "device_tilted": "Apparat ass gekippt", + "remote_button_double_press": "\"{subtype}\" Kn\u00e4ppche zwee mol gedr\u00e9ckt", + "remote_button_long_press": "\"{subtype}\" Kn\u00e4ppche permanent gedr\u00e9ckt", + "remote_button_long_release": "\"{subtype}\" Kn\u00e4ppche no laangem unhalen lassgelooss", + "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt", + "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt", + "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", + "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", + "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nl.json b/homeassistant/components/zha/.translations/nl.json index 56c2518f11e5b8..5e5c666b1a4e8d 100644 --- a/homeassistant/components/zha/.translations/nl.json +++ b/homeassistant/components/zha/.translations/nl.json @@ -16,5 +16,31 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "left": "Links", + "open": "Open", + "right": "Rechts", + "turn_off": "Uitschakelen", + "turn_on": "Inschakelen" + }, + "trigger_type": { + "device_dropped": "Apparaat gevallen", + "device_flipped": "Apparaat omgedraaid \"{subtype}\"", + "device_knocked": "Apparaat klopte \"{subtype}\"", + "device_rotated": "Apparaat gedraaid \" {subtype} \"", + "device_shaken": "Apparaat geschud", + "device_slid": "Apparaat geschoven \"{subtype}\"\".", + "device_tilted": "Apparaat gekanteld", + "remote_button_double_press": "\"{subtype}\" knop dubbel geklikt", + "remote_button_long_press": "\" {subtype} \" knop continu ingedrukt", + "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang indrukken van de knop", + "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt", + "remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt", + "remote_button_short_press": "\" {subtype} \" knop ingedrukt", + "remote_button_short_release": "\"{subtype}\" knop losgelaten", + "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index f639c85c682612..36bdfb6d5d33a5 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -24,9 +24,18 @@ "button_2": "Andre knapp", "button_3": "Tredje knapp", "button_4": "Fjerde knapp", + "button_5": "Femte knapp", + "button_6": "Sjette knapp", "close": "Lukk", "dim_down": "Dimm ned", "dim_up": "Dimm opp", + "face_1": "med ansikt 1 aktivert", + "face_2": "med ansikt 2 aktivert", + "face_3": "med ansikt 3 aktivert", + "face_4": "med ansikt 4 aktivert", + "face_5": "med ansikt 5 aktivert", + "face_6": "med ansikt 6 aktivert", + "face_any": "Med alle/angitte ansikt (er) aktivert", "left": "Venstre", "open": "\u00c5pen", "right": "H\u00f8yre", @@ -34,8 +43,21 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { + "device_dropped": "Enheten ble brutt", + "device_flipped": "Enheten snudd \"{undertype}\"", + "device_knocked": "Enheten sl\u00e5tt \"{undertype}\"", + "device_rotated": "Enheten roterte \"{under type}\"", + "device_shaken": "Enhet er ristet", + "device_slid": "Enheten skled \"{undertype}\"", + "device_tilted": "Enhet vippet", + "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", + "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", + "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\" {subtype} \" - knappen ble femdobbelt klikket", "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", - "remote_button_short_release": "\"{subtype}\"-knappen sluppet" + "remote_button_short_release": "\"{subtype}\"-knappen sluppet", + "remote_button_triple_press": "\" {subtype} \"-knappen ble rippel klikket" } } } \ No newline at end of file From 770ad86f126eb890f6c4021dc72261af62acd862 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 26 Sep 2019 07:42:46 +0200 Subject: [PATCH 0466/3953] Add mysensors codeowner (#26917) --- CODEOWNERS | 1 + homeassistant/components/mysensors/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index cb9180d717daa8..6abb7535574bac 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -182,6 +182,7 @@ homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core +homeassistant/components/mysensors/* @MartinHjelmare homeassistant/components/mystrom/* @fabaff homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index f18f5d4f8dda60..536848d3aef645 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -9,5 +9,7 @@ "after_dependencies": [ "mqtt" ], - "codeowners": [] + "codeowners": [ + "@MartinHjelmare" + ] } From 62cea2b7ac061d7cf819b0a004b2750e8e98ac70 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Thu, 26 Sep 2019 01:53:31 -0700 Subject: [PATCH 0467/3953] Bump pyobihai, add unique ID and availability (#26922) * Bump pyobihai, add unique ID and availability * Fix unique ID * Fetch serial correctly --- homeassistant/components/obihai/manifest.json | 2 +- homeassistant/components/obihai/sensor.py | 46 +++++++++++-------- requirements_all.txt | 2 +- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index dd4df479af431c..68045ff0584684 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -3,7 +3,7 @@ "name": "Obihai", "documentation": "https://www.home-assistant.io/components/obihai", "requirements": [ - "pyobihai==1.1.1" + "pyobihai==1.2.0" ], "dependencies": [], "codeowners": ["@dshokouhi"] diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index fbf4fffb17f807..4644875ee8b164 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -44,27 +44,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] - pyobihai = PyObihai() + pyobihai = PyObihai(host, username, password) - login = pyobihai.check_account(host, username, password) + login = pyobihai.check_account() if not login: _LOGGER.error("Invalid credentials") return - services = pyobihai.get_state(host, username, password) + serial = pyobihai.get_device_serial() - line_services = pyobihai.get_line_state(host, username, password) + services = pyobihai.get_state() - call_direction = pyobihai.get_call_direction(host, username, password) + line_services = pyobihai.get_line_state() + + call_direction = pyobihai.get_call_direction() for key in services: - sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) for key in line_services: - sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) for key in call_direction: - sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) add_entities(sensors) @@ -72,15 +74,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ObihaiServiceSensors(Entity): """Get the status of each Obihai Lines.""" - def __init__(self, pyobihai, host, username, password, service_name): + def __init__(self, pyobihai, serial, service_name): """Initialize monitor sensor.""" - self._host = host - self._username = username - self._password = password self._service_name = service_name self._state = None self._name = f"{OBIHAI} {self._service_name}" self._pyobihai = pyobihai + self._unique_id = f"{serial}-{self._service_name}" @property def name(self): @@ -92,6 +92,18 @@ def state(self): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return if sensor is available.""" + if self._state is not None: + return True + return False + + @property + def unique_id(self): + """Return the unique ID.""" + return self._unique_id + @property def device_class(self): """Return the device class for uptime sensor.""" @@ -101,21 +113,17 @@ def device_class(self): def update(self): """Update the sensor.""" - services = self._pyobihai.get_state(self._host, self._username, self._password) + services = self._pyobihai.get_state() if self._service_name in services: self._state = services.get(self._service_name) - services = self._pyobihai.get_line_state( - self._host, self._username, self._password - ) + services = self._pyobihai.get_line_state() if self._service_name in services: self._state = services.get(self._service_name) - call_direction = self._pyobihai.get_call_direction( - self._host, self._username, self._password - ) + call_direction = self._pyobihai.get_call_direction() if self._service_name in call_direction: self._state = call_direction.get(self._service_name) diff --git a/requirements_all.txt b/requirements_all.txt index 6481137f3e6a51..5bbd77f4d01b91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1346,7 +1346,7 @@ pynx584==0.4 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.1.1 +pyobihai==1.2.0 # homeassistant.components.ombi pyombi==0.1.5 From 9b204ad1629ffb0e2e14f28a547eeaa30dd4a1b1 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 26 Sep 2019 04:10:20 -0500 Subject: [PATCH 0468/3953] Add Plex config options support (#26870) * Add config options support * Actually copy dict * Move media_player options to PlexServer class * Handle updated config options * Move callback out of server --- homeassistant/components/plex/__init__.py | 23 ++++++-- homeassistant/components/plex/config_flow.py | 49 +++++++++++++++++ homeassistant/components/plex/media_player.py | 20 +++---- homeassistant/components/plex/server.py | 21 ++++++- homeassistant/components/plex/strings.json | 11 ++++ tests/components/plex/test_config_flow.py | 55 +++++++++++++++++++ 6 files changed, 160 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index dd458dda07880a..874ac6334acf5b 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -77,7 +77,7 @@ def _setup_plex(hass, config): """Pass configuration to a config flow.""" server_config = dict(config) if MP_DOMAIN in server_config: - hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN) + hass.data.setdefault(PLEX_MEDIA_PLAYER_OPTIONS, server_config.pop(MP_DOMAIN)) if CONF_HOST in server_config: prefix = "https" if server_config.pop(CONF_SSL) else "http" server_config[ @@ -96,7 +96,15 @@ async def async_setup_entry(hass, entry): """Set up Plex from a config entry.""" server_config = entry.data[PLEX_SERVER_CONFIG] - plex_server = PlexServer(server_config) + if MP_DOMAIN not in entry.options: + options = dict(entry.options) + options.setdefault( + MP_DOMAIN, + hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS) or MEDIA_PLAYER_SCHEMA({}), + ) + hass.config_entries.async_update_entry(entry, options=options) + + plex_server = PlexServer(server_config, entry.options) try: await hass.async_add_executor_job(plex_server.connect) except requests.exceptions.ConnectionError as error: @@ -123,14 +131,13 @@ async def async_setup_entry(hass, entry): ) hass.data[PLEX_DOMAIN][SERVERS][plex_server.machine_identifier] = plex_server - if not hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS): - hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = MEDIA_PLAYER_SCHEMA({}) - for platform in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, platform) ) + entry.add_update_listener(async_options_updated) + return True @@ -150,3 +157,9 @@ async def async_unload_entry(hass, entry): hass.data[PLEX_DOMAIN][SERVERS].pop(server_id) return True + + +async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + server_id = entry.data[CONF_SERVER_IDENTIFIER] + hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index e620e4869e5083..cf70b7470cdbcc 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Plex.""" +import copy import logging import plexapi.exceptions @@ -6,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -20,6 +22,8 @@ from .const import ( # pylint: disable=unused-import CONF_SERVER, CONF_SERVER_IDENTIFIER, + CONF_USE_EPISODE_ART, + CONF_SHOW_ALL_CONTROLS, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, @@ -52,6 +56,12 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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 PlexOptionsFlowHandler(config_entry) + def __init__(self): """Initialize the Plex flow.""" self.current_login = {} @@ -214,3 +224,42 @@ async def async_step_import(self, import_config): """Import from Plex configuration.""" _LOGGER.debug("Imported Plex configuration") return await self.async_step_server_validate(import_config) + + +class PlexOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Plex options.""" + + def __init__(self, config_entry): + """Initialize Plex options flow.""" + self.options = copy.deepcopy(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the Plex options.""" + return await self.async_step_plex_mp_settings() + + async def async_step_plex_mp_settings(self, user_input=None): + """Manage the Plex media_player options.""" + if user_input is not None: + self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] = user_input[ + CONF_USE_EPISODE_ART + ] + self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] = user_input[ + CONF_SHOW_ALL_CONTROLS + ] + return self.async_create_entry(title="", data=self.options) + + return self.async_show_form( + step_id="plex_mp_settings", + data_schema=vol.Schema( + { + vol.Required( + CONF_USE_EPISODE_ART, + default=self.options[MP_DOMAIN][CONF_USE_EPISODE_ART], + ): bool, + vol.Required( + CONF_SHOW_ALL_CONTROLS, + default=self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS], + ): bool, + } + ), + ) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4d097253ea1a93..356c7fe5741702 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -33,12 +33,9 @@ from homeassistant.util import dt as dt_util from .const import ( - CONF_USE_EPISODE_ART, - CONF_SHOW_ALL_CONTROLS, CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, - PLEX_MEDIA_PLAYER_OPTIONS, REFRESH_LISTENERS, SERVERS, ) @@ -67,8 +64,6 @@ def add_entities(entities, update_before_add=False): def _setup_platform(hass, config_entry, add_entities_callback): """Set up the Plex media_player platform.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] - config = hass.data[PLEX_MEDIA_PLAYER_OPTIONS] - plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] plex_clients = {} plex_sessions = {} @@ -102,7 +97,7 @@ def update_devices(): if device.machineIdentifier not in plex_clients: new_client = PlexClient( - config, device, None, plex_sessions, update_devices + plexserver, device, None, plex_sessions, update_devices ) plex_clients[device.machineIdentifier] = new_client _LOGGER.debug("New device: %s", device.machineIdentifier) @@ -141,7 +136,7 @@ def update_devices(): and machine_identifier is not None ): new_client = PlexClient( - config, player, session, plex_sessions, update_devices + plexserver, player, session, plex_sessions, update_devices ) plex_clients[machine_identifier] = new_client _LOGGER.debug("New session: %s", machine_identifier) @@ -170,7 +165,7 @@ def update_devices(): class PlexClient(MediaPlayerDevice): """Representation of a Plex device.""" - def __init__(self, config, device, session, plex_sessions, update_devices): + def __init__(self, plex_server, device, session, plex_sessions, update_devices): """Initialize the Plex device.""" self._app_name = "" self._device = None @@ -191,7 +186,7 @@ def __init__(self, config, device, session, plex_sessions, update_devices): self._state = STATE_IDLE self._volume_level = 1 # since we can't retrieve remotely self._volume_muted = False # since we can't retrieve remotely - self.config = config + self.plex_server = plex_server self.plex_sessions = plex_sessions self.update_devices = update_devices # General @@ -317,8 +312,9 @@ def refresh(self, device, session): def _set_media_image(self): thumb_url = self._session.thumbUrl - if self.media_content_type is MEDIA_TYPE_TVSHOW and not self.config.get( - CONF_USE_EPISODE_ART + if ( + self.media_content_type is MEDIA_TYPE_TVSHOW + and not self.plex_server.use_episode_art ): thumb_url = self._session.url(self._session.grandparentThumb) @@ -551,7 +547,7 @@ def supported_features(self): return 0 # force show all controls - if self.config.get(CONF_SHOW_ALL_CONTROLS): + if self.plex_server.show_all_controls: return ( SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index f41a9bdabae183..09274472915235 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -3,22 +3,29 @@ import plexapi.server from requests import Session +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL -from .const import CONF_SERVER, DEFAULT_VERIFY_SSL +from .const import ( + CONF_SERVER, + CONF_SHOW_ALL_CONTROLS, + CONF_USE_EPISODE_ART, + DEFAULT_VERIFY_SSL, +) from .errors import NoServersFound, ServerNotSpecified class PlexServer: """Manages a single Plex server connection.""" - def __init__(self, server_config): + def __init__(self, server_config, options=None): """Initialize a Plex server instance.""" self._plex_server = None self._url = server_config.get(CONF_URL) self._token = server_config.get(CONF_TOKEN) self._server_name = server_config.get(CONF_SERVER) self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) + self.options = options def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" @@ -80,3 +87,13 @@ def machine_identifier(self): def url_in_use(self): """Return URL used for connected Plex server.""" return self._plex_server._baseurl # pylint: disable=W0212 + + @property + def use_episode_art(self): + """Return use_episode_art option.""" + return self.options[MP_DOMAIN][CONF_USE_EPISODE_ART] + + @property + def show_all_controls(self): + """Return show_all_controls option.""" + return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index c093d4fe0cec1e..812e7b81a7cf24 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -41,5 +41,16 @@ "invalid_import": "Imported configuration is invalid", "unknown": "Failed for unknown reason" } + }, + "options": { + "step": { + "plex_mp_settings": { + "description": "Options for Plex Media Players", + "data": { + "use_episode_art": "Use episode art", + "show_all_controls": "Show all controls" + } + } + } } } diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index e98aed793cfdca..37cf0fa200cf94 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -28,6 +28,13 @@ MOCK_SERVER_1 = MockAvailableServer(MOCK_NAME_1, MOCK_ID_1) MOCK_SERVER_2 = MockAvailableServer(MOCK_NAME_2, MOCK_ID_2) +DEFAULT_OPTIONS = { + config_flow.MP_DOMAIN: { + config_flow.CONF_USE_EPISODE_ART: False, + config_flow.CONF_SHOW_ALL_CONTROLS: False, + } +} + def init_config_flow(hass): """Init a configuration flow.""" @@ -520,3 +527,51 @@ async def test_manual_config(hass): == mock_connections.connections[0].httpuri ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + + +async def test_no_token(hass): + """Test failing when no token provided.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": False} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"][CONF_TOKEN] == "no_token" + + +async def test_option_flow(hass): + """Test config flow selection of one of two bridges.""" + + entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=DEFAULT_OPTIONS) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.flow.async_init( + entry.entry_id, context={"source": "test"}, data=None + ) + + assert result["type"] == "form" + assert result["step_id"] == "plex_mp_settings" + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_USE_EPISODE_ART: True, + config_flow.CONF_SHOW_ALL_CONTROLS: True, + }, + ) + assert result["type"] == "create_entry" + assert result["data"] == { + config_flow.MP_DOMAIN: { + config_flow.CONF_USE_EPISODE_ART: True, + config_flow.CONF_SHOW_ALL_CONTROLS: True, + } + } From 82b77c2d29c95609d9ebdec0cbaf329cbb1d306d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 26 Sep 2019 12:14:57 +0300 Subject: [PATCH 0469/3953] Add config flow to transmission (#26434) * Add config flow to transmission * Reworked code to add all sensors and switches * applied fixes * final touches * Add tests * fixed tests * fix get_api errors and entities availabilty update * update config_flows.py * fix pylint error * update .coveragerc * add codeowner * add test_options * fixed test_options --- .coveragerc | 6 +- CODEOWNERS | 1 + .../transmission/.translations/en.json | 40 +++ .../components/transmission/__init__.py | 244 +++++++++++------ .../components/transmission/config_flow.py | 104 ++++++++ .../components/transmission/const.py | 24 ++ .../components/transmission/errors.py | 14 + .../components/transmission/manifest.json | 7 +- .../components/transmission/sensor.py | 21 +- .../components/transmission/services.yaml | 2 +- .../components/transmission/strings.json | 40 +++ .../components/transmission/switch.py | 67 +++-- homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/transmission/__init__.py | 1 + .../transmission/test_config_flow.py | 245 ++++++++++++++++++ 17 files changed, 708 insertions(+), 113 deletions(-) create mode 100644 homeassistant/components/transmission/.translations/en.json create mode 100644 homeassistant/components/transmission/config_flow.py create mode 100644 homeassistant/components/transmission/const.py create mode 100644 homeassistant/components/transmission/errors.py create mode 100644 homeassistant/components/transmission/strings.json create mode 100644 tests/components/transmission/__init__.py create mode 100644 tests/components/transmission/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index a8932f54a54725..d42d7cbb3b3250 100644 --- a/.coveragerc +++ b/.coveragerc @@ -675,7 +675,11 @@ omit = homeassistant/components/tradfri/cover.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py - homeassistant/components/transmission/* + homeassistant/components/transmission/__init__.py + homeassistant/components/transmission/sensor.py + homeassistant/components/transmission/switch.py + homeassistant/components/transmission/const.py + homeassistant/components/transmission/errors.py homeassistant/components/travisci/sensor.py homeassistant/components/tuya/* homeassistant/components/twentemilieu/const.py diff --git a/CODEOWNERS b/CODEOWNERS index 6abb7535574bac..11b99b42a44ac1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -288,6 +288,7 @@ homeassistant/components/tplink/* @rytilahti homeassistant/components/traccar/* @ludeeus homeassistant/components/tradfri/* @ggravlingen homeassistant/components/trafikverket_train/* @endor-force +homeassistant/components/transmission/* @engrbm87 homeassistant/components/tts/* @robbiet480 homeassistant/components/twentemilieu/* @frenck homeassistant/components/twilio_call/* @robbiet480 diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json new file mode 100644 index 00000000000000..7160cd109c4794 --- /dev/null +++ b/homeassistant/components/transmission/.translations/en.json @@ -0,0 +1,40 @@ +{ + "config": { + "title": "Transmission", + "step": { + "user": { + "title": "Setup Transmission Client", + "data": { + "name": "Name", + "host": "Host", + "username": "User name", + "password": "Password", + "port": "Port" + } + }, + "options": { + "title": "Configure Options", + "data": { + "scan_interval": "Update frequency" + } + } + }, + "error": { + "wrong_credentials": "Wrong username or password", + "cannot_connect": "Unable to Connect to host" + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Transmission", + "data": { + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index e7f9b94046d367..e6ddd87bdf56bd 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -2,47 +2,38 @@ from datetime import timedelta import logging +import transmissionrpc +from transmissionrpc.error import TransmissionError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, - CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + ATTR_TORRENT, + DATA_TRANSMISSION, + DATA_UPDATED, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + SERVICE_ADD_TORRENT, +) +from .errors import AuthenticationError, CannotConnect, UnknownError _LOGGER = logging.getLogger(__name__) -DOMAIN = "transmission" -DATA_UPDATED = "transmission_data_updated" -DATA_TRANSMISSION = "data_transmission" - -DEFAULT_NAME = "Transmission" -DEFAULT_PORT = 9091 -TURTLE_MODE = "turtle_mode" - -SENSOR_TYPES = { - "active_torrents": ["Active Torrents", None], - "current_status": ["Status", None], - "download_speed": ["Down Speed", "MB/s"], - "paused_torrents": ["Paused Torrents", None], - "total_torrents": ["Total Torrents", None], - "upload_speed": ["Up Speed", "MB/s"], - "completed_torrents": ["Completed Torrents", None], - "started_torrents": ["Started Torrents", None], -} - -DEFAULT_SCAN_INTERVAL = timedelta(seconds=120) - -ATTR_TORRENT = "torrent" - -SERVICE_ADD_TORRENT = "add_torrent" SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string}) @@ -55,13 +46,9 @@ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(TURTLE_MODE, default=False): cv.boolean, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL ): cv.time_period, - vol.Optional( - CONF_MONITORED_CONDITIONS, default=["current_status"] - ): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), } ) }, @@ -69,63 +56,157 @@ ) -def setup(hass, config): +async def async_setup(hass, config): + """Import the Transmission Component from config.""" + if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): """Set up the Transmission Component.""" - host = config[DOMAIN][CONF_HOST] - username = config[DOMAIN].get(CONF_USERNAME) - password = config[DOMAIN].get(CONF_PASSWORD) - port = config[DOMAIN][CONF_PORT] - scan_interval = config[DOMAIN][CONF_SCAN_INTERVAL] + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + if not config_entry.options: + await async_populate_options(hass, config_entry) + + client = TransmissionClient(hass, config_entry) + client_id = config_entry.entry_id + hass.data[DOMAIN][client_id] = client + if not await client.async_setup(): + return False + + return True + + +async def async_unload_entry(hass, entry): + """Unload Transmission Entry from config_entry.""" + hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) + if hass.data[DOMAIN][entry.entry_id].unsub_timer: + hass.data[DOMAIN][entry.entry_id].unsub_timer() + + for component in "sensor", "switch": + await hass.config_entries.async_forward_entry_unload(entry, component) + + del hass.data[DOMAIN] + + return True - import transmissionrpc - from transmissionrpc.error import TransmissionError +async def get_api(hass, host, port, username=None, password=None): + """Get Transmission client.""" try: - api = transmissionrpc.Client(host, port=port, user=username, password=password) - api.session_stats() + api = await hass.async_add_executor_job( + transmissionrpc.Client, host, port, username, password + ) + return api + except TransmissionError as error: - if str(error).find("401: Unauthorized"): - _LOGGER.error("Credentials for" " Transmission client are not valid") - return False + if "401: Unauthorized" in str(error): + _LOGGER.error("Credentials for Transmission client are not valid") + raise AuthenticationError + if "111: Connection refused" in str(error): + _LOGGER.error("Connecting to the Transmission client failed") + raise CannotConnect + + _LOGGER.error(error) + raise UnknownError + + +async def async_populate_options(hass, config_entry): + """Populate default options for Transmission Client.""" + options = {CONF_SCAN_INTERVAL: config_entry.data["options"][CONF_SCAN_INTERVAL]} - tm_data = hass.data[DATA_TRANSMISSION] = TransmissionData(hass, config, api) + hass.config_entries.async_update_entry(config_entry, options=options) - tm_data.update() - tm_data.init_torrent_list() - def refresh(event_time): - """Get the latest data from Transmission.""" - tm_data.update() +class TransmissionClient: + """Transmission Client Object.""" - track_time_interval(hass, refresh, scan_interval) + def __init__(self, hass, config_entry): + """Initialize the Transmission RPC API.""" + self.hass = hass + self.config_entry = config_entry + self.scan_interval = self.config_entry.options[CONF_SCAN_INTERVAL] + self.tm_data = None + self.unsub_timer = None + + async def async_setup(self): + """Set up the Transmission client.""" + + config = { + CONF_HOST: self.config_entry.data[CONF_HOST], + CONF_PORT: self.config_entry.data[CONF_PORT], + CONF_USERNAME: self.config_entry.data.get(CONF_USERNAME), + CONF_PASSWORD: self.config_entry.data.get(CONF_PASSWORD), + } + try: + api = await get_api(self.hass, **config) + except CannotConnect: + raise ConfigEntryNotReady + except (AuthenticationError, UnknownError): + return False + + self.tm_data = self.hass.data[DOMAIN][DATA_TRANSMISSION] = TransmissionData( + self.hass, self.config_entry, api + ) + + await self.hass.async_add_executor_job(self.tm_data.init_torrent_list) + await self.hass.async_add_executor_job(self.tm_data.update) + self.set_scan_interval(self.scan_interval) - def add_torrent(service): - """Add new torrent to download.""" - torrent = service.data[ATTR_TORRENT] - if torrent.startswith( - ("http", "ftp:", "magnet:") - ) or hass.config.is_allowed_path(torrent): - api.add_torrent(torrent) - else: - _LOGGER.warning( - "Could not add torrent: " "unsupported type or no permission" + for platform in ["sensor", "switch"]: + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, platform + ) ) - hass.services.register( - DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA - ) + def add_torrent(service): + """Add new torrent to download.""" + torrent = service.data[ATTR_TORRENT] + if torrent.startswith( + ("http", "ftp:", "magnet:") + ) or self.hass.config.is_allowed_path(torrent): + api.add_torrent(torrent) + else: + _LOGGER.warning( + "Could not add torrent: unsupported type or no permission" + ) + + self.hass.services.async_register( + DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA + ) + + self.config_entry.add_update_listener(self.async_options_updated) - sensorconfig = { - "sensors": config[DOMAIN][CONF_MONITORED_CONDITIONS], - "client_name": config[DOMAIN][CONF_NAME], - } + return True - discovery.load_platform(hass, "sensor", DOMAIN, sensorconfig, config) + def set_scan_interval(self, scan_interval): + """Update scan interval.""" - if config[DOMAIN][TURTLE_MODE]: - discovery.load_platform(hass, "switch", DOMAIN, sensorconfig, config) + def refresh(event_time): + """Get the latest data from Transmission.""" + self.tm_data.update() - return True + if self.unsub_timer is not None: + self.unsub_timer() + self.unsub_timer = async_track_time_interval( + self.hass, refresh, timedelta(seconds=scan_interval) + ) + + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + hass.data[DOMAIN][entry.entry_id].set_scan_interval( + entry.options[CONF_SCAN_INTERVAL] + ) class TransmissionData: @@ -133,6 +214,7 @@ class TransmissionData: def __init__(self, hass, config, api): """Initialize the Transmission RPC API.""" + self.hass = hass self.data = None self.torrents = None self.session = None @@ -140,12 +222,9 @@ def __init__(self, hass, config, api): self._api = api self.completed_torrents = [] self.started_torrents = [] - self.hass = hass def update(self): """Get the latest data from Transmission instance.""" - from transmissionrpc.error import TransmissionError - try: self.data = self._api.session_stats() self.torrents = self._api.get_torrents() @@ -153,15 +232,15 @@ def update(self): self.check_completed_torrent() self.check_started_torrent() + _LOGGER.debug("Torrent Data Updated") - dispatcher_send(self.hass, DATA_UPDATED) - - _LOGGER.debug("Torrent Data updated") self.available = True except TransmissionError: self.available = False _LOGGER.error("Unable to connect to Transmission client") + dispatcher_send(self.hass, DATA_UPDATED) + def init_torrent_list(self): """Initialize torrent lists.""" self.torrents = self._api.get_torrents() @@ -211,6 +290,15 @@ def get_completed_torrent_count(self): """Get the number of completed torrents.""" return len(self.completed_torrents) + def start_torrents(self): + """Start all torrents.""" + self._api.start_all() + + def stop_torrents(self): + """Stop all active torrents.""" + torrent_ids = [torrent.id for torrent in self.torrents] + self._api.stop_torrent(torrent_ids) + def set_alt_speed_enabled(self, is_enabled): """Set the alternative speed flag.""" self._api.set_session(alt_speed_enabled=is_enabled) diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py new file mode 100644 index 00000000000000..99376f4b6e0a87 --- /dev/null +++ b/homeassistant/components/transmission/config_flow.py @@ -0,0 +1,104 @@ +"""Config flow for Transmission Bittorent Client.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.core import callback + +from . import get_api +from .const import DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN +from .errors import AuthenticationError, CannotConnect, UnknownError + + +class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a UniFi config flow.""" + + 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 TransmissionOptionsFlowHandler(config_entry) + + def __init__(self): + """Initialize the Transmission flow.""" + self.config = {} + self.errors = {} + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="one_instance_allowed") + + if user_input is not None: + + self.config[CONF_NAME] = user_input.pop(CONF_NAME) + try: + await get_api(self.hass, **user_input) + self.config.update(user_input) + if "options" not in self.config: + self.config["options"] = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} + return self.async_create_entry( + title=self.config[CONF_NAME], data=self.config + ) + except AuthenticationError: + self.errors[CONF_USERNAME] = "wrong_credentials" + self.errors[CONF_PASSWORD] = "wrong_credentials" + except (CannotConnect, UnknownError): + self.errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + ), + errors=self.errors, + ) + + async def async_step_import(self, import_config): + """Import from Transmission client config.""" + self.config["options"] = { + CONF_SCAN_INTERVAL: import_config.pop(CONF_SCAN_INTERVAL).seconds + } + + return await self.async_step_user(user_input=import_config) + + +class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Transmission client options.""" + + def __init__(self, config_entry): + """Initialize Transmission options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the Transmission options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, + self.config_entry.data["options"][CONF_SCAN_INTERVAL], + ), + ): int + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py new file mode 100644 index 00000000000000..e4a8b1490c28bf --- /dev/null +++ b/homeassistant/components/transmission/const.py @@ -0,0 +1,24 @@ +"""Constants for the Transmission Bittorent Client component.""" +DOMAIN = "transmission" + +SENSOR_TYPES = { + "active_torrents": ["Active Torrents", None], + "current_status": ["Status", None], + "download_speed": ["Down Speed", "MB/s"], + "paused_torrents": ["Paused Torrents", None], + "total_torrents": ["Total Torrents", None], + "upload_speed": ["Up Speed", "MB/s"], + "completed_torrents": ["Completed Torrents", None], + "started_torrents": ["Started Torrents", None], +} +SWITCH_TYPES = {"on_off": "Switch", "turtle_mode": "Turtle Mode"} + +DEFAULT_NAME = "Transmission" +DEFAULT_PORT = 9091 +DEFAULT_SCAN_INTERVAL = 120 + +ATTR_TORRENT = "torrent" +SERVICE_ADD_TORRENT = "add_torrent" + +DATA_UPDATED = "transmission_data_updated" +DATA_TRANSMISSION = "data_transmission" diff --git a/homeassistant/components/transmission/errors.py b/homeassistant/components/transmission/errors.py new file mode 100644 index 00000000000000..b5f74f7bf4066c --- /dev/null +++ b/homeassistant/components/transmission/errors.py @@ -0,0 +1,14 @@ +"""Errors for the Transmission component.""" +from homeassistant.exceptions import HomeAssistantError + + +class AuthenticationError(HomeAssistantError): + """Wrong Username or Password.""" + + +class CannotConnect(HomeAssistantError): + """Unable to connect to client.""" + + +class UnknownError(HomeAssistantError): + """Unknown Error.""" diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index bc5da64fcacd9b..2bd4571ef93b92 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -1,10 +1,13 @@ { "domain": "transmission", "name": "Transmission", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/transmission", "requirements": [ "transmissionrpc==0.11" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@engrbm87" + ] +} \ No newline at end of file diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index ac2e64ce92f390..30dfa4a3cbed47 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -1,32 +1,29 @@ """Support for monitoring the Transmission BitTorrent client API.""" -from datetime import timedelta import logging -from homeassistant.const import STATE_IDLE +from homeassistant.const import CONF_NAME, STATE_IDLE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import DATA_TRANSMISSION, DATA_UPDATED, SENSOR_TYPES +from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "Transmission" -SCAN_INTERVAL = timedelta(seconds=120) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import config from configuration.yaml.""" + pass -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission sensors.""" - if discovery_info is None: - return - transmission_api = hass.data[DATA_TRANSMISSION] - monitored_variables = discovery_info["sensors"] - name = discovery_info["client_name"] + transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + name = config_entry.data[CONF_NAME] dev = [] - for sensor_type in monitored_variables: + for sensor_type in SENSOR_TYPES: dev.append( TransmissionSensor( sensor_type, diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index e049f89b3c6a6d..ab383584e83fdc 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -3,4 +3,4 @@ add_torrent: fields: torrent: description: URL, magnet link or Base64 encoded file. - example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent} + example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json new file mode 100644 index 00000000000000..7160cd109c4794 --- /dev/null +++ b/homeassistant/components/transmission/strings.json @@ -0,0 +1,40 @@ +{ + "config": { + "title": "Transmission", + "step": { + "user": { + "title": "Setup Transmission Client", + "data": { + "name": "Name", + "host": "Host", + "username": "User name", + "password": "Password", + "port": "Port" + } + }, + "options": { + "title": "Configure Options", + "data": { + "scan_interval": "Update frequency" + } + } + }, + "error": { + "wrong_credentials": "Wrong username or password", + "cannot_connect": "Unable to Connect to host" + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Transmission", + "data": { + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index df490cdbe47cbe..0bb43f715aca37 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -1,43 +1,50 @@ """Support for setting the Transmission BitTorrent client Turtle Mode.""" import logging -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import ToggleEntity -from . import DATA_TRANSMISSION, DATA_UPDATED +from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SWITCH_TYPES _LOGGING = logging.getLogger(__name__) -DEFAULT_NAME = "Transmission Turtle Mode" - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import config from configuration.yaml.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission switch.""" - if discovery_info is None: - return - component_name = DATA_TRANSMISSION - transmission_api = hass.data[component_name] - name = discovery_info["client_name"] + transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + name = config_entry.data[CONF_NAME] - async_add_entities([TransmissionSwitch(transmission_api, name)], True) + dev = [] + for switch_type, switch_name in SWITCH_TYPES.items(): + dev.append(TransmissionSwitch(switch_type, switch_name, transmission_api, name)) + + async_add_entities(dev, True) class TransmissionSwitch(ToggleEntity): """Representation of a Transmission switch.""" - def __init__(self, transmission_client, name): + def __init__(self, switch_type, switch_name, transmission_api, name): """Initialize the Transmission switch.""" - self._name = name - self.transmission_client = transmission_client + self._name = switch_name + self.client_name = name + self.type = switch_type + self._transmission_api = transmission_api self._state = STATE_OFF + self._data = None @property def name(self): """Return the name of the switch.""" - return self._name + return f"{self.client_name} {self._name}" @property def state(self): @@ -54,15 +61,30 @@ def is_on(self): """Return true if device is on.""" return self._state == STATE_ON + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self._transmission_api.available + def turn_on(self, **kwargs): """Turn the device on.""" - _LOGGING.debug("Turning Turtle Mode of Transmission on") - self.transmission_client.set_alt_speed_enabled(True) + if self.type == "on_off": + _LOGGING.debug("Starting all torrents") + self._transmission_api.start_torrents() + elif self.type == "turtle_mode": + _LOGGING.debug("Turning Turtle Mode of Transmission on") + self._transmission_api.set_alt_speed_enabled(True) + self._transmission_api.update() def turn_off(self, **kwargs): """Turn the device off.""" - _LOGGING.debug("Turning Turtle Mode of Transmission off") - self.transmission_client.set_alt_speed_enabled(False) + if self.type == "on_off": + _LOGGING.debug("Stoping all torrents") + self._transmission_api.stop_torrents() + if self.type == "turtle_mode": + _LOGGING.debug("Turning Turtle Mode of Transmission off") + self._transmission_api.set_alt_speed_enabled(False) + self._transmission_api.update() async def async_added_to_hass(self): """Handle entity which will be added.""" @@ -76,7 +98,14 @@ def _schedule_immediate_update(self): def update(self): """Get the latest data from Transmission and updates the state.""" - active = self.transmission_client.get_alt_speed_enabled() + active = None + if self.type == "on_off": + self._data = self._transmission_api.data + if self._data: + active = self._data.activeTorrentCount > 0 + + elif self.type == "turtle_mode": + active = self._transmission_api.get_alt_speed_enabled() if active is None: return diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b6865f9e86a16e..ab7b339e58284a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -62,6 +62,7 @@ "tplink", "traccar", "tradfri", + "transmission", "twentemilieu", "twilio", "unifi", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d42120c339ea27..d790a423de3888 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -422,6 +422,9 @@ statsd==3.2.1 # homeassistant.components.toon toonapilib==3.2.4 +# homeassistant.components.transmission +transmissionrpc==0.11 + # homeassistant.components.twentemilieu twentemilieu==0.1.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fcb265bbc97b6b..1e484e0dfc4853 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -173,6 +173,7 @@ "srpenergy", "statsd", "toonapilib", + "transmissionrpc", "twentemilieu", "uvcclient", "vsure", diff --git a/tests/components/transmission/__init__.py b/tests/components/transmission/__init__.py new file mode 100644 index 00000000000000..b8f8d8c847fa50 --- /dev/null +++ b/tests/components/transmission/__init__.py @@ -0,0 +1 @@ +"""Tests for Transmission.""" diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py new file mode 100644 index 00000000000000..e79f5c8ac96a88 --- /dev/null +++ b/tests/components/transmission/test_config_flow.py @@ -0,0 +1,245 @@ +"""Tests for Met.no config flow.""" +from datetime import timedelta +from unittest.mock import patch + +import pytest +from transmissionrpc.error import TransmissionError + +from homeassistant import data_entry_flow +from homeassistant.components.transmission import config_flow +from homeassistant.components.transmission.const import ( + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) + +from tests.common import MockConfigEntry + +NAME = "Transmission" +HOST = "192.168.1.100" +USERNAME = "username" +PASSWORD = "password" +PORT = 9091 +SCAN_INTERVAL = 10 + + +@pytest.fixture(name="api") +def mock_transmission_api(): + """Mock an api.""" + with patch("transmissionrpc.Client"): + yield + + +@pytest.fixture(name="auth_error") +def mock_api_authentication_error(): + """Mock an api.""" + with patch( + "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") + ): + yield + + +@pytest.fixture(name="conn_error") +def mock_api_connection_error(): + """Mock an api.""" + with patch( + "transmissionrpc.Client", + side_effect=TransmissionError("111: Connection refused"), + ): + yield + + +@pytest.fixture(name="unknown_error") +def mock_api_unknown_error(): + """Mock an api.""" + with patch("transmissionrpc.Client", side_effect=TransmissionError): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.TransmissionFlowHandler() + flow.hass = hass + return flow + + +async def test_flow_works(hass, api): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # test with required fields only + result = await flow.async_step_user( + {CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + + # test with all provided + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_PORT] == PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + + +async def test_options(hass): + """Test updating options.""" + entry = MockConfigEntry( + domain=DOMAIN, + title=CONF_NAME, + data={ + "name": DEFAULT_NAME, + "host": HOST, + "username": USERNAME, + "password": PASSWORD, + "port": DEFAULT_PORT, + "options": {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + }, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + ) + flow = init_config_flow(hass) + options_flow = flow.async_get_options_flow(entry) + + result = await options_flow.async_step_init() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_SCAN_INTERVAL] == 10 + + +async def test_import(hass, api): + """Test import step.""" + flow = init_config_flow(hass) + + # import with minimum fields only + result = await flow.async_step_import( + { + CONF_NAME: DEFAULT_NAME, + CONF_HOST: HOST, + CONF_PORT: DEFAULT_PORT, + CONF_SCAN_INTERVAL: timedelta(seconds=DEFAULT_SCAN_INTERVAL), + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"][CONF_NAME] == DEFAULT_NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == DEFAULT_PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + + # import with all + result = await flow.async_step_import( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + CONF_SCAN_INTERVAL: timedelta(seconds=SCAN_INTERVAL), + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_PORT] == PORT + assert result["data"]["options"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL + + +async def test_integration_already_exists(hass, api): + """Test we only allow a single config flow.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "abort" + assert result["reason"] == "one_instance_allowed" + + +async def test_error_on_wrong_credentials(hass, auth_error): + """Test with wrong credentials.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == { + CONF_USERNAME: "wrong_credentials", + CONF_PASSWORD: "wrong_credentials", + } + + +async def test_error_on_connection_failure(hass, conn_error): + """Test when connection to host fails.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_error_on_unknwon_error(hass, unknown_error): + """Test when connection to host fails.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user( + { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} From 3efdf29dfa178838002201eaea258db78e6fa2ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Vran=C3=ADk?= Date: Thu, 26 Sep 2019 11:24:03 +0200 Subject: [PATCH 0470/3953] Centralize rainbird config and add binary sensor platform (#26393) * 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 * pyrainbird version 0.3.0 * zone id * rainsensor return state * updating rainsensor * new version of pyrainbird * binary sensor state * quiet in check format * is_on instead of state for binary_sensor * no unit of measurement for binary sensor * no monitored conditions config * get keys of dict directly * removed redundant update of state * simplified switch * right states for switch * raindelay sensor * raindelay sensor * binary sensor state * binary sensor state * reorganized imports * doc on public method * reformatted * add irrigation service to rain bird, which allows you to set the duration * rebased on konikvranik and solved some feedback * add irrigation service to rain bird * sensor types to constants * synchronized register service * patform discovery * binary sensor as wrapper to sensor * version 0.4.0 * new config approach * sensors cleanup * bypass if no zones found * platform schema removed * Change config schema to list of controllers some small code improvements as suggested in CR: - dictionary acces by [] - just return instead of return False - import order - no optional parameter name * some small code improvements as suggested in CR: - supported platforms in constant - just return instead of return False - removed unused constant * No single controller configuration Co-Authored-By: Martin Hjelmare * pyrainbird 0.4.1 * individual switch configuration * imports order * generate default name out of entity * trigger time required for controller * incorporated CR remarks: - constant fo rzones - removed SCAN_INTERVAL - detection of success on initialization - removed underscore - refactored if/else - empty line on end of file - hass as first parameter * import of library on top * refactored * Update homeassistant/components/rainbird/__init__.py Co-Authored-By: Martin Hjelmare * validate time and set defaults * set defaults on right place * pylint bypass * iterate over values * codeowner * reverted changes: * irrigation time just as positive integer. Making it complex does make sense * zone edfaults fullfiled at runtime. There is no information about available zones in configuration time. * codeowners updated * accept timedelta in irrigation time * simplified time calculation * call total_seconds * irrigation time as seconds. * simplified schema --- CODEOWNERS | 1 + homeassistant/components/rainbird/__init__.py | 83 +++++++++++--- .../components/rainbird/binary_sensor.py | 64 +++++++++++ .../components/rainbird/manifest.json | 6 +- homeassistant/components/rainbird/sensor.py | 47 ++++---- .../components/rainbird/services.yaml | 9 ++ homeassistant/components/rainbird/switch.py | 105 ++++++++++-------- requirements_all.txt | 2 +- 8 files changed, 225 insertions(+), 92 deletions(-) create mode 100644 homeassistant/components/rainbird/binary_sensor.py create mode 100644 homeassistant/components/rainbird/services.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 11b99b42a44ac1..419bc1a8606abb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -226,6 +226,7 @@ homeassistant/components/qld_bushfire/* @exxamalte homeassistant/components/qnap/* @colinodell homeassistant/components/quantum_gateway/* @cisasteelersfan homeassistant/components/qwikswitch/* @kellerza +homeassistant/components/rainbird/* @konikvranik homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainforest_eagle/* @gtdiehl homeassistant/components/rainmachine/* @bachya diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 1d8ed8e37b169c..0b51be1f258018 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -1,42 +1,91 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" import logging +from pyrainbird import RainbirdController import voluptuous as vol +from homeassistant.components import binary_sensor, sensor, switch +from homeassistant.const import ( + CONF_FRIENDLY_NAME, + CONF_HOST, + CONF_PASSWORD, + CONF_TRIGGER_TIME, +) +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD + +CONF_ZONES = "zones" + +SUPPORTED_PLATFORMS = [switch.DOMAIN, sensor.DOMAIN, binary_sensor.DOMAIN] _LOGGER = logging.getLogger(__name__) +RAINBIRD_CONTROLLER = "controller" DATA_RAINBIRD = "rainbird" DOMAIN = "rainbird" -CONFIG_SCHEMA = vol.Schema( +SENSOR_TYPE_RAINDELAY = "raindelay" +SENSOR_TYPE_RAINSENSOR = "rainsensor" +# sensor_type [ description, unit, icon ] +SENSOR_TYPES = { + SENSOR_TYPE_RAINSENSOR: ["Rainsensor", None, "mdi:water"], + SENSOR_TYPE_RAINDELAY: ["Raindelay", None, "mdi:water-off"], +} + +TRIGGER_TIME_SCHEMA = vol.All( + cv.time_period, cv.positive_timedelta, lambda td: (td.total_seconds() // 60) +) + +ZONE_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string} - ) - }, + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Optional(CONF_TRIGGER_TIME): TRIGGER_TIME_SCHEMA, + } +) +CONTROLLER_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_TRIGGER_TIME): TRIGGER_TIME_SCHEMA, + vol.Optional(CONF_ZONES): vol.Schema({cv.positive_int: ZONE_SCHEMA}), + } +) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [CONTROLLER_SCHEMA]))}, extra=vol.ALLOW_EXTRA, ) def setup(hass, config): """Set up the Rain Bird component.""" - conf = config[DOMAIN] - server = conf.get(CONF_HOST) - password = conf.get(CONF_PASSWORD) - from pyrainbird import RainbirdController + hass.data[DATA_RAINBIRD] = [] + success = False + for controller_config in config[DOMAIN]: + success = success or _setup_controller(hass, controller_config, config) - controller = RainbirdController(server, password) + return success - _LOGGER.debug("Rain Bird Controller set to: %s", server) - initial_status = controller.currentIrrigation() - if initial_status and initial_status["type"] != "CurrentStationsActiveResponse": - _LOGGER.error("Error getting state. Possible configuration issues") +def _setup_controller(hass, controller_config, config): + """Set up a controller.""" + server = controller_config[CONF_HOST] + password = controller_config[CONF_PASSWORD] + controller = RainbirdController(server, password) + position = len(hass.data[DATA_RAINBIRD]) + try: + controller.get_serial_number() + except Exception as exc: # pylint: disable=W0703 + _LOGGER.error("Unable to setup controller: %s", exc) return False - - hass.data[DATA_RAINBIRD] = controller + hass.data[DATA_RAINBIRD].append(controller) + _LOGGER.debug("Rain Bird Controller %d set to: %s", position, server) + for platform in SUPPORTED_PLATFORMS: + discovery.load_platform( + hass, + platform, + DOMAIN, + {RAINBIRD_CONTROLLER: position, **controller_config}, + config, + ) return True diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py new file mode 100644 index 00000000000000..51c5f7a9dbefe7 --- /dev/null +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -0,0 +1,64 @@ +"""Support for Rain Bird Irrigation system LNK WiFi Module.""" +import logging + +from pyrainbird import RainbirdController + +from homeassistant.components.binary_sensor import BinarySensorDevice + +from . import ( + DATA_RAINBIRD, + RAINBIRD_CONTROLLER, + SENSOR_TYPE_RAINDELAY, + SENSOR_TYPE_RAINSENSOR, + SENSOR_TYPES, +) + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a Rain Bird sensor.""" + if discovery_info is None: + return + + controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]] + add_entities( + [RainBirdSensor(controller, sensor_type) for sensor_type in SENSOR_TYPES], True + ) + + +class RainBirdSensor(BinarySensorDevice): + """A sensor implementation for Rain Bird device.""" + + def __init__(self, controller: RainbirdController, sensor_type): + """Initialize the Rain Bird sensor.""" + self._sensor_type = sensor_type + self._controller = controller + self._name = SENSOR_TYPES[self._sensor_type][0] + self._icon = SENSOR_TYPES[self._sensor_type][2] + self._state = None + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return None if self._state is None else bool(self._state) + + def update(self): + """Get the latest data and updates the states.""" + _LOGGER.debug("Updating sensor: %s", self._name) + state = None + if self._sensor_type == SENSOR_TYPE_RAINSENSOR: + state = self._controller.get_rain_sensor_state() + elif self._sensor_type == SENSOR_TYPE_RAINDELAY: + state = self._controller.get_rain_delay() + self._state = None if state is None else bool(state) + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def icon(self): + """Return icon.""" + return self._icon diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 584ea22afe23ef..b911aaa57e19b5 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -3,8 +3,10 @@ "name": "Rainbird", "documentation": "https://www.home-assistant.io/components/rainbird", "requirements": [ - "pyrainbird==0.2.1" + "pyrainbird==0.4.1" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@konikvranik" + ] } diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index 2d4549a21d5dba..501566de6823bc 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -1,44 +1,37 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" import logging -import voluptuous as vol +from pyrainbird import RainbirdController -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_MONITORED_CONDITIONS -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from . import DATA_RAINBIRD +from . import ( + DATA_RAINBIRD, + RAINBIRD_CONTROLLER, + SENSOR_TYPE_RAINDELAY, + SENSOR_TYPE_RAINSENSOR, + SENSOR_TYPES, +) _LOGGER = logging.getLogger(__name__) -# sensor_type [ description, unit, icon ] -SENSOR_TYPES = {"rainsensor": ["Rainsensor", None, "mdi:water"]} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ) - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Rain Bird sensor.""" - controller = hass.data[DATA_RAINBIRD] - sensors = [] - for sensor_type in config.get(CONF_MONITORED_CONDITIONS): - sensors.append(RainBirdSensor(controller, sensor_type)) + if discovery_info is None: + return - add_entities(sensors, True) + controller = hass.data[DATA_RAINBIRD][discovery_info[RAINBIRD_CONTROLLER]] + add_entities( + [RainBirdSensor(controller, sensor_type) for sensor_type in SENSOR_TYPES], True + ) class RainBirdSensor(Entity): """A sensor implementation for Rain Bird device.""" - def __init__(self, controller, sensor_type): + def __init__(self, controller: RainbirdController, sensor_type): """Initialize the Rain Bird sensor.""" self._sensor_type = sensor_type self._controller = controller @@ -55,12 +48,10 @@ def state(self): def update(self): """Get the latest data and updates the states.""" _LOGGER.debug("Updating sensor: %s", self._name) - if self._sensor_type == "rainsensor": - result = self._controller.currentRainSensorState() - if result and result["type"] == "CurrentRainSensorStateResponse": - self._state = result["sensorState"] - else: - self._state = None + if self._sensor_type == SENSOR_TYPE_RAINSENSOR: + self._state = self._controller.get_rain_sensor_state() + elif self._sensor_type == SENSOR_TYPE_RAINDELAY: + self._state = self._controller.get_rain_delay() @property def name(self): diff --git a/homeassistant/components/rainbird/services.yaml b/homeassistant/components/rainbird/services.yaml new file mode 100644 index 00000000000000..cdac7171a25c8e --- /dev/null +++ b/homeassistant/components/rainbird/services.yaml @@ -0,0 +1,9 @@ +start_irrigation: + description: Start the irrigation + fields: + entity_id: + description: Name of a single irrigation to turn on + example: 'switch.sprinkler_1' + duration: + description: Duration for this sprinkler to be turned on + example: 1 diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 868e8ff4c7d65b..cb4ac83090f127 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -2,61 +2,85 @@ import logging +from pyrainbird import AvailableStations, RainbirdController import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import ( - CONF_FRIENDLY_NAME, - CONF_SCAN_INTERVAL, - CONF_SWITCHES, - CONF_TRIGGER_TIME, - CONF_ZONE, -) +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_TRIGGER_TIME from homeassistant.helpers import config_validation as cv -from . import DATA_RAINBIRD +from . import CONF_ZONES, DATA_RAINBIRD, DOMAIN, RAINBIRD_CONTROLLER -DOMAIN = "rainbird" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +ATTR_DURATION = "duration" + +SERVICE_START_IRRIGATION = "start_irrigation" + +SERVICE_SCHEMA_IRRIGATION = vol.Schema( { - vol.Required(CONF_SWITCHES, default={}): vol.Schema( - { - cv.string: { - vol.Optional(CONF_FRIENDLY_NAME): cv.string, - vol.Required(CONF_ZONE): cv.string, - vol.Required(CONF_TRIGGER_TIME): cv.string, - vol.Optional(CONF_SCAN_INTERVAL): cv.string, - } - } - ) + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_DURATION): vol.All(vol.Coerce(float), vol.Range(min=0)), } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Rain Bird switches over a Rain Bird controller.""" - controller = hass.data[DATA_RAINBIRD] + if discovery_info is None: + return + + controller: RainbirdController = hass.data[DATA_RAINBIRD][ + discovery_info[RAINBIRD_CONTROLLER] + ] + available_stations: AvailableStations = controller.get_available_stations() + if not (available_stations and available_stations.stations): + return devices = [] - for dev_id, switch in config.get(CONF_SWITCHES).items(): - devices.append(RainBirdSwitch(controller, switch, dev_id)) + for zone in range(1, available_stations.stations.count + 1): + if available_stations.stations.active(zone): + zone_config = discovery_info.get(CONF_ZONES, {}).get(zone, {}) + time = zone_config.get(CONF_TRIGGER_TIME, discovery_info[CONF_TRIGGER_TIME]) + name = zone_config.get(CONF_FRIENDLY_NAME) + devices.append( + RainBirdSwitch( + controller, + zone, + time, + name if name else "Sprinkler {}".format(zone), + ) + ) + add_entities(devices, True) + def start_irrigation(service): + entity_id = service.data[ATTR_ENTITY_ID] + duration = service.data[ATTR_DURATION] + + for device in devices: + if device.entity_id == entity_id: + device.turn_on(duration=duration) + + hass.services.register( + DOMAIN, + SERVICE_START_IRRIGATION, + start_irrigation, + schema=SERVICE_SCHEMA_IRRIGATION, + ) + class RainBirdSwitch(SwitchDevice): """Representation of a Rain Bird switch.""" - def __init__(self, rb, dev, dev_id): + def __init__(self, controller: RainbirdController, zone, time, name): """Initialize a Rain Bird Switch Device.""" - self._rainbird = rb - self._devid = dev_id - self._zone = int(dev.get(CONF_ZONE)) - self._name = dev.get(CONF_FRIENDLY_NAME, f"Sprinkler {self._zone}") + self._rainbird = controller + self._zone = zone + self._name = name self._state = None - self._duration = dev.get(CONF_TRIGGER_TIME) - self._attributes = {"duration": self._duration, "zone": self._zone} + self._duration = time + self._attributes = {ATTR_DURATION: self._duration, "zone": self._zone} @property def device_state_attributes(self): @@ -70,27 +94,20 @@ def name(self): def turn_on(self, **kwargs): """Turn the switch on.""" - response = self._rainbird.startIrrigation(int(self._zone), int(self._duration)) - if response and response["type"] == "AcknowledgeResponse": + if self._rainbird.irrigate_zone( + int(self._zone), + int(kwargs[ATTR_DURATION] if ATTR_DURATION in kwargs else self._duration), + ): self._state = True def turn_off(self, **kwargs): """Turn the switch off.""" - response = self._rainbird.stopIrrigation() - if response and response["type"] == "AcknowledgeResponse": + if self._rainbird.stop_irrigation(): self._state = False - def get_device_status(self): - """Get the status of the switch from Rain Bird Controller.""" - 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.""" - self._state = self.get_device_status() + self._state = self._rainbird.get_zone_state(self._zone) @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 5bbd77f4d01b91..3b9eb718737bd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1396,7 +1396,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==0.2.1 +pyrainbird==0.4.1 # homeassistant.components.recswitch pyrecswitch==1.0.2 From c194f4a8137a2ca06cfc5545510c5fe552c4b546 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Thu, 26 Sep 2019 15:23:44 -0400 Subject: [PATCH 0471/3953] Add ecobee services to create and delete vacations (#26923) * Bump pyecobee to 0.1.3 * Add functions, attrs, and services to climate Fixes * Update requirements * Update service strings * Fix typo * Fix log msg string formatting for lint * Change some parameters to Inclusive start_date, start_time, end_date, and end_time must be specified together. * Use built-in convert util for temps * Match other service variable names --- homeassistant/components/ecobee/climate.py | 139 +++++++++++++++++- homeassistant/components/ecobee/manifest.json | 2 +- homeassistant/components/ecobee/services.yaml | 52 +++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 190 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 9eb8e8f26bc6ef..460bd2bb4a4ecf 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -33,12 +33,21 @@ ATTR_TEMPERATURE, TEMP_FAHRENHEIT, ) +from homeassistant.util.temperature import convert import homeassistant.helpers.config_validation as cv from .const import DOMAIN, _LOGGER +ATTR_COOL_TEMP = "cool_temp" +ATTR_END_DATE = "end_date" +ATTR_END_TIME = "end_time" ATTR_FAN_MIN_ON_TIME = "fan_min_on_time" +ATTR_FAN_MODE = "fan_mode" +ATTR_HEAT_TEMP = "heat_temp" ATTR_RESUME_ALL = "resume_all" +ATTR_START_DATE = "start_date" +ATTR_START_TIME = "start_time" +ATTR_VACATION_NAME = "vacation_name" DEFAULT_RESUME_ALL = False PRESET_TEMPERATURE = "temp" @@ -84,13 +93,37 @@ PRESET_HOLD_INDEFINITE: "indefinite", } -SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time" +SERVICE_CREATE_VACATION = "create_vacation" +SERVICE_DELETE_VACATION = "delete_vacation" SERVICE_RESUME_PROGRAM = "resume_program" +SERVICE_SET_FAN_MIN_ON_TIME = "set_fan_min_on_time" -SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema( +DTGROUP_INCLUSIVE_MSG = ( + f"{ATTR_START_DATE}, {ATTR_START_TIME}, {ATTR_END_DATE}, " + f"and {ATTR_END_TIME} must be specified together" +) + +CREATE_VACATION_SCHEMA = vol.Schema( { - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_VACATION_NAME): cv.string, + vol.Required(ATTR_COOL_TEMP): vol.Coerce(float), + vol.Required(ATTR_HEAT_TEMP): vol.Coerce(float), + vol.Inclusive(ATTR_START_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, + vol.Inclusive(ATTR_START_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, + vol.Inclusive(ATTR_END_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, + vol.Inclusive(ATTR_END_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, + vol.Optional(ATTR_FAN_MODE, default="auto"): vol.Any("auto", "on"), + vol.Optional(ATTR_FAN_MIN_ON_TIME, default=0): vol.All( + int, vol.Range(min=0, max=60) + ), + } +) + +DELETE_VACATION_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_VACATION_NAME): cv.string, } ) @@ -101,6 +134,14 @@ } ) +SET_FAN_MIN_ON_TIME_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_FAN_MIN_ON_TIME): vol.Coerce(int), + } +) + + SUPPORT_FLAGS = ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -124,6 +165,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(devices, True) + def create_vacation_service(service): + """Create a vacation on the target thermostat.""" + entity_id = service.data[ATTR_ENTITY_ID] + + for thermostat in devices: + if thermostat.entity_id == entity_id: + thermostat.create_vacation(service.data) + thermostat.schedule_update_ha_state(True) + break + + def delete_vacation_service(service): + """Delete a vacation on the target thermostat.""" + entity_id = service.data[ATTR_ENTITY_ID] + vacation_name = service.data[ATTR_VACATION_NAME] + + for thermostat in devices: + if thermostat.entity_id == entity_id: + thermostat.delete_vacation(vacation_name) + thermostat.schedule_update_ha_state(True) + break + def fan_min_on_time_set_service(service): """Set the minimum fan on time on the target thermostats.""" entity_id = service.data.get(ATTR_ENTITY_ID) @@ -158,6 +220,20 @@ def resume_program_set_service(service): thermostat.schedule_update_ha_state(True) + hass.services.async_register( + DOMAIN, + SERVICE_CREATE_VACATION, + create_vacation_service, + schema=CREATE_VACATION_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_DELETE_VACATION, + delete_vacation_service, + schema=DELETE_VACATION_SCHEMA, + ) + hass.services.async_register( DOMAIN, SERVICE_SET_FAN_MIN_ON_TIME, @@ -536,3 +612,58 @@ def hold_preference(self): # supported; note that this should not include 'indefinite' # as an indefinite away hold is interpreted as away_mode return "nextTransition" + + def create_vacation(self, service_data): + """Create a vacation with user-specified parameters.""" + vacation_name = service_data[ATTR_VACATION_NAME] + cool_temp = convert( + service_data[ATTR_COOL_TEMP], + self.hass.config.units.temperature_unit, + TEMP_FAHRENHEIT, + ) + heat_temp = convert( + service_data[ATTR_HEAT_TEMP], + self.hass.config.units.temperature_unit, + TEMP_FAHRENHEIT, + ) + start_date = service_data.get(ATTR_START_DATE) + start_time = service_data.get(ATTR_START_TIME) + end_date = service_data.get(ATTR_END_DATE) + end_time = service_data.get(ATTR_END_TIME) + fan_mode = service_data[ATTR_FAN_MODE] + fan_min_on_time = service_data[ATTR_FAN_MIN_ON_TIME] + + kwargs = { + key: value + for key, value in { + "start_date": start_date, + "start_time": start_time, + "end_date": end_date, + "end_time": end_time, + "fan_mode": fan_mode, + "fan_min_on_time": fan_min_on_time, + }.items() + if value is not None + } + + _LOGGER.debug( + "Creating a vacation on thermostat %s with name %s, cool temp %s, heat temp %s, " + "and the following other parameters: %s", + self.name, + vacation_name, + cool_temp, + heat_temp, + kwargs, + ) + self.data.ecobee.create_vacation( + self.thermostat_index, vacation_name, cool_temp, heat_temp, **kwargs + ) + + def delete_vacation(self, vacation_name): + """Delete a vacation with the specified name.""" + _LOGGER.debug( + "Deleting a vacation on thermostat %s with name %s", + self.name, + vacation_name, + ) + self.data.ecobee.delete_vacation(self.thermostat_index, vacation_name) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 092594c41fc0d4..131c35d7f89137 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/ecobee", "dependencies": [], - "requirements": ["python-ecobee-api==0.1.2"], + "requirements": ["python-ecobee-api==0.1.3"], "codeowners": ["@marthoc"] } diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml index 87eefed9eaaa43..2155d3cf7d24c3 100644 --- a/homeassistant/components/ecobee/services.yaml +++ b/homeassistant/components/ecobee/services.yaml @@ -1,3 +1,55 @@ +create_vacation: + description: >- + Create a vacation on the selected thermostat. Note: start/end date and time must all be specified + together for these parameters to have an effect. If start/end date and time are not specified, the + vacation will start immediately and last 14 days (unless deleted earlier). + fields: + entity_id: + description: ecobee thermostat on which to create the vacation (required). + example: "climate.kitchen" + vacation_name: + description: Name of the vacation to create; must be unique on the thermostat (required). + example: "Skiing" + cool_temp: + description: Cooling temperature during the vacation (required). + example: 23 + heat_temp: + description: Heating temperature during the vacation (required). + example: 25 + start_date: + description: >- + Date the vacation starts in the YYYY-MM-DD format (optional, immediately if not provided along with + start_time, end_date, and end_time). + example: "2019-03-15" + start_time: + description: Time the vacation starts, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" + example: "20:00:00" + end_date: + description: >- + Date the vacation ends in the YYYY-MM-DD format (optional, 14 days from now if not provided along with + start_date, start_time, and end_time). + example: "2019-03-20" + end_time: + description: Time the vacation ends, in the local time of the thermostat, in the 24-hour format "HH:MM:SS" + example: "20:00:00" + fan_mode: + description: Fan mode of the thermostat during the vacation (auto or on) (optional, auto if not provided). + example: "on" + fan_min_on_time: + description: Minimum number of minutes to run the fan each hour (0 to 60) during the vacation (optional, 0 if not provided). + example: 30 + +delete_vacation: + description: >- + Delete a vacation on the selected thermostat. + fields: + entity_id: + description: ecobee thermostat on which to delete the vacation (required). + example: "climate.kitchen" + vacation_name: + description: Name of the vacation to delete (required). + example: "Skiing" + resume_program: description: Resume the programmed schedule. fields: diff --git a/requirements_all.txt b/requirements_all.txt index 3b9eb718737bd4..19bdc4efd833ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1483,7 +1483,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.1.2 +python-ecobee-api==0.1.3 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d790a423de3888..3a4fa60f15e697 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -356,7 +356,7 @@ pysonos==0.0.23 pyspcwebgw==0.4.0 # homeassistant.components.ecobee -python-ecobee-api==0.1.2 +python-ecobee-api==0.1.3 # homeassistant.components.darksky python-forecastio==1.4.0 From b04a70995ecc1a329801674fd45d8658f731e546 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 27 Sep 2019 00:32:12 +0000 Subject: [PATCH 0472/3953] [ci skip] Translation update --- .../components/adguard/.translations/da.json | 2 +- .../components/adguard/.translations/no.json | 2 +- .../components/adguard/.translations/sl.json | 2 +- .../ambiclimate/.translations/ca.json | 4 +- .../ambiclimate/.translations/ko.json | 2 +- .../ambiclimate/.translations/lb.json | 2 +- .../ambiclimate/.translations/pl.json | 2 +- .../ambiclimate/.translations/ru.json | 2 +- .../ambiclimate/.translations/sl.json | 2 +- .../ambiclimate/.translations/zh-Hant.json | 2 +- .../.translations/zh-Hant.json | 2 +- .../components/auth/.translations/es.json | 2 +- .../axis/.translations/zh-Hant.json | 14 +-- .../binary_sensor/.translations/zh-Hant.json | 92 +++++++++++++++++++ .../components/cast/.translations/no.json | 2 +- .../cast/.translations/zh-Hant.json | 2 +- .../daikin/.translations/zh-Hant.json | 6 +- .../components/deconz/.translations/cs.json | 2 +- .../components/deconz/.translations/es.json | 6 +- .../components/deconz/.translations/no.json | 2 +- .../deconz/.translations/zh-Hant.json | 8 +- .../dialogflow/.translations/cs.json | 2 +- .../dialogflow/.translations/fr.json | 2 +- .../dialogflow/.translations/no.json | 2 +- .../dialogflow/.translations/sl.json | 2 +- .../dialogflow/.translations/zh-Hant.json | 2 +- .../components/ecobee/.translations/da.json | 24 +++++ .../components/ecobee/.translations/no.json | 25 +++++ .../components/ecobee/.translations/ru.json | 25 +++++ .../components/ecobee/.translations/sv.json | 11 +++ .../ecobee/.translations/zh-Hant.json | 25 +++++ .../components/esphome/.translations/es.json | 2 +- .../components/geofency/.translations/no.json | 2 +- .../geofency/.translations/zh-Hant.json | 2 +- .../gpslogger/.translations/no.json | 2 +- .../gpslogger/.translations/zh-Hant.json | 2 +- .../components/heos/.translations/no.json | 2 +- .../heos/.translations/zh-Hant.json | 4 +- .../.translations/zh-Hant.json | 18 ++-- .../.translations/zh-Hant.json | 2 +- .../components/hue/.translations/zh-Hant.json | 2 +- .../components/ifttt/.translations/no.json | 2 +- .../components/ios/.translations/no.json | 2 +- .../components/izone/.translations/no.json | 2 +- .../components/lifx/.translations/no.json | 2 +- .../lifx/.translations/zh-Hant.json | 2 +- .../components/light/.translations/ca.json | 14 +-- .../components/light/.translations/da.json | 4 +- .../components/light/.translations/de.json | 4 +- .../components/light/.translations/hu.json | 17 ++++ .../components/light/.translations/nl.json | 10 +- .../components/light/.translations/pl.json | 6 +- .../components/locative/.translations/it.json | 2 +- .../components/locative/.translations/no.json | 2 +- .../locative/.translations/zh-Hant.json | 2 +- .../logi_circle/.translations/no.json | 2 +- .../logi_circle/.translations/zh-Hant.json | 2 +- .../components/mailgun/.translations/no.json | 2 +- .../mailgun/.translations/zh-Hant.json | 2 +- .../components/mqtt/.translations/no.json | 2 +- .../components/nest/.translations/no.json | 2 +- .../notion/.translations/zh-Hant.json | 2 +- .../owntracks/.translations/no.json | 2 +- .../owntracks/.translations/zh-Hant.json | 2 +- .../components/plaato/.translations/no.json | 2 +- .../plaato/.translations/zh-Hant.json | 2 +- .../components/plex/.translations/da.json | 11 +++ .../components/plex/.translations/en.json | 11 +++ .../components/plex/.translations/no.json | 10 ++ .../components/plex/.translations/ru.json | 11 +++ .../components/plex/.translations/sv.json | 11 +++ .../plex/.translations/zh-Hant.json | 14 ++- .../point/.translations/zh-Hant.json | 2 +- .../components/ps4/.translations/de.json | 2 +- .../components/ps4/.translations/zh-Hant.json | 8 +- .../smartthings/.translations/he.json | 2 +- .../somfy/.translations/zh-Hant.json | 2 +- .../components/sonos/.translations/no.json | 2 +- .../sonos/.translations/zh-Hant.json | 2 +- .../components/tplink/.translations/no.json | 2 +- .../tplink/.translations/zh-Hant.json | 2 +- .../components/traccar/.translations/de.json | 2 +- .../components/traccar/.translations/no.json | 2 +- .../traccar/.translations/zh-Hant.json | 2 +- .../transmission/.translations/da.json | 40 ++++++++ .../transmission/.translations/en.json | 42 ++++----- .../transmission/.translations/no.json | 40 ++++++++ .../transmission/.translations/ru.json | 40 ++++++++ .../transmission/.translations/sv.json | 37 ++++++++ .../components/twilio/.translations/no.json | 2 +- .../twilio/.translations/zh-Hant.json | 2 +- .../unifi/.translations/zh-Hant.json | 2 +- .../components/upnp/.translations/no.json | 2 +- .../upnp/.translations/zh-Hant.json | 4 +- .../components/wemo/.translations/no.json | 2 +- .../components/withings/.translations/sl.json | 2 +- .../withings/.translations/zh-Hant.json | 2 +- .../components/zha/.translations/no.json | 12 +-- .../components/zha/.translations/sv.json | 12 +++ .../components/zha/.translations/zh-Hant.json | 47 +++++++++- .../components/zwave/.translations/no.json | 2 +- 101 files changed, 648 insertions(+), 151 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/zh-Hant.json create mode 100644 homeassistant/components/ecobee/.translations/da.json create mode 100644 homeassistant/components/ecobee/.translations/no.json create mode 100644 homeassistant/components/ecobee/.translations/ru.json create mode 100644 homeassistant/components/ecobee/.translations/sv.json create mode 100644 homeassistant/components/ecobee/.translations/zh-Hant.json create mode 100644 homeassistant/components/light/.translations/hu.json create mode 100644 homeassistant/components/plex/.translations/sv.json create mode 100644 homeassistant/components/transmission/.translations/da.json create mode 100644 homeassistant/components/transmission/.translations/no.json create mode 100644 homeassistant/components/transmission/.translations/ru.json create mode 100644 homeassistant/components/transmission/.translations/sv.json diff --git a/homeassistant/components/adguard/.translations/da.json b/homeassistant/components/adguard/.translations/da.json index 0f854db0be6fe4..813405cec62471 100644 --- a/homeassistant/components/adguard/.translations/da.json +++ b/homeassistant/components/adguard/.translations/da.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Adguard Home, der leveres af Hass.io add-on: {addon}?", + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home, der leveres af Hass.io add-on: {addon}?", "title": "AdGuard Home via Hass.io add-on" }, "user": { diff --git a/homeassistant/components/adguard/.translations/no.json b/homeassistant/components/adguard/.translations/no.json index 94535d7e9450b1..2cd6cd72f6d3e7 100644 --- a/homeassistant/components/adguard/.translations/no.json +++ b/homeassistant/components/adguard/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "existing_instance_updated": "Oppdatert eksisterende konfigurasjon.", - "single_instance_allowed": "Kun \u00e9n enkelt konfigurasjon av AdGuard Hjemer tillatt." + "single_instance_allowed": "Kun en konfigurasjon av AdGuard Hjemer tillatt." }, "error": { "connection_error": "Tilkobling mislyktes." diff --git a/homeassistant/components/adguard/.translations/sl.json b/homeassistant/components/adguard/.translations/sl.json index 5c8d75d4cc8004..f1ca796363d270 100644 --- a/homeassistant/components/adguard/.translations/sl.json +++ b/homeassistant/components/adguard/.translations/sl.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja hass.io add-on {addon} ?", + "description": "\u017delite konfigurirati Home Assistant-a za povezavo z AdGuard Home, ki ga ponuja Hass.io add-on {addon} ?", "title": "AdGuard Home preko dodatka Hass.io" }, "user": { diff --git a/homeassistant/components/ambiclimate/.translations/ca.json b/homeassistant/components/ambiclimate/.translations/ca.json index 054b1a89ae8af2..f446bf7390f976 100644 --- a/homeassistant/components/ambiclimate/.translations/ca.json +++ b/homeassistant/components/ambiclimate/.translations/ca.json @@ -3,7 +3,7 @@ "abort": { "access_token": "S'ha produ\u00eft un error desconegut al generat un testimoni d'acc\u00e9s.", "already_setup": "El compte d\u2019Ambi Climate est\u00e0 configurat.", - "no_config": "Necessites configurar Ambi Climate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Necessites configurar Ambiclimate abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Ambi Climate." @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i Permet l'acc\u00e9s al teu compte de Ambi Climate, despr\u00e9s torna i prem Envia (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})", + "description": "V\u00e9s a l'[enlla\u00e7]({authorization_url}) i Permet l'acc\u00e9s al teu compte de Ambiclimate, despr\u00e9s torna i prem Envia (a sota).\n(Assegura't que l'enlla\u00e7 de retorn \u00e9s el seg\u00fcent {cb_url})", "title": "Autenticaci\u00f3 amb Ambi Climate" } }, diff --git a/homeassistant/components/ambiclimate/.translations/ko.json b/homeassistant/components/ambiclimate/.translations/ko.json index be337bd3f0edfd..3b21726bcbeb13 100644 --- a/homeassistant/components/ambiclimate/.translations/ko.json +++ b/homeassistant/components/ambiclimate/.translations/ko.json @@ -3,7 +3,7 @@ "abort": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070 \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "already_setup": "Ambi Climate \uacc4\uc815\uc774 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "no_config": "Ambi Climate \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Ambi Climate \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/ambiclimate/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + "no_config": "Ambiclimate \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Ambiclimate \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/ambiclimate/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." }, "create_entry": { "default": "Ambi Climate \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/ambiclimate/.translations/lb.json b/homeassistant/components/ambiclimate/.translations/lb.json index a6ce441749d6d3..88be279ae7a260 100644 --- a/homeassistant/components/ambiclimate/.translations/lb.json +++ b/homeassistant/components/ambiclimate/.translations/lb.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Onbekannte Feeler beim gener\u00e9ieren vum Acc\u00e8s Jeton.", "already_setup": "Den Ambiclimate Kont ass konfigur\u00e9iert.", - "no_config": "Dir musst Ambiclimate konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/ambiclimatet/)." + "no_config": "Dir musst Ambiclimate konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen.[Liest w.e.g. d'Instruktioune](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Erfollegr\u00e4ich mat Ambiclimate authentifiz\u00e9iert." diff --git a/homeassistant/components/ambiclimate/.translations/pl.json b/homeassistant/components/ambiclimate/.translations/pl.json index 7ba95b007c995a..675c5e18776ca4 100644 --- a/homeassistant/components/ambiclimate/.translations/pl.json +++ b/homeassistant/components/ambiclimate/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu.", "already_setup": "Konto Ambiclimate jest skonfigurowane.", - "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 w nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 w nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119] (https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Ambiclimate" diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json index a4300e1e5306c5..5a816bce1401d5 100644 --- a/homeassistant/components/ambiclimate/.translations/ru.json +++ b/homeassistant/components/ambiclimate/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambi Climate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambiclimate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/ambiclimate/.translations/sl.json b/homeassistant/components/ambiclimate/.translations/sl.json index cae2e940d561b6..c5d84f030fa127 100644 --- a/homeassistant/components/ambiclimate/.translations/sl.json +++ b/homeassistant/components/ambiclimate/.translations/sl.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Neznana napaka pri ustvarjanju \u017eetona za dostop.", "already_setup": "Ra\u010dun Ambiclimate je konfiguriran.", - "no_config": "Ambiclimat morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Ambiclimate morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Uspe\u0161no overjeno z funkcijo Ambiclimate" diff --git a/homeassistant/components/ambiclimate/.translations/zh-Hant.json b/homeassistant/components/ambiclimate/.translations/zh-Hant.json index 28859cbf5912f7..1539429d0efc04 100644 --- a/homeassistant/components/ambiclimate/.translations/zh-Hant.json +++ b/homeassistant/components/ambiclimate/.translations/zh-Hant.json @@ -6,7 +6,7 @@ "no_config": "\u5fc5\u9808\u5148\u8a2d\u5b9a Ambiclimate \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/ambiclimate/\uff09\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Ambiclimate \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Ambiclimate \u8a2d\u5099\u3002" }, "error": { "follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002", diff --git a/homeassistant/components/ambient_station/.translations/zh-Hant.json b/homeassistant/components/ambient_station/.translations/zh-Hant.json index 7e3ed3ef888509..6c7c88a804576f 100644 --- a/homeassistant/components/ambient_station/.translations/zh-Hant.json +++ b/homeassistant/components/ambient_station/.translations/zh-Hant.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u5df2\u8a3b\u518a", "invalid_key": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" }, "step": { "user": { diff --git a/homeassistant/components/auth/.translations/es.json b/homeassistant/components/auth/.translations/es.json index dd1d6f5437760b..5603c14fe1a774 100644 --- a/homeassistant/components/auth/.translations/es.json +++ b/homeassistant/components/auth/.translations/es.json @@ -13,7 +13,7 @@ "title": "Configure una contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n" }, "setup": { - "description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de ** notificar. {notify_service} **. Por favor introd\u00facela a continuaci\u00f3n:", + "description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de ** notify. {notify_service} **. Por favor introd\u00facela a continuaci\u00f3n:", "title": "Verificar la configuraci\u00f3n" } }, diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json index 1457db2b600276..c0d0df02135192 100644 --- a/homeassistant/components/axis/.translations/zh-Hant.json +++ b/homeassistant/components/axis/.translations/zh-Hant.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", - "not_axis_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Axis \u88dd\u7f6e" + "not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099" }, "error": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_in_progress": "\u88dd\u7f6e\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "device_unavailable": "\u88dd\u7f6e\u7121\u6cd5\u4f7f\u7528", + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "device_unavailable": "\u8a2d\u5099\u7121\u6cd5\u4f7f\u7528", "faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548" }, "step": { @@ -20,9 +20,9 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "title": "\u8a2d\u5b9a Axis \u88dd\u7f6e" + "title": "\u8a2d\u5b9a Axis \u8a2d\u5099" } }, - "title": "Axis \u88dd\u7f6e" + "title": "Axis \u8a2d\u5099" } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/zh-Hant.json b/homeassistant/components/binary_sensor/.translations/zh-Hant.json new file mode 100644 index 00000000000000..36c72dcb9e69b4 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/zh-Hant.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \u96fb\u91cf\u904e\u4f4e", + "is_cold": "{entity_name} \u51b7", + "is_connected": "{entity_name} \u5df2\u9023\u7dda", + "is_gas": "{entity_name} \u5075\u6e2c\u5230\u6c23\u9ad4", + "is_hot": "{entity_name} \u71b1", + "is_light": "{entity_name} \u5075\u6e2c\u5230\u5149\u7dda\u4e2d", + "is_locked": "{entity_name} \u5df2\u4e0a\u9396", + "is_moist": "{entity_name} \u6f6e\u6fd5", + "is_motion": "{entity_name} \u5075\u6e2c\u5230\u52d5\u4f5c\u4e2d", + "is_moving": "{entity_name} \u79fb\u52d5\u4e2d", + "is_no_gas": "{entity_name} \u672a\u5075\u6e2c\u5230\u6c23\u9ad4", + "is_no_light": "{entity_name} \u672a\u5075\u6e2c\u5230\u5149\u7dda", + "is_no_motion": "{entity_name} \u672a\u5075\u6e2c\u5230\u52d5\u4f5c", + "is_no_problem": "{entity_name} \u672a\u5075\u6e2c\u5230\u554f\u984c", + "is_no_smoke": "{entity_name} \u672a\u5075\u6e2c\u5230\u7159\u9727", + "is_no_sound": "{entity_name} \u672a\u5075\u6e2c\u5230\u8072\u97f3", + "is_no_vibration": "{entity_name} \u672a\u5075\u6e2c\u5230\u9707\u52d5", + "is_not_bat_low": "{entity_name} \u96fb\u91cf\u6b63\u5e38", + "is_not_cold": "{entity_name} \u4e0d\u51b7", + "is_not_connected": "{entity_name} \u65b7\u7dda", + "is_not_hot": "{entity_name} \u4e0d\u71b1", + "is_not_locked": "{entity_name} \u89e3\u9396", + "is_not_moist": "{entity_name} \u4e7e\u71e5", + "is_not_moving": "{entity_name} \u672a\u5728\u79fb\u52d5", + "is_not_occupied": "{entity_name} \u672a\u6709\u4eba", + "is_not_open": "{entity_name} \u95dc\u9589", + "is_not_plugged_in": "{entity_name} \u672a\u63d2\u5165", + "is_not_powered": "{entity_name} \u672a\u901a\u96fb", + "is_not_present": "{entity_name} \u672a\u51fa\u73fe", + "is_not_unsafe": "{entity_name} \u5b89\u5168", + "is_occupied": "{entity_name} \u6709\u4eba", + "is_off": "{entity_name} \u95dc\u9589", + "is_on": "{entity_name} \u958b\u555f", + "is_open": "{entity_name} \u958b\u555f", + "is_plugged_in": "{entity_name} \u63d2\u5165", + "is_powered": "{entity_name} \u901a\u96fb", + "is_present": "{entity_name} \u51fa\u73fe", + "is_problem": "{entity_name} \u6b63\u5075\u6e2c\u5230\u554f\u984c", + "is_smoke": "{entity_name} \u6b63\u5075\u6e2c\u5230\u7159\u9727", + "is_sound": "{entity_name} \u6b63\u5075\u6e2c\u5230\u8072\u97f3", + "is_unsafe": "{entity_name} \u4e0d\u5b89\u5168", + "is_vibration": "{entity_name} \u6b63\u5075\u6e2c\u5230\u9707\u52d5" + }, + "trigger_type": { + "bat_low": "{entity_name} \u96fb\u91cf\u4f4e", + "closed": "{entity_name} \u5df2\u95dc\u9589", + "cold": "{entity_name} \u5df2\u8b8a\u51b7", + "connected": "{entity_name} \u5df2\u9023\u7dda", + "gas": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u6c23\u9ad4", + "hot": "{entity_name} \u5df2\u8b8a\u71b1", + "light": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", + "locked": "{entity_name} \u5df2\u4e0a\u9396", + "moist\u00a7": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5", + "motion": "{entity_name} \u5df2\u5075\u6e2c\u5230\u52d5\u4f5c", + "moving": "{entity_name} \u958b\u59cb\u79fb\u52d5", + "no_gas": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u6c23\u9ad4", + "no_light": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u5149\u7dda", + "no_motion": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u52d5\u4f5c", + "no_problem": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u554f\u984c", + "no_smoke": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u7159\u9727", + "no_sound": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u8072\u97f3", + "no_vibration": "{entity_name} \u5df2\u505c\u6b62\u5075\u6e2c\u9707\u52d5", + "not_bat_low": "{entity_name} \u96fb\u91cf\u6b63\u5e38", + "not_cold": "{entity_name} \u5df2\u4e0d\u51b7", + "not_connected": "{entity_name} \u5df2\u65b7\u7dda", + "not_hot": "{entity_name} \u5df2\u4e0d\u71b1", + "not_locked": "{entity_name} \u5df2\u89e3\u9396", + "not_moist": "{entity_name} \u5df2\u8b8a\u4e7e", + "not_moving": "{entity_name} \u505c\u6b62\u79fb\u52d5", + "not_occupied": "{entity_name} \u672a\u6709\u4eba", + "not_plugged_in": "{entity_name} \u672a\u63d2\u5165", + "not_powered": "{entity_name} \u672a\u901a\u96fb", + "not_present": "{entity_name} \u672a\u51fa\u73fe", + "not_unsafe": "{entity_name} \u5df2\u5b89\u5168", + "occupied": "{entity_name} \u8b8a\u6210\u6709\u4eba", + "opened": "{entity_name} \u5df2\u958b\u555f", + "plugged_in": "{entity_name} \u5df2\u63d2\u5165", + "powered": "{entity_name} \u5df2\u901a\u96fb", + "present": "{entity_name} \u5df2\u51fa\u73fe", + "problem": "{entity_name} \u5df2\u5075\u6e2c\u5230\u554f\u984c", + "smoke": "{entity_name} \u5df2\u5075\u6e2c\u5230\u7159\u9727", + "sound": "{entity_name} \u5df2\u5075\u6e2c\u5230\u8072\u97f3", + "turned_off": "{entity_name} \u5df2\u95dc\u9589", + "turned_on": "{entity_name} \u5df2\u958b\u555f", + "unsafe": "{entity_name} \u5df2\u4e0d\u5b89\u5168", + "vibration": "{entity_name} \u5df2\u5075\u6e2c\u5230\u9707\u52d5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/no.json b/homeassistant/components/cast/.translations/no.json index d36c929e7211b5..6b8166f23c0cd4 100644 --- a/homeassistant/components/cast/.translations/no.json +++ b/homeassistant/components/cast/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen Google Cast enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av Google Cast er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av Google Cast er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/cast/.translations/zh-Hant.json b/homeassistant/components/cast/.translations/zh-Hant.json index d5383fb1a2bdba..711ac3203978c6 100644 --- a/homeassistant/components/cast/.translations/zh-Hant.json +++ b/homeassistant/components/cast/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Google Cast \u8a2d\u5099\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Google Cast \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/daikin/.translations/zh-Hant.json b/homeassistant/components/daikin/.translations/zh-Hant.json index 1699bcad8f08bd..457b7d1b89c523 100644 --- a/homeassistant/components/daikin/.translations/zh-Hant.json +++ b/homeassistant/components/daikin/.translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "device_fail": "\u5275\u5efa\u88dd\u7f6e\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002", - "device_timeout": "\u9023\u7dda\u81f3\u88dd\u7f6e\u903e\u6642\u3002" + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "device_fail": "\u5275\u5efa\u8a2d\u5099\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002", + "device_timeout": "\u9023\u7dda\u81f3\u8a2d\u5099\u903e\u6642\u3002" }, "step": { "user": { diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json index 0f4bdf98ac14d5..42b14833aa0d54 100644 --- a/homeassistant/components/deconz/.translations/cs.json +++ b/homeassistant/components/deconz/.translations/cs.json @@ -23,7 +23,7 @@ "options": { "data": { "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel", - "allow_deconz_groups": "Povolit import skupin deCONZ " + "allow_deconz_groups": "Povolit import skupin deCONZ" }, "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ" } diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 1bc6c8211a268d..cb5db0b8348ea4 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -59,13 +59,13 @@ }, "trigger_type": { "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces consecutivas", - "remote_button_long_press": "bot\u00f3n \"{subtype}\" pulsado continuamente", + "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de un rato pulsado", "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", - "remote_button_short_press": "bot\u00f3n \"{subtype}\" pulsado", - "remote_button_short_release": "bot\u00f3n \"{subtype}\" liberado", + "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", + "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_gyro_activated": "Dispositivo sacudido" } diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 3968c1f00c58d2..c7079fd62193e7 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -65,7 +65,7 @@ "remote_button_quintuple_press": "\" {subtype} \" - knappen femdobbelt klikket", "remote_button_rotated": "Knappen roterte \" {subtype} \"", "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", - "remote_button_short_release": "\" {subtype} \" -knappen ble utgitt", + "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\" {subtype} \"-knappen trippel klikket", "remote_gyro_activated": "Enhet er ristet" } diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index f024386aa0f873..bd47a637761ccd 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -4,9 +4,9 @@ "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Bridge \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", - "not_deconz_bridge": "\u975e deCONZ Bridge \u88dd\u7f6e", + "not_deconz_bridge": "\u975e deCONZ Bridge \u8a2d\u5099", "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u7269\u4ef6", - "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u5be6\u4f8b" + "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u7269\u4ef6" }, "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" @@ -77,14 +77,14 @@ "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b" + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b" }, "deconz_devices": { "data": { "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b" + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b" } } } diff --git a/homeassistant/components/dialogflow/.translations/cs.json b/homeassistant/components/dialogflow/.translations/cs.json index 21da9b4823b6ec..db41ee98e01740 100644 --- a/homeassistant/components/dialogflow/.translations/cs.json +++ b/homeassistant/components/dialogflow/.translations/cs.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Povolena je pouze jedna instance." }, "create_entry": { - "default": "Chcete-li odeslat ud\u00e1losti do aplikace Home Assistant, budete muset nastavit [integraci Dialogflow]({dialogflow_url}). \n\n Vypl\u0148te n\u00e1sleduj\u00edc\u00ed informace: \n\n - URL: `{webhook_url}' \n - Metoda: POST \n - Typ obsahu: aplikace/json \n\n Podrobn\u011bj\u0161\u00ed informace naleznete v [dokumentaci]({docs_url})." + "default": "Chcete-li odeslat ud\u00e1losti do aplikace Home Assistant, budete muset nastavit [integraci Dialogflow]({dialogflow_url}). \n\n Vypl\u0148te n\u00e1sleduj\u00edc\u00ed informace: \n\n - URL: `{webhook_url}' \n - Metoda: POST \n - Typ obsahu: application/json\n\n Podrobn\u011bj\u0161\u00ed informace naleznete v [dokumentaci]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/fr.json b/homeassistant/components/dialogflow/.translations/fr.json index e9eabeff6381d9..0be75b94be94b9 100644 --- a/homeassistant/components/dialogflow/.translations/fr.json +++ b/homeassistant/components/dialogflow/.translations/fr.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Une seule instance est n\u00e9cessaire." }, "create_entry": { - "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer [Webhooks avec Mailgun] ( {dialogflow_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez configurer [Webhooks avec Dialogflow] ( {dialogflow_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/no.json b/homeassistant/components/dialogflow/.translations/no.json index e27d59a40e3590..4d23ac8aaba79b 100644 --- a/homeassistant/components/dialogflow/.translations/no.json +++ b/homeassistant/components/dialogflow/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta Dialogflow meldinger.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [webhook integrasjon av Dialogflow]({dialogflow_url}). \n\nFyll ut f\u00f8lgende informasjon: \n\n- URL: `{webhook_url}` \n- Metode: POST\n- Innholdstype: application/json\n\nSe [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/dialogflow/.translations/sl.json b/homeassistant/components/dialogflow/.translations/sl.json index 18a476b6870ebe..b6bd30f7997a2c 100644 --- a/homeassistant/components/dialogflow/.translations/sl.json +++ b/homeassistant/components/dialogflow/.translations/sl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Potrebna je samo ena instanca." }, "create_entry": { - "default": "Za po\u0161iljanje dogodkov Home Assistant-u, boste morali nastaviti [webhook z dialogflow]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) za nadaljna navodila." + "default": "Za po\u0161iljanje dogodkov Home Assistant-u, boste morali nastaviti [webhook z Dialogflow]({twilio_url}).\n\nIzpolnite naslednje informacije:\n\n- URL: `{webhook_url}`\n- Metoda: POST\n- Vrsta vsebine: application/x-www-form-urlencoded\n\nGlej [dokumentacijo]({docs_url}) za nadaljna navodila." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/zh-Hant.json b/homeassistant/components/dialogflow/.translations/zh-Hant.json index 18d3d92e16b55e..3cb54145ad857d 100644 --- a/homeassistant/components/dialogflow/.translations/zh-Hant.json +++ b/homeassistant/components/dialogflow/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Dialogflow \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Dialogflow \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/ecobee/.translations/da.json b/homeassistant/components/ecobee/.translations/da.json new file mode 100644 index 00000000000000..7a42a9470db52a --- /dev/null +++ b/homeassistant/components/ecobee/.translations/da.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "one_instance_only": "Integrationen underst\u00f8tter kun \u00e9n ecobee forekomst" + }, + "error": { + "pin_request_failed": "Fejl ved anmodning om pinkode fra ecobee. Kontroller at API-n\u00f8glen er korrekt.", + "token_request_failed": "Fejl ved anmodning om tokens fra ecobee. Pr\u00f8v igen." + }, + "step": { + "authorize": { + "title": "Autoriser app p\u00e5 ecobee.com" + }, + "user": { + "data": { + "api_key": "API-n\u00f8gle" + }, + "description": "Indtast API-n\u00f8glen, du har f\u00e5et fra ecobee.com.", + "title": "ecobee API-n\u00f8gle" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/no.json b/homeassistant/components/ecobee/.translations/no.json new file mode 100644 index 00000000000000..2bf141f6489288 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare en ecobee-forekomst." + }, + "error": { + "pin_request_failed": "Feil under foresp\u00f8rsel om PIN-kode fra ecobee. Kontroller at API-n\u00f8kkelen er riktig.", + "token_request_failed": "Feil ved foresp\u00f8rsel om tokener fra ecobee; Pr\u00f8v p\u00e5 nytt." + }, + "step": { + "authorize": { + "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nDeretter, trykk p\u00e5 Send.", + "title": "Autoriser app p\u00e5 ecobee.com" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Vennligst skriv inn API-n\u00f8kkel som er innhentet fra ecobee.com.", + "title": "ecobee API-n\u00f8kkel" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/ru.json b/homeassistant/components/ecobee/.translations/ru.json new file mode 100644 index 00000000000000..660e0064bb6a74 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 ecobee." + }, + "error": { + "pin_request_failed": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 PIN-\u043a\u043e\u0434\u0430 \u0443 ecobee; \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043a\u043b\u044e\u0447\u0430 API.", + "token_request_failed": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0442\u043e\u043a\u0435\u043d\u043e\u0432 \u0443 ecobee; \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437." + }, + "step": { + "authorize": { + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://www.ecobee.com/consumerportal/index.html \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e PIN-\u043a\u043e\u0434\u0430: \n\n {pin} \n \n \u0417\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0430 ecobee.com" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 API, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043e\u0442 ecobee.com.", + "title": "ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/sv.json b/homeassistant/components/ecobee/.translations/sv.json new file mode 100644 index 00000000000000..f4a63bb449d1f8 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/zh-Hant.json b/homeassistant/components/ecobee/.translations/zh-Hant.json new file mode 100644 index 00000000000000..e1eb6ebd3570b1 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\u6b64\u6574\u5408\u76ee\u524d\u50c5\u652f\u63f4\u4e00\u7d44 ecobee \u7269\u4ef6" + }, + "error": { + "pin_request_failed": "ecobee \u6240\u9700\u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d\u5bc6\u9470\u6b63\u78ba\u6027\u3002", + "token_request_failed": "ecobee \u6240\u9700\u5bc6\u9470\u932f\u8aa4\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" + }, + "step": { + "authorize": { + "description": "\u8acb\u65bc https://www.ecobee.com/consumerportal/index.html \u8f38\u5165\u4e0b\u65b9\u4ee3\u78bc\uff0c\u8a8d\u8b49\u6b64 App\uff1a\n\n{pin}\n\n\u7136\u5f8c\u6309\u4e0b\u300cSubmit\u300d\u3002", + "title": "\u65bc ecobee.com \u4e0a\u8a8d\u8b49 App" + }, + "user": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "description": "\u8acb\u8f38\u5165\u7531 ecobee.com \u6240\u7372\u5f97\u7684 API \u5bc6\u9470\u3002", + "title": "ecobee API \u5bc6\u9470" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/es.json b/homeassistant/components/esphome/.translations/es.json index 70d766cf4c0544..be8033f316a2c9 100644 --- a/homeassistant/components/esphome/.translations/es.json +++ b/homeassistant/components/esphome/.translations/es.json @@ -8,7 +8,7 @@ "invalid_password": "\u00a1Contrase\u00f1a incorrecta!", "resolve_error": "No se puede resolver la direcci\u00f3n de ESP. Si el error persiste, configura una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, - "flow_title": "Desplom\u00e9: {name}", + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/geofency/.translations/no.json b/homeassistant/components/geofency/.translations/no.json index 4409616cef497d..1956c453a9f084 100644 --- a/homeassistant/components/geofency/.translations/no.json +++ b/homeassistant/components/geofency/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Geofency.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 kunne sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Geofency. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/geofency/.translations/zh-Hant.json b/homeassistant/components/geofency/.translations/zh-Hant.json index bec33c26d100b6..003a3db8bf181b 100644 --- a/homeassistant/components/geofency/.translations/zh-Hant.json +++ b/homeassistant/components/geofency/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Geofency \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Geofency \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/gpslogger/.translations/no.json b/homeassistant/components/gpslogger/.translations/no.json index 836b5c8bc687eb..488a09c3768693 100644 --- a/homeassistant/components/gpslogger/.translations/no.json +++ b/homeassistant/components/gpslogger/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra GPSLogger.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i GPSLogger. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/gpslogger/.translations/zh-Hant.json b/homeassistant/components/gpslogger/.translations/zh-Hant.json index c9d98da1afcf0e..c21e76a6eee209 100644 --- a/homeassistant/components/gpslogger/.translations/zh-Hant.json +++ b/homeassistant/components/gpslogger/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 GPSLogger \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 GPSLogger \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/heos/.translations/no.json b/homeassistant/components/heos/.translations/no.json index dd4cb48a0906c1..d41051b6674eb8 100644 --- a/homeassistant/components/heos/.translations/no.json +++ b/homeassistant/components/heos/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Du kan kun konfigurere en enkelt Heos tilkobling, da den st\u00f8tter alle enhetene p\u00e5 nettverket." + "already_setup": "Du kan kun konfigurere en Heos tilkobling, da den st\u00f8tter alle enhetene p\u00e5 nettverket." }, "error": { "connection_failure": "Kan ikke koble til den angitte verten." diff --git a/homeassistant/components/heos/.translations/zh-Hant.json b/homeassistant/components/heos/.translations/zh-Hant.json index c45f9c467e4a8e..9efacaf163f9d2 100644 --- a/homeassistant/components/heos/.translations/zh-Hant.json +++ b/homeassistant/components/heos/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Heos \u9023\u7dda\uff0c\u5c07\u652f\u63f4\u7db2\u8def\u4e2d\u6240\u6709\u5c0d\u61c9\u88dd\u7f6e\u3002" + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Heos \u9023\u7dda\uff0c\u5c07\u652f\u63f4\u7db2\u8def\u4e2d\u6240\u6709\u5c0d\u61c9\u8a2d\u5099\u3002" }, "error": { "connection_failure": "\u7121\u6cd5\u9023\u7dda\u81f3\u6307\u5b9a\u4e3b\u6a5f\u7aef\u3002" @@ -12,7 +12,7 @@ "access_token": "\u4e3b\u6a5f\u7aef", "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u6bb5\u540d\u7a31\u6216 Heos \u88dd\u7f6e IP \u4f4d\u5740\uff08\u5df2\u900f\u904e\u6709\u7dda\u7db2\u8def\u9023\u7dda\uff09\u3002", + "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u6bb5\u540d\u7a31\u6216 Heos \u8a2d\u5099 IP \u4f4d\u5740\uff08\u5df2\u900f\u904e\u6709\u7dda\u7db2\u8def\u9023\u7dda\uff09\u3002", "title": "\u9023\u7dda\u81f3 Heos" } }, diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hant.json b/homeassistant/components/homekit_controller/.translations/zh-Hant.json index 7340569e64ff74..68e87e9aea8bee 100644 --- a/homeassistant/components/homekit_controller/.translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/.translations/zh-Hant.json @@ -1,20 +1,20 @@ { "config": { "abort": { - "accessory_not_found_error": "\u627e\u4e0d\u5230\u88dd\u7f6e\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002", + "accessory_not_found_error": "\u627e\u4e0d\u5230\u8a2d\u5099\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002", "already_configured": "\u914d\u4ef6\u5df2\u7d93\u7531\u6b64\u63a7\u5236\u5668\u8a2d\u5b9a\u5b8c\u6210", - "already_in_progress": "\u88dd\u7f6e\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u88dd\u7f6e\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", + "already_in_progress": "\u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", + "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u8a2d\u5099\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", "ignored_model": "\u7531\u65bc\u6b64\u578b\u865f\u53ef\u539f\u751f\u652f\u63f4\u66f4\u5b8c\u6574\u529f\u80fd\uff0c\u56e0\u6b64 Homekit \u652f\u63f4\u5df2\u88ab\u7981\u6b62\u3002", - "invalid_config_entry": "\u88dd\u7f6e\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", - "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u88dd\u7f6e" + "invalid_config_entry": "\u8a2d\u5099\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002", + "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u8a2d\u5099" }, "error": { "authentication_error": "Homekit \u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u5b9a\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", - "busy_error": "\u88dd\u7f6e\u5df2\u7d93\u8207\u5176\u4ed6\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "max_peers_error": "\u88dd\u7f6e\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "max_tries_error": "\u88dd\u7f6e\u6536\u5230\u8d85\u904e 100 \u6b21\u672a\u6210\u529f\u8a8d\u8b49\u5f8c\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "pairing_failed": "\u7576\u8a66\u5716\u8207\u88dd\u7f6e\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u88dd\u7f6e\u76ee\u524d\u4e0d\u652f\u63f4\u3002", + "busy_error": "\u8a2d\u5099\u5df2\u7d93\u8207\u5176\u4ed6\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", + "max_peers_error": "\u8a2d\u5099\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", + "max_tries_error": "\u8a2d\u5099\u6536\u5230\u8d85\u904e 100 \u6b21\u672a\u6210\u529f\u8a8d\u8b49\u5f8c\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", + "pairing_failed": "\u7576\u8a66\u5716\u8207\u8a2d\u5099\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u8a2d\u5099\u76ee\u524d\u4e0d\u652f\u63f4\u3002", "unable_to_pair": "\u7121\u6cd5\u914d\u5c0d\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "unknown_error": "\u88dd\u7f6e\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002" }, diff --git a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json index f95ccf572723d9..c6a960f1a68367 100644 --- a/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json +++ b/homeassistant/components/homematicip_cloud/.translations/zh-Hant.json @@ -15,7 +15,7 @@ "init": { "data": { "hapid": "Access point ID (SGTIN)", - "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u88dd\u7f6e\u7684\u5b57\u9996\u7528\uff09", + "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u8a2d\u5099\u7684\u5b57\u9996\u7528\uff09", "pin": "PIN \u78bc\uff08\u9078\u9805\uff09" }, "title": "\u9078\u64c7 HomematicIP Access point" diff --git a/homeassistant/components/hue/.translations/zh-Hant.json b/homeassistant/components/hue/.translations/zh-Hant.json index 7a08b44f25afa7..6bbe75a8019b33 100644 --- a/homeassistant/components/hue/.translations/zh-Hant.json +++ b/homeassistant/components/hue/.translations/zh-Hant.json @@ -7,7 +7,7 @@ "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Bridge", "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", "no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge", - "not_hue_bridge": "\u975e Hue Bridge \u88dd\u7f6e", + "not_hue_bridge": "\u975e Hue Bridge \u8a2d\u5099", "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/ifttt/.translations/no.json b/homeassistant/components/ifttt/.translations/no.json index 481ab372e91d0a..2fe38659fadb93 100644 --- a/homeassistant/components/ifttt/.translations/no.json +++ b/homeassistant/components/ifttt/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant enhet m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta IFTTT-meldinger.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du bruke \"Make a web request\" handlingen fra [IFTTT Webhook applet]({applet_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." diff --git a/homeassistant/components/ios/.translations/no.json b/homeassistant/components/ios/.translations/no.json index a125b96a070ac5..798ae93d33b2dd 100644 --- a/homeassistant/components/ios/.translations/no.json +++ b/homeassistant/components/ios/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Kun en enkelt konfigurasjon av Home Assistant iOS er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av Home Assistant iOS er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/izone/.translations/no.json b/homeassistant/components/izone/.translations/no.json index fcd5c1df019a6b..9068b18c82df33 100644 --- a/homeassistant/components/izone/.translations/no.json +++ b/homeassistant/components/izone/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Finner ingen iZone-enheter p\u00e5 nettverket.", - "single_instance_allowed": "Bare en enkelt konfigurasjon av iZone er n\u00f8dvendig." + "single_instance_allowed": "Bare en konfigurasjon av iZone er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/.translations/no.json b/homeassistant/components/lifx/.translations/no.json index 63080a30ff16a3..ae32d43f1b6aaf 100644 --- a/homeassistant/components/lifx/.translations/no.json +++ b/homeassistant/components/lifx/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen LIFX-enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av LIFX er mulig." + "single_instance_allowed": "Kun en konfigurasjon av LIFX er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/.translations/zh-Hant.json b/homeassistant/components/lifx/.translations/zh-Hant.json index 4c66f0d01333d3..908394bcfd81c0 100644 --- a/homeassistant/components/lifx/.translations/zh-Hant.json +++ b/homeassistant/components/lifx/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 LIFX \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 LIFX \u8a2d\u5099\u3002", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 LIFX\u3002" }, "step": { diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json index c9b727088ab180..b8b3bbb8125e8b 100644 --- a/homeassistant/components/light/.translations/ca.json +++ b/homeassistant/components/light/.translations/ca.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Commuta {name}", - "turn_off": "Apaga {name}", - "turn_on": "Enc\u00e9n {name}" + "toggle": "Commuta {entity_name}", + "turn_off": "Apaga {entity_name}", + "turn_on": "Enc\u00e9n {entity_name}" }, "condition_type": { - "is_off": "{name} est\u00e0 apagat", - "is_on": "{name} est\u00e0 enc\u00e8s" + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s" }, "trigger_type": { - "turned_off": "{name} apagat", - "turned_on": "{name} enc\u00e8s" + "turned_off": "{entity_name} apagat", + "turned_on": "{entity_name} enc\u00e8s" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json index 4ea4a94014ea83..14a747f6effeb8 100644 --- a/homeassistant/components/light/.translations/da.json +++ b/homeassistant/components/light/.translations/da.json @@ -1,8 +1,8 @@ { "device_automation": { "trigger_type": { - "turned_off": "{name} slukket", - "turned_on": "{name} t\u00e6ndt" + "turned_off": "{entity_name} slukket", + "turned_on": "{entity_name} t\u00e6ndt" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json index 2fe1c6b42dcd42..e07adeb0a36e84 100644 --- a/homeassistant/components/light/.translations/de.json +++ b/homeassistant/components/light/.translations/de.json @@ -1,8 +1,8 @@ { "device_automation": { "trigger_type": { - "turned_off": "{name} ausgeschaltet", - "turned_on": "{name} eingeschaltet" + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/hu.json b/homeassistant/components/light/.translations/hu.json new file mode 100644 index 00000000000000..7d7e158f3cb064 --- /dev/null +++ b/homeassistant/components/light/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} fel/lekapcsol\u00e1sa", + "turn_off": "{entity_name} lekapcsol\u00e1sa", + "turn_on": "{entity_name} felkapcsol\u00e1sa" + }, + "condition_type": { + "is_off": "{entity_name} le van kapcsolva", + "is_on": "{entity_name} fel van kapcsolva" + }, + "trigger_type": { + "turned_off": "{entity_name} le lett kapcsolva", + "turned_on": "{entity_name} fel lett kapcsolva" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/nl.json b/homeassistant/components/light/.translations/nl.json index 63954ca83a9fa1..e742a337cca0fa 100644 --- a/homeassistant/components/light/.translations/nl.json +++ b/homeassistant/components/light/.translations/nl.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Omschakelen {naam}", + "toggle": "Omschakelen {entity_name}", "turn_off": "{entity_name} uitschakelen", "turn_on": "{entity_name} inschakelen" }, "condition_type": { - "is_off": "{name} is uitgeschakeld", - "is_on": "{name} is ingeschakeld" + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld" }, "trigger_type": { - "turned_off": "{name} is uitgeschakeld", - "turned_on": "{name} is ingeschakeld" + "turned_off": "{entity_name} is uitgeschakeld", + "turned_on": "{entity_name} is ingeschakeld" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index f8f4a2761d4624..17c81c471f5018 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -7,11 +7,11 @@ }, "condition_type": { "is_off": "{entity_name} jest wy\u0142\u0105czone", - "is_on": "(entity_name} jest w\u0142\u0105czony." + "is_on": "{entity_name} jest w\u0142\u0105czony." }, "trigger_type": { - "turned_off": "{nazwa} wy\u0142\u0105czone", - "turned_on": "{name} w\u0142\u0105czone" + "turned_off": "{entity_name} wy\u0142\u0105czone", + "turned_on": "{entity_name} w\u0142\u0105czone" } } } \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/it.json b/homeassistant/components/locative/.translations/it.json index de62d2ac2f772b..4fdef0a987e0ab 100644 --- a/homeassistant/components/locative/.translations/it.json +++ b/homeassistant/components/locative/.translations/it.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { - "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." + "default": "Per inviare localit\u00e0 a Home Assistant, dovrai configurare la funzionalit\u00e0 Webhook nell'app Locative.\n\n Compila le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Method: POST \n\n Vedi [la documentazione]({docs_url}) for ulteriori dettagli." }, "step": { "user": { diff --git a/homeassistant/components/locative/.translations/no.json b/homeassistant/components/locative/.translations/no.json index 8e9b3272f947b3..c5ad304300479f 100644 --- a/homeassistant/components/locative/.translations/no.json +++ b/homeassistant/components/locative/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Geofency.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 kunne sende steder til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Locative. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/locative/.translations/zh-Hant.json b/homeassistant/components/locative/.translations/zh-Hant.json index 62bb6bb9d962a4..7dd598c8fc217d 100644 --- a/homeassistant/components/locative/.translations/zh-Hant.json +++ b/homeassistant/components/locative/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Locative \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Locative \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/logi_circle/.translations/no.json b/homeassistant/components/logi_circle/.translations/no.json index c68f49509baae8..23b951bfa621bf 100644 --- a/homeassistant/components/logi_circle/.translations/no.json +++ b/homeassistant/components/logi_circle/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Du kan bare konfigurere en enkelt Logi Circle konto.", + "already_setup": "Du kan bare konfigurere en Logi Circle konto.", "external_error": "Det oppstod et unntak fra en annen flow.", "external_setup": "Logi Circle er vellykket konfigurert fra en annen flow.", "no_flows": "Du m\u00e5 konfigurere Logi Circle f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/logi_circle/)." diff --git a/homeassistant/components/logi_circle/.translations/zh-Hant.json b/homeassistant/components/logi_circle/.translations/zh-Hant.json index b9f82b6e2e507a..1eb3b71c942d77 100644 --- a/homeassistant/components/logi_circle/.translations/zh-Hant.json +++ b/homeassistant/components/logi_circle/.translations/zh-Hant.json @@ -7,7 +7,7 @@ "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Logi Circle \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/logi_circle/\uff09\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Logi Circle \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Logi Circle \u8a2d\u5099\u3002" }, "error": { "auth_error": "API \u8a8d\u8b49\u5931\u6557\u3002", diff --git a/homeassistant/components/mailgun/.translations/no.json b/homeassistant/components/mailgun/.translations/no.json index 91c616b69af789..3d1cbd41a34172 100644 --- a/homeassistant/components/mailgun/.translations/no.json +++ b/homeassistant/components/mailgun/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 motta Mailgun-meldinger.", - "one_instance_allowed": "Kun \u00e9n enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [Webhooks with Mailgun]({mailgun_url}).\n\nFyll ut f\u00f8lgende informasjon:\n\n- URL: `{webhook_url}`\n- Metode: POST\n- Innholdstype: application/json\n\nSe [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." diff --git a/homeassistant/components/mailgun/.translations/zh-Hant.json b/homeassistant/components/mailgun/.translations/zh-Hant.json index 4b9ab3a7abb782..f16533312fa21a 100644 --- a/homeassistant/components/mailgun/.translations/zh-Hant.json +++ b/homeassistant/components/mailgun/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Mailgun \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json index b3f1e4740b9e67..99a760dce4a273 100644 --- a/homeassistant/components/mqtt/.translations/no.json +++ b/homeassistant/components/mqtt/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Kun en enkelt konfigurasjon av MQTT er tillatt." + "single_instance_allowed": "Kun en konfigurasjon av MQTT er tillatt." }, "error": { "cannot_connect": "Kan ikke koble til megleren." diff --git a/homeassistant/components/nest/.translations/no.json b/homeassistant/components/nest/.translations/no.json index 03cf1a82b813bf..743c47e00c8f46 100644 --- a/homeassistant/components/nest/.translations/no.json +++ b/homeassistant/components/nest/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Du kan bare konfigurere en enkelt Nest konto.", + "already_setup": "Du kan bare konfigurere en Nest konto.", "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", "no_flows": "Du m\u00e5 konfigurere Nest f\u00f8r du kan autentisere med den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/nest/)." diff --git a/homeassistant/components/notion/.translations/zh-Hant.json b/homeassistant/components/notion/.translations/zh-Hant.json index af89dd3d39b4a5..f672f519f408de 100644 --- a/homeassistant/components/notion/.translations/zh-Hant.json +++ b/homeassistant/components/notion/.translations/zh-Hant.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "\u4f7f\u7528\u8005\u540d\u7a31\u5df2\u8a3b\u518a", "invalid_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" }, "step": { "user": { diff --git a/homeassistant/components/owntracks/.translations/no.json b/homeassistant/components/owntracks/.translations/no.json index 9f86cd12cc4f52..5838dcad30bc28 100644 --- a/homeassistant/components/owntracks/.translations/no.json +++ b/homeassistant/components/owntracks/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "one_instance_allowed": "Kun \u00e9n enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "\n\nP\u00e5 Android, \u00e5pne [OwnTracks appen]({android_url}), g\u00e5 til Instillinger -> tilkobling. Endre f\u00f8lgende innstillinger: \n - Modus: Privat HTTP\n - Vert: {webhook_url} \n - Identifikasjon: \n - Brukernavn: ` ` \n - Enhets-ID: ` ` \n\nP\u00e5 iOS, \u00e5pne [OwnTracks appen]({ios_url}), trykk p\u00e5 (i) ikonet \u00f8verst til venstre - > innstillinger. Endre f\u00f8lgende innstillinger: \n - Modus: HTTP \n - URL: {webhook_url} \n - Sl\u00e5 p\u00e5 autentisering \n - BrukerID: ` ` \n\n {secret} \n \n Se [dokumentasjonen]({docs_url}) for mer informasjon." diff --git a/homeassistant/components/owntracks/.translations/zh-Hant.json b/homeassistant/components/owntracks/.translations/zh-Hant.json index d8c195cb27738f..923f452450b035 100644 --- a/homeassistant/components/owntracks/.translations/zh-Hant.json +++ b/homeassistant/components/owntracks/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { - "default": "\n\n\u65bc Android \u88dd\u7f6e\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a ``\n - Device ID\uff1a``\n\n\u65bc iOS \u88dd\u7f6e\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: ``\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + "default": "\n\n\u65bc Android \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a ``\n - Device ID\uff1a``\n\n\u65bc iOS \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: ``\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/no.json b/homeassistant/components/plaato/.translations/no.json index 4b47f52eef915c..0de31a35eb2937 100644 --- a/homeassistant/components/plaato/.translations/no.json +++ b/homeassistant/components/plaato/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Home Assistant m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta meldinger fra Plaato Airlock.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." diff --git a/homeassistant/components/plaato/.translations/zh-Hant.json b/homeassistant/components/plaato/.translations/zh-Hant.json index 20cdb405f4ee10..1bf211476d8895 100644 --- a/homeassistant/components/plaato/.translations/zh-Hant.json +++ b/homeassistant/components/plaato/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Plaato Airlock \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Plaato Airlock \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index ea680a638eb471..670bc23ca1fd3b 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Vis alle kontrolelementer", + "use_episode_art": "Brug episode kunst" + }, + "description": "Indstillinger for Plex-medieafspillere" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index 7fa9f62be07763..d3c4c0d5a7825b 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Show all controls", + "use_episode_art": "Use episode art" + }, + "description": "Options for Plex Media Players" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index b58cdfe728e043..393639dd4c9fc9 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -41,5 +41,15 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Vis alle kontroller" + }, + "description": "Alternativer for Plex Media Players" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index b424be059f9a5c..c547e4306b4ad5 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f", + "use_episode_art": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u043b\u043e\u0436\u043a\u0438 \u044d\u043f\u0438\u0437\u043e\u0434\u043e\u0432" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/sv.json b/homeassistant/components/plex/.translations/sv.json new file mode 100644 index 00000000000000..702cec128c0353 --- /dev/null +++ b/homeassistant/components/plex/.translations/sv.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Visa alla kontroller" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index c79a49470e000d..2cf3fa2c1a7e81 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "\u9a57\u8b49\u5931\u6557", "no_servers": "\u6b64\u5e33\u865f\u672a\u7d81\u5b9a\u4f3a\u670d\u5668", + "no_token": "\u63d0\u4f9b\u5bc6\u9470\u6216\u9078\u64c7\u624b\u52d5\u8a2d\u5b9a", "not_found": "\u627e\u4e0d\u5230 Plex \u4f3a\u670d\u5668" }, "step": { + "manual_setup": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u4f7f\u7528 SSL", + "token": "\u5bc6\u9470\uff08\u5982\u679c\u9700\u8981\uff09", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "title": "Plex \u4f3a\u670d\u5668" + }, "select_server": { "data": { "server": "\u4f3a\u670d\u5668" @@ -22,9 +33,10 @@ }, "user": { "data": { + "manual_setup": "\u624b\u52d5\u8a2d\u5b9a", "token": "Plex \u5bc6\u9470" }, - "description": "\u8acb\u8f38\u5165 Plex \u5bc6\u9470\u4ee5\u9032\u884c\u81ea\u52d5\u8a2d\u5b9a\u3002", + "description": "\u8acb\u8f38\u5165 Plex \u5bc6\u9470\u4ee5\u9032\u884c\u81ea\u52d5\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" } }, diff --git a/homeassistant/components/point/.translations/zh-Hant.json b/homeassistant/components/point/.translations/zh-Hant.json index 9f688b2e5f9d52..f1bbb1c872c3d7 100644 --- a/homeassistant/components/point/.translations/zh-Hant.json +++ b/homeassistant/components/point/.translations/zh-Hant.json @@ -8,7 +8,7 @@ "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Point \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15](https://www.home-assistant.io/components/point/)\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Minut Point \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Minut Point \u8a2d\u5099\u3002" }, "error": { "follow_link": "\u8acb\u65bc\u50b3\u9001\u524d\uff0c\u5148\u4f7f\u7528\u9023\u7d50\u4e26\u9032\u884c\u8a8d\u8b49\u3002", diff --git a/homeassistant/components/ps4/.translations/de.json b/homeassistant/components/ps4/.translations/de.json index 6d152108117226..2053d2f4a80bed 100644 --- a/homeassistant/components/ps4/.translations/de.json +++ b/homeassistant/components/ps4/.translations/de.json @@ -5,7 +5,7 @@ "devices_configured": "Alle gefundenen Ger\u00e4te sind bereits konfiguriert.", "no_devices_found": "Es wurden keine PlayStation 4 im Netzwerk gefunden.", "port_987_bind_error": "Bind to Port 987 nicht m\u00f6glich.", - "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen finden Sie in der [documentation](https://www.home-assistant.io/components/ps4/)" + "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen finden Sie in der [Dokumentation](https://www.home-assistant.io/components/ps4/)" }, "error": { "credential_timeout": "Zeit\u00fcberschreitung beim Warten auf den Anmeldedienst. Klicken zum Neustarten auf Senden.", diff --git a/homeassistant/components/ps4/.translations/zh-Hant.json b/homeassistant/components/ps4/.translations/zh-Hant.json index f0a71b4be5b533..a786b0c74d3f15 100644 --- a/homeassistant/components/ps4/.translations/zh-Hant.json +++ b/homeassistant/components/ps4/.translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "credential_error": "\u53d6\u5f97\u6191\u8b49\u932f\u8aa4\u3002", - "devices_configured": "\u6240\u6709\u88dd\u7f6e\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", + "devices_configured": "\u6240\u6709\u8a2d\u5099\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 PlayStation 4 \u88dd\u7f6e\u3002", "port_987_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 987\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", "port_997_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 997\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002" @@ -15,7 +15,7 @@ }, "step": { "creds": { - "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u88dd\u7f6e\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", + "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u8a2d\u5099\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", "title": "PlayStation 4" }, "link": { @@ -25,7 +25,7 @@ "name": "\u540d\u7a31", "region": "\u5340\u57df" }, - "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u88dd\u7f6e\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", + "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u8a2d\u5099\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", "title": "PlayStation 4" }, "mode": { @@ -33,7 +33,7 @@ "ip_address": "IP \u4f4d\u5740\uff08\u5982\u679c\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22\u65b9\u5f0f\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d\uff09\u3002", "mode": "\u8a2d\u5b9a\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u88dd\u7f6e\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", + "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u8a2d\u5099\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", "title": "PlayStation 4" } }, diff --git a/homeassistant/components/smartthings/.translations/he.json b/homeassistant/components/smartthings/.translations/he.json index c38afd989d250e..7f80900d8282e0 100644 --- a/homeassistant/components/smartthings/.translations/he.json +++ b/homeassistant/components/smartthings/.translations/he.json @@ -16,7 +16,7 @@ "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" }, "description": "\u05d4\u05d6\u05df SmartThings [\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9\u05ea] ( {token_url} ) \u05e9\u05e0\u05d5\u05e6\u05e8 \u05dc\u05e4\u05d9 [\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea] ( {component_url} ).", - "title": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9\u05d9 " + "title": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05d0\u05d9\u05e9 " }, "wait_install": { "description": "\u05d4\u05ea\u05e7\u05df \u05d0\u05ea \u05d4- Home Assistant SmartApp \u05dc\u05e4\u05d7\u05d5\u05ea \u05d1\u05de\u05d9\u05e7\u05d5\u05dd \u05d0\u05d7\u05d3 \u05d5\u05dc\u05d7\u05e5 \u05e2\u05dc \u05e9\u05dc\u05d7.", diff --git a/homeassistant/components/somfy/.translations/zh-Hant.json b/homeassistant/components/somfy/.translations/zh-Hant.json index 6b53e943304383..f7f7f4a1d8e33b 100644 --- a/homeassistant/components/somfy/.translations/zh-Hant.json +++ b/homeassistant/components/somfy/.translations/zh-Hant.json @@ -6,7 +6,7 @@ "missing_configuration": "Somfy \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Somfy \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Somfy \u8a2d\u5099\u3002" }, "title": "Somfy" } diff --git a/homeassistant/components/sonos/.translations/no.json b/homeassistant/components/sonos/.translations/no.json index c837abad499db4..ae47916ac85a2d 100644 --- a/homeassistant/components/sonos/.translations/no.json +++ b/homeassistant/components/sonos/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av Sonos er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av Sonos er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/.translations/zh-Hant.json b/homeassistant/components/sonos/.translations/zh-Hant.json index 520a29b7602526..c6fb13c3605d30 100644 --- a/homeassistant/components/sonos/.translations/zh-Hant.json +++ b/homeassistant/components/sonos/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Sonos \u8a2d\u5099\u3002", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 Sonos \u5373\u53ef\u3002" }, "step": { diff --git a/homeassistant/components/tplink/.translations/no.json b/homeassistant/components/tplink/.translations/no.json index 4946eb81f02952..41148475a5f6f4 100644 --- a/homeassistant/components/tplink/.translations/no.json +++ b/homeassistant/components/tplink/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen TP-Link enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av TP-Link er n\u00f8dvendig." + "single_instance_allowed": "Kun en konfigurasjon av TP-Link er n\u00f8dvendig." }, "step": { "confirm": { diff --git a/homeassistant/components/tplink/.translations/zh-Hant.json b/homeassistant/components/tplink/.translations/zh-Hant.json index d44faf195e55ea..250a5509c4c265 100644 --- a/homeassistant/components/tplink/.translations/zh-Hant.json +++ b/homeassistant/components/tplink/.translations/zh-Hant.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u88dd\u7f6e\uff1f", + "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u8a2d\u5099\uff1f", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/traccar/.translations/de.json b/homeassistant/components/traccar/.translations/de.json index c835ddf76b2481..dccd39b6997ff5 100644 --- a/homeassistant/components/traccar/.translations/de.json +++ b/homeassistant/components/traccar/.translations/de.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Es ist nur eine einzelne Instanz erforderlich." }, "create_entry": { - "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]({docs_url}) f\u00fcr weitere Details." + "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]( {docs_url} ) f\u00fcr weitere Details." }, "step": { "user": { diff --git a/homeassistant/components/traccar/.translations/no.json b/homeassistant/components/traccar/.translations/no.json index dea146b649aace..805b952690e577 100644 --- a/homeassistant/components/traccar/.translations/no.json +++ b/homeassistant/components/traccar/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant-forekomst m\u00e5 v\u00e6re tilgjengelig fra Internett for \u00e5 motta meldinger fra Traccar.", - "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "Hvis du vil sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar.\n\nBruk f\u00f8lgende URL-adresse: ' {webhook_url} '\n\nSe [dokumentasjonen] ({docs_url}) for mer informasjon." diff --git a/homeassistant/components/traccar/.translations/zh-Hant.json b/homeassistant/components/traccar/.translations/zh-Hant.json index f5402454294856..85e8994dc55c34 100644 --- a/homeassistant/components/traccar/.translations/zh-Hant.json +++ b/homeassistant/components/traccar/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Traccar \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Traccar \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/transmission/.translations/da.json b/homeassistant/components/transmission/.translations/da.json new file mode 100644 index 00000000000000..3a619d8154af21 --- /dev/null +++ b/homeassistant/components/transmission/.translations/da.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", + "wrong_credentials": "Ugyldigt brugernavn eller adgangskode" + }, + "step": { + "options": { + "data": { + "scan_interval": "Opdateringsfrekvens" + }, + "title": "Konfigurer indstillinger" + }, + "user": { + "data": { + "host": "V\u00e6rt", + "name": "Navn", + "password": "Adgangskode", + "port": "Port", + "username": "Brugernavn" + }, + "title": "Konfigurer Transmission klient" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Opdateringsfrekvens" + }, + "description": "Konfigurationsindstillinger for Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index 7160cd109c4794..e1bc8dc322824a 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -1,39 +1,39 @@ { "config": { - "title": "Transmission", + "abort": { + "one_instance_allowed": "Only a single instance is necessary." + }, + "error": { + "cannot_connect": "Unable to Connect to host", + "wrong_credentials": "Wrong username or password" + }, "step": { + "options": { + "data": { + "scan_interval": "Update frequency" + }, + "title": "Configure Options" + }, "user": { - "title": "Setup Transmission Client", "data": { - "name": "Name", "host": "Host", - "username": "User name", + "name": "Name", "password": "Password", - "port": "Port" - } - }, - "options": { - "title": "Configure Options", - "data": { - "scan_interval": "Update frequency" - } + "port": "Port", + "username": "User name" + }, + "title": "Setup Transmission Client" } }, - "error": { - "wrong_credentials": "Wrong username or password", - "cannot_connect": "Unable to Connect to host" - }, - "abort": { - "one_instance_allowed": "Only a single instance is necessary." - } + "title": "Transmission" }, "options": { "step": { "init": { - "description": "Configure options for Transmission", "data": { "scan_interval": "Update frequency" - } + }, + "description": "Configure options for Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/no.json b/homeassistant/components/transmission/.translations/no.json new file mode 100644 index 00000000000000..f6ddce2a4a799e --- /dev/null +++ b/homeassistant/components/transmission/.translations/no.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Bare en enkel instans er n\u00f8dvendig." + }, + "error": { + "cannot_connect": "Kan ikke koble til vert", + "wrong_credentials": "Ugyldig brukernavn eller passord" + }, + "step": { + "options": { + "data": { + "scan_interval": "Oppdater frekvens" + }, + "title": "Konfigurer alternativer" + }, + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + }, + "title": "Oppsett av klient for Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdater frekvens" + }, + "description": "Konfigurer alternativer for Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json new file mode 100644 index 00000000000000..5da2d4f9ef909b --- /dev/null +++ b/homeassistant/components/transmission/.translations/ru.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443", + "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "step": { + "options": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/sv.json b/homeassistant/components/transmission/.translations/sv.json new file mode 100644 index 00000000000000..30004af17dbf98 --- /dev/null +++ b/homeassistant/components/transmission/.translations/sv.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + }, + "error": { + "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", + "wrong_credentials": "Fel anv\u00e4ndarnamn eller l\u00f6senord" + }, + "step": { + "options": { + "data": { + "scan_interval": "Uppdateringsfrekvens" + }, + "title": "Konfigurera alternativ" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsfrekvens" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/no.json b/homeassistant/components/twilio/.translations/no.json index 0d28b094340f8b..c3d6ff16e2dd32 100644 --- a/homeassistant/components/twilio/.translations/no.json +++ b/homeassistant/components/twilio/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_internet_accessible": "Din Home Assistant forekomst m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 motta Twilio-meldinger.", - "one_instance_allowed": "Kun \u00e9n enkelt forekomst er n\u00f8dvendig." + "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp [Webhooks with Twilio]({twilio_url}). \n\nFyll ut f\u00f8lgende informasjon: \n\n- URL: `{webhook_url}` \n- Metode: POST\n- Innholdstype: application/x-www-form-urlencoded \n\n Se [dokumentasjonen]({docs_url}) om hvordan du konfigurerer automatiseringer for \u00e5 h\u00e5ndtere innkommende data." diff --git a/homeassistant/components/twilio/.translations/zh-Hant.json b/homeassistant/components/twilio/.translations/zh-Hant.json index 2e85ef7b2ded10..858970539cd223 100644 --- a/homeassistant/components/twilio/.translations/zh-Hant.json +++ b/homeassistant/components/twilio/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002", + "not_internet_accessible": "Home Assistant \u7269\u4ef6\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Twilio \u8a0a\u606f\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index 2d5bd9027ac44d..498afcbb10a19f 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -29,7 +29,7 @@ "data": { "detection_time": "\u6700\u7d42\u51fa\u73fe\u5f8c\u8996\u70ba\u96e2\u958b\u7684\u6642\u9593\uff08\u4ee5\u79d2\u70ba\u55ae\u4f4d\uff09", "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", - "track_devices": "\u8ffd\u8e64\u7db2\u8def\u88dd\u7f6e\uff08Ubiquiti \u88dd\u7f6e\uff09", + "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09", "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" } } diff --git a/homeassistant/components/upnp/.translations/no.json b/homeassistant/components/upnp/.translations/no.json index 813509121e34d5..fb1508a1aab031 100644 --- a/homeassistant/components/upnp/.translations/no.json +++ b/homeassistant/components/upnp/.translations/no.json @@ -6,7 +6,7 @@ "no_devices_discovered": "Ingen UPnP / IGDs oppdaget", "no_devices_found": "Ingen UPnP / IGD-enheter funnet p\u00e5 nettverket.", "no_sensors_or_port_mapping": "Aktiver minst sensorer eller port mapping", - "single_instance_allowed": "Bare en enkelt konfigurasjon av UPnP / IGD er n\u00f8dvendig." + "single_instance_allowed": "Bare en konfigurasjon av UPnP / IGD er n\u00f8dvendig." }, "error": { "few": "f\u00e5", diff --git a/homeassistant/components/upnp/.translations/zh-Hant.json b/homeassistant/components/upnp/.translations/zh-Hant.json index 2a036a1d2f3577..1611dac27213e3 100644 --- a/homeassistant/components/upnp/.translations/zh-Hant.json +++ b/homeassistant/components/upnp/.translations/zh-Hant.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "incomplete_device": "\u5ffd\u7565\u4e0d\u76f8\u5bb9 UPnP \u88dd\u7f6e", + "incomplete_device": "\u5ffd\u7565\u4e0d\u76f8\u5bb9 UPnP \u8a2d\u5099", "no_devices_discovered": "\u672a\u641c\u5c0b\u5230 UPnP/IGD", - "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 UPnP/IGD \u88dd\u7f6e\u3002", + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 UPnP/IGD \u8a2d\u5099\u3002", "no_sensors_or_port_mapping": "\u81f3\u5c11\u958b\u555f\u611f\u61c9\u5668\u6216\u901a\u8a0a\u57e0\u8f49\u767c", "single_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u6b21 UPnP/IGD \u5373\u53ef\u3002" }, diff --git a/homeassistant/components/wemo/.translations/no.json b/homeassistant/components/wemo/.translations/no.json index 917eb0ef3a9d76..25a4172f00c092 100644 --- a/homeassistant/components/wemo/.translations/no.json +++ b/homeassistant/components/wemo/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.", - "single_instance_allowed": "Kun en enkelt konfigurasjon av Wemo er mulig." + "single_instance_allowed": "Kun en konfigurasjon av Wemo er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json index 71934516ea7f1b..7f32ade8a0892b 100644 --- a/homeassistant/components/withings/.translations/sl.json +++ b/homeassistant/components/withings/.translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_flows": "withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo." + "no_flows": "Withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo." }, "create_entry": { "default": "Uspe\u0161no overjen z Withings za izbrani profil." diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json index 9e408eb0d5c19e..f01109b2d621d8 100644 --- a/homeassistant/components/withings/.translations/zh-Hant.json +++ b/homeassistant/components/withings/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Withings \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002\u8acb\u53c3\u95b1\u6587\u4ef6\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u88dd\u7f6e\u3002" + "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u8a2d\u5099\u3002" }, "step": { "user": { diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 36bdfb6d5d33a5..623c33637e0e9d 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Kun \u00e9n enkelt konfigurasjon av ZHA er tillatt." + "single_instance_allowed": "Kun en konfigurasjon av ZHA er tillatt." }, "error": { "cannot_connect": "Kan ikke koble til ZHA-enhet." @@ -44,11 +44,11 @@ }, "trigger_type": { "device_dropped": "Enheten ble brutt", - "device_flipped": "Enheten snudd \"{undertype}\"", - "device_knocked": "Enheten sl\u00e5tt \"{undertype}\"", - "device_rotated": "Enheten roterte \"{under type}\"", + "device_flipped": "Enheten snudd \"{subtype}\"", + "device_knocked": "Enheten sl\u00e5tt \"{subtype}\"", + "device_rotated": "Enheten roterte \"{subtype}\"", "device_shaken": "Enhet er ristet", - "device_slid": "Enheten skled \"{undertype}\"", + "device_slid": "Enheten skled \"{subtype}\"", "device_tilted": "Enhet vippet", "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", @@ -57,7 +57,7 @@ "remote_button_quintuple_press": "\" {subtype} \" - knappen ble femdobbelt klikket", "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\" {subtype} \"-knappen ble rippel klikket" + "remote_button_triple_press": "\"{subtype}\"-knappen ble trippel klikket" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/sv.json b/homeassistant/components/zha/.translations/sv.json index 818d041b4f1164..2762adc0fba19f 100644 --- a/homeassistant/components/zha/.translations/sv.json +++ b/homeassistant/components/zha/.translations/sv.json @@ -16,5 +16,17 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "close": "St\u00e4ng", + "dim_down": "Dimma ned", + "dim_up": "Dimma upp", + "left": "V\u00e4nster", + "open": "\u00d6ppen", + "right": "H\u00f6ger", + "turn_off": "St\u00e4ng av", + "turn_on": "Starta" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/zh-Hant.json b/homeassistant/components/zha/.translations/zh-Hant.json index e31e42bfbcfa18..bbfb3fe7128f27 100644 --- a/homeassistant/components/zha/.translations/zh-Hant.json +++ b/homeassistant/components/zha/.translations/zh-Hant.json @@ -4,17 +4,60 @@ "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 ZHA\u3002" }, "error": { - "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 ZHA \u88dd\u7f6e\u3002" + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 ZHA \u8a2d\u5099\u3002" }, "step": { "user": { "data": { "radio_type": "\u7121\u7dda\u96fb\u985e\u578b", - "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" + "usb_path": "USB \u8a2d\u5099\u8def\u5f91" }, "title": "ZHA" } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u5169\u500b\u6309\u9215", + "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "button_5": "\u7b2c\u4e94\u500b\u6309\u9215", + "button_6": "\u7b2c\u516d\u500b\u6309\u9215", + "close": "\u95dc\u9589", + "dim_down": "\u8abf\u6697", + "dim_up": "\u8abf\u4eae", + "face_1": "\u5df2\u7531\u9762\u5bb9 1 \u958b\u555f", + "face_2": "\u5df2\u7531\u9762\u5bb9 2 \u958b\u555f", + "face_3": "\u5df2\u7531\u9762\u5bb9 3 \u958b\u555f", + "face_4": "\u5df2\u7531\u9762\u5bb9 4 \u958b\u555f", + "face_5": "\u5df2\u7531\u9762\u5bb9 5 \u958b\u555f", + "face_6": "\u5df2\u7531\u9762\u5bb9 6 \u958b\u555f", + "face_any": "\u5df2\u7531\u4efb\u4f55/\u7279\u5b9a\u9762\u5bb9\u958b\u555f", + "left": "\u5de6", + "open": "\u958b\u555f", + "right": "\u53f3", + "turn_off": "\u95dc\u9589", + "turn_on": "\u958b\u555f" + }, + "trigger_type": { + "device_dropped": "\u8a2d\u5099\u6389\u843d", + "device_flipped": "\u7ffb\u52d5 \"{subtype}\" \u8a2d\u5099", + "device_knocked": "\u6572\u64ca \"{subtype}\" \u8a2d\u5099", + "device_rotated": "\u65cb\u8f49 \"{subtype}\" \u8a2d\u5099", + "device_shaken": "\u8a2d\u5099\u6416\u6643", + "device_slid": "\u63a8\u52d5 \"{subtype}\" \u8a2d\u5099", + "device_tilted": "\u8a2d\u5099\u540d\u7a31", + "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", + "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", + "remote_button_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u64ca", + "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u64ca", + "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", + "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u64ca" + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/no.json b/homeassistant/components/zwave/.translations/no.json index f70eaa4826014d..1d5584a82a053a 100644 --- a/homeassistant/components/zwave/.translations/no.json +++ b/homeassistant/components/zwave/.translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Z-Wave er allerede konfigurert", - "one_instance_only": "Komponenten st\u00f8tter kun en enkelt Z-Wave-forekomst" + "one_instance_only": "Komponenten st\u00f8tter kun en Z-Wave-forekomst" }, "error": { "option_error": "Z-Wave-validering mislyktes. Er banen til USB dongel riktig?" From 45c548ae47ab9cfedb060e9bb1a7f8b01fbea1bc Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 26 Sep 2019 22:53:26 -0700 Subject: [PATCH 0473/3953] Bump androidtv to 0.0.28 (#26906) * Bump androidtv to 0.0.28 * Address reviewer comments * Remove adb-shell from requirements_test_all.txt * Use a one-liner to avoid a coverage failure --- .../components/androidtv/manifest.json | 3 +- .../components/androidtv/media_player.py | 32 +++++----- requirements_all.txt | 5 +- requirements_test_all.txt | 2 +- tests/components/androidtv/patchers.py | 63 ++++++++++--------- .../components/androidtv/test_media_player.py | 8 ++- 6 files changed, 61 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 6643faa85bdeb3..c085addad4dd37 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ - "androidtv==0.0.27" + "adb-shell==0.0.2", + "androidtv==0.0.28" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index d68f47b1b0a3b7..fcf4950f5e2903 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -3,6 +3,12 @@ import logging import voluptuous as vol +from adb_shell.exceptions import ( + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, +) from androidtv import setup, ha_state_detection_rules_validator from androidtv.constants import APPS, KEYS @@ -123,11 +129,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Android TV / Fire TV platform.""" hass.data.setdefault(ANDROIDTV_DOMAIN, {}) - host = "{0}:{1}".format(config[CONF_HOST], config[CONF_PORT]) + host = f"{config[CONF_HOST]}:{config[CONF_PORT]}" if CONF_ADB_SERVER_IP not in config: - # Use "python-adb" (Python ADB implementation) - adb_log = "using Python ADB implementation " + # Use "adb_shell" (Python ADB implementation) + adb_log = "using Python ADB implementation " + ( + f"with adbkey='{config[CONF_ADBKEY]}'" + if CONF_ADBKEY in config + else "without adbkey authentication" + ) if CONF_ADBKEY in config: aftv = setup( host, @@ -135,7 +145,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 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( @@ -143,7 +152,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 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) aftv = setup( @@ -153,9 +161,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 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] - ) + adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" if not aftv.available: # Determine the name that will be used for the device in the log @@ -251,6 +257,7 @@ def _adb_exception_catcher(self, *args, **kwargs): "establishing attempt in the next update. Error: %s", err, ) + self.aftv.adb.close() self._available = False # pylint: disable=protected-access return None @@ -278,14 +285,7 @@ def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): # ADB exceptions to catch if not self.aftv.adb_server_ip: - # Using "python-adb" (Python ADB implementation) - from adb.adb_protocol import ( - InvalidChecksumError, - InvalidCommandError, - InvalidResponseError, - ) - from adb.usb_exceptions import TcpTimeoutException - + # Using "adb_shell" (Python ADB implementation) self.exceptions = ( AttributeError, BrokenPipeError, diff --git a/requirements_all.txt b/requirements_all.txt index 19bdc4efd833ff..a9ec02ffcc62ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -111,6 +111,9 @@ adafruit-blinka==1.2.1 # homeassistant.components.mcp23017 adafruit-circuitpython-mcp230xx==1.1.2 +# homeassistant.components.androidtv +adb-shell==0.0.2 + # homeassistant.components.adguard adguardhome==0.2.1 @@ -197,7 +200,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.27 +androidtv==0.0.28 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a4fa60f15e697..c3f40e16349292 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -80,7 +80,7 @@ aiowwlln==2.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.27 +androidtv==0.0.28 # homeassistant.components.apns apns2==0.3.0 diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 86d1c1c15bdc02..73aa52259890ef 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -4,34 +4,24 @@ from unittest.mock import patch -class AdbCommandsFake: - """A fake of the `adb.adb_commands.AdbCommands` class.""" +class AdbDeviceFake: + """A fake of the `adb_shell.adb_device.AdbDevice` class.""" - def ConnectDevice(self, *args, **kwargs): # pylint: disable=invalid-name + def __init__(self, *args, **kwargs): + """Initialize a fake `adb_shell.adb_device.AdbDevice` instance.""" + self.available = False + + def close(self): + """Close the socket connection.""" + self.available = False + + def connect(self, *args, **kwargs): """Try to connect to a device.""" raise NotImplementedError - def Shell(self, cmd): # pylint: disable=invalid-name + def shell(self, cmd): """Send an ADB shell command.""" - raise NotImplementedError - - -class AdbCommandsFakeSuccess(AdbCommandsFake): - """A fake of the `adb.adb_commands.AdbCommands` class when the connection attempt succeeds.""" - - def ConnectDevice(self, *args, **kwargs): # pylint: disable=invalid-name - """Successfully connect to a device.""" - return self - - -class AdbCommandsFakeFail(AdbCommandsFake): - """A fake of the `adb.adb_commands.AdbCommands` class when the connection attempt fails.""" - - def ConnectDevice( - self, *args, **kwargs - ): # pylint: disable=invalid-name, no-self-use - """Fail to connect to a device.""" - raise socket_error + return None class ClientFakeSuccess: @@ -85,31 +75,39 @@ def shell(self, cmd): def patch_connect(success): - """Mock the `adb.adb_commands.AdbCommands` and `adb_messenger.client.Client` classes.""" + """Mock the `adb_shell.adb_device.AdbDevice` and `adb_messenger.client.Client` classes.""" + + def connect_success_python(self, *args, **kwargs): + """Mock the `AdbDeviceFake.connect` method when it succeeds.""" + self.available = True + + def connect_fail_python(self, *args, **kwargs): + """Mock the `AdbDeviceFake.connect` method when it fails.""" + raise socket_error if success: return { "python": patch( - "androidtv.adb_manager.AdbCommands", AdbCommandsFakeSuccess + f"{__name__}.AdbDeviceFake.connect", connect_success_python ), "server": patch("androidtv.adb_manager.Client", ClientFakeSuccess), } return { - "python": patch("androidtv.adb_manager.AdbCommands", AdbCommandsFakeFail), + "python": patch(f"{__name__}.AdbDeviceFake.connect", connect_fail_python), "server": patch("androidtv.adb_manager.Client", ClientFakeFail), } def patch_shell(response=None, error=False): - """Mock the `AdbCommandsFake.Shell` and `DeviceFake.shell` methods.""" + """Mock the `AdbDeviceFake.shell` and `DeviceFake.shell` methods.""" def shell_success(self, cmd): - """Mock the `AdbCommandsFake.Shell` and `DeviceFake.shell` methods when they are successful.""" + """Mock the `AdbDeviceFake.shell` and `DeviceFake.shell` methods when they are successful.""" self.shell_cmd = cmd return response def shell_fail_python(self, cmd): - """Mock the `AdbCommandsFake.Shell` method when it fails.""" + """Mock the `AdbDeviceFake.shell` method when it fails.""" self.shell_cmd = cmd raise AttributeError @@ -120,10 +118,13 @@ def shell_fail_server(self, cmd): if not error: return { - "python": patch(f"{__name__}.AdbCommandsFake.Shell", shell_success), + "python": patch(f"{__name__}.AdbDeviceFake.shell", shell_success), "server": patch(f"{__name__}.DeviceFake.shell", shell_success), } return { - "python": patch(f"{__name__}.AdbCommandsFake.Shell", shell_fail_python), + "python": patch(f"{__name__}.AdbDeviceFake.shell", shell_fail_python), "server": patch(f"{__name__}.DeviceFake.shell", shell_fail_server), } + + +PATCH_ADB_DEVICE = patch("androidtv.adb_manager.AdbDevice", AdbDeviceFake) diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 39b392c97ee306..feffc70d84148f 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -79,7 +79,9 @@ async def _test_reconnect(hass, caplog, config): else: entity_id = "media_player.fire_tv" - with patchers.patch_connect(True)[patch_key], patchers.patch_shell("")[patch_key]: + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -151,7 +153,9 @@ async def _test_adb_shell_returns_none(hass, config): else: entity_id = "media_player.fire_tv" - with patchers.patch_connect(True)[patch_key], patchers.patch_shell("")[patch_key]: + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) From 77b7e4665bc16cce52b6711d90d4660527d18f13 Mon Sep 17 00:00:00 2001 From: Oleksandr Omelchuk <25319+sashao@users.noreply.github.com> Date: Fri, 27 Sep 2019 08:54:40 +0300 Subject: [PATCH 0474/3953] Add more ebusd Vaillant "bai" sensors (#26750) * Added support for more ebus "bai" sensors. * Using common constants for measuring unit names. Fixed review item * dummy commit to restart CI * Fixed review item * Fixed formatting black --fast homeassistant * Removed trailing spaces * Fixed black formatting * trigger new build --- homeassistant/components/ebusd/const.py | 68 ++++++++++++++++++------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index 7587d0cd42dddb..db79d81736ef75 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,40 +1,41 @@ """Constants for ebus component.""" -from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, PRESSURE_BAR DOMAIN = "ebusd" +TIME_SECONDS = "seconds" -# SensorTypes: +# SensorTypes from ebusdpy module : # 0='decimal', 1='time-schedule', 2='switch', 3='string', 4='value;status' SENSOR_TYPES = { "700": { "ActualFlowTemperatureDesired": [ "Hc1ActualFlowTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], "MaxFlowTemperatureDesired": [ "Hc1MaxFlowTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], "MinFlowTemperatureDesired": [ "Hc1MinFlowTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], "PumpStatus": ["Hc1PumpStatus", None, "mdi:toggle-switch", 2], "HCSummerTemperatureLimit": [ "Hc1SummerTempLimit", - "°C", + TEMP_CELSIUS, "mdi:weather-sunny", 0, ], - "HolidayTemperature": ["HolidayTemp", "°C", "mdi:thermometer", 0], - "HWTemperatureDesired": ["HwcTempDesired", "°C", "mdi:thermometer", 0], + "HolidayTemperature": ["HolidayTemp", TEMP_CELSIUS, "mdi:thermometer", 0], + "HWTemperatureDesired": ["HwcTempDesired", TEMP_CELSIUS, "mdi:thermometer", 0], "HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer", 1], "HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer", 1], "HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer", 1], @@ -42,15 +43,20 @@ "HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer", 1], "HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer", 1], "HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer", 1], - "WaterPressure": ["WaterPressure", "bar", "mdi:water-pump", 0], + "WaterPressure": ["WaterPressure", PRESSURE_BAR, "mdi:water-pump", 0], "Zone1RoomZoneMapping": ["z1RoomZoneMapping", None, "mdi:label", 0], - "Zone1NightTemperature": ["z1NightTemp", "°C", "mdi:weather-night", 0], - "Zone1DayTemperature": ["z1DayTemp", "°C", "mdi:weather-sunny", 0], - "Zone1HolidayTemperature": ["z1HolidayTemp", "°C", "mdi:thermometer", 0], - "Zone1RoomTemperature": ["z1RoomTemp", "°C", "mdi:thermometer", 0], + "Zone1NightTemperature": ["z1NightTemp", TEMP_CELSIUS, "mdi:weather-night", 0], + "Zone1DayTemperature": ["z1DayTemp", TEMP_CELSIUS, "mdi:weather-sunny", 0], + "Zone1HolidayTemperature": [ + "z1HolidayTemp", + TEMP_CELSIUS, + "mdi:thermometer", + 0, + ], + "Zone1RoomTemperature": ["z1RoomTemp", TEMP_CELSIUS, "mdi:thermometer", 0], "Zone1ActualRoomTemperatureDesired": [ "z1ActualRoomTempDesired", - "°C", + TEMP_CELSIUS, "mdi:thermometer", 0, ], @@ -62,7 +68,7 @@ "Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer", 1], "Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer", 1], "Zone1OperativeMode": ["z1OpMode", None, "mdi:math-compass", 3], - "ContinuosHeating": ["ContinuosHeating", "°C", "mdi:weather-snowy", 0], + "ContinuosHeating": ["ContinuosHeating", TEMP_CELSIUS, "mdi:weather-snowy", 0], "PowerEnergyConsumptionLastMonth": [ "PrEnergySumHcLastMonth", ENERGY_KILO_WATT_HOUR, @@ -77,14 +83,38 @@ ], }, "ehp": { - "HWTemperature": ["HwcTemp", "°C", "mdi:thermometer", 4], - "OutsideTemp": ["OutsideTemp", "°C", "mdi:thermometer", 4], + "HWTemperature": ["HwcTemp", TEMP_CELSIUS, "mdi:thermometer", 4], + "OutsideTemp": ["OutsideTemp", TEMP_CELSIUS, "mdi:thermometer", 4], }, "bai": { - "ReturnTemperature": ["ReturnTemp", "°C", "mdi:thermometer", 4], + "HotWaterTemperature": ["HwcTemp", TEMP_CELSIUS, "mdi:thermometer", 4], + "StorageTemperature": ["StorageTemp", TEMP_CELSIUS, "mdi:thermometer", 4], + "DesiredStorageTemperature": [ + "StorageTempDesired", + TEMP_CELSIUS, + "mdi:thermometer", + 0, + ], + "OutdoorsTemperature": [ + "OutdoorstempSensor", + TEMP_CELSIUS, + "mdi:thermometer", + 4, + ], + "WaterPreasure": ["WaterPressure", PRESSURE_BAR, "mdi:pipe", 4], + "AverageIgnitionTime": ["averageIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], + "MaximumIgnitionTime": ["maxIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], + "MinimumIgnitionTime": ["minIgnitiontime", TIME_SECONDS, "mdi:av-timer", 0], + "ReturnTemperature": ["ReturnTemp", TEMP_CELSIUS, "mdi:thermometer", 4], "CentralHeatingPump": ["WP", None, "mdi:toggle-switch", 2], "HeatingSwitch": ["HeatingSwitch", None, "mdi:toggle-switch", 2], - "FlowTemperature": ["FlowTemp", "°C", "mdi:thermometer", 4], + "DesiredFlowTemperature": [ + "FlowTempDesired", + TEMP_CELSIUS, + "mdi:thermometer", + 0, + ], + "FlowTemperature": ["FlowTemp", TEMP_CELSIUS, "mdi:thermometer", 4], "Flame": ["Flame", None, "mdi:toggle-switch", 2], "PowerEnergyConsumptionHeatingCircuit": [ "PrEnergySumHc1", From 9f6fade2365ee43a686c03d83cef4be0f1f69cfd Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 27 Sep 2019 08:02:58 +0200 Subject: [PATCH 0475/3953] Add xbox live custom update interval (#26939) * Add xbox_live custom update schedule * Set a default update interval that is within the free limit. Consider number of users per api key when setting interval. * Add codeowner * Add callback decorator --- CODEOWNERS | 1 + .../components/xbox_live/manifest.json | 4 +- homeassistant/components/xbox_live/sensor.py | 102 +++++++++++------- 3 files changed, 68 insertions(+), 39 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 419bc1a8606abb..4a6dfdbf6e62ab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -318,6 +318,7 @@ homeassistant/components/wemo/* @sqldiablo homeassistant/components/withings/* @vangorra homeassistant/components/worldclock/* @fabaff homeassistant/components/wwlln/* @bachya +homeassistant/components/xbox_live/* @MartinHjelmare homeassistant/components/xfinity/* @cisasteelersfan homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi homeassistant/components/xiaomi_miio/* @rytilahti @syssi diff --git a/homeassistant/components/xbox_live/manifest.json b/homeassistant/components/xbox_live/manifest.json index 0d80ce770ced7c..5baf928352d514 100644 --- a/homeassistant/components/xbox_live/manifest.json +++ b/homeassistant/components/xbox_live/manifest.json @@ -6,5 +6,7 @@ "xboxapi==0.1.1" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@MartinHjelmare" + ] } diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index d7ca22b2be3c33..e837cc4bbbc0fe 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -1,12 +1,16 @@ """Sensor for Xbox Live account status.""" import logging +from datetime import timedelta import voluptuous as vol +from xboxapi import xbox_api -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, STATE_UNKNOWN +from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL +from homeassistant.core import callback +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__) @@ -24,65 +28,76 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Xbox platform.""" - from xboxapi import xbox_api - - api = xbox_api.XboxApi(config.get(CONF_API_KEY)) - devices = [] + api = xbox_api.XboxApi(config[CONF_API_KEY]) + entities = [] # request personal profile to check api connection profile = api.get_profile() if profile.get("error_code") is not None: _LOGGER.error( "Can't setup XboxAPI connection. Check your account or " - " api key on xboxapi.com. Code: %s Description: %s ", - profile.get("error_code", STATE_UNKNOWN), - profile.get("error_message", STATE_UNKNOWN), + "api key on xboxapi.com. Code: %s Description: %s ", + profile.get("error_code", "unknown"), + profile.get("error_message", "unknown"), ) return - for xuid in config.get(CONF_XUID): - new_device = XboxSensor(hass, api, xuid) - if new_device.success_init: - devices.append(new_device) + users = config[CONF_XUID] + + interval = timedelta(minutes=1 * len(users)) + interval = config.get(CONF_SCAN_INTERVAL, interval) + + for xuid in users: + gamercard = get_user_gamercard(api, xuid) + if gamercard is None: + continue + entities.append(XboxSensor(api, xuid, gamercard, interval)) + + if entities: + add_entities(entities, True) + - if devices: - add_entities(devices, True) +def get_user_gamercard(api, xuid): + """Get profile info.""" + gamercard = api.get_user_gamercard(xuid) + _LOGGER.debug("User gamercard: %s", gamercard) + + if gamercard.get("success", True) and gamercard.get("code") is None: + return gamercard + _LOGGER.error( + "Can't get user profile %s. Error Code: %s Description: %s", + xuid, + gamercard.get("code", "unknown"), + gamercard.get("description", "unknown"), + ) + return None class XboxSensor(Entity): """A class for the Xbox account.""" - def __init__(self, hass, api, xuid): + def __init__(self, api, xuid, gamercard, interval): """Initialize the sensor.""" - self._hass = hass self._state = None - self._presence = {} + self._presence = [] self._xuid = xuid self._api = api - - # get profile info - profile = self._api.get_user_gamercard(self._xuid) - - if profile.get("success", True) and profile.get("code") is None: - self.success_init = True - self._gamertag = profile.get("gamertag") - self._gamerscore = profile.get("gamerscore") - self._picture = profile.get("gamerpicSmallSslImagePath") - self._tier = profile.get("tier") - else: - _LOGGER.error( - "Can't get user profile %s. " "Error Code: %s Description: %s", - self._xuid, - profile.get("code", STATE_UNKNOWN), - profile.get("description", STATE_UNKNOWN), - ) - self.success_init = False + self._gamertag = gamercard.get("gamertag") + self._gamerscore = gamercard.get("gamerscore") + self._interval = interval + self._picture = gamercard.get("gamerpicSmallSslImagePath") + self._tier = gamercard.get("tier") @property def name(self): """Return the name of the sensor.""" return self._gamertag + @property + def should_poll(self): + """Return False as this entity has custom polling.""" + return False + @property def state(self): """Return the state of the sensor.""" @@ -98,7 +113,7 @@ def device_state_attributes(self): for device in self._presence: for title in device.get("titles"): attributes[ - "{} {}".format(device.get("type"), title.get("placement")) + f'{device.get("type")} {title.get("placement")}' ] = title.get("name") return attributes @@ -113,8 +128,19 @@ def icon(self): """Return the icon to use in the frontend.""" return ICON + async def async_added_to_hass(self): + """Start custom polling.""" + + @callback + def async_update(event_time=None): + """Update the entity.""" + self.async_schedule_update_ha_state(True) + + async_track_time_interval(self.hass, async_update, self._interval) + def update(self): """Update state data from Xbox API.""" presence = self._api.get_user_presence(self._xuid) + _LOGGER.debug("User presence: %s", presence) self._state = presence.get("state") - self._presence = presence.get("devices", {}) + self._presence = presence.get("devices", []) From fc700c79377e97f4e9f950f5c1d2707701fd5ac7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Sep 2019 23:49:51 -0700 Subject: [PATCH 0476/3953] Guard against non supported entities (#26941) --- homeassistant/components/alexa/state_report.py | 8 ++++++++ tests/components/alexa/test_state_report.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index b7ff9d17fe8214..42c16919a45cf1 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -131,6 +131,10 @@ async def async_send_add_or_update_message(hass, config, entity_ids): for entity_id in entity_ids: domain = entity_id.split(".", 1)[0] + + if domain not in ENTITY_ADAPTERS: + continue + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) endpoints.append(alexa_entity.serialize_discovery()) @@ -161,6 +165,10 @@ async def async_send_delete_message(hass, config, entity_ids): for entity_id in entity_ids: domain = entity_id.split(".", 1)[0] + + if domain not in ENTITY_ADAPTERS: + continue + alexa_entity = ENTITY_ADAPTERS[domain](hass, config, hass.states.get(entity_id)) endpoints.append({"endpointId": alexa_entity.alexa_id()}) diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 2b3f9f34adf57d..c05eed2a89baac 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -48,7 +48,7 @@ async def test_send_add_or_update_message(hass, aioclient_mock): ) await state_report.async_send_add_or_update_message( - hass, DEFAULT_CONFIG, ["binary_sensor.test_contact"] + hass, DEFAULT_CONFIG, ["binary_sensor.test_contact", "zwave.bla"] ) assert len(aioclient_mock.mock_calls) == 1 @@ -75,7 +75,7 @@ async def test_send_delete_message(hass, aioclient_mock): ) await state_report.async_send_delete_message( - hass, DEFAULT_CONFIG, ["binary_sensor.test_contact"] + hass, DEFAULT_CONFIG, ["binary_sensor.test_contact", "zwave.bla"] ) assert len(aioclient_mock.mock_calls) == 1 From 454d479b36b314e3d2308f8ef8571ae38cafa4ad Mon Sep 17 00:00:00 2001 From: jaburges Date: Fri, 27 Sep 2019 03:52:58 -0700 Subject: [PATCH 0477/3953] Bump pyowlet to 1.0.3 (#26892) * API URLs API URLs have been updated which prevented 1.0.2 from authenticating per this thread: https://community.home-assistant.io/t/owlet-setup/130751 ``` https://user-field-1a2039d9.aylanetworks.com/users/sign_in https://ads-field-1a2039d9.aylanetworks.com/apiv1 ``` have been updated in PyOwlet.py 1.0.3 * bump pyowlet to 1.0.3 --- homeassistant/components/owlet/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/owlet/manifest.json b/homeassistant/components/owlet/manifest.json index edc51dcc5333ab..b89947343c94f4 100644 --- a/homeassistant/components/owlet/manifest.json +++ b/homeassistant/components/owlet/manifest.json @@ -3,7 +3,7 @@ "name": "Owlet", "documentation": "https://www.home-assistant.io/components/owlet", "requirements": [ - "pyowlet==1.0.2" + "pyowlet==1.0.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a9ec02ffcc62ae..32983063775dcd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1372,7 +1372,7 @@ pyotgw==0.4b4 pyotp==2.3.0 # homeassistant.components.owlet -pyowlet==1.0.2 +pyowlet==1.0.3 # homeassistant.components.openweathermap pyowm==2.10.0 From b88772eea0bacea18d2a491d7ecba1303f628152 Mon Sep 17 00:00:00 2001 From: joe248 <24720046+joe248@users.noreply.github.com> Date: Fri, 27 Sep 2019 08:31:01 -0500 Subject: [PATCH 0478/3953] Revert Nest HVAC mode when disabling Eco mode (#26934) --- homeassistant/components/nest/climate.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index dc4b0bd33ae823..eec7108cdeabd9 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -192,7 +192,10 @@ def current_temperature(self): def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" if self._mode == NEST_MODE_ECO: - # We assume the first operation in operation list is the main one + if self.device.previous_mode in MODE_NEST_TO_HASS: + return MODE_NEST_TO_HASS[self.device.previous_mode] + + # previous_mode not supported so return the first compatible mode return self._operation_list[0] return MODE_NEST_TO_HASS[self._mode] @@ -270,7 +273,7 @@ def preset_mode(self): if self._mode == NEST_MODE_ECO: return PRESET_ECO - return None + return PRESET_NONE @property def preset_modes(self): @@ -294,7 +297,7 @@ def set_preset_mode(self, preset_mode): if need_eco: self.device.mode = NEST_MODE_ECO else: - self.device.mode = MODE_HASS_TO_NEST[self._operation_list[0]] + self.device.mode = self.device.previous_mode @property def fan_mode(self): From 62d515fa035fe621eb854f7074f16d514634d8be Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 27 Sep 2019 16:26:06 +0200 Subject: [PATCH 0479/3953] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 29e68a5d7acc16..31908166dda0d9 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -233,3 +233,43 @@ stages: fi displayName: 'Create Meta-Image' + +- stage: 'Addidional' + jobs: + - job: 'Updater' + pool: + vmImage: 'ubuntu-latest' + variables: + - group: gcloud + steps: + - template: templates/azp-step-ha-version.yaml@azure + - script: | + set -e + + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + + curl -o google-cloud-sdk.tar.gz https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz + tar -C . -xvf google-cloud-sdk.tar.gz + rm -f google-cloud-sdk.tar.gz + + ./google-cloud-sdk/install.sh + displayName: 'Setup gCloud' + condition: eq(variables['homeassistantReleaseStable'], 'true')) + - script: | + set -e + + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + + echo "$(gcloudAuth)" > gcloud.key + ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud.key + rm -f gcloud.key + displayName: 'Auth gCloud' + condition: eq(variables['homeassistantReleaseStable'], 'true')) + - script: | + set -e + + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + + ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver --update-env-vars VERSION=$(homeassistantRelease) + displayName: 'Push details to updater' + condition: eq(variables['homeassistantReleaseStable'], 'true')) From e989239e191c8a368168cf4bc99e7095589e4a43 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 27 Sep 2019 16:43:23 +0200 Subject: [PATCH 0480/3953] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 31908166dda0d9..626c056cef81f7 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -260,9 +260,9 @@ stages: export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - echo "$(gcloudAuth)" > gcloud.key - ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud.key - rm -f gcloud.key + echo "$(gcloudAuth)" > gcloud_auth.json + ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud_auth.json + rm -f gcloud_auth.json displayName: 'Auth gCloud' condition: eq(variables['homeassistantReleaseStable'], 'true')) - script: | From 1de002013fbb11bc2dc988743a5c17a81fa6cdbf Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Fri, 27 Sep 2019 10:45:50 -0400 Subject: [PATCH 0481/3953] Fix ecobee integration (#26951) * Check for DATA_ECOBEE_CONFIG * Update homeassistant/components/ecobee/config_flow.py Co-Authored-By: Martin Hjelmare --- homeassistant/components/ecobee/config_flow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py index f4cd4fc5bf086d..56ce13f7701b99 100644 --- a/homeassistant/components/ecobee/config_flow.py +++ b/homeassistant/components/ecobee/config_flow.py @@ -33,7 +33,11 @@ async def async_step_user(self, user_input=None): return self.async_abort(reason="one_instance_only") errors = {} - stored_api_key = self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + stored_api_key = ( + self.hass.data[DATA_ECOBEE_CONFIG].get(CONF_API_KEY) + if DATA_ECOBEE_CONFIG in self.hass.data + else "" + ) if user_input is not None: # Use the user-supplied API key to attempt to obtain a PIN from ecobee. From 4d92e76f19ab2729cbedb1cac891c4a4f38e89ff Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 27 Sep 2019 16:59:07 +0200 Subject: [PATCH 0482/3953] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 40 ------------------------------------- 1 file changed, 40 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 626c056cef81f7..29e68a5d7acc16 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -233,43 +233,3 @@ stages: fi displayName: 'Create Meta-Image' - -- stage: 'Addidional' - jobs: - - job: 'Updater' - pool: - vmImage: 'ubuntu-latest' - variables: - - group: gcloud - steps: - - template: templates/azp-step-ha-version.yaml@azure - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - curl -o google-cloud-sdk.tar.gz https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz - tar -C . -xvf google-cloud-sdk.tar.gz - rm -f google-cloud-sdk.tar.gz - - ./google-cloud-sdk/install.sh - displayName: 'Setup gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true')) - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - echo "$(gcloudAuth)" > gcloud_auth.json - ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud_auth.json - rm -f gcloud_auth.json - displayName: 'Auth gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true')) - - script: | - set -e - - export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - - ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver --update-env-vars VERSION=$(homeassistantRelease) - displayName: 'Push details to updater' - condition: eq(variables['homeassistantReleaseStable'], 'true')) From 588bc2666112f9d939c1fc47a77ea5eceafef8c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Fri, 27 Sep 2019 17:42:32 +0200 Subject: [PATCH 0483/3953] Add CO2 level reading for Kaiterra integration (#26935) --- homeassistant/components/kaiterra/air_quality.py | 5 +++++ homeassistant/components/kaiterra/api_data.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py index 4dfe04f9c2e12d..70699de394cf5c 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -82,6 +82,11 @@ def particulate_matter_10(self): """Return the particulate matter 10 level.""" return self._data("rpm10c") + @property + def carbon_dioxide(self): + """Return the CO2 (carbon dioxide) level.""" + return self._data("rco2") + @property def volatile_organic_compounds(self): """Return the VOC (Volatile Organic Compounds) level.""" diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index 0c2d6d9366147d..81e28438d567f8 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -23,7 +23,7 @@ _LOGGER = getLogger(__name__) -POLLUTANTS = {"rpm25c": "PM2.5", "rpm10c": "PM10", "rtvoc": "TVOC"} +POLLUTANTS = {"rpm25c": "PM2.5", "rpm10c": "PM10", "rtvoc": "TVOC", "rco2": "CO2"} class KaiterraApiData: From e57e7e844921e06fb629357dbe2a638989c2db24 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 Sep 2019 17:48:48 +0200 Subject: [PATCH 0484/3953] Improve validation of device trigger config (#26910) * Improve validation of device trigger config * Remove action and condition checks * Move config validation to own file * Fix tests * Fixes * Fixes * Small tweak --- homeassistant/components/automation/config.py | 60 ++++++++++++++++++ homeassistant/components/automation/device.py | 20 +++--- homeassistant/components/config/__init__.py | 17 +++-- homeassistant/components/config/automation.py | 2 + .../components/device_automation/__init__.py | 23 +++++++ homeassistant/config.py | 43 +++++++++---- homeassistant/helpers/config_validation.py | 2 +- homeassistant/loader.py | 2 +- .../components/device_automation/test_init.py | 62 +++++++++++++++++++ tests/helpers/test_check_config.py | 4 +- tests/scripts/test_check_config.py | 4 +- 11 files changed, 209 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/automation/config.py diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py new file mode 100644 index 00000000000000..04b764e271ca48 --- /dev/null +++ b/homeassistant/components/automation/config.py @@ -0,0 +1,60 @@ +"""Config validation helper for the automation integration.""" +import asyncio +import importlib + +import voluptuous as vol + +from homeassistant.const import CONF_PLATFORM +from homeassistant.config import async_log_exception, config_without_domain +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform +from homeassistant.loader import IntegrationNotFound + +from . import CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA + +# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs, no-warn-return-any + + +async def async_validate_config_item(hass, config, full_config=None): + """Validate config item.""" + try: + config = PLATFORM_SCHEMA(config) + + triggers = [] + for trigger in config[CONF_TRIGGER]: + trigger_platform = importlib.import_module( + "..{}".format(trigger[CONF_PLATFORM]), __name__ + ) + if hasattr(trigger_platform, "async_validate_trigger_config"): + trigger = await trigger_platform.async_validate_trigger_config( + hass, trigger + ) + triggers.append(trigger) + config[CONF_TRIGGER] = triggers + except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: + async_log_exception(ex, DOMAIN, full_config or config, hass) + return None + + return config + + +async def async_validate_config(hass, config): + """Validate config.""" + automations = [] + validated_automations = await asyncio.gather( + *( + async_validate_config_item(hass, p_config, config) + for _, p_config in config_per_platform(config, DOMAIN) + ) + ) + for validated_automation in validated_automations: + if validated_automation is not None: + automations.append(validated_automation) + + # Create a copy of the configuration with all config for current + # component removed and add validated config back in. + config = config_without_domain(config, DOMAIN) + config[DOMAIN] = automations + + return config diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index fe2d65edef616b..eb3e5a95c9c17e 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -1,20 +1,24 @@ """Offer device oriented automation.""" import voluptuous as vol -from homeassistant.const import CONF_DOMAIN, CONF_PLATFORM -from homeassistant.loader import async_get_integration +from homeassistant.components.device_automation import ( + TRIGGER_BASE_SCHEMA, + async_get_device_automation_platform, +) # 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, -) +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) + + +async def async_validate_trigger_config(hass, config): + """Validate config.""" + platform = await async_get_device_automation_platform(hass, config, "trigger") + return platform.TRIGGER_SCHEMA(config) async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" - integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_trigger") + platform = await async_get_device_automation_platform(hass, config, "trigger") return await platform.async_attach_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 6d4b465fceb3dc..569d1de6a02ce8 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -5,10 +5,11 @@ import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import ATTR_COMPONENT -from homeassistant.components.http import HomeAssistantView from homeassistant.util.yaml import load_yaml, dump DOMAIN = "config" @@ -80,6 +81,7 @@ def __init__( data_schema, *, post_write_hook=None, + data_validator=None, ): """Initialize a config view.""" self.url = f"/api/config/{component}/{config_type}/{{config_key}}" @@ -88,6 +90,7 @@ def __init__( self.key_schema = key_schema self.data_schema = data_schema self.post_write_hook = post_write_hook + self.data_validator = data_validator def _empty_config(self): """Empty config if file not found.""" @@ -128,14 +131,18 @@ async def post(self, request, config_key): except vol.Invalid as err: return self.json_message(f"Key malformed: {err}", 400) + hass = request.app["hass"] + try: # We just validate, we don't store that data because # we don't want to store the defaults. - self.data_schema(data) - except vol.Invalid as err: + if self.data_validator: + await self.data_validator(hass, data) + else: + self.data_schema(data) + except (vol.Invalid, HomeAssistantError) as err: return self.json_message(f"Message malformed: {err}", 400) - hass = request.app["hass"] path = hass.config.path(self.path) current = await self.read_config(hass) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 17efdba3fb573f..97ddf1e0714e69 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -3,6 +3,7 @@ import uuid from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.automation.config import async_validate_config_item from homeassistant.const import CONF_ID, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv @@ -26,6 +27,7 @@ async def hook(hass): cv.string, PLATFORM_SCHEMA, post_write_hook=hook, + data_validator=async_validate_config_item, ) ) return True diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index b444abd5238db4..62d338ece54a49 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -9,6 +9,8 @@ from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.loader import async_get_integration, IntegrationNotFound +from .exceptions import InvalidDeviceAutomationConfig + DOMAIN = "device_automation" _LOGGER = logging.getLogger(__name__) @@ -43,6 +45,27 @@ async def async_setup(hass, config): return True +async def async_get_device_automation_platform(hass, config, automation_type): + """Load device automation platform for integration. + + Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. + """ + platform_name, _ = TYPES[automation_type] + try: + integration = await async_get_integration(hass, config[CONF_DOMAIN]) + platform = integration.get_platform(platform_name) + except IntegrationNotFound: + raise InvalidDeviceAutomationConfig( + f"Integration '{config[CONF_DOMAIN]}' not found" + ) + except ImportError: + raise InvalidDeviceAutomationConfig( + f"Integration '{config[CONF_DOMAIN]}' does not support device automation {automation_type}s" + ) + + return platform + + async def _async_get_device_automations_from_domain( hass, domain, automation_type, device_id ): diff --git a/homeassistant/config.py b/homeassistant/config.py index d3bd97dad8f777..0e840e1d003062 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -416,7 +416,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: @callback def async_log_exception( - ex: vol.Invalid, domain: str, config: Dict, hass: HomeAssistant + ex: Exception, domain: str, config: Dict, hass: HomeAssistant ) -> None: """Log an error for configuration validation. @@ -428,23 +428,26 @@ def async_log_exception( @callback -def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: +def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: """Generate log exception for configuration validation. This method must be run in the event loop. """ message = f"Invalid config for [{domain}]: " - if "extra keys not allowed" in ex.error_message: - message += ( - "[{option}] is an invalid option for [{domain}]. " - "Check: {domain}->{path}.".format( - option=ex.path[-1], - domain=domain, - path="->".join(str(m) for m in ex.path), + if isinstance(ex, vol.Invalid): + if "extra keys not allowed" in ex.error_message: + message += ( + "[{option}] is an invalid option for [{domain}]. " + "Check: {domain}->{path}.".format( + option=ex.path[-1], + domain=domain, + path="->".join(str(m) for m in ex.path), + ) ) - ) + else: + message += "{}.".format(humanize_error(config, ex)) else: - message += "{}.".format(humanize_error(config, ex)) + message += str(ex) try: domain_config = config.get(domain, config) @@ -717,6 +720,24 @@ async def async_process_component_config( _LOGGER.error("Unable to import %s: %s", domain, ex) return None + # Check if the integration has a custom config validator + config_validator = None + try: + config_validator = integration.get_platform("config") + except ImportError: + pass + if config_validator is not None and hasattr( + config_validator, "async_validate_config" + ): + try: + return await config_validator.async_validate_config( # type: ignore + hass, config + ) + except (vol.Invalid, HomeAssistantError) as ex: + async_log_exception(ex, domain, config, hass) + return None + + # No custom config validator, proceed with schema validation if hasattr(component, "CONFIG_SCHEMA"): try: return component.CONFIG_SCHEMA(config) # type: ignore diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 113f2437ce8cea..d567962e328a8e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -90,7 +90,7 @@ def validate(obj: Dict) -> Dict: for k in obj.keys(): if k in keys: return obj - raise vol.Invalid("must contain one of {}.".format(", ".join(keys))) + raise vol.Invalid("must contain at least one of {}.".format(", ".join(keys))) return validate diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 1a9a3d256acb2d..272271eefb31af 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -307,7 +307,7 @@ class IntegrationNotFound(LoaderError): def __init__(self, domain: str) -> None: """Initialize a component not found error.""" - super().__init__(f"Integration {domain} not found.") + super().__init__(f"Integration '{domain}' not found.") self.domain = domain diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index b05c04a16f1dc5..6dcd8391bf8954 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -2,6 +2,7 @@ import pytest from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.helpers import device_registry @@ -161,3 +162,64 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert msg["success"] triggers = msg["result"] assert _same_lists(triggers, expected_triggers) + + +async def test_automation_with_non_existing_integration(hass, caplog): + """Test device automation with non existing integration.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": "none", + "domain": "beer", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "Integration 'beer' not found" in caplog.text + + +async def test_automation_with_integration_without_device_trigger(hass, caplog): + """Test automation with integration without device trigger support.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": "none", + "domain": "test", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert ( + "Integration 'test' does not support device automation triggers" in caplog.text + ) + + +async def test_automation_with_bad_trigger(hass, caplog): + """Test automation with bad device trigger.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "device", "domain": "light"}, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "required key not provided" in caplog.text diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 9e5ea15293a484..5228f0d4882228 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( - "Component error: beer - Integration beer not found.", None, None + "Component error: beer - Integration 'beer' not found.", None, None ) # Only 1 error expected @@ -95,7 +95,7 @@ async def test_component_platform_not_found_2(hass, loop): assert res["light"] == [] assert res.errors[0] == CheckConfigError( - "Platform error light.beer - Integration beer not found.", 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 bd4f37bd1357c3..18143c088be58a 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -63,7 +63,7 @@ def test_component_platform_not_found(isfile_patch, loop): assert res["components"].keys() == {"homeassistant"} assert res["except"] == { check_config.ERROR_STR: [ - "Component error: beer - Integration beer not found." + "Component error: beer - Integration 'beer' not found." ] } assert res["secret_cache"] == {} @@ -77,7 +77,7 @@ def test_component_platform_not_found(isfile_patch, loop): assert res["components"]["light"] == [] assert res["except"] == { check_config.ERROR_STR: [ - "Platform error light.beer - Integration beer not found." + "Platform error light.beer - Integration 'beer' not found." ] } assert res["secret_cache"] == {} From f267b37105b97d7574b37af6c33c03baec9a5f54 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 27 Sep 2019 17:58:16 +0200 Subject: [PATCH 0485/3953] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 29e68a5d7acc16..51c5cdb936d2f9 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -233,3 +233,36 @@ stages: fi displayName: 'Create Meta-Image' + +- stage: 'Addidional' + jobs: + - job: 'Updater' + pool: + vmImage: 'ubuntu-latest' + variables: + - group: gcloud + steps: + - template: templates/azp-step-ha-version.yaml@azure + - script: | + set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + curl -o google-cloud-sdk.tar.gz https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz + tar -C . -xvf google-cloud-sdk.tar.gz + rm -f google-cloud-sdk.tar.gz + ./google-cloud-sdk/install.sh + displayName: 'Setup gCloud' + condition: eq(variables['homeassistantReleaseStable'], 'true')) + - script: | + set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + echo "$(gcloudAuth)" > gcloud_auth.json + ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud_auth.json + rm -f gcloud_auth.json + displayName: 'Auth gCloud' + condition: eq(variables['homeassistantReleaseStable'], 'true')) + - script: | + set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver --update-env-vars VERSION=$(homeassistantRelease) + displayName: 'Push details to updater' + condition: eq(variables['homeassistantReleaseStable'], 'true')) From b1a9fa47ca4b5c4738208ac4790389daf5d31bd4 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 27 Sep 2019 12:57:47 -0400 Subject: [PATCH 0486/3953] Add device action support for ZHA (#26903) * start implementing device actions * rename file * cleanup and add tests * fix docstrings * sort imports --- .../components/zha/.translations/en.json | 120 ++++++++-------- homeassistant/components/zha/core/helpers.py | 19 ++- homeassistant/components/zha/device_action.py | 92 ++++++++++++ .../components/zha/device_trigger.py | 19 +-- homeassistant/components/zha/strings.json | 4 + tests/components/zha/test_device_action.py | 133 ++++++++++++++++++ ...e_automation.py => test_device_trigger.py} | 2 +- 7 files changed, 313 insertions(+), 76 deletions(-) create mode 100644 homeassistant/components/zha/device_action.py create mode 100644 tests/components/zha/test_device_action.py rename tests/components/zha/{test_device_automation.py => test_device_trigger.py} (99%) diff --git a/homeassistant/components/zha/.translations/en.json b/homeassistant/components/zha/.translations/en.json index 84b335bdeaac95..ea1ad48bbff940 100644 --- a/homeassistant/components/zha/.translations/en.json +++ b/homeassistant/components/zha/.translations/en.json @@ -1,63 +1,67 @@ { - "config": { - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." - }, - "step": { - "user": { - "data": { - "radio_type": "Radio Type", - "usb_path": "USB Device Path" - }, - "title": "ZHA" - } + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "step": { + "user": { + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" }, "title": "ZHA" + } }, - "device_automation": { - "trigger_subtype": { - "both_buttons": "Both buttons", - "button_1": "First button", - "button_2": "Second button", - "button_3": "Third button", - "button_4": "Fourth button", - "button_5": "Fifth button", - "button_6": "Sixth button", - "close": "Close", - "dim_down": "Dim down", - "dim_up": "Dim up", - "face_1": "with face 1 activated", - "face_2": "with face 2 activated", - "face_3": "with face 3 activated", - "face_4": "with face 4 activated", - "face_5": "with face 5 activated", - "face_6": "with face 6 activated", - "face_any": "With any/specified face(s) activated", - "left": "Left", - "open": "Open", - "right": "Right", - "turn_off": "Turn off", - "turn_on": "Turn on" - }, - "trigger_type": { - "device_dropped": "Device dropped", - "device_flipped": "Device flipped \"{subtype}\"", - "device_knocked": "Device knocked \"{subtype}\"", - "device_rotated": "Device rotated \"{subtype}\"", - "device_shaken": "Device shaken", - "device_slid": "Device slid \"{subtype}\"", - "device_tilted": "Device tilted", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_triple_press": "\"{subtype}\" button triple clicked" - } + "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Warn" + }, + "trigger_subtype": { + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "close": "Close", + "dim_down": "Dim down", + "dim_up": "Dim up", + "face_1": "with face 1 activated", + "face_2": "with face 2 activated", + "face_3": "with face 3 activated", + "face_4": "with face 4 activated", + "face_5": "with face 5 activated", + "face_6": "with face 6 activated", + "face_any": "With any/specified face(s) activated", + "left": "Left", + "open": "Open", + "right": "Right", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"", + "device_knocked": "Device knocked \"{subtype}\"", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_triple_press": "\"{subtype}\" button triple clicked" } -} \ No newline at end of file + } +} diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 37bc6c7a2c17d0..b07658e72d01eb 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -10,7 +10,14 @@ from homeassistant.core import callback -from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DEFAULT_BAUDRATE, RadioType +from .const import ( + CLUSTER_TYPE_IN, + CLUSTER_TYPE_OUT, + DATA_ZHA, + DATA_ZHA_GATEWAY, + DEFAULT_BAUDRATE, + RadioType, +) from .registries import BINDABLE_CLUSTERS _LOGGER = logging.getLogger(__name__) @@ -132,6 +139,16 @@ def async_is_bindable_target(source_zha_device, target_zha_device): return False +async def async_get_zha_device(hass, device_id): + """Get a ZHA device for the given device registry id.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + registry_device = device_registry.async_get(device_id) + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee = convert_ieee(ieee_address) + return zha_gateway.devices[ieee] + + class LogMixin: """Log helper.""" diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py new file mode 100644 index 00000000000000..27e78507bfb317 --- /dev/null +++ b/homeassistant/components/zha/device_action.py @@ -0,0 +1,92 @@ +"""Provides device actions for ZHA devices.""" +from typing import List + +import voluptuous as vol + +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import config_validation as cv, service +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from . import DOMAIN +from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN +from .core.const import CHANNEL_IAS_WD +from .core.helpers import async_get_zha_device + +ACTION_SQUAWK = "squawk" +ACTION_WARN = "warn" +ATTR_DATA = "data" +ATTR_IEEE = "ieee" +CONF_ZHA_ACTION_TYPE = "zha_action_type" +ZHA_ACTION_TYPE_SERVICE_CALL = "service_call" + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN, vol.Required(CONF_TYPE): str} +) + +DEVICE_ACTIONS = { + CHANNEL_IAS_WD: [ + {CONF_TYPE: ACTION_SQUAWK, CONF_DOMAIN: DOMAIN}, + {CONF_TYPE: ACTION_WARN, CONF_DOMAIN: DOMAIN}, + ] +} + +DEVICE_ACTION_TYPES = { + ACTION_SQUAWK: ZHA_ACTION_TYPE_SERVICE_CALL, + ACTION_WARN: ZHA_ACTION_TYPE_SERVICE_CALL, +} + +SERVICE_NAMES = { + ACTION_SQUAWK: SERVICE_WARNING_DEVICE_SQUAWK, + ACTION_WARN: SERVICE_WARNING_DEVICE_WARN, +} + + +async def async_call_action_from_config( + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context, +) -> None: + """Perform an action based on configuration.""" + config = ACTION_SCHEMA(config) + await ZHA_ACTION_TYPES[DEVICE_ACTION_TYPES[config[CONF_TYPE]]]( + hass, config, variables, context + ) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions.""" + zha_device = await async_get_zha_device(hass, device_id) + actions = [ + action + for channel in DEVICE_ACTIONS + for action in DEVICE_ACTIONS[channel] + if channel in zha_device.cluster_channels + ] + for action in actions: + action[CONF_DEVICE_ID] = device_id + return actions + + +async def _execute_service_based_action( + hass: HomeAssistant, + config: ACTION_SCHEMA, + variables: TemplateVarsType, + context: Context, +) -> None: + action_type = config[CONF_TYPE] + service_name = SERVICE_NAMES[action_type] + zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + + service_action = { + service.CONF_SERVICE: "{}.{}".format(DOMAIN, service_name), + ATTR_DATA: {ATTR_IEEE: str(zha_device.ieee)}, + } + + await service.async_call_from_config( + hass, service_action, blocking=True, variables=variables, context=context + ) + + +ZHA_ACTION_TYPES = {ZHA_ACTION_TYPE_SERVICE_CALL: _execute_service_based_action} diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 46e3beafcaedfd..37ec14bc433a2b 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -9,8 +9,7 @@ from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from . import DOMAIN -from .core.const import DATA_ZHA, DATA_ZHA_GATEWAY -from .core.helpers import convert_ieee +from .core.helpers import async_get_zha_device CONF_SUBTYPE = "subtype" DEVICE = "device" @@ -26,7 +25,7 @@ async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" config = TRIGGER_SCHEMA(config) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - zha_device = await _async_get_zha_device(hass, config[CONF_DEVICE_ID]) + zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) if ( zha_device.device_automation_triggers is None @@ -52,7 +51,7 @@ async def async_get_triggers(hass, device_id): Make sure the device supports device automations and if it does return the trigger list. """ - zha_device = await _async_get_zha_device(hass, device_id) + zha_device = await async_get_zha_device(hass, device_id) if not zha_device.device_automation_triggers: return @@ -70,15 +69,3 @@ async def async_get_triggers(hass, device_id): ) return triggers - - -async def _async_get_zha_device(hass, device_id): - device_registry = await hass.helpers.device_registry.async_get_registry() - registry_device = device_registry.async_get(device_id) - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = convert_ieee(ieee_address) - zha_device = zha_gateway.devices[ieee] - if not zha_device: - raise InvalidDeviceAutomationConfig - return zha_device diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index cfc32a020c6dd5..a41f6de24be2b4 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -18,6 +18,10 @@ } }, "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Warn" + }, "trigger_type": { "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py new file mode 100644 index 00000000000000..6e7bc6ab4b13a7 --- /dev/null +++ b/tests/components/zha/test_device_action.py @@ -0,0 +1,133 @@ +"""The test for zha device automation actions.""" +from unittest.mock import patch + +import pytest + +import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + _async_get_device_automations as async_get_device_automations, +) +from homeassistant.components.zha import DOMAIN +from homeassistant.components.zha.core.const import CHANNEL_ON_OFF +from homeassistant.helpers.device_registry import async_get_registry +from homeassistant.setup import async_setup_component + +from .common import async_enable_traffic, async_init_zigpy_device + +from tests.common import async_mock_service, mock_coro + +SHORT_PRESS = "remote_button_short_press" +COMMAND = "command" +COMMAND_SINGLE = "single" + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "zha", "warning_device_warn") + + +async def test_get_actions(hass, config_entry, zha_gateway): + """Test we get the expected actions from a zha device.""" + from zigpy.zcl.clusters.general import Basic + from zigpy.zcl.clusters.security import IasZone, IasWd + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, + [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], + [], + None, + zha_gateway, + ) + + await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) + + actions = await async_get_device_automations(hass, "action", reg_device.id) + + expected_actions = [ + {"domain": DOMAIN, "type": "squawk", "device_id": reg_device.id}, + {"domain": DOMAIN, "type": "warn", "device_id": reg_device.id}, + ] + + assert actions == expected_actions + + +async def test_action(hass, config_entry, zha_gateway, calls): + """Test for executing a zha device action.""" + + from zigpy.zcl.clusters.general import Basic, OnOff + from zigpy.zcl.clusters.security import IasZone, IasWd + from zigpy.zcl.foundation import Status + + # create zigpy device + zigpy_device = await async_init_zigpy_device( + hass, + [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], + [OnOff.cluster_id], + None, + zha_gateway, + ) + + zigpy_device.device_automation_triggers = { + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE} + } + + await hass.config_entries.async_forward_entry_setup(config_entry, "switch") + await hass.async_block_till_done() + + hass.config_entries._entries.append(config_entry) + + zha_device = zha_gateway.get_device(zigpy_device.ieee) + ieee_address = str(zha_device.ieee) + + ha_device_registry = await async_get_registry(hass) + reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, zha_gateway, [zha_device]) + + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": SHORT_PRESS, + "subtype": SHORT_PRESS, + }, + "action": { + "domain": DOMAIN, + "device_id": reg_device.id, + "type": "warn", + }, + } + ] + }, + ) + + await hass.async_block_till_done() + + on_off_channel = zha_device.cluster_channels[CHANNEL_ON_OFF] + on_off_channel.zha_send_event(on_off_channel.cluster, COMMAND_SINGLE, []) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == DOMAIN + assert calls[0].service == "warning_device_warn" + assert calls[0].data["ieee"] == ieee_address diff --git a/tests/components/zha/test_device_automation.py b/tests/components/zha/test_device_trigger.py similarity index 99% rename from tests/components/zha/test_device_automation.py rename to tests/components/zha/test_device_trigger.py index 5a4b9d5616e8fe..2f4ddb6b8b2cf3 100644 --- a/tests/components/zha/test_device_automation.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,4 +1,4 @@ -"""ZHA device automation tests.""" +"""ZHA device automation trigger tests.""" from unittest.mock import patch import pytest From eeffd090a31d7612977e6076b98523028fc5bc01 Mon Sep 17 00:00:00 2001 From: Andrew Onyshchuk Date: Fri, 27 Sep 2019 12:21:04 -0500 Subject: [PATCH 0487/3953] Add support for Z-Wave battery level (#26943) * Add support for Z-Wave battery level * Improve coverage --- .../components/zwave/discovery_schemas.py | 1 + homeassistant/components/zwave/sensor.py | 13 ++++++++++++- tests/components/zwave/test_sensor.py | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index dbec1484508cf9..e2254073290e86 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -277,6 +277,7 @@ const.COMMAND_CLASS_ALARM, const.COMMAND_CLASS_SENSOR_ALARM, const.COMMAND_CLASS_INDICATOR, + const.COMMAND_CLASS_BATTERY, ], const.DISC_GENRE: const.GENRE_USER, } diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index 240809548ee658..0820feb8d0f4d8 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -1,7 +1,7 @@ """Support for Z-Wave sensors.""" import logging from homeassistant.core import callback -from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor import DOMAIN, DEVICE_CLASS_BATTERY from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import const, ZWaveDeviceEntity @@ -28,6 +28,8 @@ def async_add_sensor(sensor): def get_device(node, values, **kwargs): """Create Z-Wave entity device.""" # Generic Device mappings + if values.primary.command_class == const.COMMAND_CLASS_BATTERY: + return ZWaveBatterySensor(values) if node.has_command_class(const.COMMAND_CLASS_SENSOR_MULTILEVEL): return ZWaveMultilevelSensor(values) if ( @@ -107,3 +109,12 @@ class ZWaveAlarmSensor(ZWaveSensor): """ pass + + +class ZWaveBatterySensor(ZWaveSensor): + """Representation of Z-Wave device battery level.""" + + @property + def device_class(self): + """Return the class of this device.""" + return DEVICE_CLASS_BATTERY diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py index f18a66d9a5b676..cec93f5af4ac64 100644 --- a/tests/components/zwave/test_sensor.py +++ b/tests/components/zwave/test_sensor.py @@ -53,6 +53,23 @@ def test_get_device_detects_multilevel_meter(mock_openzwave): assert isinstance(device, sensor.ZWaveMultilevelSensor) +def test_get_device_detects_battery_sensor(mock_openzwave): + """Test get_device returns a Z-Wave battery sensor.""" + + node = MockNode(command_classes=[const.COMMAND_CLASS_BATTERY]) + value = MockValue( + data=0, + node=node, + type=const.TYPE_DECIMAL, + command_class=const.COMMAND_CLASS_BATTERY, + ) + values = MockEntityValues(primary=value) + + device = sensor.get_device(node=node, values=values, node_config={}) + assert isinstance(device, sensor.ZWaveBatterySensor) + assert device.device_class == homeassistant.const.DEVICE_CLASS_BATTERY + + def test_multilevelsensor_value_changed_temp_fahrenheit(mock_openzwave): """Test value changed for Z-Wave multilevel sensor for temperature.""" node = MockNode( From 80bc15e24b5c5665aa26ece36630dc6d6badc9de Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 27 Sep 2019 21:51:46 +0200 Subject: [PATCH 0488/3953] Update Alexa discovery description (#26933) * Update Alexa discovery description * Update description * Fix test * Filter special chars --- homeassistant/components/alexa/entities.py | 20 +++++++++++++------- tests/components/alexa/test_smart_home.py | 18 +++++++++++++----- tests/components/cloud/test_client.py | 2 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 03d153f5927c31..f0d72af23d50f4 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -52,6 +52,8 @@ ENTITY_ADAPTERS = Registry() +TRANSLATION_TABLE = dict.fromkeys(map(ord, r"}{\/|\"()[]+~!><*%"), None) + class DisplayCategory: """Possible display categories for Discovery response. @@ -134,15 +136,18 @@ def entity_id(self): def friendly_name(self): """Return the Alexa API friendly name.""" - return self.entity_conf.get(CONF_NAME, self.entity.name) + return self.entity_conf.get(CONF_NAME, self.entity.name).translate( + TRANSLATION_TABLE + ) def description(self): """Return the Alexa API description.""" - return self.entity_conf.get(CONF_DESCRIPTION, self.entity.entity_id) + description = self.entity_conf.get(CONF_DESCRIPTION) or self.entity_id + return f"{description} via Home Assistant".translate(TRANSLATION_TABLE) def alexa_id(self): """Return the Alexa API entity id.""" - return self.entity.entity_id.replace(".", "#") + return self.entity.entity_id.replace(".", "#").translate(TRANSLATION_TABLE) def display_categories(self): """Return a list of display categories.""" @@ -389,10 +394,11 @@ class SceneCapabilities(AlexaEntity): """Class to represent Scene capabilities.""" def description(self): - """Return the description of the entity.""" - # Required description as per Amazon Scene docs - scene_fmt = "{} (Scene connected via Home Assistant)" - return scene_fmt.format(AlexaEntity.description(self)) + """Return the Alexa API description.""" + description = AlexaEntity.description(self) + if "scene" not in description.casefold(): + return f"{description} (Scene)" + return description def default_display_categories(self): """Return the display categories for this entity.""" diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index bd5a4d25edd10c..3cafa89902471f 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1162,14 +1162,16 @@ async def test_entity_config(hass): request = get_new_request("Alexa.Discovery", "Discover") hass.states.async_set("light.test_1", "on", {"friendly_name": "Test light 1"}) + hass.states.async_set("scene.test_1", "scening", {"friendly_name": "Test 1"}) alexa_config = MockConfig(hass) alexa_config.entity_config = { "light.test_1": { - "name": "Config name", + "name": "Config *name*", "display_categories": "SWITCH", - "description": "Config description", - } + "description": "Config >! Date: Fri, 27 Sep 2019 12:54:17 -0700 Subject: [PATCH 0489/3953] Add templates to scaffold device_trigger, device_condition, (#26871) device_action --- .../components/zha/device_trigger.py | 4 +- script/scaffold/__main__.py | 4 +- script/scaffold/docs.py | 27 ++++ .../integration/device_action.py | 84 +++++++++++ .../device_action/tests/test_device_action.py | 103 ++++++++++++++ .../integration/device_condition.py | 64 +++++++++ .../tests/test_device_condition.py | 125 +++++++++++++++++ .../integration/device_trigger.py | 100 +++++++++++++ .../tests/test_device_trigger.py | 131 ++++++++++++++++++ 9 files changed, 638 insertions(+), 4 deletions(-) create mode 100644 script/scaffold/templates/device_action/integration/device_action.py create mode 100644 script/scaffold/templates/device_action/tests/test_device_action.py create mode 100644 script/scaffold/templates/device_condition/integration/device_condition.py create mode 100644 script/scaffold/templates/device_condition/tests/test_device_condition.py create mode 100644 script/scaffold/templates/device_trigger/integration/device_trigger.py create mode 100644 script/scaffold/templates/device_trigger/tests/test_device_trigger.py diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 37ec14bc433a2b..331dc3d32968d3 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -35,13 +35,13 @@ async def async_attach_trigger(hass, config, action, automation_info): trigger = zha_device.device_automation_triggers[trigger] - state_config = { + event_config = { event.CONF_EVENT_TYPE: ZHA_EVENT, event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, } return await event.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, event_config, action, automation_info, platform_type="device" ) diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 22cdee8f69e0e5..2258840f430d92 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -65,10 +65,10 @@ def main(): print() print("Running tests") - print(f"$ pytest -v tests/components/{info.domain}") + print(f"$ pytest -vvv tests/components/{info.domain}") if ( subprocess.run( - f"pytest -v tests/components/{info.domain}", shell=True + f"pytest -vvv tests/components/{info.domain}", shell=True ).returncode != 0 ): diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 801b8ebb5fd8b0..47cb9709c3d405 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -29,5 +29,32 @@ def print_relevant_docs(template: str, info: Info) -> None: - {info.tests_dir / "test_reproduce_state.py"} Please update the relevant items marked as TODO before submitting a pull request. +""" + ) + + elif template == "device_trigger": + print( + f""" +Device trigger base has been added to the {info.domain} integration: + - {info.integration_dir / "device_trigger.py"} + - {info.tests_dir / "test_device_trigger.py"} +""" + ) + + elif template == "device_condition": + print( + f""" +Device condition base has been added to the {info.domain} integration: + - {info.integration_dir / "device_condition.py"} + - {info.tests_dir / "test_device_condition.py"} +""" + ) + + elif template == "device_action": + print( + f""" +Device action base has been added to the {info.domain} integration: + - {info.integration_dir / "device_action.py"} + - {info.tests_dir / "test_device_action.py"} """ ) diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py new file mode 100644 index 00000000000000..d5674f01b2de4a --- /dev/null +++ b/script/scaffold/templates/device_action/integration/device_action.py @@ -0,0 +1,84 @@ +"""Provides device automations for NEW_NAME.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + SERVICE_TURN_ON, + SERVICE_TURN_OFF, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN + +# TODO specify your supported action types. +ACTION_TYPES = {"turn_on", "turn_off"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for NEW_NAME devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # TODO Read this comment and remove it. + # This example shows how to iterate over the entities of this device + # that match this integration. If your actions instead rely on + # calling services, do something like: + # zha_device = await _async_get_zha_device(hass, device_id) + # return zha_device.device_actions + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add actions for each entity that belongs to this integration + # TODO add your own actions. + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_on", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_off", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "turn_on": + service = SERVICE_TURN_ON + elif config[CONF_TYPE] == "turn_off": + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) diff --git a/script/scaffold/templates/device_action/tests/test_device_action.py b/script/scaffold/templates/device_action/tests/test_device_action.py new file mode 100644 index 00000000000000..f8a00bf1ec8232 --- /dev/null +++ b/script/scaffold/templates/device_action/tests/test_device_action.py @@ -0,0 +1,103 @@ +"""The tests for NEW_NAME device actions.""" +import pytest + +from homeassistant.components.NEW_DOMAIN import DOMAIN +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": "NEW_DOMAIN.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": "NEW_DOMAIN.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert actions == expected_actions + + +async def test_action(hass): + """Test for turn_on and turn_off actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_turn_off", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "NEW_DOMAIN.entity", + "type": "turn_off", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_turn_on", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "NEW_DOMAIN.entity", + "type": "turn_on", + }, + }, + ] + }, + ) + + turn_off_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_off") + turn_on_calls = async_mock_service(hass, "NEW_DOMAIN", "turn_on") + + hass.bus.async_fire("test_event_turn_off") + await hass.async_block_till_done() + assert len(turn_off_calls) == 1 + assert len(turn_on_calls) == 0 + + hass.bus.async_fire("test_event_turn_on") + await hass.async_block_till_done() + assert len(turn_off_calls) == 1 + assert len(turn_on_calls) == 1 diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py new file mode 100644 index 00000000000000..d19fa8817a0299 --- /dev/null +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -0,0 +1,64 @@ +"""Provides device automations for NEW_NAME.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +# TODO specify your supported condition types. +CONDITION_TYPES = {"is_on"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES)} +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[str]: + """List device conditions for NEW_NAME devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add conditions for each entity that belongs to this integration + # TODO add your own conditions. + conditions.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_on", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + + def test_is_on(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is on.""" + return condition.state(hass, config[ATTR_ENTITY_ID], STATE_ON) + + return test_is_on diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py new file mode 100644 index 00000000000000..d9cef083510b0f --- /dev/null +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -0,0 +1,125 @@ +"""The tests for NEW_NAME device conditions.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("NEW_DOMAIN.entity", STATE_ON) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on - event - test_event1" + + hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off - event - test_event2" diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py new file mode 100644 index 00000000000000..f7e9fc091f8809 --- /dev/null +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -0,0 +1,100 @@ +"""Provides device automations for NEW_NAME.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_ON, + STATE_OFF, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import DOMAIN + +# TODO specify your supported trigger types. +TRIGGER_TYPES = {"turned_on", "turned_off"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + {vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES)} +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for NEW_NAME devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # TODO Read this comment and remove it. + # This example shows how to iterate over the entities of this device + # that match this integration. If your triggers instead rely on + # events fired by devices without entities, do something like: + # zha_device = await _async_get_zha_device(hass, device_id) + # return zha_device.device_triggers + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add triggers for each entity that belongs to this integration + # TODO add your own triggers. + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turned_on", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turned_off", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + # TODO Implement your own logic to attach triggers. + # Generally we suggest to re-use the existing state or event + # triggers from the automation integration. + + if config[CONF_TYPE] == "turned_on": + from_state = STATE_OFF + to_state = STATE_ON + else: + from_state = STATE_ON + to_state = STATE_OFF + + return state.async_attach_trigger( + hass, + { + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + }, + action, + automation_info, + platform_type="device", + ) diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py new file mode 100644 index 00000000000000..c22197bb136832 --- /dev/null +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -0,0 +1,131 @@ +"""The tests for NEW_NAME device triggers.""" +import pytest + +from homeassistant.components.switch import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a switch.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "turned_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "turn_on - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "NEW_DOMAIN.entity", + "type": "turned_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "turn_off - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is turning on. + hass.states.async_set("NEW_DOMAIN.entity", STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "turn_on - device - {} - off - on - None".format( + "NEW_DOMAIN.entity" + ) + + # Fake that the entity is turning off. + hass.states.async_set("NEW_DOMAIN.entity", STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_off - device - {} - on - off - None".format( + "NEW_DOMAIN.entity" + ) From fde128d66ce7634c7d776b7fba2dba71f8e26122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 27 Sep 2019 22:57:59 +0300 Subject: [PATCH 0490/3953] Upgrade mypy to 0.730, address raised issues (#26959) https://mypy-lang.blogspot.com/2019/09/mypy-730-released.html --- homeassistant/auth/mfa_modules/notify.py | 6 ++++-- homeassistant/auth/mfa_modules/totp.py | 5 +++-- homeassistant/components/http/static.py | 8 +++++--- homeassistant/core.py | 4 ++-- homeassistant/helpers/config_validation.py | 3 +-- homeassistant/helpers/deprecation.py | 2 +- homeassistant/util/async_.py | 2 +- homeassistant/util/location.py | 4 ++-- homeassistant/util/logging.py | 6 ++++-- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 25 insertions(+), 19 deletions(-) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 01c5c12efb7588..b14f5fedc22f55 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -251,8 +251,10 @@ async def async_notify_user(self, user_id: str, code: str) -> None: _LOGGER.error("Cannot find user %s", user_id) return - await self.async_notify( # type: ignore - code, notify_setting.notify_service, notify_setting.target + await self.async_notify( + code, + notify_setting.notify_service, # type: ignore + notify_setting.target, ) async def async_notify( diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 4e417fca2198b1..9829044a53ece5 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -215,8 +215,9 @@ async def async_step_init( else: hass = self._auth_module.hass - self._ota_secret, self._url, self._image = await hass.async_add_executor_job( # type: ignore - _generate_secret_and_qr_code, str(self._user.name) + self._ota_secret, self._url, self._image = await hass.async_add_executor_job( + _generate_secret_and_qr_code, # type: ignore + str(self._user.name), ) return self.async_show_form( diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 952ca473fdc5f8..e6a70c9f64369a 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -42,8 +42,10 @@ async def _handle(self, request): if filepath.is_dir(): return await super()._handle(request) if filepath.is_file(): - # type ignore: https://github.com/aio-libs/aiohttp/pull/3976 - return FileResponse( # type: ignore - filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS + return FileResponse( + filepath, + chunk_size=self._chunk_size, + # type ignore: https://github.com/aio-libs/aiohttp/pull/3976 + headers=CACHE_HEADERS, # type: ignore ) raise HTTPNotFound diff --git a/homeassistant/core.py b/homeassistant/core.py index 31761f2560faf4..e011db33c343a8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -144,8 +144,8 @@ def async_loop_exception_handler(_: Any, context: Dict) -> None: if exception: kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) - _LOGGER.error( # type: ignore - "Error doing job: %s", context["message"], **kwargs + _LOGGER.error( + "Error doing job: %s", context["message"], **kwargs # type: ignore ) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index d567962e328a8e..d0aeb4f4968bc6 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -598,8 +598,7 @@ def deprecated( else: # Unclear when it is None, but it happens, so let's guard. # https://github.com/home-assistant/home-assistant/issues/24982 - # type ignore/unreachable: https://github.com/python/typeshed/pull/3137 - module_name = __name__ # type: ignore + module_name = __name__ if replacement_key and invalidation_version: warning = ( diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index db1c34d8fd4529..881534b5bed70c 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -54,7 +54,7 @@ def get_deprecated( and a warning is issued to the user. """ if old_name in config: - module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ + module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ # type: ignore logger = logging.getLogger(module_name) logger.warning( "'%s' is deprecated. Please rename '%s' to '%s' in your " diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 271b9caa62ae59..d43658d1584269 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -24,7 +24,7 @@ try: # pylint: disable=invalid-name - asyncio_run = asyncio.run + asyncio_run = asyncio.run # type: ignore except AttributeError: _T = TypeVar("_T") diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 7c61a8ab1e9197..f81c40a52bb675 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -6,7 +6,7 @@ import asyncio import collections import math -from typing import Any, Optional, Tuple, Dict +from typing import Any, Optional, Tuple, Dict, cast import aiohttp @@ -159,7 +159,7 @@ def vincenty( if miles: s *= MILES_PER_KILOMETER # kilometers to miles - return round(s, 6) + return round(cast(float, s), 6) async def _get_ipapi(session: aiohttp.ClientSession) -> Optional[Dict[str, Any]]: diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 236e2fc1aa24de..79cb2607b1045f 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -130,7 +130,7 @@ def catch_log_exception( """Decorate a callback to catch and log exceptions.""" def log_exception(*args: Any) -> None: - module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ + module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ # type: ignore # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) @@ -178,7 +178,9 @@ async def coro_wrapper(*args: Any) -> Any: try: return await target except Exception: # pylint: disable=broad-except - module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ + module_name = inspect.getmodule( # type: ignore + inspect.trace()[1][0] + ).__name__ # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) diff --git a/requirements_test.txt b/requirements_test.txt index ae4401178b1809..7e5be09a28ba9c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ codecov==2.0.15 flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 -mypy==0.720 +mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3f40e16349292..a860c67dbd1368 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ codecov==2.0.15 flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 -mypy==0.720 +mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.3.1 From 58446c79fc31216898c50dd1bfa0ef3709c9a75e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Sep 2019 13:08:30 -0700 Subject: [PATCH 0491/3953] Update scaffold text --- script/scaffold/docs.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 47cb9709c3d405..ab87799d6b2ff4 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -28,7 +28,8 @@ def print_relevant_docs(template: str, info: Info) -> None: - {info.integration_dir / "reproduce_state.py"} - {info.tests_dir / "test_reproduce_state.py"} -Please update the relevant items marked as TODO before submitting a pull request. +You will now need to update the code to make sure that every attribute +that can occur in the state will cause the right service to be called. """ ) @@ -38,6 +39,9 @@ def print_relevant_docs(template: str, info: Info) -> None: Device trigger base has been added to the {info.domain} integration: - {info.integration_dir / "device_trigger.py"} - {info.tests_dir / "test_device_trigger.py"} + +You will now need to update the code to make sure that relevant triggers +are exposed. """ ) @@ -47,6 +51,9 @@ def print_relevant_docs(template: str, info: Info) -> None: Device condition base has been added to the {info.domain} integration: - {info.integration_dir / "device_condition.py"} - {info.tests_dir / "test_device_condition.py"} + +You will now need to update the code to make sure that relevant condtions +are exposed. """ ) @@ -56,5 +63,8 @@ def print_relevant_docs(template: str, info: Info) -> None: Device action base has been added to the {info.domain} integration: - {info.integration_dir / "device_action.py"} - {info.tests_dir / "test_device_action.py"} + +You will now need to update the code to make sure that relevant services +are exposed as actions. """ ) From fc3f5163f13e202889260db477e7173a6cfaa34c Mon Sep 17 00:00:00 2001 From: Khole Date: Fri, 27 Sep 2019 22:18:34 +0100 Subject: [PATCH 0492/3953] Add hive boost to climate and water_heater (#26789) * Start the Boost work * Add services.yaml * Added Services #2 * Start the Boost work * Add services.yaml * Added Services #2 * Working Services * pyhiveapi to 0.2.19 * Update Libary to 0.2.19 * Update Water_heater boost * Added Async hass add function * Update Services * Reviewed Changes * Fixed Refresh System * Review 2 * Moved device iteration to the platform * update * Updates #2 * Review#3 New Base Class * Review #5 * Update homeassistant/components/hive/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/hive/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/hive/__init__.py Co-Authored-By: Martin Hjelmare * Review 6 * Review 7 * Removed Child classes to inhertit from the parent --- homeassistant/components/hive/__init__.py | 134 ++++++++++++++++-- .../components/hive/binary_sensor.py | 28 +--- homeassistant/components/hive/climate.py | 59 +++----- homeassistant/components/hive/light.py | 36 ++--- homeassistant/components/hive/manifest.json | 2 +- homeassistant/components/hive/sensor.py | 35 ++--- homeassistant/components/hive/services.yaml | 27 ++++ homeassistant/components/hive/switch.py | 33 ++--- homeassistant/components/hive/water_heater.py | 49 ++----- requirements_all.txt | 2 +- 10 files changed, 218 insertions(+), 187 deletions(-) create mode 100644 homeassistant/components/hive/services.yaml diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index fc96f2d8c966f7..c11eb18accad98 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -1,17 +1,32 @@ -"""Support for the Hive devices.""" +"""Support for the Hive devices and services.""" +from functools import wraps import logging from pyhiveapi import Pyhiveapi import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) DOMAIN = "hive" DATA_HIVE = "data_hive" +SERVICES = ["Heating", "HotWater"] +SERVICE_BOOST_HOTWATER = "boost_hotwater" +SERVICE_BOOST_HEATING = "boost_heating" +ATTR_TIME_PERIOD = "time_period" +ATTR_MODE = "on_off" DEVICETYPES = { "binary_sensor": "device_list_binary_sensor", "climate": "device_list_climate", @@ -34,11 +49,31 @@ extra=vol.ALLOW_EXTRA, ) +BOOST_HEATING_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_TIME_PERIOD): vol.All( + cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() // 60 + ), + vol.Optional(ATTR_TEMPERATURE, default="25.0"): vol.Coerce(float), + } +) + +BOOST_HOTWATER_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Optional(ATTR_TIME_PERIOD, default="00:30:00"): vol.All( + cv.time_period, cv.positive_timedelta, lambda td: td.total_seconds() // 60 + ), + vol.Required(ATTR_MODE): cv.string, + } +) + class HiveSession: """Initiate Hive Session Class.""" - entities = [] + entity_lookup = {} core = None heating = None hotwater = None @@ -51,6 +86,35 @@ class HiveSession: def setup(hass, config): """Set up the Hive Component.""" + + def heating_boost(service): + """Handle the service call.""" + node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID]) + if not node_id: + # log or raise error + _LOGGER.error("Cannot boost entity id entered") + return + + minutes = service.data[ATTR_TIME_PERIOD] + temperature = service.data[ATTR_TEMPERATURE] + + session.heating.turn_boost_on(node_id, minutes, temperature) + + def hotwater_boost(service): + """Handle the service call.""" + node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID]) + if not node_id: + # log or raise error + _LOGGER.error("Cannot boost entity id entered") + return + minutes = service.data[ATTR_TIME_PERIOD] + mode = service.data[ATTR_MODE] + + if mode == "on": + session.hotwater.turn_boost_on(node_id, minutes) + elif mode == "off": + session.hotwater.turn_boost_off(node_id) + session = HiveSession() session.core = Pyhiveapi() @@ -58,9 +122,9 @@ def setup(hass, config): password = config[DOMAIN][CONF_PASSWORD] update_interval = config[DOMAIN][CONF_SCAN_INTERVAL] - devicelist = session.core.initialise_api(username, password, update_interval) + devices = session.core.initialise_api(username, password, update_interval) - if devicelist is None: + if devices is None: _LOGGER.error("Hive API initialization failed") return False @@ -73,9 +137,59 @@ def setup(hass, config): session.attributes = Pyhiveapi.Attributes() hass.data[DATA_HIVE] = session - for ha_type, hive_type in DEVICETYPES.items(): - for key, devices in devicelist.items(): - if key == hive_type: - for hivedevice in devices: - load_platform(hass, ha_type, DOMAIN, hivedevice, config) + for ha_type in DEVICETYPES: + devicelist = devices.get(DEVICETYPES[ha_type]) + if devicelist: + load_platform(hass, ha_type, DOMAIN, devicelist, config) + if ha_type == "climate": + hass.services.register( + DOMAIN, + SERVICE_BOOST_HEATING, + heating_boost, + schema=BOOST_HEATING_SCHEMA, + ) + if ha_type == "water_heater": + hass.services.register( + DOMAIN, + SERVICE_BOOST_HEATING, + hotwater_boost, + schema=BOOST_HOTWATER_SCHEMA, + ) + return True + + +def refresh_system(func): + """Force update all entities after state change.""" + + @wraps(func) + def wrapper(self, *args, **kwargs): + func(self, *args, **kwargs) + dispatcher_send(self.hass, DOMAIN) + + return wrapper + + +class HiveEntity(Entity): + """Initiate Hive Base Class.""" + + def __init__(self, session, hive_device): + """Initialize the instance.""" + self.node_id = hive_device["Hive_NodeID"] + self.node_name = hive_device["Hive_NodeName"] + self.device_type = hive_device["HA_DeviceType"] + self.node_device_type = hive_device["Hive_DeviceType"] + self.session = session + self.attributes = {} + self._unique_id = f"{self.node_id}-{self.device_type}" + + async def async_added_to_hass(self): + """When entity is added to Home Assistant.""" + async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) + if self.device_type in SERVICES: + self.session.entity_lookup[self.entity_id] = self.node_id + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 50c8277302fbc7..ce7e53b77a5263 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,7 +1,7 @@ """Support for the Hive binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity DEVICETYPE_DEVICE_CLASS = {"motionsensor": "motion", "contactsensor": "opening"} @@ -10,26 +10,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive sensor devices.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - add_entities([HiveBinarySensorEntity(session, discovery_info)]) + session = hass.data.get(DATA_HIVE) + devs = [] + for dev in discovery_info: + devs.append(HiveBinarySensorEntity(session, dev)) + add_entities(devs) -class HiveBinarySensorEntity(BinarySensorDevice): +class HiveBinarySensorEntity(HiveEntity, BinarySensorDevice): """Representation of a Hive binary sensor.""" - def __init__(self, hivesession, hivedevice): - """Initialize the hive sensor.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.node_device_type = hivedevice["Hive_DeviceType"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) - @property def unique_id(self): """Return unique ID of entity.""" @@ -40,11 +31,6 @@ def device_info(self): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def device_class(self): """Return the class of this sensor.""" diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 861957e6ef0166..1fb77ce6cb9731 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -5,13 +5,14 @@ HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_BOOST, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from . import DATA_HIVE, DOMAIN + +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system HIVE_TO_HASS_STATE = { "SCHEDULE": HVAC_MODE_AUTO, @@ -34,28 +35,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive climate devices.""" if discovery_info is None: return - if discovery_info["HA_DeviceType"] != "Heating": - return session = hass.data.get(DATA_HIVE) - climate = HiveClimateEntity(session, discovery_info) - - add_entities([climate]) + devs = [] + for dev in discovery_info: + devs.append(HiveClimateEntity(session, dev)) + add_entities(devs) -class HiveClimateEntity(ClimateDevice): +class HiveClimateEntity(HiveEntity, ClimateDevice): """Hive Climate Device.""" - def __init__(self, hivesession, hivedevice): + def __init__(self, hive_session, hive_device): """Initialize the Climate device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.thermostat_node_id = hivedevice["Thermostat_NodeID"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" + super().__init__(hive_session, hive_device) + self.thermostat_node_id = hive_device["Thermostat_NodeID"] @property def unique_id(self): @@ -72,11 +66,6 @@ def supported_features(self): """Return the list of supported features.""" return SUPPORT_FLAGS - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of the Climate device.""" @@ -99,7 +88,7 @@ def hvac_modes(self): return SUPPORT_HVAC @property - def hvac_mode(self) -> str: + def hvac_mode(self): """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. @@ -143,43 +132,29 @@ def preset_modes(self): """Return a list of available preset modes.""" return SUPPORT_PRESET - async def async_added_to_hass(self): - """When entity is added to Home Assistant.""" - await super().async_added_to_hass() - self.session.entities.append(self) - + @refresh_system def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" new_mode = HASS_TO_HIVE_STATE[hvac_mode] self.session.heating.set_mode(self.node_id, new_mode) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - + @refresh_system def set_temperature(self, **kwargs): """Set new target temperature.""" new_temperature = kwargs.get(ATTR_TEMPERATURE) if new_temperature is not None: self.session.heating.set_target_temperature(self.node_id, new_temperature) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - - def set_preset_mode(self, preset_mode) -> None: + @refresh_system + def set_preset_mode(self, preset_mode): """Set new preset mode.""" if preset_mode == PRESET_NONE and self.preset_mode == PRESET_BOOST: self.session.heating.turn_boost_off(self.node_id) - elif preset_mode == PRESET_BOOST: - curtemp = self.session.heating.current_temperature(self.node_id) - curtemp = round(curtemp * 2) / 2 + curtemp = round(self.current_temperature * 2) / 2 temperature = curtemp + 0.5 - self.session.heating.turn_boost_on(self.node_id, 30, temperature) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index a85c3a43992eb8..41fc286d13b0e7 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -10,32 +10,28 @@ ) import homeassistant.util.color as color_util -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive light devices.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - add_entities([HiveDeviceLight(session, discovery_info)]) + session = hass.data.get(DATA_HIVE) + devs = [] + for dev in discovery_info: + devs.append(HiveDeviceLight(session, dev)) + add_entities(devs) -class HiveDeviceLight(Light): +class HiveDeviceLight(HiveEntity, Light): """Hive Active Light Device.""" - def __init__(self, hivesession, hivedevice): + def __init__(self, hive_session, hive_device): """Initialize the Light device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.light_device_type = hivedevice["Hive_Light_DeviceType"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) + super().__init__(hive_session, hive_device) + self.light_device_type = hive_device["Hive_Light_DeviceType"] @property def unique_id(self): @@ -47,11 +43,6 @@ def device_info(self): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the display name of this light.""" @@ -106,6 +97,7 @@ def is_on(self): """Return true if light is on.""" return self.session.light.get_state(self.node_id) + @refresh_system def turn_on(self, **kwargs): """Instruct the light to turn on.""" new_brightness = None @@ -134,14 +126,10 @@ def turn_on(self, **kwargs): new_color, ) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - + @refresh_system def turn_off(self, **kwargs): """Instruct the light to turn off.""" self.session.light.turn_off(self.node_id) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) @property def supported_features(self): diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 886d6841ebb850..2e7c4f4f179dee 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "documentation": "https://www.home-assistant.io/components/hive", "requirements": [ - "pyhiveapi==0.2.18.1" + "pyhiveapi==0.2.19" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index c43fe461a8e6a1..ccd635015de320 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -2,7 +2,7 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity FRIENDLY_NAMES = { "Hub_OnlineStatus": "Hive Hub Status", @@ -19,28 +19,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive sensor devices.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - if ( - discovery_info["HA_DeviceType"] == "Hub_OnlineStatus" - or discovery_info["HA_DeviceType"] == "Hive_OutsideTemperature" - ): - add_entities([HiveSensorEntity(session, discovery_info)]) + session = hass.data.get(DATA_HIVE) + devs = [] + for dev in discovery_info: + if dev["HA_DeviceType"] in FRIENDLY_NAMES: + devs.append(HiveSensorEntity(session, dev)) + add_entities(devs) -class HiveSensorEntity(Entity): +class HiveSensorEntity(HiveEntity, Entity): """Hive Sensor Entity.""" - def __init__(self, hivesession, hivedevice): - """Initialize the sensor.""" - self.node_id = hivedevice["Hive_NodeID"] - self.device_type = hivedevice["HA_DeviceType"] - self.node_device_type = hivedevice["Hive_DeviceType"] - self.session = hivesession - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) - @property def unique_id(self): """Return unique ID of entity.""" @@ -51,11 +41,6 @@ def device_info(self): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of the sensor.""" @@ -82,6 +67,4 @@ def icon(self): def update(self): """Update all Node data from Hive.""" - if self.session.core.update_data(self.node_id): - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) + self.session.core.update_data(self.node_id) diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml new file mode 100644 index 00000000000000..27d7acfc83bab0 --- /dev/null +++ b/homeassistant/components/hive/services.yaml @@ -0,0 +1,27 @@ +boost_heating: + description: "Set the boost mode ON defining the period of time and the desired target temperature + for the boost." + fields: + entity_id: + { + description: Enter the entity_id for the device required to set the boost mode., + example: "climate.heating", + } + time_period: + { description: Set the time period for the boost., example: "01:30:00" } + temperature: + { + description: Set the target temperature for the boost period., + example: "20.5", + } +boost_hotwater: + description: + "Set the boost mode ON or OFF defining the period of time for the boost." + fields: + entity_id: + { + description: Enter the entity_id for the device reuired to set the boost mode., + example: "water_heater.hot_water", + } + time_period: { description: Set the time period for the boost., example: "01:30:00" } + on_off: { description: Set the boost function on or off., example: "on" } diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 75efdfe3e5d461..1447f5483a4a0e 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,32 +1,24 @@ """Support for the Hive switches.""" from homeassistant.components.switch import SwitchDevice -from . import DATA_HIVE, DOMAIN +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hive switches.""" if discovery_info is None: return - session = hass.data.get(DATA_HIVE) - add_entities([HiveDevicePlug(session, discovery_info)]) + session = hass.data.get(DATA_HIVE) + devs = [] + for dev in discovery_info: + devs.append(HiveDevicePlug(session, dev)) + add_entities(devs) -class HiveDevicePlug(SwitchDevice): +class HiveDevicePlug(HiveEntity, SwitchDevice): """Hive Active Plug.""" - def __init__(self, hivesession, hivedevice): - """Initialize the Switch device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.session = hivesession - self.attributes = {} - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self.session.entities.append(self) - @property def unique_id(self): """Return unique ID of entity.""" @@ -37,11 +29,6 @@ def device_info(self): """Return device information.""" return {"identifiers": {(DOMAIN, self.unique_id)}, "name": self.name} - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of this Switch device if any.""" @@ -62,17 +49,15 @@ def is_on(self): """Return true if switch is on.""" return self.session.switch.get_state(self.node_id) + @refresh_system def turn_on(self, **kwargs): """Turn the switch on.""" self.session.switch.turn_on(self.node_id) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) + @refresh_system def turn_off(self, **kwargs): """Turn the device off.""" self.session.switch.turn_off(self.node_id) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) def update(self): """Update all Node data from Hive.""" diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 1b009582c1aec7..c60a9ec01d1fcf 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -1,51 +1,36 @@ """Support for hive water heaters.""" -from homeassistant.const import TEMP_CELSIUS - from homeassistant.components.water_heater import ( STATE_ECO, - STATE_ON, STATE_OFF, + STATE_ON, SUPPORT_OPERATION_MODE, WaterHeaterDevice, ) - -from . import DATA_HIVE, DOMAIN +from homeassistant.const import TEMP_CELSIUS +from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE HIVE_TO_HASS_STATE = {"SCHEDULE": STATE_ECO, "ON": STATE_ON, "OFF": STATE_OFF} - HASS_TO_HIVE_STATE = {STATE_ECO: "SCHEDULE", STATE_ON: "ON", STATE_OFF: "OFF"} - SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF] def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Wink water heater devices.""" + """Set up the Hive water heater devices.""" if discovery_info is None: return - if discovery_info["HA_DeviceType"] != "HotWater": - return session = hass.data.get(DATA_HIVE) - water_heater = HiveWaterHeater(session, discovery_info) + devs = [] + for dev in discovery_info: + devs.append(HiveWaterHeater(session, dev)) + add_entities(devs) - add_entities([water_heater]) - -class HiveWaterHeater(WaterHeaterDevice): +class HiveWaterHeater(HiveEntity, WaterHeaterDevice): """Hive Water Heater Device.""" - def __init__(self, hivesession, hivedevice): - """Initialize the Water Heater device.""" - self.node_id = hivedevice["Hive_NodeID"] - self.node_name = hivedevice["Hive_NodeName"] - self.device_type = hivedevice["HA_DeviceType"] - self.session = hivesession - self.data_updatesource = f"{self.device_type}.{self.node_id}" - self._unique_id = f"{self.node_id}-{self.device_type}" - self._unit_of_measurement = TEMP_CELSIUS - @property def unique_id(self): """Return unique ID of entity.""" @@ -61,11 +46,6 @@ def supported_features(self): """Return the list of supported features.""" return SUPPORT_FLAGS_HEATER - def handle_update(self, updatesource): - """Handle the new update request.""" - if f"{self.device_type}.{self.node_id}" not in updatesource: - self.schedule_update_ha_state() - @property def name(self): """Return the name of the water heater.""" @@ -76,7 +56,7 @@ def name(self): @property def temperature_unit(self): """Return the unit of measurement.""" - return self._unit_of_measurement + return TEMP_CELSIUS @property def current_operation(self): @@ -88,19 +68,12 @@ def operation_list(self): """List of available operation modes.""" return SUPPORT_WATER_HEATER - async def async_added_to_hass(self): - """When entity is added to Home Assistant.""" - await super().async_added_to_hass() - self.session.entities.append(self) - + @refresh_system def set_operation_mode(self, operation_mode): """Set operation mode.""" new_mode = HASS_TO_HIVE_STATE[operation_mode] self.session.hotwater.set_mode(self.node_id, new_mode) - for entity in self.session.entities: - entity.handle_update(self.data_updatesource) - def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) diff --git a/requirements_all.txt b/requirements_all.txt index 32983063775dcd..a68f7f71e9c88b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1223,7 +1223,7 @@ pyheos==0.6.0 pyhik==0.2.3 # homeassistant.components.hive -pyhiveapi==0.2.18.1 +pyhiveapi==0.2.19 # homeassistant.components.homematic pyhomematic==0.1.60 From b0df14db1401a9b4450acd80c1aa3b5cfd7f7673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 28 Sep 2019 00:20:00 +0300 Subject: [PATCH 0493/3953] Bump Travis timeout to 50 minutes (#26978) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 525a4c8e72c1f5..0e9e030128e2a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,4 +30,4 @@ matrix: cache: pip install: pip install -U tox language: python -script: travis_wait 40 tox --develop +script: travis_wait 50 tox --develop From ac634d71f4cc75946012e51f955c6f5292da2aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 28 Sep 2019 03:02:48 +0300 Subject: [PATCH 0494/3953] Remove no longer needed Python < 3.6 compatibility code (#27024) --- homeassistant/util/async_.py | 130 +---------------------------------- 1 file changed, 3 insertions(+), 127 deletions(-) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index d43658d1584269..6920e0d97f64cd 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -1,23 +1,13 @@ -"""Asyncio backports for Python 3.4.3 compatibility.""" +"""Asyncio backports for Python 3.6 compatibility.""" import concurrent.futures import threading import logging from asyncio import coroutines from asyncio.events import AbstractEventLoop -from asyncio.futures import Future import asyncio from asyncio import ensure_future -from typing import ( - Any, - Union, - Coroutine, - Callable, - Generator, - TypeVar, - Awaitable, - Optional, -) +from typing import Any, Union, Coroutine, Callable, Generator, TypeVar, Awaitable _LOGGER = logging.getLogger(__name__) @@ -40,105 +30,6 @@ def asyncio_run(main: Awaitable[_T], *, debug: bool = False) -> _T: loop.close() -def _set_result_unless_cancelled(fut: Future, result: Any) -> None: - """Set the result only if the Future was not cancelled.""" - if fut.cancelled(): - return - fut.set_result(result) - - -def _set_concurrent_future_state( - concurr: concurrent.futures.Future, source: Union[concurrent.futures.Future, Future] -) -> None: - """Copy state from a future to a concurrent.futures.Future.""" - assert source.done() - if source.cancelled(): - concurr.cancel() - if not concurr.set_running_or_notify_cancel(): - return - exception = source.exception() - if exception is not None: - concurr.set_exception(exception) - else: - result = source.result() - concurr.set_result(result) - - -def _copy_future_state( - source: Union[concurrent.futures.Future, Future], - dest: Union[concurrent.futures.Future, Future], -) -> None: - """Copy state from another Future. - - The other Future may be a concurrent.futures.Future. - """ - assert source.done() - if dest.cancelled(): - return - assert not dest.done() - if source.cancelled(): - dest.cancel() - else: - exception = source.exception() - if exception is not None: - dest.set_exception(exception) - else: - result = source.result() - dest.set_result(result) - - -def _chain_future( - source: Union[concurrent.futures.Future, Future], - destination: Union[concurrent.futures.Future, Future], -) -> None: - """Chain two futures so that when one completes, so does the other. - - The result (or exception) of source will be copied to destination. - If destination is cancelled, source gets cancelled too. - Compatible with both asyncio.Future and concurrent.futures.Future. - """ - if not isinstance(source, (Future, concurrent.futures.Future)): - raise TypeError("A future is required for source argument") - if not isinstance(destination, (Future, concurrent.futures.Future)): - raise TypeError("A future is required for destination argument") - # pylint: disable=protected-access - if isinstance(source, Future): - source_loop: Optional[AbstractEventLoop] = source._loop - else: - source_loop = None - if isinstance(destination, Future): - dest_loop: Optional[AbstractEventLoop] = destination._loop - else: - dest_loop = None - - def _set_state( - future: Union[concurrent.futures.Future, Future], - other: Union[concurrent.futures.Future, Future], - ) -> None: - if isinstance(future, Future): - _copy_future_state(other, future) - else: - _set_concurrent_future_state(future, other) - - def _call_check_cancel( - destination: Union[concurrent.futures.Future, Future] - ) -> None: - if destination.cancelled(): - if source_loop is None or source_loop is dest_loop: - source.cancel() - else: - source_loop.call_soon_threadsafe(source.cancel) - - def _call_set_state(source: Union[concurrent.futures.Future, Future]) -> None: - if dest_loop is None or dest_loop is source_loop: - _set_state(destination, source) - else: - dest_loop.call_soon_threadsafe(_set_state, destination, source) - - destination.add_done_callback(_call_check_cancel) - source.add_done_callback(_call_set_state) - - def run_coroutine_threadsafe( coro: Union[Coroutine, Generator], loop: AbstractEventLoop ) -> concurrent.futures.Future: @@ -150,22 +41,7 @@ def run_coroutine_threadsafe( if ident is not None and ident == threading.get_ident(): raise RuntimeError("Cannot be called from within the event loop") - if not coroutines.iscoroutine(coro): - raise TypeError("A coroutine object is required") - future: concurrent.futures.Future = concurrent.futures.Future() - - def callback() -> None: - """Handle the call to the coroutine.""" - try: - _chain_future(ensure_future(coro, loop=loop), future) - except Exception as exc: # pylint: disable=broad-except - if future.set_running_or_notify_cancel(): - future.set_exception(exc) - else: - _LOGGER.warning("Exception on lost future: ", exc_info=True) - - loop.call_soon_threadsafe(callback) - return future + return asyncio.run_coroutine_threadsafe(coro, loop) def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None: From ce97c27a7fa0512d4d5251ae4e543384b20be384 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 27 Sep 2019 18:03:15 -0600 Subject: [PATCH 0495/3953] Fix possible OpenUV exception due to missing data (#26958) --- homeassistant/components/openuv/binary_sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 59f6e4d1c67e47..621950965f6c2e 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -102,6 +102,11 @@ async def async_update(self): if not data: return + for key in ("from_time", "to_time", "from_uv", "to_uv"): + if not data.get(key): + _LOGGER.info("Skipping update due to missing data: %s", key) + return + if self._sensor_type == TYPE_PROTECTION_WINDOW: self._state = ( parse_datetime(data["from_time"]) From 2af34b461a523e008b11ac8105fd3785dd976ce8 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 28 Sep 2019 00:32:10 +0000 Subject: [PATCH 0496/3953] [ci skip] Translation update --- .../binary_sensor/.translations/ko.json | 48 +++++++ .../binary_sensor/.translations/pl.json | 4 + .../components/ecobee/.translations/lb.json | 13 ++ .../components/ecobee/.translations/pl.json | 25 ++++ .../components/plex/.translations/lb.json | 11 ++ .../components/plex/.translations/no.json | 3 +- .../components/plex/.translations/pl.json | 11 ++ .../transmission/.translations/lb.json | 40 ++++++ .../transmission/.translations/pl.json | 40 ++++++ .../components/zha/.translations/da.json | 4 + .../components/zha/.translations/en.json | 124 +++++++++--------- 11 files changed, 260 insertions(+), 63 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/ko.json create mode 100644 homeassistant/components/ecobee/.translations/lb.json create mode 100644 homeassistant/components/ecobee/.translations/pl.json create mode 100644 homeassistant/components/transmission/.translations/lb.json create mode 100644 homeassistant/components/transmission/.translations/pl.json diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json new file mode 100644 index 00000000000000..02443d449c543f --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -0,0 +1,48 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", + "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc2b5\ub2c8\ub2e4", + "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc2b5\ub2c8\ub2e4", + "is_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4", + "is_moist": "{entity_name} \uc774(\uac00) \uc2b5\ud569\ub2c8\ub2e4", + "is_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc600\uc2b5\ub2c8\ub2e4", + "is_no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", + "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4", + "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "is_not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud569\ub2c8\ub2e4", + "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_not_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", + "is_not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud614\uc2b5\ub2c8\ub2e4", + "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc2b5\ub2c8\ub2e4", + "is_not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud569\ub2c8\ub2e4", + "is_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4", + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", + "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", + "is_plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud614\uc2b5\ub2c8\ub2e4", + "is_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "is_present": "{entity_name} \uc774(\uac00) \uc788\uc2b5\ub2c8\ub2e4", + "is_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", + "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index 139cff2187fcfb..7862644c727826 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -14,6 +14,10 @@ "is_no_gas": "{entity_name} nie wykrywa gazu", "is_no_light": "{entity_name} nie wykrywa \u015bwiat\u0142a", "is_no_motion": "{entity_name} nie wykrywa ruchu", + "is_no_problem": "{entity_name} nie wykrywa problemu", + "is_no_smoke": "{entity_name} nie wykrywa dymu", + "is_no_sound": "{entity_name} nie wykrywa d\u017awi\u0119ku", + "is_no_vibration": "{entity_name} nie wykrywa wibracji", "is_off": "{entity_name} jest wy\u0142\u0105czone", "is_on": "{entity_name} jest w\u0142\u0105czone" } diff --git a/homeassistant/components/ecobee/.translations/lb.json b/homeassistant/components/ecobee/.translations/lb.json new file mode 100644 index 00000000000000..1982dd40840283 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/lb.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel" + }, + "title": "ecobee API Schl\u00ebssel" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/pl.json b/homeassistant/components/ecobee/.translations/pl.json new file mode 100644 index 00000000000000..5c51d86fee4980 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 ecobee" + }, + "error": { + "pin_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania kodu PIN od ecobee; sprawd\u017a, czy klucz API jest poprawny.", + "token_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania token\u00f3w od ecobee; prosz\u0119 spr\u00f3buj ponownie." + }, + "step": { + "authorize": { + "description": "Autoryzuj t\u0119 aplikacj\u0119 na https://www.ecobee.com/consumerportal/index.html za pomoc\u0105 kodu PIN: \n\n {pin} \n \n Nast\u0119pnie naci\u015bnij przycisk Prze\u015blij.", + "title": "Autoryzuj aplikacj\u0119 na ecobee.com" + }, + "user": { + "data": { + "api_key": "Klucz API" + }, + "description": "Prosz\u0119 wprowadzi\u0107 klucz API uzyskany na ecobee.com.", + "title": "Klucz API" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 244044b2f67a80..1e6488784d409a 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Weis all Kontrollen", + "use_episode_art": "Benotz Biller vun der Episode" + }, + "description": "Optioune fir Plex Medie Spiller" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index 393639dd4c9fc9..f7a6bfd9c7f1b8 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -46,7 +46,8 @@ "step": { "plex_mp_settings": { "data": { - "show_all_controls": "Vis alle kontroller" + "show_all_controls": "Vis alle kontroller", + "use_episode_art": "Bruk episode bilde" }, "description": "Alternativer for Plex Media Players" } diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index 606f97d6965c60..ea1db4ec2f3243 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -29,5 +29,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Poka\u017c wszystkie elementy steruj\u0105ce", + "use_episode_art": "U\u017cyj grafiki episodu" + }, + "description": "Opcje dla odtwarzaczy multimedialnych Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/lb.json b/homeassistant/components/transmission/.translations/lb.json new file mode 100644 index 00000000000000..6cc611fcb71550 --- /dev/null +++ b/homeassistant/components/transmission/.translations/lb.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + }, + "error": { + "cannot_connect": "Kann sech net mam Server verbannen.", + "wrong_credentials": "Falsche Benotzernumm oder Passwuert" + }, + "step": { + "options": { + "data": { + "scan_interval": "Intervalle vun de Mise \u00e0 jour" + }, + "title": "Optioune konfigur\u00e9ieren" + }, + "user": { + "data": { + "host": "Server", + "name": "Numm", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" + }, + "title": "Transmission Client ariichten" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle vun de Mise \u00e0 jour" + }, + "description": "Optioune fir Transmission konfigur\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pl.json b/homeassistant/components/transmission/.translations/pl.json new file mode 100644 index 00000000000000..9b45e3107672af --- /dev/null +++ b/homeassistant/components/transmission/.translations/pl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Wymagana jest tylko jedna instancja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem", + "wrong_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o" + }, + "step": { + "options": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + }, + "title": "Opcje" + }, + "user": { + "data": { + "host": "Host", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + }, + "title": "Konfiguracja klienta Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + }, + "description": "Konfiguracja opcji dla Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 8d99b6eebc9db8..0b800ecd80a7ef 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -18,6 +18,10 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Advare" + }, "trigger_subtype": { "both_buttons": "Begge knapper", "button_1": "F\u00f8rste knap", diff --git a/homeassistant/components/zha/.translations/en.json b/homeassistant/components/zha/.translations/en.json index ea1ad48bbff940..d8e8955a935072 100644 --- a/homeassistant/components/zha/.translations/en.json +++ b/homeassistant/components/zha/.translations/en.json @@ -1,67 +1,67 @@ { - "config": { - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." - }, - "step": { - "user": { - "data": { - "radio_type": "Radio Type", - "usb_path": "USB Device Path" + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "step": { + "user": { + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" + }, + "title": "ZHA" + } }, "title": "ZHA" - } }, - "title": "ZHA" - }, - "device_automation": { - "action_type": { - "squawk": "Squawk", - "warn": "Warn" - }, - "trigger_subtype": { - "both_buttons": "Both buttons", - "button_1": "First button", - "button_2": "Second button", - "button_3": "Third button", - "button_4": "Fourth button", - "button_5": "Fifth button", - "button_6": "Sixth button", - "close": "Close", - "dim_down": "Dim down", - "dim_up": "Dim up", - "face_1": "with face 1 activated", - "face_2": "with face 2 activated", - "face_3": "with face 3 activated", - "face_4": "with face 4 activated", - "face_5": "with face 5 activated", - "face_6": "with face 6 activated", - "face_any": "With any/specified face(s) activated", - "left": "Left", - "open": "Open", - "right": "Right", - "turn_off": "Turn off", - "turn_on": "Turn on" - }, - "trigger_type": { - "device_dropped": "Device dropped", - "device_flipped": "Device flipped \"{subtype}\"", - "device_knocked": "Device knocked \"{subtype}\"", - "device_rotated": "Device rotated \"{subtype}\"", - "device_shaken": "Device shaken", - "device_slid": "Device slid \"{subtype}\"", - "device_tilted": "Device tilted", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_triple_press": "\"{subtype}\" button triple clicked" + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Warn" + }, + "trigger_subtype": { + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "button_5": "Fifth button", + "button_6": "Sixth button", + "close": "Close", + "dim_down": "Dim down", + "dim_up": "Dim up", + "face_1": "with face 1 activated", + "face_2": "with face 2 activated", + "face_3": "with face 3 activated", + "face_4": "with face 4 activated", + "face_5": "with face 5 activated", + "face_6": "with face 6 activated", + "face_any": "With any/specified face(s) activated", + "left": "Left", + "open": "Open", + "right": "Right", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "device_dropped": "Device dropped", + "device_flipped": "Device flipped \"{subtype}\"", + "device_knocked": "Device knocked \"{subtype}\"", + "device_rotated": "Device rotated \"{subtype}\"", + "device_shaken": "Device shaken", + "device_slid": "Device slid \"{subtype}\"", + "device_tilted": "Device tilted", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_triple_press": "\"{subtype}\" button triple clicked" + } } - } -} +} \ No newline at end of file From 1c72a246a048d0f2f3d3dca0b77be559dae01a78 Mon Sep 17 00:00:00 2001 From: SneakSnackSnake <55602459+SneakSnackSnake@users.noreply.github.com> Date: Sat, 28 Sep 2019 08:15:29 +0200 Subject: [PATCH 0497/3953] Update pythonegardia to 1.0.40 (#27009) --- homeassistant/components/egardia/manifest.json | 8 ++------ requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/egardia/manifest.json b/homeassistant/components/egardia/manifest.json index 3a95b90db99001..6f103449868ab1 100644 --- a/homeassistant/components/egardia/manifest.json +++ b/homeassistant/components/egardia/manifest.json @@ -2,11 +2,7 @@ "domain": "egardia", "name": "Egardia", "documentation": "https://www.home-assistant.io/components/egardia", - "requirements": [ - "pythonegardia==1.0.39" - ], + "requirements": ["pythonegardia==1.0.40"], "dependencies": [], - "codeowners": [ - "@jeroenterheerdt" - ] + "codeowners": ["@jeroenterheerdt"] } diff --git a/requirements_all.txt b/requirements_all.txt index a68f7f71e9c88b..bd0a11f3739751 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1585,7 +1585,7 @@ python_awair==0.0.4 python_opendata_transport==0.1.4 # homeassistant.components.egardia -pythonegardia==1.0.39 +pythonegardia==1.0.40 # homeassistant.components.tile pytile==2.0.6 From 2dfdc5f6f884d3bbb5729bcdaf811a30ae46b8f9 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Sat, 28 Sep 2019 05:32:22 -0400 Subject: [PATCH 0498/3953] Improve ecobee service schemas (#26955) * Validate date and time in create vaction Improve validation with utility functions. * Improve validate ATTR_VACATION_NAME * Add tests for ecobee.util functions * Revise tests as standalone functions --- homeassistant/components/ecobee/climate.py | 17 +++++++---- homeassistant/components/ecobee/util.py | 21 +++++++++++++ tests/components/ecobee/test_util.py | 35 ++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/ecobee/util.py create mode 100644 tests/components/ecobee/test_util.py diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 460bd2bb4a4ecf..6eccdccf8c6ac0 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -37,6 +37,7 @@ import homeassistant.helpers.config_validation as cv from .const import DOMAIN, _LOGGER +from .util import ecobee_date, ecobee_time ATTR_COOL_TEMP = "cool_temp" ATTR_END_DATE = "end_date" @@ -106,13 +107,17 @@ CREATE_VACATION_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_VACATION_NAME): cv.string, + vol.Required(ATTR_VACATION_NAME): vol.All(cv.string, vol.Length(max=12)), vol.Required(ATTR_COOL_TEMP): vol.Coerce(float), vol.Required(ATTR_HEAT_TEMP): vol.Coerce(float), - vol.Inclusive(ATTR_START_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, - vol.Inclusive(ATTR_START_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, - vol.Inclusive(ATTR_END_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, - vol.Inclusive(ATTR_END_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): cv.string, + vol.Inclusive( + ATTR_START_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG + ): ecobee_date, + vol.Inclusive( + ATTR_START_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG + ): ecobee_time, + vol.Inclusive(ATTR_END_DATE, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): ecobee_date, + vol.Inclusive(ATTR_END_TIME, "dtgroup", msg=DTGROUP_INCLUSIVE_MSG): ecobee_time, vol.Optional(ATTR_FAN_MODE, default="auto"): vol.Any("auto", "on"), vol.Optional(ATTR_FAN_MIN_ON_TIME, default=0): vol.All( int, vol.Range(min=0, max=60) @@ -123,7 +128,7 @@ DELETE_VACATION_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_VACATION_NAME): cv.string, + vol.Required(ATTR_VACATION_NAME): vol.All(cv.string, vol.Length(max=12)), } ) diff --git a/homeassistant/components/ecobee/util.py b/homeassistant/components/ecobee/util.py new file mode 100644 index 00000000000000..3acc3e5676d8d6 --- /dev/null +++ b/homeassistant/components/ecobee/util.py @@ -0,0 +1,21 @@ +"""Validation utility functions for ecobee services.""" +from datetime import datetime +import voluptuous as vol + + +def ecobee_date(date_string): + """Validate a date_string as valid for the ecobee API.""" + try: + datetime.strptime(date_string, "%Y-%m-%d") + except ValueError: + raise vol.Invalid("Date does not match ecobee date format YYYY-MM-DD") + return date_string + + +def ecobee_time(time_string): + """Validate a time_string as valid for the ecobee API.""" + try: + datetime.strptime(time_string, "%H:%M:%S") + except ValueError: + raise vol.Invalid("Time does not match ecobee 24-hour time format HH:MM:SS") + return time_string diff --git a/tests/components/ecobee/test_util.py b/tests/components/ecobee/test_util.py new file mode 100644 index 00000000000000..ee02f2a33aa7f7 --- /dev/null +++ b/tests/components/ecobee/test_util.py @@ -0,0 +1,35 @@ +"""Tests for the ecobee.util module.""" +import pytest +import voluptuous as vol + +from homeassistant.components.ecobee.util import ecobee_date, ecobee_time + + +def test_ecobee_date_with_valid_input(): + """Test that the date function returns the expected result.""" + test_input = "2019-09-27" + + assert ecobee_date(test_input) == test_input + + +def test_ecobee_date_with_invalid_input(): + """Test that the date function raises the expected exception.""" + test_input = "20190927" + + with pytest.raises(vol.Invalid): + ecobee_date(test_input) + + +def test_ecobee_time_with_valid_input(): + """Test that the time function returns the expected result.""" + test_input = "20:55:15" + + assert ecobee_time(test_input) == test_input + + +def test_ecobee_time_with_invalid_input(): + """Test that the time function raises the expected exception.""" + test_input = "20:55" + + with pytest.raises(vol.Invalid): + ecobee_time(test_input) From f9ac204cc546250d997640965d7797850ead7f8f Mon Sep 17 00:00:00 2001 From: Florian Klien Date: Sat, 28 Sep 2019 11:33:48 +0200 Subject: [PATCH 0499/3953] Add more providers, bump yessssms version to 0.4.1 (#26874) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bump yessssms version to 0.4.0 adds 'provider' config parameter adds support for providers: * billitel * EDUCOM * fenercell * georg * goood * kronemobile * kuriermobil * SIMfonie * teleplanet * WOWWW * yooopi * black formatting * moved CONF_PROVIDER to component * black formatting * moved error handling on init to get_service * return None, init logging moved to get_service * moved YesssSMS import to top of module * test login data on init. add flag for login data test. removed KeyError * catch connection error, remove CONF_TEST_LOGIN_DATA config flag * requirements updated * lint * lint: use getters for protected members, bump version to 0.4.1b4 * requirements updated to 0.4.1b4 * fix logging messages, info to warning, clear up login_data check * change valid login data message to debug * fix tests * add tests for get_service * bump yessssms version 0.4.1 * tests for get_service refurbished * test refactoring with fixtures * polish fixtures ✨ * replace Mock with patch 🔄 * tiny string fixes, removed unused return_value 🐈 --- homeassistant/components/yessssms/const.py | 3 + .../components/yessssms/manifest.json | 2 +- homeassistant/components/yessssms/notify.py | 52 ++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/yessssms/test_notify.py | 148 +++++++++++++++++- 6 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/yessssms/const.py diff --git a/homeassistant/components/yessssms/const.py b/homeassistant/components/yessssms/const.py new file mode 100644 index 00000000000000..473cdfff1e0257 --- /dev/null +++ b/homeassistant/components/yessssms/const.py @@ -0,0 +1,3 @@ +"""Const for YesssSMS.""" + +CONF_PROVIDER = "provider" diff --git a/homeassistant/components/yessssms/manifest.json b/homeassistant/components/yessssms/manifest.json index 103a9fce31ede2..c7b5535d03c6f6 100644 --- a/homeassistant/components/yessssms/manifest.json +++ b/homeassistant/components/yessssms/manifest.json @@ -3,7 +3,7 @@ "name": "Yessssms", "documentation": "https://www.home-assistant.io/components/yessssms", "requirements": [ - "YesssSMS==0.2.3" + "YesssSMS==0.4.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/yessssms/notify.py b/homeassistant/components/yessssms/notify.py index 28c02080f16621..1c1eed0e89d18c 100644 --- a/homeassistant/components/yessssms/notify.py +++ b/homeassistant/components/yessssms/notify.py @@ -3,11 +3,16 @@ import voluptuous as vol +from YesssSMS import YesssSMS + from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService + +from .const import CONF_PROVIDER + _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -15,27 +20,52 @@ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_RECIPIENT): cv.string, + vol.Optional(CONF_PROVIDER, default="YESSS"): cv.string, } ) def get_service(hass, config, discovery_info=None): """Get the YesssSMS notification service.""" - return YesssSMSNotificationService( - config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_RECIPIENT] + + try: + yesss = YesssSMS( + config[CONF_USERNAME], config[CONF_PASSWORD], provider=config[CONF_PROVIDER] + ) + except YesssSMS.UnsupportedProviderError as ex: + _LOGGER.error("Unknown provider: %s", ex) + return None + try: + if not yesss.login_data_valid(): + _LOGGER.error( + "Login data is not valid! Please double check your login data at %s", + yesss.get_login_url(), + ) + return None + + _LOGGER.debug("Login data for '%s' valid", yesss.get_provider()) + except YesssSMS.ConnectionError: + _LOGGER.warning( + "Connection Error, could not verify login data for '%s'", + yesss.get_provider(), + ) + pass + + _LOGGER.debug( + "initialized; library version: %s, with %s", + yesss.version(), + yesss.get_provider(), ) + return YesssSMSNotificationService(yesss, config[CONF_RECIPIENT]) class YesssSMSNotificationService(BaseNotificationService): """Implement a notification service for the YesssSMS service.""" - def __init__(self, username, password, recipient): + def __init__(self, client, recipient): """Initialize the service.""" - from YesssSMS import YesssSMS - - self.yesss = YesssSMS(username, password) + self.yesss = client self._recipient = recipient - _LOGGER.debug("initialized; library version: %s", self.yesss.version()) def send_message(self, message="", **kwargs): """Send a SMS message via Yesss.at's website.""" @@ -56,10 +86,12 @@ def send_message(self, message="", **kwargs): except self.yesss.EmptyMessageError as ex: _LOGGER.error("Cannot send empty SMS message: %s", ex) except self.yesss.SMSSendingError as ex: - _LOGGER.error(str(ex), exc_info=ex) - except ConnectionError as ex: + _LOGGER.error(ex) + except self.yesss.ConnectionError as ex: _LOGGER.error( - "YesssSMS: unable to connect to yesss.at server.", exc_info=ex + "Unable to connect to server of provider (%s): %s", + self.yesss.get_provider(), + ex, ) except self.yesss.AccountSuspendedError as ex: _LOGGER.error( diff --git a/requirements_all.txt b/requirements_all.txt index bd0a11f3739751..53f72801cc363b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -100,7 +100,7 @@ TwitterAPI==2.5.9 WazeRouteCalculator==0.10 # homeassistant.components.yessssms -YesssSMS==0.2.3 +YesssSMS==0.4.1 # homeassistant.components.abode abodepy==0.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a860c67dbd1368..7ac46e96cd4d78 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.1.3 PyTransportNSW==0.1.1 # homeassistant.components.yessssms -YesssSMS==0.2.3 +YesssSMS==0.4.1 # homeassistant.components.adguard adguardhome==0.2.1 diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py index 3d11cdedc67bd7..5cc204ccc4d4ca 100644 --- a/tests/components/yessssms/test_notify.py +++ b/tests/components/yessssms/test_notify.py @@ -1,7 +1,148 @@ """The tests for the notify yessssms platform.""" import unittest +from unittest.mock import patch + +import pytest import requests_mock + +from homeassistant.setup import async_setup_component import homeassistant.components.yessssms.notify as yessssms +from homeassistant.components.yessssms.const import CONF_PROVIDER + +from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME + + +@pytest.fixture(name="config") +def config_data(): + """Set valid config data.""" + config = { + "notify": { + "platform": "yessssms", + "name": "sms", + CONF_USERNAME: "06641234567", + CONF_PASSWORD: "secretPassword", + CONF_RECIPIENT: "06509876543", + CONF_PROVIDER: "educom", + } + } + return config + + +@pytest.fixture(name="valid_settings") +def init_valid_settings(hass, config): + """Initialize component with valid settings.""" + return async_setup_component(hass, "notify", config) + + +@pytest.fixture(name="invalid_provider_settings") +def init_invalid_provider_settings(hass, config): + """Set invalid provider data and initalize component.""" + config["notify"][CONF_PROVIDER] = "FantasyMobile" # invalid provider + return async_setup_component(hass, "notify", config) + + +@pytest.fixture(name="invalid_login_data") +def mock_invalid_login_data(): + """Mock invalid login data.""" + path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid" + with patch(path, return_value=False): + yield + + +@pytest.fixture(name="valid_login_data") +def mock_valid_login_data(): + """Mock valid login data.""" + path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid" + with patch(path, return_value=True): + yield + + +@pytest.fixture(name="connection_error") +def mock_connection_error(): + """Mock a connection error.""" + path = "homeassistant.components.yessssms.notify.YesssSMS.login_data_valid" + with patch(path, side_effect=yessssms.YesssSMS.ConnectionError()): + yield + + +async def test_unsupported_provider_error(hass, caplog, invalid_provider_settings): + """Test for error on unsupported provider.""" + await invalid_provider_settings + for record in caplog.records: + if ( + record.levelname == "ERROR" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "Unknown provider: provider (fantasymobile) is not known to YesssSMS" + in record.message + ) + assert ( + "Unknown provider: provider (fantasymobile) is not known to YesssSMS" + in caplog.text + ) + assert not hass.services.has_service("notify", "sms") + + +async def test_false_login_data_error(hass, caplog, valid_settings, invalid_login_data): + """Test login data check error.""" + await valid_settings + assert not hass.services.has_service("notify", "sms") + for record in caplog.records: + if ( + record.levelname == "ERROR" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "Login data is not valid! Please double check your login data at" + in record.message + ) + + +async def test_init_success(hass, caplog, valid_settings, valid_login_data): + """Test for successful init of yessssms.""" + await valid_settings + assert hass.services.has_service("notify", "sms") + messages = [] + for record in caplog.records: + if ( + record.levelname == "DEBUG" + and record.name == "homeassistant.components.yessssms.notify" + ): + messages.append(record.message) + assert "Login data for 'educom' valid" in messages[0] + assert ( + "initialized; library version: {}".format(yessssms.YesssSMS("", "").version()) + in messages[1] + ) + + +async def test_connection_error_on_init(hass, caplog, valid_settings, connection_error): + """Test for connection error on init.""" + await valid_settings + assert hass.services.has_service("notify", "sms") + for record in caplog.records: + if ( + record.levelname == "WARNING" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "Connection Error, could not verify login data for '{}'".format( + "educom" + ) + in record.message + ) + for record in caplog.records: + if ( + record.levelname == "DEBUG" + and record.name == "homeassistant.components.yessssms.notify" + ): + assert ( + "initialized; library version: {}".format( + yessssms.YesssSMS("", "").version() + ) + in record.message + ) class TestNotifyYesssSMS(unittest.TestCase): @@ -12,7 +153,8 @@ def setUp(self): # pylint: disable=invalid-name login = "06641234567" passwd = "testpasswd" recipient = "06501234567" - self.yessssms = yessssms.YesssSMSNotificationService(login, passwd, recipient) + client = yessssms.YesssSMS(login, passwd) + self.yessssms = yessssms.YesssSMSNotificationService(client, recipient) @requests_mock.Mocker() def test_login_error(self, mock): @@ -197,7 +339,7 @@ def test_connection_error(self, mock): "POST", # pylint: disable=protected-access self.yessssms.yesss._login_url, - exc=ConnectionError, + exc=yessssms.YesssSMS.ConnectionError, ) message = "Testing YesssSMS platform :)" @@ -209,4 +351,4 @@ def test_connection_error(self, mock): self.assertTrue(mock.called) self.assertEqual(mock.call_count, 1) - self.assertIn("unable to connect", context.output[0]) + self.assertIn("cannot connect to provider", context.output[0]) From f3d408aca4e707f62ccd4902001b550cbe1a8148 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Sat, 28 Sep 2019 13:13:12 +0200 Subject: [PATCH 0500/3953] Upgrade youtube_dl to 2019.09.28 (#27031) --- 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 4e253741b051f8..71e1a81135a662 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.09.12.1" + "youtube_dl==2019.09.28" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 53f72801cc363b..ab99704f9653cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2005,7 +2005,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.09.12.1 +youtube_dl==2019.09.28 # homeassistant.components.zengge zengge==0.2 From 6d773198a12f399cb9d89795d27fa4c5023c8734 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Sat, 28 Sep 2019 21:53:16 +1000 Subject: [PATCH 0501/3953] Add availability_template to Template Cover platform (#26509) * Added availability_template to Template Cover platform * Added to test for invalid values in availability_template * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Moved availability_template rendering to common loop * Removed 'Magic' string and removed duplicate code * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (async, magic values and state checks) --- homeassistant/components/template/cover.py | 39 +++++--- tests/components/template/test_cover.py | 109 +++++++++++++++++++++ 2 files changed, 137 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 51b9a523b3bbe2..483ee1ae8723fb 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -38,6 +38,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_OPEN, STATE_CLOSED, "true", "false"] @@ -74,6 +75,7 @@ vol.Exclusive( CONF_VALUE_TEMPLATE, CONF_VALUE_OR_POSITION_TEMPLATE ): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_POSITION_TEMPLATE): cv.template, vol.Optional(CONF_TILT_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, @@ -103,6 +105,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= position_template = device_config.get(CONF_POSITION_TEMPLATE) tilt_template = device_config.get(CONF_TILT_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) device_class = device_config.get(CONF_DEVICE_CLASS) open_action = device_config.get(OPEN_ACTION) @@ -144,6 +147,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if str(temp_ids) != MATCH_ALL: template_entity_ids |= set(temp_ids) + if availability_template is not None: + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + if not template_entity_ids: template_entity_ids = MATCH_ALL @@ -160,6 +168,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= tilt_template, icon_template, entity_picture_template, + availability_template, open_action, close_action, stop_action, @@ -192,6 +201,7 @@ def __init__( tilt_template, icon_template, entity_picture_template, + availability_template, open_action, close_action, stop_action, @@ -213,6 +223,7 @@ def __init__( self._icon_template = icon_template self._device_class = device_class self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._open_script = None if open_action is not None: self._open_script = Script(hass, open_action) @@ -235,6 +246,7 @@ def __init__( self._position = None self._tilt_value = None self._entities = entity_ids + self._available = True if self._template is not None: self._template.hass = self.hass @@ -246,6 +258,8 @@ def __init__( self._icon_template.hass = self.hass if self._entity_picture_template is not None: self._entity_picture_template.hass = self.hass + if self._availability_template is not None: + self._availability_template.hass = self.hass async def async_added_to_hass(self): """Register callbacks.""" @@ -332,6 +346,11 @@ def should_poll(self): """Return the polling state.""" return False + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_open_cover(self, **kwargs): """Move the cover up.""" if self._open_script: @@ -430,11 +449,8 @@ async def async_update(self): ) else: self._position = state - except TemplateError as ex: - _LOGGER.error(ex) - self._position = None - except ValueError as ex: - _LOGGER.error(ex) + except (TemplateError, ValueError) as err: + _LOGGER.error(err) self._position = None if self._tilt_template is not None: try: @@ -447,22 +463,23 @@ async def async_update(self): ) else: self._tilt_value = state - except TemplateError as ex: - _LOGGER.error(ex) - self._tilt_value = None - except ValueError as ex: - _LOGGER.error(ex) + except (TemplateError, ValueError) as err: + _LOGGER.error(err) self._tilt_value = None for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), + ("_available", self._availability_template), ): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 247ee25027c94f..d3be01cbdc3ae8 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -16,7 +16,10 @@ SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, STATE_CLOSED, + STATE_UNAVAILABLE, STATE_OPEN, + STATE_ON, + STATE_OFF, ) from tests.common import assert_setup_component, async_mock_service @@ -839,6 +842,112 @@ async def test_entity_picture_template(hass, calls): assert state.attributes["entity_picture"] == "/local/cover.png" +async def test_availability_template(hass, calls): + """Test availability template.""" + with assert_setup_component(1, "cover"): + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + "availability_template": "{{ is_state('availability_state.state','on') }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover").state == STATE_UNAVAILABLE + + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover").state != STATE_UNAVAILABLE + + +async def test_availability_without_availability_template(hass, calls): + """Test that component is availble if there is no.""" + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("cover.test_template_cover") + assert state.state != STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + assert await setup.async_setup_component( + hass, + "cover", + { + "cover": { + "platform": "template", + "covers": { + "test_template_cover": { + "availability_template": "{{ x - 12 }}", + "value_template": "open", + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("cover.test_template_cover") != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + async def test_device_class(hass, calls): """Test device class.""" with assert_setup_component(1, "cover"): From 5c5f6a21af3193c7f41827d28b94bd8734130261 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Sat, 28 Sep 2019 21:55:29 +1000 Subject: [PATCH 0502/3953] Add availability_template to Template Binary Sensor platform (#26510) * Added availability_template to Template Binary Sensor platform * Added to test for invalid values in availability_template * black * simplified exception handler * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Moved availability_template rendering to common loop * Removed 'Magic' string * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (magic values and state checks) --- .../components/template/binary_sensor.py | 29 ++++-- .../components/template/test_binary_sensor.py | 89 ++++++++++++++++++- 2 files changed, 111 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index e0fc867720010c..d5ade703c97440 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -26,6 +26,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change, async_track_same_state +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -38,6 +39,7 @@ 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_AVAILABILITY_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, @@ -60,6 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= value_template = device_config[CONF_VALUE_TEMPLATE] icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) @@ -70,6 +73,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_VALUE_TEMPLATE: value_template, CONF_ICON_TEMPLATE: icon_template, CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, } for tpl_name, template in chain(templates.items(), attribute_templates.items()): @@ -117,6 +121,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= value_template, icon_template, entity_picture_template, + availability_template, entity_ids, delay_on, delay_off, @@ -143,6 +148,7 @@ def __init__( value_template, icon_template, entity_picture_template, + availability_template, entity_ids, delay_on, delay_off, @@ -156,12 +162,14 @@ def __init__( self._template = value_template self._state = None self._icon_template = icon_template + self._availability_template = availability_template self._entity_picture_template = entity_picture_template self._icon = None self._entity_picture = None self._entities = entity_ids self._delay_on = delay_on self._delay_off = delay_off + self._available = True self._attribute_templates = attribute_templates self._attributes = {} @@ -223,6 +231,11 @@ def should_poll(self): """No polling needed.""" return False + @property + def available(self): + """Availability indicator.""" + return self._available + @callback def _async_render(self): """Get the state of template.""" @@ -240,11 +253,6 @@ def _async_render(self): return _LOGGER.error("Could not render template %s: %s", self._name, ex) - 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(): @@ -254,12 +262,21 @@ def _async_render(self): _LOGGER.error("Error rendering attribute %s: %s", key, err) self._attributes = attrs + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + "_available": self._availability_template, + } + for property_name, template in templates.items(): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index c8cec168d6e6cd..143811da2099e5 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -3,7 +3,13 @@ import unittest from unittest import mock -from homeassistant.const import MATCH_ALL, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + MATCH_ALL, + EVENT_HOMEASSISTANT_START, + STATE_UNAVAILABLE, + STATE_ON, + STATE_OFF, +) from homeassistant import setup from homeassistant.components.template import binary_sensor as template from homeassistant.exceptions import TemplateError @@ -238,6 +244,7 @@ def test_attributes(self): template_hlpr.Template("{{ 1 > 1 }}", self.hass), None, None, + None, MATCH_ALL, None, None, @@ -298,6 +305,7 @@ def test_update_template_error(self, mock_render): template_hlpr.Template("{{ 1 > 1 }}", self.hass), None, None, + None, MATCH_ALL, None, None, @@ -428,6 +436,59 @@ async def test_template_delay_off(hass): assert state.state == "on" +async def test_available_without_availability_template(hass): + """Ensure availability is true without an availability_template.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "true", + "device_class": "motion", + "delay_off": 5, + } + }, + } + } + await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + + +async def test_availability_template(hass): + """Test availability template.""" + config = { + "binary_sensor": { + "platform": "template", + "sensors": { + "test": { + "friendly_name": "virtual thingy", + "value_template": "true", + "device_class": "motion", + "delay_off": 5, + "availability_template": "{{ is_state('sensor.test_state','on') }}", + } + }, + } + } + await setup.async_setup_component(hass, "binary_sensor", config) + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_state", STATE_OFF) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state == STATE_UNAVAILABLE + + hass.states.async_set("sensor.test_state", STATE_ON) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test").state != STATE_UNAVAILABLE + + 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") @@ -458,6 +519,32 @@ async def test_invalid_attribute_template(hass, caplog): assert ("Error rendering attribute test_attribute") in caplog.text +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + + await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "my_sensor": { + "value_template": "{{ states.binary_sensor.test_sensor }}", + "availability_template": "{{ x - 12 }}", + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.my_sensor").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") 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") From 74196eaf8b0538f55a7477a00dcec1ba79c4c71c Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Sat, 28 Sep 2019 21:59:40 +1000 Subject: [PATCH 0503/3953] Add availability_template to Template Fan platform (#26511) * Added availability_template to Template Fan platform * Added to test for invalid values in availability_template * fixed component ID in test * Made availability_template redering erorr more concise * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Removed 'Magic' string * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (magic values and state checks) --- homeassistant/components/template/fan.py | 29 +++++++++ tests/components/template/test_fan.py | 80 +++++++++++++++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 7fd8c4d9b3cea6..42790e618d96ec 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -33,6 +33,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -58,6 +59,7 @@ vol.Optional(CONF_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, @@ -86,6 +88,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template = device_config.get(CONF_SPEED_TEMPLATE) oscillating_template = device_config.get(CONF_OSCILLATING_TEMPLATE) direction_template = device_config.get(CONF_DIRECTION_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] @@ -103,6 +106,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template, oscillating_template, direction_template, + availability_template, ): if template is None: continue @@ -131,6 +135,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_template, oscillating_template, direction_template, + availability_template, on_action, off_action, set_speed_action, @@ -156,6 +161,7 @@ def __init__( speed_template, oscillating_template, direction_template, + availability_template, on_action, off_action, set_speed_action, @@ -175,6 +181,8 @@ def __init__( self._speed_template = speed_template self._oscillating_template = oscillating_template self._direction_template = direction_template + self._availability_template = availability_template + self._available = True self._supported_features = 0 self._on_script = Script(hass, on_action) @@ -207,6 +215,8 @@ def __init__( if self._direction_template: self._direction_template.hass = self.hass self._supported_features |= SUPPORT_DIRECTION + if self._availability_template: + self._availability_template.hass = self.hass self._entities = entity_ids # List of valid speeds @@ -252,6 +262,11 @@ def should_poll(self): """Return the polling state.""" return False + @property + def available(self): + """Return availability of Device.""" + return self._available + # pylint: disable=arguments-differ async def async_turn_on(self, speed: str = None) -> None: """Turn on the fan.""" @@ -422,3 +437,17 @@ async def async_update(self): ", ".join(_VALID_DIRECTIONS), ) self._direction = None + + # Update Availability if 'availability_template' is defined + if self._availability_template is not None: + try: + self._available = ( + self._availability_template.async_render().lower() == "true" + ) + except (TemplateError, ValueError) as ex: + _LOGGER.error( + "Could not render %s template %s: %s", + CONF_AVAILABILITY_TEMPLATE, + self._name, + ex, + ) diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index b80522b37e24c5..5753684795b127 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.fan import ( ATTR_SPEED, ATTR_OSCILLATING, @@ -26,6 +26,8 @@ _TEST_FAN = "fan.test_fan" # Represent for fan's state _STATE_INPUT_BOOLEAN = "input_boolean.state" +# Represent for fan's state +_STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" # Represent for fan's speed _SPEED_INPUT_SELECT = "input_select.speed" # Represent for fan's oscillating @@ -214,6 +216,49 @@ async def test_templates_with_entities(hass, calls): _verify(hass, STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) +async def test_availability_template_with_entities(hass, calls): + """Test availability tempalates with values from other entities.""" + + with assert_setup_component(1, "fan"): + assert await setup.async_setup_component( + hass, + "fan", + { + "fan": { + "platform": "template", + "fans": { + "test_fan": { + "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", + "value_template": "{{ 'on' }}", + "speed_template": "{{ 'medium' }}", + "oscillating_template": "{{ 1 == 1 }}", + "direction_template": "{{ 'forward' }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get(_TEST_FAN).state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get(_TEST_FAN).state == STATE_UNAVAILABLE + + async def test_templates_with_valid_values(hass, calls): """Test templates with valid values.""" with assert_setup_component(1, "fan"): @@ -272,6 +317,39 @@ async def test_templates_invalid_values(hass, calls): _verify(hass, STATE_OFF, None, None, None) +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + + with assert_setup_component(1, "fan"): + assert await setup.async_setup_component( + hass, + "fan", + { + "fan": { + "platform": "template", + "fans": { + "test_fan": { + "value_template": "{{ 'on' }}", + "availability_template": "{{ x - 12 }}", + "speed_template": "{{ states('input_select.speed') }}", + "oscillating_template": "{{ states('input_select.osc') }}", + "direction_template": "{{ states('input_select.direction') }}", + "turn_on": {"service": "script.fan_on"}, + "turn_off": {"service": "script.fan_off"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("fan.test_fan").state != STATE_UNAVAILABLE + assert ("Could not render availability_template template") in caplog.text + assert ("UndefinedError: 'x' is undefined") in caplog.text + + # End of template tests # From ed82ec5d8e5293746dd288827da21afc942d835b Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Sat, 28 Sep 2019 22:01:18 +1000 Subject: [PATCH 0504/3953] Add availability_template to Template Light platform (#26512) * Added availability_template to Template Light platform * Added to test for invalid values in availability_template * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Moved availability_template rendering to common loop * Removed 'Magic' string * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (async, magic values and state checks) --- homeassistant/components/template/light.py | 31 ++++++- tests/components/template/test_light.py | 94 +++++++++++++++++++++- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 320dcd2e22feb1..552c21f170dbae 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -28,6 +28,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) _VALID_STATES = [STATE_ON, STATE_OFF, "true", "false"] @@ -44,6 +45,7 @@ vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_LEVEL_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME): cv.string, @@ -65,6 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config.get(CONF_VALUE_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] level_action = device_config.get(CONF_LEVEL_ACTION) @@ -92,6 +95,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if str(temp_ids) != MATCH_ALL: template_entity_ids |= set(temp_ids) + if availability_template is not None: + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + if not template_entity_ids: template_entity_ids = MATCH_ALL @@ -105,6 +113,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, level_action, @@ -132,6 +141,7 @@ def __init__( state_template, icon_template, entity_picture_template, + availability_template, on_action, off_action, level_action, @@ -147,6 +157,7 @@ def __init__( self._template = state_template self._icon_template = icon_template self._entity_picture_template = entity_picture_template + self._availability_template = availability_template self._on_script = Script(hass, on_action) self._off_script = Script(hass, off_action) self._level_script = None @@ -159,6 +170,7 @@ def __init__( self._entity_picture = None self._brightness = None self._entities = entity_ids + self._available = True if self._template is not None: self._template.hass = self.hass @@ -168,6 +180,8 @@ def __init__( self._icon_template.hass = self.hass if self._entity_picture_template is not None: self._entity_picture_template.hass = self.hass + if self._availability_template is not None: + self._availability_template.hass = self.hass @property def brightness(self): @@ -207,6 +221,11 @@ def entity_picture(self): """Return the entity picture to use in the frontend, if any.""" return self._entity_picture + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_added_to_hass(self): """Register callbacks.""" @@ -218,7 +237,11 @@ def template_light_state_listener(entity, old_state, new_state): @callback def template_light_startup(event): """Update template on startup.""" - if self._template is not None or self._level_template is not None: + if ( + self._template is not None + or self._level_template is not None + or self._availability_template is not None + ): async_track_state_change( self.hass, self._entities, template_light_state_listener ) @@ -298,12 +321,16 @@ async def async_update(self): for property_name, template in ( ("_icon", self._icon_template), ("_entity_picture", self._entity_picture_template), + ("_available", self._availability_template), ): if template is None: continue try: - setattr(self, property_name, template.async_render()) + value = template.async_render() + if property_name == "_available": + value = value.lower() == "true" + setattr(self, property_name, value) except TemplateError as ex: friendly_property_name = property_name[1:].replace("_", " ") if ex.args and ex.args[0].startswith( diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 87fd8cd4db3f34..c2dd49a76fbd8a 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -4,13 +4,16 @@ from homeassistant.core import callback from homeassistant import setup from homeassistant.components.light import ATTR_BRIGHTNESS -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component from tests.components.light import common _LOGGER = logging.getLogger(__name__) +# Represent for light's availability +_STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" + class TestTemplateLight: """Test the Template light.""" @@ -774,3 +777,92 @@ def test_entity_picture_template(self): state = self.hass.states.get("light.test_template_light") assert state.attributes["entity_picture"] == "/local/light.png" + + +async def test_available_template_with_entities(hass): + """Test availability templates with values from other entities.""" + + await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, + } + }, + } + }, + ) + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("light.test_template_light").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("light.test_template_light").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "availability_template": "{{ x - 12 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("light.test_template_light").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text From 11c9bab07810074ee60b21e055fd3cd05a6ed787 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Sat, 28 Sep 2019 22:02:46 +1000 Subject: [PATCH 0505/3953] Add availability_template to Template Vacuum platform (#26514) * Added availability_template to Template Vacuum platform * Added to test for invalid values in availability_template * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Removed 'Magic' string * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (async, magic values and state checks) --- homeassistant/components/template/vacuum.py | 27 +++++++++ tests/components/template/test_vacuum.py | 64 ++++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 5374247daccd70..6a6523514c489a 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -44,6 +44,8 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE + _LOGGER = logging.getLogger(__name__) CONF_VACUUMS = "vacuums" @@ -67,6 +69,7 @@ vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_BATTERY_LEVEL_TEMPLATE): cv.template, vol.Optional(CONF_FAN_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_PAUSE): cv.SCRIPT_SCHEMA, vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA, @@ -94,6 +97,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template = device_config.get(CONF_VALUE_TEMPLATE) battery_level_template = device_config.get(CONF_BATTERY_LEVEL_TEMPLATE) fan_speed_template = device_config.get(CONF_FAN_SPEED_TEMPLATE) + availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) start_action = device_config[SERVICE_START] pause_action = device_config.get(SERVICE_PAUSE) @@ -113,6 +117,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= (CONF_VALUE_TEMPLATE, state_template), (CONF_BATTERY_LEVEL_TEMPLATE, battery_level_template), (CONF_FAN_SPEED_TEMPLATE, fan_speed_template), + (CONF_AVAILABILITY_TEMPLATE, availability_template), ): if template is None: continue @@ -152,6 +157,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= state_template, battery_level_template, fan_speed_template, + availability_template, start_action, pause_action, stop_action, @@ -178,6 +184,7 @@ def __init__( state_template, battery_level_template, fan_speed_template, + availability_template, start_action, pause_action, stop_action, @@ -198,6 +205,7 @@ def __init__( self._template = state_template self._battery_level_template = battery_level_template self._fan_speed_template = fan_speed_template + self._availability_template = availability_template self._supported_features = SUPPORT_START self._start_script = Script(hass, start_action) @@ -235,6 +243,7 @@ def __init__( self._state = None self._battery_level = None self._fan_speed = None + self._available = True if self._template: self._supported_features |= SUPPORT_STATE @@ -280,6 +289,11 @@ def should_poll(self): """Return the polling state.""" return False + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_start(self): """Start or resume the cleaning task.""" await self._start_script.async_run(context=self._context) @@ -421,3 +435,16 @@ async def async_update(self): self._fan_speed_list, ) self._fan_speed = None + # Update availability if availability template is defined + if self._availability_template is not None: + try: + self._available = ( + self._availability_template.async_render().lower() == "true" + ) + except (TemplateError, ValueError) as ex: + _LOGGER.error( + "Could not render %s template %s: %s", + CONF_AVAILABILITY_TEMPLATE, + self._name, + ex, + ) diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 9e3c535f136d3c..da0e8e59ededb8 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -3,7 +3,7 @@ import pytest from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_UNKNOWN +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN, STATE_UNAVAILABLE from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, STATE_CLEANING, @@ -210,6 +210,68 @@ async def test_invalid_templates(hass, calls): _verify(hass, STATE_UNKNOWN, None) +async def test_available_template_with_entities(hass, calls): + """Test availability templates with values from other entities.""" + + assert await setup.async_setup_component( + hass, + "vacuum", + { + "vacuum": { + "platform": "template", + "vacuums": { + "test_template_vacuum": { + "availability_template": "{{ is_state('availability_state.state', 'on') }}", + "start": {"service": "script.vacuum_start"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("vacuum.test_template_vacuum").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("vacuum.test_template_vacuum").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + assert await setup.async_setup_component( + hass, + "vacuum", + { + "vacuum": { + "platform": "template", + "vacuums": { + "test_template_vacuum": { + "availability_template": "{{ x - 12 }}", + "start": {"service": "script.vacuum_start"}, + } + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("vacuum.test_template_vacuum") != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text + + # End of template tests # From 61a7d8e3d26c19288f9b55af2bcf63fe907c2906 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 28 Sep 2019 22:34:14 +0200 Subject: [PATCH 0506/3953] Add create, remove of devices for HomematicIP_Cloud (#27030) --- .../components/homematicip_cloud/device.py | 47 +++++++++++++++++++ .../components/homematicip_cloud/hap.py | 14 ++++++ .../homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 05853d4b260bca..1273278189d89f 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -6,6 +6,8 @@ from homematicip.aio.home import AsyncHome from homeassistant.components import homematicip_cloud +from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -51,6 +53,8 @@ def __init__(self, home: AsyncHome, device, post: Optional[str] = None) -> None: self._home = home self._device = device self.post = post + # Marker showing that the HmIP device hase been removed. + self.hmip_device_removed = False _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @property @@ -74,7 +78,9 @@ def device_info(self): async def async_added_to_hass(self): """Register callbacks.""" self._device.on_update(self._async_device_changed) + self._device.on_remove(self._async_device_removed) + @callback def _async_device_changed(self, *args, **kwargs): """Handle device state changes.""" # Don't update disabled entities @@ -88,6 +94,47 @@ def _async_device_changed(self, *args, **kwargs): self._device.modelType, ) + async def async_will_remove_from_hass(self) -> None: + """Run when entity will be removed from hass.""" + + # Only go further if the device/entity should be removed from registries + # due to a removal of the HmIP device. + if self.hmip_device_removed: + await self.async_remove_from_registries() + + async def async_remove_from_registries(self) -> None: + """Remove entity/device from registry.""" + + # Remove callback from device. + self._device.remove_callback(self._async_device_changed) + self._device.remove_callback(self._async_device_removed) + + if not self.registry_entry: + return + + device_id = self.registry_entry.device_id + if device_id: + # Remove from device registry. + device_registry = await dr.async_get_registry(self.hass) + if device_id in device_registry.devices: + # This will also remove associated entities from entity registry. + device_registry.async_remove_device(device_id) + else: + # Remove from entity registry. + # Only relevant for entities that do not belong to a device. + entity_id = self.registry_entry.entity_id + if entity_id: + entity_registry = await er.async_get_registry(self.hass) + if entity_id in entity_registry.entities: + entity_registry.async_remove(entity_id) + + @callback + def _async_device_removed(self, *args, **kwargs): + """Handle hmip device removal.""" + # Set marker showing that the HmIP device hase been removed. + self.hmip_device_removed = True + self.hass.async_create_task(self.async_remove()) + @property def name(self) -> str: """Return the name of the generic device.""" diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 23973efb07b1c7..abba183d339b8e 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -5,6 +5,7 @@ from homematicip.aio.auth import AsyncAuth from homematicip.aio.home import AsyncHome from homematicip.base.base_connection import HmipConnectionError +from homematicip.base.enums import EventType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -137,6 +138,18 @@ def async_update(self, *args, **kwargs): self.home.update_home_only(args[0]) + @callback + def async_create_entity(self, *args, **kwargs): + """Create a device or a group.""" + is_device = EventType(kwargs["event_type"]) == EventType.DEVICE_ADDED + self.hass.async_create_task(self.async_create_entity_lazy(is_device)) + + async def async_create_entity_lazy(self, is_device=True): + """Delay entity creation to allow the user to enter a device name.""" + if is_device: + await asyncio.sleep(30) + await self.hass.config_entries.async_reload(self.config_entry.entry_id) + async def get_state(self): """Update HMIP state and tell Home Assistant.""" await self.home.get_current_state() @@ -225,6 +238,7 @@ async def get_hap( except HmipConnectionError: raise HmipcConnectionError home.on_update(self.async_update) + home.on_create(self.async_create_entity) hass.loop.create_task(self.async_connect()) return home diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b83358822b9f3f..2075f88ded25ab 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.11" + "homematicip==0.10.12" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ab99704f9653cb..9b814ef8eddf12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -649,7 +649,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.11 +homematicip==0.10.12 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ac46e96cd4d78..9599d24b20a1b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -188,7 +188,7 @@ home-assistant-frontend==20190919.1 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.11 +homematicip==0.10.12 # homeassistant.components.google # homeassistant.components.remember_the_milk From 560ac3df3a400f5b413fa4028c1bcfeefac9d9c9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 29 Sep 2019 00:32:13 +0000 Subject: [PATCH 0507/3953] [ci skip] Translation update --- .../components/adguard/.translations/hu.json | 13 +++ .../components/axis/.translations/hu.json | 3 +- .../binary_sensor/.translations/hu.json | 92 +++++++++++++++++++ .../binary_sensor/.translations/it.json | 92 +++++++++++++++++++ .../binary_sensor/.translations/pl.json | 73 ++++++++++++++- .../components/deconz/.translations/hu.json | 5 + .../components/deconz/.translations/pl.json | 22 ++--- .../components/ecobee/.translations/it.json | 25 +++++ .../components/light/.translations/pl.json | 8 +- .../components/met/.translations/hu.json | 13 ++- .../components/plex/.translations/it.json | 23 +++++ .../components/plex/.translations/pl.json | 12 +++ .../simplisafe/.translations/pl.json | 2 +- .../components/switch/.translations/hu.json | 19 ++++ .../components/switch/.translations/pl.json | 12 +-- .../transmission/.translations/it.json | 40 ++++++++ .../components/zha/.translations/it.json | 47 ++++++++++ .../components/zha/.translations/pl.json | 47 ++++++++++ 18 files changed, 521 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/adguard/.translations/hu.json create mode 100644 homeassistant/components/binary_sensor/.translations/hu.json create mode 100644 homeassistant/components/binary_sensor/.translations/it.json create mode 100644 homeassistant/components/ecobee/.translations/it.json create mode 100644 homeassistant/components/switch/.translations/hu.json create mode 100644 homeassistant/components/transmission/.translations/it.json diff --git a/homeassistant/components/adguard/.translations/hu.json b/homeassistant/components/adguard/.translations/hu.json new file mode 100644 index 00000000000000..34b601027c2211 --- /dev/null +++ b/homeassistant/components/adguard/.translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/hu.json b/homeassistant/components/axis/.translations/hu.json index b0c8051e69f9fa..41dd3c00d2b32d 100644 --- a/homeassistant/components/axis/.translations/hu.json +++ b/homeassistant/components/axis/.translations/hu.json @@ -14,6 +14,7 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } } - } + }, + "title": "Axis eszk\u00f6z" } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/hu.json b/homeassistant/components/binary_sensor/.translations/hu.json new file mode 100644 index 00000000000000..e53d918f98d28e --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/hu.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", + "is_cold": "{entity_name} hideg", + "is_connected": "{entity_name} csatlakoztatva van", + "is_gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", + "is_hot": "{entity_name} forr\u00f3", + "is_light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", + "is_locked": "{entity_name} z\u00e1rva van", + "is_moist": "{entity_name} nedves", + "is_motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", + "is_moving": "{entity_name} mozog", + "is_no_gas": "{entity_name} nem \u00e9rz\u00e9kel g\u00e1zt", + "is_no_light": "{entity_name} nem \u00e9rz\u00e9kel f\u00e9nyt", + "is_no_motion": "{entity_name} nem \u00e9rz\u00e9kel mozg\u00e1st", + "is_no_problem": "{entity_name} nem \u00e9szlel probl\u00e9m\u00e1t", + "is_no_smoke": "{entity_name} nem \u00e9rz\u00e9kel f\u00fcst\u00f6t", + "is_no_sound": "{entity_name} nem \u00e9rz\u00e9kel hangot", + "is_no_vibration": "{entity_name} nem \u00e9rz\u00e9kel rezg\u00e9st", + "is_not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", + "is_not_cold": "{entity_name} nem hideg", + "is_not_connected": "{entity_name} le van csatlakoztatva", + "is_not_hot": "{entity_name} nem forr\u00f3", + "is_not_locked": "{entity_name} nyitva van", + "is_not_moist": "{entity_name} sz\u00e1raz", + "is_not_moving": "{entity_name} nem mozog", + "is_not_occupied": "{entity_name} nem foglalt", + "is_not_open": "{entity_name} z\u00e1rva van", + "is_not_plugged_in": "{entity_name} nincs csatlakoztatva", + "is_not_powered": "{entity_name} nincs fesz\u00fcts\u00e9g alatt", + "is_not_present": "{entity_name} nincs jelen", + "is_not_unsafe": "{entity_name} biztons\u00e1gos", + "is_occupied": "{entity_name} foglalt", + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva", + "is_open": "{entity_name} nyitva van", + "is_plugged_in": "{entity_name} csatlakoztatva van", + "is_powered": "{entity_name} fesz\u00fclts\u00e9g alatt van", + "is_present": "{entity_name} jelen van", + "is_problem": "{entity_name} probl\u00e9m\u00e1t \u00e9szlel", + "is_smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", + "is_sound": "{entity_name} hangot \u00e9rz\u00e9kel", + "is_unsafe": "{entity_name} nem biztons\u00e1gos", + "is_vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" + }, + "trigger_type": { + "bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", + "closed": "{entity_name} be lett z\u00e1rva", + "cold": "{entity_name} hideg lett", + "connected": "{entity_name} csatlakozott", + "gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", + "hot": "{entity_name} felforr\u00f3sodott", + "light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", + "locked": "{entity_name} be lett z\u00e1rva", + "moist\u00a7": "{entity_name} nedves lett", + "motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", + "moving": "{entity_name} mozog", + "no_gas": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel g\u00e1zt", + "no_light": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel f\u00e9nyt", + "no_motion": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel mozg\u00e1st", + "no_problem": "{entity_name} m\u00e1r nem \u00e9szlel probl\u00e9m\u00e1t", + "no_smoke": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel f\u00fcst\u00f6t", + "no_sound": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel hangot", + "no_vibration": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel rezg\u00e9st", + "not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", + "not_cold": "{entity_name} m\u00e1r nem hideg", + "not_connected": "{entity_name} lecsatlakozott", + "not_hot": "{entity_name} m\u00e1r nem forr\u00f3", + "not_locked": "{entity_name} ki lett nyitva", + "not_moist": "{entity_name} sz\u00e1raz lett", + "not_moving": "{entity_name} m\u00e1r nem mozog", + "not_occupied": "{entity_name} m\u00e1r nem foglalt", + "not_plugged_in": "{entity_name} m\u00e1r nincs csatlakoztatva", + "not_powered": "{entity_name} m\u00e1r nincs fesz\u00fcts\u00e9g alatt", + "not_present": "{entity_name} m\u00e1r nincs jelen", + "not_unsafe": "{entity_name} biztons\u00e1gos lett", + "occupied": "{entity_name} foglalt lett", + "opened": "{entity_name} ki lett nyitva", + "plugged_in": "{entity_name} csatlakoztatva lett", + "powered": "{entity_name} m\u00e1r fesz\u00fclts\u00e9g alatt van", + "present": "{entity_name} m\u00e1r jelen van", + "problem": "{entity_name} probl\u00e9m\u00e1t \u00e9szlel", + "smoke": "{entity_name} f\u00fcst\u00f6t \u00e9rz\u00e9kel", + "sound": "{entity_name} hangot \u00e9rz\u00e9kel", + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva", + "unsafe": "{entity_name} m\u00e1r nem biztons\u00e1gos", + "vibration": "{entity_name} rezg\u00e9st \u00e9rz\u00e9kel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/it.json b/homeassistant/components/binary_sensor/.translations/it.json new file mode 100644 index 00000000000000..0583a4d4f74c09 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/it.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la batteria \u00e8 scarica", + "is_cold": "{entity_name} \u00e8 freddo", + "is_connected": "{entity_name} \u00e8 collegato", + "is_gas": "{entity_name} sta rilevando il gas", + "is_hot": "{entity_name} \u00e8 caldo", + "is_light": "{entity_name} sta rilevando la luce", + "is_locked": "{entity_name} \u00e8 bloccato", + "is_moist": "{entity_name} \u00e8 umido", + "is_motion": "{entity_name} sta rilevando il movimento", + "is_moving": "{entity_name} si sta muovendo", + "is_no_gas": "{entity_name} non sta rilevando il gas", + "is_no_light": "{entity_name} non sta rilevando la luce", + "is_no_motion": "{entity_name} non sta rilevando il movimento", + "is_no_problem": "{entity_name} non sta rilevando un problema", + "is_no_smoke": "{entity_name} non sta rilevando il fumo", + "is_no_sound": "{entity_name} non sta rilevando il suono", + "is_no_vibration": "{entity_name} non sta rilevando la vibrazione", + "is_not_bat_low": "{entity_name} la batteria \u00e8 normale", + "is_not_cold": "{entity_name} non \u00e8 freddo", + "is_not_connected": "{entity_name} \u00e8 disconnesso", + "is_not_hot": "{entity_name} non \u00e8 caldo", + "is_not_locked": "{entity_name} \u00e8 sbloccato", + "is_not_moist": "{entity_name} \u00e8 asciutto", + "is_not_moving": "{entity_name} non si sta muovendo", + "is_not_occupied": "{entity_name} non \u00e8 occupato", + "is_not_open": "{entity_name} \u00e8 chiuso", + "is_not_plugged_in": "{entity_name} \u00e8 collegato", + "is_not_powered": "{entity_name} non \u00e8 alimentato", + "is_not_present": "{entity_name} non \u00e8 presente", + "is_not_unsafe": "{entity_name} \u00e8 sicuro", + "is_occupied": "{entity_name} \u00e8 occupato", + "is_off": "{entity_name} \u00e8 spento", + "is_on": "{entity_name} \u00e8 acceso", + "is_open": "{entity_name} \u00e8 aperto", + "is_plugged_in": "{entity_name} \u00e8 collegato", + "is_powered": "{entity_name} \u00e8 alimentato", + "is_present": "{entity_name} \u00e8 presente", + "is_problem": "{entity_name} sta rilevando un problema", + "is_smoke": "{entity_name} sta rilevando il fumo", + "is_sound": "{entity_name} sta rilevando il suono", + "is_unsafe": "{entity_name} non \u00e8 sicuro", + "is_vibration": "{entity_name} sta rilevando la vibrazione" + }, + "trigger_type": { + "bat_low": "{entity_name} batteria scarica", + "closed": "{entity_name} \u00e8 chiuso", + "cold": "{entity_name} \u00e8 diventato freddo", + "connected": "{entity_name} connesso", + "gas": "{entity_name} ha iniziato a rilevare il gas", + "hot": "{entity_name} \u00e8 diventato caldo", + "light": "{entity_name} ha iniziato a rilevare la luce", + "locked": "{entity_name} bloccato", + "moist\u00a7": "{entity_name} \u00e8 diventato umido", + "motion": "{entity_name} ha iniziato a rilevare il movimento", + "moving": "{entity_name} ha iniziato a muoversi", + "no_gas": "{entity_name} ha smesso la rilevazione di gas", + "no_light": "{entity_name} smesso il rilevamento di luce", + "no_motion": "{nome_entit\u00e0} ha smesso di rilevare il movimento", + "no_problem": "{nome_entit\u00e0} ha smesso di rilevare un problema", + "no_smoke": "{entity_name} ha smesso la rilevazione di fumo", + "no_sound": "{nome_entit\u00e0} ha smesso di rilevare il suono", + "no_vibration": "{nome_entit\u00e0} ha smesso di rilevare le vibrazioni", + "not_bat_low": "{entity_name} batteria normale", + "not_cold": "{entity_name} non \u00e8 diventato freddo", + "not_connected": "{entity_name} \u00e8 disconnesso", + "not_hot": "{entity_name} non \u00e8 diventato caldo", + "not_locked": "{entity_name} \u00e8 sbloccato", + "not_moist": "{entity_name} \u00e8 diventato asciutto", + "not_moving": "{entity_name} ha smesso di muoversi", + "not_occupied": "{entity_name} non \u00e8 occupato", + "not_plugged_in": "{entity_name} \u00e8 scollegato", + "not_powered": "{entity_name} non \u00e8 alimentato", + "not_present": "{entity_name} non \u00e8 presente", + "not_unsafe": "{entity_name} \u00e8 diventato sicuro", + "occupied": "{entity_name} \u00e8 diventato occupato", + "opened": "{entity_name} \u00e8 aperto", + "plugged_in": "{entity_name} \u00e8 collegato", + "powered": "{entity_name} \u00e8 alimentato", + "present": "{entity_name} \u00e8 presente", + "problem": "{entity_name} ha iniziato a rilevare un problema", + "smoke": "{entity_name} ha iniziato la rilevazione di fumo", + "sound": "{entity_name} ha iniziato il rilevamento del suono", + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato", + "unsafe": "{entity_name} diventato non sicuro", + "vibration": "{entity_name} iniziato a rilevare le vibrazioni" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index 7862644c727826..059800a116fe9d 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -3,11 +3,11 @@ "condition_type": { "is_bat_low": "Bateria {entity_name} jest roz\u0142adowana", "is_cold": "{entity_name} wykrywa zimno", - "is_connected": "{entity_name} jest po\u0142\u0105czone", + "is_connected": "{entity_name} jest po\u0142\u0105czony", "is_gas": "{entity_name} wykrywa gaz", "is_hot": "{entity_name} wykrywa gor\u0105co", "is_light": "{entity_name} wykrywa \u015bwiat\u0142o", - "is_locked": "{entity_name} jest zamkni\u0119te", + "is_locked": "{entity_name} jest zamkni\u0119ty", "is_moist": "{entity_name} wykrywa wilgo\u0107", "is_motion": "{entity_name} wykrywa ruch", "is_moving": "{entity_name} porusza si\u0119", @@ -18,8 +18,75 @@ "is_no_smoke": "{entity_name} nie wykrywa dymu", "is_no_sound": "{entity_name} nie wykrywa d\u017awi\u0119ku", "is_no_vibration": "{entity_name} nie wykrywa wibracji", + "is_not_bat_low": "Bateria {entity_name} nie jest roz\u0142adowana", + "is_not_cold": "{entity_name} nie wykrywa zimna", + "is_not_connected": "{entity_name} jest roz\u0142\u0105czony", + "is_not_hot": "{entity_name} nie wykrywa gor\u0105ca", + "is_not_locked": "{entity_name} jest otwarty", + "is_not_moist": "{entity_name} nie wykrywa wilgoci", + "is_not_moving": "{entity_name} nie porusza si\u0119", + "is_not_occupied": "{entity_name} nie jest zaj\u0119ty", + "is_not_open": "{entity_name} jest zamkni\u0119ty", + "is_not_plugged_in": "{entity_name} jest od\u0142\u0105czony", + "is_not_powered": "{entity_name} nie jest zasilany", + "is_not_present": "{entity_name} nie wykrywa obecno\u015bci", + "is_not_unsafe": "{entity_name} raportuje bezpiecze\u0144stwo", + "is_occupied": "{entity_name} jest zaj\u0119ty", "is_off": "{entity_name} jest wy\u0142\u0105czone", - "is_on": "{entity_name} jest w\u0142\u0105czone" + "is_on": "{entity_name} jest w\u0142\u0105czone", + "is_open": "{entity_name} jest otwarty", + "is_plugged_in": "{entity_name} jest pod\u0142\u0105czony", + "is_powered": "{entity_name} jest zasilany", + "is_present": "{entity_name} wykrywa obecno\u015b\u0107", + "is_problem": "{entity_name} wykrywa problem", + "is_smoke": "{entity_name} wykrywa dym", + "is_sound": "{entity_name} wykrywa d\u017awi\u0119k", + "is_unsafe": "{entity_name} raportuje niebezpiecze\u0144stwo", + "is_vibration": "{entity_name} wykrywa wibracje" + }, + "trigger_type": { + "bat_low": "Bateria {entity_name} staje si\u0119 roz\u0142adowanie", + "closed": "Zamkni\u0119cie {entity_name}", + "cold": "Wykrycie zimna przez {entity_name}", + "connected": "Pod\u0142\u0105czenie {entity_name}", + "gas": "Wykrycie gazu przez {entity_name}", + "hot": "Wykrycie gor\u0105ca przez {entity_name}", + "light": "Wykrycie \u015bwiat\u0142a przez {entity_name}", + "locked": "Zamkni\u0119cie {entity_name}", + "moist\u00a7": "Wykrycie wilgoci przez {entity_name}", + "motion": "Wykrycie ruchu przez {entity_name}", + "moving": "{entity_name} zacz\u0105\u0142 si\u0119 porusza\u0107", + "no_gas": "Wykrycie braku gazu przez {entity_name}", + "no_light": "Wykrycie braku \u015bwiat\u0142a przez {entity_name}", + "no_motion": "Wykrycie braku ruchu przez {entity_name}", + "no_problem": "Wykrycie braku problemu przez {entity_name}", + "no_smoke": "Wykrycie braku dymu przez {entity_name}", + "no_sound": "Wykrycie braku d\u017awi\u0119ku przez {entity_name}", + "no_vibration": "Wykrycie braku wibracji przez {entity_name}", + "not_bat_low": "Bateria {entity_name} staje si\u0119 na\u0142adowana", + "not_cold": "Wykrycie braku zimna przez {entity_name}", + "not_connected": "Roz\u0142\u0105czenie {entity_name}", + "not_hot": "Wykrycie braku gor\u0105ca przez {entity_name}", + "not_locked": "Otwarcie {entity_name}", + "not_moist": "Wykrycie braku wilgoci przez {entity_name}", + "not_moving": "{entity_name} przesta\u0142 si\u0119 porusza\u0107", + "not_occupied": "{entity_name} sta\u0142 si\u0119 niezaj\u0119ty", + "not_plugged_in": "Od\u0142\u0105czenie {entity_name}", + "not_powered": "Brak zasilania dla {entity_name}", + "not_present": "Wykrycie braku obecno\u015bci przez {entity_name}", + "not_unsafe": "Raportowanie bezpiecze\u0144stwa przez {entity_name}", + "occupied": "{entity_name} sta\u0142 si\u0119 zaj\u0119ty", + "opened": "Otwarcie {entity_name}", + "plugged_in": "Pod\u0142\u0105czenie {entity_name}", + "powered": "Zasilenie {entity_name}", + "present": "Wykrycie obecno\u015bci przez {entity_name}", + "problem": "Wykrycie problemu przez {entity_name}", + "smoke": "Wykrycie dymu przez {entity_name}", + "sound": "Wykrycie d\u017awi\u0119ku przez {entity_name}", + "turned_off": "Wy\u0142\u0105czenie {entity_name}", + "turned_on": "W\u0142\u0105czenie {entity_name}", + "unsafe": "Raportowanie niebezpiecze\u0144stwa przez {entity_name}", + "vibration": "Wykrycie wibracji przez {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index 5bf8db4684190f..9e8109107436df 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -29,5 +29,10 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "device_automation": { + "trigger_subtype": { + "close": "Bez\u00e1r\u00e1s" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 70c33cf3c02f4f..d92f318f61fda7 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -44,18 +44,18 @@ "device_automation": { "trigger_subtype": { "both_buttons": "Oba przyciski", - "button_1": "Pierwszy przycisk", - "button_2": "Drugi przycisk", - "button_3": "Trzeci przycisk", - "button_4": "Czwarty przycisk", - "close": "Zamknij", + "button_1": "pierwszy przycisk", + "button_2": "drugi przycisk", + "button_3": "trzeci przycisk", + "button_4": "czwarty przycisk", + "close": "Zamkni\u0119cie", "dim_down": "Przyciemnienie", "dim_up": "Przyciemnienie", - "left": "Lewo", - "open": "Otw\u00f3rz", - "right": "Prawo", - "turn_off": "Wy\u0142\u0105cz", - "turn_on": "W\u0142\u0105cz" + "left": "w lewo", + "open": "Otwarcie", + "right": "w prawo", + "turn_off": "Wy\u0142\u0105czenie", + "turn_on": "W\u0142\u0105czenie" }, "trigger_type": { "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", @@ -67,7 +67,7 @@ "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", - "remote_gyro_activated": "Urz\u0105dzenie potrz\u0105\u015bni\u0119te" + "remote_gyro_activated": "Potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" } }, "options": { diff --git a/homeassistant/components/ecobee/.translations/it.json b/homeassistant/components/ecobee/.translations/it.json new file mode 100644 index 00000000000000..2ecb587f19e13a --- /dev/null +++ b/homeassistant/components/ecobee/.translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Questa integrazione supporta attualmente una sola istanza ecobee." + }, + "error": { + "pin_request_failed": "Errore durante la richiesta del PIN da ecobee; verificare che la chiave API sia corretta.", + "token_request_failed": "Errore durante la richiesta di token da ecobee; per favore riprova." + }, + "step": { + "authorize": { + "description": "Autorizza questa app su https://www.ecobee.com/consumerportal/index.html con il codice PIN: \n\n {pin} \n \n Quindi, premi Invia.", + "title": "Autorizza l'app su ecobee.com" + }, + "user": { + "data": { + "api_key": "API Key" + }, + "description": "Inserisci la chiave API ottenuta da ecobee.com.", + "title": "chiave API ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 17c81c471f5018..4b649744ed977c 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -6,12 +6,12 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "{entity_name} jest wy\u0142\u0105czone", - "is_on": "{entity_name} jest w\u0142\u0105czony." + "is_off": "{entity_name} jest wy\u0142\u0105czony", + "is_on": "{entity_name} jest w\u0142\u0105czony" }, "trigger_type": { - "turned_off": "{entity_name} wy\u0142\u0105czone", - "turned_on": "{entity_name} w\u0142\u0105czone" + "turned_off": "Wy\u0142\u0105czenie {entity_name}", + "turned_on": "W\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/met/.translations/hu.json b/homeassistant/components/met/.translations/hu.json index 3b34d8f6354d6a..dcbc40b4c71729 100644 --- a/homeassistant/components/met/.translations/hu.json +++ b/homeassistant/components/met/.translations/hu.json @@ -1,9 +1,20 @@ { "config": { + "error": { + "name_exists": "A hely m\u00e1r l\u00e9tezik" + }, "step": { "user": { + "data": { + "elevation": "Magass\u00e1g", + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g", + "name": "N\u00e9v" + }, + "description": "Meteorol\u00f3giai int\u00e9zet", "title": "Elhelyezked\u00e9s" } - } + }, + "title": "Met.no" } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 2e77b4ba9768b5..3c28f1d25f9c6b 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Autorizzazione non riuscita", "no_servers": "Nessun server collegato all'account", + "no_token": "Fornire un token o selezionare la configurazione manuale", "not_found": "Server Plex non trovato" }, "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Porta", + "ssl": "Usa SSL", + "token": "Token (se richiesto)", + "verify_ssl": "Verificare il certificato SSL" + }, + "title": "Server Plex" + }, "select_server": { "data": { "server": "Server" @@ -22,6 +33,7 @@ }, "user": { "data": { + "manual_setup": "Configurazione manuale", "token": "Token Plex" }, "description": "Immettere un token Plex per la configurazione automatica.", @@ -29,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostra tutti i controlli", + "use_episode_art": "Usa la grafica dell'episodio" + }, + "description": "Opzioni per i lettori multimediali Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index ea1db4ec2f3243..ce9d2e1e88d73c 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -10,9 +10,20 @@ "error": { "faulty_credentials": "Autoryzacja nie powiod\u0142a si\u0119", "no_servers": "Brak serwer\u00f3w po\u0142\u0105czonych z kontem", + "no_token": "Wprowad\u017a token lub wybierz konfiguracj\u0119 r\u0119czn\u0105", "not_found": "Nie znaleziono serwera Plex" }, "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "U\u017cyj SSL", + "token": "Token (je\u015bli wymagany)", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "title": "Serwer Plex" + }, "select_server": { "data": { "server": "Serwer" @@ -22,6 +33,7 @@ }, "user": { "data": { + "manual_setup": "Konfiguracja r\u0119czna", "token": "Token Plex" }, "description": "Wprowad\u017a token Plex do automatycznej konfiguracji.", diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json index c4d616600f56a7..ad8a15d06b7455 100644 --- a/homeassistant/components/simplisafe/.translations/pl.json +++ b/homeassistant/components/simplisafe/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "Konto jest ju\u017c zarejestrowane", - "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { "user": { diff --git a/homeassistant/components/switch/.translations/hu.json b/homeassistant/components/switch/.translations/hu.json new file mode 100644 index 00000000000000..c3ea3190694baf --- /dev/null +++ b/homeassistant/components/switch/.translations/hu.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} be/kikapcsol\u00e1sa", + "turn_off": "{entity_name} kikapcsol\u00e1sa", + "turn_on": "{entity_name} bekapcsol\u00e1sa" + }, + "condition_type": { + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva", + "turn_off": "{entity_name} ki lett kapcsolva", + "turn_on": "{entity_name} be lett kapcsolva" + }, + "trigger_type": { + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 31187aaa1b7cc4..201a77a76a5d09 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -6,14 +6,14 @@ "turn_on": "W\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "{entity_name} jest wy\u0142\u0105czone", - "is_on": "{entity_name} jest w\u0142\u0105czone", - "turn_off": "{entity_name} wy\u0142\u0105czone", - "turn_on": "{entity_name} w\u0142\u0105czone" + "is_off": "{entity_name} jest wy\u0142\u0105czony", + "is_on": "{entity_name} jest w\u0142\u0105czony", + "turn_off": "{entity_name} wy\u0142\u0105czony", + "turn_on": "{entity_name} w\u0142\u0105czony" }, "trigger_type": { - "turned_off": "{entity_name} wy\u0142\u0105czone", - "turned_on": "{entity_name} w\u0142\u0105czone" + "turned_off": "Wy\u0142\u0105czenie {entity_name}", + "turned_on": "W\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/it.json b/homeassistant/components/transmission/.translations/it.json new file mode 100644 index 00000000000000..17a03b6dba1745 --- /dev/null +++ b/homeassistant/components/transmission/.translations/it.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u00c8 necessaria solo una singola istanza." + }, + "error": { + "cannot_connect": "Impossibile connettersi all'host", + "wrong_credentials": "Nome utente o password non validi" + }, + "step": { + "options": { + "data": { + "scan_interval": "Frequenza di aggiornamento" + }, + "title": "Configura opzioni" + }, + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "title": "Configura client di Trasmissione" + } + }, + "title": "Trasmissione" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequenza di aggiornamento" + }, + "description": "Configurare le opzioni per Trasmissione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/it.json b/homeassistant/components/zha/.translations/it.json index e4b87c9d7b6dfb..bb05977fd098b9 100644 --- a/homeassistant/components/zha/.translations/it.json +++ b/homeassistant/components/zha/.translations/it.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Strillare", + "warn": "Avvertire" + }, + "trigger_subtype": { + "both_buttons": "Entrambi i pulsanti", + "button_1": "Primo pulsante", + "button_2": "Secondo pulsante", + "button_3": "Terzo pulsante", + "button_4": "Quarto pulsante", + "button_5": "Quinto pulsante", + "button_6": "Sesto pulsante", + "close": "Chiudere", + "dim_down": "Diminuire luminosit\u00e0", + "dim_up": "Aumentare luminosit\u00e0", + "face_1": "con faccia 1 attivata", + "face_2": "con faccia 2 attivata", + "face_3": "con faccia 3 attivata", + "face_4": "con faccia 4 attivata", + "face_5": "con faccia 5 attivata", + "face_6": "con faccia 6 attivata", + "face_any": "Con una o pi\u00f9 facce specificate attivate", + "left": "Sinistra", + "open": "Aperto", + "right": "Destra", + "turn_off": "Spento", + "turn_on": "Acceso" + }, + "trigger_type": { + "device_dropped": "Dispositivo caduto", + "device_flipped": "Dispositivo capovolto \" {subtype} \"", + "device_knocked": "Dispositivo bussato \" {subtype} \"", + "device_rotated": "Dispositivo ruotato \" {subtype} \"", + "device_shaken": "Dispositivo in vibrazione", + "device_slid": "Dispositivo scivolato \"{sottotipo}\"", + "device_tilted": "Dispositivo inclinato", + "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", + "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", + "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", + "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", + "remote_button_short_press": "Pulsante \"{subtype}\" premuto", + "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", + "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json index 93867c0c84f051..76f1c58fe7ccc8 100644 --- a/homeassistant/components/zha/.translations/pl.json +++ b/homeassistant/components/zha/.translations/pl.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Skrzek", + "warn": "Ostrze\u017cenie" + }, + "trigger_subtype": { + "both_buttons": "Oba przyciski", + "button_1": "Pierwszy przycisk", + "button_2": "Drugi przycisk", + "button_3": "Trzeci przycisk", + "button_4": "Czwarty przycisk", + "button_5": "Pi\u0105ty przycisk", + "button_6": "Sz\u00f3sty przycisk", + "close": "Zamkni\u0119cie", + "dim_down": "\u015aciemnianie", + "dim_up": "Rozja\u015bnienie", + "face_1": "z aktywowan\u0105 twarz\u0105 1", + "face_2": "z aktywowan\u0105 twarz\u0105 2", + "face_3": "z aktywowan\u0105 twarz\u0105 3", + "face_4": "z aktywowan\u0105 twarz\u0105 4", + "face_5": "z aktywowan\u0105 twarz\u0105 5", + "face_6": "z aktywowan\u0105 twarz\u0105 6", + "face_any": "z dowoln\u0105 twarz\u0105 aktywowan\u0105", + "left": "w lewo", + "open": "Otwarcie", + "right": "w prawo", + "turn_off": "Wy\u0142\u0105czenie", + "turn_on": "W\u0142\u0105czenie" + }, + "trigger_type": { + "device_dropped": "Upadek urz\u0105dzenia", + "device_flipped": "Odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_knocked": "Pukni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_rotated": "Obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_shaken": "Potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", + "device_slid": "Przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_tilted": "Przechylenie urz\u0105dzenia", + "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "Przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "Przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", + "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", + "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty" + } } } \ No newline at end of file From f464a7808878093351986c87109724539163ec4c Mon Sep 17 00:00:00 2001 From: david81 Date: Sat, 28 Sep 2019 23:36:35 -0400 Subject: [PATCH 0508/3953] Add venstar support for hvac action (#26956) * Added support for current fan state and hvac action * Corrected handling of fan_mode --- homeassistant/components/venstar/climate.py | 29 ++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 7e1ae1ecd60b09..7be31d56c08275 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -11,14 +11,20 @@ HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, SUPPORT_FAN_MODE, + FAN_ON, + FAN_AUTO, SUPPORT_TARGET_HUMIDITY, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, PRESET_AWAY, PRESET_NONE, SUPPORT_TARGET_TEMPERATURE_RANGE, - HVAC_MODE_OFF, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -156,7 +162,7 @@ def current_humidity(self): @property def hvac_mode(self): - """Return current operation ie. heat, cool, idle.""" + """Return current operation mode ie. heat, cool, auto.""" if self._client.mode == self._client.MODE_HEAT: return HVAC_MODE_HEAT if self._client.mode == self._client.MODE_COOL: @@ -165,12 +171,23 @@ def hvac_mode(self): return HVAC_MODE_AUTO return HVAC_MODE_OFF + @property + def hvac_action(self): + """Return current operation mode ie. heat, cool, auto.""" + if self._client.state == self._client.STATE_IDLE: + return CURRENT_HVAC_IDLE + if self._client.state == self._client.STATE_HEATING: + return CURRENT_HVAC_HEAT + if self._client.state == self._client.STATE_COOLING: + return CURRENT_HVAC_COOL + return CURRENT_HVAC_OFF + @property def fan_mode(self): - """Return the fan setting.""" - if self._client.fan == self._client.FAN_AUTO: - return HVAC_MODE_AUTO - return STATE_ON + """Return the current fan mode.""" + if self._client.fan == self._client.FAN_ON: + return FAN_ON + return FAN_AUTO @property def device_state_attributes(self): From 2ebc1901abb8b4e1fadcef44ae9d5509f9b8052a Mon Sep 17 00:00:00 2001 From: Khole Date: Sun, 29 Sep 2019 10:38:43 +0100 Subject: [PATCH 0509/3953] Change hive hotwater to hot_water + bug fix (#27038) * Updated hotwater to hot_water + bug fix * Updated version seperating dependancy --- homeassistant/components/hive/__init__.py | 12 ++++++------ homeassistant/components/hive/manifest.json | 2 +- homeassistant/components/hive/services.yaml | 2 +- requirements_all.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index c11eb18accad98..3301097bab79b2 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -23,7 +23,7 @@ DOMAIN = "hive" DATA_HIVE = "data_hive" SERVICES = ["Heating", "HotWater"] -SERVICE_BOOST_HOTWATER = "boost_hotwater" +SERVICE_BOOST_HOT_WATER = "boost_hot_water" SERVICE_BOOST_HEATING = "boost_heating" ATTR_TIME_PERIOD = "time_period" ATTR_MODE = "on_off" @@ -59,7 +59,7 @@ } ) -BOOST_HOTWATER_SCHEMA = vol.Schema( +BOOST_HOT_WATER_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_id, vol.Optional(ATTR_TIME_PERIOD, default="00:30:00"): vol.All( @@ -100,7 +100,7 @@ def heating_boost(service): session.heating.turn_boost_on(node_id, minutes, temperature) - def hotwater_boost(service): + def hot_water_boost(service): """Handle the service call.""" node_id = HiveSession.entity_lookup.get(service.data[ATTR_ENTITY_ID]) if not node_id: @@ -151,9 +151,9 @@ def hotwater_boost(service): if ha_type == "water_heater": hass.services.register( DOMAIN, - SERVICE_BOOST_HEATING, - hotwater_boost, - schema=BOOST_HOTWATER_SCHEMA, + SERVICE_BOOST_HOT_WATER, + hot_water_boost, + schema=BOOST_HOT_WATER_SCHEMA, ) return True diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 2e7c4f4f179dee..d9fae3fe54b936 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "documentation": "https://www.home-assistant.io/components/hive", "requirements": [ - "pyhiveapi==0.2.19" + "pyhiveapi==0.2.19.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/hive/services.yaml b/homeassistant/components/hive/services.yaml index 27d7acfc83bab0..6513d76ca89981 100644 --- a/homeassistant/components/hive/services.yaml +++ b/homeassistant/components/hive/services.yaml @@ -14,7 +14,7 @@ boost_heating: description: Set the target temperature for the boost period., example: "20.5", } -boost_hotwater: +boost_hot_water: description: "Set the boost mode ON or OFF defining the period of time for the boost." fields: diff --git a/requirements_all.txt b/requirements_all.txt index 9b814ef8eddf12..79313b48e61ed2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1223,7 +1223,7 @@ pyheos==0.6.0 pyhik==0.2.3 # homeassistant.components.hive -pyhiveapi==0.2.19 +pyhiveapi==0.2.19.2 # homeassistant.components.homematic pyhomematic==0.1.60 From 4f55235aa2f6801b3b1e2a08ef2971df7bcb0c93 Mon Sep 17 00:00:00 2001 From: David K <142583+neffs@users.noreply.github.com> Date: Sun, 29 Sep 2019 12:06:51 +0200 Subject: [PATCH 0510/3953] Return esphome cover position as Integer (#27039) cover position is specified as integer 0-100, we should not return float here. fixes #25738 --- homeassistant/components/esphome/cover.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 7da2fcee3805ad..31b895b4eb2f55 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -91,11 +91,11 @@ def is_closing(self) -> bool: return self._state.current_operation == CoverOperation.IS_CLOSING @esphome_state_property - def current_cover_position(self) -> Optional[float]: + def current_cover_position(self) -> Optional[int]: """Return current position of cover. 0 is closed, 100 is open.""" if not self._static_info.supports_position: return None - return self._state.position * 100.0 + return round(self._state.position * 100.0) @esphome_state_property def current_cover_tilt_position(self) -> Optional[float]: From f259ff17d525cd945eb965440691b11a92ccb2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 29 Sep 2019 20:07:49 +0300 Subject: [PATCH 0511/3953] Type hint additions (#26831) * Type hint additions * Remove optional from sidebar_icon comment Co-Authored-By: Franck Nijhof * Remove optional from sidebar_title comment Co-Authored-By: Franck Nijhof * Fix issues after rebase and mypy 0.730 --- homeassistant/components/automation/state.py | 18 +++++++++---- .../components/device_automation/__init__.py | 6 ++++- .../device_automation/toggle_entity.py | 9 ++++--- homeassistant/components/frontend/__init__.py | 16 ++++++------ homeassistant/components/group/__init__.py | 14 +++++++--- homeassistant/components/group/cover.py | 26 +++++++++++++++---- homeassistant/components/group/light.py | 11 +++++--- homeassistant/components/group/notify.py | 3 +++ .../components/media_player/__init__.py | 5 ++-- .../persistent_notification/__init__.py | 20 +++++++++----- homeassistant/components/sun/__init__.py | 3 +++ .../components/websocket_api/__init__.py | 3 +++ .../components/websocket_api/auth.py | 8 +++++- .../components/websocket_api/commands.py | 3 +++ .../components/websocket_api/connection.py | 7 ++++- .../components/websocket_api/decorators.py | 2 ++ .../components/websocket_api/http.py | 19 ++++++++------ .../components/websocket_api/messages.py | 2 ++ .../components/websocket_api/sensor.py | 3 +++ homeassistant/components/zone/__init__.py | 13 +++++++--- homeassistant/components/zone/config_flow.py | 12 +++++++-- homeassistant/components/zone/zone.py | 13 ++++++++-- homeassistant/core.py | 3 ++- homeassistant/data_entry_flow.py | 12 ++++----- homeassistant/helpers/entity.py | 13 ++++++---- homeassistant/helpers/intent.py | 2 +- mypyrc | 6 +++++ 27 files changed, 184 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 184b9ea302b11c..154394075a02c6 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -1,16 +1,19 @@ """Offer state listening automation rules.""" +from datetime import timedelta import logging +from typing import Dict import voluptuous as vol from homeassistant import exceptions -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, CALLBACK_TYPE, callback 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 +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -38,8 +41,13 @@ async def async_attach_trigger( - hass, config, action, automation_info, *, platform_type="state" -): + hass: HomeAssistant, + config, + action, + automation_info, + *, + platform_type: str = "state", +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) from_state = config.get(CONF_FROM, MATCH_ALL) @@ -48,7 +56,7 @@ async def async_attach_trigger( template.attach(hass, time_delta) match_all = from_state == MATCH_ALL and to_state == MATCH_ALL unsub_track_same = {} - period = {} + period: Dict[str, timedelta] = {} @callback def state_automation_listener(entity, from_s, to_s): diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 62d338ece54a49..23e320fe153b60 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,6 +1,7 @@ """Helpers for device automations.""" import asyncio import logging +from typing import Any, List, MutableMapping import voluptuous as vol @@ -11,6 +12,9 @@ from .exceptions import InvalidDeviceAutomationConfig + +# mypy: allow-untyped-calls, allow-untyped-defs + DOMAIN = "device_automation" _LOGGER = logging.getLogger(__name__) @@ -96,7 +100,7 @@ async def _async_get_device_automations(hass, automation_type, device_id): ) domains = set() - automations = [] + automations: List[MutableMapping[str, Any]] = [] device = device_registry.async_get(device_id) for entry_id in device.config_entries: config_entry = hass.config_entries.async_get_entry(entry_id) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index b7cadd1349a3eb..ef1b605f4d68e0 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,5 +1,5 @@ """Device automation helpers for toggle entity.""" -from typing import List +from typing import Any, Dict, List import voluptuous as vol from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE @@ -19,6 +19,9 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import TRIGGER_BASE_SCHEMA + +# mypy: allow-untyped-calls, allow-untyped-defs + ENTITY_ACTIONS = [ { # Turn entity off @@ -88,7 +91,7 @@ async def async_call_action_from_config( variables: TemplateVarsType, context: Context, domain: str, -): +) -> None: """Change state based on configuration.""" config = ACTION_SCHEMA(config) action_type = config[CONF_TYPE] @@ -156,7 +159,7 @@ async def _async_get_automations( hass: HomeAssistant, device_id: str, automation_templates: List[dict], domain: str ) -> List[dict]: """List device automations.""" - automations = [] + automations: List[Dict[str, Any]] = [] entity_registry = await hass.helpers.entity_registry.async_get_registry() entries = [ diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8ef662ec878f90..e46423c8271c96 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -4,7 +4,7 @@ import mimetypes import os import pathlib -from typing import Optional, Set, Tuple +from typing import Any, Dict, Optional, Set, Tuple from aiohttp import web, web_urldispatcher, hdrs import voluptuous as vol @@ -122,19 +122,19 @@ class Panel: """Abstract class for panels.""" # Name of the webcomponent - component_name = None + component_name: Optional[str] = None - # Icon to show in the sidebar (optional) - sidebar_icon = None + # Icon to show in the sidebar + sidebar_icon: Optional[str] = None - # Title to show in the sidebar (optional) - sidebar_title = None + # Title to show in the sidebar + sidebar_title: Optional[str] = None # Url to show the panel in the frontend - frontend_url_path = None + frontend_url_path: Optional[str] = None # Config to pass to the webcomponent - config = None + config: Optional[Dict[str, Any]] = None # If the panel should only be visible to admins require_admin = False diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 75b45471982b49..204fcab0381aeb 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -1,6 +1,7 @@ """Provide the functionality to group entities.""" import asyncio import logging +from typing import Any, Iterable, List, Optional, cast import voluptuous as vol @@ -32,9 +33,12 @@ from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.async_ import run_coroutine_threadsafe +# mypy: allow-untyped-calls, allow-untyped-defs + DOMAIN = "group" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -143,12 +147,12 @@ def is_on(hass, entity_id): @bind_hass -def expand_entity_ids(hass, entity_ids): +def expand_entity_ids(hass: HomeAssistantType, entity_ids: Iterable[Any]) -> List[str]: """Return entity_ids with group entity ids replaced by their members. Async friendly. """ - found_ids = [] + found_ids: List[str] = [] for entity_id in entity_ids: if not isinstance(entity_id, str): continue @@ -182,7 +186,9 @@ def expand_entity_ids(hass, entity_ids): @bind_hass -def get_entity_ids(hass, entity_id, domain_filter=None): +def get_entity_ids( + hass: HomeAssistantType, entity_id: str, domain_filter: Optional[str] = None +) -> List[str]: """Get members of this group. Async friendly. @@ -194,7 +200,7 @@ def get_entity_ids(hass, entity_id, domain_filter=None): entity_ids = group.attributes[ATTR_ENTITY_ID] if not domain_filter: - return entity_ids + return cast(List[str], entity_ids) domain_filter = domain_filter.lower() + "." diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index faa4ddfc87d608..c5200082f2fb25 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -1,5 +1,6 @@ """This platform allows several cover to be grouped into one cover.""" import logging +from typing import Dict, Optional, Set import voluptuous as vol @@ -11,7 +12,7 @@ CONF_NAME, STATE_CLOSED, ) -from homeassistant.core import callback +from homeassistant.core import callback, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change @@ -41,6 +42,9 @@ CoverDevice, ) + +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) KEY_OPEN_CLOSE = "open_close" @@ -76,13 +80,25 @@ def __init__(self, name, entities): self._assumed_state = True self._entities = entities - self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), KEY_POSITION: set()} - self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), KEY_POSITION: set()} + self._covers: Dict[str, Set[str]] = { + KEY_OPEN_CLOSE: set(), + KEY_STOP: set(), + KEY_POSITION: set(), + } + self._tilts: Dict[str, Set[str]] = { + KEY_OPEN_CLOSE: set(), + KEY_STOP: set(), + KEY_POSITION: set(), + } @callback def update_supported_features( - self, entity_id, old_state, new_state, update_state=True - ): + self, + entity_id: str, + old_state: Optional[State], + new_state: Optional[State], + update_state: bool = True, + ) -> None: """Update dictionaries with supported features.""" if not new_state: for values in self._covers.values(): diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 0b1291d4045f42..e77c858fc0272a 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -3,7 +3,7 @@ from collections import Counter import itertools import logging -from typing import Any, Callable, Iterator, List, Optional, Tuple +from typing import Any, Callable, Iterator, List, Optional, Tuple, cast import voluptuous as vol @@ -43,6 +43,9 @@ SUPPORT_WHITE_VALUE, ) + +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Light Group" @@ -69,7 +72,9 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Initialize light.group platform.""" - async_add_entities([LightGroup(config.get(CONF_NAME), config[CONF_ENTITIES])]) + async_add_entities( + [LightGroup(cast(str, config.get(CONF_NAME)), config[CONF_ENTITIES])] + ) class LightGroup(light.Light): @@ -263,7 +268,7 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] - states = list(filter(None, all_states)) + states: List[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] self._is_on = len(on_states) > 0 diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 3d3c644fea9673..2ffb7fea049438 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -17,6 +17,9 @@ BaseNotificationService, ) + +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) CONF_SERVICES = "services" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 791dacb70243c9..98da19fd98efe0 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -7,6 +7,7 @@ import hashlib import logging from random import SystemRandom +from typing import Optional from urllib.parse import urlparse from aiohttp import web @@ -347,7 +348,7 @@ async def async_unload_entry(hass, entry): class MediaPlayerDevice(Entity): """ABC for media player devices.""" - _access_token = None + _access_token: Optional[str] = None # Implement these for your media player @property @@ -356,7 +357,7 @@ def state(self): return None @property - def access_token(self): + def access_token(self) -> str: """Access token for this media player.""" if self._access_token is None: self._access_token = hashlib.sha256( diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 6c49784ededcb2..6b9c7c44ddf5ce 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -1,7 +1,7 @@ """Support for displaying persistent notifications.""" from collections import OrderedDict import logging -from typing import Awaitable +from typing import Any, Mapping, MutableMapping, Optional import voluptuous as vol @@ -14,6 +14,9 @@ from homeassistant.util import slugify import homeassistant.util.dt as dt_util + +# mypy: allow-untyped-calls, allow-untyped-defs + ATTR_CREATED_AT = "created_at" ATTR_MESSAGE = "message" ATTR_NOTIFICATION_ID = "notification_id" @@ -70,7 +73,10 @@ def dismiss(hass, notification_id): @callback @bind_hass def async_create( - hass: HomeAssistant, message: str, title: str = None, notification_id: str = None + hass: HomeAssistant, + message: str, + title: Optional[str] = None, + notification_id: Optional[str] = None, ) -> None: """Generate a notification.""" data = { @@ -95,9 +101,9 @@ def async_dismiss(hass: HomeAssistant, notification_id: str) -> None: hass.async_create_task(hass.services.async_call(DOMAIN, SERVICE_DISMISS, data)) -async def async_setup(hass: HomeAssistant, config: dict) -> Awaitable[bool]: +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the persistent notification component.""" - persistent_notifications = OrderedDict() + persistent_notifications: MutableMapping[str, MutableMapping] = OrderedDict() hass.data[DOMAIN] = {"notifications": persistent_notifications} @callback @@ -201,8 +207,10 @@ def mark_read_service(call): @callback def websocket_get_notifications( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: Mapping[str, Any], +) -> None: """Return a list of persistent_notifications.""" connection.send_message( websocket_api.result_message( diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 3e6048e15320b6..7d883e273e5053 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -17,6 +17,9 @@ ) from homeassistant.util import dt as dt_util + +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "sun" diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 57ab34a6a5a409..1ec758ebd4d1a5 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -4,6 +4,9 @@ from . import commands, connection, const, decorators, http, messages + +# mypy: allow-untyped-calls, allow-untyped-defs + DOMAIN = const.DOMAIN DEPENDENCIES = ("http",) diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 2d748f5cc6aca3..716b20f4ca4068 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -2,6 +2,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant.auth.models import RefreshToken, User from homeassistant.auth.providers import legacy_api_password from homeassistant.components.http.ban import process_wrong_login, process_success_login from homeassistant.const import __version__ @@ -9,6 +10,9 @@ from .connection import ActiveConnection from .error import Disconnect + +# mypy: allow-untyped-calls, allow-untyped-defs + TYPE_AUTH = "auth" TYPE_AUTH_INVALID = "auth_invalid" TYPE_AUTH_OK = "auth_ok" @@ -87,7 +91,9 @@ async def async_handle(self, msg): await process_wrong_login(self._request) raise Disconnect - async def _async_finish_auth(self, user, refresh_token) -> ActiveConnection: + async def _async_finish_auth( + self, user: User, refresh_token: RefreshToken + ) -> ActiveConnection: """Create an active connection.""" self._logger.debug("Auth OK") await process_success_login(self._request) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index deb3600574fb4b..9d46238b24104f 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -12,6 +12,9 @@ from . import const, decorators, messages +# mypy: allow-untyped-calls, allow-untyped-defs + + @callback def async_register_commands(hass, async_reg): """Register commands.""" diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 3886d6c21d081e..41232b097d14e1 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -1,5 +1,7 @@ """Connection session.""" import asyncio +from typing import Any, Callable, Dict, Hashable + import voluptuous as vol from homeassistant.core import callback, Context @@ -8,6 +10,9 @@ from . import const, messages +# mypy: allow-untyped-calls, allow-untyped-defs + + class ActiveConnection: """Handle an active websocket client connection.""" @@ -22,7 +27,7 @@ def __init__(self, logger, hass, send_message, user, refresh_token): else: self.refresh_token_id = None - self.subscriptions = {} + self.subscriptions: Dict[Hashable, Callable[[], Any]] = {} self.last_id = 0 def context(self, msg): diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index ba65d5e19a843e..025131643e894a 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -8,6 +8,8 @@ from . import messages +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 9a1f375fdfda20..17a6709496a4e6 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -25,6 +25,9 @@ from .messages import error_message +# mypy: allow-untyped-calls, allow-untyped-defs + + class WebsocketAPIView(HomeAssistantView): """View to serve a websockets endpoint.""" @@ -45,7 +48,7 @@ def __init__(self, hass, request): self.hass = hass self.request = request self.wsock = None - self._to_write = asyncio.Queue(maxsize=MAX_PENDING_MSG) + self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) self._handle_task = None self._writer_task = None self._logger = logging.getLogger("{}.connection.{}".format(__name__, id(self))) @@ -106,7 +109,7 @@ async def async_handle(self): # Py3.7+ if hasattr(asyncio, "current_task"): # pylint: disable=no-member - self._handle_task = asyncio.current_task() + self._handle_task = asyncio.current_task() # type: ignore else: self._handle_task = asyncio.Task.current_task() @@ -144,13 +147,13 @@ def handle_hass_stop(event): raise Disconnect try: - msg = msg.json() + msg_data = msg.json() except ValueError: disconnect_warn = "Received invalid JSON." raise Disconnect - self._logger.debug("Received %s", msg) - connection = await auth.async_handle(msg) + self._logger.debug("Received %s", msg_data) + connection = await auth.async_handle(msg_data) self.hass.data[DATA_CONNECTIONS] = ( self.hass.data.get(DATA_CONNECTIONS, 0) + 1 ) @@ -170,13 +173,13 @@ def handle_hass_stop(event): break try: - msg = msg.json() + msg_data = msg.json() except ValueError: disconnect_warn = "Received invalid JSON." break - self._logger.debug("Received %s", msg) - connection.async_handle(msg) + self._logger.debug("Received %s", msg_data) + connection.async_handle(msg_data) except asyncio.CancelledError: self._logger.info("Connection closed by client") diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 65291bc55e7ee9..c8c760a6549560 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -7,6 +7,8 @@ from . import const +# mypy: allow-untyped-defs + # Minimal requirements of a message MINIMAL_MESSAGE_SCHEMA = vol.Schema( {vol.Required("id"): cv.positive_int, vol.Required("type"): cv.string}, diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 1ae76b562525c8..20a6a90860be42 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -10,6 +10,9 @@ ) +# mypy: allow-untyped-calls, allow-untyped-defs + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the API streams platform.""" entity = APICount() diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 2ee03c08189aea..6ae62be3eb9b2d 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,9 +1,10 @@ """Support for the definition of zones.""" import logging +from typing import Set, cast import voluptuous as vol -from homeassistant.core import callback +from homeassistant.core import callback, State from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.const import ( @@ -25,6 +26,9 @@ from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE, ATTR_PASSIVE, ATTR_RADIUS from .zone import Zone + +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Unnamed zone" @@ -78,10 +82,11 @@ def async_active_zone(hass, latitude, longitude, radius=0): ) within_zone = zone_dist - radius < zone.attributes[ATTR_RADIUS] - closer_zone = closest is None or zone_dist < min_dist + closer_zone = closest is None or zone_dist < min_dist # type: ignore smaller_zone = ( zone_dist == min_dist - and zone.attributes[ATTR_RADIUS] < closest.attributes[ATTR_RADIUS] + and zone.attributes[ATTR_RADIUS] + < cast(State, closest).attributes[ATTR_RADIUS] ) if within_zone and (closer_zone or smaller_zone): @@ -94,7 +99,7 @@ def async_active_zone(hass, latitude, longitude, radius=0): async def async_setup(hass, config): """Set up configured zones as well as home assistant zone if necessary.""" hass.data[DOMAIN] = {} - entities = set() + entities: Set[str] = set() zone_entries = configured_zones(hass) for _, entry in config_per_platform(config, DOMAIN): if slugify(entry[CONF_NAME]) not in zone_entries: diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index 05ba28e4ca7360..d23fb5a47577ca 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -1,5 +1,7 @@ """Config flow to configure zone component.""" +from typing import Set + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -12,17 +14,23 @@ CONF_RADIUS, ) from homeassistant.core import callback +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE +# mypy: allow-untyped-defs + + @callback -def configured_zones(hass): +def configured_zones(hass: HomeAssistantType) -> Set[str]: """Return a set of the configured zones.""" return set( (slugify(entry.data[CONF_NAME])) - for entry in hass.config_entries.async_entries(DOMAIN) + for entry in ( + hass.config_entries.async_entries(DOMAIN) if hass.config_entries else [] + ) ) diff --git a/homeassistant/components/zone/zone.py b/homeassistant/components/zone/zone.py index ccd8e55a4ce4d5..f084492bd34fe8 100644 --- a/homeassistant/components/zone/zone.py +++ b/homeassistant/components/zone/zone.py @@ -1,5 +1,9 @@ """Zone entity and functionality.""" + +from typing import cast + from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import State from homeassistant.helpers.entity import Entity from homeassistant.util.location import distance @@ -8,7 +12,10 @@ STATE = "zoning" -def in_zone(zone, latitude, longitude, radius=0) -> bool: +# mypy: allow-untyped-defs + + +def in_zone(zone: State, latitude: float, longitude: float, radius: float = 0) -> bool: """Test if given latitude, longitude is in given zone. Async friendly. @@ -20,7 +27,9 @@ def in_zone(zone, latitude, longitude, radius=0) -> bool: zone.attributes[ATTR_LONGITUDE], ) - return zone_dist - radius < zone.attributes[ATTR_RADIUS] + if zone_dist is None or zone.attributes[ATTR_RADIUS] is None: + return False + return zone_dist - radius < cast(float, zone.attributes[ATTR_RADIUS]) class Zone(Entity): diff --git a/homeassistant/core.py b/homeassistant/core.py index e011db33c343a8..f4be3b66323d1b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -28,6 +28,7 @@ Set, TYPE_CHECKING, Awaitable, + Mapping, ) from async_timeout import timeout @@ -704,7 +705,7 @@ def __init__( self, entity_id: str, state: str, - attributes: Optional[Dict] = None, + attributes: Optional[Mapping] = None, last_changed: Optional[datetime.datetime] = None, last_updated: Optional[datetime.datetime] = None, context: Optional[Context] = None, diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 3b128646219db2..0bc27498f767e8 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -170,7 +170,7 @@ class FlowHandler: # Set by flow manager flow_id: Optional[str] = None hass: Optional[HomeAssistant] = None - handler = None + handler: Optional[Hashable] = None cur_step: Optional[Dict[str, str]] = None context: Dict @@ -188,7 +188,7 @@ def async_show_form( data_schema: vol.Schema = None, errors: Optional[Dict] = None, description_placeholders: Optional[Dict] = None, - ) -> Dict: + ) -> Dict[str, Any]: """Return the definition of a form to gather user input.""" return { "type": RESULT_TYPE_FORM, @@ -208,7 +208,7 @@ def async_create_entry( data: Dict, description: Optional[str] = None, description_placeholders: Optional[Dict] = None, - ) -> Dict: + ) -> Dict[str, Any]: """Finish config flow and create a config entry.""" return { "version": self.VERSION, @@ -224,7 +224,7 @@ def async_create_entry( @callback def async_abort( self, *, reason: str, description_placeholders: Optional[Dict] = None - ) -> Dict: + ) -> Dict[str, Any]: """Abort the config flow.""" return { "type": RESULT_TYPE_ABORT, @@ -237,7 +237,7 @@ def async_abort( @callback def async_external_step( self, *, step_id: str, url: str, description_placeholders: Optional[Dict] = None - ) -> Dict: + ) -> Dict[str, Any]: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP, @@ -249,7 +249,7 @@ def async_external_step( } @callback - def async_external_step_done(self, *, next_step_id: str) -> Dict: + def async_external_step_done(self, *, next_step_id: str) -> Dict[str, Any]: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP_DONE, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index fad02dee075839..836ad954ae0c6b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,5 +1,7 @@ """An abstract class for entities.""" -from datetime import timedelta + +import asyncio +from datetime import datetime, timedelta import logging import functools as ft from timeit import default_timer as timer @@ -22,6 +24,7 @@ ATTR_SUPPORTED_FEATURES, ATTR_DEVICE_CLASS, ) +from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, @@ -94,7 +97,7 @@ class Entity: hass: Optional[HomeAssistant] = None # Owning platform instance. Will be set by EntityPlatform - platform = None + platform: Optional[EntityPlatform] = None # If we reported if this entity was slow _slow_reported = False @@ -106,7 +109,7 @@ class Entity: _update_staged = False # Process updates in parallel - parallel_updates = None + parallel_updates: Optional[asyncio.Semaphore] = None # Entry in the entity registry registry_entry: Optional[RegistryEntry] = None @@ -115,8 +118,8 @@ class Entity: _on_remove: Optional[List[CALLBACK_TYPE]] = None # Context - _context = None - _context_set = None + _context: Optional[Context] = None + _context_set: Optional[datetime] = None @property def should_poll(self) -> bool: diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 1fa0ec76a67c1b..dc48d825348f04 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -124,7 +124,7 @@ class IntentHandler: intent_type: Optional[str] = None slot_schema: Optional[vol.Schema] = None - _slot_schema = None + _slot_schema: Optional[vol.Schema] = None platforms: Optional[Iterable[str]] = [] @callback diff --git a/mypyrc b/mypyrc index f3866f40e5743c..08413ecd23c696 100644 --- a/mypyrc +++ b/mypyrc @@ -6,8 +6,10 @@ homeassistant/components/binary_sensor/ homeassistant/components/calendar/ homeassistant/components/camera/ homeassistant/components/cover/ +homeassistant/components/device_automation/ homeassistant/components/frontend/ homeassistant/components/geo_location/ +homeassistant/components/group/ homeassistant/components/history/ homeassistant/components/http/ homeassistant/components/image_processing/ @@ -17,16 +19,20 @@ homeassistant/components/lock/ homeassistant/components/mailbox/ homeassistant/components/media_player/ homeassistant/components/notify/ +homeassistant/components/persistent_notification/ homeassistant/components/proximity/ homeassistant/components/remote/ homeassistant/components/scene/ homeassistant/components/sensor/ +homeassistant/components/sun/ homeassistant/components/switch/ homeassistant/components/systemmonitor/ homeassistant/components/tts/ homeassistant/components/vacuum/ homeassistant/components/water_heater/ homeassistant/components/weather/ +homeassistant/components/websocket_api/ +homeassistant/components/zone/ homeassistant/helpers/ homeassistant/scripts/ homeassistant/util/ From 52bbb6242cfdebf29adbcf608fbd52bf7858d9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 30 Sep 2019 00:00:39 +0300 Subject: [PATCH 0512/3953] Upgrade pytest to 5.2.0 (#27058) https://docs.pytest.org/en/latest/changelog.html#pytest-5-2-0-2019-09-28 --- 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 7e5be09a28ba9c..9da375b33c8a91 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.1.3 +pytest==5.2.0 requests_mock==1.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9599d24b20a1b7..2701513a6de7b9 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.1.3 +pytest==5.2.0 requests_mock==1.7.0 From cdb469f711310d75f805261df99aa37aeb723f2e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 30 Sep 2019 00:32:17 +0000 Subject: [PATCH 0513/3953] [ci skip] Translation update --- .../ambient_station/.translations/es.json | 2 +- .../arcam_fmj/.translations/pt-BR.json | 5 + .../binary_sensor/.translations/ca.json | 27 +++ .../binary_sensor/.translations/es.json | 92 ++++++++++ .../binary_sensor/.translations/pl.json | 168 +++++++++--------- .../cert_expiry/.translations/pt-BR.json | 21 +++ .../components/deconz/.translations/pl.json | 34 ++-- .../dialogflow/.translations/es.json | 2 +- .../components/ecobee/.translations/ca.json | 25 +++ .../components/ecobee/.translations/es.json | 25 +++ .../components/ecobee/.translations/sl.json | 25 +++ .../components/izone/.translations/es.json | 15 ++ .../components/light/.translations/pl.json | 14 +- .../components/plex/.translations/ca.json | 11 ++ .../components/plex/.translations/es.json | 56 ++++++ .../components/plex/.translations/sl.json | 11 ++ .../plex/.translations/zh-Hant.json | 11 ++ .../components/switch/.translations/pl.json | 18 +- .../traccar/.translations/pt-BR.json | 5 + .../transmission/.translations/ca.json | 40 +++++ .../transmission/.translations/es.json | 40 +++++ .../transmission/.translations/pt-BR.json | 37 ++++ .../transmission/.translations/sl.json | 40 +++++ .../transmission/.translations/zh-Hant.json | 40 +++++ .../components/unifi/.translations/pt-BR.json | 10 ++ .../components/withings/.translations/es.json | 3 + .../components/zha/.translations/ca.json | 47 +++++ .../components/zha/.translations/es.json | 47 +++++ .../components/zha/.translations/pl.json | 60 +++---- .../components/zha/.translations/pt-BR.json | 5 + .../components/zha/.translations/sl.json | 47 +++++ .../components/zha/.translations/zh-Hant.json | 4 + 32 files changed, 838 insertions(+), 149 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/pt-BR.json create mode 100644 homeassistant/components/binary_sensor/.translations/es.json create mode 100644 homeassistant/components/cert_expiry/.translations/pt-BR.json create mode 100644 homeassistant/components/ecobee/.translations/ca.json create mode 100644 homeassistant/components/ecobee/.translations/es.json create mode 100644 homeassistant/components/ecobee/.translations/sl.json create mode 100644 homeassistant/components/izone/.translations/es.json create mode 100644 homeassistant/components/plex/.translations/es.json create mode 100644 homeassistant/components/traccar/.translations/pt-BR.json create mode 100644 homeassistant/components/transmission/.translations/ca.json create mode 100644 homeassistant/components/transmission/.translations/es.json create mode 100644 homeassistant/components/transmission/.translations/pt-BR.json create mode 100644 homeassistant/components/transmission/.translations/sl.json create mode 100644 homeassistant/components/transmission/.translations/zh-Hant.json diff --git a/homeassistant/components/ambient_station/.translations/es.json b/homeassistant/components/ambient_station/.translations/es.json index d4b0075aa6576e..d4222f1d2ebea9 100644 --- a/homeassistant/components/ambient_station/.translations/es.json +++ b/homeassistant/components/ambient_station/.translations/es.json @@ -14,6 +14,6 @@ "title": "Completa tu informaci\u00f3n" } }, - "title": "Ambient PWS" + "title": "Ambiente PWS" } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/pt-BR.json b/homeassistant/components/arcam_fmj/.translations/pt-BR.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ca.json b/homeassistant/components/binary_sensor/.translations/ca.json index 434c236418b37d..de7d837b12cc0d 100644 --- a/homeassistant/components/binary_sensor/.translations/ca.json +++ b/homeassistant/components/binary_sensor/.translations/ca.json @@ -2,29 +2,43 @@ "device_automation": { "condition_type": { "is_bat_low": "Bateria de {entity_name} baixa", + "is_cold": "{entity_name} est\u00e0 fred", "is_connected": "{entity_name} est\u00e0 connectat", "is_gas": "{entity_name} est\u00e0 detectant gas", + "is_hot": "{entity_name} est\u00e0 calent", "is_light": "{entity_name} est\u00e0 detectant llum", "is_locked": "{entity_name} est\u00e0 bloquejat", + "is_moist": "{entity_name} est\u00e0 humit", "is_motion": "{entity_name} est\u00e0 detectant moviment", + "is_moving": "{entity_name} s'est\u00e0 movent", "is_no_gas": "{entity_name} no detecta gas", "is_no_light": "{entity_name} no detecta llum", "is_no_motion": "{entity_name} no detecta moviment", + "is_no_problem": "{entity_name} no est\u00e0 detectant cap problema", "is_no_smoke": "{entity_name} no detecta fum", "is_no_sound": "{entity_name} no detecta so", "is_no_vibration": "{entity_name} no detecta vibraci\u00f3", "is_not_bat_low": "Bateria de {entity_name} normal", + "is_not_cold": "{entity_name} no est\u00e0 fred", "is_not_connected": "{entity_name} est\u00e0 desconnectat", + "is_not_hot": "{entity_name} no est\u00e0 calent", "is_not_locked": "{entity_name} est\u00e0 desbloquejat", + "is_not_moist": "{entity_name} est\u00e0 sec", + "is_not_moving": "{entity_name} no s'est\u00e0 movent", "is_not_occupied": "{entity_name} no est\u00e0 ocupat", + "is_not_open": "{entity_name} est\u00e0 tancat", + "is_not_plugged_in": "{entity_name} est\u00e0 desendollat", "is_not_powered": "{entity_name} no est\u00e0 alimentat", "is_not_present": "{entity_name} no est\u00e0 present", + "is_not_unsafe": "{entity_name} \u00e9s segur", "is_occupied": "{entity_name} est\u00e0 ocupat", "is_off": "{entity_name} est\u00e0 apagat", "is_on": "{entity_name} est\u00e0 enc\u00e8s", "is_open": "{entity_name} est\u00e0 obert", + "is_plugged_in": "{entity_name} est\u00e0 endollat", "is_powered": "{entity_name} est\u00e0 alimentat", "is_present": "{entity_name} est\u00e0 present", + "is_problem": "{entity_name} est\u00e0 detectant un problema", "is_smoke": "{entity_name} est\u00e0 detectant fum", "is_sound": "{entity_name} est\u00e0 detectant so", "is_unsafe": "{entity_name} \u00e9s insegur", @@ -33,10 +47,13 @@ "trigger_type": { "bat_low": "Bateria de {entity_name} baixa", "closed": "{entity_name} est\u00e0 tancat", + "cold": "{entity_name} es torna fred", "connected": "{entity_name} est\u00e0 connectat", "gas": "{entity_name} ha comen\u00e7at a detectar gas", + "hot": "{entity_name} es torna calent", "light": "{entity_name} ha comen\u00e7at a detectar llum", "locked": "{entity_name} est\u00e0 bloquejat", + "moist\u00a7": "{entity_name} es torna humit", "motion": "{entity_name} ha comen\u00e7at a detectar moviment", "moving": "{entity_name} ha comen\u00e7at a moure's", "no_gas": "{entity_name} ha deixat de detectar gas", @@ -47,11 +64,20 @@ "no_sound": "{entity_name} ha deixat de detectar so", "no_vibration": "{entity_name} ha deixat de detectar vibraci\u00f3", "not_bat_low": "Bateria de {entity_name} normal", + "not_cold": "{entity_name} es torna no-fred", "not_connected": "{entity_name} est\u00e0 desconnectat", + "not_hot": "{entity_name} es torna no-calent", "not_locked": "{entity_name} est\u00e0 desbloquejat", + "not_moist": "{entity_name} es torna sec", "not_moving": "{entity_name} ha parat de moure's", + "not_occupied": "{entity_name} es desocupa", + "not_plugged_in": "{entity_name} desendollat", "not_powered": "{entity_name} no est\u00e0 alimentat", "not_present": "{entity_name} no est\u00e0 present", + "not_unsafe": "{entity_name} es torna segur", + "occupied": "{entity_name} s'ocupa", + "opened": "{entity_name} s'ha obert", + "plugged_in": "{entity_name} s'ha endollat", "powered": "{entity_name} alimentat", "present": "{entity_name} present", "problem": "{entity_name} ha comen\u00e7at a detectar un problema", @@ -59,6 +85,7 @@ "sound": "{entity_name} ha comen\u00e7at a detectar so", "turned_off": "{entity_name} apagat", "turned_on": "{entity_name} enc\u00e8s", + "unsafe": "{entity_name} es torna insegur", "vibration": "{entity_name} ha comen\u00e7at a detectar vibraci\u00f3" } } diff --git a/homeassistant/components/binary_sensor/.translations/es.json b/homeassistant/components/binary_sensor/.translations/es.json new file mode 100644 index 00000000000000..8e2d326d9d3fe9 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/es.json @@ -0,0 +1,92 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la bater\u00eda est\u00e1 baja", + "is_cold": "{entity_name} est\u00e1 fr\u00edo", + "is_connected": "{entity_name} est\u00e1 conectado", + "is_gas": "{entity_name} est\u00e1 detectando gas", + "is_hot": "{entity_name} est\u00e1 caliente", + "is_light": "{entity_name} est\u00e1 detectando luz", + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_moist": "{entity_name} est\u00e1 h\u00famedo", + "is_motion": "{entity_name} est\u00e1 detectando movimiento", + "is_moving": "{entity_name} se est\u00e1 moviendo", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta la luz", + "is_no_motion": "{entity_name} no detecta movimiento", + "is_no_problem": "{entity_name} no detecta el problema", + "is_no_smoke": "{entity_name} no detecta humo", + "is_no_sound": "{entity_name} no detecta sonido", + "is_no_vibration": "{entity_name} no detecta vibraci\u00f3n", + "is_not_bat_low": "La bater\u00eda de {entity_name} es normal", + "is_not_cold": "{entity_name} no est\u00e1 fr\u00edo", + "is_not_connected": "{entity_name} est\u00e1 desconectado", + "is_not_hot": "{entity_name} no est\u00e1 caliente", + "is_not_locked": "{entity_name} est\u00e1 desbloqueado", + "is_not_moist": "{entity_name} est\u00e1 seco", + "is_not_moving": "{entity_name} no se mueve", + "is_not_occupied": "{entity_name} no est\u00e1 ocupado", + "is_not_open": "{entity_name} est\u00e1 cerrado", + "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_powered": "{entity_name} no tiene alimentaci\u00f3n", + "is_not_present": "{entity_name} no est\u00e1 presente", + "is_not_unsafe": "{entity_name} es seguro", + "is_occupied": "{entity_name} est\u00e1 ocupado", + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 activado", + "is_open": "{entity_name} est\u00e1 abierto", + "is_plugged_in": "{entity_name} est\u00e1 conectado", + "is_powered": "{entity_name} est\u00e1 activado", + "is_present": "{entity_name} est\u00e1 presente", + "is_problem": "{entity_name} est\u00e1 detectando un problema", + "is_smoke": "{entity_name} est\u00e1 detectando humo", + "is_sound": "{entity_name} est\u00e1 detectando sonido", + "is_unsafe": "{entity_name} no es seguro", + "is_vibration": "{entity_name} est\u00e1 detectando vibraciones" + }, + "trigger_type": { + "bat_low": "{entity_name} bater\u00eda baja", + "closed": "{entity_name} cerrado", + "cold": "{entity_name} se enfri\u00f3", + "connected": "{entity_name} conectado", + "gas": "{entity_name} empez\u00f3 a detectar gas", + "hot": "{entity_name} se est\u00e1 calentando", + "light": "{entity_name} empez\u00f3 a detectar la luz", + "locked": "{entity_name} bloqueado", + "moist\u00a7": "{entity_name} se humedeci\u00f3", + "motion": "{entity_name} comenz\u00f3 a detectar movimiento", + "moving": "{entity_name} empez\u00f3 a moverse", + "no_gas": "{entity_name} dej\u00f3 de detectar gas", + "no_light": "{entity_name} dej\u00f3 de detectar la luz", + "no_motion": "{entity_name} dej\u00f3 de detectar movimiento", + "no_problem": "{entity_name} dej\u00f3 de detectar el problema", + "no_smoke": "{entity_name} dej\u00f3 de detectar humo", + "no_sound": "{entity_name} dej\u00f3 de detectar sonido", + "no_vibration": "{entity_name} dej\u00f3 de detectar vibraci\u00f3n", + "not_bat_low": "{entity_name} bater\u00eda normal", + "not_cold": "{entity_name} no se enfri\u00f3", + "not_connected": "{entity_name} desconectado", + "not_hot": "{entity_name} no se calent\u00f3", + "not_locked": "{entity_name} desbloqueado", + "not_moist": "{entity_name} se sec\u00f3", + "not_moving": "{entity_name} dej\u00f3 de moverse", + "not_occupied": "{entity_name} no est\u00e1 ocupado", + "not_plugged_in": "{entity_name} desconectado", + "not_powered": "{entity_name} no est\u00e1 activado", + "not_present": "{entity_name} no est\u00e1 presente", + "not_unsafe": "{entity_name} se volvi\u00f3 seguro", + "occupied": "{entity_name} se convirti\u00f3 en ocupado", + "opened": "{entity_name} abierto", + "plugged_in": "{nombre_de_la_entidad} conectado", + "powered": "{entity_name} alimentado", + "present": "{entity_name} presente", + "problem": "{entity_name} empez\u00f3 a detectar problemas", + "smoke": "{entity_name} empez\u00f3 a detectar humo", + "sound": "{entity_name} empez\u00f3 a detectar sonido", + "turned_off": "{entity_name} desactivado", + "turned_on": "{entity_name} activado", + "unsafe": "{entity_name} se volvi\u00f3 inseguro", + "vibration": "{entity_name} empez\u00f3 a detectar vibraciones" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index 059800a116fe9d..a7f0bd516a08f5 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -1,92 +1,92 @@ { "device_automation": { "condition_type": { - "is_bat_low": "Bateria {entity_name} jest roz\u0142adowana", - "is_cold": "{entity_name} wykrywa zimno", - "is_connected": "{entity_name} jest po\u0142\u0105czony", - "is_gas": "{entity_name} wykrywa gaz", - "is_hot": "{entity_name} wykrywa gor\u0105co", - "is_light": "{entity_name} wykrywa \u015bwiat\u0142o", - "is_locked": "{entity_name} jest zamkni\u0119ty", - "is_moist": "{entity_name} wykrywa wilgo\u0107", - "is_motion": "{entity_name} wykrywa ruch", - "is_moving": "{entity_name} porusza si\u0119", - "is_no_gas": "{entity_name} nie wykrywa gazu", - "is_no_light": "{entity_name} nie wykrywa \u015bwiat\u0142a", - "is_no_motion": "{entity_name} nie wykrywa ruchu", - "is_no_problem": "{entity_name} nie wykrywa problemu", - "is_no_smoke": "{entity_name} nie wykrywa dymu", - "is_no_sound": "{entity_name} nie wykrywa d\u017awi\u0119ku", - "is_no_vibration": "{entity_name} nie wykrywa wibracji", - "is_not_bat_low": "Bateria {entity_name} nie jest roz\u0142adowana", - "is_not_cold": "{entity_name} nie wykrywa zimna", - "is_not_connected": "{entity_name} jest roz\u0142\u0105czony", - "is_not_hot": "{entity_name} nie wykrywa gor\u0105ca", - "is_not_locked": "{entity_name} jest otwarty", - "is_not_moist": "{entity_name} nie wykrywa wilgoci", - "is_not_moving": "{entity_name} nie porusza si\u0119", - "is_not_occupied": "{entity_name} nie jest zaj\u0119ty", - "is_not_open": "{entity_name} jest zamkni\u0119ty", - "is_not_plugged_in": "{entity_name} jest od\u0142\u0105czony", - "is_not_powered": "{entity_name} nie jest zasilany", - "is_not_present": "{entity_name} nie wykrywa obecno\u015bci", - "is_not_unsafe": "{entity_name} raportuje bezpiecze\u0144stwo", - "is_occupied": "{entity_name} jest zaj\u0119ty", - "is_off": "{entity_name} jest wy\u0142\u0105czone", - "is_on": "{entity_name} jest w\u0142\u0105czone", - "is_open": "{entity_name} jest otwarty", - "is_plugged_in": "{entity_name} jest pod\u0142\u0105czony", - "is_powered": "{entity_name} jest zasilany", - "is_present": "{entity_name} wykrywa obecno\u015b\u0107", - "is_problem": "{entity_name} wykrywa problem", - "is_smoke": "{entity_name} wykrywa dym", - "is_sound": "{entity_name} wykrywa d\u017awi\u0119k", - "is_unsafe": "{entity_name} raportuje niebezpiecze\u0144stwo", - "is_vibration": "{entity_name} wykrywa wibracje" + "is_bat_low": "bateria {entity_name} jest roz\u0142adowana", + "is_cold": "sensor {entity_name} wykrywa zimno", + "is_connected": "sensor {entity_name} raportuje po\u0142\u0105czenie", + "is_gas": "sensor {entity_name} wykrywa gaz", + "is_hot": "sensor {entity_name} wykrywa gor\u0105co", + "is_light": "sensor {entity_name} wykrywa \u015bwiat\u0142o", + "is_locked": "sensor {entity_name} wykrywa zamkni\u0119cie", + "is_moist": "sensor {entity_name} wykrywa wilgo\u0107", + "is_motion": "sensor {entity_name} wykrywa ruch", + "is_moving": "sensor {entity_name} porusza si\u0119", + "is_no_gas": "sensor {entity_name} nie wykrywa gazu", + "is_no_light": "sensor {entity_name} nie wykrywa \u015bwiat\u0142a", + "is_no_motion": "sensor {entity_name} nie wykrywa ruchu", + "is_no_problem": "sensor {entity_name} nie wykrywa problemu", + "is_no_smoke": "sensor {entity_name} nie wykrywa dymu", + "is_no_sound": "sensor {entity_name} nie wykrywa d\u017awi\u0119ku", + "is_no_vibration": "sensor {entity_name} nie wykrywa wibracji", + "is_not_bat_low": "bateria {entity_name} nie jest roz\u0142adowana", + "is_not_cold": "sensor {entity_name} nie wykrywa zimna", + "is_not_connected": "sensor {entity_name} nie wykrywa roz\u0142\u0105czenia", + "is_not_hot": "sensor {entity_name} nie wykrywa gor\u0105ca", + "is_not_locked": "sensor {entity_name} nie wykrywa otwarcia", + "is_not_moist": "sensor {entity_name} nie wykrywa wilgoci", + "is_not_moving": "sensor {entity_name} nie porusza si\u0119", + "is_not_occupied": "sensor {entity_name} nie jest zaj\u0119ty", + "is_not_open": "sensor {entity_name} jest zamkni\u0119ty", + "is_not_plugged_in": "sensor {entity_name} wykrywa od\u0142\u0105czenie", + "is_not_powered": "sensor {entity_name} nie wykrywa zasilania", + "is_not_present": "sensor {entity_name} nie wykrywa obecno\u015bci", + "is_not_unsafe": "sensor {entity_name} nie wykrywa niebezpiecze\u0144stwa", + "is_occupied": "sensor {entity_name} jest zaj\u0119ty", + "is_off": "sensor {entity_name} jest wy\u0142\u0105czony", + "is_on": "sensor {entity_name} jest w\u0142\u0105czony", + "is_open": "sensor {entity_name} jest otwarty", + "is_plugged_in": "sensor {entity_name} wykrywa pod\u0142\u0105czenie", + "is_powered": "sensor {entity_name} wykrywa zasilanie", + "is_present": "sensor {entity_name} wykrywa obecno\u015b\u0107", + "is_problem": "sensor {entity_name} wykrywa problem", + "is_smoke": "sensor {entity_name} wykrywa dym", + "is_sound": "sensor {entity_name} wykrywa d\u017awi\u0119k", + "is_unsafe": "sensor {entity_name} wykrywa niebezpiecze\u0144stwo", + "is_vibration": "sensor {entity_name} wykrywa wibracje" }, "trigger_type": { - "bat_low": "Bateria {entity_name} staje si\u0119 roz\u0142adowanie", - "closed": "Zamkni\u0119cie {entity_name}", - "cold": "Wykrycie zimna przez {entity_name}", - "connected": "Pod\u0142\u0105czenie {entity_name}", - "gas": "Wykrycie gazu przez {entity_name}", - "hot": "Wykrycie gor\u0105ca przez {entity_name}", - "light": "Wykrycie \u015bwiat\u0142a przez {entity_name}", - "locked": "Zamkni\u0119cie {entity_name}", - "moist\u00a7": "Wykrycie wilgoci przez {entity_name}", - "motion": "Wykrycie ruchu przez {entity_name}", - "moving": "{entity_name} zacz\u0105\u0142 si\u0119 porusza\u0107", - "no_gas": "Wykrycie braku gazu przez {entity_name}", - "no_light": "Wykrycie braku \u015bwiat\u0142a przez {entity_name}", - "no_motion": "Wykrycie braku ruchu przez {entity_name}", - "no_problem": "Wykrycie braku problemu przez {entity_name}", - "no_smoke": "Wykrycie braku dymu przez {entity_name}", - "no_sound": "Wykrycie braku d\u017awi\u0119ku przez {entity_name}", - "no_vibration": "Wykrycie braku wibracji przez {entity_name}", - "not_bat_low": "Bateria {entity_name} staje si\u0119 na\u0142adowana", - "not_cold": "Wykrycie braku zimna przez {entity_name}", - "not_connected": "Roz\u0142\u0105czenie {entity_name}", - "not_hot": "Wykrycie braku gor\u0105ca przez {entity_name}", - "not_locked": "Otwarcie {entity_name}", - "not_moist": "Wykrycie braku wilgoci przez {entity_name}", - "not_moving": "{entity_name} przesta\u0142 si\u0119 porusza\u0107", - "not_occupied": "{entity_name} sta\u0142 si\u0119 niezaj\u0119ty", - "not_plugged_in": "Od\u0142\u0105czenie {entity_name}", - "not_powered": "Brak zasilania dla {entity_name}", - "not_present": "Wykrycie braku obecno\u015bci przez {entity_name}", - "not_unsafe": "Raportowanie bezpiecze\u0144stwa przez {entity_name}", - "occupied": "{entity_name} sta\u0142 si\u0119 zaj\u0119ty", - "opened": "Otwarcie {entity_name}", - "plugged_in": "Pod\u0142\u0105czenie {entity_name}", - "powered": "Zasilenie {entity_name}", - "present": "Wykrycie obecno\u015bci przez {entity_name}", - "problem": "Wykrycie problemu przez {entity_name}", - "smoke": "Wykrycie dymu przez {entity_name}", - "sound": "Wykrycie d\u017awi\u0119ku przez {entity_name}", - "turned_off": "Wy\u0142\u0105czenie {entity_name}", - "turned_on": "W\u0142\u0105czenie {entity_name}", - "unsafe": "Raportowanie niebezpiecze\u0144stwa przez {entity_name}", - "vibration": "Wykrycie wibracji przez {entity_name}" + "bat_low": "bateria {entity_name} stanie si\u0119 roz\u0142adowana", + "closed": "zamkni\u0119cie {entity_name}", + "cold": "sensor {entity_name} wykryje zimno", + "connected": "pod\u0142\u0105czenie {entity_name}", + "gas": "sensor {entity_name} wykryje gaz", + "hot": "sensor {entity_name} wykryje gor\u0105co", + "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", + "locked": "zamkni\u0119cie {entity_name}", + "moist\u00a7": "sensor {entity_name} wykryje wilgo\u0107", + "motion": "sensor {entity_name} wykryje ruch", + "moving": "sensor {entity_name} zacznie porusza\u0107 si\u0119", + "no_gas": "sensor {entity_name} przestanie wykrywa\u0107 gaz", + "no_light": "sensor {entity_name} przestanie wykrywa\u0107 \u015bwiat\u0142o", + "no_motion": "sensor {entity_name} przestanie wykrywa\u0107 ruch", + "no_problem": "sensor {entity_name} przestanie wykrywa\u0107 problem", + "no_smoke": "sensor {entity_name} przestanie wykrywa\u0107 dym", + "no_sound": "sensor {entity_name} przestanie wykrywa\u0107 d\u017awi\u0119k", + "no_vibration": "sensor {entity_name} przestanie wykrywa\u0107 wibracje", + "not_bat_low": "bateria {entity_name} staje si\u0119 na\u0142adowana", + "not_cold": "sensor {entity_name} przestanie wykrywa\u0107 zimno", + "not_connected": "roz\u0142\u0105czenie {entity_name}", + "not_hot": "sensor {entity_name} przestanie wykrywa\u0107 gor\u0105co", + "not_locked": "otwarcie {entity_name}", + "not_moist": "sensor {entity_name} przestanie wykrywa\u0107 wilgo\u0107", + "not_moving": "sensor {entity_name} przestanie porusza\u0107 si\u0119", + "not_occupied": "sensor {entity_name} przesta\u0142 by\u0107 zaj\u0119ty", + "not_plugged_in": "od\u0142\u0105czenie {entity_name}", + "not_powered": "od\u0142\u0105czenie zasilania {entity_name}", + "not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107", + "not_unsafe": "sensor {entity_name} przestanie wykrywa\u0107 niebezpiecze\u0144stwo", + "occupied": "sensor {entity_name} sta\u0142 si\u0119 zaj\u0119ty", + "opened": "otwarcie {entity_name}", + "plugged_in": "pod\u0142\u0105czenie {entity_name}", + "powered": "pod\u0142\u0105czenie zasilenia {entity_name}", + "present": "sensor {entity_name} wykryje obecno\u015b\u0107", + "problem": "sensor {entity_name} wykryje problem", + "smoke": "sensor {entity_name} wykryje dym", + "sound": "sensor {entity_name} wykryje d\u017awi\u0119k", + "turned_off": "wy\u0142\u0105czenie {entity_name}", + "turned_on": "w\u0142\u0105czenie {entity_name}", + "unsafe": "sensor {entity_name} wykryje niebezpiecze\u0144stwo", + "vibration": "sensor {entity_name} wykryje wibracje" } } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/pt-BR.json b/homeassistant/components/cert_expiry/.translations/pt-BR.json new file mode 100644 index 00000000000000..d26f0f9470e7e0 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "host_port_exists": "Essa combina\u00e7\u00e3o de host e porta j\u00e1 est\u00e1 configurada" + }, + "error": { + "certificate_fetch_failed": "N\u00e3o \u00e9 poss\u00edvel buscar o certificado dessa combina\u00e7\u00e3o de host e porta", + "connection_timeout": "Tempo limite ao conectar-se a este host", + "resolve_failed": "Este host n\u00e3o pode ser resolvido" + }, + "step": { + "user": { + "data": { + "host": "O nome do host do certificado", + "name": "O nome do certificado", + "port": "A porta do certificado" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index d92f318f61fda7..11a1beb10d6fa1 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -43,31 +43,31 @@ }, "device_automation": { "trigger_subtype": { - "both_buttons": "Oba przyciski", + "both_buttons": "oba przyciski", "button_1": "pierwszy przycisk", "button_2": "drugi przycisk", "button_3": "trzeci przycisk", "button_4": "czwarty przycisk", - "close": "Zamkni\u0119cie", - "dim_down": "Przyciemnienie", - "dim_up": "Przyciemnienie", + "close": "zamkni\u0119cie", + "dim_down": "zmniejszenie jasno\u015bci", + "dim_up": "zwi\u0119kszenie jasno\u015bci", "left": "w lewo", - "open": "Otwarcie", + "open": "otwarcie", "right": "w prawo", - "turn_off": "Wy\u0142\u0105czenie", - "turn_on": "W\u0142\u0105czenie" + "turn_off": "wy\u0142\u0105czenie", + "turn_on": "wy\u0142\u0105czenie" }, "trigger_type": { - "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "Przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "Przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_rotated": "Przycisk obr\u00f3cony \"{subtype}\"", - "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", - "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", - "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", - "remote_gyro_activated": "Potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" + "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_rotated": "przycisk obr\u00f3cony \"{subtype}\"", + "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", + "remote_gyro_activated": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" } }, "options": { diff --git a/homeassistant/components/dialogflow/.translations/es.json b/homeassistant/components/dialogflow/.translations/es.json index 1d6a849f3a8701..c106543e158b47 100644 --- a/homeassistant/components/dialogflow/.translations/es.json +++ b/homeassistant/components/dialogflow/.translations/es.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Solo una instancia es necesaria." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant, necesitas configurar [Integracion de flujos de dialogo de webhook]({dialogflow_url}).\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nVer [Documentaci\u00f3n]({docs_url}) para mas detalles." + "default": "Para enviar eventos a Home Assistant, necesitas configurar [Integraci\u00f3n de flujos de dialogo de webhook]({dialogflow_url}).\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nVer [Documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { diff --git a/homeassistant/components/ecobee/.translations/ca.json b/homeassistant/components/ecobee/.translations/ca.json new file mode 100644 index 00000000000000..2c4d16b5787cbf --- /dev/null +++ b/homeassistant/components/ecobee/.translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Aquesta integraci\u00f3 nom\u00e9s admet una sola inst\u00e0ncia ecobee." + }, + "error": { + "pin_request_failed": "Error al sol\u00b7licitar els PIN d'ecobee; verifica que la clau API \u00e9s correcta.", + "token_request_failed": "Error al sol\u00b7licitar els testimonis d'autenticaci\u00f3 d'ecobee; torna-ho a provar." + }, + "step": { + "authorize": { + "description": "Autoritza aquesta aplicaci\u00f3 a https://www.ecobee.com/consumerportal/index.html amb el codi pin seg\u00fcent: \n\n {pin} \n \n A continuaci\u00f3, prem Enviar.", + "title": "Autoritzaci\u00f3 de l'aplicaci\u00f3 a ecobee.com" + }, + "user": { + "data": { + "api_key": "Clau API" + }, + "description": "Introdueix la clau API obteinguda a trav\u00e9s del lloc web ecobee.com.", + "title": "Clau API d'ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/es.json b/homeassistant/components/ecobee/.translations/es.json new file mode 100644 index 00000000000000..5544d2e7f7bfdb --- /dev/null +++ b/homeassistant/components/ecobee/.translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Esta integraci\u00f3n actualmente solo admite una instancia de ecobee." + }, + "error": { + "pin_request_failed": "Error al solicitar el PIN de ecobee; verifique que la clave API sea correcta.", + "token_request_failed": "Error al solicitar tokens de ecobee; Int\u00e9ntalo de nuevo." + }, + "step": { + "authorize": { + "description": "Por favor, autorizar esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo pin:\n\n{pin}\n\nA continuaci\u00f3n, pulse Enviar.", + "title": "Autorizar aplicaci\u00f3n en ecobee.com" + }, + "user": { + "data": { + "api_key": "Clave API" + }, + "description": "Introduzca la clave de API obtenida de ecobee.com.", + "title": "Clave API de ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/sl.json b/homeassistant/components/ecobee/.translations/sl.json new file mode 100644 index 00000000000000..d70be59afb5d4d --- /dev/null +++ b/homeassistant/components/ecobee/.translations/sl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "Ta integracija trenutno podpira samo en primerek ecobee." + }, + "error": { + "pin_request_failed": "Napaka pri zahtevi PIN-a od ecobee; preverite, ali je klju\u010d API pravilen.", + "token_request_failed": "Napaka pri zahtevanju \u017eetonov od ecobeeja; prosim poskusite ponovno." + }, + "step": { + "authorize": { + "description": "Prosimo, pooblastite to aplikacijo na https://www.ecobee.com/consumerportal/index.html s kodo PIN:\n\n{pin}\n\nNato pritisnite Po\u0161lji.", + "title": "Pooblasti aplikacijo na ecobee.com" + }, + "user": { + "data": { + "api_key": "API Klju\u010d" + }, + "description": "Prosimo vnesite API klju\u010d, pridobljen iz ecobee.com.", + "title": "ecobee API klju\u010d" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/es.json b/homeassistant/components/izone/.translations/es.json new file mode 100644 index 00000000000000..9f82b1a7b1fd73 --- /dev/null +++ b/homeassistant/components/izone/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se han encontrado dispositivos iZone en la red.", + "single_instance_allowed": "Solo es necesaria una \u00fanica configuraci\u00f3n de iZone." + }, + "step": { + "confirm": { + "description": "\u00bfQuieres configurar iZone?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 4b649744ed977c..33a38fc930e403 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "toggle": "Prze\u0142\u0105cz {entity_name}", - "turn_off": "Wy\u0142\u0105cz {entity_name}", - "turn_on": "W\u0142\u0105cz {entity_name}" + "toggle": "prze\u0142\u0105cz {entity_name}", + "turn_off": "wy\u0142\u0105cz {entity_name}", + "turn_on": "w\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "{entity_name} jest wy\u0142\u0105czony", - "is_on": "{entity_name} jest w\u0142\u0105czony" + "is_off": "\u015bwiat\u0142o {entity_name} jest wy\u0142\u0105czone", + "is_on": "\u015bwiat\u0142o {entity_name} jest w\u0142\u0105czone" }, "trigger_type": { - "turned_off": "Wy\u0142\u0105czenie {entity_name}", - "turned_on": "W\u0142\u0105czenie {entity_name}" + "turned_off": "wy\u0142\u0105czenie {entity_name}", + "turned_on": "w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 4c24dddbe87917..14607868907960 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostra tots els controls", + "use_episode_art": "Utilitza imatges de l'episodi" + }, + "description": "Opcions per als reproductors multim\u00e8dia Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json new file mode 100644 index 00000000000000..6d1ad1f62daa8c --- /dev/null +++ b/homeassistant/components/plex/.translations/es.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "all_configured": "Todos los servidores vinculados ya configurados", + "already_configured": "Este servidor Plex ya est\u00e1 configurado", + "already_in_progress": "Plex se est\u00e1 configurando", + "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "unknown": "Fall\u00f3 por razones desconocidas" + }, + "error": { + "faulty_credentials": "Error en la autorizaci\u00f3n", + "no_servers": "No hay servidores vinculados a la cuenta", + "no_token": "Proporcione un token o seleccione la configuraci\u00f3n manual", + "not_found": "No se ha encontrado el servidor Plex" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Puerto", + "ssl": "Usar SSL", + "token": "Token (es necesario)", + "verify_ssl": "Verificar certificado SSL" + }, + "title": "Servidor Plex" + }, + "select_server": { + "data": { + "server": "Servidor" + }, + "description": "Varios servidores disponibles, seleccione uno:", + "title": "Seleccione el servidor Plex" + }, + "user": { + "data": { + "manual_setup": "Configuraci\u00f3n manual", + "token": "Token Plex" + }, + "description": "Introduzca un token Plex para la configuraci\u00f3n autom\u00e1tica o configure manualmente un servidor.", + "title": "Conectar servidor Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos los controles", + "use_episode_art": "Usar el arte de episodios" + }, + "description": "Opciones para reproductores multimedia Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 2a03b7d0e8cd3d..49ed34baf763b2 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Poka\u017ei vse kontrole", + "use_episode_art": "Uporabi naslovno sliko epizode" + }, + "description": "Mo\u017enosti za predvajalnike Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index 2cf3fa2c1a7e81..5f6d0c41c13506 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -41,5 +41,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "\u986f\u793a\u6240\u6709\u63a7\u5236", + "use_episode_art": "\u4f7f\u7528\u5f71\u96c6\u5287\u7167" + }, + "description": "Plex \u64ad\u653e\u5668\u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 201a77a76a5d09..09b43f4100d660 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -1,19 +1,19 @@ { "device_automation": { "action_type": { - "toggle": "Prze\u0142\u0105cz {entity_name}", - "turn_off": "Wy\u0142\u0105cz {entity_name}", - "turn_on": "W\u0142\u0105cz {entity_name}" + "toggle": "prze\u0142\u0105cz {entity_name}", + "turn_off": "wy\u0142\u0105cz {entity_name}", + "turn_on": "w\u0142\u0105cz {entity_name}" }, "condition_type": { - "is_off": "{entity_name} jest wy\u0142\u0105czony", - "is_on": "{entity_name} jest w\u0142\u0105czony", - "turn_off": "{entity_name} wy\u0142\u0105czony", - "turn_on": "{entity_name} w\u0142\u0105czony" + "is_off": "prze\u0142\u0105cznik {entity_name} jest wy\u0142\u0105czony", + "is_on": "prze\u0142\u0105cznik {entity_name} jest w\u0142\u0105czony", + "turn_off": "prze\u0142\u0105cznik {entity_name} wy\u0142\u0105czony", + "turn_on": "prze\u0142\u0105cznik {entity_name} w\u0142\u0105czony" }, "trigger_type": { - "turned_off": "Wy\u0142\u0105czenie {entity_name}", - "turned_on": "W\u0142\u0105czenie {entity_name}" + "turned_off": "wy\u0142\u0105czenie {entity_name}", + "turned_on": "w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/pt-BR.json b/homeassistant/components/traccar/.translations/pt-BR.json new file mode 100644 index 00000000000000..9fc23b3e394074 --- /dev/null +++ b/homeassistant/components/traccar/.translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ca.json b/homeassistant/components/transmission/.translations/ca.json new file mode 100644 index 00000000000000..395f6e2d6810c9 --- /dev/null +++ b/homeassistant/components/transmission/.translations/ca.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar amb l'amfitri\u00f3", + "wrong_credentials": "Nom d'usuari o contrasenya incorrectes" + }, + "step": { + "options": { + "data": { + "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + }, + "title": "Opcions de configuraci\u00f3" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + }, + "title": "Configuraci\u00f3 del client de Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + }, + "description": "Opcions de configuraci\u00f3 per a Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/es.json b/homeassistant/components/transmission/.translations/es.json new file mode 100644 index 00000000000000..f210eb42f8a613 --- /dev/null +++ b/homeassistant/components/transmission/.translations/es.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "S\u00f3lo se necesita una sola instancia." + }, + "error": { + "cannot_connect": "No se puede conectar al host", + "wrong_credentials": "Nombre de usuario o contrase\u00f1a incorrectos" + }, + "step": { + "options": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + }, + "title": "Configurar opciones" + }, + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + }, + "title": "Configuraci\u00f3n del cliente de transmisi\u00f3n" + } + }, + "title": "Transmisi\u00f3n" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + }, + "description": "Configurar opciones para la transmisi\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pt-BR.json b/homeassistant/components/transmission/.translations/pt-BR.json new file mode 100644 index 00000000000000..a2d3d177e22d9f --- /dev/null +++ b/homeassistant/components/transmission/.translations/pt-BR.json @@ -0,0 +1,37 @@ +{ + "config": { + "error": { + "cannot_connect": "N\u00e3o foi poss\u00edvel conectar ao host", + "wrong_credentials": "Nome de usu\u00e1rio ou senha incorretos" + }, + "step": { + "options": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" + }, + "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o" + }, + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio" + }, + "title": "Configurar o cliente de transmiss\u00e3o" + } + }, + "title": "Transmiss\u00e3o" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" + }, + "description": "Configurar op\u00e7\u00f5es para transmiss\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/sl.json b/homeassistant/components/transmission/.translations/sl.json new file mode 100644 index 00000000000000..122c332f429d84 --- /dev/null +++ b/homeassistant/components/transmission/.translations/sl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "error": { + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem", + "wrong_credentials": "Napa\u010dno uporabni\u0161ko ime ali geslo" + }, + "step": { + "options": { + "data": { + "scan_interval": "Pogostost posodabljanja" + }, + "title": "Nastavite mo\u017enosti" + }, + "user": { + "data": { + "host": "Gostitelj", + "name": "Ime", + "password": "Geslo", + "port": "Vrata", + "username": "Uporabni\u0161ko ime" + }, + "title": "Namestitev odjemalca Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Pogostost posodabljanja" + }, + "description": "Nastavite mo\u017enosti za Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/zh-Hant.json b/homeassistant/components/transmission/.translations/zh-Hant.json new file mode 100644 index 00000000000000..479e25c6d8a124 --- /dev/null +++ b/homeassistant/components/transmission/.translations/zh-Hant.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef", + "wrong_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4" + }, + "step": { + "options": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387" + }, + "title": "\u8a2d\u5b9a\u9078\u9805" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u8a2d\u5b9a Transmission \u5ba2\u6236\u7aef" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387" + }, + "description": "Transmission \u8a2d\u5b9a\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pt-BR.json b/homeassistant/components/unifi/.translations/pt-BR.json index a7eac61bab399d..ea13035e09bec1 100644 --- a/homeassistant/components/unifi/.translations/pt-BR.json +++ b/homeassistant/components/unifi/.translations/pt-BR.json @@ -22,5 +22,15 @@ } }, "title": "Controlador UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "track_clients": "Rastrear clientes da rede", + "track_wired_clients": "Incluir clientes de rede com fio" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/es.json b/homeassistant/components/withings/.translations/es.json index fac325a7097645..ee0cf5235884c2 100644 --- a/homeassistant/components/withings/.translations/es.json +++ b/homeassistant/components/withings/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Debe configurar Withings antes de poder autenticarse con \u00e9l. Por favor, lea la documentaci\u00f3n." + }, "create_entry": { "default": "Autenticado correctamente con Withings para el perfil seleccionado." }, diff --git a/homeassistant/components/zha/.translations/ca.json b/homeassistant/components/zha/.translations/ca.json index 635d0ecbde2f9b..2b8230ad689109 100644 --- a/homeassistant/components/zha/.translations/ca.json +++ b/homeassistant/components/zha/.translations/ca.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Av\u00eds" + }, + "trigger_subtype": { + "both_buttons": "Ambd\u00f3s botons", + "button_1": "Primer bot\u00f3", + "button_2": "Segon bot\u00f3", + "button_3": "Tercer bot\u00f3", + "button_4": "Quart bot\u00f3", + "button_5": "Cinqu\u00e8 bot\u00f3", + "button_6": "Sis\u00e8 bot\u00f3", + "close": "Tanca", + "dim_down": "Atenua la brillantor", + "dim_up": "Augmenta la brillantor", + "face_1": "amb la cara 1 activada", + "face_2": "amb la cara 2 activada", + "face_3": "amb la cara 3 activada", + "face_4": "amb la cara 4 activada", + "face_5": "amb la cara 5 activada", + "face_6": "amb la cara 6 activada", + "face_any": "Amb qualsevol o alguna de les cares especificades activades.", + "left": "Esquerra", + "open": "Obert", + "right": "Dreta", + "turn_off": "Desactiva", + "turn_on": "Activa" + }, + "trigger_type": { + "device_dropped": "Dispositiu caigut", + "device_flipped": "Dispositiu voltejat a \"{subtype}\"", + "device_knocked": "Dispositiu colpejat a \"{subtype}\"", + "device_rotated": "Dispositiu rotat a \"{subtype}\"", + "device_shaken": "Dispositiu sacsejat", + "device_slid": "Dispositiu lliscat a \"{subtype}\"", + "device_tilted": "Dispositiu inclinat", + "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives", + "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament", + "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives", + "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives", + "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", + "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", + "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/es.json b/homeassistant/components/zha/.translations/es.json index 0047c762a9de0d..b8529ce9047376 100644 --- a/homeassistant/components/zha/.translations/es.json +++ b/homeassistant/components/zha/.translations/es.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Advertir" + }, + "trigger_subtype": { + "both_buttons": "Ambos botones", + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "button_5": "Quinto bot\u00f3n", + "button_6": "Sexto bot\u00f3n", + "close": "Cerrar", + "dim_down": "Bajar la intensidad", + "dim_up": "Subir la intensidad", + "face_1": "con la cara 1 activada", + "face_2": "con la cara 2 activada", + "face_3": "con la cara 3 activada", + "face_4": "con la cara 4 activada", + "face_5": "con la cara 5 activada", + "face_6": "con la cara 6 activada", + "face_any": "Con cualquier cara/especificada(s) activada(s)", + "left": "Izquierda", + "open": "Abrir", + "right": "Derecha", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "device_dropped": "Dispositivo ca\u00eddo", + "device_flipped": "Dispositivo volteado \" {subtype} \"", + "device_knocked": "Dispositivo eliminado \" {subtype} \"", + "device_rotated": "Dispositivo girado \" {subtype} \"", + "device_shaken": "Dispositivo agitado", + "device_slid": "Dispositivo deslizado \" {subtype} \"", + "device_tilted": "Dispositivo inclinado", + "remote_button_double_press": "\"{subtipo}\" bot\u00f3n de doble clic", + "remote_button_long_press": "Bot\u00f3n \"{subtipo}\" pulsado continuamente", + "remote_button_long_release": "Bot\u00f3n \"{subtipo}\" liberado despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_quadruple_press": "\"{subtipo}\" bot\u00f3n cu\u00e1druple pulsado", + "remote_button_quintuple_press": "\"{subtipo}\" bot\u00f3n qu\u00edntuple pulsado", + "remote_button_short_press": "Bot\u00f3n \"{subtipo}\" pulsado", + "remote_button_short_release": "Bot\u00f3n \"{subtipo}\" liberado", + "remote_button_triple_press": "\"{subtipo}\" bot\u00f3n de triple clic" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json index 76f1c58fe7ccc8..0e1b7028dbbd14 100644 --- a/homeassistant/components/zha/.translations/pl.json +++ b/homeassistant/components/zha/.translations/pl.json @@ -19,20 +19,20 @@ }, "device_automation": { "action_type": { - "squawk": "Skrzek", - "warn": "Ostrze\u017cenie" + "squawk": "squawk", + "warn": "ostrze\u017cenie" }, "trigger_subtype": { - "both_buttons": "Oba przyciski", - "button_1": "Pierwszy przycisk", - "button_2": "Drugi przycisk", - "button_3": "Trzeci przycisk", - "button_4": "Czwarty przycisk", - "button_5": "Pi\u0105ty przycisk", - "button_6": "Sz\u00f3sty przycisk", - "close": "Zamkni\u0119cie", - "dim_down": "\u015aciemnianie", - "dim_up": "Rozja\u015bnienie", + "both_buttons": "oba przyciski", + "button_1": "pierwszy przycisk", + "button_2": "drugi przycisk", + "button_3": "trzeci przycisk", + "button_4": "czwarty przycisk", + "button_5": "pi\u0105ty przycisk", + "button_6": "sz\u00f3sty przycisk", + "close": "zamkni\u0119cie", + "dim_down": "zmniejszenie jasno\u015bci", + "dim_up": "zwi\u0119kszenie jasno\u015bci", "face_1": "z aktywowan\u0105 twarz\u0105 1", "face_2": "z aktywowan\u0105 twarz\u0105 2", "face_3": "z aktywowan\u0105 twarz\u0105 3", @@ -41,27 +41,27 @@ "face_6": "z aktywowan\u0105 twarz\u0105 6", "face_any": "z dowoln\u0105 twarz\u0105 aktywowan\u0105", "left": "w lewo", - "open": "Otwarcie", + "open": "otwarcie", "right": "w prawo", - "turn_off": "Wy\u0142\u0105czenie", - "turn_on": "W\u0142\u0105czenie" + "turn_off": "wy\u0142\u0105czenie", + "turn_on": "w\u0142\u0105czenie" }, "trigger_type": { - "device_dropped": "Upadek urz\u0105dzenia", - "device_flipped": "Odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", - "device_knocked": "Pukni\u0119cie urz\u0105dzenia \"{subtype}\"", - "device_rotated": "Obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", - "device_shaken": "Potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", - "device_slid": "Przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", - "device_tilted": "Przechylenie urz\u0105dzenia", - "remote_button_double_press": "Przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "Przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "Przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "Przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_short_press": "Przycisk \"{subtype}\" naci\u015bni\u0119ty", - "remote_button_short_release": "Przycisk \"{subtype}\" zwolniony", - "remote_button_triple_press": "Przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty" + "device_dropped": "upadek urz\u0105dzenia", + "device_flipped": "odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_knocked": "pukni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_rotated": "obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_shaken": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", + "device_slid": "przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_tilted": "przechylenie urz\u0105dzenia", + "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pt-BR.json b/homeassistant/components/zha/.translations/pt-BR.json index 8606a04e197985..0bc3afe28eca5d 100644 --- a/homeassistant/components/zha/.translations/pt-BR.json +++ b/homeassistant/components/zha/.translations/pt-BR.json @@ -16,5 +16,10 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "warn": "Aviso" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/sl.json b/homeassistant/components/zha/.translations/sl.json index 30df6716f97fda..226bd37200efb6 100644 --- a/homeassistant/components/zha/.translations/sl.json +++ b/homeassistant/components/zha/.translations/sl.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Opozori" + }, + "trigger_subtype": { + "both_buttons": "Oba gumba", + "button_1": "Prvi gumb", + "button_2": "Drugi gumb", + "button_3": "Tretji gumb", + "button_4": "\u010cetrti gumb", + "button_5": "Peti gumb", + "button_6": "\u0160esti gumb", + "close": "Zapri", + "dim_down": "Zatemnite", + "dim_up": "pove\u010dajte mo\u010d", + "face_1": "aktivirano z obrazom 1", + "face_2": "aktivirano z obrazom 2", + "face_3": "aktivirano z obrazom 3", + "face_4": "aktivirano z obrazom 4", + "face_5": "aktivirano z obrazom 5", + "face_6": "aktivirano z obrazom 6", + "face_any": "Z vsemi/dolo\u010denimi obrazi vklju\u010deno", + "left": "Levo", + "open": "Odprto", + "right": "Desno", + "turn_off": "Ugasni", + "turn_on": "Pri\u017egi" + }, + "trigger_type": { + "device_dropped": "Naprava padla", + "device_flipped": "Naprava obrnjena \"{subtype}\"", + "device_knocked": "Naprava prevrnjena \"{subtype}\"", + "device_rotated": "Naprava je zasukana \"{subtype}\"", + "device_shaken": "Naprava se je pretresla", + "device_slid": "Naprava zdrsnila \"{subtype}\"", + "device_tilted": "Naprava je nagnjena", + "remote_button_double_press": "Dvakrat kliknete gumb \"{subtype}\"", + "remote_button_long_press": "\"{subtype}\" gumb neprekinjeno pritisnjen", + "remote_button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku", + "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", + "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", + "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", + "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", + "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/zh-Hant.json b/homeassistant/components/zha/.translations/zh-Hant.json index bbfb3fe7128f27..d7f421c7e84498 100644 --- a/homeassistant/components/zha/.translations/zh-Hant.json +++ b/homeassistant/components/zha/.translations/zh-Hant.json @@ -18,6 +18,10 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "\u61c9\u7b54", + "warn": "\u8b66\u544a" + }, "trigger_subtype": { "both_buttons": "\u5169\u500b\u6309\u9215", "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", From 5bd3d4aa0bf7829114d8932beeae7587583c975b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 29 Sep 2019 20:33:42 -0400 Subject: [PATCH 0514/3953] Bump zha quirks to 0.0.26 (#27051) --- 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 c4de1d66e838b7..f0f6389a061e13 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.10.0", - "zha-quirks==0.0.25", + "zha-quirks==0.0.26", "zigpy-deconz==0.4.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", diff --git a/requirements_all.txt b/requirements_all.txt index 79313b48e61ed2..dadf60e166bdef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2014,7 +2014,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.25 +zha-quirks==0.0.26 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From 245e51df7a1e30dc602bff805417bbbe094abf09 Mon Sep 17 00:00:00 2001 From: John Luetke Date: Sun, 29 Sep 2019 17:35:56 -0700 Subject: [PATCH 0515/3953] Add Pi-hole enable and disable services (#27055) * Add service to disable pihole * Add service to enable pihole * Redefine optional string validator * code review changes * Change service parameter to timedelta * code review changes --- homeassistant/components/pi_hole/__init__.py | 44 ++++++++++++++++++- homeassistant/components/pi_hole/const.py | 4 ++ .../components/pi_hole/services.yaml | 8 ++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/pi_hole/services.yaml diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index ffc9827eed41fe..00bb0e2d673486 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -5,7 +5,13 @@ from hole import Hole from hole.exceptions import HoleError -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_API_KEY, + CONF_SSL, + CONF_VERIFY_SSL, +) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -21,6 +27,9 @@ DEFAULT_SSL, DEFAULT_VERIFY_SSL, MIN_TIME_BETWEEN_UPDATES, + SERVICE_DISABLE, + SERVICE_DISABLE_ATTR_DURATION, + SERVICE_ENABLE, ) LOGGER = logging.getLogger(__name__) @@ -31,6 +40,7 @@ { vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, @@ -40,6 +50,14 @@ extra=vol.ALLOW_EXTRA, ) +SERVICE_DISABLE_SCHEMA = vol.Schema( + { + vol.Required(SERVICE_DISABLE_ATTR_DURATION): vol.All( + cv.time_period_str, cv.positive_timedelta + ) + } +) + async def async_setup(hass, config): """Set up the pi_hole integration.""" @@ -50,6 +68,7 @@ async def async_setup(hass, config): use_tls = conf[CONF_SSL] verify_tls = conf[CONF_VERIFY_SSL] location = conf[CONF_LOCATION] + api_key = conf.get(CONF_API_KEY) LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) @@ -62,6 +81,7 @@ async def async_setup(hass, config): location=location, tls=use_tls, verify_tls=verify_tls, + api_token=api_key, ), name, ) @@ -70,6 +90,28 @@ async def async_setup(hass, config): hass.data[DOMAIN] = pi_hole + async def handle_disable(call): + if api_key is None: + raise vol.Invalid("Pi-hole api_key must be provided in configuration") + + duration = call.data[SERVICE_DISABLE_ATTR_DURATION].total_seconds() + + LOGGER.debug("Disabling %s %s for %d seconds", DOMAIN, host, duration) + await pi_hole.api.disable(duration) + + async def handle_enable(call): + if api_key is None: + raise vol.Invalid("Pi-hole api_key must be provided in configuration") + + LOGGER.debug("Enabling %s %s", DOMAIN, host) + await pi_hole.api.enable() + + hass.services.async_register( + DOMAIN, SERVICE_DISABLE, handle_disable, schema=SERVICE_DISABLE_SCHEMA + ) + + hass.services.async_register(DOMAIN, SERVICE_ENABLE, handle_enable) + hass.async_create_task(async_load_platform(hass, SENSOR_DOMAIN, DOMAIN, {}, config)) return True diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index ba83bf1d805334..5422054795097e 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -12,6 +12,10 @@ DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True +SERVICE_DISABLE = "disable" +SERVICE_ENABLE = "enable" +SERVICE_DISABLE_ATTR_DURATION = "duration" + ATTR_BLOCKED_DOMAINS = "domains_blocked" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) diff --git a/homeassistant/components/pi_hole/services.yaml b/homeassistant/components/pi_hole/services.yaml new file mode 100644 index 00000000000000..b16ed21a5d3c2b --- /dev/null +++ b/homeassistant/components/pi_hole/services.yaml @@ -0,0 +1,8 @@ +disable: + description: Disable Pi-hole for an amount of time + fields: + duration: + description: Time that the Pi-hole should be disabled for + example: "00:00:15" +enable: + description: Enable Pi-hole \ No newline at end of file From 43bd1168521c9296658f9df480161c0966b87fe9 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Mon, 30 Sep 2019 02:56:02 -0400 Subject: [PATCH 0516/3953] add utc tz to forecast (#27049) --- homeassistant/components/darksky/weather.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index e95381cdf73c61..5296f34662618e 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -1,5 +1,5 @@ """Support for retrieving meteorological data from Dark Sky.""" -from datetime import datetime, timedelta +from datetime import timedelta import logging from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout @@ -29,6 +29,7 @@ ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +from homeassistant.util.dt import utc_from_timestamp from homeassistant.util.pressure import convert as convert_pressure _LOGGER = logging.getLogger(__name__) @@ -178,7 +179,7 @@ def calc_precipitation(intensity, hours): if self._mode == "daily": data = [ { - ATTR_FORECAST_TIME: datetime.fromtimestamp( + ATTR_FORECAST_TIME: utc_from_timestamp( entry.d.get("time") ).isoformat(), ATTR_FORECAST_TEMP: entry.d.get("temperatureHigh"), @@ -195,7 +196,7 @@ def calc_precipitation(intensity, hours): else: data = [ { - ATTR_FORECAST_TIME: datetime.fromtimestamp( + ATTR_FORECAST_TIME: utc_from_timestamp( entry.d.get("time") ).isoformat(), ATTR_FORECAST_TEMP: entry.d.get("temperature"), From c527e0f16440b9592dbf6c7af4f2a8623f83d8fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20K=C3=BCgler?= Date: Mon, 30 Sep 2019 09:06:10 +0200 Subject: [PATCH 0517/3953] Fix rest_command when server is unreachable (#26948) * fix rest_command when server is unreachable When a server doesn't exist, the connection fails immediately, rather than waiting for a timeout. This means that the async handler is never reached, and the request variable never filled, yet it's used in the client error exception handler, so this one bugs out. By using the command_config, we avoid using the potentially unassigned request variable, avoiding this problem. This patch makes scripts work that have a rest_command in them which fails due to a server being offline. * render template_url instead of printing the template object * fix formatting * fix format using black * only render url once * blacken... --- homeassistant/components/rest_command/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 038787ac8d4579..1607000e8d985a 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -94,13 +94,11 @@ async def async_service_handler(service): template_payload.async_render(variables=service.data), "utf-8" ) + request_url = template_url.async_render(variables=service.data) try: with async_timeout.timeout(timeout): request = await getattr(websession, method)( - template_url.async_render(variables=service.data), - data=payload, - auth=auth, - headers=headers, + request_url, data=payload, auth=auth, headers=headers ) if request.status < 400: @@ -112,7 +110,7 @@ async def async_service_handler(service): _LOGGER.warning("Timeout call %s.", request.url) except aiohttp.ClientError: - _LOGGER.error("Client error %s.", request.url) + _LOGGER.error("Client error %s.", request_url) # register services hass.services.async_register(DOMAIN, name, async_service_handler) From fa92d0e6d8a8539975012b9e7c2010fd0251011d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 30 Sep 2019 09:31:35 +0100 Subject: [PATCH 0518/3953] Fix incomfort and Bump client to 0.3.5 (#26802) * remove superfluous device state attributes * fix water_heater icon * add type hints * fix issue #26760 * bump client to v0.3.5 * add unique_id --- .../components/incomfort/binary_sensor.py | 32 ++++--- homeassistant/components/incomfort/climate.py | 24 +++-- .../components/incomfort/manifest.json | 2 +- homeassistant/components/incomfort/sensor.py | 89 +++++++++++-------- .../components/incomfort/water_heater.py | 72 +++++++-------- requirements_all.txt | 2 +- 6 files changed, 124 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 004086ab5c825c..39a45429cb1d15 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -1,4 +1,6 @@ -"""Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" +"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" +from typing import Any, Dict, Optional + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -8,6 +10,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/InTouch binary_sensor device.""" + if discovery_info is None: + return + async_add_entities( [IncomfortFailed(hass.data[DOMAIN]["client"], hass.data[DOMAIN]["heater"])] ) @@ -16,33 +21,40 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class IncomfortFailed(BinarySensorDevice): """Representation of an InComfort Failed sensor.""" - def __init__(self, client, boiler): + def __init__(self, client, heater) -> None: """Initialize the binary sensor.""" + self._unique_id = f"{heater.serial_no}_failed" + self._client = client - self._boiler = boiler + self._heater = heater - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @callback - def _refresh(self): + def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) @property - def name(self): + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> Optional[str]: """Return the name of the sensor.""" return "Fault state" @property - def is_on(self): + def is_on(self) -> bool: """Return the status of the sensor.""" - return self._boiler.status["is_failed"] + return self._heater.status["is_failed"] @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" - return {"fault_code": self._boiler.status["fault_code"]} + return {"fault_code": self._heater.status["fault_code"]} @property def should_poll(self) -> bool: diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index cccb9d256444dc..3918244d4e856d 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -1,5 +1,5 @@ """Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" -from typing import Any, Dict, Optional, List +from typing import Any, Dict, List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -13,21 +13,24 @@ from . import DOMAIN -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/InTouch climate device.""" + if discovery_info is None: + return + client = hass.data[DOMAIN]["client"] heater = hass.data[DOMAIN]["heater"] - async_add_entities([InComfortClimate(client, r) for r in heater.rooms]) + async_add_entities([InComfortClimate(client, heater, r) for r in heater.rooms]) class InComfortClimate(ClimateDevice): """Representation of an InComfort/InTouch climate device.""" - def __init__(self, client, room): + def __init__(self, client, heater, room) -> None: """Initialize the climate device.""" + self._unique_id = f"{heater.serial_no}_{room.room_no}" + self._client = client self._room = room self._name = f"Room {room.room_no}" @@ -37,7 +40,7 @@ async def async_added_to_hass(self) -> None: async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @callback - def _refresh(self): + def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) @property @@ -45,6 +48,11 @@ def should_poll(self) -> bool: """Return False as this device should never be polled.""" return False + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + @property def name(self) -> str: """Return the name of the climate device.""" @@ -78,7 +86,7 @@ def current_temperature(self) -> Optional[float]: @property def target_temperature(self) -> Optional[float]: """Return the temperature we try to reach.""" - return self._room.override + return self._room.setpoint @property def supported_features(self) -> int: diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 8b5f461c8afb08..c26ba27a29ae42 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.1" + "incomfort-client==0.3.5" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index e19bbf42aeacbc..772b5dab1834a1 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -1,31 +1,43 @@ -"""Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" -from homeassistant.const import PRESSURE_BAR, TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE +"""Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" +from typing import Any, Dict, Optional + +from homeassistant.const import ( + PRESSURE_BAR, + TEMP_CELSIUS, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify from . import DOMAIN -INTOUCH_HEATER_TEMP = "CV Temp" -INTOUCH_PRESSURE = "CV Pressure" -INTOUCH_TAP_TEMP = "Tap Temp" +INCOMFORT_HEATER_TEMP = "CV Temp" +INCOMFORT_PRESSURE = "CV Pressure" +INCOMFORT_TAP_TEMP = "Tap Temp" -INTOUCH_MAP_ATTRS = { - INTOUCH_HEATER_TEMP: ["heater_temp", "is_pumping"], - INTOUCH_TAP_TEMP: ["tap_temp", "is_tapping"], +INCOMFORT_MAP_ATTRS = { + INCOMFORT_HEATER_TEMP: ["heater_temp", "is_pumping"], + INCOMFORT_PRESSURE: ["pressure", None], + INCOMFORT_TAP_TEMP: ["tap_temp", "is_tapping"], } async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/InTouch sensor device.""" + if discovery_info is None: + return + client = hass.data[DOMAIN]["client"] heater = hass.data[DOMAIN]["heater"] async_add_entities( [ - IncomfortPressure(client, heater, INTOUCH_PRESSURE), - IncomfortTemperature(client, heater, INTOUCH_HEATER_TEMP), - IncomfortTemperature(client, heater, INTOUCH_TAP_TEMP), + IncomfortPressure(client, heater, INCOMFORT_PRESSURE), + IncomfortTemperature(client, heater, INCOMFORT_HEATER_TEMP), + IncomfortTemperature(client, heater, INCOMFORT_TAP_TEMP), ] ) @@ -33,35 +45,47 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class IncomfortSensor(Entity): """Representation of an InComfort/InTouch sensor device.""" - def __init__(self, client, boiler): + def __init__(self, client, heater, name) -> None: """Initialize the sensor.""" self._client = client - self._boiler = boiler + self._heater = heater + + self._unique_id = f"{heater.serial_no}_{slugify(name)}" - self._name = None + self._name = name self._device_class = None self._unit_of_measurement = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @callback - def _refresh(self): + def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) @property - def name(self): + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> Optional[str]: """Return the name of the sensor.""" return self._name @property - def device_class(self): + def state(self) -> Optional[str]: + """Return the state of the sensor.""" + return self._heater.status[INCOMFORT_MAP_ATTRS[self._name][0]] + + @property + def device_class(self) -> Optional[str]: """Return the device class of the sensor.""" return self._device_class @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> Optional[str]: """Return the unit of measurement of the sensor.""" return self._unit_of_measurement @@ -74,37 +98,26 @@ def should_poll(self) -> bool: class IncomfortPressure(IncomfortSensor): """Representation of an InTouch CV Pressure sensor.""" - def __init__(self, client, boiler, name): + def __init__(self, client, heater, name) -> None: """Initialize the sensor.""" - super().__init__(client, boiler) + super().__init__(client, heater, name) - self._name = name + self._device_class = DEVICE_CLASS_PRESSURE self._unit_of_measurement = PRESSURE_BAR - @property - def state(self): - """Return the state/value of the sensor.""" - return self._boiler.status["pressure"] - class IncomfortTemperature(IncomfortSensor): """Representation of an InTouch Temperature sensor.""" - def __init__(self, client, boiler, name): + def __init__(self, client, heater, name) -> None: """Initialize the signal strength sensor.""" - super().__init__(client, boiler) + super().__init__(client, heater, name) - self._name = name self._device_class = DEVICE_CLASS_TEMPERATURE self._unit_of_measurement = TEMP_CELSIUS @property - def state(self): - """Return the state of the sensor.""" - return self._boiler.status[INTOUCH_MAP_ATTRS[self._name][0]] - - @property - def device_state_attributes(self): + def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" - key = INTOUCH_MAP_ATTRS[self._name][1] - return {key: self._boiler.status[key]} + key = INCOMFORT_MAP_ATTRS[self._name][1] + return {key: self._heater.status[key]} diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 2449a1223ccc5f..70423611705d20 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -1,6 +1,7 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" import asyncio import logging +from typing import Any, Dict, Optional from aiohttp import ClientResponseError from homeassistant.components.water_heater import WaterHeaterDevice @@ -11,60 +12,52 @@ _LOGGER = logging.getLogger(__name__) -HEATER_SUPPORT_FLAGS = 0 +HEATER_ATTRS = ["display_code", "display_text", "is_burning"] -HEATER_MAX_TEMP = 80.0 -HEATER_MIN_TEMP = 30.0 -HEATER_NAME = "Boiler" -HEATER_ATTRS = [ - "display_code", - "display_text", - "is_burning", - "rf_message_rssi", - "nodenr", - "rfstatus_cntr", -] - - -async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up an InComfort/Intouch water_heater device.""" + if discovery_info is None: + return + client = hass.data[DOMAIN]["client"] heater = hass.data[DOMAIN]["heater"] - async_add_entities([IncomfortWaterHeater(client, heater)], update_before_add=True) + async_add_entities([IncomfortWaterHeater(client, heater)]) class IncomfortWaterHeater(WaterHeaterDevice): """Representation of an InComfort/Intouch water_heater device.""" - def __init__(self, client, heater): + def __init__(self, client, heater) -> None: """Initialize the water_heater device.""" + self._unique_id = f"{heater.serial_no}" + self._client = client self._heater = heater @property - def name(self): + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: """Return the name of the water_heater device.""" - return HEATER_NAME + return "Boiler" @property - def icon(self): + def icon(self) -> str: """Return the icon of the water_heater device.""" - return "mdi:oil-temperature" + return "mdi:thermometer-lines" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" - state = { - k: self._heater.status[k] for k in self._heater.status if k in HEATER_ATTRS - } - return state + return {k: v for k, v in self._heater.status.items() if k in HEATER_ATTRS} @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" if self._heater.is_tapping: return self._heater.tap_temp @@ -73,34 +66,34 @@ def current_temperature(self): return max(self._heater.heater_temp, self._heater.tap_temp) @property - def min_temp(self): + def min_temp(self) -> float: """Return max valid temperature that can be set.""" - return HEATER_MIN_TEMP + return 80.0 @property - def max_temp(self): + def max_temp(self) -> float: """Return max valid temperature that can be set.""" - return HEATER_MAX_TEMP + return 30.0 @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 HEATER_SUPPORT_FLAGS + return 0 @property - def current_operation(self): + def current_operation(self) -> str: """Return the current operation mode.""" if self._heater.is_failed: return f"Fault code: {self._heater.fault_code}" return self._heater.display_text - async def async_update(self): + async def async_update(self) -> None: """Get the latest state data from the gateway.""" try: await self._heater.update() @@ -108,4 +101,5 @@ async def async_update(self): except (ClientResponseError, asyncio.TimeoutError) as err: _LOGGER.warning("Update failed, message is: %s", err) - async_dispatcher_send(self.hass, DOMAIN) + else: + async_dispatcher_send(self.hass, DOMAIN) diff --git a/requirements_all.txt b/requirements_all.txt index dadf60e166bdef..c25ca6a54fe400 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -685,7 +685,7 @@ iglo==1.2.7 ihcsdk==2.3.0 # homeassistant.components.incomfort -incomfort-client==0.3.1 +incomfort-client==0.3.5 # homeassistant.components.influxdb influxdb==5.2.3 From 21453df73eca24f62ed449ce74a29756aaaa1cf0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 30 Sep 2019 11:01:08 +0200 Subject: [PATCH 0519/3953] Update devcontainer.json --- .devcontainer/devcontainer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e78a8e6851c728..afb273331aada5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,6 +8,7 @@ "runArgs": ["-e", "GIT_EDITOR=\"code --wait\""], "extensions": [ "ms-python.python", + "visualstudioexptteam.vscodeintellicode", "ms-azure-devops.azure-pipelines", "redhat.vscode-yaml", "esbenp.prettier-vscode" From 48d07467d913367c85338d6883dbc538cb359e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiit=20R=C3=A4tsep?= Date: Mon, 30 Sep 2019 15:23:08 +0300 Subject: [PATCH 0520/3953] Add support for SOMA Smartshades devices (#26226) * Add Soma integration * Fixed cover position get/set * Try to list devices before creating config entries to see if Soma Connect can be polled * Style fixes * Updated requirements * Updated .coveragerc to ignore Soma component * Fixed linter errors * Implemented stop command * Test coverage fixes according to feedback * Fixes to code according to feedback * Added error logging and tested config from yaml * Indentation fix * Removed unnecessary method * Wrong indentation * Added some tests * Added test for import step leading to entry creation * Added feedback to user form in case of connection error * Minor fixes according to feedback * Changed exception type in error handling for connection to Connect * To keep API consistent for Google Home and Alexa we swapped the open/closed position values back and I reversed them in this integration as well * regenerated requirements, ran black, addde __init__.py to ignore file * Added pysoma library to gen_requirements_all.py * Added missing test case * removed useless return value --- .coveragerc | 2 + CODEOWNERS | 1 + .../components/soma/.translations/en.json | 24 ++++ homeassistant/components/soma/__init__.py | 111 ++++++++++++++++++ homeassistant/components/soma/config_flow.py | 56 +++++++++ homeassistant/components/soma/const.py | 6 + homeassistant/components/soma/cover.py | 79 +++++++++++++ homeassistant/components/soma/manifest.json | 13 ++ homeassistant/components/soma/strings.json | 13 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/soma/__init__.py | 1 + tests/components/soma/test_config_flow.py | 60 ++++++++++ 15 files changed, 374 insertions(+) create mode 100644 homeassistant/components/soma/.translations/en.json create mode 100644 homeassistant/components/soma/__init__.py create mode 100644 homeassistant/components/soma/config_flow.py create mode 100644 homeassistant/components/soma/const.py create mode 100644 homeassistant/components/soma/cover.py create mode 100644 homeassistant/components/soma/manifest.json create mode 100644 homeassistant/components/soma/strings.json create mode 100644 tests/components/soma/__init__.py create mode 100644 tests/components/soma/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index d42d7cbb3b3250..f28e9aaeda67e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -599,6 +599,8 @@ omit = homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py homeassistant/components/solax/sensor.py + homeassistant/components/soma/cover.py + homeassistant/components/soma/__init__.py homeassistant/components/somfy/* homeassistant/components/somfy_mylink/* homeassistant/components/sonarr/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 4a6dfdbf6e62ab..db0ff3226c38ba 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -254,6 +254,7 @@ homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/smtp/* @fabaff homeassistant/components/solaredge_local/* @drobtravels @scheric homeassistant/components/solax/* @squishykid +homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne homeassistant/components/songpal/* @rytilahti homeassistant/components/spaceapi/* @fabaff diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json new file mode 100644 index 00000000000000..738d0fd642268f --- /dev/null +++ b/homeassistant/components/soma/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Soma Connect.", + "missing_configuration": "The Soma component is not configured. Please follow the documentation.", + "connection_error": "Connection to the specified device failed." + }, + "create_entry": { + "default": "Successfully authenticated with Soma." + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "title": "Set up Soma Connect" + } + }, + "title": "Soma" + } +} diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py new file mode 100644 index 00000000000000..5bf51e743e9c1a --- /dev/null +++ b/homeassistant/components/soma/__init__.py @@ -0,0 +1,111 @@ +"""Support for Soma Smartshades.""" +import logging + +import voluptuous as vol +from api.soma_api import SomaApi + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DOMAIN, HOST, PORT, API + + +DEVICES = "devices" + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.string} + ) + }, + extra=vol.ALLOW_EXTRA, +) + +SOMA_COMPONENTS = ["cover"] + + +async def async_setup(hass, config): + """Set up the Soma component.""" + if DOMAIN not in config: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + data=config[DOMAIN], + context={"source": config_entries.SOURCE_IMPORT}, + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up Soma from a config entry.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][API] = SomaApi(entry.data[HOST], entry.data[PORT]) + devices = await hass.async_add_executor_job(hass.data[DOMAIN][API].list_devices) + hass.data[DOMAIN][DEVICES] = devices["shades"] + + for component in SOMA_COMPONENTS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload a config entry.""" + return True + + +class SomaEntity(Entity): + """Representation of a generic Soma device.""" + + def __init__(self, device, api): + """Initialize the Soma device.""" + self.device = device + self.api = api + self.current_position = 50 + + @property + def unique_id(self): + """Return the unique id base on the id returned by pysoma API.""" + return self.device["mac"] + + @property + def name(self): + """Return the name of the device.""" + return self.device["name"] + + @property + def device_info(self): + """Return device specific attributes. + + Implemented by platform classes. + """ + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Wazombi Labs", + } + + async def async_update(self): + """Update the device with the latest data.""" + response = await self.hass.async_add_executor_job( + self.api.get_shade_state, self.device["mac"] + ) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + return + self.current_position = 100 - response["position"] diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py new file mode 100644 index 00000000000000..e2f89273520da4 --- /dev/null +++ b/homeassistant/components/soma/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow for Soma.""" +import logging + +import voluptuous as vol +from api.soma_api import SomaApi +from requests import RequestException + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_PORT = 3000 + + +class SomaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Instantiate config flow.""" + + async def async_step_user(self, user_input=None): + """Handle a flow start.""" + if user_input is None: + data = { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } + + return self.async_show_form(step_id="user", data_schema=vol.Schema(data)) + + return await self.async_step_creation(user_input) + + async def async_step_creation(self, user_input=None): + """Finish config flow.""" + api = SomaApi(user_input["host"], user_input["port"]) + try: + await self.hass.async_add_executor_job(api.list_devices) + _LOGGER.info("Successfully set up Soma Connect") + return self.async_create_entry( + title="Soma Connect", + data={"host": user_input["host"], "port": user_input["port"]}, + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + return self.async_abort(reason="connection_error") + + async def async_step_import(self, user_input=None): + """Handle flow start from existing config section.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_setup") + return await self.async_step_creation(user_input) diff --git a/homeassistant/components/soma/const.py b/homeassistant/components/soma/const.py new file mode 100644 index 00000000000000..815a0176e7e6fe --- /dev/null +++ b/homeassistant/components/soma/const.py @@ -0,0 +1,6 @@ +"""Define constants for the Soma component.""" + +DOMAIN = "soma" +HOST = "host" +PORT = "port" +API = "api" diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py new file mode 100644 index 00000000000000..1577b7f2911f03 --- /dev/null +++ b/homeassistant/components/soma/cover.py @@ -0,0 +1,79 @@ +"""Support for Soma Covers.""" + +import logging + +from homeassistant.components.cover import CoverDevice, ATTR_POSITION +from homeassistant.components.soma import DOMAIN, SomaEntity, DEVICES, API + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Soma cover platform.""" + + devices = hass.data[DOMAIN][DEVICES] + + async_add_entities( + [SomaCover(cover, hass.data[DOMAIN][API]) for cover in devices], True + ) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up platform. + + Can only be called when a user accidentally mentions the platform in their + config. But even in that case it would have been ignored. + """ + pass + + +class SomaCover(SomaEntity, CoverDevice): + """Representation of a Soma cover device.""" + + def close_cover(self, **kwargs): + """Close the cover.""" + response = self.api.set_shade_position(self.device["mac"], 100) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + def open_cover(self, **kwargs): + """Open the cover.""" + response = self.api.set_shade_position(self.device["mac"], 0) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + def stop_cover(self, **kwargs): + """Stop the cover.""" + # Set cover position to some value where up/down are both enabled + self.current_position = 50 + response = self.api.stop_shade(self.device["mac"]) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + def set_cover_position(self, **kwargs): + """Move the cover shutter to a specific position.""" + self.current_position = kwargs[ATTR_POSITION] + response = self.api.set_shade_position( + self.device["mac"], 100 - kwargs[ATTR_POSITION] + ) + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + + @property + def current_cover_position(self): + """Return the current position of cover shutter.""" + return self.current_position + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self.current_position == 0 diff --git a/homeassistant/components/soma/manifest.json b/homeassistant/components/soma/manifest.json new file mode 100644 index 00000000000000..35a77c063b89e6 --- /dev/null +++ b/homeassistant/components/soma/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "soma", + "name": "Soma Open API", + "config_flow": true, + "documentation": "", + "dependencies": [], + "codeowners": [ + "@ratsept" + ], + "requirements": [ + "pysoma==0.0.10" + ] +} diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json new file mode 100644 index 00000000000000..eac817ce1199dc --- /dev/null +++ b/homeassistant/components/soma/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Soma account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Soma component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Soma." + }, + "title": "Soma" + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ab7b339e58284a..21f57934e95bd6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -55,6 +55,7 @@ "smartthings", "smhi", "solaredge", + "soma", "somfy", "sonos", "tellduslive", diff --git a/requirements_all.txt b/requirements_all.txt index c25ca6a54fe400..5482af01cc2b09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1443,6 +1443,9 @@ pysmarty==0.8 # homeassistant.components.snmp pysnmp==4.4.11 +# homeassistant.components.soma +pysoma==0.0.10 + # homeassistant.components.sonos pysonos==0.0.23 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2701513a6de7b9..801c09f322d910 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,6 +349,9 @@ pysmartapp==0.3.2 # homeassistant.components.smartthings pysmartthings==0.6.9 +# homeassistant.components.soma +pysoma==0.0.10 + # homeassistant.components.sonos pysonos==0.0.23 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1e484e0dfc4853..9991a6bc1f0b60 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -143,6 +143,7 @@ "pysma", "pysmartapp", "pysmartthings", + "pysoma", "pysonos", "pyspcwebgw", "python_awair", diff --git a/tests/components/soma/__init__.py b/tests/components/soma/__init__.py new file mode 100644 index 00000000000000..8d84668e5ea9b9 --- /dev/null +++ b/tests/components/soma/__init__.py @@ -0,0 +1 @@ +"""Tests for the Soma component.""" diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py new file mode 100644 index 00000000000000..764a18d1b8b98a --- /dev/null +++ b/tests/components/soma/test_config_flow.py @@ -0,0 +1,60 @@ +"""Tests for the Soma config flow.""" +from unittest.mock import patch + +from api.soma_api import SomaApi +from requests import RequestException + +from homeassistant import data_entry_flow +from homeassistant.components.soma import config_flow, DOMAIN +from tests.common import MockConfigEntry + + +MOCK_HOST = "123.45.67.89" +MOCK_PORT = 3000 + + +async def test_form(hass): + """Test user form showing.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_import_abort(hass): + """Test configuration from YAML aborting with existing entity.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + result = await flow.async_step_import() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + +async def test_import_create(hass): + """Test configuration from YAML.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + with patch.object(SomaApi, "list_devices", return_value={}): + result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_exception(hass): + """Test if RequestException fires when no connection can be made.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + with patch.object(SomaApi, "list_devices", side_effect=RequestException()): + result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_error" + + +async def test_full_flow(hass): + """Check classic use case.""" + hass.data[DOMAIN] = {} + flow = config_flow.SomaFlowHandler() + flow.hass = hass + with patch.object(SomaApi, "list_devices", return_value={}): + result = await flow.async_step_user({"host": MOCK_HOST, "port": MOCK_PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY From d116d2c1a47335941d1dce626a3f58e2550523be Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 30 Sep 2019 14:49:08 +0200 Subject: [PATCH 0521/3953] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 51c5cdb936d2f9..60eff866676462 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -245,24 +245,33 @@ stages: - template: templates/azp-step-ha-version.yaml@azure - script: | set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 + curl -o google-cloud-sdk.tar.gz https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz tar -C . -xvf google-cloud-sdk.tar.gz rm -f google-cloud-sdk.tar.gz ./google-cloud-sdk/install.sh displayName: 'Setup gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true')) + condition: eq(variables['homeassistantReleaseStable'], 'true') - script: | set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - echo "$(gcloudAuth)" > gcloud_auth.json + + echo "$(gcloudAnalytic)" > gcloud_auth.json ./google-cloud-sdk/bin/gcloud auth activate-service-account --key-file gcloud_auth.json rm -f gcloud_auth.json displayName: 'Auth gCloud' - condition: eq(variables['homeassistantReleaseStable'], 'true')) + condition: eq(variables['homeassistantReleaseStable'], 'true') - script: | set -e + export CLOUDSDK_CORE_DISABLE_PROMPTS=1 - ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver --update-env-vars VERSION=$(homeassistantRelease) + + ./google-cloud-sdk/bin/gcloud functions deploy Analytics-Receiver \ + --project home-assistant-analytics \ + --update-env-vars VERSION=$(homeassistantRelease) \ + --source gs://analytics-src/function-source.zip displayName: 'Push details to updater' - condition: eq(variables['homeassistantReleaseStable'], 'true')) + condition: eq(variables['homeassistantReleaseStable'], 'true') From d28980b0974bebfe5284adfc82189b8bed91d26f Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Mon, 30 Sep 2019 12:56:58 -0400 Subject: [PATCH 0522/3953] Bump pyecobee to 0.1.4 (#27074) --- homeassistant/components/ecobee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 131c35d7f89137..148e355a3d9e9a 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/ecobee", "dependencies": [], - "requirements": ["python-ecobee-api==0.1.3"], + "requirements": ["python-ecobee-api==0.1.4"], "codeowners": ["@marthoc"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5482af01cc2b09..f6f78bab918299 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1489,7 +1489,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.1.3 +python-ecobee-api==0.1.4 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 801c09f322d910..faf775ac5b844a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -359,7 +359,7 @@ pysonos==0.0.23 pyspcwebgw==0.4.0 # homeassistant.components.ecobee -python-ecobee-api==0.1.3 +python-ecobee-api==0.1.4 # homeassistant.components.darksky python-forecastio==1.4.0 From 8c01ed8a1ff92f9b00018b870025de67307d18ed Mon Sep 17 00:00:00 2001 From: John Luetke Date: Mon, 30 Sep 2019 11:26:26 -0700 Subject: [PATCH 0523/3953] Fix SSL connections to Pi-hole (#27073) --- homeassistant/components/pi_hole/__init__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 00bb0e2d673486..95351083b5abfe 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -72,16 +72,10 @@ async def async_setup(hass, config): LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) - session = async_get_clientsession(hass, True) + session = async_get_clientsession(hass, verify_tls) pi_hole = PiHoleData( Hole( - host, - hass.loop, - session, - location=location, - tls=use_tls, - verify_tls=verify_tls, - api_token=api_key, + host, hass.loop, session, location=location, tls=use_tls, api_token=api_key ), name, ) From 9615ba3d99490e199a8f691a1be02626c04c5e25 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 30 Sep 2019 23:46:59 +0200 Subject: [PATCH 0524/3953] Bump shodan to 1.19.0 (#27079) --- homeassistant/components/shodan/manifest.json | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index be7f0a524dcd0b..fa704a6550a491 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -3,10 +3,10 @@ "name": "Shodan", "documentation": "https://www.home-assistant.io/components/shodan", "requirements": [ - "shodan==1.17.0" + "shodan==1.19.0" ], "dependencies": [], "codeowners": [ "@fabaff" ] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index f6f78bab918299..54a9ec2d438f68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1739,7 +1739,7 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.17.0 +shodan==1.19.0 # homeassistant.components.simplepush simplepush==1.1.4 From 513d2652e4774589caa97db29c6ed68b01ca18f9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 1 Oct 2019 00:32:19 +0000 Subject: [PATCH 0525/3953] [ci skip] Translation update --- .../cert_expiry/.translations/pt-BR.json | 7 ++++-- .../deconz/.translations/pt-BR.json | 11 +++++++++ .../components/ecobee/.translations/lb.json | 12 ++++++++++ .../ecobee/.translations/pt-BR.json | 24 +++++++++++++++++++ .../geonetnz_quakes/.translations/pt-BR.json | 17 +++++++++++++ .../life360/.translations/pt-BR.json | 1 + .../components/light/.translations/pt-BR.json | 13 ++++++++++ .../components/linky/.translations/pt-BR.json | 19 +++++++++++++++ .../components/plex/.translations/pt-BR.json | 13 ++++++++++ .../components/soma/.translations/ca.json | 13 ++++++++++ .../components/soma/.translations/da.json | 12 ++++++++++ .../components/soma/.translations/en.json | 19 ++++----------- .../components/soma/.translations/it.json | 13 ++++++++++ .../components/soma/.translations/ru.json | 13 ++++++++++ .../components/soma/.translations/sl.json | 13 ++++++++++ .../traccar/.translations/pt-BR.json | 13 ++++++++++ .../transmission/.translations/pt-BR.json | 3 +++ .../twentemilieu/.translations/pt-BR.json | 23 ++++++++++++++++++ .../components/unifi/.translations/pt-BR.json | 2 ++ .../velbus/.translations/pt-BR.json | 11 +++++++++ .../components/zha/.translations/lb.json | 4 ++++ .../components/zha/.translations/no.json | 4 ++++ .../components/zha/.translations/pt-BR.json | 1 + 23 files changed, 244 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/ecobee/.translations/pt-BR.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/pt-BR.json create mode 100644 homeassistant/components/light/.translations/pt-BR.json create mode 100644 homeassistant/components/linky/.translations/pt-BR.json create mode 100644 homeassistant/components/plex/.translations/pt-BR.json create mode 100644 homeassistant/components/soma/.translations/ca.json create mode 100644 homeassistant/components/soma/.translations/da.json create mode 100644 homeassistant/components/soma/.translations/it.json create mode 100644 homeassistant/components/soma/.translations/ru.json create mode 100644 homeassistant/components/soma/.translations/sl.json create mode 100644 homeassistant/components/twentemilieu/.translations/pt-BR.json create mode 100644 homeassistant/components/velbus/.translations/pt-BR.json diff --git a/homeassistant/components/cert_expiry/.translations/pt-BR.json b/homeassistant/components/cert_expiry/.translations/pt-BR.json index d26f0f9470e7e0..06534314e0099a 100644 --- a/homeassistant/components/cert_expiry/.translations/pt-BR.json +++ b/homeassistant/components/cert_expiry/.translations/pt-BR.json @@ -6,6 +6,7 @@ "error": { "certificate_fetch_failed": "N\u00e3o \u00e9 poss\u00edvel buscar o certificado dessa combina\u00e7\u00e3o de host e porta", "connection_timeout": "Tempo limite ao conectar-se a este host", + "host_port_exists": "Essa combina\u00e7\u00e3o de host e porta j\u00e1 est\u00e1 configurada", "resolve_failed": "Este host n\u00e3o pode ser resolvido" }, "step": { @@ -14,8 +15,10 @@ "host": "O nome do host do certificado", "name": "O nome do certificado", "port": "A porta do certificado" - } + }, + "title": "Defina o certificado para testar" } - } + }, + "title": "Expira\u00e7\u00e3o do certificado" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt-BR.json b/homeassistant/components/deconz/.translations/pt-BR.json index d066cbcc510deb..8d54c470846b21 100644 --- a/homeassistant/components/deconz/.translations/pt-BR.json +++ b/homeassistant/components/deconz/.translations/pt-BR.json @@ -40,5 +40,16 @@ } }, "title": "Gateway deCONZ Zigbee" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + }, + "description": "Configurar visibilidade dos tipos de dispositivos deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/lb.json b/homeassistant/components/ecobee/.translations/lb.json index 1982dd40840283..ee1fd5246c0788 100644 --- a/homeassistant/components/ecobee/.translations/lb.json +++ b/homeassistant/components/ecobee/.translations/lb.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "one_instance_only": "D\u00ebs Integratioun \u00ebnnerst\u00ebtzt n\u00ebmmen eng ecobee Instanz." + }, + "error": { + "pin_request_failed": "Feeler beim ufroe vum PIN vun ecobee; iwwerpr\u00e9ift op den API Schl\u00ebssel korrekt ass.", + "token_request_failed": "Feeler beim ufroe vum Jeton vun ecobee; prob\u00e9iert nach emol." + }, "step": { + "authorize": { + "description": "Autoris\u00e9iert d\u00ebs App op https://www.ecobee.com/consumerportal/index.html mam Pin Code:\n\n{pin}\n\nKlickt dann op ofsch\u00e9cken.", + "title": "App autoris\u00e9ieren op ecobee.com" + }, "user": { "data": { "api_key": "API Schl\u00ebssel" }, + "description": "Gitt den API Schl\u00ebssel vun ecobee.com an:", "title": "ecobee API Schl\u00ebssel" } }, diff --git a/homeassistant/components/ecobee/.translations/pt-BR.json b/homeassistant/components/ecobee/.translations/pt-BR.json new file mode 100644 index 00000000000000..65394faba177c2 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "one_instance_only": "Essa integra\u00e7\u00e3o atualmente suporta apenas uma inst\u00e2ncia ecobee." + }, + "error": { + "token_request_failed": "Erro ao solicitar tokens da ecobee; Por favor, tente novamente." + }, + "step": { + "authorize": { + "description": "Por favor, autorize este aplicativo em https://www.ecobee.com/consumerportal/index.html com c\u00f3digo PIN:\n\n{pin}\n\nEm seguida, pressione Submit.", + "title": "Autorizar aplicativo em ecobee.com" + }, + "user": { + "data": { + "api_key": "Chave API" + }, + "description": "Por favor, insira a chave de API obtida em ecobee.com.", + "title": "chave da API ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json b/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json new file mode 100644 index 00000000000000..7e3ee3b24dab4c --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Localiza\u00e7\u00e3o j\u00e1 registrada" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "Preencha os detalhes do filtro." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/pt-BR.json b/homeassistant/components/life360/.translations/pt-BR.json index ca4cee896b37ac..5181c37969a585 100644 --- a/homeassistant/components/life360/.translations/pt-BR.json +++ b/homeassistant/components/life360/.translations/pt-BR.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Credenciais inv\u00e1lidas", "invalid_username": "Nome de usu\u00e1rio Inv\u00e1lido", + "unexpected": "Erro inesperado na comunica\u00e7\u00e3o com o servidor Life360", "user_already_configured": "A conta j\u00e1 foi configurada" }, "step": { diff --git a/homeassistant/components/light/.translations/pt-BR.json b/homeassistant/components/light/.translations/pt-BR.json new file mode 100644 index 00000000000000..05414b1e03c55f --- /dev/null +++ b/homeassistant/components/light/.translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Alternar {entity_name}", + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/pt-BR.json b/homeassistant/components/linky/.translations/pt-BR.json new file mode 100644 index 00000000000000..23f519353b4852 --- /dev/null +++ b/homeassistant/components/linky/.translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "username_exists": "Conta j\u00e1 configurada", + "wrong_login": "Erro de Login: por favor, verifique seu e-mail e senha" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "E-mail" + }, + "description": "Insira suas credenciais", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pt-BR.json b/homeassistant/components/plex/.translations/pt-BR.json new file mode 100644 index 00000000000000..9a759e309c2a10 --- /dev/null +++ b/homeassistant/components/plex/.translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos os controles", + "use_episode_art": "Usar arte epis\u00f3dio" + }, + "description": "Op\u00e7\u00f5es para Plex Media Players" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ca.json b/homeassistant/components/soma/.translations/ca.json new file mode 100644 index 00000000000000..6bd4737d6fc802 --- /dev/null +++ b/homeassistant/components/soma/.translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s pots configurar un compte de Soma.", + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "missing_configuration": "El component Soma no est\u00e0 configurat. Mira'n la documentaci\u00f3." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa amb Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json new file mode 100644 index 00000000000000..460f01e301feba --- /dev/null +++ b/homeassistant/components/soma/.translations/da.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en Soma-konto.", + "missing_configuration": "Soma-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen." + }, + "create_entry": { + "default": "Godkendt med Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json index 738d0fd642268f..5dea73fcc22a2e 100644 --- a/homeassistant/components/soma/.translations/en.json +++ b/homeassistant/components/soma/.translations/en.json @@ -1,24 +1,13 @@ { "config": { "abort": { - "already_setup": "You can only configure one Soma Connect.", - "missing_configuration": "The Soma component is not configured. Please follow the documentation.", - "connection_error": "Connection to the specified device failed." + "already_setup": "You can only configure one Soma account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Soma component is not configured. Please follow the documentation." }, "create_entry": { "default": "Successfully authenticated with Soma." }, - "step": { - "user": { - "data": { - "host": "Host", - "password": "Password", - "port": "Port", - "username": "Username" - }, - "title": "Set up Soma Connect" - } - }, "title": "Soma" } -} +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/it.json b/homeassistant/components/soma/.translations/it.json new file mode 100644 index 00000000000000..ce8e950daccc5c --- /dev/null +++ b/homeassistant/components/soma/.translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account Soma.", + "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", + "missing_configuration": "Il componente Soma non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticato con successo con Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json new file mode 100644 index 00000000000000..5ab3af0ecf8895 --- /dev/null +++ b/homeassistant/components/soma/.translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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.", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/sl.json b/homeassistant/components/soma/.translations/sl.json new file mode 100644 index 00000000000000..7dd523f366c443 --- /dev/null +++ b/homeassistant/components/soma/.translations/sl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Nastavite lahko samo en ra\u010dun Soma.", + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "missing_configuration": "Komponenta Soma ni konfigurirana. Upo\u0161tevajte dokumentacijo." + }, + "create_entry": { + "default": "Uspe\u0161no overjen s Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/pt-BR.json b/homeassistant/components/traccar/.translations/pt-BR.json index 9fc23b3e394074..4fa0c4e6714f73 100644 --- a/homeassistant/components/traccar/.translations/pt-BR.json +++ b/homeassistant/components/traccar/.translations/pt-BR.json @@ -1,5 +1,18 @@ { "config": { + "abort": { + "not_internet_accessible": "Sua inst\u00e2ncia do Home Assistant precisa estar acess\u00edvel na Internet para receber mensagens do Traccar.", + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, + "create_entry": { + "default": "Para enviar eventos ao Home Assistant, voc\u00ea precisar\u00e1 configurar o recurso de webhook no Traccar. \n\n Use o seguinte URL: ` {webhook_url} ` \n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) para mais detalhes." + }, + "step": { + "user": { + "description": "Tem certeza de que deseja configurar o Traccar?", + "title": "Configurar Traccar" + } + }, "title": "Traccar" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pt-BR.json b/homeassistant/components/transmission/.translations/pt-BR.json index a2d3d177e22d9f..cabbb6d91494c1 100644 --- a/homeassistant/components/transmission/.translations/pt-BR.json +++ b/homeassistant/components/transmission/.translations/pt-BR.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + }, "error": { "cannot_connect": "N\u00e3o foi poss\u00edvel conectar ao host", "wrong_credentials": "Nome de usu\u00e1rio ou senha incorretos" diff --git a/homeassistant/components/twentemilieu/.translations/pt-BR.json b/homeassistant/components/twentemilieu/.translations/pt-BR.json new file mode 100644 index 00000000000000..73735dda1d9752 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Endere\u00e7o j\u00e1 configurado." + }, + "error": { + "connection_error": "Falha ao conectar.", + "invalid_address": "Endere\u00e7o n\u00e3o encontrado na \u00e1rea de servi\u00e7o de Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Carta da casa/adicional", + "house_number": "N\u00famero da casa", + "post_code": "C\u00f3digo postal" + }, + "description": "Configure o Twente Milieu, fornecendo informa\u00e7\u00f5es de coleta de lixo em seu endere\u00e7o.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pt-BR.json b/homeassistant/components/unifi/.translations/pt-BR.json index ea13035e09bec1..113eaa000fe620 100644 --- a/homeassistant/components/unifi/.translations/pt-BR.json +++ b/homeassistant/components/unifi/.translations/pt-BR.json @@ -27,7 +27,9 @@ "step": { "device_tracker": { "data": { + "detection_time": "Tempo em segundos desde a \u00faltima vez que foi visto at\u00e9 ser considerado afastado", "track_clients": "Rastrear clientes da rede", + "track_devices": "Rastrear dispositivos de rede (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de rede com fio" } } diff --git a/homeassistant/components/velbus/.translations/pt-BR.json b/homeassistant/components/velbus/.translations/pt-BR.json new file mode 100644 index 00000000000000..cb2031dc7e556e --- /dev/null +++ b/homeassistant/components/velbus/.translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "port_exists": "Esta porta j\u00e1 est\u00e1 configurada" + }, + "error": { + "connection_failed": "A conex\u00e3o velbus falhou", + "port_exists": "Esta porta j\u00e1 est\u00e1 configurada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/lb.json b/homeassistant/components/zha/.translations/lb.json index 49a754f1da587f..a289e05e6676bc 100644 --- a/homeassistant/components/zha/.translations/lb.json +++ b/homeassistant/components/zha/.translations/lb.json @@ -18,6 +18,10 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Mellen", + "warn": "Warnen" + }, "trigger_subtype": { "both_buttons": "B\u00e9id Kn\u00e4ppchen", "button_1": "\u00c9ischte Kn\u00e4ppchen", diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 623c33637e0e9d..95550ca0999966 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -18,6 +18,10 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Squawk", + "warn": "Advarer" + }, "trigger_subtype": { "both_buttons": "Begge knapper", "button_1": "F\u00f8rste knapp", diff --git a/homeassistant/components/zha/.translations/pt-BR.json b/homeassistant/components/zha/.translations/pt-BR.json index 0bc3afe28eca5d..7ccc661dd28d56 100644 --- a/homeassistant/components/zha/.translations/pt-BR.json +++ b/homeassistant/components/zha/.translations/pt-BR.json @@ -19,6 +19,7 @@ }, "device_automation": { "action_type": { + "squawk": "Squawk", "warn": "Aviso" } } From bce49233ca64d9ec191259bae5855860429f734f Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Mon, 30 Sep 2019 17:42:06 -0700 Subject: [PATCH 0526/3953] Add some icons for Obihai (#27075) * Add some icons for Obihai * Lint * Lint * Lint fixes --- homeassistant/components/obihai/sensor.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 4644875ee8b164..89bfee7d4eebfe 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -111,6 +111,25 @@ def device_class(self): return DEVICE_CLASS_TIMESTAMP return None + @property + def icon(self): + """Return an icon.""" + if self._service_name == "Call Direction": + if self._state == "No Active Calls": + return "mdi:phone-off" + if self._state == "Inbound Call": + return "mdi:phone-incoming" + return "mdi:phone-outgoing" + if "Caller Info" in self._service_name: + return "mdi:phone-log" + if "Port" in self._service_name: + if self._state == "Ringing": + return "mdi:phone-ring" + if self._state == "Off Hook": + return "mdi:phone-in-talk" + return "mdi:phone-hangup" + return "mdi:phone" + def update(self): """Update the sensor.""" services = self._pyobihai.get_state() From a9398a362f61e4501e93d33a7ff6bee1b48388ce Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 1 Oct 2019 10:46:33 +1000 Subject: [PATCH 0527/3953] bumped version of upstream library (#27083) --- 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 c84a4152582a60..77f3c64752e9f5 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.9" + "aio_geojson_geonetnz_quakes==0.10" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 54a9ec2d438f68..561e3345417251 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -121,7 +121,7 @@ adguardhome==0.2.1 afsapi==0.0.4 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.9 +aio_geojson_geonetnz_quakes==0.10 # homeassistant.components.ambient_station aioambient==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index faf775ac5b844a..e7e4ed37e0012f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,7 +43,7 @@ YesssSMS==0.4.1 adguardhome==0.2.1 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.9 +aio_geojson_geonetnz_quakes==0.10 # homeassistant.components.ambient_station aioambient==0.3.2 From e2d7a01d65104efe47fcc58f05151545dc82d60e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 1 Oct 2019 06:19:51 +0200 Subject: [PATCH 0528/3953] Remove last of device tracker scanner (#27082) --- homeassistant/components/unifi/device_tracker.py | 12 ++---------- tests/components/unifi/test_device_tracker.py | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b3982e7327d42d..ad04b8a0eb37da 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,9 +1,8 @@ """Track devices using UniFi controllers.""" import logging -import voluptuous as vol from homeassistant.components.unifi.config_flow import get_controller_from_config_entry -from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER from homeassistant.core import callback @@ -39,13 +38,6 @@ "vlan", ] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) - - -async def async_setup_scanner(hass, config, sync_see, discovery_info): - """Set up the Unifi integration.""" - return True - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up device tracker for UniFi component.""" @@ -59,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if ( entity.config_entry_id == config_entry.entry_id - and entity.domain == DOMAIN + and entity.domain == DEVICE_TRACKER_DOMAIN and "-" in entity.unique_id ): diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 969c2a734d3448..760e1e4fa4c50c 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -163,12 +163,12 @@ async def setup_controller(hass, mock_controller, options={}): async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a bridge.""" + """Test that nothing happens when configuring unifi through device tracker platform.""" assert ( await async_setup_component( hass, device_tracker.DOMAIN, {device_tracker.DOMAIN: {"platform": "unifi"}} ) - is True + is False ) assert unifi.DOMAIN not in hass.data From a1997ee891be5ed5b5c2d85b6eb738fdf9bc0e00 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 1 Oct 2019 05:35:10 +0100 Subject: [PATCH 0529/3953] Bugfix evohome (#26810) * address issues #25984, #25985 * small tweak * refactor - fix bugs, coding erros, consolidate * some zones don't have schedules * some zones don't have schedules 2 * some zones don't have schedules 3 * fix water_heater, add away mode * readbility tweak * bugfix: no refesh after state change * bugfix: no refesh after state change 2 * temove dodgy wrappers (protected-access), fix until logic * remove dodgy _set_zone_mode wrapper * tweak * tweak docstrings * refactor as per PR review * refactor as per PR review 3 * refactor to use dt_util * small tweak * tweak doc strings * remove packet from _refresh * set_temp() don't have until * add unique_id * add unique_id 2 --- homeassistant/components/evohome/__init__.py | 247 +++++++++++------- homeassistant/components/evohome/climate.py | 240 ++++++++--------- homeassistant/components/evohome/const.py | 2 - .../components/evohome/water_heater.py | 92 ++++--- 4 files changed, 315 insertions(+), 266 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index ba7a72024ed16b..14bf12239538b2 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -4,6 +4,7 @@ """ from datetime import datetime, timedelta import logging +import re from typing import Any, Dict, Optional, Tuple import aiohttp.client_exceptions @@ -25,9 +26,9 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.util.dt import parse_datetime, utcnow +import homeassistant.util.dt as dt_util -from .const import DOMAIN, STORAGE_VERSION, STORAGE_KEY, GWS, TCS +from .const import DOMAIN, EVO_FOLLOW, STORAGE_VERSION, STORAGE_KEY, GWS, TCS _LOGGER = logging.getLogger(__name__) @@ -55,20 +56,45 @@ ) -def _local_dt_to_utc(dt_naive: datetime) -> datetime: - dt_aware = utcnow() + (dt_naive - datetime.now()) +def _local_dt_to_aware(dt_naive: datetime) -> datetime: + dt_aware = dt_util.now() + (dt_naive - datetime.now()) if dt_aware.microsecond >= 500000: dt_aware += timedelta(seconds=1) return dt_aware.replace(microsecond=0) -def _utc_to_local_dt(dt_aware: datetime) -> datetime: - dt_naive = datetime.now() + (dt_aware - utcnow()) +def _dt_to_local_naive(dt_aware: datetime) -> datetime: + dt_naive = datetime.now() + (dt_aware - dt_util.now()) if dt_naive.microsecond >= 500000: dt_naive += timedelta(seconds=1) return dt_naive.replace(microsecond=0) +def convert_until(status_dict, until_key) -> str: + """Convert datetime string from "%Y-%m-%dT%H:%M:%SZ" to local/aware/isoformat.""" + if until_key in status_dict: # only present for certain modes + dt_utc_naive = dt_util.parse_datetime(status_dict[until_key]) + status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat() + + +def convert_dict(dictionary: Dict[str, Any]) -> Dict[str, Any]: + """Recursively convert a dict's keys to snake_case.""" + + def convert_key(key: str) -> str: + """Convert a string to snake_case.""" + string = re.sub(r"[\-\.\s]", "_", str(key)) + return (string[0]).lower() + re.sub( + r"[A-Z]", lambda matched: "_" + matched.group(0).lower(), string[1:] + ) + + return { + (convert_key(k) if isinstance(k, str) else k): ( + convert_dict(v) if isinstance(v, dict) else v + ) + for k, v in dictionary.items() + } + + def _handle_exception(err) -> bool: try: raise err @@ -135,7 +161,7 @@ class EvoBroker: """Container for evohome client and data.""" def __init__(self, hass, params) -> None: - """Initialize the evohome client and data structure.""" + """Initialize the evohome client and its data structure.""" self.hass = hass self.params = params self.config = {} @@ -157,7 +183,7 @@ async def init_client(self) -> bool: # evohomeasync2 uses naive/local datetimes if access_token_expires is not None: - access_token_expires = _utc_to_local_dt(access_token_expires) + access_token_expires = _dt_to_local_naive(access_token_expires) client = self.client = evohomeasync2.EvohomeClient( self.params[CONF_USERNAME], @@ -220,7 +246,7 @@ async def _load_auth_tokens( access_token = app_storage.get(CONF_ACCESS_TOKEN) at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES) if at_expires_str: - at_expires_dt = parse_datetime(at_expires_str) + at_expires_dt = dt_util.parse_datetime(at_expires_str) else: at_expires_dt = None @@ -230,7 +256,7 @@ async def _load_auth_tokens( async def _save_auth_tokens(self, *args) -> None: # evohomeasync2 uses naive/local datetimes - access_token_expires = _local_dt_to_utc(self.client.access_token_expires) + access_token_expires = _local_dt_to_aware(self.client.access_token_expires) self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME] self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token @@ -246,11 +272,11 @@ async def _save_auth_tokens(self, *args) -> None: ) async def update(self, *args, **kwargs) -> None: - """Get the latest state data of the entire evohome Location. + """Get the latest state data of an entire evohome Location. - This includes state data for the Controller and all its child devices, - such as the operating mode of the Controller and the current temp of - its children (e.g. Zones, DHW controller). + This includes state data for a Controller and all its child devices, such as the + operating mode of the Controller and the current temp of its children (e.g. + Zones, DHW controller). """ loc_idx = self.params[CONF_LOCATION_IDX] @@ -260,9 +286,7 @@ async def update(self, *args, **kwargs) -> None: _handle_exception(err) else: # inform the evohome devices that state data has been updated - self.hass.helpers.dispatcher.async_dispatcher_send( - DOMAIN, {"signal": "refresh"} - ) + self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) @@ -270,8 +294,8 @@ async def update(self, *args, **kwargs) -> None: class EvoDevice(Entity): """Base for any evohome device. - This includes the Controller, (up to 12) Heating Zones and - (optionally) a DHW controller. + This includes the Controller, (up to 12) Heating Zones and (optionally) a + DHW controller. """ def __init__(self, evo_broker, evo_device) -> None: @@ -280,72 +304,26 @@ def __init__(self, evo_broker, evo_device) -> None: self._evo_broker = evo_broker self._evo_tcs = evo_broker.tcs - self._name = self._icon = self._precision = None - self._state_attributes = [] + self._unique_id = self._name = self._icon = self._precision = None + self._device_state_attrs = {} + self._state_attributes = [] self._supported_features = None - self._schedule = {} @callback - def _refresh(self, packet): - if packet["signal"] == "refresh": - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def setpoints(self) -> Dict[str, Any]: - """Return the current/next setpoints from the schedule. - - Only Zones & DHW controllers (but not the TCS) can have schedules. - """ - if not self._schedule["DailySchedules"]: - return {} - - switchpoints = {} - - day_time = datetime.now() - day_of_week = int(day_time.strftime("%w")) # 0 is Sunday - - # Iterate today's switchpoints until past the current time of day... - day = self._schedule["DailySchedules"][day_of_week] - sp_idx = -1 # last switchpoint of the day before - for i, tmp in enumerate(day["Switchpoints"]): - if day_time.strftime("%H:%M:%S") > tmp["TimeOfDay"]: - sp_idx = i # current setpoint - else: - break - - # Did the current SP start yesterday? Does the next start SP tomorrow? - current_sp_day = -1 if sp_idx == -1 else 0 - next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 - - for key, offset, idx in [ - ("current", current_sp_day, sp_idx), - ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), - ]: - - spt = switchpoints[key] = {} - - sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") - day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] - switchpoint = day["Switchpoints"][idx] - - dt_naive = datetime.strptime( - f"{sp_date}T{switchpoint['TimeOfDay']}", "%Y-%m-%dT%H:%M:%S" - ) - - spt["from"] = _local_dt_to_utc(dt_naive).isoformat() - try: - spt["temperature"] = switchpoint["heatSetpoint"] - except KeyError: - spt["state"] = switchpoint["DhwState"] - - return switchpoints + def _refresh(self) -> None: + self.async_schedule_update_ha_state(force_refresh=True) @property def should_poll(self) -> bool: """Evohome entities should not be polled.""" return False + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + @property def name(self) -> str: """Return the name of the Evohome entity.""" @@ -354,15 +332,15 @@ def name(self) -> str: @property def device_state_attributes(self) -> Dict[str, Any]: """Return the Evohome-specific state attributes.""" - status = {} - for attr in self._state_attributes: - if attr != "setpoints": - status[attr] = getattr(self._evo_device, attr) - - if "setpoints" in self._state_attributes: - status["setpoints"] = self.setpoints + status = self._device_state_attrs + if "systemModeStatus" in status: + convert_until(status["systemModeStatus"], "timeUntil") + if "setpointStatus" in status: + convert_until(status["setpointStatus"], "until") + if "stateStatus" in status: + convert_until(status["stateStatus"], "until") - return {"status": status} + return {"status": convert_dict(status)} @property def icon(self) -> str: @@ -388,27 +366,98 @@ def temperature_unit(self) -> str: """Return the temperature unit to use in the frontend UI.""" return TEMP_CELSIUS - async def _call_client_api(self, api_function) -> None: + async def _call_client_api(self, api_function, refresh=True) -> Any: try: - await api_function + result = await api_function except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - _handle_exception(err) + if not _handle_exception(err): + return - self.hass.helpers.event.async_call_later( - 2, self._evo_broker.update() - ) # call update() in 2 seconds + if refresh is True: + self.hass.helpers.event.async_call_later(1, self._evo_broker.update()) - async def _update_schedule(self) -> None: - """Get the latest state data.""" - if ( - not self._schedule.get("DailySchedules") - or parse_datetime(self.setpoints["next"]["from"]) < utcnow() - ): + return result + + +class EvoChild(EvoDevice): + """Base for any evohome child. + + This includes (up to 12) Heating Zones and (optionally) a DHW controller. + """ + + def __init__(self, evo_broker, evo_device) -> None: + """Initialize a evohome Controller (hub).""" + super().__init__(evo_broker, evo_device) + self._schedule = {} + self._setpoints = {} + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature of a Zone.""" + if self._evo_device.temperatureStatus["isAvailable"]: + return self._evo_device.temperatureStatus["temperature"] + return None + + @property + def setpoints(self) -> Dict[str, Any]: + """Return the current/next setpoints from the schedule. + + Only Zones & DHW controllers (but not the TCS) can have schedules. + """ + if not self._schedule["DailySchedules"]: + return {} # no schedule {'DailySchedules': []}, so no scheduled setpoints + + day_time = dt_util.now() + day_of_week = int(day_time.strftime("%w")) # 0 is Sunday + time_of_day = day_time.strftime("%H:%M:%S") + + # Iterate today's switchpoints until past the current time of day... + day = self._schedule["DailySchedules"][day_of_week] + sp_idx = -1 # last switchpoint of the day before + for i, tmp in enumerate(day["Switchpoints"]): + if time_of_day > tmp["TimeOfDay"]: + sp_idx = i # current setpoint + else: + break + + # Did the current SP start yesterday? Does the next start SP tomorrow? + this_sp_day = -1 if sp_idx == -1 else 0 + next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 + + for key, offset, idx in [ + ("this", this_sp_day, sp_idx), + ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), + ]: + sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") + day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] + switchpoint = day["Switchpoints"][idx] + + dt_local_aware = _local_dt_to_aware( + dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}") + ) + + self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat() try: - self._schedule = await self._evo_device.schedule() - except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - _handle_exception(err) + self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"] + except KeyError: + self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] + + return self._setpoints + + async def _update_schedule(self) -> None: + """Get the latest schedule.""" + if "DailySchedules" in self._schedule and not self._schedule["DailySchedules"]: + if not self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: + return # avoid unnecessary I/O - there's nothing to update + + self._schedule = await self._call_client_api( + self._evo_device.schedule(), refresh=False + ) async def async_update(self) -> None: """Get the latest state data.""" - await self._update_schedule() + next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00") + if dt_util.now() >= dt_util.parse_datetime(next_sp_from): + await self._update_schedule() # no schedule, or it's out-of-date + + self._device_state_attrs = {"setpoints": self.setpoints} diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 0264f76f38f5db..e5c8c6af14bde1 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -1,7 +1,6 @@ """Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems.""" -from datetime import datetime import logging -from typing import Any, Dict, Optional, List +from typing import Optional, List from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -22,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import CONF_LOCATION_IDX, EvoDevice +from . import CONF_LOCATION_IDX, EvoDevice, EvoChild from .const import ( DOMAIN, EVO_RESET, @@ -61,6 +60,9 @@ } HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items()} +STATE_ATTRS_TCS = ["systemId", "activeFaults", "systemModeStatus"] +STATE_ATTRS_ZONES = ["zoneId", "activeFaults", "setpointStatus", "temperatureStatus"] + async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None @@ -114,63 +116,20 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): """Base for a Honeywell evohome Climate device.""" def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome Climate device.""" + """Initialize a Climate device.""" super().__init__(evo_broker, evo_device) self._preset_modes = None - async def _set_temperature( - self, temperature: float, until: Optional[datetime] = None - ) -> None: - """Set a new target temperature for the Zone. - - until == None means indefinitely (i.e. PermanentOverride) - """ - await self._call_client_api( - self._evo_device.set_temperature(temperature, until) - ) - - async def _set_zone_mode(self, op_mode: str) -> None: - """Set a Zone to one of its native EVO_* operating modes. - - Zones inherit their _effective_ operating mode from the Controller. - - Usually, Zones are in 'FollowSchedule' mode, where their setpoints are - a function of their own schedule and the Controller's operating mode, - e.g. 'AutoWithEco' mode means their setpoint is (by default) 3C less - than scheduled. - - However, Zones can _override_ these setpoints, either indefinitely, - 'PermanentOverride' mode, or for a period of time, 'TemporaryOverride', - after which they will revert back to 'FollowSchedule'. - - Finally, some of the Controller's operating modes are _forced_ upon the - Zones, regardless of any override mode, e.g. 'HeatingOff', Zones to - (by default) 5C, and 'Away', Zones to (by default) 12C. - """ - if op_mode == EVO_FOLLOW: - await self._call_client_api(self._evo_device.cancel_temp_override()) - return - - temperature = self._evo_device.setpointStatus["targetHeatTemperature"] - until = None # EVO_PERMOVER - - if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: - await self._update_schedule() - if self._schedule["DailySchedules"]: - until = parse_datetime(self.setpoints["next"]["from"]) - - await self._set_temperature(temperature, until=until) - async def _set_tcs_mode(self, op_mode: str) -> None: - """Set the Controller to any of its native EVO_* operating modes.""" + """Set a Controller to any of its native EVO_* operating modes.""" await self._call_client_api( self._evo_tcs._set_status(op_mode) # pylint: disable=protected-access ) @property def hvac_modes(self) -> List[str]: - """Return the list of available hvac operation modes.""" + """Return a list of available hvac operation modes.""" return list(HA_HVAC_TO_TCS) @property @@ -179,36 +138,24 @@ def preset_modes(self) -> Optional[List[str]]: return self._preset_modes -class EvoZone(EvoClimateDevice): +class EvoZone(EvoChild, EvoClimateDevice): """Base for a Honeywell evohome Zone.""" def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome Zone.""" + """Initialize a Zone.""" super().__init__(evo_broker, evo_device) + self._unique_id = evo_device.zoneId self._name = evo_device.name self._icon = "mdi:radiator" self._precision = self._evo_device.setpointCapabilities["valueResolution"] - self._state_attributes = [ - "zoneId", - "activeFaults", - "setpointStatus", - "temperatureStatus", - "setpoints", - ] - self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE self._preset_modes = list(HA_PRESET_TO_EVO) - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._evo_device.temperatureStatus["isAvailable"] - @property def hvac_mode(self) -> str: - """Return the current operating mode of the evohome Zone.""" + """Return the current operating mode of a Zone.""" if self._evo_tcs.systemModeStatus["mode"] in [EVO_AWAY, EVO_HEATOFF]: return HVAC_MODE_AUTO is_off = self.target_temperature <= self.min_temp @@ -221,24 +168,15 @@ def hvac_action(self) -> Optional[str]: return CURRENT_HVAC_OFF if self.target_temperature <= self.min_temp: return CURRENT_HVAC_OFF - if self.target_temperature < self.current_temperature: + if not self._evo_device.temperatureStatus["isAvailable"]: + return None + if self.target_temperature <= self.current_temperature: return CURRENT_HVAC_IDLE return CURRENT_HVAC_HEAT - @property - def current_temperature(self) -> Optional[float]: - """Return the current temperature of the evohome Zone.""" - return ( - self._evo_device.temperatureStatus["temperature"] - if self._evo_device.temperatureStatus["isAvailable"] - else None - ) - @property def target_temperature(self) -> float: - """Return the target temperature of the evohome Zone.""" - if self._evo_tcs.systemModeStatus["mode"] == EVO_HEATOFF: - return self._evo_device.setpointCapabilities["minHeatSetpoint"] + """Return the target temperature of a Zone.""" return self._evo_device.setpointStatus["targetHeatTemperature"] @property @@ -252,7 +190,7 @@ def preset_mode(self) -> Optional[str]: @property def min_temp(self) -> float: - """Return the minimum target temperature of a evohome Zone. + """Return the minimum target temperature of a Zone. The default is 5, but is user-configurable within 5-35 (in Celsius). """ @@ -260,7 +198,7 @@ def min_temp(self) -> float: @property def max_temp(self) -> float: - """Return the maximum target temperature of a evohome Zone. + """Return the maximum target temperature of a Zone. The default is 35, but is user-configurable within 5-35 (in Celsius). """ @@ -268,26 +206,70 @@ def max_temp(self) -> float: async def async_set_temperature(self, **kwargs) -> None: """Set a new target temperature.""" - until = kwargs.get("until") - if until: - until = parse_datetime(until) + temperature = kwargs["temperature"] - await self._set_temperature(kwargs["temperature"], until) + if self._evo_device.setpointStatus["setpointMode"] == EVO_FOLLOW: + await self._update_schedule() + until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + elif self._evo_device.setpointStatus["setpointMode"] == EVO_TEMPOVER: + until = parse_datetime(self._evo_device.setpointStatus["until"]) + else: # EVO_PERMOVER + until = None + + await self._call_client_api( + self._evo_device.set_temperature(temperature, until) + ) async def async_set_hvac_mode(self, hvac_mode: str) -> None: - """Set an operating mode for the Zone.""" - if hvac_mode == HVAC_MODE_OFF: - await self._set_temperature(self.min_temp, until=None) + """Set a Zone to one of its native EVO_* operating modes. + + Zones inherit their _effective_ operating mode from their Controller. + Usually, Zones are in 'FollowSchedule' mode, where their setpoints are a + function of their own schedule and the Controller's operating mode, e.g. + 'AutoWithEco' mode means their setpoint is (by default) 3C less than scheduled. + + However, Zones can _override_ these setpoints, either indefinitely, + 'PermanentOverride' mode, or for a set period of time, 'TemporaryOverride' mode + (after which they will revert back to 'FollowSchedule' mode). + + Finally, some of the Controller's operating modes are _forced_ upon the Zones, + regardless of any override mode, e.g. 'HeatingOff', Zones to (by default) 5C, + and 'Away', Zones to (by default) 12C. + """ + if hvac_mode == HVAC_MODE_OFF: + await self._call_client_api( + self._evo_device.set_temperature(self.min_temp, until=None) + ) else: # HVAC_MODE_HEAT - await self._set_zone_mode(EVO_FOLLOW) + await self._call_client_api(self._evo_device.cancel_temp_override()) async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: - """Set a new preset mode. + """Set the preset mode; if None, then revert to following the schedule.""" + evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW) - If preset_mode is None, then revert to following the schedule. - """ - await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) + if evo_preset_mode == EVO_FOLLOW: + await self._call_client_api(self._evo_device.cancel_temp_override()) + return + + temperature = self._evo_device.setpointStatus["targetHeatTemperature"] + + if evo_preset_mode == EVO_TEMPOVER: + await self._update_schedule() + until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + else: # EVO_PERMOVER + until = None + + await self._call_client_api( + self._evo_device.set_temperature(temperature, until) + ) + + async def async_update(self) -> None: + """Get the latest state data for a Zone.""" + await super().async_update() + + for attr in STATE_ATTRS_ZONES: + self._device_state_attrs[attr] = getattr(self._evo_device, attr) class EvoController(EvoClimateDevice): @@ -298,21 +280,20 @@ class EvoController(EvoClimateDevice): """ def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome Controller (hub).""" + """Initialize a evohome Controller (hub).""" super().__init__(evo_broker, evo_device) + self._unique_id = evo_device.systemId self._name = evo_device.location.name self._icon = "mdi:thermostat" self._precision = PRECISION_TENTHS - self._state_attributes = ["systemId", "activeFaults", "systemModeStatus"] - self._supported_features = SUPPORT_PRESET_MODE self._preset_modes = list(HA_PRESET_TO_TCS) @property def hvac_mode(self) -> str: - """Return the current operating mode of the evohome Controller.""" + """Return the current operating mode of a Controller.""" tcs_mode = self._evo_tcs.systemModeStatus["mode"] return HVAC_MODE_OFF if tcs_mode == EVO_HEATOFF else HVAC_MODE_HEAT @@ -334,52 +315,53 @@ def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - async def async_set_temperature(self, **kwargs) -> None: - """Do nothing. + @property + def min_temp(self) -> float: + """Return None as Controllers don't have a target temperature.""" + return None - The evohome Controller doesn't have a target temperature. - """ - return + @property + def max_temp(self) -> float: + """Return None as Controllers don't have a target temperature.""" + return None + + async def async_set_temperature(self, **kwargs) -> None: + """Raise exception as Controllers don't have a target temperature.""" + raise NotImplementedError("Evohome Controllers don't have target temperatures.") async def async_set_hvac_mode(self, hvac_mode: str) -> None: - """Set an operating mode for the Controller.""" + """Set an operating mode for a Controller.""" await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: - """Set a new preset mode. - - If preset_mode is None, then revert to 'Auto' mode. - """ + """Set the preset mode; if None, then revert to 'Auto' mode.""" await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) async def async_update(self) -> None: - """Get the latest state data.""" - return + """Get the latest state data for a Controller.""" + self._device_state_attrs = {} + + attrs = self._device_state_attrs + for attr in STATE_ATTRS_TCS: + if attr == "activeFaults": + attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr) + else: + attrs[attr] = getattr(self._evo_tcs, attr) class EvoThermostat(EvoZone): """Base for a Honeywell Round Thermostat. - Implemented as a combined Controller/Zone. + These are implemented as a combined Controller/Zone. """ def __init__(self, evo_broker, evo_device) -> None: - """Initialize the Round Thermostat.""" + """Initialize the Thermostat.""" super().__init__(evo_broker, evo_device) self._name = evo_broker.tcs.location.name self._preset_modes = [PRESET_AWAY, PRESET_ECO] - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device-specific state attributes.""" - status = super().device_state_attributes["status"] - - status["systemModeStatus"] = self._evo_tcs.systemModeStatus - status["activeFaults"] += self._evo_tcs.activeFaults - - return {"status": status} - @property def hvac_mode(self) -> str: """Return the current operating mode.""" @@ -404,11 +386,19 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: - """Set a new preset mode. - - If preset_mode is None, then revert to following the schedule. - """ + """Set the preset mode; if None, then revert to following the schedule.""" if preset_mode in list(HA_PRESET_TO_TCS): await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode)) else: - await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) + await super().async_set_hvac_mode(preset_mode) + + async def async_update(self) -> None: + """Get the latest state data for the Thermostat.""" + await super().async_update() + + attrs = self._device_state_attrs + for attr in STATE_ATTRS_TCS: + if attr == "activeFaults": # self._evo_device also has "activeFaults" + attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr) + else: + attrs[attr] = getattr(self._evo_tcs, attr) diff --git a/homeassistant/components/evohome/const.py b/homeassistant/components/evohome/const.py index a0480d62a10aa2..444671cf82aa8f 100644 --- a/homeassistant/components/evohome/const.py +++ b/homeassistant/components/evohome/const.py @@ -21,5 +21,3 @@ # These are used only to help prevent E501 (line too long) violations GWS = "gateways" TCS = "temperatureControlSystems" - -EVO_STRFTIME = "%Y-%m-%dT%H:%M:%SZ" diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 1b37bc3b2b58c2..b65665eb2c9ad2 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -3,27 +3,31 @@ from typing import List from homeassistant.components.water_heater import ( + SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, WaterHeaterDevice, ) from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import EvoDevice -from .const import DOMAIN, EVO_STRFTIME, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER +from . import EvoChild +from .const import DOMAIN, EVO_FOLLOW, EVO_PERMOVER _LOGGER = logging.getLogger(__name__) -HA_STATE_TO_EVO = {STATE_ON: "On", STATE_OFF: "Off"} -EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items()} +STATE_AUTO = "auto" -HA_OPMODE_TO_DHW = {STATE_ON: EVO_FOLLOW, STATE_OFF: EVO_PERMOVER} +HA_STATE_TO_EVO = {STATE_AUTO: "", STATE_ON: "On", STATE_OFF: "Off"} +EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items() if k != ""} + +STATE_ATTRS_DHW = ["dhwId", "activeFaults", "stateStatus", "temperatureStatus"] async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ) -> None: - """Create the DHW controller.""" + """Create a DHW controller.""" if discovery_info is None: return @@ -38,63 +42,71 @@ async def async_setup_platform( async_add_entities([evo_dhw], update_before_add=True) -class EvoDHW(EvoDevice, WaterHeaterDevice): +class EvoDHW(EvoChild, WaterHeaterDevice): """Base for a Honeywell evohome DHW controller (aka boiler).""" def __init__(self, evo_broker, evo_device) -> None: - """Initialize the evohome DHW controller.""" + """Initialize a evohome DHW controller.""" super().__init__(evo_broker, evo_device) + self._unique_id = evo_device.dhwId self._name = "DHW controller" self._icon = "mdi:thermometer-lines" self._precision = PRECISION_WHOLE - self._state_attributes = [ - "dhwId", - "activeFaults", - "stateStatus", - "temperatureStatus", - "setpoints", - ] - - self._supported_features = SUPPORT_OPERATION_MODE - self._operation_list = list(HA_OPMODE_TO_DHW) + self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._evo_device.temperatureStatus.get("isAvailable", False) + def state(self): + """Return the current state.""" + return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] @property def current_operation(self) -> str: - """Return the current operating mode (On, or Off).""" + """Return the current operating mode (Auto, On, or Off).""" + if self._evo_device.stateStatus["mode"] == EVO_FOLLOW: + return STATE_AUTO return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] @property def operation_list(self) -> List[str]: """Return the list of available operations.""" - return self._operation_list + return list(HA_STATE_TO_EVO) @property - def current_temperature(self) -> float: - """Return the current temperature.""" - return self._evo_device.temperatureStatus["temperature"] + def is_away_mode_on(self): + """Return True if away mode is on.""" + is_off = EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] == STATE_OFF + is_permanent = self._evo_device.stateStatus["mode"] == EVO_PERMOVER + return is_off and is_permanent async def async_set_operation_mode(self, operation_mode: str) -> None: - """Set new operation mode for a DHW controller.""" - op_mode = HA_OPMODE_TO_DHW[operation_mode] - - state = "" if op_mode == EVO_FOLLOW else HA_STATE_TO_EVO[STATE_OFF] - until = None # EVO_FOLLOW, EVO_PERMOVER + """Set new operation mode for a DHW controller. - if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: + Except for Auto, the mode is only until the next SetPoint. + """ + if operation_mode == STATE_AUTO: + await self._call_client_api(self._evo_device.set_dhw_auto()) + else: await self._update_schedule() - if self._schedule["DailySchedules"]: - until = parse_datetime(self.setpoints["next"]["from"]) - until = until.strftime(EVO_STRFTIME) + until = parse_datetime(str(self.setpoints.get("next_sp_from"))) + + if operation_mode == STATE_ON: + await self._call_client_api(self._evo_device.set_dhw_on(until)) + else: # STATE_OFF + await self._call_client_api(self._evo_device.set_dhw_off(until)) + + async def async_turn_away_mode_on(self): + """Turn away mode on.""" + await self._call_client_api(self._evo_device.set_dhw_off()) + + async def async_turn_away_mode_off(self): + """Turn away mode off.""" + await self._call_client_api(self._evo_device.set_dhw_auto()) - data = {"Mode": op_mode, "State": state, "UntilTime": until} + async def async_update(self) -> None: + """Get the latest state data for a DHW controller.""" + await super().async_update() - await self._call_client_api( - self._evo_device._set_dhw(data) # pylint: disable=protected-access - ) + for attr in STATE_ATTRS_DHW: + self._device_state_attrs[attr] = getattr(self._evo_device, attr) From 2e3bc5964dc4cbbdc7fd78b1aaa10fa7cfe660b9 Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Tue, 1 Oct 2019 13:25:57 +0200 Subject: [PATCH 0530/3953] Add saj component (#26902) * Add saj component * Performed requested changes after review * Performed requested changes after review 2 * Performed requested changes after review 3 * Black * Bump pysaj library version * Changes after review * Fix flake8 * Review changes + isort --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/saj/__init__.py | 1 + homeassistant/components/saj/manifest.json | 12 ++ homeassistant/components/saj/sensor.py | 202 +++++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 220 insertions(+) create mode 100644 homeassistant/components/saj/__init__.py create mode 100644 homeassistant/components/saj/manifest.json create mode 100644 homeassistant/components/saj/sensor.py diff --git a/.coveragerc b/.coveragerc index f28e9aaeda67e2..aa8f2d8c03d16e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -558,6 +558,7 @@ omit = homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py homeassistant/components/sabnzbd/* + homeassistant/components/saj/sensor.py homeassistant/components/satel_integra/* homeassistant/components/scrape/sensor.py homeassistant/components/scsgate/* diff --git a/CODEOWNERS b/CODEOWNERS index db0ff3226c38ba..f5cd03882c5944 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -235,6 +235,7 @@ homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt +homeassistant/components/saj/* @fredericvl homeassistant/components/scene/* @home-assistant/core homeassistant/components/scrape/* @fabaff homeassistant/components/script/* @home-assistant/core diff --git a/homeassistant/components/saj/__init__.py b/homeassistant/components/saj/__init__.py new file mode 100644 index 00000000000000..03277bba4df351 --- /dev/null +++ b/homeassistant/components/saj/__init__.py @@ -0,0 +1 @@ +"""The saj component.""" diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json new file mode 100644 index 00000000000000..c0367c47902089 --- /dev/null +++ b/homeassistant/components/saj/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "saj", + "name": "SAJ", + "documentation": "https://www.home-assistant.io/components/saj", + "requirements": [ + "pysaj==0.0.9" + ], + "dependencies": [], + "codeowners": [ + "@fredericvl" + ] +} diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py new file mode 100644 index 00000000000000..fa06b2b9125ec7 --- /dev/null +++ b/homeassistant/components/saj/sensor.py @@ -0,0 +1,202 @@ +"""SAJ solar inverter interface.""" +import asyncio +from datetime import date +import logging + +import pysaj +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, + MASS_KILOGRAMS, + POWER_WATT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import CALLBACK_TYPE, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_call_later + +_LOGGER = logging.getLogger(__name__) + +MIN_INTERVAL = 5 +MAX_INTERVAL = 300 + +UNIT_OF_MEASUREMENT_HOURS = "h" + +SAJ_UNIT_MAPPINGS = { + "W": POWER_WATT, + "kWh": ENERGY_KILO_WATT_HOUR, + "h": UNIT_OF_MEASUREMENT_HOURS, + "kg": MASS_KILOGRAMS, + "°C": TEMP_CELSIUS, + "": None, +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up SAJ sensors.""" + + remove_interval_update = None + + # Init all sensors + sensor_def = pysaj.Sensors() + + # Use all sensors by default + hass_sensors = [] + + for sensor in sensor_def: + hass_sensors.append(SAJsensor(sensor)) + + saj = pysaj.SAJ(config[CONF_HOST]) + + async_add_entities(hass_sensors) + + async def async_saj(): + """Update all the SAJ sensors.""" + tasks = [] + + values = await saj.read(sensor_def) + + for sensor in hass_sensors: + state_unknown = False + if not values: + # SAJ inverters are powered by DC via solar panels and thus are + # offline after the sun has set. If a sensor resets on a daily + # basis like "today_yield", this reset won't happen automatically. + # Code below checks if today > day when sensor was last updated + # and if so: set state to None. + # Sensors with live values like "temperature" or "current_power" + # will also be reset to None. + if (sensor.per_day_basis and date.today() > sensor.date_updated) or ( + not sensor.per_day_basis and not sensor.per_total_basis + ): + state_unknown = True + task = sensor.async_update_values(unknown_state=state_unknown) + if task: + tasks.append(task) + if tasks: + await asyncio.wait(tasks) + return values + + def start_update_interval(event): + """Start the update interval scheduling.""" + nonlocal remove_interval_update + remove_interval_update = async_track_time_interval_backoff(hass, async_saj) + + def stop_update_interval(event): + """Properly cancel the scheduled update.""" + remove_interval_update() # pylint: disable=not-callable + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_update_interval) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_update_interval) + + +@callback +def async_track_time_interval_backoff(hass, action) -> CALLBACK_TYPE: + """Add a listener that fires repetitively and increases the interval when failed.""" + remove = None + interval = MIN_INTERVAL + + async def interval_listener(now=None): + """Handle elapsed interval with backoff.""" + nonlocal interval, remove + try: + if await action(): + interval = MIN_INTERVAL + else: + interval = min(interval * 2, MAX_INTERVAL) + finally: + remove = async_call_later(hass, interval, interval_listener) + + hass.async_create_task(interval_listener()) + + def remove_listener(): + """Remove interval listener.""" + if remove: + remove() # pylint: disable=not-callable + + return remove_listener + + +class SAJsensor(Entity): + """Representation of a SAJ sensor.""" + + def __init__(self, pysaj_sensor): + """Initialize the sensor.""" + self._sensor = pysaj_sensor + self._state = self._sensor.value + + @property + def name(self): + """Return the name of the sensor.""" + return f"saj_{self._sensor.name}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SAJ_UNIT_MAPPINGS[self._sensor.unit] + + @property + def device_class(self): + """Return the device class the sensor belongs to.""" + if self.unit_of_measurement == POWER_WATT: + return DEVICE_CLASS_POWER + if ( + self.unit_of_measurement == TEMP_CELSIUS + or self._sensor.unit == TEMP_FAHRENHEIT + ): + return DEVICE_CLASS_TEMPERATURE + + @property + def should_poll(self) -> bool: + """SAJ sensors are updated & don't poll.""" + return False + + @property + def per_day_basis(self) -> bool: + """Return if the sensors value is on daily basis or not.""" + return self._sensor.per_day_basis + + @property + def per_total_basis(self) -> bool: + """Return if the sensors value is cummulative or not.""" + return self._sensor.per_total_basis + + @property + def date_updated(self) -> date: + """Return the date when the sensor was last updated.""" + return self._sensor.date + + def async_update_values(self, unknown_state=False): + """Update this sensor.""" + update = False + + if self._sensor.value != self._state: + update = True + self._state = self._sensor.value + + if unknown_state and self._state is not None: + update = True + self._state = None + + return self.async_update_ha_state() if update else None + + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return f"{self._sensor.name}" diff --git a/requirements_all.txt b/requirements_all.txt index 561e3345417251..ef1b56222b5c9c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1410,6 +1410,9 @@ pyrepetier==3.0.5 # homeassistant.components.sabnzbd pysabnzbd==1.1.0 +# homeassistant.components.saj +pysaj==0.0.9 + # homeassistant.components.sony_projector pysdcp==1 From f4a1f2809bdbcd74dd1bf6476467f474912ed041 Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Tue, 1 Oct 2019 22:15:15 +1000 Subject: [PATCH 0531/3953] Add availability_template to Template Lock platform (#26517) * Added availability_template to Template Lock platform * Added to test for invalid values in availability_template * Black and Lint fix * black formatting * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Fix import order (pylint) * Moved availability_template rendering to common loop * Brought contant into line * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * Fixed tests (async, magic values and state checks) --- homeassistant/components/template/lock.py | 40 ++++++++++++- tests/components/template/test_lock.py | 70 ++++++++++++++++++++++- 2 files changed, 104 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index d7f501987f9527..aa8cc8b1224e3f 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -19,6 +19,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -34,6 +35,7 @@ vol.Required(CONF_LOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_UNLOCK): cv.SCRIPT_SCHEMA, vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_AVAILABILITY_TEMPLATE): cv.template, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, } ) @@ -48,21 +50,32 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=N if value_template_entity_ids == MATCH_ALL: _LOGGER.warning( - "Template lock %s has no entity ids configured to track nor " - "were we able to extract the entities to track from the %s " + "Template lock '%s' has no entity ids configured to track nor " + "were we able to extract the entities to track from the '%s' " "template. This entity will only be able to be updated " "manually.", name, CONF_VALUE_TEMPLATE, ) + template_entity_ids = set() + template_entity_ids |= set(value_template_entity_ids) + + availability_template = config.get(CONF_AVAILABILITY_TEMPLATE) + if availability_template is not None: + availability_template.hass = hass + temp_ids = availability_template.extract_entities() + if str(temp_ids) != MATCH_ALL: + template_entity_ids |= set(temp_ids) + async_add_devices( [ TemplateLock( hass, name, value_template, - value_template_entity_ids, + availability_template, + template_entity_ids, config.get(CONF_LOCK), config.get(CONF_UNLOCK), config.get(CONF_OPTIMISTIC), @@ -79,6 +92,7 @@ def __init__( hass, name, value_template, + availability_template, entity_ids, command_lock, command_unlock, @@ -89,10 +103,12 @@ def __init__( self._hass = hass self._name = name self._state_template = value_template + self._availability_template = availability_template self._state_entities = entity_ids self._command_lock = Script(hass, command_lock) self._command_unlock = Script(hass, command_unlock) self._optimistic = optimistic + self._available = True async def async_added_to_hass(self): """Register callbacks.""" @@ -136,6 +152,11 @@ def is_locked(self): """Return true if lock is locked.""" return self._state + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._available + async def async_update(self): """Update the state from the template.""" try: @@ -148,6 +169,19 @@ async def async_update(self): self._state = None _LOGGER.error("Could not render template %s: %s", self._name, ex) + if self._availability_template is not None: + try: + self._available = ( + self._availability_template.async_render().lower() == "true" + ) + except (TemplateError, ValueError) as ex: + _LOGGER.error( + "Could not render %s template %s: %s", + CONF_AVAILABILITY_TEMPLATE, + self._name, + ex, + ) + async def async_lock(self, **kwargs): """Lock the device.""" if self._optimistic: diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 24cde24051a69a..d1d3020737549b 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -5,7 +5,7 @@ from homeassistant import setup from homeassistant.components import lock from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from tests.common import get_test_home_assistant, assert_setup_component @@ -254,9 +254,9 @@ def test_no_template_match_all(self, caplog): assert state.state == lock.STATE_UNLOCKED assert ( - "Template lock Template Lock has no entity ids configured " + "Template lock 'Template Lock' has no entity ids configured " "to track nor were we able to extract the entities to track " - "from the value_template template. This entity will only " + "from the 'value_template' template. This entity will only " "be able to be updated manually." ) in caplog.text @@ -332,3 +332,67 @@ def test_unlock_action(self): self.hass.block_till_done() assert len(self.calls) == 1 + + +async def test_available_template_with_entities(hass): + """Test availability templates with values from other entities.""" + + await setup.async_setup_component( + hass, + "lock", + { + "lock": { + "platform": "template", + "value_template": "{{ 'on' }}", + "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, + "unlock": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + "availability_template": "{{ is_state('availability_state.state', 'on') }}", + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + # When template returns true.. + hass.states.async_set("availability_state.state", STATE_ON) + await hass.async_block_till_done() + + # Device State should not be unavailable + assert hass.states.get("lock.template_lock").state != STATE_UNAVAILABLE + + # When Availability template returns false + hass.states.async_set("availability_state.state", STATE_OFF) + await hass.async_block_till_done() + + # device state should be unavailable + assert hass.states.get("lock.template_lock").state == STATE_UNAVAILABLE + + +async def test_invalid_availability_template_keeps_component_available(hass, caplog): + """Test that an invalid availability keeps the device available.""" + await setup.async_setup_component( + hass, + "lock", + { + "lock": { + "platform": "template", + "value_template": "{{ 1 + 1 }}", + "availability_template": "{{ x - 12 }}", + "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, + "unlock": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert hass.states.get("lock.template_lock").state != STATE_UNAVAILABLE + assert ("UndefinedError: 'x' is undefined") in caplog.text From c1851a2d94d724ceee0eab56249623fe2d92a606 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 1 Oct 2019 16:59:06 +0200 Subject: [PATCH 0532/3953] Cleanup coroutine threadsafe (#27080) * Cleanup coroutine threadsafe * fix lint * Fix typing * Fix tests * Fix black --- .../bluetooth_le_tracker/device_tracker.py | 4 +- homeassistant/components/generic/camera.py | 3 +- homeassistant/components/group/__init__.py | 5 +- homeassistant/components/mqtt/__init__.py | 4 +- homeassistant/components/proxy/camera.py | 3 +- homeassistant/core.py | 12 +- homeassistant/helpers/entity_platform.py | 4 +- homeassistant/helpers/script.py | 6 +- homeassistant/helpers/service.py | 5 +- homeassistant/helpers/state.py | 3 +- homeassistant/setup.py | 3 +- homeassistant/util/async_.py | 16 +-- homeassistant/util/logging.py | 6 +- tests/common.py | 8 +- tests/components/camera/test_init.py | 9 +- tests/components/group/test_notify.py | 6 +- tests/components/homeassistant/test_init.py | 4 +- .../media_player/test_async_helpers.py | 41 ++++--- tests/components/rest/test_switch.py | 49 +++++--- .../components/universal/test_media_player.py | 116 ++++++++++-------- tests/components/uptime/test_sensor.py | 26 ++-- tests/test_core.py | 15 ++- tests/util/test_async.py | 80 ------------ 23 files changed, 196 insertions(+), 232 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 8cba3032f54cb4..29eecdfd07741c 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -1,4 +1,5 @@ """Tracking for bluetooth low energy devices.""" +import asyncio import logging from homeassistant.helpers.event import track_point_in_utc_time @@ -14,7 +15,6 @@ ) from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.util.dt as dt_util -from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -89,7 +89,7 @@ def discover_ble_devices(): # Load all known devices. # We just need the devices so set consider_home and home range # to 0 - for device in run_coroutine_threadsafe( + for device in asyncio.run_coroutine_threadsafe( async_load_config(yaml_path, hass, 0), hass.loop ).result(): # check if device is a valid bluetooth device diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 307142ed98964f..01d2fb948eda1e 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -26,7 +26,6 @@ ) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv -from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -105,7 +104,7 @@ def frame_interval(self): def camera_image(self): """Return bytes of camera image.""" - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop ).result() diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 204fcab0381aeb..39574a2b03b7e2 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.helpers.typing import HomeAssistantType -from homeassistant.util.async_ import run_coroutine_threadsafe # mypy: allow-untyped-calls, allow-untyped-defs @@ -430,7 +429,7 @@ def create_group( mode=None, ): """Initialize a group.""" - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( Group.async_create_group( hass, name, @@ -546,7 +545,7 @@ def assumed_state(self): def update_tracked_entity_ids(self, entity_ids): """Update the member entity IDs.""" - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.async_update_tracked_entity_ids(entity_ids), self.hass.loop ).result() diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 8d83cd0cc2ba0f..9b25a6ef6e4817 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -39,7 +39,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.loader import bind_hass -from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow @@ -463,7 +463,7 @@ def subscribe( encoding: str = "utf-8", ) -> Callable[[], None]: """Subscribe to an MQTT topic.""" - async_remove = run_coroutine_threadsafe( + async_remove = asyncio.run_coroutine_threadsafe( async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop ).result() diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 53a4f620dcc3d4..b1ce8ad7ac0466 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -9,7 +9,6 @@ from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -220,7 +219,7 @@ def __init__(self, hass, config): def camera_image(self): """Return camera image.""" - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( self.async_camera_image(), self.hass.loop ).result() diff --git a/homeassistant/core.py b/homeassistant/core.py index f4be3b66323d1b..feb4445d36d5bd 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -64,11 +64,7 @@ Unauthorized, ServiceNotFound, ) -from homeassistant.util.async_ import ( - run_coroutine_threadsafe, - run_callback_threadsafe, - fire_coroutine_threadsafe, -) +from homeassistant.util.async_ import run_callback_threadsafe, fire_coroutine_threadsafe from homeassistant import util import homeassistant.util.dt as dt_util from homeassistant.util import location, slugify @@ -375,7 +371,9 @@ def async_run_job(self, target: Callable[..., None], *args: Any) -> None: def block_till_done(self) -> None: """Block till all pending work is done.""" - run_coroutine_threadsafe(self.async_block_till_done(), self.loop).result() + asyncio.run_coroutine_threadsafe( + self.async_block_till_done(), self.loop + ).result() async def async_block_till_done(self) -> None: """Block till all pending work is done.""" @@ -1168,7 +1166,7 @@ def call( Because the service is sent as an event you are not allowed to use the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. """ - return run_coroutine_threadsafe( # type: ignore + return asyncio.run_coroutine_threadsafe( self.async_call(domain, service, service_data, blocking, context), self._hass.loop, ).result() diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 7d5debd484d4e8..5c59dc6c13e3a0 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -6,7 +6,7 @@ from homeassistant.const import DEVICE_DEFAULT_NAME 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 homeassistant.util.async_ import run_callback_threadsafe from .entity_registry import DISABLED_INTEGRATION from .event import async_track_time_interval, async_call_later @@ -220,7 +220,7 @@ def add_entities(self, new_entities, update_before_add=False): "only inside tests or you can run into a deadlock!" ) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.async_add_entities(list(new_entities), update_before_add), self.hass.loop, ).result() diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 14ff873d4d173b..a4f6afa16366af 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,5 +1,5 @@ """Helpers to execute scripts.""" - +import asyncio import logging from contextlib import suppress from datetime import datetime @@ -29,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration import homeassistant.util.dt as date_util -from homeassistant.util.async_ import run_coroutine_threadsafe, run_callback_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -136,7 +136,7 @@ def is_running(self) -> bool: def run(self, variables=None, context=None): """Run script.""" - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.async_run(variables, context), self.hass.loop ).result() diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index f29d1885d1e145..e177c86c65c971 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -20,7 +20,6 @@ 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 @@ -42,7 +41,7 @@ def call_from_config( hass, config, blocking=False, variables=None, validate_config=True ): """Call a service based on a config hash.""" - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( async_call_from_config(hass, config, blocking, variables, validate_config), hass.loop, ).result() @@ -105,7 +104,7 @@ def extract_entity_ids(hass, service_call, expand_group=True): Will convert group entity ids to the entity ids it represents. """ - return run_coroutine_threadsafe( + return asyncio.run_coroutine_threadsafe( async_extract_entity_ids(hass, service_call, expand_group), hass.loop ).result() diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 7f9692b3380264..2f49a566a32e5e 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -44,7 +44,6 @@ SERVICE_SELECT_OPTION, ) from homeassistant.core import Context, State, DOMAIN as HASS_DOMAIN -from homeassistant.util.async_ import run_coroutine_threadsafe from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -122,7 +121,7 @@ def reproduce_state( blocking: bool = False, ) -> None: """Reproduce given state.""" - return run_coroutine_threadsafe( # type: ignore + return asyncio.run_coroutine_threadsafe( async_reproduce_state(hass, states, blocking), hass.loop ).result() diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 58e4fc19eb0ee8..07de3b2942d061 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -10,7 +10,6 @@ from homeassistant.config import async_notify_setup_error from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.async_ import run_coroutine_threadsafe _LOGGER = logging.getLogger(__name__) @@ -25,7 +24,7 @@ def setup_component(hass: core.HomeAssistant, domain: str, config: Dict) -> bool: """Set up a component and all its dependencies.""" - return run_coroutine_threadsafe( # type: ignore + return asyncio.run_coroutine_threadsafe( async_setup_component(hass, domain, config), hass.loop ).result() diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 6920e0d97f64cd..64bedfe2501676 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -7,7 +7,7 @@ import asyncio from asyncio import ensure_future -from typing import Any, Union, Coroutine, Callable, Generator, TypeVar, Awaitable +from typing import Any, Coroutine, Callable, TypeVar, Awaitable _LOGGER = logging.getLogger(__name__) @@ -30,20 +30,6 @@ def asyncio_run(main: Awaitable[_T], *, debug: bool = False) -> _T: loop.close() -def run_coroutine_threadsafe( - coro: Union[Coroutine, Generator], loop: AbstractEventLoop -) -> concurrent.futures.Future: - """Submit a coroutine object to a given event loop. - - Return a concurrent.futures.Future to access the result. - """ - ident = loop.__dict__.get("_thread_ident") - if ident is not None and ident == threading.get_ident(): - raise RuntimeError("Cannot be called from within the event loop") - - return asyncio.run_coroutine_threadsafe(coro, loop) - - def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None: """Submit a coroutine object to a given event loop. diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 79cb2607b1045f..99e606d28662ef 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -8,8 +8,6 @@ import traceback from typing import Any, Callable, Coroutine, Optional -from .async_ import run_coroutine_threadsafe - class HideSensitiveDataFilter(logging.Filter): """Filter API password calls.""" @@ -83,7 +81,9 @@ def __repr__(self) -> str: def _process(self) -> None: """Process log in a thread.""" while True: - record = run_coroutine_threadsafe(self._queue.get(), self.loop).result() + record = asyncio.run_coroutine_threadsafe( + self._queue.get(), self.loop + ).result() if record is None: self.handler.close() diff --git a/tests/common.py b/tests/common.py index bc39b1f5e0b62a..1982e80dfe949e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -53,7 +53,7 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.unit_system import METRIC_SYSTEM -from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe +from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.components.device_automation import ( # noqa _async_get_device_automations as async_get_device_automations, ) @@ -92,7 +92,9 @@ def threadsafe_coroutine_factory(func): def threadsafe(*args, **kwargs): """Call func threadsafe.""" hass = args[0] - return run_coroutine_threadsafe(func(*args, **kwargs), hass.loop).result() + return asyncio.run_coroutine_threadsafe( + func(*args, **kwargs), hass.loop + ).result() return threadsafe @@ -125,7 +127,7 @@ def run_loop(): def start_hass(*mocks): """Start hass.""" - run_coroutine_threadsafe(hass.async_start(), loop).result() + asyncio.run_coroutine_threadsafe(hass.async_start(), loop).result() def stop_hass(): """Stop hass.""" diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 09793b4303e4c8..17bcaadb92b6ab 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -17,7 +17,6 @@ from homeassistant.components.camera.prefs import CameraEntityPreferences from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, @@ -110,7 +109,7 @@ def test_get_image_from_camera(self, mock_camera): """Grab an image from camera entity.""" self.hass.start() - image = run_coroutine_threadsafe( + image = asyncio.run_coroutine_threadsafe( camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop ).result() @@ -123,7 +122,7 @@ def test_get_image_without_exists_camera(self): "homeassistant.helpers.entity_component.EntityComponent." "get_entity", return_value=None, ), pytest.raises(HomeAssistantError): - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop ).result() @@ -133,7 +132,7 @@ def test_get_image_with_timeout(self): "homeassistant.components.camera.Camera.async_camera_image", side_effect=asyncio.TimeoutError, ), pytest.raises(HomeAssistantError): - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop ).result() @@ -143,7 +142,7 @@ def test_get_image_fails(self): "homeassistant.components.camera.Camera.async_camera_image", return_value=mock_coro(None), ), pytest.raises(HomeAssistantError): - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( camera.async_get_image(self.hass, "camera.demo_camera"), self.hass.loop ).result() diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index 75a151d7868b8f..d7b7496573bebd 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,4 +1,5 @@ """The tests for the notify.group platform.""" +import asyncio import unittest from unittest.mock import MagicMock, patch @@ -6,7 +7,6 @@ import homeassistant.components.notify as notify import homeassistant.components.group.notify as group import homeassistant.components.demo.notify as demo -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import assert_setup_component, get_test_home_assistant @@ -43,7 +43,7 @@ def mock_get_service(hass, config, discovery_info=None): }, ) - self.service = run_coroutine_threadsafe( + self.service = asyncio.run_coroutine_threadsafe( group.async_get_service( self.hass, { @@ -70,7 +70,7 @@ def tearDown(self): # pylint: disable=invalid-name def test_send_message_with_data(self): """Test sending a message with to a notify group.""" - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.service.async_send_message( "Hello", title="Test notification", data={"hello": "world"} ), diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index e6f05cc2be098d..7a97de0f68e42f 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -1,5 +1,6 @@ """The tests for Core components.""" # pylint: disable=protected-access +import asyncio import unittest from unittest.mock import patch, Mock @@ -27,7 +28,6 @@ import homeassistant.helpers.intent as intent from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( get_test_home_assistant, @@ -111,7 +111,7 @@ class TestComponentsCore(unittest.TestCase): def setUp(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - assert run_coroutine_threadsafe( + assert asyncio.run_coroutine_threadsafe( async_setup_component(self.hass, "homeassistant", {}), self.hass.loop ).result() diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index a12b9af0ebd3d0..4a2e4fed6c565d 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -10,7 +10,6 @@ STATE_OFF, STATE_IDLE, ) -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import get_test_home_assistant @@ -162,21 +161,23 @@ def tearDown(self): def test_volume_up(self): """Test the volume_up helper function.""" assert self.player.volume_level == 0 - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_set_volume_level(0.5), self.hass.loop ).result() assert self.player.volume_level == 0.5 - run_coroutine_threadsafe(self.player.async_volume_up(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_volume_up(), self.hass.loop + ).result() assert self.player.volume_level == 0.6 def test_volume_down(self): """Test the volume_down helper function.""" assert self.player.volume_level == 0 - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_set_volume_level(0.5), self.hass.loop ).result() assert self.player.volume_level == 0.5 - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_volume_down(), self.hass.loop ).result() assert self.player.volume_level == 0.4 @@ -184,11 +185,11 @@ def test_volume_down(self): def test_media_play_pause(self): """Test the media_play_pause helper function.""" assert self.player.state == STATE_OFF - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_media_play_pause(), self.hass.loop ).result() assert self.player.state == STATE_PLAYING - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_media_play_pause(), self.hass.loop ).result() assert self.player.state == STATE_PAUSED @@ -196,9 +197,13 @@ def test_media_play_pause(self): def test_toggle(self): """Test the toggle helper function.""" assert self.player.state == STATE_OFF - run_coroutine_threadsafe(self.player.async_toggle(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_toggle(), self.hass.loop + ).result() assert self.player.state == STATE_ON - run_coroutine_threadsafe(self.player.async_toggle(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_toggle(), self.hass.loop + ).result() assert self.player.state == STATE_OFF @@ -219,7 +224,9 @@ def test_volume_up(self): assert self.player.volume_level == 0 self.player.set_volume_level(0.5) assert self.player.volume_level == 0.5 - run_coroutine_threadsafe(self.player.async_volume_up(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_volume_up(), self.hass.loop + ).result() assert self.player.volume_level == 0.7 def test_volume_down(self): @@ -227,7 +234,7 @@ def test_volume_down(self): assert self.player.volume_level == 0 self.player.set_volume_level(0.5) assert self.player.volume_level == 0.5 - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_volume_down(), self.hass.loop ).result() assert self.player.volume_level == 0.3 @@ -235,11 +242,11 @@ def test_volume_down(self): def test_media_play_pause(self): """Test the media_play_pause helper function.""" assert self.player.state == STATE_OFF - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_media_play_pause(), self.hass.loop ).result() assert self.player.state == STATE_PLAYING - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( self.player.async_media_play_pause(), self.hass.loop ).result() assert self.player.state == STATE_PAUSED @@ -247,7 +254,11 @@ def test_media_play_pause(self): def test_toggle(self): """Test the toggle helper function.""" assert self.player.state == STATE_OFF - run_coroutine_threadsafe(self.player.async_toggle(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_toggle(), self.hass.loop + ).result() assert self.player.state == STATE_ON - run_coroutine_threadsafe(self.player.async_toggle(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.player.async_toggle(), self.hass.loop + ).result() assert self.player.state == STATE_OFF diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index ced2f512b49651..81430cff349db4 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -5,7 +5,6 @@ import homeassistant.components.rest.switch as rest from homeassistant.setup import setup_component -from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers.template import Template from tests.common import get_test_home_assistant, assert_setup_component @@ -23,14 +22,14 @@ def teardown_method(self): def test_setup_missing_config(self): """Test setup with configuration missing required entries.""" - assert not run_coroutine_threadsafe( + assert not asyncio.run_coroutine_threadsafe( rest.async_setup_platform(self.hass, {"platform": "rest"}, None), self.hass.loop, ).result() def test_setup_missing_schema(self): """Test setup with resource missing schema.""" - assert not run_coroutine_threadsafe( + assert not asyncio.run_coroutine_threadsafe( rest.async_setup_platform( self.hass, {"platform": "rest", "resource": "localhost"}, None ), @@ -40,7 +39,7 @@ def test_setup_missing_schema(self): def test_setup_failed_connect(self, aioclient_mock): """Test setup when connection error occurs.""" aioclient_mock.get("http://localhost", exc=aiohttp.ClientError) - assert not run_coroutine_threadsafe( + assert not asyncio.run_coroutine_threadsafe( rest.async_setup_platform( self.hass, {"platform": "rest", "resource": "http://localhost"}, None ), @@ -50,7 +49,7 @@ def test_setup_failed_connect(self, aioclient_mock): def test_setup_timeout(self, aioclient_mock): """Test setup when connection timeout occurs.""" aioclient_mock.get("http://localhost", exc=asyncio.TimeoutError()) - assert not run_coroutine_threadsafe( + assert not asyncio.run_coroutine_threadsafe( rest.async_setup_platform( self.hass, {"platform": "rest", "resource": "http://localhost"}, None ), @@ -131,7 +130,9 @@ def test_is_on_before_update(self): def test_turn_on_success(self, aioclient_mock): """Test turn_on.""" aioclient_mock.post(self.resource, status=200) - run_coroutine_threadsafe(self.switch.async_turn_on(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop + ).result() assert self.body_on.template == aioclient_mock.mock_calls[-1][2].decode() assert self.switch.is_on @@ -139,7 +140,9 @@ def test_turn_on_success(self, aioclient_mock): def test_turn_on_status_not_ok(self, aioclient_mock): """Test turn_on when error status returned.""" aioclient_mock.post(self.resource, status=500) - run_coroutine_threadsafe(self.switch.async_turn_on(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop + ).result() assert self.body_on.template == aioclient_mock.mock_calls[-1][2].decode() assert self.switch.is_on is None @@ -147,14 +150,18 @@ def test_turn_on_status_not_ok(self, aioclient_mock): def test_turn_on_timeout(self, aioclient_mock): """Test turn_on when timeout occurs.""" aioclient_mock.post(self.resource, status=500) - run_coroutine_threadsafe(self.switch.async_turn_on(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop + ).result() assert self.switch.is_on is None def test_turn_off_success(self, aioclient_mock): """Test turn_off.""" aioclient_mock.post(self.resource, status=200) - run_coroutine_threadsafe(self.switch.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_off(), self.hass.loop + ).result() assert self.body_off.template == aioclient_mock.mock_calls[-1][2].decode() assert not self.switch.is_on @@ -162,7 +169,9 @@ def test_turn_off_success(self, aioclient_mock): def test_turn_off_status_not_ok(self, aioclient_mock): """Test turn_off when error status returned.""" aioclient_mock.post(self.resource, status=500) - run_coroutine_threadsafe(self.switch.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_off(), self.hass.loop + ).result() assert self.body_off.template == aioclient_mock.mock_calls[-1][2].decode() assert self.switch.is_on is None @@ -170,34 +179,44 @@ def test_turn_off_status_not_ok(self, aioclient_mock): def test_turn_off_timeout(self, aioclient_mock): """Test turn_off when timeout occurs.""" aioclient_mock.post(self.resource, exc=asyncio.TimeoutError()) - run_coroutine_threadsafe(self.switch.async_turn_on(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_turn_on(), self.hass.loop + ).result() assert self.switch.is_on is None def test_update_when_on(self, aioclient_mock): """Test update when switch is on.""" aioclient_mock.get(self.resource, text=self.body_on.template) - run_coroutine_threadsafe(self.switch.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop + ).result() assert self.switch.is_on def test_update_when_off(self, aioclient_mock): """Test update when switch is off.""" aioclient_mock.get(self.resource, text=self.body_off.template) - run_coroutine_threadsafe(self.switch.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop + ).result() assert not self.switch.is_on def test_update_when_unknown(self, aioclient_mock): """Test update when unknown status returned.""" aioclient_mock.get(self.resource, text="unknown status") - run_coroutine_threadsafe(self.switch.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop + ).result() assert self.switch.is_on is None def test_update_timeout(self, aioclient_mock): """Test update when timeout occurs.""" aioclient_mock.get(self.resource, exc=asyncio.TimeoutError()) - run_coroutine_threadsafe(self.switch.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + self.switch.async_update(), self.hass.loop + ).result() assert self.switch.is_on is None diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index fd6c0f73303988..67d826f576b5d7 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -1,4 +1,5 @@ """The tests for the Universal Media player platform.""" +import asyncio from copy import copy import unittest @@ -10,7 +11,6 @@ import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player import homeassistant.components.universal.media_player as universal -from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import mock_service, get_test_home_assistant @@ -298,7 +298,7 @@ def add_entities(new_entities): setup_ok = True try: - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( universal.async_setup_platform( self.hass, validate_config(bad_config), add_entities ), @@ -309,7 +309,7 @@ def add_entities(new_entities): assert not setup_ok assert 0 == len(entities) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( universal.async_setup_platform( self.hass, validate_config(config), add_entities ), @@ -369,26 +369,26 @@ def test_active_child_state(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump._child_state is None self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert self.mock_mp_1.entity_id == ump._child_state.entity_id self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert self.mock_mp_1.entity_id == ump._child_state.entity_id self.mock_mp_1._state = STATE_OFF self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert self.mock_mp_2.entity_id == ump._child_state.entity_id def test_name(self): @@ -413,14 +413,14 @@ def test_state_children_only(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.state, STATE_OFF self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_PLAYING == ump.state def test_state_with_children_and_attrs(self): @@ -429,22 +429,22 @@ def test_state_with_children_and_attrs(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_OFF == ump.state self.hass.states.set(self.mock_state_switch_id, STATE_ON) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_ON == ump.state self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_PLAYING == ump.state self.hass.states.set(self.mock_state_switch_id, STATE_OFF) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert STATE_OFF == ump.state def test_volume_level(self): @@ -453,20 +453,20 @@ def test_volume_level(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.volume_level is None self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 0 == ump.volume_level self.mock_mp_1._volume_level = 1 self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 1 == ump.volume_level def test_media_image_url(self): @@ -476,7 +476,7 @@ def test_media_image_url(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.media_image_url is None @@ -484,7 +484,7 @@ def test_media_image_url(self): self.mock_mp_1._media_image_url = test_url self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() # mock_mp_1 will convert the url to the api proxy url. This test # ensures ump passes through the same url without an additional proxy. assert self.mock_mp_1.entity_picture == ump.entity_picture @@ -495,20 +495,20 @@ def test_is_volume_muted_children_only(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert not ump.is_volume_muted self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert not ump.is_volume_muted self.mock_mp_1._is_volume_muted = True self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert ump.is_volume_muted def test_source_list_children_and_attr(self): @@ -561,7 +561,7 @@ def test_supported_features_children_only(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 0 == ump.supported_features @@ -569,7 +569,7 @@ def test_supported_features_children_only(self): self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() assert 512 == ump.supported_features def test_supported_features_children_and_cmds(self): @@ -590,12 +590,12 @@ def test_supported_features_children_and_cmds(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_1._state = STATE_PLAYING self.mock_mp_1.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() check_flags = ( universal.SUPPORT_TURN_ON @@ -615,16 +615,16 @@ def test_service_call_no_active_child(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_1._state = STATE_OFF self.mock_mp_1.schedule_update_ha_state() self.mock_mp_2._state = STATE_OFF self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() assert 0 == len(self.mock_mp_1.service_calls["turn_off"]) assert 0 == len(self.mock_mp_2.service_calls["turn_off"]) @@ -634,67 +634,85 @@ def test_service_call_to_child(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() assert 1 == len(self.mock_mp_2.service_calls["turn_off"]) - run_coroutine_threadsafe(ump.async_turn_on(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_on(), self.hass.loop).result() assert 1 == len(self.mock_mp_2.service_calls["turn_on"]) - run_coroutine_threadsafe(ump.async_mute_volume(True), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_mute_volume(True), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["mute_volume"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_set_volume_level(0.5), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["set_volume_level"]) - run_coroutine_threadsafe(ump.async_media_play(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_play(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_play"]) - run_coroutine_threadsafe(ump.async_media_pause(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_pause(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_pause"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_media_previous_track(), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["media_previous_track"]) - run_coroutine_threadsafe(ump.async_media_next_track(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_next_track(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_next_track"]) - run_coroutine_threadsafe(ump.async_media_seek(100), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_seek(100), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_seek"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_play_media("movie", "batman"), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["play_media"]) - run_coroutine_threadsafe(ump.async_volume_up(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_volume_up(), self.hass.loop).result() assert 1 == len(self.mock_mp_2.service_calls["volume_up"]) - run_coroutine_threadsafe(ump.async_volume_down(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_volume_down(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["volume_down"]) - run_coroutine_threadsafe(ump.async_media_play_pause(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_media_play_pause(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["media_play_pause"]) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( ump.async_select_source("dvd"), self.hass.loop ).result() assert 1 == len(self.mock_mp_2.service_calls["select_source"]) - run_coroutine_threadsafe(ump.async_clear_playlist(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_clear_playlist(), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["clear_playlist"]) - run_coroutine_threadsafe(ump.async_set_shuffle(True), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + ump.async_set_shuffle(True), self.hass.loop + ).result() assert 1 == len(self.mock_mp_2.service_calls["shuffle_set"]) def test_service_call_to_command(self): @@ -707,12 +725,12 @@ def test_service_call_to_command(self): ump = universal.UniversalMediaPlayer(self.hass, **config) ump.entity_id = media_player.ENTITY_ID_FORMAT.format(config["name"]) - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() self.mock_mp_2._state = STATE_PLAYING self.mock_mp_2.schedule_update_ha_state() self.hass.block_till_done() - run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_update(), self.hass.loop).result() - run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe(ump.async_turn_off(), self.hass.loop).result() assert 1 == len(service) diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index 32d73c70d4569e..b3dcddfba6a290 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -1,9 +1,9 @@ """The tests for the uptime sensor platform.""" +import asyncio import unittest from unittest.mock import patch from datetime import timedelta -from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.setup import setup_component from homeassistant.components.uptime.sensor import UptimeSensor from tests.common import get_test_home_assistant @@ -46,11 +46,15 @@ def test_uptime_sensor_days_output(self): assert sensor.unit_of_measurement == "days" new_time = sensor.initial + timedelta(days=1) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 1.00 new_time = sensor.initial + timedelta(days=111.499) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 111.50 def test_uptime_sensor_hours_output(self): @@ -59,11 +63,15 @@ def test_uptime_sensor_hours_output(self): assert sensor.unit_of_measurement == "hours" new_time = sensor.initial + timedelta(hours=16) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 16.00 new_time = sensor.initial + timedelta(hours=72.499) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 72.50 def test_uptime_sensor_minutes_output(self): @@ -72,9 +80,13 @@ def test_uptime_sensor_minutes_output(self): assert sensor.unit_of_measurement == "minutes" new_time = sensor.initial + timedelta(minutes=16) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 16.00 new_time = sensor.initial + timedelta(minutes=12.499) with patch("homeassistant.util.dt.now", return_value=new_time): - run_coroutine_threadsafe(sensor.async_update(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + sensor.async_update(), self.hass.loop + ).result() assert sensor.state == 12.50 diff --git a/tests/test_core.py b/tests/test_core.py index e81ce7a4a5a2d8..5ac13027f288a7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -15,7 +15,6 @@ import homeassistant.core as ha from homeassistant.exceptions import InvalidEntityFormatError, InvalidStateError -from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.const import ( @@ -191,7 +190,7 @@ def test_coro(): for _ in range(3): self.hass.add_job(test_coro()) - run_coroutine_threadsafe( + asyncio.run_coroutine_threadsafe( asyncio.wait(self.hass._pending_tasks), loop=self.hass.loop ).result() @@ -216,7 +215,9 @@ def wait_finish_callback(): yield from asyncio.sleep(0) yield from asyncio.sleep(0) - run_coroutine_threadsafe(wait_finish_callback(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop + ).result() assert len(self.hass._pending_tasks) == 2 self.hass.block_till_done() @@ -239,7 +240,9 @@ def wait_finish_callback(): for _ in range(2): self.hass.add_job(test_executor) - run_coroutine_threadsafe(wait_finish_callback(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop + ).result() assert len(self.hass._pending_tasks) == 2 self.hass.block_till_done() @@ -263,7 +266,9 @@ def wait_finish_callback(): for _ in range(2): self.hass.add_job(test_callback) - run_coroutine_threadsafe(wait_finish_callback(), self.hass.loop).result() + asyncio.run_coroutine_threadsafe( + wait_finish_callback(), self.hass.loop + ).result() self.hass.block_till_done() diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 023c42cc81781f..8dede61869c39b 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -9,43 +9,6 @@ from homeassistant.util import async_ as hasync -@patch("asyncio.coroutines.iscoroutine") -@patch("concurrent.futures.Future") -@patch("threading.get_ident") -def test_run_coroutine_threadsafe_from_inside_event_loop( - mock_ident, _, mock_iscoroutine -): - """Testing calling run_coroutine_threadsafe from inside an event loop.""" - coro = MagicMock() - loop = MagicMock() - - loop._thread_ident = None - mock_ident.return_value = 5 - mock_iscoroutine.return_value = True - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 1 - - loop._thread_ident = 5 - mock_ident.return_value = 5 - mock_iscoroutine.return_value = True - with pytest.raises(RuntimeError): - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 1 - - loop._thread_ident = 1 - mock_ident.return_value = 5 - mock_iscoroutine.return_value = False - with pytest.raises(TypeError): - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 1 - - loop._thread_ident = 1 - mock_ident.return_value = 5 - mock_iscoroutine.return_value = True - hasync.run_coroutine_threadsafe(coro, loop) - assert len(loop.call_soon_threadsafe.mock_calls) == 2 - - @patch("asyncio.coroutines.iscoroutine") @patch("concurrent.futures.Future") @patch("threading.get_ident") @@ -187,49 +150,6 @@ def target_coroutine( finally: future.done() or future.cancel() - def test_run_coroutine_threadsafe(self): - """Test coroutine submission from a thread to an event loop.""" - future = self.loop.run_in_executor(None, self.target_coroutine) - result = self.loop.run_until_complete(future) - self.assertEqual(result, 3) - - def test_run_coroutine_threadsafe_with_exception(self): - """Test coroutine submission from thread to event loop on exception.""" - future = self.loop.run_in_executor(None, self.target_coroutine, True) - with self.assertRaises(RuntimeError) as exc_context: - self.loop.run_until_complete(future) - self.assertIn("Fail!", exc_context.exception.args) - - def test_run_coroutine_threadsafe_with_invalid(self): - """Test coroutine submission from thread to event loop on invalid.""" - callback = lambda: self.target_coroutine(invalid=True) # noqa - future = self.loop.run_in_executor(None, callback) - with self.assertRaises(ValueError) as exc_context: - self.loop.run_until_complete(future) - self.assertIn("Invalid!", exc_context.exception.args) - - def test_run_coroutine_threadsafe_with_timeout(self): - """Test coroutine submission from thread to event loop on timeout.""" - callback = lambda: self.target_coroutine(timeout=0) # noqa - future = self.loop.run_in_executor(None, callback) - with self.assertRaises(asyncio.TimeoutError): - self.loop.run_until_complete(future) - self.run_briefly(self.loop) - # Check that there's no pending task (add has been cancelled) - if sys.version_info[:2] >= (3, 7): - all_tasks = asyncio.all_tasks - else: - all_tasks = asyncio.Task.all_tasks - for task in all_tasks(self.loop): - self.assertTrue(task.done()) - - def test_run_coroutine_threadsafe_task_cancelled(self): - """Test coroutine submission from tread to event loop on cancel.""" - callback = lambda: self.target_coroutine(cancel=True) # noqa - future = self.loop.run_in_executor(None, callback) - with self.assertRaises(asyncio.CancelledError): - self.loop.run_until_complete(future) - def test_run_callback_threadsafe(self): """Test callback submission from a thread to an event loop.""" future = self.loop.run_in_executor(None, self.target_callback) From 571ab5a97839a17c41b857fee1220925ce116590 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 1 Oct 2019 10:20:30 -0500 Subject: [PATCH 0533/3953] Plex external config flow (#26936) * Plex external auth config flow * Update requirements_all * Test dependency * Bad await, delay variable * Use hass aiohttp session, bump plexauth * Bump requirements * Bump library version again * Use callback view instead of polling * Update tests for callback view * Reduce timeout with callback * Review feedback * F-string * Wrap sync call * Unused * Revert unnecessary async wrap --- homeassistant/components/plex/config_flow.py | 78 ++++++- homeassistant/components/plex/const.py | 10 + homeassistant/components/plex/manifest.json | 3 +- homeassistant/components/plex/server.py | 12 + homeassistant/components/plex/strings.json | 7 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/plex/mock_classes.py | 4 +- tests/components/plex/test_config_flow.py | 221 +++++++++++++------ 10 files changed, 266 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index cf70b7470cdbcc..dd5401950e9a3c 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -2,10 +2,14 @@ import copy import logging +from aiohttp import web_response import plexapi.exceptions +from plexauth import PlexAuth import requests.exceptions import voluptuous as vol +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import ( @@ -20,6 +24,8 @@ from homeassistant.util.json import load_json from .const import ( # pylint: disable=unused-import + AUTH_CALLBACK_NAME, + AUTH_CALLBACK_PATH, CONF_SERVER, CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, @@ -30,13 +36,15 @@ DOMAIN, PLEX_CONFIG_FILE, PLEX_SERVER_CONFIG, + X_PLEX_DEVICE_NAME, + X_PLEX_VERSION, + X_PLEX_PRODUCT, + X_PLEX_PLATFORM, ) from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer -USER_SCHEMA = vol.Schema( - {vol.Optional(CONF_TOKEN): str, vol.Optional("manual_setup"): bool} -) +USER_SCHEMA = vol.Schema({vol.Optional("manual_setup"): bool}) _LOGGER = logging.getLogger(__package__) @@ -67,6 +75,8 @@ def __init__(self): self.current_login = {} self.discovery_info = {} self.available_servers = None + self.plexauth = None + self.token = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -74,9 +84,8 @@ async def async_step_user(self, user_input=None): if user_input is not None: if user_input.pop("manual_setup", False): return await self.async_step_manual_setup(user_input) - if CONF_TOKEN in user_input: - return await self.async_step_server_validate(user_input) - errors[CONF_TOKEN] = "no_token" + + return await self.async_step_plex_website_auth() return self.async_show_form( step_id="user", data_schema=USER_SCHEMA, errors=errors @@ -225,6 +234,43 @@ async def async_step_import(self, import_config): _LOGGER.debug("Imported Plex configuration") return await self.async_step_server_validate(import_config) + async def async_step_plex_website_auth(self): + """Begin external auth flow on Plex website.""" + self.hass.http.register_view(PlexAuthorizationCallbackView) + payload = { + "X-Plex-Device-Name": X_PLEX_DEVICE_NAME, + "X-Plex-Version": X_PLEX_VERSION, + "X-Plex-Product": X_PLEX_PRODUCT, + "X-Plex-Device": self.hass.config.location_name, + "X-Plex-Platform": X_PLEX_PLATFORM, + "X-Plex-Model": "Plex OAuth", + } + session = async_get_clientsession(self.hass) + self.plexauth = PlexAuth(payload, session) + await self.plexauth.initiate_auth() + forward_url = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}?flow_id={self.flow_id}" + auth_url = self.plexauth.auth_url(forward_url) + return self.async_external_step(step_id="obtain_token", url=auth_url) + + async def async_step_obtain_token(self, user_input=None): + """Obtain token after external auth completed.""" + token = await self.plexauth.token(10) + + if not token: + return self.async_external_step_done(next_step_id="timed_out") + + self.token = token + return self.async_external_step_done(next_step_id="use_external_token") + + async def async_step_timed_out(self, user_input=None): + """Abort flow when time expires.""" + return self.async_abort(reason="token_request_timeout") + + async def async_step_use_external_token(self, user_input=None): + """Continue server validation with external token.""" + server_config = {CONF_TOKEN: self.token} + return await self.async_step_server_validate(server_config) + class PlexOptionsFlowHandler(config_entries.OptionsFlow): """Handle Plex options.""" @@ -263,3 +309,23 @@ async def async_step_plex_mp_settings(self, user_input=None): } ), ) + + +class PlexAuthorizationCallbackView(HomeAssistantView): + """Handle callback from external auth.""" + + url = AUTH_CALLBACK_PATH + name = AUTH_CALLBACK_NAME + requires_auth = False + + async def get(self, request): + """Receive authorization confirmation.""" + hass = request.app["hass"] + await hass.config_entries.flow.async_configure( + flow_id=request.query["flow_id"], user_input=None + ) + + return web_response.Response( + headers={"content-type": "text/html"}, + text="Success! This window can be closed", + ) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 478dd3754e7d8f..0b436c4e208ce4 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -1,4 +1,6 @@ """Constants for the Plex component.""" +from homeassistant.const import __version__ + DOMAIN = "plex" NAME_FORMAT = "Plex {}" @@ -18,3 +20,11 @@ CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" CONF_SHOW_ALL_CONTROLS = "show_all_controls" + +AUTH_CALLBACK_PATH = "/auth/plex/callback" +AUTH_CALLBACK_NAME = "auth:plex:callback" + +X_PLEX_DEVICE_NAME = "Home Assistant" +X_PLEX_PLATFORM = "Home Assistant" +X_PLEX_PRODUCT = "Home Assistant" +X_PLEX_VERSION = __version__ diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 94d990952a684e..137619b27b00b9 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,8 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/plex", "requirements": [ - "plexapi==3.0.6" + "plexapi==3.0.6", + "plexauth==0.0.4" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 09274472915235..d4393d38c97427 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -11,9 +11,21 @@ CONF_SHOW_ALL_CONTROLS, CONF_USE_EPISODE_ART, DEFAULT_VERIFY_SSL, + X_PLEX_DEVICE_NAME, + X_PLEX_PLATFORM, + X_PLEX_PRODUCT, + X_PLEX_VERSION, ) from .errors import NoServersFound, ServerNotSpecified +# Set default headers sent by plexapi +plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME +plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM +plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT +plexapi.X_PLEX_VERSION = X_PLEX_VERSION +plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers() +plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() + class PlexServer: """Manages a single Plex server connection.""" diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 812e7b81a7cf24..6538d8e887e18c 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -21,9 +21,8 @@ }, "user": { "title": "Connect Plex server", - "description": "Enter a Plex token for automatic setup or manually configure a server.", + "description": "Continue to authorize at plex.tv or manually configure a server.", "data": { - "token": "Plex token", "manual_setup": "Manual setup" } } @@ -31,14 +30,14 @@ "error": { "faulty_credentials": "Authorization failed", "no_servers": "No servers linked to account", - "not_found": "Plex server not found", - "no_token": "Provide a token or select manual setup" + "not_found": "Plex server not found" }, "abort": { "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", "invalid_import": "Imported configuration is invalid", + "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" } }, diff --git a/requirements_all.txt b/requirements_all.txt index ef1b56222b5c9c..ee439d455923b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -964,6 +964,9 @@ pizzapi==0.0.3 # homeassistant.components.plex plexapi==3.0.6 +# homeassistant.components.plex +plexauth==0.0.4 + # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7e4ed37e0012f..949da3ad4029bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -261,6 +261,9 @@ pillow==6.1.0 # homeassistant.components.plex plexapi==3.0.6 +# homeassistant.components.plex +plexauth==0.0.4 + # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9991a6bc1f0b60..e35a83bd24d7ea 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -113,6 +113,7 @@ "pilight", "pillow", "plexapi", + "plexauth", "pmsensor", "prometheus_client", "ptvsd", diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index d027087828073c..87fb6df597182f 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -1,9 +1,9 @@ """Mock classes used in tests.""" MOCK_HOST_1 = "1.2.3.4" -MOCK_PORT_1 = "32400" +MOCK_PORT_1 = 32400 MOCK_HOST_2 = "4.3.2.1" -MOCK_PORT_2 = "32400" +MOCK_PORT_2 = 32400 class MockAvailableServer: # pylint: disable=too-few-public-methods diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 37cf0fa200cf94..753d565a82b981 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for Plex config flow.""" from unittest.mock import MagicMock, Mock, patch, PropertyMock + +import asynctest import plexapi.exceptions import requests.exceptions @@ -12,6 +14,7 @@ CONF_TOKEN, CONF_URL, ) +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -49,19 +52,28 @@ async def test_bad_credentials(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": True} + ) + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + with patch( - "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized + "plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized ): - result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + user_input={ + CONF_HOST: MOCK_HOST_1, + CONF_PORT: MOCK_PORT_1, + CONF_SSL: False, + CONF_VERIFY_SSL: False, + CONF_TOKEN: "BAD TOKEN", + }, ) - assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"]["base"] == "faulty_credentials" @@ -92,7 +104,6 @@ async def test_import_file_from_discovery(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "create_entry" assert result["title"] == MOCK_NAME_1 assert result["data"][config_flow.CONF_SERVER] == MOCK_NAME_1 @@ -112,7 +123,6 @@ async def test_discovery(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -129,7 +139,6 @@ async def test_discovery_while_in_progress(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -191,7 +200,6 @@ async def test_import_bad_hostname(hass): CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}", }, ) - assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"]["base"] == "not_found" @@ -203,15 +211,25 @@ async def test_unknown_exception(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" - with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception): - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": "user"}, - data={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": True} + ) + assert result["type"] == "form" + assert result["step_id"] == "manual_setup" + + with patch("plexapi.server.PlexServer", side_effect=Exception): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: MOCK_HOST_1, + CONF_PORT: MOCK_PORT_1, + CONF_SSL: True, + CONF_VERIFY_SSL: True, + CONF_TOKEN: MOCK_TOKEN, + }, ) assert result["type"] == "abort" @@ -221,23 +239,32 @@ async def test_unknown_exception(hass): async def test_no_servers_found(hass): """Test when no servers are on an account.""" + await async_setup_component(hass, "http", {"http": {}}) + result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" mm_plex_account = MagicMock() mm_plex_account.resources = Mock(return_value=[]) - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): + with patch( + "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"]["base"] == "no_servers" @@ -246,10 +273,11 @@ async def test_no_servers_found(hass): async def test_single_available_server(hass): """Test creating an entry with one server available.""" + await async_setup_component(hass, "http", {"http": {}}) + result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -261,7 +289,11 @@ async def test_single_available_server(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer" - ) as mock_plex_server: + ) as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -273,10 +305,14 @@ async def test_single_available_server(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -294,10 +330,11 @@ async def test_single_available_server(hass): async def test_multiple_servers_with_selection(hass): """Test creating an entry with multiple servers available.""" + await async_setup_component(hass, "http", {"http": {}}) + result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -308,7 +345,11 @@ async def test_multiple_servers_with_selection(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer" - ) as mock_plex_server: + ) as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -320,17 +361,20 @@ async def test_multiple_servers_with_selection(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" assert result["step_id"] == "select_server" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={config_flow.CONF_SERVER: MOCK_SERVER_1.name} ) - assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -348,6 +392,8 @@ async def test_multiple_servers_with_selection(hass): async def test_adding_last_unconfigured_server(hass): """Test automatically adding last unconfigured server when multiple servers on account.""" + await async_setup_component(hass, "http", {"http": {}}) + MockConfigEntry( domain=config_flow.DOMAIN, data={ @@ -359,7 +405,6 @@ async def test_adding_last_unconfigured_server(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -370,7 +415,11 @@ async def test_adding_last_unconfigured_server(hass): with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer" - ) as mock_plex_server: + ) as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -382,10 +431,14 @@ async def test_adding_last_unconfigured_server(hass): )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -414,7 +467,9 @@ async def test_already_configured(hass): mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.server.PlexServer") as mock_plex_server: + with patch("plexapi.server.PlexServer") as mock_plex_server, asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN): type(mock_plex_server.return_value).machineIdentifier = PropertyMock( return_value=MOCK_SERVER_1.clientIdentifier ) @@ -424,10 +479,10 @@ async def test_already_configured(hass): type( # pylint: disable=protected-access mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + result = await flow.async_step_import( {CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}"} ) - assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -435,6 +490,8 @@ async def test_already_configured(hass): async def test_all_available_servers_configured(hass): """Test when all available servers are already configured.""" + await async_setup_component(hass, "http", {"http": {}}) + MockConfigEntry( domain=config_flow.DOMAIN, data={ @@ -454,7 +511,6 @@ async def test_all_available_servers_configured(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" @@ -463,13 +519,21 @@ async def test_all_available_servers_configured(hass): mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account): + with patch( + "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TOKEN: MOCK_TOKEN, "manual_setup": False}, + result["flow_id"], user_input={"manual_setup": False} ) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "abort" assert result["reason"] == "all_configured" @@ -480,14 +544,12 @@ async def test_manual_config(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} ) - assert result["type"] == "form" assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_TOKEN: "", "manual_setup": True} + result["flow_id"], user_input={"manual_setup": True} ) - assert result["type"] == "form" assert result["step_id"] == "manual_setup" @@ -508,13 +570,12 @@ async def test_manual_config(hass): result["flow_id"], user_input={ CONF_HOST: MOCK_HOST_1, - CONF_PORT: int(MOCK_PORT_1), + CONF_PORT: MOCK_PORT_1, CONF_SSL: True, CONF_VERIFY_SSL: True, CONF_TOKEN: MOCK_TOKEN, }, ) - assert result["type"] == "create_entry" assert result["title"] == MOCK_SERVER_1.name assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name @@ -529,25 +590,6 @@ async def test_manual_config(hass): assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_no_token(hass): - """Test failing when no token provided.""" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - - assert result["type"] == "form" - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) - - assert result["type"] == "form" - assert result["step_id"] == "user" - assert result["errors"][CONF_TOKEN] == "no_token" - - async def test_option_flow(hass): """Test config flow selection of one of two bridges.""" @@ -557,7 +599,6 @@ async def test_option_flow(hass): result = await hass.config_entries.options.flow.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == "form" assert result["step_id"] == "plex_mp_settings" @@ -575,3 +616,57 @@ async def test_option_flow(hass): config_flow.CONF_SHOW_ALL_CONTROLS: True, } } + + +async def test_external_timed_out(hass): + """Test when external flow times out.""" + + await async_setup_component(hass, "http", {"http": {}}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=None + ): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": False} + ) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "abort" + assert result["reason"] == "token_request_timeout" + + +async def test_callback_view(hass, aiohttp_client): + """Test callback view.""" + + await async_setup_component(hass, "http", {"http": {}}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"manual_setup": False} + ) + assert result["type"] == "external" + + client = await aiohttp_client(hass.http.app) + forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' + + resp = await client.get(forward_url) + assert resp.status == 200 From 3b0744d0214d8cdbf19ecfa95b4a964bc7ae7e34 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 1 Oct 2019 21:19:50 +0200 Subject: [PATCH 0534/3953] Bump attrs to 19.2.0 (#27102) --- 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 47325a6c930e61..684285a1cf10b3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiohttp==3.6.1 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 -attrs==19.1.0 +attrs==19.2.0 bcrypt==3.1.7 certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" diff --git a/requirements_all.txt b/requirements_all.txt index ee439d455923b5..4c963ddab23175 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ aiohttp==3.6.1 astral==1.10.1 async_timeout==3.0.1 -attrs==19.1.0 +attrs==19.2.0 bcrypt==3.1.7 certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" diff --git a/setup.py b/setup.py index 26f112bb008207..d842ae39ae1b77 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ "aiohttp==3.6.1", "astral==1.10.1", "async_timeout==3.0.1", - "attrs==19.1.0", + "attrs==19.2.0", "bcrypt==3.1.7", "certifi>=2019.6.16", 'contextvars==2.4;python_version<"3.7"', From ca9b3b5a4c662c89e5c834bdc4f205d1a860e951 Mon Sep 17 00:00:00 2001 From: rolfberkenbosch <30292281+rolfberkenbosch@users.noreply.github.com> Date: Tue, 1 Oct 2019 21:20:28 +0200 Subject: [PATCH 0535/3953] Update meteoalertapi to version 0.1.6 (#27099) --- homeassistant/components/meteoalarm/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 2148375621307c..692e55260850d3 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -3,7 +3,7 @@ "name": "meteoalarm", "documentation": "https://www.home-assistant.io/components/meteoalarm", "requirements": [ - "meteoalertapi==0.1.5" + "meteoalertapi==0.1.6" ], "dependencies": [], "codeowners": ["@rolfberkenbosch"] diff --git a/requirements_all.txt b/requirements_all.txt index 4c963ddab23175..dd38be3bfbd638 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -805,7 +805,7 @@ mbddns==0.1.2 messagebird==1.2.0 # homeassistant.components.meteoalarm -meteoalertapi==0.1.5 +meteoalertapi==0.1.6 # homeassistant.components.meteo_france meteofrance==0.3.7 From 2e4c92104d5b7b6f354377df8ae3afc51ad7ca66 Mon Sep 17 00:00:00 2001 From: chriscla Date: Tue, 1 Oct 2019 12:51:11 -0700 Subject: [PATCH 0536/3953] Nzbget services (#26900) * Add NZBGet Queue control. * Fix error message * Addressing review comments * Moving import to top of file. --- homeassistant/components/nzbget/__init__.py | 64 ++++++++++++++++++- homeassistant/components/nzbget/services.yaml | 14 ++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/nzbget/services.yaml diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index 37744dce180342..ae3ce7c594479f 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -20,15 +20,26 @@ _LOGGER = logging.getLogger(__name__) +ATTR_SPEED = "speed" + DOMAIN = "nzbget" DATA_NZBGET = "data_nzbget" DATA_UPDATED = "nzbget_data_updated" DEFAULT_NAME = "NZBGet" DEFAULT_PORT = 6789 +DEFAULT_SPEED_LIMIT = 1000 # 1 Megabyte/Sec DEFAULT_SCAN_INTERVAL = timedelta(seconds=5) +SERVICE_PAUSE = "pause" +SERVICE_RESUME = "resume" +SERVICE_SET_SPEED = "set_speed" + +SPEED_LIMIT_SCHEMA = vol.Schema( + {vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int} +) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -51,6 +62,7 @@ def setup(hass, config): """Set up the NZBGet sensors.""" + host = config[DOMAIN][CONF_HOST] port = config[DOMAIN][CONF_PORT] ssl = "s" if config[DOMAIN][CONF_SSL] else "" @@ -71,6 +83,28 @@ def setup(hass, config): nzbget_data = hass.data[DATA_NZBGET] = NZBGetData(hass, nzbget_api) nzbget_data.update() + def service_handler(service): + """Handle service calls.""" + if service.service == SERVICE_PAUSE: + nzbget_data.pause_download() + elif service.service == SERVICE_RESUME: + nzbget_data.resume_download() + elif service.service == SERVICE_SET_SPEED: + limit = service.data[ATTR_SPEED] + nzbget_data.rate(limit) + + hass.services.register( + DOMAIN, SERVICE_PAUSE, service_handler, schema=vol.Schema({}) + ) + + hass.services.register( + DOMAIN, SERVICE_RESUME, service_handler, schema=vol.Schema({}) + ) + + hass.services.register( + DOMAIN, SERVICE_SET_SPEED, service_handler, schema=SPEED_LIMIT_SCHEMA + ) + def refresh(event_time): """Get the latest data from NZBGet.""" nzbget_data.update() @@ -96,10 +130,36 @@ def __init__(self, hass, api): def update(self): """Get the latest data from NZBGet instance.""" + try: self.status = self._api.status() self.available = True dispatcher_send(self.hass, DATA_UPDATED) - except pynzbgetapi.NZBGetAPIException: + except pynzbgetapi.NZBGetAPIException as err: self.available = False - _LOGGER.error("Unable to refresh NZBGet data") + _LOGGER.error("Unable to refresh NZBGet data: %s", err) + + def pause_download(self): + """Pause download queue.""" + + try: + self._api.pausedownload() + except pynzbgetapi.NZBGetAPIException as err: + _LOGGER.error("Unable to pause queue: %s", err) + + def resume_download(self): + """Resume download queue.""" + + try: + self._api.resumedownload() + except pynzbgetapi.NZBGetAPIException as err: + _LOGGER.error("Unable to resume download queue: %s", err) + + def rate(self, limit): + """Set download speed.""" + + try: + if not self._api.rate(limit): + _LOGGER.error("Limit was out of range") + except pynzbgetapi.NZBGetAPIException as err: + _LOGGER.error("Unable to set download speed: %s", err) diff --git a/homeassistant/components/nzbget/services.yaml b/homeassistant/components/nzbget/services.yaml new file mode 100644 index 00000000000000..84e4af15b5d735 --- /dev/null +++ b/homeassistant/components/nzbget/services.yaml @@ -0,0 +1,14 @@ +# Describes the format for available nzbget services + +pause: + description: Pause download queue. + +resume: + description: Resume download queue. + +set_speed: + description: Set download speed limit + fields: + speed: + description: Speed limit in KB/s. 0 is unlimited. + example: 1000 \ No newline at end of file From 8d251c190fd15b5096a6fce038697b4eb72a31aa Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Tue, 1 Oct 2019 22:02:43 +0200 Subject: [PATCH 0537/3953] Delete here_travel_time dead code COORDINATE_SCHEMA (#27090) --- homeassistant/components/here_travel_time/sensor.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index ba4908fe85c3ac..8fd4b4fe94a5cb 100755 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -93,13 +93,6 @@ NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" -COORDINATE_SCHEMA = vol.Schema( - { - vol.Inclusive(CONF_DESTINATION_LATITUDE, "coordinates"): cv.latitude, - vol.Inclusive(CONF_DESTINATION_LONGITUDE, "coordinates"): cv.longitude, - } -) - PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_DESTINATION_LATITUDE, CONF_DESTINATION_ENTITY_ID), cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID), From e033c46c91a265a34e2bf9c5ff299c0f294b4742 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 1 Oct 2019 16:26:33 -0500 Subject: [PATCH 0538/3953] Add missing http dependency (#27097) --- homeassistant/components/plex/manifest.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 137619b27b00b9..8e068c909c5d5e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -7,7 +7,9 @@ "plexapi==3.0.6", "plexauth==0.0.4" ], - "dependencies": [], + "dependencies": [ + "http" + ], "codeowners": [ "@jjlawren" ] From ee45431d0eeb629afe038c72d686a5cc5e4b0792 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Tue, 1 Oct 2019 17:28:13 -0400 Subject: [PATCH 0539/3953] Add entity registry support to ecobee integration (#27088) * Add unique id to platforms Add unique id for binary sensor, climate, and sensor. * Add unique id to weather platform * Simplify unique_id for weather platform * Fix lint for unique_id in sensor * Fix lint for unique_id in binary sensor --- homeassistant/components/ecobee/binary_sensor.py | 9 +++++++++ homeassistant/components/ecobee/climate.py | 5 +++++ homeassistant/components/ecobee/sensor.py | 9 +++++++++ homeassistant/components/ecobee/weather.py | 5 +++++ 4 files changed, 28 insertions(+) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 8b7b819cfc7141..68d8a88df47b64 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -43,6 +43,15 @@ def name(self): """Return the name of the Ecobee sensor.""" return self._name.rstrip() + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] == self.sensor_name: + if "code" in sensor: + return f"{sensor['code']}-{self.device_class}" + return f"{sensor['id']}-{self.device_class}" + @property def is_on(self): """Return the status of the sensor.""" diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 6eccdccf8c6ac0..f930282ba7b90a 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -305,6 +305,11 @@ def name(self): """Return the name of the Ecobee Thermostat.""" return self.thermostat["name"] + @property + def unique_id(self): + """Return a unique identifier for this ecobee thermostat.""" + return self.thermostat["identifier"] + @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index e62d68dc9bcfcd..8cf9af0e3b445f 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -54,6 +54,15 @@ def name(self): """Return the name of the Ecobee sensor.""" return self._name + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] == self.sensor_name: + if "code" in sensor: + return f"{sensor['code']}-{self.device_class}" + return f"{sensor['id']}-{self.device_class}" + @property def device_class(self): """Return the device class of the sensor.""" diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index dd3112b636e0c3..6175405638e846 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -61,6 +61,11 @@ def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return a unique identifier for the weather platform.""" + return self.data.ecobee.get_thermostat(self._index)["identifier"] + @property def condition(self): """Return the current condition.""" From 26d78cab60dfd9c495af8ac5eaf13b664aba2700 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Tue, 1 Oct 2019 23:38:48 +0200 Subject: [PATCH 0540/3953] Update opentherm_gw.climate to match Climate 1.0 (#25931) * Update opentherm_gw.climate to match Climate 1.0 * Change hvac_mode to reflect last active hvac_action --- .../components/opentherm_gw/climate.py | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index d37422d4177a61..fab028560bb66e 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -5,11 +5,14 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_COOL, HVAC_MODE_HEAT, - HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, PRESET_AWAY, + PRESET_NONE, SUPPORT_PRESET_MODE, ) from homeassistant.const import ( @@ -47,8 +50,9 @@ def __init__(self, gw_dev): self.friendly_name = gw_dev.name self.floor_temp = gw_dev.climate_config[CONF_FLOOR_TEMP] self.temp_precision = gw_dev.climate_config.get(CONF_PRECISION) - self._current_operation = HVAC_MODE_OFF + self._current_operation = None self._current_temperature = None + self._hvac_mode = HVAC_MODE_HEAT self._new_target_temperature = None self._target_temperature = None self._away_mode_a = None @@ -70,11 +74,13 @@ def receive_report(self, status): flame_on = status.get(gw_vars.DATA_SLAVE_FLAME_ON) cooling_active = status.get(gw_vars.DATA_SLAVE_COOLING_ACTIVE) if ch_active and flame_on: - self._current_operation = HVAC_MODE_HEAT + self._current_operation = CURRENT_HVAC_HEAT + self._hvac_mode = HVAC_MODE_HEAT elif cooling_active: - self._current_operation = HVAC_MODE_COOL + self._current_operation = CURRENT_HVAC_COOL + self._hvac_mode = HVAC_MODE_COOL else: - self._current_operation = HVAC_MODE_OFF + self._current_operation = CURRENT_HVAC_IDLE self._current_temperature = status.get(gw_vars.DATA_ROOM_TEMP) temp_upd = status.get(gw_vars.DATA_ROOM_SETPOINT) @@ -138,16 +144,25 @@ def temperature_unit(self): """Return the unit of measurement used by the platform.""" return TEMP_CELSIUS + @property + def hvac_action(self): + """Return current HVAC operation.""" + return self._current_operation + @property def hvac_mode(self): """Return current HVAC mode.""" - return self._current_operation + return self._hvac_mode @property def hvac_modes(self): """Return available HVAC modes.""" return [] + def set_hvac_mode(self, hvac_mode): + """Set the HVAC mode.""" + _LOGGER.warning("Changing HVAC mode is not supported") + @property def current_temperature(self): """Return the current temperature.""" @@ -176,6 +191,7 @@ def preset_mode(self): """Return current preset mode.""" if self._away_state_a or self._away_state_b: return PRESET_AWAY + return PRESET_NONE @property def preset_modes(self): From 2090d650c645fb5d3d3c6c8c0f3037a7f9350fe9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 2 Oct 2019 00:32:11 +0000 Subject: [PATCH 0541/3953] [ci skip] Translation update --- .../components/binary_sensor/.translations/ko.json | 8 ++++++-- homeassistant/components/plex/.translations/ca.json | 1 + homeassistant/components/plex/.translations/en.json | 3 ++- homeassistant/components/plex/.translations/ru.json | 1 + homeassistant/components/soma/.translations/ko.json | 13 +++++++++++++ homeassistant/components/soma/.translations/no.json | 13 +++++++++++++ .../components/tellduslive/.translations/no.json | 1 + homeassistant/components/zha/.translations/no.json | 2 +- 8 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/soma/.translations/ko.json create mode 100644 homeassistant/components/soma/.translations/no.json diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json index 02443d449c543f..3c12eabe8ff0c8 100644 --- a/homeassistant/components/binary_sensor/.translations/ko.json +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", + "is_bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc2b5\ub2c8\ub2e4", "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", @@ -18,7 +18,7 @@ "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", + "is_not_bat_low": "{entity_name} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4", "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", @@ -43,6 +43,10 @@ "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4" + }, + "trigger_type": { + "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871", + "closed": "{entity_name} \ub2eb\ud798" } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 14607868907960..11e11ebc6fe85b 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -5,6 +5,7 @@ "already_configured": "Aquest servidor Plex ja est\u00e0 configurat", "already_in_progress": "S\u2019est\u00e0 configurant Plex", "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", + "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del testimoni.", "unknown": "Ha fallat per motiu desconegut" }, "error": { diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index d3c4c0d5a7825b..efdd75b14819ab 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -5,6 +5,7 @@ "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", "invalid_import": "Imported configuration is invalid", + "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" }, "error": { @@ -36,7 +37,7 @@ "manual_setup": "Manual setup", "token": "Plex token" }, - "description": "Enter a Plex token for automatic setup or manually configure a server.", + "description": "Continue to authorize at plex.tv or manually configure a server.", "title": "Connect Plex server" } }, diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index c547e4306b4ad5..2b63840d00198b 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -5,6 +5,7 @@ "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", + "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430", "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" }, "error": { diff --git a/homeassistant/components/soma/.translations/ko.json b/homeassistant/components/soma/.translations/ko.json new file mode 100644 index 00000000000000..53146bebf83c16 --- /dev/null +++ b/homeassistant/components/soma/.translations/ko.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Soma \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "Soma \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + }, + "create_entry": { + "default": "Soma \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json new file mode 100644 index 00000000000000..1ea53b778ea11a --- /dev/null +++ b/homeassistant/components/soma/.translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en Soma-konto.", + "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", + "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + }, + "create_entry": { + "default": "Vellykket autentisering med Somfy." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/no.json b/homeassistant/components/tellduslive/.translations/no.json index 090de51703654f..3258cf2ddcad3a 100644 --- a/homeassistant/components/tellduslive/.translations/no.json +++ b/homeassistant/components/tellduslive/.translations/no.json @@ -18,6 +18,7 @@ "data": { "host": "Vert" }, + "description": "Tom", "title": "Velg endepunkt." } }, diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 95550ca0999966..390069b7698c4e 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -53,7 +53,7 @@ "device_rotated": "Enheten roterte \"{subtype}\"", "device_shaken": "Enhet er ristet", "device_slid": "Enheten skled \"{subtype}\"", - "device_tilted": "Enhet vippet", + "device_tilted": "Enhetn skr\u00e5stilt", "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", From 5a1da72d5ed26d2409d74caddee5610a1f71f700 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Oct 2019 05:35:36 +0200 Subject: [PATCH 0542/3953] Improve validation of device action config (#27029) --- homeassistant/components/automation/config.py | 10 ++++-- homeassistant/helpers/script.py | 29 ++++++++++----- .../components/device_automation/test_init.py | 36 +++++++++++++++++++ 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 04b764e271ca48..3f48e2afde67f8 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -7,10 +7,10 @@ from homeassistant.const import CONF_PLATFORM from homeassistant.config import async_log_exception, config_without_domain from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform +from homeassistant.helpers import config_per_platform, script from homeassistant.loader import IntegrationNotFound -from . import CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA +from . import CONF_ACTION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -32,6 +32,12 @@ async def async_validate_config_item(hass, config, full_config=None): ) triggers.append(trigger) config[CONF_TRIGGER] = triggers + + actions = [] + for action in config[CONF_ACTION]: + action = await script.async_validate_action_config(hass, action) + actions.append(action) + config[CONF_ACTION] = actions except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: async_log_exception(ex, DOMAIN, full_config or config, hass) return None diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index a4f6afa16366af..e383f1013ab204 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -8,13 +8,9 @@ import voluptuous as vol +import homeassistant.components.device_automation as device_automation from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE -from homeassistant.const import ( - CONF_CONDITION, - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_TIMEOUT, -) +from homeassistant.const import CONF_CONDITION, CONF_DEVICE_ID, CONF_TIMEOUT from homeassistant import exceptions from homeassistant.helpers import ( service, @@ -27,7 +23,6 @@ async_track_template, ) from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import async_get_integration import homeassistant.util.dt as date_util from homeassistant.util.async_ import run_callback_threadsafe @@ -86,6 +81,21 @@ def call_from_config( Script(hass, cv.SCRIPT_SCHEMA(config)).run(variables, context) +async def async_validate_action_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + action_type = _determine_action(config) + + if action_type == ACTION_DEVICE_AUTOMATION: + platform = await device_automation.async_get_device_automation_platform( + hass, config, "action" + ) + config = platform.ACTION_SCHEMA(config) + + return config + + class _StopScript(Exception): """Throw if script needs to stop.""" @@ -335,8 +345,9 @@ async def _async_device_automation(self, action, variables, context): """ self.last_action = action.get(CONF_ALIAS, "device automation") self._log("Executing step %s" % self.last_action) - integration = await async_get_integration(self.hass, action[CONF_DOMAIN]) - platform = integration.get_platform("device_action") + platform = await device_automation.async_get_device_automation_platform( + self.hass, action, "action" + ) await platform.async_call_action_from_config( self.hass, action, variables, context ) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 6dcd8391bf8954..acfa853d596f27 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -185,6 +185,25 @@ async def test_automation_with_non_existing_integration(hass, caplog): assert "Integration 'beer' not found" in caplog.text +async def test_automation_with_integration_without_device_action(hass, caplog): + """Test automation with integration without device action support.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": {"device_id": "", "domain": "test"}, + } + }, + ) + + assert ( + "Integration 'test' does not support device automation actions" in caplog.text + ) + + async def test_automation_with_integration_without_device_trigger(hass, caplog): """Test automation with integration without device trigger support.""" assert await async_setup_component( @@ -208,6 +227,23 @@ async def test_automation_with_integration_without_device_trigger(hass, caplog): ) +async def test_automation_with_bad_action(hass, caplog): + """Test automation with bad device action.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": {"device_id": "", "domain": "light"}, + } + }, + ) + + assert "required key not provided" in caplog.text + + async def test_automation_with_bad_trigger(hass, caplog): """Test automation with bad device trigger.""" assert await async_setup_component( From 7d2a8b81370fdf643ce059e8f53d831de442eeaa Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 1 Oct 2019 23:17:30 -0700 Subject: [PATCH 0543/3953] Bump adb-shell to 0.0.3 (#27108) --- 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 c085addad4dd37..dbd76a2bc9b0bd 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": [ - "adb-shell==0.0.2", + "adb-shell==0.0.3", "androidtv==0.0.28" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index dd38be3bfbd638..385f2d0e1a2495 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.2 +adb-shell==0.0.3 # homeassistant.components.adguard adguardhome==0.2.1 From ce2e80339c67f3a9ce6bb230f8345f13da660236 Mon Sep 17 00:00:00 2001 From: Chris Colohan Date: Wed, 2 Oct 2019 00:50:45 -0700 Subject: [PATCH 0544/3953] Add Vera last user and low battery attributes (#27043) * Add in attributes to track when a user unlocks the lock with a PIN, and when the battery runs low. * Vera attributes for who entered PIN on lock, and low battery warning. * Changed last_user_id to use changed_by interface. * Bump pyvera version to 0.3.6; remove guard code for earlier pyvera versions. * Bump pyvera version to 0.3.6 --- homeassistant/components/vera/lock.py | 32 +++++++++++++++++++++ homeassistant/components/vera/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index cf2d6d25c4a340..23b62bb0331466 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -8,6 +8,9 @@ _LOGGER = logging.getLogger(__name__) +ATTR_LAST_USER_NAME = "changed_by_name" +ATTR_LOW_BATTERY = "low_battery" + def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return Vera locks.""" @@ -44,6 +47,35 @@ def is_locked(self): """Return true if device is on.""" return self._state == STATE_LOCKED + @property + def device_state_attributes(self): + """Who unlocked the lock and did a low battery alert fire. + + Reports on the previous poll cycle. + changed_by_name is a string like 'Bob'. + low_battery is 1 if an alert fired, 0 otherwise. + """ + data = super().device_state_attributes + + last_user = self.vera_device.get_last_user_alert() + if last_user is not None: + data[ATTR_LAST_USER_NAME] = last_user[1] + + data[ATTR_LOW_BATTERY] = self.vera_device.get_low_battery_alert() + return data + + @property + def changed_by(self): + """Who unlocked the lock. + + Reports on the previous poll cycle. + changed_by is an integer user ID. + """ + last_user = self.vera_device.get_last_user_alert() + if last_user is not None: + return last_user[0] + return None + def update(self): """Update state by the Vera device callback.""" self._state = ( diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 8365ca1a765218..b2f1581e76f191 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.4" + "pyvera==0.3.6" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 385f2d0e1a2495..245f91dc9c65be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1625,7 +1625,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.4 +pyvera==0.3.6 # homeassistant.components.vesync pyvesync==1.1.0 From 8488b572150a5b3f667efc50848c9ecbd40fe4f8 Mon Sep 17 00:00:00 2001 From: Brendon Baumgartner Date: Wed, 2 Oct 2019 01:25:35 -0700 Subject: [PATCH 0545/3953] Add neural support to amazon polly (#27101) * amazon polly - add neural support * bumped boto3 for route53 to 1.9.233 --- homeassistant/components/amazon_polly/manifest.json | 2 +- homeassistant/components/amazon_polly/tts.py | 10 ++++++++-- homeassistant/components/route53/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 19140aac939682..20d443f225eb18 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -3,7 +3,7 @@ "name": "Amazon polly", "documentation": "https://www.home-assistant.io/components/amazon_polly", "requirements": [ - "boto3==1.9.16" + "boto3==1.9.233" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index c7098867ee899e..64b8b71457cd3f 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -33,6 +33,7 @@ "sa-east-1", ] +CONF_ENGINE = "engine" CONF_VOICE = "voice" CONF_OUTPUT_FORMAT = "output_format" CONF_SAMPLE_RATE = "sample_rate" @@ -101,10 +102,12 @@ SUPPORTED_OUTPUT_FORMATS = ["mp3", "ogg_vorbis", "pcm"] -SUPPORTED_SAMPLE_RATES = ["8000", "16000", "22050"] +SUPPORTED_ENGINES = ["neural", "standard"] + +SUPPORTED_SAMPLE_RATES = ["8000", "16000", "22050", "24000"] SUPPORTED_SAMPLE_RATES_MAP = { - "mp3": ["8000", "16000", "22050"], + "mp3": ["8000", "16000", "22050", "24000"], "ogg_vorbis": ["8000", "16000", "22050"], "pcm": ["8000", "16000"], } @@ -113,6 +116,7 @@ CONTENT_TYPE_EXTENSIONS = {"audio/mpeg": "mp3", "audio/ogg": "ogg", "audio/pcm": "pcm"} +DEFAULT_ENGINE = "standard" DEFAULT_VOICE = "Joanna" DEFAULT_OUTPUT_FORMAT = "mp3" DEFAULT_TEXT_TYPE = "text" @@ -126,6 +130,7 @@ vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string, vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string, vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES), + vol.Optional(CONF_ENGINE, default=DEFAULT_ENGINE): vol.In(SUPPORTED_ENGINES), vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT): vol.In( SUPPORTED_OUTPUT_FORMATS ), @@ -225,6 +230,7 @@ def get_tts_audio(self, message, language=None, options=None): return None, None resp = self.client.synthesize_speech( + Engine=self.config[CONF_ENGINE], OutputFormat=self.config[CONF_OUTPUT_FORMAT], SampleRate=self.config[CONF_SAMPLE_RATE], Text=message, diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index d377ca7dca03c0..7ac91964a98720 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -3,7 +3,7 @@ "name": "Route53", "documentation": "https://www.home-assistant.io/components/route53", "requirements": [ - "boto3==1.9.16", + "boto3==1.9.233", "ipify==1.0.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 245f91dc9c65be..35ee05977d8e22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -302,7 +302,7 @@ bomradarloop==0.1.3 # homeassistant.components.amazon_polly # homeassistant.components.route53 -boto3==1.9.16 +boto3==1.9.233 # homeassistant.components.braviatv braviarc-homeassistant==0.3.7.dev0 From ed49b2f155e929e8f9288d005f53d1d8d396567b Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 2 Oct 2019 08:38:14 -0700 Subject: [PATCH 0546/3953] Bump androidtv to 0.0.29 (#27120) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_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 dbd76a2bc9b0bd..dc2d31c23583ef 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ "adb-shell==0.0.3", - "androidtv==0.0.28" + "androidtv==0.0.29" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index 35ee05977d8e22..2cf2c765e8b0e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -200,7 +200,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.28 +androidtv==0.0.29 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 949da3ad4029bb..bb29540d6d93c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -80,7 +80,7 @@ aiowwlln==2.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.28 +androidtv==0.0.29 # homeassistant.components.apns apns2==0.3.0 From c7da781efcb47c29e1724d1716d729037c5b38bc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Oct 2019 18:25:44 +0200 Subject: [PATCH 0547/3953] Update documentation link URL for integrations in all manifests (#27114) --- homeassistant/components/abode/manifest.json | 2 +- homeassistant/components/acer_projector/manifest.json | 2 +- homeassistant/components/actiontec/manifest.json | 2 +- homeassistant/components/adguard/manifest.json | 2 +- homeassistant/components/ads/manifest.json | 2 +- homeassistant/components/aftership/manifest.json | 2 +- homeassistant/components/air_quality/manifest.json | 2 +- homeassistant/components/airvisual/manifest.json | 2 +- homeassistant/components/aladdin_connect/manifest.json | 2 +- homeassistant/components/alarm_control_panel/manifest.json | 2 +- homeassistant/components/alarmdecoder/manifest.json | 2 +- homeassistant/components/alarmdotcom/manifest.json | 2 +- homeassistant/components/alert/manifest.json | 2 +- homeassistant/components/alexa/manifest.json | 2 +- homeassistant/components/alpha_vantage/manifest.json | 2 +- homeassistant/components/amazon_polly/manifest.json | 2 +- homeassistant/components/ambiclimate/manifest.json | 2 +- homeassistant/components/ambient_station/manifest.json | 2 +- homeassistant/components/amcrest/manifest.json | 2 +- homeassistant/components/ampio/manifest.json | 2 +- homeassistant/components/android_ip_webcam/manifest.json | 2 +- homeassistant/components/androidtv/manifest.json | 2 +- homeassistant/components/anel_pwrctrl/manifest.json | 2 +- homeassistant/components/anthemav/manifest.json | 2 +- homeassistant/components/apache_kafka/manifest.json | 2 +- homeassistant/components/apcupsd/manifest.json | 2 +- homeassistant/components/api/manifest.json | 2 +- homeassistant/components/apns/manifest.json | 2 +- homeassistant/components/apple_tv/manifest.json | 2 +- homeassistant/components/aprs/manifest.json | 2 +- homeassistant/components/aqualogic/manifest.json | 2 +- homeassistant/components/aquostv/manifest.json | 2 +- homeassistant/components/arcam_fmj/manifest.json | 2 +- homeassistant/components/arduino/manifest.json | 2 +- homeassistant/components/arest/manifest.json | 2 +- homeassistant/components/arlo/manifest.json | 2 +- homeassistant/components/aruba/manifest.json | 2 +- homeassistant/components/arwn/manifest.json | 2 +- homeassistant/components/asterisk_cdr/manifest.json | 2 +- homeassistant/components/asterisk_mbox/manifest.json | 2 +- homeassistant/components/asuswrt/manifest.json | 2 +- homeassistant/components/atome/manifest.json | 2 +- homeassistant/components/august/manifest.json | 2 +- homeassistant/components/aurora/manifest.json | 2 +- homeassistant/components/aurora_abb_powerone/manifest.json | 2 +- homeassistant/components/auth/manifest.json | 2 +- homeassistant/components/automatic/manifest.json | 2 +- homeassistant/components/automation/manifest.json | 2 +- homeassistant/components/avea/manifest.json | 2 +- homeassistant/components/avion/manifest.json | 2 +- homeassistant/components/awair/manifest.json | 2 +- homeassistant/components/aws/manifest.json | 2 +- homeassistant/components/axis/manifest.json | 2 +- homeassistant/components/azure_event_hub/manifest.json | 2 +- homeassistant/components/baidu/manifest.json | 2 +- homeassistant/components/bayesian/manifest.json | 2 +- homeassistant/components/bbb_gpio/manifest.json | 2 +- homeassistant/components/bbox/manifest.json | 2 +- homeassistant/components/beewi_smartclim/manifest.json | 2 +- homeassistant/components/bh1750/manifest.json | 2 +- homeassistant/components/binary_sensor/manifest.json | 2 +- homeassistant/components/bitcoin/manifest.json | 2 +- homeassistant/components/bizkaibus/manifest.json | 2 +- homeassistant/components/blackbird/manifest.json | 2 +- homeassistant/components/blink/manifest.json | 2 +- homeassistant/components/blinksticklight/manifest.json | 2 +- homeassistant/components/blinkt/manifest.json | 2 +- homeassistant/components/blockchain/manifest.json | 2 +- homeassistant/components/bloomsky/manifest.json | 2 +- homeassistant/components/bluesound/manifest.json | 2 +- homeassistant/components/bluetooth_le_tracker/manifest.json | 2 +- homeassistant/components/bluetooth_tracker/manifest.json | 2 +- homeassistant/components/bme280/manifest.json | 2 +- homeassistant/components/bme680/manifest.json | 2 +- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- homeassistant/components/bom/manifest.json | 2 +- homeassistant/components/braviatv/manifest.json | 2 +- homeassistant/components/broadlink/manifest.json | 2 +- homeassistant/components/brottsplatskartan/manifest.json | 2 +- homeassistant/components/browser/manifest.json | 2 +- homeassistant/components/brunt/manifest.json | 2 +- homeassistant/components/bt_home_hub_5/manifest.json | 2 +- homeassistant/components/bt_smarthub/manifest.json | 2 +- homeassistant/components/buienradar/manifest.json | 2 +- homeassistant/components/caldav/manifest.json | 2 +- homeassistant/components/calendar/manifest.json | 2 +- homeassistant/components/camera/manifest.json | 2 +- homeassistant/components/canary/manifest.json | 2 +- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cert_expiry/manifest.json | 2 +- homeassistant/components/channels/manifest.json | 2 +- homeassistant/components/cisco_ios/manifest.json | 2 +- homeassistant/components/cisco_mobility_express/manifest.json | 2 +- homeassistant/components/cisco_webex_teams/manifest.json | 2 +- homeassistant/components/ciscospark/manifest.json | 2 +- homeassistant/components/citybikes/manifest.json | 2 +- homeassistant/components/clementine/manifest.json | 2 +- homeassistant/components/clickatell/manifest.json | 2 +- homeassistant/components/clicksend/manifest.json | 2 +- homeassistant/components/clicksend_tts/manifest.json | 2 +- homeassistant/components/climate/manifest.json | 2 +- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/components/cloudflare/manifest.json | 2 +- homeassistant/components/cmus/manifest.json | 2 +- homeassistant/components/co2signal/manifest.json | 2 +- homeassistant/components/coinbase/manifest.json | 2 +- homeassistant/components/coinmarketcap/manifest.json | 2 +- homeassistant/components/comed_hourly_pricing/manifest.json | 2 +- homeassistant/components/comfoconnect/manifest.json | 2 +- homeassistant/components/command_line/manifest.json | 2 +- homeassistant/components/concord232/manifest.json | 2 +- homeassistant/components/config/manifest.json | 2 +- homeassistant/components/configurator/manifest.json | 2 +- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/components/coolmaster/manifest.json | 2 +- homeassistant/components/counter/manifest.json | 2 +- homeassistant/components/cover/manifest.json | 2 +- homeassistant/components/cppm_tracker/manifest.json | 2 +- homeassistant/components/cpuspeed/manifest.json | 2 +- homeassistant/components/crimereports/manifest.json | 2 +- homeassistant/components/cups/manifest.json | 2 +- homeassistant/components/currencylayer/manifest.json | 2 +- homeassistant/components/daikin/manifest.json | 2 +- homeassistant/components/danfoss_air/manifest.json | 2 +- homeassistant/components/darksky/manifest.json | 2 +- homeassistant/components/datadog/manifest.json | 2 +- homeassistant/components/ddwrt/manifest.json | 2 +- homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/decora/manifest.json | 2 +- homeassistant/components/decora_wifi/manifest.json | 2 +- homeassistant/components/default_config/manifest.json | 2 +- homeassistant/components/delijn/manifest.json | 2 +- homeassistant/components/deluge/manifest.json | 2 +- homeassistant/components/demo/manifest.json | 2 +- homeassistant/components/denon/manifest.json | 2 +- homeassistant/components/denonavr/manifest.json | 2 +- homeassistant/components/deutsche_bahn/manifest.json | 2 +- homeassistant/components/device_automation/manifest.json | 2 +- homeassistant/components/device_sun_light_trigger/manifest.json | 2 +- homeassistant/components/device_tracker/manifest.json | 2 +- homeassistant/components/dht/manifest.json | 2 +- homeassistant/components/dialogflow/manifest.json | 2 +- homeassistant/components/digital_ocean/manifest.json | 2 +- homeassistant/components/digitalloggers/manifest.json | 2 +- homeassistant/components/directv/manifest.json | 2 +- homeassistant/components/discogs/manifest.json | 2 +- homeassistant/components/discord/manifest.json | 2 +- homeassistant/components/discovery/manifest.json | 2 +- homeassistant/components/dlib_face_detect/manifest.json | 2 +- homeassistant/components/dlib_face_identify/manifest.json | 2 +- homeassistant/components/dlink/manifest.json | 2 +- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dnsip/manifest.json | 2 +- homeassistant/components/dominos/manifest.json | 2 +- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/doorbird/manifest.json | 2 +- homeassistant/components/dovado/manifest.json | 2 +- homeassistant/components/downloader/manifest.json | 2 +- homeassistant/components/dsmr/manifest.json | 2 +- homeassistant/components/dte_energy_bridge/manifest.json | 2 +- homeassistant/components/dublin_bus_transport/manifest.json | 2 +- homeassistant/components/duckdns/manifest.json | 2 +- homeassistant/components/duke_energy/manifest.json | 2 +- homeassistant/components/dunehd/manifest.json | 2 +- homeassistant/components/dwd_weather_warnings/manifest.json | 2 +- homeassistant/components/dweet/manifest.json | 2 +- homeassistant/components/dyson/manifest.json | 2 +- homeassistant/components/ebox/manifest.json | 2 +- homeassistant/components/ebusd/manifest.json | 2 +- homeassistant/components/ecoal_boiler/manifest.json | 2 +- homeassistant/components/ecobee/manifest.json | 2 +- homeassistant/components/econet/manifest.json | 2 +- homeassistant/components/ecovacs/manifest.json | 2 +- homeassistant/components/eddystone_temperature/manifest.json | 2 +- homeassistant/components/edimax/manifest.json | 2 +- homeassistant/components/ee_brightbox/manifest.json | 2 +- homeassistant/components/efergy/manifest.json | 2 +- homeassistant/components/egardia/manifest.json | 2 +- homeassistant/components/eight_sleep/manifest.json | 2 +- homeassistant/components/eliqonline/manifest.json | 2 +- homeassistant/components/elkm1/manifest.json | 2 +- homeassistant/components/elv/manifest.json | 2 +- homeassistant/components/emby/manifest.json | 2 +- homeassistant/components/emoncms/manifest.json | 2 +- homeassistant/components/emoncms_history/manifest.json | 2 +- homeassistant/components/emulated_hue/manifest.json | 2 +- homeassistant/components/emulated_roku/manifest.json | 2 +- homeassistant/components/enigma2/manifest.json | 2 +- homeassistant/components/enocean/manifest.json | 2 +- homeassistant/components/enphase_envoy/manifest.json | 2 +- homeassistant/components/entur_public_transport/manifest.json | 2 +- homeassistant/components/environment_canada/manifest.json | 2 +- homeassistant/components/envirophat/manifest.json | 2 +- homeassistant/components/envisalink/manifest.json | 2 +- homeassistant/components/ephember/manifest.json | 2 +- homeassistant/components/epson/manifest.json | 2 +- homeassistant/components/epsonworkforce/manifest.json | 2 +- homeassistant/components/eq3btsmart/manifest.json | 2 +- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/essent/manifest.json | 2 +- homeassistant/components/etherscan/manifest.json | 2 +- homeassistant/components/eufy/manifest.json | 2 +- homeassistant/components/everlights/manifest.json | 2 +- homeassistant/components/evohome/manifest.json | 2 +- homeassistant/components/facebook/manifest.json | 2 +- homeassistant/components/facebox/manifest.json | 2 +- homeassistant/components/fail2ban/manifest.json | 2 +- homeassistant/components/familyhub/manifest.json | 2 +- homeassistant/components/fan/manifest.json | 2 +- homeassistant/components/fastdotcom/manifest.json | 2 +- homeassistant/components/feedreader/manifest.json | 2 +- homeassistant/components/ffmpeg/manifest.json | 2 +- homeassistant/components/ffmpeg_motion/manifest.json | 2 +- homeassistant/components/ffmpeg_noise/manifest.json | 2 +- homeassistant/components/fibaro/manifest.json | 2 +- homeassistant/components/fido/manifest.json | 2 +- homeassistant/components/file/manifest.json | 2 +- homeassistant/components/filesize/manifest.json | 2 +- homeassistant/components/filter/manifest.json | 2 +- homeassistant/components/fints/manifest.json | 2 +- homeassistant/components/fitbit/manifest.json | 2 +- homeassistant/components/fixer/manifest.json | 2 +- homeassistant/components/fleetgo/manifest.json | 2 +- homeassistant/components/flexit/manifest.json | 2 +- homeassistant/components/flic/manifest.json | 2 +- homeassistant/components/flock/manifest.json | 2 +- homeassistant/components/flunearyou/manifest.json | 2 +- homeassistant/components/flux/manifest.json | 2 +- homeassistant/components/flux_led/manifest.json | 2 +- homeassistant/components/folder/manifest.json | 2 +- homeassistant/components/folder_watcher/manifest.json | 2 +- homeassistant/components/foobot/manifest.json | 2 +- homeassistant/components/fortigate/manifest.json | 2 +- homeassistant/components/fortios/manifest.json | 2 +- homeassistant/components/foscam/manifest.json | 2 +- homeassistant/components/foursquare/manifest.json | 2 +- homeassistant/components/free_mobile/manifest.json | 2 +- homeassistant/components/freebox/manifest.json | 2 +- homeassistant/components/freedns/manifest.json | 2 +- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- homeassistant/components/fritzbox_netmonitor/manifest.json | 2 +- homeassistant/components/fritzdect/manifest.json | 2 +- homeassistant/components/fronius/manifest.json | 2 +- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/components/frontier_silicon/manifest.json | 2 +- homeassistant/components/futurenow/manifest.json | 2 +- homeassistant/components/garadget/manifest.json | 2 +- homeassistant/components/gc100/manifest.json | 2 +- homeassistant/components/gearbest/manifest.json | 2 +- homeassistant/components/geizhals/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/generic_thermostat/manifest.json | 2 +- homeassistant/components/geniushub/manifest.json | 2 +- homeassistant/components/geo_json_events/manifest.json | 2 +- homeassistant/components/geo_location/manifest.json | 2 +- homeassistant/components/geo_rss_events/manifest.json | 2 +- homeassistant/components/geofency/manifest.json | 2 +- homeassistant/components/geonetnz_quakes/manifest.json | 2 +- homeassistant/components/github/manifest.json | 2 +- homeassistant/components/gitlab_ci/manifest.json | 2 +- homeassistant/components/gitter/manifest.json | 2 +- homeassistant/components/glances/manifest.json | 2 +- homeassistant/components/gntp/manifest.json | 2 +- homeassistant/components/goalfeed/manifest.json | 2 +- homeassistant/components/gogogate2/manifest.json | 2 +- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/google_assistant/manifest.json | 2 +- homeassistant/components/google_cloud/manifest.json | 2 +- homeassistant/components/google_domains/manifest.json | 2 +- homeassistant/components/google_maps/manifest.json | 2 +- homeassistant/components/google_pubsub/manifest.json | 2 +- homeassistant/components/google_translate/manifest.json | 2 +- homeassistant/components/google_travel_time/manifest.json | 2 +- homeassistant/components/google_wifi/manifest.json | 2 +- homeassistant/components/gpmdp/manifest.json | 2 +- homeassistant/components/gpsd/manifest.json | 2 +- homeassistant/components/gpslogger/manifest.json | 2 +- homeassistant/components/graphite/manifest.json | 2 +- homeassistant/components/greeneye_monitor/manifest.json | 2 +- homeassistant/components/greenwave/manifest.json | 2 +- homeassistant/components/group/manifest.json | 2 +- homeassistant/components/growatt_server/manifest.json | 2 +- homeassistant/components/gstreamer/manifest.json | 2 +- homeassistant/components/gtfs/manifest.json | 2 +- homeassistant/components/gtt/manifest.json | 2 +- homeassistant/components/habitica/manifest.json | 2 +- homeassistant/components/hangouts/manifest.json | 2 +- homeassistant/components/harman_kardon_avr/manifest.json | 2 +- homeassistant/components/harmony/manifest.json | 2 +- homeassistant/components/haveibeenpwned/manifest.json | 2 +- homeassistant/components/hddtemp/manifest.json | 2 +- homeassistant/components/hdmi_cec/manifest.json | 2 +- homeassistant/components/heatmiser/manifest.json | 2 +- homeassistant/components/heos/manifest.json | 2 +- homeassistant/components/here_travel_time/manifest.json | 2 +- homeassistant/components/hikvision/manifest.json | 2 +- homeassistant/components/hikvisioncam/manifest.json | 2 +- homeassistant/components/hipchat/manifest.json | 2 +- homeassistant/components/history/manifest.json | 2 +- homeassistant/components/history_graph/manifest.json | 2 +- homeassistant/components/history_stats/manifest.json | 2 +- homeassistant/components/hitron_coda/manifest.json | 2 +- homeassistant/components/hive/manifest.json | 2 +- homeassistant/components/hlk_sw16/manifest.json | 2 +- homeassistant/components/homeassistant/manifest.json | 2 +- homeassistant/components/homekit/manifest.json | 2 +- homeassistant/components/homekit_controller/manifest.json | 2 +- homeassistant/components/homematic/manifest.json | 2 +- homeassistant/components/homematicip_cloud/manifest.json | 2 +- homeassistant/components/homeworks/manifest.json | 2 +- homeassistant/components/honeywell/manifest.json | 2 +- homeassistant/components/hook/manifest.json | 2 +- homeassistant/components/horizon/manifest.json | 2 +- homeassistant/components/hp_ilo/manifest.json | 2 +- homeassistant/components/html5/manifest.json | 2 +- homeassistant/components/http/manifest.json | 2 +- homeassistant/components/htu21d/manifest.json | 2 +- homeassistant/components/huawei_lte/manifest.json | 2 +- homeassistant/components/huawei_router/manifest.json | 2 +- homeassistant/components/hue/manifest.json | 2 +- homeassistant/components/hunterdouglas_powerview/manifest.json | 2 +- homeassistant/components/hydrawise/manifest.json | 2 +- homeassistant/components/hydroquebec/manifest.json | 2 +- homeassistant/components/hyperion/manifest.json | 2 +- homeassistant/components/ialarm/manifest.json | 2 +- homeassistant/components/iaqualink/manifest.json | 2 +- homeassistant/components/icloud/manifest.json | 2 +- homeassistant/components/idteck_prox/manifest.json | 2 +- homeassistant/components/ifttt/manifest.json | 2 +- homeassistant/components/iglo/manifest.json | 2 +- homeassistant/components/ign_sismologia/manifest.json | 2 +- homeassistant/components/ihc/manifest.json | 2 +- homeassistant/components/image_processing/manifest.json | 2 +- homeassistant/components/imap/manifest.json | 2 +- homeassistant/components/imap_email_content/manifest.json | 2 +- homeassistant/components/incomfort/manifest.json | 2 +- homeassistant/components/influxdb/manifest.json | 2 +- homeassistant/components/input_boolean/manifest.json | 2 +- homeassistant/components/input_datetime/manifest.json | 2 +- homeassistant/components/input_number/manifest.json | 2 +- homeassistant/components/input_select/manifest.json | 2 +- homeassistant/components/input_text/manifest.json | 2 +- homeassistant/components/insteon/manifest.json | 2 +- homeassistant/components/integration/manifest.json | 2 +- homeassistant/components/intent_script/manifest.json | 2 +- homeassistant/components/ios/manifest.json | 2 +- homeassistant/components/iota/manifest.json | 2 +- homeassistant/components/iperf3/manifest.json | 2 +- homeassistant/components/ipma/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/irish_rail_transport/manifest.json | 2 +- homeassistant/components/islamic_prayer_times/manifest.json | 2 +- homeassistant/components/iss/manifest.json | 2 +- homeassistant/components/isy994/manifest.json | 2 +- homeassistant/components/itach/manifest.json | 2 +- homeassistant/components/itunes/manifest.json | 2 +- homeassistant/components/izone/manifest.json | 2 +- homeassistant/components/jewish_calendar/manifest.json | 2 +- homeassistant/components/joaoapps_join/manifest.json | 2 +- homeassistant/components/juicenet/manifest.json | 2 +- homeassistant/components/kaiterra/manifest.json | 2 +- homeassistant/components/kankun/manifest.json | 2 +- homeassistant/components/keba/manifest.json | 2 +- homeassistant/components/keenetic_ndms2/manifest.json | 2 +- homeassistant/components/keyboard/manifest.json | 2 +- homeassistant/components/keyboard_remote/manifest.json | 2 +- homeassistant/components/kira/manifest.json | 2 +- homeassistant/components/kiwi/manifest.json | 2 +- homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/kodi/manifest.json | 2 +- homeassistant/components/konnected/manifest.json | 2 +- homeassistant/components/kwb/manifest.json | 2 +- homeassistant/components/lacrosse/manifest.json | 2 +- homeassistant/components/lametric/manifest.json | 2 +- homeassistant/components/lannouncer/manifest.json | 2 +- homeassistant/components/lastfm/manifest.json | 2 +- homeassistant/components/launch_library/manifest.json | 2 +- homeassistant/components/lcn/manifest.json | 2 +- homeassistant/components/lg_netcast/manifest.json | 2 +- homeassistant/components/lg_soundbar/manifest.json | 2 +- homeassistant/components/life360/manifest.json | 2 +- homeassistant/components/lifx/manifest.json | 2 +- homeassistant/components/lifx_cloud/manifest.json | 2 +- homeassistant/components/lifx_legacy/manifest.json | 2 +- homeassistant/components/light/manifest.json | 2 +- homeassistant/components/lightwave/manifest.json | 2 +- homeassistant/components/limitlessled/manifest.json | 2 +- homeassistant/components/linksys_smart/manifest.json | 2 +- homeassistant/components/linky/manifest.json | 2 +- homeassistant/components/linode/manifest.json | 2 +- homeassistant/components/linux_battery/manifest.json | 2 +- homeassistant/components/lirc/manifest.json | 2 +- homeassistant/components/litejet/manifest.json | 2 +- homeassistant/components/liveboxplaytv/manifest.json | 2 +- homeassistant/components/llamalab_automate/manifest.json | 2 +- homeassistant/components/local_file/manifest.json | 2 +- homeassistant/components/locative/manifest.json | 2 +- homeassistant/components/lock/manifest.json | 2 +- homeassistant/components/lockitron/manifest.json | 2 +- homeassistant/components/logbook/manifest.json | 2 +- homeassistant/components/logentries/manifest.json | 2 +- homeassistant/components/logger/manifest.json | 2 +- homeassistant/components/logi_circle/manifest.json | 2 +- homeassistant/components/london_air/manifest.json | 2 +- homeassistant/components/london_underground/manifest.json | 2 +- homeassistant/components/loopenergy/manifest.json | 2 +- homeassistant/components/lovelace/manifest.json | 2 +- homeassistant/components/luci/manifest.json | 2 +- homeassistant/components/luftdaten/manifest.json | 2 +- homeassistant/components/lupusec/manifest.json | 2 +- homeassistant/components/lutron/manifest.json | 2 +- homeassistant/components/lutron_caseta/manifest.json | 2 +- homeassistant/components/lw12wifi/manifest.json | 2 +- homeassistant/components/lyft/manifest.json | 2 +- homeassistant/components/magicseaweed/manifest.json | 2 +- homeassistant/components/mailbox/manifest.json | 2 +- homeassistant/components/mailgun/manifest.json | 2 +- homeassistant/components/manual/manifest.json | 2 +- homeassistant/components/manual_mqtt/manifest.json | 2 +- homeassistant/components/map/manifest.json | 2 +- homeassistant/components/marytts/manifest.json | 2 +- homeassistant/components/mastodon/manifest.json | 2 +- homeassistant/components/matrix/manifest.json | 2 +- homeassistant/components/maxcube/manifest.json | 2 +- homeassistant/components/mcp23017/manifest.json | 2 +- homeassistant/components/media_extractor/manifest.json | 2 +- homeassistant/components/media_player/manifest.json | 2 +- homeassistant/components/mediaroom/manifest.json | 2 +- homeassistant/components/melissa/manifest.json | 2 +- homeassistant/components/meraki/manifest.json | 2 +- homeassistant/components/message_bird/manifest.json | 2 +- homeassistant/components/met/manifest.json | 2 +- homeassistant/components/meteo_france/manifest.json | 2 +- homeassistant/components/meteoalarm/manifest.json | 2 +- homeassistant/components/metoffice/manifest.json | 2 +- homeassistant/components/mfi/manifest.json | 2 +- homeassistant/components/mhz19/manifest.json | 2 +- homeassistant/components/microsoft/manifest.json | 2 +- homeassistant/components/microsoft_face/manifest.json | 2 +- homeassistant/components/microsoft_face_detect/manifest.json | 2 +- homeassistant/components/microsoft_face_identify/manifest.json | 2 +- homeassistant/components/miflora/manifest.json | 2 +- homeassistant/components/mikrotik/manifest.json | 2 +- homeassistant/components/mill/manifest.json | 2 +- homeassistant/components/min_max/manifest.json | 2 +- homeassistant/components/minio/manifest.json | 2 +- homeassistant/components/mitemp_bt/manifest.json | 2 +- homeassistant/components/mjpeg/manifest.json | 2 +- homeassistant/components/mobile_app/manifest.json | 2 +- homeassistant/components/mochad/manifest.json | 2 +- homeassistant/components/modbus/manifest.json | 2 +- homeassistant/components/modem_callerid/manifest.json | 2 +- homeassistant/components/mold_indicator/manifest.json | 2 +- homeassistant/components/monoprice/manifest.json | 2 +- homeassistant/components/moon/manifest.json | 2 +- homeassistant/components/mopar/manifest.json | 2 +- homeassistant/components/mpchc/manifest.json | 2 +- homeassistant/components/mpd/manifest.json | 2 +- homeassistant/components/mqtt/manifest.json | 2 +- homeassistant/components/mqtt_eventstream/manifest.json | 2 +- homeassistant/components/mqtt_json/manifest.json | 2 +- homeassistant/components/mqtt_room/manifest.json | 2 +- homeassistant/components/mqtt_statestream/manifest.json | 2 +- homeassistant/components/mvglive/manifest.json | 2 +- homeassistant/components/mychevy/manifest.json | 2 +- homeassistant/components/mycroft/manifest.json | 2 +- homeassistant/components/myq/manifest.json | 2 +- homeassistant/components/mysensors/manifest.json | 2 +- homeassistant/components/mystrom/manifest.json | 2 +- homeassistant/components/mythicbeastsdns/manifest.json | 2 +- homeassistant/components/n26/manifest.json | 2 +- homeassistant/components/nad/manifest.json | 2 +- homeassistant/components/namecheapdns/manifest.json | 2 +- homeassistant/components/nanoleaf/manifest.json | 2 +- homeassistant/components/neato/manifest.json | 2 +- homeassistant/components/nederlandse_spoorwegen/manifest.json | 2 +- homeassistant/components/nello/manifest.json | 2 +- homeassistant/components/ness_alarm/manifest.json | 2 +- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/netatmo/manifest.json | 2 +- homeassistant/components/netdata/manifest.json | 2 +- homeassistant/components/netgear/manifest.json | 2 +- homeassistant/components/netgear_lte/manifest.json | 2 +- homeassistant/components/netio/manifest.json | 2 +- homeassistant/components/neurio_energy/manifest.json | 2 +- homeassistant/components/nextbus/manifest.json | 2 +- homeassistant/components/nfandroidtv/manifest.json | 2 +- homeassistant/components/niko_home_control/manifest.json | 2 +- homeassistant/components/nilu/manifest.json | 2 +- homeassistant/components/nissan_leaf/manifest.json | 2 +- homeassistant/components/nmap_tracker/manifest.json | 2 +- homeassistant/components/nmbs/manifest.json | 2 +- homeassistant/components/no_ip/manifest.json | 2 +- homeassistant/components/noaa_tides/manifest.json | 2 +- homeassistant/components/norway_air/manifest.json | 2 +- homeassistant/components/notify/manifest.json | 2 +- homeassistant/components/notion/manifest.json | 2 +- homeassistant/components/nsw_fuel_station/manifest.json | 2 +- .../components/nsw_rural_fire_service_feed/manifest.json | 2 +- homeassistant/components/nuheat/manifest.json | 2 +- homeassistant/components/nuimo_controller/manifest.json | 2 +- homeassistant/components/nuki/manifest.json | 2 +- homeassistant/components/nut/manifest.json | 2 +- homeassistant/components/nws/manifest.json | 2 +- homeassistant/components/nx584/manifest.json | 2 +- homeassistant/components/nzbget/manifest.json | 2 +- homeassistant/components/oasa_telematics/manifest.json | 2 +- homeassistant/components/obihai/manifest.json | 2 +- homeassistant/components/octoprint/manifest.json | 2 +- homeassistant/components/oem/manifest.json | 2 +- homeassistant/components/ohmconnect/manifest.json | 2 +- homeassistant/components/ombi/manifest.json | 2 +- homeassistant/components/onboarding/manifest.json | 2 +- homeassistant/components/onewire/manifest.json | 2 +- homeassistant/components/onkyo/manifest.json | 2 +- homeassistant/components/onvif/manifest.json | 2 +- homeassistant/components/openalpr_cloud/manifest.json | 2 +- homeassistant/components/openalpr_local/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/openevse/manifest.json | 2 +- homeassistant/components/openexchangerates/manifest.json | 2 +- homeassistant/components/opengarage/manifest.json | 2 +- homeassistant/components/openhardwaremonitor/manifest.json | 2 +- homeassistant/components/openhome/manifest.json | 2 +- homeassistant/components/opensensemap/manifest.json | 2 +- homeassistant/components/opensky/manifest.json | 2 +- homeassistant/components/opentherm_gw/manifest.json | 2 +- homeassistant/components/openuv/manifest.json | 2 +- homeassistant/components/openweathermap/manifest.json | 2 +- homeassistant/components/opple/manifest.json | 2 +- homeassistant/components/orangepi_gpio/manifest.json | 2 +- homeassistant/components/orvibo/manifest.json | 2 +- homeassistant/components/osramlightify/manifest.json | 2 +- homeassistant/components/otp/manifest.json | 2 +- homeassistant/components/owlet/manifest.json | 2 +- homeassistant/components/owntracks/manifest.json | 2 +- homeassistant/components/panasonic_bluray/manifest.json | 2 +- homeassistant/components/panasonic_viera/manifest.json | 2 +- homeassistant/components/pandora/manifest.json | 2 +- homeassistant/components/panel_custom/manifest.json | 2 +- homeassistant/components/panel_iframe/manifest.json | 2 +- homeassistant/components/pencom/manifest.json | 2 +- homeassistant/components/persistent_notification/manifest.json | 2 +- homeassistant/components/person/manifest.json | 2 +- homeassistant/components/philips_js/manifest.json | 2 +- homeassistant/components/pi_hole/manifest.json | 2 +- homeassistant/components/picotts/manifest.json | 2 +- homeassistant/components/piglow/manifest.json | 2 +- homeassistant/components/pilight/manifest.json | 2 +- homeassistant/components/ping/manifest.json | 2 +- homeassistant/components/pioneer/manifest.json | 2 +- homeassistant/components/pjlink/manifest.json | 2 +- homeassistant/components/plaato/manifest.json | 2 +- homeassistant/components/plant/manifest.json | 2 +- homeassistant/components/plex/manifest.json | 2 +- homeassistant/components/plugwise/manifest.json | 2 +- homeassistant/components/plum_lightpad/manifest.json | 2 +- homeassistant/components/pocketcasts/manifest.json | 2 +- homeassistant/components/point/manifest.json | 2 +- homeassistant/components/postnl/manifest.json | 2 +- homeassistant/components/prezzibenzina/manifest.json | 2 +- homeassistant/components/proliphix/manifest.json | 2 +- homeassistant/components/prometheus/manifest.json | 2 +- homeassistant/components/prowl/manifest.json | 2 +- homeassistant/components/proximity/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/ps4/manifest.json | 2 +- homeassistant/components/ptvsd/manifest.json | 2 +- homeassistant/components/pulseaudio_loopback/manifest.json | 2 +- homeassistant/components/push/manifest.json | 2 +- homeassistant/components/pushbullet/manifest.json | 2 +- homeassistant/components/pushetta/manifest.json | 2 +- homeassistant/components/pushover/manifest.json | 2 +- homeassistant/components/pushsafer/manifest.json | 2 +- homeassistant/components/pvoutput/manifest.json | 2 +- homeassistant/components/pyload/manifest.json | 2 +- homeassistant/components/python_script/manifest.json | 2 +- homeassistant/components/qbittorrent/manifest.json | 2 +- homeassistant/components/qld_bushfire/manifest.json | 2 +- homeassistant/components/qnap/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/quantum_gateway/manifest.json | 2 +- homeassistant/components/qwikswitch/manifest.json | 2 +- homeassistant/components/rachio/manifest.json | 2 +- homeassistant/components/radarr/manifest.json | 2 +- homeassistant/components/radiotherm/manifest.json | 2 +- homeassistant/components/rainbird/manifest.json | 2 +- homeassistant/components/raincloud/manifest.json | 2 +- homeassistant/components/rainforest_eagle/manifest.json | 2 +- homeassistant/components/rainmachine/manifest.json | 2 +- homeassistant/components/random/manifest.json | 2 +- homeassistant/components/raspihats/manifest.json | 2 +- homeassistant/components/raspyrfm/manifest.json | 2 +- homeassistant/components/recollect_waste/manifest.json | 2 +- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/recswitch/manifest.json | 2 +- homeassistant/components/reddit/manifest.json | 2 +- homeassistant/components/rejseplanen/manifest.json | 2 +- homeassistant/components/remember_the_milk/manifest.json | 2 +- homeassistant/components/remote/manifest.json | 2 +- homeassistant/components/remote_rpi_gpio/manifest.json | 2 +- homeassistant/components/repetier/manifest.json | 2 +- homeassistant/components/rest/manifest.json | 2 +- homeassistant/components/rest_command/manifest.json | 2 +- homeassistant/components/rflink/manifest.json | 2 +- homeassistant/components/rfxtrx/manifest.json | 2 +- homeassistant/components/ring/manifest.json | 2 +- homeassistant/components/ripple/manifest.json | 2 +- homeassistant/components/rmvtransport/manifest.json | 2 +- homeassistant/components/rocketchat/manifest.json | 2 +- homeassistant/components/roku/manifest.json | 2 +- homeassistant/components/roomba/manifest.json | 2 +- homeassistant/components/route53/manifest.json | 2 +- homeassistant/components/rova/manifest.json | 2 +- homeassistant/components/rpi_camera/manifest.json | 2 +- homeassistant/components/rpi_gpio/manifest.json | 2 +- homeassistant/components/rpi_gpio_pwm/manifest.json | 2 +- homeassistant/components/rpi_pfio/manifest.json | 2 +- homeassistant/components/rpi_rf/manifest.json | 2 +- homeassistant/components/rss_feed_template/manifest.json | 2 +- homeassistant/components/rtorrent/manifest.json | 2 +- homeassistant/components/russound_rio/manifest.json | 2 +- homeassistant/components/russound_rnet/manifest.json | 2 +- homeassistant/components/sabnzbd/manifest.json | 2 +- homeassistant/components/saj/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/satel_integra/manifest.json | 2 +- homeassistant/components/scene/manifest.json | 2 +- homeassistant/components/scrape/manifest.json | 2 +- homeassistant/components/script/manifest.json | 2 +- homeassistant/components/scsgate/manifest.json | 2 +- homeassistant/components/season/manifest.json | 2 +- homeassistant/components/sendgrid/manifest.json | 2 +- homeassistant/components/sense/manifest.json | 2 +- homeassistant/components/sensehat/manifest.json | 2 +- homeassistant/components/sensibo/manifest.json | 2 +- homeassistant/components/sensor/manifest.json | 2 +- homeassistant/components/serial/manifest.json | 2 +- homeassistant/components/serial_pm/manifest.json | 2 +- homeassistant/components/sesame/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/seventeentrack/manifest.json | 2 +- homeassistant/components/shell_command/manifest.json | 2 +- homeassistant/components/shiftr/manifest.json | 2 +- homeassistant/components/shodan/manifest.json | 2 +- homeassistant/components/shopping_list/manifest.json | 2 +- homeassistant/components/sht31/manifest.json | 2 +- homeassistant/components/sigfox/manifest.json | 2 +- homeassistant/components/simplepush/manifest.json | 2 +- homeassistant/components/simplisafe/manifest.json | 2 +- homeassistant/components/simulated/manifest.json | 2 +- homeassistant/components/sisyphus/manifest.json | 2 +- homeassistant/components/sky_hub/manifest.json | 2 +- homeassistant/components/skybeacon/manifest.json | 2 +- homeassistant/components/skybell/manifest.json | 2 +- homeassistant/components/slack/manifest.json | 2 +- homeassistant/components/sleepiq/manifest.json | 2 +- homeassistant/components/slide/manifest.json | 2 +- homeassistant/components/sma/manifest.json | 2 +- homeassistant/components/smappee/manifest.json | 2 +- homeassistant/components/smarthab/manifest.json | 2 +- homeassistant/components/smartthings/manifest.json | 2 +- homeassistant/components/smarty/manifest.json | 2 +- homeassistant/components/smhi/manifest.json | 2 +- homeassistant/components/smtp/manifest.json | 2 +- homeassistant/components/snapcast/manifest.json | 2 +- homeassistant/components/snips/manifest.json | 2 +- homeassistant/components/snmp/manifest.json | 2 +- homeassistant/components/sochain/manifest.json | 2 +- homeassistant/components/socialblade/manifest.json | 2 +- homeassistant/components/solaredge/manifest.json | 2 +- homeassistant/components/solaredge_local/manifest.json | 2 +- homeassistant/components/solax/manifest.json | 2 +- homeassistant/components/somfy/manifest.json | 2 +- homeassistant/components/somfy_mylink/manifest.json | 2 +- homeassistant/components/sonarr/manifest.json | 2 +- homeassistant/components/songpal/manifest.json | 2 +- homeassistant/components/sonos/manifest.json | 2 +- homeassistant/components/sony_projector/manifest.json | 2 +- homeassistant/components/soundtouch/manifest.json | 2 +- homeassistant/components/spaceapi/manifest.json | 2 +- homeassistant/components/spc/manifest.json | 2 +- homeassistant/components/speedtestdotnet/manifest.json | 2 +- homeassistant/components/spider/manifest.json | 2 +- homeassistant/components/splunk/manifest.json | 2 +- homeassistant/components/spotcrime/manifest.json | 2 +- homeassistant/components/spotify/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/components/squeezebox/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/starlingbank/manifest.json | 2 +- homeassistant/components/startca/manifest.json | 2 +- homeassistant/components/statistics/manifest.json | 2 +- homeassistant/components/statsd/manifest.json | 2 +- homeassistant/components/steam_online/manifest.json | 2 +- homeassistant/components/stiebel_eltron/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- homeassistant/components/streamlabswater/manifest.json | 2 +- homeassistant/components/stride/manifest.json | 2 +- homeassistant/components/suez_water/manifest.json | 2 +- homeassistant/components/sun/manifest.json | 2 +- homeassistant/components/supervisord/manifest.json | 2 +- homeassistant/components/supla/manifest.json | 2 +- homeassistant/components/swiss_hydrological_data/manifest.json | 2 +- homeassistant/components/swiss_public_transport/manifest.json | 2 +- homeassistant/components/swisscom/manifest.json | 2 +- homeassistant/components/switch/manifest.json | 2 +- homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switcher_kis/manifest.json | 2 +- homeassistant/components/switchmate/manifest.json | 2 +- homeassistant/components/syncthru/manifest.json | 2 +- homeassistant/components/synology/manifest.json | 2 +- homeassistant/components/synology_chat/manifest.json | 2 +- homeassistant/components/synology_srm/manifest.json | 2 +- homeassistant/components/synologydsm/manifest.json | 2 +- homeassistant/components/syslog/manifest.json | 2 +- homeassistant/components/system_health/manifest.json | 2 +- homeassistant/components/system_log/manifest.json | 2 +- homeassistant/components/systemmonitor/manifest.json | 2 +- homeassistant/components/tado/manifest.json | 2 +- homeassistant/components/tahoma/manifest.json | 2 +- homeassistant/components/tank_utility/manifest.json | 2 +- homeassistant/components/tapsaff/manifest.json | 2 +- homeassistant/components/tautulli/manifest.json | 2 +- homeassistant/components/tcp/manifest.json | 2 +- homeassistant/components/ted5000/manifest.json | 2 +- homeassistant/components/teksavvy/manifest.json | 2 +- homeassistant/components/telegram/manifest.json | 2 +- homeassistant/components/telegram_bot/manifest.json | 2 +- homeassistant/components/tellduslive/manifest.json | 2 +- homeassistant/components/tellstick/manifest.json | 2 +- homeassistant/components/telnet/manifest.json | 2 +- homeassistant/components/temper/manifest.json | 2 +- homeassistant/components/template/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/tesla/manifest.json | 2 +- homeassistant/components/tfiac/manifest.json | 2 +- homeassistant/components/thermoworks_smoke/manifest.json | 2 +- homeassistant/components/thethingsnetwork/manifest.json | 2 +- homeassistant/components/thingspeak/manifest.json | 2 +- homeassistant/components/thinkingcleaner/manifest.json | 2 +- homeassistant/components/thomson/manifest.json | 2 +- homeassistant/components/threshold/manifest.json | 2 +- homeassistant/components/tibber/manifest.json | 2 +- homeassistant/components/tikteck/manifest.json | 2 +- homeassistant/components/tile/manifest.json | 2 +- homeassistant/components/time_date/manifest.json | 2 +- homeassistant/components/timer/manifest.json | 2 +- homeassistant/components/tod/manifest.json | 2 +- homeassistant/components/todoist/manifest.json | 2 +- homeassistant/components/tof/manifest.json | 2 +- homeassistant/components/tomato/manifest.json | 2 +- homeassistant/components/toon/manifest.json | 2 +- homeassistant/components/torque/manifest.json | 2 +- homeassistant/components/totalconnect/manifest.json | 2 +- homeassistant/components/touchline/manifest.json | 2 +- homeassistant/components/tplink/manifest.json | 2 +- homeassistant/components/tplink_lte/manifest.json | 2 +- homeassistant/components/traccar/manifest.json | 2 +- homeassistant/components/trackr/manifest.json | 2 +- homeassistant/components/tradfri/manifest.json | 2 +- homeassistant/components/trafikverket_train/manifest.json | 2 +- .../components/trafikverket_weatherstation/manifest.json | 2 +- homeassistant/components/transmission/manifest.json | 2 +- homeassistant/components/transport_nsw/manifest.json | 2 +- homeassistant/components/travisci/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/components/tts/manifest.json | 2 +- homeassistant/components/tuya/manifest.json | 2 +- homeassistant/components/twentemilieu/manifest.json | 2 +- homeassistant/components/twilio/manifest.json | 2 +- homeassistant/components/twilio_call/manifest.json | 2 +- homeassistant/components/twilio_sms/manifest.json | 2 +- homeassistant/components/twitch/manifest.json | 2 +- homeassistant/components/twitter/manifest.json | 2 +- homeassistant/components/ubee/manifest.json | 2 +- homeassistant/components/ubus/manifest.json | 2 +- homeassistant/components/ue_smart_radio/manifest.json | 2 +- homeassistant/components/uk_transport/manifest.json | 2 +- homeassistant/components/unifi/manifest.json | 2 +- homeassistant/components/unifi_direct/manifest.json | 2 +- homeassistant/components/universal/manifest.json | 2 +- homeassistant/components/upc_connect/manifest.json | 2 +- homeassistant/components/upcloud/manifest.json | 2 +- homeassistant/components/updater/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/uptime/manifest.json | 2 +- homeassistant/components/uptimerobot/manifest.json | 2 +- homeassistant/components/uscis/manifest.json | 2 +- homeassistant/components/usgs_earthquakes_feed/manifest.json | 2 +- homeassistant/components/utility_meter/manifest.json | 2 +- homeassistant/components/uvc/manifest.json | 2 +- homeassistant/components/vacuum/manifest.json | 2 +- homeassistant/components/vallox/manifest.json | 2 +- homeassistant/components/vasttrafik/manifest.json | 2 +- homeassistant/components/velbus/manifest.json | 2 +- homeassistant/components/velux/manifest.json | 2 +- homeassistant/components/venstar/manifest.json | 2 +- homeassistant/components/vera/manifest.json | 2 +- homeassistant/components/verisure/manifest.json | 2 +- homeassistant/components/version/manifest.json | 2 +- homeassistant/components/vesync/manifest.json | 2 +- homeassistant/components/viaggiatreno/manifest.json | 2 +- homeassistant/components/vicare/manifest.json | 2 +- homeassistant/components/vivotek/manifest.json | 2 +- homeassistant/components/vizio/manifest.json | 2 +- homeassistant/components/vlc/manifest.json | 2 +- homeassistant/components/vlc_telnet/manifest.json | 2 +- homeassistant/components/voicerss/manifest.json | 2 +- homeassistant/components/volkszaehler/manifest.json | 2 +- homeassistant/components/volumio/manifest.json | 2 +- homeassistant/components/volvooncall/manifest.json | 2 +- homeassistant/components/vultr/manifest.json | 2 +- homeassistant/components/w800rf32/manifest.json | 2 +- homeassistant/components/wake_on_lan/manifest.json | 2 +- homeassistant/components/waqi/manifest.json | 2 +- homeassistant/components/water_heater/manifest.json | 2 +- homeassistant/components/waterfurnace/manifest.json | 2 +- homeassistant/components/watson_iot/manifest.json | 2 +- homeassistant/components/watson_tts/manifest.json | 2 +- homeassistant/components/waze_travel_time/manifest.json | 2 +- homeassistant/components/weather/manifest.json | 2 +- homeassistant/components/webhook/manifest.json | 2 +- homeassistant/components/weblink/manifest.json | 2 +- homeassistant/components/webostv/manifest.json | 2 +- homeassistant/components/websocket_api/manifest.json | 2 +- homeassistant/components/wemo/manifest.json | 2 +- homeassistant/components/whois/manifest.json | 2 +- homeassistant/components/wink/manifest.json | 2 +- homeassistant/components/wirelesstag/manifest.json | 2 +- homeassistant/components/withings/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- homeassistant/components/worldclock/manifest.json | 2 +- homeassistant/components/worldtidesinfo/manifest.json | 2 +- homeassistant/components/worxlandroid/manifest.json | 2 +- homeassistant/components/wsdot/manifest.json | 2 +- homeassistant/components/wunderground/manifest.json | 2 +- homeassistant/components/wunderlist/manifest.json | 2 +- homeassistant/components/wwlln/manifest.json | 2 +- homeassistant/components/x10/manifest.json | 2 +- homeassistant/components/xbox_live/manifest.json | 2 +- homeassistant/components/xeoma/manifest.json | 2 +- homeassistant/components/xfinity/manifest.json | 2 +- homeassistant/components/xiaomi/manifest.json | 2 +- homeassistant/components/xiaomi_aqara/manifest.json | 2 +- homeassistant/components/xiaomi_miio/manifest.json | 2 +- homeassistant/components/xiaomi_tv/manifest.json | 2 +- homeassistant/components/xmpp/manifest.json | 2 +- homeassistant/components/xs1/manifest.json | 2 +- homeassistant/components/yale_smart_alarm/manifest.json | 2 +- homeassistant/components/yamaha/manifest.json | 2 +- homeassistant/components/yamaha_musiccast/manifest.json | 2 +- homeassistant/components/yandex_transport/manifest.json | 2 +- homeassistant/components/yandextts/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/components/yeelightsunflower/manifest.json | 2 +- homeassistant/components/yessssms/manifest.json | 2 +- homeassistant/components/yi/manifest.json | 2 +- homeassistant/components/yr/manifest.json | 2 +- homeassistant/components/yweather/manifest.json | 2 +- homeassistant/components/zabbix/manifest.json | 2 +- homeassistant/components/zamg/manifest.json | 2 +- homeassistant/components/zengge/manifest.json | 2 +- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/components/zestimate/manifest.json | 2 +- homeassistant/components/zha/manifest.json | 2 +- homeassistant/components/zhong_hong/manifest.json | 2 +- homeassistant/components/zigbee/manifest.json | 2 +- homeassistant/components/ziggo_mediabox_xl/manifest.json | 2 +- homeassistant/components/zone/manifest.json | 2 +- homeassistant/components/zoneminder/manifest.json | 2 +- homeassistant/components/zwave/manifest.json | 2 +- 874 files changed, 874 insertions(+), 874 deletions(-) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 49e0c46fd553ba..793c19cc466e63 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -1,7 +1,7 @@ { "domain": "abode", "name": "Abode", - "documentation": "https://www.home-assistant.io/components/abode", + "documentation": "https://www.home-assistant.io/integrations/abode", "requirements": [ "abodepy==0.15.0" ], diff --git a/homeassistant/components/acer_projector/manifest.json b/homeassistant/components/acer_projector/manifest.json index 4b8d6967491571..9f712ba5c7eed5 100644 --- a/homeassistant/components/acer_projector/manifest.json +++ b/homeassistant/components/acer_projector/manifest.json @@ -1,7 +1,7 @@ { "domain": "acer_projector", "name": "Acer projector", - "documentation": "https://www.home-assistant.io/components/acer_projector", + "documentation": "https://www.home-assistant.io/integrations/acer_projector", "requirements": [ "pyserial==3.1.1" ], diff --git a/homeassistant/components/actiontec/manifest.json b/homeassistant/components/actiontec/manifest.json index e233f430cfcbbb..ddb4954794cb5d 100644 --- a/homeassistant/components/actiontec/manifest.json +++ b/homeassistant/components/actiontec/manifest.json @@ -1,7 +1,7 @@ { "domain": "actiontec", "name": "Actiontec", - "documentation": "https://www.home-assistant.io/components/actiontec", + "documentation": "https://www.home-assistant.io/integrations/actiontec", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 0063f1ec37064c..f207e6dff09b06 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -2,7 +2,7 @@ "domain": "adguard", "name": "AdGuard Home", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/adguard", + "documentation": "https://www.home-assistant.io/integrations/adguard", "requirements": [ "adguardhome==0.2.1" ], diff --git a/homeassistant/components/ads/manifest.json b/homeassistant/components/ads/manifest.json index 0c759f0ad60c57..cf3e621fd071ca 100644 --- a/homeassistant/components/ads/manifest.json +++ b/homeassistant/components/ads/manifest.json @@ -1,7 +1,7 @@ { "domain": "ads", "name": "Ads", - "documentation": "https://www.home-assistant.io/components/ads", + "documentation": "https://www.home-assistant.io/integrations/ads", "requirements": [ "pyads==3.0.7" ], diff --git a/homeassistant/components/aftership/manifest.json b/homeassistant/components/aftership/manifest.json index b9ee8939dc4631..a7e8aeb8debd11 100644 --- a/homeassistant/components/aftership/manifest.json +++ b/homeassistant/components/aftership/manifest.json @@ -1,7 +1,7 @@ { "domain": "aftership", "name": "Aftership", - "documentation": "https://www.home-assistant.io/components/aftership", + "documentation": "https://www.home-assistant.io/integrations/aftership", "requirements": [ "pyaftership==0.1.2" ], diff --git a/homeassistant/components/air_quality/manifest.json b/homeassistant/components/air_quality/manifest.json index 5bfe85547ffce4..17fa14a9cfac0a 100644 --- a/homeassistant/components/air_quality/manifest.json +++ b/homeassistant/components/air_quality/manifest.json @@ -1,7 +1,7 @@ { "domain": "air_quality", "name": "Air quality", - "documentation": "https://www.home-assistant.io/components/air_quality", + "documentation": "https://www.home-assistant.io/integrations/air_quality", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index ddb109a99b07e5..e7ea23a43a1a63 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -1,7 +1,7 @@ { "domain": "airvisual", "name": "Airvisual", - "documentation": "https://www.home-assistant.io/components/airvisual", + "documentation": "https://www.home-assistant.io/integrations/airvisual", "requirements": [ "pyairvisual==3.0.1" ], diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 0681d5df38b248..7d440407427e45 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -1,7 +1,7 @@ { "domain": "aladdin_connect", "name": "Aladdin connect", - "documentation": "https://www.home-assistant.io/components/aladdin_connect", + "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "requirements": [ "aladdin_connect==0.3" ], diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index 95e26de53bcb3b..04ef58769dad7b 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -1,7 +1,7 @@ { "domain": "alarm_control_panel", "name": "Alarm control panel", - "documentation": "https://www.home-assistant.io/components/alarm_control_panel", + "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index 3e0d4112d2735f..5ab69d94cf2218 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -1,7 +1,7 @@ { "domain": "alarmdecoder", "name": "Alarmdecoder", - "documentation": "https://www.home-assistant.io/components/alarmdecoder", + "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", "requirements": [ "alarmdecoder==1.13.2" ], diff --git a/homeassistant/components/alarmdotcom/manifest.json b/homeassistant/components/alarmdotcom/manifest.json index 9d2c0a2056e362..fd5c3010ab6757 100644 --- a/homeassistant/components/alarmdotcom/manifest.json +++ b/homeassistant/components/alarmdotcom/manifest.json @@ -1,7 +1,7 @@ { "domain": "alarmdotcom", "name": "Alarmdotcom", - "documentation": "https://www.home-assistant.io/components/alarmdotcom", + "documentation": "https://www.home-assistant.io/integrations/alarmdotcom", "requirements": [ "pyalarmdotcom==0.3.2" ], diff --git a/homeassistant/components/alert/manifest.json b/homeassistant/components/alert/manifest.json index 2e27beb48e68f0..06269390753d5c 100644 --- a/homeassistant/components/alert/manifest.json +++ b/homeassistant/components/alert/manifest.json @@ -1,7 +1,7 @@ { "domain": "alert", "name": "Alert", - "documentation": "https://www.home-assistant.io/components/alert", + "documentation": "https://www.home-assistant.io/integrations/alert", "requirements": [], "dependencies": [], "after_dependencies": [ diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index e4fc9eb86805aa..c6629982d53fae 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -1,7 +1,7 @@ { "domain": "alexa", "name": "Alexa", - "documentation": "https://www.home-assistant.io/components/alexa", + "documentation": "https://www.home-assistant.io/integrations/alexa", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index dacc428ea2ee36..9ac8d1ea1e0b70 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -1,7 +1,7 @@ { "domain": "alpha_vantage", "name": "Alpha vantage", - "documentation": "https://www.home-assistant.io/components/alpha_vantage", + "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "requirements": [ "alpha_vantage==2.1.0" ], diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 20d443f225eb18..45e382647f8d38 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -1,7 +1,7 @@ { "domain": "amazon_polly", "name": "Amazon polly", - "documentation": "https://www.home-assistant.io/components/amazon_polly", + "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "requirements": [ "boto3==1.9.233" ], diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index 8e5ddb924ca655..3d175165abd1fc 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -2,7 +2,7 @@ "domain": "ambiclimate", "name": "Ambiclimate", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ambiclimate", + "documentation": "https://www.home-assistant.io/integrations/ambiclimate", "requirements": [ "ambiclimate==0.2.1" ], diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 056930edfc7bc3..8f363ba219f99d 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -2,7 +2,7 @@ "domain": "ambient_station", "name": "Ambient station", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ambient_station", + "documentation": "https://www.home-assistant.io/integrations/ambient_station", "requirements": [ "aioambient==0.3.2" ], diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index f79ce34897b92c..4453687b895312 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -1,7 +1,7 @@ { "domain": "amcrest", "name": "Amcrest", - "documentation": "https://www.home-assistant.io/components/amcrest", + "documentation": "https://www.home-assistant.io/integrations/amcrest", "requirements": [ "amcrest==1.5.3" ], diff --git a/homeassistant/components/ampio/manifest.json b/homeassistant/components/ampio/manifest.json index d20b10b2d1509e..6bf79e27f85e3a 100644 --- a/homeassistant/components/ampio/manifest.json +++ b/homeassistant/components/ampio/manifest.json @@ -1,7 +1,7 @@ { "domain": "ampio", "name": "Ampio", - "documentation": "https://www.home-assistant.io/components/ampio", + "documentation": "https://www.home-assistant.io/integrations/ampio", "requirements": [ "asmog==0.0.6" ], diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 28909f7e053379..c9602d757510ed 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -1,7 +1,7 @@ { "domain": "android_ip_webcam", "name": "Android ip webcam", - "documentation": "https://www.home-assistant.io/components/android_ip_webcam", + "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", "requirements": [ "pydroid-ipcam==0.8" ], diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index dc2d31c23583ef..4fd3b062a10721 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -1,7 +1,7 @@ { "domain": "androidtv", "name": "Androidtv", - "documentation": "https://www.home-assistant.io/components/androidtv", + "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell==0.0.3", "androidtv==0.0.29" diff --git a/homeassistant/components/anel_pwrctrl/manifest.json b/homeassistant/components/anel_pwrctrl/manifest.json index 17802918cd2263..d4055a4068f335 100644 --- a/homeassistant/components/anel_pwrctrl/manifest.json +++ b/homeassistant/components/anel_pwrctrl/manifest.json @@ -1,7 +1,7 @@ { "domain": "anel_pwrctrl", "name": "Anel pwrctrl", - "documentation": "https://www.home-assistant.io/components/anel_pwrctrl", + "documentation": "https://www.home-assistant.io/integrations/anel_pwrctrl", "requirements": [ "anel_pwrctrl-homeassistant==0.0.1.dev2" ], diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index 9b2e3c697bb220..7c648d37b10d56 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -1,7 +1,7 @@ { "domain": "anthemav", "name": "Anthemav", - "documentation": "https://www.home-assistant.io/components/anthemav", + "documentation": "https://www.home-assistant.io/integrations/anthemav", "requirements": [ "anthemav==1.1.10" ], diff --git a/homeassistant/components/apache_kafka/manifest.json b/homeassistant/components/apache_kafka/manifest.json index ac36af7fa48fe4..fb2bd64352579f 100644 --- a/homeassistant/components/apache_kafka/manifest.json +++ b/homeassistant/components/apache_kafka/manifest.json @@ -1,7 +1,7 @@ { "domain": "apache_kafka", "name": "Apache Kafka", - "documentation": "https://www.home-assistant.io/components/apache_kafka", + "documentation": "https://www.home-assistant.io/integrations/apache_kafka", "requirements": [ "aiokafka==0.5.1" ], diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 813176728f2843..08cac545249a20 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "apcupsd", "name": "Apcupsd", - "documentation": "https://www.home-assistant.io/components/apcupsd", + "documentation": "https://www.home-assistant.io/integrations/apcupsd", "requirements": [ "apcaccess==0.0.13" ], diff --git a/homeassistant/components/api/manifest.json b/homeassistant/components/api/manifest.json index 25d9a76036eec6..830fc04445a855 100644 --- a/homeassistant/components/api/manifest.json +++ b/homeassistant/components/api/manifest.json @@ -1,7 +1,7 @@ { "domain": "api", "name": "Home Assistant API", - "documentation": "https://www.home-assistant.io/components/api", + "documentation": "https://www.home-assistant.io/integrations/api", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/apns/manifest.json b/homeassistant/components/apns/manifest.json index 9a310a096a5a4d..4845c45a963dce 100644 --- a/homeassistant/components/apns/manifest.json +++ b/homeassistant/components/apns/manifest.json @@ -1,7 +1,7 @@ { "domain": "apns", "name": "Apns", - "documentation": "https://www.home-assistant.io/components/apns", + "documentation": "https://www.home-assistant.io/integrations/apns", "requirements": [ "apns2==0.3.0" ], diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index c391fb0e14ba0e..f8fd2e0efa2ad1 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -1,7 +1,7 @@ { "domain": "apple_tv", "name": "Apple tv", - "documentation": "https://www.home-assistant.io/components/apple_tv", + "documentation": "https://www.home-assistant.io/integrations/apple_tv", "requirements": [ "pyatv==0.3.13" ], diff --git a/homeassistant/components/aprs/manifest.json b/homeassistant/components/aprs/manifest.json index fbe13ca85782c9..c7615817b50a91 100644 --- a/homeassistant/components/aprs/manifest.json +++ b/homeassistant/components/aprs/manifest.json @@ -1,7 +1,7 @@ { "domain": "aprs", "name": "APRS", - "documentation": "https://www.home-assistant.io/components/aprs", + "documentation": "https://www.home-assistant.io/integrations/aprs", "dependencies": [], "codeowners": ["@PhilRW"], "requirements": [ diff --git a/homeassistant/components/aqualogic/manifest.json b/homeassistant/components/aqualogic/manifest.json index 40f1805d83abec..2e5dded4af6582 100644 --- a/homeassistant/components/aqualogic/manifest.json +++ b/homeassistant/components/aqualogic/manifest.json @@ -1,7 +1,7 @@ { "domain": "aqualogic", "name": "Aqualogic", - "documentation": "https://www.home-assistant.io/components/aqualogic", + "documentation": "https://www.home-assistant.io/integrations/aqualogic", "requirements": [ "aqualogic==1.0" ], diff --git a/homeassistant/components/aquostv/manifest.json b/homeassistant/components/aquostv/manifest.json index 16865905ae984a..d4c491cb70d539 100644 --- a/homeassistant/components/aquostv/manifest.json +++ b/homeassistant/components/aquostv/manifest.json @@ -1,7 +1,7 @@ { "domain": "aquostv", "name": "Aquostv", - "documentation": "https://www.home-assistant.io/components/aquostv", + "documentation": "https://www.home-assistant.io/integrations/aquostv", "requirements": [ "sharp_aquos_rc==0.3.2" ], diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 59ab3c03d9273e..288b8fb38903e1 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -2,7 +2,7 @@ "domain": "arcam_fmj", "name": "Arcam FMJ Receiver control", "config_flow": false, - "documentation": "https://www.home-assistant.io/components/arcam_fmj", + "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", "requirements": [ "arcam-fmj==0.4.3" ], diff --git a/homeassistant/components/arduino/manifest.json b/homeassistant/components/arduino/manifest.json index cf21cbe87eafe2..3567ce71cd12cf 100644 --- a/homeassistant/components/arduino/manifest.json +++ b/homeassistant/components/arduino/manifest.json @@ -1,7 +1,7 @@ { "domain": "arduino", "name": "Arduino", - "documentation": "https://www.home-assistant.io/components/arduino", + "documentation": "https://www.home-assistant.io/integrations/arduino", "requirements": [ "PyMata==2.14" ], diff --git a/homeassistant/components/arest/manifest.json b/homeassistant/components/arest/manifest.json index d5bcf92a39dc4b..ee6b915e658a62 100644 --- a/homeassistant/components/arest/manifest.json +++ b/homeassistant/components/arest/manifest.json @@ -1,7 +1,7 @@ { "domain": "arest", "name": "Arest", - "documentation": "https://www.home-assistant.io/components/arest", + "documentation": "https://www.home-assistant.io/integrations/arest", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/arlo/manifest.json b/homeassistant/components/arlo/manifest.json index 35803d0d4f6afb..8779e051dc0e14 100644 --- a/homeassistant/components/arlo/manifest.json +++ b/homeassistant/components/arlo/manifest.json @@ -1,7 +1,7 @@ { "domain": "arlo", "name": "Arlo", - "documentation": "https://www.home-assistant.io/components/arlo", + "documentation": "https://www.home-assistant.io/integrations/arlo", "requirements": [ "pyarlo==0.2.3" ], diff --git a/homeassistant/components/aruba/manifest.json b/homeassistant/components/aruba/manifest.json index 597975619e6a4e..ccc4f1190926f6 100644 --- a/homeassistant/components/aruba/manifest.json +++ b/homeassistant/components/aruba/manifest.json @@ -1,7 +1,7 @@ { "domain": "aruba", "name": "Aruba", - "documentation": "https://www.home-assistant.io/components/aruba", + "documentation": "https://www.home-assistant.io/integrations/aruba", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/arwn/manifest.json b/homeassistant/components/arwn/manifest.json index 1c861aa67e28bf..d84202cbac8902 100644 --- a/homeassistant/components/arwn/manifest.json +++ b/homeassistant/components/arwn/manifest.json @@ -1,7 +1,7 @@ { "domain": "arwn", "name": "Arwn", - "documentation": "https://www.home-assistant.io/components/arwn", + "documentation": "https://www.home-assistant.io/integrations/arwn", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/asterisk_cdr/manifest.json b/homeassistant/components/asterisk_cdr/manifest.json index db1308b0483d78..56018ba777bd82 100644 --- a/homeassistant/components/asterisk_cdr/manifest.json +++ b/homeassistant/components/asterisk_cdr/manifest.json @@ -1,7 +1,7 @@ { "domain": "asterisk_cdr", "name": "Asterisk cdr", - "documentation": "https://www.home-assistant.io/components/asterisk_cdr", + "documentation": "https://www.home-assistant.io/integrations/asterisk_cdr", "requirements": [], "dependencies": [ "asterisk_mbox" diff --git a/homeassistant/components/asterisk_mbox/manifest.json b/homeassistant/components/asterisk_mbox/manifest.json index bafe43c480f4f7..cf793328d932eb 100644 --- a/homeassistant/components/asterisk_mbox/manifest.json +++ b/homeassistant/components/asterisk_mbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "asterisk_mbox", "name": "Asterisk mbox", - "documentation": "https://www.home-assistant.io/components/asterisk_mbox", + "documentation": "https://www.home-assistant.io/integrations/asterisk_mbox", "requirements": [ "asterisk_mbox==0.5.0" ], diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index f36819f133ddb8..3fcdcf42ab1b3e 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -1,7 +1,7 @@ { "domain": "asuswrt", "name": "Asuswrt", - "documentation": "https://www.home-assistant.io/components/asuswrt", + "documentation": "https://www.home-assistant.io/integrations/asuswrt", "requirements": [ "aioasuswrt==1.1.21" ], diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 621faba4fc0a33..55739cad8cc7af 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -1,7 +1,7 @@ { "domain": "atome", "name": "Atome", - "documentation": "https://www.home-assistant.io/components/atome", + "documentation": "https://www.home-assistant.io/integrations/atome", "dependencies": [], "codeowners": ["@baqs"], "requirements": ["pyatome==0.1.1"] diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index e41491c4b0ac85..ebaa564736f19f 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -1,7 +1,7 @@ { "domain": "august", "name": "August", - "documentation": "https://www.home-assistant.io/components/august", + "documentation": "https://www.home-assistant.io/integrations/august", "requirements": [ "py-august==0.7.0" ], diff --git a/homeassistant/components/aurora/manifest.json b/homeassistant/components/aurora/manifest.json index 56ba3fe935608a..204327043f9c07 100644 --- a/homeassistant/components/aurora/manifest.json +++ b/homeassistant/components/aurora/manifest.json @@ -1,7 +1,7 @@ { "domain": "aurora", "name": "Aurora", - "documentation": "https://www.home-assistant.io/components/aurora", + "documentation": "https://www.home-assistant.io/integrations/aurora", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/aurora_abb_powerone/manifest.json b/homeassistant/components/aurora_abb_powerone/manifest.json index 56325dd40af64f..f49421ea954821 100644 --- a/homeassistant/components/aurora_abb_powerone/manifest.json +++ b/homeassistant/components/aurora_abb_powerone/manifest.json @@ -1,7 +1,7 @@ { "domain": "aurora_abb_powerone", "name": "Aurora ABB Solar PV", - "documentation": "https://www.home-assistant.io/components/aurora_abb_powerone/", + "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone/", "dependencies": [], "codeowners": [ "@davet2001" diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index 10be545f5e14eb..1b0ab33f38155b 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -1,7 +1,7 @@ { "domain": "auth", "name": "Auth", - "documentation": "https://www.home-assistant.io/components/auth", + "documentation": "https://www.home-assistant.io/integrations/auth", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/automatic/manifest.json b/homeassistant/components/automatic/manifest.json index 9743835af20ab2..63cf0da0f46fb5 100644 --- a/homeassistant/components/automatic/manifest.json +++ b/homeassistant/components/automatic/manifest.json @@ -1,7 +1,7 @@ { "domain": "automatic", "name": "Automatic", - "documentation": "https://www.home-assistant.io/components/automatic", + "documentation": "https://www.home-assistant.io/integrations/automatic", "requirements": [ "aioautomatic==0.6.5" ], diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 935cc7a9175058..79a6877692ed94 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -1,7 +1,7 @@ { "domain": "automation", "name": "Automation", - "documentation": "https://www.home-assistant.io/components/automation", + "documentation": "https://www.home-assistant.io/integrations/automation", "requirements": [], "dependencies": [ "device_automation", diff --git a/homeassistant/components/avea/manifest.json b/homeassistant/components/avea/manifest.json index 273cefcbcfa83d..4fb9ab9f4205c7 100644 --- a/homeassistant/components/avea/manifest.json +++ b/homeassistant/components/avea/manifest.json @@ -1,7 +1,7 @@ { "domain": "avea", "name": "Elgato Avea", - "documentation": "https://www.home-assistant.io/components/avea", + "documentation": "https://www.home-assistant.io/integrations/avea", "dependencies": [], "codeowners": ["@pattyland"], "requirements": ["avea==1.2.8"] diff --git a/homeassistant/components/avion/manifest.json b/homeassistant/components/avion/manifest.json index e7d97f13313088..8bb8b56cb9d858 100644 --- a/homeassistant/components/avion/manifest.json +++ b/homeassistant/components/avion/manifest.json @@ -1,7 +1,7 @@ { "domain": "avion", "name": "Avion", - "documentation": "https://www.home-assistant.io/components/avion", + "documentation": "https://www.home-assistant.io/integrations/avion", "requirements": [ "avion==0.10" ], diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index dfa5bec3c00309..f4e632cb7d23d3 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -1,7 +1,7 @@ { "domain": "awair", "name": "Awair", - "documentation": "https://www.home-assistant.io/components/awair", + "documentation": "https://www.home-assistant.io/integrations/awair", "requirements": [ "python_awair==0.0.4" ], diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index a473a23f917ab0..a4543cc4b0f5b7 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -1,7 +1,7 @@ { "domain": "aws", "name": "Aws", - "documentation": "https://www.home-assistant.io/components/aws", + "documentation": "https://www.home-assistant.io/integrations/aws", "requirements": [ "aiobotocore==0.10.2" ], diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 2b1bef9081e9cd..348f6148386328 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -2,7 +2,7 @@ "domain": "axis", "name": "Axis", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/axis", + "documentation": "https://www.home-assistant.io/integrations/axis", "requirements": ["axis==25"], "dependencies": [], "zeroconf": ["_axis-video._tcp.local."], diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json index e2223fc97c3b3b..0b705bddc297fc 100644 --- a/homeassistant/components/azure_event_hub/manifest.json +++ b/homeassistant/components/azure_event_hub/manifest.json @@ -1,7 +1,7 @@ { "domain": "azure_event_hub", "name": "Azure Event Hub", - "documentation": "https://www.home-assistant.io/components/azure_event_hub", + "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", "requirements": ["azure-eventhub==1.3.1"], "dependencies": [], "codeowners": ["@eavanvalkenburg"] diff --git a/homeassistant/components/baidu/manifest.json b/homeassistant/components/baidu/manifest.json index 1dea1b7e37b147..756a1c5adcc1bb 100644 --- a/homeassistant/components/baidu/manifest.json +++ b/homeassistant/components/baidu/manifest.json @@ -1,7 +1,7 @@ { "domain": "baidu", "name": "Baidu", - "documentation": "https://www.home-assistant.io/components/baidu", + "documentation": "https://www.home-assistant.io/integrations/baidu", "requirements": [ "baidu-aip==1.6.6" ], diff --git a/homeassistant/components/bayesian/manifest.json b/homeassistant/components/bayesian/manifest.json index 25480ac8bdc870..7060dbd396bd03 100644 --- a/homeassistant/components/bayesian/manifest.json +++ b/homeassistant/components/bayesian/manifest.json @@ -1,7 +1,7 @@ { "domain": "bayesian", "name": "Bayesian", - "documentation": "https://www.home-assistant.io/components/bayesian", + "documentation": "https://www.home-assistant.io/integrations/bayesian", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/bbb_gpio/manifest.json b/homeassistant/components/bbb_gpio/manifest.json index 5632836bfbb623..edd6032682812a 100644 --- a/homeassistant/components/bbb_gpio/manifest.json +++ b/homeassistant/components/bbb_gpio/manifest.json @@ -1,7 +1,7 @@ { "domain": "bbb_gpio", "name": "Bbb gpio", - "documentation": "https://www.home-assistant.io/components/bbb_gpio", + "documentation": "https://www.home-assistant.io/integrations/bbb_gpio", "requirements": [ "Adafruit_BBIO==1.0.0" ], diff --git a/homeassistant/components/bbox/manifest.json b/homeassistant/components/bbox/manifest.json index 54cd9a3af64dda..15a648167c5cc4 100644 --- a/homeassistant/components/bbox/manifest.json +++ b/homeassistant/components/bbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "bbox", "name": "Bbox", - "documentation": "https://www.home-assistant.io/components/bbox", + "documentation": "https://www.home-assistant.io/integrations/bbox", "requirements": [ "pybbox==0.0.5-alpha" ], diff --git a/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant/components/beewi_smartclim/manifest.json index 3e9ad732b74342..bc69efb64903fd 100644 --- a/homeassistant/components/beewi_smartclim/manifest.json +++ b/homeassistant/components/beewi_smartclim/manifest.json @@ -1,7 +1,7 @@ { "domain": "beewi_smartclim", "name": "BeeWi SmartClim BLE sensor", - "documentation": "https://www.home-assistant.io/components/beewi_smartclim", + "documentation": "https://www.home-assistant.io/integrations/beewi_smartclim", "requirements": [ "beewi_smartclim==0.0.7" ], diff --git a/homeassistant/components/bh1750/manifest.json b/homeassistant/components/bh1750/manifest.json index 90e62c783569cb..63816f967e7564 100644 --- a/homeassistant/components/bh1750/manifest.json +++ b/homeassistant/components/bh1750/manifest.json @@ -1,7 +1,7 @@ { "domain": "bh1750", "name": "Bh1750", - "documentation": "https://www.home-assistant.io/components/bh1750", + "documentation": "https://www.home-assistant.io/integrations/bh1750", "requirements": [ "i2csense==0.0.4", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/binary_sensor/manifest.json b/homeassistant/components/binary_sensor/manifest.json index d627351958d573..ea29314eb1b3de 100644 --- a/homeassistant/components/binary_sensor/manifest.json +++ b/homeassistant/components/binary_sensor/manifest.json @@ -1,7 +1,7 @@ { "domain": "binary_sensor", "name": "Binary sensor", - "documentation": "https://www.home-assistant.io/components/binary_sensor", + "documentation": "https://www.home-assistant.io/integrations/binary_sensor", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/bitcoin/manifest.json b/homeassistant/components/bitcoin/manifest.json index 85da99a68850cf..1ffc34fcd9df39 100644 --- a/homeassistant/components/bitcoin/manifest.json +++ b/homeassistant/components/bitcoin/manifest.json @@ -1,7 +1,7 @@ { "domain": "bitcoin", "name": "Bitcoin", - "documentation": "https://www.home-assistant.io/components/bitcoin", + "documentation": "https://www.home-assistant.io/integrations/bitcoin", "requirements": [ "blockchain==1.4.4" ], diff --git a/homeassistant/components/bizkaibus/manifest.json b/homeassistant/components/bizkaibus/manifest.json index 98cbbc9be56298..63c0494c2f18f5 100644 --- a/homeassistant/components/bizkaibus/manifest.json +++ b/homeassistant/components/bizkaibus/manifest.json @@ -1,7 +1,7 @@ { "domain": "bizkaibus", "name": "Bizkaibus", - "documentation": "https://www.home-assistant.io/components/bizkaibus", + "documentation": "https://www.home-assistant.io/integrations/bizkaibus", "dependencies": [], "codeowners": ["@UgaitzEtxebarria"], "requirements": ["bizkaibus==0.1.1"] diff --git a/homeassistant/components/blackbird/manifest.json b/homeassistant/components/blackbird/manifest.json index 9e3e41290ea373..c5b3a632c16ca2 100644 --- a/homeassistant/components/blackbird/manifest.json +++ b/homeassistant/components/blackbird/manifest.json @@ -1,7 +1,7 @@ { "domain": "blackbird", "name": "Blackbird", - "documentation": "https://www.home-assistant.io/components/blackbird", + "documentation": "https://www.home-assistant.io/integrations/blackbird", "requirements": [ "pyblackbird==0.5" ], diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 98c609731c6d31..a38ba0bd613b82 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -1,7 +1,7 @@ { "domain": "blink", "name": "Blink", - "documentation": "https://www.home-assistant.io/components/blink", + "documentation": "https://www.home-assistant.io/integrations/blink", "requirements": [ "blinkpy==0.14.1" ], diff --git a/homeassistant/components/blinksticklight/manifest.json b/homeassistant/components/blinksticklight/manifest.json index a5277c97d99386..0a6e25407900bd 100644 --- a/homeassistant/components/blinksticklight/manifest.json +++ b/homeassistant/components/blinksticklight/manifest.json @@ -1,7 +1,7 @@ { "domain": "blinksticklight", "name": "Blinksticklight", - "documentation": "https://www.home-assistant.io/components/blinksticklight", + "documentation": "https://www.home-assistant.io/integrations/blinksticklight", "requirements": [ "blinkstick==1.1.8" ], diff --git a/homeassistant/components/blinkt/manifest.json b/homeassistant/components/blinkt/manifest.json index c11583ed59ec3d..629bdebf27e6c4 100644 --- a/homeassistant/components/blinkt/manifest.json +++ b/homeassistant/components/blinkt/manifest.json @@ -1,7 +1,7 @@ { "domain": "blinkt", "name": "Blinkt", - "documentation": "https://www.home-assistant.io/components/blinkt", + "documentation": "https://www.home-assistant.io/integrations/blinkt", "requirements": [ "blinkt==0.1.0" ], diff --git a/homeassistant/components/blockchain/manifest.json b/homeassistant/components/blockchain/manifest.json index 8a2a9f7b71f00d..773b52e724bb9e 100644 --- a/homeassistant/components/blockchain/manifest.json +++ b/homeassistant/components/blockchain/manifest.json @@ -1,7 +1,7 @@ { "domain": "blockchain", "name": "Blockchain", - "documentation": "https://www.home-assistant.io/components/blockchain", + "documentation": "https://www.home-assistant.io/integrations/blockchain", "requirements": [ "python-blockchain-api==0.0.2" ], diff --git a/homeassistant/components/bloomsky/manifest.json b/homeassistant/components/bloomsky/manifest.json index 3a780507dd59c4..49da6534bae4ff 100644 --- a/homeassistant/components/bloomsky/manifest.json +++ b/homeassistant/components/bloomsky/manifest.json @@ -1,7 +1,7 @@ { "domain": "bloomsky", "name": "Bloomsky", - "documentation": "https://www.home-assistant.io/components/bloomsky", + "documentation": "https://www.home-assistant.io/integrations/bloomsky", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index 7731f845005ded..e64e3e61f16ea0 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -1,7 +1,7 @@ { "domain": "bluesound", "name": "Bluesound", - "documentation": "https://www.home-assistant.io/components/bluesound", + "documentation": "https://www.home-assistant.io/integrations/bluesound", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index d2f8f10290e5ea..30ed924a9dc404 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "bluetooth_le_tracker", "name": "Bluetooth le tracker", - "documentation": "https://www.home-assistant.io/components/bluetooth_le_tracker", + "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", "requirements": [ "pygatt[GATTTOOL]==4.0.1" ], diff --git a/homeassistant/components/bluetooth_tracker/manifest.json b/homeassistant/components/bluetooth_tracker/manifest.json index c853bc5a83801d..20fe51c561bf2d 100644 --- a/homeassistant/components/bluetooth_tracker/manifest.json +++ b/homeassistant/components/bluetooth_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "bluetooth_tracker", "name": "Bluetooth tracker", - "documentation": "https://www.home-assistant.io/components/bluetooth_tracker", + "documentation": "https://www.home-assistant.io/integrations/bluetooth_tracker", "requirements": [ "bt_proximity==0.2", "pybluez==0.22" diff --git a/homeassistant/components/bme280/manifest.json b/homeassistant/components/bme280/manifest.json index 2342c8418ebce2..9d01b301d432d4 100644 --- a/homeassistant/components/bme280/manifest.json +++ b/homeassistant/components/bme280/manifest.json @@ -1,7 +1,7 @@ { "domain": "bme280", "name": "Bme280", - "documentation": "https://www.home-assistant.io/components/bme280", + "documentation": "https://www.home-assistant.io/integrations/bme280", "requirements": [ "i2csense==0.0.4", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/bme680/manifest.json b/homeassistant/components/bme680/manifest.json index 976be85ca9413e..c062d14f8c9684 100644 --- a/homeassistant/components/bme680/manifest.json +++ b/homeassistant/components/bme680/manifest.json @@ -1,7 +1,7 @@ { "domain": "bme680", "name": "Bme680", - "documentation": "https://www.home-assistant.io/components/bme680", + "documentation": "https://www.home-assistant.io/integrations/bme680", "requirements": [ "bme680==1.0.5", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 0cc875c50f9f51..29366715d239a3 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -1,7 +1,7 @@ { "domain": "bmw_connected_drive", "name": "BMW Connected Drive", - "documentation": "https://www.home-assistant.io/components/bmw_connected_drive", + "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "requirements": [ "bimmer_connected==0.6.0" ], diff --git a/homeassistant/components/bom/manifest.json b/homeassistant/components/bom/manifest.json index eb1f1d8ca9428e..b2e7eb08ef6e5d 100644 --- a/homeassistant/components/bom/manifest.json +++ b/homeassistant/components/bom/manifest.json @@ -1,7 +1,7 @@ { "domain": "bom", "name": "Bom", - "documentation": "https://www.home-assistant.io/components/bom", + "documentation": "https://www.home-assistant.io/integrations/bom", "requirements": [ "bomradarloop==0.1.3" ], diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 52e8e1bec76bc7..e49e45865c4611 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -1,7 +1,7 @@ { "domain": "braviatv", "name": "Braviatv", - "documentation": "https://www.home-assistant.io/components/braviatv", + "documentation": "https://www.home-assistant.io/integrations/braviatv", "requirements": [ "braviarc-homeassistant==0.3.7.dev0", "getmac==0.8.1" diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 45ed2003026fd2..d77c32966b19e0 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -1,7 +1,7 @@ { "domain": "broadlink", "name": "Broadlink", - "documentation": "https://www.home-assistant.io/components/broadlink", + "documentation": "https://www.home-assistant.io/integrations/broadlink", "requirements": [ "broadlink==0.11.1" ], diff --git a/homeassistant/components/brottsplatskartan/manifest.json b/homeassistant/components/brottsplatskartan/manifest.json index d3b0657fed82e8..f3dd46c96fc406 100644 --- a/homeassistant/components/brottsplatskartan/manifest.json +++ b/homeassistant/components/brottsplatskartan/manifest.json @@ -1,7 +1,7 @@ { "domain": "brottsplatskartan", "name": "Brottsplatskartan", - "documentation": "https://www.home-assistant.io/components/brottsplatskartan", + "documentation": "https://www.home-assistant.io/integrations/brottsplatskartan", "requirements": [ "brottsplatskartan==0.0.1" ], diff --git a/homeassistant/components/browser/manifest.json b/homeassistant/components/browser/manifest.json index 61823564fe9180..2905bfcfe96138 100644 --- a/homeassistant/components/browser/manifest.json +++ b/homeassistant/components/browser/manifest.json @@ -1,7 +1,7 @@ { "domain": "browser", "name": "Browser", - "documentation": "https://www.home-assistant.io/components/browser", + "documentation": "https://www.home-assistant.io/integrations/browser", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index a47e3f69d5cf08..6ee4344b946fac 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -1,7 +1,7 @@ { "domain": "brunt", "name": "Brunt", - "documentation": "https://www.home-assistant.io/components/brunt", + "documentation": "https://www.home-assistant.io/integrations/brunt", "requirements": [ "brunt==0.1.3" ], diff --git a/homeassistant/components/bt_home_hub_5/manifest.json b/homeassistant/components/bt_home_hub_5/manifest.json index 927d9ea941230b..bee9cefce17fd1 100644 --- a/homeassistant/components/bt_home_hub_5/manifest.json +++ b/homeassistant/components/bt_home_hub_5/manifest.json @@ -1,7 +1,7 @@ { "domain": "bt_home_hub_5", "name": "Bt home hub 5", - "documentation": "https://www.home-assistant.io/components/bt_home_hub_5", + "documentation": "https://www.home-assistant.io/integrations/bt_home_hub_5", "requirements": [ "bthomehub5-devicelist==0.1.1" ], diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index 725541082e701d..985f3012422d91 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -1,7 +1,7 @@ { "domain": "bt_smarthub", "name": "Bt smarthub", - "documentation": "https://www.home-assistant.io/components/bt_smarthub", + "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", "requirements": [ "btsmarthub_devicelist==0.1.3" ], diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index d25a2526a5dc37..cc3c3b02981596 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -1,7 +1,7 @@ { "domain": "buienradar", "name": "Buienradar", - "documentation": "https://www.home-assistant.io/components/buienradar", + "documentation": "https://www.home-assistant.io/integrations/buienradar", "requirements": [ "buienradar==1.0.1" ], diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 55cd555d989554..896ace7ba6a686 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -1,7 +1,7 @@ { "domain": "caldav", "name": "Caldav", - "documentation": "https://www.home-assistant.io/components/caldav", + "documentation": "https://www.home-assistant.io/integrations/caldav", "requirements": [ "caldav==0.6.1" ], diff --git a/homeassistant/components/calendar/manifest.json b/homeassistant/components/calendar/manifest.json index 3a09cd090a523d..3e6ee8422b4cd3 100644 --- a/homeassistant/components/calendar/manifest.json +++ b/homeassistant/components/calendar/manifest.json @@ -1,7 +1,7 @@ { "domain": "calendar", "name": "Calendar", - "documentation": "https://www.home-assistant.io/components/calendar", + "documentation": "https://www.home-assistant.io/integrations/calendar", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index 3af6a15ca5249f..a3395965e4f749 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -1,7 +1,7 @@ { "domain": "camera", "name": "Camera", - "documentation": "https://www.home-assistant.io/components/camera", + "documentation": "https://www.home-assistant.io/integrations/camera", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index 346c1c99f6df6b..f76d521853d231 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -1,7 +1,7 @@ { "domain": "canary", "name": "Canary", - "documentation": "https://www.home-assistant.io/components/canary", + "documentation": "https://www.home-assistant.io/integrations/canary", "requirements": [ "py-canary==0.5.0" ], diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 84a6a6e2934fd6..b6776a17f7cba0 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -2,7 +2,7 @@ "domain": "cast", "name": "Cast", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/cast", + "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==4.0.1"], "dependencies": [], "zeroconf": ["_googlecast._tcp.local."], diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 781f27afb5f115..97f72f2ad1194a 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -1,7 +1,7 @@ { "domain": "cert_expiry", "name": "Cert expiry", - "documentation": "https://www.home-assistant.io/components/cert_expiry", + "documentation": "https://www.home-assistant.io/integrations/cert_expiry", "requirements": [], "config_flow": true, "dependencies": [], diff --git a/homeassistant/components/channels/manifest.json b/homeassistant/components/channels/manifest.json index 152c7d3a2dc5eb..c6ef7f8f127e11 100644 --- a/homeassistant/components/channels/manifest.json +++ b/homeassistant/components/channels/manifest.json @@ -1,7 +1,7 @@ { "domain": "channels", "name": "Channels", - "documentation": "https://www.home-assistant.io/components/channels", + "documentation": "https://www.home-assistant.io/integrations/channels", "requirements": [ "pychannels==1.0.0" ], diff --git a/homeassistant/components/cisco_ios/manifest.json b/homeassistant/components/cisco_ios/manifest.json index 9a12ba252e374a..4a04ffa32e6962 100644 --- a/homeassistant/components/cisco_ios/manifest.json +++ b/homeassistant/components/cisco_ios/manifest.json @@ -1,7 +1,7 @@ { "domain": "cisco_ios", "name": "Cisco ios", - "documentation": "https://www.home-assistant.io/components/cisco_ios", + "documentation": "https://www.home-assistant.io/integrations/cisco_ios", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index abdd2400311f24..b4bc2ff86d950a 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -1,7 +1,7 @@ { "domain": "cisco_mobility_express", "name": "Cisco mobility express", - "documentation": "https://www.home-assistant.io/components/cisco_mobility_express", + "documentation": "https://www.home-assistant.io/integrations/cisco_mobility_express", "requirements": [ "ciscomobilityexpress==0.3.3" ], diff --git a/homeassistant/components/cisco_webex_teams/manifest.json b/homeassistant/components/cisco_webex_teams/manifest.json index 21c4efe071c95c..3560b1e7fcd318 100644 --- a/homeassistant/components/cisco_webex_teams/manifest.json +++ b/homeassistant/components/cisco_webex_teams/manifest.json @@ -1,7 +1,7 @@ { "domain": "cisco_webex_teams", "name": "Cisco webex teams", - "documentation": "https://www.home-assistant.io/components/cisco_webex_teams", + "documentation": "https://www.home-assistant.io/integrations/cisco_webex_teams", "requirements": [ "webexteamssdk==1.1.1" ], diff --git a/homeassistant/components/ciscospark/manifest.json b/homeassistant/components/ciscospark/manifest.json index 926925a7bf19ec..8058088bf8ac6f 100644 --- a/homeassistant/components/ciscospark/manifest.json +++ b/homeassistant/components/ciscospark/manifest.json @@ -1,7 +1,7 @@ { "domain": "ciscospark", "name": "Ciscospark", - "documentation": "https://www.home-assistant.io/components/ciscospark", + "documentation": "https://www.home-assistant.io/integrations/ciscospark", "requirements": [ "ciscosparkapi==0.4.2" ], diff --git a/homeassistant/components/citybikes/manifest.json b/homeassistant/components/citybikes/manifest.json index ea1ceaa9531a54..f46c7ba708f688 100644 --- a/homeassistant/components/citybikes/manifest.json +++ b/homeassistant/components/citybikes/manifest.json @@ -1,7 +1,7 @@ { "domain": "citybikes", "name": "Citybikes", - "documentation": "https://www.home-assistant.io/components/citybikes", + "documentation": "https://www.home-assistant.io/integrations/citybikes", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/clementine/manifest.json b/homeassistant/components/clementine/manifest.json index 4d835ed4e7c2d1..35368dd6cdfa00 100644 --- a/homeassistant/components/clementine/manifest.json +++ b/homeassistant/components/clementine/manifest.json @@ -1,7 +1,7 @@ { "domain": "clementine", "name": "Clementine", - "documentation": "https://www.home-assistant.io/components/clementine", + "documentation": "https://www.home-assistant.io/integrations/clementine", "requirements": [ "python-clementine-remote==1.0.1" ], diff --git a/homeassistant/components/clickatell/manifest.json b/homeassistant/components/clickatell/manifest.json index ffd550eebee871..a10da6e1cc0ca0 100644 --- a/homeassistant/components/clickatell/manifest.json +++ b/homeassistant/components/clickatell/manifest.json @@ -1,7 +1,7 @@ { "domain": "clickatell", "name": "Clickatell", - "documentation": "https://www.home-assistant.io/components/clickatell", + "documentation": "https://www.home-assistant.io/integrations/clickatell", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/clicksend/manifest.json b/homeassistant/components/clicksend/manifest.json index 3831982509431e..6a28b3c30ca415 100644 --- a/homeassistant/components/clicksend/manifest.json +++ b/homeassistant/components/clicksend/manifest.json @@ -1,7 +1,7 @@ { "domain": "clicksend", "name": "Clicksend", - "documentation": "https://www.home-assistant.io/components/clicksend", + "documentation": "https://www.home-assistant.io/integrations/clicksend", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/clicksend_tts/manifest.json b/homeassistant/components/clicksend_tts/manifest.json index c2a86f426e43e0..8aa3eacf405f21 100644 --- a/homeassistant/components/clicksend_tts/manifest.json +++ b/homeassistant/components/clicksend_tts/manifest.json @@ -1,7 +1,7 @@ { "domain": "clicksend_tts", "name": "Clicksend tts", - "documentation": "https://www.home-assistant.io/components/clicksend_tts", + "documentation": "https://www.home-assistant.io/integrations/clicksend_tts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/climate/manifest.json b/homeassistant/components/climate/manifest.json index ca5312e7670b1c..5933eaf9071e23 100644 --- a/homeassistant/components/climate/manifest.json +++ b/homeassistant/components/climate/manifest.json @@ -1,7 +1,7 @@ { "domain": "climate", "name": "Climate", - "documentation": "https://www.home-assistant.io/components/climate", + "documentation": "https://www.home-assistant.io/integrations/climate", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 3daeac43da942c..b15fa32cb1378c 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "cloud", "name": "Cloud", - "documentation": "https://www.home-assistant.io/components/cloud", + "documentation": "https://www.home-assistant.io/integrations/cloud", "requirements": ["hass-nabucasa==0.17"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/components/cloudflare/manifest.json b/homeassistant/components/cloudflare/manifest.json index 7716ae65c4e1a1..78bc6de99c8187 100644 --- a/homeassistant/components/cloudflare/manifest.json +++ b/homeassistant/components/cloudflare/manifest.json @@ -1,7 +1,7 @@ { "domain": "cloudflare", "name": "Cloudflare", - "documentation": "https://www.home-assistant.io/components/cloudflare", + "documentation": "https://www.home-assistant.io/integrations/cloudflare", "requirements": [ "pycfdns==0.0.1" ], diff --git a/homeassistant/components/cmus/manifest.json b/homeassistant/components/cmus/manifest.json index 1528f4252b1098..fe5b8e155c266f 100644 --- a/homeassistant/components/cmus/manifest.json +++ b/homeassistant/components/cmus/manifest.json @@ -1,7 +1,7 @@ { "domain": "cmus", "name": "Cmus", - "documentation": "https://www.home-assistant.io/components/cmus", + "documentation": "https://www.home-assistant.io/integrations/cmus", "requirements": [ "pycmus==0.1.1" ], diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index ac42e374fdd23e..f07813b3db5a14 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -1,7 +1,7 @@ { "domain": "co2signal", "name": "Co2signal", - "documentation": "https://www.home-assistant.io/components/co2signal", + "documentation": "https://www.home-assistant.io/integrations/co2signal", "requirements": [ "co2signal==0.4.2" ], diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index 5f8a189c7d129f..2da323f08158fc 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -1,7 +1,7 @@ { "domain": "coinbase", "name": "Coinbase", - "documentation": "https://www.home-assistant.io/components/coinbase", + "documentation": "https://www.home-assistant.io/integrations/coinbase", "requirements": [ "coinbase==2.1.0" ], diff --git a/homeassistant/components/coinmarketcap/manifest.json b/homeassistant/components/coinmarketcap/manifest.json index 0afb1b1c28f317..ec9aec6c65449e 100644 --- a/homeassistant/components/coinmarketcap/manifest.json +++ b/homeassistant/components/coinmarketcap/manifest.json @@ -1,7 +1,7 @@ { "domain": "coinmarketcap", "name": "Coinmarketcap", - "documentation": "https://www.home-assistant.io/components/coinmarketcap", + "documentation": "https://www.home-assistant.io/integrations/coinmarketcap", "requirements": [ "coinmarketcap==5.0.3" ], diff --git a/homeassistant/components/comed_hourly_pricing/manifest.json b/homeassistant/components/comed_hourly_pricing/manifest.json index 47c7931a0e93d2..89fbb84e82d30a 100644 --- a/homeassistant/components/comed_hourly_pricing/manifest.json +++ b/homeassistant/components/comed_hourly_pricing/manifest.json @@ -1,7 +1,7 @@ { "domain": "comed_hourly_pricing", "name": "Comed hourly pricing", - "documentation": "https://www.home-assistant.io/components/comed_hourly_pricing", + "documentation": "https://www.home-assistant.io/integrations/comed_hourly_pricing", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/comfoconnect/manifest.json b/homeassistant/components/comfoconnect/manifest.json index 03319aeffa89b3..57daba7fdbd1dd 100644 --- a/homeassistant/components/comfoconnect/manifest.json +++ b/homeassistant/components/comfoconnect/manifest.json @@ -1,7 +1,7 @@ { "domain": "comfoconnect", "name": "Comfoconnect", - "documentation": "https://www.home-assistant.io/components/comfoconnect", + "documentation": "https://www.home-assistant.io/integrations/comfoconnect", "requirements": [ "pycomfoconnect==0.3" ], diff --git a/homeassistant/components/command_line/manifest.json b/homeassistant/components/command_line/manifest.json index ff94522210d818..4d7dfc8994ffec 100644 --- a/homeassistant/components/command_line/manifest.json +++ b/homeassistant/components/command_line/manifest.json @@ -1,7 +1,7 @@ { "domain": "command_line", "name": "Command line", - "documentation": "https://www.home-assistant.io/components/command_line", + "documentation": "https://www.home-assistant.io/integrations/command_line", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/concord232/manifest.json b/homeassistant/components/concord232/manifest.json index f26da49d3f1b84..f5ff021b6d1766 100644 --- a/homeassistant/components/concord232/manifest.json +++ b/homeassistant/components/concord232/manifest.json @@ -1,7 +1,7 @@ { "domain": "concord232", "name": "Concord232", - "documentation": "https://www.home-assistant.io/components/concord232", + "documentation": "https://www.home-assistant.io/integrations/concord232", "requirements": [ "concord232==0.15" ], diff --git a/homeassistant/components/config/manifest.json b/homeassistant/components/config/manifest.json index 9c0c50a25957ee..c6e99b43c494c8 100644 --- a/homeassistant/components/config/manifest.json +++ b/homeassistant/components/config/manifest.json @@ -1,7 +1,7 @@ { "domain": "config", "name": "Config", - "documentation": "https://www.home-assistant.io/components/config", + "documentation": "https://www.home-assistant.io/integrations/config", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/configurator/manifest.json b/homeassistant/components/configurator/manifest.json index f01fe7324fa493..10c067d4a22525 100644 --- a/homeassistant/components/configurator/manifest.json +++ b/homeassistant/components/configurator/manifest.json @@ -1,7 +1,7 @@ { "domain": "configurator", "name": "Configurator", - "documentation": "https://www.home-assistant.io/components/configurator", + "documentation": "https://www.home-assistant.io/integrations/configurator", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index ddd3b6205efdd4..0d6d67cf254d51 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -1,7 +1,7 @@ { "domain": "conversation", "name": "Conversation", - "documentation": "https://www.home-assistant.io/components/conversation", + "documentation": "https://www.home-assistant.io/integrations/conversation", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 9489dc72689e5b..69ab8ee3c4b199 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -1,7 +1,7 @@ { "domain": "coolmaster", "name": "Coolmaster", - "documentation": "https://www.home-assistant.io/components/coolmaster", + "documentation": "https://www.home-assistant.io/integrations/coolmaster", "requirements": [ "pycoolmasternet==0.0.4" ], diff --git a/homeassistant/components/counter/manifest.json b/homeassistant/components/counter/manifest.json index ae7066ea82d28d..3fd533054d8eb2 100644 --- a/homeassistant/components/counter/manifest.json +++ b/homeassistant/components/counter/manifest.json @@ -1,7 +1,7 @@ { "domain": "counter", "name": "Counter", - "documentation": "https://www.home-assistant.io/components/counter", + "documentation": "https://www.home-assistant.io/integrations/counter", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/cover/manifest.json b/homeassistant/components/cover/manifest.json index da5a644334cb5e..1d82dcb5b0c7a4 100644 --- a/homeassistant/components/cover/manifest.json +++ b/homeassistant/components/cover/manifest.json @@ -1,7 +1,7 @@ { "domain": "cover", "name": "Cover", - "documentation": "https://www.home-assistant.io/components/cover", + "documentation": "https://www.home-assistant.io/integrations/cover", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/cppm_tracker/manifest.json b/homeassistant/components/cppm_tracker/manifest.json index 5a1bdbf5a452ec..b2abc40dca23f2 100644 --- a/homeassistant/components/cppm_tracker/manifest.json +++ b/homeassistant/components/cppm_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "cppm_tracker", "name": "Cppm tracker", - "documentation": "https://www.home-assistant.io/components/cppm_tracker", + "documentation": "https://www.home-assistant.io/integrations/cppm_tracker", "requirements": [ "clearpasspy==1.0.2" ], diff --git a/homeassistant/components/cpuspeed/manifest.json b/homeassistant/components/cpuspeed/manifest.json index 9034cb7740d0f3..7950cad9b8bbc8 100644 --- a/homeassistant/components/cpuspeed/manifest.json +++ b/homeassistant/components/cpuspeed/manifest.json @@ -1,7 +1,7 @@ { "domain": "cpuspeed", "name": "Cpuspeed", - "documentation": "https://www.home-assistant.io/components/cpuspeed", + "documentation": "https://www.home-assistant.io/integrations/cpuspeed", "requirements": [ "py-cpuinfo==5.0.0" ], diff --git a/homeassistant/components/crimereports/manifest.json b/homeassistant/components/crimereports/manifest.json index 0f74216b9b2156..c5cc45d3192542 100644 --- a/homeassistant/components/crimereports/manifest.json +++ b/homeassistant/components/crimereports/manifest.json @@ -1,7 +1,7 @@ { "domain": "crimereports", "name": "Crimereports", - "documentation": "https://www.home-assistant.io/components/crimereports", + "documentation": "https://www.home-assistant.io/integrations/crimereports", "requirements": [ "crimereports==1.0.1" ], diff --git a/homeassistant/components/cups/manifest.json b/homeassistant/components/cups/manifest.json index def2846c4ca3b6..e30d64510ff4ec 100644 --- a/homeassistant/components/cups/manifest.json +++ b/homeassistant/components/cups/manifest.json @@ -1,7 +1,7 @@ { "domain": "cups", "name": "Cups", - "documentation": "https://www.home-assistant.io/components/cups", + "documentation": "https://www.home-assistant.io/integrations/cups", "requirements": [ "pycups==1.9.73" ], diff --git a/homeassistant/components/currencylayer/manifest.json b/homeassistant/components/currencylayer/manifest.json index 7064590bf25877..2db35dead60bfb 100644 --- a/homeassistant/components/currencylayer/manifest.json +++ b/homeassistant/components/currencylayer/manifest.json @@ -1,7 +1,7 @@ { "domain": "currencylayer", "name": "Currencylayer", - "documentation": "https://www.home-assistant.io/components/currencylayer", + "documentation": "https://www.home-assistant.io/integrations/currencylayer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index a60355efa0bac7..f81cb1715807c0 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -2,7 +2,7 @@ "domain": "daikin", "name": "Daikin", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/daikin", + "documentation": "https://www.home-assistant.io/integrations/daikin", "requirements": [ "pydaikin==1.6.1" ], diff --git a/homeassistant/components/danfoss_air/manifest.json b/homeassistant/components/danfoss_air/manifest.json index a210b5a78d1d50..189b685d4ce480 100644 --- a/homeassistant/components/danfoss_air/manifest.json +++ b/homeassistant/components/danfoss_air/manifest.json @@ -1,7 +1,7 @@ { "domain": "danfoss_air", "name": "Danfoss air", - "documentation": "https://www.home-assistant.io/components/danfoss_air", + "documentation": "https://www.home-assistant.io/integrations/danfoss_air", "requirements": [ "pydanfossair==0.1.0" ], diff --git a/homeassistant/components/darksky/manifest.json b/homeassistant/components/darksky/manifest.json index e4e6482484cd21..0046e51463e2f8 100644 --- a/homeassistant/components/darksky/manifest.json +++ b/homeassistant/components/darksky/manifest.json @@ -1,7 +1,7 @@ { "domain": "darksky", "name": "Darksky", - "documentation": "https://www.home-assistant.io/components/darksky", + "documentation": "https://www.home-assistant.io/integrations/darksky", "requirements": [ "python-forecastio==1.4.0" ], diff --git a/homeassistant/components/datadog/manifest.json b/homeassistant/components/datadog/manifest.json index 40a2e82d53ac3e..36de2ff2101b24 100644 --- a/homeassistant/components/datadog/manifest.json +++ b/homeassistant/components/datadog/manifest.json @@ -1,7 +1,7 @@ { "domain": "datadog", "name": "Datadog", - "documentation": "https://www.home-assistant.io/components/datadog", + "documentation": "https://www.home-assistant.io/integrations/datadog", "requirements": [ "datadog==0.15.0" ], diff --git a/homeassistant/components/ddwrt/manifest.json b/homeassistant/components/ddwrt/manifest.json index 3c877a34841478..874b24e370b540 100644 --- a/homeassistant/components/ddwrt/manifest.json +++ b/homeassistant/components/ddwrt/manifest.json @@ -1,7 +1,7 @@ { "domain": "ddwrt", "name": "Ddwrt", - "documentation": "https://www.home-assistant.io/components/ddwrt", + "documentation": "https://www.home-assistant.io/integrations/ddwrt", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 4aec29008dee80..1e5cd4144253d2 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -2,7 +2,7 @@ "domain": "deconz", "name": "Deconz", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/deconz", + "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ "pydeconz==63" ], diff --git a/homeassistant/components/decora/manifest.json b/homeassistant/components/decora/manifest.json index 923a543e82788b..5142b5fb2e2cd6 100644 --- a/homeassistant/components/decora/manifest.json +++ b/homeassistant/components/decora/manifest.json @@ -1,7 +1,7 @@ { "domain": "decora", "name": "Decora", - "documentation": "https://www.home-assistant.io/components/decora", + "documentation": "https://www.home-assistant.io/integrations/decora", "requirements": [ "bluepy==1.1.4", "decora==0.6" diff --git a/homeassistant/components/decora_wifi/manifest.json b/homeassistant/components/decora_wifi/manifest.json index 42ab6bfd6c1668..14b8829fae8671 100644 --- a/homeassistant/components/decora_wifi/manifest.json +++ b/homeassistant/components/decora_wifi/manifest.json @@ -1,7 +1,7 @@ { "domain": "decora_wifi", "name": "Decora wifi", - "documentation": "https://www.home-assistant.io/components/decora_wifi", + "documentation": "https://www.home-assistant.io/integrations/decora_wifi", "requirements": [ "decora_wifi==1.4" ], diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 6969d9bba7e9a4..a240599c42029c 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -1,7 +1,7 @@ { "domain": "default_config", "name": "Default config", - "documentation": "https://www.home-assistant.io/components/default_config", + "documentation": "https://www.home-assistant.io/integrations/default_config", "requirements": [], "dependencies": [ "automation", diff --git a/homeassistant/components/delijn/manifest.json b/homeassistant/components/delijn/manifest.json index 90e1a4e3b151c6..2d550a0851f839 100644 --- a/homeassistant/components/delijn/manifest.json +++ b/homeassistant/components/delijn/manifest.json @@ -1,7 +1,7 @@ { "domain": "delijn", "name": "De Lijn", - "documentation": "https://www.home-assistant.io/components/delijn", + "documentation": "https://www.home-assistant.io/integrations/delijn", "dependencies": [], "codeowners": ["@bollewolle"], "requirements": ["pydelijn==0.5.1"] diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json index d33a140cedb914..b2eb3ada65f0b3 100644 --- a/homeassistant/components/deluge/manifest.json +++ b/homeassistant/components/deluge/manifest.json @@ -1,7 +1,7 @@ { "domain": "deluge", "name": "Deluge", - "documentation": "https://www.home-assistant.io/components/deluge", + "documentation": "https://www.home-assistant.io/integrations/deluge", "requirements": [ "deluge-client==1.7.1" ], diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index 4f167ecae25afa..a4802c3b67b968 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -1,7 +1,7 @@ { "domain": "demo", "name": "Demo", - "documentation": "https://www.home-assistant.io/components/demo", + "documentation": "https://www.home-assistant.io/integrations/demo", "requirements": [], "dependencies": [ "conversation", diff --git a/homeassistant/components/denon/manifest.json b/homeassistant/components/denon/manifest.json index 2068b72fa9d494..92b2aebab40f5f 100644 --- a/homeassistant/components/denon/manifest.json +++ b/homeassistant/components/denon/manifest.json @@ -1,7 +1,7 @@ { "domain": "denon", "name": "Denon", - "documentation": "https://www.home-assistant.io/components/denon", + "documentation": "https://www.home-assistant.io/integrations/denon", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 34699d666ad02d..9e084c78e21c92 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -1,7 +1,7 @@ { "domain": "denonavr", "name": "Denonavr", - "documentation": "https://www.home-assistant.io/components/denonavr", + "documentation": "https://www.home-assistant.io/integrations/denonavr", "requirements": [ "denonavr==0.7.10" ], diff --git a/homeassistant/components/deutsche_bahn/manifest.json b/homeassistant/components/deutsche_bahn/manifest.json index 463c7d03fbb236..9a2bf66601617b 100644 --- a/homeassistant/components/deutsche_bahn/manifest.json +++ b/homeassistant/components/deutsche_bahn/manifest.json @@ -1,7 +1,7 @@ { "domain": "deutsche_bahn", "name": "Deutsche bahn", - "documentation": "https://www.home-assistant.io/components/deutsche_bahn", + "documentation": "https://www.home-assistant.io/integrations/deutsche_bahn", "requirements": [ "schiene==0.23" ], diff --git a/homeassistant/components/device_automation/manifest.json b/homeassistant/components/device_automation/manifest.json index a95e9c4f68fbb1..50b1f9d357a85f 100644 --- a/homeassistant/components/device_automation/manifest.json +++ b/homeassistant/components/device_automation/manifest.json @@ -1,7 +1,7 @@ { "domain": "device_automation", "name": "Device automation", - "documentation": "https://www.home-assistant.io/components/device_automation", + "documentation": "https://www.home-assistant.io/integrations/device_automation", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/device_sun_light_trigger/manifest.json b/homeassistant/components/device_sun_light_trigger/manifest.json index 40ab85bc1e5fbb..3bea097b831f26 100644 --- a/homeassistant/components/device_sun_light_trigger/manifest.json +++ b/homeassistant/components/device_sun_light_trigger/manifest.json @@ -1,7 +1,7 @@ { "domain": "device_sun_light_trigger", "name": "Device sun light trigger", - "documentation": "https://www.home-assistant.io/components/device_sun_light_trigger", + "documentation": "https://www.home-assistant.io/integrations/device_sun_light_trigger", "requirements": [], "dependencies": [ "device_tracker", diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 7b32f7845a6d5b..0e1e0e8cd81a88 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "device_tracker", "name": "Device tracker", - "documentation": "https://www.home-assistant.io/components/device_tracker", + "documentation": "https://www.home-assistant.io/integrations/device_tracker", "requirements": [], "dependencies": [ "group", diff --git a/homeassistant/components/dht/manifest.json b/homeassistant/components/dht/manifest.json index 05889bdd326106..e1d9273b7975a1 100644 --- a/homeassistant/components/dht/manifest.json +++ b/homeassistant/components/dht/manifest.json @@ -1,7 +1,7 @@ { "domain": "dht", "name": "Dht", - "documentation": "https://www.home-assistant.io/components/dht", + "documentation": "https://www.home-assistant.io/integrations/dht", "requirements": [ "Adafruit-DHT==1.4.0" ], diff --git a/homeassistant/components/dialogflow/manifest.json b/homeassistant/components/dialogflow/manifest.json index aa8b584aecaa8f..d9e821c82fd6af 100644 --- a/homeassistant/components/dialogflow/manifest.json +++ b/homeassistant/components/dialogflow/manifest.json @@ -2,7 +2,7 @@ "domain": "dialogflow", "name": "Dialogflow", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/dialogflow", + "documentation": "https://www.home-assistant.io/integrations/dialogflow", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/digital_ocean/manifest.json b/homeassistant/components/digital_ocean/manifest.json index 2ef940f60bd963..eb19a5c3a85945 100644 --- a/homeassistant/components/digital_ocean/manifest.json +++ b/homeassistant/components/digital_ocean/manifest.json @@ -1,7 +1,7 @@ { "domain": "digital_ocean", "name": "Digital ocean", - "documentation": "https://www.home-assistant.io/components/digital_ocean", + "documentation": "https://www.home-assistant.io/integrations/digital_ocean", "requirements": [ "python-digitalocean==1.13.2" ], diff --git a/homeassistant/components/digitalloggers/manifest.json b/homeassistant/components/digitalloggers/manifest.json index 990b39b21a5fdb..4c58e090a95d71 100644 --- a/homeassistant/components/digitalloggers/manifest.json +++ b/homeassistant/components/digitalloggers/manifest.json @@ -1,7 +1,7 @@ { "domain": "digitalloggers", "name": "Digitalloggers", - "documentation": "https://www.home-assistant.io/components/digitalloggers", + "documentation": "https://www.home-assistant.io/integrations/digitalloggers", "requirements": [ "dlipower==0.7.165" ], diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index 7dbe6122ac1e34..8b65d7a680bdbe 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -1,7 +1,7 @@ { "domain": "directv", "name": "Directv", - "documentation": "https://www.home-assistant.io/components/directv", + "documentation": "https://www.home-assistant.io/integrations/directv", "requirements": [ "directpy==0.5" ], diff --git a/homeassistant/components/discogs/manifest.json b/homeassistant/components/discogs/manifest.json index ca304bce88bcff..18282f077817a6 100644 --- a/homeassistant/components/discogs/manifest.json +++ b/homeassistant/components/discogs/manifest.json @@ -1,7 +1,7 @@ { "domain": "discogs", "name": "Discogs", - "documentation": "https://www.home-assistant.io/components/discogs", + "documentation": "https://www.home-assistant.io/integrations/discogs", "requirements": [ "discogs_client==2.2.1" ], diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 31940c28c4ef7a..50c03bad25d18a 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -1,7 +1,7 @@ { "domain": "discord", "name": "Discord", - "documentation": "https://www.home-assistant.io/components/discord", + "documentation": "https://www.home-assistant.io/integrations/discord", "requirements": [ "discord.py==1.2.3" ], diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 845e1af15d4053..56d968189cf917 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -1,7 +1,7 @@ { "domain": "discovery", "name": "Discovery", - "documentation": "https://www.home-assistant.io/components/discovery", + "documentation": "https://www.home-assistant.io/integrations/discovery", "requirements": [ "netdisco==2.6.0" ], diff --git a/homeassistant/components/dlib_face_detect/manifest.json b/homeassistant/components/dlib_face_detect/manifest.json index c2ede62ee5b015..431afc080f442a 100644 --- a/homeassistant/components/dlib_face_detect/manifest.json +++ b/homeassistant/components/dlib_face_detect/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlib_face_detect", "name": "Dlib face detect", - "documentation": "https://www.home-assistant.io/components/dlib_face_detect", + "documentation": "https://www.home-assistant.io/integrations/dlib_face_detect", "requirements": [ "face_recognition==1.2.3" ], diff --git a/homeassistant/components/dlib_face_identify/manifest.json b/homeassistant/components/dlib_face_identify/manifest.json index 388017f78bb45a..2f764ae2817f5f 100644 --- a/homeassistant/components/dlib_face_identify/manifest.json +++ b/homeassistant/components/dlib_face_identify/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlib_face_identify", "name": "Dlib face identify", - "documentation": "https://www.home-assistant.io/components/dlib_face_identify", + "documentation": "https://www.home-assistant.io/integrations/dlib_face_identify", "requirements": [ "face_recognition==1.2.3" ], diff --git a/homeassistant/components/dlink/manifest.json b/homeassistant/components/dlink/manifest.json index 8f7d07eb0db39b..b0b8c60121aa7a 100644 --- a/homeassistant/components/dlink/manifest.json +++ b/homeassistant/components/dlink/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlink", "name": "Dlink", - "documentation": "https://www.home-assistant.io/components/dlink", + "documentation": "https://www.home-assistant.io/integrations/dlink", "requirements": [ "pyW215==0.6.0" ], diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index bf05d5c7f63fdd..008a1293e41c95 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -1,7 +1,7 @@ { "domain": "dlna_dmr", "name": "Dlna dmr", - "documentation": "https://www.home-assistant.io/components/dlna_dmr", + "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": [ "async-upnp-client==0.14.11" ], diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 544ac9b0fbafa5..4f3d84da476cac 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -1,7 +1,7 @@ { "domain": "dnsip", "name": "Dnsip", - "documentation": "https://www.home-assistant.io/components/dnsip", + "documentation": "https://www.home-assistant.io/integrations/dnsip", "requirements": [ "aiodns==2.0.0" ], diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json index f8d13b49f93327..933af93a77aa7c 100644 --- a/homeassistant/components/dominos/manifest.json +++ b/homeassistant/components/dominos/manifest.json @@ -1,7 +1,7 @@ { "domain": "dominos", "name": "Dominos", - "documentation": "https://www.home-assistant.io/components/dominos", + "documentation": "https://www.home-assistant.io/integrations/dominos", "requirements": [ "pizzapi==0.0.3" ], diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 75c1bd3dcd3814..e0dcb48527f91b 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -1,7 +1,7 @@ { "domain": "doods", "name": "DOODS - Distributed Outside Object Detection Service", - "documentation": "https://www.home-assistant.io/components/doods", + "documentation": "https://www.home-assistant.io/integrations/doods", "requirements": [ "pydoods==1.0.2" ], diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 3fb9fdc753b7d5..c9cdb32e18af31 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -1,7 +1,7 @@ { "domain": "doorbird", "name": "Doorbird", - "documentation": "https://www.home-assistant.io/components/doorbird", + "documentation": "https://www.home-assistant.io/integrations/doorbird", "requirements": [ "doorbirdpy==2.0.8" ], diff --git a/homeassistant/components/dovado/manifest.json b/homeassistant/components/dovado/manifest.json index 122d774c268221..ac0044c7a89127 100644 --- a/homeassistant/components/dovado/manifest.json +++ b/homeassistant/components/dovado/manifest.json @@ -1,7 +1,7 @@ { "domain": "dovado", "name": "Dovado", - "documentation": "https://www.home-assistant.io/components/dovado", + "documentation": "https://www.home-assistant.io/integrations/dovado", "requirements": [ "dovado==0.4.1" ], diff --git a/homeassistant/components/downloader/manifest.json b/homeassistant/components/downloader/manifest.json index 514509c004d508..241dadddf4ed7c 100644 --- a/homeassistant/components/downloader/manifest.json +++ b/homeassistant/components/downloader/manifest.json @@ -1,7 +1,7 @@ { "domain": "downloader", "name": "Downloader", - "documentation": "https://www.home-assistant.io/components/downloader", + "documentation": "https://www.home-assistant.io/integrations/downloader", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index 21c98d56d1d55a..250adab263b09f 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -1,7 +1,7 @@ { "domain": "dsmr", "name": "Dsmr", - "documentation": "https://www.home-assistant.io/components/dsmr", + "documentation": "https://www.home-assistant.io/integrations/dsmr", "requirements": [ "dsmr_parser==0.12" ], diff --git a/homeassistant/components/dte_energy_bridge/manifest.json b/homeassistant/components/dte_energy_bridge/manifest.json index fbf7a00f8e6b0a..f3ccbdeebb221e 100644 --- a/homeassistant/components/dte_energy_bridge/manifest.json +++ b/homeassistant/components/dte_energy_bridge/manifest.json @@ -1,7 +1,7 @@ { "domain": "dte_energy_bridge", "name": "Dte energy bridge", - "documentation": "https://www.home-assistant.io/components/dte_energy_bridge", + "documentation": "https://www.home-assistant.io/integrations/dte_energy_bridge", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/dublin_bus_transport/manifest.json b/homeassistant/components/dublin_bus_transport/manifest.json index fc13fddc9364e6..3e377f3a2eaffa 100644 --- a/homeassistant/components/dublin_bus_transport/manifest.json +++ b/homeassistant/components/dublin_bus_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "dublin_bus_transport", "name": "Dublin bus transport", - "documentation": "https://www.home-assistant.io/components/dublin_bus_transport", + "documentation": "https://www.home-assistant.io/integrations/dublin_bus_transport", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/duckdns/manifest.json b/homeassistant/components/duckdns/manifest.json index ed38d35346f220..9e0ad6c001cae8 100644 --- a/homeassistant/components/duckdns/manifest.json +++ b/homeassistant/components/duckdns/manifest.json @@ -1,7 +1,7 @@ { "domain": "duckdns", "name": "Duckdns", - "documentation": "https://www.home-assistant.io/components/duckdns", + "documentation": "https://www.home-assistant.io/integrations/duckdns", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/duke_energy/manifest.json b/homeassistant/components/duke_energy/manifest.json index 602dfec801fd75..131063ad81fb42 100644 --- a/homeassistant/components/duke_energy/manifest.json +++ b/homeassistant/components/duke_energy/manifest.json @@ -1,7 +1,7 @@ { "domain": "duke_energy", "name": "Duke energy", - "documentation": "https://www.home-assistant.io/components/duke_energy", + "documentation": "https://www.home-assistant.io/integrations/duke_energy", "requirements": [ "pydukeenergy==0.0.6" ], diff --git a/homeassistant/components/dunehd/manifest.json b/homeassistant/components/dunehd/manifest.json index 35e6c4a2449ed9..47250b32cbc2a6 100644 --- a/homeassistant/components/dunehd/manifest.json +++ b/homeassistant/components/dunehd/manifest.json @@ -1,7 +1,7 @@ { "domain": "dunehd", "name": "Dunehd", - "documentation": "https://www.home-assistant.io/components/dunehd", + "documentation": "https://www.home-assistant.io/integrations/dunehd", "requirements": [ "pdunehd==1.3" ], diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index a2b21a9e0bf946..a35fcbcdf6834d 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -1,7 +1,7 @@ { "domain": "dwd_weather_warnings", "name": "Dwd weather warnings", - "documentation": "https://www.home-assistant.io/components/dwd_weather_warnings", + "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/dweet/manifest.json b/homeassistant/components/dweet/manifest.json index e0a00620210afe..75d8cfb6054c16 100644 --- a/homeassistant/components/dweet/manifest.json +++ b/homeassistant/components/dweet/manifest.json @@ -1,7 +1,7 @@ { "domain": "dweet", "name": "Dweet", - "documentation": "https://www.home-assistant.io/components/dweet", + "documentation": "https://www.home-assistant.io/integrations/dweet", "requirements": [ "dweepy==0.3.0" ], diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 7b956dd96c8324..92940c8c1e19fa 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -1,7 +1,7 @@ { "domain": "dyson", "name": "Dyson", - "documentation": "https://www.home-assistant.io/components/dyson", + "documentation": "https://www.home-assistant.io/integrations/dyson", "requirements": [ "libpurecool==0.5.0" ], diff --git a/homeassistant/components/ebox/manifest.json b/homeassistant/components/ebox/manifest.json index 16b033df8fdc09..d32206da72607c 100644 --- a/homeassistant/components/ebox/manifest.json +++ b/homeassistant/components/ebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "ebox", "name": "Ebox", - "documentation": "https://www.home-assistant.io/components/ebox", + "documentation": "https://www.home-assistant.io/integrations/ebox", "requirements": [ "pyebox==1.1.4" ], diff --git a/homeassistant/components/ebusd/manifest.json b/homeassistant/components/ebusd/manifest.json index 46b8fb761dcb7e..b9be062d3e2ad3 100644 --- a/homeassistant/components/ebusd/manifest.json +++ b/homeassistant/components/ebusd/manifest.json @@ -1,7 +1,7 @@ { "domain": "ebusd", "name": "Ebusd", - "documentation": "https://www.home-assistant.io/components/ebusd", + "documentation": "https://www.home-assistant.io/integrations/ebusd", "requirements": [ "ebusdpy==0.0.16" ], diff --git a/homeassistant/components/ecoal_boiler/manifest.json b/homeassistant/components/ecoal_boiler/manifest.json index 5bd488e0ff4bd1..c1bf938dc34044 100644 --- a/homeassistant/components/ecoal_boiler/manifest.json +++ b/homeassistant/components/ecoal_boiler/manifest.json @@ -1,7 +1,7 @@ { "domain": "ecoal_boiler", "name": "Ecoal boiler", - "documentation": "https://www.home-assistant.io/components/ecoal_boiler", + "documentation": "https://www.home-assistant.io/integrations/ecoal_boiler", "requirements": [ "ecoaliface==0.4.0" ], diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 148e355a3d9e9a..bc87b3cd64e654 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -2,7 +2,7 @@ "domain": "ecobee", "name": "Ecobee", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ecobee", + "documentation": "https://www.home-assistant.io/integrations/ecobee", "dependencies": [], "requirements": ["python-ecobee-api==0.1.4"], "codeowners": ["@marthoc"] diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 3ae6b1eac35559..7ce52c021a1ac6 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -1,7 +1,7 @@ { "domain": "econet", "name": "Econet", - "documentation": "https://www.home-assistant.io/components/econet", + "documentation": "https://www.home-assistant.io/integrations/econet", "requirements": [ "pyeconet==0.0.11" ], diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 4495cb3c2f9048..5de2390dd30fcc 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -1,7 +1,7 @@ { "domain": "ecovacs", "name": "Ecovacs", - "documentation": "https://www.home-assistant.io/components/ecovacs", + "documentation": "https://www.home-assistant.io/integrations/ecovacs", "requirements": [ "sucks==0.9.4" ], diff --git a/homeassistant/components/eddystone_temperature/manifest.json b/homeassistant/components/eddystone_temperature/manifest.json index 4684655aa372a8..36918fa5ee5f82 100644 --- a/homeassistant/components/eddystone_temperature/manifest.json +++ b/homeassistant/components/eddystone_temperature/manifest.json @@ -1,7 +1,7 @@ { "domain": "eddystone_temperature", "name": "Eddystone temperature", - "documentation": "https://www.home-assistant.io/components/eddystone_temperature", + "documentation": "https://www.home-assistant.io/integrations/eddystone_temperature", "requirements": [ "beacontools[scan]==1.2.3", "construct==2.9.45" diff --git a/homeassistant/components/edimax/manifest.json b/homeassistant/components/edimax/manifest.json index 9fe0e4c50c9693..6d1d444d2f4754 100644 --- a/homeassistant/components/edimax/manifest.json +++ b/homeassistant/components/edimax/manifest.json @@ -1,7 +1,7 @@ { "domain": "edimax", "name": "Edimax", - "documentation": "https://www.home-assistant.io/components/edimax", + "documentation": "https://www.home-assistant.io/integrations/edimax", "requirements": [ "pyedimax==0.1" ], diff --git a/homeassistant/components/ee_brightbox/manifest.json b/homeassistant/components/ee_brightbox/manifest.json index 967f04228a825e..d4ae0c9d6dfc14 100644 --- a/homeassistant/components/ee_brightbox/manifest.json +++ b/homeassistant/components/ee_brightbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "ee_brightbox", "name": "Ee brightbox", - "documentation": "https://www.home-assistant.io/components/ee_brightbox", + "documentation": "https://www.home-assistant.io/integrations/ee_brightbox", "requirements": [ "eebrightbox==0.0.4" ], diff --git a/homeassistant/components/efergy/manifest.json b/homeassistant/components/efergy/manifest.json index f4ca116a647084..99b966c6c501ac 100644 --- a/homeassistant/components/efergy/manifest.json +++ b/homeassistant/components/efergy/manifest.json @@ -1,7 +1,7 @@ { "domain": "efergy", "name": "Efergy", - "documentation": "https://www.home-assistant.io/components/efergy", + "documentation": "https://www.home-assistant.io/integrations/efergy", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/egardia/manifest.json b/homeassistant/components/egardia/manifest.json index 6f103449868ab1..b0dfc63d929b27 100644 --- a/homeassistant/components/egardia/manifest.json +++ b/homeassistant/components/egardia/manifest.json @@ -1,7 +1,7 @@ { "domain": "egardia", "name": "Egardia", - "documentation": "https://www.home-assistant.io/components/egardia", + "documentation": "https://www.home-assistant.io/integrations/egardia", "requirements": ["pythonegardia==1.0.40"], "dependencies": [], "codeowners": ["@jeroenterheerdt"] diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 2b008c3c370fb7..3353f1fa4d962f 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -1,7 +1,7 @@ { "domain": "eight_sleep", "name": "Eight sleep", - "documentation": "https://www.home-assistant.io/components/eight_sleep", + "documentation": "https://www.home-assistant.io/integrations/eight_sleep", "requirements": [ "pyeight==0.1.1" ], diff --git a/homeassistant/components/eliqonline/manifest.json b/homeassistant/components/eliqonline/manifest.json index 98d94fd009ea3d..0bc242509c3452 100644 --- a/homeassistant/components/eliqonline/manifest.json +++ b/homeassistant/components/eliqonline/manifest.json @@ -1,7 +1,7 @@ { "domain": "eliqonline", "name": "Eliqonline", - "documentation": "https://www.home-assistant.io/components/eliqonline", + "documentation": "https://www.home-assistant.io/integrations/eliqonline", "requirements": [ "eliqonline==1.2.2" ], diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 466f9da7e9012c..75acab5860db1f 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -1,7 +1,7 @@ { "domain": "elkm1", "name": "Elkm1", - "documentation": "https://www.home-assistant.io/components/elkm1", + "documentation": "https://www.home-assistant.io/integrations/elkm1", "requirements": [ "elkm1-lib==0.7.15" ], diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index 04d384416217a8..b4871a805d2d61 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -1,7 +1,7 @@ { "domain": "elv", "name": "ELV PCA", - "documentation": "https://www.home-assistant.io/components/pca", + "documentation": "https://www.home-assistant.io/integrations/pca", "dependencies": [], "codeowners": ["@majuss"], "requirements": ["pypca==0.0.5"] diff --git a/homeassistant/components/emby/manifest.json b/homeassistant/components/emby/manifest.json index 87688733e593ab..12dbb1522061f8 100644 --- a/homeassistant/components/emby/manifest.json +++ b/homeassistant/components/emby/manifest.json @@ -1,7 +1,7 @@ { "domain": "emby", "name": "Emby", - "documentation": "https://www.home-assistant.io/components/emby", + "documentation": "https://www.home-assistant.io/integrations/emby", "requirements": [ "pyemby==1.6" ], diff --git a/homeassistant/components/emoncms/manifest.json b/homeassistant/components/emoncms/manifest.json index 90623c01d1be50..83833d4f79bc5c 100644 --- a/homeassistant/components/emoncms/manifest.json +++ b/homeassistant/components/emoncms/manifest.json @@ -1,7 +1,7 @@ { "domain": "emoncms", "name": "Emoncms", - "documentation": "https://www.home-assistant.io/components/emoncms", + "documentation": "https://www.home-assistant.io/integrations/emoncms", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/emoncms_history/manifest.json b/homeassistant/components/emoncms_history/manifest.json index 0cb09e3fb73b88..80d946f4868dbd 100644 --- a/homeassistant/components/emoncms_history/manifest.json +++ b/homeassistant/components/emoncms_history/manifest.json @@ -1,7 +1,7 @@ { "domain": "emoncms_history", "name": "Emoncms history", - "documentation": "https://www.home-assistant.io/components/emoncms_history", + "documentation": "https://www.home-assistant.io/integrations/emoncms_history", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index 75fcbc4c55500b..9b3b00d20b21e9 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -1,7 +1,7 @@ { "domain": "emulated_hue", "name": "Emulated hue", - "documentation": "https://www.home-assistant.io/components/emulated_hue", + "documentation": "https://www.home-assistant.io/integrations/emulated_hue", "requirements": [ "aiohttp_cors==0.7.0" ], diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index ba68ce94951392..824e5bef7c8da0 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -2,7 +2,7 @@ "domain": "emulated_roku", "name": "Emulated roku", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/emulated_roku", + "documentation": "https://www.home-assistant.io/integrations/emulated_roku", "requirements": [ "emulated_roku==0.1.8" ], diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index d523bd72b720dc..870681fd4a51ed 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -1,7 +1,7 @@ { "domain": "enigma2", "name": "Enigma2", - "documentation": "https://www.home-assistant.io/components/enigma2", + "documentation": "https://www.home-assistant.io/integrations/enigma2", "requirements": [ "openwebifpy==3.1.1" ], diff --git a/homeassistant/components/enocean/manifest.json b/homeassistant/components/enocean/manifest.json index e6f1c5d78262c9..4dffbabd2196a9 100644 --- a/homeassistant/components/enocean/manifest.json +++ b/homeassistant/components/enocean/manifest.json @@ -1,7 +1,7 @@ { "domain": "enocean", "name": "Enocean", - "documentation": "https://www.home-assistant.io/components/enocean", + "documentation": "https://www.home-assistant.io/integrations/enocean", "requirements": [ "enocean==0.50" ], diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 86d2d69cf9b47d..5b5f94f7c8cae2 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -1,7 +1,7 @@ { "domain": "enphase_envoy", "name": "Enphase envoy", - "documentation": "https://www.home-assistant.io/components/enphase_envoy", + "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "requirements": [ "envoy_reader==0.8.6" ], diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index b2b60cff95a486..b0910f165363c2 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "entur_public_transport", "name": "Entur public transport", - "documentation": "https://www.home-assistant.io/components/entur_public_transport", + "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", "requirements": [ "enturclient==0.2.0" ], diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 2ae2006512b03a..c62e1e356b6031 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -1,7 +1,7 @@ { "domain": "environment_canada", "name": "Environment Canada", - "documentation": "https://www.home-assistant.io/components/environment_canada", + "documentation": "https://www.home-assistant.io/integrations/environment_canada", "requirements": [ "env_canada==0.0.25" ], diff --git a/homeassistant/components/envirophat/manifest.json b/homeassistant/components/envirophat/manifest.json index c69a66d43f85eb..ddf69d0d417e41 100644 --- a/homeassistant/components/envirophat/manifest.json +++ b/homeassistant/components/envirophat/manifest.json @@ -1,7 +1,7 @@ { "domain": "envirophat", "name": "Envirophat", - "documentation": "https://www.home-assistant.io/components/envirophat", + "documentation": "https://www.home-assistant.io/integrations/envirophat", "requirements": [ "envirophat==0.0.6", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index b34aa08951ca85..6c5405c75eaae9 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -1,7 +1,7 @@ { "domain": "envisalink", "name": "Envisalink", - "documentation": "https://www.home-assistant.io/components/envisalink", + "documentation": "https://www.home-assistant.io/integrations/envisalink", "requirements": [ "pyenvisalink==3.8" ], diff --git a/homeassistant/components/ephember/manifest.json b/homeassistant/components/ephember/manifest.json index 3fed307aed5f3e..7509e627621396 100644 --- a/homeassistant/components/ephember/manifest.json +++ b/homeassistant/components/ephember/manifest.json @@ -1,7 +1,7 @@ { "domain": "ephember", "name": "Ephember", - "documentation": "https://www.home-assistant.io/components/ephember", + "documentation": "https://www.home-assistant.io/integrations/ephember", "requirements": [ "pyephember==0.2.0" ], diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index e6623b83013ad6..22055e347affe1 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -1,7 +1,7 @@ { "domain": "epson", "name": "Epson", - "documentation": "https://www.home-assistant.io/components/epson", + "documentation": "https://www.home-assistant.io/integrations/epson", "requirements": [ "epson-projector==0.1.3" ], diff --git a/homeassistant/components/epsonworkforce/manifest.json b/homeassistant/components/epsonworkforce/manifest.json index 21f76c3a31f09a..d73b331d5f0d5e 100644 --- a/homeassistant/components/epsonworkforce/manifest.json +++ b/homeassistant/components/epsonworkforce/manifest.json @@ -1,7 +1,7 @@ { "domain": "epsonworkforce", "name": "Epson Workforce", - "documentation": "https://www.home-assistant.io/components/epsonworkforce", + "documentation": "https://www.home-assistant.io/integrations/epsonworkforce", "dependencies": [], "codeowners": ["@ThaStealth"], "requirements": ["epsonprinter==0.0.9"] diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 6d13c79bcec097..1065b94c12ac49 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -1,7 +1,7 @@ { "domain": "eq3btsmart", "name": "Eq3btsmart", - "documentation": "https://www.home-assistant.io/components/eq3btsmart", + "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", "requirements": [ "construct==2.9.45", "python-eq3bt==0.1.9" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 43987cce2c9789..bde64762121ebd 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -2,7 +2,7 @@ "domain": "esphome", "name": "ESPHome", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/esphome", + "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ "aioesphomeapi==2.2.0" ], diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json index aeb3b48311eff4..914c8f1556fa19 100644 --- a/homeassistant/components/essent/manifest.json +++ b/homeassistant/components/essent/manifest.json @@ -1,7 +1,7 @@ { "domain": "essent", "name": "Essent", - "documentation": "https://www.home-assistant.io/components/essent", + "documentation": "https://www.home-assistant.io/integrations/essent", "requirements": ["PyEssent==0.13"], "dependencies": [], "codeowners": ["@TheLastProject"] diff --git a/homeassistant/components/etherscan/manifest.json b/homeassistant/components/etherscan/manifest.json index 452d1c4c475343..f0abf8c7de0da1 100644 --- a/homeassistant/components/etherscan/manifest.json +++ b/homeassistant/components/etherscan/manifest.json @@ -1,7 +1,7 @@ { "domain": "etherscan", "name": "Etherscan", - "documentation": "https://www.home-assistant.io/components/etherscan", + "documentation": "https://www.home-assistant.io/integrations/etherscan", "requirements": [ "python-etherscan-api==0.0.3" ], diff --git a/homeassistant/components/eufy/manifest.json b/homeassistant/components/eufy/manifest.json index ec7f1fe7072231..92a91976500474 100644 --- a/homeassistant/components/eufy/manifest.json +++ b/homeassistant/components/eufy/manifest.json @@ -1,7 +1,7 @@ { "domain": "eufy", "name": "Eufy", - "documentation": "https://www.home-assistant.io/components/eufy", + "documentation": "https://www.home-assistant.io/integrations/eufy", "requirements": [ "lakeside==0.12" ], diff --git a/homeassistant/components/everlights/manifest.json b/homeassistant/components/everlights/manifest.json index 9c2e1b2ae4f667..53e2dbf2cb4eb1 100644 --- a/homeassistant/components/everlights/manifest.json +++ b/homeassistant/components/everlights/manifest.json @@ -1,7 +1,7 @@ { "domain": "everlights", "name": "Everlights", - "documentation": "https://www.home-assistant.io/components/everlights", + "documentation": "https://www.home-assistant.io/integrations/everlights", "requirements": [ "pyeverlights==0.1.0" ], diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 32a57cf20b1c77..5633880be35e58 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -1,7 +1,7 @@ { "domain": "evohome", "name": "Evohome", - "documentation": "https://www.home-assistant.io/components/evohome", + "documentation": "https://www.home-assistant.io/integrations/evohome", "requirements": [ "evohome-async==0.3.3b4" ], diff --git a/homeassistant/components/facebook/manifest.json b/homeassistant/components/facebook/manifest.json index 9632906a25a74e..930047065c58e7 100644 --- a/homeassistant/components/facebook/manifest.json +++ b/homeassistant/components/facebook/manifest.json @@ -1,7 +1,7 @@ { "domain": "facebook", "name": "Facebook", - "documentation": "https://www.home-assistant.io/components/facebook", + "documentation": "https://www.home-assistant.io/integrations/facebook", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/facebox/manifest.json b/homeassistant/components/facebox/manifest.json index 4a3eefc135c563..2c911eb04ef455 100644 --- a/homeassistant/components/facebox/manifest.json +++ b/homeassistant/components/facebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "facebox", "name": "Facebox", - "documentation": "https://www.home-assistant.io/components/facebox", + "documentation": "https://www.home-assistant.io/integrations/facebox", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fail2ban/manifest.json b/homeassistant/components/fail2ban/manifest.json index fc60658a3f2c6a..6ff0c7be0e4e24 100644 --- a/homeassistant/components/fail2ban/manifest.json +++ b/homeassistant/components/fail2ban/manifest.json @@ -1,7 +1,7 @@ { "domain": "fail2ban", "name": "Fail2ban", - "documentation": "https://www.home-assistant.io/components/fail2ban", + "documentation": "https://www.home-assistant.io/integrations/fail2ban", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/familyhub/manifest.json b/homeassistant/components/familyhub/manifest.json index 48a73dfb0300ba..e8aa3ab51b3a27 100644 --- a/homeassistant/components/familyhub/manifest.json +++ b/homeassistant/components/familyhub/manifest.json @@ -1,7 +1,7 @@ { "domain": "familyhub", "name": "Familyhub", - "documentation": "https://www.home-assistant.io/components/familyhub", + "documentation": "https://www.home-assistant.io/integrations/familyhub", "requirements": [ "python-family-hub-local==0.0.2" ], diff --git a/homeassistant/components/fan/manifest.json b/homeassistant/components/fan/manifest.json index 85bb982d2d1f46..0df3b7a850e3cc 100644 --- a/homeassistant/components/fan/manifest.json +++ b/homeassistant/components/fan/manifest.json @@ -1,7 +1,7 @@ { "domain": "fan", "name": "Fan", - "documentation": "https://www.home-assistant.io/components/fan", + "documentation": "https://www.home-assistant.io/integrations/fan", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index f4bf021380c986..3655ce22ba70ee 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -1,7 +1,7 @@ { "domain": "fastdotcom", "name": "Fastdotcom", - "documentation": "https://www.home-assistant.io/components/fastdotcom", + "documentation": "https://www.home-assistant.io/integrations/fastdotcom", "requirements": [ "fastdotcom==0.0.3" ], diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index e458d30073e8a7..6ecc9efffb8406 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -1,7 +1,7 @@ { "domain": "feedreader", "name": "Feedreader", - "documentation": "https://www.home-assistant.io/components/feedreader", + "documentation": "https://www.home-assistant.io/integrations/feedreader", "requirements": [ "feedparser-homeassistant==5.2.2.dev1" ], diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json index 4a3695e7dcc52f..438891eca92c2f 100644 --- a/homeassistant/components/ffmpeg/manifest.json +++ b/homeassistant/components/ffmpeg/manifest.json @@ -1,7 +1,7 @@ { "domain": "ffmpeg", "name": "Ffmpeg", - "documentation": "https://www.home-assistant.io/components/ffmpeg", + "documentation": "https://www.home-assistant.io/integrations/ffmpeg", "requirements": [ "ha-ffmpeg==2.0" ], diff --git a/homeassistant/components/ffmpeg_motion/manifest.json b/homeassistant/components/ffmpeg_motion/manifest.json index e9a0e7b10143f5..5b445dd3094623 100644 --- a/homeassistant/components/ffmpeg_motion/manifest.json +++ b/homeassistant/components/ffmpeg_motion/manifest.json @@ -1,7 +1,7 @@ { "domain": "ffmpeg_motion", "name": "Ffmpeg motion", - "documentation": "https://www.home-assistant.io/components/ffmpeg_motion", + "documentation": "https://www.home-assistant.io/integrations/ffmpeg_motion", "requirements": [], "dependencies": ["ffmpeg"], "codeowners": [] diff --git a/homeassistant/components/ffmpeg_noise/manifest.json b/homeassistant/components/ffmpeg_noise/manifest.json index 71600b311177f5..1bb8e7353dc3b5 100644 --- a/homeassistant/components/ffmpeg_noise/manifest.json +++ b/homeassistant/components/ffmpeg_noise/manifest.json @@ -1,7 +1,7 @@ { "domain": "ffmpeg_noise", "name": "Ffmpeg noise", - "documentation": "https://www.home-assistant.io/components/ffmpeg_noise", + "documentation": "https://www.home-assistant.io/integrations/ffmpeg_noise", "requirements": [], "dependencies": ["ffmpeg"], "codeowners": [] diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 3574e6254ded39..0a5d1316561b1a 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -1,7 +1,7 @@ { "domain": "fibaro", "name": "Fibaro", - "documentation": "https://www.home-assistant.io/components/fibaro", + "documentation": "https://www.home-assistant.io/integrations/fibaro", "requirements": [ "fiblary3==0.1.7" ], diff --git a/homeassistant/components/fido/manifest.json b/homeassistant/components/fido/manifest.json index 343a21ff072fae..638505d5dd9e2d 100644 --- a/homeassistant/components/fido/manifest.json +++ b/homeassistant/components/fido/manifest.json @@ -1,7 +1,7 @@ { "domain": "fido", "name": "Fido", - "documentation": "https://www.home-assistant.io/components/fido", + "documentation": "https://www.home-assistant.io/integrations/fido", "requirements": [ "pyfido==2.1.1" ], diff --git a/homeassistant/components/file/manifest.json b/homeassistant/components/file/manifest.json index 581b0e14156666..07a9cde900fada 100644 --- a/homeassistant/components/file/manifest.json +++ b/homeassistant/components/file/manifest.json @@ -1,7 +1,7 @@ { "domain": "file", "name": "File", - "documentation": "https://www.home-assistant.io/components/file", + "documentation": "https://www.home-assistant.io/integrations/file", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/filesize/manifest.json b/homeassistant/components/filesize/manifest.json index f76bcd27466c64..14e0a6a487c129 100644 --- a/homeassistant/components/filesize/manifest.json +++ b/homeassistant/components/filesize/manifest.json @@ -1,7 +1,7 @@ { "domain": "filesize", "name": "Filesize", - "documentation": "https://www.home-assistant.io/components/filesize", + "documentation": "https://www.home-assistant.io/integrations/filesize", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/filter/manifest.json b/homeassistant/components/filter/manifest.json index 28f061d26f7c5c..f28007ba552882 100644 --- a/homeassistant/components/filter/manifest.json +++ b/homeassistant/components/filter/manifest.json @@ -1,7 +1,7 @@ { "domain": "filter", "name": "Filter", - "documentation": "https://www.home-assistant.io/components/filter", + "documentation": "https://www.home-assistant.io/integrations/filter", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/fints/manifest.json b/homeassistant/components/fints/manifest.json index e3580676290b9a..1d0879c291aac6 100644 --- a/homeassistant/components/fints/manifest.json +++ b/homeassistant/components/fints/manifest.json @@ -1,7 +1,7 @@ { "domain": "fints", "name": "Fints", - "documentation": "https://www.home-assistant.io/components/fints", + "documentation": "https://www.home-assistant.io/integrations/fints", "requirements": [ "fints==1.0.1" ], diff --git a/homeassistant/components/fitbit/manifest.json b/homeassistant/components/fitbit/manifest.json index 6a6316d80a3f6a..f550cb75c5dbe3 100644 --- a/homeassistant/components/fitbit/manifest.json +++ b/homeassistant/components/fitbit/manifest.json @@ -1,7 +1,7 @@ { "domain": "fitbit", "name": "Fitbit", - "documentation": "https://www.home-assistant.io/components/fitbit", + "documentation": "https://www.home-assistant.io/integrations/fitbit", "requirements": [ "fitbit==0.3.1" ], diff --git a/homeassistant/components/fixer/manifest.json b/homeassistant/components/fixer/manifest.json index 1e010bb06ed0c7..e6661ca6ce4e9a 100644 --- a/homeassistant/components/fixer/manifest.json +++ b/homeassistant/components/fixer/manifest.json @@ -1,7 +1,7 @@ { "domain": "fixer", "name": "Fixer", - "documentation": "https://www.home-assistant.io/components/fixer", + "documentation": "https://www.home-assistant.io/integrations/fixer", "requirements": [ "fixerio==1.0.0a0" ], diff --git a/homeassistant/components/fleetgo/manifest.json b/homeassistant/components/fleetgo/manifest.json index c37ece4ebd8860..eece29e167c6e0 100644 --- a/homeassistant/components/fleetgo/manifest.json +++ b/homeassistant/components/fleetgo/manifest.json @@ -1,7 +1,7 @@ { "domain": "fleetgo", "name": "FleetGO", - "documentation": "https://www.home-assistant.io/components/fleetgo", + "documentation": "https://www.home-assistant.io/integrations/fleetgo", "requirements": [ "ritassist==0.9.2" ], diff --git a/homeassistant/components/flexit/manifest.json b/homeassistant/components/flexit/manifest.json index 0ee0e81143cd6c..311904166d5a16 100644 --- a/homeassistant/components/flexit/manifest.json +++ b/homeassistant/components/flexit/manifest.json @@ -1,7 +1,7 @@ { "domain": "flexit", "name": "Flexit", - "documentation": "https://www.home-assistant.io/components/flexit", + "documentation": "https://www.home-assistant.io/integrations/flexit", "requirements": [ "pyflexit==0.3" ], diff --git a/homeassistant/components/flic/manifest.json b/homeassistant/components/flic/manifest.json index 827bcb167c397d..d0651c7fbe9933 100644 --- a/homeassistant/components/flic/manifest.json +++ b/homeassistant/components/flic/manifest.json @@ -1,7 +1,7 @@ { "domain": "flic", "name": "Flic", - "documentation": "https://www.home-assistant.io/components/flic", + "documentation": "https://www.home-assistant.io/integrations/flic", "requirements": [ "pyflic-homeassistant==0.4.dev0" ], diff --git a/homeassistant/components/flock/manifest.json b/homeassistant/components/flock/manifest.json index a5af541eeeef38..ba6f4b1f43f584 100644 --- a/homeassistant/components/flock/manifest.json +++ b/homeassistant/components/flock/manifest.json @@ -1,7 +1,7 @@ { "domain": "flock", "name": "Flock", - "documentation": "https://www.home-assistant.io/components/flock", + "documentation": "https://www.home-assistant.io/integrations/flock", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index 76053f75081735..a5dfaf4027f2ee 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -1,7 +1,7 @@ { "domain": "flunearyou", "name": "Flunearyou", - "documentation": "https://www.home-assistant.io/components/flunearyou", + "documentation": "https://www.home-assistant.io/integrations/flunearyou", "requirements": [ "pyflunearyou==1.0.3" ], diff --git a/homeassistant/components/flux/manifest.json b/homeassistant/components/flux/manifest.json index 9bf3ba09ce7135..e7b0387698ddd6 100644 --- a/homeassistant/components/flux/manifest.json +++ b/homeassistant/components/flux/manifest.json @@ -1,7 +1,7 @@ { "domain": "flux", "name": "Flux", - "documentation": "https://www.home-assistant.io/components/flux", + "documentation": "https://www.home-assistant.io/integrations/flux", "requirements": [], "dependencies": [], "after_dependencies": ["light"], diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 0d00275200caba..4e5531ab4e34f3 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -1,7 +1,7 @@ { "domain": "flux_led", "name": "Flux led", - "documentation": "https://www.home-assistant.io/components/flux_led", + "documentation": "https://www.home-assistant.io/integrations/flux_led", "requirements": [ "flux_led==0.22" ], diff --git a/homeassistant/components/folder/manifest.json b/homeassistant/components/folder/manifest.json index 7a0bf76e0aa310..d4026e7689daf1 100644 --- a/homeassistant/components/folder/manifest.json +++ b/homeassistant/components/folder/manifest.json @@ -1,7 +1,7 @@ { "domain": "folder", "name": "Folder", - "documentation": "https://www.home-assistant.io/components/folder", + "documentation": "https://www.home-assistant.io/integrations/folder", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 1a5b547e5ff21c..17c54a39763952 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -1,7 +1,7 @@ { "domain": "folder_watcher", "name": "Folder watcher", - "documentation": "https://www.home-assistant.io/components/folder_watcher", + "documentation": "https://www.home-assistant.io/integrations/folder_watcher", "requirements": [ "watchdog==0.8.3" ], diff --git a/homeassistant/components/foobot/manifest.json b/homeassistant/components/foobot/manifest.json index 9ed95597e41700..b02149d2bcd441 100644 --- a/homeassistant/components/foobot/manifest.json +++ b/homeassistant/components/foobot/manifest.json @@ -1,7 +1,7 @@ { "domain": "foobot", "name": "Foobot", - "documentation": "https://www.home-assistant.io/components/foobot", + "documentation": "https://www.home-assistant.io/integrations/foobot", "requirements": [ "foobot_async==0.3.1" ], diff --git a/homeassistant/components/fortigate/manifest.json b/homeassistant/components/fortigate/manifest.json index 544ea860778897..ff063d6b7e2d3f 100644 --- a/homeassistant/components/fortigate/manifest.json +++ b/homeassistant/components/fortigate/manifest.json @@ -1,7 +1,7 @@ { "domain": "fortigate", "name": "Fortigate", - "documentation": "https://www.home-assistant.io/components/fortigate", + "documentation": "https://www.home-assistant.io/integrations/fortigate", "dependencies": [], "codeowners": [ "@kifeo" diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json index a63d4b292ada83..4ec5a4fcb2aa6a 100644 --- a/homeassistant/components/fortios/manifest.json +++ b/homeassistant/components/fortios/manifest.json @@ -1,7 +1,7 @@ { "domain": "fortios", "name": "Home Assistant Device Tracker to support FortiOS", - "documentation": "https://www.home-assistant.io/components/fortios/", + "documentation": "https://www.home-assistant.io/integrations/fortios/", "requirements": [ "fortiosapi==0.10.8" ], diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index b05aa956b42a84..b2c44c113ee9ee 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -1,7 +1,7 @@ { "domain": "foscam", "name": "Foscam", - "documentation": "https://www.home-assistant.io/components/foscam", + "documentation": "https://www.home-assistant.io/integrations/foscam", "requirements": [ "libpyfoscam==1.0" ], diff --git a/homeassistant/components/foursquare/manifest.json b/homeassistant/components/foursquare/manifest.json index 84a98ca033625b..c488bf790d507c 100644 --- a/homeassistant/components/foursquare/manifest.json +++ b/homeassistant/components/foursquare/manifest.json @@ -1,7 +1,7 @@ { "domain": "foursquare", "name": "Foursquare", - "documentation": "https://www.home-assistant.io/components/foursquare", + "documentation": "https://www.home-assistant.io/integrations/foursquare", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/free_mobile/manifest.json b/homeassistant/components/free_mobile/manifest.json index b8a40c3fc1d2ae..19d1ce0aff7766 100644 --- a/homeassistant/components/free_mobile/manifest.json +++ b/homeassistant/components/free_mobile/manifest.json @@ -1,7 +1,7 @@ { "domain": "free_mobile", "name": "Free mobile", - "documentation": "https://www.home-assistant.io/components/free_mobile", + "documentation": "https://www.home-assistant.io/integrations/free_mobile", "requirements": [ "freesms==0.1.2" ], diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index 9ee134d41709f3..ac507f59c7f8fc 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "freebox", "name": "Freebox", - "documentation": "https://www.home-assistant.io/components/freebox", + "documentation": "https://www.home-assistant.io/integrations/freebox", "requirements": [ "aiofreepybox==0.0.8" ], diff --git a/homeassistant/components/freedns/manifest.json b/homeassistant/components/freedns/manifest.json index 63f929754db60f..02332c9fb10f87 100644 --- a/homeassistant/components/freedns/manifest.json +++ b/homeassistant/components/freedns/manifest.json @@ -1,7 +1,7 @@ { "domain": "freedns", "name": "Freedns", - "documentation": "https://www.home-assistant.io/components/freedns", + "documentation": "https://www.home-assistant.io/integrations/freedns", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index b2aacbd48ad795..e6c1fee2c958d6 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritz", "name": "Fritz", - "documentation": "https://www.home-assistant.io/components/fritz", + "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ "fritzconnection==0.6.5" ], diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 1ed18140bd2842..a1e28966568881 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzbox", "name": "Fritzbox", - "documentation": "https://www.home-assistant.io/components/fritzbox", + "documentation": "https://www.home-assistant.io/integrations/fritzbox", "requirements": [ "pyfritzhome==0.4.0" ], diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 19f232ed6677c5..35c27b7ca84f1c 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzbox_callmonitor", "name": "Fritzbox callmonitor", - "documentation": "https://www.home-assistant.io/components/fritzbox_callmonitor", + "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", "requirements": [ "fritzconnection==0.6.5" ], diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index ac1ce2893e488d..88a7ab5a338fc4 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzbox_netmonitor", "name": "Fritzbox netmonitor", - "documentation": "https://www.home-assistant.io/components/fritzbox_netmonitor", + "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", "requirements": [ "fritzconnection==0.6.5" ], diff --git a/homeassistant/components/fritzdect/manifest.json b/homeassistant/components/fritzdect/manifest.json index 98d628fe078aaa..e7b59b0713831b 100644 --- a/homeassistant/components/fritzdect/manifest.json +++ b/homeassistant/components/fritzdect/manifest.json @@ -1,7 +1,7 @@ { "domain": "fritzdect", "name": "Fritzdect", - "documentation": "https://www.home-assistant.io/components/fritzdect", + "documentation": "https://www.home-assistant.io/integrations/fritzdect", "requirements": [ "fritzhome==1.0.4" ], diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json index 8f737e2e1ff7f8..c7e919c95e5636 100644 --- a/homeassistant/components/fronius/manifest.json +++ b/homeassistant/components/fronius/manifest.json @@ -1,7 +1,7 @@ { "domain": "fronius", "name": "Fronius", - "documentation": "https://www.home-assistant.io/components/fronius", + "documentation": "https://www.home-assistant.io/integrations/fronius", "requirements": ["pyfronius==0.4.6"], "dependencies": [], "codeowners": ["@nielstron"] diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index e50989a15df3d4..f1d91879f155eb 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -1,7 +1,7 @@ { "domain": "frontend", "name": "Home Assistant Frontend", - "documentation": "https://www.home-assistant.io/components/frontend", + "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": ["home-assistant-frontend==20190919.1"], "dependencies": [ "api", diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 0e20a509d1f226..17e9f973fd6038 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -1,7 +1,7 @@ { "domain": "frontier_silicon", "name": "Frontier silicon", - "documentation": "https://www.home-assistant.io/components/frontier_silicon", + "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", "requirements": [ "afsapi==0.0.4" ], diff --git a/homeassistant/components/futurenow/manifest.json b/homeassistant/components/futurenow/manifest.json index 5191ab611acf2e..c1b0cd2c0ffce4 100644 --- a/homeassistant/components/futurenow/manifest.json +++ b/homeassistant/components/futurenow/manifest.json @@ -1,7 +1,7 @@ { "domain": "futurenow", "name": "Futurenow", - "documentation": "https://www.home-assistant.io/components/futurenow", + "documentation": "https://www.home-assistant.io/integrations/futurenow", "requirements": [ "pyfnip==0.2" ], diff --git a/homeassistant/components/garadget/manifest.json b/homeassistant/components/garadget/manifest.json index d3781f81d046aa..b86f4e26b11fd9 100644 --- a/homeassistant/components/garadget/manifest.json +++ b/homeassistant/components/garadget/manifest.json @@ -1,7 +1,7 @@ { "domain": "garadget", "name": "Garadget", - "documentation": "https://www.home-assistant.io/components/garadget", + "documentation": "https://www.home-assistant.io/integrations/garadget", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/gc100/manifest.json b/homeassistant/components/gc100/manifest.json index 96d792196ce956..5ea7cb2fb41ed4 100644 --- a/homeassistant/components/gc100/manifest.json +++ b/homeassistant/components/gc100/manifest.json @@ -1,7 +1,7 @@ { "domain": "gc100", "name": "Gc100", - "documentation": "https://www.home-assistant.io/components/gc100", + "documentation": "https://www.home-assistant.io/integrations/gc100", "requirements": [ "python-gc100==1.0.3a" ], diff --git a/homeassistant/components/gearbest/manifest.json b/homeassistant/components/gearbest/manifest.json index 39ceca41d08028..c8bb89c71a9f62 100644 --- a/homeassistant/components/gearbest/manifest.json +++ b/homeassistant/components/gearbest/manifest.json @@ -1,7 +1,7 @@ { "domain": "gearbest", "name": "Gearbest", - "documentation": "https://www.home-assistant.io/components/gearbest", + "documentation": "https://www.home-assistant.io/integrations/gearbest", "requirements": [ "gearbest_parser==1.0.7" ], diff --git a/homeassistant/components/geizhals/manifest.json b/homeassistant/components/geizhals/manifest.json index d53bceaa1455c8..0f81ecbf1be2f3 100644 --- a/homeassistant/components/geizhals/manifest.json +++ b/homeassistant/components/geizhals/manifest.json @@ -1,7 +1,7 @@ { "domain": "geizhals", "name": "Geizhals", - "documentation": "https://www.home-assistant.io/components/geizhals", + "documentation": "https://www.home-assistant.io/integrations/geizhals", "requirements": [ "geizhals==0.0.9" ], diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index e4d3622a562539..9d59d5b991933a 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -1,7 +1,7 @@ { "domain": "generic", "name": "Generic", - "documentation": "https://www.home-assistant.io/components/generic", + "documentation": "https://www.home-assistant.io/integrations/generic", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/generic_thermostat/manifest.json b/homeassistant/components/generic_thermostat/manifest.json index 41fb04c84566b0..283ea3c45fa6b4 100644 --- a/homeassistant/components/generic_thermostat/manifest.json +++ b/homeassistant/components/generic_thermostat/manifest.json @@ -1,7 +1,7 @@ { "domain": "generic_thermostat", "name": "Generic thermostat", - "documentation": "https://www.home-assistant.io/components/generic_thermostat", + "documentation": "https://www.home-assistant.io/integrations/generic_thermostat", "requirements": [], "dependencies": [ "sensor", diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index f2110ffb2f0220..feedf3be607b71 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -1,7 +1,7 @@ { "domain": "geniushub", "name": "Genius Hub", - "documentation": "https://www.home-assistant.io/components/geniushub", + "documentation": "https://www.home-assistant.io/integrations/geniushub", "requirements": [ "geniushub-client==0.6.13" ], diff --git a/homeassistant/components/geo_json_events/manifest.json b/homeassistant/components/geo_json_events/manifest.json index 6ee78fec5620bd..c4c892e88f40af 100644 --- a/homeassistant/components/geo_json_events/manifest.json +++ b/homeassistant/components/geo_json_events/manifest.json @@ -1,7 +1,7 @@ { "domain": "geo_json_events", "name": "Geo json events", - "documentation": "https://www.home-assistant.io/components/geo_json_events", + "documentation": "https://www.home-assistant.io/integrations/geo_json_events", "requirements": [ "geojson_client==0.4" ], diff --git a/homeassistant/components/geo_location/manifest.json b/homeassistant/components/geo_location/manifest.json index 83b4241284e89a..74dd7cbbf87ca0 100644 --- a/homeassistant/components/geo_location/manifest.json +++ b/homeassistant/components/geo_location/manifest.json @@ -1,7 +1,7 @@ { "domain": "geo_location", "name": "Geo location", - "documentation": "https://www.home-assistant.io/components/geo_location", + "documentation": "https://www.home-assistant.io/integrations/geo_location", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index bce6758b0fe9ea..8fd19f6b034411 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -1,7 +1,7 @@ { "domain": "geo_rss_events", "name": "Geo rss events", - "documentation": "https://www.home-assistant.io/components/geo_rss_events", + "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": [ "georss_generic_client==0.2" ], diff --git a/homeassistant/components/geofency/manifest.json b/homeassistant/components/geofency/manifest.json index d593aec46a46de..f7939e2b02591e 100644 --- a/homeassistant/components/geofency/manifest.json +++ b/homeassistant/components/geofency/manifest.json @@ -2,7 +2,7 @@ "domain": "geofency", "name": "Geofency", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/geofency", + "documentation": "https://www.home-assistant.io/integrations/geofency", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index 77f3c64752e9f5..f7aa53b0a3ae50 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -2,7 +2,7 @@ "domain": "geonetnz_quakes", "name": "GeoNet NZ Quakes", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/geonetnz_quakes", + "documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes", "requirements": [ "aio_geojson_geonetnz_quakes==0.10" ], diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index a2c2ae04376bd9..0b5e3c0df9f2bc 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -1,7 +1,7 @@ { "domain": "github", "name": "Github", - "documentation": "https://www.home-assistant.io/components/github", + "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ "PyGithub==1.43.5" ], diff --git a/homeassistant/components/gitlab_ci/manifest.json b/homeassistant/components/gitlab_ci/manifest.json index 4ea04de9e02394..e439e8d7eda85f 100644 --- a/homeassistant/components/gitlab_ci/manifest.json +++ b/homeassistant/components/gitlab_ci/manifest.json @@ -1,7 +1,7 @@ { "domain": "gitlab_ci", "name": "Gitlab ci", - "documentation": "https://www.home-assistant.io/components/gitlab_ci", + "documentation": "https://www.home-assistant.io/integrations/gitlab_ci", "requirements": [ "python-gitlab==1.6.0" ], diff --git a/homeassistant/components/gitter/manifest.json b/homeassistant/components/gitter/manifest.json index 6600e46a4ce953..96df8c4e083c6a 100644 --- a/homeassistant/components/gitter/manifest.json +++ b/homeassistant/components/gitter/manifest.json @@ -1,7 +1,7 @@ { "domain": "gitter", "name": "Gitter", - "documentation": "https://www.home-assistant.io/components/gitter", + "documentation": "https://www.home-assistant.io/integrations/gitter", "requirements": [ "gitterpy==0.1.7" ], diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 621bca8c4309ac..775d208c1c48e5 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -1,7 +1,7 @@ { "domain": "glances", "name": "Glances", - "documentation": "https://www.home-assistant.io/components/glances", + "documentation": "https://www.home-assistant.io/integrations/glances", "requirements": [ "glances_api==0.2.0" ], diff --git a/homeassistant/components/gntp/manifest.json b/homeassistant/components/gntp/manifest.json index 7315e3c7c849be..f1c030125ac096 100644 --- a/homeassistant/components/gntp/manifest.json +++ b/homeassistant/components/gntp/manifest.json @@ -1,7 +1,7 @@ { "domain": "gntp", "name": "Gntp", - "documentation": "https://www.home-assistant.io/components/gntp", + "documentation": "https://www.home-assistant.io/integrations/gntp", "requirements": [ "gntp==1.0.3" ], diff --git a/homeassistant/components/goalfeed/manifest.json b/homeassistant/components/goalfeed/manifest.json index 861abe0b462d9c..a4d7cd50686bc3 100644 --- a/homeassistant/components/goalfeed/manifest.json +++ b/homeassistant/components/goalfeed/manifest.json @@ -1,7 +1,7 @@ { "domain": "goalfeed", "name": "Goalfeed", - "documentation": "https://www.home-assistant.io/components/goalfeed", + "documentation": "https://www.home-assistant.io/integrations/goalfeed", "requirements": [ "pysher==1.0.1" ], diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 3f3f2c25d0c784..d8878c8b3516d3 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -1,7 +1,7 @@ { "domain": "gogogate2", "name": "Gogogate2", - "documentation": "https://www.home-assistant.io/components/gogogate2", + "documentation": "https://www.home-assistant.io/integrations/gogogate2", "requirements": [ "pygogogate2==0.1.1" ], diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 4c7e82ecfef424..d72cc992f6d100 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -1,7 +1,7 @@ { "domain": "google", "name": "Google", - "documentation": "https://www.home-assistant.io/components/google", + "documentation": "https://www.home-assistant.io/integrations/google", "requirements": [ "google-api-python-client==1.6.4", "httplib2==0.10.3", diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index ff916930216548..d2e016cb5d16b7 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_assistant", "name": "Google assistant", - "documentation": "https://www.home-assistant.io/components/google_assistant", + "documentation": "https://www.home-assistant.io/integrations/google_assistant", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index c8ac0d2e81e588..bc9c71c5a61bfe 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_cloud", "name": "Google Cloud Platform", - "documentation": "https://www.home-assistant.io/components/google_cloud", + "documentation": "https://www.home-assistant.io/integrations/google_cloud", "requirements": [ "google-cloud-texttospeech==0.4.0" ], diff --git a/homeassistant/components/google_domains/manifest.json b/homeassistant/components/google_domains/manifest.json index 190e5860ee60f6..64076434aa591f 100644 --- a/homeassistant/components/google_domains/manifest.json +++ b/homeassistant/components/google_domains/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_domains", "name": "Google domains", - "documentation": "https://www.home-assistant.io/components/google_domains", + "documentation": "https://www.home-assistant.io/integrations/google_domains", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index ec48e5252a8ab2..30571c33865a27 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_maps", "name": "Google maps", - "documentation": "https://www.home-assistant.io/components/google_maps", + "documentation": "https://www.home-assistant.io/integrations/google_maps", "requirements": [ "locationsharinglib==4.1.0" ], diff --git a/homeassistant/components/google_pubsub/manifest.json b/homeassistant/components/google_pubsub/manifest.json index ff61ad0e05df57..b23a101ca466cf 100644 --- a/homeassistant/components/google_pubsub/manifest.json +++ b/homeassistant/components/google_pubsub/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_pubsub", "name": "Google pubsub", - "documentation": "https://www.home-assistant.io/components/google_pubsub", + "documentation": "https://www.home-assistant.io/integrations/google_pubsub", "requirements": [ "google-cloud-pubsub==0.39.1" ], diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index cb3cd350c04a92..8b9621b4236fef 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_translate", "name": "Google Translate", - "documentation": "https://www.home-assistant.io/components/google_translate", + "documentation": "https://www.home-assistant.io/integrations/google_translate", "requirements": [ "gTTS-token==1.1.3" ], diff --git a/homeassistant/components/google_travel_time/manifest.json b/homeassistant/components/google_travel_time/manifest.json index eaa168332a63d2..f4113f85a311ce 100644 --- a/homeassistant/components/google_travel_time/manifest.json +++ b/homeassistant/components/google_travel_time/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_travel_time", "name": "Google travel time", - "documentation": "https://www.home-assistant.io/components/google_travel_time", + "documentation": "https://www.home-assistant.io/integrations/google_travel_time", "requirements": [ "googlemaps==2.5.1" ], diff --git a/homeassistant/components/google_wifi/manifest.json b/homeassistant/components/google_wifi/manifest.json index 6e840458207e12..77062cbdd26063 100644 --- a/homeassistant/components/google_wifi/manifest.json +++ b/homeassistant/components/google_wifi/manifest.json @@ -1,7 +1,7 @@ { "domain": "google_wifi", "name": "Google wifi", - "documentation": "https://www.home-assistant.io/components/google_wifi", + "documentation": "https://www.home-assistant.io/integrations/google_wifi", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/gpmdp/manifest.json b/homeassistant/components/gpmdp/manifest.json index 98ab8035023d04..a3c2389478ed9d 100644 --- a/homeassistant/components/gpmdp/manifest.json +++ b/homeassistant/components/gpmdp/manifest.json @@ -1,7 +1,7 @@ { "domain": "gpmdp", "name": "Gpmdp", - "documentation": "https://www.home-assistant.io/components/gpmdp", + "documentation": "https://www.home-assistant.io/integrations/gpmdp", "requirements": [ "websocket-client==0.54.0" ], diff --git a/homeassistant/components/gpsd/manifest.json b/homeassistant/components/gpsd/manifest.json index b35d5cb1850c62..7bb828cacf94b2 100644 --- a/homeassistant/components/gpsd/manifest.json +++ b/homeassistant/components/gpsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "gpsd", "name": "Gpsd", - "documentation": "https://www.home-assistant.io/components/gpsd", + "documentation": "https://www.home-assistant.io/integrations/gpsd", "requirements": [ "gps3==0.33.3" ], diff --git a/homeassistant/components/gpslogger/manifest.json b/homeassistant/components/gpslogger/manifest.json index f039e50914b107..cbfd79671eb1c1 100644 --- a/homeassistant/components/gpslogger/manifest.json +++ b/homeassistant/components/gpslogger/manifest.json @@ -2,7 +2,7 @@ "domain": "gpslogger", "name": "Gpslogger", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/gpslogger", + "documentation": "https://www.home-assistant.io/integrations/gpslogger", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/graphite/manifest.json b/homeassistant/components/graphite/manifest.json index a5eefc5af04375..49748128258e42 100644 --- a/homeassistant/components/graphite/manifest.json +++ b/homeassistant/components/graphite/manifest.json @@ -1,7 +1,7 @@ { "domain": "graphite", "name": "Graphite", - "documentation": "https://www.home-assistant.io/components/graphite", + "documentation": "https://www.home-assistant.io/integrations/graphite", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index 7bfb87ede474ed..1e9569e850921f 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "greeneye_monitor", "name": "Greeneye monitor", - "documentation": "https://www.home-assistant.io/components/greeneye_monitor", + "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", "requirements": [ "greeneye_monitor==1.0" ], diff --git a/homeassistant/components/greenwave/manifest.json b/homeassistant/components/greenwave/manifest.json index 1032b5eaf2a2ed..20a49e834b0a65 100644 --- a/homeassistant/components/greenwave/manifest.json +++ b/homeassistant/components/greenwave/manifest.json @@ -1,7 +1,7 @@ { "domain": "greenwave", "name": "Greenwave", - "documentation": "https://www.home-assistant.io/components/greenwave", + "documentation": "https://www.home-assistant.io/integrations/greenwave", "requirements": [ "greenwavereality==0.5.1" ], diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index aa99e20a4dfe46..195227ca242cde 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -1,7 +1,7 @@ { "domain": "group", "name": "Group", - "documentation": "https://www.home-assistant.io/components/group", + "documentation": "https://www.home-assistant.io/integrations/group", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index a6a1d2b8aebb7a..0d4508c26dcece 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -1,7 +1,7 @@ { "domain": "growatt_server", "name": "Growatt Server", - "documentation": "https://www.home-assistant.io/components/growatt_server/", + "documentation": "https://www.home-assistant.io/integrations/growatt_server/", "requirements": [ "growattServer==0.0.1" ], diff --git a/homeassistant/components/gstreamer/manifest.json b/homeassistant/components/gstreamer/manifest.json index 6bfb8abbe0b5b8..66cae733d9c9f8 100644 --- a/homeassistant/components/gstreamer/manifest.json +++ b/homeassistant/components/gstreamer/manifest.json @@ -1,7 +1,7 @@ { "domain": "gstreamer", "name": "Gstreamer", - "documentation": "https://www.home-assistant.io/components/gstreamer", + "documentation": "https://www.home-assistant.io/integrations/gstreamer", "requirements": [ "gstreamer-player==1.1.2" ], diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index 1c7ddbd65ee90d..b25134bb79e237 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -1,7 +1,7 @@ { "domain": "gtfs", "name": "Gtfs", - "documentation": "https://www.home-assistant.io/components/gtfs", + "documentation": "https://www.home-assistant.io/integrations/gtfs", "requirements": [ "pygtfs==0.1.5" ], diff --git a/homeassistant/components/gtt/manifest.json b/homeassistant/components/gtt/manifest.json index 142261fe155717..217b17555549ba 100644 --- a/homeassistant/components/gtt/manifest.json +++ b/homeassistant/components/gtt/manifest.json @@ -1,7 +1,7 @@ { "domain": "gtt", "name": "Gtt", - "documentation": "https://www.home-assistant.io/components/gtt", + "documentation": "https://www.home-assistant.io/integrations/gtt", "requirements": [ "pygtt==1.1.2" ], diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index b8e622823d31d3..a3ac10a1c6d8a8 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -1,7 +1,7 @@ { "domain": "habitica", "name": "Habitica", - "documentation": "https://www.home-assistant.io/components/habitica", + "documentation": "https://www.home-assistant.io/integrations/habitica", "requirements": [ "habitipy==0.2.0" ], diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 4a90e9c977ec9a..2f222b3c16e919 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -2,7 +2,7 @@ "domain": "hangouts", "name": "Hangouts", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/hangouts", + "documentation": "https://www.home-assistant.io/integrations/hangouts", "requirements": [ "hangups==0.4.9" ], diff --git a/homeassistant/components/harman_kardon_avr/manifest.json b/homeassistant/components/harman_kardon_avr/manifest.json index eecbf0edd63e77..6bd64942619725 100644 --- a/homeassistant/components/harman_kardon_avr/manifest.json +++ b/homeassistant/components/harman_kardon_avr/manifest.json @@ -1,7 +1,7 @@ { "domain": "harman_kardon_avr", "name": "Harman kardon avr", - "documentation": "https://www.home-assistant.io/components/harman_kardon_avr", + "documentation": "https://www.home-assistant.io/integrations/harman_kardon_avr", "requirements": [ "hkavr==0.0.5" ], diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index a957db0675fb94..af4427c5db3208 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -1,7 +1,7 @@ { "domain": "harmony", "name": "Harmony", - "documentation": "https://www.home-assistant.io/components/harmony", + "documentation": "https://www.home-assistant.io/integrations/harmony", "requirements": [ "aioharmony==0.1.13" ], diff --git a/homeassistant/components/haveibeenpwned/manifest.json b/homeassistant/components/haveibeenpwned/manifest.json index 40572f82ea80eb..00c7b7a19b8b70 100644 --- a/homeassistant/components/haveibeenpwned/manifest.json +++ b/homeassistant/components/haveibeenpwned/manifest.json @@ -1,7 +1,7 @@ { "domain": "haveibeenpwned", "name": "Haveibeenpwned", - "documentation": "https://www.home-assistant.io/components/haveibeenpwned", + "documentation": "https://www.home-assistant.io/integrations/haveibeenpwned", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/hddtemp/manifest.json b/homeassistant/components/hddtemp/manifest.json index 2d34d3b4e7b645..484886aff2112b 100644 --- a/homeassistant/components/hddtemp/manifest.json +++ b/homeassistant/components/hddtemp/manifest.json @@ -1,7 +1,7 @@ { "domain": "hddtemp", "name": "Hddtemp", - "documentation": "https://www.home-assistant.io/components/hddtemp", + "documentation": "https://www.home-assistant.io/integrations/hddtemp", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index b59d5622821db3..7c877f5c2a7b45 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -1,7 +1,7 @@ { "domain": "hdmi_cec", "name": "Hdmi cec", - "documentation": "https://www.home-assistant.io/components/hdmi_cec", + "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", "requirements": [ "pyCEC==0.4.13" ], diff --git a/homeassistant/components/heatmiser/manifest.json b/homeassistant/components/heatmiser/manifest.json index 0a11aecd079d9b..b3882c63c51f28 100644 --- a/homeassistant/components/heatmiser/manifest.json +++ b/homeassistant/components/heatmiser/manifest.json @@ -1,7 +1,7 @@ { "domain": "heatmiser", "name": "Heatmiser", - "documentation": "https://www.home-assistant.io/components/heatmiser", + "documentation": "https://www.home-assistant.io/integrations/heatmiser", "requirements": [ "heatmiserV3==0.9.1" ], diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index eb9ef258a3cbd2..fb21a43356f657 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -2,7 +2,7 @@ "domain": "heos", "name": "HEOS", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/heos", + "documentation": "https://www.home-assistant.io/integrations/heos", "requirements": [ "pyheos==0.6.0" ], diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index e26e2e1d6ea57c..0f2bde253de5a9 100755 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -1,7 +1,7 @@ { "domain": "here_travel_time", "name": "HERE travel time", - "documentation": "https://www.home-assistant.io/components/here_travel_time", + "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "requirements": [ "herepy==0.6.3.1" ], diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index bee53c89cdf729..78917a5351bcd9 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -1,7 +1,7 @@ { "domain": "hikvision", "name": "Hikvision", - "documentation": "https://www.home-assistant.io/components/hikvision", + "documentation": "https://www.home-assistant.io/integrations/hikvision", "requirements": [ "pyhik==0.2.3" ], diff --git a/homeassistant/components/hikvisioncam/manifest.json b/homeassistant/components/hikvisioncam/manifest.json index f2bb0822d17c19..8dcef17fad6cde 100644 --- a/homeassistant/components/hikvisioncam/manifest.json +++ b/homeassistant/components/hikvisioncam/manifest.json @@ -1,7 +1,7 @@ { "domain": "hikvisioncam", "name": "Hikvisioncam", - "documentation": "https://www.home-assistant.io/components/hikvisioncam", + "documentation": "https://www.home-assistant.io/integrations/hikvisioncam", "requirements": [ "hikvision==0.4" ], diff --git a/homeassistant/components/hipchat/manifest.json b/homeassistant/components/hipchat/manifest.json index d49e05a5416f9c..9d563719a2e3f5 100644 --- a/homeassistant/components/hipchat/manifest.json +++ b/homeassistant/components/hipchat/manifest.json @@ -1,7 +1,7 @@ { "domain": "hipchat", "name": "Hipchat", - "documentation": "https://www.home-assistant.io/components/hipchat", + "documentation": "https://www.home-assistant.io/integrations/hipchat", "requirements": [ "hipnotify==1.0.8" ], diff --git a/homeassistant/components/history/manifest.json b/homeassistant/components/history/manifest.json index e0989958626a17..00789c905c225d 100644 --- a/homeassistant/components/history/manifest.json +++ b/homeassistant/components/history/manifest.json @@ -1,7 +1,7 @@ { "domain": "history", "name": "History", - "documentation": "https://www.home-assistant.io/components/history", + "documentation": "https://www.home-assistant.io/integrations/history", "requirements": [], "dependencies": [ "http", diff --git a/homeassistant/components/history_graph/manifest.json b/homeassistant/components/history_graph/manifest.json index fa0d437a700c96..a4a0eb4d3e9125 100644 --- a/homeassistant/components/history_graph/manifest.json +++ b/homeassistant/components/history_graph/manifest.json @@ -1,7 +1,7 @@ { "domain": "history_graph", "name": "History graph", - "documentation": "https://www.home-assistant.io/components/history_graph", + "documentation": "https://www.home-assistant.io/integrations/history_graph", "requirements": [], "dependencies": [ "history" diff --git a/homeassistant/components/history_stats/manifest.json b/homeassistant/components/history_stats/manifest.json index ea0abd87c28c42..55a3449f4d682e 100644 --- a/homeassistant/components/history_stats/manifest.json +++ b/homeassistant/components/history_stats/manifest.json @@ -1,7 +1,7 @@ { "domain": "history_stats", "name": "History stats", - "documentation": "https://www.home-assistant.io/components/history_stats", + "documentation": "https://www.home-assistant.io/integrations/history_stats", "requirements": [], "dependencies": [ "history" diff --git a/homeassistant/components/hitron_coda/manifest.json b/homeassistant/components/hitron_coda/manifest.json index 9f3c20fcca534b..6b0492881fb7ff 100644 --- a/homeassistant/components/hitron_coda/manifest.json +++ b/homeassistant/components/hitron_coda/manifest.json @@ -1,7 +1,7 @@ { "domain": "hitron_coda", "name": "Hitron coda", - "documentation": "https://www.home-assistant.io/components/hitron_coda", + "documentation": "https://www.home-assistant.io/integrations/hitron_coda", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index d9fae3fe54b936..4164283f9f88bd 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -1,7 +1,7 @@ { "domain": "hive", "name": "Hive", - "documentation": "https://www.home-assistant.io/components/hive", + "documentation": "https://www.home-assistant.io/integrations/hive", "requirements": [ "pyhiveapi==0.2.19.2" ], diff --git a/homeassistant/components/hlk_sw16/manifest.json b/homeassistant/components/hlk_sw16/manifest.json index 5266b81ab0383d..037df20b35df37 100644 --- a/homeassistant/components/hlk_sw16/manifest.json +++ b/homeassistant/components/hlk_sw16/manifest.json @@ -1,7 +1,7 @@ { "domain": "hlk_sw16", "name": "Hlk sw16", - "documentation": "https://www.home-assistant.io/components/hlk_sw16", + "documentation": "https://www.home-assistant.io/integrations/hlk_sw16", "requirements": [ "hlk-sw16==0.0.7" ], diff --git a/homeassistant/components/homeassistant/manifest.json b/homeassistant/components/homeassistant/manifest.json index b612c3a9fa6454..b4c03047a7a776 100644 --- a/homeassistant/components/homeassistant/manifest.json +++ b/homeassistant/components/homeassistant/manifest.json @@ -1,7 +1,7 @@ { "domain": "homeassistant", "name": "Home Assistant Core Integration", - "documentation": "https://www.home-assistant.io/components/homeassistant", + "documentation": "https://www.home-assistant.io/integrations/homeassistant", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index ebb0895bd7a100..c0ab61d8568642 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -1,7 +1,7 @@ { "domain": "homekit", "name": "Homekit", - "documentation": "https://www.home-assistant.io/components/homekit", + "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ "HAP-python==2.6.0" ], diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 70f6f6a3ce40cf..2a6602813119ab 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -2,7 +2,7 @@ "domain": "homekit_controller", "name": "Homekit controller", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/homekit_controller", + "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": [ "homekit[IP]==0.15.0" ], diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 3c350e75730075..260e54e65c4b60 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -1,7 +1,7 @@ { "domain": "homematic", "name": "Homematic", - "documentation": "https://www.home-assistant.io/components/homematic", + "documentation": "https://www.home-assistant.io/integrations/homematic", "requirements": [ "pyhomematic==0.1.60" ], diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 2075f88ded25ab..40c8c7c359893a 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "homematicip_cloud", "name": "Homematicip cloud", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/homematicip_cloud", + "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "requirements": [ "homematicip==0.10.12" ], diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index cdbbffb8d3686f..f2929fb655ebce 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -1,7 +1,7 @@ { "domain": "homeworks", "name": "Homeworks", - "documentation": "https://www.home-assistant.io/components/homeworks", + "documentation": "https://www.home-assistant.io/integrations/homeworks", "requirements": [ "pyhomeworks==0.0.6" ], diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index b50c7f61dd50e3..9d644de4445597 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -1,7 +1,7 @@ { "domain": "honeywell", "name": "Honeywell", - "documentation": "https://www.home-assistant.io/components/honeywell", + "documentation": "https://www.home-assistant.io/integrations/honeywell", "requirements": [ "somecomfort==0.5.2" ], diff --git a/homeassistant/components/hook/manifest.json b/homeassistant/components/hook/manifest.json index d9898a71f8b717..035354c969a82d 100644 --- a/homeassistant/components/hook/manifest.json +++ b/homeassistant/components/hook/manifest.json @@ -1,7 +1,7 @@ { "domain": "hook", "name": "Hook", - "documentation": "https://www.home-assistant.io/components/hook", + "documentation": "https://www.home-assistant.io/integrations/hook", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/horizon/manifest.json b/homeassistant/components/horizon/manifest.json index 2916e81ce4f4e2..4ba3a61d8b7ec0 100644 --- a/homeassistant/components/horizon/manifest.json +++ b/homeassistant/components/horizon/manifest.json @@ -1,7 +1,7 @@ { "domain": "horizon", "name": "Horizon", - "documentation": "https://www.home-assistant.io/components/horizon", + "documentation": "https://www.home-assistant.io/integrations/horizon", "requirements": [ "horimote==0.4.1" ], diff --git a/homeassistant/components/hp_ilo/manifest.json b/homeassistant/components/hp_ilo/manifest.json index a3d5853541f58b..3dc591cac4cafc 100644 --- a/homeassistant/components/hp_ilo/manifest.json +++ b/homeassistant/components/hp_ilo/manifest.json @@ -1,7 +1,7 @@ { "domain": "hp_ilo", "name": "Hp ilo", - "documentation": "https://www.home-assistant.io/components/hp_ilo", + "documentation": "https://www.home-assistant.io/integrations/hp_ilo", "requirements": [ "python-hpilo==4.3" ], diff --git a/homeassistant/components/html5/manifest.json b/homeassistant/components/html5/manifest.json index 7b43ec44ef3868..667a57891821a7 100644 --- a/homeassistant/components/html5/manifest.json +++ b/homeassistant/components/html5/manifest.json @@ -1,7 +1,7 @@ { "domain": "html5", "name": "HTML5 Notifications", - "documentation": "https://www.home-assistant.io/components/html5", + "documentation": "https://www.home-assistant.io/integrations/html5", "requirements": [ "pywebpush==1.9.2" ], diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index 0bc5586445dd60..6db8b041cfd75d 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -1,7 +1,7 @@ { "domain": "http", "name": "HTTP", - "documentation": "https://www.home-assistant.io/components/http", + "documentation": "https://www.home-assistant.io/integrations/http", "requirements": [ "aiohttp_cors==0.7.0" ], diff --git a/homeassistant/components/htu21d/manifest.json b/homeassistant/components/htu21d/manifest.json index 70093df9b55fd2..14b0d7b3f15e9f 100644 --- a/homeassistant/components/htu21d/manifest.json +++ b/homeassistant/components/htu21d/manifest.json @@ -1,7 +1,7 @@ { "domain": "htu21d", "name": "Htu21d", - "documentation": "https://www.home-assistant.io/components/htu21d", + "documentation": "https://www.home-assistant.io/integrations/htu21d", "requirements": [ "i2csense==0.0.4", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 3af23be4f0b5f2..5d559cc60c5c3e 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -1,7 +1,7 @@ { "domain": "huawei_lte", "name": "Huawei LTE", - "documentation": "https://www.home-assistant.io/components/huawei_lte", + "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.1", "huawei-lte-api==1.3.0" diff --git a/homeassistant/components/huawei_router/manifest.json b/homeassistant/components/huawei_router/manifest.json index 54fd155b557bd1..2affcb8ee2e953 100644 --- a/homeassistant/components/huawei_router/manifest.json +++ b/homeassistant/components/huawei_router/manifest.json @@ -1,7 +1,7 @@ { "domain": "huawei_router", "name": "Huawei router", - "documentation": "https://www.home-assistant.io/components/huawei_router", + "documentation": "https://www.home-assistant.io/integrations/huawei_router", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index cb37dd3036f245..9a3e478d108f67 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -2,7 +2,7 @@ "domain": "hue", "name": "Philips Hue", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/hue", + "documentation": "https://www.home-assistant.io/integrations/hue", "requirements": [ "aiohue==1.9.2" ], diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index c4e1bcc28e8534..3aeffd025ee4bf 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -1,7 +1,7 @@ { "domain": "hunterdouglas_powerview", "name": "Hunterdouglas powerview", - "documentation": "https://www.home-assistant.io/components/hunterdouglas_powerview", + "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", "requirements": [ "aiopvapi==1.6.14" ], diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 6d332a28bcc45d..eaa0d10622a331 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -1,7 +1,7 @@ { "domain": "hydrawise", "name": "Hydrawise", - "documentation": "https://www.home-assistant.io/components/hydrawise", + "documentation": "https://www.home-assistant.io/integrations/hydrawise", "requirements": [ "hydrawiser==0.1.1" ], diff --git a/homeassistant/components/hydroquebec/manifest.json b/homeassistant/components/hydroquebec/manifest.json index efea5ce0f2e0c2..dbe8af0b41b125 100644 --- a/homeassistant/components/hydroquebec/manifest.json +++ b/homeassistant/components/hydroquebec/manifest.json @@ -1,7 +1,7 @@ { "domain": "hydroquebec", "name": "Hydroquebec", - "documentation": "https://www.home-assistant.io/components/hydroquebec", + "documentation": "https://www.home-assistant.io/integrations/hydroquebec", "requirements": [ "pyhydroquebec==2.2.2" ], diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 980c227944a640..e4ac9c0897b912 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -1,7 +1,7 @@ { "domain": "hyperion", "name": "Hyperion", - "documentation": "https://www.home-assistant.io/components/hyperion", + "documentation": "https://www.home-assistant.io/integrations/hyperion", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index df492d136fd016..6e575ef17bcca9 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -1,7 +1,7 @@ { "domain": "ialarm", "name": "Ialarm", - "documentation": "https://www.home-assistant.io/components/ialarm", + "documentation": "https://www.home-assistant.io/integrations/ialarm", "requirements": [ "pyialarm==0.3" ], diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index 25e02536897915..e883aec371c06b 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -2,7 +2,7 @@ "domain": "iaqualink", "name": "Jandy iAqualink", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/iaqualink/", + "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "dependencies": [], "codeowners": [ "@flz" diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 5f2075a0fd6316..d3924ee61a8b21 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "icloud", "name": "Icloud", - "documentation": "https://www.home-assistant.io/components/icloud", + "documentation": "https://www.home-assistant.io/integrations/icloud", "requirements": [ "pyicloud==0.9.1" ], diff --git a/homeassistant/components/idteck_prox/manifest.json b/homeassistant/components/idteck_prox/manifest.json index 8df144a0f8150b..7a8e3955686c1c 100644 --- a/homeassistant/components/idteck_prox/manifest.json +++ b/homeassistant/components/idteck_prox/manifest.json @@ -1,7 +1,7 @@ { "domain": "idteck_prox", "name": "Idteck prox", - "documentation": "https://www.home-assistant.io/components/idteck_prox", + "documentation": "https://www.home-assistant.io/integrations/idteck_prox", "requirements": [ "rfk101py==0.0.1" ], diff --git a/homeassistant/components/ifttt/manifest.json b/homeassistant/components/ifttt/manifest.json index 58490569e6537e..975a3128f36201 100644 --- a/homeassistant/components/ifttt/manifest.json +++ b/homeassistant/components/ifttt/manifest.json @@ -2,7 +2,7 @@ "domain": "ifttt", "name": "Ifttt", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ifttt", + "documentation": "https://www.home-assistant.io/integrations/ifttt", "requirements": [ "pyfttt==0.3" ], diff --git a/homeassistant/components/iglo/manifest.json b/homeassistant/components/iglo/manifest.json index 4d84c27cd93f82..8771ada45e1cbc 100644 --- a/homeassistant/components/iglo/manifest.json +++ b/homeassistant/components/iglo/manifest.json @@ -1,7 +1,7 @@ { "domain": "iglo", "name": "Iglo", - "documentation": "https://www.home-assistant.io/components/iglo", + "documentation": "https://www.home-assistant.io/integrations/iglo", "requirements": [ "iglo==1.2.7" ], diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index d2ab3ad449cd11..edb77f1dc6d570 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -1,7 +1,7 @@ { "domain": "ign_sismologia", "name": "IGN Sismologia", - "documentation": "https://www.home-assistant.io/components/ign_sismologia", + "documentation": "https://www.home-assistant.io/integrations/ign_sismologia", "requirements": [ "georss_ign_sismologia_client==0.2" ], diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index 25d0317078f6fb..a415b0e3103a90 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -1,7 +1,7 @@ { "domain": "ihc", "name": "Ihc", - "documentation": "https://www.home-assistant.io/components/ihc", + "documentation": "https://www.home-assistant.io/integrations/ihc", "requirements": [ "defusedxml==0.6.0", "ihcsdk==2.3.0" diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index f3a7121c0b4ff3..4a96e9828cb453 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -1,7 +1,7 @@ { "domain": "image_processing", "name": "Image processing", - "documentation": "https://www.home-assistant.io/components/image_processing", + "documentation": "https://www.home-assistant.io/integrations/image_processing", "requirements": [ "pillow==6.1.0" ], diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 9e0f387a7a6ec6..20767a0d49df90 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -1,7 +1,7 @@ { "domain": "imap", "name": "Imap", - "documentation": "https://www.home-assistant.io/components/imap", + "documentation": "https://www.home-assistant.io/integrations/imap", "requirements": [ "aioimaplib==0.7.15" ], diff --git a/homeassistant/components/imap_email_content/manifest.json b/homeassistant/components/imap_email_content/manifest.json index a1e2c616832f2e..e689cb859da552 100644 --- a/homeassistant/components/imap_email_content/manifest.json +++ b/homeassistant/components/imap_email_content/manifest.json @@ -1,7 +1,7 @@ { "domain": "imap_email_content", "name": "Imap email content", - "documentation": "https://www.home-assistant.io/components/imap_email_content", + "documentation": "https://www.home-assistant.io/integrations/imap_email_content", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index c26ba27a29ae42..4bdf43f89578d2 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -1,7 +1,7 @@ { "domain": "incomfort", "name": "Intergas InComfort/Intouch Lan2RF gateway", - "documentation": "https://www.home-assistant.io/components/incomfort", + "documentation": "https://www.home-assistant.io/integrations/incomfort", "requirements": [ "incomfort-client==0.3.5" ], diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index feda5da732cab7..df936eafa903e0 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -1,7 +1,7 @@ { "domain": "influxdb", "name": "Influxdb", - "documentation": "https://www.home-assistant.io/components/influxdb", + "documentation": "https://www.home-assistant.io/integrations/influxdb", "requirements": [ "influxdb==5.2.3" ], diff --git a/homeassistant/components/input_boolean/manifest.json b/homeassistant/components/input_boolean/manifest.json index e233b5635fc77e..09ae235e6b878a 100644 --- a/homeassistant/components/input_boolean/manifest.json +++ b/homeassistant/components/input_boolean/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_boolean", "name": "Input boolean", - "documentation": "https://www.home-assistant.io/components/input_boolean", + "documentation": "https://www.home-assistant.io/integrations/input_boolean", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_datetime/manifest.json b/homeassistant/components/input_datetime/manifest.json index 287777e2ccf5a5..9808c45aa74b3c 100644 --- a/homeassistant/components/input_datetime/manifest.json +++ b/homeassistant/components/input_datetime/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_datetime", "name": "Input datetime", - "documentation": "https://www.home-assistant.io/components/input_datetime", + "documentation": "https://www.home-assistant.io/integrations/input_datetime", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_number/manifest.json b/homeassistant/components/input_number/manifest.json index 2015b8ea734f3d..31e00d0fce8d93 100644 --- a/homeassistant/components/input_number/manifest.json +++ b/homeassistant/components/input_number/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_number", "name": "Input number", - "documentation": "https://www.home-assistant.io/components/input_number", + "documentation": "https://www.home-assistant.io/integrations/input_number", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_select/manifest.json b/homeassistant/components/input_select/manifest.json index a71fb53a5d1b45..d71674fd40cce0 100644 --- a/homeassistant/components/input_select/manifest.json +++ b/homeassistant/components/input_select/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_select", "name": "Input select", - "documentation": "https://www.home-assistant.io/components/input_select", + "documentation": "https://www.home-assistant.io/integrations/input_select", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/input_text/manifest.json b/homeassistant/components/input_text/manifest.json index 6362e6793192f3..eaddaf49b84a24 100644 --- a/homeassistant/components/input_text/manifest.json +++ b/homeassistant/components/input_text/manifest.json @@ -1,7 +1,7 @@ { "domain": "input_text", "name": "Input text", - "documentation": "https://www.home-assistant.io/components/input_text", + "documentation": "https://www.home-assistant.io/integrations/input_text", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 5fcc2c5b507ef3..c8821f3b176571 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -1,7 +1,7 @@ { "domain": "insteon", "name": "Insteon", - "documentation": "https://www.home-assistant.io/components/insteon", + "documentation": "https://www.home-assistant.io/integrations/insteon", "requirements": [ "insteonplm==0.16.5" ], diff --git a/homeassistant/components/integration/manifest.json b/homeassistant/components/integration/manifest.json index 869ad2766f90bc..d910e7bdc78735 100644 --- a/homeassistant/components/integration/manifest.json +++ b/homeassistant/components/integration/manifest.json @@ -1,7 +1,7 @@ { "domain": "integration", "name": "Integration", - "documentation": "https://www.home-assistant.io/components/integration", + "documentation": "https://www.home-assistant.io/integrations/integration", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/intent_script/manifest.json b/homeassistant/components/intent_script/manifest.json index 891be6b21802ed..f2d9a338560bc9 100644 --- a/homeassistant/components/intent_script/manifest.json +++ b/homeassistant/components/intent_script/manifest.json @@ -1,7 +1,7 @@ { "domain": "intent_script", "name": "Intent script", - "documentation": "https://www.home-assistant.io/components/intent_script", + "documentation": "https://www.home-assistant.io/integrations/intent_script", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ios/manifest.json b/homeassistant/components/ios/manifest.json index 28c9ea1e952b30..6e011a43ded05d 100644 --- a/homeassistant/components/ios/manifest.json +++ b/homeassistant/components/ios/manifest.json @@ -2,7 +2,7 @@ "domain": "ios", "name": "Ios", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ios", + "documentation": "https://www.home-assistant.io/integrations/ios", "requirements": [], "dependencies": [ "device_tracker", diff --git a/homeassistant/components/iota/manifest.json b/homeassistant/components/iota/manifest.json index d83defbbec3332..018ea4ac167ecc 100644 --- a/homeassistant/components/iota/manifest.json +++ b/homeassistant/components/iota/manifest.json @@ -1,7 +1,7 @@ { "domain": "iota", "name": "Iota", - "documentation": "https://www.home-assistant.io/components/iota", + "documentation": "https://www.home-assistant.io/integrations/iota", "requirements": [ "pyota==2.0.5" ], diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index 0547628b4bfd2c..c3b1e27c77acc9 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -1,7 +1,7 @@ { "domain": "iperf3", "name": "Iperf3", - "documentation": "https://www.home-assistant.io/components/iperf3", + "documentation": "https://www.home-assistant.io/integrations/iperf3", "requirements": [ "iperf3==0.1.11" ], diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 093ccbf6a5b519..47759759a56600 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -2,7 +2,7 @@ "domain": "ipma", "name": "Ipma", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ipma", + "documentation": "https://www.home-assistant.io/integrations/ipma", "requirements": [ "pyipma==1.2.1" ], diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 7392c931f48339..caf422938b2599 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -2,7 +2,7 @@ "domain": "iqvia", "name": "IQVIA", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/iqvia", + "documentation": "https://www.home-assistant.io/integrations/iqvia", "requirements": [ "numpy==1.17.1", "pyiqvia==0.2.1" diff --git a/homeassistant/components/irish_rail_transport/manifest.json b/homeassistant/components/irish_rail_transport/manifest.json index 5961400e68ec8c..da15d87ab02556 100644 --- a/homeassistant/components/irish_rail_transport/manifest.json +++ b/homeassistant/components/irish_rail_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "irish_rail_transport", "name": "Irish rail transport", - "documentation": "https://www.home-assistant.io/components/irish_rail_transport", + "documentation": "https://www.home-assistant.io/integrations/irish_rail_transport", "requirements": [ "pyirishrail==0.0.2" ], diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index 4dc9e2cb7c3f53..035b61d0f2d1b9 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -1,7 +1,7 @@ { "domain": "islamic_prayer_times", "name": "Islamic prayer times", - "documentation": "https://www.home-assistant.io/components/islamic_prayer_times", + "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", "requirements": [ "prayer_times_calculator==0.0.3" ], diff --git a/homeassistant/components/iss/manifest.json b/homeassistant/components/iss/manifest.json index dc71e81ac0808d..72521e67af916f 100644 --- a/homeassistant/components/iss/manifest.json +++ b/homeassistant/components/iss/manifest.json @@ -1,7 +1,7 @@ { "domain": "iss", "name": "Iss", - "documentation": "https://www.home-assistant.io/components/iss", + "documentation": "https://www.home-assistant.io/integrations/iss", "requirements": [ "pyiss==1.0.1" ], diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 0dd0f1eae80a0b..759e1b78e8ed70 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -1,7 +1,7 @@ { "domain": "isy994", "name": "Isy994", - "documentation": "https://www.home-assistant.io/components/isy994", + "documentation": "https://www.home-assistant.io/integrations/isy994", "requirements": [ "PyISY==1.1.2" ], diff --git a/homeassistant/components/itach/manifest.json b/homeassistant/components/itach/manifest.json index c26b19c636e59c..86bb362f8dc4b7 100644 --- a/homeassistant/components/itach/manifest.json +++ b/homeassistant/components/itach/manifest.json @@ -1,7 +1,7 @@ { "domain": "itach", "name": "Itach", - "documentation": "https://www.home-assistant.io/components/itach", + "documentation": "https://www.home-assistant.io/integrations/itach", "requirements": [ "pyitachip2ir==0.0.7" ], diff --git a/homeassistant/components/itunes/manifest.json b/homeassistant/components/itunes/manifest.json index 6f05125661e2c8..ec47deabc23ec5 100644 --- a/homeassistant/components/itunes/manifest.json +++ b/homeassistant/components/itunes/manifest.json @@ -1,7 +1,7 @@ { "domain": "itunes", "name": "Itunes", - "documentation": "https://www.home-assistant.io/components/itunes", + "documentation": "https://www.home-assistant.io/integrations/itunes", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index 2f6747ab4cc51d..a3aa3ad3fa8987 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -1,7 +1,7 @@ { "domain": "izone", "name": "izone", - "documentation": "https://www.home-assistant.io/components/izone", + "documentation": "https://www.home-assistant.io/integrations/izone", "requirements": [ "python-izone==1.1.1" ], "dependencies": [], "codeowners": [ "@Swamp-Ig" ], diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index fdc1d2943e6fe9..7b6653ba832141 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -1,7 +1,7 @@ { "domain": "jewish_calendar", "name": "Jewish calendar", - "documentation": "https://www.home-assistant.io/components/jewish_calendar", + "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "requirements": [ "hdate==0.9.0" ], diff --git a/homeassistant/components/joaoapps_join/manifest.json b/homeassistant/components/joaoapps_join/manifest.json index 220f2af2035552..a2c2e4b11b6378 100644 --- a/homeassistant/components/joaoapps_join/manifest.json +++ b/homeassistant/components/joaoapps_join/manifest.json @@ -1,7 +1,7 @@ { "domain": "joaoapps_join", "name": "Joaoapps join", - "documentation": "https://www.home-assistant.io/components/joaoapps_join", + "documentation": "https://www.home-assistant.io/integrations/joaoapps_join", "requirements": [ "python-join-api==0.0.4" ], diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index e65aab2b69da28..1ef84b74502a9f 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -1,7 +1,7 @@ { "domain": "juicenet", "name": "Juicenet", - "documentation": "https://www.home-assistant.io/components/juicenet", + "documentation": "https://www.home-assistant.io/integrations/juicenet", "requirements": [ "python-juicenet==0.0.5" ], diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json index 926f73fa4dbea9..eb3626a315be6e 100644 --- a/homeassistant/components/kaiterra/manifest.json +++ b/homeassistant/components/kaiterra/manifest.json @@ -1,7 +1,7 @@ { "domain": "kaiterra", "name": "Kaiterra", - "documentation": "https://www.home-assistant.io/components/kaiterra", + "documentation": "https://www.home-assistant.io/integrations/kaiterra", "requirements": ["kaiterra-async-client==0.0.2"], "codeowners": ["@Michsior14"], "dependencies": [] diff --git a/homeassistant/components/kankun/manifest.json b/homeassistant/components/kankun/manifest.json index 8e4e9747901e68..ef6bcbf92e2786 100644 --- a/homeassistant/components/kankun/manifest.json +++ b/homeassistant/components/kankun/manifest.json @@ -1,7 +1,7 @@ { "domain": "kankun", "name": "Kankun", - "documentation": "https://www.home-assistant.io/components/kankun", + "documentation": "https://www.home-assistant.io/integrations/kankun", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json index 9e959f35c9f32a..422a79cd0bedd6 100644 --- a/homeassistant/components/keba/manifest.json +++ b/homeassistant/components/keba/manifest.json @@ -1,7 +1,7 @@ { "domain": "keba", "name": "Keba Charging Station", - "documentation": "https://www.home-assistant.io/components/keba", + "documentation": "https://www.home-assistant.io/integrations/keba", "requirements": ["keba-kecontact==0.2.0"], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 417616161e56ea..41e45a9e5782f4 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -1,7 +1,7 @@ { "domain": "keenetic_ndms2", "name": "Keenetic ndms2", - "documentation": "https://www.home-assistant.io/components/keenetic_ndms2", + "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": [ "ndms2_client==0.0.9" ], diff --git a/homeassistant/components/keyboard/manifest.json b/homeassistant/components/keyboard/manifest.json index 0e8ade339c2104..a3cf7a51ed7779 100644 --- a/homeassistant/components/keyboard/manifest.json +++ b/homeassistant/components/keyboard/manifest.json @@ -1,7 +1,7 @@ { "domain": "keyboard", "name": "Keyboard", - "documentation": "https://www.home-assistant.io/components/keyboard", + "documentation": "https://www.home-assistant.io/integrations/keyboard", "requirements": [ "pyuserinput==0.1.11" ], diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index d87d1abca4831d..6172de132bb8e1 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -1,7 +1,7 @@ { "domain": "keyboard_remote", "name": "Keyboard remote", - "documentation": "https://www.home-assistant.io/components/keyboard_remote", + "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "requirements": [ "evdev==0.6.1" ], diff --git a/homeassistant/components/kira/manifest.json b/homeassistant/components/kira/manifest.json index b7edd1f6c5f05d..78542bb7b66f86 100644 --- a/homeassistant/components/kira/manifest.json +++ b/homeassistant/components/kira/manifest.json @@ -1,7 +1,7 @@ { "domain": "kira", "name": "Kira", - "documentation": "https://www.home-assistant.io/components/kira", + "documentation": "https://www.home-assistant.io/integrations/kira", "requirements": [ "pykira==0.1.1" ], diff --git a/homeassistant/components/kiwi/manifest.json b/homeassistant/components/kiwi/manifest.json index 9f1595ebd77245..888c6533013f93 100644 --- a/homeassistant/components/kiwi/manifest.json +++ b/homeassistant/components/kiwi/manifest.json @@ -1,7 +1,7 @@ { "domain": "kiwi", "name": "Kiwi", - "documentation": "https://www.home-assistant.io/components/kiwi", + "documentation": "https://www.home-assistant.io/integrations/kiwi", "requirements": [ "kiwiki-client==0.1.1" ], diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 3b2d1414034f4f..76f15f3bdb8efd 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -1,7 +1,7 @@ { "domain": "knx", "name": "Knx", - "documentation": "https://www.home-assistant.io/components/knx", + "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ "xknx==0.11.1" ], diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 8c684d495e91db..ef138bb2ee26ee 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -1,7 +1,7 @@ { "domain": "kodi", "name": "Kodi", - "documentation": "https://www.home-assistant.io/components/kodi", + "documentation": "https://www.home-assistant.io/integrations/kodi", "requirements": [ "jsonrpc-async==0.6", "jsonrpc-websocket==0.6" diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json index e4129af39bd10a..397373499aef67 100644 --- a/homeassistant/components/konnected/manifest.json +++ b/homeassistant/components/konnected/manifest.json @@ -1,7 +1,7 @@ { "domain": "konnected", "name": "Konnected", - "documentation": "https://www.home-assistant.io/components/konnected", + "documentation": "https://www.home-assistant.io/integrations/konnected", "requirements": [ "konnected==0.1.5" ], diff --git a/homeassistant/components/kwb/manifest.json b/homeassistant/components/kwb/manifest.json index 783907c02202e4..f79b0f5b35254d 100644 --- a/homeassistant/components/kwb/manifest.json +++ b/homeassistant/components/kwb/manifest.json @@ -1,7 +1,7 @@ { "domain": "kwb", "name": "Kwb", - "documentation": "https://www.home-assistant.io/components/kwb", + "documentation": "https://www.home-assistant.io/integrations/kwb", "requirements": [ "pykwb==0.0.8" ], diff --git a/homeassistant/components/lacrosse/manifest.json b/homeassistant/components/lacrosse/manifest.json index 99dd4889213361..c30a147342b36c 100644 --- a/homeassistant/components/lacrosse/manifest.json +++ b/homeassistant/components/lacrosse/manifest.json @@ -1,7 +1,7 @@ { "domain": "lacrosse", "name": "Lacrosse", - "documentation": "https://www.home-assistant.io/components/lacrosse", + "documentation": "https://www.home-assistant.io/integrations/lacrosse", "requirements": [ "pylacrosse==0.4.0" ], diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index bbf22918a75545..72e12e78ba4b92 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -1,7 +1,7 @@ { "domain": "lametric", "name": "Lametric", - "documentation": "https://www.home-assistant.io/components/lametric", + "documentation": "https://www.home-assistant.io/integrations/lametric", "requirements": [ "lmnotify==0.0.4" ], diff --git a/homeassistant/components/lannouncer/manifest.json b/homeassistant/components/lannouncer/manifest.json index 951dd3ff85b5e2..47bdd1ee0aef21 100644 --- a/homeassistant/components/lannouncer/manifest.json +++ b/homeassistant/components/lannouncer/manifest.json @@ -1,7 +1,7 @@ { "domain": "lannouncer", "name": "Lannouncer", - "documentation": "https://www.home-assistant.io/components/lannouncer", + "documentation": "https://www.home-assistant.io/integrations/lannouncer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index 2617b3e206bea8..78ecfd4efb07de 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -1,7 +1,7 @@ { "domain": "lastfm", "name": "Lastfm", - "documentation": "https://www.home-assistant.io/components/lastfm", + "documentation": "https://www.home-assistant.io/integrations/lastfm", "requirements": [ "pylast==3.1.0" ], diff --git a/homeassistant/components/launch_library/manifest.json b/homeassistant/components/launch_library/manifest.json index bbe9fa8ad054ce..5bf63f2b099044 100644 --- a/homeassistant/components/launch_library/manifest.json +++ b/homeassistant/components/launch_library/manifest.json @@ -1,7 +1,7 @@ { "domain": "launch_library", "name": "Launch library", - "documentation": "https://www.home-assistant.io/components/launch_library", + "documentation": "https://www.home-assistant.io/integrations/launch_library", "requirements": [ "pylaunches==0.2.0" ], diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 5a85b6673f2471..dcafe908d5cb28 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -1,7 +1,7 @@ { "domain": "lcn", "name": "Lcn", - "documentation": "https://www.home-assistant.io/components/lcn", + "documentation": "https://www.home-assistant.io/integrations/lcn", "requirements": [ "pypck==0.6.3" ], diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index 1728aa50614656..3f3d9d85c52091 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -1,7 +1,7 @@ { "domain": "lg_netcast", "name": "Lg netcast", - "documentation": "https://www.home-assistant.io/components/lg_netcast", + "documentation": "https://www.home-assistant.io/integrations/lg_netcast", "requirements": [ "pylgnetcast-homeassistant==0.2.0.dev0" ], diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index b09c8809382a7d..603f755fac11c8 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -1,7 +1,7 @@ { "domain": "lg_soundbar", "name": "Lg soundbar", - "documentation": "https://www.home-assistant.io/components/lg_soundbar", + "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", "requirements": [ "temescal==0.1" ], diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 9eae371070a5b9..a890ec39375788 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -2,7 +2,7 @@ "domain": "life360", "name": "Life360", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/life360", + "documentation": "https://www.home-assistant.io/integrations/life360", "dependencies": [], "codeowners": [ "@pnbruckner" diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 131d1a23b6a5f3..a8c2755aefb5ad 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -2,7 +2,7 @@ "domain": "lifx", "name": "Lifx", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/lifx", + "documentation": "https://www.home-assistant.io/integrations/lifx", "requirements": [ "aiolifx==0.6.7", "aiolifx_effects==0.2.2" diff --git a/homeassistant/components/lifx_cloud/manifest.json b/homeassistant/components/lifx_cloud/manifest.json index 83805692e4d246..f6aa8ccb7c5b86 100644 --- a/homeassistant/components/lifx_cloud/manifest.json +++ b/homeassistant/components/lifx_cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "lifx_cloud", "name": "Lifx cloud", - "documentation": "https://www.home-assistant.io/components/lifx_cloud", + "documentation": "https://www.home-assistant.io/integrations/lifx_cloud", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/lifx_legacy/manifest.json b/homeassistant/components/lifx_legacy/manifest.json index fb38b41f314c4a..de5f5ff04dee4f 100644 --- a/homeassistant/components/lifx_legacy/manifest.json +++ b/homeassistant/components/lifx_legacy/manifest.json @@ -1,7 +1,7 @@ { "domain": "lifx_legacy", "name": "Lifx legacy", - "documentation": "https://www.home-assistant.io/components/lifx_legacy", + "documentation": "https://www.home-assistant.io/integrations/lifx_legacy", "requirements": [ "liffylights==0.9.4" ], diff --git a/homeassistant/components/light/manifest.json b/homeassistant/components/light/manifest.json index 62eb96967f5ec5..88e7585f802b8a 100644 --- a/homeassistant/components/light/manifest.json +++ b/homeassistant/components/light/manifest.json @@ -1,7 +1,7 @@ { "domain": "light", "name": "Light", - "documentation": "https://www.home-assistant.io/components/light", + "documentation": "https://www.home-assistant.io/integrations/light", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/lightwave/manifest.json b/homeassistant/components/lightwave/manifest.json index a26500f69a6e25..4b2456f0df5814 100644 --- a/homeassistant/components/lightwave/manifest.json +++ b/homeassistant/components/lightwave/manifest.json @@ -1,7 +1,7 @@ { "domain": "lightwave", "name": "Lightwave", - "documentation": "https://www.home-assistant.io/components/lightwave", + "documentation": "https://www.home-assistant.io/integrations/lightwave", "requirements": [ "lightwave==0.15" ], diff --git a/homeassistant/components/limitlessled/manifest.json b/homeassistant/components/limitlessled/manifest.json index f8b42fabcbe12b..5eff655e806c2f 100644 --- a/homeassistant/components/limitlessled/manifest.json +++ b/homeassistant/components/limitlessled/manifest.json @@ -1,7 +1,7 @@ { "domain": "limitlessled", "name": "Limitlessled", - "documentation": "https://www.home-assistant.io/components/limitlessled", + "documentation": "https://www.home-assistant.io/integrations/limitlessled", "requirements": [ "limitlessled==1.1.3" ], diff --git a/homeassistant/components/linksys_smart/manifest.json b/homeassistant/components/linksys_smart/manifest.json index 19bb079c29cef3..28ed3f52036b4d 100644 --- a/homeassistant/components/linksys_smart/manifest.json +++ b/homeassistant/components/linksys_smart/manifest.json @@ -1,7 +1,7 @@ { "domain": "linksys_smart", "name": "Linksys smart", - "documentation": "https://www.home-assistant.io/components/linksys_smart", + "documentation": "https://www.home-assistant.io/integrations/linksys_smart", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/linky/manifest.json b/homeassistant/components/linky/manifest.json index 10a5bbcf86498c..a2505427f450d0 100644 --- a/homeassistant/components/linky/manifest.json +++ b/homeassistant/components/linky/manifest.json @@ -2,7 +2,7 @@ "domain": "linky", "name": "Linky", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/linky", + "documentation": "https://www.home-assistant.io/integrations/linky", "requirements": [ "pylinky==0.4.0" ], diff --git a/homeassistant/components/linode/manifest.json b/homeassistant/components/linode/manifest.json index 7dc2e0d7518e69..064f7e1ccf07dc 100644 --- a/homeassistant/components/linode/manifest.json +++ b/homeassistant/components/linode/manifest.json @@ -1,7 +1,7 @@ { "domain": "linode", "name": "Linode", - "documentation": "https://www.home-assistant.io/components/linode", + "documentation": "https://www.home-assistant.io/integrations/linode", "requirements": [ "linode-api==4.1.9b1" ], diff --git a/homeassistant/components/linux_battery/manifest.json b/homeassistant/components/linux_battery/manifest.json index 4c32b88b2d5b7e..3730f01622fd81 100644 --- a/homeassistant/components/linux_battery/manifest.json +++ b/homeassistant/components/linux_battery/manifest.json @@ -1,7 +1,7 @@ { "domain": "linux_battery", "name": "Linux battery", - "documentation": "https://www.home-assistant.io/components/linux_battery", + "documentation": "https://www.home-assistant.io/integrations/linux_battery", "requirements": [ "batinfo==0.4.2" ], diff --git a/homeassistant/components/lirc/manifest.json b/homeassistant/components/lirc/manifest.json index d11cf0b2f1ef71..b15799b54e330d 100644 --- a/homeassistant/components/lirc/manifest.json +++ b/homeassistant/components/lirc/manifest.json @@ -1,7 +1,7 @@ { "domain": "lirc", "name": "Lirc", - "documentation": "https://www.home-assistant.io/components/lirc", + "documentation": "https://www.home-assistant.io/integrations/lirc", "requirements": [ "python-lirc==1.2.3" ], diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index 08bcac67903088..988e2bd1ed47b0 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -1,7 +1,7 @@ { "domain": "litejet", "name": "Litejet", - "documentation": "https://www.home-assistant.io/components/litejet", + "documentation": "https://www.home-assistant.io/integrations/litejet", "requirements": [ "pylitejet==0.1" ], diff --git a/homeassistant/components/liveboxplaytv/manifest.json b/homeassistant/components/liveboxplaytv/manifest.json index 3393022a363d62..bcb2b53f081af6 100644 --- a/homeassistant/components/liveboxplaytv/manifest.json +++ b/homeassistant/components/liveboxplaytv/manifest.json @@ -1,7 +1,7 @@ { "domain": "liveboxplaytv", "name": "Liveboxplaytv", - "documentation": "https://www.home-assistant.io/components/liveboxplaytv", + "documentation": "https://www.home-assistant.io/integrations/liveboxplaytv", "requirements": [ "liveboxplaytv==2.0.2", "pyteleloisirs==3.5" diff --git a/homeassistant/components/llamalab_automate/manifest.json b/homeassistant/components/llamalab_automate/manifest.json index e66050fceb5728..2f46e6e790c177 100644 --- a/homeassistant/components/llamalab_automate/manifest.json +++ b/homeassistant/components/llamalab_automate/manifest.json @@ -1,7 +1,7 @@ { "domain": "llamalab_automate", "name": "Llamalab automate", - "documentation": "https://www.home-assistant.io/components/llamalab_automate", + "documentation": "https://www.home-assistant.io/integrations/llamalab_automate", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/local_file/manifest.json b/homeassistant/components/local_file/manifest.json index 14a503f33f571d..ded748be8dc5ac 100644 --- a/homeassistant/components/local_file/manifest.json +++ b/homeassistant/components/local_file/manifest.json @@ -1,7 +1,7 @@ { "domain": "local_file", "name": "Local file", - "documentation": "https://www.home-assistant.io/components/local_file", + "documentation": "https://www.home-assistant.io/integrations/local_file", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/locative/manifest.json b/homeassistant/components/locative/manifest.json index be2eb07a23cb1b..46a2d4de20e022 100644 --- a/homeassistant/components/locative/manifest.json +++ b/homeassistant/components/locative/manifest.json @@ -2,7 +2,7 @@ "domain": "locative", "name": "Locative", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/locative", + "documentation": "https://www.home-assistant.io/integrations/locative", "requirements": [], "dependencies": [ "webhook" diff --git a/homeassistant/components/lock/manifest.json b/homeassistant/components/lock/manifest.json index 29a7a5513d0854..0a76628a5b5211 100644 --- a/homeassistant/components/lock/manifest.json +++ b/homeassistant/components/lock/manifest.json @@ -1,7 +1,7 @@ { "domain": "lock", "name": "Lock", - "documentation": "https://www.home-assistant.io/components/lock", + "documentation": "https://www.home-assistant.io/integrations/lock", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/lockitron/manifest.json b/homeassistant/components/lockitron/manifest.json index b515d65a14fda4..18ab9036c5ec03 100644 --- a/homeassistant/components/lockitron/manifest.json +++ b/homeassistant/components/lockitron/manifest.json @@ -1,7 +1,7 @@ { "domain": "lockitron", "name": "Lockitron", - "documentation": "https://www.home-assistant.io/components/lockitron", + "documentation": "https://www.home-assistant.io/integrations/lockitron", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index cedce8152a294b..e8e3ad8ac2e580 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -1,7 +1,7 @@ { "domain": "logbook", "name": "Logbook", - "documentation": "https://www.home-assistant.io/components/logbook", + "documentation": "https://www.home-assistant.io/integrations/logbook", "requirements": [], "dependencies": [ "frontend", diff --git a/homeassistant/components/logentries/manifest.json b/homeassistant/components/logentries/manifest.json index 60be8f275eef60..c546030853faef 100644 --- a/homeassistant/components/logentries/manifest.json +++ b/homeassistant/components/logentries/manifest.json @@ -1,7 +1,7 @@ { "domain": "logentries", "name": "Logentries", - "documentation": "https://www.home-assistant.io/components/logentries", + "documentation": "https://www.home-assistant.io/integrations/logentries", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/logger/manifest.json b/homeassistant/components/logger/manifest.json index c6b6238703982d..ac201fb00a1d62 100644 --- a/homeassistant/components/logger/manifest.json +++ b/homeassistant/components/logger/manifest.json @@ -1,7 +1,7 @@ { "domain": "logger", "name": "Logger", - "documentation": "https://www.home-assistant.io/components/logger", + "documentation": "https://www.home-assistant.io/integrations/logger", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index b1767748395206..22502956e06aa5 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -2,7 +2,7 @@ "domain": "logi_circle", "name": "Logi Circle", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/logi_circle", + "documentation": "https://www.home-assistant.io/integrations/logi_circle", "requirements": ["logi_circle==0.2.2"], "dependencies": ["ffmpeg"], "codeowners": ["@evanjd"] diff --git a/homeassistant/components/london_air/manifest.json b/homeassistant/components/london_air/manifest.json index 3f0c97edfe012b..cca4a54bda8bd2 100644 --- a/homeassistant/components/london_air/manifest.json +++ b/homeassistant/components/london_air/manifest.json @@ -1,7 +1,7 @@ { "domain": "london_air", "name": "London air", - "documentation": "https://www.home-assistant.io/components/london_air", + "documentation": "https://www.home-assistant.io/integrations/london_air", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/london_underground/manifest.json b/homeassistant/components/london_underground/manifest.json index 5262fa4837ea9a..e66d5c1820dcdc 100644 --- a/homeassistant/components/london_underground/manifest.json +++ b/homeassistant/components/london_underground/manifest.json @@ -1,7 +1,7 @@ { "domain": "london_underground", "name": "London underground", - "documentation": "https://www.home-assistant.io/components/london_underground", + "documentation": "https://www.home-assistant.io/integrations/london_underground", "requirements": [ "london-tube-status==0.2" ], diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json index 20fe6fac2aa998..41e3d0dd6b07cc 100644 --- a/homeassistant/components/loopenergy/manifest.json +++ b/homeassistant/components/loopenergy/manifest.json @@ -1,7 +1,7 @@ { "domain": "loopenergy", "name": "Loopenergy", - "documentation": "https://www.home-assistant.io/components/loopenergy", + "documentation": "https://www.home-assistant.io/integrations/loopenergy", "requirements": [ "pyloopenergy==0.1.3" ], diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index dd8da40efe41a9..72be440d54c0e2 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -1,7 +1,7 @@ { "domain": "lovelace", "name": "Lovelace", - "documentation": "https://www.home-assistant.io/components/lovelace", + "documentation": "https://www.home-assistant.io/integrations/lovelace", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index dffb4b52667369..646fc1a3cbfa50 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -1,7 +1,7 @@ { "domain": "luci", "name": "Luci", - "documentation": "https://www.home-assistant.io/components/luci", + "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": [ "openwrt-luci-rpc==1.1.1" ], diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index 26d6c21f3a95df..112b58ba65df16 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -2,7 +2,7 @@ "domain": "luftdaten", "name": "Luftdaten", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/luftdaten", + "documentation": "https://www.home-assistant.io/integrations/luftdaten", "requirements": [ "luftdaten==0.6.3" ], diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 344ec82d976cb0..bb6b18243ec13f 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -1,7 +1,7 @@ { "domain": "lupusec", "name": "Lupusec", - "documentation": "https://www.home-assistant.io/components/lupusec", + "documentation": "https://www.home-assistant.io/integrations/lupusec", "requirements": [ "lupupy==0.0.17" ], diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index 451a6f3e33d822..cace2770de0bea 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -1,7 +1,7 @@ { "domain": "lutron", "name": "Lutron", - "documentation": "https://www.home-assistant.io/components/lutron", + "documentation": "https://www.home-assistant.io/integrations/lutron", "requirements": [ "pylutron==0.2.5" ], diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 4da58cdfc40274..d1501a562db4bd 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -1,7 +1,7 @@ { "domain": "lutron_caseta", "name": "Lutron caseta", - "documentation": "https://www.home-assistant.io/components/lutron_caseta", + "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", "requirements": [ "pylutron-caseta==0.5.0" ], diff --git a/homeassistant/components/lw12wifi/manifest.json b/homeassistant/components/lw12wifi/manifest.json index 205072055bbe22..7f830cda25becc 100644 --- a/homeassistant/components/lw12wifi/manifest.json +++ b/homeassistant/components/lw12wifi/manifest.json @@ -1,7 +1,7 @@ { "domain": "lw12wifi", "name": "Lw12wifi", - "documentation": "https://www.home-assistant.io/components/lw12wifi", + "documentation": "https://www.home-assistant.io/integrations/lw12wifi", "requirements": [ "lw12==0.9.2" ], diff --git a/homeassistant/components/lyft/manifest.json b/homeassistant/components/lyft/manifest.json index ff7da7190d94e6..e8b593b31453d1 100644 --- a/homeassistant/components/lyft/manifest.json +++ b/homeassistant/components/lyft/manifest.json @@ -1,7 +1,7 @@ { "domain": "lyft", "name": "Lyft", - "documentation": "https://www.home-assistant.io/components/lyft", + "documentation": "https://www.home-assistant.io/integrations/lyft", "requirements": [ "lyft_rides==0.2" ], diff --git a/homeassistant/components/magicseaweed/manifest.json b/homeassistant/components/magicseaweed/manifest.json index 6534d927f1b872..41795c117a9fa6 100644 --- a/homeassistant/components/magicseaweed/manifest.json +++ b/homeassistant/components/magicseaweed/manifest.json @@ -1,7 +1,7 @@ { "domain": "magicseaweed", "name": "Magicseaweed", - "documentation": "https://www.home-assistant.io/components/magicseaweed", + "documentation": "https://www.home-assistant.io/integrations/magicseaweed", "requirements": [ "magicseaweed==1.0.3" ], diff --git a/homeassistant/components/mailbox/manifest.json b/homeassistant/components/mailbox/manifest.json index 4ca1db564a4cf0..2883c9caf3429d 100644 --- a/homeassistant/components/mailbox/manifest.json +++ b/homeassistant/components/mailbox/manifest.json @@ -1,7 +1,7 @@ { "domain": "mailbox", "name": "Mailbox", - "documentation": "https://www.home-assistant.io/components/mailbox", + "documentation": "https://www.home-assistant.io/integrations/mailbox", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/mailgun/manifest.json b/homeassistant/components/mailgun/manifest.json index 9ed7a50a8e3c10..c0bd0823b8fe20 100644 --- a/homeassistant/components/mailgun/manifest.json +++ b/homeassistant/components/mailgun/manifest.json @@ -2,7 +2,7 @@ "domain": "mailgun", "name": "Mailgun", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/mailgun", + "documentation": "https://www.home-assistant.io/integrations/mailgun", "requirements": [ "pymailgunner==1.4" ], diff --git a/homeassistant/components/manual/manifest.json b/homeassistant/components/manual/manifest.json index 6c788971629ea8..12d5297c010167 100644 --- a/homeassistant/components/manual/manifest.json +++ b/homeassistant/components/manual/manifest.json @@ -1,7 +1,7 @@ { "domain": "manual", "name": "Manual", - "documentation": "https://www.home-assistant.io/components/manual", + "documentation": "https://www.home-assistant.io/integrations/manual", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/manual_mqtt/manifest.json b/homeassistant/components/manual_mqtt/manifest.json index 81cd1338450fd9..d9ec004e6a82dc 100644 --- a/homeassistant/components/manual_mqtt/manifest.json +++ b/homeassistant/components/manual_mqtt/manifest.json @@ -1,7 +1,7 @@ { "domain": "manual_mqtt", "name": "Manual mqtt", - "documentation": "https://www.home-assistant.io/components/manual_mqtt", + "documentation": "https://www.home-assistant.io/integrations/manual_mqtt", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/map/manifest.json b/homeassistant/components/map/manifest.json index d26d7d9530fdda..e64d18c92e1d5c 100644 --- a/homeassistant/components/map/manifest.json +++ b/homeassistant/components/map/manifest.json @@ -1,7 +1,7 @@ { "domain": "map", "name": "Map", - "documentation": "https://www.home-assistant.io/components/map", + "documentation": "https://www.home-assistant.io/integrations/map", "requirements": [], "dependencies": [ "frontend" diff --git a/homeassistant/components/marytts/manifest.json b/homeassistant/components/marytts/manifest.json index 5316935c442db6..0188f405e1492e 100644 --- a/homeassistant/components/marytts/manifest.json +++ b/homeassistant/components/marytts/manifest.json @@ -1,7 +1,7 @@ { "domain": "marytts", "name": "Marytts", - "documentation": "https://www.home-assistant.io/components/marytts", + "documentation": "https://www.home-assistant.io/integrations/marytts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index 4005e51e373505..e041ba2f669a51 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -1,7 +1,7 @@ { "domain": "mastodon", "name": "Mastodon", - "documentation": "https://www.home-assistant.io/components/mastodon", + "documentation": "https://www.home-assistant.io/integrations/mastodon", "requirements": [ "Mastodon.py==1.4.6" ], diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 9ea1a6f0c5558c..a467518c04ef87 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -1,7 +1,7 @@ { "domain": "matrix", "name": "Matrix", - "documentation": "https://www.home-assistant.io/components/matrix", + "documentation": "https://www.home-assistant.io/integrations/matrix", "requirements": [ "matrix-client==0.2.0" ], diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index a28096c5eb7767..1f5b1eef93592d 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -1,7 +1,7 @@ { "domain": "maxcube", "name": "Maxcube", - "documentation": "https://www.home-assistant.io/components/maxcube", + "documentation": "https://www.home-assistant.io/integrations/maxcube", "requirements": [ "maxcube-api==0.1.0" ], diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index 41048683c9214a..2dbffd829f8b8a 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -1,7 +1,7 @@ { "domain": "mcp23017", "name": "MCP23017 I/O Expander", - "documentation": "https://www.home-assistant.io/components/mcp23017", + "documentation": "https://www.home-assistant.io/integrations/mcp23017", "requirements": [ "RPi.GPIO==0.6.5", "adafruit-blinka==1.2.1", diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 71e1a81135a662..886535555d5a84 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -1,7 +1,7 @@ { "domain": "media_extractor", "name": "Media extractor", - "documentation": "https://www.home-assistant.io/components/media_extractor", + "documentation": "https://www.home-assistant.io/integrations/media_extractor", "requirements": [ "youtube_dl==2019.09.28" ], diff --git a/homeassistant/components/media_player/manifest.json b/homeassistant/components/media_player/manifest.json index bf6f8fabafa43c..4df8ad8442a727 100644 --- a/homeassistant/components/media_player/manifest.json +++ b/homeassistant/components/media_player/manifest.json @@ -1,7 +1,7 @@ { "domain": "media_player", "name": "Media player", - "documentation": "https://www.home-assistant.io/components/media_player", + "documentation": "https://www.home-assistant.io/integrations/media_player", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/mediaroom/manifest.json b/homeassistant/components/mediaroom/manifest.json index 134d85fa1712d2..0e42cd7e535ad3 100644 --- a/homeassistant/components/mediaroom/manifest.json +++ b/homeassistant/components/mediaroom/manifest.json @@ -1,7 +1,7 @@ { "domain": "mediaroom", "name": "Mediaroom", - "documentation": "https://www.home-assistant.io/components/mediaroom", + "documentation": "https://www.home-assistant.io/integrations/mediaroom", "requirements": [ "pymediaroom==0.6.4" ], diff --git a/homeassistant/components/melissa/manifest.json b/homeassistant/components/melissa/manifest.json index f9fa1cab502cdb..64760338f35cd1 100644 --- a/homeassistant/components/melissa/manifest.json +++ b/homeassistant/components/melissa/manifest.json @@ -1,7 +1,7 @@ { "domain": "melissa", "name": "Melissa", - "documentation": "https://www.home-assistant.io/components/melissa", + "documentation": "https://www.home-assistant.io/integrations/melissa", "requirements": [ "py-melissa-climate==2.0.0" ], diff --git a/homeassistant/components/meraki/manifest.json b/homeassistant/components/meraki/manifest.json index d03679ed41ed4b..2add8663555a95 100644 --- a/homeassistant/components/meraki/manifest.json +++ b/homeassistant/components/meraki/manifest.json @@ -1,7 +1,7 @@ { "domain": "meraki", "name": "Meraki", - "documentation": "https://www.home-assistant.io/components/meraki", + "documentation": "https://www.home-assistant.io/integrations/meraki", "requirements": [], "dependencies": ["http"], "codeowners": [] diff --git a/homeassistant/components/message_bird/manifest.json b/homeassistant/components/message_bird/manifest.json index a6c49b3c39688f..79428f951f134d 100644 --- a/homeassistant/components/message_bird/manifest.json +++ b/homeassistant/components/message_bird/manifest.json @@ -1,7 +1,7 @@ { "domain": "message_bird", "name": "Message bird", - "documentation": "https://www.home-assistant.io/components/message_bird", + "documentation": "https://www.home-assistant.io/integrations/message_bird", "requirements": [ "messagebird==1.2.0" ], diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 426d0faf8608b7..2652e33b76cfae 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -2,7 +2,7 @@ "domain": "met", "name": "Met", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/met", + "documentation": "https://www.home-assistant.io/integrations/met", "requirements": [ "pyMetno==0.4.6" ], diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index b485458be409e2..ba043bf2a714f8 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -1,7 +1,7 @@ { "domain": "meteo_france", "name": "Meteo france", - "documentation": "https://www.home-assistant.io/components/meteo_france", + "documentation": "https://www.home-assistant.io/integrations/meteo_france", "requirements": [ "meteofrance==0.3.7", "vigilancemeteo==3.0.0" diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 692e55260850d3..ee14ce7d26d8d2 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -1,7 +1,7 @@ { "domain": "meteoalarm", "name": "meteoalarm", - "documentation": "https://www.home-assistant.io/components/meteoalarm", + "documentation": "https://www.home-assistant.io/integrations/meteoalarm", "requirements": [ "meteoalertapi==0.1.6" ], diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index f5d358854f6f79..f5624e33edbefd 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -1,7 +1,7 @@ { "domain": "metoffice", "name": "Metoffice", - "documentation": "https://www.home-assistant.io/components/metoffice", + "documentation": "https://www.home-assistant.io/integrations/metoffice", "requirements": [ "datapoint==0.4.3" ], diff --git a/homeassistant/components/mfi/manifest.json b/homeassistant/components/mfi/manifest.json index 1e84b39a366e4f..c08b4e4c88a577 100644 --- a/homeassistant/components/mfi/manifest.json +++ b/homeassistant/components/mfi/manifest.json @@ -1,7 +1,7 @@ { "domain": "mfi", "name": "Mfi", - "documentation": "https://www.home-assistant.io/components/mfi", + "documentation": "https://www.home-assistant.io/integrations/mfi", "requirements": [ "mficlient==0.3.0" ], diff --git a/homeassistant/components/mhz19/manifest.json b/homeassistant/components/mhz19/manifest.json index 8545db90e27583..5bffcf8e92c25c 100644 --- a/homeassistant/components/mhz19/manifest.json +++ b/homeassistant/components/mhz19/manifest.json @@ -1,7 +1,7 @@ { "domain": "mhz19", "name": "Mhz19", - "documentation": "https://www.home-assistant.io/components/mhz19", + "documentation": "https://www.home-assistant.io/integrations/mhz19", "requirements": [ "pmsensor==0.4" ], diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 827d961a093859..16ae94c212ef62 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft", "name": "Microsoft", - "documentation": "https://www.home-assistant.io/components/microsoft", + "documentation": "https://www.home-assistant.io/integrations/microsoft", "requirements": [ "pycsspeechtts==1.0.2" ], diff --git a/homeassistant/components/microsoft_face/manifest.json b/homeassistant/components/microsoft_face/manifest.json index 7f6c4fbd935759..1d51dca42cd214 100644 --- a/homeassistant/components/microsoft_face/manifest.json +++ b/homeassistant/components/microsoft_face/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft_face", "name": "Microsoft face", - "documentation": "https://www.home-assistant.io/components/microsoft_face", + "documentation": "https://www.home-assistant.io/integrations/microsoft_face", "requirements": [], "dependencies": [ "camera" diff --git a/homeassistant/components/microsoft_face_detect/manifest.json b/homeassistant/components/microsoft_face_detect/manifest.json index b272a299cf5b93..12d73623e750d5 100644 --- a/homeassistant/components/microsoft_face_detect/manifest.json +++ b/homeassistant/components/microsoft_face_detect/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft_face_detect", "name": "Microsoft face detect", - "documentation": "https://www.home-assistant.io/components/microsoft_face_detect", + "documentation": "https://www.home-assistant.io/integrations/microsoft_face_detect", "requirements": [], "dependencies": ["microsoft_face"], "codeowners": [] diff --git a/homeassistant/components/microsoft_face_identify/manifest.json b/homeassistant/components/microsoft_face_identify/manifest.json index 10e4bde103cfc9..a52aca1ac0a049 100644 --- a/homeassistant/components/microsoft_face_identify/manifest.json +++ b/homeassistant/components/microsoft_face_identify/manifest.json @@ -1,7 +1,7 @@ { "domain": "microsoft_face_identify", "name": "Microsoft face identify", - "documentation": "https://www.home-assistant.io/components/microsoft_face_identify", + "documentation": "https://www.home-assistant.io/integrations/microsoft_face_identify", "requirements": [], "dependencies": ["microsoft_face"], "codeowners": [] diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index c7ef2b89611c43..54fa59135b338a 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -1,7 +1,7 @@ { "domain": "miflora", "name": "Miflora", - "documentation": "https://www.home-assistant.io/components/miflora", + "documentation": "https://www.home-assistant.io/integrations/miflora", "requirements": [ "bluepy==1.1.4", "miflora==0.4.0" diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index 92869856545ca6..9a05f5a9f870e0 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -1,7 +1,7 @@ { "domain": "mikrotik", "name": "Mikrotik", - "documentation": "https://www.home-assistant.io/components/mikrotik", + "documentation": "https://www.home-assistant.io/integrations/mikrotik", "requirements": [ "librouteros==2.3.0" ], diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 05efb845c12ed5..85e70c78ede6eb 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -1,7 +1,7 @@ { "domain": "mill", "name": "Mill", - "documentation": "https://www.home-assistant.io/components/mill", + "documentation": "https://www.home-assistant.io/integrations/mill", "requirements": [ "millheater==0.3.4" ], diff --git a/homeassistant/components/min_max/manifest.json b/homeassistant/components/min_max/manifest.json index ea6befe498b42e..ed899a0438fac0 100644 --- a/homeassistant/components/min_max/manifest.json +++ b/homeassistant/components/min_max/manifest.json @@ -1,7 +1,7 @@ { "domain": "min_max", "name": "Min max", - "documentation": "https://www.home-assistant.io/components/min_max", + "documentation": "https://www.home-assistant.io/integrations/min_max", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json index 2b2f84836ead39..bc373633503819 100644 --- a/homeassistant/components/minio/manifest.json +++ b/homeassistant/components/minio/manifest.json @@ -1,7 +1,7 @@ { "domain": "minio", "name": "Minio", - "documentation": "https://www.home-assistant.io/components/minio", + "documentation": "https://www.home-assistant.io/integrations/minio", "requirements": [ "minio==4.0.9" ], diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index 2324a861b38e5d..612e7c19f8bcd9 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -1,7 +1,7 @@ { "domain": "mitemp_bt", "name": "Mitemp bt", - "documentation": "https://www.home-assistant.io/components/mitemp_bt", + "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", "requirements": [ "mitemp_bt==0.0.1" ], diff --git a/homeassistant/components/mjpeg/manifest.json b/homeassistant/components/mjpeg/manifest.json index 2ecd66910be6c0..93f01ac2e3ab23 100644 --- a/homeassistant/components/mjpeg/manifest.json +++ b/homeassistant/components/mjpeg/manifest.json @@ -1,7 +1,7 @@ { "domain": "mjpeg", "name": "Mjpeg", - "documentation": "https://www.home-assistant.io/components/mjpeg", + "documentation": "https://www.home-assistant.io/integrations/mjpeg", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 85c6231daa8839..8c95ca4ad41e49 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -2,7 +2,7 @@ "domain": "mobile_app", "name": "Home Assistant Mobile App Support", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/mobile_app", + "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": [ "PyNaCl==1.3.0" ], diff --git a/homeassistant/components/mochad/manifest.json b/homeassistant/components/mochad/manifest.json index 0e5c4dd1ff3c1b..8994223fe31443 100644 --- a/homeassistant/components/mochad/manifest.json +++ b/homeassistant/components/mochad/manifest.json @@ -1,7 +1,7 @@ { "domain": "mochad", "name": "Mochad", - "documentation": "https://www.home-assistant.io/components/mochad", + "documentation": "https://www.home-assistant.io/integrations/mochad", "requirements": [ "pymochad==0.2.0" ], diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index e27f594b0af020..8d271d5a95f106 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -1,7 +1,7 @@ { "domain": "modbus", "name": "Modbus", - "documentation": "https://www.home-assistant.io/components/modbus", + "documentation": "https://www.home-assistant.io/integrations/modbus", "requirements": [ "pymodbus==1.5.2" ], diff --git a/homeassistant/components/modem_callerid/manifest.json b/homeassistant/components/modem_callerid/manifest.json index e3d6d19b803ddf..80174b1a83a90e 100644 --- a/homeassistant/components/modem_callerid/manifest.json +++ b/homeassistant/components/modem_callerid/manifest.json @@ -1,7 +1,7 @@ { "domain": "modem_callerid", "name": "Modem callerid", - "documentation": "https://www.home-assistant.io/components/modem_callerid", + "documentation": "https://www.home-assistant.io/integrations/modem_callerid", "requirements": [ "basicmodem==0.7" ], diff --git a/homeassistant/components/mold_indicator/manifest.json b/homeassistant/components/mold_indicator/manifest.json index de4680927a4737..1205b53ccaf1f0 100644 --- a/homeassistant/components/mold_indicator/manifest.json +++ b/homeassistant/components/mold_indicator/manifest.json @@ -1,7 +1,7 @@ { "domain": "mold_indicator", "name": "Mold indicator", - "documentation": "https://www.home-assistant.io/components/mold_indicator", + "documentation": "https://www.home-assistant.io/integrations/mold_indicator", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index aa07911a697304..47db73ce0de834 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -1,7 +1,7 @@ { "domain": "monoprice", "name": "Monoprice", - "documentation": "https://www.home-assistant.io/components/monoprice", + "documentation": "https://www.home-assistant.io/integrations/monoprice", "requirements": [ "pymonoprice==0.3" ], diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 50a93fce20a50c..56b5a1b8181239 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -1,7 +1,7 @@ { "domain": "moon", "name": "Moon", - "documentation": "https://www.home-assistant.io/components/moon", + "documentation": "https://www.home-assistant.io/integrations/moon", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/mopar/manifest.json b/homeassistant/components/mopar/manifest.json index 5acd5bbdcdbbbd..ddb51c9e682a59 100644 --- a/homeassistant/components/mopar/manifest.json +++ b/homeassistant/components/mopar/manifest.json @@ -1,7 +1,7 @@ { "domain": "mopar", "name": "Mopar", - "documentation": "https://www.home-assistant.io/components/mopar", + "documentation": "https://www.home-assistant.io/integrations/mopar", "requirements": [ "motorparts==1.1.0" ], diff --git a/homeassistant/components/mpchc/manifest.json b/homeassistant/components/mpchc/manifest.json index e874ca288912b7..7d4192434721c6 100644 --- a/homeassistant/components/mpchc/manifest.json +++ b/homeassistant/components/mpchc/manifest.json @@ -1,7 +1,7 @@ { "domain": "mpchc", "name": "Mpchc", - "documentation": "https://www.home-assistant.io/components/mpchc", + "documentation": "https://www.home-assistant.io/integrations/mpchc", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index beee3137ef544e..b7440f655dbb1f 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -1,7 +1,7 @@ { "domain": "mpd", "name": "Mpd", - "documentation": "https://www.home-assistant.io/components/mpd", + "documentation": "https://www.home-assistant.io/integrations/mpd", "requirements": [ "python-mpd2==1.0.0" ], diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index 2df50699a9d8d3..c1b6659e1c0733 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -2,7 +2,7 @@ "domain": "mqtt", "name": "MQTT", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/mqtt", + "documentation": "https://www.home-assistant.io/integrations/mqtt", "requirements": [ "hbmqtt==0.9.5", "paho-mqtt==1.4.0" diff --git a/homeassistant/components/mqtt_eventstream/manifest.json b/homeassistant/components/mqtt_eventstream/manifest.json index e795c8aaf181d8..0d36cd6616d148 100644 --- a/homeassistant/components/mqtt_eventstream/manifest.json +++ b/homeassistant/components/mqtt_eventstream/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_eventstream", "name": "Mqtt eventstream", - "documentation": "https://www.home-assistant.io/components/mqtt_eventstream", + "documentation": "https://www.home-assistant.io/integrations/mqtt_eventstream", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mqtt_json/manifest.json b/homeassistant/components/mqtt_json/manifest.json index a1986b2bf2eee2..b54488397941c1 100644 --- a/homeassistant/components/mqtt_json/manifest.json +++ b/homeassistant/components/mqtt_json/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_json", "name": "Mqtt json", - "documentation": "https://www.home-assistant.io/components/mqtt_json", + "documentation": "https://www.home-assistant.io/integrations/mqtt_json", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mqtt_room/manifest.json b/homeassistant/components/mqtt_room/manifest.json index 8fc90b0bcb1837..93450b001638fd 100644 --- a/homeassistant/components/mqtt_room/manifest.json +++ b/homeassistant/components/mqtt_room/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_room", "name": "Mqtt room", - "documentation": "https://www.home-assistant.io/components/mqtt_room", + "documentation": "https://www.home-assistant.io/integrations/mqtt_room", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mqtt_statestream/manifest.json b/homeassistant/components/mqtt_statestream/manifest.json index 5fa9936372932d..840a53591a8235 100644 --- a/homeassistant/components/mqtt_statestream/manifest.json +++ b/homeassistant/components/mqtt_statestream/manifest.json @@ -1,7 +1,7 @@ { "domain": "mqtt_statestream", "name": "Mqtt statestream", - "documentation": "https://www.home-assistant.io/components/mqtt_statestream", + "documentation": "https://www.home-assistant.io/integrations/mqtt_statestream", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/mvglive/manifest.json b/homeassistant/components/mvglive/manifest.json index 5626e2444849fb..e47b7c07d8bc5f 100644 --- a/homeassistant/components/mvglive/manifest.json +++ b/homeassistant/components/mvglive/manifest.json @@ -1,7 +1,7 @@ { "domain": "mvglive", "name": "Mvglive", - "documentation": "https://www.home-assistant.io/components/mvglive", + "documentation": "https://www.home-assistant.io/integrations/mvglive", "requirements": [ "PyMVGLive==1.1.4" ], diff --git a/homeassistant/components/mychevy/manifest.json b/homeassistant/components/mychevy/manifest.json index 1ff997372ed214..20933c9b2fcf0b 100644 --- a/homeassistant/components/mychevy/manifest.json +++ b/homeassistant/components/mychevy/manifest.json @@ -1,7 +1,7 @@ { "domain": "mychevy", "name": "Mychevy", - "documentation": "https://www.home-assistant.io/components/mychevy", + "documentation": "https://www.home-assistant.io/integrations/mychevy", "requirements": [ "mychevy==1.2.0" ], diff --git a/homeassistant/components/mycroft/manifest.json b/homeassistant/components/mycroft/manifest.json index 77e5a524aacc47..5d5ee195381568 100644 --- a/homeassistant/components/mycroft/manifest.json +++ b/homeassistant/components/mycroft/manifest.json @@ -1,7 +1,7 @@ { "domain": "mycroft", "name": "Mycroft", - "documentation": "https://www.home-assistant.io/components/mycroft", + "documentation": "https://www.home-assistant.io/integrations/mycroft", "requirements": [ "mycroftapi==2.0" ], diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index b870ff663098f6..213679b320a632 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -1,7 +1,7 @@ { "domain": "myq", "name": "Myq", - "documentation": "https://www.home-assistant.io/components/myq", + "documentation": "https://www.home-assistant.io/integrations/myq", "requirements": [ "pymyq==1.2.1" ], diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index 536848d3aef645..8701424ea60e4a 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -1,7 +1,7 @@ { "domain": "mysensors", "name": "Mysensors", - "documentation": "https://www.home-assistant.io/components/mysensors", + "documentation": "https://www.home-assistant.io/integrations/mysensors", "requirements": [ "pymysensors==0.18.0" ], diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index 0e17f33f72ebd2..fe09461bc82f17 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -1,7 +1,7 @@ { "domain": "mystrom", "name": "Mystrom", - "documentation": "https://www.home-assistant.io/components/mystrom", + "documentation": "https://www.home-assistant.io/integrations/mystrom", "requirements": [ "python-mystrom==0.5.0" ], diff --git a/homeassistant/components/mythicbeastsdns/manifest.json b/homeassistant/components/mythicbeastsdns/manifest.json index 4e37544a99ad2c..b912a80f75d86a 100644 --- a/homeassistant/components/mythicbeastsdns/manifest.json +++ b/homeassistant/components/mythicbeastsdns/manifest.json @@ -1,7 +1,7 @@ { "domain": "mythicbeastsdns", "name": "Mythicbeastsdns", - "documentation": "https://www.home-assistant.io/components/mythicbeastsdns", + "documentation": "https://www.home-assistant.io/integrations/mythicbeastsdns", "requirements": [ "mbddns==0.1.2" ], diff --git a/homeassistant/components/n26/manifest.json b/homeassistant/components/n26/manifest.json index b49932887d504b..7f010ec6d39f52 100644 --- a/homeassistant/components/n26/manifest.json +++ b/homeassistant/components/n26/manifest.json @@ -1,7 +1,7 @@ { "domain": "n26", "name": "N26", - "documentation": "https://www.home-assistant.io/components/n26", + "documentation": "https://www.home-assistant.io/integrations/n26", "requirements": [ "n26==0.2.7" ], diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json index c624acd73da5e4..7e01f818e351eb 100644 --- a/homeassistant/components/nad/manifest.json +++ b/homeassistant/components/nad/manifest.json @@ -1,7 +1,7 @@ { "domain": "nad", "name": "Nad", - "documentation": "https://www.home-assistant.io/components/nad", + "documentation": "https://www.home-assistant.io/integrations/nad", "requirements": [ "nad_receiver==0.0.11" ], diff --git a/homeassistant/components/namecheapdns/manifest.json b/homeassistant/components/namecheapdns/manifest.json index e75e2caa37ae22..aa1e2eb7422c0c 100644 --- a/homeassistant/components/namecheapdns/manifest.json +++ b/homeassistant/components/namecheapdns/manifest.json @@ -1,7 +1,7 @@ { "domain": "namecheapdns", "name": "Namecheapdns", - "documentation": "https://www.home-assistant.io/components/namecheapdns", + "documentation": "https://www.home-assistant.io/integrations/namecheapdns", "requirements": [ "defusedxml==0.6.0" ], diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index a59a6352af213d..3318ad3438fc35 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -1,7 +1,7 @@ { "domain": "nanoleaf", "name": "Nanoleaf", - "documentation": "https://www.home-assistant.io/components/nanoleaf", + "documentation": "https://www.home-assistant.io/integrations/nanoleaf", "requirements": [ "pynanoleaf==0.0.5" ], diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 71553fabc8e14f..8b0c5acc72362c 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -1,7 +1,7 @@ { "domain": "neato", "name": "Neato", - "documentation": "https://www.home-assistant.io/components/neato", + "documentation": "https://www.home-assistant.io/integrations/neato", "requirements": [ "pybotvac==0.0.15" ], diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index baa6551cc7c2fa..c1360ecbe3050b 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -1,7 +1,7 @@ { "domain": "nederlandse_spoorwegen", "name": "Nederlandse spoorwegen", - "documentation": "https://www.home-assistant.io/components/nederlandse_spoorwegen", + "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", "requirements": [ "nsapi==2.7.4" ], diff --git a/homeassistant/components/nello/manifest.json b/homeassistant/components/nello/manifest.json index 0caafd7e27adf7..e747bf7739f06e 100644 --- a/homeassistant/components/nello/manifest.json +++ b/homeassistant/components/nello/manifest.json @@ -1,7 +1,7 @@ { "domain": "nello", "name": "Nello", - "documentation": "https://www.home-assistant.io/components/nello", + "documentation": "https://www.home-assistant.io/integrations/nello", "requirements": [ "pynello==2.0.2" ], diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index 93b19470ac434f..a1cae2df1d7319 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -1,7 +1,7 @@ { "domain": "ness_alarm", "name": "Ness alarm", - "documentation": "https://www.home-assistant.io/components/ness_alarm", + "documentation": "https://www.home-assistant.io/integrations/ness_alarm", "requirements": [ "nessclient==0.9.15" ], diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 8a6e8ec611afc9..b7d5f132045f0f 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,7 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/nest", + "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": [ "python-nest==4.1.0" ], diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 82f32c34407cab..83091368aff2ee 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -1,7 +1,7 @@ { "domain": "netatmo", "name": "Netatmo", - "documentation": "https://www.home-assistant.io/components/netatmo", + "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ "pyatmo==2.2.1" ], diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 9c3b8ad33d23c9..da4d15c28aa838 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -1,7 +1,7 @@ { "domain": "netdata", "name": "Netdata", - "documentation": "https://www.home-assistant.io/components/netdata", + "documentation": "https://www.home-assistant.io/integrations/netdata", "requirements": [ "netdata==0.1.2" ], diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 3ee3b189939dc8..af709c755c8a9a 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -1,7 +1,7 @@ { "domain": "netgear", "name": "Netgear", - "documentation": "https://www.home-assistant.io/components/netgear", + "documentation": "https://www.home-assistant.io/integrations/netgear", "requirements": [ "pynetgear==0.6.1" ], diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 99ca3cb1ccfb82..7e085d063077aa 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -1,7 +1,7 @@ { "domain": "netgear_lte", "name": "Netgear lte", - "documentation": "https://www.home-assistant.io/components/netgear_lte", + "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "requirements": [ "eternalegypt==0.0.10" ], diff --git a/homeassistant/components/netio/manifest.json b/homeassistant/components/netio/manifest.json index e3675db04d7305..3755746aee1147 100644 --- a/homeassistant/components/netio/manifest.json +++ b/homeassistant/components/netio/manifest.json @@ -1,7 +1,7 @@ { "domain": "netio", "name": "Netio", - "documentation": "https://www.home-assistant.io/components/netio", + "documentation": "https://www.home-assistant.io/integrations/netio", "requirements": [ "pynetio==0.1.9.1" ], diff --git a/homeassistant/components/neurio_energy/manifest.json b/homeassistant/components/neurio_energy/manifest.json index 04420d5c4f2116..735baf58e5aeb6 100644 --- a/homeassistant/components/neurio_energy/manifest.json +++ b/homeassistant/components/neurio_energy/manifest.json @@ -1,7 +1,7 @@ { "domain": "neurio_energy", "name": "Neurio energy", - "documentation": "https://www.home-assistant.io/components/neurio_energy", + "documentation": "https://www.home-assistant.io/integrations/neurio_energy", "requirements": [ "neurio==0.3.1" ], diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 5c5a095c8f4cef..525947a8c74555 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -1,7 +1,7 @@ { "domain": "nextbus", "name": "NextBus", - "documentation": "https://www.home-assistant.io/components/nextbus", + "documentation": "https://www.home-assistant.io/integrations/nextbus", "dependencies": [], "codeowners": ["@vividboarder"], "requirements": ["py_nextbusnext==0.1.4"] diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index 8f3d88b58ee604..45eedf8a1540bd 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -1,7 +1,7 @@ { "domain": "nfandroidtv", "name": "Nfandroidtv", - "documentation": "https://www.home-assistant.io/components/nfandroidtv", + "documentation": "https://www.home-assistant.io/integrations/nfandroidtv", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index 8cb58a7b74c81c..0e168601c4cf54 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -1,7 +1,7 @@ { "domain": "niko_home_control", "name": "Niko home control", - "documentation": "https://www.home-assistant.io/components/niko_home_control", + "documentation": "https://www.home-assistant.io/integrations/niko_home_control", "requirements": [ "niko-home-control==0.2.1" ], diff --git a/homeassistant/components/nilu/manifest.json b/homeassistant/components/nilu/manifest.json index ee7645653e6d0b..fe7a92bc2705bb 100644 --- a/homeassistant/components/nilu/manifest.json +++ b/homeassistant/components/nilu/manifest.json @@ -1,7 +1,7 @@ { "domain": "nilu", "name": "Nilu", - "documentation": "https://www.home-assistant.io/components/nilu", + "documentation": "https://www.home-assistant.io/integrations/nilu", "requirements": [ "niluclient==0.1.2" ], diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index 70aaa112414beb..a31812921470f4 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -1,7 +1,7 @@ { "domain": "nissan_leaf", "name": "Nissan leaf", - "documentation": "https://www.home-assistant.io/components/nissan_leaf", + "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", "requirements": [ "pycarwings2==2.9" ], diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index 0380acba1aca58..b207bd07a423a3 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -1,7 +1,7 @@ { "domain": "nmap_tracker", "name": "Nmap tracker", - "documentation": "https://www.home-assistant.io/components/nmap_tracker", + "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", "requirements": [ "python-nmap==0.6.1", "getmac==0.8.1" diff --git a/homeassistant/components/nmbs/manifest.json b/homeassistant/components/nmbs/manifest.json index 1a2fa0556883f1..5fe5f743fd3a9c 100644 --- a/homeassistant/components/nmbs/manifest.json +++ b/homeassistant/components/nmbs/manifest.json @@ -1,7 +1,7 @@ { "domain": "nmbs", "name": "Nmbs", - "documentation": "https://www.home-assistant.io/components/nmbs", + "documentation": "https://www.home-assistant.io/integrations/nmbs", "requirements": [ "pyrail==0.0.3" ], diff --git a/homeassistant/components/no_ip/manifest.json b/homeassistant/components/no_ip/manifest.json index 125815995329e4..438b61136eb923 100644 --- a/homeassistant/components/no_ip/manifest.json +++ b/homeassistant/components/no_ip/manifest.json @@ -1,7 +1,7 @@ { "domain": "no_ip", "name": "No ip", - "documentation": "https://www.home-assistant.io/components/no_ip", + "documentation": "https://www.home-assistant.io/integrations/no_ip", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/noaa_tides/manifest.json b/homeassistant/components/noaa_tides/manifest.json index 9ffc0215fd15b7..069fc238f72101 100644 --- a/homeassistant/components/noaa_tides/manifest.json +++ b/homeassistant/components/noaa_tides/manifest.json @@ -1,7 +1,7 @@ { "domain": "noaa_tides", "name": "Noaa tides", - "documentation": "https://www.home-assistant.io/components/noaa_tides", + "documentation": "https://www.home-assistant.io/integrations/noaa_tides", "requirements": [ "py_noaa==0.3.0" ], diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index 08c9932c36f5ce..e7ee2d2dcd5cbb 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -1,7 +1,7 @@ { "domain": "norway_air", "name": "Norway air", - "documentation": "https://www.home-assistant.io/components/norway_air", + "documentation": "https://www.home-assistant.io/integrations/norway_air", "requirements": [ "pyMetno==0.4.6" ], diff --git a/homeassistant/components/notify/manifest.json b/homeassistant/components/notify/manifest.json index bad39a1cb97ff5..6586496abbef27 100644 --- a/homeassistant/components/notify/manifest.json +++ b/homeassistant/components/notify/manifest.json @@ -1,7 +1,7 @@ { "domain": "notify", "name": "Notify", - "documentation": "https://www.home-assistant.io/components/notify", + "documentation": "https://www.home-assistant.io/integrations/notify", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index 827d406a1b5c79..2a400754b46526 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -2,7 +2,7 @@ "domain": "notion", "name": "Notion", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/notion", + "documentation": "https://www.home-assistant.io/integrations/notion", "requirements": [ "aionotion==1.1.0" ], diff --git a/homeassistant/components/nsw_fuel_station/manifest.json b/homeassistant/components/nsw_fuel_station/manifest.json index 6be24fb5a2cb0c..744497c22c47b2 100644 --- a/homeassistant/components/nsw_fuel_station/manifest.json +++ b/homeassistant/components/nsw_fuel_station/manifest.json @@ -1,7 +1,7 @@ { "domain": "nsw_fuel_station", "name": "Nsw fuel station", - "documentation": "https://www.home-assistant.io/components/nsw_fuel_station", + "documentation": "https://www.home-assistant.io/integrations/nsw_fuel_station", "requirements": [ "nsw-fuel-api-client==1.0.10" ], diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index 4542eb45c8222e..3d16f0a57e34cc 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -1,7 +1,7 @@ { "domain": "nsw_rural_fire_service_feed", "name": "Nsw rural fire service feed", - "documentation": "https://www.home-assistant.io/components/nsw_rural_fire_service_feed", + "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", "requirements": [ "geojson_client==0.4" ], diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index c9e69c44ec2e19..3d055f8f975436 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -1,7 +1,7 @@ { "domain": "nuheat", "name": "Nuheat", - "documentation": "https://www.home-assistant.io/components/nuheat", + "documentation": "https://www.home-assistant.io/integrations/nuheat", "requirements": [ "nuheat==0.3.0" ], diff --git a/homeassistant/components/nuimo_controller/manifest.json b/homeassistant/components/nuimo_controller/manifest.json index 9f18d2849f8ee3..a88faaa6f300b5 100644 --- a/homeassistant/components/nuimo_controller/manifest.json +++ b/homeassistant/components/nuimo_controller/manifest.json @@ -1,7 +1,7 @@ { "domain": "nuimo_controller", "name": "Nuimo controller", - "documentation": "https://www.home-assistant.io/components/nuimo_controller", + "documentation": "https://www.home-assistant.io/integrations/nuimo_controller", "requirements": [ "--only-binary=all nuimo==0.1.0" ], diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index e7f078a1a0594a..77043f37134f89 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -1,7 +1,7 @@ { "domain": "nuki", "name": "Nuki", - "documentation": "https://www.home-assistant.io/components/nuki", + "documentation": "https://www.home-assistant.io/integrations/nuki", "requirements": ["pynuki==1.3.3"], "dependencies": [], "codeowners": ["@pvizeli"] diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 920e56fba7cd47..21231c27873243 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -1,7 +1,7 @@ { "domain": "nut", "name": "Nut", - "documentation": "https://www.home-assistant.io/components/nut", + "documentation": "https://www.home-assistant.io/integrations/nut", "requirements": [ "pynut2==2.1.2" ], diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index bad90d9e827d77..b112a9ea4ea4c8 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -1,7 +1,7 @@ { "domain": "nws", "name": "National Weather Service", - "documentation": "https://www.home-assistant.io/components/nws", + "documentation": "https://www.home-assistant.io/integrations/nws", "dependencies": [], "codeowners": ["@MatthewFlamm"], "requirements": ["pynws==0.8.1"] diff --git a/homeassistant/components/nx584/manifest.json b/homeassistant/components/nx584/manifest.json index 67b5b0e2eeb96c..64f72986d077a2 100644 --- a/homeassistant/components/nx584/manifest.json +++ b/homeassistant/components/nx584/manifest.json @@ -1,7 +1,7 @@ { "domain": "nx584", "name": "Nx584", - "documentation": "https://www.home-assistant.io/components/nx584", + "documentation": "https://www.home-assistant.io/integrations/nx584", "requirements": [ "pynx584==0.4" ], diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json index 17b11d6aef9fdc..1dc16fdba407d2 100644 --- a/homeassistant/components/nzbget/manifest.json +++ b/homeassistant/components/nzbget/manifest.json @@ -1,7 +1,7 @@ { "domain": "nzbget", "name": "Nzbget", - "documentation": "https://www.home-assistant.io/components/nzbget", + "documentation": "https://www.home-assistant.io/integrations/nzbget", "requirements": ["pynzbgetapi==0.2.0"], "dependencies": [], "codeowners": ["@chriscla"] diff --git a/homeassistant/components/oasa_telematics/manifest.json b/homeassistant/components/oasa_telematics/manifest.json index 15bf40e63c865c..e6cc998352df66 100644 --- a/homeassistant/components/oasa_telematics/manifest.json +++ b/homeassistant/components/oasa_telematics/manifest.json @@ -1,7 +1,7 @@ { "domain": "oasa_telematics", "name": "OASA Telematics", - "documentation": "https://www.home-assistant.io/components/oasa_telematics/", + "documentation": "https://www.home-assistant.io/integrations/oasa_telematics/", "requirements": [ "oasatelematics==0.3" ], diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index 68045ff0584684..09087663b47037 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -1,7 +1,7 @@ { "domain": "obihai", "name": "Obihai", - "documentation": "https://www.home-assistant.io/components/obihai", + "documentation": "https://www.home-assistant.io/integrations/obihai", "requirements": [ "pyobihai==1.2.0" ], diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index c34e1458e4bdb5..77f6a22e69f5a6 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -1,7 +1,7 @@ { "domain": "octoprint", "name": "Octoprint", - "documentation": "https://www.home-assistant.io/components/octoprint", + "documentation": "https://www.home-assistant.io/integrations/octoprint", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/oem/manifest.json b/homeassistant/components/oem/manifest.json index d23b07b2756633..1634676a60beeb 100644 --- a/homeassistant/components/oem/manifest.json +++ b/homeassistant/components/oem/manifest.json @@ -1,7 +1,7 @@ { "domain": "oem", "name": "Oem", - "documentation": "https://www.home-assistant.io/components/oem", + "documentation": "https://www.home-assistant.io/integrations/oem", "requirements": [ "oemthermostat==1.1" ], diff --git a/homeassistant/components/ohmconnect/manifest.json b/homeassistant/components/ohmconnect/manifest.json index 33c93bc8ac139a..c958d23e725d6a 100644 --- a/homeassistant/components/ohmconnect/manifest.json +++ b/homeassistant/components/ohmconnect/manifest.json @@ -1,7 +1,7 @@ { "domain": "ohmconnect", "name": "Ohmconnect", - "documentation": "https://www.home-assistant.io/components/ohmconnect", + "documentation": "https://www.home-assistant.io/integrations/ohmconnect", "requirements": [ "defusedxml==0.6.0" ], diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json index 066f3270ccdd5b..fb6daf00f6646b 100644 --- a/homeassistant/components/ombi/manifest.json +++ b/homeassistant/components/ombi/manifest.json @@ -1,7 +1,7 @@ { "domain": "ombi", "name": "Ombi", - "documentation": "https://www.home-assistant.io/components/ombi/", + "documentation": "https://www.home-assistant.io/integrations/ombi/", "dependencies": [], "codeowners": ["@larssont"], "requirements": ["pyombi==0.1.5"] diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index ffb01bd56021d5..2febfc481e01f9 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -1,7 +1,7 @@ { "domain": "onboarding", "name": "Onboarding", - "documentation": "https://www.home-assistant.io/components/onboarding", + "documentation": "https://www.home-assistant.io/integrations/onboarding", "requirements": [], "dependencies": [ "auth", diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index 00075d4485f4e4..2d8c6c7107107e 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -1,7 +1,7 @@ { "domain": "onewire", "name": "Onewire", - "documentation": "https://www.home-assistant.io/components/onewire", + "documentation": "https://www.home-assistant.io/integrations/onewire", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index 7fd27dd7edf8b1..5bb116dece8efa 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -1,7 +1,7 @@ { "domain": "onkyo", "name": "Onkyo", - "documentation": "https://www.home-assistant.io/components/onkyo", + "documentation": "https://www.home-assistant.io/integrations/onkyo", "requirements": [ "onkyo-eiscp==1.2.4" ], diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index d86ec38ccb7178..f6c23712188ca9 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -1,7 +1,7 @@ { "domain": "onvif", "name": "Onvif", - "documentation": "https://www.home-assistant.io/components/onvif", + "documentation": "https://www.home-assistant.io/integrations/onvif", "requirements": [ "onvif-zeep-async==0.2.0" ], diff --git a/homeassistant/components/openalpr_cloud/manifest.json b/homeassistant/components/openalpr_cloud/manifest.json index f0421295836f03..56128f0e8866e0 100644 --- a/homeassistant/components/openalpr_cloud/manifest.json +++ b/homeassistant/components/openalpr_cloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "openalpr_cloud", "name": "Openalpr cloud", - "documentation": "https://www.home-assistant.io/components/openalpr_cloud", + "documentation": "https://www.home-assistant.io/integrations/openalpr_cloud", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json index 3c92e840f43406..65fa6b1bec6d6a 100644 --- a/homeassistant/components/openalpr_local/manifest.json +++ b/homeassistant/components/openalpr_local/manifest.json @@ -1,7 +1,7 @@ { "domain": "openalpr_local", "name": "Openalpr local", - "documentation": "https://www.home-assistant.io/components/openalpr_local", + "documentation": "https://www.home-assistant.io/integrations/openalpr_local", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index e8ebeb102e67b1..40421674a4b203 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -1,7 +1,7 @@ { "domain": "opencv", "name": "Opencv", - "documentation": "https://www.home-assistant.io/components/opencv", + "documentation": "https://www.home-assistant.io/integrations/opencv", "requirements": [ "numpy==1.17.1", "opencv-python-headless==4.1.1.26" diff --git a/homeassistant/components/openevse/manifest.json b/homeassistant/components/openevse/manifest.json index f37c769d20e5be..a56f0caddabedd 100644 --- a/homeassistant/components/openevse/manifest.json +++ b/homeassistant/components/openevse/manifest.json @@ -1,7 +1,7 @@ { "domain": "openevse", "name": "Openevse", - "documentation": "https://www.home-assistant.io/components/openevse", + "documentation": "https://www.home-assistant.io/integrations/openevse", "requirements": [ "openevsewifi==0.4" ], diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index ffb86d4a5e2d6c..fae9e616e5e737 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -1,7 +1,7 @@ { "domain": "openexchangerates", "name": "Openexchangerates", - "documentation": "https://www.home-assistant.io/components/openexchangerates", + "documentation": "https://www.home-assistant.io/integrations/openexchangerates", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index 95f944b7087a75..4dcb53e98ce369 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -1,7 +1,7 @@ { "domain": "opengarage", "name": "Opengarage", - "documentation": "https://www.home-assistant.io/components/opengarage", + "documentation": "https://www.home-assistant.io/integrations/opengarage", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/openhardwaremonitor/manifest.json b/homeassistant/components/openhardwaremonitor/manifest.json index d9281f08eda102..4a17f9335150ec 100644 --- a/homeassistant/components/openhardwaremonitor/manifest.json +++ b/homeassistant/components/openhardwaremonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "openhardwaremonitor", "name": "Openhardwaremonitor", - "documentation": "https://www.home-assistant.io/components/openhardwaremonitor", + "documentation": "https://www.home-assistant.io/integrations/openhardwaremonitor", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 276346ae79bff0..2ec58b86125fb8 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -1,7 +1,7 @@ { "domain": "openhome", "name": "Openhome", - "documentation": "https://www.home-assistant.io/components/openhome", + "documentation": "https://www.home-assistant.io/integrations/openhome", "requirements": [ "openhomedevice==0.4.2" ], diff --git a/homeassistant/components/opensensemap/manifest.json b/homeassistant/components/opensensemap/manifest.json index ab03f1cf7c6824..632ab82918e036 100644 --- a/homeassistant/components/opensensemap/manifest.json +++ b/homeassistant/components/opensensemap/manifest.json @@ -1,7 +1,7 @@ { "domain": "opensensemap", "name": "Opensensemap", - "documentation": "https://www.home-assistant.io/components/opensensemap", + "documentation": "https://www.home-assistant.io/integrations/opensensemap", "requirements": [ "opensensemap-api==0.1.5" ], diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index dd58cdd416816b..a98e828a52ef80 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -1,7 +1,7 @@ { "domain": "opensky", "name": "Opensky", - "documentation": "https://www.home-assistant.io/components/opensky", + "documentation": "https://www.home-assistant.io/integrations/opensky", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index c6097a01cc421c..9c7f165c6dfa19 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -1,7 +1,7 @@ { "domain": "opentherm_gw", "name": "Opentherm Gateway", - "documentation": "https://www.home-assistant.io/components/opentherm_gw", + "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": [ "pyotgw==0.4b4" ], diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index 0cfb02e81d64eb..69342df235f38f 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -2,7 +2,7 @@ "domain": "openuv", "name": "Openuv", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/openuv", + "documentation": "https://www.home-assistant.io/integrations/openuv", "requirements": [ "pyopenuv==1.0.9" ], diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index d24b23f64bb470..c28d27c1b8b5d1 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -1,7 +1,7 @@ { "domain": "openweathermap", "name": "Openweathermap", - "documentation": "https://www.home-assistant.io/components/openweathermap", + "documentation": "https://www.home-assistant.io/integrations/openweathermap", "requirements": [ "pyowm==2.10.0" ], diff --git a/homeassistant/components/opple/manifest.json b/homeassistant/components/opple/manifest.json index c10be48f3fa3cc..cee6447abc1afc 100644 --- a/homeassistant/components/opple/manifest.json +++ b/homeassistant/components/opple/manifest.json @@ -1,7 +1,7 @@ { "domain": "opple", "name": "Opple", - "documentation": "https://www.home-assistant.io/components/opple", + "documentation": "https://www.home-assistant.io/integrations/opple", "requirements": [ "pyoppleio==1.0.5" ], diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json index 65fd0f7de50836..51bca8fbbbe85c 100644 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -1,7 +1,7 @@ { "domain": "orangepi_gpio", "name": "Orangepi GPIO", - "documentation": "https://www.home-assistant.io/components/orangepi_gpio", + "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", "requirements": [ "OPi.GPIO==0.3.6" ], diff --git a/homeassistant/components/orvibo/manifest.json b/homeassistant/components/orvibo/manifest.json index 73f4eaed7dae11..9dee62697c34ee 100644 --- a/homeassistant/components/orvibo/manifest.json +++ b/homeassistant/components/orvibo/manifest.json @@ -1,7 +1,7 @@ { "domain": "orvibo", "name": "Orvibo", - "documentation": "https://www.home-assistant.io/components/orvibo", + "documentation": "https://www.home-assistant.io/integrations/orvibo", "requirements": [ "orvibo==1.1.1" ], diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json index 0b158b967423b9..8c6c9f30b1060b 100644 --- a/homeassistant/components/osramlightify/manifest.json +++ b/homeassistant/components/osramlightify/manifest.json @@ -1,7 +1,7 @@ { "domain": "osramlightify", "name": "Osramlightify", - "documentation": "https://www.home-assistant.io/components/osramlightify", + "documentation": "https://www.home-assistant.io/integrations/osramlightify", "requirements": [ "lightify==1.0.7.2" ], diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index 112fca24194a8e..25ece71c11b56b 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -1,7 +1,7 @@ { "domain": "otp", "name": "Otp", - "documentation": "https://www.home-assistant.io/components/otp", + "documentation": "https://www.home-assistant.io/integrations/otp", "requirements": [ "pyotp==2.3.0" ], diff --git a/homeassistant/components/owlet/manifest.json b/homeassistant/components/owlet/manifest.json index b89947343c94f4..2ba00671b485fe 100644 --- a/homeassistant/components/owlet/manifest.json +++ b/homeassistant/components/owlet/manifest.json @@ -1,7 +1,7 @@ { "domain": "owlet", "name": "Owlet", - "documentation": "https://www.home-assistant.io/components/owlet", + "documentation": "https://www.home-assistant.io/integrations/owlet", "requirements": [ "pyowlet==1.0.3" ], diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index bc4fe97bc7f138..529d7990a8644e 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -2,7 +2,7 @@ "domain": "owntracks", "name": "Owntracks", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/owntracks", + "documentation": "https://www.home-assistant.io/integrations/owntracks", "requirements": [ "PyNaCl==1.3.0" ], diff --git a/homeassistant/components/panasonic_bluray/manifest.json b/homeassistant/components/panasonic_bluray/manifest.json index fe2387744ab216..7f386464dc99a3 100644 --- a/homeassistant/components/panasonic_bluray/manifest.json +++ b/homeassistant/components/panasonic_bluray/manifest.json @@ -1,7 +1,7 @@ { "domain": "panasonic_bluray", "name": "Panasonic bluray", - "documentation": "https://www.home-assistant.io/components/panasonic_bluray", + "documentation": "https://www.home-assistant.io/integrations/panasonic_bluray", "requirements": [ "panacotta==0.1" ], diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index 432e729ef20a0c..e7e06479eec72a 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -1,7 +1,7 @@ { "domain": "panasonic_viera", "name": "Panasonic viera", - "documentation": "https://www.home-assistant.io/components/panasonic_viera", + "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", "requirements": [ "panasonic_viera==0.3.2", "wakeonlan==1.1.6" diff --git a/homeassistant/components/pandora/manifest.json b/homeassistant/components/pandora/manifest.json index 68e8337a33db05..a15267b7d85ef6 100644 --- a/homeassistant/components/pandora/manifest.json +++ b/homeassistant/components/pandora/manifest.json @@ -1,7 +1,7 @@ { "domain": "pandora", "name": "Pandora", - "documentation": "https://www.home-assistant.io/components/pandora", + "documentation": "https://www.home-assistant.io/integrations/pandora", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/panel_custom/manifest.json b/homeassistant/components/panel_custom/manifest.json index 06c9338742cb0a..5d0cc555705774 100644 --- a/homeassistant/components/panel_custom/manifest.json +++ b/homeassistant/components/panel_custom/manifest.json @@ -1,7 +1,7 @@ { "domain": "panel_custom", "name": "Panel custom", - "documentation": "https://www.home-assistant.io/components/panel_custom", + "documentation": "https://www.home-assistant.io/integrations/panel_custom", "requirements": [], "dependencies": [ "frontend" diff --git a/homeassistant/components/panel_iframe/manifest.json b/homeassistant/components/panel_iframe/manifest.json index e66f94bdcc20f3..6d7c66f6097f9e 100644 --- a/homeassistant/components/panel_iframe/manifest.json +++ b/homeassistant/components/panel_iframe/manifest.json @@ -1,7 +1,7 @@ { "domain": "panel_iframe", "name": "Panel iframe", - "documentation": "https://www.home-assistant.io/components/panel_iframe", + "documentation": "https://www.home-assistant.io/integrations/panel_iframe", "requirements": [], "dependencies": [ "frontend" diff --git a/homeassistant/components/pencom/manifest.json b/homeassistant/components/pencom/manifest.json index 186e071d25b55d..d31b9a811cfc13 100644 --- a/homeassistant/components/pencom/manifest.json +++ b/homeassistant/components/pencom/manifest.json @@ -1,7 +1,7 @@ { "domain": "pencom", "name": "Pencom", - "documentation": "https://www.home-assistant.io/components/pencom", + "documentation": "https://www.home-assistant.io/integrations/pencom", "requirements": [ "pencompy==0.0.3" ], diff --git a/homeassistant/components/persistent_notification/manifest.json b/homeassistant/components/persistent_notification/manifest.json index 8bc343e1f0876a..9ee9692c65530c 100644 --- a/homeassistant/components/persistent_notification/manifest.json +++ b/homeassistant/components/persistent_notification/manifest.json @@ -1,7 +1,7 @@ { "domain": "persistent_notification", "name": "Persistent notification", - "documentation": "https://www.home-assistant.io/components/persistent_notification", + "documentation": "https://www.home-assistant.io/integrations/persistent_notification", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index d2cba929259248..cf50b8029c2e91 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -1,7 +1,7 @@ { "domain": "person", "name": "Person", - "documentation": "https://www.home-assistant.io/components/person", + "documentation": "https://www.home-assistant.io/integrations/person", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 0b1579a139df40..4845aa16a37576 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -1,7 +1,7 @@ { "domain": "philips_js", "name": "Philips js", - "documentation": "https://www.home-assistant.io/components/philips_js", + "documentation": "https://www.home-assistant.io/integrations/philips_js", "requirements": [ "ha-philipsjs==0.0.8" ], diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 7fe8bba6873913..089e1e60a11d81 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -1,7 +1,7 @@ { "domain": "pi_hole", "name": "Pi hole", - "documentation": "https://www.home-assistant.io/components/pi_hole", + "documentation": "https://www.home-assistant.io/integrations/pi_hole", "requirements": [ "hole==0.5.0" ], diff --git a/homeassistant/components/picotts/manifest.json b/homeassistant/components/picotts/manifest.json index bfe7f449ca073b..5150ddd0404dae 100644 --- a/homeassistant/components/picotts/manifest.json +++ b/homeassistant/components/picotts/manifest.json @@ -1,7 +1,7 @@ { "domain": "picotts", "name": "Picotts", - "documentation": "https://www.home-assistant.io/components/picotts", + "documentation": "https://www.home-assistant.io/integrations/picotts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/piglow/manifest.json b/homeassistant/components/piglow/manifest.json index 67b1033c51ea42..012ce4f014ee7e 100644 --- a/homeassistant/components/piglow/manifest.json +++ b/homeassistant/components/piglow/manifest.json @@ -1,7 +1,7 @@ { "domain": "piglow", "name": "Piglow", - "documentation": "https://www.home-assistant.io/components/piglow", + "documentation": "https://www.home-assistant.io/integrations/piglow", "requirements": [ "piglow==1.2.4" ], diff --git a/homeassistant/components/pilight/manifest.json b/homeassistant/components/pilight/manifest.json index dfe4952e1a17fb..7613f9e2a3477b 100644 --- a/homeassistant/components/pilight/manifest.json +++ b/homeassistant/components/pilight/manifest.json @@ -1,7 +1,7 @@ { "domain": "pilight", "name": "Pilight", - "documentation": "https://www.home-assistant.io/components/pilight", + "documentation": "https://www.home-assistant.io/integrations/pilight", "requirements": [ "pilight==0.1.1" ], diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index d98adef87a7bd4..19e84365fac19b 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -1,7 +1,7 @@ { "domain": "ping", "name": "Ping", - "documentation": "https://www.home-assistant.io/components/ping", + "documentation": "https://www.home-assistant.io/integrations/ping", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/pioneer/manifest.json b/homeassistant/components/pioneer/manifest.json index b06874149ed17e..3aa046f234d6b2 100644 --- a/homeassistant/components/pioneer/manifest.json +++ b/homeassistant/components/pioneer/manifest.json @@ -1,7 +1,7 @@ { "domain": "pioneer", "name": "Pioneer", - "documentation": "https://www.home-assistant.io/components/pioneer", + "documentation": "https://www.home-assistant.io/integrations/pioneer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/pjlink/manifest.json b/homeassistant/components/pjlink/manifest.json index 6901847bd8d0bd..3c2b67e3425693 100644 --- a/homeassistant/components/pjlink/manifest.json +++ b/homeassistant/components/pjlink/manifest.json @@ -1,7 +1,7 @@ { "domain": "pjlink", "name": "Pjlink", - "documentation": "https://www.home-assistant.io/components/pjlink", + "documentation": "https://www.home-assistant.io/integrations/pjlink", "requirements": [ "pypjlink2==1.2.0" ], diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index cd6111ba9da789..658173d3db2eaf 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -2,7 +2,7 @@ "domain": "plaato", "name": "Plaato Airlock", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/plaato", + "documentation": "https://www.home-assistant.io/integrations/plaato", "dependencies": ["webhook"], "codeowners": ["@JohNan"], "requirements": [] diff --git a/homeassistant/components/plant/manifest.json b/homeassistant/components/plant/manifest.json index cbde894173b7cf..721a57e782248a 100644 --- a/homeassistant/components/plant/manifest.json +++ b/homeassistant/components/plant/manifest.json @@ -1,7 +1,7 @@ { "domain": "plant", "name": "Plant", - "documentation": "https://www.home-assistant.io/components/plant", + "documentation": "https://www.home-assistant.io/integrations/plant", "requirements": [], "dependencies": [ "group", diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 8e068c909c5d5e..d4f2ae0517a296 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -2,7 +2,7 @@ "domain": "plex", "name": "Plex", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/plex", + "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ "plexapi==3.0.6", "plexauth==0.0.4" diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index c399232f3151b1..1069c0bcdf02bd 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -1,7 +1,7 @@ { "domain": "plugwise", "name": "Plugwise", - "documentation": "https://www.home-assistant.io/components/plugwise", + "documentation": "https://www.home-assistant.io/integrations/plugwise", "dependencies": [], "codeowners": ["@laetificat","@CoMPaTech"], "requirements": ["haanna==0.10.1"] diff --git a/homeassistant/components/plum_lightpad/manifest.json b/homeassistant/components/plum_lightpad/manifest.json index 389eca09c4238d..8c2da09026977b 100644 --- a/homeassistant/components/plum_lightpad/manifest.json +++ b/homeassistant/components/plum_lightpad/manifest.json @@ -1,7 +1,7 @@ { "domain": "plum_lightpad", "name": "Plum lightpad", - "documentation": "https://www.home-assistant.io/components/plum_lightpad", + "documentation": "https://www.home-assistant.io/integrations/plum_lightpad", "requirements": [ "plumlightpad==0.0.11" ], diff --git a/homeassistant/components/pocketcasts/manifest.json b/homeassistant/components/pocketcasts/manifest.json index 11c202363246a7..f72984f9fc0e38 100644 --- a/homeassistant/components/pocketcasts/manifest.json +++ b/homeassistant/components/pocketcasts/manifest.json @@ -1,7 +1,7 @@ { "domain": "pocketcasts", "name": "Pocketcasts", - "documentation": "https://www.home-assistant.io/components/pocketcasts", + "documentation": "https://www.home-assistant.io/integrations/pocketcasts", "requirements": [ "pocketcasts==0.1" ], diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index fcc9265ce9b4fe..0f2faef86dfaed 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -2,7 +2,7 @@ "domain": "point", "name": "Point", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/point", + "documentation": "https://www.home-assistant.io/integrations/point", "requirements": [ "pypoint==1.1.1" ], diff --git a/homeassistant/components/postnl/manifest.json b/homeassistant/components/postnl/manifest.json index 9746cb168aa137..d07f9746ee8f28 100644 --- a/homeassistant/components/postnl/manifest.json +++ b/homeassistant/components/postnl/manifest.json @@ -1,7 +1,7 @@ { "domain": "postnl", "name": "Postnl", - "documentation": "https://www.home-assistant.io/components/postnl", + "documentation": "https://www.home-assistant.io/integrations/postnl", "requirements": [ "postnl_api==1.0.2" ], diff --git a/homeassistant/components/prezzibenzina/manifest.json b/homeassistant/components/prezzibenzina/manifest.json index 2427ebbfdb0508..99a8a1e27019f8 100644 --- a/homeassistant/components/prezzibenzina/manifest.json +++ b/homeassistant/components/prezzibenzina/manifest.json @@ -1,7 +1,7 @@ { "domain": "prezzibenzina", "name": "Prezzibenzina", - "documentation": "https://www.home-assistant.io/components/prezzibenzina", + "documentation": "https://www.home-assistant.io/integrations/prezzibenzina", "requirements": [ "prezzibenzina-py==1.1.4" ], diff --git a/homeassistant/components/proliphix/manifest.json b/homeassistant/components/proliphix/manifest.json index 3aa356823c1823..a035657a090248 100644 --- a/homeassistant/components/proliphix/manifest.json +++ b/homeassistant/components/proliphix/manifest.json @@ -1,7 +1,7 @@ { "domain": "proliphix", "name": "Proliphix", - "documentation": "https://www.home-assistant.io/components/proliphix", + "documentation": "https://www.home-assistant.io/integrations/proliphix", "requirements": [ "proliphix==0.4.1" ], diff --git a/homeassistant/components/prometheus/manifest.json b/homeassistant/components/prometheus/manifest.json index cab1228aa5682e..309284e5f62b16 100644 --- a/homeassistant/components/prometheus/manifest.json +++ b/homeassistant/components/prometheus/manifest.json @@ -1,7 +1,7 @@ { "domain": "prometheus", "name": "Prometheus", - "documentation": "https://www.home-assistant.io/components/prometheus", + "documentation": "https://www.home-assistant.io/integrations/prometheus", "requirements": [ "prometheus_client==0.7.1" ], diff --git a/homeassistant/components/prowl/manifest.json b/homeassistant/components/prowl/manifest.json index a8b4893c995a29..73aa818a2addc5 100644 --- a/homeassistant/components/prowl/manifest.json +++ b/homeassistant/components/prowl/manifest.json @@ -1,7 +1,7 @@ { "domain": "prowl", "name": "Prowl", - "documentation": "https://www.home-assistant.io/components/prowl", + "documentation": "https://www.home-assistant.io/integrations/prowl", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/proximity/manifest.json b/homeassistant/components/proximity/manifest.json index 335bea82fc91d8..708a3fb2616ed9 100644 --- a/homeassistant/components/proximity/manifest.json +++ b/homeassistant/components/proximity/manifest.json @@ -1,7 +1,7 @@ { "domain": "proximity", "name": "Proximity", - "documentation": "https://www.home-assistant.io/components/proximity", + "documentation": "https://www.home-assistant.io/integrations/proximity", "requirements": [], "dependencies": [ "device_tracker", diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 54d5ebe5f14bdc..c67fd4afc090c5 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -1,7 +1,7 @@ { "domain": "proxy", "name": "Proxy", - "documentation": "https://www.home-assistant.io/components/proxy", + "documentation": "https://www.home-assistant.io/integrations/proxy", "requirements": [ "pillow==6.1.0" ], diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 797a5432f9722b..98a14d877e8f72 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -2,7 +2,7 @@ "domain": "ps4", "name": "Ps4", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/ps4", + "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": [ "pyps4-homeassistant==0.8.7" ], diff --git a/homeassistant/components/ptvsd/manifest.json b/homeassistant/components/ptvsd/manifest.json index 8bd46c3dc32f1b..2f8398531c729b 100644 --- a/homeassistant/components/ptvsd/manifest.json +++ b/homeassistant/components/ptvsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "ptvsd", "name": "ptvsd", - "documentation": "https://www.home-assistant.io/components/ptvsd", + "documentation": "https://www.home-assistant.io/integrations/ptvsd", "requirements": [ "ptvsd==4.2.8" ], diff --git a/homeassistant/components/pulseaudio_loopback/manifest.json b/homeassistant/components/pulseaudio_loopback/manifest.json index 58a2871e027939..19247e1a7d5b80 100644 --- a/homeassistant/components/pulseaudio_loopback/manifest.json +++ b/homeassistant/components/pulseaudio_loopback/manifest.json @@ -1,7 +1,7 @@ { "domain": "pulseaudio_loopback", "name": "Pulseaudio loopback", - "documentation": "https://www.home-assistant.io/components/pulseaudio_loopback", + "documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/push/manifest.json b/homeassistant/components/push/manifest.json index 278638caff8841..161ea29b3b3393 100644 --- a/homeassistant/components/push/manifest.json +++ b/homeassistant/components/push/manifest.json @@ -1,7 +1,7 @@ { "domain": "push", "name": "Push", - "documentation": "https://www.home-assistant.io/components/push", + "documentation": "https://www.home-assistant.io/integrations/push", "requirements": [], "dependencies": ["webhook"], "codeowners": [ diff --git a/homeassistant/components/pushbullet/manifest.json b/homeassistant/components/pushbullet/manifest.json index 51e77959d7ad9c..f649e5467a4580 100644 --- a/homeassistant/components/pushbullet/manifest.json +++ b/homeassistant/components/pushbullet/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushbullet", "name": "Pushbullet", - "documentation": "https://www.home-assistant.io/components/pushbullet", + "documentation": "https://www.home-assistant.io/integrations/pushbullet", "requirements": [ "pushbullet.py==0.11.0" ], diff --git a/homeassistant/components/pushetta/manifest.json b/homeassistant/components/pushetta/manifest.json index b42180c7268035..3000f9fd17bc25 100644 --- a/homeassistant/components/pushetta/manifest.json +++ b/homeassistant/components/pushetta/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushetta", "name": "Pushetta", - "documentation": "https://www.home-assistant.io/components/pushetta", + "documentation": "https://www.home-assistant.io/integrations/pushetta", "requirements": [ "pushetta==1.0.15" ], diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 1cdbb4ff48b012..01c3fc270fbe5c 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushover", "name": "Pushover", - "documentation": "https://www.home-assistant.io/components/pushover", + "documentation": "https://www.home-assistant.io/integrations/pushover", "requirements": [ "python-pushover==0.4" ], diff --git a/homeassistant/components/pushsafer/manifest.json b/homeassistant/components/pushsafer/manifest.json index 300d0ead4a5c3a..18592124c24d4b 100644 --- a/homeassistant/components/pushsafer/manifest.json +++ b/homeassistant/components/pushsafer/manifest.json @@ -1,7 +1,7 @@ { "domain": "pushsafer", "name": "Pushsafer", - "documentation": "https://www.home-assistant.io/components/pushsafer", + "documentation": "https://www.home-assistant.io/integrations/pushsafer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index b61c7100828f09..7e6879de9a1437 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -1,7 +1,7 @@ { "domain": "pvoutput", "name": "Pvoutput", - "documentation": "https://www.home-assistant.io/components/pvoutput", + "documentation": "https://www.home-assistant.io/integrations/pvoutput", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 437bd3bc4d2ce1..4cbcd8321b597b 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -1,7 +1,7 @@ { "domain": "pyload", "name": "Pyload", - "documentation": "https://www.home-assistant.io/components/pyload", + "documentation": "https://www.home-assistant.io/integrations/pyload", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 83d70830b11a43..2e5c12082100f8 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -1,7 +1,7 @@ { "domain": "python_script", "name": "Python script", - "documentation": "https://www.home-assistant.io/components/python_script", + "documentation": "https://www.home-assistant.io/integrations/python_script", "requirements": [ "restrictedpython==5.0" ], diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index 5fb850739d8d0d..c41d4ba46d362f 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -1,7 +1,7 @@ { "domain": "qbittorrent", "name": "Qbittorrent", - "documentation": "https://www.home-assistant.io/components/qbittorrent", + "documentation": "https://www.home-assistant.io/integrations/qbittorrent", "requirements": [ "python-qbittorrent==0.3.1" ], diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json index 47a4a4b5f85ce1..c113e6034cd904 100644 --- a/homeassistant/components/qld_bushfire/manifest.json +++ b/homeassistant/components/qld_bushfire/manifest.json @@ -1,7 +1,7 @@ { "domain": "qld_bushfire", "name": "Queensland Bushfire Alert", - "documentation": "https://www.home-assistant.io/components/qld_bushfire", + "documentation": "https://www.home-assistant.io/integrations/qld_bushfire", "requirements": [ "georss_qld_bushfire_alert_client==0.3" ], diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index f02d416c7e6be9..a34c49645ceae8 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -1,7 +1,7 @@ { "domain": "qnap", "name": "Qnap", - "documentation": "https://www.home-assistant.io/components/qnap", + "documentation": "https://www.home-assistant.io/integrations/qnap", "requirements": [ "qnapstats==0.2.7" ], diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index eb8da25bace095..87e16f629876e8 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -1,7 +1,7 @@ { "domain": "qrcode", "name": "Qrcode", - "documentation": "https://www.home-assistant.io/components/qrcode", + "documentation": "https://www.home-assistant.io/integrations/qrcode", "requirements": [ "pillow==6.1.0", "pyzbar==0.1.7" diff --git a/homeassistant/components/quantum_gateway/manifest.json b/homeassistant/components/quantum_gateway/manifest.json index 9c062482a4c2f2..da2fc20510fee4 100644 --- a/homeassistant/components/quantum_gateway/manifest.json +++ b/homeassistant/components/quantum_gateway/manifest.json @@ -1,7 +1,7 @@ { "domain": "quantum_gateway", "name": "Quantum gateway", - "documentation": "https://www.home-assistant.io/components/quantum_gateway", + "documentation": "https://www.home-assistant.io/integrations/quantum_gateway", "requirements": [ "quantum-gateway==0.0.5" ], diff --git a/homeassistant/components/qwikswitch/manifest.json b/homeassistant/components/qwikswitch/manifest.json index 4907cb462b6f1a..6d2944282ba550 100644 --- a/homeassistant/components/qwikswitch/manifest.json +++ b/homeassistant/components/qwikswitch/manifest.json @@ -1,7 +1,7 @@ { "domain": "qwikswitch", "name": "Qwikswitch", - "documentation": "https://www.home-assistant.io/components/qwikswitch", + "documentation": "https://www.home-assistant.io/integrations/qwikswitch", "requirements": [ "pyqwikswitch==0.93" ], diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index 30bde9a297d396..79e3677d65e821 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -1,7 +1,7 @@ { "domain": "rachio", "name": "Rachio", - "documentation": "https://www.home-assistant.io/components/rachio", + "documentation": "https://www.home-assistant.io/integrations/rachio", "requirements": [ "rachiopy==0.1.3" ], diff --git a/homeassistant/components/radarr/manifest.json b/homeassistant/components/radarr/manifest.json index f12fcf4220cfe9..2683525e7b44fd 100644 --- a/homeassistant/components/radarr/manifest.json +++ b/homeassistant/components/radarr/manifest.json @@ -1,7 +1,7 @@ { "domain": "radarr", "name": "Radarr", - "documentation": "https://www.home-assistant.io/components/radarr", + "documentation": "https://www.home-assistant.io/integrations/radarr", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index 002fdb632739c0..c3f8079cccdf43 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -1,7 +1,7 @@ { "domain": "radiotherm", "name": "Radiotherm", - "documentation": "https://www.home-assistant.io/components/radiotherm", + "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": [ "radiotherm==2.0.0" ], diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index b911aaa57e19b5..bb421c725ca69f 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -1,7 +1,7 @@ { "domain": "rainbird", "name": "Rainbird", - "documentation": "https://www.home-assistant.io/components/rainbird", + "documentation": "https://www.home-assistant.io/integrations/rainbird", "requirements": [ "pyrainbird==0.4.1" ], diff --git a/homeassistant/components/raincloud/manifest.json b/homeassistant/components/raincloud/manifest.json index 4d07f2a3ce4c6e..612fc32c01498d 100644 --- a/homeassistant/components/raincloud/manifest.json +++ b/homeassistant/components/raincloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "raincloud", "name": "Raincloud", - "documentation": "https://www.home-assistant.io/components/raincloud", + "documentation": "https://www.home-assistant.io/integrations/raincloud", "requirements": [ "raincloudy==0.0.7" ], diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index 354eaf8b313105..c5503976d3fbde 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -1,7 +1,7 @@ { "domain": "rainforest_eagle", "name": "Rainforest Eagle-200", - "documentation": "https://www.home-assistant.io/components/rainforest_eagle", + "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", "requirements": [ "eagle200_reader==0.2.1" ], diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 25b36c798c593d..37db2b31355261 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -2,7 +2,7 @@ "domain": "rainmachine", "name": "Rainmachine", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/rainmachine", + "documentation": "https://www.home-assistant.io/integrations/rainmachine", "requirements": [ "regenmaschine==1.5.1" ], diff --git a/homeassistant/components/random/manifest.json b/homeassistant/components/random/manifest.json index c184f35734c530..cbbaa562abde07 100644 --- a/homeassistant/components/random/manifest.json +++ b/homeassistant/components/random/manifest.json @@ -1,7 +1,7 @@ { "domain": "random", "name": "Random", - "documentation": "https://www.home-assistant.io/components/random", + "documentation": "https://www.home-assistant.io/integrations/random", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/raspihats/manifest.json b/homeassistant/components/raspihats/manifest.json index 8f5040152a2caf..b241cd6db15eeb 100644 --- a/homeassistant/components/raspihats/manifest.json +++ b/homeassistant/components/raspihats/manifest.json @@ -1,7 +1,7 @@ { "domain": "raspihats", "name": "Raspihats", - "documentation": "https://www.home-assistant.io/components/raspihats", + "documentation": "https://www.home-assistant.io/integrations/raspihats", "requirements": [ "raspihats==2.2.3", "smbus-cffi==0.5.1" diff --git a/homeassistant/components/raspyrfm/manifest.json b/homeassistant/components/raspyrfm/manifest.json index fee815a7e6b17d..f1330c2abe56f2 100644 --- a/homeassistant/components/raspyrfm/manifest.json +++ b/homeassistant/components/raspyrfm/manifest.json @@ -1,7 +1,7 @@ { "domain": "raspyrfm", "name": "Raspyrfm", - "documentation": "https://www.home-assistant.io/components/raspyrfm", + "documentation": "https://www.home-assistant.io/integrations/raspyrfm", "requirements": [ "raspyrfm-client==1.2.8" ], diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index 2cccf32f298608..396afe30dcf074 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -1,7 +1,7 @@ { "domain": "recollect_waste", "name": "Recollect waste", - "documentation": "https://www.home-assistant.io/components/recollect_waste", + "documentation": "https://www.home-assistant.io/integrations/recollect_waste", "requirements": [ "recollect-waste==1.0.1" ], diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 9ecfa88053fa69..cdb09d66067170 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -1,7 +1,7 @@ { "domain": "recorder", "name": "Recorder", - "documentation": "https://www.home-assistant.io/components/recorder", + "documentation": "https://www.home-assistant.io/integrations/recorder", "requirements": [ "sqlalchemy==1.3.8" ], diff --git a/homeassistant/components/recswitch/manifest.json b/homeassistant/components/recswitch/manifest.json index af8e802c5ec2be..b11eca2b08822a 100644 --- a/homeassistant/components/recswitch/manifest.json +++ b/homeassistant/components/recswitch/manifest.json @@ -1,7 +1,7 @@ { "domain": "recswitch", "name": "Recswitch", - "documentation": "https://www.home-assistant.io/components/recswitch", + "documentation": "https://www.home-assistant.io/integrations/recswitch", "requirements": [ "pyrecswitch==1.0.2" ], diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index c6d3b3458e5287..55baecc486cdcb 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -1,7 +1,7 @@ { "domain": "reddit", "name": "Reddit", - "documentation": "https://www.home-assistant.io/components/reddit", + "documentation": "https://www.home-assistant.io/integrations/reddit", "requirements": [ "praw==6.3.1" ], diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json index 7256239933006f..f1f8299918824f 100644 --- a/homeassistant/components/rejseplanen/manifest.json +++ b/homeassistant/components/rejseplanen/manifest.json @@ -1,7 +1,7 @@ { "domain": "rejseplanen", "name": "Rejseplanen", - "documentation": "https://www.home-assistant.io/components/rejseplanen", + "documentation": "https://www.home-assistant.io/integrations/rejseplanen", "requirements": [ "rjpl==0.3.5" ], diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index c9d35e9d2c9d7d..4979fe29e0eb74 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -1,7 +1,7 @@ { "domain": "remember_the_milk", "name": "Remember the milk", - "documentation": "https://www.home-assistant.io/components/remember_the_milk", + "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", "requirements": [ "RtmAPI==0.7.0", "httplib2==0.10.3" diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index 5fe585dcd8346d..5b9e70ebcf0cf6 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -1,7 +1,7 @@ { "domain": "remote", "name": "Remote", - "documentation": "https://www.home-assistant.io/components/remote", + "documentation": "https://www.home-assistant.io/integrations/remote", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json index f15defd63dcf3e..df9a9c7512358c 100644 --- a/homeassistant/components/remote_rpi_gpio/manifest.json +++ b/homeassistant/components/remote_rpi_gpio/manifest.json @@ -1,7 +1,7 @@ { "domain": "remote_rpi_gpio", "name": "remote_rpi_gpio", - "documentation": "https://www.home-assistant.io/components/remote_rpi_gpio", + "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio", "requirements": [ "gpiozero==1.4.1" ], diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json index 14af98cfb641e6..a894285f729127 100644 --- a/homeassistant/components/repetier/manifest.json +++ b/homeassistant/components/repetier/manifest.json @@ -1,7 +1,7 @@ { "domain": "repetier", "name": "Repetier Server", - "documentation": "https://www.home-assistant.io/components/repetier", + "documentation": "https://www.home-assistant.io/integrations/repetier", "requirements": [ "pyrepetier==3.0.5" ], diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json index 999f57407151d6..0b197d104eda0f 100644 --- a/homeassistant/components/rest/manifest.json +++ b/homeassistant/components/rest/manifest.json @@ -1,7 +1,7 @@ { "domain": "rest", "name": "Rest", - "documentation": "https://www.home-assistant.io/components/rest", + "documentation": "https://www.home-assistant.io/integrations/rest", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rest_command/manifest.json b/homeassistant/components/rest_command/manifest.json index ced930fc64f5e2..238191a9343a14 100644 --- a/homeassistant/components/rest_command/manifest.json +++ b/homeassistant/components/rest_command/manifest.json @@ -1,7 +1,7 @@ { "domain": "rest_command", "name": "Rest command", - "documentation": "https://www.home-assistant.io/components/rest_command", + "documentation": "https://www.home-assistant.io/integrations/rest_command", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index bbdb49ad401240..bda260bdff2230 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -1,7 +1,7 @@ { "domain": "rflink", "name": "Rflink", - "documentation": "https://www.home-assistant.io/components/rflink", + "documentation": "https://www.home-assistant.io/integrations/rflink", "requirements": [ "rflink==0.0.46" ], diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index 5d6cd4b038cbb8..a75a8ba9eb1d0c 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -1,7 +1,7 @@ { "domain": "rfxtrx", "name": "Rfxtrx", - "documentation": "https://www.home-assistant.io/components/rfxtrx", + "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "requirements": [ "pyRFXtrx==0.23" ], diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 9dbedad1ffc136..47fc2f3a6a8bbd 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -1,7 +1,7 @@ { "domain": "ring", "name": "Ring", - "documentation": "https://www.home-assistant.io/components/ring", + "documentation": "https://www.home-assistant.io/integrations/ring", "requirements": [ "ring_doorbell==0.2.3" ], diff --git a/homeassistant/components/ripple/manifest.json b/homeassistant/components/ripple/manifest.json index fe93bf02445dce..b8aa5c74302428 100644 --- a/homeassistant/components/ripple/manifest.json +++ b/homeassistant/components/ripple/manifest.json @@ -1,7 +1,7 @@ { "domain": "ripple", "name": "Ripple", - "documentation": "https://www.home-assistant.io/components/ripple", + "documentation": "https://www.home-assistant.io/integrations/ripple", "requirements": [ "python-ripple-api==0.0.3" ], diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 3f32a61c081fc5..1f06daf0623f19 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -1,7 +1,7 @@ { "domain": "rmvtransport", "name": "Rmvtransport", - "documentation": "https://www.home-assistant.io/components/rmvtransport", + "documentation": "https://www.home-assistant.io/integrations/rmvtransport", "requirements": [ "PyRMVtransport==0.1.3" ], diff --git a/homeassistant/components/rocketchat/manifest.json b/homeassistant/components/rocketchat/manifest.json index 3a8959f1be61b5..924a9b86d47a9a 100644 --- a/homeassistant/components/rocketchat/manifest.json +++ b/homeassistant/components/rocketchat/manifest.json @@ -1,7 +1,7 @@ { "domain": "rocketchat", "name": "Rocketchat", - "documentation": "https://www.home-assistant.io/components/rocketchat", + "documentation": "https://www.home-assistant.io/integrations/rocketchat", "requirements": [ "rocketchat-API==0.6.1" ], diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 477bcb105f7f5b..f2639b31d1583a 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -1,7 +1,7 @@ { "domain": "roku", "name": "Roku", - "documentation": "https://www.home-assistant.io/components/roku", + "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": [ "roku==3.1" ], diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 058ad0c5e815ac..5064357a7df48a 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -1,7 +1,7 @@ { "domain": "roomba", "name": "Roomba", - "documentation": "https://www.home-assistant.io/components/roomba", + "documentation": "https://www.home-assistant.io/integrations/roomba", "requirements": [ "roombapy==1.3.1" ], diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 7ac91964a98720..34a296b0f9dece 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -1,7 +1,7 @@ { "domain": "route53", "name": "Route53", - "documentation": "https://www.home-assistant.io/components/route53", + "documentation": "https://www.home-assistant.io/integrations/route53", "requirements": [ "boto3==1.9.233", "ipify==1.0.0" diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index 71ec8fcbc9b3ab..e033e82d922029 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -1,7 +1,7 @@ { "domain": "rova", "name": "Rova", - "documentation": "https://www.home-assistant.io/components/rova", + "documentation": "https://www.home-assistant.io/integrations/rova", "requirements": [ "rova==0.1.0" ], diff --git a/homeassistant/components/rpi_camera/manifest.json b/homeassistant/components/rpi_camera/manifest.json index 1f905b103fed24..d5443787d39d53 100644 --- a/homeassistant/components/rpi_camera/manifest.json +++ b/homeassistant/components/rpi_camera/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_camera", "name": "Rpi camera", - "documentation": "https://www.home-assistant.io/components/rpi_camera", + "documentation": "https://www.home-assistant.io/integrations/rpi_camera", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index 88322708b27738..0bee2baeddf741 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_gpio", "name": "Rpi gpio", - "documentation": "https://www.home-assistant.io/components/rpi_gpio", + "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", "requirements": [ "RPi.GPIO==0.6.5" ], diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json index d2ed380d68ad0b..ccff3d3357fdb0 100644 --- a/homeassistant/components/rpi_gpio_pwm/manifest.json +++ b/homeassistant/components/rpi_gpio_pwm/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_gpio_pwm", "name": "Rpi gpio pwm", - "documentation": "https://www.home-assistant.io/components/rpi_gpio_pwm", + "documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm", "requirements": [ "pwmled==1.4.1" ], diff --git a/homeassistant/components/rpi_pfio/manifest.json b/homeassistant/components/rpi_pfio/manifest.json index 7fc724bf90a3ef..fd0a0a99f58d49 100644 --- a/homeassistant/components/rpi_pfio/manifest.json +++ b/homeassistant/components/rpi_pfio/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_pfio", "name": "Rpi pfio", - "documentation": "https://www.home-assistant.io/components/rpi_pfio", + "documentation": "https://www.home-assistant.io/integrations/rpi_pfio", "requirements": [ "pifacecommon==4.2.2", "pifacedigitalio==3.0.5" diff --git a/homeassistant/components/rpi_rf/manifest.json b/homeassistant/components/rpi_rf/manifest.json index e5fffee131e337..5914f65ef6926d 100644 --- a/homeassistant/components/rpi_rf/manifest.json +++ b/homeassistant/components/rpi_rf/manifest.json @@ -1,7 +1,7 @@ { "domain": "rpi_rf", "name": "Rpi rf", - "documentation": "https://www.home-assistant.io/components/rpi_rf", + "documentation": "https://www.home-assistant.io/integrations/rpi_rf", "requirements": [ "rpi-rf==0.9.7" ], diff --git a/homeassistant/components/rss_feed_template/manifest.json b/homeassistant/components/rss_feed_template/manifest.json index c92f6b2a0bad3a..7bd928764561a1 100644 --- a/homeassistant/components/rss_feed_template/manifest.json +++ b/homeassistant/components/rss_feed_template/manifest.json @@ -1,7 +1,7 @@ { "domain": "rss_feed_template", "name": "Rss feed template", - "documentation": "https://www.home-assistant.io/components/rss_feed_template", + "documentation": "https://www.home-assistant.io/integrations/rss_feed_template", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/rtorrent/manifest.json b/homeassistant/components/rtorrent/manifest.json index ce2dca9e0853d3..899ee3a8ae81cc 100644 --- a/homeassistant/components/rtorrent/manifest.json +++ b/homeassistant/components/rtorrent/manifest.json @@ -1,7 +1,7 @@ { "domain": "rtorrent", "name": "Rtorrent", - "documentation": "https://www.home-assistant.io/components/rtorrent", + "documentation": "https://www.home-assistant.io/integrations/rtorrent", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 4667e9b8314852..9404153f4e5132 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -1,7 +1,7 @@ { "domain": "russound_rio", "name": "Russound rio", - "documentation": "https://www.home-assistant.io/components/russound_rio", + "documentation": "https://www.home-assistant.io/integrations/russound_rio", "requirements": [ "russound_rio==0.1.7" ], diff --git a/homeassistant/components/russound_rnet/manifest.json b/homeassistant/components/russound_rnet/manifest.json index 716f383040f6f1..687dd05b61df73 100644 --- a/homeassistant/components/russound_rnet/manifest.json +++ b/homeassistant/components/russound_rnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "russound_rnet", "name": "Russound rnet", - "documentation": "https://www.home-assistant.io/components/russound_rnet", + "documentation": "https://www.home-assistant.io/integrations/russound_rnet", "requirements": [ "russound==0.1.9" ], diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 9424e5f3a1a591..e69c227f14884a 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -1,7 +1,7 @@ { "domain": "sabnzbd", "name": "Sabnzbd", - "documentation": "https://www.home-assistant.io/components/sabnzbd", + "documentation": "https://www.home-assistant.io/integrations/sabnzbd", "requirements": [ "pysabnzbd==1.1.0" ], diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index c0367c47902089..e42b37195a42de 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -1,7 +1,7 @@ { "domain": "saj", "name": "SAJ", - "documentation": "https://www.home-assistant.io/components/saj", + "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": [ "pysaj==0.0.9" ], diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index c8825f4ac3ff69..a080fac112a32d 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -1,7 +1,7 @@ { "domain": "samsungtv", "name": "Samsungtv", - "documentation": "https://www.home-assistant.io/components/samsungtv", + "documentation": "https://www.home-assistant.io/integrations/samsungtv", "requirements": [ "samsungctl[websocket]==0.7.1", "wakeonlan==1.1.6" diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index ae56b54ce18bbf..dbbfebeaccbc13 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -1,7 +1,7 @@ { "domain": "satel_integra", "name": "Satel integra", - "documentation": "https://www.home-assistant.io/components/satel_integra", + "documentation": "https://www.home-assistant.io/integrations/satel_integra", "requirements": [ "satel_integra==0.3.4" ], diff --git a/homeassistant/components/scene/manifest.json b/homeassistant/components/scene/manifest.json index e1becfd1936423..b80e4de5041642 100644 --- a/homeassistant/components/scene/manifest.json +++ b/homeassistant/components/scene/manifest.json @@ -1,7 +1,7 @@ { "domain": "scene", "name": "Scene", - "documentation": "https://www.home-assistant.io/components/scene", + "documentation": "https://www.home-assistant.io/integrations/scene", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index ec9807d4e004c7..989070900ca462 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -1,7 +1,7 @@ { "domain": "scrape", "name": "Scrape", - "documentation": "https://www.home-assistant.io/components/scrape", + "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": [ "beautifulsoup4==4.8.0" ], diff --git a/homeassistant/components/script/manifest.json b/homeassistant/components/script/manifest.json index 56a3c39b7b6b76..51ce17c500a041 100644 --- a/homeassistant/components/script/manifest.json +++ b/homeassistant/components/script/manifest.json @@ -1,7 +1,7 @@ { "domain": "script", "name": "Script", - "documentation": "https://www.home-assistant.io/components/script", + "documentation": "https://www.home-assistant.io/integrations/script", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/scsgate/manifest.json b/homeassistant/components/scsgate/manifest.json index d565a5d336d5d3..b6334272f23851 100644 --- a/homeassistant/components/scsgate/manifest.json +++ b/homeassistant/components/scsgate/manifest.json @@ -1,7 +1,7 @@ { "domain": "scsgate", "name": "Scsgate", - "documentation": "https://www.home-assistant.io/components/scsgate", + "documentation": "https://www.home-assistant.io/integrations/scsgate", "requirements": [ "scsgate==0.1.0" ], diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json index 74bdb3d8b883a9..528e9ef35f1f12 100644 --- a/homeassistant/components/season/manifest.json +++ b/homeassistant/components/season/manifest.json @@ -1,7 +1,7 @@ { "domain": "season", "name": "Season", - "documentation": "https://www.home-assistant.io/components/season", + "documentation": "https://www.home-assistant.io/integrations/season", "requirements": [ "ephem==3.7.6.0" ], diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index 1ffbe69888f329..f9bd8cc31b4d5b 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -1,7 +1,7 @@ { "domain": "sendgrid", "name": "Sendgrid", - "documentation": "https://www.home-assistant.io/components/sendgrid", + "documentation": "https://www.home-assistant.io/integrations/sendgrid", "requirements": [ "sendgrid==6.1.0" ], diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 8763234c5ed64d..0112ca28469d66 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -1,7 +1,7 @@ { "domain": "sense", "name": "Sense", - "documentation": "https://www.home-assistant.io/components/sense", + "documentation": "https://www.home-assistant.io/integrations/sense", "requirements": [ "sense_energy==0.7.0" ], diff --git a/homeassistant/components/sensehat/manifest.json b/homeassistant/components/sensehat/manifest.json index cb148c92198875..deec8c23ab7f80 100644 --- a/homeassistant/components/sensehat/manifest.json +++ b/homeassistant/components/sensehat/manifest.json @@ -1,7 +1,7 @@ { "domain": "sensehat", "name": "Sensehat", - "documentation": "https://www.home-assistant.io/components/sensehat", + "documentation": "https://www.home-assistant.io/integrations/sensehat", "requirements": [ "sense-hat==2.2.0" ], diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 776b8444b82276..9b49f442d15560 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -1,7 +1,7 @@ { "domain": "sensibo", "name": "Sensibo", - "documentation": "https://www.home-assistant.io/components/sensibo", + "documentation": "https://www.home-assistant.io/integrations/sensibo", "requirements": [ "pysensibo==1.0.3" ], diff --git a/homeassistant/components/sensor/manifest.json b/homeassistant/components/sensor/manifest.json index 813bcc27acaf5d..1813d782d31a30 100644 --- a/homeassistant/components/sensor/manifest.json +++ b/homeassistant/components/sensor/manifest.json @@ -1,7 +1,7 @@ { "domain": "sensor", "name": "Sensor", - "documentation": "https://www.home-assistant.io/components/sensor", + "documentation": "https://www.home-assistant.io/integrations/sensor", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/serial/manifest.json b/homeassistant/components/serial/manifest.json index 945464dbdec027..61da7b4ff30cf4 100644 --- a/homeassistant/components/serial/manifest.json +++ b/homeassistant/components/serial/manifest.json @@ -1,7 +1,7 @@ { "domain": "serial", "name": "Serial", - "documentation": "https://www.home-assistant.io/components/serial", + "documentation": "https://www.home-assistant.io/integrations/serial", "requirements": [ "pyserial-asyncio==0.4" ], diff --git a/homeassistant/components/serial_pm/manifest.json b/homeassistant/components/serial_pm/manifest.json index b2a645c88f3c87..b326481c55b736 100644 --- a/homeassistant/components/serial_pm/manifest.json +++ b/homeassistant/components/serial_pm/manifest.json @@ -1,7 +1,7 @@ { "domain": "serial_pm", "name": "Serial pm", - "documentation": "https://www.home-assistant.io/components/serial_pm", + "documentation": "https://www.home-assistant.io/integrations/serial_pm", "requirements": [ "pmsensor==0.4" ], diff --git a/homeassistant/components/sesame/manifest.json b/homeassistant/components/sesame/manifest.json index ad6c71bd19fe9a..f689cd46858112 100644 --- a/homeassistant/components/sesame/manifest.json +++ b/homeassistant/components/sesame/manifest.json @@ -1,7 +1,7 @@ { "domain": "sesame", "name": "Sesame Smart Lock", - "documentation": "https://www.home-assistant.io/components/sesame", + "documentation": "https://www.home-assistant.io/integrations/sesame", "requirements": [ "pysesame2==1.0.1" ], diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 45ce2f6a7a00c2..0bf49d4b9066e7 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -1,7 +1,7 @@ { "domain": "seven_segments", "name": "Seven segments", - "documentation": "https://www.home-assistant.io/components/seven_segments", + "documentation": "https://www.home-assistant.io/integrations/seven_segments", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 47b36c122913a2..6a5ac165291ec4 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -1,7 +1,7 @@ { "domain": "seventeentrack", "name": "Seventeentrack", - "documentation": "https://www.home-assistant.io/components/seventeentrack", + "documentation": "https://www.home-assistant.io/integrations/seventeentrack", "requirements": [ "py17track==2.2.2" ], diff --git a/homeassistant/components/shell_command/manifest.json b/homeassistant/components/shell_command/manifest.json index dfe9a8e8e6fb5b..ca354ff2331343 100644 --- a/homeassistant/components/shell_command/manifest.json +++ b/homeassistant/components/shell_command/manifest.json @@ -1,7 +1,7 @@ { "domain": "shell_command", "name": "Shell command", - "documentation": "https://www.home-assistant.io/components/shell_command", + "documentation": "https://www.home-assistant.io/integrations/shell_command", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json index 02718396e5e64c..282d86ce4195c4 100644 --- a/homeassistant/components/shiftr/manifest.json +++ b/homeassistant/components/shiftr/manifest.json @@ -1,7 +1,7 @@ { "domain": "shiftr", "name": "Shiftr", - "documentation": "https://www.home-assistant.io/components/shiftr", + "documentation": "https://www.home-assistant.io/integrations/shiftr", "requirements": [ "paho-mqtt==1.4.0" ], diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index fa704a6550a491..3ef01a44315a01 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -1,7 +1,7 @@ { "domain": "shodan", "name": "Shodan", - "documentation": "https://www.home-assistant.io/components/shodan", + "documentation": "https://www.home-assistant.io/integrations/shodan", "requirements": [ "shodan==1.19.0" ], diff --git a/homeassistant/components/shopping_list/manifest.json b/homeassistant/components/shopping_list/manifest.json index b4ea3c2d38816a..41fe28defaa6c3 100644 --- a/homeassistant/components/shopping_list/manifest.json +++ b/homeassistant/components/shopping_list/manifest.json @@ -1,7 +1,7 @@ { "domain": "shopping_list", "name": "Shopping list", - "documentation": "https://www.home-assistant.io/components/shopping_list", + "documentation": "https://www.home-assistant.io/integrations/shopping_list", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/sht31/manifest.json b/homeassistant/components/sht31/manifest.json index dfa22fc6e23b4a..6ed947e5c82c47 100644 --- a/homeassistant/components/sht31/manifest.json +++ b/homeassistant/components/sht31/manifest.json @@ -1,7 +1,7 @@ { "domain": "sht31", "name": "Sht31", - "documentation": "https://www.home-assistant.io/components/sht31", + "documentation": "https://www.home-assistant.io/integrations/sht31", "requirements": [ "Adafruit-GPIO==1.0.3", "Adafruit-SHT31==1.0.2" diff --git a/homeassistant/components/sigfox/manifest.json b/homeassistant/components/sigfox/manifest.json index 1dc8f5255cea1d..689703302a7953 100644 --- a/homeassistant/components/sigfox/manifest.json +++ b/homeassistant/components/sigfox/manifest.json @@ -1,7 +1,7 @@ { "domain": "sigfox", "name": "Sigfox", - "documentation": "https://www.home-assistant.io/components/sigfox", + "documentation": "https://www.home-assistant.io/integrations/sigfox", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index cbf2833a4f765b..d303ac221dd708 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -1,7 +1,7 @@ { "domain": "simplepush", "name": "Simplepush", - "documentation": "https://www.home-assistant.io/components/simplepush", + "documentation": "https://www.home-assistant.io/integrations/simplepush", "requirements": [ "simplepush==1.1.4" ], diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index cf26955b207b5e..96b337def55a6d 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -2,7 +2,7 @@ "domain": "simplisafe", "name": "Simplisafe", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/simplisafe", + "documentation": "https://www.home-assistant.io/integrations/simplisafe", "requirements": [ "simplisafe-python==5.0.1" ], diff --git a/homeassistant/components/simulated/manifest.json b/homeassistant/components/simulated/manifest.json index b972152aea4c9d..4dcd07157042e4 100644 --- a/homeassistant/components/simulated/manifest.json +++ b/homeassistant/components/simulated/manifest.json @@ -1,7 +1,7 @@ { "domain": "simulated", "name": "Simulated", - "documentation": "https://www.home-assistant.io/components/simulated", + "documentation": "https://www.home-assistant.io/integrations/simulated", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index f8e6e1bf14de9c..d5cfc48815130f 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -1,7 +1,7 @@ { "domain": "sisyphus", "name": "Sisyphus", - "documentation": "https://www.home-assistant.io/components/sisyphus", + "documentation": "https://www.home-assistant.io/integrations/sisyphus", "requirements": [ "sisyphus-control==2.2.1" ], diff --git a/homeassistant/components/sky_hub/manifest.json b/homeassistant/components/sky_hub/manifest.json index 46337918f84bcc..6be45690629415 100644 --- a/homeassistant/components/sky_hub/manifest.json +++ b/homeassistant/components/sky_hub/manifest.json @@ -1,7 +1,7 @@ { "domain": "sky_hub", "name": "Sky hub", - "documentation": "https://www.home-assistant.io/components/sky_hub", + "documentation": "https://www.home-assistant.io/integrations/sky_hub", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/skybeacon/manifest.json b/homeassistant/components/skybeacon/manifest.json index 893a1f3469e4f5..a3cb97cdc2db1f 100644 --- a/homeassistant/components/skybeacon/manifest.json +++ b/homeassistant/components/skybeacon/manifest.json @@ -1,7 +1,7 @@ { "domain": "skybeacon", "name": "Skybeacon", - "documentation": "https://www.home-assistant.io/components/skybeacon", + "documentation": "https://www.home-assistant.io/integrations/skybeacon", "requirements": [ "pygatt[GATTTOOL]==4.0.1" ], diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 843fd3d13b0b9f..8a005be52e29b6 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -1,7 +1,7 @@ { "domain": "skybell", "name": "Skybell", - "documentation": "https://www.home-assistant.io/components/skybell", + "documentation": "https://www.home-assistant.io/integrations/skybell", "requirements": [ "skybellpy==0.4.0" ], diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index 38fa5fa0894e01..10dc4bffccf581 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -1,7 +1,7 @@ { "domain": "slack", "name": "Slack", - "documentation": "https://www.home-assistant.io/components/slack", + "documentation": "https://www.home-assistant.io/integrations/slack", "requirements": [ "slacker==0.13.0" ], diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index ea16d626af4816..ac605f42b0c671 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -1,7 +1,7 @@ { "domain": "sleepiq", "name": "Sleepiq", - "documentation": "https://www.home-assistant.io/components/sleepiq", + "documentation": "https://www.home-assistant.io/integrations/sleepiq", "requirements": [ "sleepyq==0.7" ], diff --git a/homeassistant/components/slide/manifest.json b/homeassistant/components/slide/manifest.json index f9fd7f242b6332..3d2836e1809939 100644 --- a/homeassistant/components/slide/manifest.json +++ b/homeassistant/components/slide/manifest.json @@ -1,7 +1,7 @@ { "domain": "slide", "name": "Slide", - "documentation": "https://www.home-assistant.io/components/slide", + "documentation": "https://www.home-assistant.io/integrations/slide", "requirements": [ "goslide-api==0.5.1" ], diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index ea3a33d55ff818..8f0caa7c548ee0 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -1,7 +1,7 @@ { "domain": "sma", "name": "Sma", - "documentation": "https://www.home-assistant.io/components/sma", + "documentation": "https://www.home-assistant.io/integrations/sma", "requirements": [ "pysma==0.3.4" ], diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index 361802f312e46a..6f5a4b7b64bf8b 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -1,7 +1,7 @@ { "domain": "smappee", "name": "Smappee", - "documentation": "https://www.home-assistant.io/components/smappee", + "documentation": "https://www.home-assistant.io/integrations/smappee", "requirements": [ "smappy==0.2.16" ], diff --git a/homeassistant/components/smarthab/manifest.json b/homeassistant/components/smarthab/manifest.json index 18b587bac9280a..515f3dcbf651fe 100644 --- a/homeassistant/components/smarthab/manifest.json +++ b/homeassistant/components/smarthab/manifest.json @@ -1,7 +1,7 @@ { "domain": "smarthab", "name": "SmartHab", - "documentation": "https://www.home-assistant.io/components/smarthab", + "documentation": "https://www.home-assistant.io/integrations/smarthab", "requirements": [ "smarthab==0.20" ], diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 621da91f4f8c5f..8b5bf65afa1349 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -2,7 +2,7 @@ "domain": "smartthings", "name": "Smartthings", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/smartthings", + "documentation": "https://www.home-assistant.io/integrations/smartthings", "requirements": [ "pysmartapp==0.3.2", "pysmartthings==0.6.9" diff --git a/homeassistant/components/smarty/manifest.json b/homeassistant/components/smarty/manifest.json index b2e3deb4008c5f..003ee804b555ab 100644 --- a/homeassistant/components/smarty/manifest.json +++ b/homeassistant/components/smarty/manifest.json @@ -1,7 +1,7 @@ { "domain": "smarty", "name": "smarty", - "documentation": "https://www.home-assistant.io/components/smarty", + "documentation": "https://www.home-assistant.io/integrations/smarty", "requirements": [ "pysmarty==0.8" ], diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index 421eadca51caaa..b3fdc2825ae23e 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -2,7 +2,7 @@ "domain": "smhi", "name": "Smhi", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/smhi", + "documentation": "https://www.home-assistant.io/integrations/smhi", "requirements": [ "smhi-pkg==1.0.10" ], diff --git a/homeassistant/components/smtp/manifest.json b/homeassistant/components/smtp/manifest.json index 2e1a8d826ce53d..b6ec6b4ca0d4ab 100644 --- a/homeassistant/components/smtp/manifest.json +++ b/homeassistant/components/smtp/manifest.json @@ -1,7 +1,7 @@ { "domain": "smtp", "name": "Smtp", - "documentation": "https://www.home-assistant.io/components/smtp", + "documentation": "https://www.home-assistant.io/integrations/smtp", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json index 0bb36c1317aa79..1314b91f982a47 100644 --- a/homeassistant/components/snapcast/manifest.json +++ b/homeassistant/components/snapcast/manifest.json @@ -1,7 +1,7 @@ { "domain": "snapcast", "name": "Snapcast", - "documentation": "https://www.home-assistant.io/components/snapcast", + "documentation": "https://www.home-assistant.io/integrations/snapcast", "requirements": [ "snapcast==2.0.10" ], diff --git a/homeassistant/components/snips/manifest.json b/homeassistant/components/snips/manifest.json index 58fddb7a3f4ef4..e15d86e811f4ae 100644 --- a/homeassistant/components/snips/manifest.json +++ b/homeassistant/components/snips/manifest.json @@ -1,7 +1,7 @@ { "domain": "snips", "name": "Snips", - "documentation": "https://www.home-assistant.io/components/snips", + "documentation": "https://www.home-assistant.io/integrations/snips", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index a3ac3af985df86..d3942ab4a327f9 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -1,7 +1,7 @@ { "domain": "snmp", "name": "Snmp", - "documentation": "https://www.home-assistant.io/components/snmp", + "documentation": "https://www.home-assistant.io/integrations/snmp", "requirements": [ "pysnmp==4.4.11" ], diff --git a/homeassistant/components/sochain/manifest.json b/homeassistant/components/sochain/manifest.json index 23fad3683cb419..93361d41e2bcdb 100644 --- a/homeassistant/components/sochain/manifest.json +++ b/homeassistant/components/sochain/manifest.json @@ -1,7 +1,7 @@ { "domain": "sochain", "name": "Sochain", - "documentation": "https://www.home-assistant.io/components/sochain", + "documentation": "https://www.home-assistant.io/integrations/sochain", "requirements": [ "python-sochain-api==0.0.2" ], diff --git a/homeassistant/components/socialblade/manifest.json b/homeassistant/components/socialblade/manifest.json index e800bd7266a0ba..c90342018f8d2c 100644 --- a/homeassistant/components/socialblade/manifest.json +++ b/homeassistant/components/socialblade/manifest.json @@ -1,7 +1,7 @@ { "domain": "socialblade", "name": "Socialblade", - "documentation": "https://www.home-assistant.io/components/socialblade", + "documentation": "https://www.home-assistant.io/integrations/socialblade", "requirements": [ "socialbladeclient==0.2" ], diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index 7452790cd6043f..f2635936cb5169 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -1,7 +1,7 @@ { "domain": "solaredge", "name": "Solaredge", - "documentation": "https://www.home-assistant.io/components/solaredge", + "documentation": "https://www.home-assistant.io/integrations/solaredge", "requirements": [ "solaredge==0.0.2", "stringcase==1.2.0" diff --git a/homeassistant/components/solaredge_local/manifest.json b/homeassistant/components/solaredge_local/manifest.json index 291c774c383d56..91ed6f55ee13de 100644 --- a/homeassistant/components/solaredge_local/manifest.json +++ b/homeassistant/components/solaredge_local/manifest.json @@ -1,7 +1,7 @@ { "domain": "solaredge_local", "name": "Solar Edge Local", - "documentation": "https://www.home-assistant.io/components/solaredge_local", + "documentation": "https://www.home-assistant.io/integrations/solaredge_local", "requirements": [ "solaredge-local==0.2.0" ], diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 3a154b857fe878..14ac59d46214a1 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -1,7 +1,7 @@ { "domain": "solax", "name": "Solax Inverter", - "documentation": "https://www.home-assistant.io/components/solax", + "documentation": "https://www.home-assistant.io/integrations/solax", "requirements": [ "solax==0.2.2" ], diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 02eab03c8bb31d..83b50684fda8be 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -2,7 +2,7 @@ "domain": "somfy", "name": "Somfy Open API", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/somfy", + "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": [], "codeowners": [ "@tetienne" diff --git a/homeassistant/components/somfy_mylink/manifest.json b/homeassistant/components/somfy_mylink/manifest.json index d4e799c2cf1ab6..35422cf09a1abb 100644 --- a/homeassistant/components/somfy_mylink/manifest.json +++ b/homeassistant/components/somfy_mylink/manifest.json @@ -1,7 +1,7 @@ { "domain": "somfy_mylink", "name": "Somfy MyLink", - "documentation": "https://www.home-assistant.io/components/somfy_mylink", + "documentation": "https://www.home-assistant.io/integrations/somfy_mylink", "requirements": [ "somfy-mylink-synergy==1.0.6" ], diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index bc0235ec5b3dbc..ae32083da39c8d 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -1,7 +1,7 @@ { "domain": "sonarr", "name": "Sonarr", - "documentation": "https://www.home-assistant.io/components/sonarr", + "documentation": "https://www.home-assistant.io/integrations/sonarr", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 5a01d9f9e2fa09..3160c4cee4b43a 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -1,7 +1,7 @@ { "domain": "songpal", "name": "Songpal", - "documentation": "https://www.home-assistant.io/components/songpal", + "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ "python-songpal==0.0.9.1" ], diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index a08c0a59c07fd6..6d636f36b3fb5f 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -2,7 +2,7 @@ "domain": "sonos", "name": "Sonos", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/sonos", + "documentation": "https://www.home-assistant.io/integrations/sonos", "requirements": [ "pysonos==0.0.23" ], diff --git a/homeassistant/components/sony_projector/manifest.json b/homeassistant/components/sony_projector/manifest.json index 1cc25d93f59863..497347671a9504 100644 --- a/homeassistant/components/sony_projector/manifest.json +++ b/homeassistant/components/sony_projector/manifest.json @@ -1,7 +1,7 @@ { "domain": "sony_projector", "name": "Sony projector", - "documentation": "https://www.home-assistant.io/components/sony_projector", + "documentation": "https://www.home-assistant.io/integrations/sony_projector", "requirements": [ "pysdcp==1" ], diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index eba60bc6e34693..e3ca9ed72e8814 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -1,7 +1,7 @@ { "domain": "soundtouch", "name": "Soundtouch", - "documentation": "https://www.home-assistant.io/components/soundtouch", + "documentation": "https://www.home-assistant.io/integrations/soundtouch", "requirements": [ "libsoundtouch==0.7.2" ], diff --git a/homeassistant/components/spaceapi/manifest.json b/homeassistant/components/spaceapi/manifest.json index 03aa5c0a1f7e2b..4ab05f170ca8a1 100644 --- a/homeassistant/components/spaceapi/manifest.json +++ b/homeassistant/components/spaceapi/manifest.json @@ -1,7 +1,7 @@ { "domain": "spaceapi", "name": "Spaceapi", - "documentation": "https://www.home-assistant.io/components/spaceapi", + "documentation": "https://www.home-assistant.io/integrations/spaceapi", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/spc/manifest.json b/homeassistant/components/spc/manifest.json index 572d4b04b87cf9..5b59d3bfe29e68 100644 --- a/homeassistant/components/spc/manifest.json +++ b/homeassistant/components/spc/manifest.json @@ -1,7 +1,7 @@ { "domain": "spc", "name": "Spc", - "documentation": "https://www.home-assistant.io/components/spc", + "documentation": "https://www.home-assistant.io/integrations/spc", "requirements": [ "pyspcwebgw==0.4.0" ], diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index 91b7e7c5c0fcff..b35df8ee7c8fbd 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "speedtestdotnet", "name": "Speedtestdotnet", - "documentation": "https://www.home-assistant.io/components/speedtestdotnet", + "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", "requirements": [ "speedtest-cli==2.1.1" ], diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index 4cd7a4677370a8..567f6f0e513e88 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -1,7 +1,7 @@ { "domain": "spider", "name": "Spider", - "documentation": "https://www.home-assistant.io/components/spider", + "documentation": "https://www.home-assistant.io/integrations/spider", "requirements": [ "spiderpy==1.3.1" ], diff --git a/homeassistant/components/splunk/manifest.json b/homeassistant/components/splunk/manifest.json index 2e81da3409a6fc..a6972e8881d3c3 100644 --- a/homeassistant/components/splunk/manifest.json +++ b/homeassistant/components/splunk/manifest.json @@ -1,7 +1,7 @@ { "domain": "splunk", "name": "Splunk", - "documentation": "https://www.home-assistant.io/components/splunk", + "documentation": "https://www.home-assistant.io/integrations/splunk", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/spotcrime/manifest.json b/homeassistant/components/spotcrime/manifest.json index 5827f307ecfc94..50d9ba3163d2bd 100644 --- a/homeassistant/components/spotcrime/manifest.json +++ b/homeassistant/components/spotcrime/manifest.json @@ -1,7 +1,7 @@ { "domain": "spotcrime", "name": "Spotcrime", - "documentation": "https://www.home-assistant.io/components/spotcrime", + "documentation": "https://www.home-assistant.io/integrations/spotcrime", "requirements": [ "spotcrime==1.0.4" ], diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 366a5eef0ad99e..f514a95c04205c 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -1,7 +1,7 @@ { "domain": "spotify", "name": "Spotify", - "documentation": "https://www.home-assistant.io/components/spotify", + "documentation": "https://www.home-assistant.io/integrations/spotify", "requirements": [ "spotipy-homeassistant==2.4.4.dev1" ], diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 38a320543a9cca..41d80ebccf9a03 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -1,7 +1,7 @@ { "domain": "sql", "name": "Sql", - "documentation": "https://www.home-assistant.io/components/sql", + "documentation": "https://www.home-assistant.io/integrations/sql", "requirements": [ "sqlalchemy==1.3.8" ], diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index ae124d6c03d517..1f651f746e6a3c 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -1,7 +1,7 @@ { "domain": "squeezebox", "name": "Squeezebox", - "documentation": "https://www.home-assistant.io/components/squeezebox", + "documentation": "https://www.home-assistant.io/integrations/squeezebox", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index ce00bcbc888e5a..1c3d56fe7fe9a7 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -1,7 +1,7 @@ { "domain": "ssdp", "name": "SSDP", - "documentation": "https://www.home-assistant.io/components/ssdp", + "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ "netdisco==2.6.0" ], diff --git a/homeassistant/components/starlingbank/manifest.json b/homeassistant/components/starlingbank/manifest.json index 1314fda5099fba..33dbb40f78a17b 100644 --- a/homeassistant/components/starlingbank/manifest.json +++ b/homeassistant/components/starlingbank/manifest.json @@ -1,7 +1,7 @@ { "domain": "starlingbank", "name": "Starlingbank", - "documentation": "https://www.home-assistant.io/components/starlingbank", + "documentation": "https://www.home-assistant.io/integrations/starlingbank", "requirements": [ "starlingbank==3.1" ], diff --git a/homeassistant/components/startca/manifest.json b/homeassistant/components/startca/manifest.json index d2f9e90c41a9dd..79637bfb124f89 100644 --- a/homeassistant/components/startca/manifest.json +++ b/homeassistant/components/startca/manifest.json @@ -1,7 +1,7 @@ { "domain": "startca", "name": "Startca", - "documentation": "https://www.home-assistant.io/components/startca", + "documentation": "https://www.home-assistant.io/integrations/startca", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/statistics/manifest.json b/homeassistant/components/statistics/manifest.json index 49e476a687632b..3dab05942b947b 100644 --- a/homeassistant/components/statistics/manifest.json +++ b/homeassistant/components/statistics/manifest.json @@ -1,7 +1,7 @@ { "domain": "statistics", "name": "Statistics", - "documentation": "https://www.home-assistant.io/components/statistics", + "documentation": "https://www.home-assistant.io/integrations/statistics", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/statsd/manifest.json b/homeassistant/components/statsd/manifest.json index 20f4cc7f5443a6..387576f8845fea 100644 --- a/homeassistant/components/statsd/manifest.json +++ b/homeassistant/components/statsd/manifest.json @@ -1,7 +1,7 @@ { "domain": "statsd", "name": "Statsd", - "documentation": "https://www.home-assistant.io/components/statsd", + "documentation": "https://www.home-assistant.io/integrations/statsd", "requirements": [ "statsd==3.2.1" ], diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index 735a1869c34d86..ecc6198cb88888 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -1,7 +1,7 @@ { "domain": "steam_online", "name": "Steam online", - "documentation": "https://www.home-assistant.io/components/steam_online", + "documentation": "https://www.home-assistant.io/integrations/steam_online", "requirements": [ "steamodd==4.21" ], diff --git a/homeassistant/components/stiebel_eltron/manifest.json b/homeassistant/components/stiebel_eltron/manifest.json index 0f8b586a9c2d6a..385df6a5063641 100644 --- a/homeassistant/components/stiebel_eltron/manifest.json +++ b/homeassistant/components/stiebel_eltron/manifest.json @@ -1,7 +1,7 @@ { "domain": "stiebel_eltron", "name": "STIEBEL ELTRON", - "documentation": "https://www.home-assistant.io/components/stiebel_eltron", + "documentation": "https://www.home-assistant.io/integrations/stiebel_eltron", "requirements": [ "pystiebeleltron==0.0.1.dev2" ], diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index f285f81f27fdac..a00315e1064dd3 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -1,7 +1,7 @@ { "domain": "stream", "name": "Stream", - "documentation": "https://www.home-assistant.io/components/stream", + "documentation": "https://www.home-assistant.io/integrations/stream", "requirements": [ "av==6.1.2" ], diff --git a/homeassistant/components/streamlabswater/manifest.json b/homeassistant/components/streamlabswater/manifest.json index b4173ebf0e9297..83b6314c4ab725 100644 --- a/homeassistant/components/streamlabswater/manifest.json +++ b/homeassistant/components/streamlabswater/manifest.json @@ -1,7 +1,7 @@ { "domain": "streamlabswater", "name": "Streamlabs Water", - "documentation": "https://www.home-assistant.io/components/streamlabswater", + "documentation": "https://www.home-assistant.io/integrations/streamlabswater", "requirements": [ "streamlabswater==1.0.1" ], diff --git a/homeassistant/components/stride/manifest.json b/homeassistant/components/stride/manifest.json index 307f4c929cfb4e..840984ad073a3f 100644 --- a/homeassistant/components/stride/manifest.json +++ b/homeassistant/components/stride/manifest.json @@ -1,7 +1,7 @@ { "domain": "stride", "name": "Stride", - "documentation": "https://www.home-assistant.io/components/stride", + "documentation": "https://www.home-assistant.io/integrations/stride", "requirements": [ "pystride==0.1.7" ], diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index 8ee3de2d77fa34..654f99f7ea47b6 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -1,7 +1,7 @@ { "domain": "suez_water", "name": "Suez Water Consumption Sensor", - "documentation": "https://www.home-assistant.io/components/suez_water", + "documentation": "https://www.home-assistant.io/integrations/suez_water", "dependencies": [], "codeowners": ["@ooii"], "requirements": ["pysuez==0.1.17"] diff --git a/homeassistant/components/sun/manifest.json b/homeassistant/components/sun/manifest.json index e55131306dcea7..31fcb1d7c2e5d3 100644 --- a/homeassistant/components/sun/manifest.json +++ b/homeassistant/components/sun/manifest.json @@ -1,7 +1,7 @@ { "domain": "sun", "name": "Sun", - "documentation": "https://www.home-assistant.io/components/sun", + "documentation": "https://www.home-assistant.io/integrations/sun", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/supervisord/manifest.json b/homeassistant/components/supervisord/manifest.json index 1fc849165ef00c..eaf1e66cff454e 100644 --- a/homeassistant/components/supervisord/manifest.json +++ b/homeassistant/components/supervisord/manifest.json @@ -1,7 +1,7 @@ { "domain": "supervisord", "name": "Supervisord", - "documentation": "https://www.home-assistant.io/components/supervisord", + "documentation": "https://www.home-assistant.io/integrations/supervisord", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/supla/manifest.json b/homeassistant/components/supla/manifest.json index cac1a5f18abf20..72635ca641a303 100644 --- a/homeassistant/components/supla/manifest.json +++ b/homeassistant/components/supla/manifest.json @@ -1,7 +1,7 @@ { "domain": "supla", "name": "Supla", - "documentation": "https://www.home-assistant.io/components/supla", + "documentation": "https://www.home-assistant.io/integrations/supla", "requirements": [ "pysupla==0.0.3" ], diff --git a/homeassistant/components/swiss_hydrological_data/manifest.json b/homeassistant/components/swiss_hydrological_data/manifest.json index d6b18d6cba80a0..f54ef55ce17e96 100644 --- a/homeassistant/components/swiss_hydrological_data/manifest.json +++ b/homeassistant/components/swiss_hydrological_data/manifest.json @@ -1,7 +1,7 @@ { "domain": "swiss_hydrological_data", "name": "Swiss hydrological data", - "documentation": "https://www.home-assistant.io/components/swiss_hydrological_data", + "documentation": "https://www.home-assistant.io/integrations/swiss_hydrological_data", "requirements": [ "swisshydrodata==0.0.3" ], diff --git a/homeassistant/components/swiss_public_transport/manifest.json b/homeassistant/components/swiss_public_transport/manifest.json index 99dcdbd0c882c7..c91d85fecd7e11 100644 --- a/homeassistant/components/swiss_public_transport/manifest.json +++ b/homeassistant/components/swiss_public_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "swiss_public_transport", "name": "Swiss public transport", - "documentation": "https://www.home-assistant.io/components/swiss_public_transport", + "documentation": "https://www.home-assistant.io/integrations/swiss_public_transport", "requirements": [ "python_opendata_transport==0.1.4" ], diff --git a/homeassistant/components/swisscom/manifest.json b/homeassistant/components/swisscom/manifest.json index e52fda3408395f..27e33c81607e9a 100644 --- a/homeassistant/components/swisscom/manifest.json +++ b/homeassistant/components/swisscom/manifest.json @@ -1,7 +1,7 @@ { "domain": "swisscom", "name": "Swisscom", - "documentation": "https://www.home-assistant.io/components/swisscom", + "documentation": "https://www.home-assistant.io/integrations/swisscom", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index 0f2872515827ac..73daca18c6a698 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -1,7 +1,7 @@ { "domain": "switch", "name": "Switch", - "documentation": "https://www.home-assistant.io/components/switch", + "documentation": "https://www.home-assistant.io/integrations/switch", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b9ea4eb276e428..204a3605bb8860 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -1,7 +1,7 @@ { "domain": "switchbot", "name": "Switchbot", - "documentation": "https://www.home-assistant.io/components/switchbot", + "documentation": "https://www.home-assistant.io/integrations/switchbot", "requirements": [ "PySwitchbot==0.6.2" ], diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 2f3b3b6e84a5bc..453ae542b2cb5c 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -1,7 +1,7 @@ { "domain": "switcher_kis", "name": "Switcher", - "documentation": "https://www.home-assistant.io/components/switcher_kis/", + "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": [ "@tomerfi" ], diff --git a/homeassistant/components/switchmate/manifest.json b/homeassistant/components/switchmate/manifest.json index 94f100abe86513..b15024b545c9dd 100644 --- a/homeassistant/components/switchmate/manifest.json +++ b/homeassistant/components/switchmate/manifest.json @@ -1,7 +1,7 @@ { "domain": "switchmate", "name": "Switchmate", - "documentation": "https://www.home-assistant.io/components/switchmate", + "documentation": "https://www.home-assistant.io/integrations/switchmate", "requirements": [ "pySwitchmate==0.4.6" ], diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index a2a45826d9db0e..79e6f8e5571ce1 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -1,7 +1,7 @@ { "domain": "syncthru", "name": "Syncthru", - "documentation": "https://www.home-assistant.io/components/syncthru", + "documentation": "https://www.home-assistant.io/integrations/syncthru", "requirements": [ "pysyncthru==0.4.3" ], diff --git a/homeassistant/components/synology/manifest.json b/homeassistant/components/synology/manifest.json index a108f5fa98352b..ea743836c74464 100644 --- a/homeassistant/components/synology/manifest.json +++ b/homeassistant/components/synology/manifest.json @@ -1,7 +1,7 @@ { "domain": "synology", "name": "Synology", - "documentation": "https://www.home-assistant.io/components/synology", + "documentation": "https://www.home-assistant.io/integrations/synology", "requirements": [ "py-synology==0.2.0" ], diff --git a/homeassistant/components/synology_chat/manifest.json b/homeassistant/components/synology_chat/manifest.json index d35b1d8c902302..3522ba0405c846 100644 --- a/homeassistant/components/synology_chat/manifest.json +++ b/homeassistant/components/synology_chat/manifest.json @@ -1,7 +1,7 @@ { "domain": "synology_chat", "name": "Synology chat", - "documentation": "https://www.home-assistant.io/components/synology_chat", + "documentation": "https://www.home-assistant.io/integrations/synology_chat", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/synology_srm/manifest.json b/homeassistant/components/synology_srm/manifest.json index a790a6c453cd1d..c507e07c717bc5 100644 --- a/homeassistant/components/synology_srm/manifest.json +++ b/homeassistant/components/synology_srm/manifest.json @@ -1,7 +1,7 @@ { "domain": "synology_srm", "name": "Synology SRM", - "documentation": "https://www.home-assistant.io/components/synology_srm", + "documentation": "https://www.home-assistant.io/integrations/synology_srm", "requirements": [ "synology-srm==0.0.7" ], diff --git a/homeassistant/components/synologydsm/manifest.json b/homeassistant/components/synologydsm/manifest.json index fcce2e52a215d4..d7dc76b6ac917c 100644 --- a/homeassistant/components/synologydsm/manifest.json +++ b/homeassistant/components/synologydsm/manifest.json @@ -1,7 +1,7 @@ { "domain": "synologydsm", "name": "Synologydsm", - "documentation": "https://www.home-assistant.io/components/synologydsm", + "documentation": "https://www.home-assistant.io/integrations/synologydsm", "requirements": [ "python-synology==0.2.0" ], diff --git a/homeassistant/components/syslog/manifest.json b/homeassistant/components/syslog/manifest.json index 19836ffa67f094..00dda3ebc02b39 100644 --- a/homeassistant/components/syslog/manifest.json +++ b/homeassistant/components/syslog/manifest.json @@ -1,7 +1,7 @@ { "domain": "syslog", "name": "Syslog", - "documentation": "https://www.home-assistant.io/components/syslog", + "documentation": "https://www.home-assistant.io/integrations/syslog", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/system_health/manifest.json b/homeassistant/components/system_health/manifest.json index 9c2b7bcae39c2e..4de7af3f8628d7 100644 --- a/homeassistant/components/system_health/manifest.json +++ b/homeassistant/components/system_health/manifest.json @@ -1,7 +1,7 @@ { "domain": "system_health", "name": "System health", - "documentation": "https://www.home-assistant.io/components/system_health", + "documentation": "https://www.home-assistant.io/integrations/system_health", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/system_log/manifest.json b/homeassistant/components/system_log/manifest.json index 01f70af4a15c36..0bc14a56e71dec 100644 --- a/homeassistant/components/system_log/manifest.json +++ b/homeassistant/components/system_log/manifest.json @@ -1,7 +1,7 @@ { "domain": "system_log", "name": "System log", - "documentation": "https://www.home-assistant.io/components/system_log", + "documentation": "https://www.home-assistant.io/integrations/system_log", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 565a459818fd14..a45a59e410a087 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -1,7 +1,7 @@ { "domain": "systemmonitor", "name": "Systemmonitor", - "documentation": "https://www.home-assistant.io/components/systemmonitor", + "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "requirements": [ "psutil==5.6.3" ], diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 8d42cde1c05162..9a884fa010c4a3 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -1,7 +1,7 @@ { "domain": "tado", "name": "Tado", - "documentation": "https://www.home-assistant.io/components/tado", + "documentation": "https://www.home-assistant.io/integrations/tado", "requirements": [ "python-tado==0.2.9" ], diff --git a/homeassistant/components/tahoma/manifest.json b/homeassistant/components/tahoma/manifest.json index ca3ab0bc882e21..1e99d4b288d725 100644 --- a/homeassistant/components/tahoma/manifest.json +++ b/homeassistant/components/tahoma/manifest.json @@ -1,7 +1,7 @@ { "domain": "tahoma", "name": "Tahoma", - "documentation": "https://www.home-assistant.io/components/tahoma", + "documentation": "https://www.home-assistant.io/integrations/tahoma", "requirements": [ "tahoma-api==0.0.14" ], diff --git a/homeassistant/components/tank_utility/manifest.json b/homeassistant/components/tank_utility/manifest.json index 04ffb48f39656c..328285ab67b599 100644 --- a/homeassistant/components/tank_utility/manifest.json +++ b/homeassistant/components/tank_utility/manifest.json @@ -1,7 +1,7 @@ { "domain": "tank_utility", "name": "Tank utility", - "documentation": "https://www.home-assistant.io/components/tank_utility", + "documentation": "https://www.home-assistant.io/integrations/tank_utility", "requirements": [ "tank_utility==1.4.0" ], diff --git a/homeassistant/components/tapsaff/manifest.json b/homeassistant/components/tapsaff/manifest.json index 1c8e8476987b25..79fff1b2b4c2b1 100644 --- a/homeassistant/components/tapsaff/manifest.json +++ b/homeassistant/components/tapsaff/manifest.json @@ -1,7 +1,7 @@ { "domain": "tapsaff", "name": "Tapsaff", - "documentation": "https://www.home-assistant.io/components/tapsaff", + "documentation": "https://www.home-assistant.io/integrations/tapsaff", "requirements": [ "tapsaff==0.2.1" ], diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index d49b52801813d8..cc635082c35b5c 100644 --- a/homeassistant/components/tautulli/manifest.json +++ b/homeassistant/components/tautulli/manifest.json @@ -1,7 +1,7 @@ { "domain": "tautulli", "name": "Tautulli", - "documentation": "https://www.home-assistant.io/components/tautulli", + "documentation": "https://www.home-assistant.io/integrations/tautulli", "requirements": [ "pytautulli==0.5.0" ], diff --git a/homeassistant/components/tcp/manifest.json b/homeassistant/components/tcp/manifest.json index 2ff29a27f3169b..53f3f6c64636d7 100644 --- a/homeassistant/components/tcp/manifest.json +++ b/homeassistant/components/tcp/manifest.json @@ -1,7 +1,7 @@ { "domain": "tcp", "name": "Tcp", - "documentation": "https://www.home-assistant.io/components/tcp", + "documentation": "https://www.home-assistant.io/integrations/tcp", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ted5000/manifest.json b/homeassistant/components/ted5000/manifest.json index 9cc50405bad9c0..b02a9db3fdc639 100644 --- a/homeassistant/components/ted5000/manifest.json +++ b/homeassistant/components/ted5000/manifest.json @@ -1,7 +1,7 @@ { "domain": "ted5000", "name": "Ted5000", - "documentation": "https://www.home-assistant.io/components/ted5000", + "documentation": "https://www.home-assistant.io/integrations/ted5000", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/teksavvy/manifest.json b/homeassistant/components/teksavvy/manifest.json index 14afdec3b71551..220a086e0be91d 100644 --- a/homeassistant/components/teksavvy/manifest.json +++ b/homeassistant/components/teksavvy/manifest.json @@ -1,7 +1,7 @@ { "domain": "teksavvy", "name": "Teksavvy", - "documentation": "https://www.home-assistant.io/components/teksavvy", + "documentation": "https://www.home-assistant.io/integrations/teksavvy", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/telegram/manifest.json b/homeassistant/components/telegram/manifest.json index 8a6dd7fb369d4c..392c45ea886dbd 100644 --- a/homeassistant/components/telegram/manifest.json +++ b/homeassistant/components/telegram/manifest.json @@ -1,7 +1,7 @@ { "domain": "telegram", "name": "Telegram", - "documentation": "https://www.home-assistant.io/components/telegram", + "documentation": "https://www.home-assistant.io/integrations/telegram", "requirements": [], "dependencies": [ "telegram_bot" diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index f341fd587ca0e1..afc83879cb0c1e 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -1,7 +1,7 @@ { "domain": "telegram_bot", "name": "Telegram bot", - "documentation": "https://www.home-assistant.io/components/telegram_bot", + "documentation": "https://www.home-assistant.io/integrations/telegram_bot", "requirements": [ "python-telegram-bot==11.1.0" ], diff --git a/homeassistant/components/tellduslive/manifest.json b/homeassistant/components/tellduslive/manifest.json index 7f431ba92b1b61..777138d44ab6a8 100644 --- a/homeassistant/components/tellduslive/manifest.json +++ b/homeassistant/components/tellduslive/manifest.json @@ -2,7 +2,7 @@ "domain": "tellduslive", "name": "Tellduslive", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/tellduslive", + "documentation": "https://www.home-assistant.io/integrations/tellduslive", "requirements": [ "tellduslive==0.10.10" ], diff --git a/homeassistant/components/tellstick/manifest.json b/homeassistant/components/tellstick/manifest.json index c50ba514f2aaab..3391533b081012 100644 --- a/homeassistant/components/tellstick/manifest.json +++ b/homeassistant/components/tellstick/manifest.json @@ -1,7 +1,7 @@ { "domain": "tellstick", "name": "Tellstick", - "documentation": "https://www.home-assistant.io/components/tellstick", + "documentation": "https://www.home-assistant.io/integrations/tellstick", "requirements": [ "tellcore-net==0.4", "tellcore-py==1.1.2" diff --git a/homeassistant/components/telnet/manifest.json b/homeassistant/components/telnet/manifest.json index 58f5e15cc1a37a..afba0e38301c90 100644 --- a/homeassistant/components/telnet/manifest.json +++ b/homeassistant/components/telnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "telnet", "name": "Telnet", - "documentation": "https://www.home-assistant.io/components/telnet", + "documentation": "https://www.home-assistant.io/integrations/telnet", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/temper/manifest.json b/homeassistant/components/temper/manifest.json index 0e60c957d9d6a9..76656e9936fdd9 100644 --- a/homeassistant/components/temper/manifest.json +++ b/homeassistant/components/temper/manifest.json @@ -1,7 +1,7 @@ { "domain": "temper", "name": "Temper", - "documentation": "https://www.home-assistant.io/components/temper", + "documentation": "https://www.home-assistant.io/integrations/temper", "requirements": [ "temperusb==1.5.3" ], diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index c8406c9d08494c..20a35f1afe75a3 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -1,7 +1,7 @@ { "domain": "template", "name": "Template", - "documentation": "https://www.home-assistant.io/components/template", + "documentation": "https://www.home-assistant.io/integrations/template", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 279ac3b103cba8..e7d35829ffb2fb 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -1,7 +1,7 @@ { "domain": "tensorflow", "name": "Tensorflow", - "documentation": "https://www.home-assistant.io/components/tensorflow", + "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", "numpy==1.17.1", diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index ab32a64e670f25..4071178c7c3ae0 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -1,7 +1,7 @@ { "domain": "tesla", "name": "Tesla", - "documentation": "https://www.home-assistant.io/components/tesla", + "documentation": "https://www.home-assistant.io/integrations/tesla", "requirements": [ "teslajsonpy==0.0.25" ], diff --git a/homeassistant/components/tfiac/manifest.json b/homeassistant/components/tfiac/manifest.json index d7282317d95dfb..7c93000790cc2c 100644 --- a/homeassistant/components/tfiac/manifest.json +++ b/homeassistant/components/tfiac/manifest.json @@ -1,7 +1,7 @@ { "domain": "tfiac", "name": "Tfiac", - "documentation": "https://www.home-assistant.io/components/tfiac", + "documentation": "https://www.home-assistant.io/integrations/tfiac", "requirements": [ "pytfiac==0.4" ], diff --git a/homeassistant/components/thermoworks_smoke/manifest.json b/homeassistant/components/thermoworks_smoke/manifest.json index fab670627ba891..93a334fc986b0a 100644 --- a/homeassistant/components/thermoworks_smoke/manifest.json +++ b/homeassistant/components/thermoworks_smoke/manifest.json @@ -1,7 +1,7 @@ { "domain": "thermoworks_smoke", "name": "Thermoworks smoke", - "documentation": "https://www.home-assistant.io/components/thermoworks_smoke", + "documentation": "https://www.home-assistant.io/integrations/thermoworks_smoke", "requirements": [ "stringcase==1.2.0", "thermoworks_smoke==0.1.8" diff --git a/homeassistant/components/thethingsnetwork/manifest.json b/homeassistant/components/thethingsnetwork/manifest.json index 8d6082d74bfbb8..98a852dea08a0b 100644 --- a/homeassistant/components/thethingsnetwork/manifest.json +++ b/homeassistant/components/thethingsnetwork/manifest.json @@ -1,7 +1,7 @@ { "domain": "thethingsnetwork", "name": "Thethingsnetwork", - "documentation": "https://www.home-assistant.io/components/thethingsnetwork", + "documentation": "https://www.home-assistant.io/integrations/thethingsnetwork", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/thingspeak/manifest.json b/homeassistant/components/thingspeak/manifest.json index 482bb94ac2ae0a..689a6678cab505 100644 --- a/homeassistant/components/thingspeak/manifest.json +++ b/homeassistant/components/thingspeak/manifest.json @@ -1,7 +1,7 @@ { "domain": "thingspeak", "name": "Thingspeak", - "documentation": "https://www.home-assistant.io/components/thingspeak", + "documentation": "https://www.home-assistant.io/integrations/thingspeak", "requirements": [ "thingspeak==0.4.1" ], diff --git a/homeassistant/components/thinkingcleaner/manifest.json b/homeassistant/components/thinkingcleaner/manifest.json index 4e43270a5e04ef..c7c8aaaf16a65e 100644 --- a/homeassistant/components/thinkingcleaner/manifest.json +++ b/homeassistant/components/thinkingcleaner/manifest.json @@ -1,7 +1,7 @@ { "domain": "thinkingcleaner", "name": "Thinkingcleaner", - "documentation": "https://www.home-assistant.io/components/thinkingcleaner", + "documentation": "https://www.home-assistant.io/integrations/thinkingcleaner", "requirements": [ "pythinkingcleaner==0.0.3" ], diff --git a/homeassistant/components/thomson/manifest.json b/homeassistant/components/thomson/manifest.json index 063c84d4ff7e8f..ac07a2f77ad5d9 100644 --- a/homeassistant/components/thomson/manifest.json +++ b/homeassistant/components/thomson/manifest.json @@ -1,7 +1,7 @@ { "domain": "thomson", "name": "Thomson", - "documentation": "https://www.home-assistant.io/components/thomson", + "documentation": "https://www.home-assistant.io/integrations/thomson", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/threshold/manifest.json b/homeassistant/components/threshold/manifest.json index 107b4351505a7f..f3aa54052789de 100644 --- a/homeassistant/components/threshold/manifest.json +++ b/homeassistant/components/threshold/manifest.json @@ -1,7 +1,7 @@ { "domain": "threshold", "name": "Threshold", - "documentation": "https://www.home-assistant.io/components/threshold", + "documentation": "https://www.home-assistant.io/integrations/threshold", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index d0f358c5902bec..11c5a676bf6b5c 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -1,7 +1,7 @@ { "domain": "tibber", "name": "Tibber", - "documentation": "https://www.home-assistant.io/components/tibber", + "documentation": "https://www.home-assistant.io/integrations/tibber", "requirements": [ "pyTibber==0.11.7" ], diff --git a/homeassistant/components/tikteck/manifest.json b/homeassistant/components/tikteck/manifest.json index 7edaf9ba978bff..c81b6f0a0b1f17 100644 --- a/homeassistant/components/tikteck/manifest.json +++ b/homeassistant/components/tikteck/manifest.json @@ -1,7 +1,7 @@ { "domain": "tikteck", "name": "Tikteck", - "documentation": "https://www.home-assistant.io/components/tikteck", + "documentation": "https://www.home-assistant.io/integrations/tikteck", "requirements": [ "tikteck==0.4" ], diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 3d26e8315ae4b6..5e40c89369ac14 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -1,7 +1,7 @@ { "domain": "tile", "name": "Tile", - "documentation": "https://www.home-assistant.io/components/tile", + "documentation": "https://www.home-assistant.io/integrations/tile", "requirements": [ "pytile==2.0.6" ], diff --git a/homeassistant/components/time_date/manifest.json b/homeassistant/components/time_date/manifest.json index bd620d4a18fc55..1824ea2db41b39 100644 --- a/homeassistant/components/time_date/manifest.json +++ b/homeassistant/components/time_date/manifest.json @@ -1,7 +1,7 @@ { "domain": "time_date", "name": "Time date", - "documentation": "https://www.home-assistant.io/components/time_date", + "documentation": "https://www.home-assistant.io/integrations/time_date", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/timer/manifest.json b/homeassistant/components/timer/manifest.json index 76a506faee86fb..1cf2c610014695 100644 --- a/homeassistant/components/timer/manifest.json +++ b/homeassistant/components/timer/manifest.json @@ -1,7 +1,7 @@ { "domain": "timer", "name": "Timer", - "documentation": "https://www.home-assistant.io/components/timer", + "documentation": "https://www.home-assistant.io/integrations/timer", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/tod/manifest.json b/homeassistant/components/tod/manifest.json index ff67748d64cd04..ff582b33cefa9f 100644 --- a/homeassistant/components/tod/manifest.json +++ b/homeassistant/components/tod/manifest.json @@ -1,7 +1,7 @@ { "domain": "tod", "name": "Tod", - "documentation": "https://www.home-assistant.io/components/tod", + "documentation": "https://www.home-assistant.io/integrations/tod", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index 7a6b4e2efab7a9..dbf1a941e00ba3 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -1,7 +1,7 @@ { "domain": "todoist", "name": "Todoist", - "documentation": "https://www.home-assistant.io/components/todoist", + "documentation": "https://www.home-assistant.io/integrations/todoist", "requirements": [ "todoist-python==7.0.17" ], diff --git a/homeassistant/components/tof/manifest.json b/homeassistant/components/tof/manifest.json index 5f64e661a9a5f4..dc87b6c2fc77ae 100644 --- a/homeassistant/components/tof/manifest.json +++ b/homeassistant/components/tof/manifest.json @@ -1,7 +1,7 @@ { "domain": "tof", "name": "Tof", - "documentation": "https://www.home-assistant.io/components/tof", + "documentation": "https://www.home-assistant.io/integrations/tof", "requirements": [ "VL53L1X2==0.1.5" ], diff --git a/homeassistant/components/tomato/manifest.json b/homeassistant/components/tomato/manifest.json index 615ea9ecd7eaa7..5f6584ce2501d6 100644 --- a/homeassistant/components/tomato/manifest.json +++ b/homeassistant/components/tomato/manifest.json @@ -1,7 +1,7 @@ { "domain": "tomato", "name": "Tomato", - "documentation": "https://www.home-assistant.io/components/tomato", + "documentation": "https://www.home-assistant.io/integrations/tomato", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/toon/manifest.json b/homeassistant/components/toon/manifest.json index 3fd00e88a0c16f..721f7037fce28c 100644 --- a/homeassistant/components/toon/manifest.json +++ b/homeassistant/components/toon/manifest.json @@ -2,7 +2,7 @@ "domain": "toon", "name": "Toon", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/toon", + "documentation": "https://www.home-assistant.io/integrations/toon", "requirements": [ "toonapilib==3.2.4" ], diff --git a/homeassistant/components/torque/manifest.json b/homeassistant/components/torque/manifest.json index 9ce41b59861a69..fbc788d252d125 100644 --- a/homeassistant/components/torque/manifest.json +++ b/homeassistant/components/torque/manifest.json @@ -1,7 +1,7 @@ { "domain": "torque", "name": "Torque", - "documentation": "https://www.home-assistant.io/components/torque", + "documentation": "https://www.home-assistant.io/integrations/torque", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index e6bcd6dd00a3ae..39cd0e0f1e6ca3 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -1,7 +1,7 @@ { "domain": "totalconnect", "name": "Totalconnect", - "documentation": "https://www.home-assistant.io/components/totalconnect", + "documentation": "https://www.home-assistant.io/integrations/totalconnect", "requirements": [ "total_connect_client==0.28" ], diff --git a/homeassistant/components/touchline/manifest.json b/homeassistant/components/touchline/manifest.json index 5b8b4f521ee268..7c0b36b50fe8dd 100644 --- a/homeassistant/components/touchline/manifest.json +++ b/homeassistant/components/touchline/manifest.json @@ -1,7 +1,7 @@ { "domain": "touchline", "name": "Touchline", - "documentation": "https://www.home-assistant.io/components/touchline", + "documentation": "https://www.home-assistant.io/integrations/touchline", "requirements": [ "pytouchline==0.7" ], diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index e0f85757afda5f..f299e02e2d3406 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -2,7 +2,7 @@ "domain": "tplink", "name": "Tplink", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/tplink", + "documentation": "https://www.home-assistant.io/integrations/tplink", "requirements": [ "pyHS100==0.3.5", "tplink==0.2.1" diff --git a/homeassistant/components/tplink_lte/manifest.json b/homeassistant/components/tplink_lte/manifest.json index e3efd8c83310a3..e1cdacde6d8cf4 100644 --- a/homeassistant/components/tplink_lte/manifest.json +++ b/homeassistant/components/tplink_lte/manifest.json @@ -1,7 +1,7 @@ { "domain": "tplink_lte", "name": "Tplink lte", - "documentation": "https://www.home-assistant.io/components/tplink_lte", + "documentation": "https://www.home-assistant.io/integrations/tplink_lte", "requirements": [ "tp-connected==0.0.4" ], diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 7d3e2f22d65aab..ffc82ee13e8ae8 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -2,7 +2,7 @@ "domain": "traccar", "name": "Traccar", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/traccar", + "documentation": "https://www.home-assistant.io/integrations/traccar", "requirements": [ "pytraccar==0.9.0", "stringcase==1.2.0" diff --git a/homeassistant/components/trackr/manifest.json b/homeassistant/components/trackr/manifest.json index 6ad348176ba11c..638d63cb487ab7 100644 --- a/homeassistant/components/trackr/manifest.json +++ b/homeassistant/components/trackr/manifest.json @@ -1,7 +1,7 @@ { "domain": "trackr", "name": "Trackr", - "documentation": "https://www.home-assistant.io/components/trackr", + "documentation": "https://www.home-assistant.io/integrations/trackr", "requirements": [ "pytrackr==0.0.5" ], diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index d847c6df24f7cb..d9fa4ad5696b8e 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -2,7 +2,7 @@ "domain": "tradfri", "name": "Tradfri", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/tradfri", + "documentation": "https://www.home-assistant.io/integrations/tradfri", "requirements": ["pytradfri[async]==6.3.1"], "homekit": { "models": ["TRADFRI"] diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 2e24100edd0b8f..1e24895af44b2e 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -1,7 +1,7 @@ { "domain": "trafikverket_train", "name": "Trafikverket train information", - "documentation": "https://www.home-assistant.io/components/trafikverket_train", + "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", "requirements": [ "pytrafikverket==0.1.5.9" ], diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 9bd734fe094065..64f1d636a1a979 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -1,7 +1,7 @@ { "domain": "trafikverket_weatherstation", "name": "Trafikverket weatherstation", - "documentation": "https://www.home-assistant.io/components/trafikverket_weatherstation", + "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", "requirements": [ "pytrafikverket==0.1.5.9" ], diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index 2bd4571ef93b92..c2fa31d7b500c9 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -2,7 +2,7 @@ "domain": "transmission", "name": "Transmission", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/transmission", + "documentation": "https://www.home-assistant.io/integrations/transmission", "requirements": [ "transmissionrpc==0.11" ], diff --git a/homeassistant/components/transport_nsw/manifest.json b/homeassistant/components/transport_nsw/manifest.json index 491cce7407f861..9d2e675c75712f 100644 --- a/homeassistant/components/transport_nsw/manifest.json +++ b/homeassistant/components/transport_nsw/manifest.json @@ -1,7 +1,7 @@ { "domain": "transport_nsw", "name": "Transport nsw", - "documentation": "https://www.home-assistant.io/components/transport_nsw", + "documentation": "https://www.home-assistant.io/integrations/transport_nsw", "requirements": [ "PyTransportNSW==0.1.1" ], diff --git a/homeassistant/components/travisci/manifest.json b/homeassistant/components/travisci/manifest.json index eb553fbe73c38f..f0d5d54c8cfd5c 100644 --- a/homeassistant/components/travisci/manifest.json +++ b/homeassistant/components/travisci/manifest.json @@ -1,7 +1,7 @@ { "domain": "travisci", "name": "Travisci", - "documentation": "https://www.home-assistant.io/components/travisci", + "documentation": "https://www.home-assistant.io/integrations/travisci", "requirements": [ "TravisPy==0.3.5" ], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 8719138f3ac213..1432d2d21a0662 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -1,7 +1,7 @@ { "domain": "trend", "name": "Trend", - "documentation": "https://www.home-assistant.io/components/trend", + "documentation": "https://www.home-assistant.io/integrations/trend", "requirements": [ "numpy==1.17.1" ], diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index ce600473cc5429..ca2059a4d19e8f 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -1,7 +1,7 @@ { "domain": "tts", "name": "Tts", - "documentation": "https://www.home-assistant.io/components/tts", + "documentation": "https://www.home-assistant.io/integrations/tts", "requirements": [ "mutagen==1.42.0" ], diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 9c83056f6aca81..cf16d587e87709 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -1,7 +1,7 @@ { "domain": "tuya", "name": "Tuya", - "documentation": "https://www.home-assistant.io/components/tuya", + "documentation": "https://www.home-assistant.io/integrations/tuya", "requirements": [ "tuyaha==0.0.4" ], diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index d1acf936a243ad..4eebba28ef67ed 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -2,7 +2,7 @@ "domain": "twentemilieu", "name": "Twente Milieu", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/twentemilieu", + "documentation": "https://www.home-assistant.io/integrations/twentemilieu", "requirements": [ "twentemilieu==0.1.0" ], diff --git a/homeassistant/components/twilio/manifest.json b/homeassistant/components/twilio/manifest.json index f96afa18115f0e..23fac51a3476b2 100644 --- a/homeassistant/components/twilio/manifest.json +++ b/homeassistant/components/twilio/manifest.json @@ -2,7 +2,7 @@ "domain": "twilio", "name": "Twilio", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/twilio", + "documentation": "https://www.home-assistant.io/integrations/twilio", "requirements": [ "twilio==6.19.1" ], diff --git a/homeassistant/components/twilio_call/manifest.json b/homeassistant/components/twilio_call/manifest.json index b235385396be12..3fe8f129b5db60 100644 --- a/homeassistant/components/twilio_call/manifest.json +++ b/homeassistant/components/twilio_call/manifest.json @@ -1,7 +1,7 @@ { "domain": "twilio_call", "name": "Twilio call", - "documentation": "https://www.home-assistant.io/components/twilio_call", + "documentation": "https://www.home-assistant.io/integrations/twilio_call", "requirements": [], "dependencies": [ "twilio" diff --git a/homeassistant/components/twilio_sms/manifest.json b/homeassistant/components/twilio_sms/manifest.json index 2174dc275b52cf..00c843d2dff28e 100644 --- a/homeassistant/components/twilio_sms/manifest.json +++ b/homeassistant/components/twilio_sms/manifest.json @@ -1,7 +1,7 @@ { "domain": "twilio_sms", "name": "Twilio sms", - "documentation": "https://www.home-assistant.io/components/twilio_sms", + "documentation": "https://www.home-assistant.io/integrations/twilio_sms", "requirements": [], "dependencies": [ "twilio" diff --git a/homeassistant/components/twitch/manifest.json b/homeassistant/components/twitch/manifest.json index 80bc795b536d09..f64182f6e4def7 100644 --- a/homeassistant/components/twitch/manifest.json +++ b/homeassistant/components/twitch/manifest.json @@ -1,7 +1,7 @@ { "domain": "twitch", "name": "Twitch", - "documentation": "https://www.home-assistant.io/components/twitch", + "documentation": "https://www.home-assistant.io/integrations/twitch", "requirements": [ "python-twitch-client==0.6.0" ], diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index e721bb669ed41c..2713004343ba5e 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -1,7 +1,7 @@ { "domain": "twitter", "name": "Twitter", - "documentation": "https://www.home-assistant.io/components/twitter", + "documentation": "https://www.home-assistant.io/integrations/twitter", "requirements": [ "TwitterAPI==2.5.9" ], diff --git a/homeassistant/components/ubee/manifest.json b/homeassistant/components/ubee/manifest.json index 39ffe7686579f6..0bf29beb0dc206 100644 --- a/homeassistant/components/ubee/manifest.json +++ b/homeassistant/components/ubee/manifest.json @@ -1,7 +1,7 @@ { "domain": "ubee", "name": "Ubee", - "documentation": "https://www.home-assistant.io/components/ubee", + "documentation": "https://www.home-assistant.io/integrations/ubee", "requirements": [ "pyubee==0.7" ], diff --git a/homeassistant/components/ubus/manifest.json b/homeassistant/components/ubus/manifest.json index f886e84254b179..664ae861442da6 100644 --- a/homeassistant/components/ubus/manifest.json +++ b/homeassistant/components/ubus/manifest.json @@ -1,7 +1,7 @@ { "domain": "ubus", "name": "Ubus", - "documentation": "https://www.home-assistant.io/components/ubus", + "documentation": "https://www.home-assistant.io/integrations/ubus", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ue_smart_radio/manifest.json b/homeassistant/components/ue_smart_radio/manifest.json index 189ac690758551..3711d7cbeb4730 100644 --- a/homeassistant/components/ue_smart_radio/manifest.json +++ b/homeassistant/components/ue_smart_radio/manifest.json @@ -1,7 +1,7 @@ { "domain": "ue_smart_radio", "name": "Ue smart radio", - "documentation": "https://www.home-assistant.io/components/ue_smart_radio", + "documentation": "https://www.home-assistant.io/integrations/ue_smart_radio", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/uk_transport/manifest.json b/homeassistant/components/uk_transport/manifest.json index be44a9d8cc82bb..cf349a205712ad 100644 --- a/homeassistant/components/uk_transport/manifest.json +++ b/homeassistant/components/uk_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "uk_transport", "name": "Uk transport", - "documentation": "https://www.home-assistant.io/components/uk_transport", + "documentation": "https://www.home-assistant.io/integrations/uk_transport", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index d182806c4ac7b7..ecbeb002f04959 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -2,7 +2,7 @@ "domain": "unifi", "name": "Unifi", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/unifi", + "documentation": "https://www.home-assistant.io/integrations/unifi", "requirements": [ "aiounifi==11" ], diff --git a/homeassistant/components/unifi_direct/manifest.json b/homeassistant/components/unifi_direct/manifest.json index 515bd68d011f73..805dc6638bb63a 100644 --- a/homeassistant/components/unifi_direct/manifest.json +++ b/homeassistant/components/unifi_direct/manifest.json @@ -1,7 +1,7 @@ { "domain": "unifi_direct", "name": "Unifi direct", - "documentation": "https://www.home-assistant.io/components/unifi_direct", + "documentation": "https://www.home-assistant.io/integrations/unifi_direct", "requirements": [ "pexpect==4.6.0" ], diff --git a/homeassistant/components/universal/manifest.json b/homeassistant/components/universal/manifest.json index ac72d10f07fbdd..3e066b48598869 100644 --- a/homeassistant/components/universal/manifest.json +++ b/homeassistant/components/universal/manifest.json @@ -1,7 +1,7 @@ { "domain": "universal", "name": "Universal", - "documentation": "https://www.home-assistant.io/components/universal", + "documentation": "https://www.home-assistant.io/integrations/universal", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index efa38286e7e2f6..2cf463d1cf0b25 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -1,7 +1,7 @@ { "domain": "upc_connect", "name": "Upc connect", - "documentation": "https://www.home-assistant.io/components/upc_connect", + "documentation": "https://www.home-assistant.io/integrations/upc_connect", "requirements": ["connect-box==0.2.4"], "dependencies": [], "codeowners": ["@pvizeli"] diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index 3a58d80f64aafe..62ce608a911f42 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "upcloud", "name": "Upcloud", - "documentation": "https://www.home-assistant.io/components/upcloud", + "documentation": "https://www.home-assistant.io/integrations/upcloud", "requirements": [ "upcloud-api==0.4.3" ], diff --git a/homeassistant/components/updater/manifest.json b/homeassistant/components/updater/manifest.json index 9275ef34968249..eb26d6e36b781f 100644 --- a/homeassistant/components/updater/manifest.json +++ b/homeassistant/components/updater/manifest.json @@ -1,7 +1,7 @@ { "domain": "updater", "name": "Updater", - "documentation": "https://www.home-assistant.io/components/updater", + "documentation": "https://www.home-assistant.io/integrations/updater", "requirements": [ "distro==1.4.0" ], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 9aec23a687ce0d..d4446b271f9a18 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -2,7 +2,7 @@ "domain": "upnp", "name": "Upnp", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/upnp", + "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": [ "async-upnp-client==0.14.11" ], diff --git a/homeassistant/components/uptime/manifest.json b/homeassistant/components/uptime/manifest.json index 1019717838108b..5997916e2c3fc8 100644 --- a/homeassistant/components/uptime/manifest.json +++ b/homeassistant/components/uptime/manifest.json @@ -1,7 +1,7 @@ { "domain": "uptime", "name": "Uptime", - "documentation": "https://www.home-assistant.io/components/uptime", + "documentation": "https://www.home-assistant.io/integrations/uptime", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index 375baf12565e00..cc2d1b6c2e85ae 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -1,7 +1,7 @@ { "domain": "uptimerobot", "name": "Uptimerobot", - "documentation": "https://www.home-assistant.io/components/uptimerobot", + "documentation": "https://www.home-assistant.io/integrations/uptimerobot", "requirements": [ "pyuptimerobot==0.0.5" ], diff --git a/homeassistant/components/uscis/manifest.json b/homeassistant/components/uscis/manifest.json index f2ffcfbf8a379d..707b7f278603dd 100644 --- a/homeassistant/components/uscis/manifest.json +++ b/homeassistant/components/uscis/manifest.json @@ -1,7 +1,7 @@ { "domain": "uscis", "name": "Uscis", - "documentation": "https://www.home-assistant.io/components/uscis", + "documentation": "https://www.home-assistant.io/integrations/uscis", "requirements": [ "uscisstatus==0.1.1" ], diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index 0d1c116786ab6b..d1ae97b550ada5 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -1,7 +1,7 @@ { "domain": "usgs_earthquakes_feed", "name": "Usgs earthquakes feed", - "documentation": "https://www.home-assistant.io/components/usgs_earthquakes_feed", + "documentation": "https://www.home-assistant.io/integrations/usgs_earthquakes_feed", "requirements": [ "geojson_client==0.4" ], diff --git a/homeassistant/components/utility_meter/manifest.json b/homeassistant/components/utility_meter/manifest.json index 59f4d1ca21b06b..7a470037ba4f86 100644 --- a/homeassistant/components/utility_meter/manifest.json +++ b/homeassistant/components/utility_meter/manifest.json @@ -1,7 +1,7 @@ { "domain": "utility_meter", "name": "Utility meter", - "documentation": "https://www.home-assistant.io/components/utility_meter", + "documentation": "https://www.home-assistant.io/integrations/utility_meter", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/uvc/manifest.json b/homeassistant/components/uvc/manifest.json index 5c77f9ecc70bab..497bdac656e4cb 100644 --- a/homeassistant/components/uvc/manifest.json +++ b/homeassistant/components/uvc/manifest.json @@ -1,7 +1,7 @@ { "domain": "uvc", "name": "Uvc", - "documentation": "https://www.home-assistant.io/components/uvc", + "documentation": "https://www.home-assistant.io/integrations/uvc", "requirements": [ "uvcclient==0.11.0" ], diff --git a/homeassistant/components/vacuum/manifest.json b/homeassistant/components/vacuum/manifest.json index 8dfbb8ed968c74..69edc40b15be7f 100644 --- a/homeassistant/components/vacuum/manifest.json +++ b/homeassistant/components/vacuum/manifest.json @@ -1,7 +1,7 @@ { "domain": "vacuum", "name": "Vacuum", - "documentation": "https://www.home-assistant.io/components/vacuum", + "documentation": "https://www.home-assistant.io/integrations/vacuum", "requirements": [], "dependencies": [ "group" diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 4f9b0f4d1269bd..51ecda914045de 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -1,7 +1,7 @@ { "domain": "vallox", "name": "Vallox", - "documentation": "https://www.home-assistant.io/components/vallox", + "documentation": "https://www.home-assistant.io/integrations/vallox", "requirements": [ "vallox-websocket-api==2.2.0" ], diff --git a/homeassistant/components/vasttrafik/manifest.json b/homeassistant/components/vasttrafik/manifest.json index 47153dcf17f50d..c4e3a2c97bb431 100644 --- a/homeassistant/components/vasttrafik/manifest.json +++ b/homeassistant/components/vasttrafik/manifest.json @@ -1,7 +1,7 @@ { "domain": "vasttrafik", "name": "Vasttrafik", - "documentation": "https://www.home-assistant.io/components/vasttrafik", + "documentation": "https://www.home-assistant.io/integrations/vasttrafik", "requirements": [ "vtjp==0.1.14" ], diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index b071b354d744a4..1d9401f6cfe827 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -1,7 +1,7 @@ { "domain": "velbus", "name": "Velbus", - "documentation": "https://www.home-assistant.io/components/velbus", + "documentation": "https://www.home-assistant.io/integrations/velbus", "requirements": [ "python-velbus==2.0.27" ], diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 9f1f4a7200afcb..783e23a8171800 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -1,7 +1,7 @@ { "domain": "velux", "name": "Velux", - "documentation": "https://www.home-assistant.io/components/velux", + "documentation": "https://www.home-assistant.io/integrations/velux", "requirements": [ "pyvlx==0.2.11" ], diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 655ba019e8c93a..e8e36d04467ce9 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -1,7 +1,7 @@ { "domain": "venstar", "name": "Venstar", - "documentation": "https://www.home-assistant.io/components/venstar", + "documentation": "https://www.home-assistant.io/integrations/venstar", "requirements": [ "venstarcolortouch==0.9" ], diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index b2f1581e76f191..120ec241d60f7b 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -1,7 +1,7 @@ { "domain": "vera", "name": "Vera", - "documentation": "https://www.home-assistant.io/components/vera", + "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": [ "pyvera==0.3.6" ], diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 7c895233f770d0..38ea8c7314769e 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -1,7 +1,7 @@ { "domain": "verisure", "name": "Verisure", - "documentation": "https://www.home-assistant.io/components/verisure", + "documentation": "https://www.home-assistant.io/integrations/verisure", "requirements": [ "jsonpath==0.75", "vsure==1.5.2" diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 815e7ff9a25794..3f35b0dc9a56ae 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -1,7 +1,7 @@ { "domain": "version", "name": "Version", - "documentation": "https://www.home-assistant.io/components/version", + "documentation": "https://www.home-assistant.io/integrations/version", "requirements": [ "pyhaversion==3.1.0" ], diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index 53cc96be388c55..d52380c10253f7 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -1,7 +1,7 @@ { "domain": "vesync", "name": "VeSync", - "documentation": "https://www.home-assistant.io/components/vesync", + "documentation": "https://www.home-assistant.io/integrations/vesync", "dependencies": [], "codeowners": ["@markperdue", "@webdjoe"], "requirements": ["pyvesync==1.1.0"], diff --git a/homeassistant/components/viaggiatreno/manifest.json b/homeassistant/components/viaggiatreno/manifest.json index e145b26b0c9a42..99857fc2f7ebf3 100644 --- a/homeassistant/components/viaggiatreno/manifest.json +++ b/homeassistant/components/viaggiatreno/manifest.json @@ -1,7 +1,7 @@ { "domain": "viaggiatreno", "name": "Viaggiatreno", - "documentation": "https://www.home-assistant.io/components/viaggiatreno", + "documentation": "https://www.home-assistant.io/integrations/viaggiatreno", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index e5f55b20ddaf8d..9f7c703fe4b514 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -1,7 +1,7 @@ { "domain": "vicare", "name": "Viessmann ViCare", - "documentation": "https://www.home-assistant.io/components/vicare", + "documentation": "https://www.home-assistant.io/integrations/vicare", "dependencies": [], "codeowners": ["@oischinger"], "requirements": ["PyViCare==0.1.1"] diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index cce2307bc4b554..20b2ac347f685e 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -1,7 +1,7 @@ { "domain": "vivotek", "name": "Vivotek", - "documentation": "https://www.home-assistant.io/components/vivotek", + "documentation": "https://www.home-assistant.io/integrations/vivotek", "requirements": [ "libpyvivotek==0.2.2" ], diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index c65204d78e8c71..682405a375b869 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -1,7 +1,7 @@ { "domain": "vizio", "name": "Vizio", - "documentation": "https://www.home-assistant.io/components/vizio", + "documentation": "https://www.home-assistant.io/integrations/vizio", "requirements": [ "pyvizio==0.0.7" ], diff --git a/homeassistant/components/vlc/manifest.json b/homeassistant/components/vlc/manifest.json index a40b0e8c7d61d1..0dfc9f1ceb9a01 100644 --- a/homeassistant/components/vlc/manifest.json +++ b/homeassistant/components/vlc/manifest.json @@ -1,7 +1,7 @@ { "domain": "vlc", "name": "Vlc", - "documentation": "https://www.home-assistant.io/components/vlc", + "documentation": "https://www.home-assistant.io/integrations/vlc", "requirements": [ "python-vlc==1.1.2" ], diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json index 1e0f1c71df5061..7894eb2982b4cb 100644 --- a/homeassistant/components/vlc_telnet/manifest.json +++ b/homeassistant/components/vlc_telnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "vlc_telnet", "name": "VLC telnet", - "documentation": "https://www.home-assistant.io/components/vlc-telnet", + "documentation": "https://www.home-assistant.io/integrations/vlc-telnet", "requirements": [ "python-telnet-vlc==1.0.4" ], diff --git a/homeassistant/components/voicerss/manifest.json b/homeassistant/components/voicerss/manifest.json index 6f0b4ae5fd258d..7a079d9ccf2b82 100644 --- a/homeassistant/components/voicerss/manifest.json +++ b/homeassistant/components/voicerss/manifest.json @@ -1,7 +1,7 @@ { "domain": "voicerss", "name": "Voicerss", - "documentation": "https://www.home-assistant.io/components/voicerss", + "documentation": "https://www.home-assistant.io/integrations/voicerss", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index db068e350566d9..0b210bc780b2d8 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -1,7 +1,7 @@ { "domain": "volkszaehler", "name": "Volkszaehler", - "documentation": "https://www.home-assistant.io/components/volkszaehler", + "documentation": "https://www.home-assistant.io/integrations/volkszaehler", "requirements": [ "volkszaehler==0.1.2" ], diff --git a/homeassistant/components/volumio/manifest.json b/homeassistant/components/volumio/manifest.json index e7c4bac4abd71b..a97c9d637ef01c 100644 --- a/homeassistant/components/volumio/manifest.json +++ b/homeassistant/components/volumio/manifest.json @@ -1,7 +1,7 @@ { "domain": "volumio", "name": "Volumio", - "documentation": "https://www.home-assistant.io/components/volumio", + "documentation": "https://www.home-assistant.io/integrations/volumio", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index aa691d7766c75e..3f75c391115fd8 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -1,7 +1,7 @@ { "domain": "volvooncall", "name": "Volvooncall", - "documentation": "https://www.home-assistant.io/components/volvooncall", + "documentation": "https://www.home-assistant.io/integrations/volvooncall", "requirements": [ "volvooncall==0.8.7" ], diff --git a/homeassistant/components/vultr/manifest.json b/homeassistant/components/vultr/manifest.json index 5f5461f2d63fb7..0a0afe3d71b59b 100644 --- a/homeassistant/components/vultr/manifest.json +++ b/homeassistant/components/vultr/manifest.json @@ -1,7 +1,7 @@ { "domain": "vultr", "name": "Vultr", - "documentation": "https://www.home-assistant.io/components/vultr", + "documentation": "https://www.home-assistant.io/integrations/vultr", "requirements": [ "vultr==0.1.2" ], diff --git a/homeassistant/components/w800rf32/manifest.json b/homeassistant/components/w800rf32/manifest.json index 920ee1120a7c50..89b0ac591ea937 100644 --- a/homeassistant/components/w800rf32/manifest.json +++ b/homeassistant/components/w800rf32/manifest.json @@ -1,7 +1,7 @@ { "domain": "w800rf32", "name": "W800rf32", - "documentation": "https://www.home-assistant.io/components/w800rf32", + "documentation": "https://www.home-assistant.io/integrations/w800rf32", "requirements": [ "pyW800rf32==0.1" ], diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index dc689f8d617f56..ef6dbd0647083c 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -1,7 +1,7 @@ { "domain": "wake_on_lan", "name": "Wake on lan", - "documentation": "https://www.home-assistant.io/components/wake_on_lan", + "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", "requirements": [ "wakeonlan==1.1.6" ], diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index 4b692c669d1ea7..8ce03e2e8e2608 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -1,7 +1,7 @@ { "domain": "waqi", "name": "Waqi", - "documentation": "https://www.home-assistant.io/components/waqi", + "documentation": "https://www.home-assistant.io/integrations/waqi", "requirements": [ "waqiasync==1.0.0" ], diff --git a/homeassistant/components/water_heater/manifest.json b/homeassistant/components/water_heater/manifest.json index e291777483ef00..7ed6493b843a89 100644 --- a/homeassistant/components/water_heater/manifest.json +++ b/homeassistant/components/water_heater/manifest.json @@ -1,7 +1,7 @@ { "domain": "water_heater", "name": "Water heater", - "documentation": "https://www.home-assistant.io/components/water_heater", + "documentation": "https://www.home-assistant.io/integrations/water_heater", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/waterfurnace/manifest.json b/homeassistant/components/waterfurnace/manifest.json index 57aa663a348ebb..e2982bd0145893 100644 --- a/homeassistant/components/waterfurnace/manifest.json +++ b/homeassistant/components/waterfurnace/manifest.json @@ -1,7 +1,7 @@ { "domain": "waterfurnace", "name": "Waterfurnace", - "documentation": "https://www.home-assistant.io/components/waterfurnace", + "documentation": "https://www.home-assistant.io/integrations/waterfurnace", "requirements": [ "waterfurnace==1.1.0" ], diff --git a/homeassistant/components/watson_iot/manifest.json b/homeassistant/components/watson_iot/manifest.json index 8896f34f976afe..20834bf0bf4ec5 100644 --- a/homeassistant/components/watson_iot/manifest.json +++ b/homeassistant/components/watson_iot/manifest.json @@ -1,7 +1,7 @@ { "domain": "watson_iot", "name": "Watson iot", - "documentation": "https://www.home-assistant.io/components/watson_iot", + "documentation": "https://www.home-assistant.io/integrations/watson_iot", "requirements": [ "ibmiotf==0.3.4" ], diff --git a/homeassistant/components/watson_tts/manifest.json b/homeassistant/components/watson_tts/manifest.json index d40baaca13220e..4cde3f764e5080 100644 --- a/homeassistant/components/watson_tts/manifest.json +++ b/homeassistant/components/watson_tts/manifest.json @@ -1,7 +1,7 @@ { "domain": "watson_tts", "name": "IBM Watson TTS", - "documentation": "https://www.home-assistant.io/components/watson_tts", + "documentation": "https://www.home-assistant.io/integrations/watson_tts", "requirements": [ "ibm-watson==3.0.3" ], diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 09ae4f812d7ade..85bcc19032e965 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -1,7 +1,7 @@ { "domain": "waze_travel_time", "name": "Waze travel time", - "documentation": "https://www.home-assistant.io/components/waze_travel_time", + "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "requirements": [ "WazeRouteCalculator==0.10" ], diff --git a/homeassistant/components/weather/manifest.json b/homeassistant/components/weather/manifest.json index 7008c033f95bc9..568ce57ed6213b 100644 --- a/homeassistant/components/weather/manifest.json +++ b/homeassistant/components/weather/manifest.json @@ -1,7 +1,7 @@ { "domain": "weather", "name": "Weather", - "documentation": "https://www.home-assistant.io/components/weather", + "documentation": "https://www.home-assistant.io/integrations/weather", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/webhook/manifest.json b/homeassistant/components/webhook/manifest.json index 384e61aed2a848..f93d8a5e295234 100644 --- a/homeassistant/components/webhook/manifest.json +++ b/homeassistant/components/webhook/manifest.json @@ -1,7 +1,7 @@ { "domain": "webhook", "name": "Webhook", - "documentation": "https://www.home-assistant.io/components/webhook", + "documentation": "https://www.home-assistant.io/integrations/webhook", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/weblink/manifest.json b/homeassistant/components/weblink/manifest.json index 7c30ad6c5d3549..7cdf8bea4ff1c4 100644 --- a/homeassistant/components/weblink/manifest.json +++ b/homeassistant/components/weblink/manifest.json @@ -1,7 +1,7 @@ { "domain": "weblink", "name": "Weblink", - "documentation": "https://www.home-assistant.io/components/weblink", + "documentation": "https://www.home-assistant.io/integrations/weblink", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 4dd2f92628d630..dcf908cd6037d4 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -1,7 +1,7 @@ { "domain": "webostv", "name": "Webostv", - "documentation": "https://www.home-assistant.io/components/webostv", + "documentation": "https://www.home-assistant.io/integrations/webostv", "requirements": [ "pylgtv==0.1.9", "websockets==6.0" diff --git a/homeassistant/components/websocket_api/manifest.json b/homeassistant/components/websocket_api/manifest.json index bc630b2947fca5..9826b11ec45e36 100644 --- a/homeassistant/components/websocket_api/manifest.json +++ b/homeassistant/components/websocket_api/manifest.json @@ -1,7 +1,7 @@ { "domain": "websocket_api", "name": "Websocket api", - "documentation": "https://www.home-assistant.io/components/websocket_api", + "documentation": "https://www.home-assistant.io/integrations/websocket_api", "requirements": [], "dependencies": [ "http" diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 1902df1060b317..aa863bcff0d4ad 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -2,7 +2,7 @@ "domain": "wemo", "name": "Wemo", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/wemo", + "documentation": "https://www.home-assistant.io/integrations/wemo", "requirements": [ "pywemo==0.4.34" ], diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json index 6040c8655b9f25..1566366362a44b 100644 --- a/homeassistant/components/whois/manifest.json +++ b/homeassistant/components/whois/manifest.json @@ -1,7 +1,7 @@ { "domain": "whois", "name": "Whois", - "documentation": "https://www.home-assistant.io/components/whois", + "documentation": "https://www.home-assistant.io/integrations/whois", "requirements": [ "python-whois==0.7.2" ], diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index cddfdc5dc9c878..acf9c38e6322c1 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -1,7 +1,7 @@ { "domain": "wink", "name": "Wink", - "documentation": "https://www.home-assistant.io/components/wink", + "documentation": "https://www.home-assistant.io/integrations/wink", "requirements": [ "pubnubsub-handler==1.0.8", "python-wink==1.10.5" diff --git a/homeassistant/components/wirelesstag/manifest.json b/homeassistant/components/wirelesstag/manifest.json index c3da00ce951c75..7472898b7ca6f2 100644 --- a/homeassistant/components/wirelesstag/manifest.json +++ b/homeassistant/components/wirelesstag/manifest.json @@ -1,7 +1,7 @@ { "domain": "wirelesstag", "name": "Wirelesstag", - "documentation": "https://www.home-assistant.io/components/wirelesstag", + "documentation": "https://www.home-assistant.io/integrations/wirelesstag", "requirements": [ "wirelesstagpy==0.4.0" ], diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 726d9f13eda0b0..d38b69f2248bcc 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -2,7 +2,7 @@ "domain": "withings", "name": "Withings", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/withings", + "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ "nokia==1.2.0" ], diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index ba8712f0575282..4b407e9523540c 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -1,7 +1,7 @@ { "domain": "workday", "name": "Workday", - "documentation": "https://www.home-assistant.io/components/workday", + "documentation": "https://www.home-assistant.io/integrations/workday", "requirements": [ "holidays==0.9.11" ], diff --git a/homeassistant/components/worldclock/manifest.json b/homeassistant/components/worldclock/manifest.json index 2da33f942b8f65..8f7b72491b8eb7 100644 --- a/homeassistant/components/worldclock/manifest.json +++ b/homeassistant/components/worldclock/manifest.json @@ -1,7 +1,7 @@ { "domain": "worldclock", "name": "Worldclock", - "documentation": "https://www.home-assistant.io/components/worldclock", + "documentation": "https://www.home-assistant.io/integrations/worldclock", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/worldtidesinfo/manifest.json b/homeassistant/components/worldtidesinfo/manifest.json index dfc116c97db359..467a98a6660e22 100644 --- a/homeassistant/components/worldtidesinfo/manifest.json +++ b/homeassistant/components/worldtidesinfo/manifest.json @@ -1,7 +1,7 @@ { "domain": "worldtidesinfo", "name": "Worldtidesinfo", - "documentation": "https://www.home-assistant.io/components/worldtidesinfo", + "documentation": "https://www.home-assistant.io/integrations/worldtidesinfo", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/worxlandroid/manifest.json b/homeassistant/components/worxlandroid/manifest.json index 3e7c626ddd0f8a..b112bb7771cde5 100644 --- a/homeassistant/components/worxlandroid/manifest.json +++ b/homeassistant/components/worxlandroid/manifest.json @@ -1,7 +1,7 @@ { "domain": "worxlandroid", "name": "Worxlandroid", - "documentation": "https://www.home-assistant.io/components/worxlandroid", + "documentation": "https://www.home-assistant.io/integrations/worxlandroid", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/wsdot/manifest.json b/homeassistant/components/wsdot/manifest.json index c778ed1049f598..1c20b8842238e0 100644 --- a/homeassistant/components/wsdot/manifest.json +++ b/homeassistant/components/wsdot/manifest.json @@ -1,7 +1,7 @@ { "domain": "wsdot", "name": "Wsdot", - "documentation": "https://www.home-assistant.io/components/wsdot", + "documentation": "https://www.home-assistant.io/integrations/wsdot", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/wunderground/manifest.json b/homeassistant/components/wunderground/manifest.json index d14c9db419a53a..8309f03bca4be0 100644 --- a/homeassistant/components/wunderground/manifest.json +++ b/homeassistant/components/wunderground/manifest.json @@ -1,7 +1,7 @@ { "domain": "wunderground", "name": "Wunderground", - "documentation": "https://www.home-assistant.io/components/wunderground", + "documentation": "https://www.home-assistant.io/integrations/wunderground", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/wunderlist/manifest.json b/homeassistant/components/wunderlist/manifest.json index 505447f454c031..90a55ad48e8f67 100644 --- a/homeassistant/components/wunderlist/manifest.json +++ b/homeassistant/components/wunderlist/manifest.json @@ -1,7 +1,7 @@ { "domain": "wunderlist", "name": "Wunderlist", - "documentation": "https://www.home-assistant.io/components/wunderlist", + "documentation": "https://www.home-assistant.io/integrations/wunderlist", "requirements": [ "wunderpy2==0.1.6" ], diff --git a/homeassistant/components/wwlln/manifest.json b/homeassistant/components/wwlln/manifest.json index 189b9365105159..a7bf14454da8d5 100644 --- a/homeassistant/components/wwlln/manifest.json +++ b/homeassistant/components/wwlln/manifest.json @@ -2,7 +2,7 @@ "domain": "wwlln", "name": "World Wide Lightning Location Network", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/wwlln", + "documentation": "https://www.home-assistant.io/integrations/wwlln", "requirements": [ "aiowwlln==2.0.2" ], diff --git a/homeassistant/components/x10/manifest.json b/homeassistant/components/x10/manifest.json index 2fbe16a6e7adae..bae5247ffbc553 100644 --- a/homeassistant/components/x10/manifest.json +++ b/homeassistant/components/x10/manifest.json @@ -1,7 +1,7 @@ { "domain": "x10", "name": "X10", - "documentation": "https://www.home-assistant.io/components/x10", + "documentation": "https://www.home-assistant.io/integrations/x10", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/xbox_live/manifest.json b/homeassistant/components/xbox_live/manifest.json index 5baf928352d514..79f4ce6c87fc29 100644 --- a/homeassistant/components/xbox_live/manifest.json +++ b/homeassistant/components/xbox_live/manifest.json @@ -1,7 +1,7 @@ { "domain": "xbox_live", "name": "Xbox live", - "documentation": "https://www.home-assistant.io/components/xbox_live", + "documentation": "https://www.home-assistant.io/integrations/xbox_live", "requirements": [ "xboxapi==0.1.1" ], diff --git a/homeassistant/components/xeoma/manifest.json b/homeassistant/components/xeoma/manifest.json index ee8ed2f6de31de..7cf061018b9615 100644 --- a/homeassistant/components/xeoma/manifest.json +++ b/homeassistant/components/xeoma/manifest.json @@ -1,7 +1,7 @@ { "domain": "xeoma", "name": "Xeoma", - "documentation": "https://www.home-assistant.io/components/xeoma", + "documentation": "https://www.home-assistant.io/integrations/xeoma", "requirements": [ "pyxeoma==1.4.1" ], diff --git a/homeassistant/components/xfinity/manifest.json b/homeassistant/components/xfinity/manifest.json index 71750ccf0889a4..9e800dc2e4a341 100644 --- a/homeassistant/components/xfinity/manifest.json +++ b/homeassistant/components/xfinity/manifest.json @@ -1,7 +1,7 @@ { "domain": "xfinity", "name": "Xfinity", - "documentation": "https://www.home-assistant.io/components/xfinity", + "documentation": "https://www.home-assistant.io/integrations/xfinity", "requirements": [ "xfinity-gateway==0.0.4" ], diff --git a/homeassistant/components/xiaomi/manifest.json b/homeassistant/components/xiaomi/manifest.json index d3587100501f04..a607cda511ebae 100644 --- a/homeassistant/components/xiaomi/manifest.json +++ b/homeassistant/components/xiaomi/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi", "name": "Xiaomi", - "documentation": "https://www.home-assistant.io/components/xiaomi", + "documentation": "https://www.home-assistant.io/integrations/xiaomi", "requirements": [], "dependencies": [ "ffmpeg" diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index 36da259f82e5cf..9eeddd357f6bf6 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi_aqara", "name": "Xiaomi aqara", - "documentation": "https://www.home-assistant.io/components/xiaomi_aqara", + "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", "requirements": [ "PyXiaomiGateway==0.12.4" ], diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index d7e0d0d732eee8..4c01cce2d3c42e 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi_miio", "name": "Xiaomi miio", - "documentation": "https://www.home-assistant.io/components/xiaomi_miio", + "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": [ "construct==2.9.45", "python-miio==0.4.5" diff --git a/homeassistant/components/xiaomi_tv/manifest.json b/homeassistant/components/xiaomi_tv/manifest.json index 26940a57c78743..740eaf3ea1caed 100644 --- a/homeassistant/components/xiaomi_tv/manifest.json +++ b/homeassistant/components/xiaomi_tv/manifest.json @@ -1,7 +1,7 @@ { "domain": "xiaomi_tv", "name": "Xiaomi tv", - "documentation": "https://www.home-assistant.io/components/xiaomi_tv", + "documentation": "https://www.home-assistant.io/integrations/xiaomi_tv", "requirements": [ "pymitv==1.4.3" ], diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 3d2c3a5e9119cb..21255497503b47 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -1,7 +1,7 @@ { "domain": "xmpp", "name": "Xmpp", - "documentation": "https://www.home-assistant.io/components/xmpp", + "documentation": "https://www.home-assistant.io/integrations/xmpp", "requirements": [ "slixmpp==1.4.2" ], diff --git a/homeassistant/components/xs1/manifest.json b/homeassistant/components/xs1/manifest.json index 4ee13acf6472e7..290c552309b8ab 100644 --- a/homeassistant/components/xs1/manifest.json +++ b/homeassistant/components/xs1/manifest.json @@ -1,7 +1,7 @@ { "domain": "xs1", "name": "Xs1", - "documentation": "https://www.home-assistant.io/components/xs1", + "documentation": "https://www.home-assistant.io/integrations/xs1", "requirements": [ "xs1-api-client==2.3.5" ], diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index 7b786c7bf7c58e..05e979ffb0a065 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -1,7 +1,7 @@ { "domain": "yale_smart_alarm", "name": "Yale smart alarm", - "documentation": "https://www.home-assistant.io/components/yale_smart_alarm", + "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", "requirements": [ "yalesmartalarmclient==0.1.6" ], diff --git a/homeassistant/components/yamaha/manifest.json b/homeassistant/components/yamaha/manifest.json index 5a277fc7ce879c..bacb9fc33055ed 100644 --- a/homeassistant/components/yamaha/manifest.json +++ b/homeassistant/components/yamaha/manifest.json @@ -1,7 +1,7 @@ { "domain": "yamaha", "name": "Yamaha", - "documentation": "https://www.home-assistant.io/components/yamaha", + "documentation": "https://www.home-assistant.io/integrations/yamaha", "requirements": [ "rxv==0.6.0" ], diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 7769026e09279a..ea36c4921c56c2 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -1,7 +1,7 @@ { "domain": "yamaha_musiccast", "name": "Yamaha musiccast", - "documentation": "https://www.home-assistant.io/components/yamaha_musiccast", + "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", "requirements": [ "pymusiccast==0.1.6" ], diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index 6c633f848c0c68..91267b43480b06 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -1,7 +1,7 @@ { "domain": "yandex_transport", "name": "Yandex Transport", - "documentation": "https://www.home-assistant.io/components/yandex_transport", + "documentation": "https://www.home-assistant.io/integrations/yandex_transport", "requirements": [ "ya_ma==0.3.7" ], diff --git a/homeassistant/components/yandextts/manifest.json b/homeassistant/components/yandextts/manifest.json index 7f622a1e25fe68..66a546abdb41e5 100644 --- a/homeassistant/components/yandextts/manifest.json +++ b/homeassistant/components/yandextts/manifest.json @@ -1,7 +1,7 @@ { "domain": "yandextts", "name": "Yandextts", - "documentation": "https://www.home-assistant.io/components/yandextts", + "documentation": "https://www.home-assistant.io/integrations/yandextts", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 061d2b065c4cbd..3d27d5bd393add 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -1,7 +1,7 @@ { "domain": "yeelight", "name": "Yeelight", - "documentation": "https://www.home-assistant.io/components/yeelight", + "documentation": "https://www.home-assistant.io/integrations/yeelight", "requirements": [ "yeelight==0.5.0" ], diff --git a/homeassistant/components/yeelightsunflower/manifest.json b/homeassistant/components/yeelightsunflower/manifest.json index 1a75472b80131a..390ff7427244e4 100644 --- a/homeassistant/components/yeelightsunflower/manifest.json +++ b/homeassistant/components/yeelightsunflower/manifest.json @@ -1,7 +1,7 @@ { "domain": "yeelightsunflower", "name": "Yeelightsunflower", - "documentation": "https://www.home-assistant.io/components/yeelightsunflower", + "documentation": "https://www.home-assistant.io/integrations/yeelightsunflower", "requirements": [ "yeelightsunflower==0.0.10" ], diff --git a/homeassistant/components/yessssms/manifest.json b/homeassistant/components/yessssms/manifest.json index c7b5535d03c6f6..b68649525c2a04 100644 --- a/homeassistant/components/yessssms/manifest.json +++ b/homeassistant/components/yessssms/manifest.json @@ -1,7 +1,7 @@ { "domain": "yessssms", "name": "Yessssms", - "documentation": "https://www.home-assistant.io/components/yessssms", + "documentation": "https://www.home-assistant.io/integrations/yessssms", "requirements": [ "YesssSMS==0.4.1" ], diff --git a/homeassistant/components/yi/manifest.json b/homeassistant/components/yi/manifest.json index bb7fbf55cbc205..461f3e24330113 100644 --- a/homeassistant/components/yi/manifest.json +++ b/homeassistant/components/yi/manifest.json @@ -1,7 +1,7 @@ { "domain": "yi", "name": "Yi", - "documentation": "https://www.home-assistant.io/components/yi", + "documentation": "https://www.home-assistant.io/integrations/yi", "requirements": [ "aioftp==0.12.0" ], diff --git a/homeassistant/components/yr/manifest.json b/homeassistant/components/yr/manifest.json index 7f06ddddcb5700..d49004cc0e3944 100644 --- a/homeassistant/components/yr/manifest.json +++ b/homeassistant/components/yr/manifest.json @@ -1,7 +1,7 @@ { "domain": "yr", "name": "Yr", - "documentation": "https://www.home-assistant.io/components/yr", + "documentation": "https://www.home-assistant.io/integrations/yr", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/yweather/manifest.json b/homeassistant/components/yweather/manifest.json index c3048601595f85..482951e156f6ec 100644 --- a/homeassistant/components/yweather/manifest.json +++ b/homeassistant/components/yweather/manifest.json @@ -1,7 +1,7 @@ { "domain": "yweather", "name": "Yweather", - "documentation": "https://www.home-assistant.io/components/yweather", + "documentation": "https://www.home-assistant.io/integrations/yweather", "requirements": [ "yahooweather==0.10" ], diff --git a/homeassistant/components/zabbix/manifest.json b/homeassistant/components/zabbix/manifest.json index c0f100fa62ffa7..5959fba5fa2b17 100644 --- a/homeassistant/components/zabbix/manifest.json +++ b/homeassistant/components/zabbix/manifest.json @@ -1,7 +1,7 @@ { "domain": "zabbix", "name": "Zabbix", - "documentation": "https://www.home-assistant.io/components/zabbix", + "documentation": "https://www.home-assistant.io/integrations/zabbix", "requirements": [ "pyzabbix==0.7.4" ], diff --git a/homeassistant/components/zamg/manifest.json b/homeassistant/components/zamg/manifest.json index ce16e1b523c392..2b95a9ec0c7d9d 100644 --- a/homeassistant/components/zamg/manifest.json +++ b/homeassistant/components/zamg/manifest.json @@ -1,7 +1,7 @@ { "domain": "zamg", "name": "Zamg", - "documentation": "https://www.home-assistant.io/components/zamg", + "documentation": "https://www.home-assistant.io/integrations/zamg", "requirements": [], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/zengge/manifest.json b/homeassistant/components/zengge/manifest.json index b846c95f5fa57c..e24e4537837948 100644 --- a/homeassistant/components/zengge/manifest.json +++ b/homeassistant/components/zengge/manifest.json @@ -1,7 +1,7 @@ { "domain": "zengge", "name": "Zengge", - "documentation": "https://www.home-assistant.io/components/zengge", + "documentation": "https://www.home-assistant.io/integrations/zengge", "requirements": [ "zengge==0.2" ], diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 1461a54d147a7c..39f016e9d0e8ca 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -1,7 +1,7 @@ { "domain": "zeroconf", "name": "Zeroconf", - "documentation": "https://www.home-assistant.io/components/zeroconf", + "documentation": "https://www.home-assistant.io/integrations/zeroconf", "requirements": [ "zeroconf==0.23.0" ], diff --git a/homeassistant/components/zestimate/manifest.json b/homeassistant/components/zestimate/manifest.json index 4d1a55eaa09596..79c89406fac4dc 100644 --- a/homeassistant/components/zestimate/manifest.json +++ b/homeassistant/components/zestimate/manifest.json @@ -1,7 +1,7 @@ { "domain": "zestimate", "name": "Zestimate", - "documentation": "https://www.home-assistant.io/components/zestimate", + "documentation": "https://www.home-assistant.io/integrations/zestimate", "requirements": [ "xmltodict==0.12.0" ], diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index f0f6389a061e13..ab8a20822a1365 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -2,7 +2,7 @@ "domain": "zha", "name": "Zigbee Home Automation", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/zha", + "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.10.0", "zha-quirks==0.0.26", diff --git a/homeassistant/components/zhong_hong/manifest.json b/homeassistant/components/zhong_hong/manifest.json index 6382a830dcfdb4..32ee69643216b1 100644 --- a/homeassistant/components/zhong_hong/manifest.json +++ b/homeassistant/components/zhong_hong/manifest.json @@ -1,7 +1,7 @@ { "domain": "zhong_hong", "name": "Zhong hong", - "documentation": "https://www.home-assistant.io/components/zhong_hong", + "documentation": "https://www.home-assistant.io/integrations/zhong_hong", "requirements": [ "zhong_hong_hvac==1.0.9" ], diff --git a/homeassistant/components/zigbee/manifest.json b/homeassistant/components/zigbee/manifest.json index 1e4076b84392c5..3968b0e294f624 100644 --- a/homeassistant/components/zigbee/manifest.json +++ b/homeassistant/components/zigbee/manifest.json @@ -1,7 +1,7 @@ { "domain": "zigbee", "name": "Zigbee", - "documentation": "https://www.home-assistant.io/components/zigbee", + "documentation": "https://www.home-assistant.io/integrations/zigbee", "requirements": [ "xbee-helper==0.0.7" ], diff --git a/homeassistant/components/ziggo_mediabox_xl/manifest.json b/homeassistant/components/ziggo_mediabox_xl/manifest.json index 9e587137922e70..ff9e64ae78e5c2 100644 --- a/homeassistant/components/ziggo_mediabox_xl/manifest.json +++ b/homeassistant/components/ziggo_mediabox_xl/manifest.json @@ -1,7 +1,7 @@ { "domain": "ziggo_mediabox_xl", "name": "Ziggo mediabox xl", - "documentation": "https://www.home-assistant.io/components/ziggo_mediabox_xl", + "documentation": "https://www.home-assistant.io/integrations/ziggo_mediabox_xl", "requirements": [ "ziggo-mediabox-xl==1.1.0" ], diff --git a/homeassistant/components/zone/manifest.json b/homeassistant/components/zone/manifest.json index e9281fec3f785e..7a8cdcf6c6c0dc 100644 --- a/homeassistant/components/zone/manifest.json +++ b/homeassistant/components/zone/manifest.json @@ -2,7 +2,7 @@ "domain": "zone", "name": "Zone", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/zone", + "documentation": "https://www.home-assistant.io/integrations/zone", "requirements": [], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index 9d371fbabf7670..c29a97e857eca6 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -1,7 +1,7 @@ { "domain": "zoneminder", "name": "Zoneminder", - "documentation": "https://www.home-assistant.io/components/zoneminder", + "documentation": "https://www.home-assistant.io/integrations/zoneminder", "requirements": [ "zm-py==0.3.3" ], diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index f88945fa281279..9268a50a14d78c 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -2,7 +2,7 @@ "domain": "zwave", "name": "Z-Wave", "config_flow": true, - "documentation": "https://www.home-assistant.io/components/zwave", + "documentation": "https://www.home-assistant.io/integrations/zwave", "requirements": [ "homeassistant-pyozw==0.1.4", "pydispatcher==2.0.5" From c78b3a44391a79f65522a03c7dddafe785a4922d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 2 Oct 2019 17:27:13 +0100 Subject: [PATCH 0548/3953] Tweak geniushub and bump client to v0.6.26 (#26640) * use state attribute rather than type * HA style tweaks * small tweak * bump client * add more device_state_attributes * bump client * small tweak * bump client for concurrent IO * force snake_case, and refactor (consolidate) Devices/Zones * force snake_case, and refactor (consolidate) Devices/Zones 2 * force snake_case, and refactor (consolidate) Devices/Zones 3 * refactor last_comms / wakeup_interval check * movement sensor is dynamic, and tweaking * tweak * bump client to v0.6.20 * dummy * dummy 2 * bump client to handle another edge case * use entity_id fro zones * small tweak * bump client to 0.6.22 * add recursive snake_case converter * fix regression * fix regression 2 * fix regression 3 * remove Awaitables * don't dynamically create function every scan_interval * log kast_comms as localtime, delint dt_util * add sensors fro v1 API * tweak entity_id * bump client * bump client to v0.6.24 * bump client to v0.6.25 * explicit device attrs, dt as UTC * add unique_id, remove entity_id * Bump client to 0.6.26 - add Hub UID * remove convert_dict() * add mac_address (uid) for v1 API * tweak var names * add UID.upper() to avoid unwanted unique_id changes * Update homeassistant/components/geniushub/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/geniushub/__init__.py Co-Authored-By: Martin Hjelmare * remove underscores * refactor for broker * ready now * validate UID (MAC address) * move uid to broker * use existing constant * pass client to broker --- .../components/geniushub/__init__.py | 198 +++++++++++++++--- .../components/geniushub/binary_sensor.py | 49 ++--- homeassistant/components/geniushub/climate.py | 104 +++------ .../components/geniushub/manifest.json | 2 +- homeassistant/components/geniushub/sensor.py | 78 ++++--- .../components/geniushub/water_heater.py | 107 +++------- requirements_all.txt | 2 +- 7 files changed, 284 insertions(+), 256 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 45f3f91cd6d82a..d9f6c877cbcb39 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,14 +1,22 @@ """Support for a Genius Hub system.""" from datetime import timedelta import logging -from typing import Awaitable +from typing import Any, Dict, Optional import aiohttp import voluptuous as vol from geniushubclient import GeniusHub -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_HOST, + CONF_MAC, + CONF_PASSWORD, + CONF_TOKEN, + CONF_USERNAME, + TEMP_CELSIUS, +) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -19,39 +27,66 @@ ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +import homeassistant.util.dt as dt_util + +ATTR_DURATION = "duration" _LOGGER = logging.getLogger(__name__) DOMAIN = "geniushub" +# temperature is repeated here, as it gives access to high-precision temps +GH_ZONE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] +GH_DEVICE_ATTRS = { + "luminance": "luminance", + "measuredTemperature": "measured_temperature", + "occupancyTrigger": "occupancy_trigger", + "setback": "setback", + "setTemperature": "set_temperature", + "wakeupInterval": "wakeup_interval", +} + SCAN_INTERVAL = timedelta(seconds=60) -_V1_API_SCHEMA = vol.Schema({vol.Required(CONF_TOKEN): cv.string}) -_V3_API_SCHEMA = vol.Schema( +MAC_ADDRESS_REGEXP = r"^([0-9A-F]{2}:){5}([0-9A-F]{2})$" + +V1_API_SCHEMA = vol.Schema( + { + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP), + } +) +V3_API_SCHEMA = vol.Schema( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_MAC): vol.Match(MAC_ADDRESS_REGEXP), } ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Any(_V3_API_SCHEMA, _V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA + {DOMAIN: vol.Any(V3_API_SCHEMA, V1_API_SCHEMA)}, extra=vol.ALLOW_EXTRA ) -async def async_setup(hass, hass_config): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Create a Genius Hub system.""" - kwargs = dict(hass_config[DOMAIN]) + hass.data[DOMAIN] = {} + + kwargs = dict(config[DOMAIN]) if CONF_HOST in kwargs: args = (kwargs.pop(CONF_HOST),) else: args = (kwargs.pop(CONF_TOKEN),) + hub_uid = kwargs.pop(CONF_MAC, None) - hass.data[DOMAIN] = {} - broker = GeniusBroker(hass, args, kwargs) + client = GeniusHub(*args, **kwargs, session=async_get_clientsession(hass)) + + broker = hass.data[DOMAIN]["broker"] = GeniusBroker(hass, client, hub_uid) try: - await broker.client.update() + await client.update() except aiohttp.ClientResponseError as err: _LOGGER.error("Setup failed, check your configuration, %s", err) return False @@ -59,16 +94,8 @@ async def async_setup(hass, hass_config): 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 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) - ) + for platform in ["climate", "water_heater", "sensor", "binary_sensor"]: + hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) return True @@ -76,25 +103,30 @@ async def async_setup(hass, hass_config): class GeniusBroker: """Container for geniushub client and data.""" - def __init__(self, hass, args, kwargs): + def __init__(self, hass, client, hub_uid) -> None: """Initialize the geniushub client.""" self.hass = hass - self.client = hass.data[DOMAIN]["client"] = GeniusHub( - *args, **kwargs, session=async_get_clientsession(hass) - ) + self.client = client + self._hub_uid = hub_uid + + @property + def hub_uid(self) -> int: + """Return the Hub UID (MAC address).""" + # pylint: disable=no-member + return self._hub_uid if self._hub_uid is not None else self.client.uid - async def async_update(self, now, **kwargs): + async def async_update(self, now, **kwargs) -> None: """Update the geniushub client's data.""" try: await self.client.update() except aiohttp.ClientResponseError as err: - _LOGGER.warning("Update failed, %s", err) + _LOGGER.warning("Update failed, message is: %s", err) return self.make_debug_log_entries() async_dispatcher_send(self.hass, DOMAIN) - def make_debug_log_entries(self): + def make_debug_log_entries(self) -> None: """Make any useful debug log entries.""" # pylint: disable=protected-access _LOGGER.debug( @@ -105,13 +137,13 @@ def make_debug_log_entries(self): class GeniusEntity(Entity): - """Base for all Genius Hub endtities.""" + """Base for all Genius Hub entities.""" - def __init__(self): + def __init__(self) -> None: """Initialize the entity.""" - self._name = None + self._unique_id = self._name = None - async def async_added_to_hass(self) -> Awaitable[None]: + async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" async_dispatcher_connect(self.hass, DOMAIN, self._refresh) @@ -119,6 +151,11 @@ async def async_added_to_hass(self) -> Awaitable[None]: def _refresh(self) -> None: self.async_schedule_update_ha_state(force_refresh=True) + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + @property def name(self) -> str: """Return the name of the geniushub entity.""" @@ -128,3 +165,102 @@ def name(self) -> str: def should_poll(self) -> bool: """Return False as geniushub entities should not be polled.""" return False + + +class GeniusDevice(GeniusEntity): + """Base for all Genius Hub devices.""" + + def __init__(self, broker, device) -> None: + """Initialize the Device.""" + super().__init__() + + self._device = device + self._unique_id = f"{broker.hub_uid}_device_{device.id}" + + self._last_comms = self._state_attr = None + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the device state attributes.""" + + attrs = {} + attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] + if self._last_comms: + attrs["last_comms"] = self._last_comms.isoformat() + + state = dict(self._device.data["state"]) + if "_state" in self._device.data: # only for v3 API + state.update(self._device.data["_state"]) + + attrs["state"] = { + GH_DEVICE_ATTRS[k]: v for k, v in state.items() if k in GH_DEVICE_ATTRS + } + + return attrs + + async def async_update(self) -> None: + """Update an entity's state data.""" + if "_state" in self._device.data: # only for v3 API + self._last_comms = dt_util.utc_from_timestamp( + self._device.data["_state"]["lastComms"] + ) + + +class GeniusZone(GeniusEntity): + """Base for all Genius Hub zones.""" + + def __init__(self, broker, zone) -> None: + """Initialize the Zone.""" + super().__init__() + + self._zone = zone + self._unique_id = f"{broker.hub_uid}_device_{zone.id}" + + self._max_temp = self._min_temp = self._supported_features = None + + @property + def name(self) -> str: + """Return the name of the climate device.""" + return self._zone.name + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the device state attributes.""" + status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS} + return {"status": status} + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self._zone.data.get("temperature") + + @property + def target_temperature(self) -> float: + """Return the temperature we try to reach.""" + return self._zone.data["setpoint"] + + @property + def min_temp(self) -> float: + """Return max valid temperature that can be set.""" + return self._min_temp + + @property + def max_temp(self) -> float: + """Return max valid temperature that can be set.""" + return self._max_temp + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def supported_features(self) -> int: + """Return the bitmask of supported features.""" + return self._supported_features + + async def async_set_temperature(self, **kwargs) -> None: + """Set a new target temperature for this zone.""" + await self._zone.set_override( + kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600) + ) diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index 105a03bf757c68..33458d049a2841 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,52 +1,45 @@ """Support for Genius Hub binary_sensor devices.""" -from typing import Any, Dict - from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.util.dt import utc_from_timestamp +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusEntity +from . import DOMAIN, GeniusDevice -GH_IS_SWITCH = ["Dual Channel Receiver", "Electric Switch", "Smart Plug"] +GH_STATE_ATTR = "outputOnOff" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub sensor entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return + + broker = hass.data[DOMAIN]["broker"] switches = [ - GeniusBinarySensor(d) for d in client.device_objs if d.type[:21] in GH_IS_SWITCH + GeniusBinarySensor(broker, d, GH_STATE_ATTR) + for d in broker.client.device_objs + if GH_STATE_ATTR in d.data["state"] ] - async_add_entities(switches) + async_add_entities(switches, update_before_add=True) -class GeniusBinarySensor(GeniusEntity, BinarySensorDevice): +class GeniusBinarySensor(GeniusDevice, BinarySensorDevice): """Representation of a Genius Hub binary_sensor.""" - def __init__(self, device) -> None: + def __init__(self, broker, device, state_attr) -> None: """Initialize the binary sensor.""" - super().__init__() + super().__init__(broker, device) + + self._state_attr = state_attr - self._device = device if device.type[:21] == "Dual Channel Receiver": - self._name = f"Dual Channel Receiver {device.id}" + self._name = f"{device.type[:21]} {device.id}" else: self._name = f"{device.type} {device.id}" @property def is_on(self) -> bool: """Return the status of the sensor.""" - return self._device.data["state"]["outputOnOff"] - - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - attrs = {} - attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - - # 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() - - return {**attrs} + return self._device.data["state"][self._state_attr] diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index a856e48438fcd6..f27b1cc7f1aebc 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,5 +1,5 @@ """Support for Genius Hub climate devices.""" -from typing import Any, Awaitable, Dict, Optional, List +from typing import Optional, List from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -10,16 +10,9 @@ SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusEntity - -ATTR_DURATION = "duration" - -GH_ZONES = ["radiator"] - -# temperature is repeated here, as it gives access to high-precision temps -GH_STATE_ATTRS = ["mode", "temperature", "type", "occupied", "override"] +from . import DOMAIN, GeniusZone # GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes HA_HVAC_TO_GH = {HVAC_MODE_OFF: "off", HVAC_MODE_HEAT: "timer"} @@ -28,78 +21,43 @@ HA_PRESET_TO_GH = {PRESET_ACTIVITY: "footprint", PRESET_BOOST: "override"} GH_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_GH.items()} +GH_ZONES = ["radiator", "wet underfloor"] + async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub climate entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return + + broker = hass.data[DOMAIN]["broker"] - entities = [ - GeniusClimateZone(z) for z in client.zone_objs if z.data["type"] in GH_ZONES - ] - async_add_entities(entities) + async_add_entities( + [ + GeniusClimateZone(broker, z) + for z in broker.client.zone_objs + if z.data["type"] in GH_ZONES + ] + ) -class GeniusClimateZone(GeniusEntity, ClimateDevice): +class GeniusClimateZone(GeniusZone, ClimateDevice): """Representation of a Genius Hub climate device.""" - def __init__(self, zone) -> None: + def __init__(self, broker, zone) -> None: """Initialize the climate device.""" - super().__init__() + super().__init__(broker, zone) - 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] - - @property - def name(self) -> str: - """Return the name of the climate device.""" - return self._zone.name - - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - tmp = self._zone.data.items() - return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} + self._max_temp = 28.0 + self._min_temp = 4.0 + self._supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @property def icon(self) -> str: """Return the icon to use in the frontend UI.""" return "mdi:radiator" - @property - def current_temperature(self) -> Optional[float]: - """Return the current temperature.""" - return self._zone.data["temperature"] - - @property - def target_temperature(self) -> float: - """Return the temperature we try to reach.""" - return self._zone.data["setpoint"] - - @property - def min_temp(self) -> float: - """Return max valid temperature that can be set.""" - return 4.0 - - @property - def max_temp(self) -> float: - """Return max valid temperature that can be set.""" - return 28.0 - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE - @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" @@ -118,18 +76,14 @@ def preset_mode(self) -> Optional[str]: @property def preset_modes(self) -> Optional[List[str]]: """Return a list of available preset modes.""" - return self._preset_modes - - async def async_set_temperature(self, **kwargs) -> Awaitable[None]: - """Set a new target temperature for this zone.""" - await self._zone.set_override( - kwargs[ATTR_TEMPERATURE], kwargs.get(ATTR_DURATION, 3600) - ) + if "occupied" in self._zone.data: # if has a movement sensor + return [PRESET_ACTIVITY, PRESET_BOOST] + return [PRESET_BOOST] - async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set a new hvac mode.""" await self._zone.set_mode(HA_HVAC_TO_GH.get(hvac_mode)) - async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set a new preset mode.""" await self._zone.set_mode(HA_PRESET_TO_GH.get(preset_mode, "timer")) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index feedf3be607b71..96497388a4818c 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/integrations/geniushub", "requirements": [ - "geniushub-client==0.6.13" + "geniushub-client==0.6.26" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 82db3d4224e45a..2f5d9bceb8bcda 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -1,13 +1,14 @@ """Support for Genius Hub sensor devices.""" from datetime import timedelta -from typing import Any, Awaitable, Dict +from typing import Any, Dict from homeassistant.const import DEVICE_CLASS_BATTERY -from homeassistant.util.dt import utc_from_timestamp, utcnow +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +import homeassistant.util.dt as dt_util -from . import DOMAIN, GeniusEntity +from . import DOMAIN, GeniusDevice, GeniusEntity -GH_HAS_BATTERY = ["Room Thermostat", "Genius Valve", "Room Sensor", "Radiator Valve"] +GH_STATE_ATTR = "batteryLevel" GH_LEVEL_MAPPING = { "error": "Errors", @@ -16,42 +17,47 @@ } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub sensor entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return - 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)] + broker = hass.data[DOMAIN]["broker"] + + sensors = [ + GeniusBattery(broker, d, GH_STATE_ATTR) + for d in broker.client.device_objs + if GH_STATE_ATTR in d.data["state"] + ] + issues = [GeniusIssue(broker, i) for i in list(GH_LEVEL_MAPPING)] async_add_entities(sensors + issues, update_before_add=True) -class GeniusBattery(GeniusEntity): +class GeniusBattery(GeniusDevice): """Representation of a Genius Hub sensor.""" - def __init__(self, device) -> None: + def __init__(self, broker, device, state_attr) -> None: """Initialize the sensor.""" - super().__init__() + super().__init__(broker, device) + + self._state_attr = state_attr - self._device = device self._name = f"{device.type} {device.id}" @property def icon(self) -> str: """Return the icon of the sensor.""" - - values = self._device._raw["childValues"] # pylint: disable=protected-access - - last_comms = utc_from_timestamp(values["lastComms"]["val"]) - if "WakeUp_Interval" in values: - interval = timedelta(seconds=values["WakeUp_Interval"]["val"]) - else: - interval = timedelta(minutes=20) - - if last_comms < utcnow() - interval * 3: - return "mdi:battery-unknown" - - battery_level = self._device.data["state"]["batteryLevel"] + if "_state" in self._device.data: # only for v3 API + interval = timedelta( + seconds=self._device.data["_state"].get("wakeupInterval", 30 * 60) + ) + if self._last_comms < dt_util.utcnow() - interval * 3: + return "mdi:battery-unknown" + + battery_level = self._device.data["state"][self._state_attr] if battery_level == 255: return "mdi:battery-unknown" if battery_level < 40: @@ -76,31 +82,19 @@ def unit_of_measurement(self) -> str: @property def state(self) -> str: """Return the state of the sensor.""" - level = self._device.data["state"].get("batteryLevel", 255) + level = self._device.data["state"][self._state_attr] return level if level != 255 else 0 - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - attrs = {} - attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - - # 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(GeniusEntity): """Representation of a Genius Hub sensor.""" - def __init__(self, hub, level) -> None: + def __init__(self, broker, level) -> None: """Initialize the sensor.""" super().__init__() - self._hub = hub - self._name = GH_LEVEL_MAPPING[level] + self._hub = broker.client + self._name = f"GeniusHub {GH_LEVEL_MAPPING[level]}" self._level = level self._issues = [] @@ -114,7 +108,7 @@ def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" return {f"{self._level}_list": self._issues} - async def async_update(self) -> Awaitable[None]: + async def async_update(self) -> 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 1086160e77c864..cd4f536e14fa12 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -1,27 +1,20 @@ """Support for Genius Hub water_heater devices.""" -from typing import Any, Awaitable, Dict, Optional, List +from typing import 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.const import STATE_OFF +from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusEntity +from . import DOMAIN, GeniusZone STATE_AUTO = "auto" STATE_MANUAL = "manual" -GH_HEATERS = ["hot water temperature"] - -GH_SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE -# HA does not have SUPPORT_ON_OFF for water_heater - -GH_MAX_TEMP = 80.0 -GH_MIN_TEMP = 30.0 - -# Genius Hub HW supports only Off, Override/Boost & Timer modes +# Genius Hub HW zones support only Off, Override/Boost & Timer modes HA_OPMODE_TO_GH = {STATE_OFF: "off", STATE_AUTO: "timer", STATE_MANUAL: "override"} GH_STATE_TO_HA = { "off": STATE_OFF, @@ -34,91 +27,49 @@ "linked": None, "other": None, } -GH_STATE_ATTRS = ["type", "override"] + +GH_HEATERS = ["hot water temperature"] async def async_setup_platform( - hass, hass_config, async_add_entities, discovery_info=None -): + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: """Set up the Genius Hub water_heater entities.""" - client = hass.data[DOMAIN]["client"] + if discovery_info is None: + return - entities = [ - GeniusWaterHeater(z) for z in client.zone_objs if z.data["type"] in GH_HEATERS - ] + broker = hass.data[DOMAIN]["broker"] - async_add_entities(entities) + async_add_entities( + [ + GeniusWaterHeater(broker, z) + for z in broker.client.zone_objs + if z.data["type"] in GH_HEATERS + ] + ) -class GeniusWaterHeater(GeniusEntity, WaterHeaterDevice): +class GeniusWaterHeater(GeniusZone, WaterHeaterDevice): """Representation of a Genius Hub water_heater device.""" - def __init__(self, boiler) -> None: + def __init__(self, broker, zone) -> None: """Initialize the water_heater device.""" - super().__init__() + super().__init__(broker, zone) - self._boiler = boiler - self._operation_list = list(HA_OPMODE_TO_GH) - - @property - def name(self) -> str: - """Return the name of the water_heater device.""" - return self._boiler.name - - @property - def device_state_attributes(self) -> Dict[str, Any]: - """Return the device state attributes.""" - return { - "status": { - k: v for k, v in self._boiler.data.items() if k in GH_STATE_ATTRS - } - } - - @property - def current_temperature(self) -> Optional[float]: - """Return the current temperature.""" - return self._boiler.data.get("temperature") - - @property - def target_temperature(self) -> float: - """Return the temperature we try to reach.""" - return self._boiler.data["setpoint"] - - @property - def min_temp(self) -> float: - """Return max valid temperature that can be set.""" - return GH_MIN_TEMP - - @property - def max_temp(self) -> float: - """Return max valid temperature that can be set.""" - return GH_MAX_TEMP - - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return GH_SUPPORT_FLAGS + self._max_temp = 80.0 + self._min_temp = 30.0 + self._supported_features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE @property def operation_list(self) -> List[str]: """Return the list of available operation modes.""" - return self._operation_list + return list(HA_OPMODE_TO_GH) @property def current_operation(self) -> str: """Return the current operation mode.""" - return GH_STATE_TO_HA[self._boiler.data["mode"]] + return GH_STATE_TO_HA[self._zone.data["mode"]] - async def async_set_operation_mode(self, operation_mode) -> Awaitable[None]: + async def async_set_operation_mode(self, operation_mode) -> 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) -> Awaitable[None]: - """Set a new target temperature for this boiler.""" - temperature = kwargs[ATTR_TEMPERATURE] - await self._boiler.set_override(temperature, 3600) # 1 hour + await self._zone.set_mode(HA_OPMODE_TO_GH[operation_mode]) diff --git a/requirements_all.txt b/requirements_all.txt index 2cf2c765e8b0e1..a0f64eeec74b9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -525,7 +525,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.6.13 +geniushub-client==0.6.26 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From 04ead6f273d4c179ded4c7bc0fa293ef86e9a702 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Wed, 2 Oct 2019 18:33:47 +0200 Subject: [PATCH 0549/3953] move ATTR_MODE to homeassistant.const (#27118) --- homeassistant/components/flux_led/light.py | 3 +-- homeassistant/components/gpsd/sensor.py | 2 +- homeassistant/components/here_travel_time/sensor.py | 2 +- homeassistant/components/homematic/__init__.py | 2 +- homeassistant/components/input_number/__init__.py | 2 +- homeassistant/components/input_text/__init__.py | 2 +- homeassistant/components/lifx/light.py | 3 +-- homeassistant/components/logi_circle/__init__.py | 2 +- homeassistant/components/neato/vacuum.py | 3 +-- homeassistant/components/opentherm_gw/__init__.py | 2 +- homeassistant/components/opentherm_gw/const.py | 1 - homeassistant/components/transport_nsw/sensor.py | 3 +-- homeassistant/components/wink/lock.py | 9 +++++++-- homeassistant/components/xiaomi_miio/fan.py | 9 +++++++-- homeassistant/components/xiaomi_miio/switch.py | 9 +++++++-- homeassistant/components/yeelight/light.py | 3 +-- homeassistant/const.py | 2 ++ 17 files changed, 35 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 23fdb38aa05f7b..0a95de783fa273 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -5,7 +5,7 @@ import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL +from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL, ATTR_MODE from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, @@ -30,7 +30,6 @@ CONF_COLORS = "colors" CONF_SPEED_PCT = "speed_pct" CONF_TRANSITION = "transition" -ATTR_MODE = "mode" DOMAIN = "flux_led" diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index ab4545256ae3e3..197e424ce86e69 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -7,6 +7,7 @@ from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, + ATTR_MODE, CONF_HOST, CONF_PORT, CONF_NAME, @@ -19,7 +20,6 @@ ATTR_CLIMB = "climb" ATTR_ELEVATION = "elevation" ATTR_GPS_TIME = "gps_time" -ATTR_MODE = "mode" ATTR_SPEED = "speed" DEFAULT_HOST = "localhost" diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 8fd4b4fe94a5cb..b752b82d08750b 100755 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -11,6 +11,7 @@ ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + ATTR_MODE, CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM, @@ -77,7 +78,6 @@ ATTR_ORIGIN = "origin" ATTR_DESTINATION = "destination" -ATTR_MODE = "mode" ATTR_UNIT_SYSTEM = CONF_UNIT_SYSTEM ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 598e3765612420..cd791434f90072 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -7,6 +7,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_MODE, ATTR_NAME, CONF_HOST, CONF_HOSTS, @@ -47,7 +48,6 @@ ATTR_INTERFACE = "interface" ATTR_ERRORCODE = "error" ATTR_MESSAGE = "message" -ATTR_MODE = "mode" ATTR_TIME = "time" ATTR_UNIQUE_ID = "unique_id" ATTR_PARAMSET_KEY = "paramset_key" diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 007ed6517efe47..9b4d5a961ba3a0 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -7,6 +7,7 @@ from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + ATTR_MODE, CONF_ICON, CONF_NAME, CONF_MODE, @@ -32,7 +33,6 @@ ATTR_MIN = "min" ATTR_MAX = "max" ATTR_STEP = "step" -ATTR_MODE = "mode" SERVICE_SET_VALUE = "set_value" SERVICE_INCREMENT = "increment" diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 41d78e6e7c540c..1b4670cf1e6443 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -7,6 +7,7 @@ from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + ATTR_MODE, CONF_ICON, CONF_NAME, CONF_MODE, @@ -30,7 +31,6 @@ ATTR_MIN = "min" ATTR_MAX = "max" ATTR_PATTERN = "pattern" -ATTR_MODE = "mode" SERVICE_SET_VALUE = "set_value" diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index ed26db3d49e535..d183dcb0fa2627 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -33,7 +33,7 @@ Light, preprocess_turn_on_alternatives, ) -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr @@ -77,7 +77,6 @@ SERVICE_EFFECT_STOP = "lifx_effect_stop" ATTR_POWER_ON = "power_on" -ATTR_MODE = "mode" ATTR_PERIOD = "period" ATTR_CYCLES = "cycles" ATTR_SPREAD = "spread" diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 12484a655d635f..f7ed3a73fce033 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components.camera import ATTR_FILENAME, CAMERA_SERVICE_SCHEMA from homeassistant.const import ( + ATTR_MODE, CONF_MONITORED_CONDITIONS, CONF_SENSORS, EVENT_HOMEASSISTANT_STOP, @@ -42,7 +43,6 @@ SERVICE_LIVESTREAM_SNAPSHOT = "livestream_snapshot" SERVICE_LIVESTREAM_RECORD = "livestream_record" -ATTR_MODE = "mode" ATTR_VALUE = "value" ATTR_DURATION = "duration" diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 93fe285dcfdb69..f284b2eda1ed25 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -27,7 +27,7 @@ SUPPORT_STOP, StateVacuumDevice, ) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids @@ -66,7 +66,6 @@ ATTR_CLEAN_SUSP_COUNT = "clean_suspension_count" ATTR_CLEAN_SUSP_TIME = "clean_suspension_time" -ATTR_MODE = "mode" ATTR_NAVIGATION = "navigation" ATTR_CATEGORY = "category" ATTR_ZONE = "zone" diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 0c145963653c3e..a32c375ac65258 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ATTR_DATE, ATTR_ID, + ATTR_MODE, ATTR_TEMPERATURE, ATTR_TIME, CONF_DEVICE, @@ -28,7 +29,6 @@ from .const import ( ATTR_GW_ID, - ATTR_MODE, ATTR_LEVEL, ATTR_DHW_OVRD, CONF_CLIMATE, diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 77b0bf9b313c8e..60042b92867ca4 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -4,7 +4,6 @@ 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" diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 5f08d0a4750718..9d0610c139e910 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -7,7 +7,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_API_KEY, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_MODE, CONF_NAME, CONF_API_KEY, ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -17,7 +17,6 @@ ATTR_DELAY = "delay" ATTR_REAL_TIME = "real_time" ATTR_DESTINATION = "destination" -ATTR_MODE = "mode" ATTRIBUTION = "Data provided by Transport NSW" diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py index 0d4d373b2b65b0..5246fb49eed23e 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -4,7 +4,13 @@ import voluptuous as vol from homeassistant.components.lock import LockDevice -from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, ATTR_NAME, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_CODE, + ATTR_ENTITY_ID, + ATTR_MODE, + ATTR_NAME, + STATE_UNKNOWN, +) import homeassistant.helpers.config_validation as cv from . import DOMAIN, WinkDevice @@ -20,7 +26,6 @@ ATTR_ENABLED = "enabled" ATTR_SENSITIVITY = "sensitivity" -ATTR_MODE = "mode" ALARM_SENSITIVITY_MAP = { "low": 0.2, diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index c6ca6db32fbce6..67dc12565d8680 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -12,7 +12,13 @@ SUPPORT_SET_SPEED, DOMAIN, ) -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_TOKEN, ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_MODE, + CONF_NAME, + CONF_HOST, + CONF_TOKEN, + ATTR_ENTITY_ID, +) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -75,7 +81,6 @@ ATTR_TEMPERATURE = "temperature" ATTR_HUMIDITY = "humidity" ATTR_AIR_QUALITY_INDEX = "aqi" -ATTR_MODE = "mode" ATTR_FILTER_HOURS_USED = "filter_hours_used" ATTR_FILTER_LIFE = "filter_life_remaining" ATTR_FAVORITE_LEVEL = "favorite_level" diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 5f79652621bab3..7fa1638253cd24 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -6,7 +6,13 @@ import voluptuous as vol from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_MODE, + CONF_HOST, + CONF_NAME, + CONF_TOKEN, +) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -44,7 +50,6 @@ ATTR_TEMPERATURE = "temperature" ATTR_LOAD_POWER = "load_power" ATTR_MODEL = "model" -ATTR_MODE = "mode" ATTR_POWER_MODE = "power_mode" ATTR_WIFI_LED = "wifi_led" ATTR_POWER_PRICE = "power_price" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index b47cdb981612e4..ab63e6fb319455 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -11,7 +11,7 @@ color_temperature_mired_to_kelvin as mired_to_kelvin, color_temperature_kelvin_to_mired as kelvin_to_mired, ) -from homeassistant.const import CONF_HOST, ATTR_ENTITY_ID, CONF_NAME +from homeassistant.const import CONF_HOST, ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME from homeassistant.core import callback from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -64,7 +64,6 @@ SUPPORT_YEELIGHT_RGB = SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR -ATTR_MODE = "mode" ATTR_MINUTES = "minutes" SERVICE_SET_MODE = "set_mode" diff --git a/homeassistant/const.py b/homeassistant/const.py index 9aa4544f5c34e7..7d8a68a9707ad4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -271,6 +271,8 @@ # Location of the device/sensor ATTR_LOCATION = "location" +ATTR_MODE = "mode" + ATTR_BATTERY_CHARGING = "battery_charging" ATTR_BATTERY_LEVEL = "battery_level" ATTR_WAKEUP = "wake_up_interval" From d4a67e3a300944c53f8f2aeffc1091e199437bf6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 2 Oct 2019 18:34:07 +0200 Subject: [PATCH 0550/3953] Update documentation link URL for integrations (part2) (#27117) --- .github/ISSUE_TEMPLATE.md | 4 ++-- .github/ISSUE_TEMPLATE/Bug_report.md | 4 ++-- README.rst | 2 +- homeassistant/components/ambiclimate/.translations/en.json | 2 +- homeassistant/components/deconz/services.yaml | 2 +- homeassistant/components/dialogflow/config_flow.py | 2 +- homeassistant/components/geofency/config_flow.py | 2 +- homeassistant/components/gpslogger/config_flow.py | 2 +- homeassistant/components/honeywell/climate.py | 2 +- homeassistant/components/ifttt/config_flow.py | 2 +- homeassistant/components/izone/__init__.py | 2 +- homeassistant/components/life360/config_flow.py | 2 +- homeassistant/components/locative/config_flow.py | 2 +- homeassistant/components/logi_circle/.translations/en.json | 2 +- homeassistant/components/mailgun/config_flow.py | 2 +- homeassistant/components/nest/.translations/en.json | 2 +- homeassistant/components/opentherm_gw/services.yaml | 4 ++-- homeassistant/components/owntracks/config_flow.py | 2 +- homeassistant/components/plaato/config_flow.py | 4 +++- homeassistant/components/point/.translations/en.json | 2 +- homeassistant/components/ps4/.translations/en.json | 6 +++--- homeassistant/components/smarthab/__init__.py | 2 +- homeassistant/components/smarthab/cover.py | 2 +- homeassistant/components/smarthab/light.py | 2 +- homeassistant/components/smartthings/config_flow.py | 2 +- homeassistant/components/somfy/__init__.py | 2 +- homeassistant/components/toon/.translations/en.json | 2 +- homeassistant/components/traccar/config_flow.py | 2 +- homeassistant/components/twilio/config_flow.py | 2 +- homeassistant/components/zha/core/__init__.py | 2 +- homeassistant/components/zha/core/channels/__init__.py | 2 +- homeassistant/components/zha/core/channels/closures.py | 2 +- homeassistant/components/zha/core/channels/general.py | 2 +- .../components/zha/core/channels/homeautomation.py | 2 +- homeassistant/components/zha/core/channels/hvac.py | 2 +- homeassistant/components/zha/core/channels/lighting.py | 2 +- homeassistant/components/zha/core/channels/lightlink.py | 2 +- .../components/zha/core/channels/manufacturerspecific.py | 2 +- homeassistant/components/zha/core/channels/measurement.py | 2 +- homeassistant/components/zha/core/channels/protocol.py | 2 +- homeassistant/components/zha/core/channels/security.py | 2 +- homeassistant/components/zha/core/channels/smartenergy.py | 2 +- homeassistant/components/zha/core/device.py | 2 +- homeassistant/components/zha/core/discovery.py | 2 +- homeassistant/components/zha/core/gateway.py | 2 +- homeassistant/components/zha/core/helpers.py | 2 +- homeassistant/components/zha/core/patches.py | 2 +- homeassistant/components/zha/core/registries.py | 2 +- homeassistant/config.py | 4 ++-- .../templates/integration/integration/manifest.json | 2 +- 50 files changed, 58 insertions(+), 56 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 28dade82d9878e..1af7fc0490e679 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -23,9 +23,9 @@ Please provide details about your environment. --> -**Component/platform:** +**Integration:** diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 3b962f38caf2ad..885164d7a3469f 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -29,9 +29,9 @@ about: Create a report to help us improve Please provide details about your environment. --> -**Component/platform:** +**Integration:** diff --git a/README.rst b/README.rst index 08f20778d701af..ae9531456fdbba 100644 --- a/README.rst +++ b/README.rst @@ -32,4 +32,4 @@ of a component, check the `Home Assistant help section Mode to set on the GPIO pin. Values 0 through 6 are accepted for both GPIOs, 7 is only accepted for GPIO "B". - See https://www.home-assistant.io/components/opentherm_gw/#gpio-modes for an explanation of the values. + See https://www.home-assistant.io/integrations/opentherm_gw/#gpio-modes for an explanation of the values. example: '5' set_led_mode: @@ -79,7 +79,7 @@ set_led_mode: mode: description: > The function to assign to the LED. One of "R", "X", "T", "B", "O", "F", "H", "W", "C", "E", "M" or "P". - See https://www.home-assistant.io/components/opentherm_gw/#led-modes for an explanation of the values. + See https://www.home-assistant.io/integrations/opentherm_gw/#led-modes for an explanation of the values. example: 'F' set_max_modulation: diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 93a75d44e9b3fb..67553ef608ffdb 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -58,7 +58,7 @@ async def async_step_user(self, user_input=None): "android_url": "https://play.google.com/store/apps/details?" "id=org.owntracks.android", "ios_url": "https://itunes.apple.com/us/app/owntracks/id692424691?mt=8", - "docs_url": "https://www.home-assistant.io/components/owntracks/", + "docs_url": "https://www.home-assistant.io/integrations/owntracks/", }, ) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 2790d9a93acb7b..59cb270c616af4 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -3,5 +3,7 @@ from .const import DOMAIN config_entry_flow.register_webhook_flow( - DOMAIN, "Webhook", {"docs_url": "https://www.home-assistant.io/components/plaato/"} + DOMAIN, + "Webhook", + {"docs_url": "https://www.home-assistant.io/integrations/plaato/"}, ) diff --git a/homeassistant/components/point/.translations/en.json b/homeassistant/components/point/.translations/en.json index 705ac59b98d010..25f0545c340942 100644 --- a/homeassistant/components/point/.translations/en.json +++ b/homeassistant/components/point/.translations/en.json @@ -5,7 +5,7 @@ "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", "external_setup": "Point successfully configured from another flow.", - "no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/point/)." + "no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/point/)." }, "create_entry": { "default": "Successfully authenticated with Minut for your Point device(s)" diff --git a/homeassistant/components/ps4/.translations/en.json b/homeassistant/components/ps4/.translations/en.json index 756eb65d4f7c58..3a7223ade29d99 100644 --- a/homeassistant/components/ps4/.translations/en.json +++ b/homeassistant/components/ps4/.translations/en.json @@ -4,8 +4,8 @@ "credential_error": "Error fetching credentials.", "devices_configured": "All devices found are already configured.", "no_devices_found": "No PlayStation 4 devices found on the network.", - "port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", - "port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info." + "port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info.", + "port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info." }, "error": { "credential_timeout": "Credential service timed out. Press submit to restart.", @@ -25,7 +25,7 @@ "name": "Name", "region": "Region" }, - "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", + "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index 198a5e9cabcc1e..7206bea110ba38 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -2,7 +2,7 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/smarthab/ +https://home-assistant.io/integrations/smarthab/ """ import logging diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py index 2ae9cadf1acc75..3d5b4259aa9cf5 100644 --- a/homeassistant/components/smarthab/cover.py +++ b/homeassistant/components/smarthab/cover.py @@ -2,7 +2,7 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/smarthab/ +https://home-assistant.io/integrations/smarthab/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/smarthab/light.py b/homeassistant/components/smarthab/light.py index 0f7b3c9ef80785..a8a55dea48a739 100644 --- a/homeassistant/components/smarthab/light.py +++ b/homeassistant/components/smarthab/light.py @@ -2,7 +2,7 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/smarthab/ +https://home-assistant.io/integrations/smarthab/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/smartthings/config_flow.py b/homeassistant/components/smartthings/config_flow.py index a3ca8fc7629991..54c9f815008e16 100644 --- a/homeassistant/components/smartthings/config_flow.py +++ b/homeassistant/components/smartthings/config_flow.py @@ -176,7 +176,7 @@ def _show_step_user(self, errors): errors=errors, description_placeholders={ "token_url": "https://account.smartthings.com/tokens", - "component_url": "https://www.home-assistant.io/components/smartthings/", + "component_url": "https://www.home-assistant.io/integrations/smartthings/", }, ) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 8c7edb26d46f39..2c7c71d7a69686 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -2,7 +2,7 @@ Support for Somfy hubs. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/somfy/ +https://home-assistant.io/integrations/somfy/ """ import logging from datetime import timedelta diff --git a/homeassistant/components/toon/.translations/en.json b/homeassistant/components/toon/.translations/en.json index cea3146a3a557f..dde5165c5c17f7 100644 --- a/homeassistant/components/toon/.translations/en.json +++ b/homeassistant/components/toon/.translations/en.json @@ -4,7 +4,7 @@ "client_id": "The client ID from the configuration is invalid.", "client_secret": "The client secret from the configuration is invalid.", "no_agreements": "This account has no Toon displays.", - "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/).", + "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/toon/).", "unknown_auth_fail": "Unexpected error occured, while authenticating." }, "error": { diff --git a/homeassistant/components/traccar/config_flow.py b/homeassistant/components/traccar/config_flow.py index cc3f1f2372771e..4bd75910163fdd 100644 --- a/homeassistant/components/traccar/config_flow.py +++ b/homeassistant/components/traccar/config_flow.py @@ -6,5 +6,5 @@ config_entry_flow.register_webhook_flow( DOMAIN, "Traccar Webhook", - {"docs_url": "https://www.home-assistant.io/components/traccar/"}, + {"docs_url": "https://www.home-assistant.io/integrations/traccar/"}, ) diff --git a/homeassistant/components/twilio/config_flow.py b/homeassistant/components/twilio/config_flow.py index 1408e05e738edf..dad8e0bf4966a3 100644 --- a/homeassistant/components/twilio/config_flow.py +++ b/homeassistant/components/twilio/config_flow.py @@ -9,6 +9,6 @@ "Twilio Webhook", { "twilio_url": "https://www.twilio.com/docs/glossary/what-is-a-webhook", - "docs_url": "https://www.home-assistant.io/components/twilio/", + "docs_url": "https://www.home-assistant.io/integrations/twilio/", }, ) diff --git a/homeassistant/components/zha/core/__init__.py b/homeassistant/components/zha/core/__init__.py index 145b725fc7968b..1873cd7dc55be3 100644 --- a/homeassistant/components/zha/core/__init__.py +++ b/homeassistant/components/zha/core/__init__.py @@ -2,7 +2,7 @@ Core module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ # flake8: noqa diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 3d4a03fb0acac0..37b0bec207b986 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -2,7 +2,7 @@ Channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio from concurrent.futures import TimeoutError as Timeout diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 378be778e6f565..16592c9a8df029 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -2,7 +2,7 @@ Closures channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index f67ee2fb75ac77..7afde3e5f781fd 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -2,7 +2,7 @@ General channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 7a5f0161fb4385..dda6c1f4c13f3b 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -2,7 +2,7 @@ Home automation channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 2f6e6c1b3e8390..14d982ab1e8e9f 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -2,7 +2,7 @@ HVAC channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index d8f769a3e24399..272fa28905cfb9 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -2,7 +2,7 @@ Lighting channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index 99fed7d5d68617..7cd2134988d367 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -2,7 +2,7 @@ Lightlink channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index e15acdaf5e31bd..31dd5cd63d14ea 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -2,7 +2,7 @@ Manufacturer specific channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 94d885592eb39c..369ecb69aa1020 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -2,7 +2,7 @@ Measurement channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/protocol.py b/homeassistant/components/zha/core/channels/protocol.py index b9785068f21d46..aa463392e557c1 100644 --- a/homeassistant/components/zha/core/channels/protocol.py +++ b/homeassistant/components/zha/core/channels/protocol.py @@ -2,7 +2,7 @@ Protocol channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 25c11a9fd4f1cd..e4840dae86dabc 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -2,7 +2,7 @@ Security channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 8e2fa7e3d5a3f2..c7de2943691a5f 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -2,7 +2,7 @@ Smart energy channels module for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 82d20ff78c207e..e9e2c3b7ea6aaf 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -2,7 +2,7 @@ Device for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio from datetime import timedelta diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 80642a373da7e4..622adead803f1c 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -2,7 +2,7 @@ Device discovery functions for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import logging diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index d2f842956dabef..a64e8cf7fd98ed 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -2,7 +2,7 @@ Virtual gateway for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index b07658e72d01eb..88a472716cc580 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -2,7 +2,7 @@ Helpers for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ import asyncio import collections diff --git a/homeassistant/components/zha/core/patches.py b/homeassistant/components/zha/core/patches.py index d64839026025ea..a4e84e83105805 100644 --- a/homeassistant/components/zha/core/patches.py +++ b/homeassistant/components/zha/core/patches.py @@ -2,7 +2,7 @@ Patch functions for Zigbee Home Automation. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ +https://home-assistant.io/integrations/zha/ """ diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index db7e89dce822c7..43ddc888d2fda0 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -2,7 +2,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/ +https://home-assistant.io/integrations/zha/ """ import collections diff --git a/homeassistant/config.py b/homeassistant/config.py index 0e840e1d003062..97c996d9e59380 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -60,7 +60,7 @@ DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors" RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") RE_ASCII = re.compile(r"\033\[[^m]*m") -HA_COMPONENT_URL = "[{}](https://home-assistant.io/components/{}/)" +HA_COMPONENT_URL = "[{}](https://home-assistant.io/integrations/{}/)" YAML_CONFIG_FILE = "configuration.yaml" VERSION_FILE = ".HA_VERSION" CONFIG_DIR_NAME = ".homeassistant" @@ -462,7 +462,7 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: if domain != CONF_CORE: message += ( "Please check the docs at " - "https://home-assistant.io/components/{}/".format(domain) + "https://home-assistant.io/integrations/{}/".format(domain) ) return message diff --git a/script/scaffold/templates/integration/integration/manifest.json b/script/scaffold/templates/integration/integration/manifest.json index cb4ecac61fb76c..0bc54519ce9de8 100644 --- a/script/scaffold/templates/integration/integration/manifest.json +++ b/script/scaffold/templates/integration/integration/manifest.json @@ -2,7 +2,7 @@ "domain": "NEW_DOMAIN", "name": "NEW_NAME", "config_flow": false, - "documentation": "https://www.home-assistant.io/components/NEW_DOMAIN", + "documentation": "https://www.home-assistant.io/integrations/NEW_DOMAIN", "requirements": [], "ssdp": {}, "homekit": {}, From 9c49b8dfc1b9723abe77fb7bb975f94b5233ad00 Mon Sep 17 00:00:00 2001 From: Felix Eckhofer Date: Wed, 2 Oct 2019 18:34:27 +0200 Subject: [PATCH 0551/3953] Fix generated comment in CODEOWNERS (#27115) codeowners.py was moved from `/script/manifest/` to `/script/hassfest/` in e8343452cd4702a61166fd74d78323bf95092f7c. --- CODEOWNERS | 2 +- script/hassfest/codeowners.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index f5cd03882c5944..2bfebf145dfada 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,4 +1,4 @@ -# This file is generated by script/manifest/codeowners.py +# This file is generated by script/hassfest/codeowners.py # People marked here will be automatically requested for a review # when the code that they own is touched. # https://github.com/blog/2392-introducing-code-owners diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 1341bd75d1b0cf..6f63fab3fdb8ba 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -4,7 +4,7 @@ from .model import Integration, Config BASE = """ -# This file is generated by script/manifest/codeowners.py +# This file is generated by script/hassfest/codeowners.py # People marked here will be automatically requested for a review # when the code that they own is touched. # https://github.com/blog/2392-introducing-code-owners From 0eb1d490467c40a6d46a6b94851548b1a5ef2eb8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Oct 2019 20:52:15 +0200 Subject: [PATCH 0552/3953] Disable flaky/slow test (#27125) --- tests/components/ecobee/test_config_flow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 7b4d1f96a3761c..4008e6a17b18b4 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the ecobee config flow.""" +import pytest from unittest.mock import patch from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN @@ -116,6 +117,7 @@ async def test_token_request_fails(hass): assert result["description_placeholders"] == {"pin": "test-pin"} +@pytest.mark.skip(reason="Flaky/slow") async def test_import_flow_triggered_but_no_ecobee_conf(hass): """Test expected result if import flow triggers but ecobee.conf doesn't exist.""" flow = config_flow.EcobeeFlowHandler() From 09c5b9feb35b70c96848b8796b6d4a747de998f6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 2 Oct 2019 21:43:14 +0200 Subject: [PATCH 0553/3953] UniFi - Try to handle when UniFi erroneously marks offline client as wired (#26960) * Add controls to catch when client goes offline and UniFi bug marks client as wired * Device trackers shouldn't jump between going away and home * POE control shouldn't add normally wireless clients as POE control switches --- homeassistant/components/unifi/__init__.py | 55 +++++++++++++++++-- homeassistant/components/unifi/const.py | 1 + homeassistant/components/unifi/controller.py | 24 ++++++++ .../components/unifi/device_tracker.py | 31 ++++++++--- homeassistant/components/unifi/switch.py | 5 +- tests/components/unifi/test_controller.py | 14 +++-- tests/components/unifi/test_device_tracker.py | 48 +++++++++++++++- tests/components/unifi/test_switch.py | 5 +- 8 files changed, 161 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index db6358285296a3..5b43289e403b13 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,14 +1,13 @@ """Support for devices connected to UniFi POE.""" import voluptuous as vol -from homeassistant.components.unifi.config_flow import ( - get_controller_id_from_config_entry, -) from homeassistant.const import CONF_HOST +from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC import homeassistant.helpers.config_validation as cv +from .config_flow import get_controller_id_from_config_entry from .const import ( ATTR_MANUFACTURER, CONF_BLOCK_CLIENT, @@ -20,9 +19,14 @@ CONF_SSID_FILTER, DOMAIN, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from .controller import UniFiController +SAVE_DELAY = 10 +STORAGE_KEY = "unifi_data" +STORAGE_VERSION = 1 + CONF_CONTROLLERS = "controllers" CONTROLLER_SCHEMA = vol.Schema( @@ -61,6 +65,9 @@ async def async_setup(hass, config): if DOMAIN in config: hass.data[UNIFI_CONFIG] = config[DOMAIN][CONF_CONTROLLERS] + hass.data[UNIFI_WIRELESS_CLIENTS] = wireless_clients = UnifiWirelessClients(hass) + await wireless_clients.async_load() + return True @@ -70,9 +77,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = {} controller = UniFiController(hass, config_entry) - controller_id = get_controller_id_from_config_entry(config_entry) - hass.data[DOMAIN][controller_id] = controller if not await controller.async_setup(): @@ -99,3 +104,43 @@ async def async_unload_entry(hass, config_entry): controller_id = get_controller_id_from_config_entry(config_entry) controller = hass.data[DOMAIN].pop(controller_id) return await controller.async_reset() + + +class UnifiWirelessClients: + """Class to store clients known to be wireless. + + This is needed since wireless devices going offline might get marked as wired by UniFi. + """ + + def __init__(self, hass): + """Set up client storage.""" + self.hass = hass + self.data = {} + self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + async def async_load(self): + """Load data from file.""" + data = await self._store.async_load() + + if data is not None: + self.data = data + + @callback + def get_data(self, config_entry): + """Get data related to a specific controller.""" + controller_id = get_controller_id_from_config_entry(config_entry) + data = self.data.get(controller_id, {"wireless_devices": []}) + return set(data["wireless_devices"]) + + @callback + def update_data(self, data, config_entry): + """Update data and schedule to save to file.""" + controller_id = get_controller_id_from_config_entry(config_entry) + self.data[controller_id] = {"wireless_devices": list(data)} + + self._store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self): + """Return data of UniFi wireless clients to store in a file.""" + return self.data diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 4522ac4254a0c9..eac14735074109 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -10,6 +10,7 @@ CONF_SITE_ID = "site" UNIFI_CONFIG = "unifi_config" +UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index b29b088a815f51..ffea98b90502ff 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -36,6 +36,7 @@ DOMAIN, LOGGER, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from .errors import AuthenticationRequired, CannotConnect @@ -50,6 +51,7 @@ def __init__(self, hass, config_entry): self.available = True self.api = None self.progress = None + self.wireless_clients = None self._site_name = None self._site_role = None @@ -128,6 +130,22 @@ def signal_options_update(self): """Event specific per UniFi entry to signal new options.""" return f"unifi-options-{CONTROLLER_ID.format(host=self.host, site=self.site)}" + def update_wireless_clients(self): + """Update set of known to be wireless clients.""" + new_wireless_clients = set() + + for client_id in self.api.clients: + if ( + client_id not in self.wireless_clients + and not self.api.clients[client_id].is_wired + ): + new_wireless_clients.add(client_id) + + if new_wireless_clients: + self.wireless_clients |= new_wireless_clients + unifi_wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] + unifi_wireless_clients.update_data(self.wireless_clients, self.config_entry) + async def request_update(self): """Request an update.""" if self.progress is not None: @@ -170,6 +188,8 @@ async def async_update(self): LOGGER.info("Reconnected to controller %s", self.host) self.available = True + self.update_wireless_clients() + async_dispatcher_send(self.hass, self.signal_update) async def async_setup(self): @@ -197,6 +217,10 @@ async def async_setup(self): LOGGER.error("Unknown error connecting with UniFi controller: %s", err) return False + wireless_clients = hass.data[UNIFI_WIRELESS_CLIENTS] + self.wireless_clients = wireless_clients.get_data(self.config_entry) + self.update_wireless_clients() + self.import_configuration() self.config_entry.add_update_listener(self.async_options_updated) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index ad04b8a0eb37da..48b19d7bada4ca 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -26,7 +26,6 @@ "ip", "is_11r", "is_guest", - "is_wired", "mac", "name", "noted", @@ -121,6 +120,7 @@ def __init__(self, client, controller): """Set up tracked client.""" self.client = client self.controller = controller + self.is_wired = self.client.mac not in controller.wireless_clients @property def entity_registry_enabled_default(self): @@ -129,13 +129,13 @@ def entity_registry_enabled_default(self): return False if ( - not self.client.is_wired + not self.is_wired and self.controller.option_ssid_filter and self.client.essid not in self.controller.option_ssid_filter ): return False - if not self.controller.option_track_wired_clients and self.client.is_wired: + if not self.controller.option_track_wired_clients and self.is_wired: return False return True @@ -145,18 +145,31 @@ async def async_added_to_hass(self): LOGGER.debug("New UniFi client tracker %s (%s)", self.name, self.client.mac) async def async_update(self): - """Synchronize state with controller.""" + """Synchronize state with controller. + + Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired. + """ LOGGER.debug( "Updating UniFi tracked client %s (%s)", self.entity_id, self.client.mac ) await self.controller.request_update() + if self.is_wired and self.client.mac in self.controller.wireless_clients: + self.is_wired = False + @property def is_connected(self): - """Return true if the client is connected to the network.""" - if ( - dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.client.last_seen)) - ) < self.controller.option_detection_time: + """Return true if the client is connected to the network. + + If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. + """ + if self.is_wired == self.client.is_wired and ( + ( + dt_util.utcnow() + - dt_util.utc_from_timestamp(float(self.client.last_seen)) + ) + < self.controller.option_detection_time + ): return True return False @@ -195,6 +208,8 @@ def device_state_attributes(self): if variable in self.client.raw: attributes[variable] = self.client.raw[variable] + attributes["is_wired"] = self.is_wired + return attributes diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 4f757102d530e8..f0183a7ecb3951 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -88,7 +88,7 @@ def update_items(controller, async_add_entities, switches, switches_off): new_switches.append(switches[block_client_id]) LOGGER.debug("New UniFi Block switch %s (%s)", client.hostname, client.mac) - # control poe + # control POE for client_id in controller.api.clients: poe_client_id = f"poe-{client_id}" @@ -108,9 +108,10 @@ def update_items(controller, async_add_entities, switches, switches_off): pass # Network device with active POE elif ( - not client.is_wired + client_id in controller.wireless_clients or client.sw_mac not in devices or not devices[client.sw_mac].ports[client.sw_port].port_poe + or not devices[client.sw_mac].ports[client.sw_port].poe_enable or controller.mac == client.mac ): continue diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index b28044bc3c7476..e73719205f7698 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -8,6 +8,7 @@ CONF_CONTROLLER, CONF_SITE_ID, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from homeassistant.const import ( CONF_HOST, @@ -49,7 +50,8 @@ async def test_controller_setup(): controller.CONF_DETECTION_TIME: 30, controller.CONF_SSID_FILTER: ["ssid"], } - ] + ], + UNIFI_WIRELESS_CLIENTS: Mock(), } entry = Mock() entry.data = ENTRY_CONFIG @@ -57,6 +59,7 @@ async def test_controller_setup(): api = Mock() api.initialize.return_value = mock_coro(True) api.sites.return_value = mock_coro(CONTROLLER_SITES) + api.clients = [] unifi_controller = controller.UniFiController(hass, entry) @@ -100,7 +103,8 @@ async def test_controller_site(): async def test_controller_mac(): """Test that it is possible to identify controller mac.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} + hass.data[UNIFI_WIRELESS_CLIENTS].get_data.return_value = set() entry = Mock() entry.data = ENTRY_CONFIG entry.options = {} @@ -123,7 +127,7 @@ async def test_controller_mac(): async def test_controller_no_mac(): """Test that it works to not find the controllers mac.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} entry = Mock() entry.data = ENTRY_CONFIG entry.options = {} @@ -133,6 +137,7 @@ async def test_controller_no_mac(): api.initialize.return_value = mock_coro(True) api.clients = {"client1": client} api.sites.return_value = mock_coro(CONTROLLER_SITES) + api.clients = {} unifi_controller = controller.UniFiController(hass, entry) @@ -195,13 +200,14 @@ async def test_reset_if_entry_had_wrong_auth(): async def test_reset_unloads_entry_if_setup(): """Calling reset when the entry has been setup.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} 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) + api.clients = [] unifi_controller = controller.UniFiController(hass, entry) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 760e1e4fa4c50c..3a2b37487afcdd 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,9 +1,11 @@ """The tests for the UniFi device tracker platform.""" from collections import deque from copy import copy -from unittest.mock import Mock + from datetime import timedelta +from asynctest import Mock + import pytest from aiounifi.clients import Clients, ClientsAll @@ -19,6 +21,7 @@ CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID as CONF_CONTROLLER_ID, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from homeassistant.const import ( CONF_HOST, @@ -96,7 +99,7 @@ CONF_PASSWORD: "mock-pswd", CONF_PORT: 1234, CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: True, + CONF_VERIFY_SSL: False, } ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} @@ -108,7 +111,9 @@ def mock_controller(hass): """Mock a UniFi Controller.""" hass.data[UNIFI_CONFIG] = {} + hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() controller = unifi.UniFiController(hass, None) + controller.wireless_clients = set() controller.api = Mock() controller.mock_requests = [] @@ -253,6 +258,45 @@ async def test_tracked_devices(hass, mock_controller): assert device_1 is None +async def test_wireless_client_go_wired_issue(hass, mock_controller): + """Test the solution to catch wireless device go wired UniFi issue. + + UniFi has a known issue that when a wireless device goes away it sometimes gets marked as wired. + """ + client_1_client = copy(CLIENT_1) + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + mock_controller.mock_client_responses.append([client_1_client]) + mock_controller.mock_device_responses.append({}) + + 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 == "home" + + client_1_client["is_wired"] = True + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + mock_controller.mock_client_responses.append([client_1_client]) + mock_controller.mock_device_responses.append({}) + await mock_controller.async_update() + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" + + client_1_client["is_wired"] = False + client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + mock_controller.mock_client_responses.append([client_1_client]) + mock_controller.mock_device_responses.append({}) + await mock_controller.async_update() + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "home" + + async def test_restoring_client(hass, mock_controller): """Test the update_items function with some clients.""" mock_controller.mock_client_responses.append([CLIENT_2]) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index e660e57fc671d5..7ea5e0680b9c44 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -17,6 +17,7 @@ CONF_SITE_ID, CONTROLLER_ID as CONF_CONTROLLER_ID, UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, ) from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component @@ -221,7 +222,9 @@ def mock_controller(hass): """Mock a UniFi Controller.""" hass.data[UNIFI_CONFIG] = {} + hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() controller = unifi.UniFiController(hass, None) + controller.wireless_clients = set() controller._site_role = "admin" @@ -326,7 +329,7 @@ async def test_switches(hass, mock_controller): await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 4 switch_1 = hass.states.get("switch.poe_client_1") assert switch_1 is not None From d8c6b281b88f97af3b150012ea3bbe5e8dacbc2f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 2 Oct 2019 22:12:59 +0200 Subject: [PATCH 0554/3953] deCONZ - Support Symfonisk sound controller with device triggers (#26913) * Device trigger tests shall use the common gateway mock * Follow ebaauws clarification of signals * Fix translations --- .../components/deconz/.translations/en.json | 1 + .../components/deconz/device_trigger.py | 15 ++++- homeassistant/components/deconz/strings.json | 1 + .../components/deconz/test_device_trigger.py | 56 +++---------------- 4 files changed, 24 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index ead71db8c27d89..c00bfca35641c2 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", "remote_button_triple_press": "\"{subtype}\" button triple clicked", diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 77efc78562a924..5339eff055e89f 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -31,6 +31,7 @@ CONF_QUADRUPLE_PRESS = "remote_button_quadruple_press" CONF_QUINTUPLE_PRESS = "remote_button_quintuple_press" CONF_ROTATED = "remote_button_rotated" +CONF_ROTATION_STOPPED = "remote_button_rotation_stopped" CONF_SHAKE = "remote_gyro_activated" CONF_TURN_ON = "turn_on" @@ -75,6 +76,17 @@ (CONF_SHORT_PRESS, CONF_BUTTON_4): 18, } +SYMFONISK_SOUND_CONTROLLER_MODEL = "SYMFONISK Sound Controller" +SYMFONISK_SOUND_CONTROLLER = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): 1005, + (CONF_ROTATED, CONF_LEFT): 2001, + (CONF_ROTATION_STOPPED, CONF_LEFT): 2003, + (CONF_ROTATED, CONF_RIGHT): 3001, + (CONF_ROTATION_STOPPED, CONF_RIGHT): 3003, +} + TRADFRI_ON_OFF_SWITCH_MODEL = "TRADFRI on/off switch" TRADFRI_ON_OFF_SWITCH = { (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, @@ -162,6 +174,7 @@ REMOTES = { HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, + SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER, TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH, TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE, TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE, @@ -200,7 +213,7 @@ async def async_attach_trigger(hass, config, action, automation_info): trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - if device.model not in REMOTES and trigger not in REMOTES[device.model]: + if device.model not in REMOTES or trigger not in REMOTES[device.model]: raise InvalidDeviceAutomationConfig trigger = REMOTES[device.model][trigger] diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 00aa463349cf68..db43c022822838 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -63,6 +63,7 @@ "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", "remote_gyro_activated": "Device shaken" }, "trigger_subtype": { diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 6590028d76638a..4677ea8d5a7c77 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -1,30 +1,13 @@ """deCONZ device automation tests.""" -from asynctest import patch +from copy import deepcopy -from homeassistant import config_entries -from homeassistant.components import deconz from homeassistant.components.deconz import device_trigger from tests.common import async_get_device_automations -BRIDGEID = "0123456789" +from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration -ENTRY_CONFIG = { - deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, - deconz.config_flow.CONF_HOST: "1.2.3.4", - deconz.config_flow.CONF_PORT: 80, -} - -DECONZ_CONFIG = { - "bridgeid": BRIDGEID, - "mac": "00:11:22:33:44:55", - "name": "deCONZ mock gateway", - "sw_version": "2.05.69", - "websocketport": 1234, -} - -DECONZ_SENSOR = { +SENSORS = { "1": { "config": { "alert": "none", @@ -46,37 +29,14 @@ } } -DECONZ_WEB_REQUEST = {"config": DECONZ_CONFIG, "sensors": DECONZ_SENSOR} - - -async def setup_deconz(hass, options): - """Create the deCONZ gateway.""" - config_entry = config_entries.ConfigEntry( - version=1, - domain=deconz.DOMAIN, - title="Mock Title", - data=ENTRY_CONFIG, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=options, - entry_id="1", - ) - - with patch( - "pydeconz.DeconzSession.async_get_state", return_value=DECONZ_WEB_REQUEST - ): - await deconz.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - - hass.config_entries._entries.append(config_entry) - - return hass.data[deconz.DOMAIN][BRIDGEID] - async def test_get_triggers(hass): """Test triggers work.""" - gateway = await setup_deconz(hass, options={}) + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = deepcopy(SENSORS) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) device_id = gateway.events[0].device_id triggers = await async_get_device_automations(hass, "trigger", device_id) From 65ce3b49c18a4a1887393619271d970645084284 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 2 Oct 2019 22:14:52 +0200 Subject: [PATCH 0555/3953] Add support for `for` to binary_sensor, light and switch device triggers (#26658) * Add support for `for` to binary_sensor, light and switch device triggers * Add WS API device_automation/trigger/capabilities --- homeassistant/components/automation/device.py | 9 +- .../binary_sensor/device_trigger.py | 15 ++- .../components/deconz/device_trigger.py | 2 - .../components/device_automation/__init__.py | 93 ++++++++++++++---- .../device_automation/toggle_entity.py | 21 +++- .../components/light/device_action.py | 1 - .../components/light/device_trigger.py | 6 +- .../components/switch/device_action.py | 1 - .../components/switch/device_trigger.py | 6 +- homeassistant/components/zha/device_action.py | 1 - .../components/zha/device_trigger.py | 1 - homeassistant/helpers/config_validation.py | 14 ++- homeassistant/helpers/script.py | 11 ++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- tests/common.py | 1 + .../binary_sensor/test_device_trigger.py | 84 ++++++++++++++++ .../components/device_automation/test_init.py | 97 +++++++++++++++++++ tests/components/light/test_device_trigger.py | 84 ++++++++++++++++ .../components/switch/test_device_trigger.py | 84 ++++++++++++++++ 21 files changed, 495 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index eb3e5a95c9c17e..dc65008c3fb268 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -5,6 +5,7 @@ TRIGGER_BASE_SCHEMA, async_get_device_automation_platform, ) +from homeassistant.const import CONF_DOMAIN # mypy: allow-untyped-defs, no-check-untyped-defs @@ -14,11 +15,15 @@ async def async_validate_trigger_config(hass, config): """Validate config.""" - platform = await async_get_device_automation_platform(hass, config, "trigger") + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "trigger" + ) return platform.TRIGGER_SCHEMA(config) async def async_attach_trigger(hass, config, action, automation_info): """Listen for trigger.""" - platform = await async_get_device_automation_platform(hass, config, "trigger") + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "trigger" + ) return await platform.async_attach_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 2211b3001045ed..c4d2efcb63b2bb 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -7,7 +7,7 @@ CONF_TURNED_OFF, CONF_TURNED_ON, ) -from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import config_validation as cv @@ -175,13 +175,13 @@ { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(TURNED_OFF + TURNED_ON), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) trigger_type = config[CONF_TYPE] if trigger_type in TURNED_ON: from_state = "off" @@ -195,6 +195,8 @@ async def async_attach_trigger(hass, config, action, automation_info): state_automation.CONF_FROM: from_state, state_automation.CONF_TO: to_state, } + if "for" in config: + state_config["for"] = config["for"] return await state_automation.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" @@ -236,3 +238,12 @@ async def async_get_triggers(hass, device_id): ) return triggers + + +async def async_get_trigger_capabilities(hass, trigger): + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 5339eff055e89f..badbe8b8651278 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -206,8 +206,6 @@ def _get_deconz_event_from_device_id(hass, device_id): async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) - device_registry = await hass.helpers.device_registry.async_get_registry() device = device_registry.async_get(config[CONF_DEVICE_ID]) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 23e320fe153b60..a7e04f874b4d83 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -4,9 +4,11 @@ from typing import Any, List, MutableMapping import voluptuous as vol +import voluptuous_serialize from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.components import websocket_api +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.loader import async_get_integration, IntegrationNotFound @@ -29,9 +31,18 @@ ) TYPES = { - "trigger": ("device_trigger", "async_get_triggers"), - "condition": ("device_condition", "async_get_conditions"), - "action": ("device_action", "async_get_actions"), + # platform name, get automations function, get capabilities function + "trigger": ( + "device_trigger", + "async_get_triggers", + "async_get_trigger_capabilities", + ), + "condition": ( + "device_condition", + "async_get_conditions", + "async_get_condition_capabilities", + ), + "action": ("device_action", "async_get_actions", "async_get_action_capabilities"), } @@ -46,25 +57,26 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( websocket_device_automation_list_triggers ) + hass.components.websocket_api.async_register_command( + websocket_device_automation_get_trigger_capabilities + ) return True -async def async_get_device_automation_platform(hass, config, automation_type): +async def async_get_device_automation_platform(hass, domain, automation_type): """Load device automation platform for integration. Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. """ - platform_name, _ = TYPES[automation_type] + platform_name = TYPES[automation_type][0] try: - integration = await async_get_integration(hass, config[CONF_DOMAIN]) + integration = await async_get_integration(hass, domain) platform = integration.get_platform(platform_name) except IntegrationNotFound: - raise InvalidDeviceAutomationConfig( - f"Integration '{config[CONF_DOMAIN]}' not found" - ) + raise InvalidDeviceAutomationConfig(f"Integration '{domain}' not found") except ImportError: raise InvalidDeviceAutomationConfig( - f"Integration '{config[CONF_DOMAIN]}' does not support device automation {automation_type}s" + f"Integration '{domain}' does not support device automation {automation_type}s" ) return platform @@ -74,20 +86,14 @@ async def _async_get_device_automations_from_domain( hass, domain, automation_type, device_id ): """List device automations.""" - integration = None try: - integration = await async_get_integration(hass, domain) - except IntegrationNotFound: - _LOGGER.warning("Integration %s not found", domain) + platform = await async_get_device_automation_platform( + hass, domain, automation_type + ) + except InvalidDeviceAutomationConfig: return None - platform_name, function_name = TYPES[automation_type] - - try: - platform = integration.get_platform(platform_name) - except ImportError: - # The domain does not have device automations - return None + function_name = TYPES[automation_type][1] return await getattr(platform, function_name)(hass, device_id) @@ -125,6 +131,35 @@ async def _async_get_device_automations(hass, automation_type, device_id): return automations +async def _async_get_device_automation_capabilities(hass, automation_type, automation): + """List device automations.""" + try: + platform = await async_get_device_automation_platform( + hass, automation[CONF_DOMAIN], automation_type + ) + except InvalidDeviceAutomationConfig: + return {} + + function_name = TYPES[automation_type][2] + + if not hasattr(platform, function_name): + # The device automation has no capabilities + return {} + + capabilities = await getattr(platform, function_name)(hass, automation) + capabilities = capabilities.copy() + + extra_fields = capabilities.get("extra_fields") + if extra_fields is None: + capabilities["extra_fields"] = [] + else: + capabilities["extra_fields"] = voluptuous_serialize.convert( + extra_fields, custom_serializer=cv.custom_serializer + ) + + return capabilities + + @websocket_api.async_response @websocket_api.websocket_command( { @@ -165,3 +200,19 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): device_id = msg["device_id"] triggers = await _async_get_device_automations(hass, "trigger", device_id) connection.send_result(msg["id"], triggers) + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/trigger/capabilities", + vol.Required("trigger"): dict, + } +) +async def websocket_device_automation_get_trigger_capabilities(hass, connection, msg): + """Handle request for device trigger capabilities.""" + trigger = msg["trigger"] + capabilities = await _async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + connection.send_result(msg["id"], capabilities) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index ef1b605f4d68e0..7c68be83ba30ec 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -13,7 +13,13 @@ CONF_TURNED_OFF, CONF_TURNED_ON, ) -from homeassistant.const import CONF_CONDITION, CONF_ENTITY_ID, CONF_PLATFORM, CONF_TYPE +from homeassistant.const import ( + CONF_CONDITION, + CONF_ENTITY_ID, + CONF_FOR, + CONF_PLATFORM, + CONF_TYPE, +) from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import condition, config_validation as cv, service from homeassistant.helpers.typing import ConfigType, TemplateVarsType @@ -81,6 +87,7 @@ { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) @@ -93,7 +100,6 @@ async def async_call_action_from_config( domain: str, ) -> None: """Change state based on configuration.""" - config = ACTION_SCHEMA(config) action_type = config[CONF_TYPE] if action_type == CONF_TURN_ON: action = "turn_on" @@ -149,6 +155,8 @@ async def async_attach_trigger( state.CONF_FROM: from_state, state.CONF_TO: to_state, } + if "for" in config: + state_config["for"] = config["for"] return await state.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" @@ -203,3 +211,12 @@ async def async_get_triggers( ) -> List[dict]: """List device triggers.""" return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index ea37b8e9470534..9d8ef6bceafb32 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -19,7 +19,6 @@ async def async_call_action_from_config( context: Context, ) -> None: """Change state based on configuration.""" - config = ACTION_SCHEMA(config) await toggle_entity.async_call_action_from_config( hass, config, variables, context, DOMAIN ) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index f2a82afdc2d41c..5bd5d83e1c02ee 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -22,7 +22,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) return await toggle_entity.async_attach_trigger( hass, config, action, automation_info ) @@ -31,3 +30,8 @@ async def async_attach_trigger( async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: """List device triggers.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: + """List trigger capabilities.""" + return await toggle_entity.async_get_trigger_capabilities(hass, trigger) diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py index ca91cc70512be5..a65c1acc5124b8 100644 --- a/homeassistant/components/switch/device_action.py +++ b/homeassistant/components/switch/device_action.py @@ -19,7 +19,6 @@ async def async_call_action_from_config( context: Context, ) -> None: """Change state based on configuration.""" - config = ACTION_SCHEMA(config) await toggle_entity.async_call_action_from_config( hass, config, variables, context, DOMAIN ) diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 9be294d5460c99..22a016e49b9bc2 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -22,7 +22,6 @@ async def async_attach_trigger( automation_info: dict, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) return await toggle_entity.async_attach_trigger( hass, config, action, automation_info ) @@ -31,3 +30,8 @@ async def async_attach_trigger( async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: """List device triggers.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: + """List trigger capabilities.""" + return await toggle_entity.async_get_trigger_capabilities(hass, trigger) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 27e78507bfb317..460676a75a0045 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -49,7 +49,6 @@ async def async_call_action_from_config( context: Context, ) -> None: """Perform an action based on configuration.""" - config = ACTION_SCHEMA(config) await ZHA_ACTION_TYPES[DEVICE_ACTION_TYPES[config[CONF_TYPE]]]( hass, config, variables, context ) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 331dc3d32968d3..c1ea3c2b761698 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -23,7 +23,6 @@ async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" - config = TRIGGER_SCHEMA(config) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index d0aeb4f4968bc6..2d1bb89d23a56e 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -15,8 +15,9 @@ from urllib.parse import urlparse from uuid import UUID -import voluptuous as vol from pkg_resources import parse_version +import voluptuous as vol +import voluptuous_serialize import homeassistant.util.dt as dt_util from homeassistant.const import ( @@ -374,6 +375,9 @@ def positive_timedelta(value: timedelta) -> timedelta: return value +positive_time_period_dict = vol.All(time_period_dict, positive_timedelta) + + def remove_falsy(value: List[T]) -> List[T]: """Remove falsy values from a list.""" return [v for v in value if v] @@ -690,6 +694,14 @@ def validator(value): return validator +def custom_serializer(schema): + """Serialize additional types for voluptuous_serialize.""" + if schema is positive_time_period_dict: + return {"type": "positive_time_period_dict"} + + return voluptuous_serialize.UNSUPPORTED + + # Schemas PLATFORM_SCHEMA = vol.Schema( { diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index e383f1013ab204..d9b3df8c01b717 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -10,7 +10,12 @@ import homeassistant.components.device_automation as device_automation from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE -from homeassistant.const import CONF_CONDITION, CONF_DEVICE_ID, CONF_TIMEOUT +from homeassistant.const import ( + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_TIMEOUT, +) from homeassistant import exceptions from homeassistant.helpers import ( service, @@ -89,7 +94,7 @@ async def async_validate_action_config( if action_type == ACTION_DEVICE_AUTOMATION: platform = await device_automation.async_get_device_automation_platform( - hass, config, "action" + hass, config[CONF_DOMAIN], "action" ) config = platform.ACTION_SCHEMA(config) @@ -346,7 +351,7 @@ async def _async_device_automation(self, action, variables, context): self.last_action = action.get(CONF_ALIAS, "device automation") self._log("Executing step %s" % self.last_action) platform = await device_automation.async_get_device_automation_platform( - self.hass, action, "action" + self.hass, action[CONF_DOMAIN], "action" ) await platform.async_call_action_from_config( self.hass, action, variables, context diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 684285a1cf10b3..c500ddca85d4c5 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.100 sqlalchemy==1.3.8 -voluptuous-serialize==2.2.0 +voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index a0f64eeec74b9d..4a2a2cf45fcf76 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.100 voluptuous==0.11.7 -voluptuous-serialize==2.2.0 +voluptuous-serialize==2.3.0 # homeassistant.components.nuimo_controller --only-binary=all nuimo==0.1.0 diff --git a/setup.py b/setup.py index d842ae39ae1b77..23a8a808f4376f 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ "requests==2.22.0", "ruamel.yaml==0.15.100", "voluptuous==0.11.7", - "voluptuous-serialize==2.2.0", + "voluptuous-serialize==2.3.0", ] MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) diff --git a/tests/common.py b/tests/common.py index 1982e80dfe949e..bd611e04c37a6e 100644 --- a/tests/common.py +++ b/tests/common.py @@ -56,6 +56,7 @@ from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.components.device_automation import ( # noqa _async_get_device_automations as async_get_device_automations, + _async_get_device_automation_capabilities as async_get_device_automation_capabilities, ) _TEST_INSTANCE_PORT = SERVER_PORT diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 5be354c78fc5cf..9bab1ff1f363e9 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for binary_sensor device automation.""" +from datetime import timedelta import pytest from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES @@ -7,13 +8,16 @@ from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, + async_fire_time_changed, async_mock_service, mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -71,6 +75,28 @@ async def test_get_triggers(hass, device_reg, entity_reg): assert triggers == expected_triggers +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a binary_sensor trigger.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + async def test_if_fires_on_state_change(hass, calls): """Test for on and off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -152,3 +178,61 @@ async def test_if_fires_on_state_change(hass, calls): assert calls[1].data["some"] == "bat_low device - {} - off - on - None".format( sensor1.entity_id ) + + +async def test_if_fires_on_state_change_with_for(hass, calls): + """Test for triggers firing with delay.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "turned_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + await hass.async_block_till_done() + assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format( + sensor1.entity_id + ) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index acfa853d596f27..8a92f69e57493a 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -164,6 +164,103 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert _same_lists(triggers, expected_triggers) +async def test_websocket_get_trigger_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get the expected trigger capabilities for a light through websocket.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/trigger/list", + "device_id": device_entry.id, + } + ) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + triggers = msg["result"] + + id = 2 + for trigger in triggers: + await client.send_json( + { + "id": id, + "type": "device_automation/trigger/capabilities", + "trigger": trigger, + } + ) + msg = await client.receive_json() + assert msg["id"] == id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + id = id + 1 + + +async def test_websocket_get_bad_trigger_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no trigger capabilities for a non existing domain.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/trigger/capabilities", + "trigger": {"domain": "beer"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + +async def test_websocket_get_no_trigger_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no trigger capabilities for a domain with no device trigger capabilities.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/trigger/capabilities", + "trigger": {"domain": "deconz"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + async def test_automation_with_non_existing_integration(hass, caplog): """Test device automation with non existing integration.""" assert await async_setup_component( diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index 9b540c7aa15af7..a6437ef9ee0a64 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for light device automation.""" +from datetime import timedelta import pytest from homeassistant.components.light import DOMAIN @@ -6,13 +7,16 @@ from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, + async_fire_time_changed, async_mock_service, mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -63,6 +67,28 @@ async def test_get_triggers(hass, device_reg, entity_reg): assert triggers == expected_triggers +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a light trigger.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + async def test_if_fires_on_state_change(hass, calls): """Test for turn_on and turn_off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -145,3 +171,61 @@ async def test_if_fires_on_state_change(hass, calls): assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( ent1.entity_id ) + + +async def test_if_fires_on_state_change_with_for(hass, calls): + """Test for triggers firing with delay.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + await hass.async_block_till_done() + assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format( + ent1.entity_id + ) diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 43af9fe3df34b2..31fb6d30f60994 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -1,4 +1,5 @@ """The test for switch device automation.""" +from datetime import timedelta import pytest from homeassistant.components.switch import DOMAIN @@ -6,13 +7,16 @@ from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, + async_fire_time_changed, async_mock_service, mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -63,6 +67,28 @@ async def test_get_triggers(hass, device_reg, entity_reg): assert triggers == expected_triggers +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a switch trigger.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + async def test_if_fires_on_state_change(hass, calls): """Test for turn_on and turn_off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -145,3 +171,61 @@ async def test_if_fires_on_state_change(hass, calls): assert calls[1].data["some"] == "turn_on device - {} - off - on - None".format( ent1.entity_id ) + + +async def test_if_fires_on_state_change_with_for(hass, calls): + """Test for triggers firing with delay.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "turned_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + await hass.async_block_till_done() + assert calls[0].data["some"] == "turn_off device - {} - on - off - 0:00:05".format( + ent1.entity_id + ) From 743cb848e883062b55a6e897ecb5842598058644 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 3 Oct 2019 00:08:01 +0200 Subject: [PATCH 0556/3953] Updated frontend to 20191002.0 (#27134) --- homeassistant/components/frontend/manifest.json | 10 +++++++--- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f1d91879f155eb..60a4f0faa9c16f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,9 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20190919.1"], + "requirements": [ + "home-assistant-frontend==20191002.0" + ], "dependencies": [ "api", "auth", @@ -12,5 +14,7 @@ "system_log", "websocket_api" ], - "codeowners": ["@home-assistant/frontend"] -} + "codeowners": [ + "@home-assistant/frontend" + ] +} \ No newline at end of file diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c500ddca85d4c5..29484b671ed0e1 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==20190919.1 +home-assistant-frontend==20191002.0 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4a2a2cf45fcf76..2aa04f8ae1551e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -640,7 +640,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.1 +home-assistant-frontend==20191002.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb29540d6d93c7..61d3479d8f62d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -182,7 +182,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190919.1 +home-assistant-frontend==20191002.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 30245f68741986f951f19cd5c8283cb6e15cfdeb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 2 Oct 2019 17:51:18 -0500 Subject: [PATCH 0557/3953] Fix error on failed Plex setup (#27132) --- homeassistant/components/plex/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 874ac6334acf5b..ed94b6913bcf57 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -121,7 +121,7 @@ async def async_setup_entry(hass, entry): ) as error: _LOGGER.error( "Login to %s failed, verify token and SSL settings: [%s]", - server_config[CONF_SERVER], + entry.data[CONF_SERVER], error, ) return False From e011a94ce9024ab29774c5b5f49b20ba727eb26f Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 2 Oct 2019 18:51:52 -0400 Subject: [PATCH 0558/3953] Bump up ZHA dependencies. (#27127) --- 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 ab8a20822a1365..59d9508ac338d8 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "bellows-homeassistant==0.10.0", "zha-quirks==0.0.26", - "zigpy-deconz==0.4.0", + "zigpy-deconz==0.5.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", "zigpy-zigate==0.4.0" diff --git a/requirements_all.txt b/requirements_all.txt index 2aa04f8ae1551e..cda5afabe7cb8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2032,7 +2032,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.4.0 +zigpy-deconz==0.5.0 # homeassistant.components.zha zigpy-homeassistant==0.9.0 From 6dfeed6cd1aacf27bb8cb588965057bc58a0d447 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 2 Oct 2019 18:53:04 -0400 Subject: [PATCH 0559/3953] Fix unavailable climate entities in Alexa StateReport (#27128) * Return None for AlexaThermostatController and AlexaTemperatureSensor properties if climate state is unavailable. Preserves raising an error for UnsupportedProperty, and allows Alexa.EndpointHealth to handle the unavailable state. * Added additional tests for climate state reporting. --- .../components/alexa/capabilities.py | 5 +- tests/components/alexa/test_capabilities.py | 109 +++++++++++++++++- 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index aeaa0a62c4bdbb..c8bc76fbe83edb 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -445,7 +445,7 @@ def get_property(self, name): unit = self.hass.config.units.temperature_unit temp = self.entity.attributes.get(climate.ATTR_CURRENT_TEMPERATURE) - if temp in (STATE_UNAVAILABLE, STATE_UNKNOWN): + if temp in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): return None try: @@ -572,6 +572,9 @@ def properties_retrievable(self): def get_property(self, name): """Read and return a property.""" + if self.entity.state == STATE_UNAVAILABLE: + return None + if name == "thermostatMode": preset = self.entity.attributes.get(climate.ATTR_PRESET_MODE) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 357e0e3026d47c..94c931e351405f 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -9,8 +9,9 @@ STATE_UNKNOWN, STATE_UNAVAILABLE, ) -from homeassistant.components import climate +from homeassistant.components.climate import const as climate from homeassistant.components.alexa import smart_home +from homeassistant.components.alexa.errors import UnsupportedProperty from tests.common import async_mock_service from . import ( @@ -378,6 +379,112 @@ async def test_report_cover_percentage_state(hass): properties.assert_equal("Alexa.PercentageController", "percentage", 0) +async def test_report_climate_state(hass): + """Test ThermostatController reports state correctly.""" + for auto_modes in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): + hass.states.async_set( + "climate.downstairs", + auto_modes, + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "AUTO") + properties.assert_equal( + "Alexa.TemperatureSensor", + "temperature", + {"value": 34.0, "scale": "CELSIUS"}, + ) + + for off_modes in ( + climate.HVAC_MODE_OFF, + climate.HVAC_MODE_FAN_ONLY, + climate.HVAC_MODE_DRY, + ): + hass.states.async_set( + "climate.downstairs", + off_modes, + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "OFF") + properties.assert_equal( + "Alexa.TemperatureSensor", + "temperature", + {"value": 34.0, "scale": "CELSIUS"}, + ) + + hass.states.async_set( + "climate.heat", + "heat", + { + "friendly_name": "Climate Heat", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.heat") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "HEAT") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) + + hass.states.async_set( + "climate.cool", + "cool", + { + "friendly_name": "Climate Cool", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.cool") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "COOL") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) + + hass.states.async_set( + "climate.unavailable", + "unavailable", + {"friendly_name": "Climate Unavailable", "supported_features": 91}, + ) + properties = await reported_properties(hass, "climate.unavailable") + properties.assert_not_has_property("Alexa.ThermostatController", "thermostatMode") + + hass.states.async_set( + "climate.unsupported", + "blablabla", + { + "friendly_name": "Climate Unsupported", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + with pytest.raises(UnsupportedProperty): + properties = await reported_properties(hass, "climate.unsupported") + properties.assert_not_has_property( + "Alexa.ThermostatController", "thermostatMode" + ) + properties.assert_equal( + "Alexa.TemperatureSensor", + "temperature", + {"value": 34.0, "scale": "CELSIUS"}, + ) + + async def test_temperature_sensor_sensor(hass): """Test TemperatureSensor reports sensor temperature correctly.""" for bad_value in (STATE_UNKNOWN, STATE_UNAVAILABLE, "not-number"): From 39c7d069b8d264353ce7ebe63a09cfec54224a3a Mon Sep 17 00:00:00 2001 From: Brendon Baumgartner Date: Wed, 2 Oct 2019 15:53:37 -0700 Subject: [PATCH 0560/3953] gpiozero requirement ver (#27129) --- homeassistant/components/remote_rpi_gpio/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json index df9a9c7512358c..c3b93346916a5f 100644 --- a/homeassistant/components/remote_rpi_gpio/manifest.json +++ b/homeassistant/components/remote_rpi_gpio/manifest.json @@ -3,7 +3,7 @@ "name": "remote_rpi_gpio", "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio", "requirements": [ - "gpiozero==1.4.1" + "gpiozero==1.5.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index cda5afabe7cb8f..4f0963cdbef6dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -574,7 +574,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.remote_rpi_gpio -gpiozero==1.4.1 +gpiozero==1.5.1 # homeassistant.components.gpsd gps3==0.33.3 From 75bce84ad5c4bbc4a9a3de798a6e6753ae982926 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 3 Oct 2019 00:53:55 +0200 Subject: [PATCH 0561/3953] Update KNX integration to xknx 0.11.2 (#27130) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 76f15f3bdb8efd..f99ec2f22c01fd 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "Knx", "documentation": "https://www.home-assistant.io/integrations/knx", "requirements": [ - "xknx==0.11.1" + "xknx==0.11.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4f0963cdbef6dc..c84b57a09f36b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1986,7 +1986,7 @@ xboxapi==0.1.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.11.1 +xknx==0.11.2 # homeassistant.components.bluesound # homeassistant.components.startca From 363873dfcba399d45f1ee618cb294044c765eef5 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 2 Oct 2019 18:55:01 -0400 Subject: [PATCH 0562/3953] Display Fan entity as Fan category in Alexa (#27135) * Added Fan to display categories. * Added Doorbell to display categories. * Added Microwave to display categories. * Added Security Panel to display categories. * Updated FanCapabilities to use FAN display category. * Updated Tests for FanCapabilities to use FAN display category. --- homeassistant/components/alexa/entities.py | 14 +++++++++++++- tests/components/alexa/test_smart_home.py | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index f0d72af23d50f4..55b5878f6672a3 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -76,9 +76,18 @@ class DisplayCategory: # Indicates a door. DOOR = "DOOR" + # Indicates a doorbell. + DOOR_BELL = "DOORBELL" + + # Indicates a fan. + FAN = "FAN" + # Indicates light sources or fixtures. LIGHT = "LIGHT" + # Indicates a microwave oven. + MICROWAVE = "MICROWAVE" + # Indicates an endpoint that detects and reports motion. MOTION_SENSOR = "MOTION_SENSOR" @@ -91,6 +100,9 @@ class DisplayCategory: # order is unimportant. Applies to Scenes SCENE_TRIGGER = "SCENE_TRIGGER" + # Indicates a security panel. + SECURITY_PANEL = "SECURITY_PANEL" + # Indicates an endpoint that locks. SMARTLOCK = "SMARTLOCK" @@ -324,7 +336,7 @@ class FanCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" - return [DisplayCategory.OTHER] + return [DisplayCategory.FAN] def interfaces(self): """Yield the supported interfaces.""" diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3cafa89902471f..e5e5b8ab7aecf0 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -308,7 +308,7 @@ async def test_fan(hass): appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "fan#test_1" - assert appliance["displayCategories"][0] == "OTHER" + assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" assert_endpoint_capabilities( appliance, "Alexa.PowerController", "Alexa.EndpointHealth" @@ -333,7 +333,7 @@ async def test_variable_fan(hass): appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "fan#test_2" - assert appliance["displayCategories"][0] == "OTHER" + assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 2" assert_endpoint_capabilities( From c43eeee62f2187000b1abe1506e1c6025f0fcad0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 00:58:14 +0200 Subject: [PATCH 0563/3953] Improve validation of device condition config (#27131) * Improve validation of device condition config * Fix typing --- homeassistant/components/automation/config.py | 11 +- .../binary_sensor/device_condition.py | 3 +- .../components/device_automation/__init__.py | 6 +- .../components/light/device_condition.py | 3 +- .../components/switch/device_condition.py | 3 +- homeassistant/helpers/condition.py | 51 +++-- homeassistant/helpers/script.py | 7 +- .../components/device_automation/test_init.py | 206 +++++++++++++++++- 8 files changed, 269 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 3f48e2afde67f8..581ce6b461d844 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -7,10 +7,10 @@ from homeassistant.const import CONF_PLATFORM from homeassistant.config import async_log_exception, config_without_domain from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform, script +from homeassistant.helpers import condition, config_per_platform, script from homeassistant.loader import IntegrationNotFound -from . import CONF_ACTION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA +from . import CONF_ACTION, CONF_CONDITION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -33,6 +33,13 @@ async def async_validate_config_item(hass, config, full_config=None): triggers.append(trigger) config[CONF_TRIGGER] = triggers + if CONF_CONDITION in config: + conditions = [] + for cond in config[CONF_CONDITION]: + cond = await condition.async_validate_condition_config(hass, cond) + conditions.append(cond) + config[CONF_CONDITION] = conditions + actions = [] for action in config[CONF_ACTION]: action = await script.async_validate_action_config(hass, action) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 70b79becb8b12d..1749ea91c5b552 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -232,7 +232,8 @@ def async_condition_from_config( config: ConfigType, config_validation: bool ) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) + if config_validation: + config = CONDITION_SCHEMA(config) condition_type = config[CONF_TYPE] if condition_type in IS_ON: stat = "on" diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index a7e04f874b4d83..fa6deac40ba6ac 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -2,12 +2,14 @@ import asyncio import logging from typing import Any, List, MutableMapping +from types import ModuleType import voluptuous as vol import voluptuous_serialize from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.loader import async_get_integration, IntegrationNotFound @@ -63,7 +65,9 @@ async def async_setup(hass, config): return True -async def async_get_device_automation_platform(hass, domain, automation_type): +async def async_get_device_automation_platform( + hass: HomeAssistant, domain: str, automation_type: str +) -> ModuleType: """Load device automation platform for integration. Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation. diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index a69ca7ab8f2f2f..4abf34e6661bac 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -19,7 +19,8 @@ def async_condition_from_config( config: ConfigType, config_validation: bool ) -> ConditionCheckerType: """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) + if config_validation: + config = CONDITION_SCHEMA(config) return toggle_entity.async_condition_from_config(config, config_validation) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 032c765bf5907c..5825a3ba91ad4f 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -19,7 +19,8 @@ def async_condition_from_config( config: ConfigType, config_validation: bool ) -> ConditionCheckerType: """Evaluate state based on configuration.""" - config = CONDITION_SCHEMA(config) + if config_validation: + config = CONDITION_SCHEMA(config) return toggle_entity.async_condition_from_config(config, config_validation) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index afb8c3934a7442..df82ba6076fd83 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -8,29 +8,31 @@ from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from homeassistant.loader import async_get_integration from homeassistant.core import HomeAssistant, State from homeassistant.components import zone as zone_cmp +from homeassistant.components.device_automation import ( + async_get_device_automation_platform, +) from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_ABOVE, + CONF_AFTER, + CONF_BEFORE, + CONF_BELOW, + CONF_CONDITION, CONF_DOMAIN, CONF_ENTITY_ID, - CONF_VALUE_TEMPLATE, - CONF_CONDITION, - WEEKDAYS, CONF_STATE, - CONF_ZONE, - CONF_BEFORE, - CONF_AFTER, + CONF_VALUE_TEMPLATE, CONF_WEEKDAY, - SUN_EVENT_SUNRISE, - SUN_EVENT_SUNSET, - CONF_BELOW, - CONF_ABOVE, + CONF_ZONE, STATE_UNAVAILABLE, STATE_UNKNOWN, + SUN_EVENT_SUNRISE, + SUN_EVENT_SUNSET, + WEEKDAYS, ) from homeassistant.exceptions import TemplateError, HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -498,9 +500,32 @@ async def async_device_from_config( """Test a device condition.""" if config_validation: config = cv.DEVICE_CONDITION_SCHEMA(config) - integration = await async_get_integration(hass, config[CONF_DOMAIN]) - platform = integration.get_platform("device_condition") + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "condition" + ) return cast( ConditionCheckerType, platform.async_condition_from_config(config, config_validation), # type: ignore ) + + +async def async_validate_condition_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate config.""" + condition = config[CONF_CONDITION] + if condition in ("and", "or"): + conditions = [] + for sub_cond in config["conditions"]: + sub_cond = await async_validate_condition_config(hass, sub_cond) + conditions.append(sub_cond) + config["conditions"] = conditions + + if condition == "device": + config = cv.DEVICE_CONDITION_SCHEMA(config) + platform = await async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "condition" + ) + return cast(ConfigType, platform.CONDITION_SCHEMA(config)) # type: ignore + + return config diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d9b3df8c01b717..05b281027262ff 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -96,7 +96,12 @@ async def async_validate_action_config( platform = await device_automation.async_get_device_automation_platform( hass, config[CONF_DOMAIN], "action" ) - config = platform.ACTION_SCHEMA(config) + config = platform.ACTION_SCHEMA(config) # type: ignore + if action_type == ACTION_CHECK_CONDITION and config[CONF_CONDITION] == "device": + platform = await device_automation.async_get_device_automation_platform( + hass, config[CONF_DOMAIN], "condition" + ) + config = platform.CONDITION_SCHEMA(config) # type: ignore return config diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 8a92f69e57493a..fa78ae94416361 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -4,10 +4,16 @@ from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM from homeassistant.helpers import device_registry -from tests.common import MockConfigEntry, mock_device_registry, mock_registry +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, +) @pytest.fixture @@ -301,6 +307,31 @@ async def test_automation_with_integration_without_device_action(hass, caplog): ) +async def test_automation_with_integration_without_device_condition(hass, caplog): + """Test automation with integration without device condition support.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "device_id": "none", + "domain": "test", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert ( + "Integration 'test' does not support device automation conditions" + in caplog.text + ) + + async def test_automation_with_integration_without_device_trigger(hass, caplog): """Test automation with integration without device trigger support.""" assert await async_setup_component( @@ -341,6 +372,179 @@ async def test_automation_with_bad_action(hass, caplog): assert "required key not provided" in caplog.text +async def test_automation_with_bad_condition_action(hass, caplog): + """Test automation with bad device action.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": {"condition": "device", "device_id": "", "domain": "light"}, + } + }, + ) + + assert "required key not provided" in caplog.text + + +async def test_automation_with_bad_condition(hass, caplog): + """Test automation with bad device condition.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": {"condition": "device", "domain": "light"}, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "required key not provided" in caplog.text + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_automation_with_sub_condition(hass, calls): + """Test automation with device condition under and/or conditions.""" + DOMAIN = "light" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "and", + "conditions": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_on", + }, + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent2.entity_id, + "type": "is_on", + }, + ], + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "and {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "or", + "conditions": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_on", + }, + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent2.entity_id, + "type": "is_on", + }, + ], + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "or {{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert hass.states.get(ent2.entity_id).state == STATE_OFF + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "or event - test_event1" + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_set(ent2.entity_id, STATE_ON) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "or event - test_event1" + + hass.states.async_set(ent1.entity_id, STATE_ON) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 4 + assert _same_lists( + [calls[2].data["some"], calls[3].data["some"]], + ["or event - test_event1", "and event - test_event1"], + ) + + +async def test_automation_with_bad_sub_condition(hass, caplog): + """Test automation with bad device condition under and/or conditions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "and", + "conditions": [{"condition": "device", "domain": "light"}], + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "required key not provided" in caplog.text + + async def test_automation_with_bad_trigger(hass, caplog): """Test automation with bad device trigger.""" assert await async_setup_component( From 9c1feacd47671eb4935215c31110b778f3a63eb5 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 2 Oct 2019 18:59:21 -0400 Subject: [PATCH 0564/3953] Fix colorTemperatureInKelvin in Alexa report when light is off (#27107) * Fixes #26405 Return None if light state is off since attribute is unavailable, prevents property from being reported with invalid value of 0. * Update Test to check property is not reported when light state is off. --- homeassistant/components/alexa/capabilities.py | 2 +- tests/components/alexa/test_capabilities.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index c8bc76fbe83edb..b8bd3841a78c4c 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -326,7 +326,7 @@ def get_property(self, name): return color_util.color_temperature_mired_to_kelvin( self.entity.attributes["color_temp"] ) - return 0 + return None class AlexaPercentageController(AlexaCapibility): diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 94c931e351405f..d53f145e6ff7f6 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -294,8 +294,8 @@ async def test_report_colored_temp_light_state(hass): ) properties = await reported_properties(hass, "light.test_off") - properties.assert_equal( - "Alexa.ColorTemperatureController", "colorTemperatureInKelvin", 0 + properties.assert_not_has_property( + "Alexa.ColorTemperatureController", "colorTemperatureInKelvin" ) From e005f6f23a3ff9ff052afd317891da206ea618e0 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 3 Oct 2019 00:34:28 +0000 Subject: [PATCH 0565/3953] [ci skip] Translation update --- .../ambiclimate/.translations/en.json | 2 +- .../arcam_fmj/.translations/es-419.json | 5 ++ .../binary_sensor/.translations/es-419.json | 65 +++++++++++++++++++ .../cert_expiry/.translations/es-419.json | 5 ++ .../deconz/.translations/es-419.json | 36 +++++++++- .../components/heos/.translations/es-419.json | 5 ++ .../.translations/es-419.json | 1 + .../iaqualink/.translations/es-419.json | 13 ++++ .../life360/.translations/es-419.json | 18 +++++ .../light/.translations/es-419.json | 12 ++++ .../linky/.translations/es-419.json | 25 +++++++ .../logi_circle/.translations/en.json | 2 +- .../components/nest/.translations/en.json | 2 +- .../components/nest/.translations/es-419.json | 3 +- .../notion/.translations/es-419.json | 1 + .../components/plex/.translations/da.json | 1 + .../components/plex/.translations/es-419.json | 55 ++++++++++++++++ .../components/plex/.translations/no.json | 3 +- .../components/point/.translations/en.json | 2 +- .../components/ps4/.translations/en.json | 6 +- .../components/soma/.translations/da.json | 1 + .../somfy/.translations/es-419.json | 5 ++ .../switch/.translations/es-419.json | 18 +++++ .../tellduslive/.translations/es-419.json | 4 +- .../components/toon/.translations/en.json | 2 +- .../components/toon/.translations/es-419.json | 14 +++- .../traccar/.translations/es-419.json | 8 +++ .../twentemilieu/.translations/es-419.json | 10 +++ .../velbus/.translations/es-419.json | 21 ++++++ .../vesync/.translations/es-419.json | 20 ++++++ .../withings/.translations/es-419.json | 19 ++++++ .../components/zha/.translations/es-419.json | 18 +++++ .../components/zha/.translations/ru.json | 5 ++ 33 files changed, 392 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/arcam_fmj/.translations/es-419.json create mode 100644 homeassistant/components/binary_sensor/.translations/es-419.json create mode 100644 homeassistant/components/cert_expiry/.translations/es-419.json create mode 100644 homeassistant/components/iaqualink/.translations/es-419.json create mode 100644 homeassistant/components/light/.translations/es-419.json create mode 100644 homeassistant/components/linky/.translations/es-419.json create mode 100644 homeassistant/components/plex/.translations/es-419.json create mode 100644 homeassistant/components/somfy/.translations/es-419.json create mode 100644 homeassistant/components/switch/.translations/es-419.json create mode 100644 homeassistant/components/traccar/.translations/es-419.json create mode 100644 homeassistant/components/twentemilieu/.translations/es-419.json create mode 100644 homeassistant/components/velbus/.translations/es-419.json create mode 100644 homeassistant/components/vesync/.translations/es-419.json create mode 100644 homeassistant/components/withings/.translations/es-419.json diff --git a/homeassistant/components/ambiclimate/.translations/en.json b/homeassistant/components/ambiclimate/.translations/en.json index 32b4a7e2b249c6..da1e173b4a816b 100644 --- a/homeassistant/components/ambiclimate/.translations/en.json +++ b/homeassistant/components/ambiclimate/.translations/en.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Unknown error generating an access token.", "already_setup": "The Ambiclimate account is configured.", - "no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/ambiclimate/)." + "no_config": "You need to configure Ambiclimate before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Successfully authenticated with Ambiclimate" diff --git a/homeassistant/components/arcam_fmj/.translations/es-419.json b/homeassistant/components/arcam_fmj/.translations/es-419.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/es-419.json b/homeassistant/components/binary_sensor/.translations/es-419.json new file mode 100644 index 00000000000000..f1c20e5346b510 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/es-419.json @@ -0,0 +1,65 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} la bater\u00eda est\u00e1 baja", + "is_cold": "{entity_name} est\u00e1 fr\u00edo", + "is_connected": "{entity_name} est\u00e1 conectado", + "is_gas": "{entity_name} est\u00e1 detectando gas", + "is_hot": "{entity_name} est\u00e1 caliente", + "is_light": "{entity_name} est\u00e1 detectando luz", + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_moist": "{entity_name} est\u00e1 h\u00famedo", + "is_motion": "{entity_name} est\u00e1 detectando movimiento", + "is_moving": "{entity_name} se est\u00e1 moviendo", + "is_no_gas": "{entity_name} no detecta gas", + "is_no_light": "{entity_name} no detecta luz", + "is_no_motion": "{entity_name} no detecta movimiento", + "is_no_problem": "{entity_name} no detecta el problema", + "is_no_smoke": "{entity_name} no detecta humo", + "is_no_sound": "{entity_name} no detecta sonido", + "is_no_vibration": "{entity_name} no detecta vibraciones", + "is_not_bat_low": "{entity_name} bater\u00eda est\u00e1 normal", + "is_not_cold": "{entity_name} no est\u00e1 fr\u00edo", + "is_not_connected": "{entity_name} est\u00e1 desconectado", + "is_not_hot": "{entity_name} no est\u00e1 caliente", + "is_not_locked": "{entity_name} est\u00e1 desbloqueado", + "is_not_moist": "{entity_name} est\u00e1 seco", + "is_not_moving": "{entity_name} no se mueve", + "is_not_occupied": "{entity_name} no est\u00e1 ocupado", + "is_not_open": "{entity_name} est\u00e1 cerrado", + "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_powered": "{entity_name} est\u00e1 encendido", + "is_present": "{entity_name} est\u00e1 presente", + "is_problem": "{entity_name} est\u00e1 detectando un problema", + "is_smoke": "{entity_name} est\u00e1 detectando humo", + "is_sound": "{entity_name} est\u00e1 detectando sonido", + "is_unsafe": "{entity_name} es inseguro", + "is_vibration": "{entity_name} est\u00e1 detectando vibraciones" + }, + "trigger_type": { + "bat_low": "{entity_name} bater\u00eda baja", + "closed": "{entity_name} cerrado", + "cold": "{entity_name} se enfri\u00f3", + "connected": "{entity_name} conectado", + "gas": "{entity_name} comenz\u00f3 a detectar gas", + "hot": "{entity_name} se calent\u00f3", + "light": "{entity_name} comenz\u00f3 a detectar luz", + "locked": "{entity_name} bloqueado", + "moist\u00a7": "{entity_name} se humedeci\u00f3", + "motion": "{entity_name} comenz\u00f3 a detectar movimiento", + "moving": "{entity_name} comenz\u00f3 a moverse", + "no_gas": "{entity_name} dej\u00f3 de detectar gas", + "no_light": "{entity_name} dej\u00f3 de detectar luz", + "no_motion": "{entity_name} dej\u00f3 de detectar movimiento", + "no_problem": "{entity_name} dej\u00f3 de detectar problemas", + "no_smoke": "{entity_name} dej\u00f3 de detectar humo", + "no_sound": "{entity_name} dej\u00f3 de detectar sonido", + "no_vibration": "{entity_name} dej\u00f3 de detectar vibraciones", + "not_bat_low": "{entity_name} bater\u00eda normal", + "not_cold": "{entity_name} no se enfri\u00f3", + "not_connected": "{entity_name} desconectado", + "not_hot": "{entity_name} no se calent\u00f3", + "not_locked": "{entity_name} desbloqueado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/es-419.json b/homeassistant/components/cert_expiry/.translations/es-419.json new file mode 100644 index 00000000000000..392dbf35f5ae13 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Expiraci\u00f3n del certificado" + } +} \ 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 1a5d992ef7b914..448b654c86e42d 100644 --- a/homeassistant/components/deconz/.translations/es-419.json +++ b/homeassistant/components/deconz/.translations/es-419.json @@ -5,7 +5,8 @@ "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" + "one_instance_only": "El componente solo admite una instancia deCONZ", + "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, "error": { "no_key": "No se pudo obtener una clave de API" @@ -16,7 +17,8 @@ "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}?" + "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?", + "title": "deCONZ Zigbee gateway a trav\u00e9s del complemento Hass.io" }, "init": { "data": { @@ -38,5 +40,35 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Ambos botones", + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "close": "Cerrar", + "left": "Izquierda", + "open": "Abrir", + "right": "Derecha", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "remote_button_rotated": "Bot\u00f3n girado \"{subtype}\"", + "remote_gyro_activated": "Dispositivo agitado" + } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + }, + "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/es-419.json b/homeassistant/components/heos/.translations/es-419.json index 66c02884a7e257..4d442a4543b50f 100644 --- a/homeassistant/components/heos/.translations/es-419.json +++ b/homeassistant/components/heos/.translations/es-419.json @@ -3,6 +3,11 @@ "abort": { "already_setup": "Solo puede configurar una sola conexi\u00f3n Heos, ya que ser\u00e1 compatible con todos los dispositivos de la red." }, + "step": { + "user": { + "title": "Con\u00e9ctate a Heos" + } + }, "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 9ddf336c0605c6..67a65f752b48f5 100644 --- a/homeassistant/components/homekit_controller/.translations/es-419.json +++ b/homeassistant/components/homekit_controller/.translations/es-419.json @@ -4,6 +4,7 @@ "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicie el accesorio y vuelva a intentarlo." }, + "flow_title": "Accesorio HomeKit: {name}", "step": { "pair": { "data": { diff --git a/homeassistant/components/iaqualink/.translations/es-419.json b/homeassistant/components/iaqualink/.translations/es-419.json new file mode 100644 index 00000000000000..170c2851d0801b --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/es-419.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario / direcci\u00f3n de correo electr\u00f3nico" + }, + "description": "Por favor, Ingrese el nombre de usuario y la contrase\u00f1a para su cuenta de iAqualink." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/es-419.json b/homeassistant/components/life360/.translations/es-419.json index 3f9bfab3304728..512d0285ac5e32 100644 --- a/homeassistant/components/life360/.translations/es-419.json +++ b/homeassistant/components/life360/.translations/es-419.json @@ -1,5 +1,23 @@ { "config": { + "abort": { + "user_already_configured": "La cuenta ya ha sido configurada" + }, + "error": { + "invalid_credentials": "Credenciales no v\u00e1lidas", + "invalid_username": "Nombre de usuario inv\u00e1lido", + "unexpected": "Error inesperado al comunicarse con el servidor Life360", + "user_already_configured": "La cuenta ya ha sido configurada" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Informaci\u00f3n de la cuenta Life360" + } + }, "title": "Life360" } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/es-419.json b/homeassistant/components/light/.translations/es-419.json new file mode 100644 index 00000000000000..b63f0d4445232d --- /dev/null +++ b/homeassistant/components/light/.translations/es-419.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagada", + "is_on": "{entity_name} est\u00e1 encendida" + }, + "trigger_type": { + "turned_off": "{entity_name} desactivada", + "turned_on": "{entity_name} activada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/es-419.json b/homeassistant/components/linky/.translations/es-419.json new file mode 100644 index 00000000000000..130a856826e9d7 --- /dev/null +++ b/homeassistant/components/linky/.translations/es-419.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "La cuenta ya ha sido configurada" + }, + "error": { + "access": "No se pudo acceder a Enedis.fr, compruebe su conexi\u00f3n a Internet.", + "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11 p.m. y las 2 a.m.)", + "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11 p.m. y las 2 a.m.)", + "username_exists": "La cuenta ya ha sido configurada", + "wrong_login": "Error de inicio de sesi\u00f3n: por favor revise su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "description": "Ingrese sus credenciales", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/en.json b/homeassistant/components/logi_circle/.translations/en.json index 3604eb66ae20df..bf3c059f81ab9b 100644 --- a/homeassistant/components/logi_circle/.translations/en.json +++ b/homeassistant/components/logi_circle/.translations/en.json @@ -4,7 +4,7 @@ "already_setup": "You can only configure a single Logi Circle account.", "external_error": "Exception occurred from another flow.", "external_setup": "Logi Circle successfully configured from another flow.", - "no_flows": "You need to configure Logi Circle before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/logi_circle/)." + "no_flows": "You need to configure Logi Circle before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "Successfully authenticated with Logi Circle." diff --git a/homeassistant/components/nest/.translations/en.json b/homeassistant/components/nest/.translations/en.json index b68c7784d052ff..cf448bb35e7273 100644 --- a/homeassistant/components/nest/.translations/en.json +++ b/homeassistant/components/nest/.translations/en.json @@ -4,7 +4,7 @@ "already_setup": "You can only configure a single Nest account.", "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", - "no_flows": "You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/nest/)." + "no_flows": "You need to configure Nest before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Internal error validating code", diff --git a/homeassistant/components/nest/.translations/es-419.json b/homeassistant/components/nest/.translations/es-419.json index 78239148a4ea5e..60a3eb65ca97e3 100644 --- a/homeassistant/components/nest/.translations/es-419.json +++ b/homeassistant/components/nest/.translations/es-419.json @@ -6,7 +6,8 @@ "no_flows": "Debe configurar Nest antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/nest/)." }, "error": { - "invalid_code": "Codigo invalido" + "invalid_code": "Codigo invalido", + "unknown": "Error desconocido al validar el c\u00f3digo" }, "step": { "init": { diff --git a/homeassistant/components/notion/.translations/es-419.json b/homeassistant/components/notion/.translations/es-419.json index ad2f19b0668ce1..1f4968f24e131f 100644 --- a/homeassistant/components/notion/.translations/es-419.json +++ b/homeassistant/components/notion/.translations/es-419.json @@ -1,6 +1,7 @@ { "config": { "error": { + "identifier_exists": "Nombre de usuario ya registrado", "invalid_credentials": "Nombre de usuario o contrase\u00f1a inv\u00e1lidos", "no_devices": "No se han encontrado dispositivos en la cuenta." }, diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 670bc23ca1fd3b..1da4b4b4b49364 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -5,6 +5,7 @@ "already_configured": "Denne Plex-server er allerede konfigureret", "already_in_progress": "Plex konfigureres", "invalid_import": "Importeret konfiguration er ugyldig", + "token_request_timeout": "Timeout ved hentning af token", "unknown": "Mislykkedes af ukendt \u00e5rsag" }, "error": { diff --git a/homeassistant/components/plex/.translations/es-419.json b/homeassistant/components/plex/.translations/es-419.json new file mode 100644 index 00000000000000..2fc98a70ead8b7 --- /dev/null +++ b/homeassistant/components/plex/.translations/es-419.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "all_configured": "Todos los servidores vinculados ya fueron configurados", + "already_configured": "Este servidor Plex ya est\u00e1 configurado", + "already_in_progress": "Plex se est\u00e1 configurando", + "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "token_request_timeout": "Se agot\u00f3 el tiempo de espera para obtener el token", + "unknown": "Fall\u00f3 por razones desconocidas" + }, + "error": { + "faulty_credentials": "Autorizaci\u00f3n fallida", + "no_servers": "No hay servidores vinculados a la cuenta", + "no_token": "Proporcione un token o seleccione la configuraci\u00f3n manual", + "not_found": "Servidor Plex no encontrado" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Puerto", + "ssl": "Usar SSL", + "token": "Token (si es necesario)", + "verify_ssl": "Verificar el certificado SSL" + }, + "title": "Servidor Plex" + }, + "select_server": { + "data": { + "server": "Servidor" + }, + "description": "M\u00faltiples servidores disponibles, seleccione uno:", + "title": "Seleccionar servidor Plex" + }, + "user": { + "data": { + "manual_setup": "Configuraci\u00f3n manual", + "token": "Token Plex" + }, + "title": "Conectar servidor Plex" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos los controles" + }, + "description": "Opciones para reproductores multimedia Plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index f7a6bfd9c7f1b8..a0a9d087d1e58b 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -5,6 +5,7 @@ "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Plex blir konfigurert", "invalid_import": "Den importerte konfigurasjonen er ugyldig", + "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Mislyktes av ukjent \u00e5rsak" }, "error": { @@ -36,7 +37,7 @@ "manual_setup": "Manuelt oppsett", "token": "Plex token" }, - "description": "Angi et Plex-token for automatisk oppsett eller Konfigurer en servern manuelt.", + "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv eller manuelt konfigurere en server.", "title": "Koble til Plex-server" } }, diff --git a/homeassistant/components/point/.translations/en.json b/homeassistant/components/point/.translations/en.json index 25f0545c340942..705ac59b98d010 100644 --- a/homeassistant/components/point/.translations/en.json +++ b/homeassistant/components/point/.translations/en.json @@ -5,7 +5,7 @@ "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", "external_setup": "Point successfully configured from another flow.", - "no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/point/)." + "no_flows": "You need to configure Point before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/point/)." }, "create_entry": { "default": "Successfully authenticated with Minut for your Point device(s)" diff --git a/homeassistant/components/ps4/.translations/en.json b/homeassistant/components/ps4/.translations/en.json index 3a7223ade29d99..756eb65d4f7c58 100644 --- a/homeassistant/components/ps4/.translations/en.json +++ b/homeassistant/components/ps4/.translations/en.json @@ -4,8 +4,8 @@ "credential_error": "Error fetching credentials.", "devices_configured": "All devices found are already configured.", "no_devices_found": "No PlayStation 4 devices found on the network.", - "port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info.", - "port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info." + "port_987_bind_error": "Could not bind to port 987. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", + "port_997_bind_error": "Could not bind to port 997. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info." }, "error": { "credential_timeout": "Credential service timed out. Press submit to restart.", @@ -25,7 +25,7 @@ "name": "Name", "region": "Region" }, - "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed. Refer to the [documentation](https://www.home-assistant.io/integrations/ps4/) for additional info.", + "description": "Enter your PlayStation 4 information. For 'PIN', navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN that is displayed. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json index 460f01e301feba..a82da0ce24db9f 100644 --- a/homeassistant/components/soma/.translations/da.json +++ b/homeassistant/components/soma/.translations/da.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_setup": "Du kan kun konfigurere en Soma-konto.", + "authorize_url_timeout": "Timeout ved generering af autoriseret url.", "missing_configuration": "Soma-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen." }, "create_entry": { diff --git a/homeassistant/components/somfy/.translations/es-419.json b/homeassistant/components/somfy/.translations/es-419.json new file mode 100644 index 00000000000000..ff0383c7f015cf --- /dev/null +++ b/homeassistant/components/somfy/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/es-419.json b/homeassistant/components/switch/.translations/es-419.json new file mode 100644 index 00000000000000..f960785203606b --- /dev/null +++ b/homeassistant/components/switch/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Desactivar {entity_name}", + "turn_on": "Activar {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido", + "turn_off": "{entity_name} apagado", + "turn_on": "{entity_name} encendido" + }, + "trigger_type": { + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/es-419.json b/homeassistant/components/tellduslive/.translations/es-419.json index 503530e728a3bb..1281784dceb93a 100644 --- a/homeassistant/components/tellduslive/.translations/es-419.json +++ b/homeassistant/components/tellduslive/.translations/es-419.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_setup": "TelldusLive ya est\u00e1 configurado", + "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", "unknown": "Se produjo un error desconocido" }, "error": { @@ -17,6 +18,7 @@ "host": "Host" } } - } + }, + "title": "Telldus Live" } } \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/en.json b/homeassistant/components/toon/.translations/en.json index dde5165c5c17f7..cea3146a3a557f 100644 --- a/homeassistant/components/toon/.translations/en.json +++ b/homeassistant/components/toon/.translations/en.json @@ -4,7 +4,7 @@ "client_id": "The client ID from the configuration is invalid.", "client_secret": "The client secret from the configuration is invalid.", "no_agreements": "This account has no Toon displays.", - "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/integrations/toon/).", + "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Unexpected error occured, while authenticating." }, "error": { diff --git a/homeassistant/components/toon/.translations/es-419.json b/homeassistant/components/toon/.translations/es-419.json index a0ce81495a8bbb..598bc77aee9bdb 100644 --- a/homeassistant/components/toon/.translations/es-419.json +++ b/homeassistant/components/toon/.translations/es-419.json @@ -1,17 +1,27 @@ { "config": { "abort": { + "no_agreements": "Esta cuenta no tiene pantallas Toon.", "unknown_auth_fail": "Ocurri\u00f3 un error inesperado, mientras se autenticaba." }, "error": { - "credentials": "Las credenciales proporcionadas no son v\u00e1lidas." + "credentials": "Las credenciales proporcionadas no son v\u00e1lidas.", + "display_exists": "La pantalla seleccionada ya est\u00e1 configurada." }, "step": { "authenticate": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" - } + }, + "title": "Vincula tu cuenta de Toon" + }, + "display": { + "data": { + "display": "Elegir pantalla" + }, + "description": "Seleccione la pantalla Toon para conectarse.", + "title": "Seleccionar pantalla" } }, "title": "Toon" diff --git a/homeassistant/components/traccar/.translations/es-419.json b/homeassistant/components/traccar/.translations/es-419.json new file mode 100644 index 00000000000000..bfe62cc4e78466 --- /dev/null +++ b/homeassistant/components/traccar/.translations/es-419.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes de Traccar.", + "one_instance_allowed": "Solo una instancia es necesaria." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/es-419.json b/homeassistant/components/twentemilieu/.translations/es-419.json new file mode 100644 index 00000000000000..02ac8ecf27a2e9 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/es-419.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/es-419.json b/homeassistant/components/velbus/.translations/es-419.json new file mode 100644 index 00000000000000..1e1e8897c30155 --- /dev/null +++ b/homeassistant/components/velbus/.translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Este puerto ya est\u00e1 configurado" + }, + "error": { + "connection_failed": "La conexi\u00f3n velbus fall\u00f3", + "port_exists": "Este puerto ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "name": "El nombre de esta conexi\u00f3n velbus", + "port": "Cadena de conexi\u00f3n" + }, + "title": "Definir el tipo de conexi\u00f3n velbus" + } + }, + "title": "Interfaz Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/es-419.json b/homeassistant/components/vesync/.translations/es-419.json new file mode 100644 index 00000000000000..58c62fb64b60a3 --- /dev/null +++ b/homeassistant/components/vesync/.translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Solo se permite una instancia de Vesync" + }, + "error": { + "invalid_login": "Nombre de usuario o contrase\u00f1a inv\u00e1lidos" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Ingrese nombre de usuario y contrase\u00f1a" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/es-419.json b/homeassistant/components/withings/.translations/es-419.json new file mode 100644 index 00000000000000..485150d29288c1 --- /dev/null +++ b/homeassistant/components/withings/.translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "no_flows": "Debe configurar Withings antes de poder autenticarse con \u00e9l. Por favor lea la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado correctamente con Withings para el perfil seleccionado." + }, + "step": { + "user": { + "data": { + "profile": "Perfil" + }, + "title": "Perfil del usuario." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/es-419.json b/homeassistant/components/zha/.translations/es-419.json index 0047c762a9de0d..edf38b4fd3bdf7 100644 --- a/homeassistant/components/zha/.translations/es-419.json +++ b/homeassistant/components/zha/.translations/es-419.json @@ -16,5 +16,23 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "left": "Izquierda", + "open": "Abrir", + "right": "Derecha", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "device_dropped": "Dispositivo ca\u00eddo", + "device_flipped": "Dispositivo volteado \"{subtype}\"", + "device_knocked": "Dispositivo golpeado \"{subtype}\"", + "device_rotated": "Dispositivo girado \"{subtype}\"", + "device_shaken": "Dispositivo agitado", + "device_slid": "Dispositivo deslizado \"{subtype}\"", + "device_tilted": "Dispositivo inclinado" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index cd618072592422..2f6f42311c3490 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -16,5 +16,10 @@ } }, "title": "Zigbee Home Automation" + }, + "device_automation": { + "action_type": { + "warn": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435" + } } } \ No newline at end of file From 3e9974324480fe113003b0c80a5be267d017691c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 06:14:35 +0200 Subject: [PATCH 0566/3953] Add device trigger support to sensor entities (#27133) * Add device trigger support to sensor entities * Fix typing * Fix tests, add test helper for comparing lists --- .../components/automation/numeric_state.py | 6 +- .../binary_sensor/device_trigger.py | 6 +- .../device_automation/toggle_entity.py | 4 +- .../components/sensor/device_trigger.py | 145 +++++++ homeassistant/components/sensor/strings.json | 26 ++ tests/common.py | 83 ++++ .../components/deconz/test_device_trigger.py | 11 +- .../components/sensor/test_device_trigger.py | 368 ++++++++++++++++++ tests/conftest.py | 7 +- .../custom_components/test/sensor.py | 44 +++ 10 files changed, 689 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/sensor/device_trigger.py create mode 100644 homeassistant/components/sensor/strings.json create mode 100644 tests/components/sensor/test_device_trigger.py create mode 100644 tests/testing_config/custom_components/test/sensor.py diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 9dd4657291dc21..8d88fe9cae6823 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -40,7 +40,9 @@ _LOGGER = logging.getLogger(__name__) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="numeric_state" +): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) @@ -84,7 +86,7 @@ def call_action(): action( { "trigger": { - "platform": "numeric_state", + "platform": platform_type, "entity_id": entity, "below": below, "above": above, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index c4d2efcb63b2bb..89fd9add69a940 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -195,8 +195,8 @@ async def async_attach_trigger(hass, config, action, automation_info): state_automation.CONF_FROM: from_state, state_automation.CONF_TO: to_state, } - if "for" in config: - state_config["for"] = config["for"] + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] return await state_automation.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" @@ -215,7 +215,7 @@ async def async_get_triggers(hass, device_id): ] for entry in entries: - device_class = None + device_class = DEVICE_CLASS_NONE state = hass.states.get(entry.entity_id) if state: device_class = state.attributes.get(ATTR_DEVICE_CLASS) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 7c68be83ba30ec..c9588c1efa7385 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -155,8 +155,8 @@ async def async_attach_trigger( state.CONF_FROM: from_state, state.CONF_TO: to_state, } - if "for" in config: - state_config["for"] = config["for"] + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] return await state.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py new file mode 100644 index 00000000000000..1074236eedf843 --- /dev/null +++ b/homeassistant/components/sensor/device_trigger.py @@ -0,0 +1,145 @@ +"""Provides device triggers for sensors.""" +import voluptuous as vol + +import homeassistant.components.automation.numeric_state as numeric_state_automation +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_ABOVE, + CONF_BELOW, + CONF_ENTITY_ID, + CONF_FOR, + CONF_TYPE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, +) +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import config_validation as cv + +from . import DOMAIN + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_BATTERY_LEVEL = "battery_level" +CONF_HUMIDITY = "humidity" +CONF_ILLUMINANCE = "illuminance" +CONF_POWER = "power" +CONF_PRESSURE = "pressure" +CONF_SIGNAL_STRENGTH = "signal_strength" +CONF_TEMPERATURE = "temperature" +CONF_TIMESTAMP = "timestamp" +CONF_VALUE = "value" + +ENTITY_TRIGGERS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], + DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], + DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWER}], + DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], + DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], + DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], + DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_TIMESTAMP}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}], +} + + +TRIGGER_SCHEMA = vol.All( + TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In( + [ + CONF_BATTERY_LEVEL, + CONF_HUMIDITY, + CONF_ILLUMINANCE, + CONF_POWER, + CONF_PRESSURE, + CONF_SIGNAL_STRENGTH, + CONF_TEMPERATURE, + CONF_TIMESTAMP, + CONF_VALUE, + ] + ), + vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_FOR): vol.Any( + vol.All(cv.time_period, cv.positive_timedelta), + cv.template, + cv.template_complex, + ), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + numeric_state_config = { + numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE), + numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW), + numeric_state_automation.CONF_FOR: config.get(CONF_FOR), + } + if CONF_FOR in config: + numeric_state_config[CONF_FOR] = config[CONF_FOR] + + return await numeric_state_automation.async_attach_trigger( + hass, numeric_state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + triggers = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + if state: + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + templates = ENTITY_TRIGGERS.get( + device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] + ) + + triggers.extend( + ( + { + **automation, + "platform": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for automation in templates + ) + ) + + return triggers + + +async def async_get_trigger_capabilities(hass, trigger): + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json new file mode 100644 index 00000000000000..7df239facde721 --- /dev/null +++ b/homeassistant/components/sensor/strings.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} battery level", + "is_humidity": "{entity_name} humidity", + "is_illuminance": "{entity_name} illuminance", + "is_power": "{entity_name} power", + "is_pressure": "{entity_name} pressure", + "is_signal_strength": "{entity_name} signal strength", + "is_temperature": "{entity_name} temperature", + "is_timestamp": "{entity_name} timestamp", + "is_value": "{entity_name} value" + }, + "trigger_type": { + "battery_level": "{entity_name} battery level", + "humidity": "{entity_name} humidity", + "illuminance": "{entity_name} illuminance", + "power": "{entity_name} power", + "pressure": "{entity_name} pressure", + "signal_strength": "{entity_name} signal strength", + "temperature": "{entity_name} temperature", + "timestamp": "{entity_name} timestamp", + "value": "{entity_name} value" + } + } +} diff --git a/tests/common.py b/tests/common.py index bd611e04c37a6e..0684e6daafcbef 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,5 +1,6 @@ """Test the helper method for writing tests.""" import asyncio +import collections import functools as ft import json import logging @@ -1050,3 +1051,85 @@ def mock_signal_handler(*args): hass.helpers.dispatcher.async_dispatcher_connect(signal, mock_signal_handler) return calls + + +class hashdict(dict): + """ + hashable dict implementation, suitable for use as a key into other dicts. + + >>> h1 = hashdict({"apples": 1, "bananas":2}) + >>> h2 = hashdict({"bananas": 3, "mangoes": 5}) + >>> h1+h2 + hashdict(apples=1, bananas=3, mangoes=5) + >>> d1 = {} + >>> d1[h1] = "salad" + >>> d1[h1] + 'salad' + >>> d1[h2] + Traceback (most recent call last): + ... + KeyError: hashdict(bananas=3, mangoes=5) + + based on answers from + http://stackoverflow.com/questions/1151658/python-hashable-dicts + + """ + + def __key(self): # noqa: D105 no docstring + return tuple(sorted(self.items())) + + def __repr__(self): # noqa: D105 no docstring + return ", ".join("{0}={1}".format(str(i[0]), repr(i[1])) for i in self.__key()) + + def __hash__(self): # noqa: D105 no docstring + return hash(self.__key()) + + def __setitem__(self, key, value): # noqa: D105 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def __delitem__(self, key): # noqa: D105 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def clear(self): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def pop(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def popitem(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def setdefault(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def update(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + # update is not ok because it mutates the object + # __add__ is ok because it creates a new object + # while the new object is under construction, it's ok to mutate it + def __add__(self, right): # noqa: D105 no docstring + result = hashdict(self) + dict.update(result, right) + return result + + +def assert_lists_same(a, b): + """Compare two lists, ignoring order.""" + assert collections.Counter([hashdict(i) for i in a]) == collections.Counter( + [hashdict(i) for i in b] + ) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 4677ea8d5a7c77..91714e647bd9d3 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -3,7 +3,7 @@ from homeassistant.components.deconz import device_trigger -from tests.common import async_get_device_automations +from tests.common import assert_lists_same, async_get_device_automations from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration @@ -83,6 +83,13 @@ async def test_get_triggers(hass): "type": device_trigger.CONF_LONG_RELEASE, "subtype": device_trigger.CONF_TURN_OFF, }, + { + "device_id": device_id, + "domain": "sensor", + "entity_id": "sensor.tradfri_on_off_switch_battery_level", + "platform": "device", + "type": "battery_level", + }, ] - assert triggers == expected_triggers + assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py new file mode 100644 index 00000000000000..1dc41f5bffa247 --- /dev/null +++ b/tests/components/sensor/test_device_trigger.py @@ -0,0 +1,368 @@ +"""The test for sensor device automation.""" +from datetime import timedelta +import pytest + +from homeassistant.components.sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS +from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, + async_get_device_automation_capabilities, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": trigger["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for trigger in ENTITY_TRIGGERS[device_class] + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a binary_sensor trigger.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + +async def test_if_fires_not_on_above_below(hass, calls, caplog): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + }, + "action": {"service": "test.automation"}, + } + ] + }, + ) + assert "must contain at least one of below, above" in caplog.text + + +async def test_if_fires_on_state_above(hass, calls): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "above": 10, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "bat_low device - {} - 9 - 11 - None".format( + sensor1.entity_id + ) + + +async def test_if_fires_on_state_below(hass, calls): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "below": 10, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "bat_low device - {} - 11 - 9 - None".format( + sensor1.entity_id + ) + + +async def test_if_fires_on_state_between(hass, calls): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "above": 10, + "below": 20, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "bat_low device - {} - 9 - 11 - None".format( + sensor1.entity_id + ) + + hass.states.async_set(sensor1.entity_id, 21) + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_set(sensor1.entity_id, 19) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "bat_low device - {} - 21 - 19 - None".format( + sensor1.entity_id + ) + + +async def test_if_fires_on_state_change_with_for(hass, calls): + """Test for triggers firing with delay.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "above": 10, + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + await hass.async_block_till_done() + assert calls[0].data[ + "some" + ] == "turn_off device - {} - unknown - 11 - 0:00:05".format(sensor1.entity_id) diff --git a/tests/conftest.py b/tests/conftest.py index 36c0f52f41a6b8..5e1bbc76fb5641 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,8 @@ from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.providers import legacy_api_password, homeassistant -from tests.common import ( +pytest.register_assert_rewrite("tests.common") +from tests.common import ( # noqa: E402 module level import not at top of file async_test_home_assistant, INSTANCES, mock_coro, @@ -21,7 +22,9 @@ MockUser, CLIENT_ID, ) -from tests.test_util.aiohttp import mock_aiohttp_client +from tests.test_util.aiohttp import ( + mock_aiohttp_client, +) # noqa: E402 module level import not at top of file if os.environ.get("UVLOOP") == "1": import uvloop diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py new file mode 100644 index 00000000000000..c25be28fdb03ee --- /dev/null +++ b/tests/testing_config/custom_components/test/sensor.py @@ -0,0 +1,44 @@ +""" +Provide a mock sensor platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.sensor import DEVICE_CLASSES +from tests.common import MockEntity + + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + device_class: MockSensor( + name=f"{device_class} sensor", + unique_id=f"unique_{device_class}", + device_class=device_class, + ) + for device_class in DEVICE_CLASSES + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockSensor(MockEntity): + """Mock Sensor class.""" + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._handle("device_class") From f184bf4d8506c47db0860c2497900c5c44c17233 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 Oct 2019 04:02:38 -0700 Subject: [PATCH 0567/3953] Add Google Report State (#27112) * Add Google Report State * UPDATE codeowners" * Add config option for dev mode * update library * lint * Bug fixes --- CODEOWNERS | 2 + homeassistant/components/alexa/manifest.json | 6 +- homeassistant/components/cloud/__init__.py | 4 +- homeassistant/components/cloud/client.py | 16 ++- homeassistant/components/cloud/const.py | 3 + .../components/cloud/google_config.py | 113 +++++++++++++++- homeassistant/components/cloud/http_api.py | 18 +-- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/components/cloud/prefs.py | 10 ++ .../components/google_assistant/__init__.py | 7 + .../components/google_assistant/helpers.py | 89 ++++++++++++- .../components/google_assistant/http.py | 10 +- .../components/google_assistant/manifest.json | 6 +- .../google_assistant/report_state.py | 39 ++++++ .../components/google_assistant/smart_home.py | 3 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_alexa_config.py | 8 +- tests/components/cloud/test_google_config.py | 121 ++++++++++++++++++ tests/components/cloud/test_http_api.py | 5 +- tests/components/cloud/test_init.py | 17 +++ tests/components/google_assistant/__init__.py | 8 +- .../components/google_assistant/test_init.py | 6 +- .../google_assistant/test_report_state.py | 47 +++++++ .../google_assistant/test_smart_home.py | 20 ++- 26 files changed, 510 insertions(+), 56 deletions(-) create mode 100644 homeassistant/components/google_assistant/report_state.py create mode 100644 tests/components/cloud/test_google_config.py create mode 100644 tests/components/google_assistant/test_report_state.py diff --git a/CODEOWNERS b/CODEOWNERS index 2bfebf145dfada..4e7b0a0cd2ab83 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -16,6 +16,7 @@ homeassistant/scripts/check_config.py @kellerza homeassistant/components/adguard/* @frenck homeassistant/components/airvisual/* @bachya homeassistant/components/alarm_control_panel/* @colinodell +homeassistant/components/alexa/* @home-assistant/cloud homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen @@ -106,6 +107,7 @@ homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff homeassistant/components/gntp/* @robbiet480 +homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton homeassistant/components/google_translate/* @awarecan homeassistant/components/google_travel_time/* @robbiet480 diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index c6629982d53fae..9db7e270e6137b 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -3,8 +3,6 @@ "name": "Alexa", "documentation": "https://www.home-assistant.io/integrations/alexa", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [] + "dependencies": ["http"], + "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 8b295634c99e58..71550fc37b14ba 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -34,6 +34,7 @@ CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, CONF_USER_POOL_ID, + CONF_GOOGLE_ACTIONS_REPORT_STATE_URL, DOMAIN, MODE_DEV, MODE_PROD, @@ -96,7 +97,8 @@ vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(), vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, - vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): str, + vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(), + vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(), } ) }, diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 07882d8dac202c..38ae09ced9399b 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -98,7 +98,7 @@ def google_config(self) -> google_config.CloudGoogleConfig: if not self._google_config: assert self.cloud is not None self._google_config = google_config.CloudGoogleConfig( - self.google_user_config, self._prefs, self.cloud + self._hass, self.google_user_config, self._prefs, self.cloud ) return self._google_config @@ -107,13 +107,17 @@ async def async_initialize(self, cloud) -> None: """Initialize the client.""" self.cloud = cloud - if not self.alexa_config.should_report_state or not self.cloud.is_logged_in: + if not self.cloud.is_logged_in: return - try: - await self.alexa_config.async_enable_proactive_mode() - except alexa_errors.NoTokenAvailable: - pass + if self.alexa_config.should_report_state: + try: + await self.alexa_config.async_enable_proactive_mode() + except alexa_errors.NoTokenAvailable: + pass + + if self.google_config.should_report_state: + self.google_config.async_enable_report_state() async def cleanups(self) -> None: """Cleanup some stuff after logout.""" diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index df1b8ef165d773..e28d75f017de74 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -9,6 +9,7 @@ PREF_CLOUDHOOKS = "cloudhooks" PREF_CLOUD_USER = "cloud_user" PREF_GOOGLE_ENTITY_CONFIGS = "google_entity_configs" +PREF_GOOGLE_REPORT_STATE = "google_report_state" PREF_ALEXA_ENTITY_CONFIGS = "alexa_entity_configs" PREF_ALEXA_REPORT_STATE = "alexa_report_state" PREF_OVERRIDE_NAME = "override_name" @@ -18,6 +19,7 @@ DEFAULT_SHOULD_EXPOSE = True DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False +DEFAULT_GOOGLE_REPORT_STATE = False CONF_ALEXA = "alexa" CONF_ALIASES = "aliases" @@ -33,6 +35,7 @@ CONF_REMOTE_API_URL = "remote_api_url" CONF_ACME_DIRECTORY_SERVER = "acme_directory_server" CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url" +CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url" MODE_DEV = "development" MODE_PROD = "production" diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 8986f8f399547d..38e4aec56e07ca 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -1,6 +1,13 @@ """Google config for Cloud.""" +import asyncio +import logging + +import async_timeout +from hass_nabucasa.google_report_state import ErrorResponse + from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.components.google_assistant.helpers import AbstractConfig +from homeassistant.helpers import entity_registry from .const import ( PREF_SHOULD_EXPOSE, @@ -10,15 +17,31 @@ DEFAULT_DISABLE_2FA, ) +_LOGGER = logging.getLogger(__name__) + class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, config, prefs, cloud): - """Initialize the Alexa config.""" + def __init__(self, hass, config, prefs, cloud): + """Initialize the Google config.""" + super().__init__(hass) self._config = config self._prefs = prefs self._cloud = cloud + self._cur_entity_prefs = self._prefs.google_entity_configs + self._sync_entities_lock = asyncio.Lock() + + prefs.async_listen_updates(self._async_prefs_updated) + hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + self._handle_entity_registry_updated, + ) + + @property + def enabled(self): + """Return if Google is enabled.""" + return self._prefs.google_enabled @property def agent_user_id(self): @@ -35,16 +58,25 @@ def secure_devices_pin(self): """Return entity config.""" return self._prefs.google_secure_devices_pin + @property + def should_report_state(self): + """Return if states should be proactively reported.""" + return self._prefs.google_report_state + def should_expose(self, state): - """If an entity should be exposed.""" - if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: + """If a state object should be exposed.""" + return self._should_expose_entity_id(state.entity_id) + + def _should_expose_entity_id(self, entity_id): + """If an entity ID should be exposed.""" + if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: return False if not self._config["filter"].empty_filter: - return self._config["filter"](state.entity_id) + return self._config["filter"](entity_id) entity_configs = self._prefs.google_entity_configs - entity_config = entity_configs.get(state.entity_id, {}) + entity_config = entity_configs.get(entity_id, {}) return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE) def should_2fa(self, state): @@ -52,3 +84,72 @@ def should_2fa(self, state): entity_configs = self._prefs.google_entity_configs entity_config = entity_configs.get(state.entity_id, {}) return not entity_config.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) + + async def async_report_state(self, message): + """Send a state report to Google.""" + try: + await self._cloud.google_report_state.async_send_message(message) + except ErrorResponse as err: + _LOGGER.warning("Error reporting state - %s: %s", err.code, err.message) + + async def _async_request_sync_devices(self): + """Trigger a sync with Google.""" + if self._sync_entities_lock.locked(): + return 200 + + websession = self.hass.helpers.aiohttp_client.async_get_clientsession() + + async with self._sync_entities_lock: + with async_timeout.timeout(10): + await self._cloud.auth.async_check_token() + + _LOGGER.debug("Requesting sync") + + with async_timeout.timeout(30): + req = await websession.post( + self._cloud.google_actions_sync_url, + headers={"authorization": self._cloud.id_token}, + ) + _LOGGER.debug("Finished requesting syncing: %s", req.status) + return req.status + + async def async_deactivate_report_state(self): + """Turn off report state and disable further state reporting. + + Called when the user disconnects their account from Google. + """ + await self._prefs.async_update(google_report_state=False) + + async def _async_prefs_updated(self, prefs): + """Handle updated preferences.""" + if self.should_report_state != self.is_reporting_state: + if self.should_report_state: + self.async_enable_report_state() + else: + self.async_disable_report_state() + + # State reporting is reported as a property on entities. + # So when we change it, we need to sync all entities. + await self.async_sync_entities() + return + + # If entity prefs are the same or we have filter in config.yaml, + # don't sync. + if ( + self._cur_entity_prefs is prefs.google_entity_configs + or not self._config["filter"].empty_filter + ): + return + + self.async_schedule_google_sync() + + async def _handle_entity_registry_updated(self, event): + """Handle when entity registry updated.""" + if not self.enabled or not self._cloud.is_logged_in: + return + + entity_id = event.data["entity_id"] + + # Schedule a sync if a change was made to an entity that Google knows about + if self._should_expose_entity_id(entity_id): + await self.async_sync_entities() diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index fce530ddce5dc7..f243eab8fd0df4 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -7,6 +7,7 @@ import aiohttp import async_timeout import voluptuous as vol +from hass_nabucasa import Cloud from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView @@ -28,6 +29,7 @@ InvalidTrustedNetworks, InvalidTrustedProxies, PREF_ALEXA_REPORT_STATE, + PREF_GOOGLE_REPORT_STATE, RequireRelink, ) @@ -171,18 +173,9 @@ class GoogleActionsSyncView(HomeAssistantView): async def post(self, request): """Trigger a Google Actions sync.""" hass = request.app["hass"] - cloud = hass.data[DOMAIN] - websession = hass.helpers.aiohttp_client.async_get_clientsession() - - with async_timeout.timeout(REQUEST_TIMEOUT): - await hass.async_add_job(cloud.auth.check_token) - - with async_timeout.timeout(REQUEST_TIMEOUT): - req = await websession.post( - cloud.google_actions_sync_url, headers={"authorization": cloud.id_token} - ) - - return self.json({}, status_code=req.status) + cloud: Cloud = hass.data[DOMAIN] + status = await cloud.client.google_config.async_sync_entities() + return self.json({}, status_code=status) class CloudLoginView(HomeAssistantView): @@ -366,6 +359,7 @@ async def websocket_subscription(hass, connection, msg): vol.Optional(PREF_ENABLE_GOOGLE): bool, vol.Optional(PREF_ENABLE_ALEXA): bool, vol.Optional(PREF_ALEXA_REPORT_STATE): bool, + vol.Optional(PREF_GOOGLE_REPORT_STATE): bool, vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str), } ) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index b15fa32cb1378c..c8fa6884563d41 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/integrations/cloud", - "requirements": ["hass-nabucasa==0.17"], + "requirements": ["hass-nabucasa==0.22"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index d6e78e87e25422..a8ff775a22788a 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -20,6 +20,8 @@ PREF_ALEXA_ENTITY_CONFIGS, PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE, + PREF_GOOGLE_REPORT_STATE, + DEFAULT_GOOGLE_REPORT_STATE, InvalidTrustedNetworks, InvalidTrustedProxies, ) @@ -74,6 +76,7 @@ async def async_update( google_entity_configs=_UNDEF, alexa_entity_configs=_UNDEF, alexa_report_state=_UNDEF, + google_report_state=_UNDEF, ): """Update user preferences.""" for key, value in ( @@ -86,6 +89,7 @@ async def async_update( (PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs), (PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs), (PREF_ALEXA_REPORT_STATE, alexa_report_state), + (PREF_GOOGLE_REPORT_STATE, google_report_state), ): if value is not _UNDEF: self._prefs[key] = value @@ -164,6 +168,7 @@ def as_dict(self): PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs, PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs, PREF_ALEXA_REPORT_STATE: self.alexa_report_state, + PREF_GOOGLE_REPORT_STATE: self.google_report_state, PREF_CLOUDHOOKS: self.cloudhooks, PREF_CLOUD_USER: self.cloud_user, } @@ -196,6 +201,11 @@ def google_enabled(self): """Return if Google is enabled.""" return self._prefs[PREF_ENABLE_GOOGLE] + @property + def google_report_state(self): + """Return if Google report state is enabled.""" + return self._prefs.get(PREF_GOOGLE_REPORT_STATE, DEFAULT_GOOGLE_REPORT_STATE) + @property def google_secure_devices_pin(self): """Return if Google is allowed to unlock locks.""" diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 61e0c70b6b368e..a1252d67fff4c3 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -83,6 +83,13 @@ async def request_sync_service_handler(call: ServiceCall): try: with async_timeout.timeout(15): agent_user_id = call.data.get("agent_user_id") or call.context.user_id + + if agent_user_id is None: + _LOGGER.warning( + "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." + ) + return + res = await websession.post( REQUEST_SYNC_BASE_URL, params={"key": api_key}, diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index daaf790a0c1521..207194d79ed0e1 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -3,7 +3,8 @@ from collections.abc import Mapping from typing import List -from homeassistant.core import Context, callback +from homeassistant.core import Context, callback, HomeAssistant, State +from homeassistant.helpers.event import async_call_later from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, @@ -22,10 +23,24 @@ ) from .error import SmartHomeError +SYNC_DELAY = 15 + class AbstractConfig: """Hold the configuration for Google Assistant.""" + _unsub_report_state = None + + def __init__(self, hass): + """Initialize abstract config.""" + self.hass = hass + self._google_sync_unsub = None + + @property + def enabled(self): + """Return if Google is enabled.""" + return False + @property def agent_user_id(self): """Return Agent User Id to use for query responses.""" @@ -41,6 +56,17 @@ def secure_devices_pin(self): """Return entity config.""" return None + @property + def is_reporting_state(self): + """Return if we're actively reporting states.""" + return self._unsub_report_state is not None + + @property + def should_report_state(self): + """Return if states should be proactively reported.""" + # pylint: disable=no-self-use + return False + def should_expose(self, state) -> bool: """Return if entity should be exposed.""" raise NotImplementedError @@ -50,11 +76,66 @@ def should_2fa(self, state): # pylint: disable=no-self-use return True + async def async_report_state(self, message): + """Send a state report to Google.""" + raise NotImplementedError + + def async_enable_report_state(self): + """Enable proactive mode.""" + # Circular dep + from .report_state import async_enable_report_state + + if self._unsub_report_state is None: + self._unsub_report_state = async_enable_report_state(self.hass, self) + + def async_disable_report_state(self): + """Disable report state.""" + if self._unsub_report_state is not None: + self._unsub_report_state() + self._unsub_report_state = None + + async def async_sync_entities(self): + """Sync all entities to Google.""" + # Remove any pending sync + if self._google_sync_unsub: + self._google_sync_unsub() + self._google_sync_unsub = None + + return await self._async_request_sync_devices() + + async def _schedule_callback(self, _now): + """Handle a scheduled sync callback.""" + self._google_sync_unsub = None + await self.async_sync_entities() + + @callback + def async_schedule_google_sync(self): + """Schedule a sync.""" + if self._google_sync_unsub: + self._google_sync_unsub() + + self._google_sync_unsub = async_call_later( + self.hass, SYNC_DELAY, self._schedule_callback + ) + + async def _async_request_sync_devices(self) -> int: + """Trigger a sync with Google. + + Return value is the HTTP status code of the sync request. + """ + raise NotImplementedError + + async def async_deactivate_report_state(self): + """Turn off report state and disable further state reporting. + + Called when the user disconnects their account from Google. + """ + class RequestData: """Hold data associated with a particular request.""" - def __init__(self, config, user_id, request_id): + def __init__(self, config: AbstractConfig, user_id: str, request_id: str): """Initialize the request data.""" self.config = config self.request_id = request_id @@ -71,7 +152,7 @@ def get_google_type(domain, device_class): class GoogleEntity: """Adaptation of Entity expressed in Google's terms.""" - def __init__(self, hass, config, state): + def __init__(self, hass: HomeAssistant, config: AbstractConfig, state: State): """Initialize a Google entity.""" self.hass = hass self.config = config @@ -139,7 +220,7 @@ async def sync_serialize(self): "name": {"name": name}, "attributes": {}, "traits": [trait.name for trait in traits], - "willReportState": False, + "willReportState": self.config.should_report_state, "type": device_type, } diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index d68650fb6384c3..aea226348b803c 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -25,10 +25,16 @@ class GoogleConfig(AbstractConfig): """Config for manual setup of Google.""" - def __init__(self, config): + def __init__(self, hass, config): """Initialize the config.""" + super().__init__(hass) self._config = config + @property + def enabled(self): + """Return if Google is enabled.""" + return True + @property def agent_user_id(self): """Return Agent User Id to use for query responses.""" @@ -77,7 +83,7 @@ def should_2fa(self, state): @callback def async_register_http(hass, cfg): """Register HTTP views for Google Assistant.""" - hass.http.register_view(GoogleAssistantView(GoogleConfig(cfg))) + hass.http.register_view(GoogleAssistantView(GoogleConfig(hass, cfg))) class GoogleAssistantView(HomeAssistantView): diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index d2e016cb5d16b7..f97977a74001bf 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -3,8 +3,6 @@ "name": "Google assistant", "documentation": "https://www.home-assistant.io/integrations/google_assistant", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [] + "dependencies": ["http"], + "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py new file mode 100644 index 00000000000000..33bb16d7830033 --- /dev/null +++ b/homeassistant/components/google_assistant/report_state.py @@ -0,0 +1,39 @@ +"""Google Report State implementation.""" +from homeassistant.core import HomeAssistant, callback +from homeassistant.const import MATCH_ALL + +from .helpers import AbstractConfig, GoogleEntity + + +@callback +def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig): + """Enable state reporting.""" + + async def async_entity_state_listener(changed_entity, old_state, new_state): + if not new_state: + return + + if not google_config.should_expose(new_state): + return + + entity = GoogleEntity(hass, google_config, new_state) + + if not entity.is_supported(): + return + + entity_data = entity.query_serialize() + + if old_state: + old_entity = GoogleEntity(hass, google_config, old_state) + + # Only report to Google if data that Google cares about has changed + if entity_data == old_entity.query_serialize(): + return + + await google_config.async_report_state( + {"devices": {"states": {changed_entity: entity_data}}} + ) + + return hass.helpers.event.async_track_state_change( + MATCH_ALL, async_entity_state_listener + ) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 6ab6d937b51e84..f9b311a3880879 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -193,11 +193,12 @@ async def handle_devices_execute(hass, data, payload): @HANDLERS.register("action.devices.DISCONNECT") -async def async_devices_disconnect(hass, data, payload): +async def async_devices_disconnect(hass, data: RequestData, payload): """Handle action.devices.DISCONNECT request. https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect """ + await data.config.async_deactivate_report_state() return None diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 29484b671ed0e1..a64e0dc38e7b75 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.17 +hass-nabucasa==0.22 home-assistant-frontend==20191002.0 importlib-metadata==0.23 jinja2>=2.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index c84b57a09f36b5..fd44b46c64bb4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -607,7 +607,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.17 +hass-nabucasa==0.22 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61d3479d8f62d3..9bc9870be10c5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.17 +hass-nabucasa==0.22 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index c8e84016a28a9f..a7c8898659a432 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -59,7 +59,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock): cloud_prefs, Mock( alexa_access_token_url="http://example/alexa_token", - run_executor=Mock(side_effect=mock_coro), + auth=Mock(async_check_token=Mock(side_effect=mock_coro)), websession=hass.helpers.aiohttp_client.async_get_clientsession(), ), ) @@ -160,7 +160,11 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( EVENT_ENTITY_REGISTRY_UPDATED, - {"action": "update", "entity_id": "light.kitchen"}, + { + "action": "update", + "entity_id": "light.kitchen", + "changes": ["entity_id"], + }, ) await hass.async_block_till_done() diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py new file mode 100644 index 00000000000000..43914f489d6eff --- /dev/null +++ b/tests/components/cloud/test_google_config.py @@ -0,0 +1,121 @@ +"""Test the Cloud Google Config.""" +from unittest.mock import patch, Mock + +from homeassistant.components.google_assistant import helpers as ga_helpers +from homeassistant.components.cloud import GACTIONS_SCHEMA +from homeassistant.components.cloud.google_config import CloudGoogleConfig +from homeassistant.util.dt import utcnow +from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED + +from tests.common import mock_coro, async_fire_time_changed + + +async def test_google_update_report_state(hass, cloud_prefs): + """Test Google config responds to updating preference.""" + config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None) + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch( + "homeassistant.components.google_assistant.report_state.async_enable_report_state" + ) as mock_report_state: + await cloud_prefs.async_update(google_report_state=True) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 + assert len(mock_report_state.mock_calls) == 1 + + +async def test_sync_entities(aioclient_mock, hass, cloud_prefs): + """Test sync devices.""" + aioclient_mock.post("http://example.com", status=404) + config = CloudGoogleConfig( + hass, + GACTIONS_SCHEMA({}), + cloud_prefs, + Mock( + google_actions_sync_url="http://example.com", + auth=Mock(async_check_token=Mock(side_effect=mock_coro)), + ), + ) + + assert await config.async_sync_entities() == 404 + + +async def test_google_update_expose_trigger_sync(hass, cloud_prefs): + """Test Google config responds to updating exposed entities.""" + config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None) + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0): + await cloud_prefs.async_update_google_entity_config( + entity_id="light.kitchen", should_expose=True + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0): + await cloud_prefs.async_update_google_entity_config( + entity_id="light.kitchen", should_expose=False + ) + await cloud_prefs.async_update_google_entity_config( + entity_id="binary_sensor.door", should_expose=True + ) + await cloud_prefs.async_update_google_entity_config( + entity_id="sensor.temp", should_expose=True + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 + + +async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): + """Test Google config responds to entity registry.""" + config = CloudGoogleConfig( + hass, GACTIONS_SCHEMA({}), cloud_prefs, hass.data["cloud"] + ) + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0): + hass.bus.async_fire( + EVENT_ENTITY_REGISTRY_UPDATED, + {"action": "create", "entity_id": "light.kitchen"}, + ) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0): + hass.bus.async_fire( + EVENT_ENTITY_REGISTRY_UPDATED, + {"action": "remove", "entity_id": "light.kitchen"}, + ) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 + + with patch.object( + config, "async_sync_entities", side_effect=mock_coro + ) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0): + hass.bus.async_fire( + EVENT_ENTITY_REGISTRY_UPDATED, + { + "action": "update", + "entity_id": "light.kitchen", + "changes": ["entity_id"], + }, + ) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index d5a3395440be1a..8e03fb82b2ce62 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -33,7 +33,9 @@ @pytest.fixture() def mock_auth(): """Mock check token.""" - with patch("hass_nabucasa.auth.CognitoAuth.check_token"): + with patch( + "hass_nabucasa.auth.CognitoAuth.async_check_token", side_effect=mock_coro + ): yield @@ -357,6 +359,7 @@ async def test_websocket_status( "google_secure_devices_pin": None, "alexa_entity_configs": {}, "alexa_report_state": False, + "google_report_state": False, "remote_enabled": False, }, "alexa_entities": { diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 244c22d2486c5d..e160ea8826ab7e 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -28,6 +28,13 @@ async def test_constructor_loads_info_from_config(hass): "user_pool_id": "test-user_pool_id", "region": "test-region", "relayer": "test-relayer", + "google_actions_sync_url": "http://test-google_actions_sync_url", + "subscription_info_url": "http://test-subscription-info-url", + "cloudhook_create_url": "http://test-cloudhook_create_url", + "remote_api_url": "http://test-remote_api_url", + "alexa_access_token_url": "http://test-alexa-token-url", + "acme_directory_server": "http://test-acme-directory-server", + "google_actions_report_state_url": "http://test-google-actions-report-state-url", }, }, ) @@ -39,6 +46,16 @@ async def test_constructor_loads_info_from_config(hass): assert cl.user_pool_id == "test-user_pool_id" assert cl.region == "test-region" assert cl.relayer == "test-relayer" + assert cl.google_actions_sync_url == "http://test-google_actions_sync_url" + assert cl.subscription_info_url == "http://test-subscription-info-url" + assert cl.cloudhook_create_url == "http://test-cloudhook_create_url" + assert cl.remote_api_url == "http://test-remote_api_url" + assert cl.alexa_access_token_url == "http://test-alexa-token-url" + assert cl.acme_directory_server == "http://test-acme-directory-server" + assert ( + cl.google_actions_report_state_url + == "http://test-google-actions-report-state-url" + ) async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user): diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 12de2eaba1cdc9..8049ac4b0dbcee 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -6,9 +6,15 @@ class MockConfig(helpers.AbstractConfig): """Fake config that always exposes everything.""" def __init__( - self, *, secure_devices_pin=None, should_expose=None, entity_config=None + self, + *, + secure_devices_pin=None, + should_expose=None, + entity_config=None, + hass=None, ): """Initialize config.""" + super().__init__(hass) self._should_expose = should_expose self._secure_devices_pin = secure_devices_pin self._entity_config = entity_config or {} diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index 1c7e0201135417..9a8b9643cfe2a3 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -1,6 +1,7 @@ """The tests for google-assistant init.""" import asyncio +from homeassistant.core import Context from homeassistant.setup import async_setup_component from homeassistant.components import google_assistant as ga @@ -20,7 +21,10 @@ def test_request_sync_service(aioclient_mock, hass): assert aioclient_mock.call_count == 0 yield from hass.services.async_call( - ga.const.DOMAIN, ga.const.SERVICE_REQUEST_SYNC, blocking=True + ga.const.DOMAIN, + ga.const.SERVICE_REQUEST_SYNC, + blocking=True, + context=Context(user_id="123"), ) assert aioclient_mock.call_count == 1 diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py new file mode 100644 index 00000000000000..bd59502a3a1b58 --- /dev/null +++ b/tests/components/google_assistant/test_report_state.py @@ -0,0 +1,47 @@ +"""Test Google report state.""" +from unittest.mock import patch + +from homeassistant.components.google_assistant.report_state import ( + async_enable_report_state, +) +from . import BASIC_CONFIG + +from tests.common import mock_coro + + +async def test_report_state(hass): + """Test report state works.""" + unsub = async_enable_report_state(hass, BASIC_CONFIG) + + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report: + hass.states.async_set("light.kitchen", "on") + await hass.async_block_till_done() + + assert len(mock_report.mock_calls) == 1 + assert mock_report.mock_calls[0][1][0] == { + "devices": {"states": {"light.kitchen": {"on": True, "online": True}}} + } + + # Test that state changes that change something that Google doesn't care about + # do not trigger a state report. + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report: + hass.states.async_set( + "light.kitchen", "on", {"irrelevant": "should_be_ignored"} + ) + await hass.async_block_till_done() + + assert len(mock_report.mock_calls) == 0 + + unsub() + + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report: + hass.states.async_set("light.kitchen", "on") + await hass.async_block_till_done() + + assert len(mock_report.mock_calls) == 0 diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 6a82204a261ab9..6ecd4af446b01a 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -657,14 +657,20 @@ async def test_device_media_player(hass, device_class, google_type): async def test_query_disconnect(hass): """Test a disconnect message.""" - result = await sh.async_handle_message( - hass, - BASIC_CONFIG, - "test-agent", - {"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID}, - ) - + config = MockConfig(hass=hass) + config.async_enable_report_state() + assert config._unsub_report_state is not None + with patch.object( + config, "async_deactivate_report_state", side_effect=mock_coro + ) as mock_deactivate: + result = await sh.async_handle_message( + hass, + config, + "test-agent", + {"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID}, + ) assert result is None + assert len(mock_deactivate.mock_calls) == 1 async def test_trait_execute_adding_query_data(hass): From bd7adf9585e588e479edc88653bf0df5fcfb2d8e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 3 Oct 2019 11:15:43 +0000 Subject: [PATCH 0568/3953] Bump version 0.100.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d8a68a9707ad4..b3f99038b5947e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -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) From 69bdce768c1e4d5c36d2fed909db6971731fdd89 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 3 Oct 2019 11:19:02 +0000 Subject: [PATCH 0569/3953] Bump version 0.101.0dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d8a68a9707ad4..9baa4a1f71c160 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 100 +MINOR_VERSION = 101 PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) From 2307cac942d4f5b1cef5bdf4a11d9baa5160791c Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Oct 2019 07:26:19 -0500 Subject: [PATCH 0570/3953] Add unique_id to cert_expiry (#27140) * Add unique_id to cert_expiry * Simplify ID --- homeassistant/components/cert_expiry/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index b564cff7338584..2cd5c9abc8efc6 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -67,6 +67,11 @@ def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return a unique id for the sensor.""" + return f"{self.server_name}:{self.server_port}" + @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" From bb45bdd8dd937a9fbbb21def1da93299498a9290 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Oct 2019 10:39:14 -0500 Subject: [PATCH 0571/3953] Fix update on cert_expiry startup (#27137) * Don't force extra update on startup * Skip on entity add instead * Conditional update based on HA state * Only force entity state update when postponed * Clean up state updating * Delay YAML import --- .../components/cert_expiry/sensor.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 2cd5c9abc8efc6..2d578ef2c3b72f 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -15,6 +15,7 @@ CONF_PORT, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT @@ -35,18 +36,26 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up certificate expiry sensor.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + + @callback + def do_import(_): + """Process YAML import after HA is fully started.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + ) ) - ) + + # Delay to avoid validation during setup in case we're checking our own cert. + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_import) async def async_setup_entry(hass, entry, async_add_entities): """Add cert-expiry entry.""" async_add_entities( [SSLCertificate(entry.title, entry.data[CONF_HOST], entry.data[CONF_PORT])], - True, + False, + # Don't update in case we're checking our own cert. ) return True @@ -89,17 +98,22 @@ def icon(self): @property def available(self): - """Icon to use in the frontend, if any.""" + """Return the availability of the sensor.""" return self._available async def async_added_to_hass(self): """Once the entity is added we should update to get the initial data loaded.""" + @callback def do_update(_): """Run the update method when the start event was fired.""" - self.update() + self.async_schedule_update_ha_state(True) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) + if self.hass.is_running: + self.async_schedule_update_ha_state(True) + else: + # Delay until HA is fully started in case we're checking our own cert. + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) def update(self): """Fetch the certificate information.""" From 9902209ad2ad9b7e92731533bb6a1e68aa3d808c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 22:17:58 +0200 Subject: [PATCH 0572/3953] Add above and below to sensor trigger extra_fields (#27160) --- homeassistant/components/sensor/device_trigger.py | 6 +++++- tests/components/sensor/test_device_trigger.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 1074236eedf843..00a05f4e5907f8 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -140,6 +140,10 @@ async def async_get_trigger_capabilities(hass, trigger): """List trigger capabilities.""" return { "extra_fields": vol.Schema( - {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + { + vol.Optional(CONF_ABOVE): vol.Coerce(float), + vol.Optional(CONF_BELOW): vol.Coerce(float), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } ) } diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 1dc41f5bffa247..fcf3c474f46537 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -86,7 +86,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + {"name": "above", "optional": True, "type": "float"}, + {"name": "below", "optional": True, "type": "float"}, + {"name": "for", "optional": True, "type": "positive_time_period_dict"}, ] } triggers = await async_get_device_automations(hass, "trigger", device_entry.id) From 565302ed34822fe48270455fdf6723699f74a7c2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 3 Oct 2019 22:23:25 +0200 Subject: [PATCH 0573/3953] Improve device tracker tests (#27159) --- tests/components/unifi/test_device_tracker.py | 262 +++++++++--------- 1 file changed, 136 insertions(+), 126 deletions(-) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 3a2b37487afcdd..8e05d8a1dd1b58 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -4,12 +4,7 @@ from datetime import timedelta -from asynctest import Mock - -import pytest - -from aiounifi.clients import Clients, ClientsAll -from aiounifi.devices import Devices +from asynctest import patch from homeassistant import config_entries from homeassistant.components import unifi @@ -107,64 +102,63 @@ CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") -@pytest.fixture -def mock_controller(hass): - """Mock a UniFi Controller.""" - hass.data[UNIFI_CONFIG] = {} - hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() - controller = unifi.UniFiController(hass, None) - controller.wireless_clients = set() - - controller.api = Mock() - controller.mock_requests = [] - - controller.mock_client_responses = deque() - controller.mock_device_responses = deque() - controller.mock_client_all_responses = deque() - - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - controller.mock_requests.append(kwargs) - if path == "s/{site}/stat/sta": - return controller.mock_client_responses.popleft() - if path == "s/{site}/stat/device": - return controller.mock_device_responses.popleft() - if path == "s/{site}/rest/user": - return controller.mock_client_all_responses.popleft() - return None - - controller.api.clients = Clients({}, mock_request) - controller.api.devices = Devices({}, mock_request) - controller.api.clients_all = ClientsAll({}, mock_request) - - return 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} +async def setup_unifi_integration( + hass, config, options, clients_response, devices_response, clients_all_response +): + """Create the UniFi controller.""" + hass.data[UNIFI_CONFIG] = [] + hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) config_entry = config_entries.ConfigEntry( - 1, - unifi.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - entry_id=1, + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, options=options, + entry_id=1, ) - hass.config_entries._entries.append(config_entry) - mock_controller.config_entry = config_entry - await mock_controller.async_update() - await hass.config_entries.async_forward_entry_setup( - config_entry, device_tracker.DOMAIN - ) + sites = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "viewer"}} + mock_client_responses = deque() + mock_client_responses.append(clients_response) + + mock_device_responses = deque() + mock_device_responses.append(devices_response) + + mock_client_all_responses = deque() + mock_client_all_responses.append(clients_all_response) + + mock_requests = [] + + async def mock_request(self, method, path, json=None): + mock_requests.append({"method": method, "path": path, "json": json}) + print(mock_requests, mock_client_responses, mock_device_responses) + if path == "s/{site}/stat/sta" and mock_client_responses: + return mock_client_responses.popleft() + if path == "s/{site}/stat/device" and mock_device_responses: + return mock_device_responses.popleft() + if path == "s/{site}/rest/user" and mock_client_all_responses: + return mock_client_all_responses.popleft() + return {} + + with patch("aiounifi.Controller.login", return_value=True), patch( + "aiounifi.Controller.sites", return_value=sites + ), patch("aiounifi.Controller.request", new=mock_request): + await unifi.async_setup_entry(hass, config_entry) await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + controller_id = unifi.get_controller_id_from_config_entry(config_entry) + controller = hass.data[unifi.DOMAIN][controller_id] + + controller.mock_client_responses = mock_client_responses + controller.mock_device_responses = mock_device_responses + controller.mock_client_all_responses = mock_client_all_responses + + return controller async def test_platform_manually_configured(hass): @@ -178,24 +172,30 @@ async def test_platform_manually_configured(hass): assert unifi.DOMAIN not in hass.data -async def test_no_clients(hass, mock_controller): +async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" - mock_controller.mock_client_responses.append({}) - mock_controller.mock_device_responses.append({}) + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + clients_response={}, + devices_response={}, + clients_all_response={}, + ) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 2 -async def test_tracked_devices(hass, mock_controller): +async def test_tracked_devices(hass): """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]) - options = {CONF_SSID_FILTER: ["ssid"]} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={CONF_SSID_FILTER: ["ssid"]}, + clients_response=[CLIENT_1, CLIENT_2, CLIENT_3], + devices_response=[DEVICE_1, DEVICE_2], + clients_all_response={}, + ) assert len(hass.states.async_all()) == 5 client_1 = hass.states.get("device_tracker.client_1") @@ -217,9 +217,9 @@ async def test_tracked_devices(hass, mock_controller): client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) device_1_copy = copy(DEVICE_1) device_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_copy]) - mock_controller.mock_device_responses.append([device_1_copy]) - await mock_controller.async_update() + controller.mock_client_responses.append([client_1_copy]) + controller.mock_device_responses.append([device_1_copy]) + await controller.async_update() await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") @@ -230,19 +230,17 @@ async def test_tracked_devices(hass, mock_controller): 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() + controller.mock_client_responses.append({}) + controller.mock_device_responses.append([device_1_copy]) + await controller.async_update() await hass.async_block_till_done() device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == STATE_UNAVAILABLE - mock_controller.config_entry.add_update_listener( - mock_controller.async_options_updated - ) + controller.config_entry.add_update_listener(controller.async_options_updated) hass.config_entries.async_update_entry( - mock_controller.config_entry, + controller.config_entry, options={ CONF_SSID_FILTER: [], CONF_TRACK_WIRED_CLIENTS: False, @@ -258,18 +256,22 @@ async def test_tracked_devices(hass, mock_controller): assert device_1 is None -async def test_wireless_client_go_wired_issue(hass, mock_controller): +async def test_wireless_client_go_wired_issue(hass): """Test the solution to catch wireless device go wired UniFi issue. UniFi has a known issue that when a wireless device goes away it sometimes gets marked as wired. """ client_1_client = copy(CLIENT_1) client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_client]) - mock_controller.mock_device_responses.append({}) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + clients_response=[client_1_client], + devices_response={}, + clients_all_response={}, + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") @@ -278,9 +280,9 @@ async def test_wireless_client_go_wired_issue(hass, mock_controller): client_1_client["is_wired"] = True client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_client]) - mock_controller.mock_device_responses.append({}) - await mock_controller.async_update() + controller.mock_client_responses.append([client_1_client]) + controller.mock_device_responses.append({}) + await controller.async_update() await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") @@ -288,31 +290,27 @@ async def test_wireless_client_go_wired_issue(hass, mock_controller): client_1_client["is_wired"] = False client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_client]) - mock_controller.mock_device_responses.append({}) - await mock_controller.async_update() + controller.mock_client_responses.append([client_1_client]) + controller.mock_device_responses.append({}) + await controller.async_update() await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" -async def test_restoring_client(hass, mock_controller): +async def test_restoring_client(hass): """Test the update_items function with some clients.""" - mock_controller.mock_client_responses.append([CLIENT_2]) - mock_controller.mock_device_responses.append({}) - mock_controller.mock_client_all_responses.append([CLIENT_1]) - options = {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, + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, + options={}, + entry_id=1, ) registry = await entity_registry.async_get_registry(hass) @@ -331,22 +329,30 @@ async def test_restoring_client(hass, mock_controller): config_entry=config_entry, ) - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 3 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.CONF_BLOCK_CLIENT: True}, + clients_response=[CLIENT_2], + devices_response={}, + clients_all_response=[CLIENT_1], + ) assert len(hass.states.async_all()) == 4 device_1 = hass.states.get("device_tracker.client_1") assert device_1 is not None -async def test_dont_track_clients(hass, mock_controller): +async def test_dont_track_clients(hass): """Test dont track clients config works.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([DEVICE_1]) - options = {unifi.controller.CONF_TRACK_CLIENTS: False} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.controller.CONF_TRACK_CLIENTS: False}, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response={}, + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") @@ -357,14 +363,16 @@ async def test_dont_track_clients(hass, mock_controller): assert device_1.state == "not_home" -async def test_dont_track_devices(hass, mock_controller): +async def test_dont_track_devices(hass): """Test dont track devices config works.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([DEVICE_1]) - options = {unifi.controller.CONF_TRACK_DEVICES: False} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.controller.CONF_TRACK_DEVICES: False}, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response={}, + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") @@ -375,14 +383,16 @@ async def test_dont_track_devices(hass, mock_controller): assert device_1 is None -async def test_dont_track_wired_clients(hass, mock_controller): +async def test_dont_track_wired_clients(hass): """Test dont track wired clients config works.""" - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) - mock_controller.mock_device_responses.append({}) - options = {unifi.controller.CONF_TRACK_WIRED_CLIENTS: False} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False}, + clients_response=[CLIENT_1, CLIENT_2], + devices_response={}, + clients_all_response={}, + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") From af81878d08f55be6a14ebc2f746f53a3b6ad0513 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Thu, 3 Oct 2019 16:28:02 -0400 Subject: [PATCH 0574/3953] Add PowerLevelController for fan to alexa (#27158) * Implement AlexaPowerLevelController * Implement AlexaPowerLevelController Tests --- .../components/alexa/capabilities.py | 35 ++++++++++ homeassistant/components/alexa/const.py | 7 +- homeassistant/components/alexa/entities.py | 2 + homeassistant/components/alexa/handlers.py | 70 +++++++++++++++++++ tests/components/alexa/test_smart_home.py | 22 ++++++ 5 files changed, 135 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index b8bd3841a78c4c..fca63adab0e3e5 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -614,3 +614,38 @@ def get_property(self, name): return None return {"value": temp, "scale": API_TEMP_UNITS[unit]} + + +class AlexaPowerLevelController(AlexaCapibility): + """Implements Alexa.PowerLevelController. + + https://developer.amazon.com/docs/device-apis/alexa-powerlevelcontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.PowerLevelController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "powerLevel"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "powerLevel": + raise UnsupportedProperty(name) + + if self.entity.domain == fan.DOMAIN: + speed = self.entity.attributes.get(fan.ATTR_SPEED) + + return PERCENTAGE_FAN_MAP.get(speed, None) + + return None diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 83c7da41c16d5a..cd0cb85a0a5e1b 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -62,7 +62,12 @@ ) API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} -PERCENTAGE_FAN_MAP = {fan.SPEED_LOW: 33, fan.SPEED_MEDIUM: 66, fan.SPEED_HIGH: 100} +PERCENTAGE_FAN_MAP = { + fan.SPEED_OFF: 0, + fan.SPEED_LOW: 33, + fan.SPEED_MEDIUM: 66, + fan.SPEED_HIGH: 100, +} class Cause: diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 55b5878f6672a3..63231f714477c0 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -43,6 +43,7 @@ AlexaPercentageController, AlexaPlaybackController, AlexaPowerController, + AlexaPowerLevelController, AlexaSceneController, AlexaSpeaker, AlexaStepSpeaker, @@ -344,6 +345,7 @@ def interfaces(self): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & fan.SUPPORT_SET_SPEED: yield AlexaPercentageController(self.entity) + yield AlexaPowerLevelController(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index c72101460c4819..3cb61675f92ecd 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -779,3 +779,73 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): async def async_api_reportstate(hass, config, directive, context): """Process a ReportState request.""" return directive.response(name="StateReport") + + +@HANDLERS.register(("Alexa.PowerLevelController", "SetPowerLevel")) +async def async_api_set_power_level(hass, config, directive, context): + """Process a SetPowerLevel request.""" + entity = directive.entity + percentage = int(directive.payload["powerLevel"]) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_SPEED + speed = "off" + + if percentage <= 33: + speed = "low" + elif percentage <= 66: + speed = "medium" + else: + speed = "high" + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.PowerLevelController", "AdjustPowerLevel")) +async def async_api_adjust_power_level(hass, config, directive, context): + """Process an AdjustPowerLevel request.""" + entity = directive.entity + percentage_delta = int(directive.payload["powerLevelDelta"]) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + current = 0 + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_SPEED + speed = entity.attributes.get(fan.ATTR_SPEED) + + if speed == "off": + current = 0 + elif speed == "low": + current = 33 + elif speed == "medium": + current = 66 + else: + current = 100 + + # set percentage + percentage = max(0, percentage_delta + current) + speed = "off" + + if percentage <= 33: + speed = "low" + elif percentage <= 66: + speed = "medium" + else: + speed = "high" + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index e5e5b8ab7aecf0..78ce2963eaf120 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -340,6 +340,7 @@ async def test_variable_fan(hass): appliance, "Alexa.PercentageController", "Alexa.PowerController", + "Alexa.PowerLevelController", "Alexa.EndpointHealth", ) @@ -364,6 +365,27 @@ async def test_variable_fan(hass): "speed", ) + call, _ = await assert_request_calls_service( + "Alexa.PowerLevelController", + "SetPowerLevel", + "fan#test_2", + "fan.set_speed", + hass, + payload={"powerLevel": "50"}, + ) + assert call.data["speed"] == "medium" + + await assert_percentage_changes( + hass, + [("high", "-5"), ("high", "5"), ("low", "-80")], + "Alexa.PowerLevelController", + "AdjustPowerLevel", + "fan#test_2", + "powerLevelDelta", + "fan.set_speed", + "speed", + ) + async def test_lock(hass): """Test lock discovery.""" From 2f251104e3f01aaad8d972966b5c717ecf0449dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 3 Oct 2019 23:28:12 +0300 Subject: [PATCH 0575/3953] update broadlink library (#27157) --- homeassistant/components/broadlink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index d77c32966b19e0..c2c128909cc6ba 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -3,7 +3,7 @@ "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", "requirements": [ - "broadlink==0.11.1" + "broadlink==0.12.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index fd44b46c64bb4a..dcb114c1896072 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -308,7 +308,7 @@ boto3==1.9.233 braviarc-homeassistant==0.3.7.dev0 # homeassistant.components.broadlink -broadlink==0.11.1 +broadlink==0.12.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 From 4733fea4163ee6e71e9824ad5ffc184caadcfed1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Oct 2019 22:28:53 +0200 Subject: [PATCH 0576/3953] Adds fields to light.toggle service description (#27155) --- homeassistant/components/light/services.yaml | 84 +++++++++++++++----- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index ef944d75efc9d3..97186f56a8f51d 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -5,22 +5,22 @@ turn_on: fields: entity_id: description: Name(s) of entities to turn on - example: 'light.kitchen' + example: "light.kitchen" transition: description: Duration in seconds it takes to get to next state example: 60 rgb_color: description: Color for the light in RGB-format. - example: '[255, 100, 100]' + example: "[255, 100, 100]" color_name: description: A human readable color name. - example: 'red' + example: "red" hs_color: description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. - example: '[300, 70]' + example: "[300, 70]" xy_color: description: Color for the light in XY-format. - example: '[0.52, 0.43]' + example: "[0.52, 0.43]" color_temp: description: Color temperature for the light in mireds. example: 250 @@ -29,7 +29,7 @@ turn_on: example: 4000 white_value: description: Number between 0..255 indicating level of white. - example: '250' + example: "250" brightness: description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. example: 120 @@ -55,7 +55,7 @@ turn_off: fields: entity_id: description: Name(s) of entities to turn off. - example: 'light.kitchen' + example: "light.kitchen" transition: description: Duration in seconds it takes to get to next state. example: 60 @@ -68,23 +68,67 @@ turn_off: toggle: description: Toggles a light. fields: - '...': - description: All turn_on parameters can be used. + entity_id: + description: Name(s) of entities to turn on + example: "light.kitchen" + transition: + description: Duration in seconds it takes to get to next state + example: 60 + rgb_color: + description: Color for the light in RGB-format. + example: "[255, 100, 100]" + color_name: + description: A human readable color name. + example: "red" + hs_color: + description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. + example: "[300, 70]" + xy_color: + description: Color for the light in XY-format. + example: "[0.52, 0.43]" + color_temp: + description: Color temperature for the light in mireds. + example: 250 + kelvin: + description: Color temperature for the light in Kelvin. + example: 4000 + white_value: + description: Number between 0..255 indicating level of white. + example: "250" + brightness: + description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. + example: 120 + brightness_pct: + description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. + example: 47 + profile: + description: Name of a light profile to use. + example: relax + flash: + description: If the light should flash. + values: + - short + - long + effect: + description: Light effect. + values: + - colorloop + - random lifx_set_state: description: Set a color/brightness and possibliy turn the light on/off. fields: entity_id: description: Name(s) of entities to set a state on. - example: 'light.garage' - '...': + example: "light.garage" + "...": description: All turn_on parameters can be used to specify a color. infrared: description: Automatic infrared level (0..255) when light brightness is low. example: 255 zones: description: List of zone numbers to affect (8 per LIFX Z, starts at 0). - example: '[0,5]' + example: "[0,5]" transition: description: Duration in seconds it takes to get to the final state. example: 10 @@ -97,19 +141,19 @@ lifx_effect_pulse: fields: entity_id: description: Name(s) of entities to run the effect on. - example: 'light.kitchen' + example: "light.kitchen" mode: - description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid.' + description: "Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid." example: strobe brightness: description: Number between 0..255 indicating brightness of the temporary color. example: 120 color_name: description: A human readable color name. - example: 'red' + example: "red" rgb_color: description: The temporary color in RGB-format. - example: '[255, 100, 100]' + example: "[255, 100, 100]" period: description: Duration of the effect in seconds (default 1.0). example: 3 @@ -125,7 +169,7 @@ lifx_effect_colorloop: fields: entity_id: description: Name(s) of entities to run the effect on. - example: 'light.disco1, light.disco2, light.disco3' + example: "light.disco1, light.disco2, light.disco3" brightness: description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. example: 120 @@ -147,14 +191,14 @@ lifx_effect_stop: fields: entity_id: description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. - example: 'light.bedroom' + example: "light.bedroom" xiaomi_miio_set_scene: description: Set a fixed scene. fields: entity_id: description: Name of the light entity. - example: 'light.xiaomi_miio' + example: "light.xiaomi_miio" scene: description: Number of the fixed scene, between 1 and 4. example: 1 @@ -164,7 +208,7 @@ xiaomi_miio_set_delayed_turn_off: fields: entity_id: description: Name of the light entity. - example: 'light.xiaomi_miio' + example: "light.xiaomi_miio" time_period: description: Time period for the delayed turn off. example: "5, '0:05', {'minutes': 5}" From cda7692f2449590b6bbb275fb094cef7b76f7095 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 22:29:57 +0200 Subject: [PATCH 0577/3953] Add support for `for` to binary_sensor, light and switch device conditions (#27153) * Add support for `for` to binary_sensor, light and switch device conditions * Fix typing * Fixup * Fixup --- .../binary_sensor/device_condition.py | 14 ++- .../binary_sensor/device_trigger.py | 2 +- .../components/device_automation/__init__.py | 19 ++++ .../device_automation/toggle_entity.py | 14 ++- .../components/light/device_condition.py | 5 + .../components/light/device_trigger.py | 4 +- .../components/sensor/device_trigger.py | 2 +- .../components/switch/device_condition.py | 5 + .../components/switch/device_trigger.py | 4 +- .../binary_sensor/test_device_condition.py | 97 ++++++++++++++++- .../components/device_automation/test_init.py | 97 +++++++++++++++++ .../components/light/test_device_condition.py | 96 +++++++++++++++++ .../switch/test_device_condition.py | 100 +++++++++++++++++- 13 files changed, 447 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 1749ea91c5b552..d686ef412c1ed3 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -4,7 +4,7 @@ from homeassistant.core import HomeAssistant from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON -from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import ( async_entries_for_device, @@ -188,6 +188,7 @@ { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) @@ -244,5 +245,16 @@ def async_condition_from_config( condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], condition.CONF_STATE: stat, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] return condition.state_from_config(state_config, config_validation) + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 89fd9add69a940..e05713b5c67d09 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -240,7 +240,7 @@ async def async_get_triggers(hass, device_id): return triggers -async def async_get_trigger_capabilities(hass, trigger): +async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index fa6deac40ba6ac..9d0a5a72a47188 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -59,6 +59,9 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( websocket_device_automation_list_triggers ) + hass.components.websocket_api.async_register_command( + websocket_device_automation_get_condition_capabilities + ) hass.components.websocket_api.async_register_command( websocket_device_automation_get_trigger_capabilities ) @@ -206,6 +209,22 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): connection.send_result(msg["id"], triggers) +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/condition/capabilities", + vol.Required("condition"): dict, + } +) +async def websocket_device_automation_get_condition_capabilities(hass, connection, msg): + """Handle request for device condition capabilities.""" + condition = msg["condition"] + capabilities = await _async_get_device_automation_capabilities( + hass, "condition", condition + ) + connection.send_result(msg["id"], capabilities) + + @websocket_api.async_response @websocket_api.websocket_command( { diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index c9588c1efa7385..47953dc5e81d44 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -80,6 +80,7 @@ { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) @@ -132,6 +133,8 @@ def async_condition_from_config( condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], condition.CONF_STATE: stat, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] return condition.state_from_config(state_config, config_validation) @@ -213,7 +216,16 @@ async def async_get_triggers( return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) -async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } + + +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: """List trigger capabilities.""" return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index 4abf34e6661bac..86f5761ddf565a 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -27,3 +27,8 @@ def async_condition_from_config( async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return await toggle_entity.async_get_condition_capabilities(hass, config) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 5bd5d83e1c02ee..432d24d3c14d90 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -32,6 +32,6 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) -async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: """List trigger capabilities.""" - return await toggle_entity.async_get_trigger_capabilities(hass, trigger) + return await toggle_entity.async_get_trigger_capabilities(hass, config) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 00a05f4e5907f8..5ec6a57ffb4a45 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -136,7 +136,7 @@ async def async_get_triggers(hass, device_id): return triggers -async def async_get_trigger_capabilities(hass, trigger): +async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 5825a3ba91ad4f..f3d5903bcf31f5 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -27,3 +27,8 @@ def async_condition_from_config( async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return await toggle_entity.async_get_condition_capabilities(hass, config) diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 22a016e49b9bc2..7f0458b3e9f28d 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -32,6 +32,6 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) -async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: """List trigger capabilities.""" - return await toggle_entity.async_get_trigger_capabilities(hass, trigger) + return await toggle_entity.async_get_trigger_capabilities(hass, config) diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index b5502d8fe3da39..34cf4030a50192 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -1,5 +1,7 @@ """The test for binary_sensor device automation.""" +from datetime import timedelta import pytest +from unittest.mock import patch from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS @@ -7,6 +9,7 @@ from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, @@ -14,6 +17,7 @@ mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -71,6 +75,28 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a binary_sensor condition.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -131,7 +157,6 @@ async def test_if_state(hass, calls): assert len(calls) == 0 hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["some"] == "is_on event - test_event1" @@ -142,3 +167,73 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_if_fires_on_for_condition(hass, calls): + """Test for firing if condition is on with delay.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=10) + point3 = point2 + timedelta(seconds=10) + + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = point1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_not_bat_low", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ("platform", "event.event_type") + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 10 secs into the future + mock_utcnow.return_value = point2 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 20 secs into the future + mock_utcnow.return_value = point3 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index fa78ae94416361..1af4b541a92958 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -170,6 +170,103 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert _same_lists(triggers, expected_triggers) +async def test_websocket_get_condition_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get the expected condition capabilities for a light through websocket.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/list", + "device_id": device_entry.id, + } + ) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + conditions = msg["result"] + + id = 2 + for condition in conditions: + await client.send_json( + { + "id": id, + "type": "device_automation/condition/capabilities", + "condition": condition, + } + ) + msg = await client.receive_json() + assert msg["id"] == id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + id = id + 1 + + +async def test_websocket_get_bad_condition_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no condition capabilities for a non existing domain.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/capabilities", + "condition": {"domain": "beer"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + +async def test_websocket_get_no_condition_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no condition capabilities for a domain with no device condition capabilities.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/capabilities", + "condition": {"domain": "deconz"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + async def test_websocket_get_trigger_capabilities( hass, hass_ws_client, device_reg, entity_reg ): diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 8009fbd633798a..a9f4adddfabfe1 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -1,11 +1,14 @@ """The test for light device automation.""" +from datetime import timedelta import pytest +from unittest.mock import patch from homeassistant.components.light import DOMAIN from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, @@ -13,6 +16,7 @@ mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -63,6 +67,28 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a light condition.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -134,3 +160,73 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_if_fires_on_for_condition(hass, calls): + """Test for firing if condition is on with delay.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=10) + point3 = point2 + timedelta(seconds=10) + + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = point1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ("platform", "event.event_type") + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 10 secs into the future + mock_utcnow.return_value = point2 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 20 secs into the future + mock_utcnow.return_value = point3 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index e2ce5a373d227f..e673527fadaedc 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -1,20 +1,22 @@ """The test for switch device automation.""" +from datetime import timedelta import pytest +from unittest.mock import patch from homeassistant.components.switch import DOMAIN from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, async_mock_service, mock_device_registry, mock_registry, + async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -65,6 +67,28 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a switch condition.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -136,3 +160,73 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_if_fires_on_for_condition(hass, calls): + """Test for firing if condition is on with delay.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=10) + point3 = point2 + timedelta(seconds=10) + + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = point1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ("platform", "event.event_type") + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 10 secs into the future + mock_utcnow.return_value = point2 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 20 secs into the future + mock_utcnow.return_value = point3 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_off event - test_event1" From 89ebc17fb139c4ded65fc717a094d8a9038f052c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 22:30:59 +0200 Subject: [PATCH 0578/3953] Only generate device trigger for sensor with unit (#27152) --- .../components/sensor/device_trigger.py | 11 ++++++++-- .../components/sensor/test_device_trigger.py | 4 +++- .../custom_components/test/sensor.py | 22 ++++++++++++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 5ec6a57ffb4a45..377b202db187c4 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -5,6 +5,7 @@ from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, CONF_ABOVE, CONF_BELOW, CONF_ENTITY_ID, @@ -113,8 +114,14 @@ async def async_get_triggers(hass, device_id): for entry in entries: device_class = DEVICE_CLASS_NONE state = hass.states.get(entry.entity_id) - if state: - device_class = state.attributes.get(ATTR_DEVICE_CLASS) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + continue + + device_class = state.attributes.get(ATTR_DEVICE_CLASS) templates = ENTITY_TRIGGERS.get( device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index fcf3c474f46537..45452dc84a0dca 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -2,7 +2,7 @@ from datetime import timedelta import pytest -from homeassistant.components.sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM from homeassistant.setup import async_setup_component @@ -19,6 +19,7 @@ async_get_device_automations, async_get_device_automation_capabilities, ) +from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES @pytest.fixture @@ -70,6 +71,7 @@ async def test_get_triggers(hass, device_reg, entity_reg): } for device_class in DEVICE_CLASSES for trigger in ENTITY_TRIGGERS[device_class] + if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert triggers == expected_triggers diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index c25be28fdb03ee..651ee17bd65d46 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -3,10 +3,24 @@ Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.sensor import DEVICE_CLASSES +import homeassistant.components.sensor as sensor from tests.common import MockEntity +DEVICE_CLASSES = list(sensor.DEVICE_CLASSES) +DEVICE_CLASSES.append("none") + +UNITS_OF_MEASUREMENT = { + sensor.DEVICE_CLASS_BATTERY: "%", # % of battery that is left + sensor.DEVICE_CLASS_HUMIDITY: "%", # % of humidity in the air + sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm) + sensor.DEVICE_CLASS_SIGNAL_STRENGTH: "dB", # signal strength (dB/dBm) + sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F) + sensor.DEVICE_CLASS_TIMESTAMP: "hh:mm:ss", # timestamp (ISO8601) + sensor.DEVICE_CLASS_PRESSURE: "hPa", # pressure (hPa/mbar) + sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW) +} + ENTITIES = {} @@ -22,6 +36,7 @@ def init(empty=False): name=f"{device_class} sensor", unique_id=f"unique_{device_class}", device_class=device_class, + unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), ) for device_class in DEVICE_CLASSES } @@ -42,3 +57,8 @@ class MockSensor(MockEntity): def device_class(self): """Return the class of this sensor.""" return self._handle("device_class") + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of this sensor.""" + return self._handle("unit_of_measurement") From adab228012e445750abe6a7ca1a6e3c834832fcb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Oct 2019 18:50:15 -0500 Subject: [PATCH 0579/3953] Unload cert_expiry config entries (#27150) * Allow cert_expiry unloading * Update codeowners --- CODEOWNERS | 2 +- homeassistant/components/cert_expiry/__init__.py | 8 ++++++++ homeassistant/components/cert_expiry/manifest.json | 5 ++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 4e7b0a0cd2ab83..418eb745ecb9b3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -49,7 +49,7 @@ homeassistant/components/broadlink/* @danielhiversen homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties -homeassistant/components/cert_expiry/* @cereal2nd +homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren homeassistant/components/cisco_ios/* @fbradyirl homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 7c7efea7333120..f5078219809397 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -15,3 +15,11 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, "sensor") ) return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_unload(entry, "sensor") + ) + return True diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 97f72f2ad1194a..48816809bbdce2 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -5,5 +5,8 @@ "requirements": [], "config_flow": true, "dependencies": [], - "codeowners": ["@cereal2nd"] + "codeowners": [ + "@Cereal2nd", + "@jjlawren" + ] } From b63b207519f88230d325fe0e2c711f00848eb13d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 4 Oct 2019 01:10:26 +0100 Subject: [PATCH 0580/3953] Handle all single zone thermostats (#27168) --- homeassistant/components/evohome/climate.py | 17 ++++++++--------- .../components/evohome/water_heater.py | 4 +++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index e5c8c6af14bde1..7df2db1b17e512 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -75,21 +75,20 @@ async def async_setup_platform( loc_idx = broker.params[CONF_LOCATION_IDX] _LOGGER.debug( - "Found Location/Controller, id=%s [%s], name=%s (location_idx=%s)", - broker.tcs.systemId, + "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)", broker.tcs.modelType, + broker.tcs.systemId, broker.tcs.location.name, loc_idx, ) - # special case of RoundThermostat (is single zone) - if broker.config["zones"][0]["modelType"] == "RoundModulation": + # special case of RoundModulation/RoundWireless (is a single zone system) + if broker.config["zones"][0]["zoneType"] == "Thermostat": zone = list(broker.tcs.zones.values())[0] _LOGGER.debug( - "Found %s, id=%s [%s], name=%s", - zone.zoneType, - zone.zoneId, + "Found the Thermostat (%s), id=%s, name=%s", zone.modelType, + zone.zoneId, zone.name, ) @@ -101,10 +100,10 @@ async def async_setup_platform( zones = [] for zone in broker.tcs.zones.values(): _LOGGER.debug( - "Found %s, id=%s [%s], name=%s", + "Found a %s (%s), id=%s, name=%s", zone.zoneType, - zone.zoneId, zone.modelType, + zone.zoneId, zone.name, ) zones.append(EvoZone(broker, zone)) diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index b65665eb2c9ad2..37bdcd82afcac7 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -34,7 +34,9 @@ async def async_setup_platform( broker = hass.data[DOMAIN]["broker"] _LOGGER.debug( - "Found %s, id: %s", broker.tcs.hotwater.zone_type, broker.tcs.hotwater.zoneId + "Found the DHW Controller (%s), id: %s", + broker.tcs.hotwater.zone_type, + broker.tcs.hotwater.zoneId, ) evo_dhw = EvoDHW(broker, broker.tcs.hotwater) From f2c5c249d29e54121fa3e8e79d4ccceb712c7933 Mon Sep 17 00:00:00 2001 From: Dan Cinnamon Date: Thu, 3 Oct 2019 19:15:52 -0500 Subject: [PATCH 0581/3953] Envisalink startup reconnect (#27063) * Added retry capability to the component initialization. * Removed extra chars * Black formatting. * Removed issue with block upon setup. Now setup will only fail if auth failed to the device. --- homeassistant/components/envisalink/__init__.py | 7 +++++-- homeassistant/components/envisalink/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 76d6a7e369c1d7..6cdedf897446d2 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -141,9 +141,12 @@ def login_fail_callback(data): @callback def connection_fail_callback(data): """Network failure callback.""" - _LOGGER.error("Could not establish a connection with the Envisalink") + _LOGGER.error( + "Could not establish a connection with the Envisalink- retrying..." + ) if not sync_connect.done(): - sync_connect.set_result(False) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink) + sync_connect.set_result(True) @callback def connection_success_callback(data): diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 6c5405c75eaae9..3cee270f099a80 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -3,7 +3,7 @@ "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", "requirements": [ - "pyenvisalink==3.8" + "pyenvisalink==4.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index dcb114c1896072..208eeac899b837 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1168,7 +1168,7 @@ pyeight==0.1.1 pyemby==1.6 # homeassistant.components.envisalink -pyenvisalink==3.8 +pyenvisalink==4.0 # homeassistant.components.ephember pyephember==0.2.0 From 85947591c5ffc0fa04b9ffd734f67ed9d5153237 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 4 Oct 2019 00:32:16 +0000 Subject: [PATCH 0582/3953] [ci skip] Translation update --- .../components/deconz/.translations/it.json | 1 + .../components/deconz/.translations/no.json | 1 + .../deconz/.translations/zh-Hant.json | 1 + .../components/plex/.translations/it.json | 3 ++- .../plex/.translations/zh-Hant.json | 3 ++- .../components/sensor/.translations/ca.json | 24 +++++++++++++++++ .../components/sensor/.translations/da.json | 26 +++++++++++++++++++ .../components/sensor/.translations/en.json | 26 +++++++++++++++++++ .../components/sensor/.translations/it.json | 26 +++++++++++++++++++ .../components/sensor/.translations/no.json | 26 +++++++++++++++++++ .../sensor/.translations/zh-Hant.json | 26 +++++++++++++++++++ .../soma/.translations/zh-Hant.json | 13 ++++++++++ .../components/zha/.translations/da.json | 3 +++ 13 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/sensor/.translations/ca.json create mode 100644 homeassistant/components/sensor/.translations/da.json create mode 100644 homeassistant/components/sensor/.translations/en.json create mode 100644 homeassistant/components/sensor/.translations/it.json create mode 100644 homeassistant/components/sensor/.translations/no.json create mode 100644 homeassistant/components/sensor/.translations/zh-Hant.json create mode 100644 homeassistant/components/soma/.translations/zh-Hant.json diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 7a2b8832864e2b..1f0b344a32d6ba 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", "remote_button_rotated": "Pulsante ruotato \"{subtype}\"", + "remote_button_rotation_stopped": "La rotazione dei pulsanti \"{subtype}\" si \u00e8 arrestata", "remote_button_short_press": "Pulsante \"{subtype}\" premuto", "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index c7079fd62193e7..f779f0918fe5fd 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", "remote_button_quintuple_press": "\" {subtype} \" - knappen femdobbelt klikket", "remote_button_rotated": "Knappen roterte \" {subtype} \"", + "remote_button_rotation_stopped": "Knappe rotasjon \"{under type}\" stoppet", "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\" {subtype} \"-knappen trippel klikket", diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index bd47a637761ccd..2ad613cde6868f 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u9ede\u64ca", "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u9ede\u64ca", "remote_button_rotated": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215", + "remote_button_rotation_stopped": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215\u5df2\u505c\u6b62", "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 3c28f1d25f9c6b..99a6d13e0d4987 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -5,6 +5,7 @@ "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", "already_in_progress": "Plex \u00e8 in fase di configurazione", "invalid_import": "La configurazione importata non \u00e8 valida", + "token_request_timeout": "Timeout per l'ottenimento del token", "unknown": "Non riuscito per motivo sconosciuto" }, "error": { @@ -36,7 +37,7 @@ "manual_setup": "Configurazione manuale", "token": "Token Plex" }, - "description": "Immettere un token Plex per la configurazione automatica.", + "description": "Continuare ad autorizzare plex.tv o configurare manualmente un server.", "title": "Collegare il server Plex" } }, diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index 5f6d0c41c13506..a0a033651a57bb 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -5,6 +5,7 @@ "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", + "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" }, "error": { @@ -36,7 +37,7 @@ "manual_setup": "\u624b\u52d5\u8a2d\u5b9a", "token": "Plex \u5bc6\u9470" }, - "description": "\u8acb\u8f38\u5165 Plex \u5bc6\u9470\u4ee5\u9032\u884c\u81ea\u52d5\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", + "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" } }, diff --git a/homeassistant/components/sensor/.translations/ca.json b/homeassistant/components/sensor/.translations/ca.json new file mode 100644 index 00000000000000..59db5a62f86062 --- /dev/null +++ b/homeassistant/components/sensor/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Nivell de bateria de {entity_name}", + "is_humidity": "Humitat de {entity_name}", + "is_illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "is_pressure": "Pressi\u00f3 de {entity_name}", + "is_signal_strength": "For\u00e7a del senyal de {entity_name}", + "is_temperature": "Temperatura de {entity_name}", + "is_timestamp": "Marca de temps de {entity_name}", + "is_value": "Valor de {entity_name}" + }, + "trigger_type": { + "battery_level": "Nivell de bateria de {entity_name}", + "humidity": "Humitat de {entity_name}", + "illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "pressure": "Pressi\u00f3 de {entity_name}", + "signal_strength": "For\u00e7a del senyal de {entity_name}", + "temperature": "Temperatura de {entity_name}", + "timestamp": "Marca de temps de {entity_name}", + "value": "Valor de {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/da.json b/homeassistant/components/sensor/.translations/da.json new file mode 100644 index 00000000000000..df9b9935dc149c --- /dev/null +++ b/homeassistant/components/sensor/.translations/da.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} batteriniveau", + "is_humidity": "{entity_name} fugtighed", + "is_illuminance": "{entity_name} belysningsstyrke", + "is_power": "{entity_name} str\u00f8m", + "is_pressure": "{entity_name} tryk", + "is_signal_strength": "{entity_name} signalstyrke", + "is_temperature": "{entity_name} temperatur", + "is_timestamp": "{entity_name} tidsstempel", + "is_value": "{entity_name} v\u00e6rdi" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniveau", + "humidity": "{entity_name} fugtighed", + "illuminance": "{entity_name} belysningsstyrke", + "power": "{entity_name} str\u00f8m", + "pressure": "{entity_name} tryk", + "signal_strength": "{entity_name} signalstyrke", + "temperature": "{entity_name} temperatur", + "timestamp": "{entity_name} tidsstempel", + "value": "{entity_name} v\u00e6rdi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/en.json b/homeassistant/components/sensor/.translations/en.json new file mode 100644 index 00000000000000..7bbbe660feb94c --- /dev/null +++ b/homeassistant/components/sensor/.translations/en.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} battery level", + "is_humidity": "{entity_name} humidity", + "is_illuminance": "{entity_name} illuminance", + "is_power": "{entity_name} power", + "is_pressure": "{entity_name} pressure", + "is_signal_strength": "{entity_name} signal strength", + "is_temperature": "{entity_name} temperature", + "is_timestamp": "{entity_name} timestamp", + "is_value": "{entity_name} value" + }, + "trigger_type": { + "battery_level": "{entity_name} battery level", + "humidity": "{entity_name} humidity", + "illuminance": "{entity_name} illuminance", + "power": "{entity_name} power", + "pressure": "{entity_name} pressure", + "signal_strength": "{entity_name} signal strength", + "temperature": "{entity_name} temperature", + "timestamp": "{entity_name} timestamp", + "value": "{entity_name} value" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/it.json b/homeassistant/components/sensor/.translations/it.json new file mode 100644 index 00000000000000..07b20245c1634d --- /dev/null +++ b/homeassistant/components/sensor/.translations/it.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Livello della batteria di {entity_name}", + "is_humidity": "Umidit\u00e0 di {entity_name}", + "is_illuminance": "Illuminazione di {entity_name}", + "is_power": "Potenza di {entity_name}", + "is_pressure": "Pressione di {entity_name}", + "is_signal_strength": "Potenza del segnale di {entity_name}", + "is_temperature": "Temperatura di {entity_name}", + "is_timestamp": "Data di {entity_name}", + "is_value": "Valore di {entity_name}" + }, + "trigger_type": { + "battery_level": "Livello della batteria di {entity_name}", + "humidity": "Umidit\u00e0 di {entity_name}", + "illuminance": "Illuminazione di {entity_name}", + "power": "Potenza di {entity_name}", + "pressure": "Pressione di {entity_name}", + "signal_strength": "Potenza del segnale di {entity_name}", + "temperature": "Temperatura di {entity_name}", + "timestamp": "Data di {entity_name}", + "value": "Valore di {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json new file mode 100644 index 00000000000000..5f5eeaacd11e75 --- /dev/null +++ b/homeassistant/components/sensor/.translations/no.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} batteriniv\u00e5", + "is_humidity": "{entity_name} fuktighet", + "is_illuminance": "{entity_name} belysningsstyrke", + "is_power": "{entity_name} str\u00f8m", + "is_pressure": "{entity_name} trykk", + "is_signal_strength": "{entity_name} signalstyrke", + "is_temperature": "{entity_name} temperatur", + "is_timestamp": "{entity_name} tidsstempel", + "is_value": "{entity_name} verdi" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniv\u00e5", + "humidity": "{entity_name} fuktighet", + "illuminance": "{entity_name} belysningsstyrke", + "power": "{entity_name} str\u00f8m", + "pressure": "{entity_name} trykk", + "signal_strength": "{entity_name} signalstyrke", + "temperature": "{entity_name} temperatur", + "timestamp": "{entity_name} tidsstempel", + "value": "{entity_name} verdi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/zh-Hant.json b/homeassistant/components/sensor/.translations/zh-Hant.json new file mode 100644 index 00000000000000..af97681ee764d1 --- /dev/null +++ b/homeassistant/components/sensor/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} \u96fb\u91cf", + "is_humidity": "{entity_name} \u6fd5\u5ea6", + "is_illuminance": "{entity_name} \u7167\u5ea6", + "is_power": "{entity_name} \u96fb\u529b", + "is_pressure": "{entity_name} \u58d3\u529b", + "is_signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", + "is_temperature": "{entity_name} \u6eab\u5ea6", + "is_timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", + "is_value": "{entity_name} \u503c" + }, + "trigger_type": { + "battery_level": "{entity_name} \u96fb\u91cf", + "humidity": "{entity_name} \u6fd5\u5ea6", + "illuminance": "{entity_name} \u7167\u5ea6", + "power": "{entity_name} \u96fb\u529b", + "pressure": "{entity_name} \u58d3\u529b", + "signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", + "temperature": "{entity_name} \u6eab\u5ea6", + "timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", + "value": "{entity_name} \u503c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json new file mode 100644 index 00000000000000..3d28389ff9147c --- /dev/null +++ b/homeassistant/components/soma/.translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Soma \u5e33\u865f\u3002", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", + "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 0b800ecd80a7ef..39f254ac9af075 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -29,7 +29,10 @@ "button_3": "Tredje knap", "button_4": "Fjerde knap", "button_5": "Femte knap", + "button_6": "Sjette knap", "close": "Luk", + "dim_down": "D\u00e6mp ned", + "dim_up": "D\u00e6mp op", "left": "Venstre", "open": "\u00c5ben", "right": "H\u00f8jre" From c6b08b28b2e07d02d3c6d1e3279dad36295c8a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Gonz=C3=A1lez=20Calleja?= Date: Fri, 4 Oct 2019 02:44:07 +0200 Subject: [PATCH 0583/3953] Fix homekit temperaturesensor round (#27047) * Fix homekit temperature sensor for round with one decimal * Removing unnecesary operations * Adapting tests for new temperature_to_homekit() result precision --- homeassistant/components/homekit/type_sensors.py | 2 +- homeassistant/components/homekit/util.py | 2 +- tests/components/homekit/test_type_thermostats.py | 6 +++--- tests/components/homekit/test_util.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 87c8d5247a5d21..a1450518e0cf8d 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -96,7 +96,7 @@ def update_state(self, new_state): temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature) _LOGGER.debug( - "%s: Current temperature set to %d°C", self.entity_id, temperature + "%s: Current temperature set to %.1f°C", self.entity_id, temperature ) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index d60c94d420deb9..608c9a974e57d9 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -235,7 +235,7 @@ def convert_to_float(state): def temperature_to_homekit(temperature, unit): """Convert temperature to Celsius for HomeKit.""" - return round(temp_util.convert(temperature, unit, TEMP_CELSIUS) * 2) / 2 + return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) def temperature_to_states(temperature, unit): diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index d967d325561a2e..8ad46e489d6c20 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -96,10 +96,10 @@ async def test_thermostat(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - assert acc.char_target_temp.value == 22.0 + assert acc.char_target_temp.value == 22.2 assert acc.char_current_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1 - assert acc.char_current_temp.value == 18.0 + assert acc.char_current_temp.value == 17.8 assert acc.char_display_units.value == 0 hass.states.async_set( @@ -432,7 +432,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): ) await hass.async_block_till_done() assert acc.get_temperature_range() == (7.0, 35.0) - assert acc.char_heating_thresh_temp.value == 20.0 + assert acc.char_heating_thresh_temp.value == 20.1 assert acc.char_cooling_thresh_temp.value == 24.0 assert acc.char_current_temp.value == 23.0 assert acc.char_target_temp.value == 22.0 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 923cbaca42f4ce..8898f988f9a373 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -173,7 +173,7 @@ def test_convert_to_float(): def test_temperature_to_homekit(): """Test temperature conversion from HA to HomeKit.""" assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5 - assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.5 + assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.4 def test_temperature_to_states(): From d36d123cf7c64f4c48be5a65a704a512b99248da Mon Sep 17 00:00:00 2001 From: Hugh Eaves Date: Thu, 3 Oct 2019 21:01:06 -0400 Subject: [PATCH 0584/3953] Support zone expanders in alarmdecoder (#27167) --- homeassistant/components/alarmdecoder/__init__.py | 4 ++-- homeassistant/components/alarmdecoder/binary_sensor.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index e0ff80ae9faa7a..61cb0effe53e2a 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -174,7 +174,7 @@ def zone_restore_callback(sender, zone): hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_RESTORE, zone) def handle_rel_message(sender, message): - """Handle relay message from AlarmDecoder.""" + """Handle relay or zone expander message from AlarmDecoder.""" hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message) controller = False @@ -195,7 +195,7 @@ def handle_rel_message(sender, message): controller.on_zone_fault += zone_fault_callback controller.on_zone_restore += zone_restore_callback controller.on_close += handle_closed_connection - controller.on_relay_changed += handle_rel_message + controller.on_expander_message += handle_rel_message hass.data[DATA_AD] = controller diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index bbcc4fd6eae1c2..dc3f16b7d22487 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -151,10 +151,15 @@ def _rfx_message_callback(self, message): self.schedule_update_ha_state() def _rel_message_callback(self, message): - """Update relay state.""" + """Update relay / expander state.""" + if self._relay_addr == message.address and self._relay_chan == message.channel: _LOGGER.debug( - "Relay %d:%d value:%d", message.address, message.channel, message.value + "%s %d:%d value:%d", + "Relay" if message.type == message.RELAY else "ZoneExpander", + message.address, + message.channel, + message.value, ) self._state = message.value self.schedule_update_ha_state() From f50036772196a1a520f19137be28504ca9edf03f Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 4 Oct 2019 01:10:26 +0100 Subject: [PATCH 0585/3953] Handle all single zone thermostats (#27168) --- homeassistant/components/evohome/climate.py | 17 ++++++++--------- .../components/evohome/water_heater.py | 4 +++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index e5c8c6af14bde1..7df2db1b17e512 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -75,21 +75,20 @@ async def async_setup_platform( loc_idx = broker.params[CONF_LOCATION_IDX] _LOGGER.debug( - "Found Location/Controller, id=%s [%s], name=%s (location_idx=%s)", - broker.tcs.systemId, + "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)", broker.tcs.modelType, + broker.tcs.systemId, broker.tcs.location.name, loc_idx, ) - # special case of RoundThermostat (is single zone) - if broker.config["zones"][0]["modelType"] == "RoundModulation": + # special case of RoundModulation/RoundWireless (is a single zone system) + if broker.config["zones"][0]["zoneType"] == "Thermostat": zone = list(broker.tcs.zones.values())[0] _LOGGER.debug( - "Found %s, id=%s [%s], name=%s", - zone.zoneType, - zone.zoneId, + "Found the Thermostat (%s), id=%s, name=%s", zone.modelType, + zone.zoneId, zone.name, ) @@ -101,10 +100,10 @@ async def async_setup_platform( zones = [] for zone in broker.tcs.zones.values(): _LOGGER.debug( - "Found %s, id=%s [%s], name=%s", + "Found a %s (%s), id=%s, name=%s", zone.zoneType, - zone.zoneId, zone.modelType, + zone.zoneId, zone.name, ) zones.append(EvoZone(broker, zone)) diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index b65665eb2c9ad2..37bdcd82afcac7 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -34,7 +34,9 @@ async def async_setup_platform( broker = hass.data[DOMAIN]["broker"] _LOGGER.debug( - "Found %s, id: %s", broker.tcs.hotwater.zone_type, broker.tcs.hotwater.zoneId + "Found the DHW Controller (%s), id: %s", + broker.tcs.hotwater.zone_type, + broker.tcs.hotwater.zoneId, ) evo_dhw = EvoDHW(broker, broker.tcs.hotwater) From 98eaecf61dfd4a18a2b76401d56228666647981f Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Fri, 4 Oct 2019 02:31:45 -0400 Subject: [PATCH 0586/3953] Add device registry support to ecobee integration (#27109) * Add manufacturer const * Add device_info to binary sensor * Add device info to climate * Add device info to sensor * Add device info to weather * Add constant for device info * Fix log messages * Use guard clauses --- .../components/ecobee/binary_sensor.py | 40 ++++++++++++++++++- homeassistant/components/ecobee/climate.py | 25 +++++++++++- homeassistant/components/ecobee/const.py | 14 +++++++ homeassistant/components/ecobee/sensor.py | 40 ++++++++++++++++++- homeassistant/components/ecobee/weather.py | 26 +++++++++++- 5 files changed, 141 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 68d8a88df47b64..97ba94aaa7004f 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -4,7 +4,7 @@ DEVICE_CLASS_OCCUPANCY, ) -from .const import DOMAIN +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -52,6 +52,44 @@ def unique_id(self): return f"{sensor['code']}-{self.device_class}" return f"{sensor['id']}-{self.device_class}" + @property + def device_info(self): + """Return device information for this sensor.""" + identifier = None + model = None + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue + if "code" in sensor: + identifier = sensor["code"] + model = "ecobee Room Sensor" + else: + thermostat = self.data.ecobee.get_thermostat(self.index) + identifier = thermostat["identifier"] + try: + model = ( + f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" + ) + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + thermostat["name"], + thermostat["modelNumber"], + ) + break + + if identifier is not None and model is not None: + return { + "identifiers": {(DOMAIN, identifier)}, + "name": self.sensor_name, + "manufacturer": MANUFACTURER, + "model": model, + } + return None + @property def is_on(self): """Return the status of the sensor.""" diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index f930282ba7b90a..491fd8d686a2b7 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -36,7 +36,7 @@ from homeassistant.util.temperature import convert import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, _LOGGER +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER from .util import ecobee_date, ecobee_time ATTR_COOL_TEMP = "cool_temp" @@ -310,6 +310,29 @@ def unique_id(self): """Return a unique identifier for this ecobee thermostat.""" return self.thermostat["identifier"] + @property + def device_info(self): + """Return device information for this ecobee thermostat.""" + try: + model = f"{ECOBEE_MODEL_TO_NAME[self.thermostat['modelNumber']]} Thermostat" + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + self.name, + self.thermostat["modelNumber"], + ) + return None + + return { + "identifiers": {(DOMAIN, self.thermostat["identifier"])}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": model, + } + @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index c3a23099b8abcd..411f5ddeeeba01 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -9,4 +9,18 @@ CONF_INDEX = "index" CONF_REFRESH_TOKEN = "refresh_token" +ECOBEE_MODEL_TO_NAME = { + "idtSmart": "ecobee Smart", + "idtEms": "ecobee Smart EMS", + "siSmart": "ecobee Si Smart", + "siEms": "ecobee Si EMS", + "athenaSmart": "ecobee3 Smart", + "athenaEms": "ecobee3 EMS", + "corSmart": "Carrier/Bryant Cor", + "nikeSmart": "ecobee3 lite Smart", + "nikeEms": "ecobee3 lite EMS", +} + ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] + +MANUFACTURER = "ecobee" diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 8cf9af0e3b445f..27a8ae3ef07ec6 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -8,7 +8,7 @@ ) from homeassistant.helpers.entity import Entity -from .const import DOMAIN +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER SENSOR_TYPES = { "temperature": ["Temperature", TEMP_FAHRENHEIT], @@ -63,6 +63,44 @@ def unique_id(self): return f"{sensor['code']}-{self.device_class}" return f"{sensor['id']}-{self.device_class}" + @property + def device_info(self): + """Return device information for this sensor.""" + identifier = None + model = None + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue + if "code" in sensor: + identifier = sensor["code"] + model = "ecobee Room Sensor" + else: + thermostat = self.data.ecobee.get_thermostat(self.index) + identifier = thermostat["identifier"] + try: + model = ( + f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" + ) + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + thermostat["name"], + thermostat["modelNumber"], + ) + break + + if identifier is not None and model is not None: + return { + "identifiers": {(DOMAIN, identifier)}, + "name": self.sensor_name, + "manufacturer": MANUFACTURER, + "model": model, + } + return None + @property def device_class(self): """Return the device class of the sensor.""" diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 6175405638e846..53e9842aae7393 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -13,7 +13,7 @@ ) from homeassistant.const import TEMP_FAHRENHEIT -from .const import DOMAIN +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER ATTR_FORECAST_TEMP_HIGH = "temphigh" ATTR_FORECAST_PRESSURE = "pressure" @@ -66,6 +66,30 @@ def unique_id(self): """Return a unique identifier for the weather platform.""" return self.data.ecobee.get_thermostat(self._index)["identifier"] + @property + def device_info(self): + """Return device information for the ecobee weather platform.""" + thermostat = self.data.ecobee.get_thermostat(self._index) + try: + model = f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + thermostat["name"], + thermostat["modelNumber"], + ) + return None + + return { + "identifiers": {(DOMAIN, thermostat["identifier"])}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": model, + } + @property def condition(self): """Return the current condition.""" From 4b4a290f718ca05d06b64368d9feba7ac9385fb1 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Fri, 4 Oct 2019 01:37:30 -0700 Subject: [PATCH 0587/3953] WAQI add unique ID and availability (#27086) * WAQI add unique ID and availability * Review comments * Fix unique ID * Fix unique ID --- homeassistant/components/waqi/sensor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index dbfe6de1a60b46..9f3c3ffc13ebee 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -128,6 +128,16 @@ def state(self): return self._data.get("aqi") return None + @property + def available(self): + """Return sensor availability.""" + return self._data is not None + + @property + def unique_id(self): + """Return unique ID.""" + return self.uid + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" From 8ba4ee1012fcdbfe7b238ef1ac4de34e09adf641 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 4 Oct 2019 13:58:29 +0200 Subject: [PATCH 0588/3953] Add Airly integration (#26375) * Add Airly integration * Update .coveragerc file * Remove AVAILABLE_CONDITIONS and fix device_class * Don't create client on every update * Rename client to session * Rename state_attributes to device_state_attributes * Remove log latitude and longitude * Fix try...except * Change latitude and longitude to HA defaults * _show_config_form doesn't need coroutine * Simplify config_flow errors handlig * Preetier * Remove unnecessary condition * Change sensor platform to air_quality * Remove PM1 * Make unique_id more unique * Remove , * Add tests for config_flow * Move conf to CONFIG * Remove domain from unique_id * Change the way update of attrs * Language and attrs * Fix attrs * Add aiohttp error handling * Throttle as decorator * Suggested change * Suggested change * Invert condition * Cleaning * Add tests * Polish no sesnor error handling * Better strings * Fix test_invalid_api_key * Fix documentation url * Remove unnecessary test * Remove language option * Fix test_invalid_api_key once again * Sort imports * Remove splits in strings --- .coveragerc | 3 + CODEOWNERS | 1 + homeassistant/components/airly/__init__.py | 21 + homeassistant/components/airly/air_quality.py | 201 ++ homeassistant/components/airly/config_flow.py | 114 ++ homeassistant/components/airly/const.py | 4 + homeassistant/components/airly/manifest.json | 9 + homeassistant/components/airly/strings.json | 22 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/airly/__init__.py | 1 + tests/components/airly/test_config_flow.py | 93 + tests/fixtures/airly_no_station.json | 642 ++++++ tests/fixtures/airly_valid_station.json | 1726 +++++++++++++++++ 16 files changed, 2845 insertions(+) create mode 100644 homeassistant/components/airly/__init__.py create mode 100644 homeassistant/components/airly/air_quality.py create mode 100644 homeassistant/components/airly/config_flow.py create mode 100644 homeassistant/components/airly/const.py create mode 100644 homeassistant/components/airly/manifest.json create mode 100644 homeassistant/components/airly/strings.json create mode 100644 tests/components/airly/__init__.py create mode 100644 tests/components/airly/test_config_flow.py create mode 100644 tests/fixtures/airly_no_station.json create mode 100644 tests/fixtures/airly_valid_station.json diff --git a/.coveragerc b/.coveragerc index aa8f2d8c03d16e..25ee023ac84cd7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -19,6 +19,9 @@ omit = homeassistant/components/adguard/switch.py homeassistant/components/ads/* homeassistant/components/aftership/sensor.py + homeassistant/components/airly/__init__.py + homeassistant/components/airly/air_quality.py + homeassistant/components/airly/const.py homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/cover.py homeassistant/components/alarm_control_panel/manual_mqtt.py diff --git a/CODEOWNERS b/CODEOWNERS index 418eb745ecb9b3..8073020712d6be 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -14,6 +14,7 @@ homeassistant/scripts/check_config.py @kellerza # Integrations homeassistant/components/adguard/* @frenck +homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya homeassistant/components/alarm_control_panel/* @colinodell homeassistant/components/alexa/* @home-assistant/cloud diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py new file mode 100644 index 00000000000000..56b3477ac89b36 --- /dev/null +++ b/homeassistant/components/airly/__init__.py @@ -0,0 +1,21 @@ +"""The Airly component.""" +from homeassistant.core import Config, HomeAssistant + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Set up configured Airly.""" + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Airly as config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") + ) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality") + return True diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py new file mode 100644 index 00000000000000..a8ec82ab3041ec --- /dev/null +++ b/homeassistant/components/airly/air_quality.py @@ -0,0 +1,201 @@ +"""Support for the Airly service.""" +import asyncio +import logging +from datetime import timedelta + +import async_timeout +from aiohttp.client_exceptions import ClientConnectorError +from airly import Airly +from airly.exceptions import AirlyError + +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.components.air_quality import ( + AirQualityEntity, + ATTR_AQI, + ATTR_PM_10, + ATTR_PM_2_5, +) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import Throttle + +from .const import NO_AIRLY_SENSORS + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Data provided by Airly" + +ATTR_API_ADVICE = "ADVICE" +ATTR_API_CAQI = "CAQI" +ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION" +ATTR_API_CAQI_LEVEL = "LEVEL" +ATTR_API_PM10 = "PM10" +ATTR_API_PM10_LIMIT = "PM10_LIMIT" +ATTR_API_PM10_PERCENT = "PM10_PERCENT" +ATTR_API_PM25 = "PM25" +ATTR_API_PM25_LIMIT = "PM25_LIMIT" +ATTR_API_PM25_PERCENT = "PM25_PERCENT" + +LABEL_ADVICE = "advice" +LABEL_AQI_LEVEL = f"{ATTR_AQI}_level" +LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit" +LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit" +LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit" +LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a Airly entities from a config_entry.""" + api_key = config_entry.data[CONF_API_KEY] + name = config_entry.data[CONF_NAME] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + + websession = async_get_clientsession(hass) + + data = AirlyData(websession, api_key, latitude, longitude) + + async_add_entities([AirlyAirQuality(data, name)], True) + + +def round_state(func): + """Round state.""" + + def _decorator(self): + res = func(self) + if isinstance(res, float): + return round(res) + return res + + return _decorator + + +class AirlyAirQuality(AirQualityEntity): + """Define an Airly air_quality.""" + + def __init__(self, airly, name): + """Initialize.""" + self.airly = airly + self.data = airly.data + self._name = name + self._pm_2_5 = None + self._pm_10 = None + self._aqi = None + self._icon = "mdi:blur" + self._attrs = {} + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + @round_state + def air_quality_index(self): + """Return the air quality index.""" + return self._aqi + + @property + @round_state + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._pm_2_5 + + @property + @round_state + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._pm_10 + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def state(self): + """Return the CAQI description.""" + return self.data[ATTR_API_CAQI_DESCRIPTION] + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self.airly.latitude}-{self.airly.longitude}" + + @property + def available(self): + """Return True if entity is available.""" + return bool(self.airly.data) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + self._attrs[LABEL_ADVICE] = self.data[ATTR_API_ADVICE] + self._attrs[LABEL_AQI_LEVEL] = self.data[ATTR_API_CAQI_LEVEL] + self._attrs[LABEL_PM_2_5_LIMIT] = self.data[ATTR_API_PM25_LIMIT] + self._attrs[LABEL_PM_2_5_PERCENT] = round(self.data[ATTR_API_PM25_PERCENT]) + self._attrs[LABEL_PM_10_LIMIT] = self.data[ATTR_API_PM10_LIMIT] + self._attrs[LABEL_PM_10_PERCENT] = round(self.data[ATTR_API_PM10_PERCENT]) + return self._attrs + + async def async_update(self): + """Get the data from Airly.""" + await self.airly.async_update() + + self._pm_10 = self.data[ATTR_API_PM10] + self._pm_2_5 = self.data[ATTR_API_PM25] + self._aqi = self.data[ATTR_API_CAQI] + + +class AirlyData: + """Define an object to hold sensor data.""" + + def __init__(self, session, api_key, latitude, longitude): + """Initialize.""" + self.latitude = latitude + self.longitude = longitude + self.airly = Airly(api_key, session) + self.data = {} + + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update Airly data.""" + + try: + with async_timeout.timeout(10): + measurements = self.airly.create_measurements_session_point( + self.latitude, self.longitude + ) + await measurements.update() + + values = measurements.current["values"] + index = measurements.current["indexes"][0] + standards = measurements.current["standards"] + + if index["description"] == NO_AIRLY_SENSORS: + _LOGGER.error("Can't retrieve data: no Airly sensors in this area") + return + for value in values: + self.data[value["name"]] = value["value"] + for standard in standards: + self.data[f"{standard['pollutant']}_LIMIT"] = standard["limit"] + self.data[f"{standard['pollutant']}_PERCENT"] = standard["percent"] + self.data[ATTR_API_CAQI] = index["value"] + self.data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ") + self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"] + self.data[ATTR_API_ADVICE] = index["advice"] + _LOGGER.debug("Data retrieved from Airly") + except ( + ValueError, + AirlyError, + asyncio.TimeoutError, + ClientConnectorError, + ) as error: + _LOGGER.error(error) + self.data = {} diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py new file mode 100644 index 00000000000000..b361930fa7da0a --- /dev/null +++ b/homeassistant/components/airly/config_flow.py @@ -0,0 +1,114 @@ +"""Adds config flow for Airly.""" +import async_timeout +import voluptuous as vol +from airly import Airly +from airly.exceptions import AirlyError + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS + + +@callback +def configured_instances(hass): + """Return a set of configured Airly instances.""" + return set( + entry.data[CONF_NAME] for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Airly.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize.""" + self._errors = {} + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + self._errors = {} + + websession = async_get_clientsession(self.hass) + + if user_input is not None: + if user_input[CONF_NAME] in configured_instances(self.hass): + self._errors[CONF_NAME] = "name_exists" + api_key_valid = await self._test_api_key(websession, user_input["api_key"]) + if not api_key_valid: + self._errors["base"] = "auth" + else: + location_valid = await self._test_location( + websession, + user_input["api_key"], + user_input["latitude"], + user_input["longitude"], + ) + if not location_valid: + self._errors["base"] = "wrong_location" + + if not self._errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + + return self._show_config_form( + name=DEFAULT_NAME, + api_key="", + latitude=self.hass.config.latitude, + longitude=self.hass.config.longitude, + ) + + def _show_config_form(self, name=None, api_key=None, latitude=None, longitude=None): + """Show the configuration form to edit data.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY, default=api_key): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Optional(CONF_NAME, default=name): str, + } + ), + errors=self._errors, + ) + + async def _test_api_key(self, client, api_key): + """Return true if api_key is valid.""" + + with async_timeout.timeout(10): + airly = Airly(api_key, client) + measurements = airly.create_measurements_session_point( + latitude=52.24131, longitude=20.99101 + ) + try: + await measurements.update() + except AirlyError: + return False + return True + + async def _test_location(self, client, api_key, latitude, longitude): + """Return true if location is valid.""" + + with async_timeout.timeout(10): + airly = Airly(api_key, client) + measurements = airly.create_measurements_session_point( + latitude=latitude, longitude=longitude + ) + + await measurements.update() + current = measurements.current + if current["indexes"][0]["description"] == NO_AIRLY_SENSORS: + return False + return True diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py new file mode 100644 index 00000000000000..5313ba0e494cbb --- /dev/null +++ b/homeassistant/components/airly/const.py @@ -0,0 +1,4 @@ +"""Constants for Airly integration.""" +DEFAULT_NAME = "Airly" +DOMAIN = "airly" +NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json new file mode 100644 index 00000000000000..1859f084bf1fae --- /dev/null +++ b/homeassistant/components/airly/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "airly", + "name": "Airly", + "documentation": "https://www.home-assistant.io/integrations/airly", + "dependencies": [], + "codeowners": ["@bieniu"], + "requirements": ["airly==0.0.2"], + "config_flow": true +} diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json new file mode 100644 index 00000000000000..116b6df83e605a --- /dev/null +++ b/homeassistant/components/airly/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "Airly", + "step": { + "user": { + "title": "Airly", + "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "data": { + "name": "Name of the integration", + "api_key": "Airly API key", + "latitude": "Latitude", + "longitude": "Longitude" + } + } + }, + "error": { + "name_exists": "Name already exists.", + "wrong_location": "No Airly measuring stations in this area.", + "auth": "API key is not correct." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 21f57934e95bd6..7557fc32b401f1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -7,6 +7,7 @@ FLOWS = [ "adguard", + "airly", "ambiclimate", "ambient_station", "axis", diff --git a/requirements_all.txt b/requirements_all.txt index 208eeac899b837..5731158cd2f477 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -184,6 +184,9 @@ aiounifi==11 # homeassistant.components.wwlln aiowwlln==2.0.2 +# homeassistant.components.airly +airly==0.0.2 + # homeassistant.components.aladdin_connect aladdin_connect==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bc9870be10c5c..1e948ded8a7328 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -76,6 +76,9 @@ aiounifi==11 # homeassistant.components.wwlln aiowwlln==2.0.2 +# homeassistant.components.airly +airly==0.0.2 + # homeassistant.components.ambiclimate ambiclimate==0.2.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e35a83bd24d7ea..a6f2584e256c22 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -54,6 +54,7 @@ "aioswitcher", "aiounifi", "aiowwlln", + "airly", "ambiclimate", "androidtv", "apns2", diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py new file mode 100644 index 00000000000000..f31dfb7712d498 --- /dev/null +++ b/tests/components/airly/__init__.py @@ -0,0 +1 @@ +"""Tests for Airly.""" diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py new file mode 100644 index 00000000000000..8b615b34c2aa13 --- /dev/null +++ b/tests/components/airly/test_config_flow.py @@ -0,0 +1,93 @@ +"""Define tests for the Airly config flow.""" +import json + +from airly.exceptions import AirlyError +from asynctest import patch + +from homeassistant import data_entry_flow +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.components.airly import config_flow +from homeassistant.components.airly.const import DOMAIN + +from tests.common import load_fixture, MockConfigEntry + +CONFIG = { + CONF_NAME: "abcd", + CONF_API_KEY: "foo", + CONF_LATITUDE: 123, + CONF_LONGITUDE: 456, +} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.AirlyFlowHandler() + 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_invalid_api_key(hass): + """Test that errors are shown when API key is invalid.""" + with patch( + "airly._private._RequestsHandler.get", + side_effect=AirlyError(403, {"message": "Invalid authentication credentials"}), + ): + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {"base": "auth"} + + +async def test_invalid_location(hass): + """Test that errors are shown when location is invalid.""" + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_no_station.json")), + ): + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {"base": "wrong_location"} + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_valid_station.json")), + ): + MockConfigEntry(domain=DOMAIN, data=CONFIG).add_to_hass(hass) + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {CONF_NAME: "name_exists"} + + +async def test_create_entry(hass): + """Test that the user step works.""" + + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_valid_station.json")), + ): + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONFIG[CONF_NAME] + assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] + assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] + assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] diff --git a/tests/fixtures/airly_no_station.json b/tests/fixtures/airly_no_station.json new file mode 100644 index 00000000000000..cc64934938f540 --- /dev/null +++ b/tests/fixtures/airly_no_station.json @@ -0,0 +1,642 @@ +{ + "current": { + "fromDateTime": "2019-10-02T05:53:00.608Z", + "tillDateTime": "2019-10-02T06:53:00.608Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, + "history": [{ + "fromDateTime": "2019-10-01T06:00:00.000Z", + "tillDateTime": "2019-10-01T07:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T07:00:00.000Z", + "tillDateTime": "2019-10-01T08:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T08:00:00.000Z", + "tillDateTime": "2019-10-01T09:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T09:00:00.000Z", + "tillDateTime": "2019-10-01T10:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T10:00:00.000Z", + "tillDateTime": "2019-10-01T11:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T11:00:00.000Z", + "tillDateTime": "2019-10-01T12:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T12:00:00.000Z", + "tillDateTime": "2019-10-01T13:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T13:00:00.000Z", + "tillDateTime": "2019-10-01T14:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T14:00:00.000Z", + "tillDateTime": "2019-10-01T15:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T15:00:00.000Z", + "tillDateTime": "2019-10-01T16:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T16:00:00.000Z", + "tillDateTime": "2019-10-01T17:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T17:00:00.000Z", + "tillDateTime": "2019-10-01T18:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T18:00:00.000Z", + "tillDateTime": "2019-10-01T19:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T19:00:00.000Z", + "tillDateTime": "2019-10-01T20:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T20:00:00.000Z", + "tillDateTime": "2019-10-01T21:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T21:00:00.000Z", + "tillDateTime": "2019-10-01T22:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T22:00:00.000Z", + "tillDateTime": "2019-10-01T23:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T23:00:00.000Z", + "tillDateTime": "2019-10-02T00:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T00:00:00.000Z", + "tillDateTime": "2019-10-02T01:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T01:00:00.000Z", + "tillDateTime": "2019-10-02T02:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T02:00:00.000Z", + "tillDateTime": "2019-10-02T03:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T03:00:00.000Z", + "tillDateTime": "2019-10-02T04:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T04:00:00.000Z", + "tillDateTime": "2019-10-02T05:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T05:00:00.000Z", + "tillDateTime": "2019-10-02T06:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }], + "forecast": [{ + "fromDateTime": "2019-10-02T06:00:00.000Z", + "tillDateTime": "2019-10-02T07:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T07:00:00.000Z", + "tillDateTime": "2019-10-02T08:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T08:00:00.000Z", + "tillDateTime": "2019-10-02T09:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T09:00:00.000Z", + "tillDateTime": "2019-10-02T10:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T10:00:00.000Z", + "tillDateTime": "2019-10-02T11:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T11:00:00.000Z", + "tillDateTime": "2019-10-02T12:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T12:00:00.000Z", + "tillDateTime": "2019-10-02T13:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T13:00:00.000Z", + "tillDateTime": "2019-10-02T14:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T14:00:00.000Z", + "tillDateTime": "2019-10-02T15:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T15:00:00.000Z", + "tillDateTime": "2019-10-02T16:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T16:00:00.000Z", + "tillDateTime": "2019-10-02T17:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T17:00:00.000Z", + "tillDateTime": "2019-10-02T18:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T18:00:00.000Z", + "tillDateTime": "2019-10-02T19:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T19:00:00.000Z", + "tillDateTime": "2019-10-02T20:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T20:00:00.000Z", + "tillDateTime": "2019-10-02T21:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T21:00:00.000Z", + "tillDateTime": "2019-10-02T22:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T22:00:00.000Z", + "tillDateTime": "2019-10-02T23:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T23:00:00.000Z", + "tillDateTime": "2019-10-03T00:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T00:00:00.000Z", + "tillDateTime": "2019-10-03T01:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T01:00:00.000Z", + "tillDateTime": "2019-10-03T02:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T02:00:00.000Z", + "tillDateTime": "2019-10-03T03:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T03:00:00.000Z", + "tillDateTime": "2019-10-03T04:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T04:00:00.000Z", + "tillDateTime": "2019-10-03T05:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T05:00:00.000Z", + "tillDateTime": "2019-10-03T06:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }] +} \ No newline at end of file diff --git a/tests/fixtures/airly_valid_station.json b/tests/fixtures/airly_valid_station.json new file mode 100644 index 00000000000000..656c62c04c27b9 --- /dev/null +++ b/tests/fixtures/airly_valid_station.json @@ -0,0 +1,1726 @@ +{ + "current": { + "fromDateTime": "2019-10-02T05:54:57.204Z", + "tillDateTime": "2019-10-02T06:54:57.204Z", + "values": [{ + "name": "PM1", + "value": 9.23 + }, { + "name": "PM25", + "value": 13.71 + }, { + "name": "PM10", + "value": 18.58 + }, { + "name": "PRESSURE", + "value": 1000.87 + }, { + "name": "HUMIDITY", + "value": 92.84 + }, { + "name": "TEMPERATURE", + "value": 14.23 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.85, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 54.84 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 37.17 + }] + }, + "history": [{ + "fromDateTime": "2019-10-01T06:00:00.000Z", + "tillDateTime": "2019-10-01T07:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 5.95 + }, { + "name": "PM25", + "value": 8.54 + }, { + "name": "PM10", + "value": 11.46 + }, { + "name": "PRESSURE", + "value": 1009.61 + }, { + "name": "HUMIDITY", + "value": 97.6 + }, { + "name": "TEMPERATURE", + "value": 9.71 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 14.24, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green equals clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 34.18 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 22.91 + }] + }, { + "fromDateTime": "2019-10-01T07:00:00.000Z", + "tillDateTime": "2019-10-01T08:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 4.2 + }, { + "name": "PM25", + "value": 5.88 + }, { + "name": "PM10", + "value": 7.88 + }, { + "name": "PRESSURE", + "value": 1009.13 + }, { + "name": "HUMIDITY", + "value": 90.84 + }, { + "name": "TEMPERATURE", + "value": 12.65 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.81, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 23.53 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 15.75 + }] + }, { + "fromDateTime": "2019-10-01T08:00:00.000Z", + "tillDateTime": "2019-10-01T09:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 3.63 + }, { + "name": "PM25", + "value": 5.56 + }, { + "name": "PM10", + "value": 7.71 + }, { + "name": "PRESSURE", + "value": 1008.27 + }, { + "name": "HUMIDITY", + "value": 84.61 + }, { + "name": "TEMPERATURE", + "value": 15.57 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.26, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 22.23 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 15.42 + }] + }, { + "fromDateTime": "2019-10-01T09:00:00.000Z", + "tillDateTime": "2019-10-01T10:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.9 + }, { + "name": "PM25", + "value": 3.93 + }, { + "name": "PM10", + "value": 5.24 + }, { + "name": "PRESSURE", + "value": 1007.57 + }, { + "name": "HUMIDITY", + "value": 79.52 + }, { + "name": "TEMPERATURE", + "value": 16.57 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 6.56, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deep! The air is clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 15.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.48 + }] + }, { + "fromDateTime": "2019-10-01T10:00:00.000Z", + "tillDateTime": "2019-10-01T11:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.45 + }, { + "name": "PM25", + "value": 3.33 + }, { + "name": "PM10", + "value": 4.52 + }, { + "name": "PRESSURE", + "value": 1006.75 + }, { + "name": "HUMIDITY", + "value": 74.09 + }, { + "name": "TEMPERATURE", + "value": 16.95 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.55, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is grand today. ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 13.31 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 9.04 + }] + }, { + "fromDateTime": "2019-10-01T11:00:00.000Z", + "tillDateTime": "2019-10-01T12:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.0 + }, { + "name": "PM25", + "value": 2.93 + }, { + "name": "PM10", + "value": 3.98 + }, { + "name": "PRESSURE", + "value": 1005.71 + }, { + "name": "HUMIDITY", + "value": 69.06 + }, { + "name": "TEMPERATURE", + "value": 17.31 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 4.89, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green equals clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 11.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.96 + }] + }, { + "fromDateTime": "2019-10-01T12:00:00.000Z", + "tillDateTime": "2019-10-01T13:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 1.92 + }, { + "name": "PM25", + "value": 2.69 + }, { + "name": "PM10", + "value": 3.68 + }, { + "name": "PRESSURE", + "value": 1005.03 + }, { + "name": "HUMIDITY", + "value": 65.08 + }, { + "name": "TEMPERATURE", + "value": 17.47 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 4.49, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.77 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.36 + }] + }, { + "fromDateTime": "2019-10-01T13:00:00.000Z", + "tillDateTime": "2019-10-01T14:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 1.79 + }, { + "name": "PM25", + "value": 2.57 + }, { + "name": "PM10", + "value": 3.53 + }, { + "name": "PRESSURE", + "value": 1004.26 + }, { + "name": "HUMIDITY", + "value": 63.72 + }, { + "name": "TEMPERATURE", + "value": 17.91 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 4.29, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.29 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.06 + }] + }, { + "fromDateTime": "2019-10-01T14:00:00.000Z", + "tillDateTime": "2019-10-01T15:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.06 + }, { + "name": "PM25", + "value": 3.08 + }, { + "name": "PM10", + "value": 4.23 + }, { + "name": "PRESSURE", + "value": 1003.46 + }, { + "name": "HUMIDITY", + "value": 64.44 + }, { + "name": "TEMPERATURE", + "value": 17.84 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.14, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is grand today. ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 12.33 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 8.47 + }] + }, { + "fromDateTime": "2019-10-01T15:00:00.000Z", + "tillDateTime": "2019-10-01T16:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 3.17 + }, { + "name": "PM25", + "value": 4.61 + }, { + "name": "PM10", + "value": 6.25 + }, { + "name": "PRESSURE", + "value": 1003.18 + }, { + "name": "HUMIDITY", + "value": 65.32 + }, { + "name": "TEMPERATURE", + "value": 18.08 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 7.68, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 18.44 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 12.5 + }] + }, { + "fromDateTime": "2019-10-01T16:00:00.000Z", + "tillDateTime": "2019-10-01T17:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 4.17 + }, { + "name": "PM25", + "value": 5.91 + }, { + "name": "PM10", + "value": 8.06 + }, { + "name": "PRESSURE", + "value": 1003.05 + }, { + "name": "HUMIDITY", + "value": 66.14 + }, { + "name": "TEMPERATURE", + "value": 17.04 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.84, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 23.62 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 16.11 + }] + }, { + "fromDateTime": "2019-10-01T17:00:00.000Z", + "tillDateTime": "2019-10-01T18:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 6.4 + }, { + "name": "PM25", + "value": 10.93 + }, { + "name": "PM10", + "value": 15.7 + }, { + "name": "PRESSURE", + "value": 1002.85 + }, { + "name": "HUMIDITY", + "value": 68.31 + }, { + "name": "TEMPERATURE", + "value": 16.33 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 18.22, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.4 + }] + }, { + "fromDateTime": "2019-10-01T18:00:00.000Z", + "tillDateTime": "2019-10-01T19:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 4.79 + }, { + "name": "PM25", + "value": 7.41 + }, { + "name": "PM10", + "value": 10.31 + }, { + "name": "PRESSURE", + "value": 1002.52 + }, { + "name": "HUMIDITY", + "value": 69.88 + }, { + "name": "TEMPERATURE", + "value": 15.98 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 12.35, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 29.65 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 20.63 + }] + }, { + "fromDateTime": "2019-10-01T19:00:00.000Z", + "tillDateTime": "2019-10-01T20:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 5.99 + }, { + "name": "PM25", + "value": 9.45 + }, { + "name": "PM10", + "value": 13.22 + }, { + "name": "PRESSURE", + "value": 1002.32 + }, { + "name": "HUMIDITY", + "value": 70.47 + }, { + "name": "TEMPERATURE", + "value": 15.76 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 15.74, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deeply!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 37.78 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 26.44 + }] + }, { + "fromDateTime": "2019-10-01T20:00:00.000Z", + "tillDateTime": "2019-10-01T21:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.35 + }, { + "name": "PM25", + "value": 14.67 + }, { + "name": "PM10", + "value": 20.57 + }, { + "name": "PRESSURE", + "value": 1002.46 + }, { + "name": "HUMIDITY", + "value": 72.61 + }, { + "name": "TEMPERATURE", + "value": 15.47 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 24.45, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 58.68 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.13 + }] + }, { + "fromDateTime": "2019-10-01T21:00:00.000Z", + "tillDateTime": "2019-10-01T22:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.95 + }, { + "name": "PM25", + "value": 15.37 + }, { + "name": "PM10", + "value": 21.33 + }, { + "name": "PRESSURE", + "value": 1002.59 + }, { + "name": "HUMIDITY", + "value": 75.09 + }, { + "name": "TEMPERATURE", + "value": 15.17 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.62, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Take a breath!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 61.48 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.66 + }] + }, { + "fromDateTime": "2019-10-01T22:00:00.000Z", + "tillDateTime": "2019-10-01T23:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 10.16 + }, { + "name": "PM25", + "value": 15.78 + }, { + "name": "PM10", + "value": 21.97 + }, { + "name": "PRESSURE", + "value": 1002.59 + }, { + "name": "HUMIDITY", + "value": 77.68 + }, { + "name": "TEMPERATURE", + "value": 14.9 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 26.31, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Great air for a walk to the park!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 63.14 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 43.93 + }] + }, { + "fromDateTime": "2019-10-01T23:00:00.000Z", + "tillDateTime": "2019-10-02T00:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.86 + }, { + "name": "PM25", + "value": 15.14 + }, { + "name": "PM10", + "value": 21.07 + }, { + "name": "PRESSURE", + "value": 1002.49 + }, { + "name": "HUMIDITY", + "value": 79.86 + }, { + "name": "TEMPERATURE", + "value": 14.56 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.24, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Leave the mask at home today!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 60.57 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.14 + }] + }, { + "fromDateTime": "2019-10-02T00:00:00.000Z", + "tillDateTime": "2019-10-02T01:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.77 + }, { + "name": "PM25", + "value": 15.04 + }, { + "name": "PM10", + "value": 20.97 + }, { + "name": "PRESSURE", + "value": 1002.18 + }, { + "name": "HUMIDITY", + "value": 81.77 + }, { + "name": "TEMPERATURE", + "value": 14.13 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.07, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Time for a walk with friends or activities with your family - because the air is clean!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 60.18 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.94 + }] + }, { + "fromDateTime": "2019-10-02T01:00:00.000Z", + "tillDateTime": "2019-10-02T02:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.67 + }, { + "name": "PM25", + "value": 14.9 + }, { + "name": "PM10", + "value": 20.67 + }, { + "name": "PRESSURE", + "value": 1002.01 + }, { + "name": "HUMIDITY", + "value": 84.5 + }, { + "name": "TEMPERATURE", + "value": 13.7 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 24.84, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 59.62 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.33 + }] + }, { + "fromDateTime": "2019-10-02T02:00:00.000Z", + "tillDateTime": "2019-10-02T03:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 7.17 + }, { + "name": "PM25", + "value": 10.7 + }, { + "name": "PM10", + "value": 14.58 + }, { + "name": "PRESSURE", + "value": 1001.56 + }, { + "name": "HUMIDITY", + "value": 88.55 + }, { + "name": "TEMPERATURE", + "value": 13.44 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 17.83, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Catch your breath!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 42.8 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 29.17 + }] + }, { + "fromDateTime": "2019-10-02T03:00:00.000Z", + "tillDateTime": "2019-10-02T04:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 6.99 + }, { + "name": "PM25", + "value": 10.23 + }, { + "name": "PM10", + "value": 13.66 + }, { + "name": "PRESSURE", + "value": 1001.34 + }, { + "name": "HUMIDITY", + "value": 90.82 + }, { + "name": "TEMPERATURE", + "value": 13.3 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 17.05, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Perfect air for exercising! Go for it!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 40.91 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.33 + }] + }, { + "fromDateTime": "2019-10-02T04:00:00.000Z", + "tillDateTime": "2019-10-02T05:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 7.82 + }, { + "name": "PM25", + "value": 11.59 + }, { + "name": "PM10", + "value": 15.77 + }, { + "name": "PRESSURE", + "value": 1000.92 + }, { + "name": "HUMIDITY", + "value": 91.8 + }, { + "name": "TEMPERATURE", + "value": 13.34 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.32, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 46.36 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.54 + }] + }, { + "fromDateTime": "2019-10-02T05:00:00.000Z", + "tillDateTime": "2019-10-02T06:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 10.16 + }, { + "name": "PM25", + "value": 15.35 + }, { + "name": "PM10", + "value": 21.45 + }, { + "name": "PRESSURE", + "value": 1000.82 + }, { + "name": "HUMIDITY", + "value": 92.15 + }, { + "name": "TEMPERATURE", + "value": 13.74 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.59, + "level": "LOW", + "description": "Air is quite good.", + "advice": "How about going for a walk?", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 61.42 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.9 + }] + }], + "forecast": [{ + "fromDateTime": "2019-10-02T06:00:00.000Z", + "tillDateTime": "2019-10-02T07:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.28 + }, { + "name": "PM10", + "value": 18.37 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.14, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 53.13 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.73 + }] + }, { + "fromDateTime": "2019-10-02T07:00:00.000Z", + "tillDateTime": "2019-10-02T08:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.19 + }, { + "name": "PM10", + "value": 15.65 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 18.65, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 44.76 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.31 + }] + }, { + "fromDateTime": "2019-10-02T08:00:00.000Z", + "tillDateTime": "2019-10-02T09:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 8.79 + }, { + "name": "PM10", + "value": 12.8 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 14.65, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deep! The air is clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 35.15 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 25.59 + }] + }, { + "fromDateTime": "2019-10-02T09:00:00.000Z", + "tillDateTime": "2019-10-02T10:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 5.46 + }, { + "name": "PM10", + "value": 8.91 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.11, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 21.86 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 17.83 + }] + }, { + "fromDateTime": "2019-10-02T10:00:00.000Z", + "tillDateTime": "2019-10-02T11:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 2.26 + }, { + "name": "PM10", + "value": 5.02 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 9.06 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.05 + }] + }, { + "fromDateTime": "2019-10-02T11:00:00.000Z", + "tillDateTime": "2019-10-02T12:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 1.06 + }, { + "name": "PM10", + "value": 2.52 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 2.52, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 4.22 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 5.05 + }] + }, { + "fromDateTime": "2019-10-02T12:00:00.000Z", + "tillDateTime": "2019-10-02T13:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 0.48 + }, { + "name": "PM10", + "value": 1.94 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 1.94, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 1.94 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 3.89 + }] + }, { + "fromDateTime": "2019-10-02T13:00:00.000Z", + "tillDateTime": "2019-10-02T14:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 0.63 + }, { + "name": "PM10", + "value": 2.26 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 2.26, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 2.53 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 4.52 + }] + }, { + "fromDateTime": "2019-10-02T14:00:00.000Z", + "tillDateTime": "2019-10-02T15:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 1.47 + }, { + "name": "PM10", + "value": 3.39 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 3.39, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 5.87 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 6.78 + }] + }, { + "fromDateTime": "2019-10-02T15:00:00.000Z", + "tillDateTime": "2019-10-02T16:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 2.62 + }, { + "name": "PM10", + "value": 5.02 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.5 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.05 + }] + }, { + "fromDateTime": "2019-10-02T16:00:00.000Z", + "tillDateTime": "2019-10-02T17:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 3.89 + }, { + "name": "PM10", + "value": 8.02 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 8.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 15.56 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 16.04 + }] + }, { + "fromDateTime": "2019-10-02T17:00:00.000Z", + "tillDateTime": "2019-10-02T18:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 6.26 + }, { + "name": "PM10", + "value": 11.41 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 11.41, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 25.05 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 22.83 + }] + }, { + "fromDateTime": "2019-10-02T18:00:00.000Z", + "tillDateTime": "2019-10-02T19:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 8.69 + }, { + "name": "PM10", + "value": 14.48 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 14.48, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Zero dust - zero worries!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 34.76 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 28.96 + }] + }, { + "fromDateTime": "2019-10-02T19:00:00.000Z", + "tillDateTime": "2019-10-02T20:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 10.78 + }, { + "name": "PM10", + "value": 16.86 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 17.97, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Zero dust - zero worries!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.13 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 33.72 + }] + }, { + "fromDateTime": "2019-10-02T20:00:00.000Z", + "tillDateTime": "2019-10-02T21:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 12.22 + }, { + "name": "PM10", + "value": 18.19 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 20.36, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 48.88 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.38 + }] + }, { + "fromDateTime": "2019-10-02T21:00:00.000Z", + "tillDateTime": "2019-10-02T22:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.06 + }, { + "name": "PM10", + "value": 18.62 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 21.77, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 52.25 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 37.24 + }] + }, { + "fromDateTime": "2019-10-02T22:00:00.000Z", + "tillDateTime": "2019-10-02T23:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.51 + }, { + "name": "PM10", + "value": 18.49 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.52, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 54.06 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.98 + }] + }, { + "fromDateTime": "2019-10-02T23:00:00.000Z", + "tillDateTime": "2019-10-03T00:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.46 + }, { + "name": "PM10", + "value": 17.63 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.44, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 53.85 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 35.26 + }] + }, { + "fromDateTime": "2019-10-03T00:00:00.000Z", + "tillDateTime": "2019-10-03T01:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.05 + }, { + "name": "PM10", + "value": 16.36 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 21.74, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Catch your breath!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 52.19 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 32.73 + }] + }, { + "fromDateTime": "2019-10-03T01:00:00.000Z", + "tillDateTime": "2019-10-03T02:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 12.47 + }, { + "name": "PM10", + "value": 15.16 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 20.79, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 49.9 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 30.32 + }] + }, { + "fromDateTime": "2019-10-03T02:00:00.000Z", + "tillDateTime": "2019-10-03T03:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.99 + }, { + "name": "PM10", + "value": 14.07 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.98, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 47.94 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 28.14 + }] + }, { + "fromDateTime": "2019-10-03T03:00:00.000Z", + "tillDateTime": "2019-10-03T04:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.74 + }, { + "name": "PM10", + "value": 13.67 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.56, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 46.95 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.34 + }] + }, { + "fromDateTime": "2019-10-03T04:00:00.000Z", + "tillDateTime": "2019-10-03T05:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.44 + }, { + "name": "PM10", + "value": 13.51 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.06, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 45.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.02 + }] + }, { + "fromDateTime": "2019-10-03T05:00:00.000Z", + "tillDateTime": "2019-10-03T06:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 10.88 + }, { + "name": "PM10", + "value": 13.38 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 18.13, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.52 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 26.76 + }] + }] +} \ No newline at end of file From f169e84d2170aa4dca16fedbdcfd4c00c6e85449 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 4 Oct 2019 17:05:52 +0200 Subject: [PATCH 0589/3953] Update connect-box to fix issue with attrs (#27194) --- homeassistant/components/upc_connect/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 2cf463d1cf0b25..cd5d327f2c22ae 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "upc_connect", "name": "Upc connect", "documentation": "https://www.home-assistant.io/integrations/upc_connect", - "requirements": ["connect-box==0.2.4"], + "requirements": ["connect-box==0.2.5"], "dependencies": [], "codeowners": ["@pvizeli"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5731158cd2f477..9ce8b070b1e102 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -359,7 +359,7 @@ colorlog==4.0.2 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.4 +connect-box==0.2.5 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart From 9a5c1fbaedb7d8b20015c65037fb01b7d4c3e06f Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 4 Oct 2019 11:41:47 -0400 Subject: [PATCH 0590/3953] Add SecurityPanelController for alarm_control_panel to alexa (#27081) * Implemented Alexa.SecurityPanelController Interface for alarm_control_panel https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html * Implemented Tests for Alexa.SecurityPanelController Interface for alarm_control_panel * Added additional AuthorizationRequired error handling * Removed optional exitDelayInSeconds * Updating elif to if to please pylint * Adding self to code owners. * Adding self to code owners. * Added AlexaEndpointHealth Interface to alarm_control_panel entities. * Added additional entity tests. * Code reformatted with Black. * Updated alexa alarm_control_panel tests for more coverage. * Updated alexa alarm_control_panel tests for more coverage. Fixed Test. * Adding self to code owners. --- CODEOWNERS | 2 +- .../components/alexa/capabilities.py | 67 ++++++++++ homeassistant/components/alexa/entities.py | 17 +++ homeassistant/components/alexa/errors.py | 14 ++ homeassistant/components/alexa/handlers.py | 75 +++++++++++ homeassistant/components/alexa/manifest.json | 5 +- tests/components/alexa/test_capabilities.py | 35 +++++ tests/components/alexa/test_smart_home.py | 124 ++++++++++++++++++ 8 files changed, 337 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8073020712d6be..935d68033e3b7b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,7 +17,7 @@ homeassistant/components/adguard/* @frenck homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya homeassistant/components/alarm_control_panel/* @colinodell -homeassistant/components/alexa/* @home-assistant/cloud +homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index fca63adab0e3e5..7be3188fea1fea 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -5,6 +5,10 @@ ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_LOCKED, STATE_OFF, STATE_ON, @@ -13,6 +17,7 @@ STATE_UNKNOWN, ) import homeassistant.components.climate.const as climate +from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER from homeassistant.components import light, fan, cover import homeassistant.util.color as color_util import homeassistant.util.dt as dt_util @@ -79,6 +84,11 @@ def supports_deactivation(): """Applicable only to scenes.""" return None + @staticmethod + def configuration(): + """Applicable only to security control panel.""" + return [] + def serialize_discovery(self): """Serialize according to the Discovery API.""" result = { @@ -96,6 +106,11 @@ def serialize_discovery(self): supports_deactivation = self.supports_deactivation() if supports_deactivation is not None: result["supportsDeactivation"] = supports_deactivation + + configuration = self.configuration() + if configuration: + result["configuration"] = configuration + return result def serialize_properties(self): @@ -649,3 +664,55 @@ def get_property(self, name): return PERCENTAGE_FAN_MAP.get(speed, None) return None + + +class AlexaSecurityPanelController(AlexaCapibility): + """Implements Alexa.SecurityPanelController. + + https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html + """ + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.SecurityPanelController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "armState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "armState": + raise UnsupportedProperty(name) + + arm_state = self.entity.state + if arm_state == STATE_ALARM_ARMED_HOME: + return "ARMED_STAY" + if arm_state == STATE_ALARM_ARMED_AWAY: + return "ARMED_AWAY" + if arm_state == STATE_ALARM_ARMED_NIGHT: + return "ARMED_NIGHT" + if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS: + return "ARMED_STAY" + return "DISARMED" + + def configuration(self): + """Return supported authorization types.""" + code_format = self.entity.attributes.get(ATTR_CODE_FORMAT) + + if code_format == FORMAT_NUMBER: + return {"supportedAuthorizationTypes": [{"type": "FOUR_DIGIT_PIN"}]} + return [] diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 63231f714477c0..0f07e525fa9316 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -14,6 +14,7 @@ from homeassistant.util.decorator import Registry from homeassistant.components.climate import const as climate from homeassistant.components import ( + alarm_control_panel, alert, automation, binary_sensor, @@ -45,6 +46,7 @@ AlexaPowerController, AlexaPowerLevelController, AlexaSceneController, + AlexaSecurityPanelController, AlexaSpeaker, AlexaStepSpeaker, AlexaTemperatureSensor, @@ -487,3 +489,18 @@ def get_type(self): return self.TYPE_CONTACT if attrs.get(ATTR_DEVICE_CLASS) == "motion": return self.TYPE_MOTION + + +@ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN) +class AlarmControlPanelCapabilities(AlexaEntity): + """Class to represent Alarm capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.SECURITY_PANEL] + + def interfaces(self): + """Yield the supported interfaces.""" + if not self.entity.attributes.get("code_arm_required"): + yield AlexaSecurityPanelController(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 8c2fa692267fb0..8e32ed9c7ee715 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -83,3 +83,17 @@ class AlexaBridgeUnreachableError(AlexaError): namespace = "Alexa" error_type = "BRIDGE_UNREACHABLE" + + +class AlexaSecurityPanelUnauthorizedError(AlexaError): + """Class to represent SecurityPanelController Unauthorized errors.""" + + namespace = "Alexa.SecurityPanelController" + error_type = "UNAUTHORIZED" + + +class AlexaSecurityPanelAuthorizationRequired(AlexaError): + """Class to represent SecurityPanelController AuthorizationRequired errors.""" + + namespace = "Alexa.SecurityPanelController" + error_type = "AUTHORIZATION_REQUIRED" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 3cb61675f92ecd..bd07b71ca29aa7 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -9,6 +9,11 @@ ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, + STATE_ALARM_DISARMED, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, @@ -35,6 +40,8 @@ from .entities import async_get_entities from .errors import ( AlexaInvalidValueError, + AlexaSecurityPanelAuthorizationRequired, + AlexaSecurityPanelUnauthorizedError, AlexaTempRangeError, AlexaUnsupportedThermostatModeError, ) @@ -849,3 +856,71 @@ async def async_api_adjust_power_level(hass, config, directive, context): ) return directive.response() + + +@HANDLERS.register(("Alexa.SecurityPanelController", "Arm")) +async def async_api_arm(hass, config, directive, context): + """Process a Security Panel Arm request.""" + entity = directive.entity + service = None + arm_state = directive.payload["armState"] + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.state != STATE_ALARM_DISARMED: + msg = "You must disarm the system before you can set the requested arm state." + raise AlexaSecurityPanelAuthorizationRequired(msg) + + if arm_state == "ARMED_AWAY": + service = SERVICE_ALARM_ARM_AWAY + if arm_state == "ARMED_STAY": + service = SERVICE_ALARM_ARM_HOME + if arm_state == "ARMED_NIGHT": + service = SERVICE_ALARM_ARM_NIGHT + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + response = directive.response( + name="Arm.Response", namespace="Alexa.SecurityPanelController" + ) + + response.add_context_property( + { + "name": "armState", + "namespace": "Alexa.SecurityPanelController", + "value": arm_state, + } + ) + + return response + + +@HANDLERS.register(("Alexa.SecurityPanelController", "Disarm")) +async def async_api_disarm(hass, config, directive, context): + """Process a Security Panel Disarm request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + payload = directive.payload + if "authorization" in payload: + value = payload["authorization"]["value"] + if payload["authorization"]["type"] == "FOUR_DIGIT_PIN": + data["code"] = value + + if not await hass.services.async_call( + entity.domain, SERVICE_ALARM_DISARM, data, blocking=True, context=context + ): + msg = "Invalid Code" + raise AlexaSecurityPanelUnauthorizedError(msg) + + response = directive.response() + response.add_context_property( + { + "name": "armState", + "namespace": "Alexa.SecurityPanelController", + "value": "DISARMED", + } + ) + + return response diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index 9db7e270e6137b..ad0f1c33d492d6 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -4,5 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/alexa", "requirements": [], "dependencies": ["http"], - "codeowners": ["@home-assistant/cloud"] + "codeowners": [ + "@home-assistant/cloud", + "@ochlocracy" + ] } diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index d53f145e6ff7f6..280a76dc3f039f 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -8,6 +8,11 @@ STATE_UNLOCKED, STATE_UNKNOWN, STATE_UNAVAILABLE, + STATE_ALARM_DISARMED, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, ) from homeassistant.components.climate import const as climate from homeassistant.components.alexa import smart_home @@ -527,3 +532,33 @@ async def test_temperature_sensor_climate(hass): properties.assert_equal( "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} ) + + +async def test_report_alarm_control_panel_state(hass): + """Test SecurityPanelController implements armState property.""" + hass.states.async_set("alarm_control_panel.armed_away", STATE_ALARM_ARMED_AWAY, {}) + hass.states.async_set( + "alarm_control_panel.armed_custom_bypass", STATE_ALARM_ARMED_CUSTOM_BYPASS, {} + ) + hass.states.async_set("alarm_control_panel.armed_home", STATE_ALARM_ARMED_HOME, {}) + hass.states.async_set( + "alarm_control_panel.armed_night", STATE_ALARM_ARMED_NIGHT, {} + ) + hass.states.async_set("alarm_control_panel.disarmed", STATE_ALARM_DISARMED, {}) + + properties = await reported_properties(hass, "alarm_control_panel.armed_away") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY") + + properties = await reported_properties( + hass, "alarm_control_panel.armed_custom_bypass" + ) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY") + + properties = await reported_properties(hass, "alarm_control_panel.armed_home") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY") + + properties = await reported_properties(hass, "alarm_control_panel.armed_night") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_NIGHT") + + properties = await reported_properties(hass, "alarm_control_panel.disarmed") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 78ce2963eaf120..78bdd8e09082a2 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1306,3 +1306,127 @@ async def test_endpoint_bad_health(hass): properties.assert_equal( "Alexa.EndpointHealth", "connectivity", {"value": "UNREACHABLE"} ) + + +async def test_alarm_control_panel_disarmed(hass): + """Test alarm_control_panel discovery.""" + device = ( + "alarm_control_panel.test_1", + "disarmed", + { + "friendly_name": "Test Alarm Control Panel 1", + "code_arm_required": False, + "code_format": "number", + "code": "1234", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "alarm_control_panel#test_1" + assert appliance["displayCategories"][0] == "SECURITY_PANEL" + assert appliance["friendlyName"] == "Test Alarm Control Panel 1" + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + ) + security_panel_capability = get_capability( + capabilities, "Alexa.SecurityPanelController" + ) + assert security_panel_capability is not None + configuration = security_panel_capability["configuration"] + assert {"type": "FOUR_DIGIT_PIN"} in configuration["supportedAuthorizationTypes"] + + properties = await reported_properties(hass, "alarm_control_panel#test_1") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_1", + "alarm_control_panel.alarm_arm_home", + hass, + response_type="Arm.Response", + payload={"armState": "ARMED_STAY"}, + ) + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_1", + "alarm_control_panel.alarm_arm_away", + hass, + response_type="Arm.Response", + payload={"armState": "ARMED_AWAY"}, + ) + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_1", + "alarm_control_panel.alarm_arm_night", + hass, + response_type="Arm.Response", + payload={"armState": "ARMED_NIGHT"}, + ) + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_NIGHT") + + +async def test_alarm_control_panel_armed(hass): + """Test alarm_control_panel discovery.""" + device = ( + "alarm_control_panel.test_2", + "armed_away", + { + "friendly_name": "Test Alarm Control Panel 2", + "code_arm_required": False, + "code_format": "FORMAT_NUMBER", + "code": "1234", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "alarm_control_panel#test_2" + assert appliance["displayCategories"][0] == "SECURITY_PANEL" + assert appliance["friendlyName"] == "Test Alarm Control Panel 2" + assert_endpoint_capabilities( + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + ) + + properties = await reported_properties(hass, "alarm_control_panel#test_2") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Disarm", + "alarm_control_panel#test_2", + "alarm_control_panel.alarm_disarm", + hass, + payload={"authorization": {"type": "FOUR_DIGIT_PIN", "value": "1234"}}, + ) + assert call.data["code"] == "1234" + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") + + msg = await assert_request_fails( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_2", + "alarm_control_panel.alarm_arm_home", + hass, + payload={"armState": "ARMED_STAY"}, + ) + assert msg["event"]["payload"]["type"] == "AUTHORIZATION_REQUIRED" + + +async def test_alarm_control_panel_code_arm_required(hass): + """Test alarm_control_panel with code_arm_required discovery.""" + device = ( + "alarm_control_panel.test_3", + "disarmed", + {"friendly_name": "Test Alarm Control Panel 3", "code_arm_required": True}, + ) + await discovery_test(device, hass, expected_endpoints=0) From 3547b8691e019bc86e2ed61afd01e389c964ddc6 Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 4 Oct 2019 17:46:23 +0200 Subject: [PATCH 0591/3953] Add examples to lights service (#27192) --- homeassistant/components/light/services.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 97186f56a8f51d..9173f79f9647c7 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -40,12 +40,14 @@ turn_on: description: Name of a light profile to use. example: relax flash: - description: If the light should flash. + description: If the light should flash. Valid values are short and long. + example: short values: - short - long effect: description: Light effect. + example: random values: - colorloop - random @@ -60,7 +62,8 @@ turn_off: description: Duration in seconds it takes to get to next state. example: 60 flash: - description: If the light should flash. + description: If the light should flash. Valid values are short and long. + example: short values: - short - long @@ -105,12 +108,14 @@ toggle: description: Name of a light profile to use. example: relax flash: - description: If the light should flash. + description: If the light should flash. Valid values are short and long. + example: short values: - short - long effect: description: Light effect. + example: random values: - colorloop - random From 45d4586bc2a8b77a1efee7bb187c63f84f0cc2d8 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 4 Oct 2019 16:54:16 +0100 Subject: [PATCH 0592/3953] Improve evohome debug logging (#27178) * add debug logging for schedule updates * add debug logging for schedules * change back to debug from warn --- homeassistant/components/evohome/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 14bf12239538b2..e9254c373d94ab 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -454,6 +454,8 @@ async def _update_schedule(self) -> None: self._evo_device.schedule(), refresh=False ) + _LOGGER.debug("Schedule['%s'] = %s", self.name, self._schedule) + async def async_update(self) -> None: """Get the latest state data.""" next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00") From b8f41dbb758aceaa1a4ab65d21ca4bd7544ff478 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Oct 2019 19:11:14 +0200 Subject: [PATCH 0593/3953] Add device condition support to sensor entities (#27163) * Add device condition support to sensor entities * Fix typing --- .../components/sensor/device_condition.py | 143 +++++++++ .../components/sensor/device_trigger.py | 3 +- .../sensor/test_device_condition.py | 289 ++++++++++++++++++ 3 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/sensor/device_condition.py create mode 100644 tests/components/sensor/test_device_condition.py diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py new file mode 100644 index 00000000000000..76f1b3909ef620 --- /dev/null +++ b/homeassistant/components/sensor/device_condition.py @@ -0,0 +1,143 @@ +"""Provides device conditions for sensors.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +import homeassistant.components.automation.numeric_state as numeric_state_automation +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + CONF_ABOVE, + CONF_BELOW, + CONF_ENTITY_ID, + CONF_FOR, + CONF_TYPE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, +) +from homeassistant.helpers.entity_registry import ( + async_entries_for_device, + async_get_registry, +) +from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_IS_BATTERY_LEVEL = "is_battery_level" +CONF_IS_HUMIDITY = "is_humidity" +CONF_IS_ILLUMINANCE = "is_illuminance" +CONF_IS_POWER = "is_power" +CONF_IS_PRESSURE = "is_pressure" +CONF_IS_SIGNAL_STRENGTH = "is_signal_strength" +CONF_IS_TEMPERATURE = "is_temperature" +CONF_IS_TIMESTAMP = "is_timestamp" +CONF_IS_VALUE = "is_value" + +ENTITY_CONDITIONS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], + DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], + DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_IS_POWER}], + DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}], + DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}], + DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}], + DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_IS_TIMESTAMP}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}], +} + +CONDITION_SCHEMA = vol.All( + cv.DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In( + [ + CONF_IS_BATTERY_LEVEL, + CONF_IS_HUMIDITY, + CONF_IS_ILLUMINANCE, + CONF_IS_POWER, + CONF_IS_PRESSURE, + CONF_IS_SIGNAL_STRENGTH, + CONF_IS_TEMPERATURE, + CONF_IS_TIMESTAMP, + CONF_IS_VALUE, + ] + ), + vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + conditions: List[dict] = [] + entity_registry = await async_get_registry(hass) + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + continue + + if ATTR_DEVICE_CLASS in state.attributes: + device_class = state.attributes[ATTR_DEVICE_CLASS] + + templates = ENTITY_CONDITIONS.get( + device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE] + ) + + conditions.extend( + ( + { + **template, + "condition": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for template in templates + ) + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Evaluate state based on configuration.""" + if config_validation: + config = CONDITION_SCHEMA(config) + numeric_state_config = { + numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE), + numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW), + numeric_state_automation.CONF_FOR: config.get(CONF_FOR), + } + + return condition.async_numeric_state_from_config( + numeric_state_config, config_validation + ) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 377b202db187c4..0d9e8f5af80838 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -121,7 +121,8 @@ async def async_get_triggers(hass, device_id): if not state or not unit_of_measurement: continue - device_class = state.attributes.get(ATTR_DEVICE_CLASS) + if ATTR_DEVICE_CLASS in state.attributes: + device_class = state.attributes[ATTR_DEVICE_CLASS] templates = ENTITY_TRIGGERS.get( device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py new file mode 100644 index 00000000000000..e28e487f4ef2bd --- /dev/null +++ b/tests/components/sensor/test_device_condition.py @@ -0,0 +1,289 @@ +"""The test for sensor device automation.""" +import pytest + +from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS +from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) +from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": condition["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for condition in ENTITY_CONDITIONS[device_class] + if device_class != "none" + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state_not_above_below(hass, calls, caplog): + """Test for bad value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + } + ], + "action": {"service": "test.automation"}, + } + ] + }, + ) + assert "must contain at least one of below, above" in caplog.text + + +async def test_if_state_above(hass, calls): + """Test for value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + "above": 10, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "event - test_event1" + + +async def test_if_state_below(hass, calls): + """Test for value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + "below": 10, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "event - test_event1" + + +async def test_if_state_between(hass, calls): + """Test for value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + "above": 10, + "below": 20, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "event - test_event1" + + hass.states.async_set(sensor1.entity_id, 21) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_set(sensor1.entity_id, 19) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "event - test_event1" From e27051aa61159940f3969d52cb75f12951c57ff4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Oct 2019 19:17:57 +0200 Subject: [PATCH 0594/3953] Fix validation when automation is saved from frontend (#27195) --- homeassistant/components/automation/config.py | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 581ce6b461d844..ebbd1771e84efd 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -18,33 +18,40 @@ async def async_validate_config_item(hass, config, full_config=None): """Validate config item.""" - try: - config = PLATFORM_SCHEMA(config) + config = PLATFORM_SCHEMA(config) - triggers = [] - for trigger in config[CONF_TRIGGER]: - trigger_platform = importlib.import_module( - "..{}".format(trigger[CONF_PLATFORM]), __name__ + triggers = [] + for trigger in config[CONF_TRIGGER]: + trigger_platform = importlib.import_module( + "..{}".format(trigger[CONF_PLATFORM]), __name__ + ) + if hasattr(trigger_platform, "async_validate_trigger_config"): + trigger = await trigger_platform.async_validate_trigger_config( + hass, trigger ) - if hasattr(trigger_platform, "async_validate_trigger_config"): - trigger = await trigger_platform.async_validate_trigger_config( - hass, trigger - ) - triggers.append(trigger) - config[CONF_TRIGGER] = triggers - - if CONF_CONDITION in config: - conditions = [] - for cond in config[CONF_CONDITION]: - cond = await condition.async_validate_condition_config(hass, cond) - conditions.append(cond) - config[CONF_CONDITION] = conditions - - actions = [] - for action in config[CONF_ACTION]: - action = await script.async_validate_action_config(hass, action) - actions.append(action) - config[CONF_ACTION] = actions + triggers.append(trigger) + config[CONF_TRIGGER] = triggers + + if CONF_CONDITION in config: + conditions = [] + for cond in config[CONF_CONDITION]: + cond = await condition.async_validate_condition_config(hass, cond) + conditions.append(cond) + config[CONF_CONDITION] = conditions + + actions = [] + for action in config[CONF_ACTION]: + action = await script.async_validate_action_config(hass, action) + actions.append(action) + config[CONF_ACTION] = actions + + return config + + +async def _try_async_validate_config_item(hass, config, full_config=None): + """Validate config item.""" + try: + config = await async_validate_config_item(hass, config, full_config) except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: async_log_exception(ex, DOMAIN, full_config or config, hass) return None @@ -57,7 +64,7 @@ async def async_validate_config(hass, config): automations = [] validated_automations = await asyncio.gather( *( - async_validate_config_item(hass, p_config, config) + _try_async_validate_config_item(hass, p_config, config) for _, p_config in config_per_platform(config, DOMAIN) ) ) From e5a2e188817532537af15e46ff8a7ccd0f80d0b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mayoral=20Mart=C3=ADnez?= Date: Fri, 4 Oct 2019 21:07:19 +0200 Subject: [PATCH 0595/3953] Fix template fan turn_on action (#27181) * Fix template fan turn_on action The turn_on action of a template fan should receive the 'speed' attribute in order to give the user the possibility of define the behaviour of this action as he desires Fixes #27176 * Format * Update fan.py --- homeassistant/components/template/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 42790e618d96ec..606f18e5fe1923 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -270,7 +270,7 @@ def available(self): # pylint: disable=arguments-differ async def async_turn_on(self, speed: str = None) -> None: """Turn on the fan.""" - await self._on_script.async_run(context=self._context) + await self._on_script.async_run({ATTR_SPEED: speed}, context=self._context) self._state = STATE_ON if speed is not None: From 23686710b1c021198848c79e0c2c8798c4d29079 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Oct 2019 13:49:51 -0700 Subject: [PATCH 0596/3953] Fix tests running in hass.io image (#27169) * Fix tests running in hass.io image * Real fix now * Only remove wheel links --- tests/test_requirements.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index b5574fe96fd8c3..780b175778edb8 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -17,6 +17,13 @@ from tests.common import get_test_home_assistant, MockModule, mock_integration +def env_without_wheel_links(): + """Return env without wheel links.""" + env = dict(os.environ) + env.pop("WHEEL_LINKS", None) + return env + + class TestRequirements: """Test the requirements module.""" @@ -36,6 +43,7 @@ def teardown_method(self, method): @patch("homeassistant.util.package.is_virtual_env", return_value=True) @patch("homeassistant.util.package.is_docker_env", return_value=False) @patch("homeassistant.util.package.install_package", return_value=True) + @patch.dict(os.environ, env_without_wheel_links(), clear=True) def test_requirement_installed_in_venv( self, mock_install, mock_denv, mock_venv, mock_dirname ): @@ -55,6 +63,7 @@ def test_requirement_installed_in_venv( @patch("homeassistant.util.package.is_virtual_env", return_value=False) @patch("homeassistant.util.package.is_docker_env", return_value=False) @patch("homeassistant.util.package.install_package", return_value=True) + @patch.dict(os.environ, env_without_wheel_links(), clear=True) def test_requirement_installed_in_deps( self, mock_install, mock_denv, mock_venv, mock_dirname ): @@ -136,7 +145,7 @@ async def test_install_with_wheels_index(hass): mock_dir.return_value = "ha_package_path" assert await setup.async_setup_component(hass, "comp", {}) assert "comp" in hass.config.components - print(mock_inst.call_args) + assert mock_inst.call_args == call( "hello==1.0.0", find_links="https://wheels.hass.io/test", @@ -154,11 +163,13 @@ async def test_install_on_docker(hass): "homeassistant.util.package.is_docker_env", return_value=True ), patch("homeassistant.util.package.install_package") as mock_inst, patch( "os.path.dirname" - ) as mock_dir: + ) as mock_dir, patch.dict( + os.environ, env_without_wheel_links(), clear=True + ): mock_dir.return_value = "ha_package_path" assert await setup.async_setup_component(hass, "comp", {}) assert "comp" in hass.config.components - print(mock_inst.call_args) + assert mock_inst.call_args == call( "hello==1.0.0", constraints=os.path.join("ha_package_path", CONSTRAINT_FILE), From de246fa7d88d8c163bdcacccc7f6dbee9bd2893d Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 4 Oct 2019 23:14:47 +0200 Subject: [PATCH 0597/3953] lock open service data (#27204) --- homeassistant/components/lock/services.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index 0b4688c02a2090..d17e00addd19fd 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -47,6 +47,16 @@ lock: description: An optional code to lock the lock with. example: 1234 +open: + description: Open all or specified locks. + fields: + entity_id: + description: Name of lock to open. + example: 'lock.front_door' + code: + description: An optional code to open the lock with. + example: 1234 + set_usercode: description: Set a usercode to lock. fields: From c62d1a77ecff5c809a72a44eab09d02c2e26b95c Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Fri, 4 Oct 2019 17:15:43 -0400 Subject: [PATCH 0598/3953] Fix ecobee binary sensor and sensor unique ids (#27208) * Fix sensor unique id * Fix binary sensor unique id --- homeassistant/components/ecobee/binary_sensor.py | 3 ++- homeassistant/components/ecobee/sensor.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 97ba94aaa7004f..3367f33a66ffa7 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -50,7 +50,8 @@ def unique_id(self): if sensor["name"] == self.sensor_name: if "code" in sensor: return f"{sensor['code']}-{self.device_class}" - return f"{sensor['id']}-{self.device_class}" + thermostat = self.data.ecobee.get_thermostat(self.index) + return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property def device_info(self): diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 27a8ae3ef07ec6..1c47bc9b26db9c 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -61,7 +61,8 @@ def unique_id(self): if sensor["name"] == self.sensor_name: if "code" in sensor: return f"{sensor['code']}-{self.device_class}" - return f"{sensor['id']}-{self.device_class}" + thermostat = self.data.ecobee.get_thermostat(self.index) + return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property def device_info(self): From 0be1269b2007c2cdf98c79a1007b3e9f7de01435 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 4 Oct 2019 23:21:19 +0200 Subject: [PATCH 0599/3953] Add acceleration sensor to Homematic IP Cloud (#27199) --- .../homematicip_cloud/binary_sensor.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 4ac4614379b905..b3a21ba0b5c5f3 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 ( + AsyncAccelerationSensor, AsyncContactInterface, AsyncDevice, AsyncFullFlushContactInterface, @@ -28,6 +29,7 @@ DEVICE_CLASS_LIGHT, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, DEVICE_CLASS_OPENING, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_SAFETY, @@ -42,6 +44,10 @@ _LOGGER = logging.getLogger(__name__) +ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode" +ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position" +ATTR_ACCELERATION_SENSOR_SENSITIVITY = "acceleration_sensor_sensitivity" +ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE = "acceleration_sensor_trigger_angle" ATTR_LOW_BATTERY = "low_battery" ATTR_MOISTURE_DETECTED = "moisture_detected" ATTR_MOTION_DETECTED = "motion_detected" @@ -63,6 +69,13 @@ "waterlevelDetected": ATTR_WATER_LEVEL_DETECTED, } +SAM_DEVICE_ATTRIBUTES = { + "accelerationSensorNeutralPosition": ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION, + "accelerationSensorMode": ATTR_ACCELERATION_SENSOR_MODE, + "accelerationSensorSensitivity": ATTR_ACCELERATION_SENSOR_SENSITIVITY, + "accelerationSensorTriggerAngle": ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE, +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud binary sensor devices.""" @@ -76,6 +89,8 @@ 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, AsyncAccelerationSensor): + devices.append(HomematicipAccelerationSensor(home, device)) if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): devices.append(HomematicipContactInterface(home, device)) if isinstance( @@ -118,6 +133,32 @@ async def async_setup_entry( async_add_entities(devices) +class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorDevice): + """Representation of a HomematicIP Cloud acceleration sensor.""" + + @property + def device_class(self) -> str: + """Return the class of this sensor.""" + return DEVICE_CLASS_MOVING + + @property + def is_on(self) -> bool: + """Return true if acceleration is detected.""" + return self._device.accelerationSensorTriggered + + @property + def device_state_attributes(self): + """Return the state attributes of the acceleration sensor.""" + state_attr = super().device_state_attributes + + for attr, attr_key in SAM_DEVICE_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value + + return state_attr + + class HomematicipContactInterface(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud contact interface.""" From cc4926afb1ff26e1fd65c366461be49e70d1f4bb Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 4 Oct 2019 23:29:53 +0200 Subject: [PATCH 0600/3953] lock_reproduce_state (#27203) --- .../components/lock/reproduce_state.py | 61 +++++++++++++++++++ tests/components/lock/test_reproduce_state.py | 53 ++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 homeassistant/components/lock/reproduce_state.py create mode 100644 tests/components/lock/test_reproduce_state.py diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py new file mode 100644 index 00000000000000..dc1bee85839dab --- /dev/null +++ b/homeassistant/components/lock/reproduce_state.py @@ -0,0 +1,61 @@ +"""Reproduce an Lock state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_LOCKED, + STATE_UNLOCKED, + SERVICE_LOCK, + SERVICE_UNLOCK, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_LOCKED, STATE_UNLOCKED} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_LOCKED: + service = SERVICE_LOCK + elif state.state == STATE_UNLOCKED: + service = SERVICE_UNLOCK + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Lock states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/lock/test_reproduce_state.py b/tests/components/lock/test_reproduce_state.py new file mode 100644 index 00000000000000..a9b61fa121906c --- /dev/null +++ b/tests/components/lock/test_reproduce_state.py @@ -0,0 +1,53 @@ +"""Test reproduce state for Lock.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Lock states.""" + hass.states.async_set("lock.entity_locked", "locked", {}) + hass.states.async_set("lock.entity_unlocked", "unlocked", {}) + + lock_calls = async_mock_service(hass, "lock", "lock") + unlock_calls = async_mock_service(hass, "lock", "unlock") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("lock.entity_locked", "locked"), + State("lock.entity_unlocked", "unlocked", {}), + ], + blocking=True, + ) + + assert len(lock_calls) == 0 + assert len(unlock_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("lock.entity_locked", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(lock_calls) == 0 + assert len(unlock_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("lock.entity_locked", "unlocked"), + State("lock.entity_unlocked", "locked"), + # Should not raise + State("lock.non_existing", "on"), + ], + blocking=True, + ) + + assert len(lock_calls) == 1 + assert lock_calls[0].domain == "lock" + assert lock_calls[0].data == {"entity_id": "lock.entity_unlocked"} + + assert len(unlock_calls) == 1 + assert unlock_calls[0].domain == "lock" + assert unlock_calls[0].data == {"entity_id": "lock.entity_locked"} From 9c96ec858a1506a60bcc7ee4031ba030efea530a Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 4 Oct 2019 23:32:10 +0200 Subject: [PATCH 0601/3953] switch reproduce state (#27202) --- .../components/switch/reproduce_state.py | 61 +++++++++++++++++++ .../components/switch/test_reproduce_state.py | 50 +++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 homeassistant/components/switch/reproduce_state.py create mode 100644 tests/components/switch/test_reproduce_state.py diff --git a/homeassistant/components/switch/reproduce_state.py b/homeassistant/components/switch/reproduce_state.py new file mode 100644 index 00000000000000..7ed1f70cb97804 --- /dev/null +++ b/homeassistant/components/switch/reproduce_state.py @@ -0,0 +1,61 @@ +"""Reproduce an Switch state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Switch states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/switch/test_reproduce_state.py b/tests/components/switch/test_reproduce_state.py new file mode 100644 index 00000000000000..4b6db84bfdd825 --- /dev/null +++ b/tests/components/switch/test_reproduce_state.py @@ -0,0 +1,50 @@ +"""Test reproduce state for Switch.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Switch states.""" + hass.states.async_set("switch.entity_off", "off", {}) + hass.states.async_set("switch.entity_on", "on", {}) + + turn_on_calls = async_mock_service(hass, "switch", "turn_on") + turn_off_calls = async_mock_service(hass, "switch", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [State("switch.entity_off", "off"), State("switch.entity_on", "on", {})], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("switch.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("switch.entity_on", "off"), + State("switch.entity_off", "on", {}), + # Should not raise + State("switch.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "switch" + assert turn_on_calls[0].data == {"entity_id": "switch.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "switch" + assert turn_off_calls[0].data == {"entity_id": "switch.entity_on"} From 7e7868f0a6d6baa710e93132861f03c59f85aaf8 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 5 Oct 2019 00:32:19 +0000 Subject: [PATCH 0602/3953] [ci skip] Translation update --- .../components/airly/.translations/da.json | 22 ++++++++++++++++ .../components/airly/.translations/en.json | 22 ++++++++++++++++ .../components/airly/.translations/lb.json | 20 ++++++++++++++ .../components/airly/.translations/no.json | 22 ++++++++++++++++ .../components/airly/.translations/pl.json | 22 ++++++++++++++++ .../components/deconz/.translations/lb.json | 1 + .../components/plex/.translations/lb.json | 1 + .../components/sensor/.translations/lb.json | 26 +++++++++++++++++++ .../components/soma/.translations/lb.json | 13 ++++++++++ 9 files changed, 149 insertions(+) create mode 100644 homeassistant/components/airly/.translations/da.json create mode 100644 homeassistant/components/airly/.translations/en.json create mode 100644 homeassistant/components/airly/.translations/lb.json create mode 100644 homeassistant/components/airly/.translations/no.json create mode 100644 homeassistant/components/airly/.translations/pl.json create mode 100644 homeassistant/components/sensor/.translations/lb.json create mode 100644 homeassistant/components/soma/.translations/lb.json diff --git a/homeassistant/components/airly/.translations/da.json b/homeassistant/components/airly/.translations/da.json new file mode 100644 index 00000000000000..652cc46a7b3e0a --- /dev/null +++ b/homeassistant/components/airly/.translations/da.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-n\u00f8glen er ikke korrekt.", + "name_exists": "Navnet findes allerede.", + "wrong_location": "Ingen Airly m\u00e5lestationer i dette omr\u00e5de." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-n\u00f8gle", + "latitude": "Breddegrad", + "longitude": "L\u00e6ngdegrad", + "name": "Integrationens navn" + }, + "description": "Konfigurer Airly luftkvalitet integration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/en.json b/homeassistant/components/airly/.translations/en.json new file mode 100644 index 00000000000000..83284aaeb7b6e0 --- /dev/null +++ b/homeassistant/components/airly/.translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API key is not correct.", + "name_exists": "Name already exists.", + "wrong_location": "No Airly measuring stations in this area." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name of the integration" + }, + "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/lb.json b/homeassistant/components/airly/.translations/lb.json new file mode 100644 index 00000000000000..ca71c2e647f2a9 --- /dev/null +++ b/homeassistant/components/airly/.translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "auth": "Api Schl\u00ebssel ass net korrekt.", + "name_exists": "Numm g\u00ebtt et schonn" + }, + "step": { + "user": { + "data": { + "api_key": "Airly API Schl\u00ebssel", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm vun der Installatioun" + }, + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/no.json b/homeassistant/components/airly/.translations/no.json new file mode 100644 index 00000000000000..70924bb7bf4b9e --- /dev/null +++ b/homeassistant/components/airly/.translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-n\u00f8kkelen er ikke korrekt.", + "name_exists": "Navnet finnes allerede.", + "wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn p\u00e5 integrasjonen" + }, + "description": "Sett opp Airly luftkvalitet integrering. For \u00e5 generere API-n\u00f8kkel g\u00e5 til https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/pl.json b/homeassistant/components/airly/.translations/pl.json new file mode 100644 index 00000000000000..5d601b37591b68 --- /dev/null +++ b/homeassistant/components/airly/.translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Klucz API jest nieprawid\u0142owy.", + "name_exists": "Nazwa ju\u017c istnieje.", + "wrong_location": "Brak stacji pomiarowych Airly w tym rejonie." + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API Airly", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa integracji" + }, + "description": "Konfiguracja integracji Airly. By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 840bc8929a7366..f5f41a28a32c95 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt", "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt", "remote_button_rotated": "Kn\u00e4ppche gedr\u00e9int \"{subtype}\"", + "remote_button_rotation_stopped": "Kn\u00e4ppchen Rotatioun \"{subtype}\" gestoppt", "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 1e6488784d409a..1795ef6b6d358a 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -5,6 +5,7 @@ "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert", "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", + "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", "unknown": "Onbekannte Feeler opgetrueden" }, "error": { diff --git a/homeassistant/components/sensor/.translations/lb.json b/homeassistant/components/sensor/.translations/lb.json new file mode 100644 index 00000000000000..01a4e89c9f4623 --- /dev/null +++ b/homeassistant/components/sensor/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} Batterie niveau", + "is_humidity": "{entity_name} Fiichtegkeet", + "is_illuminance": "{entity_name} Beliichtung", + "is_power": "{entity_name} Leeschtung", + "is_pressure": "{entity_name} Drock", + "is_signal_strength": "{entity_name} Signal St\u00e4erkt", + "is_temperature": "{entity_name} Temperatur", + "is_timestamp": "{entity_name} Z\u00e4itstempel", + "is_value": "{entity_name} W\u00e4ert" + }, + "trigger_type": { + "battery_level": "{entity_name} Batterie niveau", + "humidity": "{entity_name} Fiichtegkeet", + "illuminance": "{entity_name} Beliichtung", + "power": "{entity_name} Leeschtung", + "pressure": "{entity_name} Drock", + "signal_strength": "{entity_name} Signal St\u00e4erkt", + "temperature": "{entity_name} Temperatur", + "timestamp": "{entity_name} Z\u00e4itstempel", + "value": "{entity_name} W\u00e4ert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json new file mode 100644 index 00000000000000..d8aba082537bf7 --- /dev/null +++ b/homeassistant/components/soma/.translations/lb.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Soma Kont konfigur\u00e9ieren.", + "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", + "missing_configuration": "D'Soma Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert." + }, + "title": "Soma" + } +} \ No newline at end of file From 2e49303401f18bd3b99e3ab68be7b6122e607b7e Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Fri, 4 Oct 2019 20:35:31 -0400 Subject: [PATCH 0603/3953] Add turn_on method to ecobee climate platform (#27103) * Add turn on method to ecobee climate * Update climate.py * Update value in async_update * Fix update in async_update * Simplify async_update * Fix lint complaining about log string * Cleanup inline if statement --- homeassistant/components/ecobee/climate.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 491fd8d686a2b7..e29e23810089d4 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -264,6 +264,7 @@ def __init__(self, data, thermostat_index): self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) self._name = self.thermostat["name"] self.vacation = None + self._last_active_hvac_mode = HVAC_MODE_AUTO self._operation_list = [] if self.thermostat["settings"]["heatStages"]: @@ -289,6 +290,8 @@ async def async_update(self): else: await self.data.update() self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + if self.hvac_mode is not HVAC_MODE_OFF: + self._last_active_hvac_mode = self.hvac_mode @property def available(self): @@ -700,3 +703,12 @@ def delete_vacation(self, vacation_name): vacation_name, ) self.data.ecobee.delete_vacation(self.thermostat_index, vacation_name) + + def turn_on(self): + """Set the thermostat to the last active HVAC mode.""" + _LOGGER.debug( + "Turning on ecobee thermostat %s in %s mode", + self.name, + self._last_active_hvac_mode, + ) + self.set_hvac_mode(self._last_active_hvac_mode) From 6ae908b883cb76f93a9d6303e8bab3d7a6619689 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 5 Oct 2019 02:38:26 +0200 Subject: [PATCH 0604/3953] Add opentherm_gw config flow (#27148) * Add config flow support to opentherm_gw. Bump pyotgw to 0.5b0 (required for connection testing) Existing entries in configuration.yaml will be converted to config entries and ignored in future runs. * Fix not connecting to Gateway on startup. Pylint fixes. * Add tests for config flow. Remove non-essential options from config flow. Restructure config entry data. * Make sure gw_id is slugified --- .coveragerc | 5 +- .../opentherm_gw/.translations/en.json | 23 +++ .../opentherm_gw/.translations/nl.json | 23 +++ .../components/opentherm_gw/__init__.py | 66 ++++--- .../components/opentherm_gw/binary_sensor.py | 14 +- .../components/opentherm_gw/climate.py | 19 +- .../components/opentherm_gw/config_flow.py | 91 ++++++++++ .../components/opentherm_gw/const.py | 2 + .../components/opentherm_gw/manifest.json | 7 +- .../components/opentherm_gw/sensor.py | 15 +- .../components/opentherm_gw/strings.json | 23 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/opentherm_gw/__init__.py | 1 + .../opentherm_gw/test_config_flow.py | 163 ++++++++++++++++++ 17 files changed, 411 insertions(+), 48 deletions(-) create mode 100644 homeassistant/components/opentherm_gw/.translations/en.json create mode 100644 homeassistant/components/opentherm_gw/.translations/nl.json create mode 100644 homeassistant/components/opentherm_gw/config_flow.py create mode 100644 homeassistant/components/opentherm_gw/strings.json create mode 100644 tests/components/opentherm_gw/__init__.py create mode 100644 tests/components/opentherm_gw/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 25ee023ac84cd7..a6f65430074190 100644 --- a/.coveragerc +++ b/.coveragerc @@ -464,7 +464,10 @@ omit = homeassistant/components/openhome/media_player.py homeassistant/components/opensensemap/air_quality.py homeassistant/components/opensky/sensor.py - homeassistant/components/opentherm_gw/* + homeassistant/components/opentherm_gw/__init__.py + homeassistant/components/opentherm_gw/binary_sensor.py + homeassistant/components/opentherm_gw/climate.py + homeassistant/components/opentherm_gw/sensor.py homeassistant/components/openuv/__init__.py homeassistant/components/openuv/binary_sensor.py homeassistant/components/openuv/sensor.py diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json new file mode 100644 index 00000000000000..65d7d9e92bb1bd --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway already configured", + "id_exists": "Gateway id already exists", + "serial_error": "Error connecting to device", + "timeout": "Connection attempt timed out" + }, + "step": { + "init": { + "data": { + "device": "Path or URL", + "floor_temperature": "Floor climate temperature", + "id": "ID", + "name": "Name", + "precision": "Climate temperature precision" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json new file mode 100644 index 00000000000000..ef3daafe4fe6ee --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway is reeds geconfigureerd", + "id_exists": "Gateway id bestaat reeds", + "serial_error": "Kan niet verbinden met de Gateway", + "timeout": "Time-out van de verbinding" + }, + "step": { + "init": { + "data": { + "device": "Pad of URL", + "floor_temperature": "Thermostaat temperaturen naar beneden afronden", + "id": "ID", + "name": "Naam", + "precision": "Thermostaat temperatuur precisie" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index a32c375ac65258..ba6de4c0bea6ee 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -6,6 +6,7 @@ import pyotgw.vars as gw_vars import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR from homeassistant.components.climate import DOMAIN as COMP_CLIMATE from homeassistant.components.sensor import DOMAIN as COMP_SENSOR @@ -16,13 +17,13 @@ ATTR_TEMPERATURE, ATTR_TIME, CONF_DEVICE, + CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_STOP, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, ) -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.config_validation as cv @@ -36,6 +37,7 @@ CONF_PRECISION, DATA_GATEWAYS, DATA_OPENTHERM_GW, + DOMAIN, SERVICE_RESET_GATEWAY, SERVICE_SET_CLOCK, SERVICE_SET_CONTROL_SETPOINT, @@ -50,8 +52,6 @@ _LOGGER = logging.getLogger(__name__) -DOMAIN = "opentherm_gw" - CLIMATE_SCHEMA = vol.Schema( { vol.Optional(CONF_PRECISION): vol.In( @@ -75,28 +75,41 @@ ) -async def async_setup(hass, config): +async def async_setup_entry(hass, config_entry): """Set up the OpenTherm Gateway component.""" - conf = config[DOMAIN] - hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}} - for gw_id, cfg in conf.items(): - gateway = OpenThermGatewayDevice(hass, gw_id, cfg) - hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] = gateway - hass.async_create_task( - async_load_platform(hass, COMP_CLIMATE, DOMAIN, gw_id, config) - ) - hass.async_create_task( - async_load_platform(hass, COMP_BINARY_SENSOR, DOMAIN, gw_id, config) - ) + if DATA_OPENTHERM_GW not in hass.data: + hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}} + + gateway = OpenThermGatewayDevice(hass, config_entry) + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] = gateway + + # Schedule directly on the loop to avoid blocking HA startup. + hass.loop.create_task(gateway.connect_and_subscribe()) + + for comp in [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR]: hass.async_create_task( - async_load_platform(hass, COMP_SENSOR, DOMAIN, gw_id, config) + hass.config_entries.async_forward_entry_setup(config_entry, comp) ) - # Schedule directly on the loop to avoid blocking HA startup. - hass.loop.create_task(gateway.connect_and_subscribe(cfg[CONF_DEVICE])) + register_services(hass) return True +async def async_setup(hass, config): + """Set up the OpenTherm Gateway component.""" + if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: + conf = config[DOMAIN] + for device_id, device_config in conf.items(): + device_config[CONF_ID] = device_id + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=device_config + ) + ) + return True + + def register_services(hass): """Register services for the component.""" service_reset_schema = vol.Schema( @@ -326,20 +339,21 @@ async def set_setback_temp(call): class OpenThermGatewayDevice: """OpenTherm Gateway device class.""" - def __init__(self, hass, gw_id, config): + def __init__(self, hass, config_entry): """Initialize the OpenTherm Gateway.""" self.hass = hass - self.gw_id = gw_id - self.name = config.get(CONF_NAME, gw_id) - self.climate_config = config[CONF_CLIMATE] + self.device_path = config_entry.data[CONF_DEVICE] + self.gw_id = config_entry.data[CONF_ID] + self.name = config_entry.data[CONF_NAME] + self.climate_config = config_entry.options self.status = {} - self.update_signal = f"{DATA_OPENTHERM_GW}_{gw_id}_update" + self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" self.gateway = pyotgw.pyotgw() - async def connect_and_subscribe(self, device_path): + async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" - await self.gateway.connect(self.hass.loop, device_path) - _LOGGER.debug("Connected to OpenTherm Gateway at %s", device_path) + await self.gateway.connect(self.hass.loop, self.device_path) + _LOGGER.debug("Connected to OpenTherm Gateway at %s", self.device_path) async def cleanup(event): """Reset overrides on the gateway.""" diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 614829265e2dcc..36867feda61e52 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice +from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id @@ -12,18 +13,21 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway binary sensors.""" - if discovery_info is None: - return - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] sensors = [] for var, info in BINARY_SENSOR_INFO.items(): device_class = info[0] friendly_name_format = info[1] sensors.append( - OpenThermBinarySensor(gw_dev, var, device_class, friendly_name_format) + OpenThermBinarySensor( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], + var, + device_class, + friendly_name_format, + ) ) + async_add_entities(sensors) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index fab028560bb66e..19763121e89d64 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -17,6 +17,7 @@ ) from homeassistant.const import ( ATTR_TEMPERATURE, + CONF_ID, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, @@ -33,12 +34,16 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the opentherm_gw device.""" - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up an OpenTherm Gateway climate entity.""" + ents = [] + ents.append( + OpenThermClimate( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] + ) + ) - gateway = OpenThermClimate(gw_dev) - async_add_entities([gateway]) + async_add_entities(ents) class OpenThermClimate(ClimateDevice): @@ -48,7 +53,7 @@ def __init__(self, gw_dev): """Initialize the device.""" self._gateway = gw_dev self.friendly_name = gw_dev.name - self.floor_temp = gw_dev.climate_config[CONF_FLOOR_TEMP] + self.floor_temp = gw_dev.climate_config.get(CONF_FLOOR_TEMP) self.temp_precision = gw_dev.climate_config.get(CONF_PRECISION) self._current_operation = None self._current_temperature = None @@ -62,7 +67,7 @@ def __init__(self, gw_dev): async def async_added_to_hass(self): """Connect to the OpenTherm Gateway device.""" - _LOGGER.debug("Added device %s", self.friendly_name) + _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name) async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py new file mode 100644 index 00000000000000..e1b68f1ae4903b --- /dev/null +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -0,0 +1,91 @@ +"""OpenTherm Gateway config flow.""" +import asyncio +from serial import SerialException + +import pyotgw +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME + +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN + + +class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """OpenTherm Gateway Config Flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_init(self, info=None): + """Handle config flow initiation.""" + if info: + name = info[CONF_NAME] + device = info[CONF_DEVICE] + gw_id = cv.slugify(info.get(CONF_ID, name)) + + entries = [e.data for e in self.hass.config_entries.async_entries(DOMAIN)] + + if gw_id in [e[CONF_ID] for e in entries]: + return self._show_form({"base": "id_exists"}) + + if device in [e[CONF_DEVICE] for e in entries]: + return self._show_form({"base": "already_configured"}) + + async def test_connection(): + """Try to connect to the OpenTherm Gateway.""" + otgw = pyotgw.pyotgw() + status = await otgw.connect(self.hass.loop, device) + await otgw.disconnect() + return status.get(pyotgw.OTGW_ABOUT) + + try: + res = await asyncio.wait_for(test_connection(), timeout=10) + except asyncio.TimeoutError: + return self._show_form({"base": "timeout"}) + except SerialException: + return self._show_form({"base": "serial_error"}) + + if res: + return self._create_entry(gw_id, name, device) + + return self._show_form() + + async def async_step_user(self, info=None): + """Handle manual initiation of the config flow.""" + return await self.async_step_init(info) + + async def async_step_import(self, import_config): + """ + Import an OpenTherm Gateway device as a config entry. + + This flow is triggered by `async_setup` for configured devices. + """ + formatted_config = { + CONF_NAME: import_config.get(CONF_NAME, import_config[CONF_ID]), + CONF_DEVICE: import_config[CONF_DEVICE], + CONF_ID: import_config[CONF_ID], + } + return await self.async_step_init(info=formatted_config) + + def _show_form(self, errors=None): + """Show the config flow form with possible errors.""" + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME): str, + vol.Required(CONF_DEVICE): str, + vol.Optional(CONF_ID): str, + } + ), + errors=errors or {}, + ) + + def _create_entry(self, gw_id, name, device): + """Create entry for the OpenTherm Gateway device.""" + return self.async_create_entry( + title=name, data={CONF_ID: gw_id, CONF_DEVICE: device, CONF_NAME: name} + ) diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 60042b92867ca4..bd9b372de33200 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -18,6 +18,8 @@ DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_PROBLEM = "problem" +DOMAIN = "opentherm_gw" + SERVICE_RESET_GATEWAY = "reset_gateway" SERVICE_SET_CLOCK = "set_clock" SERVICE_SET_CONTROL_SETPOINT = "set_control_setpoint" diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 9c7f165c6dfa19..a632096cd75e9a 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -3,10 +3,11 @@ "name": "Opentherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": [ - "pyotgw==0.4b4" + "pyotgw==0.5b0" ], "dependencies": [], "codeowners": [ "@mvn23" - ] -} + ], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 1449caf5defdb9..c77a73cd18032b 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -12,19 +13,23 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway sensors.""" - if discovery_info is None: - return - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] sensors = [] for var, info in SENSOR_INFO.items(): device_class = info[0] unit = info[1] friendly_name_format = info[2] sensors.append( - OpenThermSensor(gw_dev, var, device_class, unit, friendly_name_format) + OpenThermSensor( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], + var, + device_class, + unit, + friendly_name_format, + ) ) + async_add_entities(sensors) diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json new file mode 100644 index 00000000000000..a62a462504954c --- /dev/null +++ b/homeassistant/components/opentherm_gw/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "OpenTherm Gateway", + "step": { + "init": { + "title": "OpenTherm Gateway", + "data": { + "name": "Name", + "device": "Path or URL", + "id": "ID", + "precision": "Climate temperature precision", + "floor_temperature": "Floor climate temperature" + } + } + }, + "error": { + "already_configured": "Gateway already configured", + "id_exists": "Gateway id already exists", + "serial_error": "Error connecting to device", + "timeout": "Connection attempt timed out" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7557fc32b401f1..1eb08709741394 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -45,6 +45,7 @@ "mqtt", "nest", "notion", + "opentherm_gw", "openuv", "owntracks", "plaato", diff --git a/requirements_all.txt b/requirements_all.txt index 9ce8b070b1e102..9f0ee493fa1249 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1370,7 +1370,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.4b4 +pyotgw==0.5b0 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e948ded8a7328..e8114352b04d0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,6 +335,9 @@ pynx584==0.4 # homeassistant.components.openuv pyopenuv==1.0.9 +# homeassistant.components.opentherm_gw +pyotgw==0.5b0 + # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a6f2584e256c22..70c81c660256dc 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -138,6 +138,7 @@ "pynws", "pynx584", "pyopenuv", + "pyotgw", "pyotp", "pyps4-homeassistant", "pyqwikswitch", diff --git a/tests/components/opentherm_gw/__init__.py b/tests/components/opentherm_gw/__init__.py new file mode 100644 index 00000000000000..2dfe926765185b --- /dev/null +++ b/tests/components/opentherm_gw/__init__.py @@ -0,0 +1 @@ +"""Tests for the Opentherm Gateway integration.""" diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py new file mode 100644 index 00000000000000..da80e2f9fbb74e --- /dev/null +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -0,0 +1,163 @@ +"""Test the Opentherm Gateway config flow.""" +import asyncio +from serial import SerialException +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME +from homeassistant.components.opentherm_gw.const import DOMAIN + +from pyotgw import OTGW_ABOUT +from tests.common import mock_coro + + +async def test_form_user(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Test Entry 1" + assert result2["data"] == { + CONF_NAME: "Test Entry 1", + CONF_DEVICE: "/dev/ttyUSB0", + CONF_ID: "test_entry_1", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_import(hass): + """Test import from existing config.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_ID: "legacy_gateway", CONF_DEVICE: "/dev/ttyUSB1"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "legacy_gateway" + assert result["data"] == { + CONF_NAME: "legacy_gateway", + CONF_DEVICE: "/dev/ttyUSB1", + CONF_ID: "legacy_gateway", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_duplicate_entries(hass): + """Test duplicate device or id errors.""" + flow1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + flow2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + flow3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result1 = await hass.config_entries.flow.async_configure( + flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + result2 = await hass.config_entries.flow.async_configure( + flow2["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB1"} + ) + result3 = await hass.config_entries.flow.async_configure( + flow3["flow_id"], {CONF_NAME: "Test Entry 2", CONF_DEVICE: "/dev/ttyUSB0"} + ) + assert result1["type"] == "create_entry" + assert result2["type"] == "form" + assert result2["errors"] == {"base": "id_exists"} + assert result3["type"] == "form" + assert result3["errors"] == {"base": "already_configured"} + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_connection_timeout(hass): + """Test we handle connection timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyotgw.pyotgw.connect", side_effect=(asyncio.TimeoutError) + ) as mock_connect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "timeout"} + assert len(mock_connect.mock_calls) == 1 + + +async def test_form_connection_error(hass): + """Test we handle serial connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("pyotgw.pyotgw.connect", side_effect=(SerialException)) as mock_connect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "serial_error"} + assert len(mock_connect.mock_calls) == 1 From bbd2078986a765c36b554680f21236365b4dbafa Mon Sep 17 00:00:00 2001 From: Zach Date: Fri, 4 Oct 2019 20:48:45 -0400 Subject: [PATCH 0605/3953] Add doods contains flags on areas to allow specifying overlap (#27035) * Add support for the contains flags on areas to allow specifying overlap vs contains * Remove draw_box * Add timeout option * Fix import for CONF_TIMEOUT * Change contains to covers --- .../components/doods/image_processing.py | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 3eec85b3e5304f..02d7ce26f1ce18 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -7,6 +7,7 @@ from PIL import Image, ImageDraw from pydoods import PyDOODS +from homeassistant.const import CONF_TIMEOUT from homeassistant.components.image_processing import ( CONF_CONFIDENCE, CONF_ENTITY_ID, @@ -31,6 +32,7 @@ CONF_DETECTOR = "detector" CONF_LABELS = "labels" CONF_AREA = "area" +CONF_COVERS = "covers" CONF_TOP = "top" CONF_BOTTOM = "bottom" CONF_RIGHT = "right" @@ -43,6 +45,7 @@ vol.Optional(CONF_LEFT, default=0): cv.small_float, vol.Optional(CONF_RIGHT, default=1): cv.small_float, vol.Optional(CONF_TOP, default=0): cv.small_float, + vol.Optional(CONF_COVERS, default=True): cv.boolean, } ) @@ -50,7 +53,7 @@ { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_AREA): AREA_SCHEMA, - vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), + vol.Optional(CONF_CONFIDENCE): vol.Range(min=0, max=100), } ) @@ -58,6 +61,7 @@ { vol.Required(CONF_URL): cv.string, vol.Required(CONF_DETECTOR): cv.string, + vol.Required(CONF_TIMEOUT, default=90): cv.positive_int, vol.Optional(CONF_AUTH_KEY, default=""): cv.string, vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]), vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), @@ -74,8 +78,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): url = config[CONF_URL] auth_key = config[CONF_AUTH_KEY] detector_name = config[CONF_DETECTOR] + timeout = config[CONF_TIMEOUT] - doods = PyDOODS(url, auth_key) + doods = PyDOODS(url, auth_key, timeout) response = doods.get_detectors() if not isinstance(response, dict): _LOGGER.warning("Could not connect to doods server: %s", url) @@ -140,6 +145,7 @@ def __init__(self, hass, camera_entity, name, doods, detector, config): # handle labels and specific detection areas labels = config[CONF_LABELS] self._label_areas = {} + self._label_covers = {} for label in labels: if isinstance(label, dict): label_name = label[CONF_NAME] @@ -147,14 +153,17 @@ def __init__(self, hass, camera_entity, name, doods, detector, config): _LOGGER.warning("Detector does not support label %s", label_name) continue - # Label Confidence - label_confidence = label[CONF_CONFIDENCE] + # If label confidence is not specified, use global confidence + label_confidence = label.get(CONF_CONFIDENCE) + if not label_confidence: + label_confidence = confidence if label_name not in dconfig or dconfig[label_name] > label_confidence: dconfig[label_name] = label_confidence # Label area label_area = label.get(CONF_AREA) self._label_areas[label_name] = [0, 0, 1, 1] + self._label_covers[label_name] = True if label_area: self._label_areas[label_name] = [ label_area[CONF_TOP], @@ -162,6 +171,7 @@ def __init__(self, hass, camera_entity, name, doods, detector, config): label_area[CONF_BOTTOM], label_area[CONF_RIGHT], ] + self._label_covers[label_name] = label_area[CONF_COVERS] else: if label not in detector["labels"] and label != "*": _LOGGER.warning("Detector does not support label %s", label) @@ -175,6 +185,7 @@ def __init__(self, hass, camera_entity, name, doods, detector, config): # Handle global detection area self._area = [0, 0, 1, 1] + self._covers = True area_config = config.get(CONF_AREA) if area_config: self._area = [ @@ -183,6 +194,7 @@ def __init__(self, hass, camera_entity, name, doods, detector, config): area_config[CONF_BOTTOM], area_config[CONF_RIGHT], ] + self._covers = area_config[CONF_COVERS] template.attach(hass, self._file_out) @@ -308,22 +320,41 @@ def process_image(self, image): continue # Exclude matches outside global area definition - if ( - boxes[0] < self._area[0] - or boxes[1] < self._area[1] - or boxes[2] > self._area[2] - or boxes[3] > self._area[3] - ): - continue + if self._covers: + if ( + boxes[0] < self._area[0] + or boxes[1] < self._area[1] + or boxes[2] > self._area[2] + or boxes[3] > self._area[3] + ): + continue + else: + if ( + boxes[0] > self._area[2] + or boxes[1] > self._area[3] + or boxes[2] < self._area[0] + or boxes[3] < self._area[1] + ): + continue # Exclude matches outside label specific area definition - if self._label_areas and ( - boxes[0] < self._label_areas[label][0] - or boxes[1] < self._label_areas[label][1] - or boxes[2] > self._label_areas[label][2] - or boxes[3] > self._label_areas[label][3] - ): - continue + if self._label_areas.get(label): + if self._label_covers[label]: + if ( + boxes[0] < self._label_areas[label][0] + or boxes[1] < self._label_areas[label][1] + or boxes[2] > self._label_areas[label][2] + or boxes[3] > self._label_areas[label][3] + ): + continue + else: + if ( + boxes[0] > self._label_areas[label][2] + or boxes[1] > self._label_areas[label][3] + or boxes[2] < self._label_areas[label][0] + or boxes[3] < self._label_areas[label][1] + ): + continue if label not in matches: matches[label] = [] From 71a351605382136c5580a2eb8f46d5b24216ac64 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 4 Oct 2019 22:28:55 -0400 Subject: [PATCH 0606/3953] Guard against network errors for Dark Sky (#27141) * Guard against network errors for Dark Sky - Prevents network errors from throwing an exception during state updates for the Dark Sky weather component. * Implement `available` for Dark Sky component * unknown -> unavailable --- homeassistant/components/darksky/weather.py | 8 +++++++- tests/components/darksky/test_weather.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 5296f34662618e..dc5708d12a0cbe 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -102,6 +102,11 @@ def __init__(self, name, dark_sky, mode): self._ds_hourly = None self._ds_daily = None + @property + def available(self): + """Return if weather data is available from Dark Sky.""" + return self._ds_data is not None + @property def attribution(self): """Return the attribution.""" @@ -215,7 +220,8 @@ def update(self): self._dark_sky.update() self._ds_data = self._dark_sky.data - self._ds_currently = self._dark_sky.currently.d + currently = self._dark_sky.currently + self._ds_currently = currently.d if currently else {} self._ds_hourly = self._dark_sky.hourly self._ds_daily = self._dark_sky.daily diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py index ea28d3facb929d..ca328f458397b9 100644 --- a/tests/components/darksky/test_weather.py +++ b/tests/components/darksky/test_weather.py @@ -6,6 +6,8 @@ import forecastio import requests_mock +from requests.exceptions import ConnectionError + from homeassistant.components import weather from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.setup import setup_component @@ -48,3 +50,16 @@ def test_setup(self, mock_req, mock_get_forecast): state = self.hass.states.get("weather.test") assert state.state == "sunny" + + @patch("forecastio.load_forecast", side_effect=ConnectionError()) + def test_failed_setup(self, mock_load_forecast): + """Test to ensure that a network error does not break component state.""" + + assert setup_component( + self.hass, + weather.DOMAIN, + {"weather": {"name": "test", "platform": "darksky", "api_key": "foo"}}, + ) + + state = self.hass.states.get("weather.test") + assert state.state == "unavailable" From 2e17ad86af660091591facc8cf682197bffb6d04 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 5 Oct 2019 11:59:34 +0200 Subject: [PATCH 0607/3953] Adds guards for missing information in call stack frames (#27217) --- homeassistant/helpers/config_validation.py | 3 ++- homeassistant/helpers/deprecation.py | 10 +++++++++- homeassistant/util/logging.py | 22 ++++++++++++++++++---- tests/helpers/test_config_validation.py | 5 ++++- tests/util/test_logging.py | 6 +----- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 2d1bb89d23a56e..4d5df8785e244c 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -600,7 +600,8 @@ def deprecated( if module is not None: module_name = module.__name__ else: - # Unclear when it is None, but it happens, so let's guard. + # If Python is unable to access the sources files, the call stack frame + # will be missing information, so let's guard. # https://github.com/home-assistant/home-assistant/issues/24982 module_name = __name__ diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 881534b5bed70c..2a4fafde75bad9 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -54,7 +54,15 @@ def get_deprecated( and a warning is issued to the user. """ if old_name in config: - module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ # type: ignore + module = inspect.getmodule(inspect.stack()[1][0]) + if module is not None: + module_name = module.__name__ + else: + # If Python is unable to access the sources files, the call stack frame + # will be missing information, so let's guard. + # https://github.com/home-assistant/home-assistant/issues/24982 + module_name = __name__ + logger = logging.getLogger(module_name) logger.warning( "'%s' is deprecated. Please rename '%s' to '%s' in your " diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 99e606d28662ef..de04f23d9dd9f2 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -130,7 +130,15 @@ def catch_log_exception( """Decorate a callback to catch and log exceptions.""" def log_exception(*args: Any) -> None: - module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ # type: ignore + module = inspect.getmodule(inspect.stack()[1][0]) + if module is not None: + module_name = module.__name__ + else: + # If Python is unable to access the sources files, the call stack frame + # will be missing information, so let's guard. + # https://github.com/home-assistant/home-assistant/issues/24982 + module_name = __name__ + # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) @@ -178,9 +186,15 @@ async def coro_wrapper(*args: Any) -> Any: try: return await target except Exception: # pylint: disable=broad-except - module_name = inspect.getmodule( # type: ignore - inspect.trace()[1][0] - ).__name__ + module = inspect.getmodule(inspect.stack()[1][0]) + if module is not None: + module_name = module.__name__ + else: + # If Python is unable to access the sources files, the frame + # will be missing information, so let's guard. + # https://github.com/home-assistant/home-assistant/issues/24982 + module_name = __name__ + # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index e09f8cf57aadae..1f5d6ddfc402e7 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -494,7 +494,10 @@ def test_deprecated_with_no_optionals(caplog, schema): test_data = {"mars": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 1 - assert caplog.records[0].name == __name__ + assert caplog.records[0].name in [ + __name__, + "homeassistant.helpers.config_validation", + ] assert ( "The 'mars' option (with value 'True') is deprecated, " "please remove it from your configuration" diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 414b246466c475..d5f8eb4a2c7872 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -72,12 +72,8 @@ async def test_async_create_catching_coro(hass, caplog): async def job(): raise Exception("This is a bad coroutine") - pass hass.async_create_task(logging_util.async_create_catching_coro(job())) await hass.async_block_till_done() assert "This is a bad coroutine" in caplog.text - assert ( - "hass.async_create_task(" - "logging_util.async_create_catching_coro(job()))" in caplog.text - ) + assert "in test_async_create_catching_coro" in caplog.text From a9073451f818d65e16f4c1c2dcf3952e8c23ac39 Mon Sep 17 00:00:00 2001 From: MagicalTrev89 <52410270+MagicalTrev89@users.noreply.github.com> Date: Sat, 5 Oct 2019 14:52:42 +0100 Subject: [PATCH 0608/3953] Add hive trv support (#27033) * TRV-Support * pyhive import update * Moved HVAC to new line * updated pyhiveapi version * Update for pylint errors * Fix Pylint Errors * Fixed Pylint 2 * removed whitespace * Black * Updates following review * updated phyhive to 0.2.19.3 * Corrected logic on TRV name * updated requirements as requested * Black run --- homeassistant/components/hive/__init__.py | 1 + homeassistant/components/hive/climate.py | 27 +++++++++++++++++++-- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 3301097bab79b2..976821513b6183 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -82,6 +82,7 @@ class HiveSession: switch = None weather = None attributes = None + trv = None def setup(hass, config): diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 1fb77ce6cb9731..ed13e3019ce13f 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -8,6 +8,9 @@ PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + CURRENT_HVAC_HEAT, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -26,6 +29,12 @@ HVAC_MODE_OFF: "OFF", } +HIVE_TO_HASS_HVAC_ACTION = { + "UNKNOWN": CURRENT_HVAC_OFF, + False: CURRENT_HVAC_IDLE, + True: CURRENT_HVAC_HEAT, +} + SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] SUPPORT_PRESET = [PRESET_NONE, PRESET_BOOST] @@ -71,7 +80,11 @@ def name(self): """Return the name of the Climate device.""" friendly_name = "Heating" if self.node_name is not None: - friendly_name = f"{self.node_name} {friendly_name}" + if self.device_type == "TRV": + friendly_name = self.node_name + else: + friendly_name = f"{self.node_name} {friendly_name}" + return friendly_name @property @@ -95,6 +108,13 @@ def hvac_mode(self): """ return HIVE_TO_HASS_STATE[self.session.heating.get_mode(self.node_id)] + @property + def hvac_action(self): + """Return current HVAC action.""" + return HIVE_TO_HASS_HVAC_ACTION[ + self.session.heating.operational_status(self.node_id, self.device_type) + ] + @property def temperature_unit(self): """Return the unit of measurement.""" @@ -123,7 +143,10 @@ def max_temp(self): @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" - if self.session.heating.get_boost(self.node_id) == "ON": + if ( + self.device_type == "Heating" + and self.session.heating.get_boost(self.node_id) == "ON" + ): return PRESET_BOOST return None diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 4164283f9f88bd..e87e3387a6211b 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "documentation": "https://www.home-assistant.io/integrations/hive", "requirements": [ - "pyhiveapi==0.2.19.2" + "pyhiveapi==0.2.19.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9f0ee493fa1249..112304cd48ae61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1229,7 +1229,7 @@ pyheos==0.6.0 pyhik==0.2.3 # homeassistant.components.hive -pyhiveapi==0.2.19.2 +pyhiveapi==0.2.19.3 # homeassistant.components.homematic pyhomematic==0.1.60 From a8567a746bceb01103d942bd0982a18bff041abc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 5 Oct 2019 16:16:08 +0200 Subject: [PATCH 0609/3953] UniFi - Improve switch tests (#27200) * Continue rewriting tests for UniFi --- tests/components/unifi/test_device_tracker.py | 1 - tests/components/unifi/test_switch.py | 425 +++++++++++------- 2 files changed, 254 insertions(+), 172 deletions(-) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 8e05d8a1dd1b58..d2cedb91d8d942 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -135,7 +135,6 @@ async def setup_unifi_integration( async def mock_request(self, method, path, json=None): mock_requests.append({"method": method, "path": path, "json": json}) - print(mock_requests, mock_client_responses, mock_device_responses) if path == "s/{site}/stat/sta" and mock_client_responses: return mock_client_responses.popleft() if path == "s/{site}/stat/device" and mock_device_responses: diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 7ea5e0680b9c44..56a96b2b5b24ff 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1,14 +1,10 @@ """UniFi POE control platform tests.""" from collections import deque -from unittest.mock import Mock +from copy import deepcopy -import pytest - -from tests.common import mock_coro +from asynctest import Mock, patch import aiounifi -from aiounifi.clients import Clients, ClientsAll -from aiounifi.devices import Devices from homeassistant import config_entries from homeassistant.components import unifi @@ -35,6 +31,7 @@ "hostname": "client_1", "ip": "10.0.0.1", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:01", "name": "POE Client 1", "oui": "Producer", @@ -47,6 +44,7 @@ "hostname": "client_2", "ip": "10.0.0.2", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:02", "name": "POE Client 2", "oui": "Producer", @@ -59,6 +57,7 @@ "hostname": "client_3", "ip": "10.0.0.3", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:03", "name": "Non-POE Client 3", "oui": "Producer", @@ -71,6 +70,7 @@ "hostname": "client_4", "ip": "10.0.0.4", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:04", "name": "Non-POE Client 4", "oui": "Producer", @@ -83,6 +83,7 @@ "hostname": "client_1", "ip": "mock-host", "is_wired": True, + "last_seen": 1562600145, "mac": "10:00:00:00:00:01", "name": "Cloud key", "oui": "Producer", @@ -96,6 +97,7 @@ "hostname": "client_1", "ip": "10.0.0.1", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:01", "name": "POE Client 1", "oui": "Producer", @@ -108,6 +110,7 @@ "hostname": "client_2", "ip": "10.0.0.2", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:02", "name": "POE Client 2", "oui": "Producer", @@ -122,7 +125,8 @@ "device_id": "mock-id", "ip": "10.0.1.1", "mac": "00:00:00:00:01:01", - "type": "usw", + "last_seen": 1562600145, + "model": "US16P150", "name": "mock-name", "port_overrides": [], "port_table": [ @@ -179,6 +183,9 @@ "up": True, }, ], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", } BLOCKED = { @@ -210,126 +217,170 @@ CONF_PASSWORD: "mock-pswd", CONF_PORT: 1234, CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: True, + CONF_VERIFY_SSL: False, } ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") +SITES = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "admin"}} + + +async def setup_unifi_integration( + hass, + config, + options, + sites, + clients_response, + devices_response, + clients_all_response, +): + """Create the UniFi controller.""" + hass.data[UNIFI_CONFIG] = [] + hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) + config_entry = config_entries.ConfigEntry( + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, + options=options, + entry_id=1, + ) -@pytest.fixture -def mock_controller(hass): - """Mock a UniFi Controller.""" - hass.data[UNIFI_CONFIG] = {} - hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() - controller = unifi.UniFiController(hass, None) - controller.wireless_clients = set() + mock_client_responses = deque() + mock_client_responses.append(clients_response) - controller._site_role = "admin" + mock_device_responses = deque() + mock_device_responses.append(devices_response) - controller.api = Mock() - controller.mock_requests = [] + mock_client_all_responses = deque() + mock_client_all_responses.append(clients_all_response) - controller.mock_client_responses = deque() - controller.mock_device_responses = deque() - controller.mock_client_all_responses = deque() + mock_requests = [] - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - controller.mock_requests.append(kwargs) - if path == "s/{site}/stat/sta": - return controller.mock_client_responses.popleft() - if path == "s/{site}/stat/device": - return controller.mock_device_responses.popleft() - if path == "s/{site}/rest/user": - return controller.mock_client_all_responses.popleft() - return None + async def mock_request(self, method, path, json=None): + mock_requests.append({"method": method, "path": path, "json": json}) + print(mock_requests, mock_client_responses, mock_device_responses) + if path == "s/{site}/stat/sta" and mock_client_responses: + return mock_client_responses.popleft() + if path == "s/{site}/stat/device" and mock_device_responses: + return mock_device_responses.popleft() + if path == "s/{site}/rest/user" and mock_client_all_responses: + return mock_client_all_responses.popleft() + return {} - controller.api.clients = Clients({}, mock_request) - controller.api.devices = Devices({}, mock_request) - controller.api.clients_all = ClientsAll({}, mock_request) + with patch("aiounifi.Controller.login", return_value=True), patch( + "aiounifi.Controller.sites", return_value=sites + ), patch("aiounifi.Controller.request", new=mock_request): + await unifi.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) - return controller + controller_id = unifi.get_controller_id_from_config_entry(config_entry) + controller = hass.data[unifi.DOMAIN][controller_id] + controller.mock_client_responses = mock_client_responses + controller.mock_device_responses = mock_device_responses + controller.mock_client_all_responses = mock_client_all_responses + controller.mock_requests = mock_requests -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} - config_entry = config_entries.ConfigEntry( - 1, - unifi.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - entry_id=1, - system_options={}, - options=options, - ) - mock_controller.config_entry = config_entry - - await mock_controller.async_update() - await hass.config_entries.async_forward_entry_setup(config_entry, "switch") - # To flush out the service call to update the group - await hass.async_block_till_done() + return controller async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a bridge.""" + """Test that we do not discover anything or try to set up a controller.""" assert ( await async_setup_component( - hass, switch.DOMAIN, {"switch": {"platform": "unifi"}} + hass, switch.DOMAIN, {switch.DOMAIN: {"platform": "unifi"}} ) is True ) assert unifi.DOMAIN not in hass.data -async def test_no_clients(hass, mock_controller): +async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" - mock_controller.mock_client_responses.append({}) - mock_controller.mock_device_responses.append({}) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert not hass.states.async_all() + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 -async def test_controller_not_client(hass, mock_controller): +async def test_controller_not_client(hass): """Test that the controller doesn't become a switch.""" - mock_controller.mock_client_responses.append([CLOUDKEY]) - mock_controller.mock_device_responses.append([DEVICE_1]) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert not hass.states.async_all() + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CLOUDKEY], + devices_response=[DEVICE_1], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 cloudkey = hass.states.get("switch.cloud_key") assert cloudkey is None -async def test_not_admin(hass, mock_controller): +async def test_not_admin(hass): """Test that switch platform only work on an admin account.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([]) - - mock_controller._site_role = "viewer" + sites = deepcopy(SITES) + sites["Site name"]["role"] = "not admin" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=sites, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response=[], + ) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert len(hass.states.async_all()) == 0 + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 -async def test_switches(hass, mock_controller): +async def test_switches(hass): """Test the update_items function with some clients.""" - 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]) - options = {unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]} + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CLIENT_1, CLIENT_4], + devices_response=[DEVICE_1], + clients_all_response=[BLOCKED, UNBLOCKED, CLIENT_1], + ) - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 4 + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 6 switch_1 = hass.states.get("switch.poe_client_1") assert switch_1 is not None @@ -353,25 +404,34 @@ async def test_switches(hass, mock_controller): assert unblocked.state == "on" -async def test_new_client_discovered(hass, mock_controller): +async def test_new_client_discovered(hass): """Test if 2nd update has a new client.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([DEVICE_1]) + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response=[], + ) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert len(hass.states.async_all()) == 2 + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 4 - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) - mock_controller.mock_device_responses.append([DEVICE_1]) + controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) + controller.mock_device_responses.append([DEVICE_1]) # Calling a service will trigger the updates to run await hass.services.async_call( "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True ) - assert len(mock_controller.mock_requests) == 5 - assert len(hass.states.async_all()) == 3 - assert mock_controller.mock_requests[2] == { + assert len(controller.mock_requests) == 6 + assert len(hass.states.async_all()) == 5 + assert controller.mock_requests[3] == { "json": { "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}] }, @@ -382,8 +442,8 @@ async def test_new_client_discovered(hass, mock_controller): await hass.services.async_call( "switch", "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True ) - assert len(mock_controller.mock_requests) == 7 - assert mock_controller.mock_requests[5] == { + assert len(controller.mock_requests) == 9 + assert controller.mock_requests[3] == { "json": { "port_overrides": [ {"port_idx": 1, "portconf_id": "1a1", "poe_mode": "auto"} @@ -398,66 +458,24 @@ async def test_new_client_discovered(hass, mock_controller): assert switch_2.state == "on" -async def test_failed_update_successful_login(hass, mock_controller): - """Running update can login when requested.""" - mock_controller.available = False - mock_controller.api.clients.update = Mock() - mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired - mock_controller.api.login = Mock() - mock_controller.api.login.return_value = mock_coro() - - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 0 - - assert mock_controller.available is True - - -async def test_failed_update_failed_login(hass, mock_controller): - """Running update can handle a failed login.""" - mock_controller.api.clients.update = Mock() - mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired - mock_controller.api.login = Mock() - mock_controller.api.login.side_effect = aiounifi.AiounifiException - - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 0 - - assert mock_controller.available is False - - -async def test_failed_update_unreachable_controller(hass, mock_controller): - """Running update can handle a unreachable controller.""" - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) - mock_controller.mock_device_responses.append([DEVICE_1]) - - await setup_controller(hass, mock_controller) - - mock_controller.api.clients.update = Mock() - mock_controller.api.clients.update.side_effect = aiounifi.AiounifiException - - # Calling a service will trigger the updates to run - await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True - ) - - assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 3 - - assert mock_controller.available is False - - -async def test_ignore_multiple_poe_clients_on_same_port(hass, mock_controller): +async def test_ignore_multiple_poe_clients_on_same_port(hass): """Ignore when there are multiple POE driven clients on same port. If there is a non-UniFi switch powered by POE, clients will be transparently marked as having POE as well. """ - mock_controller.mock_client_responses.append(POE_SWITCH_CLIENTS) - mock_controller.mock_device_responses.append([DEVICE_1]) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - # 1 All Lights group, 2 lights - assert len(hass.states.async_all()) == 0 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=POE_SWITCH_CLIENTS, + devices_response=[DEVICE_1], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 5 switch_1 = hass.states.get("switch.poe_client_1") switch_2 = hass.states.get("switch.poe_client_2") @@ -465,22 +483,18 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass, mock_controller): assert switch_2 is None -async def test_restoring_client(hass, mock_controller): +async def test_restoring_client(hass): """Test the update_items function with some clients.""" - 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]) - options = {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, + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, + options={}, + entry_id=1, ) registry = await entity_registry.async_get_registry(hass) @@ -499,9 +513,78 @@ async def test_restoring_client(hass, mock_controller): config_entry=config_entry, ) - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 3 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: ["random mac"], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CLIENT_2], + devices_response=[DEVICE_1], + clients_all_response=[CLIENT_1], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 5 device_1 = hass.states.get("switch.client_1") assert device_1 is not None + + +async def test_failed_update_failed_login(hass): + """Running update can handle a failed login.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: ["random mac"], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 + + with patch.object( + controller.api.clients, "update", side_effect=aiounifi.LoginRequired + ), patch.object(controller.api, "login", side_effect=aiounifi.AiounifiException): + await controller.async_update() + await hass.async_block_till_done() + + assert controller.available is False + + +async def test_failed_update_successful_login(hass): + """Running update can login when requested.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: ["random mac"], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 + + with patch.object( + controller.api.clients, "update", side_effect=aiounifi.LoginRequired + ), patch.object(controller.api, "login", return_value=Mock(True)): + await controller.async_update() + await hass.async_block_till_done() + + assert controller.available is True From 25bfdbc8df383e801de977fa8ac1aba3fe1c3d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 5 Oct 2019 22:20:11 +0300 Subject: [PATCH 0610/3953] Require Python >= 3.6.1 (#27226) https://github.com/home-assistant/architecture/issues/278 --- homeassistant/bootstrap.py | 11 ----------- homeassistant/const.py | 2 +- tests/test_main.py | 29 ++++++++++++++++++++++++++++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 7c4ec731b49b93..ef294491141042 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -97,17 +97,6 @@ async def async_from_config_dict( stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) - if sys.version_info[:3] < (3, 6, 1): - msg = ( - "Python 3.6.0 support is deprecated and will " - "be removed in the first release after October 2. Please " - "upgrade Python to 3.6.1 or higher." - ) - _LOGGER.warning(msg) - hass.components.persistent_notification.async_create( - msg, "Python version", "python_version" - ) - return hass diff --git a/homeassistant/const.py b/homeassistant/const.py index 9baa4a1f71c160..e0f90834d943dd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -4,7 +4,7 @@ PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) -REQUIRED_PYTHON_VER = (3, 6, 0) +REQUIRED_PYTHON_VER = (3, 6, 1) # Format for platform files PLATFORM_FORMAT = "{platform}.{domain}" diff --git a/tests/test_main.py b/tests/test_main.py index 509425ce418a5d..29454d269af494 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,6 +2,7 @@ from unittest.mock import patch, PropertyMock from homeassistant import __main__ as main +from homeassistant.const import REQUIRED_PYTHON_VER @patch("sys.exit") @@ -31,6 +32,32 @@ def test_validate_python(mock_exit): mock_exit.reset_mock() - with patch("sys.version_info", new_callable=PropertyMock(return_value=(3, 6, 0))): + with patch( + "sys.version_info", + new_callable=PropertyMock( + return_value=(REQUIRED_PYTHON_VER[0] - 1,) + REQUIRED_PYTHON_VER[1:] + ), + ): + main.validate_python() + assert mock_exit.called is True + + mock_exit.reset_mock() + + with patch( + "sys.version_info", new_callable=PropertyMock(return_value=REQUIRED_PYTHON_VER) + ): + main.validate_python() + assert mock_exit.called is False + + mock_exit.reset_mock() + + with patch( + "sys.version_info", + new_callable=PropertyMock( + return_value=(REQUIRED_PYTHON_VER[:2]) + (REQUIRED_PYTHON_VER[2] + 1,) + ), + ): main.validate_python() assert mock_exit.called is False + + mock_exit.reset_mock() From cc1cca0a14205018f2723ca29e7ab45898149ff4 Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 5 Oct 2019 21:31:51 +0200 Subject: [PATCH 0611/3953] automation_reproduce_state (#27222) --- .../components/automation/reproduce_state.py | 61 +++++++++++++++++++ .../automation/test_reproduce_state.py | 50 +++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 homeassistant/components/automation/reproduce_state.py create mode 100644 tests/components/automation/test_reproduce_state.py diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py new file mode 100644 index 00000000000000..553d6871087729 --- /dev/null +++ b/homeassistant/components/automation/reproduce_state.py @@ -0,0 +1,61 @@ +"""Reproduce an Automation state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Automation states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/automation/test_reproduce_state.py b/tests/components/automation/test_reproduce_state.py new file mode 100644 index 00000000000000..4f3fd735fc5336 --- /dev/null +++ b/tests/components/automation/test_reproduce_state.py @@ -0,0 +1,50 @@ +"""Test reproduce state for Automation.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Automation states.""" + hass.states.async_set("automation.entity_off", "off", {}) + hass.states.async_set("automation.entity_on", "on", {}) + + turn_on_calls = async_mock_service(hass, "automation", "turn_on") + turn_off_calls = async_mock_service(hass, "automation", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [State("automation.entity_off", "off"), State("automation.entity_on", "on")], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("automation.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("automation.entity_on", "off"), + State("automation.entity_off", "on"), + # Should not raise + State("automation.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "automation" + assert turn_on_calls[0].data == {"entity_id": "automation.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "automation" + assert turn_off_calls[0].data == {"entity_id": "automation.entity_on"} From 3d1e743b0cf24ee8af27541f3326f3b9479faa8c Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Sat, 5 Oct 2019 21:34:18 +0200 Subject: [PATCH 0612/3953] Add set_location service doc (#27216) --- homeassistant/components/homeassistant/services.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index 2219564abb875a..cb3efb0d524a67 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -7,6 +7,16 @@ reload_core_config: restart: description: Restart the Home Assistant service. +set_location: + description: Update the Home Assistant location. + fields: + latitude: + description: Latitude of your location + example: 32.87336 + longitude: + description: Longitude of your location + example: 117.22743 + stop: description: Stop the Home Assistant service. From e088119d6d1fe5063998f047807b8be43da5273d Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 5 Oct 2019 21:42:37 +0200 Subject: [PATCH 0613/3953] fan_reproduce_state (#27227) --- .../components/fan/reproduce_state.py | 100 ++++++++++++++++++ tests/components/fan/test_reproduce_state.py | 89 ++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 homeassistant/components/fan/reproduce_state.py create mode 100644 tests/components/fan/test_reproduce_state.py diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py new file mode 100644 index 00000000000000..1053861e2bf547 --- /dev/null +++ b/homeassistant/components/fan/reproduce_state.py @@ -0,0 +1,100 @@ +"""Reproduce an Fan state.""" +import asyncio +import logging +from types import MappingProxyType +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + DOMAIN, + ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_SPEED, + SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, + SERVICE_SET_SPEED, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} +ATTRIBUTES = { # attribute: service + ATTR_DIRECTION: SERVICE_SET_DIRECTION, + ATTR_OSCILLATING: SERVICE_OSCILLATE, + ATTR_SPEED: SERVICE_SET_SPEED, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and all( + check_attr_equal(cur_state.attributes, state.attributes, attr) + for attr in ATTRIBUTES + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + service_calls = {} # service: service_data + + if state.state == STATE_ON: + # The fan should be on + if cur_state.state != STATE_ON: + # Turn on the fan at first + service_calls[SERVICE_TURN_ON] = service_data + + for attr, service in ATTRIBUTES.items(): + # Call services to adjust the attributes + if attr in state.attributes and not check_attr_equal( + state.attributes, cur_state.attributes, attr + ): + data = service_data.copy() + data[attr] = state.attributes[attr] + service_calls[service] = data + + elif state.state == STATE_OFF: + service_calls[SERVICE_TURN_OFF] = service_data + + for service, data in service_calls.items(): + await hass.services.async_call( + DOMAIN, service, data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Fan states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + +def check_attr_equal( + attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str +) -> bool: + """Return true if the given attributes are equal.""" + return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/tests/components/fan/test_reproduce_state.py b/tests/components/fan/test_reproduce_state.py new file mode 100644 index 00000000000000..0dcd38580b8c78 --- /dev/null +++ b/tests/components/fan/test_reproduce_state.py @@ -0,0 +1,89 @@ +"""Test reproduce state for Fan.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Fan states.""" + hass.states.async_set("fan.entity_off", "off", {}) + hass.states.async_set("fan.entity_on", "on", {}) + hass.states.async_set("fan.entity_speed", "on", {"speed": "high"}) + hass.states.async_set("fan.entity_oscillating", "on", {"oscillating": True}) + hass.states.async_set("fan.entity_direction", "on", {"direction": "forward"}) + + turn_on_calls = async_mock_service(hass, "fan", "turn_on") + turn_off_calls = async_mock_service(hass, "fan", "turn_off") + set_direction_calls = async_mock_service(hass, "fan", "set_direction") + oscillate_calls = async_mock_service(hass, "fan", "oscillate") + set_speed_calls = async_mock_service(hass, "fan", "set_speed") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("fan.entity_off", "off"), + State("fan.entity_on", "on"), + State("fan.entity_speed", "on", {"speed": "high"}), + State("fan.entity_oscillating", "on", {"oscillating": True}), + State("fan.entity_direction", "on", {"direction": "forward"}), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_direction_calls) == 0 + assert len(oscillate_calls) == 0 + assert len(set_speed_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("fan.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_direction_calls) == 0 + assert len(oscillate_calls) == 0 + assert len(set_speed_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("fan.entity_on", "off"), + State("fan.entity_off", "on"), + State("fan.entity_speed", "on", {"speed": "low"}), + State("fan.entity_oscillating", "on", {"oscillating": False}), + State("fan.entity_direction", "on", {"direction": "reverse"}), + # Should not raise + State("fan.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "fan" + assert turn_on_calls[0].data == {"entity_id": "fan.entity_off"} + + assert len(set_direction_calls) == 1 + assert set_direction_calls[0].domain == "fan" + assert set_direction_calls[0].data == { + "entity_id": "fan.entity_direction", + "direction": "reverse", + } + + assert len(oscillate_calls) == 1 + assert oscillate_calls[0].domain == "fan" + assert oscillate_calls[0].data == { + "entity_id": "fan.entity_oscillating", + "oscillating": False, + } + + assert len(set_speed_calls) == 1 + assert set_speed_calls[0].domain == "fan" + assert set_speed_calls[0].data == {"entity_id": "fan.entity_speed", "speed": "low"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "fan" + assert turn_off_calls[0].data == {"entity_id": "fan.entity_on"} From 46ac98379eec66a5bb44a882e2d4b2d94ee0a6ee Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 5 Oct 2019 21:43:12 +0200 Subject: [PATCH 0614/3953] Add improved scene support to the light integration (#27182) * light reproduce state * Add types * Fix linting error * Add tests * Improve test * Fix failing tests * Another try * avoid repetition * simplified if * Remove attributes that are no attributes --- .../components/light/reproduce_state.py | 94 ++++++++++++++ .../components/light/test_reproduce_state.py | 117 ++++++++++++++++++ tests/helpers/test_state.py | 10 +- 3 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/light/reproduce_state.py create mode 100644 tests/components/light/test_reproduce_state.py diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py new file mode 100644 index 00000000000000..ae618f7a8efa7d --- /dev/null +++ b/homeassistant/components/light/reproduce_state.py @@ -0,0 +1,94 @@ +"""Reproduce an Light state.""" +import asyncio +import logging +from types import MappingProxyType +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + DOMAIN, + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} +ATTR_GROUP = [ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_WHITE_VALUE] +COLOR_GROUP = [ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_XY_COLOR] + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and all( + check_attr_equal(cur_state.attributes, state.attributes, attr) + for attr in ATTR_GROUP + COLOR_GROUP + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + for attr in ATTR_GROUP: + # All attributes that are not colors + if attr in state.attributes: + service_data[attr] = state.attributes[attr] + + for color_attr in COLOR_GROUP: + # Choose the first color that is specified + if color_attr in state.attributes: + service_data[color_attr] = state.attributes[color_attr] + break + + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Light states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + +def check_attr_equal( + attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str +) -> bool: + """Return true if the given attributes are equal.""" + return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py new file mode 100644 index 00000000000000..92790890a4c25e --- /dev/null +++ b/tests/components/light/test_reproduce_state.py @@ -0,0 +1,117 @@ +"""Test reproduce state for Light.""" +from homeassistant.core import State + +from tests.common import async_mock_service + +VALID_BRIGHTNESS = {"brightness": 180} +VALID_WHITE_VALUE = {"white_value": 200} +VALID_EFFECT = {"effect": "random"} +VALID_COLOR_TEMP = {"color_temp": 240} +VALID_HS_COLOR = {"hs_color": (345, 75)} +VALID_RGB_COLOR = {"rgb_color": (255, 63, 111)} +VALID_XY_COLOR = {"xy_color": (0.59, 0.274)} + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Light states.""" + hass.states.async_set("light.entity_off", "off", {}) + hass.states.async_set("light.entity_bright", "on", VALID_BRIGHTNESS) + hass.states.async_set("light.entity_white", "on", VALID_WHITE_VALUE) + hass.states.async_set("light.entity_effect", "on", VALID_EFFECT) + hass.states.async_set("light.entity_temp", "on", VALID_COLOR_TEMP) + hass.states.async_set("light.entity_hs", "on", VALID_HS_COLOR) + hass.states.async_set("light.entity_rgb", "on", VALID_RGB_COLOR) + hass.states.async_set("light.entity_xy", "on", VALID_XY_COLOR) + + turn_on_calls = async_mock_service(hass, "light", "turn_on") + turn_off_calls = async_mock_service(hass, "light", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("light.entity_off", "off"), + State("light.entity_bright", "on", VALID_BRIGHTNESS), + State("light.entity_white", "on", VALID_WHITE_VALUE), + State("light.entity_effect", "on", VALID_EFFECT), + State("light.entity_temp", "on", VALID_COLOR_TEMP), + State("light.entity_hs", "on", VALID_HS_COLOR), + State("light.entity_rgb", "on", VALID_RGB_COLOR), + State("light.entity_xy", "on", VALID_XY_COLOR), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("light.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("light.entity_xy", "off"), + State("light.entity_off", "on", VALID_BRIGHTNESS), + State("light.entity_bright", "on", VALID_WHITE_VALUE), + State("light.entity_white", "on", VALID_EFFECT), + State("light.entity_effect", "on", VALID_COLOR_TEMP), + State("light.entity_temp", "on", VALID_HS_COLOR), + State("light.entity_hs", "on", VALID_RGB_COLOR), + State("light.entity_rgb", "on", VALID_XY_COLOR), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 7 + + expected_calls = [] + + expected_off = VALID_BRIGHTNESS + expected_off["entity_id"] = "light.entity_off" + expected_calls.append(expected_off) + + expected_bright = VALID_WHITE_VALUE + expected_bright["entity_id"] = "light.entity_bright" + expected_calls.append(expected_bright) + + expected_white = VALID_EFFECT + expected_white["entity_id"] = "light.entity_white" + expected_calls.append(expected_white) + + expected_effect = VALID_COLOR_TEMP + expected_effect["entity_id"] = "light.entity_effect" + expected_calls.append(expected_effect) + + expected_temp = VALID_HS_COLOR + expected_temp["entity_id"] = "light.entity_temp" + expected_calls.append(expected_temp) + + expected_hs = VALID_RGB_COLOR + expected_hs["entity_id"] = "light.entity_hs" + expected_calls.append(expected_hs) + + expected_rgb = VALID_XY_COLOR + expected_rgb["entity_id"] = "light.entity_rgb" + expected_calls.append(expected_rgb) + + for call in turn_on_calls: + assert call.domain == "light" + found = False + for expected in expected_calls: + if call.data["entity_id"] == expected["entity_id"]: + # We found the matching entry + assert call.data == expected + found = True + break + # No entry found + assert found + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "light" + assert turn_off_calls[0].data == {"entity_id": "light.entity_xy"} diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 7f428c0833d877..14bcbde50949b5 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -129,7 +129,7 @@ async def test_reproduce_turn_on(hass): last_call = calls[-1] assert last_call.domain == "light" assert SERVICE_TURN_ON == last_call.service - assert ["light.test"] == last_call.data.get("entity_id") + assert "light.test" == last_call.data.get("entity_id") async def test_reproduce_turn_off(hass): @@ -146,7 +146,7 @@ async def test_reproduce_turn_off(hass): last_call = calls[-1] assert last_call.domain == "light" assert SERVICE_TURN_OFF == last_call.service - assert ["light.test"] == last_call.data.get("entity_id") + assert "light.test" == last_call.data.get("entity_id") async def test_reproduce_complex_data(hass): @@ -155,10 +155,10 @@ async def test_reproduce_complex_data(hass): hass.states.async_set("light.test", "off") - complex_data = ["hello", {"11": "22"}] + complex_data = [255, 100, 100] await state.async_reproduce_state( - hass, ha.State("light.test", "on", {"complex": complex_data}) + hass, ha.State("light.test", "on", {"rgb_color": complex_data}) ) await hass.async_block_till_done() @@ -167,7 +167,7 @@ async def test_reproduce_complex_data(hass): last_call = calls[-1] assert last_call.domain == "light" assert SERVICE_TURN_ON == last_call.service - assert complex_data == last_call.data.get("complex") + assert complex_data == last_call.data.get("rgb_color") async def test_reproduce_bad_state(hass): From 9c08c3588179d5adb8bfc3536f937537153d6231 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Sat, 5 Oct 2019 23:43:57 +0400 Subject: [PATCH 0615/3953] Improve influxdb error handling (#27225) --- homeassistant/components/influxdb/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 2bb5207aa85408..86d489621eaad3 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -353,7 +353,11 @@ def write_to_influxdb(self, json): _LOGGER.debug("Wrote %d events", len(json)) break - except (exceptions.InfluxDBClientError, IOError) as err: + except ( + exceptions.InfluxDBClientError, + exceptions.InfluxDBServerError, + IOError, + ) as err: if retry < self.max_tries: time.sleep(RETRY_DELAY) else: From 0b838f88c1ba03735e9f7293d399b1a2227bcc77 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 5 Oct 2019 12:44:51 -0700 Subject: [PATCH 0616/3953] Bump adb-shell to 0.0.4; bump androidtv to 0.0.30 (#27224) --- homeassistant/components/androidtv/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 4fd3b062a10721..e84ed35c7636f7 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.3", - "androidtv==0.0.29" + "adb-shell==0.0.4", + "androidtv==0.0.30" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index 112304cd48ae61..086482eda7d963 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.3 +adb-shell==0.0.4 # homeassistant.components.adguard adguardhome==0.2.1 @@ -203,7 +203,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.29 +androidtv==0.0.30 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8114352b04d0b..60780ec7c55c76 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -83,7 +83,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.29 +androidtv==0.0.30 # homeassistant.components.apns apns2==0.3.0 From 5ae497bfdc6dd3358752b3ff523721a10b53b871 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sat, 5 Oct 2019 21:46:16 +0200 Subject: [PATCH 0617/3953] Refactor Tradfri switch device (#26864) * Refactor Tradfri switch device * Lint * Lint * Removed unused constant * Add base_class * Lint * Improvements after review * Typo --- .coveragerc | 1 + .../components/tradfri/base_class.py | 96 +++++++++++++++ homeassistant/components/tradfri/switch.py | 111 +++--------------- 3 files changed, 112 insertions(+), 96 deletions(-) create mode 100644 homeassistant/components/tradfri/base_class.py diff --git a/.coveragerc b/.coveragerc index a6f65430074190..5c2d2e02f45020 100644 --- a/.coveragerc +++ b/.coveragerc @@ -682,6 +682,7 @@ omit = homeassistant/components/tradfri/* homeassistant/components/tradfri/light.py homeassistant/components/tradfri/cover.py + homeassistant/components/tradfri/base_class.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/transmission/__init__.py diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py new file mode 100644 index 00000000000000..5fce3c08510543 --- /dev/null +++ b/homeassistant/components/tradfri/base_class.py @@ -0,0 +1,96 @@ +"""Base class for IKEA TRADFRI.""" +import logging + +from pytradfri.error import PytradfriError + +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from . import DOMAIN as TRADFRI_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class TradfriBaseDevice(Entity): + """Base class for a TRADFRI device.""" + + def __init__(self, device, api, gateway_id): + """Initialize a device.""" + self._available = True + self._api = api + self._device = None + self._device_control = None + self._device_data = None + self._gateway_id = gateway_id + self._name = None + self._unique_id = None + + self._refresh(device) + + @callback + def _async_start_observe(self, exc=None): + """Start observation of device.""" + if exc: + self._available = False + self.async_schedule_update_ha_state() + _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) + + try: + cmd = self._device.observe( + callback=self._observe_update, + err_callback=self._async_start_observe, + duration=0, + ) + self.hass.async_create_task(self._api(cmd)) + except PytradfriError as err: + _LOGGER.warning("Observation failed, trying again", exc_info=err) + self._async_start_observe() + + async def async_added_to_hass(self): + """Start thread when added to hass.""" + self._async_start_observe() + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def device_info(self): + """Return the device info.""" + info = self._device.device_info + + return { + "identifiers": {(TRADFRI_DOMAIN, self._device.id)}, + "name": self._name, + "manufacturer": info.manufacturer, + "model": info.model_number, + "sw_version": info.firmware_version, + "via_device": (TRADFRI_DOMAIN, self._gateway_id), + } + + @property + def name(self): + """Return the display name of this device.""" + return self._name + + @property + def should_poll(self): + """No polling needed for tradfri device.""" + return False + + @property + def unique_id(self): + """Return unique ID for device.""" + return self._unique_id + + @callback + def _observe_update(self, device): + """Receive new state data for this device.""" + self._refresh(device) + self.async_schedule_update_ha_state() + + def _refresh(self, device): + """Refresh the device data.""" + self._device = device + self._name = device.name + self._available = device.reachable diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 545c1ad93cec17..1e322ff47f5111 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,17 +1,9 @@ """Support for IKEA Tradfri switches.""" -import logging - -from pytradfri.error import PytradfriError - from homeassistant.components.switch import SwitchDevice -from homeassistant.core import callback -from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY +from . import KEY_API, KEY_GATEWAY +from .base_class import TradfriBaseDevice from .const import CONF_GATEWAY_ID -_LOGGER = logging.getLogger(__name__) - -TRADFRI_SWITCH_MANAGER = "Tradfri Switch Manager" - async def async_setup_entry(hass, config_entry, async_add_entities): """Load Tradfri switches based on a config entry.""" @@ -28,104 +20,31 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class TradfriSwitch(SwitchDevice): +class TradfriSwitch(TradfriBaseDevice, SwitchDevice): """The platform class required by Home Assistant.""" - def __init__(self, switch, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a switch.""" - self._api = api - self._unique_id = f"{gateway_id}-{switch.id}" - self._switch = None - self._socket_control = None - self._switch_data = None - self._name = None - self._available = True - self._gateway_id = gateway_id - - self._refresh(switch) - - @property - def unique_id(self): - """Return unique ID for switch.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - info = self._switch.device_info - - return { - "identifiers": {(TRADFRI_DOMAIN, self._switch.id)}, - "name": self._name, - "manufacturer": info.manufacturer, - "model": info.model_number, - "sw_version": info.firmware_version, - "via_device": (TRADFRI_DOMAIN, self._gateway_id), - } - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() + super().__init__(device, api, gateway_id) + self._unique_id = f"{gateway_id}-{device.id}" - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def should_poll(self): - """No polling needed for tradfri switch.""" - return False + def _refresh(self, device): + """Refresh the switch data.""" + super()._refresh(device) - @property - def name(self): - """Return the display name of this switch.""" - return self._name + # Caching of switch control and switch object + self._device_control = device.socket_control + self._device_data = device.socket_control.sockets[0] @property def is_on(self): """Return true if switch is on.""" - return self._switch_data.state + return self._device_data.state async def async_turn_off(self, **kwargs): """Instruct the switch to turn off.""" - await self._api(self._socket_control.set_state(False)) + await self._api(self._device_control.set_state(False)) async def async_turn_on(self, **kwargs): """Instruct the switch to turn on.""" - await self._api(self._socket_control.set_state(True)) - - @callback - def _async_start_observe(self, exc=None): - """Start observation of switch.""" - if exc: - self._available = False - self.async_schedule_update_ha_state() - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._switch.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, switch): - """Refresh the switch data.""" - self._switch = switch - - # Caching of switchControl and switch object - self._available = switch.reachable - self._socket_control = switch.socket_control - self._switch_data = switch.socket_control.sockets[0] - self._name = switch.name - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this switch.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() + await self._api(self._device_control.set_state(True)) From 601d15701bd899ade881567ba1ab857d11d6982c Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 5 Oct 2019 21:57:12 +0200 Subject: [PATCH 0618/3953] Add initial state to Flux Switch (#27089) * flux restore state * Add config options * Add tests * Add more tests * just restores state --- homeassistant/components/flux/switch.py | 10 +++++- tests/components/flux/test_switch.py | 48 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 800ccd1938fb2d..7b58ffbe449e9e 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -31,10 +31,12 @@ CONF_LIGHTS, CONF_MODE, SERVICE_TURN_ON, + STATE_ON, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify from homeassistant.util.color import ( @@ -169,7 +171,7 @@ async def async_update(call=None): hass.services.async_register(DOMAIN, service_name, async_update) -class FluxSwitch(SwitchDevice): +class FluxSwitch(SwitchDevice, RestoreEntity): """Representation of a Flux switch.""" def __init__( @@ -214,6 +216,12 @@ def is_on(self): """Return true if switch is on.""" return self.unsub_tracker is not None + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + last_state = await self.async_get_last_state() + if last_state and last_state.state == STATE_ON: + await self.async_turn_on() + async def async_turn_on(self, **kwargs): """Turn on flux.""" if self.is_on: diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index fb35485f5c9685..91871666f467a7 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -10,12 +10,14 @@ SERVICE_TURN_ON, SUN_EVENT_SUNRISE, ) +from homeassistant.core import State import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, async_fire_time_changed, async_mock_service, + mock_restore_cache, ) from tests.components.light import common as common_light from tests.components.switch import common @@ -35,6 +37,52 @@ async def test_valid_config(hass): }, ) + state = hass.states.get("switch.flux") + assert state + assert state.state == "off" + + +async def test_restore_state_last_on(hass): + """Test restoring state when the last state is on.""" + mock_restore_cache(hass, [State("switch.flux", "on")]) + + assert await async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "flux", + "name": "flux", + "lights": ["light.desk", "light.lamp"], + } + }, + ) + + state = hass.states.get("switch.flux") + assert state + assert state.state == "on" + + +async def test_restore_state_last_off(hass): + """Test restoring state when the last state is off.""" + mock_restore_cache(hass, [State("switch.flux", "off")]) + + assert await async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "flux", + "name": "flux", + "lights": ["light.desk", "light.lamp"], + } + }, + ) + + state = hass.states.get("switch.flux") + assert state + assert state.state == "off" + async def test_valid_config_with_info(hass): """Test configuration.""" From 99859485e21fc652d537e3d559c129dbea064496 Mon Sep 17 00:00:00 2001 From: scheric <38077357+scheric@users.noreply.github.com> Date: Sat, 5 Oct 2019 22:07:01 +0200 Subject: [PATCH 0619/3953] Repair SolarEdge_local inverter fahrenheit temperature (#27096) * Add Fahrenheit check * Rounding values * add missing bracket * Fix spelling * round fahrenheit to 1 decimal * Change unit on the fly * Use new sensor names * Use TEMP_FAHRENHEIT constant * Pass new sensors fully to SolarEdgeSensor * applying snake_case * applying snake_case lower case * Update sensor.py * applying feedback --- .../components/solaredge_local/sensor.py | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 4fc62e44921448..ce51efa07caff9 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta import statistics +from copy import deepcopy from requests.exceptions import HTTPError, ConnectTimeout from solaredge_local import SolarEdge @@ -14,6 +15,7 @@ POWER_WATT, ENERGY_WATT_HOUR, TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -58,25 +60,25 @@ ], "optimizer_current": [ "optimizercurrent", - "Avrage Optimizer Current", + "Average Optimizer Current", "A", "mdi:solar-panel", ], "optimizer_power": [ "optimizerpower", - "Avrage Optimizer Power", + "Average Optimizer Power", POWER_WATT, "mdi:solar-panel", ], "optimizer_temperature": [ "optimizertemperature", - "Avrage Optimizer Temperature", + "Average Optimizer Temperature", TEMP_CELSIUS, "mdi:solar-panel", ], "optimizer_voltage": [ "optimizervoltage", - "Avrage Optimizer Voltage", + "Average Optimizer Voltage", "V", "mdi:solar-panel", ], @@ -112,13 +114,30 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Could not retrieve details from SolarEdge API") return + # Changing inverter temperature unit. + sensors = deepcopy(SENSOR_TYPES) + if status.inverters.primary.temperature.units.farenheit: + sensors["inverter_temperature"] = [ + "invertertemperature", + "Inverter Temperature", + TEMP_FAHRENHEIT, + "mdi:thermometer", + ] + # Create solaredge data service which will retrieve and update the data. data = SolarEdgeData(hass, api) # Create a new sensor for each sensor type. entities = [] - for sensor_key in SENSOR_TYPES: - sensor = SolarEdgeSensor(platform_name, sensor_key, data) + for sensor_info in sensors.values(): + sensor = SolarEdgeSensor( + platform_name, + data, + sensor_info[0], + sensor_info[1], + sensor_info[2], + sensor_info[3], + ) entities.append(sensor) add_entities(entities, True) @@ -127,20 +146,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SolarEdgeSensor(Entity): """Representation of an SolarEdge Monitoring API sensor.""" - def __init__(self, platform_name, sensor_key, data): + def __init__(self, platform_name, data, json_key, name, unit, icon): """Initialize the sensor.""" - self.platform_name = platform_name - self.sensor_key = sensor_key - self.data = data + self._platform_name = platform_name + self._data = data self._state = None - self._json_key = SENSOR_TYPES[self.sensor_key][0] - self._unit_of_measurement = SENSOR_TYPES[self.sensor_key][2] + self._json_key = json_key + self._name = name + self._unit_of_measurement = unit + self._icon = icon @property def name(self): """Return the name.""" - return f"{self.platform_name} ({SENSOR_TYPES[self.sensor_key][1]})" + return f"{self._platform_name} ({self._name})" @property def unit_of_measurement(self): @@ -150,7 +170,7 @@ def unit_of_measurement(self): @property def icon(self): """Return the sensor icon.""" - return SENSOR_TYPES[self.sensor_key][3] + return self._icon @property def state(self): @@ -159,8 +179,8 @@ def state(self): def update(self): """Get the latest data from the sensor and update the state.""" - self.data.update() - self._state = self.data.data[self._json_key] + self._data.update() + self._state = self._data.data[self._json_key] class SolarEdgeData: @@ -220,11 +240,11 @@ def update(self): self.data["energyThisMonth"] = round(status.energy.thisMonth, 2) self.data["energyToday"] = round(status.energy.today, 2) self.data["currentPower"] = round(status.powerWatt, 2) - self.data[ - "invertertemperature" - ] = status.inverters.primary.temperature.value + self.data["invertertemperature"] = round( + status.inverters.primary.temperature.value, 2 + ) if maintenance.system.name: - self.data["optimizertemperature"] = statistics.mean(temperature) - self.data["optimizervoltage"] = statistics.mean(voltage) - self.data["optimizercurrent"] = statistics.mean(current) - self.data["optimizerpower"] = power + self.data["optimizertemperature"] = round(statistics.mean(temperature), 2) + self.data["optimizervoltage"] = round(statistics.mean(voltage), 2) + self.data["optimizercurrent"] = round(statistics.mean(current), 2) + self.data["optimizerpower"] = round(power, 2) From 43d14130507904dd578c52a47532a6cb486a6a29 Mon Sep 17 00:00:00 2001 From: Pierre Sicot Date: Sat, 5 Oct 2019 22:28:19 +0200 Subject: [PATCH 0620/3953] Fix closed status for non horizontal awnings. (#26840) --- homeassistant/components/tahoma/cover.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index a189199bfb2b3c..7448eb27ae072f 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -137,14 +137,13 @@ def update(self): if self._closure is not None: if self.tahoma_device.type == HORIZONTAL_AWNING: self._position = self._closure - self._closed = self._position == 0 else: self._position = 100 - self._closure - self._closed = self._position == 100 if self._position <= 5: self._position = 0 if self._position >= 95: self._position = 100 + self._closed = self._position == 0 else: self._position = None if "core:OpenClosedState" in self.tahoma_device.active_states: From d16edb3ef016e8515410a84206e6bbc695a23cd2 Mon Sep 17 00:00:00 2001 From: Matthew Donoughe Date: Sat, 5 Oct 2019 16:30:43 -0400 Subject: [PATCH 0621/3953] add script shortcut for activating scenes (#27223) * add script shortcut for activating scenes use `- scene: scene.` in a script to activate a scene * Update validation --- homeassistant/helpers/config_validation.py | 3 +++ homeassistant/helpers/script.py | 24 +++++++++++++++++++ tests/helpers/test_script.py | 27 ++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 4d5df8785e244c..8598b50f140e64 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -885,6 +885,8 @@ def custom_serializer(schema): DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +_SCRIPT_SCENE_SCHEMA = vol.Schema({vol.Required("scene"): entity_domain("scene")}) + SCRIPT_SCHEMA = vol.All( ensure_list, [ @@ -895,6 +897,7 @@ def custom_serializer(schema): EVENT_SCHEMA, CONDITION_SCHEMA, DEVICE_ACTION_SCHEMA, + _SCRIPT_SCENE_SCHEMA, ) ], ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 05b281027262ff..1e65c24eaaf4cd 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -9,12 +9,15 @@ import voluptuous as vol import homeassistant.components.device_automation as device_automation +import homeassistant.components.scene as scene from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, CONF_TIMEOUT, + SERVICE_TURN_ON, ) from homeassistant import exceptions from homeassistant.helpers import ( @@ -46,6 +49,7 @@ CONF_DELAY = "delay" CONF_WAIT_TEMPLATE = "wait_template" CONF_CONTINUE = "continue_on_timeout" +CONF_SCENE = "scene" ACTION_DELAY = "delay" @@ -54,6 +58,7 @@ ACTION_FIRE_EVENT = "event" ACTION_CALL_SERVICE = "call_service" ACTION_DEVICE_AUTOMATION = "device" +ACTION_ACTIVATE_SCENE = "scene" def _determine_action(action): @@ -73,6 +78,9 @@ def _determine_action(action): if CONF_DEVICE_ID in action: return ACTION_DEVICE_AUTOMATION + if CONF_SCENE in action: + return ACTION_ACTIVATE_SCENE + return ACTION_CALL_SERVICE @@ -147,6 +155,7 @@ def __init__( ACTION_FIRE_EVENT: self._async_fire_event, ACTION_CALL_SERVICE: self._async_call_service, ACTION_DEVICE_AUTOMATION: self._async_device_automation, + ACTION_ACTIVATE_SCENE: self._async_activate_scene, } @property @@ -362,6 +371,21 @@ async def _async_device_automation(self, action, variables, context): self.hass, action, variables, context ) + async def _async_activate_scene(self, action, variables, context): + """Activate the scene specified in the action. + + This method is a coroutine. + """ + self.last_action = action.get(CONF_ALIAS, "activate scene") + self._log("Executing step %s" % self.last_action) + await self.hass.services.async_call( + scene.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: action[CONF_SCENE]}, + blocking=True, + context=context, + ) + async def _async_fire_event(self, action, variables, context): """Fire an event.""" self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT]) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index ebc56c111ee4dc..4b8be715f37ca5 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -9,7 +9,9 @@ import voluptuous as vol import pytest +import homeassistant.components.scene as scene from homeassistant import exceptions +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON from homeassistant.core import Context, callback # Otherwise can't test just this file (import order issue) @@ -120,6 +122,31 @@ def record_call(service): assert calls[0].data.get("hello") == "world" +async def test_activating_scene(hass): + """Test the activation of a scene.""" + calls = [] + context = Context() + + @callback + def record_call(service): + """Add recorded event to set.""" + calls.append(service) + + hass.services.async_register(scene.DOMAIN, SERVICE_TURN_ON, record_call) + + hass.async_add_job( + ft.partial( + script.call_from_config, hass, {"scene": "scene.hello"}, context=context + ) + ) + + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data.get(ATTR_ENTITY_ID) == "scene.hello" + + async def test_calling_service_template(hass): """Test the calling of a service.""" calls = [] From be60b065a348233901113635e5ad3be3cfec0114 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 5 Oct 2019 22:31:01 +0200 Subject: [PATCH 0622/3953] Bump python-miio version to 0.4.6 (#27231) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 4c01cce2d3c42e..b675e6e6746234 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": [ "construct==2.9.45", - "python-miio==0.4.5" + "python-miio==0.4.6" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 086482eda7d963..c2c2c96a26fc61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1534,7 +1534,7 @@ python-juicenet==0.0.5 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.4.5 +python-miio==0.4.6 # homeassistant.components.mpd python-mpd2==1.0.0 From 5c01dd483fc6bf2d15848b7f4ebcc99fb45bb705 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 5 Oct 2019 22:31:10 +0200 Subject: [PATCH 0623/3953] Add Xiaomi Air Humidifier CB1 (zhimi.humidifier.cb1) support (#27232) --- homeassistant/components/xiaomi_miio/fan.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 67dc12565d8680..acac60e108a59b 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -43,7 +43,8 @@ MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1" -MODEL_AIRHUMIDIFIER_CA = "zhimi.humidifier.ca1" +MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1" +MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1" MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2" @@ -68,7 +69,8 @@ MODEL_AIRPURIFIER_SA2, MODEL_AIRPURIFIER_2S, MODEL_AIRHUMIDIFIER_V1, - MODEL_AIRHUMIDIFIER_CA, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRFRESH_VA2, ] ), @@ -235,7 +237,7 @@ ATTR_BUTTON_PRESSED: "button_pressed", } -AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = { +AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB = { **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, ATTR_MOTOR_SPEED: "motor_speed", ATTR_DEPTH: "depth", @@ -335,7 +337,7 @@ | FEATURE_SET_TARGET_HUMIDITY ) -FEATURE_FLAGS_AIRHUMIDIFIER_CA = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY +FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER @@ -880,9 +882,9 @@ def __init__(self, name, device, model, unique_id): super().__init__(name, device, model, unique_id) - if self._model == MODEL_AIRHUMIDIFIER_CA: - self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA + if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: + self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB self._speed_list = [ mode.name for mode in OperationMode if mode is not OperationMode.Strong ] From fdb603527548c5bc01513a6f6b73c480885d192d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 22:30:59 +0200 Subject: [PATCH 0624/3953] Only generate device trigger for sensor with unit (#27152) --- .../components/sensor/device_trigger.py | 11 ++++++++-- .../components/sensor/test_device_trigger.py | 4 +++- .../custom_components/test/sensor.py | 22 ++++++++++++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 1074236eedf843..c8655d91c5c0af 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -5,6 +5,7 @@ from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, CONF_ABOVE, CONF_BELOW, CONF_ENTITY_ID, @@ -113,8 +114,14 @@ async def async_get_triggers(hass, device_id): for entry in entries: device_class = DEVICE_CLASS_NONE state = hass.states.get(entry.entity_id) - if state: - device_class = state.attributes.get(ATTR_DEVICE_CLASS) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + continue + + device_class = state.attributes.get(ATTR_DEVICE_CLASS) templates = ENTITY_TRIGGERS.get( device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 1dc41f5bffa247..7118c94c5c9c7d 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -2,7 +2,7 @@ from datetime import timedelta import pytest -from homeassistant.components.sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM from homeassistant.setup import async_setup_component @@ -19,6 +19,7 @@ async_get_device_automations, async_get_device_automation_capabilities, ) +from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES @pytest.fixture @@ -70,6 +71,7 @@ async def test_get_triggers(hass, device_reg, entity_reg): } for device_class in DEVICE_CLASSES for trigger in ENTITY_TRIGGERS[device_class] + if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert triggers == expected_triggers diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index c25be28fdb03ee..651ee17bd65d46 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -3,10 +3,24 @@ Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.sensor import DEVICE_CLASSES +import homeassistant.components.sensor as sensor from tests.common import MockEntity +DEVICE_CLASSES = list(sensor.DEVICE_CLASSES) +DEVICE_CLASSES.append("none") + +UNITS_OF_MEASUREMENT = { + sensor.DEVICE_CLASS_BATTERY: "%", # % of battery that is left + sensor.DEVICE_CLASS_HUMIDITY: "%", # % of humidity in the air + sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm) + sensor.DEVICE_CLASS_SIGNAL_STRENGTH: "dB", # signal strength (dB/dBm) + sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F) + sensor.DEVICE_CLASS_TIMESTAMP: "hh:mm:ss", # timestamp (ISO8601) + sensor.DEVICE_CLASS_PRESSURE: "hPa", # pressure (hPa/mbar) + sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW) +} + ENTITIES = {} @@ -22,6 +36,7 @@ def init(empty=False): name=f"{device_class} sensor", unique_id=f"unique_{device_class}", device_class=device_class, + unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), ) for device_class in DEVICE_CLASSES } @@ -42,3 +57,8 @@ class MockSensor(MockEntity): def device_class(self): """Return the class of this sensor.""" return self._handle("device_class") + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of this sensor.""" + return self._handle("unit_of_measurement") From 143e42362b28680480e536a934f2678e0388e07e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 22:17:58 +0200 Subject: [PATCH 0625/3953] Add above and below to sensor trigger extra_fields (#27160) --- homeassistant/components/sensor/device_trigger.py | 6 +++++- tests/components/sensor/test_device_trigger.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index c8655d91c5c0af..50fb1dd5c14602 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -147,6 +147,10 @@ async def async_get_trigger_capabilities(hass, trigger): """List trigger capabilities.""" return { "extra_fields": vol.Schema( - {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + { + vol.Optional(CONF_ABOVE): vol.Coerce(float), + vol.Optional(CONF_BELOW): vol.Coerce(float), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } ) } diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 7118c94c5c9c7d..45452dc84a0dca 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -88,7 +88,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + {"name": "above", "optional": True, "type": "float"}, + {"name": "below", "optional": True, "type": "float"}, + {"name": "for", "optional": True, "type": "positive_time_period_dict"}, ] } triggers = await async_get_device_automations(hass, "trigger", device_entry.id) From 8c3f743efdd242417268cc2fb1346b6719274bb2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 4 Oct 2019 17:05:52 +0200 Subject: [PATCH 0626/3953] Update connect-box to fix issue with attrs (#27194) --- homeassistant/components/upc_connect/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 2cf463d1cf0b25..cd5d327f2c22ae 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "upc_connect", "name": "Upc connect", "documentation": "https://www.home-assistant.io/integrations/upc_connect", - "requirements": ["connect-box==0.2.4"], + "requirements": ["connect-box==0.2.5"], "dependencies": [], "codeowners": ["@pvizeli"] } diff --git a/requirements_all.txt b/requirements_all.txt index fd44b46c64bb4a..45296e425026e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ colorlog==4.0.2 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.4 +connect-box==0.2.5 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart From 756e22290d14469f00de751d552f96aed0921aae Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Oct 2019 19:17:57 +0200 Subject: [PATCH 0627/3953] Fix validation when automation is saved from frontend (#27195) --- homeassistant/components/automation/config.py | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 581ce6b461d844..ebbd1771e84efd 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -18,33 +18,40 @@ async def async_validate_config_item(hass, config, full_config=None): """Validate config item.""" - try: - config = PLATFORM_SCHEMA(config) + config = PLATFORM_SCHEMA(config) - triggers = [] - for trigger in config[CONF_TRIGGER]: - trigger_platform = importlib.import_module( - "..{}".format(trigger[CONF_PLATFORM]), __name__ + triggers = [] + for trigger in config[CONF_TRIGGER]: + trigger_platform = importlib.import_module( + "..{}".format(trigger[CONF_PLATFORM]), __name__ + ) + if hasattr(trigger_platform, "async_validate_trigger_config"): + trigger = await trigger_platform.async_validate_trigger_config( + hass, trigger ) - if hasattr(trigger_platform, "async_validate_trigger_config"): - trigger = await trigger_platform.async_validate_trigger_config( - hass, trigger - ) - triggers.append(trigger) - config[CONF_TRIGGER] = triggers - - if CONF_CONDITION in config: - conditions = [] - for cond in config[CONF_CONDITION]: - cond = await condition.async_validate_condition_config(hass, cond) - conditions.append(cond) - config[CONF_CONDITION] = conditions - - actions = [] - for action in config[CONF_ACTION]: - action = await script.async_validate_action_config(hass, action) - actions.append(action) - config[CONF_ACTION] = actions + triggers.append(trigger) + config[CONF_TRIGGER] = triggers + + if CONF_CONDITION in config: + conditions = [] + for cond in config[CONF_CONDITION]: + cond = await condition.async_validate_condition_config(hass, cond) + conditions.append(cond) + config[CONF_CONDITION] = conditions + + actions = [] + for action in config[CONF_ACTION]: + action = await script.async_validate_action_config(hass, action) + actions.append(action) + config[CONF_ACTION] = actions + + return config + + +async def _try_async_validate_config_item(hass, config, full_config=None): + """Validate config item.""" + try: + config = await async_validate_config_item(hass, config, full_config) except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: async_log_exception(ex, DOMAIN, full_config or config, hass) return None @@ -57,7 +64,7 @@ async def async_validate_config(hass, config): automations = [] validated_automations = await asyncio.gather( *( - async_validate_config_item(hass, p_config, config) + _try_async_validate_config_item(hass, p_config, config) for _, p_config in config_per_platform(config, DOMAIN) ) ) From 33da7d341df507d7d18655676560df81bafc16c7 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Fri, 4 Oct 2019 17:15:43 -0400 Subject: [PATCH 0628/3953] Fix ecobee binary sensor and sensor unique ids (#27208) * Fix sensor unique id * Fix binary sensor unique id --- homeassistant/components/ecobee/binary_sensor.py | 3 ++- homeassistant/components/ecobee/sensor.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 68d8a88df47b64..7afdbae5a28fae 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -50,7 +50,8 @@ def unique_id(self): if sensor["name"] == self.sensor_name: if "code" in sensor: return f"{sensor['code']}-{self.device_class}" - return f"{sensor['id']}-{self.device_class}" + thermostat = self.data.ecobee.get_thermostat(self.index) + return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property def is_on(self): diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 8cf9af0e3b445f..24ea3d281bc618 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -61,7 +61,8 @@ def unique_id(self): if sensor["name"] == self.sensor_name: if "code" in sensor: return f"{sensor['code']}-{self.device_class}" - return f"{sensor['id']}-{self.device_class}" + thermostat = self.data.ecobee.get_thermostat(self.index) + return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property def device_class(self): From df0a233b6452a18d438bf24101c6c9d3b22f8a9b Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 5 Oct 2019 12:44:51 -0700 Subject: [PATCH 0629/3953] Bump adb-shell to 0.0.4; bump androidtv to 0.0.30 (#27224) --- homeassistant/components/androidtv/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 4fd3b062a10721..e84ed35c7636f7 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.3", - "androidtv==0.0.29" + "adb-shell==0.0.4", + "androidtv==0.0.30" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index 45296e425026e7..5328ca322c7297 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.3 +adb-shell==0.0.4 # homeassistant.components.adguard adguardhome==0.2.1 @@ -200,7 +200,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.29 +androidtv==0.0.30 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bc9870be10c5c..f3cacfa88887fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -80,7 +80,7 @@ aiowwlln==2.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.29 +androidtv==0.0.30 # homeassistant.components.apns apns2==0.3.0 From 1e1f79e45b49cc3068d6cb3cfd012b67e02d1111 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 Oct 2019 13:40:29 -0700 Subject: [PATCH 0630/3953] Bumped version to 0.100.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b3f99038b5947e..68537aff298735 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -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 bd92532ebbea07cac5ea574d68e13b7a57a3a74e Mon Sep 17 00:00:00 2001 From: Jens Date: Sun, 6 Oct 2019 01:12:50 +0200 Subject: [PATCH 0631/3953] Add io:SomfyBasicContactIOSystemSensor to TaHoma component (#27234) --- homeassistant/components/tahoma/__init__.py | 1 + homeassistant/components/tahoma/sensor.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 4400df6db96683..6bcc783400c8d3 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -42,6 +42,7 @@ "io:RollerShutterUnoIOComponent": "cover", "io:RollerShutterVeluxIOComponent": "cover", "io:RollerShutterWithLowSpeedManagementIOComponent": "cover", + "io:SomfyBasicContactIOSystemSensor": "sensor", "io:SomfyContactIOSystemSensor": "sensor", "io:VerticalExteriorAwningIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 0ed3879cc7a980..5279b160d9c540 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -44,6 +44,8 @@ def unit_of_measurement(self): return None if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": return None + if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": + return None if self.tahoma_device.type == "io:LightIOSystemSensor": return "lx" if self.tahoma_device.type == "Humidity Sensor": @@ -66,6 +68,11 @@ def update(self): self._available = bool( self.tahoma_device.active_states.get("core:StatusState") == "available" ) + if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": + self.current_value = self.tahoma_device.active_states["core:ContactState"] + self._available = bool( + self.tahoma_device.active_states.get("core:StatusState") == "available" + ) if self.tahoma_device.type == "rtds:RTDSContactSensor": self.current_value = self.tahoma_device.active_states["core:ContactState"] self._available = True From 2c6a869bc61d922b77cc4a7588d079e8a2836013 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 6 Oct 2019 00:32:15 +0000 Subject: [PATCH 0632/3953] [ci skip] Translation update --- .../components/airly/.translations/ca.json | 22 ++++++++++++++++++ .../components/airly/.translations/it.json | 22 ++++++++++++++++++ .../components/airly/.translations/ru.json | 22 ++++++++++++++++++ .../ambient_station/.translations/ru.json | 2 +- .../components/axis/.translations/ru.json | 4 ++-- .../cert_expiry/.translations/ru.json | 4 ++-- .../components/daikin/.translations/ru.json | 2 +- .../emulated_roku/.translations/ru.json | 2 +- .../components/esphome/.translations/ru.json | 2 +- .../geonetnz_quakes/.translations/ru.json | 2 +- .../components/hangouts/.translations/ru.json | 2 +- .../homematicip_cloud/.translations/ru.json | 2 +- .../components/hue/.translations/ru.json | 4 ++-- .../components/ipma/.translations/ru.json | 2 +- .../components/iqvia/.translations/ru.json | 2 +- .../components/life360/.translations/ru.json | 4 ++-- .../components/linky/.translations/ru.json | 4 ++-- .../luftdaten/.translations/ru.json | 2 +- .../components/met/.translations/ru.json | 2 +- .../components/notion/.translations/ru.json | 2 +- .../opentherm_gw/.translations/ca.json | 23 +++++++++++++++++++ .../opentherm_gw/.translations/it.json | 23 +++++++++++++++++++ .../opentherm_gw/.translations/nl.json | 11 +-------- .../opentherm_gw/.translations/no.json | 14 +++++++++++ .../opentherm_gw/.translations/ru.json | 23 +++++++++++++++++++ .../components/openuv/.translations/ru.json | 2 +- .../components/plex/.translations/ru.json | 6 ++--- .../rainmachine/.translations/ru.json | 2 +- .../simplisafe/.translations/ru.json | 2 +- .../components/smhi/.translations/ru.json | 2 +- .../solaredge/.translations/ru.json | 4 ++-- .../tellduslive/.translations/ru.json | 2 +- .../components/unifi/.translations/ru.json | 2 +- .../components/upnp/.translations/ru.json | 2 +- .../components/wwlln/.translations/ru.json | 2 +- .../components/zone/.translations/ru.json | 2 +- .../components/zwave/.translations/ru.json | 2 +- 37 files changed, 187 insertions(+), 47 deletions(-) create mode 100644 homeassistant/components/airly/.translations/ca.json create mode 100644 homeassistant/components/airly/.translations/it.json create mode 100644 homeassistant/components/airly/.translations/ru.json create mode 100644 homeassistant/components/opentherm_gw/.translations/ca.json create mode 100644 homeassistant/components/opentherm_gw/.translations/it.json create mode 100644 homeassistant/components/opentherm_gw/.translations/no.json create mode 100644 homeassistant/components/opentherm_gw/.translations/ru.json diff --git a/homeassistant/components/airly/.translations/ca.json b/homeassistant/components/airly/.translations/ca.json new file mode 100644 index 00000000000000..bf50b4f23e55b5 --- /dev/null +++ b/homeassistant/components/airly/.translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clau API no \u00e9s correcta.", + "name_exists": "El nom ja existeix.", + "wrong_location": "No hi ha estacions de mesura Airly en aquesta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clau API d'Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom de la integraci\u00f3" + }, + "description": "Configura una integraci\u00f3 de qualitat d\u2019aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/it.json b/homeassistant/components/airly/.translations/it.json new file mode 100644 index 00000000000000..e50f618575bfd6 --- /dev/null +++ b/homeassistant/components/airly/.translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La chiave API non \u00e8 corretta.", + "name_exists": "Il nome \u00e8 gi\u00e0 esistente", + "wrong_location": "Nessuna stazione di misurazione Airly in quest'area." + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API Airly", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome dell'integrazione" + }, + "description": "Configurazione dell'integrazione della qualit\u00e0 dell'aria Airly. Per generare la chiave API andare su https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/ru.json b/homeassistant/components/airly/.translations/ru.json new file mode 100644 index 00000000000000..36080c9f372e24 --- /dev/null +++ b/homeassistant/components/airly/.translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", + "wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 Airly. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register.", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index d1264010b75c1c..2d7964f18ebe61 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b" }, diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 67d720aa85f06a..951263d53f9668 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis" }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json index 6a795dee13e26b..d962c7931218c7 100644 --- a/homeassistant/components/cert_expiry/.translations/ru.json +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430" + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." }, "error": { "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430", "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443", - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442" }, "step": { diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index ce1f1ab3caa974..98ab98e6b170d2 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, diff --git a/homeassistant/components/emulated_roku/.translations/ru.json b/homeassistant/components/emulated_roku/.translations/ru.json index c7b85c195929d3..32bf473ac38546 100644 --- a/homeassistant/components/emulated_roku/.translations/ru.json +++ b/homeassistant/components/emulated_roku/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index 1405112c07022f..62d24662ab6a77 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430" + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json index 7d6583bc1d5b59..d6763d17e2d0a8 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ru.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 52b8798c0f4084..6942f683fa6e4a 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" }, "error": { diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 82ecd4a32504f2..5155a42c4c3b55 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP", "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index be5d2b7159d40b..79a46e1861bce6 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json index a260efa5bd9dc2..a302572ed121d5 100644 --- a/homeassistant/components/ipma/.translations/ru.json +++ b/homeassistant/components/ipma/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/iqvia/.translations/ru.json b/homeassistant/components/iqvia/.translations/ru.json index 06a5b7e69ddde5..0c3afc88c94dd0 100644 --- a/homeassistant/components/iqvia/.translations/ru.json +++ b/homeassistant/components/iqvia/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" }, "step": { diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index 1e962142373f89..d033da4bae71eb 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "create_entry": { "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." @@ -11,7 +11,7 @@ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d", "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "step": { "user": { diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index 498b5b2f12f29b..b569cce9239066 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443", "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430", + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c" }, "step": { diff --git a/homeassistant/components/luftdaten/.translations/ru.json b/homeassistant/components/luftdaten/.translations/ru.json index d37aa3567d1977..7ae83b550e3d44 100644 --- a/homeassistant/components/luftdaten/.translations/ru.json +++ b/homeassistant/components/luftdaten/.translations/ru.json @@ -3,7 +3,7 @@ "error": { "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten", "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d", - "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json index d92d28d948419d..559382cf209d67 100644 --- a/homeassistant/components/met/.translations/ru.json +++ b/homeassistant/components/met/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index c7e89c368c178c..7345cf462957c9 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c", "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e" }, diff --git a/homeassistant/components/opentherm_gw/.translations/ca.json b/homeassistant/components/opentherm_gw/.translations/ca.json new file mode 100644 index 00000000000000..0224d663a83953 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Passarel\u00b7la ja configurada", + "id_exists": "L'identificador de passarel\u00b7la ja existeix", + "serial_error": "S'ha produ\u00eft un error en connectar-se al dispositiu", + "timeout": "S'ha acabat el temps d'espera en l'intent de connexi\u00f3" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "floor_temperature": "Temperatura del pis", + "id": "ID", + "name": "Nom", + "precision": "Precisi\u00f3 de la temperatura" + }, + "title": "Passarel\u00b7la d'OpenTherm" + } + }, + "title": "Passarel\u00b7la d'OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/it.json b/homeassistant/components/opentherm_gw/.translations/it.json new file mode 100644 index 00000000000000..9c62686e19083d --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway gi\u00e0 configurato", + "id_exists": "ID del gateway esiste gi\u00e0", + "serial_error": "Errore durante la connessione al dispositivo", + "timeout": "Tentativo di connessione scaduto" + }, + "step": { + "init": { + "data": { + "device": "Percorso o URL", + "floor_temperature": "Temperatura climatica del pavimento", + "id": "ID", + "name": "Nome", + "precision": "Precisione della temperatura climatica" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "Gateway OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json index ef3daafe4fe6ee..4fec1baba7badb 100644 --- a/homeassistant/components/opentherm_gw/.translations/nl.json +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -1,19 +1,10 @@ { "config": { - "error": { - "already_configured": "Gateway is reeds geconfigureerd", - "id_exists": "Gateway id bestaat reeds", - "serial_error": "Kan niet verbinden met de Gateway", - "timeout": "Time-out van de verbinding" - }, "step": { "init": { "data": { "device": "Pad of URL", - "floor_temperature": "Thermostaat temperaturen naar beneden afronden", - "id": "ID", - "name": "Naam", - "precision": "Thermostaat temperatuur precisie" + "id": "ID" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json new file mode 100644 index 00000000000000..a1df80f3b12c72 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "id": "ID", + "name": "Navn" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json new file mode 100644 index 00000000000000..718322ec171088 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", + "serial_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "init": { + "data": { + "device": "\u041f\u0443\u0442\u044c \u0438\u043b\u0438 URL-\u0430\u0434\u0440\u0435\u0441", + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", + "id": "ID", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b" + }, + "title": "OpenTherm" + } + }, + "title": "OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/ru.json b/homeassistant/components/openuv/.translations/ru.json index 9683c5d7c3679b..58d57b280567f1 100644 --- a/homeassistant/components/openuv/.translations/ru.json +++ b/homeassistant/components/openuv/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b", + "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API" }, "step": { diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 2b63840d00198b..48cacacddfef09 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", - "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", - "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", + "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", + "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430", "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index 6eec3ef0ebac07..6248890389d140 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" }, "step": { diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index f685297890eae7..e82172f92f8579 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" }, "step": { diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json index 88ea988ff1bb9a..03b17b3ba8b85c 100644 --- a/homeassistant/components/smhi/.translations/ru.json +++ b/homeassistant/components/smhi/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442", + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438" }, "step": { diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json index fe36e4296feb92..d8622cdd2c1a3b 100644 --- a/homeassistant/components/solaredge/.translations/ru.json +++ b/homeassistant/components/solaredge/.translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "error": { - "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { "user": { diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index afaaf4edbf5e5f..9d3c97ad902eb3 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\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.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index 76802a96367677..2a3a6207cf5a81 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c" }, "error": { diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 8d41ec1d5def31..3351f0d5d8a815 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP", "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP / IGD \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", diff --git a/homeassistant/components/wwlln/.translations/ru.json b/homeassistant/components/wwlln/.translations/ru.json index ad553def6c3ee7..3bdaf85498bcd4 100644 --- a/homeassistant/components/wwlln/.translations/ru.json +++ b/homeassistant/components/wwlln/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/zone/.translations/ru.json b/homeassistant/components/zone/.translations/ru.json index dc408035d0f657..6a017e9e1c373e 100644 --- a/homeassistant/components/zone/.translations/ru.json +++ b/homeassistant/components/zone/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "init": { diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json index a64b4db185d7a1..ed2e20f35270c2 100644 --- a/homeassistant/components/zwave/.translations/ru.json +++ b/homeassistant/components/zwave/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave" }, "error": { From 476f24e451ee1797f7c67aed080da5a8178eca97 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sun, 6 Oct 2019 11:54:26 +0200 Subject: [PATCH 0633/3953] Add basic test support to Homematic IP Cloud (#27228) * Add basic test support to Homematic IP Cloud * move test data address comments --- .../components/homematicip_cloud/sensor.py | 1 + .../components/homematicip_cloud/conftest.py | 75 + tests/components/homematicip_cloud/helper.py | 128 + .../homematicip_cloud/test_binary_sensors.py | 41 + .../homematicip_cloud/test_config_flow.py | 3 +- .../components/homematicip_cloud/test_hap.py | 10 +- .../components/homematicip_cloud/test_init.py | 6 +- .../homematicip_cloud/test_lights.py | 77 + tests/fixtures/homematicip_cloud.json | 5341 +++++++++++++++++ 9 files changed, 5672 insertions(+), 10 deletions(-) create mode 100644 tests/components/homematicip_cloud/conftest.py create mode 100644 tests/components/homematicip_cloud/helper.py create mode 100644 tests/components/homematicip_cloud/test_binary_sensors.py create mode 100644 tests/components/homematicip_cloud/test_lights.py create mode 100644 tests/fixtures/homematicip_cloud.json diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 770921288b9341..30e910cc33a394 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -115,6 +115,7 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): def __init__(self, home: AsyncHome) -> None: """Initialize access point device.""" + home.modelType = "HmIP-HAP" super().__init__(home, home) @property diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py new file mode 100644 index 00000000000000..c301c73b4d0cac --- /dev/null +++ b/tests/components/homematicip_cloud/conftest.py @@ -0,0 +1,75 @@ +"""Initializer helpers for HomematicIP fake server.""" +from unittest.mock import MagicMock, patch + +from homematicip.aio.connection import AsyncConnection +import pytest + +from homeassistant import config_entries +from homeassistant.components.homematicip_cloud import ( + DOMAIN as HMIPC_DOMAIN, + const as hmipc, + hap as hmip_hap, +) +from homeassistant.core import HomeAssistant + +from .helper import AUTH_TOKEN, HAPID, HomeTemplate + +from tests.common import MockConfigEntry, mock_coro + + +@pytest.fixture(name="mock_connection") +def mock_connection_fixture(): + """Return a mockked connection.""" + connection = MagicMock(spec=AsyncConnection) + + def _rest_call_side_effect(path, body=None): + return path, body + + connection._restCall.side_effect = _rest_call_side_effect # pylint: disable=W0212 + connection.api_call.return_value = mock_coro(True) + + return connection + + +@pytest.fixture(name="default_mock_home") +def default_mock_home_fixture(mock_connection): + """Create a fake homematic async home.""" + return HomeTemplate(connection=mock_connection).init_home().get_async_home_mock() + + +@pytest.fixture(name="hmip_config_entry") +def hmip_config_entry_fixture(): + """Create a fake config entriy for homematic ip cloud.""" + entry_data = { + hmipc.HMIPC_HAPID: HAPID, + hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, + hmipc.HMIPC_NAME: "", + } + config_entry = MockConfigEntry( + version=1, + domain=HMIPC_DOMAIN, + title=HAPID, + data=entry_data, + source="import", + connection_class=config_entries.CONN_CLASS_CLOUD_PUSH, + system_options={"disable_new_entities": False}, + ) + + return config_entry + + +@pytest.fixture(name="default_mock_hap") +async def default_mock_hap_fixture( + hass: HomeAssistant, default_mock_home, hmip_config_entry +): + """Create a fake homematic access point.""" + hass.config.components.add(HMIPC_DOMAIN) + hap = hmip_hap.HomematicipHAP(hass, hmip_config_entry) + with patch.object(hap, "get_hap", return_value=mock_coro(default_mock_home)): + assert await hap.async_setup() is True + + hass.data[HMIPC_DOMAIN] = {HAPID: hap} + + await hass.async_block_till_done() + + return hap diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py new file mode 100644 index 00000000000000..79a5bc0b201d36 --- /dev/null +++ b/tests/components/homematicip_cloud/helper.py @@ -0,0 +1,128 @@ +"""Helper for HomematicIP Cloud Tests.""" +import json +from unittest.mock import Mock + +from homematicip.aio.class_maps import ( + TYPE_CLASS_MAP, + TYPE_GROUP_MAP, + TYPE_SECURITY_EVENT_MAP, +) +from homematicip.aio.home import AsyncHome +from homematicip.home import Home + +from tests.common import load_fixture + +HAPID = "Mock_HAP" +AUTH_TOKEN = "1234" +HOME_JSON = "homematicip_cloud.json" + + +def get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model +): + """Get and test basic device.""" + ha_entity = hass.states.get(entity_id) + assert ha_entity is not None + assert ha_entity.attributes["model_type"] == device_model + assert ha_entity.name == entity_name + + hmip_device = default_mock_hap.home.template.search_mock_device_by_id( + ha_entity.attributes["id"] + ) + assert hmip_device is not None + return ha_entity, hmip_device + + +async def async_manipulate_test_data( + hass, hmip_device, attribute, new_value, channel=1 +): + """Set new value on hmip device.""" + if channel == 1: + setattr(hmip_device, attribute, new_value) + functional_channel = hmip_device.functionalChannels[channel] + setattr(functional_channel, attribute, new_value) + + hmip_device.fire_update_event() + await hass.async_block_till_done() + + +class HomeTemplate(Home): + """ + Home template as builder for home mock. + + It is based on the upstream libs home class to generate hmip devices + and groups based on the given homematicip_cloud.json. + + All further testing activities should be done by using the AsyncHome mock, + that is generated by get_async_home_mock(self). + + The class also generated mocks of devices and groups for further testing. + """ + + _typeClassMap = TYPE_CLASS_MAP + _typeGroupMap = TYPE_GROUP_MAP + _typeSecurityEventMap = TYPE_SECURITY_EVENT_MAP + + def __init__(self, connection=None): + """Init template with connection.""" + super().__init__(connection=connection) + self.mock_devices = [] + self.mock_groups = [] + + def init_home(self, json_path=HOME_JSON): + """Init template with json.""" + json_state = json.loads(load_fixture(HOME_JSON), encoding="UTF-8") + self.update_home(json_state=json_state, clearConfig=True) + self._generate_mocks() + return self + + def _generate_mocks(self): + """Generate mocks for groups and devices.""" + for device in self.devices: + self.mock_devices.append(_get_mock(device)) + for group in self.groups: + self.mock_groups.append(_get_mock(group)) + + def search_mock_device_by_id(self, device_id): + """Search a device by given id.""" + for device in self.mock_devices: + if device.id == device_id: + return device + return None + + def search_mock_group_by_id(self, group_id): + """Search a group by given id.""" + for group in self.mock_groups: + if group.id == group_id: + return group + return None + + def get_async_home_mock(self): + """ + Create Mock for Async_Home. based on template to be used for testing. + + It adds collections of mocked devices and groups to the home objects, + and sets reuired attributes. + """ + mock_home = Mock( + check_connection=self._connection, + id=HAPID, + connected=True, + dutyCycle=self.dutyCycle, + devices=self.mock_devices, + groups=self.mock_groups, + weather=self.weather, + location=self.location, + label="home label", + template=self, + spec=AsyncHome, + ) + mock_home.name = "" + return mock_home + + +def _get_mock(instance): + """Create a mock and copy instance attributes over mock.""" + mock = Mock(spec=instance, wraps=instance) + mock.__dict__.update(instance.__dict__) + return mock diff --git a/tests/components/homematicip_cloud/test_binary_sensors.py b/tests/components/homematicip_cloud/test_binary_sensors.py new file mode 100644 index 00000000000000..4471c5dd7f3d8d --- /dev/null +++ b/tests/components/homematicip_cloud/test_binary_sensors.py @@ -0,0 +1,41 @@ +"""Tests for HomematicIP Cloud lights.""" +import logging + +from tests.components.homematicip_cloud.helper import ( + async_manipulate_test_data, + get_and_check_entity_basics, +) + +_LOGGER = logging.getLogger(__name__) + + +async def test_hmip_sam(hass, default_mock_hap): + """Test HomematicipLight.""" + entity_id = "binary_sensor.garagentor" + entity_name = "Garagentor" + device_model = "HmIP-SAM" + + ha_entity, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_entity.state == "on" + assert ha_entity.attributes["acceleration_sensor_mode"] == "FLAT_DECT" + assert ha_entity.attributes["acceleration_sensor_neutral_position"] == "VERTICAL" + assert ha_entity.attributes["acceleration_sensor_sensitivity"] == "SENSOR_RANGE_4G" + assert ha_entity.attributes["acceleration_sensor_trigger_angle"] == 45 + service_call_counter = len(hmip_device.mock_calls) + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", False + ) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "off" + assert len(hmip_device.mock_calls) == service_call_counter + 1 + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", True + ) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "on" + assert len(hmip_device.mock_calls) == service_call_counter + 2 diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index c1bad8557012bb..54cb309755db79 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,8 +1,7 @@ """Tests for HomematicIP Cloud config flow.""" from unittest.mock import patch -from homeassistant.components.homematicip_cloud import hap as hmipc -from homeassistant.components.homematicip_cloud import config_flow, const +from homeassistant.components.homematicip_cloud import config_flow, const, hap as hmipc from tests.common import MockConfigEntry, mock_coro diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 34afd19310fd4a..cd8ead40c438e0 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -3,9 +3,9 @@ import pytest +from homeassistant.components.homematicip_cloud import const, errors, hap as hmipc from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.components.homematicip_cloud import hap as hmipc -from homeassistant.components.homematicip_cloud import const, errors + from tests.common import mock_coro, mock_coro_func @@ -94,8 +94,8 @@ async def test_hap_setup_connection_error(): ), pytest.raises(ConfigEntryNotReady): await hap.async_setup() - assert len(hass.async_add_job.mock_calls) == 0 - assert len(hass.config_entries.flow.async_init.mock_calls) == 0 + assert not hass.async_add_job.mock_calls + assert not hass.config_entries.flow.async_init.mock_calls async def test_hap_reset_unloads_entry_if_setup(): @@ -114,7 +114,7 @@ async def test_hap_reset_unloads_entry_if_setup(): assert await hap.async_setup() is True assert hap.home is home - assert len(hass.services.async_register.mock_calls) == 0 + assert not hass.services.async_register.mock_calls assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8 hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index d77d4a7e5b26a3..894db2e691b2df 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -2,10 +2,10 @@ from unittest.mock import patch -from homeassistant.setup import async_setup_component from homeassistant.components import homematicip_cloud as hmipc +from homeassistant.setup import async_setup_component -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_config_with_accesspoint_passed_to_config_entry(hass): @@ -53,7 +53,7 @@ async def test_config_already_registered_not_passed_to_config_entry(hass): ) # No flow started - assert len(mock_config_entries.flow.mock_calls) == 0 + assert not mock_config_entries.flow.mock_calls async def test_setup_entry_successful(hass): diff --git a/tests/components/homematicip_cloud/test_lights.py b/tests/components/homematicip_cloud/test_lights.py new file mode 100644 index 00000000000000..dcf5f76d0a0f4c --- /dev/null +++ b/tests/components/homematicip_cloud/test_lights.py @@ -0,0 +1,77 @@ +"""Tests for HomematicIP Cloud lights.""" +import logging + +from tests.components.homematicip_cloud.helper import ( + async_manipulate_test_data, + get_and_check_entity_basics, +) + +_LOGGER = logging.getLogger(__name__) + + +async def test_hmip_light(hass, default_mock_hap): + """Test HomematicipLight.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_entity, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_entity.state == "on" + + service_call_counter = len(hmip_device.mock_calls) + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "off" + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "on" + + +# HomematicipLightMeasuring +# HomematicipDimmer + + +async def test_hmip_notification_light(hass, default_mock_hap): + """Test HomematicipNotificationLight.""" + entity_id = "light.treppe_top_notification" + entity_name = "Treppe Top Notification" + device_model = "HmIP-BSL" + + ha_entity, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_entity.state == "off" + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 100, 2) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "on" + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, 2) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "off" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json new file mode 100644 index 00000000000000..b96bff8fac9fb9 --- /dev/null +++ b/tests/fixtures/homematicip_cloud.json @@ -0,0 +1,5341 @@ +{ + "clients": { + "00000000-0000-0000-0000-000000000000": { + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000000", + "label": "TEST-Client", + "clientType": "APP" + } + }, + "devices": { + "3014F7110000000000000031": { + "availableFirmwareVersion": "1.2.1", + "firmwareVersion": "1.2.1", + "firmwareVersionInteger": 66049, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000031", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -88, + "rssiPeerValue": null, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "accelerationSensorEventFilterPeriod": 3.0, + "accelerationSensorMode": "FLAT_DECT", + "accelerationSensorNeutralPosition": "VERTICAL", + "accelerationSensorSensitivity": "SENSOR_RANGE_4G", + "accelerationSensorTriggerAngle": 45, + "accelerationSensorTriggered": true, + "deviceId": "3014F7110000000000000031", + "functionalChannelType": "ACCELERATION_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "notificationSoundTypeHighToLow": "SOUND_LONG", + "notificationSoundTypeLowToHigh": "SOUND_LONG" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000031", + "label": "Garagentor", + "lastStatusUpdate": 1567850423788, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 315, + "modelType": "HmIP-SAM", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000031", + "type": "ACCELERATION_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000052": { + "availableFirmwareVersion": "1.0.5", + "firmwareVersion": "1.0.5", + "firmwareVersionInteger": 65541, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000052", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": null, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 8, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000052", + "label": "Alarm-Melder", + "lastStatusUpdate": 1564733931898, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 336, + "modelType": "HmIP-MOD-RC8", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000052", + "type": "REMOTE_CONTROL_8_MODULE", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000FAL24C10": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "coolingEmergencyValue": 0.0, + "deviceId": "3014F71100000000FAL24C10", + "dutyCycle": false, + "frostProtectionTemperature": 8.0, + "functionalChannelType": "DEVICE_GLOBAL_PUMP_CONTROL", + "globalPumpControl": true, + "groupIndex": 0, + "groups": [ + ], + "heatingEmergencyValue": 0.25, + "heatingLoadType": "LOAD_BALANCING", + "heatingValveType": "NORMALLY_CLOSE", + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": -74, + "unreach": false, + "valveProtectionDuration": 5, + "valveProtectionSwitchingInterval": 14 + }, + "1": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_LOCAL_PUMP_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "pumpFollowUpTime": 2, + "pumpLeadTime": 2, + "pumpProtectionDuration": 1, + "pumpProtectionSwitchingInterval": 14 + }, + "10": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 10, + "groups": [ + ], + "index": 10, + "label": "" + }, + "11": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "HEAT_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + ], + "index": 11, + "label": "" + }, + "12": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "DEHUMIDIFIER_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + ], + "index": 12, + "label": "" + }, + "2": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 4, + "groups": [ + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 5, + "groups": [ + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 6, + "groups": [ + ], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 7, + "groups": [ + ], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 8, + "groups": [ + ], + "index": 8, + "label": "" + }, + "9": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 9, + "groups": [ + ], + "index": 9, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000FAL24C10", + "label": "Fu\u00dfbodenheizungsaktor", + "lastStatusUpdate": 1558461135830, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 280, + "modelType": "HmIP-FAL24-C10", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000FAL24C10", + "type": "FLOOR_TERMINAL_BLOCK_10", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000000BBL24": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000000BBL24", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000034" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -64, + "rssiPeerValue": -76, + "unreach": false + }, + "1": { + "blindModeActive": true, + "bottomToTopReferenceTime": 54.88, + "changeOverDelay": 0.5, + "delayCompensationValue": 12.7, + "deviceId": "3014F71100000000000BBL24", + "endpositionAutoDetectionEnabled": true, + "functionalChannelType": "BLIND_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 0.885, + "slatsLevel": 1.0, + "slatsReferenceTime": 1.6, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": true, + "supportingSelfCalibration": true, + "topToBottomReferenceTime": 53.68, + "userDesiredProfileMode": "MANUAL" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000000BBL24", + "label": "Jalousie Schiebet\u00fcr", + "lastStatusUpdate": 1558464454532, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 332, + "modelType": "HmIP-BBL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000000BBL24", + "type": "BRAND_BLIND", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000BCBB11": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.10.10", + "firmwareVersionInteger": 68106, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000BCBB11", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -53, + "rssiPeerValue": -56, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000BCBB11", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F7110000000000BCBB11", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000038" + ], + "index": 2, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000BCBB11", + "label": "Jalousien - 1 KiZi, 2 SchlaZi", + "lastStatusUpdate": 1555621612744, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 357, + "modelType": "HmIP-PCBS2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000BCBB11", + "type": "PRINTED_CIRCUIT_BOARD_SWITCH_2", + "updateState": "UP_TO_DATE" + }, + "3014F711ABCD0ABCD000002": { + "availableFirmwareVersion": "1.6.4", + "firmwareVersion": "1.6.4", + "firmwareVersionInteger": 67076, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711ABCD0ABCD000002", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000027" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -79, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "on": true, + "profileMode": null, + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 2, + "label": "", + "on": false, + "profileMode": null, + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "GENERIC_INPUT_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "GENERIC_INPUT_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 4, + "label": "" + }, + "5": { + "analogOutputLevel": 12.5, + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "ANALOG_OUTPUT_CHANNEL", + "groupIndex": 0, + "groups": [], + "index": 5, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711ABCD0ABCD000002", + "label": "Multi IO Box", + "lastStatusUpdate": 1552508702220, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 283, + "modelType": "HmIP-MIOB", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711ABCD0ABCD000002", + "type": "MULTI_IO_BOX", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000ABCDEF10": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000ABCDEF10", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -47, + "rssiPeerValue": -50, + "sabotage": null, + "unreach": false + }, + "1": { + "deviceId": "3014F71100000000ABCDEF10", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000013" + ], + "index": 1, + "label": "", + "setPointTemperature": 21.0, + "temperatureOffset": 0.0, + "valveActualTemperature": 21.6, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000ABCDEF10", + "label": "Wohnzimmer 3", + "lastStatusUpdate": 1550912664486, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 325, + "modelType": "HmIP-eTRV-C", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000ABCDEF10", + "type": "HEATING_THERMOSTAT_COMPACT", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000000TEST1": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.8", + "firmwareVersionInteger": 67592, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000000TEST1", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -51, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F71100000000000TEST1", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F71100000000000TEST1", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000000TEST1", + "label": "Remote", + "lastStatusUpdate": 1550512733995, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 358, + "modelType": "HmIP-BRC2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000000TEST1", + "type": "BRAND_PUSH_BUTTON", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000064": { + "availableFirmwareVersion": "1.0.6", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000064", + "deviceOverheated": true, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000032", + "00000000-0000-0000-0000-000000000013" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -42, + "rssiPeerValue": null, + "sabotage": false, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "alarmContactType": "WINDOW_DOOR_CONTACT", + "contactType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000000064", + "eventDelay": 0, + "functionalChannelType": "CONTACT_INTERFACE_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000033", + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000013" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000064", + "label": "Schlie\u00dfer Magnet", + "lastStatusUpdate": 1524515854304, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 375, + "modelType": "HmIP-SCI", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000064", + "type": "SHUTTER_CONTACT_INTERFACE", + "updateState": "UP_TO_DATE" + }, + "3014F711BADCAFE000000001": { + "availableFirmwareVersion": "1.2.0", + "firmwareVersion": "1.2.0", + "firmwareVersionInteger": 66048, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BADCAFE000000001", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": -78, + "unreach": false + }, + "1": { + "blindModeActive": true, + "bottomToTopReferenceTime": 41.0, + "changeOverDelay": 0.5, + "delayCompensationValue": 1.0, + "deviceId": "3014F711BADCAFE000000001", + "endpositionAutoDetectionEnabled": false, + "functionalChannelType": "BLIND_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 1.0, + "slatsLevel": 1.0, + "slatsReferenceTime": 2.0, + "supportingDelayCompensation": false, + "supportingEndpositionAutoDetection": false, + "supportingSelfCalibration": false, + "topToBottomReferenceTime": 41.0, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BADCAFE000000001", + "label": "Sofa links", + "lastStatusUpdate": 1548616026922, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 333, + "modelType": "HmIP-FBL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711BADCAFE000000001", + "type": "FULL_FLUSH_BLIND", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000055": { + "availableFirmwareVersion": "1.2.4", + "firmwareVersion": "1.2.4", + "firmwareVersionInteger": 66052, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000055", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000034" + ], + "index": 0, + "label": "", + "lowBat": null, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": -77, + "unreach": false + }, + "1": { + "actualTemperature": 21.0, + "deviceId": "3014F7110000000000000055", + "display": "SETPOINT", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000035" + ], + "humidity": 40, + "index": 1, + "label": "", + "vaporAmount": 6.177718198711658, + "valveActualTemperature": 20.0, + "setPointTemperature": 21.5, + "temperatureOffset": 0.0 + }, + "2": { + "deviceId": "3014F7110000000000000055", + "frostProtectionTemperature": 8.0, + "functionalChannelType": "INTERNAL_SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000035" + ], + "heatingValveType": "NORMALLY_CLOSE", + "index": 2, + "internalSwitchOutputEnabled": true, + "label": "", + "valveProtectionDuration": 5, + "valveProtectionSwitchingInterval": 14 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000055", + "label": "BWTH 1", + "lastStatusUpdate": 1547283716818, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 305, + "modelType": "HmIP-BWTH", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000055", + "type": "BRAND_WALL_MOUNTED_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F711ABCDEF0000000014": { + "availableFirmwareVersion": "1.4.2", + "firmwareVersion": "1.4.2", + "firmwareVersionInteger": 66562, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711ABCDEF0000000014", + "dutyCycle": null, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000033" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": null, + "rssiPeerValue": null, + "unreach": null + }, + "1": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 4, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711ABCDEF0000000014", + "label": "FFB 1", + "lastStatusUpdate": 0, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 266, + "modelType": "HmIP-KRC4", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711ABCDEF0000000014", + "type": "KEY_REMOTE_CONTROL_4", + "updateState": "UP_TO_DATE" + }, + "3014F711BSL0000000000050": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.2", + "firmwareVersionInteger": 65538, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BSL0000000000050", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -67, + "rssiPeerValue": -70, + "unreach": false + }, + "1": { + "deviceId": "3014F711BSL0000000000050", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F711BSL0000000000050", + "dimLevel": 0.0, + "functionalChannelType": "NOTIFICATION_LIGHT_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 2, + "label": "", + "on": null, + "profileMode": "AUTOMATIC", + "simpleRGBColorState": "RED", + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "deviceId": "3014F711BSL0000000000050", + "dimLevel": 1.0, + "functionalChannelType": "NOTIFICATION_LIGHT_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 3, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "simpleRGBColorState": "GREEN", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BSL0000000000050", + "label": "Treppe", + "lastStatusUpdate": 1548431183264, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 360, + "modelType": "HmIP-BSL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711BSL0000000000050", + "type": "BRAND_SWITCH_NOTIFICATION_LIGHT", + "updateState": "UP_TO_DATE" + }, + "3014F711SLO0000000000026": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.16", + "firmwareVersionInteger": 65552, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711SLO0000000000026", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -60, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "averageIllumination": 807.3, + "currentIllumination": 785.2, + "deviceId": "3014F711SLO0000000000026", + "functionalChannelType": "LIGHT_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [], + "highestIllumination": 837.1, + "index": 1, + "label": "", + "lowestIllumination": 785.2 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711SLO0000000000026", + "label": "Lichtsensor Nord", + "lastStatusUpdate": 1548494235548, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 308, + "modelType": "HmIP-SLO", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711SLO0000000000026", + "type": "LIGHT_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000054": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.0", + "firmwareVersionInteger": 65536, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000054", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000053" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000054", + "functionalChannelType": "PASSAGE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000055" + ], + "index": 1, + "label": "", + "leftCounter": 966, + "leftRightCounterDelta": 164, + "passageBlindtime": 1.5, + "passageDirection": "LEFT", + "passageSensorSensitivity": 50.0, + "passageTimeout": 0.5, + "rightCounter": 802 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000054", + "label": "SPDR_1", + "lastStatusUpdate": 1547282742305, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 304, + "modelType": "HmIP-SPDR", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000054", + "type": "PASSAGE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F711000000000AAAAA25": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.12", + "firmwareVersionInteger": 65548, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711000000000AAAAA25", + "dutyCycle": false, + "functionalChannelType": "DEVICE_PERMANENT_FULL_RX", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 0, + "label": "", + "lowBat": false, + "permanentFullRx": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711000000000AAAAA25", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000048", + "00000000-0000-0000-0000-000000000034" + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711000000000AAAAA25", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000048", + "00000000-0000-0000-0000-000000000034" + ], + "index": 2, + "label": "" + }, + "3": { + "currentIllumination": null, + "deviceId": "3014F711000000000AAAAA25", + "functionalChannelType": "MOTION_DETECTION_CHANNEL", + "groupIndex": 2, + "groups": [], + "illumination": 14.2, + "index": 3, + "label": "", + "motionBufferActive": true, + "motionDetected": false, + "motionDetectionSendInterval": "SECONDS_240", + "numberOfBrightnessMeasurements": 7 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000000000AAAAA25", + "label": "Bewegungsmelder für 55er Rahmen – innen", + "lastStatusUpdate": 1546776387401, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 338, + "modelType": "HmIP-SMI55", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711000000000AAAAA25", + "type": "MOTION_DETECTOR_PUSH_BUTTON", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000038": { + "availableFirmwareVersion": "1.0.18", + "firmwareVersion": "1.0.18", + "firmwareVersionInteger": 65554, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000038", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -55, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 4.3, + "deviceId": "3014F7110000000000000038", + "functionalChannelType": "WEATHER_SENSOR_PLUS_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "humidity": 97, + "vaporAmount": 6.177718198711658, + "illumination": 26.4, + "illuminationThresholdSunshine": 3500.0, + "index": 1, + "label": "", + "raining": false, + "storm": false, + "sunshine": false, + "todayRainCounter": 3.8999999999999773, + "todaySunshineDuration": 0, + "totalRainCounter": 544.0999999999999, + "totalSunshineDuration": 132057, + "windSpeed": 15.0, + "windValueType": "CURRENT_VALUE", + "yesterdayRainCounter": 25.600000000000023, + "yesterdaySunshineDuration": 0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000038", + "label": "Weather Sensor – plus", + "lastStatusUpdate": 1546789939739, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 351, + "modelType": "HmIP-SWO-PL", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000038", + "type": "WEATHER_SENSOR_PLUS", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000BBBBB1": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "coolingEmergencyValue": 0.0, + "deviceId": "3014F7110000000000BBBBB1", + "dutyCycle": false, + "frostProtectionTemperature": 8.0, + "functionalChannelType": "DEVICE_GLOBAL_PUMP_CONTROL", + "globalPumpControl": true, + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "heatingEmergencyValue": 0.25, + "heatingLoadType": "LOAD_BALANCING", + "heatingValveType": "NORMALLY_CLOSE", + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -62, + "rssiPeerValue": null, + "unreach": false, + "valveProtectionDuration": 5, + "valveProtectionSwitchingInterval": 14 + }, + "1": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_LOCAL_PUMP_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "pumpFollowUpTime": 2, + "pumpLeadTime": 2, + "pumpProtectionDuration": 1, + "pumpProtectionSwitchingInterval": 14 + }, + "2": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 3, + "groups": [ + "00000000-0000-0000-0000-000000000009" + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 4, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 5, + "groups": [ + "00000000-0000-0000-0000-000000000011" + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 6, + "groups": [], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "HEAT_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013" + ], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "DEHUMIDIFIER_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014" + ], + "index": 8, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000BBBBB1", + "label": "Fußbodenheizungsaktor", + "lastStatusUpdate": 1545746610807, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 277, + "modelType": "HmIP-FAL230-C6", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000BBBBB1", + "type": "FLOOR_TERMINAL_BLOCK_6", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000BBBBB8": { + "availableFirmwareVersion": "1.2.16", + "firmwareVersion": "1.2.16", + "firmwareVersionInteger": 66064, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000BBBBB8", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -59, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000BBBBB8", + "functionalChannelType": "ALARM_SIREN_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000BBBBB8", + "label": "Alarmsirene", + "lastStatusUpdate": 1544480290322, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 298, + "modelType": "HmIP-ASIR", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000BBBBB8", + "type": "ALARM_SIREN_INDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F711000000000000BB11": { + "availableFirmwareVersion": "1.4.8", + "firmwareVersion": "1.4.8", + "firmwareVersionInteger": 66568, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711000000000000BB11", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -56, + "rssiPeerValue": -52, + "sabotage": false, + "unreach": false + }, + "1": { + "currentIllumination": null, + "deviceId": "3014F711000000000000BB11", + "functionalChannelType": "MOTION_DETECTION_CHANNEL", + "groupIndex": 1, + "groups": [], + "illumination": 0.1, + "index": 1, + "label": "", + "motionBufferActive": false, + "motionDetected": true, + "motionDetectionSendInterval": "SECONDS_480", + "numberOfBrightnessMeasurements": 7 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000000000000BB11", + "label": "Wohnzimmer", + "lastStatusUpdate": 1544480290322, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 291, + "modelType": "HmIP-SMI", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000011", + "type": "MOTION_DETECTOR_INDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000000BBB17": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.2", + "firmwareVersionInteger": 65538, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000000BBB17", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -70, + "rssiPeerValue": -67, + "unreach": false + }, + "1": { + "currentIllumination": null, + "deviceId": "3014F71100000000000BBB17", + "functionalChannelType": "MOTION_DETECTION_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "illumination": 233.4, + "index": 1, + "label": "", + "motionBufferActive": true, + "motionDetected": true, + "motionDetectionSendInterval": "SECONDS_240", + "numberOfBrightnessMeasurements": 7 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000000BBB17", + "label": "Außen Küche", + "lastStatusUpdate": 1546776559553, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 302, + "modelType": "HmIP-SMO-A", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000000BBB17", + "type": "MOTION_DETECTOR_OUTDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000050": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.2", + "firmwareVersionInteger": "65538", + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000050", + "dutyCycle": false, + "functionalChannelType": "DEVICE_INCORRECT_POSITIONED", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "incorrectPositioned": true, + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -65, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "acousticAlarmSignal": "FREQUENCY_RISING", + "acousticAlarmTiming": "ONCE_PER_MINUTE", + "acousticWaterAlarmTrigger": "WATER_DETECTION", + "deviceId": "3014F7110000000000000050", + "functionalChannelType": "WATER_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000023" + ], + "inAppWaterAlarmTrigger": "WATER_MOISTURE_DETECTION", + "index": 1, + "label": "", + "moistureDetected": false, + "sirenWaterAlarmTrigger": "WATER_MOISTURE_DETECTION", + "waterlevelDetected": false + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000050", + "label": "Wassersensor", + "lastStatusUpdate": 1530802738493, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 353, + "modelType": "HmIP-SWD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000050", + "type": "WATER_SENSOR", + "updateState": "UP_TO_DATE" + + }, + "3014F7110000000000000000": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000000", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -85, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000000", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "OPEN" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000000", + "label": "Balkontüre", + "lastStatusUpdate": 1524516526498, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000000", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000005551": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.2.12", + "firmwareVersionInteger": 66060, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000005551", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000005551", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000005551", + "label": "Eingangst\u00fcrkontakt", + "lastStatusUpdate": 1524515854304, + "liveUpdateState": "UP_TO_DATE", + "manufacturerCode": 1, + "modelId": 340, + "modelType": "HmIP-SWDM", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000005551", + "type": "SHUTTER_CONTACT_MAGNETIC", + "updateState": "BACKGROUND_UPDATE_NOT_SUPPORTED" + }, + "3014F7110000000000000001": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000001", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -64, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000001", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000009", + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000001", + "label": "Fenster", + "lastStatusUpdate": 1524515854304, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000001", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000002": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000002", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -95, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000002", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000002", + "label": "Balkonfenster", + "lastStatusUpdate": 1524516088763, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000002", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000003": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000003", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -78, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000003", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000003", + "label": "Küche", + "lastStatusUpdate": 1524514836466, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000003", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000004": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000004", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -56, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000004", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "OPEN" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000004", + "label": "Fenster", + "lastStatusUpdate": 1524512404032, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000004", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000005": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000005", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -80, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000005", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "OPEN" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000005", + "label": "Wohnzimmer", + "lastStatusUpdate": 0, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000005", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000006": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000006", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000006", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000015", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000006", + "label": "Wohnungstüre", + "lastStatusUpdate": 1524516489316, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000006", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000007": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000007", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -56, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000007", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000016", + "00000000-0000-0000-0000-000000000015" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000007", + "label": "Vorzimmer", + "lastStatusUpdate": 1524515489257, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000007", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000008": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000008", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -48, + "rssiPeerValue": -49, + "unreach": false + }, + "1": { + "currentPowerConsumption": 195.3, + "deviceId": "3014F7110000000000000008", + "energyCounter": 35.536, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000008", + "label": "Pc", + "lastStatusUpdate": 1524516554056, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000008", + "type": "PLUGABLE_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000009": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000009", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -60, + "rssiPeerValue": -66, + "unreach": false + }, + "1": { + "currentPowerConsumption": 0.0, + "deviceId": "3014F7110000000000000009", + "energyCounter": 0.4754, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000009", + "label": "Brunnen", + "lastStatusUpdate": 1524515786303, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000009", + "type": "PLUGABLE_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000010": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000010", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -47, + "rssiPeerValue": -49, + "unreach": false + }, + "1": { + "currentPowerConsumption": 2.04, + "deviceId": "3014F7110000000000000010", + "energyCounter": 1.5343, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000010", + "label": "Büro", + "lastStatusUpdate": 1524513613922, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000010", + "type": "PLUGABLE_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000011": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000011", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -54, + "rssiPeerValue": -51, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000011", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000012" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000011", + "label": "Heizung", + "lastStatusUpdate": 1524516360178, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000011", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000012": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000012", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": -54, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000012", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 19.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000012", + "label": "Heizkörperthermostat", + "lastStatusUpdate": 1524514105832, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000012", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000013": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000013", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -58, + "rssiPeerValue": -58, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000013", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000019" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000013", + "label": "Heizkörperthermostat", + "lastStatusUpdate": 1524514007132, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000013", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000014": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000014", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -60, + "rssiPeerValue": -58, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000014", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000014", + "label": "Küche-Heizung", + "lastStatusUpdate": 1524513898337, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000014", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000015": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000015", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -65, + "rssiPeerValue": -66, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000015", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000015", + "label": "Wohnzimmer-Heizung", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000015", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000016": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000016", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -50, + "rssiPeerValue": -51, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000016", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000021" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000016", + "label": "Heizkörperthermostat", + "lastStatusUpdate": 1524514626157, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000016", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000017": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000017", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -67, + "rssiPeerValue": -62, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000017", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000017", + "label": "Balkon-Heizung", + "lastStatusUpdate": 1524511331830, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000017", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000018": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000018", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -67, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000018", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000006" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000018", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524461072721, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000018", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000019": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000019", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -50, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000019", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000009" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000019", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524480981494, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000019", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000020": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000020", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -54, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000020", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000013", + "00000000-0000-0000-0000-000000000022" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000020", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524456324824, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000020", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000021": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000021", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -80, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000021", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000015" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000021", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524443129876, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000021", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000022": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000022", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": -63, + "unreach": false + }, + "1": { + "actualTemperature": 24.7, + "deviceId": "3014F7110000000000000022", + "display": "ACTUAL_HUMIDITY", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000012" + ], + "humidity": 43, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000022", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516534382, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000022", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000023": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000023", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -61, + "rssiPeerValue": -58, + "unreach": false + }, + "1": { + "actualTemperature": 24.5, + "deviceId": "3014F7110000000000000023", + "display": "ACTUAL_HUMIDITY", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "humidity": 46, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 19.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000023", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516454116, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000023", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000024": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000024", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -75, + "rssiPeerValue": -85, + "unreach": false + }, + "1": { + "actualTemperature": 23.6, + "deviceId": "3014F7110000000000000024", + "display": "ACTUAL", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "humidity": 45, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000024", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516436601, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000024", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000025": { + "availableFirmwareVersion": "1.8.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000025", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": -47, + "unreach": false + }, + "1": { + "actualTemperature": 23.8, + "deviceId": "3014F7110000000000000025", + "display": "ACTUAL_HUMIDITY", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000021" + ], + "humidity": 47, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000025", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516556479, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000025", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000029": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.14", + "firmwareVersionInteger": 65550, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000029", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000019" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000000029", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "index": 1, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000029", + "label": "Kontakt-Schnittstelle Unterputz – 1-fach", + "lastStatusUpdate": 1547923306429, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 382, + "modelType": "HmIP-FCI1", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000029", + "type": "FULL_FLUSH_CONTACT_INTERFACE", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000001": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.10", + "firmwareVersionInteger": 65546, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000001", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -68, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 15.4, + "deviceId": "3014F711AAAA000000000001", + "functionalChannelType": "WEATHER_SENSOR_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-AAAA-0000-0000-000000000001" + ], + "humidity": 65, + "vaporAmount": 6.177718198711658, + "illumination": 4153.0, + "illuminationThresholdSunshine": 10.0, + "index": 1, + "label": "", + "raining": false, + "storm": false, + "sunshine": true, + "todayRainCounter": 6.5, + "todaySunshineDuration": 100, + "totalRainCounter": 6.5, + "totalSunshineDuration": 100, + "weathervaneAlignmentNeeded": false, + "windDirection": 295.0, + "windDirectionVariation": 56.25, + "windSpeed": 2.6, + "windValueType": "AVERAGE_VALUE", + "yesterdayRainCounter": 0.0, + "yesterdaySunshineDuration": 0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000001", + "label": "Wettersensor - pro", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 352, + "modelType": "HmIP-SWO-PR", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000001", + "type": "WEATHER_SENSOR_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000002": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000002", + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -55, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 15.1, + "deviceId": "3014F711AAAA000000000002", + "functionalChannelType": "CLIMATE_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-AAAA-0000-0000-000000000001" + ], + "humidity": 70, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000002", + "label": "Temperatur- und Luftfeuchtigkeitssensor - außen", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 314, + "modelType": "HmIP-STHO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000002", + "type": "TEMPERATURE_HUMIDITY_SENSOR_OUTDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000003": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.10", + "firmwareVersionInteger": 65546, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000003", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ "00000000-0000-0000-0000-000000000008" ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -77, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 15.2, + "deviceId": "3014F711AAAA000000000003", + "functionalChannelType": "WEATHER_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [ "00000000-AAAA-0000-0000-000000000001" ], + "humidity": 42, + "vaporAmount": 6.177718198711658, + "illumination": 4890.0, + "illuminationThresholdSunshine": 3500.0, + "index": 1, + "label": "", + "storm": false, + "sunshine": true, + "todaySunshineDuration": 51, + "totalSunshineDuration": 54, + "windSpeed": 6.6, + "windValueType": "MAX_VALUE", + "yesterdaySunshineDuration": 3 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000003", + "label": "Wettersensor", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 350, + "modelType": "HmIP-SWO-B", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000003", + "type": "WEATHER_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000004": { + "availableFirmwareVersion": "1.2.10", + "firmwareVersion": "1.2.10", + "firmwareVersionInteger": 66058, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000004", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ "00000000-0000-0000-0000-000000000008" ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -54, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F711AAAA000000000004", + "eventDelay": 0, + "functionalChannelType": "ROTARY_HANDLE_CHANNEL", + "groupIndex": 1, + "groups": [ "00000000-0000-0000-0000-000000000009" ], + "index": 1, + "label": "", + "windowState": "TILTED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000004", + "label": "Fenstergriffsensor", + "lastStatusUpdate": 1524816385462, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 286, + "modelType": "HmIP-SRH", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000004", + "type": "ROTARY_HANDLE_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000005": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.4.8", + "firmwareVersionInteger": 66568, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000005", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -44, + "rssiPeerValue": -42, + "unreach": false + }, + "1": { + "deviceId": "3014F711AAAA000000000005", + "dimLevel": 0.0, + "functionalChannelType": "DIMMER_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 1, + "label": "", + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000005", + "label": "Schlafzimmerlicht", + "lastStatusUpdate": 1524816385462, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 290, + "modelType": "HmIP-BDT", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000005", + "type": "BRAND_DIMMER", + "updateState": "UP_TO_DATE" + }, + "3014F711BBBBBBBBBBBBB017": { + "availableFirmwareVersion": "1.0.19", + "firmwareVersion": "1.0.19", + "firmwareVersionInteger": 65555, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BBBBBBBBBBBBB017", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -61, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 6, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BBBBBBBBBBBBB017", + "label": "Wandtaster - 6-fach", + "lastStatusUpdate": 1544475961687, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 300, + "modelType": "HmIP-WRC6", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB017", + "type": "PUSH_BUTTON_6", + "updateState": "UP_TO_DATE" + }, + "3014F711BBBBBBBBBBBBB016": { + "availableFirmwareVersion": "1.0.19", + "firmwareVersion": "1.0.19", + "firmwareVersionInteger": 65555, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BBBBBBBBBBBBB016", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -42, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [ + ], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [ + ], + "index": 8, + "label": "" + } + + + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BBBBBBBBBBBBB016", + "label": "Fernbedienung - 8 Tasten", + "lastStatusUpdate": 1544479483638, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 299, + "modelType": "HmIP-RC8", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB016", + "type": "REMOTE_CONTROL_8", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAAAAAAAAAAAA51": { + "availableFirmwareVersion": "1.4.0", + "firmwareVersion": "1.4.0", + "firmwareVersionInteger": 66560, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAAAAAAAAAAAA51", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000021", + "00000000-0000-0000-0000-000000000060" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -62, + "rssiPeerValue": -61, + "sabotage": false, + "unreach": false + }, + "1": { + "currentIllumination": null, + "deviceId": "3014F711AAAAAAAAAAAAAA51", + "functionalChannelType": "PRESENCE_DETECTION_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000060" + ], + "illumination": 1.8, + "index": 1, + "label": "", + "motionBufferActive": false, + "motionDetectionSendInterval": "SECONDS_240", + "numberOfBrightnessMeasurements": 7, + "presenceDetected": false + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAAAAAAAAAAAA51", + "label": "SPI_1", + "lastStatusUpdate": 1542758692234, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 303, + "modelType": "HmIP-SPI", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAAAAAAAAAAAA51", + "type": "PRESENCE_DETECTOR_INDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F711ACBCDABCADCA66": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711ACBCDABCADCA66", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000024" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -78, + "rssiPeerValue": -77, + "unreach": false + }, + "1": { + "bottomToTopReferenceTime": 30.080000000000002, + "changeOverDelay": 0.5, + "delayCompensationValue": 12.7, + "deviceId": "3014F711ACBCDABCADCA66", + "endpositionAutoDetectionEnabled": true, + "functionalChannelType": "SHUTTER_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000069", + "00000000-0000-0000-0000-000000000070" + ], + "index": 1, + "label": "", + "previousShutterLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 1.0, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": true, + "supportingSelfCalibration": true, + "topToBottomReferenceTime": 24.68, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711ACBCDABCADCA66", + "label": "BROLL_1", + "lastStatusUpdate": 1542756558785, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 323, + "modelType": "HmIP-BROLL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711ACBCDABCADCA66", + "type": "BRAND_SHUTTER", + "updateState": "UP_TO_DATE" + }, + "3014F711BBBBBBBBBBBBB18": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.12", + "firmwareVersionInteger": 67596, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BBBBBBBBBBBBB18", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000041" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -35, + "rssiPeerValue": -36, + "unreach": false + }, + "1": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000042", + "00000000-0000-0000-0000-000000000040" + ], + "index": 2, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 3, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 3, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "4": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 4, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 4, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "5": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 5, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 5, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "6": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 6, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 6, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "7": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 7, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 7, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "8": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 8, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 8, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BBBBBBBBBBBBB18", + "label": "ioBroker", + "lastStatusUpdate": 1543746604446, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 307, + "modelType": "HmIP-MOD-OC8", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB18", + "type": "OPEN_COLLECTOR_8_MODULE", + "updateState": "UP_TO_DATE" + } + }, + "groups": { + "00000000-0000-0000-0000-000000000020": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000025" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000016" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000050" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000021" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000020", + "incorrectPositioned": null, + "label": "Badezimmer", + "lastStatusUpdate": 1524516556479, + "lowBat": false, + "metaGroupId": null, + "sabotage": null, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000012": { + "activeProfile": "PROFILE_1", + "actualTemperature": 24.7, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000022" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000011" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 43, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000012", + "label": "Schlafzimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516534382, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000011", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000023", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000024", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000025", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000026", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000027", + "visible": true + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000028", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000016": { + "active": false, + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000021" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000020" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000019" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000018" + } + ], + "configPending": false, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000016", + "ignorableDevices": [], + "label": "INTERNAL", + "lastStatusUpdate": 1524515489257, + "lowBat": false, + "metaGroupId": null, + "motionDetected": null, + "presenceDetected": null, + "sabotage": false, + "silent": true, + "type": "SECURITY_ZONE", + "unreach": false, + "windowState": "CLOSED", + "zoneAssignmentIndex": "ALARM_MODE_ZONE_3" + }, + "00000000-0000-0000-0000-000000000017": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000008" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000009" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000010" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000017", + "incorrectPositioned": null, + "label": "Strom", + "lastStatusUpdate": 1524516554056, + "lowBat": null, + "metaGroupId": null, + "sabotage": null, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000029": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000029", + "label": "HEATING_TEMPERATURE_LIMITER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "HEATING_TEMPERATURE_LIMITER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000030": { + "boilerFollowUpTime": 0, + "boilerLeadTime": 0, + "channels": [], + "dutyCycle": null, + "heatDemand": null, + "heatDemandRuleEnabled": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000030", + "label": "HEATING_COOLING_DEMAND_BOILER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "triggered": false, + "type": "HEATING_COOLING_DEMAND_BOILER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000010": { + "activeProfile": "PROFILE_1", + "actualTemperature": 24.5, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000023" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000012" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 46, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000010", + "label": "Büro", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516454116, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000031", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000032", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000033", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000034", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000035", + "visible": true + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000036", + "visible": false + } + }, + "setPointTemperature": 19.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": "CLOSED" + }, + "00000000-0000-0000-0000-000000000018": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000010" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000009" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000008" + } + ], + "dimLevel": null, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000018", + "label": "Strom", + "lastStatusUpdate": 1524516554056, + "lowBat": null, + "metaGroupId": "00000000-0000-0000-0000-000000000017", + "on": true, + "processing": null, + "shutterLevel": null, + "slatsLevel": null, + "type": "SWITCHING", + "unreach": false + }, + "00000000-0000-0000-0000-000000000009": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000019" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000009", + "label": "Büro", + "lastStatusUpdate": 1524515854304, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "CLOSED" + }, + "00000000-0000-0000-0000-000000000013": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000020" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000013", + "label": "Schlafzimmer", + "lastStatusUpdate": 1524512404032, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000011", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000005": { + "active": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000003" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000003" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000004" + } + ], + "configPending": false, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000005", + "ignorableDevices": [], + "label": "EXTERNAL", + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "metaGroupId": null, + "motionDetected": null, + "presenceDetected": null, + "sabotage": false, + "silent": true, + "type": "SECURITY_ZONE", + "unreach": false, + "windowState": "OPEN", + "zoneAssignmentIndex": "ALARM_MODE_ZONE_2" + }, + "00000000-0000-0000-0000-000000000022": { + "acousticFeedbackEnabled": true, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000020" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000018" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000021" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000019" + } + ], + "dimLevel": null, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000022", + "label": "SIREN", + "lastStatusUpdate": 1524480981494, + "lowBat": false, + "metaGroupId": null, + "on": false, + "onTime": 180.0, + "signalAcoustic": "FREQUENCY_RISING", + "signalOptical": "DOUBLE_FLASHING_REPEATING", + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "ALARM_SWITCHING", + "unreach": false + }, + "00000000-0000-0000-0000-000000000037": { + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000037", + "label": "COMING_HOME", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "LINKED_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-0000-000000000021": { + "activeProfile": "PROFILE_1", + "actualTemperature": 23.8, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000025" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000016" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 47, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000021", + "label": "Badezimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516556479, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000020", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000038", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000039", + "visible": false + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000040", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000041", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000042", + "visible": false + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000043", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, + "00000000-0000-0000-0000-000000000006": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000018" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000003" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000006", + "label": "Wohnzimmer", + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000004", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000044": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000044", + "label": "INBOX", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "INBOX", + "unreach": null + }, + "00000000-0000-0000-0000-000000000045": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000045", + "label": "HEATING_HUMIDITY_LIMITER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "HEATING_HUMIDITY_LIMITER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000008": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000012" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000023" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000019" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000009" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000008", + "incorrectPositioned": null, + "label": "Büro", + "lastStatusUpdate": 1524516454116, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000011": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000022" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000020" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000011" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000011", + "incorrectPositioned": null, + "label": "Schlafzimmer", + "lastStatusUpdate": 1524516534382, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000046": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000046", + "label": "HEATING_CHANGEOVER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "HEATING_CHANGEOVER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000014": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000021" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000013" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000015", + "00000000-0000-0000-0000-000000000019" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000014", + "incorrectPositioned": null, + "label": "Vorzimmer", + "lastStatusUpdate": 1524516489316, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000007": { + "activeProfile": "PROFILE_1", + "actualTemperature": 23.6, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000024" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000017" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000015" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000014" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000003" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 45, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000007", + "label": "Wohnzimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000004", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000047", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000048", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000049", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000050", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000051", + "visible": true + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000052", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000053": { + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000053", + "label": "PANIC", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "LINKED_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-0000-000000000054": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000054", + "label": "HEATING_EXTERNAL_CLOCK", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "HEATING_EXTERNAL_CLOCK", + "unreach": null + }, + "00000000-0000-0000-0000-000000000055": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000055", + "label": "HEATING_DEHUMIDIFIER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "HEATING_DEHUMIDIFIER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000056": { + "acousticFeedbackEnabled": true, + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000056", + "label": "ALARM", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "onTime": 7200.0, + "signalAcoustic": "FREQUENCY_RISING", + "signalOptical": "DOUBLE_FLASHING_REPEATING", + "smokeDetectorAlarmType": null, + "type": "ALARM_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-0000-000000000057": { + "channels": [], + "dutyCycle": null, + "heatDemand": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000057", + "label": "HEATING_COOLING_DEMAND_PUMP", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "pumpFollowUpTime": 2, + "pumpLeadTime": 2, + "pumpProtectionDuration": 1, + "pumpProtectionSwitchingInterval": 14, + "type": "HEATING_COOLING_DEMAND_PUMP", + "unreach": null + }, + "00000000-0000-0000-0000-000000000015": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000021" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000015", + "label": "Vorzimmer", + "lastStatusUpdate": 1524516489316, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "CLOSED" + }, + "00000000-0000-0000-0000-000000000004": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000024" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000014" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000003" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000017" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000015" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000018" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000004", + "incorrectPositioned": null, + "label": "Wohnzimmer", + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000019": { + "activeProfile": "PROFILE_1", + "actualTemperature": null, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000013" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": null, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": null, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000019", + "label": "Vorzimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524514007132, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000058", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000059", + "visible": false + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000060", + "visible": false + }, + "PROFILE_4": { + "enabled": false, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000061", + "visible": true + }, + "PROFILE_5": { + "enabled": false, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000062", + "visible": false + }, + "PROFILE_6": { + "enabled": false, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000063", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, + "00000000-AAAA-0000-0000-000000000001": { + "actualTemperature": 15.4, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F711AAAA000000000003" + }, + { + "channelIndex": 1, + "deviceId": "3014F711AAAA000000000002" + }, + { + "channelIndex": 1, + "deviceId": "3014F711AAAA000000000001" + } + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 65, + "id": "00000000-AAAA-0000-0000-000000000001", + "illumination": 4703.0, + "label": "Terrasse", + "lastStatusUpdate": 1520770214834, + "lowBat": false, + "metaGroupId": "76df95a5-afa5-45ee-b817-f724ffaf04a1", + "raining": false, + "type": "ENVIRONMENT", + "unreach": false, + "windSpeed": 29.1 + }, + "00000000-BBBB-0000-0000-000000000052": { + "channels": [], + "checkInterval": 600, + "dutyCycle": null, + "enabled": true, + "heatingFailureValidationResult": "NO_HEATING_FAILURE", + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-BBBB-0000-0000-000000000052", + "label": "HEATING_FAILURE_ALERT_RULE_GROUP", + "lastExecutionTimestamp": 1550773800084, + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "triggered": false, + "type": "HEATING_FAILURE_ALERT_RULE_GROUP", + "unreach": null, + "validationTimeout": 86400000 + }, + "00000000-AAAA-0000-0000-000000000068": { + "acousticFeedbackEnabled": true, + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-AAAA-0000-0000-000000000068", + "label": "BACKUP_ALARM_SIREN", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "onTime": 180.0, + "signalAcoustic": "FREQUENCY_RISING", + "signalOptical": "DISABLE_OPTICAL_SIGNAL", + "smokeDetectorAlarmType": null, + "type": "SECURITY_BACKUP_ALARM_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-AAAA-000000000029": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000023" + } + ], + "dutyCycle": false, + "enabled": true, + "homeId": "00000000-0000-0000-0000-000000000001", + "humidityLowerThreshold": 40, + "humidityUpperThreshold": 60, + "humidityValidationResult": "LESSER_LOWER_THRESHOLD", + "id": "00000000-0000-0000-AAAA-000000000029", + "label": "B\u00fcro", + "lastExecutionTimestamp": 1551387905665, + "lastStatusUpdate": 1551388104260, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "outdoorClimateSensor": null, + "triggered": false, + "type": "HUMIDITY_WARNING_RULE_GROUP", + "unreach": false, + "ventilationRecommended": true + }, + "00000000-0000-0000-0000-000000000049": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000038" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000023" + } + ], + "dutyCycle": false, + "enabled": true, + "homeId": "00000000-0000-0000-0000-000000000001", + "humidityLowerThreshold": 30, + "humidityUpperThreshold": 60, + "humidityValidationResult": null, + "id": "00000000-0000-0000-0000-000000000049", + "label": "Schlafzimmer", + "lastExecutionTimestamp": 0, + "lastStatusUpdate": 1551003370150, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "outdoorClimateSensor": { + "channelIndex": 1, + "deviceId": "3014F7110000000000000038" + }, + "triggered": false, + "type": "HUMIDITY_WARNING_RULE_GROUP", + "unreach": false, + "ventilationRecommended": false + } + }, + "home": { + "apExchangeClientId": null, + "apExchangeState": "NONE", + "availableAPVersion": null, + "carrierSense": null, + "clients": [ + "00000000-0000-0000-0000-000000000000" + ], + "connected": true, + "currentAPVersion": "1.2.4", + "deviceUpdateStrategy": "AUTOMATICALLY_IF_POSSIBLE", + "dutyCycle": 8.0, + "functionalHomes": { + "INDOOR_CLIMATE": { + "absenceEndTime": null, + "absenceType": "NOT_ABSENT", + "active": true, + "coolingEnabled": false, + "ecoDuration": "PERMANENT", + "ecoTemperature": 17.0, + "floorHeatingSpecificGroups": { + "HEATING_CHANGEOVER": "00000000-0000-0000-0000-000000000046", + "HEATING_COOLING_DEMAND_BOILER": "00000000-0000-0000-0000-000000000030", + "HEATING_COOLING_DEMAND_PUMP": "00000000-0000-0000-0000-000000000057", + "HEATING_DEHUMIDIFIER": "00000000-0000-0000-0000-000000000055", + "HEATING_EXTERNAL_CLOCK": "00000000-0000-0000-0000-000000000054", + "HEATING_HUMIDITY_LIMITER": "00000000-0000-0000-0000-000000000045", + "HEATING_TEMPERATURE_LIMITER": "00000000-0000-0000-0000-000000000029" + }, + "functionalGroups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000019", + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000021" + ], + "optimumStartStopEnabled": false, + "solution": "INDOOR_CLIMATE" + }, + "LIGHT_AND_SHADOW": { + "active": true, + "extendedLinkedShutterGroups": [], + "extendedLinkedSwitchingGroups": [], + "functionalGroups": [ + "00000000-0000-0000-0000-000000000018" + ], + "shutterProfileGroups": [], + "solution": "LIGHT_AND_SHADOW", + "switchingProfileGroups": [] + }, + "SECURITY_AND_ALARM": { + "activationInProgress": false, + "active": true, + "alarmActive": false, + "alarmEventDeviceId": "3014F7110000000000000007", + "alarmEventTimestamp": 1524504122047, + "alarmSecurityJournalEntryType": "SENSOR_EVENT", + "functionalGroups": [ + "00000000-0000-0000-0000-000000000013", + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000015", + "00000000-0000-0000-0000-000000000009" + ], + "intrusionAlertThroughSmokeDetectors": false, + "securitySwitchingGroups": { + "ALARM": "00000000-0000-0000-0000-000000000056", + "BACKUP_ALARM_SIREN": "00000000-AAAA-0000-0000-000000000068", + "COMING_HOME": "00000000-0000-0000-0000-000000000037", + "PANIC": "00000000-0000-0000-0000-000000000053", + "SIREN": "00000000-0000-0000-0000-000000000022" + }, + "securityZoneActivationMode": "ACTIVATION_WITH_DEVICE_IGNORELIST", + "securityZones": { + "EXTERNAL": "00000000-0000-0000-0000-000000000005", + "INTERNAL": "00000000-0000-0000-0000-000000000016" + }, + "solution": "SECURITY_AND_ALARM", + "zoneActivationDelay": 0.0 + }, + "WEATHER_AND_ENVIRONMENT": { + "active": true, + "functionalGroups": [ + "00000000-AAAA-0000-0000-000000000001" + ], + "solution": "WEATHER_AND_ENVIRONMENT" + } + }, + "id": "00000000-0000-0000-0000-000000000001", + "inboxGroup": "00000000-0000-0000-0000-000000000044", + "lastReadyForUpdateTimestamp": 1522319489138, + "location": { + "city": "1010 Wien, Österreich", + "latitude": "48.208088", + "longitude": "16.358608" + }, + "metaGroups": [ + "00000000-0000-0000-0000-000000000011", + "00000000-0000-0000-0000-000000000008", + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000017", + "00000000-0000-0000-0000-000000000020" + ], + "pinAssigned": false, + "powerMeterCurrency": "EUR", + "powerMeterUnitPrice": 0.0, + "ruleGroups": [ + "00000000-0000-0000-0000-000000000057", + "00000000-0000-0000-0000-000000000030" + ], + "ruleMetaDatas": { + "00000000-0000-0000-0000-000000000065": { + "active": true, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000065", + "label": "Alarmanlage", + "ruleErrorCategories": [], + "type": "SIMPLE" + } + }, + "timeZoneId": "Europe/Vienna", + "updateState": "UP_TO_DATE", + "voiceControlSettings": { + "allowedActiveSecurityZoneIds": [] + }, + "weather": { + "humidity": 54, + "maxTemperature": 16.6, + "minTemperature": 16.6, + "temperature": 16.6, + "vaporAmount": 5.465858858389302, + "weatherCondition": "LIGHT_CLOUDY", + "weatherDayTime": "NIGHT", + "windDirection": 294, + "windSpeed": 8.568 + } + } +} From bd6bbcd5affaeac0076357937c3da94dee8f987f Mon Sep 17 00:00:00 2001 From: Santobert Date: Sun, 6 Oct 2019 13:05:51 +0200 Subject: [PATCH 0634/3953] Neato config flow (#26579) * initial commit * Minor changes * add async setup entry * Add translations and some other stuff * add and remove entry * use async_setup_entry * Update config_flows.py * dshokouhi's changes * Improve workflow * Add valid_vendors * Add entity registry * Add device registry * Update entry from configuration.yaml * Revert unneccesary changes * Update .coveragerc * Prepared tests * Add dshokouhi and Santobert as codeowners * Fix unload entry and abort when already_configured * First tests * Add test for abort cases * Add test for invalid credentials on import * Add one last test * Add test_init.py with some tests * Address reviews, part 1 * Update outdated entry * await instead of add_job * run IO inside an executor * remove faulty test * Fix pylint issues * Move IO out of constructur * Edit error translations * Edit imports * Minor changes * Remove test for invalid vendor * Async setup platform * Edit login function * Moved IO out if init * Update switches after added to hass * Revert update outdated entry * try and update new entrys from config.yaml * Add test invalid vendor * Default to neato --- .coveragerc | 4 +- CODEOWNERS | 1 + .../components/neato/.translations/en.json | 26 ++ homeassistant/components/neato/__init__.py | 271 +++++++----------- homeassistant/components/neato/camera.py | 20 +- homeassistant/components/neato/config_flow.py | 112 ++++++++ homeassistant/components/neato/const.py | 150 ++++++++++ homeassistant/components/neato/manifest.json | 8 +- homeassistant/components/neato/strings.json | 26 ++ homeassistant/components/neato/switch.py | 29 +- homeassistant/components/neato/vacuum.py | 29 +- homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/neato/__init__.py | 1 + tests/components/neato/test_config_flow.py | 129 +++++++++ tests/components/neato/test_init.py | 70 +++++ 17 files changed, 691 insertions(+), 190 deletions(-) create mode 100644 homeassistant/components/neato/.translations/en.json create mode 100644 homeassistant/components/neato/config_flow.py create mode 100644 homeassistant/components/neato/const.py create mode 100644 homeassistant/components/neato/strings.json create mode 100644 tests/components/neato/__init__.py create mode 100644 tests/components/neato/test_config_flow.py create mode 100644 tests/components/neato/test_init.py diff --git a/.coveragerc b/.coveragerc index 5c2d2e02f45020..6f3dfbc94a832e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -420,7 +420,9 @@ omit = homeassistant/components/n26/* homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py - homeassistant/components/neato/* + homeassistant/components/neato/camera.py + homeassistant/components/neato/vacuum.py + homeassistant/components/neato/switch.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py homeassistant/components/nest/* diff --git a/CODEOWNERS b/CODEOWNERS index 935d68033e3b7b..ba4058d5acf8db 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -187,6 +187,7 @@ homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core homeassistant/components/mysensors/* @MartinHjelmare homeassistant/components/mystrom/* @fabaff +homeassistant/components/neato/* @dshokouhi @Santobert homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan diff --git a/homeassistant/components/neato/.translations/en.json b/homeassistant/components/neato/.translations/en.json new file mode 100644 index 00000000000000..dc13242cc1de87 --- /dev/null +++ b/homeassistant/components/neato/.translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "Neato", + "step": { + "user": { + "title": "Neato Account Info", + "data": { + "username": "Username", + "password": "Password", + "vendor": "Vendor" + }, + "description": "See [Neato documentation]({docs_url})." + } + }, + "error": { + "invalid_credentials": "Invalid credentials" + }, + "create_entry": { + "default": "See [Neato documentation]({docs_url})." + }, + "abort": { + "already_configured": "Already configured", + "invalid_credentials": "Invalid credentials" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index e17c562171ae57..8fd545c59bbf2a 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,194 +1,125 @@ """Support for Neato botvac connected vacuum cleaners.""" +import asyncio import logging from datetime import timedelta -from urllib.error import HTTPError +from requests.exceptions import HTTPError, ConnectionError as ConnError import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle +from .config_flow import NeatoConfigFlow +from .const import ( + CONF_VENDOR, + NEATO_CONFIG, + NEATO_DOMAIN, + NEATO_LOGIN, + NEATO_ROBOTS, + NEATO_PERSISTENT_MAPS, + NEATO_MAP_DATA, + VALID_VENDORS, +) + _LOGGER = logging.getLogger(__name__) -CONF_VENDOR = "vendor" -DOMAIN = "neato" -NEATO_ROBOTS = "neato_robots" -NEATO_LOGIN = "neato_login" -NEATO_MAP_DATA = "neato_map_data" -NEATO_PERSISTENT_MAPS = "neato_persistent_maps" CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( + NEATO_DOMAIN: vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_VENDOR, default="neato"): vol.In( - ["neato", "vorwerk"] - ), + vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS), } ) }, extra=vol.ALLOW_EXTRA, ) -MODE = {1: "Eco", 2: "Turbo"} - -ACTION = { - 0: "Invalid", - 1: "House Cleaning", - 2: "Spot Cleaning", - 3: "Manual Cleaning", - 4: "Docking", - 5: "User Menu Active", - 6: "Suspended Cleaning", - 7: "Updating", - 8: "Copying logs", - 9: "Recovering Location", - 10: "IEC test", - 11: "Map cleaning", - 12: "Exploring map (creating a persistent map)", - 13: "Acquiring Persistent Map IDs", - 14: "Creating & Uploading Map", - 15: "Suspended Exploration", -} - -ERRORS = { - "ui_error_battery_battundervoltlithiumsafety": "Replace battery", - "ui_error_battery_critical": "Replace battery", - "ui_error_battery_invalidsensor": "Replace battery", - "ui_error_battery_lithiumadapterfailure": "Replace battery", - "ui_error_battery_mismatch": "Replace battery", - "ui_error_battery_nothermistor": "Replace battery", - "ui_error_battery_overtemp": "Replace battery", - "ui_error_battery_overvolt": "Replace battery", - "ui_error_battery_undercurrent": "Replace battery", - "ui_error_battery_undertemp": "Replace battery", - "ui_error_battery_undervolt": "Replace battery", - "ui_error_battery_unplugged": "Replace battery", - "ui_error_brush_stuck": "Brush stuck", - "ui_error_brush_overloaded": "Brush overloaded", - "ui_error_bumper_stuck": "Bumper stuck", - "ui_error_check_battery_switch": "Check battery", - "ui_error_corrupt_scb": "Call customer service corrupt board", - "ui_error_deck_debris": "Deck debris", - "ui_error_dflt_app": "Check Neato app", - "ui_error_disconnect_chrg_cable": "Disconnected charge cable", - "ui_error_disconnect_usb_cable": "Disconnected USB cable", - "ui_error_dust_bin_missing": "Dust bin missing", - "ui_error_dust_bin_full": "Dust bin full", - "ui_error_dust_bin_emptied": "Dust bin emptied", - "ui_error_hardware_failure": "Hardware failure", - "ui_error_ldrop_stuck": "Clear my path", - "ui_error_lds_jammed": "Clear my path", - "ui_error_lds_bad_packets": "Check Neato app", - "ui_error_lds_disconnected": "Check Neato app", - "ui_error_lds_missed_packets": "Check Neato app", - "ui_error_lwheel_stuck": "Clear my path", - "ui_error_navigation_backdrop_frontbump": "Clear my path", - "ui_error_navigation_backdrop_leftbump": "Clear my path", - "ui_error_navigation_backdrop_wheelextended": "Clear my path", - "ui_error_navigation_noprogress": "Clear my path", - "ui_error_navigation_origin_unclean": "Clear my path", - "ui_error_navigation_pathproblems": "Cannot return to base", - "ui_error_navigation_pinkycommsfail": "Clear my path", - "ui_error_navigation_falling": "Clear my path", - "ui_error_navigation_noexitstogo": "Clear my path", - "ui_error_navigation_nomotioncommands": "Clear my path", - "ui_error_navigation_rightdrop_leftbump": "Clear my path", - "ui_error_navigation_undockingfailed": "Clear my path", - "ui_error_picked_up": "Picked up", - "ui_error_qa_fail": "Check Neato app", - "ui_error_rdrop_stuck": "Clear my path", - "ui_error_reconnect_failed": "Reconnect failed", - "ui_error_rwheel_stuck": "Clear my path", - "ui_error_stuck": "Stuck!", - "ui_error_unable_to_return_to_base": "Unable to return to base", - "ui_error_unable_to_see": "Clean vacuum sensors", - "ui_error_vacuum_slip": "Clear my path", - "ui_error_vacuum_stuck": "Clear my path", - "ui_error_warning": "Error check app", - "batt_base_connect_fail": "Battery failed to connect to base", - "batt_base_no_power": "Battery base has no power", - "batt_low": "Battery low", - "batt_on_base": "Battery on base", - "clean_tilt_on_start": "Clean the tilt on start", - "dustbin_full": "Dust bin full", - "dustbin_missing": "Dust bin missing", - "gen_picked_up": "Picked up", - "hw_fail": "Hardware failure", - "hw_tof_sensor_sensor": "Hardware sensor disconnected", - "lds_bad_packets": "Bad packets", - "lds_deck_debris": "Debris on deck", - "lds_disconnected": "Disconnected", - "lds_jammed": "Jammed", - "lds_missed_packets": "Missed packets", - "maint_brush_stuck": "Brush stuck", - "maint_brush_overload": "Brush overloaded", - "maint_bumper_stuck": "Bumper stuck", - "maint_customer_support_qa": "Contact customer support", - "maint_vacuum_stuck": "Vacuum is stuck", - "maint_vacuum_slip": "Vacuum is stuck", - "maint_left_drop_stuck": "Vacuum is stuck", - "maint_left_wheel_stuck": "Vacuum is stuck", - "maint_right_drop_stuck": "Vacuum is stuck", - "maint_right_wheel_stuck": "Vacuum is stuck", - "not_on_charge_base": "Not on the charge base", - "nav_robot_falling": "Clear my path", - "nav_no_path": "Clear my path", - "nav_path_problem": "Clear my path", - "nav_backdrop_frontbump": "Clear my path", - "nav_backdrop_leftbump": "Clear my path", - "nav_backdrop_wheelextended": "Clear my path", - "nav_mag_sensor": "Clear my path", - "nav_no_exit": "Clear my path", - "nav_no_movement": "Clear my path", - "nav_rightdrop_leftbump": "Clear my path", - "nav_undocking_failed": "Clear my path", -} - -ALERTS = { - "ui_alert_dust_bin_full": "Please empty dust bin", - "ui_alert_recovering_location": "Returning to start", - "ui_alert_battery_chargebasecommerr": "Battery error", - "ui_alert_busy_charging": "Busy charging", - "ui_alert_charging_base": "Base charging", - "ui_alert_charging_power": "Charging power", - "ui_alert_connect_chrg_cable": "Connect charge cable", - "ui_alert_info_thank_you": "Thank you", - "ui_alert_invalid": "Invalid check app", - "ui_alert_old_error": "Old error", - "ui_alert_swupdate_fail": "Update failed", - "dustbin_full": "Please empty dust bin", - "maint_brush_change": "Change the brush", - "maint_filter_change": "Change the filter", - "clean_completed_to_start": "Cleaning completed", - "nav_floorplan_not_created": "No floorplan found", - "nav_floorplan_load_fail": "Failed to load floorplan", - "nav_floorplan_localization_fail": "Failed to load floorplan", - "clean_incomplete_to_start": "Cleaning incomplete", - "log_upload_failed": "Logs failed to upload", -} - - -def setup(hass, config): + +async def async_setup(hass, config): """Set up the Neato component.""" + + if NEATO_DOMAIN not in config: + # There is an entry and nothing in configuration.yaml + return True + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + hass.data[NEATO_CONFIG] = config[NEATO_DOMAIN] + + if entries: + # There is an entry and something in the configuration.yaml + entry = entries[0] + conf = config[NEATO_DOMAIN] + if ( + entry.data[CONF_USERNAME] == conf[CONF_USERNAME] + and entry.data[CONF_PASSWORD] == conf[CONF_PASSWORD] + and entry.data[CONF_VENDOR] == conf[CONF_VENDOR] + ): + # The entry is not outdated + return True + + # The entry is outdated + error = await hass.async_add_executor_job( + NeatoConfigFlow.try_login, + conf[CONF_USERNAME], + conf[CONF_PASSWORD], + conf[CONF_VENDOR], + ) + if error is not None: + _LOGGER.error(error) + return False + + # Update the entry + hass.config_entries.async_update_entry(entry, data=config[NEATO_DOMAIN]) + else: + # Create the new entry + hass.async_create_task( + hass.config_entries.flow.async_init( + NEATO_DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[NEATO_DOMAIN], + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up config entry.""" from pybotvac import Account, Neato, Vorwerk - if config[DOMAIN][CONF_VENDOR] == "neato": - hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Neato) - elif config[DOMAIN][CONF_VENDOR] == "vorwerk": - hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Vorwerk) + if entry.data[CONF_VENDOR] == "neato": + hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Neato) + elif entry.data[CONF_VENDOR] == "vorwerk": + hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Vorwerk) + hub = hass.data[NEATO_LOGIN] - if not hub.login(): + await hass.async_add_executor_job(hub.login) + if not hub.logged_in: _LOGGER.debug("Failed to login to Neato API") return False - hub.update_robots() + + await hass.async_add_executor_job(hub.update_robots) for component in ("camera", "vacuum", "switch"): - discovery.load_platform(hass, component, DOMAIN, {}, config) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + +async def async_unload_entry(hass, entry): + """Unload config entry.""" + hass.data.pop(NEATO_LOGIN) + await asyncio.gather( + hass.config_entries.async_forward_entry_unload(entry, "camera"), + hass.config_entries.async_forward_entry_unload(entry, "vacuum"), + hass.config_entries.async_forward_entry_unload(entry, "switch"), + ) return True @@ -202,12 +133,8 @@ def __init__(self, hass, domain_config, neato, vendor): self._hass = hass self._vendor = vendor - self.my_neato = neato( - domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD], vendor - ) - self._hass.data[NEATO_ROBOTS] = self.my_neato.robots - self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps - self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + self.my_neato = None + self.logged_in = False def login(self): """Login to My Neato.""" @@ -216,10 +143,16 @@ def login(self): self.my_neato = self._neato( self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor ) - return True - except HTTPError: + self.logged_in = True + except (HTTPError, ConnError): _LOGGER.error("Unable to connect to Neato API") - return False + self.logged_in = False + return + + _LOGGER.debug("Successfully connected to Neato API") + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps + self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps @Throttle(timedelta(seconds=300)) def update_robots(self): diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 5d4e00579606d1..c565fa3d9ac85e 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -4,21 +4,30 @@ from homeassistant.components.camera import Camera -from . import NEATO_LOGIN, NEATO_MAP_DATA, NEATO_ROBOTS +from .const import NEATO_DOMAIN, NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=10) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Neato Camera.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Neato camera with config entry.""" dev = [] for robot in hass.data[NEATO_ROBOTS]: if "maps" in robot.traits: dev.append(NeatoCleaningMap(hass, robot)) + + if not dev: + return + _LOGGER.debug("Adding robots for cleaning maps %s", dev) - add_entities(dev, True) + async_add_entities(dev, True) class NeatoCleaningMap(Camera): @@ -61,3 +70,8 @@ def name(self): def unique_id(self): """Return unique ID.""" return self._robot_serial + + @property + def device_info(self): + """Device info for neato robot.""" + return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py new file mode 100644 index 00000000000000..0c71cdbd069f21 --- /dev/null +++ b/homeassistant/components/neato/config_flow.py @@ -0,0 +1,112 @@ +"""Config flow to configure Neato integration.""" + +import logging + +import voluptuous as vol +from requests.exceptions import HTTPError, ConnectionError as ConnError + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +# pylint: disable=unused-import +from .const import CONF_VENDOR, NEATO_DOMAIN, VALID_VENDORS + + +DOCS_URL = "https://www.home-assistant.io/components/neato" +DEFAULT_VENDOR = "neato" + +_LOGGER = logging.getLogger(__name__) + + +class NeatoConfigFlow(config_entries.ConfigFlow, domain=NEATO_DOMAIN): + """Neato integration config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize flow.""" + self._username = vol.UNDEFINED + self._password = vol.UNDEFINED + self._vendor = vol.UNDEFINED + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + if user_input is not None: + self._username = user_input["username"] + self._password = user_input["password"] + self._vendor = user_input["vendor"] + + error = await self.hass.async_add_executor_job( + self.try_login, self._username, self._password, self._vendor + ) + if error: + errors["base"] = error + else: + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data=user_input, + description_placeholders={"docs_url": DOCS_URL}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS), + } + ), + description_placeholders={"docs_url": DOCS_URL}, + errors=errors, + ) + + async def async_step_import(self, user_input): + """Import a config flow from configuration.""" + + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + vendor = user_input[CONF_VENDOR] + + error = await self.hass.async_add_executor_job( + self.try_login, username, password, vendor + ) + if error is not None: + _LOGGER.error(error) + return self.async_abort(reason=error) + + return self.async_create_entry( + title=f"{username} (from configuration)", + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_VENDOR: vendor, + }, + ) + + @staticmethod + def try_login(username, password, vendor): + """Try logging in to device and return any errors.""" + from pybotvac import Account, Neato, Vorwerk + + this_vendor = None + if vendor == "vorwerk": + this_vendor = Vorwerk() + else: # Neato + this_vendor = Neato() + + try: + Account(username, password, this_vendor) + except (HTTPError, ConnError): + return "invalid_credentials" + + return None diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py new file mode 100644 index 00000000000000..6fb41bda710bc4 --- /dev/null +++ b/homeassistant/components/neato/const.py @@ -0,0 +1,150 @@ +"""Constants for Neato integration.""" + +NEATO_DOMAIN = "neato" + +CONF_VENDOR = "vendor" +NEATO_ROBOTS = "neato_robots" +NEATO_LOGIN = "neato_login" +NEATO_CONFIG = "neato_config" +NEATO_MAP_DATA = "neato_map_data" +NEATO_PERSISTENT_MAPS = "neato_persistent_maps" + +VALID_VENDORS = ["neato", "vorwerk"] + +MODE = {1: "Eco", 2: "Turbo"} + +ACTION = { + 0: "Invalid", + 1: "House Cleaning", + 2: "Spot Cleaning", + 3: "Manual Cleaning", + 4: "Docking", + 5: "User Menu Active", + 6: "Suspended Cleaning", + 7: "Updating", + 8: "Copying logs", + 9: "Recovering Location", + 10: "IEC test", + 11: "Map cleaning", + 12: "Exploring map (creating a persistent map)", + 13: "Acquiring Persistent Map IDs", + 14: "Creating & Uploading Map", + 15: "Suspended Exploration", +} + +ERRORS = { + "ui_error_battery_battundervoltlithiumsafety": "Replace battery", + "ui_error_battery_critical": "Replace battery", + "ui_error_battery_invalidsensor": "Replace battery", + "ui_error_battery_lithiumadapterfailure": "Replace battery", + "ui_error_battery_mismatch": "Replace battery", + "ui_error_battery_nothermistor": "Replace battery", + "ui_error_battery_overtemp": "Replace battery", + "ui_error_battery_overvolt": "Replace battery", + "ui_error_battery_undercurrent": "Replace battery", + "ui_error_battery_undertemp": "Replace battery", + "ui_error_battery_undervolt": "Replace battery", + "ui_error_battery_unplugged": "Replace battery", + "ui_error_brush_stuck": "Brush stuck", + "ui_error_brush_overloaded": "Brush overloaded", + "ui_error_bumper_stuck": "Bumper stuck", + "ui_error_check_battery_switch": "Check battery", + "ui_error_corrupt_scb": "Call customer service corrupt board", + "ui_error_deck_debris": "Deck debris", + "ui_error_dflt_app": "Check Neato app", + "ui_error_disconnect_chrg_cable": "Disconnected charge cable", + "ui_error_disconnect_usb_cable": "Disconnected USB cable", + "ui_error_dust_bin_missing": "Dust bin missing", + "ui_error_dust_bin_full": "Dust bin full", + "ui_error_dust_bin_emptied": "Dust bin emptied", + "ui_error_hardware_failure": "Hardware failure", + "ui_error_ldrop_stuck": "Clear my path", + "ui_error_lds_jammed": "Clear my path", + "ui_error_lds_bad_packets": "Check Neato app", + "ui_error_lds_disconnected": "Check Neato app", + "ui_error_lds_missed_packets": "Check Neato app", + "ui_error_lwheel_stuck": "Clear my path", + "ui_error_navigation_backdrop_frontbump": "Clear my path", + "ui_error_navigation_backdrop_leftbump": "Clear my path", + "ui_error_navigation_backdrop_wheelextended": "Clear my path", + "ui_error_navigation_noprogress": "Clear my path", + "ui_error_navigation_origin_unclean": "Clear my path", + "ui_error_navigation_pathproblems": "Cannot return to base", + "ui_error_navigation_pinkycommsfail": "Clear my path", + "ui_error_navigation_falling": "Clear my path", + "ui_error_navigation_noexitstogo": "Clear my path", + "ui_error_navigation_nomotioncommands": "Clear my path", + "ui_error_navigation_rightdrop_leftbump": "Clear my path", + "ui_error_navigation_undockingfailed": "Clear my path", + "ui_error_picked_up": "Picked up", + "ui_error_qa_fail": "Check Neato app", + "ui_error_rdrop_stuck": "Clear my path", + "ui_error_reconnect_failed": "Reconnect failed", + "ui_error_rwheel_stuck": "Clear my path", + "ui_error_stuck": "Stuck!", + "ui_error_unable_to_return_to_base": "Unable to return to base", + "ui_error_unable_to_see": "Clean vacuum sensors", + "ui_error_vacuum_slip": "Clear my path", + "ui_error_vacuum_stuck": "Clear my path", + "ui_error_warning": "Error check app", + "batt_base_connect_fail": "Battery failed to connect to base", + "batt_base_no_power": "Battery base has no power", + "batt_low": "Battery low", + "batt_on_base": "Battery on base", + "clean_tilt_on_start": "Clean the tilt on start", + "dustbin_full": "Dust bin full", + "dustbin_missing": "Dust bin missing", + "gen_picked_up": "Picked up", + "hw_fail": "Hardware failure", + "hw_tof_sensor_sensor": "Hardware sensor disconnected", + "lds_bad_packets": "Bad packets", + "lds_deck_debris": "Debris on deck", + "lds_disconnected": "Disconnected", + "lds_jammed": "Jammed", + "lds_missed_packets": "Missed packets", + "maint_brush_stuck": "Brush stuck", + "maint_brush_overload": "Brush overloaded", + "maint_bumper_stuck": "Bumper stuck", + "maint_customer_support_qa": "Contact customer support", + "maint_vacuum_stuck": "Vacuum is stuck", + "maint_vacuum_slip": "Vacuum is stuck", + "maint_left_drop_stuck": "Vacuum is stuck", + "maint_left_wheel_stuck": "Vacuum is stuck", + "maint_right_drop_stuck": "Vacuum is stuck", + "maint_right_wheel_stuck": "Vacuum is stuck", + "not_on_charge_base": "Not on the charge base", + "nav_robot_falling": "Clear my path", + "nav_no_path": "Clear my path", + "nav_path_problem": "Clear my path", + "nav_backdrop_frontbump": "Clear my path", + "nav_backdrop_leftbump": "Clear my path", + "nav_backdrop_wheelextended": "Clear my path", + "nav_mag_sensor": "Clear my path", + "nav_no_exit": "Clear my path", + "nav_no_movement": "Clear my path", + "nav_rightdrop_leftbump": "Clear my path", + "nav_undocking_failed": "Clear my path", +} + +ALERTS = { + "ui_alert_dust_bin_full": "Please empty dust bin", + "ui_alert_recovering_location": "Returning to start", + "ui_alert_battery_chargebasecommerr": "Battery error", + "ui_alert_busy_charging": "Busy charging", + "ui_alert_charging_base": "Base charging", + "ui_alert_charging_power": "Charging power", + "ui_alert_connect_chrg_cable": "Connect charge cable", + "ui_alert_info_thank_you": "Thank you", + "ui_alert_invalid": "Invalid check app", + "ui_alert_old_error": "Old error", + "ui_alert_swupdate_fail": "Update failed", + "dustbin_full": "Please empty dust bin", + "maint_brush_change": "Change the brush", + "maint_filter_change": "Change the filter", + "clean_completed_to_start": "Cleaning completed", + "nav_floorplan_not_created": "No floorplan found", + "nav_floorplan_load_fail": "Failed to load floorplan", + "nav_floorplan_localization_fail": "Failed to load floorplan", + "clean_incomplete_to_start": "Cleaning incomplete", + "log_upload_failed": "Logs failed to upload", +} diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 8b0c5acc72362c..160f194cd63efe 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -1,10 +1,14 @@ { "domain": "neato", "name": "Neato", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", "requirements": [ "pybotvac==0.0.15" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@dshokouhi", + "@Santobert" + ] +} \ No newline at end of file diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json new file mode 100644 index 00000000000000..dc13242cc1de87 --- /dev/null +++ b/homeassistant/components/neato/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "Neato", + "step": { + "user": { + "title": "Neato Account Info", + "data": { + "username": "Username", + "password": "Password", + "vendor": "Vendor" + }, + "description": "See [Neato documentation]({docs_url})." + } + }, + "error": { + "invalid_credentials": "Invalid credentials" + }, + "create_entry": { + "default": "See [Neato documentation]({docs_url})." + }, + "abort": { + "already_configured": "Already configured", + "invalid_credentials": "Invalid credentials" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 539e8cb748cfaa..3efee11853d19e 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -7,7 +7,7 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity -from . import NEATO_LOGIN, NEATO_ROBOTS +from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS _LOGGER = logging.getLogger(__name__) @@ -18,14 +18,23 @@ SWITCH_TYPES = {SWITCH_TYPE_SCHEDULE: ["Schedule"]} -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Neato switches.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Neato switch with config entry.""" dev = [] for robot in hass.data[NEATO_ROBOTS]: for type_name in SWITCH_TYPES: dev.append(NeatoConnectedSwitch(hass, robot, type_name)) + + if not dev: + return + _LOGGER.debug("Adding switches %s", dev) - add_entities(dev) + async_add_entities(dev, True) class NeatoConnectedSwitch(ToggleEntity): @@ -37,14 +46,7 @@ def __init__(self, hass, robot, switch_type): self.robot = robot self.neato = hass.data[NEATO_LOGIN] self._robot_name = "{} {}".format(self.robot.name, SWITCH_TYPES[self.type][0]) - try: - self._state = self.robot.state - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as ex: - _LOGGER.warning("Neato connection error: %s", ex) - self._state = None + self._state = None self._schedule_state = None self._clean_state = None self._robot_serial = self.robot.serial @@ -94,6 +96,11 @@ def is_on(self): return True return False + @property + def device_info(self): + """Device info for neato robot.""" + return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} + def turn_on(self, **kwargs): """Turn the switch on.""" if self.type == SWITCH_TYPE_SCHEDULE: diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index f284b2eda1ed25..96c4e8f3c5f1c3 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -31,12 +31,13 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids -from . import ( +from .const import ( ACTION, ALERTS, ERRORS, MODE, NEATO_LOGIN, + NEATO_DOMAIN, NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, @@ -83,8 +84,13 @@ ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Neato vacuum.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Neato vacuum with config entry.""" dev = [] for robot in hass.data[NEATO_ROBOTS]: dev.append(NeatoConnectedVacuum(hass, robot)) @@ -93,7 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return _LOGGER.debug("Adding vacuums %s", dev) - add_entities(dev, True) + async_add_entities(dev, True) def neato_custom_cleaning_service(call): """Zone cleaning service that allows user to change options.""" @@ -111,7 +117,7 @@ def service_to_entities(call): entities = [entity for entity in dev if entity.entity_id in entity_ids] return entities - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_NEATO_CUSTOM_CLEANING, neato_custom_cleaning_service, @@ -144,10 +150,14 @@ def __init__(self, hass, robot): self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS] self._robot_boundaries = {} self._robot_has_map = self.robot.has_persistent_maps + self._robot_stats = None def update(self): """Update the states of Neato Vacuums.""" _LOGGER.debug("Running Neato Vacuums update") + if self._robot_stats is None: + self._robot_stats = self.robot.get_robot_info().json() + self.neato.update_robots() try: self._state = self.robot.state @@ -290,6 +300,17 @@ def device_state_attributes(self): return data + @property + def device_info(self): + """Device info for neato robot.""" + return { + "identifiers": {(NEATO_DOMAIN, self._robot_serial)}, + "name": self._name, + "manufacturer": self._robot_stats["data"]["mfg_name"], + "model": self._robot_stats["data"]["modelName"], + "sw_version": self._state["meta"]["firmware"], + } + def start(self): """Start cleaning or resume cleaning.""" if self._state["state"] == 1: diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1eb08709741394..4a4effc36ce9a2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -43,6 +43,7 @@ "met", "mobile_app", "mqtt", + "neato", "nest", "notion", "opentherm_gw", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60780ec7c55c76..8627ddb0d86ec5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -296,6 +296,9 @@ pyMetno==0.4.6 # homeassistant.components.blackbird pyblackbird==0.5 +# homeassistant.components.neato +pybotvac==0.0.15 + # homeassistant.components.cast pychromecast==4.0.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 70c81c660256dc..3c0941fc887a0f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -122,6 +122,7 @@ "py-canary", "py17track", "pyblackbird", + "pybotvac", "pychromecast", "pydeconz", "pydispatcher", diff --git a/tests/components/neato/__init__.py b/tests/components/neato/__init__.py new file mode 100644 index 00000000000000..7927918395c550 --- /dev/null +++ b/tests/components/neato/__init__.py @@ -0,0 +1 @@ +"""Tests for the Neato component.""" diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py new file mode 100644 index 00000000000000..99691c101a6747 --- /dev/null +++ b/tests/components/neato/test_config_flow.py @@ -0,0 +1,129 @@ +"""Tests for the Neato config flow.""" +import pytest +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components.neato import config_flow +from homeassistant.components.neato.const import NEATO_DOMAIN, CONF_VENDOR +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +USERNAME = "myUsername" +PASSWORD = "myPassword" +VENDOR_NEATO = "neato" +VENDOR_VORWERK = "vorwerk" +VENDOR_INVALID = "invalid" + + +@pytest.fixture(name="account") +def mock_controller_login(): + """Mock a successful login.""" + with patch("pybotvac.Account", return_value=True): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.NeatoConfigFlow() + flow.hass = hass + return flow + + +async def test_user(hass, account): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_VENDOR] == VENDOR_NEATO + + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_VORWERK} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_VENDOR] == VENDOR_VORWERK + + +async def test_import(hass, account): + """Test import step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{USERNAME} (from configuration)" + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_VENDOR] == VENDOR_NEATO + + +async def test_abort_if_already_setup(hass, account): + """Test we abort if Neato is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain=NEATO_DOMAIN, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + }, + ).add_to_hass(hass) + + # Should fail, same USERNAME (import) + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same USERNAME (flow) + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_abort_on_invalid_credentials(hass): + """Test when we have invalid credentials.""" + from requests.exceptions import HTTPError + + flow = init_config_flow(hass) + + with patch("pybotvac.Account", side_effect=HTTPError()): + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_credentials"} + + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "invalid_credentials" diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py new file mode 100644 index 00000000000000..be7e43fdc0aad4 --- /dev/null +++ b/tests/components/neato/test_init.py @@ -0,0 +1,70 @@ +"""Tests for the Neato init file.""" +import pytest +from unittest.mock import patch + +from homeassistant.components.neato.const import NEATO_DOMAIN, CONF_VENDOR +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +USERNAME = "myUsername" +PASSWORD = "myPassword" +VENDOR_NEATO = "neato" +VENDOR_VORWERK = "vorwerk" +VENDOR_INVALID = "invalid" + +VALID_CONFIG = { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, +} + +INVALID_CONFIG = { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_INVALID, +} + + +@pytest.fixture(name="account") +def mock_controller_login(): + """Mock a successful login.""" + with patch("pybotvac.Account", return_value=True): + yield + + +async def test_no_config_entry(hass): + """There is nothing in configuration.yaml.""" + res = await async_setup_component(hass, NEATO_DOMAIN, {}) + assert res is True + + +async def test_config_entries_in_sync(hass, account): + """The config entry and configuration.yaml are in sync.""" + MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) + + assert hass.config_entries.async_entries(NEATO_DOMAIN) + assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO + + +async def test_config_entries_not_in_sync(hass, account): + """The config entry and configuration.yaml are not in sync.""" + MockConfigEntry(domain=NEATO_DOMAIN, data=INVALID_CONFIG).add_to_hass(hass) + + assert hass.config_entries.async_entries(NEATO_DOMAIN) + assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO From 1ecc883ef46120d569e39c288a0fd3cc4b35f8e7 Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Sun, 6 Oct 2019 05:43:34 -0700 Subject: [PATCH 0635/3953] PS4 bump to renamed dependency (#27144) * Change to renamed dependency pyps4-2ndscreen 0.9.0 * Rename / bump to ps4 dependency to 1.0.0 * update requirements * Rename test req * Fix import * Bump 1.0.1 * Fix flaky test leaving files behind --- homeassistant/components/ps4/__init__.py | 12 ++-- homeassistant/components/ps4/config_flow.py | 9 +-- homeassistant/components/ps4/manifest.json | 2 +- homeassistant/components/ps4/media_player.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- tests/components/ps4/test_config_flow.py | 63 ++++++++++---------- tests/components/ps4/test_media_player.py | 24 ++++---- 9 files changed, 56 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 60635bba525664..205059be608f99 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -3,8 +3,8 @@ import os import voluptuous as vol -from pyps4_homeassistant.ddp import async_create_ddp_endpoint -from pyps4_homeassistant.media_art import COUNTRIES +from pyps4_2ndscreen.ddp import async_create_ddp_endpoint +from pyps4_2ndscreen.media_art import COUNTRIES from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, @@ -172,12 +172,8 @@ def load_games(hass: HomeAssistantType) -> dict: _LOGGER.error("Games file was not parsed correctly") games = {} - # If file does not exist, create empty file. - if not os.path.isfile(g_file): - _LOGGER.info("Creating PS4 Games File") - games = {} - save_games(hass, games) - else: + # If file exists + if os.path.isfile(g_file): games = _reformat_data(hass, games) return games diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index a4b740777932cd..44523aea85adcf 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -2,6 +2,9 @@ from collections import OrderedDict import logging +from pyps4_2ndscreen.errors import CredentialTimeout +from pyps4_2ndscreen.helpers import Helper +from pyps4_2ndscreen.media_art import COUNTRIES import voluptuous as vol from homeassistant import config_entries @@ -37,8 +40,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): def __init__(self): """Initialize the config flow.""" - from pyps4_homeassistant import Helper - self.helper = Helper() self.creds = None self.name = None @@ -61,8 +62,6 @@ async def async_step_user(self, user_input=None): async def async_step_creds(self, user_input=None): """Return PS4 credentials from 2nd Screen App.""" - from pyps4_homeassistant.errors import CredentialTimeout - errors = {} if user_input is not None: try: @@ -103,8 +102,6 @@ async def async_step_mode(self, user_input=None): async def async_step_link(self, user_input=None): """Prompt user input. Create or edit entry.""" - from pyps4_homeassistant.media_art import COUNTRIES - regions = sorted(COUNTRIES.keys()) default_region = None errors = {} diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 98a14d877e8f72..361711c400c7af 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": [ - "pyps4-homeassistant==0.8.7" + "pyps4-2ndscreen==1.0.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index e1ec32ddd1fb8a..3e8b667cd13a3a 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -2,8 +2,8 @@ import logging import asyncio -import pyps4_homeassistant.ps4 as pyps4 -from pyps4_homeassistant.errors import NotReady +import pyps4_2ndscreen.ps4 as pyps4 +from pyps4_2ndscreen.errors import NotReady from homeassistant.core import callback from homeassistant.components.media_player import ENTITY_IMAGE_URL, MediaPlayerDevice @@ -254,7 +254,7 @@ def reset_title(self): async def async_get_title_data(self, title_id, name): """Get PS Store Data.""" - from pyps4_homeassistant.errors import PSDataIncomplete + from pyps4_2ndscreen.errors import PSDataIncomplete app_name = None art = None diff --git a/requirements_all.txt b/requirements_all.txt index c2c2c96a26fc61..39d4ddcda629ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1396,7 +1396,7 @@ pypjlink2==1.2.0 pypoint==1.1.1 # homeassistant.components.ps4 -pyps4-homeassistant==0.8.7 +pyps4-2ndscreen==1.0.1 # homeassistant.components.qwikswitch pyqwikswitch==0.93 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8627ddb0d86ec5..187e50c4691948 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ pyotgw==0.5b0 pyotp==2.3.0 # homeassistant.components.ps4 -pyps4-homeassistant==0.8.7 +pyps4-2ndscreen==1.0.1 # homeassistant.components.qwikswitch pyqwikswitch==0.93 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3c0941fc887a0f..61174e86a439ff 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -141,7 +141,7 @@ "pyopenuv", "pyotgw", "pyotp", - "pyps4-homeassistant", + "pyps4-2ndscreen", "pyqwikswitch", "PyRMVtransport", "pysma", diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 42f319e7343afb..81f81093a67d2c 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -1,6 +1,8 @@ """Define tests for the PlayStation 4 config flow.""" from unittest.mock import patch +from pyps4_2ndscreen.errors import CredentialTimeout + from homeassistant import data_entry_flow from homeassistant.components import ps4 from homeassistant.components.ps4.const import DEFAULT_NAME, DEFAULT_REGION @@ -73,28 +75,28 @@ async def test_full_flow_implementation(hass): manager = hass.config_entries # User Step Started, results in Step Creds - with patch("pyps4_homeassistant.Helper.port_bind", return_value=None): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. - with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_mode(MOCK_AUTO) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" # User Input results in created entry. - with patch("pyps4_homeassistant.Helper.link", return_value=(True, True)), patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(MOCK_CONFIG) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -126,20 +128,20 @@ async def test_multiple_flow_implementation(hass): manager = hass.config_entries # User Step Started, results in Step Creds - with patch("pyps4_homeassistant.Helper.port_bind", return_value=None): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. - with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_mode(MOCK_AUTO) @@ -147,8 +149,8 @@ async def test_multiple_flow_implementation(hass): assert result["step_id"] == "link" # User Input results in created entry. - with patch("pyps4_homeassistant.Helper.link", return_value=(True, True)), patch( - "pyps4_homeassistant.Helper.has_devices", + with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_link(MOCK_CONFIG) @@ -175,8 +177,8 @@ async def test_multiple_flow_implementation(hass): # Test additional flow. # User Step Started, results in Step Mode: - with patch("pyps4_homeassistant.Helper.port_bind", return_value=None), patch( - "pyps4_homeassistant.Helper.has_devices", + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_user() @@ -184,14 +186,14 @@ async def test_multiple_flow_implementation(hass): assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. - with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_mode(MOCK_AUTO) @@ -200,9 +202,9 @@ async def test_multiple_flow_implementation(hass): # Step Link with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], - ), patch("pyps4_homeassistant.Helper.link", return_value=(True, True)): + ), patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)): result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS @@ -232,13 +234,13 @@ async def test_port_bind_abort(hass): flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.port_bind", return_value=MOCK_UDP_PORT): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_UDP_PORT): reason = "port_987_bind_error" result = await flow.async_step_user(user_input=None) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == reason - with patch("pyps4_homeassistant.Helper.port_bind", return_value=MOCK_TCP_PORT): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_TCP_PORT): reason = "port_997_bind_error" result = await flow.async_step_user(user_input=None) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -253,7 +255,7 @@ async def test_duplicate_abort(hass): flow.creds = MOCK_CREDS with patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(user_input=None) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -274,9 +276,9 @@ async def test_additional_device(hass): assert len(manager.async_entries()) == 1 with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], - ), patch("pyps4_homeassistant.Helper.link", return_value=(True, True)): + ), patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)): result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS @@ -296,7 +298,7 @@ async def test_no_devices_found_abort(hass): flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.has_devices", return_value=[]): + with patch("pyps4_2ndscreen.Helper.has_devices", return_value=[]): result = await flow.async_step_link() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" @@ -310,8 +312,7 @@ async def test_manual_mode(hass): # Step Mode with User Input: manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", - return_value=[{"host-ip": flow.m_device}], + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": flow.m_device}] ): result = await flow.async_step_mode(MOCK_MANUAL) assert flow.m_device == MOCK_HOST @@ -324,7 +325,7 @@ async def test_credential_abort(hass): flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.get_creds", return_value=None): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=None): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "credential_error" @@ -332,12 +333,10 @@ async def test_credential_abort(hass): async def test_credential_timeout(hass): """Test that Credential Timeout shows error.""" - from pyps4_homeassistant.errors import CredentialTimeout - flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.get_creds", side_effect=CredentialTimeout): + with patch("pyps4_2ndscreen.Helper.get_creds", side_effect=CredentialTimeout): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "credential_timeout"} @@ -349,8 +348,8 @@ async def test_wrong_pin_error(hass): flow.hass = hass flow.location = MOCK_LOCATION - with patch("pyps4_homeassistant.Helper.link", return_value=(True, False)), patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + with patch("pyps4_2ndscreen.Helper.link", return_value=(True, False)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(MOCK_CONFIG) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -364,8 +363,8 @@ async def test_device_connection_error(hass): flow.hass = hass flow.location = MOCK_LOCATION - with patch("pyps4_homeassistant.Helper.link", return_value=(False, True)), patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + with patch("pyps4_2ndscreen.Helper.link", return_value=(False, True)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(MOCK_CONFIG) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index e4f2033c3cbbeb..d6eeb31695c485 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -1,7 +1,7 @@ """Tests for the PS4 media player platform.""" from unittest.mock import MagicMock, patch -from pyps4_homeassistant.credential import get_ddp_message +from pyps4_2ndscreen.credential import get_ddp_message from homeassistant.components import ps4 from homeassistant.components.media_player.const import ( @@ -295,9 +295,7 @@ 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 - ), patch(MOCK_SAVE, side_effect=MagicMock()): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_OFF): mock_entity_id = await setup_mock_component(hass) await hass.async_block_till_done() @@ -447,9 +445,9 @@ async def test_media_stop(hass): async def test_select_source(hass): """Test that select source service calls function with title.""" mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch( - "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE - ), patch(MOCK_LOAD, return_value=mock_data): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( + MOCK_LOAD, return_value=mock_data + ): mock_entity_id = await setup_mock_component(hass) mock_func = "{}{}".format( @@ -473,9 +471,9 @@ async def test_select_source(hass): async def test_select_source_caps(hass): """Test that select source service calls function with upper case title.""" mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch( - "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE - ), patch(MOCK_LOAD, return_value=mock_data): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( + MOCK_LOAD, return_value=mock_data + ): mock_entity_id = await setup_mock_component(hass) mock_func = "{}{}".format( @@ -502,9 +500,9 @@ async def test_select_source_caps(hass): async def test_select_source_id(hass): """Test that select source service calls function with Title ID.""" mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch( - "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE - ), patch(MOCK_LOAD, return_value=mock_data): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( + MOCK_LOAD, return_value=mock_data + ): mock_entity_id = await setup_mock_component(hass) mock_func = "{}{}".format( From 6cc71db385ce9c9cf9bac32624568ee5658b92b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Sun, 6 Oct 2019 17:00:44 +0200 Subject: [PATCH 0636/3953] Fix onvif PTZ service freeze (#27250) --- homeassistant/components/onvif/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 0c116568780a50..29af1049faede3 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -23,7 +23,7 @@ from homeassistant.components.ffmpeg import DATA_FFMPEG, CONF_EXTRA_ARGUMENTS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.helpers.service import extract_entity_ids +from homeassistant.helpers.service import async_extract_entity_ids import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -88,7 +88,7 @@ async def async_handle_ptz(service): tilt = service.data.get(ATTR_TILT, None) zoom = service.data.get(ATTR_ZOOM, None) all_cameras = hass.data[ONVIF_DATA][ENTITIES] - entity_ids = extract_entity_ids(hass, service) + entity_ids = await async_extract_entity_ids(hass, service) target_cameras = [] if not entity_ids: target_cameras = all_cameras From c7c88b2b68d22d82458599fb3c9c4377946d0412 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 6 Oct 2019 17:17:40 +0200 Subject: [PATCH 0637/3953] UniFi - Bandwidth sensors (#27229) * First draft of UniFi bandwidth sensors * Clean up * Add tests for sensors --- .../components/unifi/.translations/en.json | 5 + homeassistant/components/unifi/config_flow.py | 31 ++- homeassistant/components/unifi/const.py | 2 + homeassistant/components/unifi/controller.py | 15 +- homeassistant/components/unifi/sensor.py | 168 ++++++++++++++ homeassistant/components/unifi/strings.json | 5 + homeassistant/components/unifi/switch.py | 3 - tests/components/unifi/test_controller.py | 16 +- tests/components/unifi/test_sensor.py | 207 ++++++++++++++++++ tests/components/unifi/test_switch.py | 4 +- 10 files changed, 444 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/unifi/sensor.py create mode 100644 tests/components/unifi/test_sensor.py diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 2025bad6246f1f..d9b65b6d1dab06 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -32,6 +32,11 @@ "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients" + } } } } diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index fdb75d09194ef9..92281837f48e45 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -12,6 +12,7 @@ ) from .const import ( + CONF_ALLOW_BANDWIDTH_SENSORS, CONF_CONTROLLER, CONF_DETECTION_TIME, CONF_SITE_ID, @@ -19,6 +20,7 @@ CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID, + DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, @@ -171,6 +173,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry): """Initialize UniFi options flow.""" self.config_entry = config_entry + self.options = dict(config_entry.options) async def async_step_init(self, user_input=None): """Manage the UniFi options.""" @@ -179,7 +182,8 @@ async def async_step_init(self, user_input=None): 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) + self.options.update(user_input) + return await self.async_step_statistics_sensors() return self.async_show_form( step_id="device_tracker", @@ -212,3 +216,28 @@ async def async_step_device_tracker(self, user_input=None): } ), ) + + async def async_step_statistics_sensors(self, user_input=None): + """Manage the statistics sensors options.""" + if user_input is not None: + self.options.update(user_input) + return await self._update_options() + + return self.async_show_form( + step_id="statistics_sensors", + data_schema=vol.Schema( + { + vol.Optional( + CONF_ALLOW_BANDWIDTH_SENSORS, + default=self.config_entry.options.get( + CONF_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_ALLOW_BANDWIDTH_SENSORS, + ), + ): bool + } + ), + ) + + async def _update_options(self): + """Update config entry options.""" + return self.async_create_entry(title="", data=self.options) diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index eac14735074109..d82b7b49d4515d 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -12,6 +12,7 @@ UNIFI_CONFIG = "unifi_config" UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients" +CONF_ALLOW_BANDWIDTH_SENSORS = "allow_bandwidth_sensors" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" CONF_TRACK_CLIENTS = "track_clients" @@ -23,6 +24,7 @@ CONF_DONT_TRACK_DEVICES = "dont_track_devices" CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" +DEFAULT_ALLOW_BANDWIDTH_SENSORS = False DEFAULT_BLOCK_CLIENTS = [] DEFAULT_TRACK_CLIENTS = True DEFAULT_TRACK_DEVICES = True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index ffea98b90502ff..fa1164166bdb28 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -15,6 +15,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( + CONF_ALLOW_BANDWIDTH_SENSORS, CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, @@ -27,6 +28,7 @@ CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, + DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_BLOCK_CLIENTS, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, @@ -40,6 +42,8 @@ ) from .errors import AuthenticationRequired, CannotConnect +SUPPORTED_PLATFORMS = ["device_tracker", "sensor", "switch"] + class UniFiController: """Manages a single UniFi Controller.""" @@ -76,6 +80,13 @@ def site_role(self): """Return the site user role of this controller.""" return self._site_role + @property + def option_allow_bandwidth_sensors(self): + """Config entry option to allow bandwidth sensors.""" + return self.config_entry.options.get( + CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS + ) + @property def option_block_clients(self): """Config entry option with list of clients to control network access.""" @@ -225,7 +236,7 @@ async def async_setup(self): self.config_entry.add_update_listener(self.async_options_updated) - for platform in ["device_tracker", "switch"]: + for platform in SUPPORTED_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup( self.config_entry, platform @@ -294,7 +305,7 @@ async def async_reset(self): if self.api is None: return True - for platform in ["device_tracker", "switch"]: + for platform in SUPPORTED_PLATFORMS: await self.hass.config_entries.async_forward_entry_unload( self.config_entry, platform ) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py new file mode 100644 index 00000000000000..aad013970d103f --- /dev/null +++ b/homeassistant/components/unifi/sensor.py @@ -0,0 +1,168 @@ +"""Support for bandwidth sensors with UniFi clients.""" +import logging + +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry +from homeassistant.core import callback +from homeassistant.helpers import entity_registry +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY + +LOGGER = logging.getLogger(__name__) + +ATTR_RECEIVING = "receiving" +ATTR_TRANSMITTING = "transmitting" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Sensor platform doesn't support configuration through configuration.yaml.""" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensors for UniFi integration.""" + controller = get_controller_from_config_entry(hass, config_entry) + sensors = {} + + registry = await entity_registry.async_get_registry(hass) + + @callback + def update_controller(): + """Update the values of the controller.""" + update_items(controller, async_add_entities, sensors) + + async_dispatcher_connect(hass, controller.signal_update, update_controller) + + @callback + def update_disable_on_entities(): + """Update the values of the controller.""" + for entity in sensors.values(): + + disabled_by = None + if not entity.entity_registry_enabled_default and entity.enabled: + disabled_by = DISABLED_CONFIG_ENTRY + + registry.async_update_entity( + entity.registry_entry.entity_id, disabled_by=disabled_by + ) + + async_dispatcher_connect( + hass, controller.signal_options_update, update_disable_on_entities + ) + + update_controller() + + +@callback +def update_items(controller, async_add_entities, sensors): + """Update sensors from the controller.""" + new_sensors = [] + + for client_id in controller.api.clients: + for direction, sensor_class in ( + ("rx", UniFiRxBandwidthSensor), + ("tx", UniFiTxBandwidthSensor), + ): + item_id = f"{direction}-{client_id}" + + if item_id in sensors: + sensor = sensors[item_id] + if sensor.enabled: + sensor.async_schedule_update_ha_state() + continue + + sensors[item_id] = sensor_class( + controller.api.clients[client_id], controller + ) + new_sensors.append(sensors[item_id]) + + if new_sensors: + async_add_entities(new_sensors) + + +class UniFiBandwidthSensor(Entity): + """UniFi Bandwidth sensor base class.""" + + def __init__(self, client, controller): + """Set up client.""" + self.client = client + self.controller = controller + self.is_wired = self.client.mac not in controller.wireless_clients + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + if self.controller.option_allow_bandwidth_sensors: + return True + return False + + async def async_added_to_hass(self): + """Client entity created.""" + LOGGER.debug("New UniFi bandwidth sensor %s (%s)", self.name, self.client.mac) + + async def async_update(self): + """Synchronize state with controller. + + Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired. + """ + LOGGER.debug( + "Updating UniFi bandwidth sensor %s (%s)", self.entity_id, self.client.mac + ) + await self.controller.request_update() + + if self.is_wired and self.client.mac in self.controller.wireless_clients: + self.is_wired = False + + @property + def available(self) -> bool: + """Return if controller is available.""" + return self.controller.available + + @property + def device_info(self): + """Return a device description for device registry.""" + return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}} + + +class UniFiRxBandwidthSensor(UniFiBandwidthSensor): + """Receiving bandwidth sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self.is_wired: + return self.client.wired_rx_bytes / 1000000 + return self.client.raw.get("rx_bytes", 0) / 1000000 + + @property + def name(self): + """Return the name of the client.""" + name = self.client.name or self.client.hostname + return f"{name} RX" + + @property + def unique_id(self): + """Return a unique identifier for this bandwidth sensor.""" + return f"rx-{self.client.mac}" + + +class UniFiTxBandwidthSensor(UniFiBandwidthSensor): + """Transmitting bandwidth sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self.is_wired: + return self.client.wired_tx_bytes / 1000000 + return self.client.raw.get("tx_bytes", 0) / 1000000 + + @property + def name(self): + """Return the name of the client.""" + name = self.client.name or self.client.hostname + return f"{name} TX" + + @property + def unique_id(self): + """Return a unique identifier for this bandwidth sensor.""" + return f"tx-{self.client.mac}" diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index c484bfbf09fa5d..ce2f2345917b13 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -35,6 +35,11 @@ "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients" + } } } } diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index f0183a7ecb3951..f8fad6dac8ecc6 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -14,7 +14,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Component doesn't support configuration through configuration.yaml.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -231,8 +230,6 @@ def device_state_attributes(self): """Return the device state attributes.""" attributes = { "power": self.port.poe_power, - "received": self.client.wired_rx_bytes / 1000000, - "sent": self.client.wired_tx_bytes / 1000000, "switch": self.client.sw_mac, "port": self.client.sw_port, "poe_mode": self.poe_mode, diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index e73719205f7698..ae6f3776b4f08f 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -67,12 +67,18 @@ async def test_controller_setup(): assert await unifi_controller.async_setup() is True assert unifi_controller.api is api - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 2 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == len( + controller.SUPPORTED_PLATFORMS + ) assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == ( entry, "device_tracker", ) assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == ( + entry, + "sensor", + ) + assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == ( entry, "switch", ) @@ -214,12 +220,16 @@ async def test_reset_unloads_entry_if_setup(): with patch.object(controller, "get_controller", return_value=mock_coro(api)): assert await unifi_controller.async_setup() is True - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 2 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == len( + controller.SUPPORTED_PLATFORMS + ) hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) assert await unifi_controller.async_reset() - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 2 + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == len( + controller.SUPPORTED_PLATFORMS + ) async def test_get_controller(hass): diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py new file mode 100644 index 00000000000000..9064f1c9aba7c4 --- /dev/null +++ b/tests/components/unifi/test_sensor.py @@ -0,0 +1,207 @@ +"""UniFi sensor platform tests.""" +from collections import deque +from copy import deepcopy + +from asynctest import patch + +from homeassistant import config_entries +from homeassistant.components import unifi +from homeassistant.components.unifi.const import ( + CONF_CONTROLLER, + CONF_SITE_ID, + CONTROLLER_ID as CONF_CONTROLLER_ID, + UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, +) +from homeassistant.setup import async_setup_component +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +import homeassistant.components.sensor as sensor + +CLIENTS = [ + { + "hostname": "Wired client hostname", + "ip": "10.0.0.1", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + "name": "Wired client name", + "oui": "Producer", + "sw_mac": "00:00:00:00:01:01", + "sw_port": 1, + "wired-rx_bytes": 1234000000, + "wired-tx_bytes": 5678000000, + }, + { + "hostname": "Wireless client hostname", + "ip": "10.0.0.2", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + "name": "Wireless client name", + "oui": "Producer", + "sw_mac": "00:00:00:00:01:01", + "sw_port": 2, + "rx_bytes": 1234000000, + "tx_bytes": 5678000000, + }, +] + +CONTROLLER_DATA = { + CONF_HOST: "mock-host", + CONF_USERNAME: "mock-user", + CONF_PASSWORD: "mock-pswd", + CONF_PORT: 1234, + CONF_SITE_ID: "mock-site", + CONF_VERIFY_SSL: False, +} + +ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} + +CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") + +SITES = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "admin"}} + + +async def setup_unifi_integration( + hass, + config, + options, + sites, + clients_response, + devices_response, + clients_all_response, +): + """Create the UniFi controller.""" + hass.data[UNIFI_CONFIG] = [] + hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) + config_entry = config_entries.ConfigEntry( + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, + options=options, + entry_id=1, + ) + + mock_client_responses = deque() + mock_client_responses.append(clients_response) + + mock_device_responses = deque() + mock_device_responses.append(devices_response) + + mock_client_all_responses = deque() + mock_client_all_responses.append(clients_all_response) + + mock_requests = [] + + async def mock_request(self, method, path, json=None): + mock_requests.append({"method": method, "path": path, "json": json}) + + if path == "s/{site}/stat/sta" and mock_client_responses: + return mock_client_responses.popleft() + if path == "s/{site}/stat/device" and mock_device_responses: + return mock_device_responses.popleft() + if path == "s/{site}/rest/user" and mock_client_all_responses: + return mock_client_all_responses.popleft() + return {} + + with patch("aiounifi.Controller.login", return_value=True), patch( + "aiounifi.Controller.sites", return_value=sites + ), patch("aiounifi.Controller.request", new=mock_request): + await unifi.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + controller_id = unifi.get_controller_id_from_config_entry(config_entry) + controller = hass.data[unifi.DOMAIN][controller_id] + + controller.mock_client_responses = mock_client_responses + controller.mock_device_responses = mock_device_responses + controller.mock_client_all_responses = mock_client_all_responses + controller.mock_requests = mock_requests + + return controller + + +async def test_platform_manually_configured(hass): + """Test that we do not discover anything or try to set up a controller.""" + assert ( + await async_setup_component( + hass, sensor.DOMAIN, {sensor.DOMAIN: {"platform": "unifi"}} + ) + is True + ) + assert unifi.DOMAIN not in hass.data + + +async def test_no_clients(hass): + """Test the update_clients function when no clients are found.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 + + +async def test_switches(hass): + """Test the update_items function with some clients.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True, + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=CLIENTS, + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 6 + + wired_client_rx = hass.states.get("sensor.wired_client_name_rx") + assert wired_client_rx.state == "1234.0" + + wired_client_tx = hass.states.get("sensor.wired_client_name_tx") + assert wired_client_tx.state == "5678.0" + + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") + assert wireless_client_rx.state == "1234.0" + + wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") + assert wireless_client_tx.state == "5678.0" + + clients = deepcopy(CLIENTS) + clients[0]["is_wired"] = False + clients[1]["rx_bytes"] = 2345000000 + clients[1]["tx_bytes"] = 6789000000 + + controller.mock_client_responses.append(clients) + await controller.async_update() + await hass.async_block_till_done() + + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") + assert wireless_client_rx.state == "2345.0" + + wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") + assert wireless_client_tx.state == "6789.0" diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 56a96b2b5b24ff..97dda4415279df 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -264,7 +264,7 @@ async def setup_unifi_integration( async def mock_request(self, method, path, json=None): mock_requests.append({"method": method, "path": path, "json": json}) - print(mock_requests, mock_client_responses, mock_device_responses) + if path == "s/{site}/stat/sta" and mock_client_responses: return mock_client_responses.popleft() if path == "s/{site}/stat/device" and mock_device_responses: @@ -386,8 +386,6 @@ async def test_switches(hass): assert switch_1 is not None assert switch_1.state == "on" assert switch_1.attributes["power"] == "2.56" - assert switch_1.attributes["received"] == 1234 - assert switch_1.attributes["sent"] == 5678 assert switch_1.attributes["switch"] == "00:00:00:00:01:01" assert switch_1.attributes["port"] == 1 assert switch_1.attributes["poe_mode"] == "auto" From 7a156059e9ef154a875518738951ca60e25d026b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robbert=20M=C3=BCller?= Date: Sun, 6 Oct 2019 17:23:12 +0200 Subject: [PATCH 0638/3953] Switch on/off all lights, and wait for the result (#27078) * Switch on/off all lights, and wait for the result Reuses the parallel_updates semaphore. This is a small crutch which serializes platforms which already do tis for updates. Platforms which can parallelize everything, this makes it go faster * Fix broken unittest With manual validation, with help from @frenck, we found out that the assertions are wrong and the test should be failing. The sequence requested is OFF ON without cancelation, this code should result in: off,off,off,on,on,on testable, by adding a `await hass.async_block_till_done()` between the off and on call. with cancelation. there should be less off call's so off,on,on,on * Adding tests for async_request_call * Process review feedback * Switch gather with wait * :shirt: running black --- homeassistant/components/light/__init__.py | 22 +++--- homeassistant/helpers/entity.py | 13 ++++ tests/components/rflink/test_light.py | 8 +-- tests/helpers/test_entity.py | 82 ++++++++++++++++++++++ 4 files changed, 113 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index ed61d961d881df..fbd908a9e45d51 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -292,7 +292,8 @@ async def async_handle_light_on_service(service): preprocess_turn_on_alternatives(params) turn_lights_off, off_params = preprocess_turn_off(params) - update_tasks = [] + poll_lights = [] + change_tasks = [] for light in target_lights: light.async_set_context(service.context) @@ -305,17 +306,22 @@ async def async_handle_light_on_service(service): preprocess_turn_on_alternatives(pars) turn_light_off, off_pars = preprocess_turn_off(pars) if turn_light_off: - await light.async_turn_off(**off_pars) + task = light.async_request_call(light.async_turn_off(**off_pars)) else: - await light.async_turn_on(**pars) + task = light.async_request_call(light.async_turn_on(**pars)) - if not light.should_poll: - continue + change_tasks.append(task) - update_tasks.append(light.async_update_ha_state(True)) + if light.should_poll: + poll_lights.append(light) - if update_tasks: - await asyncio.wait(update_tasks) + if change_tasks: + await asyncio.wait(change_tasks) + + if poll_lights: + await asyncio.wait( + [light.async_update_ha_state(True) for light in poll_lights] + ) # Listen for light on and light off service calls. hass.services.async_register( diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 836ad954ae0c6b..5754d99d9b20f9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -551,6 +551,19 @@ def __repr__(self) -> str: """Return the representation.""" return "".format(self.name, self.state) + # call an requests + async def async_request_call(self, coro): + """Process request batched.""" + + if self.parallel_updates: + await self.parallel_updates.acquire() + + try: + await coro + finally: + if self.parallel_updates: + self.parallel_updates.release() + class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index ba4122724ce419..a22e7680ac846a 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -313,10 +313,10 @@ async def test_signal_repetitions_cancelling(hass, monkeypatch): await hass.async_block_till_done() - assert protocol.send_command_ack.call_args_list[0][0][1] == "on" - assert protocol.send_command_ack.call_args_list[1][0][1] == "off" - assert protocol.send_command_ack.call_args_list[2][0][1] == "off" - assert protocol.send_command_ack.call_args_list[3][0][1] == "off" + assert protocol.send_command_ack.call_args_list[0][0][1] == "off" + assert protocol.send_command_ack.call_args_list[1][0][1] == "on" + assert protocol.send_command_ack.call_args_list[2][0][1] == "on" + assert protocol.send_command_ack.call_args_list[3][0][1] == "on" async def test_type_toggle(hass, monkeypatch): diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 18cedf1c46ac5a..9d05920f78b792 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -231,6 +231,88 @@ def async_update(): assert update_call is True +async def test_async_async_request_call_without_lock(hass): + """Test for async_requests_call works without a lock.""" + updates = [] + + class AsyncEntity(entity.Entity): + def __init__(self, entity_id): + """Initialize Async test entity.""" + self.entity_id = entity_id + self.hass = hass + + async def testhelper(self, count): + """Helper function.""" + updates.append(count) + + ent_1 = AsyncEntity("light.test_1") + ent_2 = AsyncEntity("light.test_2") + try: + job1 = ent_1.async_request_call(ent_1.testhelper(1)) + job2 = ent_2.async_request_call(ent_2.testhelper(2)) + + await asyncio.wait([job1, job2]) + while True: + if len(updates) >= 2: + break + await asyncio.sleep(0) + finally: + pass + + assert len(updates) == 2 + updates.sort() + assert updates == [1, 2] + + +async def test_async_async_request_call_with_lock(hass): + """Test for async_requests_call works with a semaphore.""" + updates = [] + + test_semaphore = asyncio.Semaphore(1) + + class AsyncEntity(entity.Entity): + def __init__(self, entity_id, lock): + """Initialize Async test entity.""" + self.entity_id = entity_id + self.hass = hass + self.parallel_updates = lock + + async def testhelper(self, count): + """Helper function.""" + updates.append(count) + + ent_1 = AsyncEntity("light.test_1", test_semaphore) + ent_2 = AsyncEntity("light.test_2", test_semaphore) + + try: + assert test_semaphore.locked() is False + await test_semaphore.acquire() + assert test_semaphore.locked() + + job1 = ent_1.async_request_call(ent_1.testhelper(1)) + job2 = ent_2.async_request_call(ent_2.testhelper(2)) + + hass.async_create_task(job1) + hass.async_create_task(job2) + + assert len(updates) == 0 + assert updates == [] + assert test_semaphore._value == 0 + + test_semaphore.release() + + while True: + if len(updates) >= 2: + break + await asyncio.sleep(0) + finally: + test_semaphore.release() + + assert len(updates) == 2 + updates.sort() + assert updates == [1, 2] + + async def test_async_parallel_updates_with_zero(hass): """Test parallel updates with 0 (disabled).""" updates = [] From 1059cea28fa206a688eb2c8c1e924d02b654d5b3 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sun, 6 Oct 2019 19:24:56 +0200 Subject: [PATCH 0639/3953] Refactor IKEA Tradfri, part 2 (#27245) * Add more device info data * Add attributes to device_info * Refactor sensor * Filter devices * Update following review * Update following review * Add device_Class --- .../components/tradfri/base_class.py | 2 +- homeassistant/components/tradfri/sensor.py | 92 ++++--------------- 2 files changed, 20 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 5fce3c08510543..aa8487b087ec5e 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -61,9 +61,9 @@ def device_info(self): return { "identifiers": {(TRADFRI_DOMAIN, self._device.id)}, - "name": self._name, "manufacturer": info.manufacturer, "model": info.model_number, + "name": self._name, "sw_version": info.firmware_version, "via_device": (TRADFRI_DOMAIN, self._gateway_id), } diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 4877dbbb541f1a..7814daf8f7ad4f 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,20 +1,17 @@ """Support for IKEA Tradfri sensors.""" import logging -from datetime import timedelta -from pytradfri.error import PytradfriError - -from homeassistant.core import callback -from homeassistant.helpers.entity import Entity +from homeassistant.components.tradfri.base_class import TradfriBaseDevice +from homeassistant.const import DEVICE_CLASS_BATTERY from . import KEY_API, KEY_GATEWAY +from .const import CONF_GATEWAY_ID _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=5) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Tradfri config entry.""" + gateway_id = config_entry.data[CONF_GATEWAY_ID] api = hass.data[KEY_API][config_entry.entry_id] gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] @@ -23,84 +20,33 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices = ( dev for dev in all_devices - if not dev.has_light_control and not dev.has_socket_control + if not dev.has_light_control + and not dev.has_socket_control + and not dev.has_blind_control ) - async_add_entities(TradfriDevice(device, api) for device in devices) + if devices: + async_add_entities(TradfriSensor(device, api, gateway_id) for device in devices) -class TradfriDevice(Entity): +class TradfriSensor(TradfriBaseDevice): """The platform class required by Home Assistant.""" - def __init__(self, device, api): + def __init__(self, device, api, gateway_id): """Initialize the device.""" - self._api = api - self._device = None - self._name = None - - self._refresh(device) - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() - - @property - def should_poll(self): - """No polling needed for tradfri.""" - return False + super().__init__(device, api, gateway_id) + self._unique_id = f"{gateway_id}-{device.id}" @property - def name(self): - """Return the display name of this device.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit_of_measurement of the device.""" - return "%" - - @property - def device_state_attributes(self): + def device_class(self): """Return the devices' state attributes.""" - info = self._device.device_info - attrs = { - "manufacturer": info.manufacturer, - "model_number": info.model_number, - "serial": info.serial, - "firmware_version": info.firmware_version, - "power_source": info.power_source_str, - "battery_level": info.battery_level, - } - return attrs + return DEVICE_CLASS_BATTERY @property def state(self): """Return the current state of the device.""" return self._device.device_info.battery_level - @callback - def _async_start_observe(self, exc=None): - """Start observation of light.""" - if exc: - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._device.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, device): - """Refresh the device data.""" - self._device = device - self._name = device.name - - def _observe_update(self, tradfri_device): - """Receive new state data for this device.""" - self._refresh(tradfri_device) - - self.hass.async_create_task(self.async_update_ha_state()) + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of the device.""" + return "%" From dae8cd880183d6e375cf229f6c6375d077507d65 Mon Sep 17 00:00:00 2001 From: Santobert Date: Sun, 6 Oct 2019 20:10:11 +0200 Subject: [PATCH 0640/3953] Bump pybotvac and use new exceptions (#27249) * Bump pybotvac * Fix tests * Remove super calls * Surround some more statements * Correct logger message for vacuum --- .../components/neato/.translations/en.json | 3 +- homeassistant/components/neato/__init__.py | 30 +++++--- homeassistant/components/neato/camera.py | 49 ++++++++---- homeassistant/components/neato/config_flow.py | 6 +- homeassistant/components/neato/const.py | 2 + homeassistant/components/neato/manifest.json | 2 +- homeassistant/components/neato/strings.json | 3 +- homeassistant/components/neato/switch.py | 35 +++++---- homeassistant/components/neato/vacuum.py | 75 +++++++++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/neato/test_config_flow.py | 32 +++++++- 12 files changed, 171 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/neato/.translations/en.json b/homeassistant/components/neato/.translations/en.json index dc13242cc1de87..69cdb48a560ead 100644 --- a/homeassistant/components/neato/.translations/en.json +++ b/homeassistant/components/neato/.translations/en.json @@ -13,7 +13,8 @@ } }, "error": { - "invalid_credentials": "Invalid credentials" + "invalid_credentials": "Invalid credentials", + "unexpected_error": "Unexpected error" }, "create_entry": { "default": "See [Neato documentation]({docs_url})." diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 8fd545c59bbf2a..feaffeaeb6d63c 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -3,7 +3,7 @@ import logging from datetime import timedelta -from requests.exceptions import HTTPError, ConnectionError as ConnError +from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -20,6 +20,7 @@ NEATO_ROBOTS, NEATO_PERSISTENT_MAPS, NEATO_MAP_DATA, + SCAN_INTERVAL_MINUTES, VALID_VENDORS, ) @@ -103,7 +104,12 @@ async def async_setup_entry(hass, entry): _LOGGER.debug("Failed to login to Neato API") return False - await hass.async_add_executor_job(hub.update_robots) + try: + await hass.async_add_executor_job(hub.update_robots) + except NeatoRobotException: + _LOGGER.debug("Failed to connect to Neato API") + return False + for component in ("camera", "vacuum", "switch"): hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -144,17 +150,19 @@ def login(self): self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor ) self.logged_in = True - except (HTTPError, ConnError): - _LOGGER.error("Unable to connect to Neato API") - self.logged_in = False - return - _LOGGER.debug("Successfully connected to Neato API") - self._hass.data[NEATO_ROBOTS] = self.my_neato.robots - self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps - self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + _LOGGER.debug("Successfully connected to Neato API") + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps + self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + except NeatoException as ex: + if isinstance(ex, NeatoLoginException): + _LOGGER.error("Invalid credentials") + else: + _LOGGER.error("Unable to connect to Neato API") + self.logged_in = False - @Throttle(timedelta(seconds=300)) + @Throttle(timedelta(minutes=SCAN_INTERVAL_MINUTES)) def update_robots(self): """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", self._hass.data[NEATO_ROBOTS]) diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index c565fa3d9ac85e..2604c6276a5731 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -2,13 +2,21 @@ from datetime import timedelta import logging +from pybotvac.exceptions import NeatoRobotException + from homeassistant.components.camera import Camera -from .const import NEATO_DOMAIN, NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN +from .const import ( + NEATO_DOMAIN, + NEATO_MAP_DATA, + NEATO_ROBOTS, + NEATO_LOGIN, + SCAN_INTERVAL_MINUTES, +) _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -37,9 +45,9 @@ def __init__(self, hass, robot): """Initialize Neato cleaning map.""" super().__init__() self.robot = robot - self._robot_name = "{} {}".format(self.robot.name, "Cleaning Map") + self.neato = hass.data[NEATO_LOGIN] if NEATO_LOGIN in hass.data else None + self._robot_name = f"{self.robot.name} Cleaning Map" self._robot_serial = self.robot.serial - self.neato = hass.data[NEATO_LOGIN] self._image_url = None self._image = None @@ -50,16 +58,31 @@ def camera_image(self): def update(self): """Check the contents of the map list.""" - self.neato.update_robots() - image_url = None - map_data = self.hass.data[NEATO_MAP_DATA] - image_url = map_data[self._robot_serial]["maps"][0]["url"] - if image_url == self._image_url: - _LOGGER.debug("The map image_url is the same as old") + if self.neato is None: + _LOGGER.error("Error while updating camera") + self._image = None + self._image_url = None return - image = self.neato.download_map(image_url) - self._image = image.read() - self._image_url = image_url + + _LOGGER.debug("Running camera update") + try: + self.neato.update_robots() + + image_url = None + map_data = self.hass.data[NEATO_MAP_DATA][self._robot_serial]["maps"][0] + image_url = map_data["url"] + if image_url == self._image_url: + _LOGGER.debug("The map image_url is the same as old") + return + + image = self.neato.download_map(image_url) + self._image = image.read() + self._image_url = image_url + + except NeatoRobotException as ex: + _LOGGER.error("Neato camera connection error: %s", ex) + self._image = None + self._image_url = None @property def name(self): diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 0c71cdbd069f21..7ece3b8d300e4a 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from requests.exceptions import HTTPError, ConnectionError as ConnError +from pybotvac.exceptions import NeatoLoginException, NeatoRobotException from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -106,7 +106,9 @@ def try_login(username, password, vendor): try: Account(username, password, this_vendor) - except (HTTPError, ConnError): + except NeatoLoginException: return "invalid_credentials" + except NeatoRobotException: + return "unexpected_error" return None diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index 6fb41bda710bc4..4d4178a6875b6d 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -9,6 +9,8 @@ NEATO_MAP_DATA = "neato_map_data" NEATO_PERSISTENT_MAPS = "neato_persistent_maps" +SCAN_INTERVAL_MINUTES = 5 + VALID_VENDORS = ["neato", "vorwerk"] MODE = {1: "Eco", 2: "Turbo"} diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 160f194cd63efe..a4d05e8849ac6f 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", "requirements": [ - "pybotvac==0.0.15" + "pybotvac==0.0.16" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json index dc13242cc1de87..69cdb48a560ead 100644 --- a/homeassistant/components/neato/strings.json +++ b/homeassistant/components/neato/strings.json @@ -13,7 +13,8 @@ } }, "error": { - "invalid_credentials": "Invalid credentials" + "invalid_credentials": "Invalid credentials", + "unexpected_error": "Unexpected error" }, "create_entry": { "default": "See [Neato documentation]({docs_url})." diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 3efee11853d19e..8e85bef23b29b0 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -2,16 +2,16 @@ from datetime import timedelta import logging -import requests +from pybotvac.exceptions import NeatoRobotException from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity -from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS +from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) SWITCH_TYPE_SCHEDULE = "schedule" @@ -44,8 +44,8 @@ def __init__(self, hass, robot, switch_type): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self.neato = hass.data[NEATO_LOGIN] - self._robot_name = "{} {}".format(self.robot.name, SWITCH_TYPES[self.type][0]) + self.neato = hass.data[NEATO_LOGIN] if NEATO_LOGIN in hass.data else None + self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None self._schedule_state = None self._clean_state = None @@ -53,17 +53,20 @@ def __init__(self, hass, robot, switch_type): def update(self): """Update the states of Neato switches.""" + if self.neato is None: + _LOGGER.error("Error while updating switches") + self._state = None + return + _LOGGER.debug("Running switch update") - self.neato.update_robots() try: + self.neato.update_robots() self._state = self.robot.state - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as ex: - _LOGGER.warning("Neato connection error: %s", ex) + except NeatoRobotException as ex: + _LOGGER.error("Neato switch connection error: %s", ex) self._state = None return + _LOGGER.debug("self._state=%s", self._state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug("State: %s", self._state) @@ -104,9 +107,15 @@ def device_info(self): def turn_on(self, **kwargs): """Turn the switch on.""" if self.type == SWITCH_TYPE_SCHEDULE: - self.robot.enable_schedule() + try: + self.robot.enable_schedule() + except NeatoRobotException as ex: + _LOGGER.error("Neato switch connection error: %s", ex) def turn_off(self, **kwargs): """Turn the switch off.""" if self.type == SWITCH_TYPE_SCHEDULE: - self.robot.disable_schedule() + try: + self.robot.disable_schedule() + except NeatoRobotException as ex: + _LOGGER.error("Neato switch connection error: %s", ex) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 96c4e8f3c5f1c3..bdb8cd0875efff 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -2,7 +2,8 @@ from datetime import timedelta import logging -import requests +from pybotvac.exceptions import NeatoRobotException + import voluptuous as vol from homeassistant.components.vacuum import ( @@ -41,11 +42,12 @@ NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, + SCAN_INTERVAL_MINUTES, ) _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=5) +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) SUPPORT_NEATO = ( SUPPORT_BATTERY @@ -154,22 +156,26 @@ def __init__(self, hass, robot): def update(self): """Update the states of Neato Vacuums.""" - _LOGGER.debug("Running Neato Vacuums update") - if self._robot_stats is None: - self._robot_stats = self.robot.get_robot_info().json() + if self.neato is None: + _LOGGER.error("Error while updating vacuum") + self._state = None + self._available = False + return - self.neato.update_robots() try: + _LOGGER.debug("Running Neato Vacuums update") + if self._robot_stats is None: + self._robot_stats = self.robot.get_robot_info().json() + self.neato.update_robots() self._state = self.robot.state self._available = True - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as ex: - _LOGGER.warning("Neato connection error: %s", ex) + except NeatoRobotException as ex: + if self._available: # print only once when available + _LOGGER.error("Neato vacuum connection error: %s", ex) self._state = None self._available = False return + _LOGGER.debug("self._state=%s", self._state) if "alert" in self._state: robot_alert = ALERTS.get(self._state["alert"]) @@ -313,33 +319,51 @@ def device_info(self): def start(self): """Start cleaning or resume cleaning.""" - if self._state["state"] == 1: - self.robot.start_cleaning() - elif self._state["state"] == 3: - self.robot.resume_cleaning() + try: + if self._state["state"] == 1: + self.robot.start_cleaning() + elif self._state["state"] == 3: + self.robot.resume_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def pause(self): """Pause the vacuum.""" - self.robot.pause_cleaning() + try: + self.robot.pause_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - if self._clean_state == STATE_CLEANING: - self.robot.pause_cleaning() - self._clean_state = STATE_RETURNING - self.robot.send_to_base() + try: + if self._clean_state == STATE_CLEANING: + self.robot.pause_cleaning() + self._clean_state = STATE_RETURNING + self.robot.send_to_base() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def stop(self, **kwargs): """Stop the vacuum cleaner.""" - self.robot.stop_cleaning() + try: + self.robot.stop_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def locate(self, **kwargs): """Locate the robot by making it emit a sound.""" - self.robot.locate() + try: + self.robot.locate() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def clean_spot(self, **kwargs): """Run a spot cleaning starting from the base.""" - self.robot.start_spot_cleaning() + try: + self.robot.start_spot_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def neato_custom_cleaning(self, mode, navigation, category, zone=None, **kwargs): """Zone cleaning service call.""" @@ -355,4 +379,7 @@ def neato_custom_cleaning(self, mode, navigation, category, zone=None, **kwargs) return self._clean_state = STATE_CLEANING - self.robot.start_cleaning(mode, navigation, category, boundary_id) + try: + self.robot.start_cleaning(mode, navigation, category, boundary_id) + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) diff --git a/requirements_all.txt b/requirements_all.txt index 39d4ddcda629ab..075884183ff023 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1102,7 +1102,7 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.15 +pybotvac==0.0.16 # homeassistant.components.nissan_leaf pycarwings2==2.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 187e50c4691948..62be2f035a5ad5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -297,7 +297,7 @@ pyMetno==0.4.6 pyblackbird==0.5 # homeassistant.components.neato -pybotvac==0.0.15 +pybotvac==0.0.16 # homeassistant.components.cast pychromecast==4.0.1 diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 99691c101a6747..8eb67e5d3e1444 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -103,11 +103,11 @@ async def test_abort_if_already_setup(hass, account): async def test_abort_on_invalid_credentials(hass): """Test when we have invalid credentials.""" - from requests.exceptions import HTTPError + from pybotvac.exceptions import NeatoLoginException flow = init_config_flow(hass) - with patch("pybotvac.Account", side_effect=HTTPError()): + with patch("pybotvac.Account", side_effect=NeatoLoginException()): result = await flow.async_step_user( { CONF_USERNAME: USERNAME, @@ -127,3 +127,31 @@ async def test_abort_on_invalid_credentials(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "invalid_credentials" + + +async def test_abort_on_unexpected_error(hass): + """Test when we have an unexpected error.""" + from pybotvac.exceptions import NeatoRobotException + + flow = init_config_flow(hass) + + with patch("pybotvac.Account", side_effect=NeatoRobotException()): + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unexpected_error"} + + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unexpected_error" From 02c983d3329734de25028abed320ace8dc4a2bfa Mon Sep 17 00:00:00 2001 From: CQoute Date: Mon, 7 Oct 2019 06:19:31 +1030 Subject: [PATCH 0641/3953] Add 'flash_length' to esphome light async_turn_off (#27214) flash_length was overlooked when fixing the asyn_turn_on flash attribute. async_turn_off is now fixed with the flash attribute. --- homeassistant/components/esphome/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 1205521706eb21..334e7e645a7518 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -91,7 +91,7 @@ async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" data = {"key": self._static_info.key, "state": False} if ATTR_FLASH in kwargs: - data["flash"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] + data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] if ATTR_TRANSITION in kwargs: data["transition_length"] = kwargs[ATTR_TRANSITION] await self._client.light_command(**data) From 3b9f0062a2919d8726c5e557183038aeab84006f Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Sun, 6 Oct 2019 23:02:15 +0200 Subject: [PATCH 0642/3953] Add missing documentation for some Hassio services (#27215) * Add services doc * Add missing services doc and reformat * improve readability * content improvement * HassIO to Hass.io --- homeassistant/components/hassio/services.yaml | 89 ++++++++++++++----- 1 file changed, 68 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 33574c5dd713b4..30314c646b0309 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -1,37 +1,84 @@ addon_install: - description: Install a HassIO docker addon. + description: Install a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} - version: {description: Optional or it will be use the latest version., example: '0.2'} + addon: + description: The add-on slug. + example: core_ssh + version: + description: Optional or it will be use the latest version. + example: "0.2" + addon_start: - description: Start a HassIO docker addon. + description: Start a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} + addon: + description: The add-on slug. + example: core_ssh + +addon_restart: + description: Restart a Hass.io docker add-on. + fields: + addon: + description: The add-on slug. + example: core_ssh + +addon_stdin: + description: Write data to a Hass.io docker add-on stdin . + fields: + addon: + description: The add-on slug. + example: core_ssh + addon_stop: - description: Stop a HassIO docker addon. + description: Stop a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} + addon: + description: The add-on slug. + example: core_ssh + addon_uninstall: - description: Uninstall a HassIO docker addon. + description: Uninstall a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} + addon: + description: The add-on slug. + example: core_ssh + addon_update: - description: Update a HassIO docker addon. + description: Update a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} - version: {description: Optional or it will be use the latest version., example: '0.2'} + addon: + description: The add-on slug. + example: core_ssh + version: + description: Optional or it will be use the latest version. + example: "0.2" + homeassistant_update: - description: Update HomeAssistant docker image. + description: Update the Home Assistant docker image. fields: - version: {description: Optional or it will be use the latest version., example: 0.40.1} -host_reboot: {description: Reboot host computer.} -host_shutdown: {description: Poweroff host computer.} + version: + description: Optional or it will be use the latest version. + example: 0.40.1 + +host_reboot: + description: Reboot the host system. + +host_shutdown: + description: Poweroff the host system. + host_update: - description: Update host computer. + description: Update the host system. fields: - version: {description: Optional or it will be use the latest version., example: '0.3'} -supervisor_reload: {description: Reload HassIO supervisor addons/updates/configs.} + version: + description: Optional or it will be use the latest version. + example: "0.3" + +supervisor_reload: + description: Reload the Hass.io supervisor. + supervisor_update: - description: Update HassIO supervisor. + description: Update the Hass.io supervisor. fields: - version: {description: Optional or it will be use the latest version., example: '0.3'} + version: + description: Optional or it will be use the latest version. + example: "0.3" From 073bdd672a53f526be548d914adc7fe98cec4485 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 7 Oct 2019 00:32:19 +0000 Subject: [PATCH 0643/3953] [ci skip] Translation update --- .../components/airly/.translations/de.json | 18 +++++++++++ .../components/airly/.translations/es.json | 22 ++++++++++++++ .../components/airly/.translations/fr.json | 11 +++++++ .../components/deconz/.translations/de.json | 10 +++++++ .../components/deconz/.translations/es.json | 1 + .../components/light/.translations/de.json | 9 ++++++ .../components/met/.translations/de.json | 2 +- .../components/neato/.translations/da.json | 22 ++++++++++++++ .../components/neato/.translations/de.json | 26 ++++++++++++++++ .../components/neato/.translations/en.json | 30 +++++++++---------- .../components/neato/.translations/es.json | 26 ++++++++++++++++ .../components/neato/.translations/no.json | 26 ++++++++++++++++ .../components/neato/.translations/ru.json | 26 ++++++++++++++++ .../components/neato/.translations/sl.json | 10 +++++++ .../opentherm_gw/.translations/da.json | 20 +++++++++++++ .../opentherm_gw/.translations/de.json | 19 ++++++++++++ .../opentherm_gw/.translations/es.json | 23 ++++++++++++++ .../opentherm_gw/.translations/fr.json | 13 ++++++++ .../opentherm_gw/.translations/no.json | 11 ++++++- .../components/plex/.translations/de.json | 9 ++++++ .../components/plex/.translations/es.json | 1 + .../components/sensor/.translations/es.json | 26 ++++++++++++++++ .../components/soma/.translations/es.json | 13 ++++++++ .../components/unifi/.translations/da.json | 5 ++++ 24 files changed, 362 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/airly/.translations/de.json create mode 100644 homeassistant/components/airly/.translations/es.json create mode 100644 homeassistant/components/airly/.translations/fr.json create mode 100644 homeassistant/components/neato/.translations/da.json create mode 100644 homeassistant/components/neato/.translations/de.json create mode 100644 homeassistant/components/neato/.translations/es.json create mode 100644 homeassistant/components/neato/.translations/no.json create mode 100644 homeassistant/components/neato/.translations/ru.json create mode 100644 homeassistant/components/neato/.translations/sl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/da.json create mode 100644 homeassistant/components/opentherm_gw/.translations/de.json create mode 100644 homeassistant/components/opentherm_gw/.translations/es.json create mode 100644 homeassistant/components/opentherm_gw/.translations/fr.json create mode 100644 homeassistant/components/plex/.translations/de.json create mode 100644 homeassistant/components/sensor/.translations/es.json create mode 100644 homeassistant/components/soma/.translations/es.json diff --git a/homeassistant/components/airly/.translations/de.json b/homeassistant/components/airly/.translations/de.json new file mode 100644 index 00000000000000..cb290dc46c087e --- /dev/null +++ b/homeassistant/components/airly/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "Name existiert bereits" + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name der Integration" + }, + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/es.json b/homeassistant/components/airly/.translations/es.json new file mode 100644 index 00000000000000..0c29ad0bc668b9 --- /dev/null +++ b/homeassistant/components/airly/.translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clave de la API no es correcta.", + "name_exists": "El nombre ya existe.", + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API de Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre de la integraci\u00f3n" + }, + "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/fr.json b/homeassistant/components/airly/.translations/fr.json new file mode 100644 index 00000000000000..19561130e15902 --- /dev/null +++ b/homeassistant/components/airly/.translations/fr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nom de l'int\u00e9gration" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 97e25e28965c5f..830ae0fd13f2f1 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -41,6 +41,16 @@ }, "title": "deCONZ Zigbee Gateway" }, + "device_automation": { + "trigger_subtype": { + "close": "Schlie\u00dfen", + "left": "Links", + "open": "Offen", + "right": "Rechts", + "turn_off": "Ausschalten", + "turn_on": "Einschalten" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index cb5db0b8348ea4..04a08d185b30fb 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", + "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtipo}\" detenido", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json index e07adeb0a36e84..be8966d95569af 100644 --- a/homeassistant/components/light/.translations/de.json +++ b/homeassistant/components/light/.translations/de.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Schalte {entity_name} um.", + "turn_off": "Schalte {entity_name} aus.", + "turn_on": "Schalte {entity_name} ein." + }, + "condition_type": { + "is_off": "{entity_name} ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet" + }, "trigger_type": { "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet" diff --git a/homeassistant/components/met/.translations/de.json b/homeassistant/components/met/.translations/de.json index b70d3f12a838ce..2fd772c8619bdd 100644 --- a/homeassistant/components/met/.translations/de.json +++ b/homeassistant/components/met/.translations/de.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Name existiert bereits" + "name_exists": "Ort existiert bereits" }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json new file mode 100644 index 00000000000000..61c594e1011efa --- /dev/null +++ b/homeassistant/components/neato/.translations/da.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Allerede konfigureret", + "invalid_credentials": "Ugyldige legitimationsoplysninger" + }, + "error": { + "invalid_credentials": "Ugyldige legitimationsoplysninger" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "description": "Se [Neato-dokumentation] ({docs_url}).", + "title": "Neato kontooplysninger" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/de.json b/homeassistant/components/neato/.translations/de.json new file mode 100644 index 00000000000000..a3f54f9f69afd3 --- /dev/null +++ b/homeassistant/components/neato/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Bereits konfiguriert", + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + }, + "create_entry": { + "default": "Siehe [Neato-Dokumentation]({docs_url})." + }, + "error": { + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername", + "vendor": "Hersteller" + }, + "description": "Siehe [Neato-Dokumentation]({docs_url}).", + "title": "Neato-Kontoinformationen" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/en.json b/homeassistant/components/neato/.translations/en.json index 69cdb48a560ead..73628c8646e849 100644 --- a/homeassistant/components/neato/.translations/en.json +++ b/homeassistant/components/neato/.translations/en.json @@ -1,27 +1,27 @@ { "config": { - "title": "Neato", + "abort": { + "already_configured": "Already configured", + "invalid_credentials": "Invalid credentials" + }, + "create_entry": { + "default": "See [Neato documentation]({docs_url})." + }, + "error": { + "invalid_credentials": "Invalid credentials", + "unexpected_error": "Unexpected error" + }, "step": { "user": { - "title": "Neato Account Info", "data": { - "username": "Username", "password": "Password", + "username": "Username", "vendor": "Vendor" }, - "description": "See [Neato documentation]({docs_url})." + "description": "See [Neato documentation]({docs_url}).", + "title": "Neato Account Info" } }, - "error": { - "invalid_credentials": "Invalid credentials", - "unexpected_error": "Unexpected error" - }, - "create_entry": { - "default": "See [Neato documentation]({docs_url})." - }, - "abort": { - "already_configured": "Already configured", - "invalid_credentials": "Invalid credentials" - } + "title": "Neato" } } \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/es.json b/homeassistant/components/neato/.translations/es.json new file mode 100644 index 00000000000000..d033b8af6a47d6 --- /dev/null +++ b/homeassistant/components/neato/.translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ya est\u00e1 configurado", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "create_entry": { + "default": "Ver [documentaci\u00f3n Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario", + "vendor": "Vendedor" + }, + "description": "Ver [documentaci\u00f3n Neato]({docs_url}).", + "title": "Informaci\u00f3n de la cuenta de Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/no.json b/homeassistant/components/neato/.translations/no.json new file mode 100644 index 00000000000000..d869fa59a18a51 --- /dev/null +++ b/homeassistant/components/neato/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Allerede konfigurert", + "invalid_credentials": "Ugyldig brukerinformasjon" + }, + "create_entry": { + "default": "Se [Neato dokumentasjon]({docs_url})." + }, + "error": { + "invalid_credentials": "Ugyldig brukerinformasjon", + "unexpected_error": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn", + "vendor": "Leverand\u00f8r" + }, + "title": "Neato kontoinformasjon" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json new file mode 100644 index 00000000000000..2d08deb2b1313b --- /dev/null +++ b/homeassistant/components/neato/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + }, + "create_entry": { + "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + }, + "error": { + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d", + "vendor": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c" + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "title": "Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/sl.json b/homeassistant/components/neato/.translations/sl.json new file mode 100644 index 00000000000000..1d256918617a5d --- /dev/null +++ b/homeassistant/components/neato/.translations/sl.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Podatki o ra\u010dunu Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json new file mode 100644 index 00000000000000..b8abb48af4e89c --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "already_configured": "Gateway allerede konfigureret", + "id_exists": "Gateway-id findes allerede", + "serial_error": "Fejl ved tilslutning til enheden" + }, + "step": { + "init": { + "data": { + "device": "Sti eller URL", + "id": "ID", + "name": "Navn" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json new file mode 100644 index 00000000000000..274dd46488b8d6 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Gateway bereits konfiguriert", + "id_exists": "Gateway-ID ist bereits vorhanden", + "serial_error": "Fehler beim Verbinden mit dem Ger\u00e4t", + "timeout": "Zeit\u00fcberschreitung beim Verbindungsversuch" + }, + "step": { + "init": { + "data": { + "device": "Pfad oder URL", + "id": "ID", + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/es.json b/homeassistant/components/opentherm_gw/.translations/es.json new file mode 100644 index 00000000000000..8ad9d89b07a57d --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ya configurado", + "id_exists": "El ID del Gateway ya existe", + "serial_error": "Error de conexi\u00f3n al dispositivo", + "timeout": "Intento de conexi\u00f3n agotado" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "floor_temperature": "Temperatura del suelo", + "id": "ID", + "name": "Nombre", + "precision": "Precisi\u00f3n de la temperatura clim\u00e1tica" + }, + "title": "Gateway OpenTherm" + } + }, + "title": "Gateway OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json new file mode 100644 index 00000000000000..a9f9acd9045773 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "init": { + "data": { + "device": "Chemin ou URL", + "id": "ID", + "name": "Nom" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json index a1df80f3b12c72..6104aa7de724cb 100644 --- a/homeassistant/components/opentherm_gw/.translations/no.json +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -1,10 +1,19 @@ { "config": { + "error": { + "already_configured": "Gateway er allerede konfigurert", + "id_exists": "Gateway-ID finnes allerede", + "serial_error": "Feil ved tilkobling til enhet", + "timeout": "Tilkoblingsfors\u00f8k ble tidsavbrutt" + }, "step": { "init": { "data": { + "device": "Bane eller URL-adresse", + "floor_temperature": "Gulv klimatemperatur", "id": "ID", - "name": "Navn" + "name": "Navn", + "precision": "Klima temperaturpresisjon" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json new file mode 100644 index 00000000000000..210fe7323602c3 --- /dev/null +++ b/homeassistant/components/plex/.translations/de.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json index 6d1ad1f62daa8c..45417d09d02127 100644 --- a/homeassistant/components/plex/.translations/es.json +++ b/homeassistant/components/plex/.translations/es.json @@ -5,6 +5,7 @@ "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "Plex se est\u00e1 configurando", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Fall\u00f3 por razones desconocidas" }, "error": { diff --git a/homeassistant/components/sensor/.translations/es.json b/homeassistant/components/sensor/.translations/es.json new file mode 100644 index 00000000000000..a9039d2e410cf7 --- /dev/null +++ b/homeassistant/components/sensor/.translations/es.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} nivel de bater\u00eda", + "is_humidity": "{entity_name} humedad", + "is_illuminance": "{entity_name} iluminancia", + "is_power": "{entity_name} alimentaci\u00f3n", + "is_pressure": "{entity_name} presi\u00f3n", + "is_signal_strength": "{entity_name} intensidad de la se\u00f1al", + "is_temperature": "{entity_name} temperatura", + "is_timestamp": "{entity_name} marca de tiempo", + "is_value": "{entity_name} valor" + }, + "trigger_type": { + "battery_level": "{entity_name} nivel de bater\u00eda", + "humidity": "{entity_name} humedad", + "illuminance": "{entity_name} iluminancia", + "power": "{entity_name} alimentaci\u00f3n", + "pressure": "{entity_name} presi\u00f3n", + "signal_strength": "{entity_name} intensidad de la se\u00f1al", + "temperature": "{entity_name} temperatura", + "timestamp": "{entity_name} marca de tiempo", + "value": "{entity_name} valor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json new file mode 100644 index 00000000000000..8126b6ea5aec63 --- /dev/null +++ b/homeassistant/components/soma/.translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", + "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json index 53b794ed4353ff..0d0315e49c752d 100644 --- a/homeassistant/components/unifi/.translations/da.json +++ b/homeassistant/components/unifi/.translations/da.json @@ -32,6 +32,11 @@ "track_devices": "Spor netv\u00e6rksenheder (Ubiquiti-enheder)", "track_wired_clients": "Inkluder kablede netv\u00e6rksklienter" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Opret b\u00e5ndbredde sensorer for netv\u00e6rksklienter" + } } } } From 0915d927dfa6d52519cda84b64ab521ecc8f4c93 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 6 Oct 2019 23:02:58 -0500 Subject: [PATCH 0644/3953] Fix Plex media_player.play_media service (#27278) * First attempt to fix play_media * More changes to media playback * Use playqueues, clean up play_media * Use similar function name, add docstring --- homeassistant/components/plex/media_player.py | 139 ++++++++---------- homeassistant/components/plex/server.py | 14 ++ 2 files changed, 76 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 356c7fe5741702..a49e4c9c057ee1 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -2,10 +2,9 @@ from datetime import timedelta import json import logging +from xml.etree.ElementTree import ParseError import plexapi.exceptions -import plexapi.playlist -import plexapi.playqueue import requests.exceptions from homeassistant.components.media_player import MediaPlayerDevice @@ -16,6 +15,7 @@ SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -543,9 +543,6 @@ def make(self): @property def supported_features(self): """Flag media player features that are supported.""" - if not self._is_player_active: - return 0 - # force show all controls if self.plex_server.show_all_controls: return ( @@ -555,13 +552,11 @@ def supported_features(self): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE ) - # only show controls when we know what device is connecting - if not self._make: - return 0 # no mute support if self.make.lower() == "shield android tv": _LOGGER.debug( @@ -575,8 +570,10 @@ def supported_features(self): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF ) + # Only supports play,pause,stop (and off which really is stop) if self.make.lower().startswith("tivo"): _LOGGER.debug( @@ -585,8 +582,7 @@ def supported_features(self): self.entity_id, ) return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF - # Not all devices support playback functionality - # Playback includes volume, stop/play/pause, etc. + if self.device and "playback" in self._device_protocol_capabilities: return ( SUPPORT_PAUSE @@ -595,6 +591,7 @@ def supported_features(self): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE ) @@ -682,49 +679,74 @@ def play_media(self, media_type, media_id, **kwargs): return src = json.loads(media_id) + library = src.get("library_name") + shuffle = src.get("shuffle", 0) media = None + if media_type == "MUSIC": - media = ( - self.device.server.library.section(src["library_name"]) - .get(src["artist_name"]) - .album(src["album_name"]) - .get(src["track_name"]) - ) + media = self._get_music_media(library, src) elif media_type == "EPISODE": - media = self._get_tv_media( - src["library_name"], - src["show_name"], - src["season_number"], - src["episode_number"], - ) + media = self._get_tv_media(library, src) elif media_type == "PLAYLIST": - media = self.device.server.playlist(src["playlist_name"]) + media = self.plex_server.playlist(src["playlist_name"]) elif media_type == "VIDEO": - media = self.device.server.library.section(src["library_name"]).get( - src["video_name"] - ) + media = self.plex_server.library.section(library).get(src["video_name"]) - if ( - media - and media_type == "EPISODE" - and isinstance(media, plexapi.playlist.Playlist) - ): - # delete episode playlist after being loaded into a play queue - self._client_play_media(media=media, delete=True, shuffle=src["shuffle"]) - elif media: - self._client_play_media(media=media, shuffle=src["shuffle"]) + if media is None: + _LOGGER.error("Media could not be found: %s", media_id) + return + + playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) + try: + self.device.playMedia(playqueue) + except ParseError: + # Temporary workaround for Plexamp / plexapi issue + pass + except requests.exceptions.ConnectTimeout: + _LOGGER.error("Timed out playing on %s", self.name) + + self.update_devices() - def _get_tv_media(self, library_name, show_name, season_number, episode_number): + def _get_music_media(self, library_name, src): + """Find music media and return a Plex media object.""" + artist_name = src["artist_name"] + album_name = src.get("album_name") + track_name = src.get("track_name") + track_number = src.get("track_number") + + artist = self.plex_server.library.section(library_name).get(artist_name) + + if album_name: + album = artist.album(album_name) + + if track_name: + return album.track(track_name) + + if track_number: + for track in album.tracks(): + if int(track.index) == int(track_number): + return track + return None + + return album + + if track_name: + return artist.searchTracks(track_name, maxresults=1) + return artist + + def _get_tv_media(self, library_name, src): """Find TV media and return a Plex media object.""" + show_name = src["show_name"] + season_number = src.get("season_number") + episode_number = src.get("episode_number") target_season = None target_episode = None - show = self.device.server.library.section(library_name).get(show_name) + show = self.plex_server.library.section(library_name).get(show_name) if not season_number: - playlist_name = f"{self.entity_id} - {show_name} Episodes" - return self.device.server.createPlaylist(playlist_name, show.episodes()) + return show for season in show.seasons(): if int(season.seasonNumber) == int(season_number): @@ -741,12 +763,7 @@ def _get_tv_media(self, library_name, show_name, season_number, episode_number): ) else: if not episode_number: - playlist_name = "{} - {} Season {} Episodes".format( - self.entity_id, show_name, str(season_number) - ) - return self.device.server.createPlaylist( - playlist_name, target_season.episodes() - ) + return target_season for episode in target_season.episodes(): if int(episode.index) == int(episode_number): @@ -764,38 +781,6 @@ def _get_tv_media(self, library_name, show_name, season_number, episode_number): return target_episode - def _client_play_media(self, media, delete=False, **params): - """Instruct Plex client to play a piece of media.""" - if not (self.device and "playback" in self._device_protocol_capabilities): - _LOGGER.error("Client cannot play media: %s", self.entity_id) - return - - playqueue = plexapi.playqueue.PlayQueue.create( - self.device.server, media, **params - ) - - # Delete dynamic playlists used to build playqueue (ex. play tv season) - if delete: - media.delete() - - server_url = self.device.server.baseurl.split(":") - self.device.sendCommand( - "playback/playMedia", - **dict( - { - "machineIdentifier": self.device.server.machineIdentifier, - "address": server_url[1].strip("/"), - "port": server_url[-1], - "key": media.key, - "containerKey": "/playQueues/{}?window=100&own=1".format( - playqueue.playQueueID - ), - }, - **params, - ), - ) - self.update_devices() - @property def device_state_attributes(self): """Return the scene state attributes.""" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d4393d38c97427..df9e9f9f6c3528 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,5 +1,6 @@ """Shared class to maintain Plex server instances.""" import plexapi.myplex +import plexapi.playqueue import plexapi.server from requests import Session @@ -109,3 +110,16 @@ def use_episode_art(self): def show_all_controls(self): """Return show_all_controls option.""" return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] + + @property + def library(self): + """Return library attribute from server object.""" + return self._plex_server.library + + def playlist(self, title): + """Return playlist from server object.""" + return self._plex_server.playlist(title) + + def create_playqueue(self, media, **kwargs): + """Create playqueue on Plex server.""" + return plexapi.playqueue.PlayQueue.create(self._plex_server, media, **kwargs) From 5d1dd6390d28e12d4cc60a947e17e41aef9009b7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Oct 2019 06:06:16 +0200 Subject: [PATCH 0645/3953] Validate generated condition (#27263) --- .../components/binary_sensor/device_condition.py | 2 +- .../device_automation/toggle_entity.py | 6 ++---- .../components/light/device_condition.py | 2 +- .../components/sensor/device_condition.py | 16 +++++++--------- .../components/switch/device_condition.py | 2 +- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index d686ef412c1ed3..a38c0c09ee5d26 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -248,7 +248,7 @@ def async_condition_from_config( if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - return condition.state_from_config(state_config, config_validation) + return condition.state_from_config(state_config) async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 47953dc5e81d44..af29625f3a1498 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -119,9 +119,7 @@ async def async_call_action_from_config( ) -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" condition_type = config[CONF_TYPE] if condition_type == CONF_IS_ON: @@ -136,7 +134,7 @@ def async_condition_from_config( if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - return condition.state_from_config(state_config, config_validation) + return condition.state_from_config(state_config) async def async_attach_trigger( diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index 86f5761ddf565a..0b3cecbea41264 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -21,7 +21,7 @@ def async_condition_from_config( """Evaluate state based on configuration.""" if config_validation: config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) + return toggle_entity.async_condition_from_config(config) async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 76f1b3909ef620..18aa46d78e1edf 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -3,14 +3,12 @@ import voluptuous as vol from homeassistant.core import HomeAssistant -import homeassistant.components.automation.numeric_state as numeric_state_automation from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, CONF_ABOVE, CONF_BELOW, CONF_ENTITY_ID, - CONF_FOR, CONF_TYPE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, @@ -132,12 +130,12 @@ def async_condition_from_config( if config_validation: config = CONDITION_SCHEMA(config) numeric_state_config = { - numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE), - numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW), - numeric_state_automation.CONF_FOR: config.get(CONF_FOR), + condition.CONF_CONDITION: "numeric_state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], } + if CONF_ABOVE in config: + numeric_state_config[condition.CONF_ABOVE] = config[CONF_ABOVE] + if CONF_BELOW in config: + numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW] - return condition.async_numeric_state_from_config( - numeric_state_config, config_validation - ) + return condition.async_numeric_state_from_config(numeric_state_config) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index f3d5903bcf31f5..7df972151c7ca3 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -21,7 +21,7 @@ def async_condition_from_config( """Evaluate state based on configuration.""" if config_validation: config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) + return toggle_entity.async_condition_from_config(config) async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: From 41242110957cbab8b27fb1bfeefa3a37f5cd92dd Mon Sep 17 00:00:00 2001 From: Santobert Date: Mon, 7 Oct 2019 08:30:49 +0200 Subject: [PATCH 0646/3953] Add attributes to neato integration (#27260) * inital commit * simplify self.neato --- homeassistant/components/neato/camera.py | 28 ++++++- homeassistant/components/neato/switch.py | 11 ++- homeassistant/components/neato/vacuum.py | 101 ++++++++++++----------- 3 files changed, 85 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 2604c6276a5731..d1f86ea6637b57 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -17,6 +17,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) +ATTR_GENERATED_AT = "generated_at" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -45,9 +46,11 @@ def __init__(self, hass, robot): """Initialize Neato cleaning map.""" super().__init__() self.robot = robot - self.neato = hass.data[NEATO_LOGIN] if NEATO_LOGIN in hass.data else None + self.neato = hass.data.get(NEATO_LOGIN) + self._available = self.neato.logged_in if self.neato is not None else False self._robot_name = f"{self.robot.name} Cleaning Map" self._robot_serial = self.robot.serial + self._generated_at = None self._image_url = None self._image = None @@ -62,6 +65,7 @@ def update(self): _LOGGER.error("Error while updating camera") self._image = None self._image_url = None + self._available = False return _LOGGER.debug("Running camera update") @@ -78,11 +82,14 @@ def update(self): image = self.neato.download_map(image_url) self._image = image.read() self._image_url = image_url - + self._generated_at = (map_data["generated_at"].strip("Z")).replace("T", " ") + self._available = True except NeatoRobotException as ex: - _LOGGER.error("Neato camera connection error: %s", ex) + if self._available: # Print only once when available + _LOGGER.error("Neato camera connection error: %s", ex) self._image = None self._image_url = None + self._available = False @property def name(self): @@ -94,7 +101,22 @@ def unique_id(self): """Return unique ID.""" return self._robot_serial + @property + def available(self): + """Return if the robot is available.""" + return self._available + @property def device_info(self): """Device info for neato robot.""" return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} + + @property + def device_state_attributes(self): + """Return the state attributes of the vacuum cleaner.""" + data = {} + + if self._generated_at is not None: + data[ATTR_GENERATED_AT] = self._generated_at + + return data diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 8e85bef23b29b0..94d92c857fe2cf 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -44,7 +44,8 @@ def __init__(self, hass, robot, switch_type): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self.neato = hass.data[NEATO_LOGIN] if NEATO_LOGIN in hass.data else None + self.neato = hass.data.get(NEATO_LOGIN) + self._available = self.neato.logged_in if self.neato is not None else False self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None self._schedule_state = None @@ -56,15 +57,19 @@ def update(self): if self.neato is None: _LOGGER.error("Error while updating switches") self._state = None + self._available = False return _LOGGER.debug("Running switch update") try: self.neato.update_robots() self._state = self.robot.state + self._available = True except NeatoRobotException as ex: - _LOGGER.error("Neato switch connection error: %s", ex) + if self._available: # Print only once when available + _LOGGER.error("Neato switch connection error: %s", ex) self._state = None + self._available = False return _LOGGER.debug("self._state=%s", self._state) @@ -84,7 +89,7 @@ def name(self): @property def available(self): """Return True if entity is available.""" - return self._state + return self._available @property def unique_id(self): diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index bdb8cd0875efff..bf30b31eee765c 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -7,8 +7,6 @@ import voluptuous as vol from homeassistant.components.vacuum import ( - ATTR_BATTERY_ICON, - ATTR_BATTERY_LEVEL, ATTR_STATUS, DOMAIN, STATE_CLEANING, @@ -68,6 +66,9 @@ ATTR_CLEAN_BATTERY_END = "battery_level_at_clean_end" ATTR_CLEAN_SUSP_COUNT = "clean_suspension_count" ATTR_CLEAN_SUSP_TIME = "clean_suspension_time" +ATTR_CLEAN_PAUSE_TIME = "clean_pause_time" +ATTR_CLEAN_ERROR_TIME = "clean_error_time" +ATTR_LAUNCHED_FROM = "launched_from" ATTR_NAVIGATION = "navigation" ATTR_CATEGORY = "category" @@ -133,20 +134,23 @@ class NeatoConnectedVacuum(StateVacuumDevice): def __init__(self, hass, robot): """Initialize the Neato Connected Vacuum.""" self.robot = robot - self.neato = hass.data[NEATO_LOGIN] + self.neato = hass.data.get(NEATO_LOGIN) + self._available = self.neato.logged_in if self.neato is not None else False self._name = f"{self.robot.name}" self._status_state = None self._clean_state = None self._state = None self._mapdata = hass.data[NEATO_MAP_DATA] - self.clean_time_start = None - self.clean_time_stop = None - self.clean_area = None - self.clean_battery_start = None - self.clean_battery_end = None - self.clean_suspension_charge_count = None - self.clean_suspension_time = None - self._available = False + self._clean_time_start = None + self._clean_time_stop = None + self._clean_area = None + self._clean_battery_start = None + self._clean_battery_end = None + self._clean_susp_charge_count = None + self._clean_susp_time = None + self._clean_pause_time = None + self._clean_error_time = None + self._launched_from = None self._battery_level = None self._robot_serial = self.robot.serial self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS] @@ -218,25 +222,18 @@ def update(self): if not self._mapdata.get(self._robot_serial, {}).get("maps", []): return - self.clean_time_start = ( - self._mapdata[self._robot_serial]["maps"][0]["start_at"].strip("Z") - ).replace("T", " ") - self.clean_time_stop = ( - self._mapdata[self._robot_serial]["maps"][0]["end_at"].strip("Z") - ).replace("T", " ") - self.clean_area = self._mapdata[self._robot_serial]["maps"][0]["cleaned_area"] - self.clean_suspension_charge_count = self._mapdata[self._robot_serial]["maps"][ - 0 - ]["suspended_cleaning_charging_count"] - self.clean_suspension_time = self._mapdata[self._robot_serial]["maps"][0][ - "time_in_suspended_cleaning" - ] - self.clean_battery_start = self._mapdata[self._robot_serial]["maps"][0][ - "run_charge_at_start" - ] - self.clean_battery_end = self._mapdata[self._robot_serial]["maps"][0][ - "run_charge_at_end" - ] + + mapdata = self._mapdata[self._robot_serial]["maps"][0] + self._clean_time_start = (mapdata["start_at"].strip("Z")).replace("T", " ") + self._clean_time_stop = (mapdata["end_at"].strip("Z")).replace("T", " ") + self._clean_area = mapdata["cleaned_area"] + self._clean_susp_charge_count = mapdata["suspended_cleaning_charging_count"] + self._clean_susp_time = mapdata["time_in_suspended_cleaning"] + self._clean_pause_time = mapdata["time_in_pause"] + self._clean_error_time = mapdata["time_in_error"] + self._clean_battery_start = mapdata["run_charge_at_start"] + self._clean_battery_end = mapdata["run_charge_at_end"] + self._launched_from = mapdata["launched_from"] if self._robot_has_map: if self._state["availableServices"]["maps"] != "basic-1": @@ -267,6 +264,11 @@ def available(self): """Return if the robot is available.""" return self._available + @property + def icon(self): + """Return neato specific icon.""" + return "mdi:robot-vacuum-variant" + @property def state(self): """Return the status of the vacuum cleaner.""" @@ -284,25 +286,26 @@ def device_state_attributes(self): if self._status_state is not None: data[ATTR_STATUS] = self._status_state - - if self.battery_level is not None: - data[ATTR_BATTERY_LEVEL] = self.battery_level - data[ATTR_BATTERY_ICON] = self.battery_icon - - if self.clean_time_start is not None: - data[ATTR_CLEAN_START] = self.clean_time_start - if self.clean_time_stop is not None: - data[ATTR_CLEAN_STOP] = self.clean_time_stop - if self.clean_area is not None: - data[ATTR_CLEAN_AREA] = self.clean_area - if self.clean_suspension_charge_count is not None: - data[ATTR_CLEAN_SUSP_COUNT] = self.clean_suspension_charge_count - if self.clean_suspension_time is not None: - data[ATTR_CLEAN_SUSP_TIME] = self.clean_suspension_time - if self.clean_battery_start is not None: - data[ATTR_CLEAN_BATTERY_START] = self.clean_battery_start - if self.clean_battery_end is not None: - data[ATTR_CLEAN_BATTERY_END] = self.clean_battery_end + if self._clean_time_start is not None: + data[ATTR_CLEAN_START] = self._clean_time_start + if self._clean_time_stop is not None: + data[ATTR_CLEAN_STOP] = self._clean_time_stop + if self._clean_area is not None: + data[ATTR_CLEAN_AREA] = self._clean_area + if self._clean_susp_charge_count is not None: + data[ATTR_CLEAN_SUSP_COUNT] = self._clean_susp_charge_count + if self._clean_susp_time is not None: + data[ATTR_CLEAN_SUSP_TIME] = self._clean_susp_time + if self._clean_pause_time is not None: + data[ATTR_CLEAN_PAUSE_TIME] = self._clean_pause_time + if self._clean_error_time is not None: + data[ATTR_CLEAN_ERROR_TIME] = self._clean_error_time + if self._clean_battery_start is not None: + data[ATTR_CLEAN_BATTERY_START] = self._clean_battery_start + if self._clean_battery_end is not None: + data[ATTR_CLEAN_BATTERY_END] = self._clean_battery_end + if self._launched_from is not None: + data[ATTR_LAUNCHED_FROM] = self._launched_from return data From f6b8cffeafd05a88ab34c2de7d364f7f54fc8aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Mon, 7 Oct 2019 13:17:43 +0200 Subject: [PATCH 0647/3953] Add PTZ support to Foscam camera component (#27238) * Add PTZ support to Foscam camera component * Address review comments: - Move service to foscam domain - Use `dict[key]` for required schema keys or with defaults - Fix sync operations in async context - Remove excessive logging * Fix import order * Move all the initialization to setup_platform and fix motion detection status logic * Move function dictionary out of the function. * Change user input to lowercase snake case * Change user input to lowercase snake case * Fix service example value * Omit foscam const module from code coverage tests * Add myself to foscam codeowners --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/foscam/camera.py | 194 +++++++++++++++--- homeassistant/components/foscam/const.py | 5 + homeassistant/components/foscam/manifest.json | 2 +- homeassistant/components/foscam/services.yaml | 12 ++ 6 files changed, 186 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/foscam/const.py create mode 100644 homeassistant/components/foscam/services.yaml diff --git a/.coveragerc b/.coveragerc index 6f3dfbc94a832e..8253b5522f76bf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -224,6 +224,7 @@ omit = homeassistant/components/fortios/device_tracker.py homeassistant/components/fortigate/* homeassistant/components/foscam/camera.py + homeassistant/components/foscam/const.py homeassistant/components/foursquare/* homeassistant/components/free_mobile/notify.py homeassistant/components/freebox/* diff --git a/CODEOWNERS b/CODEOWNERS index ba4058d5acf8db..d550f3e6924d92 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -98,6 +98,7 @@ homeassistant/components/flock/* @fabaff homeassistant/components/flunearyou/* @bachya homeassistant/components/fortigate/* @kifeo homeassistant/components/fortios/* @kimfrellsen +homeassistant/components/foscam/* @skgsergio homeassistant/components/foursquare/* @robbiet480 homeassistant/components/freebox/* @snoof85 homeassistant/components/fronius/* @nielstron diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 63e9956d0dfaf6..0e2ca4073bfa51 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -1,11 +1,26 @@ """This component provides basic support for Foscam IP cameras.""" import logging +import asyncio + +from libpyfoscam import FoscamCamera import voluptuous as vol from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM -from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT +from homeassistant.const import ( + CONF_NAME, + CONF_USERNAME, + CONF_PASSWORD, + CONF_PORT, + ATTR_ENTITY_ID, +) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.service import async_extract_entity_ids + +from .const import DOMAIN as FOSCAM_DOMAIN +from .const import DATA as FOSCAM_DATA +from .const import ENTITIES as FOSCAM_ENTITIES + _LOGGER = logging.getLogger(__name__) @@ -15,7 +30,32 @@ DEFAULT_NAME = "Foscam Camera" DEFAULT_PORT = 88 -FOSCAM_COMM_ERROR = -8 +SERVICE_PTZ = "ptz" +ATTR_MOVEMENT = "movement" +ATTR_TRAVELTIME = "travel_time" + +DEFAULT_TRAVELTIME = 0.125 + +DIR_UP = "up" +DIR_DOWN = "down" +DIR_LEFT = "left" +DIR_RIGHT = "right" + +DIR_TOPLEFT = "top_left" +DIR_TOPRIGHT = "top_right" +DIR_BOTTOMLEFT = "bottom_left" +DIR_BOTTOMRIGHT = "bottom_right" + +MOVEMENT_ATTRS = { + DIR_UP: "ptz_move_up", + DIR_DOWN: "ptz_move_down", + DIR_LEFT: "ptz_move_left", + DIR_RIGHT: "ptz_move_right", + DIR_TOPLEFT: "ptz_move_top_left", + DIR_TOPRIGHT: "ptz_move_top_right", + DIR_BOTTOMLEFT: "ptz_move_bottom_left", + DIR_BOTTOMRIGHT: "ptz_move_bottom_right", +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -28,44 +68,114 @@ } ) +SERVICE_PTZ_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_MOVEMENT): vol.In( + [ + DIR_UP, + DIR_DOWN, + DIR_LEFT, + DIR_RIGHT, + DIR_TOPLEFT, + DIR_TOPRIGHT, + DIR_BOTTOMLEFT, + DIR_BOTTOMRIGHT, + ] + ), + vol.Optional(ATTR_TRAVELTIME, default=DEFAULT_TRAVELTIME): cv.small_float, + } +) + -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a Foscam IP Camera.""" - add_entities([FoscamCam(config)]) + + async def async_handle_ptz(service): + """Handle PTZ service call.""" + movement = service.data[ATTR_MOVEMENT] + travel_time = service.data[ATTR_TRAVELTIME] + entity_ids = await async_extract_entity_ids(hass, service) + + if not entity_ids: + return + + _LOGGER.debug("Moving '%s' camera(s): %s", movement, entity_ids) + + all_cameras = hass.data[FOSCAM_DATA][FOSCAM_ENTITIES] + target_cameras = [ + camera for camera in all_cameras if camera.entity_id in entity_ids + ] + + for camera in target_cameras: + await camera.async_perform_ptz(movement, travel_time) + + hass.services.async_register( + FOSCAM_DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA + ) + + camera = FoscamCamera( + config[CONF_IP], + config[CONF_PORT], + config[CONF_USERNAME], + config[CONF_PASSWORD], + verbose=False, + ) + + rtsp_port = config.get(CONF_RTSP_PORT) + if not rtsp_port: + ret, response = await hass.async_add_executor_job(camera.get_port_info) + + if ret == 0: + rtsp_port = response.get("rtspPort") or response.get("mediaPort") + + ret, response = await hass.async_add_executor_job(camera.get_motion_detect_config) + + motion_status = False + if ret != 0 and response == 1: + motion_status = True + + async_add_entities( + [ + HassFoscamCamera( + camera, + config[CONF_NAME], + config[CONF_USERNAME], + config[CONF_PASSWORD], + rtsp_port, + motion_status, + ) + ] + ) -class FoscamCam(Camera): +class HassFoscamCamera(Camera): """An implementation of a Foscam IP camera.""" - def __init__(self, device_info): + def __init__(self, camera, name, username, password, rtsp_port, motion_status): """Initialize a Foscam camera.""" - from libpyfoscam import FoscamCamera - super().__init__() - ip_address = device_info.get(CONF_IP) - port = device_info.get(CONF_PORT) - self._username = device_info.get(CONF_USERNAME) - self._password = device_info.get(CONF_PASSWORD) - self._name = device_info.get(CONF_NAME) - self._motion_status = False - - self._foscam_session = FoscamCamera( - ip_address, port, self._username, self._password, verbose=False + self._foscam_session = camera + self._name = name + self._username = username + self._password = password + self._rtsp_port = rtsp_port + self._motion_status = motion_status + + async def async_added_to_hass(self): + """Handle entity addition to hass.""" + entities = self.hass.data.setdefault(FOSCAM_DATA, {}).setdefault( + FOSCAM_ENTITIES, [] ) - - self._rtsp_port = device_info.get(CONF_RTSP_PORT) - if not self._rtsp_port: - result, response = self._foscam_session.get_port_info() - if result == 0: - self._rtsp_port = response.get("rtspPort") or response.get("mediaPort") + entities.append(self) def camera_image(self): """Return a still image response from the camera.""" # Send the request to snap a picture and return raw jpg data # Handle exception if host is not reachable or url failed result, response = self._foscam_session.snap_picture_2() - if result == FOSCAM_COMM_ERROR: + if result != 0: return None return response @@ -97,19 +207,47 @@ def enable_motion_detection(self): """Enable motion detection in camera.""" try: ret = self._foscam_session.enable_motion_detection() - self._motion_status = ret == FOSCAM_COMM_ERROR + + if ret != 0: + return + + self._motion_status = True except TypeError: _LOGGER.debug("Communication problem") - self._motion_status = False def disable_motion_detection(self): """Disable motion detection.""" try: ret = self._foscam_session.disable_motion_detection() - self._motion_status = ret == FOSCAM_COMM_ERROR + + if ret != 0: + return + + self._motion_status = False except TypeError: _LOGGER.debug("Communication problem") - self._motion_status = False + + async def async_perform_ptz(self, movement, travel_time): + """Perform a PTZ action on the camera.""" + _LOGGER.debug("PTZ action '%s' on %s", movement, self._name) + + movement_function = getattr(self._foscam_session, MOVEMENT_ATTRS[movement]) + + ret, _ = await self.hass.async_add_executor_job(movement_function) + + if ret != 0: + _LOGGER.error("Error moving %s '%s': %s", movement, self._name, ret) + return + + await asyncio.sleep(travel_time) + + ret, _ = await self.hass.async_add_executor_job( + self._foscam_session.ptz_stop_run + ) + + if ret != 0: + _LOGGER.error("Error stopping movement on '%s': %s", self._name, ret) + return @property def name(self): diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py new file mode 100644 index 00000000000000..63b4b74a763304 --- /dev/null +++ b/homeassistant/components/foscam/const.py @@ -0,0 +1,5 @@ +"""Constants for Foscam component.""" + +DOMAIN = "foscam" +DATA = "foscam" +ENTITIES = "entities" diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index b2c44c113ee9ee..6a47012ef84789 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -6,5 +6,5 @@ "libpyfoscam==1.0" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@skgsergio"] } diff --git a/homeassistant/components/foscam/services.yaml b/homeassistant/components/foscam/services.yaml new file mode 100644 index 00000000000000..64e68dd5bc42b3 --- /dev/null +++ b/homeassistant/components/foscam/services.yaml @@ -0,0 +1,12 @@ +ptz: + description: Pan/Tilt service for Foscam camera. + fields: + entity_id: + description: Name(s) of entities to move. + example: 'camera.living_room_camera' + movement: + description: "Direction of the movement. Allowed values: up, down, left, right, top_left, top_right, bottom_left, bottom_right." + example: 'up' + travel_time: + description: "(Optional) Travel time in seconds. Allowed values: float from 0 to 1. Default: 0.125" + example: 0.125 From 3adac699c7cb0c79ac022c27daf92c38e3c9212b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 7 Oct 2019 18:16:26 +0300 Subject: [PATCH 0648/3953] Note snake_case state attribute name convention in entity docs (#27287) https://github.com/home-assistant/home-assistant/pull/26675#discussion_r331763063 --- homeassistant/helpers/entity.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 5754d99d9b20f9..0d2182f88e1f0a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -148,7 +148,8 @@ def state(self) -> Union[None, str, int, float]: def state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes. - Implemented by component base class. + Implemented by component base class. Convention for attribute names + is lowercase snake_case. """ return None @@ -156,7 +157,8 @@ def state_attributes(self) -> Optional[Dict[str, Any]]: def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return device specific state attributes. - Implemented by platform classes. + Implemented by platform classes. Convention for attribute names + is lowercase snake_case. """ return None From 761d7f21e90026d4a38fb20ee125ae2594ad5a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 7 Oct 2019 18:17:39 +0300 Subject: [PATCH 0649/3953] Upgrade pylint (#27279) * Upgrade pylint to 2.4.2 and astroid to 2.3.1 https://pylint.readthedocs.io/en/latest/whatsnew/2.4.html https://pylint.readthedocs.io/en/latest/whatsnew/changelog.html#what-s-new-in-pylint-2-4-1 https://pylint.readthedocs.io/en/latest/whatsnew/changelog.html#what-s-new-in-pylint-2-4-2 * unnecessary-comprehension fixes * invalid-name fixes * self-assigning-variable fixes * Re-enable not-an-iterable * used-before-assignment fix * invalid-overridden-method fixes * undefined-variable __class__ workarounds https://github.com/PyCQA/pylint/issues/3090 * no-member false positive disabling * Remove some no longer needed disables * using-constant-test fix * Disable import-outside-toplevel for now * Disable some apparent no-value-for-parameter false positives * invalid-overridden-method false positive disables https://github.com/PyCQA/pylint/issues/3150 * Fix unintentional Entity.force_update override in AfterShipSensor --- homeassistant/components/aftership/sensor.py | 4 ++-- homeassistant/components/axis/config_flow.py | 2 +- homeassistant/components/bayesian/binary_sensor.py | 2 +- homeassistant/components/bt_smarthub/device_tracker.py | 2 +- homeassistant/components/cert_expiry/helper.py | 3 ++- homeassistant/components/ddwrt/device_tracker.py | 2 +- homeassistant/components/deconz/config_flow.py | 2 +- homeassistant/components/esphome/climate.py | 3 +++ homeassistant/components/esphome/config_flow.py | 3 ++- homeassistant/components/esphome/cover.py | 3 +++ homeassistant/components/esphome/fan.py | 3 +++ homeassistant/components/esphome/light.py | 3 +++ homeassistant/components/esphome/sensor.py | 4 ++++ homeassistant/components/esphome/switch.py | 2 ++ .../components/homekit_controller/config_flow.py | 2 +- homeassistant/components/hue/config_flow.py | 2 ++ homeassistant/components/mqtt/__init__.py | 10 ++++++++-- homeassistant/components/onkyo/media_player.py | 4 ++-- homeassistant/components/rmvtransport/sensor.py | 2 +- homeassistant/components/sabnzbd/sensor.py | 1 + homeassistant/components/synology/camera.py | 1 + homeassistant/components/tplink/device_tracker.py | 4 ++-- homeassistant/components/tradfri/config_flow.py | 2 +- homeassistant/components/upnp/sensor.py | 1 - homeassistant/components/vacuum/__init__.py | 4 +--- homeassistant/components/vallox/__init__.py | 2 +- homeassistant/components/wink/climate.py | 4 ---- homeassistant/components/zha/core/device.py | 4 +--- homeassistant/components/zha/fan.py | 2 +- homeassistant/components/zha/lock.py | 2 +- homeassistant/core.py | 4 +++- homeassistant/helpers/config_entry_flow.py | 2 +- homeassistant/util/color.py | 4 ++-- homeassistant/util/dt.py | 2 +- pylintrc | 6 +++--- requirements_test.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 37 files changed, 67 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index e54a48f7ee43ab..c41e5aec7b51d6 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -146,10 +146,10 @@ def icon(self): async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.force_update + UPDATE_TOPIC, self._force_update ) - async def force_update(self): + async def _force_update(self): """Force update of data.""" await self.async_update(no_throttle=True) await self.async_update_ha_state() diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 3b5efe96760efd..3473eba30653dd 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -171,7 +171,7 @@ async def async_step_zeroconf(self, discovery_info): if discovery_info[CONF_HOST].startswith("169.254"): return self.async_abort(reason="link_local_address") - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["macaddress"] = serialnumber if any( diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index acefc5a3b26660..ffa13a6288ca56 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -250,7 +250,7 @@ def device_class(self): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_OBSERVATIONS: [val for val in self.current_obs.values()], + ATTR_OBSERVATIONS: list(self.current_obs.values()), ATTR_PROBABILITY: round(self.probability, 2), ATTR_PROBABILITY_THRESHOLD: self._probability_threshold, } diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index 58f409c2d4b564..ece67e3b635b92 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -69,7 +69,7 @@ def _update_info(self): _LOGGER.warning("Error scanning devices") return - clients = [client for client in data.values()] + clients = list(data.values()) self.last_results = clients def get_bt_smarthub_data(self): diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py index 9c10887293adbd..cd49588ec89f31 100644 --- a/homeassistant/components/cert_expiry/helper.py +++ b/homeassistant/components/cert_expiry/helper.py @@ -11,5 +11,6 @@ def get_cert(host, port): address = (host, port) with socket.create_connection(address, timeout=TIMEOUT) as sock: with ctx.wrap_socket(sock, server_hostname=address[0]) as ssock: - cert = ssock.getpeercert() + # pylint disable: https://github.com/PyCQA/pylint/issues/3166 + cert = ssock.getpeercert() # pylint: disable=no-member return cert diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 4e661376719ec9..bd2728d03dcab4 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -165,4 +165,4 @@ def get_ddwrt_data(self, url): def _parse_ddwrt_response(data_str): """Parse the DD-WRT data format.""" - return {key: val for key, val in _DDWRT_DATA_REGEX.findall(data_str)} + return dict(_DDWRT_DATA_REGEX.findall(data_str)) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 66df687047f209..91768584e8ac95 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -187,7 +187,7 @@ async def async_step_ssdp(self, discovery_info): ): return self.async_abort(reason="already_in_progress") - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context[CONF_BRIDGEID] = bridgeid self.deconz_config = { diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 7337aec4541fe1..fa840078aa4ee7 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -126,6 +126,9 @@ def supported_features(self) -> int: features |= SUPPORT_PRESET_MODE return features + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def hvac_mode(self) -> Optional[str]: """Return current operation ie. heat, cool, idle.""" diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 9680ed46acdf5d..47c00f434635b6 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -44,11 +44,12 @@ async def async_step_user( @property def _name(self): + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 return self.context.get("name") @_name.setter def _name(self, value): - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["name"] = value self.context["title_placeholders"] = {"name": self._name} diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 31b895b4eb2f55..980fc936940620 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -70,6 +70,9 @@ def assumed_state(self) -> bool: def _state(self) -> Optional[CoverState]: return super()._state + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def is_closed(self) -> Optional[bool]: """Return if the cover is closed or not.""" diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 44059673f15404..cddb75b41bfa79 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -92,6 +92,9 @@ async def async_oscillate(self, oscillating: bool) -> None: key=self._static_info.key, oscillating=oscillating ) + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def is_on(self) -> Optional[bool]: """Return true if the entity is on.""" diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 334e7e645a7518..9a2a0ccd0bca0e 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -61,6 +61,9 @@ def _static_info(self) -> LightInfo: def _state(self) -> Optional[LightState]: return super()._state + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def is_on(self) -> Optional[bool]: """Return true if the switch is on.""" diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 3168bae7ec88d2..2b7a8b94f1eaa4 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -37,6 +37,10 @@ async def async_setup_entry( ) +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# pylint: disable=invalid-overridden-method + + class EsphomeSensor(EsphomeEntity): """A sensor implementation for esphome.""" diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index f66bfaa39f3b9e..b52d630e1b4714 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -49,6 +49,8 @@ def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._static_info.assumed_state + # https://github.com/PyCQA/pylint/issues/3150 for @esphome_state_property + # pylint: disable=invalid-overridden-method @esphome_state_property def is_on(self) -> Optional[bool]: """Return true if the switch is on.""" diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 008e0f8566dcdd..40bf87d6f0a911 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -122,7 +122,7 @@ async def async_step_zeroconf(self, discovery_info): _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["hkid"] = hkid self.context["title_placeholders"] = {"name": name} diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 9c0e94bc3bdce0..ebd71ba7c1cf9a 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -50,6 +50,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + def __init__(self): """Initialize the Hue flow.""" self.host = None diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 9b25a6ef6e4817..e3605cb866484c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -776,7 +776,9 @@ def __init__( self._mqttc.on_message = self._mqtt_on_message if will_message is not None: - self._mqttc.will_set(*attr.astuple(will_message)) + self._mqttc.will_set( # pylint: disable=no-value-for-parameter + *attr.astuple(will_message) + ) async def async_publish( self, topic: str, payload: PublishPayloadType, qos: int, retain: bool @@ -909,7 +911,11 @@ def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: self.hass.add_job(self._async_perform_subscription, topic, max_qos) if self.birth_message: - self.hass.add_job(self.async_publish(*attr.astuple(self.birth_message))) + self.hass.add_job( + self.async_publish( # pylint: disable=no-value-for-parameter + *attr.astuple(self.birth_message) + ) + ) def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: """Message received callback.""" diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index af92f6c5f0510a..92e5f01d486861 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -264,7 +264,7 @@ def update(self): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join(current_source_tuples[1]) if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] elif ATTR_PRESET in self._attributes: @@ -413,7 +413,7 @@ def update(self): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join(current_source_tuples[1]) self._muted = bool(mute_raw[1] == "on") if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index d7d075f48f7c75..f66f22dda17318 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -157,7 +157,7 @@ def state_attributes(self): """Return the state attributes.""" try: return { - "next_departures": [val for val in self.data.departures[1:]], + "next_departures": self.data.departures[1:], "direction": self.data.departures[0].get("direction"), "line": self.data.departures[0].get("line"), "minutes": self.data.departures[0].get("minutes"), diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 58624c758d9c83..21ac9eefdb29a5 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -49,6 +49,7 @@ def state(self): """Return the state of the sensor.""" return self._state + @property def should_poll(self): """Don't poll. Will be updated by dispatcher signal.""" return False diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py index 5594a4b3c9a921..8c176f488034ca 100644 --- a/homeassistant/components/synology/camera.py +++ b/homeassistant/components/synology/camera.py @@ -105,6 +105,7 @@ def is_recording(self): """Return true if the device is recording.""" return self._camera.is_recording + @property def should_poll(self): """Update the recording state periodically.""" return True diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py index e7f87074cb4c00..f6921efed9183b 100644 --- a/homeassistant/components/tplink/device_tracker.py +++ b/homeassistant/components/tplink/device_tracker.py @@ -102,7 +102,7 @@ def __init__(self, config): self.success_init = self._update_info() except requests.exceptions.RequestException: - _LOGGER.debug("RequestException in %s", __class__.__name__) + _LOGGER.debug("RequestException in %s", self.__class__.__name__) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -150,7 +150,7 @@ def __init__(self, config): try: self.success_init = self._update_info() except requests.exceptions.RequestException: - _LOGGER.debug("RequestException in %s", __class__.__name__) + _LOGGER.debug("RequestException in %s", self.__class__.__name__) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 6266766f394f60..9da381deb75658 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -83,7 +83,7 @@ async def async_step_zeroconf(self, user_input): """Handle zeroconf discovery.""" host = user_input["host"] - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["host"] = host if any(host == flow["context"]["host"] for flow in self._async_in_progress()): diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index b721fa29cdd860..40cb7ef2032f85 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -164,7 +164,6 @@ def unit(self) -> str: """Get unit we are measuring in.""" raise NotImplementedError() - @property def _async_fetch_value(self): """Fetch a value from the IGD.""" raise NotImplementedError() diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 9bc376916c6edf..55e56415b0deaf 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components import group -from homeassistant.const import ( +from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API ATTR_BATTERY_LEVEL, ATTR_COMMAND, SERVICE_TOGGLE, @@ -68,8 +68,6 @@ STATE_CLEANING = "cleaning" STATE_DOCKED = "docked" -STATE_IDLE = STATE_IDLE -STATE_PAUSED = STATE_PAUSED STATE_RETURNING = "returning" STATE_ERROR = "error" diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index c107e4f8894551..eb5edfe7fcf51b 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -252,7 +252,7 @@ async def async_set_profile_fan_speed_boost( async def async_handle(self, service): """Dispatch a service call.""" method = SERVICE_TO_METHOD.get(service.service) - params = {key: value for key, value in service.data.items()} + params = service.data.copy() if not hasattr(self, method["method"]): _LOGGER.error("Service not implemented: %s", method["method"]) diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index 38f25ef0a83912..6323fa7bbfe8aa 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -283,10 +283,6 @@ def set_temperature(self, **kwargs): target_temp_high = target_temp if self.hvac_mode == HVAC_MODE_HEAT: target_temp_low = target_temp - if target_temp_low is not None: - target_temp_low = target_temp_low - if target_temp_high is not None: - target_temp_high = target_temp_high self.wink.set_temperature(target_temp_low, target_temp_high) def set_hvac_mode(self, hvac_mode): diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e9e2c3b7ea6aaf..f4a3a2c3d48b5e 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -348,7 +348,6 @@ async def _execute_channel_tasks(self, channels, task_name, *args): zdo_task = None for channel in channels: 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( semaphore, channel, task_name, *args @@ -373,8 +372,7 @@ async def _async_create_task(self, semaphore, channel, func_name, *args): @callback def async_unsub_dispatcher(self): """Unsubscribe the dispatcher.""" - if self._unsub: - self._unsub() + self._unsub() @callback def async_update_last_seen(self, last_seen): diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 1f119ef6657e3e..43ad2291cb775d 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -43,7 +43,7 @@ SPEED_SMART, ] -VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)} +VALUE_TO_SPEED = dict(enumerate(SPEED_LIST)) SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index afc4618343cba2..a2151b4bdcb4af 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -27,7 +27,7 @@ STATE_LIST = [STATE_UNLOCKED, STATE_LOCKED, STATE_UNLOCKED] -VALUE_TO_STATE = {i: state for i, state in enumerate(STATE_LIST)} +VALUE_TO_STATE = dict(enumerate(STATE_LIST)) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/core.py b/homeassistant/core.py index feb4445d36d5bd..90d197906cbe5b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -186,7 +186,9 @@ def __init__(self, loop: Optional[asyncio.events.AbstractEventLoop] = None) -> N self.data: dict = {} self.state = CoreState.not_running self.exit_code = 0 - self.config_entries: Optional[ConfigEntries] = None + self.config_entries: Optional[ + ConfigEntries # pylint: disable=used-before-assignment + ] = None # If not None, use to signal end-of-loop self._stopped: Optional[asyncio.Event] = None diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 922878fb324127..88aae3721b1a1f 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -38,7 +38,7 @@ async def async_step_confirm(self, user_input=None): if user_input is None: return self.async_show_form(step_id="confirm") - if ( + if ( # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context and self.context.get("source") != config_entries.SOURCE_DISCOVERY ): diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 89d1dcfc4c1afe..640e5c5540a4d6 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -167,8 +167,8 @@ class XYPoint: """Represents a CIE 1931 XY coordinate pair.""" - x = attr.ib(type=float) - y = attr.ib(type=float) + x = attr.ib(type=float) # pylint: disable=invalid-name + y = attr.ib(type=float) # pylint: disable=invalid-name @attr.s() diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index a948c4407ae6e7..1abb429439876c 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -220,7 +220,7 @@ def q_n_r(first: int, second: int) -> Tuple[int, int]: def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> List[int]: """Parse the time expression part and return a list of times to match.""" if parameter is None or parameter == MATCH_ALL: - res = [x for x in range(min_value, max_value + 1)] + res = list(range(min_value, max_value + 1)) elif isinstance(parameter, str) and parameter.startswith("/"): parameter = int(parameter[1:]) res = [x for x in range(min_value, max_value + 1) if x % parameter == 0] diff --git a/pylintrc b/pylintrc index bb4f1fe96d03ca..3d69800e5c3120 100644 --- a/pylintrc +++ b/pylintrc @@ -2,7 +2,7 @@ ignore=tests [BASIC] -good-names=i,j,k,ex,Run,_,fp +good-names=id,i,j,k,ex,Run,_,fp [MESSAGES CONTROL] # Reasons disabled: @@ -18,8 +18,8 @@ good-names=i,j,k,ex,Run,_,fp # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise -# not-an-iterable - https://github.com/PyCQA/pylint/issues/2311 # unnecessary-pass - readability for functions which only contain pass +# import-outside-toplevel - TODO disable= format, abstract-class-little-used, @@ -27,9 +27,9 @@ disable= cyclic-import, duplicate-code, global-statement, + import-outside-toplevel, inconsistent-return-statements, locally-disabled, - not-an-iterable, not-context-manager, redefined-variable-type, too-few-public-methods, diff --git a/requirements_test.txt b/requirements_test.txt index 9da375b33c8a91..d0b7880d78daf3 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,8 +12,8 @@ mock-open==1.3.1 mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 -pylint==2.3.1 -astroid==2.2.5 +pylint==2.4.2 +astroid==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62be2f035a5ad5..425170d168dc16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -13,8 +13,8 @@ mock-open==1.3.1 mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 -pylint==2.3.1 -astroid==2.2.5 +pylint==2.4.2 +astroid==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 From eb10f8dcd3b95e95db474a44eac449f220289643 Mon Sep 17 00:00:00 2001 From: Chandan Rai Date: Mon, 7 Oct 2019 22:55:36 +0530 Subject: [PATCH 0650/3953] fixed minor typo in docs/source/api/helpers.rst (#27282) --- docs/source/api/helpers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/api/helpers.rst b/docs/source/api/helpers.rst index 28f4059d60da8c..8ad645b7977c31 100644 --- a/docs/source/api/helpers.rst +++ b/docs/source/api/helpers.rst @@ -56,7 +56,7 @@ homeassistant.helpers.data_entry_flow module homeassistant.helpers.deprecation module ---------------------------------------- -.. automodule:: homeassistant.helpers.depracation +.. automodule:: homeassistant.helpers.deprecation :members: :undoc-members: :show-inheritance: From feb1986459f5c123471d2ab66a7190bead94f81e Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Mon, 7 Oct 2019 10:40:52 -0700 Subject: [PATCH 0651/3953] Fix the todoist integration (#27273) * Fixed the todoist integration. * Removing unused import * Flake8 fixes. * Added username to codeowners. * Updated global codeowners --- CODEOWNERS | 1 + homeassistant/components/todoist/calendar.py | 40 +++++++++++-------- .../components/todoist/manifest.json | 4 +- .../components/todoist/services.yaml | 25 ++++++++++++ requirements_all.txt | 2 +- 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d550f3e6924d92..6e343e91533006 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -291,6 +291,7 @@ homeassistant/components/threshold/* @fabaff homeassistant/components/tibber/* @danielhiversen homeassistant/components/tile/* @bachya homeassistant/components/time_date/* @fabaff +homeassistant/components/todoist/* @boralyl homeassistant/components/toon/* @frenck homeassistant/components/tplink/* @rytilahti homeassistant/components/traccar/* @ludeeus diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 75aec037a25ed5..1179fd9086836f 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -36,6 +36,7 @@ DESCRIPTION = "description" # Calendar Platform: Used in the '_get_date()' method DATETIME = "dateTime" +DUE = "due" # Service Call: When is this task due (in natural language)? DUE_DATE_STRING = "due_date_string" # Service Call: The language of DUE_DATE_STRING @@ -206,7 +207,7 @@ def handle_new_task(call): project_id = project_id_lookup[project_name] # Create the task - item = api.items.add(call.data[CONTENT], project_id) + item = api.items.add(call.data[CONTENT], project_id=project_id) if LABELS in call.data: task_labels = call.data[LABELS] @@ -216,11 +217,12 @@ def handle_new_task(call): if PRIORITY in call.data: item.update(priority=call.data[PRIORITY]) + _due: dict = {} if DUE_DATE_STRING in call.data: - item.update(date_string=call.data[DUE_DATE_STRING]) + _due["string"] = call.data[DUE_DATE_STRING] if DUE_DATE_LANG in call.data: - item.update(date_lang=call.data[DUE_DATE_LANG]) + _due["lang"] = call.data[DUE_DATE_LANG] if DUE_DATE in call.data: due_date = dt.parse_datetime(call.data[DUE_DATE]) @@ -231,7 +233,11 @@ def handle_new_task(call): due_date = dt.as_utc(due_date) date_format = "%Y-%m-%dT%H:%M" due_date = datetime.strftime(due_date, date_format) - item.update(due_date_utc=due_date) + _due["date"] = due_date + + if _due: + item.update(due=_due) + # Commit changes api.commit() _LOGGER.debug("Created Todoist task: %s", call.data[CONTENT]) @@ -241,6 +247,17 @@ def handle_new_task(call): ) +def _parse_due_date(data: dict) -> datetime: + """Parse the due date dict into a datetime object.""" + # Add time information to date only strings. + if len(data["date"]) == 10: + data["date"] += "T00:00:00" + # If there is no timezone provided, use UTC. + if data["timezone"] is None: + data["date"] += "Z" + return dt.parse_datetime(data["date"]) + + class TodoistProjectDevice(CalendarEventDevice): """A device for getting the next Task from a Todoist Project.""" @@ -412,16 +429,8 @@ def create_todoist_task(self, data): # complete the task. # Generally speaking, that means right now. task[START] = dt.utcnow() - if data[DUE_DATE_UTC] is not None: - due_date = data[DUE_DATE_UTC] - - # Due dates are represented in RFC3339 format, in UTC. - # Home Assistant exclusively uses UTC, so it'll - # handle the conversion. - time_format = "%a %d %b %Y %H:%M:%S %z" - # HASS' built-in parse time function doesn't like - # Todoist's time format; strptime has to be used. - task[END] = datetime.strptime(due_date, time_format) + if data[DUE] is not None: + task[END] = _parse_due_date(data[DUE]) if self._latest_due_date is not None and ( task[END] > self._latest_due_date @@ -540,9 +549,8 @@ async def async_get_events(self, hass, start_date, end_date): project_task_data = project_data[TASKS] events = [] - time_format = "%a %d %b %Y %H:%M:%S %z" for task in project_task_data: - due_date = datetime.strptime(task["due_date_utc"], time_format) + due_date = _parse_due_date(task["due"]) if start_date < due_date < end_date: event = { "uid": task["id"], diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index dbf1a941e00ba3..e7876c953cc5c9 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -3,8 +3,8 @@ "name": "Todoist", "documentation": "https://www.home-assistant.io/integrations/todoist", "requirements": [ - "todoist-python==7.0.17" + "todoist-python==8.0.0" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@boralyl"] } diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index e69de29bb2d1d6..c2d23cc4bec5a6 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -0,0 +1,25 @@ +new_task: + description: Create a new task and add it to a project. + fields: + content: + description: The name of the task. + example: Pick up the mail. + project: + description: The name of the project this task should belong to. Defaults to Inbox. + example: Errands + labels: + description: Any labels that you want to apply to this task, separated by a comma. + example: Chores,Delivieries + priority: + description: The priority of this task, from 1 (normal) to 4 (urgent). + example: 2 + due_date_string: + description: The day this task is due, in natural language. + example: Tomorrow + due_date_lang: + description: The language of due_date_string. + example: en + due_date: + description: The day this task is due, in format YYYY-MM-DD. + example: 2019-10-22 + diff --git a/requirements_all.txt b/requirements_all.txt index 075884183ff023..17f39ac1d8df95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1891,7 +1891,7 @@ thingspeak==0.4.1 tikteck==0.4 # homeassistant.components.todoist -todoist-python==7.0.17 +todoist-python==8.0.0 # homeassistant.components.toon toonapilib==3.2.4 From 7cdb76eedb58fd55b56e07d90d8e57fa0d1ba005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgardo=20Ram=C3=ADrez?= Date: Mon, 7 Oct 2019 12:41:26 -0500 Subject: [PATCH 0652/3953] FIX: Typo (#27267) --- homeassistant/components/fan/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 16d3742d9ab991..0e3978690e69d1 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -42,7 +42,7 @@ toggle: fields: entity_id: description: Name(s) of the entities to toggle - exampl: 'fan.living_room' + example: 'fan.living_room' set_direction: description: Set the fan rotation. From fe155faf6a12c462947155d86dba4157e74421d4 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Mon, 7 Oct 2019 19:43:47 +0200 Subject: [PATCH 0653/3953] Refactor tradfri light (#27259) * Refactor light file * Update following review --- homeassistant/components/tradfri/light.py | 149 ++++++---------------- 1 file changed, 39 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 615899a98c8d22..f5d61f0aaed4d8 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -16,8 +16,9 @@ SUPPORT_TRANSITION, Light, ) +from homeassistant.components.tradfri.base_class import TradfriBaseDevice from homeassistant.core import callback -from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY +from . import KEY_API, KEY_GATEWAY from .const import CONF_GATEWAY_ID, CONF_IMPORT_GROUPS _LOGGER = logging.getLogger(__name__) @@ -143,99 +144,55 @@ async def async_update(self): await self._api(self._group.update()) -class TradfriLight(Light): +class TradfriLight(TradfriBaseDevice, Light): """The platform class required by Home Assistant.""" - def __init__(self, light, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a Light.""" - self._api = api - self._unique_id = f"light-{gateway_id}-{light.id}" - self._light = None - self._light_control = None - self._light_data = None - self._name = None + super().__init__(device, api, gateway_id) + self._unique_id = f"light-{gateway_id}-{device.id}" self._hs_color = None self._features = SUPPORTED_FEATURES - self._available = True - self._gateway_id = gateway_id - - self._refresh(light) - @property - def unique_id(self): - """Return unique ID for light.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - info = self._light.device_info - - return { - "identifiers": {(TRADFRI_DOMAIN, self._light.id)}, - "name": self._name, - "manufacturer": info.manufacturer, - "model": info.model_number, - "sw_version": info.firmware_version, - "via_device": (TRADFRI_DOMAIN, self._gateway_id), - } + self._refresh(device) @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" - return self._light_control.min_mireds + return self._device_control.min_mireds @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" - return self._light_control.max_mireds - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def should_poll(self): - """No polling needed for tradfri light.""" - return False + return self._device_control.max_mireds @property def supported_features(self): """Flag supported features.""" return self._features - @property - def name(self): - """Return the display name of this light.""" - return self._name - @property def is_on(self): """Return true if light is on.""" - return self._light_data.state + return self._device_data.state @property def brightness(self): """Return the brightness of the light.""" - return self._light_data.dimmer + return self._device_data.dimmer @property def color_temp(self): """Return the color temp value in mireds.""" - return self._light_data.color_temp + return self._device_data.color_temp @property def hs_color(self): """HS color of the light.""" - if self._light_control.can_set_color: - hsbxy = self._light_data.hsb_xy_color - hue = hsbxy[0] / (self._light_control.max_hue / 360) - sat = hsbxy[1] / (self._light_control.max_saturation / 100) + if self._device_control.can_set_color: + hsbxy = self._device_data.hsb_xy_color + hue = hsbxy[0] / (self._device_control.max_hue / 360) + sat = hsbxy[1] / (self._device_control.max_saturation / 100) if hue is not None and sat is not None: return hue, sat @@ -248,9 +205,9 @@ async def async_turn_off(self, **kwargs): transition_time = int(kwargs[ATTR_TRANSITION]) * 10 dimmer_data = {ATTR_DIMMER: 0, ATTR_TRANSITION_TIME: transition_time} - await self._api(self._light_control.set_dimmer(**dimmer_data)) + await self._api(self._device_control.set_dimmer(**dimmer_data)) else: - await self._api(self._light_control.set_state(False)) + await self._api(self._device_control.set_state(False)) async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" @@ -267,32 +224,32 @@ async def async_turn_on(self, **kwargs): ATTR_DIMMER: brightness, ATTR_TRANSITION_TIME: transition_time, } - dimmer_command = self._light_control.set_dimmer(**dimmer_data) + dimmer_command = self._device_control.set_dimmer(**dimmer_data) transition_time = None else: - dimmer_command = self._light_control.set_state(True) + dimmer_command = self._device_control.set_state(True) color_command = None - if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color: - hue = int(kwargs[ATTR_HS_COLOR][0] * (self._light_control.max_hue / 360)) + if ATTR_HS_COLOR in kwargs and self._device_control.can_set_color: + hue = int(kwargs[ATTR_HS_COLOR][0] * (self._device_control.max_hue / 360)) sat = int( - kwargs[ATTR_HS_COLOR][1] * (self._light_control.max_saturation / 100) + kwargs[ATTR_HS_COLOR][1] * (self._device_control.max_saturation / 100) ) color_data = { ATTR_HUE: hue, ATTR_SAT: sat, ATTR_TRANSITION_TIME: transition_time, } - color_command = self._light_control.set_hsb(**color_data) + color_command = self._device_control.set_hsb(**color_data) transition_time = None temp_command = None if ATTR_COLOR_TEMP in kwargs and ( - self._light_control.can_set_temp or self._light_control.can_set_color + self._device_control.can_set_temp or self._device_control.can_set_color ): temp = kwargs[ATTR_COLOR_TEMP] # White Spectrum bulb - if self._light_control.can_set_temp: + if self._device_control.can_set_temp: if temp > self.max_mireds: temp = self.max_mireds elif temp < self.min_mireds: @@ -301,21 +258,21 @@ async def async_turn_on(self, **kwargs): ATTR_COLOR_TEMP: temp, ATTR_TRANSITION_TIME: transition_time, } - temp_command = self._light_control.set_color_temp(**temp_data) + temp_command = self._device_control.set_color_temp(**temp_data) transition_time = None # Color bulb (CWS) # color_temp needs to be set with hue/saturation - elif self._light_control.can_set_color: + elif self._device_control.can_set_color: temp_k = color_util.color_temperature_mired_to_kelvin(temp) hs_color = color_util.color_temperature_to_hs(temp_k) - hue = int(hs_color[0] * (self._light_control.max_hue / 360)) - sat = int(hs_color[1] * (self._light_control.max_saturation / 100)) + hue = int(hs_color[0] * (self._device_control.max_hue / 360)) + sat = int(hs_color[1] * (self._device_control.max_saturation / 100)) color_data = { ATTR_HUE: hue, ATTR_SAT: sat, ATTR_TRANSITION_TIME: transition_time, } - color_command = self._light_control.set_hsb(**color_data) + color_command = self._device_control.set_hsb(**color_data) transition_time = None # HSB can always be set, but color temp + brightness is bulb dependant @@ -325,7 +282,7 @@ async def async_turn_on(self, **kwargs): else: command = color_command - if self._light_control.can_combine_commands: + if self._device_control.can_combine_commands: await self._api(command + temp_command) else: if temp_command is not None: @@ -333,46 +290,18 @@ async def async_turn_on(self, **kwargs): if command is not None: await self._api(command) - @callback - def _async_start_observe(self, exc=None): - """Start observation of light.""" - - if exc: - self._available = False - self.async_schedule_update_ha_state() - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._light.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, light): + def _refresh(self, device): """Refresh the light data.""" - self._light = light + super()._refresh(device) # Caching of LightControl and light object - self._available = light.reachable - self._light_control = light.light_control - self._light_data = light.light_control.lights[0] - self._name = light.name + self._device_control = device.light_control + self._device_data = device.light_control.lights[0] self._features = SUPPORTED_FEATURES - if light.light_control.can_set_dimmer: + if device.light_control.can_set_dimmer: self._features |= SUPPORT_BRIGHTNESS - if light.light_control.can_set_color: + if device.light_control.can_set_color: self._features |= SUPPORT_COLOR - if light.light_control.can_set_temp: + if device.light_control.can_set_temp: self._features |= SUPPORT_COLOR_TEMP - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this light.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() From 35bca702b4835bf6029897d210ab07949accb728 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Mon, 7 Oct 2019 11:09:08 -0700 Subject: [PATCH 0654/3953] Neato battery sensor (#27286) * initial commit * Pring log only once if available * Update coverage * Review comments * Move variables --- .coveragerc | 3 +- homeassistant/components/neato/__init__.py | 3 +- homeassistant/components/neato/sensor.py | 104 +++++++++++++++++++++ 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/neato/sensor.py diff --git a/.coveragerc b/.coveragerc index 8253b5522f76bf..3de008439de501 100644 --- a/.coveragerc +++ b/.coveragerc @@ -422,8 +422,9 @@ omit = homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/camera.py - homeassistant/components/neato/vacuum.py + homeassistant/components/neato/sensor.py homeassistant/components/neato/switch.py + homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py homeassistant/components/nest/* diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index feaffeaeb6d63c..c1fb128a1d1f3c 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass, entry): _LOGGER.debug("Failed to connect to Neato API") return False - for component in ("camera", "vacuum", "switch"): + for component in ("camera", "vacuum", "switch", "sensor"): hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) @@ -125,6 +125,7 @@ async def async_unload_entry(hass, entry): hass.config_entries.async_forward_entry_unload(entry, "camera"), hass.config_entries.async_forward_entry_unload(entry, "vacuum"), hass.config_entries.async_forward_entry_unload(entry, "switch"), + hass.config_entries.async_forward_entry_unload(entry, "sensor"), ) return True diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py new file mode 100644 index 00000000000000..0201012cc37f96 --- /dev/null +++ b/homeassistant/components/neato/sensor.py @@ -0,0 +1,104 @@ +"""Support for Neato sensors.""" +import logging + +from datetime import timedelta +from pybotvac.exceptions import NeatoRobotException + +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY +from homeassistant.helpers.entity import Entity + +from .const import NEATO_ROBOTS, NEATO_LOGIN, NEATO_DOMAIN, SCAN_INTERVAL_MINUTES + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) + +BATTERY = "Battery" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Neato sensor.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Neato sensor using config entry.""" + dev = [] + neato = hass.data.get(NEATO_LOGIN) + for robot in hass.data[NEATO_ROBOTS]: + dev.append(NeatoSensor(neato, robot)) + + if not dev: + return + + _LOGGER.debug("Adding robots for sensors %s", dev) + async_add_entities(dev, True) + + +class NeatoSensor(Entity): + """Neato sensor.""" + + def __init__(self, neato, robot): + """Initialize Neato sensor.""" + self.robot = robot + self.neato = neato + self._available = self.neato.logged_in if self.neato is not None else False + self._robot_name = f"{self.robot.name} {BATTERY}" + self._robot_serial = self.robot.serial + self._state = None + + def update(self): + """Update Neato Sensor.""" + if self.neato is None: + _LOGGER.error("Error while updating sensor") + self._state = None + self._available = False + return + + try: + self.neato.update_robots() + self._state = self.robot.state + except NeatoRobotException as ex: + if self._available: + _LOGGER.error("Neato sensor connection error: %s", ex) + self._state = None + self._available = False + return + + self._available = True + _LOGGER.debug("self._state=%s", self._state) + + @property + def name(self): + """Return the name of this sensor.""" + return self._robot_name + + @property + def unique_id(self): + """Return unique ID.""" + return self._robot_serial + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_BATTERY + + @property + def available(self): + """Return availability.""" + return self._available + + @property + def state(self): + """Return the state.""" + return self._state["details"]["charge"] + + @property + def unit_of_measurement(self): + """Return unit of measurement.""" + return "%" + + @property + def device_info(self): + """Device info for neato robot.""" + return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} From a3c98440e0e208a162f90ad67a5f843e5011f153 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Oct 2019 13:29:12 -0500 Subject: [PATCH 0655/3953] Remove manual config flow step (#27291) --- homeassistant/components/plex/config_flow.py | 59 +----- homeassistant/components/plex/strings.json | 18 +- tests/components/plex/test_config_flow.py | 188 ++++++------------- 3 files changed, 66 insertions(+), 199 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index dd5401950e9a3c..38727ccff067f4 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -12,14 +12,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_URL, - CONF_TOKEN, - CONF_SSL, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL from homeassistant.core import callback from homeassistant.util.json import load_json @@ -30,8 +23,6 @@ CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, - DEFAULT_PORT, - DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, PLEX_CONFIG_FILE, @@ -44,8 +35,6 @@ from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer -USER_SCHEMA = vol.Schema({vol.Optional("manual_setup"): bool}) - _LOGGER = logging.getLogger(__package__) @@ -73,23 +62,17 @@ def async_get_options_flow(config_entry): def __init__(self): """Initialize the Plex flow.""" self.current_login = {} - self.discovery_info = {} self.available_servers = None self.plexauth = None self.token = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - errors = {} - if user_input is not None: - if user_input.pop("manual_setup", False): - return await self.async_step_manual_setup(user_input) + return self.async_show_form(step_id="start_website_auth") - return await self.async_step_plex_website_auth() - - return self.async_show_form( - step_id="user", data_schema=USER_SCHEMA, errors=errors - ) + async def async_step_start_website_auth(self, user_input=None): + """Show a form before starting external authentication.""" + return await self.async_step_plex_website_auth() async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" @@ -120,9 +103,7 @@ async def async_step_server_validate(self, server_config): return self.async_abort(reason="unknown") if errors: - return self.async_show_form( - step_id="user", data_schema=USER_SCHEMA, errors=errors - ) + return self.async_show_form(step_id="start_website_auth", errors=errors) server_id = plex_server.machine_identifier @@ -152,30 +133,6 @@ async def async_step_server_validate(self, server_config): }, ) - async def async_step_manual_setup(self, user_input=None): - """Begin manual configuration.""" - if len(user_input) > 1: - host = user_input.pop(CONF_HOST) - port = user_input.pop(CONF_PORT) - prefix = "https" if user_input.pop(CONF_SSL) else "http" - user_input[CONF_URL] = f"{prefix}://{host}:{port}" - return await self.async_step_server_validate(user_input) - - data_schema = vol.Schema( - { - vol.Required( - CONF_HOST, default=self.discovery_info.get(CONF_HOST) - ): str, - vol.Required( - CONF_PORT, default=self.discovery_info.get(CONF_PORT, DEFAULT_PORT) - ): int, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, - vol.Optional(CONF_TOKEN, default=user_input.get(CONF_TOKEN, "")): str, - } - ) - return self.async_show_form(step_id="manual_setup", data_schema=data_schema) - async def async_step_select_server(self, user_input=None): """Use selected Plex server.""" config = dict(self.current_login) @@ -210,8 +167,6 @@ async def async_step_discovery(self, discovery_info): # Skip discovery if a config already exists or is in progress. return self.async_abort(reason="already_configured") - discovery_info[CONF_PORT] = int(discovery_info[CONF_PORT]) - self.discovery_info = discovery_info json_file = self.hass.config.path(PLEX_CONFIG_FILE) file_config = await self.hass.async_add_executor_job(load_json, json_file) @@ -227,7 +182,7 @@ async def async_step_discovery(self, discovery_info): _LOGGER.info("Imported legacy config, file can be removed: %s", json_file) return await self.async_step_server_validate(server_config) - return await self.async_step_user() + return self.async_abort(reason="discovery_no_file") async def async_step_import(self, import_config): """Import from Plex configuration.""" diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 6538d8e887e18c..aff79acc2ed4cf 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -2,16 +2,6 @@ "config": { "title": "Plex", "step": { - "manual_setup": { - "title": "Plex server", - "data": { - "host": "Host", - "port": "Port", - "ssl": "Use SSL", - "verify_ssl": "Verify SSL certificate", - "token": "Token (if required)" - } - }, "select_server": { "title": "Select Plex server", "description": "Multiple servers available, select one:", @@ -19,12 +9,9 @@ "server": "Server" } }, - "user": { + "start_website_auth": { "title": "Connect Plex server", - "description": "Continue to authorize at plex.tv or manually configure a server.", - "data": { - "manual_setup": "Manual setup" - } + "description": "Continue to authorize at plex.tv." } }, "error": { @@ -36,6 +23,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", + "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 753d565a82b981..e9f48f6a4f80b4 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -6,14 +6,7 @@ import requests.exceptions from homeassistant.components.plex import config_flow -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_VERIFY_SSL, - CONF_TOKEN, - CONF_URL, -) +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -48,34 +41,32 @@ def init_config_flow(hass): async def test_bad_credentials(hass): """Test when provided credentials are rejected.""" + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" - - with patch( + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value="BAD TOKEN" ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: False, - CONF_VERIFY_SSL: False, - CONF_TOKEN: "BAD TOKEN", - }, + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "faulty_credentials" @@ -123,8 +114,8 @@ async def test_discovery(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["type"] == "abort" + assert result["reason"] == "discovery_no_file" async def test_discovery_while_in_progress(hass): @@ -201,7 +192,7 @@ async def test_import_bad_hostname(hass): }, ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "not_found" @@ -212,26 +203,25 @@ async def test_unknown_exception(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.server.PlexServer", side_effect=Exception): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: True, - CONF_VERIFY_SSL: True, - CONF_TOKEN: MOCK_TOKEN, - }, - ) + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( + "plexapi.server.PlexServer", side_effect=Exception + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value="MOCK_TOKEN" + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "abort" assert result["reason"] == "unknown" @@ -245,7 +235,7 @@ async def test_no_servers_found(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mm_plex_account = MagicMock() mm_plex_account.resources = Mock(return_value=[]) @@ -256,9 +246,7 @@ async def test_no_servers_found(hass): "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -266,7 +254,7 @@ async def test_no_servers_found(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "no_servers" @@ -279,7 +267,7 @@ async def test_single_available_server(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() @@ -304,9 +292,7 @@ async def test_single_available_server(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -336,7 +322,7 @@ async def test_multiple_servers_with_selection(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -360,9 +346,7 @@ async def test_multiple_servers_with_selection(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -406,7 +390,7 @@ async def test_adding_last_unconfigured_server(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -430,9 +414,7 @@ async def test_adding_last_unconfigured_server(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -512,7 +494,7 @@ async def test_all_available_servers_configured(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -525,9 +507,7 @@ async def test_all_available_servers_configured(hass): "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -538,58 +518,6 @@ async def test_all_available_servers_configured(hass): assert result["reason"] == "all_configured" -async def test_manual_config(hass): - """Test creating via manual configuration.""" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" - - mock_connections = MockConnections(ssl=True) - - with patch("plexapi.server.PlexServer") as mock_plex_server: - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: True, - CONF_VERIFY_SSL: True, - CONF_TOKEN: MOCK_TOKEN, - }, - ) - assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name - assert ( - result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier - ) - assert ( - result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri - ) - assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN - - async def test_option_flow(hass): """Test config flow selection of one of two bridges.""" @@ -627,15 +555,13 @@ async def test_external_timed_out(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=None ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -655,14 +581,12 @@ async def test_callback_view(hass, aiohttp_client): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" client = await aiohttp_client(hass.http.app) From 1febb32dd953ed9cda612aae235744b643756e7e Mon Sep 17 00:00:00 2001 From: Santobert Date: Mon, 7 Oct 2019 21:49:54 +0200 Subject: [PATCH 0656/3953] Neato clean up (#27294) * Replace hass with neato * Clean up try-except blocks * Add some new try-except blocks * Clean up vacuum * Minor fix * Another fix --- homeassistant/components/neato/camera.py | 39 ++++++++++++------- homeassistant/components/neato/switch.py | 9 +++-- homeassistant/components/neato/vacuum.py | 48 +++++++++++++++--------- 3 files changed, 61 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index d1f86ea6637b57..98b48dd72255ec 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -28,9 +28,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up Neato camera with config entry.""" dev = [] + neato = hass.data.get(NEATO_LOGIN) + mapdata = hass.data.get(NEATO_MAP_DATA) for robot in hass.data[NEATO_ROBOTS]: if "maps" in robot.traits: - dev.append(NeatoCleaningMap(hass, robot)) + dev.append(NeatoCleaningMap(neato, robot, mapdata)) if not dev: return @@ -42,11 +44,12 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoCleaningMap(Camera): """Neato cleaning map for last clean.""" - def __init__(self, hass, robot): + def __init__(self, neato, robot, mapdata): """Initialize Neato cleaning map.""" super().__init__() self.robot = robot - self.neato = hass.data.get(NEATO_LOGIN) + self.neato = neato + self._mapdata = mapdata self._available = self.neato.logged_in if self.neato is not None else False self._robot_name = f"{self.robot.name} Cleaning Map" self._robot_serial = self.robot.serial @@ -71,25 +74,35 @@ def update(self): _LOGGER.debug("Running camera update") try: self.neato.update_robots() + except NeatoRobotException as ex: + if self._available: # Print only once when available + _LOGGER.error("Neato camera connection error: %s", ex) + self._image = None + self._image_url = None + self._available = False + return - image_url = None - map_data = self.hass.data[NEATO_MAP_DATA][self._robot_serial]["maps"][0] - image_url = map_data["url"] - if image_url == self._image_url: - _LOGGER.debug("The map image_url is the same as old") - return + image_url = None + map_data = self._mapdata[self._robot_serial]["maps"][0] + image_url = map_data["url"] + if image_url == self._image_url: + _LOGGER.debug("The map image_url is the same as old") + return + try: image = self.neato.download_map(image_url) - self._image = image.read() - self._image_url = image_url - self._generated_at = (map_data["generated_at"].strip("Z")).replace("T", " ") - self._available = True except NeatoRobotException as ex: if self._available: # Print only once when available _LOGGER.error("Neato camera connection error: %s", ex) self._image = None self._image_url = None self._available = False + return + + self._image = image.read() + self._image_url = image_url + self._generated_at = (map_data["generated_at"].strip("Z")).replace("T", " ") + self._available = True @property def name(self): diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 94d92c857fe2cf..8536af63945ecd 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -26,9 +26,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up Neato switch with config entry.""" dev = [] + neato = hass.data.get(NEATO_LOGIN) for robot in hass.data[NEATO_ROBOTS]: for type_name in SWITCH_TYPES: - dev.append(NeatoConnectedSwitch(hass, robot, type_name)) + dev.append(NeatoConnectedSwitch(neato, robot, type_name)) if not dev: return @@ -40,11 +41,11 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoConnectedSwitch(ToggleEntity): """Neato Connected Switches.""" - def __init__(self, hass, robot, switch_type): + def __init__(self, neato, robot, switch_type): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self.neato = hass.data.get(NEATO_LOGIN) + self.neato = neato self._available = self.neato.logged_in if self.neato is not None else False self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None @@ -64,7 +65,6 @@ def update(self): try: self.neato.update_robots() self._state = self.robot.state - self._available = True except NeatoRobotException as ex: if self._available: # Print only once when available _LOGGER.error("Neato switch connection error: %s", ex) @@ -72,6 +72,7 @@ def update(self): self._available = False return + self._available = True _LOGGER.debug("self._state=%s", self._state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug("State: %s", self._state) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index bf30b31eee765c..5d8fd42a5f7178 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -95,8 +95,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up Neato vacuum with config entry.""" dev = [] + neato = hass.data.get(NEATO_LOGIN) + mapdata = hass.data.get(NEATO_MAP_DATA) + persistent_maps = hass.data.get(NEATO_PERSISTENT_MAPS) for robot in hass.data[NEATO_ROBOTS]: - dev.append(NeatoConnectedVacuum(hass, robot)) + dev.append(NeatoConnectedVacuum(neato, robot, mapdata, persistent_maps)) if not dev: return @@ -112,7 +115,10 @@ def neato_custom_cleaning_service(call): navigation = call.data.get(ATTR_NAVIGATION) category = call.data.get(ATTR_CATEGORY) zone = call.data.get(ATTR_ZONE) - robot.neato_custom_cleaning(mode, navigation, category, zone) + try: + robot.neato_custom_cleaning(mode, navigation, category, zone) + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def service_to_entities(call): """Return the known devices that a service call mentions.""" @@ -131,16 +137,19 @@ def service_to_entities(call): class NeatoConnectedVacuum(StateVacuumDevice): """Representation of a Neato Connected Vacuum.""" - def __init__(self, hass, robot): + def __init__(self, neato, robot, mapdata, persistent_maps): """Initialize the Neato Connected Vacuum.""" self.robot = robot - self.neato = hass.data.get(NEATO_LOGIN) + self.neato = neato self._available = self.neato.logged_in if self.neato is not None else False + self._mapdata = mapdata self._name = f"{self.robot.name}" + self._robot_has_map = self.robot.has_persistent_maps + self._robot_maps = persistent_maps + self._robot_serial = self.robot.serial self._status_state = None self._clean_state = None self._state = None - self._mapdata = hass.data[NEATO_MAP_DATA] self._clean_time_start = None self._clean_time_stop = None self._clean_area = None @@ -152,10 +161,7 @@ def __init__(self, hass, robot): self._clean_error_time = None self._launched_from = None self._battery_level = None - self._robot_serial = self.robot.serial - self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS] self._robot_boundaries = {} - self._robot_has_map = self.robot.has_persistent_maps self._robot_stats = None def update(self): @@ -166,13 +172,12 @@ def update(self): self._available = False return + _LOGGER.debug("Running Neato Vacuums update") try: - _LOGGER.debug("Running Neato Vacuums update") if self._robot_stats is None: self._robot_stats = self.robot.get_robot_info().json() self.neato.update_robots() self._state = self.robot.state - self._available = True except NeatoRobotException as ex: if self._available: # print only once when available _LOGGER.error("Neato vacuum connection error: %s", ex) @@ -180,6 +185,7 @@ def update(self): self._available = False return + self._available = True _LOGGER.debug("self._state=%s", self._state) if "alert" in self._state: robot_alert = ALERTS.get(self._state["alert"]) @@ -235,14 +241,20 @@ def update(self): self._clean_battery_end = mapdata["run_charge_at_end"] self._launched_from = mapdata["launched_from"] - if self._robot_has_map: - if self._state["availableServices"]["maps"] != "basic-1": - if self._robot_maps[self._robot_serial]: - allmaps = self._robot_maps[self._robot_serial] - for maps in allmaps: - self._robot_boundaries = self.robot.get_map_boundaries( - maps["id"] - ).json() + if ( + self._robot_has_map + and self._state["availableServices"]["maps"] != "basic-1" + and self._robot_maps[self._robot_serial] + ): + allmaps = self._robot_maps[self._robot_serial] + for maps in allmaps: + try: + self._robot_boundaries = self.robot.get_map_boundaries( + maps["id"] + ).json() + except NeatoRobotException as ex: + _LOGGER.error("Could not fetch map boundaries: %s", ex) + self._robot_boundaries = {} @property def name(self): From 6565c17828daf683cd7833e76a638f90bc121bf0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 7 Oct 2019 21:55:35 +0200 Subject: [PATCH 0657/3953] UniFi - Improve controller tests (#27261) * Improve controller tests and harmonize setup_unifi_integration to one * Store listeners to dispatchers to be used during reset --- homeassistant/components/unifi/__init__.py | 5 +- homeassistant/components/unifi/controller.py | 25 +- .../components/unifi/device_tracker.py | 10 +- homeassistant/components/unifi/sensor.py | 10 +- homeassistant/components/unifi/switch.py | 4 +- tests/components/unifi/test_controller.py | 480 +++++++++++------- tests/components/unifi/test_device_tracker.py | 122 +---- tests/components/unifi/test_init.py | 9 +- tests/components/unifi/test_sensor.py | 101 +--- tests/components/unifi/test_switch.py | 222 ++------ 10 files changed, 403 insertions(+), 585 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 5b43289e403b13..4f3edf9ce79eac 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -77,12 +77,13 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = {} controller = UniFiController(hass, config_entry) - controller_id = get_controller_id_from_config_entry(config_entry) - hass.data[DOMAIN][controller_id] = controller if not await controller.async_setup(): return False + controller_id = get_controller_id_from_config_entry(config_entry) + hass.data[DOMAIN][controller_id] = controller + if controller.mac is None: return True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index fa1164166bdb28..3deb2e9040aa15 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -57,6 +57,7 @@ def __init__(self, hass, config_entry): self.progress = None self.wireless_clients = None + self.listeners = [] self._site_name = None self._site_role = None @@ -258,13 +259,14 @@ async def async_options_updated(hass, entry): def import_configuration(self): """Import configuration to config entry options.""" - unifi_config = {} + import_config = {} + for config in self.hass.data[UNIFI_CONFIG]: if ( self.host == config[CONF_HOST] and self.site_name == config[CONF_SITE_ID] ): - unifi_config = config + import_config = config break old_options = dict(self.config_entry.options) @@ -278,16 +280,17 @@ def import_configuration(self): (CONF_DETECTION_TIME, CONF_DETECTION_TIME), (CONF_SSID_FILTER, CONF_SSID_FILTER), ): - if config in unifi_config: - if config == option and unifi_config[ + if config in import_config: + print(config) + if config == option and import_config[ config ] != self.config_entry.options.get(option): - new_options[option] = unifi_config[config] + new_options[option] = import_config[config] elif config != option and ( option not in self.config_entry.options - or unifi_config[config] == self.config_entry.options.get(option) + or import_config[config] == self.config_entry.options.get(option) ): - new_options[option] = not unifi_config[config] + new_options[option] = not import_config[config] if new_options: options = {**old_options, **new_options} @@ -301,15 +304,15 @@ async def async_reset(self): Will cancel any scheduled setup retry and will unload the config entry. """ - # If the authentication was wrong. - if self.api is None: - return True - for platform in SUPPORTED_PLATFORMS: await self.hass.config_entries.async_forward_entry_unload( self.config_entry, platform ) + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + self.listeners = [] + return True diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 48b19d7bada4ca..b92211c4eae5a2 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -67,7 +67,9 @@ def update_controller(): """Update the values of the controller.""" update_items(controller, async_add_entities, tracked) - async_dispatcher_connect(hass, controller.signal_update, update_controller) + controller.listeners.append( + async_dispatcher_connect(hass, controller.signal_update, update_controller) + ) @callback def update_disable_on_entities(): @@ -82,8 +84,10 @@ def update_disable_on_entities(): entity.registry_entry.entity_id, disabled_by=disabled_by ) - async_dispatcher_connect( - hass, controller.signal_options_update, update_disable_on_entities + controller.listeners.append( + async_dispatcher_connect( + hass, controller.signal_options_update, update_disable_on_entities + ) ) update_controller() diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index aad013970d103f..e4f9b0df6c9dab 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -31,7 +31,9 @@ def update_controller(): """Update the values of the controller.""" update_items(controller, async_add_entities, sensors) - async_dispatcher_connect(hass, controller.signal_update, update_controller) + controller.listeners.append( + async_dispatcher_connect(hass, controller.signal_update, update_controller) + ) @callback def update_disable_on_entities(): @@ -46,8 +48,10 @@ def update_disable_on_entities(): entity.registry_entry.entity_id, disabled_by=disabled_by ) - async_dispatcher_connect( - hass, controller.signal_options_update, update_disable_on_entities + controller.listeners.append( + async_dispatcher_connect( + hass, controller.signal_options_update, update_disable_on_entities + ) ) update_controller() diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index f8fad6dac8ecc6..82aa6f0384de00 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -53,7 +53,9 @@ def update_controller(): """Update the values of the controller.""" update_items(controller, async_add_entities, switches, switches_off) - async_dispatcher_connect(hass, controller.signal_update, update_controller) + controller.listeners.append( + async_dispatcher_connect(hass, controller.signal_update, update_controller) + ) update_controller() switches_off.clear() diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index ae6f3776b4f08f..2b64e56cd996e9 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -1,9 +1,14 @@ """Test UniFi Controller.""" -from unittest.mock import Mock, patch +from collections import deque +from datetime import timedelta + +from asynctest import Mock, patch import pytest +from homeassistant import config_entries from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.components import unifi from homeassistant.components.unifi.const import ( CONF_CONTROLLER, CONF_SITE_ID, @@ -17,269 +22,362 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.components.unifi import controller, errors - -from tests.common import mock_coro - -CONTROLLER_SITES = {"site1": {"desc": "nice name", "name": "site", "role": "admin"}} +import aiounifi + +CONTROLLER_HOST = { + "hostname": "controller_host", + "ip": "1.2.3.4", + "is_wired": True, + "last_seen": 1562600145, + "mac": "10:00:00:00:00:01", + "name": "Controller host", + "oui": "Producer", + "sw_mac": "00:00:00:00:01:01", + "sw_port": 1, + "wired-rx_bytes": 1234000000, + "wired-tx_bytes": 5678000000, +} CONTROLLER_DATA = { CONF_HOST: "1.2.3.4", CONF_USERNAME: "username", CONF_PASSWORD: "password", CONF_PORT: 1234, - CONF_SITE_ID: "site", - CONF_VERIFY_SSL: True, + CONF_SITE_ID: "site_id", + CONF_VERIFY_SSL: False, } ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} - -async def test_controller_setup(): - """Successful setup.""" - hass = Mock() - hass.data = { - UNIFI_CONFIG: [ - { - CONF_HOST: CONTROLLER_DATA[CONF_HOST], - CONF_SITE_ID: "nice name", - controller.CONF_BLOCK_CLIENT: ["mac"], - controller.CONF_DONT_TRACK_CLIENTS: True, - controller.CONF_DONT_TRACK_DEVICES: True, - controller.CONF_DONT_TRACK_WIRED_CLIENTS: True, - controller.CONF_DETECTION_TIME: 30, - controller.CONF_SSID_FILTER: ["ssid"], - } - ], - UNIFI_WIRELESS_CLIENTS: Mock(), - } - 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) - api.clients = [] - - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True - - assert unifi_controller.api is api - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == len( - controller.SUPPORTED_PLATFORMS - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == ( - entry, - "device_tracker", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == ( - entry, - "sensor", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == ( - entry, - "switch", +SITES = {"Site name": {"desc": "Site name", "name": "site_id", "role": "admin"}} + + +async def setup_unifi_integration( + hass, + config, + options, + sites, + clients_response, + devices_response, + clients_all_response, +): + """Create the UniFi controller.""" + if UNIFI_CONFIG not in hass.data: + hass.data[UNIFI_CONFIG] = [] + hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) + config_entry = config_entries.ConfigEntry( + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, + options=options, + entry_id=1, ) + mock_client_responses = deque() + mock_client_responses.append(clients_response) -async def test_controller_host(): - """Config entry host and controller host are the same.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG + mock_device_responses = deque() + mock_device_responses.append(devices_response) - unifi_controller = controller.UniFiController(hass, entry) + mock_client_all_responses = deque() + mock_client_all_responses.append(clients_all_response) - assert unifi_controller.host == CONTROLLER_DATA[CONF_HOST] + mock_requests = [] + async def mock_request(self, method, path, json=None): + mock_requests.append({"method": method, "path": path, "json": json}) -async def test_controller_site(): - """Config entry site and controller site are the same.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG + if path == "s/{site}/stat/sta" and mock_client_responses: + return mock_client_responses.popleft() + if path == "s/{site}/stat/device" and mock_device_responses: + return mock_device_responses.popleft() + if path == "s/{site}/rest/user" and mock_client_all_responses: + return mock_client_all_responses.popleft() + return {} - unifi_controller = controller.UniFiController(hass, entry) + with patch("aiounifi.Controller.login", return_value=True), patch( + "aiounifi.Controller.sites", return_value=sites + ), patch("aiounifi.Controller.request", new=mock_request): + await unifi.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) - assert unifi_controller.site == CONTROLLER_DATA[CONF_SITE_ID] + controller_id = unifi.get_controller_id_from_config_entry(config_entry) + if controller_id not in hass.data[unifi.DOMAIN]: + return None + controller = hass.data[unifi.DOMAIN][controller_id] + controller.mock_client_responses = mock_client_responses + controller.mock_device_responses = mock_device_responses + controller.mock_client_all_responses = mock_client_all_responses + controller.mock_requests = mock_requests -async def test_controller_mac(): - """Test that it is possible to identify controller mac.""" - hass = Mock() - hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} - hass.data[UNIFI_WIRELESS_CLIENTS].get_data.return_value = set() - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = {} - client = Mock() - client.ip = "1.2.3.4" - client.mac = "00:11:22:33:44:55" - api = Mock() - api.initialize.return_value = mock_coro(True) - api.clients = {"client1": client} - api.sites.return_value = mock_coro(CONTROLLER_SITES) - - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True - - assert unifi_controller.mac == "00:11:22:33:44:55" - - -async def test_controller_no_mac(): - """Test that it works to not find the controllers mac.""" - hass = Mock() - hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = {} - client = Mock() - client.ip = "5.6.7.8" - api = Mock() - api.initialize.return_value = mock_coro(True) - api.clients = {"client1": client} - api.sites.return_value = mock_coro(CONTROLLER_SITES) - api.clients = {} - - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True - - assert unifi_controller.mac is None - - -async def test_controller_not_accessible(): - """Retry to login gets scheduled when connection fails.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - api = Mock() - api.initialize.return_value = mock_coro(True) + return controller - unifi_controller = controller.UniFiController(hass, entry) - with patch.object( - controller, "get_controller", side_effect=errors.CannotConnect - ), pytest.raises(ConfigEntryNotReady): - await unifi_controller.async_setup() - - -async def test_controller_unknown_error(): - """Unknown errors are handled.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - api = Mock() - api.initialize.return_value = mock_coro(True) +async def test_controller_setup(hass): + """Successful setup.""" + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ) as forward_entry_setup: + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + entry = controller.config_entry + assert len(forward_entry_setup.mock_calls) == len( + unifi.controller.SUPPORTED_PLATFORMS + ) + assert forward_entry_setup.mock_calls[0][1] == (entry, "device_tracker") + assert forward_entry_setup.mock_calls[1][1] == (entry, "sensor") + assert forward_entry_setup.mock_calls[2][1] == (entry, "switch") + + assert controller.host == CONTROLLER_DATA[CONF_HOST] + assert controller.site == CONTROLLER_DATA[CONF_SITE_ID] + assert controller.site_name in SITES + assert controller.site_role == SITES[controller.site_name]["role"] + + assert ( + controller.option_allow_bandwidth_sensors + == unifi.const.DEFAULT_ALLOW_BANDWIDTH_SENSORS + ) + assert controller.option_block_clients == unifi.const.DEFAULT_BLOCK_CLIENTS + assert controller.option_track_clients == unifi.const.DEFAULT_TRACK_CLIENTS + assert controller.option_track_devices == unifi.const.DEFAULT_TRACK_DEVICES + assert ( + controller.option_track_wired_clients == unifi.const.DEFAULT_TRACK_WIRED_CLIENTS + ) + assert controller.option_detection_time == timedelta( + seconds=unifi.const.DEFAULT_DETECTION_TIME + ) + assert controller.option_ssid_filter == unifi.const.DEFAULT_SSID_FILTER - unifi_controller = controller.UniFiController(hass, entry) + assert controller.mac is None - with patch.object(controller, "get_controller", side_effect=Exception): - assert await unifi_controller.async_setup() is False + assert controller.signal_update == "unifi-update-1.2.3.4-site_id" + assert controller.signal_options_update == "unifi-options-1.2.3.4-site_id" - assert not hass.helpers.event.async_call_later.mock_calls +async def test_controller_mac(hass): + """Test that it is possible to identify controller mac.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[CONTROLLER_HOST], + devices_response=[], + clients_all_response=[], + ) + assert controller.mac == "10:00:00:00:00:01" + + +async def test_controller_import_config(hass): + """Test that import configuration.yaml instructions work.""" + hass.data[UNIFI_CONFIG] = [ + { + CONF_HOST: "1.2.3.4", + CONF_SITE_ID: "Site name", + unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True, + unifi.CONF_BLOCK_CLIENT: ["random mac"], + unifi.CONF_DONT_TRACK_CLIENTS: True, + unifi.CONF_DONT_TRACK_DEVICES: True, + unifi.CONF_DONT_TRACK_WIRED_CLIENTS: True, + unifi.CONF_DETECTION_TIME: 150, + unifi.CONF_SSID_FILTER: ["SSID"], + } + ] + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) -async def test_reset_if_entry_had_wrong_auth(): - """Calling reset when the entry contains wrong auth.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG + assert controller.option_allow_bandwidth_sensors is False + assert controller.option_block_clients == ["random mac"] + assert controller.option_track_clients is False + assert controller.option_track_devices is False + assert controller.option_track_wired_clients is False + assert controller.option_detection_time == timedelta(seconds=150) + assert controller.option_ssid_filter == ["SSID"] - unifi_controller = controller.UniFiController(hass, entry) +async def test_controller_not_accessible(hass): + """Retry to login gets scheduled when connection fails.""" with patch.object( - controller, "get_controller", side_effect=errors.AuthenticationRequired - ): - assert await unifi_controller.async_setup() is False + unifi.controller, "get_controller", side_effect=unifi.errors.CannotConnect + ), pytest.raises(ConfigEntryNotReady): + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + +async def test_controller_unknown_error(hass): + """Unknown errors are handled.""" + with patch.object(unifi.controller, "get_controller", side_effect=Exception): + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + assert hass.data[unifi.DOMAIN] == {} + + +async def test_reset_after_successful_setup(hass): + """Calling reset when the entry has been setup.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) - assert not hass.async_add_job.mock_calls + assert len(controller.listeners) == 5 - assert await unifi_controller.async_reset() + result = await controller.async_reset() + await hass.async_block_till_done() + assert result is True + assert len(controller.listeners) == 0 -async def test_reset_unloads_entry_if_setup(): - """Calling reset when the entry has been setup.""" - hass = Mock() - hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} - 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) - api.clients = [] - - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True - - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == len( - controller.SUPPORTED_PLATFORMS + +async def test_failed_update_failed_login(hass): + """Running update can handle a failed login.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], ) - hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) - assert await unifi_controller.async_reset() + with patch.object( + controller.api.clients, "update", side_effect=aiounifi.LoginRequired + ), patch.object(controller.api, "login", side_effect=aiounifi.AiounifiException): + await controller.async_update() + await hass.async_block_till_done() + + assert controller.available is False + + +async def test_failed_update_successful_login(hass): + """Running update can login when requested.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == len( - controller.SUPPORTED_PLATFORMS + with patch.object( + controller.api.clients, "update", side_effect=aiounifi.LoginRequired + ), patch.object(controller.api, "login", return_value=Mock(True)): + await controller.async_update() + await hass.async_block_till_done() + + assert controller.available is True + + +async def test_failed_update(hass): + """Running update can login when requested.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], ) + with patch.object( + controller.api.clients, "update", side_effect=aiounifi.AiounifiException + ): + await controller.async_update() + await hass.async_block_till_done() + + assert controller.available is False + + await controller.async_update() + await hass.async_block_till_done() + assert controller.available is True + async def test_get_controller(hass): """Successful call.""" - with patch("aiounifi.Controller.login", return_value=mock_coro()): - assert await controller.get_controller(hass, **CONTROLLER_DATA) + with patch("aiounifi.Controller.login", return_value=Mock()): + assert await unifi.controller.get_controller(hass, **CONTROLLER_DATA) async def test_get_controller_verify_ssl_false(hass): """Successful call with verify ssl set to false.""" controller_data = dict(CONTROLLER_DATA) controller_data[CONF_VERIFY_SSL] = False - with patch("aiounifi.Controller.login", return_value=mock_coro()): - assert await controller.get_controller(hass, **controller_data) + with patch("aiounifi.Controller.login", return_value=Mock()): + assert await unifi.controller.get_controller(hass, **controller_data) async def test_get_controller_login_failed(hass): """Check that get_controller can handle a failed login.""" - import aiounifi - result = None with patch("aiounifi.Controller.login", side_effect=aiounifi.Unauthorized): try: - result = await controller.get_controller(hass, **CONTROLLER_DATA) - except errors.AuthenticationRequired: + result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + except unifi.errors.AuthenticationRequired: pass assert result is None async def test_get_controller_controller_unavailable(hass): """Check that get_controller can handle controller being unavailable.""" - import aiounifi - result = None with patch("aiounifi.Controller.login", side_effect=aiounifi.RequestError): try: - result = await controller.get_controller(hass, **CONTROLLER_DATA) - except errors.CannotConnect: + result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + except unifi.errors.CannotConnect: pass assert result is None async def test_get_controller_unknown_error(hass): """Check that get_controller can handle unkown errors.""" - import aiounifi - result = None with patch("aiounifi.Controller.login", side_effect=aiounifi.AiounifiException): try: - result = await controller.get_controller(hass, **CONTROLLER_DATA) - except errors.AuthenticationRequired: + result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + except unifi.errors.AuthenticationRequired: pass assert result is None diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index d2cedb91d8d942..29b165537571b5 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,37 +1,23 @@ """The tests for the UniFi device tracker platform.""" -from collections import deque from copy import copy - from datetime import timedelta -from asynctest import patch - from homeassistant import config_entries from homeassistant.components import unifi from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, - CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, - CONTROLLER_ID as CONF_CONTROLLER_ID, - UNIFI_CONFIG, - UNIFI_WIRELESS_CLIENTS, -) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, - STATE_UNAVAILABLE, ) +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component import homeassistant.components.device_tracker as device_tracker import homeassistant.util.dt as dt_util +from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration + DEFAULT_DETECTION_TIME = timedelta(seconds=300) CLIENT_1 = { @@ -88,77 +74,6 @@ "version": "4.0.42.10433", } -CONTROLLER_DATA = { - CONF_HOST: "mock-host", - CONF_USERNAME: "mock-user", - CONF_PASSWORD: "mock-pswd", - CONF_PORT: 1234, - CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: False, -} - -ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} - -CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") - - -async def setup_unifi_integration( - hass, config, options, clients_response, devices_response, clients_all_response -): - """Create the UniFi controller.""" - hass.data[UNIFI_CONFIG] = [] - hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) - config_entry = config_entries.ConfigEntry( - version=1, - domain=unifi.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, - options=options, - entry_id=1, - ) - - sites = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "viewer"}} - - mock_client_responses = deque() - mock_client_responses.append(clients_response) - - mock_device_responses = deque() - mock_device_responses.append(devices_response) - - mock_client_all_responses = deque() - mock_client_all_responses.append(clients_all_response) - - mock_requests = [] - - async def mock_request(self, method, path, json=None): - mock_requests.append({"method": method, "path": path, "json": json}) - if path == "s/{site}/stat/sta" and mock_client_responses: - return mock_client_responses.popleft() - if path == "s/{site}/stat/device" and mock_device_responses: - return mock_device_responses.popleft() - if path == "s/{site}/rest/user" and mock_client_all_responses: - return mock_client_all_responses.popleft() - return {} - - with patch("aiounifi.Controller.login", return_value=True), patch( - "aiounifi.Controller.sites", return_value=sites - ), patch("aiounifi.Controller.request", new=mock_request): - await unifi.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - - controller_id = unifi.get_controller_id_from_config_entry(config_entry) - controller = hass.data[unifi.DOMAIN][controller_id] - - controller.mock_client_responses = mock_client_responses - controller.mock_device_responses = mock_device_responses - controller.mock_client_all_responses = mock_client_all_responses - - return controller - async def test_platform_manually_configured(hass): """Test that nothing happens when configuring unifi through device tracker platform.""" @@ -177,9 +92,10 @@ async def test_no_clients(hass): hass, ENTRY_CONFIG, options={}, - clients_response={}, - devices_response={}, - clients_all_response={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], ) assert len(hass.states.async_all()) == 2 @@ -191,6 +107,7 @@ async def test_tracked_devices(hass): hass, ENTRY_CONFIG, options={CONF_SSID_FILTER: ["ssid"]}, + sites=SITES, clients_response=[CLIENT_1, CLIENT_2, CLIENT_3], devices_response=[DEVICE_1, DEVICE_2], clients_all_response={}, @@ -267,9 +184,10 @@ async def test_wireless_client_go_wired_issue(hass): hass, ENTRY_CONFIG, options={}, + sites=SITES, clients_response=[client_1_client], - devices_response={}, - clients_all_response={}, + devices_response=[], + clients_all_response=[], ) assert len(hass.states.async_all()) == 3 @@ -316,14 +234,14 @@ async def test_restoring_client(hass): registry.async_get_or_create( device_tracker.DOMAIN, unifi.DOMAIN, - "{}-mock-site".format(CLIENT_1["mac"]), + "{}-site_id".format(CLIENT_1["mac"]), suggested_object_id=CLIENT_1["hostname"], config_entry=config_entry, ) registry.async_get_or_create( device_tracker.DOMAIN, unifi.DOMAIN, - "{}-mock-site".format(CLIENT_2["mac"]), + "{}-site_id".format(CLIENT_2["mac"]), suggested_object_id=CLIENT_2["hostname"], config_entry=config_entry, ) @@ -332,8 +250,9 @@ async def test_restoring_client(hass): hass, ENTRY_CONFIG, options={unifi.CONF_BLOCK_CLIENT: True}, + sites=SITES, clients_response=[CLIENT_2], - devices_response={}, + devices_response=[], clients_all_response=[CLIENT_1], ) assert len(hass.states.async_all()) == 4 @@ -348,9 +267,10 @@ async def test_dont_track_clients(hass): hass, ENTRY_CONFIG, options={unifi.controller.CONF_TRACK_CLIENTS: False}, + sites=SITES, clients_response=[CLIENT_1], devices_response=[DEVICE_1], - clients_all_response={}, + clients_all_response=[], ) assert len(hass.states.async_all()) == 3 @@ -368,9 +288,10 @@ async def test_dont_track_devices(hass): hass, ENTRY_CONFIG, options={unifi.controller.CONF_TRACK_DEVICES: False}, + sites=SITES, clients_response=[CLIENT_1], devices_response=[DEVICE_1], - clients_all_response={}, + clients_all_response=[], ) assert len(hass.states.async_all()) == 3 @@ -388,9 +309,10 @@ async def test_dont_track_wired_clients(hass): hass, ENTRY_CONFIG, options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False}, + sites=SITES, clients_response=[CLIENT_1, CLIENT_2], - devices_response={}, - clients_all_response={}, + devices_response=[], + clients_all_response=[], ) assert len(hass.states.async_all()) == 3 diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index ffd6d97e5b3bcd..845954d8134abf 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -4,11 +4,7 @@ from homeassistant.components import unifi from homeassistant.components.unifi import config_flow from homeassistant.setup import async_setup_component -from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, - CONF_SITE_ID, - CONTROLLER_ID as CONF_CONTROLLER_ID, -) +from homeassistant.components.unifi.const import CONF_CONTROLLER, CONF_SITE_ID from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -117,8 +113,7 @@ async def test_controller_fail_setup(hass): mock_cntrlr.return_value.async_setup.return_value = mock_coro(False) assert await unifi.async_setup_entry(hass, entry) is False - controller_id = CONF_CONTROLLER_ID.format(host="0.0.0.0", site="default") - assert controller_id in hass.data[unifi.DOMAIN] + assert hass.data[unifi.DOMAIN] == {} async def test_controller_no_mac(hass): diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 9064f1c9aba7c4..f591801a966d4c 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1,29 +1,13 @@ """UniFi sensor platform tests.""" -from collections import deque from copy import deepcopy -from asynctest import patch - -from homeassistant import config_entries from homeassistant.components import unifi -from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, - CONF_SITE_ID, - CONTROLLER_ID as CONF_CONTROLLER_ID, - UNIFI_CONFIG, - UNIFI_WIRELESS_CLIENTS, -) from homeassistant.setup import async_setup_component -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) import homeassistant.components.sensor as sensor +from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration + CLIENTS = [ { "hostname": "Wired client hostname", @@ -53,85 +37,6 @@ }, ] -CONTROLLER_DATA = { - CONF_HOST: "mock-host", - CONF_USERNAME: "mock-user", - CONF_PASSWORD: "mock-pswd", - CONF_PORT: 1234, - CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: False, -} - -ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} - -CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") - -SITES = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "admin"}} - - -async def setup_unifi_integration( - hass, - config, - options, - sites, - clients_response, - devices_response, - clients_all_response, -): - """Create the UniFi controller.""" - hass.data[UNIFI_CONFIG] = [] - hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) - config_entry = config_entries.ConfigEntry( - version=1, - domain=unifi.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, - options=options, - entry_id=1, - ) - - mock_client_responses = deque() - mock_client_responses.append(clients_response) - - mock_device_responses = deque() - mock_device_responses.append(devices_response) - - mock_client_all_responses = deque() - mock_client_all_responses.append(clients_all_response) - - mock_requests = [] - - async def mock_request(self, method, path, json=None): - mock_requests.append({"method": method, "path": path, "json": json}) - - if path == "s/{site}/stat/sta" and mock_client_responses: - return mock_client_responses.popleft() - if path == "s/{site}/stat/device" and mock_device_responses: - return mock_device_responses.popleft() - if path == "s/{site}/rest/user" and mock_client_all_responses: - return mock_client_all_responses.popleft() - return {} - - with patch("aiounifi.Controller.login", return_value=True), patch( - "aiounifi.Controller.sites", return_value=sites - ), patch("aiounifi.Controller.request", new=mock_request): - await unifi.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - - controller_id = unifi.get_controller_id_from_config_entry(config_entry) - controller = hass.data[unifi.DOMAIN][controller_id] - - controller.mock_client_responses = mock_client_responses - controller.mock_device_responses = mock_device_responses - controller.mock_client_all_responses = mock_client_all_responses - controller.mock_requests = mock_requests - - return controller - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a controller.""" @@ -160,7 +65,7 @@ async def test_no_clients(hass): assert len(hass.states.async_all()) == 2 -async def test_switches(hass): +async def test_sensors(hass): """Test the update_items function with some clients.""" controller = await setup_unifi_integration( hass, diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 97dda4415279df..3d754fb5dffb14 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1,32 +1,20 @@ """UniFi POE control platform tests.""" -from collections import deque from copy import deepcopy -from asynctest import Mock, patch - -import aiounifi - from homeassistant import config_entries from homeassistant.components import unifi -from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, - CONF_SITE_ID, - CONTROLLER_ID as CONF_CONTROLLER_ID, - UNIFI_CONFIG, - UNIFI_WIRELESS_CLIENTS, -) from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) import homeassistant.components.switch as switch +from .test_controller import ( + CONTROLLER_HOST, + ENTRY_CONFIG, + SITES, + setup_unifi_integration, +) + CLIENT_1 = { "hostname": "client_1", "ip": "10.0.0.1", @@ -79,19 +67,6 @@ "wired-rx_bytes": 1234000000, "wired-tx_bytes": 5678000000, } -CLOUDKEY = { - "hostname": "client_1", - "ip": "mock-host", - "is_wired": True, - "last_seen": 1562600145, - "mac": "10:00:00:00:00:01", - "name": "Cloud key", - "oui": "Producer", - "sw_mac": "00:00:00:00:01:01", - "sw_port": 1, - "wired-rx_bytes": 1234000000, - "wired-tx_bytes": 5678000000, -} POE_SWITCH_CLIENTS = [ { "hostname": "client_1", @@ -211,85 +186,6 @@ "oui": "Producer", } -CONTROLLER_DATA = { - CONF_HOST: "mock-host", - CONF_USERNAME: "mock-user", - CONF_PASSWORD: "mock-pswd", - CONF_PORT: 1234, - CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: False, -} - -ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} - -CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") - -SITES = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "admin"}} - - -async def setup_unifi_integration( - hass, - config, - options, - sites, - clients_response, - devices_response, - clients_all_response, -): - """Create the UniFi controller.""" - hass.data[UNIFI_CONFIG] = [] - hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) - config_entry = config_entries.ConfigEntry( - version=1, - domain=unifi.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, - options=options, - entry_id=1, - ) - - mock_client_responses = deque() - mock_client_responses.append(clients_response) - - mock_device_responses = deque() - mock_device_responses.append(devices_response) - - mock_client_all_responses = deque() - mock_client_all_responses.append(clients_all_response) - - mock_requests = [] - - async def mock_request(self, method, path, json=None): - mock_requests.append({"method": method, "path": path, "json": json}) - - if path == "s/{site}/stat/sta" and mock_client_responses: - return mock_client_responses.popleft() - if path == "s/{site}/stat/device" and mock_device_responses: - return mock_device_responses.popleft() - if path == "s/{site}/rest/user" and mock_client_all_responses: - return mock_client_all_responses.popleft() - return {} - - with patch("aiounifi.Controller.login", return_value=True), patch( - "aiounifi.Controller.sites", return_value=sites - ), patch("aiounifi.Controller.request", new=mock_request): - await unifi.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - - controller_id = unifi.get_controller_id_from_config_entry(config_entry) - controller = hass.data[unifi.DOMAIN][controller_id] - - controller.mock_client_responses = mock_client_responses - controller.mock_device_responses = mock_device_responses - controller.mock_client_all_responses = mock_client_all_responses - controller.mock_requests = mock_requests - - return controller - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a controller.""" @@ -331,7 +227,7 @@ async def test_controller_not_client(hass): unifi.const.CONF_TRACK_DEVICES: False, }, sites=SITES, - clients_response=[CLOUDKEY], + clients_response=[CONTROLLER_HOST], devices_response=[DEVICE_1], clients_all_response=[], ) @@ -402,7 +298,51 @@ async def test_switches(hass): assert unblocked.state == "on" -async def test_new_client_discovered(hass): +async def test_new_client_discovered_on_block_control(hass): + """Test if 2nd update has a new client.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"]], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[BLOCKED], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 4 + + controller.mock_client_all_responses.append([BLOCKED]) + + # Calling a service will trigger the updates to run + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True + ) + assert len(controller.mock_requests) == 7 + assert len(hass.states.async_all()) == 4 + assert controller.mock_requests[3] == { + "json": {"mac": "00:00:00:00:01:01", "cmd": "block-sta"}, + "method": "post", + "path": "s/{site}/cmd/stamgr/", + } + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True + ) + assert len(controller.mock_requests) == 11 + assert controller.mock_requests[7] == { + "json": {"mac": "00:00:00:00:01:01", "cmd": "unblock-sta"}, + "method": "post", + "path": "s/{site}/cmd/stamgr/", + } + + +async def test_new_client_discovered_on_poe_control(hass): """Test if 2nd update has a new client.""" controller = await setup_unifi_integration( hass, @@ -530,59 +470,3 @@ async def test_restoring_client(hass): device_1 = hass.states.get("switch.client_1") assert device_1 is not None - - -async def test_failed_update_failed_login(hass): - """Running update can handle a failed login.""" - controller = await setup_unifi_integration( - hass, - ENTRY_CONFIG, - options={ - unifi.CONF_BLOCK_CLIENT: ["random mac"], - unifi.const.CONF_TRACK_CLIENTS: False, - unifi.const.CONF_TRACK_DEVICES: False, - }, - sites=SITES, - clients_response=[], - devices_response=[], - clients_all_response=[], - ) - - assert len(controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 2 - - with patch.object( - controller.api.clients, "update", side_effect=aiounifi.LoginRequired - ), patch.object(controller.api, "login", side_effect=aiounifi.AiounifiException): - await controller.async_update() - await hass.async_block_till_done() - - assert controller.available is False - - -async def test_failed_update_successful_login(hass): - """Running update can login when requested.""" - controller = await setup_unifi_integration( - hass, - ENTRY_CONFIG, - options={ - unifi.CONF_BLOCK_CLIENT: ["random mac"], - unifi.const.CONF_TRACK_CLIENTS: False, - unifi.const.CONF_TRACK_DEVICES: False, - }, - sites=SITES, - clients_response=[], - devices_response=[], - clients_all_response=[], - ) - - assert len(controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 2 - - with patch.object( - controller.api.clients, "update", side_effect=aiounifi.LoginRequired - ), patch.object(controller.api, "login", return_value=Mock(True)): - await controller.async_update() - await hass.async_block_till_done() - - assert controller.available is True From dabdf8b577516bd04168234671b6ada335c7c763 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Oct 2019 22:09:48 +0200 Subject: [PATCH 0658/3953] Validate generated device triggers (#27264) * Validate generated trigger * Update scaffold --- .../binary_sensor/device_trigger.py | 2 ++ .../components/deconz/device_trigger.py | 5 ++-- .../device_automation/toggle_entity.py | 15 +++++++---- .../components/sensor/device_trigger.py | 9 ++++--- .../components/zha/device_trigger.py | 2 ++ .../integration/device_trigger.py | 26 ++++++++++--------- .../tests/test_device_trigger.py | 7 ++--- 7 files changed, 41 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index e05713b5c67d09..5d58131fde9be7 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -191,6 +191,7 @@ async def async_attach_trigger(hass, config, action, automation_info): to_state = "off" state_config = { + state_automation.CONF_PLATFORM: "state", state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], state_automation.CONF_FROM: from_state, state_automation.CONF_TO: to_state, @@ -198,6 +199,7 @@ async def async_attach_trigger(hass, config, action, automation_info): if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] + state_config = state_automation.TRIGGER_SCHEMA(state_config) return await state_automation.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index badbe8b8651278..9f66cf156aab9e 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -222,13 +222,14 @@ async def async_attach_trigger(hass, config, action, automation_info): event_id = deconz_event.serial - state_config = { + event_config = { event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, } + event_config = event.TRIGGER_SCHEMA(event_config) return await event.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, event_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index af29625f3a1498..29110144c14ccd 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -3,7 +3,10 @@ import voluptuous as vol from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE -from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.automation import ( + state as state_automation, + AutomationActionType, +) from homeassistant.components.device_automation.const import ( CONF_IS_OFF, CONF_IS_ON, @@ -152,14 +155,16 @@ async def async_attach_trigger( from_state = "on" to_state = "off" state_config = { - state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, - state.CONF_TO: to_state, + state_automation.CONF_PLATFORM: "state", + state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_automation.CONF_FROM: from_state, + state_automation.CONF_TO: to_state, } if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - return await state.async_attach_trigger( + state_config = state_automation.TRIGGER_SCHEMA(state_config) + return await state_automation.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 0d9e8f5af80838..bd53dca0c9d11a 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -87,14 +87,17 @@ async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" numeric_state_config = { + numeric_state_automation.CONF_PLATFORM: "numeric_state", numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE), - numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW), - numeric_state_automation.CONF_FOR: config.get(CONF_FOR), } + if CONF_ABOVE in config: + numeric_state_config[numeric_state_automation.CONF_ABOVE] = config[CONF_ABOVE] + if CONF_BELOW in config: + numeric_state_config[numeric_state_automation.CONF_BELOW] = config[CONF_BELOW] if CONF_FOR in config: numeric_state_config[CONF_FOR] = config[CONF_FOR] + numeric_state_config = numeric_state_automation.TRIGGER_SCHEMA(numeric_state_config) return await numeric_state_automation.async_attach_trigger( hass, numeric_state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index c1ea3c2b761698..ddf7465e0c064e 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -35,10 +35,12 @@ async def async_attach_trigger(hass, config, action, automation_info): trigger = zha_device.device_automation_triggers[trigger] event_config = { + event.CONF_PLATFORM: "event", event.CONF_EVENT_TYPE: ZHA_EVENT, event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, } + event_config = event.TRIGGER_SCHEMA(event_config) return await event.async_attach_trigger( hass, event_config, action, automation_info, platform_type="device" ) diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index f7e9fc091f8809..e0741734d5f7ff 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -12,7 +12,7 @@ STATE_OFF, ) from homeassistant.core import HomeAssistant, CALLBACK_TYPE -from homeassistant.helpers import entity_registry +from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType from homeassistant.components.automation import state, AutomationActionType from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA @@ -22,7 +22,10 @@ TRIGGER_TYPES = {"turned_on", "turned_off"} TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( - {vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES)} + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } ) @@ -87,14 +90,13 @@ async def async_attach_trigger( from_state = STATE_ON to_state = STATE_OFF - return state.async_attach_trigger( - hass, - { - CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, - state.CONF_TO: to_state, - }, - action, - automation_info, - platform_type="device", + state_config = { + state.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + state_config = state.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" ) diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py index c22197bb136832..99e1f8937afd96 100644 --- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -1,7 +1,7 @@ """The tests for NEW_NAME device triggers.""" import pytest -from homeassistant.components.switch import DOMAIN +from homeassistant.components.NEW_DOMAIN import DOMAIN from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation @@ -9,6 +9,7 @@ from tests.common import ( MockConfigEntry, + assert_lists_same, async_mock_service, mock_device_registry, mock_registry, @@ -35,7 +36,7 @@ def calls(hass): async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a switch.""" + """Test we get the expected triggers from a NEW_DOMAIN.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -60,7 +61,7 @@ async def test_get_triggers(hass, device_reg, entity_reg): }, ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert triggers == expected_triggers + assert_lists_same(triggers, expected_triggers) async def test_if_fires_on_state_change(hass, calls): From 1087abd3b52f73640b189946e9cabdc0887e2410 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 8 Oct 2019 00:32:12 +0000 Subject: [PATCH 0659/3953] [ci skip] Translation update --- .../components/airly/.translations/fr.json | 14 ++++- .../components/airly/.translations/lb.json | 4 +- .../components/airly/.translations/sl.json | 22 ++++++++ .../airly/.translations/zh-Hant.json | 22 ++++++++ .../binary_sensor/.translations/fr.json | 54 +++++++++++++++++++ .../components/deconz/.translations/fr.json | 1 + .../components/deconz/.translations/no.json | 18 +++---- .../components/deconz/.translations/sl.json | 1 + .../components/ecobee/.translations/fr.json | 24 +++++++++ .../components/met/.translations/pl.json | 2 +- .../components/neato/.translations/ca.json | 27 ++++++++++ .../components/neato/.translations/da.json | 3 +- .../components/neato/.translations/fr.json | 27 ++++++++++ .../components/neato/.translations/it.json | 27 ++++++++++ .../components/neato/.translations/lb.json | 27 ++++++++++ .../components/neato/.translations/no.json | 1 + .../components/neato/.translations/pl.json | 27 ++++++++++ .../components/neato/.translations/ru.json | 3 +- .../components/neato/.translations/sl.json | 17 ++++++ .../neato/.translations/zh-Hant.json | 27 ++++++++++ .../opentherm_gw/.translations/fr.json | 15 ++++-- .../opentherm_gw/.translations/lb.json | 23 ++++++++ .../opentherm_gw/.translations/pl.json | 12 +++++ .../opentherm_gw/.translations/sl.json | 23 ++++++++ .../opentherm_gw/.translations/zh-Hant.json | 23 ++++++++ .../components/plex/.translations/ca.json | 4 ++ .../components/plex/.translations/en.json | 5 ++ .../components/plex/.translations/fr.json | 23 +++++++- .../components/plex/.translations/it.json | 5 ++ .../components/plex/.translations/pl.json | 1 + .../components/plex/.translations/ru.json | 2 +- .../components/plex/.translations/sl.json | 1 + .../rainmachine/.translations/pl.json | 2 +- .../components/sensor/.translations/fr.json | 26 +++++++++ .../components/sensor/.translations/pl.json | 9 ++++ .../components/sensor/.translations/sl.json | 13 +++++ .../components/soma/.translations/fr.json | 13 +++++ .../components/soma/.translations/pl.json | 13 +++++ .../transmission/.translations/fr.json | 40 ++++++++++++++ .../transmission/.translations/ru.json | 2 +- .../components/unifi/.translations/ca.json | 5 ++ .../components/unifi/.translations/fr.json | 5 ++ .../components/unifi/.translations/it.json | 5 ++ .../components/unifi/.translations/lb.json | 5 ++ .../components/unifi/.translations/no.json | 5 ++ .../components/unifi/.translations/pl.json | 5 ++ .../components/unifi/.translations/ru.json | 5 ++ .../components/unifi/.translations/sl.json | 5 ++ .../unifi/.translations/zh-Hant.json | 5 ++ .../components/zha/.translations/fr.json | 27 ++++++++++ .../components/zha/.translations/no.json | 12 ++--- 51 files changed, 658 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/airly/.translations/sl.json create mode 100644 homeassistant/components/airly/.translations/zh-Hant.json create mode 100644 homeassistant/components/binary_sensor/.translations/fr.json create mode 100644 homeassistant/components/ecobee/.translations/fr.json create mode 100644 homeassistant/components/neato/.translations/ca.json create mode 100644 homeassistant/components/neato/.translations/fr.json create mode 100644 homeassistant/components/neato/.translations/it.json create mode 100644 homeassistant/components/neato/.translations/lb.json create mode 100644 homeassistant/components/neato/.translations/pl.json create mode 100644 homeassistant/components/neato/.translations/zh-Hant.json create mode 100644 homeassistant/components/opentherm_gw/.translations/lb.json create mode 100644 homeassistant/components/opentherm_gw/.translations/pl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/sl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/zh-Hant.json create mode 100644 homeassistant/components/sensor/.translations/fr.json create mode 100644 homeassistant/components/sensor/.translations/pl.json create mode 100644 homeassistant/components/sensor/.translations/sl.json create mode 100644 homeassistant/components/soma/.translations/fr.json create mode 100644 homeassistant/components/soma/.translations/pl.json create mode 100644 homeassistant/components/transmission/.translations/fr.json diff --git a/homeassistant/components/airly/.translations/fr.json b/homeassistant/components/airly/.translations/fr.json index 19561130e15902..cf756a9f4928b9 100644 --- a/homeassistant/components/airly/.translations/fr.json +++ b/homeassistant/components/airly/.translations/fr.json @@ -1,11 +1,21 @@ { "config": { + "error": { + "auth": "La cl\u00e9 API n'est pas correcte.", + "name_exists": "Le nom existe d\u00e9j\u00e0.", + "wrong_location": "Aucune station de mesure Airly dans cette zone." + }, "step": { "user": { "data": { + "api_key": "Cl\u00e9 API Airly", + "latitude": "Latitude", + "longitude": "Longitude", "name": "Nom de l'int\u00e9gration" - } + }, + "title": "Airly" } - } + }, + "title": "Airly" } } \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/lb.json b/homeassistant/components/airly/.translations/lb.json index ca71c2e647f2a9..08aac57d162ae9 100644 --- a/homeassistant/components/airly/.translations/lb.json +++ b/homeassistant/components/airly/.translations/lb.json @@ -2,7 +2,8 @@ "config": { "error": { "auth": "Api Schl\u00ebssel ass net korrekt.", - "name_exists": "Numm g\u00ebtt et schonn" + "name_exists": "Numm g\u00ebtt et schonn", + "wrong_location": "Keng Airly Moos Statioun an d\u00ebsem Ber\u00e4ich" }, "step": { "user": { @@ -12,6 +13,7 @@ "longitude": "L\u00e4ngegrad", "name": "Numm vun der Installatioun" }, + "description": "Airly Loft Qualit\u00e9it Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle gitt op https://developer.airly.eu/register", "title": "Airly" } }, diff --git a/homeassistant/components/airly/.translations/sl.json b/homeassistant/components/airly/.translations/sl.json new file mode 100644 index 00000000000000..08f57d88bcba98 --- /dev/null +++ b/homeassistant/components/airly/.translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Klju\u010d API ni pravilen.", + "name_exists": "Ime \u017ee obstaja", + "wrong_location": "Na tem obmo\u010dju ni merilnih postaj Airly." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API klju\u010d", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime integracije" + }, + "description": "Nastavite Airly integracijo za kakovost zraka. \u010ce \u017eelite ustvariti API klju\u010d pojdite na https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/zh-Hant.json b/homeassistant/components/airly/.translations/zh-Hant.json new file mode 100644 index 00000000000000..bb38d2b9b8c739 --- /dev/null +++ b/homeassistant/components/airly/.translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002", + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", + "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" + }, + "step": { + "user": { + "data": { + "api_key": "Airly API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u6574\u5408\u540d\u7a31" + }, + "description": "\u6b32\u8a2d\u5b9a Airly \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://developer.airly.eu/register \u7522\u751f API \u5bc6\u9470", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json new file mode 100644 index 00000000000000..80792f166354ec --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -0,0 +1,54 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batterie faible", + "is_cold": "{entity_name} est froid", + "is_connected": "{entity_name} est connect\u00e9", + "is_gas": "{entity_name} d\u00e9tecte du gaz", + "is_hot": "{entity_name} est chaud", + "is_light": "{entity_name} d\u00e9tecte de la lumi\u00e8re", + "is_locked": "{entity_name} est verrouill\u00e9", + "is_moist": "{entity_name} est humide", + "is_motion": "{entity_name} d\u00e9tecte un mouvement", + "is_moving": "{entity_name} se d\u00e9place", + "is_no_gas": "{entity_name} ne d\u00e9tecte pas de gaz", + "is_no_light": "{entity_name} ne d\u00e9tecte pas de lumi\u00e8re", + "is_no_motion": "{entity_name} ne d\u00e9tecte pas de mouvement", + "is_no_problem": "{entity_name} ne d\u00e9tecte pas de probl\u00e8me", + "is_no_smoke": "{entity_name} ne d\u00e9tecte pas de fum\u00e9e", + "is_no_sound": "{entity_name} ne d\u00e9tecte pas de son", + "is_no_vibration": "{entity_name} ne d\u00e9tecte pas de vibration", + "is_not_bat_low": "{entity_name} batterie normale", + "is_not_cold": "{entity_name} n'est pas froid", + "is_not_connected": "{entity_name} est d\u00e9connect\u00e9", + "is_not_hot": "{entity_name} n'est pas chaud", + "is_not_locked": "{entity_name} est d\u00e9verrouill\u00e9", + "is_not_moist": "{entity_name} est sec", + "is_not_moving": "{entity_name} ne bouge pas", + "is_not_occupied": "{entity_name} n'est pas occup\u00e9", + "is_not_open": "{entity_name} est ferm\u00e9", + "is_not_plugged_in": "{entity_name} est d\u00e9branch\u00e9", + "is_not_powered": "{entity_name} n'est pas aliment\u00e9", + "is_not_present": "{entity_name} n'est pas pr\u00e9sent", + "is_not_unsafe": "{entity_name} est en s\u00e9curit\u00e9", + "is_occupied": "{entity_name} est occup\u00e9", + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9", + "is_open": "{entity_name} est ouvert", + "is_plugged_in": "{entity_name} est branch\u00e9", + "is_powered": "{entity_name} est aliment\u00e9", + "is_present": "{entity_name} est pr\u00e9sent", + "is_problem": "{entity_name} d\u00e9tecte un probl\u00e8me", + "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", + "is_sound": "{entity_name} d\u00e9tecte du son" + }, + "trigger_type": { + "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", + "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", + "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} activ\u00e9", + "unsafe": "{entity_name} est devenu dangereux", + "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index cc6d22945dcb79..3729f7f556afb9 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Bouton \"{subtype}\" quadruple cliqu\u00e9", "remote_button_quintuple_press": "Bouton \"{subtype}\" quintuple cliqu\u00e9", "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9", + "remote_button_rotation_stopped": "La rotation du bouton \" {subtype} \" s'est arr\u00eat\u00e9e", "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index f779f0918fe5fd..7d05a366cf2248 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -58,16 +58,16 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { - "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", - "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", - "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\" {subtype} \" - knappen femdobbelt klikket", - "remote_button_rotated": "Knappen roterte \" {subtype} \"", - "remote_button_rotation_stopped": "Knappe rotasjon \"{under type}\" stoppet", - "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", + "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt klikket", + "remote_button_rotated": "Knappen roterte \"{subtype}\"", + "remote_button_rotation_stopped": "Knappe rotasjon \"{subtype}\" stoppet", + "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\" {subtype} \"-knappen trippel klikket", + "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", "remote_gyro_activated": "Enhet er ristet" } }, diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 9aebb2a556f6cd..0717bcfc39f455 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", "remote_button_rotated": "Gumb \"{subtype}\" zasukan", + "remote_button_rotation_stopped": "Vrtenje \"{subtype}\" gumba se je ustavilo", "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", diff --git a/homeassistant/components/ecobee/.translations/fr.json b/homeassistant/components/ecobee/.translations/fr.json new file mode 100644 index 00000000000000..85da5b3a4ecc41 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "one_instance_only": "Cette int\u00e9gration ne prend actuellement en charge qu'une seule instance ecobee." + }, + "error": { + "pin_request_failed": "Erreur lors de la demande du code PIN \u00e0 ecobee; veuillez v\u00e9rifier que la cl\u00e9 API est correcte.", + "token_request_failed": "Erreur lors de la demande de jetons \u00e0 ecobee; Veuillez r\u00e9essayer." + }, + "step": { + "authorize": { + "title": "Autoriser l'application sur ecobee.com" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 API" + }, + "description": "Veuillez entrer la cl\u00e9 API obtenue aupr\u00e8s d'ecobee.com.", + "title": "Cl\u00e9 API ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/pl.json b/homeassistant/components/met/.translations/pl.json index d44142213bf066..f647dcf7b45365 100644 --- a/homeassistant/components/met/.translations/pl.json +++ b/homeassistant/components/met/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nazwa ju\u017c istnieje" + "name_exists": "Lokalizacja ju\u017c istnieje" }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/ca.json b/homeassistant/components/neato/.translations/ca.json new file mode 100644 index 00000000000000..d30f9e5ad4bd7b --- /dev/null +++ b/homeassistant/components/neato/.translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ja configurat", + "invalid_credentials": "Credencials inv\u00e0lides" + }, + "create_entry": { + "default": "Consulta la [documentaci\u00f3 de Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credencials inv\u00e0lides", + "unexpected_error": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari", + "vendor": "Venedor" + }, + "description": "Consulta la [documentaci\u00f3 de Neato]({docs_url}).", + "title": "Informaci\u00f3 del compte Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json index 61c594e1011efa..7f0d122f38b2a7 100644 --- a/homeassistant/components/neato/.translations/da.json +++ b/homeassistant/components/neato/.translations/da.json @@ -5,7 +5,8 @@ "invalid_credentials": "Ugyldige legitimationsoplysninger" }, "error": { - "invalid_credentials": "Ugyldige legitimationsoplysninger" + "invalid_credentials": "Ugyldige legitimationsoplysninger", + "unexpected_error": "Uventet fejl" }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/fr.json b/homeassistant/components/neato/.translations/fr.json new file mode 100644 index 00000000000000..941ed18660efff --- /dev/null +++ b/homeassistant/components/neato/.translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00e9j\u00e0 configur\u00e9", + "invalid_credentials": "Informations d'identification invalides" + }, + "create_entry": { + "default": "Voir [Documentation Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Informations d'identification invalides", + "unexpected_error": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur", + "vendor": "Vendeur" + }, + "description": "Voir [Documentation Neato] ( {docs_url} ).", + "title": "Informations compte Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/it.json b/homeassistant/components/neato/.translations/it.json new file mode 100644 index 00000000000000..d5615815dc769b --- /dev/null +++ b/homeassistant/components/neato/.translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Gi\u00e0 configurato", + "invalid_credentials": "Credenziali non valide" + }, + "create_entry": { + "default": "Vedere la [Documentazione di Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credenziali non valide", + "unexpected_error": "Errore inaspettato" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente", + "vendor": "Fornitore" + }, + "description": "Vedere la [Documentazione di Neato]({docs_url}).", + "title": "Informazioni sull'account Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/lb.json b/homeassistant/components/neato/.translations/lb.json new file mode 100644 index 00000000000000..3043ec6ec3754c --- /dev/null +++ b/homeassistant/components/neato/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Scho konfigur\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune" + }, + "create_entry": { + "default": "Kuckt [Neato Dokumentatioun]({docs_url})." + }, + "error": { + "invalid_credentials": "Ong\u00eblteg Login Informatioune", + "unexpected_error": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm", + "vendor": "Hiersteller" + }, + "description": "Kuckt [Neato Dokumentatioun]({docs_url}).", + "title": "Neato Kont Informatiounen" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/no.json b/homeassistant/components/neato/.translations/no.json index d869fa59a18a51..084c4b50e457ed 100644 --- a/homeassistant/components/neato/.translations/no.json +++ b/homeassistant/components/neato/.translations/no.json @@ -18,6 +18,7 @@ "username": "Brukernavn", "vendor": "Leverand\u00f8r" }, + "description": "Se [Neato dokumentasjon]({docs_url}).", "title": "Neato kontoinformasjon" } }, diff --git a/homeassistant/components/neato/.translations/pl.json b/homeassistant/components/neato/.translations/pl.json new file mode 100644 index 00000000000000..caea115b7d5101 --- /dev/null +++ b/homeassistant/components/neato/.translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" + }, + "create_entry": { + "default": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", + "unexpected_error": "Niespodziewany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika", + "vendor": "Dostawca" + }, + "description": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url}).", + "title": "Informacje o koncie Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json index 2d08deb2b1313b..1a206258e24979 100644 --- a/homeassistant/components/neato/.translations/ru.json +++ b/homeassistant/components/neato/.translations/ru.json @@ -8,7 +8,8 @@ "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "error": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/sl.json b/homeassistant/components/neato/.translations/sl.json index 1d256918617a5d..7acbb718d17631 100644 --- a/homeassistant/components/neato/.translations/sl.json +++ b/homeassistant/components/neato/.translations/sl.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "already_configured": "\u017de konfigurirano", + "invalid_credentials": "Neveljavne poverilnice" + }, + "create_entry": { + "default": "Glejte [neato dokumentacija] ({docs_url})." + }, + "error": { + "invalid_credentials": "Neveljavne poverilnice", + "unexpected_error": "Nepri\u010dakovana napaka" + }, "step": { "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime", + "vendor": "Prodajalec" + }, + "description": "Glejte [neato dokumentacija] ({docs_url}).", "title": "Podatki o ra\u010dunu Neato" } }, diff --git a/homeassistant/components/neato/.translations/zh-Hant.json b/homeassistant/components/neato/.translations/zh-Hant.json new file mode 100644 index 00000000000000..61f49cd5da0010 --- /dev/null +++ b/homeassistant/components/neato/.translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "invalid_credentials": "\u6191\u8b49\u7121\u6548" + }, + "create_entry": { + "default": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002" + }, + "error": { + "invalid_credentials": "\u6191\u8b49\u7121\u6548", + "unexpected_error": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "vendor": "\u5ee0\u5546" + }, + "description": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002", + "title": "Neato \u5e33\u865f\u8cc7\u8a0a" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json index a9f9acd9045773..82b9a7aee88167 100644 --- a/homeassistant/components/opentherm_gw/.translations/fr.json +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -1,13 +1,22 @@ { "config": { + "error": { + "already_configured": "Passerelle d\u00e9j\u00e0 configur\u00e9e", + "id_exists": "L'identifiant de la passerelle existe d\u00e9j\u00e0", + "serial_error": "Erreur de connexion \u00e0 l'appareil", + "timeout": "La tentative de connexion a expir\u00e9" + }, "step": { "init": { "data": { "device": "Chemin ou URL", "id": "ID", - "name": "Nom" - } + "name": "Nom", + "precision": "Pr\u00e9cision de la temp\u00e9rature climatique" + }, + "title": "Passerelle OpenTherm" } - } + }, + "title": "Passerelle OpenTherm" } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/lb.json b/homeassistant/components/opentherm_gw/.translations/lb.json new file mode 100644 index 00000000000000..ec1f719a6cc3d5 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ass scho konfigur\u00e9iert", + "id_exists": "Gateway ID g\u00ebtt et schonn", + "serial_error": "Feeler beim verbannen", + "timeout": "Z\u00e4it Iwwerschreidung beim Verbindungs Versuch" + }, + "step": { + "init": { + "data": { + "device": "Pfad oder URL", + "floor_temperature": "Buedem Klima Temperatur", + "id": "ID", + "name": "Numm", + "precision": "Klima Temperatur Prezisioun" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json new file mode 100644 index 00000000000000..7e4a0eed013f3e --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "device": "\u015acie\u017cka lub adres URL", + "name": "Nazwa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/sl.json b/homeassistant/components/opentherm_gw/.translations/sl.json new file mode 100644 index 00000000000000..5de551d5d0c0e9 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Prehod je \u017ee konfiguriran", + "id_exists": "ID prehoda \u017ee obstaja", + "serial_error": "Napaka pri povezovanju z napravo", + "timeout": "Poskus povezave je potekel" + }, + "step": { + "init": { + "data": { + "device": "Pot ali URL", + "floor_temperature": "Temperatura nadstropja", + "id": "ID", + "name": "Ime", + "precision": "Natan\u010dnost temperature " + }, + "title": "OpenTherm Prehod" + } + }, + "title": "OpenTherm Prehod" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json new file mode 100644 index 00000000000000..648f156e8643b1 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "\u9598\u9053\u5668\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728", + "serial_error": "\u9023\u7dda\u81f3\u88dd\u7f6e\u932f\u8aa4", + "timeout": "\u9023\u7dda\u5617\u8a66\u903e\u6642" + }, + "step": { + "init": { + "data": { + "device": "\u8def\u5f91\u6216 URL", + "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", + "id": "ID", + "name": "\u540d\u7a31", + "precision": "\u6eab\u63a7\u7cbe\u6e96\u5ea6" + }, + "title": "OpenTherm \u9598\u9053\u5668" + } + }, + "title": "OpenTherm \u9598\u9053\u5668" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 11e11ebc6fe85b..a3ba5185371eef 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -32,6 +32,10 @@ "description": "Hi ha diversos servidors disponibles, selecciona'n un:", "title": "Selecciona servidor Plex" }, + "start_website_auth": { + "description": "Continua l'autoritzaci\u00f3 a plex.tv.", + "title": "Connexi\u00f3 amb el servidor Plex" + }, "user": { "data": { "manual_setup": "Configuraci\u00f3 manual", diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index efdd75b14819ab..bf927b7f1be07b 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -4,6 +4,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", + "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" @@ -32,6 +33,10 @@ "description": "Multiple servers available, select one:", "title": "Select Plex server" }, + "start_website_auth": { + "description": "Continue to authorize at plex.tv.", + "title": "Connect Plex server" + }, "user": { "data": { "manual_setup": "Manual setup", diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 812de425ef49f2..c9e61dcf2e9407 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -5,18 +5,25 @@ "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "Plex en cours de configuration", "invalid_import": "La configuration import\u00e9e est invalide", + "token_request_timeout": "D\u00e9lai d'obtention du jeton", "unknown": "\u00c9chec pour une raison inconnue" }, "error": { "faulty_credentials": "L'autorisation \u00e0 \u00e9chou\u00e9e", "no_servers": "Aucun serveur li\u00e9 au compte", + "no_token": "Fournir un jeton ou s\u00e9lectionner l'installation manuelle", "not_found": "Serveur Plex introuvable" }, "step": { "manual_setup": { "data": { - "port": "Port" - } + "host": "H\u00f4te", + "port": "Port", + "ssl": "Utiliser SSL", + "token": "Jeton (si n\u00e9cessaire)", + "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "title": "Serveur Plex" }, "select_server": { "data": { @@ -27,6 +34,7 @@ }, "user": { "data": { + "manual_setup": "Installation manuelle", "token": "Jeton plex" }, "description": "Entrez un jeton Plex pour la configuration automatique.", @@ -34,5 +42,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Afficher tous les contr\u00f4les", + "use_episode_art": "Utiliser l'art de l'\u00e9pisode" + }, + "description": "Options pour lecteurs multim\u00e9dia Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 99a6d13e0d4987..8f61f968dba8c4 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -4,6 +4,7 @@ "all_configured": "Tutti i server collegati sono gi\u00e0 configurati", "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", "already_in_progress": "Plex \u00e8 in fase di configurazione", + "discovery_no_file": "Nessun file di configurazione legacy trovato", "invalid_import": "La configurazione importata non \u00e8 valida", "token_request_timeout": "Timeout per l'ottenimento del token", "unknown": "Non riuscito per motivo sconosciuto" @@ -32,6 +33,10 @@ "description": "Sono disponibili pi\u00f9 server, selezionarne uno:", "title": "Selezionare il server Plex" }, + "start_website_auth": { + "description": "Continuare ad autorizzare su plex.tv.", + "title": "Collegare il server Plex" + }, "user": { "data": { "manual_setup": "Configurazione manuale", diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index ce9d2e1e88d73c..9b75a0061e8d1b 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -5,6 +5,7 @@ "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", "already_in_progress": "Plex jest konfigurowany", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", + "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena", "unknown": "Nieznany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 48cacacddfef09..53b4bfd9bb59bc 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -37,7 +37,7 @@ "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "token": "\u0422\u043e\u043a\u0435\u043d" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", + "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", "title": "Plex" } }, diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 49ed34baf763b2..d6bc85725ebb5d 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -5,6 +5,7 @@ "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", "already_in_progress": "Plex se konfigurira", "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", + "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", "unknown": "Ni uspelo iz neznanega razloga" }, "error": { diff --git a/homeassistant/components/rainmachine/.translations/pl.json b/homeassistant/components/rainmachine/.translations/pl.json index 9ab6156549d5a1..cf842efe9f6d0f 100644 --- a/homeassistant/components/rainmachine/.translations/pl.json +++ b/homeassistant/components/rainmachine/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "Konto jest ju\u017c zarejestrowane", - "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { "user": { diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json new file mode 100644 index 00000000000000..676a5aa413fe9c --- /dev/null +++ b/homeassistant/components/sensor/.translations/fr.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} niveau batterie", + "is_humidity": "{entity_name} humidit\u00e9", + "is_illuminance": "{entity_name} \u00e9clairement", + "is_power": "{entity_name} puissance", + "is_pressure": "{entity_name} pression", + "is_signal_strength": "{entity_name} force du signal", + "is_temperature": "{entity_name} temp\u00e9rature", + "is_timestamp": "{entity_name} horodatage", + "is_value": "{entity_name} valeur" + }, + "trigger_type": { + "battery_level": "{entity_name} niveau batterie", + "humidity": "{entity_name} humidit\u00e9", + "illuminance": "{entity_name} \u00e9clairement", + "power": "{entity_name} puissance", + "pressure": "{entity_name} pression", + "signal_strength": "{entity_name} force du signal", + "temperature": "{entity_name} temp\u00e9rature", + "timestamp": "{entity_name} horodatage", + "value": "{entity_name} valeur" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/pl.json b/homeassistant/components/sensor/.translations/pl.json new file mode 100644 index 00000000000000..da1dcc1d6fd890 --- /dev/null +++ b/homeassistant/components/sensor/.translations/pl.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} poziom na\u0142adowania baterii", + "is_humidity": "{entity_name} wilgotno\u015b\u0107", + "is_temperature": "{entity_name} temperatura" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/sl.json b/homeassistant/components/sensor/.translations/sl.json new file mode 100644 index 00000000000000..472af1dfe3b9ea --- /dev/null +++ b/homeassistant/components/sensor/.translations/sl.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} raven baterije", + "is_humidity": "{entity_name} vla\u017enost", + "is_illuminance": "{entity_name} osvetlitev", + "is_power": "{entity_name} mo\u010d", + "is_pressure": "{entity_name} pritisk", + "is_signal_strength": "{entity_name} jakost signala", + "is_temperature": "{entity_name} temperatura" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json new file mode 100644 index 00000000000000..e990fb98dc233a --- /dev/null +++ b/homeassistant/components/soma/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Soma.", + "authorize_url_timeout": "D\u00e9lai d'attente g\u00e9n\u00e9rant l'autorisation de l'URL.", + "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation." + }, + "create_entry": { + "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json new file mode 100644 index 00000000000000..0ed881853b8afd --- /dev/null +++ b/homeassistant/components/soma/.translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", + "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono z Soma" + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/fr.json b/homeassistant/components/transmission/.translations/fr.json new file mode 100644 index 00000000000000..e2360c016ca304 --- /dev/null +++ b/homeassistant/components/transmission/.translations/fr.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "error": { + "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te", + "wrong_credentials": "Mauvais nom d'utilisateur ou mot de passe" + }, + "step": { + "options": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "title": "Configurer les options" + }, + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "title": "Configuration du client Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "description": "Configurer les options pour Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json index 5da2d4f9ef909b..e7a438cae11091 100644 --- a/homeassistant/components/transmission/.translations/ru.json +++ b/homeassistant/components/transmission/.translations/ru.json @@ -33,7 +33,7 @@ "data": { "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" }, - "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission" } } } diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 3741b035d7a9d6..899b532290e929 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -38,6 +38,11 @@ "one": "un", "other": "altre" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Crea sensors d'\u00fas d'ample de banda per a clients de la xarxa" + } } } } diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index 8c2526f8a1565a..c40b7822073dc9 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -32,6 +32,11 @@ "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)", "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau" + } } } } diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json index 5285ed21873529..80b546ebcf8cd8 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -38,6 +38,11 @@ "one": "uno", "other": "altro" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Creare sensori di utilizzo della larghezza di banda per i client di rete" + } } } } diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json index 05b0ffc0c44cdf..4fa1f62c602b65 100644 --- a/homeassistant/components/unifi/.translations/lb.json +++ b/homeassistant/components/unifi/.translations/lb.json @@ -38,6 +38,11 @@ "one": "Een", "other": "M\u00e9i" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente erstellen" + } } } } diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index c21a47c7ea2c8b..9041f0184232ec 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -38,6 +38,11 @@ "one": "en", "other": "andre" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Opprett b\u00e5ndbreddesensorer for nettverksklienter" + } } } } diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index 6366f82b3da0a8..5887460a8a5a56 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -40,6 +40,11 @@ "one": "Jeden", "other": "Inne" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Stw\u00f3rz sensory wykorzystania przepustowo\u015bci przez klient\u00f3w sieciowych" + } } } } diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index 2a3a6207cf5a81..d7451bd81a0ce4 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -32,6 +32,11 @@ "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)", "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" + } } } } diff --git a/homeassistant/components/unifi/.translations/sl.json b/homeassistant/components/unifi/.translations/sl.json index 35000bf4e1f28d..7084c4609c5844 100644 --- a/homeassistant/components/unifi/.translations/sl.json +++ b/homeassistant/components/unifi/.translations/sl.json @@ -40,6 +40,11 @@ "other": "OSTALO", "two": "DVA" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Ustvarite senzorje porabe pasovne \u0161irine za omre\u017ene odjemalce" + } } } } diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index 498afcbb10a19f..5e0b881af15200 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -32,6 +32,11 @@ "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09", "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u65b0\u589e\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668" + } } } } diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index 48328aed878189..f8b78af5721ab4 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -16,5 +16,32 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Hurlement", + "warn": "Pr\u00e9venir" + }, + "trigger_subtype": { + "both_buttons": "Les deux boutons", + "button_1": "Premier bouton", + "button_2": "Deuxi\u00e8me bouton", + "button_3": "Troisi\u00e8me bouton", + "button_4": "Quatri\u00e8me bouton", + "button_5": "Cinqui\u00e8me bouton", + "button_6": "Sixi\u00e8me bouton", + "close": "Fermer", + "left": "Gauche", + "open": "Ouvert", + "right": "Droite", + "turn_off": "\u00c9teindre", + "turn_on": "Allumer" + }, + "trigger_type": { + "device_shaken": "Appareil secou\u00e9", + "device_tilted": "Dispositif inclin\u00e9", + "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", + "remote_button_triple_press": "Bouton\"{sous-type}\" \u00e0 trois clics" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 390069b7698c4e..18c4c3c9ff2a14 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -53,12 +53,12 @@ "device_rotated": "Enheten roterte \"{subtype}\"", "device_shaken": "Enhet er ristet", "device_slid": "Enheten skled \"{subtype}\"", - "device_tilted": "Enhetn skr\u00e5stilt", - "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", - "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", - "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\" {subtype} \" - knappen ble femdobbelt klikket", + "device_tilted": "Enheten skr\u00e5stilt", + "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen ble femdobbelt klikket", "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen ble trippel klikket" From c9e26b6fd0cb062678f69fdda43d8e63f31b8092 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Oct 2019 20:08:07 -0700 Subject: [PATCH 0660/3953] Improve speed websocket sends messages (#27295) * Improve speed websocket sends messages * return -> continue --- homeassistant/components/websocket_api/http.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 17a6709496a4e6..08a0430ee2a976 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -61,12 +61,15 @@ async def _writer(self): message = await self._to_write.get() if message is None: break + self._logger.debug("Sending %s", message) + + if isinstance(message, str): + await self.wsock.send_str(message) + continue + try: - if isinstance(message, str): - await self.wsock.send_str(message) - else: - await self.wsock.send_json(message, dumps=JSON_DUMP) + dumped = JSON_DUMP(message) except (ValueError, TypeError) as err: self._logger.error( "Unable to serialize to JSON: %s\n%s", err, message @@ -76,6 +79,9 @@ async def _writer(self): message["id"], ERR_UNKNOWN_ERROR, "Invalid JSON in response" ) ) + continue + + await self.wsock.send_str(dumped) @callback def _send_message(self, message): From c72ac87c73dc0ea8ce2c1f279c640872c12dbaac Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Oct 2019 05:10:21 +0200 Subject: [PATCH 0661/3953] Fix device condition scaffold (#27300) --- .../integration/device_condition.py | 35 ++++++++++++++----- .../tests/test_device_condition.py | 7 ++-- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index d19fa8817a0299..e9c7e55e23ac7a 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -4,24 +4,28 @@ from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_CONDITION, CONF_DOMAIN, CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, CONF_ENTITY_ID, + STATE_OFF, STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import condition, entity_registry +from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from . import DOMAIN # TODO specify your supported condition types. -CONDITION_TYPES = {"is_on"} +CONDITION_TYPES = {"is_on", "is_off"} CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( - {vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES)} + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } ) @@ -39,13 +43,22 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[str] # TODO add your own conditions. conditions.append( { - CONF_PLATFORM: "device", + CONF_CONDITION: "device", CONF_DEVICE_ID: device_id, CONF_DOMAIN: DOMAIN, CONF_ENTITY_ID: entry.entity_id, CONF_TYPE: "is_on", } ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_off", + } + ) return conditions @@ -56,9 +69,13 @@ def async_condition_from_config( """Create a function to test a device condition.""" if config_validation: config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_on": + state = STATE_ON + else: + state = STATE_OFF - def test_is_on(hass: HomeAssistant, variables: TemplateVarsType) -> bool: - """Test if an entity is on.""" - return condition.state(hass, config[ATTR_ENTITY_ID], STATE_ON) + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) - return test_is_on + return test_is_state diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py index d9cef083510b0f..1ae4df5f1b76b4 100644 --- a/script/scaffold/templates/device_condition/tests/test_device_condition.py +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -1,7 +1,7 @@ """The tests for NEW_NAME device conditions.""" import pytest -from homeassistant.components.switch import DOMAIN +from homeassistant.components.NEW_DOMAIN import DOMAIN from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation @@ -9,6 +9,7 @@ from tests.common import ( MockConfigEntry, + assert_lists_same, async_mock_service, mock_device_registry, mock_registry, @@ -35,7 +36,7 @@ def calls(hass): async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a switch.""" + """Test we get the expected conditions from a NEW_DOMAIN.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -60,7 +61,7 @@ async def test_get_conditions(hass, device_reg, entity_reg): }, ] conditions = await async_get_device_automations(hass, "condition", device_entry.id) - assert conditions == expected_conditions + assert_lists_same(conditions, expected_conditions) async def test_if_state(hass, calls): From 2ccd0039d7aca89297846c1df7b01786fb52dbf6 Mon Sep 17 00:00:00 2001 From: Pierre Sicot Date: Sat, 5 Oct 2019 22:28:19 +0200 Subject: [PATCH 0662/3953] Fix closed status for non horizontal awnings. (#26840) --- homeassistant/components/tahoma/cover.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index a189199bfb2b3c..7448eb27ae072f 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -137,14 +137,13 @@ def update(self): if self._closure is not None: if self.tahoma_device.type == HORIZONTAL_AWNING: self._position = self._closure - self._closed = self._position == 0 else: self._position = 100 - self._closure - self._closed = self._position == 100 if self._position <= 5: self._position = 0 if self._position >= 95: self._position = 100 + self._closed = self._position == 0 else: self._position = None if "core:OpenClosedState" in self.tahoma_device.active_states: From d39e320b9e74b494e988e46a1f62337174e32653 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Oct 2019 10:39:14 -0500 Subject: [PATCH 0663/3953] Fix update on cert_expiry startup (#27137) * Don't force extra update on startup * Skip on entity add instead * Conditional update based on HA state * Only force entity state update when postponed * Clean up state updating * Delay YAML import --- .../components/cert_expiry/sensor.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index b564cff7338584..a5b879e5661e85 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -15,6 +15,7 @@ CONF_PORT, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT @@ -35,18 +36,26 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up certificate expiry sensor.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + + @callback + def do_import(_): + """Process YAML import after HA is fully started.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + ) ) - ) + + # Delay to avoid validation during setup in case we're checking our own cert. + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_import) async def async_setup_entry(hass, entry, async_add_entities): """Add cert-expiry entry.""" async_add_entities( [SSLCertificate(entry.title, entry.data[CONF_HOST], entry.data[CONF_PORT])], - True, + False, + # Don't update in case we're checking our own cert. ) return True @@ -84,17 +93,22 @@ def icon(self): @property def available(self): - """Icon to use in the frontend, if any.""" + """Return the availability of the sensor.""" return self._available async def async_added_to_hass(self): """Once the entity is added we should update to get the initial data loaded.""" + @callback def do_update(_): """Run the update method when the start event was fired.""" - self.update() + self.async_schedule_update_ha_state(True) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) + if self.hass.is_running: + self.async_schedule_update_ha_state(True) + else: + # Delay until HA is fully started in case we're checking our own cert. + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) def update(self): """Fetch the certificate information.""" From 8de942f00f4b11dc7a83d8add88445513789596c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Sun, 6 Oct 2019 17:00:44 +0200 Subject: [PATCH 0664/3953] Fix onvif PTZ service freeze (#27250) --- homeassistant/components/onvif/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 0c116568780a50..29af1049faede3 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -23,7 +23,7 @@ from homeassistant.components.ffmpeg import DATA_FFMPEG, CONF_EXTRA_ARGUMENTS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.helpers.service import extract_entity_ids +from homeassistant.helpers.service import async_extract_entity_ids import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -88,7 +88,7 @@ async def async_handle_ptz(service): tilt = service.data.get(ATTR_TILT, None) zoom = service.data.get(ATTR_ZOOM, None) all_cameras = hass.data[ONVIF_DATA][ENTITIES] - entity_ids = extract_entity_ids(hass, service) + entity_ids = await async_extract_entity_ids(hass, service) target_cameras = [] if not entity_ids: target_cameras = all_cameras From c4165418149986f3316b06072a368dd34cf1c577 Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Mon, 7 Oct 2019 10:40:52 -0700 Subject: [PATCH 0665/3953] Fix the todoist integration (#27273) * Fixed the todoist integration. * Removing unused import * Flake8 fixes. * Added username to codeowners. * Updated global codeowners --- CODEOWNERS | 1 + homeassistant/components/todoist/calendar.py | 40 +++++++++++-------- .../components/todoist/manifest.json | 4 +- .../components/todoist/services.yaml | 25 ++++++++++++ requirements_all.txt | 2 +- 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 4e7b0a0cd2ab83..d2cda1f1d07435 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -288,6 +288,7 @@ homeassistant/components/threshold/* @fabaff homeassistant/components/tibber/* @danielhiversen homeassistant/components/tile/* @bachya homeassistant/components/time_date/* @fabaff +homeassistant/components/todoist/* @boralyl homeassistant/components/toon/* @frenck homeassistant/components/tplink/* @rytilahti homeassistant/components/traccar/* @ludeeus diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 75aec037a25ed5..1179fd9086836f 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -36,6 +36,7 @@ DESCRIPTION = "description" # Calendar Platform: Used in the '_get_date()' method DATETIME = "dateTime" +DUE = "due" # Service Call: When is this task due (in natural language)? DUE_DATE_STRING = "due_date_string" # Service Call: The language of DUE_DATE_STRING @@ -206,7 +207,7 @@ def handle_new_task(call): project_id = project_id_lookup[project_name] # Create the task - item = api.items.add(call.data[CONTENT], project_id) + item = api.items.add(call.data[CONTENT], project_id=project_id) if LABELS in call.data: task_labels = call.data[LABELS] @@ -216,11 +217,12 @@ def handle_new_task(call): if PRIORITY in call.data: item.update(priority=call.data[PRIORITY]) + _due: dict = {} if DUE_DATE_STRING in call.data: - item.update(date_string=call.data[DUE_DATE_STRING]) + _due["string"] = call.data[DUE_DATE_STRING] if DUE_DATE_LANG in call.data: - item.update(date_lang=call.data[DUE_DATE_LANG]) + _due["lang"] = call.data[DUE_DATE_LANG] if DUE_DATE in call.data: due_date = dt.parse_datetime(call.data[DUE_DATE]) @@ -231,7 +233,11 @@ def handle_new_task(call): due_date = dt.as_utc(due_date) date_format = "%Y-%m-%dT%H:%M" due_date = datetime.strftime(due_date, date_format) - item.update(due_date_utc=due_date) + _due["date"] = due_date + + if _due: + item.update(due=_due) + # Commit changes api.commit() _LOGGER.debug("Created Todoist task: %s", call.data[CONTENT]) @@ -241,6 +247,17 @@ def handle_new_task(call): ) +def _parse_due_date(data: dict) -> datetime: + """Parse the due date dict into a datetime object.""" + # Add time information to date only strings. + if len(data["date"]) == 10: + data["date"] += "T00:00:00" + # If there is no timezone provided, use UTC. + if data["timezone"] is None: + data["date"] += "Z" + return dt.parse_datetime(data["date"]) + + class TodoistProjectDevice(CalendarEventDevice): """A device for getting the next Task from a Todoist Project.""" @@ -412,16 +429,8 @@ def create_todoist_task(self, data): # complete the task. # Generally speaking, that means right now. task[START] = dt.utcnow() - if data[DUE_DATE_UTC] is not None: - due_date = data[DUE_DATE_UTC] - - # Due dates are represented in RFC3339 format, in UTC. - # Home Assistant exclusively uses UTC, so it'll - # handle the conversion. - time_format = "%a %d %b %Y %H:%M:%S %z" - # HASS' built-in parse time function doesn't like - # Todoist's time format; strptime has to be used. - task[END] = datetime.strptime(due_date, time_format) + if data[DUE] is not None: + task[END] = _parse_due_date(data[DUE]) if self._latest_due_date is not None and ( task[END] > self._latest_due_date @@ -540,9 +549,8 @@ async def async_get_events(self, hass, start_date, end_date): project_task_data = project_data[TASKS] events = [] - time_format = "%a %d %b %Y %H:%M:%S %z" for task in project_task_data: - due_date = datetime.strptime(task["due_date_utc"], time_format) + due_date = _parse_due_date(task["due"]) if start_date < due_date < end_date: event = { "uid": task["id"], diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index dbf1a941e00ba3..e7876c953cc5c9 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -3,8 +3,8 @@ "name": "Todoist", "documentation": "https://www.home-assistant.io/integrations/todoist", "requirements": [ - "todoist-python==7.0.17" + "todoist-python==8.0.0" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@boralyl"] } diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index e69de29bb2d1d6..c2d23cc4bec5a6 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -0,0 +1,25 @@ +new_task: + description: Create a new task and add it to a project. + fields: + content: + description: The name of the task. + example: Pick up the mail. + project: + description: The name of the project this task should belong to. Defaults to Inbox. + example: Errands + labels: + description: Any labels that you want to apply to this task, separated by a comma. + example: Chores,Delivieries + priority: + description: The priority of this task, from 1 (normal) to 4 (urgent). + example: 2 + due_date_string: + description: The day this task is due, in natural language. + example: Tomorrow + due_date_lang: + description: The language of due_date_string. + example: en + due_date: + description: The day this task is due, in format YYYY-MM-DD. + example: 2019-10-22 + diff --git a/requirements_all.txt b/requirements_all.txt index 5328ca322c7297..be88d6c60cb309 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1888,7 +1888,7 @@ thingspeak==0.4.1 tikteck==0.4 # homeassistant.components.todoist -todoist-python==7.0.17 +todoist-python==8.0.0 # homeassistant.components.toon toonapilib==3.2.4 From 73aa341ed880f6aeca164128a172f9a8f48e2f8b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 6 Oct 2019 23:02:58 -0500 Subject: [PATCH 0666/3953] Fix Plex media_player.play_media service (#27278) * First attempt to fix play_media * More changes to media playback * Use playqueues, clean up play_media * Use similar function name, add docstring --- homeassistant/components/plex/media_player.py | 139 ++++++++---------- homeassistant/components/plex/server.py | 14 ++ 2 files changed, 76 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 356c7fe5741702..a49e4c9c057ee1 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -2,10 +2,9 @@ from datetime import timedelta import json import logging +from xml.etree.ElementTree import ParseError import plexapi.exceptions -import plexapi.playlist -import plexapi.playqueue import requests.exceptions from homeassistant.components.media_player import MediaPlayerDevice @@ -16,6 +15,7 @@ SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -543,9 +543,6 @@ def make(self): @property def supported_features(self): """Flag media player features that are supported.""" - if not self._is_player_active: - return 0 - # force show all controls if self.plex_server.show_all_controls: return ( @@ -555,13 +552,11 @@ def supported_features(self): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE ) - # only show controls when we know what device is connecting - if not self._make: - return 0 # no mute support if self.make.lower() == "shield android tv": _LOGGER.debug( @@ -575,8 +570,10 @@ def supported_features(self): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF ) + # Only supports play,pause,stop (and off which really is stop) if self.make.lower().startswith("tivo"): _LOGGER.debug( @@ -585,8 +582,7 @@ def supported_features(self): self.entity_id, ) return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF - # Not all devices support playback functionality - # Playback includes volume, stop/play/pause, etc. + if self.device and "playback" in self._device_protocol_capabilities: return ( SUPPORT_PAUSE @@ -595,6 +591,7 @@ def supported_features(self): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE ) @@ -682,49 +679,74 @@ def play_media(self, media_type, media_id, **kwargs): return src = json.loads(media_id) + library = src.get("library_name") + shuffle = src.get("shuffle", 0) media = None + if media_type == "MUSIC": - media = ( - self.device.server.library.section(src["library_name"]) - .get(src["artist_name"]) - .album(src["album_name"]) - .get(src["track_name"]) - ) + media = self._get_music_media(library, src) elif media_type == "EPISODE": - media = self._get_tv_media( - src["library_name"], - src["show_name"], - src["season_number"], - src["episode_number"], - ) + media = self._get_tv_media(library, src) elif media_type == "PLAYLIST": - media = self.device.server.playlist(src["playlist_name"]) + media = self.plex_server.playlist(src["playlist_name"]) elif media_type == "VIDEO": - media = self.device.server.library.section(src["library_name"]).get( - src["video_name"] - ) + media = self.plex_server.library.section(library).get(src["video_name"]) - if ( - media - and media_type == "EPISODE" - and isinstance(media, plexapi.playlist.Playlist) - ): - # delete episode playlist after being loaded into a play queue - self._client_play_media(media=media, delete=True, shuffle=src["shuffle"]) - elif media: - self._client_play_media(media=media, shuffle=src["shuffle"]) + if media is None: + _LOGGER.error("Media could not be found: %s", media_id) + return + + playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) + try: + self.device.playMedia(playqueue) + except ParseError: + # Temporary workaround for Plexamp / plexapi issue + pass + except requests.exceptions.ConnectTimeout: + _LOGGER.error("Timed out playing on %s", self.name) + + self.update_devices() - def _get_tv_media(self, library_name, show_name, season_number, episode_number): + def _get_music_media(self, library_name, src): + """Find music media and return a Plex media object.""" + artist_name = src["artist_name"] + album_name = src.get("album_name") + track_name = src.get("track_name") + track_number = src.get("track_number") + + artist = self.plex_server.library.section(library_name).get(artist_name) + + if album_name: + album = artist.album(album_name) + + if track_name: + return album.track(track_name) + + if track_number: + for track in album.tracks(): + if int(track.index) == int(track_number): + return track + return None + + return album + + if track_name: + return artist.searchTracks(track_name, maxresults=1) + return artist + + def _get_tv_media(self, library_name, src): """Find TV media and return a Plex media object.""" + show_name = src["show_name"] + season_number = src.get("season_number") + episode_number = src.get("episode_number") target_season = None target_episode = None - show = self.device.server.library.section(library_name).get(show_name) + show = self.plex_server.library.section(library_name).get(show_name) if not season_number: - playlist_name = f"{self.entity_id} - {show_name} Episodes" - return self.device.server.createPlaylist(playlist_name, show.episodes()) + return show for season in show.seasons(): if int(season.seasonNumber) == int(season_number): @@ -741,12 +763,7 @@ def _get_tv_media(self, library_name, show_name, season_number, episode_number): ) else: if not episode_number: - playlist_name = "{} - {} Season {} Episodes".format( - self.entity_id, show_name, str(season_number) - ) - return self.device.server.createPlaylist( - playlist_name, target_season.episodes() - ) + return target_season for episode in target_season.episodes(): if int(episode.index) == int(episode_number): @@ -764,38 +781,6 @@ def _get_tv_media(self, library_name, show_name, season_number, episode_number): return target_episode - def _client_play_media(self, media, delete=False, **params): - """Instruct Plex client to play a piece of media.""" - if not (self.device and "playback" in self._device_protocol_capabilities): - _LOGGER.error("Client cannot play media: %s", self.entity_id) - return - - playqueue = plexapi.playqueue.PlayQueue.create( - self.device.server, media, **params - ) - - # Delete dynamic playlists used to build playqueue (ex. play tv season) - if delete: - media.delete() - - server_url = self.device.server.baseurl.split(":") - self.device.sendCommand( - "playback/playMedia", - **dict( - { - "machineIdentifier": self.device.server.machineIdentifier, - "address": server_url[1].strip("/"), - "port": server_url[-1], - "key": media.key, - "containerKey": "/playQueues/{}?window=100&own=1".format( - playqueue.playQueueID - ), - }, - **params, - ), - ) - self.update_devices() - @property def device_state_attributes(self): """Return the scene state attributes.""" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d4393d38c97427..df9e9f9f6c3528 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,5 +1,6 @@ """Shared class to maintain Plex server instances.""" import plexapi.myplex +import plexapi.playqueue import plexapi.server from requests import Session @@ -109,3 +110,16 @@ def use_episode_art(self): def show_all_controls(self): """Return show_all_controls option.""" return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] + + @property + def library(self): + """Return library attribute from server object.""" + return self._plex_server.library + + def playlist(self, title): + """Return playlist from server object.""" + return self._plex_server.playlist(title) + + def create_playqueue(self, media, **kwargs): + """Create playqueue on Plex server.""" + return plexapi.playqueue.PlayQueue.create(self._plex_server, media, **kwargs) From 463c2e8d45bf89199ea2cb64881cc38e37f14627 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Oct 2019 13:29:12 -0500 Subject: [PATCH 0667/3953] Remove manual config flow step (#27291) --- homeassistant/components/plex/config_flow.py | 59 +----- homeassistant/components/plex/strings.json | 18 +- tests/components/plex/test_config_flow.py | 188 ++++++------------- 3 files changed, 66 insertions(+), 199 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index dd5401950e9a3c..38727ccff067f4 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -12,14 +12,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_URL, - CONF_TOKEN, - CONF_SSL, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL from homeassistant.core import callback from homeassistant.util.json import load_json @@ -30,8 +23,6 @@ CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, - DEFAULT_PORT, - DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, PLEX_CONFIG_FILE, @@ -44,8 +35,6 @@ from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer -USER_SCHEMA = vol.Schema({vol.Optional("manual_setup"): bool}) - _LOGGER = logging.getLogger(__package__) @@ -73,23 +62,17 @@ def async_get_options_flow(config_entry): def __init__(self): """Initialize the Plex flow.""" self.current_login = {} - self.discovery_info = {} self.available_servers = None self.plexauth = None self.token = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - errors = {} - if user_input is not None: - if user_input.pop("manual_setup", False): - return await self.async_step_manual_setup(user_input) + return self.async_show_form(step_id="start_website_auth") - return await self.async_step_plex_website_auth() - - return self.async_show_form( - step_id="user", data_schema=USER_SCHEMA, errors=errors - ) + async def async_step_start_website_auth(self, user_input=None): + """Show a form before starting external authentication.""" + return await self.async_step_plex_website_auth() async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" @@ -120,9 +103,7 @@ async def async_step_server_validate(self, server_config): return self.async_abort(reason="unknown") if errors: - return self.async_show_form( - step_id="user", data_schema=USER_SCHEMA, errors=errors - ) + return self.async_show_form(step_id="start_website_auth", errors=errors) server_id = plex_server.machine_identifier @@ -152,30 +133,6 @@ async def async_step_server_validate(self, server_config): }, ) - async def async_step_manual_setup(self, user_input=None): - """Begin manual configuration.""" - if len(user_input) > 1: - host = user_input.pop(CONF_HOST) - port = user_input.pop(CONF_PORT) - prefix = "https" if user_input.pop(CONF_SSL) else "http" - user_input[CONF_URL] = f"{prefix}://{host}:{port}" - return await self.async_step_server_validate(user_input) - - data_schema = vol.Schema( - { - vol.Required( - CONF_HOST, default=self.discovery_info.get(CONF_HOST) - ): str, - vol.Required( - CONF_PORT, default=self.discovery_info.get(CONF_PORT, DEFAULT_PORT) - ): int, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, - vol.Optional(CONF_TOKEN, default=user_input.get(CONF_TOKEN, "")): str, - } - ) - return self.async_show_form(step_id="manual_setup", data_schema=data_schema) - async def async_step_select_server(self, user_input=None): """Use selected Plex server.""" config = dict(self.current_login) @@ -210,8 +167,6 @@ async def async_step_discovery(self, discovery_info): # Skip discovery if a config already exists or is in progress. return self.async_abort(reason="already_configured") - discovery_info[CONF_PORT] = int(discovery_info[CONF_PORT]) - self.discovery_info = discovery_info json_file = self.hass.config.path(PLEX_CONFIG_FILE) file_config = await self.hass.async_add_executor_job(load_json, json_file) @@ -227,7 +182,7 @@ async def async_step_discovery(self, discovery_info): _LOGGER.info("Imported legacy config, file can be removed: %s", json_file) return await self.async_step_server_validate(server_config) - return await self.async_step_user() + return self.async_abort(reason="discovery_no_file") async def async_step_import(self, import_config): """Import from Plex configuration.""" diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 6538d8e887e18c..aff79acc2ed4cf 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -2,16 +2,6 @@ "config": { "title": "Plex", "step": { - "manual_setup": { - "title": "Plex server", - "data": { - "host": "Host", - "port": "Port", - "ssl": "Use SSL", - "verify_ssl": "Verify SSL certificate", - "token": "Token (if required)" - } - }, "select_server": { "title": "Select Plex server", "description": "Multiple servers available, select one:", @@ -19,12 +9,9 @@ "server": "Server" } }, - "user": { + "start_website_auth": { "title": "Connect Plex server", - "description": "Continue to authorize at plex.tv or manually configure a server.", - "data": { - "manual_setup": "Manual setup" - } + "description": "Continue to authorize at plex.tv." } }, "error": { @@ -36,6 +23,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", + "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 753d565a82b981..e9f48f6a4f80b4 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -6,14 +6,7 @@ import requests.exceptions from homeassistant.components.plex import config_flow -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_VERIFY_SSL, - CONF_TOKEN, - CONF_URL, -) +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -48,34 +41,32 @@ def init_config_flow(hass): async def test_bad_credentials(hass): """Test when provided credentials are rejected.""" + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" - - with patch( + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value="BAD TOKEN" ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: False, - CONF_VERIFY_SSL: False, - CONF_TOKEN: "BAD TOKEN", - }, + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "faulty_credentials" @@ -123,8 +114,8 @@ async def test_discovery(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["type"] == "abort" + assert result["reason"] == "discovery_no_file" async def test_discovery_while_in_progress(hass): @@ -201,7 +192,7 @@ async def test_import_bad_hostname(hass): }, ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "not_found" @@ -212,26 +203,25 @@ async def test_unknown_exception(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.server.PlexServer", side_effect=Exception): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: True, - CONF_VERIFY_SSL: True, - CONF_TOKEN: MOCK_TOKEN, - }, - ) + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( + "plexapi.server.PlexServer", side_effect=Exception + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value="MOCK_TOKEN" + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "abort" assert result["reason"] == "unknown" @@ -245,7 +235,7 @@ async def test_no_servers_found(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mm_plex_account = MagicMock() mm_plex_account.resources = Mock(return_value=[]) @@ -256,9 +246,7 @@ async def test_no_servers_found(hass): "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -266,7 +254,7 @@ async def test_no_servers_found(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "no_servers" @@ -279,7 +267,7 @@ async def test_single_available_server(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() @@ -304,9 +292,7 @@ async def test_single_available_server(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -336,7 +322,7 @@ async def test_multiple_servers_with_selection(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -360,9 +346,7 @@ async def test_multiple_servers_with_selection(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -406,7 +390,7 @@ async def test_adding_last_unconfigured_server(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -430,9 +414,7 @@ async def test_adding_last_unconfigured_server(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -512,7 +494,7 @@ async def test_all_available_servers_configured(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -525,9 +507,7 @@ async def test_all_available_servers_configured(hass): "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -538,58 +518,6 @@ async def test_all_available_servers_configured(hass): assert result["reason"] == "all_configured" -async def test_manual_config(hass): - """Test creating via manual configuration.""" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" - - mock_connections = MockConnections(ssl=True) - - with patch("plexapi.server.PlexServer") as mock_plex_server: - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: True, - CONF_VERIFY_SSL: True, - CONF_TOKEN: MOCK_TOKEN, - }, - ) - assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name - assert ( - result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier - ) - assert ( - result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri - ) - assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN - - async def test_option_flow(hass): """Test config flow selection of one of two bridges.""" @@ -627,15 +555,13 @@ async def test_external_timed_out(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=None ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -655,14 +581,12 @@ async def test_callback_view(hass, aiohttp_client): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" client = await aiohttp_client(hass.http.app) From 1614e0d866ff7eab910e109d6d3a095eb331e050 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Oct 2019 20:08:07 -0700 Subject: [PATCH 0668/3953] Improve speed websocket sends messages (#27295) * Improve speed websocket sends messages * return -> continue --- homeassistant/components/websocket_api/http.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 17a6709496a4e6..08a0430ee2a976 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -61,12 +61,15 @@ async def _writer(self): message = await self._to_write.get() if message is None: break + self._logger.debug("Sending %s", message) + + if isinstance(message, str): + await self.wsock.send_str(message) + continue + try: - if isinstance(message, str): - await self.wsock.send_str(message) - else: - await self.wsock.send_json(message, dumps=JSON_DUMP) + dumped = JSON_DUMP(message) except (ValueError, TypeError) as err: self._logger.error( "Unable to serialize to JSON: %s\n%s", err, message @@ -76,6 +79,9 @@ async def _writer(self): message["id"], ERR_UNKNOWN_ERROR, "Invalid JSON in response" ) ) + continue + + await self.wsock.send_str(dumped) @callback def _send_message(self, message): From 4322310d3670854a469abc8dd5b87ca1a543541e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Oct 2019 21:28:58 -0700 Subject: [PATCH 0669/3953] Bumped version to 0.100.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 68537aff298735..5bd5b7ea7650be 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -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 50b5dba43eb2ec8270b949554aea8c78d64180d8 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Mon, 7 Oct 2019 22:22:13 -0700 Subject: [PATCH 0670/3953] Making withings logs less noisy. (#27311) --- homeassistant/components/withings/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 67cf966c1bcd4a..0293784fd3e4ac 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -397,7 +397,7 @@ async def async_update_measure(self, data) -> None: ] if not measure_groups: - _LOGGER.warning("No measure groups found, setting state to %s", None) + _LOGGER.debug("No measure groups found, setting state to %s", None) self._state = None return @@ -417,7 +417,7 @@ async def async_update_sleep_state(self, data) -> None: return if not data.series: - _LOGGER.warning("No sleep data, setting state to %s", None) + _LOGGER.debug("No sleep data, setting state to %s", None) self._state = None return @@ -444,7 +444,7 @@ async def async_update_sleep_summary(self, data) -> None: return if not data.series: - _LOGGER.warning("Sleep data has no series, setting state to %s", None) + _LOGGER.debug("Sleep data has no series, setting state to %s", None) self._state = None return From 15870e0185fef1c0a97d1d342e30dcc015bef2ac Mon Sep 17 00:00:00 2001 From: Brendon Baumgartner Date: Tue, 8 Oct 2019 01:14:17 -0700 Subject: [PATCH 0671/3953] Do not fail smtp notify service on connection error (#27240) * smtp notify dont disable service * auth fails smtp notify service --- homeassistant/components/smtp/notify.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 8a96865ab8db63..d592f25a61dc11 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -136,16 +136,15 @@ def connection_is_valid(self): server = None try: server = self.connect() - except smtplib.socket.gaierror: + except (smtplib.socket.gaierror, ConnectionRefusedError): _LOGGER.exception( - "SMTP server not found (%s:%s). " - "Please check the IP address or hostname of your SMTP server", + "SMTP server not found or refused connection (%s:%s). " + "Please check the IP address, hostname, and availability of your SMTP server.", self._server, self._port, ) - return False - except (smtplib.SMTPAuthenticationError, ConnectionRefusedError): + except smtplib.SMTPAuthenticationError: _LOGGER.exception( "Login not possible. " "Please check your setting and/or your credentials" From 5645d43bd1981dce5536eb66cef1dbd555509e04 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 8 Oct 2019 21:50:23 +1100 Subject: [PATCH 0672/3953] move import to top-level (#27314) --- homeassistant/components/transport_nsw/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 9d0610c139e910..79df41ac4899d0 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -3,6 +3,7 @@ import logging import voluptuous as vol +from TransportNSW import TransportNSW import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -120,8 +121,6 @@ class PublicTransportData: def __init__(self, stop_id, route, destination, api_key): """Initialize the data object.""" - import TransportNSW - self._stop_id = stop_id self._route = route self._destination = destination @@ -134,7 +133,7 @@ def __init__(self, stop_id, route, destination, api_key): ATTR_DESTINATION: "n/a", ATTR_MODE: None, } - self.tnsw = TransportNSW.TransportNSW() + self.tnsw = TransportNSW() def update(self): """Get the next leave time.""" From 15c56f1f64bead00c479d47365d778ec014db787 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 8 Oct 2019 21:50:46 +1100 Subject: [PATCH 0673/3953] Move imports in geo_rss_events component (#27313) * move imports to top-level * fixed patch path * added myself as codeowner * regenerated codeowners --- CODEOWNERS | 1 + homeassistant/components/geo_rss_events/manifest.json | 6 ++++-- homeassistant/components/geo_rss_events/sensor.py | 8 ++++---- tests/components/geo_rss_events/test_sensor.py | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6e343e91533006..070151d01e076b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -105,6 +105,7 @@ homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb +homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index 8fd19f6b034411..c681807ad0113d 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -1,10 +1,12 @@ { "domain": "geo_rss_events", - "name": "Geo rss events", + "name": "Geo RSS events", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": [ "georss_generic_client==0.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@exxamalte" + ] } diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index 9f336668142155..39e6c5c7e824a1 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -12,6 +12,8 @@ from datetime import timedelta import voluptuous as vol +from georss_client import UPDATE_OK, UPDATE_OK_NO_DATA +from georss_client.generic_feed import GenericFeed import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -108,7 +110,6 @@ def __init__( self._state = None self._state_attributes = None self._unit_of_measurement = unit_of_measurement - from georss_client.generic_feed import GenericFeed self._feed = GenericFeed( coordinates, @@ -146,10 +147,9 @@ def device_state_attributes(self): def update(self): """Update this sensor from the GeoRSS service.""" - import georss_client status, feed_entries = self._feed.update() - if status == georss_client.UPDATE_OK: + if status == UPDATE_OK: _LOGGER.debug( "Adding events to sensor %s: %s", self.entity_id, feed_entries ) @@ -159,7 +159,7 @@ def update(self): for entry in feed_entries: matrix[entry.title] = f"{entry.distance_to_home:.0f}km" self._state_attributes = matrix - elif status == georss_client.UPDATE_OK_NO_DATA: + elif status == UPDATE_OK_NO_DATA: _LOGGER.debug("Update successful, but no data received from %s", self._feed) # Don't change the state or state attributes. else: diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index 3a4c5333ba836a..492290b9519202 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -59,7 +59,7 @@ def _generate_mock_feed_entry( feed_entry.category = category return feed_entry - @mock.patch("georss_client.generic_feed.GenericFeed") + @mock.patch("homeassistant.components.geo_rss_events.sensor.GenericFeed") def test_setup(self, mock_feed): """Test the general setup of the platform.""" # Set up some mock feed entries for this test. @@ -122,7 +122,7 @@ def test_setup(self, mock_feed): ATTR_ICON: "mdi:alert", } - @mock.patch("georss_client.generic_feed.GenericFeed") + @mock.patch("homeassistant.components.geo_rss_events.sensor.GenericFeed") def test_setup_with_categories(self, mock_feed): """Test the general setup of the platform.""" # Set up some mock feed entries for this test. From e176f16141f4f08bf42e5fa49cb973b025aa0ae4 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 9 Oct 2019 01:14:50 +1100 Subject: [PATCH 0674/3953] move import to top-level (#27320) --- homeassistant/components/feedreader/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 44ec95f8213154..e4ec154620e94a 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -6,6 +6,7 @@ import pickle import voluptuous as vol +import feedparser from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL from homeassistant.helpers.event import track_time_interval @@ -87,8 +88,6 @@ def last_update_successful(self): def _update(self): """Update the feed and publish new entries to the event bus.""" - import feedparser - _LOGGER.info("Fetching new data from feed %s", self._url) self._feed = feedparser.parse( self._url, From 7a57c3a66af0e47cb6a1f9971dd2b14e6acae1bf Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 8 Oct 2019 16:23:21 +0200 Subject: [PATCH 0675/3953] Set pytz to >=2019.03 --- 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 a64e0dc38e7b75..d583dbffe05491 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.4 -pytz>=2019.02 +pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 diff --git a/requirements_all.txt b/requirements_all.txt index 17f39ac1d8df95..2782c3709d316d 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.4 -pytz>=2019.02 +pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 diff --git a/setup.py b/setup.py index 23a8a808f4376f..1824ba2d41b0b4 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ "cryptography==2.7", "pip>=8.0.3", "python-slugify==3.0.4", - "pytz>=2019.02", + "pytz>=2019.03", "pyyaml==5.1.2", "requests==2.22.0", "ruamel.yaml==0.15.100", From df8bf5159888169c437c4f297e59c68e23734f17 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 8 Oct 2019 10:54:01 -0500 Subject: [PATCH 0676/3953] Fix single Plex server case (#27326) --- homeassistant/components/plex/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index df9e9f9f6c3528..6ab114307664b3 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -57,7 +57,7 @@ def _set_missing_url(): raise ServerNotSpecified(available_servers) server_choice = ( - self._server_name if self._server_name else available_servers[0] + self._server_name if self._server_name else available_servers[0][0] ) connections = account.resource(server_choice).connections local_url = [x.httpuri for x in connections if x.local] From 937d3488674375c5bddcf1585ff9ad64444b98a0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 8 Oct 2019 18:00:11 +0200 Subject: [PATCH 0677/3953] Upgrade certifi to >=2019.9.11 (#27323) --- 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 d583dbffe05491..6bd21ac3849e89 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.2.0 bcrypt==3.1.7 -certifi>=2019.6.16 +certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2782c3709d316d..1369d0ff0e6bb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,7 +4,7 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.2.0 bcrypt==3.1.7 -certifi>=2019.6.16 +certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" importlib-metadata==0.23 jinja2>=2.10.1 diff --git a/setup.py b/setup.py index 1824ba2d41b0b4..f4e94faf553cf9 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ "async_timeout==3.0.1", "attrs==19.2.0", "bcrypt==3.1.7", - "certifi>=2019.6.16", + "certifi>=2019.9.11", 'contextvars==2.4;python_version<"3.7"', "importlib-metadata==0.23", "jinja2>=2.10.1", From 0a66a03de607313311e7593ddfefbc5e830ecadf Mon Sep 17 00:00:00 2001 From: ottersen <42288505+ottersen@users.noreply.github.com> Date: Tue, 8 Oct 2019 18:27:49 +0200 Subject: [PATCH 0678/3953] Align user name vs username (#27328) Align to be Username as all other integrations --- homeassistant/components/transmission/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 7160cd109c4794..203ed07adb54a0 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -7,7 +7,7 @@ "data": { "name": "Name", "host": "Host", - "username": "User name", + "username": "Username", "password": "Password", "port": "Port" } @@ -37,4 +37,4 @@ } } } -} \ No newline at end of file +} From 396e68a4b90a092f2ea4987695c4b37e13820da9 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 8 Oct 2019 18:28:37 +0200 Subject: [PATCH 0679/3953] Upgrade beautifulsoup4 to 4.8.1 (#27325) --- homeassistant/components/scrape/manifest.json | 2 +- homeassistant/components/scrape/sensor.py | 3 +-- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 989070900ca462..5fdcca372b91d8 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/integrations/scrape", "requirements": [ - "beautifulsoup4==4.8.0" + "beautifulsoup4==4.8.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b6a6fdf4896f1f..0bfb7351c881e6 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,6 +1,7 @@ """Support for getting data from websites with scraping.""" import logging +from bs4 import BeautifulSoup import voluptuous as vol from requests.auth import HTTPBasicAuth, HTTPDigestAuth @@ -124,8 +125,6 @@ def update(self): _LOGGER.error("Unable to retrieve data") return - from bs4 import BeautifulSoup - raw_data = BeautifulSoup(self.rest.data, "html.parser") _LOGGER.debug(raw_data) diff --git a/requirements_all.txt b/requirements_all.txt index 1369d0ff0e6bb8..f6993cc72c55ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ batinfo==0.4.2 # beacontools[scan]==1.2.3 # homeassistant.components.scrape -beautifulsoup4==4.8.0 +beautifulsoup4==4.8.1 # homeassistant.components.beewi_smartclim beewi_smartclim==0.0.7 From 13956d35164491d59c1c7fa0dbdb369649ba4fcc Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 8 Oct 2019 18:30:18 +0200 Subject: [PATCH 0680/3953] Upgrade sqlalchemy to 1.3.9 (#27322) --- 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 cdb09d66067170..d349560e3852bc 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/integrations/recorder", "requirements": [ - "sqlalchemy==1.3.8" + "sqlalchemy==1.3.9" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 41d80ebccf9a03..5a87c813bc5f3a 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/integrations/sql", "requirements": [ - "sqlalchemy==1.3.8" + "sqlalchemy==1.3.9" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6bd21ac3849e89..ae4fd687b94496 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.8 +sqlalchemy==1.3.9 voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index f6993cc72c55ad..100e6f55fe73c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1823,7 +1823,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.8 +sqlalchemy==1.3.9 # homeassistant.components.starlingbank starlingbank==3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 425170d168dc16..c2ac5bb2631394 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.8 +sqlalchemy==1.3.9 # homeassistant.components.statsd statsd==3.2.1 From 15c54f34dfebcb45430781d173253398728312e2 Mon Sep 17 00:00:00 2001 From: Evan Bruhn Date: Wed, 9 Oct 2019 03:31:52 +1100 Subject: [PATCH 0681/3953] Fix Logi Circle cameras not responding to turn on/off commands (#27317) --- homeassistant/components/logi_circle/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 27b81d8331e4ae..ec8f1595168798 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -148,11 +148,11 @@ async def async_camera_image(self): async def async_turn_off(self): """Disable streaming mode for this camera.""" - await self._camera.set_streaming_mode(False) + await self._camera.set_config("streaming", False) async def async_turn_on(self): """Enable streaming mode for this camera.""" - await self._camera.set_streaming_mode(True) + await self._camera.set_config("streaming", True) @property def should_poll(self): From cf555428d099e3d79c34e6b499f310f9ff58962b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 8 Oct 2019 18:33:14 +0200 Subject: [PATCH 0682/3953] Updated frontend to 20191002.1 (#27329) --- 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 60a4f0faa9c16f..58e5558781a2ed 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191002.0" + "home-assistant-frontend==20191002.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ae4fd687b94496..99bb622e989b52 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.22 -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 100e6f55fe73c0..2327d4ce2d5dd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -643,7 +643,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2ac5bb2631394..3bbb834eb550a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -185,7 +185,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 0cfd0388d6f8b9625aa434f1d5cea5de4dba66ca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Oct 2019 18:57:24 +0200 Subject: [PATCH 0683/3953] Fix translations for binary_sensor triggers (#27330) --- .../components/binary_sensor/device_trigger.py | 16 ++++++++-------- .../components/binary_sensor/strings.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 5d58131fde9be7..c51b9749288bb5 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -81,8 +81,8 @@ CONF_NO_SOUND = "no_sound" CONF_VIBRATION = "vibration" CONF_NO_VIBRATION = "no_vibration" -CONF_OPEN = "open" -CONF_NOT_OPEN = "not_open" +CONF_OPENED = "opened" +CONF_NOT_OPENED = "not_opened" TURNED_ON = [ @@ -97,7 +97,7 @@ CONF_MOTION, CONF_MOVING, CONF_OCCUPIED, - CONF_OPEN, + CONF_OPENED, CONF_PLUGGED_IN, CONF_POWERED, CONF_PRESENT, @@ -118,7 +118,7 @@ CONF_NOT_MOIST, CONF_NOT_MOVING, CONF_NOT_OCCUPIED, - CONF_NOT_OPEN, + CONF_NOT_OPENED, CONF_NOT_PLUGGED_IN, CONF_NOT_POWERED, CONF_NOT_PRESENT, @@ -141,8 +141,8 @@ {CONF_TYPE: CONF_CONNECTED}, {CONF_TYPE: CONF_NOT_CONNECTED}, ], - DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], @@ -154,7 +154,7 @@ {CONF_TYPE: CONF_OCCUPIED}, {CONF_TYPE: CONF_NOT_OCCUPIED}, ], - DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], @@ -166,7 +166,7 @@ {CONF_TYPE: CONF_VIBRATION}, {CONF_TYPE: CONF_NO_VIBRATION}, ], - DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], } diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 109a2b1fd45f61..e01af8d183ecb0 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -59,7 +59,7 @@ "no_light": "{entity_name} stopped detecting light", "locked": "{entity_name} locked", "not_locked": "{entity_name} unlocked", - "moist§": "{entity_name} became moist", + "moist": "{entity_name} became moist", "not_moist": "{entity_name} became dry", "motion": "{entity_name} started detecting motion", "no_motion": "{entity_name} stopped detecting motion", @@ -84,7 +84,7 @@ "vibration": "{entity_name} started detecting vibration", "no_vibration": "{entity_name} stopped detecting vibration", "opened": "{entity_name} opened", - "closed": "{entity_name} closed", + "not_opened": "{entity_name} closed", "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" From a51e0d7a5f4e470d3f8e64ff0db1da2d440a5d72 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Oct 2019 09:58:36 -0700 Subject: [PATCH 0684/3953] Google: Report all states on activating report state (#27312) --- .../components/google_assistant/helpers.py | 5 +++ .../google_assistant/report_state.py | 24 +++++++++++++- .../google_assistant/test_report_state.py | 31 ++++++++++++++++--- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 207194d79ed0e1..933f0c07999884 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -182,6 +182,11 @@ def traits(self): ] return self._traits + @callback + def should_expose(self): + """If entity should be exposed.""" + return self.config.should_expose(self.state) + @callback def is_supported(self) -> bool: """Return if the entity is supported by Google.""" diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 33bb16d7830033..869bc61d7a3624 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,8 +1,13 @@ """Google Report State implementation.""" from homeassistant.core import HomeAssistant, callback from homeassistant.const import MATCH_ALL +from homeassistant.helpers.event import async_call_later -from .helpers import AbstractConfig, GoogleEntity +from .helpers import AbstractConfig, GoogleEntity, async_get_entities + +# Time to wait until the homegraph updates +# https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 +INITIAL_REPORT_DELAY = 60 @callback @@ -34,6 +39,23 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): {"devices": {"states": {changed_entity: entity_data}}} ) + async_call_later( + hass, INITIAL_REPORT_DELAY, _async_report_all_states(hass, google_config) + ) + return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener ) + + +async def _async_report_all_states(hass: HomeAssistant, google_config: AbstractConfig): + """Report all states.""" + entities = {} + + for entity in async_get_entities(hass, google_config): + if not entity.should_expose(): + continue + + entities[entity.entity_id] = entity.query_serialize() + + await google_config.async_report_state({"devices": {"states": entities}}) diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index bd59502a3a1b58..734d9ec7fc83af 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,17 +1,38 @@ """Test Google report state.""" from unittest.mock import patch -from homeassistant.components.google_assistant.report_state import ( - async_enable_report_state, -) +from homeassistant.components.google_assistant import report_state +from homeassistant.util.dt import utcnow + from . import BASIC_CONFIG -from tests.common import mock_coro + +from tests.common import mock_coro, async_fire_time_changed async def test_report_state(hass): """Test report state works.""" - unsub = async_enable_report_state(hass, BASIC_CONFIG) + hass.states.async_set("light.ceiling", "off") + hass.states.async_set("switch.ac", "on") + + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): + unsub = report_state.async_enable_report_state(hass, BASIC_CONFIG) + + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + # Test that enabling report state does a report on all entities + assert len(mock_report.mock_calls) == 1 + assert mock_report.mock_calls[0][1][0] == { + "devices": { + "states": { + "light.ceiling": {"on": False, "online": True}, + "switch.ac": {"on": True, "online": True}, + } + } + } with patch.object( BASIC_CONFIG, "async_report_state", side_effect=mock_coro From f5bd0f29b41bacdccddb49f05db9736580a09ce2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Oct 2019 09:59:32 -0700 Subject: [PATCH 0685/3953] Add scene.apply service (#27298) * Add scene.apply service * Use return value entity ID validator" * Require entities field in service call * Simplify scene service --- .../components/homeassistant/scene.py | 91 +++++++++++-------- homeassistant/components/scene/__init__.py | 17 +--- homeassistant/components/scene/services.yaml | 16 +++- tests/components/homeassistant/test_scene.py | 23 +++++ 4 files changed, 93 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 66b0410964009d..39b04f6d3eacd1 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -26,6 +26,36 @@ from homeassistant.helpers.state import HASS_DOMAIN, async_reproduce_state from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene + +def _convert_states(states): + """Convert state definitions to State objects.""" + result = {} + + for entity_id in states: + entity_id = cv.entity_id(entity_id) + + if isinstance(states[entity_id], dict): + entity_attrs = states[entity_id].copy() + state = entity_attrs.pop(ATTR_STATE, None) + attributes = entity_attrs + else: + state = states[entity_id] + attributes = {} + + # YAML translates 'on' to a boolean + # http://yaml.org/type/bool.html + if isinstance(state, bool): + state = STATE_ON if state else STATE_OFF + elif not isinstance(state, str): + raise vol.Invalid(f"State for {entity_id} should be a string") + + result[entity_id] = State(entity_id, state, attributes) + + return result + + +STATES_SCHEMA = vol.All(dict, _convert_states) + PLATFORM_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): HASS_DOMAIN, @@ -34,9 +64,7 @@ [ { vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ENTITIES): { - cv.entity_id: vol.Any(str, bool, dict) - }, + vol.Required(CONF_ENTITIES): STATES_SCHEMA, } ], ), @@ -44,6 +72,7 @@ extra=vol.ALLOW_EXTRA, ) +SERVICE_APPLY = "apply" SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) _LOGGER = logging.getLogger(__name__) @@ -87,6 +116,19 @@ async def reload_config(call): SCENE_DOMAIN, SERVICE_RELOAD, reload_config ) + async def apply_service(call): + """Apply a scene.""" + await async_reproduce_state( + hass, call.data[CONF_ENTITIES].values(), blocking=True, context=call.context + ) + + hass.services.async_register( + SCENE_DOMAIN, + SERVICE_APPLY, + apply_service, + vol.Schema({vol.Required(CONF_ENTITIES): STATES_SCHEMA}), + ) + def _process_scenes_config(hass, async_add_entities, config): """Process multiple scenes and add them.""" @@ -97,41 +139,11 @@ def _process_scenes_config(hass, async_add_entities, config): return async_add_entities( - HomeAssistantScene(hass, _process_scene_config(scene)) for scene in scene_config + HomeAssistantScene(hass, SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES])) + for scene in scene_config ) -def _process_scene_config(scene_config): - """Process passed in config into a format to work with. - - Async friendly. - """ - name = scene_config.get(CONF_NAME) - - states = {} - c_entities = dict(scene_config.get(CONF_ENTITIES, {})) - - for entity_id in c_entities: - if isinstance(c_entities[entity_id], dict): - entity_attrs = c_entities[entity_id].copy() - state = entity_attrs.pop(ATTR_STATE, None) - attributes = entity_attrs - else: - state = c_entities[entity_id] - attributes = {} - - # YAML translates 'on' to a boolean - # http://yaml.org/type/bool.html - if isinstance(state, bool): - state = STATE_ON if state else STATE_OFF - else: - state = str(state) - - states[entity_id.lower()] = State(entity_id, state, attributes) - - return SCENECONFIG(name, states) - - class HomeAssistantScene(Scene): """A scene is a group of entities and the states we want them to be.""" @@ -148,8 +160,13 @@ def name(self): @property def device_state_attributes(self): """Return the scene state attributes.""" - return {ATTR_ENTITY_ID: list(self.scene_config.states.keys())} + return {ATTR_ENTITY_ID: list(self.scene_config.states)} async def async_activate(self): """Activate scene. Try to get entities into requested state.""" - await async_reproduce_state(self.hass, self.scene_config.states.values(), True) + await async_reproduce_state( + self.hass, + self.scene_config.states.values(), + blocking=True, + context=self._context, + ) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 1f71a24c304688..ec2dc3118a911f 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -1,5 +1,4 @@ """Allow users to set and activate scenes.""" -import asyncio import importlib import logging @@ -7,7 +6,6 @@ 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 from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import HASS_DOMAIN @@ -69,20 +67,7 @@ async def async_setup(hass, config): HA_DOMAIN, {"platform": "homeasistant", STATES: []} ) - async def async_handle_scene_service(service): - """Handle calls to the switch services.""" - target_scenes = await component.async_extract_from_service(service) - - tasks = [scene.async_activate() for scene in target_scenes] - if tasks: - await asyncio.wait(tasks) - - hass.services.async_register( - DOMAIN, - SERVICE_TURN_ON, - async_handle_scene_service, - schema=ENTITY_SERVICE_SCHEMA, - ) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_activate") return True diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index ee255affe44ed7..0f1e7103aaf6fa 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -5,4 +5,18 @@ turn_on: fields: entity_id: description: Name(s) of scenes to turn on - example: 'scene.romantic' + example: "scene.romantic" + +reload: + description: Reload the scene configuration + +apply: + description: Activate a scene. Takes same data as the entities field from a single scene in the config. + fields: + entities: + description: The entities and the state that they need to be. + example: + light.kitchen: "on" + light.ceiling: + state: "on" + brightness: 80 diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 02c018a0b49b01..c7c3f2bc5d50f7 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -28,3 +28,26 @@ async def test_reload_config_service(hass): assert hass.states.get("scene.hallo") is None assert hass.states.get("scene.bye") is not None + + +async def test_apply_service(hass): + """Test the apply service.""" + assert await async_setup_component(hass, "scene", {}) + assert await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + + assert await hass.services.async_call( + "scene", "apply", {"entities": {"light.bed_light": "off"}}, blocking=True + ) + + assert hass.states.get("light.bed_light").state == "off" + + assert await hass.services.async_call( + "scene", + "apply", + {"entities": {"light.bed_light": {"state": "on", "brightness": 50}}}, + blocking=True, + ) + + state = hass.states.get("light.bed_light") + assert state.state == "on" + assert state.attributes["brightness"] == 50 From 1a9d07dbdc620674be96ce7ed77087602c538478 Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 8 Oct 2019 19:05:35 +0200 Subject: [PATCH 0686/3953] Improve Neato login process (#27327) * initial commit * Readded log message * Clean up try-except --- homeassistant/components/neato/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index c1fb128a1d1f3c..14090c99a5553f 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -145,28 +145,26 @@ def __init__(self, hass, domain_config, neato, vendor): def login(self): """Login to My Neato.""" + _LOGGER.debug("Trying to connect to Neato API") try: - _LOGGER.debug("Trying to connect to Neato API") self.my_neato = self._neato( self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor ) - self.logged_in = True - - _LOGGER.debug("Successfully connected to Neato API") - self._hass.data[NEATO_ROBOTS] = self.my_neato.robots - self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps - self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps except NeatoException as ex: if isinstance(ex, NeatoLoginException): _LOGGER.error("Invalid credentials") else: _LOGGER.error("Unable to connect to Neato API") self.logged_in = False + return + + self.logged_in = True + _LOGGER.debug("Successfully connected to Neato API") @Throttle(timedelta(minutes=SCAN_INTERVAL_MINUTES)) def update_robots(self): """Update the robot states.""" - _LOGGER.debug("Running HUB.update_robots %s", self._hass.data[NEATO_ROBOTS]) + _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS)) self._hass.data[NEATO_ROBOTS] = self.my_neato.robots self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps From 0ba4ee139838b48c76f23b032146f65218e4fd3a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Oct 2019 19:06:17 +0200 Subject: [PATCH 0687/3953] Validate generated device actions (#27262) * Validate generated actions * Use hass.services.async_call instead of service.async_call_from_config --- .../components/device_automation/toggle_entity.py | 12 +++++------- homeassistant/components/zha/device_action.py | 11 ++++------- .../device_action/tests/test_device_action.py | 5 +++-- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 29110144c14ccd..98a1af9c4cad06 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -17,6 +17,7 @@ CONF_TURNED_ON, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_CONDITION, CONF_ENTITY_ID, CONF_FOR, @@ -24,7 +25,7 @@ CONF_TYPE, ) from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers import condition, config_validation as cv, service +from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import TRIGGER_BASE_SCHEMA @@ -112,13 +113,10 @@ async def async_call_action_from_config( else: action = "toggle" - service_action = { - service.CONF_SERVICE: "{}.{}".format(domain, action), - CONF_ENTITY_ID: config[CONF_ENTITY_ID], - } + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} - await service.async_call_from_config( - hass, service_action, blocking=True, variables=variables, context=context + await hass.services.async_call( + domain, action, service_data, blocking=True, context=context ) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 460676a75a0045..60cfa0eec0004a 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -5,7 +5,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import config_validation as cv, service +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN @@ -78,13 +78,10 @@ async def _execute_service_based_action( service_name = SERVICE_NAMES[action_type] zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) - service_action = { - service.CONF_SERVICE: "{}.{}".format(DOMAIN, service_name), - ATTR_DATA: {ATTR_IEEE: str(zha_device.ieee)}, - } + service_data = {ATTR_IEEE: str(zha_device.ieee)} - await service.async_call_from_config( - hass, service_action, blocking=True, variables=variables, context=context + await hass.services.async_call( + DOMAIN, service_name, service_data, blocking=True, context=context ) diff --git a/script/scaffold/templates/device_action/tests/test_device_action.py b/script/scaffold/templates/device_action/tests/test_device_action.py index f8a00bf1ec8232..b65c8257531cd4 100644 --- a/script/scaffold/templates/device_action/tests/test_device_action.py +++ b/script/scaffold/templates/device_action/tests/test_device_action.py @@ -8,6 +8,7 @@ from tests.common import ( MockConfigEntry, + assert_lists_same, async_mock_service, mock_device_registry, mock_registry, @@ -28,7 +29,7 @@ def entity_reg(hass): async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a switch.""" + """Test we get the expected actions from a NEW_DOMAIN.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -51,7 +52,7 @@ async def test_get_actions(hass, device_reg, entity_reg): }, ] actions = await async_get_device_automations(hass, "action", device_entry.id) - assert actions == expected_actions + assert_lists_same(actions, expected_actions) async def test_action(hass): From 55e10d552e28f9c8414a2c432b95f482dec46899 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Tue, 8 Oct 2019 19:52:43 +0200 Subject: [PATCH 0688/3953] Cleanup handling of attributes for HomematicIP Cloud (#27331) * Cleanup handling of attributes for HomematicIP Cloud * Remove special climate handling --- .../components/homematicip_cloud/binary_sensor.py | 5 ++--- .../components/homematicip_cloud/device.py | 15 ++++++++++++++- .../components/homematicip_cloud/sensor.py | 10 +++++++--- .../components/homematicip_cloud/switch.py | 6 ++++-- .../components/homematicip_cloud/weather.py | 2 +- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index b3a21ba0b5c5f3..1114f10b622740 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -40,7 +40,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP, ATTR_MODEL_TYPE +from .device import ATTR_GROUP_MEMBER_UNREACHABLE _LOGGER = logging.getLogger(__name__) @@ -60,7 +60,6 @@ GROUP_ATTRIBUTES = { "lowBat": ATTR_LOW_BATTERY, - "modelType": ATTR_MODEL_TYPE, "moistureDetected": ATTR_MOISTURE_DETECTED, "motionDetected": ATTR_MOTION_DETECTED, "powerMainsFailure": ATTR_POWER_MAINS_FAILURE, @@ -353,7 +352,7 @@ def available(self) -> bool: @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" - state_attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: True} + state_attr = super().device_state_attributes for attr, attr_key in GROUP_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 1273278189d89f..0389e0b9935329 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -3,6 +3,7 @@ from typing import Optional from homematicip.aio.device import AsyncDevice +from homematicip.aio.group import AsyncGroup from homematicip.aio.home import AsyncHome from homeassistant.components import homematicip_cloud @@ -13,6 +14,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" +ATTR_GROUP_ID = "group_id" ATTR_ID = "id" ATTR_IS_GROUP = "is_group" # RSSI HAP -> Device @@ -35,15 +37,17 @@ 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, + "id": ATTR_ID, } +GROUP_ATTRIBUTES = {"modelType": ATTR_MODEL_TYPE, "id": ATTR_GROUP_ID} + class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" @@ -173,6 +177,7 @@ def icon(self) -> Optional[str]: def device_state_attributes(self): """Return the state attributes of the generic device.""" state_attr = {} + if isinstance(self._device, AsyncDevice): for attr, attr_key in DEVICE_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) @@ -181,4 +186,12 @@ def device_state_attributes(self): state_attr[ATTR_IS_GROUP] = False + if isinstance(self._device, AsyncGroup): + for attr, attr_key in GROUP_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value + + state_attr[ATTR_IS_GROUP] = True + return state_attr diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 30e910cc33a394..ceb7fc39fd7e3f 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -115,7 +115,6 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): def __init__(self, home: AsyncHome) -> None: """Initialize access point device.""" - home.modelType = "HmIP-HAP" super().__init__(home, home) @property @@ -152,7 +151,12 @@ def unit_of_measurement(self) -> str: @property def device_state_attributes(self): """Return the state attributes of the access point.""" - return {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: False} + state_attr = super().device_state_attributes + + state_attr[ATTR_MODEL_TYPE] = "HmIP-HAP" + state_attr[ATTR_IS_GROUP] = False + + return state_attr class HomematicipHeatingThermostat(HomematicipGenericDevice): @@ -316,7 +320,7 @@ def device_state_attributes(self): state_attr = super().device_state_attributes wind_direction = getattr(self._device, "windDirection", None) - if wind_direction: + if wind_direction is not None: state_attr[ATTR_WIND_DIRECTION] = _get_wind_direction(wind_direction) wind_direction_variation = getattr(self._device, "windDirectionVariation", None) diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index ababf793f0ce6e..7994aa446b899d 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP +from .device import ATTR_GROUP_MEMBER_UNREACHABLE _LOGGER = logging.getLogger(__name__) @@ -113,9 +113,11 @@ def available(self) -> bool: @property def device_state_attributes(self): """Return the state attributes of the switch-group.""" - state_attr = {ATTR_IS_GROUP: True} + state_attr = super().device_state_attributes + if self._device.unreach: state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True + return state_attr async def async_turn_on(self, **kwargs): diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index ed9098559a344c..0d020312fe914a 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -123,7 +123,7 @@ class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity): def __init__(self, home: AsyncHome) -> None: """Initialize the home weather.""" - home.weather.modelType = "HmIP-Home-Weather" + home.modelType = "HmIP-Home-Weather" super().__init__(home, home) @property From 071476343c10cdf8fe4a448082021eca3df7c8d7 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Tue, 8 Oct 2019 11:14:52 -0700 Subject: [PATCH 0689/3953] Fix connection issues with withings API by switching to a maintained codebase (#27310) * Fixing connection issues with withings API by switching to a maintained client codebase. * Updating requirements files. * Adding withings api to requirements script. --- homeassistant/components/withings/common.py | 14 +- .../components/withings/config_flow.py | 4 +- .../components/withings/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- script/gen_requirements_all.py | 2 +- tests/components/withings/common.py | 12 +- tests/components/withings/conftest.py | 139 +++++++++--------- tests/components/withings/test_common.py | 20 +-- tests/components/withings/test_sensor.py | 44 +++--- 10 files changed, 131 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index f2be849cbc794b..9acca6f0cd68e6 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import re import time -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import TokenUpdated @@ -68,7 +68,9 @@ class WithingsDataManager: service_available = None - def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): + def __init__( + self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi + ): """Constructor.""" self._hass = hass self._api = api @@ -253,7 +255,7 @@ def create_withings_data_manager( """Set up the sensor config entry.""" entry_creds = entry.data.get(const.CREDENTIALS) or {} profile = entry.data[const.PROFILE] - credentials = nokia.NokiaCredentials( + credentials = withings.WithingsCredentials( entry_creds.get("access_token"), entry_creds.get("token_expiry"), entry_creds.get("token_type"), @@ -266,7 +268,7 @@ def create_withings_data_manager( def credentials_saver(credentials_param): _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - # Sanitizing the data as sometimes a NokiaCredentials object + # Sanitizing the data as sometimes a WithingsCredentials object # is passed through from the API. cred_data = credentials_param if not isinstance(credentials_param, dict): @@ -275,8 +277,8 @@ def credentials_saver(credentials_param): entry.data[const.CREDENTIALS] = cred_data hass.config_entries.async_update_entry(entry, data={**entry.data}) - _LOGGER.debug("Creating nokia api instance") - api = nokia.NokiaApi( + _LOGGER.debug("Creating withings api instance") + api = withings.WithingsApi( credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index f28a4f59d80195..c781e785f5e122 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ from typing import Optional import aiohttp -import nokia +import withings_api as withings import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -75,7 +75,7 @@ def get_auth_client(self, profile: str): profile, ) - return nokia.NokiaAuth( + return withings.WithingsAuth( client_id, client_secret, callback_uri, diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index d38b69f2248bcc..ae5cd4bcdd9ee9 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "nokia==1.2.0" + "withings-api==2.0.0b7" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 2327d4ce2d5dd0..7d4ebb0074e6be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,9 +868,6 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 @@ -1976,6 +1973,9 @@ websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 +# homeassistant.components.withings +withings-api==2.0.0b7 + # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bbb834eb550a1..048601b3b99635 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,9 +231,6 @@ minio==4.0.9 # homeassistant.components.ssdp netdisco==2.6.0 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow @@ -457,6 +454,9 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==1.1.6 +# homeassistant.components.withings +withings-api==2.0.0b7 + # homeassistant.components.zeroconf zeroconf==0.23.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 61174e86a439ff..e8837b8d295a94 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -106,7 +106,6 @@ "mficlient", "minio", "netdisco", - "nokia", "numpy", "oauth2client", "paho-mqtt", @@ -185,6 +184,7 @@ "vultr", "wakeonlan", "warrant", + "withings-api", "YesssSMS", "zeroconf", "zigpy-homeassistant", diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b8406c39711e57..f3839a1be5524c 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,7 +1,7 @@ """Common data for for the withings component tests.""" import time -import nokia +import withings_api as withings import homeassistant.components.withings.const as const @@ -92,7 +92,7 @@ def new_measure(type_str, value, unit): } -def nokia_sleep_response(states): +def withings_sleep_response(states): """Create a sleep response based on states.""" data = [] for state in states: @@ -104,10 +104,10 @@ def nokia_sleep_response(states): ) ) - return nokia.NokiaSleep(new_sleep_data("aa", data)) + return withings.WithingsSleep(new_sleep_data("aa", data)) -NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( +WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( { "updatetime": "", "timezone": "", @@ -174,7 +174,7 @@ def nokia_sleep_response(states): ) -NOKIA_SLEEP_RESPONSE = nokia_sleep_response( +WITHINGS_SLEEP_RESPONSE = withings_sleep_response( [ const.MEASURE_TYPE_SLEEP_STATE_AWAKE, const.MEASURE_TYPE_SLEEP_STATE_LIGHT, @@ -183,7 +183,7 @@ def nokia_sleep_response(states): ] ) -NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( +WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( { "series": [ new_sleep_summary( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 7cbe3dc1cd4cd0..0aa6af0d7c098a 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ from typing import Awaitable, Callable, List import asynctest -import nokia +import withings_api as withings import pytest import homeassistant.components.api as api @@ -15,9 +15,9 @@ from homeassistant.setup import async_setup_component from .common import ( - NOKIA_MEASURES_RESPONSE, - NOKIA_SLEEP_RESPONSE, - NOKIA_SLEEP_SUMMARY_RESPONSE, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_SLEEP_RESPONSE, + WITHINGS_SLEEP_SUMMARY_RESPONSE, ) @@ -34,17 +34,17 @@ def __init__( measures: List[str] = None, unit_system: str = None, throttle_interval: int = const.THROTTLE_INTERVAL, - nokia_request_response="DATA", - nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, - nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, - nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, + withings_request_response="DATA", + withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, + withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, + withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, ) -> None: """Constructor.""" self._throttle_interval = throttle_interval - self._nokia_request_response = nokia_request_response - self._nokia_measures_response = nokia_measures_response - self._nokia_sleep_response = nokia_sleep_response - self._nokia_sleep_summary_response = nokia_sleep_summary_response + self._withings_request_response = withings_request_response + self._withings_measures_response = withings_measures_response + self._withings_sleep_response = withings_sleep_response + self._withings_sleep_summary_response = withings_sleep_summary_response self._withings_config = { const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret", @@ -103,24 +103,24 @@ def throttle_interval(self): return self._throttle_interval @property - def nokia_request_response(self): + def withings_request_response(self): """Request response.""" - return self._nokia_request_response + return self._withings_request_response @property - def nokia_measures_response(self) -> nokia.NokiaMeasures: + def withings_measures_response(self) -> withings.WithingsMeasures: """Measures response.""" - return self._nokia_measures_response + return self._withings_measures_response @property - def nokia_sleep_response(self) -> nokia.NokiaSleep: + def withings_sleep_response(self) -> withings.WithingsSleep: """Sleep response.""" - return self._nokia_sleep_response + return self._withings_sleep_response @property - def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: + def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: """Sleep summary response.""" - return self._nokia_sleep_summary_response + return self._withings_sleep_summary_response class WithingsFactoryData: @@ -130,21 +130,21 @@ def __init__( self, hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ): """Constructor.""" self._hass = hass self._flow_id = flow_id - self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock - self._nokia_api_request_mock = nokia_api_request_mock - self._nokia_api_get_measures_mock = nokia_api_get_measures_mock - self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock - self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock + self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock + self._withings_api_request_mock = withings_api_request_mock + self._withings_api_get_measures_mock = withings_api_get_measures_mock + self._withings_api_get_sleep_mock = withings_api_get_sleep_mock + self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock self._data_manager_get_throttle_interval_mock = ( data_manager_get_throttle_interval_mock ) @@ -160,29 +160,29 @@ def flow_id(self): return self._flow_id @property - def nokia_auth_get_credentials_mock(self): + def withings_auth_get_credentials_mock(self): """Get auth credentials mock.""" - return self._nokia_auth_get_credentials_mock + return self._withings_auth_get_credentials_mock @property - def nokia_api_request_mock(self): + def withings_api_request_mock(self): """Get request mock.""" - return self._nokia_api_request_mock + return self._withings_api_request_mock @property - def nokia_api_get_measures_mock(self): + def withings_api_get_measures_mock(self): """Get measures mock.""" - return self._nokia_api_get_measures_mock + return self._withings_api_get_measures_mock @property - def nokia_api_get_sleep_mock(self): + def withings_api_get_sleep_mock(self): """Get sleep mock.""" - return self._nokia_api_get_sleep_mock + return self._withings_api_get_sleep_mock @property - def nokia_api_get_sleep_summary_mock(self): + def withings_api_get_sleep_summary_mock(self): """Get sleep summary mock.""" - return self._nokia_api_get_sleep_summary_mock + return self._withings_api_get_sleep_summary_mock @property def data_manager_get_throttle_interval_mock(self): @@ -243,9 +243,9 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: assert await async_setup_component(hass, http.DOMAIN, config.hass_config) assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - nokia_auth_get_credentials_patch = asynctest.patch( - "nokia.NokiaAuth.get_credentials", - return_value=nokia.NokiaCredentials( + withings_auth_get_credentials_patch = asynctest.patch( + "withings_api.WithingsAuth.get_credentials", + return_value=withings.WithingsCredentials( access_token="my_access_token", token_expiry=time.time() + 600, token_type="my_token_type", @@ -255,28 +255,33 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: consumer_secret=config.withings_config.get(const.CLIENT_SECRET), ), ) - nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() + withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() - nokia_api_request_patch = asynctest.patch( - "nokia.NokiaApi.request", return_value=config.nokia_request_response + withings_api_request_patch = asynctest.patch( + "withings_api.WithingsApi.request", + return_value=config.withings_request_response, ) - nokia_api_request_mock = nokia_api_request_patch.start() + withings_api_request_mock = withings_api_request_patch.start() - nokia_api_get_measures_patch = asynctest.patch( - "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response + withings_api_get_measures_patch = asynctest.patch( + "withings_api.WithingsApi.get_measures", + return_value=config.withings_measures_response, ) - nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() + withings_api_get_measures_mock = withings_api_get_measures_patch.start() - nokia_api_get_sleep_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response + withings_api_get_sleep_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep", + return_value=config.withings_sleep_response, ) - nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() + withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() - nokia_api_get_sleep_summary_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep_summary", - return_value=config.nokia_sleep_summary_response, + withings_api_get_sleep_summary_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep_summary", + return_value=config.withings_sleep_summary_response, + ) + withings_api_get_sleep_summary_mock = ( + withings_api_get_sleep_summary_patch.start() ) - nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() data_manager_get_throttle_interval_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" @@ -295,11 +300,11 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: patches.extend( [ - nokia_auth_get_credentials_patch, - nokia_api_request_patch, - nokia_api_get_measures_patch, - nokia_api_get_sleep_patch, - nokia_api_get_sleep_summary_patch, + withings_auth_get_credentials_patch, + withings_api_request_patch, + withings_api_get_measures_patch, + withings_api_get_sleep_patch, + withings_api_get_sleep_summary_patch, data_manager_get_throttle_interval_patch, get_measures_patch, ] @@ -328,11 +333,11 @@ def create_task(*args): return WithingsFactoryData( hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index a22689f92bb6b5..9f2480f9094e55 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,6 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError import pytest from requests_oauthlib import TokenUpdated @@ -13,19 +13,19 @@ from homeassistant.exceptions import PlatformNotReady -@pytest.fixture(name="nokia_api") -def nokia_api_fixture(): - """Provide nokia api.""" - nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) - nokia_api.get_measures = MagicMock() - nokia_api.get_sleep = MagicMock() - return nokia_api +@pytest.fixture(name="withings_api") +def withings_api_fixture(): + """Provide withings api.""" + withings_api = withings.WithingsApi.__new__(withings.WithingsApi) + withings_api.get_measures = MagicMock() + withings_api.get_sleep = MagicMock() + return withings_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): +def data_manager_fixture(hass, withings_api: withings.WithingsApi): """Provide data manager.""" - return WithingsDataManager(hass, "My Profile", nokia_api) + return WithingsDataManager(hass, "My Profile", withings_api) def test_print_service(): diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index da77910097be89..697d0a8b86413f 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,7 +2,12 @@ from unittest.mock import MagicMock, patch import asynctest -from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary +from withings_api import ( + WithingsApi, + WithingsMeasures, + WithingsSleep, + WithingsSleepSummary, +) import pytest from homeassistant.components.withings import DOMAIN @@ -15,7 +20,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from .common import nokia_sleep_response +from .common import withings_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig @@ -120,9 +125,9 @@ async def test_health_sensor_state_none( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=None, - nokia_sleep_response=None, - nokia_sleep_summary_response=None, + withings_measures_response=None, + withings_sleep_response=None, + withings_sleep_summary_response=None, ) ) @@ -153,9 +158,9 @@ async def test_health_sensor_state_empty( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=NokiaMeasures({"measuregrps": []}), - nokia_sleep_response=NokiaSleep({"series": []}), - nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), + withings_measures_response=WithingsMeasures({"measuregrps": []}), + withings_sleep_response=WithingsSleep({"series": []}), + withings_sleep_summary_response=WithingsSleepSummary({"series": []}), ) ) @@ -201,7 +206,8 @@ async def test_sleep_state_throttled( data = await withings_factory( WithingsFactoryConfig( - measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) + measures=[measure], + withings_sleep_response=withings_sleep_response(sleep_states), ) ) @@ -257,16 +263,16 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): "expires_in": "2", } - original_nokia_api = NokiaApi - nokia_api_instance = None + original_withings_api = WithingsApi + withings_api_instance = None - def new_nokia_api(*args, **kwargs): - nonlocal nokia_api_instance - nokia_api_instance = original_nokia_api(*args, **kwargs) - nokia_api_instance.request = MagicMock() - return nokia_api_instance + def new_withings_api(*args, **kwargs): + nonlocal withings_api_instance + withings_api_instance = original_withings_api(*args, **kwargs) + withings_api_instance.request = MagicMock() + return withings_api_instance - nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) + withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( @@ -275,7 +281,7 @@ def new_nokia_api(*args, **kwargs): wraps=hass.config_entries.async_update_entry, ) - with session_patch, client_patch, nokia_api_patch, update_entry_patch: + with session_patch, client_patch, withings_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( @@ -298,7 +304,7 @@ def new_nokia_api(*args, **kwargs): await async_setup_entry(hass, config_entry, async_add_entities) - nokia_api_instance.set_token(expected_creds) + withings_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2" From c214d7a972d46cd960080cdf94e0e83d20946612 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Oct 2019 09:58:36 -0700 Subject: [PATCH 0690/3953] Google: Report all states on activating report state (#27312) --- .../components/google_assistant/helpers.py | 5 +++ .../google_assistant/report_state.py | 24 +++++++++++++- .../google_assistant/test_report_state.py | 31 ++++++++++++++++--- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 207194d79ed0e1..933f0c07999884 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -182,6 +182,11 @@ def traits(self): ] return self._traits + @callback + def should_expose(self): + """If entity should be exposed.""" + return self.config.should_expose(self.state) + @callback def is_supported(self) -> bool: """Return if the entity is supported by Google.""" diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 33bb16d7830033..869bc61d7a3624 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,8 +1,13 @@ """Google Report State implementation.""" from homeassistant.core import HomeAssistant, callback from homeassistant.const import MATCH_ALL +from homeassistant.helpers.event import async_call_later -from .helpers import AbstractConfig, GoogleEntity +from .helpers import AbstractConfig, GoogleEntity, async_get_entities + +# Time to wait until the homegraph updates +# https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 +INITIAL_REPORT_DELAY = 60 @callback @@ -34,6 +39,23 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): {"devices": {"states": {changed_entity: entity_data}}} ) + async_call_later( + hass, INITIAL_REPORT_DELAY, _async_report_all_states(hass, google_config) + ) + return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener ) + + +async def _async_report_all_states(hass: HomeAssistant, google_config: AbstractConfig): + """Report all states.""" + entities = {} + + for entity in async_get_entities(hass, google_config): + if not entity.should_expose(): + continue + + entities[entity.entity_id] = entity.query_serialize() + + await google_config.async_report_state({"devices": {"states": entities}}) diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index bd59502a3a1b58..734d9ec7fc83af 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,17 +1,38 @@ """Test Google report state.""" from unittest.mock import patch -from homeassistant.components.google_assistant.report_state import ( - async_enable_report_state, -) +from homeassistant.components.google_assistant import report_state +from homeassistant.util.dt import utcnow + from . import BASIC_CONFIG -from tests.common import mock_coro + +from tests.common import mock_coro, async_fire_time_changed async def test_report_state(hass): """Test report state works.""" - unsub = async_enable_report_state(hass, BASIC_CONFIG) + hass.states.async_set("light.ceiling", "off") + hass.states.async_set("switch.ac", "on") + + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): + unsub = report_state.async_enable_report_state(hass, BASIC_CONFIG) + + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + # Test that enabling report state does a report on all entities + assert len(mock_report.mock_calls) == 1 + assert mock_report.mock_calls[0][1][0] == { + "devices": { + "states": { + "light.ceiling": {"on": False, "online": True}, + "switch.ac": {"on": True, "online": True}, + } + } + } with patch.object( BASIC_CONFIG, "async_report_state", side_effect=mock_coro From 07b1976f7d40388467e2a549f043eda443734d45 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 8 Oct 2019 10:54:01 -0500 Subject: [PATCH 0691/3953] Fix single Plex server case (#27326) --- homeassistant/components/plex/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index df9e9f9f6c3528..6ab114307664b3 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -57,7 +57,7 @@ def _set_missing_url(): raise ServerNotSpecified(available_servers) server_choice = ( - self._server_name if self._server_name else available_servers[0] + self._server_name if self._server_name else available_servers[0][0] ) connections = account.resource(server_choice).connections local_url = [x.httpuri for x in connections if x.local] From 579c91da1b16d0b43d07062508ee2d82ab791ba2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 8 Oct 2019 18:33:14 +0200 Subject: [PATCH 0692/3953] Updated frontend to 20191002.1 (#27329) --- 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 60a4f0faa9c16f..58e5558781a2ed 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191002.0" + "home-assistant-frontend==20191002.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a64e0dc38e7b75..04a68bf9633d4f 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.22 -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index be88d6c60cb309..7aa5090688c092 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -640,7 +640,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3cacfa88887fa..69c6ded580688f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -182,7 +182,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 58f444c779b5e8edad4b00f12eb3efd89841695e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Oct 2019 18:57:24 +0200 Subject: [PATCH 0693/3953] Fix translations for binary_sensor triggers (#27330) --- .../components/binary_sensor/device_trigger.py | 16 ++++++++-------- .../components/binary_sensor/strings.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 89fd9add69a940..f138bcfd5a813d 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -81,8 +81,8 @@ CONF_NO_SOUND = "no_sound" CONF_VIBRATION = "vibration" CONF_NO_VIBRATION = "no_vibration" -CONF_OPEN = "open" -CONF_NOT_OPEN = "not_open" +CONF_OPENED = "opened" +CONF_NOT_OPENED = "not_opened" TURNED_ON = [ @@ -97,7 +97,7 @@ CONF_MOTION, CONF_MOVING, CONF_OCCUPIED, - CONF_OPEN, + CONF_OPENED, CONF_PLUGGED_IN, CONF_POWERED, CONF_PRESENT, @@ -118,7 +118,7 @@ CONF_NOT_MOIST, CONF_NOT_MOVING, CONF_NOT_OCCUPIED, - CONF_NOT_OPEN, + CONF_NOT_OPENED, CONF_NOT_PLUGGED_IN, CONF_NOT_POWERED, CONF_NOT_PRESENT, @@ -141,8 +141,8 @@ {CONF_TYPE: CONF_CONNECTED}, {CONF_TYPE: CONF_NOT_CONNECTED}, ], - DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], @@ -154,7 +154,7 @@ {CONF_TYPE: CONF_OCCUPIED}, {CONF_TYPE: CONF_NOT_OCCUPIED}, ], - DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], @@ -166,7 +166,7 @@ {CONF_TYPE: CONF_VIBRATION}, {CONF_TYPE: CONF_NO_VIBRATION}, ], - DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], } diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 109a2b1fd45f61..e01af8d183ecb0 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -59,7 +59,7 @@ "no_light": "{entity_name} stopped detecting light", "locked": "{entity_name} locked", "not_locked": "{entity_name} unlocked", - "moist§": "{entity_name} became moist", + "moist": "{entity_name} became moist", "not_moist": "{entity_name} became dry", "motion": "{entity_name} started detecting motion", "no_motion": "{entity_name} stopped detecting motion", @@ -84,7 +84,7 @@ "vibration": "{entity_name} started detecting vibration", "no_vibration": "{entity_name} stopped detecting vibration", "opened": "{entity_name} opened", - "closed": "{entity_name} closed", + "not_opened": "{entity_name} closed", "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" From d4436951c5feab6d77f49e0e9f40e7e88170d333 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Oct 2019 11:19:39 -0700 Subject: [PATCH 0694/3953] Update translations --- .../components/.translations/airly.ca.json | 22 ++++++++ .../components/.translations/airly.da.json | 22 ++++++++ .../components/.translations/airly.de.json | 18 +++++++ .../components/.translations/airly.en.json | 22 ++++++++ .../components/.translations/airly.es.json | 22 ++++++++ .../components/.translations/airly.fr.json | 21 ++++++++ .../components/.translations/airly.it.json | 22 ++++++++ .../components/.translations/airly.lb.json | 22 ++++++++ .../components/.translations/airly.nn.json | 10 ++++ .../components/.translations/airly.no.json | 22 ++++++++ .../components/.translations/airly.pl.json | 22 ++++++++ .../components/.translations/airly.ru.json | 22 ++++++++ .../components/.translations/airly.sl.json | 22 ++++++++ .../.translations/airly.zh-Hant.json | 22 ++++++++ .../components/adguard/.translations/nn.json | 11 ++++ .../ambient_station/.translations/ru.json | 2 +- .../arcam_fmj/.translations/nn.json | 5 ++ .../components/axis/.translations/nn.json | 3 +- .../components/axis/.translations/ru.json | 4 +- .../binary_sensor/.translations/en.json | 2 + .../binary_sensor/.translations/fr.json | 54 +++++++++++++++++++ .../cert_expiry/.translations/ru.json | 4 +- .../components/daikin/.translations/nn.json | 5 ++ .../components/daikin/.translations/ru.json | 2 +- .../components/deconz/.translations/de.json | 10 ++++ .../components/deconz/.translations/es.json | 1 + .../components/deconz/.translations/fr.json | 1 + .../components/deconz/.translations/it.json | 1 + .../components/deconz/.translations/lb.json | 1 + .../components/deconz/.translations/no.json | 17 +++--- .../components/deconz/.translations/sl.json | 1 + .../deconz/.translations/zh-Hant.json | 1 + .../dialogflow/.translations/nn.json | 5 ++ .../components/ecobee/.translations/de.json | 11 ++++ .../components/ecobee/.translations/fr.json | 24 +++++++++ .../components/ecobee/.translations/nn.json | 5 ++ .../emulated_roku/.translations/nn.json | 5 ++ .../emulated_roku/.translations/ru.json | 2 +- .../components/esphome/.translations/nn.json | 7 ++- .../components/esphome/.translations/ru.json | 2 +- .../geonetnz_quakes/.translations/nn.json | 12 +++++ .../geonetnz_quakes/.translations/ru.json | 2 +- .../components/hangouts/.translations/ru.json | 2 +- .../components/heos/.translations/de.json | 2 +- .../homematicip_cloud/.translations/ru.json | 2 +- .../components/hue/.translations/ru.json | 4 +- .../iaqualink/.translations/nn.json | 5 ++ .../components/ipma/.translations/nn.json | 11 ++++ .../components/ipma/.translations/ru.json | 2 +- .../components/iqvia/.translations/nn.json | 10 ++++ .../components/iqvia/.translations/ru.json | 2 +- .../components/izone/.translations/nn.json | 10 ++++ .../components/life360/.translations/nn.json | 12 +++++ .../components/life360/.translations/ru.json | 4 +- .../components/lifx/.translations/nn.json | 10 ++++ .../components/light/.translations/de.json | 9 ++++ .../components/linky/.translations/nn.json | 10 ++++ .../components/linky/.translations/ru.json | 4 +- .../luftdaten/.translations/ru.json | 2 +- .../components/mailgun/.translations/nn.json | 5 ++ .../components/met/.translations/de.json | 2 +- .../components/met/.translations/nn.json | 11 ++++ .../components/met/.translations/pl.json | 2 +- .../components/met/.translations/ru.json | 2 +- .../components/neato/.translations/ca.json | 27 ++++++++++ .../components/neato/.translations/da.json | 23 ++++++++ .../components/neato/.translations/de.json | 27 ++++++++++ .../components/neato/.translations/en.json | 27 ++++++++++ .../components/neato/.translations/es.json | 27 ++++++++++ .../components/neato/.translations/fr.json | 27 ++++++++++ .../components/neato/.translations/it.json | 27 ++++++++++ .../components/neato/.translations/lb.json | 27 ++++++++++ .../components/neato/.translations/nn.json | 12 +++++ .../components/neato/.translations/no.json | 27 ++++++++++ .../components/neato/.translations/pl.json | 27 ++++++++++ .../components/neato/.translations/ru.json | 27 ++++++++++ .../components/neato/.translations/sl.json | 27 ++++++++++ .../neato/.translations/zh-Hant.json | 27 ++++++++++ .../components/notion/.translations/ru.json | 2 +- .../opentherm_gw/.translations/ca.json | 23 ++++++++ .../opentherm_gw/.translations/da.json | 20 +++++++ .../opentherm_gw/.translations/de.json | 19 +++++++ .../opentherm_gw/.translations/en.json | 23 ++++++++ .../opentherm_gw/.translations/es.json | 23 ++++++++ .../opentherm_gw/.translations/fr.json | 22 ++++++++ .../opentherm_gw/.translations/it.json | 23 ++++++++ .../opentherm_gw/.translations/lb.json | 23 ++++++++ .../opentherm_gw/.translations/nl.json | 14 +++++ .../opentherm_gw/.translations/nn.json | 12 +++++ .../opentherm_gw/.translations/no.json | 23 ++++++++ .../opentherm_gw/.translations/pl.json | 12 +++++ .../opentherm_gw/.translations/ru.json | 23 ++++++++ .../opentherm_gw/.translations/sl.json | 23 ++++++++ .../opentherm_gw/.translations/zh-Hant.json | 23 ++++++++ .../components/openuv/.translations/ru.json | 2 +- .../owntracks/.translations/nn.json | 5 ++ .../components/plaato/.translations/nn.json | 5 ++ .../components/plex/.translations/ca.json | 4 ++ .../components/plex/.translations/de.json | 26 +++++++++ .../components/plex/.translations/en.json | 5 ++ .../components/plex/.translations/es.json | 6 +++ .../components/plex/.translations/fr.json | 23 +++++++- .../components/plex/.translations/it.json | 8 ++- .../components/plex/.translations/lb.json | 6 +++ .../components/plex/.translations/nn.json | 5 ++ .../components/plex/.translations/no.json | 5 ++ .../components/plex/.translations/pl.json | 1 + .../components/plex/.translations/ru.json | 13 +++-- .../components/plex/.translations/sl.json | 6 +++ .../plex/.translations/zh-Hant.json | 8 ++- .../components/point/.translations/nn.json | 5 ++ .../components/ps4/.translations/nn.json | 13 ++++- .../rainmachine/.translations/pl.json | 2 +- .../rainmachine/.translations/ru.json | 2 +- .../components/sensor/.translations/ca.json | 24 +++++++++ .../components/sensor/.translations/da.json | 26 +++++++++ .../components/sensor/.translations/de.json | 21 ++++++++ .../components/sensor/.translations/en.json | 26 +++++++++ .../components/sensor/.translations/es.json | 26 +++++++++ .../components/sensor/.translations/fr.json | 26 +++++++++ .../components/sensor/.translations/it.json | 26 +++++++++ .../components/sensor/.translations/lb.json | 26 +++++++++ .../components/sensor/.translations/no.json | 26 +++++++++ .../components/sensor/.translations/pl.json | 9 ++++ .../components/sensor/.translations/sl.json | 26 +++++++++ .../sensor/.translations/zh-Hant.json | 26 +++++++++ .../simplisafe/.translations/nn.json | 5 ++ .../simplisafe/.translations/ru.json | 2 +- .../components/smhi/.translations/ru.json | 2 +- .../solaredge/.translations/ru.json | 4 +- .../components/soma/.translations/de.json | 9 ++++ .../components/soma/.translations/es.json | 13 +++++ .../components/soma/.translations/fr.json | 13 +++++ .../components/soma/.translations/lb.json | 13 +++++ .../components/soma/.translations/nn.json | 5 ++ .../components/soma/.translations/pl.json | 13 +++++ .../soma/.translations/zh-Hant.json | 13 +++++ .../components/somfy/.translations/nn.json | 5 ++ .../tellduslive/.translations/nn.json | 5 ++ .../tellduslive/.translations/ru.json | 2 +- .../components/toon/.translations/nn.json | 7 +++ .../components/tplink/.translations/nn.json | 10 ++++ .../components/traccar/.translations/nn.json | 5 ++ .../transmission/.translations/de.json | 37 +++++++++++++ .../transmission/.translations/en.json | 2 +- .../transmission/.translations/fr.json | 40 ++++++++++++++ .../transmission/.translations/nn.json | 12 +++++ .../transmission/.translations/ru.json | 2 +- .../twentemilieu/.translations/nn.json | 10 ++++ .../components/twilio/.translations/nn.json | 5 ++ .../components/unifi/.translations/ca.json | 5 ++ .../components/unifi/.translations/da.json | 5 ++ .../components/unifi/.translations/de.json | 5 ++ .../components/unifi/.translations/en.json | 5 ++ .../components/unifi/.translations/es.json | 5 ++ .../components/unifi/.translations/fr.json | 5 ++ .../components/unifi/.translations/it.json | 5 ++ .../components/unifi/.translations/lb.json | 5 ++ .../components/unifi/.translations/nn.json | 11 ++++ .../components/unifi/.translations/no.json | 5 ++ .../components/unifi/.translations/pl.json | 5 ++ .../components/unifi/.translations/ru.json | 7 ++- .../components/unifi/.translations/sl.json | 5 ++ .../unifi/.translations/zh-Hant.json | 5 ++ .../components/upnp/.translations/nn.json | 3 +- .../components/upnp/.translations/ru.json | 2 +- .../components/vesync/.translations/nn.json | 5 ++ .../components/wemo/.translations/nn.json | 10 ++++ .../components/withings/.translations/nn.json | 5 ++ .../components/wwlln/.translations/ru.json | 2 +- .../components/zha/.translations/da.json | 3 ++ .../components/zha/.translations/de.json | 8 +++ .../components/zha/.translations/fr.json | 27 ++++++++++ .../components/zha/.translations/nn.json | 10 ++++ .../components/zha/.translations/no.json | 12 ++--- .../components/zone/.translations/ru.json | 2 +- .../components/zwave/.translations/nn.json | 3 +- .../components/zwave/.translations/ru.json | 2 +- 178 files changed, 2058 insertions(+), 67 deletions(-) create mode 100644 homeassistant/components/.translations/airly.ca.json create mode 100644 homeassistant/components/.translations/airly.da.json create mode 100644 homeassistant/components/.translations/airly.de.json create mode 100644 homeassistant/components/.translations/airly.en.json create mode 100644 homeassistant/components/.translations/airly.es.json create mode 100644 homeassistant/components/.translations/airly.fr.json create mode 100644 homeassistant/components/.translations/airly.it.json create mode 100644 homeassistant/components/.translations/airly.lb.json create mode 100644 homeassistant/components/.translations/airly.nn.json create mode 100644 homeassistant/components/.translations/airly.no.json create mode 100644 homeassistant/components/.translations/airly.pl.json create mode 100644 homeassistant/components/.translations/airly.ru.json create mode 100644 homeassistant/components/.translations/airly.sl.json create mode 100644 homeassistant/components/.translations/airly.zh-Hant.json create mode 100644 homeassistant/components/adguard/.translations/nn.json create mode 100644 homeassistant/components/arcam_fmj/.translations/nn.json create mode 100644 homeassistant/components/binary_sensor/.translations/fr.json create mode 100644 homeassistant/components/daikin/.translations/nn.json create mode 100644 homeassistant/components/dialogflow/.translations/nn.json create mode 100644 homeassistant/components/ecobee/.translations/de.json create mode 100644 homeassistant/components/ecobee/.translations/fr.json create mode 100644 homeassistant/components/ecobee/.translations/nn.json create mode 100644 homeassistant/components/emulated_roku/.translations/nn.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/nn.json create mode 100644 homeassistant/components/iaqualink/.translations/nn.json create mode 100644 homeassistant/components/ipma/.translations/nn.json create mode 100644 homeassistant/components/iqvia/.translations/nn.json create mode 100644 homeassistant/components/izone/.translations/nn.json create mode 100644 homeassistant/components/life360/.translations/nn.json create mode 100644 homeassistant/components/lifx/.translations/nn.json create mode 100644 homeassistant/components/linky/.translations/nn.json create mode 100644 homeassistant/components/mailgun/.translations/nn.json create mode 100644 homeassistant/components/met/.translations/nn.json create mode 100644 homeassistant/components/neato/.translations/ca.json create mode 100644 homeassistant/components/neato/.translations/da.json create mode 100644 homeassistant/components/neato/.translations/de.json create mode 100644 homeassistant/components/neato/.translations/en.json create mode 100644 homeassistant/components/neato/.translations/es.json create mode 100644 homeassistant/components/neato/.translations/fr.json create mode 100644 homeassistant/components/neato/.translations/it.json create mode 100644 homeassistant/components/neato/.translations/lb.json create mode 100644 homeassistant/components/neato/.translations/nn.json create mode 100644 homeassistant/components/neato/.translations/no.json create mode 100644 homeassistant/components/neato/.translations/pl.json create mode 100644 homeassistant/components/neato/.translations/ru.json create mode 100644 homeassistant/components/neato/.translations/sl.json create mode 100644 homeassistant/components/neato/.translations/zh-Hant.json create mode 100644 homeassistant/components/opentherm_gw/.translations/ca.json create mode 100644 homeassistant/components/opentherm_gw/.translations/da.json create mode 100644 homeassistant/components/opentherm_gw/.translations/de.json create mode 100644 homeassistant/components/opentherm_gw/.translations/en.json create mode 100644 homeassistant/components/opentherm_gw/.translations/es.json create mode 100644 homeassistant/components/opentherm_gw/.translations/fr.json create mode 100644 homeassistant/components/opentherm_gw/.translations/it.json create mode 100644 homeassistant/components/opentherm_gw/.translations/lb.json create mode 100644 homeassistant/components/opentherm_gw/.translations/nl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/nn.json create mode 100644 homeassistant/components/opentherm_gw/.translations/no.json create mode 100644 homeassistant/components/opentherm_gw/.translations/pl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/ru.json create mode 100644 homeassistant/components/opentherm_gw/.translations/sl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/zh-Hant.json create mode 100644 homeassistant/components/owntracks/.translations/nn.json create mode 100644 homeassistant/components/plaato/.translations/nn.json create mode 100644 homeassistant/components/plex/.translations/de.json create mode 100644 homeassistant/components/plex/.translations/nn.json create mode 100644 homeassistant/components/point/.translations/nn.json create mode 100644 homeassistant/components/sensor/.translations/ca.json create mode 100644 homeassistant/components/sensor/.translations/da.json create mode 100644 homeassistant/components/sensor/.translations/de.json create mode 100644 homeassistant/components/sensor/.translations/en.json create mode 100644 homeassistant/components/sensor/.translations/es.json create mode 100644 homeassistant/components/sensor/.translations/fr.json create mode 100644 homeassistant/components/sensor/.translations/it.json create mode 100644 homeassistant/components/sensor/.translations/lb.json create mode 100644 homeassistant/components/sensor/.translations/no.json create mode 100644 homeassistant/components/sensor/.translations/pl.json create mode 100644 homeassistant/components/sensor/.translations/sl.json create mode 100644 homeassistant/components/sensor/.translations/zh-Hant.json create mode 100644 homeassistant/components/simplisafe/.translations/nn.json create mode 100644 homeassistant/components/soma/.translations/de.json create mode 100644 homeassistant/components/soma/.translations/es.json create mode 100644 homeassistant/components/soma/.translations/fr.json create mode 100644 homeassistant/components/soma/.translations/lb.json create mode 100644 homeassistant/components/soma/.translations/nn.json create mode 100644 homeassistant/components/soma/.translations/pl.json create mode 100644 homeassistant/components/soma/.translations/zh-Hant.json create mode 100644 homeassistant/components/somfy/.translations/nn.json create mode 100644 homeassistant/components/tellduslive/.translations/nn.json create mode 100644 homeassistant/components/tplink/.translations/nn.json create mode 100644 homeassistant/components/traccar/.translations/nn.json create mode 100644 homeassistant/components/transmission/.translations/de.json create mode 100644 homeassistant/components/transmission/.translations/fr.json create mode 100644 homeassistant/components/transmission/.translations/nn.json create mode 100644 homeassistant/components/twentemilieu/.translations/nn.json create mode 100644 homeassistant/components/twilio/.translations/nn.json create mode 100644 homeassistant/components/unifi/.translations/nn.json create mode 100644 homeassistant/components/vesync/.translations/nn.json create mode 100644 homeassistant/components/wemo/.translations/nn.json create mode 100644 homeassistant/components/withings/.translations/nn.json create mode 100644 homeassistant/components/zha/.translations/nn.json diff --git a/homeassistant/components/.translations/airly.ca.json b/homeassistant/components/.translations/airly.ca.json new file mode 100644 index 00000000000000..bf50b4f23e55b5 --- /dev/null +++ b/homeassistant/components/.translations/airly.ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clau API no \u00e9s correcta.", + "name_exists": "El nom ja existeix.", + "wrong_location": "No hi ha estacions de mesura Airly en aquesta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clau API d'Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom de la integraci\u00f3" + }, + "description": "Configura una integraci\u00f3 de qualitat d\u2019aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.da.json b/homeassistant/components/.translations/airly.da.json new file mode 100644 index 00000000000000..652cc46a7b3e0a --- /dev/null +++ b/homeassistant/components/.translations/airly.da.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-n\u00f8glen er ikke korrekt.", + "name_exists": "Navnet findes allerede.", + "wrong_location": "Ingen Airly m\u00e5lestationer i dette omr\u00e5de." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-n\u00f8gle", + "latitude": "Breddegrad", + "longitude": "L\u00e6ngdegrad", + "name": "Integrationens navn" + }, + "description": "Konfigurer Airly luftkvalitet integration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.de.json b/homeassistant/components/.translations/airly.de.json new file mode 100644 index 00000000000000..cb290dc46c087e --- /dev/null +++ b/homeassistant/components/.translations/airly.de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "Name existiert bereits" + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name der Integration" + }, + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.en.json b/homeassistant/components/.translations/airly.en.json new file mode 100644 index 00000000000000..83284aaeb7b6e0 --- /dev/null +++ b/homeassistant/components/.translations/airly.en.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API key is not correct.", + "name_exists": "Name already exists.", + "wrong_location": "No Airly measuring stations in this area." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name of the integration" + }, + "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.es.json b/homeassistant/components/.translations/airly.es.json new file mode 100644 index 00000000000000..0c29ad0bc668b9 --- /dev/null +++ b/homeassistant/components/.translations/airly.es.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clave de la API no es correcta.", + "name_exists": "El nombre ya existe.", + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API de Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre de la integraci\u00f3n" + }, + "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.fr.json b/homeassistant/components/.translations/airly.fr.json new file mode 100644 index 00000000000000..cf756a9f4928b9 --- /dev/null +++ b/homeassistant/components/.translations/airly.fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "auth": "La cl\u00e9 API n'est pas correcte.", + "name_exists": "Le nom existe d\u00e9j\u00e0.", + "wrong_location": "Aucune station de mesure Airly dans cette zone." + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 API Airly", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom de l'int\u00e9gration" + }, + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.it.json b/homeassistant/components/.translations/airly.it.json new file mode 100644 index 00000000000000..e50f618575bfd6 --- /dev/null +++ b/homeassistant/components/.translations/airly.it.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La chiave API non \u00e8 corretta.", + "name_exists": "Il nome \u00e8 gi\u00e0 esistente", + "wrong_location": "Nessuna stazione di misurazione Airly in quest'area." + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API Airly", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome dell'integrazione" + }, + "description": "Configurazione dell'integrazione della qualit\u00e0 dell'aria Airly. Per generare la chiave API andare su https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.lb.json b/homeassistant/components/.translations/airly.lb.json new file mode 100644 index 00000000000000..08aac57d162ae9 --- /dev/null +++ b/homeassistant/components/.translations/airly.lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Api Schl\u00ebssel ass net korrekt.", + "name_exists": "Numm g\u00ebtt et schonn", + "wrong_location": "Keng Airly Moos Statioun an d\u00ebsem Ber\u00e4ich" + }, + "step": { + "user": { + "data": { + "api_key": "Airly API Schl\u00ebssel", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm vun der Installatioun" + }, + "description": "Airly Loft Qualit\u00e9it Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle gitt op https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.nn.json b/homeassistant/components/.translations/airly.nn.json new file mode 100644 index 00000000000000..7e2f4f1ff6bcef --- /dev/null +++ b/homeassistant/components/.translations/airly.nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.no.json b/homeassistant/components/.translations/airly.no.json new file mode 100644 index 00000000000000..70924bb7bf4b9e --- /dev/null +++ b/homeassistant/components/.translations/airly.no.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-n\u00f8kkelen er ikke korrekt.", + "name_exists": "Navnet finnes allerede.", + "wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn p\u00e5 integrasjonen" + }, + "description": "Sett opp Airly luftkvalitet integrering. For \u00e5 generere API-n\u00f8kkel g\u00e5 til https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.pl.json b/homeassistant/components/.translations/airly.pl.json new file mode 100644 index 00000000000000..5d601b37591b68 --- /dev/null +++ b/homeassistant/components/.translations/airly.pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Klucz API jest nieprawid\u0142owy.", + "name_exists": "Nazwa ju\u017c istnieje.", + "wrong_location": "Brak stacji pomiarowych Airly w tym rejonie." + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API Airly", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa integracji" + }, + "description": "Konfiguracja integracji Airly. By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.ru.json b/homeassistant/components/.translations/airly.ru.json new file mode 100644 index 00000000000000..36080c9f372e24 --- /dev/null +++ b/homeassistant/components/.translations/airly.ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", + "wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 Airly. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register.", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.sl.json b/homeassistant/components/.translations/airly.sl.json new file mode 100644 index 00000000000000..08f57d88bcba98 --- /dev/null +++ b/homeassistant/components/.translations/airly.sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Klju\u010d API ni pravilen.", + "name_exists": "Ime \u017ee obstaja", + "wrong_location": "Na tem obmo\u010dju ni merilnih postaj Airly." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API klju\u010d", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime integracije" + }, + "description": "Nastavite Airly integracijo za kakovost zraka. \u010ce \u017eelite ustvariti API klju\u010d pojdite na https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.zh-Hant.json b/homeassistant/components/.translations/airly.zh-Hant.json new file mode 100644 index 00000000000000..bb38d2b9b8c739 --- /dev/null +++ b/homeassistant/components/.translations/airly.zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002", + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", + "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" + }, + "step": { + "user": { + "data": { + "api_key": "Airly API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u6574\u5408\u540d\u7a31" + }, + "description": "\u6b32\u8a2d\u5b9a Airly \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://developer.airly.eu/register \u7522\u751f API \u5bc6\u9470", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/nn.json b/homeassistant/components/adguard/.translations/nn.json new file mode 100644 index 00000000000000..7c129cba3afc71 --- /dev/null +++ b/homeassistant/components/adguard/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index d1264010b75c1c..2d7964f18ebe61 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b" }, diff --git a/homeassistant/components/arcam_fmj/.translations/nn.json b/homeassistant/components/arcam_fmj/.translations/nn.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/nn.json b/homeassistant/components/axis/.translations/nn.json index 3364446935953a..b6296d1acab703 100644 --- a/homeassistant/components/axis/.translations/nn.json +++ b/homeassistant/components/axis/.translations/nn.json @@ -5,7 +5,8 @@ "data": { "host": "Vert", "password": "Passord", - "port": "Port" + "port": "Port", + "username": "Brukarnamn" } } } diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 67d720aa85f06a..951263d53f9668 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis" }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" diff --git a/homeassistant/components/binary_sensor/.translations/en.json b/homeassistant/components/binary_sensor/.translations/en.json index 6379df936b898d..93b61893980eb6 100644 --- a/homeassistant/components/binary_sensor/.translations/en.json +++ b/homeassistant/components/binary_sensor/.translations/en.json @@ -53,6 +53,7 @@ "hot": "{entity_name} became hot", "light": "{entity_name} started detecting light", "locked": "{entity_name} locked", + "moist": "{entity_name} became moist", "moist\u00a7": "{entity_name} became moist", "motion": "{entity_name} started detecting motion", "moving": "{entity_name} started moving", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} became dry", "not_moving": "{entity_name} stopped moving", "not_occupied": "{entity_name} became not occupied", + "not_opened": "{entity_name} closed", "not_plugged_in": "{entity_name} unplugged", "not_powered": "{entity_name} not powered", "not_present": "{entity_name} not present", diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json new file mode 100644 index 00000000000000..80792f166354ec --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -0,0 +1,54 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batterie faible", + "is_cold": "{entity_name} est froid", + "is_connected": "{entity_name} est connect\u00e9", + "is_gas": "{entity_name} d\u00e9tecte du gaz", + "is_hot": "{entity_name} est chaud", + "is_light": "{entity_name} d\u00e9tecte de la lumi\u00e8re", + "is_locked": "{entity_name} est verrouill\u00e9", + "is_moist": "{entity_name} est humide", + "is_motion": "{entity_name} d\u00e9tecte un mouvement", + "is_moving": "{entity_name} se d\u00e9place", + "is_no_gas": "{entity_name} ne d\u00e9tecte pas de gaz", + "is_no_light": "{entity_name} ne d\u00e9tecte pas de lumi\u00e8re", + "is_no_motion": "{entity_name} ne d\u00e9tecte pas de mouvement", + "is_no_problem": "{entity_name} ne d\u00e9tecte pas de probl\u00e8me", + "is_no_smoke": "{entity_name} ne d\u00e9tecte pas de fum\u00e9e", + "is_no_sound": "{entity_name} ne d\u00e9tecte pas de son", + "is_no_vibration": "{entity_name} ne d\u00e9tecte pas de vibration", + "is_not_bat_low": "{entity_name} batterie normale", + "is_not_cold": "{entity_name} n'est pas froid", + "is_not_connected": "{entity_name} est d\u00e9connect\u00e9", + "is_not_hot": "{entity_name} n'est pas chaud", + "is_not_locked": "{entity_name} est d\u00e9verrouill\u00e9", + "is_not_moist": "{entity_name} est sec", + "is_not_moving": "{entity_name} ne bouge pas", + "is_not_occupied": "{entity_name} n'est pas occup\u00e9", + "is_not_open": "{entity_name} est ferm\u00e9", + "is_not_plugged_in": "{entity_name} est d\u00e9branch\u00e9", + "is_not_powered": "{entity_name} n'est pas aliment\u00e9", + "is_not_present": "{entity_name} n'est pas pr\u00e9sent", + "is_not_unsafe": "{entity_name} est en s\u00e9curit\u00e9", + "is_occupied": "{entity_name} est occup\u00e9", + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9", + "is_open": "{entity_name} est ouvert", + "is_plugged_in": "{entity_name} est branch\u00e9", + "is_powered": "{entity_name} est aliment\u00e9", + "is_present": "{entity_name} est pr\u00e9sent", + "is_problem": "{entity_name} d\u00e9tecte un probl\u00e8me", + "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", + "is_sound": "{entity_name} d\u00e9tecte du son" + }, + "trigger_type": { + "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", + "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", + "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} activ\u00e9", + "unsafe": "{entity_name} est devenu dangereux", + "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json index 6a795dee13e26b..d962c7931218c7 100644 --- a/homeassistant/components/cert_expiry/.translations/ru.json +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430" + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." }, "error": { "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430", "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443", - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442" }, "step": { diff --git a/homeassistant/components/daikin/.translations/nn.json b/homeassistant/components/daikin/.translations/nn.json new file mode 100644 index 00000000000000..67d4f85262572e --- /dev/null +++ b/homeassistant/components/daikin/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index ce1f1ab3caa974..98ab98e6b170d2 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 97e25e28965c5f..830ae0fd13f2f1 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -41,6 +41,16 @@ }, "title": "deCONZ Zigbee Gateway" }, + "device_automation": { + "trigger_subtype": { + "close": "Schlie\u00dfen", + "left": "Links", + "open": "Offen", + "right": "Rechts", + "turn_off": "Ausschalten", + "turn_on": "Einschalten" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index cb5db0b8348ea4..04a08d185b30fb 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", + "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtipo}\" detenido", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index cc6d22945dcb79..3729f7f556afb9 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Bouton \"{subtype}\" quadruple cliqu\u00e9", "remote_button_quintuple_press": "Bouton \"{subtype}\" quintuple cliqu\u00e9", "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9", + "remote_button_rotation_stopped": "La rotation du bouton \" {subtype} \" s'est arr\u00eat\u00e9e", "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 7a2b8832864e2b..1f0b344a32d6ba 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", "remote_button_rotated": "Pulsante ruotato \"{subtype}\"", + "remote_button_rotation_stopped": "La rotazione dei pulsanti \"{subtype}\" si \u00e8 arrestata", "remote_button_short_press": "Pulsante \"{subtype}\" premuto", "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 840bc8929a7366..f5f41a28a32c95 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt", "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt", "remote_button_rotated": "Kn\u00e4ppche gedr\u00e9int \"{subtype}\"", + "remote_button_rotation_stopped": "Kn\u00e4ppchen Rotatioun \"{subtype}\" gestoppt", "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index c7079fd62193e7..7d05a366cf2248 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -58,15 +58,16 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { - "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", - "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", - "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\" {subtype} \" - knappen femdobbelt klikket", - "remote_button_rotated": "Knappen roterte \" {subtype} \"", - "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", + "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt klikket", + "remote_button_rotated": "Knappen roterte \"{subtype}\"", + "remote_button_rotation_stopped": "Knappe rotasjon \"{subtype}\" stoppet", + "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\" {subtype} \"-knappen trippel klikket", + "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", "remote_gyro_activated": "Enhet er ristet" } }, diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 9aebb2a556f6cd..0717bcfc39f455 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", "remote_button_rotated": "Gumb \"{subtype}\" zasukan", + "remote_button_rotation_stopped": "Vrtenje \"{subtype}\" gumba se je ustavilo", "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index bd47a637761ccd..2ad613cde6868f 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u9ede\u64ca", "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u9ede\u64ca", "remote_button_rotated": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215", + "remote_button_rotation_stopped": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215\u5df2\u505c\u6b62", "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", diff --git a/homeassistant/components/dialogflow/.translations/nn.json b/homeassistant/components/dialogflow/.translations/nn.json new file mode 100644 index 00000000000000..5a96b853eb0944 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/de.json b/homeassistant/components/ecobee/.translations/de.json new file mode 100644 index 00000000000000..1959f769d3a4c9 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/de.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/fr.json b/homeassistant/components/ecobee/.translations/fr.json new file mode 100644 index 00000000000000..85da5b3a4ecc41 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "one_instance_only": "Cette int\u00e9gration ne prend actuellement en charge qu'une seule instance ecobee." + }, + "error": { + "pin_request_failed": "Erreur lors de la demande du code PIN \u00e0 ecobee; veuillez v\u00e9rifier que la cl\u00e9 API est correcte.", + "token_request_failed": "Erreur lors de la demande de jetons \u00e0 ecobee; Veuillez r\u00e9essayer." + }, + "step": { + "authorize": { + "title": "Autoriser l'application sur ecobee.com" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 API" + }, + "description": "Veuillez entrer la cl\u00e9 API obtenue aupr\u00e8s d'ecobee.com.", + "title": "Cl\u00e9 API ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/nn.json b/homeassistant/components/ecobee/.translations/nn.json new file mode 100644 index 00000000000000..301239cf31a6c9 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/nn.json b/homeassistant/components/emulated_roku/.translations/nn.json new file mode 100644 index 00000000000000..fc349a0d9de9df --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/ru.json b/homeassistant/components/emulated_roku/.translations/ru.json index c7b85c195929d3..32bf473ac38546 100644 --- a/homeassistant/components/emulated_roku/.translations/ru.json +++ b/homeassistant/components/emulated_roku/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/esphome/.translations/nn.json b/homeassistant/components/esphome/.translations/nn.json index 830391f58f6e30..5e40c8ec5e5635 100644 --- a/homeassistant/components/esphome/.translations/nn.json +++ b/homeassistant/components/esphome/.translations/nn.json @@ -1,9 +1,14 @@ { "config": { + "flow_title": "ESPHome: {name}", "step": { "discovery_confirm": { "title": "Fann ESPhome node" + }, + "user": { + "title": "ESPHome" } - } + }, + "title": "ESPHome" } } \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index 1405112c07022f..62d24662ab6a77 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430" + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", diff --git a/homeassistant/components/geonetnz_quakes/.translations/nn.json b/homeassistant/components/geonetnz_quakes/.translations/nn.json new file mode 100644 index 00000000000000..d8afb1e7aaead1 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json index 7d6583bc1d5b59..d6763d17e2d0a8 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ru.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 52b8798c0f4084..6942f683fa6e4a 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" }, "error": { diff --git a/homeassistant/components/heos/.translations/de.json b/homeassistant/components/heos/.translations/de.json index e8f4df930dbe99..e98df7466ff69c 100644 --- a/homeassistant/components/heos/.translations/de.json +++ b/homeassistant/components/heos/.translations/de.json @@ -16,6 +16,6 @@ "title": "Mit Heos verbinden" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 82ecd4a32504f2..5155a42c4c3b55 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP", "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index be5d2b7159d40b..79a46e1861bce6 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", diff --git a/homeassistant/components/iaqualink/.translations/nn.json b/homeassistant/components/iaqualink/.translations/nn.json new file mode 100644 index 00000000000000..ea78f0d0d5dd0c --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/nn.json b/homeassistant/components/ipma/.translations/nn.json new file mode 100644 index 00000000000000..0e024a0e1eb7b0 --- /dev/null +++ b/homeassistant/components/ipma/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json index a260efa5bd9dc2..a302572ed121d5 100644 --- a/homeassistant/components/ipma/.translations/ru.json +++ b/homeassistant/components/ipma/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/iqvia/.translations/nn.json b/homeassistant/components/iqvia/.translations/nn.json new file mode 100644 index 00000000000000..89922b66f03dca --- /dev/null +++ b/homeassistant/components/iqvia/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/ru.json b/homeassistant/components/iqvia/.translations/ru.json index 06a5b7e69ddde5..0c3afc88c94dd0 100644 --- a/homeassistant/components/iqvia/.translations/ru.json +++ b/homeassistant/components/iqvia/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" }, "step": { diff --git a/homeassistant/components/izone/.translations/nn.json b/homeassistant/components/izone/.translations/nn.json new file mode 100644 index 00000000000000..eaf7601be9c3d8 --- /dev/null +++ b/homeassistant/components/izone/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/nn.json b/homeassistant/components/life360/.translations/nn.json new file mode 100644 index 00000000000000..98345b022f2e1b --- /dev/null +++ b/homeassistant/components/life360/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + }, + "title": "Life360" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index 1e962142373f89..d033da4bae71eb 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "create_entry": { "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." @@ -11,7 +11,7 @@ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d", "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "step": { "user": { diff --git a/homeassistant/components/lifx/.translations/nn.json b/homeassistant/components/lifx/.translations/nn.json new file mode 100644 index 00000000000000..c78905b09c8f95 --- /dev/null +++ b/homeassistant/components/lifx/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json index e07adeb0a36e84..be8966d95569af 100644 --- a/homeassistant/components/light/.translations/de.json +++ b/homeassistant/components/light/.translations/de.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Schalte {entity_name} um.", + "turn_off": "Schalte {entity_name} aus.", + "turn_on": "Schalte {entity_name} ein." + }, + "condition_type": { + "is_off": "{entity_name} ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet" + }, "trigger_type": { "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet" diff --git a/homeassistant/components/linky/.translations/nn.json b/homeassistant/components/linky/.translations/nn.json new file mode 100644 index 00000000000000..6e084d1a9d28fc --- /dev/null +++ b/homeassistant/components/linky/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index 498b5b2f12f29b..b569cce9239066 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443", "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430", + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c" }, "step": { diff --git a/homeassistant/components/luftdaten/.translations/ru.json b/homeassistant/components/luftdaten/.translations/ru.json index d37aa3567d1977..7ae83b550e3d44 100644 --- a/homeassistant/components/luftdaten/.translations/ru.json +++ b/homeassistant/components/luftdaten/.translations/ru.json @@ -3,7 +3,7 @@ "error": { "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten", "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d", - "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/.translations/nn.json b/homeassistant/components/mailgun/.translations/nn.json new file mode 100644 index 00000000000000..2bab2e430016cf --- /dev/null +++ b/homeassistant/components/mailgun/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/de.json b/homeassistant/components/met/.translations/de.json index b70d3f12a838ce..2fd772c8619bdd 100644 --- a/homeassistant/components/met/.translations/de.json +++ b/homeassistant/components/met/.translations/de.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Name existiert bereits" + "name_exists": "Ort existiert bereits" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/nn.json b/homeassistant/components/met/.translations/nn.json new file mode 100644 index 00000000000000..0e024a0e1eb7b0 --- /dev/null +++ b/homeassistant/components/met/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/pl.json b/homeassistant/components/met/.translations/pl.json index d44142213bf066..f647dcf7b45365 100644 --- a/homeassistant/components/met/.translations/pl.json +++ b/homeassistant/components/met/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nazwa ju\u017c istnieje" + "name_exists": "Lokalizacja ju\u017c istnieje" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json index d92d28d948419d..559382cf209d67 100644 --- a/homeassistant/components/met/.translations/ru.json +++ b/homeassistant/components/met/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/ca.json b/homeassistant/components/neato/.translations/ca.json new file mode 100644 index 00000000000000..d30f9e5ad4bd7b --- /dev/null +++ b/homeassistant/components/neato/.translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ja configurat", + "invalid_credentials": "Credencials inv\u00e0lides" + }, + "create_entry": { + "default": "Consulta la [documentaci\u00f3 de Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credencials inv\u00e0lides", + "unexpected_error": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari", + "vendor": "Venedor" + }, + "description": "Consulta la [documentaci\u00f3 de Neato]({docs_url}).", + "title": "Informaci\u00f3 del compte Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json new file mode 100644 index 00000000000000..7f0d122f38b2a7 --- /dev/null +++ b/homeassistant/components/neato/.translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Allerede konfigureret", + "invalid_credentials": "Ugyldige legitimationsoplysninger" + }, + "error": { + "invalid_credentials": "Ugyldige legitimationsoplysninger", + "unexpected_error": "Uventet fejl" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "description": "Se [Neato-dokumentation] ({docs_url}).", + "title": "Neato kontooplysninger" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/de.json b/homeassistant/components/neato/.translations/de.json new file mode 100644 index 00000000000000..2a75d11a9ec0c4 --- /dev/null +++ b/homeassistant/components/neato/.translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Bereits konfiguriert", + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + }, + "create_entry": { + "default": "Siehe [Neato-Dokumentation]({docs_url})." + }, + "error": { + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen", + "unexpected_error": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername", + "vendor": "Hersteller" + }, + "description": "Siehe [Neato-Dokumentation]({docs_url}).", + "title": "Neato-Kontoinformationen" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/en.json b/homeassistant/components/neato/.translations/en.json new file mode 100644 index 00000000000000..73628c8646e849 --- /dev/null +++ b/homeassistant/components/neato/.translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Already configured", + "invalid_credentials": "Invalid credentials" + }, + "create_entry": { + "default": "See [Neato documentation]({docs_url})." + }, + "error": { + "invalid_credentials": "Invalid credentials", + "unexpected_error": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username", + "vendor": "Vendor" + }, + "description": "See [Neato documentation]({docs_url}).", + "title": "Neato Account Info" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/es.json b/homeassistant/components/neato/.translations/es.json new file mode 100644 index 00000000000000..99e7574e4b2743 --- /dev/null +++ b/homeassistant/components/neato/.translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ya est\u00e1 configurado", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "create_entry": { + "default": "Ver [documentaci\u00f3n Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unexpected_error": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario", + "vendor": "Vendedor" + }, + "description": "Ver [documentaci\u00f3n Neato]({docs_url}).", + "title": "Informaci\u00f3n de la cuenta de Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/fr.json b/homeassistant/components/neato/.translations/fr.json new file mode 100644 index 00000000000000..941ed18660efff --- /dev/null +++ b/homeassistant/components/neato/.translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00e9j\u00e0 configur\u00e9", + "invalid_credentials": "Informations d'identification invalides" + }, + "create_entry": { + "default": "Voir [Documentation Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Informations d'identification invalides", + "unexpected_error": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur", + "vendor": "Vendeur" + }, + "description": "Voir [Documentation Neato] ( {docs_url} ).", + "title": "Informations compte Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/it.json b/homeassistant/components/neato/.translations/it.json new file mode 100644 index 00000000000000..d5615815dc769b --- /dev/null +++ b/homeassistant/components/neato/.translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Gi\u00e0 configurato", + "invalid_credentials": "Credenziali non valide" + }, + "create_entry": { + "default": "Vedere la [Documentazione di Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credenziali non valide", + "unexpected_error": "Errore inaspettato" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente", + "vendor": "Fornitore" + }, + "description": "Vedere la [Documentazione di Neato]({docs_url}).", + "title": "Informazioni sull'account Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/lb.json b/homeassistant/components/neato/.translations/lb.json new file mode 100644 index 00000000000000..3043ec6ec3754c --- /dev/null +++ b/homeassistant/components/neato/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Scho konfigur\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune" + }, + "create_entry": { + "default": "Kuckt [Neato Dokumentatioun]({docs_url})." + }, + "error": { + "invalid_credentials": "Ong\u00eblteg Login Informatioune", + "unexpected_error": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm", + "vendor": "Hiersteller" + }, + "description": "Kuckt [Neato Dokumentatioun]({docs_url}).", + "title": "Neato Kont Informatiounen" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/nn.json b/homeassistant/components/neato/.translations/nn.json new file mode 100644 index 00000000000000..e04e73aef24ea3 --- /dev/null +++ b/homeassistant/components/neato/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/no.json b/homeassistant/components/neato/.translations/no.json new file mode 100644 index 00000000000000..084c4b50e457ed --- /dev/null +++ b/homeassistant/components/neato/.translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Allerede konfigurert", + "invalid_credentials": "Ugyldig brukerinformasjon" + }, + "create_entry": { + "default": "Se [Neato dokumentasjon]({docs_url})." + }, + "error": { + "invalid_credentials": "Ugyldig brukerinformasjon", + "unexpected_error": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn", + "vendor": "Leverand\u00f8r" + }, + "description": "Se [Neato dokumentasjon]({docs_url}).", + "title": "Neato kontoinformasjon" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/pl.json b/homeassistant/components/neato/.translations/pl.json new file mode 100644 index 00000000000000..caea115b7d5101 --- /dev/null +++ b/homeassistant/components/neato/.translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" + }, + "create_entry": { + "default": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", + "unexpected_error": "Niespodziewany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika", + "vendor": "Dostawca" + }, + "description": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url}).", + "title": "Informacje o koncie Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json new file mode 100644 index 00000000000000..1a206258e24979 --- /dev/null +++ b/homeassistant/components/neato/.translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + }, + "create_entry": { + "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + }, + "error": { + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d", + "vendor": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c" + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "title": "Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/sl.json b/homeassistant/components/neato/.translations/sl.json new file mode 100644 index 00000000000000..7acbb718d17631 --- /dev/null +++ b/homeassistant/components/neato/.translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u017de konfigurirano", + "invalid_credentials": "Neveljavne poverilnice" + }, + "create_entry": { + "default": "Glejte [neato dokumentacija] ({docs_url})." + }, + "error": { + "invalid_credentials": "Neveljavne poverilnice", + "unexpected_error": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime", + "vendor": "Prodajalec" + }, + "description": "Glejte [neato dokumentacija] ({docs_url}).", + "title": "Podatki o ra\u010dunu Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/zh-Hant.json b/homeassistant/components/neato/.translations/zh-Hant.json new file mode 100644 index 00000000000000..61f49cd5da0010 --- /dev/null +++ b/homeassistant/components/neato/.translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "invalid_credentials": "\u6191\u8b49\u7121\u6548" + }, + "create_entry": { + "default": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002" + }, + "error": { + "invalid_credentials": "\u6191\u8b49\u7121\u6548", + "unexpected_error": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "vendor": "\u5ee0\u5546" + }, + "description": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002", + "title": "Neato \u5e33\u865f\u8cc7\u8a0a" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index c7e89c368c178c..7345cf462957c9 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c", "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e" }, diff --git a/homeassistant/components/opentherm_gw/.translations/ca.json b/homeassistant/components/opentherm_gw/.translations/ca.json new file mode 100644 index 00000000000000..0224d663a83953 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Passarel\u00b7la ja configurada", + "id_exists": "L'identificador de passarel\u00b7la ja existeix", + "serial_error": "S'ha produ\u00eft un error en connectar-se al dispositiu", + "timeout": "S'ha acabat el temps d'espera en l'intent de connexi\u00f3" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "floor_temperature": "Temperatura del pis", + "id": "ID", + "name": "Nom", + "precision": "Precisi\u00f3 de la temperatura" + }, + "title": "Passarel\u00b7la d'OpenTherm" + } + }, + "title": "Passarel\u00b7la d'OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json new file mode 100644 index 00000000000000..b8abb48af4e89c --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "already_configured": "Gateway allerede konfigureret", + "id_exists": "Gateway-id findes allerede", + "serial_error": "Fejl ved tilslutning til enheden" + }, + "step": { + "init": { + "data": { + "device": "Sti eller URL", + "id": "ID", + "name": "Navn" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json new file mode 100644 index 00000000000000..274dd46488b8d6 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Gateway bereits konfiguriert", + "id_exists": "Gateway-ID ist bereits vorhanden", + "serial_error": "Fehler beim Verbinden mit dem Ger\u00e4t", + "timeout": "Zeit\u00fcberschreitung beim Verbindungsversuch" + }, + "step": { + "init": { + "data": { + "device": "Pfad oder URL", + "id": "ID", + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json new file mode 100644 index 00000000000000..65d7d9e92bb1bd --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway already configured", + "id_exists": "Gateway id already exists", + "serial_error": "Error connecting to device", + "timeout": "Connection attempt timed out" + }, + "step": { + "init": { + "data": { + "device": "Path or URL", + "floor_temperature": "Floor climate temperature", + "id": "ID", + "name": "Name", + "precision": "Climate temperature precision" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/es.json b/homeassistant/components/opentherm_gw/.translations/es.json new file mode 100644 index 00000000000000..8ad9d89b07a57d --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ya configurado", + "id_exists": "El ID del Gateway ya existe", + "serial_error": "Error de conexi\u00f3n al dispositivo", + "timeout": "Intento de conexi\u00f3n agotado" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "floor_temperature": "Temperatura del suelo", + "id": "ID", + "name": "Nombre", + "precision": "Precisi\u00f3n de la temperatura clim\u00e1tica" + }, + "title": "Gateway OpenTherm" + } + }, + "title": "Gateway OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json new file mode 100644 index 00000000000000..82b9a7aee88167 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "already_configured": "Passerelle d\u00e9j\u00e0 configur\u00e9e", + "id_exists": "L'identifiant de la passerelle existe d\u00e9j\u00e0", + "serial_error": "Erreur de connexion \u00e0 l'appareil", + "timeout": "La tentative de connexion a expir\u00e9" + }, + "step": { + "init": { + "data": { + "device": "Chemin ou URL", + "id": "ID", + "name": "Nom", + "precision": "Pr\u00e9cision de la temp\u00e9rature climatique" + }, + "title": "Passerelle OpenTherm" + } + }, + "title": "Passerelle OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/it.json b/homeassistant/components/opentherm_gw/.translations/it.json new file mode 100644 index 00000000000000..9c62686e19083d --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway gi\u00e0 configurato", + "id_exists": "ID del gateway esiste gi\u00e0", + "serial_error": "Errore durante la connessione al dispositivo", + "timeout": "Tentativo di connessione scaduto" + }, + "step": { + "init": { + "data": { + "device": "Percorso o URL", + "floor_temperature": "Temperatura climatica del pavimento", + "id": "ID", + "name": "Nome", + "precision": "Precisione della temperatura climatica" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "Gateway OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/lb.json b/homeassistant/components/opentherm_gw/.translations/lb.json new file mode 100644 index 00000000000000..ec1f719a6cc3d5 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ass scho konfigur\u00e9iert", + "id_exists": "Gateway ID g\u00ebtt et schonn", + "serial_error": "Feeler beim verbannen", + "timeout": "Z\u00e4it Iwwerschreidung beim Verbindungs Versuch" + }, + "step": { + "init": { + "data": { + "device": "Pfad oder URL", + "floor_temperature": "Buedem Klima Temperatur", + "id": "ID", + "name": "Numm", + "precision": "Klima Temperatur Prezisioun" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json new file mode 100644 index 00000000000000..4fec1baba7badb --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "device": "Pad of URL", + "id": "ID" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nn.json b/homeassistant/components/opentherm_gw/.translations/nn.json new file mode 100644 index 00000000000000..3d018a2292d0c2 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "id": "ID", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json new file mode 100644 index 00000000000000..6104aa7de724cb --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway er allerede konfigurert", + "id_exists": "Gateway-ID finnes allerede", + "serial_error": "Feil ved tilkobling til enhet", + "timeout": "Tilkoblingsfors\u00f8k ble tidsavbrutt" + }, + "step": { + "init": { + "data": { + "device": "Bane eller URL-adresse", + "floor_temperature": "Gulv klimatemperatur", + "id": "ID", + "name": "Navn", + "precision": "Klima temperaturpresisjon" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json new file mode 100644 index 00000000000000..7e4a0eed013f3e --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "device": "\u015acie\u017cka lub adres URL", + "name": "Nazwa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json new file mode 100644 index 00000000000000..718322ec171088 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", + "serial_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "init": { + "data": { + "device": "\u041f\u0443\u0442\u044c \u0438\u043b\u0438 URL-\u0430\u0434\u0440\u0435\u0441", + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", + "id": "ID", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b" + }, + "title": "OpenTherm" + } + }, + "title": "OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/sl.json b/homeassistant/components/opentherm_gw/.translations/sl.json new file mode 100644 index 00000000000000..5de551d5d0c0e9 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Prehod je \u017ee konfiguriran", + "id_exists": "ID prehoda \u017ee obstaja", + "serial_error": "Napaka pri povezovanju z napravo", + "timeout": "Poskus povezave je potekel" + }, + "step": { + "init": { + "data": { + "device": "Pot ali URL", + "floor_temperature": "Temperatura nadstropja", + "id": "ID", + "name": "Ime", + "precision": "Natan\u010dnost temperature " + }, + "title": "OpenTherm Prehod" + } + }, + "title": "OpenTherm Prehod" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json new file mode 100644 index 00000000000000..648f156e8643b1 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "\u9598\u9053\u5668\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728", + "serial_error": "\u9023\u7dda\u81f3\u88dd\u7f6e\u932f\u8aa4", + "timeout": "\u9023\u7dda\u5617\u8a66\u903e\u6642" + }, + "step": { + "init": { + "data": { + "device": "\u8def\u5f91\u6216 URL", + "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", + "id": "ID", + "name": "\u540d\u7a31", + "precision": "\u6eab\u63a7\u7cbe\u6e96\u5ea6" + }, + "title": "OpenTherm \u9598\u9053\u5668" + } + }, + "title": "OpenTherm \u9598\u9053\u5668" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/ru.json b/homeassistant/components/openuv/.translations/ru.json index 9683c5d7c3679b..58d57b280567f1 100644 --- a/homeassistant/components/openuv/.translations/ru.json +++ b/homeassistant/components/openuv/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b", + "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API" }, "step": { diff --git a/homeassistant/components/owntracks/.translations/nn.json b/homeassistant/components/owntracks/.translations/nn.json new file mode 100644 index 00000000000000..cdfd651beecffc --- /dev/null +++ b/homeassistant/components/owntracks/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/nn.json b/homeassistant/components/plaato/.translations/nn.json new file mode 100644 index 00000000000000..750e14b1daeeaa --- /dev/null +++ b/homeassistant/components/plaato/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 11e11ebc6fe85b..a3ba5185371eef 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -32,6 +32,10 @@ "description": "Hi ha diversos servidors disponibles, selecciona'n un:", "title": "Selecciona servidor Plex" }, + "start_website_auth": { + "description": "Continua l'autoritzaci\u00f3 a plex.tv.", + "title": "Connexi\u00f3 amb el servidor Plex" + }, "user": { "data": { "manual_setup": "Configuraci\u00f3 manual", diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json new file mode 100644 index 00000000000000..95083102273a1b --- /dev/null +++ b/homeassistant/components/plex/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden" + }, + "step": { + "manual_setup": { + "title": "Plex Server" + }, + "start_website_auth": { + "description": "Weiter zur Autorisierung unter plex.tv.", + "title": "Plex Server verbinden" + }, + "user": { + "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." + } + } + }, + "options": { + "step": { + "plex_mp_settings": { + "description": "Optionen f\u00fcr Plex-Media-Player" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index efdd75b14819ab..bf927b7f1be07b 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -4,6 +4,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", + "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" @@ -32,6 +33,10 @@ "description": "Multiple servers available, select one:", "title": "Select Plex server" }, + "start_website_auth": { + "description": "Continue to authorize at plex.tv.", + "title": "Connect Plex server" + }, "user": { "data": { "manual_setup": "Manual setup", diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json index 6d1ad1f62daa8c..261ca9514905ec 100644 --- a/homeassistant/components/plex/.translations/es.json +++ b/homeassistant/components/plex/.translations/es.json @@ -4,7 +4,9 @@ "all_configured": "Todos los servidores vinculados ya configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "Plex se est\u00e1 configurando", + "discovery_no_file": "No se ha encontrado ning\u00fan archivo de configuraci\u00f3n antiguo", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Fall\u00f3 por razones desconocidas" }, "error": { @@ -31,6 +33,10 @@ "description": "Varios servidores disponibles, seleccione uno:", "title": "Seleccione el servidor Plex" }, + "start_website_auth": { + "description": "Contin\u00fae en plex.tv para autorizar", + "title": "Conectar servidor Plex" + }, "user": { "data": { "manual_setup": "Configuraci\u00f3n manual", diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 812de425ef49f2..c9e61dcf2e9407 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -5,18 +5,25 @@ "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "Plex en cours de configuration", "invalid_import": "La configuration import\u00e9e est invalide", + "token_request_timeout": "D\u00e9lai d'obtention du jeton", "unknown": "\u00c9chec pour une raison inconnue" }, "error": { "faulty_credentials": "L'autorisation \u00e0 \u00e9chou\u00e9e", "no_servers": "Aucun serveur li\u00e9 au compte", + "no_token": "Fournir un jeton ou s\u00e9lectionner l'installation manuelle", "not_found": "Serveur Plex introuvable" }, "step": { "manual_setup": { "data": { - "port": "Port" - } + "host": "H\u00f4te", + "port": "Port", + "ssl": "Utiliser SSL", + "token": "Jeton (si n\u00e9cessaire)", + "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "title": "Serveur Plex" }, "select_server": { "data": { @@ -27,6 +34,7 @@ }, "user": { "data": { + "manual_setup": "Installation manuelle", "token": "Jeton plex" }, "description": "Entrez un jeton Plex pour la configuration automatique.", @@ -34,5 +42,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Afficher tous les contr\u00f4les", + "use_episode_art": "Utiliser l'art de l'\u00e9pisode" + }, + "description": "Options pour lecteurs multim\u00e9dia Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 3c28f1d25f9c6b..8f61f968dba8c4 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -4,7 +4,9 @@ "all_configured": "Tutti i server collegati sono gi\u00e0 configurati", "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", "already_in_progress": "Plex \u00e8 in fase di configurazione", + "discovery_no_file": "Nessun file di configurazione legacy trovato", "invalid_import": "La configurazione importata non \u00e8 valida", + "token_request_timeout": "Timeout per l'ottenimento del token", "unknown": "Non riuscito per motivo sconosciuto" }, "error": { @@ -31,12 +33,16 @@ "description": "Sono disponibili pi\u00f9 server, selezionarne uno:", "title": "Selezionare il server Plex" }, + "start_website_auth": { + "description": "Continuare ad autorizzare su plex.tv.", + "title": "Collegare il server Plex" + }, "user": { "data": { "manual_setup": "Configurazione manuale", "token": "Token Plex" }, - "description": "Immettere un token Plex per la configurazione automatica.", + "description": "Continuare ad autorizzare plex.tv o configurare manualmente un server.", "title": "Collegare il server Plex" } }, diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 1e6488784d409a..7b0f7232976a67 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -4,7 +4,9 @@ "all_configured": "All verbonne Server sinn scho konfigur\u00e9iert", "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert", "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", + "discovery_no_file": "Kee Konfiguratioun Fichier am ale Format fonnt.", "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", + "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", "unknown": "Onbekannte Feeler opgetrueden" }, "error": { @@ -31,6 +33,10 @@ "description": "M\u00e9i Server disponibel, wielt een aus:", "title": "Plex Server auswielen" }, + "start_website_auth": { + "description": "Weiderfueren op plex.tv fir d'Autorisatioun.", + "title": "Plex Server verbannen" + }, "user": { "data": { "manual_setup": "Manuell Konfiguratioun", diff --git a/homeassistant/components/plex/.translations/nn.json b/homeassistant/components/plex/.translations/nn.json new file mode 100644 index 00000000000000..a16deb2fca2899 --- /dev/null +++ b/homeassistant/components/plex/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index a0a9d087d1e58b..18c4e865a84690 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -4,6 +4,7 @@ "all_configured": "Alle knyttet servere som allerede er konfigurert", "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Plex blir konfigurert", + "discovery_no_file": "Ingen eldre konfigurasjonsfil ble funnet", "invalid_import": "Den importerte konfigurasjonen er ugyldig", "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Mislyktes av ukjent \u00e5rsak" @@ -32,6 +33,10 @@ "description": "Flere servere tilgjengelig, velg en:", "title": "Velg Plex-server" }, + "start_website_auth": { + "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv.", + "title": "Koble til Plex server" + }, "user": { "data": { "manual_setup": "Manuelt oppsett", diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index ce9d2e1e88d73c..9b75a0061e8d1b 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -5,6 +5,7 @@ "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", "already_in_progress": "Plex jest konfigurowany", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", + "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena", "unknown": "Nieznany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 2b63840d00198b..fe773f72be9721 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", - "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", - "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", + "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", + "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", + "discovery_no_file": "\u0421\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430", "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" @@ -32,12 +33,16 @@ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u0438\u043d \u0438\u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432:", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" }, + "start_website_auth": { + "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", + "title": "Plex" + }, "user": { "data": { "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "token": "\u0422\u043e\u043a\u0435\u043d" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", + "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", "title": "Plex" } }, diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 49ed34baf763b2..9be270a017ce4d 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -4,7 +4,9 @@ "all_configured": "Vsi povezani stre\u017eniki so \u017ee konfigurirani", "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", "already_in_progress": "Plex se konfigurira", + "discovery_no_file": "Podatkovne konfiguracijske datoteke ni bilo mogo\u010de najti", "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", + "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", "unknown": "Ni uspelo iz neznanega razloga" }, "error": { @@ -31,6 +33,10 @@ "description": "Na voljo je ve\u010d stre\u017enikov, izberite enega:", "title": "Izberite stre\u017enik Plex" }, + "start_website_auth": { + "description": "Nadaljujte z avtorizacijo na plex.tv.", + "title": "Pove\u017eite stre\u017enik Plex" + }, "user": { "data": { "manual_setup": "Ro\u010dna nastavitev", diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index 5f6d0c41c13506..2d4ce1ea6aa2c5 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -4,7 +4,9 @@ "all_configured": "\u6240\u6709\u7d81\u5b9a\u4f3a\u670d\u5668\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210", "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", + "discovery_no_file": "\u627e\u4e0d\u5230\u820a\u7248\u8a2d\u5b9a\u6a94\u6848", "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", + "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" }, "error": { @@ -31,12 +33,16 @@ "description": "\u627e\u5230\u591a\u500b\u4f3a\u670d\u5668\uff0c\u8acb\u9078\u64c7\u4e00\u7d44\uff1a", "title": "\u9078\u64c7 Plex \u4f3a\u670d\u5668" }, + "start_website_auth": { + "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u3002", + "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" + }, "user": { "data": { "manual_setup": "\u624b\u52d5\u8a2d\u5b9a", "token": "Plex \u5bc6\u9470" }, - "description": "\u8acb\u8f38\u5165 Plex \u5bc6\u9470\u4ee5\u9032\u884c\u81ea\u52d5\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", + "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" } }, diff --git a/homeassistant/components/point/.translations/nn.json b/homeassistant/components/point/.translations/nn.json new file mode 100644 index 00000000000000..865155c04949a7 --- /dev/null +++ b/homeassistant/components/point/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Minut Point" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/nn.json b/homeassistant/components/ps4/.translations/nn.json index b3302389c88c9a..86920906003ecc 100644 --- a/homeassistant/components/ps4/.translations/nn.json +++ b/homeassistant/components/ps4/.translations/nn.json @@ -5,9 +5,20 @@ "port_997_bind_error": "Kunne ikkje binde til port 997. Sj\u00e5 [dokumentasjonen] (https://www.home-assistant.io/components/ps4/) for ytterlegare informasjon." }, "step": { + "creds": { + "title": "Playstation 4" + }, + "link": { + "data": { + "code": "PIN", + "name": "Namn" + }, + "title": "Playstation 4" + }, "mode": { "title": "Playstation 4" } - } + }, + "title": "Playstation 4" } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/pl.json b/homeassistant/components/rainmachine/.translations/pl.json index 9ab6156549d5a1..cf842efe9f6d0f 100644 --- a/homeassistant/components/rainmachine/.translations/pl.json +++ b/homeassistant/components/rainmachine/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "Konto jest ju\u017c zarejestrowane", - "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { "user": { diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index 6eec3ef0ebac07..6248890389d140 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" }, "step": { diff --git a/homeassistant/components/sensor/.translations/ca.json b/homeassistant/components/sensor/.translations/ca.json new file mode 100644 index 00000000000000..59db5a62f86062 --- /dev/null +++ b/homeassistant/components/sensor/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Nivell de bateria de {entity_name}", + "is_humidity": "Humitat de {entity_name}", + "is_illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "is_pressure": "Pressi\u00f3 de {entity_name}", + "is_signal_strength": "For\u00e7a del senyal de {entity_name}", + "is_temperature": "Temperatura de {entity_name}", + "is_timestamp": "Marca de temps de {entity_name}", + "is_value": "Valor de {entity_name}" + }, + "trigger_type": { + "battery_level": "Nivell de bateria de {entity_name}", + "humidity": "Humitat de {entity_name}", + "illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "pressure": "Pressi\u00f3 de {entity_name}", + "signal_strength": "For\u00e7a del senyal de {entity_name}", + "temperature": "Temperatura de {entity_name}", + "timestamp": "Marca de temps de {entity_name}", + "value": "Valor de {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/da.json b/homeassistant/components/sensor/.translations/da.json new file mode 100644 index 00000000000000..df9b9935dc149c --- /dev/null +++ b/homeassistant/components/sensor/.translations/da.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} batteriniveau", + "is_humidity": "{entity_name} fugtighed", + "is_illuminance": "{entity_name} belysningsstyrke", + "is_power": "{entity_name} str\u00f8m", + "is_pressure": "{entity_name} tryk", + "is_signal_strength": "{entity_name} signalstyrke", + "is_temperature": "{entity_name} temperatur", + "is_timestamp": "{entity_name} tidsstempel", + "is_value": "{entity_name} v\u00e6rdi" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniveau", + "humidity": "{entity_name} fugtighed", + "illuminance": "{entity_name} belysningsstyrke", + "power": "{entity_name} str\u00f8m", + "pressure": "{entity_name} tryk", + "signal_strength": "{entity_name} signalstyrke", + "temperature": "{entity_name} temperatur", + "timestamp": "{entity_name} tidsstempel", + "value": "{entity_name} v\u00e6rdi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json new file mode 100644 index 00000000000000..1f248099df34e5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/de.json @@ -0,0 +1,21 @@ +{ + "device_automation": { + "condition_type": { + "is_humidity": "{entity_name} Feuchtigkeit", + "is_pressure": "{entity_name} Druck", + "is_signal_strength": "{entity_name} Signalst\u00e4rke", + "is_temperature": "{entity_name} Temperatur", + "is_timestamp": "{entity_name} Zeitstempel", + "is_value": "{entity_name} Wert" + }, + "trigger_type": { + "battery_level": "{entity_name} Batteriestatus", + "humidity": "{entity_name} Feuchtigkeit", + "pressure": "{entity_name} Druck", + "signal_strength": "{entity_name} Signalst\u00e4rke", + "temperature": "{entity_name} Temperatur", + "timestamp": "{entity_name} Zeitstempel", + "value": "{entity_name} Wert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/en.json b/homeassistant/components/sensor/.translations/en.json new file mode 100644 index 00000000000000..7bbbe660feb94c --- /dev/null +++ b/homeassistant/components/sensor/.translations/en.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} battery level", + "is_humidity": "{entity_name} humidity", + "is_illuminance": "{entity_name} illuminance", + "is_power": "{entity_name} power", + "is_pressure": "{entity_name} pressure", + "is_signal_strength": "{entity_name} signal strength", + "is_temperature": "{entity_name} temperature", + "is_timestamp": "{entity_name} timestamp", + "is_value": "{entity_name} value" + }, + "trigger_type": { + "battery_level": "{entity_name} battery level", + "humidity": "{entity_name} humidity", + "illuminance": "{entity_name} illuminance", + "power": "{entity_name} power", + "pressure": "{entity_name} pressure", + "signal_strength": "{entity_name} signal strength", + "temperature": "{entity_name} temperature", + "timestamp": "{entity_name} timestamp", + "value": "{entity_name} value" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/es.json b/homeassistant/components/sensor/.translations/es.json new file mode 100644 index 00000000000000..a9039d2e410cf7 --- /dev/null +++ b/homeassistant/components/sensor/.translations/es.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} nivel de bater\u00eda", + "is_humidity": "{entity_name} humedad", + "is_illuminance": "{entity_name} iluminancia", + "is_power": "{entity_name} alimentaci\u00f3n", + "is_pressure": "{entity_name} presi\u00f3n", + "is_signal_strength": "{entity_name} intensidad de la se\u00f1al", + "is_temperature": "{entity_name} temperatura", + "is_timestamp": "{entity_name} marca de tiempo", + "is_value": "{entity_name} valor" + }, + "trigger_type": { + "battery_level": "{entity_name} nivel de bater\u00eda", + "humidity": "{entity_name} humedad", + "illuminance": "{entity_name} iluminancia", + "power": "{entity_name} alimentaci\u00f3n", + "pressure": "{entity_name} presi\u00f3n", + "signal_strength": "{entity_name} intensidad de la se\u00f1al", + "temperature": "{entity_name} temperatura", + "timestamp": "{entity_name} marca de tiempo", + "value": "{entity_name} valor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json new file mode 100644 index 00000000000000..676a5aa413fe9c --- /dev/null +++ b/homeassistant/components/sensor/.translations/fr.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} niveau batterie", + "is_humidity": "{entity_name} humidit\u00e9", + "is_illuminance": "{entity_name} \u00e9clairement", + "is_power": "{entity_name} puissance", + "is_pressure": "{entity_name} pression", + "is_signal_strength": "{entity_name} force du signal", + "is_temperature": "{entity_name} temp\u00e9rature", + "is_timestamp": "{entity_name} horodatage", + "is_value": "{entity_name} valeur" + }, + "trigger_type": { + "battery_level": "{entity_name} niveau batterie", + "humidity": "{entity_name} humidit\u00e9", + "illuminance": "{entity_name} \u00e9clairement", + "power": "{entity_name} puissance", + "pressure": "{entity_name} pression", + "signal_strength": "{entity_name} force du signal", + "temperature": "{entity_name} temp\u00e9rature", + "timestamp": "{entity_name} horodatage", + "value": "{entity_name} valeur" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/it.json b/homeassistant/components/sensor/.translations/it.json new file mode 100644 index 00000000000000..07b20245c1634d --- /dev/null +++ b/homeassistant/components/sensor/.translations/it.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Livello della batteria di {entity_name}", + "is_humidity": "Umidit\u00e0 di {entity_name}", + "is_illuminance": "Illuminazione di {entity_name}", + "is_power": "Potenza di {entity_name}", + "is_pressure": "Pressione di {entity_name}", + "is_signal_strength": "Potenza del segnale di {entity_name}", + "is_temperature": "Temperatura di {entity_name}", + "is_timestamp": "Data di {entity_name}", + "is_value": "Valore di {entity_name}" + }, + "trigger_type": { + "battery_level": "Livello della batteria di {entity_name}", + "humidity": "Umidit\u00e0 di {entity_name}", + "illuminance": "Illuminazione di {entity_name}", + "power": "Potenza di {entity_name}", + "pressure": "Pressione di {entity_name}", + "signal_strength": "Potenza del segnale di {entity_name}", + "temperature": "Temperatura di {entity_name}", + "timestamp": "Data di {entity_name}", + "value": "Valore di {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/lb.json b/homeassistant/components/sensor/.translations/lb.json new file mode 100644 index 00000000000000..01a4e89c9f4623 --- /dev/null +++ b/homeassistant/components/sensor/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} Batterie niveau", + "is_humidity": "{entity_name} Fiichtegkeet", + "is_illuminance": "{entity_name} Beliichtung", + "is_power": "{entity_name} Leeschtung", + "is_pressure": "{entity_name} Drock", + "is_signal_strength": "{entity_name} Signal St\u00e4erkt", + "is_temperature": "{entity_name} Temperatur", + "is_timestamp": "{entity_name} Z\u00e4itstempel", + "is_value": "{entity_name} W\u00e4ert" + }, + "trigger_type": { + "battery_level": "{entity_name} Batterie niveau", + "humidity": "{entity_name} Fiichtegkeet", + "illuminance": "{entity_name} Beliichtung", + "power": "{entity_name} Leeschtung", + "pressure": "{entity_name} Drock", + "signal_strength": "{entity_name} Signal St\u00e4erkt", + "temperature": "{entity_name} Temperatur", + "timestamp": "{entity_name} Z\u00e4itstempel", + "value": "{entity_name} W\u00e4ert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json new file mode 100644 index 00000000000000..5f5eeaacd11e75 --- /dev/null +++ b/homeassistant/components/sensor/.translations/no.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} batteriniv\u00e5", + "is_humidity": "{entity_name} fuktighet", + "is_illuminance": "{entity_name} belysningsstyrke", + "is_power": "{entity_name} str\u00f8m", + "is_pressure": "{entity_name} trykk", + "is_signal_strength": "{entity_name} signalstyrke", + "is_temperature": "{entity_name} temperatur", + "is_timestamp": "{entity_name} tidsstempel", + "is_value": "{entity_name} verdi" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniv\u00e5", + "humidity": "{entity_name} fuktighet", + "illuminance": "{entity_name} belysningsstyrke", + "power": "{entity_name} str\u00f8m", + "pressure": "{entity_name} trykk", + "signal_strength": "{entity_name} signalstyrke", + "temperature": "{entity_name} temperatur", + "timestamp": "{entity_name} tidsstempel", + "value": "{entity_name} verdi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/pl.json b/homeassistant/components/sensor/.translations/pl.json new file mode 100644 index 00000000000000..da1dcc1d6fd890 --- /dev/null +++ b/homeassistant/components/sensor/.translations/pl.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} poziom na\u0142adowania baterii", + "is_humidity": "{entity_name} wilgotno\u015b\u0107", + "is_temperature": "{entity_name} temperatura" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/sl.json b/homeassistant/components/sensor/.translations/sl.json new file mode 100644 index 00000000000000..e3bc994b6ea9b5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} raven baterije", + "is_humidity": "{entity_name} vla\u017enost", + "is_illuminance": "{entity_name} osvetlitev", + "is_power": "{entity_name} mo\u010d", + "is_pressure": "{entity_name} pritisk", + "is_signal_strength": "{entity_name} jakost signala", + "is_temperature": "{entity_name} temperatura", + "is_timestamp": "{entity_name} \u010dasovni \u017eig", + "is_value": "{entity_name} vrednost" + }, + "trigger_type": { + "battery_level": "{entity_name} raven baterije", + "humidity": "{entity_name} vla\u017enost", + "illuminance": "{entity_name} osvetljenosti", + "power": "{entity_name} mo\u010d", + "pressure": "{entity_name} tlak", + "signal_strength": "{entity_name} jakost signala", + "temperature": "{entity_name} temperatura", + "timestamp": "{entity_name} \u010dasovni \u017eig", + "value": "{entity_name} vrednost" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/zh-Hant.json b/homeassistant/components/sensor/.translations/zh-Hant.json new file mode 100644 index 00000000000000..af97681ee764d1 --- /dev/null +++ b/homeassistant/components/sensor/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} \u96fb\u91cf", + "is_humidity": "{entity_name} \u6fd5\u5ea6", + "is_illuminance": "{entity_name} \u7167\u5ea6", + "is_power": "{entity_name} \u96fb\u529b", + "is_pressure": "{entity_name} \u58d3\u529b", + "is_signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", + "is_temperature": "{entity_name} \u6eab\u5ea6", + "is_timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", + "is_value": "{entity_name} \u503c" + }, + "trigger_type": { + "battery_level": "{entity_name} \u96fb\u91cf", + "humidity": "{entity_name} \u6fd5\u5ea6", + "illuminance": "{entity_name} \u7167\u5ea6", + "power": "{entity_name} \u96fb\u529b", + "pressure": "{entity_name} \u58d3\u529b", + "signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", + "temperature": "{entity_name} \u6eab\u5ea6", + "timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", + "value": "{entity_name} \u503c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/nn.json b/homeassistant/components/simplisafe/.translations/nn.json new file mode 100644 index 00000000000000..0568cad3f6dc4b --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index f685297890eae7..e82172f92f8579 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" }, "step": { diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json index 88ea988ff1bb9a..03b17b3ba8b85c 100644 --- a/homeassistant/components/smhi/.translations/ru.json +++ b/homeassistant/components/smhi/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442", + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438" }, "step": { diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json index fe36e4296feb92..d8622cdd2c1a3b 100644 --- a/homeassistant/components/solaredge/.translations/ru.json +++ b/homeassistant/components/solaredge/.translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "error": { - "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { "user": { diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json new file mode 100644 index 00000000000000..d93eec8aed7442 --- /dev/null +++ b/homeassistant/components/soma/.translations/de.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json new file mode 100644 index 00000000000000..8126b6ea5aec63 --- /dev/null +++ b/homeassistant/components/soma/.translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", + "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json new file mode 100644 index 00000000000000..e990fb98dc233a --- /dev/null +++ b/homeassistant/components/soma/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Soma.", + "authorize_url_timeout": "D\u00e9lai d'attente g\u00e9n\u00e9rant l'autorisation de l'URL.", + "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation." + }, + "create_entry": { + "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json new file mode 100644 index 00000000000000..d8aba082537bf7 --- /dev/null +++ b/homeassistant/components/soma/.translations/lb.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Soma Kont konfigur\u00e9ieren.", + "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", + "missing_configuration": "D'Soma Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/nn.json b/homeassistant/components/soma/.translations/nn.json new file mode 100644 index 00000000000000..6eeb4f75a3c5c4 --- /dev/null +++ b/homeassistant/components/soma/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json new file mode 100644 index 00000000000000..0ed881853b8afd --- /dev/null +++ b/homeassistant/components/soma/.translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", + "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono z Soma" + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json new file mode 100644 index 00000000000000..3d28389ff9147c --- /dev/null +++ b/homeassistant/components/soma/.translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Soma \u5e33\u865f\u3002", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", + "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/nn.json b/homeassistant/components/somfy/.translations/nn.json new file mode 100644 index 00000000000000..ff0383c7f015cf --- /dev/null +++ b/homeassistant/components/somfy/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/nn.json b/homeassistant/components/tellduslive/.translations/nn.json new file mode 100644 index 00000000000000..934f56a420b353 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Telldus Live" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index afaaf4edbf5e5f..9d3c97ad902eb3 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\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.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" diff --git a/homeassistant/components/toon/.translations/nn.json b/homeassistant/components/toon/.translations/nn.json index b8dbeff27cacce..eed288a5e39a33 100644 --- a/homeassistant/components/toon/.translations/nn.json +++ b/homeassistant/components/toon/.translations/nn.json @@ -1,5 +1,12 @@ { "config": { + "step": { + "authenticate": { + "data": { + "username": "Brukarnamn" + } + } + }, "title": "Toon" } } \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/nn.json b/homeassistant/components/tplink/.translations/nn.json new file mode 100644 index 00000000000000..1d9fb41fc8ce11 --- /dev/null +++ b/homeassistant/components/tplink/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/nn.json b/homeassistant/components/traccar/.translations/nn.json new file mode 100644 index 00000000000000..9fc23b3e394074 --- /dev/null +++ b/homeassistant/components/traccar/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json new file mode 100644 index 00000000000000..ed0342b9430924 --- /dev/null +++ b/homeassistant/components/transmission/.translations/de.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "error": { + "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "wrong_credentials": "Falscher Benutzername oder Kennwort" + }, + "step": { + "options": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + }, + "title": "Konfigurationsoptionen" + }, + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index e1bc8dc322824a..67461d1a3e8565 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -20,7 +20,7 @@ "name": "Name", "password": "Password", "port": "Port", - "username": "User name" + "username": "Username" }, "title": "Setup Transmission Client" } diff --git a/homeassistant/components/transmission/.translations/fr.json b/homeassistant/components/transmission/.translations/fr.json new file mode 100644 index 00000000000000..e2360c016ca304 --- /dev/null +++ b/homeassistant/components/transmission/.translations/fr.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "error": { + "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te", + "wrong_credentials": "Mauvais nom d'utilisateur ou mot de passe" + }, + "step": { + "options": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "title": "Configurer les options" + }, + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "title": "Configuration du client Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "description": "Configurer les options pour Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/nn.json b/homeassistant/components/transmission/.translations/nn.json new file mode 100644 index 00000000000000..7622ac1b459096 --- /dev/null +++ b/homeassistant/components/transmission/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn", + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json index 5da2d4f9ef909b..e7a438cae11091 100644 --- a/homeassistant/components/transmission/.translations/ru.json +++ b/homeassistant/components/transmission/.translations/ru.json @@ -33,7 +33,7 @@ "data": { "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" }, - "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission" } } } diff --git a/homeassistant/components/twentemilieu/.translations/nn.json b/homeassistant/components/twentemilieu/.translations/nn.json new file mode 100644 index 00000000000000..02ac8ecf27a2e9 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/nn.json b/homeassistant/components/twilio/.translations/nn.json new file mode 100644 index 00000000000000..86e5d9051b339c --- /dev/null +++ b/homeassistant/components/twilio/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 3741b035d7a9d6..899b532290e929 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -38,6 +38,11 @@ "one": "un", "other": "altre" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Crea sensors d'\u00fas d'ample de banda per a clients de la xarxa" + } } } } diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json index 53b794ed4353ff..0d0315e49c752d 100644 --- a/homeassistant/components/unifi/.translations/da.json +++ b/homeassistant/components/unifi/.translations/da.json @@ -32,6 +32,11 @@ "track_devices": "Spor netv\u00e6rksenheder (Ubiquiti-enheder)", "track_wired_clients": "Inkluder kablede netv\u00e6rksklienter" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Opret b\u00e5ndbredde sensorer for netv\u00e6rksklienter" + } } } } diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index e447e89644f5b5..32a378b7c00c90 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -38,6 +38,11 @@ "one": "eins", "other": "andere" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Erstellen von Bandbreiten-Nutzungssensoren f\u00fcr Netzwerk-Clients" + } } } } diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 2025bad6246f1f..d9b65b6d1dab06 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -32,6 +32,11 @@ "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients" + } } } } diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 0539f5607b4c36..1db6712142d5d8 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -38,6 +38,11 @@ "one": "uno", "other": "otro" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Crear sensores para monitorizar uso de ancho de banda de clientes de red" + } } } } diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index 8c2526f8a1565a..c40b7822073dc9 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -32,6 +32,11 @@ "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)", "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau" + } } } } diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json index 5285ed21873529..80b546ebcf8cd8 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -38,6 +38,11 @@ "one": "uno", "other": "altro" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Creare sensori di utilizzo della larghezza di banda per i client di rete" + } } } } diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json index 05b0ffc0c44cdf..4fa1f62c602b65 100644 --- a/homeassistant/components/unifi/.translations/lb.json +++ b/homeassistant/components/unifi/.translations/lb.json @@ -38,6 +38,11 @@ "one": "Een", "other": "M\u00e9i" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente erstellen" + } } } } diff --git a/homeassistant/components/unifi/.translations/nn.json b/homeassistant/components/unifi/.translations/nn.json new file mode 100644 index 00000000000000..7c129cba3afc71 --- /dev/null +++ b/homeassistant/components/unifi/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index c21a47c7ea2c8b..9041f0184232ec 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -38,6 +38,11 @@ "one": "en", "other": "andre" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Opprett b\u00e5ndbreddesensorer for nettverksklienter" + } } } } diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index 6366f82b3da0a8..5887460a8a5a56 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -40,6 +40,11 @@ "one": "Jeden", "other": "Inne" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Stw\u00f3rz sensory wykorzystania przepustowo\u015bci przez klient\u00f3w sieciowych" + } } } } diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index 76802a96367677..d7451bd81a0ce4 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c" }, "error": { @@ -32,6 +32,11 @@ "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)", "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" + } } } } diff --git a/homeassistant/components/unifi/.translations/sl.json b/homeassistant/components/unifi/.translations/sl.json index 35000bf4e1f28d..7084c4609c5844 100644 --- a/homeassistant/components/unifi/.translations/sl.json +++ b/homeassistant/components/unifi/.translations/sl.json @@ -40,6 +40,11 @@ "other": "OSTALO", "two": "DVA" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Ustvarite senzorje porabe pasovne \u0161irine za omre\u017ene odjemalce" + } } } } diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index 498afcbb10a19f..5e0b881af15200 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -32,6 +32,11 @@ "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09", "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u65b0\u589e\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668" + } } } } diff --git a/homeassistant/components/upnp/.translations/nn.json b/homeassistant/components/upnp/.translations/nn.json index 286efcf0353076..cfbedd994afa43 100644 --- a/homeassistant/components/upnp/.translations/nn.json +++ b/homeassistant/components/upnp/.translations/nn.json @@ -11,6 +11,7 @@ "init": { "title": "UPnP / IGD" } - } + }, + "title": "UPnP / IGD" } } \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 8d41ec1d5def31..3351f0d5d8a815 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP", "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP / IGD \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", diff --git a/homeassistant/components/vesync/.translations/nn.json b/homeassistant/components/vesync/.translations/nn.json new file mode 100644 index 00000000000000..372e37133b12c6 --- /dev/null +++ b/homeassistant/components/vesync/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/.translations/nn.json b/homeassistant/components/wemo/.translations/nn.json new file mode 100644 index 00000000000000..c1c8830cb25618 --- /dev/null +++ b/homeassistant/components/wemo/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Wemo" + } + }, + "title": "Wemo" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nn.json b/homeassistant/components/withings/.translations/nn.json new file mode 100644 index 00000000000000..7d8b268367c374 --- /dev/null +++ b/homeassistant/components/withings/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/ru.json b/homeassistant/components/wwlln/.translations/ru.json index ad553def6c3ee7..3bdaf85498bcd4 100644 --- a/homeassistant/components/wwlln/.translations/ru.json +++ b/homeassistant/components/wwlln/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 0b800ecd80a7ef..39f254ac9af075 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -29,7 +29,10 @@ "button_3": "Tredje knap", "button_4": "Fjerde knap", "button_5": "Femte knap", + "button_6": "Sjette knap", "close": "Luk", + "dim_down": "D\u00e6mp ned", + "dim_up": "D\u00e6mp op", "left": "Venstre", "open": "\u00c5ben", "right": "H\u00f8jre" diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 280c941b427f4e..9ffd5211a1fca0 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -16,5 +16,13 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "close": "Schlie\u00dfen", + "left": "Links", + "open": "Offen", + "right": "Rechts" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index 48328aed878189..f8b78af5721ab4 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -16,5 +16,32 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Hurlement", + "warn": "Pr\u00e9venir" + }, + "trigger_subtype": { + "both_buttons": "Les deux boutons", + "button_1": "Premier bouton", + "button_2": "Deuxi\u00e8me bouton", + "button_3": "Troisi\u00e8me bouton", + "button_4": "Quatri\u00e8me bouton", + "button_5": "Cinqui\u00e8me bouton", + "button_6": "Sixi\u00e8me bouton", + "close": "Fermer", + "left": "Gauche", + "open": "Ouvert", + "right": "Droite", + "turn_off": "\u00c9teindre", + "turn_on": "Allumer" + }, + "trigger_type": { + "device_shaken": "Appareil secou\u00e9", + "device_tilted": "Dispositif inclin\u00e9", + "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", + "remote_button_triple_press": "Bouton\"{sous-type}\" \u00e0 trois clics" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nn.json b/homeassistant/components/zha/.translations/nn.json new file mode 100644 index 00000000000000..ad2c240baf18f2 --- /dev/null +++ b/homeassistant/components/zha/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 390069b7698c4e..18c4c3c9ff2a14 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -53,12 +53,12 @@ "device_rotated": "Enheten roterte \"{subtype}\"", "device_shaken": "Enhet er ristet", "device_slid": "Enheten skled \"{subtype}\"", - "device_tilted": "Enhetn skr\u00e5stilt", - "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", - "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", - "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\" {subtype} \" - knappen ble femdobbelt klikket", + "device_tilted": "Enheten skr\u00e5stilt", + "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen ble femdobbelt klikket", "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen ble trippel klikket" diff --git a/homeassistant/components/zone/.translations/ru.json b/homeassistant/components/zone/.translations/ru.json index dc408035d0f657..6a017e9e1c373e 100644 --- a/homeassistant/components/zone/.translations/ru.json +++ b/homeassistant/components/zone/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "init": { diff --git a/homeassistant/components/zwave/.translations/nn.json b/homeassistant/components/zwave/.translations/nn.json index ebd9d44796c7d2..8d1c737170f35e 100644 --- a/homeassistant/components/zwave/.translations/nn.json +++ b/homeassistant/components/zwave/.translations/nn.json @@ -4,6 +4,7 @@ "user": { "description": "Sj\u00e5 [www.home-assistant.io/docs/z-wave/installation/](https://www.home-assistant.io/docs/z-wave/installation/) for informasjon om konfigurasjonsvariablene." } - } + }, + "title": "Z-Wave" } } \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json index a64b4db185d7a1..ed2e20f35270c2 100644 --- a/homeassistant/components/zwave/.translations/ru.json +++ b/homeassistant/components/zwave/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave" }, "error": { From d97943575ac5bcced97ad376bf70f07747aa27d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Oct 2019 11:19:46 -0700 Subject: [PATCH 0695/3953] Bumped version to 0.100.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5bd5b7ea7650be..2ded4cde472f94 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 7f20210e934ab6208dc6b1be46728c35117bb7c3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Oct 2019 21:52:25 +0200 Subject: [PATCH 0696/3953] Include unit_of_measurement in sensor device trigger capabilities (#27265) * Expose unit_of_measurement in sensor device trigger * Update test --- .../components/sensor/device_trigger.py | 16 ++++++++-- .../components/sensor/test_device_trigger.py | 31 ++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index bd53dca0c9d11a..7eabc4571610f3 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -11,6 +11,7 @@ CONF_ENTITY_ID, CONF_FOR, CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -149,11 +150,22 @@ async def async_get_triggers(hass, device_id): async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else "" + ) + return { "extra_fields": vol.Schema( { - vol.Optional(CONF_ABOVE): vol.Coerce(float), - vol.Optional(CONF_BELOW): vol.Coerce(float), + vol.Optional( + CONF_ABOVE, + description={CONF_UNIT_OF_MEASUREMENT: unit_of_measurement}, + ): vol.Coerce(float), + vol.Optional( + CONF_BELOW, + description={CONF_UNIT_OF_MEASUREMENT: unit_of_measurement}, + ): vol.Coerce(float), vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 45452dc84a0dca..1bc7e5e1ee572f 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -74,26 +74,49 @@ async def test_get_triggers(hass, device_reg, entity_reg): if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 8 assert triggers == expected_triggers async def test_get_trigger_capabilities(hass, device_reg, entity_reg): - """Test we get the expected capabilities from a binary_sensor trigger.""" + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["battery"].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + expected_capabilities = { "extra_fields": [ - {"name": "above", "optional": True, "type": "float"}, - {"name": "below", "optional": True, "type": "float"}, + { + "description": {"unit_of_measurement": "%"}, + "name": "above", + "optional": True, + "type": "float", + }, + { + "description": {"unit_of_measurement": "%"}, + "name": "below", + "optional": True, + "type": "float", + }, {"name": "for", "optional": True, "type": "positive_time_period_dict"}, ] } triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 1 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( hass, "trigger", trigger From d345b58ce6b9f0486bc901b850dfd72b5238c6fe Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 8 Oct 2019 23:44:33 +0200 Subject: [PATCH 0697/3953] Improve UniFi config flow tests and add options flow test (#27340) --- homeassistant/components/unifi/config_flow.py | 6 - tests/components/deconz/test_config_flow.py | 2 +- tests/components/unifi/test_config_flow.py | 265 ++++++++++++++++++ tests/components/unifi/test_init.py | 172 +----------- 4 files changed, 268 insertions(+), 177 deletions(-) create mode 100644 tests/components/unifi/test_config_flow.py diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 92281837f48e45..01b97a78366116 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -150,12 +150,6 @@ async def async_step_site(self, user_input=None): self.desc = next(iter(self.sites.values()))["desc"] return await self.async_step_site(user_input={}) - if self.desc is not None: - for site in self.sites.values(): - if self.desc == site["name"]: - self.desc = site["desc"] - return await self.async_step_site(user_input={}) - sites = [] for site in self.sites.values(): sites.append(site["desc"]) diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d0423c394a6975..4045201bd18084 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -387,7 +387,7 @@ async def test_hassio_confirm(hass): async def test_option_flow(hass): - """Test config flow selection of one of two bridges.""" + """Test config flow options.""" entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None) hass.config_entries._entries.append(entry) diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py new file mode 100644 index 00000000000000..aea4d565f3d36b --- /dev/null +++ b/tests/components/unifi/test_config_flow.py @@ -0,0 +1,265 @@ +"""Test UniFi config flow.""" +from asynctest import patch + +from homeassistant.components import unifi +from homeassistant.components.unifi import config_flow +from homeassistant.components.unifi.const import CONF_CONTROLLER, CONF_SITE_ID + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from tests.common import MockConfigEntry + +import aiounifi + + +async def test_flow_works(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + aioclient_mock.post( + "https://1.2.3.4:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": "application/json"}, + ) + + aioclient_mock.get( + "https://1.2.3.4:1234/api/self/sites", + json={ + "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "meta": {"rc": "ok"}, + }, + headers={"content-type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "Site name" + assert result["data"] == { + CONF_CONTROLLER: { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_SITE_ID: "site_id", + CONF_VERIFY_SSL: True, + } + } + + +async def test_flow_works_multiple_sites(hass, aioclient_mock): + """Test config flow works when finding multiple sites.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + aioclient_mock.post( + "https://1.2.3.4:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": "application/json"}, + ) + + aioclient_mock.get( + "https://1.2.3.4:1234/api/self/sites", + json={ + "data": [ + {"name": "default", "role": "admin", "desc": "site name"}, + {"name": "site2", "role": "admin", "desc": "site2 name"}, + ], + "meta": {"rc": "ok"}, + }, + headers={"content-type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "site" + assert result["data_schema"]({"site": "site name"}) + assert result["data_schema"]({"site": "site2 name"}) + + +async def test_flow_fails_site_already_configured(hass, aioclient_mock): + """Test config flow.""" + entry = MockConfigEntry( + domain=unifi.DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + aioclient_mock.post( + "https://1.2.3.4:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": "application/json"}, + ) + + aioclient_mock.get( + "https://1.2.3.4:1234/api/self/sites", + json={ + "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "meta": {"rc": "ok"}, + }, + headers={"content-type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "abort" + + +async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.Unauthorized): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "faulty_credentials"} + + +async def test_flow_fails_controller_unavailable(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.RequestError): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "service_unavailable"} + + +async def test_flow_fails_unknown_problem(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("aiounifi.Controller.login", side_effect=Exception): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "abort" + + +async def test_option_flow(hass): + """Test config flow options.""" + entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None) + hass.config_entries._entries.append(entry) + + flow = await hass.config_entries.options._async_create_flow( + entry.entry_id, context={"source": "test"}, data=None + ) + + result = await flow.async_step_init() + assert result["type"] == "form" + assert result["step_id"] == "device_tracker" + + result = await flow.async_step_device_tracker( + user_input={ + config_flow.CONF_TRACK_CLIENTS: False, + config_flow.CONF_TRACK_WIRED_CLIENTS: False, + config_flow.CONF_TRACK_DEVICES: False, + config_flow.CONF_DETECTION_TIME: 100, + } + ) + assert result["type"] == "form" + assert result["step_id"] == "statistics_sensors" + + result = await flow.async_step_statistics_sensors( + user_input={config_flow.CONF_ALLOW_BANDWIDTH_SENSORS: True} + ) + assert result["type"] == "create_entry" + assert result["data"] == { + config_flow.CONF_TRACK_CLIENTS: False, + config_flow.CONF_TRACK_WIRED_CLIENTS: False, + config_flow.CONF_TRACK_DEVICES: False, + config_flow.CONF_DETECTION_TIME: 100, + config_flow.CONF_ALLOW_BANDWIDTH_SENSORS: True, + } diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 845954d8134abf..6b17b803390488 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -2,16 +2,9 @@ from unittest.mock import Mock, patch from homeassistant.components import unifi -from homeassistant.components.unifi import config_flow + from homeassistant.setup import async_setup_component -from homeassistant.components.unifi.const import CONF_CONTROLLER, CONF_SITE_ID -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) + from tests.common import mock_coro, MockConfigEntry @@ -179,164 +172,3 @@ async def test_unload_entry(hass): assert await unifi.async_unload_entry(hass, entry) assert len(mock_controller.return_value.async_reset.mock_calls) == 1 assert hass.data[unifi.DOMAIN] == {} - - -async def test_flow_works(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch("aiounifi.Controller") as mock_controller: - - def mock_constructor( - host, username, password, port, site, websession, sslcontext - ): - """Fake the controller constructor.""" - mock_controller.host = host - mock_controller.username = username - mock_controller.password = password - mock_controller.port = port - mock_controller.site = site - return mock_controller - - mock_controller.side_effect = mock_constructor - mock_controller.login.return_value = mock_coro() - mock_controller.sites.return_value = mock_coro( - {"site1": {"name": "default", "role": "admin", "desc": "site name"}} - ) - - await flow.async_step_user( - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - CONF_VERIFY_SSL: True, - } - ) - - result = await flow.async_step_site(user_input={}) - - assert mock_controller.host == "1.2.3.4" - assert len(mock_controller.login.mock_calls) == 1 - assert len(mock_controller.sites.mock_calls) == 1 - - assert result["type"] == "create_entry" - assert result["title"] == "site name" - assert result["data"] == { - CONF_CONTROLLER: { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - CONF_SITE_ID: "default", - CONF_VERIFY_SSL: True, - } - } - - -async def test_controller_multiple_sites(hass): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - flow.config = { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - } - flow.sites = { - "site1": {"name": "default", "role": "admin", "desc": "site name"}, - "site2": {"name": "site2", "role": "admin", "desc": "site2 name"}, - } - - result = await flow.async_step_site() - - assert result["type"] == "form" - assert result["step_id"] == "site" - - assert result["data_schema"]({"site": "site name"}) - assert result["data_schema"]({"site": "site2 name"}) - - -async def test_controller_site_already_configured(hass): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - entry = MockConfigEntry( - domain=unifi.DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "default"}} - ) - entry.add_to_hass(hass) - - flow.config = { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - } - flow.desc = "site name" - flow.sites = {"site1": {"name": "default", "role": "admin", "desc": "site name"}} - - result = await flow.async_step_site() - - assert result["type"] == "abort" - - -async def test_user_credentials_faulty(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch.object( - config_flow, "get_controller", side_effect=unifi.errors.AuthenticationRequired - ): - result = await flow.async_step_user( - { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_SITE_ID: "default", - } - ) - - assert result["type"] == "form" - assert result["errors"] == {"base": "faulty_credentials"} - - -async def test_controller_is_unavailable(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch.object( - config_flow, "get_controller", side_effect=unifi.errors.CannotConnect - ): - result = await flow.async_step_user( - { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_SITE_ID: "default", - } - ) - - assert result["type"] == "form" - assert result["errors"] == {"base": "service_unavailable"} - - -async def test_controller_unkown_problem(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch.object(config_flow, "get_controller", side_effect=Exception): - result = await flow.async_step_user( - { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_SITE_ID: "default", - } - ) - - assert result["type"] == "abort" From 3e6b9a17cc98a20efa27ffb8a8d4dded734786b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 9 Oct 2019 00:45:24 +0300 Subject: [PATCH 0698/3953] Run mypy in pre-commit (#27339) * Move mypy files config to setup.cfg * Add mypy in pre-commit --- .pre-commit-config.yaml | 4 ++++ azure-pipelines-ci.yml | 5 +---- mypyrc | 38 -------------------------------------- setup.cfg | 14 +++++++++----- tox.ini | 3 +-- 5 files changed, 15 insertions(+), 49 deletions(-) delete mode 100644 mypyrc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 78b7ec29859e1a..8e8792e88c9453 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,3 +13,7 @@ repos: additional_dependencies: - flake8-docstrings==1.3.1 - pydocstyle==4.0.0 +- repo: https://github.com/pre-commit/mirrors-mypy.git + rev: v0.730 + hooks: + - id: mypy diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 13f0915bc56f12..74e9ea107c56cd 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -166,9 +166,6 @@ stages: pip install -r requirements_test.txt -c homeassistant/package_constraints.txt displayName: 'Setup Env' - script: | - TYPING_FILES=$(cat mypyrc) - echo -e "Run mypy on: \n$TYPING_FILES" - . venv/bin/activate - mypy $TYPING_FILES + mypy homeassistant displayName: 'Run mypy' diff --git a/mypyrc b/mypyrc deleted file mode 100644 index 08413ecd23c696..00000000000000 --- a/mypyrc +++ /dev/null @@ -1,38 +0,0 @@ -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/device_automation/ -homeassistant/components/frontend/ -homeassistant/components/geo_location/ -homeassistant/components/group/ -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/persistent_notification/ -homeassistant/components/proximity/ -homeassistant/components/remote/ -homeassistant/components/scene/ -homeassistant/components/sensor/ -homeassistant/components/sun/ -homeassistant/components/switch/ -homeassistant/components/systemmonitor/ -homeassistant/components/tts/ -homeassistant/components/vacuum/ -homeassistant/components/water_heater/ -homeassistant/components/weather/ -homeassistant/components/websocket_api/ -homeassistant/components/zone/ -homeassistant/helpers/ -homeassistant/scripts/ -homeassistant/util/ diff --git a/setup.cfg b/setup.cfg index 4c9c892b93fbb4..6d0e5378b44de9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,17 +57,21 @@ combine_as_imports = true [mypy] python_version = 3.6 +ignore_errors = true +follow_imports = silent +ignore_missing_imports = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_unused_configs = true + +[mypy-homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.loader,homeassistant.__main__,homeassistant.monkey_patch,homeassistant.requirements,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,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.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*] +ignore_errors = false 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/tox.ini b/tox.ini index 2d4cf7c54ba2bf..8c3563dac837c8 100644 --- a/tox.ini +++ b/tox.ini @@ -37,9 +37,8 @@ commands = flake8 {posargs: homeassistant tests script} [testenv:typing] -whitelist_externals=/bin/bash deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - /bin/bash -c 'TYPING_FILES=$(cat mypyrc); mypy $TYPING_FILES' + mypy homeassistant From 768bb0017789f0d3a1737d032f9662b66cffe8b9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 9 Oct 2019 00:32:17 +0000 Subject: [PATCH 0699/3953] [ci skip] Translation update --- .../components/adguard/.translations/nn.json | 11 ++++ .../components/airly/.translations/ko.json | 22 ++++++++ .../components/airly/.translations/nn.json | 10 ++++ .../arcam_fmj/.translations/nn.json | 5 ++ .../components/axis/.translations/nn.json | 3 +- .../binary_sensor/.translations/en.json | 2 + .../binary_sensor/.translations/ko.json | 54 ++++++++++++++++--- .../components/daikin/.translations/nn.json | 5 ++ .../components/deconz/.translations/ko.json | 1 + .../dialogflow/.translations/nn.json | 5 ++ .../components/ecobee/.translations/de.json | 11 ++++ .../components/ecobee/.translations/ko.json | 25 +++++++++ .../components/ecobee/.translations/nn.json | 5 ++ .../emulated_roku/.translations/nn.json | 5 ++ .../components/esphome/.translations/nn.json | 7 ++- .../geonetnz_quakes/.translations/nn.json | 12 +++++ .../components/heos/.translations/de.json | 2 +- .../iaqualink/.translations/nn.json | 5 ++ .../components/ipma/.translations/nn.json | 11 ++++ .../components/iqvia/.translations/nn.json | 10 ++++ .../components/izone/.translations/nn.json | 10 ++++ .../components/life360/.translations/nn.json | 12 +++++ .../components/lifx/.translations/nn.json | 10 ++++ .../components/linky/.translations/nn.json | 10 ++++ .../components/mailgun/.translations/nn.json | 5 ++ .../components/met/.translations/nn.json | 11 ++++ .../components/neato/.translations/de.json | 3 +- .../components/neato/.translations/es.json | 3 +- .../components/neato/.translations/ko.json | 27 ++++++++++ .../components/neato/.translations/nn.json | 12 +++++ .../opentherm_gw/.translations/ko.json | 23 ++++++++ .../opentherm_gw/.translations/nn.json | 12 +++++ .../owntracks/.translations/nn.json | 5 ++ .../components/plaato/.translations/nn.json | 5 ++ .../components/plex/.translations/de.json | 17 ++++++ .../components/plex/.translations/es.json | 5 ++ .../components/plex/.translations/ko.json | 31 ++++++++++- .../components/plex/.translations/lb.json | 5 ++ .../components/plex/.translations/nn.json | 5 ++ .../components/plex/.translations/no.json | 5 ++ .../components/plex/.translations/ru.json | 5 ++ .../components/plex/.translations/sl.json | 5 ++ .../plex/.translations/zh-Hant.json | 5 ++ .../components/point/.translations/nn.json | 5 ++ .../components/ps4/.translations/nn.json | 13 ++++- .../components/sensor/.translations/de.json | 21 ++++++++ .../components/sensor/.translations/ko.json | 26 +++++++++ .../components/sensor/.translations/sl.json | 15 +++++- .../simplisafe/.translations/nn.json | 5 ++ .../components/soma/.translations/de.json | 9 ++++ .../components/soma/.translations/nn.json | 5 ++ .../components/somfy/.translations/nn.json | 5 ++ .../tellduslive/.translations/nn.json | 5 ++ .../components/toon/.translations/nn.json | 7 +++ .../components/tplink/.translations/nn.json | 10 ++++ .../components/traccar/.translations/nn.json | 5 ++ .../transmission/.translations/de.json | 37 +++++++++++++ .../transmission/.translations/en.json | 2 +- .../transmission/.translations/ko.json | 40 ++++++++++++++ .../transmission/.translations/nn.json | 12 +++++ .../twentemilieu/.translations/nn.json | 10 ++++ .../components/twilio/.translations/nn.json | 5 ++ .../components/unifi/.translations/de.json | 5 ++ .../components/unifi/.translations/es.json | 5 ++ .../components/unifi/.translations/ko.json | 5 ++ .../components/unifi/.translations/nn.json | 11 ++++ .../components/upnp/.translations/nn.json | 3 +- .../components/vesync/.translations/nn.json | 5 ++ .../components/wemo/.translations/nn.json | 10 ++++ .../components/withings/.translations/nn.json | 5 ++ .../components/zha/.translations/de.json | 8 +++ .../components/zha/.translations/ko.json | 47 ++++++++++++++++ .../components/zha/.translations/nn.json | 10 ++++ .../components/zwave/.translations/nn.json | 3 +- 74 files changed, 779 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/adguard/.translations/nn.json create mode 100644 homeassistant/components/airly/.translations/ko.json create mode 100644 homeassistant/components/airly/.translations/nn.json create mode 100644 homeassistant/components/arcam_fmj/.translations/nn.json create mode 100644 homeassistant/components/daikin/.translations/nn.json create mode 100644 homeassistant/components/dialogflow/.translations/nn.json create mode 100644 homeassistant/components/ecobee/.translations/de.json create mode 100644 homeassistant/components/ecobee/.translations/ko.json create mode 100644 homeassistant/components/ecobee/.translations/nn.json create mode 100644 homeassistant/components/emulated_roku/.translations/nn.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/nn.json create mode 100644 homeassistant/components/iaqualink/.translations/nn.json create mode 100644 homeassistant/components/ipma/.translations/nn.json create mode 100644 homeassistant/components/iqvia/.translations/nn.json create mode 100644 homeassistant/components/izone/.translations/nn.json create mode 100644 homeassistant/components/life360/.translations/nn.json create mode 100644 homeassistant/components/lifx/.translations/nn.json create mode 100644 homeassistant/components/linky/.translations/nn.json create mode 100644 homeassistant/components/mailgun/.translations/nn.json create mode 100644 homeassistant/components/met/.translations/nn.json create mode 100644 homeassistant/components/neato/.translations/ko.json create mode 100644 homeassistant/components/neato/.translations/nn.json create mode 100644 homeassistant/components/opentherm_gw/.translations/ko.json create mode 100644 homeassistant/components/opentherm_gw/.translations/nn.json create mode 100644 homeassistant/components/owntracks/.translations/nn.json create mode 100644 homeassistant/components/plaato/.translations/nn.json create mode 100644 homeassistant/components/plex/.translations/nn.json create mode 100644 homeassistant/components/point/.translations/nn.json create mode 100644 homeassistant/components/sensor/.translations/de.json create mode 100644 homeassistant/components/sensor/.translations/ko.json create mode 100644 homeassistant/components/simplisafe/.translations/nn.json create mode 100644 homeassistant/components/soma/.translations/de.json create mode 100644 homeassistant/components/soma/.translations/nn.json create mode 100644 homeassistant/components/somfy/.translations/nn.json create mode 100644 homeassistant/components/tellduslive/.translations/nn.json create mode 100644 homeassistant/components/tplink/.translations/nn.json create mode 100644 homeassistant/components/traccar/.translations/nn.json create mode 100644 homeassistant/components/transmission/.translations/de.json create mode 100644 homeassistant/components/transmission/.translations/ko.json create mode 100644 homeassistant/components/transmission/.translations/nn.json create mode 100644 homeassistant/components/twentemilieu/.translations/nn.json create mode 100644 homeassistant/components/twilio/.translations/nn.json create mode 100644 homeassistant/components/unifi/.translations/nn.json create mode 100644 homeassistant/components/vesync/.translations/nn.json create mode 100644 homeassistant/components/wemo/.translations/nn.json create mode 100644 homeassistant/components/withings/.translations/nn.json create mode 100644 homeassistant/components/zha/.translations/nn.json diff --git a/homeassistant/components/adguard/.translations/nn.json b/homeassistant/components/adguard/.translations/nn.json new file mode 100644 index 00000000000000..7c129cba3afc71 --- /dev/null +++ b/homeassistant/components/adguard/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/ko.json b/homeassistant/components/airly/.translations/ko.json new file mode 100644 index 00000000000000..eb20c9174b4911 --- /dev/null +++ b/homeassistant/components/airly/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API \ud0a4\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4.", + "wrong_location": "\uc774 \uc9c0\uc5ed\uc5d0\ub294 Airly \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984" + }, + "description": "Airly \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://developer.airly.eu/register \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/nn.json b/homeassistant/components/airly/.translations/nn.json new file mode 100644 index 00000000000000..7e2f4f1ff6bcef --- /dev/null +++ b/homeassistant/components/airly/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/nn.json b/homeassistant/components/arcam_fmj/.translations/nn.json new file mode 100644 index 00000000000000..b0ad4660d0fef1 --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/nn.json b/homeassistant/components/axis/.translations/nn.json index 3364446935953a..b6296d1acab703 100644 --- a/homeassistant/components/axis/.translations/nn.json +++ b/homeassistant/components/axis/.translations/nn.json @@ -5,7 +5,8 @@ "data": { "host": "Vert", "password": "Passord", - "port": "Port" + "port": "Port", + "username": "Brukarnamn" } } } diff --git a/homeassistant/components/binary_sensor/.translations/en.json b/homeassistant/components/binary_sensor/.translations/en.json index 6379df936b898d..93b61893980eb6 100644 --- a/homeassistant/components/binary_sensor/.translations/en.json +++ b/homeassistant/components/binary_sensor/.translations/en.json @@ -53,6 +53,7 @@ "hot": "{entity_name} became hot", "light": "{entity_name} started detecting light", "locked": "{entity_name} locked", + "moist": "{entity_name} became moist", "moist\u00a7": "{entity_name} became moist", "motion": "{entity_name} started detecting motion", "moving": "{entity_name} started moving", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} became dry", "not_moving": "{entity_name} stopped moving", "not_occupied": "{entity_name} became not occupied", + "not_opened": "{entity_name} closed", "not_plugged_in": "{entity_name} unplugged", "not_powered": "{entity_name} not powered", "not_present": "{entity_name} not present", diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json index 3c12eabe8ff0c8..167708c2cf14bd 100644 --- a/homeassistant/components/binary_sensor/.translations/ko.json +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", + "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc2b5\ub2c8\ub2e4", "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", @@ -18,20 +18,20 @@ "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_not_bat_low": "{entity_name} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", + "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4", "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", "is_not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud569\ub2c8\ub2e4", "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_not_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", "is_not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud614\uc2b5\ub2c8\ub2e4", "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "is_not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc2b5\ub2c8\ub2e4", "is_not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud569\ub2c8\ub2e4", - "is_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4", + "is_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4", "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", @@ -45,8 +45,50 @@ "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4" }, "trigger_type": { - "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871", - "closed": "{entity_name} \ub2eb\ud798" + "bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871", + "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", + "cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6cc\uc9d0", + "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub428", + "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud568", + "hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6cc\uc9d0", + "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud568", + "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae40", + "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0", + "moist\u00a7": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0", + "motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud568", + "moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784", + "no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0 \ubabb\ud568", + "no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0 \ubabb\ud568", + "no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0 \ubabb\ud568", + "no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0 \ubabb\ud568", + "no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0 \ubabb\ud568", + "no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0 \ubabb\ud568", + "no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0 \ubabb\ud568", + "not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc815\uc0c1", + "not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc74c", + "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9d0", + "not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc74c", + "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub428", + "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9d0", + "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc74c", + "not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc74c", + "not_opened": "{entity_name} \uc774(\uac00) \ub2eb\ud798", + "not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud798", + "not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc74c", + "not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc74c", + "not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud574\uc9d0", + "occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911", + "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9bc", + "plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud798", + "powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub428", + "present": "{entity_name} \uc774(\uac00) \uc788\uc74c", + "problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud568", + "smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud568", + "sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud568", + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9d0", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9d0", + "unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc74c", + "vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud568" } } } \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/nn.json b/homeassistant/components/daikin/.translations/nn.json new file mode 100644 index 00000000000000..67d4f85262572e --- /dev/null +++ b/homeassistant/components/daikin/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 923a2beb2ffba2..ef8d3910ecfac1 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", "remote_button_rotated": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804", + "remote_button_rotation_stopped": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804 \uc815\uc9c0", "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984", diff --git a/homeassistant/components/dialogflow/.translations/nn.json b/homeassistant/components/dialogflow/.translations/nn.json new file mode 100644 index 00000000000000..5a96b853eb0944 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/de.json b/homeassistant/components/ecobee/.translations/de.json new file mode 100644 index 00000000000000..1959f769d3a4c9 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/de.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/ko.json b/homeassistant/components/ecobee/.translations/ko.json new file mode 100644 index 00000000000000..2fea66a9d3858c --- /dev/null +++ b/homeassistant/components/ecobee/.translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ud604\uc7ac \ud558\ub098\uc758 ecobee \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4." + }, + "error": { + "pin_request_failed": "ecobee \ub85c\ubd80\ud130 PIN \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; API \ud0a4\uac00 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "token_request_failed": "ecobee \ub85c\ubd80\ud130 \ud1a0\ud070 \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "step": { + "authorize": { + "description": "https://www.ecobee.com/consumerportal/index.html \uc5d0\uc11c PIN \ucf54\ub4dc\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc774 \uc571\uc744 \uc2b9\uc778\ud574\uc8fc\uc138\uc694:\n\n {pin} \n \n \uadf8\ub7f0 \ub2e4\uc74c Submit \uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", + "title": "ecobee.com \uc5d0\uc11c \uc571 \uc2b9\uc778\ud558\uae30" + }, + "user": { + "data": { + "api_key": "API \ud0a4" + }, + "description": "ecobee.com \uc5d0\uc11c \uc5bb\uc740 API \ud0a4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "ecobee API \ud0a4" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/nn.json b/homeassistant/components/ecobee/.translations/nn.json new file mode 100644 index 00000000000000..301239cf31a6c9 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/nn.json b/homeassistant/components/emulated_roku/.translations/nn.json new file mode 100644 index 00000000000000..fc349a0d9de9df --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/nn.json b/homeassistant/components/esphome/.translations/nn.json index 830391f58f6e30..5e40c8ec5e5635 100644 --- a/homeassistant/components/esphome/.translations/nn.json +++ b/homeassistant/components/esphome/.translations/nn.json @@ -1,9 +1,14 @@ { "config": { + "flow_title": "ESPHome: {name}", "step": { "discovery_confirm": { "title": "Fann ESPhome node" + }, + "user": { + "title": "ESPHome" } - } + }, + "title": "ESPHome" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/nn.json b/homeassistant/components/geonetnz_quakes/.translations/nn.json new file mode 100644 index 00000000000000..d8afb1e7aaead1 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/de.json b/homeassistant/components/heos/.translations/de.json index e8f4df930dbe99..e98df7466ff69c 100644 --- a/homeassistant/components/heos/.translations/de.json +++ b/homeassistant/components/heos/.translations/de.json @@ -16,6 +16,6 @@ "title": "Mit Heos verbinden" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/nn.json b/homeassistant/components/iaqualink/.translations/nn.json new file mode 100644 index 00000000000000..ea78f0d0d5dd0c --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/nn.json b/homeassistant/components/ipma/.translations/nn.json new file mode 100644 index 00000000000000..0e024a0e1eb7b0 --- /dev/null +++ b/homeassistant/components/ipma/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/nn.json b/homeassistant/components/iqvia/.translations/nn.json new file mode 100644 index 00000000000000..89922b66f03dca --- /dev/null +++ b/homeassistant/components/iqvia/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/nn.json b/homeassistant/components/izone/.translations/nn.json new file mode 100644 index 00000000000000..eaf7601be9c3d8 --- /dev/null +++ b/homeassistant/components/izone/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/nn.json b/homeassistant/components/life360/.translations/nn.json new file mode 100644 index 00000000000000..98345b022f2e1b --- /dev/null +++ b/homeassistant/components/life360/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + }, + "title": "Life360" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/nn.json b/homeassistant/components/lifx/.translations/nn.json new file mode 100644 index 00000000000000..c78905b09c8f95 --- /dev/null +++ b/homeassistant/components/lifx/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/nn.json b/homeassistant/components/linky/.translations/nn.json new file mode 100644 index 00000000000000..6e084d1a9d28fc --- /dev/null +++ b/homeassistant/components/linky/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/nn.json b/homeassistant/components/mailgun/.translations/nn.json new file mode 100644 index 00000000000000..2bab2e430016cf --- /dev/null +++ b/homeassistant/components/mailgun/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/nn.json b/homeassistant/components/met/.translations/nn.json new file mode 100644 index 00000000000000..0e024a0e1eb7b0 --- /dev/null +++ b/homeassistant/components/met/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/de.json b/homeassistant/components/neato/.translations/de.json index a3f54f9f69afd3..2a75d11a9ec0c4 100644 --- a/homeassistant/components/neato/.translations/de.json +++ b/homeassistant/components/neato/.translations/de.json @@ -8,7 +8,8 @@ "default": "Siehe [Neato-Dokumentation]({docs_url})." }, "error": { - "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen", + "unexpected_error": "Unerwarteter Fehler" }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/es.json b/homeassistant/components/neato/.translations/es.json index d033b8af6a47d6..99e7574e4b2743 100644 --- a/homeassistant/components/neato/.translations/es.json +++ b/homeassistant/components/neato/.translations/es.json @@ -8,7 +8,8 @@ "default": "Ver [documentaci\u00f3n Neato]({docs_url})." }, "error": { - "invalid_credentials": "Credenciales no v\u00e1lidas" + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unexpected_error": "Error inesperado" }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/ko.json b/homeassistant/components/neato/.translations/ko.json new file mode 100644 index 00000000000000..aeb591f7b20ccd --- /dev/null +++ b/homeassistant/components/neato/.translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "create_entry": { + "default": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + }, + "error": { + "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unexpected_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "vendor": "\uacf5\uae09 \uc5c5\uccb4" + }, + "description": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "title": "Neato \uacc4\uc815 \uc815\ubcf4" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/nn.json b/homeassistant/components/neato/.translations/nn.json new file mode 100644 index 00000000000000..e04e73aef24ea3 --- /dev/null +++ b/homeassistant/components/neato/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/ko.json b/homeassistant/components/opentherm_gw/.translations/ko.json new file mode 100644 index 00000000000000..85790702435b1c --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "OpenTherm Gateway \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "id_exists": "OpenTherm Gateway id \uac00 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4", + "serial_error": "\uae30\uae30 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "timeout": "\uc5f0\uacb0 \uc2dc\ub3c4 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "device": "\uacbd\ub85c \ub610\ub294 URL", + "floor_temperature": "\uc9c0\uba74 \uae30\ud6c4 \uc628\ub3c4", + "id": "ID", + "name": "\uc774\ub984", + "precision": "\uae30\ud6c4 \uc628\ub3c4 \uc815\ubc00\ub3c4" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nn.json b/homeassistant/components/opentherm_gw/.translations/nn.json new file mode 100644 index 00000000000000..3d018a2292d0c2 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "id": "ID", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/nn.json b/homeassistant/components/owntracks/.translations/nn.json new file mode 100644 index 00000000000000..cdfd651beecffc --- /dev/null +++ b/homeassistant/components/owntracks/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/nn.json b/homeassistant/components/plaato/.translations/nn.json new file mode 100644 index 00000000000000..750e14b1daeeaa --- /dev/null +++ b/homeassistant/components/plaato/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json index 210fe7323602c3..95083102273a1b 100644 --- a/homeassistant/components/plex/.translations/de.json +++ b/homeassistant/components/plex/.translations/de.json @@ -1,9 +1,26 @@ { "config": { + "abort": { + "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden" + }, "step": { + "manual_setup": { + "title": "Plex Server" + }, + "start_website_auth": { + "description": "Weiter zur Autorisierung unter plex.tv.", + "title": "Plex Server verbinden" + }, "user": { "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." } } + }, + "options": { + "step": { + "plex_mp_settings": { + "description": "Optionen f\u00fcr Plex-Media-Player" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json index 45417d09d02127..261ca9514905ec 100644 --- a/homeassistant/components/plex/.translations/es.json +++ b/homeassistant/components/plex/.translations/es.json @@ -4,6 +4,7 @@ "all_configured": "Todos los servidores vinculados ya configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "Plex se est\u00e1 configurando", + "discovery_no_file": "No se ha encontrado ning\u00fan archivo de configuraci\u00f3n antiguo", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Fall\u00f3 por razones desconocidas" @@ -32,6 +33,10 @@ "description": "Varios servidores disponibles, seleccione uno:", "title": "Seleccione el servidor Plex" }, + "start_website_auth": { + "description": "Contin\u00fae en plex.tv para autorizar", + "title": "Conectar servidor Plex" + }, "user": { "data": { "manual_setup": "Configuraci\u00f3n manual", diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json index 171c656566d279..f8e78945802bd1 100644 --- a/homeassistant/components/plex/.translations/ko.json +++ b/homeassistant/components/plex/.translations/ko.json @@ -4,15 +4,28 @@ "all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84", "already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", + "discovery_no_file": "\ub808\uac70\uc2dc \uad6c\uc131 \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "token_request_timeout": "\ud1a0\ud070 \ud68d\ub4dd \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4", "no_servers": "\uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc11c\ubc84\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "no_token": "\ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc218\ub3d9 \uc124\uc815\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", "not_found": "Plex \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "step": { + "manual_setup": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8", + "ssl": "SSL \uc0ac\uc6a9", + "token": "\ud1a0\ud070 (\ud544\uc694\ud55c \uacbd\uc6b0)", + "verify_ssl": "SSL \uc778\uc99d\uc11c \uac80\uc99d" + }, + "title": "Plex \uc11c\ubc84" + }, "select_server": { "data": { "server": "\uc11c\ubc84" @@ -20,14 +33,30 @@ "description": "\uc5ec\ub7ec \uc11c\ubc84\uac00 \uc0ac\uc6a9 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", "title": "Plex \uc11c\ubc84 \uc120\ud0dd" }, + "start_website_auth": { + "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud574\uc8fc\uc138\uc694.", + "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" + }, "user": { "data": { + "manual_setup": "\uc218\ub3d9 \uc124\uc815", "token": "Plex \ud1a0\ud070" }, - "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ud574\uc8fc\uc138\uc694.", + "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "\ubaa8\ub4e0 \ucee8\ud2b8\ub864 \ud45c\uc2dc\ud558\uae30", + "use_episode_art": "\uc5d0\ud53c\uc18c\ub4dc \uc544\ud2b8 \uc0ac\uc6a9" + }, + "description": "Plex \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4 \uc635\uc158" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 1795ef6b6d358a..7b0f7232976a67 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -4,6 +4,7 @@ "all_configured": "All verbonne Server sinn scho konfigur\u00e9iert", "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert", "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", + "discovery_no_file": "Kee Konfiguratioun Fichier am ale Format fonnt.", "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", "unknown": "Onbekannte Feeler opgetrueden" @@ -32,6 +33,10 @@ "description": "M\u00e9i Server disponibel, wielt een aus:", "title": "Plex Server auswielen" }, + "start_website_auth": { + "description": "Weiderfueren op plex.tv fir d'Autorisatioun.", + "title": "Plex Server verbannen" + }, "user": { "data": { "manual_setup": "Manuell Konfiguratioun", diff --git a/homeassistant/components/plex/.translations/nn.json b/homeassistant/components/plex/.translations/nn.json new file mode 100644 index 00000000000000..a16deb2fca2899 --- /dev/null +++ b/homeassistant/components/plex/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index a0a9d087d1e58b..18c4e865a84690 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -4,6 +4,7 @@ "all_configured": "Alle knyttet servere som allerede er konfigurert", "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Plex blir konfigurert", + "discovery_no_file": "Ingen eldre konfigurasjonsfil ble funnet", "invalid_import": "Den importerte konfigurasjonen er ugyldig", "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Mislyktes av ukjent \u00e5rsak" @@ -32,6 +33,10 @@ "description": "Flere servere tilgjengelig, velg en:", "title": "Velg Plex-server" }, + "start_website_auth": { + "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv.", + "title": "Koble til Plex server" + }, "user": { "data": { "manual_setup": "Manuelt oppsett", diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 53b4bfd9bb59bc..fe773f72be9721 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -4,6 +4,7 @@ "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", + "discovery_no_file": "\u0421\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430", "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" @@ -32,6 +33,10 @@ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u0438\u043d \u0438\u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432:", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" }, + "start_website_auth": { + "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", + "title": "Plex" + }, "user": { "data": { "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index d6bc85725ebb5d..9be270a017ce4d 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -4,6 +4,7 @@ "all_configured": "Vsi povezani stre\u017eniki so \u017ee konfigurirani", "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", "already_in_progress": "Plex se konfigurira", + "discovery_no_file": "Podatkovne konfiguracijske datoteke ni bilo mogo\u010de najti", "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", "unknown": "Ni uspelo iz neznanega razloga" @@ -32,6 +33,10 @@ "description": "Na voljo je ve\u010d stre\u017enikov, izberite enega:", "title": "Izberite stre\u017enik Plex" }, + "start_website_auth": { + "description": "Nadaljujte z avtorizacijo na plex.tv.", + "title": "Pove\u017eite stre\u017enik Plex" + }, "user": { "data": { "manual_setup": "Ro\u010dna nastavitev", diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index a0a033651a57bb..2d4ce1ea6aa2c5 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -4,6 +4,7 @@ "all_configured": "\u6240\u6709\u7d81\u5b9a\u4f3a\u670d\u5668\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210", "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", + "discovery_no_file": "\u627e\u4e0d\u5230\u820a\u7248\u8a2d\u5b9a\u6a94\u6848", "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" @@ -32,6 +33,10 @@ "description": "\u627e\u5230\u591a\u500b\u4f3a\u670d\u5668\uff0c\u8acb\u9078\u64c7\u4e00\u7d44\uff1a", "title": "\u9078\u64c7 Plex \u4f3a\u670d\u5668" }, + "start_website_auth": { + "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u3002", + "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" + }, "user": { "data": { "manual_setup": "\u624b\u52d5\u8a2d\u5b9a", diff --git a/homeassistant/components/point/.translations/nn.json b/homeassistant/components/point/.translations/nn.json new file mode 100644 index 00000000000000..865155c04949a7 --- /dev/null +++ b/homeassistant/components/point/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Minut Point" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/nn.json b/homeassistant/components/ps4/.translations/nn.json index b3302389c88c9a..86920906003ecc 100644 --- a/homeassistant/components/ps4/.translations/nn.json +++ b/homeassistant/components/ps4/.translations/nn.json @@ -5,9 +5,20 @@ "port_997_bind_error": "Kunne ikkje binde til port 997. Sj\u00e5 [dokumentasjonen] (https://www.home-assistant.io/components/ps4/) for ytterlegare informasjon." }, "step": { + "creds": { + "title": "Playstation 4" + }, + "link": { + "data": { + "code": "PIN", + "name": "Namn" + }, + "title": "Playstation 4" + }, "mode": { "title": "Playstation 4" } - } + }, + "title": "Playstation 4" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json new file mode 100644 index 00000000000000..1f248099df34e5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/de.json @@ -0,0 +1,21 @@ +{ + "device_automation": { + "condition_type": { + "is_humidity": "{entity_name} Feuchtigkeit", + "is_pressure": "{entity_name} Druck", + "is_signal_strength": "{entity_name} Signalst\u00e4rke", + "is_temperature": "{entity_name} Temperatur", + "is_timestamp": "{entity_name} Zeitstempel", + "is_value": "{entity_name} Wert" + }, + "trigger_type": { + "battery_level": "{entity_name} Batteriestatus", + "humidity": "{entity_name} Feuchtigkeit", + "pressure": "{entity_name} Druck", + "signal_strength": "{entity_name} Signalst\u00e4rke", + "temperature": "{entity_name} Temperatur", + "timestamp": "{entity_name} Zeitstempel", + "value": "{entity_name} Wert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/ko.json b/homeassistant/components/sensor/.translations/ko.json new file mode 100644 index 00000000000000..d24a40583434e9 --- /dev/null +++ b/homeassistant/components/sensor/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", + "is_humidity": "{entity_name} \uc2b5\ub3c4", + "is_illuminance": "{entity_name} \uc870\ub3c4", + "is_power": "{entity_name} \uc18c\ube44 \uc804\ub825", + "is_pressure": "{entity_name} \uc555\ub825", + "is_signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4", + "is_temperature": "{entity_name} \uc628\ub3c4", + "is_timestamp": "{entity_name} \uc2dc\uac01", + "is_value": "{entity_name} \uac12" + }, + "trigger_type": { + "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", + "humidity": "{entity_name} \uc2b5\ub3c4", + "illuminance": "{entity_name} \uc870\ub3c4", + "power": "{entity_name} \uc18c\ube44 \uc804\ub825", + "pressure": "{entity_name} \uc555\ub825", + "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4", + "temperature": "{entity_name} \uc628\ub3c4", + "timestamp": "{entity_name} \uc2dc\uac01", + "value": "{entity_name} \uac12" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/sl.json b/homeassistant/components/sensor/.translations/sl.json index 472af1dfe3b9ea..e3bc994b6ea9b5 100644 --- a/homeassistant/components/sensor/.translations/sl.json +++ b/homeassistant/components/sensor/.translations/sl.json @@ -7,7 +7,20 @@ "is_power": "{entity_name} mo\u010d", "is_pressure": "{entity_name} pritisk", "is_signal_strength": "{entity_name} jakost signala", - "is_temperature": "{entity_name} temperatura" + "is_temperature": "{entity_name} temperatura", + "is_timestamp": "{entity_name} \u010dasovni \u017eig", + "is_value": "{entity_name} vrednost" + }, + "trigger_type": { + "battery_level": "{entity_name} raven baterije", + "humidity": "{entity_name} vla\u017enost", + "illuminance": "{entity_name} osvetljenosti", + "power": "{entity_name} mo\u010d", + "pressure": "{entity_name} tlak", + "signal_strength": "{entity_name} jakost signala", + "temperature": "{entity_name} temperatura", + "timestamp": "{entity_name} \u010dasovni \u017eig", + "value": "{entity_name} vrednost" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/nn.json b/homeassistant/components/simplisafe/.translations/nn.json new file mode 100644 index 00000000000000..0568cad3f6dc4b --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json new file mode 100644 index 00000000000000..d93eec8aed7442 --- /dev/null +++ b/homeassistant/components/soma/.translations/de.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/nn.json b/homeassistant/components/soma/.translations/nn.json new file mode 100644 index 00000000000000..6eeb4f75a3c5c4 --- /dev/null +++ b/homeassistant/components/soma/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/nn.json b/homeassistant/components/somfy/.translations/nn.json new file mode 100644 index 00000000000000..ff0383c7f015cf --- /dev/null +++ b/homeassistant/components/somfy/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/nn.json b/homeassistant/components/tellduslive/.translations/nn.json new file mode 100644 index 00000000000000..934f56a420b353 --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Telldus Live" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/nn.json b/homeassistant/components/toon/.translations/nn.json index b8dbeff27cacce..eed288a5e39a33 100644 --- a/homeassistant/components/toon/.translations/nn.json +++ b/homeassistant/components/toon/.translations/nn.json @@ -1,5 +1,12 @@ { "config": { + "step": { + "authenticate": { + "data": { + "username": "Brukarnamn" + } + } + }, "title": "Toon" } } \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/nn.json b/homeassistant/components/tplink/.translations/nn.json new file mode 100644 index 00000000000000..1d9fb41fc8ce11 --- /dev/null +++ b/homeassistant/components/tplink/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/nn.json b/homeassistant/components/traccar/.translations/nn.json new file mode 100644 index 00000000000000..9fc23b3e394074 --- /dev/null +++ b/homeassistant/components/traccar/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json new file mode 100644 index 00000000000000..ed0342b9430924 --- /dev/null +++ b/homeassistant/components/transmission/.translations/de.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "error": { + "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "wrong_credentials": "Falscher Benutzername oder Kennwort" + }, + "step": { + "options": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + }, + "title": "Konfigurationsoptionen" + }, + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index e1bc8dc322824a..67461d1a3e8565 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -20,7 +20,7 @@ "name": "Name", "password": "Password", "port": "Port", - "username": "User name" + "username": "Username" }, "title": "Setup Transmission Client" } diff --git a/homeassistant/components/transmission/.translations/ko.json b/homeassistant/components/transmission/.translations/ko.json new file mode 100644 index 00000000000000..a9b1b369f9029e --- /dev/null +++ b/homeassistant/components/transmission/.translations/ko.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "wrong_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "options": { + "data": { + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" + }, + "title": "\uc635\uc158 \uc124\uc815" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "Transmission \ud074\ub77c\uc774\uc5b8\ud2b8 \uc124\uc815" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" + }, + "description": "Transmission \uc635\uc158 \uc124\uc815" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/nn.json b/homeassistant/components/transmission/.translations/nn.json new file mode 100644 index 00000000000000..7622ac1b459096 --- /dev/null +++ b/homeassistant/components/transmission/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn", + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/nn.json b/homeassistant/components/twentemilieu/.translations/nn.json new file mode 100644 index 00000000000000..02ac8ecf27a2e9 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/nn.json b/homeassistant/components/twilio/.translations/nn.json new file mode 100644 index 00000000000000..86e5d9051b339c --- /dev/null +++ b/homeassistant/components/twilio/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index e447e89644f5b5..32a378b7c00c90 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -38,6 +38,11 @@ "one": "eins", "other": "andere" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Erstellen von Bandbreiten-Nutzungssensoren f\u00fcr Netzwerk-Clients" + } } } } diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 0539f5607b4c36..1db6712142d5d8 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -38,6 +38,11 @@ "one": "uno", "other": "otro" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Crear sensores para monitorizar uso de ancho de banda de clientes de red" + } } } } diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json index 1fff9887906b53..295430b7284072 100644 --- a/homeassistant/components/unifi/.translations/ko.json +++ b/homeassistant/components/unifi/.translations/ko.json @@ -32,6 +32,11 @@ "track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)", "track_wired_clients": "\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ud3ec\ud568" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \uc704\ud55c \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c \uc0dd\uc131\ud558\uae30" + } } } } diff --git a/homeassistant/components/unifi/.translations/nn.json b/homeassistant/components/unifi/.translations/nn.json new file mode 100644 index 00000000000000..7c129cba3afc71 --- /dev/null +++ b/homeassistant/components/unifi/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/nn.json b/homeassistant/components/upnp/.translations/nn.json index 286efcf0353076..cfbedd994afa43 100644 --- a/homeassistant/components/upnp/.translations/nn.json +++ b/homeassistant/components/upnp/.translations/nn.json @@ -11,6 +11,7 @@ "init": { "title": "UPnP / IGD" } - } + }, + "title": "UPnP / IGD" } } \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/nn.json b/homeassistant/components/vesync/.translations/nn.json new file mode 100644 index 00000000000000..372e37133b12c6 --- /dev/null +++ b/homeassistant/components/vesync/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/.translations/nn.json b/homeassistant/components/wemo/.translations/nn.json new file mode 100644 index 00000000000000..c1c8830cb25618 --- /dev/null +++ b/homeassistant/components/wemo/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Wemo" + } + }, + "title": "Wemo" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nn.json b/homeassistant/components/withings/.translations/nn.json new file mode 100644 index 00000000000000..7d8b268367c374 --- /dev/null +++ b/homeassistant/components/withings/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 280c941b427f4e..9ffd5211a1fca0 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -16,5 +16,13 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "close": "Schlie\u00dfen", + "left": "Links", + "open": "Offen", + "right": "Rechts" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json index 44f45f43570f8a..7ed1a8c69b473f 100644 --- a/homeassistant/components/zha/.translations/ko.json +++ b/homeassistant/components/zha/.translations/ko.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "\ube44\uc0c1", + "warn": "\uacbd\uace0" + }, + "trigger_subtype": { + "both_buttons": "\ub450 \uac1c", + "button_1": "\uccab \ubc88\uc9f8", + "button_2": "\ub450 \ubc88\uc9f8", + "button_3": "\uc138 \ubc88\uc9f8", + "button_4": "\ub124 \ubc88\uc9f8", + "button_5": "\ub2e4\uc12f \ubc88\uc9f8", + "button_6": "\uc5ec\uc12f \ubc88\uc9f8", + "close": "\ub2eb\uae30", + "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30", + "dim_up": "\ubc1d\uac8c \ud558\uae30", + "face_1": "\uba74 1\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_2": "\uba74 2\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_3": "\uba74 3\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_4": "\uba74 4\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_5": "\uba74 5\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_6": "\uba74 6\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_any": "\uc784\uc758\uc758 \uba74 \ub610\ub294 \ud2b9\uc815 \uba74 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "left": "\uc67c\ucabd", + "open": "\uc5f4\uae30", + "right": "\uc624\ub978\ucabd", + "turn_off": "\ub044\uae30", + "turn_on": "\ucf1c\uae30" + }, + "trigger_type": { + "device_dropped": "\uc7a5\uce58\ub97c \ub5a8\uc5b4\ub728\ub9bc", + "device_flipped": "\"{subtype}\" \uae30\uae30\ub97c \ub4a4\uc9d1\uc74c", + "device_knocked": "\"{subtype}\" \uae30\uae30\ub97c \ub450\ub4dc\ub9bc", + "device_rotated": "\"{subtype}\" \uae30\uae30\ub97c \ud68c\uc804", + "device_shaken": "\uae30\uae30 \ud754\ub4e6", + "device_slid": "\"{subtype}\" \uae30\uae30\ub97c \uc2ac\ub77c\uc774\ub4dc", + "device_tilted": "\uae30\uae30\ub97c \uae30\uc6b8\uc784", + "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", + "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", + "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", + "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", + "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nn.json b/homeassistant/components/zha/.translations/nn.json new file mode 100644 index 00000000000000..ad2c240baf18f2 --- /dev/null +++ b/homeassistant/components/zha/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/nn.json b/homeassistant/components/zwave/.translations/nn.json index ebd9d44796c7d2..8d1c737170f35e 100644 --- a/homeassistant/components/zwave/.translations/nn.json +++ b/homeassistant/components/zwave/.translations/nn.json @@ -4,6 +4,7 @@ "user": { "description": "Sj\u00e5 [www.home-assistant.io/docs/z-wave/installation/](https://www.home-assistant.io/docs/z-wave/installation/) for informasjon om konfigurasjonsvariablene." } - } + }, + "title": "Z-Wave" } } \ No newline at end of file From 3d937bfd8ac4d45d8119deb6c2e8c85d5e1c3164 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 9 Oct 2019 19:57:51 +1100 Subject: [PATCH 0700/3953] move import to top-level (#27348) --- homeassistant/components/workday/binary_sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 19edf231624bfc..3ca2afcc749087 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -2,6 +2,7 @@ import logging from datetime import datetime, timedelta +import holidays import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -141,8 +142,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Workday sensor.""" - import holidays - sensor_name = config.get(CONF_NAME) country = config.get(CONF_COUNTRY) province = config.get(CONF_PROVINCE) From 9ea58b970e759d909c7133a6ef29894a11a38e7a Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 9 Oct 2019 21:02:09 +1100 Subject: [PATCH 0701/3953] Move imports in caldav component (#27349) --- homeassistant/components/caldav/calendar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 6251679b22512f..2bbff2a6bc78e3 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -4,6 +4,7 @@ import logging import re +import caldav import voluptuous as vol from homeassistant.components.calendar import ( @@ -62,8 +63,6 @@ def setup_platform(hass, config, add_entities, disc_info=None): """Set up the WebDav Calendar platform.""" - import caldav - url = config[CONF_URL] username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) From 1257706bd9d4e314f7161da2d1322b2a83096f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Wed, 9 Oct 2019 14:47:43 +0200 Subject: [PATCH 0702/3953] Update zigpy-zigate to 0.4.1 (#27345) * Update zigpy-zigate to 0.4.1 Fix #27297 * Update zigpy-zigate to 0.4.1 Fix #27297 --- 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 59d9508ac338d8..9790fbffd06312 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.5.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", - "zigpy-zigate==0.4.0" + "zigpy-zigate==0.4.1" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index 7d4ebb0074e6be..fc2d498da7b554 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2044,7 +2044,7 @@ zigpy-homeassistant==0.9.0 zigpy-xbee-homeassistant==0.5.0 # homeassistant.components.zha -zigpy-zigate==0.4.0 +zigpy-zigate==0.4.1 # homeassistant.components.zoneminder zm-py==0.3.3 From 3194dd34562c98bf564cf8ff4a9d38a2666d70d9 Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Wed, 9 Oct 2019 19:58:36 +0200 Subject: [PATCH 0703/3953] Add documentation for logger.set_level service (#27211) * Add set_level doc * use only yaml * reformat * improvements --- homeassistant/components/logger/services.yaml | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml index 4d1ba649d36517..514aac4c71cb0b 100644 --- a/homeassistant/components/logger/services.yaml +++ b/homeassistant/components/logger/services.yaml @@ -1,6 +1,22 @@ set_default_level: description: Set the default log level for components. fields: - level: {description: 'Default severity level. Possible values are notset, debug, - info, warn, warning, error, fatal, critical', example: debug} -set_level: {description: Set log level for components.} + level: + description: "Default severity level. Possible values are debug, info, warn, warning, error, fatal, critical" + example: debug + +set_level: + description: Set log level for components. + fields: + homeassistant.core: + description: "Example on how to change the logging level for a Home Assistant core components. Possible values are debug, info, warn, warning, error, fatal, critical" + example: debug + homeassistant.components.mqtt: + description: "Example on how to change the logging level for an Integration. Possible values are debug, info, warn, warning, error, fatal, critical" + example: warning + custom_components.my_integration: + description: "Example on how to change the logging level for a Custom Integration. Possible values are debug, info, warn, warning, error, fatal, critical" + example: debug + aiohttp: + description: "Example on how to change the logging level for a Python module. Possible values are debug, info, warn, warning, error, fatal, critical" + example: error From fdf4f398a79e775e11dc9fdd391a8b53f7b773c5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Oct 2019 21:04:11 +0200 Subject: [PATCH 0704/3953] Support async validation of device trigger (#27333) --- .../components/automation/__init__.py | 8 +- homeassistant/components/automation/config.py | 10 +- homeassistant/components/automation/device.py | 3 + .../components/deconz/device_trigger.py | 16 ++- .../components/zha/device_trigger.py | 14 ++- tests/components/zha/test_device_trigger.py | 104 +++++++++--------- 6 files changed, 89 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f669d415854b9d..3409ce832ddb33 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -7,9 +7,6 @@ import voluptuous as vol -from homeassistant.components.device_automation.exceptions import ( - InvalidDeviceAutomationConfig, -) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -476,10 +473,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): for conf in trigger_configs: platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) - try: - remove = await platform.async_attach_trigger(hass, conf, action, info) - except InvalidDeviceAutomationConfig: - remove = False + remove = await platform.async_attach_trigger(hass, conf, action, info) if not remove: _LOGGER.error("Error setting up trigger %s", name) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index ebbd1771e84efd..5733cd2e83ec2a 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -4,6 +4,9 @@ import voluptuous as vol +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.const import CONF_PLATFORM from homeassistant.config import async_log_exception, config_without_domain from homeassistant.exceptions import HomeAssistantError @@ -52,7 +55,12 @@ async def _try_async_validate_config_item(hass, config, full_config=None): """Validate config item.""" try: config = await async_validate_config_item(hass, config, full_config) - except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: + except ( + vol.Invalid, + HomeAssistantError, + IntegrationNotFound, + InvalidDeviceAutomationConfig, + ) as ex: async_log_exception(ex, DOMAIN, full_config or config, hass) return None diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index dc65008c3fb268..ced8f65cbf5379 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -18,6 +18,9 @@ async def async_validate_trigger_config(hass, config): platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], "trigger" ) + if hasattr(platform, "async_validate_trigger_config"): + return await getattr(platform, "async_validate_trigger_config")(hass, config) + return platform.TRIGGER_SCHEMA(config) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 9f66cf156aab9e..27ff6fcd590ad9 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -204,8 +204,10 @@ def _get_deconz_event_from_device_id(hass, device_id): return None -async def async_attach_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" +async def async_validate_trigger_config(hass, config): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + device_registry = await hass.helpers.device_registry.async_get_registry() device = device_registry.async_get(config[CONF_DEVICE_ID]) @@ -214,6 +216,16 @@ async def async_attach_trigger(hass, config, action, automation_info): if device.model not in REMOTES or trigger not in REMOTES[device.model]: raise InvalidDeviceAutomationConfig + return config + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(config[CONF_DEVICE_ID]) + + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + trigger = REMOTES[device.model][trigger] deconz_event = _get_deconz_event_from_device_id(hass, device.id) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index ddf7465e0c064e..8d74ae108a2fb7 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -21,8 +21,10 @@ ) -async def async_attach_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" +async def async_validate_trigger_config(hass, config): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) @@ -32,6 +34,14 @@ async def async_attach_trigger(hass, config, action, automation_info): ): raise InvalidDeviceAutomationConfig + return config + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + trigger = zha_device.device_automation_triggers[trigger] event_config = { diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 2f4ddb6b8b2cf3..8df1a072801ff8 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,6 +1,4 @@ """ZHA device automation trigger tests.""" -from unittest.mock import patch - import pytest import homeassistant.components.automation as automation @@ -197,7 +195,7 @@ async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): assert calls[0].data["message"] == "service called" -async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls): +async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, caplog): """Test for exception on event triggers firing.""" from zigpy.zcl.clusters.general import OnOff, Basic @@ -219,33 +217,32 @@ async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - with patch("logging.Logger.error") as mock: - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "device_id": reg_device.id, - "domain": "zha", - "platform": "device", - "type": "junk", - "subtype": "junk", - }, - "action": { - "service": "test.automation", - "data": {"message": "service called"}, - }, - } - ] - }, - ) - await hass.async_block_till_done() - mock.assert_called_with("Error setting up trigger %s", "automation 0") - - -async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls): + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert "Invalid config for [automation]" in caplog.text + + +async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls, caplog): """Test for exception on event triggers firing.""" from zigpy.zcl.clusters.general import OnOff, Basic @@ -275,27 +272,26 @@ async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - with patch("logging.Logger.error") as mock: - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "device_id": reg_device.id, - "domain": "zha", - "platform": "device", - "type": "junk", - "subtype": "junk", - }, - "action": { - "service": "test.automation", - "data": {"message": "service called"}, - }, - } - ] - }, - ) - await hass.async_block_till_done() - mock.assert_called_with("Error setting up trigger %s", "automation 0") + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert "Invalid config for [automation]" in caplog.text From a8db8d8c0b02cd7f259fe1fcf6fff3c55050b9bc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 9 Oct 2019 21:44:02 +0200 Subject: [PATCH 0705/3953] deCONZ - Update discovery address (#27365) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 1e5cd4144253d2..63ab17d001acec 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==63" + "pydeconz==64" ], "ssdp": { "manufacturer": [ diff --git a/requirements_all.txt b/requirements_all.txt index fc2d498da7b554..f17a70057b87f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1135,7 +1135,7 @@ pydaikin==1.6.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==63 +pydeconz==64 # homeassistant.components.delijn pydelijn==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 048601b3b99635..328675e89bc45c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -300,7 +300,7 @@ pybotvac==0.0.16 pychromecast==4.0.1 # homeassistant.components.deconz -pydeconz==63 +pydeconz==64 # homeassistant.components.zwave pydispatcher==2.0.5 From 78e9bba279878a581a494da534617aebaae61f90 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Wed, 9 Oct 2019 21:56:16 +0200 Subject: [PATCH 0706/3953] Refactor Tradfri constants (#27334) * Refactor constants * Rename constant * Rename constant * Rename constant * Review update * Remove duplicate constant * Reorder constants * Dont refresh features * Order package imports * Fix bug * Put back features in refresh * Fix import order * Refactor supported features * Refactor supported features, take 2 --- homeassistant/components/tradfri/__init__.py | 46 ++++++++---------- .../components/tradfri/base_class.py | 6 +-- .../components/tradfri/config_flow.py | 5 +- homeassistant/components/tradfri/const.py | 22 ++++++++- homeassistant/components/tradfri/cover.py | 7 ++- homeassistant/components/tradfri/light.py | 48 +++++++++---------- homeassistant/components/tradfri/sensor.py | 5 +- homeassistant/components/tradfri/switch.py | 3 +- 8 files changed, 73 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index bca91134bedf47..c719fa4161423b 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -3,12 +3,22 @@ import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP -import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json - +from . import config_flow # noqa pylint_disable=unused-import from .const import ( + DOMAIN, + CONFIG_FILE, + KEY_GATEWAY, + KEY_API, + CONF_ALLOW_TRADFRI_GROUPS, + DEFAULT_ALLOW_TRADFRI_GROUPS, + TRADFRI_DEVICE_TYPES, + ATTR_TRADFRI_MANUFACTURER, + ATTR_TRADFRI_GATEWAY, + ATTR_TRADFRI_GATEWAY_MODEL, CONF_IMPORT_GROUPS, CONF_IDENTITY, CONF_HOST, @@ -16,18 +26,8 @@ CONF_GATEWAY_ID, ) -from . import config_flow # noqa pylint_disable=unused-import - _LOGGER = logging.getLogger(__name__) - -DOMAIN = "tradfri" -CONFIG_FILE = ".tradfri_psk.conf" -KEY_GATEWAY = "tradfri_gateway" -KEY_API = "tradfri_api" -CONF_ALLOW_TRADFRI_GROUPS = "allow_tradfri_groups" -DEFAULT_ALLOW_TRADFRI_GROUPS = False - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -124,24 +124,16 @@ async def on_hass_stop(event): config_entry_id=entry.entry_id, connections=set(), identifiers={(DOMAIN, entry.data[CONF_GATEWAY_ID])}, - manufacturer="IKEA", - name="Gateway", + manufacturer=ATTR_TRADFRI_MANUFACTURER, + name=ATTR_TRADFRI_GATEWAY, # They just have 1 gateway model. Type is not exposed yet. - model="E1526", + model=ATTR_TRADFRI_GATEWAY_MODEL, sw_version=gateway_info.firmware_version, ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "cover") - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "light") - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "switch") - ) + for device in TRADFRI_DEVICE_TYPES: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, device) + ) return True diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index aa8487b087ec5e..8430a342c09d3c 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -5,7 +5,7 @@ from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from . import DOMAIN as TRADFRI_DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -60,12 +60,12 @@ def device_info(self): info = self._device.device_info return { - "identifiers": {(TRADFRI_DOMAIN, self._device.id)}, + "identifiers": {(DOMAIN, self._device.id)}, "manufacturer": info.manufacturer, "model": info.model_number, "name": self._name, "sw_version": info.firmware_version, - "via_device": (TRADFRI_DOMAIN, self._gateway_id), + "via_device": (DOMAIN, self._gateway_id), } @property diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 9da381deb75658..bdb195cf53f514 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -7,18 +7,15 @@ import voluptuous as vol from homeassistant import config_entries - from .const import ( CONF_IMPORT_GROUPS, CONF_IDENTITY, CONF_HOST, CONF_KEY, CONF_GATEWAY_ID, + KEY_SECURITY_CODE, ) -KEY_SECURITY_CODE = "security_code" -KEY_IMPORT_GROUPS = "import_groups" - class AuthError(Exception): """Exception if authentication occurs.""" diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index d37b5d99f9fc69..a7acfcbf87625c 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -1,7 +1,25 @@ """Consts used by Tradfri.""" +from homeassistant.components.light import SUPPORT_TRANSITION, SUPPORT_BRIGHTNESS from homeassistant.const import CONF_HOST # noqa pylint: disable=unused-import -CONF_IMPORT_GROUPS = "import_groups" +ATTR_DIMMER = "dimmer" +ATTR_HUE = "hue" +ATTR_SAT = "saturation" +ATTR_TRADFRI_GATEWAY = "Gateway" +ATTR_TRADFRI_GATEWAY_MODEL = "E1526" +ATTR_TRADFRI_MANUFACTURER = "IKEA" +ATTR_TRANSITION_TIME = "transition_time" +CONF_ALLOW_TRADFRI_GROUPS = "allow_tradfri_groups" CONF_IDENTITY = "identity" -CONF_KEY = "key" +CONF_IMPORT_GROUPS = "import_groups" CONF_GATEWAY_ID = "gateway_id" +CONF_KEY = "key" +CONFIG_FILE = ".tradfri_psk.conf" +DEFAULT_ALLOW_TRADFRI_GROUPS = False +DOMAIN = "tradfri" +KEY_API = "tradfri_api" +KEY_GATEWAY = "tradfri_gateway" +KEY_SECURITY_CODE = "security_code" +SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION +SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION +TRADFRI_DEVICE_TYPES = ["cover", "light", "sensor", "switch"] diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 3dea978044fcae..1a3bf841665b4d 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -11,8 +11,7 @@ SUPPORT_SET_POSITION, ) from homeassistant.core import callback -from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY -from .const import CONF_GATEWAY_ID +from .const import DOMAIN, KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID _LOGGER = logging.getLogger(__name__) @@ -62,12 +61,12 @@ def device_info(self): info = self._cover.device_info return { - "identifiers": {(TRADFRI_DOMAIN, self._cover.id)}, + "identifiers": {(DOMAIN, self._cover.id)}, "name": self._name, "manufacturer": info.manufacturer, "model": info.model_number, "sw_version": info.firmware_version, - "via_device": (TRADFRI_DOMAIN, self._gateway_id), + "via_device": (DOMAIN, self._gateway_id), } async def async_added_to_hass(self): diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index f5d61f0aaed4d8..089f80223e8bab 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -9,29 +9,28 @@ ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, + Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_TRANSITION, - Light, ) -from homeassistant.components.tradfri.base_class import TradfriBaseDevice from homeassistant.core import callback -from . import KEY_API, KEY_GATEWAY -from .const import CONF_GATEWAY_ID, CONF_IMPORT_GROUPS +from .base_class import TradfriBaseDevice +from .const import ( + ATTR_DIMMER, + ATTR_HUE, + ATTR_SAT, + ATTR_TRANSITION_TIME, + SUPPORTED_LIGHT_FEATURES, + SUPPORTED_GROUP_FEATURES, + CONF_GATEWAY_ID, + CONF_IMPORT_GROUPS, + KEY_GATEWAY, + KEY_API, +) _LOGGER = logging.getLogger(__name__) -ATTR_DIMMER = "dimmer" -ATTR_HUE = "hue" -ATTR_SAT = "saturation" -ATTR_TRANSITION_TIME = "transition_time" -PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA -TRADFRI_LIGHT_MANAGER = "Tradfri Light Manager" -SUPPORTED_FEATURES = SUPPORT_TRANSITION -SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION - async def async_setup_entry(hass, config_entry, async_add_entities): """Load Tradfri lights based on a config entry.""" @@ -152,7 +151,16 @@ def __init__(self, device, api, gateway_id): super().__init__(device, api, gateway_id) self._unique_id = f"light-{gateway_id}-{device.id}" self._hs_color = None - self._features = SUPPORTED_FEATURES + + # Calculate supported features + _features = SUPPORTED_LIGHT_FEATURES + if device.light_control.can_set_dimmer: + _features |= SUPPORT_BRIGHTNESS + if device.light_control.can_set_color: + _features |= SUPPORT_COLOR + if device.light_control.can_set_temp: + _features |= SUPPORT_COLOR_TEMP + self._features = _features self._refresh(device) @@ -297,11 +305,3 @@ def _refresh(self, device): # Caching of LightControl and light object self._device_control = device.light_control self._device_data = device.light_control.lights[0] - self._features = SUPPORTED_FEATURES - - if device.light_control.can_set_dimmer: - self._features |= SUPPORT_BRIGHTNESS - if device.light_control.can_set_color: - self._features |= SUPPORT_COLOR - if device.light_control.can_set_temp: - self._features |= SUPPORT_COLOR_TEMP diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 7814daf8f7ad4f..56c1a464580ba2 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,10 +1,9 @@ """Support for IKEA Tradfri sensors.""" import logging -from homeassistant.components.tradfri.base_class import TradfriBaseDevice from homeassistant.const import DEVICE_CLASS_BATTERY -from . import KEY_API, KEY_GATEWAY -from .const import CONF_GATEWAY_ID +from .base_class import TradfriBaseDevice +from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 1e322ff47f5111..e1c549a1805f7f 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,8 +1,7 @@ """Support for IKEA Tradfri switches.""" from homeassistant.components.switch import SwitchDevice -from . import KEY_API, KEY_GATEWAY from .base_class import TradfriBaseDevice -from .const import CONF_GATEWAY_ID +from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID async def async_setup_entry(hass, config_entry, async_add_entities): From 74ef1358daa00330f10a93653fb9cc15161bb97b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 9 Oct 2019 23:06:27 +0200 Subject: [PATCH 0707/3953] Updated frontend to 20191002.2 (#27370) --- 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 58e5558781a2ed..67a66bc96125c2 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191002.1" + "home-assistant-frontend==20191002.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 99bb622e989b52..3f0588f2a99403 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.22 -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index f17a70057b87f0..cc1071762616d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -643,7 +643,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 328675e89bc45c..acc8de8a1b3609 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -185,7 +185,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From a57642833b78ee64763ef12787a80cf543b36ffb Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Tue, 8 Oct 2019 11:14:52 -0700 Subject: [PATCH 0708/3953] Fix connection issues with withings API by switching to a maintained codebase (#27310) * Fixing connection issues with withings API by switching to a maintained client codebase. * Updating requirements files. * Adding withings api to requirements script. --- homeassistant/components/withings/common.py | 14 +- .../components/withings/config_flow.py | 4 +- .../components/withings/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- script/gen_requirements_all.py | 2 +- tests/components/withings/common.py | 12 +- tests/components/withings/conftest.py | 139 +++++++++--------- tests/components/withings/test_common.py | 20 +-- tests/components/withings/test_sensor.py | 44 +++--- 10 files changed, 131 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index f2be849cbc794b..9acca6f0cd68e6 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import re import time -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import TokenUpdated @@ -68,7 +68,9 @@ class WithingsDataManager: service_available = None - def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): + def __init__( + self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi + ): """Constructor.""" self._hass = hass self._api = api @@ -253,7 +255,7 @@ def create_withings_data_manager( """Set up the sensor config entry.""" entry_creds = entry.data.get(const.CREDENTIALS) or {} profile = entry.data[const.PROFILE] - credentials = nokia.NokiaCredentials( + credentials = withings.WithingsCredentials( entry_creds.get("access_token"), entry_creds.get("token_expiry"), entry_creds.get("token_type"), @@ -266,7 +268,7 @@ def create_withings_data_manager( def credentials_saver(credentials_param): _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - # Sanitizing the data as sometimes a NokiaCredentials object + # Sanitizing the data as sometimes a WithingsCredentials object # is passed through from the API. cred_data = credentials_param if not isinstance(credentials_param, dict): @@ -275,8 +277,8 @@ def credentials_saver(credentials_param): entry.data[const.CREDENTIALS] = cred_data hass.config_entries.async_update_entry(entry, data={**entry.data}) - _LOGGER.debug("Creating nokia api instance") - api = nokia.NokiaApi( + _LOGGER.debug("Creating withings api instance") + api = withings.WithingsApi( credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index f28a4f59d80195..c781e785f5e122 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ from typing import Optional import aiohttp -import nokia +import withings_api as withings import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -75,7 +75,7 @@ def get_auth_client(self, profile: str): profile, ) - return nokia.NokiaAuth( + return withings.WithingsAuth( client_id, client_secret, callback_uri, diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index d38b69f2248bcc..ae5cd4bcdd9ee9 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "nokia==1.2.0" + "withings-api==2.0.0b7" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 7aa5090688c092..c9869b25530af3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,9 +865,6 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 @@ -1973,6 +1970,9 @@ websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 +# homeassistant.components.withings +withings-api==2.0.0b7 + # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69c6ded580688f..03a0c13dfce0b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,9 +228,6 @@ minio==4.0.9 # homeassistant.components.ssdp netdisco==2.6.0 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow @@ -448,6 +445,9 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==1.1.6 +# homeassistant.components.withings +withings-api==2.0.0b7 + # homeassistant.components.zeroconf zeroconf==0.23.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e35a83bd24d7ea..8a33baabc2954f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -105,7 +105,6 @@ "mficlient", "minio", "netdisco", - "nokia", "numpy", "oauth2client", "paho-mqtt", @@ -182,6 +181,7 @@ "vultr", "wakeonlan", "warrant", + "withings-api", "YesssSMS", "zeroconf", "zigpy-homeassistant", diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b8406c39711e57..f3839a1be5524c 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,7 +1,7 @@ """Common data for for the withings component tests.""" import time -import nokia +import withings_api as withings import homeassistant.components.withings.const as const @@ -92,7 +92,7 @@ def new_measure(type_str, value, unit): } -def nokia_sleep_response(states): +def withings_sleep_response(states): """Create a sleep response based on states.""" data = [] for state in states: @@ -104,10 +104,10 @@ def nokia_sleep_response(states): ) ) - return nokia.NokiaSleep(new_sleep_data("aa", data)) + return withings.WithingsSleep(new_sleep_data("aa", data)) -NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( +WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( { "updatetime": "", "timezone": "", @@ -174,7 +174,7 @@ def nokia_sleep_response(states): ) -NOKIA_SLEEP_RESPONSE = nokia_sleep_response( +WITHINGS_SLEEP_RESPONSE = withings_sleep_response( [ const.MEASURE_TYPE_SLEEP_STATE_AWAKE, const.MEASURE_TYPE_SLEEP_STATE_LIGHT, @@ -183,7 +183,7 @@ def nokia_sleep_response(states): ] ) -NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( +WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( { "series": [ new_sleep_summary( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 7cbe3dc1cd4cd0..0aa6af0d7c098a 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ from typing import Awaitable, Callable, List import asynctest -import nokia +import withings_api as withings import pytest import homeassistant.components.api as api @@ -15,9 +15,9 @@ from homeassistant.setup import async_setup_component from .common import ( - NOKIA_MEASURES_RESPONSE, - NOKIA_SLEEP_RESPONSE, - NOKIA_SLEEP_SUMMARY_RESPONSE, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_SLEEP_RESPONSE, + WITHINGS_SLEEP_SUMMARY_RESPONSE, ) @@ -34,17 +34,17 @@ def __init__( measures: List[str] = None, unit_system: str = None, throttle_interval: int = const.THROTTLE_INTERVAL, - nokia_request_response="DATA", - nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, - nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, - nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, + withings_request_response="DATA", + withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, + withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, + withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, ) -> None: """Constructor.""" self._throttle_interval = throttle_interval - self._nokia_request_response = nokia_request_response - self._nokia_measures_response = nokia_measures_response - self._nokia_sleep_response = nokia_sleep_response - self._nokia_sleep_summary_response = nokia_sleep_summary_response + self._withings_request_response = withings_request_response + self._withings_measures_response = withings_measures_response + self._withings_sleep_response = withings_sleep_response + self._withings_sleep_summary_response = withings_sleep_summary_response self._withings_config = { const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret", @@ -103,24 +103,24 @@ def throttle_interval(self): return self._throttle_interval @property - def nokia_request_response(self): + def withings_request_response(self): """Request response.""" - return self._nokia_request_response + return self._withings_request_response @property - def nokia_measures_response(self) -> nokia.NokiaMeasures: + def withings_measures_response(self) -> withings.WithingsMeasures: """Measures response.""" - return self._nokia_measures_response + return self._withings_measures_response @property - def nokia_sleep_response(self) -> nokia.NokiaSleep: + def withings_sleep_response(self) -> withings.WithingsSleep: """Sleep response.""" - return self._nokia_sleep_response + return self._withings_sleep_response @property - def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: + def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: """Sleep summary response.""" - return self._nokia_sleep_summary_response + return self._withings_sleep_summary_response class WithingsFactoryData: @@ -130,21 +130,21 @@ def __init__( self, hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ): """Constructor.""" self._hass = hass self._flow_id = flow_id - self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock - self._nokia_api_request_mock = nokia_api_request_mock - self._nokia_api_get_measures_mock = nokia_api_get_measures_mock - self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock - self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock + self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock + self._withings_api_request_mock = withings_api_request_mock + self._withings_api_get_measures_mock = withings_api_get_measures_mock + self._withings_api_get_sleep_mock = withings_api_get_sleep_mock + self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock self._data_manager_get_throttle_interval_mock = ( data_manager_get_throttle_interval_mock ) @@ -160,29 +160,29 @@ def flow_id(self): return self._flow_id @property - def nokia_auth_get_credentials_mock(self): + def withings_auth_get_credentials_mock(self): """Get auth credentials mock.""" - return self._nokia_auth_get_credentials_mock + return self._withings_auth_get_credentials_mock @property - def nokia_api_request_mock(self): + def withings_api_request_mock(self): """Get request mock.""" - return self._nokia_api_request_mock + return self._withings_api_request_mock @property - def nokia_api_get_measures_mock(self): + def withings_api_get_measures_mock(self): """Get measures mock.""" - return self._nokia_api_get_measures_mock + return self._withings_api_get_measures_mock @property - def nokia_api_get_sleep_mock(self): + def withings_api_get_sleep_mock(self): """Get sleep mock.""" - return self._nokia_api_get_sleep_mock + return self._withings_api_get_sleep_mock @property - def nokia_api_get_sleep_summary_mock(self): + def withings_api_get_sleep_summary_mock(self): """Get sleep summary mock.""" - return self._nokia_api_get_sleep_summary_mock + return self._withings_api_get_sleep_summary_mock @property def data_manager_get_throttle_interval_mock(self): @@ -243,9 +243,9 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: assert await async_setup_component(hass, http.DOMAIN, config.hass_config) assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - nokia_auth_get_credentials_patch = asynctest.patch( - "nokia.NokiaAuth.get_credentials", - return_value=nokia.NokiaCredentials( + withings_auth_get_credentials_patch = asynctest.patch( + "withings_api.WithingsAuth.get_credentials", + return_value=withings.WithingsCredentials( access_token="my_access_token", token_expiry=time.time() + 600, token_type="my_token_type", @@ -255,28 +255,33 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: consumer_secret=config.withings_config.get(const.CLIENT_SECRET), ), ) - nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() + withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() - nokia_api_request_patch = asynctest.patch( - "nokia.NokiaApi.request", return_value=config.nokia_request_response + withings_api_request_patch = asynctest.patch( + "withings_api.WithingsApi.request", + return_value=config.withings_request_response, ) - nokia_api_request_mock = nokia_api_request_patch.start() + withings_api_request_mock = withings_api_request_patch.start() - nokia_api_get_measures_patch = asynctest.patch( - "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response + withings_api_get_measures_patch = asynctest.patch( + "withings_api.WithingsApi.get_measures", + return_value=config.withings_measures_response, ) - nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() + withings_api_get_measures_mock = withings_api_get_measures_patch.start() - nokia_api_get_sleep_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response + withings_api_get_sleep_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep", + return_value=config.withings_sleep_response, ) - nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() + withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() - nokia_api_get_sleep_summary_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep_summary", - return_value=config.nokia_sleep_summary_response, + withings_api_get_sleep_summary_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep_summary", + return_value=config.withings_sleep_summary_response, + ) + withings_api_get_sleep_summary_mock = ( + withings_api_get_sleep_summary_patch.start() ) - nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() data_manager_get_throttle_interval_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" @@ -295,11 +300,11 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: patches.extend( [ - nokia_auth_get_credentials_patch, - nokia_api_request_patch, - nokia_api_get_measures_patch, - nokia_api_get_sleep_patch, - nokia_api_get_sleep_summary_patch, + withings_auth_get_credentials_patch, + withings_api_request_patch, + withings_api_get_measures_patch, + withings_api_get_sleep_patch, + withings_api_get_sleep_summary_patch, data_manager_get_throttle_interval_patch, get_measures_patch, ] @@ -328,11 +333,11 @@ def create_task(*args): return WithingsFactoryData( hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index a22689f92bb6b5..9f2480f9094e55 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,6 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError import pytest from requests_oauthlib import TokenUpdated @@ -13,19 +13,19 @@ from homeassistant.exceptions import PlatformNotReady -@pytest.fixture(name="nokia_api") -def nokia_api_fixture(): - """Provide nokia api.""" - nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) - nokia_api.get_measures = MagicMock() - nokia_api.get_sleep = MagicMock() - return nokia_api +@pytest.fixture(name="withings_api") +def withings_api_fixture(): + """Provide withings api.""" + withings_api = withings.WithingsApi.__new__(withings.WithingsApi) + withings_api.get_measures = MagicMock() + withings_api.get_sleep = MagicMock() + return withings_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): +def data_manager_fixture(hass, withings_api: withings.WithingsApi): """Provide data manager.""" - return WithingsDataManager(hass, "My Profile", nokia_api) + return WithingsDataManager(hass, "My Profile", withings_api) def test_print_service(): diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index da77910097be89..697d0a8b86413f 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,7 +2,12 @@ from unittest.mock import MagicMock, patch import asynctest -from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary +from withings_api import ( + WithingsApi, + WithingsMeasures, + WithingsSleep, + WithingsSleepSummary, +) import pytest from homeassistant.components.withings import DOMAIN @@ -15,7 +20,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from .common import nokia_sleep_response +from .common import withings_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig @@ -120,9 +125,9 @@ async def test_health_sensor_state_none( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=None, - nokia_sleep_response=None, - nokia_sleep_summary_response=None, + withings_measures_response=None, + withings_sleep_response=None, + withings_sleep_summary_response=None, ) ) @@ -153,9 +158,9 @@ async def test_health_sensor_state_empty( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=NokiaMeasures({"measuregrps": []}), - nokia_sleep_response=NokiaSleep({"series": []}), - nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), + withings_measures_response=WithingsMeasures({"measuregrps": []}), + withings_sleep_response=WithingsSleep({"series": []}), + withings_sleep_summary_response=WithingsSleepSummary({"series": []}), ) ) @@ -201,7 +206,8 @@ async def test_sleep_state_throttled( data = await withings_factory( WithingsFactoryConfig( - measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) + measures=[measure], + withings_sleep_response=withings_sleep_response(sleep_states), ) ) @@ -257,16 +263,16 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): "expires_in": "2", } - original_nokia_api = NokiaApi - nokia_api_instance = None + original_withings_api = WithingsApi + withings_api_instance = None - def new_nokia_api(*args, **kwargs): - nonlocal nokia_api_instance - nokia_api_instance = original_nokia_api(*args, **kwargs) - nokia_api_instance.request = MagicMock() - return nokia_api_instance + def new_withings_api(*args, **kwargs): + nonlocal withings_api_instance + withings_api_instance = original_withings_api(*args, **kwargs) + withings_api_instance.request = MagicMock() + return withings_api_instance - nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) + withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( @@ -275,7 +281,7 @@ def new_nokia_api(*args, **kwargs): wraps=hass.config_entries.async_update_entry, ) - with session_patch, client_patch, nokia_api_patch, update_entry_patch: + with session_patch, client_patch, withings_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( @@ -298,7 +304,7 @@ def new_nokia_api(*args, **kwargs): await async_setup_entry(hass, config_entry, async_add_entities) - nokia_api_instance.set_token(expected_creds) + withings_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2" From f9c4bb04e3b16343f843a21ff58fc6607354303a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Wed, 9 Oct 2019 14:47:43 +0200 Subject: [PATCH 0709/3953] Update zigpy-zigate to 0.4.1 (#27345) * Update zigpy-zigate to 0.4.1 Fix #27297 * Update zigpy-zigate to 0.4.1 Fix #27297 --- 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 59d9508ac338d8..9790fbffd06312 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.5.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", - "zigpy-zigate==0.4.0" + "zigpy-zigate==0.4.1" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index c9869b25530af3..7a8ce0780b7ae2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2041,7 +2041,7 @@ zigpy-homeassistant==0.9.0 zigpy-xbee-homeassistant==0.5.0 # homeassistant.components.zha -zigpy-zigate==0.4.0 +zigpy-zigate==0.4.1 # homeassistant.components.zoneminder zm-py==0.3.3 From 4dc50107cd4cddb295c5cce1c0758df154b5d879 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 9 Oct 2019 23:06:27 +0200 Subject: [PATCH 0710/3953] Updated frontend to 20191002.2 (#27370) --- 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 58e5558781a2ed..67a66bc96125c2 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191002.1" + "home-assistant-frontend==20191002.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 04a68bf9633d4f..19acae64b16971 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.22 -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7a8ce0780b7ae2..44c10f8ff296ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -640,7 +640,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03a0c13dfce0b4..2a8f51cd1956cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -182,7 +182,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 6e86b8c42f1f46c45b74372335b0328734d0bd6c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Oct 2019 15:24:20 -0700 Subject: [PATCH 0711/3953] Bumped version to 0.100.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2ded4cde472f94..c5b566e5084fba 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 54c24de158daf3f03b684b0478b69e14b4780d9d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Oct 2019 16:16:29 -0700 Subject: [PATCH 0712/3953] Install requirements for all deps with tests (#27362) * Install requirements for all deps with tests * Remove unused REQUIREMENTS var * Print diff if not the same * Simplify * Update command line * Fix detecting empty dirs * Install non-integration * Fix upnp tests * Lint * Fix ZHA test --- .../components/epsonworkforce/sensor.py | 2 - .../components/ign_sismologia/geo_location.py | 2 - homeassistant/components/supla/__init__.py | 2 - homeassistant/components/upnp/device.py | 7 +- homeassistant/package_constraints.txt | 3 - requirements_test_all.txt | 192 +++++++++++- script/gen_requirements_all.py | 296 +++++------------- tests/components/upnp/test_init.py | 30 +- tests/components/zha/test_device_action.py | 2 +- 9 files changed, 284 insertions(+), 252 deletions(-) diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index 99e2723bf4af16..b310376e5cc55c 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -10,8 +10,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ["epsonprinter==0.0.9"] - _LOGGER = logging.getLogger(__name__) MONITORED_CONDITIONS = { "black": ["Ink level Black", "%", "mdi:water"], diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index 057d832b4faad8..8ad045c9f7a249 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -19,8 +19,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.event import track_time_interval -REQUIREMENTS = ["georss_ign_sismologia_client==0.2"] - _LOGGER = logging.getLogger(__name__) ATTR_EXTERNAL_ID = "external_id" diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 86e763142e6191..4293f187f5bb7c 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -9,8 +9,6 @@ from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ["pysupla==0.0.3"] - _LOGGER = logging.getLogger(__name__) DOMAIN = "supla" diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 5a5c7b38e7eef5..7f7f0f5b93a3a1 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -3,6 +3,7 @@ from ipaddress import IPv4Address import aiohttp +from async_upnp_client.profiles.igd import IgdDevice from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType @@ -29,9 +30,6 @@ async def async_discover(cls, hass: HomeAssistantType): if local_ip: local_ip = IPv4Address(local_ip) - # discover devices - from async_upnp_client.profiles.igd import IgdDevice - discovery_infos = await IgdDevice.async_search(source_ip=local_ip, timeout=10) # add extra info and store devices @@ -61,9 +59,6 @@ async def async_create_device(cls, hass: HomeAssistantType, ssdp_description: st factory = UpnpFactory(requester, disable_state_variable_validation=True) upnp_device = await factory.async_create_device(ssdp_description) - # wrap with async_upnp_client.IgdDevice - from async_upnp_client.profiles.igd import IgdDevice - igd_device = IgdDevice(upnp_device, None) return cls(igd_device) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f0588f2a99403..fb6239c807087e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,6 +33,3 @@ enum34==1000000000.0.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 - -# Contains code to modify Home Assistant to work around our rules -python-systemair-savecair==1000000000.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index acc8de8a1b3609..2ce7eeb54a4fc5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -30,15 +30,24 @@ HAP-python==2.6.0 # homeassistant.components.owntracks PyNaCl==1.3.0 +# homeassistant.auth.mfa_modules.totp +PyQRCode==1.2.1 + # homeassistant.components.rmvtransport PyRMVtransport==0.1.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 +# homeassistant.components.remember_the_milk +RtmAPI==0.7.0 + # homeassistant.components.yessssms YesssSMS==0.4.1 +# homeassistant.components.androidtv +adb-shell==0.0.4 + # homeassistant.components.adguard adguardhome==0.2.1 @@ -48,6 +57,9 @@ aio_geojson_geonetnz_quakes==0.10 # homeassistant.components.ambient_station aioambient==0.3.2 +# homeassistant.components.asuswrt +aioasuswrt==1.1.21 + # homeassistant.components.automatic aioautomatic==0.6.5 @@ -91,6 +103,13 @@ apns2==0.3.0 # homeassistant.components.aprs aprslib==0.6.46 +# homeassistant.components.arcam_fmj +arcam-fmj==0.4.3 + +# homeassistant.components.dlna_dmr +# homeassistant.components.upnp +async-upnp-client==0.14.11 + # homeassistant.components.stream av==6.1.2 @@ -100,17 +119,46 @@ axis==25 # homeassistant.components.zha bellows-homeassistant==0.10.0 +# homeassistant.components.bom +bomradarloop==0.1.3 + +# homeassistant.components.broadlink +broadlink==0.12.0 + +# homeassistant.components.buienradar +buienradar==1.0.1 + # homeassistant.components.caldav caldav==0.6.1 # homeassistant.components.coinmarketcap coinmarketcap==5.0.3 +# homeassistant.scripts.check_config +colorlog==4.0.2 + +# homeassistant.components.eddystone_temperature +# homeassistant.components.eq3btsmart +# homeassistant.components.xiaomi_miio +construct==2.9.45 + +# homeassistant.scripts.credstash +# credstash==1.15.0 + +# homeassistant.components.datadog +datadog==0.15.0 + # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect defusedxml==0.6.0 +# homeassistant.components.directv +directpy==0.5 + +# homeassistant.components.updater +distro==1.4.0 + # homeassistant.components.dsmr dsmr_parser==0.12 @@ -120,9 +168,6 @@ eebrightbox==0.0.4 # homeassistant.components.emulated_roku emulated_roku==0.1.8 -# homeassistant.components.enocean -enocean==0.50 - # homeassistant.components.season ephem==3.7.6.0 @@ -160,6 +205,9 @@ getmac==0.8.1 # homeassistant.components.google google-api-python-client==1.6.4 +# homeassistant.components.google_pubsub +google-cloud-pubsub==0.39.1 + # homeassistant.components.ffmpeg ha-ffmpeg==2.0 @@ -187,6 +235,9 @@ holidays==0.9.11 # homeassistant.components.frontend home-assistant-frontend==20191002.2 +# homeassistant.components.zwave +homeassistant-pyozw==0.1.4 + # homeassistant.components.homekit_controller homekit[IP]==0.15.0 @@ -209,12 +260,21 @@ influxdb==5.2.3 # homeassistant.components.verisure jsonpath==0.75 +# homeassistant.scripts.keyring +keyring==17.1.1 + +# homeassistant.scripts.keyring +keyrings.alt==3.1.1 + # homeassistant.components.dyson libpurecool==0.5.0 # homeassistant.components.soundtouch libsoundtouch==0.7.2 +# homeassistant.components.logi_circle +logi_circle==0.2.2 + # homeassistant.components.luftdaten luftdaten==0.6.3 @@ -227,10 +287,22 @@ mficlient==0.3.0 # homeassistant.components.minio minio==4.0.9 +# homeassistant.components.tts +mutagen==1.42.0 + +# homeassistant.components.ness_alarm +nessclient==0.9.15 + # homeassistant.components.discovery # homeassistant.components.ssdp netdisco==2.6.0 +# homeassistant.components.nsw_fuel_station +nsw-fuel-api-client==1.0.10 + +# homeassistant.components.nuheat +nuheat==0.3.0 + # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow @@ -268,6 +340,12 @@ plexauth==0.0.4 # homeassistant.components.serial_pm pmsensor==0.4 +# homeassistant.components.reddit +praw==6.3.1 + +# homeassistant.components.islamic_prayer_times +prayer_times_calculator==0.0.3 + # homeassistant.components.prometheus prometheus_client==0.7.1 @@ -280,6 +358,9 @@ pushbullet.py==0.11.0 # homeassistant.components.canary py-canary==0.5.0 +# homeassistant.components.melissa +py-melissa-climate==2.0.0 + # homeassistant.components.seventeentrack py17track==2.2.2 @@ -290,6 +371,15 @@ pyHS100==0.3.5 # homeassistant.components.norway_air pyMetno==0.4.6 +# homeassistant.components.rfxtrx +pyRFXtrx==0.23 + +# homeassistant.components.nextbus +py_nextbusnext==0.1.4 + +# homeassistant.components.arlo +pyarlo==0.2.3 + # homeassistant.components.blackbird pyblackbird==0.5 @@ -299,30 +389,69 @@ pybotvac==0.0.16 # homeassistant.components.cast pychromecast==4.0.1 +# homeassistant.components.daikin +pydaikin==1.6.1 + # homeassistant.components.deconz pydeconz==64 # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.everlights +pyeverlights==0.1.0 + +# homeassistant.components.fido +pyfido==2.1.1 + +# homeassistant.components.fritzbox +pyfritzhome==0.4.0 + +# homeassistant.components.ifttt +pyfttt==0.3 + +# homeassistant.components.version +pyhaversion==3.1.0 + # homeassistant.components.heos pyheos==0.6.0 # homeassistant.components.homematic pyhomematic==0.1.60 +# homeassistant.components.hydroquebec +pyhydroquebec==2.2.2 + +# homeassistant.components.ipma +pyipma==1.2.1 + # homeassistant.components.iqvia pyiqvia==0.2.1 +# homeassistant.components.kira +pykira==0.1.1 + +# homeassistant.components.webostv +pylgtv==0.1.9 + # homeassistant.components.linky pylinky==0.4.0 # homeassistant.components.litejet pylitejet==0.1 +# homeassistant.components.mailgun +pymailgunner==1.4 + # homeassistant.components.somfy pymfy==0.5.2 +# homeassistant.components.mochad +pymochad==0.2.0 + +# homeassistant.components.modbus +pymodbus==1.5.2 + # homeassistant.components.monoprice pymonoprice==0.3 @@ -343,6 +472,9 @@ pyotgw==0.5b0 # homeassistant.components.otp pyotp==2.3.0 +# homeassistant.components.point +pypoint==1.1.1 + # homeassistant.components.ps4 pyps4-2ndscreen==1.0.1 @@ -376,6 +508,9 @@ python-forecastio==1.4.0 # homeassistant.components.izone python-izone==1.1.1 +# homeassistant.components.xiaomi_miio +python-miio==0.4.6 + # homeassistant.components.nest python-nest==4.1.0 @@ -385,6 +520,9 @@ python-velbus==2.0.27 # homeassistant.components.awair python_awair==0.0.4 +# homeassistant.components.traccar +pytraccar==0.9.0 + # homeassistant.components.tradfri pytradfri[async]==6.3.1 @@ -409,6 +547,9 @@ ring_doorbell==0.2.3 # homeassistant.components.yamaha rxv==0.6.0 +# homeassistant.components.samsungtv +samsungctl[websocket]==0.7.1 + # homeassistant.components.simplisafe simplisafe-python==5.0.1 @@ -431,15 +572,29 @@ sqlalchemy==1.3.9 # homeassistant.components.statsd statsd==3.2.1 +# homeassistant.components.solaredge +# homeassistant.components.thermoworks_smoke +# homeassistant.components.traccar +stringcase==1.2.0 + +# homeassistant.components.tellduslive +tellduslive==0.10.10 + # homeassistant.components.toon toonapilib==3.2.4 +# homeassistant.components.tplink +tplink==0.2.1 + # homeassistant.components.transmission transmissionrpc==0.11 # homeassistant.components.twentemilieu twentemilieu==0.1.0 +# homeassistant.components.twilio +twilio==6.19.1 + # homeassistant.components.uvc uvcclient==0.11.0 @@ -454,11 +609,42 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==1.1.6 +# homeassistant.components.folder_watcher +watchdog==0.8.3 + +# homeassistant.components.webostv +websockets==6.0 + # homeassistant.components.withings withings-api==2.0.0b7 +# homeassistant.components.bluesound +# homeassistant.components.startca +# homeassistant.components.ted5000 +# homeassistant.components.yr +# homeassistant.components.zestimate +xmltodict==0.12.0 + +# homeassistant.components.yandex_transport +ya_ma==0.3.7 + +# homeassistant.components.yweather +yahooweather==0.10 + # homeassistant.components.zeroconf zeroconf==0.23.0 +# homeassistant.components.zha +zha-quirks==0.0.26 + +# homeassistant.components.zha +zigpy-deconz==0.5.0 + # homeassistant.components.zha zigpy-homeassistant==0.9.0 + +# homeassistant.components.zha +zigpy-xbee-homeassistant==0.5.0 + +# homeassistant.components.zha +zigpy-zigate==0.4.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e8837b8d295a94..930ffa11b5f2cd 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 """Generate an updated requirements_all.txt.""" +import difflib import importlib import os -import pathlib +from pathlib import Path import pkgutil import re import sys @@ -41,159 +42,8 @@ "VL53L1X2", ) -TEST_REQUIREMENTS = ( - "adguardhome", - "aio_geojson_geonetnz_quakes", - "aioambient", - "aioautomatic", - "aiobotocore", - "aioesphomeapi", - "aiohttp_cors", - "aiohue", - "aionotion", - "aioswitcher", - "aiounifi", - "aiowwlln", - "airly", - "ambiclimate", - "androidtv", - "apns2", - "aprslib", - "av", - "axis", - "bellows-homeassistant", - "caldav", - "coinmarketcap", - "defusedxml", - "dsmr_parser", - "eebrightbox", - "emulated_roku", - "enocean", - "ephem", - "evohomeclient", - "feedparser-homeassistant", - "foobot_async", - "geojson_client", - "geopy", - "georss_generic_client", - "georss_ign_sismologia_client", - "georss_qld_bushfire_alert_client", - "getmac", - "google-api-python-client", - "gTTS-token", - "ha-ffmpeg", - "hangups", - "HAP-python", - "hass-nabucasa", - "haversine", - "hbmqtt", - "hdate", - "herepy", - "hole", - "holidays", - "home-assistant-frontend", - "homekit[IP]", - "homematicip", - "httplib2", - "huawei-lte-api", - "iaqualink", - "influxdb", - "jsonpath", - "libpurecool", - "libsoundtouch", - "luftdaten", - "mbddns", - "mficlient", - "minio", - "netdisco", - "numpy", - "oauth2client", - "paho-mqtt", - "pexpect", - "pilight", - "pillow", - "plexapi", - "plexauth", - "pmsensor", - "prometheus_client", - "ptvsd", - "pushbullet.py", - "py-canary", - "py17track", - "pyblackbird", - "pybotvac", - "pychromecast", - "pydeconz", - "pydispatcher", - "pyheos", - "pyhomematic", - "pyHS100", - "pyiqvia", - "pylinky", - "pylitejet", - "pyMetno", - "pymfy", - "pymonoprice", - "PyNaCl", - "pynws", - "pynx584", - "pyopenuv", - "pyotgw", - "pyotp", - "pyps4-2ndscreen", - "pyqwikswitch", - "PyRMVtransport", - "pysma", - "pysmartapp", - "pysmartthings", - "pysoma", - "pysonos", - "pyspcwebgw", - "python_awair", - "python-ecobee-api", - "python-forecastio", - "python-izone", - "python-nest", - "python-velbus", - "pythonwhois", - "pytradfri[async]", - "PyTransportNSW", - "pyunifi", - "pyupnp-async", - "pyvesync", - "pywebpush", - "regenmaschine", - "restrictedpython", - "rflink", - "ring_doorbell", - "ruamel.yaml", - "rxv", - "simplisafe-python", - "sleepyq", - "smhi-pkg", - "solaredge", - "somecomfort", - "sqlalchemy", - "srpenergy", - "statsd", - "toonapilib", - "transmissionrpc", - "twentemilieu", - "uvcclient", - "vsure", - "vultr", - "wakeonlan", - "warrant", - "withings-api", - "YesssSMS", - "zeroconf", - "zigpy-homeassistant", -) - IGNORE_PIN = ("colorlog>2.1,<3", "keyring>=9.3,<10.0", "urllib3") -IGNORE_REQ = ("colorama<=1",) # Windows only requirement in check_config - URL_PIN = ( "https://developers.home-assistant.io/docs/" "creating_platform_code_review.html#1-requirements" @@ -211,12 +61,31 @@ # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 - -# Contains code to modify Home Assistant to work around our rules -python-systemair-savecair==1000000000.0.0 """ +def has_tests(module: str): + """Test if a module has tests. + + Module format: homeassistant.components.hue + Test if exists: tests/components/hue + """ + path = Path(module.replace(".", "/").replace("homeassistant", "tests")) + if not path.exists(): + return False + + if not path.is_dir(): + return True + + # Dev environments might have stale directories around + # from removed tests. Check for that. + content = [f.name for f in path.glob("*")] + + # Directories need to contain more than `__pycache__` + # to exist in Git and so be seen by CI. + return content != ["__pycache__"] + + def explore_module(package, explore_children): """Explore the modules.""" module = importlib.import_module(package) @@ -237,8 +106,9 @@ def explore_module(package, explore_children): 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) + reqs_raw = re.search( + r"REQUIRES = \[(.*?)\]", Path("setup.py").read_text(), re.S + ).group(1) return [x[1] for x in re.findall(r"(['\"])(.*?)\1", reqs_raw)] @@ -248,7 +118,7 @@ def gather_recursive_requirements(domain, seen=None): seen = set() seen.add(domain) - integration = Integration(pathlib.Path(f"homeassistant/components/{domain}")) + integration = Integration(Path(f"homeassistant/components/{domain}")) integration.load_manifest() reqs = set(integration.manifest["requirements"]) for dep_domain in integration.manifest["dependencies"]: @@ -283,7 +153,7 @@ def gather_modules(): def gather_requirements_from_manifests(errors, reqs): """Gather all of the requirements from manifests.""" - integrations = Integration.load_dir(pathlib.Path("homeassistant/components")) + integrations = Integration.load_dir(Path("homeassistant/components")) for domain in sorted(integrations): integration = integrations[domain] @@ -319,8 +189,6 @@ def gather_requirements_from_modules(errors, reqs): def process_requirements(errors, module_requirements, package, reqs): """Process all of the requirements.""" for req in module_requirements: - if req in IGNORE_REQ: - continue if "://" in req: errors.append(f"{package}[Only pypi dependencies are allowed: {req}]") if req.partition("==")[1] == "" and req not in IGNORE_PIN: @@ -359,15 +227,18 @@ def requirements_test_output(reqs): output = [] output.append("# Home Assistant test") output.append("\n") - with open("requirements_test.txt") as test_file: - output.append(test_file.read()) + output.append(Path("requirements_test.txt").read_text()) output.append("\n") + filtered = { - key: value - for key, value in reqs.items() + requirement: modules + for requirement, modules in reqs.items() if any( - re.search(r"(^|#){}($|[=><])".format(re.escape(ign)), key) is not None - for ign in TEST_REQUIREMENTS + # Always install requirements that are not part of integrations + not mdl.startswith("homeassistant.components.") or + # Install tests for integrations that have tests + has_tests(mdl) + for mdl in modules ) } output.append(generate_requirements_list(filtered)) @@ -377,48 +248,28 @@ def requirements_test_output(reqs): def gather_constraints(): """Construct output for constraint file.""" - return "\n".join( - sorted( - core_requirements() + list(gather_recursive_requirements("default_config")) + return ( + "\n".join( + sorted( + core_requirements() + + list(gather_recursive_requirements("default_config")) + ) + + [""] ) - + [""] + + CONSTRAINT_BASE ) -def write_requirements_file(data): - """Write the modules to the requirements_all.txt.""" - with open("requirements_all.txt", "w+", newline="\n") as req_file: - req_file.write(data) - - -def write_test_requirements_file(data): - """Write the modules to the requirements_test_all.txt.""" - with open("requirements_test_all.txt", "w+", newline="\n") as req_file: - req_file.write(data) - - -def write_constraints_file(data): - """Write constraints to a file.""" - with open(CONSTRAINT_PATH, "w+", newline="\n") as req_file: - req_file.write(data + CONSTRAINT_BASE) - - -def validate_requirements_file(data): - """Validate if requirements_all.txt is up to date.""" - with open("requirements_all.txt", "r") as req_file: - return data == req_file.read() - - -def validate_requirements_test_file(data): - """Validate if requirements_test_all.txt is up to date.""" - with open("requirements_test_all.txt", "r") as req_file: - return data == req_file.read() - - -def validate_constraints_file(data): - """Validate if constraints is up to date.""" - with open(CONSTRAINT_PATH, "r") as req_file: - return data + CONSTRAINT_BASE == req_file.read() +def diff_file(filename, content): + """Diff a file.""" + return list( + difflib.context_diff( + [line + "\n" for line in Path(filename).read_text().split("\n")], + [line + "\n" for line in content.split("\n")], + filename, + "generated", + ) + ) def main(validate): @@ -432,33 +283,38 @@ def main(validate): if data is None: return 1 - constraints = gather_constraints() - reqs_file = requirements_all_output(data) reqs_test_file = requirements_test_output(data) + constraints = gather_constraints() + + files = ( + ("requirements_all.txt", reqs_file), + ("requirements_test_all.txt", reqs_test_file), + ("homeassistant/package_constraints.txt", constraints), + ) if validate: errors = [] - if not validate_requirements_file(reqs_file): - errors.append("requirements_all.txt is not up to date") - - if not validate_requirements_test_file(reqs_test_file): - errors.append("requirements_test_all.txt is not up to date") - if not validate_constraints_file(constraints): - errors.append("home-assistant/package_constraints.txt is not up to date") + for filename, content in files: + diff = diff_file(filename, content) + if diff: + errors.append("".join(diff)) if errors: - print("******* ERROR") - print("\n".join(errors)) - print("Please run script/gen_requirements_all.py") + print("ERROR - FOUND THE FOLLOWING DIFFERENCES") + print() + print() + print("\n\n".join(errors)) + print() + print("Please run python3 -m script.gen_requirements_all") return 1 return 0 - write_requirements_file(reqs_file) - write_test_requirements_file(reqs_test_file) - write_constraints_file(constraints) + for filename, content in files: + Path(filename).write_text(content) + return 0 diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 5f17606146b0b1..5e2106ff208d99 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -59,17 +59,20 @@ async def test_async_setup_entry_default(hass): } with MockDependency("netdisco.discovery"), patch( "homeassistant.components.upnp.get_local_ip", return_value="192.168.1.10" - ): + ), patch.object(Device, "async_create_device") as create_device, patch.object( + Device, "async_create_device" + ) as create_device, patch.object( + Device, "async_discover", return_value=mock_coro([]) + ) as async_discover: await async_setup_component(hass, "http", config) await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() - # mock homeassistant.components.upnp.device.Device - mock_device = MockDevice(udn) - discovery_infos = [{"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"}] - with patch.object(Device, "async_create_device") as create_device, patch.object( - Device, "async_discover" - ) as async_discover: # noqa:E125 + # mock homeassistant.components.upnp.device.Device + mock_device = MockDevice(udn) + discovery_infos = [ + {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} + ] create_device.return_value = mock_coro(return_value=mock_device) async_discover.return_value = mock_coro(return_value=discovery_infos) @@ -100,16 +103,17 @@ async def test_async_setup_entry_port_mapping(hass): } with MockDependency("netdisco.discovery"), patch( "homeassistant.components.upnp.get_local_ip", return_value="192.168.1.10" - ): + ), patch.object(Device, "async_create_device") as create_device, patch.object( + Device, "async_discover", return_value=mock_coro([]) + ) as async_discover: await async_setup_component(hass, "http", config) await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() - mock_device = MockDevice(udn) - discovery_infos = [{"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"}] - with patch.object(Device, "async_create_device") as create_device, patch.object( - Device, "async_discover" - ) as async_discover: # noqa:E125 + mock_device = MockDevice(udn) + discovery_infos = [ + {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} + ] create_device.return_value = mock_coro(return_value=mock_device) async_discover.return_value = mock_coro(return_value=discovery_infos) diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 6e7bc6ab4b13a7..91049a9bfa8833 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -41,7 +41,7 @@ async def test_get_actions(hass, config_entry, zha_gateway): zha_gateway, ) - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") await hass.async_block_till_done() hass.config_entries._entries.append(config_entry) From 762a714d87f30eaa09264a93384f8bc77ce171b7 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 10 Oct 2019 00:31:40 +0000 Subject: [PATCH 0713/3953] [ci skip] Translation update --- .../binary_sensor/.translations/ca.json | 2 ++ .../binary_sensor/.translations/fr.json | 20 ++++++++++++++++++- .../binary_sensor/.translations/lb.json | 2 ++ .../binary_sensor/.translations/no.json | 2 ++ .../binary_sensor/.translations/pl.json | 2 ++ .../binary_sensor/.translations/sl.json | 2 ++ .../components/deconz/.translations/pl.json | 1 + .../components/ecobee/.translations/pl.json | 2 +- .../components/hue/.translations/pl.json | 2 +- .../opentherm_gw/.translations/ko.json | 4 ++-- .../opentherm_gw/.translations/pl.json | 17 +++++++++++++--- .../components/plex/.translations/da.json | 4 ++++ .../components/plex/.translations/fr.json | 4 ++++ .../components/plex/.translations/pl.json | 7 ++++++- .../components/sensor/.translations/pl.json | 19 +++++++++++++++++- .../smartthings/.translations/pl.json | 2 +- .../components/zha/.translations/fr.json | 1 + .../components/zha/.translations/ko.json | 2 +- 18 files changed, 83 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/binary_sensor/.translations/ca.json b/homeassistant/components/binary_sensor/.translations/ca.json index de7d837b12cc0d..8bbd19a0d45c0a 100644 --- a/homeassistant/components/binary_sensor/.translations/ca.json +++ b/homeassistant/components/binary_sensor/.translations/ca.json @@ -53,6 +53,7 @@ "hot": "{entity_name} es torna calent", "light": "{entity_name} ha comen\u00e7at a detectar llum", "locked": "{entity_name} est\u00e0 bloquejat", + "moist": "{entity_name} es torna humit", "moist\u00a7": "{entity_name} es torna humit", "motion": "{entity_name} ha comen\u00e7at a detectar moviment", "moving": "{entity_name} ha comen\u00e7at a moure's", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} es torna sec", "not_moving": "{entity_name} ha parat de moure's", "not_occupied": "{entity_name} es desocupa", + "not_opened": "{entity_name} es tanca", "not_plugged_in": "{entity_name} desendollat", "not_powered": "{entity_name} no est\u00e0 alimentat", "not_present": "{entity_name} no est\u00e0 present", diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json index 80792f166354ec..1a11bfa4bc26e4 100644 --- a/homeassistant/components/binary_sensor/.translations/fr.json +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -40,9 +40,27 @@ "is_present": "{entity_name} est pr\u00e9sent", "is_problem": "{entity_name} d\u00e9tecte un probl\u00e8me", "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", - "is_sound": "{entity_name} d\u00e9tecte du son" + "is_sound": "{entity_name} d\u00e9tecte du son", + "is_unsafe": "{entity_name} est dangereux", + "is_vibration": "{entity_name} d\u00e9tecte des vibrations" }, "trigger_type": { + "bat_low": "{entity_name} batterie faible", + "closed": "{entity_name} ferm\u00e9", + "cold": "{entity_name} est devenu froid", + "connected": "{entity_name} connect\u00e9", + "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz", + "not_occupied": "{entity_name} est devenu non occup\u00e9", + "not_plugged_in": "{entity_name} d\u00e9branch\u00e9", + "not_powered": "{entity_name} non aliment\u00e9", + "not_present": "{entity_name} non pr\u00e9sent", + "not_unsafe": "{entity_name} est devenu s\u00fbr", + "occupied": "{entity_name} est devenu occup\u00e9", + "opened": "{entity_name} ouvert", + "plugged_in": "{entity_name} branch\u00e9", + "powered": "{entity_name} aliment\u00e9", + "present": "{entity_name} pr\u00e9sent", + "problem": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter un probl\u00e8me", "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", "turned_off": "{entity_name} d\u00e9sactiv\u00e9", diff --git a/homeassistant/components/binary_sensor/.translations/lb.json b/homeassistant/components/binary_sensor/.translations/lb.json index 0b10e1f51a52c5..c65ae94396bbf4 100644 --- a/homeassistant/components/binary_sensor/.translations/lb.json +++ b/homeassistant/components/binary_sensor/.translations/lb.json @@ -53,6 +53,7 @@ "hot": "{entity_name} gouf waarm", "light": "{entity_name} huet ugefange Luucht z'entdecken", "locked": "{entity_name} gespaart", + "moist": "{entity_name} gouf fiicht", "moist\u00a7": "{entity_name} gouf fiicht", "motion": "{entity_name} huet ugefaange Beweegung z'entdecken", "moving": "{entity_name} huet ugefaangen sech ze beweegen", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} gouf dr\u00e9chen", "not_moving": "{entity_name} huet opgehale sech ze beweegen", "not_occupied": "{entity_name} gouf fr\u00e4i", + "not_opened": "{entity_name} gouf zougemaach", "not_plugged_in": "{entity_name} net ugeschloss", "not_powered": "{entity_name} net aliment\u00e9iert", "not_present": "{entity_name} net pr\u00e4sent", diff --git a/homeassistant/components/binary_sensor/.translations/no.json b/homeassistant/components/binary_sensor/.translations/no.json index 5a1916bce59c33..4194102948b837 100644 --- a/homeassistant/components/binary_sensor/.translations/no.json +++ b/homeassistant/components/binary_sensor/.translations/no.json @@ -53,6 +53,7 @@ "hot": "{entity_name} ble varm", "light": "{entity_name} begynte \u00e5 registrere lys", "locked": "{entity_name} l\u00e5st", + "moist": "{entity_name} ble fuktig", "moist\u00a7": "{entity_name} ble fuktig", "motion": "{entity_name} begynte \u00e5 registrere bevegelse", "moving": "{entity_name} begynte \u00e5 bevege seg", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} ble t\u00f8rr", "not_moving": "{entity_name} sluttet \u00e5 bevege seg", "not_occupied": "{entity_name} ble ledig", + "not_opened": "{entity_name} stengt", "not_plugged_in": "{entity_name} koblet fra", "not_powered": "{entity_name} spenningsl\u00f8s", "not_present": "{entity_name} ikke til stede", diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index a7f0bd516a08f5..a1ab03770f9995 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -53,6 +53,7 @@ "hot": "sensor {entity_name} wykryje gor\u0105co", "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", "locked": "zamkni\u0119cie {entity_name}", + "moist": "sensor {entity_name} wykry\u0142 wilgo\u0107", "moist\u00a7": "sensor {entity_name} wykryje wilgo\u0107", "motion": "sensor {entity_name} wykryje ruch", "moving": "sensor {entity_name} zacznie porusza\u0107 si\u0119", @@ -71,6 +72,7 @@ "not_moist": "sensor {entity_name} przestanie wykrywa\u0107 wilgo\u0107", "not_moving": "sensor {entity_name} przestanie porusza\u0107 si\u0119", "not_occupied": "sensor {entity_name} przesta\u0142 by\u0107 zaj\u0119ty", + "not_opened": "sensor {entity_name} zosta\u0142 zamkni\u0119ty", "not_plugged_in": "od\u0142\u0105czenie {entity_name}", "not_powered": "od\u0142\u0105czenie zasilania {entity_name}", "not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107", diff --git a/homeassistant/components/binary_sensor/.translations/sl.json b/homeassistant/components/binary_sensor/.translations/sl.json index 6b4e144d9a6ac2..2004caeb342890 100644 --- a/homeassistant/components/binary_sensor/.translations/sl.json +++ b/homeassistant/components/binary_sensor/.translations/sl.json @@ -53,6 +53,7 @@ "hot": "{entity_name} je postal vro\u010d", "light": "{entity_name} za\u010del zaznavati svetlobo", "locked": "{entity_name} zaklenjen", + "moist": "{entity_name} postal vla\u017een", "moist\u00a7": "{entity_name} postal vla\u017een", "motion": "{entity_name} za\u010del zaznavati gibanje", "moving": "{entity_name} se je za\u010del premikati", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} je postalo suh", "not_moving": "{entity_name} se je prenehal premikati", "not_occupied": "{entity_name} ni zaseden", + "not_opened": "{entity_name} zaprto", "not_plugged_in": "{entity_name} odklopljen", "not_powered": "{entity_name} ni napajan", "not_present": "{entity_name} ni prisoten", diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 11a1beb10d6fa1..498c8dd18d6d39 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", "remote_button_rotated": "przycisk obr\u00f3cony \"{subtype}\"", + "remote_button_rotation_stopped": "obr\u00f3t przycisku \"{subtype}\" zatrzymany", "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", diff --git a/homeassistant/components/ecobee/.translations/pl.json b/homeassistant/components/ecobee/.translations/pl.json index 5c51d86fee4980..bd4e7aa1ddc109 100644 --- a/homeassistant/components/ecobee/.translations/pl.json +++ b/homeassistant/components/ecobee/.translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "pin_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania kodu PIN od ecobee; sprawd\u017a, czy klucz API jest poprawny.", - "token_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania token\u00f3w od ecobee; prosz\u0119 spr\u00f3buj ponownie." + "token_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania token\u00f3w od ecobee. Spr\u00f3buj ponownie." }, "step": { "authorize": { diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index 9062e427a27c25..33b1ffbfe86c74 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -12,7 +12,7 @@ }, "error": { "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", - "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Prosz\u0119 spr\u00f3bowa\u0107 ponownie." + "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Spr\u00f3buj ponownie." }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/.translations/ko.json b/homeassistant/components/opentherm_gw/.translations/ko.json index 85790702435b1c..e5daf826ee519d 100644 --- a/homeassistant/components/opentherm_gw/.translations/ko.json +++ b/homeassistant/components/opentherm_gw/.translations/ko.json @@ -10,10 +10,10 @@ "init": { "data": { "device": "\uacbd\ub85c \ub610\ub294 URL", - "floor_temperature": "\uc9c0\uba74 \uae30\ud6c4 \uc628\ub3c4", + "floor_temperature": "\uc2e4\ub0b4\uc628\ub3c4 \uc18c\uc218\uc810 \ub0b4\ub9bc", "id": "ID", "name": "\uc774\ub984", - "precision": "\uae30\ud6c4 \uc628\ub3c4 \uc815\ubc00\ub3c4" + "precision": "\uc2e4\ub0b4\uc628\ub3c4 \uc815\ubc00\ub3c4" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index 7e4a0eed013f3e..32e5cde82cb3af 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -1,12 +1,23 @@ { "config": { + "error": { + "already_configured": "Bramka jest ju\u017c skonfigurowana", + "id_exists": "Identyfikator bramki ju\u017c istnieje", + "serial_error": "B\u0142\u0105d po\u0142\u0105czenia z urz\u0105dzeniem", + "timeout": "Up\u0142yn\u0105\u0142 limit czasu pr\u00f3by po\u0142\u0105czenia" + }, "step": { "init": { "data": { "device": "\u015acie\u017cka lub adres URL", - "name": "Nazwa" - } + "floor_temperature": "Temperatura pod\u0142ogi", + "id": "Identyfikator", + "name": "Nazwa", + "precision": "Precyzja temperatury" + }, + "title": "Bramka OpenTherm" } - } + }, + "title": "Bramka OpenTherm" } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 1da4b4b4b49364..4ca695e74d8b4b 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -4,6 +4,7 @@ "all_configured": "Alle linkede servere er allerede konfigureret", "already_configured": "Denne Plex-server er allerede konfigureret", "already_in_progress": "Plex konfigureres", + "discovery_no_file": "Der blev ikke fundet nogen legacy konfigurationsfil", "invalid_import": "Importeret konfiguration er ugyldig", "token_request_timeout": "Timeout ved hentning af token", "unknown": "Mislykkedes af ukendt \u00e5rsag" @@ -32,6 +33,9 @@ "description": "Flere servere til r\u00e5dighed, v\u00e6lg en:", "title": "V\u00e6lg Plex-server" }, + "start_website_auth": { + "title": "Tilslut Plex-server" + }, "user": { "data": { "manual_setup": "Manuel ops\u00e6tning", diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index c9e61dcf2e9407..3854b19b5d2f76 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -4,6 +4,7 @@ "all_configured": "Tous les serveurs li\u00e9s sont d\u00e9j\u00e0 configur\u00e9s", "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "Plex en cours de configuration", + "discovery_no_file": "Aucun fichier de configuration h\u00e9rit\u00e9 trouv\u00e9", "invalid_import": "La configuration import\u00e9e est invalide", "token_request_timeout": "D\u00e9lai d'obtention du jeton", "unknown": "\u00c9chec pour une raison inconnue" @@ -32,6 +33,9 @@ "description": "Plusieurs serveurs disponibles, s\u00e9lectionnez-en un:", "title": "S\u00e9lectionnez le serveur Plex" }, + "start_website_auth": { + "title": "Connecter un serveur Plex" + }, "user": { "data": { "manual_setup": "Installation manuelle", diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index 9b75a0061e8d1b..0b94e3eacb6af9 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -4,6 +4,7 @@ "all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.", "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", "already_in_progress": "Plex jest konfigurowany", + "discovery_no_file": "Nie znaleziono pliku konfiguracyjnego", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena", "unknown": "Nieznany b\u0142\u0105d" @@ -32,6 +33,10 @@ "description": "Dost\u0119pnych jest wiele serwer\u00f3w, wybierz jeden:", "title": "Wybierz serwer Plex" }, + "start_website_auth": { + "description": "Kontynuuj, by dokona\u0107 autoryzacji w plex.tv.", + "title": "Po\u0142\u0105cz z serwerem Plex" + }, "user": { "data": { "manual_setup": "Konfiguracja r\u0119czna", @@ -48,7 +53,7 @@ "plex_mp_settings": { "data": { "show_all_controls": "Poka\u017c wszystkie elementy steruj\u0105ce", - "use_episode_art": "U\u017cyj grafiki episodu" + "use_episode_art": "U\u017cyj grafiki odcinka" }, "description": "Opcje dla odtwarzaczy multimedialnych Plex" } diff --git a/homeassistant/components/sensor/.translations/pl.json b/homeassistant/components/sensor/.translations/pl.json index da1dcc1d6fd890..68a3a0fecfdc3f 100644 --- a/homeassistant/components/sensor/.translations/pl.json +++ b/homeassistant/components/sensor/.translations/pl.json @@ -3,7 +3,24 @@ "condition_type": { "is_battery_level": "{entity_name} poziom na\u0142adowania baterii", "is_humidity": "{entity_name} wilgotno\u015b\u0107", - "is_temperature": "{entity_name} temperatura" + "is_illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "is_power": "moc {entity_name}", + "is_pressure": "ci\u015bnienie {entity_name}", + "is_signal_strength": "si\u0142a sygna\u0142u {entity_name}", + "is_temperature": "temperatura {entity_name}", + "is_timestamp": "znacznik czasu {entity_name}", + "is_value": "warto\u015b\u0107 {entity_name}" + }, + "trigger_type": { + "battery_level": "poziom baterii {entity_name}", + "humidity": "wilgotno\u015b\u0107 {entity_name}", + "illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "power": "moc {entity_name}", + "pressure": "ci\u015bnienie {entity_name}", + "signal_strength": "si\u0142a sygna\u0142u {entity_name}", + "temperature": "temperatura {entity_name}", + "timestamp": "znacznik czasu {entity_name}", + "value": "warto\u015b\u0107 {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/pl.json b/homeassistant/components/smartthings/.translations/pl.json index 33803994764230..849ad174134513 100644 --- a/homeassistant/components/smartthings/.translations/pl.json +++ b/homeassistant/components/smartthings/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "app_not_installed": "Upewnij si\u0119, \u017ce zainstalowa\u0142e\u015b i autoryzowa\u0142e\u015b Home Assistant SmartApp i spr\u00f3buj ponownie.", - "app_setup_error": "Nie mo\u017cna skonfigurowa\u0107 SmartApp. Prosz\u0119 spr\u00f3buj ponownie.", + "app_setup_error": "Nie mo\u017cna skonfigurowa\u0107 SmartApp. Spr\u00f3buj ponownie.", "base_url_not_https": "Parametr `base_url` dla komponentu `http` musi by\u0107 skonfigurowany i rozpoczyna\u0107 si\u0119 od `https://`.", "token_already_setup": "Token zosta\u0142 ju\u017c skonfigurowany.", "token_forbidden": "Token nie ma wymaganych zakres\u00f3w OAuth.", diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index f8b78af5721ab4..d7b0a78311626b 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -38,6 +38,7 @@ "turn_on": "Allumer" }, "trigger_type": { + "device_dropped": "Appareil tomb\u00e9", "device_shaken": "Appareil secou\u00e9", "device_tilted": "Dispositif inclin\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json index 7ed1a8c69b473f..f2277414a3e649 100644 --- a/homeassistant/components/zha/.translations/ko.json +++ b/homeassistant/components/zha/.translations/ko.json @@ -39,7 +39,7 @@ "face_4": "\uba74 4\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", "face_5": "\uba74 5\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", "face_6": "\uba74 6\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", - "face_any": "\uc784\uc758\uc758 \uba74 \ub610\ub294 \ud2b9\uc815 \uba74 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_any": "\uc784\uc758\uc758 \uba74 \ub610\ub294 \ud2b9\uc815 \uba74\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", "left": "\uc67c\ucabd", "open": "\uc5f4\uae30", "right": "\uc624\ub978\ucabd", From 80f6781f21f335fcd1524e00214c4c3c6706f5fc Mon Sep 17 00:00:00 2001 From: Santobert Date: Thu, 10 Oct 2019 08:08:11 +0200 Subject: [PATCH 0714/3953] Migrate Neato to use top-level imports (#27363) * Neato move imports up * Move one last import * Fix tests --- homeassistant/components/neato/__init__.py | 9 ++++----- homeassistant/components/neato/camera.py | 2 +- homeassistant/components/neato/config_flow.py | 6 ++---- homeassistant/components/neato/const.py | 4 ++-- homeassistant/components/neato/sensor.py | 4 ++-- homeassistant/components/neato/vacuum.py | 3 +-- tests/components/neato/test_config_flow.py | 20 +++++++++++-------- tests/components/neato/test_init.py | 4 ++-- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 14090c99a5553f..839c24568d8d1b 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -3,8 +3,9 @@ import logging from datetime import timedelta -from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException import voluptuous as vol +from pybotvac import Account, Neato, Vorwerk +from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -17,9 +18,9 @@ NEATO_CONFIG, NEATO_DOMAIN, NEATO_LOGIN, - NEATO_ROBOTS, - NEATO_PERSISTENT_MAPS, NEATO_MAP_DATA, + NEATO_PERSISTENT_MAPS, + NEATO_ROBOTS, SCAN_INTERVAL_MINUTES, VALID_VENDORS, ) @@ -91,8 +92,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up config entry.""" - from pybotvac import Account, Neato, Vorwerk - if entry.data[CONF_VENDOR] == "neato": hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Neato) elif entry.data[CONF_VENDOR] == "vorwerk": diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 98b48dd72255ec..f60835b1146b81 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -8,9 +8,9 @@ from .const import ( NEATO_DOMAIN, + NEATO_LOGIN, NEATO_MAP_DATA, NEATO_ROBOTS, - NEATO_LOGIN, SCAN_INTERVAL_MINUTES, ) diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 7ece3b8d300e4a..56fba9047e7a9a 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -2,8 +2,9 @@ import logging -import voluptuous as vol +from pybotvac import Account, Neato, Vorwerk from pybotvac.exceptions import NeatoLoginException, NeatoRobotException +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -11,7 +12,6 @@ # pylint: disable=unused-import from .const import CONF_VENDOR, NEATO_DOMAIN, VALID_VENDORS - DOCS_URL = "https://www.home-assistant.io/components/neato" DEFAULT_VENDOR = "neato" @@ -96,8 +96,6 @@ async def async_step_import(self, user_input): @staticmethod def try_login(username, password, vendor): """Try logging in to device and return any errors.""" - from pybotvac import Account, Neato, Vorwerk - this_vendor = None if vendor == "vorwerk": this_vendor = Vorwerk() diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index 4d4178a6875b6d..6dbaeb10d36500 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -3,11 +3,11 @@ NEATO_DOMAIN = "neato" CONF_VENDOR = "vendor" -NEATO_ROBOTS = "neato_robots" -NEATO_LOGIN = "neato_login" NEATO_CONFIG = "neato_config" +NEATO_LOGIN = "neato_login" NEATO_MAP_DATA = "neato_map_data" NEATO_PERSISTENT_MAPS = "neato_persistent_maps" +NEATO_ROBOTS = "neato_robots" SCAN_INTERVAL_MINUTES = 5 diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 0201012cc37f96..36175151e0e736 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -1,13 +1,13 @@ """Support for Neato sensors.""" +from datetime import timedelta import logging -from datetime import timedelta from pybotvac.exceptions import NeatoRobotException from homeassistant.components.sensor import DEVICE_CLASS_BATTERY from homeassistant.helpers.entity import Entity -from .const import NEATO_ROBOTS, NEATO_LOGIN, NEATO_DOMAIN, SCAN_INTERVAL_MINUTES +from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 5d8fd42a5f7178..40ed79042c760c 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -3,7 +3,6 @@ import logging from pybotvac.exceptions import NeatoRobotException - import voluptuous as vol from homeassistant.components.vacuum import ( @@ -35,8 +34,8 @@ ALERTS, ERRORS, MODE, - NEATO_LOGIN, NEATO_DOMAIN, + NEATO_LOGIN, NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 8eb67e5d3e1444..3f4bd90d0c1b0b 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -2,9 +2,11 @@ import pytest from unittest.mock import patch +from pybotvac.exceptions import NeatoLoginException, NeatoRobotException + from homeassistant import data_entry_flow from homeassistant.components.neato import config_flow -from homeassistant.components.neato.const import NEATO_DOMAIN, CONF_VENDOR +from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry @@ -19,7 +21,7 @@ @pytest.fixture(name="account") def mock_controller_login(): """Mock a successful login.""" - with patch("pybotvac.Account", return_value=True): + with patch("homeassistant.components.neato.config_flow.Account", return_value=True): yield @@ -103,11 +105,12 @@ async def test_abort_if_already_setup(hass, account): async def test_abort_on_invalid_credentials(hass): """Test when we have invalid credentials.""" - from pybotvac.exceptions import NeatoLoginException - flow = init_config_flow(hass) - with patch("pybotvac.Account", side_effect=NeatoLoginException()): + with patch( + "homeassistant.components.neato.config_flow.Account", + side_effect=NeatoLoginException(), + ): result = await flow.async_step_user( { CONF_USERNAME: USERNAME, @@ -131,11 +134,12 @@ async def test_abort_on_invalid_credentials(hass): async def test_abort_on_unexpected_error(hass): """Test when we have an unexpected error.""" - from pybotvac.exceptions import NeatoRobotException - flow = init_config_flow(hass) - with patch("pybotvac.Account", side_effect=NeatoRobotException()): + with patch( + "homeassistant.components.neato.config_flow.Account", + side_effect=NeatoRobotException(), + ): result = await flow.async_step_user( { CONF_USERNAME: USERNAME, diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py index be7e43fdc0aad4..361f9eab1dbadf 100644 --- a/tests/components/neato/test_init.py +++ b/tests/components/neato/test_init.py @@ -2,7 +2,7 @@ import pytest from unittest.mock import patch -from homeassistant.components.neato.const import NEATO_DOMAIN, CONF_VENDOR +from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component @@ -30,7 +30,7 @@ @pytest.fixture(name="account") def mock_controller_login(): """Mock a successful login.""" - with patch("pybotvac.Account", return_value=True): + with patch("homeassistant.components.neato.config_flow.Account", return_value=True): yield From 829cffd5def92f53786ba552eb2e2788e28c02bd Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Thu, 10 Oct 2019 03:05:46 -0400 Subject: [PATCH 0715/3953] Fix ecobee weather platform (#27369) * Fix ecobee weather platform * Remove custom forecast attributes * Tidy up process forecast method * Fix lint complaints * Add missed weather symbol --- homeassistant/components/ecobee/const.py | 28 +++++++ homeassistant/components/ecobee/weather.py | 94 +++++++++++++--------- 2 files changed, 83 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 411f5ddeeeba01..a6141d874f1647 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -24,3 +24,31 @@ ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] MANUFACTURER = "ecobee" + +# Translates ecobee API weatherSymbol to HASS usable names +# https://www.ecobee.com/home/developer/api/documentation/v1/objects/WeatherForecast.shtml +ECOBEE_WEATHER_SYMBOL_TO_HASS = { + 0: "sunny", + 1: "partlycloudy", + 2: "partlycloudy", + 3: "cloudy", + 4: "cloudy", + 5: "cloudy", + 6: "rainy", + 7: "snowy-rainy", + 8: "pouring", + 9: "hail", + 10: "snowy", + 11: "snowy", + 12: "snowy-rainy", + 13: "snowy-heavy", + 14: "hail", + 15: "lightning-rainy", + 16: "windy", + 17: "tornado", + 18: "fog", + 19: "hazy", + 20: "hazy", + 21: "hazy", + -2: None, +} diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 53e9842aae7393..7b057f09a0cbb3 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -8,17 +8,19 @@ ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.const import TEMP_FAHRENHEIT -from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER - -ATTR_FORECAST_TEMP_HIGH = "temphigh" -ATTR_FORECAST_PRESSURE = "pressure" -ATTR_FORECAST_VISIBILITY = "visibility" -ATTR_FORECAST_HUMIDITY = "humidity" +from .const import ( + DOMAIN, + ECOBEE_MODEL_TO_NAME, + ECOBEE_WEATHER_SYMBOL_TO_HASS, + MANUFACTURER, + _LOGGER, +) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -94,7 +96,7 @@ def device_info(self): def condition(self): """Return the current condition.""" try: - return self.get_forecast(0, "condition") + return ECOBEE_WEATHER_SYMBOL_TO_HASS[self.get_forecast(0, "weatherSymbol")] except ValueError: return None @@ -131,7 +133,7 @@ def humidity(self): def visibility(self): """Return the visibility.""" try: - return int(self.get_forecast(0, "visibility")) + return int(self.get_forecast(0, "visibility")) / 1000 except ValueError: return None @@ -154,45 +156,59 @@ def wind_bearing(self): @property def attribution(self): """Return the attribution.""" - if self.weather: - station = self.weather.get("weatherStation", "UNKNOWN") - time = self.weather.get("timestamp", "UNKNOWN") - return f"Ecobee weather provided by {station} at {time}" - return None + if not self.weather: + return None + + station = self.weather.get("weatherStation", "UNKNOWN") + time = self.weather.get("timestamp", "UNKNOWN") + return f"Ecobee weather provided by {station} at {time} UTC" @property def forecast(self): """Return the forecast array.""" - try: - forecasts = [] - for day in self.weather["forecasts"]: - date_time = datetime.strptime( - day["dateTime"], "%Y-%m-%d %H:%M:%S" - ).isoformat() - forecast = { - ATTR_FORECAST_TIME: date_time, - ATTR_FORECAST_CONDITION: day["condition"], - ATTR_FORECAST_TEMP: float(day["tempHigh"]) / 10, - } - if day["tempHigh"] == ECOBEE_STATE_UNKNOWN: - break - if day["tempLow"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_TEMP_LOW] = float(day["tempLow"]) / 10 - if day["pressure"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_PRESSURE] = int(day["pressure"]) - if day["windSpeed"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_WIND_SPEED] = int(day["windSpeed"]) - if day["visibility"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_WIND_SPEED] = int(day["visibility"]) - if day["relativeHumidity"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_HUMIDITY] = int(day["relativeHumidity"]) - forecasts.append(forecast) - return forecasts - except (ValueError, IndexError, KeyError): + if "forecasts" not in self.weather: return None + forecasts = list() + for day in range(1, 5): + forecast = _process_forecast(self.weather["forecasts"][day]) + if forecast is None: + continue + forecasts.append(forecast) + + if forecasts: + return forecasts + return None + async def async_update(self): """Get the latest weather data.""" await self.data.update() thermostat = self.data.ecobee.get_thermostat(self._index) self.weather = thermostat.get("weather", None) + + +def _process_forecast(json): + """Process a single ecobee API forecast to return expected values.""" + forecast = dict() + try: + forecast[ATTR_FORECAST_TIME] = datetime.strptime( + json["dateTime"], "%Y-%m-%d %H:%M:%S" + ).isoformat() + forecast[ATTR_FORECAST_CONDITION] = ECOBEE_WEATHER_SYMBOL_TO_HASS[ + json["weatherSymbol"] + ] + if json["tempHigh"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_TEMP] = float(json["tempHigh"]) / 10 + if json["tempLow"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_TEMP_LOW] = float(json["tempLow"]) / 10 + if json["windBearing"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_WIND_BEARING] = int(json["windBearing"]) + if json["windSpeed"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_WIND_SPEED] = int(json["windSpeed"]) + + except (ValueError, IndexError, KeyError): + return None + + if forecast: + return forecast + return None From a2591e696cfb5112cc30c7af505949e8092afedf Mon Sep 17 00:00:00 2001 From: Markus Nigbur Date: Thu, 10 Oct 2019 09:19:46 +0200 Subject: [PATCH 0716/3953] Move imports in vlc component (#27361) --- homeassistant/components/vlc/media_player.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index aaef128f33d2da..30b316cb4e8fcb 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import vlc from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -17,6 +18,7 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util + _LOGGER = logging.getLogger(__name__) CONF_ARGUMENTS = "arguments" @@ -51,8 +53,6 @@ class VlcDevice(MediaPlayerDevice): def __init__(self, name, arguments): """Initialize the vlc device.""" - import vlc - self._instance = vlc.Instance(arguments) self._vlc = self._instance.media_player_new() self._name = name @@ -65,8 +65,6 @@ def __init__(self, name, arguments): def update(self): """Get the latest details from the device.""" - import vlc - status = self._vlc.get_state() if status == vlc.State.Playing: self._state = STATE_PLAYING From 549c79b6ce7b477e5a224b812cd428052fca9ada Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 09:21:18 +0200 Subject: [PATCH 0717/3953] Move imports in season component (#27358) --- homeassistant/components/season/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index cdd6af57617e46..46d2291cf81284 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import datetime +import ephem import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -67,7 +68,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def get_season(date, hemisphere, season_tracking_type): """Calculate the current season.""" - import ephem if hemisphere == "equator": return None From 6c739f4be5913ba46c25527c5883f6945716526c Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 09:21:40 +0200 Subject: [PATCH 0718/3953] Move imports in nissan_leaf component (#27359) --- homeassistant/components/nissan_leaf/__init__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 38b7018af6c6c2..0c72f4f43ea5e9 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -3,7 +3,7 @@ import asyncio import logging import sys - +from pycarwings2 import CarwingsError, Session import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -95,7 +95,6 @@ def setup(hass, config): """Set up the Nissan Leaf component.""" - import pycarwings2 async def async_handle_update(service): """Handle service to update leaf data from Nissan servers.""" @@ -148,7 +147,7 @@ def setup_leaf(car_config): try: # This might need to be made async (somehow) causes # homeassistant to be slow to start - sess = pycarwings2.Session(username, password, region) + sess = Session(username, password, region) leaf = sess.get_leaf() except KeyError: _LOGGER.error( @@ -156,7 +155,7 @@ def setup_leaf(car_config): " do you actually have a Leaf connected to your account?" ) return False - except pycarwings2.CarwingsError: + except CarwingsError: _LOGGER.error( "An unknown error occurred while connecting to Nissan: %s", sys.exc_info()[0], @@ -274,7 +273,6 @@ def get_next_interval(self): async def async_refresh_data(self, now): """Refresh the leaf data and update the datastore.""" - from pycarwings2 import CarwingsError if self.request_in_progress: _LOGGER.debug("Refresh currently in progress for %s", self.leaf.nickname) @@ -339,7 +337,6 @@ def _extract_start_date(battery_info): async def async_get_battery(self): """Request battery update from Nissan servers.""" - from pycarwings2 import CarwingsError try: # Request battery update from the car @@ -389,7 +386,6 @@ async def async_get_battery(self): async def async_get_climate(self): """Request climate data from Nissan servers.""" - from pycarwings2 import CarwingsError try: return await self.hass.async_add_executor_job( From 7718d61cd7911fe5f3a351f63b3c84b1dcb7115f Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 09:22:10 +0200 Subject: [PATCH 0719/3953] Move imports in netatmo component (#27360) --- homeassistant/components/netatmo/__init__.py | 4 +--- homeassistant/components/netatmo/binary_sensor.py | 5 ++--- homeassistant/components/netatmo/camera.py | 4 ++-- homeassistant/components/netatmo/climate.py | 9 +-------- homeassistant/components/netatmo/sensor.py | 9 ++------- 5 files changed, 8 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 28d422557da3f3..4b9f0690ac5a4c 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -3,6 +3,7 @@ from datetime import timedelta from urllib.error import HTTPError +import pyatmo import voluptuous as vol from homeassistant.const import ( @@ -89,7 +90,6 @@ def setup(hass, config): """Set up the Netatmo devices.""" - import pyatmo hass.data[DATA_PERSONS] = {} try: @@ -254,8 +254,6 @@ def get_persons(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the Netatmo API to update the data.""" - import pyatmo - self.camera_data = pyatmo.CameraData(self.auth, size=100) @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 591cd790ecf5d7..1a40d3952e9456 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -1,6 +1,7 @@ """Support for the Netatmo binary sensors.""" import logging +from pyatmo import NoDevice import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -58,15 +59,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): module_name = None - import pyatmo - auth = hass.data[DATA_NETATMO_AUTH] try: data = CameraData(hass, auth, home) if not data.get_camera_names(): return None - except pyatmo.NoDevice: + except NoDevice: return None welcome_sensors = config.get(CONF_WELCOME_SENSORS, WELCOME_SENSOR_TYPES) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 60428961cb95be..ecc38add3b41cb 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -1,6 +1,7 @@ """Support for the Netatmo cameras.""" import logging +from pyatmo import NoDevice import requests import voluptuous as vol @@ -38,7 +39,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): home = config.get(CONF_HOME) verify_ssl = config.get(CONF_VERIFY_SSL, True) quality = config.get(CONF_QUALITY, DEFAULT_QUALITY) - import pyatmo auth = hass.data[DATA_NETATMO_AUTH] @@ -60,7 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] ) data.get_persons() - except pyatmo.NoDevice: + except NoDevice: return None diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 1465058652dea2..8ba13a03889d2c 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -3,6 +3,7 @@ import logging from typing import Optional, List +import pyatmo import requests import voluptuous as vol @@ -103,8 +104,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NetAtmo Thermostat.""" - import pyatmo - homes_conf = config.get(CONF_HOMES) auth = hass.data[DATA_NETATMO_AUTH] @@ -365,8 +364,6 @@ def get_home_ids(self): def setup(self): """Retrieve HomeData by NetAtmo API.""" - import pyatmo - try: self.homedata = pyatmo.HomeData(self.auth) self.home_id = self.homedata.gethomeId(self.home) @@ -408,8 +405,6 @@ def get_room_ids(self): def setup(self): """Retrieve HomeData and HomeStatus by NetAtmo API.""" - import pyatmo - try: self.homedata = pyatmo.HomeData(self.auth) self.homestatus = pyatmo.HomeStatus(self.auth, home_id=self.home_id) @@ -423,8 +418,6 @@ def setup(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the NetAtmo API to update the data.""" - import pyatmo - try: self.homestatus = pyatmo.HomeStatus(self.auth, home_id=self.home_id) except TypeError: diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 9e68c078cdcb34..38e3753708e1b3 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -4,6 +4,7 @@ from datetime import timedelta from time import time +import pyatmo import requests import voluptuous as vol @@ -174,8 +175,6 @@ def _retry(_data): if _dev: add_entities(_dev, True) - import pyatmo - for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: try: data = NetatmoData(auth, data_class, config.get(CONF_STATION)) @@ -512,8 +511,6 @@ def __init__(self, auth, lat_ne, lon_ne, lat_sw, lon_sw): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Request an update from the Netatmo API.""" - import pyatmo - data = pyatmo.PublicData( self.auth, LAT_NE=self.lat_ne, @@ -559,12 +556,10 @@ def update(self): if time() < self._next_update or not self._update_in_progress.acquire(False): return try: - from pyatmo import NoDevice - try: self.station_data = self.data_class(self.auth) _LOGGER.debug("%s detected!", str(self.data_class.__name__)) - except NoDevice: + except pyatmo.NoDevice: _LOGGER.warning( "No Weather or HomeCoach devices found for %s", str(self.station) ) From 0cc2d0d557d28bf76cc11a09c9966729176aaaa6 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 10 Oct 2019 18:24:39 +1100 Subject: [PATCH 0720/3953] move import to top-level (#27353) --- homeassistant/components/onkyo/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 92e5f01d486861..d6117283da7ac2 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -3,6 +3,8 @@ from typing import List import voluptuous as vol +import eiscp +from eiscp import eISCP from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -133,9 +135,6 @@ def determine_zones(receiver): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Onkyo platform.""" - import eiscp - from eiscp import eISCP - host = config.get(CONF_HOST) hosts = [] From d337b71725d9964bc07e29772e68edd649f613d5 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 10 Oct 2019 18:25:21 +1100 Subject: [PATCH 0721/3953] move import to top-level (#27352) --- homeassistant/components/systemmonitor/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index ad2072baaa5230..b4621c59798625 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -3,6 +3,7 @@ import os import socket +import psutil import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -134,8 +135,6 @@ def unit_of_measurement(self): def update(self): """Get the latest system information.""" - import psutil - if self.type == "disk_use_percent": self._state = psutil.disk_usage(self.argument).percent elif self.type == "disk_use": From c188ecf79b4b0a335ee55fae5a02704fb7f29ddc Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 10 Oct 2019 14:21:42 +0200 Subject: [PATCH 0722/3953] Revert "Fix connection issues with withings API by switching to a maintained codebase (#27310)" (#27385) This reverts commit 071476343c10cdf8fe4a448082021eca3df7c8d7. --- homeassistant/components/withings/common.py | 14 +- .../components/withings/config_flow.py | 4 +- .../components/withings/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/withings/common.py | 12 +- tests/components/withings/conftest.py | 139 +++++++++--------- tests/components/withings/test_common.py | 20 +-- tests/components/withings/test_sensor.py | 44 +++--- 9 files changed, 117 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 9acca6f0cd68e6..f2be849cbc794b 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import re import time -import withings_api as withings +import nokia from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import TokenUpdated @@ -68,9 +68,7 @@ class WithingsDataManager: service_available = None - def __init__( - self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi - ): + def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): """Constructor.""" self._hass = hass self._api = api @@ -255,7 +253,7 @@ def create_withings_data_manager( """Set up the sensor config entry.""" entry_creds = entry.data.get(const.CREDENTIALS) or {} profile = entry.data[const.PROFILE] - credentials = withings.WithingsCredentials( + credentials = nokia.NokiaCredentials( entry_creds.get("access_token"), entry_creds.get("token_expiry"), entry_creds.get("token_type"), @@ -268,7 +266,7 @@ def create_withings_data_manager( def credentials_saver(credentials_param): _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - # Sanitizing the data as sometimes a WithingsCredentials object + # Sanitizing the data as sometimes a NokiaCredentials object # is passed through from the API. cred_data = credentials_param if not isinstance(credentials_param, dict): @@ -277,8 +275,8 @@ def credentials_saver(credentials_param): entry.data[const.CREDENTIALS] = cred_data hass.config_entries.async_update_entry(entry, data={**entry.data}) - _LOGGER.debug("Creating withings api instance") - api = withings.WithingsApi( + _LOGGER.debug("Creating nokia api instance") + api = nokia.NokiaApi( credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index c781e785f5e122..f28a4f59d80195 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ from typing import Optional import aiohttp -import withings_api as withings +import nokia import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -75,7 +75,7 @@ def get_auth_client(self, profile: str): profile, ) - return withings.WithingsAuth( + return nokia.NokiaAuth( client_id, client_secret, callback_uri, diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index ae5cd4bcdd9ee9..d38b69f2248bcc 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "withings-api==2.0.0b7" + "nokia==1.2.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index cc1071762616d3..01ef364fdc6943 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,6 +868,9 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 +# homeassistant.components.withings +nokia==1.2.0 + # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 @@ -1973,9 +1976,6 @@ websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 -# homeassistant.components.withings -withings-api==2.0.0b7 - # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ce7eeb54a4fc5..b14232fd5cdb61 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -297,6 +297,9 @@ nessclient==0.9.15 # homeassistant.components.ssdp netdisco==2.6.0 +# homeassistant.components.withings +nokia==1.2.0 + # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 @@ -615,9 +618,6 @@ watchdog==0.8.3 # homeassistant.components.webostv websockets==6.0 -# homeassistant.components.withings -withings-api==2.0.0b7 - # homeassistant.components.bluesound # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index f3839a1be5524c..b8406c39711e57 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,7 +1,7 @@ """Common data for for the withings component tests.""" import time -import withings_api as withings +import nokia import homeassistant.components.withings.const as const @@ -92,7 +92,7 @@ def new_measure(type_str, value, unit): } -def withings_sleep_response(states): +def nokia_sleep_response(states): """Create a sleep response based on states.""" data = [] for state in states: @@ -104,10 +104,10 @@ def withings_sleep_response(states): ) ) - return withings.WithingsSleep(new_sleep_data("aa", data)) + return nokia.NokiaSleep(new_sleep_data("aa", data)) -WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( +NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( { "updatetime": "", "timezone": "", @@ -174,7 +174,7 @@ def withings_sleep_response(states): ) -WITHINGS_SLEEP_RESPONSE = withings_sleep_response( +NOKIA_SLEEP_RESPONSE = nokia_sleep_response( [ const.MEASURE_TYPE_SLEEP_STATE_AWAKE, const.MEASURE_TYPE_SLEEP_STATE_LIGHT, @@ -183,7 +183,7 @@ def withings_sleep_response(states): ] ) -WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( +NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( { "series": [ new_sleep_summary( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 0aa6af0d7c098a..7cbe3dc1cd4cd0 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ from typing import Awaitable, Callable, List import asynctest -import withings_api as withings +import nokia import pytest import homeassistant.components.api as api @@ -15,9 +15,9 @@ from homeassistant.setup import async_setup_component from .common import ( - WITHINGS_MEASURES_RESPONSE, - WITHINGS_SLEEP_RESPONSE, - WITHINGS_SLEEP_SUMMARY_RESPONSE, + NOKIA_MEASURES_RESPONSE, + NOKIA_SLEEP_RESPONSE, + NOKIA_SLEEP_SUMMARY_RESPONSE, ) @@ -34,17 +34,17 @@ def __init__( measures: List[str] = None, unit_system: str = None, throttle_interval: int = const.THROTTLE_INTERVAL, - withings_request_response="DATA", - withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, - withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, - withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, + nokia_request_response="DATA", + nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, + nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, + nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, ) -> None: """Constructor.""" self._throttle_interval = throttle_interval - self._withings_request_response = withings_request_response - self._withings_measures_response = withings_measures_response - self._withings_sleep_response = withings_sleep_response - self._withings_sleep_summary_response = withings_sleep_summary_response + self._nokia_request_response = nokia_request_response + self._nokia_measures_response = nokia_measures_response + self._nokia_sleep_response = nokia_sleep_response + self._nokia_sleep_summary_response = nokia_sleep_summary_response self._withings_config = { const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret", @@ -103,24 +103,24 @@ def throttle_interval(self): return self._throttle_interval @property - def withings_request_response(self): + def nokia_request_response(self): """Request response.""" - return self._withings_request_response + return self._nokia_request_response @property - def withings_measures_response(self) -> withings.WithingsMeasures: + def nokia_measures_response(self) -> nokia.NokiaMeasures: """Measures response.""" - return self._withings_measures_response + return self._nokia_measures_response @property - def withings_sleep_response(self) -> withings.WithingsSleep: + def nokia_sleep_response(self) -> nokia.NokiaSleep: """Sleep response.""" - return self._withings_sleep_response + return self._nokia_sleep_response @property - def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: + def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: """Sleep summary response.""" - return self._withings_sleep_summary_response + return self._nokia_sleep_summary_response class WithingsFactoryData: @@ -130,21 +130,21 @@ def __init__( self, hass, flow_id, - withings_auth_get_credentials_mock, - withings_api_request_mock, - withings_api_get_measures_mock, - withings_api_get_sleep_mock, - withings_api_get_sleep_summary_mock, + nokia_auth_get_credentials_mock, + nokia_api_request_mock, + nokia_api_get_measures_mock, + nokia_api_get_sleep_mock, + nokia_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ): """Constructor.""" self._hass = hass self._flow_id = flow_id - self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock - self._withings_api_request_mock = withings_api_request_mock - self._withings_api_get_measures_mock = withings_api_get_measures_mock - self._withings_api_get_sleep_mock = withings_api_get_sleep_mock - self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock + self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock + self._nokia_api_request_mock = nokia_api_request_mock + self._nokia_api_get_measures_mock = nokia_api_get_measures_mock + self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock + self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock self._data_manager_get_throttle_interval_mock = ( data_manager_get_throttle_interval_mock ) @@ -160,29 +160,29 @@ def flow_id(self): return self._flow_id @property - def withings_auth_get_credentials_mock(self): + def nokia_auth_get_credentials_mock(self): """Get auth credentials mock.""" - return self._withings_auth_get_credentials_mock + return self._nokia_auth_get_credentials_mock @property - def withings_api_request_mock(self): + def nokia_api_request_mock(self): """Get request mock.""" - return self._withings_api_request_mock + return self._nokia_api_request_mock @property - def withings_api_get_measures_mock(self): + def nokia_api_get_measures_mock(self): """Get measures mock.""" - return self._withings_api_get_measures_mock + return self._nokia_api_get_measures_mock @property - def withings_api_get_sleep_mock(self): + def nokia_api_get_sleep_mock(self): """Get sleep mock.""" - return self._withings_api_get_sleep_mock + return self._nokia_api_get_sleep_mock @property - def withings_api_get_sleep_summary_mock(self): + def nokia_api_get_sleep_summary_mock(self): """Get sleep summary mock.""" - return self._withings_api_get_sleep_summary_mock + return self._nokia_api_get_sleep_summary_mock @property def data_manager_get_throttle_interval_mock(self): @@ -243,9 +243,9 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: assert await async_setup_component(hass, http.DOMAIN, config.hass_config) assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - withings_auth_get_credentials_patch = asynctest.patch( - "withings_api.WithingsAuth.get_credentials", - return_value=withings.WithingsCredentials( + nokia_auth_get_credentials_patch = asynctest.patch( + "nokia.NokiaAuth.get_credentials", + return_value=nokia.NokiaCredentials( access_token="my_access_token", token_expiry=time.time() + 600, token_type="my_token_type", @@ -255,33 +255,28 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: consumer_secret=config.withings_config.get(const.CLIENT_SECRET), ), ) - withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() + nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() - withings_api_request_patch = asynctest.patch( - "withings_api.WithingsApi.request", - return_value=config.withings_request_response, + nokia_api_request_patch = asynctest.patch( + "nokia.NokiaApi.request", return_value=config.nokia_request_response ) - withings_api_request_mock = withings_api_request_patch.start() + nokia_api_request_mock = nokia_api_request_patch.start() - withings_api_get_measures_patch = asynctest.patch( - "withings_api.WithingsApi.get_measures", - return_value=config.withings_measures_response, + nokia_api_get_measures_patch = asynctest.patch( + "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response ) - withings_api_get_measures_mock = withings_api_get_measures_patch.start() + nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() - withings_api_get_sleep_patch = asynctest.patch( - "withings_api.WithingsApi.get_sleep", - return_value=config.withings_sleep_response, + nokia_api_get_sleep_patch = asynctest.patch( + "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response ) - withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() + nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() - withings_api_get_sleep_summary_patch = asynctest.patch( - "withings_api.WithingsApi.get_sleep_summary", - return_value=config.withings_sleep_summary_response, - ) - withings_api_get_sleep_summary_mock = ( - withings_api_get_sleep_summary_patch.start() + nokia_api_get_sleep_summary_patch = asynctest.patch( + "nokia.NokiaApi.get_sleep_summary", + return_value=config.nokia_sleep_summary_response, ) + nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() data_manager_get_throttle_interval_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" @@ -300,11 +295,11 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: patches.extend( [ - withings_auth_get_credentials_patch, - withings_api_request_patch, - withings_api_get_measures_patch, - withings_api_get_sleep_patch, - withings_api_get_sleep_summary_patch, + nokia_auth_get_credentials_patch, + nokia_api_request_patch, + nokia_api_get_measures_patch, + nokia_api_get_sleep_patch, + nokia_api_get_sleep_summary_patch, data_manager_get_throttle_interval_patch, get_measures_patch, ] @@ -333,11 +328,11 @@ def create_task(*args): return WithingsFactoryData( hass, flow_id, - withings_auth_get_credentials_mock, - withings_api_request_mock, - withings_api_get_measures_mock, - withings_api_get_sleep_mock, - withings_api_get_sleep_summary_mock, + nokia_auth_get_credentials_mock, + nokia_api_request_mock, + nokia_api_get_measures_mock, + nokia_api_get_sleep_mock, + nokia_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 9f2480f9094e55..a22689f92bb6b5 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,6 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import withings_api as withings +import nokia from oauthlib.oauth2.rfc6749.errors import MissingTokenError import pytest from requests_oauthlib import TokenUpdated @@ -13,19 +13,19 @@ from homeassistant.exceptions import PlatformNotReady -@pytest.fixture(name="withings_api") -def withings_api_fixture(): - """Provide withings api.""" - withings_api = withings.WithingsApi.__new__(withings.WithingsApi) - withings_api.get_measures = MagicMock() - withings_api.get_sleep = MagicMock() - return withings_api +@pytest.fixture(name="nokia_api") +def nokia_api_fixture(): + """Provide nokia api.""" + nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) + nokia_api.get_measures = MagicMock() + nokia_api.get_sleep = MagicMock() + return nokia_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, withings_api: withings.WithingsApi): +def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): """Provide data manager.""" - return WithingsDataManager(hass, "My Profile", withings_api) + return WithingsDataManager(hass, "My Profile", nokia_api) def test_print_service(): diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 697d0a8b86413f..da77910097be89 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,12 +2,7 @@ from unittest.mock import MagicMock, patch import asynctest -from withings_api import ( - WithingsApi, - WithingsMeasures, - WithingsSleep, - WithingsSleepSummary, -) +from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary import pytest from homeassistant.components.withings import DOMAIN @@ -20,7 +15,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from .common import withings_sleep_response +from .common import nokia_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig @@ -125,9 +120,9 @@ async def test_health_sensor_state_none( data = await withings_factory( WithingsFactoryConfig( measures=measure, - withings_measures_response=None, - withings_sleep_response=None, - withings_sleep_summary_response=None, + nokia_measures_response=None, + nokia_sleep_response=None, + nokia_sleep_summary_response=None, ) ) @@ -158,9 +153,9 @@ async def test_health_sensor_state_empty( data = await withings_factory( WithingsFactoryConfig( measures=measure, - withings_measures_response=WithingsMeasures({"measuregrps": []}), - withings_sleep_response=WithingsSleep({"series": []}), - withings_sleep_summary_response=WithingsSleepSummary({"series": []}), + nokia_measures_response=NokiaMeasures({"measuregrps": []}), + nokia_sleep_response=NokiaSleep({"series": []}), + nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), ) ) @@ -206,8 +201,7 @@ async def test_sleep_state_throttled( data = await withings_factory( WithingsFactoryConfig( - measures=[measure], - withings_sleep_response=withings_sleep_response(sleep_states), + measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) ) ) @@ -263,16 +257,16 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): "expires_in": "2", } - original_withings_api = WithingsApi - withings_api_instance = None + original_nokia_api = NokiaApi + nokia_api_instance = None - def new_withings_api(*args, **kwargs): - nonlocal withings_api_instance - withings_api_instance = original_withings_api(*args, **kwargs) - withings_api_instance.request = MagicMock() - return withings_api_instance + def new_nokia_api(*args, **kwargs): + nonlocal nokia_api_instance + nokia_api_instance = original_nokia_api(*args, **kwargs) + nokia_api_instance.request = MagicMock() + return nokia_api_instance - withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) + nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( @@ -281,7 +275,7 @@ def new_withings_api(*args, **kwargs): wraps=hass.config_entries.async_update_entry, ) - with session_patch, client_patch, withings_api_patch, update_entry_patch: + with session_patch, client_patch, nokia_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( @@ -304,7 +298,7 @@ def new_withings_api(*args, **kwargs): await async_setup_entry(hass, config_entry, async_add_entities) - withings_api_instance.set_token(expected_creds) + nokia_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2" From 65372da2418a56eb026ed24f746a06ff81611af1 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 10 Oct 2019 14:21:42 +0200 Subject: [PATCH 0723/3953] Revert "Fix connection issues with withings API by switching to a maintained codebase (#27310)" (#27385) This reverts commit 071476343c10cdf8fe4a448082021eca3df7c8d7. --- homeassistant/components/withings/common.py | 14 +- .../components/withings/config_flow.py | 4 +- .../components/withings/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 3 - tests/components/withings/common.py | 12 +- tests/components/withings/conftest.py | 139 +++++++++--------- tests/components/withings/test_common.py | 20 +-- tests/components/withings/test_sensor.py | 44 +++--- 9 files changed, 114 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 9acca6f0cd68e6..f2be849cbc794b 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import re import time -import withings_api as withings +import nokia from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import TokenUpdated @@ -68,9 +68,7 @@ class WithingsDataManager: service_available = None - def __init__( - self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi - ): + def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): """Constructor.""" self._hass = hass self._api = api @@ -255,7 +253,7 @@ def create_withings_data_manager( """Set up the sensor config entry.""" entry_creds = entry.data.get(const.CREDENTIALS) or {} profile = entry.data[const.PROFILE] - credentials = withings.WithingsCredentials( + credentials = nokia.NokiaCredentials( entry_creds.get("access_token"), entry_creds.get("token_expiry"), entry_creds.get("token_type"), @@ -268,7 +266,7 @@ def create_withings_data_manager( def credentials_saver(credentials_param): _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - # Sanitizing the data as sometimes a WithingsCredentials object + # Sanitizing the data as sometimes a NokiaCredentials object # is passed through from the API. cred_data = credentials_param if not isinstance(credentials_param, dict): @@ -277,8 +275,8 @@ def credentials_saver(credentials_param): entry.data[const.CREDENTIALS] = cred_data hass.config_entries.async_update_entry(entry, data={**entry.data}) - _LOGGER.debug("Creating withings api instance") - api = withings.WithingsApi( + _LOGGER.debug("Creating nokia api instance") + api = nokia.NokiaApi( credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index c781e785f5e122..f28a4f59d80195 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ from typing import Optional import aiohttp -import withings_api as withings +import nokia import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -75,7 +75,7 @@ def get_auth_client(self, profile: str): profile, ) - return withings.WithingsAuth( + return nokia.NokiaAuth( client_id, client_secret, callback_uri, diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index ae5cd4bcdd9ee9..d38b69f2248bcc 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "withings-api==2.0.0b7" + "nokia==1.2.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 44c10f8ff296ef..e085782b178180 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,6 +865,9 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 +# homeassistant.components.withings +nokia==1.2.0 + # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 @@ -1970,9 +1973,6 @@ websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 -# homeassistant.components.withings -withings-api==2.0.0b7 - # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a8f51cd1956cb..f5d7301b0ed599 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -445,9 +445,6 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==1.1.6 -# homeassistant.components.withings -withings-api==2.0.0b7 - # homeassistant.components.zeroconf zeroconf==0.23.0 diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index f3839a1be5524c..b8406c39711e57 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,7 +1,7 @@ """Common data for for the withings component tests.""" import time -import withings_api as withings +import nokia import homeassistant.components.withings.const as const @@ -92,7 +92,7 @@ def new_measure(type_str, value, unit): } -def withings_sleep_response(states): +def nokia_sleep_response(states): """Create a sleep response based on states.""" data = [] for state in states: @@ -104,10 +104,10 @@ def withings_sleep_response(states): ) ) - return withings.WithingsSleep(new_sleep_data("aa", data)) + return nokia.NokiaSleep(new_sleep_data("aa", data)) -WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( +NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( { "updatetime": "", "timezone": "", @@ -174,7 +174,7 @@ def withings_sleep_response(states): ) -WITHINGS_SLEEP_RESPONSE = withings_sleep_response( +NOKIA_SLEEP_RESPONSE = nokia_sleep_response( [ const.MEASURE_TYPE_SLEEP_STATE_AWAKE, const.MEASURE_TYPE_SLEEP_STATE_LIGHT, @@ -183,7 +183,7 @@ def withings_sleep_response(states): ] ) -WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( +NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( { "series": [ new_sleep_summary( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 0aa6af0d7c098a..7cbe3dc1cd4cd0 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ from typing import Awaitable, Callable, List import asynctest -import withings_api as withings +import nokia import pytest import homeassistant.components.api as api @@ -15,9 +15,9 @@ from homeassistant.setup import async_setup_component from .common import ( - WITHINGS_MEASURES_RESPONSE, - WITHINGS_SLEEP_RESPONSE, - WITHINGS_SLEEP_SUMMARY_RESPONSE, + NOKIA_MEASURES_RESPONSE, + NOKIA_SLEEP_RESPONSE, + NOKIA_SLEEP_SUMMARY_RESPONSE, ) @@ -34,17 +34,17 @@ def __init__( measures: List[str] = None, unit_system: str = None, throttle_interval: int = const.THROTTLE_INTERVAL, - withings_request_response="DATA", - withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, - withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, - withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, + nokia_request_response="DATA", + nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, + nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, + nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, ) -> None: """Constructor.""" self._throttle_interval = throttle_interval - self._withings_request_response = withings_request_response - self._withings_measures_response = withings_measures_response - self._withings_sleep_response = withings_sleep_response - self._withings_sleep_summary_response = withings_sleep_summary_response + self._nokia_request_response = nokia_request_response + self._nokia_measures_response = nokia_measures_response + self._nokia_sleep_response = nokia_sleep_response + self._nokia_sleep_summary_response = nokia_sleep_summary_response self._withings_config = { const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret", @@ -103,24 +103,24 @@ def throttle_interval(self): return self._throttle_interval @property - def withings_request_response(self): + def nokia_request_response(self): """Request response.""" - return self._withings_request_response + return self._nokia_request_response @property - def withings_measures_response(self) -> withings.WithingsMeasures: + def nokia_measures_response(self) -> nokia.NokiaMeasures: """Measures response.""" - return self._withings_measures_response + return self._nokia_measures_response @property - def withings_sleep_response(self) -> withings.WithingsSleep: + def nokia_sleep_response(self) -> nokia.NokiaSleep: """Sleep response.""" - return self._withings_sleep_response + return self._nokia_sleep_response @property - def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: + def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: """Sleep summary response.""" - return self._withings_sleep_summary_response + return self._nokia_sleep_summary_response class WithingsFactoryData: @@ -130,21 +130,21 @@ def __init__( self, hass, flow_id, - withings_auth_get_credentials_mock, - withings_api_request_mock, - withings_api_get_measures_mock, - withings_api_get_sleep_mock, - withings_api_get_sleep_summary_mock, + nokia_auth_get_credentials_mock, + nokia_api_request_mock, + nokia_api_get_measures_mock, + nokia_api_get_sleep_mock, + nokia_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ): """Constructor.""" self._hass = hass self._flow_id = flow_id - self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock - self._withings_api_request_mock = withings_api_request_mock - self._withings_api_get_measures_mock = withings_api_get_measures_mock - self._withings_api_get_sleep_mock = withings_api_get_sleep_mock - self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock + self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock + self._nokia_api_request_mock = nokia_api_request_mock + self._nokia_api_get_measures_mock = nokia_api_get_measures_mock + self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock + self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock self._data_manager_get_throttle_interval_mock = ( data_manager_get_throttle_interval_mock ) @@ -160,29 +160,29 @@ def flow_id(self): return self._flow_id @property - def withings_auth_get_credentials_mock(self): + def nokia_auth_get_credentials_mock(self): """Get auth credentials mock.""" - return self._withings_auth_get_credentials_mock + return self._nokia_auth_get_credentials_mock @property - def withings_api_request_mock(self): + def nokia_api_request_mock(self): """Get request mock.""" - return self._withings_api_request_mock + return self._nokia_api_request_mock @property - def withings_api_get_measures_mock(self): + def nokia_api_get_measures_mock(self): """Get measures mock.""" - return self._withings_api_get_measures_mock + return self._nokia_api_get_measures_mock @property - def withings_api_get_sleep_mock(self): + def nokia_api_get_sleep_mock(self): """Get sleep mock.""" - return self._withings_api_get_sleep_mock + return self._nokia_api_get_sleep_mock @property - def withings_api_get_sleep_summary_mock(self): + def nokia_api_get_sleep_summary_mock(self): """Get sleep summary mock.""" - return self._withings_api_get_sleep_summary_mock + return self._nokia_api_get_sleep_summary_mock @property def data_manager_get_throttle_interval_mock(self): @@ -243,9 +243,9 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: assert await async_setup_component(hass, http.DOMAIN, config.hass_config) assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - withings_auth_get_credentials_patch = asynctest.patch( - "withings_api.WithingsAuth.get_credentials", - return_value=withings.WithingsCredentials( + nokia_auth_get_credentials_patch = asynctest.patch( + "nokia.NokiaAuth.get_credentials", + return_value=nokia.NokiaCredentials( access_token="my_access_token", token_expiry=time.time() + 600, token_type="my_token_type", @@ -255,33 +255,28 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: consumer_secret=config.withings_config.get(const.CLIENT_SECRET), ), ) - withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() + nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() - withings_api_request_patch = asynctest.patch( - "withings_api.WithingsApi.request", - return_value=config.withings_request_response, + nokia_api_request_patch = asynctest.patch( + "nokia.NokiaApi.request", return_value=config.nokia_request_response ) - withings_api_request_mock = withings_api_request_patch.start() + nokia_api_request_mock = nokia_api_request_patch.start() - withings_api_get_measures_patch = asynctest.patch( - "withings_api.WithingsApi.get_measures", - return_value=config.withings_measures_response, + nokia_api_get_measures_patch = asynctest.patch( + "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response ) - withings_api_get_measures_mock = withings_api_get_measures_patch.start() + nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() - withings_api_get_sleep_patch = asynctest.patch( - "withings_api.WithingsApi.get_sleep", - return_value=config.withings_sleep_response, + nokia_api_get_sleep_patch = asynctest.patch( + "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response ) - withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() + nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() - withings_api_get_sleep_summary_patch = asynctest.patch( - "withings_api.WithingsApi.get_sleep_summary", - return_value=config.withings_sleep_summary_response, - ) - withings_api_get_sleep_summary_mock = ( - withings_api_get_sleep_summary_patch.start() + nokia_api_get_sleep_summary_patch = asynctest.patch( + "nokia.NokiaApi.get_sleep_summary", + return_value=config.nokia_sleep_summary_response, ) + nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() data_manager_get_throttle_interval_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" @@ -300,11 +295,11 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: patches.extend( [ - withings_auth_get_credentials_patch, - withings_api_request_patch, - withings_api_get_measures_patch, - withings_api_get_sleep_patch, - withings_api_get_sleep_summary_patch, + nokia_auth_get_credentials_patch, + nokia_api_request_patch, + nokia_api_get_measures_patch, + nokia_api_get_sleep_patch, + nokia_api_get_sleep_summary_patch, data_manager_get_throttle_interval_patch, get_measures_patch, ] @@ -333,11 +328,11 @@ def create_task(*args): return WithingsFactoryData( hass, flow_id, - withings_auth_get_credentials_mock, - withings_api_request_mock, - withings_api_get_measures_mock, - withings_api_get_sleep_mock, - withings_api_get_sleep_summary_mock, + nokia_auth_get_credentials_mock, + nokia_api_request_mock, + nokia_api_get_measures_mock, + nokia_api_get_sleep_mock, + nokia_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 9f2480f9094e55..a22689f92bb6b5 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,6 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import withings_api as withings +import nokia from oauthlib.oauth2.rfc6749.errors import MissingTokenError import pytest from requests_oauthlib import TokenUpdated @@ -13,19 +13,19 @@ from homeassistant.exceptions import PlatformNotReady -@pytest.fixture(name="withings_api") -def withings_api_fixture(): - """Provide withings api.""" - withings_api = withings.WithingsApi.__new__(withings.WithingsApi) - withings_api.get_measures = MagicMock() - withings_api.get_sleep = MagicMock() - return withings_api +@pytest.fixture(name="nokia_api") +def nokia_api_fixture(): + """Provide nokia api.""" + nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) + nokia_api.get_measures = MagicMock() + nokia_api.get_sleep = MagicMock() + return nokia_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, withings_api: withings.WithingsApi): +def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): """Provide data manager.""" - return WithingsDataManager(hass, "My Profile", withings_api) + return WithingsDataManager(hass, "My Profile", nokia_api) def test_print_service(): diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 697d0a8b86413f..da77910097be89 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,12 +2,7 @@ from unittest.mock import MagicMock, patch import asynctest -from withings_api import ( - WithingsApi, - WithingsMeasures, - WithingsSleep, - WithingsSleepSummary, -) +from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary import pytest from homeassistant.components.withings import DOMAIN @@ -20,7 +15,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from .common import withings_sleep_response +from .common import nokia_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig @@ -125,9 +120,9 @@ async def test_health_sensor_state_none( data = await withings_factory( WithingsFactoryConfig( measures=measure, - withings_measures_response=None, - withings_sleep_response=None, - withings_sleep_summary_response=None, + nokia_measures_response=None, + nokia_sleep_response=None, + nokia_sleep_summary_response=None, ) ) @@ -158,9 +153,9 @@ async def test_health_sensor_state_empty( data = await withings_factory( WithingsFactoryConfig( measures=measure, - withings_measures_response=WithingsMeasures({"measuregrps": []}), - withings_sleep_response=WithingsSleep({"series": []}), - withings_sleep_summary_response=WithingsSleepSummary({"series": []}), + nokia_measures_response=NokiaMeasures({"measuregrps": []}), + nokia_sleep_response=NokiaSleep({"series": []}), + nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), ) ) @@ -206,8 +201,7 @@ async def test_sleep_state_throttled( data = await withings_factory( WithingsFactoryConfig( - measures=[measure], - withings_sleep_response=withings_sleep_response(sleep_states), + measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) ) ) @@ -263,16 +257,16 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): "expires_in": "2", } - original_withings_api = WithingsApi - withings_api_instance = None + original_nokia_api = NokiaApi + nokia_api_instance = None - def new_withings_api(*args, **kwargs): - nonlocal withings_api_instance - withings_api_instance = original_withings_api(*args, **kwargs) - withings_api_instance.request = MagicMock() - return withings_api_instance + def new_nokia_api(*args, **kwargs): + nonlocal nokia_api_instance + nokia_api_instance = original_nokia_api(*args, **kwargs) + nokia_api_instance.request = MagicMock() + return nokia_api_instance - withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) + nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( @@ -281,7 +275,7 @@ def new_withings_api(*args, **kwargs): wraps=hass.config_entries.async_update_entry, ) - with session_patch, client_patch, withings_api_patch, update_entry_patch: + with session_patch, client_patch, nokia_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( @@ -304,7 +298,7 @@ def new_withings_api(*args, **kwargs): await async_setup_entry(hass, config_entry, async_add_entities) - withings_api_instance.set_token(expected_creds) + nokia_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2" From 9f32e5cf46bc73dccb3b6d0ea951b20b79bc7f81 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Oct 2019 14:32:40 +0200 Subject: [PATCH 0724/3953] Bumped version to 0.100.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c5b566e5084fba..05be5f77cb4472 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 7398e06c782737cb06732704217edc46904b30d8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 10 Oct 2019 15:03:39 +0200 Subject: [PATCH 0725/3953] fix withings nokia test req --- requirements_test_all.txt | 3 +++ script/gen_requirements_all.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5d7301b0ed599..328bf6b614b8b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,6 +228,9 @@ minio==4.0.9 # homeassistant.components.ssdp netdisco==2.6.0 +# homeassistant.components.withings +nokia==1.2.0 + # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 8a33baabc2954f..ab2f5e51db42e5 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -181,7 +181,7 @@ "vultr", "wakeonlan", "warrant", - "withings-api", + "nokia", "YesssSMS", "zeroconf", "zigpy-homeassistant", From 95c537bee88fd2db7507024a1e32eb52364facbb Mon Sep 17 00:00:00 2001 From: Ryan Ewen Date: Thu, 10 Oct 2019 10:53:52 -0400 Subject: [PATCH 0726/3953] Allow Google Assistant relative volume control (#26585) * Allow Google Assistant volume control without volume_level * Add test for relative volume control w/o volume_level --- .../components/google_assistant/trait.py | 37 +++++++++++++------ .../components/google_assistant/test_trait.py | 29 +++++++++++++++ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 7d6e79a82372be..26c2e2ee0027e3 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1427,18 +1427,33 @@ async def _execute_set_volume(self, data, params): async def _execute_volume_relative(self, data, params): # This could also support up/down commands using relativeSteps relative = params["volumeRelativeLevel"] - current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) - await self.hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_VOLUME_SET, - { - ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_MEDIA_VOLUME_LEVEL: current + relative / 100, - }, - blocking=True, - context=data.context, - ) + # if we have access to current volume level, do a single 'set' call + if media_player.ATTR_MEDIA_VOLUME_LEVEL in self.state.attributes: + current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) + + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_VOLUME_SET, + { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: current + relative / 100, + }, + blocking=True, + context=data.context, + ) + # otherwise do multiple 'up' or 'down' calls + else: + for _ in range(abs(relative)): + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_VOLUME_UP + if relative > 0 + else media_player.SERVICE_VOLUME_DOWN, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) async def execute(self, command, data, params, challenge): """Execute a brightness command.""" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a5c527dacfe6e2..d58281b0e110d2 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1599,6 +1599,35 @@ async def test_volume_media_player_relative(hass): } +async def test_volume_media_player_relative_no_vol_lvl(hass): + """Test volume trait support for media player domain.""" + trt = trait.VolumeTrait( + hass, State("media_player.bla", media_player.STATE_PLAYING, {}), BASIC_CONFIG + ) + + assert trt.sync_attributes() == {} + + assert trt.query_attributes() == {} + + up_calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_UP + ) + + await trt.execute( + trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"volumeRelativeLevel": 2}, {} + ) + assert len(up_calls) == 2 + + down_calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_DOWN + ) + + await trt.execute( + trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"volumeRelativeLevel": -2}, {} + ) + assert len(down_calls) == 2 + + async def test_temperature_setting_sensor(hass): """Test TemperatureSetting trait support for temperature sensor.""" assert ( From 1719bc6fd3e6ba780b584812c443e037584096ef Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 10 Oct 2019 18:30:15 +0200 Subject: [PATCH 0727/3953] Remove hipchat (#27399) * Delete hipchat integration * Remove hipchat --- .coveragerc | 1 - homeassistant/components/hipchat/__init__.py | 1 - .../components/hipchat/manifest.json | 10 -- homeassistant/components/hipchat/notify.py | 108 ------------------ requirements_all.txt | 3 - 5 files changed, 123 deletions(-) delete mode 100644 homeassistant/components/hipchat/__init__.py delete mode 100644 homeassistant/components/hipchat/manifest.json delete mode 100644 homeassistant/components/hipchat/notify.py diff --git a/.coveragerc b/.coveragerc index 3de008439de501..d241260fdf0821 100644 --- a/.coveragerc +++ b/.coveragerc @@ -275,7 +275,6 @@ omit = homeassistant/components/heatmiser/climate.py homeassistant/components/hikvision/binary_sensor.py homeassistant/components/hikvisioncam/switch.py - homeassistant/components/hipchat/notify.py homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hive/* homeassistant/components/hlk_sw16/* diff --git a/homeassistant/components/hipchat/__init__.py b/homeassistant/components/hipchat/__init__.py deleted file mode 100644 index 8b79982fa43d67..00000000000000 --- a/homeassistant/components/hipchat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The hipchat component.""" diff --git a/homeassistant/components/hipchat/manifest.json b/homeassistant/components/hipchat/manifest.json deleted file mode 100644 index 9d563719a2e3f5..00000000000000 --- a/homeassistant/components/hipchat/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "hipchat", - "name": "Hipchat", - "documentation": "https://www.home-assistant.io/integrations/hipchat", - "requirements": [ - "hipnotify==1.0.8" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/hipchat/notify.py b/homeassistant/components/hipchat/notify.py deleted file mode 100644 index 03556db386a280..00000000000000 --- a/homeassistant/components/hipchat/notify.py +++ /dev/null @@ -1,108 +0,0 @@ -"""HipChat platform for notify component.""" -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_HOST, CONF_ROOM, CONF_TOKEN -import homeassistant.helpers.config_validation as cv - -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService, -) - -_LOGGER = logging.getLogger(__name__) - -CONF_COLOR = "color" -CONF_NOTIFY = "notify" -CONF_FORMAT = "format" - -DEFAULT_COLOR = "yellow" -DEFAULT_FORMAT = "text" -DEFAULT_HOST = "https://api.hipchat.com/" -DEFAULT_NOTIFY = False - -VALID_COLORS = {"yellow", "green", "red", "purple", "gray", "random"} -VALID_FORMATS = {"text", "html"} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ROOM): vol.Coerce(int), - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): vol.In(VALID_COLORS), - vol.Optional(CONF_FORMAT, default=DEFAULT_FORMAT): vol.In(VALID_FORMATS), - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NOTIFY, default=DEFAULT_NOTIFY): cv.boolean, - } -) - - -def get_service(hass, config, discovery_info=None): - """Get the HipChat notification service.""" - return HipchatNotificationService( - config[CONF_TOKEN], - config[CONF_ROOM], - config[CONF_COLOR], - config[CONF_NOTIFY], - config[CONF_FORMAT], - config[CONF_HOST], - ) - - -class HipchatNotificationService(BaseNotificationService): - """Implement the notification service for HipChat.""" - - def __init__( - self, token, default_room, default_color, default_notify, default_format, host - ): - """Initialize the service.""" - self._token = token - self._default_room = default_room - self._default_color = default_color - self._default_notify = default_notify - self._default_format = default_format - self._host = host - - self._rooms = {} - self._get_room(self._default_room) - - def _get_room(self, room): - """Get Room object, creating it if necessary.""" - from hipnotify import Room - - if room not in self._rooms: - self._rooms[room] = Room( - token=self._token, room_id=room, endpoint_url=self._host - ) - return self._rooms[room] - - def send_message(self, message="", **kwargs): - """Send a message.""" - color = self._default_color - notify = self._default_notify - message_format = self._default_format - - if kwargs.get(ATTR_DATA) is not None: - data = kwargs.get(ATTR_DATA) - if (data.get(CONF_COLOR) is not None) and ( - data.get(CONF_COLOR) in VALID_COLORS - ): - color = data.get(CONF_COLOR) - if (data.get(CONF_NOTIFY) is not None) and isinstance( - data.get(CONF_NOTIFY), bool - ): - notify = data.get(CONF_NOTIFY) - if (data.get(CONF_FORMAT) is not None) and ( - data.get(CONF_FORMAT) in VALID_FORMATS - ): - message_format = data.get(CONF_FORMAT) - - targets = kwargs.get(ATTR_TARGET, [self._default_room]) - - for target in targets: - room = self._get_room(target) - room.notify( - msg=message, color=color, notify=notify, message_format=message_format - ) diff --git a/requirements_all.txt b/requirements_all.txt index 01ef364fdc6943..001f199cdb8530 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -627,9 +627,6 @@ herepy==0.6.3.1 # homeassistant.components.hikvisioncam hikvision==0.4 -# homeassistant.components.hipchat -hipnotify==1.0.8 - # homeassistant.components.harman_kardon_avr hkavr==0.0.5 From 6c945c845e2f121cb6e323245b6cdc9b06a076f7 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 10 Oct 2019 19:30:30 +0300 Subject: [PATCH 0728/3953] Bump python-songpal (#27398) Fixes #24269 and fixes #26776 - potentially also #22116 --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 3160c4cee4b43a..2f0c44da47ba6a 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Songpal", "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ - "python-songpal==0.0.9.1" + "python-songpal==0.11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 001f199cdb8530..b079438e3bff21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1558,7 +1558,7 @@ python-ripple-api==0.0.3 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.0.9.1 +python-songpal==0.11 # homeassistant.components.synologydsm python-synology==0.2.0 From e93ffa56881e7f638ffb91afb6e5922c131de392 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 10 Oct 2019 18:48:59 +0200 Subject: [PATCH 0729/3953] Move imports in waze_travel_time component (#27384) --- homeassistant/components/waze_travel_time/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 340c0adbc9705e..4392a20d801d84 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -3,21 +3,22 @@ import logging import re +import WazeRouteCalculator import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, - CONF_NAME, - CONF_REGION, - EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE, - CONF_UNIT_SYSTEM_METRIC, + CONF_NAME, + CONF_REGION, CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, + EVENT_HOMEASSISTANT_START, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import location +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -237,7 +238,6 @@ def __init__( vehicle_type, ): """Set up WazeRouteCalculator.""" - import WazeRouteCalculator self._calc = WazeRouteCalculator From 7b13f0caf71f510a5a3eb62ba577eafda1668e90 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:50:58 +0200 Subject: [PATCH 0730/3953] Move imports in wemo component (#27393) --- homeassistant/components/wemo/__init__.py | 2 +- homeassistant/components/wemo/binary_sensor.py | 2 +- homeassistant/components/wemo/fan.py | 6 +++--- homeassistant/components/wemo/light.py | 4 ++-- homeassistant/components/wemo/switch.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 9e479991d15548..df2d8ed1f314f9 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,6 +1,7 @@ """Support for WeMo device discovery.""" import logging +import pywemo import requests import voluptuous as vol @@ -87,7 +88,6 @@ def setup(hass, config): async def async_setup_entry(hass, entry): """Set up a wemo config entry.""" - import pywemo config = hass.data[DOMAIN] diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 4ef18f29021b35..bc300fde5710c9 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -3,6 +3,7 @@ import logging import async_timeout +from pywemo import discovery import requests from homeassistant.components.binary_sensor import BinarySensorDevice @@ -15,7 +16,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Register discovered WeMo binary sensors.""" - from pywemo import discovery if discovery_info is not None: location = discovery_info["ssdp_description"] diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index dde5aa1cd89ed6..91273fa033fc30 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -3,11 +3,12 @@ import logging from datetime import timedelta -import requests import async_timeout +from pywemo import discovery +import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( DOMAIN, SUPPORT_SET_SPEED, @@ -96,7 +97,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up discovered WeMo humidifiers.""" - from pywemo import discovery if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index be6aa6f47f706c..dab96eb8c948cd 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -3,8 +3,9 @@ import logging from datetime import timedelta -import requests import async_timeout +from pywemo import discovery +import requests from homeassistant import util from homeassistant.components.light import ( @@ -35,7 +36,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up discovered WeMo switches.""" - from pywemo import discovery if discovery_info is not None: location = discovery_info["ssdp_description"] diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 1bc855069872fd..c1d07a069021ea 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -2,9 +2,10 @@ import asyncio import logging from datetime import datetime, timedelta -import requests import async_timeout +from pywemo import discovery +import requests from homeassistant.components.switch import SwitchDevice from homeassistant.exceptions import PlatformNotReady @@ -32,7 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up discovered WeMo switches.""" - from pywemo import discovery if discovery_info is not None: location = discovery_info["ssdp_description"] From 91379b0ff755a2138fd4d4ddeaba27940e16db6d Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:51:28 +0200 Subject: [PATCH 0731/3953] Move imports in wink component (#27392) --- homeassistant/components/wink/__init__.py | 13 +++++-------- .../components/wink/alarm_control_panel.py | 3 ++- homeassistant/components/wink/binary_sensor.py | 3 ++- homeassistant/components/wink/cover.py | 3 ++- homeassistant/components/wink/fan.py | 3 ++- homeassistant/components/wink/light.py | 3 ++- homeassistant/components/wink/lock.py | 2 +- homeassistant/components/wink/scene.py | 3 ++- homeassistant/components/wink/sensor.py | 3 ++- homeassistant/components/wink/switch.py | 3 ++- homeassistant/components/wink/water_heater.py | 3 ++- 11 files changed, 24 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index d0bb27c06e11b9..e2eb98938bb73c 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -5,6 +5,9 @@ import os import time +from aiohttp.web import Response +import pywink +from pubnubsubhandler import PubNubSubscriptionHandler import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -279,8 +282,6 @@ def wink_configuration_callback(callback_data): def setup(hass, config): """Set up the Wink component.""" - import pywink - from pubnubsubhandler import PubNubSubscriptionHandler if hass.data.get(DOMAIN) is None: hass.data[DOMAIN] = { @@ -689,8 +690,6 @@ def __init__(self, config, config_file, request_token): @callback def get(self, request): """Finish OAuth callback request.""" - from aiohttp import web - hass = request.app["hass"] data = request.query @@ -715,15 +714,13 @@ def get(self, request): hass.async_add_job(setup, hass, self.config) - return web.Response( + return Response( text=html_response.format(response_message), content_type="text/html" ) error_msg = "No code returned from Wink API" _LOGGER.error(error_msg) - return web.Response( - text=html_response.format(error_msg), content_type="text/html" - ) + return Response(text=html_response.format(error_msg), content_type="text/html") class WinkDevice(Entity): diff --git a/homeassistant/components/wink/alarm_control_panel.py b/homeassistant/components/wink/alarm_control_panel.py index 4708b6efee8a43..654252f5ffea84 100644 --- a/homeassistant/components/wink/alarm_control_panel.py +++ b/homeassistant/components/wink/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support Wink alarm control panels.""" import logging +import pywink + import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -17,7 +19,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for camera in pywink.get_cameras(): # get_cameras returns multiple device types. diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index e82a767fde8315..6dd22a3f7b8fd8 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Wink binary sensors.""" import logging +import pywink + from homeassistant.components.binary_sensor import BinarySensorDevice from . import DOMAIN, WinkDevice @@ -26,7 +28,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink binary sensor platform.""" - import pywink for sensor in pywink.get_sensors(): _id = sensor.object_id() + sensor.name() diff --git a/homeassistant/components/wink/cover.py b/homeassistant/components/wink/cover.py index fa39909512a36f..1ce7f9b8875a98 100644 --- a/homeassistant/components/wink/cover.py +++ b/homeassistant/components/wink/cover.py @@ -1,4 +1,6 @@ """Support for Wink covers.""" +import pywink + from homeassistant.components.cover import ATTR_POSITION, CoverDevice from . import DOMAIN, WinkDevice @@ -6,7 +8,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink cover platform.""" - import pywink for shade in pywink.get_shades(): _id = shade.object_id() + shade.name() diff --git a/homeassistant/components/wink/fan.py b/homeassistant/components/wink/fan.py index 9f5f2f9b3a0e36..d1d4e30ada3045 100644 --- a/homeassistant/components/wink/fan.py +++ b/homeassistant/components/wink/fan.py @@ -1,6 +1,8 @@ """Support for Wink fans.""" import logging +import pywink + from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -21,7 +23,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for fan in pywink.get_fans(): if fan.object_id() + fan.name() not in hass.data[DOMAIN]["unique_ids"]: diff --git a/homeassistant/components/wink/light.py b/homeassistant/components/wink/light.py index 76576f804fa80e..bd125e6a7c2fec 100644 --- a/homeassistant/components/wink/light.py +++ b/homeassistant/components/wink/light.py @@ -1,4 +1,6 @@ """Support for Wink lights.""" +import pywink + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -18,7 +20,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink lights.""" - import pywink for light in pywink.get_light_bulbs(): _id = light.object_id() + light.name() diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py index 5246fb49eed23e..37b27c0d500b80 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -1,6 +1,7 @@ """Support for Wink locks.""" import logging +import pywink import voluptuous as vol from homeassistant.components.lock import LockDevice @@ -70,7 +71,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for lock in pywink.get_locks(): _id = lock.object_id() + lock.name() diff --git a/homeassistant/components/wink/scene.py b/homeassistant/components/wink/scene.py index a00600ad784379..ff083598b2ebb2 100644 --- a/homeassistant/components/wink/scene.py +++ b/homeassistant/components/wink/scene.py @@ -1,6 +1,8 @@ """Support for Wink scenes.""" import logging +import pywink + from homeassistant.components.scene import Scene from . import DOMAIN, WinkDevice @@ -10,7 +12,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for scene in pywink.get_scenes(): _id = scene.object_id() + scene.name() diff --git a/homeassistant/components/wink/sensor.py b/homeassistant/components/wink/sensor.py index 030a1e5b9ec67e..2d0313ec2110c1 100644 --- a/homeassistant/components/wink/sensor.py +++ b/homeassistant/components/wink/sensor.py @@ -1,6 +1,8 @@ """Support for Wink sensors.""" import logging +import pywink + from homeassistant.const import TEMP_CELSIUS from . import DOMAIN, WinkDevice @@ -12,7 +14,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for sensor in pywink.get_sensors(): _id = sensor.object_id() + sensor.name() diff --git a/homeassistant/components/wink/switch.py b/homeassistant/components/wink/switch.py index 07d3ff4becc04c..cf2264e7eeb758 100644 --- a/homeassistant/components/wink/switch.py +++ b/homeassistant/components/wink/switch.py @@ -1,6 +1,8 @@ """Support for Wink switches.""" import logging +import pywink + from homeassistant.helpers.entity import ToggleEntity from . import DOMAIN, WinkDevice @@ -10,7 +12,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for switch in pywink.get_switches(): _id = switch.object_id() + switch.name() diff --git a/homeassistant/components/wink/water_heater.py b/homeassistant/components/wink/water_heater.py index 4fceeeb313d028..11330c7c9a5cab 100644 --- a/homeassistant/components/wink/water_heater.py +++ b/homeassistant/components/wink/water_heater.py @@ -1,6 +1,8 @@ """Support for Wink water heaters.""" import logging +import pywink + from homeassistant.components.water_heater import ( ATTR_TEMPERATURE, STATE_ECO, @@ -42,7 +44,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink water heater devices.""" - import pywink for water_heater in pywink.get_water_heaters(): _id = water_heater.object_id() + water_heater.name() From 84d1c0ca31ec6ca308ea41c30af0560c30aa5b72 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:52:03 +0200 Subject: [PATCH 0732/3953] Move imports in wunderlist component (#27391) --- homeassistant/components/wunderlist/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py index ce044499c63f76..122d09feaa4040 100644 --- a/homeassistant/components/wunderlist/__init__.py +++ b/homeassistant/components/wunderlist/__init__.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from wunderpy2 import WunderApi import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_ACCESS_TOKEN @@ -59,9 +60,7 @@ class Wunderlist: def __init__(self, access_token, client_id): """Create new instance of Wunderlist component.""" - import wunderpy2 - - api = wunderpy2.WunderApi() + api = WunderApi() self._client = api.get_client(access_token, client_id) _LOGGER.debug("Instance created") From a5ee138d563f096002aadbd0e14b0eff767f5ff6 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:52:19 +0200 Subject: [PATCH 0733/3953] Move imports in xmpp component (#27390) --- homeassistant/components/xmpp/notify.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 3719113f7c9d18..5aa9dbfffd11cd 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -7,6 +7,14 @@ import string import requests +import slixmpp +from slixmpp.exceptions import IqError, IqTimeout, XMPPError +from slixmpp.xmlstream.xmlstream import NotConnectedError +from slixmpp.plugins.xep_0363.http_upload import ( + FileTooBig, + FileUploadError, + UploadServiceNotFound, +) import voluptuous as vol from homeassistant.const import ( @@ -118,14 +126,6 @@ async def async_send_message( data=None, ): """Send a message over XMPP.""" - import slixmpp - from slixmpp.exceptions import IqError, IqTimeout, XMPPError - from slixmpp.xmlstream.xmlstream import NotConnectedError - from slixmpp.plugins.xep_0363.http_upload import ( - FileTooBig, - FileUploadError, - UploadServiceNotFound, - ) class SendNotificationBot(slixmpp.ClientXMPP): """Service for sending Jabber (XMPP) messages.""" From 19c8710698726ab2a57346b49628c14256cce4a7 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:53:27 +0200 Subject: [PATCH 0734/3953] Move imports in yamaha + yamaha_musiccast component (#27389) --- homeassistant/components/yamaha/media_player.py | 4 +--- homeassistant/components/yamaha_musiccast/media_player.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index e699ab74e680d9..eabb1ef34f1360 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -2,6 +2,7 @@ import logging import requests +import rxv import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -82,7 +83,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yamaha platform.""" - import rxv # Keep track of configured receivers so that we don't end up # discovering a receiver dynamically that we have static config @@ -336,8 +336,6 @@ def media_next_track(self): self._call_playback_function(self.receiver.next, "next track") def _call_playback_function(self, function, function_text): - import rxv - try: function() except rxv.exceptions.ResponseException: diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 38e606a0962cd1..18b80cc40855b0 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -1,6 +1,8 @@ """Support for Yamaha MusicCast Receivers.""" import logging +import socket +import pymusiccast import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -61,8 +63,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yamaha MusicCast platform.""" - import socket - import pymusiccast known_hosts = hass.data.get(KNOWN_HOSTS_KEY) if known_hosts is None: From f5560e2b18d5526a75166213af38f50c5ededc1b Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:53:52 +0200 Subject: [PATCH 0735/3953] Move imports in zengge component (#27387) --- homeassistant/components/zengge/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index a75cbba5f42afb..d890b193d729c0 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -1,6 +1,7 @@ """Support for Zengge lights.""" import logging +from zengge import zengge import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME @@ -47,12 +48,11 @@ class ZenggeLight(Light): def __init__(self, device): """Initialize the light.""" - import zengge self._name = device["name"] self._address = device["address"] self.is_valid = True - self._bulb = zengge.zengge(self._address) + self._bulb = zengge(self._address) self._white = 0 self._brightness = 0 self._hs_color = (0, 0) From ec08c251eaae3a76c7c62c614ddb1e447eba7535 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:54:20 +0200 Subject: [PATCH 0736/3953] Move imports in zestimate component (#27386) --- homeassistant/components/zestimate/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 703e3bf25a0cee..4b8bdf5fa2e681 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -3,6 +3,7 @@ import logging import requests +import xmltodict import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -101,7 +102,6 @@ def icon(self): def update(self): """Get the latest data and update the states.""" - import xmltodict try: response = requests.get(_RESOURCE, params=self.params, timeout=5) From 6364da115004f4f0074ff6ab17173526237ab3c7 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:56:07 +0200 Subject: [PATCH 0737/3953] Move imports in zigbee component (#27383) --- homeassistant/components/zigbee/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/zigbee/__init__.py index 31cbc0c65b6aa7..e74726a70f909f 100644 --- a/homeassistant/components/zigbee/__init__.py +++ b/homeassistant/components/zigbee/__init__.py @@ -2,6 +2,11 @@ import logging from binascii import hexlify, unhexlify +import xbee_helper.const as xb_const +from xbee_helper import ZigBee +from xbee_helper.device import convert_adc +from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure +from serial import Serial, SerialException import voluptuous as vol from homeassistant.const import ( @@ -75,12 +80,6 @@ def setup(hass, config): global ZIGBEE_EXCEPTION global ZIGBEE_TX_FAILURE - import xbee_helper.const as xb_const - from xbee_helper import ZigBee - from xbee_helper.device import convert_adc - from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure - from serial import Serial, SerialException - GPIO_DIGITAL_OUTPUT_LOW = xb_const.GPIO_DIGITAL_OUTPUT_LOW GPIO_DIGITAL_OUTPUT_HIGH = xb_const.GPIO_DIGITAL_OUTPUT_HIGH ADC_PERCENTAGE = xb_const.ADC_PERCENTAGE From fc7a20d1805134cbc0646a66ba298a66ef8274c1 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:57:00 +0200 Subject: [PATCH 0738/3953] Move imports in yr component (#27382) --- homeassistant/components/yr/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index 3d8c63621be977..f562f519ab57f1 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -7,6 +7,7 @@ import aiohttp import async_timeout +import xmltodict import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -155,7 +156,6 @@ def __init__(self, hass, coordinates, forecast, devices): async def fetching_data(self, *_): """Get the latest data from yr.no.""" - import xmltodict def try_again(err: str): """Retry in 15 to 20 minutes.""" From 99885b9acfe5679bfb5c192dfb74606095c1c0b3 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 10 Oct 2019 18:57:14 +0200 Subject: [PATCH 0739/3953] Move imports in google_travel_time component (#27381) --- .../components/google_travel_time/sensor.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 32947867958330..3ee72928fc1c16 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -1,25 +1,25 @@ """Support for Google travel time sensors.""" +from datetime import datetime, timedelta import logging -from datetime import datetime -from datetime import timedelta +import googlemaps import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, - EVENT_HOMEASSISTANT_START, + ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_ATTRIBUTION, + CONF_API_KEY, CONF_MODE, + CONF_NAME, + EVENT_HOMEASSISTANT_START, ) from homeassistant.helpers import location +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -203,8 +203,6 @@ def __init__(self, hass, name, api_key, origin, destination, options): else: self._destination = destination - import googlemaps - self._client = googlemaps.Client(api_key, timeout=10) try: self.update() From 13ac6ac315413fc24b0916c67961b35a0d6887c7 Mon Sep 17 00:00:00 2001 From: Markus Nigbur Date: Thu, 10 Oct 2019 20:16:19 +0200 Subject: [PATCH 0740/3953] Move imports in github component (#27406) --- homeassistant/components/github/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index a85364ebeca1a5..5e8200b41ab4b8 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -1,6 +1,7 @@ """Support for GitHub.""" from datetime import timedelta import logging +import github import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -148,8 +149,6 @@ class GitHubData: def __init__(self, repository, access_token=None, server_url=None): """Set up GitHub.""" - import github - self._github = github self.setup_error = False From 27f036c691c0fd53cc54ee59e263117968d546b9 Mon Sep 17 00:00:00 2001 From: Markus Nigbur Date: Thu, 10 Oct 2019 20:16:30 +0200 Subject: [PATCH 0741/3953] Move imports in eufy component (#27405) --- homeassistant/components/eufy/__init__.py | 2 +- homeassistant/components/eufy/light.py | 2 +- homeassistant/components/eufy/switch.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index df6aed3582f6a5..191d6ab5315c8e 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -1,5 +1,6 @@ """Support for Eufy devices.""" import logging +import lakeside import voluptuous as vol @@ -56,7 +57,6 @@ def setup(hass, config): """Set up Eufy devices.""" - import lakeside if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]: data = lakeside.get_devices( diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index f5359e6f2f6a49..21c26606bdd7ae 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -1,5 +1,6 @@ """Support for Eufy lights.""" import logging +import lakeside from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -36,7 +37,6 @@ class EufyLight(Light): def __init__(self, device): """Initialize the light.""" - import lakeside self._temp = None self._brightness = None diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index 3d05ef5d351bf4..2e13886dd2a5a7 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -1,5 +1,6 @@ """Support for Eufy switches.""" import logging +import lakeside from homeassistant.components.switch import SwitchDevice @@ -18,7 +19,6 @@ class EufySwitch(SwitchDevice): def __init__(self, device): """Initialize the light.""" - import lakeside self._state = None self._name = device["name"] From 77490a3246b98f9f34c296e50d8277b8b57b38a7 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Thu, 10 Oct 2019 11:22:36 -0700 Subject: [PATCH 0742/3953] Vangorra withings fix (#27404) * Fixing connection issues with withings API by switching to a maintained client codebase. * Updating requirements files. * Adding withings api to requirements script. * Using version of withings api with static version in setup.py. * Updating requirements files. --- homeassistant/components/withings/common.py | 14 +- .../components/withings/config_flow.py | 4 +- .../components/withings/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/withings/common.py | 12 +- tests/components/withings/conftest.py | 139 +++++++++--------- tests/components/withings/test_common.py | 20 +-- tests/components/withings/test_sensor.py | 44 +++--- 9 files changed, 130 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index f2be849cbc794b..9acca6f0cd68e6 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import re import time -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import TokenUpdated @@ -68,7 +68,9 @@ class WithingsDataManager: service_available = None - def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): + def __init__( + self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi + ): """Constructor.""" self._hass = hass self._api = api @@ -253,7 +255,7 @@ def create_withings_data_manager( """Set up the sensor config entry.""" entry_creds = entry.data.get(const.CREDENTIALS) or {} profile = entry.data[const.PROFILE] - credentials = nokia.NokiaCredentials( + credentials = withings.WithingsCredentials( entry_creds.get("access_token"), entry_creds.get("token_expiry"), entry_creds.get("token_type"), @@ -266,7 +268,7 @@ def create_withings_data_manager( def credentials_saver(credentials_param): _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - # Sanitizing the data as sometimes a NokiaCredentials object + # Sanitizing the data as sometimes a WithingsCredentials object # is passed through from the API. cred_data = credentials_param if not isinstance(credentials_param, dict): @@ -275,8 +277,8 @@ def credentials_saver(credentials_param): entry.data[const.CREDENTIALS] = cred_data hass.config_entries.async_update_entry(entry, data={**entry.data}) - _LOGGER.debug("Creating nokia api instance") - api = nokia.NokiaApi( + _LOGGER.debug("Creating withings api instance") + api = withings.WithingsApi( credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index f28a4f59d80195..c781e785f5e122 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ from typing import Optional import aiohttp -import nokia +import withings_api as withings import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -75,7 +75,7 @@ def get_auth_client(self, profile: str): profile, ) - return nokia.NokiaAuth( + return withings.WithingsAuth( client_id, client_secret, callback_uri, diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index d38b69f2248bcc..7c6e4ec044afaf 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "nokia==1.2.0" + "withings-api==2.0.0b8" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index b079438e3bff21..7e9d78afb4699d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,9 +865,6 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 @@ -1973,6 +1970,9 @@ websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 +# homeassistant.components.withings +withings-api==2.0.0b8 + # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b14232fd5cdb61..0e40702b9d9315 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -297,9 +297,6 @@ nessclient==0.9.15 # homeassistant.components.ssdp netdisco==2.6.0 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 @@ -618,6 +615,9 @@ watchdog==0.8.3 # homeassistant.components.webostv websockets==6.0 +# homeassistant.components.withings +withings-api==2.0.0b8 + # homeassistant.components.bluesound # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b8406c39711e57..f3839a1be5524c 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,7 +1,7 @@ """Common data for for the withings component tests.""" import time -import nokia +import withings_api as withings import homeassistant.components.withings.const as const @@ -92,7 +92,7 @@ def new_measure(type_str, value, unit): } -def nokia_sleep_response(states): +def withings_sleep_response(states): """Create a sleep response based on states.""" data = [] for state in states: @@ -104,10 +104,10 @@ def nokia_sleep_response(states): ) ) - return nokia.NokiaSleep(new_sleep_data("aa", data)) + return withings.WithingsSleep(new_sleep_data("aa", data)) -NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( +WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( { "updatetime": "", "timezone": "", @@ -174,7 +174,7 @@ def nokia_sleep_response(states): ) -NOKIA_SLEEP_RESPONSE = nokia_sleep_response( +WITHINGS_SLEEP_RESPONSE = withings_sleep_response( [ const.MEASURE_TYPE_SLEEP_STATE_AWAKE, const.MEASURE_TYPE_SLEEP_STATE_LIGHT, @@ -183,7 +183,7 @@ def nokia_sleep_response(states): ] ) -NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( +WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( { "series": [ new_sleep_summary( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 7cbe3dc1cd4cd0..0aa6af0d7c098a 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ from typing import Awaitable, Callable, List import asynctest -import nokia +import withings_api as withings import pytest import homeassistant.components.api as api @@ -15,9 +15,9 @@ from homeassistant.setup import async_setup_component from .common import ( - NOKIA_MEASURES_RESPONSE, - NOKIA_SLEEP_RESPONSE, - NOKIA_SLEEP_SUMMARY_RESPONSE, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_SLEEP_RESPONSE, + WITHINGS_SLEEP_SUMMARY_RESPONSE, ) @@ -34,17 +34,17 @@ def __init__( measures: List[str] = None, unit_system: str = None, throttle_interval: int = const.THROTTLE_INTERVAL, - nokia_request_response="DATA", - nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, - nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, - nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, + withings_request_response="DATA", + withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, + withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, + withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, ) -> None: """Constructor.""" self._throttle_interval = throttle_interval - self._nokia_request_response = nokia_request_response - self._nokia_measures_response = nokia_measures_response - self._nokia_sleep_response = nokia_sleep_response - self._nokia_sleep_summary_response = nokia_sleep_summary_response + self._withings_request_response = withings_request_response + self._withings_measures_response = withings_measures_response + self._withings_sleep_response = withings_sleep_response + self._withings_sleep_summary_response = withings_sleep_summary_response self._withings_config = { const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret", @@ -103,24 +103,24 @@ def throttle_interval(self): return self._throttle_interval @property - def nokia_request_response(self): + def withings_request_response(self): """Request response.""" - return self._nokia_request_response + return self._withings_request_response @property - def nokia_measures_response(self) -> nokia.NokiaMeasures: + def withings_measures_response(self) -> withings.WithingsMeasures: """Measures response.""" - return self._nokia_measures_response + return self._withings_measures_response @property - def nokia_sleep_response(self) -> nokia.NokiaSleep: + def withings_sleep_response(self) -> withings.WithingsSleep: """Sleep response.""" - return self._nokia_sleep_response + return self._withings_sleep_response @property - def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: + def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: """Sleep summary response.""" - return self._nokia_sleep_summary_response + return self._withings_sleep_summary_response class WithingsFactoryData: @@ -130,21 +130,21 @@ def __init__( self, hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ): """Constructor.""" self._hass = hass self._flow_id = flow_id - self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock - self._nokia_api_request_mock = nokia_api_request_mock - self._nokia_api_get_measures_mock = nokia_api_get_measures_mock - self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock - self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock + self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock + self._withings_api_request_mock = withings_api_request_mock + self._withings_api_get_measures_mock = withings_api_get_measures_mock + self._withings_api_get_sleep_mock = withings_api_get_sleep_mock + self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock self._data_manager_get_throttle_interval_mock = ( data_manager_get_throttle_interval_mock ) @@ -160,29 +160,29 @@ def flow_id(self): return self._flow_id @property - def nokia_auth_get_credentials_mock(self): + def withings_auth_get_credentials_mock(self): """Get auth credentials mock.""" - return self._nokia_auth_get_credentials_mock + return self._withings_auth_get_credentials_mock @property - def nokia_api_request_mock(self): + def withings_api_request_mock(self): """Get request mock.""" - return self._nokia_api_request_mock + return self._withings_api_request_mock @property - def nokia_api_get_measures_mock(self): + def withings_api_get_measures_mock(self): """Get measures mock.""" - return self._nokia_api_get_measures_mock + return self._withings_api_get_measures_mock @property - def nokia_api_get_sleep_mock(self): + def withings_api_get_sleep_mock(self): """Get sleep mock.""" - return self._nokia_api_get_sleep_mock + return self._withings_api_get_sleep_mock @property - def nokia_api_get_sleep_summary_mock(self): + def withings_api_get_sleep_summary_mock(self): """Get sleep summary mock.""" - return self._nokia_api_get_sleep_summary_mock + return self._withings_api_get_sleep_summary_mock @property def data_manager_get_throttle_interval_mock(self): @@ -243,9 +243,9 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: assert await async_setup_component(hass, http.DOMAIN, config.hass_config) assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - nokia_auth_get_credentials_patch = asynctest.patch( - "nokia.NokiaAuth.get_credentials", - return_value=nokia.NokiaCredentials( + withings_auth_get_credentials_patch = asynctest.patch( + "withings_api.WithingsAuth.get_credentials", + return_value=withings.WithingsCredentials( access_token="my_access_token", token_expiry=time.time() + 600, token_type="my_token_type", @@ -255,28 +255,33 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: consumer_secret=config.withings_config.get(const.CLIENT_SECRET), ), ) - nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() + withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() - nokia_api_request_patch = asynctest.patch( - "nokia.NokiaApi.request", return_value=config.nokia_request_response + withings_api_request_patch = asynctest.patch( + "withings_api.WithingsApi.request", + return_value=config.withings_request_response, ) - nokia_api_request_mock = nokia_api_request_patch.start() + withings_api_request_mock = withings_api_request_patch.start() - nokia_api_get_measures_patch = asynctest.patch( - "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response + withings_api_get_measures_patch = asynctest.patch( + "withings_api.WithingsApi.get_measures", + return_value=config.withings_measures_response, ) - nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() + withings_api_get_measures_mock = withings_api_get_measures_patch.start() - nokia_api_get_sleep_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response + withings_api_get_sleep_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep", + return_value=config.withings_sleep_response, ) - nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() + withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() - nokia_api_get_sleep_summary_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep_summary", - return_value=config.nokia_sleep_summary_response, + withings_api_get_sleep_summary_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep_summary", + return_value=config.withings_sleep_summary_response, + ) + withings_api_get_sleep_summary_mock = ( + withings_api_get_sleep_summary_patch.start() ) - nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() data_manager_get_throttle_interval_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" @@ -295,11 +300,11 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: patches.extend( [ - nokia_auth_get_credentials_patch, - nokia_api_request_patch, - nokia_api_get_measures_patch, - nokia_api_get_sleep_patch, - nokia_api_get_sleep_summary_patch, + withings_auth_get_credentials_patch, + withings_api_request_patch, + withings_api_get_measures_patch, + withings_api_get_sleep_patch, + withings_api_get_sleep_summary_patch, data_manager_get_throttle_interval_patch, get_measures_patch, ] @@ -328,11 +333,11 @@ def create_task(*args): return WithingsFactoryData( hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index a22689f92bb6b5..9f2480f9094e55 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,6 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError import pytest from requests_oauthlib import TokenUpdated @@ -13,19 +13,19 @@ from homeassistant.exceptions import PlatformNotReady -@pytest.fixture(name="nokia_api") -def nokia_api_fixture(): - """Provide nokia api.""" - nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) - nokia_api.get_measures = MagicMock() - nokia_api.get_sleep = MagicMock() - return nokia_api +@pytest.fixture(name="withings_api") +def withings_api_fixture(): + """Provide withings api.""" + withings_api = withings.WithingsApi.__new__(withings.WithingsApi) + withings_api.get_measures = MagicMock() + withings_api.get_sleep = MagicMock() + return withings_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): +def data_manager_fixture(hass, withings_api: withings.WithingsApi): """Provide data manager.""" - return WithingsDataManager(hass, "My Profile", nokia_api) + return WithingsDataManager(hass, "My Profile", withings_api) def test_print_service(): diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index da77910097be89..697d0a8b86413f 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,7 +2,12 @@ from unittest.mock import MagicMock, patch import asynctest -from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary +from withings_api import ( + WithingsApi, + WithingsMeasures, + WithingsSleep, + WithingsSleepSummary, +) import pytest from homeassistant.components.withings import DOMAIN @@ -15,7 +20,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from .common import nokia_sleep_response +from .common import withings_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig @@ -120,9 +125,9 @@ async def test_health_sensor_state_none( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=None, - nokia_sleep_response=None, - nokia_sleep_summary_response=None, + withings_measures_response=None, + withings_sleep_response=None, + withings_sleep_summary_response=None, ) ) @@ -153,9 +158,9 @@ async def test_health_sensor_state_empty( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=NokiaMeasures({"measuregrps": []}), - nokia_sleep_response=NokiaSleep({"series": []}), - nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), + withings_measures_response=WithingsMeasures({"measuregrps": []}), + withings_sleep_response=WithingsSleep({"series": []}), + withings_sleep_summary_response=WithingsSleepSummary({"series": []}), ) ) @@ -201,7 +206,8 @@ async def test_sleep_state_throttled( data = await withings_factory( WithingsFactoryConfig( - measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) + measures=[measure], + withings_sleep_response=withings_sleep_response(sleep_states), ) ) @@ -257,16 +263,16 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): "expires_in": "2", } - original_nokia_api = NokiaApi - nokia_api_instance = None + original_withings_api = WithingsApi + withings_api_instance = None - def new_nokia_api(*args, **kwargs): - nonlocal nokia_api_instance - nokia_api_instance = original_nokia_api(*args, **kwargs) - nokia_api_instance.request = MagicMock() - return nokia_api_instance + def new_withings_api(*args, **kwargs): + nonlocal withings_api_instance + withings_api_instance = original_withings_api(*args, **kwargs) + withings_api_instance.request = MagicMock() + return withings_api_instance - nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) + withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( @@ -275,7 +281,7 @@ def new_nokia_api(*args, **kwargs): wraps=hass.config_entries.async_update_entry, ) - with session_patch, client_patch, nokia_api_patch, update_entry_patch: + with session_patch, client_patch, withings_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( @@ -298,7 +304,7 @@ def new_nokia_api(*args, **kwargs): await async_setup_entry(hass, config_entry, async_add_entities) - nokia_api_instance.set_token(expected_creds) + withings_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2" From 7e916773627ef889444d71675644099976ca5bc5 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 20:39:09 +0200 Subject: [PATCH 0743/3953] Move imports in apple_tv component (#27356) * Move imports in apple_tv component * Fix pylint --- homeassistant/components/apple_tv/__init__.py | 19 +++++++------- .../components/apple_tv/media_player.py | 26 +++++++++---------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 51c2ee7e1a5f9f..38d520f73daa35 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -3,13 +3,15 @@ import logging from typing import Sequence, TypeVar, Union +from pyatv import AppleTVDevice, connect_to_apple_tv, scan_for_apple_tvs +from pyatv.exceptions import DeviceAuthenticationError import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.discovery import SERVICE_APPLE_TV from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -80,7 +82,6 @@ def request_configuration(hass, config, atv, credentials): async def configuration_callback(callback_data): """Handle the submitted configuration.""" - from pyatv import exceptions pin = callback_data.get("pin") @@ -93,7 +94,7 @@ async def configuration_callback(callback_data): title=NOTIFICATION_AUTH_TITLE, notification_id=NOTIFICATION_AUTH_ID, ) - except exceptions.DeviceAuthenticationError as ex: + except DeviceAuthenticationError as ex: hass.components.persistent_notification.async_create( "Authentication failed! Did you enter correct PIN?

" "Details: {0}".format(ex), @@ -112,11 +113,10 @@ async def configuration_callback(callback_data): ) -async def scan_for_apple_tvs(hass): +async def scan_apple_tvs(hass): """Scan for devices and present a notification of the ones found.""" - import pyatv - atvs = await pyatv.scan_for_apple_tvs(hass.loop, timeout=3) + atvs = await scan_for_apple_tvs(hass.loop, timeout=3) devices = [] for atv in atvs: @@ -149,7 +149,7 @@ async def async_service_handler(service): entity_ids = service.data.get(ATTR_ENTITY_ID) if service.service == SERVICE_SCAN: - hass.async_add_job(scan_for_apple_tvs, hass) + hass.async_add_job(scan_apple_tvs, hass) return if entity_ids: @@ -207,7 +207,6 @@ async def atv_discovered(service, info): async def _setup_atv(hass, hass_config, atv_config): """Set up an Apple TV.""" - import pyatv name = atv_config.get(CONF_NAME) host = atv_config.get(CONF_HOST) @@ -218,9 +217,9 @@ async def _setup_atv(hass, hass_config, atv_config): if host in hass.data[DATA_APPLE_TV]: return - details = pyatv.AppleTVDevice(name, host, login_id) + details = AppleTVDevice(name, host, login_id) session = async_get_clientsession(hass) - atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session) + atv = connect_to_apple_tv(details, hass.loop, session=session) if credentials: await atv.airplay.load_credentials(credentials) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 9ac5ba77f98d48..c816be52259ffb 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,6 +1,8 @@ """Support for Apple TV media player.""" import logging +import pyatv.const as atv_const + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, @@ -112,22 +114,21 @@ def state(self): return STATE_OFF if self._playing: - from pyatv import const state = self._playing.play_state if state in ( - const.PLAY_STATE_IDLE, - const.PLAY_STATE_NO_MEDIA, - const.PLAY_STATE_LOADING, + atv_const.PLAY_STATE_IDLE, + atv_const.PLAY_STATE_NO_MEDIA, + atv_const.PLAY_STATE_LOADING, ): return STATE_IDLE - if state == const.PLAY_STATE_PLAYING: + if state == atv_const.PLAY_STATE_PLAYING: return STATE_PLAYING if state in ( - const.PLAY_STATE_PAUSED, - const.PLAY_STATE_FAST_FORWARD, - const.PLAY_STATE_FAST_BACKWARD, - const.PLAY_STATE_STOPPED, + atv_const.PLAY_STATE_PAUSED, + atv_const.PLAY_STATE_FAST_FORWARD, + atv_const.PLAY_STATE_FAST_BACKWARD, + atv_const.PLAY_STATE_STOPPED, ): # Catch fast forward/backward here so "play" is default action return STATE_PAUSED @@ -156,14 +157,13 @@ def playstatus_error(self, updater, exception): def media_content_type(self): """Content type of current playing media.""" if self._playing: - from pyatv import const media_type = self._playing.media_type - if media_type == const.MEDIA_TYPE_VIDEO: + if media_type == atv_const.MEDIA_TYPE_VIDEO: return MEDIA_TYPE_VIDEO - if media_type == const.MEDIA_TYPE_MUSIC: + if media_type == atv_const.MEDIA_TYPE_MUSIC: return MEDIA_TYPE_MUSIC - if media_type == const.MEDIA_TYPE_TV: + if media_type == atv_const.MEDIA_TYPE_TV: return MEDIA_TYPE_TVSHOW @property From 2e9e8a16bd001358c2f865c998fdad3a68f2edfa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Oct 2019 20:51:04 +0200 Subject: [PATCH 0744/3953] Remove hydroquebec integration (ADR-0004) (#27407) --- .../components/hydroquebec/__init__.py | 1 - .../components/hydroquebec/manifest.json | 10 - .../components/hydroquebec/sensor.py | 227 ------------------ requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/hydroquebec/__init__.py | 1 - tests/components/hydroquebec/test_sensor.py | 102 -------- 7 files changed, 347 deletions(-) delete mode 100644 homeassistant/components/hydroquebec/__init__.py delete mode 100644 homeassistant/components/hydroquebec/manifest.json delete mode 100644 homeassistant/components/hydroquebec/sensor.py delete mode 100644 tests/components/hydroquebec/__init__.py delete mode 100644 tests/components/hydroquebec/test_sensor.py diff --git a/homeassistant/components/hydroquebec/__init__.py b/homeassistant/components/hydroquebec/__init__.py deleted file mode 100644 index 08a12f7955e0b2..00000000000000 --- a/homeassistant/components/hydroquebec/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The hydroquebec component.""" diff --git a/homeassistant/components/hydroquebec/manifest.json b/homeassistant/components/hydroquebec/manifest.json deleted file mode 100644 index dbe8af0b41b125..00000000000000 --- a/homeassistant/components/hydroquebec/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "hydroquebec", - "name": "Hydroquebec", - "documentation": "https://www.home-assistant.io/integrations/hydroquebec", - "requirements": [ - "pyhydroquebec==2.2.2" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/hydroquebec/sensor.py b/homeassistant/components/hydroquebec/sensor.py deleted file mode 100644 index c3ad79c1c9866e..00000000000000 --- a/homeassistant/components/hydroquebec/sensor.py +++ /dev/null @@ -1,227 +0,0 @@ -""" -Support for HydroQuebec. - -Get data from 'My Consumption Profile' page: -https://www.hydroquebec.com/portail/en/group/clientele/portrait-de-consommation - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.hydroquebec/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - ENERGY_KILO_WATT_HOUR, - CONF_NAME, - CONF_MONITORED_VARIABLES, - TEMP_CELSIUS, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -KILOWATT_HOUR = ENERGY_KILO_WATT_HOUR -PRICE = "CAD" -DAYS = "days" -CONF_CONTRACT = "contract" - -DEFAULT_NAME = "HydroQuebec" - -REQUESTS_TIMEOUT = 15 -MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) -SCAN_INTERVAL = timedelta(hours=1) - -SENSOR_TYPES = { - "balance": ["Balance", PRICE, "mdi:square-inc-cash"], - "period_total_bill": ["Period total bill", PRICE, "mdi:square-inc-cash"], - "period_length": ["Period length", DAYS, "mdi:calendar-today"], - "period_total_days": ["Period total days", DAYS, "mdi:calendar-today"], - "period_mean_daily_bill": ["Period mean daily bill", PRICE, "mdi:square-inc-cash"], - "period_mean_daily_consumption": [ - "Period mean daily consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "period_total_consumption": [ - "Period total consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "period_lower_price_consumption": [ - "Period lower price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "period_higher_price_consumption": [ - "Period higher price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_total_consumption": [ - "Yesterday total consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_lower_price_consumption": [ - "Yesterday lower price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_higher_price_consumption": [ - "Yesterday higher price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_average_temperature": [ - "Yesterday average temperature", - TEMP_CELSIUS, - "mdi:thermometer", - ], - "period_average_temperature": [ - "Period average temperature", - TEMP_CELSIUS, - "mdi:thermometer", - ], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MONITORED_VARIABLES): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_CONTRACT): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - -HOST = "https://www.hydroquebec.com" -HOME_URL = f"{HOST}/portail/web/clientele/authentification" -PROFILE_URL = "{}/portail/fr/group/clientele/" "portrait-de-consommation".format(HOST) -MONTHLY_MAP = ( - ("period_total_bill", "montantFacturePeriode"), - ("period_length", "nbJourLecturePeriode"), - ("period_total_days", "nbJourPrevuPeriode"), - ("period_mean_daily_bill", "moyenneDollarsJourPeriode"), - ("period_mean_daily_consumption", "moyenneKwhJourPeriode"), - ("period_total_consumption", "consoTotalPeriode"), - ("period_lower_price_consumption", "consoRegPeriode"), - ("period_higher_price_consumption", "consoHautPeriode"), -) -DAILY_MAP = ( - ("yesterday_total_consumption", "consoTotalQuot"), - ("yesterday_lower_price_consumption", "consoRegQuot"), - ("yesterday_higher_price_consumption", "consoHautQuot"), -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the HydroQuebec sensor.""" - # Create a data fetcher to support all of the configured sensors. Then make - # the first call to init the data. - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - contract = config.get(CONF_CONTRACT) - - httpsession = hass.helpers.aiohttp_client.async_get_clientsession() - hydroquebec_data = HydroquebecData(username, password, httpsession, contract) - contracts = await hydroquebec_data.get_contract_list() - if not contracts: - return - _LOGGER.info("Contract list: %s", ", ".join(contracts)) - - name = config.get(CONF_NAME) - - sensors = [] - for variable in config[CONF_MONITORED_VARIABLES]: - sensors.append(HydroQuebecSensor(hydroquebec_data, variable, name)) - - async_add_entities(sensors, True) - - -class HydroQuebecSensor(Entity): - """Implementation of a HydroQuebec sensor.""" - - def __init__(self, hydroquebec_data, sensor_type, name): - """Initialize the sensor.""" - self.client_name = name - self.type = sensor_type - self._name = SENSOR_TYPES[sensor_type][0] - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._icon = SENSOR_TYPES[sensor_type][2] - self.hydroquebec_data = hydroquebec_data - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - async def async_update(self): - """Get the latest data from Hydroquebec and update the state.""" - await self.hydroquebec_data.async_update() - if self.hydroquebec_data.data.get(self.type) is not None: - self._state = round(self.hydroquebec_data.data[self.type], 2) - - -class HydroquebecData: - """Get data from HydroQuebec.""" - - def __init__(self, username, password, httpsession, contract=None): - """Initialize the data object.""" - from pyhydroquebec import HydroQuebecClient - - self.client = HydroQuebecClient( - username, password, REQUESTS_TIMEOUT, httpsession - ) - self._contract = contract - self.data = {} - - async def get_contract_list(self): - """Return the contract list.""" - # Fetch data - ret = await self._fetch_data() - if ret: - return self.client.get_contracts() - return [] - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def _fetch_data(self): - """Fetch latest data from HydroQuebec.""" - from pyhydroquebec.client import PyHydroQuebecError - - try: - await self.client.fetch_data() - except PyHydroQuebecError as exp: - _LOGGER.error("Error on receive last Hydroquebec data: %s", exp) - return False - return True - - async def async_update(self): - """Return the latest collected data from HydroQuebec.""" - await self._fetch_data() - self.data = self.client.get_data(self._contract)[self._contract] diff --git a/requirements_all.txt b/requirements_all.txt index 7e9d78afb4699d..8a215b6350bba7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1231,9 +1231,6 @@ pyhomematic==0.1.60 # homeassistant.components.homeworks pyhomeworks==0.0.6 -# homeassistant.components.hydroquebec -pyhydroquebec==2.2.2 - # homeassistant.components.ialarm pyialarm==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e40702b9d9315..1bc01f6e73edeb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -419,9 +419,6 @@ pyheos==0.6.0 # homeassistant.components.homematic pyhomematic==0.1.60 -# homeassistant.components.hydroquebec -pyhydroquebec==2.2.2 - # homeassistant.components.ipma pyipma==1.2.1 diff --git a/tests/components/hydroquebec/__init__.py b/tests/components/hydroquebec/__init__.py deleted file mode 100644 index 1342395d265a4f..00000000000000 --- a/tests/components/hydroquebec/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the hydroquebec component.""" diff --git a/tests/components/hydroquebec/test_sensor.py b/tests/components/hydroquebec/test_sensor.py deleted file mode 100644 index 9b2dd5ab5b58b9..00000000000000 --- a/tests/components/hydroquebec/test_sensor.py +++ /dev/null @@ -1,102 +0,0 @@ -"""The test for the hydroquebec sensor platform.""" -import asyncio -import logging -import sys -from unittest.mock import MagicMock - -from homeassistant.bootstrap import async_setup_component -from homeassistant.components.hydroquebec import sensor as hydroquebec -from tests.common import assert_setup_component - - -CONTRACT = "123456789" - - -class HydroQuebecClientMock: - """Fake Hydroquebec client.""" - - def __init__(self, username, password, contract=None, httpsession=None): - """Fake Hydroquebec client init.""" - pass - - def get_data(self, contract): - """Return fake hydroquebec data.""" - return {CONTRACT: {"balance": 160.12}} - - def get_contracts(self): - """Return fake hydroquebec contracts.""" - return [CONTRACT] - - @asyncio.coroutine - def fetch_data(self): - """Return fake fetching data.""" - pass - - -class HydroQuebecClientMockError(HydroQuebecClientMock): - """Fake Hydroquebec client error.""" - - def get_contracts(self): - """Return fake hydroquebec contracts.""" - return [] - - @asyncio.coroutine - def fetch_data(self): - """Return fake fetching data.""" - raise PyHydroQuebecErrorMock("Fake Error") - - -class PyHydroQuebecErrorMock(BaseException): - """Fake PyHydroquebec Error.""" - - -class PyHydroQuebecClientFakeModule: - """Fake pyfido.client module.""" - - PyHydroQuebecError = PyHydroQuebecErrorMock - - -class PyHydroQuebecFakeModule: - """Fake pyfido module.""" - - HydroQuebecClient = HydroQuebecClientMockError - - -@asyncio.coroutine -def test_hydroquebec_sensor(loop, hass): - """Test the Hydroquebec number sensor.""" - sys.modules["pyhydroquebec"] = MagicMock() - sys.modules["pyhydroquebec.client"] = MagicMock() - sys.modules["pyhydroquebec.client.PyHydroQuebecError"] = PyHydroQuebecErrorMock - import pyhydroquebec.client - - pyhydroquebec.HydroQuebecClient = HydroQuebecClientMock - pyhydroquebec.client.PyHydroQuebecError = PyHydroQuebecErrorMock - config = { - "sensor": { - "platform": "hydroquebec", - "name": "hydro", - "contract": CONTRACT, - "username": "myusername", - "password": "password", - "monitored_variables": ["balance"], - } - } - with assert_setup_component(1): - yield from async_setup_component(hass, "sensor", config) - state = hass.states.get("sensor.hydro_balance") - assert state.state == "160.12" - assert state.attributes.get("unit_of_measurement") == "CAD" - - -@asyncio.coroutine -def test_error(hass, caplog): - """Test the Hydroquebec sensor errors.""" - caplog.set_level(logging.ERROR) - sys.modules["pyhydroquebec"] = PyHydroQuebecFakeModule() - sys.modules["pyhydroquebec.client"] = PyHydroQuebecClientFakeModule() - - config = {} - fake_async_add_entities = MagicMock() - yield from hydroquebec.async_setup_platform(hass, config, fake_async_add_entities) - assert fake_async_add_entities.called is False From 9e3005133a6e72e7895bdf38a64b0eab3512cdd6 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 10 Oct 2019 21:57:48 +0300 Subject: [PATCH 0745/3953] Standardize times in time sensors Jewish calendar (#26940) * Standardize times in time sensors Jewish calendar * Fix pylint errors * Add non-default time format test * Make black happy * Remove timestamp device class Timestamp device class requires ISO 8601 format * Revert "Remove timestamp device class" This reverts commit 8a2fda39831bc750c3a77aa774b84b054d78032c. * Remove time_format As this is part of the UI decision, it should be decided by lovelace. A nice addition for a future PR, might be the option to hint to lovelace the preferred way to display some data. * Update name of state_attributes * State of timestamp variable to be shown in UTC Although I don't understand it, I give up :) * Remove unnecessary attributes I don't really see the value in these attributes, if there are any they should be implemented in the sensor component for the timestamp device class --- .../components/jewish_calendar/sensor.py | 114 ++++++++++++------ .../components/jewish_calendar/test_sensor.py | 16 ++- 2 files changed, 88 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 405838b1fb10f9..453b3de4bae31e 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -23,7 +23,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for sensor, sensor_info in SENSOR_TYPES["data"].items() ] sensors.extend( - JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info) + JewishCalendarTimeSensor(hass.data[DOMAIN], sensor, sensor_info) for sensor, sensor_info in SENSOR_TYPES["time"].items() ) @@ -63,7 +63,7 @@ def state(self): async def async_update(self): """Update the state of the sensor.""" now = dt_util.now() - _LOGGER.debug("Now: %s Timezone = %s", now, now.tzinfo) + _LOGGER.debug("Now: %s Location: %r", now, self._location) today = now.date() sunset = dt_util.as_local( @@ -72,16 +72,6 @@ async def async_update(self): _LOGGER.debug("Now: %s Sunset: %s", now, sunset) - def make_zmanim(date): - """Create a Zmanim object.""" - return hdate.Zmanim( - date=date, - location=self._location, - candle_lighting_offset=self._candle_lighting_offset, - havdalah_offset=self._havdalah_offset, - hebrew=self._hebrew, - ) - date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew) # The Jewish day starts after darkness (called "tzais") and finishes at @@ -92,7 +82,7 @@ def make_zmanim(date): # tomorrow based on sunset ("shkia"), for others based on "tzais". # Hence the following variables. after_tzais_date = after_shkia_date = date - today_times = make_zmanim(today) + today_times = self.make_zmanim(today) if now > sunset: after_shkia_date = date.next_day @@ -100,37 +90,83 @@ def make_zmanim(date): if today_times.havdalah and now > today_times.havdalah: after_tzais_date = date.next_day + self._state = self.get_state(after_shkia_date, after_tzais_date) + _LOGGER.debug("New value for %s: %s", self._type, self._state) + + def make_zmanim(self, date): + """Create a Zmanim object.""" + return hdate.Zmanim( + date=date, + location=self._location, + candle_lighting_offset=self._candle_lighting_offset, + havdalah_offset=self._havdalah_offset, + hebrew=self._hebrew, + ) + + def get_state(self, after_shkia_date, after_tzais_date): + """For a given type of sensor, return the state.""" # Terminology note: by convention in py-libhdate library, "upcoming" # refers to "current" or "upcoming" dates. if self._type == "date": - self._state = after_shkia_date.hebrew_date - elif self._type == "weekly_portion": + return after_shkia_date.hebrew_date + if self._type == "weekly_portion": # Compute the weekly portion based on the upcoming shabbat. - self._state = after_tzais_date.upcoming_shabbat.parasha - elif self._type == "holiday_name": - self._state = after_shkia_date.holiday_description - elif self._type == "holiday_type": - self._state = after_shkia_date.holiday_type - elif self._type == "upcoming_shabbat_candle_lighting": - times = make_zmanim(after_tzais_date.upcoming_shabbat.previous_day.gdate) - self._state = times.candle_lighting - elif self._type == "upcoming_candle_lighting": - times = make_zmanim( + return after_tzais_date.upcoming_shabbat.parasha + if self._type == "holiday_name": + return after_shkia_date.holiday_description + if self._type == "holiday_type": + return after_shkia_date.holiday_type + if self._type == "omer_count": + return after_shkia_date.omer_day + + return None + + +class JewishCalendarTimeSensor(JewishCalendarSensor): + """Implement attrbutes for sensors returning times.""" + + @property + def state(self): + """Return the state of the sensor.""" + return dt_util.as_utc(self._state) if self._state is not None else None + + @property + def device_class(self): + """Return the class of this sensor.""" + return "timestamp" + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = {} + + if self._state is None: + return attrs + + attrs["timestamp"] = self._state.timestamp() + + return attrs + + def get_state(self, after_shkia_date, after_tzais_date): + """For a given type of sensor, return the state.""" + if self._type == "upcoming_shabbat_candle_lighting": + times = self.make_zmanim( + after_tzais_date.upcoming_shabbat.previous_day.gdate + ) + return times.candle_lighting + if self._type == "upcoming_candle_lighting": + times = self.make_zmanim( after_tzais_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate ) - self._state = times.candle_lighting - elif self._type == "upcoming_shabbat_havdalah": - times = make_zmanim(after_tzais_date.upcoming_shabbat.gdate) - self._state = times.havdalah - elif self._type == "upcoming_havdalah": - times = make_zmanim( + return times.candle_lighting + if self._type == "upcoming_shabbat_havdalah": + times = self.make_zmanim(after_tzais_date.upcoming_shabbat.gdate) + return times.havdalah + if self._type == "upcoming_havdalah": + times = self.make_zmanim( after_tzais_date.upcoming_shabbat_or_yom_tov.last_day.gdate ) - self._state = times.havdalah - elif self._type == "omer_count": - self._state = after_shkia_date.omer_day - else: - times = make_zmanim(today).zmanim - self._state = times[self._type].time() - - _LOGGER.debug("New value: %s", self._state) + return times.havdalah + + times = self.make_zmanim(dt_util.now()).zmanim + return times[self._type] diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 8d72830b3698ab..94b26f80d2dfce 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -1,5 +1,5 @@ """The tests for the Jewish calendar sensors.""" -from datetime import time, timedelta +from datetime import timedelta from datetime import datetime as dt import pytest @@ -81,7 +81,7 @@ async def test_jewish_calendar_hebrew(hass): "hebrew", "t_set_hakochavim", True, - time(19, 48), + dt(2018, 9, 8, 19, 48), ), ( dt(2018, 9, 8), @@ -91,7 +91,7 @@ async def test_jewish_calendar_hebrew(hass): "hebrew", "t_set_hakochavim", False, - time(19, 21), + dt(2018, 9, 8, 19, 21), ), ( dt(2018, 10, 14), @@ -183,6 +183,10 @@ async def test_jewish_calendar_sensor( async_fire_time_changed(hass, future) await hass.async_block_till_done() + result = ( + dt_util.as_utc(time_zone.localize(result)) if isinstance(result, dt) else result + ) + assert hass.states.get(f"sensor.test_{sensor}").state == str(result) @@ -524,6 +528,12 @@ async def test_shabbat_times_sensor( sensor_type = sensor_type.replace(f"{language}_", "") + result_value = ( + dt_util.as_utc(result_value) + if isinstance(result_value, dt) + else result_value + ) + assert hass.states.get(f"sensor.test_{sensor_type}").state == str( result_value ), f"Value for {sensor_type}" From 2ab6eb4fa03a0ab698e4ec6cba8c6c5915a72b04 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Oct 2019 12:46:40 -0700 Subject: [PATCH 0746/3953] Revert "Allow Google Assistant relative volume control (#26585)" (#27416) This reverts commit 95c537bee88fd2db7507024a1e32eb52364facbb. --- .../components/google_assistant/trait.py | 37 ++++++------------- .../components/google_assistant/test_trait.py | 29 --------------- 2 files changed, 11 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 26c2e2ee0027e3..7d6e79a82372be 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1427,33 +1427,18 @@ async def _execute_set_volume(self, data, params): async def _execute_volume_relative(self, data, params): # This could also support up/down commands using relativeSteps relative = params["volumeRelativeLevel"] + current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) - # if we have access to current volume level, do a single 'set' call - if media_player.ATTR_MEDIA_VOLUME_LEVEL in self.state.attributes: - current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) - - await self.hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_VOLUME_SET, - { - ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_MEDIA_VOLUME_LEVEL: current + relative / 100, - }, - blocking=True, - context=data.context, - ) - # otherwise do multiple 'up' or 'down' calls - else: - for _ in range(abs(relative)): - await self.hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_VOLUME_UP - if relative > 0 - else media_player.SERVICE_VOLUME_DOWN, - {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, - context=data.context, - ) + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_VOLUME_SET, + { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: current + relative / 100, + }, + blocking=True, + context=data.context, + ) async def execute(self, command, data, params, challenge): """Execute a brightness command.""" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index d58281b0e110d2..a5c527dacfe6e2 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1599,35 +1599,6 @@ async def test_volume_media_player_relative(hass): } -async def test_volume_media_player_relative_no_vol_lvl(hass): - """Test volume trait support for media player domain.""" - trt = trait.VolumeTrait( - hass, State("media_player.bla", media_player.STATE_PLAYING, {}), BASIC_CONFIG - ) - - assert trt.sync_attributes() == {} - - assert trt.query_attributes() == {} - - up_calls = async_mock_service( - hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_UP - ) - - await trt.execute( - trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"volumeRelativeLevel": 2}, {} - ) - assert len(up_calls) == 2 - - down_calls = async_mock_service( - hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_DOWN - ) - - await trt.execute( - trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"volumeRelativeLevel": -2}, {} - ) - assert len(down_calls) == 2 - - async def test_temperature_setting_sensor(hass): """Test TemperatureSetting trait support for temperature sensor.""" assert ( From 4f4bbedc581fc7d538e1e1f05e5af968f67cab74 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 10 Oct 2019 22:52:29 +0300 Subject: [PATCH 0747/3953] bump songpal to fix attrs usage when using its most recent version (#27410) --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 2f0c44da47ba6a..55b02b66a59bc4 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Songpal", "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ - "python-songpal==0.11" + "python-songpal==0.11.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8a215b6350bba7..bdb130ac9aacdd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1552,7 +1552,7 @@ python-ripple-api==0.0.3 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.11 +python-songpal==0.11.1 # homeassistant.components.synologydsm python-synology==0.2.0 From aecf7e65ff11e97f0762f54613ac42b17a7c935f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Oct 2019 21:52:40 +0200 Subject: [PATCH 0748/3953] Bump aiohttp to 3.6.2 (#27409) --- 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 fb6239c807087e..4bdad0b62f32be 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiohttp==3.6.1 +aiohttp==3.6.2 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index bdb130ac9aacdd..d58d61c018833a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.6.1 +aiohttp==3.6.2 astral==1.10.1 async_timeout==3.0.1 attrs==19.2.0 diff --git a/setup.py b/setup.py index f4e94faf553cf9..33d4ede25511f5 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.6.1", + "aiohttp==3.6.2", "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.2.0", From ed3516186bead77af07391dd2220c47fa002c0ab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Oct 2019 21:52:54 +0200 Subject: [PATCH 0749/3953] Bump sqlalchemy to 1.3.10 (#27408) --- homeassistant/components/recorder/manifest.json | 4 ++-- homeassistant/components/sql/manifest.json | 4 ++-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index d349560e3852bc..1f00cf89f158be 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -3,8 +3,8 @@ "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", "requirements": [ - "sqlalchemy==1.3.9" + "sqlalchemy==1.3.10" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 5a87c813bc5f3a..fa641adc839e0f 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -3,10 +3,10 @@ "name": "Sql", "documentation": "https://www.home-assistant.io/integrations/sql", "requirements": [ - "sqlalchemy==1.3.9" + "sqlalchemy==1.3.10" ], "dependencies": [], "codeowners": [ "@dgomes" ] -} +} \ No newline at end of file diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4bdad0b62f32be..f74bdac2337dd6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.9 +sqlalchemy==1.3.10 voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index d58d61c018833a..2a0987c596ba7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1814,7 +1814,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.9 +sqlalchemy==1.3.10 # homeassistant.components.starlingbank starlingbank==3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1bc01f6e73edeb..84cef21a1fe5ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -564,7 +564,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.9 +sqlalchemy==1.3.10 # homeassistant.components.statsd statsd==3.2.1 From 4b8a35dffb003e593480a0213252794afc155407 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 10 Oct 2019 22:53:05 +0300 Subject: [PATCH 0750/3953] move songpal imports to top (#27402) * move songpal imports to top * Update media_player.py --- homeassistant/components/songpal/media_player.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 9f5ae9d1ac6682..0567cd0ea6a297 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -4,6 +4,14 @@ from collections import OrderedDict import voluptuous as vol +from songpal import ( + Device, + SongpalException, + VolumeChange, + ContentChange, + PowerChange, + ConnectChange, +) from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -60,8 +68,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Songpal platform.""" - from songpal import SongpalException - if PLATFORM not in hass.data: hass.data[PLATFORM] = {} @@ -117,8 +123,6 @@ class SongpalDevice(MediaPlayerDevice): def __init__(self, name, endpoint, poll=False): """Init.""" - from songpal import Device - self._name = name self._endpoint = endpoint self._poll = poll @@ -151,7 +155,6 @@ async def initialize(self): async def async_activate_websocket(self): """Activate websocket for listening if wanted.""" _LOGGER.info("Activating websocket connection..") - from songpal import VolumeChange, ContentChange, PowerChange, ConnectChange async def _volume_changed(volume: VolumeChange): _LOGGER.debug("Volume changed: %s", volume) @@ -230,8 +233,6 @@ async def async_set_sound_setting(self, name, value): async def async_update(self): """Fetch updates from the device.""" - from songpal import SongpalException - try: volumes = await self.dev.get_volume_information() if not volumes: From 4c71c6df6f584bf220d1f9051ba7df55317c46b1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 11 Oct 2019 00:31:40 +0000 Subject: [PATCH 0751/3953] [ci skip] Translation update --- .../components/airly/.translations/fr.json | 1 + .../binary_sensor/.translations/da.json | 2 ++ .../binary_sensor/.translations/fr.json | 22 +++++++++++++++++++ .../binary_sensor/.translations/it.json | 2 ++ .../components/ecobee/.translations/fr.json | 1 + .../opentherm_gw/.translations/fr.json | 1 + .../components/plex/.translations/fr.json | 1 + .../components/zha/.translations/fr.json | 19 ++++++++++++++++ 8 files changed, 49 insertions(+) diff --git a/homeassistant/components/airly/.translations/fr.json b/homeassistant/components/airly/.translations/fr.json index cf756a9f4928b9..374e578eed29cb 100644 --- a/homeassistant/components/airly/.translations/fr.json +++ b/homeassistant/components/airly/.translations/fr.json @@ -13,6 +13,7 @@ "longitude": "Longitude", "name": "Nom de l'int\u00e9gration" }, + "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air Airly. Pour g\u00e9n\u00e9rer une cl\u00e9 API, rendez-vous sur https://developer.airly.eu/register.", "title": "Airly" } }, diff --git a/homeassistant/components/binary_sensor/.translations/da.json b/homeassistant/components/binary_sensor/.translations/da.json index 56822c2365ce71..f7bd834561c11e 100644 --- a/homeassistant/components/binary_sensor/.translations/da.json +++ b/homeassistant/components/binary_sensor/.translations/da.json @@ -39,6 +39,7 @@ "closed": "{entity_name} lukket", "cold": "{entity_name} blev kold", "connected": "{entity_name} tilsluttet", + "moist": "{entity_name} blev fugtig", "moist\u00a7": "{entity_name} blev fugtig", "motion": "{entity_name} begyndte at registrere bev\u00e6gelse", "moving": "{entity_name} begyndte at bev\u00e6ge sig", @@ -53,6 +54,7 @@ "not_hot": "{entity_name} blev ikke varm", "not_locked": "{entity_name} l\u00e5st op", "not_moist": "{entity_name} blev t\u00f8r", + "not_opened": "{entity_name} lukket", "not_present": "{entity_name} ikke til stede", "not_unsafe": "{entity_name} blev sikker", "occupied": "{entity_name} blev optaget", diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json index 1a11bfa4bc26e4..9a04ea17747f95 100644 --- a/homeassistant/components/binary_sensor/.translations/fr.json +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -50,7 +50,29 @@ "cold": "{entity_name} est devenu froid", "connected": "{entity_name} connect\u00e9", "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz", + "hot": "{entity_name} est devenu chaud", + "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter la lumi\u00e8re", + "locked": "{entity_name} verrouill\u00e9", + "moist": "{entity_name} est devenu humide", + "moist\u00a7": "{entity_name} est devenu humide", + "motion": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du mouvement", + "moving": "{entity_name} a commenc\u00e9 \u00e0 se d\u00e9placer", + "no_gas": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le gaz", + "no_light": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter la lumi\u00e8re", + "no_motion": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le mouvement", + "no_problem": "{entity_name} a cess\u00e9 de d\u00e9tecter un probl\u00e8me", + "no_smoke": "{entity_name} a cess\u00e9 de d\u00e9tecter de la fum\u00e9e", + "no_sound": "{entity_name} a cess\u00e9 de d\u00e9tecter du bruit", + "no_vibration": "{entity_name} a cess\u00e9 de d\u00e9tecter des vibrations", + "not_bat_low": "{entity_name} batterie normale", + "not_cold": "{entity_name} n'est plus froid", + "not_connected": "{entity_name} d\u00e9connect\u00e9", + "not_hot": "{entity_name} n'est plus chaud", + "not_locked": "{entity_name} d\u00e9verrouill\u00e9", + "not_moist": "{entity_name} est devenu sec", + "not_moving": "{entity_name} a cess\u00e9 de bouger", "not_occupied": "{entity_name} est devenu non occup\u00e9", + "not_opened": "{entity_name} ferm\u00e9", "not_plugged_in": "{entity_name} d\u00e9branch\u00e9", "not_powered": "{entity_name} non aliment\u00e9", "not_present": "{entity_name} non pr\u00e9sent", diff --git a/homeassistant/components/binary_sensor/.translations/it.json b/homeassistant/components/binary_sensor/.translations/it.json index 0583a4d4f74c09..c69f5a07a41fb7 100644 --- a/homeassistant/components/binary_sensor/.translations/it.json +++ b/homeassistant/components/binary_sensor/.translations/it.json @@ -53,6 +53,7 @@ "hot": "{entity_name} \u00e8 diventato caldo", "light": "{entity_name} ha iniziato a rilevare la luce", "locked": "{entity_name} bloccato", + "moist": "{entity_name} diventato umido", "moist\u00a7": "{entity_name} \u00e8 diventato umido", "motion": "{entity_name} ha iniziato a rilevare il movimento", "moving": "{entity_name} ha iniziato a muoversi", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} \u00e8 diventato asciutto", "not_moving": "{entity_name} ha smesso di muoversi", "not_occupied": "{entity_name} non \u00e8 occupato", + "not_opened": "{entity_name} chiuso", "not_plugged_in": "{entity_name} \u00e8 scollegato", "not_powered": "{entity_name} non \u00e8 alimentato", "not_present": "{entity_name} non \u00e8 presente", diff --git a/homeassistant/components/ecobee/.translations/fr.json b/homeassistant/components/ecobee/.translations/fr.json index 85da5b3a4ecc41..7f308fdf3a3e9e 100644 --- a/homeassistant/components/ecobee/.translations/fr.json +++ b/homeassistant/components/ecobee/.translations/fr.json @@ -9,6 +9,7 @@ }, "step": { "authorize": { + "description": "Veuillez autoriser cette application \u00e0 https://www.ecobee.com/consumerportal/index.html avec un code PIN :\n\n{pin}\n\nEnsuite, appuyez sur Soumettre.", "title": "Autoriser l'application sur ecobee.com" }, "user": { diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json index 82b9a7aee88167..f5f25da48bd1e9 100644 --- a/homeassistant/components/opentherm_gw/.translations/fr.json +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -10,6 +10,7 @@ "init": { "data": { "device": "Chemin ou URL", + "floor_temperature": "Temp\u00e9rature du sol", "id": "ID", "name": "Nom", "precision": "Pr\u00e9cision de la temp\u00e9rature climatique" diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 3854b19b5d2f76..c06d314ec721dc 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -34,6 +34,7 @@ "title": "S\u00e9lectionnez le serveur Plex" }, "start_website_auth": { + "description": "Continuer d'autoriser sur plex.tv.", "title": "Connecter un serveur Plex" }, "user": { diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index d7b0a78311626b..b4adac8e997dce 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -31,6 +31,15 @@ "button_5": "Cinqui\u00e8me bouton", "button_6": "Sixi\u00e8me bouton", "close": "Fermer", + "dim_down": "Assombrir", + "dim_up": "\u00c9claircir", + "face_1": "avec face 1 activ\u00e9e", + "face_2": "avec face 2 activ\u00e9e", + "face_3": "avec face 3 activ\u00e9e", + "face_4": "avec face 4 activ\u00e9e", + "face_5": "avec face 5 activ\u00e9e", + "face_6": "avec face 6 activ\u00e9e", + "face_any": "Avec n'importe quelle face / face sp\u00e9cifi\u00e9e(s) activ\u00e9e", "left": "Gauche", "open": "Ouvert", "right": "Droite", @@ -39,8 +48,18 @@ }, "trigger_type": { "device_dropped": "Appareil tomb\u00e9", + "device_flipped": "Appareil retourn\u00e9 \"{subtype}\"", + "device_knocked": "Appareil frapp\u00e9 \"{subtype}\"", + "device_rotated": "Appareil tourn\u00e9 \"{subtype}\"", "device_shaken": "Appareil secou\u00e9", + "device_slid": "Appareil gliss\u00e9 \"{subtype}\"", "device_tilted": "Dispositif inclin\u00e9", + "remote_button_double_press": "Bouton \"{subtype}\" cliqu\u00e9", + "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", + "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", + "remote_button_quadruple_press": "bouton \" {subtype} \" quadruple clic", + "remote_button_quintuple_press": "bouton \" {subtype} \" quintuple clic", + "remote_button_short_press": "bouton \" {subtype} \" enfonc\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton\"{sous-type}\" \u00e0 trois clics" } From 0c8e208fd850c1b13e60c407f5d999dc7dca174b Mon Sep 17 00:00:00 2001 From: quthla Date: Fri, 11 Oct 2019 09:27:07 +0200 Subject: [PATCH 0752/3953] Bump python-slugify to 3.0.6 (#27430) * Bump python-slugify to 3.0.6 * Bump python-slugify to 3.0.6 * Bump python-slugify to 3.0.6 --- 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 f74bdac2337dd6..2259f3053fe328 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 -python-slugify==3.0.4 +python-slugify==3.0.6 pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2a0987c596ba7a..5661649866212f 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.4 +python-slugify==3.0.6 pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 diff --git a/setup.py b/setup.py index 33d4ede25511f5..5b9988cff27bcf 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ # PyJWT has loose dependency. We want the latest one. "cryptography==2.7", "pip>=8.0.3", - "python-slugify==3.0.4", + "python-slugify==3.0.6", "pytz>=2019.03", "pyyaml==5.1.2", "requests==2.22.0", From 8bbf26130246a3532e5634eda4a6a38ef46a02c1 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 11 Oct 2019 16:36:46 +0200 Subject: [PATCH 0753/3953] Refactor home --> hap for Homematic IP Cloud (#27368) * Refactor home to hap for Homematic IP Cloud * Add some tests * Rename ha_entity --> ha_state * use asynctest.Mock --- .../homematicip_cloud/alarm_control_panel.py | 12 +- .../homematicip_cloud/binary_sensor.py | 58 ++-- .../components/homematicip_cloud/climate.py | 12 +- .../components/homematicip_cloud/cover.py | 8 +- .../components/homematicip_cloud/device.py | 13 +- .../components/homematicip_cloud/hap.py | 1 + .../components/homematicip_cloud/light.py | 30 +- .../components/homematicip_cloud/sensor.py | 62 ++-- .../components/homematicip_cloud/switch.py | 32 +- .../components/homematicip_cloud/weather.py | 22 +- .../components/homematicip_cloud/conftest.py | 31 +- tests/components/homematicip_cloud/helper.py | 88 +++--- .../homematicip_cloud/test_binary_sensor.py | 289 ++++++++++++++++++ .../homematicip_cloud/test_binary_sensors.py | 41 --- .../homematicip_cloud/test_light.py | 196 ++++++++++++ .../homematicip_cloud/test_lights.py | 77 ----- tests/fixtures/homematicip_cloud.json | 189 ++++++++++++ 17 files changed, 872 insertions(+), 289 deletions(-) create mode 100644 tests/components/homematicip_cloud/test_binary_sensor.py delete mode 100644 tests/components/homematicip_cloud/test_binary_sensors.py create mode 100644 tests/components/homematicip_cloud/test_light.py delete mode 100644 tests/components/homematicip_cloud/test_lights.py diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 592d234225cff9..bb5999108ce012 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -2,7 +2,6 @@ import logging from homematicip.aio.group import AsyncSecurityZoneGroup -from homematicip.aio.home import AsyncHome from homematicip.base.enums import WindowState from homeassistant.components.alarm_control_panel import AlarmControlPanel @@ -16,6 +15,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -31,15 +31,15 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] security_zones = [] - for group in home.groups: + for group in hap.home.groups: if isinstance(group, AsyncSecurityZoneGroup): security_zones.append(group) if security_zones: - devices.append(HomematicipAlarmControlPanel(home, security_zones)) + devices.append(HomematicipAlarmControlPanel(hap, security_zones)) if devices: async_add_entities(devices) @@ -48,9 +48,9 @@ async def async_setup_entry( class HomematicipAlarmControlPanel(AlarmControlPanel): """Representation of an alarm control panel.""" - def __init__(self, home: AsyncHome, security_zones) -> None: + def __init__(self, hap: HomematicipHAP, security_zones) -> None: """Initialize the alarm control panel.""" - self._home = home + self._home = hap.home self.alarm_state = STATE_ALARM_DISARMED for security_zone in security_zones: diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 1114f10b622740..964ab4d823420d 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -20,7 +20,6 @@ AsyncWeatherSensorPro, ) from homematicip.aio.group import AsyncSecurityGroup, AsyncSecurityZoneGroup -from homematicip.aio.home import AsyncHome from homematicip.base.enums import SmokeDetectorAlarmType, WindowState from homeassistant.components.binary_sensor import ( @@ -41,6 +40,7 @@ from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -85,18 +85,18 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncAccelerationSensor): - devices.append(HomematicipAccelerationSensor(home, device)) + devices.append(HomematicipAccelerationSensor(hap, device)) if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): - devices.append(HomematicipContactInterface(home, device)) + devices.append(HomematicipContactInterface(hap, device)) if isinstance( device, (AsyncShutterContact, AsyncShutterContactMagnetic, AsyncRotaryHandleSensor), ): - devices.append(HomematicipShutterContact(home, device)) + devices.append(HomematicipShutterContact(hap, device)) if isinstance( device, ( @@ -105,28 +105,28 @@ async def async_setup_entry( AsyncMotionDetectorPushButton, ), ): - devices.append(HomematicipMotionDetector(home, device)) + devices.append(HomematicipMotionDetector(hap, device)) if isinstance(device, AsyncPresenceDetectorIndoor): - devices.append(HomematicipPresenceDetector(home, device)) + devices.append(HomematicipPresenceDetector(hap, device)) if isinstance(device, AsyncSmokeDetector): - devices.append(HomematicipSmokeDetector(home, device)) + devices.append(HomematicipSmokeDetector(hap, device)) if isinstance(device, AsyncWaterSensor): - devices.append(HomematicipWaterDetector(home, device)) + devices.append(HomematicipWaterDetector(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipRainSensor(home, device)) + devices.append(HomematicipRainSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipStormSensor(home, device)) - devices.append(HomematicipSunshineSensor(home, device)) + devices.append(HomematicipStormSensor(hap, device)) + devices.append(HomematicipSunshineSensor(hap, device)) if isinstance(device, AsyncDevice) and device.lowBat is not None: - devices.append(HomematicipBatterySensor(home, device)) + devices.append(HomematicipBatterySensor(hap, device)) - for group in home.groups: + for group in hap.home.groups: if isinstance(group, AsyncSecurityGroup): - devices.append(HomematicipSecuritySensorGroup(home, group)) + devices.append(HomematicipSecuritySensorGroup(hap, group)) elif isinstance(group, AsyncSecurityZoneGroup): - devices.append(HomematicipSecurityZoneSensorGroup(home, group)) + devices.append(HomematicipSecurityZoneSensorGroup(hap, group)) if devices: async_add_entities(devices) @@ -249,9 +249,9 @@ def is_on(self) -> bool: class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud storm sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize storm sensor.""" - super().__init__(home, device, "Storm") + super().__init__(hap, device, "Storm") @property def icon(self) -> str: @@ -267,9 +267,9 @@ def is_on(self) -> bool: class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud rain sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize rain sensor.""" - super().__init__(home, device, "Raining") + super().__init__(hap, device, "Raining") @property def device_class(self) -> str: @@ -285,9 +285,9 @@ def is_on(self) -> bool: class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud sunshine sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize sunshine sensor.""" - super().__init__(home, device, "Sunshine") + super().__init__(hap, device, "Sunshine") @property def device_class(self) -> str: @@ -314,9 +314,9 @@ def device_state_attributes(self): class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud low battery sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize battery sensor.""" - super().__init__(home, device, "Battery") + super().__init__(hap, device, "Battery") @property def device_class(self) -> str: @@ -332,10 +332,10 @@ def is_on(self) -> bool: class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud security zone group.""" - def __init__(self, home: AsyncHome, device, post: str = "SecurityZone") -> None: + def __init__(self, hap: HomematicipHAP, device, post: str = "SecurityZone") -> None: """Initialize security zone group.""" device.modelType = f"HmIP-{post}" - super().__init__(home, device, post) + super().__init__(hap, device, post) @property def device_class(self) -> str: @@ -389,9 +389,9 @@ class HomematicipSecuritySensorGroup( ): """Representation of a HomematicIP security group.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize security group.""" - super().__init__(home, device, "Sensors") + super().__init__(hap, device, "Sensors") @property def device_state_attributes(self): diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 794a8b44cbcdd5..cf1c1baabe0229 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -4,7 +4,6 @@ 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 @@ -24,6 +23,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -41,11 +41,11 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP climate from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.groups: + for device in hap.home.groups: if isinstance(device, AsyncHeatingGroup): - devices.append(HomematicipHeatingGroup(home, device)) + devices.append(HomematicipHeatingGroup(hap, device)) if devices: async_add_entities(devices) @@ -54,13 +54,13 @@ async def async_setup_entry( class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """Representation of a HomematicIP heating group.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize heating group.""" device.modelType = "Group-Heating" self._simple_heating = None if device.actualTemperature is None: self._simple_heating = _get_first_heating_thermostat(device) - super().__init__(home, device) + super().__init__(hap, device) @property def temperature_unit(self) -> str: diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 9252c4322d9cb1..c5821c4f75e4ec 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -31,13 +31,13 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP cover from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncFullFlushBlind): - devices.append(HomematicipCoverSlats(home, device)) + devices.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): - devices.append(HomematicipCoverShutter(home, device)) + devices.append(HomematicipCoverShutter(hap, device)) if devices: async_add_entities(devices) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 0389e0b9935329..3d64014883d131 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -4,17 +4,17 @@ from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup -from homematicip.aio.home import AsyncHome from homeassistant.components import homematicip_cloud from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import Entity +from .hap import HomematicipHAP + _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" -ATTR_GROUP_ID = "group_id" ATTR_ID = "id" ATTR_IS_GROUP = "is_group" # RSSI HAP -> Device @@ -46,15 +46,16 @@ "id": ATTR_ID, } -GROUP_ATTRIBUTES = {"modelType": ATTR_MODEL_TYPE, "id": ATTR_GROUP_ID} +GROUP_ATTRIBUTES = {"modelType": ATTR_MODEL_TYPE} class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" - def __init__(self, home: AsyncHome, device, post: Optional[str] = None) -> None: + def __init__(self, hap: HomematicipHAP, device, post: Optional[str] = None) -> None: """Initialize the generic device.""" - self._home = home + self._hap = hap + self._home = hap.home self._device = device self.post = post # Marker showing that the HmIP device hase been removed. @@ -81,6 +82,7 @@ def device_info(self): async def async_added_to_hass(self): """Register callbacks.""" + self._hap.hmip_device_by_entity_id[self.entity_id] = self._device self._device.on_update(self._async_device_changed) self._device.on_remove(self._async_device_removed) @@ -104,6 +106,7 @@ async def async_will_remove_from_hass(self) -> None: # Only go further if the device/entity should be removed from registries # due to a removal of the HmIP device. if self.hmip_device_removed: + del self._hap.hmip_device_by_entity_id[self.entity_id] await self.async_remove_from_registries() async def async_remove_from_registries(self) -> None: diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index abba183d339b8e..22ab1fd617c408 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -79,6 +79,7 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: self._retry_task = None self._tries = 0 self._accesspoint_connected = True + self.hmip_device_by_entity_id = {} async def async_setup(self, tries: int = 0): """Initialize connection.""" diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 42ff6d30478fb4..80ee4cc5743939 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -9,7 +9,6 @@ AsyncFullFlushDimmer, AsyncPluggableDimmer, ) -from homematicip.aio.home import AsyncHome from homematicip.base.enums import RGBColorState from homematicip.base.functionalChannels import NotificationLightChannel @@ -25,6 +24,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -41,26 +41,26 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): - devices.append(HomematicipLightMeasuring(home, device)) + devices.append(HomematicipLightMeasuring(hap, device)) elif isinstance(device, AsyncBrandSwitchNotificationLight): - devices.append(HomematicipLight(home, device)) + devices.append(HomematicipLight(hap, device)) devices.append( - HomematicipNotificationLight(home, device, device.topLightChannelIndex) + HomematicipNotificationLight(hap, device, device.topLightChannelIndex) ) devices.append( HomematicipNotificationLight( - home, device, device.bottomLightChannelIndex + hap, device, device.bottomLightChannelIndex ) ) elif isinstance( device, (AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer), ): - devices.append(HomematicipDimmer(home, device)) + devices.append(HomematicipDimmer(hap, device)) if devices: async_add_entities(devices) @@ -69,9 +69,9 @@ async def async_setup_entry( class HomematicipLight(HomematicipGenericDevice, Light): """Representation of a HomematicIP Cloud light device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the light device.""" - super().__init__(home, device) + super().__init__(hap, device) @property def is_on(self) -> bool: @@ -107,9 +107,9 @@ def device_state_attributes(self): class HomematicipDimmer(HomematicipGenericDevice, Light): """Representation of HomematicIP Cloud dimmer light device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the dimmer light device.""" - super().__init__(home, device) + super().__init__(hap, device) @property def is_on(self) -> bool: @@ -143,13 +143,13 @@ async def async_turn_off(self, **kwargs): class HomematicipNotificationLight(HomematicipGenericDevice, Light): """Representation of HomematicIP Cloud dimmer light device.""" - def __init__(self, home: AsyncHome, device, channel: int) -> None: + def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: """Initialize the dimmer light device.""" self.channel = channel if self.channel == 2: - super().__init__(home, device, "Top") + super().__init__(hap, device, "Top") else: - super().__init__(home, device, "Bottom") + super().__init__(hap, device, "Bottom") self._color_switcher = { RGBColorState.WHITE: [0.0, 0.0], diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index ceb7fc39fd7e3f..18d483f6adf365 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -20,7 +20,6 @@ AsyncWeatherSensorPlus, AsyncWeatherSensorPro, ) -from homematicip.aio.home import AsyncHome from homematicip.base.enums import ValveState from homeassistant.config_entries import ConfigEntry @@ -36,6 +35,7 @@ from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -55,12 +55,12 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home - devices = [HomematicipAccesspointStatus(home)] - for device in home.devices: + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] + devices = [HomematicipAccesspointStatus(hap)] + for device in hap.home.devices: if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): - devices.append(HomematicipHeatingThermostat(home, device)) - devices.append(HomematicipTemperatureSensor(home, device)) + devices.append(HomematicipHeatingThermostat(hap, device)) + devices.append(HomematicipTemperatureSensor(hap, device)) if isinstance( device, ( @@ -72,8 +72,8 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipTemperatureSensor(home, device)) - devices.append(HomematicipHumiditySensor(home, device)) + devices.append(HomematicipTemperatureSensor(hap, device)) + devices.append(HomematicipHumiditySensor(hap, device)) if isinstance( device, ( @@ -87,7 +87,7 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipIlluminanceSensor(home, device)) + devices.append(HomematicipIlluminanceSensor(hap, device)) if isinstance( device, ( @@ -96,15 +96,15 @@ async def async_setup_entry( AsyncFullFlushSwitchMeasuring, ), ): - devices.append(HomematicipPowerSensor(home, device)) + devices.append(HomematicipPowerSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipWindspeedSensor(home, device)) + devices.append(HomematicipWindspeedSensor(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipTodayRainSensor(home, device)) + devices.append(HomematicipTodayRainSensor(hap, device)) if isinstance(device, AsyncPassageDetector): - devices.append(HomematicipPassageDetectorDeltaCounter(home, device)) + devices.append(HomematicipPassageDetectorDeltaCounter(hap, device)) if devices: async_add_entities(devices) @@ -113,9 +113,9 @@ async def async_setup_entry( class HomematicipAccesspointStatus(HomematicipGenericDevice): """Representation of an HomeMaticIP Cloud access point.""" - def __init__(self, home: AsyncHome) -> None: + def __init__(self, hap: HomematicipHAP) -> None: """Initialize access point device.""" - super().__init__(home, home) + super().__init__(hap, hap.home) @property def device_info(self): @@ -162,9 +162,9 @@ def device_state_attributes(self): class HomematicipHeatingThermostat(HomematicipGenericDevice): """Representation of a HomematicIP heating thermostat device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize heating thermostat device.""" - super().__init__(home, device, "Heating") + super().__init__(hap, device, "Heating") @property def icon(self) -> str: @@ -191,9 +191,9 @@ def unit_of_measurement(self) -> str: class HomematicipHumiditySensor(HomematicipGenericDevice): """Representation of a HomematicIP Cloud humidity device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" - super().__init__(home, device, "Humidity") + super().__init__(hap, device, "Humidity") @property def device_class(self) -> str: @@ -214,9 +214,9 @@ def unit_of_measurement(self) -> str: class HomematicipTemperatureSensor(HomematicipGenericDevice): """Representation of a HomematicIP Cloud thermometer device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" - super().__init__(home, device, "Temperature") + super().__init__(hap, device, "Temperature") @property def device_class(self) -> str: @@ -251,9 +251,9 @@ def device_state_attributes(self): class HomematicipIlluminanceSensor(HomematicipGenericDevice): """Representation of a HomematicIP Illuminance device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Illuminance") + super().__init__(hap, device, "Illuminance") @property def device_class(self) -> str: @@ -277,9 +277,9 @@ def unit_of_measurement(self) -> str: class HomematicipPowerSensor(HomematicipGenericDevice): """Representation of a HomematicIP power measuring device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Power") + super().__init__(hap, device, "Power") @property def device_class(self) -> str: @@ -300,9 +300,9 @@ def unit_of_measurement(self) -> str: class HomematicipWindspeedSensor(HomematicipGenericDevice): """Representation of a HomematicIP wind speed sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Windspeed") + super().__init__(hap, device, "Windspeed") @property def state(self) -> float: @@ -333,9 +333,9 @@ def device_state_attributes(self): class HomematicipTodayRainSensor(HomematicipGenericDevice): """Representation of a HomematicIP rain counter of a day sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Today Rain") + super().__init__(hap, device, "Today Rain") @property def state(self) -> float: @@ -351,10 +351,6 @@ def unit_of_measurement(self) -> str: class HomematicipPassageDetectorDeltaCounter(HomematicipGenericDevice): """Representation of a HomematicIP passage detector delta counter.""" - def __init__(self, home: AsyncHome, device) -> None: - """Initialize the device.""" - super().__init__(home, device) - @property def state(self) -> int: """Representation of the HomematicIP passage detector delta counter value.""" diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 7994aa446b899d..3b54d3fc2790da 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -12,7 +12,6 @@ AsyncPrintedCircuitBoardSwitchBattery, ) from homematicip.aio.group import AsyncSwitchingGroup -from homematicip.aio.home import AsyncHome from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry @@ -20,6 +19,7 @@ from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -33,9 +33,9 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP switch from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): # BrandSwitchMeasuring inherits PlugableSwitchMeasuring # This device is implemented in the light platform and will @@ -44,24 +44,24 @@ async def async_setup_entry( elif isinstance( device, (AsyncPlugableSwitchMeasuring, AsyncFullFlushSwitchMeasuring) ): - devices.append(HomematicipSwitchMeasuring(home, device)) + devices.append(HomematicipSwitchMeasuring(hap, device)) elif isinstance( device, (AsyncPlugableSwitch, AsyncPrintedCircuitBoardSwitchBattery) ): - devices.append(HomematicipSwitch(home, device)) + devices.append(HomematicipSwitch(hap, device)) elif isinstance(device, AsyncOpenCollector8Module): for channel in range(1, 9): - devices.append(HomematicipMultiSwitch(home, device, channel)) + devices.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncMultiIOBox): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(home, device, channel)) + devices.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncPrintedCircuitBoardSwitch2): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(home, device, channel)) + devices.append(HomematicipMultiSwitch(hap, device, channel)) - for group in home.groups: + for group in hap.home.groups: if isinstance(group, AsyncSwitchingGroup): - devices.append(HomematicipGroupSwitch(home, group)) + devices.append(HomematicipGroupSwitch(hap, group)) if devices: async_add_entities(devices) @@ -70,9 +70,9 @@ async def async_setup_entry( class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): """representation of a HomematicIP Cloud switch device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the switch device.""" - super().__init__(home, device) + super().__init__(hap, device) @property def is_on(self) -> bool: @@ -91,10 +91,10 @@ async def async_turn_off(self, **kwargs): class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): """representation of a HomematicIP switching group.""" - def __init__(self, home: AsyncHome, device, post: str = "Group") -> None: + def __init__(self, hap: HomematicipHAP, device, post: str = "Group") -> None: """Initialize switching group.""" device.modelType = f"HmIP-{post}" - super().__init__(home, device, post) + super().__init__(hap, device, post) @property def is_on(self) -> bool: @@ -148,10 +148,10 @@ def today_energy_kwh(self) -> int: class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): """Representation of a HomematicIP Cloud multi switch device.""" - def __init__(self, home: AsyncHome, device, channel: int): + def __init__(self, hap: HomematicipHAP, device, channel: int): """Initialize the multi switch device.""" self.channel = channel - super().__init__(home, device, f"Channel{channel}") + super().__init__(hap, device, f"Channel{channel}") @property def unique_id(self) -> str: diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 0d020312fe914a..6b92b639c7afc2 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -6,7 +6,6 @@ AsyncWeatherSensorPlus, AsyncWeatherSensorPro, ) -from homematicip.aio.home import AsyncHome from homematicip.base.enums import WeatherCondition from homeassistant.components.weather import WeatherEntity @@ -15,6 +14,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -46,15 +46,15 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncWeatherSensorPro): - devices.append(HomematicipWeatherSensorPro(home, device)) + devices.append(HomematicipWeatherSensorPro(hap, device)) elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)): - devices.append(HomematicipWeatherSensor(home, device)) + devices.append(HomematicipWeatherSensor(hap, device)) - devices.append(HomematicipHomeWeather(home)) + devices.append(HomematicipHomeWeather(hap)) if devices: async_add_entities(devices) @@ -63,9 +63,9 @@ async def async_setup_entry( class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): """representation of a HomematicIP Cloud weather sensor plus & basic.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the weather sensor.""" - super().__init__(home, device) + super().__init__(hap, device) @property def name(self) -> str: @@ -121,10 +121,10 @@ def wind_bearing(self) -> float: class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity): """representation of a HomematicIP Cloud home weather.""" - def __init__(self, home: AsyncHome) -> None: + def __init__(self, hap: HomematicipHAP) -> None: """Initialize the home weather.""" - home.modelType = "HmIP-Home-Weather" - super().__init__(home, home) + hap.home.modelType = "HmIP-Home-Weather" + super().__init__(hap, hap.home) @property def available(self) -> bool: diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index c301c73b4d0cac..2c2b020f3a0ef6 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -6,10 +6,14 @@ from homeassistant import config_entries from homeassistant.components.homematicip_cloud import ( + CONF_ACCESSPOINT, + CONF_AUTHTOKEN, DOMAIN as HMIPC_DOMAIN, + async_setup as hmip_async_setup, const as hmipc, hap as hmip_hap, ) +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from .helper import AUTH_TOKEN, HAPID, HomeTemplate @@ -19,7 +23,7 @@ @pytest.fixture(name="mock_connection") def mock_connection_fixture(): - """Return a mockked connection.""" + """Return a mocked connection.""" connection = MagicMock(spec=AsyncConnection) def _rest_call_side_effect(path, body=None): @@ -39,7 +43,7 @@ def default_mock_home_fixture(mock_connection): @pytest.fixture(name="hmip_config_entry") def hmip_config_entry_fixture(): - """Create a fake config entriy for homematic ip cloud.""" + """Create a mock config entriy for homematic ip cloud.""" entry_data = { hmipc.HMIPC_HAPID: HAPID, hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, @@ -67,9 +71,32 @@ async def default_mock_hap_fixture( hap = hmip_hap.HomematicipHAP(hass, hmip_config_entry) with patch.object(hap, "get_hap", return_value=mock_coro(default_mock_home)): assert await hap.async_setup() is True + default_mock_home.on_update(hap.async_update) + default_mock_home.on_create(hap.async_create_entity) hass.data[HMIPC_DOMAIN] = {HAPID: hap} await hass.async_block_till_done() return hap + + +@pytest.fixture(name="hmip_config") +def hmip_config_fixture(): + """Create a config for homematic ip cloud.""" + + entry_data = {CONF_ACCESSPOINT: HAPID, CONF_AUTHTOKEN: AUTH_TOKEN, CONF_NAME: ""} + + return {hmipc.DOMAIN: [entry_data]} + + +@pytest.fixture(name="mock_hap_with_service") +async def mock_hap_with_service_fixture( + hass: HomeAssistant, default_mock_hap, hmip_config +): + """Create a fake homematic access point with hass services.""" + + await hmip_async_setup(hass, hmip_config) + await hass.async_block_till_done() + hass.data[HMIPC_DOMAIN] = {HAPID: default_mock_hap} + return default_mock_hap diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 79a5bc0b201d36..b5e41a6ae86407 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -1,18 +1,25 @@ """Helper for HomematicIP Cloud Tests.""" import json -from unittest.mock import Mock +from asynctest import Mock from homematicip.aio.class_maps import ( TYPE_CLASS_MAP, TYPE_GROUP_MAP, TYPE_SECURITY_EVENT_MAP, ) +from homematicip.aio.device import AsyncDevice +from homematicip.aio.group import AsyncGroup from homematicip.aio.home import AsyncHome from homematicip.home import Home +from homeassistant.components.homematicip_cloud.device import ( + ATTR_IS_GROUP, + ATTR_MODEL_TYPE, +) + from tests.common import load_fixture -HAPID = "Mock_HAP" +HAPID = "3014F7110000000000000001" AUTH_TOKEN = "1234" HOME_JSON = "homematicip_cloud.json" @@ -21,28 +28,38 @@ def get_and_check_entity_basics( hass, default_mock_hap, entity_id, entity_name, device_model ): """Get and test basic device.""" - ha_entity = hass.states.get(entity_id) - assert ha_entity is not None - assert ha_entity.attributes["model_type"] == device_model - assert ha_entity.name == entity_name + ha_state = hass.states.get(entity_id) + assert ha_state is not None + if device_model: + assert ha_state.attributes[ATTR_MODEL_TYPE] == device_model + assert ha_state.name == entity_name - hmip_device = default_mock_hap.home.template.search_mock_device_by_id( - ha_entity.attributes["id"] - ) - assert hmip_device is not None - return ha_entity, hmip_device + hmip_device = default_mock_hap.hmip_device_by_entity_id.get(entity_id) + if hmip_device: + if isinstance(hmip_device, AsyncDevice): + assert ha_state.attributes[ATTR_IS_GROUP] is False + elif isinstance(hmip_device, AsyncGroup): + assert ha_state.attributes[ATTR_IS_GROUP] is True + return ha_state, hmip_device async def async_manipulate_test_data( - hass, hmip_device, attribute, new_value, channel=1 + hass, hmip_device, attribute, new_value, channel=1, fire_device=None ): """Set new value on hmip device.""" if channel == 1: setattr(hmip_device, attribute, new_value) - functional_channel = hmip_device.functionalChannels[channel] - setattr(functional_channel, attribute, new_value) + if hasattr(hmip_device, "functionalChannels"): + functional_channel = hmip_device.functionalChannels[channel] + setattr(functional_channel, attribute, new_value) + + fire_target = hmip_device if fire_device is None else fire_device + + if isinstance(fire_target, AsyncHome): + fire_target.fire_update_event(fire_target._rawJSONData) # pylint: disable=W0212 + else: + fire_target.fire_update_event() - hmip_device.fire_update_event() await hass.async_block_till_done() @@ -66,8 +83,8 @@ class HomeTemplate(Home): def __init__(self, connection=None): """Init template with connection.""" super().__init__(connection=connection) - self.mock_devices = [] - self.mock_groups = [] + self.label = "Access Point" + self.model_type = "HmIP-HAP" def init_home(self, json_path=HOME_JSON): """Init template with json.""" @@ -78,24 +95,15 @@ def init_home(self, json_path=HOME_JSON): def _generate_mocks(self): """Generate mocks for groups and devices.""" + mock_devices = [] for device in self.devices: - self.mock_devices.append(_get_mock(device)) + mock_devices.append(_get_mock(device)) + self.devices = mock_devices + + mock_groups = [] for group in self.groups: - self.mock_groups.append(_get_mock(group)) - - def search_mock_device_by_id(self, device_id): - """Search a device by given id.""" - for device in self.mock_devices: - if device.id == device_id: - return device - return None - - def search_mock_group_by_id(self, group_id): - """Search a group by given id.""" - for group in self.mock_groups: - if group.id == group_id: - return group - return None + mock_groups.append(_get_mock(group)) + self.groups = mock_groups def get_async_home_mock(self): """ @@ -105,19 +113,11 @@ def get_async_home_mock(self): and sets reuired attributes. """ mock_home = Mock( - check_connection=self._connection, - id=HAPID, - connected=True, - dutyCycle=self.dutyCycle, - devices=self.mock_devices, - groups=self.mock_groups, - weather=self.weather, - location=self.location, - label="home label", - template=self, - spec=AsyncHome, + spec=AsyncHome, wraps=self, label="Access Point", modelType="HmIP-HAP" ) + mock_home.__dict__.update(self.__dict__) mock_home.name = "" + return mock_home diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py new file mode 100644 index 00000000000000..0de2101d287e73 --- /dev/null +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -0,0 +1,289 @@ +"""Tests for HomematicIP Cloud binary sensor.""" +from homematicip.base.enums import SmokeDetectorAlarmType, WindowState + +from homeassistant.components.homematicip_cloud.binary_sensor import ( + ATTR_ACCELERATION_SENSOR_MODE, + ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION, + ATTR_ACCELERATION_SENSOR_SENSITIVITY, + ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE, + ATTR_LOW_BATTERY, + ATTR_MOTION_DETECTED, +) +from homeassistant.const import STATE_OFF, STATE_ON + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_acceleration_sensor(hass, default_mock_hap): + """Test HomematicipAccelerationSensor.""" + entity_id = "binary_sensor.garagentor" + entity_name = "Garagentor" + device_model = "HmIP-SAM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_MODE] == "FLAT_DECT" + assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION] == "VERTICAL" + assert ( + ha_state.attributes[ATTR_ACCELERATION_SENSOR_SENSITIVITY] == "SENSOR_RANGE_4G" + ) + assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE] == 45 + service_call_counter = len(hmip_device.mock_calls) + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", False + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert len(hmip_device.mock_calls) == service_call_counter + 1 + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", True + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert len(hmip_device.mock_calls) == service_call_counter + 2 + + +async def test_hmip_contact_interface(hass, default_mock_hap): + """Test HomematicipContactInterface.""" + entity_id = "binary_sensor.kontakt_schnittstelle_unterputz_1_fach" + entity_name = "Kontakt-Schnittstelle Unterputz – 1-fach" + device_model = "HmIP-FCI1" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "windowState", WindowState.OPEN) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "windowState", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_shutter_contact(hass, default_mock_hap): + """Test HomematicipShutterContact.""" + entity_id = "binary_sensor.fenstergriffsensor" + entity_name = "Fenstergriffsensor" + device_model = "HmIP-SRH" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + await async_manipulate_test_data( + hass, hmip_device, "windowState", WindowState.CLOSED + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await async_manipulate_test_data(hass, hmip_device, "windowState", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_motion_detector(hass, default_mock_hap): + """Test HomematicipMotionDetector.""" + entity_id = "binary_sensor.bewegungsmelder_fur_55er_rahmen_innen" + entity_name = "Bewegungsmelder für 55er Rahmen – innen" + device_model = "HmIP-SMI55" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "motionDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_presence_detector(hass, default_mock_hap): + """Test HomematicipPresenceDetector.""" + entity_id = "binary_sensor.spi_1" + entity_name = "SPI_1" + device_model = "HmIP-SPI" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "presenceDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_smoke_detector(hass, default_mock_hap): + """Test HomematicipSmokeDetector.""" + entity_id = "binary_sensor.rauchwarnmelder" + entity_name = "Rauchwarnmelder" + device_model = "HmIP-SWSD" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data( + hass, + hmip_device, + "smokeDetectorAlarmType", + SmokeDetectorAlarmType.PRIMARY_ALARM, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_water_detector(hass, default_mock_hap): + """Test HomematicipWaterDetector.""" + entity_id = "binary_sensor.wassersensor" + entity_name = "Wassersensor" + device_model = "HmIP-SWD" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", True) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", True) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", False) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", False) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_storm_sensor(hass, default_mock_hap): + """Test HomematicipStormSensor.""" + entity_id = "binary_sensor.weather_sensor_plus_storm" + entity_name = "Weather Sensor – plus Storm" + device_model = "HmIP-SWO-PL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "storm", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_rain_sensor(hass, default_mock_hap): + """Test HomematicipRainSensor.""" + entity_id = "binary_sensor.wettersensor_pro_raining" + entity_name = "Wettersensor - pro Raining" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "raining", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_sunshine_sensor(hass, default_mock_hap): + """Test HomematicipSunshineSensor.""" + entity_id = "binary_sensor.wettersensor_pro_sunshine" + entity_name = "Wettersensor - pro Sunshine" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert ha_state.attributes["today_sunshine_duration_in_minutes"] == 100 + await async_manipulate_test_data(hass, hmip_device, "sunshine", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_battery_sensor(hass, default_mock_hap): + """Test HomematicipSunshineSensor.""" + entity_id = "binary_sensor.wohnungsture_battery" + entity_name = "Wohnungstüre Battery" + device_model = "HMIP-SWDO" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "lowBat", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_security_zone_sensor_group(hass, default_mock_hap): + """Test HomematicipSecurityZoneSensorGroup.""" + entity_id = "binary_sensor.internal_securityzone" + entity_name = "INTERNAL SecurityZone" + device_model = "HmIP-SecurityZone" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "motionDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_MOTION_DETECTED] is True + + +async def test_hmip_security_sensor_group(hass, default_mock_hap): + """Test HomematicipSecuritySensorGroup.""" + entity_id = "binary_sensor.buro_sensors" + entity_name = "Büro Sensors" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get("low_bat") + await async_manipulate_test_data(hass, hmip_device, "lowBat", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_LOW_BATTERY] is True + + await async_manipulate_test_data(hass, hmip_device, "lowBat", False) + await async_manipulate_test_data( + hass, + hmip_device, + "smokeDetectorAlarmType", + SmokeDetectorAlarmType.PRIMARY_ALARM, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ( + ha_state.attributes["smoke_detector_alarm"] + == SmokeDetectorAlarmType.PRIMARY_ALARM + ) diff --git a/tests/components/homematicip_cloud/test_binary_sensors.py b/tests/components/homematicip_cloud/test_binary_sensors.py deleted file mode 100644 index 4471c5dd7f3d8d..00000000000000 --- a/tests/components/homematicip_cloud/test_binary_sensors.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Tests for HomematicIP Cloud lights.""" -import logging - -from tests.components.homematicip_cloud.helper import ( - async_manipulate_test_data, - get_and_check_entity_basics, -) - -_LOGGER = logging.getLogger(__name__) - - -async def test_hmip_sam(hass, default_mock_hap): - """Test HomematicipLight.""" - entity_id = "binary_sensor.garagentor" - entity_name = "Garagentor" - device_model = "HmIP-SAM" - - ha_entity, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model - ) - - assert ha_entity.state == "on" - assert ha_entity.attributes["acceleration_sensor_mode"] == "FLAT_DECT" - assert ha_entity.attributes["acceleration_sensor_neutral_position"] == "VERTICAL" - assert ha_entity.attributes["acceleration_sensor_sensitivity"] == "SENSOR_RANGE_4G" - assert ha_entity.attributes["acceleration_sensor_trigger_angle"] == 45 - service_call_counter = len(hmip_device.mock_calls) - - await async_manipulate_test_data( - hass, hmip_device, "accelerationSensorTriggered", False - ) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "off" - assert len(hmip_device.mock_calls) == service_call_counter + 1 - - await async_manipulate_test_data( - hass, hmip_device, "accelerationSensorTriggered", True - ) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "on" - assert len(hmip_device.mock_calls) == service_call_counter + 2 diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py new file mode 100644 index 00000000000000..a8d4984520c83b --- /dev/null +++ b/tests/components/homematicip_cloud/test_light.py @@ -0,0 +1,196 @@ +"""Tests for HomematicIP Cloud light.""" +from homematicip.base.enums import RGBColorState + +from homeassistant.components.homematicip_cloud.light import ( + ATTR_ENERGY_COUNTER, + ATTR_POWER_CONSUMPTION, +) +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_NAME +from homeassistant.const import STATE_OFF, STATE_ON + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_light(hass, default_mock_hap): + """Test HomematicipLight.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + + service_call_counter = len(hmip_device.mock_calls) + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_notification_light(hass, default_mock_hap): + """Test HomematicipNotificationLight.""" + entity_id = "light.treppe_top_notification" + entity_name = "Treppe Top Notification" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + # Send all color via service call. + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.RED, 1.0) + + color_list = { + RGBColorState.WHITE: [0.0, 0.0], + RGBColorState.RED: [0.0, 100.0], + RGBColorState.YELLOW: [60.0, 100.0], + RGBColorState.GREEN: [120.0, 100.0], + RGBColorState.TURQUOISE: [180.0, 100.0], + RGBColorState.BLUE: [240.0, 100.0], + RGBColorState.PURPLE: [300.0, 100.0], + } + + for color, hs_color in color_list.items(): + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "hs_color": hs_color}, + blocking=True, + ) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == (2, color, 0.0392156862745098) + + assert len(hmip_device.mock_calls) == service_call_counter + 8 + + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == ( + 2, + RGBColorState.PURPLE, + 0.0392156862745098, + ) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1, 2) + await async_manipulate_test_data( + hass, hmip_device, "simpleRGBColorState", RGBColorState.PURPLE, 2 + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_COLOR_NAME] == RGBColorState.PURPLE + assert ha_state.attributes[ATTR_BRIGHTNESS] == 255 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 11 + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.PURPLE, 0.0) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, 2) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_dimmer(hass, default_mock_hap): + """Test HomematicipDimmer.""" + entity_id = "light.schlafzimmerlicht" + entity_name = "Schlafzimmerlicht" + device_model = "HmIP-BDT" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (1,) + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "brightness_pct": "100"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 2 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (1.0,) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_BRIGHTNESS] == 255 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (0,) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_light_measuring(hass, default_mock_hap): + """Test HomematicipLightMeasuring.""" + entity_id = "light.flur_oben" + entity_name = "Flur oben" + device_model = "HmIP-BSM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_POWER_CONSUMPTION] == 50 + assert ha_state.attributes[ATTR_ENERGY_COUNTER] == 6.33 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_lights.py b/tests/components/homematicip_cloud/test_lights.py deleted file mode 100644 index dcf5f76d0a0f4c..00000000000000 --- a/tests/components/homematicip_cloud/test_lights.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Tests for HomematicIP Cloud lights.""" -import logging - -from tests.components.homematicip_cloud.helper import ( - async_manipulate_test_data, - get_and_check_entity_basics, -) - -_LOGGER = logging.getLogger(__name__) - - -async def test_hmip_light(hass, default_mock_hap): - """Test HomematicipLight.""" - entity_id = "light.treppe" - entity_name = "Treppe" - device_model = "HmIP-BSL" - - ha_entity, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model - ) - - assert ha_entity.state == "on" - - service_call_counter = len(hmip_device.mock_calls) - await hass.services.async_call( - "light", "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert len(hmip_device.mock_calls) == service_call_counter + 1 - assert hmip_device.mock_calls[-1][0] == "turn_off" - await async_manipulate_test_data(hass, hmip_device, "on", False) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "off" - - await hass.services.async_call( - "light", "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert len(hmip_device.mock_calls) == service_call_counter + 3 - assert hmip_device.mock_calls[-1][0] == "turn_on" - await async_manipulate_test_data(hass, hmip_device, "on", True) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "on" - - -# HomematicipLightMeasuring -# HomematicipDimmer - - -async def test_hmip_notification_light(hass, default_mock_hap): - """Test HomematicipNotificationLight.""" - entity_id = "light.treppe_top_notification" - entity_name = "Treppe Top Notification" - device_model = "HmIP-BSL" - - ha_entity, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model - ) - - assert ha_entity.state == "off" - service_call_counter = len(hmip_device.mock_calls) - - await hass.services.async_call( - "light", "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert len(hmip_device.mock_calls) == service_call_counter + 1 - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - await async_manipulate_test_data(hass, hmip_device, "dimLevel", 100, 2) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "on" - - await hass.services.async_call( - "light", "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert len(hmip_device.mock_calls) == service_call_counter + 3 - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, 2) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "off" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index b96bff8fac9fb9..1d3d5bfd8f45da 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -2077,6 +2077,144 @@ "type": "SHUTTER_CONTACT", "updateState": "UP_TO_DATE" }, + "3014F7110000000000000108": { + "availableFirmwareVersion": "1.12.6", + "firmwareVersion": "1.12.6", + "firmwareVersionInteger": 68614, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000108", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000009" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -68, + "rssiPeerValue": -63, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "currentPowerConsumption": 0.0, + "deviceId": "3014F7110000000000000108", + "energyCounter": 6.333200000000001, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000023" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000108", + "label": "Flur oben", + "lastStatusUpdate": 1570365990392, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 288, + "modelType": "HmIP-BSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000108", + "type": "BRAND_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000109": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000109", + "deviceOverheated": null, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000029" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -80, + "rssiPeerValue": -73, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "currentPowerConsumption": 0.0, + "deviceId": "3014F7110000000000000109", + "energyCounter": 0.0011, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000030" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000011", + "label": "Ausschalter Terrasse Bewegungsmelder", + "lastStatusUpdate": 1570366291250, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 289, + "modelType": "HmIP-FSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000109", + "type": "FULL_FLUSH_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, "3014F7110000000000000008": { "availableFirmwareVersion": "0.0.0", "firmwareVersion": "2.6.2", @@ -2236,6 +2374,57 @@ "type": "PLUGABLE_SWITCH_MEASURING", "updateState": "UP_TO_DATE" }, + "3014F7110000000000000110": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000110", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -47, + "rssiPeerValue": -49, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000110", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000110", + "label": "Schrank", + "lastStatusUpdate": 1524513613922, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PS", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000110", + "type": "PLUGABLE_SWITCH", + "updateState": "UP_TO_DATE" + }, "3014F7110000000000000011": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "2.0.2", From 618cf5fa0433dfda65494101f8a7c00be4989340 Mon Sep 17 00:00:00 2001 From: Paolo Tuninetto Date: Fri, 11 Oct 2019 17:52:38 +0200 Subject: [PATCH 0754/3953] Move Arduino imports (#27438) --- homeassistant/components/arduino/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py index 4dcde93e749dd4..f973ec136e320f 100644 --- a/homeassistant/components/arduino/__init__.py +++ b/homeassistant/components/arduino/__init__.py @@ -1,8 +1,11 @@ """Support for Arduino boards running with the Firmata firmware.""" import logging +import serial import voluptuous as vol +from PyMata.pymata import PyMata + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_PORT import homeassistant.helpers.config_validation as cv @@ -20,7 +23,6 @@ def setup(hass, config): """Set up the Arduino component.""" - import serial port = config[DOMAIN][CONF_PORT] @@ -59,7 +61,6 @@ class ArduinoBoard: def __init__(self, port): """Initialize the board.""" - from PyMata.pymata import PyMata self._port = port self._board = PyMata(self._port, verbose=False) From cb30065a4027fa462b83019a5cef5e90085676b3 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 11 Oct 2019 18:29:27 +0200 Subject: [PATCH 0755/3953] Update upstream (#27440) --- homeassistant/components/rmvtransport/manifest.json | 2 +- homeassistant/components/rmvtransport/sensor.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 1f06daf0623f19..ed33caa1264aa4 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -3,7 +3,7 @@ "name": "Rmvtransport", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", "requirements": [ - "PyRMVtransport==0.1.3" + "PyRMVtransport==0.2.9" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index f66f22dda17318..9acafbbf81f9f9 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -230,8 +230,8 @@ async def async_update(self): _data = await self.rmv.get_departures( self._station_id, products=self._products, - directionId=self._direction, - maxJourneys=50, + direction_id=self._direction, + max_journeys=50, ) except RMVtransportApiConnectionError: self.departures = [] diff --git a/requirements_all.txt b/requirements_all.txt index 5661649866212f..bff608964a4482 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -66,7 +66,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.1.3 +PyRMVtransport==0.2.9 # homeassistant.components.switchbot # PySwitchbot==0.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84cef21a1fe5ca..0f37ef15992ec9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -34,7 +34,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.1.3 +PyRMVtransport==0.2.9 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 8bd847ed3969e75a1c03dcf652df28ab65106d1e Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 11 Oct 2019 18:30:27 +0200 Subject: [PATCH 0756/3953] Move imports in waterfurnace component (#27449) --- homeassistant/components/waterfurnace/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/waterfurnace/__init__.py b/homeassistant/components/waterfurnace/__init__.py index acc1c22c7345d3..b6eb22c89ae248 100644 --- a/homeassistant/components/waterfurnace/__init__.py +++ b/homeassistant/components/waterfurnace/__init__.py @@ -5,6 +5,7 @@ import threading import voluptuous as vol +from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -37,19 +38,18 @@ def setup(hass, base_config): """Set up waterfurnace platform.""" - import waterfurnace.waterfurnace as wf config = base_config.get(DOMAIN) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - wfconn = wf.WaterFurnace(username, password) + wfconn = WaterFurnace(username, password) # NOTE(sdague): login will throw an exception if this doesn't # work, which will abort the setup. try: wfconn.login() - except wf.WFCredentialError: + except WFCredentialError: _LOGGER.error("Invalid credentials for waterfurnace login.") return False @@ -83,7 +83,6 @@ def __init__(self, hass, client): def _reconnect(self): """Reconnect on a failure.""" - import waterfurnace.waterfurnace as wf self._fails += 1 if self._fails > MAX_FAILS: @@ -105,7 +104,7 @@ def _reconnect(self): try: self.client.login() self.data = self.client.read() - except wf.WFException: + except WFException: _LOGGER.exception("Failed to reconnect attempt %s", self._fails) else: _LOGGER.debug("Reconnected to furnace") @@ -113,7 +112,6 @@ def _reconnect(self): def run(self): """Thread run loop.""" - import waterfurnace.waterfurnace as wf @callback def register(): @@ -143,7 +141,7 @@ def shutdown(event): try: self.data = self.client.read() - except wf.WFException: + except WFException: # WFExceptions are things the WF library understands # that pretty much can all be solved by logging in and # back out again. From 78a08d04259b904fd4caf9cd71fd4ace1091cee1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 12 Oct 2019 00:31:47 +0000 Subject: [PATCH 0757/3953] [ci skip] Translation update --- .../binary_sensor/.translations/de.json | 7 ++ .../binary_sensor/.translations/ru.json | 79 ++++++++++++++++--- .../components/deconz/.translations/ru.json | 21 ++--- .../components/light/.translations/ru.json | 8 +- .../components/neato/.translations/da.json | 3 + .../components/plex/.translations/ca.json | 1 + .../components/plex/.translations/da.json | 1 + .../components/plex/.translations/de.json | 8 ++ .../components/sensor/.translations/ru.json | 15 ++++ .../components/switch/.translations/ru.json | 12 +-- .../components/zha/.translations/de.json | 4 +- .../components/zha/.translations/ru.json | 37 ++++++++- 12 files changed, 165 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/de.json create mode 100644 homeassistant/components/sensor/.translations/ru.json diff --git a/homeassistant/components/binary_sensor/.translations/de.json b/homeassistant/components/binary_sensor/.translations/de.json new file mode 100644 index 00000000000000..25e8ea2f86bde1 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/de.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "plugged_in": "{entity_name} eingesteckt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json index 7d73cb8d4aa489..012a5c4fa45d30 100644 --- a/homeassistant/components/binary_sensor/.translations/ru.json +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -1,15 +1,76 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name}: \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", - "is_cold": "{entity_name}: \u0445\u043e\u043b\u043e\u0434\u043d\u043e", - "is_connected": "{entity_name}: \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "is_gas": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0433\u0430\u0437", - "is_hot": "{entity_name}: \u0433\u043e\u0440\u044f\u0447\u043e", - "is_light": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0441\u0432\u0435\u0442", - "is_locked": "{entity_name}: \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e", - "is_moist": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0432\u043b\u0430\u0433\u0430", - "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435" + "is_gas": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437", + "is_light": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442", + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_moist": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443", + "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_moving": "{entity_name} \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "is_no_gas": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437", + "is_no_light": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442", + "is_no_motion": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", + "is_no_sound": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_no_vibration": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", + "is_not_locked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_not_moist": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443", + "is_not_moving": "{entity_name} \u043d\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", + "is_sound": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_vibration": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" + }, + "trigger_type": { + "bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "cold": "{entity_name} \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0435\u0442\u0441\u044f", + "connected": "{entity_name} \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "gas": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", + "hot": "{entity_name} \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0435\u0442\u0441\u044f", + "light": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442", + "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", + "moist": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443", + "moist\u00a7": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443", + "motion": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "moving": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435", + "no_gas": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", + "no_light": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442", + "no_motion": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "no_problem": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "no_smoke": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", + "no_sound": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a", + "no_vibration": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", + "not_bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439 \u0437\u0430\u0440\u044f\u0434", + "not_cold": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0442\u044c\u0441\u044f", + "not_connected": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "not_hot": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0442\u044c\u0441\u044f", + "not_locked": "{entity_name} \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", + "not_moist": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443", + "not_moving": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435", + "not_occupied": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "not_plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u044d\u043d\u0435\u0440\u0433\u0438\u0438", + "not_present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "not_unsafe": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", + "occupied": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "powered": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u044d\u043d\u0435\u0440\u0433\u0438\u0438", + "present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "problem": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "smoke": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", + "sound": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a", + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "unsafe": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", + "vibration": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 558fd9e5897e7f..a7200a0cbb4796 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -48,26 +48,27 @@ "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", - "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", - "dim_down": "\u0423\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", - "dim_up": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", + "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "dim_down": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0435\u0442\u0441\u044f", + "dim_up": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f", "left": "\u041d\u0430\u043b\u0435\u0432\u043e", - "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", - "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" }, "trigger_type": { - "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", + "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430", + "remote_button_rotation_stopped": "\u041f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \"{subtype}\"", "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", - "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b", - "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438" + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438" } }, "options": { diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json index a6a7994b7c3603..8ca964606ae8f6 100644 --- a/homeassistant/components/light/.translations/ru.json +++ b/homeassistant/components/light/.translations/ru.json @@ -6,12 +6,12 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, "condition_type": { - "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json index 7f0d122f38b2a7..ca180efa005966 100644 --- a/homeassistant/components/neato/.translations/da.json +++ b/homeassistant/components/neato/.translations/da.json @@ -4,6 +4,9 @@ "already_configured": "Allerede konfigureret", "invalid_credentials": "Ugyldige legitimationsoplysninger" }, + "create_entry": { + "default": "Se [Neato-dokumentation] ({docs_url})." + }, "error": { "invalid_credentials": "Ugyldige legitimationsoplysninger", "unexpected_error": "Uventet fejl" diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index a3ba5185371eef..7a8cf7a1424e84 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -4,6 +4,7 @@ "all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats", "already_configured": "Aquest servidor Plex ja est\u00e0 configurat", "already_in_progress": "S\u2019est\u00e0 configurant Plex", + "discovery_no_file": "No s'ha trobat cap fitxer de configuraci\u00f3 heretat", "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del testimoni.", "unknown": "Ha fallat per motiu desconegut" diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 4ca695e74d8b4b..99d5d4d1685c64 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -34,6 +34,7 @@ "title": "V\u00e6lg Plex-server" }, "start_website_auth": { + "description": "Forts\u00e6t for at autorisere p\u00e5 plex.tv.", "title": "Tilslut Plex-server" }, "user": { diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json index 95083102273a1b..56715e60a8c671 100644 --- a/homeassistant/components/plex/.translations/de.json +++ b/homeassistant/components/plex/.translations/de.json @@ -5,6 +5,11 @@ }, "step": { "manual_setup": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "SSL verwenden" + }, "title": "Plex Server" }, "start_website_auth": { @@ -12,6 +17,9 @@ "title": "Plex Server verbinden" }, "user": { + "data": { + "manual_setup": "Manuelle Einrichtung" + }, "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." } } diff --git a/homeassistant/components/sensor/.translations/ru.json b/homeassistant/components/sensor/.translations/ru.json new file mode 100644 index 00000000000000..8c70f41fcb71ae --- /dev/null +++ b/homeassistant/components/sensor/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "device_automation": { + "trigger_type": { + "battery_level": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "illuminance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "pressure": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "signal_strength": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "temperature": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "timestamp": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "value": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index cd5cbc0d6a174b..74503eea60b0e7 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -6,14 +6,14 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, "condition_type": { - "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "turn_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "turn_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 9ffd5211a1fca0..969c78e7b13e47 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -22,7 +22,9 @@ "close": "Schlie\u00dfen", "left": "Links", "open": "Offen", - "right": "Rechts" + "right": "Rechts", + "turn_off": "Ausschalten", + "turn_on": "Einschalten" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index 2f6f42311c3490..291d760dbc84fc 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -19,7 +19,42 @@ }, "device_automation": { "action_type": { - "warn": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435" + "squawk": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u0438\u0440\u0435\u043d\u0443", + "warn": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0435" + }, + "trigger_subtype": { + "both_buttons": "\u041e\u0431\u0435 \u043a\u043d\u043e\u043f\u043a\u0438", + "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_5": "\u041f\u044f\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_6": "\u0428\u0435\u0441\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "dim_down": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0435\u0442\u0441\u044f", + "dim_up": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f", + "left": "\u041d\u0430\u043b\u0435\u0432\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" + }, + "trigger_type": { + "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0431\u0440\u043e\u0441\u0438\u043b\u0438", + "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", + "device_knocked": "\u041f\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \"{subtype}\"", + "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", + "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438", + "device_slid": "\u0421\u0434\u0432\u0438\u0433 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \"{subtype}\"", + "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u043a\u043b\u043e\u043d\u0438\u043b\u0438", + "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } } } \ No newline at end of file From d58717d7720769d703662fa3e2f093742bfd18a7 Mon Sep 17 00:00:00 2001 From: John Mihalic <2854333+mezz64@users.noreply.github.com> Date: Sat, 12 Oct 2019 01:18:15 -0400 Subject: [PATCH 0758/3953] Bump pyhik to 0.2.4 (#27523) --- homeassistant/components/hikvision/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index 78917a5351bcd9..11775ed3ae085d 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -3,7 +3,7 @@ "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", "requirements": [ - "pyhik==0.2.3" + "pyhik==0.2.4" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index bff608964a4482..0512c343b742e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1220,7 +1220,7 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.hikvision -pyhik==0.2.3 +pyhik==0.2.4 # homeassistant.components.hive pyhiveapi==0.2.19.3 From 712628395e18e088ce70d89b5c19890e1c752e77 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 07:18:47 +0200 Subject: [PATCH 0759/3953] moved imports to top level (#27511) --- homeassistant/components/browser/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py index b163f16a5c47ea..b7612def70152c 100644 --- a/homeassistant/components/browser/__init__.py +++ b/homeassistant/components/browser/__init__.py @@ -1,4 +1,5 @@ """Support for launching a web browser on the host machine.""" +import webbrowser import voluptuous as vol ATTR_URL = "url" @@ -18,7 +19,6 @@ def setup(hass, config): """Listen for browse_url events.""" - import webbrowser hass.services.register( DOMAIN, From f236e84753d5836a76303ce05d46fc1da2623bd6 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 07:19:53 +0200 Subject: [PATCH 0760/3953] Move imports in updater component (#27485) --- homeassistant/components/updater/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index dd270a0bb757e9..22c11d0c38ef9a 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -10,6 +10,7 @@ import aiohttp import async_timeout +from distro import linux_distribution import voluptuous as vol from homeassistant.const import __version__ as current_version @@ -145,9 +146,7 @@ async def get_newest_version(hass, huuid, include_components): if include_components: info_object["components"] = list(hass.config.components) - import distro - - linux_dist = await hass.async_add_executor_job(distro.linux_distribution, False) + linux_dist = await hass.async_add_executor_job(linux_distribution, False) info_object["distribution"] = linux_dist[0] info_object["os_version"] = linux_dist[1] From 99e78084415b3f87790bd4ceb26684dcef30cf83 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 12 Oct 2019 07:21:53 +0200 Subject: [PATCH 0761/3953] Move imports in rmvtransport (#27420) --- homeassistant/components/rmvtransport/sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 9acafbbf81f9f9..190274518cd5f9 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -3,6 +3,8 @@ import logging from datetime import timedelta +from RMVtransport import RMVtransport +from RMVtransport.rmvtransport import RMVtransportApiConnectionError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -208,8 +210,6 @@ def __init__( timeout, ): """Initialize the sensor.""" - from RMVtransport import RMVtransport - self.station = None self._station_id = station_id self._destinations = destinations @@ -224,8 +224,6 @@ def __init__( @Throttle(SCAN_INTERVAL) async def async_update(self): """Update the connection data.""" - from RMVtransport.rmvtransport import RMVtransportApiConnectionError - try: _data = await self.rmv.get_departures( self._station_id, From a712c9b9f5ebf761d6b45f6015b20c0987d565d2 Mon Sep 17 00:00:00 2001 From: Jacob Mansfield Date: Sat, 12 Oct 2019 06:23:55 +0100 Subject: [PATCH 0762/3953] SNMP Switch payloads are not guaranteed to be integers (#27422) Fixes #27171 --- homeassistant/components/snmp/switch.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 95496cb6a45fb1..204e98cca831e9 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -186,20 +186,15 @@ def __init__( async def async_turn_on(self, **kwargs): """Turn on the switch.""" - from pyasn1.type.univ import Integer - - await self._set(Integer(self._command_payload_on)) + await self._set(self._command_payload_on) async def async_turn_off(self, **kwargs): """Turn off the switch.""" - from pyasn1.type.univ import Integer - - await self._set(Integer(self._command_payload_off)) + await self._set(self._command_payload_off) async def async_update(self): """Update the state.""" from pysnmp.hlapi.asyncio import getCmd, ObjectType, ObjectIdentity - from pyasn1.type.univ import Integer errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) @@ -215,9 +210,9 @@ async def async_update(self): ) else: for resrow in restable: - if resrow[-1] == Integer(self._payload_on): + if resrow[-1] == self._payload_on: self._state = True - elif resrow[-1] == Integer(self._payload_off): + elif resrow[-1] == self._payload_off: self._state = False else: self._state = None From d516bc44fa77a3b827972f8446d94a25d64fede9 Mon Sep 17 00:00:00 2001 From: thaohtp Date: Sat, 12 Oct 2019 07:40:44 +0200 Subject: [PATCH 0763/3953] Move trend imports to top level (#27507) --- homeassistant/components/trend/binary_sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 9154f891cc19c1..7c4a2dc4067a05 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -3,6 +3,7 @@ import logging import math +import numpy as np import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -17,9 +18,9 @@ CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, - STATE_UNKNOWN, - STATE_UNAVAILABLE, CONF_SENSORS, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -207,8 +208,6 @@ def _calculate_gradient(self): This need run inside executor. """ - import numpy as np - timestamps = np.array([t for t, _ in self.samples]) values = np.array([s for _, s in self.samples]) coeffs = np.polyfit(timestamps, values, 1) From af4bcf8de6a01d1b9eab1e3a3441c0a685a35e92 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 07:44:22 +0200 Subject: [PATCH 0764/3953] Move imports in waqi component (#27450) --- homeassistant/components/waqi/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index 9f3c3ffc13ebee..b53723a29b62da 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -5,6 +5,7 @@ import aiohttp import voluptuous as vol +from waqiasync import WaqiClient from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -60,13 +61,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the requested World Air Quality Index locations.""" - import waqiasync token = config.get(CONF_TOKEN) station_filter = config.get(CONF_STATIONS) locations = config.get(CONF_LOCATIONS) - client = waqiasync.WaqiClient(token, async_get_clientsession(hass), timeout=TIMEOUT) + client = WaqiClient(token, async_get_clientsession(hass), timeout=TIMEOUT) dev = [] try: for location_name in locations: From de4482e8d3cc2659d7f4898bdeaa6bf1b010e663 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 08:43:34 +0200 Subject: [PATCH 0765/3953] Move imports in acer_projector component (#27456) --- homeassistant/components/acer_projector/switch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 558cf84d0e1256..39a79636c9360a 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -1,6 +1,7 @@ """Use serial protocol of Acer projector to obtain state of the projector.""" import logging import re +import serial import voluptuous as vol @@ -73,7 +74,6 @@ class AcerSwitch(SwitchDevice): def __init__(self, serial_port, name, timeout, write_timeout, **kwargs): """Init of the Acer projector.""" - import serial self.ser = serial.Serial( port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs @@ -90,7 +90,6 @@ def __init__(self, serial_port, name, timeout, write_timeout, **kwargs): def _write_read(self, msg): """Write to the projector and read the return.""" - import serial ret = "" # Sometimes the projector won't answer for no reason or the projector From 3d05228ec1fec7ac3f3dcc3bcac6be083e7b143c Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 14:09:39 +0200 Subject: [PATCH 0766/3953] Move imports in vizio component (#27452) --- homeassistant/components/vizio/media_player.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index b844f94a187bf0..f64fd2ca531c67 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,7 +1,10 @@ """Vizio SmartCast Device support.""" from datetime import timedelta import logging + import voluptuous as vol +from pyvizio import Vizio + from homeassistant import util from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -122,7 +125,6 @@ class VizioDevice(MediaPlayerDevice): def __init__(self, host, token, name, volume_step, device_type): """Initialize Vizio device.""" - import pyvizio self._name = name self._state = None @@ -132,7 +134,7 @@ def __init__(self, host, token, name, volume_step, device_type): self._available_inputs = None self._device_type = device_type self._supported_commands = SUPPORTED_COMMANDS[device_type] - self._device = pyvizio.Vizio(DEVICE_ID, host, DEFAULT_NAME, token, device_type) + self._device = Vizio(DEVICE_ID, host, DEFAULT_NAME, token, device_type) self._max_volume = float(self._device.get_max_volume()) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) From 9d7a218df5156dc9afff73504f9e8e61becb7c19 Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Sat, 12 Oct 2019 15:08:57 +0200 Subject: [PATCH 0767/3953] Bump pygatt to 4.0.5 (#27526) --- homeassistant/components/bluetooth_le_tracker/manifest.json | 2 +- homeassistant/components/skybeacon/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index 30ed924a9dc404..d9f4cb0a2b54e5 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -3,7 +3,7 @@ "name": "Bluetooth le tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", "requirements": [ - "pygatt[GATTTOOL]==4.0.1" + "pygatt[GATTTOOL]==4.0.5" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/skybeacon/manifest.json b/homeassistant/components/skybeacon/manifest.json index a3cb97cdc2db1f..7ab42c5da878ce 100644 --- a/homeassistant/components/skybeacon/manifest.json +++ b/homeassistant/components/skybeacon/manifest.json @@ -3,7 +3,7 @@ "name": "Skybeacon", "documentation": "https://www.home-assistant.io/integrations/skybeacon", "requirements": [ - "pygatt[GATTTOOL]==4.0.1" + "pygatt[GATTTOOL]==4.0.5" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 0512c343b742e0..f105038592940f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1202,7 +1202,7 @@ pyfttt==0.3 # homeassistant.components.bluetooth_le_tracker # homeassistant.components.skybeacon -pygatt[GATTTOOL]==4.0.1 +pygatt[GATTTOOL]==4.0.5 # homeassistant.components.gogogate2 pygogogate2==0.1.1 From 22eaff9897b65525269b88e73aebd4cd64e9ea3b Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Sat, 12 Oct 2019 06:17:02 -0700 Subject: [PATCH 0768/3953] iaqualink: set 5s timeout, use cookiejar defaults (#27426) --- homeassistant/components/iaqualink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index dec91186be2869..9ce0e04895f2e6 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -3,7 +3,7 @@ from functools import wraps import logging -from aiohttp import CookieJar +from aiohttp import ClientTimeout import voluptuous as vol from iaqualink import ( @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] switches = hass.data[DOMAIN][SWITCH_DOMAIN] = [] - session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) + session = async_create_clientsession(hass, timeout=ClientTimeout(total=5)) aqualink = AqualinkClient(username, password, session) try: await aqualink.login() From dbe366933f6be943887d365ec7128498054a7316 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Oct 2019 15:37:32 +0200 Subject: [PATCH 0769/3953] Fix typing for device condition scaffold (#27487) --- .../templates/device_condition/integration/device_condition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index e9c7e55e23ac7a..9acb351b19799a 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -29,7 +29,7 @@ ) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[str]: +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: """List device conditions for NEW_NAME devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] From 21ca936d33ca824614621fbe7a17c20c220b2291 Mon Sep 17 00:00:00 2001 From: thaohtp Date: Sat, 12 Oct 2019 16:30:21 +0200 Subject: [PATCH 0770/3953] Move imports in upcloud component to top-level (#27514) * Move imports in upcloud component to top-level * Additional isort ordering --- homeassistant/components/upcloud/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 3656ba48e74a98..c77b0fe3cddffe 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -1,15 +1,16 @@ """Support for UpCloud.""" -import logging from datetime import timedelta +import logging +import upcloud_api import voluptuous as vol from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, - STATE_ON, + CONF_USERNAME, STATE_OFF, + STATE_ON, STATE_PROBLEM, ) from homeassistant.core import callback @@ -60,8 +61,6 @@ def setup(hass, config): """Set up the UpCloud component.""" - import upcloud_api - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) From 86386912b9fd6bc4bf4a0b8729aea74c376c4f83 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sat, 12 Oct 2019 17:53:25 +0200 Subject: [PATCH 0771/3953] Refactor Tradfri cover (#27413) * Remove unused logging * Refactor cover * Remove method * Fix typo and use consistent wording for gateway * Revert changes --- homeassistant/components/tradfri/cover.py | 106 ++++----------------- homeassistant/components/tradfri/sensor.py | 3 - 2 files changed, 16 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 1a3bf841665b4d..9b831dce0ec59d 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -1,7 +1,4 @@ """Support for IKEA Tradfri covers.""" -import logging - -from pytradfri.error import PytradfriError from homeassistant.components.cover import ( CoverDevice, @@ -10,10 +7,8 @@ SUPPORT_CLOSE, SUPPORT_SET_POSITION, ) -from homeassistant.core import callback -from .const import DOMAIN, KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID - -_LOGGER = logging.getLogger(__name__) +from .base_class import TradfriBaseDevice +from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID async def async_setup_entry(hass, config_entry, async_add_entities): @@ -29,120 +24,51 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(TradfriCover(cover, api, gateway_id) for cover in covers) -class TradfriCover(CoverDevice): +class TradfriCover(TradfriBaseDevice, CoverDevice): """The platform class required by Home Assistant.""" - def __init__(self, cover, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a cover.""" - self._api = api - self._unique_id = f"{gateway_id}-{cover.id}" - self._cover = None - self._cover_control = None - self._cover_data = None - self._name = None - self._available = True - self._gateway_id = gateway_id + super().__init__(device, api, gateway_id) + self._unique_id = f"{gateway_id}-{device.id}" - self._refresh(cover) + self._refresh(device) @property def supported_features(self): """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - @property - def unique_id(self): - """Return unique ID for cover.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - info = self._cover.device_info - - return { - "identifiers": {(DOMAIN, self._cover.id)}, - "name": self._name, - "manufacturer": info.manufacturer, - "model": info.model_number, - "sw_version": info.firmware_version, - "via_device": (DOMAIN, self._gateway_id), - } - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def should_poll(self): - """No polling needed for tradfri cover.""" - return False - - @property - def name(self): - """Return the display name of this cover.""" - return self._name - @property def current_cover_position(self): """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. """ - return 100 - self._cover_data.current_cover_position + return 100 - self._device_data.current_cover_position async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - await self._api(self._cover_control.set_state(100 - kwargs[ATTR_POSITION])) + await self._api(self._device_control.set_state(100 - kwargs[ATTR_POSITION])) async def async_open_cover(self, **kwargs): """Open the cover.""" - await self._api(self._cover_control.set_state(0)) + await self._api(self._device_control.set_state(0)) async def async_close_cover(self, **kwargs): """Close cover.""" - await self._api(self._cover_control.set_state(100)) + await self._api(self._device_control.set_state(100)) @property def is_closed(self): """Return if the cover is closed or not.""" return self.current_cover_position == 0 - @callback - def _async_start_observe(self, exc=None): - """Start observation of cover.""" - if exc: - self._available = False - self.async_schedule_update_ha_state() - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - try: - cmd = self._cover.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, cover): + def _refresh(self, device): """Refresh the cover data.""" - self._cover = cover + super()._refresh(device) + self._device = device # Caching of BlindControl and cover object - self._available = cover.reachable - self._cover_control = cover.blind_control - self._cover_data = cover.blind_control.blinds[0] - self._name = cover.name - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this cover.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() + self._device_control = device.blind_control + self._device_data = device.blind_control.blinds[0] diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 56c1a464580ba2..68a2c10291b6df 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,12 +1,9 @@ """Support for IKEA Tradfri sensors.""" -import logging from homeassistant.const import DEVICE_CLASS_BATTERY from .base_class import TradfriBaseDevice from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Tradfri config entry.""" From 96d35379f286e465ac2383fd0c7d436092ab9427 Mon Sep 17 00:00:00 2001 From: Rolf K Date: Sat, 12 Oct 2019 20:46:09 +0200 Subject: [PATCH 0772/3953] Add improved scene support to input number integration (#27530) * Added improved scene support to the input_number integration. * Minor fix in test. * Use snake case for variable names in test_reproduce_state. * Remove redundant tests. --- .../input_number/reproduce_state.py | 52 ++++++++++++++++ .../input_number/test_reproduce_state.py | 62 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 homeassistant/components/input_number/reproduce_state.py create mode 100644 tests/components/input_number/test_reproduce_state.py diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py new file mode 100644 index 00000000000000..97a4837d3711de --- /dev/null +++ b/homeassistant/components/input_number/reproduce_state.py @@ -0,0 +1,52 @@ +"""Reproduce an Input number state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + try: + float(state.state) + except ValueError: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service = SERVICE_SET_VALUE + service_data = {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: state.state} + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input number states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/input_number/test_reproduce_state.py b/tests/components/input_number/test_reproduce_state.py new file mode 100644 index 00000000000000..37ab83f3204fc3 --- /dev/null +++ b/tests/components/input_number/test_reproduce_state.py @@ -0,0 +1,62 @@ +"""Test reproduce state for Input number.""" +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +VALID_NUMBER1 = "19.0" +VALID_NUMBER2 = "99.9" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input number states.""" + + assert await async_setup_component( + hass, + "input_number", + { + "input_number": { + "test_number": {"min": "5", "max": "100", "initial": VALID_NUMBER1} + } + }, + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("input_number.test_number", VALID_NUMBER1), + # Should not raise + State("input_number.non_existing", "234"), + ], + blocking=True, + ) + + assert hass.states.get("input_number.test_number").state == VALID_NUMBER1 + + # Test reproducing with different state + await hass.helpers.state.async_reproduce_state( + [ + State("input_number.test_number", VALID_NUMBER2), + # Should not raise + State("input_number.non_existing", "234"), + ], + blocking=True, + ) + + assert hass.states.get("input_number.test_number").state == VALID_NUMBER2 + + # Test setting state to number out of range + await hass.helpers.state.async_reproduce_state( + [State("input_number.test_number", "150")], blocking=True + ) + + # The entity states should be unchanged after trying to set them to out-of-range number + assert hass.states.get("input_number.test_number").state == VALID_NUMBER2 + + await hass.helpers.state.async_reproduce_state( + [ + # Test invalid state + State("input_number.test_number", "invalid_state"), + # Set to state it already is. + State("input_number.test_number", VALID_NUMBER2), + ], + blocking=True, + ) From ee8b72fb71bcfdd4610d1acf77aef1d3604ccf92 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:27:27 +0200 Subject: [PATCH 0773/3953] Move imports in http component (#27474) --- homeassistant/components/http/cors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 39ff45fd4e48e3..bd8213355424c6 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,4 +1,5 @@ """Provide CORS support for the HTTP component.""" +import aiohttp_cors from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION @@ -22,8 +23,6 @@ @callback def setup_cors(app, origins): """Set up CORS.""" - import aiohttp_cors - cors = aiohttp_cors.setup( app, defaults={ From 42691b783eb5038bed439a3f1bb450258ab73799 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Oct 2019 21:28:47 +0200 Subject: [PATCH 0774/3953] Handle empty service in script action gracefully (#27467) * Handle empty service in script action gracefully * Add test --- homeassistant/helpers/config_validation.py | 1 + tests/components/automation/test_init.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 8598b50f140e64..7ca5a7e86f923b 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -386,6 +386,7 @@ def remove_falsy(value: List[T]) -> List[T]: def service(value): """Validate service.""" # Services use same format as entities so we can use same helper. + value = string(value).lower() if valid_entity_id(value): return value raise vol.Invalid("Service {} does not match format .".format(value)) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 6acb40cec88ef0..a0573ce7c1b88e 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -842,6 +842,25 @@ async def test_automation_with_error_in_script(hass, caplog): assert "Service not found" in caplog.text +async def test_automation_with_error_in_script_2(hass, caplog): + """Test automation with an error in script.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": {"service": None, "entity_id": "hello.world"}, + } + }, + ) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert "string value is None" in caplog.text + + async def test_automation_restore_last_triggered_with_initial_state(hass): """Ensure last_triggered is restored, even when initial state is set.""" time = dt_util.utcnow() From 3873a1b07020b95d0721b2ecc47ef23539883d42 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:35:39 +0200 Subject: [PATCH 0775/3953] moved imports to top level (#27494) --- homeassistant/components/auth/login_flow.py | 3 +-- homeassistant/components/auth/mfa_setup_flow.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 0f5da5d752763b..4fa0f866124d56 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -68,6 +68,7 @@ """ from aiohttp import web import voluptuous as vol +import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components.http import KEY_REAL_IP @@ -120,8 +121,6 @@ def _prepare_result_json(result): if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result - import voluptuous_serialize - data = result.copy() schema = data["data_schema"] diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 42dab7ebb5a0d6..271e9ae163417c 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components import websocket_api @@ -134,8 +135,6 @@ def _prepare_result_json(result): if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result - import voluptuous_serialize - data = result.copy() schema = data["data_schema"] From 40e5beb0ed0c6700a349a4be746f27cfc7da611b Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sat, 12 Oct 2019 21:37:59 +0200 Subject: [PATCH 0776/3953] Move imports in rfxtrx component (#27549) --- homeassistant/components/rfxtrx/__init__.py | 9 ++------- homeassistant/components/rfxtrx/binary_sensor.py | 4 +--- homeassistant/components/rfxtrx/cover.py | 3 +-- homeassistant/components/rfxtrx/light.py | 4 +--- homeassistant/components/rfxtrx/sensor.py | 5 ++--- homeassistant/components/rfxtrx/switch.py | 4 +--- tests/components/rfxtrx/test_cover.py | 4 +--- tests/components/rfxtrx/test_light.py | 4 +--- tests/components/rfxtrx/test_switch.py | 6 +----- 9 files changed, 11 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 79b3054ecf2750..73ee07cfb5f762 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -1,7 +1,8 @@ """Support for RFXtrx devices.""" from collections import OrderedDict +import binascii import logging - +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.const import ( @@ -113,9 +114,6 @@ def handle_receive(event): for subscriber in RECEIVED_EVT_SUBSCRIBERS: subscriber(event) - # Try to load the RFXtrx module. - import RFXtrx as rfxtrxmod - device = config[DOMAIN][ATTR_DEVICE] debug = config[DOMAIN][ATTR_DEBUG] dummy_connection = config[DOMAIN][ATTR_DUMMY] @@ -144,8 +142,6 @@ def _shutdown_rfxtrx(event): def get_rfx_object(packetid): """Return the RFXObject with the packetid.""" - import RFXtrx as rfxtrxmod - try: binarypacket = bytearray.fromhex(packetid) except ValueError: @@ -167,7 +163,6 @@ def get_pt2262_deviceid(device_id, nb_data_bits): """Extract and return the address bits from a Lighting4/PT2262 packet.""" if nb_data_bits is None: return - import binascii try: data = bytearray.fromhex(device_id) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 8f1c7e6fa55f3d..259f914b408ae8 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -1,6 +1,6 @@ """Support for RFXtrx binary sensors.""" import logging - +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -54,8 +54,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Binary Sensor platform to RFXtrx.""" - import RFXtrx as rfxtrxmod - sensors = [] for packet_id, entity in config[CONF_DEVICES].items(): diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 3d420981685e9c..7aff22bd12460f 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -1,4 +1,5 @@ """Support for RFXtrx covers.""" +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -34,8 +35,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx cover.""" - import RFXtrx as rfxtrxmod - covers = rfxtrx.get_devices_from_config(config, RfxtrxCover) add_entities(covers) diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index d2d2e842c0aa8c..82b1407c79875b 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -1,6 +1,6 @@ """Support for RFXtrx lights.""" import logging - +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -45,8 +45,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" - import RFXtrx as rfxtrxmod - lights = rfxtrx.get_devices_from_config(config, RfxtrxLight) add_entities(lights) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 5941b00764b8ce..5f6b90b600f1c9 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -1,8 +1,9 @@ """Support for RFXtrx sensors.""" import logging - import voluptuous as vol +from RFXtrx import SensorEvent + from homeassistant.components import rfxtrx from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_NAME @@ -43,8 +44,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" - from RFXtrx import SensorEvent - sensors = [] for packet_id, entity_info in config[CONF_DEVICES].items(): event = rfxtrx.get_rfx_object(packet_id) diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index bb5d5fe6d439d9..b5c830a298d868 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -1,6 +1,6 @@ """Support for RFXtrx switches.""" import logging - +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -38,8 +38,6 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the RFXtrx platform.""" - import RFXtrx as rfxtrxmod - # Add switch from config file switches = rfxtrx.get_devices_from_config(config, RfxtrxSwitch) add_entities_callback(switches) diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index 9fa71bdab6751c..d2bfb1148049f1 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -1,7 +1,7 @@ """The tests for the Rfxtrx cover platform.""" import unittest - import pytest +import RFXtrx as rfxtrxmod from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core @@ -142,8 +142,6 @@ def test_one_cover(self): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index f3a6bcab1b18f4..1254a6d6697443 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -1,7 +1,7 @@ """The tests for the Rfxtrx light platform.""" import unittest - import pytest +import RFXtrx as rfxtrxmod from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core @@ -109,8 +109,6 @@ def test_old_config(self): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index dc955a198a7a3d..1e39d4afb75e33 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -1,7 +1,7 @@ """The tests for the Rfxtrx switch platform.""" import unittest - import pytest +import RFXtrx as rfxtrxmod from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core @@ -166,8 +166,6 @@ def test_old_config(self): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) @@ -200,8 +198,6 @@ def test_one_switch(self): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) From ddeac071b32d9d25db5e43d75d050f5a36fe6e0a Mon Sep 17 00:00:00 2001 From: Moritz Fey Date: Sat, 12 Oct 2019 21:38:39 +0200 Subject: [PATCH 0777/3953] fill services.yaml for downloader (#27553) --- homeassistant/components/downloader/services.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/downloader/services.yaml b/homeassistant/components/downloader/services.yaml index e69de29bb2d1d6..d16b2788c70ae2 100644 --- a/homeassistant/components/downloader/services.yaml +++ b/homeassistant/components/downloader/services.yaml @@ -0,0 +1,15 @@ +download_file: + description: Downloads a file to the download location. + fields: + url: + description: The URL of the file to download. + example: 'http://example.org/myfile' + subdir: + description: Download into subdirectory. + example: 'download_dir' + filename: + description: Determine the filename. + example: 'my_file_name' + overwrite: + description: Whether to overwrite the file or not. + example: 'false' \ No newline at end of file From 5030be274a36d5c0f166aa408d9cc933951a1a01 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 21:43:06 +0200 Subject: [PATCH 0778/3953] Add test to Homematic IP Cloud weather (#27536) --- .../homematicip_cloud/test_weather.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/components/homematicip_cloud/test_weather.py diff --git a/tests/components/homematicip_cloud/test_weather.py b/tests/components/homematicip_cloud/test_weather.py new file mode 100644 index 00000000000000..0b5d59215bbea0 --- /dev/null +++ b/tests/components/homematicip_cloud/test_weather.py @@ -0,0 +1,82 @@ +"""Tests for HomematicIP Cloud weather.""" +from homeassistant.components.weather import ( + ATTR_WEATHER_ATTRIBUTION, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, +) + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_weather_sensor(hass, default_mock_hap): + """Test HomematicipWeatherSensor.""" + entity_id = "weather.weather_sensor_plus" + entity_name = "Weather Sensor – plus" + device_model = "HmIP-SWO-PL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "" + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 4.3 + assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 97 + assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0 + assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 12.1) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1 + + +async def test_hmip_weather_sensor_pro(hass, default_mock_hap): + """Test HomematicipWeatherSensorPro.""" + entity_id = "weather.wettersensor_pro" + entity_name = "Wettersensor - pro" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "sunny" + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 15.4 + assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 65 + assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 2.6 + assert ha_state.attributes[ATTR_WEATHER_WIND_BEARING] == 295.0 + assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 12.1) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1 + + +async def test_hmip_home_weather(hass, default_mock_hap): + """Test HomematicipHomeWeather.""" + entity_id = "weather.weather_1010_wien_osterreich" + entity_name = "Weather 1010 Wien, Österreich" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + assert hmip_device + assert ha_state.state == "partlycloudy" + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 16.6 + assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 54 + assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 8.6 + assert ha_state.attributes[ATTR_WEATHER_WIND_BEARING] == 294 + assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + + await async_manipulate_test_data( + hass, + default_mock_hap.home.weather, + "temperature", + 28.3, + fire_device=default_mock_hap.home, + ) + + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 28.3 From eb77db6569a352824548be8f5381ab294c06f028 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 21:43:46 +0200 Subject: [PATCH 0779/3953] Add test to Homematic IP Cloud alarm control panel (#27534) --- .../test_alarm_control_panel.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 tests/components/homematicip_cloud/test_alarm_control_panel.py diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py new file mode 100644 index 00000000000000..0a68ac6d509a14 --- /dev/null +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -0,0 +1,130 @@ +"""Tests for HomematicIP Cloud alarm control panel.""" +from homematicip.base.enums import WindowState +from homematicip.group import SecurityZoneGroup + +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) + +from .helper import get_and_check_entity_basics + + +def _get_security_zones(groups): # pylint: disable=W0221 + """Get the security zones.""" + for group in groups: + if isinstance(group, SecurityZoneGroup): + if group.label == "EXTERNAL": + external = group + elif group.label == "INTERNAL": + internal = group + return internal, external + + +async def _async_manipulate_security_zones( + hass, home, internal_active, external_active, window_state +): + """Set new values on hmip security zones.""" + internal_zone, external_zone = _get_security_zones(home.groups) + external_zone.active = external_active + external_zone.windowState = window_state + internal_zone.active = internal_active + + # Just one call to a security zone is required to refresh the ACP. + internal_zone.fire_update_event() + + await hass.async_block_till_done() + + +async def test_hmip_alarm_control_panel(hass, default_mock_hap): + """Test HomematicipAlarmControlPanel.""" + entity_id = "alarm_control_panel.hmip_alarm_control_panel" + entity_name = "HmIP Alarm Control Panel" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "disarmed" + assert not hmip_device + + home = default_mock_hap.home + service_call_counter = len(home.mock_calls) + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 1 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (True, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=True, + external_active=True, + window_state=WindowState.CLOSED, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_AWAY + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 3 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (False, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=False, + external_active=True, + window_state=WindowState.CLOSED, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_HOME + + await hass.services.async_call( + "alarm_control_panel", "alarm_disarm", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 5 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (False, False) + await _async_manipulate_security_zones( + hass, + home, + internal_active=False, + external_active=False, + window_state=WindowState.CLOSED, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_DISARMED + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 7 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (True, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=True, + external_active=True, + window_state=WindowState.OPEN, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 9 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (False, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=False, + external_active=True, + window_state=WindowState.OPEN, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED From bb1be5327e98cf6fed2137102d815e2b2b677cc4 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 21:44:13 +0200 Subject: [PATCH 0780/3953] Add test to Homematic IP Cloud cover (#27535) --- .../homematicip_cloud/test_cover.py | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 tests/components/homematicip_cloud/test_cover.py diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py new file mode 100644 index 00000000000000..7bfb842a0df626 --- /dev/null +++ b/tests/components/homematicip_cloud/test_cover.py @@ -0,0 +1,141 @@ +"""Tests for HomematicIP Cloud cover.""" +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, +) +from homeassistant.const import STATE_CLOSED, STATE_OPEN + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_cover_shutter(hass, default_mock_hap): + """Test HomematicipCoverShutte.""" + entity_id = "cover.sofa_links" + entity_name = "Sofa links" + device_model = "HmIP-FBL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "closed" + assert ha_state.attributes["current_position"] == 0 + assert ha_state.attributes["current_tilt_position"] == 0 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_shutter_level" + assert hmip_device.mock_calls[-1][1] == (0,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": entity_id, "position": "50"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "set_shutter_level" + assert hmip_device.mock_calls[-1][1] == (0.5,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 50 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 5 + assert hmip_device.mock_calls[-1][0] == "set_shutter_level" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 7 + assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_CLOSED + + +async def test_hmip_cover_slats(hass, default_mock_hap): + """Test HomematicipCoverSlats.""" + entity_id = "cover.sofa_links" + entity_name = "Sofa links" + device_model = "HmIP-FBL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (0,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + + await hass.services.async_call( + "cover", + "set_cover_tilt_position", + {"entity_id": entity_id, "tilt_position": "50"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (0.5,) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 + + await hass.services.async_call( + "cover", "close_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 6 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 8 + assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN From 28e3cf29b3d8256a7daa5a919c503f7f1a45faee Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 21:44:19 +0200 Subject: [PATCH 0781/3953] Add test to Homematic IP Cloud sensor (#27533) --- .../homematicip_cloud/test_sensor.py | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 tests/components/homematicip_cloud/test_sensor.py diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py new file mode 100644 index 00000000000000..d4307477975a92 --- /dev/null +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -0,0 +1,242 @@ +"""Tests for HomematicIP Cloud sensor.""" +from homeassistant.components.homematicip_cloud.sensor import ( + ATTR_LEFT_COUNTER, + ATTR_RIGHT_COUNTER, + ATTR_TEMPERATURE_OFFSET, + ATTR_WIND_DIRECTION, + ATTR_WIND_DIRECTION_VARIATION, +) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_accesspoint_status(hass, default_mock_hap): + """Test HomematicipSwitch.""" + entity_id = "sensor.access_point" + entity_name = "Access Point" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + assert hmip_device + assert ha_state.state == "8.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + + await async_manipulate_test_data(hass, hmip_device, "dutyCycle", 17.3) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "17.3" + + +async def test_hmip_heating_thermostat(hass, default_mock_hap): + """Test HomematicipHeatingThermostat.""" + entity_id = "sensor.heizkorperthermostat_heating" + entity_name = "Heizkörperthermostat Heating" + device_model = "HMIP-eTRV" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.37) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "37" + + await async_manipulate_test_data(hass, hmip_device, "valveState", "nn") + ha_state = hass.states.get(entity_id) + assert ha_state.state == "nn" + + +async def test_hmip_humidity_sensor(hass, default_mock_hap): + """Test HomematicipHumiditySensor.""" + entity_id = "sensor.bwth_1_humidity" + entity_name = "BWTH 1 Humidity" + device_model = "HmIP-BWTH" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "40" + assert ha_state.attributes["unit_of_measurement"] == "%" + await async_manipulate_test_data(hass, hmip_device, "humidity", 45) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "45" + + +async def test_hmip_temperature_sensor1(hass, default_mock_hap): + """Test HomematicipTemperatureSensor.""" + entity_id = "sensor.bwth_1_temperature" + entity_name = "BWTH 1 Temperature" + device_model = "HmIP-BWTH" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "21.0" + assert ha_state.attributes["unit_of_measurement"] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + assert not ha_state.attributes.get("temperature_offset") + await async_manipulate_test_data(hass, hmip_device, "temperatureOffset", 10) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10 + + +async def test_hmip_temperature_sensor2(hass, default_mock_hap): + """Test HomematicipTemperatureSensor.""" + entity_id = "sensor.heizkorperthermostat_temperature" + entity_name = "Heizkörperthermostat Temperature" + device_model = "HMIP-eTRV" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "20.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "valveActualTemperature", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + assert not ha_state.attributes.get(ATTR_TEMPERATURE_OFFSET) + await async_manipulate_test_data(hass, hmip_device, "temperatureOffset", 10) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10 + + +async def test_hmip_power_sensor(hass, default_mock_hap): + """Test HomematicipPowerSensor.""" + entity_id = "sensor.flur_oben_power" + entity_name = "Flur oben Power" + device_model = "HmIP-BSM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "0.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + +async def test_hmip_illuminance_sensor1(hass, default_mock_hap): + """Test HomematicipIlluminanceSensor.""" + entity_id = "sensor.wettersensor_illuminance" + entity_name = "Wettersensor Illuminance" + device_model = "HmIP-SWO-B" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "4890.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx" + await async_manipulate_test_data(hass, hmip_device, "illumination", 231) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "231" + + +async def test_hmip_illuminance_sensor2(hass, default_mock_hap): + """Test HomematicipIlluminanceSensor.""" + entity_id = "sensor.lichtsensor_nord_illuminance" + entity_name = "Lichtsensor Nord Illuminance" + device_model = "HmIP-SLO" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "807.3" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx" + await async_manipulate_test_data(hass, hmip_device, "averageIllumination", 231) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "231" + + +async def test_hmip_windspeed_sensor(hass, default_mock_hap): + """Test HomematicipWindspeedSensor.""" + entity_id = "sensor.wettersensor_pro_windspeed" + entity_name = "Wettersensor - pro Windspeed" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "2.6" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "km/h" + await async_manipulate_test_data(hass, hmip_device, "windSpeed", 9.4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "9.4" + + assert ha_state.attributes[ATTR_WIND_DIRECTION_VARIATION] == 56.25 + assert ha_state.attributes[ATTR_WIND_DIRECTION] == "WNW" + + wind_directions = { + 25: "NNE", + 37.5: "NE", + 70: "ENE", + 92.5: "E", + 115: "ESE", + 137.5: "SE", + 160: "SSE", + 182.5: "S", + 205: "SSW", + 227.5: "SW", + 250: "WSW", + 272.5: "W", + 295: "WNW", + 317.5: "NW", + 340: "NNW", + 0: "N", + } + + for direction, txt in wind_directions.items(): + await async_manipulate_test_data(hass, hmip_device, "windDirection", direction) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WIND_DIRECTION] == txt + + +async def test_hmip_today_rain_sensor(hass, default_mock_hap): + """Test HomematicipTodayRainSensor.""" + entity_id = "sensor.weather_sensor_plus_today_rain" + entity_name = "Weather Sensor – plus Today Rain" + device_model = "HmIP-SWO-PL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "3.9" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "mm" + await async_manipulate_test_data(hass, hmip_device, "todayRainCounter", 14.2) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "14.2" + + +async def test_hmip_passage_detector_delta_counter(hass, default_mock_hap): + """Test HomematicipPassageDetectorDeltaCounter.""" + entity_id = "sensor.spdr_1" + entity_name = "SPDR_1" + device_model = "HmIP-SPDR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "164" + assert ha_state.attributes[ATTR_LEFT_COUNTER] == 966 + assert ha_state.attributes[ATTR_RIGHT_COUNTER] == 802 + await async_manipulate_test_data(hass, hmip_device, "leftRightCounterDelta", 190) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "190" From 6317ef13245602238d07d570f4fe23bb6f69f82d Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:44:47 +0200 Subject: [PATCH 0782/3953] moved imports to top level (#27512) --- homeassistant/components/bt_home_hub_5/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 0a068a3981faf9..20ad909c44e4e4 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -1,6 +1,8 @@ """Support for BT Home Hub 5.""" import logging +import bthomehub5_devicelist + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -32,7 +34,6 @@ class BTHomeHub5DeviceScanner(DeviceScanner): def __init__(self, config): """Initialise the scanner.""" - import bthomehub5_devicelist _LOGGER.info("Initialising BT Home Hub 5") self.host = config[CONF_HOST] @@ -61,7 +62,6 @@ def get_device_name(self, device): def update_info(self): """Ensure the information from the BT Home Hub 5 is up to date.""" - import bthomehub5_devicelist _LOGGER.info("Scanning") From f979eca83ac364e4c5a334dcc41efd2974dbea6d Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 21:45:11 +0200 Subject: [PATCH 0783/3953] Add test to Homematic IP Cloud climate (#27472) --- .../components/homematicip_cloud/hap.py | 1 + tests/components/homematicip_cloud/helper.py | 21 +- .../homematicip_cloud/test_climate.py | 230 ++++++++++++++++++ .../homematicip_cloud/test_device.py | 111 +++++++++ 4 files changed, 360 insertions(+), 3 deletions(-) create mode 100644 tests/components/homematicip_cloud/test_climate.py create mode 100644 tests/components/homematicip_cloud/test_device.py diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 22ab1fd617c408..f6727f91c7e7e9 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -220,6 +220,7 @@ async def async_reset(self): await self.hass.config_entries.async_forward_entry_unload( self.config_entry, component ) + self.hmip_device_by_entity_id = {} return True async def get_hap( diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index b5e41a6ae86407..e5c5c4569d7274 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -35,6 +35,7 @@ def get_and_check_entity_basics( assert ha_state.name == entity_name hmip_device = default_mock_hap.hmip_device_by_entity_id.get(entity_id) + if hmip_device: if isinstance(hmip_device, AsyncDevice): assert ha_state.attributes[ATTR_IS_GROUP] is False @@ -85,14 +86,20 @@ def __init__(self, connection=None): super().__init__(connection=connection) self.label = "Access Point" self.model_type = "HmIP-HAP" + self.init_json_state = None def init_home(self, json_path=HOME_JSON): """Init template with json.""" - json_state = json.loads(load_fixture(HOME_JSON), encoding="UTF-8") - self.update_home(json_state=json_state, clearConfig=True) - self._generate_mocks() + self.init_json_state = json.loads(load_fixture(HOME_JSON), encoding="UTF-8") + self.update_home(json_state=self.init_json_state, clearConfig=True) return self + def update_home(self, json_state, clearConfig: bool = False): + """Update home and ensure that mocks are created.""" + result = super().update_home(json_state, clearConfig) + self._generate_mocks() + return result + def _generate_mocks(self): """Generate mocks for groups and devices.""" mock_devices = [] @@ -105,6 +112,10 @@ def _generate_mocks(self): mock_groups.append(_get_mock(group)) self.groups = mock_groups + def download_configuration(self): + """Return the initial json config.""" + return self.init_json_state + def get_async_home_mock(self): """ Create Mock for Async_Home. based on template to be used for testing. @@ -123,6 +134,10 @@ def get_async_home_mock(self): def _get_mock(instance): """Create a mock and copy instance attributes over mock.""" + if isinstance(instance, Mock): + instance.__dict__.update(instance._mock_wraps.__dict__) # pylint: disable=W0212 + return instance + mock = Mock(spec=instance, wraps=instance) mock.__dict__.update(instance.__dict__) return mock diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py new file mode 100644 index 00000000000000..8f8a681fad855c --- /dev/null +++ b/tests/components/homematicip_cloud/test_climate.py @@ -0,0 +1,230 @@ +"""Tests for HomematicIP Cloud climate.""" +import datetime + +from homematicip.base.enums import AbsenceType +from homematicip.functionalHomes import IndoorClimateHome + +from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + PRESET_AWAY, + PRESET_BOOST, + PRESET_ECO, + PRESET_NONE, +) + +from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_heating_group(hass, default_mock_hap): + """Test HomematicipHeatingGroup.""" + entity_id = "climate.badezimmer" + entity_name = "Badezimmer" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes["current_temperature"] == 23.8 + assert ha_state.attributes["min_temp"] == 5.0 + assert ha_state.attributes["max_temp"] == 30.0 + assert ha_state.attributes["temperature"] == 5.0 + assert ha_state.attributes["current_humidity"] == 47 + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_NONE, PRESET_BOOST] + + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": entity_id, "temperature": 22.5}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_point_temperature" + assert hmip_device.mock_calls[-1][1] == (22.5,) + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 22.5) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("MANUAL",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "MANUAL") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_HEAT + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_AUTO}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 5 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("AUTOMATIC",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "AUTO") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": PRESET_BOOST}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 7 + assert hmip_device.mock_calls[-1][0] == "set_boost" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "boostMode", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_BOOST + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": PRESET_NONE}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 9 + assert hmip_device.mock_calls[-1][0] == "set_boost" + assert hmip_device.mock_calls[-1][1] == (False,) + await async_manipulate_test_data(hass, hmip_device, "boostMode", False) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + # Not required for hmip, but a posiblity to send no temperature. + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": entity_id, "target_temp_low": 10, "target_temp_high": 10}, + blocking=True, + ) + # No new service call should be in mock_calls. + assert len(hmip_device.mock_calls) == service_call_counter + 10 + # Only fire event from last async_manipulate_test_data available. + assert hmip_device.mock_calls[-1][0] == "fire_update_event" + + await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") + await async_manipulate_test_data( + hass, + default_mock_hap.home.get_functionalHome(IndoorClimateHome), + "absenceType", + AbsenceType.VACATION, + fire_device=hmip_device, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY + + await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") + await async_manipulate_test_data( + hass, + default_mock_hap.home.get_functionalHome(IndoorClimateHome), + "absenceType", + AbsenceType.PERIOD, + fire_device=hmip_device, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO + + +async def test_hmip_climate_services(hass, mock_hap_with_service): + """Test HomematicipHeatingGroup.""" + + home = mock_hap_with_service.home + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_duration", + {"duration": 60, "accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_absence_with_duration" + assert home.mock_calls[-1][1] == (60,) + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_duration", + {"duration": 60}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_absence_with_duration" + assert home.mock_calls[-1][1] == (60,) + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_period", + {"endtime": "2019-02-17 14:00", "accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_absence_with_period" + assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_period", + {"endtime": "2019-02-17 14:00"}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_absence_with_period" + assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) + + await hass.services.async_call( + "homematicip_cloud", + "activate_vacation", + {"endtime": "2019-02-17 14:00", "temperature": 18.5, "accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_vacation" + assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) + + await hass.services.async_call( + "homematicip_cloud", + "activate_vacation", + {"endtime": "2019-02-17 14:00", "temperature": 18.5}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_vacation" + assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) + + await hass.services.async_call( + "homematicip_cloud", + "deactivate_eco_mode", + {"accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "deactivate_absence" + assert home.mock_calls[-1][1] == () + + await hass.services.async_call( + "homematicip_cloud", "deactivate_eco_mode", blocking=True + ) + assert home.mock_calls[-1][0] == "deactivate_absence" + assert home.mock_calls[-1][1] == () + + await hass.services.async_call( + "homematicip_cloud", + "deactivate_vacation", + {"accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "deactivate_vacation" + assert home.mock_calls[-1][1] == () + + await hass.services.async_call( + "homematicip_cloud", "deactivate_vacation", blocking=True + ) + assert home.mock_calls[-1][0] == "deactivate_vacation" + assert home.mock_calls[-1][1] == () diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py new file mode 100644 index 00000000000000..81c35f8e2a93b1 --- /dev/null +++ b/tests/components/homematicip_cloud/test_device.py @@ -0,0 +1,111 @@ +"""Common tests for HomematicIP devices.""" +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_remove_device(hass, default_mock_hap): + """Test Remove of hmip device.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + device_registry = await dr.async_get_registry(hass) + entity_registry = await er.async_get_registry(hass) + + pre_device_count = len(device_registry.devices) + pre_entity_count = len(entity_registry.entities) + pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id) + + hmip_device.fire_remove_event() + + await hass.async_block_till_done() + + assert len(device_registry.devices) == pre_device_count - 1 + assert len(entity_registry.entities) == pre_entity_count - 3 + assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3 + + +async def test_hmip_remove_group(hass, default_mock_hap): + """Test Remove of hmip group.""" + entity_id = "switch.strom_group" + entity_name = "Strom Group" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + device_registry = await dr.async_get_registry(hass) + entity_registry = await er.async_get_registry(hass) + + pre_device_count = len(device_registry.devices) + pre_entity_count = len(entity_registry.entities) + pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id) + + hmip_device.fire_remove_event() + + await hass.async_block_till_done() + + assert len(device_registry.devices) == pre_device_count + assert len(entity_registry.entities) == pre_entity_count - 1 + assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 1 + + +async def test_all_devices_unavailable_when_hap_not_connected(hass, default_mock_hap): + """Test make all devices unavaulable when hap is not connected.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + assert default_mock_hap.home.connected + + await async_manipulate_test_data(hass, default_mock_hap.home, "connected", False) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNAVAILABLE + + +async def test_hap_reconnected(hass, default_mock_hap): + """Test reconnect hap.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + assert default_mock_hap.home.connected + + await async_manipulate_test_data(hass, default_mock_hap.home, "connected", False) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNAVAILABLE + + default_mock_hap._accesspoint_connected = False # pylint: disable=W0212 + await async_manipulate_test_data(hass, default_mock_hap.home, "connected", True) + await hass.async_block_till_done() + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON From ae5cb82908ecad64600daf0b23f9d10180cbad6f Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:45:31 +0200 Subject: [PATCH 0784/3953] moved imports to top level (#27508) --- homeassistant/components/broadlink/sensor.py | 3 ++- homeassistant/components/broadlink/switch.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 98988965ca05a7..6374f35c503e16 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -3,6 +3,8 @@ import logging from datetime import timedelta +import broadlink + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -128,7 +130,6 @@ def __init__(self, interval, ip_addr, mac_addr, timeout): _LOGGER.warning("Failed to connect to device") def _connect(self): - import broadlink self._device = broadlink.a1((self.ip_addr, 80), self.mac_addr, None) self._device.timeout = self.timeout diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index d60331aaa4407e..bfb6dc4f42e370 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -4,6 +4,8 @@ import logging import socket +import broadlink + import voluptuous as vol from homeassistant.components.switch import ( @@ -91,7 +93,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Broadlink switches.""" - import broadlink devices = config.get(CONF_SWITCHES) slots = config.get("slots", {}) From b9d54de09bb73af3440a7bd83b3f752b9b7c017a Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:45:40 +0200 Subject: [PATCH 0785/3953] moved imports to top level (#27509) --- homeassistant/components/brottsplatskartan/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 5a3f72c3ef234c..d8592f44fff7c0 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -4,6 +4,8 @@ import logging import uuid +import brottsplatskartan + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -60,7 +62,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Brottsplatskartan platform.""" - import brottsplatskartan area = config.get(CONF_AREA) latitude = config.get(CONF_LATITUDE, hass.config.latitude) @@ -105,7 +106,6 @@ def device_state_attributes(self): def update(self): """Update device state.""" - import brottsplatskartan incident_counts = defaultdict(int) incidents = self._brottsplatskartan.get_incidents() From 1dcdc17202045b0bec3071c34efb5de1a64ccf39 Mon Sep 17 00:00:00 2001 From: thaohtp Date: Sat, 12 Oct 2019 21:46:26 +0200 Subject: [PATCH 0786/3953] Move imports in startca to top-level (#27510) --- homeassistant/components/startca/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 1b567c58b45b87..55ae15cede7ea9 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -1,10 +1,11 @@ """Support for Start.ca Bandwidth Monitor.""" from datetime import timedelta -from xml.parsers.expat import ExpatError import logging -import async_timeout +from xml.parsers.expat import ExpatError +import async_timeout import voluptuous as vol +import xmltodict from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_API_KEY, CONF_MONITORED_VARIABLES, CONF_NAME @@ -138,8 +139,6 @@ def bytes_to_gb(value): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the Start.ca bandwidth data from the web service.""" - import xmltodict - _LOGGER.debug("Updating Start.ca usage data") url = "https://www.start.ca/support/usage/api?key=" + self.api_key with async_timeout.timeout(REQUEST_TIMEOUT): From 0331c8453a9a3cd54b1149b32b0531fd54cb4822 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:48:12 +0200 Subject: [PATCH 0787/3953] moved imports to top level (#27503) --- .../components/bluetooth_le_tracker/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 29eecdfd07741c..18edd750639526 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -2,6 +2,8 @@ import asyncio import logging +import pygatt # pylint: disable=import-error + from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, @@ -26,8 +28,6 @@ def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth LE Scanner.""" - # pylint: disable=import-error - import pygatt new_devices = {} hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None}) From b8256316764a6096496bebf6de6326eeaf8a138e Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:48:30 +0200 Subject: [PATCH 0788/3953] moved imports to top level (#27501) --- homeassistant/components/bh1750/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index 0a305c21adbebc..cc91fa48bae443 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -2,6 +2,9 @@ from functools import partial import logging +import smbus # pylint: disable=import-error +from i2csense.bh1750 import BH1750 # pylint: disable=import-error + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -60,8 +63,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BH1750 sensor.""" - import smbus # pylint: disable=import-error - from i2csense.bh1750 import BH1750 # pylint: disable=import-error name = config.get(CONF_NAME) bus_number = config.get(CONF_I2C_BUS) From 4cded9782dc7932c8b88fadad7e333540f4025e3 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:50:30 +0200 Subject: [PATCH 0789/3953] moved imports to top level (#27498) --- homeassistant/components/axis/device.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 3b91f7e147456b..e42a758f3c414c 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -3,6 +3,9 @@ import asyncio import async_timeout +import axis +from axis.streammanager import SIGNAL_PLAYING + from homeassistant.const import ( CONF_DEVICE, CONF_HOST, @@ -140,7 +143,6 @@ def async_connection_status_callback(self, status): This is called on every RTSP keep-alive message. Only signal state change if state change is true. """ - from axis.streammanager import SIGNAL_PLAYING if self.available != (status == SIGNAL_PLAYING): self.available = not self.available @@ -198,7 +200,6 @@ async def async_reset(self): async def get_device(hass, config): """Create a Axis device.""" - import axis device = axis.AxisDevice( loop=hass.loop, From 3f9f8eb379c064bc3073e6ecab85242a25ceed07 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sat, 12 Oct 2019 15:51:10 -0400 Subject: [PATCH 0790/3953] Update blink version to 0.14.2 (#27555) * Update blink version to 0.14.2 * Ren gen_requirements_all script --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index a38ba0bd613b82..47cded00cc02f6 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -3,7 +3,7 @@ "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", "requirements": [ - "blinkpy==0.14.1" + "blinkpy==0.14.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index f105038592940f..df024f34cdac0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -282,7 +282,7 @@ bimmer_connected==0.6.0 bizkaibus==0.1.1 # homeassistant.components.blink -blinkpy==0.14.1 +blinkpy==0.14.2 # homeassistant.components.blinksticklight blinkstick==1.1.8 From 3ca74373d3f86e6da1252d906829a937a55c0539 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:51:32 +0200 Subject: [PATCH 0791/3953] moved imports to top level (#27500) --- homeassistant/components/bbox/device_tracker.py | 4 ++-- homeassistant/components/bbox/sensor.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 89449aeab4592f..122016ecf96d2b 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -4,6 +4,8 @@ import logging from typing import List +import pybbox + import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -75,8 +77,6 @@ def _update_info(self): """ _LOGGER.info("Scanning...") - import pybbox - box = pybbox.Bbox(ip=self.host) result = box.get_all_connected_devices() diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index ba38f8d2607dc3..ad6bcc39796e53 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -3,6 +3,8 @@ from datetime import timedelta import requests +import pybbox + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -136,7 +138,6 @@ def __init__(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the Bbox.""" - import pybbox try: box = pybbox.Bbox() From 468e6c30b3ee37b54f6135d3890a1f4d66e95a9e Mon Sep 17 00:00:00 2001 From: thaohtp Date: Sat, 12 Oct 2019 21:52:04 +0200 Subject: [PATCH 0792/3953] Move imports in aruba component to top-level (#27497) Issue: https://github.com/home-assistant/home-assistant/issues/27284 --- homeassistant/components/aruba/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index f93533b6beb32d..485c731ff6a3be 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -2,15 +2,16 @@ import logging import re +import pexpect import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -82,7 +83,6 @@ def _update_info(self): def get_aruba_data(self): """Retrieve data from Aruba Access Point and return parsed result.""" - import pexpect connect = "ssh {}@{}" ssh = pexpect.spawn(connect.format(self.username, self.host)) From 2c8e24eb14eaccfacb8f3504a36113966e8d6cf7 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:52:19 +0200 Subject: [PATCH 0793/3953] moved imports to top level (#27496) --- homeassistant/components/aws/__init__.py | 3 ++- homeassistant/components/aws/notify.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index 1959cc05e80e5b..780a65b2d47336 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -3,6 +3,8 @@ import logging from collections import OrderedDict +import aiobotocore + import voluptuous as vol from homeassistant import config_entries @@ -151,7 +153,6 @@ async def async_setup_entry(hass, entry): async def _validate_aws_credentials(hass, credential): """Validate AWS credential config.""" - import aiobotocore aws_config = credential.copy() del aws_config[CONF_NAME] diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index fa1cf3fa363345..2afa9a3a4024ff 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -4,6 +4,8 @@ import json import logging +import aiobotocore + from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, @@ -26,7 +28,6 @@ async def get_available_regions(hass, service): """Get available regions for a service.""" - import aiobotocore session = aiobotocore.get_session() # get_available_regions is not a coroutine since it does not perform @@ -41,8 +42,6 @@ async def async_get_service(hass, config, discovery_info=None): _LOGGER.error("Please config aws notify platform in aws component") return None - import aiobotocore - session = None conf = discovery_info From 8436acbffa581d7d7e5759813fd2f5736b0b919b Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:52:34 +0200 Subject: [PATCH 0794/3953] moved imports to top level (#27495) --- homeassistant/components/automatic/device_tracker.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automatic/device_tracker.py b/homeassistant/components/automatic/device_tracker.py index 09cf3f67114a2e..fbb823dd329270 100644 --- a/homeassistant/components/automatic/device_tracker.py +++ b/homeassistant/components/automatic/device_tracker.py @@ -6,6 +6,8 @@ import os from aiohttp import web +import aioautomatic + import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -82,7 +84,6 @@ def _write_refresh_token_to_file(hass, filename, refresh_token): @asyncio.coroutine def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return an Automatic scanner.""" - import aioautomatic hass.http.register_view(AutomaticAuthCallbackView()) @@ -215,7 +216,6 @@ def __init__(self, hass, client, session, devices, async_see): @asyncio.coroutine def handle_event(self, name, event): """Coroutine to update state for a real time event.""" - import aioautomatic self.hass.bus.async_fire(EVENT_AUTOMATIC_UPDATE, event.data) @@ -261,7 +261,6 @@ def handle_event(self, name, event): @asyncio.coroutine def ws_connect(self, now=None): """Open the websocket connection.""" - import aioautomatic self.ws_close_requested = False @@ -321,7 +320,6 @@ def load_vehicle(self, vehicle): @asyncio.coroutine def get_vehicle_info(self, vehicle): """Fetch the latest vehicle info from automatic.""" - import aioautomatic name = vehicle.display_name if name is None: From 15820c6751d53df6d1e2c775cf670addfa11dba2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Oct 2019 21:53:15 +0200 Subject: [PATCH 0795/3953] Add device condition support to the lock integration (#27488) --- .../components/lock/device_condition.py | 79 +++++++++++ homeassistant/components/lock/strings.json | 8 ++ .../components/lock/test_device_condition.py | 126 ++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 homeassistant/components/lock/device_condition.py create mode 100644 homeassistant/components/lock/strings.json create mode 100644 tests/components/lock/test_device_condition.py diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py new file mode 100644 index 00000000000000..328da6ad450e1c --- /dev/null +++ b/homeassistant/components/lock/device_condition.py @@ -0,0 +1,79 @@ +"""Provides device automations for Lock.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_LOCKED, + STATE_UNLOCKED, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +CONDITION_TYPES = {"is_locked", "is_unlocked"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions for Lock devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add conditions for each entity that belongs to this integration + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_locked", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_unlocked", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_locked": + state = STATE_LOCKED + else: + state = STATE_UNLOCKED + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json new file mode 100644 index 00000000000000..baa9bc1604fd10 --- /dev/null +++ b/homeassistant/components/lock/strings.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} is locked", + "is_unlocked": "{entity_name} is unlocked" + } + } +} diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py new file mode 100644 index 00000000000000..675f402e770cb5 --- /dev/null +++ b/tests/components/lock/test_device_condition.py @@ -0,0 +1,126 @@ +"""The tests for Lock device conditions.""" +import pytest + +from homeassistant.components.lock import DOMAIN +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a lock.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_locked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_unlocked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("lock.entity", STATE_LOCKED) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "is_locked", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_locked - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "is_unlocked", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_unlocked - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_locked - event - test_event1" + + hass.states.async_set("lock.entity", STATE_UNLOCKED) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_unlocked - event - test_event2" From 5198f522c7137c6d7361935edbfbc6d07c191c6b Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:53:43 +0200 Subject: [PATCH 0796/3953] moved imports to top level (#27483) --- homeassistant/components/aquostv/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index 016db478fc9cfd..d8770592c9fdca 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -1,6 +1,8 @@ """Support for interface with an Aquos TV.""" import logging +import sharp_aquos_rc + import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -77,7 +79,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Sharp Aquos TV platform.""" - import sharp_aquos_rc name = config.get(CONF_NAME) port = config.get(CONF_PORT) From 54d63c63c30488a2340c204bd2ae18d9be06faf5 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:54:16 +0200 Subject: [PATCH 0797/3953] Move imports in uscis component (#27481) --- homeassistant/components/uscis/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index bda6ad9041bc12..3f5175ad09d6ca 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -1,7 +1,8 @@ """Support for USCIS Case Status.""" - import logging from datetime import timedelta + +import uscisstatus import voluptuous as vol from homeassistant.helpers.entity import Entity @@ -67,8 +68,6 @@ def device_state_attributes(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Fetch data from the USCIS website and update state attributes.""" - import uscisstatus - try: status = uscisstatus.get_case_status(self._case_id) self._attributes = {self.CURRENT_STATUS: status["status"]} From 0a2ec30ce37227e111cf78817cca9b4b680f71c5 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:54:41 +0200 Subject: [PATCH 0798/3953] Move imports in vasttrafik component (#27480) --- homeassistant/components/vasttrafik/sensor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 0da730165fedcd..d13383a083265e 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import vasttrafik import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -54,7 +55,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the departure sensor.""" - import vasttrafik planner = vasttrafik.JournyPlanner(config.get(CONF_KEY), config.get(CONF_SECRET)) sensors = [] @@ -62,7 +62,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for departure in config.get(CONF_DEPARTURES): sensors.append( VasttrafikDepartureSensor( - vasttrafik, planner, departure.get(CONF_NAME), departure.get(CONF_FROM), @@ -77,9 +76,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class VasttrafikDepartureSensor(Entity): """Implementation of a Vasttrafik Departure Sensor.""" - def __init__(self, vasttrafik, planner, name, departure, heading, lines, delay): + def __init__(self, planner, name, departure, heading, lines, delay): """Initialize the sensor.""" - self._vasttrafik = vasttrafik self._planner = planner self._name = name or departure self._departure = planner.location_name(departure)[0] @@ -119,7 +117,7 @@ def update(self): direction=self._heading["id"] if self._heading else None, date=now() + self._delay, ) - except self._vasttrafik.Error: + except vasttrafik.Error: _LOGGER.debug("Unable to read departure board, updating token") self._planner.update_token() From c5b12d6006fbb10220a636b8e5d1826e60598071 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:54:55 +0200 Subject: [PATCH 0799/3953] Move imports in venstar component (#27478) --- homeassistant/components/venstar/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 7be31d56c08275..81afef975412f3 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -1,6 +1,7 @@ """Support for Venstar WiFi Thermostats.""" import logging +from venstarcolortouch import VenstarColorTouch import voluptuous as vol from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA @@ -71,7 +72,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Venstar thermostat.""" - import venstarcolortouch username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -84,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: proto = "http" - client = venstarcolortouch.VenstarColorTouch( + client = VenstarColorTouch( addr=host, timeout=timeout, user=username, password=password, proto=proto ) From eaf855286bfb0079d04b9c9ff5dc72f4786c0c78 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:55:33 +0200 Subject: [PATCH 0800/3953] Move imports in verisure component (#27476) --- homeassistant/components/verisure/__init__.py | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index df95ed07ca5726..f4313c7c1acfa5 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -3,6 +3,8 @@ import threading from datetime import timedelta +from jsonpath import jsonpath +import verisure import voluptuous as vol from homeassistant.const import ( @@ -71,10 +73,8 @@ def setup(hass, config): """Set up the Verisure component.""" - import verisure - global HUB - HUB = VerisureHub(config[DOMAIN], verisure) + HUB = VerisureHub(config[DOMAIN]) HUB.update_overview = Throttle(config[DOMAIN][CONF_SCAN_INTERVAL])( HUB.update_overview ) @@ -109,13 +109,12 @@ def capture_smartcam(service): class VerisureHub: """A Verisure hub wrapper class.""" - def __init__(self, domain_config, verisure): + def __init__(self, domain_config): """Initialize the Verisure hub.""" self.overview = {} self.imageseries = {} self.config = domain_config - self._verisure = verisure self._lock = threading.Lock() @@ -125,15 +124,11 @@ def __init__(self, domain_config, verisure): self.giid = domain_config.get(CONF_GIID) - import jsonpath - - self.jsonpath = jsonpath.jsonpath - def login(self): """Login to Verisure.""" try: self.session.login() - except self._verisure.Error as ex: + except verisure.Error as ex: _LOGGER.error("Could not log in to verisure, %s", ex) return False if self.giid: @@ -144,7 +139,7 @@ def logout(self): """Logout from Verisure.""" try: self.session.logout() - except self._verisure.Error as ex: + except verisure.Error as ex: _LOGGER.error("Could not log out from verisure, %s", ex) return False return True @@ -153,7 +148,7 @@ def set_giid(self): """Set installation GIID.""" try: self.session.set_giid(self.giid) - except self._verisure.Error as ex: + except verisure.Error as ex: _LOGGER.error("Could not set installation GIID, %s", ex) return False return True @@ -162,7 +157,7 @@ def update_overview(self): """Update the overview.""" try: self.overview = self.session.get_overview() - except self._verisure.ResponseError as ex: + except verisure.ResponseError as ex: _LOGGER.error("Could not read overview, %s", ex) if ex.status_code == 503: # Service unavailable _LOGGER.info("Trying to log in again") @@ -182,7 +177,7 @@ def smartcam_capture(self, device_id): def get(self, jpath, *args): """Get values from the overview that matches the jsonpath.""" - res = self.jsonpath(self.overview, jpath % args) + res = jsonpath(self.overview, jpath % args) return res if res else [] def get_first(self, jpath, *args): @@ -192,5 +187,5 @@ def get_first(self, jpath, *args): def get_image_info(self, jpath, *args): """Get values from the imageseries that matches the jsonpath.""" - res = self.jsonpath(self.imageseries, jpath % args) + res = jsonpath(self.imageseries, jpath % args) return res if res else [] From bac337889f993ea3d5bb75a52c3982060b434b7b Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:55:40 +0200 Subject: [PATCH 0801/3953] Move imports in vera component (#27477) --- homeassistant/components/vera/__init__.py | 3 +-- homeassistant/components/vera/sensor.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 5d9dd80061c39b..8fcc8a4a2fea1b 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -2,6 +2,7 @@ import logging from collections import defaultdict +import pyvera as veraApi import voluptuous as vol from requests.exceptions import RequestException @@ -65,7 +66,6 @@ def setup(hass, base_config): """Set up for Vera devices.""" - import pyvera as veraApi def stop_subscription(event): """Shutdown Vera subscriptions and subscription thread on exit.""" @@ -118,7 +118,6 @@ def stop_subscription(event): def map_vera_device(vera_device, remap): """Map vera classes to Home Assistant types.""" - import pyvera as veraApi if isinstance(vera_device, veraApi.VeraDimmer): return "light" diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index c33187cd90485e..e409a1238877d7 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import pyvera as veraApi + from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.entity import Entity @@ -44,7 +46,6 @@ def state(self): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - import pyvera as veraApi if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: return self._temperature_units @@ -59,7 +60,6 @@ def unit_of_measurement(self): def update(self): """Update the state.""" - import pyvera as veraApi if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: self.current_value = self.vera_device.temperature From 64f9ecbac9ce74f95c3bbf79b3754a7f925d80e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Oct 2019 12:56:10 -0700 Subject: [PATCH 0802/3953] Remove incorrect translation folder --- .../components/.translations/airly.ca.json | 22 ------------------- .../components/.translations/airly.da.json | 22 ------------------- .../components/.translations/airly.de.json | 18 --------------- .../components/.translations/airly.en.json | 22 ------------------- .../components/.translations/airly.es.json | 22 ------------------- .../components/.translations/airly.fr.json | 21 ------------------ .../components/.translations/airly.it.json | 22 ------------------- .../components/.translations/airly.lb.json | 22 ------------------- .../components/.translations/airly.nn.json | 10 --------- .../components/.translations/airly.no.json | 22 ------------------- .../components/.translations/airly.pl.json | 22 ------------------- .../components/.translations/airly.ru.json | 22 ------------------- .../components/.translations/airly.sl.json | 22 ------------------- .../.translations/airly.zh-Hant.json | 22 ------------------- 14 files changed, 291 deletions(-) delete mode 100644 homeassistant/components/.translations/airly.ca.json delete mode 100644 homeassistant/components/.translations/airly.da.json delete mode 100644 homeassistant/components/.translations/airly.de.json delete mode 100644 homeassistant/components/.translations/airly.en.json delete mode 100644 homeassistant/components/.translations/airly.es.json delete mode 100644 homeassistant/components/.translations/airly.fr.json delete mode 100644 homeassistant/components/.translations/airly.it.json delete mode 100644 homeassistant/components/.translations/airly.lb.json delete mode 100644 homeassistant/components/.translations/airly.nn.json delete mode 100644 homeassistant/components/.translations/airly.no.json delete mode 100644 homeassistant/components/.translations/airly.pl.json delete mode 100644 homeassistant/components/.translations/airly.ru.json delete mode 100644 homeassistant/components/.translations/airly.sl.json delete mode 100644 homeassistant/components/.translations/airly.zh-Hant.json diff --git a/homeassistant/components/.translations/airly.ca.json b/homeassistant/components/.translations/airly.ca.json deleted file mode 100644 index bf50b4f23e55b5..00000000000000 --- a/homeassistant/components/.translations/airly.ca.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "La clau API no \u00e9s correcta.", - "name_exists": "El nom ja existeix.", - "wrong_location": "No hi ha estacions de mesura Airly en aquesta zona." - }, - "step": { - "user": { - "data": { - "api_key": "Clau API d'Airly", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nom de la integraci\u00f3" - }, - "description": "Configura una integraci\u00f3 de qualitat d\u2019aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.da.json b/homeassistant/components/.translations/airly.da.json deleted file mode 100644 index 652cc46a7b3e0a..00000000000000 --- a/homeassistant/components/.translations/airly.da.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "API-n\u00f8glen er ikke korrekt.", - "name_exists": "Navnet findes allerede.", - "wrong_location": "Ingen Airly m\u00e5lestationer i dette omr\u00e5de." - }, - "step": { - "user": { - "data": { - "api_key": "Airly API-n\u00f8gle", - "latitude": "Breddegrad", - "longitude": "L\u00e6ngdegrad", - "name": "Integrationens navn" - }, - "description": "Konfigurer Airly luftkvalitet integration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.de.json b/homeassistant/components/.translations/airly.de.json deleted file mode 100644 index cb290dc46c087e..00000000000000 --- a/homeassistant/components/.translations/airly.de.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Name existiert bereits" - }, - "step": { - "user": { - "data": { - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad", - "name": "Name der Integration" - }, - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.en.json b/homeassistant/components/.translations/airly.en.json deleted file mode 100644 index 83284aaeb7b6e0..00000000000000 --- a/homeassistant/components/.translations/airly.en.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "API key is not correct.", - "name_exists": "Name already exists.", - "wrong_location": "No Airly measuring stations in this area." - }, - "step": { - "user": { - "data": { - "api_key": "Airly API key", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Name of the integration" - }, - "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.es.json b/homeassistant/components/.translations/airly.es.json deleted file mode 100644 index 0c29ad0bc668b9..00000000000000 --- a/homeassistant/components/.translations/airly.es.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "La clave de la API no es correcta.", - "name_exists": "El nombre ya existe.", - "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona." - }, - "step": { - "user": { - "data": { - "api_key": "Clave API de Airly", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nombre de la integraci\u00f3n" - }, - "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.fr.json b/homeassistant/components/.translations/airly.fr.json deleted file mode 100644 index cf756a9f4928b9..00000000000000 --- a/homeassistant/components/.translations/airly.fr.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "auth": "La cl\u00e9 API n'est pas correcte.", - "name_exists": "Le nom existe d\u00e9j\u00e0.", - "wrong_location": "Aucune station de mesure Airly dans cette zone." - }, - "step": { - "user": { - "data": { - "api_key": "Cl\u00e9 API Airly", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nom de l'int\u00e9gration" - }, - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.it.json b/homeassistant/components/.translations/airly.it.json deleted file mode 100644 index e50f618575bfd6..00000000000000 --- a/homeassistant/components/.translations/airly.it.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "La chiave API non \u00e8 corretta.", - "name_exists": "Il nome \u00e8 gi\u00e0 esistente", - "wrong_location": "Nessuna stazione di misurazione Airly in quest'area." - }, - "step": { - "user": { - "data": { - "api_key": "Chiave API Airly", - "latitude": "Latitudine", - "longitude": "Logitudine", - "name": "Nome dell'integrazione" - }, - "description": "Configurazione dell'integrazione della qualit\u00e0 dell'aria Airly. Per generare la chiave API andare su https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.lb.json b/homeassistant/components/.translations/airly.lb.json deleted file mode 100644 index 08aac57d162ae9..00000000000000 --- a/homeassistant/components/.translations/airly.lb.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "Api Schl\u00ebssel ass net korrekt.", - "name_exists": "Numm g\u00ebtt et schonn", - "wrong_location": "Keng Airly Moos Statioun an d\u00ebsem Ber\u00e4ich" - }, - "step": { - "user": { - "data": { - "api_key": "Airly API Schl\u00ebssel", - "latitude": "Breedegrad", - "longitude": "L\u00e4ngegrad", - "name": "Numm vun der Installatioun" - }, - "description": "Airly Loft Qualit\u00e9it Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle gitt op https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.nn.json b/homeassistant/components/.translations/airly.nn.json deleted file mode 100644 index 7e2f4f1ff6bcef..00000000000000 --- a/homeassistant/components/.translations/airly.nn.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.no.json b/homeassistant/components/.translations/airly.no.json deleted file mode 100644 index 70924bb7bf4b9e..00000000000000 --- a/homeassistant/components/.translations/airly.no.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "API-n\u00f8kkelen er ikke korrekt.", - "name_exists": "Navnet finnes allerede.", - "wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det." - }, - "step": { - "user": { - "data": { - "api_key": "Airly API-n\u00f8kkel", - "latitude": "Breddegrad", - "longitude": "Lengdegrad", - "name": "Navn p\u00e5 integrasjonen" - }, - "description": "Sett opp Airly luftkvalitet integrering. For \u00e5 generere API-n\u00f8kkel g\u00e5 til https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.pl.json b/homeassistant/components/.translations/airly.pl.json deleted file mode 100644 index 5d601b37591b68..00000000000000 --- a/homeassistant/components/.translations/airly.pl.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "Klucz API jest nieprawid\u0142owy.", - "name_exists": "Nazwa ju\u017c istnieje.", - "wrong_location": "Brak stacji pomiarowych Airly w tym rejonie." - }, - "step": { - "user": { - "data": { - "api_key": "Klucz API Airly", - "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna", - "name": "Nazwa integracji" - }, - "description": "Konfiguracja integracji Airly. By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.ru.json b/homeassistant/components/.translations/airly.ru.json deleted file mode 100644 index 36080c9f372e24..00000000000000 --- a/homeassistant/components/.translations/airly.ru.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", - "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", - "wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly." - }, - "step": { - "user": { - "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" - }, - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 Airly. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register.", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.sl.json b/homeassistant/components/.translations/airly.sl.json deleted file mode 100644 index 08f57d88bcba98..00000000000000 --- a/homeassistant/components/.translations/airly.sl.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "Klju\u010d API ni pravilen.", - "name_exists": "Ime \u017ee obstaja", - "wrong_location": "Na tem obmo\u010dju ni merilnih postaj Airly." - }, - "step": { - "user": { - "data": { - "api_key": "Airly API klju\u010d", - "latitude": "Zemljepisna \u0161irina", - "longitude": "Zemljepisna dol\u017eina", - "name": "Ime integracije" - }, - "description": "Nastavite Airly integracijo za kakovost zraka. \u010ce \u017eelite ustvariti API klju\u010d pojdite na https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.zh-Hant.json b/homeassistant/components/.translations/airly.zh-Hant.json deleted file mode 100644 index bb38d2b9b8c739..00000000000000 --- a/homeassistant/components/.translations/airly.zh-Hant.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002", - "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", - "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" - }, - "step": { - "user": { - "data": { - "api_key": "Airly API \u5bc6\u9470", - "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6", - "name": "\u6574\u5408\u540d\u7a31" - }, - "description": "\u6b32\u8a2d\u5b9a Airly \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://developer.airly.eu/register \u7522\u751f API \u5bc6\u9470", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file From 652bf540442700626d2ee24af2a46b7e3184f9a9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 12 Oct 2019 21:57:01 +0200 Subject: [PATCH 0803/3953] Fix update after network error (#27444) --- homeassistant/components/airly/air_quality.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index a8ec82ab3041ec..f8500869509edd 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -148,6 +148,9 @@ async def async_update(self): """Get the data from Airly.""" await self.airly.async_update() + if self.airly.data: + self.data = self.airly.data + self._pm_10 = self.data[ATTR_API_PM10] self._pm_2_5 = self.data[ATTR_API_PM25] self._aqi = self.data[ATTR_API_CAQI] From 17b1ba2e9fccc8d8c0e0bfdcdef06588803089db Mon Sep 17 00:00:00 2001 From: Paolo Tuninetto Date: Sat, 12 Oct 2019 21:57:18 +0200 Subject: [PATCH 0804/3953] Move AmazonPolly imports (#27443) --- homeassistant/components/amazon_polly/tts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 64b8b71457cd3f..3acfd472320023 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -1,5 +1,6 @@ """Support for the Amazon Polly text to speech service.""" import logging +import boto3 import voluptuous as vol @@ -156,8 +157,6 @@ def get_engine(hass, config): config[CONF_SAMPLE_RATE] = sample_rate - import boto3 - profile = config.get(CONF_PROFILE_NAME) if profile is not None: From 22e7cb11f4f423553ef9a7deea8e79de6dbe7d53 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Sat, 12 Oct 2019 21:58:52 +0200 Subject: [PATCH 0805/3953] Change persistent notification about dev-info panel (#27441) * there is no dev-info panel anymore * Update __init__.py * Update __init__.py --- homeassistant/components/hassio/__init__.py | 2 +- homeassistant/components/homeassistant/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6603728e0370fb..e0c0a57375a36b 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -269,7 +269,7 @@ async def async_handle_core_service(call): if errors: _LOGGER.error(errors) hass.components.persistent_notification.async_create( - "Config error. See dev-info panel for details.", + "Config error. See [the logs](/developer-tools/logs) for details.", "Config validating", f"{HASS_DOMAIN}.check_config", ) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 02e53d1de108e8..d2d6abdadb5f07 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -108,7 +108,7 @@ async def async_handle_core_service(call): if errors: _LOGGER.error(errors) hass.components.persistent_notification.async_create( - "Config error. See dev-info panel for details.", + "Config error. See [the logs](/developer-tools/logs) for details.", "Config validating", f"{ha.DOMAIN}.check_config", ) From 15f0fabe9d76489a54a7fd47ec719985517e04f4 Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Sat, 12 Oct 2019 21:59:36 +0200 Subject: [PATCH 0806/3953] Bump pysyncthru to 0.5.0 (#27439) --- 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 79e6f8e5571ce1..41ac1024a8570b 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/integrations/syncthru", "requirements": [ - "pysyncthru==0.4.3" + "pysyncthru==0.5.0" ], "dependencies": [], "codeowners": ["@nielstron"] diff --git a/requirements_all.txt b/requirements_all.txt index df024f34cdac0f..923c27fa241a8f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1465,7 +1465,7 @@ pysuez==0.1.17 pysupla==0.0.3 # homeassistant.components.syncthru -pysyncthru==0.4.3 +pysyncthru==0.5.0 # homeassistant.components.tautulli pytautulli==0.5.0 From 3b9ee9c9015c5a622b63787ef9fc378c1a1a875c Mon Sep 17 00:00:00 2001 From: quthla Date: Sat, 12 Oct 2019 22:00:48 +0200 Subject: [PATCH 0807/3953] Bump RtmAPI to 0.7.2 (#27433) * Bump RtmAPI to 0.7.2 * Bump RtmAPI to 0.7.2 * Bump RtmAPI to 0.7.2 --- homeassistant/components/remember_the_milk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index 4979fe29e0eb74..6ec9dc6f8f462c 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -3,7 +3,7 @@ "name": "Remember the milk", "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", "requirements": [ - "RtmAPI==0.7.0", + "RtmAPI==0.7.2", "httplib2==0.10.3" ], "dependencies": ["configurator"], diff --git a/requirements_all.txt b/requirements_all.txt index 923c27fa241a8f..15022ba161f5b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -85,7 +85,7 @@ PyXiaomiGateway==0.12.4 # RPi.GPIO==0.6.5 # homeassistant.components.remember_the_milk -RtmAPI==0.7.0 +RtmAPI==0.7.2 # homeassistant.components.travisci TravisPy==0.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f37ef15992ec9..143f219fecf6b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.2.9 PyTransportNSW==0.1.1 # homeassistant.components.remember_the_milk -RtmAPI==0.7.0 +RtmAPI==0.7.2 # homeassistant.components.yessssms YesssSMS==0.4.1 From 701bb666c4977a0949273b5de6ec442ae3ea47c6 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 22:01:35 +0200 Subject: [PATCH 0808/3953] Move imports in watson_iot component (#27448) --- homeassistant/components/watson_iot/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/watson_iot/__init__.py b/homeassistant/components/watson_iot/__init__.py index aef2cc8ccce274..adc05893fde15c 100644 --- a/homeassistant/components/watson_iot/__init__.py +++ b/homeassistant/components/watson_iot/__init__.py @@ -4,6 +4,8 @@ import threading import time +from ibmiotf import MissingMessageEncoderException +from ibmiotf.gateway import Client import voluptuous as vol from homeassistant.const import ( @@ -67,7 +69,6 @@ def setup(hass, config): """Set up the Watson IoT Platform component.""" - from ibmiotf import gateway conf = config[DOMAIN] @@ -85,7 +86,7 @@ def setup(hass, config): "auth-method": "token", "auth-token": conf[CONF_TOKEN], } - watson_gateway = gateway.Client(client_args) + watson_gateway = Client(client_args) def event_to_json(event): """Add an event to the outgoing list.""" @@ -190,7 +191,6 @@ def get_events_json(self): def write_to_watson(self, events): """Write preprocessed events to watson.""" - import ibmiotf for event in events: for retry in range(MAX_TRIES + 1): @@ -208,7 +208,7 @@ def write_to_watson(self, events): _LOGGER.error("Failed to publish message to Watson IoT") continue break - except (ibmiotf.MissingMessageEncoderException, IOError): + except (MissingMessageEncoderException, IOError): if retry < MAX_TRIES: time.sleep(RETRY_DELAY) else: From 8a1738281aff91aa4f10871f858c27a82b44da7c Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 22:02:12 +0200 Subject: [PATCH 0809/3953] moved imports to top level (#27454) --- homeassistant/components/abode/__init__.py | 9 ++++----- homeassistant/components/abode/binary_sensor.py | 5 +++-- homeassistant/components/abode/camera.py | 4 ++-- homeassistant/components/abode/cover.py | 3 ++- homeassistant/components/abode/light.py | 3 ++- homeassistant/components/abode/lock.py | 3 ++- homeassistant/components/abode/sensor.py | 3 ++- homeassistant/components/abode/switch.py | 5 +++-- 8 files changed, 20 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index f43cbc50f988af..b7f13d49b69c1f 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -2,6 +2,10 @@ import logging from functools import partial from requests.exceptions import HTTPError, ConnectTimeout +import abodepy +import abodepy.helpers.constants as CONST +from abodepy.exceptions import AbodeException +import abodepy.helpers.timeline as TIMELINE import voluptuous as vol @@ -98,7 +102,6 @@ class AbodeSystem: def __init__(self, username, password, cache, name, polling, exclude, lights): """Initialize the system.""" - import abodepy self.abode = abodepy.Abode( username, @@ -124,7 +127,6 @@ def is_automation_excluded(self, automation): def is_light(self, device): """Check if a switch device is configured as a light.""" - import abodepy.helpers.constants as CONST return device.generic_type == CONST.TYPE_LIGHT or ( device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights @@ -133,7 +135,6 @@ def is_light(self, device): def setup(hass, config): """Set up Abode component.""" - from abodepy.exceptions import AbodeException conf = config[DOMAIN] username = conf.get(CONF_USERNAME) @@ -172,7 +173,6 @@ def setup(hass, config): def setup_hass_services(hass): """Home assistant services.""" - from abodepy.exceptions import AbodeException def change_setting(call): """Change an Abode system setting.""" @@ -246,7 +246,6 @@ def logout(event): def setup_abode_events(hass): """Event callbacks.""" - import abodepy.helpers.timeline as TIMELINE def event_callback(event, event_json): """Handle an event callback from Abode.""" diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index e37f6a465a495f..3ae7f41d84ea34 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -1,6 +1,9 @@ """Support for Abode Security System binary sensors.""" import logging +import abodepy.helpers.constants as CONST +import abodepy.helpers.timeline as TIMELINE + from homeassistant.components.binary_sensor import BinarySensorDevice from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice @@ -10,8 +13,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a sensor for an Abode device.""" - import abodepy.helpers.constants as CONST - import abodepy.helpers.timeline as TIMELINE data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 95755a644e2d8b..f52bbe17475a32 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -3,6 +3,8 @@ import logging import requests +import abodepy.helpers.constants as CONST +import abodepy.helpers.timeline as TIMELINE from homeassistant.components.camera import Camera from homeassistant.util import Throttle @@ -16,8 +18,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Abode camera devices.""" - import abodepy.helpers.constants as CONST - import abodepy.helpers.timeline as TIMELINE data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 4c868daf4ba932..13d46c53f7360c 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -1,6 +1,8 @@ """Support for Abode Security System covers.""" import logging +import abodepy.helpers.constants as CONST + from homeassistant.components.cover import CoverDevice from . import DOMAIN as ABODE_DOMAIN, AbodeDevice @@ -10,7 +12,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Abode cover devices.""" - import abodepy.helpers.constants as CONST data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 8e6691560e5458..6551cba2ef112c 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -2,6 +2,8 @@ import logging from math import ceil +import abodepy.helpers.constants as CONST + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -23,7 +25,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Abode light devices.""" - import abodepy.helpers.constants as CONST data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index c1272a3de5f405..ff069751605e42 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -1,6 +1,8 @@ """Support for Abode Security System locks.""" import logging +import abodepy.helpers.constants as CONST + from homeassistant.components.lock import LockDevice from . import DOMAIN as ABODE_DOMAIN, AbodeDevice @@ -10,7 +12,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Abode lock devices.""" - import abodepy.helpers.constants as CONST data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index ba28eab79c7212..fca32b8dc43b64 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -1,6 +1,8 @@ """Support for Abode Security System sensors.""" import logging +import abodepy.helpers.constants as CONST + from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -21,7 +23,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a sensor for an Abode device.""" - import abodepy.helpers.constants as CONST data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 82a550df1a57bf..4192ebb4485f8e 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -1,6 +1,9 @@ """Support for Abode Security System switches.""" import logging +import abodepy.helpers.constants as CONST +import abodepy.helpers.timeline as TIMELINE + from homeassistant.components.switch import SwitchDevice from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice @@ -10,8 +13,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Abode switch devices.""" - import abodepy.helpers.constants as CONST - import abodepy.helpers.timeline as TIMELINE data = hass.data[ABODE_DOMAIN] From e9642a0f65dc45fe8dea9d503822a790cccfcbf9 Mon Sep 17 00:00:00 2001 From: quthla Date: Sat, 12 Oct 2019 22:02:20 +0200 Subject: [PATCH 0810/3953] Bump PyGithub to 1.43.8 (#27432) * Bump PyGithub to 1.43.8 * Bump PyGithub to 1.43.8 --- homeassistant/components/github/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 0b5e3c0df9f2bc..02593bf603d625 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -3,7 +3,7 @@ "name": "Github", "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ - "PyGithub==1.43.5" + "PyGithub==1.43.8" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 15022ba161f5b3..f5b4bf38da3270 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -47,7 +47,7 @@ OPi.GPIO==0.3.6 PyEssent==0.13 # homeassistant.components.github -PyGithub==1.43.5 +PyGithub==1.43.8 # homeassistant.components.isy994 PyISY==1.1.2 From 72b711fde62a80230e1eef09ad7c7523950e9eea Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 22:03:02 +0200 Subject: [PATCH 0811/3953] Move imports in w800rf32 component (#27451) --- homeassistant/components/w800rf32/__init__.py | 3 +-- homeassistant/components/w800rf32/binary_sensor.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/w800rf32/__init__.py b/homeassistant/components/w800rf32/__init__.py index 9f15e0b2aa1a21..805cca47023d42 100644 --- a/homeassistant/components/w800rf32/__init__.py +++ b/homeassistant/components/w800rf32/__init__.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import W800rf32 as w800 from homeassistant.const import ( CONF_DEVICE, @@ -26,8 +27,6 @@ def setup(hass, config): """Set up the w800rf32 component.""" - # Try to load the W800rf32 module. - import W800rf32 as w800 # Declare the Handle event def handle_receive(event): diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index f1f1890f7aa556..e08111da8ba08f 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import W800rf32 as w800 from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, @@ -104,9 +105,8 @@ def is_on(self): @callback def binary_sensor_update(self, event): """Call for control updates from the w800rf32 gateway.""" - import W800rf32 as w800rf32mod - if not isinstance(event, w800rf32mod.W800rf32Event): + if not isinstance(event, w800.W800rf32Event): return dev_id = event.device From 930b576685c3126b726dd51911e66a6f7f1e0a08 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 22:03:42 +0200 Subject: [PATCH 0812/3953] moved imports to top level (#27458) --- homeassistant/components/ads/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 1b4f11c7cc1613..ba4762da84a4ae 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -7,6 +7,8 @@ import asyncio import async_timeout +import pyads + import voluptuous as vol from homeassistant.const import ( @@ -78,7 +80,6 @@ def setup(hass, config): """Set up the ADS component.""" - import pyads conf = config[DOMAIN] @@ -161,7 +162,6 @@ def __init__(self, ads_client): def shutdown(self, *args, **kwargs): """Shutdown ADS connection.""" - import pyads _LOGGER.debug("Shutting down ADS") for notification_item in self._notification_items.values(): @@ -187,7 +187,6 @@ def register_device(self, device): def write_by_name(self, name, value, plc_datatype): """Write a value to the device.""" - import pyads with self._lock: try: @@ -197,7 +196,6 @@ def write_by_name(self, name, value, plc_datatype): def read_by_name(self, name, plc_datatype): """Read a value from the device.""" - import pyads with self._lock: try: @@ -207,7 +205,6 @@ def read_by_name(self, name, plc_datatype): def add_device_notification(self, name, plc_datatype, callback): """Add a notification to the ADS devices.""" - import pyads attr = pyads.NotificationAttrib(ctypes.sizeof(plc_datatype)) From 3b4e2572141e616dbe86b1dd70139ce8fa94dc54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sat, 12 Oct 2019 22:03:56 +0200 Subject: [PATCH 0813/3953] Move imports in dht component (#27459) * Move imports in dht component * remove empty line * Move imports for pushbullet component * revert unwanted changes --- homeassistant/components/dht/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index aadb6b2d4cbf92..648e0e1ed7267a 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import Adafruit_DHT # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -50,7 +51,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the DHT sensor.""" - import Adafruit_DHT # pylint: disable=import-error SENSOR_TYPES[SENSOR_TEMPERATURE][1] = hass.config.units.temperature_unit available_sensors = { From 08ccaac21fb8b6fc68339d0b43ebf3e4d2f18043 Mon Sep 17 00:00:00 2001 From: Paolo Tuninetto Date: Sat, 12 Oct 2019 22:04:14 +0200 Subject: [PATCH 0814/3953] Move Epson imports (#27457) --- .../components/epson/media_player.py | 68 ++++++++----------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 435ef582da8765..638f012ac7ab77 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -3,6 +3,30 @@ import voluptuous as vol +from epson_projector.const import ( + BACK, + BUSY, + CMODE, + CMODE_LIST, + CMODE_LIST_SET, + DEFAULT_SOURCES, + EPSON_CODES, + FAST, + INV_SOURCES, + MUTE, + PAUSE, + PLAY, + POWER, + SOURCE, + SOURCE_LIST, + TURN_ON, + TURN_OFF, + VOLUME, + VOL_DOWN, + VOL_UP, +) +import epson_projector as epson + from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( DOMAIN, @@ -61,8 +85,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Epson media player platform.""" - from epson_projector.const import CMODE_LIST_SET - if DATA_EPSON not in hass.data: hass.data[DATA_EPSON] = [] @@ -71,12 +93,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= port = config.get(CONF_PORT) ssl = config.get(CONF_SSL) - epson = EpsonProjector( + epson_proj = EpsonProjector( async_get_clientsession(hass, verify_ssl=False), name, host, port, ssl ) - hass.data[DATA_EPSON].append(epson) - async_add_entities([epson], update_before_add=True) + hass.data[DATA_EPSON].append(epson_proj) + async_add_entities([epson_proj], update_before_add=True) async def async_service_handler(service): """Handle for services.""" @@ -108,9 +130,6 @@ class EpsonProjector(MediaPlayerDevice): def __init__(self, websession, name, host, port, encryption): """Initialize entity to control Epson projector.""" - import epson_projector as epson - from epson_projector.const import DEFAULT_SOURCES - self._name = name self._projector = epson.Projector(host, websession=websession, port=port) self._cmode = None @@ -121,17 +140,6 @@ def __init__(self, websession, name, host, port, encryption): async def async_update(self): """Update state of device.""" - from epson_projector.const import ( - EPSON_CODES, - POWER, - CMODE, - CMODE_LIST, - SOURCE, - VOLUME, - BUSY, - SOURCE_LIST, - ) - is_turned_on = await self._projector.get_property(POWER) _LOGGER.debug("Project turn on/off status: %s", is_turned_on) if is_turned_on and is_turned_on == EPSON_CODES[POWER]: @@ -165,15 +173,11 @@ def supported_features(self): async def async_turn_on(self): """Turn on epson.""" - from epson_projector.const import TURN_ON - if self._state == STATE_OFF: await self._projector.send_command(TURN_ON) async def async_turn_off(self): """Turn off epson.""" - from epson_projector.const import TURN_OFF - if self._state == STATE_ON: await self._projector.send_command(TURN_OFF) @@ -194,57 +198,39 @@ def volume_level(self): async def select_cmode(self, cmode): """Set color mode in Epson.""" - from epson_projector.const import CMODE_LIST_SET - await self._projector.send_command(CMODE_LIST_SET[cmode]) async def async_select_source(self, source): """Select input source.""" - from epson_projector.const import INV_SOURCES - selected_source = INV_SOURCES[source] await self._projector.send_command(selected_source) async def async_mute_volume(self, mute): """Mute (true) or unmute (false) sound.""" - from epson_projector.const import MUTE - await self._projector.send_command(MUTE) async def async_volume_up(self): """Increase volume.""" - from epson_projector.const import VOL_UP - await self._projector.send_command(VOL_UP) async def async_volume_down(self): """Decrease volume.""" - from epson_projector.const import VOL_DOWN - await self._projector.send_command(VOL_DOWN) async def async_media_play(self): """Play media via Epson.""" - from epson_projector.const import PLAY - await self._projector.send_command(PLAY) async def async_media_pause(self): """Pause media via Epson.""" - from epson_projector.const import PAUSE - await self._projector.send_command(PAUSE) async def async_media_next_track(self): """Skip to next.""" - from epson_projector.const import FAST - await self._projector.send_command(FAST) async def async_media_previous_track(self): """Skip to previous.""" - from epson_projector.const import BACK - await self._projector.send_command(BACK) @property From 88e54a4ce6095bf4ca78f64bdc2ce63b1d9bca03 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 22:04:35 +0200 Subject: [PATCH 0815/3953] moved imports to top level (#27468) --- homeassistant/components/anthemav/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index a033470e5c90ef..d472af6104ef26 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -1,6 +1,8 @@ """Support for Anthem Network Receivers and Processors.""" import logging +import anthemav + import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -46,7 +48,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up our socket to the AVR.""" - import anthemav host = config.get(CONF_HOST) port = config.get(CONF_PORT) From 3096e94343aa287059bcabcf48c4a5e87355706e Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 22:04:42 +0200 Subject: [PATCH 0816/3953] moved imports to top level (#27469) --- homeassistant/components/aprs/device_tracker.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index 86b0b6f48afd65..0d23cedb4eeef9 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -3,6 +3,11 @@ import logging import threading +import geopy.distance +import aprslib +from aprslib import ConnectionError as AprsConnectionError +from aprslib import LoginError + import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA @@ -59,7 +64,6 @@ def make_filter(callsigns: list) -> str: def gps_accuracy(gps, posambiguity: int) -> int: """Calculate the GPS accuracy based on APRS posambiguity.""" - import geopy.distance pos_a_map = {0: 0, 1: 1 / 600, 2: 1 / 60, 3: 1 / 6, 4: 1} if posambiguity in pos_a_map: @@ -115,8 +119,6 @@ def __init__( """Initialize the class.""" super().__init__() - import aprslib - self.callsign = callsign self.host = host self.start_event = threading.Event() @@ -138,8 +140,6 @@ def start_complete(self, success: bool, message: str): def run(self): """Connect to APRS and listen for data.""" self.ais.set_filter(self.server_filter) - from aprslib import ConnectionError as AprsConnectionError - from aprslib import LoginError try: _LOGGER.info( From 86da3fb334811ec8246da9e8423cdf1e6efe71c8 Mon Sep 17 00:00:00 2001 From: Jordan Speicher Date: Sat, 12 Oct 2019 15:05:01 -0500 Subject: [PATCH 0817/3953] Add mobile_app dependency on cloud (#27470) --- homeassistant/components/mobile_app/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 8c95ca4ad41e49..ab140b4148e435 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -7,6 +7,7 @@ "PyNaCl==1.3.0" ], "dependencies": [ + "cloud", "http", "webhook" ], From 6b92dbe20914e9e704aaa084eef55ed18eea6a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sat, 12 Oct 2019 22:05:36 +0200 Subject: [PATCH 0818/3953] Move imports for pushbullet component (#27460) * Move imports in dht component * remove empty line * Move imports for pushbullet component * revert unwanted changes * Move imports for pushbullet component * remove dht change from that branch * remove dht changes from this branch --- homeassistant/components/pushbullet/notify.py | 6 +++--- homeassistant/components/pushbullet/sensor.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py index 70738965340b00..76c1e14e5a5188 100644 --- a/homeassistant/components/pushbullet/notify.py +++ b/homeassistant/components/pushbullet/notify.py @@ -2,6 +2,9 @@ import logging import mimetypes +from pushbullet import PushBullet +from pushbullet import InvalidKeyError +from pushbullet import PushError import voluptuous as vol from homeassistant.const import CONF_API_KEY @@ -28,8 +31,6 @@ def get_service(hass, config, discovery_info=None): """Get the Pushbullet notification service.""" - from pushbullet import PushBullet - from pushbullet import InvalidKeyError try: pushbullet = PushBullet(config[CONF_API_KEY]) @@ -124,7 +125,6 @@ def send_message(self, message=None, **kwargs): def _push_data(self, message, title, data, pusher, email=None): """Create the message content.""" - from pushbullet import PushError if data is None: data = {} diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 3ed53fb01f64c8..600b38b6eaf4d4 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -1,6 +1,10 @@ """Pushbullet platform for sensor component.""" import logging +import threading +from pushbullet import PushBullet +from pushbullet import InvalidKeyError +from pushbullet import Listener import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS @@ -35,8 +39,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Pushbullet Sensor platform.""" - from pushbullet import PushBullet - from pushbullet import InvalidKeyError try: pushbullet = PushBullet(config.get(CONF_API_KEY)) @@ -95,7 +97,6 @@ class PushBulletNotificationProvider: def __init__(self, pb): """Start to retrieve pushes from the given Pushbullet instance.""" - import threading self.pushbullet = pb self._data = None @@ -123,7 +124,6 @@ def retrieve_pushes(self): Spawn a new Listener and links it to self.on_push. """ - from pushbullet import Listener self.listener = Listener(account=self.pushbullet, on_push=self.on_push) _LOGGER.debug("Getting pushes") From 1f7bd4235cbae607950e6f4f9641b969b441ab17 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 22:07:54 +0200 Subject: [PATCH 0819/3953] Add test to Homematic IP Cloud switch (#27532) --- .../homematicip_cloud/test_switch.py | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 tests/components/homematicip_cloud/test_switch.py diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py new file mode 100644 index 00000000000000..15eaf6da04c88a --- /dev/null +++ b/tests/components/homematicip_cloud/test_switch.py @@ -0,0 +1,156 @@ +"""Tests for HomematicIP Cloud switch.""" +from homeassistant.components.homematicip_cloud.device import ( + ATTR_GROUP_MEMBER_UNREACHABLE, +) +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH +from homeassistant.const import STATE_OFF, STATE_ON + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_switch(hass, default_mock_hap): + """Test HomematicipSwitch.""" + entity_id = "switch.schrank" + entity_name = "Schrank" + device_model = "HMIP-PS" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_switch_measuring(hass, default_mock_hap): + """Test HomematicipSwitchMeasuring.""" + entity_id = "switch.pc" + entity_name = "Pc" + device_model = "HMIP-PSM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_CURRENT_POWER_W] == 50 + assert ha_state.attributes[ATTR_TODAY_ENERGY_KWH] == 36 + + await async_manipulate_test_data(hass, hmip_device, "energyCounter", None) + ha_state = hass.states.get(entity_id) + assert not ha_state.attributes.get(ATTR_TODAY_ENERGY_KWH) + + +async def test_hmip_group_switch(hass, default_mock_hap): + """Test HomematicipGroupSwitch.""" + entity_id = "switch.strom_group" + entity_name = "Strom Group" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + assert not ha_state.attributes.get(ATTR_GROUP_MEMBER_UNREACHABLE) + await async_manipulate_test_data(hass, hmip_device, "unreach", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] is True + + +async def test_hmip_multi_switch(hass, default_mock_hap): + """Test HomematicipMultiSwitch.""" + entity_id = "switch.jalousien_1_kizi_2_schlazi_channel1" + entity_name = "Jalousien - 1 KiZi, 2 SchlaZi Channel1" + device_model = "HmIP-PCBS2" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF From 96c0ad302f0ec6871b6443b25433c76091d965b4 Mon Sep 17 00:00:00 2001 From: Moritz Fey Date: Sat, 12 Oct 2019 22:08:45 +0200 Subject: [PATCH 0820/3953] add device conditions for platform cover (#27544) * add device condition support to the cover integration * remove TODO comment * add strings.json --- .../components/cover/device_condition.py | 103 ++++++++++ homeassistant/components/cover/strings.json | 10 + .../components/cover/test_device_condition.py | 190 ++++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 homeassistant/components/cover/device_condition.py create mode 100644 homeassistant/components/cover/strings.json create mode 100644 tests/components/cover/test_device_condition.py diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py new file mode 100644 index 00000000000000..129462047e402a --- /dev/null +++ b/homeassistant/components/cover/device_condition.py @@ -0,0 +1,103 @@ +"""Provides device automations for Cover.""" +from typing import Any, Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_OPEN, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +CONDITION_TYPES = {"is_open", "is_closed", "is_opening", "is_closing"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions for Cover devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions: List[Dict[str, Any]] = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add conditions for each entity that belongs to this integration + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_open", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_closed", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_opening", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_closing", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_open": + state = STATE_OPEN + elif config[CONF_TYPE] == "is_closed": + state = STATE_CLOSED + elif config[CONF_TYPE] == "is_opening": + state = STATE_OPENING + elif config[CONF_TYPE] == "is_closing": + state = STATE_CLOSING + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json new file mode 100644 index 00000000000000..db3ccf9119f3de --- /dev/null +++ b/homeassistant/components/cover/strings.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_open": "{entity_name} is open", + "is_closed": "{entity_name} is closed", + "is_opening": "{entity_name} is opening", + "is_closing": "{entity_name} is closing" + } + } +} \ No newline at end of file diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py new file mode 100644 index 00000000000000..494368f76ff218 --- /dev/null +++ b/tests/components/cover/test_device_condition.py @@ -0,0 +1,190 @@ +"""The tests for Cover device conditions.""" +import pytest + +from homeassistant.components.cover import DOMAIN +from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a cover.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_open", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("cover.entity", STATE_OPEN) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_open", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_open - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_closed", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_closed - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_opening", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_opening - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event4"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_closing", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_closing - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_open - event - test_event1" + + hass.states.async_set("cover.entity", STATE_CLOSED) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_closed - event - test_event2" + + hass.states.async_set("cover.entity", STATE_OPENING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "is_opening - event - test_event3" + + hass.states.async_set("cover.entity", STATE_CLOSING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event4") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "is_closing - event - test_event4" From 6c947f58b8f50a7decb0230946434fe0a3b5d42e Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Sat, 12 Oct 2019 16:11:30 -0400 Subject: [PATCH 0821/3953] Fix for unknown sensor state (#27542) --- homeassistant/components/ecobee/binary_sensor.py | 8 ++++++-- homeassistant/components/ecobee/sensor.py | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 3367f33a66ffa7..06289572aeaacb 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -105,6 +105,10 @@ async def async_update(self): """Get the latest state of the sensor.""" await self.data.update() for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue for item in sensor["capability"]: - if item["type"] == "occupancy" and self.sensor_name == sensor["name"]: - self._state = item["value"] + if item["type"] != "occupancy": + continue + self._state = item["value"] + break diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 1c47bc9b26db9c..76945080bfa685 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -112,7 +112,7 @@ def device_class(self): @property def state(self): """Return the state of the sensor.""" - if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN]: + if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN, "unknown"]: return None if self.type == "temperature": @@ -129,6 +129,10 @@ async def async_update(self): """Get the latest state of the sensor.""" await self.data.update() for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue for item in sensor["capability"]: - if item["type"] == self.type and self.sensor_name == sensor["name"]: - self._state = item["value"] + if item["type"] != self.type: + continue + self._state = item["value"] + break From 9e121b785a38b1b72888ef8b6014bdefc4c77d2a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Oct 2019 14:07:01 -0700 Subject: [PATCH 0822/3953] Google: catch query not supported (#27559) --- .../components/google_assistant/report_state.py | 12 +++++++++++- .../google_assistant/test_report_state.py | 17 +++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 869bc61d7a3624..b842a552714ef7 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,15 +1,21 @@ """Google Report State implementation.""" +import logging + from homeassistant.core import HomeAssistant, callback from homeassistant.const import MATCH_ALL from homeassistant.helpers.event import async_call_later from .helpers import AbstractConfig, GoogleEntity, async_get_entities +from .error import SmartHomeError # Time to wait until the homegraph updates # https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 INITIAL_REPORT_DELAY = 60 +_LOGGER = logging.getLogger(__name__) + + @callback def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig): """Enable state reporting.""" @@ -26,7 +32,11 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): if not entity.is_supported(): return - entity_data = entity.query_serialize() + try: + entity_data = entity.query_serialize() + except SmartHomeError as err: + _LOGGER.debug("Not reporting state for %s: %s", changed_entity, err.code) + return if old_state: old_entity = GoogleEntity(hass, google_config, old_state) diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index 734d9ec7fc83af..6ab88286a69593 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,7 +1,7 @@ """Test Google report state.""" from unittest.mock import patch -from homeassistant.components.google_assistant import report_state +from homeassistant.components.google_assistant import report_state, error from homeassistant.util.dt import utcnow from . import BASIC_CONFIG @@ -10,7 +10,7 @@ from tests.common import mock_coro, async_fire_time_changed -async def test_report_state(hass): +async def test_report_state(hass, caplog): """Test report state works.""" hass.states.async_set("light.ceiling", "off") hass.states.async_set("switch.ac", "on") @@ -57,6 +57,19 @@ async def test_report_state(hass): assert len(mock_report.mock_calls) == 0 + # Test that entities that we can't query don't report a state + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report, patch( + "homeassistant.components.google_assistant.report_state.GoogleEntity.query_serialize", + side_effect=error.SmartHomeError("mock-error", "mock-msg"), + ): + hass.states.async_set("light.kitchen", "off") + await hass.async_block_till_done() + + assert "Not reporting state for light.kitchen: mock-error" + assert len(mock_report.mock_calls) == 0 + unsub() with patch.object( From a82ff4f7a94fc6f2c635f9ecc260211654fcc42d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Oct 2019 14:09:06 -0700 Subject: [PATCH 0823/3953] Add strings for device automations to scaffold (#27556) --- script/scaffold/docs.py | 3 +++ script/scaffold/generate.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index ab87799d6b2ff4..bb119c0e42e4b3 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -38,6 +38,7 @@ def print_relevant_docs(template: str, info: Info) -> None: f""" Device trigger base has been added to the {info.domain} integration: - {info.integration_dir / "device_trigger.py"} + - {info.integration_dir / "strings.json"} (translations) - {info.tests_dir / "test_device_trigger.py"} You will now need to update the code to make sure that relevant triggers @@ -50,6 +51,7 @@ def print_relevant_docs(template: str, info: Info) -> None: f""" Device condition base has been added to the {info.domain} integration: - {info.integration_dir / "device_condition.py"} + - {info.integration_dir / "strings.json"} (translations) - {info.tests_dir / "test_device_condition.py"} You will now need to update the code to make sure that relevant condtions @@ -62,6 +64,7 @@ def print_relevant_docs(template: str, info: Info) -> None: f""" Device action base has been added to the {info.domain} integration: - {info.integration_dir / "device_action.py"} + - {info.integration_dir / "strings.json"} (translations) - {info.tests_dir / "test_device_action.py"} You will now need to update the code to make sure that relevant services diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 6bccf6529feeea..e16316fd76b129 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -68,6 +68,39 @@ def _custom_tasks(template, info) -> None: info.update_manifest(**changes) + if template == "device_trigger": + info.update_strings( + device_automation={ + **info.strings().get("device_automation", {}), + "trigger_type": { + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off", + }, + } + ) + + if template == "device_condition": + info.update_strings( + device_automation={ + **info.strings().get("device_automation", {}), + "condtion_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off", + }, + } + ) + + if template == "device_action": + info.update_strings( + device_automation={ + **info.strings().get("device_automation", {}), + "action_type": { + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}", + }, + } + ) + if template == "config_flow": info.update_manifest(config_flow=True) info.update_strings( From 62b886a5d58f8ce2e7b0fc53e197b70f25328cb1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 13 Oct 2019 00:31:39 +0000 Subject: [PATCH 0824/3953] [ci skip] Translation update --- .../binary_sensor/.translations/pl.json | 36 +++++++++--------- .../binary_sensor/.translations/ru.json | 18 +++++++++ .../binary_sensor/.translations/zh-Hant.json | 2 + .../components/cover/.translations/en.json | 10 +++++ .../components/cover/.translations/no.json | 10 +++++ .../components/deconz/.translations/pl.json | 32 ++++++++-------- .../components/light/.translations/pl.json | 4 +- .../components/lock/.translations/en.json | 8 ++++ .../components/lock/.translations/no.json | 8 ++++ .../components/switch/.translations/pl.json | 4 +- .../components/zha/.translations/pl.json | 38 +++++++++---------- 11 files changed, 113 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/cover/.translations/en.json create mode 100644 homeassistant/components/cover/.translations/no.json create mode 100644 homeassistant/components/lock/.translations/en.json create mode 100644 homeassistant/components/lock/.translations/no.json diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index a1ab03770f9995..bc474e3d514f08 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -45,15 +45,15 @@ "is_vibration": "sensor {entity_name} wykrywa wibracje" }, "trigger_type": { - "bat_low": "bateria {entity_name} stanie si\u0119 roz\u0142adowana", - "closed": "zamkni\u0119cie {entity_name}", + "bat_low": "nast\u0105pi roz\u0142adowanie baterii {entity_name}", + "closed": "nast\u0105pi zamkni\u0119cie {entity_name}", "cold": "sensor {entity_name} wykryje zimno", - "connected": "pod\u0142\u0105czenie {entity_name}", + "connected": "nast\u0105pi pod\u0142\u0105czenie {entity_name}", "gas": "sensor {entity_name} wykryje gaz", "hot": "sensor {entity_name} wykryje gor\u0105co", "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", - "locked": "zamkni\u0119cie {entity_name}", - "moist": "sensor {entity_name} wykry\u0142 wilgo\u0107", + "locked": "nast\u0105pi zamkni\u0119cie {entity_name}", + "moist": "nast\u0105pi wykrycie wilgoci {entity_name}", "moist\u00a7": "sensor {entity_name} wykryje wilgo\u0107", "motion": "sensor {entity_name} wykryje ruch", "moving": "sensor {entity_name} zacznie porusza\u0107 si\u0119", @@ -64,29 +64,29 @@ "no_smoke": "sensor {entity_name} przestanie wykrywa\u0107 dym", "no_sound": "sensor {entity_name} przestanie wykrywa\u0107 d\u017awi\u0119k", "no_vibration": "sensor {entity_name} przestanie wykrywa\u0107 wibracje", - "not_bat_low": "bateria {entity_name} staje si\u0119 na\u0142adowana", + "not_bat_low": "nast\u0105pi na\u0142adowanie baterii {entity_name}", "not_cold": "sensor {entity_name} przestanie wykrywa\u0107 zimno", - "not_connected": "roz\u0142\u0105czenie {entity_name}", + "not_connected": "nast\u0105pi roz\u0142\u0105czenie {entity_name}", "not_hot": "sensor {entity_name} przestanie wykrywa\u0107 gor\u0105co", - "not_locked": "otwarcie {entity_name}", + "not_locked": "nast\u0105pi otwarcie {entity_name}", "not_moist": "sensor {entity_name} przestanie wykrywa\u0107 wilgo\u0107", "not_moving": "sensor {entity_name} przestanie porusza\u0107 si\u0119", - "not_occupied": "sensor {entity_name} przesta\u0142 by\u0107 zaj\u0119ty", - "not_opened": "sensor {entity_name} zosta\u0142 zamkni\u0119ty", - "not_plugged_in": "od\u0142\u0105czenie {entity_name}", - "not_powered": "od\u0142\u0105czenie zasilania {entity_name}", + "not_occupied": "sensor {entity_name} przestanie by\u0107 zaj\u0119ty", + "not_opened": "nast\u0105pi zamkni\u0119cie {entity_name}", + "not_plugged_in": "nast\u0105pi od\u0142\u0105czenie {entity_name}", + "not_powered": "nast\u0105pi od\u0142\u0105czenie zasilania {entity_name}", "not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107", "not_unsafe": "sensor {entity_name} przestanie wykrywa\u0107 niebezpiecze\u0144stwo", - "occupied": "sensor {entity_name} sta\u0142 si\u0119 zaj\u0119ty", - "opened": "otwarcie {entity_name}", - "plugged_in": "pod\u0142\u0105czenie {entity_name}", - "powered": "pod\u0142\u0105czenie zasilenia {entity_name}", + "occupied": "sensor {entity_name} stanie si\u0119 zaj\u0119ty", + "opened": "nast\u0105pi otwarcie {entity_name}", + "plugged_in": "nast\u0105pi pod\u0142\u0105czenie {entity_name}", + "powered": "nast\u0105pi pod\u0142\u0105czenie zasilenia {entity_name}", "present": "sensor {entity_name} wykryje obecno\u015b\u0107", "problem": "sensor {entity_name} wykryje problem", "smoke": "sensor {entity_name} wykryje dym", "sound": "sensor {entity_name} wykryje d\u017awi\u0119k", - "turned_off": "wy\u0142\u0105czenie {entity_name}", - "turned_on": "w\u0142\u0105czenie {entity_name}", + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}", "unsafe": "sensor {entity_name} wykryje niebezpiecze\u0144stwo", "vibration": "sensor {entity_name} wykryje wibracje" } diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json index 012a5c4fa45d30..a7ecced3f11a05 100644 --- a/homeassistant/components/binary_sensor/.translations/ru.json +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -1,7 +1,11 @@ { "device_automation": { "condition_type": { + "is_bat_low": "{entity_name} \u0432 \u0440\u0430\u0437\u0440\u044f\u0436\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_cold": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", + "is_connected": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", "is_gas": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437", + "is_hot": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432", "is_light": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442", "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_moist": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443", @@ -14,16 +18,30 @@ "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", "is_no_sound": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", "is_no_vibration": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", + "is_not_bat_low": "{entity_name} \u0432 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_not_cold": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", + "is_not_connected": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_not_hot": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432", "is_not_locked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_not_moist": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443", "is_not_moving": "{entity_name} \u043d\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "is_not_occupied": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_not_plugged_in": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", + "is_not_present": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_occupied": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_plugged_in": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", + "is_present": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", "is_sound": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_vibration": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" }, "trigger_type": { diff --git a/homeassistant/components/binary_sensor/.translations/zh-Hant.json b/homeassistant/components/binary_sensor/.translations/zh-Hant.json index 36c72dcb9e69b4..046b999cb8c08c 100644 --- a/homeassistant/components/binary_sensor/.translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/.translations/zh-Hant.json @@ -53,6 +53,7 @@ "hot": "{entity_name} \u5df2\u8b8a\u71b1", "light": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", "locked": "{entity_name} \u5df2\u4e0a\u9396", + "moist": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5", "moist\u00a7": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5", "motion": "{entity_name} \u5df2\u5075\u6e2c\u5230\u52d5\u4f5c", "moving": "{entity_name} \u958b\u59cb\u79fb\u52d5", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} \u5df2\u8b8a\u4e7e", "not_moving": "{entity_name} \u505c\u6b62\u79fb\u52d5", "not_occupied": "{entity_name} \u672a\u6709\u4eba", + "not_opened": "{entity_name} \u5df2\u95dc\u9589", "not_plugged_in": "{entity_name} \u672a\u63d2\u5165", "not_powered": "{entity_name} \u672a\u901a\u96fb", "not_present": "{entity_name} \u672a\u51fa\u73fe", diff --git a/homeassistant/components/cover/.translations/en.json b/homeassistant/components/cover/.translations/en.json new file mode 100644 index 00000000000000..f9f47be3104173 --- /dev/null +++ b/homeassistant/components/cover/.translations/en.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} is closed", + "is_closing": "{entity_name} is closing", + "is_open": "{entity_name} is open", + "is_opening": "{entity_name} is opening" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/no.json b/homeassistant/components/cover/.translations/no.json new file mode 100644 index 00000000000000..ff37aa27d586f4 --- /dev/null +++ b/homeassistant/components/cover/.translations/no.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} er lukket", + "is_closing": "{entity_name} lukker", + "is_open": "{entity_name} er \u00e5pen", + "is_opening": "{entity_name} \u00e5pnes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 498c8dd18d6d39..6d36d6ab39dea2 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -48,27 +48,27 @@ "button_2": "drugi przycisk", "button_3": "trzeci przycisk", "button_4": "czwarty przycisk", - "close": "zamkni\u0119cie", - "dim_down": "zmniejszenie jasno\u015bci", - "dim_up": "zwi\u0119kszenie jasno\u015bci", + "close": "nast\u0105pi zamkni\u0119cie", + "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci", + "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci", "left": "w lewo", "open": "otwarcie", "right": "w prawo", - "turn_off": "wy\u0142\u0105czenie", - "turn_on": "wy\u0142\u0105czenie" + "turn_off": "nast\u0105pi wy\u0142\u0105czenie", + "turn_on": "nast\u0105pi w\u0142\u0105czenie" }, "trigger_type": { - "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_rotated": "przycisk obr\u00f3cony \"{subtype}\"", - "remote_button_rotation_stopped": "obr\u00f3t przycisku \"{subtype}\" zatrzymany", - "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", - "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", - "remote_gyro_activated": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" + "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_rotated": "przycisk zostanie obr\u00f3cony \"{subtype}\"", + "remote_button_rotation_stopped": "nast\u0105pi zatrzymanie obrotu przycisku \"{subtype}\"", + "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", + "remote_gyro_activated": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" } }, "options": { diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 33a38fc930e403..05589210dba762 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -10,8 +10,8 @@ "is_on": "\u015bwiat\u0142o {entity_name} jest w\u0142\u0105czone" }, "trigger_type": { - "turned_off": "wy\u0142\u0105czenie {entity_name}", - "turned_on": "w\u0142\u0105czenie {entity_name}" + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/en.json b/homeassistant/components/lock/.translations/en.json new file mode 100644 index 00000000000000..a4b69197a91f2e --- /dev/null +++ b/homeassistant/components/lock/.translations/en.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} is locked", + "is_unlocked": "{entity_name} is unlocked" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/no.json b/homeassistant/components/lock/.translations/no.json new file mode 100644 index 00000000000000..c60fc52ce5394a --- /dev/null +++ b/homeassistant/components/lock/.translations/no.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} er l\u00e5st", + "is_unlocked": "{entity_name} er l\u00e5st opp" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 09b43f4100d660..3d352aa2b58d70 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -12,8 +12,8 @@ "turn_on": "prze\u0142\u0105cznik {entity_name} w\u0142\u0105czony" }, "trigger_type": { - "turned_off": "wy\u0142\u0105czenie {entity_name}", - "turned_on": "w\u0142\u0105czenie {entity_name}" + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json index 0e1b7028dbbd14..4189ea6d9befe9 100644 --- a/homeassistant/components/zha/.translations/pl.json +++ b/homeassistant/components/zha/.translations/pl.json @@ -30,9 +30,9 @@ "button_4": "czwarty przycisk", "button_5": "pi\u0105ty przycisk", "button_6": "sz\u00f3sty przycisk", - "close": "zamkni\u0119cie", - "dim_down": "zmniejszenie jasno\u015bci", - "dim_up": "zwi\u0119kszenie jasno\u015bci", + "close": "nast\u0105pi zamkni\u0119cie", + "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci", + "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci", "face_1": "z aktywowan\u0105 twarz\u0105 1", "face_2": "z aktywowan\u0105 twarz\u0105 2", "face_3": "z aktywowan\u0105 twarz\u0105 3", @@ -43,25 +43,25 @@ "left": "w lewo", "open": "otwarcie", "right": "w prawo", - "turn_off": "wy\u0142\u0105czenie", - "turn_on": "w\u0142\u0105czenie" + "turn_off": "nast\u0105pi wy\u0142\u0105czenie", + "turn_on": "nast\u0105pi w\u0142\u0105czenie" }, "trigger_type": { - "device_dropped": "upadek urz\u0105dzenia", - "device_flipped": "odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", - "device_knocked": "pukni\u0119cie urz\u0105dzenia \"{subtype}\"", - "device_rotated": "obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", - "device_shaken": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", - "device_slid": "przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", - "device_tilted": "przechylenie urz\u0105dzenia", - "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "device_dropped": "nast\u0105pi upadek urz\u0105dzenia", + "device_flipped": "nast\u0105pi odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_knocked": "nast\u0105pi pukni\u0119cie w urz\u0105dzenie \"{subtype}\"", + "device_rotated": "nast\u0105pi obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_shaken": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", + "device_slid": "nast\u0105pi przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_tilted": "nast\u0105pi przechylenie urz\u0105dzenia", + "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", - "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty" + "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } } } \ No newline at end of file From 1fa6d9887e5b62b7861e0bfbbe0cc1c15176257e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 04:59:31 +0200 Subject: [PATCH 0825/3953] Move imports in tts component (#27565) * move imports in tts components * fix: order of imports --- homeassistant/components/tts/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 3e7900502d6c37..2ce0e18bee5355 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -11,17 +11,18 @@ from typing import Optional from aiohttp import web +import mutagen import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as DOMAIN_MP, MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, ) -from homeassistant.components.media_player.const import DOMAIN as DOMAIN_MP -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, CONF_PLATFORM +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform @@ -29,7 +30,6 @@ 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__) @@ -433,7 +433,6 @@ def write_tags(filename, data, provider, message, language, options): Async friendly. """ - import mutagen data_bytes = io.BytesIO(data) data_bytes.name = filename From a8f43843bffc3b5a41803d0bf3c601352f41199d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Oct 2019 05:00:48 +0200 Subject: [PATCH 0826/3953] Filled services.yaml for browser integration (#27563) * Filled services.yaml for browser integration * Update services.yaml --- homeassistant/components/browser/services.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/browser/services.yaml b/homeassistant/components/browser/services.yaml index e69de29bb2d1d6..460def22dc1445 100644 --- a/homeassistant/components/browser/services.yaml +++ b/homeassistant/components/browser/services.yaml @@ -0,0 +1,6 @@ +browse_url: + description: Open a URL in the default browser on the host machine of Home Assistant. + fields: + url: + description: The URL to open. + example: "https://www.home-assistant.io" From 25bec13335889846844a6ebd3dadfb05bf7a3bf6 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Oct 2019 05:01:12 +0200 Subject: [PATCH 0827/3953] Filled services.yaml for logbook integration (#27560) --- homeassistant/components/logbook/services.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/logbook/services.yaml b/homeassistant/components/logbook/services.yaml index e69de29bb2d1d6..08c463feed2563 100644 --- a/homeassistant/components/logbook/services.yaml +++ b/homeassistant/components/logbook/services.yaml @@ -0,0 +1,15 @@ +log: + description: Create a custom entry in your logbook. + fields: + name: + description: Custom name for an entity, can be referenced with entity_id + example: "Kitchen" + message: + description: Message of the custom logbook entry + example: "is being used" + entity_id: + description: Entity to reference in custom logbook entry [Optional] + example: "light.kitchen" + domain: + description: Icon of domain to display in custom logbook entry [Optional] + example: "light" \ No newline at end of file From df646f5db100333534a45a7ab66e596171721f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 05:02:29 +0200 Subject: [PATCH 0828/3953] Move imports in tikteck component (#27568) * move imports in tikteck component * fix: order of imports --- homeassistant/components/tikteck/light.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py index 013e5276f49002..6c623f29f182f5 100644 --- a/homeassistant/components/tikteck/light.py +++ b/homeassistant/components/tikteck/light.py @@ -1,17 +1,18 @@ """Support for Tikteck lights.""" import logging +import tikteck import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -48,7 +49,6 @@ class TikteckLight(Light): def __init__(self, device): """Initialize the light.""" - import tikteck self._name = device["name"] self._address = device["address"] From 863cf9823b8972b8d480959b62e8ee3dedd95d0d Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Thu, 10 Oct 2019 11:22:36 -0700 Subject: [PATCH 0829/3953] Vangorra withings fix (#27404) * Fixing connection issues with withings API by switching to a maintained client codebase. * Updating requirements files. * Adding withings api to requirements script. * Using version of withings api with static version in setup.py. * Updating requirements files. --- homeassistant/components/withings/common.py | 14 +- .../components/withings/config_flow.py | 4 +- .../components/withings/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 3 - tests/components/withings/common.py | 12 +- tests/components/withings/conftest.py | 139 +++++++++--------- tests/components/withings/test_common.py | 20 +-- tests/components/withings/test_sensor.py | 44 +++--- 9 files changed, 127 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index f2be849cbc794b..9acca6f0cd68e6 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import re import time -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import TokenUpdated @@ -68,7 +68,9 @@ class WithingsDataManager: service_available = None - def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): + def __init__( + self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi + ): """Constructor.""" self._hass = hass self._api = api @@ -253,7 +255,7 @@ def create_withings_data_manager( """Set up the sensor config entry.""" entry_creds = entry.data.get(const.CREDENTIALS) or {} profile = entry.data[const.PROFILE] - credentials = nokia.NokiaCredentials( + credentials = withings.WithingsCredentials( entry_creds.get("access_token"), entry_creds.get("token_expiry"), entry_creds.get("token_type"), @@ -266,7 +268,7 @@ def create_withings_data_manager( def credentials_saver(credentials_param): _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - # Sanitizing the data as sometimes a NokiaCredentials object + # Sanitizing the data as sometimes a WithingsCredentials object # is passed through from the API. cred_data = credentials_param if not isinstance(credentials_param, dict): @@ -275,8 +277,8 @@ def credentials_saver(credentials_param): entry.data[const.CREDENTIALS] = cred_data hass.config_entries.async_update_entry(entry, data={**entry.data}) - _LOGGER.debug("Creating nokia api instance") - api = nokia.NokiaApi( + _LOGGER.debug("Creating withings api instance") + api = withings.WithingsApi( credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index f28a4f59d80195..c781e785f5e122 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ from typing import Optional import aiohttp -import nokia +import withings_api as withings import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -75,7 +75,7 @@ def get_auth_client(self, profile: str): profile, ) - return nokia.NokiaAuth( + return withings.WithingsAuth( client_id, client_secret, callback_uri, diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index d38b69f2248bcc..7c6e4ec044afaf 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "nokia==1.2.0" + "withings-api==2.0.0b8" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index e085782b178180..d04f8aa07cef5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,9 +865,6 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 @@ -1973,6 +1970,9 @@ websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 +# homeassistant.components.withings +withings-api==2.0.0b8 + # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 328bf6b614b8b7..f5d7301b0ed599 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,9 +228,6 @@ minio==4.0.9 # homeassistant.components.ssdp netdisco==2.6.0 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b8406c39711e57..f3839a1be5524c 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,7 +1,7 @@ """Common data for for the withings component tests.""" import time -import nokia +import withings_api as withings import homeassistant.components.withings.const as const @@ -92,7 +92,7 @@ def new_measure(type_str, value, unit): } -def nokia_sleep_response(states): +def withings_sleep_response(states): """Create a sleep response based on states.""" data = [] for state in states: @@ -104,10 +104,10 @@ def nokia_sleep_response(states): ) ) - return nokia.NokiaSleep(new_sleep_data("aa", data)) + return withings.WithingsSleep(new_sleep_data("aa", data)) -NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( +WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( { "updatetime": "", "timezone": "", @@ -174,7 +174,7 @@ def nokia_sleep_response(states): ) -NOKIA_SLEEP_RESPONSE = nokia_sleep_response( +WITHINGS_SLEEP_RESPONSE = withings_sleep_response( [ const.MEASURE_TYPE_SLEEP_STATE_AWAKE, const.MEASURE_TYPE_SLEEP_STATE_LIGHT, @@ -183,7 +183,7 @@ def nokia_sleep_response(states): ] ) -NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( +WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( { "series": [ new_sleep_summary( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 7cbe3dc1cd4cd0..0aa6af0d7c098a 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ from typing import Awaitable, Callable, List import asynctest -import nokia +import withings_api as withings import pytest import homeassistant.components.api as api @@ -15,9 +15,9 @@ from homeassistant.setup import async_setup_component from .common import ( - NOKIA_MEASURES_RESPONSE, - NOKIA_SLEEP_RESPONSE, - NOKIA_SLEEP_SUMMARY_RESPONSE, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_SLEEP_RESPONSE, + WITHINGS_SLEEP_SUMMARY_RESPONSE, ) @@ -34,17 +34,17 @@ def __init__( measures: List[str] = None, unit_system: str = None, throttle_interval: int = const.THROTTLE_INTERVAL, - nokia_request_response="DATA", - nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, - nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, - nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, + withings_request_response="DATA", + withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, + withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, + withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, ) -> None: """Constructor.""" self._throttle_interval = throttle_interval - self._nokia_request_response = nokia_request_response - self._nokia_measures_response = nokia_measures_response - self._nokia_sleep_response = nokia_sleep_response - self._nokia_sleep_summary_response = nokia_sleep_summary_response + self._withings_request_response = withings_request_response + self._withings_measures_response = withings_measures_response + self._withings_sleep_response = withings_sleep_response + self._withings_sleep_summary_response = withings_sleep_summary_response self._withings_config = { const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret", @@ -103,24 +103,24 @@ def throttle_interval(self): return self._throttle_interval @property - def nokia_request_response(self): + def withings_request_response(self): """Request response.""" - return self._nokia_request_response + return self._withings_request_response @property - def nokia_measures_response(self) -> nokia.NokiaMeasures: + def withings_measures_response(self) -> withings.WithingsMeasures: """Measures response.""" - return self._nokia_measures_response + return self._withings_measures_response @property - def nokia_sleep_response(self) -> nokia.NokiaSleep: + def withings_sleep_response(self) -> withings.WithingsSleep: """Sleep response.""" - return self._nokia_sleep_response + return self._withings_sleep_response @property - def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: + def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: """Sleep summary response.""" - return self._nokia_sleep_summary_response + return self._withings_sleep_summary_response class WithingsFactoryData: @@ -130,21 +130,21 @@ def __init__( self, hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ): """Constructor.""" self._hass = hass self._flow_id = flow_id - self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock - self._nokia_api_request_mock = nokia_api_request_mock - self._nokia_api_get_measures_mock = nokia_api_get_measures_mock - self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock - self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock + self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock + self._withings_api_request_mock = withings_api_request_mock + self._withings_api_get_measures_mock = withings_api_get_measures_mock + self._withings_api_get_sleep_mock = withings_api_get_sleep_mock + self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock self._data_manager_get_throttle_interval_mock = ( data_manager_get_throttle_interval_mock ) @@ -160,29 +160,29 @@ def flow_id(self): return self._flow_id @property - def nokia_auth_get_credentials_mock(self): + def withings_auth_get_credentials_mock(self): """Get auth credentials mock.""" - return self._nokia_auth_get_credentials_mock + return self._withings_auth_get_credentials_mock @property - def nokia_api_request_mock(self): + def withings_api_request_mock(self): """Get request mock.""" - return self._nokia_api_request_mock + return self._withings_api_request_mock @property - def nokia_api_get_measures_mock(self): + def withings_api_get_measures_mock(self): """Get measures mock.""" - return self._nokia_api_get_measures_mock + return self._withings_api_get_measures_mock @property - def nokia_api_get_sleep_mock(self): + def withings_api_get_sleep_mock(self): """Get sleep mock.""" - return self._nokia_api_get_sleep_mock + return self._withings_api_get_sleep_mock @property - def nokia_api_get_sleep_summary_mock(self): + def withings_api_get_sleep_summary_mock(self): """Get sleep summary mock.""" - return self._nokia_api_get_sleep_summary_mock + return self._withings_api_get_sleep_summary_mock @property def data_manager_get_throttle_interval_mock(self): @@ -243,9 +243,9 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: assert await async_setup_component(hass, http.DOMAIN, config.hass_config) assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - nokia_auth_get_credentials_patch = asynctest.patch( - "nokia.NokiaAuth.get_credentials", - return_value=nokia.NokiaCredentials( + withings_auth_get_credentials_patch = asynctest.patch( + "withings_api.WithingsAuth.get_credentials", + return_value=withings.WithingsCredentials( access_token="my_access_token", token_expiry=time.time() + 600, token_type="my_token_type", @@ -255,28 +255,33 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: consumer_secret=config.withings_config.get(const.CLIENT_SECRET), ), ) - nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() + withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() - nokia_api_request_patch = asynctest.patch( - "nokia.NokiaApi.request", return_value=config.nokia_request_response + withings_api_request_patch = asynctest.patch( + "withings_api.WithingsApi.request", + return_value=config.withings_request_response, ) - nokia_api_request_mock = nokia_api_request_patch.start() + withings_api_request_mock = withings_api_request_patch.start() - nokia_api_get_measures_patch = asynctest.patch( - "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response + withings_api_get_measures_patch = asynctest.patch( + "withings_api.WithingsApi.get_measures", + return_value=config.withings_measures_response, ) - nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() + withings_api_get_measures_mock = withings_api_get_measures_patch.start() - nokia_api_get_sleep_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response + withings_api_get_sleep_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep", + return_value=config.withings_sleep_response, ) - nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() + withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() - nokia_api_get_sleep_summary_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep_summary", - return_value=config.nokia_sleep_summary_response, + withings_api_get_sleep_summary_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep_summary", + return_value=config.withings_sleep_summary_response, + ) + withings_api_get_sleep_summary_mock = ( + withings_api_get_sleep_summary_patch.start() ) - nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() data_manager_get_throttle_interval_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" @@ -295,11 +300,11 @@ async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: patches.extend( [ - nokia_auth_get_credentials_patch, - nokia_api_request_patch, - nokia_api_get_measures_patch, - nokia_api_get_sleep_patch, - nokia_api_get_sleep_summary_patch, + withings_auth_get_credentials_patch, + withings_api_request_patch, + withings_api_get_measures_patch, + withings_api_get_sleep_patch, + withings_api_get_sleep_summary_patch, data_manager_get_throttle_interval_patch, get_measures_patch, ] @@ -328,11 +333,11 @@ def create_task(*args): return WithingsFactoryData( hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index a22689f92bb6b5..9f2480f9094e55 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,6 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError import pytest from requests_oauthlib import TokenUpdated @@ -13,19 +13,19 @@ from homeassistant.exceptions import PlatformNotReady -@pytest.fixture(name="nokia_api") -def nokia_api_fixture(): - """Provide nokia api.""" - nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) - nokia_api.get_measures = MagicMock() - nokia_api.get_sleep = MagicMock() - return nokia_api +@pytest.fixture(name="withings_api") +def withings_api_fixture(): + """Provide withings api.""" + withings_api = withings.WithingsApi.__new__(withings.WithingsApi) + withings_api.get_measures = MagicMock() + withings_api.get_sleep = MagicMock() + return withings_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): +def data_manager_fixture(hass, withings_api: withings.WithingsApi): """Provide data manager.""" - return WithingsDataManager(hass, "My Profile", nokia_api) + return WithingsDataManager(hass, "My Profile", withings_api) def test_print_service(): diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index da77910097be89..697d0a8b86413f 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,7 +2,12 @@ from unittest.mock import MagicMock, patch import asynctest -from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary +from withings_api import ( + WithingsApi, + WithingsMeasures, + WithingsSleep, + WithingsSleepSummary, +) import pytest from homeassistant.components.withings import DOMAIN @@ -15,7 +20,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from .common import nokia_sleep_response +from .common import withings_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig @@ -120,9 +125,9 @@ async def test_health_sensor_state_none( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=None, - nokia_sleep_response=None, - nokia_sleep_summary_response=None, + withings_measures_response=None, + withings_sleep_response=None, + withings_sleep_summary_response=None, ) ) @@ -153,9 +158,9 @@ async def test_health_sensor_state_empty( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=NokiaMeasures({"measuregrps": []}), - nokia_sleep_response=NokiaSleep({"series": []}), - nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), + withings_measures_response=WithingsMeasures({"measuregrps": []}), + withings_sleep_response=WithingsSleep({"series": []}), + withings_sleep_summary_response=WithingsSleepSummary({"series": []}), ) ) @@ -201,7 +206,8 @@ async def test_sleep_state_throttled( data = await withings_factory( WithingsFactoryConfig( - measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) + measures=[measure], + withings_sleep_response=withings_sleep_response(sleep_states), ) ) @@ -257,16 +263,16 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): "expires_in": "2", } - original_nokia_api = NokiaApi - nokia_api_instance = None + original_withings_api = WithingsApi + withings_api_instance = None - def new_nokia_api(*args, **kwargs): - nonlocal nokia_api_instance - nokia_api_instance = original_nokia_api(*args, **kwargs) - nokia_api_instance.request = MagicMock() - return nokia_api_instance + def new_withings_api(*args, **kwargs): + nonlocal withings_api_instance + withings_api_instance = original_withings_api(*args, **kwargs) + withings_api_instance.request = MagicMock() + return withings_api_instance - nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) + withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( @@ -275,7 +281,7 @@ def new_nokia_api(*args, **kwargs): wraps=hass.config_entries.async_update_entry, ) - with session_patch, client_patch, nokia_api_patch, update_entry_patch: + with session_patch, client_patch, withings_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( @@ -298,7 +304,7 @@ def new_nokia_api(*args, **kwargs): await async_setup_entry(hass, config_entry, async_add_entities) - nokia_api_instance.set_token(expected_creds) + withings_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2" From 258f86801c9a43948659d2857069018b53a3d9ca Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 10 Oct 2019 19:30:30 +0300 Subject: [PATCH 0830/3953] Bump python-songpal (#27398) Fixes #24269 and fixes #26776 - potentially also #22116 --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 3160c4cee4b43a..2f0c44da47ba6a 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Songpal", "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ - "python-songpal==0.0.9.1" + "python-songpal==0.11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d04f8aa07cef5b..0446b66a21e876 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1555,7 +1555,7 @@ python-ripple-api==0.0.3 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.0.9.1 +python-songpal==0.11 # homeassistant.components.synologydsm python-synology==0.2.0 From e8e32e3ed411f345f691a3c827970ff9e918cd2b Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 10 Oct 2019 22:52:29 +0300 Subject: [PATCH 0831/3953] bump songpal to fix attrs usage when using its most recent version (#27410) --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 2f0c44da47ba6a..55b02b66a59bc4 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Songpal", "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ - "python-songpal==0.11" + "python-songpal==0.11.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0446b66a21e876..3707eeae99ede5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1555,7 +1555,7 @@ python-ripple-api==0.0.3 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.11 +python-songpal==0.11.1 # homeassistant.components.synologydsm python-synology==0.2.0 From 093ee7d5b271efe0336260da305d588c4ab8a06a Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Sat, 12 Oct 2019 06:17:02 -0700 Subject: [PATCH 0832/3953] iaqualink: set 5s timeout, use cookiejar defaults (#27426) --- homeassistant/components/iaqualink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index dec91186be2869..9ce0e04895f2e6 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -3,7 +3,7 @@ from functools import wraps import logging -from aiohttp import CookieJar +from aiohttp import ClientTimeout import voluptuous as vol from iaqualink import ( @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] switches = hass.data[DOMAIN][SWITCH_DOMAIN] = [] - session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) + session = async_create_clientsession(hass, timeout=ClientTimeout(total=5)) aqualink = AqualinkClient(username, password, session) try: await aqualink.login() From 0fd49e13da634b9d01490839edc20935637300be Mon Sep 17 00:00:00 2001 From: Jordan Speicher Date: Sat, 12 Oct 2019 15:05:01 -0500 Subject: [PATCH 0833/3953] Add mobile_app dependency on cloud (#27470) --- homeassistant/components/mobile_app/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 8c95ca4ad41e49..ab140b4148e435 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -7,6 +7,7 @@ "PyNaCl==1.3.0" ], "dependencies": [ + "cloud", "http", "webhook" ], From 134137dd1cbb74a0a57998e70e20d119473090a8 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Sat, 12 Oct 2019 16:11:30 -0400 Subject: [PATCH 0834/3953] Fix for unknown sensor state (#27542) --- homeassistant/components/ecobee/binary_sensor.py | 8 ++++++-- homeassistant/components/ecobee/sensor.py | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 7afdbae5a28fae..375146b96e8819 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -67,6 +67,10 @@ async def async_update(self): """Get the latest state of the sensor.""" await self.data.update() for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue for item in sensor["capability"]: - if item["type"] == "occupancy" and self.sensor_name == sensor["name"]: - self._state = item["value"] + if item["type"] != "occupancy": + continue + self._state = item["value"] + break diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 24ea3d281bc618..48a616a1d1f114 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -74,7 +74,7 @@ def device_class(self): @property def state(self): """Return the state of the sensor.""" - if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN]: + if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN, "unknown"]: return None if self.type == "temperature": @@ -91,6 +91,10 @@ async def async_update(self): """Get the latest state of the sensor.""" await self.data.update() for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue for item in sensor["capability"]: - if item["type"] == self.type and self.sensor_name == sensor["name"]: - self._state = item["value"] + if item["type"] != self.type: + continue + self._state = item["value"] + break From 422ba89c6d7e645446a2e791951e8e6e01af307b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Oct 2019 14:07:01 -0700 Subject: [PATCH 0835/3953] Google: catch query not supported (#27559) --- .../components/google_assistant/report_state.py | 12 +++++++++++- .../google_assistant/test_report_state.py | 17 +++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 869bc61d7a3624..b842a552714ef7 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,15 +1,21 @@ """Google Report State implementation.""" +import logging + from homeassistant.core import HomeAssistant, callback from homeassistant.const import MATCH_ALL from homeassistant.helpers.event import async_call_later from .helpers import AbstractConfig, GoogleEntity, async_get_entities +from .error import SmartHomeError # Time to wait until the homegraph updates # https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 INITIAL_REPORT_DELAY = 60 +_LOGGER = logging.getLogger(__name__) + + @callback def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig): """Enable state reporting.""" @@ -26,7 +32,11 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): if not entity.is_supported(): return - entity_data = entity.query_serialize() + try: + entity_data = entity.query_serialize() + except SmartHomeError as err: + _LOGGER.debug("Not reporting state for %s: %s", changed_entity, err.code) + return if old_state: old_entity = GoogleEntity(hass, google_config, old_state) diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index 734d9ec7fc83af..6ab88286a69593 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,7 +1,7 @@ """Test Google report state.""" from unittest.mock import patch -from homeassistant.components.google_assistant import report_state +from homeassistant.components.google_assistant import report_state, error from homeassistant.util.dt import utcnow from . import BASIC_CONFIG @@ -10,7 +10,7 @@ from tests.common import mock_coro, async_fire_time_changed -async def test_report_state(hass): +async def test_report_state(hass, caplog): """Test report state works.""" hass.states.async_set("light.ceiling", "off") hass.states.async_set("switch.ac", "on") @@ -57,6 +57,19 @@ async def test_report_state(hass): assert len(mock_report.mock_calls) == 0 + # Test that entities that we can't query don't report a state + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report, patch( + "homeassistant.components.google_assistant.report_state.GoogleEntity.query_serialize", + side_effect=error.SmartHomeError("mock-error", "mock-msg"), + ): + hass.states.async_set("light.kitchen", "off") + await hass.async_block_till_done() + + assert "Not reporting state for light.kitchen: mock-error" + assert len(mock_report.mock_calls) == 0 + unsub() with patch.object( From 17c3efa556bcd1d0248965096b8eea7d7eb7d7f5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Oct 2019 20:12:54 -0700 Subject: [PATCH 0836/3953] Bumped version to 0.100.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 05be5f77cb4472..5c47c5e85b2543 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 6cbc9d6abb3db99ca1db2521d0c8e725c54139bb Mon Sep 17 00:00:00 2001 From: chriscla Date: Sat, 12 Oct 2019 20:18:30 -0700 Subject: [PATCH 0837/3953] Fixing nzbget units display (#27521) --- homeassistant/components/nzbget/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index ce1fda0839e10b..20b49a492f3ae1 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -34,9 +34,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = discovery_info["client_name"] devices = [] - for sensor_type, sensor_config in SENSOR_TYPES.items(): + for sensor_config in SENSOR_TYPES.values(): new_sensor = NZBGetSensor( - nzbget_data, sensor_type, name, sensor_config[0], sensor_config[1] + nzbget_data, sensor_config[0], name, sensor_config[1], sensor_config[2] ) devices.append(new_sensor) @@ -50,8 +50,8 @@ def __init__( self, nzbget_data, sensor_type, client_name, sensor_name, unit_of_measurement ): """Initialize a new NZBGet sensor.""" - self._name = f"{client_name} {sensor_type}" - self.type = sensor_name + self._name = f"{client_name} {sensor_name}" + self.type = sensor_type self.client_name = client_name self.nzbget_data = nzbget_data self._state = None From 57e6dc3f7a7495b6e951da367f7b79c2dfd3ff9c Mon Sep 17 00:00:00 2001 From: chriscla Date: Sat, 12 Oct 2019 20:18:30 -0700 Subject: [PATCH 0838/3953] Fixing nzbget units display (#27521) --- homeassistant/components/nzbget/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index ce1fda0839e10b..20b49a492f3ae1 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -34,9 +34,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = discovery_info["client_name"] devices = [] - for sensor_type, sensor_config in SENSOR_TYPES.items(): + for sensor_config in SENSOR_TYPES.values(): new_sensor = NZBGetSensor( - nzbget_data, sensor_type, name, sensor_config[0], sensor_config[1] + nzbget_data, sensor_config[0], name, sensor_config[1], sensor_config[2] ) devices.append(new_sensor) @@ -50,8 +50,8 @@ def __init__( self, nzbget_data, sensor_type, client_name, sensor_name, unit_of_measurement ): """Initialize a new NZBGet sensor.""" - self._name = f"{client_name} {sensor_type}" - self.type = sensor_name + self._name = f"{client_name} {sensor_name}" + self.type = sensor_type self.client_name = client_name self.nzbget_data = nzbget_data self._state = None From 24a1139c1d5509ef6aa60e707aa451b5a8baab85 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Oct 2019 20:27:48 -0700 Subject: [PATCH 0839/3953] Update reqs --- requirements_test_all.txt | 3 +++ script/gen_requirements_all.py | 1 + 2 files changed, 4 insertions(+) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5d7301b0ed599..22a69139b7a732 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -445,6 +445,9 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==1.1.6 +# homeassistant.components.withings +withings-api==2.0.0b8 + # homeassistant.components.zeroconf zeroconf==0.23.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ab2f5e51db42e5..b1ad5240d6824e 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -185,6 +185,7 @@ "YesssSMS", "zeroconf", "zigpy-homeassistant", + "withings-api", ) IGNORE_PIN = ("colorlog>2.1,<3", "keyring>=9.3,<10.0", "urllib3") From 7aae1065256848677ff7f9a54a99a484d9b7594c Mon Sep 17 00:00:00 2001 From: foxy82 Date: Sun, 13 Oct 2019 09:59:35 +0100 Subject: [PATCH 0840/3953] Fix pioneer volume when using onkyo component (#27218) * Fix Onkyo when using pioneer AV receiver so it can use max volume of 164 * Update media_player.py Change to make receiver max volume configurable * Update manifest.json Update to latest onkyo-eiscp with a fix required for Pionner AVR * Fix Onkyo when using pioneer AV receiver so it can use max volume of 164 * Fix Onkyo when using pioneer AV receiver so it can use max volume of 164 * Format * Requirements all * Fix CI errors * Black --- homeassistant/components/onkyo/manifest.json | 2 +- .../components/onkyo/media_player.py | 71 +++++++++++++++---- requirements_all.txt | 2 +- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index 5bb116dece8efa..ef28c14a8b0582 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -3,7 +3,7 @@ "name": "Onkyo", "documentation": "https://www.home-assistant.io/integrations/onkyo", "requirements": [ - "onkyo-eiscp==1.2.4" + "onkyo-eiscp==1.2.7" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index d6117283da7ac2..86f0f418c3f674 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -31,9 +31,11 @@ CONF_SOURCES = "sources" CONF_MAX_VOLUME = "max_volume" +CONF_RECEIVER_MAX_VOLUME = "receiver_max_volume" DEFAULT_NAME = "Onkyo Receiver" -SUPPORTED_MAX_VOLUME = 80 +SUPPORTED_MAX_VOLUME = 100 +DEFAULT_RECEIVER_MAX_VOLUME = 80 SUPPORT_ONKYO = ( SUPPORT_VOLUME_SET @@ -77,8 +79,11 @@ vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MAX_VOLUME, default=SUPPORTED_MAX_VOLUME): vol.All( - vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME) + vol.Coerce(int), vol.Range(min=1, max=100) ), + vol.Optional( + CONF_RECEIVER_MAX_VOLUME, default=DEFAULT_RECEIVER_MAX_VOLUME + ): vol.All(vol.Coerce(int), vol.Range(min=0)), vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string}, } ) @@ -163,6 +168,7 @@ def service_handle(service): config.get(CONF_SOURCES), name=config.get(CONF_NAME), max_volume=config.get(CONF_MAX_VOLUME), + receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), ) ) KNOWN_HOSTS.append(host) @@ -178,6 +184,8 @@ def service_handle(service): receiver, config.get(CONF_SOURCES), name=f"{config[CONF_NAME]} Zone 2", + max_volume=config.get(CONF_MAX_VOLUME), + receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), ) ) # Add Zone3 if available @@ -189,6 +197,8 @@ def service_handle(service): receiver, config.get(CONF_SOURCES), name=f"{config[CONF_NAME]} Zone 3", + max_volume=config.get(CONF_MAX_VOLUME), + receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), ) ) except OSError: @@ -204,7 +214,14 @@ def service_handle(service): class OnkyoDevice(MediaPlayerDevice): """Representation of an Onkyo device.""" - def __init__(self, receiver, sources, name=None, max_volume=SUPPORTED_MAX_VOLUME): + def __init__( + self, + receiver, + sources, + name=None, + max_volume=SUPPORTED_MAX_VOLUME, + receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME, + ): """Initialize the Onkyo Receiver.""" self._receiver = receiver self._muted = False @@ -214,6 +231,7 @@ def __init__(self, receiver, sources, name=None, max_volume=SUPPORTED_MAX_VOLUME name or f"{receiver.info['model_name']}_{receiver.info['identifier']}" ) self._max_volume = max_volume + self._receiver_max_volume = receiver_max_volume self._current_source = None self._source_list = list(sources.values()) self._source_mapping = sources @@ -270,7 +288,10 @@ def update(self): del self._attributes[ATTR_PRESET] self._muted = bool(mute_raw[1] == "on") - self._volume = volume_raw[1] / self._max_volume + # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100) + self._volume = ( + volume_raw[1] / self._receiver_max_volume * (self._max_volume / 100) + ) if not hdmi_out_raw: return @@ -324,10 +345,15 @@ def set_volume_level(self, volume): """ Set volume level, input is range 0..1. - Onkyo ranges from 1-80 however 80 is usually far too loud - so allow the user to specify the upper range with CONF_MAX_VOLUME + However full volume on the amp is usually far too loud so allow the user to specify the upper range + with CONF_MAX_VOLUME. we change as per max_volume set by user. This means that if max volume is 80 then full + volume in HA will give 80% volume on the receiver. Then we convert + that to the correct scale for the receiver. """ - self.command(f"volume {int(volume * self._max_volume)}") + # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL + self.command( + f"volume {int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" + ) def volume_up(self): """Increase volume by 1 step.""" @@ -368,11 +394,19 @@ def select_output(self, output): class OnkyoDeviceZone(OnkyoDevice): """Representation of an Onkyo device's extra zone.""" - def __init__(self, zone, receiver, sources, name=None): + def __init__( + self, + zone, + receiver, + sources, + name=None, + max_volume=SUPPORTED_MAX_VOLUME, + receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME, + ): """Initialize the Zone with the zone identifier.""" self._zone = zone self._supports_volume = True - super().__init__(receiver, sources, name) + super().__init__(receiver, sources, name, max_volume, receiver_max_volume) def update(self): """Get the latest state from the device.""" @@ -419,7 +453,10 @@ def update(self): elif ATTR_PRESET in self._attributes: del self._attributes[ATTR_PRESET] if self._supports_volume: - self._volume = volume_raw[1] / 80.0 + # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100) + self._volume = ( + volume_raw[1] / self._receiver_max_volume * (self._max_volume / 100) + ) @property def supported_features(self): @@ -433,8 +470,18 @@ def turn_off(self): self.command(f"zone{self._zone}.power=standby") def set_volume_level(self, volume): - """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" - self.command(f"zone{self._zone}.volume={int(volume * 80)}") + """ + Set volume level, input is range 0..1. + + However full volume on the amp is usually far too loud so allow the user to specify the upper range + with CONF_MAX_VOLUME. we change as per max_volume set by user. This means that if max volume is 80 then full + volume in HA will give 80% volume on the receiver. Then we convert + that to the correct scale for the receiver. + """ + # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL + self.command( + f"zone{self._zone}.volume={int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" + ) def volume_up(self): """Increase volume by 1 step.""" diff --git a/requirements_all.txt b/requirements_all.txt index f5b4bf38da3270..dfdf84bf6ed0d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -890,7 +890,7 @@ oauth2client==4.0.0 oemthermostat==1.1 # homeassistant.components.onkyo -onkyo-eiscp==1.2.4 +onkyo-eiscp==1.2.7 # homeassistant.components.onvif onvif-zeep-async==0.2.0 From b570be47ca253f06e357eea49bbc609012bb87e7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 Oct 2019 12:46:43 +0200 Subject: [PATCH 0841/3953] Upgrade alpha_vantage to 2.1.1 (#27580) * Upgrade alpha_vantage to 2.1.1 * Move imports --- homeassistant/components/alpha_vantage/manifest.json | 2 +- homeassistant/components/alpha_vantage/sensor.py | 7 +++---- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 9ac8d1ea1e0b70..1213bb12e74c3c 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -3,7 +3,7 @@ "name": "Alpha vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "requirements": [ - "alpha_vantage==2.1.0" + "alpha_vantage==2.1.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 188567e4cf4276..da29e4e25e1baa 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -3,6 +3,8 @@ import logging import voluptuous as vol +from alpha_vantage.timeseries import TimeSeries +from alpha_vantage.foreignexchange import ForeignExchange from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME @@ -62,15 +64,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Alpha Vantage sensor.""" - from alpha_vantage.timeseries import TimeSeries - from alpha_vantage.foreignexchange import ForeignExchange - api_key = config.get(CONF_API_KEY) symbols = config.get(CONF_SYMBOLS, []) conversions = config.get(CONF_FOREIGN_EXCHANGE, []) if not symbols and not conversions: - msg = "Warning: No symbols or currencies configured." + msg = "No symbols or currencies configured." hass.components.persistent_notification.create(msg, "Sensor alpha_vantage") _LOGGER.warning(msg) return diff --git a/requirements_all.txt b/requirements_all.txt index dfdf84bf6ed0d4..b5ad094401ffeb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ aladdin_connect==0.3 alarmdecoder==1.13.2 # homeassistant.components.alpha_vantage -alpha_vantage==2.1.0 +alpha_vantage==2.1.1 # homeassistant.components.ambiclimate ambiclimate==0.2.1 From bb2a1cd439493934ef1925a7a129a891c23879a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 14:42:29 +0200 Subject: [PATCH 0842/3953] Move imports in thermoworks_smoke component (#27586) --- .../components/thermoworks_smoke/sensor.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 70a16287fcc245..d5af021108a268 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -9,18 +9,21 @@ import logging from requests import RequestException +from requests.exceptions import HTTPError +from stringcase import camelcase, snakecase +import thermoworks_smoke import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_FAHRENHEIT, + ATTR_BATTERY_LEVEL, CONF_EMAIL, - CONF_PASSWORD, - CONF_MONITORED_CONDITIONS, CONF_EXCLUDE, - ATTR_BATTERY_LEVEL, + CONF_MONITORED_CONDITIONS, + CONF_PASSWORD, + TEMP_FAHRENHEIT, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -65,8 +68,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the thermoworks sensor.""" - import thermoworks_smoke - from requests.exceptions import HTTPError email = config[CONF_EMAIL] password = config[CONF_PASSWORD] @@ -144,7 +145,6 @@ def update_unit(self): def update(self): """Get the monitored data from firebase.""" - from stringcase import camelcase, snakecase try: values = self.mgr.data(self.serial) From bbafeb5da21179f38aaeec5ed36dcd9be0b73a33 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 Oct 2019 14:46:12 +0200 Subject: [PATCH 0843/3953] Upgrade pillow to 6.2.0 (#27581) --- .../components/image_processing/manifest.json | 2 +- homeassistant/components/proxy/camera.py | 14 ++++--------- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/__init__.py | 2 +- .../components/qrcode/image_processing.py | 21 ++++++++++--------- homeassistant/components/qrcode/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 21 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 4a96e9828cb453..6a88a358f1d5c9 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -3,7 +3,7 @@ "name": "Image processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", "requirements": [ - "pillow==6.1.0" + "pillow==6.2.0" ], "dependencies": [ "camera" diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index b1ce8ad7ac0466..90487120ffe0a7 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -1,12 +1,14 @@ """Proxy camera platform that enables image processing of camera data.""" import asyncio +from datetime import timedelta +import io import logging -from datetime import timedelta +from PIL import Image import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE +from homeassistant.const import CONF_ENTITY_ID, CONF_MODE, CONF_NAME from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv import homeassistant.util.dt as dt_util @@ -58,9 +60,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _precheck_image(image, opts): """Perform some pre-checks on the given image.""" - from PIL import Image - import io - if not opts: raise ValueError() try: @@ -77,9 +76,6 @@ def _precheck_image(image, opts): def _resize_image(image, opts): """Resize image.""" - from PIL import Image - import io - try: img = _precheck_image(image, opts) except ValueError: @@ -125,8 +121,6 @@ def _resize_image(image, opts): def _crop_image(image, opts): """Crop image.""" - import io - try: img = _precheck_image(image, opts) except ValueError: diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index c67fd4afc090c5..e3f62514801000 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/integrations/proxy", "requirements": [ - "pillow==6.1.0" + "pillow==6.2.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/qrcode/__init__.py b/homeassistant/components/qrcode/__init__.py index bcc1985a2dc7f4..55b1a2a9d6be7b 100644 --- a/homeassistant/components/qrcode/__init__.py +++ b/homeassistant/components/qrcode/__init__.py @@ -1 +1 @@ -"""The qrcode component.""" +"""The QR code component.""" diff --git a/homeassistant/components/qrcode/image_processing.py b/homeassistant/components/qrcode/image_processing.py index 5e1b7c11b25c7d..018f074a6d2f96 100644 --- a/homeassistant/components/qrcode/image_processing.py +++ b/homeassistant/components/qrcode/image_processing.py @@ -1,15 +1,20 @@ -"""Support for the QR image processing.""" -from homeassistant.core import split_entity_id +"""Support for the QR code image processing.""" +import io + +from PIL import Image +from pyzbar import pyzbar + from homeassistant.components.image_processing import ( - ImageProcessingEntity, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + ImageProcessingEntity, ) +from homeassistant.core import split_entity_id def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the demo image processing platform.""" + """Set up the QR code image processing platform.""" # pylint: disable=unused-argument entities = [] for camera in config[CONF_SOURCE]: @@ -19,7 +24,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class QrEntity(ImageProcessingEntity): - """QR image processing entity.""" + """A QR image processing entity.""" def __init__(self, camera_entity, name): """Initialize QR image processing entity.""" @@ -49,10 +54,6 @@ def name(self): def process_image(self, image): """Process image.""" - import io - from pyzbar import pyzbar - from PIL import Image - stream = io.BytesIO(image) img = Image.open(stream) diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 87e16f629876e8..a3130070cc3518 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/integrations/qrcode", "requirements": [ - "pillow==6.1.0", + "pillow==6.2.0", "pyzbar==0.1.7" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index b5ad094401ffeb..a4623fe8bfb5c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -953,7 +953,7 @@ pilight==0.1.1 # homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -pillow==6.1.0 +pillow==6.2.0 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 143f219fecf6b0..5c9f80e408c1ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -328,7 +328,7 @@ pilight==0.1.1 # homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -pillow==6.1.0 +pillow==6.2.0 # homeassistant.components.plex plexapi==3.0.6 From 930182a7cbcfa3b240a84fd73137b8b694ea6a6f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Oct 2019 14:54:38 +0200 Subject: [PATCH 0844/3953] Move import in deutsche_bahn integration (#27579) * Moved import in deutsche_bahn integration * Moved import schiene before PLATFORM_SCHEMA --- homeassistant/components/deutsche_bahn/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index fbe0efa15ac7a2..ad7b40f78dbb37 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -4,6 +4,8 @@ import voluptuous as vol +import schiene + from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -89,7 +91,6 @@ class SchieneData: def __init__(self, start, goal, offset, only_direct): """Initialize the sensor.""" - import schiene self.start = start self.goal = goal From 627ca3182a927af91af871b5fd4e31e7ca4d31e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 14:56:02 +0200 Subject: [PATCH 0845/3953] Move imports in thingspeak component (#27585) --- homeassistant/components/thingspeak/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/thingspeak/__init__.py b/homeassistant/components/thingspeak/__init__.py index 0893b3311bb7eb..1870a317752f66 100644 --- a/homeassistant/components/thingspeak/__init__.py +++ b/homeassistant/components/thingspeak/__init__.py @@ -2,6 +2,7 @@ import logging from requests.exceptions import RequestException +import thingspeak import voluptuous as vol from homeassistant.const import ( @@ -36,7 +37,6 @@ def setup(hass, config): """Set up the Thingspeak environment.""" - import thingspeak conf = config[DOMAIN] api_key = conf.get(CONF_API_KEY) From 989c3581ac80e4fb5b92f3d8b0bbbe63a567cbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 15:01:13 +0200 Subject: [PATCH 0846/3953] Move imports in tplink_lte component (#27583) --- homeassistant/components/tplink_lte/__init__.py | 3 +-- homeassistant/components/tplink_lte/notify.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink_lte/__init__.py b/homeassistant/components/tplink_lte/__init__.py index 215dd5c94b2189..e495a14a38cb90 100644 --- a/homeassistant/components/tplink_lte/__init__.py +++ b/homeassistant/components/tplink_lte/__init__.py @@ -4,6 +4,7 @@ import aiohttp import attr +import tp_connected import voluptuous as vol from homeassistant.const import ( @@ -106,7 +107,6 @@ async def async_setup(hass, config): async def _setup_lte(hass, lte_config, delay=0): """Set up a TP-Link LTE modem.""" - import tp_connected host = lte_config[CONF_HOST] password = lte_config[CONF_PASSWORD] @@ -145,7 +145,6 @@ async def cleanup(event): async def _retry_login(hass, modem_data, password): """Sleep and retry setup.""" - import tp_connected _LOGGER.warning("Could not connect to %s. Will keep trying.", modem_data.host) diff --git a/homeassistant/components/tplink_lte/notify.py b/homeassistant/components/tplink_lte/notify.py index e677b42a51139a..478b3e998c09a8 100644 --- a/homeassistant/components/tplink_lte/notify.py +++ b/homeassistant/components/tplink_lte/notify.py @@ -2,6 +2,7 @@ import logging import attr +import tp_connected from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import CONF_RECIPIENT @@ -27,7 +28,6 @@ class TplinkNotifyService(BaseNotificationService): async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" - import tp_connected modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config) if not modem_data: From 5e4b33c740478819de44b3446d596d6b108806fb Mon Sep 17 00:00:00 2001 From: bouni Date: Sun, 13 Oct 2019 15:09:44 +0200 Subject: [PATCH 0847/3953] Move imports in bme280 component (#27505) --- homeassistant/components/bme280/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index ee4e1731156c17..b9bc18e6abf437 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -3,6 +3,9 @@ from functools import partial import logging +import smbus # pylint: disable=import-error +from i2csense.bme280 import BME280 # pylint: disable=import-error + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -76,8 +79,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BME280 sensor.""" - import smbus # pylint: disable=import-error - from i2csense.bme280 import BME280 # pylint: disable=import-error SENSOR_TYPES[SENSOR_TEMP][1] = hass.config.units.temperature_unit name = config.get(CONF_NAME) From d96cd4c4ea1999f87db5b660ec225ad26cb3d471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 17:05:04 +0200 Subject: [PATCH 0848/3953] Move imports in tplink component (#27567) * move imports in tplink component * fix: order of imports * fix: tplink tests * fix: import order in tests * fix: tests formatting --- homeassistant/components/tplink/__init__.py | 6 +- homeassistant/components/tplink/common.py | 5 +- .../components/tplink/config_flow.py | 6 +- .../components/tplink/device_tracker.py | 14 ++--- tests/components/tplink/test_init.py | 56 +++++++++++++------ 5 files changed, 53 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 075bffb9f26880..85258b5e94edc1 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -3,20 +3,20 @@ import voluptuous as vol -from homeassistant.const import CONF_HOST from homeassistant import config_entries +from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .common import ( - async_discover_devices, - get_static_devices, ATTR_CONFIG, CONF_DIMMER, CONF_DISCOVERY, CONF_LIGHT, CONF_SWITCH, SmartDevices, + async_discover_devices, + get_static_devices, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 90895104170928..75636c8dc2817a 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -1,10 +1,10 @@ """Common code for tplink.""" import asyncio -import logging from datetime import timedelta +import logging from typing import Any, Callable, List -from pyHS100 import SmartBulb, SmartDevice, SmartPlug, SmartDeviceException +from pyHS100 import Discover, SmartBulb, SmartDevice, SmartDeviceException, SmartPlug from homeassistant.helpers.typing import HomeAssistantType @@ -49,7 +49,6 @@ def has_device_with_host(self, host): async def async_get_discoverable_devices(hass): """Return if there are devices that can be discovered.""" - from pyHS100 import Discover def discover(): devs = Discover.discover() diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index c4888ecee961d0..40583294bfd65a 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -1,9 +1,9 @@ """Config flow for TP-Link.""" -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries -from .const import DOMAIN -from .common import async_get_discoverable_devices +from homeassistant.helpers import config_entry_flow +from .common import async_get_discoverable_devices +from .const import DOMAIN config_entry_flow.register_discovery_flow( DOMAIN, diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py index f6921efed9183b..ce17f6e465f730 100644 --- a/homeassistant/components/tplink/device_tracker.py +++ b/homeassistant/components/tplink/device_tracker.py @@ -7,18 +7,19 @@ from aiohttp.hdrs import ( ACCEPT, + ACCEPT_ENCODING, + ACCEPT_LANGUAGE, + CACHE_CONTROL, + CONNECTION, + CONTENT_TYPE, COOKIE, + KEEP_ALIVE, PRAGMA, REFERER, - CONNECTION, - KEEP_ALIVE, USER_AGENT, - CONTENT_TYPE, - CACHE_CONTROL, - ACCEPT_ENCODING, - ACCEPT_LANGUAGE, ) import requests +from tplink.tplink import TpLinkClient import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -88,7 +89,6 @@ class TplinkDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from tplink.tplink import TpLinkClient host = config[CONF_HOST] password = config[CONF_PASSWORD] diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index df9bf2c2ca2ef1..9428bf05483da0 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -1,21 +1,22 @@ """Tests for the TP-Link component.""" -from typing import Dict, Any +from typing import Any, Dict from unittest.mock import MagicMock, patch +from pyHS100 import SmartBulb, SmartDevice, SmartDeviceException, SmartPlug import pytest -from pyHS100 import SmartPlug, SmartBulb, SmartDevice, SmartDeviceException from homeassistant import config_entries, data_entry_flow from homeassistant.components import tplink from homeassistant.components.tplink.common import ( - CONF_DISCOVERY, CONF_DIMMER, + CONF_DISCOVERY, CONF_LIGHT, CONF_SWITCH, ) from homeassistant.const import CONF_HOST from homeassistant.setup import async_setup_component -from tests.common import MockDependency, MockConfigEntry, mock_coro + +from tests.common import MockConfigEntry, MockDependency, mock_coro MOCK_PYHS100 = MockDependency("pyHS100") @@ -25,7 +26,10 @@ async def test_creating_entry_tries_discover(hass): with MOCK_PYHS100, patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), - ) as mock_setup, patch("pyHS100.Discover.discover", return_value={"host": 1234}): + ) as mock_setup, patch( + "homeassistant.components.tplink.common.Discover.discover", + return_value={"host": 1234}, + ): result = await hass.config_entries.flow.async_init( tplink.DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -43,7 +47,9 @@ async def test_creating_entry_tries_discover(hass): async def test_configuring_tplink_causes_discovery(hass): """Test that specifying empty config does discovery.""" - with MOCK_PYHS100, patch("pyHS100.Discover.discover") as discover: + with MOCK_PYHS100, patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover: discover.return_value = {"host": 1234} await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -61,8 +67,10 @@ async def test_configuring_tplink_causes_discovery(hass): @pytest.mark.parametrize("count", [1, 2, 3]) async def test_configuring_device_types(hass, name, cls, platform, count): """Test that light or switch platform list is filled correctly.""" - with patch("pyHS100.Discover.discover") as discover, patch( - "pyHS100.SmartDevice._query_helper" + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" ): discovery_data = { "123.123.123.{}".format(c): cls("123.123.123.123") for c in range(count) @@ -104,8 +112,10 @@ def state_information(self) -> Dict[str, Any]: async def test_configuring_devices_from_multiple_sources(hass): """Test static and discover devices are not duplicated.""" - with patch("pyHS100.Discover.discover") as discover, patch( - "pyHS100.SmartDevice._query_helper" + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" ): discover_device_fail = SmartPlug("123.123.123.123") discover_device_fail.get_sysinfo = MagicMock(side_effect=SmartDeviceException()) @@ -139,11 +149,15 @@ async def test_configuring_devices_from_multiple_sources(hass): async def test_is_dimmable(hass): """Test that is_dimmable switches are correctly added as lights.""" - with patch("pyHS100.Discover.discover") as discover, patch( + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), - ) as setup, patch("pyHS100.SmartDevice._query_helper"), patch( - "pyHS100.SmartPlug.is_dimmable", True + ) as setup, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", True ): dimmable_switch = SmartPlug("123.123.123.123") discover.return_value = {"host": dimmable_switch} @@ -162,7 +176,9 @@ async def test_configuring_discovery_disabled(hass): with MOCK_PYHS100, patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), - ) as mock_setup, patch("pyHS100.Discover.discover", return_value=[]) as discover: + ) as mock_setup, patch( + "homeassistant.components.tplink.common.Discover.discover", return_value=[] + ) as discover: await async_setup_component( hass, tplink.DOMAIN, {tplink.DOMAIN: {tplink.CONF_DISCOVERY: False}} ) @@ -182,8 +198,10 @@ async def test_platforms_are_initialized(hass): } } - with patch("pyHS100.Discover.discover") as discover, patch( - "pyHS100.SmartDevice._query_helper" + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), @@ -191,7 +209,7 @@ async def test_platforms_are_initialized(hass): "homeassistant.components.tplink.switch.async_setup_entry", return_value=mock_coro(True), ) as switch_setup, patch( - "pyHS100.SmartPlug.is_dimmable", False + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False ): # patching is_dimmable is necessray to avoid misdetection as light. await async_setup_component(hass, tplink.DOMAIN, config) @@ -221,7 +239,9 @@ async def test_unload(hass, platform): entry = MockConfigEntry(domain=tplink.DOMAIN) entry.add_to_hass(hass) - with patch("pyHS100.SmartDevice._query_helper"), patch( + with patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( "homeassistant.components.tplink.{}" ".async_setup_entry".format(platform), return_value=mock_coro(True), ) as light_setup: From 1774a1427ba48994fab875b82aff8dea49995796 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Sun, 13 Oct 2019 11:01:04 -0700 Subject: [PATCH 0849/3953] Add abode config entries and device registry (#26699) * Adds support for config entries and device registry * Fixing string formatting for logger * Add unit test for abode config flow * Fix for lights, only allow one config, add ability to unload entry * Fix for subscribing to hass_events on adding abode component * Several fixes from code review * Several fixes from second code review * Several fixes from third code review * Update documentation url to fix branch conflict * Fixes config flow and removes unused constants * Fix for switches, polling entry option, improved tests * Update .coveragerc, disable pylint W0611, remove polling from UI * Multiple fixes and edits to adhere to style guidelines * Removed unique_id * Minor correction for formatting error in rebase * Resolves issue causing CI to fail * Bump abodepy version * Add remove device callback and minor clean up * Fix incorrect method name * Docstring edits * Fix duplicate import issues from rebase * Add logout_listener attribute to AbodeSystem * Add additional test for complete coverage --- .coveragerc | 10 +- CODEOWNERS | 1 + homeassistant/components/abode/__init__.py | 180 +++++++++--------- .../components/abode/alarm_control_panel.py | 28 ++- .../components/abode/binary_sensor.py | 24 ++- homeassistant/components/abode/camera.py | 23 +-- homeassistant/components/abode/config_flow.py | 79 ++++++++ homeassistant/components/abode/const.py | 3 + homeassistant/components/abode/cover.py | 19 +- homeassistant/components/abode/light.py | 23 +-- homeassistant/components/abode/lock.py | 21 +- homeassistant/components/abode/manifest.json | 9 +- homeassistant/components/abode/sensor.py | 19 +- homeassistant/components/abode/strings.json | 22 +++ homeassistant/components/abode/switch.py | 25 +-- homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/abode/__init__.py | 1 + tests/components/abode/test_config_flow.py | 120 ++++++++++++ 20 files changed, 426 insertions(+), 187 deletions(-) create mode 100644 homeassistant/components/abode/config_flow.py create mode 100644 homeassistant/components/abode/const.py create mode 100644 homeassistant/components/abode/strings.json create mode 100644 tests/components/abode/__init__.py create mode 100644 tests/components/abode/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index d241260fdf0821..145350b6b19955 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,7 +10,15 @@ omit = homeassistant/util/async.py # omit pieces of code that rely on external devices being present - homeassistant/components/abode/* + homeassistant/components/abode/__init__.py + homeassistant/components/abode/alarm_control_panel.py + homeassistant/components/abode/binary_sensor.py + homeassistant/components/abode/camera.py + homeassistant/components/abode/cover.py + homeassistant/components/abode/light.py + homeassistant/components/abode/lock.py + homeassistant/components/abode/sensor.py + homeassistant/components/abode/switch.py homeassistant/components/acer_projector/switch.py homeassistant/components/actiontec/device_tracker.py homeassistant/components/adguard/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 070151d01e076b..ea50d24095c1ba 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,6 +13,7 @@ homeassistant/util/* @home-assistant/core homeassistant/scripts/check_config.py @kellerza # Integrations +homeassistant/components/abode/* @shred86 homeassistant/components/adguard/* @frenck homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index b7f13d49b69c1f..aeba437acebd47 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -1,49 +1,36 @@ -"""Support for Abode Home Security system.""" -import logging +"""Support for the Abode Security System.""" +from asyncio import gather +from copy import deepcopy from functools import partial -from requests.exceptions import HTTPError, ConnectTimeout -import abodepy -import abodepy.helpers.constants as CONST +import logging + +from abodepy import Abode from abodepy.exceptions import AbodeException import abodepy.helpers.timeline as TIMELINE - +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DATE, - ATTR_TIME, ATTR_ENTITY_ID, - CONF_USERNAME, + ATTR_TIME, CONF_PASSWORD, - CONF_EXCLUDE, - CONF_NAME, - CONF_LIGHTS, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) +from .const import ATTRIBUTION, DOMAIN -ATTRIBUTION = "Data provided by goabode.com" +_LOGGER = logging.getLogger(__name__) CONF_POLLING = "polling" -DOMAIN = "abode" DEFAULT_CACHEDB = "./abodepy_cache.pickle" -NOTIFICATION_ID = "abode_notification" -NOTIFICATION_TITLE = "Abode Security Setup" - -EVENT_ABODE_ALARM = "abode_alarm" -EVENT_ABODE_ALARM_END = "abode_alarm_end" -EVENT_ABODE_AUTOMATION = "abode_automation" -EVENT_ABODE_FAULT = "abode_panel_fault" -EVENT_ABODE_RESTORE = "abode_panel_restore" - SERVICE_SETTINGS = "change_setting" SERVICE_CAPTURE_IMAGE = "capture_image" SERVICE_TRIGGER = "trigger_quick_action" @@ -67,10 +54,7 @@ { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_POLLING, default=False): cv.boolean, - vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, - vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, } ) }, @@ -100,73 +84,80 @@ class AbodeSystem: """Abode System class.""" - def __init__(self, username, password, cache, name, polling, exclude, lights): + def __init__(self, abode, polling): """Initialize the system.""" - self.abode = abodepy.Abode( - username, - password, - auto_login=True, - get_devices=True, - get_automations=True, - cache_path=cache, - ) - self.name = name + self.abode = abode self.polling = polling - self.exclude = exclude - self.lights = lights self.devices = [] + self.logout_listener = None - def is_excluded(self, device): - """Check if a device is configured to be excluded.""" - return device.device_id in self.exclude - def is_automation_excluded(self, automation): - """Check if an automation is configured to be excluded.""" - return automation.automation_id in self.exclude +async def async_setup(hass, config): + """Set up Abode integration.""" + if DOMAIN not in config: + return True - def is_light(self, device): - """Check if a switch device is configured as a light.""" + conf = config[DOMAIN] - return device.generic_type == CONST.TYPE_LIGHT or ( - device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=deepcopy(conf) ) + ) + return True -def setup(hass, config): - """Set up Abode component.""" - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - name = conf.get(CONF_NAME) - polling = conf.get(CONF_POLLING) - exclude = conf.get(CONF_EXCLUDE) - lights = conf.get(CONF_LIGHTS) +async def async_setup_entry(hass, config_entry): + """Set up Abode integration from a config entry.""" + username = config_entry.data.get(CONF_USERNAME) + password = config_entry.data.get(CONF_PASSWORD) + polling = config_entry.data.get(CONF_POLLING) try: cache = hass.config.path(DEFAULT_CACHEDB) - hass.data[DOMAIN] = AbodeSystem( - username, password, cache, name, polling, exclude, lights + abode = await hass.async_add_executor_job( + Abode, username, password, True, True, True, cache ) + hass.data[DOMAIN] = AbodeSystem(abode, polling) + except (AbodeException, ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Abode: %s", str(ex)) + return False - hass.components.persistent_notification.create( - "Error: {}
" - "You will need to restart hass after fixing." - "".format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, + for platform in ABODE_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) ) - return False - setup_hass_services(hass) - setup_hass_events(hass) - setup_abode_events(hass) + await setup_hass_events(hass) + await hass.async_add_executor_job(setup_hass_services, hass) + await hass.async_add_executor_job(setup_abode_events, hass) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + hass.services.async_remove(DOMAIN, SERVICE_SETTINGS) + hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE) + hass.services.async_remove(DOMAIN, SERVICE_TRIGGER) + + tasks = [] for platform in ABODE_PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) + tasks.append( + hass.config_entries.async_forward_entry_unload(config_entry, platform) + ) + + await gather(*tasks) + + await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop) + await hass.async_add_executor_job(hass.data[DOMAIN].abode.logout) + + hass.data[DOMAIN].logout_listener() + hass.data.pop(DOMAIN) return True @@ -223,13 +214,9 @@ def trigger_quick_action(call): ) -def setup_hass_events(hass): +async def setup_hass_events(hass): """Home Assistant start and stop callbacks.""" - def startup(event): - """Listen for push events.""" - hass.data[DOMAIN].abode.events.start() - def logout(event): """Logout of Abode.""" if not hass.data[DOMAIN].polling: @@ -239,9 +226,11 @@ def logout(event): _LOGGER.info("Logged out of Abode") if not hass.data[DOMAIN].polling: - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, startup) + await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.start) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout) + hass.data[DOMAIN].logout_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, logout + ) def setup_abode_events(hass): @@ -282,30 +271,36 @@ class AbodeDevice(Entity): """Representation of an Abode device.""" def __init__(self, data, device): - """Initialize a sensor for Abode device.""" + """Initialize Abode device.""" self._data = data self._device = device async def async_added_to_hass(self): - """Subscribe Abode events.""" + """Subscribe to device events.""" self.hass.async_add_job( self._data.abode.events.add_device_callback, self._device.device_id, self._update_callback, ) + async def async_will_remove_from_hass(self): + """Unsubscribe from device events.""" + self.hass.async_add_job( + self._data.abode.events.remove_all_device_callbacks, self._device.device_id + ) + @property def should_poll(self): """Return the polling state.""" return self._data.polling def update(self): - """Update automation state.""" + """Update device and automation states.""" self._device.refresh() @property def name(self): - """Return the name of the sensor.""" + """Return the name of the device.""" return self._device.name @property @@ -319,6 +314,21 @@ def device_state_attributes(self): "device_type": self._device.type, } + @property + def unique_id(self): + """Return a unique ID to use for this device.""" + return self._device.device_uuid + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "manufacturer": "Abode", + "name": self._device.name, + "device_type": self._device.type, + } + def _update_callback(self, device): """Update the device state.""" self.schedule_update_ha_state() @@ -353,7 +363,7 @@ def update(self): @property def name(self): - """Return the name of the sensor.""" + """Return the name of the automation.""" return self._automation.name @property @@ -367,6 +377,6 @@ def device_state_attributes(self): } def _update_callback(self, device): - """Update the device state.""" + """Update the automation state.""" self._automation.refresh() self.schedule_update_ha_state() diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index c5c10e65302b43..f774e773cb527c 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -9,32 +9,31 @@ STATE_ALARM_DISARMED, ) -from . import ATTRIBUTION, DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import ATTRIBUTION, DOMAIN _LOGGER = logging.getLogger(__name__) ICON = "mdi:security" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up an alarm control panel for an Abode device.""" - data = hass.data[ABODE_DOMAIN] +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - alarm_devices = [AbodeAlarm(data, data.abode.get_alarm(), data.name)] - data.devices.extend(alarm_devices) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up an alarm control panel for an Abode device.""" + data = hass.data[DOMAIN] - add_entities(alarm_devices) + async_add_entities( + [AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))] + ) class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): """An alarm_control_panel implementation for Abode.""" - def __init__(self, data, device, name): - """Initialize the alarm control panel.""" - super().__init__(data, device) - self._name = name - @property def icon(self): """Return the icon.""" @@ -65,11 +64,6 @@ def alarm_arm_away(self, code=None): """Send arm away command.""" self._device.set_away() - @property - def name(self): - """Return the name of the alarm.""" - return self._name or super().name - @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 3ae7f41d84ea34..31f744484963f7 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -6,15 +6,20 @@ from homeassistant.components.binary_sensor import BinarySensorDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice +from . import AbodeAutomation, AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a sensor for an Abode device.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + - data = hass.data[ABODE_DOMAIN] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a sensor for an Abode device.""" + data = hass.data[DOMAIN] device_types = [ CONST.TYPE_CONNECTIVITY, @@ -25,25 +30,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] devices = [] - for device in data.abode.get_devices(generic_type=device_types): - if data.is_excluded(device): - continue + for device in data.abode.get_devices(generic_type=device_types): devices.append(AbodeBinarySensor(data, device)) for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION): - if data.is_automation_excluded(automation): - continue - devices.append( AbodeQuickActionBinarySensor( data, automation, TIMELINE.AUTOMATION_EDIT_GROUP ) ) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index f52bbe17475a32..e98a59a985ce64 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -2,35 +2,36 @@ from datetime import timedelta import logging -import requests import abodepy.helpers.constants as CONST import abodepy.helpers.timeline as TIMELINE +import requests from homeassistant.components.camera import Camera from homeassistant.util import Throttle -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode camera devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + - data = hass.data[ABODE_DOMAIN] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a camera for an Abode device.""" + + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): - if data.is_excluded(device): - continue - devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeCamera(AbodeDevice, Camera): diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py new file mode 100644 index 00000000000000..d8d914f7998d63 --- /dev/null +++ b/homeassistant/components/abode/config_flow.py @@ -0,0 +1,79 @@ +"""Config flow for the Abode Security System component.""" +import logging + +from abodepy import Abode +from abodepy.exceptions import AbodeException +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback + +from .const import DOMAIN # pylint: disable=W0611 + +CONF_POLLING = "polling" + +_LOGGER = logging.getLogger(__name__) + + +class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Abode.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize.""" + self.data_schema = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if not user_input: + return self._show_form() + + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + polling = user_input.get(CONF_POLLING, False) + + try: + await self.hass.async_add_executor_job(Abode, username, password, True) + + except (AbodeException, ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Abode: %s", str(ex)) + if ex.errcode == 400: + return self._show_form({"base": "invalid_credentials"}) + return self._show_form({"base": "connection_error"}) + + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_POLLING: polling, + }, + ) + + @callback + def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(self.data_schema), + errors=errors if errors else {}, + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + if self._async_current_entries(): + _LOGGER.warning("Only one configuration of abode is allowed.") + return self.async_abort(reason="single_instance_allowed") + + return await self.async_step_user(import_config) diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py new file mode 100644 index 00000000000000..35e74e154cffe4 --- /dev/null +++ b/homeassistant/components/abode/const.py @@ -0,0 +1,3 @@ +"""Constants for the Abode Security System component.""" +DOMAIN = "abode" +ATTRIBUTION = "Data provided by goabode.com" diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 13d46c53f7360c..ebe59ee45c7391 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -5,26 +5,27 @@ from homeassistant.components.cover import CoverDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode cover devices.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): - if data.is_excluded(device): - continue - devices.append(AbodeCover(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeCover(AbodeDevice, CoverDevice): diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 6551cba2ef112c..163982d040eab6 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -18,30 +18,27 @@ color_temperature_mired_to_kelvin, ) -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode light devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] - device_types = [CONST.TYPE_LIGHT, CONST.TYPE_SWITCH] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode light devices.""" + data = hass.data[DOMAIN] devices = [] - # Get all regular lights that are not excluded or switches marked as lights - for device in data.abode.get_devices(generic_type=device_types): - if data.is_excluded(device) or not data.is_light(device): - continue - + for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT): devices.append(AbodeLight(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeLight(AbodeDevice, Light): diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index ff069751605e42..11f792f88fd8c8 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -1,30 +1,31 @@ -"""Support for Abode Security System locks.""" +"""Support for the Abode Security System locks.""" import logging import abodepy.helpers.constants as CONST from homeassistant.components.lock import LockDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode lock devices.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): - if data.is_excluded(device): - continue - devices.append(AbodeLock(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeLock(AbodeDevice, LockDevice): diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 793c19cc466e63..8316691f70100c 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -1,10 +1,13 @@ { "domain": "abode", "name": "Abode", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", "requirements": [ - "abodepy==0.15.0" + "abodepy==0.16.5" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@shred86" + ] +} \ No newline at end of file diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index fca32b8dc43b64..e25921f295f3dc 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -9,7 +9,8 @@ DEVICE_CLASS_TEMPERATURE, ) -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,22 +22,22 @@ } -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for an Abode device.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): - if data.is_excluded(device): - continue - for sensor_type in SENSOR_TYPES: devices.append(AbodeSensor(data, device, sensor_type)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeSensor(AbodeDevice): diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json new file mode 100644 index 00000000000000..bf7e768f6e325c --- /dev/null +++ b/homeassistant/components/abode/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "Abode", + "step": { + "user": { + "title": "Fill in your Abode login information", + "data": { + "username": "Email Address", + "password": "Password" + } + } + }, + "error": { + "identifier_exists": "Account already registered.", + "invalid_credentials": "Invalid credentials.", + "connection_error": "Unable to connect to Abode." + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Abode is allowed." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 4192ebb4485f8e..7bd7f394d30bee 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -6,37 +6,32 @@ from homeassistant.components.switch import SwitchDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice +from . import AbodeAutomation, AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode switch devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + - data = hass.data[ABODE_DOMAIN] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode switch devices.""" + data = hass.data[DOMAIN] devices = [] - # Get all regular switches that are not excluded or marked as lights for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH): - if data.is_excluded(device) or data.is_light(device): - continue - devices.append(AbodeSwitch(data, device)) - # Get all Abode automations that can be enabled/disabled for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION): - if data.is_automation_excluded(automation): - continue - devices.append( AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP) ) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeSwitch(AbodeDevice, SwitchDevice): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4a4effc36ce9a2..664e83fba33aea 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -6,6 +6,7 @@ # fmt: off FLOWS = [ + "abode", "adguard", "airly", "ambiclimate", diff --git a/requirements_all.txt b/requirements_all.txt index a4623fe8bfb5c0..9a9540c87473d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -103,7 +103,7 @@ WazeRouteCalculator==0.10 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.15.0 +abodepy==0.16.5 # homeassistant.components.mcp23017 adafruit-blinka==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c9f80e408c1ba..4c3a6e7721bccb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,6 +45,9 @@ RtmAPI==0.7.2 # homeassistant.components.yessssms YesssSMS==0.4.1 +# homeassistant.components.abode +abodepy==0.16.5 + # homeassistant.components.androidtv adb-shell==0.0.4 diff --git a/tests/components/abode/__init__.py b/tests/components/abode/__init__.py new file mode 100644 index 00000000000000..a34320c21de958 --- /dev/null +++ b/tests/components/abode/__init__.py @@ -0,0 +1 @@ +"""Tests for the Abode component.""" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py new file mode 100644 index 00000000000000..c3f5d17076753e --- /dev/null +++ b/tests/components/abode/test_config_flow.py @@ -0,0 +1,120 @@ +"""Tests for the Abode config flow.""" +from unittest.mock import patch + +from abodepy.exceptions import AbodeAuthenticationException + +from homeassistant import data_entry_flow +from homeassistant.components.abode import config_flow +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.common import MockConfigEntry + +CONF_POLLING = "polling" + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.AbodeFlowHandler() + 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_one_config_allowed(hass): + """Test that only one Abode configuration is allowed.""" + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + MockConfigEntry( + domain="abode", + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ).add_to_hass(hass) + + step_user_result = await flow.async_step_user() + + assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step_user_result["reason"] == "single_instance_allowed" + + conf = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + import_config_result = await flow.async_step_import(conf) + + assert import_config_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert import_config_result["reason"] == "single_instance_allowed" + + +async def test_invalid_credentials(hass): + """Test that invalid credentials throws an error.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch( + "homeassistant.components.abode.config_flow.Abode", + side_effect=AbodeAuthenticationException((400, "auth error")), + ): + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "invalid_credentials"} + + +async def test_connection_error(hass): + """Test other than invalid credentials throws an error.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch( + "homeassistant.components.abode.config_flow.Abode", + side_effect=AbodeAuthenticationException((500, "connection error")), + ): + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "connection_error"} + + +async def test_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch("homeassistant.components.abode.config_flow.Abode"): + result = await flow.async_step_import(import_config=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await flow.async_step_user(user_input=result["data"]) + assert result["title"] == "user@email.com" + assert result["data"] == { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch("homeassistant.components.abode.config_flow.Abode"): + result = await flow.async_step_user(user_input=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "user@email.com" + assert result["data"] == { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } From 2acd3f9e98763605ad0abe68e5532c79253fc11f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 13 Oct 2019 20:29:14 +0200 Subject: [PATCH 0850/3953] Allow MQTT json light floating point transition (#27253) * MQTT json light: allow floating point transition Allow to use floating point values for the transition time of the MQTT json light. In this way transitions shorter than 1s can be used (0.5 seconds for instance) if the MQTT light supports it. * Always sent a float --- homeassistant/components/mqtt/light/schema_json.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 1a46cd5e535799..1e8114a48e6b7c 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -463,7 +463,7 @@ async def async_turn_on(self, **kwargs): message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT] if ATTR_TRANSITION in kwargs: - message["transition"] = int(kwargs[ATTR_TRANSITION]) + message["transition"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs and self._brightness is not None: message["brightness"] = int( @@ -521,7 +521,7 @@ async def async_turn_off(self, **kwargs): message = {"state": "OFF"} if ATTR_TRANSITION in kwargs: - message["transition"] = int(kwargs[ATTR_TRANSITION]) + message["transition"] = kwargs[ATTR_TRANSITION] mqtt.async_publish( self.hass, From dd8fc4174779ee3010aaec8799595621ea5189bf Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sun, 13 Oct 2019 22:19:11 +0200 Subject: [PATCH 0851/3953] Move imports in rflink component (#27367) * Move imports in rflink component * import order * import order * Update __init__.py * Update __init__.py I don't understand why tests are failing... * Fix RFLink imports * Fix monkeypatch for 'create_rflink_connection' * isort for rflink classes --- homeassistant/components/rflink/__init__.py | 15 +++++++-------- homeassistant/components/rflink/sensor.py | 3 +-- tests/components/rflink/test_binary_sensor.py | 5 ++--- tests/components/rflink/test_cover.py | 8 ++++---- tests/components/rflink/test_init.py | 12 +++++++----- tests/components/rflink/test_light.py | 4 ++-- tests/components/rflink/test_sensor.py | 3 ++- tests/components/rflink/test_switch.py | 4 ++-- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index c218bc271ce7c9..b3e1d2b16b7617 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -2,8 +2,10 @@ import asyncio from collections import defaultdict import logging -import async_timeout +import async_timeout +from rflink.protocol import create_rflink_connection +from serial import SerialException import voluptuous as vol from homeassistant.const import ( @@ -11,18 +13,18 @@ CONF_COMMAND, CONF_HOST, CONF_PORT, - STATE_ON, EVENT_HOMEASSISTANT_STOP, + STATE_ON, ) from homeassistant.core import CoreState, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.deprecation import get_deprecated -from homeassistant.helpers.entity import Entity from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) +from homeassistant.helpers.entity import Entity from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) @@ -118,9 +120,6 @@ def identify_event_type(event): async def async_setup(hass, config): """Set up the Rflink component.""" - from rflink.protocol import create_rflink_connection - import serial - # Allow entities to register themselves by device_id to be looked up when # new rflink events arrive to be handled hass.data[DATA_ENTITY_LOOKUP] = { @@ -239,7 +238,7 @@ async def connect(): transport, protocol = await connection except ( - serial.serialutil.SerialException, + SerialException, ConnectionRefusedError, TimeoutError, OSError, diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index 48484621c4d80e..aa0ef4f9c62b6f 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -1,6 +1,7 @@ """Support for Rflink sensors.""" import logging +from rflink.parser import PACKET_FIELDS, UNITS import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -66,8 +67,6 @@ def lookup_unit_for_sensor_type(sensor_type): Async friendly. """ - from rflink.parser import UNITS, PACKET_FIELDS - field_abbrev = {v: k for k, v in PACKET_FIELDS.items()} return UNITS.get(field_abbrev.get(sensor_type)) diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index 442ebebdffe932..d1fdec579c9db8 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -8,14 +8,13 @@ from unittest.mock import patch from homeassistant.components.rflink import CONF_RECONNECT_INTERVAL - -import homeassistant.core as ha from homeassistant.const import ( EVENT_STATE_CHANGED, - STATE_ON, STATE_OFF, + STATE_ON, STATE_UNAVAILABLE, ) +import homeassistant.core as ha import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py index 858258e7efd0c4..dc286502068b68 100644 --- a/tests/components/rflink/test_cover.py +++ b/tests/components/rflink/test_cover.py @@ -9,13 +9,13 @@ from homeassistant.components.rflink import EVENT_BUTTON_PRESSED from homeassistant.const import ( - SERVICE_OPEN_COVER, + ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, - STATE_OPEN, + SERVICE_OPEN_COVER, STATE_CLOSED, - ATTR_ENTITY_ID, + STATE_OPEN, ) -from homeassistant.core import callback, State, CoreState +from homeassistant.core import CoreState, State, callback from tests.common import mock_restore_cache from tests.components.rflink.test_init import mock_rflink diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 5e821fbdeb225a..df96b0e87aea9c 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -5,14 +5,14 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components.rflink import ( CONF_RECONNECT_INTERVAL, - SERVICE_SEND_COMMAND, - RflinkCommand, - TMP_ENTITY, DATA_ENTITY_LOOKUP, EVENT_KEY_COMMAND, EVENT_KEY_SENSOR, + SERVICE_SEND_COMMAND, + TMP_ENTITY, + RflinkCommand, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_STOP_COVER +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_STOP_COVER, SERVICE_TURN_OFF async def mock_rflink( @@ -46,7 +46,9 @@ async def create_rflink_connection(*args, **kwargs): return transport, protocol mock_create = Mock(wraps=create_rflink_connection) - monkeypatch.setattr("rflink.protocol.create_rflink_connection", mock_create) + monkeypatch.setattr( + "homeassistant.components.rflink.create_rflink_connection", mock_create + ) await async_setup_component(hass, "rflink", config) await async_setup_component(hass, domain, config) diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index a22e7680ac846a..b22730a331093b 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -11,10 +11,10 @@ ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_ON, STATE_OFF, + STATE_ON, ) -from homeassistant.core import callback, State, CoreState +from homeassistant.core import CoreState, State, callback from tests.common import mock_restore_cache from tests.components.rflink.test_init import mock_rflink diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index bf6c9e03fbc395..3fea3ef6ef4cda 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -7,12 +7,13 @@ from homeassistant.components.rflink import ( CONF_RECONNECT_INTERVAL, - TMP_ENTITY, DATA_ENTITY_LOOKUP, EVENT_KEY_COMMAND, EVENT_KEY_SENSOR, + TMP_ENTITY, ) from homeassistant.const import STATE_UNKNOWN + from tests.components.rflink.test_init import mock_rflink DOMAIN = "sensor" diff --git a/tests/components/rflink/test_switch.py b/tests/components/rflink/test_switch.py index 4503f1a232f29a..d1fced3320807e 100644 --- a/tests/components/rflink/test_switch.py +++ b/tests/components/rflink/test_switch.py @@ -10,10 +10,10 @@ ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_ON, STATE_OFF, + STATE_ON, ) -from homeassistant.core import callback, State, CoreState +from homeassistant.core import CoreState, State, callback from tests.common import mock_restore_cache from tests.components.rflink.test_init import mock_rflink From 45694de2ee34c58ec86a14cf16c4f69449fe1a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 22:25:54 +0200 Subject: [PATCH 0852/3953] move imports in tibber component (#27584) --- homeassistant/components/tibber/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 0622b70f127a8e..df56989714fcd8 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -3,12 +3,13 @@ import logging import aiohttp +import tibber import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, CONF_NAME +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util DOMAIN = "tibber" @@ -25,8 +26,6 @@ async def async_setup(hass, config): """Set up the Tibber component.""" conf = config.get(DOMAIN) - import tibber - tibber_connection = tibber.Tibber( conf[CONF_ACCESS_TOKEN], websession=async_get_clientsession(hass), From 5e79408708adda5e4108e66588a8418ed824f156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 13 Oct 2019 23:27:42 +0300 Subject: [PATCH 0853/3953] Upgrade to flake8-docstrings 1.5.0, pytest 5.2.1, and pytest-cov 2.8.1 (#27588) https://gitlab.com/pycqa/flake8-docstrings/blob/1.5.0/HISTORY.rst https://docs.pytest.org/en/latest/changelog.html#pytest-5-2-1-2019-10-06 https://pytest-cov.readthedocs.io/en/latest/changelog.html#id1 --- requirements_test.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index d0b7880d78daf3..b6d5bdd7ee9fd4 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -flake8-docstrings==1.3.1 +flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 mypy==0.730 @@ -15,8 +15,8 @@ pydocstyle==4.0.1 pylint==2.4.2 astroid==2.3.1 pytest-aiohttp==0.3.0 -pytest-cov==2.7.1 +pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.0 +pytest==5.2.1 requests_mock==1.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c3a6e7721bccb..c3fa3024041b52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -flake8-docstrings==1.3.1 +flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 mypy==0.730 @@ -16,10 +16,10 @@ pydocstyle==4.0.1 pylint==2.4.2 astroid==2.3.1 pytest-aiohttp==0.3.0 -pytest-cov==2.7.1 +pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.0 +pytest==5.2.1 requests_mock==1.7.0 From 585214f3a4d055ba397d856e95ca8a93c3ee2cc2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 Oct 2019 22:29:48 +0200 Subject: [PATCH 0854/3953] Upgrade Mastodon.py to 1.5.0 (#27598) --- homeassistant/components/mastodon/manifest.json | 2 +- homeassistant/components/mastodon/notify.py | 10 +++------- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index e041ba2f669a51..cacdf9dd502c96 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/integrations/mastodon", "requirements": [ - "Mastodon.py==1.4.6" + "Mastodon.py==1.5.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py index e7f7de5917f17b..88716de5773c14 100644 --- a/homeassistant/components/mastodon/notify.py +++ b/homeassistant/components/mastodon/notify.py @@ -1,13 +1,14 @@ """Mastodon platform for notify component.""" import logging +from mastodon import Mastodon +from mastodon.Mastodon import MastodonAPIError, MastodonUnauthorizedError import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ACCESS_TOKEN import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) CONF_BASE_URL = "base_url" @@ -28,9 +29,6 @@ def get_service(hass, config, discovery_info=None): """Get the Mastodon notification service.""" - from mastodon import Mastodon - from mastodon.Mastodon import MastodonUnauthorizedError - client_id = config.get(CONF_CLIENT_ID) client_secret = config.get(CONF_CLIENT_SECRET) access_token = config.get(CONF_ACCESS_TOKEN) @@ -60,8 +58,6 @@ def __init__(self, api): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from mastodon.Mastodon import MastodonAPIError - try: self._api.toot(message) except MastodonAPIError: diff --git a/requirements_all.txt b/requirements_all.txt index 9a9540c87473d2..bf36c898b1ed2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -38,7 +38,7 @@ Adafruit-SHT31==1.0.2 HAP-python==2.6.0 # homeassistant.components.mastodon -Mastodon.py==1.4.6 +Mastodon.py==1.5.0 # homeassistant.components.orangepi_gpio OPi.GPIO==0.3.6 From 1d2b59db8277d95b33c0d766124901780f2f0616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 22:38:42 +0200 Subject: [PATCH 0855/3953] Move imports in syslog (#27602) --- homeassistant/components/syslog/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/syslog/notify.py b/homeassistant/components/syslog/notify.py index d7696e43befe8b..67d4882e5c3e9b 100644 --- a/homeassistant/components/syslog/notify.py +++ b/homeassistant/components/syslog/notify.py @@ -1,5 +1,6 @@ """Syslog notification service.""" import logging +import syslog import voluptuous as vol @@ -67,7 +68,6 @@ def get_service(hass, config, discovery_info=None): """Get the syslog notification service.""" - import syslog facility = getattr(syslog, SYSLOG_FACILITY[config.get(CONF_FACILITY)]) option = getattr(syslog, SYSLOG_OPTION[config.get(CONF_OPTION)]) @@ -87,7 +87,6 @@ def __init__(self, facility, option, priority): def send_message(self, message="", **kwargs): """Send a message to a user.""" - import syslog title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) From 48c1a0290c7ac82c6adbc1066095bf5a93387346 Mon Sep 17 00:00:00 2001 From: Moritz Fey Date: Sun, 13 Oct 2019 22:53:42 +0200 Subject: [PATCH 0856/3953] add content for services.yaml in component media_extractor (#27608) * add content for services.yaml for media_extractor component * remove example data * add empty line on end of file * Update services.yaml --- .../components/media_extractor/services.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml index e69de29bb2d1d6..c5588c341340fd 100644 --- a/homeassistant/components/media_extractor/services.yaml +++ b/homeassistant/components/media_extractor/services.yaml @@ -0,0 +1,13 @@ +play_media: + description: Downloads file from given url. + fields: + entity_id: + description: Name(s) of entities to play media on. + example: 'media_player.living_room_chromecast' + media_content_id: + description: The ID of the content to play. Platform dependent. + example: 'https://soundcloud.com/bruttoband/brutto-11' + media_content_type: + description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC. + example: 'music' + From d8b73f945929721d03ab23089f9878c0bfb5b767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 22:55:24 +0200 Subject: [PATCH 0857/3953] move imports in ted5000 component (#27601) --- homeassistant/components/ted5000/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index ea0963a092e59a..e0025a050c307d 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -1,9 +1,10 @@ -"""Support gathering ted500 information.""" -import logging +"""Support gathering ted5000 information.""" from datetime import timedelta +import logging import requests import voluptuous as vol +import xmltodict from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, POWER_WATT @@ -94,7 +95,6 @@ def __init__(self, url): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the Ted5000 XML API.""" - import xmltodict try: request = requests.get(self.url, timeout=10) From c5dc670b36bd3aa7b80749e79e5ef4ac8ef0a2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 22:55:43 +0200 Subject: [PATCH 0858/3953] move imports in tellstick component (#27600) --- .../components/tellstick/__init__.py | 31 +++++++------------ homeassistant/components/tellstick/sensor.py | 10 +++--- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/tellstick/__init__.py b/homeassistant/components/tellstick/__init__.py index 526db5e73dfa43..e7f341c90b26ed 100644 --- a/homeassistant/components/tellstick/__init__.py +++ b/homeassistant/components/tellstick/__init__.py @@ -2,13 +2,22 @@ import logging import threading +from tellcore.constants import ( + TELLSTICK_DIM, + TELLSTICK_TURNOFF, + TELLSTICK_TURNON, + TELLSTICK_UP, +) +from tellcore.library import TelldusError +from tellcore.telldus import AsyncioCallbackDispatcher, TelldusCore +from tellcorenet import TellCoreClient import voluptuous as vol -from homeassistant.helpers import discovery +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT -from homeassistant.helpers.entity import Entity +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -73,10 +82,6 @@ def _discover(hass, config, component_name, found_tellcore_devices): def setup(hass, config): """Set up the Tellstick component.""" - from tellcore.constants import TELLSTICK_DIM, TELLSTICK_UP - from tellcore.telldus import AsyncioCallbackDispatcher - from tellcore.telldus import TelldusCore - from tellcorenet import TellCoreClient conf = config.get(DOMAIN, {}) net_host = conf.get(CONF_HOST) @@ -219,7 +224,6 @@ def _send_device_command(self, requested_state, requested_data): def _send_repeated_command(self): """Send a tellstick command once and decrease the repeat count.""" - from tellcore.library import TelldusError with TELLSTICK_LOCK: if self._repeats_left > 0: @@ -259,11 +263,6 @@ def turn_off(self, **kwargs): def _update_model_from_command(self, tellcore_command, tellcore_data): """Update the model, from a sent tellcore command and data.""" - from tellcore.constants import ( - TELLSTICK_TURNON, - TELLSTICK_TURNOFF, - TELLSTICK_DIM, - ) if tellcore_command not in [TELLSTICK_TURNON, TELLSTICK_TURNOFF, TELLSTICK_DIM]: _LOGGER.debug("Unhandled tellstick command: %d", tellcore_command) @@ -289,12 +288,6 @@ def update_from_callback(self, tellcore_id, tellcore_command, tellcore_data): def _update_from_tellcore(self): """Read the current state of the device from the tellcore library.""" - from tellcore.library import TelldusError - from tellcore.constants import ( - TELLSTICK_TURNON, - TELLSTICK_TURNOFF, - TELLSTICK_DIM, - ) with TELLSTICK_LOCK: try: diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 98d162d6d81b5d..1a55e67ac43de7 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -1,13 +1,15 @@ """Support for Tellstick sensors.""" -import logging from collections import namedtuple +import logging +from tellcore import telldus +import tellcore.constants as tellcore_constants import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_ID, CONF_NAME, CONF_PROTOCOL -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_ID, CONF_NAME, CONF_PROTOCOL, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -48,8 +50,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tellstick sensors.""" - from tellcore import telldus - import tellcore.constants as tellcore_constants sensor_value_descriptions = { tellcore_constants.TELLSTICK_TEMPERATURE: DatatypeDescription( From 0235487a229a871010a45edc7d60ec583396e450 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sun, 13 Oct 2019 22:56:01 +0200 Subject: [PATCH 0859/3953] Move top level imports (#27597) --- homeassistant/components/tradfri/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index c719fa4161423b..bdfabb4b00a3fd 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -2,6 +2,8 @@ import logging import voluptuous as vol +from pytradfri import Gateway, RequestError +from pytradfri.api.aiocoap_api import APIFactory import homeassistant.helpers.config_validation as cv from homeassistant import config_entries @@ -91,8 +93,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Create a gateway.""" # host, identity, key, allow_tradfri_groups - from pytradfri import Gateway, RequestError # pylint: disable=import-error - from pytradfri.api.aiocoap_api import APIFactory factory = APIFactory( entry.data[CONF_HOST], From 9fb0812ce5bdfb3dd4f47656f3406ba0bc2cae69 Mon Sep 17 00:00:00 2001 From: Santobert Date: Sun, 13 Oct 2019 22:56:34 +0200 Subject: [PATCH 0860/3953] Improve neato tests (#27578) * Improve tests * Rename account to configflow * configflow to config_flow * Patch pybotvac instead of own code --- homeassistant/components/neato/__init__.py | 13 ++--- tests/components/neato/test_init.py | 59 ++++++++++++++++++++-- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 839c24568d8d1b..ddf9789f678fa2 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -92,10 +92,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up config entry.""" - if entry.data[CONF_VENDOR] == "neato": - hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Neato) - elif entry.data[CONF_VENDOR] == "vorwerk": - hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Vorwerk) + hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account) hub = hass.data[NEATO_LOGIN] await hass.async_add_executor_job(hub.login) @@ -132,12 +129,16 @@ async def async_unload_entry(hass, entry): class NeatoHub: """A My Neato hub wrapper class.""" - def __init__(self, hass, domain_config, neato, vendor): + def __init__(self, hass, domain_config, neato): """Initialize the Neato hub.""" self.config = domain_config self._neato = neato self._hass = hass - self._vendor = vendor + + if self.config[CONF_VENDOR] == "vorwerk": + self._vendor = Vorwerk() + else: # Neato + self._vendor = Neato() self.my_neato = None self.logged_in = False diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py index 361f9eab1dbadf..444cbe8cc5d462 100644 --- a/tests/components/neato/test_init.py +++ b/tests/components/neato/test_init.py @@ -2,6 +2,8 @@ import pytest from unittest.mock import patch +from pybotvac.exceptions import NeatoLoginException + from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component @@ -20,6 +22,12 @@ CONF_VENDOR: VENDOR_NEATO, } +DIFFERENT_CONFIG = { + CONF_USERNAME: "anotherUsername", + CONF_PASSWORD: "anotherPassword", + CONF_VENDOR: VENDOR_VORWERK, +} + INVALID_CONFIG = { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, @@ -27,20 +35,40 @@ } -@pytest.fixture(name="account") -def mock_controller_login(): +@pytest.fixture(name="config_flow") +def mock_config_flow_login(): """Mock a successful login.""" with patch("homeassistant.components.neato.config_flow.Account", return_value=True): yield +@pytest.fixture(name="hub") +def mock_controller_login(): + """Mock a successful login.""" + with patch("homeassistant.components.neato.Account", return_value=True): + yield + + async def test_no_config_entry(hass): """There is nothing in configuration.yaml.""" res = await async_setup_component(hass, NEATO_DOMAIN, {}) assert res is True -async def test_config_entries_in_sync(hass, account): +async def test_create_valid_config_entry(hass, config_flow, hub): + """There is something in configuration.yaml.""" + assert hass.config_entries.async_entries(NEATO_DOMAIN) == [] + assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO + + +async def test_config_entries_in_sync(hass, hub): """The config entry and configuration.yaml are in sync.""" MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) @@ -55,9 +83,9 @@ async def test_config_entries_in_sync(hass, account): assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO -async def test_config_entries_not_in_sync(hass, account): +async def test_config_entries_not_in_sync(hass, config_flow, hub): """The config entry and configuration.yaml are not in sync.""" - MockConfigEntry(domain=NEATO_DOMAIN, data=INVALID_CONFIG).add_to_hass(hass) + MockConfigEntry(domain=NEATO_DOMAIN, data=DIFFERENT_CONFIG).add_to_hass(hass) assert hass.config_entries.async_entries(NEATO_DOMAIN) assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) @@ -68,3 +96,24 @@ async def test_config_entries_not_in_sync(hass, account): assert entries[0].data[CONF_USERNAME] == USERNAME assert entries[0].data[CONF_PASSWORD] == PASSWORD assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO + + +async def test_config_entries_not_in_sync_error(hass): + """The config entry and configuration.yaml are not in sync, the new configuration is wrong.""" + MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) + + assert hass.config_entries.async_entries(NEATO_DOMAIN) + with patch( + "homeassistant.components.neato.config_flow.Account", + side_effect=NeatoLoginException(), + ): + assert not await async_setup_component( + hass, NEATO_DOMAIN, {NEATO_DOMAIN: DIFFERENT_CONFIG} + ) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO From 3454b6fa877f22e7b25e59d1d27ebcd5e39e36ab Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sun, 13 Oct 2019 22:59:28 +0200 Subject: [PATCH 0861/3953] Refactor Tradfri base class (#27589) * Refactor Tradfri base class * Clarify doc * Fix pylint * Review fix * Move --- .../components/tradfri/base_class.py | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 8430a342c09d3c..632ce6b164ed64 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -10,8 +10,11 @@ _LOGGER = logging.getLogger(__name__) -class TradfriBaseDevice(Entity): - """Base class for a TRADFRI device.""" +class TradfriBaseClass(Entity): + """Base class for IKEA TRADFRI. + + All devices and groups should ultimately inherit from this class. + """ def __init__(self, device, api, gateway_id): """Initialize a device.""" @@ -54,20 +57,6 @@ def available(self): """Return True if entity is available.""" return self._available - @property - def device_info(self): - """Return the device info.""" - info = self._device.device_info - - return { - "identifiers": {(DOMAIN, self._device.id)}, - "manufacturer": info.manufacturer, - "model": info.model_number, - "name": self._name, - "sw_version": info.firmware_version, - "via_device": (DOMAIN, self._gateway_id), - } - @property def name(self): """Return the display name of this device.""" @@ -94,3 +83,24 @@ def _refresh(self, device): self._device = device self._name = device.name self._available = device.reachable + + +class TradfriBaseDevice(TradfriBaseClass): + """Base class for a TRADFRI device. + + All devices should inherit from this class. + """ + + @property + def device_info(self): + """Return the device info.""" + info = self._device.device_info + + return { + "identifiers": {(DOMAIN, self._device.id)}, + "manufacturer": info.manufacturer, + "model": info.model_number, + "name": self._name, + "sw_version": info.firmware_version, + "via_device": (DOMAIN, self._gateway_id), + } From e866d769e8248440270d3e9a51e89d6049dd2634 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Oct 2019 14:16:27 -0700 Subject: [PATCH 0862/3953] Google Assistant Local SDK (#27428) * Local Google * Fix test * Fix tests --- homeassistant/components/cloud/client.py | 9 +- homeassistant/components/cloud/const.py | 1 + .../components/cloud/google_config.py | 27 +++- homeassistant/components/cloud/prefs.py | 35 ++++- .../components/google_assistant/helpers.py | 95 +++++++++++- .../components/google_assistant/smart_home.py | 55 +++++-- homeassistant/components/http/__init__.py | 1 + homeassistant/components/webhook/__init__.py | 6 +- homeassistant/components/zeroconf/__init__.py | 17 ++- tests/common.py | 1 - tests/components/cloud/__init__.py | 2 +- tests/components/cloud/test_client.py | 4 +- tests/components/cloud/test_http_api.py | 20 +-- tests/components/google_assistant/__init__.py | 21 +++ .../google_assistant/test_helpers.py | 130 +++++++++++++++++ .../google_assistant/test_smart_home.py | 136 +++++++++++++++++- .../components/google_assistant/test_trait.py | 4 +- 17 files changed, 512 insertions(+), 52 deletions(-) create mode 100644 tests/components/google_assistant/test_helpers.py diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 38ae09ced9399b..c7626777943da1 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -110,14 +110,17 @@ async def async_initialize(self, cloud) -> None: if not self.cloud.is_logged_in: return - if self.alexa_config.should_report_state: + if self.alexa_config.enabled and self.alexa_config.should_report_state: try: await self.alexa_config.async_enable_proactive_mode() except alexa_errors.NoTokenAvailable: pass - if self.google_config.should_report_state: - self.google_config.async_enable_report_state() + if self.google_config.enabled: + self.google_config.async_enable_local_sdk() + + if self.google_config.should_report_state: + self.google_config.async_enable_report_state() async def cleanups(self) -> None: """Cleanup some stuff after logout.""" diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index e28d75f017de74..6495cba23b7291 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -16,6 +16,7 @@ PREF_DISABLE_2FA = "disable_2fa" PREF_ALIASES = "aliases" PREF_SHOULD_EXPOSE = "should_expose" +PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id" DEFAULT_SHOULD_EXPOSE = True DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 38e4aec56e07ca..582fa0075504f8 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -63,6 +63,19 @@ def should_report_state(self): """Return if states should be proactively reported.""" return self._prefs.google_report_state + @property + def local_sdk_webhook_id(self): + """Return the local SDK webhook. + + Return None to disable the local SDK. + """ + return self._prefs.google_local_webhook_id + + @property + def local_sdk_user_id(self): + """Return the user ID to be used for actions received via the local SDK.""" + return self._prefs.cloud_user + def should_expose(self, state): """If a state object should be exposed.""" return self._should_expose_entity_id(state.entity_id) @@ -131,17 +144,19 @@ async def _async_prefs_updated(self, prefs): # State reporting is reported as a property on entities. # So when we change it, we need to sync all entities. await self.async_sync_entities() - return # If entity prefs are the same or we have filter in config.yaml, # don't sync. - if ( - self._cur_entity_prefs is prefs.google_entity_configs - or not self._config["filter"].empty_filter + elif ( + self._cur_entity_prefs is not prefs.google_entity_configs + and self._config["filter"].empty_filter ): - return + self.async_schedule_google_sync() - self.async_schedule_google_sync() + if self.enabled and not self.is_local_sdk_active: + self.async_enable_local_sdk() + elif not self.enabled and self.is_local_sdk_active: + self.async_disable_local_sdk() async def _handle_entity_registry_updated(self, event): """Handle when entity registry updated.""" diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index a8ff775a22788a..0599b00a8bd8ee 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -21,6 +21,7 @@ PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE, PREF_GOOGLE_REPORT_STATE, + PREF_GOOGLE_LOCAL_WEBHOOK_ID, DEFAULT_GOOGLE_REPORT_STATE, InvalidTrustedNetworks, InvalidTrustedProxies, @@ -59,6 +60,14 @@ async def async_initialize(self): self._prefs = prefs + if PREF_GOOGLE_LOCAL_WEBHOOK_ID not in self._prefs: + await self._save_prefs( + { + **self._prefs, + PREF_GOOGLE_LOCAL_WEBHOOK_ID: self._hass.components.webhook.async_generate_id(), + } + ) + @callback def async_listen_updates(self, listener): """Listen for updates to the preferences.""" @@ -79,6 +88,8 @@ async def async_update( google_report_state=_UNDEF, ): """Update user preferences.""" + prefs = {**self._prefs} + for key, value in ( (PREF_ENABLE_GOOGLE, google_enabled), (PREF_ENABLE_ALEXA, alexa_enabled), @@ -92,20 +103,17 @@ async def async_update( (PREF_GOOGLE_REPORT_STATE, google_report_state), ): if value is not _UNDEF: - self._prefs[key] = value + prefs[key] = value if remote_enabled is True and self._has_local_trusted_network: - self._prefs[PREF_ENABLE_REMOTE] = False + prefs[PREF_ENABLE_REMOTE] = False raise InvalidTrustedNetworks if remote_enabled is True and self._has_local_trusted_proxies: - self._prefs[PREF_ENABLE_REMOTE] = False + prefs[PREF_ENABLE_REMOTE] = False raise InvalidTrustedProxies - await self._store.async_save(self._prefs) - - for listener in self._listeners: - self._hass.async_create_task(async_create_catching_coro(listener(self))) + await self._save_prefs(prefs) async def async_update_google_entity_config( self, @@ -216,6 +224,11 @@ def google_entity_configs(self): """Return Google Entity configurations.""" return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {}) + @property + def google_local_webhook_id(self): + """Return Google webhook ID to receive local messages.""" + return self._prefs[PREF_GOOGLE_LOCAL_WEBHOOK_ID] + @property def alexa_entity_configs(self): """Return Alexa Entity configurations.""" @@ -262,3 +275,11 @@ def _has_local_trusted_proxies(self) -> bool: return True return False + + async def _save_prefs(self, prefs): + """Save preferences to disk.""" + self._prefs = prefs + await self._store.async_save(self._prefs) + + for listener in self._listeners: + self._hass.async_create_task(async_create_catching_coro(listener(self))) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 933f0c07999884..96b9b93d70a4e9 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -1,10 +1,15 @@ """Helper classes for Google Assistant integration.""" from asyncio import gather from collections.abc import Mapping -from typing import List +import logging +import pprint +from typing import List, Optional + +from aiohttp.web import json_response from homeassistant.core import Context, callback, HomeAssistant, State from homeassistant.helpers.event import async_call_later +from homeassistant.components import webhook from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, @@ -15,6 +20,7 @@ from . import trait from .const import ( + DOMAIN, DOMAIN_TO_GOOGLE_TYPES, CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED, @@ -24,6 +30,7 @@ from .error import SmartHomeError SYNC_DELAY = 15 +_LOGGER = logging.getLogger(__name__) class AbstractConfig: @@ -35,6 +42,7 @@ def __init__(self, hass): """Initialize abstract config.""" self.hass = hass self._google_sync_unsub = None + self._local_sdk_active = False @property def enabled(self): @@ -61,12 +69,30 @@ def is_reporting_state(self): """Return if we're actively reporting states.""" return self._unsub_report_state is not None + @property + def is_local_sdk_active(self): + """Return if we're actively accepting local messages.""" + return self._local_sdk_active + @property def should_report_state(self): """Return if states should be proactively reported.""" # pylint: disable=no-self-use return False + @property + def local_sdk_webhook_id(self): + """Return the local SDK webhook ID. + + Return None to disable the local SDK. + """ + return None + + @property + def local_sdk_user_id(self): + """Return the user ID to be used for actions received via the local SDK.""" + raise NotImplementedError + def should_expose(self, state) -> bool: """Return if entity should be exposed.""" raise NotImplementedError @@ -131,15 +157,66 @@ async def async_deactivate_report_state(self): Called when the user disconnects their account from Google. """ + @callback + def async_enable_local_sdk(self): + """Enable the local SDK.""" + webhook_id = self.local_sdk_webhook_id + + if webhook_id is None: + return + + webhook.async_register( + self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook + ) + + self._local_sdk_active = True + + @callback + def async_disable_local_sdk(self): + """Disable the local SDK.""" + if not self._local_sdk_active: + return + + webhook.async_unregister(self.hass, self.local_sdk_webhook_id) + self._local_sdk_active = False + + async def _handle_local_webhook(self, hass, webhook_id, request): + """Handle an incoming local SDK message.""" + from . import smart_home + + payload = await request.json() + + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("Received local message:\n%s\n", pprint.pformat(payload)) + + if not self.enabled: + return json_response(smart_home.turned_off_response(payload)) + + result = await smart_home.async_handle_message( + self.hass, self, self.local_sdk_user_id, payload + ) + + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("Responding to local message:\n%s\n", pprint.pformat(result)) + + return json_response(result) + class RequestData: """Hold data associated with a particular request.""" - def __init__(self, config: AbstractConfig, user_id: str, request_id: str): + def __init__( + self, + config: AbstractConfig, + user_id: str, + request_id: str, + devices: Optional[List[dict]], + ): """Initialize the request data.""" self.config = config self.request_id = request_id self.context = Context(user_id=user_id) + self.devices = devices def get_google_type(domain, device_class): @@ -234,6 +311,15 @@ async def sync_serialize(self): if aliases: device["name"]["nicknames"] = aliases + if self.config.is_local_sdk_active: + device["otherDeviceIds"] = [{"deviceId": self.entity_id}] + device["customData"] = { + "webhookId": self.config.local_sdk_webhook_id, + "httpPort": self.hass.config.api.port, + "httpSSL": self.hass.config.api.use_ssl, + "proxyDeviceId": self.config.agent_user_id, + } + for trt in traits: device["attributes"].update(trt.sync_attributes()) @@ -280,6 +366,11 @@ def query_serialize(self): return attrs + @callback + def reachable_device_serialize(self): + """Serialize entity for a REACHABLE_DEVICE response.""" + return {"verificationId": self.entity_id} + async def execute(self, data, command_payload): """Execute a command. diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index f9b311a3880879..0944c9532effdb 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -5,7 +5,7 @@ from homeassistant.util.decorator import Registry -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, __version__ from .const import ( ERR_PROTOCOL_ERROR, @@ -24,9 +24,7 @@ async def async_handle_message(hass, config, user_id, message): """Handle incoming API messages.""" - request_id: str = message.get("requestId") - - data = RequestData(config, user_id, request_id) + data = RequestData(config, user_id, message["requestId"], message.get("devices")) response = await _process(hass, data, message) @@ -67,6 +65,7 @@ async def _process(hass, data, message): if result is None: return None + return {"requestId": data.request_id, "payload": result} @@ -74,7 +73,7 @@ async def _process(hass, data, message): async def async_devices_sync(hass, data, payload): """Handle action.devices.SYNC request. - https://developers.google.com/actions/smarthome/create-app#actiondevicessync + https://developers.google.com/assistant/smarthome/develop/process-intents#SYNC """ hass.bus.async_fire( EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context @@ -84,7 +83,7 @@ async def async_devices_sync(hass, data, payload): *( entity.sync_serialize() for entity in async_get_entities(hass, data.config) - if data.config.should_expose(entity.state) + if entity.should_expose() ) ) @@ -100,7 +99,7 @@ async def async_devices_sync(hass, data, payload): async def async_devices_query(hass, data, payload): """Handle action.devices.QUERY request. - https://developers.google.com/actions/smarthome/create-app#actiondevicesquery + https://developers.google.com/assistant/smarthome/develop/process-intents#QUERY """ devices = {} for device in payload.get("devices", []): @@ -128,7 +127,7 @@ async def async_devices_query(hass, data, payload): async def handle_devices_execute(hass, data, payload): """Handle action.devices.EXECUTE request. - https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute + https://developers.google.com/assistant/smarthome/develop/process-intents#EXECUTE """ entities = {} results = {} @@ -196,12 +195,50 @@ async def handle_devices_execute(hass, data, payload): async def async_devices_disconnect(hass, data: RequestData, payload): """Handle action.devices.DISCONNECT request. - https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect + https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT """ await data.config.async_deactivate_report_state() return None +@HANDLERS.register("action.devices.IDENTIFY") +async def async_devices_identify(hass, data: RequestData, payload): + """Handle action.devices.IDENTIFY request. + + https://developers.google.com/assistant/smarthome/develop/local#implement_the_identify_handler + """ + return { + "device": { + "id": data.config.agent_user_id, + "isLocalOnly": True, + "isProxy": True, + "deviceInfo": { + "hwVersion": "UNKNOWN_HW_VERSION", + "manufacturer": "Home Assistant", + "model": "Home Assistant", + "swVersion": __version__, + }, + } + } + + +@HANDLERS.register("action.devices.REACHABLE_DEVICES") +async def async_devices_reachable(hass, data: RequestData, payload): + """Handle action.devices.REACHABLE_DEVICES request. + + https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect + """ + google_ids = set(dev["id"] for dev in (data.devices or [])) + + return { + "devices": [ + entity.reachable_device_serialize() + for entity in async_get_entities(hass, data.config) + if entity.entity_id in google_ids and entity.should_expose() + ] + } + + def turned_off_response(message): """Return a device turned off response.""" return { diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index a8aaa3390a73d3..1b61e74769f446 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -128,6 +128,7 @@ def __init__( """Initialize a new API config object.""" self.host = host self.port = port + self.use_ssl = use_ssl host = host.rstrip("/") if host.startswith(("http://", "https://")): diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index a12e55c771a61e..5a41bfa9851404 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -1,7 +1,7 @@ """Webhooks for Home Assistant.""" import logging -from aiohttp.web import Response +from aiohttp.web import Response, Request import voluptuous as vol from homeassistant.core import callback @@ -98,9 +98,11 @@ class WebhookView(HomeAssistantView): url = URL_WEBHOOK_PATH name = "api:webhook" requires_auth = False + cors_allowed = True - async def _handle(self, request, webhook_id): + async def _handle(self, request: Request, webhook_id): """Handle webhook call.""" + _LOGGER.debug("Handling webhook %s payload for %s", request.method, webhook_id) hass = request.app["hass"] return await async_handle_webhook(hass, webhook_id, request) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index af107a6ae0df10..2f9fb7b4580fa6 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -11,7 +11,11 @@ from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf from homeassistant import util -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, __version__ +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_START, + __version__, +) from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT _LOGGER = logging.getLogger(__name__) @@ -33,6 +37,7 @@ def setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" + zeroconf = Zeroconf() zeroconf_name = f"{hass.config.location_name}.{ZEROCONF_TYPE}" params = { @@ -58,9 +63,15 @@ def setup(hass, config): properties=params, ) - zeroconf = Zeroconf() + def zeroconf_hass_start(_event): + """Expose Home Assistant on zeroconf when it starts. + + Wait till started or otherwise HTTP is not up and running. + """ + _LOGGER.info("Starting Zeroconf broadcast") + zeroconf.register_service(info) - zeroconf.register_service(info) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, zeroconf_hass_start) def service_update(zeroconf, service_type, name, state_change): """Service state changed.""" diff --git a/tests/common.py b/tests/common.py index 0684e6daafcbef..40e02842146012 100644 --- a/tests/common.py +++ b/tests/common.py @@ -230,7 +230,6 @@ def get_test_instance_port(): return _TEST_INSTANCE_PORT -@ha.callback def async_mock_service(hass, domain, service, schema=None): """Set up a fake service & return a calls log list to this service.""" calls = [] diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index a0196cae32a49e..45ea4e43ee4347 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -25,4 +25,4 @@ def mock_cloud_prefs(hass, prefs={}): } prefs_to_set.update(prefs) hass.data[cloud.DOMAIN].client._prefs._prefs = prefs_to_set - return prefs_to_set + return hass.data[cloud.DOMAIN].client._prefs diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index b7ac5f4cffd8b0..054b38daffc2ed 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -61,7 +61,7 @@ async def test_handler_alexa(hass): async def test_handler_alexa_disabled(hass, mock_cloud_fixture): """Test handler Alexa when user has disabled it.""" - mock_cloud_fixture[PREF_ENABLE_ALEXA] = False + mock_cloud_fixture._prefs[PREF_ENABLE_ALEXA] = False cloud = hass.data["cloud"] resp = await cloud.client.async_alexa_message( @@ -125,7 +125,7 @@ async def test_handler_google_actions(hass): async def test_handler_google_actions_disabled(hass, mock_cloud_fixture): """Test handler Google Actions when user has disabled it.""" - mock_cloud_fixture[PREF_ENABLE_GOOGLE] = False + mock_cloud_fixture._prefs[PREF_ENABLE_GOOGLE] = False with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): assert await async_setup_component(hass, "cloud", {}) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 8e03fb82b2ce62..314db3a9e885a3 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -10,13 +10,7 @@ from homeassistant.core import State from homeassistant.auth.providers import trusted_networks as tn_auth -from homeassistant.components.cloud.const import ( - PREF_ENABLE_GOOGLE, - PREF_ENABLE_ALEXA, - PREF_GOOGLE_SECURE_DEVICES_PIN, - DOMAIN, - RequireRelink, -) +from homeassistant.components.cloud.const import DOMAIN, RequireRelink from homeassistant.components.google_assistant.helpers import GoogleEntity from homeassistant.components.alexa.entities import LightCapabilities from homeassistant.components.alexa import errors as alexa_errors @@ -474,9 +468,9 @@ async def test_websocket_update_preferences( hass, hass_ws_client, aioclient_mock, setup_api, mock_cloud_login ): """Test updating preference.""" - assert setup_api[PREF_ENABLE_GOOGLE] - assert setup_api[PREF_ENABLE_ALEXA] - assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] is None + assert setup_api.google_enabled + assert setup_api.alexa_enabled + assert setup_api.google_secure_devices_pin is None client = await hass_ws_client(hass) await client.send_json( { @@ -490,9 +484,9 @@ async def test_websocket_update_preferences( response = await client.receive_json() assert response["success"] - assert not setup_api[PREF_ENABLE_GOOGLE] - assert not setup_api[PREF_ENABLE_ALEXA] - assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] == "1234" + assert not setup_api.google_enabled + assert not setup_api.alexa_enabled + assert setup_api.google_secure_devices_pin == "1234" async def test_websocket_update_preferences_require_relink( diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 8049ac4b0dbcee..09522e9c86fc33 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -12,12 +12,23 @@ def __init__( should_expose=None, entity_config=None, hass=None, + local_sdk_webhook_id=None, + local_sdk_user_id=None, + enabled=True, ): """Initialize config.""" super().__init__(hass) self._should_expose = should_expose self._secure_devices_pin = secure_devices_pin self._entity_config = entity_config or {} + self._local_sdk_webhook_id = local_sdk_webhook_id + self._local_sdk_user_id = local_sdk_user_id + self._enabled = enabled + + @property + def enabled(self): + """Return if Google is enabled.""" + return self._enabled @property def secure_devices_pin(self): @@ -29,6 +40,16 @@ def entity_config(self): """Return secure devices pin.""" return self._entity_config + @property + def local_sdk_webhook_id(self): + """Return local SDK webhook id.""" + return self._local_sdk_webhook_id + + @property + def local_sdk_user_id(self): + """Return local SDK webhook id.""" + return self._local_sdk_user_id + def should_expose(self, state): """Expose it all.""" return self._should_expose is None or self._should_expose(state) diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py new file mode 100644 index 00000000000000..497b7b1f0ae8ad --- /dev/null +++ b/tests/components/google_assistant/test_helpers.py @@ -0,0 +1,130 @@ +"""Test Google Assistant helpers.""" +from unittest.mock import Mock +from homeassistant.setup import async_setup_component +from homeassistant.components.google_assistant import helpers +from homeassistant.components.google_assistant.const import EVENT_COMMAND_RECEIVED +from . import MockConfig + +from tests.common import async_capture_events, async_mock_service + + +async def test_google_entity_sync_serialize_with_local_sdk(hass): + """Test sync serialize attributes of a GoogleEntity.""" + hass.states.async_set("light.ceiling_lights", "off") + hass.config.api = Mock(port=1234, use_ssl=True) + config = MockConfig( + hass=hass, + local_sdk_webhook_id="mock-webhook-id", + local_sdk_user_id="mock-user-id", + ) + entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights")) + + serialized = await entity.sync_serialize() + assert "otherDeviceIds" not in serialized + assert "customData" not in serialized + + config.async_enable_local_sdk() + + serialized = await entity.sync_serialize() + assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] + assert serialized["customData"] == { + "httpPort": 1234, + "httpSSL": True, + "proxyDeviceId": None, + "webhookId": "mock-webhook-id", + } + + +async def test_config_local_sdk(hass, hass_client): + """Test the local SDK.""" + command_events = async_capture_events(hass, EVENT_COMMAND_RECEIVED) + turn_on_calls = async_mock_service(hass, "light", "turn_on") + hass.states.async_set("light.ceiling_lights", "off") + + assert await async_setup_component(hass, "webhook", {}) + + config = MockConfig( + hass=hass, + local_sdk_webhook_id="mock-webhook-id", + local_sdk_user_id="mock-user-id", + ) + + client = await hass_client() + + config.async_enable_local_sdk() + + resp = await client.post( + "/api/webhook/mock-webhook-id", + json={ + "inputs": [ + { + "context": {"locale_country": "US", "locale_language": "en"}, + "intent": "action.devices.EXECUTE", + "payload": { + "commands": [ + { + "devices": [{"id": "light.ceiling_lights"}], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": True}, + } + ], + } + ], + "structureData": {}, + }, + } + ], + "requestId": "mock-req-id", + }, + ) + assert resp.status == 200 + result = await resp.json() + assert result["requestId"] == "mock-req-id" + + assert len(command_events) == 1 + assert command_events[0].context.user_id == config.local_sdk_user_id + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].context is command_events[0].context + + config.async_disable_local_sdk() + + # Webhook is no longer active + resp = await client.post("/api/webhook/mock-webhook-id") + assert resp.status == 200 + assert await resp.read() == b"" + + +async def test_config_local_sdk_if_disabled(hass, hass_client): + """Test the local SDK.""" + assert await async_setup_component(hass, "webhook", {}) + + config = MockConfig( + hass=hass, + local_sdk_webhook_id="mock-webhook-id", + local_sdk_user_id="mock-user-id", + enabled=False, + ) + + client = await hass_client() + + config.async_enable_local_sdk() + + resp = await client.post( + "/api/webhook/mock-webhook-id", json={"requestId": "mock-req-id"} + ) + assert resp.status == 200 + result = await resp.json() + assert result == { + "payload": {"errorCode": "deviceTurnedOff"}, + "requestId": "mock-req-id", + } + + config.async_disable_local_sdk() + + # Webhook is no longer active + resp = await client.post("/api/webhook/mock-webhook-id") + assert resp.status == 200 + assert await resp.read() == b"" diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 6ecd4af446b01a..2f7fdb8e131297 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -3,7 +3,7 @@ import pytest from homeassistant.core import State, EVENT_CALL_SERVICE -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__ from homeassistant.setup import async_setup_component from homeassistant.components import camera from homeassistant.components.climate.const import ( @@ -734,3 +734,137 @@ async def test_trait_execute_adding_query_data(hass): ] }, } + + +async def test_identify(hass): + """Test identify message.""" + result = await sh.async_handle_message( + hass, + BASIC_CONFIG, + None, + { + "requestId": REQ_ID, + "inputs": [ + { + "intent": "action.devices.IDENTIFY", + "payload": { + "device": { + "mdnsScanData": { + "additionals": [ + { + "type": "TXT", + "class": "IN", + "name": "devhome._home-assistant._tcp.local", + "ttl": 4500, + "data": [ + "version=0.101.0.dev0", + "base_url=http://192.168.1.101:8123", + "requires_api_password=true", + ], + } + ] + } + }, + "structureData": {}, + }, + } + ], + "devices": [ + { + "id": "light.ceiling_lights", + "customData": { + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", + }, + } + ], + }, + ) + + assert result == { + "requestId": REQ_ID, + "payload": { + "device": { + "id": BASIC_CONFIG.agent_user_id, + "isLocalOnly": True, + "isProxy": True, + "deviceInfo": { + "hwVersion": "UNKNOWN_HW_VERSION", + "manufacturer": "Home Assistant", + "model": "Home Assistant", + "swVersion": __version__, + }, + } + }, + } + + +async def test_reachable_devices(hass): + """Test REACHABLE_DEVICES intent.""" + # Matching passed in device. + hass.states.async_set("light.ceiling_lights", "on") + + # Unsupported entity + hass.states.async_set("not_supported.entity", "something") + + # Excluded via config + hass.states.async_set("light.not_expose", "on") + + # Not passed in as google_id + hass.states.async_set("light.not_mentioned", "on") + + config = MockConfig( + should_expose=lambda state: state.entity_id != "light.not_expose" + ) + + result = await sh.async_handle_message( + hass, + config, + None, + { + "requestId": REQ_ID, + "inputs": [ + { + "intent": "action.devices.REACHABLE_DEVICES", + "payload": { + "device": { + "proxyDevice": { + "id": "6a04f0f7-6125-4356-a846-861df7e01497", + "customData": "{}", + "proxyData": "{}", + } + }, + "structureData": {}, + }, + } + ], + "devices": [ + { + "id": "light.ceiling_lights", + "customData": { + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", + }, + }, + { + "id": "light.not_expose", + "customData": { + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", + }, + }, + {"id": BASIC_CONFIG.agent_user_id, "customData": {}}, + ], + }, + ) + + assert result == { + "requestId": REQ_ID, + "payload": {"devices": [{"verificationId": "light.ceiling_lights"}]}, + } diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a5c527dacfe6e2..d6ec24a7867953 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -48,11 +48,11 @@ REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" -BASIC_DATA = helpers.RequestData(BASIC_CONFIG, "test-agent", REQ_ID) +BASIC_DATA = helpers.RequestData(BASIC_CONFIG, "test-agent", REQ_ID, None) PIN_CONFIG = MockConfig(secure_devices_pin="1234") -PIN_DATA = helpers.RequestData(PIN_CONFIG, "test-agent", REQ_ID) +PIN_DATA = helpers.RequestData(PIN_CONFIG, "test-agent", REQ_ID, None) async def test_brightness_light(hass): From fe7467cd5c894a87c723bad77c2c228d9027f607 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Mon, 14 Oct 2019 00:01:14 +0200 Subject: [PATCH 0863/3953] Update pyhomematic to 0.1.61 (#27620) --- homeassistant/components/homematic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 260e54e65c4b60..5db547e3f0a438 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -3,7 +3,7 @@ "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", "requirements": [ - "pyhomematic==0.1.60" + "pyhomematic==0.1.61" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index bf36c898b1ed2c..46a6916e7875ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1226,7 +1226,7 @@ pyhik==0.2.4 pyhiveapi==0.2.19.3 # homeassistant.components.homematic -pyhomematic==0.1.60 +pyhomematic==0.1.61 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3fa3024041b52..1c3444c37f9f2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.homematic -pyhomematic==0.1.60 +pyhomematic==0.1.61 # homeassistant.components.ipma pyipma==1.2.1 From b37f0ad8124f0077d699f218720148e167a420b7 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 14 Oct 2019 00:32:27 +0000 Subject: [PATCH 0864/3953] [ci skip] Translation update --- .../components/abode/.translations/en.json | 22 +++++++++++++++++++ .../components/abode/.translations/no.json | 22 +++++++++++++++++++ .../binary_sensor/.translations/es.json | 2 ++ .../components/cover/.translations/es.json | 10 +++++++++ .../components/cover/.translations/no.json | 4 ++-- .../components/cover/.translations/ru.json | 10 +++++++++ .../cover/.translations/zh-Hant.json | 10 +++++++++ .../components/deconz/.translations/no.json | 2 +- .../components/ecobee/.translations/no.json | 6 ++--- .../components/lock/.translations/es.json | 8 +++++++ .../components/lock/.translations/ru.json | 8 +++++++ .../lock/.translations/zh-Hant.json | 8 +++++++ .../components/plex/.translations/no.json | 4 ++-- .../components/soma/.translations/no.json | 2 +- .../transmission/.translations/no.json | 2 +- .../components/zha/.translations/no.json | 16 +++++++------- 16 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/abode/.translations/en.json create mode 100644 homeassistant/components/abode/.translations/no.json create mode 100644 homeassistant/components/cover/.translations/es.json create mode 100644 homeassistant/components/cover/.translations/ru.json create mode 100644 homeassistant/components/cover/.translations/zh-Hant.json create mode 100644 homeassistant/components/lock/.translations/es.json create mode 100644 homeassistant/components/lock/.translations/ru.json create mode 100644 homeassistant/components/lock/.translations/zh-Hant.json diff --git a/homeassistant/components/abode/.translations/en.json b/homeassistant/components/abode/.translations/en.json new file mode 100644 index 00000000000000..e8daeb22c0a75e --- /dev/null +++ b/homeassistant/components/abode/.translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of Abode is allowed." + }, + "error": { + "connection_error": "Unable to connect to Abode.", + "identifier_exists": "Account already registered.", + "invalid_credentials": "Invalid credentials." + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email Address" + }, + "title": "Fill in your Abode login information" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/no.json b/homeassistant/components/abode/.translations/no.json new file mode 100644 index 00000000000000..542381cbb64153 --- /dev/null +++ b/homeassistant/components/abode/.translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bare en enkelt konfigurasjon av Abode er tillatt." + }, + "error": { + "connection_error": "Kan ikke koble til Abode.", + "identifier_exists": "Kontoen er allerede registrert.", + "invalid_credentials": "Ugyldig brukerinformasjon" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-postadresse" + }, + "title": "Fyll ut innloggingsinformasjonen for Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/es.json b/homeassistant/components/binary_sensor/.translations/es.json index 8e2d326d9d3fe9..756a370ca3c870 100644 --- a/homeassistant/components/binary_sensor/.translations/es.json +++ b/homeassistant/components/binary_sensor/.translations/es.json @@ -53,6 +53,7 @@ "hot": "{entity_name} se est\u00e1 calentando", "light": "{entity_name} empez\u00f3 a detectar la luz", "locked": "{entity_name} bloqueado", + "moist": "{entity_name} se humedece", "moist\u00a7": "{entity_name} se humedeci\u00f3", "motion": "{entity_name} comenz\u00f3 a detectar movimiento", "moving": "{entity_name} empez\u00f3 a moverse", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} se sec\u00f3", "not_moving": "{entity_name} dej\u00f3 de moverse", "not_occupied": "{entity_name} no est\u00e1 ocupado", + "not_opened": "{nombre_de_la_entidad} cerrado", "not_plugged_in": "{entity_name} desconectado", "not_powered": "{entity_name} no est\u00e1 activado", "not_present": "{entity_name} no est\u00e1 presente", diff --git a/homeassistant/components/cover/.translations/es.json b/homeassistant/components/cover/.translations/es.json new file mode 100644 index 00000000000000..d0193b939a5db0 --- /dev/null +++ b/homeassistant/components/cover/.translations/es.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est\u00e1 cerrado", + "is_closing": "{entity_name} se est\u00e1 cerrando", + "is_open": "{entity_name} est\u00e1 abierto", + "is_opening": "{entity_name} se est\u00e1 abriendo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/no.json b/homeassistant/components/cover/.translations/no.json index ff37aa27d586f4..af567bcfcfcc3d 100644 --- a/homeassistant/components/cover/.translations/no.json +++ b/homeassistant/components/cover/.translations/no.json @@ -1,8 +1,8 @@ { "device_automation": { "condition_type": { - "is_closed": "{entity_name} er lukket", - "is_closing": "{entity_name} lukker", + "is_closed": "{entity_name} er stengt", + "is_closing": "{entity_name} stenges", "is_open": "{entity_name} er \u00e5pen", "is_opening": "{entity_name} \u00e5pnes" } diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json new file mode 100644 index 00000000000000..46456bb9464a2e --- /dev/null +++ b/homeassistant/components/cover/.translations/ru.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/zh-Hant.json b/homeassistant/components/cover/.translations/zh-Hant.json new file mode 100644 index 00000000000000..9723d1a0dd6743 --- /dev/null +++ b/homeassistant/components/cover/.translations/zh-Hant.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u5df2\u95dc\u9589", + "is_closing": "{entity_name} \u6b63\u5728\u95dc\u9589", + "is_open": "{entity_name} \u5df2\u958b\u555f", + "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 7d05a366cf2248..71fba6043f75b9 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -64,7 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt klikket", "remote_button_rotated": "Knappen roterte \"{subtype}\"", - "remote_button_rotation_stopped": "Knappe rotasjon \"{subtype}\" stoppet", + "remote_button_rotation_stopped": "Knapperotasjon \"{subtype}\" stoppet", "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", diff --git a/homeassistant/components/ecobee/.translations/no.json b/homeassistant/components/ecobee/.translations/no.json index 2bf141f6489288..efaa566c4240c1 100644 --- a/homeassistant/components/ecobee/.translations/no.json +++ b/homeassistant/components/ecobee/.translations/no.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare en ecobee-forekomst." + "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare \u00e9n ecobee-forekomst." }, "error": { "pin_request_failed": "Feil under foresp\u00f8rsel om PIN-kode fra ecobee. Kontroller at API-n\u00f8kkelen er riktig.", - "token_request_failed": "Feil ved foresp\u00f8rsel om tokener fra ecobee; Pr\u00f8v p\u00e5 nytt." + "token_request_failed": "Feil ved foresp\u00f8rsel om tokener fra ecobee: Pr\u00f8v p\u00e5 nytt." }, "step": { "authorize": { - "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nDeretter, trykk p\u00e5 Send.", + "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nTrykk deretter p\u00e5 Send.", "title": "Autoriser app p\u00e5 ecobee.com" }, "user": { diff --git a/homeassistant/components/lock/.translations/es.json b/homeassistant/components/lock/.translations/es.json new file mode 100644 index 00000000000000..5c23c270f61c1c --- /dev/null +++ b/homeassistant/components/lock/.translations/es.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_unlocked": "{entity_name} est\u00e1 desbloqueado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ru.json b/homeassistant/components/lock/.translations/ru.json new file mode 100644 index 00000000000000..f74df838ae5d42 --- /dev/null +++ b/homeassistant/components/lock/.translations/ru.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_unlocked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/zh-Hant.json b/homeassistant/components/lock/.translations/zh-Hant.json new file mode 100644 index 00000000000000..a423c4331b738e --- /dev/null +++ b/homeassistant/components/lock/.translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} \u5df2\u4e0a\u9396", + "is_unlocked": "{entity_name} \u5df2\u89e3\u9396" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index 18c4e865a84690..8ebd2b69bb93e8 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -24,7 +24,7 @@ "token": "Token (hvis n\u00f8dvendig)", "verify_ssl": "Verifisere SSL-sertifikat" }, - "title": "Plex server" + "title": "Plex-server" }, "select_server": { "data": { @@ -35,7 +35,7 @@ }, "start_website_auth": { "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv.", - "title": "Koble til Plex server" + "title": "Koble til Plex-server" }, "user": { "data": { diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json index 1ea53b778ea11a..c3d9d7e70d4268 100644 --- a/homeassistant/components/soma/.translations/no.json +++ b/homeassistant/components/soma/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Du kan bare konfigurere en Soma-konto.", + "already_setup": "Du kan bare konfigurere \u00e9n Soma-konto.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." }, diff --git a/homeassistant/components/transmission/.translations/no.json b/homeassistant/components/transmission/.translations/no.json index f6ddce2a4a799e..94044e692d954e 100644 --- a/homeassistant/components/transmission/.translations/no.json +++ b/homeassistant/components/transmission/.translations/no.json @@ -22,7 +22,7 @@ "port": "Port", "username": "Brukernavn" }, - "title": "Oppsett av klient for Transmission" + "title": "Oppsett av Transmission-klient" } }, "title": "Transmission" diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 18c4c3c9ff2a14..a70f5ad1c33443 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -19,8 +19,8 @@ }, "device_automation": { "action_type": { - "squawk": "Squawk", - "warn": "Advarer" + "squawk": "Varsle", + "warn": "Advar" }, "trigger_subtype": { "both_buttons": "Begge knapper", @@ -39,7 +39,7 @@ "face_4": "med ansikt 4 aktivert", "face_5": "med ansikt 5 aktivert", "face_6": "med ansikt 6 aktivert", - "face_any": "Med alle/angitte ansikt (er) aktivert", + "face_any": "Med alle/angitte ansikt(er) aktivert", "left": "Venstre", "open": "\u00c5pen", "right": "H\u00f8yre", @@ -47,7 +47,7 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { - "device_dropped": "Enheten ble brutt", + "device_dropped": "Enheten ble sluppet", "device_flipped": "Enheten snudd \"{subtype}\"", "device_knocked": "Enheten sl\u00e5tt \"{subtype}\"", "device_rotated": "Enheten roterte \"{subtype}\"", @@ -55,13 +55,13 @@ "device_slid": "Enheten skled \"{subtype}\"", "device_tilted": "Enheten skr\u00e5stilt", "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", - "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_press": "\"{subtype}\"-knappen ble holdt inne", "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\"{subtype}\"-knappen ble femdobbelt klikket", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble trykket fire ganger", + "remote_button_quintuple_press": "\"{subtype}\"-knappen ble trykket fem ganger", "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\"{subtype}\"-knappen ble trippel klikket" + "remote_button_triple_press": "\"{subtype}\"-knappen ble trippelklikket" } } } \ No newline at end of file From cf76f22c8933ccac9ccbaf2bfe7e97cb6c7e3feb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 13 Oct 2019 23:59:25 -0500 Subject: [PATCH 0865/3953] Rewrite Plex tests (#27624) --- tests/components/plex/mock_classes.py | 96 ++++-- tests/components/plex/test_config_flow.py | 339 +++++++++------------- 2 files changed, 217 insertions(+), 218 deletions(-) diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 87fb6df597182f..756249110ed962 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -1,35 +1,97 @@ """Mock classes used in tests.""" +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.components.plex.const import CONF_SERVER, CONF_SERVER_IDENTIFIER -MOCK_HOST_1 = "1.2.3.4" -MOCK_PORT_1 = 32400 -MOCK_HOST_2 = "4.3.2.1" -MOCK_PORT_2 = 32400 +MOCK_SERVERS = [ + { + CONF_HOST: "1.2.3.4", + CONF_PORT: 32400, + CONF_SERVER: "Plex Server 1", + CONF_SERVER_IDENTIFIER: "unique_id_123", + }, + { + CONF_HOST: "4.3.2.1", + CONF_PORT: 32400, + CONF_SERVER: "Plex Server 2", + CONF_SERVER_IDENTIFIER: "unique_id_456", + }, +] -class MockAvailableServer: # pylint: disable=too-few-public-methods - """Mock avilable server objects.""" +class MockResource: + """Mock a PlexAccount resource.""" - def __init__(self, name, client_id): + def __init__(self, index): """Initialize the object.""" - self.name = name - self.clientIdentifier = client_id # pylint: disable=invalid-name + self.name = MOCK_SERVERS[index][CONF_SERVER] + self.clientIdentifier = MOCK_SERVERS[index][ # pylint: disable=invalid-name + CONF_SERVER_IDENTIFIER + ] self.provides = ["server"] + self._mock_plex_server = MockPlexServer(index) + self._connections = [] + for connection in range(2): + self._connections.append(MockConnection(connection)) + + @property + def connections(self): + """Mock the resource connection listing method.""" + return self._connections + + def connect(self): + """Mock the resource connect method.""" + return self._mock_plex_server class MockConnection: # pylint: disable=too-few-public-methods """Mock a single account resource connection object.""" - def __init__(self, ssl): + def __init__(self, index, ssl=True): """Initialize the object.""" prefix = "https" if ssl else "http" - self.httpuri = f"{prefix}://{MOCK_HOST_1}:{MOCK_PORT_1}" - self.uri = "{prefix}://{MOCK_HOST_2}:{MOCK_PORT_2}" - self.local = True + self.httpuri = ( + f"http://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" + ) + self.uri = f"{prefix}://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" + # Only first server is local + self.local = not bool(index) + + +class MockPlexAccount: + """Mock a PlexAccount instance.""" + + def __init__(self, servers=1): + """Initialize the object.""" + self._resources = [] + for index in range(servers): + self._resources.append(MockResource(index)) + + def resource(self, name): + """Mock the PlexAccount resource lookup method.""" + return [x for x in self._resources if x.name == name][0] + + def resources(self): + """Mock the PlexAccount resources listing method.""" + return self._resources -class MockConnections: # pylint: disable=too-few-public-methods - """Mock a list of resource connections.""" +class MockPlexServer: + """Mock a PlexServer instance.""" - def __init__(self, ssl=False): + def __init__(self, index=0, ssl=True): """Initialize the object.""" - self.connections = [MockConnection(ssl)] + host = MOCK_SERVERS[index][CONF_HOST] + port = MOCK_SERVERS[index][CONF_PORT] + self.friendlyName = MOCK_SERVERS[index][ # pylint: disable=invalid-name + CONF_SERVER + ] + self.machineIdentifier = MOCK_SERVERS[index][ # pylint: disable=invalid-name + CONF_SERVER_IDENTIFIER + ] + prefix = "https" if ssl else "http" + self._baseurl = f"{prefix}://{host}:{port}" + + @property + def url_in_use(self): + """Return URL used by PlexServer.""" + return self._baseurl diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index e9f48f6a4f80b4..2a2178da9d522b 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,28 +1,26 @@ """Tests for Plex config flow.""" -from unittest.mock import MagicMock, Mock, patch, PropertyMock +from unittest.mock import patch import asynctest import plexapi.exceptions import requests.exceptions from homeassistant.components.plex import config_flow -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_TOKEN, CONF_URL from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -from .mock_classes import MOCK_HOST_1, MOCK_PORT_1, MockAvailableServer, MockConnections +from .mock_classes import MOCK_SERVERS, MockPlexAccount, MockPlexServer -MOCK_NAME_1 = "Plex Server 1" -MOCK_ID_1 = "unique_id_123" -MOCK_NAME_2 = "Plex Server 2" -MOCK_ID_2 = "unique_id_456" MOCK_TOKEN = "secret_token" MOCK_FILE_CONTENTS = { - f"{MOCK_HOST_1}:{MOCK_PORT_1}": {"ssl": False, "token": MOCK_TOKEN, "verify": True} + f"{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}": { + "ssl": False, + "token": MOCK_TOKEN, + "verify": True, + } } -MOCK_SERVER_1 = MockAvailableServer(MOCK_NAME_1, MOCK_ID_1) -MOCK_SERVER_2 = MockAvailableServer(MOCK_NAME_2, MOCK_ID_2) DEFAULT_OPTIONS = { config_flow.MP_DOMAIN: { @@ -41,25 +39,21 @@ def init_config_flow(hass): async def test_bad_credentials(hass): """Test when provided credentials are rejected.""" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "start_website_auth" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + + with patch( + "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value="BAD TOKEN" ): - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -74,31 +68,32 @@ async def test_import_file_from_discovery(hass): """Test importing a legacy file during discovery.""" file_host_and_port, file_config = list(MOCK_FILE_CONTENTS.items())[0] - used_url = f"http://{file_host_and_port}" + file_use_ssl = file_config[CONF_SSL] + file_prefix = "https" if file_use_ssl else "http" + used_url = f"{file_prefix}://{file_host_and_port}" + + mock_plex_server = MockPlexServer(ssl=file_use_ssl) - with patch("plexapi.server.PlexServer") as mock_plex_server, patch( + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "homeassistant.components.plex.config_flow.load_json", return_value=MOCK_FILE_CONTENTS, ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_ID_1 - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_NAME_1 - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=used_url) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "discovery"}, - data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, ) assert result["type"] == "create_entry" - assert result["title"] == MOCK_NAME_1 - assert result["data"][config_flow.CONF_SERVER] == MOCK_NAME_1 - assert result["data"][config_flow.CONF_SERVER_IDENTIFIER] == MOCK_ID_1 + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == mock_plex_server.machineIdentifier + ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] == used_url assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] @@ -112,7 +107,10 @@ async def test_discovery(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "discovery"}, - data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, ) assert result["type"] == "abort" assert result["reason"] == "discovery_no_file" @@ -128,7 +126,10 @@ async def test_discovery_while_in_progress(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "discovery"}, - data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -137,42 +138,28 @@ async def test_discovery_while_in_progress(hass): async def test_import_success(hass): """Test a successful configuration import.""" - mock_connections = MockConnections(ssl=True) - - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - - with patch("plexapi.server.PlexServer") as mock_plex_server: - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + mock_plex_server = MockPlexServer() + with patch("plexapi.server.PlexServer", return_value=mock_plex_server): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "import"}, data={ CONF_TOKEN: MOCK_TOKEN, - CONF_URL: f"https://{MOCK_HOST_1}:{MOCK_PORT_1}", + CONF_URL: f"https://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", }, ) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -188,7 +175,7 @@ async def test_import_bad_hostname(hass): context={"source": "import"}, data={ CONF_TOKEN: MOCK_TOKEN, - CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}", + CONF_URL: f"http://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", }, ) assert result["type"] == "form" @@ -205,19 +192,12 @@ async def test_unknown_exception(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer", side_effect=Exception - ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( - "plexauth.PlexAuth.token", return_value="MOCK_TOKEN" - ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception), asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch("plexauth.PlexAuth.token", return_value="MOCK_TOKEN"): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -237,18 +217,15 @@ async def test_no_servers_found(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[]) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" with patch( - "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0) ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -261,6 +238,8 @@ async def test_no_servers_found(hass): async def test_single_available_server(hass): """Test creating an entry with one server available.""" + mock_plex_server = MockPlexServer() + await async_setup_component(hass, "http", {"http": {}}) result = await hass.config_entries.flow.async_init( @@ -269,46 +248,28 @@ async def test_single_available_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer" - ) as mock_plex_server, asynctest.patch( - "plexauth.PlexAuth.initiate_auth" - ), asynctest.patch( + with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -316,6 +277,8 @@ async def test_single_available_server(hass): async def test_multiple_servers_with_selection(hass): """Test creating an entry with multiple servers available.""" + mock_plex_server = MockPlexServer() + await async_setup_component(hass, "http", {"http": {}}) result = await hass.config_entries.flow.async_init( @@ -324,31 +287,18 @@ async def test_multiple_servers_with_selection(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) - mm_plex_account.resource = Mock(return_value=mock_connections) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer" - ) as mock_plex_server, asynctest.patch( + with patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) + ), patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch( "plexauth.PlexAuth.initiate_auth" ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -357,18 +307,21 @@ async def test_multiple_servers_with_selection(hass): assert result["step_id"] == "select_server" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={config_flow.CONF_SERVER: MOCK_SERVER_1.name} + result["flow_id"], + user_input={ + config_flow.CONF_SERVER: MOCK_SERVERS[0][config_flow.CONF_SERVER] + }, ) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -376,13 +329,17 @@ async def test_multiple_servers_with_selection(hass): async def test_adding_last_unconfigured_server(hass): """Test automatically adding last unconfigured server when multiple servers on account.""" + mock_plex_server = MockPlexServer() + await async_setup_component(hass, "http", {"http": {}}) MockConfigEntry( domain=config_flow.DOMAIN, data={ - config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2, - config_flow.CONF_SERVER: MOCK_NAME_2, + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[1][ + config_flow.CONF_SERVER_IDENTIFIER + ], + config_flow.CONF_SERVER: MOCK_SERVERS[1][config_flow.CONF_SERVER], }, ).add_to_hass(hass) @@ -392,45 +349,32 @@ async def test_adding_last_unconfigured_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) - mm_plex_account.resource = Mock(return_value=mock_connections) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer" - ) as mock_plex_server, asynctest.patch( + with patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) + ), patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch( "plexauth.PlexAuth.initiate_auth" ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -438,32 +382,28 @@ async def test_adding_last_unconfigured_server(hass): async def test_already_configured(hass): """Test a duplicated successful flow.""" + mock_plex_server = MockPlexServer() + flow = init_config_flow(hass) MockConfigEntry( - domain=config_flow.DOMAIN, data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1} + domain=config_flow.DOMAIN, + data={ + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][ + config_flow.CONF_SERVER_IDENTIFIER + ] + }, ).add_to_hass(hass) - mock_connections = MockConnections() - - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - - with patch("plexapi.server.PlexServer") as mock_plex_server, asynctest.patch( - "plexauth.PlexAuth.initiate_auth" - ), asynctest.patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - + with patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): result = await flow.async_step_import( - {CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}"} + { + CONF_TOKEN: MOCK_TOKEN, + CONF_URL: f"http://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", + } ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -477,16 +417,20 @@ async def test_all_available_servers_configured(hass): MockConfigEntry( domain=config_flow.DOMAIN, data={ - config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1, - config_flow.CONF_SERVER: MOCK_NAME_1, + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][ + config_flow.CONF_SERVER_IDENTIFIER + ], + config_flow.CONF_SERVER: MOCK_SERVERS[0][config_flow.CONF_SERVER], }, ).add_to_hass(hass) MockConfigEntry( domain=config_flow.DOMAIN, data={ - config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2, - config_flow.CONF_SERVER: MOCK_NAME_2, + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[1][ + config_flow.CONF_SERVER_IDENTIFIER + ], + config_flow.CONF_SERVER: MOCK_SERVERS[1][config_flow.CONF_SERVER], }, ).add_to_hass(hass) @@ -496,20 +440,14 @@ async def test_all_available_servers_configured(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) - mm_plex_account.resource = Mock(return_value=mock_connections) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" with patch( - "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -557,13 +495,12 @@ async def test_external_timed_out(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=None ): - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -583,12 +520,12 @@ async def test_callback_view(hass, aiohttp_client): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - client = await aiohttp_client(hass.http.app) forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' From afa7e0bfe875ec2df146658f85eddf97e5816cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 07:01:40 +0200 Subject: [PATCH 0866/3953] fix: exception after kaiterra api call timeout (#27622) --- homeassistant/components/kaiterra/__init__.py | 20 ++++++++--------- .../components/kaiterra/air_quality.py | 8 +++---- homeassistant/components/kaiterra/api_data.py | 22 ++++++++----------- homeassistant/components/kaiterra/sensor.py | 8 +++---- 4 files changed, 24 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/kaiterra/__init__.py b/homeassistant/components/kaiterra/__init__.py index 8c61ad5418481a..d043dc15eafc26 100644 --- a/homeassistant/components/kaiterra/__init__.py +++ b/homeassistant/components/kaiterra/__init__.py @@ -1,35 +1,33 @@ """Support for Kaiterra devices.""" import voluptuous as vol -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers import config_validation as cv - from homeassistant.const import ( CONF_API_KEY, - CONF_DEVICES, CONF_DEVICE_ID, + CONF_DEVICES, + CONF_NAME, CONF_SCAN_INTERVAL, CONF_TYPE, - CONF_NAME, ) +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.event import async_track_time_interval +from .api_data import KaiterraApiData from .const import ( AVAILABLE_AQI_STANDARDS, - AVAILABLE_UNITS, AVAILABLE_DEVICE_TYPES, + AVAILABLE_UNITS, CONF_AQI_STANDARD, CONF_PREFERRED_UNITS, - DOMAIN, DEFAULT_AQI_STANDARD, DEFAULT_PREFERRED_UNIT, DEFAULT_SCAN_INTERVAL, + DOMAIN, KAITERRA_COMPONENTS, ) -from .api_data import KaiterraApiData - KAITERRA_DEVICE_SCHEMA = vol.Schema( { vol.Required(CONF_DEVICE_ID): cv.string, diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py index 70699de394cf5c..1de1a4bd6c5b05 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -1,16 +1,14 @@ """Support for Kaiterra Air Quality Sensors.""" from homeassistant.components.air_quality import AirQualityEntity - -from homeassistant.helpers.dispatcher import async_dispatcher_connect - from homeassistant.const import CONF_DEVICE_ID, CONF_NAME +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( - DOMAIN, - ATTR_VOC, ATTR_AQI_LEVEL, ATTR_AQI_POLLUTANT, + ATTR_VOC, DISPATCHER_KAITERRA, + DOMAIN, ) diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index 81e28438d567f8..e0f4d817e035bb 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -1,21 +1,17 @@ """Data for all Kaiterra devices.""" -from logging import getLogger - import asyncio - -import async_timeout +from logging import getLogger from aiohttp.client_exceptions import ClientResponseError +import async_timeout +from kaiterra_async_client import AQIStandard, KaiterraAPIClient, Units -from kaiterra_async_client import KaiterraAPIClient, AQIStandard, Units - +from homeassistant.const import CONF_API_KEY, CONF_DEVICE_ID, CONF_DEVICES, CONF_TYPE from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_DEVICE_ID, CONF_TYPE - from .const import ( - AQI_SCALE, AQI_LEVEL, + AQI_SCALE, CONF_AQI_STANDARD, CONF_PREFERRED_UNITS, DISPATCHER_KAITERRA, @@ -60,9 +56,10 @@ async def async_update(self) -> None: with async_timeout.timeout(10): data = await self._api.get_latest_sensor_readings(self._devices) except (ClientResponseError, asyncio.TimeoutError): - _LOGGER.debug("Couldn't fetch data") + _LOGGER.debug("Couldn't fetch data from Kaiterra API") self.data = {} async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + return _LOGGER.debug("New data retrieved: %s", data) @@ -102,8 +99,7 @@ async def async_update(self) -> None: device["aqi_pollutant"] = {"value": main_pollutant} self.data[self._devices_ids[i]] = device - - async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) except IndexError as err: _LOGGER.error("Parsing error %s", err) - async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index 4ff6435b64d8b0..e86d6f7d836443 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -1,11 +1,9 @@ """Support for Kaiterra Temperature ahn Humidity Sensors.""" -from homeassistant.helpers.entity import Entity - -from homeassistant.helpers.dispatcher import async_dispatcher_connect - from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity -from .const import DOMAIN, DISPATCHER_KAITERRA +from .const import DISPATCHER_KAITERRA, DOMAIN SENSORS = [ {"name": "Temperature", "prop": "rtemp", "device_class": "temperature"}, From 2cf3f6bffaa0782a5bfbf890f357c843dea6a204 Mon Sep 17 00:00:00 2001 From: "Steven D. Lander" <3169732+stevendlander@users.noreply.github.com> Date: Mon, 14 Oct 2019 01:02:01 -0400 Subject: [PATCH 0867/3953] Issue #27288 Moving imports to top for tesla component (#27618) --- homeassistant/components/tesla/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 4c90f0784af176..a08112d66b31f1 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -3,6 +3,8 @@ import logging import voluptuous as vol +from teslajsonpy import Controller as teslaAPI, TeslaException + from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -52,8 +54,6 @@ def setup(hass, base_config): """Set up of Tesla component.""" - from teslajsonpy import Controller as teslaAPI, TeslaException - config = base_config.get(DOMAIN) email = config.get(CONF_USERNAME) From da29c1125fac4fd9059ffec82899d65e751ef6be Mon Sep 17 00:00:00 2001 From: Moritz Fey Date: Mon, 14 Oct 2019 07:13:32 +0200 Subject: [PATCH 0868/3953] add content for services.yaml for ccomponent stream (#27610) --- homeassistant/components/stream/services.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/stream/services.yaml b/homeassistant/components/stream/services.yaml index e69de29bb2d1d6..c3b25e06348a2d 100644 --- a/homeassistant/components/stream/services.yaml +++ b/homeassistant/components/stream/services.yaml @@ -0,0 +1,15 @@ +record: + description: Make a .mp4 recording from a provided stream. + fields: + stream_source: + description: The input source for the stream. + example: "rtsp://my.stream.feed:554" + filename: + description: The file name string. + example: "/tmp/my_stream.mp4" + duration: + description: "Target recording length (in seconds). Default: 30" + example: 30 + lookback: + description: "Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream for stream_source. Default: 0" + example: 5 \ No newline at end of file From ff4e35e0ad86d2d569dfeda96277a723594a4f28 Mon Sep 17 00:00:00 2001 From: Askarov Rishat Date: Mon, 14 Oct 2019 11:12:08 +0300 Subject: [PATCH 0869/3953] Update yandex transport after api change (#27591) * yandex maps api changed ("threads" in "Transport" added), ya_ma=>0.3.8 bug_fixed * Update homeassistant/components/yandex_transport/sensor.py Co-Authored-By: Paulus Schoutsen * additional fix * reformat * fix mistake --- .../components/yandex_transport/manifest.json | 2 +- .../components/yandex_transport/sensor.py | 27 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../test_yandex_transport_sensor.py | 8 +- tests/fixtures/yandex_transport_reply.json | 3568 +++++++---------- 6 files changed, 1533 insertions(+), 2076 deletions(-) diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index 91267b43480b06..44dcf5b100c328 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -3,7 +3,7 @@ "name": "Yandex Transport", "documentation": "https://www.home-assistant.io/integrations/yandex_transport", "requirements": [ - "ya_ma==0.3.7" + "ya_ma==0.3.8" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 26311a4c72e58c..4bf634a61f407a 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -79,18 +79,21 @@ def update(self): transport_list = stop_metadata["Transport"] for transport in transport_list: route = transport["name"] - if self._routes and route not in self._routes: - # skip unnecessary route info - continue - if "Events" in transport["BriefSchedule"]: - for event in transport["BriefSchedule"]["Events"]: - if "Estimated" in event: - posix_time_next = int(event["Estimated"]["value"]) - if closer_time is None or closer_time > posix_time_next: - closer_time = posix_time_next - if route not in attrs: - attrs[route] = [] - attrs[route].append(event["Estimated"]["text"]) + for thread in transport["threads"]: + if self._routes and route not in self._routes: + # skip unnecessary route info + continue + if "Events" not in thread["BriefSchedule"]: + continue + for event in thread["BriefSchedule"]["Events"]: + if "Estimated" not in event: + continue + posix_time_next = int(event["Estimated"]["value"]) + if closer_time is None or closer_time > posix_time_next: + closer_time = posix_time_next + if route not in attrs: + attrs[route] = [] + attrs[route].append(event["Estimated"]["text"]) attrs[STOP_NAME] = stop_name attrs[ATTR_ATTRIBUTION] = ATTRIBUTION if closer_time is None: diff --git a/requirements_all.txt b/requirements_all.txt index 46a6916e7875ae..e4c608793b8286 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ xmltodict==0.12.0 xs1-api-client==2.3.5 # homeassistant.components.yandex_transport -ya_ma==0.3.7 +ya_ma==0.3.8 # homeassistant.components.yweather yahooweather==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c3444c37f9f2d..ec8477c9369c8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -626,7 +626,7 @@ withings-api==2.0.0b8 xmltodict==0.12.0 # homeassistant.components.yandex_transport -ya_ma==0.3.7 +ya_ma==0.3.8 # homeassistant.components.yweather yahooweather==0.10 diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index 50d945e7fae371..7997d01bd13a16 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -38,14 +38,14 @@ def mock_requester(): } FILTERED_ATTRS = { - "т36": ["21:43", "21:47", "22:02"], - "т47": ["21:40", "22:01"], - "м10": ["21:48", "22:00"], + "т36": ["16:10", "16:17", "16:26"], + "т47": ["16:09", "16:10"], + "м10": ["16:12", "16:20"], "stop_name": "7-й автобусный парк", "attribution": "Data provided by maps.yandex.ru", } -RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds") +RESULT_STATE = dt_util.utc_from_timestamp(1570972183).isoformat(timespec="seconds") async def assert_setup_sensor(hass, config, count=1): diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json index c5e4857297aa93..3189d7a9d9bcc0 100644 --- a/tests/fixtures/yandex_transport_reply.json +++ b/tests/fixtures/yandex_transport_reply.json @@ -1,2106 +1,1560 @@ { - "data": { - "geometries": [ - { - "type": "Point", - "coordinates": [ - 37.565280044, - 55.851959656 + "data": { + "geometries": [ + { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + } + ], + "geometry": { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + }, + "properties": { + "name": "7-й автобусный парк", + "description": "7-й автобусный парк", + "currentTime": 1570971868567, + "tzOffset": 10800, + "StopMetaData": { + "id": "stop__9639579", + "name": "7-й автобусный парк", + "type": "urban", + "region": { + "id": 213, + "type": 6, + "parent_id": 1, + "capital_id": 0, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow", + "native_name": "", + "iso_name": "RU MOW", + "is_main": true, + "en_name": "Moscow", + "short_en_name": "MSK", + "phone_code": "495 499", + "phone_code_old": "095", + "zip_code": "", + "population": 12506468, + "synonyms": "Moskau, Moskva", + "latitude": 55.753215, + "longitude": 37.622504, + "latitude_size": 0.878654, + "longitude_size": 1.164423, + "zoom": 10, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "weather", + "afisha", + "maps", + "tv", + "ad", + "etrain", + "subway", + "delivery", + "route" + ], + "ename": "moscow", + "bounds": [ + [ + 37.0402925, + 55.31141404514547 + ], + [ + 38.2047155, + 56.190068045145466 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву", + "dative": "Москве", + "directional": "", + "genitive": "Москвы", + "instrumental": "Москвой", + "locative": "", + "nominative": "Москва", + "preposition": "в", + "prepositional": "Москве" + }, + "parent": { + "id": 1, + "type": 5, + "parent_id": 3, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow-and-moscow-oblast", + "native_name": "", + "iso_name": "RU-MOS", + "is_main": true, + "en_name": "Moscow and Moscow Oblast", + "short_en_name": "RU-MOS", + "phone_code": "495 496 498 499", + "phone_code_old": "", + "zip_code": "", + "population": 7503385, + "synonyms": "Московская область, Подмосковье, Podmoskovye", + "latitude": 55.815792, + "longitude": 37.380031, + "latitude_size": 2.705659, + "longitude_size": 5.060749, + "zoom": 8, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 10716, + 10747, + 10758, + 20728, + 10740, + 10738, + 20523, + 10735, + 10734, + 10743, + 21622 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "moscow-and-moscow-oblast", + "bounds": [ + [ + 34.8496565, + 54.439456064325434 + ], + [ + 39.9104055, + 57.14511506432543 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву и Московскую область", + "dative": "Москве и Московской области", + "directional": "", + "genitive": "Москвы и Московской области", + "instrumental": "Москвой и Московской областью", + "locative": "", + "nominative": "Москва и Московская область", + "preposition": "в", + "prepositional": "Москве и Московской области" + }, + "parent": { + "id": 225, + "type": 3, + "parent_id": 10001, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "russia", + "native_name": "", + "iso_name": "RU", + "is_main": false, + "en_name": "Russia", + "short_en_name": "RU", + "phone_code": "7", + "phone_code_old": "", + "zip_code": "", + "population": 146880432, + "synonyms": "Russian Federation,Российская Федерация", + "latitude": 61.698653, + "longitude": 99.505405, + "latitude_size": 40.700127, + "longitude_size": 171.643239, + "zoom": 3, + "tzname": "", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 2, + 65, + 54, + 47, + 43, + 66, + 51, + 56, + 172, + 39, + 62 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "russia", + "bounds": [ + [ + 13.683785499999999, + 35.290400699917846 + ], + [ + -174.6729755, + 75.99052769991785 ] + ], + "names": { + "ablative": "", + "accusative": "Россию", + "dative": "России", + "directional": "", + "genitive": "России", + "instrumental": "Россией", + "locative": "", + "nominative": "Россия", + "preposition": "в", + "prepositional": "России" + } } - ], - "geometry": { - "type": "Point", - "coordinates": [ - 37.565280044, - 55.851959656 - ] + } }, - "properties": { - "name": "7-й автобусный парк", - "description": "7-й автобусный парк", - "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)", - "StopMetaData": { - "id": "stop__9639579", - "name": "7-й автобусный парк", - "type": "urban", - "region": { - "id": 213, - "type": 6, - "parent_id": 1, - "capital_id": 0, - "geo_parent_id": 0, - "city_id": 213, - "name": "moscow", - "native_name": "", - "iso_name": "RU MOW", - "is_main": true, - "en_name": "Moscow", - "short_en_name": "MSK", - "phone_code": "495 499", - "phone_code_old": "095", - "zip_code": "", - "population": 12506468, - "synonyms": "Moskau, Moskva", - "latitude": 55.753215, - "longitude": 37.622504, - "latitude_size": 0.878654, - "longitude_size": 1.164423, - "zoom": 10, - "tzname": "Europe/Moscow", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "weather", - "afisha", - "maps", - "tv", - "ad", - "etrain", - "subway", - "delivery", - "route" - ], - "ename": "moscow", - "bounds": [ - [ - 37.0402925, - 55.31141404514547 - ], - [ - 38.2047155, - 56.190068045145466 - ] - ], - "names": { - "ablative": "", - "accusative": "Москву", - "dative": "Москве", - "directional": "", - "genitive": "Москвы", - "instrumental": "Москвой", - "locative": "", - "nominative": "Москва", - "preposition": "в", - "prepositional": "Москве" + "Transport": [ + { + "lineId": "2036924720", + "name": "692", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570973441", + "tzOffset": 10800, + "text": "16:30" + }, + "vehicleId": "codd%5Fnew|144020%5F31402" + } + ], + "Frequency": { + "text": "1 ч", + "value": 3600, + "begin": { + "value": "1570938428", + "tzOffset": 10800, + "text": "6:47" + }, + "end": { + "value": "1570990628", + "tzOffset": 10800, + "text": "21:17" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036924720&ll=37.577436%2C55.828981&name=692&r=4037&type=bus", + "seoname": "692" + }, + { + "lineId": "2036924968", + "name": "82", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "34 мин", + "value": 2040, + "begin": { + "value": "1570944072", + "tzOffset": 10800, + "text": "8:21" + }, + "end": { + "value": "1570997592", + "tzOffset": 10800, + "text": "23:13" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036924968&ll=37.571504%2C55.816622&name=82&r=4164&type=bus", + "seoname": "82" + }, + { + "lineId": "2036925416", + "name": "194", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "12 мин", + "value": 720, + "begin": { + "value": "1570933976", + "tzOffset": 10800, + "text": "5:32" }, - "parent": { - "id": 1, - "type": 5, - "parent_id": 3, - "capital_id": 213, - "geo_parent_id": 0, - "city_id": 213, - "name": "moscow-and-moscow-oblast", - "native_name": "", - "iso_name": "RU-MOS", - "is_main": true, - "en_name": "Moscow and Moscow Oblast", - "short_en_name": "RU-MOS", - "phone_code": "495 496 498 499", - "phone_code_old": "", - "zip_code": "", - "population": 7503385, - "synonyms": "Московская область, Подмосковье, Podmoskovye", - "latitude": 55.815792, - "longitude": 37.380031, - "latitude_size": 2.705659, - "longitude_size": 5.060749, - "zoom": 8, - "tzname": "Europe/Moscow", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [ - 213, - 10716, - 10747, - 10758, - 20728, - 10740, - 10738, - 20523, - 10735, - 10734, - 10743, - 21622 - ], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "ad" - ], - "ename": "moscow-and-moscow-oblast", - "bounds": [ - [ - 34.8496565, - 54.439456064325434 - ], - [ - 39.9104055, - 57.14511506432543 - ] - ], - "names": { - "ablative": "", - "accusative": "Москву и Московскую область", - "dative": "Москве и Московской области", - "directional": "", - "genitive": "Москвы и Московской области", - "instrumental": "Москвой и Московской областью", - "locative": "", - "nominative": "Москва и Московская область", - "preposition": "в", - "prepositional": "Москве и Московской области" - }, - "parent": { - "id": 225, - "type": 3, - "parent_id": 10001, - "capital_id": 213, - "geo_parent_id": 0, - "city_id": 213, - "name": "russia", - "native_name": "", - "iso_name": "RU", - "is_main": false, - "en_name": "Russia", - "short_en_name": "RU", - "phone_code": "7", - "phone_code_old": "", - "zip_code": "", - "population": 146880432, - "synonyms": "Russian Federation,Российская Федерация", - "latitude": 61.698653, - "longitude": 99.505405, - "latitude_size": 40.700127, - "longitude_size": 171.643239, - "zoom": 3, - "tzname": "", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [ - 213, - 2, - 65, - 54, - 47, - 43, - 66, - 51, - 56, - 172, - 39, - 62 - ], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "ad" - ], - "ename": "russia", - "bounds": [ - [ - 13.683785499999999, - 35.290400699917846 - ], - [ - -174.6729755, - 75.99052769991785 - ] - ], - "names": { - "ablative": "", - "accusative": "Россию", - "dative": "России", - "directional": "", - "genitive": "России", - "instrumental": "Россией", - "locative": "", - "nominative": "Россия", - "preposition": "в", - "prepositional": "России" - } - } + "end": { + "value": "1571004356", + "tzOffset": 10800, + "text": "1:05" } - }, - "Transport": [ + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036925416&ll=37.544800%2C55.865286&name=194&r=3667&type=bus", + "seoname": "194" + }, + { + "lineId": "2036925728", + "name": "282", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ { - "lineId": "2036925416", - "name": "194", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036927196", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9648742", - "name": "Коровино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659860", - "tzOffset": 10800, - "text": "21:51" - } - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661840", - "tzOffset": 10800, - "text": "22:24" - } - } - ], - "departureTime": "21:51" - } - } - ], - "threadId": "2036927196", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9648742", - "name": "Коровино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659860", - "tzOffset": 10800, - "text": "21:51" - } - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661840", - "tzOffset": 10800, - "text": "22:24" - } - } - ], - "departureTime": "21:51" - } + "Estimated": { + "value": "1570971861", + "tzOffset": 10800, + "text": "16:04" + }, + "vehicleId": "codd%5Fnew|34854%5F9345401" }, { - "lineId": "213_114_bus_mosgortrans", - "name": "114", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_114_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568603405", - "tzOffset": 10800, - "text": "6:10" - }, - "end": { - "value": "1568672165", - "tzOffset": 10800, - "text": "1:16" - } - } - } - } - ], - "threadId": "213B_114_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568603405", - "tzOffset": 10800, - "text": "6:10" - }, - "end": { - "value": "1568672165", - "tzOffset": 10800, - "text": "1:16" - } - } - } + "Estimated": { + "value": "1570973231", + "tzOffset": 10800, + "text": "16:27" + }, + "vehicleId": "codd%5Fnew|37913%5F9225419" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570934963", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1571005163", + "tzOffset": 10800, + "text": "1:19" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036925728&ll=37.553526%2C55.860385&name=282&r=5779&type=bus", + "seoname": "282" + }, + { + "lineId": "2036926781", + "name": "154", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972424", + "tzOffset": 10800, + "text": "16:13" + }, + "vehicleId": "codd%5Fnew|1161539%5F191543" + }, + { + "Estimated": { + "value": "1570973620", + "tzOffset": 10800, + "text": "16:33" + }, + "vehicleId": "codd%5Fnew|58773%5F190599" + } + ], + "Frequency": { + "text": "20 мин", + "value": 1200, + "begin": { + "value": "1570938166", + "tzOffset": 10800, + "text": "6:42" + }, + "end": { + "value": "1571006446", + "tzOffset": 10800, + "text": "1:40" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036926781&ll=37.576158%2C55.846301&name=154&r=4917&type=bus", + "seoname": "154" + }, + { + "lineId": "2036926818", + "name": "994", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1570934327", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1571004527", + "tzOffset": 10800, + "text": "1:08" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036926818&ll=37.560060%2C55.868431&name=994&r=3637&type=bus", + "seoname": "994" + }, + { + "lineId": "2036926890", + "name": "466", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570937447", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1571008247", + "tzOffset": 10800, + "text": "2:10" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036926890&ll=37.564238%2C55.845050&name=466&r=4163&type=bus", + "seoname": "466" + }, + { + "lineId": "213_114_bus_mosgortrans", + "name": "114", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972913", + "tzOffset": 10800, + "text": "16:21" + }, + "vehicleId": "codd%5Fnew|1092230%5F191422" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570936205", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1571004965", + "tzOffset": 10800, + "text": "1:16" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_114_bus_mosgortrans&ll=37.508487%2C55.852137&name=114&r=3544&type=bus", + "seoname": "114" + }, + { + "lineId": "213_179_bus_mosgortrans", + "name": "179", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570971963", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|194519%5F31367" + }, + { + "Estimated": { + "value": "1570973105", + "tzOffset": 10800, + "text": "16:25" + }, + "vehicleId": "codd%5Fnew|56358%5F31365" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570936823", + "tzOffset": 10800, + "text": "6:20" + }, + "end": { + "value": "1571005583", + "tzOffset": 10800, + "text": "1:26" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_179_bus_mosgortrans&ll=37.526151%2C55.858031&name=179&r=4634&type=bus", + "seoname": "179" + }, + { + "lineId": "213_191m_minibus_default", + "name": "591", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972150", + "tzOffset": 10800, + "text": "16:09" + }, + "vehicleId": "codd%5Fnew|35595%5F9345307" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570934833", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1571005033", + "tzOffset": 10800, + "text": "1:17" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_191m_minibus_default&ll=37.510906%2C55.848214&name=591&r=3384&type=bus", + "seoname": "591" + }, + { + "lineId": "213_206m_minibus_default", + "name": "206к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570934039", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1571004239", + "tzOffset": 10800, + "text": "1:03" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_206m_minibus_default&ll=37.548997%2C55.864997&name=206%D0%BA&r=3515&type=bus", + "seoname": "206k" + }, + { + "lineId": "213_215_bus_mosgortrans", + "name": "215", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1570934076", + "tzOffset": 10800, + "text": "5:34" }, + "end": { + "value": "1571004276", + "tzOffset": 10800, + "text": "1:04" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_215_bus_mosgortrans&ll=37.543701%2C55.854527&name=215&r=2763&type=bus", + "seoname": "215" + }, + { + "lineId": "213_36_trolleybus_mosgortrans", + "name": "т36", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ { - "lineId": "213_154_bus_mosgortrans", - "name": "154", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_154_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642548", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659260", - "tzOffset": 10800, - "text": "21:41" - }, - "Estimated": { - "value": "1568659252", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1054764%5F191500" - }, - { - "Scheduled": { - "value": "1568660580", - "tzOffset": 10800, - "text": "22:03" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:41" - } - } - ], - "threadId": "213B_154_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642548", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659260", - "tzOffset": 10800, - "text": "21:41" - }, - "Estimated": { - "value": "1568659252", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1054764%5F191500" - }, - { - "Scheduled": { - "value": "1568660580", - "tzOffset": 10800, - "text": "22:03" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:41" - } + "Estimated": { + "value": "1570972236", + "tzOffset": 10800, + "text": "16:10" + }, + "vehicleId": "codd%5Fnew|1084830%5F430261" }, { - "lineId": "213_179_bus_mosgortrans", - "name": "179", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_179_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568659351", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|59832%5F31359" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661660", - "tzOffset": 10800, - "text": "22:21" - } - } - ], - "departureTime": "21:52" - } - } - ], - "threadId": "213B_179_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568659351", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|59832%5F31359" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661660", - "tzOffset": 10800, - "text": "22:21" - } - } - ], - "departureTime": "21:52" - } + "Estimated": { + "value": "1570972641", + "tzOffset": 10800, + "text": "16:17" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" }, { - "lineId": "213_191m_minibus_default", - "name": "591", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_191m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660525", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|38278%5F9345312" - } - ], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568602033", - "tzOffset": 10800, - "text": "5:47" - }, - "end": { - "value": "1568672233", - "tzOffset": 10800, - "text": "1:17" - } - } - } - } - ], - "threadId": "213A_191m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660525", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|38278%5F9345312" - } - ], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568602033", - "tzOffset": 10800, - "text": "5:47" - }, - "end": { - "value": "1568672233", - "tzOffset": 10800, - "text": "1:17" - } - } - } + "Estimated": { + "value": "1570973178", + "tzOffset": 10800, + "text": "16:26" + }, + "vehicleId": "codd%5Fnew|1084827%5F430255" + } + ], + "Frequency": { + "text": "12 мин", + "value": 720, + "begin": { + "value": "1570932741", + "tzOffset": 10800, + "text": "5:12" }, + "end": { + "value": "1571003121", + "tzOffset": 10800, + "text": "0:45" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_36_trolleybus_mosgortrans&ll=37.588604%2C55.859705&name=%D1%8236&r=5104&type=bus", + "seoname": "t36" + }, + { + "lineId": "213_47_trolleybus_mosgortrans", + "name": "т47", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ { - "lineId": "213_206m_minibus_default", - "name": "206к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_206m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568601239", - "tzOffset": 10800, - "text": "5:33" - }, - "end": { - "value": "1568671439", - "tzOffset": 10800, - "text": "1:03" - } - } - } - } - ], - "threadId": "213A_206m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568601239", - "tzOffset": 10800, - "text": "5:33" - }, - "end": { - "value": "1568671439", - "tzOffset": 10800, - "text": "1:03" - } - } - } + "Scheduled": { + "value": "1570972080", + "tzOffset": 10800, + "text": "16:08" + }, + "Estimated": { + "value": "1570972183", + "tzOffset": 10800, + "text": "16:09" + }, + "vehicleId": "codd%5Fnew|1132404%5F430361" }, { - "lineId": "213_215_bus_mosgortrans", - "name": "215", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_215_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "27 мин", - "value": 1620, - "begin": { - "value": "1568601276", - "tzOffset": 10800, - "text": "5:34" - }, - "end": { - "value": "1568671476", - "tzOffset": 10800, - "text": "1:04" - } - } - } - } - ], - "threadId": "213B_215_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "27 мин", - "value": 1620, - "begin": { - "value": "1568601276", - "tzOffset": 10800, - "text": "5:34" - }, - "end": { - "value": "1568671476", - "tzOffset": 10800, - "text": "1:04" - } - } - } + "Scheduled": { + "value": "1570972980", + "tzOffset": 10800, + "text": "16:23" + }, + "Estimated": { + "value": "1570972219", + "tzOffset": 10800, + "text": "16:10" + }, + "vehicleId": "codd%5Fnew|1136132%5F430358" }, { - "lineId": "213_282_bus_mosgortrans", - "name": "282", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_282_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9641102", - "name": "Улица Корнейчука" - }, - { - "id": "2532226085", - "name": "Метро Войковская" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659888", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|34874%5F9345408" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568602180", - "tzOffset": 10800, - "text": "5:49" - }, - "end": { - "value": "1568673460", - "tzOffset": 10800, - "text": "1:37" - } - } - } - } - ], - "threadId": "213A_282_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9641102", - "name": "Улица Корнейчука" - }, - { - "id": "2532226085", - "name": "Метро Войковская" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659888", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|34874%5F9345408" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568602180", - "tzOffset": 10800, - "text": "5:49" - }, - "end": { - "value": "1568673460", - "tzOffset": 10800, - "text": "1:37" - } - } - } + "Scheduled": { + "value": "1570973940", + "tzOffset": 10800, + "text": "16:39" + } + } + ], + "departureTime": "16:08" + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_47_trolleybus_mosgortrans&ll=37.588308%2C55.818685&name=%D1%8247&r=5359&type=bus", + "seoname": "t47" + }, + { + "lineId": "213_56_trolleybus_mosgortrans", + "name": "т56", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1570971900", + "tzOffset": 10800, + "text": "16:05" + }, + "Estimated": { + "value": "1570972560", + "tzOffset": 10800, + "text": "16:16" + }, + "vehicleId": "codd%5Fnew|1117148%5F430351" }, { - "lineId": "213_294m_minibus_default", - "name": "994", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_294m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9649459", - "name": "Метро Алтуфьево" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "30 мин", - "value": 1800, - "begin": { - "value": "1568601527", - "tzOffset": 10800, - "text": "5:38" - }, - "end": { - "value": "1568671727", - "tzOffset": 10800, - "text": "1:08" - } - } - } - } - ], - "threadId": "213A_294m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9649459", - "name": "Метро Алтуфьево" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "30 мин", - "value": 1800, - "begin": { - "value": "1568601527", - "tzOffset": 10800, - "text": "5:38" - }, - "end": { - "value": "1568671727", - "tzOffset": 10800, - "text": "1:08" - } - } - } + "Scheduled": { + "value": "1570972680", + "tzOffset": 10800, + "text": "16:18" + }, + "Estimated": { + "value": "1570973442", + "tzOffset": 10800, + "text": "16:30" + }, + "vehicleId": "codd%5Fnew|1080552%5F430302" }, { - "lineId": "213_36_trolleybus_mosgortrans", - "name": "т36", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_36_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642550", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9640641", - "name": "Дмитровское шоссе, 155" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - }, - "Estimated": { - "value": "1568659426", - "tzOffset": 10800, - "text": "21:43" - }, - "vehicleId": "codd%5Fnew|1084829%5F430260" - }, - { - "Scheduled": { - "value": "1568660520", - "tzOffset": 10800, - "text": "22:02" - }, - "Estimated": { - "value": "1568659656", - "tzOffset": 10800, - "text": "21:47" - }, - "vehicleId": "codd%5Fnew|1117016%5F430280" - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - }, - "Estimated": { - "value": "1568660538", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|1054576%5F430226" - } - ], - "departureTime": "21:48" - } - } - ], - "threadId": "213A_36_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642550", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9640641", - "name": "Дмитровское шоссе, 155" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - }, - "Estimated": { - "value": "1568659426", - "tzOffset": 10800, - "text": "21:43" - }, - "vehicleId": "codd%5Fnew|1084829%5F430260" - }, - { - "Scheduled": { - "value": "1568660520", - "tzOffset": 10800, - "text": "22:02" - }, - "Estimated": { - "value": "1568659656", - "tzOffset": 10800, - "text": "21:47" - }, - "vehicleId": "codd%5Fnew|1117016%5F430280" - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - }, - "Estimated": { - "value": "1568660538", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|1054576%5F430226" - } - ], - "departureTime": "21:48" - } + "Scheduled": { + "value": "1570973400", + "tzOffset": 10800, + "text": "16:30" + } + } + ], + "departureTime": "16:05" + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_56_trolleybus_mosgortrans&ll=37.551454%2C55.830147&name=%D1%8256&r=6304&type=bus", + "seoname": "t56" + }, + { + "lineId": "213_63_bus_mosgortrans", + "name": "63", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972434", + "tzOffset": 10800, + "text": "16:13" + }, + "vehicleId": "codd%5Fnew|38700%5F9215301" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1570934207", + "tzOffset": 10800, + "text": "5:36" }, + "end": { + "value": "1571003507", + "tzOffset": 10800, + "text": "0:51" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_63_bus_mosgortrans&ll=37.550792%2C55.872690&name=63&r=3057&type=bus", + "seoname": "63" + }, + { + "lineId": "213_677_bus_mosgortrans", + "name": "677", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ { - "lineId": "213_47_trolleybus_mosgortrans", - "name": "т47", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_47_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639568", - "name": "Бескудниковский переулок" - }, - { - "id": "stop__9641903", - "name": "Бескудниковский переулок" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - }, - "Estimated": { - "value": "1568659253", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1112219%5F430329" - }, - { - "Scheduled": { - "value": "1568660940", - "tzOffset": 10800, - "text": "22:09" - }, - "Estimated": { - "value": "1568660519", - "tzOffset": 10800, - "text": "22:01" - }, - "vehicleId": "codd%5Fnew|1139620%5F430382" - }, - { - "Scheduled": { - "value": "1568663580", - "tzOffset": 10800, - "text": "22:53" - } - } - ], - "departureTime": "21:53" - } - } - ], - "threadId": "213B_47_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639568", - "name": "Бескудниковский переулок" - }, - { - "id": "stop__9641903", - "name": "Бескудниковский переулок" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - }, - "Estimated": { - "value": "1568659253", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1112219%5F430329" - }, - { - "Scheduled": { - "value": "1568660940", - "tzOffset": 10800, - "text": "22:09" - }, - "Estimated": { - "value": "1568660519", - "tzOffset": 10800, - "text": "22:01" - }, - "vehicleId": "codd%5Fnew|1139620%5F430382" - }, - { - "Scheduled": { - "value": "1568663580", - "tzOffset": 10800, - "text": "22:53" - } - } - ], - "departureTime": "21:53" - } + "Scheduled": { + "value": "1570972200", + "tzOffset": 10800, + "text": "16:10" + }, + "Estimated": { + "value": "1570971838", + "tzOffset": 10800, + "text": "16:03" + }, + "vehicleId": "codd%5Fnew|58581%5F31321" }, { - "lineId": "213_56_trolleybus_mosgortrans", - "name": "т56", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_56_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639561", - "name": "Коровинское шоссе" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660675", - "tzOffset": 10800, - "text": "22:04" - }, - "vehicleId": "codd%5Fnew|146304%5F31207" - } - ], - "Frequency": { - "text": "8 мин", - "value": 480, - "begin": { - "value": "1568606244", - "tzOffset": 10800, - "text": "6:57" - }, - "end": { - "value": "1568670144", - "tzOffset": 10800, - "text": "0:42" - } - } - } - } - ], - "threadId": "213A_56_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639561", - "name": "Коровинское шоссе" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660675", - "tzOffset": 10800, - "text": "22:04" - }, - "vehicleId": "codd%5Fnew|146304%5F31207" - } - ], - "Frequency": { - "text": "8 мин", - "value": 480, - "begin": { - "value": "1568606244", - "tzOffset": 10800, - "text": "6:57" - }, - "end": { - "value": "1568670144", - "tzOffset": 10800, - "text": "0:42" - } - } - } + "Scheduled": { + "value": "1570972560", + "tzOffset": 10800, + "text": "16:16" + } }, { - "lineId": "213_63_bus_mosgortrans", - "name": "63", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_63_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|38921%5F9215306" - }, - { - "Estimated": { - "value": "1568660136", - "tzOffset": 10800, - "text": "21:55" - }, - "vehicleId": "codd%5Fnew|38918%5F9215303" - } - ], - "Frequency": { - "text": "17 мин", - "value": 1020, - "begin": { - "value": "1568600987", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568670227", - "tzOffset": 10800, - "text": "0:43" - } - } - } - } - ], - "threadId": "213A_63_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|38921%5F9215306" - }, - { - "Estimated": { - "value": "1568660136", - "tzOffset": 10800, - "text": "21:55" - }, - "vehicleId": "codd%5Fnew|38918%5F9215303" - } - ], - "Frequency": { - "text": "17 мин", - "value": 1020, - "begin": { - "value": "1568600987", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568670227", - "tzOffset": 10800, - "text": "0:43" - } - } - } + "Scheduled": { + "value": "1570972920", + "tzOffset": 10800, + "text": "16:22" + } + } + ], + "departureTime": "16:10" + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_677_bus_mosgortrans&ll=37.564191%2C55.866620&name=677&r=3386&type=bus", + "seoname": "677" + }, + { + "lineId": "213_78_trolleybus_mosgortrans", + "name": "т78", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570971984", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|59694%5F31155" }, { - "lineId": "213_677_bus_mosgortrans", - "name": "677", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_677_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639495", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|11731%5F31376" - } - ], - "Frequency": { - "text": "4 мин", - "value": 240, - "begin": { - "value": "1568600940", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568672640", - "tzOffset": 10800, - "text": "1:24" - } - } - } - } - ], - "threadId": "213B_677_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639495", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|11731%5F31376" - } - ], - "Frequency": { - "text": "4 мин", - "value": 240, - "begin": { - "value": "1568600940", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568672640", - "tzOffset": 10800, - "text": "1:24" - } - } - } + "Estimated": { + "value": "1570972003", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|55041%5F31116" }, { - "lineId": "213_692_bus_mosgortrans", - "name": "692", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036928706", - "EssentialStops": [ - { - "id": "3163417967", - "name": "Платформа Дегунино" - }, - { - "id": "3163417967", - "name": "Платформа Дегунино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568660280", - "tzOffset": 10800, - "text": "21:58" - }, - "Estimated": { - "value": "1568660255", - "tzOffset": 10800, - "text": "21:57" - }, - "vehicleId": "codd%5Fnew|63029%5F31485" - }, - { - "Scheduled": { - "value": "1568693340", - "tzOffset": 10800, - "text": "7:09" - } - }, - { - "Scheduled": { - "value": "1568696940", - "tzOffset": 10800, - "text": "8:09" - } - } - ], - "departureTime": "21:58" - } - } - ], - "threadId": "2036928706", - "EssentialStops": [ - { - "id": "3163417967", - "name": "Платформа Дегунино" - }, - { - "id": "3163417967", - "name": "Платформа Дегунино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568660280", - "tzOffset": 10800, - "text": "21:58" - }, - "Estimated": { - "value": "1568660255", - "tzOffset": 10800, - "text": "21:57" - }, - "vehicleId": "codd%5Fnew|63029%5F31485" - }, - { - "Scheduled": { - "value": "1568693340", - "tzOffset": 10800, - "text": "7:09" - } - }, - { - "Scheduled": { - "value": "1568696940", - "tzOffset": 10800, - "text": "8:09" - } - } - ], - "departureTime": "21:58" - } + "Estimated": { + "value": "1570972550", + "tzOffset": 10800, + "text": "16:15" + }, + "vehicleId": "codd%5Fnew|62710%5F31142" }, { - "lineId": "213_78_trolleybus_mosgortrans", - "name": "т78", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_78_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9887464", - "name": "9-я Северная линия" - }, - { - "id": "stop__9887464", - "name": "9-я Северная линия" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659620", - "tzOffset": 10800, - "text": "21:47" - }, - "Estimated": { - "value": "1568659898", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|147522%5F31184" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:47" - } - } - ], - "threadId": "213A_78_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9887464", - "name": "9-я Северная линия" - }, - { - "id": "stop__9887464", - "name": "9-я Северная линия" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659620", - "tzOffset": 10800, - "text": "21:47" - }, - "Estimated": { - "value": "1568659898", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|147522%5F31184" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:47" - } + "Estimated": { + "value": "1570973307", + "tzOffset": 10800, + "text": "16:28" + }, + "vehicleId": "codd%5Fnew|1037437%5F31144" }, { - "lineId": "213_82_bus_mosgortrans", - "name": "82", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036925244", - "EssentialStops": [ - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - }, - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - } - }, - { - "Scheduled": { - "value": "1568661780", - "tzOffset": 10800, - "text": "22:23" - } - }, - { - "Scheduled": { - "value": "1568663760", - "tzOffset": 10800, - "text": "22:56" - } - } - ], - "departureTime": "21:48" - } - } - ], - "threadId": "2036925244", - "EssentialStops": [ - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - }, - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - } - }, - { - "Scheduled": { - "value": "1568661780", - "tzOffset": 10800, - "text": "22:23" - } - }, - { - "Scheduled": { - "value": "1568663760", - "tzOffset": 10800, - "text": "22:56" - } - } - ], - "departureTime": "21:48" - } + "Estimated": { + "value": "1570973456", + "tzOffset": 10800, + "text": "16:30" + }, + "vehicleId": "codd%5Fnew|318517%5F31136" + } + ], + "Frequency": { + "text": "11 мин", + "value": 660, + "begin": { + "value": "1570937045", + "tzOffset": 10800, + "text": "6:24" }, + "end": { + "value": "1571002385", + "tzOffset": 10800, + "text": "0:33" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_78_trolleybus_mosgortrans&ll=37.569453%2C55.855402&name=%D1%8278&r=8810&type=bus", + "seoname": "t78" + }, + { + "lineId": "2465131598", + "name": "179к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570935230", + "tzOffset": 10800, + "text": "5:53" + }, + "end": { + "value": "1571003030", + "tzOffset": 10800, + "text": "0:43" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2465131598&ll=37.561423%2C55.871807&name=179%D0%BA&r=2787&type=bus", + "seoname": "179k" + }, + { + "lineId": "677k_bus_default", + "name": "677к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ { - "lineId": "2465131598", - "name": "179к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2465131758", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659500", - "tzOffset": 10800, - "text": "21:45" - } - }, - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - } - }, - { - "Scheduled": { - "value": "1568660880", - "tzOffset": 10800, - "text": "22:08" - } - } - ], - "departureTime": "21:45" - } - } - ], - "threadId": "2465131758", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659500", - "tzOffset": 10800, - "text": "21:45" - } - }, - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - } - }, - { - "Scheduled": { - "value": "1568660880", - "tzOffset": 10800, - "text": "22:08" - } - } - ], - "departureTime": "21:45" - } + "Scheduled": { + "value": "1570972560", + "tzOffset": 10800, + "text": "16:16" + }, + "Estimated": { + "value": "1570971986", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|1038096%5F31398" }, { - "lineId": "466_bus_default", - "name": "466", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "466B_bus_default", - "EssentialStops": [ - { - "id": "stop__9640546", - "name": "Станция Бескудниково" - }, - { - "id": "stop__9640545", - "name": "Станция Бескудниково" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568604647", - "tzOffset": 10800, - "text": "6:30" - }, - "end": { - "value": "1568675447", - "tzOffset": 10800, - "text": "2:10" - } - } - } - } - ], - "threadId": "466B_bus_default", - "EssentialStops": [ - { - "id": "stop__9640546", - "name": "Станция Бескудниково" - }, - { - "id": "stop__9640545", - "name": "Станция Бескудниково" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568604647", - "tzOffset": 10800, - "text": "6:30" - }, - "end": { - "value": "1568675447", - "tzOffset": 10800, - "text": "2:10" - } - } - } + "Scheduled": { + "value": "1570973280", + "tzOffset": 10800, + "text": "16:28" + }, + "Estimated": { + "value": "1570972342", + "tzOffset": 10800, + "text": "16:12" + }, + "vehicleId": "codd%5Fnew|58590%5F31348" }, { - "lineId": "677k_bus_default", - "name": "677к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "677kA_bus_default", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568660003", - "tzOffset": 10800, - "text": "21:53" - }, - "vehicleId": "codd%5Fnew|130308%5F31319" - }, - { - "Scheduled": { - "value": "1568661240", - "tzOffset": 10800, - "text": "22:14" - } - }, - { - "Scheduled": { - "value": "1568662500", - "tzOffset": 10800, - "text": "22:35" - } - } - ], - "departureTime": "21:52" - } - } - ], - "threadId": "677kA_bus_default", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568660003", - "tzOffset": 10800, - "text": "21:53" - }, - "vehicleId": "codd%5Fnew|130308%5F31319" - }, - { - "Scheduled": { - "value": "1568661240", - "tzOffset": 10800, - "text": "22:14" - } - }, - { - "Scheduled": { - "value": "1568662500", - "tzOffset": 10800, - "text": "22:35" - } - } - ], - "departureTime": "21:52" - } + "Scheduled": { + "value": "1570974000", + "tzOffset": 10800, + "text": "16:40" + }, + "Estimated": { + "value": "1570973387", + "tzOffset": 10800, + "text": "16:29" + }, + "vehicleId": "codd%5Fnew|58902%5F31316" + } + ], + "departureTime": "16:16" + } + } + ], + "uri": "ymapsbm1://transit/line?id=677k_bus_default&ll=37.565257%2C55.870397&name=677%D0%BA&r=2987&type=bus", + "seoname": "677k" + }, + { + "lineId": "m10_bus_default", + "name": "м10", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972343", + "tzOffset": 10800, + "text": "16:12" + }, + "vehicleId": "codd%5Fnew|62922%5F31434" }, { - "lineId": "m10_bus_default", - "name": "м10", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036926048", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659718", - "tzOffset": 10800, - "text": "21:48" - }, - "vehicleId": "codd%5Fnew|146260%5F31212" - }, - { - "Estimated": { - "value": "1568660422", - "tzOffset": 10800, - "text": "22:00" - }, - "vehicleId": "codd%5Fnew|13997%5F31247" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568606903", - "tzOffset": 10800, - "text": "7:08" - }, - "end": { - "value": "1568675183", - "tzOffset": 10800, - "text": "2:06" - } - } - } - } - ], - "threadId": "2036926048", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659718", - "tzOffset": 10800, - "text": "21:48" - }, - "vehicleId": "codd%5Fnew|146260%5F31212" - }, - { - "Estimated": { - "value": "1568660422", - "tzOffset": 10800, - "text": "22:00" - }, - "vehicleId": "codd%5Fnew|13997%5F31247" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568606903", - "tzOffset": 10800, - "text": "7:08" - }, - "end": { - "value": "1568675183", - "tzOffset": 10800, - "text": "2:06" - } - } - } + "Estimated": { + "value": "1570972813", + "tzOffset": 10800, + "text": "16:20" + }, + "vehicleId": "codd%5Fnew|57281%5F31242" } - ] - } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570939772", + "tzOffset": 10800, + "text": "7:09" + }, + "end": { + "value": "1571008052", + "tzOffset": 10800, + "text": "2:07" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=m10_bus_default&ll=37.579221%2C55.823763&name=%D0%BC10&r=8474&type=bus", + "seoname": "m10" + } + ] + } + }, + "searchResult": { + "requestId": "1570971868582853-530182592-man1-6817", + "title": "7-й автобусный парк", + "description": "Россия, Москва, Дмитровское шоссе", + "address": "Россия, Москва, Дмитровское шоссе", + "coordinates": [ + 37.56528, + 55.85196 + ], + "bounds": [ + [ + 37.543123, + 55.77889866 + ], + [ + 37.587437, + 55.92488366 + ] + ], + "displayCoordinates": [ + 37.56528, + 55.85196 + ], + "metro": [ + { + "id": "2244536395", + "name": "Верхние Лихоборы", + "distance": "510 м", + "distanceValue": 509.265, + "coordinates": [ + 37.56121218, + 55.854501501 + ], + "type": "metro", + "color": "#99cc33" + }, + { + "id": "1727539211", + "name": "Окружная", + "distance": "640 м", + "distanceValue": 641.333, + "coordinates": [ + 37.572849014, + 55.848814359 + ], + "type": "metro", + "color": "#ffa8af" + }, + { + "id": "2244535785", + "name": "Окружная", + "distance": "1,3 км", + "distanceValue": 1263.44, + "coordinates": [ + 37.575977155, + 55.844377845 + ], + "type": "metro", + "color": "#99cc33" + } + ], + "stops": [ + { + "id": "stop__9639579", + "name": "7-й автобусный парк", + "distance": "0 м", + "distanceValue": 0.0383997, + "coordinates": [ + 37.565280044, + 55.851959656 + ], + "type": "common" }, - "toponymSeoname": "dmitrovskoye_shosse" - } -} + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы", + "distance": "420 м", + "distanceValue": 424.274, + "coordinates": [ + 37.563047501, + 55.853727589 + ], + "type": "common" + }, + { + "id": "stop__9639678", + "name": "Метро Верхние Лихоборы (северный вестибюль)", + "distance": "630 м", + "distanceValue": 629.689, + "coordinates": [ + 37.562346735, + 55.857147019 + ], + "type": "common" + }, + { + "id": "station__lh_9601830", + "name": "Окружная", + "distance": "860 м", + "distanceValue": 857.487, + "coordinates": [ + 37.574303, + 55.847684 + ], + "type": "common" + }, + { + "id": "stop__9639906", + "name": "Платформа Окружная", + "distance": "930 м", + "distanceValue": 926.144, + "coordinates": [ + 37.576123886, + 55.847913668 + ], + "type": "common" + } + ], + "logId": "dHlwZT1iaXpmaW5kZXI7aWQ9MjM5MzY2OTUwNjU4", + "type": "business", + "id": "239366950658", + "shortTitle": "7-й автобусный парк", + "additionalAddress": "", + "fullAddress": "Россия, Москва, Дмитровское шоссе", + "postalCode": "", + "addressDetails": { + "locality": "Москва", + "street": "Дмитровское шоссе" + }, + "categories": [ + { + "name": "Остановка общественного транспорта", + "class": "bus stop", + "seoname": "public_transport_stop", + "pluralName": "Остановки общественного транспорта", + "id": "223677355200" + } + ], + "status": "open", + "businessLinks": [], + "businessProperties": { + "geoproduct_poi_color": "#ABAEB3", + "snippet_show_title": "short_title", + "snippet_show_rating": "five_star_rating", + "snippet_show_photo": "single_photo", + "snippet_show_eta": "show_eta", + "snippet_show_category": "single_category", + "snippet_show_subline": [ + "no_subline" + ], + "snippet_show_geoproduct_offer": "show_geoproduct_offer", + "snippet_show_bookmark": "show_bookmark", + "detailview_show_claim_organization": "not_show_claim_organization", + "detailview_show_reviews": "show_reviews", + "detailview_show_add_photo_button": "show_add_photo_button", + "detailview_show_taxi_button": "show_taxi_button", + "sensitive": "1" + }, + "seoname": "7_y_avtobusny_park", + "geoId": 117015, + "uri": "ymapsbm1://org?oid=239366950658", + "uriList": [ + "ymapsbm1://org?oid=239366950658", + "ymapsbm1://transit/stop?id=stop__9639579" + ], + "references": [ + { + "id": "2036929560", + "scope": "nyak" + } + ], + "ratingData": { + "ratingCount": 0, + "ratingValue": 0, + "reviewCount": 0 + }, + "sources": [ + { + "id": "yandex", + "name": "Яндекс", + "href": "https://www.yandex.ru" + } + ], + "analyticsId": "1" + }, + "toponymSeoname": "dmitrovskoye_shosse" + } +} \ No newline at end of file From b7023a96a3e5aa349e75c2790c1c14c7158f430f Mon Sep 17 00:00:00 2001 From: "Steven D. Lander" <3169732+stevendlander@users.noreply.github.com> Date: Mon, 14 Oct 2019 04:51:37 -0400 Subject: [PATCH 0870/3953] Issue #27288 Move imports to top for FFMPEG (#27613) --- homeassistant/components/ffmpeg/__init__.py | 2 +- homeassistant/components/ffmpeg/camera.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 51e1cac3859617..673a34230fc2f6 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -3,6 +3,7 @@ import re import voluptuous as vol +from haffmpeg.tools import FFVersion from homeassistant.core import callback from homeassistant.const import ( @@ -105,7 +106,6 @@ def binary(self): async def async_get_version(self): """Return ffmpeg version.""" - from haffmpeg.tools import FFVersion ffversion = FFVersion(self._bin, self.hass.loop) self._version = await ffversion.get_version() diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 598ffe36bd4a97..0f5001769339d8 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -3,6 +3,8 @@ import logging import voluptuous as vol +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import ImageFrame, IMAGE_JPEG from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_STREAM from homeassistant.const import CONF_NAME @@ -53,7 +55,6 @@ async def stream_source(self): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) @@ -66,7 +67,6 @@ async def async_camera_image(self): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) await stream.open_camera(self._input, extra_cmd=self._extra_arguments) From 1cae6e664c39f50c7254c80adf228e362e8099b9 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 14 Oct 2019 19:56:40 +1100 Subject: [PATCH 0871/3953] move imports to top-level (#27630) --- homeassistant/components/pushover/notify.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 83da9a657fea7c..3f78897838d293 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -1,7 +1,9 @@ """Pushover platform for notify component.""" import logging +import requests import voluptuous as vol +from pushover import InitError, Client, RequestError from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv @@ -28,8 +30,6 @@ def get_service(hass, config, discovery_info=None): """Get the Pushover notification service.""" - from pushover import InitError - try: return PushoverNotificationService( hass, config[CONF_USER_KEY], config[CONF_API_KEY] @@ -44,8 +44,6 @@ class PushoverNotificationService(BaseNotificationService): def __init__(self, hass, user_key, api_token): """Initialize the service.""" - from pushover import Client - self._hass = hass self._user_key = user_key self._api_token = api_token @@ -53,8 +51,6 @@ def __init__(self, hass, user_key, api_token): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from pushover import RequestError - # Make a copy and use empty dict if necessary data = dict(kwargs.get(ATTR_DATA) or {}) @@ -65,8 +61,6 @@ def send_message(self, message="", **kwargs): # If attachment is a URL, use requests to open it as a stream. if data[ATTR_ATTACHMENT].startswith("http"): try: - import requests - response = requests.get( data[ATTR_ATTACHMENT], stream=True, timeout=5 ) From 017a5a5b09c471c4c393eba281f77a252c62b241 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 14 Oct 2019 14:30:08 +0200 Subject: [PATCH 0872/3953] 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 42815d8c8ae779..5092010c49c385 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -18,7 +18,7 @@ schedules: always: true variables: - name: versionWheels - value: '1.3-3.7-alpine3.10' + value: '1.4-3.7-alpine3.10' resources: repositories: - repository: azure From 3c280565fa630fabe945129e60abc0e060a1eb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 14:59:26 +0200 Subject: [PATCH 0873/3953] move imports in synology_srm component (#27603) --- homeassistant/components/synology_srm/device_tracker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_srm/device_tracker.py b/homeassistant/components/synology_srm/device_tracker.py index b45b393e332d91..36306efa93e244 100644 --- a/homeassistant/components/synology_srm/device_tracker.py +++ b/homeassistant/components/synology_srm/device_tracker.py @@ -4,9 +4,10 @@ https://home-assistant.io/components/device_tracker.synology_srm/ """ import logging + +import synology_srm import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, @@ -14,12 +15,13 @@ ) from homeassistant.const import ( CONF_HOST, - CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_USERNAME, CONF_VERIFY_SSL, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -52,7 +54,6 @@ class SynologySrmDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - import synology_srm self.client = synology_srm.Client( host=config[CONF_HOST], From 14e3b3af6f10c71ed7cfe23a7c95cd314e040055 Mon Sep 17 00:00:00 2001 From: bouni Date: Mon, 14 Oct 2019 15:00:02 +0200 Subject: [PATCH 0874/3953] moved imports to top level (#27632) --- homeassistant/components/bt_smarthub/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index ece67e3b635b92..45b18b963c5427 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -1,15 +1,16 @@ """Support for BT Smart Hub (Sometimes referred to as BT Home Hub 6).""" import logging +import btsmarthub_devicelist import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -74,7 +75,6 @@ def _update_info(self): def get_bt_smarthub_data(self): """Retrieve data from BT Smart Hub and return parsed result.""" - import btsmarthub_devicelist # Request data from bt smarthub into a list of dicts. data = btsmarthub_devicelist.get_devicelist( From aefb807222f1ef949e01e8026b0c6f78c1923014 Mon Sep 17 00:00:00 2001 From: bouni Date: Mon, 14 Oct 2019 15:00:51 +0200 Subject: [PATCH 0875/3953] moved imports to top level (#27634) --- homeassistant/components/cisco_ios/device_tracker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index b442b24feb49e9..5a42ef1c8b80b2 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -1,15 +1,17 @@ """Support for Cisco IOS Routers.""" import logging +import re +from pexpect import pxssh import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -100,8 +102,6 @@ def _update_info(self): def _get_arp_data(self): """Open connection to the router and get arp entries.""" - from pexpect import pxssh - import re try: cisco_ssh = pxssh.pxssh() From 91c6cd96465ba92469bc7a05e5848f7abb2ebccc Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 15 Oct 2019 00:02:00 +1100 Subject: [PATCH 0876/3953] Move imports in darksky component (#27633) * move imports to top-level * modify patch path * removed unused mocks and patches --- homeassistant/components/darksky/sensor.py | 3 +-- homeassistant/components/darksky/weather.py | 3 +-- tests/components/darksky/test_sensor.py | 23 +++++++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index d4e7e7ec63a97c..cd8417e3e8467b 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import forecastio import voluptuous as vol from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout @@ -797,8 +798,6 @@ def __init__(self, api_key, latitude, longitude, units, language, interval): def _update(self): """Get the latest data from Dark Sky.""" - import forecastio - try: self.data = forecastio.load_forecast( self._api_key, diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index dc5708d12a0cbe..41f063399c1a3e 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import forecastio from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout import voluptuous as vol @@ -244,8 +245,6 @@ def __init__(self, api_key, latitude, longitude, units): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Dark Sky.""" - import forecastio - try: self.data = forecastio.load_forecast( self._api_key, self.latitude, self.longitude, units=self.requested_units diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index 23d16ed35f480e..be66b74c186d3d 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -112,7 +112,10 @@ def tearDown(self): # pylint: disable=invalid-name self.hass.stop() @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) + @patch( + "homeassistant.components.darksky.sensor.forecastio.load_forecast", + new=load_forecastMock, + ) def test_setup_with_config(self, mock_forecastio): """Test the platform setup with configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL) @@ -120,9 +123,7 @@ def test_setup_with_config(self, mock_forecastio): state = self.hass.states.get("sensor.dark_sky_summary") assert state is not None - @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) - def test_setup_with_invalid_config(self, mock_forecastio): + def test_setup_with_invalid_config(self): """Test the platform setup with invalid configuration.""" setup_component(self.hass, "sensor", INVALID_CONFIG_MINIMAL) @@ -130,7 +131,10 @@ def test_setup_with_invalid_config(self, mock_forecastio): assert state is None @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) + @patch( + "homeassistant.components.darksky.sensor.forecastio.load_forecast", + new=load_forecastMock, + ) def test_setup_with_language_config(self, mock_forecastio): """Test the platform setup with language configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_LANG_DE) @@ -138,9 +142,7 @@ def test_setup_with_language_config(self, mock_forecastio): state = self.hass.states.get("sensor.dark_sky_summary") assert state is not None - @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) - def test_setup_with_invalid_language_config(self, mock_forecastio): + def test_setup_with_invalid_language_config(self): """Test the platform setup with language configuration.""" setup_component(self.hass, "sensor", INVALID_CONFIG_LANG) @@ -164,7 +166,10 @@ def test_setup_bad_api_key(self, mock_get_forecast): assert not response @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) + @patch( + "homeassistant.components.darksky.sensor.forecastio.load_forecast", + new=load_forecastMock, + ) def test_setup_with_alerts_config(self, mock_forecastio): """Test the platform setup with alert configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_ALERTS) From d7d7f6a1c9db109b5ff2612f6940901f2dd390c0 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 14 Oct 2019 15:03:07 +0200 Subject: [PATCH 0877/3953] Fix temperature and heating mode (#27604) --- homeassistant/components/vicare/climate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 7010f943707303..0dcb83f758afc8 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -21,6 +21,7 @@ VICARE_MODE_DHW = "dhw" VICARE_MODE_DHWANDHEATING = "dhwAndHeating" +VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling" VICARE_MODE_FORCEDREDUCED = "forcedReduced" VICARE_MODE_FORCEDNORMAL = "forcedNormal" VICARE_MODE_OFF = "standby" @@ -46,6 +47,7 @@ VICARE_TO_HA_HVAC_HEATING = { VICARE_MODE_DHW: HVAC_MODE_OFF, VICARE_MODE_DHWANDHEATING: HVAC_MODE_AUTO, + VICARE_MODE_DHWANDHEATINGCOOLING: HVAC_MODE_AUTO, VICARE_MODE_FORCEDREDUCED: HVAC_MODE_OFF, VICARE_MODE_FORCEDNORMAL: HVAC_MODE_HEAT, VICARE_MODE_OFF: HVAC_MODE_OFF, @@ -200,9 +202,8 @@ def set_temperature(self, **kwargs): """Set new target temperatures.""" temp = kwargs.get(ATTR_TEMPERATURE) if temp is not None: - self._api.setProgramTemperature( - self._current_program, self._target_temperature - ) + self._api.setProgramTemperature(self._current_program, temp) + self._target_temperature = temp @property def preset_mode(self): From 79b391c673e61d069d9bbdd4e751a0aa8a0681d4 Mon Sep 17 00:00:00 2001 From: bouni Date: Mon, 14 Oct 2019 15:58:15 +0200 Subject: [PATCH 0878/3953] moved imports to top level (#27640) --- homeassistant/components/co2signal/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 9098a053fff2ae..7160d140b3f812 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -1,16 +1,17 @@ """Support for the CO2signal platform.""" import logging +import CO2Signal import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, - CONF_TOKEN, CONF_LATITUDE, CONF_LONGITUDE, + CONF_TOKEN, ) -from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity CONF_COUNTRY_CODE = "country_code" @@ -97,7 +98,6 @@ def device_state_attributes(self): def update(self): """Get the latest data and updates the states.""" - import CO2Signal _LOGGER.debug("Update data for %s", self._friendly_name) From a79a9809f492e8ff594f9ef3b2d9fdc046a0f081 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 14 Oct 2019 16:02:39 +0200 Subject: [PATCH 0879/3953] ESPHome Fix intermediary state published (#27638) Fixes https://github.com/esphome/issues/issues/426 --- homeassistant/components/esphome/__init__.py | 17 ++++++++++++++--- homeassistant/components/esphome/camera.py | 4 ++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index bc06aba94ead87..dd4ac699089922 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -479,7 +479,9 @@ async def async_added_to_hass(self) -> None: } self._remove_callbacks.append( async_dispatcher_connect( - self.hass, DISPATCHER_UPDATE_ENTITY.format(**kwargs), self._on_update + self.hass, + DISPATCHER_UPDATE_ENTITY.format(**kwargs), + self._on_state_update, ) ) @@ -493,14 +495,23 @@ async def async_added_to_hass(self) -> None: async_dispatcher_connect( self.hass, DISPATCHER_ON_DEVICE_UPDATE.format(**kwargs), - self.async_schedule_update_ha_state, + self._on_device_update, ) ) - async def _on_update(self) -> None: + async def _on_state_update(self) -> None: """Update the entity state when state or static info changed.""" self.async_schedule_update_ha_state() + async def _on_device_update(self) -> None: + """Update the entity state when device info has changed.""" + if self._entry_data.available: + # Don't update the HA state yet when the device comes online. + # Only update the HA state when the full state arrives + # through the next entity state packet. + return + self.async_schedule_update_ha_state() + async def async_will_remove_from_hass(self) -> None: """Unregister callbacks.""" for remove_callback in self._remove_callbacks: diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index cc2e0cede2361a..c3615c4726d408 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -47,9 +47,9 @@ def _static_info(self) -> CameraInfo: def _state(self) -> Optional[CameraState]: return super()._state - async def _on_update(self) -> None: + async def _on_state_update(self) -> None: """Notify listeners of new image when update arrives.""" - await super()._on_update() + await super()._on_state_update() async with self._image_cond: self._image_cond.notify_all() From 6d4e3945d627162299c4545e201127577a766df1 Mon Sep 17 00:00:00 2001 From: bouni Date: Mon, 14 Oct 2019 17:25:55 +0200 Subject: [PATCH 0880/3953] moved imports to top level (#27641) --- homeassistant/components/config/config_entries.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index b21991a8479180..81065665e34394 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,6 +1,7 @@ """Http views to control the config manager.""" import aiohttp.web_exceptions import voluptuous as vol +import voluptuous_serialize from homeassistant import config_entries, data_entry_flow from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES @@ -41,8 +42,6 @@ def _prepare_json(result): if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result - import voluptuous_serialize - data = result.copy() schema = data["data_schema"] From 288d370ef53605d5b5f19b82be142ea51225a3fa Mon Sep 17 00:00:00 2001 From: ju Date: Mon, 14 Oct 2019 17:28:25 +0200 Subject: [PATCH 0881/3953] Fix html5 notification documentation url (#27636) --- homeassistant/components/html5/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index ac76911b9f63ea..a802609ac85c22 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -56,7 +56,7 @@ def gcm_api_deprecated(value): "Configuring html5_push_notifications via the GCM api" " has been deprecated and will stop working after April 11," " 2019. Use the VAPID configuration instead. For instructions," - " see https://www.home-assistant.io/components/notify.html5/" + " see https://www.home-assistant.io/integrations/html5/" ) return value From de7963544fc1faab23f659ed19f094e641839398 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Mon, 14 Oct 2019 17:38:34 +0200 Subject: [PATCH 0882/3953] Apply isort on rfxtrx classes (#27615) * Move imports in rfxtrx component * Apply isort on rfxtrx files * Update test_switch.py --- homeassistant/components/rfxtrx/__init__.py | 5 +++-- homeassistant/components/rfxtrx/binary_sensor.py | 1 + homeassistant/components/rfxtrx/light.py | 1 + homeassistant/components/rfxtrx/sensor.py | 2 +- homeassistant/components/rfxtrx/switch.py | 1 + tests/components/rfxtrx/test_cover.py | 5 +++-- tests/components/rfxtrx/test_init.py | 3 ++- tests/components/rfxtrx/test_light.py | 5 +++-- tests/components/rfxtrx/test_sensor.py | 2 +- tests/components/rfxtrx/test_switch.py | 9 +++++---- 10 files changed, 21 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 73ee07cfb5f762..1515ce33c6e684 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -1,7 +1,8 @@ """Support for RFXtrx devices.""" -from collections import OrderedDict import binascii +from collections import OrderedDict import logging + import RFXtrx as rfxtrxmod import voluptuous as vol @@ -13,8 +14,8 @@ CONF_DEVICES, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - TEMP_CELSIUS, POWER_WATT, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 259f914b408ae8..6465dc36326ae6 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -1,5 +1,6 @@ """Support for RFXtrx binary sensors.""" import logging + import RFXtrx as rfxtrxmod import voluptuous as vol diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 82b1407c79875b..a745a11388ac67 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -1,5 +1,6 @@ """Support for RFXtrx lights.""" import logging + import RFXtrx as rfxtrxmod import voluptuous as vol diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 5f6b90b600f1c9..5429943a7a6123 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -1,8 +1,8 @@ """Support for RFXtrx sensors.""" import logging -import voluptuous as vol from RFXtrx import SensorEvent +import voluptuous as vol from homeassistant.components import rfxtrx from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index b5c830a298d868..6d91b261a4f5d7 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -1,5 +1,6 @@ """Support for RFXtrx switches.""" import logging + import RFXtrx as rfxtrxmod import voluptuous as vol diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index d2bfb1148049f1..d85ea5cf6f4c36 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -1,10 +1,11 @@ """The tests for the Rfxtrx cover platform.""" import unittest -import pytest + import RFXtrx as rfxtrxmod +import pytest -from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index ac046b998972fa..ec457af7575b85 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -4,9 +4,10 @@ import pytest +from homeassistant.components import rfxtrx as rfxtrx from homeassistant.core import callback from homeassistant.setup import setup_component -from homeassistant.components import rfxtrx as rfxtrx + from tests.common import get_test_home_assistant diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index 1254a6d6697443..a5230cc5f3cfee 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -1,10 +1,11 @@ """The tests for the Rfxtrx light platform.""" import unittest -import pytest + import RFXtrx as rfxtrxmod +import pytest -from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index 3f0cfead3e4e78..652c823e0cfc40 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -3,9 +3,9 @@ import pytest -from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core from homeassistant.const import TEMP_CELSIUS +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 1e39d4afb75e33..66da197aae848f 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -1,17 +1,18 @@ -"""The tests for the Rfxtrx switch platform.""" +"""The tests for the RFXtrx switch platform.""" import unittest -import pytest + import RFXtrx as rfxtrxmod +import pytest -from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component @pytest.mark.skipif("os.environ.get('RFXTRX') != 'RUN'") class TestSwitchRfxtrx(unittest.TestCase): - """Test the Rfxtrx switch platform.""" + """Test the RFXtrx switch platform.""" def setUp(self): """Set up things to be run when tests are started.""" From 09de6d58896b20c0116c5606294d58ede17bfe77 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 14 Oct 2019 17:41:16 +0200 Subject: [PATCH 0883/3953] Fix ESPHome climate preset mode refactor (#27637) Fixes https://github.com/home-assistant/home-assistant/issues/25613 --- homeassistant/components/esphome/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index fa840078aa4ee7..1dfe2184952e81 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -17,6 +17,7 @@ SUPPORT_TARGET_TEMPERATURE_RANGE, PRESET_AWAY, HVAC_MODE_OFF, + PRESET_HOME, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -96,7 +97,7 @@ def hvac_modes(self) -> List[str]: @property def preset_modes(self): """Return preset modes.""" - return [PRESET_AWAY] if self._static_info.supports_away else [] + return [PRESET_AWAY, PRESET_HOME] if self._static_info.supports_away else [] @property def target_temperature_step(self) -> float: From 5a83a92390e8a3255885198c80622556f886b9b3 Mon Sep 17 00:00:00 2001 From: "Steven D. Lander" <3169732+stevendlander@users.noreply.github.com> Date: Mon, 14 Oct 2019 11:44:30 -0400 Subject: [PATCH 0884/3953] Refactor imports for tensorflow (#27617) * Refactoring imports for tensorflow * Removing whitespace spaces on blank line 110 * Moving tensorflow to try/except block * Fixed black formatting * Refactoring try/except to if/else --- .../components/tensorflow/image_processing.py | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 65e20f558a787d..1f49888cb95252 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -2,8 +2,22 @@ import logging import os import sys - +import io import voluptuous as vol +from PIL import Image, ImageDraw +import numpy as np + +try: + import cv2 +except ImportError: + cv2 = None + +try: + # Verify that the TensorFlow Object Detection API is pre-installed + import tensorflow as tf # noqa + from object_detection.utils import label_map_util # noqa +except ImportError: + label_map_util = None from homeassistant.components.image_processing import ( CONF_CONFIDENCE, @@ -84,14 +98,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # append custom model path to sys.path sys.path.append(model_dir) - try: - # Verify that the TensorFlow Object Detection API is pre-installed - # pylint: disable=unused-import,unused-variable - os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" - import tensorflow as tf # noqa - from object_detection.utils import label_map_util # noqa - except ImportError: - # pylint: disable=line-too-long + os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" + if label_map_util is None: _LOGGER.error( "No TensorFlow Object Detection library found! Install or compile " "for your system following instructions here: " @@ -99,11 +107,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) # noqa return - try: - # Display warning that PIL will be used if no OpenCV is found. - # pylint: disable=unused-import,unused-variable - import cv2 # noqa - except ImportError: + if cv2 is None: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " "PIL at reduced resolution" @@ -236,9 +240,6 @@ def device_state_attributes(self): } def _save_image(self, image, matches, paths): - from PIL import Image, ImageDraw - import io - img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") img_width, img_height = img.size draw = ImageDraw.Draw(img) @@ -280,18 +281,8 @@ def _save_image(self, image, matches, paths): def process_image(self, image): """Process the image.""" - import numpy as np - - try: - import cv2 # pylint: disable=import-error - - img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) - inp = img[:, :, [2, 1, 0]] # BGR->RGB - inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) - except ImportError: - from PIL import Image - import io + if cv2 is None: img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") img.thumbnail((460, 460), Image.ANTIALIAS) img_width, img_height = img.size @@ -301,6 +292,10 @@ def process_image(self, image): .astype(np.uint8) ) inp_expanded = np.expand_dims(inp, axis=0) + else: + img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) + inp = img[:, :, [2, 1, 0]] # BGR->RGB + inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) image_tensor = self._graph.get_tensor_by_name("image_tensor:0") boxes = self._graph.get_tensor_by_name("detection_boxes:0") From 2295b332049af325a079605a9dbc39dc1d076c37 Mon Sep 17 00:00:00 2001 From: bouni Date: Mon, 14 Oct 2019 19:57:03 +0200 Subject: [PATCH 0885/3953] Move imports in bluesound component (#27502) * moved imports to top level * changed import order * changed import order --- homeassistant/components/bluesound/media_player.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index bf0568aed16c29..702cf5ddc30172 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -3,14 +3,16 @@ from asyncio.futures import CancelledError from datetime import timedelta import logging +from urllib import parse import aiohttp from aiohttp.client_exceptions import ClientError from aiohttp.hdrs import CONNECTION, KEEP_ALIVE import async_timeout import voluptuous as vol +import xmltodict -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, DOMAIN, @@ -329,7 +331,6 @@ async def send_bluesound_command( self, method, raise_timeout=False, allow_offline=False ): """Send command to the player.""" - import xmltodict if not self._is_online and not allow_offline: return @@ -370,7 +371,6 @@ async def send_bluesound_command( async def async_update_status(self): """Use the poll session to always get the status of the player.""" - import xmltodict response = None @@ -690,7 +690,6 @@ def source_list(self): @property def source(self): """Name of the current input source.""" - from urllib import parse if self._status is None or (self.is_grouped and not self.is_master): return None From c7bd0fe909e3864ec2c34559091d7bd5855b6dad Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Oct 2019 20:11:43 +0200 Subject: [PATCH 0886/3953] Fix ZHA regressions caused by "Support async validation of device trigger" (#27401) * Revert "Support async validation of device trigger (#27333)" This reverts commit fdf4f398a79e775e11dc9fdd391a8b53f7b773c5. * Revert only ZHA changes * Fix whitespace * Restore ZHA changes but add check to make sure ZHA is loaded * Address review comment * Remove additional check --- homeassistant/components/zha/device_trigger.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 8d74ae108a2fb7..cdd62b11d1e544 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -25,14 +25,14 @@ async def async_validate_trigger_config(hass, config): """Validate config.""" config = TRIGGER_SCHEMA(config) - trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) - - if ( - zha_device.device_automation_triggers is None - or trigger not in zha_device.device_automation_triggers - ): - raise InvalidDeviceAutomationConfig + if "zha" in hass.config.components: + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + if ( + zha_device.device_automation_triggers is None + or trigger not in zha_device.device_automation_triggers + ): + raise InvalidDeviceAutomationConfig return config From 2f6c2fadd0e033904a31afa1725fbbd1b0a0fcaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 21:20:15 +0200 Subject: [PATCH 0887/3953] move imports in squeezebox component (#27650) --- homeassistant/components/squeezebox/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 6540fca1405871..6d67f67a3cece6 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -2,13 +2,14 @@ import asyncio import json import logging +import socket import urllib.parse import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, DOMAIN, @@ -100,7 +101,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the squeezebox platform.""" - import socket known_servers = hass.data.get(KNOWN_SERVERS) if known_servers is None: From 759ad0893018bb8bce6f29c74aa007eb75e93d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 14 Oct 2019 23:03:37 +0300 Subject: [PATCH 0888/3953] Typing misc fixes (#27543) * Make async_get_conditions return type hint more specific * Exclude script/scaffold/templates/ from pre-commit mypy --- .pre-commit-config.yaml | 1 + .../components/binary_sensor/device_condition.py | 8 +++++--- .../components/device_automation/toggle_entity.py | 2 +- homeassistant/components/light/device_condition.py | 6 ++++-- homeassistant/components/sensor/device_condition.py | 8 +++++--- homeassistant/components/switch/device_condition.py | 6 ++++-- .../device_condition/integration/device_condition.py | 6 ++++-- 7 files changed, 24 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e8792e88c9453..55e00443ba1190 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,3 +17,4 @@ repos: rev: v0.730 hooks: - id: mypy + exclude: ^script/scaffold/templates/ diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index a38c0c09ee5d26..0766d82c727b09 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -1,5 +1,5 @@ """Implemenet device conditions for binary sensor.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.core import HomeAssistant @@ -193,9 +193,11 @@ ) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions.""" - conditions: List[dict] = [] + conditions: List[Dict[str, str]] = [] entity_registry = await async_get_registry(hass) entries = [ entry diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 98a1af9c4cad06..5f01f4d9d71ace 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -205,7 +205,7 @@ async def async_get_actions( async def async_get_conditions( hass: HomeAssistant, device_id: str, domain: str -) -> List[dict]: +) -> List[Dict[str, str]]: """List device conditions.""" return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain) diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index 0b3cecbea41264..e87ae3bf9450de 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for lights.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.core import HomeAssistant @@ -24,7 +24,9 @@ def async_condition_from_config( return toggle_entity.async_condition_from_config(config) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 18aa46d78e1edf..26479807991392 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for sensors.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.core import HomeAssistant @@ -80,9 +80,11 @@ ) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions.""" - conditions: List[dict] = [] + conditions: List[Dict[str, str]] = [] entity_registry = await async_get_registry(hass) entries = [ entry diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 7df972151c7ca3..56f8f6c196ea4c 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for switches.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.core import HomeAssistant @@ -24,7 +24,9 @@ def async_condition_from_config( return toggle_entity.async_condition_from_config(config) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index 9acb351b19799a..fa123cff8e00f2 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -1,5 +1,5 @@ """Provides device automations for NEW_NAME.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.const import ( @@ -29,7 +29,9 @@ ) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions for NEW_NAME devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] From 75eb33eb701bd973f01cbbc92c587fbcb84f6dec Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 14 Oct 2019 22:07:47 +0200 Subject: [PATCH 0889/3953] Updated frontend to 20191014.0 (#27661) --- 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 67a66bc96125c2..43578420212dc0 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191002.2" + "home-assistant-frontend==20191014.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2259f3053fe328..1648bdc3db58bc 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.22 -home-assistant-frontend==20191002.2 +home-assistant-frontend==20191014.0 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index e4c608793b8286..dbc94de9a1cff3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -640,7 +640,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.2 +home-assistant-frontend==20191014.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec8477c9369c8b..18970fcbac0373 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -236,7 +236,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.2 +home-assistant-frontend==20191014.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 487a5b25271929a0fa8a51dc1eeb567014cdd9f5 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Mon, 14 Oct 2019 23:15:29 +0200 Subject: [PATCH 0890/3953] Move imports in panasonic_viera component (#27665) --- homeassistant/components/panasonic_viera/media_player.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index d0b013c3bf3273..0b19a8fa55272d 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -1,9 +1,11 @@ """Support for interface with a Panasonic Viera TV.""" import logging +from panasonic_viera import RemoteControl import voluptuous as vol +import wakeonlan -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_URL, SUPPORT_NEXT_TRACK, @@ -62,8 +64,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Panasonic Viera TV platform.""" - from panasonic_viera import RemoteControl - mac = config.get(CONF_MAC) name = config.get(CONF_NAME) port = config.get(CONF_PORT) @@ -95,8 +95,6 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def __init__(self, mac, name, remote, host, app_power, uuid=None): """Initialize the Panasonic device.""" - import wakeonlan - # Save a reference to the imported class self._wol = wakeonlan self._mac = mac From 5c2bf6dc7c201fd442ea50b5e76074c6f16a44a1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 14 Oct 2019 23:15:46 +0200 Subject: [PATCH 0891/3953] Improve discovery title (#27664) --- homeassistant/components/deconz/.translations/en.json | 1 + homeassistant/components/deconz/config_flow.py | 1 + homeassistant/components/deconz/strings.json | 1 + 3 files changed, 3 insertions(+) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index c00bfca35641c2..e9c64ffe5faa64 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -11,6 +11,7 @@ "error": { "no_key": "Couldn't get an API key" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 91768584e8ac95..5ede8e715b9471 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -189,6 +189,7 @@ async def async_step_ssdp(self, discovery_info): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context[CONF_BRIDGEID] = bridgeid + self.context["title_placeholders"] = {"host": discovery_info[CONF_HOST]} self.deconz_config = { CONF_HOST: discovery_info[CONF_HOST], diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index db43c022822838..3571a9e1207c4c 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -1,6 +1,7 @@ { "config": { "title": "deCONZ Zigbee gateway", + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "init": { "title": "Define deCONZ gateway", From 6c0efe9329d94cdee6319c3faaee254e5b6f9b46 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Mon, 14 Oct 2019 23:17:08 +0200 Subject: [PATCH 0892/3953] Move imports in panasonic_bluray component (#27658) --- homeassistant/components/panasonic_bluray/media_player.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py index 393ecb827cc339..4a816252580265 100644 --- a/homeassistant/components/panasonic_bluray/media_player.py +++ b/homeassistant/components/panasonic_bluray/media_player.py @@ -2,9 +2,10 @@ from datetime import timedelta import logging +from panacotta import PanasonicBD import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, @@ -53,9 +54,7 @@ class PanasonicBluRay(MediaPlayerDevice): def __init__(self, ip, name): """Initialize the Panasonic Blue-ray device.""" - import panacotta - - self._device = panacotta.PanasonicBD(ip) + self._device = PanasonicBD(ip) self._name = name self._state = STATE_OFF self._position = 0 From a49dbb9718f8e07dad9cd92c3b0382a1ba908ae6 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Mon, 14 Oct 2019 17:19:05 -0400 Subject: [PATCH 0893/3953] Update Unlock directive for Alexa LockController (#27653) * Update the Alexa.LockController Unlock directive to include the lockState property in the context of the response. * Added Test for Alexa.LockController Unlock directive to include the lockState property in the context of the response. --- homeassistant/components/alexa/handlers.py | 8 ++++++-- tests/components/alexa/test_smart_home.py | 10 +++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index bd07b71ca29aa7..139defe8313037 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -412,7 +412,6 @@ async def async_api_lock(hass, config, directive, context): return response -# Not supported by Alexa yet @HANDLERS.register(("Alexa.LockController", "Unlock")) async def async_api_unlock(hass, config, directive, context): """Process an unlock request.""" @@ -425,7 +424,12 @@ async def async_api_unlock(hass, config, directive, context): context=context, ) - return directive.response() + response = directive.response() + response.add_context_property( + {"namespace": "Alexa.LockController", "name": "lockState", "value": "UNLOCKED"} + ) + + return response @HANDLERS.register(("Alexa.Speaker", "SetVolume")) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 78bdd8e09082a2..186cb850e341a9 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -403,12 +403,20 @@ async def test_lock(hass): "Alexa.LockController", "Lock", "lock#test", "lock.lock", hass ) - # always return LOCKED for now properties = msg["context"]["properties"][0] assert properties["name"] == "lockState" assert properties["namespace"] == "Alexa.LockController" assert properties["value"] == "LOCKED" + _, msg = await assert_request_calls_service( + "Alexa.LockController", "Unlock", "lock#test", "lock.unlock", hass + ) + + properties = msg["context"]["properties"][0] + assert properties["name"] == "lockState" + assert properties["namespace"] == "Alexa.LockController" + assert properties["value"] == "UNLOCKED" + async def test_media_player(hass): """Test media player discovery.""" From bcb14182c6128a0252643b65ecccb5f6749e953b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 23:19:37 +0200 Subject: [PATCH 0894/3953] move imports in statsd component (#27649) --- homeassistant/components/statsd/__init__.py | 4 ++-- tests/components/statsd/test_init.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/statsd/__init__.py b/homeassistant/components/statsd/__init__.py index 714de88dd87497..79065f7ba534a9 100644 --- a/homeassistant/components/statsd/__init__.py +++ b/homeassistant/components/statsd/__init__.py @@ -1,11 +1,12 @@ """Support for sending data to StatsD.""" import logging +import statsd import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PREFIX, EVENT_STATE_CHANGED -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -40,7 +41,6 @@ def setup(hass, config): """Set up the StatsD component.""" - import statsd conf = config[DOMAIN] host = conf.get(CONF_HOST) diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py index 6deb40c082d30d..4c7e9d29fee5ab 100644 --- a/tests/components/statsd/test_init.py +++ b/tests/components/statsd/test_init.py @@ -2,15 +2,15 @@ import unittest from unittest import mock +import pytest import voluptuous as vol -from homeassistant.setup import setup_component -import homeassistant.core as ha import homeassistant.components.statsd as statsd -from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON +import homeassistant.core as ha +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant -import pytest class TestStatsd(unittest.TestCase): From 4efa2f3244423c4cf804413362c4ee39e21a247a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 23:19:53 +0200 Subject: [PATCH 0895/3953] Move imports in steam_online component (#27648) * move imports in steam_online component * fix: import reassigment --- homeassistant/components/steam_online/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 6c9c5ac6079eb9..85e5c49fb2c3df 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -1,15 +1,16 @@ """Sensor for Steam account status.""" -import logging from datetime import timedelta +import logging +import steam import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_API_KEY from homeassistant.core import callback +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 CONF_API_KEY -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -38,13 +39,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Steam platform.""" - import steam as steamod - steamod.api.key.set(config.get(CONF_API_KEY)) + steam.api.key.set(config.get(CONF_API_KEY)) # Initialize steammods app list before creating sensors # to benefit from internal caching of the list. - hass.data[APP_LIST_KEY] = steamod.apps.app_list() - entities = [SteamSensor(account, steamod) for account in config.get(CONF_ACCOUNTS)] + hass.data[APP_LIST_KEY] = steam.apps.app_list() + entities = [SteamSensor(account, steam) for account in config.get(CONF_ACCOUNTS)] if not entities: return add_entities(entities, True) From 9aa28dfd544f496c5e81b3823371e963ada9bc09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 23:20:18 +0200 Subject: [PATCH 0896/3953] move imports in stream component (#27647) --- homeassistant/components/stream/__init__.py | 23 +++++++++++---------- homeassistant/components/stream/core.py | 8 +++---- homeassistant/components/stream/hls.py | 2 +- homeassistant/components/stream/recorder.py | 7 ++++--- homeassistant/components/stream/worker.py | 7 +++---- tests/components/stream/common.py | 7 ++++--- tests/components/stream/test_hls.py | 2 +- tests/components/stream/test_init.py | 10 ++++----- tests/components/stream/test_recorder.py | 3 ++- 9 files changed, 36 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 2ae8dd5f71405b..4c93ce46135aef 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -4,31 +4,32 @@ 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 +from homeassistant.const import CONF_FILENAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from .const import ( - DOMAIN, - ATTR_STREAMS, ATTR_ENDPOINTS, - CONF_STREAM_SOURCE, + ATTR_STREAMS, CONF_DURATION, CONF_LOOKBACK, + CONF_STREAM_SOURCE, + DOMAIN, SERVICE_RECORD, ) from .core import PROVIDERS -from .worker import stream_worker from .hls import async_setup_hls from .recorder import async_setup_recorder +from .worker import stream_worker + +try: + import uvloop +except ImportError: + uvloop = None + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 81335783e1a277..9282c2cb855335 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -2,17 +2,17 @@ import asyncio from collections import deque import io -from typing import List, Any +from typing import Any, List -import attr from aiohttp import web +import attr -from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView +from homeassistant.core import callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry -from .const import DOMAIN, ATTR_STREAMS +from .const import ATTR_STREAMS, DOMAIN PROVIDERS = Registry() diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index c9e62f53a5788b..2cd98c0a00f52f 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -5,7 +5,7 @@ from homeassistant.util.dt import utcnow from .const import FORMAT_CONTENT_TYPE -from .core import StreamView, StreamOutput, PROVIDERS +from .core import PROVIDERS, StreamOutput, StreamView @callback diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index cd25896aff3279..1dd90b8b804598 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -1,10 +1,13 @@ """Provide functionality to record stream.""" + import threading from typing import List +import av + from homeassistant.core import callback -from .core import Segment, StreamOutput, PROVIDERS +from .core import PROVIDERS, Segment, StreamOutput @callback @@ -14,8 +17,6 @@ def async_setup_recorder(hass): def recorder_save_worker(file_out: str, segments: List[Segment]): """Handle saving stream.""" - import av - output = av.open(file_out, "w", options={"movflags": "frag_keyframe"}) output_v = None diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index e87221304a381c..99ffd833eb3c4c 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -3,6 +3,8 @@ import io import logging +import av + from .const import AUDIO_SAMPLE_RATE from .core import Segment, StreamBuffer @@ -11,9 +13,8 @@ def generate_audio_frame(): """Generate a blank audio frame.""" - from av import AudioFrame - audio_frame = AudioFrame(format="dbl", layout="mono", samples=1024) + audio_frame = av.AudioFrame(format="dbl", layout="mono", samples=1024) # audio_bytes = b''.join(b'\x00\x00\x00\x00\x00\x00\x00\x00' # for i in range(0, 1024)) audio_bytes = b"\x00\x00\x00\x00\x00\x00\x00\x00" * 1024 @@ -25,7 +26,6 @@ def generate_audio_frame(): def create_stream_buffer(stream_output, video_stream, audio_frame): """Create a new StreamBuffer.""" - import av a_packet = None segment = io.BytesIO() @@ -45,7 +45,6 @@ def create_stream_buffer(stream_output, video_stream, audio_frame): def stream_worker(hass, stream, quit_event): """Handle consuming streams.""" - import av container = av.open(stream.source, options=stream.options) try: diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index 32ab36dc477ee2..4c34ec0b341c0f 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -1,8 +1,11 @@ """Collection of test helpers.""" import io +import av +import numpy as np + from homeassistant.components.stream import Stream -from homeassistant.components.stream.const import DOMAIN, ATTR_STREAMS +from homeassistant.components.stream.const import ATTR_STREAMS, DOMAIN def generate_h264_video(): @@ -11,8 +14,6 @@ def generate_h264_video(): See: http://docs.mikeboers.com/pyav/develop/cookbook/numpy.html """ - import numpy as np - import av duration = 5 fps = 24 diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index ac564ce75534f2..293f8d1e4cfe29 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -4,8 +4,8 @@ import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.stream import request_stream +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py index 80d703c801bea6..0661a5a9738d9c 100644 --- a/tests/components/stream/test_init.py +++ b/tests/components/stream/test_init.py @@ -1,16 +1,16 @@ """The tests for stream.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest -from homeassistant.const import CONF_FILENAME from homeassistant.components.stream.const import ( + ATTR_STREAMS, + CONF_LOOKBACK, + CONF_STREAM_SOURCE, DOMAIN, SERVICE_RECORD, - CONF_STREAM_SOURCE, - CONF_LOOKBACK, - ATTR_STREAMS, ) +from homeassistant.const import CONF_FILENAME from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index dce8b95d07cbb5..95eeeecf7ade1f 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -2,11 +2,12 @@ from datetime import timedelta from io import BytesIO from unittest.mock import patch + import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.stream.core import Segment from homeassistant.components.stream.recorder import recorder_save_worker +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed From 97478d1ef478aea3f2b3680598260d62de0c3891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 23:20:35 +0200 Subject: [PATCH 0897/3953] Move imports in switchmate component (#27646) * move imports in switchmate component * fix: bring back pylint ignore line --- homeassistant/components/switchmate/switch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index 950a8a67930ce6..6abbfd5fae5e26 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -1,12 +1,14 @@ """Support for Switchmate.""" -import logging from datetime import timedelta +import logging +# pylint: disable=import-error, no-member, no-value-for-parameter +import switchmate import voluptuous as vol +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_MAC _LOGGER = logging.getLogger(__name__) @@ -37,8 +39,6 @@ class SwitchmateEntity(SwitchDevice): def __init__(self, mac, name, flip_on_off) -> None: """Initialize the Switchmate.""" - # pylint: disable=import-error, no-member, no-value-for-parameter - import switchmate self._mac = mac self._name = name From 3231e22ddf2fdd65a36028bec5f87d8e7032118b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Oct 2019 14:56:45 -0700 Subject: [PATCH 0898/3953] Remove direct authentication via trusted networks or API password (#27656) * Remove direct authentication via trusted networks and API password * Fix tests --- homeassistant/auth/__init__.py | 12 -- homeassistant/bootstrap.py | 6 +- homeassistant/components/auth/login_flow.py | 7 +- homeassistant/components/http/__init__.py | 41 ------ homeassistant/components/http/auth.py | 129 +++--------------- homeassistant/components/http/cors.py | 3 +- homeassistant/components/http/view.py | 10 +- .../components/websocket_api/auth.py | 14 -- homeassistant/config.py | 15 +- homeassistant/const.py | 1 - tests/components/auth/__init__.py | 2 +- tests/components/auth/test_init.py | 2 +- tests/components/conftest.py | 4 +- .../google_assistant/test_google_assistant.py | 7 +- tests/components/hassio/__init__.py | 1 - tests/components/hassio/conftest.py | 12 +- tests/components/hassio/test_addon_panel.py | 18 +-- tests/components/hassio/test_auth.py | 13 +- tests/components/hassio/test_discovery.py | 8 +- tests/components/hassio/test_http.py | 16 +-- tests/components/http/__init__.py | 4 + tests/components/http/test_auth.py | 46 +++---- tests/components/http/test_ban.py | 4 +- tests/components/http/test_cors.py | 7 +- tests/components/http/test_init.py | 2 +- tests/components/mqtt/test_server.py | 7 +- tests/components/websocket_api/__init__.py | 1 - tests/components/websocket_api/conftest.py | 8 +- tests/components/websocket_api/test_auth.py | 72 ++++------ .../components/websocket_api/test_commands.py | 6 +- tests/components/websocket_api/test_sensor.py | 8 +- tests/scripts/test_check_config.py | 12 +- tests/test_config.py | 43 ------ 33 files changed, 116 insertions(+), 425 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index ee0d6c08441caa..64391debc102ec 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -86,18 +86,6 @@ def __init__( hass, self._async_create_login_flow, self._async_finish_login_flow ) - @property - def support_legacy(self) -> bool: - """ - Return if legacy_api_password auth providers are registered. - - Should be removed when we removed legacy_api_password auth providers. - """ - for provider_type, _ in self._providers: - if provider_type == "legacy_api_password": - return True - return False - @property def auth_providers(self) -> List[AuthProvider]: """Return a list of available auth providers.""" diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index ef294491141042..422eab8ed4a8fc 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -64,13 +64,9 @@ async def async_from_config_dict( ) core_config = config.get(core.DOMAIN, {}) - api_password = config.get("http", {}).get("api_password") - trusted_networks = config.get("http", {}).get("trusted_networks") try: - await conf_util.async_process_ha_core_config( - hass, core_config, api_password, trusted_networks - ) + await conf_util.async_process_ha_core_config(hass, core_config) except vol.Invalid as config_err: conf_util.async_log_exception(config_err, "homeassistant", core_config, hass) return None diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 4fa0f866124d56..d6844396ce7811 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -72,7 +72,11 @@ from homeassistant import data_entry_flow from homeassistant.components.http import KEY_REAL_IP -from homeassistant.components.http.ban import process_wrong_login, log_invalid_auth +from homeassistant.components.http.ban import ( + process_wrong_login, + process_success_login, + log_invalid_auth, +) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from . import indieauth @@ -185,6 +189,7 @@ async def post(self, request, data): return self.json_message("Handler does not support init", 400) if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + await process_success_login(request) result.pop("data") result["result"] = self._store_result(data["client_id"], result["result"]) return self.json(result) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 1b61e74769f446..4df606a3c1b29c 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -17,7 +17,6 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util as hass_util from homeassistant.util import ssl as ssl_util -from homeassistant.util.logging import HideSensitiveDataFilter from .auth import setup_auth from .ban import setup_bans @@ -32,7 +31,6 @@ DOMAIN = "http" -CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" CONF_SERVER_PORT = "server_port" CONF_BASE_URL = "base_url" @@ -42,7 +40,6 @@ CONF_CORS_ORIGINS = "cors_allowed_origins" CONF_USE_X_FORWARDED_FOR = "use_x_forwarded_for" CONF_TRUSTED_PROXIES = "trusted_proxies" -CONF_TRUSTED_NETWORKS = "trusted_networks" CONF_LOGIN_ATTEMPTS_THRESHOLD = "login_attempts_threshold" CONF_IP_BAN_ENABLED = "ip_ban_enabled" CONF_SSL_PROFILE = "ssl_profile" @@ -59,37 +56,8 @@ NO_LOGIN_ATTEMPT_THRESHOLD = -1 -def trusted_networks_deprecated(value): - """Warn user trusted_networks config is deprecated.""" - if not value: - return value - - _LOGGER.warning( - "Configuring trusted_networks via the http integration has been" - " deprecated. Use the trusted networks auth provider instead." - " For instructions, see https://www.home-assistant.io/docs/" - "authentication/providers/#trusted-networks" - ) - return value - - -def api_password_deprecated(value): - """Warn user api_password config is deprecated.""" - if not value: - return value - - _LOGGER.warning( - "Configuring api_password via the http integration has been" - " deprecated. Use the legacy api password auth provider instead." - " For instructions, see https://www.home-assistant.io/docs/" - "authentication/providers/#legacy-api-password" - ) - return value - - HTTP_SCHEMA = vol.Schema( { - vol.Optional(CONF_API_PASSWORD): vol.All(cv.string, api_password_deprecated), vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, vol.Optional(CONF_BASE_URL): cv.string, @@ -103,9 +71,6 @@ def api_password_deprecated(value): vol.Inclusive(CONF_TRUSTED_PROXIES, "proxy"): vol.All( cv.ensure_list, [ip_network] ), - vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): vol.All( - cv.ensure_list, [ip_network], trusted_networks_deprecated - ), vol.Optional( CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD ): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), @@ -149,7 +114,6 @@ async def async_setup(hass, config): if conf is None: conf = HTTP_SCHEMA({}) - api_password = conf.get(CONF_API_PASSWORD) server_host = conf[CONF_SERVER_HOST] server_port = conf[CONF_SERVER_PORT] ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) @@ -162,11 +126,6 @@ async def async_setup(hass, config): login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] ssl_profile = conf[CONF_SSL_PROFILE] - if api_password is not None: - logging.getLogger("aiohttp.access").addFilter( - HideSensitiveDataFilter(api_password) - ) - server = HomeAssistantHTTP( hass, server_host=server_host, diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 4ff581aef02e3d..97bd9b7d4bcee7 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,14 +1,11 @@ """Authentication for HTTP component.""" -import base64 import logging from aiohttp import hdrs from aiohttp.web import middleware import jwt -from homeassistant.auth.providers import legacy_api_password from homeassistant.auth.util import generate_secret -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.core import callback from homeassistant.util import dt as dt_util @@ -52,16 +49,6 @@ def async_sign_path(hass, refresh_token_id, path, expiration): @callback def setup_auth(hass, app): """Create auth middleware for the app.""" - old_auth_warning = set() - - support_legacy = hass.auth.support_legacy - if support_legacy: - _LOGGER.warning("legacy_api_password support has been enabled.") - - trusted_networks = [] - for prv in hass.auth.auth_providers: - if prv.type == "trusted_networks": - trusted_networks += prv.trusted_networks async def async_validate_auth_header(request): """ @@ -75,40 +62,16 @@ async def async_validate_auth_header(request): # If no space in authorization header return False - if auth_type == "Bearer": - refresh_token = await hass.auth.async_validate_access_token(auth_val) - if refresh_token is None: - return False - - request[KEY_HASS_USER] = refresh_token.user - return True - - if auth_type == "Basic" and support_legacy: - decoded = base64.b64decode(auth_val).decode("utf-8") - try: - username, password = decoded.split(":", 1) - except ValueError: - # If no ':' in decoded - return False - - if username != "homeassistant": - return False - - user = await legacy_api_password.async_validate_password(hass, password) - if user is None: - return False - - request[KEY_HASS_USER] = user - _LOGGER.info( - "Basic auth with api_password is going to deprecate," - " please use a bearer token to access %s from %s", - request.path, - request[KEY_REAL_IP], - ) - old_auth_warning.add(request.path) - return True + if auth_type != "Bearer": + return False + + refresh_token = await hass.auth.async_validate_access_token(auth_val) + + if refresh_token is None: + return False - return False + request[KEY_HASS_USER] = refresh_token.user + return True async def async_validate_signed_request(request): """Validate a signed request.""" @@ -140,50 +103,16 @@ async def async_validate_signed_request(request): request[KEY_HASS_USER] = refresh_token.user return True - async def async_validate_trusted_networks(request): - """Test if request is from a trusted ip.""" - ip_addr = request[KEY_REAL_IP] - - if not any(ip_addr in trusted_network for trusted_network in trusted_networks): - return False - - user = await hass.auth.async_get_owner() - if user is None: - return False - - request[KEY_HASS_USER] = user - return True - - async def async_validate_legacy_api_password(request, password): - """Validate api_password.""" - user = await legacy_api_password.async_validate_password(hass, password) - if user is None: - return False - - request[KEY_HASS_USER] = user - return True - @middleware async def auth_middleware(request, handler): """Authenticate as middleware.""" authenticated = False - if HTTP_HEADER_HA_AUTH in request.headers or DATA_API_PASSWORD in request.query: - if request.path not in old_auth_warning: - _LOGGER.log( - logging.INFO if support_legacy else logging.WARNING, - "api_password is going to deprecate. You need to use a" - " bearer token to access %s from %s", - request.path, - request[KEY_REAL_IP], - ) - old_auth_warning.add(request.path) - if hdrs.AUTHORIZATION in request.headers and await async_validate_auth_header( request ): - # it included both use_auth and api_password Basic auth authenticated = True + auth_type = "bearer token" # We first start with a string check to avoid parsing query params # for every request. @@ -193,39 +122,15 @@ async def auth_middleware(request, handler): and await async_validate_signed_request(request) ): authenticated = True + auth_type = "signed request" - elif trusted_networks and await async_validate_trusted_networks(request): - if request.path not in old_auth_warning: - # When removing this, don't forget to remove the print logic - # in http/view.py - request["deprecate_warning_message"] = ( - "Access from trusted networks without auth token is " - "going to be removed in Home Assistant 0.96. Configure " - "the trusted networks auth provider or use long-lived " - "access tokens to access {} from {}".format( - request.path, request[KEY_REAL_IP] - ) - ) - old_auth_warning.add(request.path) - authenticated = True - - elif ( - support_legacy - and HTTP_HEADER_HA_AUTH in request.headers - and await async_validate_legacy_api_password( - request, request.headers[HTTP_HEADER_HA_AUTH] - ) - ): - authenticated = True - - elif ( - support_legacy - and DATA_API_PASSWORD in request.query - and await async_validate_legacy_api_password( - request, request.query[DATA_API_PASSWORD] + if authenticated: + _LOGGER.debug( + "Authenticated %s for %s using %s", + request[KEY_REAL_IP], + request.path, + auth_type, ) - ): - authenticated = True request[KEY_AUTHENTICATED] = authenticated return await handler(request) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index bd8213355424c6..0e6b9f9439a276 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -3,7 +3,7 @@ from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION -from homeassistant.const import HTTP_HEADER_HA_AUTH, HTTP_HEADER_X_REQUESTED_WITH +from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback @@ -14,7 +14,6 @@ ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE, - HTTP_HEADER_HA_AUTH, AUTHORIZATION, ] VALID_CORS_TYPES = (Resource, ResourceRoute, StaticResource) diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 66864eba55e950..804c90d4f96ab8 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -17,7 +17,6 @@ from homeassistant.core import Context, is_callback from homeassistant.helpers.json import JSONEncoder -from .ban import process_success_login from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_REAL_IP _LOGGER = logging.getLogger(__name__) @@ -106,13 +105,8 @@ async def handle(request): authenticated = request.get(KEY_AUTHENTICATED, False) - if view.requires_auth: - if authenticated: - if "deprecate_warning_message" in request: - _LOGGER.warning(request["deprecate_warning_message"]) - await process_success_login(request) - else: - raise HTTPUnauthorized() + if view.requires_auth and not authenticated: + raise HTTPUnauthorized() _LOGGER.debug( "Serving %s to %s (auth: %s)", diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 716b20f4ca4068..3971d39ee73d3c 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -3,7 +3,6 @@ from voluptuous.humanize import humanize_error from homeassistant.auth.models import RefreshToken, User -from homeassistant.auth.providers import legacy_api_password from homeassistant.components.http.ban import process_wrong_login, process_success_login from homeassistant.const import __version__ @@ -74,19 +73,6 @@ async def async_handle(self, msg): if refresh_token is not None: return await self._async_finish_auth(refresh_token.user, refresh_token) - elif self._hass.auth.support_legacy and "api_password" in msg: - self._logger.info( - "Received api_password, it is going to deprecate, please use" - " access_token instead. For instructions, see https://" - "developers.home-assistant.io/docs/en/external_api_websocket" - ".html#authentication-phase" - ) - user = await legacy_api_password.async_validate_password( - self._hass, msg["api_password"] - ) - if user is not None: - return await self._async_finish_auth(user, None) - self._send_message(auth_invalid_message("Invalid access token or password")) await process_wrong_login(self._request) raise Disconnect diff --git a/homeassistant/config.py b/homeassistant/config.py index 97c996d9e59380..27137c08f1aeb7 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -468,12 +468,7 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: return message -async def async_process_ha_core_config( - hass: HomeAssistant, - config: Dict, - api_password: Optional[str] = None, - trusted_networks: Optional[Any] = None, -) -> None: +async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> None: """Process the [homeassistant] section from the configuration. This method is a coroutine. @@ -486,14 +481,6 @@ async def async_process_ha_core_config( if auth_conf is None: auth_conf = [{"type": "homeassistant"}] - if api_password: - auth_conf.append( - {"type": "legacy_api_password", "api_password": api_password} - ) - if trusted_networks: - auth_conf.append( - {"type": "trusted_networks", "trusted_networks": trusted_networks} - ) mfa_conf = config.get( CONF_AUTH_MFA_MODULES, diff --git a/homeassistant/const.py b/homeassistant/const.py index e0f90834d943dd..592f6b60bc64ae 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -451,7 +451,6 @@ HTTP_BASIC_AUTHENTICATION = "basic" HTTP_DIGEST_AUTHENTICATION = "digest" -HTTP_HEADER_HA_AUTH = "X-HA-access" HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With" CONTENT_TYPE_JSON = "application/json" diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index e79d8a67845385..5114e18889b842 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -30,7 +30,7 @@ async def async_setup_auth( hass, provider_configs, module_configs ) ensure_auth_manager_loaded(hass.auth) - await async_setup_component(hass, "auth", {"http": {"api_password": "bla"}}) + await async_setup_component(hass, "auth", {}) if setup_api: await async_setup_component(hass, "api", {}) return await aiohttp_client(hass.http.app) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 80527c2636b44a..de91613b74b12e 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -103,7 +103,7 @@ def test_auth_code_store_expiration(): async def test_ws_current_user(hass, hass_ws_client, hass_access_token): """Test the current user command with homeassistant creds.""" - assert await async_setup_component(hass, "auth", {"http": {"api_password": "bla"}}) + assert await async_setup_component(hass, "auth", {}) refresh_token = await hass.auth.async_validate_access_token(hass_access_token) user = refresh_token.user diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 8c2515939f38b8..4f1f3e64e025c3 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -40,7 +40,9 @@ async def create_client(hass, access_token=hass_access_token): assert auth_resp["type"] == TYPE_AUTH_REQUIRED if access_token is None: - await websocket.send_json({"type": TYPE_AUTH, "api_password": "bla"}) + await websocket.send_json( + {"type": TYPE_AUTH, "access_token": "incorrect"} + ) else: await websocket.send_json( {"type": TYPE_AUTH, "access_token": access_token} diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 6473e8964b874c..b43e913ab2765d 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -3,7 +3,7 @@ import asyncio import json -from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION +from aiohttp.hdrs import AUTHORIZATION import pytest from homeassistant import core, const, setup @@ -24,11 +24,6 @@ API_PASSWORD = "test1234" -HA_HEADERS = { - const.HTTP_HEADER_HA_AUTH: API_PASSWORD, - CONTENT_TYPE: const.CONTENT_TYPE_JSON, -} - PROJECT_ID = "hasstest-1234" CLIENT_ID = "helloworld" ACCESS_TOKEN = "superdoublesecret" diff --git a/tests/components/hassio/__init__.py b/tests/components/hassio/__init__.py index 8e2b6db777d4c9..767ec59f36690e 100644 --- a/tests/components/hassio/__init__.py +++ b/tests/components/hassio/__init__.py @@ -1,4 +1,3 @@ """Tests for Hassio component.""" -API_PASSWORD = "pass1234" HASSIO_TOKEN = "123456" diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index d7d50b97eb8eee..0e246cf1b46f2c 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -9,7 +9,7 @@ from homeassistant.components.hassio.handler import HassIO, HassioAPIError from tests.common import mock_coro -from . import API_PASSWORD, HASSIO_TOKEN +from . import HASSIO_TOKEN @pytest.fixture @@ -39,23 +39,19 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): side_effect=HassioAPIError(), ): hass.state = CoreState.starting - hass.loop.run_until_complete( - async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) - ) + hass.loop.run_until_complete(async_setup_component(hass, "hassio", {})) @pytest.fixture def hassio_client(hassio_stubs, hass, hass_client): """Return a Hass.io HTTP client.""" - yield hass.loop.run_until_complete(hass_client()) + return hass.loop.run_until_complete(hass_client()) @pytest.fixture def hassio_noauth_client(hassio_stubs, hass, aiohttp_client): """Return a Hass.io HTTP client without auth.""" - yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index 114935df3fc7e8..480df5089682fd 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -4,10 +4,8 @@ import pytest from homeassistant.setup import async_setup_component -from homeassistant.const import HTTP_HEADER_HA_AUTH from tests.common import mock_coro -from . import API_PASSWORD @pytest.fixture(autouse=True) @@ -53,9 +51,7 @@ async def test_hassio_addon_panel_startup(hass, aioclient_mock, hassio_env): "homeassistant.components.hassio.addon_panel._register_panel", Mock(return_value=mock_coro()), ) as mock_panel: - await async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) + await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() assert aioclient_mock.call_count == 3 @@ -98,9 +94,7 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli "homeassistant.components.hassio.addon_panel._register_panel", Mock(return_value=mock_coro()), ) as mock_panel: - await async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) + await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() assert aioclient_mock.call_count == 3 @@ -113,14 +107,10 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli hass_client = await hass_client() - resp = await hass_client.post( - "/api/hassio_push/panel/test2", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = await hass_client.post("/api/hassio_push/panel/test2") assert resp.status == 400 - resp = await hass_client.post( - "/api/hassio_push/panel/test1", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = await hass_client.post("/api/hassio_push/panel/test1") assert resp.status == 200 assert mock_panel.call_count == 2 diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index a2839b297b8669..1fb6d32ccf73eb 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,11 +1,9 @@ """The tests for the hassio component.""" from unittest.mock import patch, Mock -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.exceptions import HomeAssistantError from tests.common import mock_coro -from . import API_PASSWORD async def test_login_success(hass, hassio_client): @@ -18,7 +16,6 @@ async def test_login_success(hass, hassio_client): resp = await hassio_client.post( "/api/hassio_auth", json={"username": "test", "password": "123456", "addon": "samba"}, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, ) # Check we got right response @@ -36,7 +33,6 @@ async def test_login_error(hass, hassio_client): resp = await hassio_client.post( "/api/hassio_auth", json={"username": "test", "password": "123456", "addon": "samba"}, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, ) # Check we got right response @@ -51,9 +47,7 @@ async def test_login_no_data(hass, hassio_client): "HassAuthProvider.async_validate_login", Mock(side_effect=HomeAssistantError()), ) as mock_login: - resp = await hassio_client.post( - "/api/hassio_auth", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = await hassio_client.post("/api/hassio_auth") # Check we got right response assert resp.status == 400 @@ -68,9 +62,7 @@ async def test_login_no_username(hass, hassio_client): Mock(side_effect=HomeAssistantError()), ) as mock_login: resp = await hassio_client.post( - "/api/hassio_auth", - json={"password": "123456", "addon": "samba"}, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, + "/api/hassio_auth", json={"password": "123456", "addon": "samba"} ) # Check we got right response @@ -93,7 +85,6 @@ async def test_login_success_extra(hass, hassio_client): "addon": "samba", "path": "/share", }, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, ) # Check we got right response diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 89f1483ffab466..a1b4ae2e900cfc 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -3,10 +3,9 @@ from homeassistant.setup import async_setup_component from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_HEADER_HA_AUTH +from homeassistant.const import EVENT_HOMEASSISTANT_START from tests.common import mock_coro -from . import API_PASSWORD async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): @@ -101,9 +100,7 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client Mock(return_value=mock_coro({"type": "abort"})), ) as mock_mqtt: await hass.async_start() - await async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) + await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() assert aioclient_mock.call_count == 2 @@ -151,7 +148,6 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): ) as mock_mqtt: resp = await hassio_client.post( "/api/hassio_push/discovery/testuuid", - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, json={"addon": "mosquitto", "service": "mqtt", "uuid": "testuuid"}, ) await hass.async_block_till_done() diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 8f77d6b6234b1c..96d53f93c3abda 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -4,19 +4,13 @@ import pytest -from homeassistant.const import HTTP_HEADER_HA_AUTH - -from . import API_PASSWORD - @asyncio.coroutine def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" aioclient_mock.post("http://127.0.0.1/beer", text="response") - resp = yield from hassio_client.post( - "/api/hassio/beer", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = yield from hassio_client.post("/api/hassio/beer") # Check we got right response assert resp.status == 200 @@ -87,9 +81,7 @@ def test_forward_log_request(hassio_client, aioclient_mock): """Test fetching normal log path doesn't remove ANSI color escape codes.""" aioclient_mock.get("http://127.0.0.1/beer/logs", text="\033[32mresponse\033[0m") - resp = yield from hassio_client.get( - "/api/hassio/beer/logs", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = yield from hassio_client.get("/api/hassio/beer/logs") # Check we got right response assert resp.status == 200 @@ -107,9 +99,7 @@ def test_bad_gateway_when_cannot_find_supervisor(hassio_client): "homeassistant.components.hassio.http.async_timeout.timeout", side_effect=asyncio.TimeoutError, ): - resp = yield from hassio_client.get( - "/api/hassio/addons/test/info", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = yield from hassio_client.get("/api/hassio/addons/test/info") assert resp.status == 502 diff --git a/tests/components/http/__init__.py b/tests/components/http/__init__.py index c4f73fd15a69e5..db5e1ea5c7a004 100644 --- a/tests/components/http/__init__.py +++ b/tests/components/http/__init__.py @@ -6,6 +6,10 @@ from homeassistant.components.http.const import KEY_REAL_IP +# Relic from the past. Kept here so we can run negative tests. +HTTP_HEADER_HA_AUTH = "X-HA-access" + + def mock_real_ip(app): """Inject middleware to mock real IP. diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 842201beaceef0..499ceab1556fd5 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -11,10 +11,8 @@ from homeassistant.components.http.auth import setup_auth, async_sign_path from homeassistant.components.http.const import KEY_AUTHENTICATED from homeassistant.components.http.real_ip import setup_real_ip -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.setup import async_setup_component -from . import mock_real_ip - +from . import mock_real_ip, HTTP_HEADER_HA_AUTH API_PASSWORD = "test-password" @@ -87,29 +85,29 @@ async def test_auth_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_access_with_password_in_header(app, aiohttp_client, legacy_auth, hass): +async def test_cant_access_with_password_in_header( + app, aiohttp_client, legacy_auth, hass +): """Test access with password in header.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: "wrong-pass"}) assert req.status == 401 -async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, hass): +async def test_cant_access_with_password_in_query( + app, aiohttp_client, legacy_auth, hass +): """Test access with password in URL.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) resp = await client.get("/", params={"api_password": API_PASSWORD}) - assert resp.status == 200 - assert await resp.json() == {"user_id": user.id} + assert resp.status == 401 resp = await client.get("/") assert resp.status == 401 @@ -118,15 +116,13 @@ async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, h assert resp.status == 401 -async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth): +async def test_basic_auth_does_not_work(app, aiohttp_client, hass, legacy_auth): """Test access with basic authentication.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD)) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 req = await client.get("/", auth=BasicAuth("wrong_username", API_PASSWORD)) assert req.status == 401 @@ -138,7 +134,7 @@ async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth): assert req.status == 401 -async def test_access_with_trusted_ip( +async def test_cannot_access_with_trusted_ip( hass, app2, trusted_networks_auth, aiohttp_client, hass_owner_user ): """Test access with an untrusted ip address.""" @@ -155,8 +151,7 @@ async def test_access_with_trusted_ip( for remote_addr in TRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert resp.status == 200, "{} should be trusted".format(remote_addr) - assert await resp.json() == {"user_id": hass_owner_user.id} + assert resp.status == 401, "{} shouldn't be trusted".format(remote_addr) async def test_auth_active_access_with_access_token_in_header( @@ -209,29 +204,24 @@ async def test_auth_active_access_with_trusted_ip( for remote_addr in TRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert resp.status == 200, "{} should be trusted".format(remote_addr) - assert await resp.json() == {"user_id": hass_owner_user.id} + assert resp.status == 401, "{} shouldn't be trusted".format(remote_addr) -async def test_auth_legacy_support_api_password_access( +async def test_auth_legacy_support_api_password_cannot_access( app, aiohttp_client, legacy_auth, hass ): """Test access using api_password if auth.support_legacy.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 resp = await client.get("/", params={"api_password": API_PASSWORD}) - assert resp.status == 200 - assert await resp.json() == {"user_id": user.id} + assert resp.status == 401 req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD)) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 async def test_auth_access_signed_path(hass, app, aiohttp_client, hass_access_token): diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index fc31de4b95081b..f50afcef8a83f0 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -148,6 +148,8 @@ async def mock_auth(request, handler): assert resp.status == 200 assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2 + # This used to check that with trusted networks we reset login attempts + # We no longer support trusted networks. resp = await client.get("/auth_true") assert resp.status == 200 - assert remote_ip not in app[KEY_FAILED_LOGIN_ATTEMPTS] + assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2 diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 99b9e0b6e9afb0..1cea900d9719cb 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -13,11 +13,12 @@ ) import pytest -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.setup import async_setup_component from homeassistant.components.http.cors import setup_cors from homeassistant.components.http.view import HomeAssistantView +from . import HTTP_HEADER_HA_AUTH + TRUSTED_ORIGIN = "https://home-assistant.io" @@ -91,13 +92,13 @@ async def test_cors_preflight_allowed(client): headers={ ORIGIN: TRUSTED_ORIGIN, ACCESS_CONTROL_REQUEST_METHOD: "GET", - ACCESS_CONTROL_REQUEST_HEADERS: "x-ha-access", + ACCESS_CONTROL_REQUEST_HEADERS: "x-requested-with", }, ) assert req.status == 200 assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == TRUSTED_ORIGIN - assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == HTTP_HEADER_HA_AUTH.upper() + assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == "X-REQUESTED-WITH" async def test_cors_middleware_with_cors_allowed_view(hass): diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index d8e613df6df01a..ad8e3ac10fd638 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -133,7 +133,7 @@ async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth): resp = await client.get("/api/", params={"api_password": "test-password"}) - assert resp.status == 200 + assert resp.status == 401 logs = caplog.text # Ensure we don't log API passwords diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index c210d773fafa40..3627c95040e07c 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -57,12 +57,7 @@ def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): self.hass.config.api = MagicMock(api_password="api_password") assert setup_component( - self.hass, - mqtt.DOMAIN, - { - "http": {"api_password": "http_secret"}, - mqtt.DOMAIN: {CONF_PASSWORD: password}, - }, + self.hass, mqtt.DOMAIN, {mqtt.DOMAIN: {CONF_PASSWORD: password}} ) self.hass.block_till_done() assert mock_mqtt.called diff --git a/tests/components/websocket_api/__init__.py b/tests/components/websocket_api/__init__.py index 56def1b7fd9d74..4904270cc72363 100644 --- a/tests/components/websocket_api/__init__.py +++ b/tests/components/websocket_api/__init__.py @@ -1,2 +1 @@ """Tests for the websocket API.""" -API_PASSWORD = "test-password" diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py index 2ee28c0cb20be6..382de3142e87d8 100644 --- a/tests/components/websocket_api/conftest.py +++ b/tests/components/websocket_api/conftest.py @@ -5,8 +5,6 @@ from homeassistant.components.websocket_api.http import URL from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED -from . import API_PASSWORD - @pytest.fixture def websocket_client(hass, hass_ws_client, hass_access_token): @@ -17,11 +15,7 @@ def websocket_client(hass, hass_ws_client, hass_access_token): @pytest.fixture def no_auth_websocket_client(hass, loop, aiohttp_client): """Websocket connection that requires authentication.""" - assert loop.run_until_complete( - async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) - ) + assert loop.run_until_complete(async_setup_component(hass, "websocket_api", {})) client = loop.run_until_complete(aiohttp_client(hass.http.app)) ws = loop.run_until_complete(client.ws_connect(URL)) diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 19b9cbb21968b7..0038750602024c 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -17,21 +17,10 @@ from tests.common import mock_coro -from . import API_PASSWORD - -async def test_auth_via_msg(no_auth_websocket_client, legacy_auth): - """Test authenticating.""" - await no_auth_websocket_client.send_json( - {"type": TYPE_AUTH, "api_password": API_PASSWORD} - ) - - msg = await no_auth_websocket_client.receive_json() - - assert msg["type"] == TYPE_AUTH_OK - - -async def test_auth_events(hass, no_auth_websocket_client, legacy_auth): +async def test_auth_events( + hass, no_auth_websocket_client, legacy_auth, hass_access_token +): """Test authenticating.""" connected_evt = [] hass.helpers.dispatcher.async_dispatcher_connect( @@ -42,7 +31,7 @@ async def test_auth_events(hass, no_auth_websocket_client, legacy_auth): SIGNAL_WEBSOCKET_DISCONNECTED, lambda: disconnected_evt.append(1) ) - await test_auth_via_msg(no_auth_websocket_client, legacy_auth) + await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token) assert len(connected_evt) == 1 assert not disconnected_evt @@ -60,7 +49,7 @@ async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): return_value=mock_coro(), ) as mock_process_wrong_login: await no_auth_websocket_client.send_json( - {"type": TYPE_AUTH, "api_password": API_PASSWORD + "wrong"} + {"type": TYPE_AUTH, "api_password": "wrong"} ) msg = await no_auth_websocket_client.receive_json() @@ -110,31 +99,25 @@ async def test_pre_auth_only_auth_allowed(no_auth_websocket_client): assert msg["message"].startswith("Auth message incorrectly formatted") -async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): +async def test_auth_active_with_token( + hass, no_auth_websocket_client, hass_access_token +): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) - - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(URL) as ws: - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_REQUIRED + assert await async_setup_component(hass, "websocket_api", {}) - await ws.send_json({"type": TYPE_AUTH, "access_token": hass_access_token}) + await no_auth_websocket_client.send_json( + {"type": TYPE_AUTH, "access_token": hass_access_token} + ) - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_OK + auth_msg = await no_auth_websocket_client.receive_json() + assert auth_msg["type"] == TYPE_AUTH_OK async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token): """Test authenticating with a token.""" refresh_token = await hass.auth.async_validate_access_token(hass_access_token) refresh_token.user.is_active = False - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) @@ -150,9 +133,7 @@ async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token async def test_auth_active_with_password_not_allow(hass, aiohttp_client): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) @@ -160,7 +141,7 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client): auth_msg = await ws.receive_json() assert auth_msg["type"] == TYPE_AUTH_REQUIRED - await ws.send_json({"type": TYPE_AUTH, "api_password": API_PASSWORD}) + await ws.send_json({"type": TYPE_AUTH, "api_password": "some-password"}) auth_msg = await ws.receive_json() assert auth_msg["type"] == TYPE_AUTH_INVALID @@ -168,28 +149,23 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client): async def test_auth_legacy_support_with_password(hass, aiohttp_client, legacy_auth): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) async with client.ws_connect(URL) as ws: - with patch("homeassistant.auth.AuthManager.support_legacy", return_value=True): - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_REQUIRED + auth_msg = await ws.receive_json() + assert auth_msg["type"] == TYPE_AUTH_REQUIRED - await ws.send_json({"type": TYPE_AUTH, "api_password": API_PASSWORD}) + await ws.send_json({"type": TYPE_AUTH, "api_password": "some-password"}) - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_OK + auth_msg = await ws.receive_json() + assert auth_msg["type"] == TYPE_AUTH_INVALID async def test_auth_with_invalid_token(hass, aiohttp_client): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index a39a0a0e7a6b64..1de5b8bb2c1bb0 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -14,8 +14,6 @@ from tests.common import async_mock_service -from . import API_PASSWORD - async def test_call_service(hass, websocket_client): """Test call service command.""" @@ -250,9 +248,7 @@ async def test_ping(websocket_client): async def test_call_service_context_with_user(hass, aiohttp_client, hass_access_token): """Test that the user is set in the service call context.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) calls = async_mock_service(hass, "domain_test", "test_service") client = await aiohttp_client(hass.http.app) diff --git a/tests/components/websocket_api/test_sensor.py b/tests/components/websocket_api/test_sensor.py index 873b9e7269c226..84b730606987a7 100644 --- a/tests/components/websocket_api/test_sensor.py +++ b/tests/components/websocket_api/test_sensor.py @@ -3,10 +3,12 @@ from homeassistant.bootstrap import async_setup_component from tests.common import assert_setup_component -from .test_auth import test_auth_via_msg +from .test_auth import test_auth_active_with_token -async def test_websocket_api(hass, no_auth_websocket_client, legacy_auth): +async def test_websocket_api( + hass, no_auth_websocket_client, hass_access_token, legacy_auth +): """Test API streams.""" with assert_setup_component(1): await async_setup_component( @@ -16,7 +18,7 @@ async def test_websocket_api(hass, no_auth_websocket_client, legacy_auth): state = hass.states.get("sensor.connected_clients") assert state.state == "0" - await test_auth_via_msg(no_auth_websocket_client, legacy_auth) + await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token) state = hass.states.get("sensor.connected_clients") assert state.state == "1" diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 18143c088be58a..5199f01807f5ef 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -92,8 +92,8 @@ def test_secrets(isfile_patch, loop): files = { get_test_config_dir(YAML_CONFIG_FILE): BASE_CONFIG - + ("http:\n" " api_password: !secret http_pw"), - secrets_path: ("logger: debug\n" "http_pw: abc123"), + + ("http:\n" " cors_allowed_origins: !secret http_pw"), + secrets_path: ("logger: debug\n" "http_pw: http://google.com"), } with patch_yaml_files(files): @@ -103,17 +103,15 @@ def test_secrets(isfile_patch, loop): assert res["except"] == {} assert res["components"].keys() == {"homeassistant", "http"} assert res["components"]["http"] == { - "api_password": "abc123", - "cors_allowed_origins": ["https://cast.home-assistant.io"], + "cors_allowed_origins": ["http://google.com"], "ip_ban_enabled": True, "login_attempts_threshold": -1, "server_host": "0.0.0.0", "server_port": 8123, - "trusted_networks": [], "ssl_profile": "modern", } - assert res["secret_cache"] == {secrets_path: {"http_pw": "abc123"}} - assert res["secrets"] == {"http_pw": "abc123"} + assert res["secret_cache"] == {secrets_path: {"http_pw": "http://google.com"}} + assert res["secrets"] == {"http_pw": "http://google.com"} assert normalize_yaml_files(res) == [ ".../configuration.yaml", ".../secrets.yaml", diff --git a/tests/test_config.py b/tests/test_config.py index a67cd34579771e..362608e7af28b7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,7 +5,6 @@ import os import unittest.mock as mock from collections import OrderedDict -from ipaddress import ip_network import asynctest import pytest @@ -876,48 +875,6 @@ async def test_auth_provider_config_default(hass): assert hass.auth.auth_mfa_modules[0].id == "totp" -async def test_auth_provider_config_default_api_password(hass): - """Test loading default auth provider config with api password.""" - core_config = { - "latitude": 60, - "longitude": 50, - "elevation": 25, - "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, - "time_zone": "GMT", - } - if hasattr(hass, "auth"): - del hass.auth - await config_util.async_process_ha_core_config(hass, core_config, "pass") - - assert len(hass.auth.auth_providers) == 2 - assert hass.auth.auth_providers[0].type == "homeassistant" - assert hass.auth.auth_providers[1].type == "legacy_api_password" - assert hass.auth.auth_providers[1].api_password == "pass" - - -async def test_auth_provider_config_default_trusted_networks(hass): - """Test loading default auth provider config with trusted networks.""" - core_config = { - "latitude": 60, - "longitude": 50, - "elevation": 25, - "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, - "time_zone": "GMT", - } - if hasattr(hass, "auth"): - del hass.auth - await config_util.async_process_ha_core_config( - hass, core_config, trusted_networks=["192.168.0.1"] - ) - - assert len(hass.auth.auth_providers) == 2 - assert hass.auth.auth_providers[0].type == "homeassistant" - assert hass.auth.auth_providers[1].type == "trusted_networks" - assert hass.auth.auth_providers[1].trusted_networks[0] == ip_network("192.168.0.1") - - async def test_disallowed_auth_provider_config(hass): """Test loading insecure example auth provider is disallowed.""" core_config = { From 3cb844f22c46bdcaf177ab2fcdc4852f3568c98d Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Mon, 14 Oct 2019 18:53:59 -0400 Subject: [PATCH 0899/3953] Add Apprise notification integration (#26868) * Added apprise notification component * flake-8 fixes; black formatting + import merged to 1 line * pylint issues resolved * added github name to manifest.json * import moved to top as per code review request * manifest formatting to avoid failing ci * .coveragerc updated to include apprise * removed block for written tests * more test coverage * formatting as per code review * tests converted to async style as per code review * increased coverage * bumped version of apprise to 0.8.1 * test that mocked entries are called * added tests for hass.service loading * support tags for those who identify the TARGET option * renamed variable as per code review * 'assert not' used instead of 'is False' * added period (in case linter isn't happy) --- CODEOWNERS | 1 + homeassistant/components/apprise/__init__.py | 1 + .../components/apprise/manifest.json | 12 ++ homeassistant/components/apprise/notify.py | 73 +++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/apprise/__init__.py | 1 + tests/components/apprise/test_notify.py | 148 ++++++++++++++++++ 8 files changed, 242 insertions(+) create mode 100644 homeassistant/components/apprise/__init__.py create mode 100644 homeassistant/components/apprise/manifest.json create mode 100644 homeassistant/components/apprise/notify.py create mode 100644 tests/components/apprise/__init__.py create mode 100644 tests/components/apprise/test_notify.py diff --git a/CODEOWNERS b/CODEOWNERS index ea50d24095c1ba..8e52210cec7480 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -26,6 +26,7 @@ homeassistant/components/ambient_station/* @bachya homeassistant/components/androidtv/* @JeffLIrion homeassistant/components/apache_kafka/* @bachya homeassistant/components/api/* @home-assistant/core +homeassistant/components/apprise/* @caronc homeassistant/components/aprs/* @PhilRW homeassistant/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff diff --git a/homeassistant/components/apprise/__init__.py b/homeassistant/components/apprise/__init__.py new file mode 100644 index 00000000000000..6ffdaf690d9409 --- /dev/null +++ b/homeassistant/components/apprise/__init__.py @@ -0,0 +1 @@ +"""The apprise component.""" diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json new file mode 100644 index 00000000000000..3e971a96e7eac8 --- /dev/null +++ b/homeassistant/components/apprise/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "apprise", + "name": "Apprise", + "documentation": "https://www.home-assistant.io/components/apprise", + "requirements": [ + "apprise==0.8.1" + ], + "dependencies": [], + "codeowners": [ + "@caronc" + ] +} diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py new file mode 100644 index 00000000000000..662cc9c1ab6ef0 --- /dev/null +++ b/homeassistant/components/apprise/notify.py @@ -0,0 +1,73 @@ +"""Apprise platform for notify component.""" +import logging + +import voluptuous as vol + +import apprise + +import homeassistant.helpers.config_validation as cv + +from homeassistant.components.notify import ( + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) + +_LOGGER = logging.getLogger(__name__) + +CONF_FILE = "config" +CONF_URL = "url" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_URL): vol.All(cv.ensure_list, [str]), + vol.Optional(CONF_FILE): cv.string, + } +) + + +def get_service(hass, config, discovery_info=None): + """Get the Apprise notification service.""" + + # Create our object + a_obj = apprise.Apprise() + + if config.get(CONF_FILE): + # Sourced from a Configuration File + a_config = apprise.AppriseConfig() + if not a_config.add(config[CONF_FILE]): + _LOGGER.error("Invalid Apprise config url provided") + return None + + if not a_obj.add(a_config): + _LOGGER.error("Invalid Apprise config url provided") + return None + + if config.get(CONF_URL): + # Ordered list of URLs + if not a_obj.add(config[CONF_URL]): + _LOGGER.error("Invalid Apprise URL(s) supplied") + return None + + return AppriseNotificationService(a_obj) + + +class AppriseNotificationService(BaseNotificationService): + """Implement the notification service for Apprise.""" + + def __init__(self, a_obj): + """Initialize the service.""" + self.apprise = a_obj + + def send_message(self, message="", **kwargs): + """Send a message to a specified target. + + If no target/tags are specified, then services are notified as is + However, if any tags are specified, then they will be applied + to the notification causing filtering (if set up that way). + """ + targets = kwargs.get(ATTR_TARGET) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + self.apprise.notify(body=message, title=title, tag=targets) diff --git a/requirements_all.txt b/requirements_all.txt index dbc94de9a1cff3..ef07a3f44b7246 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -217,6 +217,9 @@ apcaccess==0.0.13 # homeassistant.components.apns apns2==0.3.0 +# homeassistant.components.apprise +apprise==0.8.1 + # homeassistant.components.aprs aprslib==0.6.46 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18970fcbac0373..967943894fb6a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,6 +103,9 @@ androidtv==0.0.30 # homeassistant.components.apns apns2==0.3.0 +# homeassistant.components.apprise +apprise==0.8.1 + # homeassistant.components.aprs aprslib==0.6.46 diff --git a/tests/components/apprise/__init__.py b/tests/components/apprise/__init__.py new file mode 100644 index 00000000000000..ffebc35b4e1469 --- /dev/null +++ b/tests/components/apprise/__init__.py @@ -0,0 +1 @@ +"""Tests for the apprise component.""" diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py new file mode 100644 index 00000000000000..237f99de676faa --- /dev/null +++ b/tests/components/apprise/test_notify.py @@ -0,0 +1,148 @@ +"""The tests for the apprise notification platform.""" +from unittest.mock import patch +from unittest.mock import MagicMock + +from homeassistant.setup import async_setup_component + +BASE_COMPONENT = "notify" + + +async def test_apprise_config_load_fail01(hass): + """Test apprise configuration failures 1.""" + + config = { + BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"} + } + + with patch("apprise.AppriseConfig.add", return_value=False): + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test that our service failed to load + assert not hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_config_load_fail02(hass): + """Test apprise configuration failures 2.""" + + config = { + BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"} + } + + with patch("apprise.Apprise.add", return_value=False): + with patch("apprise.AppriseConfig.add", return_value=True): + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test that our service failed to load + assert not hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_config_load_okay(hass, tmp_path): + """Test apprise configuration failures.""" + + # Test cases where our URL is invalid + d = tmp_path / "apprise-config" + d.mkdir() + f = d / "apprise" + f.write_text("mailto://user:pass@example.com/") + + config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}} + + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Valid configuration was loaded; our service is good + assert hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_url_load_fail(hass): + """Test apprise url failure.""" + + config = { + BASE_COMPONENT: { + "name": "test", + "platform": "apprise", + "url": "mailto://user:pass@example.com", + } + } + with patch("apprise.Apprise.add", return_value=False): + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test that our service failed to load + assert not hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_notification(hass): + """Test apprise notification.""" + + config = { + BASE_COMPONENT: { + "name": "test", + "platform": "apprise", + "url": "mailto://user:pass@example.com", + } + } + + # Our Message + data = {"title": "Test Title", "message": "Test Message"} + + with patch("apprise.Apprise") as mock_apprise: + obj = MagicMock() + obj.add.return_value = True + obj.notify.return_value = True + mock_apprise.return_value = obj + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test the existance of our service + assert hass.services.has_service(BASE_COMPONENT, "test") + + # Test the call to our underlining notify() call + await hass.services.async_call(BASE_COMPONENT, "test", data) + await hass.async_block_till_done() + + # Validate calls were made under the hood correctly + obj.add.assert_called_once_with([config[BASE_COMPONENT]["url"]]) + obj.notify.assert_called_once_with( + **{"body": data["message"], "title": data["title"], "tag": None} + ) + + +async def test_apprise_notification_with_target(hass, tmp_path): + """Test apprise notification with a target.""" + + # Test cases where our URL is invalid + d = tmp_path / "apprise-config" + d.mkdir() + f = d / "apprise" + + # Write 2 config entries each assigned to different tags + f.write_text("devops=mailto://user:pass@example.com/\r\n") + f.write_text("system,alert=syslog://\r\n") + + config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}} + + # Our Message, only notify the services tagged with "devops" + data = {"title": "Test Title", "message": "Test Message", "target": ["devops"]} + + with patch("apprise.Apprise") as mock_apprise: + apprise_obj = MagicMock() + apprise_obj.add.return_value = True + apprise_obj.notify.return_value = True + mock_apprise.return_value = apprise_obj + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test the existance of our service + assert hass.services.has_service(BASE_COMPONENT, "test") + + # Test the call to our underlining notify() call + await hass.services.async_call(BASE_COMPONENT, "test", data) + await hass.async_block_till_done() + + # Validate calls were made under the hood correctly + apprise_obj.notify.assert_called_once_with( + **{"body": data["message"], "title": data["title"], "tag": data["target"]} + ) From d8e325560369c8e8e0d1d42496a5d1d13f11dc85 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 15 Oct 2019 00:31:44 +0000 Subject: [PATCH 0900/3953] [ci skip] Translation update --- .../components/abode/.translations/ca.json | 22 +++++++++++++++++++ .../components/abode/.translations/da.json | 22 +++++++++++++++++++ .../components/abode/.translations/es.json | 5 +++++ .../components/abode/.translations/lb.json | 22 +++++++++++++++++++ .../components/abode/.translations/nn.json | 5 +++++ .../components/abode/.translations/ru.json | 22 +++++++++++++++++++ .../components/abode/.translations/sl.json | 22 +++++++++++++++++++ .../abode/.translations/zh-Hant.json | 22 +++++++++++++++++++ .../components/adguard/.translations/nn.json | 3 ++- .../ambiclimate/.translations/nn.json | 5 +++++ .../ambient_station/.translations/nn.json | 5 +++++ .../ambient_station/.translations/ru.json | 4 ++-- .../components/axis/.translations/ru.json | 10 ++++----- .../cert_expiry/.translations/ru.json | 14 ++++++------ .../components/cover/.translations/ca.json | 10 +++++++++ .../components/cover/.translations/lb.json | 10 +++++++++ .../components/cover/.translations/sl.json | 10 +++++++++ .../components/deconz/.translations/no.json | 1 + .../components/deconz/.translations/ru.json | 12 +++++----- .../components/esphome/.translations/ru.json | 2 +- .../components/hangouts/.translations/ru.json | 2 +- .../components/heos/.translations/ru.json | 2 +- .../homekit_controller/.translations/ru.json | 4 ++-- .../homematicip_cloud/.translations/ru.json | 6 ++--- .../components/hue/.translations/ru.json | 16 +++++++------- .../iaqualink/.translations/ru.json | 2 +- .../components/ipma/.translations/ru.json | 2 +- .../components/iqvia/.translations/ru.json | 2 +- .../components/life360/.translations/ru.json | 8 +++---- .../components/linky/.translations/ru.json | 10 ++++----- .../components/locative/.translations/hu.json | 2 +- .../components/lock/.translations/ca.json | 8 +++++++ .../components/lock/.translations/lb.json | 8 +++++++ .../components/lock/.translations/sl.json | 8 +++++++ .../luftdaten/.translations/nn.json | 5 +++++ .../luftdaten/.translations/ru.json | 4 ++-- .../components/mailgun/.translations/ru.json | 2 +- .../components/met/.translations/nn.json | 3 ++- .../components/met/.translations/ru.json | 4 ++-- .../components/mqtt/.translations/ru.json | 2 +- .../components/neato/.translations/ru.json | 4 ++-- .../components/nest/.translations/ru.json | 6 ++--- .../components/notion/.translations/nn.json | 11 ++++++++++ .../components/notion/.translations/ru.json | 6 ++--- .../components/openuv/.translations/ru.json | 2 +- .../owntracks/.translations/ru.json | 2 +- .../components/plaato/.translations/ru.json | 2 +- .../components/plex/.translations/ru.json | 16 +++++++------- .../rainmachine/.translations/ru.json | 2 +- .../components/sensor/.translations/no.json | 2 +- .../simplisafe/.translations/ru.json | 2 +- .../smartthings/.translations/nn.json | 5 +++++ .../smartthings/.translations/ru.json | 2 +- .../components/smhi/.translations/ru.json | 2 +- .../solaredge/.translations/ru.json | 4 ++-- .../tellduslive/.translations/ru.json | 8 +++---- .../components/toon/.translations/ru.json | 2 +- .../components/tradfri/.translations/ru.json | 2 +- .../transmission/.translations/ru.json | 4 ++-- .../components/twilio/.translations/ru.json | 2 +- .../components/unifi/.translations/ru.json | 6 ++--- .../components/upnp/.translations/nn.json | 8 +++++++ .../components/upnp/.translations/ru.json | 6 ++--- .../components/velbus/.translations/ru.json | 2 +- .../components/vesync/.translations/ru.json | 2 +- .../components/zha/.translations/ru.json | 2 +- .../components/zwave/.translations/ru.json | 4 ++-- 67 files changed, 341 insertions(+), 103 deletions(-) create mode 100644 homeassistant/components/abode/.translations/ca.json create mode 100644 homeassistant/components/abode/.translations/da.json create mode 100644 homeassistant/components/abode/.translations/es.json create mode 100644 homeassistant/components/abode/.translations/lb.json create mode 100644 homeassistant/components/abode/.translations/nn.json create mode 100644 homeassistant/components/abode/.translations/ru.json create mode 100644 homeassistant/components/abode/.translations/sl.json create mode 100644 homeassistant/components/abode/.translations/zh-Hant.json create mode 100644 homeassistant/components/ambiclimate/.translations/nn.json create mode 100644 homeassistant/components/ambient_station/.translations/nn.json create mode 100644 homeassistant/components/cover/.translations/ca.json create mode 100644 homeassistant/components/cover/.translations/lb.json create mode 100644 homeassistant/components/cover/.translations/sl.json create mode 100644 homeassistant/components/lock/.translations/ca.json create mode 100644 homeassistant/components/lock/.translations/lb.json create mode 100644 homeassistant/components/lock/.translations/sl.json create mode 100644 homeassistant/components/luftdaten/.translations/nn.json create mode 100644 homeassistant/components/notion/.translations/nn.json create mode 100644 homeassistant/components/smartthings/.translations/nn.json diff --git a/homeassistant/components/abode/.translations/ca.json b/homeassistant/components/abode/.translations/ca.json new file mode 100644 index 00000000000000..2424fd9b5f0ef8 --- /dev/null +++ b/homeassistant/components/abode/.translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'Abode." + }, + "error": { + "connection_error": "No es pot connectar amb Abode.", + "identifier_exists": "Compte ja registrat.", + "invalid_credentials": "Credencials inv\u00e0lides." + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Introdueix la teva informaci\u00f3 d'inici de sessi\u00f3 a Abode." + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/da.json b/homeassistant/components/abode/.translations/da.json new file mode 100644 index 00000000000000..3f094cb93bd13f --- /dev/null +++ b/homeassistant/components/abode/.translations/da.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Abode." + }, + "error": { + "connection_error": "Kunne ikke oprette forbindelse til Abode.", + "identifier_exists": "Konto er allerede registreret.", + "invalid_credentials": "Ugyldige legitimationsoplysninger." + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Email adresse" + }, + "title": "Udfyld dine Abode-loginoplysninger" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/es.json b/homeassistant/components/abode/.translations/es.json new file mode 100644 index 00000000000000..e0c1b6d6a7d607 --- /dev/null +++ b/homeassistant/components/abode/.translations/es.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/lb.json b/homeassistant/components/abode/.translations/lb.json new file mode 100644 index 00000000000000..ed65a5df7c579a --- /dev/null +++ b/homeassistant/components/abode/.translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun ZHA ass erlaabt." + }, + "error": { + "connection_error": "Kann sech net mat Abode verbannen.", + "identifier_exists": "Konto ass scho registr\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail Adress" + }, + "title": "F\u00ebllt \u00e4r Abode Login Informatiounen aus." + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/nn.json b/homeassistant/components/abode/.translations/nn.json new file mode 100644 index 00000000000000..e0c1b6d6a7d607 --- /dev/null +++ b/homeassistant/components/abode/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/ru.json b/homeassistant/components/abode/.translations/ru.json new file mode 100644 index 00000000000000..f39e6b1443b0f5 --- /dev/null +++ b/homeassistant/components/abode/.translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a Abode.", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "title": "Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/sl.json b/homeassistant/components/abode/.translations/sl.json new file mode 100644 index 00000000000000..b840913b7bea76 --- /dev/null +++ b/homeassistant/components/abode/.translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Dovoljena je samo ena konfiguracija Abode." + }, + "error": { + "connection_error": "Ni mogo\u010de vzpostaviti povezave z Abode.", + "identifier_exists": "Ra\u010dun je \u017ee registriran.", + "invalid_credentials": "Neveljavne poverilnice." + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "title": "Izpolnite svoje podatke za prijavo v Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/zh-Hant.json b/homeassistant/components/abode/.translations/zh-Hant.json new file mode 100644 index 00000000000000..5bc9efc36969e3 --- /dev/null +++ b/homeassistant/components/abode/.translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 Abode\u3002" + }, + "error": { + "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 Abode\u3002", + "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a\u3002", + "invalid_credentials": "\u6191\u8b49\u7121\u6548\u3002" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + }, + "title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/nn.json b/homeassistant/components/adguard/.translations/nn.json index 7c129cba3afc71..0e2e82437e827a 100644 --- a/homeassistant/components/adguard/.translations/nn.json +++ b/homeassistant/components/adguard/.translations/nn.json @@ -6,6 +6,7 @@ "username": "Brukarnamn" } } - } + }, + "title": "AdGuard Home" } } \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/nn.json b/homeassistant/components/ambiclimate/.translations/nn.json new file mode 100644 index 00000000000000..ce8a3ed9db6b49 --- /dev/null +++ b/homeassistant/components/ambiclimate/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Ambiclimate" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/nn.json b/homeassistant/components/ambient_station/.translations/nn.json new file mode 100644 index 00000000000000..0f878b363c92f9 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index 2d7964f18ebe61..3a7c405ea4cd48 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -2,8 +2,8 @@ "config": { "error": { "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", - "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", - "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b" + "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", + "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." }, "step": { "user": { diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 951263d53f9668..ae5f0851c44d7d 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", - "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", - "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis" + "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", + "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis." }, "error": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", - "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e", - "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.", + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json index d962c7931218c7..f9f9e2063bee54 100644 --- a/homeassistant/components/cert_expiry/.translations/ru.json +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -4,19 +4,19 @@ "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." }, "error": { - "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430", - "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443", + "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430.", + "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442" + "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442." }, "step": { "user": { "data": { - "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", - "port": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "port": "\u041f\u043e\u0440\u0442" }, - "title": "C\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + "title": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" } }, "title": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" diff --git a/homeassistant/components/cover/.translations/ca.json b/homeassistant/components/cover/.translations/ca.json new file mode 100644 index 00000000000000..ffa9ca1a927284 --- /dev/null +++ b/homeassistant/components/cover/.translations/ca.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est\u00e0 tancat/da", + "is_closing": "{entity_name} est\u00e0 tancan't-se", + "is_open": "{entity_name} est\u00e0 obert/a", + "is_opening": "{entity_name} s'est\u00e0 obrint" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/lb.json b/homeassistant/components/cover/.translations/lb.json new file mode 100644 index 00000000000000..b0c9e1d0d4c3fc --- /dev/null +++ b/homeassistant/components/cover/.translations/lb.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} ass zou", + "is_closing": "{entity_name} g\u00ebtt zougemaach", + "is_open": "{entity_name} ass op", + "is_opening": "{entity_name} g\u00ebtt opgemaach" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/sl.json b/homeassistant/components/cover/.translations/sl.json new file mode 100644 index 00000000000000..cb5109b5cb05a5 --- /dev/null +++ b/homeassistant/components/cover/.translations/sl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} je/so zaprt/a", + "is_closing": "{entity_name} se zapira/jo", + "is_open": "{entity_name} je odprt/a/o", + "is_opening": "{entity_name} se odpira/jo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 71fba6043f75b9..7db8f3f118dc1a 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -11,6 +11,7 @@ "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index a7200a0cbb4796..f342f3145b9f28 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -3,13 +3,13 @@ "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", - "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", - "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ", - "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ", - "updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d" + "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ.", + "updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." }, "error": { - "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API" + "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." }, "step": { "hassio_confirm": { @@ -28,7 +28,7 @@ "title": "deCONZ" }, "link": { - "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb", + "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.", "title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ" }, "options": { diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index 62d24662ab6a77..27d223012c0923 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -6,7 +6,7 @@ "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", "invalid_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c!", - "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips." }, "flow_title": "ESPHome: {name}", "step": { diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 6942f683fa6e4a..15d90a672ded13 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "invalid_2fa": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", diff --git a/homeassistant/components/heos/.translations/ru.json b/homeassistant/components/heos/.translations/ru.json index f19b5e5206433d..8aacc8e165dd3f 100644 --- a/homeassistant/components/heos/.translations/ru.json +++ b/homeassistant/components/heos/.translations/ru.json @@ -4,7 +4,7 @@ "already_setup": "\u041d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 HEOS \u0432 \u0441\u0435\u0442\u0438." }, "error": { - "connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0445\u043e\u0441\u0442\u0443" + "connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0445\u043e\u0441\u0442\u0443." }, "step": { "user": { diff --git a/homeassistant/components/homekit_controller/.translations/ru.json b/homeassistant/components/homekit_controller/.translations/ru.json index c7770c6a064b34..44a57a1eb258ed 100644 --- a/homeassistant/components/homekit_controller/.translations/ru.json +++ b/homeassistant/components/homekit_controller/.translations/ru.json @@ -24,14 +24,14 @@ "data": { "pairing_code": "\u041a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit" }, "user": { "data": { "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 5155a42c4c3b55..3170f4bf6cc193 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP", + "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP.", "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "press_the_button": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443.", - "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430", - "timeout_button": "\u0412\u044b \u043d\u0435 \u043d\u0430\u0436\u0430\u043b\u0438 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430" + "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", + "timeout_button": "\u0412\u044b \u043d\u0435 \u043d\u0430\u0436\u0430\u043b\u0438 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430." }, "step": { "init": { diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index 79a46e1861bce6..08fda906ea96de 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -4,15 +4,15 @@ "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", - "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", - "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", - "not_hue_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c Hue", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", + "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d.", + "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "not_hue_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c Hue.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "linking": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f", - "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430" + "linking": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", + "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430." }, "step": { "init": { @@ -23,7 +23,7 @@ }, "link": { "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435 \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 Philips Hue \u0432 Home Assistant.\n\n![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435](/static/images/config_philips_hue.jpg)", - "title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c" + "title": "Philips Hue" } }, "title": "Philips Hue" diff --git a/homeassistant/components/iaqualink/.translations/ru.json b/homeassistant/components/iaqualink/.translations/ru.json index 35444dd422b379..9a93c19ef20bdb 100644 --- a/homeassistant/components/iaqualink/.translations/ru.json +++ b/homeassistant/components/iaqualink/.translations/ru.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", "title": "Jandy iAqualink" diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json index a302572ed121d5..0db504c629cb5c 100644 --- a/homeassistant/components/ipma/.translations/ru.json +++ b/homeassistant/components/ipma/.translations/ru.json @@ -10,7 +10,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0438 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u044b", + "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0438 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u044b.", "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" } }, diff --git a/homeassistant/components/iqvia/.translations/ru.json b/homeassistant/components/iqvia/.translations/ru.json index 0c3afc88c94dd0..336877fda130ce 100644 --- a/homeassistant/components/iqvia/.translations/ru.json +++ b/homeassistant/components/iqvia/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", - "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" + "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441." }, "step": { "user": { diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index d033da4bae71eb..eba3a47ead8088 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "create_entry": { "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "error": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", - "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d", - "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", + "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360.", "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "step": { diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index b569cce9239066..463343490a7993 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -4,11 +4,11 @@ "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { - "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443", - "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", + "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443.", + "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", - "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { "user": { @@ -16,7 +16,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "title": "Linky" } }, diff --git a/homeassistant/components/locative/.translations/hu.json b/homeassistant/components/locative/.translations/hu.json index e90910c29a2008..3528f1c1e45be3 100644 --- a/homeassistant/components/locative/.translations/hu.json +++ b/homeassistant/components/locative/.translations/hu.json @@ -6,7 +6,7 @@ }, "step": { "user": { - "description": "Biztosan be szeretn\u00e9d be\u00e1ll\u00edtani a Locative Webhookot?", + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Locative Webhook-ot?", "title": "Locative Webhook be\u00e1ll\u00edt\u00e1sa" } }, diff --git a/homeassistant/components/lock/.translations/ca.json b/homeassistant/components/lock/.translations/ca.json new file mode 100644 index 00000000000000..0e05d512bf4993 --- /dev/null +++ b/homeassistant/components/lock/.translations/ca.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} est\u00e0 bloquejat/ada", + "is_unlocked": "{entity_name} est\u00e0 desbloquejat/ada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/lb.json b/homeassistant/components/lock/.translations/lb.json new file mode 100644 index 00000000000000..4526b8fb6744b4 --- /dev/null +++ b/homeassistant/components/lock/.translations/lb.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} ass gespaart", + "is_unlocked": "{entity_name} ass entspaart" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/sl.json b/homeassistant/components/lock/.translations/sl.json new file mode 100644 index 00000000000000..3c3fd5defbc9f1 --- /dev/null +++ b/homeassistant/components/lock/.translations/sl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} je/so zaklenjen/a", + "is_unlocked": "{entity_name} je/so odklenjen/a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/nn.json b/homeassistant/components/luftdaten/.translations/nn.json new file mode 100644 index 00000000000000..52b1ec331662a3 --- /dev/null +++ b/homeassistant/components/luftdaten/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Luftdaten" + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/ru.json b/homeassistant/components/luftdaten/.translations/ru.json index 7ae83b550e3d44..1a05137f82d310 100644 --- a/homeassistant/components/luftdaten/.translations/ru.json +++ b/homeassistant/components/luftdaten/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "error": { - "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten", - "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d", + "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten.", + "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { diff --git a/homeassistant/components/mailgun/.translations/ru.json b/homeassistant/components/mailgun/.translations/ru.json index 39503154b6caa1..094940e6f905e6 100644 --- a/homeassistant/components/mailgun/.translations/ru.json +++ b/homeassistant/components/mailgun/.translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Mailgun?", - "title": "Mailgun Webhook" + "title": "Mailgun" } }, "title": "Mailgun" diff --git a/homeassistant/components/met/.translations/nn.json b/homeassistant/components/met/.translations/nn.json index 0e024a0e1eb7b0..6daa5b2657ad22 100644 --- a/homeassistant/components/met/.translations/nn.json +++ b/homeassistant/components/met/.translations/nn.json @@ -6,6 +6,7 @@ "name": "Namn" } } - } + }, + "title": "Met.no" } } \ No newline at end of file diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json index 559382cf209d67..768152084aa410 100644 --- a/homeassistant/components/met/.translations/ru.json +++ b/homeassistant/components/met/.translations/ru.json @@ -11,10 +11,10 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442", + "description": "\u041d\u043e\u0440\u0432\u0435\u0436\u0441\u043a\u0438\u0439 \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442.", "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" } }, - "title": "Met.no" + "title": "\u041c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u041d\u043e\u0440\u0432\u0435\u0433\u0438\u0438 (Met.no)" } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json index ac27652cbdd6b6..925b8cf5ab4fec 100644 --- a/homeassistant/components/mqtt/.translations/ru.json +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443" + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443." }, "step": { "broker": { diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json index 1a206258e24979..999e45880cfdcf 100644 --- a/homeassistant/components/neato/.translations/ru.json +++ b/homeassistant/components/neato/.translations/ru.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "create_entry": { "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "error": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index ac88ed224edb26..ba49b788b9a639 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -7,10 +7,10 @@ "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Nest \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." }, "error": { - "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430", - "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434", + "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", + "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430" + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430." }, "step": { "init": { diff --git a/homeassistant/components/notion/.translations/nn.json b/homeassistant/components/notion/.translations/nn.json new file mode 100644 index 00000000000000..6d373424c2869e --- /dev/null +++ b/homeassistant/components/notion/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Notion" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index 7345cf462957c9..6c1d5f5d8d7715 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -2,14 +2,14 @@ "config": { "error": { "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c", - "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e" + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e." }, "step": { "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b" }, "title": "Notion" } diff --git a/homeassistant/components/openuv/.translations/ru.json b/homeassistant/components/openuv/.translations/ru.json index 58d57b280567f1..27d2921a7d4205 100644 --- a/homeassistant/components/openuv/.translations/ru.json +++ b/homeassistant/components/openuv/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b.", - "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API" + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/.translations/ru.json b/homeassistant/components/owntracks/.translations/ru.json index 6ebaa31cacf04b..31c3e77279dcfe 100644 --- a/homeassistant/components/owntracks/.translations/ru.json +++ b/homeassistant/components/owntracks/.translations/ru.json @@ -4,7 +4,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u0435\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/ru.json b/homeassistant/components/plaato/.translations/ru.json index 59964fdedd63a4..dc06e3ddab0668 100644 --- a/homeassistant/components/plaato/.translations/ru.json +++ b/homeassistant/components/plaato/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Plaato Airlock\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index fe773f72be9721..bce55d35baa449 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -5,15 +5,15 @@ "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", "discovery_no_file": "\u0421\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", - "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", - "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430", - "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" + "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430.", + "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430.", + "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435." }, "error": { - "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", - "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e", - "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443", - "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d" + "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.", + "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443.", + "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d." }, "step": { "manual_setup": { @@ -34,7 +34,7 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" }, "start_website_auth": { - "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", + "description": "\u041f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", "title": "Plex" }, "user": { diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index 6248890389d140..df9adf2d989cd1 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json index 5f5eeaacd11e75..6709e4eb28c40f 100644 --- a/homeassistant/components/sensor/.translations/no.json +++ b/homeassistant/components/sensor/.translations/no.json @@ -4,7 +4,7 @@ "is_battery_level": "{entity_name} batteriniv\u00e5", "is_humidity": "{entity_name} fuktighet", "is_illuminance": "{entity_name} belysningsstyrke", - "is_power": "{entity_name} str\u00f8m", + "is_power": "{entity_name} effekt", "is_pressure": "{entity_name} trykk", "is_signal_strength": "{entity_name} signalstyrke", "is_temperature": "{entity_name} temperatur", diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index e82172f92f8579..721ba69d67ef61 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/nn.json b/homeassistant/components/smartthings/.translations/nn.json new file mode 100644 index 00000000000000..929e95dc2ffeb0 --- /dev/null +++ b/homeassistant/components/smartthings/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/ru.json b/homeassistant/components/smartthings/.translations/ru.json index 575c593d5a481e..f07586c16e306d 100644 --- a/homeassistant/components/smartthings/.translations/ru.json +++ b/homeassistant/components/smartthings/.translations/ru.json @@ -6,7 +6,7 @@ "base_url_not_https": "\u0412 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0435 `http` \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 `base_url`, \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u0441 `https://`.", "token_already_setup": "\u0422\u043e\u043a\u0435\u043d \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "token_forbidden": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f OAuth.", - "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UID / GUID", + "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UID / GUID.", "token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u043b\u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d.", "webhook_error": "SmartThings \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443, \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u0432 `base_url`. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043a \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443." }, diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json index 03b17b3ba8b85c..f3ba34adac38e6 100644 --- a/homeassistant/components/smhi/.translations/ru.json +++ b/homeassistant/components/smhi/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", - "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438" + "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json index d8622cdd2c1a3b..e6e7094648d829 100644 --- a/homeassistant/components/solaredge/.translations/ru.json +++ b/homeassistant/components/solaredge/.translations/ru.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u0430\u0439\u0442\u0430", + "api_key": "\u041a\u043b\u044e\u0447 API", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "site_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0430\u0439\u0442\u0430 SolarEdge" + "site_id": "site-id" }, "title": "SolarEdge" } diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index 9d3c97ad902eb3..41dc39146e8d1c 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -4,15 +4,15 @@ "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.", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "auth_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443" + "auth_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." }, "step": { "auth": { - "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0430\u043a\u043a\u0430\u0443\u043d\u0442 TelldusLive:\n 1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u043d\u0438\u0436\u0435\n 2. \u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Telldus Live\n 3. Authorize **{app_name}** (\u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Yes**).\n 4. \u0412\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.\n\n [\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 TelldusLive]({auth_url})", - "title": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 TelldusLive" + "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0430\u043a\u043a\u0430\u0443\u043d\u0442 Telldus Live:\n 1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u043d\u0438\u0436\u0435\n 2. \u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Telldus Live\n 3. Authorize **{app_name}** (\u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Yes**).\n 4. \u0412\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.\n\n [\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 Telldus Live]({auth_url})", + "title": "Telldus Live" }, "user": { "data": { diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 0eddbe2a151d79..58e6f53986c176 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -8,7 +8,7 @@ "unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { diff --git a/homeassistant/components/tradfri/.translations/ru.json b/homeassistant/components/tradfri/.translations/ru.json index 99844dc91ca27b..c9121862caf8d8 100644 --- a/homeassistant/components/tradfri/.translations/ru.json +++ b/homeassistant/components/tradfri/.translations/ru.json @@ -5,7 +5,7 @@ "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", "invalid_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043a\u043b\u044e\u0447\u043e\u043c. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0448\u043b\u044e\u0437.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430." }, diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json index e7a438cae11091..23f1ceaaa94e58 100644 --- a/homeassistant/components/transmission/.translations/ru.json +++ b/homeassistant/components/transmission/.translations/ru.json @@ -4,8 +4,8 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443", - "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", + "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { "options": { diff --git a/homeassistant/components/twilio/.translations/ru.json b/homeassistant/components/twilio/.translations/ru.json index b8d6f11f7efbac..1c4e0653496ab4 100644 --- a/homeassistant/components/twilio/.translations/ru.json +++ b/homeassistant/components/twilio/.translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Twilio?", - "title": "Twilio Webhook" + "title": "Twilio" } }, "title": "Twilio" diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index d7451bd81a0ce4..dbb6efd83432d1 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c" + "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c." }, "error": { - "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", - "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430" + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430." }, "step": { "user": { diff --git a/homeassistant/components/upnp/.translations/nn.json b/homeassistant/components/upnp/.translations/nn.json index cfbedd994afa43..8e173e4297fb16 100644 --- a/homeassistant/components/upnp/.translations/nn.json +++ b/homeassistant/components/upnp/.translations/nn.json @@ -8,8 +8,16 @@ "other": "Andre" }, "step": { + "confirm": { + "title": "UPnP/IGD" + }, "init": { "title": "UPnP / IGD" + }, + "user": { + "data": { + "igd": "UPnP/IGD" + } } }, "title": "UPnP / IGD" diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 3351f0d5d8a815..9599832799f9f0 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP", - "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD", + "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP.", + "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP / IGD \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", - "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432", + "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { diff --git a/homeassistant/components/velbus/.translations/ru.json b/homeassistant/components/velbus/.translations/ru.json index 3434c584221455..10ae06ffa7c324 100644 --- a/homeassistant/components/velbus/.translations/ru.json +++ b/homeassistant/components/velbus/.translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f Velbus", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "port": "\u0421\u0442\u0440\u043e\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, "title": "Velbus" diff --git a/homeassistant/components/vesync/.translations/ru.json b/homeassistant/components/vesync/.translations/ru.json index 38b86e9e29f479..23cb6fdfac7b9a 100644 --- a/homeassistant/components/vesync/.translations/ru.json +++ b/homeassistant/components/vesync/.translations/ru.json @@ -4,7 +4,7 @@ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + "invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { "user": { diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index 291d760dbc84fc..1779ed613fc76d 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, "step": { "user": { diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json index ed2e20f35270c2..4243f583082818 100644 --- a/homeassistant/components/zwave/.translations/ru.json +++ b/homeassistant/components/zwave/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave" + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave." }, "error": { "option_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 Z-Wave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." @@ -13,7 +13,7 @@ "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", "title": "Z-Wave" } }, From 68a3c97464a3c80a05c65361d9a15d9dc4fe2883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 15 Oct 2019 11:04:58 +0300 Subject: [PATCH 0901/3953] Deprecate Python 3.6 support, 3.8.0 is out (#27680) --- homeassistant/bootstrap.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 422eab8ed4a8fc..e399205ec708e9 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -93,6 +93,17 @@ async def async_from_config_dict( stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) + if sys.version_info[:3] < (3, 7, 0): + msg = ( + "Python 3.6 support is deprecated and will " + "be removed in the first release after December 15, 2019. Please " + "upgrade Python to 3.7.0 or higher." + ) + _LOGGER.warning(msg) + hass.components.persistent_notification.async_create( + msg, "Python version", "python_version" + ) + return hass From 5938f5a3a1e6a2947e24269964660d5726314f6a Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 10:06:29 +0200 Subject: [PATCH 0902/3953] moved imports to top level (#27682) --- homeassistant/components/discord/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 17ff0a192d061d..f35cf5b0ce9f93 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -2,6 +2,7 @@ import logging import os.path +import discord import voluptuous as vol from homeassistant.const import CONF_TOKEN @@ -44,7 +45,6 @@ def file_exists(self, filename): async def async_send_message(self, message, **kwargs): """Login to Discord, send message to channel(s) and log out.""" - import discord discord.VoiceClient.warn_nacl = False discord_bot = discord.Client() From 502c65b5fd841ecadc0f249315806aee472537b6 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 10:06:56 +0200 Subject: [PATCH 0903/3953] moved imports to top level (#27678) --- homeassistant/components/digitalloggers/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py index 9983ccc93fa355..10c8ce73a4706f 100644 --- a/homeassistant/components/digitalloggers/switch.py +++ b/homeassistant/components/digitalloggers/switch.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import dlipower import voluptuous as vol from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA @@ -45,7 +46,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return DIN III Relay switch.""" - import dlipower host = config.get(CONF_HOST) controller_name = config.get(CONF_NAME) From ecc276de3866af52ed15fcc5b4847a3dedf0e0f0 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 10:07:37 +0200 Subject: [PATCH 0904/3953] moved imports to top level (#27675) --- homeassistant/components/denonavr/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 51fc890c873771..1725b2d105cf8b 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -3,9 +3,10 @@ from collections import namedtuple import logging +import denonavr import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MUSIC, @@ -88,7 +89,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Denon platform.""" - import denonavr # Initialize list with receivers to be started receivers = [] From 5b410ff3a523732efc05f52b28b29df88755fc04 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 11:33:22 +0200 Subject: [PATCH 0905/3953] moved imports to top level (#27677) --- homeassistant/components/digital_ocean/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py index 18dfb49365a822..bdb0c348803ce1 100644 --- a/homeassistant/components/digital_ocean/__init__.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import digitalocean import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN @@ -38,7 +39,6 @@ def setup(hass, config): """Set up the Digital Ocean component.""" - import digitalocean conf = config[DOMAIN] access_token = conf.get(CONF_ACCESS_TOKEN) @@ -63,7 +63,6 @@ class DigitalOcean: def __init__(self, access_token): """Initialize the Digital Ocean connection.""" - import digitalocean self._access_token = access_token self.data = None From 57b8d1889acf82af74305f66ef774feb6a107e8f Mon Sep 17 00:00:00 2001 From: "Brett T. Warden" Date: Tue, 15 Oct 2019 02:53:13 -0700 Subject: [PATCH 0906/3953] Handle marker attrs that may not exist (#27519) marker-high-levels and marker-low-levels may not exist in printer attributes returned by CUPS, so we'll use .get() to avoid this and default to None: KeyError: 'marker-high-levels' Fixes #27518 --- homeassistant/components/cups/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index f6a5133d8a9851..4af51e911a1cd9 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -276,11 +276,11 @@ def device_state_attributes(self): if self._attributes is None: return None - high_level = self._attributes[self._printer]["marker-high-levels"] + high_level = self._attributes[self._printer].get("marker-high-levels") if isinstance(high_level, list): high_level = high_level[self._index] - low_level = self._attributes[self._printer]["marker-low-levels"] + low_level = self._attributes[self._printer].get("marker-low-levels") if isinstance(low_level, list): low_level = low_level[self._index] From 3d7860391a4be9cba357e7de2544ebe7a8c01b52 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Tue, 15 Oct 2019 12:12:58 +0200 Subject: [PATCH 0907/3953] Improve code coverage for HomematicIP Cloud (#27606) * Improve tests for HomematicIP Cloud * create fixtures remove decorators * removed further decorators * remove last decorator * improve exception handling * removed not required coroutine * use the correct place for mock --- .coveragerc | 1 - .../components/homematicip_cloud/__init__.py | 4 +- .../components/homematicip_cloud/hap.py | 2 +- .../components/homematicip_cloud/light.py | 8 +- .../components/homematicip_cloud/conftest.py | 86 ++++++++++---- tests/components/homematicip_cloud/helper.py | 9 +- .../test_alarm_control_panel.py | 18 +++ .../homematicip_cloud/test_binary_sensor.py | 16 +++ .../homematicip_cloud/test_climate.py | 36 ++++++ .../homematicip_cloud/test_cover.py | 14 +++ .../homematicip_cloud/test_device.py | 21 ++++ .../components/homematicip_cloud/test_hap.py | 108 +++++++++++++++++- .../homematicip_cloud/test_light.py | 29 ++++- .../homematicip_cloud/test_sensor.py | 26 +++++ .../homematicip_cloud/test_switch.py | 19 ++- .../homematicip_cloud/test_weather.py | 14 +++ 16 files changed, 372 insertions(+), 39 deletions(-) diff --git a/.coveragerc b/.coveragerc index 145350b6b19955..69ce7f8322ceef 100644 --- a/.coveragerc +++ b/.coveragerc @@ -290,7 +290,6 @@ omit = homeassistant/components/homematic/climate.py homeassistant/components/homematic/cover.py homeassistant/components/homematic/notify.py - homeassistant/components/homematicip_cloud/* homeassistant/components/homeworks/* homeassistant/components/honeywell/climate.py homeassistant/components/hook/switch.py diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index c8fb31998ef4ad..139565bf249947 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -213,9 +213,11 @@ async def _async_deactivate_vacation(service): def _get_home(hapid: str): """Return a HmIP home.""" - hap = hass.data[DOMAIN][hapid] + hap = hass.data[DOMAIN].get(hapid) if hap: return hap.home + + _LOGGER.info("No matching access point found for access point id %s", hapid) return None return True diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index f6727f91c7e7e9..64fbd4fd0799d8 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -53,7 +53,7 @@ async def async_register(self): except HmipConnectionError: return False - async def get_auth(self, hass, hapid, pin): + async def get_auth(self, hass: HomeAssistant, hapid, pin): """Create a HomematicIP access point object.""" auth = AsyncAuth(hass.loop, async_get_clientsession(hass)) try: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 80ee4cc5743939..bc704e2ef06b3a 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -119,9 +119,7 @@ def is_on(self) -> bool: @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - if self._device.dimLevel: - return int(self._device.dimLevel * 255) - return 0 + return int((self._device.dimLevel or 0.0) * 255) @property def supported_features(self) -> int: @@ -176,9 +174,7 @@ def is_on(self) -> bool: @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - if self._func_channel.dimLevel: - return int(self._func_channel.dimLevel * 255) - return 0 + return int((self._func_channel.dimLevel or 0.0) * 255) @property def hs_color(self) -> tuple: diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 2c2b020f3a0ef6..b2fc53a28ec32a 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -1,22 +1,20 @@ """Initializer helpers for HomematicIP fake server.""" -from unittest.mock import MagicMock, patch - +from asynctest import MagicMock, Mock, patch +from homematicip.aio.auth import AsyncAuth from homematicip.aio.connection import AsyncConnection +from homematicip.aio.home import AsyncHome import pytest from homeassistant import config_entries from homeassistant.components.homematicip_cloud import ( - CONF_ACCESSPOINT, - CONF_AUTHTOKEN, DOMAIN as HMIPC_DOMAIN, async_setup as hmip_async_setup, const as hmipc, hap as hmip_hap, ) -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from .helper import AUTH_TOKEN, HAPID, HomeTemplate +from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeTemplate from tests.common import MockConfigEntry, mock_coro @@ -31,16 +29,11 @@ def _rest_call_side_effect(path, body=None): connection._restCall.side_effect = _rest_call_side_effect # pylint: disable=W0212 connection.api_call.return_value = mock_coro(True) + connection.init.side_effect = mock_coro(True) return connection -@pytest.fixture(name="default_mock_home") -def default_mock_home_fixture(mock_connection): - """Create a fake homematic async home.""" - return HomeTemplate(connection=mock_connection).init_home().get_async_home_mock() - - @pytest.fixture(name="hmip_config_entry") def hmip_config_entry_fixture(): """Create a mock config entriy for homematic ip cloud.""" @@ -48,6 +41,7 @@ def hmip_config_entry_fixture(): hmipc.HMIPC_HAPID: HAPID, hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, hmipc.HMIPC_NAME: "", + hmipc.HMIPC_PIN: HAPPIN, } config_entry = MockConfigEntry( version=1, @@ -62,17 +56,34 @@ def hmip_config_entry_fixture(): return config_entry +@pytest.fixture(name="default_mock_home") +def default_mock_home_fixture(mock_connection): + """Create a fake homematic async home.""" + return HomeTemplate(connection=mock_connection).init_home().get_async_home_mock() + + @pytest.fixture(name="default_mock_hap") async def default_mock_hap_fixture( - hass: HomeAssistant, default_mock_home, hmip_config_entry + hass: HomeAssistant, mock_connection, hmip_config_entry ): - """Create a fake homematic access point.""" + """Create a mocked homematic access point.""" + return await get_mock_hap(hass, mock_connection, hmip_config_entry) + + +async def get_mock_hap(hass: HomeAssistant, mock_connection, hmip_config_entry): + """Create a mocked homematic access point.""" hass.config.components.add(HMIPC_DOMAIN) hap = hmip_hap.HomematicipHAP(hass, hmip_config_entry) - with patch.object(hap, "get_hap", return_value=mock_coro(default_mock_home)): + home_name = hmip_config_entry.data["name"] + mock_home = ( + HomeTemplate(connection=mock_connection, home_name=home_name) + .init_home() + .get_async_home_mock() + ) + with patch.object(hap, "get_hap", return_value=mock_coro(mock_home)): assert await hap.async_setup() is True - default_mock_home.on_update(hap.async_update) - default_mock_home.on_create(hap.async_create_entity) + mock_home.on_update(hap.async_update) + mock_home.on_create(hap.async_create_entity) hass.data[HMIPC_DOMAIN] = {HAPID: hap} @@ -85,18 +96,49 @@ async def default_mock_hap_fixture( def hmip_config_fixture(): """Create a config for homematic ip cloud.""" - entry_data = {CONF_ACCESSPOINT: HAPID, CONF_AUTHTOKEN: AUTH_TOKEN, CONF_NAME: ""} + entry_data = { + hmipc.HMIPC_HAPID: HAPID, + hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, + hmipc.HMIPC_NAME: "", + hmipc.HMIPC_PIN: HAPPIN, + } + + return {HMIPC_DOMAIN: [entry_data]} - return {hmipc.DOMAIN: [entry_data]} + +@pytest.fixture(name="dummy_config") +def dummy_config_fixture(): + """Create a dummy config.""" + return {"blabla": None} @pytest.fixture(name="mock_hap_with_service") async def mock_hap_with_service_fixture( - hass: HomeAssistant, default_mock_hap, hmip_config + hass: HomeAssistant, default_mock_hap, dummy_config ): """Create a fake homematic access point with hass services.""" - - await hmip_async_setup(hass, hmip_config) + await hmip_async_setup(hass, dummy_config) await hass.async_block_till_done() hass.data[HMIPC_DOMAIN] = {HAPID: default_mock_hap} return default_mock_hap + + +@pytest.fixture(name="simple_mock_home") +def simple_mock_home_fixture(): + """Return a simple AsyncHome Mock.""" + return Mock( + spec=AsyncHome, + devices=[], + groups=[], + location=Mock(), + weather=Mock(create=True), + id=42, + dutyCycle=88, + connected=True, + ) + + +@pytest.fixture(name="simple_mock_auth") +def simple_mock_auth_fixture(): + """Return a simple AsyncAuth Mock.""" + return Mock(spec=AsyncAuth, pin=HAPPIN, create=True) diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index e5c5c4569d7274..78c78ec0ab9b3f 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -1,7 +1,7 @@ """Helper for HomematicIP Cloud Tests.""" import json -from asynctest import Mock +from asynctest import Mock from homematicip.aio.class_maps import ( TYPE_CLASS_MAP, TYPE_GROUP_MAP, @@ -20,6 +20,7 @@ from tests.common import load_fixture HAPID = "3014F7110000000000000001" +HAPPIN = "5678" AUTH_TOKEN = "1234" HOME_JSON = "homematicip_cloud.json" @@ -81,10 +82,11 @@ class HomeTemplate(Home): _typeGroupMap = TYPE_GROUP_MAP _typeSecurityEventMap = TYPE_SECURITY_EVENT_MAP - def __init__(self, connection=None): + def __init__(self, connection=None, home_name=""): """Init template with connection.""" super().__init__(connection=connection) self.label = "Access Point" + self.name = home_name self.model_type = "HmIP-HAP" self.init_json_state = None @@ -121,13 +123,12 @@ def get_async_home_mock(self): Create Mock for Async_Home. based on template to be used for testing. It adds collections of mocked devices and groups to the home objects, - and sets reuired attributes. + and sets required attributes. """ mock_home = Mock( spec=AsyncHome, wraps=self, label="Access Point", modelType="HmIP-HAP" ) mock_home.__dict__.update(self.__dict__) - mock_home.name = "" return mock_home diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 0a68ac6d509a14..2798a0879b73df 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -2,12 +2,17 @@ from homematicip.base.enums import WindowState from homematicip.group import SecurityZoneGroup +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.setup import async_setup_component from .helper import get_and_check_entity_basics @@ -38,6 +43,19 @@ async def _async_manipulate_security_zones( await hass.async_block_till_done() +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, + ALARM_CONTROL_PANEL_DOMAIN, + {ALARM_CONTROL_PANEL_DOMAIN: {"platform": HMIPC_DOMAIN}}, + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_alarm_control_panel(hass, default_mock_hap): """Test HomematicipAlarmControlPanel.""" entity_id = "alarm_control_panel.hmip_alarm_control_panel" diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 0de2101d287e73..0760518171eab0 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -1,6 +1,8 @@ """Tests for HomematicIP Cloud binary sensor.""" from homematicip.base.enums import SmokeDetectorAlarmType, WindowState +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.binary_sensor import ( ATTR_ACCELERATION_SENSOR_MODE, ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION, @@ -10,10 +12,24 @@ ATTR_MOTION_DETECTED, ) from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, + BINARY_SENSOR_DOMAIN, + {BINARY_SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}}, + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_acceleration_sensor(hass, default_mock_hap): """Test HomematicipAccelerationSensor.""" entity_id = "binary_sensor.garagentor" diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 8f8a681fad855c..bdfd26319e6a3b 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -4,6 +4,7 @@ from homematicip.base.enums import AbsenceType from homematicip.functionalHomes import IndoorClimateHome +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_PRESET_MODE, @@ -15,10 +16,23 @@ PRESET_ECO, PRESET_NONE, ) +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.setup import async_setup_component from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_heating_group(hass, default_mock_hap): """Test HomematicipHeatingGroup.""" entity_id = "climate.badezimmer" @@ -153,6 +167,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) + assert len(home._connection.mock_calls) == 1 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -162,6 +177,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) + assert len(home._connection.mock_calls) == 2 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -171,6 +187,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) + assert len(home._connection.mock_calls) == 3 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -180,6 +197,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) + assert len(home._connection.mock_calls) == 4 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -189,6 +207,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) + assert len(home._connection.mock_calls) == 5 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -198,6 +217,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) + assert len(home._connection.mock_calls) == 6 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -207,12 +227,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () + assert len(home._connection.mock_calls) == 7 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", "deactivate_eco_mode", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () + assert len(home._connection.mock_calls) == 8 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -222,9 +244,23 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () + assert len(home._connection.mock_calls) == 9 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", "deactivate_vacation", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () + assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 + + not_existing_hap_id = "5555F7110000000000000001" + await hass.services.async_call( + "homematicip_cloud", + "deactivate_vacation", + {"accesspoint_id": not_existing_hap_id}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "deactivate_vacation" + assert home.mock_calls[-1][1] == () + # There is no further call on connection. + assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 7bfb842a0df626..22922303f9e0d2 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -2,12 +2,26 @@ from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, + DOMAIN as COVER_DOMAIN, ) +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.const import STATE_CLOSED, STATE_OPEN +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, COVER_DOMAIN, {COVER_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_cover_shutter(hass, default_mock_hap): """Test HomematicipCoverShutte.""" entity_id = "cover.sofa_links" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 81c35f8e2a93b1..812f32a33442a0 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -2,6 +2,7 @@ from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er +from .conftest import get_mock_hap from .helper import async_manipulate_test_data, get_and_check_entity_basics @@ -109,3 +110,23 @@ async def test_hap_reconnected(hass, default_mock_hap): await hass.async_block_till_done() ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON + + +async def test_hap_with_name(hass, mock_connection, hmip_config_entry): + """Test hap with name.""" + home_name = "TestName" + entity_id = f"light.{home_name.lower()}_treppe" + entity_name = f"{home_name} Treppe" + device_model = "HmIP-BSL" + + hmip_config_entry.data["name"] = home_name + mock_hap = await get_mock_hap(hass, mock_connection, hmip_config_entry) + assert mock_hap + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert hmip_device + assert ha_state.state == STATE_ON + assert ha_state.attributes["friendly_name"] == entity_name diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index cd8ead40c438e0..90f557b1f93303 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -1,11 +1,24 @@ """Test HomematicIP Cloud accesspoint.""" -from unittest.mock import Mock, patch +from asynctest import Mock, patch +from homematicip.aio.auth import AsyncAuth +from homematicip.base.base_connection import HmipConnectionError import pytest -from homeassistant.components.homematicip_cloud import const, errors, hap as hmipc +from homeassistant.components.homematicip_cloud import ( + DOMAIN as HMIPC_DOMAIN, + const, + errors, + hap as hmipc, +) +from homeassistant.components.homematicip_cloud.hap import ( + HomematicipAuth, + HomematicipHAP, +) from homeassistant.exceptions import ConfigEntryNotReady +from .helper import HAPID, HAPPIN + from tests.common import mock_coro, mock_coro_func @@ -53,6 +66,22 @@ async def test_auth_auth_check_and_register(hass): assert await hap.async_register() == "ABC" +async def test_auth_auth_check_and_register_with_exception(hass): + """Test auth client registration.""" + config = { + const.HMIPC_HAPID: "ABC123", + const.HMIPC_PIN: "123", + const.HMIPC_NAME: "hmip", + } + hap = hmipc.HomematicipAuth(hass, config) + hap.auth = Mock(spec=AsyncAuth) + with patch.object( + hap.auth, "isRequestAcknowledged", side_effect=HmipConnectionError + ), patch.object(hap.auth, "requestAuthToken", side_effect=HmipConnectionError): + assert await hap.async_checkbutton() is False + assert await hap.async_register() is False + + async def test_hap_setup_works(aioclient_mock): """Test a successful setup of a accesspoint.""" hass = Mock() @@ -121,3 +150,78 @@ async def test_hap_reset_unloads_entry_if_setup(): await hap.async_reset() assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 8 + + +async def test_hap_create(hass, hmip_config_entry, simple_mock_home): + """Mock AsyncHome to execute get_hap.""" + hass.config.components.add(HMIPC_DOMAIN) + hap = HomematicipHAP(hass, hmip_config_entry) + assert hap + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome", + return_value=simple_mock_home, + ), patch.object(hap, "async_connect", return_value=mock_coro(None)): + assert await hap.async_setup() is True + + +async def test_hap_create_exception(hass, hmip_config_entry, simple_mock_home): + """Mock AsyncHome to execute get_hap.""" + hass.config.components.add(HMIPC_DOMAIN) + hap = HomematicipHAP(hass, hmip_config_entry) + assert hap + + with patch.object(hap, "get_hap", side_effect=HmipConnectionError), pytest.raises( + HmipConnectionError + ): + await hap.async_setup() + + simple_mock_home.init.side_effect = HmipConnectionError + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome", + return_value=simple_mock_home, + ), pytest.raises(ConfigEntryNotReady): + await hap.async_setup() + + +async def test_auth_create(hass, simple_mock_auth): + """Mock AsyncAuth to execute get_auth.""" + config = { + const.HMIPC_HAPID: HAPID, + const.HMIPC_PIN: HAPPIN, + const.HMIPC_NAME: "hmip", + } + hmip_auth = HomematicipAuth(hass, config) + assert hmip_auth + + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncAuth", + return_value=simple_mock_auth, + ): + assert await hmip_auth.async_setup() is True + await hass.async_block_till_done() + assert hmip_auth.auth.pin == HAPPIN + + +async def test_auth_create_exception(hass, simple_mock_auth): + """Mock AsyncAuth to execute get_auth.""" + config = { + const.HMIPC_HAPID: HAPID, + const.HMIPC_PIN: HAPPIN, + const.HMIPC_NAME: "hmip", + } + hmip_auth = HomematicipAuth(hass, config) + simple_mock_auth.connectionRequest.side_effect = HmipConnectionError + assert hmip_auth + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncAuth", + return_value=simple_mock_auth, + ): + assert await hmip_auth.async_setup() is True + await hass.async_block_till_done() + assert hmip_auth.auth is False + + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncAuth", + return_value=simple_mock_auth, + ): + assert await hmip_auth.get_auth(hass, HAPID, HAPPIN) is False diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index a8d4984520c83b..17e92d9d99d98b 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -1,16 +1,33 @@ """Tests for HomematicIP Cloud light.""" from homematicip.base.enums import RGBColorState +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.light import ( ATTR_ENERGY_COUNTER, ATTR_POWER_CONSUMPTION, ) -from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_NAME +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_NAME, + DOMAIN as LIGHT_DOMAIN, +) from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_light(hass, default_mock_hap): """Test HomematicipLight.""" entity_id = "light.treppe" @@ -114,6 +131,11 @@ async def test_hmip_notification_light(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "dimLevel", None, 2) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_BRIGHTNESS) + async def test_hmip_dimmer(hass, default_mock_hap): """Test HomematicipDimmer.""" @@ -158,6 +180,11 @@ async def test_hmip_dimmer(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "dimLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_BRIGHTNESS) + async def test_hmip_light_measuring(hass, default_mock_hap): """Test HomematicipLightMeasuring.""" diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index d4307477975a92..8412cd19f4d7a3 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -1,4 +1,7 @@ """Tests for HomematicIP Cloud sensor.""" +from homematicip.base.enums import ValveState + +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.sensor import ( ATTR_LEFT_COUNTER, ATTR_RIGHT_COUNTER, @@ -6,11 +9,24 @@ ATTR_WIND_DIRECTION, ATTR_WIND_DIRECTION_VARIATION, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_accesspoint_status(hass, default_mock_hap): """Test HomematicipSwitch.""" entity_id = "sensor.access_point" @@ -50,6 +66,16 @@ async def test_hmip_heating_thermostat(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == "nn" + await async_manipulate_test_data( + hass, hmip_device, "valveState", ValveState.ADAPTION_DONE + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "37" + + await async_manipulate_test_data(hass, hmip_device, "lowBat", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes["icon"] == "mdi:battery-outline" + async def test_hmip_humidity_sensor(hass, default_mock_hap): """Test HomematicipHumiditySensor.""" diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index 15eaf6da04c88a..9e33d1d9587774 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -1,13 +1,30 @@ """Tests for HomematicIP Cloud switch.""" +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.device import ( ATTR_GROUP_MEMBER_UNREACHABLE, ) -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH +from homeassistant.components.switch import ( + ATTR_CURRENT_POWER_W, + ATTR_TODAY_ENERGY_KWH, + DOMAIN as SWITCH_DOMAIN, +) from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_switch(hass, default_mock_hap): """Test HomematicipSwitch.""" entity_id = "switch.schrank" diff --git a/tests/components/homematicip_cloud/test_weather.py b/tests/components/homematicip_cloud/test_weather.py index 0b5d59215bbea0..9427a2d05bf1f4 100644 --- a/tests/components/homematicip_cloud/test_weather.py +++ b/tests/components/homematicip_cloud/test_weather.py @@ -1,15 +1,29 @@ """Tests for HomematicIP Cloud weather.""" +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.weather import ( ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, ) +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, WEATHER_DOMAIN, {WEATHER_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_weather_sensor(hass, default_mock_hap): """Test HomematicipWeatherSensor.""" entity_id = "weather.weather_sensor_plus" From b4a73fa87e97be9d5b562554ea42c88b7c150644 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 12:26:50 +0200 Subject: [PATCH 0908/3953] Move imports in decora component (#27645) * moved imports to top level * replaced importlib with standard import * fix for Unable to import 'decora' error --- homeassistant/components/decora/light.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index cad98f9d8a4528..4d2d10ccbd5792 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -1,18 +1,19 @@ """Support for Decora dimmers.""" -import importlib -import logging from functools import wraps +import logging import time +from bluepy.btle import BTLEException # pylint: disable=import-error, no-member +import decora # pylint: disable=import-error, no-member import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -34,9 +35,6 @@ def retry(method): @wraps(method) def wrapper_retry(device, *args, **kwargs): """Try send command and retry on error.""" - # pylint: disable=import-error, no-member - import decora - import bluepy initial = time.monotonic() while True: @@ -44,7 +42,7 @@ def wrapper_retry(device, *args, **kwargs): return None try: return method(device, *args, **kwargs) - except (decora.decoraException, AttributeError, bluepy.btle.BTLEException): + except (decora.decoraException, AttributeError, BTLEException): _LOGGER.warning( "Decora connect error for device %s. " "Reconnecting...", device.name, @@ -74,8 +72,6 @@ class DecoraLight(Light): def __init__(self, device): """Initialize the light.""" - # pylint: disable=no-member - decora = importlib.import_module("decora") self._name = device["name"] self._address = device["address"] From 0463349f0284ea6d8e72d1727a9fbc952cdab2ba Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 12:28:24 +0200 Subject: [PATCH 0909/3953] moved imports to top level (#27683) --- .../dlib_face_detect/image_processing.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 749e536e2e86a2..cdd8bc101b5df8 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -1,18 +1,20 @@ """Component that will help set the Dlib face detect processing.""" -import logging import io +import logging -from homeassistant.core import split_entity_id +import face_recognition # pylint: disable=import-error -# pylint: disable=unused-import -from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + ImageProcessingFaceEntity, ) +# pylint: disable=unused-import +from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa +from homeassistant.core import split_entity_id + _LOGGER = logging.getLogger(__name__) ATTR_LOCATION = "location" @@ -55,7 +57,6 @@ def name(self): def process_image(self, image): """Process image.""" - import face_recognition # pylint: disable=import-error fak_file = io.BytesIO(image) fak_file.name = "snapshot.jpg" From 5b1f44ba197b27c70df091e47befc552a32b4e75 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 15 Oct 2019 13:37:40 +0200 Subject: [PATCH 0910/3953] Move imports in yeelight + yeelightsunflower component (#27388) * Move imports in yeelight + yeelightsunflower component * Fix pylint * Fix pylint (again) --- homeassistant/components/yeelight/light.py | 62 ++++++++----------- .../components/yeelightsunflower/light.py | 2 +- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index ab63e6fb319455..772fb00977baac 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -2,8 +2,16 @@ import logging import voluptuous as vol -from yeelight import RGBTransition, SleepTransition, Flow, BulbException +import yeelight +from yeelight import ( + RGBTransition, + SleepTransition, + Flow, + BulbException, + transitions as yee_transitions, +) from yeelight.enums import PowerMode, LightType, BulbType, SceneClass + from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.service import extract_entity_ids import homeassistant.helpers.config_validation as cv @@ -190,8 +198,6 @@ def _transitions_config_parser(transitions): """Parse transitions config into initialized objects.""" - import yeelight - transition_objects = [] for transition_config in transitions: transition, params = list(transition_config.items())[0] @@ -652,39 +658,23 @@ def set_flash(self, flash) -> None: def set_effect(self, effect) -> None: """Activate effect.""" if effect: - from yeelight.transitions import ( - disco, - temp, - strobe, - pulse, - strobe_color, - alarm, - police, - police2, - christmas, - rgb, - randomloop, - lsd, - slowdown, - ) - if effect == EFFECT_STOP: self._bulb.stop_flow(light_type=self.light_type) return effects_map = { - EFFECT_DISCO: disco, - EFFECT_TEMP: temp, - EFFECT_STROBE: strobe, - EFFECT_STROBE_COLOR: strobe_color, - EFFECT_ALARM: alarm, - EFFECT_POLICE: police, - EFFECT_POLICE2: police2, - EFFECT_CHRISTMAS: christmas, - EFFECT_RGB: rgb, - EFFECT_RANDOM_LOOP: randomloop, - EFFECT_LSD: lsd, - EFFECT_SLOWDOWN: slowdown, + EFFECT_DISCO: yee_transitions.disco, + EFFECT_TEMP: yee_transitions.temp, + EFFECT_STROBE: yee_transitions.strobe, + EFFECT_STROBE_COLOR: yee_transitions.strobe_color, + EFFECT_ALARM: yee_transitions.alarm, + EFFECT_POLICE: yee_transitions.police, + EFFECT_POLICE2: yee_transitions.police2, + EFFECT_CHRISTMAS: yee_transitions.christmas, + EFFECT_RGB: yee_transitions.rgb, + EFFECT_RANDOM_LOOP: yee_transitions.randomloop, + EFFECT_LSD: yee_transitions.lsd, + EFFECT_SLOWDOWN: yee_transitions.slowdown, } if effect in self.custom_effects_names: @@ -692,13 +682,15 @@ def set_effect(self, effect) -> None: elif effect in effects_map: flow = Flow(count=0, transitions=effects_map[effect]()) elif effect == EFFECT_FAST_RANDOM_LOOP: - flow = Flow(count=0, transitions=randomloop(duration=250)) + flow = Flow( + count=0, transitions=yee_transitions.randomloop(duration=250) + ) elif effect == EFFECT_WHATSAPP: - flow = Flow(count=2, transitions=pulse(37, 211, 102)) + flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) elif effect == EFFECT_FACEBOOK: - flow = Flow(count=2, transitions=pulse(59, 89, 152)) + flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152)) elif effect == EFFECT_TWITTER: - flow = Flow(count=2, transitions=pulse(0, 172, 237)) + flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237)) try: self._bulb.start_flow(flow, light_type=self.light_type) diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index fa836f2776ffa5..3424014e8f4c73 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -1,6 +1,7 @@ """Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).""" import logging +import yeelightsunflower import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -24,7 +25,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight Sunflower Light platform.""" - import yeelightsunflower host = config.get(CONF_HOST) hub = yeelightsunflower.Hub(host) From 16c18d303faede4bf790b53b4e4dd0d7bfe3cdc0 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 13:39:51 +0200 Subject: [PATCH 0911/3953] Move imports in bme680 component (#27506) * moved imports to top level * fixed pylint error * moved imports to top level * fixed import error --- homeassistant/components/bme680/sensor.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index a36b35ea9d4be2..5a1e9fd120ff90 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -1,14 +1,15 @@ """Support for BME680 Sensor over SMBus.""" -import importlib import logging +import threading +from time import sleep, time -from time import time, sleep - +from smbus import SMBus # pylint: disable=import-error +import bme680 # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.util.temperature import celsius_to_fahrenheit @@ -121,9 +122,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _setup_bme680(config): """Set up and configure the BME680 sensor.""" - from smbus import SMBus # pylint: disable=import-error - - bme680 = importlib.import_module("bme680") sensor_handler = None sensor = None @@ -224,7 +222,6 @@ def __init__( self._gas_baseline = None if gas_measurement: - import threading threading.Thread( target=self._run_gas_sensor, From 40fbc3bd412f0987e4bf8b8aa3af3c71aadc0222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiit=20R=C3=A4tsep?= Date: Tue, 15 Oct 2019 15:05:10 +0300 Subject: [PATCH 0912/3953] Fix missing strings in soma config flow (#27689) --- homeassistant/components/soma/.translations/en.json | 12 +++++++++++- homeassistant/components/soma/strings.json | 10 ++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json index 5dea73fcc22a2e..aa2f92f0be603c 100644 --- a/homeassistant/components/soma/.translations/en.json +++ b/homeassistant/components/soma/.translations/en.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Successfully authenticated with Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Please enter connection settings of your SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } -} \ No newline at end of file +} diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json index eac817ce1199dc..aa2f92f0be603c 100644 --- a/homeassistant/components/soma/strings.json +++ b/homeassistant/components/soma/strings.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Successfully authenticated with Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Please enter connection settings of your SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } From b22eb223589402aa9134b33e2bdac5c850255c49 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 14:26:04 +0200 Subject: [PATCH 0913/3953] moved imports to top level (#27695) --- homeassistant/components/dnsip/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 0053d5a95ea177..fb57040f2c2ecc 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import aiodns +from aiodns.error import DNSError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -58,7 +60,6 @@ class WanIpSensor(Entity): def __init__(self, hass, name, hostname, resolver, ipv6): """Initialize the DNS IP sensor.""" - import aiodns self.hass = hass self._name = name @@ -80,7 +81,6 @@ def state(self): async def async_update(self): """Get the current DNS IP address for hostname.""" - from aiodns.error import DNSError try: response = await self.resolver.query(self.hostname, self.querytype) From 3e26b49cc2ccb9cfa8c3a8d7d5b694bc21edb7b2 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Tue, 15 Oct 2019 14:26:39 +0200 Subject: [PATCH 0914/3953] Add battery status in owntracks (#27686) * Add battery status in owntracks * Remove trailing whitespaces --- homeassistant/components/owntracks/messages.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 7ef31be13272eb..465d2762f74b98 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -79,6 +79,8 @@ def _parse_see_args(message, subscribe_topic): kwargs["attributes"]["address"] = message["addr"] if "cog" in message: kwargs["attributes"]["course"] = message["cog"] + if "bs" in message: + kwargs["attributes"]["battery_status"] = message["bs"] if "t" in message: if message["t"] in ("c", "u"): kwargs["source_type"] = SOURCE_TYPE_GPS From 0e5f24d60cbe65e241f51f64302bc078ef22f966 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 14:27:02 +0200 Subject: [PATCH 0915/3953] moved imports to top level (#27693) --- .../components/dlib_face_identify/image_processing.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index d4851be28c8791..d5b55b6a68ce35 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -2,6 +2,8 @@ import logging import io +# pylint: disable=import-error +import face_recognition import voluptuous as vol from homeassistant.core import split_entity_id @@ -49,8 +51,6 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): def __init__(self, camera_entity, faces, name, tolerance): """Initialize Dlib face identify entry.""" - # pylint: disable=import-error - import face_recognition super().__init__() @@ -83,8 +83,6 @@ def name(self): def process_image(self, image): """Process image.""" - # pylint: disable=import-error - import face_recognition fak_file = io.BytesIO(image) fak_file.name = "snapshot.jpg" From d534f30042cf530e3307f64c12610f2e32ad512c Mon Sep 17 00:00:00 2001 From: AaronDavidSchneider Date: Tue, 15 Oct 2019 17:11:17 +0200 Subject: [PATCH 0916/3953] Update fritzconnection requirement to 0.8.4 (#27698) * update fritzconnection requirement * update requierements for other components and requierements_all --- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- homeassistant/components/fritzbox_netmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index e6c1fee2c958d6..15a3406891f14a 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -3,7 +3,7 @@ "name": "Fritz", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ - "fritzconnection==0.6.5" + "fritzconnection==0.8.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 35c27b7ca84f1c..f85f16d6c0de70 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Fritzbox callmonitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", "requirements": [ - "fritzconnection==0.6.5" + "fritzconnection==0.8.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index 88a7ab5a338fc4..9afaa71e699d96 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Fritzbox netmonitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", "requirements": [ - "fritzconnection==0.6.5" + "fritzconnection==0.8.4" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index ef07a3f44b7246..ea3c8e50f93fa3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -516,7 +516,7 @@ freesms==0.1.2 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor -# fritzconnection==0.6.5 +# fritzconnection==0.8.4 # homeassistant.components.fritzdect fritzhome==1.0.4 From 26d19f9e1c26f582bc7a835235af98e39d5143c1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 15 Oct 2019 17:12:12 +0200 Subject: [PATCH 0917/3953] Moved imports to top-level in spotify integration (#27703) --- homeassistant/components/spotify/media_player.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 31fdc09af80371..236c8b8db89608 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -3,11 +3,14 @@ import logging import random +import spotipy +import spotipy.oauth2 import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, @@ -18,7 +21,6 @@ SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_VOLUME_SET, - ATTR_MEDIA_CONTENT_ID, ) from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import callback @@ -97,7 +99,6 @@ def request_configuration(hass, config, add_entities, oauth): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Spotify platform.""" - import spotipy.oauth2 callback_url = f"{hass.config.api.base_url}{AUTH_CALLBACK_PATH}" cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH)) @@ -181,7 +182,6 @@ def __init__(self, oauth, name, aliases): def refresh_spotify_instance(self): """Fetch a new spotify instance.""" - import spotipy token_refreshed = False need_token = self._token_info is None or self._oauth.is_token_expired( From 6f894d2dec988b6bee8f0df117efeea98a47629c Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 17:12:32 +0200 Subject: [PATCH 0918/3953] moved imports to top level (#27679) --- homeassistant/components/discogs/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 64528f4ca5eb66..b3f29fbe75b898 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -3,6 +3,7 @@ import logging import random +import discogs_client import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -65,19 +66,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Discogs sensor.""" - import discogs_client token = config[CONF_TOKEN] name = config[CONF_NAME] try: - discogs_client = discogs_client.Client(SERVER_SOFTWARE, user_token=token) + _discogs_client = discogs_client.Client(SERVER_SOFTWARE, user_token=token) discogs_data = { - "user": discogs_client.identity().name, - "folders": discogs_client.identity().collection_folders, - "collection_count": discogs_client.identity().num_collection, - "wantlist_count": discogs_client.identity().num_wantlist, + "user": _discogs_client.identity().name, + "folders": _discogs_client.identity().collection_folders, + "collection_count": _discogs_client.identity().num_collection, + "wantlist_count": _discogs_client.identity().num_wantlist, } except discogs_client.exceptions.HTTPError: _LOGGER.error("API token is not valid") From a591d78efe4bdbac966c301494b674c0f2ad7493 Mon Sep 17 00:00:00 2001 From: quthla Date: Tue, 15 Oct 2019 17:21:40 +0200 Subject: [PATCH 0919/3953] Bump PyMata to 2.20 (#27431) * Bump PyMata to 2.20 * Bump PyMata to 2.20 --- homeassistant/components/arduino/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arduino/manifest.json b/homeassistant/components/arduino/manifest.json index 3567ce71cd12cf..a29f65700ff39d 100644 --- a/homeassistant/components/arduino/manifest.json +++ b/homeassistant/components/arduino/manifest.json @@ -3,7 +3,7 @@ "name": "Arduino", "documentation": "https://www.home-assistant.io/integrations/arduino", "requirements": [ - "PyMata==2.14" + "PyMata==2.20" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ea3c8e50f93fa3..567acd712a1c00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -56,7 +56,7 @@ PyISY==1.1.2 PyMVGLive==1.1.4 # homeassistant.components.arduino -PyMata==2.14 +PyMata==2.20 # homeassistant.components.mobile_app # homeassistant.components.owntracks From c700085490b23a4a73cb48243ea6c692b1f27a13 Mon Sep 17 00:00:00 2001 From: Rolf K Date: Tue, 15 Oct 2019 17:37:15 +0200 Subject: [PATCH 0920/3953] Add improved scene support to input_text (#27687) * Add improved scene support for input_text. * Add tests for reproducing input_text states. * Add some comments. --- .../components/input_text/reproduce_state.py | 46 +++++++++++++ .../input_text/test_reproduce_state.py | 65 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 homeassistant/components/input_text/reproduce_state.py create mode 100644 tests/components/input_text/test_reproduce_state.py diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py new file mode 100644 index 00000000000000..f64c5c019f65dd --- /dev/null +++ b/homeassistant/components/input_text/reproduce_state.py @@ -0,0 +1,46 @@ +"""Reproduce an Input text state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + # Return if we can't find the entity + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + # Call service + service = SERVICE_SET_VALUE + service_data = {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: state.state} + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input text states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/input_text/test_reproduce_state.py b/tests/components/input_text/test_reproduce_state.py new file mode 100644 index 00000000000000..fd75948d4618c9 --- /dev/null +++ b/tests/components/input_text/test_reproduce_state.py @@ -0,0 +1,65 @@ +"""Test reproduce state for Input text.""" +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +VALID_TEXT1 = "Test text" +VALID_TEXT2 = "LoremIpsum" +INVALID_TEXT1 = "This text is too long!" +INVALID_TEXT2 = "Short" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input text states.""" + + # Setup entity for testing + assert await async_setup_component( + hass, + "input_text", + { + "input_text": { + "test_text": {"min": "6", "max": "10", "initial": VALID_TEXT1} + } + }, + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("input_text.test_text", VALID_TEXT1), + # Should not raise + State("input_text.non_existing", VALID_TEXT1), + ], + blocking=True, + ) + + # Test that entity is in desired state + assert hass.states.get("input_text.test_text").state == VALID_TEXT1 + + # Try reproducing with different state + await hass.helpers.state.async_reproduce_state( + [ + State("input_text.test_text", VALID_TEXT2), + # Should not raise + State("input_text.non_existing", VALID_TEXT2), + ], + blocking=True, + ) + + # Test that the state was changed + assert hass.states.get("input_text.test_text").state == VALID_TEXT2 + + # Test setting state to invalid state (length too long) + await hass.helpers.state.async_reproduce_state( + [State("input_text.test_text", INVALID_TEXT1)], blocking=True + ) + + # The entity state should be unchanged + assert hass.states.get("input_text.test_text").state == VALID_TEXT2 + + # Test setting state to invalid state (length too short) + await hass.helpers.state.async_reproduce_state( + [State("input_text.test_text", INVALID_TEXT2)], blocking=True + ) + + # The entity state should be unchanged + assert hass.states.get("input_text.test_text").state == VALID_TEXT2 From 93f9afcd21bcadc4d37655206eb5831af7f2c4d1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Oct 2019 16:15:26 -0700 Subject: [PATCH 0921/3953] Fix config imports (#27669) * Fix config imports * Remove old migration * Remove migrate tests --- homeassistant/components/config/automation.py | 5 +- homeassistant/components/config/group.py | 10 ++- homeassistant/components/config/script.py | 5 +- homeassistant/config.py | 24 ++----- tests/test_config.py | 67 +------------------ 5 files changed, 21 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 97ddf1e0714e69..0e9b4053b7b587 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -5,12 +5,11 @@ from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.automation.config import async_validate_config_item from homeassistant.const import CONF_ID, SERVICE_RELOAD +from homeassistant.config import AUTOMATION_CONFIG_PATH import homeassistant.helpers.config_validation as cv from . import EditIdBasedConfigView -CONFIG_PATH = "automations.yaml" - async def async_setup(hass): """Set up the Automation config API.""" @@ -23,7 +22,7 @@ async def hook(hass): EditAutomationConfigView( DOMAIN, "config", - CONFIG_PATH, + AUTOMATION_CONFIG_PATH, cv.string, PLATFORM_SCHEMA, post_write_hook=hook, diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index 371bd98cf08ef8..d104cd2e1df6f6 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -1,12 +1,11 @@ """Provide configuration end points for Groups.""" from homeassistant.components.group import DOMAIN, GROUP_SCHEMA from homeassistant.const import SERVICE_RELOAD +from homeassistant.config import GROUP_CONFIG_PATH import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView -CONFIG_PATH = "groups.yaml" - async def async_setup(hass): """Set up the Group config API.""" @@ -17,7 +16,12 @@ async def hook(hass): hass.http.register_view( EditKeyBasedConfigView( - "group", "config", CONFIG_PATH, cv.slug, GROUP_SCHEMA, post_write_hook=hook + "group", + "config", + GROUP_CONFIG_PATH, + cv.slug, + GROUP_SCHEMA, + post_write_hook=hook, ) ) return True diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 8ce163745f1106..e63651d8f2a9bf 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,12 +1,11 @@ """Provide configuration end points for scripts.""" from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA from homeassistant.const import SERVICE_RELOAD +from homeassistant.config import SCRIPT_CONFIG_PATH import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView -CONFIG_PATH = "scripts.yaml" - async def async_setup(hass): """Set up the script config API.""" @@ -19,7 +18,7 @@ async def hook(hass): EditKeyBasedConfigView( "script", "config", - CONFIG_PATH, + SCRIPT_CONFIG_PATH, cv.slug, SCRIPT_ENTRY_SCHEMA, post_write_hook=hook, diff --git a/homeassistant/config.py b/homeassistant/config.py index 27137c08f1aeb7..9f49889791e93f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -66,9 +66,11 @@ CONFIG_DIR_NAME = ".homeassistant" DATA_CUSTOMIZE = "hass_customize" -FILE_MIGRATION = (("ios.conf", ".ios.conf"),) +GROUP_CONFIG_PATH = "groups.yaml" +AUTOMATION_CONFIG_PATH = "automations.yaml" +SCRIPT_CONFIG_PATH = "scripts.yaml" -DEFAULT_CONFIG = """ +DEFAULT_CONFIG = f""" # Configure a default setup of Home Assistant (frontend, api, etc) default_config: @@ -80,9 +82,9 @@ tts: - platform: google_translate -group: !include groups.yaml -automation: !include automations.yaml -script: !include scripts.yaml +group: !include {GROUP_CONFIG_PATH} +automation: !include {AUTOMATION_CONFIG_PATH} +script: !include {SCRIPT_CONFIG_PATH} """ DEFAULT_SECRETS = """ # Use this file to store secrets like usernames and passwords. @@ -253,12 +255,6 @@ async def async_create_default_config( def _write_default_config(config_dir: str) -> Optional[str]: """Write the default config.""" - from homeassistant.components.config.group import CONFIG_PATH as GROUP_CONFIG_PATH - from homeassistant.components.config.automation import ( - CONFIG_PATH as AUTOMATION_CONFIG_PATH, - ) - from homeassistant.components.config.script import CONFIG_PATH as SCRIPT_CONFIG_PATH - config_path = os.path.join(config_dir, YAML_CONFIG_FILE) secret_path = os.path.join(config_dir, SECRET_YAML) version_path = os.path.join(config_dir, VERSION_FILE) @@ -407,12 +403,6 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: with open(version_path, "wt") as outp: outp.write(__version__) - _LOGGER.debug("Migrating old system configuration files to new locations") - for oldf, newf in FILE_MIGRATION: - if os.path.isfile(hass.config.path(oldf)): - _LOGGER.info("Migrating %s to %s", oldf, newf) - os.rename(hass.config.path(oldf), hass.config.path(newf)) - @callback def async_log_exception( diff --git a/tests/test_config.py b/tests/test_config.py index 362608e7af28b7..dab51f59176ca2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -33,11 +33,6 @@ from homeassistant.util import dt as dt_util from homeassistant.util.yaml import SECRET_YAML from homeassistant.helpers.entity import Entity -from homeassistant.components.config.group import CONFIG_PATH as GROUP_CONFIG_PATH -from homeassistant.components.config.automation import ( - CONFIG_PATH as AUTOMATIONS_CONFIG_PATH, -) -from homeassistant.components.config.script import CONFIG_PATH as SCRIPTS_CONFIG_PATH import homeassistant.helpers.check_config as check_config from tests.common import get_test_config_dir, patch_yaml_files @@ -46,9 +41,9 @@ YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) SECRET_PATH = os.path.join(CONFIG_DIR, SECRET_YAML) VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE) -GROUP_PATH = os.path.join(CONFIG_DIR, GROUP_CONFIG_PATH) -AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, AUTOMATIONS_CONFIG_PATH) -SCRIPTS_PATH = os.path.join(CONFIG_DIR, SCRIPTS_CONFIG_PATH) +GROUP_PATH = os.path.join(CONFIG_DIR, config_util.GROUP_CONFIG_PATH) +AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, config_util.AUTOMATION_CONFIG_PATH) +SCRIPTS_PATH = os.path.join(CONFIG_DIR, config_util.SCRIPT_CONFIG_PATH) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE @@ -345,62 +340,6 @@ def test_config_upgrade_no_file(hass): assert opened_file.write.call_args == mock.call(__version__) -@mock.patch("homeassistant.config.shutil") -@mock.patch("homeassistant.config.os") -@mock.patch("homeassistant.config.find_config_file", mock.Mock()) -def test_migrate_file_on_upgrade(mock_os, mock_shutil, hass): - """Test migrate of config files on upgrade.""" - ha_version = "0.7.0" - - mock_os.path.isdir = mock.Mock(return_value=True) - - mock_open = mock.mock_open() - - def _mock_isfile(filename): - return True - - with mock.patch("homeassistant.config.open", mock_open, create=True), mock.patch( - "homeassistant.config.os.path.isfile", _mock_isfile - ): - opened_file = mock_open.return_value - # pylint: disable=no-member - opened_file.readline.return_value = ha_version - - hass.config.path = mock.Mock() - - config_util.process_ha_config_upgrade(hass) - - assert mock_os.rename.call_count == 1 - - -@mock.patch("homeassistant.config.shutil") -@mock.patch("homeassistant.config.os") -@mock.patch("homeassistant.config.find_config_file", mock.Mock()) -def test_migrate_no_file_on_upgrade(mock_os, mock_shutil, hass): - """Test not migrating config files on upgrade.""" - ha_version = "0.7.0" - - mock_os.path.isdir = mock.Mock(return_value=True) - - mock_open = mock.mock_open() - - def _mock_isfile(filename): - return False - - with mock.patch("homeassistant.config.open", mock_open, create=True), mock.patch( - "homeassistant.config.os.path.isfile", _mock_isfile - ): - opened_file = mock_open.return_value - # pylint: disable=no-member - opened_file.readline.return_value = ha_version - - hass.config.path = mock.Mock() - - config_util.process_ha_config_upgrade(hass) - - assert mock_os.rename.call_count == 0 - - async def test_loading_configuration_from_storage(hass, hass_storage): """Test loading core config onto hass object.""" hass_storage["core.config"] = { From 8720ca38b58ec2739a1a5732e49299cb6c029cde Mon Sep 17 00:00:00 2001 From: Rolf K Date: Wed, 16 Oct 2019 01:15:42 +0200 Subject: [PATCH 0922/3953] Add improved scene support for input_select (#27697) * Add improved scene support for input_select * Add tests for reproducing input_select states. * Add some comments. * Add support for set_options Allows defining the options for an input_select in a scene. * Add tests for set_options in test_reproduce_state * Execute for real instead of mock execution. --- .../input_select/reproduce_state.py | 80 +++++++++++++++++++ .../input_select/test_reproduce_state.py | 72 +++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 homeassistant/components/input_select/reproduce_state.py create mode 100644 tests/components/input_select/test_reproduce_state.py diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py new file mode 100644 index 00000000000000..657f518cd3d5d6 --- /dev/null +++ b/homeassistant/components/input_select/reproduce_state.py @@ -0,0 +1,80 @@ +"""Reproduce an Input select state.""" +import asyncio +import logging +from types import MappingProxyType +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + DOMAIN, + SERVICE_SELECT_OPTION, + SERVICE_SET_OPTIONS, + ATTR_OPTION, + ATTR_OPTIONS, +) + +ATTR_GROUP = [ATTR_OPTION, ATTR_OPTIONS] + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + # Return if we can't find entity + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and all( + check_attr_equal(cur_state.attributes, state.attributes, attr) + for attr in ATTR_GROUP + ): + return + + # Set service data + service_data = {ATTR_ENTITY_ID: state.entity_id} + + # If options are specified, call SERVICE_SET_OPTIONS + if ATTR_OPTIONS in state.attributes: + service = SERVICE_SET_OPTIONS + service_data[ATTR_OPTIONS] = state.attributes[ATTR_OPTIONS] + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + # Remove ATTR_OPTIONS from service_data so we can reuse service_data in next call + del service_data[ATTR_OPTIONS] + + # Call SERVICE_SELECT_OPTION + service = SERVICE_SELECT_OPTION + service_data[ATTR_OPTION] = state.state + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input select states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + +def check_attr_equal( + attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str +) -> bool: + """Return true if the given attributes are equal.""" + return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/tests/components/input_select/test_reproduce_state.py b/tests/components/input_select/test_reproduce_state.py new file mode 100644 index 00000000000000..469c258cb4b53a --- /dev/null +++ b/tests/components/input_select/test_reproduce_state.py @@ -0,0 +1,72 @@ +"""Test reproduce state for Input select.""" +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +VALID_OPTION1 = "Option A" +VALID_OPTION2 = "Option B" +VALID_OPTION3 = "Option C" +VALID_OPTION4 = "Option D" +VALID_OPTION5 = "Option E" +VALID_OPTION6 = "Option F" +INVALID_OPTION = "Option X" +VALID_OPTION_SET1 = [VALID_OPTION1, VALID_OPTION2, VALID_OPTION3] +VALID_OPTION_SET2 = [VALID_OPTION4, VALID_OPTION5, VALID_OPTION6] +ENTITY = "input_select.test_select" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input select states.""" + + # Setup entity + assert await async_setup_component( + hass, + "input_select", + { + "input_select": { + "test_select": {"options": VALID_OPTION_SET1, "initial": VALID_OPTION1} + } + }, + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State(ENTITY, VALID_OPTION1), + # Should not raise + State("input_select.non_existing", VALID_OPTION1), + ], + blocking=True, + ) + + # Test that entity is in desired state + assert hass.states.get(ENTITY).state == VALID_OPTION1 + + # Try reproducing with different state + await hass.helpers.state.async_reproduce_state( + [ + State(ENTITY, VALID_OPTION3), + # Should not raise + State("input_select.non_existing", VALID_OPTION3), + ], + blocking=True, + ) + + # Test that we got the desired result + assert hass.states.get(ENTITY).state == VALID_OPTION3 + + # Test setting state to invalid state + await hass.helpers.state.async_reproduce_state( + [State(ENTITY, INVALID_OPTION)], blocking=True + ) + + # The entity state should be unchanged + assert hass.states.get(ENTITY).state == VALID_OPTION3 + + # Test setting a different option set + await hass.helpers.state.async_reproduce_state( + [State(ENTITY, VALID_OPTION5, {"options": VALID_OPTION_SET2})], blocking=True + ) + + # These should fail if options weren't changed to VALID_OPTION_SET2 + assert hass.states.get(ENTITY).attributes == {"options": VALID_OPTION_SET2} + assert hass.states.get(ENTITY).state == VALID_OPTION5 From a58d2429091e2dc582f5623cb289b43ce9465622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Wed, 16 Oct 2019 01:17:09 +0200 Subject: [PATCH 0923/3953] move imports in sony_projector component (#27718) --- homeassistant/components/sony_projector/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py index 43a4b7bc0fe156..e68bed34cfac4e 100644 --- a/homeassistant/components/sony_projector/switch.py +++ b/homeassistant/components/sony_projector/switch.py @@ -1,10 +1,11 @@ """Support for Sony projectors via SDCP network control.""" import logging +import pysdcp import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import STATE_ON, STATE_OFF, CONF_NAME, CONF_HOST +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Connect to Sony projector using network.""" - import pysdcp host = config[CONF_HOST] name = config[CONF_NAME] From b2f6931bbed3821841aef5326e9b13fdadfe63e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Wed, 16 Oct 2019 01:20:59 +0200 Subject: [PATCH 0924/3953] move imports in speedtestdotnet component (#27716) --- homeassistant/components/speedtestdotnet/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 029356cb082551..afccc71d28593e 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -1,15 +1,17 @@ """Support for testing internet speed via Speedtest.net.""" -import logging from datetime import timedelta +import logging +import speedtest import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval + from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -72,7 +74,6 @@ def __init__(self, hass, server_id): def update(self, now=None): """Get the latest data from speedtest.net.""" - import speedtest _LOGGER.debug("Executing speedtest.net speed test") speed = speedtest.Speedtest() From d4692367c5e36b0f1f69155e5bd2b105be4d479a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Wed, 16 Oct 2019 01:21:19 +0200 Subject: [PATCH 0925/3953] move imports in spotcrime component (#27715) --- homeassistant/components/spotcrime/sensor.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/spotcrime/sensor.py b/homeassistant/components/spotcrime/sensor.py index fc3a7592af3130..2edaa3cf933ebd 100644 --- a/homeassistant/components/spotcrime/sensor.py +++ b/homeassistant/components/spotcrime/sensor.py @@ -1,27 +1,28 @@ """Sensor for Spot Crime.""" -from datetime import timedelta from collections import defaultdict +from datetime import timedelta import logging +import spotcrime import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_API_KEY, - CONF_INCLUDE, CONF_EXCLUDE, - CONF_NAME, + CONF_INCLUDE, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ATTRIBUTION, - ATTR_LATITUDE, - ATTR_LONGITUDE, + CONF_NAME, CONF_RADIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -75,7 +76,6 @@ def __init__( self, name, latitude, longitude, radius, include, exclude, api_key, days ): """Initialize the Spot Crime sensor.""" - import spotcrime self._name = name self._include = include From 2b92fd3422e339c5659c25db2537e9c8bf252bf0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 16 Oct 2019 01:22:42 +0200 Subject: [PATCH 0926/3953] Moved imports to top-level in fritzbox_callmonitor component (#27705) --- .../components/fritzbox_callmonitor/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 4dada44f4e5076..b1d601ce382287 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -1,24 +1,25 @@ """Sensor to monitor incoming/outgoing phone calls on a Fritz!Box router.""" +import datetime import logging +import re import socket import threading -import datetime import time -import re +import fritzconnection as fc # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, - CONF_PORT, CONF_NAME, CONF_PASSWORD, + CONF_PORT, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -248,8 +249,6 @@ def __init__(self, host, port, username, password, phonebook_id=0, prefixes=None self.number_dict = None self.prefixes = prefixes or [] - import fritzconnection as fc # pylint: disable=import-error - # Establish a connection to the FRITZ!Box. self.fph = fc.FritzPhonebook( address=self.host, user=self.username, password=self.password From 04a5f19f6aaad6e6518666d707b0d5cc1ab9c18a Mon Sep 17 00:00:00 2001 From: bouni Date: Wed, 16 Oct 2019 01:24:18 +0200 Subject: [PATCH 0927/3953] moved imports to top level (#27696) --- homeassistant/components/dovado/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index 29f0cc59392691..a13c49cc61a1ae 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import dovado import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -32,7 +33,6 @@ def setup(hass, config): """Set up the Dovado component.""" - import dovado hass.data[DOMAIN] = DovadoData( dovado.Dovado( From b8e00925e7139c44ec8e3fcc832108e771ca29e7 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 16 Oct 2019 00:32:17 +0000 Subject: [PATCH 0928/3953] [ci skip] Translation update --- .../components/abode/.translations/de.json | 22 +++++ .../components/abode/.translations/fr.json | 12 +++ .../components/abode/.translations/nl.json | 22 +++++ .../components/airly/.translations/de.json | 6 +- .../binary_sensor/.translations/de.json | 89 ++++++++++++++++++- .../binary_sensor/.translations/nl.json | 58 ++++++++++++ .../components/cover/.translations/de.json | 10 +++ .../components/cover/.translations/nl.json | 10 +++ .../components/deconz/.translations/de.json | 21 +++++ .../components/deconz/.translations/lb.json | 1 + .../components/deconz/.translations/nl.json | 2 + .../components/deconz/.translations/ru.json | 1 + .../deconz/.translations/zh-Hant.json | 1 + .../components/ecobee/.translations/de.json | 18 +++- .../components/ecobee/.translations/nl.json | 15 ++++ .../iaqualink/.translations/de.json | 21 +++++ .../components/izone/.translations/de.json | 15 ++++ .../components/lock/.translations/de.json | 8 ++ .../components/lock/.translations/nl.json | 8 ++ .../components/met/.translations/nl.json | 2 +- .../components/neato/.translations/nl.json | 20 +++++ .../opentherm_gw/.translations/de.json | 10 ++- .../opentherm_gw/.translations/nl.json | 3 +- .../components/plex/.translations/de.json | 38 ++++++-- .../components/plex/.translations/nl.json | 35 ++++++++ .../components/sensor/.translations/de.json | 5 ++ .../components/sensor/.translations/nl.json | 17 ++++ .../solaredge/.translations/de.json | 21 +++++ .../components/soma/.translations/de.json | 6 +- .../components/soma/.translations/en.json | 2 +- .../components/soma/.translations/lb.json | 10 +++ .../components/soma/.translations/nl.json | 7 ++ .../components/soma/.translations/no.json | 10 +++ .../components/soma/.translations/ru.json | 10 +++ .../components/switch/.translations/de.json | 19 ++++ .../transmission/.translations/de.json | 9 +- .../transmission/.translations/nl.json | 16 ++++ .../components/unifi/.translations/nl.json | 5 ++ .../components/withings/.translations/de.json | 3 + .../components/zha/.translations/de.json | 37 ++++++++ .../components/zha/.translations/nl.json | 8 ++ 41 files changed, 614 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/abode/.translations/de.json create mode 100644 homeassistant/components/abode/.translations/fr.json create mode 100644 homeassistant/components/abode/.translations/nl.json create mode 100644 homeassistant/components/binary_sensor/.translations/nl.json create mode 100644 homeassistant/components/cover/.translations/de.json create mode 100644 homeassistant/components/cover/.translations/nl.json create mode 100644 homeassistant/components/ecobee/.translations/nl.json create mode 100644 homeassistant/components/iaqualink/.translations/de.json create mode 100644 homeassistant/components/izone/.translations/de.json create mode 100644 homeassistant/components/lock/.translations/de.json create mode 100644 homeassistant/components/lock/.translations/nl.json create mode 100644 homeassistant/components/neato/.translations/nl.json create mode 100644 homeassistant/components/plex/.translations/nl.json create mode 100644 homeassistant/components/sensor/.translations/nl.json create mode 100644 homeassistant/components/solaredge/.translations/de.json create mode 100644 homeassistant/components/soma/.translations/nl.json create mode 100644 homeassistant/components/switch/.translations/de.json create mode 100644 homeassistant/components/transmission/.translations/nl.json diff --git a/homeassistant/components/abode/.translations/de.json b/homeassistant/components/abode/.translations/de.json new file mode 100644 index 00000000000000..ed5ec85a5d7b27 --- /dev/null +++ b/homeassistant/components/abode/.translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Abode erlaubt." + }, + "error": { + "connection_error": "Es kann keine Verbindung zu Abode hergestellt werden.", + "identifier_exists": "Das Konto ist bereits registriert.", + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail-Adresse" + }, + "title": "Gib deine Abode-Anmeldeinformationen ein" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/fr.json b/homeassistant/components/abode/.translations/fr.json new file mode 100644 index 00000000000000..c2e4b241b90fbf --- /dev/null +++ b/homeassistant/components/abode/.translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Adresse e-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/nl.json b/homeassistant/components/abode/.translations/nl.json new file mode 100644 index 00000000000000..89b5ae0c4a57ed --- /dev/null +++ b/homeassistant/components/abode/.translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Slechts een enkele configuratie van Abode is toegestaan." + }, + "error": { + "connection_error": "Kan geen verbinding maken met Abode.", + "identifier_exists": "Account is al geregistreerd.", + "invalid_credentials": "Ongeldige inloggegevens." + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mailadres" + }, + "title": "Vul uw Abode-inloggegevens in" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/de.json b/homeassistant/components/airly/.translations/de.json index cb290dc46c087e..83c23a90389f69 100644 --- a/homeassistant/components/airly/.translations/de.json +++ b/homeassistant/components/airly/.translations/de.json @@ -1,15 +1,19 @@ { "config": { "error": { - "name_exists": "Name existiert bereits" + "auth": "Der API-Schl\u00fcssel ist nicht korrekt.", + "name_exists": "Name existiert bereits", + "wrong_location": "Keine Airly Luftmessstation an diesem Ort" }, "step": { "user": { "data": { + "api_key": "Airly API-Schl\u00fcssel", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", "name": "Name der Integration" }, + "description": "Einrichtung der Airly-Luftqualit\u00e4t Integration. Um einen API-Schl\u00fcssel zu generieren, registriere dich auf https://developer.airly.eu/register", "title": "Airly" } }, diff --git a/homeassistant/components/binary_sensor/.translations/de.json b/homeassistant/components/binary_sensor/.translations/de.json index 25e8ea2f86bde1..e246198864bb82 100644 --- a/homeassistant/components/binary_sensor/.translations/de.json +++ b/homeassistant/components/binary_sensor/.translations/de.json @@ -1,7 +1,94 @@ { "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} Batterie ist schwach", + "is_cold": "{entity_name} ist kalt", + "is_connected": "{entity_name} ist verbunden", + "is_gas": "{entity_name} erkennt Gas", + "is_hot": "{entity_name} ist hei\u00df", + "is_light": "{entity_name} erkennt Licht", + "is_locked": "{entity_name} ist gesperrt", + "is_moist": "{entity_name} ist feucht", + "is_motion": "{entity_name} erkennt Bewegung", + "is_moving": "{entity_name} bewegt sich", + "is_no_gas": "{entity_name} erkennt kein Gas", + "is_no_light": "{entity_name} erkennt kein Licht", + "is_no_motion": "{entity_name} erkennt keine Bewegung", + "is_no_problem": "{entity_name} erkennt kein Problem", + "is_no_smoke": "{entity_name} erkennt keinen Rauch", + "is_no_sound": "{entity_name} erkennt keine Ger\u00e4usche", + "is_no_vibration": "{entity_name} erkennt keine Vibrationen", + "is_not_bat_low": "{entity_name} Batterie ist normal", + "is_not_cold": "{entity_name} ist nicht kalt", + "is_not_connected": "{entity_name} ist nicht verbunden", + "is_not_hot": "{entity_name} ist nicht hei\u00df", + "is_not_locked": "{entity_name} ist entsperrt", + "is_not_moist": "{entity_name} ist trocken", + "is_not_moving": "{entity_name} bewegt sich nicht", + "is_not_occupied": "{entity_name} ist nicht besch\u00e4ftigt / besetzt", + "is_not_open": "{entity_name} ist geschlossen", + "is_not_plugged_in": "{entity_name} ist nicht angeschlossen", + "is_not_powered": "{entity_name} wird nicht mit Strom versorgt", + "is_not_present": "{entity_name} ist nicht vorhanden", + "is_not_unsafe": "{entity_name} ist sicher", + "is_occupied": "{entity_name} ist besch\u00e4ftigt / besetzt", + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet", + "is_open": "{entity_name} ist offen", + "is_plugged_in": "{entity_name} ist eingesteckt", + "is_powered": "{entity_name} wird mit Strom versorgt", + "is_present": "{entity_name} ist vorhanden", + "is_problem": "{entity_name} hat ein Problem festgestellt", + "is_smoke": "{entity_name} hat Rauch detektiert", + "is_sound": "{entity_name} hat Ger\u00e4usche detektiert", + "is_unsafe": "{entity_name} ist unsicher", + "is_vibration": "{entity_name} erkennt Vibrationen." + }, "trigger_type": { - "plugged_in": "{entity_name} eingesteckt" + "bat_low": "{entity_name} Batterie schwach", + "closed": "{entity_name} geschlossen", + "cold": "{entity_name} wurde kalt", + "connected": "{entity_name} verbunden", + "gas": "{entity_name} hat Gas detektiert", + "hot": "{entity_name} wurde hei\u00df", + "light": "{entity_name} hat Licht detektiert", + "locked": "{entity_name} gesperrt", + "moist": "{entity_name} wurde feucht", + "moist\u00a7": "{entity_name} wurde feucht", + "motion": "{entity_name} hat Bewegungen detektiert", + "moving": "{entity_name} hat angefangen sich zu bewegen", + "no_gas": "{entity_name} hat kein Gas mehr erkannt", + "no_light": "{entity_name} hat kein Licht mehr erkannt", + "no_motion": "{entity_name} hat keine Bewegung mehr erkannt", + "no_problem": "{entity_name} hat kein Problem mehr erkannt", + "no_smoke": "{entity_name} hat keinen Rauch mehr erkannt", + "no_sound": "{entity_name} hat keine Ger\u00e4usche mehr erkannt", + "no_vibration": "{entity_name}hat keine Vibrationen mehr erkannt", + "not_bat_low": "{entity_name} Batterie normal", + "not_cold": "{entity_name} w\u00e4rmte auf", + "not_connected": "{entity_name} getrennt", + "not_hot": "{entity_name} k\u00fchlte ab", + "not_locked": "{entity_name} entsperrt", + "not_moist": "{entity_name} wurde trocken", + "not_moving": "{entity_name} bewegt sich nicht mehr", + "not_occupied": "{entity_name} wurde frei / inaktiv", + "not_opened": "{entity_name} geschlossen", + "not_plugged_in": "{entity_name} ist nicht angeschlossen", + "not_powered": "{entity_name} nicht mit Strom versorgt", + "not_present": "{entity_name} nicht anwesend", + "not_unsafe": "{entity_name} wurde sicher", + "occupied": "{entity_name} wurde besch\u00e4ftigt / besetzt", + "opened": "{entity_name} ge\u00f6ffnet", + "plugged_in": "{entity_name} eingesteckt", + "powered": "{entity_name} wird mit Strom versorgt", + "present": "{entity_name} anwesend", + "problem": "{entity_name} hat ein Problem festgestellt", + "smoke": "{entity_name} detektiert Rauch", + "sound": "{entity_name} detektiert Ger\u00e4usche", + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet", + "unsafe": "{entity_name} ist unsicher", + "vibration": "{entity_name} detektiert Vibrationen" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/nl.json b/homeassistant/components/binary_sensor/.translations/nl.json new file mode 100644 index 00000000000000..92cadf79aa8373 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/nl.json @@ -0,0 +1,58 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batterij is bijna leeg", + "is_cold": "{entity_name} is koud", + "is_connected": "{entity_name} is verbonden", + "is_gas": "{entity_name} detecteert gas", + "is_hot": "{entity_name} is hot", + "is_no_gas": "{entity_name} detecteert geen gas", + "is_no_light": "{entity_name} detecteert geen licht", + "is_no_motion": "{entity_name} detecteert geen beweging", + "is_no_problem": "{entity_name} detecteert geen probleem", + "is_no_smoke": "{entity_name} detecteert geen rook", + "is_no_sound": "{entity_name} detecteert geen geluid", + "is_no_vibration": "{entity_name} detecteert geen trillingen", + "is_not_bat_low": "{entity_name} batterij is normaal", + "is_not_cold": "{entity_name} is niet koud", + "is_not_connected": "{entity_name} is niet verbonden", + "is_not_hot": "{entity_name} is niet heet", + "is_not_occupied": "{entity_name} is niet bezet", + "is_not_present": "{entity_name} is niet aanwezig", + "is_not_unsafe": "{entity_name} is veilig", + "is_occupied": "{entity_name} bezet is", + "is_present": "{entity_name} is aanwezig", + "is_problem": "{entity_name} detecteert een probleem", + "is_smoke": "{entity_name} detecteert rook", + "is_sound": "{entity_name} detecteert geluid", + "is_unsafe": "{entity_name} is onveilig", + "is_vibration": "{entity_name} detecteert trillingen" + }, + "trigger_type": { + "bat_low": "{entity_name} batterij bijna leeg", + "closed": "{entity_name} gesloten", + "connected": "{entity_name} verbonden", + "light": "{entity_name} begon licht te detecteren", + "locked": "{entity_name} vergrendeld", + "moist": "{entity_name} werd vochtig", + "motion": "{entity_name} begon beweging te detecteren", + "not_bat_low": "{entity_name} batterij normaal", + "not_connected": "{entity_name} verbroken", + "not_locked": "{entity_name} ontgrendeld", + "not_occupied": "{entity_name} werd niet bezet", + "not_opened": "{entity_name} gesloten", + "not_plugged_in": "{entity_name} niet verbonden", + "not_powered": "{entity_name} niet ingeschakeld", + "occupied": "{entity_name} werd bezet", + "opened": "{entity_name} geopend", + "plugged_in": "{entity_name} aangesloten", + "powered": "{entity_name} heeft vermogen", + "problem": "{entity_name} begonnen met het detecteren van een probleem", + "smoke": "{entity_name} begon rook te detecteren", + "sound": "{entity_name} begon geluid te detecteren", + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld", + "vibration": "{entity_name} begon trillingen te detecteren" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/de.json b/homeassistant/components/cover/.translations/de.json new file mode 100644 index 00000000000000..e9ed497ccc2142 --- /dev/null +++ b/homeassistant/components/cover/.translations/de.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} ist geschlossen", + "is_closing": "{entity_name} wird geschlossen", + "is_open": "{entity_name} ist offen", + "is_opening": "{entity_name} wird ge\u00f6ffnet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/nl.json b/homeassistant/components/cover/.translations/nl.json new file mode 100644 index 00000000000000..93015afbfdd8d8 --- /dev/null +++ b/homeassistant/components/cover/.translations/nl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} is gesloten", + "is_closing": "{entity_name} wordt gesloten", + "is_open": "{entity_name} is open", + "is_opening": "{entity_name} wordt geopend" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 830ae0fd13f2f1..2bf0667cadba47 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -11,6 +11,7 @@ "error": { "no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden" }, + "flow_title": "deCONZ Zigbee Gateway", "step": { "hassio_confirm": { "data": { @@ -43,12 +44,32 @@ }, "device_automation": { "trigger_subtype": { + "both_buttons": "Beide Tasten", + "button_1": "Erste Taste", + "button_2": "Zweite Taste", + "button_3": "Dritte Taste", + "button_4": "Vierte Taste", "close": "Schlie\u00dfen", + "dim_down": "Dimmer runter", + "dim_up": "Dimmer hoch", "left": "Links", "open": "Offen", "right": "Rechts", "turn_off": "Ausschalten", "turn_on": "Einschalten" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt", + "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", + "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", + "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt", + "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt", + "remote_button_rotated": "Button gedreht \"{subtype}\".", + "remote_button_rotation_stopped": "Die Tastendrehung \"{subtype}\" wurde gestoppt", + "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" Taste losgelassen", + "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt", + "remote_gyro_activated": "Ger\u00e4t ersch\u00fcttert" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index f5f41a28a32c95..49394eb9773bbb 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -11,6 +11,7 @@ "error": { "no_key": "Konnt keen API Schl\u00ebssel kr\u00e9ien" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 116f6254b37213..7f690f11f1d74c 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -11,6 +11,7 @@ "error": { "no_key": "Kon geen API-sleutel ophalen" }, + "flow_title": "deCONZ Zigbee gateway ( {host} )", "step": { "hassio_confirm": { "data": { @@ -64,6 +65,7 @@ "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt", "remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt", "remote_button_rotated": "Knop gedraaid \" {subtype} \"", + "remote_button_rotation_stopped": "Knoprotatie \" {subtype} \" gestopt", "remote_button_short_press": "\" {subtype} \" knop ingedrukt", "remote_button_short_release": "\"{subtype}\" knop losgelaten", "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt", diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index f342f3145b9f28..d3a8781bb4eef8 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -11,6 +11,7 @@ "error": { "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." }, + "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 2ad613cde6868f..975600a5745cb3 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -11,6 +11,7 @@ "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" }, + "flow_title": "deCONZ Zigbee \u9598\u9053\u5668\uff08{host}\uff09", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/ecobee/.translations/de.json b/homeassistant/components/ecobee/.translations/de.json index 1959f769d3a4c9..33d493f6db008d 100644 --- a/homeassistant/components/ecobee/.translations/de.json +++ b/homeassistant/components/ecobee/.translations/de.json @@ -1,11 +1,25 @@ { "config": { + "abort": { + "one_instance_only": "Diese Integration unterst\u00fctzt derzeit nur eine Ecobee-Instanz." + }, + "error": { + "pin_request_failed": "Fehler beim Anfordern der PIN von ecobee; Bitte \u00fcberpr\u00fcfe, ob der API-Schl\u00fcssel korrekt ist.", + "token_request_failed": "Fehler beim Anfordern eines Token von ecobee; Bitte versuche es erneut." + }, "step": { + "authorize": { + "description": "Bitte autorisiere diese App unter https://www.ecobee.com/consumerportal/index.html mit Pincode:\n\n{pin}\n\nDr\u00fccke dann auf Senden.", + "title": "App auf ecobee.com autorisieren" + }, "user": { "data": { "api_key": "API Key" - } + }, + "description": "Bitte geben Sie den von ecobee.com erhaltenen API-Schl\u00fcssel ein.", + "title": "ecobee API-Schl\u00fcssel" } - } + }, + "title": "ecobee" } } \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/nl.json b/homeassistant/components/ecobee/.translations/nl.json new file mode 100644 index 00000000000000..b2e3ce9cdd7ccd --- /dev/null +++ b/homeassistant/components/ecobee/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "one_instance_only": "Deze integratie ondersteunt momenteel slechts \u00e9\u00e9n ecobee-instantie." + }, + "error": { + "token_request_failed": "Fout bij het aanvragen van tokens bij ecobee; probeer het opnieuw." + }, + "step": { + "authorize": { + "description": "Autoriseer deze app op https://www.ecobee.com/consumerportal/index.html met pincode: \n\n {pin} \n \nDruk vervolgens op Submit." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/de.json b/homeassistant/components/iaqualink/.translations/de.json new file mode 100644 index 00000000000000..d929022c9053a6 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Es kann nur eine einzige iAqualink-Verbindung konfiguriert werden." + }, + "error": { + "connection_failure": "Die Verbindung zu iAqualink ist nicht m\u00f6glich. Bitte \u00fcberpr\u00fcfe den Benutzernamen und das Passwort." + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername/E-Mail-Adresse" + }, + "description": "Bitte geben Sie den Benutzernamen und das Passwort f\u00fcr Ihr iAqualink-Konto ein.", + "title": "Mit iAqualink verbinden" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/de.json b/homeassistant/components/izone/.translations/de.json new file mode 100644 index 00000000000000..3c7ebfa937fe6b --- /dev/null +++ b/homeassistant/components/izone/.translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Es wurden keine iZone-Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Es ist nur eine einzige Konfiguration von iZone erforderlich." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie iZone einrichten?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/de.json b/homeassistant/components/lock/.translations/de.json new file mode 100644 index 00000000000000..02c387ff48782e --- /dev/null +++ b/homeassistant/components/lock/.translations/de.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} ist gesperrt", + "is_unlocked": "{entity_name} ist entsperrt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/nl.json b/homeassistant/components/lock/.translations/nl.json new file mode 100644 index 00000000000000..6a39f9cbf58d6b --- /dev/null +++ b/homeassistant/components/lock/.translations/nl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} is vergrendeld", + "is_unlocked": "{entity_name} is ontgrendeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/nl.json b/homeassistant/components/met/.translations/nl.json index 87f13084f7e74b..c8b120b855a277 100644 --- a/homeassistant/components/met/.translations/nl.json +++ b/homeassistant/components/met/.translations/nl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Naam bestaat al" + "name_exists": "Locatie bestaat al." }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/nl.json b/homeassistant/components/neato/.translations/nl.json new file mode 100644 index 00000000000000..a90009cb7be1c6 --- /dev/null +++ b/homeassistant/components/neato/.translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "invalid_credentials": "Ongeldige gebruikersgegevens" + }, + "create_entry": { + "default": "Zie [Neato-documentatie] ({docs_url})." + }, + "error": { + "invalid_credentials": "Ongeldige inloggegevens", + "unexpected_error": "Onverwachte fout" + }, + "step": { + "user": { + "title": "Neato-account info" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json index 274dd46488b8d6..0957e233116468 100644 --- a/homeassistant/components/opentherm_gw/.translations/de.json +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -10,10 +10,14 @@ "init": { "data": { "device": "Pfad oder URL", + "floor_temperature": "Boden-Temperatur", "id": "ID", - "name": "Name" - } + "name": "Name", + "precision": "Genauigkeit der Temperatur" + }, + "title": "OpenTherm Gateway" } - } + }, + "title": "OpenTherm Gateway" } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json index 4fec1baba7badb..81f4aa028f1dee 100644 --- a/homeassistant/components/opentherm_gw/.translations/nl.json +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -4,7 +4,8 @@ "init": { "data": { "device": "Pad of URL", - "id": "ID" + "id": "ID", + "precision": "Klimaattemperatuur precisie" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json index 56715e60a8c671..4b24e6c78a67f2 100644 --- a/homeassistant/components/plex/.translations/de.json +++ b/homeassistant/components/plex/.translations/de.json @@ -1,32 +1,60 @@ { "config": { "abort": { - "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden" + "all_configured": "Alle verkn\u00fcpften Server sind bereits konfiguriert", + "already_configured": "Dieser Plex-Server ist bereits konfiguriert", + "already_in_progress": "Plex wird konfiguriert", + "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden", + "invalid_import": "Die importierte Konfiguration ist ung\u00fcltig", + "token_request_timeout": "Zeit\u00fcberschreitung beim Erhalt des Tokens", + "unknown": "Aus unbekanntem Grund fehlgeschlagen" + }, + "error": { + "faulty_credentials": "Autorisation fehlgeschlagen", + "no_servers": "Keine Server sind mit dem Konto verbunden", + "no_token": "Bereitstellen eines Tokens oder Ausw\u00e4hlen der manuellen Einrichtung", + "not_found": "Plex-Server nicht gefunden" }, "step": { "manual_setup": { "data": { "host": "Host", "port": "Port", - "ssl": "SSL verwenden" + "ssl": "SSL verwenden", + "token": "Token (falls erforderlich)", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "title": "Plex Server" }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Mehrere Server verf\u00fcgbar, w\u00e4hlen Sie einen aus:", + "title": "Plex-Server ausw\u00e4hlen" + }, "start_website_auth": { "description": "Weiter zur Autorisierung unter plex.tv.", "title": "Plex Server verbinden" }, "user": { "data": { - "manual_setup": "Manuelle Einrichtung" + "manual_setup": "Manuelle Einrichtung", + "token": "Plex Token" }, - "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." + "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell.", + "title": "Plex Server verbinden" } - } + }, + "title": "Plex" }, "options": { "step": { "plex_mp_settings": { + "data": { + "show_all_controls": "Alle Steuerelemente anzeigen", + "use_episode_art": "Episode-Bilder verwenden" + }, "description": "Optionen f\u00fcr Plex-Media-Player" } } diff --git a/homeassistant/components/plex/.translations/nl.json b/homeassistant/components/plex/.translations/nl.json new file mode 100644 index 00000000000000..413f4ad3207e80 --- /dev/null +++ b/homeassistant/components/plex/.translations/nl.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "invalid_import": "Ge\u00efmporteerde configuratie is ongeldig", + "token_request_timeout": "Time-out verkrijgen van token", + "unknown": "Mislukt om onbekende reden" + }, + "error": { + "faulty_credentials": "Autorisatie mislukt", + "no_servers": "Geen servers gekoppeld aan account", + "no_token": "Geef een token op of selecteer handmatige installatie", + "not_found": "Plex-server niet gevonden" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Poort", + "ssl": "Gebruik SSL" + }, + "title": "Plex server" + }, + "start_website_auth": { + "description": "Ga verder met autoriseren bij plex.tv.", + "title": "Verbind de Plex server" + }, + "user": { + "data": { + "manual_setup": "Handmatig setup" + }, + "description": "Ga verder met autoriseren bij plex.tv of configureer een server." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json index 1f248099df34e5..bf28653c0ce000 100644 --- a/homeassistant/components/sensor/.translations/de.json +++ b/homeassistant/components/sensor/.translations/de.json @@ -1,7 +1,10 @@ { "device_automation": { "condition_type": { + "is_battery_level": "{entity_name} Batteriestand", "is_humidity": "{entity_name} Feuchtigkeit", + "is_illuminance": "{entity_name} Beleuchtungsst\u00e4rke", + "is_power": "{entity_name} Leistung", "is_pressure": "{entity_name} Druck", "is_signal_strength": "{entity_name} Signalst\u00e4rke", "is_temperature": "{entity_name} Temperatur", @@ -11,6 +14,8 @@ "trigger_type": { "battery_level": "{entity_name} Batteriestatus", "humidity": "{entity_name} Feuchtigkeit", + "illuminance": "{entity_name} Beleuchtungsst\u00e4rke", + "power": "{entity_name} Leistung", "pressure": "{entity_name} Druck", "signal_strength": "{entity_name} Signalst\u00e4rke", "temperature": "{entity_name} Temperatur", diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json new file mode 100644 index 00000000000000..aca2306d90e17e --- /dev/null +++ b/homeassistant/components/sensor/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "condition_type": { + "is_power": "{entity_name}\nvermogen" + }, + "trigger_type": { + "battery_level": "{entity_name} batterijniveau", + "humidity": "{entity_name} vochtigheidsgraad", + "illuminance": "{entity_name} verlichtingssterkte", + "power": "{entity_name} vermogen", + "pressure": "{entity_name} druk", + "signal_strength": "{entity_name} signaalsterkte", + "temperature": "{entity_name} temperatuur", + "timestamp": "{entity_name} tijdstip" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/de.json b/homeassistant/components/solaredge/.translations/de.json new file mode 100644 index 00000000000000..cbe913e131c40a --- /dev/null +++ b/homeassistant/components/solaredge/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Diese site_id ist bereits konfiguriert" + }, + "error": { + "site_exists": "Diese site_id ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "api_key": "Der API-Schl\u00fcssel f\u00fcr diese Site", + "name": "Der Name dieser Installation", + "site_id": "Die SolarEdge-Site-ID" + }, + "title": "Definiere die API-Parameter f\u00fcr diese Installation" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json index d93eec8aed7442..838d46a6d42ada 100644 --- a/homeassistant/components/soma/.translations/de.json +++ b/homeassistant/components/soma/.translations/de.json @@ -4,6 +4,10 @@ "already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." - } + }, + "create_entry": { + "default": "Erfolgreich bei Soma authentifiziert." + }, + "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json index aa2f92f0be603c..42e09a8762c582 100644 --- a/homeassistant/components/soma/.translations/en.json +++ b/homeassistant/components/soma/.translations/en.json @@ -20,4 +20,4 @@ }, "title": "Soma" } -} +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json index d8aba082537bf7..93e9a1e66c4cff 100644 --- a/homeassistant/components/soma/.translations/lb.json +++ b/homeassistant/components/soma/.translations/lb.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert." }, + "step": { + "user": { + "data": { + "host": "Apparat", + "port": "Port" + }, + "description": "Gitt Verbindungs Informatioune vun \u00e4rem SOMA Connect an.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/nl.json b/homeassistant/components/soma/.translations/nl.json new file mode 100644 index 00000000000000..0bf2836c5a1007 --- /dev/null +++ b/homeassistant/components/soma/.translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_setup": "U kunt slechts \u00e9\u00e9n Soma-account configureren." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json index c3d9d7e70d4268..b2d80208b83e70 100644 --- a/homeassistant/components/soma/.translations/no.json +++ b/homeassistant/components/soma/.translations/no.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Vellykket autentisering med Somfy." }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "description": "Vennligst skriv tilkoblingsinnstillingene for din SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json index 5ab3af0ecf8895..f7e6574b113854 100644 --- a/homeassistant/components/soma/.translations/ru.json +++ b/homeassistant/components/soma/.translations/ru.json @@ -8,6 +8,16 @@ "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a SOMA Connect.", + "title": "Soma" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/de.json b/homeassistant/components/switch/.translations/de.json new file mode 100644 index 00000000000000..5396facadd7dcd --- /dev/null +++ b/homeassistant/components/switch/.translations/de.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} umschalten", + "turn_off": "Schalte {entity_name} aus.", + "turn_on": "Schalte {entity_name} ein." + }, + "condition_type": { + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet", + "turn_off": "{entity_name} ausgeschaltet", + "turn_on": "{entity_name} eingeschaltet" + }, + "trigger_type": { + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json index ed0342b9430924..1a2fa4a48c06d2 100644 --- a/homeassistant/components/transmission/.translations/de.json +++ b/homeassistant/components/transmission/.translations/de.json @@ -21,16 +21,19 @@ "password": "Passwort", "port": "Port", "username": "Benutzername" - } + }, + "title": "Transmission-Client einrichten" } - } + }, + "title": "Transmission" }, "options": { "step": { "init": { "data": { "scan_interval": "Aktualisierungsfrequenz" - } + }, + "description": "Konfigurieren von Optionen f\u00fcr Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/nl.json b/homeassistant/components/transmission/.translations/nl.json new file mode 100644 index 00000000000000..6d9d130f85ca9a --- /dev/null +++ b/homeassistant/components/transmission/.translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken met host", + "wrong_credentials": "verkeerde gebruikersnaam of wachtwoord" + }, + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + }, + "title": "Verzendclient instellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json index 518f0066534411..36e21728f1d5f1 100644 --- a/homeassistant/components/unifi/.translations/nl.json +++ b/homeassistant/components/unifi/.translations/nl.json @@ -38,6 +38,11 @@ "one": "Leeg", "other": "Leeg" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients" + } } } } diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json index 15b6f4e3b01f59..dabf184d7ed36a 100644 --- a/homeassistant/components/withings/.translations/de.json +++ b/homeassistant/components/withings/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Withings muss konfiguriert werden, bevor die Integration authentifiziert werden kann. Bitte lies die Dokumentation." + }, "create_entry": { "default": "Erfolgreiche Authentifizierung mit Withings f\u00fcr das ausgew\u00e4hlte Profil." }, diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 969c78e7b13e47..3329eafa1c617e 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -18,13 +18,50 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Kreischen", + "warn": "Warnen" + }, "trigger_subtype": { + "both_buttons": "Beide Tasten", + "button_1": "Erste Taste", + "button_2": "Zweite Taste", + "button_3": "Dritte Taste", + "button_4": "Vierte Taste", + "button_5": "F\u00fcnfte Taste", + "button_6": "Sechste Taste", "close": "Schlie\u00dfen", + "dim_down": "Dimmer runter", + "dim_up": "Dimmer hoch", + "face_1": "mit Fl\u00e4che 1 aktiviert", + "face_2": "mit Fl\u00e4che 2 aktiviert", + "face_3": "mit Fl\u00e4che 3 aktiviert", + "face_4": "mit Fl\u00e4che 4 aktiviert", + "face_5": "mit Fl\u00e4che 5 aktiviert", + "face_6": "mit Fl\u00e4che 6 aktiviert", + "face_any": "Mit einer beliebigen/festgelegten Fl\u00e4che(n) aktiviert", "left": "Links", "open": "Offen", "right": "Rechts", "turn_off": "Ausschalten", "turn_on": "Einschalten" + }, + "trigger_type": { + "device_dropped": "Ger\u00e4t ist gefallen", + "device_flipped": "Ger\u00e4t umgedreht \"{subtype}\"", + "device_knocked": "Ger\u00e4t klopfte \"{subtype}\"", + "device_rotated": "Ger\u00e4t wurde gedreht \"{subtype}\"", + "device_shaken": "Ger\u00e4t ersch\u00fcttert", + "device_slid": "Ger\u00e4t gerutscht \"{subtype}\"", + "device_tilted": "Ger\u00e4t gekippt", + "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt", + "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", + "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", + "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt", + "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt", + "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" Taste losgelassen", + "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nl.json b/homeassistant/components/zha/.translations/nl.json index 5e5c666b1a4e8d..bfb47c9d7fcf8b 100644 --- a/homeassistant/components/zha/.translations/nl.json +++ b/homeassistant/components/zha/.translations/nl.json @@ -18,7 +18,15 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Schreeuw", + "warn": "Waarschuwen" + }, "trigger_subtype": { + "close": "Sluiten", + "dim_down": "Dim omlaag", + "dim_up": "Dim omhoog", + "face_any": "Met elk/opgegeven gezicht (en) geactiveerd", "left": "Links", "open": "Open", "right": "Rechts", From c8f64840957a72aa826fdae6616d631736eea4c3 Mon Sep 17 00:00:00 2001 From: Bogdan Vlaicu Date: Wed, 16 Oct 2019 03:52:30 -0400 Subject: [PATCH 0929/3953] New sensor platform integration for Orange and Rockland Utility smart energy meter (#27571) * New sensor platform integration for Orange and Rockland Utility smart energy meter * New sensor platform integration for Orange and Rockland Utility smart energy meter * bumped the oru py version to 0.1.9 * Added PLATFORM_SCHEMA Adde unique_id property Changed logger level from info to debug when printing the updated sensor value Set the SCAN_INTERVAL to 15 mins Added exception handling durin init when creating the oru meter instance * Various fixes base on the PR review + Added SCAN_INTERVAL for 15 mins * fixed path to documentation --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/oru/__init__.py | 1 + homeassistant/components/oru/manifest.json | 8 ++ homeassistant/components/oru/sensor.py | 92 ++++++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 106 insertions(+) create mode 100644 homeassistant/components/oru/__init__.py create mode 100644 homeassistant/components/oru/manifest.json create mode 100644 homeassistant/components/oru/sensor.py diff --git a/.coveragerc b/.coveragerc index 69ce7f8322ceef..52cf74f384a49b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -485,6 +485,7 @@ omit = homeassistant/components/openweathermap/weather.py homeassistant/components/opple/light.py homeassistant/components/orangepi_gpio/* + homeassistant/components/oru/* homeassistant/components/orvibo/switch.py homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 8e52210cec7480..547fe504892154 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -215,6 +215,7 @@ homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff homeassistant/components/orangepi_gpio/* @pascallj +homeassistant/components/oru/* @bvlaicu homeassistant/components/owlet/* @oblogic7 homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend diff --git a/homeassistant/components/oru/__init__.py b/homeassistant/components/oru/__init__.py new file mode 100644 index 00000000000000..d1517ab0bf1b85 --- /dev/null +++ b/homeassistant/components/oru/__init__.py @@ -0,0 +1 @@ +"""The Orange and Rockland Utility smart energy meter integration.""" diff --git a/homeassistant/components/oru/manifest.json b/homeassistant/components/oru/manifest.json new file mode 100644 index 00000000000000..ff5e74fd26007e --- /dev/null +++ b/homeassistant/components/oru/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "oru", + "name": "Orange and Rockland Utility Smart Energy Meter Sensor", + "documentation": "https://www.home-assistant.io/integrations/oru", + "dependencies": [], + "codeowners": ["@bvlaicu"], + "requirements": ["oru==0.1.9"] +} \ No newline at end of file diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py new file mode 100644 index 00000000000000..e68d8e1c45a964 --- /dev/null +++ b/homeassistant/components/oru/sensor.py @@ -0,0 +1,92 @@ +"""Platform for sensor integration.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from oru import Meter +from oru import MeterError + +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +CONF_METER_NUMBER = "meter_number" + +SCAN_INTERVAL = timedelta(minutes=15) + +SENSOR_NAME = "ORU Current Energy Usage" +SENSOR_ICON = "mdi:counter" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_METER_NUMBER): cv.string}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the sensor platform.""" + + meter_number = config[CONF_METER_NUMBER] + + try: + meter = Meter(meter_number) + + except MeterError: + _LOGGER.error("Unable to create Oru meter") + return + + add_entities([CurrentEnergyUsageSensor(meter)], True) + + _LOGGER.debug("Oru meter_number = %s", meter_number) + + +class CurrentEnergyUsageSensor(Entity): + """Representation of the sensor.""" + + def __init__(self, meter): + """Initialize the sensor.""" + self._state = None + self._available = None + self.meter = meter + + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return self.meter.meter_id + + @property + def name(self): + """Return the name of the sensor.""" + return SENSOR_NAME + + @property + def icon(self): + """Return the icon of the sensor.""" + return SENSOR_ICON + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return ENERGY_KILO_WATT_HOUR + + def update(self): + """Fetch new state data for the sensor.""" + try: + last_read = self.meter.last_read() + + self._state = last_read + self._available = True + + _LOGGER.debug( + "%s = %s %s", self.name, self._state, self.unit_of_measurement + ) + except MeterError as err: + self._available = False + + _LOGGER.error("Unexpected oru meter error: %s", err) diff --git a/requirements_all.txt b/requirements_all.txt index 567acd712a1c00..51b28bdf3520bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -916,6 +916,9 @@ openwebifpy==3.1.1 # homeassistant.components.luci openwrt-luci-rpc==1.1.1 +# homeassistant.components.oru +oru==0.1.9 + # homeassistant.components.orvibo orvibo==1.1.1 From 5a35e52adf5630e9068a046d661c7808c1e3af1a Mon Sep 17 00:00:00 2001 From: bouni Date: Wed, 16 Oct 2019 10:25:37 +0200 Subject: [PATCH 0930/3953] Move imports in device_tracker component (#27676) * moved imports to top level * sorted imports using isort --- .../components/device_tracker/legacy.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 5c186cc12a1182..ad7ff3fe3f5c82 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -1,11 +1,12 @@ """Legacy device tracker classes.""" import asyncio from datetime import timedelta +import hashlib from typing import Any, List, Sequence import voluptuous as vol -from homeassistant.core import callback +from homeassistant import util from homeassistant.components import zone from homeassistant.components.group import ( ATTR_ADD_ENTITIES, @@ -16,16 +17,7 @@ SERVICE_SET, ) from homeassistant.components.zone import async_active_zone -from homeassistant.config import load_yaml_config_file, async_log_exception -from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_registry import async_get_registry -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import GPSType, HomeAssistantType -from homeassistant import util -import homeassistant.util.dt as dt_util -from homeassistant.util.yaml import dump - +from homeassistant.config import async_log_exception, load_yaml_config_file from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, @@ -37,9 +29,17 @@ CONF_MAC, CONF_NAME, DEVICE_DEFAULT_NAME, - STATE_NOT_HOME, STATE_HOME, + STATE_NOT_HOME, ) +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import GPSType, HomeAssistantType +import homeassistant.util.dt as dt_util +from homeassistant.util.yaml import dump from .const import ( ATTR_BATTERY, @@ -635,7 +635,6 @@ def get_gravatar_for_email(email: str): Async friendly. """ - import hashlib url = "https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar" return url.format(hashlib.md5(email.encode("utf-8").lower()).hexdigest()) From 44b6258e48aa3cdb1bd90280d1271e695bde8f82 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 16 Oct 2019 10:32:25 +0100 Subject: [PATCH 0931/3953] Add evohome high_precision temperatures (#27513) * add high_precision (current) temperatures * bump client to use aiohttp for v1 client * token saving now event-driven rather than scheduled * protection against invalid tokens that cause issues * tweak error message --- homeassistant/components/evohome/__init__.py | 269 +++++++++++------- homeassistant/components/evohome/climate.py | 8 +- .../components/evohome/manifest.json | 2 +- .../components/evohome/water_heater.py | 4 +- requirements_all.txt | 2 +- 5 files changed, 168 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index e9254c373d94ab..a52780c8a0f3b9 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -10,9 +10,9 @@ import aiohttp.client_exceptions import voluptuous as vol import evohomeasync2 +import evohomeasync from homeassistant.const import ( - CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, @@ -32,10 +32,13 @@ _LOGGER = logging.getLogger(__name__) -CONF_ACCESS_TOKEN_EXPIRES = "access_token_expires" -CONF_REFRESH_TOKEN = "refresh_token" +ACCESS_TOKEN = "access_token" +ACCESS_TOKEN_EXPIRES = "access_token_expires" +REFRESH_TOKEN = "refresh_token" +USER_DATA = "user_data" CONF_LOCATION_IDX = "location_idx" + SCAN_INTERVAL_DEFAULT = timedelta(seconds=300) SCAN_INTERVAL_MINIMUM = timedelta(seconds=60) @@ -96,14 +99,15 @@ def convert_key(key: str) -> str: def _handle_exception(err) -> bool: + """Return False if the exception can't be ignored.""" try: raise err except evohomeasync2.AuthenticationError: _LOGGER.error( - "Failed to (re)authenticate with the vendor's server. " + "Failed to authenticate with the vendor's server. " "Check your network and the vendor's service status page. " - "Check that your username and password are correct. " + "Also check that your username and password are correct. " "Message is: %s", err, ) @@ -135,14 +139,77 @@ def _handle_exception(err) -> bool: ) return False - raise # we don't expect/handle any other ClientResponseError + raise # we don't expect/handle any other Exceptions async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell evohome system.""" - broker = EvoBroker(hass, config[DOMAIN]) - if not await broker.init_client(): + + async def load_auth_tokens(store) -> Tuple[Dict, Optional[Dict]]: + app_storage = await store.async_load() + tokens = dict(app_storage if app_storage else {}) + + if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]: + # any tokens wont be valid, and store might be be corrupt + await store.async_save({}) + return ({}, None) + + # evohomeasync2 requires naive/local datetimes as strings + if tokens.get(ACCESS_TOKEN_EXPIRES) is not None: + tokens[ACCESS_TOKEN_EXPIRES] = _dt_to_local_naive( + dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) + ) + + user_data = tokens.pop(USER_DATA, None) + return (tokens, user_data) + + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + tokens, user_data = await load_auth_tokens(store) + + client_v2 = evohomeasync2.EvohomeClient( + config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD], + **tokens, + session=async_get_clientsession(hass), + ) + + try: + await client_v2.login() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: + _handle_exception(err) return False + finally: + config[DOMAIN][CONF_PASSWORD] = "REDACTED" + + loc_idx = config[DOMAIN][CONF_LOCATION_IDX] + try: + loc_config = client_v2.installation_info[loc_idx][GWS][0][TCS][0] + except IndexError: + _LOGGER.error( + "Config error: '%s' = %s, but the valid range is 0-%s. " + "Unable to continue. Fix any configuration errors and restart HA.", + CONF_LOCATION_IDX, + loc_idx, + len(client_v2.installation_info) - 1, + ) + return False + + _LOGGER.debug("Config = %s", loc_config) + + client_v1 = evohomeasync.EvohomeClient( + client_v2.username, + client_v2.password, + user_data=user_data, + session=async_get_clientsession(hass), + ) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN]["broker"] = broker = EvoBroker( + hass, client_v2, client_v1, store, config[DOMAIN] + ) + + await broker.save_auth_tokens() + await broker.update() # get initial state hass.async_create_task(async_load_platform(hass, "climate", DOMAIN, {}, config)) if broker.tcs.hotwater: @@ -160,116 +227,100 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: class EvoBroker: """Container for evohome client and data.""" - def __init__(self, hass, params) -> None: + def __init__(self, hass, client, client_v1, store, params) -> None: """Initialize the evohome client and its data structure.""" self.hass = hass + self.client = client + self.client_v1 = client_v1 + self._store = store self.params = params - self.config = {} - - self.client = self.tcs = None - self._app_storage = {} - - hass.data[DOMAIN] = {} - hass.data[DOMAIN]["broker"] = self - async def init_client(self) -> bool: - """Initialse the evohome data broker. - - Return True if this is successful, otherwise return False. - """ - refresh_token, access_token, access_token_expires = ( - await self._load_auth_tokens() + loc_idx = params[CONF_LOCATION_IDX] + self.config = client.installation_info[loc_idx][GWS][0][TCS][0] + self.tcs = ( + client.locations[loc_idx] # pylint: disable=protected-access + ._gateways[0] + ._control_systems[0] ) + self.temps = None + async def save_auth_tokens(self) -> None: + """Save access tokens and session IDs to the store for later use.""" # evohomeasync2 uses naive/local datetimes - if access_token_expires is not None: - access_token_expires = _dt_to_local_naive(access_token_expires) - - client = self.client = evohomeasync2.EvohomeClient( - self.params[CONF_USERNAME], - self.params[CONF_PASSWORD], - refresh_token=refresh_token, - access_token=access_token, - access_token_expires=access_token_expires, - session=async_get_clientsession(self.hass), - ) + access_token_expires = _local_dt_to_aware(self.client.access_token_expires) - try: - await client.login() - except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - if not _handle_exception(err): - return False + app_storage = {CONF_USERNAME: self.client.username} + app_storage[REFRESH_TOKEN] = self.client.refresh_token + app_storage[ACCESS_TOKEN] = self.client.access_token + app_storage[ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat() - finally: - self.params[CONF_PASSWORD] = "REDACTED" + if self.client_v1 and self.client_v1.user_data: + app_storage[USER_DATA] = { + "userInfo": {"userID": self.client_v1.user_data["userInfo"]["userID"]}, + "sessionId": self.client_v1.user_data["sessionId"], + } + else: + app_storage[USER_DATA] = None - self.hass.add_job(self._save_auth_tokens()) + await self._store.async_save(app_storage) - loc_idx = self.params[CONF_LOCATION_IDX] - try: - self.config = client.installation_info[loc_idx][GWS][0][TCS][0] - - except IndexError: - _LOGGER.error( - "Config error: '%s' = %s, but its valid range is 0-%s. " - "Unable to continue. " - "Fix any configuration errors and restart HA.", - CONF_LOCATION_IDX, - loc_idx, - len(client.installation_info) - 1, - ) - return False - - self.tcs = ( - client.locations[loc_idx] # pylint: disable=protected-access - ._gateways[0] - ._control_systems[0] - ) + async def _update_v1(self, *args, **kwargs) -> None: + """Get the latest high-precision temperatures of the default Location.""" - _LOGGER.debug("Config = %s", self.config) - if _LOGGER.isEnabledFor(logging.DEBUG): # don't do an I/O unless required - await self.update() # includes: _LOGGER.debug("Status = %s"... + def get_session_id(client_v1) -> Optional[str]: + user_data = client_v1.user_data if client_v1 else None + return user_data.get("sessionId") if user_data else None - return True + session_id = get_session_id(self.client_v1) - async def _load_auth_tokens( - self - ) -> Tuple[Optional[str], Optional[str], Optional[datetime]]: - store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - app_storage = self._app_storage = await store.async_load() + try: + temps = list(await self.client_v1.temperatures(force_refresh=True)) - if app_storage is None: - app_storage = self._app_storage = {} + except aiohttp.ClientError as err: + _LOGGER.warning( + "Unable to obtain the latest high-precision temperatures. " + "Check your network and the vendor's service status page. " + "Proceeding with low-precision temperatures. " + "Message is: %s", + err, + ) + self.temps = None # these are now stale, will fall back to v2 temps - if app_storage.get(CONF_USERNAME) == self.params[CONF_USERNAME]: - refresh_token = app_storage.get(CONF_REFRESH_TOKEN) - access_token = app_storage.get(CONF_ACCESS_TOKEN) - at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES) - if at_expires_str: - at_expires_dt = dt_util.parse_datetime(at_expires_str) + else: + if ( + str(self.client_v1.location_id) + != self.client.locations[self.params[CONF_LOCATION_IDX]].locationId + ): + _LOGGER.warning( + "The v2 API's configured location doesn't match " + "the v1 API's default location (there is more than one location), " + "so the high-precision feature will be disabled" + ) + self.client_v1 = self.temps = None else: - at_expires_dt = None + self.temps = {str(i["id"]): i["temp"] for i in temps} - return (refresh_token, access_token, at_expires_dt) + _LOGGER.debug("Temperatures = %s", self.temps) - return (None, None, None) # account switched: so tokens wont be valid + if session_id != get_session_id(self.client_v1): + await self.save_auth_tokens() - async def _save_auth_tokens(self, *args) -> None: - # evohomeasync2 uses naive/local datetimes - access_token_expires = _local_dt_to_aware(self.client.access_token_expires) + async def _update_v2(self, *args, **kwargs) -> None: + """Get the latest modes, temperatures, setpoints of a Location.""" + access_token = self.client.access_token - self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME] - self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token - self._app_storage[CONF_ACCESS_TOKEN] = self.client.access_token - self._app_storage[CONF_ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat() + loc_idx = self.params[CONF_LOCATION_IDX] + try: + status = await self.client.locations[loc_idx].status() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: + _handle_exception(err) + else: + self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) - store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - await store.async_save(self._app_storage) + _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) - self.hass.helpers.event.async_track_point_in_utc_time( - self._save_auth_tokens, - access_token_expires + self.params[CONF_SCAN_INTERVAL], - ) + if access_token != self.client.access_token: + await self.save_auth_tokens() async def update(self, *args, **kwargs) -> None: """Get the latest state data of an entire evohome Location. @@ -278,17 +329,13 @@ async def update(self, *args, **kwargs) -> None: operating mode of the Controller and the current temp of its children (e.g. Zones, DHW controller). """ - loc_idx = self.params[CONF_LOCATION_IDX] + await self._update_v2() - try: - status = await self.client.locations[loc_idx].status() - except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - _handle_exception(err) - else: - # inform the evohome devices that state data has been updated - self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) + if self.client_v1: + await self._update_v1() - _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) + # inform the evohome devices that state data has been updated + self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) class EvoDevice(Entity): @@ -305,10 +352,8 @@ def __init__(self, evo_broker, evo_device) -> None: self._evo_tcs = evo_broker.tcs self._unique_id = self._name = self._icon = self._precision = None - - self._device_state_attrs = {} - self._state_attributes = [] self._supported_features = None + self._device_state_attrs = {} @callback def _refresh(self) -> None: @@ -394,9 +439,13 @@ def __init__(self, evo_broker, evo_device) -> None: @property def current_temperature(self) -> Optional[float]: """Return the current temperature of a Zone.""" - if self._evo_device.temperatureStatus["isAvailable"]: - return self._evo_device.temperatureStatus["temperature"] - return None + if not self._evo_device.temperatureStatus["isAvailable"]: + return None + + if self._evo_broker.temps: + return self._evo_broker.temps[self._evo_device.zoneId] + + return self._evo_device.temperatureStatus["temperature"] @property def setpoints(self) -> Dict[str, Any]: diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 7df2db1b17e512..eb7f3f7d7d8a61 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -72,14 +72,13 @@ async def async_setup_platform( return broker = hass.data[DOMAIN]["broker"] - loc_idx = broker.params[CONF_LOCATION_IDX] _LOGGER.debug( "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)", broker.tcs.modelType, broker.tcs.systemId, broker.tcs.location.name, - loc_idx, + broker.params[CONF_LOCATION_IDX], ) # special case of RoundModulation/RoundWireless (is a single zone system) @@ -148,9 +147,12 @@ def __init__(self, evo_broker, evo_device) -> None: self._name = evo_device.name self._icon = "mdi:radiator" - self._precision = self._evo_device.setpointCapabilities["valueResolution"] self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE self._preset_modes = list(HA_PRESET_TO_EVO) + if evo_broker.client_v1: + self._precision = PRECISION_TENTHS + else: + self._precision = self._evo_device.setpointCapabilities["valueResolution"] @property def hvac_mode(self) -> str: diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 5633880be35e58..da942db7920ca7 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -3,7 +3,7 @@ "name": "Evohome", "documentation": "https://www.home-assistant.io/integrations/evohome", "requirements": [ - "evohome-async==0.3.3b4" + "evohome-async==0.3.3b5" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 37bdcd82afcac7..e29dbb49af262f 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -7,7 +7,7 @@ SUPPORT_OPERATION_MODE, WaterHeaterDevice, ) -from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON +from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime @@ -55,7 +55,7 @@ def __init__(self, evo_broker, evo_device) -> None: self._name = "DHW controller" self._icon = "mdi:thermometer-lines" - self._precision = PRECISION_WHOLE + self._precision = PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE @property diff --git a/requirements_all.txt b/requirements_all.txt index 51b28bdf3520bd..2b5b4f53e52551 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -477,7 +477,7 @@ eternalegypt==0.0.10 # evdev==0.6.1 # homeassistant.components.evohome -evohome-async==0.3.3b4 +evohome-async==0.3.3b5 # homeassistant.components.dlib_face_detect # homeassistant.components.dlib_face_identify From cc93dd49286759b040786b80a635d9d359ab9e78 Mon Sep 17 00:00:00 2001 From: Paolo Tuninetto Date: Wed, 16 Oct 2019 12:05:05 +0200 Subject: [PATCH 0932/3953] Move imports in Kodi component (#27728) * Move imports for Kodi component * Removed empty line ad requested by review --- homeassistant/components/kodi/media_player.py | 17 ++++------------- homeassistant/components/kodi/notify.py | 6 ++---- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 9f0aab6c00c7d8..9b2ba01e90ada2 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -7,6 +7,10 @@ import urllib import aiohttp +import jsonrpc_base +import jsonrpc_async +import jsonrpc_websocket + import voluptuous as vol from homeassistant.components.kodi import SERVICE_CALL_METHOD @@ -231,8 +235,6 @@ def cmd(func): @wraps(func) async def wrapper(obj, *args, **kwargs): """Wrap all command methods.""" - import jsonrpc_base - try: await func(obj, *args, **kwargs) except jsonrpc_base.jsonrpc.TransportError as exc: @@ -268,9 +270,6 @@ def __init__( unique_id=None, ): """Initialize the Kodi device.""" - import jsonrpc_async - import jsonrpc_websocket - self.hass = hass self._name = name self._unique_id = unique_id @@ -389,8 +388,6 @@ def async_on_quit(self, sender, data): async def _get_players(self): """Return the active player objects or None.""" - import jsonrpc_base - try: return await self.server.Player.GetActivePlayers() except jsonrpc_base.jsonrpc.TransportError: @@ -420,8 +417,6 @@ def state(self): async def async_ws_connect(self): """Connect to Kodi via websocket protocol.""" - import jsonrpc_base - try: ws_loop_future = await self._ws_server.ws_connect() except jsonrpc_base.jsonrpc.TransportError: @@ -801,8 +796,6 @@ async def async_set_shuffle(self, shuffle): async def async_call_method(self, method, **kwargs): """Run Kodi JSONRPC API method with params.""" - import jsonrpc_base - _LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs) result_ok = False try: @@ -850,8 +843,6 @@ async def async_add_media_to_playlist( All the albums of an artist can be added with media_name="ALL" """ - import jsonrpc_base - params = {"playlistid": 0} if media_type == "SONG": if media_id is None: diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index 41dfc42b5deda5..1072cf1b7329b9 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -2,6 +2,8 @@ import logging import aiohttp +import jsonrpc_async + import voluptuous as vol from homeassistant.const import ( @@ -77,8 +79,6 @@ class KodiNotificationService(BaseNotificationService): def __init__(self, hass, url, auth=None): """Initialize the service.""" - import jsonrpc_async - self._url = url kwargs = {"timeout": DEFAULT_TIMEOUT, "session": async_get_clientsession(hass)} @@ -90,8 +90,6 @@ def __init__(self, hass, url, auth=None): async def async_send_message(self, message="", **kwargs): """Send a message to Kodi.""" - import jsonrpc_async - try: data = kwargs.get(ATTR_DATA) or {} From ec788211619c10a0513eee3ea8f90db4a6d43fad Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 16 Oct 2019 12:06:52 +0200 Subject: [PATCH 0933/3953] Add sensor platform to Airly integration (#27717) * Add sesnor.py file * Move AirlyData to __init__ * Cleaning * Update .coveragerc file * Sort consts * Sort imports * Remove icons from sensors with device_class --- .coveragerc | 1 + homeassistant/components/airly/__init__.py | 93 +++++++++++ homeassistant/components/airly/air_quality.py | 108 +++--------- homeassistant/components/airly/const.py | 15 ++ homeassistant/components/airly/sensor.py | 154 ++++++++++++++++++ 5 files changed, 284 insertions(+), 87 deletions(-) create mode 100644 homeassistant/components/airly/sensor.py diff --git a/.coveragerc b/.coveragerc index 52cf74f384a49b..859e1c0f92cbae 100644 --- a/.coveragerc +++ b/.coveragerc @@ -29,6 +29,7 @@ omit = homeassistant/components/aftership/sensor.py homeassistant/components/airly/__init__.py homeassistant/components/airly/air_quality.py + homeassistant/components/airly/sensor.py homeassistant/components/airly/const.py homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/cover.py diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 56b3477ac89b36..dc2323ddd4e3fb 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -1,5 +1,31 @@ """The Airly component.""" +import asyncio +import logging +from datetime import timedelta + +import async_timeout +from aiohttp.client_exceptions import ClientConnectorError +from airly import Airly +from airly.exceptions import AirlyError + +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import Config, HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import Throttle + +from .const import ( + ATTR_API_ADVICE, + ATTR_API_CAQI, + ATTR_API_CAQI_DESCRIPTION, + ATTR_API_CAQI_LEVEL, + DATA_CLIENT, + DOMAIN, + NO_AIRLY_SENSORS, +) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) async def async_setup(hass: HomeAssistant, config: Config) -> bool: @@ -9,13 +35,80 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: async def async_setup_entry(hass, config_entry): """Set up Airly as config entry.""" + api_key = config_entry.data[CONF_API_KEY] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + + websession = async_get_clientsession(hass) + + airly = AirlyData(websession, api_key, latitude, longitude) + + await airly.async_update() + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly + hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "sensor") + ) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" + hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality") + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") return True + + +class AirlyData: + """Define an object to hold Airly data.""" + + def __init__(self, session, api_key, latitude, longitude): + """Initialize.""" + self.latitude = latitude + self.longitude = longitude + self.airly = Airly(api_key, session) + self.data = {} + + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update Airly data.""" + + try: + with async_timeout.timeout(10): + measurements = self.airly.create_measurements_session_point( + self.latitude, self.longitude + ) + await measurements.update() + + values = measurements.current["values"] + index = measurements.current["indexes"][0] + standards = measurements.current["standards"] + + if index["description"] == NO_AIRLY_SENSORS: + _LOGGER.error("Can't retrieve data: no Airly sensors in this area") + return + for value in values: + self.data[value["name"]] = value["value"] + for standard in standards: + self.data[f"{standard['pollutant']}_LIMIT"] = standard["limit"] + self.data[f"{standard['pollutant']}_PERCENT"] = standard["percent"] + self.data[ATTR_API_CAQI] = index["value"] + self.data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ") + self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"] + self.data[ATTR_API_ADVICE] = index["advice"] + _LOGGER.debug("Data retrieved from Airly") + except ( + ValueError, + AirlyError, + asyncio.TimeoutError, + ClientConnectorError, + ) as error: + _LOGGER.error(error) + self.data = {} diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index f8500869509edd..082344c14e3b94 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -1,40 +1,29 @@ -"""Support for the Airly service.""" -import asyncio -import logging -from datetime import timedelta - -import async_timeout -from aiohttp.client_exceptions import ClientConnectorError -from airly import Airly -from airly.exceptions import AirlyError - -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +"""Support for the Airly air_quality service.""" from homeassistant.components.air_quality import ( AirQualityEntity, ATTR_AQI, ATTR_PM_10, ATTR_PM_2_5, ) -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util import Throttle - -from .const import NO_AIRLY_SENSORS - -_LOGGER = logging.getLogger(__name__) +from homeassistant.const import CONF_NAME + +from .const import ( + ATTR_API_ADVICE, + ATTR_API_CAQI, + ATTR_API_CAQI_DESCRIPTION, + ATTR_API_CAQI_LEVEL, + ATTR_API_PM10, + ATTR_API_PM10_LIMIT, + ATTR_API_PM10_PERCENT, + ATTR_API_PM25, + ATTR_API_PM25_LIMIT, + ATTR_API_PM25_PERCENT, + DATA_CLIENT, + DOMAIN, +) ATTRIBUTION = "Data provided by Airly" -ATTR_API_ADVICE = "ADVICE" -ATTR_API_CAQI = "CAQI" -ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION" -ATTR_API_CAQI_LEVEL = "LEVEL" -ATTR_API_PM10 = "PM10" -ATTR_API_PM10_LIMIT = "PM10_LIMIT" -ATTR_API_PM10_PERCENT = "PM10_PERCENT" -ATTR_API_PM25 = "PM25" -ATTR_API_PM25_LIMIT = "PM25_LIMIT" -ATTR_API_PM25_PERCENT = "PM25_PERCENT" - LABEL_ADVICE = "advice" LABEL_AQI_LEVEL = f"{ATTR_AQI}_level" LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit" @@ -42,19 +31,12 @@ LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit" LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" -DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) - async def async_setup_entry(hass, config_entry, async_add_entities): - """Add a Airly entities from a config_entry.""" - api_key = config_entry.data[CONF_API_KEY] + """Set up Airly air_quality entity based on a config entry.""" name = config_entry.data[CONF_NAME] - latitude = config_entry.data[CONF_LATITUDE] - longitude = config_entry.data[CONF_LONGITUDE] - websession = async_get_clientsession(hass) - - data = AirlyData(websession, api_key, latitude, longitude) + data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] async_add_entities([AirlyAirQuality(data, name)], True) @@ -72,7 +54,7 @@ def _decorator(self): class AirlyAirQuality(AirQualityEntity): - """Define an Airly air_quality.""" + """Define an Airly air quality.""" def __init__(self, airly, name): """Initialize.""" @@ -145,7 +127,7 @@ def device_state_attributes(self): return self._attrs async def async_update(self): - """Get the data from Airly.""" + """Update the entity.""" await self.airly.async_update() if self.airly.data: @@ -154,51 +136,3 @@ async def async_update(self): self._pm_10 = self.data[ATTR_API_PM10] self._pm_2_5 = self.data[ATTR_API_PM25] self._aqi = self.data[ATTR_API_CAQI] - - -class AirlyData: - """Define an object to hold sensor data.""" - - def __init__(self, session, api_key, latitude, longitude): - """Initialize.""" - self.latitude = latitude - self.longitude = longitude - self.airly = Airly(api_key, session) - self.data = {} - - @Throttle(DEFAULT_SCAN_INTERVAL) - async def async_update(self): - """Update Airly data.""" - - try: - with async_timeout.timeout(10): - measurements = self.airly.create_measurements_session_point( - self.latitude, self.longitude - ) - await measurements.update() - - values = measurements.current["values"] - index = measurements.current["indexes"][0] - standards = measurements.current["standards"] - - if index["description"] == NO_AIRLY_SENSORS: - _LOGGER.error("Can't retrieve data: no Airly sensors in this area") - return - for value in values: - self.data[value["name"]] = value["value"] - for standard in standards: - self.data[f"{standard['pollutant']}_LIMIT"] = standard["limit"] - self.data[f"{standard['pollutant']}_PERCENT"] = standard["percent"] - self.data[ATTR_API_CAQI] = index["value"] - self.data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ") - self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"] - self.data[ATTR_API_ADVICE] = index["advice"] - _LOGGER.debug("Data retrieved from Airly") - except ( - ValueError, - AirlyError, - asyncio.TimeoutError, - ClientConnectorError, - ) as error: - _LOGGER.error(error) - self.data = {} diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 5313ba0e494cbb..2040faea6b61f0 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -1,4 +1,19 @@ """Constants for Airly integration.""" +ATTR_API_ADVICE = "ADVICE" +ATTR_API_CAQI = "CAQI" +ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION" +ATTR_API_CAQI_LEVEL = "LEVEL" +ATTR_API_HUMIDITY = "HUMIDITY" +ATTR_API_PM1 = "PM1" +ATTR_API_PM10 = "PM10" +ATTR_API_PM10_LIMIT = "PM10_LIMIT" +ATTR_API_PM10_PERCENT = "PM10_PERCENT" +ATTR_API_PM25 = "PM25" +ATTR_API_PM25_LIMIT = "PM25_LIMIT" +ATTR_API_PM25_PERCENT = "PM25_PERCENT" +ATTR_API_PRESSURE = "PRESSURE" +ATTR_API_TEMPERATURE = "TEMPERATURE" +DATA_CLIENT = "client" DEFAULT_NAME = "Airly" DOMAIN = "airly" NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py new file mode 100644 index 00000000000000..03439d7d206f62 --- /dev/null +++ b/homeassistant/components/airly/sensor.py @@ -0,0 +1,154 @@ +"""Support for the Airly sensor service.""" +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_NAME, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PRESSURE_HPA, + TEMP_CELSIUS, +) +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_API_HUMIDITY, + ATTR_API_PM1, + ATTR_API_PRESSURE, + ATTR_API_TEMPERATURE, + DATA_CLIENT, + DOMAIN, +) + +ATTRIBUTION = "Data provided by Airly" + +ATTR_ICON = "icon" +ATTR_LABEL = "label" +ATTR_UNIT = "unit" + +HUMI_PERCENT = "%" +VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m³" + +SENSOR_TYPES = { + ATTR_API_PM1: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_PM1, + ATTR_UNIT: VOLUME_MICROGRAMS_PER_CUBIC_METER, + }, + ATTR_API_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(), + ATTR_UNIT: HUMI_PERCENT, + }, + ATTR_API_PRESSURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_PRESSURE.capitalize(), + ATTR_UNIT: PRESSURE_HPA, + }, + ATTR_API_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(), + ATTR_UNIT: TEMP_CELSIUS, + }, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Airly sensor entities based on a config entry.""" + name = config_entry.data[CONF_NAME] + + data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + + sensors = [] + for sensor in SENSOR_TYPES: + sensors.append(AirlySensor(data, name, sensor)) + async_add_entities(sensors, True) + + +def round_state(func): + """Round state.""" + + def _decorator(self): + res = func(self) + if isinstance(res, float): + return round(res) + return res + + return _decorator + + +class AirlySensor(Entity): + """Define an Airly sensor.""" + + def __init__(self, airly, name, kind): + """Initialize.""" + self.airly = airly + self.data = airly.data + self._name = name + self.kind = kind + self._device_class = None + self._state = None + self._icon = None + self._unit_of_measurement = None + self._attrs = {} + + @property + def name(self): + """Return the name.""" + return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + + @property + def state(self): + """Return the state.""" + self._state = self.data[self.kind] + if self.kind in [ATTR_API_PM1, ATTR_API_PRESSURE]: + self._state = round(self._state) + if self.kind in [ATTR_API_TEMPERATURE, ATTR_API_HUMIDITY]: + self._state = round(self._state, 1) + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def icon(self): + """Return the icon.""" + self._icon = SENSOR_TYPES[self.kind][ATTR_ICON] + return self._icon + + @property + def device_class(self): + """Return the device_class.""" + return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self.airly.latitude}-{self.airly.longitude}-{self.kind.lower()}" + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_TYPES[self.kind][ATTR_UNIT] + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def available(self): + """Return True if entity is available.""" + return bool(self.airly.data) + + async def async_update(self): + """Update the sensor.""" + await self.airly.async_update() + + if self.airly.data: + self.data = self.airly.data From 14d3b9b8f932495779b7607e6f8078b255b32f00 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 16 Oct 2019 12:19:38 +0200 Subject: [PATCH 0934/3953] Bump pyatmo version to 2.3.2 (#27731) * Bump pyatmo version to 2.3.2 * Add reachable attribute * Add reachable attribute --- homeassistant/components/netatmo/manifest.json | 2 +- homeassistant/components/netatmo/sensor.py | 3 +++ requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 83091368aff2ee..efb2840216b90c 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/integrations/netatmo", "requirements": [ - "pyatmo==2.2.1" + "pyatmo==2.3.2" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 38e3753708e1b3..70b6297388cd9c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -80,6 +80,7 @@ "gustangle": ["Gust Angle", "", "mdi:compass", None], "gustangle_value": ["Gust Angle Value", "º", "mdi:compass", None], "guststrength": ["Gust Strength", "km/h", "mdi:weather-windy", None], + "reachable": ["Reachability", "", "mdi:signal", None], "rf_status": ["Radio", "", "mdi:signal", None], "rf_status_lvl": ["Radio_lvl", "", "mdi:signal", None], "wifi_status": ["Wifi", "", "mdi:wifi", None], @@ -375,6 +376,8 @@ def update(self): self._state = "N (%d\xb0)" % data["GustAngle"] elif self.type == "guststrength": self._state = data["GustStrength"] + elif self.type == "reachable": + self._state = data["reachable"] elif self.type == "rf_status_lvl": self._state = data["rf_status"] elif self.type == "rf_status": diff --git a/requirements_all.txt b/requirements_all.txt index 2b5b4f53e52551..ac5415a4923eb0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1084,7 +1084,7 @@ pyalarmdotcom==0.3.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.2.1 +pyatmo==2.3.2 # homeassistant.components.atome pyatome==0.1.1 From a1b8f4d9c39d726fcaa64deaac3011d4f67c5ff9 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 16 Oct 2019 17:11:25 +0200 Subject: [PATCH 0935/3953] New cache on Azure (#27739) * New cache on Azure * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml --- azure-pipelines-ci.yml | 48 +++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 74e9ea107c56cd..5e42281bf7e0e6 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -37,12 +37,14 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - displayName: 'Setup Env' + . venv/bin/activate + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate flake8 homeassistant tests script @@ -52,12 +54,14 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -e . - displayName: 'Setup Env' + . venv/bin/activate + pip install -e . - script: | . venv/bin/activate python -m script.hassfest validate @@ -71,12 +75,14 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - displayName: 'Setup Env' + . venv/bin/activate + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate ./script/check_format @@ -100,7 +106,7 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_test_all.txt, .cache, homeassistant/package_constraints.txt' + keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt' build: | set -e python -m venv venv @@ -111,6 +117,10 @@ stages: # 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 + pip install -e . + displayName: 'Install Home Assistant' - script: | set -e @@ -140,7 +150,7 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_all.txt, requirements_test.txt, .cache, homeassistant/package_constraints.txt' + keyfile: 'requirements_all.txt | requirements_test.txt | homeassistant/package_constraints.txt' build: | set -e python -m venv venv @@ -149,6 +159,10 @@ stages: 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 + pip install -e . + displayName: 'Install Home Assistant' - script: | . venv/bin/activate pylint homeassistant From 8a0f26e15547652c1ae7ff4050684a18762d375f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 16 Oct 2019 17:37:24 +0200 Subject: [PATCH 0936/3953] Add cache for mypy (#27745) * Add cache for mypy * Update ruamel_yaml.py --- azure-pipelines-ci.yml | 13 +++++++------ homeassistant/util/ruamel_yaml.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 5e42281bf7e0e6..a566baf6561655 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -172,13 +172,14 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -e . - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - displayName: 'Setup Env' + . venv/bin/activate + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate mypy homeassistant diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index b7e8927888c734..6793784abae310 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -90,7 +90,7 @@ def load_yaml(fname: str, round_trip: bool = False) -> JSON_TYPE: if round_trip: yaml = YAML(typ="rt") # type ignore: https://bitbucket.org/ruamel/yaml/pull-requests/42 - yaml.preserve_quotes = True # type: ignore + yaml.preserve_quotes = True else: if ExtSafeConstructor.name is None: ExtSafeConstructor.name = fname From bd95a89f45174f0430a6fdb140208c977d97242a Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Thu, 17 Oct 2019 01:28:12 +0700 Subject: [PATCH 0937/3953] Bump ndms2-client to 0.0.10 (#27734) --- CODEOWNERS | 1 + homeassistant/components/keenetic_ndms2/manifest.json | 6 ++++-- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 547fe504892154..40f1e93cfb97b5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -157,6 +157,7 @@ homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph +homeassistant/components/keenetic_ndms2/* @foxel homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills homeassistant/components/konnected/* @heythisisnate diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 41e45a9e5782f4..4613d2d9608022 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,8 +3,10 @@ "name": "Keenetic ndms2", "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": [ - "ndms2_client==0.0.9" + "ndms2_client==0.0.10" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@foxel" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index ac5415a4923eb0..8a5733df4ec22d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -847,7 +847,7 @@ n26==0.2.7 nad_receiver==0.0.11 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.0.9 +ndms2_client==0.0.10 # homeassistant.components.ness_alarm nessclient==0.9.15 From 0607a306125eb1482ad38a932c94c7a35a37104c Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Wed, 16 Oct 2019 20:28:59 +0200 Subject: [PATCH 0938/3953] Upgrade youtube_dl to 2019.10.16 (#27737) --- 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 886535555d5a84..de3d4546ca0350 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/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.09.28" + "youtube_dl==2019.10.16" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 8a5733df4ec22d..efbfb4c7dd1fb4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2017,7 +2017,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.09.28 +youtube_dl==2019.10.16 # homeassistant.components.zengge zengge==0.2 From 6ffc520b1ce3376bfa5001f4a2cb876ce4cab061 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 16 Oct 2019 20:45:03 +0200 Subject: [PATCH 0939/3953] Axis - Improve discovery title by adding placeholder support (#27663) * Improve discovery title by adding placeholder support --- homeassistant/components/axis/.translations/en.json | 1 + homeassistant/components/axis/config_flow.py | 7 +++++++ homeassistant/components/axis/strings.json | 1 + tests/components/axis/test_config_flow.py | 2 ++ 4 files changed, 11 insertions(+) diff --git a/homeassistant/components/axis/.translations/en.json b/homeassistant/components/axis/.translations/en.json index 5fd5d9be5655e9..c7d84aa8cc37dd 100644 --- a/homeassistant/components/axis/.translations/en.json +++ b/homeassistant/components/axis/.translations/en.json @@ -12,6 +12,7 @@ "device_unavailable": "Device is not available", "faulty_credentials": "Bad user credentials" }, + "flow_title": "Axis device: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 3473eba30653dd..5eb4f9daddda37 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -191,6 +191,12 @@ async def async_step_zeroconf(self, discovery_info): load_json, self.hass.config.path(CONFIG_FILE) ) + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + "name": discovery_info["hostname"][:-7], + "host": discovery_info[CONF_HOST], + } + if serialnumber not in config_file: self.discovery_schema = { vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): str, @@ -198,6 +204,7 @@ async def async_step_zeroconf(self, discovery_info): vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PORT, default=discovery_info[CONF_PORT]): int, } + return await self.async_step_user() try: diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 29fe09b7e5bd24..2dc23f3e4662f1 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -1,6 +1,7 @@ { "config": { "title": "Axis device", + "flow_title": "Axis device: {name} ({host})", "step": { "user": { "title": "Set up Axis device", diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 5ec3f933e9ef18..5aec416961d644 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -186,6 +186,7 @@ async def test_zeroconf_flow(hass): data={ config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80, + "hostname": "name", "properties": {"macaddress": "00408C12345"}, }, context={"source": "zeroconf"}, @@ -319,6 +320,7 @@ async def test_zeroconf_flow_bad_config_file(hass): config_flow.DOMAIN, data={ config_flow.CONF_HOST: "1.2.3.4", + "hostname": "name", "properties": {"macaddress": "00408C12345"}, }, context={"source": "zeroconf"}, From 43c85c0549df0e4beb4f2dcccce6ac1015fc1304 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 17 Oct 2019 06:34:56 +0200 Subject: [PATCH 0940/3953] Add device action support to the alarm_control_panel integration (#27616) * Add device action support to the alarm_control_panel integration * Improve tests --- .../alarm_control_panel/device_action.py | 126 ++++++++ .../alarm_control_panel/strings.json | 11 + .../components/device_automation/__init__.py | 19 ++ .../alarm_control_panel/test_device_action.py | 274 ++++++++++++++++++ .../components/device_automation/test_init.py | 102 +++++++ .../test/alarm_control_panel.py | 91 ++++++ 6 files changed, 623 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/device_action.py create mode 100644 homeassistant/components/alarm_control_panel/strings.json create mode 100644 tests/components/alarm_control_panel/test_device_action.py create mode 100644 tests/testing_config/custom_components/test/alarm_control_panel.py diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py new file mode 100644 index 00000000000000..a3c2b4822611e5 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -0,0 +1,126 @@ +"""Provides device automations for Alarm control panel.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_CODE, + ATTR_ENTITY_ID, + CONF_CODE, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import ATTR_CODE_ARM_REQUIRED, DOMAIN + +ACTION_TYPES = {"arm_away", "arm_home", "arm_night", "disarm", "trigger"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Optional(CONF_CODE): cv.string, + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Alarm control panel devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add actions for each entity that belongs to this integration + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_away", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_home", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_night", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "disarm", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "trigger", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + if CONF_CODE in config: + service_data[ATTR_CODE] = config[CONF_CODE] + + if config[CONF_TYPE] == "arm_away": + service = SERVICE_ALARM_ARM_AWAY + elif config[CONF_TYPE] == "arm_home": + service = SERVICE_ALARM_ARM_HOME + elif config[CONF_TYPE] == "arm_night": + service = SERVICE_ALARM_ARM_NIGHT + elif config[CONF_TYPE] == "disarm": + service = SERVICE_ALARM_DISARM + elif config[CONF_TYPE] == "trigger": + service = SERVICE_ALARM_TRIGGER + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) + + +async def async_get_action_capabilities(hass, config): + """List action capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + code_required = state.attributes.get(ATTR_CODE_ARM_REQUIRED) if state else False + + if config[CONF_TYPE] == "trigger" or ( + config[CONF_TYPE] != "disarm" and not code_required + ): + return {} + + return {"extra_fields": vol.Schema({vol.Optional(CONF_CODE): str})} diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json new file mode 100644 index 00000000000000..f67635776dd8a7 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Arm {entity_name} away", + "arm_home": "Arm {entity_name} home", + "arm_night": "Arm {entity_name} night", + "disarm": "Disarm {entity_name}", + "trigger": "Trigger {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 9d0a5a72a47188..0be1c3eb1dd63c 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -59,6 +59,9 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( websocket_device_automation_list_triggers ) + hass.components.websocket_api.async_register_command( + websocket_device_automation_get_action_capabilities + ) hass.components.websocket_api.async_register_command( websocket_device_automation_get_condition_capabilities ) @@ -209,6 +212,22 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): connection.send_result(msg["id"], triggers) +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/action/capabilities", + vol.Required("action"): dict, + } +) +async def websocket_device_automation_get_action_capabilities(hass, connection, msg): + """Handle request for device action capabilities.""" + action = msg["action"] + capabilities = await _async_get_device_automation_capabilities( + hass, "action", action + ) + connection.send_result(msg["id"], capabilities) + + @websocket_api.async_response @websocket_api.websocket_command( { diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py new file mode 100644 index 00000000000000..c2dfcbd78b985c --- /dev/null +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -0,0 +1,274 @@ +"""The tests for Alarm control panel device actions.""" +import pytest + +from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.const import ( + CONF_PLATFORM, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + STATE_UNKNOWN, +) +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + mock_device_registry, + mock_registry, + async_get_device_automations, + async_get_device_automation_capabilities, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a alarm_control_panel.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "arm_away", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "arm_home", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "arm_night", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "disarm", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "trigger", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_get_action_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["no_arm_code"].unique_id, + device_id=device_entry.id, + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "arm_away": {"extra_fields": []}, + "arm_home": {"extra_fields": []}, + "arm_night": {"extra_fields": []}, + "disarm": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "trigger": {"extra_fields": []}, + } + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert len(actions) == 5 + for action in actions: + capabilities = await async_get_device_automation_capabilities( + hass, "action", action + ) + assert capabilities == expected_capabilities[action["type"]] + + +async def test_get_action_capabilities_arm_code(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["arm_code"].unique_id, + device_id=device_entry.id, + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "arm_away": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "arm_home": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "arm_night": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "disarm": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "trigger": {"extra_fields": []}, + } + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert len(actions) == 5 + for action in actions: + capabilities = await async_get_device_automation_capabilities( + hass, "action", action + ) + assert capabilities == expected_capabilities[action["type"]] + + +async def test_action(hass): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_arm_away", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "arm_away", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_arm_home", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "arm_home", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_arm_night", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "arm_night", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event_disarm"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "disarm", + "code": "1234", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_trigger", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "trigger", + }, + }, + ] + }, + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state == STATE_UNKNOWN + ) + + hass.bus.async_fire("test_event_arm_away") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_ARMED_AWAY + ) + + hass.bus.async_fire("test_event_arm_home") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_ARMED_HOME + ) + + hass.bus.async_fire("test_event_arm_night") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_ARMED_NIGHT + ) + + hass.bus.async_fire("test_event_disarm") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_DISARMED + ) + + hass.bus.async_fire("test_event_trigger") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_TRIGGERED + ) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 1af4b541a92958..3c0e3b1eca7ad9 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -170,6 +170,106 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert _same_lists(triggers, expected_triggers) +async def test_websocket_get_action_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get the expected action capabilities for an alarm through websocket.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + "alarm_control_panel", "test", "5678", device_id=device_entry.id + ) + expected_capabilities = { + "arm_away": {"extra_fields": []}, + "arm_home": {"extra_fields": []}, + "arm_night": {"extra_fields": []}, + "disarm": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "trigger": {"extra_fields": []}, + } + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 1, "type": "device_automation/action/list", "device_id": device_entry.id} + ) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + actions = msg["result"] + + id = 2 + assert len(actions) == 5 + for action in actions: + await client.send_json( + { + "id": id, + "type": "device_automation/action/capabilities", + "action": action, + } + ) + msg = await client.receive_json() + assert msg["id"] == id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities[action["type"]] + id = id + 1 + + +async def test_websocket_get_bad_action_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no action capabilities for a non existing domain.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/action/capabilities", + "action": {"domain": "beer"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + +async def test_websocket_get_no_action_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no action capabilities for a domain with no device action capabilities.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/action/capabilities", + "action": {"domain": "deconz"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + async def test_websocket_get_condition_capabilities( hass, hass_ws_client, device_reg, entity_reg ): @@ -204,6 +304,7 @@ async def test_websocket_get_condition_capabilities( conditions = msg["result"] id = 2 + assert len(conditions) == 2 for condition in conditions: await client.send_json( { @@ -301,6 +402,7 @@ async def test_websocket_get_trigger_capabilities( triggers = msg["result"] id = 2 + assert len(triggers) == 2 for trigger in triggers: await client.send_json( { diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py new file mode 100644 index 00000000000000..0e2842f869561a --- /dev/null +++ b/tests/testing_config/custom_components/test/alarm_control_panel.py @@ -0,0 +1,91 @@ +""" +Provide a mock alarm_control_panel platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from tests.common import MockEntity + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + "arm_code": MockAlarm( + name=f"Alarm arm code", + code_arm_required=True, + unique_id="unique_arm_code", + ), + "no_arm_code": MockAlarm( + name=f"Alarm no arm code", + code_arm_required=False, + unique_id="unique_no_arm_code", + ), + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockAlarm(MockEntity, AlarmControlPanel): + """Mock Alarm control panel class.""" + + def __init__(self, **values): + """Init the Mock Alarm Control Panel.""" + self._state = None + + MockEntity.__init__(self, **values) + + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return self._handle("code_arm_required") + + @property + def state(self): + """Return the state of the device.""" + return self._state + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self._state = STATE_ALARM_ARMED_AWAY + self.async_write_ha_state() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self._state = STATE_ALARM_ARMED_HOME + self.async_write_ha_state() + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + self._state = STATE_ALARM_ARMED_NIGHT + self.async_write_ha_state() + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if code == "1234": + self._state = STATE_ALARM_DISARMED + self.async_write_ha_state() + + def alarm_trigger(self, code=None): + """Send alarm trigger command.""" + self._state = STATE_ALARM_TRIGGERED + self.async_write_ha_state() From e79a5baf9ec11bf211d8faf099e5cfc33db6d9e3 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 17 Oct 2019 06:36:19 +0200 Subject: [PATCH 0941/3953] Move imports in slack and socialblade (#27747) * Moved imports to top-level in samsungtv, slack and socialblade * Rewinded top-level imports in samsungtv component --- homeassistant/components/slack/notify.py | 10 ++++------ homeassistant/components/socialblade/sensor.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 1b9895aab7632c..b645a590c3cb5a 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -3,11 +3,10 @@ import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import slacker +from slacker import Slacker import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -15,6 +14,8 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,7 +46,6 @@ def get_service(hass, config, discovery_info=None): """Get the Slack notification service.""" - import slacker channel = config.get(CONF_CHANNEL) api_key = config.get(CONF_API_KEY) @@ -67,7 +67,6 @@ class SlackNotificationService(BaseNotificationService): def __init__(self, default_channel, api_token, username, icon, is_allowed_path): """Initialize the service.""" - from slacker import Slacker self._default_channel = default_channel self._api_token = api_token @@ -84,7 +83,6 @@ def __init__(self, default_channel, api_token, username, icon, is_allowed_path): def send_message(self, message="", **kwargs): """Send a message to a user.""" - import slacker if kwargs.get(ATTR_TARGET) is None: targets = [self._default_channel] diff --git a/homeassistant/components/socialblade/sensor.py b/homeassistant/components/socialblade/sensor.py index 0acfb63a629749..3d53e76a27a125 100644 --- a/homeassistant/components/socialblade/sensor.py +++ b/homeassistant/components/socialblade/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import socialbladeclient import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -71,7 +72,6 @@ def device_state_attributes(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Social Blade.""" - import socialbladeclient try: data = socialbladeclient.get_data(self.channel_id) From 23db94c62764c17842db4c93d980aa2941c82ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 17 Oct 2019 07:36:43 +0300 Subject: [PATCH 0942/3953] Run mypy in pre-commit without args to match CI (#27741) --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55e00443ba1190..48d77cfdc6fc77 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,4 +17,5 @@ repos: rev: v0.730 hooks: - id: mypy + args: [] exclude: ^script/scaffold/templates/ From 46f1166edd8847ce2d25104de0be7c500d590a98 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Thu, 17 Oct 2019 10:32:02 +0200 Subject: [PATCH 0943/3953] Fix On/Off for melissa (#27733) * Fixed On/Off for melissa fixes #27092 * reformatted --- homeassistant/components/melissa/climate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 10ea6200c6fb6e..38f4977c96abc5 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -156,7 +156,9 @@ async def async_set_hvac_mode(self, hvac_mode): return mode = self.hass_mode_to_melissa(hvac_mode) - await self.async_send({self._api.MODE: mode}) + await self.async_send( + {self._api.MODE: mode, self._api.STATE: self._api.STATE_ON} + ) async def async_send(self, value): """Send action to service.""" From 2d6d6ba90e5e2f179a07c4ecd7de1744e86a8025 Mon Sep 17 00:00:00 2001 From: Antonio Larrosa Date: Thu, 17 Oct 2019 11:29:08 +0200 Subject: [PATCH 0944/3953] Forget auth token when going offline so we can reconnect (#26630) When an amcrest camera was unplugged and then plugged again it was impossible to reconnect to it, since the old auth token was reused while we need to use a new one. In fact, the method that is called every minute to check the camera availability is going to fail always since we're reusing an old token. By forgetting the token (setting it to None) when going offline, we ensure that we'll regenerate it in the next commands thus allowing to reconnect to the camera when it comes back online. --- homeassistant/components/amcrest/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index f915872abf048c..d49104a0b2624b 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -167,6 +167,8 @@ def command(self, cmd, retries=None, timeout_cmd=None, stream=False): offline = not self.available if offline and was_online: _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) + with self._token_lock: + self._token = None dispatcher_send( self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) ) From 7fd606a254ed902c0edb330849654ece07d49a1b Mon Sep 17 00:00:00 2001 From: Tomasz Jagusz Date: Thu, 17 Oct 2019 11:30:18 +0200 Subject: [PATCH 0945/3953] bump rpi.gpio to 0.7.0 (#27753) --- homeassistant/components/mcp23017/manifest.json | 2 +- homeassistant/components/rpi_gpio/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index 2dbffd829f8b8a..13c36424dd6841 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -3,7 +3,7 @@ "name": "MCP23017 I/O Expander", "documentation": "https://www.home-assistant.io/integrations/mcp23017", "requirements": [ - "RPi.GPIO==0.6.5", + "RPi.GPIO==0.7.0", "adafruit-blinka==1.2.1", "adafruit-circuitpython-mcp230xx==1.1.2" ], diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index 0bee2baeddf741..4d3ea4da010d54 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -3,7 +3,7 @@ "name": "Rpi gpio", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", "requirements": [ - "RPi.GPIO==0.6.5" + "RPi.GPIO==0.7.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index efbfb4c7dd1fb4..c8d66f91468403 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -82,7 +82,7 @@ PyXiaomiGateway==0.12.4 # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio -# RPi.GPIO==0.6.5 +# RPi.GPIO==0.7.0 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 From b187ca93d02f3dea2e8f4f12fb8c29ad6b7a9b12 Mon Sep 17 00:00:00 2001 From: Tomasz Jagusz Date: Thu, 17 Oct 2019 12:24:53 +0200 Subject: [PATCH 0946/3953] Move imports in rpi_gpio (#27752) * move imports for rpi_gpio * fixed pylint error * fix pylint error * removed empty line * add missing blank line * sort with isort --- homeassistant/components/rpi_gpio/__init__.py | 13 ++----------- homeassistant/components/rpi_gpio/binary_sensor.py | 2 +- homeassistant/components/rpi_gpio/cover.py | 4 ++-- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rpi_gpio/__init__.py b/homeassistant/components/rpi_gpio/__init__.py index 31509614df4bb1..ed7eefbb1fe3cf 100644 --- a/homeassistant/components/rpi_gpio/__init__.py +++ b/homeassistant/components/rpi_gpio/__init__.py @@ -1,6 +1,8 @@ """Support for controlling GPIO pins of a Raspberry Pi.""" import logging +from RPi import GPIO # pylint: disable=import-error + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -10,7 +12,6 @@ def setup(hass, config): """Set up the Raspberry PI GPIO component.""" - from RPi import GPIO # pylint: disable=import-error def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -27,34 +28,24 @@ def prepare_gpio(event): def setup_output(port): """Set up a GPIO as output.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.setup(port, GPIO.OUT) def setup_input(port, pull_mode): """Set up a GPIO as input.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.setup(port, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP) def write_output(port, value): """Write a value to a GPIO.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.output(port, value) def read_input(port): """Read a value from a GPIO.""" - from RPi import GPIO # pylint: disable=import-error - return GPIO.input(port) def edge_detect(port, event_callback, bounce): """Add detection for RISING and FALLING events.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.add_event_detect(port, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py index 4acbed9a0facee..3e38da47eedd8f 100644 --- a/homeassistant/components/rpi_gpio/binary_sensor.py +++ b/homeassistant/components/rpi_gpio/binary_sensor.py @@ -4,7 +4,7 @@ import voluptuous as vol from homeassistant.components import rpi_gpio -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/rpi_gpio/cover.py b/homeassistant/components/rpi_gpio/cover.py index 83cc497324daf4..648171b97384c6 100644 --- a/homeassistant/components/rpi_gpio/cover.py +++ b/homeassistant/components/rpi_gpio/cover.py @@ -4,9 +4,9 @@ import voluptuous as vol -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME from homeassistant.components import rpi_gpio +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice +from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) From e54f5102aaab112d4b89fc12e34c8393cfd85d6d Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 14:58:23 +0200 Subject: [PATCH 0947/3953] Move imports in ifttt component (#27792) --- homeassistant/components/ifttt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index bed0cb45b1d4d8..05d773e9fd687a 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -2,6 +2,7 @@ import json import logging +import pyfttt import requests import voluptuous as vol @@ -69,7 +70,6 @@ def trigger_service(call): target_keys[target] = api_keys[target] try: - import pyfttt for target, key in target_keys.items(): res = pyfttt.send_event(key, event, value1, value2, value3) From 35e0acf0a531e0a4abed047de190320c708a340f Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 14:58:56 +0200 Subject: [PATCH 0948/3953] Move imports in keyboard component (#27791) --- homeassistant/components/keyboard/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/keyboard/__init__.py b/homeassistant/components/keyboard/__init__.py index 39725eec86bfb6..0c5acf5b593ea4 100644 --- a/homeassistant/components/keyboard/__init__.py +++ b/homeassistant/components/keyboard/__init__.py @@ -1,4 +1,5 @@ """Support to emulate keyboard presses on host machine.""" +from pykeyboard import PyKeyboard # pylint: disable=import-error import voluptuous as vol from homeassistant.const import ( @@ -17,9 +18,8 @@ def setup(hass, config): """Listen for keyboard events.""" - import pykeyboard # pylint: disable=import-error - keyboard = pykeyboard.PyKeyboard() + keyboard = PyKeyboard() keyboard.special_key_assignment() hass.services.register( From 9f7138452470b50f2da801dbb85ad1b8757e4c6b Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 14:59:36 +0200 Subject: [PATCH 0949/3953] Move imports in linux_battery component (#27789) --- homeassistant/components/linux_battery/sensor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index 9256c3ad18dc47..bc02affdaed50c 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -2,6 +2,7 @@ import logging import os +from batinfo import Batteries import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -72,15 +73,12 @@ class LinuxBatterySensor(Entity): def __init__(self, name, battery_id, system): """Initialize the battery sensor.""" - import batinfo - - self._battery = batinfo.Batteries() + self._battery = Batteries() self._name = name self._battery_stat = None self._battery_id = battery_id - 1 self._system = system - self._unit_of_measurement = "%" @property def name(self): @@ -100,7 +98,7 @@ def state(self): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return self._unit_of_measurement + return "%" @property def device_state_attributes(self): From 4efa6689e493760e8b96b3fe8a5f650e50506f7f Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 15:00:00 +0200 Subject: [PATCH 0950/3953] Move imports in ampio component (#27788) --- homeassistant/components/ampio/air_quality.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ampio/air_quality.py b/homeassistant/components/ampio/air_quality.py index e63f59839a80fd..c925909a9a81bd 100644 --- a/homeassistant/components/ampio/air_quality.py +++ b/homeassistant/components/ampio/air_quality.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from asmog import AmpioSmog import voluptuous as vol from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity @@ -23,7 +24,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Ampio Smog air quality platform.""" - from asmog import AmpioSmog name = config.get(CONF_NAME) station_id = config[CONF_STATION_ID] From dc72aa48da3e8d64e7aa27e12c0d61802f81121d Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 15:00:32 +0200 Subject: [PATCH 0951/3953] Move imports in liveboxplaytv component (#27790) --- homeassistant/components/liveboxplaytv/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py index c466d71c4c5fd3..996b4f33b5094e 100644 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ b/homeassistant/components/liveboxplaytv/media_player.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from liveboxplaytv import LiveboxPlayTv +import pyteleloisirs import requests import voluptuous as vol @@ -85,7 +87,6 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): def __init__(self, host, port, name): """Initialize the Livebox Play TV device.""" - from liveboxplaytv import LiveboxPlayTv self._client = LiveboxPlayTv(host, port) # Assume that the appliance is not muted @@ -103,7 +104,6 @@ def __init__(self, host, port, name): async def async_update(self): """Retrieve the latest data.""" - import pyteleloisirs try: self._state = self.refresh_state() From 88a78a4a18a8b592b63f5da39d9ae09f769cca28 Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 15:01:09 +0200 Subject: [PATCH 0952/3953] Move imports in amcrest component (#27787) --- homeassistant/components/amcrest/camera.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index f75a5adbe9cd0a..e9e1e2b5f84856 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -2,19 +2,20 @@ import asyncio from datetime import timedelta import logging -from urllib3.exceptions import HTTPError from amcrest import AmcrestError +from haffmpeg.camera import CameraMjpeg +from urllib3.exceptions import HTTPError import voluptuous as vol from homeassistant.components.camera import ( - Camera, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, SUPPORT_STREAM, + Camera, ) from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream, async_aiohttp_proxy_web, @@ -159,7 +160,6 @@ async def handle_async_mjpeg_stream(self, request): return await async_aiohttp_proxy_web(self.hass, request, stream_coro) # streaming via ffmpeg - from haffmpeg.camera import CameraMjpeg streaming_url = self._rtsp_url stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) From 9dc0c05ee0fc8d87c0f3a72366dc08bd98171097 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 15:01:50 +0200 Subject: [PATCH 0953/3953] Move imports in imap + imap_email_content component (#27793) --- homeassistant/components/imap/sensor.py | 15 +++++---------- .../components/imap_email_content/sensor.py | 5 +---- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index 0ae79d34cf0149..a10fefa1b163b6 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -2,6 +2,7 @@ import asyncio import logging +from aioimaplib import IMAP4_SSL, AioImapException import async_timeout import voluptuous as vol @@ -107,24 +108,20 @@ def should_poll(self): async def connection(self): """Return a connection to the server, establishing it if necessary.""" - import aioimaplib - if self._connection is None: try: - self._connection = aioimaplib.IMAP4_SSL(self._server, self._port) + self._connection = IMAP4_SSL(self._server, self._port) await self._connection.wait_hello_from_server() await self._connection.login(self._user, self._password) await self._connection.select(self._folder) self._does_push = self._connection.has_capability("IDLE") - except (aioimaplib.AioImapException, asyncio.TimeoutError): + except (AioImapException, asyncio.TimeoutError): self._connection = None return self._connection async def idle_loop(self): """Wait for data pushed from server.""" - import aioimaplib - while True: try: if await self.connection(): @@ -138,17 +135,15 @@ async def idle_loop(self): await idle else: await self.async_update_ha_state() - except (aioimaplib.AioImapException, asyncio.TimeoutError): + except (AioImapException, asyncio.TimeoutError): self.disconnected() async def async_update(self): """Periodic polling of state.""" - import aioimaplib - try: if await self.connection(): await self.refresh_email_count() - except (aioimaplib.AioImapException, asyncio.TimeoutError): + except (AioImapException, asyncio.TimeoutError): self.disconnected() async def refresh_email_count(self): diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index c5171cde646f14..62dceae0dadcb5 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -4,6 +4,7 @@ import email from collections import deque +import imaplib import voluptuous as vol from homeassistant.helpers.entity import Entity @@ -88,8 +89,6 @@ def __init__(self, user, password, server, port, folder): def connect(self): """Login and setup the connection.""" - import imaplib - try: self.connection = imaplib.IMAP4_SSL(self._server, self._port) self.connection.login(self._user, self._password) @@ -110,8 +109,6 @@ def _fetch_message(self, message_uid): def read_next(self): """Read the next email from the email server.""" - import imaplib - try: self.connection.select(self._folder, readonly=True) From 8350e1246a48ed3b2297baacf8a3568cd34741c5 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 15:03:05 +0200 Subject: [PATCH 0954/3953] Move imports in netgear_lte component (#27777) --- homeassistant/components/netgear_lte/__init__.py | 4 +--- homeassistant/components/netgear_lte/notify.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 2514b37657fb05..4758a13c391291 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -5,6 +5,7 @@ import aiohttp import attr +import eternalegypt import voluptuous as vol from homeassistant.const import ( @@ -139,7 +140,6 @@ class ModemData: async def async_update(self): """Call the API to update the data.""" - import eternalegypt try: self.data = await self.modem.information() @@ -264,7 +264,6 @@ async def service_handler(service): async def _setup_lte(hass, lte_config): """Set up a Netgear LTE modem.""" - import eternalegypt host = lte_config[CONF_HOST] password = lte_config[CONF_PASSWORD] @@ -322,7 +321,6 @@ async def _update(now): async def _retry_login(hass, modem_data, password): """Sleep and retry setup.""" - import eternalegypt _LOGGER.warning("Could not connect to %s. Will keep trying", modem_data.host) diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index 4f13662519d24c..9700ee3c715396 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -2,6 +2,7 @@ import logging import attr +import eternalegypt from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService, DOMAIN @@ -27,7 +28,6 @@ class NetgearNotifyService(BaseNotificationService): async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" - import eternalegypt modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config) if not modem_data: From ab598da4bae5010618def19176fb14c6b80bc8c3 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 15:03:50 +0200 Subject: [PATCH 0955/3953] Move imports in nest component (#27778) --- homeassistant/components/nest/__init__.py | 9 ++------- homeassistant/components/nest/climate.py | 4 ++-- homeassistant/components/nest/local_auth.py | 5 ++--- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index cf1ba36aa89320..32bbd009417f13 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -4,6 +4,8 @@ from datetime import datetime, timedelta import threading +from nest import Nest +from nest.nest import AuthorizationError, APIError import voluptuous as vol from homeassistant import config_entries @@ -142,7 +144,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Nest from a config entry.""" - from nest import Nest nest = Nest(access_token=entry.data["tokens"]["access_token"]) @@ -286,8 +287,6 @@ def __init__(self, hass, conf, nest): def initialize(self): """Initialize Nest.""" - from nest.nest import AuthorizationError, APIError - try: # Do not optimize next statement, it is here for initialize # persistence Nest API connection. @@ -302,8 +301,6 @@ def initialize(self): def structures(self): """Generate a list of structures.""" - from nest.nest import AuthorizationError, APIError - try: for structure in self.nest.structures: if structure.name not in self.local_structure: @@ -332,8 +329,6 @@ def cameras(self): def _devices(self, device_type): """Generate a list of Nest devices.""" - from nest.nest import AuthorizationError, APIError - try: for structure in self.nest.structures: if structure.name not in self.local_structure: diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index eec7108cdeabd9..795ce5c80e97cb 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -1,6 +1,7 @@ """Support for Nest thermostats.""" import logging +from nest.nest import APIError import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice @@ -232,7 +233,6 @@ def target_temperature_high(self): def set_temperature(self, **kwargs): """Set new target temperature.""" - import nest temp = None target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) @@ -247,7 +247,7 @@ def set_temperature(self, **kwargs): try: if temp is not None: self.device.target = temp - except nest.nest.APIError as api_error: + except APIError as api_error: _LOGGER.error("An error occurred while setting temperature: %s", api_error) # restore target temperature self.schedule_update_ha_state(True) diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index 51d826c242fbd0..38d1827326da37 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -2,6 +2,8 @@ import asyncio from functools import partial +from nest.nest import NestAuth, AUTHORIZE_URL, AuthorizationError + from homeassistant.core import callback from . import config_flow from .const import DOMAIN @@ -21,14 +23,11 @@ def initialize(hass, client_id, client_secret): async def generate_auth_url(client_id, flow_id): """Generate an authorize url.""" - from nest.nest import AUTHORIZE_URL - return AUTHORIZE_URL.format(client_id, flow_id) async def resolve_auth_code(hass, client_id, client_secret, code): """Resolve an authorization code.""" - from nest.nest import NestAuth, AuthorizationError result = asyncio.Future() auth = NestAuth( From 62fcea2a8d72cb78c8a984d65d9e76f9e3c27443 Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 15:04:41 +0200 Subject: [PATCH 0956/3953] moved imports to top level (#27781) --- homeassistant/components/airvisual/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 20e5196c0f1fdd..888d6ae6ec9b2c 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -1,7 +1,9 @@ """Support for AirVisual air quality sensors.""" -from logging import getLogger from datetime import timedelta +from logging import getLogger +from pyairvisual import Client +from pyairvisual.errors import AirVisualError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -14,8 +16,8 @@ CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL, - CONF_STATE, CONF_SHOW_ON_MAP, + CONF_STATE, ) from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.entity import Entity @@ -97,7 +99,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Configure the platform and add the sensors.""" - from pyairvisual import Client city = config.get(CONF_CITY) state = config.get(CONF_STATE) @@ -249,7 +250,6 @@ def __init__(self, client, **kwargs): async def _async_update(self): """Update AirVisual data.""" - from pyairvisual.errors import AirVisualError try: if self.city and self.state and self.country: From 12a8e7520e9785d72d31e443efb3d432fae29ef6 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 15:05:14 +0200 Subject: [PATCH 0957/3953] Move imports in netgear component (#27776) --- homeassistant/components/netgear/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index b52e446ba5d306..2e20f6423a5aac 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -1,6 +1,7 @@ """Support for Netgear routers.""" import logging +from pynetgear import Netgear import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -71,14 +72,13 @@ def __init__( accesspoints, ): """Initialize the scanner.""" - import pynetgear self.tracked_devices = devices self.excluded_devices = excluded_devices self.tracked_accesspoints = accesspoints self.last_results = [] - self._api = pynetgear.Netgear(password, host, username, port, ssl) + self._api = Netgear(password, host, username, port, ssl) _LOGGER.info("Logging in") From 2c535c92bdbb76b138ec60336f280cb5f3b4f89a Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 15:05:45 +0200 Subject: [PATCH 0958/3953] moved imports to top level (#27784) --- homeassistant/components/alarmdotcom/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alarmdotcom/alarm_control_panel.py b/homeassistant/components/alarmdotcom/alarm_control_panel.py index f80e8d6eb1ec23..07d69960e0b738 100644 --- a/homeassistant/components/alarmdotcom/alarm_control_panel.py +++ b/homeassistant/components/alarmdotcom/alarm_control_panel.py @@ -2,6 +2,7 @@ import logging import re +from pyalarmdotcom import Alarmdotcom import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm @@ -49,7 +50,6 @@ class AlarmDotCom(alarm.AlarmControlPanel): def __init__(self, hass, name, code, username, password): """Initialize the Alarm.com status.""" - from pyalarmdotcom import Alarmdotcom _LOGGER.debug("Setting up Alarm.com...") self._hass = hass From 28cef89e038381570c75e5d5fd66821fc16b76af Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 17 Oct 2019 06:33:20 -0700 Subject: [PATCH 0959/3953] Generate ADB key for Android TV integration (#27344) * Generate ADB key for Android TV integration * Remove 'do_nothing' function * Remove 'return True' * Re-add 2 'return True' lines --- .../components/androidtv/manifest.json | 4 +- .../components/androidtv/media_player.py | 36 +++++++--- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/androidtv/patchers.py | 14 +++- .../components/androidtv/test_media_player.py | 69 +++++++++++++------ 6 files changed, 91 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index e84ed35c7636f7..9ec993b9f914e9 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.4", - "androidtv==0.0.30" + "adb-shell==0.0.7", + "androidtv==0.0.32" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index fcf4950f5e2903..62ae93f96e4b84 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -1,8 +1,10 @@ """Support for functionality to interact with Android TV / Fire TV devices.""" import functools import logging +import os import voluptuous as vol +from adb_shell.auth.keygen import keygen from adb_shell.exceptions import ( InvalidChecksumError, InvalidCommandError, @@ -40,6 +42,7 @@ ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.storage import STORAGE_DIR ANDROIDTV_DOMAIN = "androidtv" @@ -133,27 +136,39 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if CONF_ADB_SERVER_IP not in config: # Use "adb_shell" (Python ADB implementation) - adb_log = "using Python ADB implementation " + ( - f"with adbkey='{config[CONF_ADBKEY]}'" - if CONF_ADBKEY in config - else "without adbkey authentication" - ) - if CONF_ADBKEY in config: + if CONF_ADBKEY not in config: + # Generate ADB key files (if they don't exist) + adbkey = hass.config.path(STORAGE_DIR, "androidtv_adbkey") + if not os.path.isfile(adbkey): + keygen(adbkey) + + adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" + aftv = setup( host, - config[CONF_ADBKEY], + adbkey, device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], + auth_timeout_s=10.0, ) else: + adb_log = ( + f"using Python ADB implementation with adbkey='{config[CONF_ADBKEY]}'" + ) + aftv = setup( host, + config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], + auth_timeout_s=10.0, ) + else: # Use "pure-python-adb" (communicate with ADB server) + adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" + aftv = setup( host, adb_server_ip=config[CONF_ADB_SERVER_IP], @@ -161,7 +176,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) - adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" if not aftv.available: # Determine the name that will be used for the device in the log @@ -257,7 +271,7 @@ def _adb_exception_catcher(self, *args, **kwargs): "establishing attempt in the next update. Error: %s", err, ) - self.aftv.adb.close() + self.aftv.adb_close() self._available = False # pylint: disable=protected-access return None @@ -429,7 +443,7 @@ def update(self): # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.connect(always_log_errors=False) + self._available = self.aftv.adb_connect(always_log_errors=False) # To be safe, wait until the next update to run ADB commands if # using the Python ADB implementation. @@ -508,7 +522,7 @@ def update(self): # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.connect(always_log_errors=False) + self._available = self.aftv.adb_connect(always_log_errors=False) # To be safe, wait until the next update to run ADB commands if # using the Python ADB implementation. diff --git a/requirements_all.txt b/requirements_all.txt index c8d66f91468403..6a70451cc127d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.4 +adb-shell==0.0.7 # homeassistant.components.adguard adguardhome==0.2.1 @@ -203,7 +203,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.30 +androidtv==0.0.32 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 967943894fb6a2..ece529ef6e8e63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -49,7 +49,7 @@ YesssSMS==0.4.1 abodepy==0.16.5 # homeassistant.components.androidtv -adb-shell==0.0.4 +adb-shell==0.0.7 # homeassistant.components.adguard adguardhome==0.2.1 @@ -98,7 +98,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.30 +androidtv==0.0.32 # homeassistant.components.apns apns2==0.3.0 diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 73aa52259890ef..5fc6bc754fa1bd 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,7 +1,7 @@ """Define patches used for androidtv tests.""" from socket import error as socket_error -from unittest.mock import patch +from unittest.mock import mock_open, patch class AdbDeviceFake: @@ -128,3 +128,15 @@ def shell_fail_server(self, cmd): PATCH_ADB_DEVICE = patch("androidtv.adb_manager.AdbDevice", AdbDeviceFake) +PATCH_ANDROIDTV_OPEN = patch("androidtv.adb_manager.open", mock_open()) +PATCH_KEYGEN = patch("homeassistant.components.androidtv.media_player.keygen") +PATCH_SIGNER = patch("androidtv.adb_manager.PythonRSASigner") + + +def isfile(filepath): + """Mock `os.path.isfile`.""" + return filepath.endswith("adbkey") + + +PATCH_ISFILE = patch("os.path.isfile", isfile) +PATCH_ACCESS = patch("os.access", return_value=True) diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index feffc70d84148f..85f562a3500288 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -5,6 +5,7 @@ from homeassistant.components.androidtv.media_player import ( ANDROIDTV_DOMAIN, CONF_ADB_SERVER_IP, + CONF_ADBKEY, ) from homeassistant.components.media_player.const import DOMAIN from homeassistant.const import ( @@ -61,14 +62,8 @@ } -async def _test_reconnect(hass, caplog, config): - """Test that the error and reconnection attempts are logged correctly. - - "Handles device/service unavailable. Log a warning once when - unavailable, log once when reconnected." - - https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html - """ +def _setup(hass, config): + """Perform common setup tasks for the tests.""" if CONF_ADB_SERVER_IP not in config[DOMAIN]: patch_key = "python" else: @@ -79,10 +74,26 @@ async def _test_reconnect(hass, caplog, config): else: entity_id = "media_player.fire_tv" + return patch_key, entity_id + + +async def _test_reconnect(hass, caplog, config): + """Test that the error and reconnection attempts are logged correctly. + + "Handles device/service unavailable. Log a warning once when + unavailable, log once when reconnected." + + https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html + """ + patch_key, entity_id = _setup(hass, config) + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ patch_key - ], patchers.patch_shell("")[patch_key]: + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -93,7 +104,7 @@ async def _test_reconnect(hass, caplog, config): with patchers.patch_connect(False)[patch_key], patchers.patch_shell(error=True)[ patch_key - ]: + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: for _ in range(5): await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -105,7 +116,9 @@ async def _test_reconnect(hass, caplog, config): assert caplog.record_tuples[1][1] == logging.WARNING caplog.set_level(logging.DEBUG) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell("1")[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell("1")[ + patch_key + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: # Update 1 will reconnect await hass.helpers.entity_component.async_update_entity(entity_id) @@ -143,19 +156,13 @@ async def _test_adb_shell_returns_none(hass, config): The state should be `None` and the device should be unavailable. """ - if CONF_ADB_SERVER_IP not in config[DOMAIN]: - patch_key = "python" - else: - patch_key = "server" - - if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": - entity_id = "media_player.android_tv" - else: - entity_id = "media_player.fire_tv" + patch_key, entity_id = _setup(hass, config) with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ patch_key - ], patchers.patch_shell("")[patch_key]: + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: assert await async_setup_component(hass, DOMAIN, config) await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -164,7 +171,7 @@ async def _test_adb_shell_returns_none(hass, config): with patchers.patch_shell(None)[patch_key], patchers.patch_shell(error=True)[ patch_key - ]: + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -251,3 +258,21 @@ async def test_adb_shell_returns_none_firetv_adb_server(hass): """ assert await _test_adb_shell_returns_none(hass, CONFIG_FIRETV_ADB_SERVER) + + +async def test_setup_with_adbkey(hass): + """Test that setup succeeds when using an ADB key.""" + config = CONFIG_ANDROIDTV_PYTHON_ADB.copy() + config[DOMAIN][CONF_ADBKEY] = hass.config.path("user_provided_adbkey") + patch_key, entity_id = _setup(hass, config) + + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, patchers.PATCH_ISFILE, patchers.PATCH_ACCESS: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF From 136df743e3da6ed07fbce3af955d290b46ff780b Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 15:38:58 +0200 Subject: [PATCH 0960/3953] moved imports to top level (#27782) --- homeassistant/components/aladdin_connect/cover.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index b3da4fb4cbc3f0..4cfcd5403ddd5a 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,21 +1,22 @@ """Platform for the Aladdin Connect cover component.""" import logging +from aladdin_connect import AladdinConnectClient import voluptuous as vol from homeassistant.components.cover import ( - CoverDevice, PLATFORM_SCHEMA, - SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_OPEN, + CoverDevice, ) from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, + CONF_USERNAME, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, STATE_OPEN, + STATE_OPENING, ) import homeassistant.helpers.config_validation as cv @@ -40,7 +41,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Aladdin Connect platform.""" - from aladdin_connect import AladdinConnectClient username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) From d52476333e935a35825149fbdf353d9fc739a8d4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 17 Oct 2019 17:06:33 +0200 Subject: [PATCH 0961/3953] 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 afb273331aada5..5bfd37fab36ae4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "dockerFile": "../Dockerfile.dev", "postCreateCommand": "mkdir -p config && pip3 install -e .", "appPort": 8123, - "runArgs": ["-e", "GIT_EDITOR=\"code --wait\""], + "runArgs": ["-e", "GIT_EDITOR=code --wait"], "extensions": [ "ms-python.python", "visualstudioexptteam.vscodeintellicode", From ba0107f9127f4c13dbbcf5ff5d23d69717bf60ab Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 17:07:36 +0200 Subject: [PATCH 0962/3953] Move imports in android_ip_webcam component (#27797) --- .../components/android_ip_webcam/__init__.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index 33362bd37ccde6..1f9df527c28203 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -1,34 +1,35 @@ """Support for Android IP Webcam.""" import asyncio -import logging from datetime import timedelta +import logging +from pydroid_ipcam import PyDroidIPCam import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components.mjpeg.camera import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL from homeassistant.const import ( - CONF_NAME, CONF_HOST, - CONF_PORT, - CONF_USERNAME, + CONF_NAME, CONF_PASSWORD, + CONF_PLATFORM, + CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, - CONF_SCAN_INTERVAL, - CONF_PLATFORM, + CONF_USERNAME, ) -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.core import callback from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -from homeassistant.components.mjpeg.camera import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL _LOGGER = logging.getLogger(__name__) @@ -187,7 +188,6 @@ async def async_setup(hass, config): """Set up the IP Webcam component.""" - from pydroid_ipcam import PyDroidIPCam webcams = hass.data[DATA_IP_WEBCAM] = {} websession = async_get_clientsession(hass) From e992cfb45c65dd0a8f5d6dcd2c1021604a0966f9 Mon Sep 17 00:00:00 2001 From: tombbo <53979375+tombbo@users.noreply.github.com> Date: Thu, 17 Oct 2019 21:07:09 +0200 Subject: [PATCH 0963/3953] Add on_off_inverted to KNX climate (#25900) * Added a new configuration boolean parameter on_off_inverted to KNX Climate component. * Remove unexpected spaces around equals. * Parameter name changed to on_off_invert and modified to new version of XKNX library. * Dict[key] for required config keys and keys with default config schema values. --- homeassistant/components/knx/climate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 07aac11b972ff4..014cd8d9ba1360 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -44,6 +44,7 @@ CONF_OPERATION_MODES = "operation_modes" CONF_ON_OFF_ADDRESS = "on_off_address" CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address" +CONF_ON_OFF_INVERT = "on_off_invert" CONF_MIN_TEMP = "min_temp" CONF_MAX_TEMP = "max_temp" @@ -51,6 +52,7 @@ DEFAULT_SETPOINT_SHIFT_STEP = 0.5 DEFAULT_SETPOINT_SHIFT_MAX = 6 DEFAULT_SETPOINT_SHIFT_MIN = -6 +DEFAULT_ON_OFF_INVERT = False # Map KNX operation modes to HA modes. This list might not be full. OPERATION_MODES = { # Map DPT 201.105 HVAC control modes @@ -102,6 +104,7 @@ vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string, vol.Optional(CONF_ON_OFF_ADDRESS): cv.string, vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT): cv.boolean, vol.Optional(CONF_OPERATION_MODES): vol.All( cv.ensure_list, [vol.In(OPERATION_MODES)] ), @@ -182,6 +185,7 @@ def async_add_entities_config(hass, config, async_add_entities): min_temp=config.get(CONF_MIN_TEMP), max_temp=config.get(CONF_MAX_TEMP), mode=climate_mode, + on_off_invert=config[CONF_ON_OFF_INVERT], ) hass.data[DATA_KNX].xknx.devices.add(climate) From 7637ceb880614558c3f19850fb7be2779c166688 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 21:17:23 +0200 Subject: [PATCH 0964/3953] Move imports in html5 component (#27473) * Move imports in html5 component * Fix tests 1 * Fix tests 2 --- homeassistant/components/html5/notify.py | 8 +++----- tests/components/html5/test_notify.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index a802609ac85c22..18b7ff27ab4b17 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -9,6 +9,9 @@ import uuid from aiohttp.hdrs import AUTHORIZATION +import jwt +from pywebpush import WebPusher +from py_vapid import Vapid import voluptuous as vol from voluptuous.humanize import humanize_error @@ -311,7 +314,6 @@ def __init__(self, registrations): def decode_jwt(self, token): """Find the registration that signed this JWT and return it.""" - import jwt # 1. Check claims w/o verifying to see if a target is in there. # 2. If target in claims, attempt to verify against the given name. @@ -335,7 +337,6 @@ def decode_jwt(self, token): # https://auth0.com/docs/quickstart/backend/python def check_authorization_header(self, request): """Check the authorization header.""" - import jwt auth = request.headers.get(AUTHORIZATION, None) if not auth: @@ -491,7 +492,6 @@ def send_message(self, message="", **kwargs): def _push_message(self, payload, **kwargs): """Send the message.""" - from pywebpush import WebPusher timestamp = int(time.time()) ttl = int(kwargs.get(ATTR_TTL, DEFAULT_TTL)) @@ -550,7 +550,6 @@ def _push_message(self, payload, **kwargs): def add_jwt(timestamp, target, tag, jwt_secret): """Create JWT json to put into payload.""" - import jwt jwt_exp = datetime.fromtimestamp(timestamp) + timedelta(days=JWT_VALID_DAYS) jwt_claims = { @@ -565,7 +564,6 @@ def add_jwt(timestamp, target, tag, jwt_secret): def create_vapid_headers(vapid_email, subscription_info, vapid_private_key): """Create encrypted headers to send to WebPusher.""" - from py_vapid import Vapid if vapid_email and vapid_private_key and ATTR_ENDPOINT in subscription_info: url = urlparse(subscription_info.get(ATTR_ENDPOINT)) diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index d9246e685dc2a5..481d7a010c94bf 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -87,7 +87,7 @@ def test_get_service_with_no_json(self): assert service is not None - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_dismissing_message(self, mock_wp): """Test dismissing message.""" hass = MagicMock() @@ -115,7 +115,7 @@ def test_dismissing_message(self, mock_wp): assert payload["dismiss"] is True assert payload["tag"] == "test" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_sending_message(self, mock_wp): """Test sending message.""" hass = MagicMock() @@ -145,7 +145,7 @@ def test_sending_message(self, mock_wp): assert payload["body"] == "Hello" assert payload["icon"] == "beer.png" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_gcm_key_include(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -176,7 +176,7 @@ def test_gcm_key_include(self, mock_wp): assert mock_wp.mock_calls[1][2]["gcm_key"] is not None assert mock_wp.mock_calls[4][2]["gcm_key"] is None - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_key_include(self, mock_wp): """Test if the FCM header is included.""" hass = MagicMock() @@ -201,7 +201,7 @@ def test_fcm_key_include(self, mock_wp): # Get the keys passed to the WebPusher's send method assert mock_wp.mock_calls[1][2]["headers"]["Authorization"] is not None - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_send_with_unknown_priority(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -226,7 +226,7 @@ def test_fcm_send_with_unknown_priority(self, mock_wp): # Get the keys passed to the WebPusher's send method assert mock_wp.mock_calls[1][2]["headers"]["priority"] == "normal" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_no_targets(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -251,7 +251,7 @@ def test_fcm_no_targets(self, mock_wp): # Get the keys passed to the WebPusher's send method assert mock_wp.mock_calls[1][2]["headers"]["priority"] == "normal" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_additional_data(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -475,7 +475,7 @@ async def test_callback_view_with_jwt(hass, hass_client): registrations = {"device": SUBSCRIPTION_1} client = await mock_client(hass, hass_client, registrations) - with patch("pywebpush.WebPusher") as mock_wp: + with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp: await hass.services.async_call( "notify", "notify", @@ -511,7 +511,7 @@ async def test_send_fcm_without_targets(hass, hass_client): """Test that the notification is send with FCM without targets.""" registrations = {"device": SUBSCRIPTION_5} await mock_client(hass, hass_client, registrations) - with patch("pywebpush.WebPusher") as mock_wp: + with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp: await hass.services.async_call( "notify", "notify", From 1a5b4c105ab2f18abecfffdc9ae4ecd41d4cabbb Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 18 Oct 2019 11:04:27 +1100 Subject: [PATCH 0965/3953] Move imports in mqtt component (#27835) * move imports to top-level in mqtt server * move imports to top-level in mqtt configflow * move imports to top-level in mqtt init * move imports to top-level in mqtt vacuum * move imports to top-level in mqtt light --- homeassistant/components/mqtt/__init__.py | 47 ++++-------------- homeassistant/components/mqtt/config_flow.py | 3 +- homeassistant/components/mqtt/const.py | 2 + .../components/mqtt/light/__init__.py | 34 ++++--------- homeassistant/components/mqtt/light/schema.py | 12 +++++ .../components/mqtt/light/schema_basic.py | 2 +- .../components/mqtt/light/schema_json.py | 2 +- .../components/mqtt/light/schema_template.py | 2 +- homeassistant/components/mqtt/models.py | 20 ++++++++ homeassistant/components/mqtt/server.py | 10 ++-- homeassistant/components/mqtt/subscription.py | 3 +- .../components/mqtt/vacuum/__init__.py | 48 ++----------------- .../components/mqtt/vacuum/schema.py | 31 ++++++++++++ .../components/mqtt/vacuum/schema_legacy.py | 2 +- .../components/mqtt/vacuum/schema_state.py | 2 +- tests/common.py | 3 +- tests/components/mqtt/test_legacy_vacuum.py | 26 +++++----- tests/components/mqtt/test_server.py | 16 +++++-- tests/components/mqtt/test_state_vacuum.py | 9 ++-- 19 files changed, 130 insertions(+), 144 deletions(-) create mode 100644 homeassistant/components/mqtt/light/schema.py create mode 100644 homeassistant/components/mqtt/models.py create mode 100644 homeassistant/components/mqtt/vacuum/schema.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e3605cb866484c..119c9b520d7193 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,5 +1,6 @@ """Support for MQTT message handling.""" import asyncio +import sys from functools import partial, wraps import inspect from itertools import groupby @@ -15,6 +16,8 @@ import attr import requests.certs import voluptuous as vol +import paho.mqtt.client as mqtt +from paho.mqtt.matcher import MQTTMatcher from homeassistant import config_entries from homeassistant.components import websocket_api @@ -36,6 +39,7 @@ ConfigEntryNotReady, ) from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.loader import bind_hass @@ -50,7 +54,12 @@ DEFAULT_DISCOVERY, CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH, + PROTOCOL_311, + DEFAULT_QOS, ) +from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .models import PublishPayloadType, Message, MessageCallbackType +from .subscription import async_subscribe_topics, async_unsubscribe_topics _LOGGER = logging.getLogger(__name__) @@ -95,11 +104,9 @@ CONF_DEPRECATED_VIA_HUB = "via_hub" PROTOCOL_31 = "3.1" -PROTOCOL_311 = "3.1.1" DEFAULT_PORT = 1883 DEFAULT_KEEPALIVE = 60 -DEFAULT_QOS = 0 DEFAULT_RETAIN = False DEFAULT_PROTOCOL = PROTOCOL_311 DEFAULT_DISCOVERY_PREFIX = "homeassistant" @@ -329,23 +336,9 @@ def embedded_broker_deprecated(value): # pylint: disable=invalid-name -PublishPayloadType = Union[str, bytes, int, float, None] SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None -@attr.s(slots=True, frozen=True) -class Message: - """MQTT Message.""" - - topic = attr.ib(type=str) - payload = attr.ib(type=PublishPayloadType) - qos = attr.ib(type=int) - retain = attr.ib(type=bool) - - -MessageCallbackType = Callable[[Message], None] - - def _build_publish_data(topic: Any, qos: int, retain: bool) -> ServiceDataType: """Build the arguments for the publish service without the payload.""" data = {ATTR_TOPIC: topic} @@ -629,8 +622,6 @@ async def async_setup_entry(hass, entry): elif conf_tls_version == "1.0": tls_version = ssl.PROTOCOL_TLSv1 else: - import sys - # Python3.6 supports automatic negotiation of highest TLS version if sys.hexversion >= 0x03060000: tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member @@ -735,8 +726,6 @@ def __init__( tls_version: Optional[int], ) -> None: """Initialize Home Assistant MQTT client.""" - import paho.mqtt.client as mqtt - self.hass = hass self.broker = broker self.port = port @@ -808,8 +797,6 @@ async def async_connect(self) -> str: return CONNECTION_FAILED_RECOVERABLE if result != 0: - import paho.mqtt.client as mqtt - _LOGGER.error("Failed to connect: %s", mqtt.error_string(result)) return CONNECTION_FAILED @@ -891,8 +878,6 @@ def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: Resubscribe to all topics we were subscribed to and publish birth message. """ - import paho.mqtt.client as mqtt - if result_code != mqtt.CONNACK_ACCEPTED: _LOGGER.error( "Unable to connect to the MQTT broker: %s", @@ -984,8 +969,6 @@ def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: def _raise_on_error(result_code: int) -> None: """Raise error if error result.""" if result_code != 0: - import paho.mqtt.client as mqtt - raise HomeAssistantError( "Error talking to MQTT: {}".format(mqtt.error_string(result_code)) ) @@ -993,8 +976,6 @@ def _raise_on_error(result_code: int) -> None: def _match_topic(subscription: str, topic: str) -> bool: """Test if topic matches subscription.""" - from paho.mqtt.matcher import MQTTMatcher - matcher = MQTTMatcher() matcher[subscription] = True try: @@ -1028,8 +1009,6 @@ async def attributes_discovery_update(self, config: dict): async def _attributes_subscribe_topics(self): """(Re)Subscribe to topics.""" - from .subscription import async_subscribe_topics - attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE) if attr_tpl is not None: attr_tpl.hass = self.hass @@ -1065,8 +1044,6 @@ def attributes_message_received(msg: Message) -> None: async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" - from .subscription import async_unsubscribe_topics - self._attributes_sub_state = await async_unsubscribe_topics( self.hass, self._attributes_sub_state ) @@ -1102,7 +1079,6 @@ async def availability_discovery_update(self, config: dict): async def _availability_subscribe_topics(self): """(Re)Subscribe to topics.""" - from .subscription import async_subscribe_topics @callback def availability_message_received(msg: Message) -> None: @@ -1128,8 +1104,6 @@ def availability_message_received(msg: Message) -> None: async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" - from .subscription import async_unsubscribe_topics - self._availability_sub_state = await async_unsubscribe_topics( self.hass, self._availability_sub_state ) @@ -1154,9 +1128,6 @@ async def async_added_to_hass(self) -> None: """Subscribe to discovery updates.""" await super().async_added_to_hass() - from homeassistant.helpers.dispatcher import async_dispatcher_connect - from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash - @callback def discovery_callback(payload): """Handle discovery update.""" diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index d3c6ee819b5f5f..a8a378e723c098 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3,6 +3,7 @@ import queue import voluptuous as vol +import paho.mqtt.client as mqtt from homeassistant import config_entries from homeassistant.const import ( @@ -125,8 +126,6 @@ async def async_step_hassio_confirm(self, user_input=None): def try_connection(broker, port, username, password, protocol="3.1"): """Test if we can connect to an MQTT broker.""" - import paho.mqtt.client as mqtt - if protocol == "3.1": proto = mqtt.MQTTv31 else: diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index b365ee9d33e070..3234bebbfc1299 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -5,3 +5,5 @@ ATTR_DISCOVERY_HASH = "discovery_hash" CONF_STATE_TOPIC = "state_topic" +PROTOCOL_311 = "3.1.1" +DEFAULT_QOS = 0 diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 688cef03467cb0..95a850fb9e864c 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -16,34 +16,24 @@ ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA +from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic +from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json +from .schema_template import PLATFORM_SCHEMA_TEMPLATE, async_setup_entity_template _LOGGER = logging.getLogger(__name__) -CONF_SCHEMA = "schema" - def validate_mqtt_light(value): """Validate MQTT light schema.""" - from . import schema_basic - from . import schema_json - from . import schema_template - schemas = { - "basic": schema_basic.PLATFORM_SCHEMA_BASIC, - "json": schema_json.PLATFORM_SCHEMA_JSON, - "template": schema_template.PLATFORM_SCHEMA_TEMPLATE, + "basic": PLATFORM_SCHEMA_BASIC, + "json": PLATFORM_SCHEMA_JSON, + "template": PLATFORM_SCHEMA_TEMPLATE, } return schemas[value[CONF_SCHEMA]](value) -MQTT_LIGHT_SCHEMA_SCHEMA = vol.Schema( - { - vol.Optional(CONF_SCHEMA, default="basic"): vol.All( - vol.Lower, vol.Any("basic", "json", "template") - ) - } -) - PLATFORM_SCHEMA = vol.All( MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_light ) @@ -81,14 +71,10 @@ async def _async_setup_entity( config, async_add_entities, config_entry=None, discovery_hash=None ): """Set up a MQTT Light.""" - from . import schema_basic - from . import schema_json - from . import schema_template - setup_entity = { - "basic": schema_basic.async_setup_entity_basic, - "json": schema_json.async_setup_entity_json, - "template": schema_template.async_setup_entity_template, + "basic": async_setup_entity_basic, + "json": async_setup_entity_json, + "template": async_setup_entity_template, } await setup_entity[config[CONF_SCHEMA]]( config, async_add_entities, config_entry, discovery_hash diff --git a/homeassistant/components/mqtt/light/schema.py b/homeassistant/components/mqtt/light/schema.py new file mode 100644 index 00000000000000..a7ab5e986a78de --- /dev/null +++ b/homeassistant/components/mqtt/light/schema.py @@ -0,0 +1,12 @@ +"""Shared schema code.""" +import voluptuous as vol + +CONF_SCHEMA = "schema" + +MQTT_LIGHT_SCHEMA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SCHEMA, default="basic"): vol.All( + vol.Lower, vol.Any("basic", "json", "template") + ) + } +) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 216762f9b2b8e1..829809dd9c31d7 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -56,7 +56,7 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -from . import MQTT_LIGHT_SCHEMA_SCHEMA +from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 1e8114a48e6b7c..c4de1edbc3c4fe 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -59,7 +59,7 @@ from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util -from . import MQTT_LIGHT_SCHEMA_SCHEMA +from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 410eff6143f673..c80ab2f95a7c26 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -49,7 +49,7 @@ import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity -from . import MQTT_LIGHT_SCHEMA_SCHEMA +from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py new file mode 100644 index 00000000000000..5f014aadd08153 --- /dev/null +++ b/homeassistant/components/mqtt/models.py @@ -0,0 +1,20 @@ +"""Modesl used by multiple MQTT modules.""" +from typing import Union, Callable + +import attr + +# pylint: disable=invalid-name +PublishPayloadType = Union[str, bytes, int, float, None] + + +@attr.s(slots=True, frozen=True) +class Message: + """MQTT Message.""" + + topic = attr.ib(type=str) + payload = attr.ib(type=PublishPayloadType) + qos = attr.ib(type=int) + retain = attr.ib(type=bool) + + +MessageCallbackType = Callable[[Message], None] diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index 2c70d18d7721ba..f5d369a75c799c 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -4,10 +4,14 @@ import tempfile import voluptuous as vol +from hbmqtt.broker import Broker, BrokerException +from passlib.apps import custom_app_context from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv +from .const import PROTOCOL_311 + _LOGGER = logging.getLogger(__name__) # None allows custom config to be created through generate_config @@ -33,8 +37,6 @@ def async_start(hass, password, server_config): This method is a coroutine. """ - from hbmqtt.broker import Broker, BrokerException - passwd = tempfile.NamedTemporaryFile() gen_server_config, client_config = generate_config(hass, passwd, password) @@ -63,8 +65,6 @@ def async_shutdown_mqtt_server(event): def generate_config(hass, passwd, password): """Generate a configuration based on current Home Assistant instance.""" - from . import PROTOCOL_311 - config = { "listeners": { "default": { @@ -83,8 +83,6 @@ def generate_config(hass, passwd, password): username = "homeassistant" # Encrypt with what hbmqtt uses to verify - from passlib.apps import custom_app_context - passwd.write( "homeassistant:{}\n".format(custom_app_context.encrypt(password)).encode( "utf-8" diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index d85399b5dcbe18..be48a769a234b3 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -8,7 +8,8 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass -from . import DEFAULT_QOS, MessageCallbackType +from .const import DEFAULT_QOS +from .models import MessageCallbackType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 5fdaa744ca99d5..12fd4c51693457 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -15,51 +15,19 @@ clear_discovery_hash, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .schema import CONF_SCHEMA, LEGACY, STATE, MQTT_VACUUM_SCHEMA +from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy +from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state _LOGGER = logging.getLogger(__name__) -CONF_SCHEMA = "schema" -LEGACY = "legacy" -STATE = "state" - def validate_mqtt_vacuum(value): """Validate MQTT vacuum schema.""" - from . import schema_legacy - from . import schema_state - - schemas = { - LEGACY: schema_legacy.PLATFORM_SCHEMA_LEGACY, - STATE: schema_state.PLATFORM_SCHEMA_STATE, - } + schemas = {LEGACY: PLATFORM_SCHEMA_LEGACY, STATE: PLATFORM_SCHEMA_STATE} return schemas[value[CONF_SCHEMA]](value) -def services_to_strings(services, service_to_string): - """Convert SUPPORT_* service bitmask to list of service strings.""" - strings = [] - for service in service_to_string: - if service & services: - strings.append(service_to_string[service]) - return strings - - -def strings_to_services(strings, string_to_service): - """Convert service strings to SUPPORT_* service bitmask.""" - services = 0 - for string in strings: - services |= string_to_service[string] - return services - - -MQTT_VACUUM_SCHEMA = vol.Schema( - { - vol.Optional(CONF_SCHEMA, default=LEGACY): vol.All( - vol.Lower, vol.Any(LEGACY, STATE) - ) - } -) - PLATFORM_SCHEMA = vol.All( MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum ) @@ -95,13 +63,7 @@ async def _async_setup_entity( config, async_add_entities, config_entry, discovery_hash=None ): """Set up the MQTT vacuum.""" - from . import schema_legacy - from . import schema_state - - setup_entity = { - LEGACY: schema_legacy.async_setup_entity_legacy, - STATE: schema_state.async_setup_entity_state, - } + setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state} await setup_entity[config[CONF_SCHEMA]]( config, async_add_entities, config_entry, discovery_hash ) diff --git a/homeassistant/components/mqtt/vacuum/schema.py b/homeassistant/components/mqtt/vacuum/schema.py new file mode 100644 index 00000000000000..949b5cede9cc35 --- /dev/null +++ b/homeassistant/components/mqtt/vacuum/schema.py @@ -0,0 +1,31 @@ +"""Shared schema code.""" +import voluptuous as vol + +CONF_SCHEMA = "schema" +LEGACY = "legacy" +STATE = "state" + +MQTT_VACUUM_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SCHEMA, default=LEGACY): vol.All( + vol.Lower, vol.Any(LEGACY, STATE) + ) + } +) + + +def services_to_strings(services, service_to_string): + """Convert SUPPORT_* service bitmask to list of service strings.""" + strings = [] + for service in service_to_string: + if service & services: + strings.append(service_to_string[service]) + return strings + + +def strings_to_services(strings, string_to_service): + """Convert service strings to SUPPORT_* service bitmask.""" + services = 0 + for string in strings: + services |= string_to_service[string] + return services diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index f2fa8f8da6622f..d770cfbb7f83d9 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -33,7 +33,7 @@ subscription, ) -from . import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services +from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 1ab415aef7b74c..40b3eeb752cd37 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -46,7 +46,7 @@ CONF_QOS, ) -from . import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services +from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) diff --git a/tests/common.py b/tests/common.py index 40e02842146012..5532e6ccb5c4a5 100644 --- a/tests/common.py +++ b/tests/common.py @@ -27,6 +27,7 @@ ) from homeassistant.auth.permissions import system_policies from homeassistant.components import mqtt, recorder +from homeassistant.components.mqtt.models import Message from homeassistant.config import async_process_component_config from homeassistant.const import ( ATTR_DISCOVERED, @@ -271,7 +272,7 @@ def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False): """Fire the MQTT message.""" if isinstance(payload, str): payload = payload.encode("utf-8") - msg = mqtt.Message(topic, payload, qos, retain) + msg = Message(topic, payload, qos, retain) hass.data["mqtt"]._mqtt_handle_message(msg) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index add27bebdeb209..c35740407c75db 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -5,10 +5,8 @@ from homeassistant.components import mqtt, vacuum from homeassistant.components.mqtt import CONF_COMMAND_TOPIC from homeassistant.components.mqtt.discovery import async_start -from homeassistant.components.mqtt.vacuum import ( - schema_legacy as mqttvacuum, - services_to_strings, -) +from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum +from homeassistant.components.mqtt.vacuum.schema import services_to_strings from homeassistant.components.mqtt.vacuum.schema_legacy import ( ALL_SERVICES, SERVICE_TO_STRING, @@ -80,7 +78,7 @@ async def test_default_supported_features(hass, mqtt_mock): async def test_all_commands(hass, mqtt_mock): """Test simple commands to the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -221,7 +219,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): async def test_status(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -260,7 +258,7 @@ async def test_status(hass, mqtt_mock): async def test_status_battery(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -277,7 +275,7 @@ async def test_status_battery(hass, mqtt_mock): async def test_status_cleaning(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -294,7 +292,7 @@ async def test_status_cleaning(hass, mqtt_mock): async def test_status_docked(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -311,7 +309,7 @@ async def test_status_docked(hass, mqtt_mock): async def test_status_charging(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -328,7 +326,7 @@ async def test_status_charging(hass, mqtt_mock): async def test_status_fan_speed(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -345,7 +343,7 @@ async def test_status_fan_speed(hass, mqtt_mock): async def test_status_error(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -371,7 +369,7 @@ async def test_battery_template(hass, mqtt_mock): config = deepcopy(DEFAULT_CONFIG) config.update( { - mqttvacuum.CONF_SUPPORTED_FEATURES: mqttvacuum.services_to_strings( + mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ), mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", @@ -390,7 +388,7 @@ async def test_battery_template(hass, mqtt_mock): async def test_status_invalid_json(hass, mqtt_mock): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 3627c95040e07c..71dff7ef3acc87 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -19,9 +19,13 @@ def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() - @patch("passlib.apps.custom_app_context", Mock(return_value="")) + @patch( + "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") + ) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) + @patch( + "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock()) + ) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): @@ -41,9 +45,13 @@ def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): assert mock_mqtt.mock_calls[1][2]["username"] == "homeassistant" assert mock_mqtt.mock_calls[1][2]["password"] == password - @patch("passlib.apps.custom_app_context", Mock(return_value="")) + @patch( + "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") + ) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) + @patch( + "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock()) + ) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index fe100bdcb6ef40..572c3b05752401 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -5,11 +5,8 @@ from homeassistant.components import mqtt, vacuum from homeassistant.components.mqtt import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC from homeassistant.components.mqtt.discovery import async_start -from homeassistant.components.mqtt.vacuum import ( - CONF_SCHEMA, - schema_state as mqttvacuum, - services_to_strings, -) +from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum +from homeassistant.components.mqtt.vacuum.schema import services_to_strings from homeassistant.components.mqtt.vacuum.schema_state import SERVICE_TO_STRING from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, @@ -259,7 +256,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock): async def test_status_invalid_json(hass, mqtt_mock): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING ) From bb80d9bd169f69b0b73e3bf9829532759c503834 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:06:41 +0200 Subject: [PATCH 0966/3953] Move imports in august component (#27810) --- homeassistant/components/august/__init__.py | 15 ++++++--------- homeassistant/components/august/binary_sensor.py | 9 +++------ homeassistant/components/august/lock.py | 6 +++--- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 93b5ec6ec78324..468e6e429a7e93 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -1,18 +1,20 @@ """Support for August devices.""" -import logging from datetime import timedelta +import logging +from august.api import Api +from august.authenticator import AuthenticationState, Authenticator, ValidationResult +from requests import RequestException, Session import voluptuous as vol -from requests import RequestException -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_PASSWORD, - CONF_USERNAME, CONF_TIMEOUT, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -62,7 +64,6 @@ def request_configuration(hass, config, api, authenticator): def august_configuration_callback(data): """Run when the configuration callback is called.""" - from august.authenticator import ValidationResult result = authenticator.validate_verification_code(data.get("verification_code")) @@ -94,7 +95,6 @@ def august_configuration_callback(data): def setup_august(hass, config, api, authenticator): """Set up the August component.""" - from august.authenticator import AuthenticationState authentication = None try: @@ -134,9 +134,6 @@ def setup_august(hass, config, api, authenticator): def setup(hass, config): """Set up the August component.""" - from august.api import Api - from august.authenticator import Authenticator - from requests import Session conf = config[DOMAIN] api_http_session = None diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index d68582d30c54c8..14d03189c92436 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -2,6 +2,9 @@ from datetime import datetime, timedelta import logging +from august.activity import ActivityType +from august.lock import LockDoorStatus + from homeassistant.components.binary_sensor import BinarySensorDevice from . import DATA_AUGUST @@ -26,7 +29,6 @@ def _retrieve_online_state(data, doorbell): def _retrieve_motion_state(data, doorbell): - from august.activity import ActivityType return _activity_time_based_state( data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING] @@ -34,7 +36,6 @@ def _retrieve_motion_state(data, doorbell): def _retrieve_ding_state(data, doorbell): - from august.activity import ActivityType return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING]) @@ -65,8 +66,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = hass.data[DATA_AUGUST] devices = [] - from august.lock import LockDoorStatus - for door in data.locks: for sensor_type in SENSOR_TYPES_DOOR: state_provider = SENSOR_TYPES_DOOR[sensor_type][2] @@ -136,8 +135,6 @@ def update(self): self._state = state_provider(self._data, self._door) self._available = self._state is not None - from august.lock import LockDoorStatus - self._state = self._state == LockDoorStatus.OPEN @property diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 8b8c019eb2db90..a541be67097371 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -2,6 +2,9 @@ from datetime import timedelta import logging +from august.activity import ActivityType +from august.lock import LockStatus + from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_BATTERY_LEVEL @@ -51,8 +54,6 @@ def update(self): self._lock_detail = self._data.get_lock_detail(self._lock.device_id) - from august.activity import ActivityType - activity = self._data.get_latest_device_activity( self._lock.device_id, ActivityType.LOCK_OPERATION ) @@ -73,7 +74,6 @@ def available(self): @property def is_locked(self): """Return true if device is on.""" - from august.lock import LockStatus return self._lock_status is LockStatus.LOCKED From 3cf7983e00231f74fb5c030e0f4d9e28c7f6e0d1 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:08:58 +0200 Subject: [PATCH 0967/3953] Move imports in asterisk_mbox component (#27807) --- homeassistant/components/asterisk_mbox/__init__.py | 12 ++++++------ homeassistant/components/asterisk_mbox/mailbox.py | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/asterisk_mbox/__init__.py b/homeassistant/components/asterisk_mbox/__init__.py index 6c9412d07d836d..1ecba9f4c8fba4 100644 --- a/homeassistant/components/asterisk_mbox/__init__.py +++ b/homeassistant/components/asterisk_mbox/__init__.py @@ -1,6 +1,12 @@ """Support for Asterisk Voicemail interface.""" import logging +from asterisk_mbox import Client as asteriskClient +from asterisk_mbox.commands import ( + CMD_MESSAGE_CDR, + CMD_MESSAGE_CDR_AVAILABLE, + CMD_MESSAGE_LIST, +) import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT @@ -51,7 +57,6 @@ class AsteriskData: def __init__(self, hass, host, port, password, config): """Init the Asterisk data object.""" - from asterisk_mbox import Client as asteriskClient self.hass = hass self.config = config @@ -76,11 +81,6 @@ def _discover_platform(self, component): @callback def handle_data(self, command, msg): """Handle changes to the mailbox.""" - from asterisk_mbox.commands import ( - CMD_MESSAGE_LIST, - CMD_MESSAGE_CDR_AVAILABLE, - CMD_MESSAGE_CDR, - ) if command == CMD_MESSAGE_LIST: _LOGGER.debug("AsteriskVM sent updated message list: Len %d", len(msg)) diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py index 4d3c255fd5b99b..3cd6fe059b6d7e 100644 --- a/homeassistant/components/asterisk_mbox/mailbox.py +++ b/homeassistant/components/asterisk_mbox/mailbox.py @@ -1,6 +1,8 @@ """Support for the Asterisk Voicemail interface.""" import logging +from asterisk_mbox import ServerError + from homeassistant.components.mailbox import CONTENT_TYPE_MPEG, Mailbox, StreamError from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -50,7 +52,6 @@ def has_media(self): async def async_get_media(self, msgid): """Return the media blob for the msgid.""" - from asterisk_mbox import ServerError client = self.hass.data[ASTERISK_DOMAIN].client try: From 5eb781d3785b140cfbee0c0eaa93a72536876001 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:09:47 +0200 Subject: [PATCH 0968/3953] Move imports in arlo component (#27806) --- homeassistant/components/arlo/__init__.py | 10 +++++----- homeassistant/components/arlo/camera.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/arlo/__init__.py b/homeassistant/components/arlo/__init__.py index 80fa37b678761a..df24bdd1a920a8 100644 --- a/homeassistant/components/arlo/__init__.py +++ b/homeassistant/components/arlo/__init__.py @@ -1,14 +1,15 @@ """Support for Netgear Arlo IP cameras.""" -import logging from datetime import timedelta +import logging +from pyarlo import PyArlo +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from requests.exceptions import HTTPError, ConnectTimeout +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.helpers import config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL -from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) @@ -47,7 +48,6 @@ def setup(hass, config): scan_interval = conf.get(CONF_SCAN_INTERVAL) try: - from pyarlo import PyArlo arlo = PyArlo(username, password, preload=False) if not arlo.is_connected: diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index a05dc40a9effa0..958c383765a101 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -1,6 +1,7 @@ """Support for Netgear Arlo IP cameras.""" import logging +from haffmpeg.camera import CameraMjpeg import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -77,7 +78,6 @@ def _update_callback(self): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg video = self._camera.last_video if not video: From 56c13503c35e1e6eed46ab6671e60a4520bb58c8 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:10:16 +0200 Subject: [PATCH 0969/3953] Move imports in aqualogic component (#27805) --- homeassistant/components/aqualogic/__init__.py | 4 ++-- homeassistant/components/aqualogic/switch.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aqualogic/__init__.py b/homeassistant/components/aqualogic/__init__.py index cabe00b6c6d13f..9f693966382c48 100644 --- a/homeassistant/components/aqualogic/__init__.py +++ b/homeassistant/components/aqualogic/__init__.py @@ -1,9 +1,10 @@ """Support for AquaLogic devices.""" from datetime import timedelta import logging -import time import threading +import time +from aqualogic.core import AquaLogic import voluptuous as vol from homeassistant.const import ( @@ -71,7 +72,6 @@ def data_changed(self, panel): def run(self): """Event thread.""" - from aqualogic.core import AquaLogic while True: self._panel = AquaLogic() diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index b5a7a409647a46..74f1a9d9f9aaf4 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -1,6 +1,7 @@ """Support for AquaLogic switches.""" import logging +from aqualogic.core import States import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -50,7 +51,6 @@ class AquaLogicSwitch(SwitchDevice): def __init__(self, processor, switch_type): """Initialize switch.""" - from aqualogic.core import States self._processor = processor self._type = switch_type From 447d99a1ae99b13fb993bd438d7669516e0c85ae Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:10:28 +0200 Subject: [PATCH 0970/3953] Move imports in apcupsd component (#27803) --- homeassistant/components/apcupsd/__init__.py | 4 ++-- homeassistant/components/apcupsd/sensor.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 512bd01b72a87c..71f25f043873e8 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -1,7 +1,8 @@ """Support for APCUPSd via its Network Information Server (NIS).""" -import logging from datetime import timedelta +import logging +from apcaccess import status import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT @@ -64,7 +65,6 @@ class APCUPSdData: def __init__(self, host, port): """Initialize the data object.""" - from apcaccess import status self._host = host self._port = port diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 837e6e45c6c9a2..255eb1624ff9f4 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -1,12 +1,13 @@ """Support for APCUPSd sensors.""" import logging +from apcaccess.status import ALL_UNITS import voluptuous as vol +from homeassistant.components import apcupsd from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_RESOURCES, POWER_WATT, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv -from homeassistant.components import apcupsd -from homeassistant.const import TEMP_CELSIUS, CONF_RESOURCES, POWER_WATT from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -135,7 +136,6 @@ def infer_unit(value): Split the unit off the end of the value and return the value, unit tuple pair. Else return the original value and None as the unit. """ - from apcaccess.status import ALL_UNITS for unit in ALL_UNITS: if value.endswith(unit): From 54ef96e79aaeeaacf60a6fffa1c9f11a526770bf Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:11:01 +0200 Subject: [PATCH 0971/3953] Move imports in awair component (#27811) --- homeassistant/components/awair/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index c899e0097964d9..f15e4a80e36989 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -4,6 +4,7 @@ import logging import math +from python_awair import AwairClient import voluptuous as vol from homeassistant.const import ( @@ -105,7 +106,6 @@ # used at this time is the `uuid` value. async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Connect to the Awair API and find devices.""" - from python_awair import AwairClient token = config[CONF_ACCESS_TOKEN] client = AwairClient(token, session=async_get_clientsession(hass)) From 9d583ad9f98cd3f1c2c6832206c039712c0fe4db Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:11:11 +0200 Subject: [PATCH 0972/3953] Move imports in baidu component (#27812) --- homeassistant/components/baidu/tts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/baidu/tts.py b/homeassistant/components/baidu/tts.py index 85737d1affdc00..8d753753e5a25d 100644 --- a/homeassistant/components/baidu/tts.py +++ b/homeassistant/components/baidu/tts.py @@ -1,6 +1,7 @@ """Support for Baidu speech service.""" import logging +from aip import AipSpeech import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider @@ -106,7 +107,6 @@ def supported_options(self): def get_tts_audio(self, message, language, options=None): """Load TTS from BaiduTTS.""" - from aip import AipSpeech aip_speech = AipSpeech( self._app_data["appid"], From 6998687742943c5e6e2d0f068fb855d657e4cbb8 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:11:20 +0200 Subject: [PATCH 0973/3953] Move imports in gitlab_ci component (#27827) --- homeassistant/components/gitlab_ci/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index d8055c88f30b3d..9edbe9733a8991 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from gitlab import Gitlab, GitlabAuthenticationError, GitlabGetError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -141,12 +142,10 @@ class GitLabData: def __init__(self, gitlab_id, priv_token, interval, url): """Fetch data from GitLab API for most recent CI job.""" - import gitlab self._gitlab_id = gitlab_id - self._gitlab = gitlab.Gitlab(url, private_token=priv_token, per_page=1) + self._gitlab = Gitlab(url, private_token=priv_token, per_page=1) self._gitlab.auth() - self._gitlab_exceptions = gitlab.exceptions self.update = Throttle(interval)(self._update) self.available = False @@ -174,9 +173,9 @@ def _update(self): self.build_id = _last_job.attributes.get("id") self.branch = _last_job.attributes.get("ref") self.available = True - except self._gitlab_exceptions.GitlabAuthenticationError as erra: + except GitlabAuthenticationError as erra: _LOGGER.error("Authentication Error: %s", erra) self.available = False - except self._gitlab_exceptions.GitlabGetError as errg: + except GitlabGetError as errg: _LOGGER.error("Project Not Found: %s", errg) self.available = False From 61edd33da74d75df3e7cd8c26c685be7db8d06c8 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:11:51 +0200 Subject: [PATCH 0974/3953] Move imports in google component (#27826) --- homeassistant/components/google/__init__.py | 20 +++++++++----------- homeassistant/components/google/calendar.py | 5 ++--- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 62aa2212bb1b72..9cb9be0fa4fad7 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -4,6 +4,15 @@ import os import yaml +import httplib2 +from oauth2client.client import ( + OAuth2WebServerFlow, + OAuth2DeviceCodeError, + FlowExchangeError, +) +from oauth2client.file import Storage +from googleapiclient import discovery as google_discovery + import voluptuous as vol from voluptuous.error import Error as VoluptuousError @@ -126,13 +135,6 @@ def do_authentication(hass, hass_config, config): Notify user of user_code and verification_url then poll until we have an access token. """ - from oauth2client.client import ( - OAuth2WebServerFlow, - OAuth2DeviceCodeError, - FlowExchangeError, - ) - from oauth2client.file import Storage - oauth = OAuth2WebServerFlow( client_id=config[CONF_CLIENT_ID], client_secret=config[CONF_CLIENT_SECRET], @@ -341,10 +343,6 @@ def __init__(self, token_file): def get(self): """Get the calendar service from the storage file token.""" - import httplib2 - from oauth2client.file import Storage - from googleapiclient import discovery as google_discovery - credentials = Storage(self.token_file).get() http = credentials.authorize(httplib2.Http()) service = google_discovery.build( diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 31e9f186a4e086..8a6eb6446210b0 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -3,6 +3,8 @@ from datetime import timedelta import logging +from httplib2 import ServerNotFoundError # pylint: disable=import-error + from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, CalendarEventDevice, @@ -126,9 +128,6 @@ def __init__( self.event = None def _prepare_query(self): - # pylint: disable=import-error - from httplib2 import ServerNotFoundError - try: service = self.calendar_service.get() except ServerNotFoundError: From 3a608314c491ef43d1e0a15ddcb0d4fd9556df41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 18 Oct 2019 03:12:58 +0300 Subject: [PATCH 0975/3953] Mypy setup fixes (#27825) * Install our core dependencies for mypy in azure To match local setups and tox. * Use "system" mypy in pre-commit instead of the "real" mypy hook The results of mypy depend on what is installed. And the mypy hook runs in a virtualenv of its own, meaning we'd need to install and maintain another set of our dependencies there... no. Use the "system" one and reuse the environment that is set up anyway already instead. * Reintroduce needed ruamel.yaml type ignore This ignore is required when ruamel.yaml is installed, and we want it to be as it's part of the core dependency set. --- .pre-commit-config.yaml | 14 +++++++++++--- azure-pipelines-ci.yml | 4 ++-- homeassistant/util/ruamel_yaml.py | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48d77cfdc6fc77..3773a3213aa7d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,9 +13,17 @@ repos: additional_dependencies: - flake8-docstrings==1.3.1 - pydocstyle==4.0.0 -- repo: https://github.com/pre-commit/mirrors-mypy.git - rev: v0.730 +# Using a local "system" mypy instead of the mypy hook, because its +# results depend on what is installed. And the mypy hook runs in a +# virtualenv of its own, meaning we'd need to install and maintain +# another set of our dependencies there... no. Use the "system" one +# and reuse the environment that is set up anyway already instead. +- repo: local hooks: - id: mypy - args: [] + name: mypy + entry: mypy + language: system + types: [python] + require_serial: true exclude: ^script/scaffold/templates/ diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index a566baf6561655..257eac57c2d67a 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -174,12 +174,12 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' + keyfile: 'requirements_test.txt | setup.py | homeassistant/package_constraints.txt' build: | python -m venv venv . venv/bin/activate - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate mypy homeassistant diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index 6793784abae310..b7e8927888c734 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -90,7 +90,7 @@ def load_yaml(fname: str, round_trip: bool = False) -> JSON_TYPE: if round_trip: yaml = YAML(typ="rt") # type ignore: https://bitbucket.org/ruamel/yaml/pull-requests/42 - yaml.preserve_quotes = True + yaml.preserve_quotes = True # type: ignore else: if ExtSafeConstructor.name is None: ExtSafeConstructor.name = fname From fe036ed0940c5be3ee901ba060ca8b40a01fd324 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:13:20 +0200 Subject: [PATCH 0976/3953] Move imports in flic component (#27821) --- .../components/flic/binary_sensor.py | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index 4fa97334889885..416d39e53326e6 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -2,6 +2,14 @@ import logging import threading +from pyflic import ( + FlicClient, + ButtonConnectionChannel, + ClickType, + ConnectionStatus, + ScanWizard, + ScanWizardResult, +) import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -49,7 +57,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the flic platform.""" - import pyflic # Initialize flic client responsible for # connecting to buttons and retrieving events @@ -58,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): discovery = config.get(CONF_DISCOVERY) try: - client = pyflic.FlicClient(host, port) + client = FlicClient(host, port) except ConnectionRefusedError: _LOGGER.error("Failed to connect to flic server") return @@ -88,15 +95,13 @@ def get_info_callback(items): def start_scanning(config, add_entities, client): """Start a new flic client for scanning and connecting to new buttons.""" - import pyflic - - scan_wizard = pyflic.ScanWizard() + scan_wizard = ScanWizard() def scan_completed_callback(scan_wizard, result, address, name): """Restart scan wizard to constantly check for new buttons.""" - if result == pyflic.ScanWizardResult.WizardSuccess: + if result == ScanWizardResult.WizardSuccess: _LOGGER.info("Found new button %s", address) - elif result != pyflic.ScanWizardResult.WizardFailedTimeout: + elif result != ScanWizardResult.WizardFailedTimeout: _LOGGER.warning( "Failed to connect to button %s. Reason: %s", address, result ) @@ -123,7 +128,6 @@ class FlicButton(BinarySensorDevice): def __init__(self, hass, client, address, timeout, ignored_click_types): """Initialize the flic button.""" - import pyflic self._hass = hass self._address = address @@ -131,10 +135,10 @@ def __init__(self, hass, client, address, timeout, ignored_click_types): self._is_down = False self._ignored_click_types = ignored_click_types or [] self._hass_click_types = { - pyflic.ClickType.ButtonClick: CLICK_TYPE_SINGLE, - pyflic.ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE, - pyflic.ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE, - pyflic.ClickType.ButtonHold: CLICK_TYPE_HOLD, + ClickType.ButtonClick: CLICK_TYPE_SINGLE, + ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE, + ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE, + ClickType.ButtonHold: CLICK_TYPE_HOLD, } self._channel = self._create_channel() @@ -142,9 +146,7 @@ def __init__(self, hass, client, address, timeout, ignored_click_types): def _create_channel(self): """Create a new connection channel to the button.""" - import pyflic - - channel = pyflic.ButtonConnectionChannel(self._address) + channel = ButtonConnectionChannel(self._address) channel.on_button_up_or_down = self._on_up_down # If all types of clicks should be ignored, skip registering callbacks @@ -212,12 +214,10 @@ def _queued_event_check(self, click_type, time_diff): def _on_up_down(self, channel, click_type, was_queued, time_diff): """Update device state, if event was not queued.""" - import pyflic - if was_queued and self._queued_event_check(click_type, time_diff): return - self._is_down = click_type == pyflic.ClickType.ButtonDown + self._is_down = click_type == ClickType.ButtonDown self.schedule_update_ha_state() def _on_click(self, channel, click_type, was_queued, time_diff): @@ -243,9 +243,7 @@ def _on_click(self, channel, click_type, was_queued, time_diff): def _connection_status_changed(self, channel, connection_status, disconnect_reason): """Remove device, if button disconnects.""" - import pyflic - - if connection_status == pyflic.ConnectionStatus.Disconnected: + if connection_status == ConnectionStatus.Disconnected: _LOGGER.warning( "Button (%s) disconnected. Reason: %s", self.address, disconnect_reason ) From 0965e358ea88aa465db93beb5d2d018cd0de61d5 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:14:53 +0200 Subject: [PATCH 0977/3953] Move imports in fitbit component (#27820) --- homeassistant/components/fitbit/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 534477d88cf792..0d4b8d61e7a7a7 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -4,6 +4,9 @@ import datetime import time +from fitbit import Fitbit +from fitbit.api import FitbitOauth2Client +from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError import voluptuous as vol from homeassistant.core import callback @@ -234,13 +237,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if "fitbit" in _CONFIGURING: hass.components.configurator.request_done(_CONFIGURING.pop("fitbit")) - import fitbit - access_token = config_file.get(ATTR_ACCESS_TOKEN) refresh_token = config_file.get(ATTR_REFRESH_TOKEN) expires_at = config_file.get(ATTR_LAST_SAVED_AT) if None not in (access_token, refresh_token): - authd_client = fitbit.Fitbit( + authd_client = Fitbit( config_file.get(ATTR_CLIENT_ID), config_file.get(ATTR_CLIENT_SECRET), access_token=access_token, @@ -294,7 +295,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) else: - oauth = fitbit.api.FitbitOauth2Client( + oauth = FitbitOauth2Client( config_file.get(ATTR_CLIENT_ID), config_file.get(ATTR_CLIENT_SECRET) ) @@ -337,9 +338,6 @@ def __init__(self, config, add_entities, oauth): @callback def get(self, request): """Finish OAuth callback request.""" - from oauthlib.oauth2.rfc6749.errors import MismatchingStateError - from oauthlib.oauth2.rfc6749.errors import MissingTokenError - hass = request.app["hass"] data = request.query From 22b904f5e05f3b0ac388f6971955b78a12d03045 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:15:18 +0200 Subject: [PATCH 0978/3953] Move imports in flux_led component (#27822) --- homeassistant/components/flux_led/light.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 0a95de783fa273..5bd84cd157fb09 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -3,6 +3,7 @@ import socket import random +from flux_led import BulbScanner, WifiLedBulb import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL, ATTR_MODE @@ -135,8 +136,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Flux lights.""" - import flux_led - lights = [] light_ips = [] @@ -156,7 +155,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return # Find the bulbs on the LAN - scanner = flux_led.BulbScanner() + scanner = BulbScanner() scanner.scan(timeout=10) for device in scanner.getBulbInfo(): ipaddr = device["ipaddr"] @@ -187,9 +186,8 @@ def __init__(self, device): def _connect(self): """Connect to Flux light.""" - import flux_led - self._bulb = flux_led.WifiLedBulb(self._ipaddr, timeout=5) + self._bulb = WifiLedBulb(self._ipaddr, timeout=5) if self._protocol: self._bulb.setProtocol(self._protocol) From fdf839774e936f6c052f522315a0fe7d1026f8e4 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:17:24 +0200 Subject: [PATCH 0979/3953] Move imports in fritz + fritzbox_netmonitor component (#27823) * Move imports in fritz + fritzbox_netmonitor component * Fix PyLint 1 --- homeassistant/components/fritz/device_tracker.py | 5 ++--- homeassistant/components/fritzbox_netmonitor/sensor.py | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index afe0aa3ed02423..c2b1e3ab54ede6 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -1,6 +1,7 @@ """Support for FRITZ!Box routers.""" import logging +from fritzconnection import FritzHosts # pylint: disable=import-error import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -41,11 +42,9 @@ def __init__(self, config): self.password = config[CONF_PASSWORD] self.success_init = True - import fritzconnection as fc # pylint: disable=import-error - # Establish a connection to the FRITZ!Box. try: - self.fritz_box = fc.FritzHosts( + self.fritz_box = FritzHosts( address=self.host, user=self.username, password=self.password ) except (ValueError, TypeError): diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py index 9d07e7a80558f7..92a29e37c5161f 100644 --- a/homeassistant/components/fritzbox_netmonitor/sensor.py +++ b/homeassistant/components/fritzbox_netmonitor/sensor.py @@ -3,6 +3,10 @@ from datetime import timedelta from requests.exceptions import RequestException +from fritzconnection import FritzStatus # pylint: disable=import-error +from fritzconnection.fritzconnection import ( # pylint: disable=import-error + FritzConnectionException, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -45,15 +49,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the FRITZ!Box monitor sensors.""" - # pylint: disable=import-error - import fritzconnection as fc - from fritzconnection.fritzconnection import FritzConnectionException - name = config.get(CONF_NAME) host = config.get(CONF_HOST) try: - fstatus = fc.FritzStatus(address=host) + fstatus = FritzStatus(address=host) except (ValueError, TypeError, FritzConnectionException): fstatus = None From bc58649c2becda4dbc46095c912c57201719c1a5 Mon Sep 17 00:00:00 2001 From: Tomasz Jagusz Date: Fri, 18 Oct 2019 02:17:56 +0200 Subject: [PATCH 0980/3953] Move imports in MCP23017 component (#27769) * mcp23017 move imports * fix pylint errors --- homeassistant/components/mcp23017/binary_sensor.py | 10 ++++------ homeassistant/components/mcp23017/switch.py | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py index c059ad6a1b610b..088052c469e042 100644 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ b/homeassistant/components/mcp23017/binary_sensor.py @@ -2,6 +2,10 @@ import logging import voluptuous as vol +import board # pylint: disable=import-error +import busio # pylint: disable=import-error +import adafruit_mcp230xx # pylint: disable=import-error +import digitalio # pylint: disable=import-error from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME @@ -37,10 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MCP23017 binary sensors.""" - import board - import busio - import adafruit_mcp230xx - pull_mode = config[CONF_PULL_MODE] invert_logic = config[CONF_INVERT_LOGIC] i2c_address = config[CONF_I2C_ADDRESS] @@ -65,8 +65,6 @@ class MCP23017BinarySensor(BinarySensorDevice): def __init__(self, name, pin, pull_mode, invert_logic): """Initialize the MCP23017 binary sensor.""" - import digitalio - self._name = name or DEVICE_DEFAULT_NAME self._pin = pin self._pull_mode = pull_mode diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py index 46978319198954..399ed17c44b51b 100644 --- a/homeassistant/components/mcp23017/switch.py +++ b/homeassistant/components/mcp23017/switch.py @@ -2,6 +2,10 @@ import logging import voluptuous as vol +import board # pylint: disable=import-error +import busio # pylint: disable=import-error +import adafruit_mcp230xx # pylint: disable=import-error +import digitalio # pylint: disable=import-error from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME @@ -31,10 +35,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MCP23017 devices.""" - import board - import busio - import adafruit_mcp230xx - invert_logic = config.get(CONF_INVERT_LOGIC) i2c_address = config.get(CONF_I2C_ADDRESS) @@ -54,8 +54,6 @@ class MCP23017Switch(ToggleEntity): def __init__(self, name, pin, invert_logic): """Initialize the pin.""" - import digitalio - self._name = name or DEVICE_DEFAULT_NAME self._pin = pin self._invert_logic = invert_logic From d95b4a6a0bdd5e6c57c34bc74fda5f07c201d556 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:18:11 +0200 Subject: [PATCH 0981/3953] Move imports in anel_pwrctrl component (#27798) --- homeassistant/components/anel_pwrctrl/switch.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py index 6184465ef16104..3c181d7d04b4b6 100644 --- a/homeassistant/components/anel_pwrctrl/switch.py +++ b/homeassistant/components/anel_pwrctrl/switch.py @@ -1,13 +1,14 @@ """Support for ANEL PwrCtrl switches.""" +from datetime import timedelta import logging import socket -from datetime import timedelta +from anel_pwrctrl import DeviceMaster import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -36,8 +37,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port_recv = config.get(CONF_PORT_RECV) port_send = config.get(CONF_PORT_SEND) - from anel_pwrctrl import DeviceMaster - try: master = DeviceMaster( username=username, From 21754fd7ccefa0bc85ea26e0dd40b3ab6ccaadd1 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:18:22 +0200 Subject: [PATCH 0982/3953] Move imports in bbb_gpio component (#27813) --- homeassistant/components/bbb_gpio/__init__.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py index bfaa2a7c50dbef..e68633c06882f4 100644 --- a/homeassistant/components/bbb_gpio/__init__.py +++ b/homeassistant/components/bbb_gpio/__init__.py @@ -1,6 +1,8 @@ """Support for controlling GPIO pins of a Beaglebone Black.""" import logging +from Adafruit_BBIO import GPIO # pylint: disable=import-error + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -11,7 +13,6 @@ def setup(hass, config): """Set up the BeagleBone Black GPIO component.""" # pylint: disable=import-error - from Adafruit_BBIO import GPIO def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -27,39 +28,29 @@ def prepare_gpio(event): def setup_output(pin): """Set up a GPIO as output.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.setup(pin, GPIO.OUT) def setup_input(pin, pull_mode): """Set up a GPIO as input.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP) def write_output(pin, value): """Write a value to a GPIO.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.output(pin, value) def read_input(pin): """Read a value from a GPIO.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO return GPIO.input(pin) is GPIO.HIGH def edge_detect(pin, event_callback, bounce): """Add detection for RISING and FALLING events.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.add_event_detect(pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) From e17b8b011a7ef6764453b4992d39b7a341918c18 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:18:47 +0200 Subject: [PATCH 0983/3953] Move imports in bitcoin component (#27814) --- homeassistant/components/bitcoin/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index 4d8d56438263df..b62bb434e85f83 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -1,11 +1,12 @@ """Bitcoin information service that uses blockchain.info.""" -import logging from datetime import timedelta +import logging +from blockchain import exchangerates, statistics import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_DISPLAY_OPTIONS, ATTR_ATTRIBUTION, CONF_CURRENCY +from homeassistant.const import ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_OPTIONS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -55,7 +56,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Bitcoin sensors.""" - from blockchain import exchangerates currency = config.get(CONF_CURRENCY) @@ -169,7 +169,6 @@ def __init__(self): def update(self): """Get the latest data from blockchain.info.""" - from blockchain import statistics, exchangerates self.stats = statistics.get() self.ticker = exchangerates.get_ticker() From 2d1f7932ba4478daf28e837b51c6e21f23ec7511 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 18 Oct 2019 01:19:07 +0100 Subject: [PATCH 0984/3953] bump client (#27799) --- homeassistant/components/geniushub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 96497388a4818c..f9e8e6eb4f03df 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/integrations/geniushub", "requirements": [ - "geniushub-client==0.6.26" + "geniushub-client==0.6.28" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/requirements_all.txt b/requirements_all.txt index 6a70451cc127d7..6b8b1931a4af69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -531,7 +531,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.6.26 +geniushub-client==0.6.28 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From bd0403c65ebd32d26063fce399609073fca3a3bd Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:19:34 +0200 Subject: [PATCH 0985/3953] Move imports in telegram_bot component (#27785) --- .../components/telegram_bot/__init__.py | 20 ++++++++++--------- .../components/telegram_bot/polling.py | 9 ++++----- .../components/telegram_bot/webhooks.py | 5 +++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index a36f41edf3b7d4..7acf4985def3b5 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -7,6 +7,16 @@ import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth +from telegram import ( + Bot, + InlineKeyboardButton, + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, +) +from telegram.error import TelegramError +from telegram.parsemode import ParseMode +from telegram.utils.request import Request import voluptuous as vol from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE @@ -375,8 +385,6 @@ def _render_template_attr(data, attribute): def initialize_bot(p_config): """Initialize telegram bot with proxy support.""" - from telegram import Bot - from telegram.utils.request import Request api_key = p_config.get(CONF_API_KEY) proxy_url = p_config.get(CONF_PROXY_URL) @@ -396,7 +404,6 @@ class TelegramNotificationService: def __init__(self, hass, bot, allowed_chat_ids, parser): """Initialize the service.""" - from telegram.parsemode import ParseMode self.allowed_chat_ids = allowed_chat_ids self._default_user = self.allowed_chat_ids[0] @@ -457,7 +464,6 @@ def _make_row_inline_keyboard(row_keyboard): - a string like: `/cmd1, /cmd2, /cmd3` - or a string like: `text_b1:/cmd1, text_b2:/cmd2` """ - from telegram import InlineKeyboardButton buttons = [] if isinstance(row_keyboard, str): @@ -507,8 +513,6 @@ def _make_row_inline_keyboard(row_keyboard): params[ATTR_REPLY_TO_MSGID] = data[ATTR_REPLY_TO_MSGID] # Keyboards: if ATTR_KEYBOARD in data: - from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove - keys = data.get(ATTR_KEYBOARD) keys = keys if isinstance(keys, list) else [keys] if keys: @@ -517,9 +521,8 @@ def _make_row_inline_keyboard(row_keyboard): ) else: params[ATTR_REPLYMARKUP] = ReplyKeyboardRemove(True) - elif ATTR_KEYBOARD_INLINE in data: - from telegram import InlineKeyboardMarkup + elif ATTR_KEYBOARD_INLINE in data: keys = data.get(ATTR_KEYBOARD_INLINE) keys = keys if isinstance(keys, list) else [keys] params[ATTR_REPLYMARKUP] = InlineKeyboardMarkup( @@ -529,7 +532,6 @@ def _make_row_inline_keyboard(row_keyboard): def _send_msg(self, func_send, msg_error, *args_msg, **kwargs_msg): """Send one message.""" - from telegram.error import TelegramError try: out = func_send(*args_msg, **kwargs_msg) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 7ca486e33b2291..314cb31a373078 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -1,6 +1,10 @@ """Support for Telegram bot using polling.""" import logging +from telegram import Update +from telegram.error import TelegramError, TimedOut, NetworkError, RetryAfter +from telegram.ext import Updater, Handler + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -32,8 +36,6 @@ def _stop_bot(_event): def process_error(bot, update, error): """Telegram bot error handler.""" - from telegram.error import TelegramError, TimedOut, NetworkError, RetryAfter - try: raise error except (TimedOut, NetworkError, RetryAfter): @@ -45,8 +47,6 @@ def process_error(bot, update, error): def message_handler(handler): """Create messages handler.""" - from telegram import Update - from telegram.ext import Handler class MessageHandler(Handler): """Telegram bot message handler.""" @@ -72,7 +72,6 @@ class TelegramPoll(BaseTelegramBotEntity): def __init__(self, bot, hass, allowed_chat_ids): """Initialize the polling instance.""" - from telegram.ext import Updater BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index c71510eddd90df..16da2e741e4b40 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -2,6 +2,8 @@ import datetime as dt import logging +from telegram.error import TimedOut + from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.const import ( @@ -26,7 +28,6 @@ async def async_setup_platform(hass, config): """Set up the Telegram webhooks platform.""" - import telegram bot = initialize_bot(config) @@ -55,7 +56,7 @@ def _try_to_set_webhook(): while retry_num < 3: try: return bot.setWebhook(handler_url, timeout=5) - except telegram.error.TimedOut: + except TimedOut: retry_num += 1 _LOGGER.warning("Timeout trying to set webhook (retry #%d)", retry_num) From 6d083969c2bb22307c5c75693d5470cfab731642 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 18 Oct 2019 02:20:10 +0200 Subject: [PATCH 0986/3953] Add device action support to the lock integration (#27499) * Add device action support to the lock integration * Check that the enitity supports open service --- .../components/lock/device_action.py | 92 ++++++++++ homeassistant/components/lock/strings.json | 5 + tests/components/lock/test_device_action.py | 170 ++++++++++++++++++ .../custom_components/test/lock.py | 54 ++++++ 4 files changed, 321 insertions(+) create mode 100644 homeassistant/components/lock/device_action.py create mode 100644 tests/components/lock/test_device_action.py create mode 100644 tests/testing_config/custom_components/test/lock.py diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py new file mode 100644 index 00000000000000..c678bfc17cf28a --- /dev/null +++ b/homeassistant/components/lock/device_action.py @@ -0,0 +1,92 @@ +"""Provides device automations for Lock.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + SERVICE_LOCK, + SERVICE_OPEN, + SERVICE_UNLOCK, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN, SUPPORT_OPEN + +ACTION_TYPES = {"lock", "unlock", "open"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Lock devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add actions for each entity that belongs to this integration + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "lock", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "unlock", + } + ) + + state = hass.states.get(entry.entity_id) + if state: + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & (SUPPORT_OPEN): + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "open", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "lock": + service = SERVICE_LOCK + elif config[CONF_TYPE] == "unlock": + service = SERVICE_UNLOCK + elif config[CONF_TYPE] == "open": + service = SERVICE_OPEN + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index baa9bc1604fd10..9c8589164763f3 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Lock {entity_name}", + "open": "Open {entity_name}", + "unlock": "Unlock {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} is locked", "is_unlocked": "{entity_name} is unlocked" diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py new file mode 100644 index 00000000000000..2006f9b3ff1431 --- /dev/null +++ b/tests/components/lock/test_device_action.py @@ -0,0 +1,170 @@ +"""The tests for Lock device actions.""" +import pytest + +from homeassistant.components.lock import DOMAIN +from homeassistant.const import CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions_support_open(hass, device_reg, entity_reg): + """Test we get the expected actions from a lock which supports open.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["support_open"].unique_id, + device_id=device_entry.id, + ) + + expected_actions = [ + { + "domain": DOMAIN, + "type": "lock", + "device_id": device_entry.id, + "entity_id": "lock.support_open_lock", + }, + { + "domain": DOMAIN, + "type": "unlock", + "device_id": device_entry.id, + "entity_id": "lock.support_open_lock", + }, + { + "domain": DOMAIN, + "type": "open", + "device_id": device_entry.id, + "entity_id": "lock.support_open_lock", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_get_actions_not_support_open(hass, device_reg, entity_reg): + """Test we get the expected actions from a lock which doesn't support open.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["no_support_open"].unique_id, + device_id=device_entry.id, + ) + + expected_actions = [ + { + "domain": DOMAIN, + "type": "lock", + "device_id": device_entry.id, + "entity_id": "lock.no_support_open_lock", + }, + { + "domain": DOMAIN, + "type": "unlock", + "device_id": device_entry.id, + "entity_id": "lock.no_support_open_lock", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass): + """Test for lock actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event_lock"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "lock.entity", + "type": "lock", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event_unlock"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "lock.entity", + "type": "unlock", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event_open"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "lock.entity", + "type": "open", + }, + }, + ] + }, + ) + + lock_calls = async_mock_service(hass, "lock", "lock") + unlock_calls = async_mock_service(hass, "lock", "unlock") + open_calls = async_mock_service(hass, "lock", "open") + + hass.bus.async_fire("test_event_lock") + await hass.async_block_till_done() + assert len(lock_calls) == 1 + assert len(unlock_calls) == 0 + assert len(open_calls) == 0 + + hass.bus.async_fire("test_event_unlock") + await hass.async_block_till_done() + assert len(lock_calls) == 1 + assert len(unlock_calls) == 1 + assert len(open_calls) == 0 + + hass.bus.async_fire("test_event_open") + await hass.async_block_till_done() + assert len(lock_calls) == 1 + assert len(unlock_calls) == 1 + assert len(open_calls) == 1 diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py new file mode 100644 index 00000000000000..db6ce38b097ac3 --- /dev/null +++ b/tests/testing_config/custom_components/test/lock.py @@ -0,0 +1,54 @@ +""" +Provide a mock lock platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN +from tests.common import MockEntity + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + "support_open": MockLock( + name=f"Support open Lock", + is_locked=True, + supported_features=SUPPORT_OPEN, + unique_id="unique_support_open", + ), + "no_support_open": MockLock( + name=f"No support open Lock", + is_locked=True, + supported_features=0, + unique_id="unique_no_support_open", + ), + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockLock(MockEntity, LockDevice): + """Mock Lock class.""" + + @property + def is_locked(self): + """Return true if the lock is locked.""" + return self._handle("is_locked") + + @property + def supported_features(self): + """Return the class of this sensor.""" + return self._handle("supported_features") From 3e7fcc757598b35f7d0c9b3a7728936fd1934d75 Mon Sep 17 00:00:00 2001 From: scheric <38077357+scheric@users.noreply.github.com> Date: Fri, 18 Oct 2019 02:21:00 +0200 Subject: [PATCH 0987/3953] Add grid sensors to SolarEdge_local (#27247) * Add grid sensors * Formatting * Add possibility to add attributes * Add optimizer attribute * Remove bare 'except' * add proper exception * Remove return attribution 0 * Ad inverter attribution * Style change * Add attribute name to sensors constants * SENSOR_TYPES alphabetical and snake_case lower * Formatting * forgot snake_case lower * Add extra meter sensors * add critical error for debugging * Update sensor.py * swam meter sensors * Add suitable icons to meter reading * Fix for pointless-statement homeassistant/components/solaredge_local/sensor.py:173:8: W0104: Statement seems to have no effect (pointless-statement) homeassistant/components/solaredge_local/sensor.py:192:8: W0104: Statement seems to have no effect (pointless-statement) homeassistant/components/solaredge_local/sensor.py:349:16: W0104: Statement seems to have no effect (pointless-statement) homeassistant/components/solaredge_local/sensor.py:356:16: W0104: Statement seems to have no effect (pointless-statement) * Rename import energy sensor * Insert feadback * Change to debug info * Add check if attribute name exist * Remove unnecessary else * Add return None if no attributes * flake --- .../components/solaredge_local/sensor.py | 133 +++++++++++++++++- 1 file changed, 126 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index ce51efa07caff9..917fb86ddcb776 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -24,63 +24,107 @@ DOMAIN = "solaredge_local" UPDATE_DELAY = timedelta(seconds=10) +INVERTER_MODES = ( + "SHUTTING_DOWN", + "ERROR", + "STANDBY", + "PAIRING", + "POWER_PRODUCTION", + "AC_CHARGING", + "NOT_PAIRED", + "NIGHT_MODE", + "GRID_MONITORING", + "IDLE", +) + # Supported sensor types: -# Key: ['json_key', 'name', unit, icon] +# Key: ['json_key', 'name', unit, icon, attribute name] SENSOR_TYPES = { - "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"], + "current_AC_voltage": ["gridvoltage", "Grid Voltage", "V", "mdi:current-ac", None], + "current_DC_voltage": ["dcvoltage", "DC Voltage", "V", "mdi:current-dc", None], + "current_frequency": [ + "gridfrequency", + "Grid Frequency", + "Hz", + "mdi:current-ac", + None, + ], + "current_power": [ + "currentPower", + "Current Power", + POWER_WATT, + "mdi:solar-power", + None, + ], "energy_this_month": [ "energyThisMonth", - "Energy this month", + "Energy This Month", ENERGY_WATT_HOUR, "mdi:solar-power", + None, ], "energy_this_year": [ "energyThisYear", - "Energy this year", + "Energy This Year", ENERGY_WATT_HOUR, "mdi:solar-power", + None, ], "energy_today": [ "energyToday", - "Energy today", + "Energy Today", ENERGY_WATT_HOUR, "mdi:solar-power", + None, ], "inverter_temperature": [ "invertertemperature", "Inverter Temperature", TEMP_CELSIUS, "mdi:thermometer", + "operating_mode", ], "lifetime_energy": [ "energyTotal", - "Lifetime energy", + "Lifetime Energy", ENERGY_WATT_HOUR, "mdi:solar-power", + None, + ], + "optimizer_connected": [ + "optimizers", + "Optimizers Online", + "optimizers", + "mdi:solar-panel", + "optimizers_connected", ], "optimizer_current": [ "optimizercurrent", "Average Optimizer Current", "A", "mdi:solar-panel", + None, ], "optimizer_power": [ "optimizerpower", "Average Optimizer Power", POWER_WATT, "mdi:solar-panel", + None, ], "optimizer_temperature": [ "optimizertemperature", "Average Optimizer Temperature", TEMP_CELSIUS, "mdi:solar-panel", + None, ], "optimizer_voltage": [ "optimizervoltage", "Average Optimizer Voltage", "V", "mdi:solar-panel", + None, ], } @@ -122,8 +166,48 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "Inverter Temperature", TEMP_FAHRENHEIT, "mdi:thermometer", + "operating_mode", + None, ] + try: + if status.metersList[0]: + sensors["import_current_power"] = [ + "currentPowerimport", + "current import Power", + POWER_WATT, + "mdi:arrow-collapse-down", + None, + ] + sensors["import_meter_reading"] = [ + "totalEnergyimport", + "total import Energy", + ENERGY_WATT_HOUR, + "mdi:counter", + None, + ] + except IndexError: + _LOGGER.debug("Import meter sensors are not created") + + try: + if status.metersList[1]: + sensors["export_current_power"] = [ + "currentPowerexport", + "current export Power", + POWER_WATT, + "mdi:arrow-expand-up", + None, + ] + sensors["export_meter_reading"] = [ + "totalEnergyexport", + "total export Energy", + ENERGY_WATT_HOUR, + "mdi:counter", + None, + ] + except IndexError: + _LOGGER.debug("Export meter sensors are not created") + # Create solaredge data service which will retrieve and update the data. data = SolarEdgeData(hass, api) @@ -137,6 +221,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_info[1], sensor_info[2], sensor_info[3], + sensor_info[4], ) entities.append(sensor) @@ -146,7 +231,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SolarEdgeSensor(Entity): """Representation of an SolarEdge Monitoring API sensor.""" - def __init__(self, platform_name, data, json_key, name, unit, icon): + def __init__(self, platform_name, data, json_key, name, unit, icon, attr): """Initialize the sensor.""" self._platform_name = platform_name self._data = data @@ -156,6 +241,7 @@ def __init__(self, platform_name, data, json_key, name, unit, icon): self._name = name self._unit_of_measurement = unit self._icon = icon + self._attr = attr @property def name(self): @@ -167,6 +253,16 @@ def unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._attr: + try: + return {self._attr: self._data.info[self._json_key]} + except KeyError: + return None + return None + @property def icon(self): """Return the sensor icon.""" @@ -191,6 +287,7 @@ def __init__(self, hass, api): self.hass = hass self.api = api self.data = {} + self.info = {} @Throttle(UPDATE_DELAY) def update(self): @@ -243,6 +340,28 @@ def update(self): self.data["invertertemperature"] = round( status.inverters.primary.temperature.value, 2 ) + self.data["dcvoltage"] = round(status.inverters.primary.voltage, 2) + self.data["gridfrequency"] = round(status.frequencyHz, 2) + self.data["gridvoltage"] = round(status.voltage, 2) + self.data["optimizers"] = status.optimizersStatus.online + + self.info["optimizers"] = status.optimizersStatus.total + self.info["invertertemperature"] = INVERTER_MODES[status.status] + + try: + if status.metersList[1]: + self.data["currentPowerimport"] = status.metersList[1].currentPower + self.data["totalEnergyimport"] = status.metersList[1].totalEnergy + except IndexError: + pass + + try: + if status.metersList[0]: + self.data["currentPowerexport"] = status.metersList[0].currentPower + self.data["totalEnergyexport"] = status.metersList[0].totalEnergy + except IndexError: + pass + if maintenance.system.name: self.data["optimizertemperature"] = round(statistics.mean(temperature), 2) self.data["optimizervoltage"] = round(statistics.mean(voltage), 2) From dcdcfdd376f0657949592da57cb04e7710be02cf Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:22:16 +0200 Subject: [PATCH 0988/3953] Unload linky config entry (#27831) --- homeassistant/components/linky/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/linky/__init__.py b/homeassistant/components/linky/__init__.py index a7f3d7bb03e9f5..ad5b6743d37d53 100644 --- a/homeassistant/components/linky/__init__.py +++ b/homeassistant/components/linky/__init__.py @@ -47,9 +47,15 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Linky sensors.""" - hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "sensor") ) + return True + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload Linky sensors.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_unload(entry, "sensor") + ) return True From 86a4be16367b6944c6ef0736d4461de1c5cc05fd Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 18 Oct 2019 02:22:40 +0200 Subject: [PATCH 0989/3953] Fix attribution (#27815) --- homeassistant/components/airly/sensor.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 03439d7d206f62..bce32d640416cb 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -1,5 +1,6 @@ """Support for the Airly sensor service.""" from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_NAME, DEVICE_CLASS_HUMIDITY, @@ -93,7 +94,7 @@ def __init__(self, airly, name, kind): self._state = None self._icon = None self._unit_of_measurement = None - self._attrs = {} + self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} @property def name(self): @@ -136,11 +137,6 @@ def unit_of_measurement(self): """Return the unit the value is expressed in.""" return SENSOR_TYPES[self.kind][ATTR_UNIT] - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION - @property def available(self): """Return True if entity is available.""" From 81178661aef2636cfab18a8f1709c907861cd34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiit=20R=C3=A4tsep?= Date: Fri, 18 Oct 2019 03:23:11 +0300 Subject: [PATCH 0990/3953] Added handling for connection errors in state update, added available property (#27794) --- homeassistant/components/soma/__init__.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 5bf51e743e9c1a..b4daa28b5b2595 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -3,6 +3,7 @@ import voluptuous as vol from api.soma_api import SomaApi +from requests import RequestException import homeassistant.helpers.config_validation as cv from homeassistant import config_entries @@ -75,6 +76,12 @@ def __init__(self, device, api): self.device = device self.api = api self.current_position = 50 + self.is_available = True + + @property + def available(self): + """Return true if the last API commands returned successfully.""" + return self.is_available @property def unique_id(self): @@ -100,12 +107,19 @@ def device_info(self): async def async_update(self): """Update the device with the latest data.""" - response = await self.hass.async_add_executor_job( - self.api.get_shade_state, self.device["mac"] - ) + try: + response = await self.hass.async_add_executor_job( + self.api.get_shade_state, self.device["mac"] + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + self.is_available = False + return if response["result"] != "success": _LOGGER.error( "Unable to reach device %s (%s)", self.device["name"], response["msg"] ) + self.is_available = False return self.current_position = 100 - response["position"] + self.is_available = True From 564789470e9bf7533c488d6f1fd3a61608fe0f96 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 18 Oct 2019 02:25:16 +0200 Subject: [PATCH 0991/3953] Add device_info to HomematicIP climate and acp (#27771) --- .../homematicip_cloud/alarm_control_panel.py | 11 +++++++++++ .../components/homematicip_cloud/climate.py | 14 ++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index bb5999108ce012..653c1ae147bc03 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -59,6 +59,17 @@ def __init__(self, hap: HomematicipHAP, security_zones) -> None: else: self._external_alarm_zone = security_zone + @property + def device_info(self): + """Return device specific attributes.""" + return { + "identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")}, + "name": self.name, + "manufacturer": "eQ-3", + "model": CONST_ALARM_CONTROL_PANEL_NAME, + "via_device": (HMIPC_DOMAIN, self._home.id), + } + @property def state(self) -> str: """Return the state of the device.""" diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index cf1c1baabe0229..b8c055dda1fe64 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -56,12 +56,23 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize heating group.""" - device.modelType = "Group-Heating" + device.modelType = "HmIP-Heating-Group" self._simple_heating = None if device.actualTemperature is None: self._simple_heating = _get_first_heating_thermostat(device) super().__init__(hap, device) + @property + def device_info(self): + """Return device specific attributes.""" + return { + "identifiers": {(HMIPC_DOMAIN, self._device.id)}, + "name": self._device.label, + "manufacturer": "eQ-3", + "model": self._device.modelType, + "via_device": (HMIPC_DOMAIN, self._device.homeId), + } + @property def temperature_unit(self) -> str: """Return the unit of measurement.""" @@ -176,4 +187,3 @@ def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): for device in heating_group.devices: if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): return device - return None From 0888098718d5afa0ddcd2a7dcb81b1573109fb55 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 17 Oct 2019 19:31:53 -0500 Subject: [PATCH 0992/3953] Use URI provided by Plex for local connections (#27515) * Use provided URI for local connections * Use provided plexapi connection method * Remove unused mock from tests * Handle potential edge case(s) --- homeassistant/components/plex/config_flow.py | 5 +++-- homeassistant/components/plex/server.py | 18 +++++++--------- tests/components/plex/mock_classes.py | 22 -------------------- 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 38727ccff067f4..9e74756977d4cf 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -89,9 +89,10 @@ async def async_step_server_validate(self, server_config): _LOGGER.error("Invalid credentials provided, config not created") errors["base"] = "faulty_credentials" except (plexapi.exceptions.NotFound, requests.exceptions.ConnectionError): - _LOGGER.error( - "Plex server could not be reached: %s", server_config[CONF_URL] + server_identifier = ( + server_config.get(CONF_URL) or plex_server.server_choice or "Unknown" ) + _LOGGER.error("Plex server could not be reached: %s", server_identifier) errors["base"] = "not_found" except ServerNotSpecified as available_servers: diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 6ab114307664b3..d9ddc28c89a851 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -39,11 +39,12 @@ def __init__(self, server_config, options=None): self._server_name = server_config.get(CONF_SERVER) self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) self.options = options + self.server_choice = None def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" - def _set_missing_url(): + def _connect_with_token(): account = plexapi.myplex.MyPlexAccount(token=self._token) available_servers = [ (x.name, x.clientIdentifier) @@ -56,13 +57,10 @@ def _set_missing_url(): if not self._server_name and len(available_servers) > 1: raise ServerNotSpecified(available_servers) - server_choice = ( + self.server_choice = ( self._server_name if self._server_name else available_servers[0][0] ) - connections = account.resource(server_choice).connections - local_url = [x.httpuri for x in connections if x.local] - remote_url = [x.uri for x in connections if not x.local] - self._url = local_url[0] if local_url else remote_url[0] + self._plex_server = account.resource(self.server_choice).connect() def _connect_with_url(): session = None @@ -73,10 +71,10 @@ def _connect_with_url(): self._url, self._token, session ) - if self._token and not self._url: - _set_missing_url() - - _connect_with_url() + if self._url: + _connect_with_url() + else: + _connect_with_token() def clients(self): """Pass through clients call to plexapi.""" diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 756249110ed962..69e6a84df63622 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -29,34 +29,12 @@ def __init__(self, index): ] self.provides = ["server"] self._mock_plex_server = MockPlexServer(index) - self._connections = [] - for connection in range(2): - self._connections.append(MockConnection(connection)) - - @property - def connections(self): - """Mock the resource connection listing method.""" - return self._connections def connect(self): """Mock the resource connect method.""" return self._mock_plex_server -class MockConnection: # pylint: disable=too-few-public-methods - """Mock a single account resource connection object.""" - - def __init__(self, index, ssl=True): - """Initialize the object.""" - prefix = "https" if ssl else "http" - self.httpuri = ( - f"http://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" - ) - self.uri = f"{prefix}://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" - # Only first server is local - self.local = not bool(index) - - class MockPlexAccount: """Mock a PlexAccount instance.""" From 489340160b68923767fc4e60cd21a76310f1d8ee Mon Sep 17 00:00:00 2001 From: mvn23 Date: Fri, 18 Oct 2019 02:36:34 +0200 Subject: [PATCH 0993/3953] Add opentherm_gw options flow. (#27316) --- .../opentherm_gw/.translations/en.json | 11 ++++ .../components/opentherm_gw/__init__.py | 9 ++++ .../components/opentherm_gw/climate.py | 19 +++++-- .../components/opentherm_gw/config_flow.py | 53 +++++++++++++++++- .../components/opentherm_gw/strings.json | 15 ++++-- .../opentherm_gw/test_config_flow.py | 54 +++++++++++++++++-- 6 files changed, 149 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json index 65d7d9e92bb1bd..4aba4ed047af4c 100644 --- a/homeassistant/components/opentherm_gw/.translations/en.json +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "description": "Options for the OpenTherm Gateway", + "data": { + "floor_temperature": "Floor Temperature", + "precision": "Precision" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index ba6de4c0bea6ee..643f80ae8f9db5 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -75,6 +75,12 @@ ) +async def options_updated(hass, entry): + """Handle options update.""" + gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]] + async_dispatcher_send(hass, gateway.options_update_signal, entry) + + async def async_setup_entry(hass, config_entry): """Set up the OpenTherm Gateway component.""" if DATA_OPENTHERM_GW not in hass.data: @@ -83,6 +89,8 @@ async def async_setup_entry(hass, config_entry): gateway = OpenThermGatewayDevice(hass, config_entry) hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] = gateway + config_entry.add_update_listener(options_updated) + # Schedule directly on the loop to avoid blocking HA startup. hass.loop.create_task(gateway.connect_and_subscribe()) @@ -348,6 +356,7 @@ def __init__(self, hass, config_entry): self.climate_config = config_entry.options self.status = {} self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" + self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update" self.gateway = pyotgw.pyotgw() async def connect_and_subscribe(self): diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 19763121e89d64..44f143d64da24a 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -39,7 +39,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ents = [] ents.append( OpenThermClimate( - hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], + config_entry.options, ) ) @@ -49,12 +50,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class OpenThermClimate(ClimateDevice): """Representation of a climate device.""" - def __init__(self, gw_dev): + def __init__(self, gw_dev, options): """Initialize the device.""" self._gateway = gw_dev self.friendly_name = gw_dev.name - self.floor_temp = gw_dev.climate_config.get(CONF_FLOOR_TEMP) - self.temp_precision = gw_dev.climate_config.get(CONF_PRECISION) + self.floor_temp = options[CONF_FLOOR_TEMP] + self.temp_precision = options.get(CONF_PRECISION) self._current_operation = None self._current_temperature = None self._hvac_mode = HVAC_MODE_HEAT @@ -65,12 +66,22 @@ def __init__(self, gw_dev): self._away_state_a = False self._away_state_b = False + @callback + def update_options(self, entry): + """Update climate entity options.""" + self.floor_temp = entry.options[CONF_FLOOR_TEMP] + self.temp_precision = entry.options.get(CONF_PRECISION) + self.async_schedule_update_ha_state() + async def async_added_to_hass(self): """Connect to the OpenTherm Gateway device.""" _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name) async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) + async_dispatcher_connect( + self.hass, self._gateway.options_update_signal, self.update_options + ) @callback def receive_report(self, status): diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index e1b68f1ae4903b..2d7a65bbd845fa 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -6,11 +6,20 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME +from homeassistant.const import ( + CONF_DEVICE, + CONF_ID, + CONF_NAME, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from . import DOMAIN +from .const import CONF_FLOOR_TEMP, CONF_PRECISION class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -19,6 +28,12 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OpenThermGwOptionsFlow(config_entry) + async def async_step_init(self, info=None): """Handle config flow initiation.""" if info: @@ -89,3 +104,39 @@ def _create_entry(self, gw_id, name, device): return self.async_create_entry( title=name, data={CONF_ID: gw_id, CONF_DEVICE: device, CONF_NAME: name} ) + + +class OpenThermGwOptionsFlow(config_entries.OptionsFlow): + """Handle opentherm_gw options.""" + + def __init__(self, config_entry): + """Initialize the options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the opentherm_gw options.""" + if user_input is not None: + if user_input.get(CONF_PRECISION) == 0: + user_input[CONF_PRECISION] = None + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_PRECISION, + default=self.config_entry.options.get(CONF_PRECISION, 0), + ): vol.All( + vol.Coerce(float), + vol.In( + [0, PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] + ), + ), + vol.Optional( + CONF_FLOOR_TEMP, + default=self.config_entry.options.get(CONF_FLOOR_TEMP, False), + ): bool, + } + ), + ) diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index a62a462504954c..1c246432fb1438 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -7,9 +7,7 @@ "data": { "name": "Name", "device": "Path or URL", - "id": "ID", - "precision": "Climate temperature precision", - "floor_temperature": "Floor climate temperature" + "id": "ID" } } }, @@ -19,5 +17,16 @@ "serial_error": "Error connecting to device", "timeout": "Connection attempt timed out" } + }, + "options": { + "step": { + "init": { + "description": "Options for the OpenTherm Gateway", + "data": { + "floor_temperature": "Floor Temperature", + "precision": "Precision" + } + } + } } } \ No newline at end of file diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index da80e2f9fbb74e..89f2783cf71f28 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -3,12 +3,16 @@ from serial import SerialException from unittest.mock import patch -from homeassistant import config_entries, setup -from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME -from homeassistant.components.opentherm_gw.const import DOMAIN +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES +from homeassistant.components.opentherm_gw.const import ( + DOMAIN, + CONF_FLOOR_TEMP, + CONF_PRECISION, +) from pyotgw import OTGW_ABOUT -from tests.common import mock_coro +from tests.common import mock_coro, MockConfigEntry async def test_form_user(hass): @@ -161,3 +165,45 @@ async def test_form_connection_error(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "serial_error"} assert len(mock_connect.mock_calls) == 1 + + +async def test_options_form(hass): + """Test the options form.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Mock Gateway", + data={ + CONF_NAME: "Mock Gateway", + CONF_DEVICE: "/dev/null", + CONF_ID: "mock_gateway", + }, + options={}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.flow.async_init( + entry.entry_id, context={"source": "test"}, data=None + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], + user_input={CONF_FLOOR_TEMP: True, CONF_PRECISION: PRECISION_HALVES}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PRECISION] == PRECISION_HALVES + assert result["data"][CONF_FLOOR_TEMP] is True + + result = await hass.config_entries.options.flow.async_init( + entry.entry_id, context={"source": "test"}, data=None + ) + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], user_input={CONF_PRECISION: 0} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PRECISION] is None + assert result["data"][CONF_FLOOR_TEMP] is True From 4e25807b7d3c330778b08527c8cdaee059c85498 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 17 Oct 2019 20:51:27 -0400 Subject: [PATCH 0994/3953] Add ability for MQTT device tracker to map non-default topic payloads to zones/states (#27143) * add ability for MQTT device tracker to map nondefault topic payloads to zones * update new parameter name and add abbreviation * support for payload_home, payload_not_home, and payload_custom * use constants STATE_NOT_HOME and STATE_HOME as defaults * reference state constants directly * add empty dict as default for payload_custom * change parameter name for custom mapping of payloads to non-home zones to be more descriptive * removed 'payload_other_zones' per ballobs review * remove abbreviation for 'payload_other_zones' * add tests for feature --- .../components/mqtt/abbreviations.py | 2 + .../components/mqtt/device_tracker.py | 24 ++++++- tests/components/mqtt/test_device_tracker.py | 64 ++++++++++++++++++- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 2350dfc66344e5..5a5ed4555db536 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -88,11 +88,13 @@ "pl_cls": "payload_close", "pl_disarm": "payload_disarm", "pl_hi_spd": "payload_high_speed", + "pl_home": "payload_home", "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_not_home": "payload_not_home", "pl_off": "payload_off", "pl_off_spd": "payload_off_speed", "pl_on": "payload_on", diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index e9613e09a95cc5..c9cce3ebeda029 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -5,16 +5,23 @@ from homeassistant.components import mqtt from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_DEVICES from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_DEVICES, STATE_NOT_HOME, STATE_HOME from . import CONF_QOS _LOGGER = logging.getLogger(__name__) +CONF_PAYLOAD_HOME = "payload_home" +CONF_PAYLOAD_NOT_HOME = "payload_not_home" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( - {vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}} + { + vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, + vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, + vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, + } ) @@ -22,13 +29,24 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] + payload_home = config[CONF_PAYLOAD_HOME] + payload_not_home = config[CONF_PAYLOAD_NOT_HOME] for dev_id, topic in devices.items(): @callback def async_message_received(msg, dev_id=dev_id): """Handle received MQTT message.""" - hass.async_create_task(async_see(dev_id=dev_id, location_name=msg.payload)) + if msg.payload == payload_home: + location_name = STATE_HOME + elif msg.payload == payload_not_home: + location_name = STATE_NOT_HOME + else: + location_name = msg.payload + + hass.async_create_task( + async_see(dev_id=dev_id, location_name=location_name) + ) await mqtt.async_subscribe(hass, topic, async_message_received, qos) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index caad12b3e39eb5..14180d2dcf9b95 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -4,7 +4,7 @@ from homeassistant.components import device_tracker from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component from tests.common import async_fire_mqtt_message @@ -156,3 +156,65 @@ async def test_multi_level_wildcard_topic_not_matching(hass, mock_device_tracker async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() assert hass.states.get(entity_id) is None + + +async def test_matching_custom_payload_for_home_and_not_home( + hass, mock_device_tracker_conf +): + """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home.""" + dev_id = "paulus" + entity_id = ENTITY_ID_FORMAT.format(dev_id) + topic = "/location/paulus" + payload_home = "present" + payload_not_home = "not present" + + hass.config.components = set(["mqtt", "zone"]) + assert await async_setup_component( + hass, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: topic}, + "payload_home": payload_home, + "payload_not_home": payload_not_home, + } + }, + ) + async_fire_mqtt_message(hass, topic, payload_home) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_HOME + + async_fire_mqtt_message(hass, topic, payload_not_home) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_NOT_HOME + + +async def test_not_matching_custom_payload_for_home_and_not_home( + hass, mock_device_tracker_conf +): + """Test not matching payload does not set state to home or not_home.""" + dev_id = "paulus" + entity_id = ENTITY_ID_FORMAT.format(dev_id) + topic = "/location/paulus" + payload_home = "present" + payload_not_home = "not present" + payload_not_matching = "test" + + hass.config.components = set(["mqtt", "zone"]) + assert await async_setup_component( + hass, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: topic}, + "payload_home": payload_home, + "payload_not_home": payload_not_home, + } + }, + ) + async_fire_mqtt_message(hass, topic, payload_not_matching) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state != STATE_HOME + assert hass.states.get(entity_id).state != STATE_NOT_HOME From 2bc6b59e79ba773999334595cd5f117959a94860 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Fri, 18 Oct 2019 06:32:24 +0300 Subject: [PATCH 0995/3953] Move holiday info into a single sensor with multiple attributess (#27654) * Move holiday onfo into a single sensor with multiple attributess * Add tests for holiday attributes --- .../components/jewish_calendar/__init__.py | 3 +- .../components/jewish_calendar/manifest.json | 2 +- .../components/jewish_calendar/sensor.py | 15 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/jewish_calendar/test_sensor.py | 78 +++++++++---------- 6 files changed, 52 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index c7bbbdb2d907a9..bbe0c1d24fd5f9 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -20,8 +20,7 @@ "data": { "date": ["Date", "mdi:judaism"], "weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"], - "holiday_name": ["Holiday name", "mdi:calendar-star"], - "holiday_type": ["Holiday type", "mdi:counter"], + "holiday": ["Holiday", "mdi:calendar-star"], "omer_count": ["Day of the Omer", "mdi:counter"], }, "time": { diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 7b6653ba832141..08182daedd05a0 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/integrations/jewish_calendar", "requirements": [ - "hdate==0.9.0" + "hdate==0.9.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 453b3de4bae31e..54a3d1497aacdd 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -44,6 +44,7 @@ def __init__(self, data, sensor, sensor_info): self._havdalah_offset = data["havdalah_offset"] self._diaspora = data["diaspora"] self._state = None + self._holiday_attrs = {} @property def name(self): @@ -103,6 +104,14 @@ def make_zmanim(self, date): hebrew=self._hebrew, ) + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._type == "holiday": + return self._holiday_attrs + + return {} + def get_state(self, after_shkia_date, after_tzais_date): """For a given type of sensor, return the state.""" # Terminology note: by convention in py-libhdate library, "upcoming" @@ -112,10 +121,10 @@ def get_state(self, after_shkia_date, after_tzais_date): if self._type == "weekly_portion": # Compute the weekly portion based on the upcoming shabbat. return after_tzais_date.upcoming_shabbat.parasha - if self._type == "holiday_name": + if self._type == "holiday": + self._holiday_attrs["type"] = after_shkia_date.holiday_type.name + self._holiday_attrs["id"] = after_shkia_date.holiday_name return after_shkia_date.holiday_description - if self._type == "holiday_type": - return after_shkia_date.holiday_type if self._type == "omer_count": return after_shkia_date.omer_day diff --git a/requirements_all.txt b/requirements_all.txt index 6b8b1931a4af69..df24fde50cfae1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -619,7 +619,7 @@ hass-nabucasa==0.22 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.0 +hdate==0.9.1 # homeassistant.components.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ece529ef6e8e63..5b224126d67516 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -227,7 +227,7 @@ hass-nabucasa==0.22 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.0 +hdate==0.9.1 # homeassistant.components.here_travel_time herepy==0.6.3.1 diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 94b26f80d2dfce..07e0b7cb192aab 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -42,27 +42,17 @@ async def test_jewish_calendar_hebrew(hass): False, 'כ"ג אלול ה\' תשע"ח', ), - ( - dt(2018, 9, 10), - "UTC", - 31.778, - 35.235, - "hebrew", - "holiday_name", - False, - "א' ראש השנה", - ), + (dt(2018, 9, 10), "UTC", 31.778, 35.235, "hebrew", "holiday", False, "א' ראש השנה"), ( dt(2018, 9, 10), "UTC", 31.778, 35.235, "english", - "holiday_name", + "holiday", False, "Rosh Hashana I", ), - (dt(2018, 9, 10), "UTC", 31.778, 35.235, "english", "holiday_type", False, 1), ( dt(2018, 9, 8), "UTC", @@ -128,9 +118,8 @@ async def test_jewish_calendar_hebrew(hass): TEST_IDS = [ "date_output", "date_output_hebrew", - "holiday_name", - "holiday_name_english", - "holiday_type", + "holiday", + "holiday_english", "torah_reading", "first_stars_ny", "first_stars_jerusalem", @@ -187,7 +176,12 @@ async def test_jewish_calendar_sensor( dt_util.as_utc(time_zone.localize(result)) if isinstance(result, dt) else result ) - assert hass.states.get(f"sensor.test_{sensor}").state == str(result) + sensor_object = hass.states.get(f"sensor.test_{sensor}") + assert sensor_object.state == str(result) + + if sensor == "holiday": + assert sensor_object.attributes.get("type") == "YOM_TOV" + assert sensor_object.attributes.get("id") == "rosh_hashana_i" SHABBAT_PARAMS = [ @@ -256,8 +250,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", - "english_holiday_name": "Erev Rosh Hashana", - "hebrew_holiday_name": "ערב ראש השנה", + "english_holiday": "Erev Rosh Hashana", + "hebrew_holiday": "ערב ראש השנה", }, ), make_nyc_test_params( @@ -269,8 +263,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", - "english_holiday_name": "Rosh Hashana I", - "hebrew_holiday_name": "א' ראש השנה", + "english_holiday": "Rosh Hashana I", + "hebrew_holiday": "א' ראש השנה", }, ), make_nyc_test_params( @@ -282,8 +276,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", - "english_holiday_name": "Rosh Hashana II", - "hebrew_holiday_name": "ב' ראש השנה", + "english_holiday": "Rosh Hashana II", + "hebrew_holiday": "ב' ראש השנה", }, ), make_nyc_test_params( @@ -306,8 +300,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Hoshana Raba", - "hebrew_holiday_name": "הושענא רבה", + "english_holiday": "Hoshana Raba", + "hebrew_holiday": "הושענא רבה", }, ), make_nyc_test_params( @@ -319,8 +313,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Shmini Atzeret", - "hebrew_holiday_name": "שמיני עצרת", + "english_holiday": "Shmini Atzeret", + "hebrew_holiday": "שמיני עצרת", }, ), make_nyc_test_params( @@ -332,8 +326,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Simchat Torah", - "hebrew_holiday_name": "שמחת תורה", + "english_holiday": "Simchat Torah", + "hebrew_holiday": "שמחת תורה", }, ), make_jerusalem_test_params( @@ -345,8 +339,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Hoshana Raba", - "hebrew_holiday_name": "הושענא רבה", + "english_holiday": "Hoshana Raba", + "hebrew_holiday": "הושענא רבה", }, ), make_jerusalem_test_params( @@ -358,8 +352,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Shmini Atzeret", - "hebrew_holiday_name": "שמיני עצרת", + "english_holiday": "Shmini Atzeret", + "hebrew_holiday": "שמיני עצרת", }, ), make_jerusalem_test_params( @@ -382,8 +376,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": "unknown", "english_parshat_hashavua": "Bamidbar", "hebrew_parshat_hashavua": "במדבר", - "english_holiday_name": "Erev Shavuot", - "hebrew_holiday_name": "ערב שבועות", + "english_holiday": "Erev Shavuot", + "hebrew_holiday": "ערב שבועות", }, ), make_nyc_test_params( @@ -395,8 +389,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2016, 6, 18, 21, 19), "english_parshat_hashavua": "Nasso", "hebrew_parshat_hashavua": "נשא", - "english_holiday_name": "Shavuot", - "hebrew_holiday_name": "שבועות", + "english_holiday": "Shavuot", + "hebrew_holiday": "שבועות", }, ), make_jerusalem_test_params( @@ -408,8 +402,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", - "english_holiday_name": "Rosh Hashana I", - "hebrew_holiday_name": "א' ראש השנה", + "english_holiday": "Rosh Hashana I", + "hebrew_holiday": "א' ראש השנה", }, ), make_jerusalem_test_params( @@ -421,8 +415,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", - "english_holiday_name": "Rosh Hashana II", - "hebrew_holiday_name": "ב' ראש השנה", + "english_holiday": "Rosh Hashana II", + "hebrew_holiday": "ב' ראש השנה", }, ), make_jerusalem_test_params( @@ -434,8 +428,8 @@ async def test_jewish_calendar_sensor( "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", - "english_holiday_name": "", - "hebrew_holiday_name": "", + "english_holiday": "", + "hebrew_holiday": "", }, ), ] From 9625e0463b24667a06481649a65750ac4d3c1df9 Mon Sep 17 00:00:00 2001 From: Bendik Brenne Date: Fri, 18 Oct 2019 06:44:09 +0200 Subject: [PATCH 0996/3953] Add sinch integration (notify component) (#26502) * Added sinch integration (notify component) * Updated requirements * Fixes according to lint * Update homeassistant/components/sinch/notify.py Co-Authored-By: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/sinch/notify.py Co-Authored-By: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/sinch/notify.py Co-Authored-By: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> * Adds @bendikrb as codeowner * Imports to the top. Catching specific exceptions. Logic fixes * Updated CODEOWNERS * Reformatting (black) * Added sinch component to .coveragerc * Conform to pylintrc * Okay, Mr. Black * Fixed: Catching too general exception Exception --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/sinch/__init__.py | 1 + homeassistant/components/sinch/manifest.json | 12 +++ homeassistant/components/sinch/notify.py | 97 ++++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 115 insertions(+) create mode 100644 homeassistant/components/sinch/__init__.py create mode 100644 homeassistant/components/sinch/manifest.json create mode 100644 homeassistant/components/sinch/notify.py diff --git a/.coveragerc b/.coveragerc index 859e1c0f92cbae..389d289ea20be9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -604,6 +604,7 @@ omit = homeassistant/components/skybeacon/sensor.py homeassistant/components/skybell/* homeassistant/components/slack/notify.py + homeassistant/components/sinch/* homeassistant/components/slide/* homeassistant/components/sma/sensor.py homeassistant/components/smappee/* diff --git a/CODEOWNERS b/CODEOWNERS index 40f1e93cfb97b5..2f228105cbbbc3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -257,6 +257,7 @@ homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/simplisafe/* @bachya +homeassistant/components/sinch/* @bendikrb homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza homeassistant/components/smarthab/* @outadoc diff --git a/homeassistant/components/sinch/__init__.py b/homeassistant/components/sinch/__init__.py new file mode 100644 index 00000000000000..43a5f2b2a5c57e --- /dev/null +++ b/homeassistant/components/sinch/__init__.py @@ -0,0 +1 @@ +"""Component to integrate with Sinch SMS API.""" diff --git a/homeassistant/components/sinch/manifest.json b/homeassistant/components/sinch/manifest.json new file mode 100644 index 00000000000000..a1864428fee643 --- /dev/null +++ b/homeassistant/components/sinch/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "sinch", + "name": "Sinch", + "documentation": "https://www.home-assistant.io/components/sinch", + "dependencies": [], + "codeowners": [ + "@bendikrb" + ], + "requirements": [ + "clx-sdk-xms==1.0.0" + ] +} \ No newline at end of file diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py new file mode 100644 index 00000000000000..173873c0a6c2eb --- /dev/null +++ b/homeassistant/components/sinch/notify.py @@ -0,0 +1,97 @@ +"""Support for Sinch notifications.""" +import logging + +import voluptuous as vol +from clx.xms.api import MtBatchTextSmsResult +from clx.xms.client import Client +from clx.xms.exceptions import ( + ErrorResponseException, + UnexpectedResponseException, + UnauthorizedException, + NotFoundException, +) + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_MESSAGE, + ATTR_DATA, + ATTR_TARGET, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.const import CONF_API_KEY, CONF_SENDER + +DOMAIN = "sinch" + +CONF_SERVICE_PLAN_ID = "service_plan_id" +CONF_DEFAULT_RECIPIENTS = "default_recipients" + +ATTR_SENDER = CONF_SENDER + +DEFAULT_SENDER = "Home Assistant" + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_SERVICE_PLAN_ID): cv.string, + vol.Optional(CONF_SENDER, default=DEFAULT_SENDER): cv.string, + vol.Optional(CONF_DEFAULT_RECIPIENTS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } +) + + +def get_service(hass, config, discovery_info=None): + """Get the Sinch notification service.""" + return SinchNotificationService(config) + + +class SinchNotificationService(BaseNotificationService): + """Send Notifications to Sinch SMS recipients.""" + + def __init__(self, config): + """Initialize the service.""" + self.default_recipients = config[CONF_DEFAULT_RECIPIENTS] + self.sender = config[CONF_SENDER] + self.client = Client(config[CONF_SERVICE_PLAN_ID], config[CONF_API_KEY]) + + def send_message(self, message="", **kwargs): + """Send a message to a user.""" + targets = kwargs.get(ATTR_TARGET, self.default_recipients) + data = kwargs.get(ATTR_DATA, {}) + + clx_args = {ATTR_MESSAGE: message, ATTR_SENDER: self.sender} + + if ATTR_SENDER in data: + clx_args[ATTR_SENDER] = data[ATTR_SENDER] + + if not targets: + _LOGGER.error("At least 1 target is required") + return + + try: + for target in targets: + result: MtBatchTextSmsResult = self.client.create_text_message( + clx_args[ATTR_SENDER], target, clx_args[ATTR_MESSAGE] + ) + batch_id = result.batch_id + _LOGGER.debug( + 'Successfully sent SMS to "%s" (batch_id: %s)', target, batch_id + ) + except ErrorResponseException as ex: + _LOGGER.error( + "Caught ErrorResponseException. Response code: %d (%s)", + ex.error_code, + ex, + ) + except NotFoundException as ex: + _LOGGER.error("Caught NotFoundException (request URL: %s)", ex.url) + except UnauthorizedException as ex: + _LOGGER.error( + "Caught UnauthorizedException (service plan: %s)", ex.service_plan_id + ) + except UnexpectedResponseException as ex: + _LOGGER.error("Caught UnexpectedResponseException: %s", ex) diff --git a/requirements_all.txt b/requirements_all.txt index df24fde50cfae1..9fe8b6622de47c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -346,6 +346,9 @@ ciscosparkapi==0.4.2 # homeassistant.components.cppm_tracker clearpasspy==1.0.2 +# homeassistant.components.sinch +clx-sdk-xms==1.0.0 + # homeassistant.components.co2signal co2signal==0.4.2 From 7ec8384fa697dec9b152ada6662edb5d7f822b3e Mon Sep 17 00:00:00 2001 From: Tobias Efinger Date: Fri, 18 Oct 2019 07:11:51 +0200 Subject: [PATCH 0997/3953] Add service description for route53 integration (#27774) --- homeassistant/components/route53/services.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/route53/services.yaml b/homeassistant/components/route53/services.yaml index e69de29bb2d1d6..20dbfa77f7acb3 100644 --- a/homeassistant/components/route53/services.yaml +++ b/homeassistant/components/route53/services.yaml @@ -0,0 +1,2 @@ +update_records: + description: Trigger update of records. \ No newline at end of file From 5cb145f5881afa4d8232240f40df36c2c132b7f9 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 07:12:32 +0200 Subject: [PATCH 0998/3953] Move imports in openweathermap component (#27779) --- homeassistant/components/openweathermap/sensor.py | 7 ++----- homeassistant/components/openweathermap/weather.py | 11 ++++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 51dc92623f3db1..23f88f59aada40 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pyowm import OWM +from pyowm.exceptions.api_call_error import APICallError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -56,7 +58,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenWeatherMap sensor.""" - from pyowm import OWM if None in (hass.config.latitude, hass.config.longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") @@ -127,8 +128,6 @@ def device_state_attributes(self): def update(self): """Get the latest data from OWM and updates the states.""" - from pyowm.exceptions.api_call_error import APICallError - try: self.owa_client.update() except APICallError: @@ -201,8 +200,6 @@ def __init__(self, owm, forecast, latitude, longitude): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from OpenWeatherMap.""" - from pyowm.exceptions.api_call_error import APICallError - try: obs = self.owm.weather_at_coords(self.latitude, self.longitude) except (APICallError, TypeError): diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index a51ea26607da3a..69ca965d660e24 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pyowm import OWM +from pyowm.exceptions.api_call_error import APICallError import voluptuous as vol from homeassistant.components.weather import ( @@ -71,7 +73,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenWeatherMap weather platform.""" - import pyowm longitude = config.get(CONF_LONGITUDE, round(hass.config.longitude, 5)) latitude = config.get(CONF_LATITUDE, round(hass.config.latitude, 5)) @@ -79,8 +80,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): mode = config.get(CONF_MODE) try: - owm = pyowm.OWM(config.get(CONF_API_KEY)) - except pyowm.exceptions.api_call_error.APICallError: + owm = OWM(config.get(CONF_API_KEY)) + except APICallError: _LOGGER.error("Error while connecting to OpenWeatherMap") return False @@ -225,8 +226,6 @@ def calc_precipitation(rain, snow): def update(self): """Get the latest data from OWM and updates the states.""" - from pyowm.exceptions.api_call_error import APICallError - try: self._owm.update() self._owm.update_forecast() @@ -263,8 +262,6 @@ def update(self): @Throttle(MIN_TIME_BETWEEN_FORECAST_UPDATES) def update_forecast(self): """Get the latest forecast from OpenWeatherMap.""" - from pyowm.exceptions.api_call_error import APICallError - try: if self._mode == "daily": fcd = self.owm.daily_forecast_at_coords( From 511766cb065dcf3ae5e487a3e0f3b7e1a1d52148 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 07:13:29 +0200 Subject: [PATCH 0999/3953] Move imports in apns component (#27804) * Move imports in apns component * fixed apns tests --- homeassistant/components/apns/notify.py | 17 ++++++++--------- tests/components/apns/test_notify.py | 8 ++++---- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index dbd45013a3ce95..c24c9cc16052ae 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -1,14 +1,11 @@ """APNS Notification platform.""" import logging +from apns2.client import APNsClient +from apns2.errors import Unregistered +from apns2.payload import Payload import voluptuous as vol -from homeassistant.config import load_yaml_config_file -from homeassistant.const import ATTR_NAME, CONF_NAME, CONF_PLATFORM -from homeassistant.helpers import template as template_helper -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_state_change - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -16,6 +13,11 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.config import load_yaml_config_file +from homeassistant.const import ATTR_NAME, CONF_NAME, CONF_PLATFORM +from homeassistant.helpers import template as template_helper +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_state_change APNS_DEVICES = "apns.yaml" CONF_CERTFILE = "cert_file" @@ -213,9 +215,6 @@ def register(self, call): def send_message(self, message=None, **kwargs): """Send push message to registered devices.""" - from apns2.client import APNsClient - from apns2.payload import Payload - from apns2.errors import Unregistered apns = APNsClient( self.certificate, use_sandbox=self.sandbox, use_alternative_port=False diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py index a4202f74d39a3e..78f597c58adef4 100644 --- a/tests/components/apns/test_notify.py +++ b/tests/components/apns/test_notify.py @@ -239,7 +239,7 @@ def fake_write(_out, device): assert "tracking123" == test_device_1.tracking_device_id assert "tracking456" == test_device_2.tracking_device_id - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") def test_send(self, mock_client): """Test updating an existing device.""" send = mock_client.return_value.send_notification @@ -274,7 +274,7 @@ def test_send(self, mock_client): assert "test.mp3" == payload.sound assert "testing" == payload.category - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") def test_send_when_disabled(self, mock_client): """Test updating an existing device.""" send = mock_client.return_value.send_notification @@ -299,7 +299,7 @@ def test_send_when_disabled(self, mock_client): assert not send.called - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") def test_send_with_state(self, mock_client): """Test updating an existing device.""" send = mock_client.return_value.send_notification @@ -334,7 +334,7 @@ def test_send_with_state(self, mock_client): assert "5678" == target assert "Hello" == payload.alert - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") @patch("homeassistant.components.apns.notify._write_device") def test_disable_when_unregistered(self, mock_write, mock_client): """Test disabling a device when it is unregistered.""" From 675c91b436341d4ac97b1382e4649adce5ae2066 Mon Sep 17 00:00:00 2001 From: Tomasz Jagusz Date: Fri, 18 Oct 2019 10:11:53 +0200 Subject: [PATCH 1000/3953] Move imports in yweather (#27842) Changes checked with pylint. Formatted with black Imports sorted with isort --- homeassistant/components/yweather/sensor.py | 19 +++++++++++-------- homeassistant/components/yweather/weather.py | 12 ++++++------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/yweather/sensor.py b/homeassistant/components/yweather/sensor.py index 4dc236998724ee..c7f752a88367d9 100644 --- a/homeassistant/components/yweather/sensor.py +++ b/homeassistant/components/yweather/sensor.py @@ -1,17 +1,23 @@ """Support for the Yahoo! Weather service.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from yahooweather import ( # pylint: disable=import-error + UNIT_C, + UNIT_F, + YahooWeather, + get_woeid, +) -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_CELSIUS, + ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME, - ATTR_ATTRIBUTION, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -20,6 +26,7 @@ ATTRIBUTION = "Weather details provided by Yahoo! Inc." CONF_FORECAST = "forecast" + CONF_WOEID = "woeid" DEFAULT_NAME = "Yweather" @@ -52,8 +59,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yahoo! weather sensor.""" - from yahooweather import get_woeid, UNIT_C, UNIT_F - unit = hass.config.units.temperature_unit woeid = config.get(CONF_WOEID) forecast = config.get(CONF_FORECAST) @@ -181,8 +186,6 @@ class YahooWeatherData: def __init__(self, woeid, temp_unit): """Initialize the data object.""" - from yahooweather import YahooWeather - self._yahoo = YahooWeather(woeid, temp_unit) @property diff --git a/homeassistant/components/yweather/weather.py b/homeassistant/components/yweather/weather.py index 6779fd1896dc40..202124aa340cb9 100644 --- a/homeassistant/components/yweather/weather.py +++ b/homeassistant/components/yweather/weather.py @@ -3,6 +3,12 @@ import logging import voluptuous as vol +from yahooweather import ( # pylint: disable=import-error + UNIT_C, + UNIT_F, + YahooWeather, + get_woeid, +) from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -21,7 +27,6 @@ ATTRIBUTION = "Weather details provided by Yahoo! Inc." - CONF_WOEID = "woeid" DEFAULT_NAME = "Yweather" @@ -46,7 +51,6 @@ "exceptional": [0, 1, 2, 19, 22], } - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_WOEID): cv.string, @@ -57,8 +61,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yahoo! weather platform.""" - from yahooweather import get_woeid, UNIT_C, UNIT_F - unit = hass.config.units.temperature_unit woeid = config.get(CONF_WOEID) name = config.get(CONF_NAME) @@ -181,8 +183,6 @@ class YahooWeatherData: def __init__(self, woeid, temp_unit): """Initialize the data object.""" - from yahooweather import YahooWeather - self._yahoo = YahooWeather(woeid, temp_unit) @property From 58d2d858cd43758b554e4f081420dcdae577e4e6 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 15:08:40 +0200 Subject: [PATCH 1001/3953] Move imports in brunt component (#27856) --- homeassistant/components/brunt/cover.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index af809cc78784f4..7d4279cf5b207a 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -2,17 +2,18 @@ import logging +from brunt import BruntAPI import voluptuous as vol -from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME from homeassistant.components.cover import ( ATTR_POSITION, - CoverDevice, PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDevice, ) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -36,7 +37,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the brunt platform.""" # pylint: disable=no-name-in-module - from brunt import BruntAPI username = config[CONF_USERNAME] password = config[CONF_PASSWORD] From c0d084fb04c1a5dcced64e018add3c619a05485a Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 15:12:00 +0200 Subject: [PATCH 1002/3953] Move imports in blockchain component (#27852) --- homeassistant/components/blockchain/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index c95ccb3fed3517..6d17484bdd7f43 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -1,12 +1,13 @@ """Support for Blockchain.info sensors.""" -import logging from datetime import timedelta +import logging +from pyblockchain import get_balance, validate_address import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -31,7 +32,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Blockchain.info sensors.""" - from pyblockchain import validate_address addresses = config.get(CONF_ADDRESSES) name = config.get(CONF_NAME) @@ -81,6 +81,5 @@ def device_state_attributes(self): def update(self): """Get the latest state of the sensor.""" - from pyblockchain import get_balance self._state = get_balance(self.addresses) From 3bb46d5080479980361ee6837b188b3ce9ed4a0e Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 15:12:36 +0200 Subject: [PATCH 1003/3953] Move blackbird imports (#27849) * Move imports in blackbird component * fixed tests after import move to top level --- homeassistant/components/blackbird/media_player.py | 7 +++---- tests/components/blackbird/test_media_player.py | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index eca7fa84f504fe..e1aa7200c07394 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -2,9 +2,11 @@ import logging import socket +from pyblackbird import get_blackbird +from serial import SerialException import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( DOMAIN, SUPPORT_SELECT_SOURCE, @@ -72,9 +74,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = config.get(CONF_PORT) host = config.get(CONF_HOST) - from pyblackbird import get_blackbird - from serial import SerialException - connection = None if port is not None: try: diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index c41d5dcef41e0f..34309fdbcf30bc 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -180,7 +180,10 @@ def setUp(self): self.hass = tests.common.get_test_home_assistant() self.hass.start() # Note, source dictionary is unsorted! - with mock.patch("pyblackbird.get_blackbird", new=lambda *a: self.blackbird): + with mock.patch( + "homeassistant.components.blackbird.media_player.get_blackbird", + new=lambda *a: self.blackbird, + ): setup_platform( self.hass, { From 9392cbff03b65539b9eb1dd5a0912b636acf08b3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 18 Oct 2019 16:11:40 +0200 Subject: [PATCH 1004/3953] cryptography + numpy for python 3.8 (#27861) --- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- setup.py | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index caf422938b2599..04723a6a1f67fe 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/integrations/iqvia", "requirements": [ - "numpy==1.17.1", + "numpy==1.17.3", "pyiqvia==0.2.1" ], "dependencies": [], diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 40421674a4b203..bd82da000cf10d 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/integrations/opencv", "requirements": [ - "numpy==1.17.1", + "numpy==1.17.3", "opencv-python-headless==4.1.1.26" ], "dependencies": [], diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index e7d35829ffb2fb..e0a8728b2957ae 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", - "numpy==1.17.1", + "numpy==1.17.3", "protobuf==3.6.1" ], "dependencies": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 1432d2d21a0662..cf9333be7c38e0 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/integrations/trend", "requirements": [ - "numpy==1.17.1" + "numpy==1.17.3" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1648bdc3db58bc..80357eccf7198d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ attrs==19.2.0 bcrypt==3.1.7 certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" -cryptography==2.7 +cryptography==2.8 distro==1.4.0 hass-nabucasa==0.22 home-assistant-frontend==20191014.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9fe8b6622de47c..951c980094315f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -9,7 +9,7 @@ contextvars==2.4;python_version<"3.7" importlib-metadata==0.23 jinja2>=2.10.1 PyJWT==1.7.1 -cryptography==2.7 +cryptography==2.8 pip>=8.0.3 python-slugify==3.0.6 pytz>=2019.03 @@ -884,7 +884,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.1 +numpy==1.17.3 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b224126d67516..c9a0013212cf3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -313,7 +313,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.1 +numpy==1.17.3 # homeassistant.components.google oauth2client==4.0.0 diff --git a/setup.py b/setup.py index 5b9988cff27bcf..0e8f8313a98781 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ "jinja2>=2.10.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. - "cryptography==2.7", + "cryptography==2.8", "pip>=8.0.3", "python-slugify==3.0.6", "pytz>=2019.03", From 03cc7f377bb6deb51e4cc431e0498bd70395ef03 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 17:12:42 +0200 Subject: [PATCH 1005/3953] Move imports in bom component (#27854) --- homeassistant/components/bom/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bom/camera.py b/homeassistant/components/bom/camera.py index f417cf769a40ac..7460b84f73438a 100644 --- a/homeassistant/components/bom/camera.py +++ b/homeassistant/components/bom/camera.py @@ -1,4 +1,5 @@ """Provide animated GIF loops of BOM radar imagery.""" +from bomradarloop import BOMRadarLoop import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -119,7 +120,6 @@ class BOMRadarCam(Camera): def __init__(self, name, location, radar_id, delta, frames, outfile): """Initialize the component.""" - from bomradarloop import BOMRadarLoop super().__init__() self._name = name From 56dde68c5baab86bf084eedd6d3d23c7e1997a36 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 17:14:01 +0200 Subject: [PATCH 1006/3953] Move imports in bmw_connected_drive component (#27853) --- homeassistant/components/bmw_connected_drive/__init__.py | 8 ++++---- .../components/bmw_connected_drive/binary_sensor.py | 4 ++-- homeassistant/components/bmw_connected_drive/lock.py | 3 ++- homeassistant/components/bmw_connected_drive/sensor.py | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 8e67da86dc3014..455d821e6692bf 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,12 +1,14 @@ """Reads vehicle status from BMW connected drive portal.""" import logging +from bimmer_connected.account import ConnectedDriveAccount +from bimmer_connected.country_selector import get_region_from_name import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery -from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_utc_time_change import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -118,8 +120,6 @@ def __init__( self, username: str, password: str, region_str: str, name: str, read_only ) -> None: """Constructor.""" - from bimmer_connected.account import ConnectedDriveAccount - from bimmer_connected.country_selector import get_region_from_name region = get_region_from_name(region_str) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index c13de4559847ef..8163ae4eae30e5 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -1,6 +1,8 @@ """Reads vehicle status from BMW connected drive portal.""" import logging +from bimmer_connected.state import ChargingState, LockState + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import LENGTH_KILOMETERS @@ -141,8 +143,6 @@ def device_state_attributes(self): def update(self): """Read new state data from the library.""" - from bimmer_connected.state import LockState - from bimmer_connected.state import ChargingState vehicle_state = self._vehicle.state diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 2055b442dcd17a..5323e94c1c38d0 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -1,6 +1,8 @@ """Support for BMW car locks with BMW ConnectedDrive.""" import logging +from bimmer_connected.state import LockState + from homeassistant.components.lock import LockDevice from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED @@ -87,7 +89,6 @@ def unlock(self, **kwargs): def update(self): """Update state of the lock.""" - from bimmer_connected.state import LockState _LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute) vehicle_state = self._vehicle.state diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 28a4e853f2cb6f..f919bba6b95fae 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,6 +1,8 @@ """Support for reading vehicle status from BMW connected drive portal.""" import logging +from bimmer_connected.state import ChargingState + from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, @@ -97,7 +99,6 @@ def name(self) -> str: @property def icon(self): """Icon to use in the frontend, if any.""" - from bimmer_connected.state import ChargingState vehicle_state = self._vehicle.state charging_state = vehicle_state.charging_status in [ChargingState.CHARGING] From bb76678b36b8a0830581eebeddc0916331b277c5 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 17:14:50 +0200 Subject: [PATCH 1007/3953] Move imports in blink component (#27850) --- homeassistant/components/blink/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index bd11572ba1c085..e233a8b21d8196 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -1,22 +1,24 @@ """Support for Blink Home Camera System.""" -import logging from datetime import timedelta +import logging + +from blinkpy import blinkpy import voluptuous as vol -from homeassistant.helpers import config_validation as cv, discovery from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_NAME, - CONF_SCAN_INTERVAL, CONF_BINARY_SENSORS, - CONF_SENSORS, CONF_FILENAME, - CONF_MONITORED_CONDITIONS, CONF_MODE, + CONF_MONITORED_CONDITIONS, + CONF_NAME, CONF_OFFSET, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_SENSORS, + CONF_USERNAME, TEMP_FAHRENHEIT, ) +from homeassistant.helpers import config_validation as cv, discovery _LOGGER = logging.getLogger(__name__) @@ -97,7 +99,6 @@ def setup(hass, config): """Set up Blink System.""" - from blinkpy import blinkpy conf = config[BLINK_DATA] username = conf[CONF_USERNAME] From e95b8035ed373f6979d7fb48536ac7a9a251f788 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 17:15:20 +0200 Subject: [PATCH 1008/3953] Move imports in blinksticklight component (#27851) --- homeassistant/components/blinksticklight/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py index 5f3cb7ebfd192e..197213f747370f 100644 --- a/homeassistant/components/blinksticklight/light.py +++ b/homeassistant/components/blinksticklight/light.py @@ -1,15 +1,16 @@ """Support for Blinkstick lights.""" import logging +from blinkstick import blinkstick import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv @@ -33,7 +34,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Blinkstick device specified by serial number.""" - from blinkstick import blinkstick name = config.get(CONF_NAME) serial = config.get(CONF_SERIAL) From 83a709b768b88389f0077ca22aa7a445c5babaac Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Sat, 19 Oct 2019 04:14:54 +1100 Subject: [PATCH 1009/3953] Move imports in recorder component (#27859) * move imports to top-level in recorder init * move imports to top-level in recorder migration * move imports to top-level in recorder models * move imports to top-level in recorder purge * move imports to top-level in recorder util * fix pylint --- homeassistant/components/recorder/__init__.py | 32 +++++++------------ .../components/recorder/migration.py | 22 ++++--------- homeassistant/components/recorder/models.py | 3 +- homeassistant/components/recorder/purge.py | 6 ++-- homeassistant/components/recorder/util.py | 8 ++--- tests/components/recorder/test_migrate.py | 6 ++-- 6 files changed, 27 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index b36e0a34fa482a..10b1d04304fd63 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -8,8 +8,14 @@ import threading import time from typing import Any, Dict, Optional +from sqlite3 import Connection import voluptuous as vol +from sqlalchemy import exc, create_engine +from sqlalchemy.engine import Engine +from sqlalchemy.event import listens_for +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.pool import StaticPool from homeassistant.const import ( ATTR_ENTITY_ID, @@ -23,6 +29,7 @@ EVENT_TIME_CHANGED, MATCH_ALL, ) +from homeassistant.components import persistent_notification from homeassistant.core import CoreState, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter @@ -31,6 +38,7 @@ from . import migration, purge from .const import DATA_INSTANCE +from .models import Base, Events, RecorderRuns, States from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -100,11 +108,9 @@ def run_information(hass, point_in_time: Optional[datetime] = None): There is also the run that covers point_in_time. """ - from . import models - ins = hass.data[DATA_INSTANCE] - recorder_runs = models.RecorderRuns + recorder_runs = RecorderRuns if point_in_time is None or point_in_time > ins.recording_start: return ins.run_info @@ -208,10 +214,6 @@ def do_adhoc_purge(self, **kwargs): def run(self): """Start processing events to save.""" - from .models import States, Events - from homeassistant.components import persistent_notification - from sqlalchemy import exc - tries = 1 connected = False @@ -393,18 +395,10 @@ def block_till_done(self): def _setup_connection(self): """Ensure database is ready to fly.""" - from sqlalchemy import create_engine, event - from sqlalchemy.engine import Engine - from sqlalchemy.orm import scoped_session - from sqlalchemy.orm import sessionmaker - from sqlite3 import Connection - - from . import models - kwargs = {} # pylint: disable=unused-variable - @event.listens_for(Engine, "connect") + @listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_connection, connection_record): """Set sqlite's WAL mode.""" if isinstance(dbapi_connection, Connection): @@ -416,8 +410,6 @@ def set_sqlite_pragma(dbapi_connection, connection_record): dbapi_connection.isolation_level = old_isolation if self.db_url == "sqlite://" or ":memory:" in self.db_url: - from sqlalchemy.pool import StaticPool - kwargs["connect_args"] = {"check_same_thread": False} kwargs["poolclass"] = StaticPool kwargs["pool_reset_on_return"] = None @@ -428,7 +420,7 @@ def set_sqlite_pragma(dbapi_connection, connection_record): self.engine.dispose() self.engine = create_engine(self.db_url, **kwargs) - models.Base.metadata.create_all(self.engine) + Base.metadata.create_all(self.engine) self.get_session = scoped_session(sessionmaker(bind=self.engine)) def _close_connection(self): @@ -439,8 +431,6 @@ def _close_connection(self): def _setup_run(self): """Log the start of the current run.""" - from .models import RecorderRuns - with session_scope(session=self.get_session()) as session: for run in session.query(RecorderRuns).filter_by(end=None): run.closed_incorrect = True diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 3de0430d8f3898..33a01ea1ac07f9 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -2,6 +2,11 @@ import logging import os +from sqlalchemy import Table, text +from sqlalchemy.engine import reflection +from sqlalchemy.exc import OperationalError, SQLAlchemyError + +from .models import SchemaChanges, SCHEMA_VERSION, Base from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -10,8 +15,6 @@ def migrate_schema(instance): """Check if the schema needs to be upgraded.""" - from .models import SchemaChanges, SCHEMA_VERSION - progress_path = instance.hass.config.path(PROGRESS_FILE) with session_scope(session=instance.get_session()) as session: @@ -60,11 +63,7 @@ def _create_index(engine, table_name, index_name): The index name should match the name given for the index within the table definition described in the models """ - from sqlalchemy import Table - from sqlalchemy.exc import OperationalError - from . import models - - table = Table(table_name, models.Base.metadata) + table = Table(table_name, Base.metadata) _LOGGER.debug("Looking up index for table %s", table_name) # Look up the index object by name from the table is the models index = next(idx for idx in table.indexes if idx.name == index_name) @@ -99,9 +98,6 @@ def _drop_index(engine, table_name, index_name): string here is generated from the method parameters without sanitizing. DO NOT USE THIS FUNCTION IN ANY OPERATION THAT TAKES USER INPUT. """ - from sqlalchemy import text - from sqlalchemy.exc import SQLAlchemyError - _LOGGER.debug("Dropping index %s from table %s", index_name, table_name) success = False @@ -159,9 +155,6 @@ def _drop_index(engine, table_name, index_name): def _add_columns(engine, table_name, columns_def): """Add columns to a table.""" - from sqlalchemy import text - from sqlalchemy.exc import OperationalError - _LOGGER.info( "Adding columns %s to table %s. Note: this can take several " "minutes on large databases and slow computers. Please " @@ -277,9 +270,6 @@ def _inspect_schema_version(engine, session): version 1 are present to make the determination. Eventually this logic can be removed and we can assume a new db is being created. """ - from sqlalchemy.engine import reflection - from .models import SchemaChanges, SCHEMA_VERSION - inspector = reflection.Inspector.from_engine(engine) indexes = inspector.get_indexes("events") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 12f4a9065af636..b512bfc8204ec1 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -15,6 +15,7 @@ distinct, ) from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm.session import Session import homeassistant.util.dt as dt_util from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id @@ -164,8 +165,6 @@ def entity_ids(self, point_in_time=None): Specify point_in_time if you want to know which existed at that point in time inside the run. """ - from sqlalchemy.orm.session import Session - session = Session.object_session(self) assert session is not None, "RecorderRuns need to be persisted" diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 81426d65f067ab..2ac0b38c694cdf 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -2,7 +2,10 @@ from datetime import timedelta import logging +from sqlalchemy.exc import SQLAlchemyError + import homeassistant.util.dt as dt_util +from .models import Events, States from .util import session_scope @@ -11,9 +14,6 @@ def purge_old_data(instance, purge_days, repack): """Purge events and states older than purge_days ago.""" - from .models import States, Events - from sqlalchemy.exc import SQLAlchemyError - purge_before = dt_util.utcnow() - timedelta(days=purge_days) _LOGGER.debug("Purging events before %s", purge_before) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 674d687ec14bb6..8cfcafea79d80a 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -3,6 +3,8 @@ import logging import time +from sqlalchemy.exc import OperationalError, SQLAlchemyError + from .const import DATA_INSTANCE _LOGGER = logging.getLogger(__name__) @@ -37,8 +39,6 @@ def session_scope(*, hass=None, session=None): def commit(session, work): """Commit & retry work: Either a model or in a function.""" - import sqlalchemy.exc - for _ in range(0, RETRIES): try: if callable(work): @@ -47,7 +47,7 @@ def commit(session, work): session.add(work) session.commit() return True - except sqlalchemy.exc.OperationalError as err: + except OperationalError as err: _LOGGER.error("Error executing query: %s", err) session.rollback() time.sleep(QUERY_RETRY_WAIT) @@ -59,8 +59,6 @@ def execute(qry): This method also retries a few times in the case of stale connections. """ - from sqlalchemy.exc import SQLAlchemyError - for tryno in range(0, RETRIES): try: timer_start = time.perf_counter() diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 19b7566c37c726..81e0423a72392b 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -23,9 +23,9 @@ def create_engine_test(*args, **kwargs): async def test_schema_update_calls(hass): """Test that schema migrations occur in correct order.""" - with patch("sqlalchemy.create_engine", new=create_engine_test), patch( - "homeassistant.components.recorder.migration._apply_update" - ) as update: + with patch( + "homeassistant.components.recorder.create_engine", new=create_engine_test + ), patch("homeassistant.components.recorder.migration._apply_update") as update: await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) From a119932ee590b9115afc97ddf56b9a95fd9f2c00 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 11:46:45 -0700 Subject: [PATCH 1010/3953] Refactor the conversation integration (#27839) * Refactor the conversation integration * Lint --- .../components/conversation/__init__.py | 133 ++++-------------- .../components/conversation/agent.py | 12 ++ .../components/conversation/const.py | 3 + .../components/conversation/default_agent.py | 127 +++++++++++++++++ homeassistant/components/hangouts/__init__.py | 3 +- .../components/shopping_list/__init__.py | 7 - tests/components/conversation/test_init.py | 75 ++++------ tests/components/conversation/test_util.py | 55 ++++++++ 8 files changed, 247 insertions(+), 168 deletions(-) create mode 100644 homeassistant/components/conversation/agent.py create mode 100644 homeassistant/components/conversation/const.py create mode 100644 homeassistant/components/conversation/default_agent.py create mode 100644 tests/components/conversation/test_util.py diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 9d7d510b10ec9a..798fc926e0f69c 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -6,15 +6,12 @@ from homeassistant import core from homeassistant.components import http -from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, intent from homeassistant.loader import bind_hass -from homeassistant.setup import ATTR_COMPONENT -from .util import create_matcher +from .agent import AbstractConversationAgent +from .default_agent import async_register, DefaultAgent _LOGGER = logging.getLogger(__name__) @@ -22,15 +19,8 @@ DOMAIN = "conversation" -REGEX_TURN_COMMAND = re.compile(r"turn (?P(?: |\w)+) (?P\w+)") REGEX_TYPE = type(re.compile("")) - -UTTERANCES = { - "cover": { - INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"], - INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"], - } -} +DATA_AGENT = "conversation_agent" SERVICE_PROCESS = "process" @@ -50,137 +40,64 @@ ) -@core.callback -@bind_hass -def async_register(hass, intent_type, utterances): - """Register utterances and any custom intents. - - Registrations don't require conversations to be loaded. They will become - active once the conversation component is loaded. - """ - intents = hass.data.get(DOMAIN) - - if intents is None: - intents = hass.data[DOMAIN] = {} - - conf = intents.get(intent_type) +async_register = bind_hass(async_register) # pylint: disable=invalid-name - if conf is None: - conf = intents[intent_type] = [] - for utterance in utterances: - if isinstance(utterance, REGEX_TYPE): - conf.append(utterance) - else: - conf.append(create_matcher(utterance)) +@core.callback +@bind_hass +def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent): + """Set the agent to handle the conversations.""" + hass.data[DATA_AGENT] = agent async def async_setup(hass, config): """Register the process service.""" - config = config.get(DOMAIN, {}) - intents = hass.data.get(DOMAIN) - - if intents is None: - intents = hass.data[DOMAIN] = {} - for intent_type, utterances in config.get("intents", {}).items(): - conf = intents.get(intent_type) + async def process(hass, text): + """Process a line of text.""" + agent = hass.data.get(DATA_AGENT) - if conf is None: - conf = intents[intent_type] = [] + if agent is None: + agent = hass.data[DATA_AGENT] = DefaultAgent(hass) + await agent.async_initialize(config) - conf.extend(create_matcher(utterance) for utterance in utterances) + return await agent.async_process(text) - async def process(service): + async def handle_service(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) try: - await _process(hass, text) + await process(hass, text) except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) hass.services.async_register( - DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA - ) - - hass.http.register_view(ConversationProcessView) - - # We strip trailing 's' from name because our state matcher will fail - # if a letter is not there. By removing 's' we can match singular and - # plural names. - - async_register( - hass, - intent.INTENT_TURN_ON, - ["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"], - ) - async_register( - hass, - intent.INTENT_TURN_OFF, - ["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"], - ) - async_register( - hass, - intent.INTENT_TOGGLE, - ["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"], + DOMAIN, SERVICE_PROCESS, handle_service, schema=SERVICE_PROCESS_SCHEMA ) - @callback - def register_utterances(component): - """Register utterances for a component.""" - if component not in UTTERANCES: - return - for intent_type, sentences in UTTERANCES[component].items(): - async_register(hass, intent_type, sentences) - - @callback - def component_loaded(event): - """Handle a new component loaded.""" - register_utterances(event.data[ATTR_COMPONENT]) - - hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) - - # Check already loaded components. - for component in hass.config.components: - register_utterances(component) + hass.http.register_view(ConversationProcessView(process)) return True -async def _process(hass, text): - """Process a line of text.""" - intents = hass.data.get(DOMAIN, {}) - - for intent_type, matchers in intents.items(): - for matcher in matchers: - match = matcher.match(text) - - if not match: - continue - - response = await hass.helpers.intent.async_handle( - DOMAIN, - intent_type, - {key: {"value": value} for key, value in match.groupdict().items()}, - text, - ) - return response - - class ConversationProcessView(http.HomeAssistantView): """View to retrieve shopping list content.""" url = "/api/conversation/process" name = "api:conversation:process" + def __init__(self, process): + """Initialize the conversation process view.""" + self._process = process + @RequestDataValidator(vol.Schema({vol.Required("text"): str})) async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] try: - intent_result = await _process(hass, data["text"]) + intent_result = await self._process(hass, data["text"]) except intent.IntentHandleError as err: intent_result = intent.IntentResponse() intent_result.async_set_speech(str(err)) diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py new file mode 100644 index 00000000000000..eae6402530ce8d --- /dev/null +++ b/homeassistant/components/conversation/agent.py @@ -0,0 +1,12 @@ +"""Agent foundation for conversation integration.""" +from abc import ABC, abstractmethod + +from homeassistant.helpers import intent + + +class AbstractConversationAgent(ABC): + """Abstract conversation agent.""" + + @abstractmethod + async def async_process(self, text: str) -> intent.IntentResponse: + """Process a sentence.""" diff --git a/homeassistant/components/conversation/const.py b/homeassistant/components/conversation/const.py new file mode 100644 index 00000000000000..04bfa37306138d --- /dev/null +++ b/homeassistant/components/conversation/const.py @@ -0,0 +1,3 @@ +"""Const for conversation integration.""" + +DOMAIN = "conversation" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py new file mode 100644 index 00000000000000..e93afcfaf65ffa --- /dev/null +++ b/homeassistant/components/conversation/default_agent.py @@ -0,0 +1,127 @@ +"""Standard conversastion implementation for Home Assistant.""" +import logging +import re + +from homeassistant import core +from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER +from homeassistant.components.shopping_list import INTENT_ADD_ITEM, INTENT_LAST_ITEMS +from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.core import callback +from homeassistant.helpers import intent +from homeassistant.setup import ATTR_COMPONENT + +from .agent import AbstractConversationAgent +from .const import DOMAIN +from .util import create_matcher + +_LOGGER = logging.getLogger(__name__) + +REGEX_TURN_COMMAND = re.compile(r"turn (?P(?: |\w)+) (?P\w+)") +REGEX_TYPE = type(re.compile("")) + +UTTERANCES = { + "cover": { + INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"], + INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"], + }, + "shopping_list": { + INTENT_ADD_ITEM: ["Add [the] [a] [an] {item} to my shopping list"], + INTENT_LAST_ITEMS: ["What is on my shopping list"], + }, +} + + +@core.callback +def async_register(hass, intent_type, utterances): + """Register utterances and any custom intents for the default agent. + + Registrations don't require conversations to be loaded. They will become + active once the conversation component is loaded. + """ + intents = hass.data.setdefault(DOMAIN, {}) + conf = intents.setdefault(intent_type, []) + + for utterance in utterances: + if isinstance(utterance, REGEX_TYPE): + conf.append(utterance) + else: + conf.append(create_matcher(utterance)) + + +class DefaultAgent(AbstractConversationAgent): + """Default agent for conversation agent.""" + + def __init__(self, hass: core.HomeAssistant): + """Initialize the default agent.""" + self.hass = hass + + async def async_initialize(self, config): + """Initialize the default agent.""" + config = config.get(DOMAIN, {}) + intents = self.hass.data.setdefault(DOMAIN, {}) + + for intent_type, utterances in config.get("intents", {}).items(): + conf = intents.get(intent_type) + + if conf is None: + conf = intents[intent_type] = [] + + conf.extend(create_matcher(utterance) for utterance in utterances) + + # We strip trailing 's' from name because our state matcher will fail + # if a letter is not there. By removing 's' we can match singular and + # plural names. + + async_register( + self.hass, + intent.INTENT_TURN_ON, + ["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"], + ) + async_register( + self.hass, + intent.INTENT_TURN_OFF, + ["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"], + ) + async_register( + self.hass, + intent.INTENT_TOGGLE, + ["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"], + ) + + @callback + def component_loaded(event): + """Handle a new component loaded.""" + self.register_utterances(event.data[ATTR_COMPONENT]) + + self.hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) + + # Check already loaded components. + for component in self.hass.config.components: + self.register_utterances(component) + + @callback + def register_utterances(self, component): + """Register utterances for a component.""" + if component not in UTTERANCES: + return + for intent_type, sentences in UTTERANCES[component].items(): + async_register(self.hass, intent_type, sentences) + + async def async_process(self, text) -> intent.IntentResponse: + """Process a sentence.""" + intents = self.hass.data[DOMAIN] + + for intent_type, matchers in intents.items(): + for matcher in matchers: + match = matcher.match(text) + + if not match: + continue + + return await intent.async_handle( + self.hass, + DOMAIN, + intent_type, + {key: {"value": value} for key, value in match.groupdict().items()}, + text, + ) diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 885ac5d1670e21..953994d6ac0cbd 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -7,6 +7,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import dispatcher, intent import homeassistant.helpers.config_validation as cv +from homeassistant.components.conversation.util import create_matcher # We need an import from .config_flow, without it .config_flow is never loaded. from .intents import HelpIntent @@ -54,8 +55,6 @@ async def async_setup(hass, config): """Set up the Hangouts bot component.""" - from homeassistant.components.conversation import create_matcher - config = config.get(DOMAIN) if config is None: hass.data[DOMAIN] = { diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 3c9cb4391a73af..a5e901b8c6e7be 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -101,13 +101,6 @@ def complete_item_service(call): hass.http.register_view(UpdateShoppingListItemView) hass.http.register_view(ClearCompletedItemsView) - hass.components.conversation.async_register( - INTENT_ADD_ITEM, ["Add [the] [a] [an] {item} to my shopping list"] - ) - hass.components.conversation.async_register( - INTENT_LAST_ITEMS, ["What is on my shopping list"] - ) - hass.components.frontend.async_register_built_in_panel( "shopping-list", "shopping_list", "mdi:cart" ) diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index d4142f2ce5f115..a9116ac0d98b75 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -263,54 +263,27 @@ async def test_http_api_wrong_data(hass, hass_client): assert resp.status == 400 -def test_create_matcher(): - """Test the create matcher method.""" - # Basic sentence - pattern = conversation.create_matcher("Hello world") - assert pattern.match("Hello world") is not None - - # Match a part - pattern = conversation.create_matcher("Hello {name}") - match = pattern.match("hello world") - assert match is not None - assert match.groupdict()["name"] == "world" - no_match = pattern.match("Hello world, how are you?") - assert no_match is None - - # Optional and matching part - pattern = conversation.create_matcher("Turn on [the] {name}") - match = pattern.match("turn on the kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn on kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn off kitchen lights") - assert match is None - - # Two different optional parts, 1 matching part - pattern = conversation.create_matcher("Turn on [the] [a] {name}") - match = pattern.match("turn on the kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn on kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn on a kitchen light") - assert match is not None - assert match.groupdict()["name"] == "kitchen light" - - # Strip plural - pattern = conversation.create_matcher("Turn {name}[s] on") - match = pattern.match("turn kitchen lights on") - assert match is not None - assert match.groupdict()["name"] == "kitchen light" - - # Optional 2 words - pattern = conversation.create_matcher("Turn [the great] {name} on") - match = pattern.match("turn the great kitchen lights on") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn kitchen lights on") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" +async def test_custom_agent(hass, hass_client): + """Test a custom conversation agent.""" + + class MyAgent(conversation.AbstractConversationAgent): + """Test Agent.""" + + async def async_process(self, text): + """Process some text.""" + response = intent.IntentResponse() + response.async_set_speech("Test response") + return response + + conversation.async_set_agent(hass, MyAgent()) + + assert await async_setup_component(hass, "conversation", {}) + + client = await hass_client() + + resp = await client.post("/api/conversation/process", json={"text": "Test Text"}) + assert resp.status == 200 + assert await resp.json() == { + "card": {}, + "speech": {"plain": {"extra_data": None, "speech": "Test response"}}, + } diff --git a/tests/components/conversation/test_util.py b/tests/components/conversation/test_util.py new file mode 100644 index 00000000000000..2fa4527e9b15c2 --- /dev/null +++ b/tests/components/conversation/test_util.py @@ -0,0 +1,55 @@ +"""Test the conversation utils.""" +from homeassistant.components.conversation.util import create_matcher + + +def test_create_matcher(): + """Test the create matcher method.""" + # Basic sentence + pattern = create_matcher("Hello world") + assert pattern.match("Hello world") is not None + + # Match a part + pattern = create_matcher("Hello {name}") + match = pattern.match("hello world") + assert match is not None + assert match.groupdict()["name"] == "world" + no_match = pattern.match("Hello world, how are you?") + assert no_match is None + + # Optional and matching part + pattern = create_matcher("Turn on [the] {name}") + match = pattern.match("turn on the kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn on kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn off kitchen lights") + assert match is None + + # Two different optional parts, 1 matching part + pattern = create_matcher("Turn on [the] [a] {name}") + match = pattern.match("turn on the kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn on kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn on a kitchen light") + assert match is not None + assert match.groupdict()["name"] == "kitchen light" + + # Strip plural + pattern = create_matcher("Turn {name}[s] on") + match = pattern.match("turn kitchen lights on") + assert match is not None + assert match.groupdict()["name"] == "kitchen light" + + # Optional 2 words + pattern = create_matcher("Turn [the great] {name} on") + match = pattern.match("turn the great kitchen lights on") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn kitchen lights on") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" From 103ffacea76179bb63365a12366c725a4674507f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 18 Oct 2019 22:20:27 +0300 Subject: [PATCH 1011/3953] Use pre-commit in CI and tox (#27743) --- .pre-commit-config.yaml | 4 +++- .travis.yml | 5 ++++- azure-pipelines-ci.yml | 9 ++++++--- tox.ini | 4 ++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3773a3213aa7d6..268cff9ea78b85 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,7 @@ repos: args: - --safe - --quiet + files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.8 hooks: @@ -13,6 +14,7 @@ repos: additional_dependencies: - flake8-docstrings==1.3.1 - pydocstyle==4.0.0 + files: ^(homeassistant|script|tests)/.+\.py$ # Using a local "system" mypy instead of the mypy hook, because its # results depend on what is installed. And the mypy hook runs in a # virtualenv of its own, meaning we'd need to install and maintain @@ -26,4 +28,4 @@ repos: language: system types: [python] require_serial: true - exclude: ^script/scaffold/templates/ + files: ^homeassistant/.+\.py$ diff --git a/.travis.yml b/.travis.yml index 0e9e030128e2a9..7b3765716eb20c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,10 @@ matrix: - python: "3.7" env: TOXENV=py37 -cache: pip +cache: + pip: true + directories: + - $HOME/.cache/pre-commit install: pip install -U tox language: python script: travis_wait 50 tox --develop diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 257eac57c2d67a..f03c5f435f9892 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -45,9 +45,10 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + pre-commit install-hooks - script: | . venv/bin/activate - flake8 homeassistant tests script + pre-commit run flake8 --all-files displayName: 'Run flake8' - job: 'Validate' pool: @@ -83,9 +84,10 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + pre-commit install-hooks - script: | . venv/bin/activate - ./script/check_format + pre-commit run black --all-files displayName: 'Check Black formatting' - stage: 'Tests' @@ -180,7 +182,8 @@ stages: . venv/bin/activate pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt + pre-commit install-hooks - script: | . venv/bin/activate - mypy homeassistant + pre-commit run mypy --all-files displayName: 'Run mypy' diff --git a/tox.ini b/tox.ini index 8c3563dac837c8..0b0c969d7811f6 100644 --- a/tox.ini +++ b/tox.ini @@ -34,11 +34,11 @@ deps = commands = python -m script.gen_requirements_all validate python -m script.hassfest validate - flake8 {posargs: homeassistant tests script} + pre-commit run flake8 {posargs: --all-files} [testenv:typing] deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - mypy homeassistant + pre-commit run mypy {posargs: --all-files} From 6157be23dce8857963150d8f162978d967f22bd2 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 21:32:14 +0200 Subject: [PATCH 1012/3953] Move imports in cloudflare integration(#27882) --- homeassistant/components/cloudflare/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 26feff069daa27..265621b6250688 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pycfdns import CloudflareUpdater import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_EMAIL, CONF_ZONE @@ -33,7 +34,6 @@ def setup(hass, config): """Set up the Cloudflare component.""" - from pycfdns import CloudflareUpdater cfupdate = CloudflareUpdater() email = config[DOMAIN][CONF_EMAIL] From b6c26cb363adf9630607948643f4af4f612d3329 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 13:06:33 -0700 Subject: [PATCH 1013/3953] Introduce new OAuth2 config flow helper (#27727) * Refactor the Somfy auth implementation * Typing * Migrate Somfy to OAuth2 flow helper * Add tests * Add more tests * Fix tests * Fix type error * More tests * Remove side effect from constructor * implementation -> auth_implementation * Make get_implementation async * Minor cleanup + Allow picking implementations. * Add support for extra authorize data --- homeassistant/bootstrap.py | 2 +- homeassistant/components/somfy/__init__.py | 63 +-- homeassistant/components/somfy/api.py | 55 +++ homeassistant/components/somfy/config_flow.py | 135 +----- homeassistant/components/somfy/const.py | 2 - homeassistant/components/somfy/manifest.json | 12 +- homeassistant/config_entries.py | 2 +- homeassistant/core.py | 9 +- homeassistant/data_entry_flow.py | 2 +- .../helpers/config_entry_oauth2_flow.py | 420 ++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/common.py | 17 +- tests/components/somfy/test_config_flow.py | 125 ++++-- .../helpers/test_config_entry_oauth2_flow.py | 266 +++++++++++ 15 files changed, 900 insertions(+), 214 deletions(-) create mode 100644 homeassistant/components/somfy/api.py create mode 100644 homeassistant/helpers/config_entry_oauth2_flow.py create mode 100644 tests/helpers/test_config_entry_oauth2_flow.py diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e399205ec708e9..6118f4f2bd734b 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -260,7 +260,7 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]: domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN) # Add config entry domains - domains.update(hass.config_entries.async_domains()) # type: ignore + domains.update(hass.config_entries.async_domains()) # Make sure the Hass.io component is loaded if "HASSIO" in os.environ: diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 2c7c71d7a69686..cd5960bf6b1c70 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -4,21 +4,21 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/somfy/ """ +import asyncio import logging from datetime import timedelta -from functools import partial import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant import config_entries +from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from homeassistant.components.somfy import config_flow from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TOKEN from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle +from . import api + API = "api" DEVICES = "devices" @@ -52,19 +52,21 @@ async def async_setup(hass, config): """Set up the Somfy component.""" - if DOMAIN not in config: - return True - hass.data[DOMAIN] = {} - config_flow.register_flow_implementation( - hass, config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET] - ) + if DOMAIN not in config: + return True - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) + config_flow.SomfyFlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + "https://accounts.somfy.com/oauth/oauth/v2/auth", + "https://accounts.somfy.com/oauth/oauth/v2/token", + ), ) return True @@ -72,25 +74,18 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Somfy from a config entry.""" - - def token_saver(token): - _LOGGER.debug("Saving updated token") - entry.data[CONF_TOKEN] = token - update_entry = partial( - hass.config_entries.async_update_entry, data={**entry.data} + # Backwards compat + if "auth_implementation" not in entry.data: + hass.config_entries.async_update_entry( + entry, data={**entry.data, "auth_implementation": DOMAIN} ) - hass.add_job(update_entry, entry) - # Force token update. - from pymfy.api.somfy_api import SomfyApi - - hass.data[DOMAIN][API] = SomfyApi( - entry.data["refresh_args"]["client_id"], - entry.data["refresh_args"]["client_secret"], - token=entry.data[CONF_TOKEN], - token_updater=token_saver, + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry ) + hass.data[DOMAIN][API] = api.ConfigEntrySomfyApi(hass, entry, implementation) + await update_all_devices(hass) for component in SOMFY_COMPONENTS: @@ -104,16 +99,22 @@ def token_saver(token): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload a config entry.""" hass.data[DOMAIN].pop(API, None) + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in SOMFY_COMPONENTS + ] + ) return True class SomfyEntity(Entity): """Representation of a generic Somfy device.""" - def __init__(self, device, api): + def __init__(self, device, somfy_api): """Initialize the Somfy device.""" self.device = device - self.api = api + self.api = somfy_api @property def unique_id(self): diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py new file mode 100644 index 00000000000000..3e7bcf9deb4cb5 --- /dev/null +++ b/homeassistant/components/somfy/api.py @@ -0,0 +1,55 @@ +"""API for Somfy bound to HASS OAuth.""" +from asyncio import run_coroutine_threadsafe +from functools import partial + +import requests +from pymfy.api import somfy_api + +from homeassistant import core, config_entries +from homeassistant.helpers import config_entry_oauth2_flow + + +class ConfigEntrySomfyApi(somfy_api.AbstractSomfyApi): + """Provide a Somfy API tied into an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize the Config Entry Somfy API.""" + self.hass = hass + self.config_entry = config_entry + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + + def get(self, path): + """Fetch a URL from the Somfy API.""" + return run_coroutine_threadsafe( + self._request("get", path), self.hass.loop + ).result() + + def post(self, path, *, json): + """Post data to the Somfy API.""" + return run_coroutine_threadsafe( + self._request("post", path, json=json), self.hass.loop + ).result() + + async def _request(self, method, path, **kwargs): + """Make a request.""" + await self.session.async_ensure_token_valid() + + return await self.hass.async_add_executor_job( + partial( + requests.request, + method, + f"{self.base_url}{path}", + **kwargs, + headers={ + **kwargs.get("headers", {}), + "authorization": f"Bearer {self.config_entry.data['token']['access_token']}", + }, + ) + ) diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index 9f3c58c8ffba70..cb180d4e2470a2 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -1,141 +1,28 @@ """Config flow for Somfy.""" -import asyncio import logging -import async_timeout - from homeassistant import config_entries -from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback -from .const import CLIENT_ID, CLIENT_SECRET, DOMAIN - -AUTH_CALLBACK_PATH = "/auth/somfy/callback" -AUTH_CALLBACK_NAME = "auth:somfy:callback" +from homeassistant.helpers import config_entry_oauth2_flow +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -@callback -def register_flow_implementation(hass, client_id, client_secret): - """Register a flow implementation. - - client_id: Client id. - client_secret: Client secret. - """ - hass.data[DOMAIN][CLIENT_ID] = client_id - hass.data[DOMAIN][CLIENT_SECRET] = client_secret - +@config_entries.HANDLERS.register(DOMAIN) +class SomfyFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Config flow to handle Somfy OAuth2 authentication.""" -@config_entries.HANDLERS.register("somfy") -class SomfyFlowHandler(config_entries.ConfigFlow): - """Handle a config flow.""" - - VERSION = 1 + DOMAIN = DOMAIN CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - def __init__(self): - """Instantiate config flow.""" - self.code = None - - async def async_step_import(self, user_input=None): - """Handle external yaml configuration.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_setup") - return await self.async_step_auth() + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) async def async_step_user(self, user_input=None): """Handle a flow start.""" if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="already_setup") - if DOMAIN not in self.hass.data: - return self.async_abort(reason="missing_configuration") - - return await self.async_step_auth() - - async def async_step_auth(self, user_input=None): - """Create an entry for auth.""" - # Flow has been triggered from Somfy website - if user_input: - return await self.async_step_code(user_input) - - try: - with async_timeout.timeout(10): - url, _ = await self._get_authorization_url() - except asyncio.TimeoutError: - return self.async_abort(reason="authorize_url_timeout") - - return self.async_external_step(step_id="auth", url=url) - - async def _get_authorization_url(self): - """Get Somfy authorization url.""" - from pymfy.api.somfy_api import SomfyApi - - client_id = self.hass.data[DOMAIN][CLIENT_ID] - client_secret = self.hass.data[DOMAIN][CLIENT_SECRET] - redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" - api = SomfyApi(client_id, client_secret, redirect_uri) - - self.hass.http.register_view(SomfyAuthCallbackView()) - # Thanks to the state, we can forward the flow id to Somfy that will - # add it in the callback. - return await self.hass.async_add_executor_job( - api.get_authorization_url, self.flow_id - ) - - async def async_step_code(self, code): - """Received code for authentication.""" - self.code = code - return self.async_external_step_done(next_step_id="creation") - - async def async_step_creation(self, user_input=None): - """Create Somfy api and entries.""" - client_id = self.hass.data[DOMAIN][CLIENT_ID] - client_secret = self.hass.data[DOMAIN][CLIENT_SECRET] - code = self.code - from pymfy.api.somfy_api import SomfyApi - - redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" - api = SomfyApi(client_id, client_secret, redirect_uri) - token = await self.hass.async_add_executor_job(api.request_token, None, code) - _LOGGER.info("Successfully authenticated Somfy") - return self.async_create_entry( - title="Somfy", - data={ - "token": token, - "refresh_args": { - "client_id": client_id, - "client_secret": client_secret, - }, - }, - ) - - -class SomfyAuthCallbackView(HomeAssistantView): - """Somfy Authorization Callback View.""" - - requires_auth = False - url = AUTH_CALLBACK_PATH - name = AUTH_CALLBACK_NAME - - @staticmethod - async def get(request): - """Receive authorization code.""" - from aiohttp import web_response - - if "code" not in request.query or "state" not in request.query: - return web_response.Response( - text="Missing code or state parameter in " + request.url - ) - - hass = request.app["hass"] - hass.async_create_task( - hass.config_entries.flow.async_configure( - flow_id=request.query["state"], user_input=request.query["code"] - ) - ) - - return web_response.Response( - headers={"content-type": "text/html"}, - text="", - ) + return await super().async_step_user(user_input) diff --git a/homeassistant/components/somfy/const.py b/homeassistant/components/somfy/const.py index 99fafb71bffa32..8765e37e6d68ac 100644 --- a/homeassistant/components/somfy/const.py +++ b/homeassistant/components/somfy/const.py @@ -1,5 +1,3 @@ """Define constants for the Somfy component.""" DOMAIN = "somfy" -CLIENT_ID = "client_id" -CLIENT_SECRET = "client_secret" diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 83b50684fda8be..a34023f76ff8a2 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -3,11 +3,7 @@ "name": "Somfy Open API", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/somfy", - "dependencies": [], - "codeowners": [ - "@tetienne" - ], - "requirements": [ - "pymfy==0.5.2" - ] -} \ No newline at end of file + "dependencies": ["http"], + "codeowners": ["@tetienne"], + "requirements": ["pymfy==0.6.0"] +} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8a40cff1bd5258..f8c7c7a9da1fa1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -337,7 +337,7 @@ async def async_migrate(self, hass: HomeAssistant) -> bool: return False if result: # pylint: disable=protected-access - hass.config_entries._async_schedule_save() # type: ignore + hass.config_entries._async_schedule_save() return result except Exception: # pylint: disable=broad-except _LOGGER.exception( diff --git a/homeassistant/core.py b/homeassistant/core.py index 90d197906cbe5b..ec11b14edaab17 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -77,7 +77,8 @@ # Typing imports that create a circular dependency # pylint: disable=using-constant-test if TYPE_CHECKING: - from homeassistant.config_entries import ConfigEntries # noqa + from homeassistant.config_entries import ConfigEntries + from homeassistant.components.http import HomeAssistantHTTP # pylint: disable=invalid-name T = TypeVar("T") @@ -162,6 +163,9 @@ def __str__(self) -> str: class HomeAssistant: """Root object of the Home Assistant home automation.""" + http: "HomeAssistantHTTP" = None # type: ignore + config_entries: "ConfigEntries" = None # type: ignore + def __init__(self, loop: Optional[asyncio.events.AbstractEventLoop] = None) -> None: """Initialize new Home Assistant object.""" self.loop: asyncio.events.AbstractEventLoop = (loop or asyncio.get_event_loop()) @@ -186,9 +190,6 @@ def __init__(self, loop: Optional[asyncio.events.AbstractEventLoop] = None) -> N self.data: dict = {} self.state = CoreState.not_running self.exit_code = 0 - self.config_entries: Optional[ - ConfigEntries # pylint: disable=used-before-assignment - ] = None # If not None, use to signal end-of-loop self._stopped: Optional[asyncio.Event] = None diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 0bc27498f767e8..c06c69d9213a35 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -168,7 +168,7 @@ class FlowHandler: """Handle the configuration flow of a component.""" # Set by flow manager - flow_id: Optional[str] = None + flow_id: str = None # type: ignore hass: Optional[HomeAssistant] = None handler: Optional[Hashable] = None cur_step: Optional[Dict[str, str]] = None diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py new file mode 100644 index 00000000000000..043a28cac27ad5 --- /dev/null +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -0,0 +1,420 @@ +"""Config Flow using OAuth2. + +This module exists of the following parts: + - OAuth2 config flow which supports multiple OAuth2 implementations + - OAuth2 implementation that works with local provided client ID/secret + +""" +import asyncio +from abc import ABCMeta, ABC, abstractmethod +import logging +from typing import Optional, Any, Dict, cast +import time + +import async_timeout +from aiohttp import web, client +import jwt +import voluptuous as vol +from yarl import URL + +from homeassistant.auth.util import generate_secret +from homeassistant.core import HomeAssistant, callback +from homeassistant import config_entries +from homeassistant.components.http import HomeAssistantView + +from .aiohttp_client import async_get_clientsession + + +DATA_JWT_SECRET = "oauth2_jwt_secret" +DATA_VIEW_REGISTERED = "oauth2_view_reg" +DATA_IMPLEMENTATIONS = "oauth2_impl" +AUTH_CALLBACK_PATH = "/auth/external/callback" + + +class AbstractOAuth2Implementation(ABC): + """Base class to abstract OAuth2 authentication.""" + + @property + @abstractmethod + def name(self) -> str: + """Name of the implementation.""" + + @property + @abstractmethod + def domain(self) -> str: + """Domain that is providing the implementation.""" + + @abstractmethod + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize. + + This step is called when a config flow is initialized. It should redirect the + user to the vendor website where they can authorize Home Assistant. + + The implementation is responsible to get notified when the user is authorized + and pass this to the specified config flow. Do as little work as possible once + notified. You can do the work inside async_resolve_external_data. This will + give the best UX. + + Pass external data in with: + + ```python + await hass.config_entries.flow.async_configure( + flow_id=flow_id, user_input=external_data + ) + ``` + """ + + @abstractmethod + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve external data to tokens. + + Turn the data that the implementation passed to the config flow as external + step data into tokens. These tokens will be stored as 'token' in the + config entry data. + """ + + async def async_refresh_token(self, token: dict) -> dict: + """Refresh a token and update expires info.""" + new_token = await self._async_refresh_token(token) + new_token["expires_at"] = time.time() + new_token["expires_in"] + return new_token + + @abstractmethod + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh a token.""" + + +class LocalOAuth2Implementation(AbstractOAuth2Implementation): + """Local OAuth2 implementation.""" + + def __init__( + self, + hass: HomeAssistant, + domain: str, + client_id: str, + client_secret: str, + authorize_url: str, + token_url: str, + ): + """Initialize local auth implementation.""" + self.hass = hass + self._domain = domain + self.client_id = client_id + self.client_secret = client_secret + self.authorize_url = authorize_url + self.token_url = token_url + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Configuration.yaml" + + @property + def domain(self) -> str: + """Domain providing the implementation.""" + return self._domain + + @property + def redirect_uri(self) -> str: + """Return the redirect uri.""" + return f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" # type: ignore + + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize.""" + return str( + URL(self.authorize_url).with_query( + { + "response_type": "code", + "client_id": self.client_id, + "redirect_uri": self.redirect_uri, + "state": _encode_jwt(self.hass, {"flow_id": flow_id}), + } + ) + ) + + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve the authorization code to tokens.""" + return await self._token_request( + { + "grant_type": "authorization_code", + "code": external_data, + "redirect_uri": self.redirect_uri, + } + ) + + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh tokens.""" + new_token = await self._token_request( + { + "grant_type": "refresh_token", + "client_id": self.client_id, + "refresh_token": token["refresh_token"], + } + ) + return {**token, **new_token} + + async def _token_request(self, data: dict) -> dict: + """Make a token request.""" + session = async_get_clientsession(self.hass) + + data["client_id"] = self.client_id + + if self.client_secret is not None: + data["client_secret"] = self.client_secret + + resp = await session.post(self.token_url, data=data) + resp.raise_for_status() + return cast(dict, await resp.json()) + + +class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): + """Handle a config flow.""" + + DOMAIN = "" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN + + def __init__(self) -> None: + """Instantiate config flow.""" + if self.DOMAIN == "": + raise TypeError( + f"Can't instantiate class {self.__class__.__name__} without DOMAIN being set" + ) + + self.external_data: Any = None + self.flow_impl: AbstractOAuth2Implementation = None # type: ignore + + @property + @abstractmethod + def logger(self) -> logging.Logger: + """Return logger.""" + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {} + + async def async_step_pick_implementation(self, user_input: dict = None) -> dict: + """Handle a flow start.""" + assert self.hass + implementations = await async_get_implementations(self.hass, self.DOMAIN) + + if user_input is not None: + self.flow_impl = implementations[user_input["implementation"]] + return await self.async_step_auth() + + if not implementations: + return self.async_abort(reason="missing_configuration") + + if len(implementations) == 1: + # Pick first implementation as we have only one. + self.flow_impl = list(implementations.values())[0] + return await self.async_step_auth() + + return self.async_show_form( + step_id="pick_implementation", + data_schema=vol.Schema( + { + vol.Required( + "implementation", default=list(implementations.keys())[0] + ): vol.In({key: impl.name for key, impl in implementations.items()}) + } + ), + ) + + async def async_step_auth(self, user_input: dict = None) -> dict: + """Create an entry for auth.""" + # Flow has been triggered by external data + if user_input: + self.external_data = user_input + return self.async_external_step_done(next_step_id="creation") + + try: + with async_timeout.timeout(10): + url = await self.flow_impl.async_generate_authorize_url(self.flow_id) + except asyncio.TimeoutError: + return self.async_abort(reason="authorize_url_timeout") + + url = str(URL(url).update_query(self.extra_authorize_data)) + + return self.async_external_step(step_id="auth", url=url) + + async def async_step_creation(self, user_input: dict = None) -> dict: + """Create config entry from external data.""" + token = await self.flow_impl.async_resolve_external_data(self.external_data) + token["expires_at"] = time.time() + token["expires_in"] + + self.logger.info("Successfully authenticated") + + return await self.async_oauth_create_entry( + {"auth_implementation": self.flow_impl.domain, "token": token} + ) + + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an entry for the flow. + + Ok to override if you want to fetch extra info or even add another step. + """ + return self.async_create_entry(title=self.flow_impl.name, data=data) + + async_step_user = async_step_pick_implementation + async_step_ssdp = async_step_pick_implementation + async_step_zeroconf = async_step_pick_implementation + async_step_homekit = async_step_pick_implementation + + @classmethod + def async_register_implementation( + cls, hass: HomeAssistant, local_impl: LocalOAuth2Implementation + ) -> None: + """Register a local implementation.""" + async_register_implementation(hass, cls.DOMAIN, local_impl) + + +@callback +def async_register_implementation( + hass: HomeAssistant, domain: str, implementation: AbstractOAuth2Implementation +) -> None: + """Register an OAuth2 flow implementation for an integration.""" + if isinstance(implementation, LocalOAuth2Implementation) and not hass.data.get( + DATA_VIEW_REGISTERED, False + ): + hass.http.register_view(OAuth2AuthorizeCallbackView()) # type: ignore + hass.data[DATA_VIEW_REGISTERED] = True + + implementations = hass.data.setdefault(DATA_IMPLEMENTATIONS, {}) + implementations.setdefault(domain, {})[implementation.domain] = implementation + + +async def async_get_implementations( + hass: HomeAssistant, domain: str +) -> Dict[str, AbstractOAuth2Implementation]: + """Return OAuth2 implementations for specified domain.""" + return cast( + Dict[str, AbstractOAuth2Implementation], + hass.data.setdefault(DATA_IMPLEMENTATIONS, {}).get(domain, {}), + ) + + +async def async_get_config_entry_implementation( + hass: HomeAssistant, config_entry: config_entries.ConfigEntry +) -> AbstractOAuth2Implementation: + """Return the implementation for this config entry.""" + implementations = await async_get_implementations(hass, config_entry.domain) + implementation = implementations.get(config_entry.data["auth_implementation"]) + + if implementation is None: + raise ValueError("Implementation not available") + + return implementation + + +class OAuth2AuthorizeCallbackView(HomeAssistantView): + """OAuth2 Authorization Callback View.""" + + requires_auth = False + url = AUTH_CALLBACK_PATH + name = "auth:external:callback" + + async def get(self, request: web.Request) -> web.Response: + """Receive authorization code.""" + if "code" not in request.query or "state" not in request.query: + return web.Response( + text=f"Missing code or state parameter in {request.url}" + ) + + hass = request.app["hass"] + + state = _decode_jwt(hass, request.query["state"]) + + if state is None: + return web.Response(text=f"Invalid state") + + await hass.config_entries.flow.async_configure( + flow_id=state["flow_id"], user_input=request.query["code"] + ) + + return web.Response( + headers={"content-type": "text/html"}, + text="", + ) + + +class OAuth2Session: + """Session to make requests authenticated with OAuth2.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: AbstractOAuth2Implementation, + ): + """Initialize an OAuth2 session.""" + self.hass = hass + self.config_entry = config_entry + self.implementation = implementation + + async def async_ensure_token_valid(self) -> None: + """Ensure that the current token is valid.""" + token = self.config_entry.data["token"] + + if token["expires_at"] > time.time(): + return + + new_token = await self.implementation.async_refresh_token(token) + + self.hass.config_entries.async_update_entry( # type: ignore + self.config_entry, data={**self.config_entry.data, "token": new_token} + ) + + async def async_request( + self, method: str, url: str, **kwargs: Any + ) -> client.ClientResponse: + """Make a request.""" + await self.async_ensure_token_valid() + return await async_oauth2_request( + self.hass, self.config_entry.data["token"], method, url, **kwargs + ) + + +async def async_oauth2_request( + hass: HomeAssistant, token: dict, method: str, url: str, **kwargs: Any +) -> client.ClientResponse: + """Make an OAuth2 authenticated request. + + This method will not refresh tokens. Use OAuth2 session for that. + """ + session = async_get_clientsession(hass) + + return await session.request( + method, + url, + **kwargs, + headers={ + **kwargs.get("headers", {}), + "authorization": f"Bearer {token['access_token']}", + }, + ) + + +@callback +def _encode_jwt(hass: HomeAssistant, data: dict) -> str: + """JWT encode data.""" + secret = hass.data.get(DATA_JWT_SECRET) + + if secret is None: + secret = hass.data[DATA_JWT_SECRET] = generate_secret() + + return jwt.encode(data, secret, algorithm="HS256").decode() + + +@callback +def _decode_jwt(hass: HomeAssistant, encoded: str) -> Optional[dict]: + """JWT encode data.""" + secret = cast(str, hass.data.get(DATA_JWT_SECRET)) + + try: + return jwt.decode(encoded, secret, algorithms=["HS256"]) + except jwt.InvalidTokenError: + return None diff --git a/requirements_all.txt b/requirements_all.txt index 951c980094315f..58a927c81ab69c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1304,7 +1304,7 @@ pymailgunner==1.4 pymediaroom==0.6.4 # homeassistant.components.somfy -pymfy==0.5.2 +pymfy==0.6.0 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9a0013212cf3f..24122915fb55ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -447,7 +447,7 @@ pylitejet==0.1 pymailgunner==1.4 # homeassistant.components.somfy -pymfy==0.5.2 +pymfy==0.6.0 # homeassistant.components.mochad pymochad==0.2.0 diff --git a/tests/common.py b/tests/common.py index 5532e6ccb5c4a5..f40019c5d24272 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1015,14 +1015,23 @@ def mock_entity_platform(hass, platform_path, module): hue.light. """ domain, platform_name = platform_path.split(".") - integration_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + mock_platform(hass, f"{platform_name}.{domain}", module) + + +def mock_platform(hass, platform_path, module=None): + """Mock a platform. + + platform_path is in form hue.config_flow. + """ + domain, platform_name = platform_path.split(".") + integration_cache = hass.data.setdefault(loader.DATA_INTEGRATIONS, {}) module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) - if platform_name not in integration_cache: - mock_integration(hass, MockModule(platform_name)) + if domain not in integration_cache: + mock_integration(hass, MockModule(domain)) _LOGGER.info("Adding mock integration platform: %s", platform_path) - module_cache["{}.{}".format(platform_name, domain)] = module + module_cache[platform_path] = module or Mock() def async_capture_events(hass, event_name): diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index cbc3784e3f5002..d42e7b8e367e4f 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -1,19 +1,35 @@ """Tests for the Somfy config flow.""" import asyncio -from unittest.mock import Mock, patch +from unittest.mock import patch -from pymfy.api.somfy_api import SomfyApi +import pytest -from homeassistant import data_entry_flow +from homeassistant import data_entry_flow, setup, config_entries from homeassistant.components.somfy import config_flow, DOMAIN -from homeassistant.components.somfy.config_flow import register_flow_implementation -from tests.common import MockConfigEntry, mock_coro +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry CLIENT_SECRET_VALUE = "5678" CLIENT_ID_VALUE = "1234" -AUTH_URL = "http://somfy.com" + +@pytest.fixture() +async def mock_impl(hass): + """Mock implementation.""" + await setup.async_setup_component(hass, "http", {}) + + impl = config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + CLIENT_ID_VALUE, + CLIENT_SECRET_VALUE, + "https://accounts.somfy.com/oauth/oauth/v2/auth", + "https://accounts.somfy.com/oauth/oauth/v2/token", + ) + config_flow.SomfyFlowHandler.async_register_implementation(hass, impl) + return impl async def test_abort_if_no_configuration(hass): @@ -30,47 +46,84 @@ async def test_abort_if_existing_entry(hass): flow = config_flow.SomfyFlowHandler() flow.hass = hass MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_setup" + result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_setup" -async def test_full_flow(hass): - """Check classic use case.""" - hass.data[DOMAIN] = {} - register_flow_implementation(hass, CLIENT_ID_VALUE, CLIENT_SECRET_VALUE) - flow = config_flow.SomfyFlowHandler() - flow.hass = hass - hass.config.api = Mock(base_url="https://example.com") - flow._get_authorization_url = Mock(return_value=mock_coro((AUTH_URL, "state"))) - result = await flow.async_step_import() +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "somfy", + { + "somfy": { + "client_id": CLIENT_ID_VALUE, + "client_secret": CLIENT_SECRET_VALUE, + }, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "somfy", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP - assert result["url"] == AUTH_URL - result = await flow.async_step_auth("my_super_code") - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP_DONE - assert result["step_id"] == "creation" - assert flow.code == "my_super_code" - with patch.object( - SomfyApi, "request_token", return_value={"access_token": "super_token"} - ): - result = await flow.async_step_creation() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"]["refresh_args"] == { - "client_id": CLIENT_ID_VALUE, - "client_secret": CLIENT_SECRET_VALUE, + assert result["url"] == ( + "https://accounts.somfy.com/oauth/oauth/v2/auth" + f"?response_type=code&client_id={CLIENT_ID_VALUE}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://accounts.somfy.com/oauth/oauth/v2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch("homeassistant.components.somfy.api.ConfigEntrySomfyApi"): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == "somfy" + + result["data"]["token"].pop("expires_at") + assert result["data"]["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, } - assert result["title"] == "Somfy" - assert result["data"]["token"] == {"access_token": "super_token"} + assert "somfy" in hass.config.components + entry = hass.config_entries.async_entries("somfy")[0] + assert entry.state == config_entries.ENTRY_STATE_LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED -async def test_abort_if_authorization_timeout(hass): + +async def test_abort_if_authorization_timeout(hass, mock_impl): """Check Somfy authorization timeout.""" flow = config_flow.SomfyFlowHandler() flow.hass = hass - flow._get_authorization_url = Mock(side_effect=asyncio.TimeoutError) - result = await flow.async_step_auth() + + with patch.object( + mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + ): + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "authorize_url_timeout" diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py new file mode 100644 index 00000000000000..e47dd834bf7dfe --- /dev/null +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -0,0 +1,266 @@ +"""Tests for the Somfy config flow.""" +import asyncio +import logging +from unittest.mock import patch +import time + +import pytest + +from homeassistant import data_entry_flow, setup, config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import mock_platform, MockConfigEntry + +TEST_DOMAIN = "oauth2_test" +CLIENT_SECRET = "5678" +CLIENT_ID = "1234" +REFRESH_TOKEN = "mock-refresh-token" +ACCESS_TOKEN_1 = "mock-access-token-1" +ACCESS_TOKEN_2 = "mock-access-token-2" +AUTHORIZE_URL = "https://example.como/auth/authorize" +TOKEN_URL = "https://example.como/auth/token" + + +@pytest.fixture +async def local_impl(hass): + """Local implementation.""" + assert await setup.async_setup_component(hass, "http", {}) + return config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, TEST_DOMAIN, CLIENT_ID, CLIENT_SECRET, AUTHORIZE_URL, TOKEN_URL + ) + + +@pytest.fixture +def flow_handler(hass): + """Return a registered config flow.""" + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + class TestFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Test flow handler.""" + + DOMAIN = TEST_DOMAIN + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": "read write"} + + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}): + yield TestFlowHandler + + +class MockOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implementation): + """Mock implementation for testing.""" + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Mock" + + @property + def domain(self) -> str: + """Domain that is providing the implementation.""" + return "test" + + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize.""" + return "http://example.com/auth" + + async def async_resolve_external_data(self, external_data) -> dict: + """Resolve external data to tokens.""" + return external_data + + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh a token.""" + raise NotImplementedError() + + +def test_inherit_enforces_domain_set(): + """Test we enforce setting DOMAIN.""" + + class TestFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Test flow handler.""" + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}): + with pytest.raises(TypeError): + TestFlowHandler() + + +async def test_abort_if_no_implementation(hass, flow_handler): + """Check flow abort when no implementations.""" + flow = flow_handler() + flow.hass = hass + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "missing_configuration" + + +async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl): + """Check timeout generating authorization url.""" + flow_handler.async_register_implementation(hass, local_impl) + + flow = flow_handler() + flow.hass = hass + + with patch.object( + local_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + ): + result = await flow.async_step_user() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "authorize_url_timeout" + + +async def test_full_flow( + hass, flow_handler, local_impl, aiohttp_client, aioclient_mock +): + """Check full flow.""" + hass.config.api.base_url = "https://example.com" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "pick_implementation" + + # Pick implementation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"implementation": TEST_DOMAIN} + ) + + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=read+write" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + TOKEN_URL, + json={ + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "type": "bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == TEST_DOMAIN + + result["data"]["token"].pop("expires_at") + assert result["data"]["token"] == { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "type": "bearer", + "expires_in": 60, + } + + entry = hass.config_entries.async_entries(TEST_DOMAIN)[0] + + assert ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + is local_impl + ) + + +async def test_local_refresh_token(hass, local_impl, aioclient_mock): + """Test we can refresh token.""" + aioclient_mock.post( + TOKEN_URL, json={"access_token": ACCESS_TOKEN_2, "expires_in": 100} + ) + + new_tokens = await local_impl.async_refresh_token( + { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "type": "bearer", + "expires_in": 60, + } + ) + new_tokens.pop("expires_at") + + assert new_tokens == { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_2, + "type": "bearer", + "expires_in": 100, + } + + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": REFRESH_TOKEN, + } + + +async def test_oauth_session(hass, flow_handler, local_impl, aioclient_mock): + """Test the OAuth2 session helper.""" + flow_handler.async_register_implementation(hass, local_impl) + + aioclient_mock.post( + TOKEN_URL, json={"access_token": ACCESS_TOKEN_2, "expires_in": 100} + ) + + aioclient_mock.post("https://example.com", status=201) + + config_entry = MockConfigEntry( + domain=TEST_DOMAIN, + data={ + "auth_implementation": TEST_DOMAIN, + "token": { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "expires_in": 10, + "expires_at": 0, # Forces a refresh, + "token_type": "bearer", + "random_other_data": "should_stay", + }, + }, + ) + + now = time.time() + session = config_entry_oauth2_flow.OAuth2Session(hass, config_entry, local_impl) + resp = await session.async_request("post", "https://example.com") + assert resp.status == 201 + + # Refresh token, make request + assert len(aioclient_mock.mock_calls) == 2 + + assert ( + aioclient_mock.mock_calls[1][3]["authorization"] == f"Bearer {ACCESS_TOKEN_2}" + ) + + assert config_entry.data["token"]["refresh_token"] == REFRESH_TOKEN + assert config_entry.data["token"]["access_token"] == ACCESS_TOKEN_2 + assert config_entry.data["token"]["expires_in"] == 100 + assert config_entry.data["token"]["random_other_data"] == "should_stay" + assert round(config_entry.data["token"]["expires_at"] - now) == 100 From 2a95180d3b7d06cfef4e66969e6963c180e6eed0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 18 Oct 2019 23:54:56 +0200 Subject: [PATCH 1014/3953] Move imports in fritzbox, fritz device tracker, fritzdect, fritzbox netmonitor (#27746) * Moved imports to top-level in fritzbox_netmonitor component * Moved imports to top-level in fritz, fritzbox and fritzdect --- homeassistant/components/fritz/device_tracker.py | 2 +- homeassistant/components/fritzbox/__init__.py | 4 ++-- .../components/fritzbox_netmonitor/sensor.py | 8 ++++---- homeassistant/components/fritzdect/switch.py | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index c2b1e3ab54ede6..ab4deec96f771e 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -4,13 +4,13 @@ from fritzconnection import FritzHosts # pylint: disable=import-error import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index a053bc6c7caad2..40aa3a881d12de 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,9 +1,9 @@ """Support for AVM Fritz!Box smarthome devices.""" import logging +from pyfritzhome import Fritzhome, LoginError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -12,6 +12,7 @@ EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -52,7 +53,6 @@ def setup(hass, config): """Set up the fritzbox component.""" - from pyfritzhome import Fritzhome, LoginError fritz_list = [] diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py index 92a29e37c5161f..0a82c5e29c32d8 100644 --- a/homeassistant/components/fritzbox_netmonitor/sensor.py +++ b/homeassistant/components/fritzbox_netmonitor/sensor.py @@ -1,18 +1,18 @@ """Support for monitoring an AVM Fritz!Box router.""" -import logging from datetime import timedelta -from requests.exceptions import RequestException +import logging from fritzconnection import FritzStatus # pylint: disable=import-error from fritzconnection.fritzconnection import ( # pylint: disable=import-error FritzConnectionException, ) +from requests.exceptions import RequestException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST, STATE_UNAVAILABLE -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fritzdect/switch.py b/homeassistant/components/fritzdect/switch.py index dcb700d6636663..cc629c54dc386a 100644 --- a/homeassistant/components/fritzdect/switch.py +++ b/homeassistant/components/fritzdect/switch.py @@ -1,20 +1,21 @@ """Support for FRITZ!DECT Switches.""" import logging -from requests.exceptions import RequestException, HTTPError - +from fritzhome.fritz import FritzBox +from requests.exceptions import HTTPError, RequestException import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, - POWER_WATT, ENERGY_KILO_WATT_HOUR, + POWER_WATT, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE _LOGGER = logging.getLogger(__name__) @@ -42,7 +43,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Add all switches connected to Fritz Box.""" - from fritzhome.fritz import FritzBox host = config.get(CONF_HOST) username = config.get(CONF_USERNAME) From c333daab107c57b5000cb6bbb155fb7c4404ac78 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 23:58:00 +0200 Subject: [PATCH 1015/3953] Move imports in cppm_tracker component (#27889) --- .../components/cppm_tracker/device_tracker.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index c1c62a26dd905a..1bb723091d446c 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -1,15 +1,17 @@ """Support for ClearPass Policy Manager.""" -import logging from datetime import timedelta +import logging +from clearpasspy import ClearPass import voluptuous as vol -import homeassistant.helpers.config_validation as cv + from homeassistant.components.device_tracker import ( + DOMAIN, PLATFORM_SCHEMA, DeviceScanner, - DOMAIN, ) -from homeassistant.const import CONF_HOST, CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_HOST +import homeassistant.helpers.config_validation as cv SCAN_INTERVAL = timedelta(seconds=120) @@ -30,7 +32,6 @@ def get_scanner(hass, config): """Initialize Scanner.""" - from clearpasspy import ClearPass data = { "server": config[DOMAIN][CONF_HOST], From dc30119d2042a3992f9dd3f6b7234e20da6e2691 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:00:00 +0200 Subject: [PATCH 1016/3953] Move imports in concord232 component (#27887) --- .../components/concord232/alarm_control_panel.py | 8 ++++---- homeassistant/components/concord232/binary_sensor.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index e86ec02040e3b3..37bbf05283812c 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -2,22 +2,23 @@ import datetime import logging +from concord232 import client as concord232_client import requests import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm -import homeassistant.helpers.config_validation as cv from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( + CONF_CODE, CONF_HOST, + CONF_MODE, CONF_NAME, CONF_PORT, - CONF_CODE, - CONF_MODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -60,7 +61,6 @@ class Concord232Alarm(alarm.AlarmControlPanel): def __init__(self, url, name, code, mode): """Initialize the Concord232 alarm panel.""" - from concord232 import client as concord232_client self._state = None self._name = name diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 1a406d743b7cfa..2d119e2cf8601a 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -2,13 +2,14 @@ import datetime import logging +from concord232 import client as concord232_client import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( - BinarySensorDevice, - PLATFORM_SCHEMA, DEVICE_CLASSES, + PLATFORM_SCHEMA, + BinarySensorDevice, ) from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -42,7 +43,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Concord232 binary sensor platform.""" - from concord232 import client as concord232_client host = config.get(CONF_HOST) port = config.get(CONF_PORT) From 93db814b15a976ddc25236e4f16382968f2816ef Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:01:03 +0200 Subject: [PATCH 1017/3953] Move imports in comfoconnect component (#27886) --- .../components/comfoconnect/__init__.py | 13 ++++++------- homeassistant/components/comfoconnect/fan.py | 17 ++++++++--------- homeassistant/components/comfoconnect/sensor.py | 17 +++++++++-------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index 22e9d95bbd8dd7..aef4bf1deebca7 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -1,6 +1,12 @@ """Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging +from pycomfoconnect import ( + SENSOR_TEMPERATURE_EXTRACT, + SENSOR_TEMPERATURE_OUTDOOR, + Bridge, + ComfoConnect, +) import voluptuous as vol from homeassistant.const import ( @@ -56,7 +62,6 @@ def setup(hass, config): """Set up the ComfoConnect bridge.""" - from pycomfoconnect import Bridge conf = config[DOMAIN] host = conf.get(CONF_HOST) @@ -97,7 +102,6 @@ class ComfoConnectBridge: def __init__(self, hass, bridge, name, token, friendly_name, pin): """Initialize the ComfoConnect bridge.""" - from pycomfoconnect import ComfoConnect self.data = {} self.name = name @@ -125,11 +129,6 @@ def sensor_callback(self, var, value): """Call function for sensor updates.""" _LOGGER.debug("Got value from bridge: %d = %d", var, value) - from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, - SENSOR_TEMPERATURE_OUTDOOR, - ) - if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]: self.data[var] = value / 10 else: diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 6c90ab8cba131a..bbb4b0176bf84c 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -1,6 +1,14 @@ """Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging +from pycomfoconnect import ( + CMD_FAN_MODE_AWAY, + CMD_FAN_MODE_HIGH, + CMD_FAN_MODE_LOW, + CMD_FAN_MODE_MEDIUM, + SENSOR_FAN_SPEED_MODE, +) + from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -30,7 +38,6 @@ class ComfoConnectFan(FanEntity): def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None: """Initialize the ComfoConnect fan.""" - from pycomfoconnect import SENSOR_FAN_SPEED_MODE self._ccb = ccb self._name = name @@ -64,7 +71,6 @@ def supported_features(self) -> int: @property def speed(self): """Return the current fan mode.""" - from pycomfoconnect import SENSOR_FAN_SPEED_MODE try: speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] @@ -91,13 +97,6 @@ def set_speed(self, speed: str): """Set fan speed.""" _LOGGER.debug("Changing fan speed to %s.", speed) - from pycomfoconnect import ( - CMD_FAN_MODE_AWAY, - CMD_FAN_MODE_LOW, - CMD_FAN_MODE_MEDIUM, - CMD_FAN_MODE_HIGH, - ) - if speed == SPEED_OFF: self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY) elif speed == SPEED_LOW: diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 4099804d4132bf..06d0506e2cf39c 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -1,6 +1,15 @@ """Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging +from pycomfoconnect import ( + SENSOR_FAN_EXHAUST_FLOW, + SENSOR_FAN_SUPPLY_FLOW, + SENSOR_HUMIDITY_EXTRACT, + SENSOR_HUMIDITY_OUTDOOR, + SENSOR_TEMPERATURE_EXTRACT, + SENSOR_TEMPERATURE_OUTDOOR, +) + from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.helpers.entity import Entity @@ -24,14 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ComfoConnect fan platform.""" - from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, - SENSOR_HUMIDITY_EXTRACT, - SENSOR_TEMPERATURE_OUTDOOR, - SENSOR_HUMIDITY_OUTDOOR, - SENSOR_FAN_SUPPLY_FLOW, - SENSOR_FAN_EXHAUST_FLOW, - ) global SENSOR_TYPES SENSOR_TYPES = { From b11dc0f50fa6a580961dc0642f6d2936f3813f92 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:04:37 +0200 Subject: [PATCH 1018/3953] Move imports in coinmarketcap component (#27885) --- homeassistant/components/coinmarketcap/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coinmarketcap/sensor.py b/homeassistant/components/coinmarketcap/sensor.py index fbe05187684991..ca166aa793a1ad 100644 --- a/homeassistant/components/coinmarketcap/sensor.py +++ b/homeassistant/components/coinmarketcap/sensor.py @@ -1,13 +1,14 @@ """Details about crypto currencies from CoinMarketCap.""" -import logging from datetime import timedelta +import logging from urllib.error import HTTPError +from coinmarketcap import Market import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_DISPLAY_CURRENCY +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -159,6 +160,5 @@ def __init__(self, currency_id, display_currency): def update(self): """Get the latest data from coinmarketcap.com.""" - from coinmarketcap import Market self.ticker = Market().ticker(self.currency_id, convert=self.display_currency) From 2e416168cf6b06c6b80cc13ca78df4a85fa901a9 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:05:42 +0200 Subject: [PATCH 1019/3953] Move imports in coinbase component (#27884) --- homeassistant/components/coinbase/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 6eca0616ca85c5..67869e6b88cf64 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from coinbase.wallet.client import Client +from coinbase.wallet.error import AuthenticationError import voluptuous as vol from homeassistant.const import CONF_API_KEY @@ -79,7 +81,6 @@ class CoinbaseData: def __init__(self, api_key, api_secret): """Init the coinbase data object.""" - from coinbase.wallet.client import Client self.client = Client(api_key, api_secret) self.update() @@ -87,7 +88,6 @@ def __init__(self, api_key, api_secret): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from coinbase.""" - from coinbase.wallet.error import AuthenticationError try: self.accounts = self.client.get_accounts() From d7a8a635ba5b8826aaa46ff200fd0f5067797a44 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:14:49 +0200 Subject: [PATCH 1020/3953] Move imports in ciscospark component (#27879) --- homeassistant/components/ciscospark/notify.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ciscospark/notify.py b/homeassistant/components/ciscospark/notify.py index 6760976636602d..e765aff05f63a4 100644 --- a/homeassistant/components/ciscospark/notify.py +++ b/homeassistant/components/ciscospark/notify.py @@ -1,16 +1,16 @@ """Cisco Spark platform for notify component.""" import logging +from ciscosparkapi import CiscoSparkAPI, SparkApiError import voluptuous as vol -from homeassistant.const import CONF_TOKEN -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_TOKEN +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -33,7 +33,6 @@ class CiscoSparkNotificationService(BaseNotificationService): def __init__(self, token, default_room): """Initialize the service.""" - from ciscosparkapi import CiscoSparkAPI self._default_room = default_room self._token = token @@ -41,7 +40,6 @@ def __init__(self, token, default_room): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from ciscosparkapi import SparkApiError try: title = "" From 7ed5616faac0004008548a9977a9bb25a1bb7222 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:23:17 +0200 Subject: [PATCH 1021/3953] Move imports in cisco_webex_teams component (#27878) --- homeassistant/components/cisco_webex_teams/notify.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index a77f5673df723e..6f80fa138d4b72 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -2,11 +2,12 @@ import logging import voluptuous as vol +from webexteamssdk import ApiError, WebexTeamsAPI, exceptions from homeassistant.components.notify import ( + ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService, - ATTR_TITLE, ) from homeassistant.const import CONF_TOKEN import homeassistant.helpers.config_validation as cv @@ -22,7 +23,6 @@ def get_service(hass, config, discovery_info=None): """Get the CiscoWebexTeams notification service.""" - from webexteamssdk import WebexTeamsAPI, exceptions client = WebexTeamsAPI(access_token=config[CONF_TOKEN]) try: @@ -45,7 +45,6 @@ def __init__(self, client, room): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from webexteamssdk import ApiError title = "" if kwargs.get(ATTR_TITLE) is not None: From b2b140e8d0b0809d60968e4b73dc19cf9ec39a74 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:26:58 +0200 Subject: [PATCH 1022/3953] Move imports in cmus component (#27883) --- homeassistant/components/cmus/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index dbaa763c46174b..3daf0bac828e81 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -1,9 +1,10 @@ """Support for interacting with and controlling the cmus music player.""" import logging +from pycmus import exceptions, remote import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -57,7 +58,6 @@ def setup_platform(hass, config, add_entities, discover_info=None): """Set up the CMUS platform.""" - from pycmus import exceptions host = config.get(CONF_HOST) password = config.get(CONF_PASSWORD) @@ -78,7 +78,6 @@ class CmusDevice(MediaPlayerDevice): # pylint: disable=no-member def __init__(self, server, password, port, name): """Initialize the CMUS device.""" - from pycmus import remote if server: self.cmus = remote.PyCmus(server=server, password=password, port=port) From 1d8e366278c7f0e041e2da2a6bd2ae026c1b7dba Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:39:37 +0200 Subject: [PATCH 1023/3953] Move imports in cloud component (#27881) --- homeassistant/components/cloud/__init__.py | 8 ++++---- homeassistant/components/cloud/http_api.py | 23 ++++++++++------------ 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 71550fc37b14ba..a2c79fdc0a721b 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -1,6 +1,7 @@ """Component to integrate the Home Assistant cloud.""" import logging +from hass_nabucasa import Cloud import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN @@ -20,25 +21,26 @@ from homeassistant.util.aiohttp import MockRequest from . import http_api +from .client import CloudClient from .const import ( CONF_ACME_DIRECTORY_SERVER, CONF_ALEXA, + CONF_ALEXA_ACCESS_TOKEN_URL, CONF_ALIASES, CONF_CLOUDHOOK_CREATE_URL, CONF_COGNITO_CLIENT_ID, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_GOOGLE_ACTIONS, + CONF_GOOGLE_ACTIONS_REPORT_STATE_URL, CONF_GOOGLE_ACTIONS_SYNC_URL, CONF_RELAYER, CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, CONF_USER_POOL_ID, - CONF_GOOGLE_ACTIONS_REPORT_STATE_URL, DOMAIN, MODE_DEV, MODE_PROD, - CONF_ALEXA_ACCESS_TOKEN_URL, ) from .prefs import CloudPreferences @@ -166,8 +168,6 @@ def is_cloudhook_request(request): async def async_setup(hass, config): """Initialize the Home Assistant cloud.""" - from hass_nabucasa import Cloud - from .client import CloudClient # Process configs if DOMAIN in config: diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index f243eab8fd0df4..97c96b0a3e88d7 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -3,33 +3,34 @@ from functools import wraps import logging -import attr import aiohttp import async_timeout +import attr +from hass_nabucasa import Cloud, auth +from hass_nabucasa.const import STATE_DISCONNECTED import voluptuous as vol -from hass_nabucasa import Cloud -from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components import websocket_api -from homeassistant.components.websocket_api import const as ws_const from homeassistant.components.alexa import ( entities as alexa_entities, errors as alexa_errors, ) from homeassistant.components.google_assistant import helpers as google_helpers +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.websocket_api import const as ws_const +from homeassistant.core import callback from .const import ( DOMAIN, - REQUEST_TIMEOUT, + PREF_ALEXA_REPORT_STATE, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, + PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_SECURE_DEVICES_PIN, + REQUEST_TIMEOUT, InvalidTrustedNetworks, InvalidTrustedProxies, - PREF_ALEXA_REPORT_STATE, - PREF_GOOGLE_REPORT_STATE, RequireRelink, ) @@ -104,8 +105,6 @@ async def async_setup(hass): hass.http.register_view(CloudResendConfirmView) hass.http.register_view(CloudForgotPasswordView) - from hass_nabucasa import auth - _CLOUD_ERRORS.update( { auth.UserNotFound: (400, "User does not exist."), @@ -320,7 +319,6 @@ def with_cloud_auth(hass, connection, msg): @websocket_api.async_response async def websocket_subscription(hass, connection, msg): """Handle request for account info.""" - from hass_nabucasa.const import STATE_DISCONNECTED cloud = hass.data[DOMAIN] @@ -417,7 +415,6 @@ async def websocket_hook_delete(hass, connection, msg): def _account_data(cloud): """Generate the auth data JSON response.""" - from hass_nabucasa.const import STATE_DISCONNECTED if not cloud.is_logged_in: return {"logged_in": False, "cloud": STATE_DISCONNECTED} From 8e3d21081868a7599392a4b5f0f0ff7f21137862 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 19 Oct 2019 00:41:11 +0200 Subject: [PATCH 1024/3953] Add remove function to hue sensors (#27652) * Add remove function to sensors * Fix + comments * Update light.py --- homeassistant/components/hue/helpers.py | 33 +++++++++++++++++++ homeassistant/components/hue/light.py | 36 ++++++--------------- homeassistant/components/hue/sensor_base.py | 13 ++++++-- 3 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/hue/helpers.py diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py new file mode 100644 index 00000000000000..388046bb8cb5c5 --- /dev/null +++ b/homeassistant/components/hue/helpers.py @@ -0,0 +1,33 @@ +"""Helper functions for Philips Hue.""" +from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg + +from .const import DOMAIN + + +async def remove_devices(hass, config_entry, api_ids, current): + """Get items that are removed from api.""" + removed_items = [] + + for item_id in current: + if item_id in api_ids: + continue + + # Device is removed from Hue, so we remove it from Home Assistant + entity = current[item_id] + removed_items.append(item_id) + await entity.async_remove() + ent_registry = await get_ent_reg(hass) + if entity.entity_id in ent_registry.entities: + ent_registry.async_remove(entity.entity_id) + dev_registry = await get_dev_reg(hass) + device = dev_registry.async_get_device( + identifiers={(DOMAIN, entity.device_id)}, connections=set() + ) + if device is not None: + dev_registry.async_update_device( + device.id, remove_config_entry_id=config_entry.entry_id + ) + + for item_id in removed_items: + del current[item_id] diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 5a3379f71ce880..dcae1cf4f5dab5 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -8,9 +8,6 @@ import aiohue import async_timeout -from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg - from homeassistant.components import hue from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -32,6 +29,7 @@ Light, ) from homeassistant.util import color +from .helpers import remove_devices SCAN_INTERVAL = timedelta(seconds=5) @@ -226,7 +224,6 @@ async def async_update_items( bridge.available = True new_items = [] - removed_items = [] for item_id in api: if item_id not in current: @@ -238,31 +235,11 @@ async def async_update_items( elif item_id not in progress_waiting: current[item_id].async_schedule_update_ha_state() - for item_id in current: - if item_id in api: - continue - - # Device is removed from Hue, so we remove it from Home Assistant - entity = current[item_id] - removed_items.append(item_id) - await entity.async_remove() - ent_registry = await get_ent_reg(hass) - if entity.entity_id in ent_registry.entities: - ent_registry.async_remove(entity.entity_id) - dev_registry = await get_dev_reg(hass) - device = dev_registry.async_get_device( - identifiers={(hue.DOMAIN, entity.unique_id)}, connections=set() - ) - dev_registry.async_update_device( - device.id, remove_config_entry_id=config_entry.entry_id - ) + await remove_devices(hass, config_entry, api, current) if new_items: async_add_entities(new_items) - for item_id in removed_items: - del current[item_id] - class HueLight(Light): """Representation of a Hue light.""" @@ -300,9 +277,14 @@ def __init__(self, light, request_bridge_update, bridge, is_group=False): @property def unique_id(self): - """Return the ID of this Hue light.""" + """Return the unique ID of this Hue light.""" return self.light.uniqueid + @property + def device_id(self): + """Return the ID of this Hue light.""" + return self.unique_id + @property def name(self): """Return the name of the Hue light.""" @@ -384,7 +366,7 @@ def device_info(self): return None return { - "identifiers": {(hue.DOMAIN, self.unique_id)}, + "identifiers": {(hue.DOMAIN, self.device_id)}, "name": self.name, "manufacturer": self.light.manufacturername, # productname added in Hue Bridge API 1.24 diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 96b9b8bf5d6f83..3f202d38bc5e38 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -11,6 +11,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow +from .helpers import remove_devices CURRENT_SENSORS = "current_sensors" SENSOR_MANAGER_FORMAT = "{}_sensor_manager" @@ -34,7 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities, binary=False sm_key = SENSOR_MANAGER_FORMAT.format(config_entry.data["host"]) manager = hass.data[hue.DOMAIN].get(sm_key) if manager is None: - manager = SensorManager(hass, bridge) + manager = SensorManager(hass, bridge, config_entry) hass.data[hue.DOMAIN][sm_key] = manager manager.register_component(binary, async_add_entities) @@ -50,7 +51,7 @@ class SensorManager: SCAN_INTERVAL = timedelta(seconds=5) sensor_config_map = {} - def __init__(self, hass, bridge): + def __init__(self, hass, bridge, config_entry): """Initialize the sensor manager.""" import aiohue from .binary_sensor import HuePresence, PRESENCE_NAME_FORMAT @@ -63,6 +64,7 @@ def __init__(self, hass, bridge): self.hass = hass self.bridge = bridge + self.config_entry = config_entry self._component_add_entities = {} self._started = False @@ -194,6 +196,13 @@ async def async_update_items(self): else: new_sensors.append(current[api[item_id].uniqueid]) + await remove_devices( + self.hass, + self.config_entry, + [value.uniqueid for value in api.values()], + current, + ) + async_add_sensor_entities = self._component_add_entities.get(False) async_add_binary_entities = self._component_add_entities.get(True) if new_sensors and async_add_sensor_entities: From 535da96d4d85759c9adf96947380922bc345ceb9 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Fri, 18 Oct 2019 15:56:59 -0700 Subject: [PATCH 1025/3953] Move imports to top for hikvisioncam (#27895) --- homeassistant/components/hikvisioncam/switch.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index 05bce5f4eac7b3..020b894c0f76bd 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -1,20 +1,22 @@ """Support turning on/off motion detection on Hikvision cameras.""" import logging +import hikvision.api +from hikvision.error import HikvisionError, MissingParamError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, + CONF_NAME, CONF_PASSWORD, - CONF_USERNAME, CONF_PORT, + CONF_USERNAME, STATE_OFF, STATE_ON, ) -from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity # This is the last working version, please test before updating @@ -38,9 +40,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hikvision camera.""" - import hikvision.api - from hikvision.error import HikvisionError, MissingParamError - host = config.get(CONF_HOST) port = config.get(CONF_PORT) name = config.get(CONF_NAME) From 29ef49fdd9c1b7429d570cae03de22332748ac56 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:57:59 +0200 Subject: [PATCH 1026/3953] Move imports in coolmaster component (#27888) --- homeassistant/components/coolmaster/climate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 8a319c655f6f0d..71115a9eebba7b 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -2,16 +2,17 @@ import logging +from pycoolmasternet import CoolMasterNet import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_OFF, - HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -70,7 +71,6 @@ def _build_entity(device, supported_modes): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the CoolMasterNet climate platform.""" - from pycoolmasternet import CoolMasterNet supported_modes = config.get(CONF_SUPPORTED_MODES) host = config[CONF_HOST] From 8cf443110a0fc9d902560d4676cc5bb3b02b677f Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:59:07 +0200 Subject: [PATCH 1027/3953] Move imports in cisco_mobility_express component (#27877) --- .../components/cisco_mobility_express/device_tracker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index ca24fcb5c52eb4..702ebdfa6112de 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -1,9 +1,9 @@ """Support for Cisco Mobility Express.""" import logging +from ciscomobilityexpress.ciscome import CiscoMobilityExpress import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, @@ -11,11 +11,12 @@ ) from homeassistant.const import ( CONF_HOST, - CONF_USERNAME, CONF_PASSWORD, CONF_SSL, + CONF_USERNAME, CONF_VERIFY_SSL, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,6 @@ def get_scanner(hass, config): """Validate the configuration and return a Cisco ME scanner.""" - from ciscomobilityexpress.ciscome import CiscoMobilityExpress config = config[DOMAIN] From dc5d38128c44e12aa840b685a46e65831b4a1878 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:59:58 +0200 Subject: [PATCH 1028/3953] Move imports in cast component (#27875) --- homeassistant/components/cast/config_flow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index c3c21944d02bc3..5c2b6dca9325a3 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -1,12 +1,14 @@ """Config flow for Cast.""" -from homeassistant.helpers import config_entry_flow +from pychromecast.discovery import discover_chromecasts + from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + from .const import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - from pychromecast.discovery import discover_chromecasts return await hass.async_add_executor_job(discover_chromecasts) From d78f14b20a9e8f0704ad0faaaec92be754ea03fc Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 01:01:59 +0200 Subject: [PATCH 1029/3953] Move imports in canary component (#27874) --- homeassistant/components/canary/__init__.py | 10 +++++----- homeassistant/components/canary/alarm_control_panel.py | 10 ++-------- homeassistant/components/canary/camera.py | 6 ++---- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index f23b6ad46c9de8..a8a45f5b94695e 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -1,13 +1,14 @@ """Support for Canary devices.""" -import logging from datetime import timedelta +import logging -import voluptuous as vol +from canary.api import Api from requests import ConnectTimeout, HTTPError +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -67,7 +68,6 @@ class CanaryData: def __init__(self, username, password, timeout): """Init the Canary data object.""" - from canary.api import Api self._api = Api(username, password, timeout) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 42b5048bc1ddbf..856ecb9f3a2559 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support for Canary alarm.""" import logging +from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT + from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -42,11 +44,6 @@ def name(self): @property def state(self): """Return the state of the device.""" - from canary.api import ( - LOCATION_MODE_AWAY, - LOCATION_MODE_HOME, - LOCATION_MODE_NIGHT, - ) location = self._data.get_location(self._location_id) @@ -75,18 +72,15 @@ def alarm_disarm(self, code=None): def alarm_arm_home(self, code=None): """Send arm home command.""" - from canary.api import LOCATION_MODE_HOME self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME) def alarm_arm_away(self, code=None): """Send arm away command.""" - from canary.api import LOCATION_MODE_AWAY self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY) def alarm_arm_night(self, code=None): """Send arm night command.""" - from canary.api import LOCATION_MODE_NIGHT self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 8a6d27b891663d..7ed1e62ab8a105 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -3,6 +3,8 @@ from datetime import timedelta import logging +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -81,8 +83,6 @@ async def async_camera_image(self): """Return a still image response from the camera.""" self.renew_live_stream_session() - from haffmpeg.tools import ImageFrame, IMAGE_JPEG - ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) image = await asyncio.shield( ffmpeg.get_image( @@ -98,8 +98,6 @@ async def handle_async_mjpeg_stream(self, request): if self._live_stream_session is None: return - from haffmpeg.camera import CameraMjpeg - stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) await stream.open_camera( self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments From 422885b7fd5fc58772baff2987f0a317335dfce3 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 01:02:54 +0200 Subject: [PATCH 1030/3953] Move imports in buienradar component (#27873) --- homeassistant/components/buienradar/sensor.py | 64 +++++++++---------- .../components/buienradar/weather.py | 27 ++++---- 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index ef65db74f165e7..300bcbf2243238 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -5,6 +5,34 @@ import aiohttp import async_timeout +from buienradar.buienradar import parse_data +from buienradar.constants import ( + ATTRIBUTION, + CONDCODE, + CONDITION, + CONTENT, + DATA, + DETAILED, + EXACT, + EXACTNL, + FORECAST, + HUMIDITY, + IMAGE, + MEASURED, + MESSAGE, + PRECIPITATION_FORECAST, + PRESSURE, + STATIONNAME, + STATUS_CODE, + SUCCESS, + TEMPERATURE, + TIMEFRAME, + VISIBILITY, + WINDAZIMUTH, + WINDGUST, + WINDSPEED, +) +from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -22,6 +50,8 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util +from .weather import DEFAULT_TIMEFRAME + _LOGGER = logging.getLogger(__name__) MEASURED_LABEL = "Measured" @@ -183,7 +213,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the buienradar sensor.""" - from .weather import DEFAULT_TIMEFRAME latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -216,7 +245,6 @@ class BrSensor(Entity): def __init__(self, sensor_type, client_name, coordinates): """Initialize the sensor.""" - from buienradar.constants import PRECIPITATION_FORECAST, CONDITION self.client_name = client_name self._name = SENSOR_TYPES[sensor_type][0] @@ -247,23 +275,6 @@ def uid(self, coordinates): def load_data(self, data): """Load the sensor with relevant data.""" # Find sensor - from buienradar.constants import ( - ATTRIBUTION, - CONDITION, - CONDCODE, - DETAILED, - EXACT, - EXACTNL, - FORECAST, - IMAGE, - MEASURED, - PRECIPITATION_FORECAST, - STATIONNAME, - TIMEFRAME, - VISIBILITY, - WINDGUST, - WINDSPEED, - ) # Check if we have a new measurement, # otherwise we do not have to update the sensor @@ -421,7 +432,6 @@ def entity_picture(self): @property def device_state_attributes(self): """Return the state attributes.""" - from buienradar.constants import PRECIPITATION_FORECAST if self.type.startswith(PRECIPITATION_FORECAST): result = {ATTR_ATTRIBUTION: self._attribution} @@ -488,7 +498,6 @@ async def schedule_update(self, minute=1): async def get_data(self, url): """Load data from specified url.""" - from buienradar.constants import CONTENT, MESSAGE, STATUS_CODE, SUCCESS _LOGGER.debug("Calling url: %s...", url) result = {SUCCESS: False, MESSAGE: None} @@ -515,9 +524,6 @@ async def get_data(self, url): async def async_update(self, *_): """Update the data from buienradar.""" - from buienradar.constants import CONTENT, DATA, MESSAGE, STATUS_CODE, SUCCESS - from buienradar.buienradar import parse_data - from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url content = await self.get_data(JSON_FEED_URL) @@ -576,28 +582,24 @@ async def async_update(self, *_): @property def attribution(self): """Return the attribution.""" - from buienradar.constants import ATTRIBUTION return self.data.get(ATTRIBUTION) @property def stationname(self): """Return the name of the selected weatherstation.""" - from buienradar.constants import STATIONNAME return self.data.get(STATIONNAME) @property def condition(self): """Return the condition.""" - from buienradar.constants import CONDITION return self.data.get(CONDITION) @property def temperature(self): """Return the temperature, or None.""" - from buienradar.constants import TEMPERATURE try: return float(self.data.get(TEMPERATURE)) @@ -607,7 +609,6 @@ def temperature(self): @property def pressure(self): """Return the pressure, or None.""" - from buienradar.constants import PRESSURE try: return float(self.data.get(PRESSURE)) @@ -617,7 +618,6 @@ def pressure(self): @property def humidity(self): """Return the humidity, or None.""" - from buienradar.constants import HUMIDITY try: return int(self.data.get(HUMIDITY)) @@ -627,7 +627,6 @@ def humidity(self): @property def visibility(self): """Return the visibility, or None.""" - from buienradar.constants import VISIBILITY try: return int(self.data.get(VISIBILITY)) @@ -637,7 +636,6 @@ def visibility(self): @property def wind_speed(self): """Return the windspeed, or None.""" - from buienradar.constants import WINDSPEED try: return float(self.data.get(WINDSPEED)) @@ -647,7 +645,6 @@ def wind_speed(self): @property def wind_bearing(self): """Return the wind bearing, or None.""" - from buienradar.constants import WINDAZIMUTH try: return int(self.data.get(WINDAZIMUTH)) @@ -657,6 +654,5 @@ def wind_bearing(self): @property def forecast(self): """Return the forecast data.""" - from buienradar.constants import FORECAST return self.data.get(FORECAST) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index d8ae448c9819b2..745bf12ffd8845 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -1,18 +1,28 @@ """Support for Buienradar.nl weather service.""" import logging +from buienradar.constants import ( + CONDCODE, + CONDITION, + DATETIME, + MAX_TEMP, + MIN_TEMP, + RAIN, + WINDAZIMUTH, + WINDSPEED, +) import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, - PLATFORM_SCHEMA, - WeatherEntity, - ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + PLATFORM_SCHEMA, + WeatherEntity, ) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv @@ -110,7 +120,6 @@ def name(self): @property def condition(self): """Return the current condition.""" - from buienradar.constants import CONDCODE if self._data and self._data.condition: ccode = self._data.condition.get(CONDCODE) @@ -161,16 +170,6 @@ def temperature_unit(self): @property def forecast(self): """Return the forecast array.""" - from buienradar.constants import ( - CONDITION, - CONDCODE, - RAIN, - DATETIME, - MIN_TEMP, - MAX_TEMP, - WINDAZIMUTH, - WINDSPEED, - ) if not self._forecast: return None From 6df34a0128cfd6297fab73c23608331b39cc79a1 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 01:04:10 +0200 Subject: [PATCH 1031/3953] Move imports in channels component (#27876) --- homeassistant/components/channels/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 6c3e18cdb05427..6d978a5451e354 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -1,9 +1,10 @@ """Support for interfacing with an instance of getchannels.com.""" import logging +from pychannels import Channels import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( DOMAIN, MEDIA_TYPE_CHANNEL, @@ -124,7 +125,6 @@ class ChannelsPlayer(MediaPlayerDevice): def __init__(self, name, host, port): """Initialize the Channels app.""" - from pychannels import Channels self._name = name self._host = host From 065c6f4c9cfa6a434cd73d3610bf36a093896038 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Sat, 19 Oct 2019 01:05:36 +0200 Subject: [PATCH 1032/3953] Move imports for nilu component (#27896) --- homeassistant/components/nilu/air_quality.py | 48 ++++++++------------ 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index 8d3d61befd5076..8e851592de3807 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -2,6 +2,22 @@ from datetime import timedelta import logging +from niluclient import ( + CO, + CO2, + NO, + NO2, + NOX, + OZONE, + PM1, + PM10, + PM25, + POLLUTION_INDEX, + SO2, + create_location_client, + create_station_client, + lookup_stations_in_area, +) import voluptuous as vol from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity @@ -95,8 +111,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NILU air quality sensor.""" - import niluclient as nilu - name = config.get(CONF_NAME) area = config.get(CONF_AREA) stations = config.get(CONF_STATION) @@ -105,15 +119,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] if area: - stations = nilu.lookup_stations_in_area(area) + stations = lookup_stations_in_area(area) elif not area and not stations: latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - location_client = nilu.create_location_client(latitude, longitude) + location_client = create_location_client(latitude, longitude) stations = location_client.station_names for station in stations: - client = NiluData(nilu.create_station_client(station)) + client = NiluData(create_station_client(station)) client.update() if client.data.sensors: sensors.append(NiluSensor(client, name, show_on_map)) @@ -178,71 +192,51 @@ def air_quality_index(self) -> str: @property def carbon_monoxide(self) -> str: """Return the CO (carbon monoxide) level.""" - from niluclient import CO - return self.get_component_state(CO) @property def carbon_dioxide(self) -> str: """Return the CO2 (carbon dioxide) level.""" - from niluclient import CO2 - return self.get_component_state(CO2) @property def nitrogen_oxide(self) -> str: """Return the N2O (nitrogen oxide) level.""" - from niluclient import NOX - return self.get_component_state(NOX) @property def nitrogen_monoxide(self) -> str: """Return the NO (nitrogen monoxide) level.""" - from niluclient import NO - return self.get_component_state(NO) @property def nitrogen_dioxide(self) -> str: """Return the NO2 (nitrogen dioxide) level.""" - from niluclient import NO2 - return self.get_component_state(NO2) @property def ozone(self) -> str: """Return the O3 (ozone) level.""" - from niluclient import OZONE - return self.get_component_state(OZONE) @property def particulate_matter_2_5(self) -> str: """Return the particulate matter 2.5 level.""" - from niluclient import PM25 - return self.get_component_state(PM25) @property def particulate_matter_10(self) -> str: """Return the particulate matter 10 level.""" - from niluclient import PM10 - return self.get_component_state(PM10) @property def particulate_matter_0_1(self) -> str: """Return the particulate matter 0.1 level.""" - from niluclient import PM1 - return self.get_component_state(PM1) @property def sulphur_dioxide(self) -> str: """Return the SO2 (sulphur dioxide) level.""" - from niluclient import SO2 - return self.get_component_state(SO2) def get_component_state(self, component_name: str) -> str: @@ -254,14 +248,12 @@ def get_component_state(self, component_name: str) -> str: def update(self) -> None: """Update the sensor.""" - import niluclient as nilu - self._api.update() sensors = self._api.data.sensors.values() if sensors: max_index = max([s.pollution_index for s in sensors]) self._max_aqi = max_index - self._attrs[ATTR_POLLUTION_INDEX] = nilu.POLLUTION_INDEX[self._max_aqi] + self._attrs[ATTR_POLLUTION_INDEX] = POLLUTION_INDEX[self._max_aqi] self._attrs[ATTR_AREA] = self._api.data.area From 9e8c391c814d2a2bc6b2acc94074be5b5616b771 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 19 Oct 2019 00:32:15 +0000 Subject: [PATCH 1033/3953] [ci skip] Translation update --- .../components/abode/.translations/fr.json | 14 ++++++- .../components/abode/.translations/it.json | 22 ++++++++++ .../components/abode/.translations/pl.json | 12 ++++++ .../components/abode/.translations/pt-BR.json | 12 ++++++ .../components/abode/.translations/pt.json | 15 +++++++ .../components/adguard/.translations/pt.json | 12 ++++++ .../components/airly/.translations/pt.json | 14 +++++++ .../alarm_control_panel/.translations/da.json | 7 ++++ .../alarm_control_panel/.translations/en.json | 11 +++++ .../alarm_control_panel/.translations/es.json | 11 +++++ .../alarm_control_panel/.translations/it.json | 11 +++++ .../alarm_control_panel/.translations/lb.json | 11 +++++ .../alarm_control_panel/.translations/no.json | 11 +++++ .../.translations/pt-BR.json | 7 ++++ .../alarm_control_panel/.translations/pt.json | 9 ++++ .../alarm_control_panel/.translations/ru.json | 7 ++++ .../alarm_control_panel/.translations/sl.json | 11 +++++ .../.translations/zh-Hant.json | 11 +++++ .../components/axis/.translations/da.json | 1 + .../components/axis/.translations/it.json | 1 + .../components/axis/.translations/lb.json | 1 + .../components/axis/.translations/no.json | 1 + .../components/axis/.translations/ru.json | 1 + .../components/axis/.translations/sl.json | 1 + .../axis/.translations/zh-Hant.json | 1 + .../binary_sensor/.translations/lv.json | 8 ++++ .../binary_sensor/.translations/pt.json | 41 +++++++++++++++++++ .../components/cover/.translations/da.json | 10 +++++ .../components/cover/.translations/it.json | 10 +++++ .../components/cover/.translations/pt.json | 10 +++++ .../components/deconz/.translations/da.json | 1 + .../components/deconz/.translations/fr.json | 1 + .../components/deconz/.translations/it.json | 1 + .../components/deconz/.translations/pt.json | 5 +++ .../components/deconz/.translations/sl.json | 1 + .../components/ecobee/.translations/pt.json | 11 +++++ .../components/heos/.translations/pt.json | 3 +- .../components/light/.translations/lv.json | 8 ++++ .../components/lock/.translations/da.json | 12 ++++++ .../components/lock/.translations/en.json | 5 +++ .../components/lock/.translations/it.json | 8 ++++ .../components/lock/.translations/lb.json | 5 +++ .../components/lock/.translations/no.json | 5 +++ .../components/lock/.translations/ru.json | 5 +++ .../components/lock/.translations/sl.json | 5 +++ .../lock/.translations/zh-Hant.json | 5 +++ .../components/met/.translations/pt.json | 12 ++++++ .../components/neato/.translations/pt-BR.json | 5 +++ .../opentherm_gw/.translations/da.json | 11 +++++ .../opentherm_gw/.translations/en.json | 8 ++-- .../opentherm_gw/.translations/fr.json | 10 +++++ .../opentherm_gw/.translations/lb.json | 11 +++++ .../opentherm_gw/.translations/no.json | 11 +++++ .../opentherm_gw/.translations/pl.json | 10 +++++ .../opentherm_gw/.translations/pt.json | 12 ++++++ .../opentherm_gw/.translations/ru.json | 11 +++++ .../opentherm_gw/.translations/sl.json | 11 +++++ .../opentherm_gw/.translations/zh-Hant.json | 11 +++++ .../components/plex/.translations/pt.json | 21 ++++++++++ .../components/sensor/.translations/hu.json | 26 ++++++++++++ .../sensor/.translations/moon.hu.json | 8 ++-- .../components/sensor/.translations/pt.json | 21 ++++++++++ .../components/soma/.translations/da.json | 10 +++++ .../components/soma/.translations/de.json | 10 +++++ .../components/soma/.translations/es.json | 8 ++++ .../components/soma/.translations/fr.json | 10 +++++ .../components/soma/.translations/it.json | 10 +++++ .../components/soma/.translations/pl.json | 7 ++++ .../components/soma/.translations/pt-BR.json | 12 ++++++ .../components/soma/.translations/pt.json | 12 ++++++ .../components/soma/.translations/sl.json | 10 +++++ .../soma/.translations/zh-Hant.json | 10 +++++ .../components/switch/.translations/lv.json | 12 ++++++ .../transmission/.translations/pt.json | 12 ++++++ .../components/unifi/.translations/pt-BR.json | 6 +++ .../components/unifi/.translations/pt.json | 23 +++++++++++ .../components/wwlln/.translations/pt.json | 12 ++++++ .../components/zha/.translations/pt.json | 8 ++++ 78 files changed, 735 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/abode/.translations/it.json create mode 100644 homeassistant/components/abode/.translations/pl.json create mode 100644 homeassistant/components/abode/.translations/pt-BR.json create mode 100644 homeassistant/components/abode/.translations/pt.json create mode 100644 homeassistant/components/adguard/.translations/pt.json create mode 100644 homeassistant/components/airly/.translations/pt.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/da.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/en.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/es.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/it.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/lb.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/no.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/pt-BR.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/pt.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/ru.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/sl.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/zh-Hant.json create mode 100644 homeassistant/components/binary_sensor/.translations/lv.json create mode 100644 homeassistant/components/binary_sensor/.translations/pt.json create mode 100644 homeassistant/components/cover/.translations/da.json create mode 100644 homeassistant/components/cover/.translations/it.json create mode 100644 homeassistant/components/cover/.translations/pt.json create mode 100644 homeassistant/components/ecobee/.translations/pt.json create mode 100644 homeassistant/components/light/.translations/lv.json create mode 100644 homeassistant/components/lock/.translations/da.json create mode 100644 homeassistant/components/lock/.translations/it.json create mode 100644 homeassistant/components/met/.translations/pt.json create mode 100644 homeassistant/components/neato/.translations/pt-BR.json create mode 100644 homeassistant/components/opentherm_gw/.translations/pt.json create mode 100644 homeassistant/components/plex/.translations/pt.json create mode 100644 homeassistant/components/sensor/.translations/hu.json create mode 100644 homeassistant/components/sensor/.translations/pt.json create mode 100644 homeassistant/components/soma/.translations/pt-BR.json create mode 100644 homeassistant/components/soma/.translations/pt.json create mode 100644 homeassistant/components/switch/.translations/lv.json create mode 100644 homeassistant/components/transmission/.translations/pt.json create mode 100644 homeassistant/components/wwlln/.translations/pt.json diff --git a/homeassistant/components/abode/.translations/fr.json b/homeassistant/components/abode/.translations/fr.json index c2e4b241b90fbf..c0d9b0b577b753 100644 --- a/homeassistant/components/abode/.translations/fr.json +++ b/homeassistant/components/abode/.translations/fr.json @@ -1,12 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "Une seule configuration de Abode est autoris\u00e9e." + }, + "error": { + "connection_error": "Impossible de se connecter \u00e0 Abode.", + "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9.", + "invalid_credentials": "Informations d'identification invalides." + }, "step": { "user": { "data": { "password": "Mot de passe", "username": "Adresse e-mail" - } + }, + "title": "Remplissez vos informations de connexion Abode" } - } + }, + "title": "Abode" } } \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/it.json b/homeassistant/components/abode/.translations/it.json new file mode 100644 index 00000000000000..af51aca8af9297 --- /dev/null +++ b/homeassistant/components/abode/.translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u00c8 consentita una sola configurazione di Abode." + }, + "error": { + "connection_error": "Impossibile connettersi ad Abode.", + "identifier_exists": "Account gi\u00e0 registrato", + "invalid_credentials": "Credenziali non valide" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Indirizzo email" + }, + "title": "Inserisci le tue informazioni di accesso Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/pl.json b/homeassistant/components/abode/.translations/pl.json new file mode 100644 index 00000000000000..09fbdc9324106d --- /dev/null +++ b/homeassistant/components/abode/.translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/pt-BR.json b/homeassistant/components/abode/.translations/pt-BR.json new file mode 100644 index 00000000000000..7a117a819931a2 --- /dev/null +++ b/homeassistant/components/abode/.translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Endere\u00e7o de e-mail" + } + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/pt.json b/homeassistant/components/abode/.translations/pt.json new file mode 100644 index 00000000000000..512bf59906c9a9 --- /dev/null +++ b/homeassistant/components/abode/.translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "identifier_exists": "Conta j\u00e1 registada" + }, + "step": { + "user": { + "data": { + "username": "Endere\u00e7o de e-mail" + } + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/pt.json b/homeassistant/components/adguard/.translations/pt.json new file mode 100644 index 00000000000000..f681da4210f8a3 --- /dev/null +++ b/homeassistant/components/adguard/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/pt.json b/homeassistant/components/airly/.translations/pt.json new file mode 100644 index 00000000000000..d99bcb907334e5 --- /dev/null +++ b/homeassistant/components/airly/.translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/da.json b/homeassistant/components/alarm_control_panel/.translations/da.json new file mode 100644 index 00000000000000..74e02e10de4aa1 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/da.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "trigger": "Udl\u00f8s {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/en.json b/homeassistant/components/alarm_control_panel/.translations/en.json new file mode 100644 index 00000000000000..b8eeb1d2e8c5b1 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/en.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Arm {entity_name} away", + "arm_home": "Arm {entity_name} home", + "arm_night": "Arm {entity_name} night", + "disarm": "Disarm {entity_name}", + "trigger": "Trigger {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/es.json b/homeassistant/components/alarm_control_panel/.translations/es.json new file mode 100644 index 00000000000000..a704080a2b494b --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/es.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armar {entity_name} exterior", + "arm_home": "Armar {entity_name} casa", + "arm_night": "Armar {entity_name}", + "disarm": "Desarmar {entity_name}", + "trigger": "Lanzar {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/it.json b/homeassistant/components/alarm_control_panel/.translations/it.json new file mode 100644 index 00000000000000..e39967e9dacc09 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/it.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armare {entity_name} uscito", + "arm_home": "Armare {entity_name} casa", + "arm_night": "Armare {entity_name} notte", + "disarm": "Disarmare {entity_name}", + "trigger": "Attivazione {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/lb.json b/homeassistant/components/alarm_control_panel/.translations/lb.json new file mode 100644 index 00000000000000..ff265a52c388cc --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/lb.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} fir \u00ebnnerwee uschalten", + "arm_home": "{entity_name} fir doheem uschalten", + "arm_night": "{entity_name} fir Nuecht uschalten", + "disarm": "{entity_name} entsch\u00e4rfen", + "trigger": "{entity_name} ausl\u00e9isen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/no.json b/homeassistant/components/alarm_control_panel/.translations/no.json new file mode 100644 index 00000000000000..93833f33d41539 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/no.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Aktiver {entity_name} borte", + "arm_home": "Aktiver {entity_name} hjemme", + "arm_night": "Aktiver {entity_name} natt", + "disarm": "Deaktiver {entity_name}", + "trigger": "Utl\u00f8ser {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json new file mode 100644 index 00000000000000..1f7c994330d4fe --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "trigger": "Disparar {entidade_nome}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pt.json b/homeassistant/components/alarm_control_panel/.translations/pt.json new file mode 100644 index 00000000000000..90b9b1d43d52f4 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/pt.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "action_type": { + "arm_home": "Armar casa {entity_name}", + "arm_night": "Armar noite {entity_name}", + "disarm": "Desarmar {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/ru.json b/homeassistant/components/alarm_control_panel/.translations/ru.json new file mode 100644 index 00000000000000..acea0ae7551cc0 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/ru.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/sl.json b/homeassistant/components/alarm_control_panel/.translations/sl.json new file mode 100644 index 00000000000000..9bf01fc62de920 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/sl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Vklju\u010di {entity_name} zdoma", + "arm_home": "Vklju\u010di {entity_name} doma", + "arm_night": "Vklju\u010di {entity_name} no\u010d", + "disarm": "Razoro\u017ei {entity_name}", + "trigger": "Spro\u017ei {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json new file mode 100644 index 00000000000000..c52288802d1175 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "\u8a2d\u5b9a {entity_name} \u5916\u51fa\u6a21\u5f0f", + "arm_home": "\u8a2d\u5b9a {entity_name} \u8fd4\u5bb6\u6a21\u5f0f", + "arm_night": "\u8a2d\u5b9a {entity_name} \u591c\u9593\u6a21\u5f0f", + "disarm": "\u89e3\u9664 {entity_name}", + "trigger": "\u89f8\u767c {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/da.json b/homeassistant/components/axis/.translations/da.json index 2d728468fc761d..c169f85f2802d5 100644 --- a/homeassistant/components/axis/.translations/da.json +++ b/homeassistant/components/axis/.translations/da.json @@ -12,6 +12,7 @@ "device_unavailable": "Enheden er ikke tilg\u00e6ngelig", "faulty_credentials": "Ugyldige legitimationsoplysninger" }, + "flow_title": "Axis enhed: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/it.json b/homeassistant/components/axis/.translations/it.json index e979af0883656f..3f303140c68401 100644 --- a/homeassistant/components/axis/.translations/it.json +++ b/homeassistant/components/axis/.translations/it.json @@ -12,6 +12,7 @@ "device_unavailable": "Il dispositivo non \u00e8 disponibile", "faulty_credentials": "Credenziali utente non valide" }, + "flow_title": "Dispositivo Axis: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/lb.json b/homeassistant/components/axis/.translations/lb.json index 281eaa7c8818ba..24ee0e24125459 100644 --- a/homeassistant/components/axis/.translations/lb.json +++ b/homeassistant/components/axis/.translations/lb.json @@ -12,6 +12,7 @@ "device_unavailable": "Apparat ass net erreechbar", "faulty_credentials": "Ong\u00eblteg Login Informatioune" }, + "flow_title": "Axis Apparat: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/no.json b/homeassistant/components/axis/.translations/no.json index 29022e39745363..190737e5a76b22 100644 --- a/homeassistant/components/axis/.translations/no.json +++ b/homeassistant/components/axis/.translations/no.json @@ -12,6 +12,7 @@ "device_unavailable": "Enheten er ikke tilgjengelig", "faulty_credentials": "Ugyldig brukerlegitimasjon" }, + "flow_title": "Akse-enhet: {Name} ({Host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index ae5f0851c44d7d..1128ad30cf5ee8 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -12,6 +12,7 @@ "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, + "flow_title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/sl.json b/homeassistant/components/axis/.translations/sl.json index 205e901553e93f..5ffa02e19f770d 100644 --- a/homeassistant/components/axis/.translations/sl.json +++ b/homeassistant/components/axis/.translations/sl.json @@ -12,6 +12,7 @@ "device_unavailable": "Naprava ni na voljo", "faulty_credentials": "Napa\u010dni uporabni\u0161ki podatki" }, + "flow_title": "OS naprava: {Name} ({Host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json index c0d0df02135192..6c78fc2166c625 100644 --- a/homeassistant/components/axis/.translations/zh-Hant.json +++ b/homeassistant/components/axis/.translations/zh-Hant.json @@ -12,6 +12,7 @@ "device_unavailable": "\u8a2d\u5099\u7121\u6cd5\u4f7f\u7528", "faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548" }, + "flow_title": "Axis \u8a2d\u5099\uff1a{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/.translations/lv.json b/homeassistant/components/binary_sensor/.translations/lv.json new file mode 100644 index 00000000000000..7668dfa5ac8653 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/lv.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} tika izsl\u0113gta", + "turned_on": "{entity_name} tika iesl\u0113gta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/pt.json b/homeassistant/components/binary_sensor/.translations/pt.json new file mode 100644 index 00000000000000..aa16576d2c1395 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/pt.json @@ -0,0 +1,41 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "a bateria {entity_name} est\u00e1 baixa", + "is_cold": "{entity_name} est\u00e1 frio", + "is_connected": "{entity_name} est\u00e1 ligado", + "is_gas": "{entity_name} est\u00e1 a detectar g\u00e1s", + "is_hot": "{entity_name} est\u00e1 quente", + "is_light": "{entity_name} est\u00e1 a detectar luz", + "is_locked": "{entity_name} est\u00e1 fechado", + "is_moist": "{entity_name} est\u00e1 h\u00famido", + "is_motion": "{entity_name} est\u00e1 a detectar movimento", + "is_moving": "{entity_name} est\u00e1 a mexer", + "is_not_open": "{entity_name} est\u00e1 fechada", + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado", + "is_vibration": "{entity_name} est\u00e1 a detectar vibra\u00e7\u00f5es" + }, + "trigger_type": { + "closed": "{entity_name} est\u00e1 fechado", + "moist": "ficou h\u00famido {entity_name}", + "not_opened": "fechado {entity_name}", + "not_plugged_in": "{entity_name} desligado", + "not_powered": "{entity_name} n\u00e3o alimentado", + "not_present": "ausente {entity_name}", + "not_unsafe": "ficou seguro {entity_name}", + "occupied": "ficou ocupado {entity_name}", + "opened": "{entity_name} aberto", + "plugged_in": "{entity_name} ligado", + "powered": "{entity_name} alimentado", + "present": "{entity_name} presente", + "problem": "foi detectado problema em {entity_name}", + "smoke": "foi detectado fumo em {entity_name}", + "sound": "foram detectadas sons em {entity_name}", + "turned_off": "foi desligado {entity_name}", + "turned_on": "foi ligado {entity_name}", + "unsafe": "ficou inseguro {entity_name}", + "vibration": "foram detectadas vibra\u00e7\u00f5es em {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/da.json b/homeassistant/components/cover/.translations/da.json new file mode 100644 index 00000000000000..e603723b56449f --- /dev/null +++ b/homeassistant/components/cover/.translations/da.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} er lukket", + "is_closing": "{entity_name} lukker", + "is_open": "{entity_name} er \u00e5ben", + "is_opening": "{entity_name} \u00e5bnes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/it.json b/homeassistant/components/cover/.translations/it.json new file mode 100644 index 00000000000000..6a25c0f3f2ffe8 --- /dev/null +++ b/homeassistant/components/cover/.translations/it.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u00e8 chiuso", + "is_closing": "{entity_name} si sta chiudendo", + "is_open": "{entity_name} \u00e8 aperto", + "is_opening": "{entity_name} si sta aprendo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/pt.json b/homeassistant/components/cover/.translations/pt.json new file mode 100644 index 00000000000000..cb9f85c4a939f6 --- /dev/null +++ b/homeassistant/components/cover/.translations/pt.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est\u00e1 fechada", + "is_closing": "{entity_name} est\u00e1 a fechar", + "is_open": "{entity_name} est\u00e1 aberta", + "is_opening": "{entity_name} est\u00e1 a abrir" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index 6b74c09107a098..ec9c4dc35b16f9 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -11,6 +11,7 @@ "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8gle" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 3729f7f556afb9..d1fc7fa7286f17 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -11,6 +11,7 @@ "error": { "no_key": "Impossible d'obtenir une cl\u00e9 d'API" }, + "flow_title": "Passerelle deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 1f0b344a32d6ba..975d69a450f75a 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -11,6 +11,7 @@ "error": { "no_key": "Impossibile ottenere una API key" }, + "flow_title": "Gateway Zigbee deCONZ ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index 47f5bb7db59a06..63a66595ace417 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -29,5 +29,10 @@ } }, "title": "Gateway Zigbee deCONZ" + }, + "device_automation": { + "trigger_subtype": { + "left": "Esquerda" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 0717bcfc39f455..217007c07d40fc 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -11,6 +11,7 @@ "error": { "no_key": "Klju\u010da API ni mogo\u010de dobiti" }, + "flow_title": "deCONZ Zigbee prehod ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/ecobee/.translations/pt.json b/homeassistant/components/ecobee/.translations/pt.json new file mode 100644 index 00000000000000..20bba0ede4bf1a --- /dev/null +++ b/homeassistant/components/ecobee/.translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/pt.json b/homeassistant/components/heos/.translations/pt.json index 33c83fdc738af5..099d1978436aa5 100644 --- a/homeassistant/components/heos/.translations/pt.json +++ b/homeassistant/components/heos/.translations/pt.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "access_token": "Servidor" + "access_token": "Servidor", + "host": "Servidor" } } }, diff --git a/homeassistant/components/light/.translations/lv.json b/homeassistant/components/light/.translations/lv.json new file mode 100644 index 00000000000000..7668dfa5ac8653 --- /dev/null +++ b/homeassistant/components/light/.translations/lv.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} tika izsl\u0113gta", + "turned_on": "{entity_name} tika iesl\u0113gta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/da.json b/homeassistant/components/lock/.translations/da.json new file mode 100644 index 00000000000000..de4f603ac43f38 --- /dev/null +++ b/homeassistant/components/lock/.translations/da.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "lock": "L\u00e5s {entity_name}", + "open": "\u00c5ben {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} er l\u00e5st", + "is_unlocked": "{entity_name} er l\u00e5st op" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/en.json b/homeassistant/components/lock/.translations/en.json index a4b69197a91f2e..a9800eecadd77d 100644 --- a/homeassistant/components/lock/.translations/en.json +++ b/homeassistant/components/lock/.translations/en.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Lock {entity_name}", + "open": "Open {entity_name}", + "unlock": "Unlock {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} is locked", "is_unlocked": "{entity_name} is unlocked" diff --git a/homeassistant/components/lock/.translations/it.json b/homeassistant/components/lock/.translations/it.json new file mode 100644 index 00000000000000..f56ef71060bc60 --- /dev/null +++ b/homeassistant/components/lock/.translations/it.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} \u00e8 bloccato", + "is_unlocked": "{entity_name} \u00e8 sbloccato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/lb.json b/homeassistant/components/lock/.translations/lb.json index 4526b8fb6744b4..90dd7e6087aebd 100644 --- a/homeassistant/components/lock/.translations/lb.json +++ b/homeassistant/components/lock/.translations/lb.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "{entity_name} sp\u00e4ren", + "open": "{entity_name} opmaachen", + "unlock": "{entity_name} entsp\u00e4ren" + }, "condition_type": { "is_locked": "{entity_name} ass gespaart", "is_unlocked": "{entity_name} ass entspaart" diff --git a/homeassistant/components/lock/.translations/no.json b/homeassistant/components/lock/.translations/no.json index c60fc52ce5394a..28c809a82d1545 100644 --- a/homeassistant/components/lock/.translations/no.json +++ b/homeassistant/components/lock/.translations/no.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "L\u00e5s {entity_name}", + "open": "\u00c5pne {entity_name}", + "unlock": "L\u00e5s opp {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} er l\u00e5st", "is_unlocked": "{entity_name} er l\u00e5st opp" diff --git a/homeassistant/components/lock/.translations/ru.json b/homeassistant/components/lock/.translations/ru.json index f74df838ae5d42..1610668721f30b 100644 --- a/homeassistant/components/lock/.translations/ru.json +++ b/homeassistant/components/lock/.translations/ru.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c {entity_name}", + "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c {entity_name}", + "unlock": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_unlocked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" diff --git a/homeassistant/components/lock/.translations/sl.json b/homeassistant/components/lock/.translations/sl.json index 3c3fd5defbc9f1..d2e32499d2e1db 100644 --- a/homeassistant/components/lock/.translations/sl.json +++ b/homeassistant/components/lock/.translations/sl.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Zakleni {entity_name}", + "open": "Odpri {entity_name}", + "unlock": "Odkleni {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} je/so zaklenjen/a", "is_unlocked": "{entity_name} je/so odklenjen/a" diff --git a/homeassistant/components/lock/.translations/zh-Hant.json b/homeassistant/components/lock/.translations/zh-Hant.json index a423c4331b738e..7c8abb76e16f71 100644 --- a/homeassistant/components/lock/.translations/zh-Hant.json +++ b/homeassistant/components/lock/.translations/zh-Hant.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "\u4e0a\u9396 {entity_name}", + "open": "\u958b\u555f {entity_name}", + "unlock": "\u89e3\u9396 {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} \u5df2\u4e0a\u9396", "is_unlocked": "{entity_name} \u5df2\u89e3\u9396" diff --git a/homeassistant/components/met/.translations/pt.json b/homeassistant/components/met/.translations/pt.json new file mode 100644 index 00000000000000..c7081cd694a0af --- /dev/null +++ b/homeassistant/components/met/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/pt-BR.json b/homeassistant/components/neato/.translations/pt-BR.json new file mode 100644 index 00000000000000..8c4c45f9c897c8 --- /dev/null +++ b/homeassistant/components/neato/.translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json index b8abb48af4e89c..152e38a5bba050 100644 --- a/homeassistant/components/opentherm_gw/.translations/da.json +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -16,5 +16,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Gulvtemperatur", + "precision": "Pr\u00e6cision" + }, + "description": "Indstillinger for OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json index 4aba4ed047af4c..a7e143505a8ab8 100644 --- a/homeassistant/components/opentherm_gw/.translations/en.json +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -23,11 +23,11 @@ "options": { "step": { "init": { - "description": "Options for the OpenTherm Gateway", "data": { - "floor_temperature": "Floor Temperature", - "precision": "Precision" - } + "floor_temperature": "Floor Temperature", + "precision": "Precision" + }, + "description": "Options for the OpenTherm Gateway" } } } diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json index f5f25da48bd1e9..cfdc6b9a738577 100644 --- a/homeassistant/components/opentherm_gw/.translations/fr.json +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -19,5 +19,15 @@ } }, "title": "Passerelle OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "precision": "Pr\u00e9cision" + }, + "description": "Options pour la passerelle OpenTherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/lb.json b/homeassistant/components/opentherm_gw/.translations/lb.json index ec1f719a6cc3d5..505815dcb4dc6a 100644 --- a/homeassistant/components/opentherm_gw/.translations/lb.json +++ b/homeassistant/components/opentherm_gw/.translations/lb.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Buedem Temperatur", + "precision": "Pr\u00e4zisioun" + }, + "description": "Optioune fir OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json index 6104aa7de724cb..9eb4444cbf10ce 100644 --- a/homeassistant/components/opentherm_gw/.translations/no.json +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Etasje Temperatur", + "precision": "Presisjon" + }, + "description": "Alternativer for OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index 32e5cde82cb3af..3d4c643b848b6c 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -19,5 +19,15 @@ } }, "title": "Bramka OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura pod\u0142ogi", + "precision": "Precyzja" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pt.json b/homeassistant/components/opentherm_gw/.translations/pt.json new file mode 100644 index 00000000000000..960e3a9cf5c37b --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "id": "", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json index 718322ec171088..f38dd669d24f6e 100644 --- a/homeassistant/components/opentherm_gw/.translations/ru.json +++ b/homeassistant/components/opentherm_gw/.translations/ru.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430 Opentherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/sl.json b/homeassistant/components/opentherm_gw/.translations/sl.json index 5de551d5d0c0e9..426459237aa482 100644 --- a/homeassistant/components/opentherm_gw/.translations/sl.json +++ b/homeassistant/components/opentherm_gw/.translations/sl.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Prehod" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura nadstropja", + "precision": "Natan\u010dnost" + }, + "description": "Mo\u017enosti za prehod OpenTherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json index 648f156e8643b1..0d2842ce76799e 100644 --- a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm \u9598\u9053\u5668" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", + "precision": "\u6e96\u78ba\u5ea6" + }, + "description": "OpenTherm \u9598\u9053\u5668\u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pt.json b/homeassistant/components/plex/.translations/pt.json new file mode 100644 index 00000000000000..4312910653fcd8 --- /dev/null +++ b/homeassistant/components/plex/.translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "manual_setup": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos os controles" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/hu.json b/homeassistant/components/sensor/.translations/hu.json new file mode 100644 index 00000000000000..78ea3e5e89befe --- /dev/null +++ b/homeassistant/components/sensor/.translations/hu.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} akku szint", + "is_humidity": "{entity_name} p\u00e1ratartalom", + "is_illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s", + "is_power": "{entity_name} teljes\u00edtm\u00e9ny", + "is_pressure": "{entity_name} nyom\u00e1s", + "is_signal_strength": "{entity_name} jeler\u0151ss\u00e9g", + "is_temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet", + "is_timestamp": "{entity_name} id\u0151b\u00e9lyeg", + "is_value": "{entity_name} \u00e9rt\u00e9k" + }, + "trigger_type": { + "battery_level": "{entity_name} akku szint", + "humidity": "{entity_name} p\u00e1ratartalom", + "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s", + "power": "{entity_name} teljes\u00edtm\u00e9ny", + "pressure": "{entity_name} nyom\u00e1s", + "signal_strength": "{entity_name} jeler\u0151ss\u00e9g", + "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet", + "timestamp": "{entity_name} id\u0151b\u00e9lyeg", + "value": "{entity_name} \u00e9rt\u00e9k" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.hu.json b/homeassistant/components/sensor/.translations/moon.hu.json index 0fcd02a6961471..fff9f51f50d5af 100644 --- a/homeassistant/components/sensor/.translations/moon.hu.json +++ b/homeassistant/components/sensor/.translations/moon.hu.json @@ -4,9 +4,9 @@ "full_moon": "Telihold", "last_quarter": "Utols\u00f3 negyed", "new_moon": "\u00dajhold", - "waning_crescent": "Fogy\u00f3 Hold (sarl\u00f3)", - "waning_gibbous": "Fogy\u00f3 Hold", - "waxing_crescent": "N\u00f6v\u0151 Hold (sarl\u00f3)", - "waxing_gibbous": "N\u00f6v\u0151 Hold" + "waning_crescent": "Fogy\u00f3 holdsarl\u00f3", + "waning_gibbous": "Fogy\u00f3 hold", + "waxing_crescent": "N\u00f6v\u0151 holdsarl\u00f3", + "waxing_gibbous": "N\u00f6v\u0151 hold" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/pt.json b/homeassistant/components/sensor/.translations/pt.json new file mode 100644 index 00000000000000..801b22f0c4544b --- /dev/null +++ b/homeassistant/components/sensor/.translations/pt.json @@ -0,0 +1,21 @@ +{ + "device_automation": { + "condition_type": { + "is_humidity": "humidade {entity_name}", + "is_power": "pot\u00eancia {entity_name}", + "is_timestamp": "momento temporal de {entity_name}", + "is_value": "valor {entity_name}" + }, + "trigger_type": { + "battery_level": "n\u00edvel da bateria {entity_name}", + "humidity": "humidade {entity_name}", + "illuminance": "ilumin\u00e2ncia {entity_name}", + "power": "pot\u00eancia {entity_name}", + "pressure": "press\u00e3o {entity_name}", + "signal_strength": "for\u00e7a do sinal de {entity_name}", + "temperature": "temperatura de {entity_name}", + "timestamp": "momento temporal de {entity_name}", + "value": "valor {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json index a82da0ce24db9f..557eeab55b1f16 100644 --- a/homeassistant/components/soma/.translations/da.json +++ b/homeassistant/components/soma/.translations/da.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Godkendt med Soma." }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt", + "port": "Port" + }, + "description": "Indtast forbindelsesindstillinger for din SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json index 838d46a6d42ada..cb08613c07bfd9 100644 --- a/homeassistant/components/soma/.translations/de.json +++ b/homeassistant/components/soma/.translations/de.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Erfolgreich bei Soma authentifiziert." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Bitte gib die Verbindungsinformationen f\u00fcr SOMA Connect ein.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json index 8126b6ea5aec63..b539130ea59d29 100644 --- a/homeassistant/components/soma/.translations/es.json +++ b/homeassistant/components/soma/.translations/es.json @@ -8,6 +8,14 @@ "create_entry": { "default": "Autenticado con \u00e9xito con Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + } + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json index e990fb98dc233a..a758ab0f615fe7 100644 --- a/homeassistant/components/soma/.translations/fr.json +++ b/homeassistant/components/soma/.translations/fr.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + }, + "description": "Veuillez entrer les param\u00e8tres de connexion de votre SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/it.json b/homeassistant/components/soma/.translations/it.json index ce8e950daccc5c..1398b2a66bee79 100644 --- a/homeassistant/components/soma/.translations/it.json +++ b/homeassistant/components/soma/.translations/it.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Autenticato con successo con Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Inserisci le impostazioni di connessione del tuo SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json index 0ed881853b8afd..dc843f29fd5045 100644 --- a/homeassistant/components/soma/.translations/pl.json +++ b/homeassistant/components/soma/.translations/pl.json @@ -8,6 +8,13 @@ "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Soma" }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pt-BR.json b/homeassistant/components/soma/.translations/pt-BR.json new file mode 100644 index 00000000000000..da05e3b43aeb3e --- /dev/null +++ b/homeassistant/components/soma/.translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pt.json b/homeassistant/components/soma/.translations/pt.json new file mode 100644 index 00000000000000..f681da4210f8a3 --- /dev/null +++ b/homeassistant/components/soma/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/sl.json b/homeassistant/components/soma/.translations/sl.json index 7dd523f366c443..b3075208d2c4fa 100644 --- a/homeassistant/components/soma/.translations/sl.json +++ b/homeassistant/components/soma/.translations/sl.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Uspe\u0161no overjen s Soma." }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "port": "Vrata" + }, + "description": "Prosimo, vnesite nastavitve povezave za va\u0161 SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json index 3d28389ff9147c..893abe82ee1919 100644 --- a/homeassistant/components/soma/.translations/zh-Hant.json +++ b/homeassistant/components/soma/.translations/zh-Hant.json @@ -8,6 +8,16 @@ "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8acb\u8f38\u5165 SOMA Connect \u9023\u7dda\u8a2d\u5b9a\u3002", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/lv.json b/homeassistant/components/switch/.translations/lv.json new file mode 100644 index 00000000000000..784a9a37afa7c0 --- /dev/null +++ b/homeassistant/components/switch/.translations/lv.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "condition_type": { + "turn_off": "{entity_name} tika izsl\u0113gta", + "turn_on": "{entity_name} tika iesl\u0113gta" + }, + "trigger_type": { + "turned_off": "{entity_name} tika izsl\u0113gta", + "turned_on": "{entity_name} tika iesl\u0113gta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pt.json b/homeassistant/components/transmission/.translations/pt.json new file mode 100644 index 00000000000000..f681da4210f8a3 --- /dev/null +++ b/homeassistant/components/transmission/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pt-BR.json b/homeassistant/components/unifi/.translations/pt-BR.json index 113eaa000fe620..a57bb33ee7a7b6 100644 --- a/homeassistant/components/unifi/.translations/pt-BR.json +++ b/homeassistant/components/unifi/.translations/pt-BR.json @@ -32,6 +32,12 @@ "track_devices": "Rastrear dispositivos de rede (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de rede com fio" } + }, + "init": { + "data": { + "one": "um", + "other": "uns" + } } } } diff --git a/homeassistant/components/unifi/.translations/pt.json b/homeassistant/components/unifi/.translations/pt.json index 6730a3d258edc4..c602a58660bb57 100644 --- a/homeassistant/components/unifi/.translations/pt.json +++ b/homeassistant/components/unifi/.translations/pt.json @@ -22,5 +22,28 @@ } }, "title": "Controlador UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tempo em segundos desde a \u00faltima vez que foi visto at\u00e9 ser considerado afastado", + "track_clients": "Acompanhar clientes da rede", + "track_devices": "Acompanhar dispositivos de rede (dispositivos Ubiquiti)", + "track_wired_clients": "Incluir clientes da rede cablada" + } + }, + "init": { + "data": { + "one": "Vazio", + "other": "Vazios" + } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Criar sensores de uso de largura de banda para clientes da rede" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/pt.json b/homeassistant/components/wwlln/.translations/pt.json new file mode 100644 index 00000000000000..c7081cd694a0af --- /dev/null +++ b/homeassistant/components/wwlln/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pt.json b/homeassistant/components/zha/.translations/pt.json index 8606a04e197985..0c86dc95d09959 100644 --- a/homeassistant/components/zha/.translations/pt.json +++ b/homeassistant/components/zha/.translations/pt.json @@ -16,5 +16,13 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "warn": "Avisar" + }, + "trigger_subtype": { + "left": "Esquerda" + } } } \ No newline at end of file From d98114d2ab78c95b9fc63e4c3debe53106d3bcb9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 18:08:54 -0700 Subject: [PATCH 1034/3953] Guard cloud check (#27901) * Guard cloud check * Fix pos args --- .../components/owntracks/config_flow.py | 5 +++- .../components/smartthings/smartapp.py | 28 ++++++++++++++----- homeassistant/helpers/config_entry_flow.py | 5 +++- .../components/owntracks/test_config_flow.py | 1 + .../smartthings/test_config_flow.py | 2 ++ tests/components/smartthings/test_init.py | 1 + 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 67553ef608ffdb..343a6d90b52087 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -78,7 +78,10 @@ async def async_step_import(self, user_input): async def _get_webhook_id(self): """Generate webhook ID.""" webhook_id = self.hass.components.webhook.async_generate_id() - if self.hass.components.cloud.async_active_subscription(): + if ( + "cloud" in self.hass.config.components + and self.hass.components.cloud.async_active_subscription() + ): webhook_url = await self.hass.components.cloud.async_create_cloudhook( webhook_id ) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index d205c1d245cb1b..ecd4da5dcabc3c 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -22,7 +22,7 @@ SubscriptionEntity, ) -from homeassistant.components import cloud, webhook +from homeassistant.components import webhook from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( @@ -88,7 +88,10 @@ async def validate_installed_app(api, installed_app_id: str): def validate_webhook_requirements(hass: HomeAssistantType) -> bool: """Ensure HASS is setup properly to receive webhooks.""" - if cloud.async_active_subscription(hass): + if ( + "cloud" in hass.config.components + and hass.components.cloud.async_active_subscription() + ): return True if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None: return True @@ -102,7 +105,11 @@ def get_webhook_url(hass: HomeAssistantType) -> str: Return the cloudhook if available, otherwise local webhook. """ cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if cloud.async_active_subscription(hass) and cloudhook_url is not None: + if ( + "cloud" in hass.config.components + and hass.components.cloud.async_active_subscription() + and cloudhook_url is not None + ): return cloudhook_url return webhook.async_generate_url(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) @@ -222,10 +229,11 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): cloudhook_url = config.get(CONF_CLOUDHOOK_URL) if ( cloudhook_url is None - and cloud.async_active_subscription(hass) + and "cloud" in hass.config.components + and hass.components.cloud.async_active_subscription() and not hass.config_entries.async_entries(DOMAIN) ): - cloudhook_url = await cloud.async_create_cloudhook( + cloudhook_url = await hass.components.cloud.async_create_cloudhook( hass, config[CONF_WEBHOOK_ID] ) config[CONF_CLOUDHOOK_URL] = cloudhook_url @@ -273,8 +281,14 @@ async def unload_smartapp_endpoint(hass: HomeAssistantType): return # Remove the cloudhook if it was created cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if cloudhook_url and cloud.async_is_logged_in(hass): - await cloud.async_delete_cloudhook(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) + if ( + cloudhook_url + and "cloud" in hass.config.components + and hass.components.cloud.async_is_logged_in() + ): + await hass.components.cloud.async_delete_cloudhook( + hass, hass.data[DOMAIN][CONF_WEBHOOK_ID] + ) # Remove cloudhook from storage store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) await store.async_save( diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 88aae3721b1a1f..374ef7958464de 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -124,7 +124,10 @@ async def async_step_user(self, user_input=None): webhook_id = self.hass.components.webhook.async_generate_id() - if self.hass.components.cloud.async_active_subscription(): + if ( + "cloud" in self.hass.config.components + and self.hass.components.cloud.async_active_subscription() + ): webhook_url = await self.hass.components.cloud.async_create_cloudhook( webhook_id ) diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 76863c61698499..c4e2a54f69a0ae 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -43,6 +43,7 @@ async def test_config_flow_unload(hass): async def test_with_cloud_sub(hass): """Test creating a config flow while subscribed.""" + hass.config.components.add("cloud") with patch( "homeassistant.components.cloud.async_active_subscription", return_value=True ), patch( diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index fce0129a7bf399..521f1c6a6a8972 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -205,6 +205,8 @@ async def test_cloudhook_app_created_then_show_wait_form( hass, app, app_oauth_client, smartthings_mock ): """Test SmartApp is created with a cloudhoko and shows wait form.""" + hass.config.components.add("cloud") + # Unload the endpoint so we can reload it under the cloud. await smartapp.unload_smartapp_endpoint(hass) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 9749ab9bb71e3d..b8cd65f5a0b9ca 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -268,6 +268,7 @@ async def test_remove_entry(hass, config_entry, smartthings_mock): async def test_remove_entry_cloudhook(hass, config_entry, smartthings_mock): """Test that the installed app, app, and cloudhook are removed up.""" + hass.config.components.add("cloud") # Arrange config_entry.add_to_hass(hass) hass.data[DOMAIN][CONF_CLOUDHOOK_URL] = "https://test.cloud" From 914ceea72d4dd7afd10ab4fdb0fbe0d3d38b3188 Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Sat, 19 Oct 2019 03:09:41 +0200 Subject: [PATCH 1035/3953] Bump keyring to 19.2.0 (#27899) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 6ca422b595ba66..0c5623a50ad1c5 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -8,7 +8,7 @@ # mypy: allow-untyped-defs -REQUIREMENTS = ["keyring==17.1.1", "keyrings.alt==3.1.1"] +REQUIREMENTS = ["keyring==19.2.0", "keyrings.alt==3.1.1"] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 58a927c81ab69c..80be965c41556d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -721,7 +721,7 @@ kaiterra-async-client==0.0.2 keba-kecontact==0.2.0 # homeassistant.scripts.keyring -keyring==17.1.1 +keyring==19.2.0 # homeassistant.scripts.keyring keyrings.alt==3.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24122915fb55ac..99a292d82f63b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ influxdb==5.2.3 jsonpath==0.75 # homeassistant.scripts.keyring -keyring==17.1.1 +keyring==19.2.0 # homeassistant.scripts.keyring keyrings.alt==3.1.1 From 6391a68fd59fccec64c7f68a3daf2584f4e66a2e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 18:11:54 -0700 Subject: [PATCH 1036/3953] Better header check for OAuth2 helper (#27897) --- homeassistant/helpers/config_entry_oauth2_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 043a28cac27ad5..7fb954378ee70d 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -392,7 +392,7 @@ async def async_oauth2_request( url, **kwargs, headers={ - **kwargs.get("headers", {}), + **(kwargs.get("headers") or {}), "authorization": f"Bearer {token['access_token']}", }, ) From 7ed659151c010f67af43e82e891b9cb9f74ffcaf Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 19 Oct 2019 04:49:08 +0200 Subject: [PATCH 1037/3953] Vacuum reproduce state (#27868) * Vacuum reproduce state * Add missing states * Improved process * Fix tests --- .../components/vacuum/reproduce_state.py | 101 +++++++++++++ .../components/vacuum/test_reproduce_state.py | 139 ++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 homeassistant/components/vacuum/reproduce_state.py create mode 100644 tests/components/vacuum/test_reproduce_state.py diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py new file mode 100644 index 00000000000000..485ffef0c9f1fd --- /dev/null +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -0,0 +1,101 @@ +"""Reproduce an Vacuum state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_FAN_SPEED, + DOMAIN, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + SERVICE_STOP, + STATE_CLEANING, + STATE_DOCKED, + STATE_RETURNING, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES_TOGGLE = {STATE_ON, STATE_OFF} +VALID_STATES_STATE = { + STATE_CLEANING, + STATE_DOCKED, + STATE_IDLE, + STATE_RETURNING, + STATE_PAUSED, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 VALID_STATES_TOGGLE and state.state not in VALID_STATES_STATE: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and cur_state.attributes.get( + ATTR_FAN_SPEED + ) == state.attributes.get(ATTR_FAN_SPEED): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if cur_state.state != state.state: + # Wrong state + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + elif state.state == STATE_CLEANING: + service = SERVICE_START + elif state.state == STATE_DOCKED or state.state == STATE_RETURNING: + service = SERVICE_RETURN_TO_BASE + elif state.state == STATE_IDLE: + service = SERVICE_STOP + elif state.state == STATE_PAUSED: + service = SERVICE_PAUSE + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + if cur_state.attributes.get(ATTR_FAN_SPEED) != state.attributes.get(ATTR_FAN_SPEED): + # Wrong fan speed + service_data["fan_speed"] = state.attributes[ATTR_FAN_SPEED] + await hass.services.async_call( + DOMAIN, SERVICE_SET_FAN_SPEED, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Vacuum states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/vacuum/test_reproduce_state.py b/tests/components/vacuum/test_reproduce_state.py new file mode 100644 index 00000000000000..d5a7051e6a6630 --- /dev/null +++ b/tests/components/vacuum/test_reproduce_state.py @@ -0,0 +1,139 @@ +"""Test reproduce state for Vacuum.""" +from homeassistant.components.vacuum import ( + ATTR_FAN_SPEED, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + SERVICE_STOP, + STATE_CLEANING, + STATE_DOCKED, + STATE_RETURNING, +) +from homeassistant.const import ( + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, +) +from homeassistant.core import State + +from tests.common import async_mock_service + +FAN_SPEED_LOW = "low" +FAN_SPEED_HIGH = "high" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Vacuum states.""" + hass.states.async_set("vacuum.entity_off", STATE_OFF, {}) + hass.states.async_set("vacuum.entity_on", STATE_ON, {}) + hass.states.async_set( + "vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_LOW} + ) + hass.states.async_set("vacuum.entity_cleaning", STATE_CLEANING, {}) + hass.states.async_set("vacuum.entity_docked", STATE_DOCKED, {}) + hass.states.async_set("vacuum.entity_idle", STATE_IDLE, {}) + hass.states.async_set("vacuum.entity_returning", STATE_RETURNING, {}) + hass.states.async_set("vacuum.entity_paused", STATE_PAUSED, {}) + + turn_on_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_ON) + turn_off_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_OFF) + start_calls = async_mock_service(hass, "vacuum", SERVICE_START) + pause_calls = async_mock_service(hass, "vacuum", SERVICE_PAUSE) + stop_calls = async_mock_service(hass, "vacuum", SERVICE_STOP) + return_calls = async_mock_service(hass, "vacuum", SERVICE_RETURN_TO_BASE) + fan_speed_calls = async_mock_service(hass, "vacuum", SERVICE_SET_FAN_SPEED) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("vacuum.entity_off", STATE_OFF), + State("vacuum.entity_on", STATE_ON), + State("vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_LOW}), + State("vacuum.entity_cleaning", STATE_CLEANING), + State("vacuum.entity_docked", STATE_DOCKED), + State("vacuum.entity_idle", STATE_IDLE), + State("vacuum.entity_returning", STATE_RETURNING), + State("vacuum.entity_paused", STATE_PAUSED), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(stop_calls) == 0 + assert len(return_calls) == 0 + assert len(fan_speed_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("vacuum.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(stop_calls) == 0 + assert len(return_calls) == 0 + assert len(fan_speed_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("vacuum.entity_off", STATE_ON), + State("vacuum.entity_on", STATE_OFF), + State("vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_HIGH}), + State("vacuum.entity_cleaning", STATE_PAUSED), + State("vacuum.entity_docked", STATE_CLEANING), + State("vacuum.entity_idle", STATE_DOCKED), + State("vacuum.entity_returning", STATE_CLEANING), + State("vacuum.entity_paused", STATE_IDLE), + # Should not raise + State("vacuum.non_existing", STATE_ON), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "vacuum" + assert turn_on_calls[0].data == {"entity_id": "vacuum.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "vacuum" + assert turn_off_calls[0].data == {"entity_id": "vacuum.entity_on"} + + assert len(start_calls) == 2 + entities = [ + {"entity_id": "vacuum.entity_docked"}, + {"entity_id": "vacuum.entity_returning"}, + ] + for call in start_calls: + assert call.domain == "vacuum" + assert call.data in entities + entities.remove(call.data) + + assert len(pause_calls) == 1 + assert pause_calls[0].domain == "vacuum" + assert pause_calls[0].data == {"entity_id": "vacuum.entity_cleaning"} + + assert len(stop_calls) == 1 + assert stop_calls[0].domain == "vacuum" + assert stop_calls[0].data == {"entity_id": "vacuum.entity_paused"} + + assert len(return_calls) == 1 + assert return_calls[0].domain == "vacuum" + assert return_calls[0].data == {"entity_id": "vacuum.entity_idle"} + + assert len(fan_speed_calls) == 1 + assert fan_speed_calls[0].domain == "vacuum" + assert fan_speed_calls[0].data == { + "entity_id": "vacuum.entity_on_fan", + ATTR_FAN_SPEED: FAN_SPEED_HIGH, + } From 435cbb7f7eb91886f5ea4e51757cb9a2170ef8f7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 19 Oct 2019 05:51:53 +0200 Subject: [PATCH 1038/3953] Azure pytest parallel (#27864) * Azure pytest parallel * Update azure-pipelines-ci.yml * Remove test that does nothing --- azure-pipelines-ci.yml | 8 ++++---- tests/components/ps4/test_media_player.py | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index f03c5f435f9892..82708151ad6589 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -108,13 +108,13 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt' + keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt | "v2"' build: | set -e python -m venv venv . venv/bin/activate - pip install -U pip setuptools pytest-azurepipelines -c homeassistant/package_constraints.txt + pip install -U pip setuptools pytest-azurepipelines pytest-xdist -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` @@ -127,7 +127,7 @@ stages: set -e . venv/bin/activate - pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 -n 2 --dist loadfile -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'])) @@ -135,7 +135,7 @@ stages: set -e . venv/bin/activate - pytest --timeout=9 --durations=10 --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 -n 2 --dist loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests codecov --token $(codecovToken) script/check_dirty displayName: 'Run pytest for python $(python.container) / coverage' diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index d6eeb31695c485..7bf93e37777e6c 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -169,11 +169,6 @@ 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 2110fea02bbda5cb9ef5565146b0e2f2e6101f36 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Fri, 18 Oct 2019 20:57:36 -0700 Subject: [PATCH 1039/3953] Move import for htu21d component (#27908) --- homeassistant/components/htu21d/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index f94b11d5ada003..954ba60abbf758 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -3,11 +3,13 @@ from functools import partial import logging +from i2csense.htu21d import HTU21D # pylint: disable=import-error +import smbus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, TEMP_FAHRENHEIT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit @@ -34,9 +36,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HTU21D sensor.""" - import smbus # pylint: disable=import-error - from i2csense.htu21d import HTU21D # pylint: disable=import-error - name = config.get(CONF_NAME) bus_number = config.get(CONF_I2C_BUS) temp_unit = hass.config.units.temperature_unit From 0cd55d6716244e260464c74d4e9a382fea46fa83 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Fri, 18 Oct 2019 20:57:47 -0700 Subject: [PATCH 1040/3953] Move imports for hp_ilo components (#27906) --- homeassistant/components/hp_ilo/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index cf95c21a8d1e7e..04c715dc010981 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import hpilo import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -180,8 +181,6 @@ def __init__(self, host, port, login, password): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from HP iLO.""" - import hpilo - try: self.data = hpilo.Ilo( hostname=self._host, From 00521b5e802f25c41d439dab83ea0af42f95c232 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 20:57:54 -0700 Subject: [PATCH 1041/3953] Fix flaky integration test (#27905) --- tests/components/integration/test_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 48d4178e992337..c65ca72023549b 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -200,4 +200,4 @@ async def test_suffix(hass): assert state is not None # Testing a network speed sensor at 1000 bytes/s over 10s = 10kbytes - assert round(float(state.state), config["sensor"]["round"]) == 10.0 + assert round(float(state.state)) == 10 From 1e727f339fd3328b7930a2700dc9f6ffd6423a77 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Fri, 18 Oct 2019 20:58:07 -0700 Subject: [PATCH 1042/3953] Move imports in harmony component (#27904) --- homeassistant/components/harmony/remote.py | 27 ++++++---------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index b78f276bf28838..118af7fe34ad61 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -3,6 +3,12 @@ import json import logging +import aioharmony.exceptions as aioexc +from aioharmony.harmonyapi import ( + ClientCallbackType, + HarmonyAPI as HarmonyClient, + SendCommandDevice, +) import voluptuous as vol from homeassistant.components import remote @@ -23,8 +29,8 @@ CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) @@ -165,8 +171,6 @@ class HarmonyRemote(remote.RemoteDevice): def __init__(self, name, host, port, activity, out_path, delay_secs): """Initialize HarmonyRemote class.""" - from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient - self._name = name self.host = host self.port = port @@ -180,8 +184,6 @@ def __init__(self, name, host, port, activity, out_path, delay_secs): async def async_added_to_hass(self): """Complete the initialization.""" - from aioharmony.harmonyapi import ClientCallbackType - _LOGGER.debug("%s: Harmony Hub added", self._name) # Register the callbacks self._client.callbacks = ClientCallbackType( @@ -195,8 +197,6 @@ async def async_added_to_hass(self): # activity await self.new_config() - import aioharmony.exceptions as aioexc - async def shutdown(_): """Close connection on shutdown.""" _LOGGER.debug("%s: Closing Harmony Hub", self._name) @@ -234,8 +234,6 @@ def available(self): async def connect(self): """Connect to the Harmony HUB.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Connecting", self._name) try: if not await self._client.connect(): @@ -284,8 +282,6 @@ async def got_disconnected(self, _=None): async def async_turn_on(self, **kwargs): """Start an activity from the Harmony device.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Turn On", self.name) activity = kwargs.get(ATTR_ACTIVITY, self._default_activity) @@ -314,8 +310,6 @@ async def async_turn_on(self, **kwargs): async def async_turn_off(self, **kwargs): """Start the PowerOff activity.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Turn Off", self.name) try: await self._client.power_off() @@ -325,9 +319,6 @@ async def async_turn_off(self, **kwargs): # pylint: disable=arguments-differ async def async_send_command(self, command, **kwargs): """Send a list of commands to one device.""" - from aioharmony.harmonyapi import SendCommandDevice - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Send Command", self.name) device = kwargs.get(ATTR_DEVICE) if device is None: @@ -390,8 +381,6 @@ async def async_send_command(self, command, **kwargs): async def change_channel(self, channel): """Change the channel using Harmony remote.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Changing channel to %s", self.name, channel) try: await self._client.change_channel(channel) @@ -400,8 +389,6 @@ async def change_channel(self, channel): async def sync(self): """Sync the Harmony device with the web service.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name) try: await self._client.sync() From 37b23e9205350a456c28255f1171e238f7fdf141 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Fri, 18 Oct 2019 20:58:15 -0700 Subject: [PATCH 1043/3953] Move imports to top for harman_kardon_avr (#27903) --- .../components/harman_kardon_avr/media_player.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index 01948943adfbdb..fd7cddcaed9ee7 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -1,18 +1,19 @@ """Support for interface with an Harman/Kardon or JBL AVR.""" import logging +import hkavr import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_TURN_ON, - SUPPORT_SELECT_SOURCE, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -38,8 +39,6 @@ def setup_platform(hass, config, add_entities, discover_info=None): """Set up the AVR platform.""" - import hkavr - name = config[CONF_NAME] host = config[CONF_HOST] port = config[CONF_PORT] From 63031173548e70f320fffc5d4ef3fb8b8cbc35c8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 20:58:43 -0700 Subject: [PATCH 1044/3953] Dont create coroutine until acting on it (#27907) --- .../google_assistant/report_state.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index b842a552714ef7..aacb90e9d2bd36 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -49,23 +49,23 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): {"devices": {"states": {changed_entity: entity_data}}} ) - async_call_later( - hass, INITIAL_REPORT_DELAY, _async_report_all_states(hass, google_config) - ) - - return hass.helpers.event.async_track_state_change( - MATCH_ALL, async_entity_state_listener - ) + async def inital_report(_now): + """Report initially all states.""" + entities = {} + for entity in async_get_entities(hass, google_config): + if not entity.should_expose(): + continue -async def _async_report_all_states(hass: HomeAssistant, google_config: AbstractConfig): - """Report all states.""" - entities = {} + try: + entities[entity.entity_id] = entity.query_serialize() + except SmartHomeError: + continue - for entity in async_get_entities(hass, google_config): - if not entity.should_expose(): - continue + await google_config.async_report_state({"devices": {"states": entities}}) - entities[entity.entity_id] = entity.query_serialize() + async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) - await google_config.async_report_state({"devices": {"states": entities}}) + return hass.helpers.event.async_track_state_change( + MATCH_ALL, async_entity_state_listener + ) From 2bd9f5680dcfc38e11a77884af36f76ed72ea9bf Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 19 Oct 2019 07:37:44 +0200 Subject: [PATCH 1045/3953] Report state (#27759) * Add report state config * Add initial steps for local report state * Use owner of system as user_id * First working prototype * Only report state if requested * Add some good logging and adjust constant name * Move jwt generation out to non member * Move cache out to caller * Remove todo about cache * Move getting token out of class * Add timeout on calls * Validate config dependency * Support using service key to do sync call when api_key is not set * Make sure timezone is fixed for datetime dummy * Use exact expire_in time * Support renewing token on 401 * Test retry on 401 * No need to declare dummy key twice * Correct some docs on functions * Add test for token expiry --- .../components/google_assistant/__init__.py | 25 +++ .../components/google_assistant/const.py | 7 + .../components/google_assistant/http.py | 141 +++++++++++++++- .../components/google_assistant/test_http.py | 157 ++++++++++++++++++ 4 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 tests/components/google_assistant/test_http.py diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index a1252d67fff4c3..ebf906b6f2ac8f 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -28,9 +28,13 @@ CONF_ENTITY_CONFIG, CONF_EXPOSE, CONF_ALIASES, + CONF_REPORT_STATE, CONF_ROOM_HINT, CONF_ALLOW_UNLOCK, CONF_SECURE_DEVICES_PIN, + CONF_SERVICE_ACCOUNT, + CONF_CLIENT_EMAIL, + CONF_PRIVATE_KEY, ) from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 from .const import EVENT_QUERY_RECEIVED # noqa: F401 @@ -47,6 +51,24 @@ } ) +GOOGLE_SERVICE_ACCOUNT = vol.Schema( + { + vol.Required(CONF_PRIVATE_KEY): cv.string, + vol.Required(CONF_CLIENT_EMAIL): cv.string, + }, + extra=vol.ALLOW_EXTRA, +) + + +def _check_report_state(data): + if data[CONF_REPORT_STATE]: + if CONF_SERVICE_ACCOUNT not in data: + raise vol.Invalid( + "If report state is enabled, a service account must exist" + ) + return data + + GOOGLE_ASSISTANT_SCHEMA = vol.All( cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version="0.95"), vol.Schema( @@ -63,9 +85,12 @@ vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean, # str on purpose, makes sure it is configured correctly. vol.Optional(CONF_SECURE_DEVICES_PIN): str, + vol.Optional(CONF_REPORT_STATE, default=False): cv.boolean, + vol.Optional(CONF_SERVICE_ACCOUNT): GOOGLE_SERVICE_ACCOUNT, }, extra=vol.PREVENT_EXTRA, ), + _check_report_state, ) CONFIG_SCHEMA = vol.Schema({DOMAIN: GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 54abd54caaffc6..03253e244fed06 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -32,6 +32,10 @@ CONF_ROOM_HINT = "room" CONF_ALLOW_UNLOCK = "allow_unlock" CONF_SECURE_DEVICES_PIN = "secure_devices_pin" +CONF_REPORT_STATE = "report_state" +CONF_SERVICE_ACCOUNT = "service_account" +CONF_CLIENT_EMAIL = "client_email" +CONF_PRIVATE_KEY = "private_key" DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ @@ -72,7 +76,10 @@ SERVICE_REQUEST_SYNC = "request_sync" HOMEGRAPH_URL = "https://homegraph.googleapis.com/" +HOMEGRAPH_SCOPE = "https://www.googleapis.com/auth/homegraph" +HOMEGRAPH_TOKEN_URL = "https://accounts.google.com/o/oauth2/token" REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + "v1/devices:requestSync" +REPORT_STATE_BASE_URL = HOMEGRAPH_URL + "v1/devices:reportStateAndNotification" # Error codes used for SmartHomeError class # https://developers.google.com/actions/reference/smarthome/errors-exceptions diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index aea226348b803c..90fa1ced157a90 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -1,20 +1,38 @@ """Support for Google Actions Smart Home Control.""" +import asyncio +from datetime import timedelta import logging +from uuid import uuid4 +import jwt +from aiohttp import ClientResponseError, ClientError from aiohttp.web import Request, Response # Typing imports from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback +from homeassistant.core import callback, ServiceCall from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import dt as dt_util from .const import ( GOOGLE_ASSISTANT_API_ENDPOINT, + CONF_API_KEY, CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, CONF_ENTITY_CONFIG, CONF_EXPOSE, + CONF_REPORT_STATE, CONF_SECURE_DEVICES_PIN, + CONF_SERVICE_ACCOUNT, + CONF_CLIENT_EMAIL, + CONF_PRIVATE_KEY, + DOMAIN, + HOMEGRAPH_TOKEN_URL, + HOMEGRAPH_SCOPE, + REPORT_STATE_BASE_URL, + REQUEST_SYNC_BASE_URL, + SERVICE_REQUEST_SYNC, ) from .smart_home import async_handle_message from .helpers import AbstractConfig @@ -22,6 +40,35 @@ _LOGGER = logging.getLogger(__name__) +def _get_homegraph_jwt(time, iss, key): + now = int(time.timestamp()) + + jwt_raw = { + "iss": iss, + "scope": HOMEGRAPH_SCOPE, + "aud": HOMEGRAPH_TOKEN_URL, + "iat": now, + "exp": now + 3600, + } + return jwt.encode(jwt_raw, key, algorithm="RS256").decode("utf-8") + + +async def _get_homegraph_token(hass, jwt_signed): + headers = { + "Authorization": "Bearer {}".format(jwt_signed), + "Content-Type": "application/x-www-form-urlencoded", + } + data = { + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": jwt_signed, + } + + session = async_get_clientsession(hass) + async with session.post(HOMEGRAPH_TOKEN_URL, headers=headers, data=data) as res: + res.raise_for_status() + return await res.json() + + class GoogleConfig(AbstractConfig): """Config for manual setup of Google.""" @@ -29,6 +76,8 @@ def __init__(self, hass, config): """Initialize the config.""" super().__init__(hass) self._config = config + self._access_token = None + self._access_token_renew = None @property def enabled(self): @@ -50,6 +99,12 @@ def secure_devices_pin(self): """Return entity config.""" return self._config.get(CONF_SECURE_DEVICES_PIN) + @property + def should_report_state(self): + """Return if states should be proactively reported.""" + # pylint: disable=no-self-use + return self._config.get(CONF_REPORT_STATE) + def should_expose(self, state) -> bool: """Return if entity should be exposed.""" expose_by_default = self._config.get(CONF_EXPOSE_BY_DEFAULT) @@ -79,11 +134,93 @@ def should_2fa(self, state): """If an entity should have 2FA checked.""" return True + async def _async_update_token(self, force=False): + if CONF_SERVICE_ACCOUNT not in self._config: + _LOGGER.error("Trying to get homegraph api token without service account") + return + + now = dt_util.utcnow() + if not self._access_token or now > self._access_token_renew or force: + token = await _get_homegraph_token( + self.hass, + _get_homegraph_jwt( + now, + self._config[CONF_SERVICE_ACCOUNT][CONF_CLIENT_EMAIL], + self._config[CONF_SERVICE_ACCOUNT][CONF_PRIVATE_KEY], + ), + ) + self._access_token = token["access_token"] + self._access_token_renew = now + timedelta(seconds=token["expires_in"]) + + async def async_call_homegraph_api(self, url, data): + """Call a homegraph api with authenticaiton.""" + session = async_get_clientsession(self.hass) + + async def _call(): + headers = { + "Authorization": "Bearer {}".format(self._access_token), + "X-GFE-SSL": "yes", + } + async with session.post(url, headers=headers, json=data) as res: + _LOGGER.debug( + "Response on %s with data %s was %s", url, data, await res.text() + ) + res.raise_for_status() + + try: + await self._async_update_token() + try: + await _call() + except ClientResponseError as error: + if error.status == 401: + _LOGGER.warning( + "Request for %s unauthorized, renewing token and retrying", url + ) + await self._async_update_token(True) + await _call() + else: + raise + except ClientResponseError as error: + _LOGGER.error("Request for %s failed: %d", url, error.status) + except (asyncio.TimeoutError, ClientError): + _LOGGER.error("Could not contact %s", url) + + async def async_report_state(self, message): + """Send a state report to Google.""" + data = { + "requestId": uuid4().hex, + "agentUserId": (await self.hass.auth.async_get_owner()).id, + "payload": message, + } + await self.async_call_homegraph_api(REPORT_STATE_BASE_URL, data) + @callback def async_register_http(hass, cfg): """Register HTTP views for Google Assistant.""" - hass.http.register_view(GoogleAssistantView(GoogleConfig(hass, cfg))) + config = GoogleConfig(hass, cfg) + hass.http.register_view(GoogleAssistantView(config)) + if config.should_report_state: + config.async_enable_report_state() + + async def request_sync_service_handler(call: ServiceCall): + """Handle request sync service calls.""" + agent_user_id = call.data.get("agent_user_id") or call.context.user_id + + if agent_user_id is None: + _LOGGER.warning( + "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." + ) + return + await config.async_call_homegraph_api( + REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} + ) + + # Register service only if api key is provided + if CONF_API_KEY not in cfg and CONF_SERVICE_ACCOUNT in cfg: + hass.services.async_register( + DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler + ) class GoogleAssistantView(HomeAssistantView): diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py new file mode 100644 index 00000000000000..4b26bbeba7f391 --- /dev/null +++ b/tests/components/google_assistant/test_http.py @@ -0,0 +1,157 @@ +"""Test Google http services.""" +from datetime import datetime, timezone, timedelta +from asynctest import patch, ANY + +from homeassistant.components.google_assistant.http import ( + GoogleConfig, + _get_homegraph_jwt, + _get_homegraph_token, +) +from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA +from homeassistant.components.google_assistant.const import ( + REPORT_STATE_BASE_URL, + HOMEGRAPH_TOKEN_URL, +) +from homeassistant.auth.models import User + +DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA( + { + "project_id": "1234", + "service_account": { + "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKYscIlwm7soDsHAz6L6YvUkCvkrX19rS6yeYOmovvhoK5WeYGWUsd8V72zmsyHB7XO94YgJVjvxfzn5K8bLePjFzwoSJjZvhBJ/ZQ05d8VmbvgyWUoPdG9oEa4fZ/lCYrXoaFdTot2xcJvrb/ZuiRl4s4eZpNeFYvVK/Am7UeFPAgMBAAECgYAUetOfzLYUudofvPCaKHu7tKZ5kQPfEa0w6BAPnBF1Mfl1JiDBRDMryFtKs6AOIAVwx00dY/Ex0BCbB3+Cr58H7t4NaPTJxCpmR09pK7o17B7xAdQv8+SynFNud9/5vQ5AEXMOLNwKiU7wpXT6Z7ZIibUBOR7ewsWgsHCDpN1iqQJBAOMODPTPSiQMwRAUHIc6GPleFSJnIz2PAoG3JOG9KFAL6RtIc19lob2ZXdbQdzKtjSkWo+O5W20WDNAl1k32h6MCQQC7W4ZCIY67mPbL6CxXfHjpSGF4Dr9VWJ7ZrKHr6XUoOIcEvsn/pHvWonjMdy93rQMSfOE8BKd/I1+GHRmNVgplAkAnSo4paxmsZVyfeKt7Jy2dMY+8tVZe17maUuQaAE7Sk00SgJYegwrbMYgQnWCTL39HBfj0dmYA2Zj8CCAuu6O7AkEAryFiYjaUAO9+4iNoL27+ZrFtypeeadyov7gKs0ZKaQpNyzW8A+Zwi7TbTeSqzic/E+z/bOa82q7p/6b7141xsQJBANCAcIwMcVb6KVCHlQbOtKspo5Eh4ZQi8bGl+IcwbQ6JSxeTx915IfAldgbuU047wOB04dYCFB2yLDiUGVXTifU=\n-----END PRIVATE KEY-----\n", + "client_email": "dummy@dummy.iam.gserviceaccount.com", + }, + } +) +MOCK_TOKEN = {"access_token": "dummtoken", "expires_in": 3600} +MOCK_JSON = {"devices": {}} +MOCK_URL = "https://dummy" +MOCK_HEADER = { + "Authorization": "Bearer {}".format(MOCK_TOKEN["access_token"]), + "X-GFE-SSL": "yes", +} + + +async def test_get_jwt(hass): + """Test signing of key.""" + + jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJkdW1teUBkdW1teS5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInNjb3BlIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9ob21lZ3JhcGgiLCJhdWQiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW4iLCJpYXQiOjE1NzEwMTEyMDAsImV4cCI6MTU3MTAxNDgwMH0.gG06SmY-zSvFwSrdFfqIdC6AnC22rwz-d2F2UDeWbywjdmFL_1zceL-OOLBwjD8MJr6nR0kmN_Osu7ml9-EzzZjJqsRUxMjGn2G8nSYHbv16R4FYIp62Ibvt6Jj_wdFobEPoy_5OJ28P5Hdu0giGMlFBJMy0Tc6MgEDZA-cwOBw" + res = _get_homegraph_jwt( + datetime(2019, 10, 14, tzinfo=timezone.utc), + DUMMY_CONFIG["service_account"]["client_email"], + DUMMY_CONFIG["service_account"]["private_key"], + ) + assert res == jwt + + +async def test_get_access_token(hass, aioclient_mock): + """Test the function to get access token.""" + jwt = "dummyjwt" + + aioclient_mock.post( + HOMEGRAPH_TOKEN_URL, + status=200, + json={"access_token": "1234", "expires_in": 3600}, + ) + + await _get_homegraph_token(hass, jwt) + assert aioclient_mock.call_count == 1 + assert aioclient_mock.mock_calls[0][3] == { + "Authorization": "Bearer {}".format(jwt), + "Content-Type": "application/x-www-form-urlencoded", + } + + +async def test_update_access_token(hass): + """Test the function to update access token when expired.""" + jwt = "dummyjwt" + + config = GoogleConfig(hass, DUMMY_CONFIG) + + base_time = datetime(2019, 10, 14, tzinfo=timezone.utc) + with patch( + "homeassistant.components.google_assistant.http._get_homegraph_token" + ) as mock_get_token, patch( + "homeassistant.components.google_assistant.http._get_homegraph_jwt" + ) as mock_get_jwt, patch( + "homeassistant.core.dt_util.utcnow" + ) as mock_utcnow: + mock_utcnow.return_value = base_time + mock_get_jwt.return_value = jwt + mock_get_token.return_value = MOCK_TOKEN + + await config._async_update_token() + mock_get_token.assert_called_once() + + mock_get_token.reset_mock() + + mock_utcnow.return_value = base_time + timedelta(seconds=3600) + await config._async_update_token() + mock_get_token.assert_not_called() + + mock_get_token.reset_mock() + + mock_utcnow.return_value = base_time + timedelta(seconds=3601) + await config._async_update_token() + mock_get_token.assert_called_once() + + +async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): + """Test the function to call the homegraph api.""" + config = GoogleConfig(hass, DUMMY_CONFIG) + with patch( + "homeassistant.components.google_assistant.http._get_homegraph_token" + ) as mock_get_token: + mock_get_token.return_value = MOCK_TOKEN + + aioclient_mock.post(MOCK_URL, status=200, json={}) + + await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + + assert mock_get_token.call_count == 1 + assert aioclient_mock.call_count == 1 + + call = aioclient_mock.mock_calls[0] + assert call[2] == MOCK_JSON + assert call[3] == MOCK_HEADER + + +async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): + """Test the that the calls get retried with new token on 401.""" + config = GoogleConfig(hass, DUMMY_CONFIG) + with patch( + "homeassistant.components.google_assistant.http._get_homegraph_token" + ) as mock_get_token: + mock_get_token.return_value = MOCK_TOKEN + + aioclient_mock.post(MOCK_URL, status=401, json={}) + + await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + + assert mock_get_token.call_count == 2 + assert aioclient_mock.call_count == 2 + + call = aioclient_mock.mock_calls[0] + assert call[2] == MOCK_JSON + assert call[3] == MOCK_HEADER + call = aioclient_mock.mock_calls[1] + assert call[2] == MOCK_JSON + assert call[3] == MOCK_HEADER + + +async def test_report_state(hass, aioclient_mock, hass_storage): + """Test the report state function.""" + config = GoogleConfig(hass, DUMMY_CONFIG) + message = {"devices": {}} + owner = User(name="Test User", perm_lookup=None, groups=[], is_owner=True) + + with patch.object(config, "async_call_homegraph_api") as mock_call, patch.object( + hass.auth, "async_get_owner" + ) as mock_get_owner: + mock_get_owner.return_value = owner + + await config.async_report_state(message) + mock_call.assert_called_once_with( + REPORT_STATE_BASE_URL, + {"requestId": ANY, "agentUserId": owner.id, "payload": message}, + ) From 1ec01b5e6cf0891b13f5733c56ef1c9c6842ec02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 19 Oct 2019 13:03:52 +0300 Subject: [PATCH 1046/3953] Upgrade pylint to 2.4.3 and astroid to 2.3.2 (#27912) https://pylint.readthedocs.io/en/latest/whatsnew/changelog.html#what-s-new-in-pylint-2-4-3 --- requirements_test.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index b6d5bdd7ee9fd4..e491d5ea42aa73 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,8 +12,8 @@ mock-open==1.3.1 mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 -pylint==2.4.2 -astroid==2.3.1 +pylint==2.4.3 +astroid==2.3.2 pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99a292d82f63b9..c2013c75a06a73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -13,8 +13,8 @@ mock-open==1.3.1 mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 -pylint==2.4.2 -astroid==2.3.1 +pylint==2.4.3 +astroid==2.3.2 pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 From 1f96a7becf8479403250e29c565767e620d6c5ee Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 19 Oct 2019 12:31:40 +0200 Subject: [PATCH 1047/3953] Update azure-pipelines-ci.yml --- azure-pipelines-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 82708151ad6589..f1abf2ff9dba9b 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -108,7 +108,7 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt | "v2"' + keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt' build: | set -e python -m venv venv From 1c0814d6f67e8b5ef4bea3f490377898e775c582 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 19 Oct 2019 13:42:49 +0200 Subject: [PATCH 1048/3953] Run pylint parallel (#27919) --- azure-pipelines-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index f1abf2ff9dba9b..1ca834b62136a1 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -167,7 +167,7 @@ stages: displayName: 'Install Home Assistant' - script: | . venv/bin/activate - pylint homeassistant + pylint -j 2 homeassistant displayName: 'Run pylint' - job: 'Mypy' pool: From f2617fd74a46ebcede264d8309437f177c1bfde5 Mon Sep 17 00:00:00 2001 From: guillempages Date: Sat, 19 Oct 2019 14:40:42 +0200 Subject: [PATCH 1049/3953] Split homematic color and effect support (#27299) * [homematic] Split color and effect support There are homematic devices (like HmIP-BSL) that support color but do not support effects. Split the support, so that color can be supported even if effects are not. * Make effect fully independent of color If a device supports effects for e.g. just brightness, it shouldn't be coupled to the color --- homeassistant/components/homematic/light.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 971a8a9cac044a..32fa0bb358e5b0 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -54,9 +54,12 @@ def is_on(self): @property def supported_features(self): """Flag supported features.""" + features = SUPPORT_BRIGHTNESS if "COLOR" in self._hmdevice.WRITENODE: - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_EFFECT - return SUPPORT_BRIGHTNESS + features |= SUPPORT_COLOR + if "PROGRAM" in self._hmdevice.WRITENODE: + features |= SUPPORT_EFFECT + return features @property def hs_color(self): @@ -110,4 +113,6 @@ def _init_data_struct(self): self._data[self._state] = None if self.supported_features & SUPPORT_COLOR: - self._data.update({"COLOR": None, "PROGRAM": None}) + self._data.update({"COLOR": None}) + if self.supported_features & SUPPORT_EFFECT: + self._data.update({"PROGRAM": None}) From eb48898687504e2d439b0d4c14cb2a3a0ffcf7c2 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 19 Oct 2019 17:44:40 +0200 Subject: [PATCH 1050/3953] Add climate profiles to Homematic IP Cloud (#27772) * Add climate service to Homematic IP Cloud to select the active profile * Add profiles ass presets * fix spelling * Re-Add PRESET_NONE for selection * Boost is a manual mode * Fixes based on review * Fixes after review --- .../components/homematicip_cloud/__init__.py | 61 +++++++++++++----- .../components/homematicip_cloud/climate.py | 63 ++++++++++++++++--- .../homematicip_cloud/services.yaml | 21 +++++-- .../homematicip_cloud/test_climate.py | 55 +++++++++++++++- tests/fixtures/homematicip_cloud.json | 6 +- 5 files changed, 170 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 139565bf249947..9a3191ac1686cd 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,14 +1,16 @@ """Support for HomematicIP Cloud devices.""" import logging +from homematicip.aio.group import AsyncHeatingGroup import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import comp_entity_ids from homeassistant.helpers.typing import ConfigType from .config_flow import configured_haps @@ -25,6 +27,7 @@ _LOGGER = logging.getLogger(__name__) +ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index" ATTR_DURATION = "duration" ATTR_ENDTIME = "endtime" ATTR_TEMPERATURE = "temperature" @@ -35,6 +38,7 @@ SERVICE_ACTIVATE_VACATION = "activate_vacation" SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode" SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" +SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" CONFIG_SCHEMA = vol.Schema( { @@ -86,6 +90,13 @@ {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} ) +SCHEMA_SET_ACTIVE_CLIMATE_PROFILE = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): comp_entity_ids, + vol.Required(ATTR_CLIMATE_PROFILE_INDEX): cv.positive_int, + } +) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" @@ -117,9 +128,8 @@ async def _async_activate_eco_mode_with_duration(service): if home: await home.activate_absence_with_duration(duration) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_absence_with_duration(duration) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_absence_with_duration(duration) hass.services.async_register( DOMAIN, @@ -138,9 +148,8 @@ async def _async_activate_eco_mode_with_period(service): if home: await home.activate_absence_with_period(endtime) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_absence_with_period(endtime) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_absence_with_period(endtime) hass.services.async_register( DOMAIN, @@ -160,9 +169,8 @@ async def _async_activate_vacation(service): if home: await home.activate_vacation(endtime, temperature) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_vacation(endtime, temperature) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_vacation(endtime, temperature) hass.services.async_register( DOMAIN, @@ -180,9 +188,8 @@ async def _async_deactivate_eco_mode(service): if home: await home.deactivate_absence() else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.deactivate_absence() + for hap in hass.data[DOMAIN].values(): + await hap.home.deactivate_absence() hass.services.async_register( DOMAIN, @@ -200,9 +207,8 @@ async def _async_deactivate_vacation(service): if home: await home.deactivate_vacation() else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.deactivate_vacation() + for hap in hass.data[DOMAIN].values(): + await hap.home.deactivate_vacation() hass.services.async_register( DOMAIN, @@ -211,6 +217,29 @@ async def _async_deactivate_vacation(service): schema=SCHEMA_DEACTIVATE_VACATION, ) + async def _set_active_climate_profile(service): + """Service to set the active climate profile.""" + entity_id_list = service.data[ATTR_ENTITY_ID] + climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 + + for hap in hass.data[DOMAIN].values(): + if entity_id_list != "all": + for entity_id in entity_id_list: + group = hap.hmip_device_by_entity_id.get(entity_id) + if group: + await group.set_active_profile(climate_profile_index) + else: + for group in hap.home.groups: + if isinstance(group, AsyncHeatingGroup): + await group.set_active_profile(climate_profile_index) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_ACTIVE_CLIMATE_PROFILE, + _set_active_climate_profile, + schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, + ) + def _get_home(hapid: str): """Return a HmIP home.""" hap = hass.data[DOMAIN].get(hapid) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index b8c055dda1fe64..f1f414169f672d 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -4,7 +4,7 @@ from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup -from homematicip.base.enums import AbsenceType +from homematicip.base.enums import AbsenceType, GroupType from homematicip.functionalHomes import IndoorClimateHome from homeassistant.components.climate import ClimateDevice @@ -25,6 +25,9 @@ from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .hap import HomematicipHAP +HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2} +COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5} + _LOGGER = logging.getLogger(__name__) HMIP_AUTOMATIC_CM = "AUTOMATIC" @@ -54,7 +57,7 @@ async def async_setup_entry( class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """Representation of a HomematicIP heating group.""" - def __init__(self, hap: HomematicipHAP, device) -> None: + def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None: """Initialize heating group.""" device.modelType = "HmIP-Heating-Group" self._simple_heating = None @@ -107,7 +110,7 @@ def hvac_mode(self) -> str: Need to be one of HVAC_MODE_*. """ if self._device.boostMode: - return HVAC_MODE_AUTO + return HVAC_MODE_HEAT if self._device.controlMode == HMIP_MANUAL_CM: return HVAC_MODE_HEAT @@ -129,6 +132,8 @@ def preset_mode(self): """ if self._device.boostMode: return PRESET_BOOST + if self.hvac_mode == HVAC_MODE_HEAT: + return PRESET_NONE if self._device.controlMode == HMIP_ECO_CM: absence_type = self._home.get_functionalHome(IndoorClimateHome).absenceType if absence_type == AbsenceType.VACATION: @@ -140,15 +145,15 @@ def preset_mode(self): ]: return PRESET_ECO - return PRESET_NONE + if self._device.activeProfile: + return self._device.activeProfile.name @property def preset_modes(self): - """Return a list of available preset modes. - - Requires SUPPORT_PRESET_MODE. - """ - return [PRESET_NONE, PRESET_BOOST] + """Return a list of available preset modes incl profiles.""" + presets = [PRESET_NONE, PRESET_BOOST] + presets.extend(self._device_profile_names) + return presets @property def min_temp(self) -> float: @@ -180,6 +185,46 @@ async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: await self._device.set_boost(False) if preset_mode == PRESET_BOOST: await self._device.set_boost() + if preset_mode in self._device_profile_names: + profile_idx = self._get_profile_idx_by_name(preset_mode) + await self.async_set_hvac_mode(HVAC_MODE_AUTO) + await self._device.set_active_profile(profile_idx) + + @property + def _device_profiles(self): + """Return the relevant profiles of the device.""" + return [ + profile + for profile in self._device.profiles + if profile.visible + and profile.name != "" + and profile.index in self._relevant_profile_group + ] + + @property + def _device_profile_names(self): + """Return a collection of profile names.""" + return [profile.name for profile in self._device_profiles] + + def _get_profile_idx_by_name(self, profile_name): + """Return a profile index by name.""" + relevant_index = self._relevant_profile_group + index_name = [ + profile.index + for profile in self._device_profiles + if profile.name == profile_name + ] + + return relevant_index[index_name[0]] + + @property + def _relevant_profile_group(self): + """Return the relevant profile groups.""" + return ( + HEATING_PROFILES + if self._device.groupType == GroupType.HEATING + else COOLING_PROFILES + ) def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index cf93b3065ee4d0..f426c9b5d220be 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -7,7 +7,7 @@ activate_eco_mode_with_duration: description: The duration of eco mode in minutes. example: 60 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx activate_eco_mode_with_period: @@ -17,7 +17,7 @@ activate_eco_mode_with_period: description: The time when the eco mode should automatically be disabled. example: 2019-02-17 14:00 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx activate_vacation: @@ -30,20 +30,31 @@ activate_vacation: description: the set temperature during the vacation mode. example: 18.5 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx deactivate_eco_mode: description: Deactivates the eco mode immediately. fields: accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx deactivate_vacation: description: Deactivates the vacation mode immediately. fields: accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx +set_active_climate_profile: + description: Set the active climate profile index. + fields: + entity_id: + description: The ID of the climte entity. Use 'all' keyword to switch the profile for all entities. + example: climate.livingroom + climate_profile_index: + description: The index of the climate profile (1 based) + example: 1 + + diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index bdfd26319e6a3b..80e4e74e4519df 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -49,8 +49,13 @@ async def test_hmip_heating_group(hass, default_mock_hap): assert ha_state.attributes["max_temp"] == 30.0 assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes["current_humidity"] == 47 - assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_NONE, PRESET_BOOST] + assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_NONE, + PRESET_BOOST, + "STD", + "Winter", + ] service_call_counter = len(hmip_device.mock_calls) @@ -117,7 +122,7 @@ async def test_hmip_heating_group(hass, default_mock_hap): assert hmip_device.mock_calls[-1][1] == (False,) await async_manipulate_test_data(hass, hmip_device, "boostMode", False) ha_state = hass.states.get(entity_id) - assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" # Not required for hmip, but a posiblity to send no temperature. await hass.services.async_call( @@ -153,6 +158,18 @@ async def test_hmip_heating_group(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO + # Not required for hmip, but a posiblity to send no temperature. + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Winter"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 16 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + async def test_hmip_climate_services(hass, mock_hap_with_service): """Test HomematicipHeatingGroup.""" @@ -264,3 +281,35 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): assert home.mock_calls[-1][1] == () # There is no further call on connection. assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 + + +async def test_hmip_heating_group_services(hass, mock_hap_with_service): + """Test HomematicipHeatingGroup services.""" + entity_id = "climate.badezimmer" + entity_name = "Badezimmer" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap_with_service, entity_id, entity_name, device_model + ) + assert ha_state + + await hass.services.async_call( + "homematicip_cloud", + "set_active_climate_profile", + {"climate_profile_index": 2, "entity_id": "climate.badezimmer"}, + blocking=True, + ) + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + assert len(hmip_device._connection.mock_calls) == 2 # pylint: disable=W0212 + + await hass.services.async_call( + "homematicip_cloud", + "set_active_climate_profile", + {"climate_profile_index": 2, "entity_id": "all"}, + blocking=True, + ) + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + assert len(hmip_device._connection.mock_calls) == 12 # pylint: disable=W0212 diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 1d3d5bfd8f45da..e17df9c2039347 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -4638,7 +4638,7 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000021", "index": "PROFILE_1", - "name": "", + "name": "STD", "profileId": "00000000-0000-0000-0000-000000000038", "visible": true }, @@ -4646,9 +4646,9 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000021", "index": "PROFILE_2", - "name": "", + "name": "Winter", "profileId": "00000000-0000-0000-0000-000000000039", - "visible": false + "visible": true }, "PROFILE_3": { "enabled": true, From 8c0deeb176911e0b9c8088c67836d83965852152 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 19 Oct 2019 18:22:32 +0200 Subject: [PATCH 1051/3953] Move imports in luftdaten component (#27929) --- homeassistant/components/luftdaten/__init__.py | 6 ++---- homeassistant/components/luftdaten/config_flow.py | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 86129eafc0295e..ac524502f8d67b 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -1,6 +1,8 @@ """Support for Luftdaten stations.""" import logging +from luftdaten import Luftdaten +from luftdaten.exceptions import LuftdatenError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -114,8 +116,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up Luftdaten as config entry.""" - from luftdaten import Luftdaten - from luftdaten.exceptions import LuftdatenError if not isinstance(config_entry.data[CONF_SENSOR_ID], int): _async_fixup_sensor_id(hass, config_entry, config_entry.data[CONF_SENSOR_ID]) @@ -191,8 +191,6 @@ def __init__(self, client, sensor_conditions): async def async_update(self): """Update sensor/binary sensor data.""" - from luftdaten.exceptions import LuftdatenError - try: await self.client.get_data() diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index 7a8ef0df8ba207..1f382b86c0fb1c 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure the Luftdaten component.""" from collections import OrderedDict +from luftdaten import Luftdaten +from luftdaten.exceptions import LuftdatenConnectionError import voluptuous as vol from homeassistant import config_entries @@ -60,7 +62,6 @@ async def async_step_import(self, import_config): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from luftdaten import Luftdaten, exceptions if not user_input: return self._show_form() @@ -75,7 +76,7 @@ async def async_step_user(self, user_input=None): try: await luftdaten.get_data() valid = await luftdaten.validate_sensor() - except exceptions.LuftdatenConnectionError: + except LuftdatenConnectionError: return self._show_form({CONF_SENSOR_ID: "communication_error"}) if not valid: From de1477f00b5cc9af165760ece900902a42129cbd Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 19 Oct 2019 18:24:28 +0200 Subject: [PATCH 1052/3953] Bump version of homematicip to 0.10.13 (#27928) The Home websocket can now automatically reopen a lost connection (default) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 40c8c7c359893a..4feef19c8dac8a 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/integrations/homematicip_cloud", "requirements": [ - "homematicip==0.10.12" + "homematicip==0.10.13" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 80be965c41556d..8e935142ff0291 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.12 +homematicip==0.10.13 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2013c75a06a73..93d639dadd9ed4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -248,7 +248,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.12 +homematicip==0.10.13 # homeassistant.components.google # homeassistant.components.remember_the_milk From 840001e168ede7a123eae7a3c93bd954145c6ccf Mon Sep 17 00:00:00 2001 From: Greg Rapp Date: Sat, 19 Oct 2019 14:26:07 -0400 Subject: [PATCH 1053/3953] Added night arm mode support to Envisalink component (#27087) --- .../components/envisalink/alarm_control_panel.py | 9 +++++++++ homeassistant/components/envisalink/manifest.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 81e656708c5caf..663f19c8ed5806 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -8,6 +8,7 @@ ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, @@ -126,6 +127,8 @@ def state(self): if self._info["status"]["alarm"]: state = STATE_ALARM_TRIGGERED + elif self._info["status"]["armed_zero_entry_delay"]: + state = STATE_ALARM_ARMED_NIGHT elif self._info["status"]["armed_away"]: state = STATE_ALARM_ARMED_AWAY elif self._info["status"]["armed_stay"]: @@ -173,6 +176,12 @@ async def async_alarm_trigger(self, code=None): """Alarm trigger command. Will be used to trigger a panic alarm.""" self.hass.data[DATA_EVL].panic_alarm(self._panic_type) + async def async_alarm_arm_night(self, code=None): + """Send arm night command.""" + self.hass.data[DATA_EVL].arm_night_partition( + str(code) if code else str(self._code), self._partition_number + ) + @callback def async_alarm_keypress(self, keypress=None): """Send custom keypress.""" diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 3cee270f099a80..52303c18413448 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -7,4 +7,4 @@ ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file From 9ec06029862c41370bd5f770745bbb2d8431e44b Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 20:33:05 +0200 Subject: [PATCH 1054/3953] Move imports in cpuspeed component (#27890) --- homeassistant/components/cpuspeed/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 9484e7709985d8..53598e24c70dfc 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -1,11 +1,12 @@ """Support for displaying the current CPU speed.""" import logging +from cpuinfo import cpuinfo import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -75,7 +76,6 @@ def icon(self): def update(self): """Get the latest data and updates the state.""" - from cpuinfo import cpuinfo self.info = cpuinfo.get_cpu_info() if HZ_ACTUAL_RAW in self.info: From 758fcc9b001e971d08f0092285f9ab285f4bcd3d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 Oct 2019 11:33:21 -0700 Subject: [PATCH 1055/3953] Remove helper imports relying on installed requirements (#27898) --- homeassistant/helpers/state.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 2f49a566a32e5e..4cb7fb85bff29a 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -11,11 +11,9 @@ 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 -from homeassistant.components.mysensors.switch import ATTR_IR_CODE, SERVICE_SEND_IR_CODE from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_OPTION, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, @@ -41,7 +39,6 @@ STATE_OPEN, STATE_UNKNOWN, STATE_UNLOCKED, - SERVICE_SELECT_OPTION, ) from homeassistant.core import Context, State, DOMAIN as HASS_DOMAIN from .typing import HomeAssistantType @@ -54,8 +51,6 @@ # Each item is a service with a list of required attributes. SERVICE_ATTRIBUTES = { SERVICE_NOTIFY: [ATTR_MESSAGE], - SERVICE_SEND_IR_CODE: [ATTR_IR_CODE], - SERVICE_SELECT_OPTION: [ATTR_OPTION], SERVICE_SET_COVER_POSITION: [ATTR_POSITION], SERVICE_SET_COVER_TILT_POSITION: [ATTR_TILT_POSITION], } From 381d423fecd95f2b901f3ca1fa277746e7756c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 19 Oct 2019 21:35:57 +0300 Subject: [PATCH 1056/3953] Upgrade mypy to 0.740 (#27913) * Upgrade mypy to 0.740 http://mypy-lang.blogspot.com/2019/10/mypy-0740-released.html * Type hint additions * Type fixes * Remove no longer needed type ignores and casts * Disable untyped definition checks in bunch of files --- homeassistant/auth/__init__.py | 4 ++-- homeassistant/components/cover/__init__.py | 2 +- homeassistant/components/group/__init__.py | 2 +- homeassistant/components/group/cover.py | 5 +++-- homeassistant/components/group/light.py | 1 + homeassistant/components/group/notify.py | 2 +- homeassistant/components/sun/__init__.py | 2 +- homeassistant/components/switch/light.py | 2 +- homeassistant/components/websocket_api/http.py | 7 ++++--- homeassistant/components/websocket_api/sensor.py | 2 +- homeassistant/components/zone/config_flow.py | 2 +- homeassistant/config_entries.py | 4 ++-- homeassistant/helpers/config_entry_flow.py | 2 +- homeassistant/helpers/storage.py | 1 + homeassistant/util/location.py | 4 ++-- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 17 files changed, 25 insertions(+), 21 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 64391debc102ec..921bec71e784c4 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -45,7 +45,7 @@ async def auth_manager_from_config( ) ) else: - providers = () + providers = [] # So returned auth providers are in same order as config provider_hash: _ProviderDict = OrderedDict() for provider in providers: @@ -57,7 +57,7 @@ async def auth_manager_from_config( *(auth_mfa_module_from_config(hass, config) for config in module_configs) ) else: - modules = () + modules = [] # So returned auth modules are in same order as config module_hash: _MfaModuleDict = OrderedDict() for module in modules: diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 8d2b4430fe110c..cfac143a5d80f7 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -34,7 +34,7 @@ ) -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 39574a2b03b7e2..29126c82d44aa7 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -36,7 +36,7 @@ from homeassistant.helpers.typing import HomeAssistantType -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs DOMAIN = "group" diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index c5200082f2fb25..f7a9643e5c8cb3 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -44,6 +44,7 @@ # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -74,7 +75,7 @@ def __init__(self, name, entities): """Initialize a CoverGroup entity.""" self._name = name self._is_closed = False - self._cover_position = 100 + self._cover_position: Optional[int] = 100 self._tilt_position = None self._supported_features = 0 self._assumed_state = True @@ -178,7 +179,7 @@ def is_closed(self): return self._is_closed @property - def current_cover_position(self): + def current_cover_position(self) -> Optional[int]: """Return current position for all covers.""" return self._cover_position diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index e77c858fc0272a..858045524948c5 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -45,6 +45,7 @@ # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 2ffb7fea049438..e17990690faadb 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -18,7 +18,7 @@ ) -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 7d883e273e5053..e848449e61e529 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -18,7 +18,7 @@ from homeassistant.util import dt as dt_util -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 8f3b5d87f8c202..b0abf95799198d 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -21,7 +21,7 @@ from homeassistant.components.light import PLATFORM_SCHEMA, Light -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 08a0430ee2a976..be1830aa07bdfc 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -2,6 +2,7 @@ import asyncio from contextlib import suppress import logging +from typing import Optional from aiohttp import web, WSMsgType import async_timeout @@ -25,7 +26,7 @@ from .messages import error_message -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs class WebsocketAPIView(HomeAssistantView): @@ -47,7 +48,7 @@ def __init__(self, hass, request): """Initialize an active connection.""" self.hass = hass self.request = request - self.wsock = None + self.wsock: Optional[web.WebSocketResponse] = None self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) self._handle_task = None self._writer_task = None @@ -115,7 +116,7 @@ async def async_handle(self): # Py3.7+ if hasattr(asyncio, "current_task"): # pylint: disable=no-member - self._handle_task = asyncio.current_task() # type: ignore + self._handle_task = asyncio.current_task() else: self._handle_task = asyncio.Task.current_task() diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 20a6a90860be42..f8f1257aefca4a 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -10,7 +10,7 @@ ) -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index d23fb5a47577ca..3963375477294f 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -20,7 +20,7 @@ from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE -# mypy: allow-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs @callback diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f8c7c7a9da1fa1..aee15d6c0ce9aa 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -15,7 +15,7 @@ from homeassistant.util.decorator import Registry from homeassistant.helpers import entity_registry -# mypy: allow-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) _UNDEF = object() @@ -676,7 +676,7 @@ 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): + def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None: """Initialize a subclass, register if possible.""" super().__init_subclass__(**kwargs) # type: ignore if domain is not None: diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 374ef7958464de..7a1512957a2962 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -3,7 +3,7 @@ from homeassistant import config_entries from .typing import HomeAssistantType -# mypy: allow-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index cd99a47cf57223..72458d24c82089 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -13,6 +13,7 @@ # mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any +# mypy: no-check-untyped-defs STORAGE_DIR = ".storage" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index f81c40a52bb675..7c61a8ab1e9197 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -6,7 +6,7 @@ import asyncio import collections import math -from typing import Any, Optional, Tuple, Dict, cast +from typing import Any, Optional, Tuple, Dict import aiohttp @@ -159,7 +159,7 @@ def vincenty( if miles: s *= MILES_PER_KILOMETER # kilometers to miles - return round(cast(float, s), 6) + return round(s, 6) async def _get_ipapi(session: aiohttp.ClientSession) -> Optional[Dict[str, Any]]: diff --git a/requirements_test.txt b/requirements_test.txt index e491d5ea42aa73..7af2ec0dde354b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ codecov==2.0.15 flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 -mypy==0.730 +mypy==0.740 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93d639dadd9ed4..dc6267be6a9ad6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ codecov==2.0.15 flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 -mypy==0.730 +mypy==0.740 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.4.3 From 5fa4632c125af71f19ae04a898789d02153c0936 Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 19 Oct 2019 20:39:31 +0200 Subject: [PATCH 1057/3953] Add improved scene support to the cover integration (#27914) --- .../components/cover/reproduce_state.py | 117 +++++++++++ .../components/cover/test_reproduce_state.py | 198 ++++++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 homeassistant/components/cover/reproduce_state.py create mode 100644 tests/components/cover/test_reproduce_state.py diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py new file mode 100644 index 00000000000000..64ea410ce933f9 --- /dev/null +++ b/homeassistant/components/cover/reproduce_state.py @@ -0,0 +1,117 @@ +"""Reproduce an Cover state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if ( + cur_state.state == state.state + and cur_state.attributes.get(ATTR_CURRENT_POSITION) + == state.attributes.get(ATTR_CURRENT_POSITION) + and cur_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + == state.attributes.get(ATTR_CURRENT_TILT_POSITION) + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + service_data_tilting = {ATTR_ENTITY_ID: state.entity_id} + + if cur_state.state != state.state or cur_state.attributes.get( + ATTR_CURRENT_POSITION + ) != state.attributes.get(ATTR_CURRENT_POSITION): + # Open/Close + if state.state == STATE_CLOSED or state.state == STATE_CLOSING: + service = SERVICE_CLOSE_COVER + elif state.state == STATE_OPEN or state.state == STATE_OPENING: + if ( + ATTR_CURRENT_POSITION in cur_state.attributes + and ATTR_CURRENT_POSITION in state.attributes + ): + service = SERVICE_SET_COVER_POSITION + service_data[ATTR_POSITION] = state.attributes[ATTR_CURRENT_POSITION] + else: + service = SERVICE_OPEN_COVER + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + if ( + ATTR_CURRENT_TILT_POSITION in state.attributes + and ATTR_CURRENT_TILT_POSITION in cur_state.attributes + and cur_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + != state.attributes.get(ATTR_CURRENT_TILT_POSITION) + ): + # Tilt position + if state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 100: + service_tilting = SERVICE_OPEN_COVER_TILT + elif state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 0: + service_tilting = SERVICE_CLOSE_COVER_TILT + else: + service_tilting = SERVICE_SET_COVER_TILT_POSITION + service_data_tilting[ATTR_TILT_POSITION] = state.attributes[ + ATTR_CURRENT_TILT_POSITION + ] + + await hass.services.async_call( + DOMAIN, + service_tilting, + service_data_tilting, + context=context, + blocking=True, + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Cover states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/cover/test_reproduce_state.py b/tests/components/cover/test_reproduce_state.py new file mode 100644 index 00000000000000..39fdf3d3992d2f --- /dev/null +++ b/tests/components/cover/test_reproduce_state.py @@ -0,0 +1,198 @@ +"""Test reproduce state for Cover.""" +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, +) +from homeassistant.const import ( + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + STATE_CLOSED, + STATE_OPEN, +) +from homeassistant.core import State +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Cover states.""" + hass.states.async_set("cover.entity_close", STATE_CLOSED, {}) + hass.states.async_set( + "cover.entity_close_attr", + STATE_CLOSED, + {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0}, + ) + hass.states.async_set( + "cover.entity_close_tilt", STATE_CLOSED, {ATTR_CURRENT_TILT_POSITION: 50} + ) + hass.states.async_set("cover.entity_open", STATE_OPEN, {}) + hass.states.async_set( + "cover.entity_slightly_open", STATE_OPEN, {ATTR_CURRENT_POSITION: 50} + ) + hass.states.async_set( + "cover.entity_open_attr", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 0}, + ) + hass.states.async_set( + "cover.entity_open_tilt", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50}, + ) + hass.states.async_set( + "cover.entity_entirely_open", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 100}, + ) + + close_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER) + open_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + close_tilt_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER_TILT) + open_tilt_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER_TILT) + position_calls = async_mock_service(hass, "cover", SERVICE_SET_COVER_POSITION) + position_tilt_calls = async_mock_service( + hass, "cover", SERVICE_SET_COVER_TILT_POSITION + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("cover.entity_close", STATE_CLOSED), + State( + "cover.entity_close_attr", + STATE_CLOSED, + {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0}, + ), + State( + "cover.entity_close_tilt", + STATE_CLOSED, + {ATTR_CURRENT_TILT_POSITION: 50}, + ), + State("cover.entity_open", STATE_OPEN), + State( + "cover.entity_slightly_open", STATE_OPEN, {ATTR_CURRENT_POSITION: 50} + ), + State( + "cover.entity_open_attr", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 0}, + ), + State( + "cover.entity_open_tilt", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50}, + ), + State( + "cover.entity_entirely_open", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 100}, + ), + ], + blocking=True, + ) + + assert len(close_calls) == 0 + assert len(open_calls) == 0 + assert len(close_tilt_calls) == 0 + assert len(open_tilt_calls) == 0 + assert len(position_calls) == 0 + assert len(position_tilt_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("cover.entity_close", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(close_calls) == 0 + assert len(open_calls) == 0 + assert len(close_tilt_calls) == 0 + assert len(open_tilt_calls) == 0 + assert len(position_calls) == 0 + assert len(position_tilt_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("cover.entity_close", STATE_OPEN), + State( + "cover.entity_close_attr", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50}, + ), + State( + "cover.entity_close_tilt", + STATE_CLOSED, + {ATTR_CURRENT_TILT_POSITION: 100}, + ), + State("cover.entity_open", STATE_CLOSED), + State("cover.entity_slightly_open", STATE_OPEN, {}), + State("cover.entity_open_attr", STATE_CLOSED, {}), + State( + "cover.entity_open_tilt", STATE_OPEN, {ATTR_CURRENT_TILT_POSITION: 0} + ), + State( + "cover.entity_entirely_open", + STATE_CLOSED, + {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0}, + ), + # Should not raise + State("cover.non_existing", "on"), + ], + blocking=True, + ) + + valid_close_calls = [ + {"entity_id": "cover.entity_open"}, + {"entity_id": "cover.entity_open_attr"}, + {"entity_id": "cover.entity_entirely_open"}, + ] + assert len(close_calls) == 3 + for call in close_calls: + assert call.domain == "cover" + assert call.data in valid_close_calls + valid_close_calls.remove(call.data) + + valid_open_calls = [ + {"entity_id": "cover.entity_close"}, + {"entity_id": "cover.entity_slightly_open"}, + {"entity_id": "cover.entity_open_tilt"}, + ] + assert len(open_calls) == 3 + for call in open_calls: + assert call.domain == "cover" + assert call.data in valid_open_calls + valid_open_calls.remove(call.data) + + valid_close_tilt_calls = [ + {"entity_id": "cover.entity_open_tilt"}, + {"entity_id": "cover.entity_entirely_open"}, + ] + assert len(close_tilt_calls) == 2 + for call in close_tilt_calls: + assert call.domain == "cover" + assert call.data in valid_close_tilt_calls + valid_close_tilt_calls.remove(call.data) + + assert len(open_tilt_calls) == 1 + assert open_tilt_calls[0].domain == "cover" + assert open_tilt_calls[0].data == {"entity_id": "cover.entity_close_tilt"} + + assert len(position_calls) == 1 + assert position_calls[0].domain == "cover" + assert position_calls[0].data == { + "entity_id": "cover.entity_close_attr", + ATTR_POSITION: 50, + } + + assert len(position_tilt_calls) == 1 + assert position_tilt_calls[0].domain == "cover" + assert position_tilt_calls[0].data == { + "entity_id": "cover.entity_close_attr", + ATTR_TILT_POSITION: 50, + } From 48e5655379ad434790d81c989f18c5ba0cfe47e1 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Sat, 19 Oct 2019 14:10:35 -0500 Subject: [PATCH 1058/3953] Bump abodepy version (#27931) --- homeassistant/components/abode/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 8316691f70100c..b54120c7cbdf33 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", "requirements": [ - "abodepy==0.16.5" + "abodepy==0.16.6" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8e935142ff0291..d829377d8a208e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -103,7 +103,7 @@ WazeRouteCalculator==0.10 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.16.5 +abodepy==0.16.6 # homeassistant.components.mcp23017 adafruit-blinka==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc6267be6a9ad6..cb0b56cb5aeb8e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -46,7 +46,7 @@ RtmAPI==0.7.2 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.16.5 +abodepy==0.16.6 # homeassistant.components.androidtv adb-shell==0.0.7 From cb061e57d25c331daea6a2ae98eb80701dd2090c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 19 Oct 2019 21:11:09 +0200 Subject: [PATCH 1059/3953] Add support for AdGuard Home v0.99.0 (#27926) * Bump adguardhome to 0.3.0 * Add a more user friendly version handling and added logs * :pencil2: Fixes spelling error in abort messages * :pencil2: Error messages improvements, suggested by cgtobi --- .../components/adguard/.translations/en.json | 2 + homeassistant/components/adguard/__init__.py | 16 ++++- .../components/adguard/config_flow.py | 25 ++++++- homeassistant/components/adguard/const.py | 2 + .../components/adguard/manifest.json | 2 +- homeassistant/components/adguard/strings.json | 8 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/adguard/test_config_flow.py | 72 +++++++++++++++++-- 9 files changed, 116 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/adguard/.translations/en.json b/homeassistant/components/adguard/.translations/en.json index 6e3b5b585039da..8bfb8516fd8f7d 100644 --- a/homeassistant/components/adguard/.translations/en.json +++ b/homeassistant/components/adguard/.translations/en.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", + "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.", "existing_instance_updated": "Updated existing configuration.", "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." }, diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index ba716ae0f9c8e2..bb53d00aab888e 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -1,8 +1,9 @@ """Support for AdGuard Home.""" +from distutils.version import LooseVersion import logging from typing import Any, Dict -from adguardhome import AdGuardHome, AdGuardHomeError +from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError import voluptuous as vol from homeassistant.components.adguard.const import ( @@ -10,6 +11,7 @@ DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN, + MIN_ADGUARD_HOME_VERSION, SERVICE_ADD_URL, SERVICE_DISABLE_URL, SERVICE_ENABLE_URL, @@ -27,6 +29,7 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity @@ -64,6 +67,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard + try: + version = await adguard.version() + except AdGuardHomeConnectionError as exception: + raise ConfigEntryNotReady from exception + + if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): + _LOGGER.error( + "This integration requires AdGuard Home v0.99.0 or higher to work correctly" + ) + raise ConfigEntryNotReady + for component in "sensor", "switch": hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index 5a096aeceed409..9f5645edb8dc27 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -1,11 +1,12 @@ """Config flow to configure the AdGuard Home integration.""" +from distutils.version import LooseVersion import logging from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.adguard.const import DOMAIN +from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_HOST, @@ -83,11 +84,20 @@ async def async_step_user(self, user_input=None): ) try: - await adguard.version() + version = await adguard.version() except AdGuardHomeConnectionError: errors["base"] = "connection_error" return await self._show_setup_form(errors) + if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): + return self.async_abort( + reason="adguard_home_outdated", + description_placeholders={ + "current_version": version, + "minimal_version": MIN_ADGUARD_HOME_VERSION, + }, + ) + return self.async_create_entry( title=user_input[CONF_HOST], data={ @@ -156,11 +166,20 @@ async def async_step_hassio_confirm(self, user_input=None): ) try: - await adguard.version() + version = await adguard.version() except AdGuardHomeConnectionError: errors["base"] = "connection_error" return await self._show_hassio_form(errors) + if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): + return self.async_abort( + reason="adguard_home_addon_outdated", + description_placeholders={ + "current_version": version, + "minimal_version": MIN_ADGUARD_HOME_VERSION, + }, + ) + return self.async_create_entry( title=self._hassio_discovery["addon"], data={ diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index c77d76a70cfa36..eb12a9c163f624 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -7,6 +7,8 @@ CONF_FORCE = "force" +MIN_ADGUARD_HOME_VERSION = "v0.99.0" + SERVICE_ADD_URL = "add_url" SERVICE_DISABLE_URL = "disable_url" SERVICE_ENABLE_URL = "enable_url" diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index f207e6dff09b06..45fd21f4fc8085 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adguard", "requirements": [ - "adguardhome==0.2.1" + "adguardhome==0.3.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index b3966bca8206a3..d33ba2b397a84e 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -23,8 +23,10 @@ "connection_error": "Failed to connect." }, "abort": { - "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed.", - "existing_instance_updated": "Updated existing configuration." + "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", + "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.", + "existing_instance_updated": "Updated existing configuration.", + "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." } } -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index d829377d8a208e..d30d01f02c04ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,7 +115,7 @@ adafruit-circuitpython-mcp230xx==1.1.2 adb-shell==0.0.7 # homeassistant.components.adguard -adguardhome==0.2.1 +adguardhome==0.3.0 # homeassistant.components.frontier_silicon afsapi==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb0b56cb5aeb8e..2a4fdf4eac7d12 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -52,7 +52,7 @@ abodepy==0.16.6 adb-shell==0.0.7 # homeassistant.components.adguard -adguardhome==0.2.1 +adguardhome==0.3.0 # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.10 diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index ea5e5ad2276d77..dbda1e99a48fc0 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -3,9 +3,9 @@ import aiohttp -from homeassistant import data_entry_flow, config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.adguard import config_flow -from homeassistant.components.adguard.const import DOMAIN +from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -65,7 +65,7 @@ async def test_full_flow_implementation(hass, aioclient_mock): FIXTURE_USER_INPUT[CONF_HOST], FIXTURE_USER_INPUT[CONF_PORT], ), - json={"version": "1.0"}, + json={"version": "v0.99.0"}, headers={"Content-Type": "application/json"}, ) @@ -133,8 +133,19 @@ async def test_hassio_update_instance_not_running(hass): assert result["reason"] == "existing_instance_updated" -async def test_hassio_update_instance_running(hass): +async def test_hassio_update_instance_running(hass, aioclient_mock): """Test we only allow a single config flow.""" + aioclient_mock.get( + "http://mock-adguard-updated:3000/control/status", + json={"version": "v0.99.0"}, + headers={"Content-Type": "application/json"}, + ) + aioclient_mock.get( + "http://mock-adguard:3000/control/status", + json={"version": "v0.99.0"}, + headers={"Content-Type": "application/json"}, + ) + entry = MockConfigEntry( domain="adguard", data={ @@ -187,7 +198,7 @@ async def test_hassio_confirm(hass, aioclient_mock): """Test we can finish a config flow.""" aioclient_mock.get( "http://mock-adguard:3000/control/status", - json={"version": "1.0"}, + json={"version": "v0.99.0"}, headers={"Content-Type": "application/json"}, ) @@ -228,3 +239,54 @@ async def test_hassio_connection_error(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "hassio_confirm" assert result["errors"] == {"base": "connection_error"} + + +async def test_outdated_adguard_version(hass, aioclient_mock): + """Test we show abort when connecting with unsupported AdGuard version.""" + aioclient_mock.get( + "{}://{}:{}/control/status".format( + "https" if FIXTURE_USER_INPUT[CONF_SSL] else "http", + FIXTURE_USER_INPUT[CONF_HOST], + FIXTURE_USER_INPUT[CONF_PORT], + ), + json={"version": "v0.98.0"}, + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.AdGuardHomeFlowHandler() + 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" + + result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "adguard_home_outdated" + assert result["description_placeholders"] == { + "current_version": "v0.98.0", + "minimal_version": MIN_ADGUARD_HOME_VERSION, + } + + +async def test_outdated_adguard_addon_version(hass, aioclient_mock): + """Test we show abort when connecting with unsupported AdGuard add-on version.""" + aioclient_mock.get( + "http://mock-adguard:3000/control/status", + json={"version": "v0.98.0"}, + headers={"Content-Type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_init( + "adguard", + data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, + context={"source": "hassio"}, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "adguard_home_addon_outdated" + assert result["description_placeholders"] == { + "current_version": "v0.98.0", + "minimal_version": MIN_ADGUARD_HOME_VERSION, + } From efae9a24d5a51e1f2312306b8ea5bf2745cf6e82 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sat, 19 Oct 2019 20:27:15 +0100 Subject: [PATCH 1060/3953] remove duplicate unique_id, add unique_id for issues (#27916) --- homeassistant/components/geniushub/__init__.py | 2 +- homeassistant/components/geniushub/sensor.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index d9f6c877cbcb39..692c72e5776cba 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -214,7 +214,7 @@ def __init__(self, broker, zone) -> None: super().__init__() self._zone = zone - self._unique_id = f"{broker.hub_uid}_device_{zone.id}" + self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" self._max_temp = self._min_temp = self._supported_features = None diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 2f5d9bceb8bcda..bd73c700e65471 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -94,6 +94,8 @@ def __init__(self, broker, level) -> None: super().__init__() self._hub = broker.client + self._unique_id = f"{broker.hub_uid}_{GH_LEVEL_MAPPING[level]}" + self._name = f"GeniusHub {GH_LEVEL_MAPPING[level]}" self._level = level self._issues = [] From febc48c84b3f36e9080e2565b8c7f9595d130137 Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Sat, 19 Oct 2019 21:40:45 +0200 Subject: [PATCH 1061/3953] Remove stride (#27934) * Remove stride * Remove Stride * Remove stride * Remove stride * Remove stride --- .coveragerc | 1 - homeassistant/components/stride/__init__.py | 1 - homeassistant/components/stride/manifest.json | 10 --- homeassistant/components/stride/notify.py | 84 ------------------- requirements_all.txt | 3 - 5 files changed, 99 deletions(-) delete mode 100644 homeassistant/components/stride/__init__.py delete mode 100644 homeassistant/components/stride/manifest.json delete mode 100644 homeassistant/components/stride/notify.py diff --git a/.coveragerc b/.coveragerc index 389d289ea20be9..7071312a99c21d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -637,7 +637,6 @@ omit = homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* homeassistant/components/streamlabswater/* - homeassistant/components/stride/notify.py homeassistant/components/suez_water/* homeassistant/components/supervisord/sensor.py homeassistant/components/swiss_hydrological_data/sensor.py diff --git a/homeassistant/components/stride/__init__.py b/homeassistant/components/stride/__init__.py deleted file mode 100644 index 461a3ee744f9bc..00000000000000 --- a/homeassistant/components/stride/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The stride component.""" diff --git a/homeassistant/components/stride/manifest.json b/homeassistant/components/stride/manifest.json deleted file mode 100644 index 840984ad073a3f..00000000000000 --- a/homeassistant/components/stride/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "stride", - "name": "Stride", - "documentation": "https://www.home-assistant.io/integrations/stride", - "requirements": [ - "pystride==0.1.7" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/stride/notify.py b/homeassistant/components/stride/notify.py deleted file mode 100644 index 082d986491a9d7..00000000000000 --- a/homeassistant/components/stride/notify.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Stride platform for notify component.""" -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_ROOM, CONF_TOKEN -import homeassistant.helpers.config_validation as cv - -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService, -) - -_LOGGER = logging.getLogger(__name__) - -CONF_PANEL = "panel" -CONF_CLOUDID = "cloudid" - -DEFAULT_PANEL = None - -VALID_PANELS = {"info", "note", "tip", "warning", None} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_CLOUDID): cv.string, - vol.Required(CONF_ROOM): cv.string, - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_PANEL, default=DEFAULT_PANEL): vol.In(VALID_PANELS), - } -) - - -def get_service(hass, config, discovery_info=None): - """Get the Stride notification service.""" - return StrideNotificationService( - config[CONF_TOKEN], config[CONF_ROOM], config[CONF_PANEL], config[CONF_CLOUDID] - ) - - -class StrideNotificationService(BaseNotificationService): - """Implement the notification service for Stride.""" - - def __init__(self, token, default_room, default_panel, cloudid): - """Initialize the service.""" - self._token = token - self._default_room = default_room - self._default_panel = default_panel - self._cloudid = cloudid - - from stride import Stride - - self._stride = Stride(self._cloudid, access_token=self._token) - - def send_message(self, message="", **kwargs): - """Send a message.""" - panel = self._default_panel - - if kwargs.get(ATTR_DATA) is not None: - data = kwargs.get(ATTR_DATA) - if (data.get(CONF_PANEL) is not None) and ( - data.get(CONF_PANEL) in VALID_PANELS - ): - panel = data.get(CONF_PANEL) - - message_text = { - "type": "paragraph", - "content": [{"type": "text", "text": message}], - } - panel_text = message_text - if panel is not None: - panel_text = { - "type": "panel", - "attrs": {"panelType": panel}, - "content": [message_text], - } - - message_doc = {"body": {"version": 1, "type": "doc", "content": [panel_text]}} - - targets = kwargs.get(ATTR_TARGET, [self._default_room]) - - for target in targets: - self._stride.message_room(target, message_doc) diff --git a/requirements_all.txt b/requirements_all.txt index d30d01f02c04ed..b4b60784a3dd6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1464,9 +1464,6 @@ pyspcwebgw==0.4.0 # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2 -# homeassistant.components.stride -pystride==0.1.7 - # homeassistant.components.suez_water pysuez==0.1.17 From bb5da77f2c6535754d6aeef4c6b21c71784ee7b9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 Oct 2019 12:44:51 -0700 Subject: [PATCH 1062/3953] Import shuffle (#27935) * Simplify persistent_notification ws command * Move cors import inside setup * Fix stream imports --- homeassistant/components/http/cors.py | 5 ++++- .../components/persistent_notification/__init__.py | 10 ++-------- homeassistant/components/stream/__init__.py | 8 ++++++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 0e6b9f9439a276..de4547f4782715 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,5 +1,4 @@ """Provide CORS support for the HTTP component.""" -import aiohttp_cors from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION @@ -22,6 +21,10 @@ @callback def setup_cors(app, origins): """Set up CORS.""" + # This import should remain here. That way the HTTP integration can always + # be imported by other integrations without it's requirements being installed. + import aiohttp_cors + cors = aiohttp_cors.setup( app, defaults={ diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 6b9c7c44ddf5ce..33f17b18a806f7 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -52,11 +52,6 @@ STATUS_UNREAD = "unread" STATUS_READ = "read" -WS_TYPE_GET_NOTIFICATIONS = "persistent_notification/get" -SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_GET_NOTIFICATIONS} -) - @bind_hass def create(hass, message, title=None, notification_id=None): @@ -198,14 +193,13 @@ def mark_read_service(call): DOMAIN, SERVICE_MARK_READ, mark_read_service, SCHEMA_SERVICE_MARK_READ ) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_NOTIFICATIONS, websocket_get_notifications, SCHEMA_WS_GET - ) + hass.components.websocket_api.async_register_command(websocket_get_notifications) return True @callback +@websocket_api.websocket_command({vol.Required("type"): "persistent_notification/get"}) def websocket_get_notifications( hass: HomeAssistant, connection: websocket_api.ActiveConnection, diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 4c93ce46135aef..a83f05820e2ea3 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -22,8 +22,6 @@ ) from .core import PROVIDERS from .hls import async_setup_hls -from .recorder import async_setup_recorder -from .worker import stream_worker try: import uvloop @@ -105,6 +103,9 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N async def async_setup(hass, config): """Set up stream.""" + # Keep import here so that we can import stream integration without installing reqs + from .recorder import async_setup_recorder + hass.data[DOMAIN] = {} hass.data[DOMAIN][ATTR_ENDPOINTS] = {} hass.data[DOMAIN][ATTR_STREAMS] = {} @@ -182,6 +183,9 @@ def check_idle(self): def start(self): """Start a stream.""" + # Keep import here so that we can import stream integration without installing reqs + from .worker import stream_worker + if self._thread is None or not self._thread.isAlive(): self._thread_quit = threading.Event() self._thread = threading.Thread( From 2a269fb9eb78f4c845d5778de09bdead9e9f0345 Mon Sep 17 00:00:00 2001 From: Tim McCormick Date: Sat, 19 Oct 2019 21:54:36 +0100 Subject: [PATCH 1063/3953] Update pysonos to 0.0.24 (#27937) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 6d636f36b3fb5f..7b0c041b2a90e1 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/integrations/sonos", "requirements": [ - "pysonos==0.0.23" + "pysonos==0.0.24" ], "dependencies": [], "ssdp": { diff --git a/requirements_all.txt b/requirements_all.txt index b4b60784a3dd6c..a929e90ac3a27e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1456,7 +1456,7 @@ pysnmp==4.4.11 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.23 +pysonos==0.0.24 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a4fdf4eac7d12..0c6c0ea0a442c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -497,7 +497,7 @@ pysmartthings==0.6.9 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.23 +pysonos==0.0.24 # homeassistant.components.spc pyspcwebgw==0.4.0 From 5c50fa34056ba2cd0c91c06cbb6a9c894b4be92e Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 19 Oct 2019 22:56:57 +0200 Subject: [PATCH 1064/3953] Bump pybotvac (#27933) --- homeassistant/components/neato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index a4d05e8849ac6f..03f8089159e6d8 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", "requirements": [ - "pybotvac==0.0.16" + "pybotvac==0.0.17" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a929e90ac3a27e..ad17b59b455224 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1105,7 +1105,7 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.16 +pybotvac==0.0.17 # homeassistant.components.nissan_leaf pycarwings2==2.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c6c0ea0a442c1..9b59a1d806b6fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -390,7 +390,7 @@ pyarlo==0.2.3 pyblackbird==0.5 # homeassistant.components.neato -pybotvac==0.0.16 +pybotvac==0.0.17 # homeassistant.components.cast pychromecast==4.0.1 From eeb1bfc6f55bd5877c3bd46f59d82f7e93d693be Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 19 Oct 2019 16:31:15 -0500 Subject: [PATCH 1065/3953] Central update for Plex platforms (#27764) * Update Plex platforms together * Remove unnecessary methods * Overhaul of Plex update logic * Apply suggestions from code review Use set instead of list Co-Authored-By: Martin Hjelmare * Review suggestions and cleanup * Fixes, remove sensor throttle * Guarantee entity name, use common scheme * Keep name stable once set --- homeassistant/components/plex/__init__.py | 23 +- homeassistant/components/plex/config_flow.py | 2 +- homeassistant/components/plex/const.py | 7 +- homeassistant/components/plex/media_player.py | 338 ++++++------------ homeassistant/components/plex/sensor.py | 63 ++-- homeassistant/components/plex/server.py | 81 ++++- 6 files changed, 250 insertions(+), 264 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index ed94b6913bcf57..b6ed3245115519 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,5 +1,6 @@ """Support to embed Plex.""" import asyncio +from datetime import timedelta import logging import plexapi.exceptions @@ -17,6 +18,7 @@ CONF_VERIFY_SSL, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_USE_EPISODE_ART, @@ -26,6 +28,7 @@ DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, + DISPATCHERS, DOMAIN as PLEX_DOMAIN, PLATFORMS, PLEX_MEDIA_PLAYER_OPTIONS, @@ -64,7 +67,9 @@ def setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}}) + hass.data.setdefault( + PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}, DISPATCHERS: {}} + ) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: @@ -104,7 +109,7 @@ async def async_setup_entry(hass, entry): ) hass.config_entries.async_update_entry(entry, options=options) - plex_server = PlexServer(server_config, entry.options) + plex_server = PlexServer(hass, server_config, entry.options) try: await hass.async_add_executor_job(plex_server.connect) except requests.exceptions.ConnectionError as error: @@ -129,7 +134,9 @@ async def async_setup_entry(hass, entry): _LOGGER.debug( "Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use ) - hass.data[PLEX_DOMAIN][SERVERS][plex_server.machine_identifier] = plex_server + server_id = plex_server.machine_identifier + hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id] = [] for platform in PLATFORMS: hass.async_create_task( @@ -138,6 +145,10 @@ async def async_setup_entry(hass, entry): entry.add_update_listener(async_options_updated) + hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = async_track_time_interval( + hass, lambda now: plex_server.update_platforms(), timedelta(seconds=10) + ) + return True @@ -146,7 +157,11 @@ async def async_unload_entry(hass, entry): server_id = entry.data[CONF_SERVER_IDENTIFIER] cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id) - await hass.async_add_executor_job(cancel) + cancel() + + dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id) + for unsub in dispatchers: + unsub() tasks = [ hass.config_entries.async_forward_entry_unload(entry, platform) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 9e74756977d4cf..a11fb9119a68cd 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -79,7 +79,7 @@ async def async_step_server_validate(self, server_config): errors = {} self.current_login = server_config - plex_server = PlexServer(server_config) + plex_server = PlexServer(self.hass, server_config) try: await self.hass.async_add_executor_job(plex_server.connect) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 0b436c4e208ce4..c576f1d6a592ea 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -2,12 +2,13 @@ from homeassistant.const import __version__ DOMAIN = "plex" -NAME_FORMAT = "Plex {}" +NAME_FORMAT = "Plex ({})" DEFAULT_PORT = 32400 DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True +DISPATCHERS = "dispatchers" PLATFORMS = ["media_player", "sensor"] REFRESH_LISTENERS = "refresh_listeners" SERVERS = "servers" @@ -16,6 +17,10 @@ PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" PLEX_SERVER_CONFIG = "server_config" +PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal" +PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" +PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal" + CONF_SERVER = "server" CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index a49e4c9c057ee1..4a48950a67ce8c 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -1,5 +1,4 @@ """Support to interface with the Plex API.""" -from datetime import timedelta import json import logging from xml.etree.ElementTree import ParseError @@ -29,14 +28,17 @@ STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.helpers.event import track_time_interval +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util from .const import ( CONF_SERVER_IDENTIFIER, + DISPATCHERS, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, - REFRESH_LISTENERS, + PLEX_NEW_MP_SIGNAL, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, SERVERS, ) @@ -53,142 +55,53 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex media_player from a config entry.""" - - def add_entities(entities, update_before_add=False): - """Sync version of async add entities.""" - hass.add_job(async_add_entities, entities, update_before_add) - - hass.async_add_executor_job(_setup_platform, hass, config_entry, add_entities) - - -def _setup_platform(hass, config_entry, add_entities_callback): - """Set up the Plex media_player platform.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] - plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] - plex_clients = {} - plex_sessions = {} - hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = track_time_interval( - hass, lambda now: update_devices(), timedelta(seconds=10) - ) - - def update_devices(): - """Update the devices objects.""" - try: - devices = plexserver.clients() - except plexapi.exceptions.BadRequest: - _LOGGER.exception("Error listing plex devices") - return - except requests.exceptions.RequestException as ex: - _LOGGER.warning( - "Could not connect to Plex server: %s (%s)", - plexserver.friendly_name, - ex, - ) - return - - new_plex_clients = [] - available_client_ids = [] - for device in devices: - # For now, let's allow all deviceClass types - if device.deviceClass in ["badClient"]: - continue - - available_client_ids.append(device.machineIdentifier) - if device.machineIdentifier not in plex_clients: - new_client = PlexClient( - plexserver, device, None, plex_sessions, update_devices - ) - plex_clients[device.machineIdentifier] = new_client - _LOGGER.debug("New device: %s", device.machineIdentifier) - new_plex_clients.append(new_client) - else: - _LOGGER.debug("Refreshing device: %s", device.machineIdentifier) - plex_clients[device.machineIdentifier].refresh(device, None) + def async_new_media_players(new_entities): + _async_add_entities( + hass, config_entry, async_add_entities, server_id, new_entities + ) - # add devices with a session and no client (ex. PlexConnect Apple TV's) - try: - sessions = plexserver.sessions() - except plexapi.exceptions.BadRequest: - _LOGGER.exception("Error listing plex sessions") - return - except requests.exceptions.RequestException as ex: - _LOGGER.warning( - "Could not connect to Plex server: %s (%s)", - plexserver.friendly_name, - ex, - ) - return + unsub = async_dispatcher_connect(hass, PLEX_NEW_MP_SIGNAL, async_new_media_players) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) - plex_sessions.clear() - for session in sessions: - for player in session.players: - plex_sessions[player.machineIdentifier] = session, player - - for machine_identifier, (session, player) in plex_sessions.items(): - if machine_identifier in available_client_ids: - # Avoid using session if already added as a device. - _LOGGER.debug("Skipping session, device exists: %s", machine_identifier) - continue - - if ( - machine_identifier not in plex_clients - and machine_identifier is not None - ): - new_client = PlexClient( - plexserver, player, session, plex_sessions, update_devices - ) - plex_clients[machine_identifier] = new_client - _LOGGER.debug("New session: %s", machine_identifier) - new_plex_clients.append(new_client) - else: - _LOGGER.debug("Refreshing session: %s", machine_identifier) - plex_clients[machine_identifier].refresh(None, session) - - for client in plex_clients.values(): - # force devices to idle that do not have a valid session - if client.session is None: - client.force_idle() - - client.set_availability( - client.machine_identifier in available_client_ids - or client.machine_identifier in plex_sessions - ) - if client not in new_plex_clients: - client.schedule_update_ha_state() +@callback +def _async_add_entities( + hass, config_entry, async_add_entities, server_id, new_entities +): + """Set up Plex media_player entities.""" + entities = [] + plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] + for entity_params in new_entities: + plex_mp = PlexMediaPlayer(plexserver, **entity_params) + entities.append(plex_mp) - if new_plex_clients: - add_entities_callback(new_plex_clients) + async_add_entities(entities, True) -class PlexClient(MediaPlayerDevice): +class PlexMediaPlayer(MediaPlayerDevice): """Representation of a Plex device.""" - def __init__(self, plex_server, device, session, plex_sessions, update_devices): + def __init__(self, plex_server, device, session=None): """Initialize the Plex device.""" + self.plex_server = plex_server + self.device = device + self.session = session self._app_name = "" - self._device = None self._available = False - self._marked_unavailable = None self._device_protocol_capabilities = None self._is_player_active = False - self._is_player_available = False - self._player = None - self._machine_identifier = None + self._machine_identifier = device.machineIdentifier self._make = "" self._name = None self._player_state = "idle" self._previous_volume_level = 1 # Used in fake muting - self._session = None self._session_type = None self._session_username = None self._state = STATE_IDLE self._volume_level = 1 # since we can't retrieve remotely self._volume_muted = False # since we can't retrieve remotely - self.plex_server = plex_server - self.plex_sessions = plex_sessions - self.update_devices = update_devices # General self._media_content_id = None self._media_content_rating = None @@ -208,7 +121,22 @@ def __init__(self, plex_server, device, session, plex_sessions, update_devices): self._media_season = None self._media_series_title = None - self.refresh(device, session) + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + server_id = self.plex_server.machine_identifier + unsub = async_dispatcher_connect( + self.hass, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id), + self.async_refresh_media_player, + ) + self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + @callback + def async_refresh_media_player(self, device, session): + """Set instance objects and trigger an entity state update.""" + self.device = device + self.session = session + self.async_schedule_update_ha_state(True) def _clear_media_details(self): """Set all Media Items to None.""" @@ -232,52 +160,46 @@ def _clear_media_details(self): # Clear library Name self._app_name = "" - def refresh(self, device, session): + def update(self): """Refresh key device data.""" self._clear_media_details() - if session: # Not being triggered by Chrome or FireTablet Plex App - self._session = session - if device: - self._device = device + self._available = self.device or self.session + name_base = None + + if self.device: try: - device_url = self._device.url("/") + device_url = self.device.url("/") except plexapi.exceptions.BadRequest: device_url = "127.0.0.1" if "127.0.0.1" in device_url: - self._device.proxyThroughServer() - self._session = None - self._machine_identifier = self._device.machineIdentifier - self._name = NAME_FORMAT.format(self._device.title or DEVICE_DEFAULT_NAME) - self._device_protocol_capabilities = self._device.protocolCapabilities - - # set valid session, preferring device session - if self._device.machineIdentifier in self.plex_sessions: - self._session = self.plex_sessions.get( - self._device.machineIdentifier, [None, None] - )[0] - - if self._session: - if ( - self._device is not None - and self._device.machineIdentifier is not None - and self._session.players - ): - self._is_player_available = True - self._player = [ + self.device.proxyThroughServer() + name_base = self.device.title or self.device.product + self._device_protocol_capabilities = self.device.protocolCapabilities + self._player_state = self.device.state + + if not self.session: + self.force_idle() + else: + session_device = next( + ( p - for p in self._session.players - if p.machineIdentifier == self._device.machineIdentifier - ][0] - self._name = NAME_FORMAT.format(self._player.title) - self._player_state = self._player.state - self._session_username = self._session.usernames[0] - self._make = self._player.device + for p in self.session.players + if p.machineIdentifier == self.device.machineIdentifier + ), + None, + ) + if session_device: + self._make = session_device.device or "" + self._player_state = session_device.state + name_base = name_base or session_device.title or session_device.product else: - self._is_player_available = False + _LOGGER.warning("No player associated with active session") + + self._session_username = self.session.usernames[0] # Calculate throttled position for proper progress display. - position = int(self._session.viewOffset / 1000) + position = int(self.session.viewOffset / 1000) now = dt_util.utcnow() if self._media_position is not None: pos_diff = position - self._media_position @@ -289,21 +211,22 @@ def refresh(self, device, session): self._media_position_updated_at = now self._media_position = position - self._media_content_id = self._session.ratingKey - self._media_content_rating = getattr(self._session, "contentRating", None) + self._media_content_id = self.session.ratingKey + self._media_content_rating = getattr(self.session, "contentRating", None) + self._name = self._name or NAME_FORMAT.format(name_base or DEVICE_DEFAULT_NAME) self._set_player_state() - if self._is_player_active and self._session is not None: - self._session_type = self._session.type - self._media_duration = int(self._session.duration / 1000) + if self._is_player_active and self.session is not None: + self._session_type = self.session.type + self._media_duration = int(self.session.duration / 1000) # title (movie name, tv episode name, music song name) - self._media_title = self._session.title + self._media_title = self.session.title # media type self._set_media_type() self._app_name = ( - self._session.section().title - if self._session.section() is not None + self.session.section().title + if self.session.section() is not None else "" ) self._set_media_image() @@ -311,33 +234,21 @@ def refresh(self, device, session): self._session_type = None def _set_media_image(self): - thumb_url = self._session.thumbUrl + thumb_url = self.session.thumbUrl if ( self.media_content_type is MEDIA_TYPE_TVSHOW and not self.plex_server.use_episode_art ): - thumb_url = self._session.url(self._session.grandparentThumb) + thumb_url = self.session.url(self.session.grandparentThumb) if thumb_url is None: _LOGGER.debug( - "Using media art because media thumb " "was not found: %s", - self.entity_id, + "Using media art because media thumb was not found: %s", self.name ) - thumb_url = self.session.url(self._session.art) + thumb_url = self.session.url(self.session.art) self._media_image_url = thumb_url - def set_availability(self, available): - """Set the device as available/unavailable noting time.""" - if not available: - self._clear_media_details() - if self._marked_unavailable is None: - self._marked_unavailable = dt_util.utcnow() - else: - self._marked_unavailable = None - - self._available = available - def _set_player_state(self): if self._player_state == "playing": self._is_player_active = True @@ -357,41 +268,41 @@ def _set_media_type(self): self._media_content_type = MEDIA_TYPE_TVSHOW # season number (00) - if callable(self._session.season): - self._media_season = str((self._session.season()).index).zfill(2) - elif self._session.parentIndex is not None: - self._media_season = self._session.parentIndex.zfill(2) + if callable(self.session.season): + self._media_season = str((self.session.season()).index).zfill(2) + elif self.session.parentIndex is not None: + self._media_season = self.session.parentIndex.zfill(2) else: self._media_season = None # show name - self._media_series_title = self._session.grandparentTitle + self._media_series_title = self.session.grandparentTitle # episode number (00) - if self._session.index is not None: - self._media_episode = str(self._session.index).zfill(2) + if self.session.index is not None: + self._media_episode = str(self.session.index).zfill(2) elif self._session_type == "movie": self._media_content_type = MEDIA_TYPE_MOVIE - if self._session.year is not None and self._media_title is not None: - self._media_title += " (" + str(self._session.year) + ")" + if self.session.year is not None and self._media_title is not None: + self._media_title += " (" + str(self.session.year) + ")" elif self._session_type == "track": self._media_content_type = MEDIA_TYPE_MUSIC - self._media_album_name = self._session.parentTitle - self._media_album_artist = self._session.grandparentTitle - self._media_track = self._session.index - self._media_artist = self._session.originalTitle + self._media_album_name = self.session.parentTitle + self._media_album_artist = self.session.grandparentTitle + self._media_track = self.session.index + self._media_artist = self.session.originalTitle # use album artist if track artist is missing if self._media_artist is None: _LOGGER.debug( - "Using album artist because track artist " "was not found: %s", - self.entity_id, + "Using album artist because track artist was not found: %s", + self.name, ) self._media_artist = self._media_album_artist def force_idle(self): """Force client to idle.""" self._state = STATE_IDLE - self._session = None + self.session = None self._clear_media_details() @property @@ -402,7 +313,7 @@ def should_poll(self): @property def unique_id(self): """Return the id of this plex client.""" - return self.machine_identifier + return self._machine_identifier @property def available(self): @@ -414,31 +325,11 @@ def name(self): """Return the name of the device.""" return self._name - @property - def machine_identifier(self): - """Return the machine identifier of the device.""" - return self._machine_identifier - @property def app_name(self): """Return the library name of playing media.""" return self._app_name - @property - def device(self): - """Return the device, if any.""" - return self._device - - @property - def marked_unavailable(self): - """Return time device was marked unavailable.""" - return self._marked_unavailable - - @property - def session(self): - """Return the session, if any.""" - return self._session - @property def state(self): """Return the state of the device.""" @@ -462,8 +353,7 @@ def media_content_type(self): """Return the content type of current playing media.""" if self._session_type == "clip": _LOGGER.debug( - "Clip content type detected, " "compatibility may vary: %s", - self.entity_id, + "Clip content type detected, compatibility may vary: %s", self.name ) return MEDIA_TYPE_TVSHOW if self._session_type == "episode": @@ -560,8 +450,8 @@ def supported_features(self): # no mute support if self.make.lower() == "shield android tv": _LOGGER.debug( - "Shield Android TV client detected, disabling mute " "controls: %s", - self.entity_id, + "Shield Android TV client detected, disabling mute controls: %s", + self.name, ) return ( SUPPORT_PAUSE @@ -579,7 +469,7 @@ def supported_features(self): _LOGGER.debug( "Tivo client detected, only enabling pause, play, " "stop, and off controls: %s", - self.entity_id, + self.name, ) return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF @@ -603,7 +493,7 @@ def set_volume_level(self, volume): if self.device and "playback" in self._device_protocol_capabilities: self.device.setVolume(int(volume * 100), self._active_media_plexapi_type) self._volume_level = volume # store since we can't retrieve - self.update_devices() + self.plex_server.update_platforms() @property def volume_level(self): @@ -642,19 +532,19 @@ def media_play(self): """Send play command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.play(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def media_pause(self): """Send pause command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.pause(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def media_stop(self): """Send stop command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.stop(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def turn_off(self): """Turn the client off.""" @@ -665,13 +555,13 @@ def media_next_track(self): """Send next track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipNext(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def media_previous_track(self): """Send previous track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipPrevious(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" @@ -706,7 +596,7 @@ def play_media(self, media_type, media_id, **kwargs): except requests.exceptions.ConnectTimeout: _LOGGER.error("Timed out playing on %s", self.name) - self.update_devices() + self.plex_server.update_platforms() def _get_music_media(self, library_name, src): """Find music media and return a Plex media object.""" diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 7d5b54356a0c82..3cde2adb8f4b78 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,19 +1,21 @@ """Support for Plex media server monitoring.""" -from datetime import timedelta import logging -import plexapi.exceptions -import requests.exceptions - +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from .const import CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, SERVERS +from .const import ( + CONF_SERVER_IDENTIFIER, + DISPATCHERS, + DOMAIN as PLEX_DOMAIN, + NAME_FORMAT, + PLEX_UPDATE_SENSOR_SIGNAL, + SERVERS, +) _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Plex sensor platform. @@ -26,8 +28,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex sensor from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] - sensor = PlexSensor(hass.data[PLEX_DOMAIN][SERVERS][server_id]) - async_add_entities([sensor], True) + plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] + sensor = PlexSensor(plexserver) + async_add_entities([sensor]) class PlexSensor(Entity): @@ -35,12 +38,27 @@ class PlexSensor(Entity): def __init__(self, plex_server): """Initialize the sensor.""" + self.sessions = [] self._state = None self._now_playing = [] self._server = plex_server - self._name = f"Plex ({plex_server.friendly_name})" + self._name = NAME_FORMAT.format(plex_server.friendly_name) self._unique_id = f"sensor-{plex_server.machine_identifier}" + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + server_id = self._server.machine_identifier + unsub = async_dispatcher_connect( + self.hass, PLEX_UPDATE_SENSOR_SIGNAL, self.async_refresh_sensor + ) + self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + @callback + def async_refresh_sensor(self, sessions): + """Set instance object and trigger an entity state update.""" + self.sessions = sessions + self.async_schedule_update_ha_state(True) + @property def name(self): """Return the name of the sensor.""" @@ -51,6 +69,11 @@ def unique_id(self): """Return the id of this plex client.""" return self._unique_id + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + @property def state(self): """Return the state of the sensor.""" @@ -66,24 +89,10 @@ def device_state_attributes(self): """Return the state attributes.""" return {content[0]: content[1] for content in self._now_playing} - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update method for Plex sensor.""" - try: - sessions = self._server.sessions() - except plexapi.exceptions.BadRequest: - _LOGGER.error( - "Error listing current Plex sessions on %s", self._server.friendly_name - ) - return - except requests.exceptions.RequestException as ex: - _LOGGER.warning( - "Temporary error connecting to %s (%s)", self._server.friendly_name, ex - ) - return - now_playing = [] - for sess in sessions: + for sess in self.sessions: user = sess.usernames[0] device = sess.players[0].title now_playing_user = f"{user} - {device}" @@ -120,5 +129,5 @@ def update(self): now_playing_title += f" ({sess.year})" now_playing.append((now_playing_user, now_playing_title)) - self._state = len(sessions) + self._state = len(self.sessions) self._now_playing = now_playing diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d9ddc28c89a851..128bcdd45c646e 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,17 +1,24 @@ """Shared class to maintain Plex server instances.""" +import logging + import plexapi.myplex import plexapi.playqueue import plexapi.server from requests import Session +import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL +from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( CONF_SERVER, CONF_SHOW_ALL_CONTROLS, CONF_USE_EPISODE_ART, DEFAULT_VERIFY_SSL, + PLEX_NEW_MP_SIGNAL, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, + PLEX_UPDATE_SENSOR_SIGNAL, X_PLEX_DEVICE_NAME, X_PLEX_PLATFORM, X_PLEX_PRODUCT, @@ -19,6 +26,8 @@ ) from .errors import NoServersFound, ServerNotSpecified +_LOGGER = logging.getLogger(__name__) + # Set default headers sent by plexapi plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM @@ -31,9 +40,11 @@ class PlexServer: """Manages a single Plex server connection.""" - def __init__(self, server_config, options=None): + def __init__(self, hass, server_config, options=None): """Initialize a Plex server instance.""" + self._hass = hass self._plex_server = None + self._known_clients = set() self._url = server_config.get(CONF_URL) self._token = server_config.get(CONF_TOKEN) self._server_name = server_config.get(CONF_SERVER) @@ -76,13 +87,69 @@ def _connect_with_url(): else: _connect_with_token() - def clients(self): - """Pass through clients call to plexapi.""" - return self._plex_server.clients() + def refresh_entity(self, machine_identifier, device, session): + """Forward refresh dispatch to media_player.""" + dispatcher_send( + self._hass, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(machine_identifier), + device, + session, + ) + + def update_platforms(self): + """Update the platform entities.""" + available_clients = {} + new_clients = set() + + try: + devices = self._plex_server.clients() + sessions = self._plex_server.sessions() + except plexapi.exceptions.BadRequest: + _LOGGER.exception("Error requesting Plex client data from server") + return + except requests.exceptions.RequestException as ex: + _LOGGER.warning( + "Could not connect to Plex server: %s (%s)", self.friendly_name, ex + ) + return + + for device in devices: + available_clients[device.machineIdentifier] = {"device": device} + + if device.machineIdentifier not in self._known_clients: + new_clients.add(device.machineIdentifier) + _LOGGER.debug("New device: %s", device.machineIdentifier) + + for session in sessions: + for player in session.players: + available_clients.setdefault( + player.machineIdentifier, {"device": player} + ) + available_clients[player.machineIdentifier]["session"] = session + + if player.machineIdentifier not in self._known_clients: + new_clients.add(player.machineIdentifier) + _LOGGER.debug("New session: %s", player.machineIdentifier) + + new_entity_configs = [] + for client_id, client_data in available_clients.items(): + if client_id in new_clients: + new_entity_configs.append(client_data) + else: + self.refresh_entity( + client_id, client_data["device"], client_data.get("session") + ) + + self._known_clients.update(new_clients) + + idle_clients = self._known_clients.difference(available_clients) + for client_id in idle_clients: + self.refresh_entity(client_id, None, None) + + if new_entity_configs: + dispatcher_send(self._hass, PLEX_NEW_MP_SIGNAL, new_entity_configs) - def sessions(self): - """Pass through sessions call to plexapi.""" - return self._plex_server.sessions() + dispatcher_send(self._hass, PLEX_UPDATE_SENSOR_SIGNAL, sessions) @property def friendly_name(self): From bfba46d64a6778d6f98bd8b0c44e92f556088b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sat, 19 Oct 2019 23:52:42 +0200 Subject: [PATCH 1066/3953] move imports in sonos component (#27938) --- homeassistant/components/sonos/__init__.py | 4 ++-- homeassistant/components/sonos/config_flow.py | 7 ++++--- homeassistant/components/sonos/media_player.py | 15 +++++++-------- tests/components/sonos/conftest.py | 2 +- tests/components/sonos/test_init.py | 2 +- tests/components/sonos/test_media_player.py | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index bd16cfe353a02a..d2c6210f01cfba 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -1,16 +1,16 @@ """Support to embed Sonos.""" import asyncio + import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_HOSTS, ATTR_ENTITY_ID, ATTR_TIME +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN - CONF_ADVERTISE_ADDR = "advertise_addr" CONF_INTERFACE_ADDR = "interface_addr" diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 3ce62f54a2f6a7..42ac32163a4b98 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -1,13 +1,14 @@ """Config flow for SONOS.""" -from homeassistant.helpers import config_entry_flow +import pysonos + from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + from .const import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - import pysonos - return await hass.async_add_executor_job(pysonos.discover) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 41472413a077b6..94d252e9fee3f7 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -8,8 +8,8 @@ import async_timeout import pysonos +from pysonos.exceptions import SoCoException, SoCoUPnPException import pysonos.snapshot -from pysonos.exceptions import SoCoUPnPException, SoCoException from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -40,11 +40,6 @@ from homeassistant.util.dt import utcnow from . import ( - CONF_ADVERTISE_ADDR, - CONF_HOSTS, - CONF_INTERFACE_ADDR, - DATA_SERVICE_EVENT, - DOMAIN as SONOS_DOMAIN, ATTR_ALARM_ID, ATTR_ENABLED, ATTR_INCLUDE_LINKED_ZONES, @@ -56,6 +51,11 @@ ATTR_TIME, ATTR_VOLUME, ATTR_WITH_GROUP, + CONF_ADVERTISE_ADDR, + CONF_HOSTS, + CONF_INTERFACE_ADDR, + DATA_SERVICE_EVENT, + DOMAIN as SONOS_DOMAIN, SERVICE_CLEAR_TIMER, SERVICE_JOIN, SERVICE_PLAY_QUEUE, @@ -1161,10 +1161,9 @@ def clear_sleep_timer(self, data): @soco_coordinator def set_alarm(self, data): """Set the alarm clock on the player.""" - from pysonos import alarms alarm = None - for one_alarm in alarms.get_alarms(self.soco): + for one_alarm in pysonos.alarms.get_alarms(self.soco): # pylint: disable=protected-access if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]): alarm = one_alarm diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 135b7279244473..e0257585ad5a39 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -2,8 +2,8 @@ from asynctest.mock import Mock, patch as patch import pytest -from homeassistant.components.sonos import DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS from tests.common import MockConfigEntry diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index b9ceaa49639c4d..86ec90f32b85a2 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow -from homeassistant.setup import async_setup_component from homeassistant.components import sonos +from homeassistant.setup import async_setup_component from tests.common import mock_coro diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index ec5861b536ad27..d21d3f017927e1 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,5 +1,5 @@ """Tests for the Sonos Media Player platform.""" -from homeassistant.components.sonos import media_player, DOMAIN +from homeassistant.components.sonos import DOMAIN, media_player from homeassistant.setup import async_setup_component From 2c00ff7e522ccb16a023816c313f4747d718810d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 20 Oct 2019 00:32:18 +0000 Subject: [PATCH 1067/3953] [ci skip] Translation update --- .../components/abode/.translations/pl.json | 14 ++++++++++++-- .../components/adguard/.translations/en.json | 2 +- .../alarm_control_panel/.translations/ca.json | 11 +++++++++++ .../alarm_control_panel/.translations/pl.json | 11 +++++++++++ .../components/axis/.translations/ca.json | 1 + .../components/axis/.translations/pl.json | 1 + .../components/cover/.translations/pl.json | 10 ++++++++++ .../components/deconz/.translations/ca.json | 1 + .../components/deconz/.translations/pl.json | 1 + .../components/ifttt/.translations/ca.json | 2 +- .../components/linky/.translations/pl.json | 2 +- .../components/lock/.translations/ca.json | 5 +++++ .../components/lock/.translations/pl.json | 13 +++++++++++++ .../components/mailgun/.translations/ca.json | 2 +- .../components/opentherm_gw/.translations/ca.json | 11 +++++++++++ .../components/opentherm_gw/.translations/pl.json | 3 ++- .../components/soma/.translations/ca.json | 10 ++++++++++ .../components/soma/.translations/pl.json | 5 ++++- .../components/twilio/.translations/ca.json | 2 +- 19 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/alarm_control_panel/.translations/ca.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/pl.json create mode 100644 homeassistant/components/cover/.translations/pl.json create mode 100644 homeassistant/components/lock/.translations/pl.json diff --git a/homeassistant/components/abode/.translations/pl.json b/homeassistant/components/abode/.translations/pl.json index 09fbdc9324106d..c3f3b8f2c88b84 100644 --- a/homeassistant/components/abode/.translations/pl.json +++ b/homeassistant/components/abode/.translations/pl.json @@ -1,12 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja Abode." + }, + "error": { + "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z Abode.", + "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" + }, "step": { "user": { "data": { "password": "Has\u0142o", "username": "Adres e-mail" - } + }, + "title": "Wprowad\u017a informacje logowania Abode" } - } + }, + "title": "Abode" } } \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/en.json b/homeassistant/components/adguard/.translations/en.json index 8bfb8516fd8f7d..00d048c3343e0b 100644 --- a/homeassistant/components/adguard/.translations/en.json +++ b/homeassistant/components/adguard/.translations/en.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.", + "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", "existing_instance_updated": "Updated existing configuration.", "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." }, diff --git a/homeassistant/components/alarm_control_panel/.translations/ca.json b/homeassistant/components/alarm_control_panel/.translations/ca.json new file mode 100644 index 00000000000000..8d95d5f648593e --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/ca.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Activa {entity_name} fora", + "arm_home": "Activa {entity_name} a casa", + "arm_night": "Activa {entity_name} nocturn", + "disarm": "Desactiva {entity_name}", + "trigger": "Dispara {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pl.json b/homeassistant/components/alarm_control_panel/.translations/pl.json new file mode 100644 index 00000000000000..a5dc326c267353 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/pl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "uzbr\u00f3j (poza domem) {entity_name}", + "arm_home": "uzbr\u00f3j (w domu) {entity_name}", + "arm_night": "uzbr\u00f3j (noc) {entity_name}", + "disarm": "rozbr\u00f3j {entity_name}", + "trigger": "wyzw\u00f3l {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/ca.json b/homeassistant/components/axis/.translations/ca.json index 75dd89ef9c15b0..3458dcc4529dc5 100644 --- a/homeassistant/components/axis/.translations/ca.json +++ b/homeassistant/components/axis/.translations/ca.json @@ -12,6 +12,7 @@ "device_unavailable": "El dispositiu no est\u00e0 disponible", "faulty_credentials": "Credencials d'usuari incorrectes" }, + "flow_title": "Dispositiu d'eix: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/pl.json b/homeassistant/components/axis/.translations/pl.json index 88e803605363de..4ca87310f48a3b 100644 --- a/homeassistant/components/axis/.translations/pl.json +++ b/homeassistant/components/axis/.translations/pl.json @@ -12,6 +12,7 @@ "device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne", "faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce" }, + "flow_title": "Urz\u0105dzenie Axis: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/cover/.translations/pl.json b/homeassistant/components/cover/.translations/pl.json new file mode 100644 index 00000000000000..4adc0c17b54288 --- /dev/null +++ b/homeassistant/components/cover/.translations/pl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "pokrywa {entity_name} jest zamkni\u0119ta", + "is_closing": "{entity_name} si\u0119 zamyka", + "is_open": "pokrywa {entity_name} jest otwarta", + "is_opening": "{entity_name} si\u0119 otwiera" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index d36de4acc1e96c..a2facf0d7c2e3a 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -11,6 +11,7 @@ "error": { "no_key": "No s'ha pogut obtenir una clau API" }, + "flow_title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 6d36d6ab39dea2..ac9f06f1f17553 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -11,6 +11,7 @@ "error": { "no_key": "Nie mo\u017cna uzyska\u0107 klucza API" }, + "flow_title": "Bramka deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/ifttt/.translations/ca.json b/homeassistant/components/ifttt/.translations/ca.json index 597328a2ee4003..979ed3cd71f603 100644 --- a/homeassistant/components/ifttt/.translations/ca.json +++ b/homeassistant/components/ifttt/.translations/ca.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, necessitar\u00e0s utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- Method: POST \n- Content Type: application/json \n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, necessitar\u00e0s utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- Method: POST \n- Content Type: application/json \n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants." }, "step": { "user": { diff --git a/homeassistant/components/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json index a4f68fa8687f0a..d4fa7ee4d11811 100644 --- a/homeassistant/components/linky/.translations/pl.json +++ b/homeassistant/components/linky/.translations/pl.json @@ -14,7 +14,7 @@ "user": { "data": { "password": "Has\u0142o", - "username": "E-mail" + "username": "Adres e-mail" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", "title": "Linky" diff --git a/homeassistant/components/lock/.translations/ca.json b/homeassistant/components/lock/.translations/ca.json index 0e05d512bf4993..53198a2157381d 100644 --- a/homeassistant/components/lock/.translations/ca.json +++ b/homeassistant/components/lock/.translations/ca.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Bloqueja {entity_name}", + "open": "Obre {entity_name}", + "unlock": "Desbloqueja {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} est\u00e0 bloquejat/ada", "is_unlocked": "{entity_name} est\u00e0 desbloquejat/ada" diff --git a/homeassistant/components/lock/.translations/pl.json b/homeassistant/components/lock/.translations/pl.json new file mode 100644 index 00000000000000..a3fe7358398c58 --- /dev/null +++ b/homeassistant/components/lock/.translations/pl.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "zablokuj {entity_name}", + "open": "otw\u00f3rz {entity_name}", + "unlock": "odblokuj {entity_name}" + }, + "condition_type": { + "is_locked": "zamek {entity_name} jest zamkni\u0119ty", + "is_unlocked": "zamek {entity_name} jest otwarty" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ca.json b/homeassistant/components/mailgun/.translations/ca.json index f43467de7d9da7..6bcb737588a426 100644 --- a/homeassistant/components/mailgun/.translations/ca.json +++ b/homeassistant/components/mailgun/.translations/ca.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants." }, "step": { "user": { diff --git a/homeassistant/components/opentherm_gw/.translations/ca.json b/homeassistant/components/opentherm_gw/.translations/ca.json index 0224d663a83953..07567149063c60 100644 --- a/homeassistant/components/opentherm_gw/.translations/ca.json +++ b/homeassistant/components/opentherm_gw/.translations/ca.json @@ -19,5 +19,16 @@ } }, "title": "Passarel\u00b7la d'OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura de la planta", + "precision": "Precisi\u00f3" + }, + "description": "Opcions del la passarel\u00b7la d'enlla\u00e7 d\u2019OpenTherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index 3d4c643b848b6c..e4403420b11af8 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -26,7 +26,8 @@ "data": { "floor_temperature": "Temperatura pod\u0142ogi", "precision": "Precyzja" - } + }, + "description": "Opcje dla bramki OpenTherm" } } } diff --git a/homeassistant/components/soma/.translations/ca.json b/homeassistant/components/soma/.translations/ca.json index 6bd4737d6fc802..18b33d1bc9b152 100644 --- a/homeassistant/components/soma/.translations/ca.json +++ b/homeassistant/components/soma/.translations/ca.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Soma." }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "description": "Introdueix la informaci\u00f3 de connexi\u00f3 de SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json index dc843f29fd5045..4d783f3f0a05c4 100644 --- a/homeassistant/components/soma/.translations/pl.json +++ b/homeassistant/components/soma/.translations/pl.json @@ -11,8 +11,11 @@ "step": { "user": { "data": { + "host": "Host", "port": "Port" - } + }, + "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia SOMA Connect.", + "title": "SOMA Connect" } }, "title": "Soma" diff --git a/homeassistant/components/twilio/.translations/ca.json b/homeassistant/components/twilio/.translations/ca.json index 324ab0dd69aa56..bad78e51a36c41 100644 --- a/homeassistant/components/twilio/.translations/ca.json +++ b/homeassistant/components/twilio/.translations/ca.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Twilio]({twilio_url}).\n\nCompleta la seg\u00fcent informaci\u00f3 : \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Twilio]({twilio_url}).\n\nCompleta la seg\u00fcent informaci\u00f3 : \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants." }, "step": { "user": { From 2706e3289dfaf6b0f1c56f35a51e430b20f90bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 10:05:11 +0200 Subject: [PATCH 1068/3953] Move imports in smappee component (#27943) * move imports in smappee component * fix: unneeded object inheritance --- homeassistant/components/smappee/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 0da0b29fbc2d1c..ecab09f6ff912e 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -1,13 +1,16 @@ """Support for Smappee energy monitor.""" -import logging from datetime import datetime, timedelta +import logging import re -import voluptuous as vol + from requests.exceptions import RequestException -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_HOST -from homeassistant.util import Throttle -from homeassistant.helpers.discovery import load_platform +import smappy +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -72,7 +75,6 @@ def __init__( self, client_id, client_secret, username, password, host, host_password ): """Initialize the data.""" - import smappy self._remote_active = False self._local_active = False From a5ec5b567e45a3ad6c204a98277398022089b275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 10:05:37 +0200 Subject: [PATCH 1069/3953] move imports in snapcast component (#27940) --- homeassistant/components/snapcast/__init__.py | 1 + homeassistant/components/snapcast/media_player.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/snapcast/__init__.py b/homeassistant/components/snapcast/__init__.py index 9e41bd8ff385d3..e6c574b7b2bd48 100644 --- a/homeassistant/components/snapcast/__init__.py +++ b/homeassistant/components/snapcast/__init__.py @@ -1,6 +1,7 @@ """The snapcast component.""" import asyncio + import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 81cd6538578c0b..c3c9138eb89ca3 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -2,9 +2,11 @@ import logging import socket +import snapcast.control +from snapcast.control.server import CONTROL_PORT import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, @@ -24,12 +26,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( + ATTR_MASTER, DOMAIN, - SERVICE_SNAPSHOT, - SERVICE_RESTORE, SERVICE_JOIN, + SERVICE_RESTORE, + SERVICE_SNAPSHOT, SERVICE_UNJOIN, - ATTR_MASTER, ) _LOGGER = logging.getLogger(__name__) @@ -55,8 +57,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Snapcast platform.""" - import snapcast.control - from snapcast.control.server import CONTROL_PORT host = config.get(CONF_HOST) port = config.get(CONF_PORT, CONTROL_PORT) From e01562ceea357b7fbf568a57adca9e099d9e8fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 10:06:32 +0200 Subject: [PATCH 1070/3953] Move imports in snmp component (#27939) * move imports in snmp component * fix: move hlapi import top level --- .../components/snmp/device_tracker.py | 4 ++-- homeassistant/components/snmp/sensor.py | 23 +++++++++---------- homeassistant/components/snmp/switch.py | 22 ++++++++++-------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index a628c426e0f823..eafae9537e5eeb 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -2,6 +2,8 @@ import binascii import logging +from pysnmp.entity import config as cfg +from pysnmp.entity.rfc3413.oneliner import cmdgen import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -45,8 +47,6 @@ class SnmpScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from pysnmp.entity.rfc3413.oneliner import cmdgen - from pysnmp.entity import config as cfg self.snmp = cmdgen.CommandGenerator() diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 5e6b5ed1f28088..b369ec83c58034 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -2,6 +2,17 @@ from datetime import timedelta import logging +import pysnmp.hlapi.asyncio as hlapi +from pysnmp.hlapi.asyncio import ( + CommunityData, + ContextData, + ObjectIdentity, + ObjectType, + SnmpEngine, + UdpTransportTarget, + UsmUserData, + getCmd, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -70,16 +81,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SNMP sensor.""" - from pysnmp.hlapi.asyncio import ( - getCmd, - CommunityData, - SnmpEngine, - UdpTransportTarget, - ContextData, - ObjectType, - ObjectIdentity, - UsmUserData, - ) name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -101,7 +102,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= value_template.hass = hass if version == "3": - import pysnmp.hlapi.asyncio as hlapi if not authkey: authproto = "none" @@ -194,7 +194,6 @@ def __init__(self, request_args, baseoid, accept_errors, default_value): async def async_update(self): """Get the latest data from the remote SNMP capable host.""" - from pysnmp.hlapi.asyncio import getCmd, ObjectType, ObjectIdentity errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 204e98cca831e9..aac43208a1f58b 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -1,6 +1,18 @@ """Support for SNMP enabled switch.""" import logging +import pysnmp.hlapi.asyncio as hlapi +from pysnmp.hlapi.asyncio import ( + CommunityData, + ContextData, + ObjectIdentity, + ObjectType, + SnmpEngine, + UdpTransportTarget, + UsmUserData, + getCmd, + setCmd, +) import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -136,13 +148,6 @@ def __init__( command_payload_off, ): """Initialize the switch.""" - from pysnmp.hlapi.asyncio import ( - CommunityData, - ContextData, - SnmpEngine, - UdpTransportTarget, - UsmUserData, - ) self._name = name self._baseoid = baseoid @@ -157,7 +162,6 @@ def __init__( self._payload_off = payload_off if version == "3": - import pysnmp.hlapi.asyncio as hlapi if not authkey: authproto = "none" @@ -194,7 +198,6 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Update the state.""" - from pysnmp.hlapi.asyncio import getCmd, ObjectType, ObjectIdentity errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) @@ -228,7 +231,6 @@ def is_on(self): return self._state async def _set(self, value): - from pysnmp.hlapi.asyncio import setCmd, ObjectType, ObjectIdentity await setCmd( *self._request_args, ObjectType(ObjectIdentity(self._commandoid), value) From 9571f869d138926d2272b5cb968f138e61d92d42 Mon Sep 17 00:00:00 2001 From: Jacob Mansfield Date: Sun, 20 Oct 2019 09:07:34 +0100 Subject: [PATCH 1071/3953] Fix whois error, check expiration_date for list and pick first (#27930) --- homeassistant/components/whois/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 09cf40f193fe87..3c78d80ba92442 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -119,7 +119,10 @@ def update(self): attrs = {} expiration_date = response["expiration_date"] - attrs[ATTR_EXPIRES] = expiration_date.isoformat() + if isinstance(expiration_date, list): + attrs[ATTR_EXPIRES] = expiration_date[0].isoformat() + else: + attrs[ATTR_EXPIRES] = expiration_date.isoformat() if "nameservers" in response: attrs[ATTR_NAME_SERVERS] = " ".join(response["nameservers"]) From ed46834a30fcff447ad674ad5cd584e8e76d7308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 10:10:27 +0200 Subject: [PATCH 1072/3953] Move imports in sql component (#27713) * move imports in sql component * fix: variable redeclaration * fix: close test db session on platform setup --- homeassistant/components/sql/sensor.py | 18 +++++++++--------- tests/components/sql/test_sensor.py | 3 ++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 3b32f2747f1940..52899c7da803aa 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -1,13 +1,15 @@ """Sensor from an SQL Query.""" -import decimal import datetime +import decimal import logging +import sqlalchemy +from sqlalchemy.orm import scoped_session, sessionmaker import voluptuous as vol +from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_DB_FILE, DEFAULT_URL from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE -from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_URL, DEFAULT_DB_FILE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -46,20 +48,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if not db_url: db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) - import sqlalchemy - from sqlalchemy.orm import sessionmaker, scoped_session - try: engine = sqlalchemy.create_engine(db_url) - sessionmaker = scoped_session(sessionmaker(bind=engine)) + sessmaker = scoped_session(sessionmaker(bind=engine)) # Run a dummy query just to test the db_url - sess = sessionmaker() + sess = sessmaker() sess.execute("SELECT 1;") except sqlalchemy.exc.SQLAlchemyError as err: _LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err) return + finally: + sess.close() queries = [] @@ -74,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): value_template.hass = hass sensor = SQLSensor( - name, sessionmaker, query_str, column_name, unit, value_template + name, sessmaker, query_str, column_name, unit, value_template ) queries.append(sensor) @@ -120,7 +121,6 @@ def device_state_attributes(self): def update(self): """Retrieve sensor data from the query.""" - import sqlalchemy try: sess = self.sessionmaker() diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 28357ab34b536d..afc7bebea09e18 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -1,11 +1,12 @@ """The test for the sql sensor platform.""" import unittest + import pytest import voluptuous as vol from homeassistant.components.sql.sensor import validate_sql_select -from homeassistant.setup import setup_component from homeassistant.const import STATE_UNKNOWN +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant From c42ca94a8656c68eebeed6c108a90afa5a2b5758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 12:14:07 +0200 Subject: [PATCH 1073/3953] move imports in smarthab component (#27942) --- homeassistant/components/smarthab/__init__.py | 4 ++-- homeassistant/components/smarthab/cover.py | 14 ++++++++------ homeassistant/components/smarthab/light.py | 8 +++++--- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index 7206bea110ba38..ef2da4e9a1dc71 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -6,11 +6,12 @@ """ import logging +import pysmarthab import voluptuous as vol -from homeassistant.helpers.discovery import load_platform from homeassistant.const import CONF_EMAIL, CONF_PASSWORD import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform DOMAIN = "smarthab" DATA_HUB = "hub" @@ -32,7 +33,6 @@ def setup(hass, config) -> bool: """Set up the SmartHab platform.""" - import pysmarthab sh_conf = config.get(DOMAIN) diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py index 3d5b4259aa9cf5..9bcb89b7ab4df5 100644 --- a/homeassistant/components/smarthab/cover.py +++ b/homeassistant/components/smarthab/cover.py @@ -4,18 +4,21 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/smarthab/ """ -import logging from datetime import timedelta +import logging + +import pysmarthab from requests.exceptions import Timeout from homeassistant.components.cover import ( - CoverDevice, - SUPPORT_OPEN, + ATTR_POSITION, SUPPORT_CLOSE, + SUPPORT_OPEN, SUPPORT_SET_POSITION, - ATTR_POSITION, + CoverDevice, ) -from . import DOMAIN, DATA_HUB + +from . import DATA_HUB, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,7 +27,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the SmartHab roller shutters platform.""" - import pysmarthab hub = hass.data[DOMAIN][DATA_HUB] devices = hub.get_device_list() diff --git a/homeassistant/components/smarthab/light.py b/homeassistant/components/smarthab/light.py index a8a55dea48a739..bc6eb31fd0415d 100644 --- a/homeassistant/components/smarthab/light.py +++ b/homeassistant/components/smarthab/light.py @@ -4,12 +4,15 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/smarthab/ """ -import logging from datetime import timedelta +import logging + +import pysmarthab from requests.exceptions import Timeout from homeassistant.components.light import Light -from . import DOMAIN, DATA_HUB + +from . import DATA_HUB, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -18,7 +21,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the SmartHab lights platform.""" - import pysmarthab hub = hass.data[DOMAIN][DATA_HUB] devices = hub.get_device_list() From 5ce437dc304520c08c7792c8cc3fafa41b3959de Mon Sep 17 00:00:00 2001 From: Quentame Date: Sun, 20 Oct 2019 12:15:46 +0200 Subject: [PATCH 1074/3953] Fixing config_entries.async_forward_entry_unload calls (step 1) (#27857) --- homeassistant/components/cert_expiry/__init__.py | 5 +---- homeassistant/components/linky/__init__.py | 5 +---- homeassistant/components/locative/__init__.py | 3 +-- homeassistant/components/luftdaten/__init__.py | 5 +---- homeassistant/components/withings/__init__.py | 6 +----- 5 files changed, 5 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index f5078219809397..28a79a3e5058ad 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -19,7 +19,4 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass, entry): """Unload a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_unload(entry, "sensor") - ) - return True + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/linky/__init__.py b/homeassistant/components/linky/__init__.py index ad5b6743d37d53..1d382b4352542a 100644 --- a/homeassistant/components/linky/__init__.py +++ b/homeassistant/components/linky/__init__.py @@ -55,7 +55,4 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload Linky sensors.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_unload(entry, "sensor") - ) - return True + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 61e0b1f7474949..ed8bcb6e7e5cfb 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -127,8 +127,7 @@ 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 + return await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) # pylint: disable=invalid-name diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index ac524502f8d67b..3dca82404c071a 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -172,12 +172,9 @@ async def async_unload_entry(hass, config_entry): ) remove_listener() - for component in ("sensor",): - await hass.config_entries.async_forward_entry_unload(config_entry, component) - hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT].pop(config_entry.entry_id) - return True + return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") class LuftDatenData: diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index ecefa681b87bbe..baed9300d46408 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -92,8 +92,4 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload Withings config entry.""" - await hass.async_create_task( - hass.config_entries.async_forward_entry_unload(entry, "sensor") - ) - - return True + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") From 5a592f1291d37dab5b5cd514e2841d7343085700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 14:33:58 +0200 Subject: [PATCH 1075/3953] move imports in sma component (#27945) --- homeassistant/components/sma/sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 56e10b03d2adca..ff1c48a141dc5e 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -3,17 +3,18 @@ from datetime import timedelta import logging +import pysma import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, + CONF_PATH, CONF_SCAN_INTERVAL, CONF_SSL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, - CONF_PATH, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -35,8 +36,6 @@ def _check_sensor_schema(conf): """Check sensors and attributes are valid.""" try: - import pysma - valid = [s.name for s in pysma.Sensors()] except (ImportError, AttributeError): return conf @@ -87,7 +86,6 @@ def _check_sensor_schema(conf): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up SMA WebConnect sensor.""" - import pysma # Check config again during load - dependency available config = _check_sensor_schema(config) From ac5ce4136edb836e439acf9f5449cf1039087277 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 20 Oct 2019 18:11:48 +0200 Subject: [PATCH 1076/3953] Remove tplink device tracker (#27936) * Remove tplink device tracker Version 0.94 added a distress signal and since then nothing has happened. This commit removes the device tracker which should have never been a part of tplink integration in the first place as it does not share anything with this pyhs100-based integration / kasa smarthome. * add updated requirements_test_all that was forgotten * remove unit tests --- .../components/tplink/device_tracker.py | 508 ------------------ homeassistant/components/tplink/manifest.json | 3 +- requirements_all.txt | 3 - requirements_test_all.txt | 3 - .../components/tplink/test_device_tracker.py | 71 --- 5 files changed, 1 insertion(+), 587 deletions(-) delete mode 100644 homeassistant/components/tplink/device_tracker.py delete mode 100644 tests/components/tplink/test_device_tracker.py diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py deleted file mode 100644 index ce17f6e465f730..00000000000000 --- a/homeassistant/components/tplink/device_tracker.py +++ /dev/null @@ -1,508 +0,0 @@ -"""Support for TP-Link routers.""" -import base64 -from datetime import datetime -import hashlib -import logging -import re - -from aiohttp.hdrs import ( - ACCEPT, - ACCEPT_ENCODING, - ACCEPT_LANGUAGE, - CACHE_CONTROL, - CONNECTION, - CONTENT_TYPE, - COOKIE, - KEEP_ALIVE, - PRAGMA, - REFERER, - USER_AGENT, -) -import requests -from tplink.tplink import TpLinkClient -import voluptuous as vol - -from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA, - DeviceScanner, -) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - HTTP_HEADER_X_REQUESTED_WITH, -) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -HTTP_HEADER_NO_CACHE = "no-cache" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - } -) - - -def get_scanner(hass, config): - """ - Validate the configuration and return a TP-Link scanner. - - The default way of integrating devices is to use a pypi - - package, The TplinkDeviceScanner has been refactored - - to depend on a pypi package, the other implementations - - should be gradually migrated in the pypi package - - """ - _LOGGER.warning( - "TP-Link device tracker is unmaintained and will be " - "removed in the future releases if no maintainer is " - "found. If you have interest in this integration, " - "feel free to create a pull request to move this code " - "to a new 'tplink_router' integration and refactoring " - "the device-specific parts to the tplink library" - ) - for cls in [ - TplinkDeviceScanner, - Tplink5DeviceScanner, - Tplink4DeviceScanner, - Tplink3DeviceScanner, - Tplink2DeviceScanner, - Tplink1DeviceScanner, - ]: - scanner = cls(config[DOMAIN]) - if scanner.success_init: - return scanner - - return None - - -class TplinkDeviceScanner(DeviceScanner): - """Queries the router for connected devices.""" - - def __init__(self, config): - """Initialize the scanner.""" - - host = config[CONF_HOST] - password = config[CONF_PASSWORD] - username = config[CONF_USERNAME] - - self.success_init = False - try: - self.tplink_client = TpLinkClient(password, host=host, username=username) - - self.last_results = {} - - self.success_init = self._update_info() - except requests.exceptions.RequestException: - _LOGGER.debug("RequestException in %s", self.__class__.__name__) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results.keys() - - def get_device_name(self, device): - """Get the name of the device.""" - return self.last_results.get(device) - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - result = self.tplink_client.get_connected_devices() - - if result: - self.last_results = result - return True - - return False - - -class Tplink1DeviceScanner(DeviceScanner): - """This class queries a wireless router running TP-Link firmware.""" - - def __init__(self, config): - """Initialize the scanner.""" - host = config[CONF_HOST] - username, password = config[CONF_USERNAME], config[CONF_PASSWORD] - - self.parse_macs = re.compile( - "[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}-" - + "[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}" - ) - - self.host = host - self.username = username - self.password = password - - self.last_results = {} - self.success_init = False - try: - self.success_init = self._update_info() - except requests.exceptions.RequestException: - _LOGGER.debug("RequestException in %s", self.__class__.__name__) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results - - def get_device_name(self, device): - """Get firmware doesn't save the name of the wireless device.""" - return None - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - - url = f"http://{self.host}/userRpm/WlanStationRpm.htm" - referer = f"http://{self.host}" - page = requests.get( - url, - auth=(self.username, self.password), - headers={REFERER: referer}, - timeout=4, - ) - - result = self.parse_macs.findall(page.text) - - if result: - self.last_results = [mac.replace("-", ":") for mac in result] - return True - - return False - - -class Tplink2DeviceScanner(Tplink1DeviceScanner): - """This class queries a router with newer version of TP-Link firmware.""" - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results.keys() - - def get_device_name(self, device): - """Get firmware doesn't save the name of the wireless device.""" - return self.last_results.get(device) - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - - url = f"http://{self.host}/data/map_access_wireless_client_grid.json" - referer = f"http://{self.host}" - - # Router uses Authorization cookie instead of header - # Let's create the cookie - username_password = f"{self.username}:{self.password}" - b64_encoded_username_password = base64.b64encode( - username_password.encode("ascii") - ).decode("ascii") - cookie = f"Authorization=Basic {b64_encoded_username_password}" - - response = requests.post( - url, headers={REFERER: referer, COOKIE: cookie}, timeout=4 - ) - - try: - result = response.json().get("data") - except ValueError: - _LOGGER.error( - "Router didn't respond with JSON. " "Check if credentials are correct." - ) - return False - - if result: - self.last_results = { - device["mac_addr"].replace("-", ":"): device["name"] - for device in result - } - return True - - return False - - -class Tplink3DeviceScanner(Tplink1DeviceScanner): - """This class queries the Archer C9 router with version 150811 or high.""" - - def __init__(self, config): - """Initialize the scanner.""" - self.stok = "" - self.sysauth = "" - super().__init__(config) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - self._log_out() - return self.last_results.keys() - - def get_device_name(self, device): - """Get the firmware doesn't save the name of the wireless device. - - We are forced to use the MAC address as name here. - """ - return self.last_results.get(device) - - def _get_auth_tokens(self): - """Retrieve auth tokens from the router.""" - _LOGGER.info("Retrieving auth tokens...") - - url = f"http://{self.host}/cgi-bin/luci/;stok=/login?form=login" - referer = f"http://{self.host}/webpages/login.html" - - # If possible implement RSA encryption of password here. - response = requests.post( - url, - params={ - "operation": "login", - "username": self.username, - "password": self.password, - }, - headers={REFERER: referer}, - timeout=4, - ) - - try: - self.stok = response.json().get("data").get("stok") - _LOGGER.info(self.stok) - regex_result = re.search("sysauth=(.*);", response.headers["set-cookie"]) - self.sysauth = regex_result.group(1) - _LOGGER.info(self.sysauth) - return True - except (ValueError, KeyError): - _LOGGER.error("Couldn't fetch auth tokens! Response was: %s", response.text) - return False - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - if (self.stok == "") or (self.sysauth == ""): - self._get_auth_tokens() - - _LOGGER.info("Loading wireless clients...") - - url = ( - "http://{}/cgi-bin/luci/;stok={}/admin/wireless?" "form=statistics" - ).format(self.host, self.stok) - referer = f"http://{self.host}/webpages/index.html" - - response = requests.post( - url, - params={"operation": "load"}, - headers={REFERER: referer}, - cookies={"sysauth": self.sysauth}, - timeout=5, - ) - - try: - json_response = response.json() - - if json_response.get("success"): - result = response.json().get("data") - else: - if json_response.get("errorcode") == "timeout": - _LOGGER.info("Token timed out. Relogging on next scan") - self.stok = "" - self.sysauth = "" - return False - _LOGGER.error("An unknown error happened while fetching data") - return False - except ValueError: - _LOGGER.error( - "Router didn't respond with JSON. " "Check if credentials are correct" - ) - return False - - if result: - self.last_results = { - device["mac"].replace("-", ":"): device["mac"] for device in result - } - return True - - return False - - def _log_out(self): - _LOGGER.info("Logging out of router admin interface...") - - url = ("http://{}/cgi-bin/luci/;stok={}/admin/system?" "form=logout").format( - self.host, self.stok - ) - referer = f"http://{self.host}/webpages/index.html" - - requests.post( - url, - params={"operation": "write"}, - headers={REFERER: referer}, - cookies={"sysauth": self.sysauth}, - ) - self.stok = "" - self.sysauth = "" - - -class Tplink4DeviceScanner(Tplink1DeviceScanner): - """This class queries an Archer C7 router with TP-Link firmware 150427.""" - - def __init__(self, config): - """Initialize the scanner.""" - self.credentials = "" - self.token = "" - super().__init__(config) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results - - def get_device_name(self, device): - """Get the name of the wireless device.""" - return None - - def _get_auth_tokens(self): - """Retrieve auth tokens from the router.""" - _LOGGER.info("Retrieving auth tokens...") - url = f"http://{self.host}/userRpm/LoginRpm.htm?Save=Save" - - # Generate md5 hash of password. The C7 appears to use the first 15 - # characters of the password only, so we truncate to remove additional - # characters from being hashed. - password = hashlib.md5(self.password.encode("utf")[:15]).hexdigest() - credentials = f"{self.username}:{password}".encode("utf") - - # Encode the credentials to be sent as a cookie. - self.credentials = base64.b64encode(credentials).decode("utf") - - # Create the authorization cookie. - cookie = f"Authorization=Basic {self.credentials}" - - response = requests.get(url, headers={COOKIE: cookie}) - - try: - result = re.search( - r"window.parent.location.href = " - r'"https?:\/\/.*\/(.*)\/userRpm\/Index.htm";', - response.text, - ) - if not result: - return False - self.token = result.group(1) - return True - except ValueError: - _LOGGER.error("Couldn't fetch auth tokens") - return False - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - if (self.credentials == "") or (self.token == ""): - self._get_auth_tokens() - - _LOGGER.info("Loading wireless clients...") - - mac_results = [] - - # Check both the 2.4GHz and 5GHz client list URLs - for clients_url in ("WlanStationRpm.htm", "WlanStationRpm_5g.htm"): - url = f"http://{self.host}/{self.token}/userRpm/{clients_url}" - referer = f"http://{self.host}" - cookie = f"Authorization=Basic {self.credentials}" - - page = requests.get(url, headers={COOKIE: cookie, REFERER: referer}) - mac_results.extend(self.parse_macs.findall(page.text)) - - if not mac_results: - return False - - self.last_results = [mac.replace("-", ":") for mac in mac_results] - return True - - -class Tplink5DeviceScanner(Tplink1DeviceScanner): - """This class queries a TP-Link EAP-225 AP with newer TP-Link FW.""" - - def scan_devices(self): - """Scan for new devices and return a list with found MAC IDs.""" - self._update_info() - return self.last_results.keys() - - def get_device_name(self, device): - """Get firmware doesn't save the name of the wireless device.""" - return None - - def _update_info(self): - """Ensure the information from the TP-Link AP is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - - base_url = f"http://{self.host}" - - header = { - USER_AGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;" - " rv:53.0) Gecko/20100101 Firefox/53.0", - ACCEPT: "application/json, text/javascript, */*; q=0.01", - ACCEPT_LANGUAGE: "Accept-Language: en-US,en;q=0.5", - ACCEPT_ENCODING: "gzip, deflate", - CONTENT_TYPE: "application/x-www-form-urlencoded; charset=UTF-8", - HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest", - REFERER: f"http://{self.host}/", - CONNECTION: KEEP_ALIVE, - PRAGMA: HTTP_HEADER_NO_CACHE, - CACHE_CONTROL: HTTP_HEADER_NO_CACHE, - } - - password_md5 = hashlib.md5(self.password.encode("utf")).hexdigest().upper() - - # Create a session to handle cookie easier - session = requests.session() - session.get(base_url, headers=header) - - login_data = {"username": self.username, "password": password_md5} - session.post(base_url, login_data, headers=header) - - # A timestamp is required to be sent as get parameter - timestamp = int(datetime.now().timestamp() * 1e3) - - client_list_url = f"{base_url}/data/monitor.client.client.json" - - get_params = {"operation": "load", "_": timestamp} - - response = session.get(client_list_url, headers=header, params=get_params) - session.close() - try: - list_of_devices = response.json() - except ValueError: - _LOGGER.error( - "AP didn't respond with JSON. " "Check if credentials are correct" - ) - return False - - if list_of_devices: - self.last_results = { - device["MAC"].replace("-", ":"): device["DeviceName"] - for device in list_of_devices["data"] - } - return True - - return False diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index f299e02e2d3406..c2a2197c8448b9 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -4,8 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink", "requirements": [ - "pyHS100==0.3.5", - "tplink==0.2.1" + "pyHS100==0.3.5" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ad17b59b455224..c79e36413a9327 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1899,9 +1899,6 @@ total_connect_client==0.28 # homeassistant.components.tplink_lte tp-connected==0.0.4 -# homeassistant.components.tplink -tplink==0.2.1 - # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b59a1d806b6fe..7912de963abbf4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -586,9 +586,6 @@ tellduslive==0.10.10 # homeassistant.components.toon toonapilib==3.2.4 -# homeassistant.components.tplink -tplink==0.2.1 - # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/tests/components/tplink/test_device_tracker.py b/tests/components/tplink/test_device_tracker.py deleted file mode 100644 index bbe73dc121a9d6..00000000000000 --- a/tests/components/tplink/test_device_tracker.py +++ /dev/null @@ -1,71 +0,0 @@ -"""The tests for the tplink device tracker platform.""" - -import os -import pytest - -from homeassistant.components.device_tracker.legacy import YAML_DEVICES -from homeassistant.components.tplink.device_tracker import Tplink4DeviceScanner -from homeassistant.const import CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST -import requests_mock - - -@pytest.fixture(autouse=True) -def setup_comp(hass): - """Initialize components.""" - yaml_devices = hass.config.path(YAML_DEVICES) - yield - if os.path.isfile(yaml_devices): - os.remove(yaml_devices) - - -async def test_get_mac_addresses_from_both_bands(hass): - """Test grabbing the mac addresses from 2.4 and 5 GHz clients pages.""" - with requests_mock.Mocker() as m: - conf_dict = { - CONF_PLATFORM: "tplink", - CONF_HOST: "fake-host", - CONF_USERNAME: "fake_user", - CONF_PASSWORD: "fake_pass", - } - - # Mock the token retrieval process - FAKE_TOKEN = "fake_token" - fake_auth_token_response = ( - "window.parent.location.href = " - '"https://a/{}/userRpm/Index.htm";'.format(FAKE_TOKEN) - ) - - m.get( - "http://{}/userRpm/LoginRpm.htm?Save=Save".format(conf_dict[CONF_HOST]), - text=fake_auth_token_response, - ) - - FAKE_MAC_1 = "CA-FC-8A-C8-BB-53" - FAKE_MAC_2 = "6C-48-83-21-46-8D" - FAKE_MAC_3 = "77-98-75-65-B1-2B" - mac_response_2_4 = "{} {}".format(FAKE_MAC_1, FAKE_MAC_2) - mac_response_5 = "{}".format(FAKE_MAC_3) - - # Mock the 2.4 GHz clients page - m.get( - "http://{}/{}/userRpm/WlanStationRpm.htm".format( - conf_dict[CONF_HOST], FAKE_TOKEN - ), - text=mac_response_2_4, - ) - - # Mock the 5 GHz clients page - m.get( - "http://{}/{}/userRpm/WlanStationRpm_5g.htm".format( - conf_dict[CONF_HOST], FAKE_TOKEN - ), - text=mac_response_5, - ) - - tplink = Tplink4DeviceScanner(conf_dict) - - expected_mac_results = [ - mac.replace("-", ":") for mac in [FAKE_MAC_1, FAKE_MAC_2, FAKE_MAC_3] - ] - - assert tplink.last_results == expected_mac_results From 22b29a800501bc3959c1035b7664acff82908a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=BDdrale?= Date: Sun, 20 Oct 2019 18:43:44 +0200 Subject: [PATCH 1077/3953] Add option to disable HTTPS verification in Luci component (#27946) * Add option to disable HTTPS verification in Luci component * Update code owners * Update code owners --- CODEOWNERS | 2 +- homeassistant/components/luci/device_tracker.py | 11 ++++++++++- homeassistant/components/luci/manifest.json | 7 +++++-- requirements_all.txt | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 2f228105cbbbc3..30946fb14f2353 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -171,7 +171,7 @@ homeassistant/components/liveboxplaytv/* @pschmitt homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd homeassistant/components/lovelace/* @home-assistant/frontend -homeassistant/components/luci/* @fbradyirl +homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luftdaten/* @fabaff homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 87a32767cc281b..59c3251a437a8d 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -8,12 +8,19 @@ PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -21,6 +28,7 @@ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, } ) @@ -44,6 +52,7 @@ def __init__(self, config): config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_SSL], + config[CONF_VERIFY_SSL], ) self.last_results = {} diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 646fc1a3cbfa50..d7cf72ebaf5ad7 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -3,8 +3,11 @@ "name": "Luci", "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": [ - "openwrt-luci-rpc==1.1.1" + "openwrt-luci-rpc==1.1.2" ], "dependencies": [], - "codeowners": ["@fbradyirl"] + "codeowners": [ + "@fbradyirl", + "@mzdrale" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index c79e36413a9327..0b86ae2d7da7c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -917,7 +917,7 @@ opensensemap-api==0.1.5 openwebifpy==3.1.1 # homeassistant.components.luci -openwrt-luci-rpc==1.1.1 +openwrt-luci-rpc==1.1.2 # homeassistant.components.oru oru==0.1.9 From 425e7fd1a7aa52995b056408324ce574d19765b5 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 20 Oct 2019 17:51:08 +0100 Subject: [PATCH 1078/3953] bugfix evohome and bump client (#27968) * bump client to 0.3.4b1 * handle bad schedules that cause issue #27768 --- homeassistant/components/evohome/__init__.py | 65 ++++++++++--------- .../components/evohome/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index a52780c8a0f3b9..f86bf9742053f4 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -460,36 +460,43 @@ def setpoints(self) -> Dict[str, Any]: day_of_week = int(day_time.strftime("%w")) # 0 is Sunday time_of_day = day_time.strftime("%H:%M:%S") - # Iterate today's switchpoints until past the current time of day... - day = self._schedule["DailySchedules"][day_of_week] - sp_idx = -1 # last switchpoint of the day before - for i, tmp in enumerate(day["Switchpoints"]): - if time_of_day > tmp["TimeOfDay"]: - sp_idx = i # current setpoint - else: - break - - # Did the current SP start yesterday? Does the next start SP tomorrow? - this_sp_day = -1 if sp_idx == -1 else 0 - next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 - - for key, offset, idx in [ - ("this", this_sp_day, sp_idx), - ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), - ]: - sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") - day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] - switchpoint = day["Switchpoints"][idx] - - dt_local_aware = _local_dt_to_aware( - dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}") - ) + try: + # Iterate today's switchpoints until past the current time of day... + day = self._schedule["DailySchedules"][day_of_week] + sp_idx = -1 # last switchpoint of the day before + for i, tmp in enumerate(day["Switchpoints"]): + if time_of_day > tmp["TimeOfDay"]: + sp_idx = i # current setpoint + else: + break + + # Did the current SP start yesterday? Does the next start SP tomorrow? + this_sp_day = -1 if sp_idx == -1 else 0 + next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 + + for key, offset, idx in [ + ("this", this_sp_day, sp_idx), + ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), + ]: + sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") + day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] + switchpoint = day["Switchpoints"][idx] + + dt_local_aware = _local_dt_to_aware( + dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}") + ) - self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat() - try: - self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"] - except KeyError: - self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] + self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat() + try: + self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"] + except KeyError: + self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] + + except IndexError: + self._setpoints = {} + _LOGGER.warning( + "Failed to get setpoints - please report as an issue", exc_info=True + ) return self._setpoints diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index da942db7920ca7..0b112df42bbd08 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -3,7 +3,7 @@ "name": "Evohome", "documentation": "https://www.home-assistant.io/integrations/evohome", "requirements": [ - "evohome-async==0.3.3b5" + "evohome-async==0.3.4b1" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/requirements_all.txt b/requirements_all.txt index 0b86ae2d7da7c1..3c649e91756574 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -480,7 +480,7 @@ eternalegypt==0.0.10 # evdev==0.6.1 # homeassistant.components.evohome -evohome-async==0.3.3b5 +evohome-async==0.3.4b1 # homeassistant.components.dlib_face_detect # homeassistant.components.dlib_face_identify From c44163548dd6a137597d699e15eb60d667cf7ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 15:40:13 -0300 Subject: [PATCH 1079/3953] Move imports in dte_energy_bridge component (#27975) --- homeassistant/components/dte_energy_bridge/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index b904d004c61d86..aa822da0d6a4ba 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -1,12 +1,13 @@ """Support for monitoring energy usage using the DTE energy bridge.""" import logging +import requests import voluptuous as vol -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -78,8 +79,6 @@ def icon(self): def update(self): """Get the energy usage data from the DTE energy bridge.""" - import requests - try: response = requests.get(self._url, timeout=5) except (requests.exceptions.RequestException, ValueError): From 57b6c2c6b086b73f168a037740bd58eb065b12a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 15:41:11 -0300 Subject: [PATCH 1080/3953] Move imports in crimereports component (#27973) --- homeassistant/components/crimereports/sensor.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/crimereports/sensor.py b/homeassistant/components/crimereports/sensor.py index 6295125b7cabd0..cf5b2e374e2cfc 100644 --- a/homeassistant/components/crimereports/sensor.py +++ b/homeassistant/components/crimereports/sensor.py @@ -3,27 +3,28 @@ from datetime import timedelta import logging +import crimereports import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_INCLUDE, - CONF_EXCLUDE, - CONF_NAME, - CONF_LATITUDE, - CONF_LONGITUDE, ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_EXCLUDE, + CONF_INCLUDE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, CONF_RADIUS, LENGTH_KILOMETERS, LENGTH_METERS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.util.distance import convert from homeassistant.util.dt import now -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -65,8 +66,6 @@ class CrimeReportsSensor(Entity): def __init__(self, hass, name, latitude, longitude, radius, include, exclude): """Initialize the Crime Reports sensor.""" - import crimereports - self._hass = hass self._name = name self._include = include @@ -113,8 +112,6 @@ def _incident_event(self, incident): def update(self): """Update device state.""" - import crimereports - incident_counts = defaultdict(int) incidents = self._crimereports.get_incidents( now().date(), include=self._include, exclude=self._exclude From 6951d788745ad1750685a48855bac0cf18bd1a1d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 20 Oct 2019 19:46:24 +0100 Subject: [PATCH 1081/3953] move imports in serial component (#27971) --- homeassistant/components/serial/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 27775b8c702018..a08f9522c4b9d4 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -1,12 +1,13 @@ """Support for reading data from a serial port.""" -import logging import json +import logging +import serial_asyncio import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ async def async_added_to_hass(self): async def serial_read(self, device, rate, **kwargs): """Read the data from the port.""" - import serial_asyncio - reader, _ = await serial_asyncio.open_serial_connection( url=device, baudrate=rate, **kwargs ) From 9db07b2a4162ae69c4f1da1977b58e49793a185a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 15:46:51 -0300 Subject: [PATCH 1082/3953] Move imports in onvif component (#27969) --- homeassistant/components/onvif/camera.py | 40 ++++++++++-------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 29af1049faede3..c73886c13c03e9 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -8,21 +8,29 @@ import datetime as dt import logging import os + +from aiohttp.client_exceptions import ClientConnectionError, ServerDisconnectedError +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame +import onvif +from onvif import ONVIFCamera, exceptions import voluptuous as vol +from zeep.exceptions import Fault +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.components.camera.const import DOMAIN +from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG from homeassistant.const import ( - CONF_NAME, + ATTR_ENTITY_ID, CONF_HOST, - CONF_USERNAME, + CONF_NAME, CONF_PASSWORD, CONF_PORT, - ATTR_ENTITY_ID, + CONF_USERNAME, ) -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM -from homeassistant.components.camera.const import DOMAIN -from homeassistant.components.ffmpeg import DATA_FFMPEG, CONF_EXTRA_ARGUMENTS -import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import async_extract_entity_ids import homeassistant.util.dt as dt_util @@ -122,9 +130,6 @@ def __init__(self, hass, config): _LOGGER.debug("Importing dependencies") - import onvif - from onvif import ONVIFCamera - _LOGGER.debug("Setting up the ONVIF camera component") self._username = config.get(CONF_USERNAME) @@ -156,10 +161,6 @@ async def async_initialize(self): Initializes the camera by obtaining the input uri and connecting to the camera. Also retrieves the ONVIF profiles. """ - from aiohttp.client_exceptions import ClientConnectionError - from homeassistant.exceptions import PlatformNotReady - from zeep.exceptions import Fault - try: _LOGGER.debug("Updating service addresses") await self._camera.update_xaddrs() @@ -169,7 +170,7 @@ async def async_initialize(self): self.setup_ptz() except ClientConnectionError as err: _LOGGER.warning( - "Couldn't connect to camera '%s', but will " "retry later. Error: %s", + "Couldn't connect to camera '%s', but will retry later. Error: %s", self._name, err, ) @@ -184,8 +185,6 @@ async def async_initialize(self): async def async_check_date_and_time(self): """Warns if camera and system date not synced.""" - from aiohttp.client_exceptions import ServerDisconnectedError - _LOGGER.debug("Setting up the ONVIF device management service") devicemgmt = self._camera.create_devicemgmt_service() @@ -228,8 +227,6 @@ async def async_check_date_and_time(self): async def async_obtain_input_uri(self): """Set the input uri for the camera.""" - from onvif import exceptions - _LOGGER.debug( "Connecting with ONVIF Camera: %s on port %s", self._host, self._port ) @@ -289,8 +286,6 @@ def setup_ptz(self): async def async_perform_ptz(self, pan, tilt, zoom): """Perform a PTZ action on the camera.""" - from onvif import exceptions - if self._ptz_service is None: _LOGGER.warning("PTZ actions are not supported on camera '%s'", self._name) return @@ -332,7 +327,6 @@ async def async_added_to_hass(self): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG _LOGGER.debug("Retrieving image from camera '%s'", self._name) @@ -347,8 +341,6 @@ async def async_camera_image(self): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg - _LOGGER.debug("Handling mjpeg stream from camera '%s'", self._name) ffmpeg_manager = self.hass.data[DATA_FFMPEG] From 54a711ca6af2ee6fb977441db2a25bea1198e65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 15:47:40 -0300 Subject: [PATCH 1083/3953] Move imports in dweet component (#27976) --- homeassistant/components/dweet/__init__.py | 7 +++---- homeassistant/components/dweet/sensor.py | 13 +++++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/dweet/__init__.py b/homeassistant/components/dweet/__init__.py index bf1298479c3604..db985e57a41bdb 100644 --- a/homeassistant/components/dweet/__init__.py +++ b/homeassistant/components/dweet/__init__.py @@ -1,7 +1,8 @@ """Support for sending data to Dweet.io.""" -import logging from datetime import timedelta +import logging +import dweepy import voluptuous as vol from homeassistant.const import ( @@ -10,8 +11,8 @@ EVENT_STATE_CHANGED, STATE_UNKNOWN, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -69,8 +70,6 @@ def dweet_event_listener(event): @Throttle(MIN_TIME_BETWEEN_UPDATES) def send_data(name, msg): """Send the collected data to Dweet.io.""" - import dweepy - try: dweepy.dweet_for(name, msg) except dweepy.DweepyError: diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index 937de9b030aa05..f3f604ff3692bf 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -1,18 +1,19 @@ """Support for showing values from Dweet.io.""" +from datetime import timedelta import json import logging -from datetime import timedelta +import dweepy import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + CONF_DEVICE, CONF_NAME, - CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, - CONF_DEVICE, + CONF_VALUE_TEMPLATE, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -33,8 +34,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dweet sensor.""" - import dweepy - name = config.get(CONF_NAME) device = config.get(CONF_DEVICE) value_template = config.get(CONF_VALUE_TEMPLATE) @@ -107,8 +106,6 @@ def __init__(self, device): def update(self): """Get the latest data from Dweet.io.""" - import dweepy - try: self.data = dweepy.get_latest_dweet_for(self._device) except dweepy.DweepyError: From 8356d92f04b9c53943f7c7b4f04d01e11dd20ec9 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 20 Oct 2019 19:48:52 +0100 Subject: [PATCH 1084/3953] Refactor entity_ids, tweak names and consolidate classes (#27921) * refactor entity_ids, and consolidate classes * isort the code --- .../components/incomfort/__init__.py | 41 +++++++++++++++- .../components/incomfort/binary_sensor.py | 35 +++----------- homeassistant/components/incomfort/climate.py | 36 +++----------- homeassistant/components/incomfort/sensor.py | 47 +++++-------------- .../components/incomfort/water_heater.py | 23 ++++----- 5 files changed, 76 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index d6f72209f06577..adf57e35093e5c 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -1,14 +1,18 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" import logging +from typing import Optional from aiohttp import ClientResponseError -import voluptuous as vol from incomfortclient import Gateway as InComfortGateway +import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PASSWORD, 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_connect +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -53,3 +57,38 @@ async def async_setup(hass, hass_config): ) return True + + +class IncomfortEntity(Entity): + """Base class for all InComfort entities.""" + + def __init__(self) -> None: + """Initialize the class.""" + self._unique_id = self._name = None + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> Optional[str]: + """Return the name of the sensor.""" + return self._name + + +class IncomfortChild(IncomfortEntity): + """Base class for all InComfort entities (excluding the boiler).""" + + async def async_added_to_hass(self) -> 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 should_poll(self) -> bool: + """Return False as this device should never be polled.""" + return False diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 39a45429cb1d15..b5dbd8e223d3f8 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -1,11 +1,9 @@ """Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" from typing import Any, Dict, Optional -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice -from . import DOMAIN +from . import DOMAIN, IncomfortChild async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -18,34 +16,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class IncomfortFailed(BinarySensorDevice): +class IncomfortFailed(IncomfortChild, BinarySensorDevice): """Representation of an InComfort Failed sensor.""" def __init__(self, client, heater) -> None: """Initialize the binary sensor.""" + super().__init__() + self._unique_id = f"{heater.serial_no}_failed" + self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_failed") + self._name = "Boiler Fault" self._client = client self._heater = heater - async def async_added_to_hass(self) -> 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 unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> Optional[str]: - """Return the name of the sensor.""" - return "Fault state" - @property def is_on(self) -> bool: """Return the status of the sensor.""" @@ -55,8 +39,3 @@ def is_on(self) -> bool: def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" return {"fault_code": self._heater.status["fault_code"]} - - @property - def should_poll(self) -> bool: - """Return False as this device should never be polled.""" - return False diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 3918244d4e856d..95ccf1863724ca 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -1,16 +1,14 @@ """Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" from typing import Any, Dict, List, Optional -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN +from . import DOMAIN, IncomfortChild async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -24,39 +22,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([InComfortClimate(client, heater, r) for r in heater.rooms]) -class InComfortClimate(ClimateDevice): +class InComfortClimate(IncomfortChild, ClimateDevice): """Representation of an InComfort/InTouch climate device.""" def __init__(self, client, heater, room) -> None: """Initialize the climate device.""" + super().__init__() + self._unique_id = f"{heater.serial_no}_{room.room_no}" + self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_{room.room_no}") + self._name = f"Thermostat {room.room_no}" self._client = client self._room = room - self._name = f"Room {room.room_no}" - - async def async_added_to_hass(self) -> 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 should_poll(self) -> bool: - """Return False as this device should never be polled.""" - return False - - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the climate device.""" - return self._name @property def device_state_attributes(self) -> Dict[str, Any]: diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index 772b5dab1834a1..f3170b7b9bba49 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -1,18 +1,16 @@ """Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" from typing import Any, Dict, Optional +from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.const import ( - PRESSURE_BAR, - TEMP_CELSIUS, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + PRESSURE_BAR, + TEMP_CELSIUS, ) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from . import DOMAIN +from . import DOMAIN, IncomfortChild INCOMFORT_HEATER_TEMP = "CV Temp" INCOMFORT_PRESSURE = "CV Pressure" @@ -42,42 +40,28 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class IncomfortSensor(Entity): +class IncomfortSensor(IncomfortChild): """Representation of an InComfort/InTouch sensor device.""" def __init__(self, client, heater, name) -> None: """Initialize the sensor.""" + super().__init__() + self._client = client self._heater = heater self._unique_id = f"{heater.serial_no}_{slugify(name)}" + self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_{slugify(name)}") + self._name = f"Boiler {name}" - self._name = name self._device_class = None + self._state_attr = INCOMFORT_MAP_ATTRS[name][0] self._unit_of_measurement = None - async def async_added_to_hass(self) -> 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 unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> Optional[str]: - """Return the name of the sensor.""" - return self._name - @property def state(self) -> Optional[str]: """Return the state of the sensor.""" - return self._heater.status[INCOMFORT_MAP_ATTRS[self._name][0]] + return self._heater.status[self._state_attr] @property def device_class(self) -> Optional[str]: @@ -89,11 +73,6 @@ def unit_of_measurement(self) -> Optional[str]: """Return the unit of measurement of the sensor.""" return self._unit_of_measurement - @property - def should_poll(self) -> bool: - """Return False as this device should never be polled.""" - return False - class IncomfortPressure(IncomfortSensor): """Representation of an InTouch CV Pressure sensor.""" @@ -113,11 +92,11 @@ def __init__(self, client, heater, name) -> None: """Initialize the signal strength sensor.""" super().__init__(client, heater, name) + self._attr = INCOMFORT_MAP_ATTRS[name][1] self._device_class = DEVICE_CLASS_TEMPERATURE self._unit_of_measurement = TEMP_CELSIUS @property def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" - key = INCOMFORT_MAP_ATTRS[self._name][1] - return {key: self._heater.status[key]} + return {self._attr: self._heater.status[self._attr]} diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 70423611705d20..0015107b40f0de 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -1,14 +1,15 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" import asyncio import logging -from typing import Any, Dict, Optional +from typing import Any, Dict from aiohttp import ClientResponseError -from homeassistant.components.water_heater import WaterHeaterDevice + +from homeassistant.components.water_heater import ENTITY_ID_FORMAT, WaterHeaterDevice from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import DOMAIN +from . import DOMAIN, IncomfortEntity _LOGGER = logging.getLogger(__name__) @@ -26,26 +27,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([IncomfortWaterHeater(client, heater)]) -class IncomfortWaterHeater(WaterHeaterDevice): +class IncomfortWaterHeater(IncomfortEntity, WaterHeaterDevice): """Representation of an InComfort/Intouch water_heater device.""" def __init__(self, client, heater) -> None: """Initialize the water_heater device.""" + super().__init__() + self._unique_id = f"{heater.serial_no}" + self.entity_id = ENTITY_ID_FORMAT.format(DOMAIN) + self._name = "Boiler" self._client = client self._heater = heater - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the water_heater device.""" - return "Boiler" - @property def icon(self) -> str: """Return the icon of the water_heater device.""" From ca0a4a8750b1b1a8a6e050d4dc6e2692cea3e5a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 16:16:04 -0300 Subject: [PATCH 1085/3953] Move imports for ebusd component (#27979) --- homeassistant/components/ebusd/__init__.py | 10 +++------- homeassistant/components/ebusd/const.py | 2 +- homeassistant/components/ebusd/sensor.py | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index e11de446e405f9..e4d0bdbcdb1ac1 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -3,13 +3,14 @@ import logging import socket +import ebusdpy import voluptuous as vol from homeassistant.const import ( - CONF_NAME, CONF_HOST, - CONF_PORT, CONF_MONITORED_CONDITIONS, + CONF_NAME, + CONF_PORT, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -66,7 +67,6 @@ def setup(hass, config): try: _LOGGER.debug("Ebusd integration setup started") - import ebusdpy ebusdpy.init(server_address) hass.data[DOMAIN] = EbusdData(server_address, circuit) @@ -98,8 +98,6 @@ def __init__(self, address, circuit): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, name, stype): """Call the Ebusd API to update the data.""" - import ebusdpy - try: _LOGGER.debug("Opening socket to ebusd %s", name) command_result = ebusdpy.read( @@ -116,8 +114,6 @@ def update(self, name, stype): def write(self, call): """Call write methon on ebusd.""" - import ebusdpy - name = call.data.get("name") value = call.data.get("value") diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index db79d81736ef75..ec097a153c97dd 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,5 +1,5 @@ """Constants for ebus component.""" -from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, PRESSURE_BAR +from homeassistant.const import ENERGY_KILO_WATT_HOUR, PRESSURE_BAR, TEMP_CELSIUS DOMAIN = "ebusd" TIME_SECONDS = "seconds" diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index 4bc79e7bd39c19..63f72a89ccd8aa 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -1,6 +1,6 @@ """Support for Ebusd sensors.""" -import logging import datetime +import logging from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util From 5d94c821751021251b5c6e1d2d185972de985f11 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 20 Oct 2019 20:17:27 +0100 Subject: [PATCH 1086/3953] isort the geniushub code (#27978) --- homeassistant/components/geniushub/__init__.py | 5 ++--- homeassistant/components/geniushub/climate.py | 8 ++++---- homeassistant/components/geniushub/water_heater.py | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 692c72e5776cba..b34c46a9f26fde 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -4,9 +4,8 @@ from typing import Any, Dict, Optional import aiohttp -import voluptuous as vol - from geniushubclient import GeniusHub +import voluptuous as vol from homeassistant.const import ( ATTR_TEMPERATURE, @@ -22,8 +21,8 @@ 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, async_dispatcher_connect, + async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index f27b1cc7f1aebc..5acc25a36ee96c 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,14 +1,14 @@ """Support for Genius Hub climate devices.""" -from typing import Optional, List +from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_OFF, HVAC_MODE_HEAT, - PRESET_BOOST, + HVAC_MODE_OFF, PRESET_ACTIVITY, - SUPPORT_TARGET_TEMPERATURE, + PRESET_BOOST, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.helpers.typing import ConfigType, HomeAssistantType diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index cd4f536e14fa12..4141e9f8c04562 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -2,9 +2,9 @@ from typing import List from homeassistant.components.water_heater import ( - WaterHeaterDevice, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterDevice, ) from homeassistant.const import STATE_OFF from homeassistant.helpers.typing import ConfigType, HomeAssistantType From bce9f14751167bfbffaa4cb315ceb0a456887981 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 20 Oct 2019 20:20:53 +0100 Subject: [PATCH 1087/3953] isort the evohome code (#27977) --- homeassistant/components/evohome/__init__.py | 6 +++--- homeassistant/components/evohome/climate.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index f86bf9742053f4..29f89dc08d60c5 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -8,9 +8,9 @@ from typing import Any, Dict, Optional, Tuple import aiohttp.client_exceptions -import voluptuous as vol -import evohomeasync2 import evohomeasync +import evohomeasync2 +import voluptuous as vol from homeassistant.const import ( CONF_PASSWORD, @@ -28,7 +28,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util -from .const import DOMAIN, EVO_FOLLOW, STORAGE_VERSION, STORAGE_KEY, GWS, TCS +from .const import DOMAIN, EVO_FOLLOW, GWS, STORAGE_KEY, STORAGE_VERSION, TCS _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index eb7f3f7d7d8a61..82a7001539df72 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -1,6 +1,6 @@ """Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems.""" import logging -from typing import Optional, List +from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -14,26 +14,26 @@ PRESET_ECO, PRESET_HOME, PRESET_NONE, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import PRECISION_TENTHS from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import CONF_LOCATION_IDX, EvoDevice, EvoChild +from . import CONF_LOCATION_IDX, EvoChild, EvoDevice from .const import ( DOMAIN, - EVO_RESET, EVO_AUTO, EVO_AUTOECO, EVO_AWAY, EVO_CUSTOM, EVO_DAYOFF, - EVO_HEATOFF, EVO_FOLLOW, - EVO_TEMPOVER, + EVO_HEATOFF, EVO_PERMOVER, + EVO_RESET, + EVO_TEMPOVER, ) _LOGGER = logging.getLogger(__name__) From 5fa8c02e6413ad9b20eda5b0dc8659999e246cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:21:17 -0300 Subject: [PATCH 1088/3953] Move imports in futurenow component (#27991) --- homeassistant/components/futurenow/light.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index eba768f82e3ab4..7b9e79dbb3eb9f 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -2,15 +2,16 @@ import logging +import pyfnip import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT, CONF_DEVICES from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -68,8 +69,6 @@ class FutureNowLight(Light): def __init__(self, device): """Initialize the light.""" - import pyfnip - self._name = device["name"] self._dimmable = device["dimmable"] self._channel = device["channel"] From dd4075d4957e2c989ee9563b1164bc8ac2cafad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:22:51 -0300 Subject: [PATCH 1089/3953] Move imports in frontier_silicon component (#27990) --- homeassistant/components/frontier_silicon/media_player.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 8ab379b050b69e..010420d0f98840 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -1,9 +1,11 @@ """Support for Frontier Silicon Devices (Medion, Hama, Auna,...).""" import logging +from afsapi import AFSAPI +import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -64,8 +66,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Frontier Silicon platform.""" - import requests - if discovery_info is not None: async_add_entities( [AFSAPIDevice(discovery_info["ssdp_description"], DEFAULT_PASSWORD)], True @@ -118,8 +118,6 @@ def fs_device(self): connected to the device in between the updates and invalidated the existing session (i.e UNDOK). """ - from afsapi import AFSAPI - return AFSAPI(self._device_url, self._password) @property From ab2d1ee134719d77a8e8181c122f54d8cafdacd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:23:14 -0300 Subject: [PATCH 1090/3953] Move imports in gc100 component (#27993) --- homeassistant/components/gc100/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gc100/__init__.py b/homeassistant/components/gc100/__init__.py index 19303fdc6d2e2c..36779b28df29a7 100644 --- a/homeassistant/components/gc100/__init__.py +++ b/homeassistant/components/gc100/__init__.py @@ -1,9 +1,10 @@ """Support for controlling Global Cache gc100.""" import logging +import gc100 import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -31,8 +32,6 @@ # pylint: disable=no-member def setup(hass, base_config): """Set up the gc100 component.""" - import gc100 - config = base_config[DOMAIN] host = config[CONF_HOST] port = config[CONF_PORT] From 92ed89969c768e5879953dee109250ffee67466a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:28:23 -0300 Subject: [PATCH 1091/3953] Move imports in gntp component (#27994) --- homeassistant/components/gntp/notify.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/gntp/notify.py b/homeassistant/components/gntp/notify.py index 48c02cf0ba88be..5c05b097a1fd8a 100644 --- a/homeassistant/components/gntp/notify.py +++ b/homeassistant/components/gntp/notify.py @@ -2,17 +2,18 @@ import logging import os +import gntp.errors +import gntp.notifier import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_PORT -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_PASSWORD, CONF_PORT +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -69,9 +70,6 @@ class GNTPNotificationService(BaseNotificationService): def __init__(self, app_name, app_icon, hostname, password, port): """Initialize the service.""" - import gntp.notifier - import gntp.errors - self.gntp = gntp.notifier.GrowlNotifier( applicationName=app_name, notifications=["Notification"], From bf6a30d1bbd57132f76067541b89e242535954d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:35:42 -0300 Subject: [PATCH 1092/3953] Move imports in goalfeed component (#27995) --- homeassistant/components/goalfeed/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/goalfeed/__init__.py b/homeassistant/components/goalfeed/__init__.py index 3a14eb2831d49b..cdca99e0309758 100644 --- a/homeassistant/components/goalfeed/__init__.py +++ b/homeassistant/components/goalfeed/__init__.py @@ -1,11 +1,12 @@ """Component for the Goalfeed service.""" import json +import pysher import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv # Version downgraded due to regression in library # For details: https://github.com/nlsdfnbch/Pysher/issues/38 @@ -30,8 +31,6 @@ def setup(hass, config): """Set up the Goalfeed component.""" - import pysher - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) From ef8f88e25a95ed5104a9b6c15e843b75f11994e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:36:43 -0300 Subject: [PATCH 1093/3953] Move imports in everlights component (#27983) --- homeassistant/components/everlights/light.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 506617e4c6028d..f7fa9deffa07fd 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -1,25 +1,26 @@ """Support for EverLights lights.""" -import logging from datetime import timedelta +import logging from typing import Tuple +import pyeverlights import voluptuous as vol -from homeassistant.const import CONF_HOSTS from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, ATTR_EFFECT, + ATTR_HS_COLOR, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, SUPPORT_COLOR, + SUPPORT_EFFECT, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_HOSTS +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) @@ -46,8 +47,6 @@ def color_int_to_rgb(value: int) -> Tuple[int, int, int]: async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the EverLights lights from configuration.yaml.""" - import pyeverlights - lights = [] for ipaddr in config[CONF_HOSTS]: @@ -159,8 +158,6 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Synchronize state with control box.""" - import pyeverlights - try: self._status = await self._api.get_status() except pyeverlights.ConnectionError: From ce00d06cbdd66364bf9a23fedb0a005d45828de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:37:52 -0300 Subject: [PATCH 1094/3953] Move imports in elkm1 component (#27982) --- homeassistant/components/elkm1/__init__.py | 9 ++++---- .../components/elkm1/alarm_control_panel.py | 15 +----------- homeassistant/components/elkm1/climate.py | 23 ++----------------- homeassistant/components/elkm1/sensor.py | 23 +++++++------------ 4 files changed, 15 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index d15399df67b15a..d257c46839cc34 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -2,7 +2,10 @@ import logging import re +import elkm1_lib as elkm1 +from elkm1_lib.const import Max import voluptuous as vol + from homeassistant.const import ( CONF_EXCLUDE, CONF_HOST, @@ -12,8 +15,7 @@ CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback # noqa -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType # noqa @@ -125,9 +127,6 @@ def _has_all_unique_prefixes(value): async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: """Set up the Elk M1 platform.""" - from elkm1_lib.const import Max - import elkm1_lib as elkm1 - devices = {} elk_datas = {} diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 927ed53115e5e6..38519ab5b3f77a 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -1,4 +1,5 @@ """Each ElkM1 area will be created as a separate alarm_control_panel.""" +from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm @@ -93,8 +94,6 @@ def _display_message_service(service): def _arm_services(): - from elkm1_lib.const import ArmLevel - return { "elkm1_alarm_arm_vacation": ArmLevel.ARMED_VACATION.value, "elkm1_alarm_arm_home_instant": ArmLevel.ARMED_STAY_INSTANT.value, @@ -147,8 +146,6 @@ def state(self): @property def device_state_attributes(self): """Attributes of the area.""" - from elkm1_lib.const import AlarmState, ArmedStatus, ArmUpState - attrs = self.initial_attrs() elmt = self._element attrs["is_exit"] = elmt.is_exit @@ -164,8 +161,6 @@ def device_state_attributes(self): return attrs def _element_changed(self, element, changeset): - from elkm1_lib.const import ArmedStatus - elk_state_to_hass_state = { ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED, ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY, @@ -191,8 +186,6 @@ def _entry_exit_timer_is_running(self): return self._element.timer1 > 0 or self._element.timer2 > 0 def _area_is_in_alarm_state(self): - from elkm1_lib.const import AlarmState - return self._element.alarm_state >= AlarmState.FIRE_ALARM.value async def async_alarm_disarm(self, code=None): @@ -201,20 +194,14 @@ async def async_alarm_disarm(self, code=None): async def async_alarm_arm_home(self, code=None): """Send arm home command.""" - from elkm1_lib.const import ArmLevel - self._element.arm(ArmLevel.ARMED_STAY.value, int(code)) async def async_alarm_arm_away(self, code=None): """Send arm away command.""" - from elkm1_lib.const import ArmLevel - self._element.arm(ArmLevel.ARMED_AWAY.value, int(code)) async def async_alarm_arm_night(self, code=None): """Send arm night command.""" - from elkm1_lib.const import ArmLevel - self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code)) async def _arm_service(self, arm_level, code): diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 58273e7122245c..abc9dc0933c57e 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -1,4 +1,6 @@ """Support for control of Elk-M1 connected thermostats.""" +from elkm1_lib.const import ThermostatFan, ThermostatMode, ThermostatSetting + from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, @@ -16,7 +18,6 @@ from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities - SUPPORT_HVAC = [ HVAC_MODE_OFF, HVAC_MODE_HEAT, @@ -67,8 +68,6 @@ def current_temperature(self): @property def target_temperature(self): """Return the temperature we are trying to reach.""" - from elkm1_lib.const import ThermostatMode - if (self._element.mode == ThermostatMode.HEAT.value) or ( self._element.mode == ThermostatMode.EMERGENCY_HEAT.value ): @@ -115,8 +114,6 @@ def precision(self): @property def is_aux_heat(self): """Return if aux heater is on.""" - from elkm1_lib.const import ThermostatMode - return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value @property @@ -132,8 +129,6 @@ def max_temp(self): @property def fan_mode(self): """Return the fan setting.""" - from elkm1_lib.const import ThermostatFan - if self._element.fan == ThermostatFan.AUTO.value: return HVAC_MODE_AUTO if self._element.fan == ThermostatFan.ON.value: @@ -141,8 +136,6 @@ def fan_mode(self): return None def _elk_set(self, mode, fan): - from elkm1_lib.const import ThermostatSetting - if mode is not None: self._element.set(ThermostatSetting.MODE.value, mode) if fan is not None: @@ -150,8 +143,6 @@ def _elk_set(self, mode, fan): async def async_set_hvac_mode(self, hvac_mode): """Set thermostat operation mode.""" - from elkm1_lib.const import ThermostatFan, ThermostatMode - settings = { HVAC_MODE_OFF: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value), HVAC_MODE_HEAT: (ThermostatMode.HEAT.value, None), @@ -163,14 +154,10 @@ async def async_set_hvac_mode(self, hvac_mode): async def async_turn_aux_heat_on(self): """Turn auxiliary heater on.""" - from elkm1_lib.const import ThermostatMode - self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None) async def async_turn_aux_heat_off(self): """Turn auxiliary heater off.""" - from elkm1_lib.const import ThermostatMode - self._elk_set(ThermostatMode.HEAT.value, None) @property @@ -180,8 +167,6 @@ def fan_modes(self): async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" - from elkm1_lib.const import ThermostatFan - if fan_mode == HVAC_MODE_AUTO: self._elk_set(None, ThermostatFan.AUTO.value) elif fan_mode == STATE_ON: @@ -189,8 +174,6 @@ async def async_set_fan_mode(self, fan_mode): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - from elkm1_lib.const import ThermostatSetting - low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) if low_temp is not None: @@ -199,8 +182,6 @@ async def async_set_temperature(self, **kwargs): self._element.set(ThermostatSetting.COOL_SETPOINT.value, round(high_temp)) def _element_changed(self, element, changeset): - from elkm1_lib.const import ThermostatFan, ThermostatMode - mode_to_state = { ThermostatMode.OFF.value: HVAC_MODE_OFF, ThermostatMode.COOL.value: HVAC_MODE_COOL, diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 3f524b778db131..3ed5356f4de136 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -1,4 +1,12 @@ """Support for control of ElkM1 sensors.""" +from elkm1_lib.const import ( + SettingFormat, + ZoneLogicalStatus, + ZonePhysicalStatus, + ZoneType, +) +from elkm1_lib.util import pretty_const, username + from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities @@ -79,8 +87,6 @@ def icon(self): @property def device_state_attributes(self): """Attributes of the sensor.""" - from elkm1_lib.util import username - attrs = self.initial_attrs() attrs["area"] = self._element.area + 1 attrs["temperature"] = self._element.temperature @@ -140,8 +146,6 @@ def _element_changed(self, element, changeset): @property def device_state_attributes(self): """Attributes of the sensor.""" - from elkm1_lib.const import SettingFormat - attrs = self.initial_attrs() attrs["value_format"] = SettingFormat(self._element.value_format).name.lower() return attrs @@ -153,8 +157,6 @@ class ElkZone(ElkSensor): @property def icon(self): """Icon to use in the frontend.""" - from elkm1_lib.const import ZoneType - zone_icons = { ZoneType.FIRE_ALARM.value: "fire", ZoneType.FIRE_VERIFIED.value: "fire", @@ -181,8 +183,6 @@ def icon(self): @property def device_state_attributes(self): """Attributes of the sensor.""" - from elkm1_lib.const import ZoneLogicalStatus, ZonePhysicalStatus, ZoneType - attrs = self.initial_attrs() attrs["physical_status"] = ZonePhysicalStatus( self._element.physical_status @@ -199,8 +199,6 @@ def device_state_attributes(self): @property def temperature_unit(self): """Return the temperature unit.""" - from elkm1_lib.const import ZoneType - if self._element.definition == ZoneType.TEMPERATURE.value: return self._temperature_unit return None @@ -208,8 +206,6 @@ def temperature_unit(self): @property def unit_of_measurement(self): """Return the unit of measurement.""" - from elkm1_lib.const import ZoneType - if self._element.definition == ZoneType.TEMPERATURE.value: return self._temperature_unit if self._element.definition == ZoneType.ANALOG_ZONE.value: @@ -217,9 +213,6 @@ def unit_of_measurement(self): return None def _element_changed(self, element, changeset): - from elkm1_lib.const import ZoneLogicalStatus, ZoneType - from elkm1_lib.util import pretty_const - if self._element.definition == ZoneType.TEMPERATURE.value: self._state = temperature_to_state(self._element.temperature, -60) elif self._element.definition == ZoneType.ANALOG_ZONE.value: From 87c0207163538192cdb824c6bd91b35812db64d2 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sun, 20 Oct 2019 23:38:45 +0200 Subject: [PATCH 1095/3953] Move imports in osramlightify component (#27985) --- homeassistant/components/osramlightify/light.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 9a2da2bce061b6..05064861844a20 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -3,14 +3,15 @@ import random import socket +from lightify import Lightify import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - ATTR_EFFECT, EFFECT_RANDOM, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, @@ -20,7 +21,6 @@ SUPPORT_TRANSITION, Light, ) - from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -71,11 +71,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Osram Lightify lights.""" - import lightify - host = config[CONF_HOST] try: - bridge = lightify.Lightify(host, log_level=logging.NOTSET) + bridge = Lightify(host, log_level=logging.NOTSET) except socket.error as err: msg = "Error connecting to bridge: {} due to: {}".format(host, str(err)) _LOGGER.exception(msg) From bb381d6060c6a675221e34829e70f4fba7e6a160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:39:24 -0300 Subject: [PATCH 1096/3953] Move imports in eliqonline component (#27980) --- homeassistant/components/eliqonline/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index 1f21263a4d60e2..b3d56e42325e0d 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -1,15 +1,16 @@ """Monitors home energy use for the ELIQ Online service.""" +import asyncio from datetime import timedelta import logging -import asyncio +import eliqonline import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, POWER_WATT -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -34,8 +35,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the ELIQ Online sensor.""" - import eliqonline - access_token = config.get(CONF_ACCESS_TOKEN) name = config.get(CONF_NAME, DEFAULT_NAME) channel_id = config.get(CONF_CHANNEL_ID) From a13f8a1781a817fa16252fab887636f1bf7388f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 19:04:56 -0300 Subject: [PATCH 1097/3953] Move imports in frontend component (#27988) --- homeassistant/components/frontend/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index e46423c8271c96..541d1bf473d859 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -6,23 +6,23 @@ import pathlib from typing import Any, Dict, Optional, Set, Tuple -from aiohttp import web, web_urldispatcher, hdrs -import voluptuous as vol +from aiohttp import hdrs, web, web_urldispatcher +import hass_frontend import jinja2 +import voluptuous as vol from yarl import URL -import homeassistant.helpers.config_validation as cv -from homeassistant.components.http.view import HomeAssistantView from homeassistant.components import websocket_api +from homeassistant.components.http.view import HomeAssistantView from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage - # mypy: allow-untyped-defs, no-check-untyped-defs # Fix mimetypes for borked Windows machines @@ -242,8 +242,6 @@ def _frontend_root(dev_repo_path): if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" - import hass_frontend - return hass_frontend.where() From ff3c0e56974ba37f633bd4f08a832da7c17a6e04 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 21 Oct 2019 00:32:17 +0000 Subject: [PATCH 1098/3953] [ci skip] Translation update --- .../components/abode/.translations/fr.json | 2 +- .../components/abode/.translations/ko.json | 22 +++++++++++++++++++ .../components/adguard/.translations/ca.json | 2 ++ .../components/adguard/.translations/ko.json | 2 ++ .../components/adguard/.translations/no.json | 2 ++ .../components/adguard/.translations/pl.json | 2 ++ .../components/adguard/.translations/ru.json | 2 ++ .../adguard/.translations/zh-Hant.json | 2 ++ .../alarm_control_panel/.translations/ko.json | 11 ++++++++++ .../components/auth/.translations/ko.json | 2 +- .../components/axis/.translations/ko.json | 1 + .../components/cast/.translations/ko.json | 10 ++++----- .../components/cover/.translations/ko.json | 10 +++++++++ .../components/deconz/.translations/ko.json | 1 + .../components/hangouts/.translations/ko.json | 6 ++--- .../components/lock/.translations/ko.json | 13 +++++++++++ .../mobile_app/.translations/ko.json | 2 +- .../opentherm_gw/.translations/ko.json | 13 ++++++++++- .../components/soma/.translations/ko.json | 10 +++++++++ 19 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/abode/.translations/ko.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/ko.json create mode 100644 homeassistant/components/cover/.translations/ko.json create mode 100644 homeassistant/components/lock/.translations/ko.json diff --git a/homeassistant/components/abode/.translations/fr.json b/homeassistant/components/abode/.translations/fr.json index c0d9b0b577b753..c0c2a35081b150 100644 --- a/homeassistant/components/abode/.translations/fr.json +++ b/homeassistant/components/abode/.translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Une seule configuration de Abode est autoris\u00e9e." + "single_instance_allowed": "Une seule configuration d'Abode est autoris\u00e9e." }, "error": { "connection_error": "Impossible de se connecter \u00e0 Abode.", diff --git a/homeassistant/components/abode/.translations/ko.json b/homeassistant/components/abode/.translations/ko.json new file mode 100644 index 00000000000000..9560dde6b3d9aa --- /dev/null +++ b/homeassistant/components/abode/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\ud558\ub098\uc758 Abode \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "Abode \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + }, + "title": "Abode \uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/ca.json b/homeassistant/components/adguard/.translations/ca.json index 30fd509cb7a3bb..9b7b3c39b03cf2 100644 --- a/homeassistant/components/adguard/.translations/ca.json +++ b/homeassistant/components/adguard/.translations/ca.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Aquesta integraci\u00f3 necessita la versi\u00f3 d'AdGuard Home {minimal_version} o una superior, tens la {current_version}. Actualitza el complement de Hass.io d'AdGuard Home.", + "adguard_home_outdated": "Aquesta integraci\u00f3 necessita la versi\u00f3 d'AdGuard Home {minimal_version} o una superior, tens la {current_version}.", "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.", "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'AdGuard Home." }, diff --git a/homeassistant/components/adguard/.translations/ko.json b/homeassistant/components/adguard/.translations/ko.json index bb93d675103da0..e1f39259292424 100644 --- a/homeassistant/components/adguard/.translations/ko.json +++ b/homeassistant/components/adguard/.translations/ko.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 AdGuard Home {minimal_version} \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud604\uc7ac \ubc84\uc804\uc740 {current_version} \uc785\ub2c8\ub2e4. Hass.io AdGuard Home \uc560\ub4dc\uc628\uc744 \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\uc138\uc694.", + "adguard_home_outdated": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 AdGuard Home {minimal_version} \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud604\uc7ac \ubc84\uc804\uc740 {current_version} \uc785\ub2c8\ub2e4.", "existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.", "single_instance_allowed": "\ud558\ub098\uc758 AdGuard Home \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/adguard/.translations/no.json b/homeassistant/components/adguard/.translations/no.json index 2cd6cd72f6d3e7..22a8c23644f6e0 100644 --- a/homeassistant/components/adguard/.translations/no.json +++ b/homeassistant/components/adguard/.translations/no.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Denne integrasjonen krever AdGuard Home {minimal_version} eller h\u00f8yere, du har {current_version}. Vennligst oppdater Hass.io AdGuard Home-tillegget.", + "adguard_home_outdated": "Denne integrasjonen krever AdGuard Home {minimal_version} eller h\u00f8yere, du har {current_version}.", "existing_instance_updated": "Oppdatert eksisterende konfigurasjon.", "single_instance_allowed": "Kun en konfigurasjon av AdGuard Hjemer tillatt." }, diff --git a/homeassistant/components/adguard/.translations/pl.json b/homeassistant/components/adguard/.translations/pl.json index f8f64d542608fe..69ba6b024e2d12 100644 --- a/homeassistant/components/adguard/.translations/pl.json +++ b/homeassistant/components/adguard/.translations/pl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Ta integracja wymaga AdGuard Home {minimal_version} lub nowszej wersji, masz {current_version}. Zaktualizuj sw\u00f3j dodatek Hass.io AdGuard Home.", + "adguard_home_outdated": "Ta integracja wymaga AdGuard Home {minimal_version} lub nowszej wersji, masz {current_version}.", "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119.", "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home." }, diff --git a/homeassistant/components/adguard/.translations/ru.json b/homeassistant/components/adguard/.translations/ru.json index c50d0197351e76..eca46d7db00942 100644 --- a/homeassistant/components/adguard/.translations/ru.json +++ b/homeassistant/components/adguard/.translations/ru.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 AdGuard Home \u0432\u0435\u0440\u0441\u0438\u0438 {current_version}. \u0414\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u044b \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f {minimal_version}, \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0430\u044f. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 Hass.io.", + "adguard_home_outdated": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 AdGuard Home \u0432\u0435\u0440\u0441\u0438\u0438 {current_version}. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0432\u0435\u0440\u0441\u0438\u044e {minimal_version} \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0443\u044e.", "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, diff --git a/homeassistant/components/adguard/.translations/zh-Hant.json b/homeassistant/components/adguard/.translations/zh-Hant.json index a693652fedf5c0..d08a5715a8e81e 100644 --- a/homeassistant/components/adguard/.translations/zh-Hant.json +++ b/homeassistant/components/adguard/.translations/zh-Hant.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "\u6574\u5408\u9700\u8981 AdGuard Home {minimal_version} \u6216\u66f4\u65b0\u7248\u672c\uff0c\u60a8\u76ee\u524d\u4f7f\u7528\u7248\u672c\u70ba {current_version}\u3002\u8acb\u66f4\u65b0 Hass.io AdGuard Home \u5143\u4ef6\u3002", + "adguard_home_outdated": "\u6574\u5408\u9700\u8981 AdGuard Home {minimal_version} \u6216\u66f4\u65b0\u7248\u672c\uff0c\u60a8\u76ee\u524d\u4f7f\u7528\u7248\u672c\u70ba {current_version}\u3002", "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002", "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 AdGuard Home\u3002" }, diff --git a/homeassistant/components/alarm_control_panel/.translations/ko.json b/homeassistant/components/alarm_control_panel/.translations/ko.json new file mode 100644 index 00000000000000..5d6caa5fe1245f --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/ko.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} \uc678\ucd9c\uacbd\ube44", + "arm_home": "{entity_name} \uc7ac\uc2e4\uacbd\ube44", + "arm_night": "{entity_name} \uc57c\uac04\uacbd\ube44", + "disarm": "{entity_name} \uacbd\ube44\ud574\uc81c", + "trigger": "{entity_name} \ud2b8\ub9ac\uac70" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 1cb70519b20f45..6c2e8988d83c58 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [\uad6c\uae00 OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, diff --git a/homeassistant/components/axis/.translations/ko.json b/homeassistant/components/axis/.translations/ko.json index 5ceaa0828103ee..f02b7cdcefa5e8 100644 --- a/homeassistant/components/axis/.translations/ko.json +++ b/homeassistant/components/axis/.translations/ko.json @@ -12,6 +12,7 @@ "device_unavailable": "\uae30\uae30\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "faulty_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, + "flow_title": "Axis \uae30\uae30: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json index 71dee3afec5ec1..1374372aa24be9 100644 --- a/homeassistant/components/cast/.translations/ko.json +++ b/homeassistant/components/cast/.translations/ko.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "no_devices_found": "\uad6c\uae00 \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 \uad6c\uae00 \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "Google \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 Google \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "step": { "confirm": { - "description": "\uad6c\uae00 \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8" + "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Google \uce90\uc2a4\ud2b8" } }, - "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8" + "title": "Google \uce90\uc2a4\ud2b8" } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json new file mode 100644 index 00000000000000..02f900a8fe50a6 --- /dev/null +++ b/homeassistant/components/cover/.translations/ko.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", + "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud799\ub2c8\ub2e4", + "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", + "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index ef8d3910ecfac1..61725316b137b9 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -11,6 +11,7 @@ "error": { "no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, + "flow_title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774 ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/hangouts/.translations/ko.json b/homeassistant/components/hangouts/.translations/ko.json index 3b1c755b3588c1..385fc128b3b73c 100644 --- a/homeassistant/components/hangouts/.translations/ko.json +++ b/homeassistant/components/hangouts/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uad6c\uae00 \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "already_configured": "Google \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -24,9 +24,9 @@ "password": "\ube44\ubc00\ubc88\ud638" }, "description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.", - "title": "\uad6c\uae00 \ud589\uc544\uc6c3 \ub85c\uadf8\uc778" + "title": "Google \ud589\uc544\uc6c3 \ub85c\uadf8\uc778" } }, - "title": "\uad6c\uae00 \ud589\uc544\uc6c3" + "title": "Google \ud589\uc544\uc6c3" } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ko.json b/homeassistant/components/lock/.translations/ko.json new file mode 100644 index 00000000000000..6abd9cd60e6449 --- /dev/null +++ b/homeassistant/components/lock/.translations/ko.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "{entity_name} \uc7a0\uae08", + "open": "{entity_name} \uc5f4\uae30", + "unlock": "{entity_name} \uc7a0\uae08 \ud574\uc81c" + }, + "condition_type": { + "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4", + "is_unlocked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/.translations/ko.json b/homeassistant/components/mobile_app/.translations/ko.json index faf30e5f985ea2..899845fcc2efc7 100644 --- a/homeassistant/components/mobile_app/.translations/ko.json +++ b/homeassistant/components/mobile_app/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "\ubaa8\ubc14\uc77c \uc571\uc744 \uc5f4\uc5b4 Home Assistant \uc640 \ud1b5\ud569\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uc548\ub0b4]({apps_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "install_app": "\ubaa8\ubc14\uc77c \uc571\uc744 \uc5f4\uc5b4 Home Assistant \uc640 \uc5f0\ub3d9\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uc548\ub0b4]({apps_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "confirm": { diff --git a/homeassistant/components/opentherm_gw/.translations/ko.json b/homeassistant/components/opentherm_gw/.translations/ko.json index e5daf826ee519d..f370427625d177 100644 --- a/homeassistant/components/opentherm_gw/.translations/ko.json +++ b/homeassistant/components/opentherm_gw/.translations/ko.json @@ -10,7 +10,7 @@ "init": { "data": { "device": "\uacbd\ub85c \ub610\ub294 URL", - "floor_temperature": "\uc2e4\ub0b4\uc628\ub3c4 \uc18c\uc218\uc810 \ub0b4\ub9bc", + "floor_temperature": "\uc2e4\ub0b4\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", "id": "ID", "name": "\uc774\ub984", "precision": "\uc2e4\ub0b4\uc628\ub3c4 \uc815\ubc00\ub3c4" @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", + "precision": "\uc815\ubc00\ub3c4" + }, + "description": "OpenTherm Gateway \uc635\uc158" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ko.json b/homeassistant/components/soma/.translations/ko.json index 53146bebf83c16..90995ebc9f2110 100644 --- a/homeassistant/components/soma/.translations/ko.json +++ b/homeassistant/components/soma/.translations/ko.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Soma \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "description": "SOMA Connect \uc640\uc758 \uc5f0\uacb0 \uc124\uc815\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file From 4bb82fa8adea2fe5c7f94abe7e85ab7ed77aef5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:48:25 -0300 Subject: [PATCH 1099/3953] Move imports in message_bird component (#28022) --- homeassistant/components/message_bird/notify.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/message_bird/notify.py b/homeassistant/components/message_bird/notify.py index 5df02ef69c45cb..ce1d275a8320bc 100644 --- a/homeassistant/components/message_bird/notify.py +++ b/homeassistant/components/message_bird/notify.py @@ -1,16 +1,17 @@ """MessageBird platform for notify component.""" import logging +import messagebird +from messagebird.client import ErrorException import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_SENDER -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY, CONF_SENDER +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -26,8 +27,6 @@ def get_service(hass, config, discovery_info=None): """Get the MessageBird notification service.""" - import messagebird - client = messagebird.Client(config[CONF_API_KEY]) try: # validates the api key @@ -49,8 +48,6 @@ def __init__(self, sender, client): def send_message(self, message=None, **kwargs): """Send a message to a specified target.""" - from messagebird.client import ErrorException - targets = kwargs.get(ATTR_TARGET) if not targets: _LOGGER.error("No target specified") From ad39b957d6232a8de739d5c5007abe603d28b784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:52:49 -0300 Subject: [PATCH 1100/3953] Move imports in mopar component (#28028) --- homeassistant/components/mopar/__init__.py | 13 +++---------- homeassistant/components/mopar/sensor.py | 4 ++-- homeassistant/components/mopar/switch.py | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mopar/__init__.py b/homeassistant/components/mopar/__init__.py index 857dbab2a3b08a..21a3c3d16ea190 100644 --- a/homeassistant/components/mopar/__init__.py +++ b/homeassistant/components/mopar/__init__.py @@ -1,17 +1,18 @@ """Support for Mopar vehicles.""" -import logging from datetime import timedelta +import logging +import motorparts import voluptuous as vol from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_PIN, CONF_SCAN_INTERVAL, + CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -53,8 +54,6 @@ def setup(hass, config): """Set up the Mopar component.""" - import motorparts - conf = config[DOMAIN] cookie = hass.config.path(COOKIE_FILE) try: @@ -101,8 +100,6 @@ def __init__(self, hass, session): def update(self, now, **kwargs): """Update data.""" - import motorparts - _LOGGER.debug("Updating vehicle data") try: self.vehicles = motorparts.get_summary(self._session)["vehicles"] @@ -123,8 +120,6 @@ def update(self, now, **kwargs): @property def attribution(self): """Get the attribution string from Mopar.""" - import motorparts - return motorparts.ATTRIBUTION def get_vehicle_name(self, index): @@ -136,8 +131,6 @@ def get_vehicle_name(self, index): def actuate(self, command, index): """Run a command on the specified Mopar vehicle.""" - import motorparts - try: response = getattr(motorparts, command)(self._session, index) except motorparts.MoparError as error: diff --git a/homeassistant/components/mopar/sensor.py b/homeassistant/components/mopar/sensor.py index a29e9c5c73981d..2243fcdaa22812 100644 --- a/homeassistant/components/mopar/sensor.py +++ b/homeassistant/components/mopar/sensor.py @@ -1,8 +1,8 @@ """Support for the Mopar vehicle sensor platform.""" from homeassistant.components.mopar import ( - DOMAIN as MOPAR_DOMAIN, - DATA_UPDATED, ATTR_VEHICLE_INDEX, + DATA_UPDATED, + DOMAIN as MOPAR_DOMAIN, ) from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_KILOMETERS from homeassistant.core import callback diff --git a/homeassistant/components/mopar/switch.py b/homeassistant/components/mopar/switch.py index bbada4ecee73e7..2dad56637ce9e6 100644 --- a/homeassistant/components/mopar/switch.py +++ b/homeassistant/components/mopar/switch.py @@ -3,7 +3,7 @@ from homeassistant.components.mopar import DOMAIN as MOPAR_DOMAIN from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON _LOGGER = logging.getLogger(__name__) From 1e27e2827d7ec200e8b9fc123d61502d5b1d625e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:53:28 -0300 Subject: [PATCH 1101/3953] Move imports in mvglive component (#28031) --- homeassistant/components/mvglive/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 3c753d832e097e..da1db0e02aa80b 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -1,14 +1,15 @@ """Support for departure information for public transport in Munich.""" -import logging +from copy import deepcopy from datetime import timedelta +import logging -from copy import deepcopy +import MVGLive import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -150,8 +151,6 @@ def __init__( self, station, destinations, directions, lines, products, timeoffset, number ): """Initialize the sensor.""" - import MVGLive - self._station = station self._destinations = destinations self._directions = directions From 6a392e13dda275a4c4332739814b5f445a63699b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:54:00 -0300 Subject: [PATCH 1102/3953] Move imports in mpd component (#28030) --- homeassistant/components/mpd/media_player.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index c19f8f49226d9f..2628815727c812 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -3,9 +3,10 @@ import logging import os +import mpd import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -85,8 +86,6 @@ class MpdDevice(MediaPlayerDevice): # pylint: disable=no-member def __init__(self, server, port, password, name): """Initialize the MPD device.""" - import mpd - self.server = server self.port = port self._name = name @@ -107,8 +106,6 @@ def __init__(self, server, port, password, name): def _connect(self): """Connect to MPD.""" - import mpd - try: self._client.connect(self.server, self.port) @@ -121,8 +118,6 @@ def _connect(self): def _disconnect(self): """Disconnect from MPD.""" - import mpd - try: self._client.disconnect() except mpd.ConnectionError: @@ -144,8 +139,6 @@ def available(self): def update(self): """Get the latest data and update the state.""" - import mpd - try: if not self._is_connected: self._connect() @@ -261,8 +254,6 @@ def select_source(self, source): @Throttle(PLAYLIST_UPDATE_INTERVAL) def _update_playlists(self, **kwargs): """Update available MPD playlists.""" - import mpd - try: self._playlists = [] for playlist_data in self._client.listplaylists(): From cc3173e3ce4dadbfa42090705036865e71c16ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:54:27 -0300 Subject: [PATCH 1103/3953] Move imports in namecheapdns component (#28034) --- homeassistant/components/namecheapdns/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/namecheapdns/__init__.py b/homeassistant/components/namecheapdns/__init__.py index 56c50ff52f8ea5..fbc78f622a147f 100644 --- a/homeassistant/components/namecheapdns/__init__.py +++ b/homeassistant/components/namecheapdns/__init__.py @@ -1,13 +1,14 @@ """Support for namecheap DNS services.""" -import logging from datetime import timedelta +import logging +import defusedxml.ElementTree as ET import voluptuous as vol +from homeassistant.const import CONF_DOMAIN, CONF_HOST, CONF_PASSWORD +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_DOMAIN from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) @@ -55,8 +56,6 @@ async def update_domain_interval(now): async def _update_namecheapdns(session, host, domain, password): """Update namecheap DNS entry.""" - import defusedxml.ElementTree as ET - params = {"host": host, "domain": domain, "password": password} resp = await session.get(UPDATE_URL, params=params) From 265a1f1fb6b8726d6cc6c129fef34a0f9bf7eb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:54:51 -0300 Subject: [PATCH 1104/3953] Move imports in neurio_energy component (#28035) --- homeassistant/components/neurio_energy/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index eac716573db7b6..894bfae6180b05 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -1,15 +1,16 @@ """Support for monitoring a Neurio energy sensor.""" -import logging from datetime import timedelta +import logging +import neurio import requests.exceptions import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, POWER_WATT, ENERGY_KILO_WATT_HOUR +from homeassistant.const import CONF_API_KEY, ENERGY_KILO_WATT_HOUR, POWER_WATT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -69,8 +70,6 @@ class NeurioData: def __init__(self, api_key, api_secret, sensor_id): """Initialize the data.""" - import neurio - self.api_key = api_key self.api_secret = api_secret self.sensor_id = sensor_id From 1342fe2b3cc87232150ccb129f9024ef99bc6f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:55:29 -0300 Subject: [PATCH 1105/3953] Move imports in openevse component (#28043) --- homeassistant/components/openevse/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index d29dec224bdc0e..0ac655cd4483f9 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -1,17 +1,18 @@ """Support for monitoring an OpenEVSE Charger.""" import logging +import openevsewifi from requests import RequestException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_CELSIUS, CONF_HOST, - ENERGY_KILO_WATT_HOUR, CONF_MONITORED_VARIABLES, + ENERGY_KILO_WATT_HOUR, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -38,8 +39,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenEVSE sensor.""" - import openevsewifi - host = config.get(CONF_HOST) monitored_variables = config.get(CONF_MONITORED_VARIABLES) From c1fccee83a805c140c8e87ec9177c89967e2b8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:55:53 -0300 Subject: [PATCH 1106/3953] Move imports in magicseaweed component (#28020) --- homeassistant/components/magicseaweed/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 66ab87a6569ffa..174ecf1882e24c 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -1,19 +1,21 @@ """Support for magicseaweed data from magicseaweed.com.""" from datetime import timedelta import logging + +import magicseaweed import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_API_KEY, - CONF_NAME, CONF_MONITORED_CONDITIONS, - ATTR_ATTRIBUTION, + CONF_NAME, ) import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -175,8 +177,6 @@ class MagicSeaweedData: def __init__(self, api_key, spot_id, units): """Initialize the data object.""" - import magicseaweed - self._msw = magicseaweed.MSW_Forecast(api_key, spot_id, None, units) self.currently = None self.hourly = {} From ff385d5e2b45299a9009992653902cb859ae754f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:56:19 -0300 Subject: [PATCH 1107/3953] Move imports in lw12wifi component (#28019) --- homeassistant/components/lw12wifi/light.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index 3b9ccae1681b40..abf75a1e318561 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -2,6 +2,7 @@ import logging +import lw12 import voluptuous as vol from homeassistant.components.light import ( @@ -9,18 +10,17 @@ ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, SUPPORT_COLOR, + SUPPORT_EFFECT, SUPPORT_TRANSITION, + Light, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util - _LOGGER = logging.getLogger(__name__) @@ -38,8 +38,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up LW-12 WiFi LED Controller platform.""" - import lw12 - # Assign configuration variables. name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -107,8 +105,6 @@ def effect_list(self): Use the Enum element name for display. """ - import lw12 - return [effect.name.replace("_", " ").title() for effect in lw12.LW12_EFFECT] @property @@ -123,8 +119,6 @@ def shoud_poll(self) -> bool: def turn_on(self, **kwargs): """Instruct the light to turn on.""" - import lw12 - self._light.light_on() if ATTR_HS_COLOR in kwargs: self._rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) From 6de95995aaef6728ec8fdd2255cea665d1d84e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:57:31 -0300 Subject: [PATCH 1108/3953] Move imports in logbook component (#28016) --- homeassistant/components/logbook/__init__.py | 39 ++++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 3c5e828765c98d..8675f778a26ad4 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -2,12 +2,26 @@ from datetime import timedelta from itertools import groupby import logging +import time +from sqlalchemy.exc import SQLAlchemyError import voluptuous as vol -from homeassistant.loader import bind_hass from homeassistant.components import sun +from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME +from homeassistant.components.homekit.const import ( + ATTR_DISPLAY_NAME, + ATTR_VALUE, + DOMAIN as DOMAIN_HOMEKIT, + EVENT_HOMEKIT_CHANGED, +) from homeassistant.components.http import HomeAssistantView +from homeassistant.components.recorder.models import Events, States +from homeassistant.components.recorder.util import ( + QUERY_RETRY_WAIT, + RETRIES, + session_scope, +) from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, @@ -16,26 +30,21 @@ ATTR_SERVICE, CONF_EXCLUDE, CONF_INCLUDE, + EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, - EVENT_STATE_CHANGED, - EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED, + EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME, STATE_OFF, STATE_ON, ) from homeassistant.core import DOMAIN as HA_DOMAIN, State, callback, split_entity_id -from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME -from homeassistant.components.homekit.const import ( - ATTR_DISPLAY_NAME, - ATTR_VALUE, - DOMAIN as DOMAIN_HOMEKIT, - EVENT_HOMEKIT_CHANGED, -) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import generate_filter +from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -371,11 +380,6 @@ def humanify(hass, events): def _get_related_entity_ids(session, entity_filter): - from homeassistant.components.recorder.models import States - from homeassistant.components.recorder.util import RETRIES, QUERY_RETRY_WAIT - from sqlalchemy.exc import SQLAlchemyError - import time - timer_start = time.perf_counter() query = session.query(States).with_entities(States.entity_id).distinct() @@ -402,8 +406,6 @@ def _get_related_entity_ids(session, entity_filter): def _generate_filter_from_config(config): - from homeassistant.helpers.entityfilter import generate_filter - excluded_entities = [] excluded_domains = [] included_entities = [] @@ -425,9 +427,6 @@ def _generate_filter_from_config(config): def _get_events(hass, config, start_day, end_day, entity_id=None): """Get events for a period of time.""" - from homeassistant.components.recorder.models import Events, States - from homeassistant.components.recorder.util import session_scope - entities_filter = _generate_filter_from_config(config) def yield_events(query): From e19663f172d54314455b9ebf19500ddd5f7eb4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:58:22 -0300 Subject: [PATCH 1109/3953] Move imports in lirc component (#28015) --- homeassistant/components/lirc/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index 47814d00e9ada3..bfc8e455624286 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -1,12 +1,13 @@ """Support for LIRC devices.""" # pylint: disable=no-member, import-error +import logging import threading import time -import logging +import lirc import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -23,8 +24,6 @@ def setup(hass, config): """Set up the LIRC capability.""" - import lirc - # blocking=True gives unexpected behavior (multiple responses for 1 press) # also by not blocking, we allow hass to shut down the thread gracefully # on exit. @@ -61,8 +60,6 @@ def __init__(self, hass): def run(self): """Run the loop of the LIRC interface thread.""" - import lirc - _LOGGER.debug("LIRC interface thread started") while not self.stopped.isSet(): try: From 67f7146cab6ccaed05b3235992a44534d431fe6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:58:46 -0300 Subject: [PATCH 1110/3953] Move imports in linode component (#28014) --- homeassistant/components/linode/__init__.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/linode/__init__.py b/homeassistant/components/linode/__init__.py index 6f590c33e08b20..a18b63d7226c84 100644 --- a/homeassistant/components/linode/__init__.py +++ b/homeassistant/components/linode/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import linode import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN @@ -35,8 +36,6 @@ def setup(hass, config): """Set up the Linode component.""" - import linode - conf = config[DOMAIN] access_token = conf.get(CONF_ACCESS_TOKEN) @@ -58,16 +57,12 @@ class Linode: def __init__(self, access_token): """Initialize the Linode connection.""" - import linode - self._access_token = access_token self.data = None self.manager = linode.LinodeClient(token=self._access_token) def get_node_id(self, node_name): """Get the status of a Linode Instance.""" - import linode - node_id = None try: @@ -83,8 +78,6 @@ def get_node_id(self, node_name): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Use the data from Linode API.""" - import linode - try: self.data = self.manager.linode.get_instances() except linode.errors.ApiError as _ex: From 96509c0c0bc61ffa4b81a77e0840361f10f01c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:58:59 -0300 Subject: [PATCH 1111/3953] Move imports in oasa_telematics component (#28039) --- homeassistant/components/oasa_telematics/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 0c16f3769d5bc6..4bf6b395d5f58e 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -1,13 +1,14 @@ """Support for OASA Telematics from telematics.oasa.gr.""" -import logging from datetime import timedelta +import logging from operator import itemgetter +import oasatelematics import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util @@ -128,8 +129,6 @@ class OASATelematicsData: def __init__(self, stop_id, route_id): """Initialize the data object.""" - import oasatelematics - self.stop_id = stop_id self.route_id = route_id self.info = self.empty_result() From cf2ee1a09f550d278163797beba6c87713064c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:59:26 -0300 Subject: [PATCH 1112/3953] Move imports in iss component (#28003) --- homeassistant/components/iss/binary_sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py index 5f38c3d166e10f..002b2e958f7a1e 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/binary_sensor.py @@ -1,18 +1,19 @@ """Support for International Space Station data sensor.""" -import logging from datetime import timedelta +import logging +import pyiss import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( - CONF_NAME, - ATTR_LONGITUDE, ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_NAME, CONF_SHOW_ON_MAP, ) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -113,8 +114,6 @@ def __init__(self, latitude, longitude): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the ISS API.""" - import pyiss - try: iss = pyiss.ISS() self.is_above = iss.is_ISS_above(self.latitude, self.longitude) From fb79c45645d24902f538bf69e7dcc78c53ec914f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:59:50 -0300 Subject: [PATCH 1113/3953] Move imports in iperf3 component (#28002) --- homeassistant/components/iperf3/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index eda601b09de3f0..753ea60efa4fe3 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -1,19 +1,20 @@ """Support for Iperf3 network measurement tool.""" -import logging from datetime import timedelta +import logging +import iperf3 import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( + CONF_HOST, + CONF_HOSTS, CONF_MONITORED_CONDITIONS, CONF_PORT, - CONF_HOST, CONF_PROTOCOL, - CONF_HOSTS, CONF_SCAN_INTERVAL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -80,8 +81,6 @@ async def async_setup(hass, config): """Set up the iperf3 component.""" - import iperf3 - hass.data[DOMAIN] = {} conf = config[DOMAIN] From bbc71441a1863fc47d6450f4b1870f30ae6c467b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:02:22 -0300 Subject: [PATCH 1114/3953] Move imports in pandora component (#28045) --- homeassistant/components/pandora/media_player.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index c242670ba48b37..417903c46e0f01 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -6,6 +6,8 @@ import shutil import signal +import pexpect + from homeassistant import util from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -104,8 +106,6 @@ def state(self): def turn_on(self): """Turn the media player on.""" - import pexpect - if self._player_state != STATE_OFF: return self._pianobar = pexpect.spawn("pianobar") @@ -136,8 +136,6 @@ def turn_on(self): def turn_off(self): """Turn the media player off.""" - import pexpect - if self._pianobar is None: _LOGGER.info("Pianobar subprocess already stopped") return @@ -226,8 +224,6 @@ def select_source(self, source): def _send_station_list_command(self): """Send a station list command.""" - import pexpect - self._pianobar.send("s") try: self._pianobar.expect("Select station:", timeout=1) @@ -248,8 +244,6 @@ def update_playing_status(self): def _query_for_playing_status(self): """Query system for info about current track.""" - import pexpect - self._clear_buffer() self._pianobar.send("i") try: @@ -372,8 +366,6 @@ def _clear_buffer(self): This is necessary because there are a bunch of 00:00 in the buffer """ - import pexpect - try: while not self._pianobar.expect(".+", timeout=0.1): pass From 2f966919387927394bf0ac1b52312186985e09e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:02:59 -0300 Subject: [PATCH 1115/3953] Move imports in otp component (#28044) --- homeassistant/components/otp/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index a175155e6f24e3..3c4cd464d444b9 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -1,13 +1,14 @@ """Support for One-Time Password (OTP).""" -import time import logging +import time +import pyotp import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_TOKEN +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -41,8 +42,6 @@ class TOTPSensor(Entity): def __init__(self, name, token): """Initialize the sensor.""" - import pyotp - self._name = name self._otp = pyotp.TOTP(token) self._state = None From 56a7233e0fc006e56897b0d17c5b9776793029a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:03:24 -0300 Subject: [PATCH 1116/3953] Move imports in ohmconnect component (#28041) --- homeassistant/components/ohmconnect/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 10b622d16c9c63..a9606e25bad746 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -1,15 +1,16 @@ """Support for OhmConnect.""" -import logging from datetime import timedelta +import logging +import defusedxml.ElementTree as ET import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ def device_state_attributes(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from OhmConnect.""" - import defusedxml.ElementTree as ET - try: url = ("https://login.ohmconnect.com" "/verify-ohm-hour/{}").format( self._ohmid From e9674374a4362c23d07cf6125e20205167b2a32a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:03:49 -0300 Subject: [PATCH 1117/3953] Move imports in norway_air component (#28037) --- homeassistant/components/norway_air/air_quality.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 9b30ad5aaa86e1..8e6c13260e5b8d 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -1,14 +1,14 @@ """Sensor for checking the air quality forecast around Norway.""" +from datetime import timedelta import logging -from datetime import timedelta +import metno import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession - +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -71,8 +71,6 @@ class AirSensor(AirQualityEntity): def __init__(self, name, coordinates, forecast, session): """Initialize the sensor.""" - import metno - self._name = name self._api = metno.AirQualityData(coordinates, forecast, session) From 92ed8362ce08b4410a6043ae449db03406f3d041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:04:10 -0300 Subject: [PATCH 1118/3953] Move imports in niko_home_control component (#28036) --- homeassistant/components/niko_home_control/light.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index 4cb8495600232a..265e51d6e6720e 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import nikohomecontrol import voluptuous as vol # Import the device class from the component that you want to support @@ -20,8 +21,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Niko Home Control light platform.""" - import nikohomecontrol - host = config[CONF_HOST] try: From 38db4b0a23e2c2aaf858d0b2bd9d5ae4df819e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:04:33 -0300 Subject: [PATCH 1119/3953] Move imports in mythicbeastsdns component (#28033) --- homeassistant/components/mythicbeastsdns/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mythicbeastsdns/__init__.py b/homeassistant/components/mythicbeastsdns/__init__.py index 993b62ac48deed..d961c2e6e3d34a 100644 --- a/homeassistant/components/mythicbeastsdns/__init__.py +++ b/homeassistant/components/mythicbeastsdns/__init__.py @@ -1,10 +1,10 @@ """Support for Mythic Beasts Dynamic DNS service.""" -import logging from datetime import timedelta +import logging +import mbddns import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_DOMAIN, CONF_HOST, @@ -12,6 +12,7 @@ CONF_SCAN_INTERVAL, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -39,8 +40,6 @@ async def async_setup(hass, config): """Initialize the Mythic Beasts component.""" - import mbddns - domain = config[DOMAIN][CONF_DOMAIN] password = config[DOMAIN][CONF_PASSWORD] host = config[DOMAIN][CONF_HOST] From 206f8cef5c47c154354c0196b256b5feedf9478f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:05:05 -0300 Subject: [PATCH 1120/3953] Move imports in mychevy component (#28032) --- homeassistant/components/mychevy/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mychevy/__init__.py b/homeassistant/components/mychevy/__init__.py index 8ec83ed374bf4f..0ec4d05a623bdb 100644 --- a/homeassistant/components/mychevy/__init__.py +++ b/homeassistant/components/mychevy/__init__.py @@ -4,11 +4,11 @@ import threading import time +import mychevy.mychevy as mc import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.util import Throttle DOMAIN = "mychevy" @@ -71,8 +71,6 @@ def __init__(self, name, attr, device_class=None): def setup(hass, base_config): """Set up the mychevy component.""" - import mychevy.mychevy as mc - config = base_config.get(DOMAIN) email = config.get(CONF_USERNAME) From 3e9d28f28a92900a44a682c85d645000bcec7c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:05:41 -0300 Subject: [PATCH 1121/3953] Move imports in mobile_app component (#28027) --- homeassistant/components/mobile_app/__init__.py | 3 +-- homeassistant/components/mobile_app/binary_sensor.py | 3 +-- homeassistant/components/mobile_app/config_flow.py | 3 ++- homeassistant/components/mobile_app/const.py | 2 +- .../components/mobile_app/device_tracker.py | 9 +++++---- homeassistant/components/mobile_app/helpers.py | 12 +++++------- homeassistant/components/mobile_app/http_api.py | 12 +++++------- homeassistant/components/mobile_app/notify.py | 1 - homeassistant/components/mobile_app/sensor.py | 1 - homeassistant/components/mobile_app/webhook.py | 12 ++++-------- homeassistant/components/mobile_app/websocket_api.py | 1 - 11 files changed, 24 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 01877099201c4a..ca2a58d1f96111 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,6 +1,6 @@ """Integrates Native Apps to Home Assistant.""" -from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.components.webhook import async_register as webhook_register +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import device_registry as dr, discovery from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -20,7 +20,6 @@ STORAGE_KEY, STORAGE_VERSION, ) - from .http_api import RegistrationsView from .webhook import handle_webhook from .websocket_api import register_websocket_handlers diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 975c4c16c32cd9..73bf925553e54c 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -1,9 +1,9 @@ """Binary sensor platform for mobile_app.""" from functools import partial +from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import callback -from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( @@ -13,7 +13,6 @@ DATA_DEVICES, DOMAIN, ) - from .entity import MobileAppEntity, sensor_id diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py index 96b0a35aae2582..bc9c6167da8049 100644 --- a/homeassistant/components/mobile_app/config_flow.py +++ b/homeassistant/components/mobile_app/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Mobile App.""" from homeassistant import config_entries -from .const import DOMAIN, ATTR_DEVICE_NAME + +from .const import ATTR_DEVICE_NAME, DOMAIN @config_entries.HANDLERS.register(DOMAIN) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index d01990b74b981a..0b6a93a39ea4cb 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -4,13 +4,13 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES as BINARY_SENSOR_CLASSES, ) -from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES from homeassistant.components.device_tracker import ( ATTR_BATTERY, ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ) +from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES from homeassistant.const import ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 0e05c424609dc8..f58f80aa5fc5e5 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -1,19 +1,20 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" import logging -from homeassistant.core import callback -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_BATTERY_LEVEL -from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity + from .const import ( ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, - ATTR_GPS_ACCURACY, ATTR_GPS, + ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ATTR_SPEED, ATTR_VERTICAL_ACCURACY, diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 3be082951c53cc..2fb949720d63c0 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -1,9 +1,11 @@ """Helpers for mobile_app.""" -import logging import json +import logging from typing import Callable, Dict, Tuple -from aiohttp.web import json_response, Response +from aiohttp.web import Response, json_response +from nacl.encoding import Base64Encoder +from nacl.secret import SecretBox from homeassistant.core import Context from homeassistant.helpers.json import JSONEncoder @@ -13,8 +15,8 @@ ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, - ATTR_DEVICE_ID, ATTR_APP_VERSION, + ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, @@ -36,8 +38,6 @@ def setup_decrypt() -> Tuple[int, Callable]: Async friendly. """ - from nacl.secret import SecretBox - from nacl.encoding import Base64Encoder def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" @@ -51,8 +51,6 @@ def setup_encrypt() -> Tuple[int, Callable]: Async friendly. """ - from nacl.secret import SecretBox - from nacl.encoding import Base64Encoder def encrypt(ciphertext, key): """Encrypt ciphertext using key.""" diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 67914ea7076396..ee69f15fb11edc 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -1,18 +1,19 @@ """Provides an HTTP API for mobile_app.""" -import uuid from typing import Dict +import uuid -from aiohttp.web import Response, Request +from aiohttp.web import Request, Response +from nacl.secret import SecretBox from homeassistant.auth.util import generate_secret from homeassistant.components.cloud import ( + CloudNotAvailable, async_create_cloudhook, async_remote_ui_url, - CloudNotAvailable, ) from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import HTTP_CREATED, CONF_WEBHOOK_ID +from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED from .const import ( ATTR_DEVICE_ID, @@ -24,7 +25,6 @@ DOMAIN, REGISTRATION_SCHEMA, ) - from .helpers import supports_encryption @@ -49,8 +49,6 @@ async def post(self, request: Request, data: Dict) -> Response: data[CONF_WEBHOOK_ID] = webhook_id if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption(): - from nacl.secret import SecretBox - data[CONF_SECRET] = generate_secret(SecretBox.KEY_SIZE) data[CONF_USER_ID] = request["hass_user"].id diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 1e6a0517026255..8ac34c9af1dc67 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -12,7 +12,6 @@ ATTR_TITLE_DEFAULT, BaseNotificationService, ) - from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index b96a6f1e2f0496..199ba968dd22a1 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -13,7 +13,6 @@ DATA_DEVICES, DOMAIN, ) - from .entity import MobileAppEntity, sensor_id diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index f95d5b993f083a..66188500fd6a2d 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,13 +1,12 @@ """Webhook handlers for mobile_app.""" import logging -from aiohttp.web import HTTPBadRequest, Response, Request +from aiohttp.web import HTTPBadRequest, Request, Response import voluptuous as vol -from homeassistant.components.cloud import async_remote_ui_url, CloudNotAvailable +from homeassistant.components.cloud import CloudNotAvailable, async_remote_ui_url from homeassistant.components.frontend import MANIFEST_JSON from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN - from homeassistant.const import ( ATTR_DOMAIN, ATTR_SERVICE, @@ -50,10 +49,10 @@ ERR_ENCRYPTION_REQUIRED, ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED, + SIGNAL_LOCATION_UPDATE, SIGNAL_SENSOR_UPDATE, WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, - WEBHOOK_TYPES, WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_GET_CONFIG, @@ -63,10 +62,8 @@ WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION, WEBHOOK_TYPE_UPDATE_SENSOR_STATES, - SIGNAL_LOCATION_UPDATE, + WEBHOOK_TYPES, ) - - from .helpers import ( _decrypt_payload, empty_okay_response, @@ -77,7 +74,6 @@ webhook_response, ) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index d0d13415b4d76a..813d0a9cf8930b 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -29,7 +29,6 @@ DATA_STORE, DOMAIN, ) - from .helpers import safe_registration, savable_state From 936dac22703aa5ae6a2644793f86f1edd35b6497 Mon Sep 17 00:00:00 2001 From: Kevin McCormack Date: Mon, 21 Oct 2019 04:06:16 -0400 Subject: [PATCH 1122/3953] Add Vivotek camera component code owner (#28024) --- CODEOWNERS | 1 + homeassistant/components/vivotek/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 30946fb14f2353..6e254c5a1d4688 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -321,6 +321,7 @@ homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe homeassistant/components/vicare/* @oischinger +homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf homeassistant/components/waqi/* @andrey-git diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index 20b2ac347f685e..ff49899112741c 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -6,5 +6,7 @@ "libpyvivotek==0.2.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@HarlemSquirrel" + ] } From 4db761e6f259938b41d6cf7dee9f70b42fe61f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:06:38 -0300 Subject: [PATCH 1123/3953] Move imports in metoffice component (#28023) --- homeassistant/components/metoffice/sensor.py | 5 +---- homeassistant/components/metoffice/weather.py | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 3ca55533ce31c5..98d94ebe6caf76 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import datapoint as dp import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -92,8 +93,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Met Office sensor platform.""" - import datapoint as dp - api_key = config.get(CONF_API_KEY) latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -193,8 +192,6 @@ def __init__(self, hass, datapoint, site): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Datapoint.""" - import datapoint as dp - try: forecast = self._datapoint.get_forecast_for_site(self._site.id, "3hourly") self.data = forecast.now() diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index bb7a64005ce287..09350588d46158 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,6 +1,7 @@ """Support for UK Met Office weather service.""" import logging +import datapoint as dp import voluptuous as vol from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity @@ -35,8 +36,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Met Office weather platform.""" - import datapoint as dp - name = config.get(CONF_NAME) datapoint = dp.connection(api_key=config.get(CONF_API_KEY)) From 8922d702ae6595f2e73231f86b7485de17233685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:07:09 -0300 Subject: [PATCH 1124/3953] Move imports in lupusec component (#28018) --- homeassistant/components/lupusec/__init__.py | 11 ++++------- homeassistant/components/lupusec/binary_sensor.py | 4 ++-- homeassistant/components/lupusec/switch.py | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py index c64789ec4ddcb9..60f3a192b0791e 100644 --- a/homeassistant/components/lupusec/__init__.py +++ b/homeassistant/components/lupusec/__init__.py @@ -1,11 +1,12 @@ """Support for Lupusec Home Security system.""" import logging +import lupupy +from lupupy.exceptions import LupusecException import voluptuous as vol -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -34,8 +35,6 @@ def setup(hass, config): """Set up Lupusec component.""" - from lupupy.exceptions import LupusecException - conf = config[DOMAIN] username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] @@ -67,8 +66,6 @@ class LupusecSystem: def __init__(self, username, password, ip_address, name): """Initialize the system.""" - import lupupy - self.lupusec = lupupy.Lupusec(username, password, ip_address) self.name = name diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py index ccd45e9f8740ba..b2a332a03e766f 100644 --- a/homeassistant/components/lupusec/binary_sensor.py +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import lupupy.constants as CONST + from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorDevice from . import DOMAIN as LUPUSEC_DOMAIN, LupusecDevice @@ -16,8 +18,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return - import lupupy.constants as CONST - data = hass.data[LUPUSEC_DOMAIN] device_types = [CONST.TYPE_OPENING] diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index b6391959397a07..a6864f39ef7d53 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import lupupy.constants as CONST + from homeassistant.components.switch import SwitchDevice from . import DOMAIN as LUPUSEC_DOMAIN, LupusecDevice @@ -16,8 +18,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return - import lupupy.constants as CONST - data = hass.data[LUPUSEC_DOMAIN] devices = [] From 90731555f87ad2f9f48fb247dd9b05ebcecc8611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:07:35 -0300 Subject: [PATCH 1125/3953] Move imports in loopenergy component (#28017) --- homeassistant/components/loopenergy/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py index 994c3e2fd8952a..537907d9d0a8f8 100644 --- a/homeassistant/components/loopenergy/sensor.py +++ b/homeassistant/components/loopenergy/sensor.py @@ -1,6 +1,7 @@ """Support for Loop Energy sensors.""" import logging +import pyloopenergy import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -54,8 +55,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Loop Energy sensors.""" - import pyloopenergy - elec_config = config.get(CONF_ELEC) gas_config = config.get(CONF_GAS, {}) From 6742b36a3dff759635ff2530793ac0fed1291581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:08:04 -0300 Subject: [PATCH 1126/3953] Move imports in lifx_legacy component (#28013) --- homeassistant/components/lifx_legacy/light.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lifx_legacy/light.py b/homeassistant/components/lifx_legacy/light.py index 78a333018f973a..8f767a2f5594a2 100644 --- a/homeassistant/components/lifx_legacy/light.py +++ b/homeassistant/components/lifx_legacy/light.py @@ -9,6 +9,7 @@ """ import logging +import liffylights import voluptuous as vol from homeassistant.components.light import ( @@ -16,19 +17,19 @@ ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, Light, - PLATFORM_SCHEMA, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_change from homeassistant.util.color import ( - color_temperature_mired_to_kelvin, color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, ) -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -71,8 +72,6 @@ class LIFX: def __init__(self, add_entities_callback, server_addr=None, broadcast_addr=None): """Initialize the light.""" - import liffylights - self._devices = [] self._add_entities_callback = add_entities_callback From 9a9cd1d0b259c2672b400dcb96e0aff183061f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:09:14 -0300 Subject: [PATCH 1127/3953] Move imports in lifx component (#28012) --- homeassistant/components/lifx/__init__.py | 6 +++--- homeassistant/components/lifx/config_flow.py | 7 ++++--- homeassistant/components/lifx/light.py | 6 ++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index f4ae2c4030a2e6..6e921a59afedd1 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -1,12 +1,12 @@ """Support for LIFX.""" import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries -from homeassistant.const import CONF_PORT from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from .const import DOMAIN +from homeassistant.const import CONF_PORT +import homeassistant.helpers.config_validation as cv +from .const import DOMAIN CONF_SERVER = "server" CONF_BROADCAST = "broadcast" diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index b324dc0cad8ca6..71fe7247c121c3 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -1,13 +1,14 @@ """Config flow flow LIFX.""" -from homeassistant.helpers import config_entry_flow +import aiolifx + from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + from .const import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - import aiolifx - lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan() return len(lifx_ip_addresses) > 0 diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index d183dcb0fa2627..50e36e8db0ac16 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -6,6 +6,8 @@ import math import sys +import aiolifx as aiolifx_module +import aiolifx_effects as aiolifx_effects_module import voluptuous as vol from homeassistant import util @@ -151,15 +153,11 @@ def aiolifx(): """Return the aiolifx module.""" - import aiolifx as aiolifx_module - return aiolifx_module def aiolifx_effects(): """Return the aiolifx_effects module.""" - import aiolifx_effects as aiolifx_effects_module - return aiolifx_effects_module From 09acbc211c90a69e33db5b54a6c32099fe83a0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:09:38 -0300 Subject: [PATCH 1128/3953] Move imports in lg_soundbar component (#28011) --- .../components/lg_soundbar/media_player.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 5c98f86a2bc76c..30cfbf17074dbf 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -1,14 +1,15 @@ """Support for LG soundbars.""" import logging +import temescal + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOUND_MODE, ) - from homeassistant.const import STATE_ON _LOGGER = logging.getLogger(__name__) @@ -32,8 +33,6 @@ class LGDevice(MediaPlayerDevice): def __init__(self, discovery_info): """Initialize the LG speakers.""" - import temescal - host = discovery_info.get("host") port = discovery_info.get("port") @@ -140,7 +139,6 @@ def state(self): @property def sound_mode(self): """Return the current sound mode.""" - import temescal if self._equaliser == -1: return "" @@ -149,8 +147,6 @@ def sound_mode(self): @property def sound_mode_list(self): """Return the available sound modes.""" - import temescal - modes = [] for equaliser in self._equalisers: modes.append(temescal.equalisers[equaliser]) @@ -159,8 +155,6 @@ def sound_mode_list(self): @property def source(self): """Return the current input source.""" - import temescal - if self._function == -1: return "" return temescal.functions[self._function] @@ -168,8 +162,6 @@ def source(self): @property def source_list(self): """List of available input sources.""" - import temescal - sources = [] for function in self._functions: sources.append(temescal.functions[function]) @@ -191,12 +183,8 @@ def mute_volume(self, mute): def select_source(self, source): """Select input source.""" - import temescal - self._device.set_func(temescal.functions.index(source)) def select_sound_mode(self, sound_mode): """Set Sound Mode for Receiver..""" - import temescal - self._device.set_eq(temescal.equalisers.index(sound_mode)) From 09f9875ccf8c7c48208c7ccaf148a90000d2e51f Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 21 Oct 2019 11:17:21 +0300 Subject: [PATCH 1129/3953] Glances config flow (#27221) * Glances Integration with config flow * Glances Integration with config flow * fix description texts * Glances Integration with config flow * Glances Integration with config flow * fix description texts * update .coverage.py * add codeowner * add test_options * Fixed typos, Added import, fixed tests * sort imports * remove commented code --- .coveragerc | 1 + CODEOWNERS | 2 +- .../components/glances/.translations/en.json | 37 ++++ homeassistant/components/glances/__init__.py | 175 +++++++++++++++++- .../components/glances/config_flow.py | 130 +++++++++++++ homeassistant/components/glances/const.py | 36 ++++ .../components/glances/manifest.json | 6 +- homeassistant/components/glances/sensor.py | 166 ++++------------- homeassistant/components/glances/strings.json | 37 ++++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/glances/__init__.py | 1 + tests/components/glances/test_config_flow.py | 102 ++++++++++ 13 files changed, 568 insertions(+), 129 deletions(-) create mode 100644 homeassistant/components/glances/.translations/en.json create mode 100644 homeassistant/components/glances/config_flow.py create mode 100644 homeassistant/components/glances/const.py create mode 100644 homeassistant/components/glances/strings.json create mode 100644 tests/components/glances/__init__.py create mode 100644 tests/components/glances/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 7071312a99c21d..7c35022c355a43 100644 --- a/.coveragerc +++ b/.coveragerc @@ -253,6 +253,7 @@ omit = homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py + homeassistant/components/glances/__init__.py homeassistant/components/glances/sensor.py homeassistant/components/gntp/notify.py homeassistant/components/goalfeed/* diff --git a/CODEOWNERS b/CODEOWNERS index 6e254c5a1d4688..e1ff7b36ff154a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -110,7 +110,7 @@ homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/gitter/* @fabaff -homeassistant/components/glances/* @fabaff +homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/gntp/* @robbiet480 homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton diff --git a/homeassistant/components/glances/.translations/en.json b/homeassistant/components/glances/.translations/en.json new file mode 100644 index 00000000000000..1bd7275daeff15 --- /dev/null +++ b/homeassistant/components/glances/.translations/en.json @@ -0,0 +1,37 @@ +{ + "config": { + "title": "Glances", + "step": { + "user": { + "title": "Setup Glances", + "data": { + "name": "Name", + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port", + "version": "Glances API Version (2 or 3)", + "ssl": "Use SSL/TLS to connect to the Glances system", + "verify_ssl": "Verify the certification of the system" + } + } + }, + "error": { + "cannot_connect": "Unable to connect to host", + "wrong_version": "Version not supported (2 or 3 only)" + }, + "abort": { + "already_configured": "Host is already configured." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Glances", + "data": { + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index b458d8788fcf7e..d09aa78253474d 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -1 +1,174 @@ -"""The glances component.""" +"""The Glances component.""" +from datetime import timedelta +import logging + +from glances_api import Glances, exceptions +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + CONF_VERSION, + DATA_UPDATED, + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DEFAULT_VERSION, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +GLANCES_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In([2, 3]), + } + ) +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [GLANCES_SCHEMA])}, extra=vol.ALLOW_EXTRA +) + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Configure Glances using config flow only.""" + if DOMAIN in config: + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Glances from config entry.""" + client = GlancesData(hass, config_entry) + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client + if not await client.async_setup(): + return False + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + hass.data[DOMAIN].pop(config_entry.entry_id) + return True + + +class GlancesData: + """Get the latest data from Glances api.""" + + def __init__(self, hass, config_entry): + """Initialize the Glances data.""" + self.hass = hass + self.config_entry = config_entry + self.api = None + self.unsub_timer = None + self.available = False + + @property + def host(self): + """Return client host.""" + return self.config_entry.data[CONF_HOST] + + async def async_update(self): + """Get the latest data from the Glances REST API.""" + try: + await self.api.get_data() + self.available = True + except exceptions.GlancesApiError: + _LOGGER.error("Unable to fetch data from Glances") + self.available = False + _LOGGER.debug("Glances data updated") + async_dispatcher_send(self.hass, DATA_UPDATED) + + async def async_setup(self): + """Set up the Glances client.""" + try: + self.api = get_api(self.hass, self.config_entry.data) + await self.api.get_data() + self.available = True + _LOGGER.debug("Successfully connected to Glances") + + except exceptions.GlancesApiConnectionError: + _LOGGER.debug("Can not connect to Glances") + raise ConfigEntryNotReady + + self.add_options() + self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) + self.config_entry.add_update_listener(self.async_options_updated) + + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, "sensor" + ) + ) + return True + + def add_options(self): + """Add options for Glances integration.""" + if not self.config_entry.options: + options = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} + self.hass.config_entries.async_update_entry( + self.config_entry, options=options + ) + + def set_scan_interval(self, scan_interval): + """Update scan interval.""" + + async def refresh(event_time): + """Get the latest data from Glances api.""" + await self.async_update() + + if self.unsub_timer is not None: + self.unsub_timer() + self.unsub_timer = async_track_time_interval( + self.hass, refresh, timedelta(seconds=scan_interval) + ) + + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + hass.data[DOMAIN][entry.entry_id].set_scan_interval( + entry.options[CONF_SCAN_INTERVAL] + ) + + +def get_api(hass, entry): + """Return the api from glances_api.""" + params = entry.copy() + params.pop(CONF_NAME) + verify_ssl = params.pop(CONF_VERIFY_SSL) + session = async_get_clientsession(hass, verify_ssl) + return Glances(hass.loop, session, **params) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py new file mode 100644 index 00000000000000..3c86fae0357350 --- /dev/null +++ b/homeassistant/components/glances/config_flow.py @@ -0,0 +1,130 @@ +"""Config flow for Glances.""" +import glances_api +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import callback + +from . import get_api +from .const import ( + CONF_VERSION, + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DEFAULT_VERSION, + DOMAIN, + SUPPORTED_VERSIONS, +) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + vol.Required(CONF_VERSION, default=DEFAULT_VERSION): int, + vol.Optional(CONF_SSL, default=False): bool, + vol.Optional(CONF_VERIFY_SSL, default=False): bool, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == data[CONF_HOST]: + raise AlreadyConfigured + + if data[CONF_VERSION] not in SUPPORTED_VERSIONS: + raise WrongVersion + try: + api = get_api(hass, data) + await api.get_data() + except glances_api.exceptions.GlancesApiConnectionError: + raise CannotConnect + + +class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Glances config flow.""" + + 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 GlancesOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + await validate_input(self.hass, user_input) + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + except CannotConnect: + errors["base"] = "cannot_connect" + except WrongVersion: + errors[CONF_VERSION] = "wrong_version" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_config): + """Import from Glances sensor config.""" + + return await self.async_step_user(user_input=import_config) + + +class GlancesOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Glances client options.""" + + def __init__(self, config_entry): + """Initialize Glances options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the Glances options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): int + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class AlreadyConfigured(exceptions.HomeAssistantError): + """Error to indicate host is already configured.""" + + +class WrongVersion(exceptions.HomeAssistantError): + """Error to indicate the selected version is wrong.""" diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py new file mode 100644 index 00000000000000..e47586ea245b5c --- /dev/null +++ b/homeassistant/components/glances/const.py @@ -0,0 +1,36 @@ +"""Constants for Glances component.""" +from homeassistant.const import TEMP_CELSIUS + +DOMAIN = "glances" +CONF_VERSION = "version" + +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "Glances" +DEFAULT_PORT = 61208 +DEFAULT_VERSION = 3 +DEFAULT_SCAN_INTERVAL = 60 + +DATA_UPDATED = "glances_data_updated" +SUPPORTED_VERSIONS = [2, 3] + +SENSOR_TYPES = { + "disk_use_percent": ["Disk used percent", "%", "mdi:harddisk"], + "disk_use": ["Disk used", "GiB", "mdi:harddisk"], + "disk_free": ["Disk free", "GiB", "mdi:harddisk"], + "memory_use_percent": ["RAM used percent", "%", "mdi:memory"], + "memory_use": ["RAM used", "MiB", "mdi:memory"], + "memory_free": ["RAM free", "MiB", "mdi:memory"], + "swap_use_percent": ["Swap used percent", "%", "mdi:memory"], + "swap_use": ["Swap used", "GiB", "mdi:memory"], + "swap_free": ["Swap free", "GiB", "mdi:memory"], + "processor_load": ["CPU load", "15 min", "mdi:memory"], + "process_running": ["Running", "Count", "mdi:memory"], + "process_total": ["Total", "Count", "mdi:memory"], + "process_thread": ["Thread", "Count", "mdi:memory"], + "process_sleeping": ["Sleeping", "Count", "mdi:memory"], + "cpu_use_percent": ["CPU used", "%", "mdi:memory"], + "cpu_temp": ["CPU Temp", TEMP_CELSIUS, "mdi:thermometer"], + "docker_active": ["Containers active", "", "mdi:docker"], + "docker_cpu_use": ["Containers CPU used", "%", "mdi:docker"], + "docker_memory_use": ["Containers RAM used", "MiB", "mdi:docker"], +} diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 775d208c1c48e5..6067b1a986860d 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -1,12 +1,14 @@ { "domain": "glances", "name": "Glances", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/glances", "requirements": [ "glances_api==0.2.0" ], "dependencies": [], "codeowners": [ - "@fabaff" + "@fabaff", + "@engrbm87" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 90b4b386f37e52..760958f0dee0da 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -1,114 +1,31 @@ """Support gathering system information of hosts which are running glances.""" -from datetime import timedelta import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_USERNAME, - CONF_PASSWORD, - CONF_SSL, - CONF_VERIFY_SSL, - CONF_RESOURCES, - STATE_UNAVAILABLE, - TEMP_CELSIUS, -) -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -CONF_VERSION = "version" -DEFAULT_HOST = "localhost" -DEFAULT_NAME = "Glances" -DEFAULT_PORT = "61208" -DEFAULT_VERSION = 2 +from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) - -SENSOR_TYPES = { - "disk_use_percent": ["Disk used percent", "%", "mdi:harddisk"], - "disk_use": ["Disk used", "GiB", "mdi:harddisk"], - "disk_free": ["Disk free", "GiB", "mdi:harddisk"], - "memory_use_percent": ["RAM used percent", "%", "mdi:memory"], - "memory_use": ["RAM used", "MiB", "mdi:memory"], - "memory_free": ["RAM free", "MiB", "mdi:memory"], - "swap_use_percent": ["Swap used percent", "%", "mdi:memory"], - "swap_use": ["Swap used", "GiB", "mdi:memory"], - "swap_free": ["Swap free", "GiB", "mdi:memory"], - "processor_load": ["CPU load", "15 min", "mdi:memory"], - "process_running": ["Running", "Count", "mdi:memory"], - "process_total": ["Total", "Count", "mdi:memory"], - "process_thread": ["Thread", "Count", "mdi:memory"], - "process_sleeping": ["Sleeping", "Count", "mdi:memory"], - "cpu_use_percent": ["CPU used", "%", "mdi:memory"], - "cpu_temp": ["CPU Temp", TEMP_CELSIUS, "mdi:thermometer"], - "docker_active": ["Containers active", "", "mdi:docker"], - "docker_cpu_use": ["Containers CPU used", "%", "mdi:docker"], - "docker_memory_use": ["Containers RAM used", "MiB", "mdi:docker"], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_RESOURCES, default=["disk_use"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In([2, 3]), - } -) +_LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Glances sensors.""" - from glances_api import Glances - - name = config[CONF_NAME] - host = config[CONF_HOST] - port = config[CONF_PORT] - version = config[CONF_VERSION] - var_conf = config[CONF_RESOURCES] - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - ssl = config[CONF_SSL] - verify_ssl = config[CONF_VERIFY_SSL] - - session = async_get_clientsession(hass, verify_ssl) - glances = GlancesData( - Glances( - hass.loop, - session, - host=host, - port=port, - version=version, - username=username, - password=password, - ssl=ssl, - ) - ) + """Set up the Glances sensors is done through async_setup_entry.""" + pass - await glances.async_update() - if glances.api.data is None: - raise PlatformNotReady +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Glances sensors.""" + glances_data = hass.data[DOMAIN][config_entry.entry_id] + name = config_entry.data[CONF_NAME] dev = [] - for resource in var_conf: - dev.append(GlancesSensor(glances, name, resource)) + for sensor_type in SENSOR_TYPES: + dev.append( + GlancesSensor(glances_data, name, SENSOR_TYPES[sensor_type][0], sensor_type) + ) async_add_entities(dev, True) @@ -116,9 +33,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class GlancesSensor(Entity): """Implementation of a Glances sensor.""" - def __init__(self, glances, name, sensor_type): + def __init__(self, glances_data, name, sensor_name, sensor_type): """Initialize the sensor.""" - self.glances = glances + self.glances_data = glances_data + self._sensor_name = sensor_name self._name = name self.type = sensor_type self._state = None @@ -127,7 +45,12 @@ def __init__(self, glances, name, sensor_type): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, SENSOR_TYPES[self.type][0]) + return f"{self._name} {self._sensor_name}" + + @property + def unique_id(self): + """Set unique_id for sensor.""" + return f"{self.glances_data.host}-{self.name}" @property def icon(self): @@ -142,17 +65,31 @@ def unit_of_measurement(self): @property def available(self): """Could the device be accessed during the last update call.""" - return self.glances.available + return self.glances_data.available @property def state(self): """Return the state of the resources.""" return self._state + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) + async def async_update(self): """Get the latest data from REST API.""" - await self.glances.async_update() - value = self.glances.api.data + value = self.glances_data.api.data if value is not None: if self.type == "disk_use_percent": @@ -249,24 +186,3 @@ async def async_update(self): self._state = round(mem_use / 1024 ** 2, 1) except KeyError: self._state = STATE_UNAVAILABLE - - -class GlancesData: - """The class for handling the data retrieval.""" - - def __init__(self, api): - """Initialize the data object.""" - self.api = api - self.available = True - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """Get the latest data from the Glances REST API.""" - from glances_api.exceptions import GlancesApiError - - try: - await self.api.get_data() - self.available = True - except GlancesApiError: - _LOGGER.error("Unable to fetch data from Glances") - self.available = False diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json new file mode 100644 index 00000000000000..1bd7275daeff15 --- /dev/null +++ b/homeassistant/components/glances/strings.json @@ -0,0 +1,37 @@ +{ + "config": { + "title": "Glances", + "step": { + "user": { + "title": "Setup Glances", + "data": { + "name": "Name", + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port", + "version": "Glances API Version (2 or 3)", + "ssl": "Use SSL/TLS to connect to the Glances system", + "verify_ssl": "Verify the certification of the system" + } + } + }, + "error": { + "cannot_connect": "Unable to connect to host", + "wrong_version": "Version not supported (2 or 3 only)" + }, + "abort": { + "already_configured": "Host is already configured." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Glances", + "data": { + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 664e83fba33aea..60aa610ec073f4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -22,6 +22,7 @@ "esphome", "geofency", "geonetnz_quakes", + "glances", "gpslogger", "hangouts", "heos", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7912de963abbf4..754a7d72a64c02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -208,6 +208,9 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.nmap_tracker getmac==0.8.1 +# homeassistant.components.glances +glances_api==0.2.0 + # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py new file mode 100644 index 00000000000000..488265f970b323 --- /dev/null +++ b/tests/components/glances/__init__.py @@ -0,0 +1 @@ +"""Tests for Glances.""" diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py new file mode 100644 index 00000000000000..e5be52e6b33783 --- /dev/null +++ b/tests/components/glances/test_config_flow.py @@ -0,0 +1,102 @@ +"""Tests for Glances config flow.""" +from unittest.mock import patch + +from glances_api import Glances + +from homeassistant.components.glances import config_flow +from homeassistant.components.glances.const import DOMAIN +from homeassistant.const import CONF_SCAN_INTERVAL + +from tests.common import MockConfigEntry, mock_coro + +NAME = "Glances" +HOST = "0.0.0.0" +USERNAME = "username" +PASSWORD = "password" +PORT = 61208 +VERSION = 3 +SCAN_INTERVAL = 10 + +DEMO_USER_INPUT = { + "name": NAME, + "host": HOST, + "username": USERNAME, + "password": PASSWORD, + "version": VERSION, + "port": PORT, + "ssl": False, + "verify_ssl": True, +} + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.GlancesFlowHandler() + flow.hass = hass + return flow + + +async def test_form(hass): + """Test config entry configured successfully.""" + flow = init_config_flow(hass) + + with patch("glances_api.Glances"), patch.object( + Glances, "get_data", return_value=mock_coro() + ): + + result = await flow.async_step_user(DEMO_USER_INPUT) + + assert result["type"] == "create_entry" + assert result["title"] == NAME + assert result["data"] == DEMO_USER_INPUT + + +async def test_form_cannot_connect(hass): + """Test to return error if we cannot connect.""" + flow = init_config_flow(hass) + + with patch("glances_api.Glances"): + result = await flow.async_step_user(DEMO_USER_INPUT) + + assert result["type"] == "form" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_wrong_version(hass): + """Test to check if wrong version is entered.""" + flow = init_config_flow(hass) + + user_input = DEMO_USER_INPUT.copy() + user_input.update(version=1) + result = await flow.async_step_user(user_input) + + assert result["type"] == "form" + assert result["errors"] == {"version": "wrong_version"} + + +async def test_form_already_configured(hass): + """Test host is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} + ) + entry.add_to_hass(hass) + + flow = init_config_flow(hass) + result = await flow.async_step_user(DEMO_USER_INPUT) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_options(hass): + """Test options for Glances.""" + entry = MockConfigEntry( + domain=DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} + ) + entry.add_to_hass(hass) + flow = init_config_flow(hass) + options_flow = flow.async_get_options_flow(entry) + + result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) + assert result["type"] == "create_entry" + assert result["data"][CONF_SCAN_INTERVAL] == 10 From 9fa99eaea905cf9ae7f1c20976cf86157ba39ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:21:40 -0300 Subject: [PATCH 1130/3953] Move imports in konnected component (#28009) --- .../components/konnected/__init__.py | 55 +++++++++---------- .../components/konnected/handlers.py | 8 +-- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 4cc872fb78b2a2..624d359e154637 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -4,59 +4,58 @@ import json import logging -import voluptuous as vol - from aiohttp.hdrs import AUTHORIZATION from aiohttp.web import Request, Response +import konnected +import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA from homeassistant.components.discovery import SERVICE_KONNECTED from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - HTTP_BAD_REQUEST, - HTTP_NOT_FOUND, - HTTP_UNAUTHORIZED, - CONF_DEVICES, + ATTR_ENTITY_ID, + ATTR_STATE, + CONF_ACCESS_TOKEN, CONF_BINARY_SENSORS, - CONF_SENSORS, - CONF_SWITCHES, + CONF_DEVICES, CONF_HOST, - CONF_PORT, CONF_ID, CONF_NAME, - CONF_TYPE, CONF_PIN, + CONF_PORT, + CONF_SENSORS, + CONF_SWITCHES, + CONF_TYPE, CONF_ZONE, - CONF_ACCESS_TOKEN, - ATTR_ENTITY_ID, - ATTR_STATE, + EVENT_HOMEASSISTANT_START, + HTTP_BAD_REQUEST, + HTTP_NOT_FOUND, + HTTP_UNAUTHORIZED, STATE_ON, ) +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers import discovery -from homeassistant.helpers import config_validation as cv from .const import ( CONF_ACTIVATION, CONF_API_HOST, + CONF_BLINK, + CONF_DHT_SENSORS, + CONF_DISCOVERY, + CONF_DS18B20_SENSORS, + CONF_INVERSE, CONF_MOMENTARY, CONF_PAUSE, CONF_POLL_INTERVAL, CONF_REPEAT, - CONF_INVERSE, - CONF_BLINK, - CONF_DISCOVERY, - CONF_DHT_SENSORS, - CONF_DS18B20_SENSORS, DOMAIN, - STATE_LOW, - STATE_HIGH, - PIN_TO_ZONE, - ZONE_TO_PIN, ENDPOINT_ROOT, - UPDATE_ENDPOINT, + PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE, + STATE_HIGH, + STATE_LOW, + UPDATE_ENDPOINT, + ZONE_TO_PIN, ) from .handlers import HANDLERS @@ -141,8 +140,6 @@ async def async_setup(hass, config): """Set up the Konnected platform.""" - import konnected - cfg = config.get(DOMAIN) if cfg is None: cfg = {} @@ -336,8 +333,6 @@ def __init__(self, hass, host, port): self.host = host self.port = port - import konnected - self.client = konnected.Client(host, str(port)) self.status = self.client.get_status() diff --git a/homeassistant/components/konnected/handlers.py b/homeassistant/components/konnected/handlers.py index a355cabba56b0b..a8914853e84f51 100644 --- a/homeassistant/components/konnected/handlers.py +++ b/homeassistant/components/konnected/handlers.py @@ -1,16 +1,16 @@ """Handle Konnected messages.""" import logging -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.util import decorator from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, - DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.util import decorator -from .const import CONF_INVERSE, SIGNAL_SENSOR_UPDATE, SIGNAL_DS18B20_NEW +from .const import CONF_INVERSE, SIGNAL_DS18B20_NEW, SIGNAL_SENSOR_UPDATE _LOGGER = logging.getLogger(__name__) HANDLERS = decorator.Registry() From 1a68591fe6d7398fb1d33ebcb35a4b40a9fe1346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:22:13 -0300 Subject: [PATCH 1131/3953] Move imports in juicenet component (#28006) --- homeassistant/components/juicenet/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index a176236b22465c..207dac7836a011 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -1,12 +1,13 @@ """Support for Juicenet cloud.""" import logging +import pyjuicenet import voluptuous as vol -from homeassistant.helpers import discovery from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.helpers.entity import Entity +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -20,8 +21,6 @@ def setup(hass, config): """Set up the Juicenet component.""" - import pyjuicenet - hass.data[DOMAIN] = {} access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) From d5799d020aaaaa547e27f628c11ee11b3573c400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:22:34 -0300 Subject: [PATCH 1132/3953] Move imports in insteon component (#28001) --- homeassistant/components/insteon/__init__.py | 73 +++++++++----------- 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 4015d472ce8d42..11f224dbfccaec 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -3,6 +3,38 @@ import logging from typing import Dict +import insteonplm +from insteonplm.devices import ALDBStatus +from insteonplm.states.cover import Cover +from insteonplm.states.dimmable import ( + DimmableKeypadA, + DimmableRemote, + DimmableSwitch, + DimmableSwitch_Fan, +) +from insteonplm.states.onOff import ( + OnOffKeypad, + OnOffKeypadA, + OnOffSwitch, + OnOffSwitch_OutletBottom, + OnOffSwitch_OutletTop, + OpenClosedRelay, +) +from insteonplm.states.sensor import ( + IoLincSensor, + LeakSensorDryWet, + OnOffSensor, + SmokeCO2Sensor, + VariableSensor, +) +from insteonplm.states.x10 import ( + X10AllLightsOffSensor, + X10AllLightsOnSensor, + X10AllUnitsOffSensor, + X10DimmableSwitch, + X10OnOffSensor, + X10OnOffSwitch, +) import voluptuous as vol from homeassistant.const import ( @@ -16,8 +48,8 @@ ) from homeassistant.core import callback from homeassistant.helpers import discovery -from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -240,8 +272,6 @@ def set_default_port(schema: Dict) -> Dict: async def async_setup(hass, config): """Set up the connection to the modem.""" - import insteonplm - ipdb = IPDB() insteon_modem = None @@ -496,41 +526,6 @@ class IPDB: def __init__(self): """Create the INSTEON Product Database (IPDB).""" - from insteonplm.states.cover import Cover - - from insteonplm.states.onOff import ( - OnOffSwitch, - OnOffSwitch_OutletTop, - OnOffSwitch_OutletBottom, - OpenClosedRelay, - OnOffKeypadA, - OnOffKeypad, - ) - - from insteonplm.states.dimmable import ( - DimmableSwitch, - DimmableSwitch_Fan, - DimmableRemote, - DimmableKeypadA, - ) - - from insteonplm.states.sensor import ( - VariableSensor, - OnOffSensor, - SmokeCO2Sensor, - IoLincSensor, - LeakSensorDryWet, - ) - - from insteonplm.states.x10 import ( - X10DimmableSwitch, - X10OnOffSwitch, - X10OnOffSensor, - X10AllUnitsOffSensor, - X10AllLightsOnSensor, - X10AllLightsOffSensor, - ) - self.states = [ State(Cover, "cover"), State(OnOffSwitch_OutletTop, "switch"), @@ -685,8 +680,6 @@ def _get_label(self): def print_aldb_to_log(aldb): """Print the All-Link Database to the log file.""" - from insteonplm.devices import ALDBStatus - _LOGGER.info("ALDB load status is %s", aldb.status.name) if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]: _LOGGER.warning("Device All-Link database not loaded") From 36ff790a39f27bd94a85f303351f05160126b2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:22:52 -0300 Subject: [PATCH 1133/3953] Move imports in greenwave component (#27998) --- homeassistant/components/greenwave/light.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index 5a6ce2c51c2c0c..8b85de598b0c8b 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -1,7 +1,9 @@ """Support for Greenwave Reality (TCP Connected) lights.""" -import logging from datetime import timedelta +import logging +import os +import greenwavereality as greenwave import voluptuous as vol from homeassistant.components.light import ( @@ -29,9 +31,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Greenwave Reality Platform.""" - import greenwavereality as greenwave - import os - host = config.get(CONF_HOST) tokenfile = hass.config.path(".greenwave") if config.get(CONF_VERSION) == 3: @@ -60,8 +59,6 @@ class GreenwaveLight(Light): def __init__(self, light, host, token, gatewaydata): """Initialize a Greenwave Reality Light.""" - import greenwavereality as greenwave - self._did = int(light["did"]) self._name = light["name"] self._state = int(light["state"]) @@ -98,22 +95,16 @@ def is_on(self): def turn_on(self, **kwargs): """Instruct the light to turn on.""" - import greenwavereality as greenwave - temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) / 255) * 100) greenwave.set_brightness(self._host, self._did, temp_brightness, self._token) greenwave.turn_on(self._host, self._did, self._token) def turn_off(self, **kwargs): """Instruct the light to turn off.""" - import greenwavereality as greenwave - greenwave.turn_off(self._host, self._did, self._token) def update(self): """Fetch new state data for this light.""" - import greenwavereality as greenwave - self._gatewaydata.update() bulbs = self._gatewaydata.greenwave @@ -128,8 +119,6 @@ class GatewayData: def __init__(self, host, token): """Initialize the data object.""" - import greenwavereality as greenwave - self._host = host self._token = token self._greenwave = greenwave.grab_bulbs(host, token) @@ -142,7 +131,5 @@ def greenwave(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the gateway.""" - import greenwavereality as greenwave - self._greenwave = greenwave.grab_bulbs(self._host, self._token) return self._greenwave From 322399c0af4e759b79c3f1581c4e7506e350636e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:24:06 -0300 Subject: [PATCH 1134/3953] Move imports in kira component (#28007) --- homeassistant/components/kira/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py index 77f91a50dfad04..8948fbd0b8f51c 100644 --- a/homeassistant/components/kira/__init__.py +++ b/homeassistant/components/kira/__init__.py @@ -2,11 +2,13 @@ import logging import os +import pykira import voluptuous as vol from voluptuous.error import Error as VoluptuousError import yaml from homeassistant.const import ( + CONF_CODE, CONF_DEVICE, CONF_HOST, CONF_NAME, @@ -15,7 +17,6 @@ CONF_TYPE, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, - CONF_CODE, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -93,8 +94,6 @@ def load_codes(path): def setup(hass, config): """Set up the KIRA component.""" - import pykira - sensors = config.get(DOMAIN, {}).get(CONF_SENSORS, []) remotes = config.get(DOMAIN, {}).get(CONF_REMOTES, []) # If no sensors or remotes were specified, add a sensor From 5fb3f6038bee56b979c5ddd7a5459caeeedc7509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:24:26 -0300 Subject: [PATCH 1135/3953] Move imports in itach component (#28005) --- homeassistant/components/itach/remote.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py index 9895b54a50dd43..5390111890cd10 100644 --- a/homeassistant/components/itach/remote.py +++ b/homeassistant/components/itach/remote.py @@ -1,19 +1,20 @@ """Support for iTach IR devices.""" import logging +import pyitachip2ir import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components import remote +from homeassistant.components.remote import PLATFORM_SCHEMA from homeassistant.const import ( - DEVICE_DEFAULT_NAME, - CONF_NAME, - CONF_MAC, + CONF_DEVICES, CONF_HOST, + CONF_MAC, + CONF_NAME, CONF_PORT, - CONF_DEVICES, + DEVICE_DEFAULT_NAME, ) -from homeassistant.components.remote import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -55,8 +56,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ITach connection and devices.""" - import pyitachip2ir - itachip2ir = pyitachip2ir.ITachIP2IR( config.get(CONF_MAC), config.get(CONF_HOST), int(config.get(CONF_PORT)) ) From 6844d203a14bdf1057ae81e8c8b78b8a33d828cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:41:20 -0300 Subject: [PATCH 1136/3953] Move imports in gpsd component (#27997) --- homeassistant/components/gpsd/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index 197e424ce86e69..8696dde72cb40f 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -1,6 +1,8 @@ """Support for GPSD.""" import logging +import socket +from gps3.agps3threaded import AGPS3mechanism import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -9,11 +11,11 @@ ATTR_LONGITUDE, ATTR_MODE, CONF_HOST, - CONF_PORT, CONF_NAME, + CONF_PORT, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -50,7 +52,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # except GPSError: # _LOGGER.warning('Not able to connect to GPSD') # return False - import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: @@ -69,8 +70,6 @@ class GpsdSensor(Entity): def __init__(self, hass, name, host, port): """Initialize the GPSD sensor.""" - from gps3.agps3threaded import AGPS3mechanism - self.hass = hass self._name = name self._host = host From d1fcc5762b3456b8698c38dc96e789a1d37c4a04 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 21 Oct 2019 03:44:07 -0500 Subject: [PATCH 1137/3953] Make dispatch signals unique per server (#28029) --- homeassistant/components/plex/const.py | 4 ++-- homeassistant/components/plex/media_player.py | 4 +++- homeassistant/components/plex/sensor.py | 4 +++- homeassistant/components/plex/server.py | 12 ++++++++++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index c576f1d6a592ea..d3a3a866361469 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -17,9 +17,9 @@ PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" PLEX_SERVER_CONFIG = "server_config" -PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal" +PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" -PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal" +PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" CONF_SERVER = "server" CONF_SERVER_IDENTIFIER = "server_id" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4a48950a67ce8c..32bf7b65fff6c7 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -62,7 +62,9 @@ def async_new_media_players(new_entities): hass, config_entry, async_add_entities, server_id, new_entities ) - unsub = async_dispatcher_connect(hass, PLEX_NEW_MP_SIGNAL, async_new_media_players) + unsub = async_dispatcher_connect( + hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players + ) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 3cde2adb8f4b78..287f0edf39aa09 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -49,7 +49,9 @@ async def async_added_to_hass(self): """Run when about to be added to hass.""" server_id = self._server.machine_identifier unsub = async_dispatcher_connect( - self.hass, PLEX_UPDATE_SENSOR_SIGNAL, self.async_refresh_sensor + self.hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(server_id), + self.async_refresh_sensor, ) self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 128bcdd45c646e..d7825ae82c3d60 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -147,9 +147,17 @@ def update_platforms(self): self.refresh_entity(client_id, None, None) if new_entity_configs: - dispatcher_send(self._hass, PLEX_NEW_MP_SIGNAL, new_entity_configs) + dispatcher_send( + self._hass, + PLEX_NEW_MP_SIGNAL.format(self.machine_identifier), + new_entity_configs, + ) - dispatcher_send(self._hass, PLEX_UPDATE_SENSOR_SIGNAL, sessions) + dispatcher_send( + self._hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier), + sessions, + ) @property def friendly_name(self): From 1e832dc9ec57c2523441f3f45756f39006571d70 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 21 Oct 2019 01:59:58 -0700 Subject: [PATCH 1138/3953] Bump teslajsonpy and add update switch (#27957) * bump teslajsonpy to 0.0.26 breaking change * add update switch to tesla * bump requirements_all.txt for teslajsonpy * address requested style changes * fix bug where update switch not loaded --- homeassistant/components/tesla/manifest.json | 8 +--- homeassistant/components/tesla/switch.py | 39 ++++++++++++++++++-- requirements_all.txt | 2 +- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 4071178c7c3ae0..87d76c16f05ffd 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -2,11 +2,7 @@ "domain": "tesla", "name": "Tesla", "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": [ - "teslajsonpy==0.0.25" - ], + "requirements": ["teslajsonpy==0.0.26"], "dependencies": [], - "codeowners": [ - "@zabuldon" - ] + "codeowners": ["@zabuldon"] } diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 19ac76018ff1bf..985194f87b2a93 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -11,11 +11,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla switch platform.""" - controller = hass.data[TESLA_DOMAIN]["devices"]["controller"] + controller = hass.data[TESLA_DOMAIN]["controller"] devices = [] for device in hass.data[TESLA_DOMAIN]["devices"]["switch"]: if device.bin_type == 0x8: devices.append(ChargerSwitch(device, controller)) + devices.append(UpdateSwitch(device, controller)) elif device.bin_type == 0x9: devices.append(RangeSwitch(device, controller)) add_entities(devices, True) @@ -72,10 +73,42 @@ def turn_off(self, **kwargs): @property def is_on(self): """Get whether the switch is in on state.""" - return self._state == STATE_ON + return self._state def update(self): """Update the state of the switch.""" _LOGGER.debug("Updating state for: %s", self._name) self.tesla_device.update() - self._state = STATE_ON if self.tesla_device.is_maxrange() else STATE_OFF + self._state = bool(self.tesla_device.is_maxrange()) + + +class UpdateSwitch(TeslaDevice, SwitchDevice): + """Representation of a Tesla update switch.""" + + def __init__(self, tesla_device, controller): + """Initialise of the switch.""" + self._state = None + super().__init__(tesla_device, controller) + self._name = self._name.replace("charger", "update") + self.tesla_id = self.tesla_id.replace("charger", "update") + + def turn_on(self, **kwargs): + """Send the on command.""" + _LOGGER.debug("Enable updates: %s %s", self._name, self.tesla_device.id()) + self.controller.set_updates(self.tesla_device.id(), True) + + def turn_off(self, **kwargs): + """Send the off command.""" + _LOGGER.debug("Disable updates: %s %s", self._name, self.tesla_device.id()) + self.controller.set_updates(self.tesla_device.id(), False) + + @property + def is_on(self): + """Get whether the switch is in on state.""" + return self._state + + def update(self): + """Update the state of the switch.""" + car_id = self.tesla_device.id() + _LOGGER.debug("Updating state for: %s %s", self._name, car_id) + self._state = bool(self.controller.get_updates(car_id)) diff --git a/requirements_all.txt b/requirements_all.txt index 3c649e91756574..81e31c8181762b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1876,7 +1876,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.0.25 +teslajsonpy==0.0.26 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 From 6c48abcaa55c36733c7c2db44a56ae6c806a9948 Mon Sep 17 00:00:00 2001 From: Matthew Turney Date: Mon, 21 Oct 2019 04:20:18 -0500 Subject: [PATCH 1139/3953] rest_command component should support PATCH method (#27989) Without PATCH the rest_command component lacks full RESTful API support. --- .../components/rest_command/__init__.py | 2 +- tests/components/rest_command/test_init.py | 37 ++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 1607000e8d985a..223dc7da7cc834 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -28,7 +28,7 @@ DEFAULT_METHOD = "get" DEFAULT_VERIFY_SSL = True -SUPPORT_REST_METHODS = ["get", "post", "put", "delete"] +SUPPORT_REST_METHODS = ["get", "patch", "post", "put", "delete"] CONF_CONTENT_TYPE = "content_type" diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 2f50f34f140b18..b7ac5a4be8a282 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -51,6 +51,7 @@ def setup_method(self): self.config = { rc.DOMAIN: { "get_test": {"url": self.url, "method": "get"}, + "patch_test": {"url": self.url, "method": "patch"}, "post_test": {"url": self.url, "method": "post"}, "put_test": {"url": self.url, "method": "put"}, "delete_test": {"url": self.url, "method": "delete"}, @@ -65,7 +66,7 @@ def teardown_method(self): def test_setup_tests(self): """Set up test config and test it.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) assert self.hass.services.has_service(rc.DOMAIN, "get_test") @@ -75,7 +76,7 @@ def test_setup_tests(self): def test_rest_command_timeout(self, aioclient_mock): """Call a rest command with timeout.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, exc=asyncio.TimeoutError()) @@ -87,7 +88,7 @@ def test_rest_command_timeout(self, aioclient_mock): def test_rest_command_aiohttp_error(self, aioclient_mock): """Call a rest command with aiohttp exception.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, exc=aiohttp.ClientError()) @@ -99,7 +100,7 @@ def test_rest_command_aiohttp_error(self, aioclient_mock): def test_rest_command_http_error(self, aioclient_mock): """Call a rest command with status code 400.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, status=400) @@ -114,7 +115,7 @@ def test_rest_command_auth(self, aioclient_mock): data = {"username": "test", "password": "123456"} self.config[rc.DOMAIN]["get_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, content=b"success") @@ -129,7 +130,7 @@ def test_rest_command_form_data(self, aioclient_mock): data = {"payload": "test"} self.config[rc.DOMAIN]["post_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.post(self.url, content=b"success") @@ -142,7 +143,7 @@ def test_rest_command_form_data(self, aioclient_mock): def test_rest_command_get(self, aioclient_mock): """Call a rest command with get.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, content=b"success") @@ -154,7 +155,7 @@ def test_rest_command_get(self, aioclient_mock): def test_rest_command_delete(self, aioclient_mock): """Call a rest command with delete.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.delete(self.url, content=b"success") @@ -164,12 +165,28 @@ def test_rest_command_delete(self, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 + def test_rest_command_patch(self, aioclient_mock): + """Call a rest command with patch.""" + data = {"payload": "data"} + self.config[rc.DOMAIN]["patch_test"].update(data) + + with assert_setup_component(5): + setup_component(self.hass, rc.DOMAIN, self.config) + + aioclient_mock.patch(self.url, content=b"success") + + self.hass.services.call(rc.DOMAIN, "patch_test", {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == b"data" + def test_rest_command_post(self, aioclient_mock): """Call a rest command with post.""" data = {"payload": "data"} self.config[rc.DOMAIN]["post_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.post(self.url, content=b"success") @@ -185,7 +202,7 @@ def test_rest_command_put(self, aioclient_mock): data = {"payload": "data"} self.config[rc.DOMAIN]["put_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.put(self.url, content=b"success") From ea1401d0b6b5bfa26cf4057275fc43b5bd079f92 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 21 Oct 2019 13:48:08 +0200 Subject: [PATCH 1140/3953] Upgrade discord.py to 1.2.4 (#28054) --- homeassistant/components/discord/__init__.py | 2 +- homeassistant/components/discord/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/discord/__init__.py b/homeassistant/components/discord/__init__.py index a3cd87bc895f6c..67b9f1b39ba309 100644 --- a/homeassistant/components/discord/__init__.py +++ b/homeassistant/components/discord/__init__.py @@ -1 +1 @@ -"""The discord component.""" +"""The discord integration.""" diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 50c03bad25d18a..d00d26d2b5eda6 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -3,7 +3,7 @@ "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", "requirements": [ - "discord.py==1.2.3" + "discord.py==1.2.4" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 81e31c8181762b..881bbd119d35e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,7 +408,7 @@ directpy==0.5 discogs_client==2.2.1 # homeassistant.components.discord -discord.py==1.2.3 +discord.py==1.2.4 # homeassistant.components.updater distro==1.4.0 From a05144bb8b45a8e0ed0f5daf7dbd48460e89f161 Mon Sep 17 00:00:00 2001 From: Ties de Kock Date: Mon, 21 Oct 2019 13:52:25 +0200 Subject: [PATCH 1141/3953] Fix buienradar component and add smoke tests (#27965) * Fixes the buienradar component and add smoke tests * Fix errors due to circular imports after imports were moved. * Add smoke test so this situation will be caught in the future. * Add buienradar.util to coveragerc * Refactor tests to standalone pytest test function style * Add __init__ to buienradar tests --- .coveragerc | 1 + homeassistant/components/buienradar/const.py | 7 + homeassistant/components/buienradar/sensor.py | 213 +--------------- homeassistant/components/buienradar/util.py | 228 ++++++++++++++++++ .../components/buienradar/weather.py | 4 +- tests/components/buienradar/__init__.py | 1 + tests/components/buienradar/test_sensor.py | 26 ++ tests/components/buienradar/test_weather.py | 25 ++ 8 files changed, 294 insertions(+), 211 deletions(-) create mode 100644 homeassistant/components/buienradar/const.py create mode 100644 homeassistant/components/buienradar/util.py create mode 100644 tests/components/buienradar/__init__.py create mode 100644 tests/components/buienradar/test_sensor.py create mode 100644 tests/components/buienradar/test_weather.py diff --git a/.coveragerc b/.coveragerc index 7c35022c355a43..f40b0c30342ae8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -100,6 +100,7 @@ omit = homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py homeassistant/components/buienradar/sensor.py + homeassistant/components/buienradar/util.py homeassistant/components/buienradar/weather.py homeassistant/components/caldav/calendar.py homeassistant/components/canary/alarm_control_panel.py diff --git a/homeassistant/components/buienradar/const.py b/homeassistant/components/buienradar/const.py new file mode 100644 index 00000000000000..b91d2497d77e35 --- /dev/null +++ b/homeassistant/components/buienradar/const.py @@ -0,0 +1,7 @@ +"""Constants for buienradar component.""" +DEFAULT_TIMEFRAME = 60 + +"""Schedule next call after (minutes).""" +SCHEDULE_OK = 10 +"""When an error occurred, new call after (minutes).""" +SCHEDULE_NOK = 2 diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 300bcbf2243238..5fe97b6fb38854 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -1,38 +1,23 @@ """Support for Buienradar.nl weather service.""" -import asyncio -from datetime import datetime, timedelta import logging -import aiohttp -import async_timeout -from buienradar.buienradar import parse_data from buienradar.constants import ( ATTRIBUTION, CONDCODE, CONDITION, - CONTENT, - DATA, DETAILED, EXACT, EXACTNL, FORECAST, - HUMIDITY, IMAGE, MEASURED, - MESSAGE, PRECIPITATION_FORECAST, - PRESSURE, STATIONNAME, - STATUS_CODE, - SUCCESS, - TEMPERATURE, TIMEFRAME, VISIBILITY, - WINDAZIMUTH, WINDGUST, WINDSPEED, ) -from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -44,13 +29,14 @@ CONF_NAME, TEMP_CELSIUS, ) -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_point_in_utc_time from homeassistant.util import dt as dt_util -from .weather import DEFAULT_TIMEFRAME + +from .const import DEFAULT_TIMEFRAME +from .util import BrData + _LOGGER = logging.getLogger(__name__) @@ -465,194 +451,3 @@ def icon(self): def force_update(self): """Return true for continuous sensors, false for discrete sensors.""" return self._force_update - - -class BrData: - """Get the latest data and updates the states.""" - - def __init__(self, hass, coordinates, timeframe, devices): - """Initialize the data object.""" - self.devices = devices - self.data = {} - self.hass = hass - self.coordinates = coordinates - self.timeframe = timeframe - - async def update_devices(self): - """Update all devices/sensors.""" - if self.devices: - tasks = [] - # Update all devices - for dev in self.devices: - if dev.load_data(self.data): - tasks.append(dev.async_update_ha_state()) - - if tasks: - await asyncio.wait(tasks) - - async def schedule_update(self, minute=1): - """Schedule an update after minute minutes.""" - _LOGGER.debug("Scheduling next update in %s minutes.", minute) - nxt = dt_util.utcnow() + timedelta(minutes=minute) - async_track_point_in_utc_time(self.hass, self.async_update, nxt) - - async def get_data(self, url): - """Load data from specified url.""" - - _LOGGER.debug("Calling url: %s...", url) - result = {SUCCESS: False, MESSAGE: None} - resp = None - try: - websession = async_get_clientsession(self.hass) - with async_timeout.timeout(10): - resp = await websession.get(url) - - result[STATUS_CODE] = resp.status - result[CONTENT] = await resp.text() - if resp.status == 200: - result[SUCCESS] = True - else: - result[MESSAGE] = "Got http statuscode: %d" % (resp.status) - - return result - except (asyncio.TimeoutError, aiohttp.ClientError) as err: - result[MESSAGE] = "%s" % err - return result - finally: - if resp is not None: - await resp.release() - - async def async_update(self, *_): - """Update the data from buienradar.""" - - content = await self.get_data(JSON_FEED_URL) - - if content.get(SUCCESS) is not True: - # unable to get the data - _LOGGER.warning( - "Unable to retrieve json data from Buienradar." - "(Msg: %s, status: %s,)", - content.get(MESSAGE), - content.get(STATUS_CODE), - ) - # schedule new call - await self.schedule_update(SCHEDULE_NOK) - return - - # rounding coordinates prevents unnecessary redirects/calls - lat = self.coordinates[CONF_LATITUDE] - lon = self.coordinates[CONF_LONGITUDE] - rainurl = json_precipitation_forecast_url(lat, lon) - raincontent = await self.get_data(rainurl) - - if raincontent.get(SUCCESS) is not True: - # unable to get the data - _LOGGER.warning( - "Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)", - raincontent.get(MESSAGE), - raincontent.get(STATUS_CODE), - ) - # schedule new call - await self.schedule_update(SCHEDULE_NOK) - return - - result = parse_data( - content.get(CONTENT), - raincontent.get(CONTENT), - self.coordinates[CONF_LATITUDE], - self.coordinates[CONF_LONGITUDE], - self.timeframe, - False, - ) - - _LOGGER.debug("Buienradar parsed data: %s", result) - if result.get(SUCCESS) is not True: - if int(datetime.now().strftime("%H")) > 0: - _LOGGER.warning( - "Unable to parse data from Buienradar." "(Msg: %s)", - result.get(MESSAGE), - ) - await self.schedule_update(SCHEDULE_NOK) - return - - self.data = result.get(DATA) - await self.update_devices() - await self.schedule_update(SCHEDULE_OK) - - @property - def attribution(self): - """Return the attribution.""" - - return self.data.get(ATTRIBUTION) - - @property - def stationname(self): - """Return the name of the selected weatherstation.""" - - return self.data.get(STATIONNAME) - - @property - def condition(self): - """Return the condition.""" - - return self.data.get(CONDITION) - - @property - def temperature(self): - """Return the temperature, or None.""" - - try: - return float(self.data.get(TEMPERATURE)) - except (ValueError, TypeError): - return None - - @property - def pressure(self): - """Return the pressure, or None.""" - - try: - return float(self.data.get(PRESSURE)) - except (ValueError, TypeError): - return None - - @property - def humidity(self): - """Return the humidity, or None.""" - - try: - return int(self.data.get(HUMIDITY)) - except (ValueError, TypeError): - return None - - @property - def visibility(self): - """Return the visibility, or None.""" - - try: - return int(self.data.get(VISIBILITY)) - except (ValueError, TypeError): - return None - - @property - def wind_speed(self): - """Return the windspeed, or None.""" - - try: - return float(self.data.get(WINDSPEED)) - except (ValueError, TypeError): - return None - - @property - def wind_bearing(self): - """Return the wind bearing, or None.""" - - try: - return int(self.data.get(WINDAZIMUTH)) - except (ValueError, TypeError): - return None - - @property - def forecast(self): - """Return the forecast data.""" - - return self.data.get(FORECAST) diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py new file mode 100644 index 00000000000000..579b341827128e --- /dev/null +++ b/homeassistant/components/buienradar/util.py @@ -0,0 +1,228 @@ +"""Shared utilities for different supported platforms.""" +import asyncio +from datetime import datetime, timedelta +import logging + +import aiohttp +import async_timeout + +from buienradar.buienradar import parse_data +from buienradar.constants import ( + ATTRIBUTION, + CONDITION, + CONTENT, + DATA, + FORECAST, + HUMIDITY, + MESSAGE, + PRESSURE, + STATIONNAME, + STATUS_CODE, + SUCCESS, + TEMPERATURE, + VISIBILITY, + WINDAZIMUTH, + WINDSPEED, +) +from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url + +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util import dt as dt_util + + +from .const import SCHEDULE_OK, SCHEDULE_NOK + + +_LOGGER = logging.getLogger(__name__) + + +class BrData: + """Get the latest data and updates the states.""" + + def __init__(self, hass, coordinates, timeframe, devices): + """Initialize the data object.""" + self.devices = devices + self.data = {} + self.hass = hass + self.coordinates = coordinates + self.timeframe = timeframe + + async def update_devices(self): + """Update all devices/sensors.""" + if self.devices: + tasks = [] + # Update all devices + for dev in self.devices: + if dev.load_data(self.data): + tasks.append(dev.async_update_ha_state()) + + if tasks: + await asyncio.wait(tasks) + + async def schedule_update(self, minute=1): + """Schedule an update after minute minutes.""" + _LOGGER.debug("Scheduling next update in %s minutes.", minute) + nxt = dt_util.utcnow() + timedelta(minutes=minute) + async_track_point_in_utc_time(self.hass, self.async_update, nxt) + + async def get_data(self, url): + """Load data from specified url.""" + _LOGGER.debug("Calling url: %s...", url) + result = {SUCCESS: False, MESSAGE: None} + resp = None + try: + websession = async_get_clientsession(self.hass) + with async_timeout.timeout(10): + resp = await websession.get(url) + + result[STATUS_CODE] = resp.status + result[CONTENT] = await resp.text() + if resp.status == 200: + result[SUCCESS] = True + else: + result[MESSAGE] = "Got http statuscode: %d" % (resp.status) + + return result + except (asyncio.TimeoutError, aiohttp.ClientError) as err: + result[MESSAGE] = "%s" % err + return result + finally: + if resp is not None: + await resp.release() + + async def async_update(self, *_): + """Update the data from buienradar.""" + + content = await self.get_data(JSON_FEED_URL) + + if content.get(SUCCESS) is not True: + # unable to get the data + _LOGGER.warning( + "Unable to retrieve json data from Buienradar." + "(Msg: %s, status: %s,)", + content.get(MESSAGE), + content.get(STATUS_CODE), + ) + # schedule new call + await self.schedule_update(SCHEDULE_NOK) + return + + # rounding coordinates prevents unnecessary redirects/calls + lat = self.coordinates[CONF_LATITUDE] + lon = self.coordinates[CONF_LONGITUDE] + rainurl = json_precipitation_forecast_url(lat, lon) + raincontent = await self.get_data(rainurl) + + if raincontent.get(SUCCESS) is not True: + # unable to get the data + _LOGGER.warning( + "Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)", + raincontent.get(MESSAGE), + raincontent.get(STATUS_CODE), + ) + # schedule new call + await self.schedule_update(SCHEDULE_NOK) + return + + result = parse_data( + content.get(CONTENT), + raincontent.get(CONTENT), + self.coordinates[CONF_LATITUDE], + self.coordinates[CONF_LONGITUDE], + self.timeframe, + False, + ) + + _LOGGER.debug("Buienradar parsed data: %s", result) + if result.get(SUCCESS) is not True: + if int(datetime.now().strftime("%H")) > 0: + _LOGGER.warning( + "Unable to parse data from Buienradar." "(Msg: %s)", + result.get(MESSAGE), + ) + await self.schedule_update(SCHEDULE_NOK) + return + + self.data = result.get(DATA) + await self.update_devices() + await self.schedule_update(SCHEDULE_OK) + + @property + def attribution(self): + """Return the attribution.""" + + return self.data.get(ATTRIBUTION) + + @property + def stationname(self): + """Return the name of the selected weatherstation.""" + + return self.data.get(STATIONNAME) + + @property + def condition(self): + """Return the condition.""" + + return self.data.get(CONDITION) + + @property + def temperature(self): + """Return the temperature, or None.""" + + try: + return float(self.data.get(TEMPERATURE)) + except (ValueError, TypeError): + return None + + @property + def pressure(self): + """Return the pressure, or None.""" + + try: + return float(self.data.get(PRESSURE)) + except (ValueError, TypeError): + return None + + @property + def humidity(self): + """Return the humidity, or None.""" + + try: + return int(self.data.get(HUMIDITY)) + except (ValueError, TypeError): + return None + + @property + def visibility(self): + """Return the visibility, or None.""" + + try: + return int(self.data.get(VISIBILITY)) + except (ValueError, TypeError): + return None + + @property + def wind_speed(self): + """Return the windspeed, or None.""" + + try: + return float(self.data.get(WINDSPEED)) + except (ValueError, TypeError): + return None + + @property + def wind_bearing(self): + """Return the wind bearing, or None.""" + + try: + return int(self.data.get(WINDAZIMUTH)) + except (ValueError, TypeError): + return None + + @property + def forecast(self): + """Return the forecast data.""" + + return self.data.get(FORECAST) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index 745bf12ffd8845..c95e57807c4aba 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -28,13 +28,13 @@ from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation -from .sensor import BrData +from .util import BrData +from .const import DEFAULT_TIMEFRAME _LOGGER = logging.getLogger(__name__) DATA_CONDITION = "buienradar_condition" -DEFAULT_TIMEFRAME = 60 CONF_FORECAST = "forecast" diff --git a/tests/components/buienradar/__init__.py b/tests/components/buienradar/__init__.py new file mode 100644 index 00000000000000..15cdd8646d2fbe --- /dev/null +++ b/tests/components/buienradar/__init__.py @@ -0,0 +1 @@ +"""Tests for the buienradar component.""" diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py new file mode 100644 index 00000000000000..c1569e4576be29 --- /dev/null +++ b/tests/components/buienradar/test_sensor.py @@ -0,0 +1,26 @@ +"""The tests for the Buienradar sensor platform.""" +from homeassistant.setup import async_setup_component +from homeassistant.components import sensor + + +CONDITIONS = ["stationname", "temperature"] +BASE_CONFIG = { + "sensor": [ + { + "platform": "buienradar", + "name": "volkel", + "latitude": 51.65, + "longitude": 5.7, + "monitored_conditions": CONDITIONS, + } + ] +} + + +async def test_smoke_test_setup_component(hass): + """Smoke test for successfully set-up with default config.""" + assert await async_setup_component(hass, sensor.DOMAIN, BASE_CONFIG) + + for cond in CONDITIONS: + state = hass.states.get(f"sensor.volkel_{cond}") + assert state.state == "unknown" diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py new file mode 100644 index 00000000000000..1a8c94e1712ccd --- /dev/null +++ b/tests/components/buienradar/test_weather.py @@ -0,0 +1,25 @@ +"""The tests for the buienradar weather component.""" +from homeassistant.components import weather +from homeassistant.setup import async_setup_component + + +# Example config snippet from documentation. +BASE_CONFIG = { + "weather": [ + { + "platform": "buienradar", + "name": "volkel", + "latitude": 51.65, + "longitude": 5.7, + "forecast": True, + } + ] +} + + +async def test_smoke_test_setup_component(hass): + """Smoke test for successfully set-up with default config.""" + assert await async_setup_component(hass, weather.DOMAIN, BASE_CONFIG) + + state = hass.states.get("weather.volkel") + assert state.state == "unknown" From 6ba437d83ac5db09833204fbb4fc0016993934bd Mon Sep 17 00:00:00 2001 From: Pascal Roeleven Date: Mon, 21 Oct 2019 13:56:03 +0200 Subject: [PATCH 1142/3953] Code cleanup for orangepi_gpio (#27958) * Code cleanup for orangepi_gpio * Move constants to const.py * Use async wherever possible * Remove obsolute functions * Use relative package integration imports * Move callbacks to async_added_to_hass * Avoid side effects in init * Prevent blocking I/O in coroutines * Make sure entity is setup before added --- .../components/orangepi_gpio/__init__.py | 39 ++++---------- .../components/orangepi_gpio/binary_sensor.py | 54 ++++++++++--------- .../components/orangepi_gpio/const.py | 6 +-- 3 files changed, 42 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/orangepi_gpio/__init__.py b/homeassistant/components/orangepi_gpio/__init__.py index 7547342d898765..7b686399d0fedc 100644 --- a/homeassistant/components/orangepi_gpio/__init__.py +++ b/homeassistant/components/orangepi_gpio/__init__.py @@ -1,18 +1,18 @@ """Support for controlling GPIO pins of a Orange Pi.""" + import logging +from OPi import GPIO + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) -CONF_PIN_MODE = "pin_mode" DOMAIN = "orangepi_gpio" -PIN_MODES = ["pc", "zeroplus", "zeroplus2", "deo", "neocore2"] -def setup(hass, config): +async def async_setup(hass, config): """Set up the Orange Pi GPIO component.""" - from OPi import GPIO def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -20,16 +20,16 @@ def cleanup_gpio(event): def prepare_gpio(event): """Stuff to do when home assistant starts.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) return True def setup_mode(mode): """Set GPIO pin mode.""" - from OPi import GPIO - + _LOGGER.debug("Setting GPIO pin mode as %s", mode) if mode == "pc": import orangepi.pc @@ -52,36 +52,19 @@ def setup_mode(mode): GPIO.setmode(nanopi.neocore2.BOARD) -def setup_output(port): - """Set up a GPIO as output.""" - from OPi import GPIO - - GPIO.setup(port, GPIO.OUT) - - def setup_input(port): """Set up a GPIO as input.""" - from OPi import GPIO - + _LOGGER.debug("Setting up GPIO pin %i as input", port) GPIO.setup(port, GPIO.IN) -def write_output(port, value): - """Write a value to a GPIO.""" - from OPi import GPIO - - GPIO.output(port, value) - - def read_input(port): """Read a value from a GPIO.""" - from OPi import GPIO - + _LOGGER.debug("Reading GPIO pin %i", port) return GPIO.input(port) def edge_detect(port, event_callback): """Add detection for RISING and FALLING events.""" - from OPi import GPIO - + _LOGGER.debug("Add callback for GPIO pin %i", port) GPIO.add_event_detect(port, GPIO.BOTH, callback=event_callback) diff --git a/homeassistant/components/orangepi_gpio/binary_sensor.py b/homeassistant/components/orangepi_gpio/binary_sensor.py index b89faf3e7d4bb1..b89442a571c39d 100644 --- a/homeassistant/components/orangepi_gpio/binary_sensor.py +++ b/homeassistant/components/orangepi_gpio/binary_sensor.py @@ -1,50 +1,52 @@ """Support for binary sensor using Orange Pi GPIO.""" -import logging -from homeassistant.components import orangepi_gpio -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from . import CONF_PIN_MODE -from .const import CONF_INVERT_LOGIC, CONF_PORTS, PORT_SCHEMA - -_LOGGER = logging.getLogger(__name__) +from . import edge_detect, read_input, setup_input, setup_mode +from .const import CONF_INVERT_LOGIC, CONF_PIN_MODE, CONF_PORTS, PORT_SCHEMA PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(PORT_SCHEMA) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Orange Pi GPIO devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Orange Pi GPIO platform.""" + binary_sensors = [] + invert_logic = config[CONF_INVERT_LOGIC] pin_mode = config[CONF_PIN_MODE] - orangepi_gpio.setup_mode(pin_mode) + ports = config[CONF_PORTS] - invert_logic = config[CONF_INVERT_LOGIC] + setup_mode(pin_mode) - binary_sensors = [] - ports = config[CONF_PORTS] for port_num, port_name in ports.items(): - binary_sensors.append(OPiGPIOBinarySensor(port_name, port_num, invert_logic)) - add_entities(binary_sensors, True) + binary_sensors.append( + OPiGPIOBinarySensor(hass, port_name, port_num, invert_logic) + ) + async_add_entities(binary_sensors) class OPiGPIOBinarySensor(BinarySensorDevice): """Represent a binary sensor that uses Orange Pi GPIO.""" - def __init__(self, name, port, invert_logic): + def __init__(self, hass, name, port, invert_logic): """Initialize the Orange Pi binary sensor.""" - self._name = name or DEVICE_DEFAULT_NAME + self._name = name self._port = port self._invert_logic = invert_logic self._state = None - orangepi_gpio.setup_input(self._port) + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" + + def gpio_edge_listener(port): + """Update GPIO when edge change is detected.""" + self.schedule_update_ha_state(True) - def read_gpio(port): - """Read state from GPIO.""" - self._state = orangepi_gpio.read_input(self._port) - self.schedule_update_ha_state() + def setup_entity(): + setup_input(self._port) + edge_detect(self._port, gpio_edge_listener) + self.schedule_update_ha_state(True) - orangepi_gpio.edge_detect(self._port, read_gpio) + await self.hass.async_add_executor_job(setup_entity) @property def should_poll(self): @@ -62,5 +64,5 @@ def is_on(self): return self._state != self._invert_logic def update(self): - """Update the GPIO state.""" - self._state = orangepi_gpio.read_input(self._port) + """Update state with new GPIO data.""" + self._state = read_input(self._port) diff --git a/homeassistant/components/orangepi_gpio/const.py b/homeassistant/components/orangepi_gpio/const.py index 6bb9ab1df1ee98..928d75a1f98c22 100644 --- a/homeassistant/components/orangepi_gpio/const.py +++ b/homeassistant/components/orangepi_gpio/const.py @@ -1,14 +1,14 @@ """Constants for Orange Pi GPIO.""" + import voluptuous as vol from homeassistant.helpers import config_validation as cv -from . import CONF_PIN_MODE, PIN_MODES - CONF_INVERT_LOGIC = "invert_logic" +CONF_PIN_MODE = "pin_mode" CONF_PORTS = "ports" - DEFAULT_INVERT_LOGIC = False +PIN_MODES = ["pc", "zeroplus", "zeroplus2", "deo", "neocore2"] _SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) From ac467d0b3fa529b588d1505addf3cf5f5b63cf6f Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 21 Oct 2019 14:30:49 +0200 Subject: [PATCH 1143/3953] Not slugify cert_expiry name (#28055) --- .../components/cert_expiry/config_flow.py | 10 +++++----- tests/components/cert_expiry/test_config_flow.py | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index d73762ce882647..43931fe5830f8f 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -5,7 +5,6 @@ from homeassistant import config_entries from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST from homeassistant.core import HomeAssistant, callback -from homeassistant.util import slugify from .const import DOMAIN, DEFAULT_PORT, DEFAULT_NAME from .helper import get_cert @@ -62,11 +61,12 @@ async def async_step_user(self, user_input=None): self._errors[CONF_HOST] = "host_port_exists" else: if await self._test_connection(user_input): - host = user_input[CONF_HOST] - name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) - prt = user_input.get(CONF_PORT, DEFAULT_PORT) return self.async_create_entry( - title=name, data={CONF_HOST: host, CONF_PORT: prt} + title=user_input.get(CONF_NAME, DEFAULT_NAME), + data={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input.get(CONF_PORT, DEFAULT_PORT), + }, ) else: user_input = {} diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index f44e65512e35e5..988f3e971060ac 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -5,7 +5,7 @@ from homeassistant import data_entry_flow from homeassistant.components.cert_expiry import config_flow -from homeassistant.components.cert_expiry.const import DEFAULT_PORT +from homeassistant.components.cert_expiry.const import DEFAULT_NAME, DEFAULT_PORT from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST from tests.common import MockConfigEntry, mock_coro @@ -45,7 +45,7 @@ async def test_user(hass, test_connect): {CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -57,21 +57,21 @@ async def test_import(hass, test_connect): # import with only host result = await flow.async_step_import({CONF_HOST: HOST}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "ssl_certificate_expiry" + assert result["title"] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT # import with host and name result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT # improt with host and port result = await flow.async_step_import({CONF_HOST: HOST, CONF_PORT: PORT}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "ssl_certificate_expiry" + assert result["title"] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -80,7 +80,7 @@ async def test_import(hass, test_connect): {CONF_HOST: HOST, CONF_PORT: PORT, CONF_NAME: NAME} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -112,7 +112,7 @@ async def test_abort_if_already_setup(hass, test_connect): {CONF_HOST: HOST, CONF_NAME: NAME, CONF_PORT: 888} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == 888 From 269c8f1d1477053f4aec683f2850cf34b5a7a012 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 21 Oct 2019 14:04:56 +0100 Subject: [PATCH 1144/3953] Add hvac_action to geniushub (#28056) * add hvac_action() to climate zones --- homeassistant/components/geniushub/climate.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 5acc25a36ee96c..9a19edd9f8b0e8 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -3,6 +3,9 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_ACTIVITY, @@ -68,6 +71,17 @@ def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes.""" return list(HA_HVAC_TO_GH) + @property + def hvac_action(self) -> Optional[str]: + """Return the current running hvac operation if supported.""" + if "_state" in self._zone.data: # only for v3 API + if not self._zone.data["_state"].get("bIsActive"): + return CURRENT_HVAC_OFF + if self._zone.data["_state"].get("bOutRequestHeat"): + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + return None + @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" From ba10d5d604e425a2ba17fcbf0d1d360ef8dd5bd8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 Oct 2019 16:06:57 +0200 Subject: [PATCH 1145/3953] Add ESPHome sensor force_update option (#28059) * Add ESPHome sensor force_update option * Update aioesphomeapi to 2.4.0 --- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/sensor.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index bde64762121ebd..b2286b8ab67b15 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.2.0" + "aioesphomeapi==2.4.0" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 2b7a8b94f1eaa4..b6adbf93c41ac3 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -57,6 +57,11 @@ def icon(self) -> str: """Return the icon.""" return self._static_info.icon + @property + def force_update(self) -> bool: + """Return if this sensor should force a state update.""" + return self._static_info.force_update + @esphome_state_property def state(self) -> Optional[str]: """Return the state of the entity.""" diff --git a/requirements_all.txt b/requirements_all.txt index 881bbd119d35e8..62e53c59a6446f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,7 +139,7 @@ aiobotocore==0.10.2 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.2.0 +aioesphomeapi==2.4.0 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 754a7d72a64c02..f7c4b68306cfd8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -70,7 +70,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.2 # homeassistant.components.esphome -aioesphomeapi==2.2.0 +aioesphomeapi==2.4.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 70ddab2f3cb21bc431a46ddc4c1ac0254553f17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 21 Oct 2019 17:54:59 +0300 Subject: [PATCH 1146/3953] Helpers type hint additions and improvements (#27986) * Helpers type hint additions and improvements * Fix async setup dump callback signature --- homeassistant/helpers/event.py | 71 +++++++++++++++++--------- homeassistant/helpers/restore_state.py | 17 +++--- homeassistant/helpers/storage.py | 10 ++-- 3 files changed, 57 insertions(+), 41 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b7707b844d417a..e819da9873abc7 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,13 +1,13 @@ """Helpers for listening to events.""" from datetime import datetime, timedelta import functools as ft -from typing import Callable +from typing import Any, Callable, Iterable, Optional, Union import attr from homeassistant.loader import bind_hass from homeassistant.helpers.sun import get_astral_event_next -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE +from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event from homeassistant.const import ( ATTR_NOW, EVENT_STATE_CHANGED, @@ -240,7 +240,9 @@ def point_in_time_listener(event): @callback @bind_hass -def async_call_later(hass, delay, action): +def async_call_later( + hass: HomeAssistant, delay: float, action: Callable[..., None] +) -> CALLBACK_TYPE: """Add a listener that is called in .""" return async_track_point_in_utc_time( hass, action, dt_util.utcnow() + timedelta(seconds=delay) @@ -252,7 +254,9 @@ def async_call_later(hass, delay, action): @callback @bind_hass -def async_track_time_interval(hass, action, interval): +def async_track_time_interval( + hass: HomeAssistant, action: Callable[..., None], interval: timedelta +) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" remove = None @@ -284,14 +288,14 @@ class SunListener: """Helper class to help listen to sun events.""" hass = attr.ib(type=HomeAssistant) - action = attr.ib(type=Callable) - event = attr.ib(type=str) - offset = attr.ib(type=timedelta) - _unsub_sun: CALLBACK_TYPE = attr.ib(default=None) - _unsub_config: CALLBACK_TYPE = attr.ib(default=None) + action: Callable[..., None] = attr.ib() + event: str = attr.ib() + offset: Optional[timedelta] = attr.ib() + _unsub_sun: Optional[CALLBACK_TYPE] = attr.ib(default=None) + _unsub_config: Optional[CALLBACK_TYPE] = attr.ib(default=None) @callback - def async_attach(self): + def async_attach(self) -> None: """Attach a sun listener.""" assert self._unsub_config is None @@ -302,7 +306,7 @@ def async_attach(self): self._listen_next_sun_event() @callback - def async_detach(self): + def async_detach(self) -> None: """Detach the sun listener.""" assert self._unsub_sun is not None assert self._unsub_config is not None @@ -313,7 +317,7 @@ def async_detach(self): self._unsub_config = None @callback - def _listen_next_sun_event(self): + def _listen_next_sun_event(self) -> None: """Set up the sun event listener.""" assert self._unsub_sun is None @@ -324,14 +328,14 @@ def _listen_next_sun_event(self): ) @callback - def _handle_sun_event(self, _now): + def _handle_sun_event(self, _now: Any) -> None: """Handle solar event.""" self._unsub_sun = None self._listen_next_sun_event() self.hass.async_run_job(self.action) @callback - def _handle_config_event(self, _event): + def _handle_config_event(self, _event: Any) -> None: """Handle core config update.""" assert self._unsub_sun is not None self._unsub_sun() @@ -341,7 +345,9 @@ def _handle_config_event(self, _event): @callback @bind_hass -def async_track_sunrise(hass, action, offset=None): +def async_track_sunrise( + hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None +) -> CALLBACK_TYPE: """Add a listener that will fire a specified offset from sunrise daily.""" listener = SunListener(hass, action, SUN_EVENT_SUNRISE, offset) listener.async_attach() @@ -353,7 +359,9 @@ def async_track_sunrise(hass, action, offset=None): @callback @bind_hass -def async_track_sunset(hass, action, offset=None): +def async_track_sunset( + hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None +) -> CALLBACK_TYPE: """Add a listener that will fire a specified offset from sunset daily.""" listener = SunListener(hass, action, SUN_EVENT_SUNSET, offset) listener.async_attach() @@ -366,8 +374,13 @@ def async_track_sunset(hass, action, offset=None): @callback @bind_hass def async_track_utc_time_change( - hass, action, hour=None, minute=None, second=None, local=False -): + hass: HomeAssistant, + action: Callable[..., None], + hour: Optional[Any] = None, + minute: Optional[Any] = None, + second: Optional[Any] = None, + local: bool = False, +) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" # We do not have to wrap the function with time pattern matching logic # if no pattern given @@ -386,7 +399,7 @@ def time_change_listener(event): next_time = None - def calculate_next(now): + def calculate_next(now: datetime) -> None: """Calculate and set the next time the trigger should fire.""" nonlocal next_time @@ -397,10 +410,10 @@ def calculate_next(now): # Make sure rolling back the clock doesn't prevent the timer from # triggering. - last_now = None + last_now: Optional[datetime] = None @callback - def pattern_time_change_listener(event): + def pattern_time_change_listener(event: Event) -> None: """Listen for matching time_changed events.""" nonlocal next_time, last_now @@ -427,7 +440,13 @@ def pattern_time_change_listener(event): @callback @bind_hass -def async_track_time_change(hass, action, hour=None, minute=None, second=None): +def async_track_time_change( + hass: HomeAssistant, + action: Callable[..., None], + hour: Optional[Any] = None, + minute: Optional[Any] = None, + second: Optional[Any] = None, +) -> CALLBACK_TYPE: """Add a listener that will fire if UTC time matches a pattern.""" return async_track_utc_time_change(hass, action, hour, minute, second, local=True) @@ -435,7 +454,9 @@ def async_track_time_change(hass, action, hour=None, minute=None, second=None): track_time_change = threaded_listener_factory(async_track_time_change) -def _process_state_match(parameter): +def _process_state_match( + parameter: Union[None, str, Iterable[str]] +) -> Callable[[str], bool]: """Convert parameter to function that matches input against parameter.""" if parameter is None or parameter == MATCH_ALL: return lambda _: True @@ -443,5 +464,5 @@ def _process_state_match(parameter): if isinstance(parameter, str) or not hasattr(parameter, "__iter__"): return lambda state: state == parameter - parameter = tuple(parameter) - return lambda state: state in parameter + parameter_tuple = tuple(parameter) + return lambda state: state in parameter_tuple diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index fdf52c99075a54..5d47f34b0021a1 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -164,23 +164,20 @@ async def async_dump_states(self) -> None: @callback def async_setup_dump(self, *args: Any) -> None: """Set up the restore state listeners.""" + + def _async_dump_states(*_: Any) -> None: + self.hass.async_create_task(self.async_dump_states()) + # Dump the initial states now. This helps minimize the risk of having # old states loaded by overwritting the last states once home assistant # has started and the old states have been read. - self.hass.async_create_task(self.async_dump_states()) + _async_dump_states() # Dump states periodically - async_track_time_interval( - self.hass, - lambda *_: self.hass.async_create_task(self.async_dump_states()), - STATE_DUMP_INTERVAL, - ) + async_track_time_interval(self.hass, _async_dump_states, STATE_DUMP_INTERVAL) # Dump states when stopping hass - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, - lambda *_: self.hass.async_create_task(self.async_dump_states()), - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_dump_states) @callback def async_restore_entity_added(self, entity_id: str) -> None: diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 72458d24c82089..bd18eebfb25c53 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -6,7 +6,7 @@ from typing import Dict, List, Optional, Callable, Union, Any, Type from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE from homeassistant.loader import bind_hass from homeassistant.util import json as json_util from homeassistant.helpers.event import async_call_later @@ -72,8 +72,8 @@ def __init__( self.hass = hass self._private = private self._data: Optional[Dict[str, Any]] = None - self._unsub_delay_listener = None - self._unsub_stop_listener = None + self._unsub_delay_listener: Optional[CALLBACK_TYPE] = None + self._unsub_stop_listener: Optional[CALLBACK_TYPE] = None self._write_lock = asyncio.Lock() self._load_task: Optional[asyncio.Future] = None self._encoder = encoder @@ -137,9 +137,7 @@ async def async_save(self, data: Union[Dict, List]) -> None: await self._async_handle_write_data() @callback - def async_delay_save( - self, data_func: Callable[[], Dict], delay: Optional[int] = None - ) -> None: + def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> None: """Save data with an optional delay.""" self._data = {"version": self.version, "key": self.key, "data_func": data_func} From 643257d911fd4272ed9805319ce31e3d2017ad30 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 21 Oct 2019 17:34:58 +0200 Subject: [PATCH 1147/3953] Include subscriber information when MQTT message can't be decoded (#28062) --- homeassistant/components/mqtt/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 119c9b520d7193..2fab599ac3f8e0 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -925,10 +925,11 @@ def _mqtt_handle_message(self, msg) -> None: payload = msg.payload.decode(subscription.encoding) except (AttributeError, UnicodeDecodeError): _LOGGER.warning( - "Can't decode payload %s on %s with encoding %s", + "Can't decode payload %s on %s with encoding %s (for %s)", msg.payload, msg.topic, subscription.encoding, + subscription.callback, ) continue From a0c50f4794b96fe2aec0b5e8b8914ae5a99a1091 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 21 Oct 2019 13:14:17 -0400 Subject: [PATCH 1148/3953] Leverage zigpy for IEEE address conversions (#27972) * Refactor EUI64 conversions. * Update ZHA dependencies. * Update tests. --- homeassistant/components/zha/api.py | 39 ++++++++++---------- homeassistant/components/zha/core/helpers.py | 13 ++----- homeassistant/components/zha/manifest.json | 8 ++-- requirements_all.txt | 8 ++-- requirements_test_all.txt | 8 ++-- tests/components/zha/common.py | 17 +++++++-- tests/components/zha/test_binary_sensor.py | 16 +++++--- tests/components/zha/test_device_tracker.py | 17 ++++++--- tests/components/zha/test_fan.py | 27 ++++++++++---- tests/components/zha/test_light.py | 14 +++++-- tests/components/zha/test_lock.py | 17 ++++++--- tests/components/zha/test_sensor.py | 13 +++++-- tests/components/zha/test_switch.py | 19 +++++++--- 13 files changed, 135 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index ff9f27d4843c20..ece644f816887b 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -4,6 +4,7 @@ import logging import voluptuous as vol +from zigpy.types.named import EUI64 from homeassistant.components import websocket_api from homeassistant.core import callback @@ -44,7 +45,7 @@ WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) -from .core.helpers import async_is_bindable_target, convert_ieee, get_matched_clusters +from .core.helpers import async_is_bindable_target, get_matched_clusters _LOGGER = logging.getLogger(__name__) @@ -76,16 +77,16 @@ SERVICE_SCHEMAS = { SERVICE_PERMIT: vol.Schema( { - vol.Optional(ATTR_IEEE_ADDRESS, default=None): convert_ieee, + vol.Optional(ATTR_IEEE_ADDRESS, default=None): EUI64.convert, vol.Optional(ATTR_DURATION, default=60): vol.All( vol.Coerce(int), vol.Range(0, 254) ), } ), - IEEE_SERVICE: vol.Schema({vol.Required(ATTR_IEEE_ADDRESS): convert_ieee}), + IEEE_SERVICE: vol.Schema({vol.Required(ATTR_IEEE_ADDRESS): EUI64.convert}), SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -96,7 +97,7 @@ ), SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED ): cv.positive_int, @@ -110,7 +111,7 @@ ), SERVICE_WARNING_DEVICE_WARN: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY ): cv.positive_int, @@ -131,7 +132,7 @@ ), SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -149,7 +150,7 @@ @websocket_api.websocket_command( { vol.Required("type"): "zha/devices/permit", - vol.Optional(ATTR_IEEE, default=None): convert_ieee, + vol.Optional(ATTR_IEEE, default=None): EUI64.convert, vol.Optional(ATTR_DURATION, default=60): vol.All( vol.Coerce(int), vol.Range(0, 254) ), @@ -200,7 +201,7 @@ async def websocket_get_devices(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): convert_ieee} + {vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): EUI64.convert} ) async def websocket_get_device(hass, connection, msg): """Get ZHA devices.""" @@ -252,7 +253,7 @@ def async_get_device_info(hass, device, ha_device_registry=None): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/reconfigure", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, } ) async def websocket_reconfigure_node(hass, connection, msg): @@ -267,7 +268,7 @@ async def websocket_reconfigure_node(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): convert_ieee} + {vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): EUI64.convert} ) async def websocket_device_clusters(hass, connection, msg): """Return a list of device clusters.""" @@ -305,7 +306,7 @@ async def websocket_device_clusters(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -346,7 +347,7 @@ async def websocket_device_cluster_attributes(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/commands", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -400,7 +401,7 @@ async def websocket_device_cluster_commands(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes/value", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -444,7 +445,7 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): convert_ieee} + {vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): EUI64.convert} ) async def websocket_get_bindable_devices(hass, connection, msg): """Directly bind devices.""" @@ -472,8 +473,8 @@ async def websocket_get_bindable_devices(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/bind", - vol.Required(ATTR_SOURCE_IEEE): convert_ieee, - vol.Required(ATTR_TARGET_IEEE): convert_ieee, + vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(ATTR_TARGET_IEEE): EUI64.convert, } ) async def websocket_bind_devices(hass, connection, msg): @@ -494,8 +495,8 @@ async def websocket_bind_devices(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/unbind", - vol.Required(ATTR_SOURCE_IEEE): convert_ieee, - vol.Required(ATTR_TARGET_IEEE): convert_ieee, + vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(ATTR_TARGET_IEEE): EUI64.convert, } ) async def websocket_unbind_devices(hass, connection, msg): diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 88a472716cc580..14103a5ea38cae 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -8,6 +8,8 @@ import collections import logging +from zigpy.types.named import EUI64 + from homeassistant.core import callback from .const import ( @@ -78,15 +80,6 @@ async def check_zigpy_connection(usb_path, radio_type, database_path): return True -def convert_ieee(ieee_str): - """Convert given ieee string to EUI64.""" - from zigpy.types import EUI64, uint8_t - - if ieee_str is None: - return None - return EUI64([uint8_t(p, base=16) for p in ieee_str.split(":")]) - - def get_attr_id_by_name(cluster, attr_name): """Get the attribute id for a cluster attribute by its name.""" return next( @@ -145,7 +138,7 @@ async def async_get_zha_device(hass, device_id): registry_device = device_registry.async_get(device_id) zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = convert_ieee(ieee_address) + ieee = EUI64.convert(ieee_address) return zha_gateway.devices[ieee] diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9790fbffd06312..9821ec2025bd9f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,10 +6,10 @@ "requirements": [ "bellows-homeassistant==0.10.0", "zha-quirks==0.0.26", - "zigpy-deconz==0.5.0", - "zigpy-homeassistant==0.9.0", - "zigpy-xbee-homeassistant==0.5.0", - "zigpy-zigate==0.4.1" + "zigpy-deconz==0.6.0", + "zigpy-homeassistant==0.10.0", + "zigpy-xbee-homeassistant==0.6.0", + "zigpy-zigate==0.5.0" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index 62e53c59a6446f..982c20e5972e24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2032,16 +2032,16 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.5.0 +zigpy-deconz==0.6.0 # homeassistant.components.zha -zigpy-homeassistant==0.9.0 +zigpy-homeassistant==0.10.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.5.0 +zigpy-xbee-homeassistant==0.6.0 # homeassistant.components.zha -zigpy-zigate==0.4.1 +zigpy-zigate==0.5.0 # homeassistant.components.zoneminder zm-py==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7c4b68306cfd8..047ed96bf10cfd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -641,13 +641,13 @@ zeroconf==0.23.0 zha-quirks==0.0.26 # homeassistant.components.zha -zigpy-deconz==0.5.0 +zigpy-deconz==0.6.0 # homeassistant.components.zha -zigpy-homeassistant==0.9.0 +zigpy-homeassistant==0.10.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.5.0 +zigpy-xbee-homeassistant==0.6.0 # homeassistant.components.zha -zigpy-zigate==0.4.1 +zigpy-zigate==0.5.0 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index fc29e4012cd6f8..9a559aae9b6b44 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -3,6 +3,8 @@ from unittest.mock import Mock, patch from asynctest import CoroutineMock +from zigpy.types.named import EUI64 +import zigpy.zcl.foundation as zcl_f from homeassistant.components.zha.core.const import ( DATA_ZHA, @@ -10,7 +12,6 @@ DATA_ZHA_CONFIG, DATA_ZHA_DISPATCHERS, ) -from homeassistant.components.zha.core.helpers import convert_ieee from homeassistant.util import slugify from tests.common import mock_coro @@ -21,7 +22,7 @@ class FakeApplication: def __init__(self): """Init fake application.""" - self.ieee = convert_ieee("00:15:8d:00:02:32:4f:32") + self.ieee = EUI64.convert("00:15:8d:00:02:32:4f:32") self.nwk = 0x087D @@ -71,7 +72,6 @@ def patch_cluster(cluster): 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.unbind = CoroutineMock(return_value=[0]) @@ -83,7 +83,7 @@ class FakeDevice: def __init__(self, ieee, manufacturer, model): """Init fake device.""" self._application = APPLICATION - self.ieee = convert_ieee(ieee) + self.ieee = EUI64.convert(ieee) self.nwk = 0xB79C self.zdo = Mock() self.endpoints = {0: self.zdo} @@ -230,3 +230,12 @@ async def async_test_device_join( domain, zigpy_device, cluster, use_suffix=device_type is None ) assert hass.states.get(entity_id) is not None + + +def make_zcl_header(command_id: int, global_command: bool = True) -> zcl_f.ZCLHeader: + """Cluster.handle_message() ZCL Header helper.""" + if global_command: + frc = zcl_f.FrameControl(zcl_f.FrameType.GLOBAL_COMMAND) + else: + frc = zcl_f.FrameControl(zcl_f.FrameType.CLUSTER_COMMAND) + return zcl_f.ZCLHeader(frc, tsn=1, command_id=command_id) diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 47f81787acd9a0..4fca5505d2916d 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,12 +1,16 @@ """Test zha binary sensor.""" +from zigpy.zcl.foundation import Command + from homeassistant.components.binary_sensor import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) @@ -74,13 +78,15 @@ async def async_test_binary_sensor_on_off(hass, cluster, entity_id): """Test getting on and off messages for binary sensors.""" # binary sensor on attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # binary sensor off attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 6a7638d9f868c0..0c27c1514f14b2 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -1,19 +1,25 @@ """Test ZHA Device Tracker.""" from datetime import timedelta import time + +from zigpy.zcl.foundation import Command + from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE from homeassistant.components.zha.core.registries import ( SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, ) +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) + from tests.common import async_fire_time_changed @@ -67,10 +73,11 @@ async def test_device_tracker(hass, config_entry, zha_gateway): # turn state flip attr = make_attribute(0x0020, 23) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) attr = make_attribute(0x0021, 200) - cluster.handle_message(1, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) zigpy_device.last_seen = time.time() + 10 next_update = dt_util.utcnow() + timedelta(seconds=30) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 3fe5e7937c86e2..1704ab2196b659 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,18 +1,30 @@ """Test zha fan.""" from unittest.mock import call, patch + +from zigpy.zcl.foundation import Command + from homeassistant.components import fan -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.fan import ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF -from tests.common import mock_coro +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) +from tests.common import mock_coro + async def test_fan(hass, config_entry, zha_gateway): """Test zha fan platform.""" @@ -44,13 +56,14 @@ async def test_fan(hass, config_entry, zha_gateway): # turn on at fan attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at fan attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 08c6cfe18cf211..567f61ad1e1b18 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -2,6 +2,8 @@ import asyncio from unittest.mock import MagicMock, call, patch, sentinel +from zigpy.zcl.foundation import Command + from homeassistant.components.light import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -11,6 +13,7 @@ async_test_device_join, make_attribute, make_entity_id, + make_zcl_header, ) from tests.common import mock_coro @@ -123,13 +126,14 @@ async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at light attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -138,7 +142,8 @@ async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -243,7 +248,8 @@ async def async_test_level_on_off_from_hass( async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): """Test dimmer functionality from the light.""" attr = make_attribute(0, level) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == expected_state # hass uses None for brightness of 0 in state attributes diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 7381b557310b7b..c7cc5bdd2a95f9 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -1,15 +1,21 @@ """Test zha lock.""" from unittest.mock import patch -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE + +from zigpy.zcl.foundation import Command + from homeassistant.components.lock import DOMAIN -from tests.common import mock_coro +from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED + from .common import ( + async_enable_traffic, async_init_zigpy_device, make_attribute, make_entity_id, - async_enable_traffic, + make_zcl_header, ) +from tests.common import mock_coro + LOCK_DOOR = 0 UNLOCK_DOOR = 1 @@ -43,13 +49,14 @@ async def test_lock(hass, config_entry, zha_gateway): # set state to locked attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_LOCKED # set state to unlocked attr.value.value = 2 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNLOCKED diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index faa44f349272a0..37d412e6a258af 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,12 +1,16 @@ """Test zha sensor.""" +from zigpy.zcl.foundation import Command + from homeassistant.components.sensor import DOMAIN -from homeassistant.const import STATE_UNKNOWN, STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) @@ -177,7 +181,8 @@ async def send_attribute_report(hass, cluster, attrid, value): device is paired to the zigbee network. """ attr = make_attribute(attrid, value) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index ac6bc73b809db6..9c2b2da9c95414 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -1,16 +1,22 @@ """Test zha switch.""" from unittest.mock import call, patch + +from zigpy.zcl.foundation import Command + from homeassistant.components.switch import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE -from tests.common import mock_coro +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) +from tests.common import mock_coro + ON = 1 OFF = 0 @@ -44,13 +50,14 @@ async def test_switch(hass, config_entry, zha_gateway): # turn on at switch attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at switch attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF From 86347a3d5f0f8b73e9fc519989524fcf53a66189 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Mon, 21 Oct 2019 21:42:17 +0200 Subject: [PATCH 1149/3953] Refactor Tradfri light group (#27714) * Set same manufacturer name of gateway as of devices * Refactor Tradfri light group * Restore should_poll and async_update --- .../components/tradfri/base_class.py | 23 ++++-- homeassistant/components/tradfri/const.py | 2 +- homeassistant/components/tradfri/light.py | 82 +++++-------------- 3 files changed, 38 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 632ce6b164ed64..ba90fe05d1e3cd 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -18,7 +18,6 @@ class TradfriBaseClass(Entity): def __init__(self, device, api, gateway_id): """Initialize a device.""" - self._available = True self._api = api self._device = None self._device_control = None @@ -33,7 +32,6 @@ def __init__(self, device, api, gateway_id): def _async_start_observe(self, exc=None): """Start observation of device.""" if exc: - self._available = False self.async_schedule_update_ha_state() _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) @@ -52,11 +50,6 @@ async def async_added_to_hass(self): """Start thread when added to hass.""" self._async_start_observe() - @property - def available(self): - """Return True if entity is available.""" - return self._available - @property def name(self): """Return the display name of this device.""" @@ -82,7 +75,6 @@ def _refresh(self, device): """Refresh the device data.""" self._device = device self._name = device.name - self._available = device.reachable class TradfriBaseDevice(TradfriBaseClass): @@ -91,6 +83,16 @@ class TradfriBaseDevice(TradfriBaseClass): All devices should inherit from this class. """ + def __init__(self, device, api, gateway_id): + """Initialize a device.""" + super().__init__(device, api, gateway_id) + self._available = True + + @property + def available(self): + """Return True if entity is available.""" + return self._available + @property def device_info(self): """Return the device info.""" @@ -104,3 +106,8 @@ def device_info(self): "sw_version": info.firmware_version, "via_device": (DOMAIN, self._gateway_id), } + + def _refresh(self, device): + """Refresh the device data.""" + super()._refresh(device) + self._available = device.reachable diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index a7acfcbf87625c..038f0e91c76cca 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -7,7 +7,7 @@ ATTR_SAT = "saturation" ATTR_TRADFRI_GATEWAY = "Gateway" ATTR_TRADFRI_GATEWAY_MODEL = "E1526" -ATTR_TRADFRI_MANUFACTURER = "IKEA" +ATTR_TRADFRI_MANUFACTURER = "IKEA of Sweden" ATTR_TRANSITION_TIME = "transition_time" CONF_ALLOW_TRADFRI_GROUPS = "allow_tradfri_groups" CONF_IDENTITY = "identity" diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 089f80223e8bab..9ee3c5d6a8cc63 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -1,8 +1,6 @@ """Support for IKEA Tradfri lights.""" import logging -from pytradfri.error import PytradfriError - import homeassistant.util.color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -14,8 +12,7 @@ SUPPORT_COLOR, SUPPORT_COLOR_TEMP, ) -from homeassistant.core import callback -from .base_class import TradfriBaseDevice +from .base_class import TradfriBaseDevice, TradfriBaseClass from .const import ( ATTR_DIMMER, ATTR_HUE, @@ -51,50 +48,47 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(TradfriGroup(group, api, gateway_id) for group in groups) -class TradfriGroup(Light): - """The platform class required by hass.""" +class TradfriGroup(TradfriBaseClass, Light): + """The platform class for light groups required by hass.""" - def __init__(self, group, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a Group.""" - self._api = api - self._unique_id = f"group-{gateway_id}-{group.id}" - self._group = group - self._name = group.name + super().__init__(device, api, gateway_id) - self._refresh(group) + self._unique_id = f"group-{gateway_id}-{device.id}" - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() + self._refresh(device) @property - def unique_id(self): - """Return unique ID for this group.""" - return self._unique_id + def should_poll(self): + """Poll needed for tradfri groups.""" + return True + + async def async_update(self): + """Fetch new state data for the group. + + This method is required for groups to update properly. + """ + await self._api(self._device.update()) @property def supported_features(self): """Flag supported features.""" return SUPPORTED_GROUP_FEATURES - @property - def name(self): - """Return the display name of this group.""" - return self._name - @property def is_on(self): """Return true if group lights are on.""" - return self._group.state + return self._device.state @property def brightness(self): """Return the brightness of the group lights.""" - return self._group.dimmer + return self._device.dimmer async def async_turn_off(self, **kwargs): """Instruct the group lights to turn off.""" - await self._api(self._group.set_state(0)) + await self._api(self._device.set_state(0)) async def async_turn_on(self, **kwargs): """Instruct the group lights to turn on, or dim.""" @@ -106,41 +100,9 @@ async def async_turn_on(self, **kwargs): if kwargs[ATTR_BRIGHTNESS] == 255: kwargs[ATTR_BRIGHTNESS] = 254 - await self._api(self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) + await self._api(self._device.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) else: - await self._api(self._group.set_state(1)) - - @callback - def _async_start_observe(self, exc=None): - """Start observation of light.""" - if exc: - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._group.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, group): - """Refresh the light data.""" - self._group = group - self._name = group.name - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this light.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() - - async def async_update(self): - """Fetch new state data for the group.""" - await self._api(self._group.update()) + await self._api(self._device.set_state(1)) class TradfriLight(TradfriBaseDevice, Light): From 86b204df0e8d8c988c3de7cc696536b1b7c30ee5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 21 Oct 2019 14:46:39 -0600 Subject: [PATCH 1150/3953] Update pymyq to 2.0.0 (#28069) * Update pymyq to 2.0.0 * Removed unnecessary update * Restore `type` parameter (as optional) --- homeassistant/components/myq/cover.py | 19 ++++++++----------- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index e9c3237490af16..b6da7174f05227 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,5 +1,8 @@ """Support for MyQ-Enabled Garage Doors.""" import logging + +from pymyq import login +from pymyq.errors import MyQError import voluptuous as vol from homeassistant.components.cover import ( @@ -30,35 +33,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_TYPE): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + # This parameter is no longer used; keeping it to avoid a breaking change in + # a hotfix, but in a future main release, this should be removed: + vol.Optional(CONF_TYPE): cv.string, } ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the platform.""" - from pymyq import login - from pymyq.errors import MyQError, UnsupportedBrandError - websession = aiohttp_client.async_get_clientsession(hass) username = config[CONF_USERNAME] password = config[CONF_PASSWORD] - brand = config[CONF_TYPE] try: - myq = await login(username, password, brand, websession) - except UnsupportedBrandError: - _LOGGER.error("Unsupported brand: %s", brand) - return + myq = await login(username, password, websession) except MyQError as err: _LOGGER.error("There was an error while logging in: %s", err) return - devices = await myq.get_devices() - async_add_entities([MyQDevice(device) for device in devices], True) + async_add_entities([MyQDevice(device) for device in myq.covers.values()], True) class MyQDevice(CoverDevice): diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 213679b320a632..73265b61c83bf7 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -3,7 +3,7 @@ "name": "Myq", "documentation": "https://www.home-assistant.io/integrations/myq", "requirements": [ - "pymyq==1.2.1" + "pymyq==2.0.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 982c20e5972e24..0330d7d8f608c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1322,7 +1322,7 @@ pymonoprice==0.3 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==1.2.1 +pymyq==2.0.0 # homeassistant.components.mysensors pymysensors==0.18.0 From ef194d1b82ae2e0cc6124ece88d41d9660e61b11 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 Oct 2019 22:56:02 +0200 Subject: [PATCH 1151/3953] Fix mypy missing from dev install script (#28060) * Fix mypy missing * Update bootstrap * Update script/bootstrap Co-Authored-By: cgtobi --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index ed6cd55be36273..ba594cbb341e6d 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -7,4 +7,4 @@ set -e cd "$(dirname "$0")/.." echo "Installing test dependencies..." -python3 -m pip install tox colorlog pre-commit +python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) From 6c39d77e2362000c29597de5452dbd61d0db26f0 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Mon, 21 Oct 2019 23:06:50 +0200 Subject: [PATCH 1152/3953] Upgrade youtube_dl to version 2019.10.22 (#28070) --- 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 de3d4546ca0350..4fd5470ebdfe30 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/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.10.16" + "youtube_dl==2019.10.22" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 0330d7d8f608c0..9a7ff3bb14bb33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2014,7 +2014,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.10.16 +youtube_dl==2019.10.22 # homeassistant.components.zengge zengge==0.2 From 4935ef5233d022516e476b373941d498e040c84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 21:30:17 +0000 Subject: [PATCH 1153/3953] Move imports in piglow component (#28046) * Move imports in piglow component * Fix pylint --- homeassistant/components/piglow/light.py | 28 +++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/piglow/light.py b/homeassistant/components/piglow/light.py index 31ece4a36a951c..27bbb81d31f7b3 100644 --- a/homeassistant/components/piglow/light.py +++ b/homeassistant/components/piglow/light.py @@ -2,18 +2,19 @@ import logging import subprocess +import piglow import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -29,23 +30,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Piglow Light platform.""" - import piglow - if subprocess.getoutput("i2cdetect -q -y 1 | grep -o 54") != "54": _LOGGER.error("A Piglow device was not found") return False name = config.get(CONF_NAME) - add_entities([PiglowLight(piglow, name)]) + add_entities([PiglowLight(name)]) class PiglowLight(Light): """Representation of an Piglow Light.""" - def __init__(self, piglow, name): + def __init__(self, name): """Initialize an PiglowLight.""" - self._piglow = piglow self._name = name self._is_on = False self._brightness = 255 @@ -88,7 +86,7 @@ def is_on(self): def turn_on(self, **kwargs): """Instruct the light to turn on.""" - self._piglow.clear() + piglow.clear() if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] @@ -99,16 +97,16 @@ def turn_on(self, **kwargs): rgb = color_util.color_hsv_to_RGB( self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 ) - self._piglow.red(rgb[0]) - self._piglow.green(rgb[1]) - self._piglow.blue(rgb[2]) - self._piglow.show() + piglow.red(rgb[0]) + piglow.green(rgb[1]) + piglow.blue(rgb[2]) + piglow.show() self._is_on = True self.schedule_update_ha_state() def turn_off(self, **kwargs): """Instruct the light to turn off.""" - self._piglow.clear() - self._piglow.show() + piglow.clear() + piglow.show() self._is_on = False self.schedule_update_ha_state() From b0d246d6a782d03d30a142194e3a4fd36846e14d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 13 Oct 2019 23:59:25 -0500 Subject: [PATCH 1154/3953] Rewrite Plex tests (#27624) --- tests/components/plex/mock_classes.py | 96 ++++-- tests/components/plex/test_config_flow.py | 339 +++++++++------------- 2 files changed, 217 insertions(+), 218 deletions(-) diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 87fb6df597182f..756249110ed962 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -1,35 +1,97 @@ """Mock classes used in tests.""" +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.components.plex.const import CONF_SERVER, CONF_SERVER_IDENTIFIER -MOCK_HOST_1 = "1.2.3.4" -MOCK_PORT_1 = 32400 -MOCK_HOST_2 = "4.3.2.1" -MOCK_PORT_2 = 32400 +MOCK_SERVERS = [ + { + CONF_HOST: "1.2.3.4", + CONF_PORT: 32400, + CONF_SERVER: "Plex Server 1", + CONF_SERVER_IDENTIFIER: "unique_id_123", + }, + { + CONF_HOST: "4.3.2.1", + CONF_PORT: 32400, + CONF_SERVER: "Plex Server 2", + CONF_SERVER_IDENTIFIER: "unique_id_456", + }, +] -class MockAvailableServer: # pylint: disable=too-few-public-methods - """Mock avilable server objects.""" +class MockResource: + """Mock a PlexAccount resource.""" - def __init__(self, name, client_id): + def __init__(self, index): """Initialize the object.""" - self.name = name - self.clientIdentifier = client_id # pylint: disable=invalid-name + self.name = MOCK_SERVERS[index][CONF_SERVER] + self.clientIdentifier = MOCK_SERVERS[index][ # pylint: disable=invalid-name + CONF_SERVER_IDENTIFIER + ] self.provides = ["server"] + self._mock_plex_server = MockPlexServer(index) + self._connections = [] + for connection in range(2): + self._connections.append(MockConnection(connection)) + + @property + def connections(self): + """Mock the resource connection listing method.""" + return self._connections + + def connect(self): + """Mock the resource connect method.""" + return self._mock_plex_server class MockConnection: # pylint: disable=too-few-public-methods """Mock a single account resource connection object.""" - def __init__(self, ssl): + def __init__(self, index, ssl=True): """Initialize the object.""" prefix = "https" if ssl else "http" - self.httpuri = f"{prefix}://{MOCK_HOST_1}:{MOCK_PORT_1}" - self.uri = "{prefix}://{MOCK_HOST_2}:{MOCK_PORT_2}" - self.local = True + self.httpuri = ( + f"http://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" + ) + self.uri = f"{prefix}://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" + # Only first server is local + self.local = not bool(index) + + +class MockPlexAccount: + """Mock a PlexAccount instance.""" + + def __init__(self, servers=1): + """Initialize the object.""" + self._resources = [] + for index in range(servers): + self._resources.append(MockResource(index)) + + def resource(self, name): + """Mock the PlexAccount resource lookup method.""" + return [x for x in self._resources if x.name == name][0] + + def resources(self): + """Mock the PlexAccount resources listing method.""" + return self._resources -class MockConnections: # pylint: disable=too-few-public-methods - """Mock a list of resource connections.""" +class MockPlexServer: + """Mock a PlexServer instance.""" - def __init__(self, ssl=False): + def __init__(self, index=0, ssl=True): """Initialize the object.""" - self.connections = [MockConnection(ssl)] + host = MOCK_SERVERS[index][CONF_HOST] + port = MOCK_SERVERS[index][CONF_PORT] + self.friendlyName = MOCK_SERVERS[index][ # pylint: disable=invalid-name + CONF_SERVER + ] + self.machineIdentifier = MOCK_SERVERS[index][ # pylint: disable=invalid-name + CONF_SERVER_IDENTIFIER + ] + prefix = "https" if ssl else "http" + self._baseurl = f"{prefix}://{host}:{port}" + + @property + def url_in_use(self): + """Return URL used by PlexServer.""" + return self._baseurl diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index e9f48f6a4f80b4..2a2178da9d522b 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,28 +1,26 @@ """Tests for Plex config flow.""" -from unittest.mock import MagicMock, Mock, patch, PropertyMock +from unittest.mock import patch import asynctest import plexapi.exceptions import requests.exceptions from homeassistant.components.plex import config_flow -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_TOKEN, CONF_URL from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -from .mock_classes import MOCK_HOST_1, MOCK_PORT_1, MockAvailableServer, MockConnections +from .mock_classes import MOCK_SERVERS, MockPlexAccount, MockPlexServer -MOCK_NAME_1 = "Plex Server 1" -MOCK_ID_1 = "unique_id_123" -MOCK_NAME_2 = "Plex Server 2" -MOCK_ID_2 = "unique_id_456" MOCK_TOKEN = "secret_token" MOCK_FILE_CONTENTS = { - f"{MOCK_HOST_1}:{MOCK_PORT_1}": {"ssl": False, "token": MOCK_TOKEN, "verify": True} + f"{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}": { + "ssl": False, + "token": MOCK_TOKEN, + "verify": True, + } } -MOCK_SERVER_1 = MockAvailableServer(MOCK_NAME_1, MOCK_ID_1) -MOCK_SERVER_2 = MockAvailableServer(MOCK_NAME_2, MOCK_ID_2) DEFAULT_OPTIONS = { config_flow.MP_DOMAIN: { @@ -41,25 +39,21 @@ def init_config_flow(hass): async def test_bad_credentials(hass): """Test when provided credentials are rejected.""" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "start_website_auth" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + + with patch( + "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value="BAD TOKEN" ): - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -74,31 +68,32 @@ async def test_import_file_from_discovery(hass): """Test importing a legacy file during discovery.""" file_host_and_port, file_config = list(MOCK_FILE_CONTENTS.items())[0] - used_url = f"http://{file_host_and_port}" + file_use_ssl = file_config[CONF_SSL] + file_prefix = "https" if file_use_ssl else "http" + used_url = f"{file_prefix}://{file_host_and_port}" + + mock_plex_server = MockPlexServer(ssl=file_use_ssl) - with patch("plexapi.server.PlexServer") as mock_plex_server, patch( + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "homeassistant.components.plex.config_flow.load_json", return_value=MOCK_FILE_CONTENTS, ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_ID_1 - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_NAME_1 - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=used_url) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "discovery"}, - data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, ) assert result["type"] == "create_entry" - assert result["title"] == MOCK_NAME_1 - assert result["data"][config_flow.CONF_SERVER] == MOCK_NAME_1 - assert result["data"][config_flow.CONF_SERVER_IDENTIFIER] == MOCK_ID_1 + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == mock_plex_server.machineIdentifier + ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] == used_url assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] @@ -112,7 +107,10 @@ async def test_discovery(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "discovery"}, - data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, ) assert result["type"] == "abort" assert result["reason"] == "discovery_no_file" @@ -128,7 +126,10 @@ async def test_discovery_while_in_progress(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "discovery"}, - data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -137,42 +138,28 @@ async def test_discovery_while_in_progress(hass): async def test_import_success(hass): """Test a successful configuration import.""" - mock_connections = MockConnections(ssl=True) - - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - - with patch("plexapi.server.PlexServer") as mock_plex_server: - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + mock_plex_server = MockPlexServer() + with patch("plexapi.server.PlexServer", return_value=mock_plex_server): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "import"}, data={ CONF_TOKEN: MOCK_TOKEN, - CONF_URL: f"https://{MOCK_HOST_1}:{MOCK_PORT_1}", + CONF_URL: f"https://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", }, ) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -188,7 +175,7 @@ async def test_import_bad_hostname(hass): context={"source": "import"}, data={ CONF_TOKEN: MOCK_TOKEN, - CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}", + CONF_URL: f"http://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", }, ) assert result["type"] == "form" @@ -205,19 +192,12 @@ async def test_unknown_exception(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer", side_effect=Exception - ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( - "plexauth.PlexAuth.token", return_value="MOCK_TOKEN" - ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception), asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch("plexauth.PlexAuth.token", return_value="MOCK_TOKEN"): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -237,18 +217,15 @@ async def test_no_servers_found(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[]) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" with patch( - "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0) ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -261,6 +238,8 @@ async def test_no_servers_found(hass): async def test_single_available_server(hass): """Test creating an entry with one server available.""" + mock_plex_server = MockPlexServer() + await async_setup_component(hass, "http", {"http": {}}) result = await hass.config_entries.flow.async_init( @@ -269,46 +248,28 @@ async def test_single_available_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer" - ) as mock_plex_server, asynctest.patch( - "plexauth.PlexAuth.initiate_auth" - ), asynctest.patch( + with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -316,6 +277,8 @@ async def test_single_available_server(hass): async def test_multiple_servers_with_selection(hass): """Test creating an entry with multiple servers available.""" + mock_plex_server = MockPlexServer() + await async_setup_component(hass, "http", {"http": {}}) result = await hass.config_entries.flow.async_init( @@ -324,31 +287,18 @@ async def test_multiple_servers_with_selection(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) - mm_plex_account.resource = Mock(return_value=mock_connections) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer" - ) as mock_plex_server, asynctest.patch( + with patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) + ), patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch( "plexauth.PlexAuth.initiate_auth" ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -357,18 +307,21 @@ async def test_multiple_servers_with_selection(hass): assert result["step_id"] == "select_server" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={config_flow.CONF_SERVER: MOCK_SERVER_1.name} + result["flow_id"], + user_input={ + config_flow.CONF_SERVER: MOCK_SERVERS[0][config_flow.CONF_SERVER] + }, ) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -376,13 +329,17 @@ async def test_multiple_servers_with_selection(hass): async def test_adding_last_unconfigured_server(hass): """Test automatically adding last unconfigured server when multiple servers on account.""" + mock_plex_server = MockPlexServer() + await async_setup_component(hass, "http", {"http": {}}) MockConfigEntry( domain=config_flow.DOMAIN, data={ - config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2, - config_flow.CONF_SERVER: MOCK_NAME_2, + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[1][ + config_flow.CONF_SERVER_IDENTIFIER + ], + config_flow.CONF_SERVER: MOCK_SERVERS[1][config_flow.CONF_SERVER], }, ).add_to_hass(hass) @@ -392,45 +349,32 @@ async def test_adding_last_unconfigured_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) - mm_plex_account.resource = Mock(return_value=mock_connections) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer" - ) as mock_plex_server, asynctest.patch( + with patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) + ), patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch( "plexauth.PlexAuth.initiate_auth" ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -438,32 +382,28 @@ async def test_adding_last_unconfigured_server(hass): async def test_already_configured(hass): """Test a duplicated successful flow.""" + mock_plex_server = MockPlexServer() + flow = init_config_flow(hass) MockConfigEntry( - domain=config_flow.DOMAIN, data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1} + domain=config_flow.DOMAIN, + data={ + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][ + config_flow.CONF_SERVER_IDENTIFIER + ] + }, ).add_to_hass(hass) - mock_connections = MockConnections() - - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - - with patch("plexapi.server.PlexServer") as mock_plex_server, asynctest.patch( - "plexauth.PlexAuth.initiate_auth" - ), asynctest.patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - + with patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): result = await flow.async_step_import( - {CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}"} + { + CONF_TOKEN: MOCK_TOKEN, + CONF_URL: f"http://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", + } ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -477,16 +417,20 @@ async def test_all_available_servers_configured(hass): MockConfigEntry( domain=config_flow.DOMAIN, data={ - config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1, - config_flow.CONF_SERVER: MOCK_NAME_1, + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][ + config_flow.CONF_SERVER_IDENTIFIER + ], + config_flow.CONF_SERVER: MOCK_SERVERS[0][config_flow.CONF_SERVER], }, ).add_to_hass(hass) MockConfigEntry( domain=config_flow.DOMAIN, data={ - config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2, - config_flow.CONF_SERVER: MOCK_NAME_2, + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[1][ + config_flow.CONF_SERVER_IDENTIFIER + ], + config_flow.CONF_SERVER: MOCK_SERVERS[1][config_flow.CONF_SERVER], }, ).add_to_hass(hass) @@ -496,20 +440,14 @@ async def test_all_available_servers_configured(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) - mm_plex_account.resource = Mock(return_value=mock_connections) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" with patch( - "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -557,13 +495,12 @@ async def test_external_timed_out(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=None ): - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -583,12 +520,12 @@ async def test_callback_view(hass, aiohttp_client): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - client = await aiohttp_client(hass.http.app) forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' From b79716ab93fd317cf4d252a7aeeb5c8efccfae3b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 17 Oct 2019 19:31:53 -0500 Subject: [PATCH 1155/3953] Use URI provided by Plex for local connections (#27515) * Use provided URI for local connections * Use provided plexapi connection method * Remove unused mock from tests * Handle potential edge case(s) --- homeassistant/components/plex/config_flow.py | 5 +++-- homeassistant/components/plex/server.py | 18 +++++++--------- tests/components/plex/mock_classes.py | 22 -------------------- 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 38727ccff067f4..9e74756977d4cf 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -89,9 +89,10 @@ async def async_step_server_validate(self, server_config): _LOGGER.error("Invalid credentials provided, config not created") errors["base"] = "faulty_credentials" except (plexapi.exceptions.NotFound, requests.exceptions.ConnectionError): - _LOGGER.error( - "Plex server could not be reached: %s", server_config[CONF_URL] + server_identifier = ( + server_config.get(CONF_URL) or plex_server.server_choice or "Unknown" ) + _LOGGER.error("Plex server could not be reached: %s", server_identifier) errors["base"] = "not_found" except ServerNotSpecified as available_servers: diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 6ab114307664b3..d9ddc28c89a851 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -39,11 +39,12 @@ def __init__(self, server_config, options=None): self._server_name = server_config.get(CONF_SERVER) self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) self.options = options + self.server_choice = None def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" - def _set_missing_url(): + def _connect_with_token(): account = plexapi.myplex.MyPlexAccount(token=self._token) available_servers = [ (x.name, x.clientIdentifier) @@ -56,13 +57,10 @@ def _set_missing_url(): if not self._server_name and len(available_servers) > 1: raise ServerNotSpecified(available_servers) - server_choice = ( + self.server_choice = ( self._server_name if self._server_name else available_servers[0][0] ) - connections = account.resource(server_choice).connections - local_url = [x.httpuri for x in connections if x.local] - remote_url = [x.uri for x in connections if not x.local] - self._url = local_url[0] if local_url else remote_url[0] + self._plex_server = account.resource(self.server_choice).connect() def _connect_with_url(): session = None @@ -73,10 +71,10 @@ def _connect_with_url(): self._url, self._token, session ) - if self._token and not self._url: - _set_missing_url() - - _connect_with_url() + if self._url: + _connect_with_url() + else: + _connect_with_token() def clients(self): """Pass through clients call to plexapi.""" diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 756249110ed962..69e6a84df63622 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -29,34 +29,12 @@ def __init__(self, index): ] self.provides = ["server"] self._mock_plex_server = MockPlexServer(index) - self._connections = [] - for connection in range(2): - self._connections.append(MockConnection(connection)) - - @property - def connections(self): - """Mock the resource connection listing method.""" - return self._connections def connect(self): """Mock the resource connect method.""" return self._mock_plex_server -class MockConnection: # pylint: disable=too-few-public-methods - """Mock a single account resource connection object.""" - - def __init__(self, index, ssl=True): - """Initialize the object.""" - prefix = "https" if ssl else "http" - self.httpuri = ( - f"http://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" - ) - self.uri = f"{prefix}://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" - # Only first server is local - self.local = not bool(index) - - class MockPlexAccount: """Mock a PlexAccount instance.""" From e8b2d7d248fb25b9b7178e2ca74ab5ae71b2e16f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 21 Oct 2019 14:46:39 -0600 Subject: [PATCH 1156/3953] Update pymyq to 2.0.0 (#28069) * Update pymyq to 2.0.0 * Removed unnecessary update * Restore `type` parameter (as optional) --- homeassistant/components/myq/cover.py | 19 ++++++++----------- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index e9c3237490af16..b6da7174f05227 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,5 +1,8 @@ """Support for MyQ-Enabled Garage Doors.""" import logging + +from pymyq import login +from pymyq.errors import MyQError import voluptuous as vol from homeassistant.components.cover import ( @@ -30,35 +33,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_TYPE): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + # This parameter is no longer used; keeping it to avoid a breaking change in + # a hotfix, but in a future main release, this should be removed: + vol.Optional(CONF_TYPE): cv.string, } ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the platform.""" - from pymyq import login - from pymyq.errors import MyQError, UnsupportedBrandError - websession = aiohttp_client.async_get_clientsession(hass) username = config[CONF_USERNAME] password = config[CONF_PASSWORD] - brand = config[CONF_TYPE] try: - myq = await login(username, password, brand, websession) - except UnsupportedBrandError: - _LOGGER.error("Unsupported brand: %s", brand) - return + myq = await login(username, password, websession) except MyQError as err: _LOGGER.error("There was an error while logging in: %s", err) return - devices = await myq.get_devices() - async_add_entities([MyQDevice(device) for device in devices], True) + async_add_entities([MyQDevice(device) for device in myq.covers.values()], True) class MyQDevice(CoverDevice): diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 213679b320a632..73265b61c83bf7 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -3,7 +3,7 @@ "name": "Myq", "documentation": "https://www.home-assistant.io/integrations/myq", "requirements": [ - "pymyq==1.2.1" + "pymyq==2.0.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 3707eeae99ede5..448504eba5b391 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1316,7 +1316,7 @@ pymonoprice==0.3 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==1.2.1 +pymyq==2.0.0 # homeassistant.components.mysensors pymysensors==0.18.0 From 60846183f01077cf75fdf45cb86ecdb02a085a33 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Oct 2019 14:57:02 -0700 Subject: [PATCH 1157/3953] Bumped version to 0.100.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5c47c5e85b2543..8c7299e29623a9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 100 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) From 0d16e025df0ecb548faec48be7831cc8026923be Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 16 Oct 2019 17:11:25 +0200 Subject: [PATCH 1158/3953] New cache on Azure (#27739) * New cache on Azure * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml --- azure-pipelines-ci.yml | 48 +++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 13f0915bc56f12..2c3728f1f8cc38 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -37,12 +37,14 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - displayName: 'Setup Env' + . venv/bin/activate + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate flake8 homeassistant tests script @@ -52,12 +54,14 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -e . - displayName: 'Setup Env' + . venv/bin/activate + pip install -e . - script: | . venv/bin/activate python -m script.hassfest validate @@ -71,12 +75,14 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - displayName: 'Setup Env' + . venv/bin/activate + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate ./script/check_format @@ -100,7 +106,7 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_test_all.txt, .cache, homeassistant/package_constraints.txt' + keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt' build: | set -e python -m venv venv @@ -111,6 +117,10 @@ stages: # 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 + pip install -e . + displayName: 'Install Home Assistant' - script: | set -e @@ -140,7 +150,7 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_all.txt, requirements_test.txt, .cache, homeassistant/package_constraints.txt' + keyfile: 'requirements_all.txt | requirements_test.txt | homeassistant/package_constraints.txt' build: | set -e python -m venv venv @@ -149,6 +159,10 @@ stages: 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 + pip install -e . + displayName: 'Install Home Assistant' - script: | . venv/bin/activate pylint homeassistant From 92508af25325a284305e1f269a94c220f08dde13 Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 22 Oct 2019 00:01:35 +0200 Subject: [PATCH 1159/3953] Counter configure with value (#28066) --- homeassistant/components/counter/__init__.py | 11 ++++ .../components/counter/services.yaml | 6 ++ tests/components/counter/test_init.py | 57 ++++++++++++++++++- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 79877d63f14ba1..aca3461b4f7fbe 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -16,6 +16,7 @@ ATTR_STEP = "step" ATTR_MINIMUM = "minimum" ATTR_MAXIMUM = "maximum" +VALUE = "value" CONF_INITIAL = "initial" CONF_RESTORE = "restore" @@ -37,6 +38,8 @@ vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), vol.Optional(ATTR_STEP): cv.positive_int, + vol.Optional(ATTR_INITIAL): cv.positive_int, + vol.Optional(VALUE): cv.positive_int, } ) @@ -171,6 +174,10 @@ async def async_added_to_hass(self): state = await self.async_get_last_state() if state is not None: self._state = self.compute_next_state(int(state.state)) + self._initial = state.attributes.get(ATTR_INITIAL) + self._max = state.attributes.get(ATTR_MAXIMUM) + self._min = state.attributes.get(ATTR_MINIMUM) + self._step = state.attributes.get(ATTR_STEP) async def async_decrement(self): """Decrement the counter.""" @@ -195,6 +202,10 @@ async def async_configure(self, **kwargs): self._max = kwargs[CONF_MAXIMUM] if CONF_STEP in kwargs: self._step = kwargs[CONF_STEP] + if CONF_INITIAL in kwargs: + self._initial = kwargs[CONF_INITIAL] + if VALUE in kwargs: + self._state = kwargs[VALUE] self._state = self.compute_next_state(self._state) await self.async_update_ha_state() diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml index fc3f0ad36cb5a4..449ae6841ffeaf 100644 --- a/homeassistant/components/counter/services.yaml +++ b/homeassistant/components/counter/services.yaml @@ -33,3 +33,9 @@ configure: step: description: New value for step example: 2 + initial: + description: New value for initial + example: 6 + value: + description: New state value + example: 3 diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 664f4d014b7fd9..8ce90e164b69e0 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -174,14 +174,22 @@ def test_initial_state_overrules_restore_state(hass): @asyncio.coroutine def test_restore_state_overrules_initial_state(hass): """Ensure states are restored on startup.""" + + attr = {"initial": 6, "minimum": 1, "maximum": 8, "step": 2} + mock_restore_cache( - hass, (State("counter.test1", "11"), State("counter.test2", "-22")) + hass, + ( + State("counter.test1", "11"), + State("counter.test2", "-22"), + State("counter.test3", "5", attr), + ), ) hass.state = CoreState.starting yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"test1": {}, "test2": {CONF_INITIAL: 10}}} + hass, DOMAIN, {DOMAIN: {"test1": {}, "test2": {CONF_INITIAL: 10}, "test3": {}}} ) state = hass.states.get("counter.test1") @@ -192,6 +200,14 @@ def test_restore_state_overrules_initial_state(hass): assert state assert int(state.state) == -22 + state = hass.states.get("counter.test3") + assert state + assert int(state.state) == 5 + assert state.attributes.get("initial") == 6 + assert state.attributes.get("minimum") == 1 + assert state.attributes.get("maximum") == 8 + assert state.attributes.get("step") == 2 + @asyncio.coroutine def test_no_initial_state_and_no_restore_state(hass): @@ -379,11 +395,45 @@ async def test_configure(hass, hass_admin_user): assert state.state == "5" assert 3 == state.attributes.get("step") + # update value + await hass.services.async_call( + "counter", + "configure", + {"entity_id": state.entity_id, "value": 6}, + True, + Context(user_id=hass_admin_user.id), + ) + + state = hass.states.get("counter.test") + assert state is not None + assert state.state == "6" + + # update initial + await hass.services.async_call( + "counter", + "configure", + {"entity_id": state.entity_id, "initial": 5}, + True, + Context(user_id=hass_admin_user.id), + ) + + state = hass.states.get("counter.test") + assert state is not None + assert state.state == "6" + assert 5 == state.attributes.get("initial") + # update all await hass.services.async_call( "counter", "configure", - {"entity_id": state.entity_id, "step": 5, "minimum": 0, "maximum": 9}, + { + "entity_id": state.entity_id, + "step": 5, + "minimum": 0, + "maximum": 9, + "value": 5, + "initial": 6, + }, True, Context(user_id=hass_admin_user.id), ) @@ -394,3 +444,4 @@ async def test_configure(hass, hass_admin_user): assert 5 == state.attributes.get("step") assert 0 == state.attributes.get("minimum") assert 9 == state.attributes.get("maximum") + assert 6 == state.attributes.get("initial") From fc8920646b4b860bd9e9230add3d47dd192a1318 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 21 Oct 2019 18:29:04 -0500 Subject: [PATCH 1160/3953] Fix Plex test timeouts (#28077) * Ensure mocked calls are inside patch * Avoid filesytem I/O --- tests/components/plex/test_config_flow.py | 75 +++++++++++------------ 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 2a2178da9d522b..c0d14f1efdcb9c 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -46,14 +46,14 @@ async def test_bad_credentials(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value="BAD TOKEN" ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -103,17 +103,17 @@ async def test_import_file_from_discovery(hass): async def test_discovery(hass): """Test starting a flow from discovery.""" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": "discovery"}, - data={ - CONF_HOST: MOCK_SERVERS[0][CONF_HOST], - CONF_PORT: MOCK_SERVERS[0][CONF_PORT], - }, - ) - assert result["type"] == "abort" - assert result["reason"] == "discovery_no_file" + with patch("homeassistant.components.plex.config_flow.load_json", return_value={}): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "discovery"}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "discovery_no_file" async def test_discovery_while_in_progress(hass): @@ -192,12 +192,12 @@ async def test_unknown_exception(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception), asynctest.patch( "plexauth.PlexAuth.initiate_auth" ), asynctest.patch("plexauth.PlexAuth.token", return_value="MOCK_TOKEN"): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -217,14 +217,13 @@ async def test_no_servers_found(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0) ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -248,14 +247,14 @@ async def test_single_available_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( "plexapi.server.PlexServer", return_value=mock_plex_server ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -287,9 +286,6 @@ async def test_multiple_servers_with_selection(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), patch( @@ -299,6 +295,9 @@ async def test_multiple_servers_with_selection(hass): ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -349,9 +348,6 @@ async def test_adding_last_unconfigured_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), patch( @@ -361,6 +357,9 @@ async def test_adding_last_unconfigured_server(hass): ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -440,14 +439,14 @@ async def test_all_available_servers_configured(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -495,12 +494,12 @@ async def test_external_timed_out(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=None ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -520,12 +519,12 @@ async def test_callback_view(hass, aiohttp_client): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + client = await aiohttp_client(hass.http.app) forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' From fe7c45b3636e80eed69433d21465b89bacb24601 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 21 Oct 2019 19:30:56 -0400 Subject: [PATCH 1161/3953] Move remaining of ZHA imports to top level. (#28071) * Move ZHA import to top level. * ZHA tests: move imports to top level. --- homeassistant/components/zha/api.py | 2 +- .../components/zha/core/channels/__init__.py | 14 +- homeassistant/components/zha/core/device.py | 13 +- .../components/zha/core/discovery.py | 7 +- homeassistant/components/zha/core/gateway.py | 6 +- homeassistant/components/zha/core/helpers.py | 28 ++-- .../components/zha/core/registries.py | 138 ++++++++---------- tests/components/zha/common.py | 41 ++---- tests/components/zha/conftest.py | 9 +- tests/components/zha/test_api.py | 20 ++- tests/components/zha/test_binary_sensor.py | 22 ++- tests/components/zha/test_config_flow.py | 2 + tests/components/zha/test_device_action.py | 26 ++-- tests/components/zha/test_device_tracker.py | 27 ++-- tests/components/zha/test_device_trigger.py | 18 +-- tests/components/zha/test_fan.py | 19 ++- tests/components/zha/test_light.py | 51 ++++--- tests/components/zha/test_lock.py | 26 ++-- tests/components/zha/test_sensor.py | 45 +++--- tests/components/zha/test_switch.py | 21 ++- 20 files changed, 262 insertions(+), 273 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index ece644f816887b..6f24db442dd26f 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -5,6 +5,7 @@ import voluptuous as vol from zigpy.types.named import EUI64 +import zigpy.zdo.types as zdo_types from homeassistant.components import websocket_api from homeassistant.core import callback @@ -514,7 +515,6 @@ async def websocket_unbind_devices(hass, connection, msg): async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operation): """Create or remove a direct zigbee binding between 2 devices.""" - from zigpy.zdo import types as zdo_types source_device = zha_gateway.get_device(source_ieee) target_device = zha_gateway.get_device(target_ieee) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 37b0bec207b986..66a31ff8f21f95 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -11,6 +11,8 @@ import logging from random import uniform +import zigpy.exceptions + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -48,8 +50,6 @@ def decorate_command(channel, command): @wraps(command) async def wrapper(*args, **kwds): - from zigpy.exceptions import DeliveryError - try: result = await command(*args, **kwds) channel.debug( @@ -61,7 +61,7 @@ async def wrapper(*args, **kwds): ) return result - except (DeliveryError, Timeout) as ex: + except (zigpy.exceptions.DeliveryError, Timeout) as ex: channel.debug("command failed: %s exception: %s", command.__name__, str(ex)) return ex @@ -143,12 +143,10 @@ async def bind(self): 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: + except (zigpy.exceptions.DeliveryError, Timeout) as ex: self.debug( "Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex) ) @@ -167,8 +165,6 @@ async def configure_reporting( 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 = {} @@ -189,7 +185,7 @@ async def configure_reporting( reportable_change, res, ) - except (DeliveryError, Timeout) as ex: + except (zigpy.exceptions.DeliveryError, Timeout) as ex: self.debug( "failed to set reporting for '%s' attr on '%s' cluster: %s", attr_name, diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index f4a3a2c3d48b5e..b3be8037ff6118 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -10,6 +10,10 @@ import logging import time +import zigpy.exceptions +import zigpy.quirks +from zigpy.profiles import zha, zll + from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -87,9 +91,7 @@ def __init__(self, hass, zigpy_device, zha_gateway): self._unsub = async_dispatcher_connect( self.hass, self._available_signal, self.async_initialize ) - from zigpy.quirks import CustomDevice - - self.quirk_applied = isinstance(self._zigpy_device, CustomDevice) + self.quirk_applied = isinstance(self._zigpy_device, zigpy.quirks.CustomDevice) self.quirk_class = "{}.{}".format( self._zigpy_device.__class__.__module__, self._zigpy_device.__class__.__name__, @@ -394,7 +396,6 @@ def async_get_clusters(self): @callback def async_get_std_clusters(self): """Get ZHA and ZLL clusters for this device.""" - from zigpy.profiles import zha, zll return { ep_id: { @@ -448,8 +449,6 @@ async def write_zigbee_attribute( if cluster is None: return None - from zigpy.exceptions import DeliveryError - try: response = await cluster.write_attributes( {attribute: value}, manufacturer=manufacturer @@ -463,7 +462,7 @@ async def write_zigbee_attribute( response, ) return response - except DeliveryError as exc: + except zigpy.exceptions.DeliveryError as exc: self.debug( "failed to set attribute: %s %s %s %s %s", f"{ATTR_VALUE}: {value}", diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 622adead803f1c..e23862a7d3e891 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -7,6 +7,9 @@ import logging +import zigpy.profiles +from zigpy.zcl.clusters.general import OnOff, PowerConfiguration + from homeassistant import const as ha_const from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.sensor import DOMAIN as SENSOR @@ -52,8 +55,6 @@ def async_process_endpoint( is_new_join, ): """Process an endpoint on a zigpy device.""" - import zigpy.profiles - if endpoint_id == 0: # ZDO _async_create_cluster_channel( endpoint, zha_device, is_new_join, channel_class=ZDOChannel @@ -179,8 +180,6 @@ def _async_handle_single_cluster_matches( hass, endpoint, zha_device, profile_clusters, device_key, is_new_join ): """Dispatch single cluster matches to HA components.""" - from zigpy.zcl.clusters.general import OnOff, PowerConfiguration - cluster_matches = [] cluster_match_results = [] matched_power_configuration = False diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index a64e8cf7fd98ed..77702c8f3de81c 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -108,9 +108,9 @@ async def async_initialize(self): baudrate = self._config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE) radio_type = self._config_entry.data.get(CONF_RADIO_TYPE) - 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] + radio_details = RADIO_TYPES[radio_type] + radio = radio_details[ZHA_GW_RADIO]() + self.radio_description = radio_details[ZHA_GW_RADIO_DESCRIPTION] await radio.connect(usb_path, baudrate) if CONF_DATABASE in self._config: diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 14103a5ea38cae..d3f06090dae34a 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -8,7 +8,15 @@ import collections import logging -from zigpy.types.named import EUI64 +import bellows.ezsp +import bellows.zigbee.application +import zigpy.types +import zigpy_deconz.api +import zigpy_deconz.zigbee.application +import zigpy_xbee.api +import zigpy_xbee.zigbee.application +import zigpy_zigate.api +import zigpy_zigate.zigbee.application from homeassistant.core import callback @@ -51,25 +59,17 @@ async def safe_read( async def check_zigpy_connection(usb_path, radio_type, database_path): """Test zigpy radio connection.""" if radio_type == RadioType.ezsp.name: - import bellows.ezsp - from bellows.zigbee.application import ControllerApplication - radio = bellows.ezsp.EZSP() + ControllerApplication = bellows.zigbee.application.ControllerApplication elif radio_type == RadioType.xbee.name: - import zigpy_xbee.api - from zigpy_xbee.zigbee.application import ControllerApplication - radio = zigpy_xbee.api.XBee() + ControllerApplication = zigpy_xbee.zigbee.application.ControllerApplication elif radio_type == RadioType.deconz.name: - import zigpy_deconz.api - from zigpy_deconz.zigbee.application import ControllerApplication - radio = zigpy_deconz.api.Deconz() + ControllerApplication = zigpy_deconz.zigbee.application.ControllerApplication elif radio_type == RadioType.zigate.name: - import zigpy_zigate.api - from zigpy_zigate.zigbee.application import ControllerApplication - radio = zigpy_zigate.api.ZiGate() + ControllerApplication = zigpy_zigate.zigbee.application.ControllerApplication try: await radio.connect(usb_path, DEFAULT_BAUDRATE) controller = ControllerApplication(radio, database_path) @@ -138,7 +138,7 @@ async def async_get_zha_device(hass, device_id): registry_device = device_registry.async_get(device_id) zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = EUI64.convert(ieee_address) + ieee = zigpy.types.EUI64.convert(ieee_address) return zha_gateway.devices[ieee] diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 43ddc888d2fda0..571e77d4fae302 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -6,6 +6,18 @@ """ import collections +import bellows.ezsp +import bellows.zigbee.application +import zigpy.profiles.zha +import zigpy.profiles.zll +import zigpy.zcl as zcl +import zigpy_deconz.api +import zigpy_deconz.zigbee.application +import zigpy_xbee.api +import zigpy_xbee.zigbee.application +import zigpy_zigate.api +import zigpy_zigate.zigbee.application + from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN @@ -14,6 +26,8 @@ from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH +# importing channels updates registries +from . import channels # noqa pylint: disable=wrong-import-position,unused-import from .const import ( CONTROLLER, SENSOR_ACCELERATION, @@ -63,9 +77,6 @@ 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. @@ -73,56 +84,27 @@ def establish_device_mappings(): These cannot be module level, as importing bellows must be done in a in a function. """ - from zigpy import zcl - from zigpy.profiles import zha, zll - - def get_ezsp_radio(): - import bellows.ezsp - from bellows.zigbee.application import ControllerApplication - - return {ZHA_GW_RADIO: bellows.ezsp.EZSP(), CONTROLLER: ControllerApplication} - RADIO_TYPES[RadioType.ezsp.name] = { - ZHA_GW_RADIO: get_ezsp_radio, + ZHA_GW_RADIO: bellows.ezsp.EZSP, + CONTROLLER: bellows.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "EZSP", } - def get_deconz_radio(): - import zigpy_deconz.api - from zigpy_deconz.zigbee.application import ControllerApplication - - return { - ZHA_GW_RADIO: zigpy_deconz.api.Deconz(), - CONTROLLER: ControllerApplication, - } - RADIO_TYPES[RadioType.deconz.name] = { - ZHA_GW_RADIO: get_deconz_radio, + ZHA_GW_RADIO: zigpy_deconz.api.Deconz, + CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, 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: zigpy_xbee.api.XBee, + CONTROLLER: zigpy_xbee.zigbee.application.ControllerApplication, 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: zigpy_zigate.api.ZiGate, + CONTROLLER: zigpy_zigate.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "ZiGate", } @@ -137,33 +119,33 @@ def get_zigate_radio(): } ) - DEVICE_CLASS[zha.PROFILE_ID].update( + DEVICE_CLASS[zigpy.profiles.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, + zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, + zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, + zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, } ) - DEVICE_CLASS[zll.PROFILE_ID].update( + DEVICE_CLASS[zigpy.profiles.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, + zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, + zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, } ) @@ -207,19 +189,21 @@ def get_zigate_radio(): } ) - 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) + zha = zigpy.profiles.zha + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_DIMMER_SWITCH) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_SCENE_CONTROLLER) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.DIMMER_SWITCH) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.NON_COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append( + zha.DeviceType.NON_COLOR_SCENE_CONTROLLER + ) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.REMOTE_CONTROL) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.SCENE_SELECTOR) + + zll = zigpy.profiles.zll + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_SCENE_CONTROLLER) + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROL_BRIDGE) + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROLLER) + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.SCENE_CONTROLLER) diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 9a559aae9b6b44..5f9172749b047c 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -3,8 +3,12 @@ from unittest.mock import Mock, patch from asynctest import CoroutineMock -from zigpy.types.named import EUI64 +import zigpy.profiles.zha +import zigpy.types +import zigpy.zcl +import zigpy.zcl.clusters.general import zigpy.zcl.foundation as zcl_f +import zigpy.zdo.types from homeassistant.components.zha.core.const import ( DATA_ZHA, @@ -22,7 +26,7 @@ class FakeApplication: def __init__(self): """Init fake application.""" - self.ieee = EUI64.convert("00:15:8d:00:02:32:4f:32") + self.ieee = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") self.nwk = 0x087D @@ -34,8 +38,6 @@ class FakeEndpoint: def __init__(self, manufacturer, model): """Init fake endpoint.""" - from zigpy.profiles.zha import PROFILE_ID - self.device = None self.endpoint_id = 1 self.in_clusters = {} @@ -44,14 +46,12 @@ def __init__(self, manufacturer, model): self.status = 1 self.manufacturer = manufacturer self.model = model - self.profile_id = PROFILE_ID + self.profile_id = zigpy.profiles.zha.PROFILE_ID self.device_type = None def add_input_cluster(self, cluster_id): """Add an input cluster.""" - from zigpy.zcl import Cluster - - cluster = Cluster.from_id(self, cluster_id, is_server=True) + cluster = zigpy.zcl.Cluster.from_id(self, cluster_id, is_server=True) patch_cluster(cluster) self.in_clusters[cluster_id] = cluster if hasattr(cluster, "ep_attribute"): @@ -59,9 +59,7 @@ def add_input_cluster(self, cluster_id): def add_output_cluster(self, cluster_id): """Add an output cluster.""" - from zigpy.zcl import Cluster - - cluster = Cluster.from_id(self, cluster_id, is_server=False) + cluster = zigpy.zcl.Cluster.from_id(self, cluster_id, is_server=False) patch_cluster(cluster) self.out_clusters[cluster_id] = cluster @@ -83,7 +81,7 @@ class FakeDevice: def __init__(self, ieee, manufacturer, model): """Init fake device.""" self._application = APPLICATION - self.ieee = EUI64.convert(ieee) + self.ieee = zigpy.types.EUI64.convert(ieee) self.nwk = 0xB79C self.zdo = Mock() self.endpoints = {0: self.zdo} @@ -94,9 +92,7 @@ def __init__(self, ieee, manufacturer, model): self.initializing = False self.manufacturer = manufacturer self.model = model - from zigpy.zdo.types import NodeDescriptor - - self.node_desc = NodeDescriptor() + self.node_desc = zigpy.zdo.types.NodeDescriptor() def make_device( @@ -150,11 +146,9 @@ async def async_init_zigpy_device( def make_attribute(attrid, value, status=0): """Make an attribute.""" - from zigpy.zcl.foundation import Attribute, TypeValue - - attr = Attribute() + attr = zcl_f.Attribute() attr.attrid = attrid - attr.value = TypeValue() + attr.value = zcl_f.TypeValue() attr.value.value = value return attr @@ -202,21 +196,18 @@ async def async_test_device_join( simulate pairing a new device to the network so that code pathways that only trigger during device joins can be tested. """ - from zigpy.zcl.foundation import Status - from zigpy.zcl.clusters.general import Basic - # create zigpy device mocking out the zigbee network operations with patch( "zigpy.zcl.Cluster.configure_reporting", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): with patch( "zigpy.zcl.Cluster.bind", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): zigpy_device = await async_init_zigpy_device( hass, - [cluster_id, Basic.cluster_id], + [cluster_id, zigpy.zcl.clusters.general.Basic.cluster_id], [], device_type, zha_gateway, diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index b836c55df17a32..e34ad208744c1b 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,13 +1,16 @@ """Test configuration for the ZHA component.""" from unittest.mock import patch + import pytest + from homeassistant import config_entries -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.const import COMPONENTS, DATA_ZHA, DOMAIN from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.registries import establish_device_mappings -from .common import async_setup_entry from homeassistant.components.zha.core.store import async_get_registry +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg + +from .common import async_setup_entry @pytest.fixture(name="config_entry") diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index ae8e460b61329e..3fea9dfe0880f3 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,33 +1,39 @@ """Test ZHA API.""" import pytest +import zigpy.zcl.clusters.general as general + from homeassistant.components.switch import DOMAIN -from homeassistant.components.zha.api import async_load_api, TYPE, ID +from homeassistant.components.websocket_api import const +from homeassistant.components.zha.api import ID, TYPE, async_load_api from homeassistant.components.zha.core.const import ( ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, - CLUSTER_TYPE_IN, + ATTR_ENDPOINT_ID, ATTR_IEEE, + ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, ATTR_QUIRK_APPLIED, - ATTR_MANUFACTURER, - ATTR_ENDPOINT_ID, + CLUSTER_TYPE_IN, ) -from homeassistant.components.websocket_api import const + from .common import async_init_zigpy_device @pytest.fixture async def zha_client(hass, config_entry, zha_gateway, hass_ws_client): """Test zha switch platform.""" - from zigpy.zcl.clusters.general import OnOff, Basic # load the ZHA API async_load_api(hass) # create zigpy device await async_init_zigpy_device( - hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [general.OnOff.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) # load up switch domain diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 4fca5505d2916d..89dc1ae25a64c6 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,5 +1,8 @@ """Test zha binary sensor.""" -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.measurement as measurement +import zigpy.zcl.clusters.security as security +import zigpy.zcl.foundation as zcl_f from homeassistant.components.binary_sensor import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -16,18 +19,19 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): """Test zha binary_sensor platform.""" - from zigpy.zcl.clusters.security import IasZone - from zigpy.zcl.clusters.measurement import OccupancySensing - from zigpy.zcl.clusters.general import Basic # create zigpy devices zigpy_device_zone = await async_init_zigpy_device( - hass, [IasZone.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [security.IasZone.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) zigpy_device_occupancy = await async_init_zigpy_device( hass, - [OccupancySensing.cluster_id, Basic.cluster_id], + [measurement.OccupancySensing.cluster_id, general.Basic.cluster_id], [], None, zha_gateway, @@ -71,14 +75,16 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): await async_test_iaszone_on_off(hass, zone_cluster, zone_entity_id) # test new sensor join - await async_test_device_join(hass, zha_gateway, OccupancySensing.cluster_id, DOMAIN) + await async_test_device_join( + hass, zha_gateway, measurement.OccupancySensing.cluster_id, DOMAIN + ) async def async_test_binary_sensor_on_off(hass, cluster, entity_id): """Test getting on and off messages for binary sensors.""" # binary sensor on attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 25b0910931ab05..5e6bf51afd61df 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,7 +1,9 @@ """Tests for ZHA config flow.""" from asynctest import patch + from homeassistant.components.zha import config_flow from homeassistant.components.zha.core.const import DOMAIN + from tests.common import MockConfigEntry diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 91049a9bfa8833..62884fe72ae94a 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -2,6 +2,9 @@ from unittest.mock import patch import pytest +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.security as security +import zigpy.zcl.foundation as zcl_f import homeassistant.components.automation as automation from homeassistant.components.device_automation import ( @@ -29,13 +32,15 @@ def calls(hass): async def test_get_actions(hass, config_entry, zha_gateway): """Test we get the expected actions from a zha device.""" - from zigpy.zcl.clusters.general import Basic - from zigpy.zcl.clusters.security import IasZone, IasWd # create zigpy device zigpy_device = await async_init_zigpy_device( hass, - [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], + [ + general.Basic.cluster_id, + security.IasZone.cluster_id, + security.IasWd.cluster_id, + ], [], None, zha_gateway, @@ -64,15 +69,15 @@ async def test_get_actions(hass, config_entry, zha_gateway): async def test_action(hass, config_entry, zha_gateway, calls): """Test for executing a zha device action.""" - from zigpy.zcl.clusters.general import Basic, OnOff - from zigpy.zcl.clusters.security import IasZone, IasWd - from zigpy.zcl.foundation import Status - # create zigpy device zigpy_device = await async_init_zigpy_device( hass, - [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], - [OnOff.cluster_id], + [ + general.Basic.cluster_id, + security.IasZone.cluster_id, + security.IasWd.cluster_id, + ], + [general.OnOff.cluster_id], None, zha_gateway, ) @@ -96,7 +101,8 @@ async def test_action(hass, config_entry, zha_gateway, calls): await async_enable_traffic(hass, zha_gateway, [zha_device]) with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), ): assert await async_setup_component( hass, diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 0c27c1514f14b2..446920eb2f9c2b 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -2,7 +2,8 @@ from datetime import timedelta import time -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER from homeassistant.components.zha.core.registries import ( @@ -25,26 +26,18 @@ async def test_device_tracker(hass, config_entry, zha_gateway): """Test zha device tracker platform.""" - from zigpy.zcl.clusters.general import ( - Basic, - PowerConfiguration, - BinaryInput, - Identify, - Ota, - PollControl, - ) # create zigpy device zigpy_device = await async_init_zigpy_device( hass, [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - PollControl.cluster_id, - BinaryInput.cluster_id, + general.Basic.cluster_id, + general.PowerConfiguration.cluster_id, + general.Identify.cluster_id, + general.PollControl.cluster_id, + general.BinaryInput.cluster_id, ], - [Identify.cluster_id, Ota.cluster_id], + [general.Identify.cluster_id, general.Ota.cluster_id], SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, zha_gateway, ) @@ -73,7 +66,7 @@ async def test_device_tracker(hass, config_entry, zha_gateway): # turn state flip attr = make_attribute(0x0020, 23) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) attr = make_attribute(0x0021, 200) @@ -96,7 +89,7 @@ async def test_device_tracker(hass, config_entry, zha_gateway): await async_test_device_join( hass, zha_gateway, - PowerConfiguration.cluster_id, + general.PowerConfiguration.cluster_id, DOMAIN, SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, ) diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 8df1a072801ff8..75e8538c5bf0d4 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,5 +1,6 @@ """ZHA device automation trigger tests.""" import pytest +import zigpy.zcl.clusters.general as general import homeassistant.components.automation as automation from homeassistant.components.switch import DOMAIN @@ -9,7 +10,7 @@ from .common import async_enable_traffic, async_init_zigpy_device -from tests.common import async_mock_service, async_get_device_automations +from tests.common import async_get_device_automations, async_mock_service ON = 1 OFF = 0 @@ -43,11 +44,10 @@ def calls(hass): async def test_triggers(hass, config_entry, zha_gateway): """Test zha device triggers.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) zigpy_device.device_automation_triggers = { @@ -112,11 +112,10 @@ async def test_triggers(hass, config_entry, zha_gateway): async def test_no_triggers(hass, config_entry, zha_gateway): """Test zha device with no triggers.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) @@ -135,11 +134,10 @@ async def test_no_triggers(hass, config_entry, zha_gateway): async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): """Test for remote triggers firing.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) zigpy_device.device_automation_triggers = { @@ -197,11 +195,10 @@ async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, caplog): """Test for exception on event triggers firing.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) @@ -244,11 +241,10 @@ async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, cap async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls, caplog): """Test for exception on event triggers firing.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) zigpy_device.device_automation_triggers = { diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 1704ab2196b659..a196ba50ba7c06 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,7 +1,9 @@ """Test zha fan.""" from unittest.mock import call, patch -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.hvac as hvac +import zigpy.zcl.foundation as zcl_f from homeassistant.components import fan from homeassistant.components.fan import ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED @@ -28,13 +30,10 @@ async def test_fan(hass, config_entry, zha_gateway): """Test zha fan platform.""" - from zigpy.zcl.clusters.hvac import Fan - from zigpy.zcl.clusters.general import Basic - from zigpy.zcl.foundation import Status # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Fan.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, [hvac.Fan.cluster_id, general.Basic.cluster_id], [], None, zha_gateway ) # load up fan domain @@ -56,7 +55,7 @@ async def test_fan(hass, config_entry, zha_gateway): # turn on at fan attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -70,7 +69,7 @@ async def test_fan(hass, config_entry, zha_gateway): # turn on from HA with patch( "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): # turn on via UI await async_turn_on(hass, entity_id) @@ -80,7 +79,7 @@ async def test_fan(hass, config_entry, zha_gateway): # turn off from HA with patch( "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): # turn off via UI await async_turn_off(hass, entity_id) @@ -90,7 +89,7 @@ async def test_fan(hass, config_entry, zha_gateway): # change speed from HA with patch( "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): # turn on via UI await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH) @@ -98,7 +97,7 @@ async def test_fan(hass, config_entry, zha_gateway): assert cluster.write_attributes.call_args == call({"fan_mode": 3}) # test adding new fan to the network and HA - await async_test_device_join(hass, zha_gateway, Fan.cluster_id, DOMAIN) + await async_test_device_join(hass, zha_gateway, hvac.Fan.cluster_id, DOMAIN) async def async_turn_on(hass, entity_id, speed=None): diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 567f61ad1e1b18..f0d9d4913e69e8 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -2,7 +2,10 @@ import asyncio from unittest.mock import MagicMock, call, patch, sentinel -from zigpy.zcl.foundation import Command +import zigpy.profiles.zha +import zigpy.types +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f from homeassistant.components.light import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -24,24 +27,25 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): """Test zha light platform.""" - from zigpy.zcl.clusters.general import OnOff, LevelControl, Basic - from zigpy.zcl.foundation import Status - from zigpy.profiles.zha import DeviceType # create zigpy devices zigpy_device_on_off = await async_init_zigpy_device( hass, - [OnOff.cluster_id, Basic.cluster_id], + [general.OnOff.cluster_id, general.Basic.cluster_id], [], - DeviceType.ON_OFF_LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, zha_gateway, ) zigpy_device_level = await async_init_zigpy_device( hass, - [OnOff.cluster_id, LevelControl.cluster_id, Basic.cluster_id], + [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + general.Basic.cluster_id, + ], [], - DeviceType.ON_OFF_LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, zha_gateway, ieee="00:0d:6f:11:0a:90:69:e7", manufacturer="FakeLevelManufacturer", @@ -64,12 +68,12 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): level_device_level_cluster = zigpy_device_level.endpoints.get(1).level on_off_mock = MagicMock( side_effect=asyncio.coroutine( - MagicMock(return_value=[sentinel.data, Status.SUCCESS]) + MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]) ) ) level_mock = MagicMock( side_effect=asyncio.coroutine( - MagicMock(return_value=[sentinel.data, Status.SUCCESS]) + MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]) ) ) monkeypatch.setattr(level_device_on_off_cluster, "request", on_off_mock) @@ -118,7 +122,11 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): # test adding a new light to the network and HA await async_test_device_join( - hass, zha_gateway, OnOff.cluster_id, DOMAIN, device_type=DeviceType.ON_OFF_LIGHT + hass, + zha_gateway, + general.OnOff.cluster_id, + DOMAIN, + device_type=zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, ) @@ -126,7 +134,7 @@ async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -142,7 +150,7 @@ async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -150,10 +158,9 @@ async def async_test_on_from_light(hass, cluster, entity_id): async def async_test_on_off_from_hass(hass, cluster, entity_id): """Test on off functionality from hass.""" - from zigpy.zcl.foundation import Status - with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), ): # turn on via UI await hass.services.async_call( @@ -169,10 +176,9 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): async def async_test_off_from_hass(hass, cluster, entity_id): """Test turning off the light from homeassistant.""" - from zigpy.zcl.foundation import Status - with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x01, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), ): # turn off via UI await hass.services.async_call( @@ -188,7 +194,6 @@ async def async_test_level_on_off_from_hass( hass, on_off_cluster, level_cluster, entity_id ): """Test on off functionality from hass.""" - from zigpy import types # turn on via UI await hass.services.async_call( @@ -213,7 +218,7 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_args == call( False, 4, - (types.uint8_t, types.uint16_t), + (zigpy.types.uint8_t, zigpy.types.uint16_t), 254, 100.0, expect_reply=True, @@ -233,7 +238,7 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_args == call( False, 4, - (types.uint8_t, types.uint16_t), + (zigpy.types.uint8_t, zigpy.types.uint16_t), 10, 0, expect_reply=True, @@ -248,7 +253,7 @@ async def async_test_level_on_off_from_hass( async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): """Test dimmer functionality from the light.""" attr = make_attribute(0, level) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == expected_state diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index c7cc5bdd2a95f9..118526a1d85917 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -1,7 +1,9 @@ """Test zha lock.""" from unittest.mock import patch -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.closures as closures +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f from homeassistant.components.lock import DOMAIN from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED @@ -22,12 +24,14 @@ async def test_lock(hass, config_entry, zha_gateway): """Test zha lock platform.""" - from zigpy.zcl.clusters.closures import DoorLock - from zigpy.zcl.clusters.general import Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [DoorLock.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [closures.DoorLock.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) # load up lock domain @@ -49,7 +53,7 @@ async def test_lock(hass, config_entry, zha_gateway): # set state to locked attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_LOCKED @@ -69,9 +73,9 @@ async def test_lock(hass, config_entry, zha_gateway): async def async_lock(hass, cluster, entity_id): """Test lock functionality from hass.""" - from zigpy.zcl.foundation import Status - - with patch("zigpy.zcl.Cluster.request", return_value=mock_coro([Status.SUCCESS])): + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS]) + ): # lock via UI await hass.services.async_call( DOMAIN, "lock", {"entity_id": entity_id}, blocking=True @@ -83,9 +87,9 @@ async def async_lock(hass, cluster, entity_id): async def async_unlock(hass, cluster, entity_id): """Test lock functionality from hass.""" - from zigpy.zcl.foundation import Status - - with patch("zigpy.zcl.Cluster.request", return_value=mock_coro([Status.SUCCESS])): + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS]) + ): # lock via UI await hass.services.async_call( DOMAIN, "unlock", {"entity_id": entity_id}, blocking=True diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 37d412e6a258af..dec551f8d627e5 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,5 +1,9 @@ """Test zha sensor.""" -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.homeautomation as homeautomation +import zigpy.zcl.clusters.measurement as measurement +import zigpy.zcl.clusters.smartenergy as smartenergy +import zigpy.zcl.foundation as zcl_f from homeassistant.components.sensor import DOMAIN from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN @@ -16,23 +20,15 @@ async def test_sensor(hass, config_entry, zha_gateway): """Test zha sensor platform.""" - from zigpy.zcl.clusters.measurement import ( - RelativeHumidity, - TemperatureMeasurement, - PressureMeasurement, - IlluminanceMeasurement, - ) - from zigpy.zcl.clusters.smartenergy import Metering - from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement # list of cluster ids to create devices and sensor entities for cluster_ids = [ - RelativeHumidity.cluster_id, - TemperatureMeasurement.cluster_id, - PressureMeasurement.cluster_id, - IlluminanceMeasurement.cluster_id, - Metering.cluster_id, - ElectricalMeasurement.cluster_id, + measurement.RelativeHumidity.cluster_id, + measurement.TemperatureMeasurement.cluster_id, + measurement.PressureMeasurement.cluster_id, + measurement.IlluminanceMeasurement.cluster_id, + smartenergy.Metering.cluster_id, + homeautomation.ElectricalMeasurement.cluster_id, ] # devices that were created from cluster_ids list above @@ -63,33 +59,33 @@ async def test_sensor(hass, config_entry, zha_gateway): assert hass.states.get(entity_id).state == STATE_UNKNOWN # get the humidity device info and test the associated sensor logic - device_info = zigpy_device_infos[RelativeHumidity.cluster_id] + device_info = zigpy_device_infos[measurement.RelativeHumidity.cluster_id] await async_test_humidity(hass, device_info) # get the temperature device info and test the associated sensor logic - device_info = zigpy_device_infos[TemperatureMeasurement.cluster_id] + device_info = zigpy_device_infos[measurement.TemperatureMeasurement.cluster_id] await async_test_temperature(hass, device_info) # get the pressure device info and test the associated sensor logic - device_info = zigpy_device_infos[PressureMeasurement.cluster_id] + device_info = zigpy_device_infos[measurement.PressureMeasurement.cluster_id] await async_test_pressure(hass, device_info) # get the illuminance device info and test the associated sensor logic - device_info = zigpy_device_infos[IlluminanceMeasurement.cluster_id] + device_info = zigpy_device_infos[measurement.IlluminanceMeasurement.cluster_id] await async_test_illuminance(hass, device_info) # get the metering device info and test the associated sensor logic - device_info = zigpy_device_infos[Metering.cluster_id] + device_info = zigpy_device_infos[smartenergy.Metering.cluster_id] await async_test_metering(hass, device_info) # get the electrical_measurement device info and test the associated # sensor logic - device_info = zigpy_device_infos[ElectricalMeasurement.cluster_id] + device_info = zigpy_device_infos[homeautomation.ElectricalMeasurement.cluster_id] await async_test_electrical_measurement(hass, device_info) # test joining a new temperature sensor to the network await async_test_device_join( - hass, zha_gateway, TemperatureMeasurement.cluster_id, DOMAIN + hass, zha_gateway, measurement.TemperatureMeasurement.cluster_id, DOMAIN ) @@ -102,7 +98,6 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): A dict containing relevant device info for testing is returned. It contains the entity id, zigpy device, and the zigbee cluster for the sensor. """ - from zigpy.zcl.clusters.general import Basic device_infos = {} counter = 0 @@ -111,7 +106,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): device_infos[cluster_id] = {"zigpy_device": None} device_infos[cluster_id]["zigpy_device"] = await async_init_zigpy_device( hass, - [cluster_id, Basic.cluster_id], + [cluster_id, general.Basic.cluster_id], [], None, zha_gateway, @@ -181,7 +176,7 @@ async def send_attribute_report(hass, cluster, attrid, value): device is paired to the zigbee network. """ attr = make_attribute(attrid, value) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 9c2b2da9c95414..bf4ff3ed628c1e 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -1,7 +1,8 @@ """Test zha switch.""" from unittest.mock import call, patch -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f from homeassistant.components.switch import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -23,12 +24,14 @@ async def test_switch(hass, config_entry, zha_gateway): """Test zha switch platform.""" - from zigpy.zcl.clusters.general import OnOff, Basic - from zigpy.zcl.foundation import Status # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [general.OnOff.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) # load up switch domain @@ -50,7 +53,7 @@ async def test_switch(hass, config_entry, zha_gateway): # turn on at switch attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -63,7 +66,8 @@ async def test_switch(hass, config_entry, zha_gateway): # turn on from HA with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), ): # turn on via UI await hass.services.async_call( @@ -76,7 +80,8 @@ async def test_switch(hass, config_entry, zha_gateway): # turn off from HA with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x01, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), ): # turn off via UI await hass.services.async_call( @@ -88,4 +93,4 @@ async def test_switch(hass, config_entry, zha_gateway): ) # test joining a new switch to the network and HA - await async_test_device_join(hass, zha_gateway, OnOff.cluster_id, DOMAIN) + await async_test_device_join(hass, zha_gateway, general.OnOff.cluster_id, DOMAIN) From 2cc039dbc4c0db88f863808204d25459c7a66246 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 22 Oct 2019 00:32:10 +0000 Subject: [PATCH 1162/3953] [ci skip] Translation update --- .../components/adguard/.translations/lb.json | 2 + .../components/adguard/.translations/nl.json | 2 + .../components/adguard/.translations/sl.json | 2 + .../components/airly/.translations/nl.json | 22 +++++++++++ .../alarm_control_panel/.translations/nl.json | 7 ++++ .../components/axis/.translations/nl.json | 1 + .../binary_sensor/.translations/nl.json | 36 ++++++++++++++++++ .../binary_sensor/.translations/ru.json | 4 +- .../cert_expiry/.translations/nl.json | 2 +- .../components/ecobee/.translations/nl.json | 14 ++++++- .../components/glances/.translations/ca.json | 37 +++++++++++++++++++ .../components/glances/.translations/da.json | 37 +++++++++++++++++++ .../components/glances/.translations/en.json | 32 ++++++++-------- .../components/glances/.translations/fi.json | 13 +++++++ .../components/glances/.translations/fr.json | 32 ++++++++++++++++ .../components/glances/.translations/ko.json | 37 +++++++++++++++++++ .../components/glances/.translations/lb.json | 37 +++++++++++++++++++ .../components/glances/.translations/nl.json | 37 +++++++++++++++++++ .../components/glances/.translations/no.json | 31 ++++++++++++++++ .../components/glances/.translations/ru.json | 37 +++++++++++++++++++ .../components/glances/.translations/sl.json | 37 +++++++++++++++++++ .../components/glances/.translations/th.json | 11 ++++++ .../components/izone/.translations/nl.json | 15 ++++++++ .../components/lock/.translations/nl.json | 5 +++ .../components/neato/.translations/nl.json | 7 ++++ .../opentherm_gw/.translations/nl.json | 19 ++++++++++ .../components/plex/.translations/nl.json | 33 +++++++++++++++-- .../components/plex/.translations/sl.json | 2 +- .../components/sensor/.translations/ca.json | 2 + .../components/sensor/.translations/nl.json | 13 ++++++- .../components/soma/.translations/nl.json | 20 +++++++++- .../transmission/.translations/nl.json | 24 ++++++++++++ .../components/zha/.translations/ko.json | 4 +- .../components/zha/.translations/nl.json | 13 +++++++ 34 files changed, 596 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/airly/.translations/nl.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/nl.json create mode 100644 homeassistant/components/glances/.translations/ca.json create mode 100644 homeassistant/components/glances/.translations/da.json create mode 100644 homeassistant/components/glances/.translations/fi.json create mode 100644 homeassistant/components/glances/.translations/fr.json create mode 100644 homeassistant/components/glances/.translations/ko.json create mode 100644 homeassistant/components/glances/.translations/lb.json create mode 100644 homeassistant/components/glances/.translations/nl.json create mode 100644 homeassistant/components/glances/.translations/no.json create mode 100644 homeassistant/components/glances/.translations/ru.json create mode 100644 homeassistant/components/glances/.translations/sl.json create mode 100644 homeassistant/components/glances/.translations/th.json create mode 100644 homeassistant/components/izone/.translations/nl.json diff --git a/homeassistant/components/adguard/.translations/lb.json b/homeassistant/components/adguard/.translations/lb.json index cc3ecf5db87355..e449f668fd9359 100644 --- a/homeassistant/components/adguard/.translations/lb.json +++ b/homeassistant/components/adguard/.translations/lb.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "D\u00ebs Integratioun ben\u00e9idegt AdgGuard Home {minimal_version} oder m\u00e9i, dir hutt {current_version}. Aktualis\u00e9iert w.e.g. \u00e4ren Hass.io AdGuard Home Add-on.", + "adguard_home_outdated": "D\u00ebs Integratioun ben\u00e9idegt AdgGuard Home {minimal_version} oder m\u00e9i, dir hutt {current_version}.", "existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert.", "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun AdGuard Home ass erlaabt." }, diff --git a/homeassistant/components/adguard/.translations/nl.json b/homeassistant/components/adguard/.translations/nl.json index 3ef86c30a3f145..bd0dcc5fa43247 100644 --- a/homeassistant/components/adguard/.translations/nl.json +++ b/homeassistant/components/adguard/.translations/nl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Deze integratie vereist AdGuard Home {minimal_version} of hoger, u heeft {current_version}. Update uw Hass.io AdGuard Home-add-on.", + "adguard_home_outdated": "Deze integratie vereist AdGuard Home {minimal_version} of hoger, u heeft {current_version}.", "existing_instance_updated": "Bestaande configuratie bijgewerkt.", "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan." }, diff --git a/homeassistant/components/adguard/.translations/sl.json b/homeassistant/components/adguard/.translations/sl.json index f1ca796363d270..974524c932da77 100644 --- a/homeassistant/components/adguard/.translations/sl.json +++ b/homeassistant/components/adguard/.translations/sl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Za to integracijo je potrebna AdGuard Home {minimal_version} ali vi\u0161ja, vi imate {current_version}. Prosimo posodobite va\u0161 hass.io AdGuard Home dodatek.", + "adguard_home_outdated": "Za to integracijo je potrebna AdGuard Home {minimal_version} ali vi\u0161ja, vi imate {current_version}.", "existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija.", "single_instance_allowed": "Dovoljena je samo ena konfiguracija AdGuard Home." }, diff --git a/homeassistant/components/airly/.translations/nl.json b/homeassistant/components/airly/.translations/nl.json new file mode 100644 index 00000000000000..232d5d54d8537e --- /dev/null +++ b/homeassistant/components/airly/.translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-sleutel is niet correct.", + "name_exists": "Naam bestaat al.", + "wrong_location": "Geen Airly meetstations in dit gebied." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam van de integratie" + }, + "description": "Airly-integratie van luchtkwaliteit instellen. Ga naar https://developer.airly.eu/register om de API-sleutel te genereren", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/nl.json b/homeassistant/components/alarm_control_panel/.translations/nl.json new file mode 100644 index 00000000000000..9f4e6a4a51c282 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/nl.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "disarm": "Uitschakelen {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/nl.json b/homeassistant/components/axis/.translations/nl.json index 83395283404040..10fc8c02d66ad6 100644 --- a/homeassistant/components/axis/.translations/nl.json +++ b/homeassistant/components/axis/.translations/nl.json @@ -12,6 +12,7 @@ "device_unavailable": "Apparaat is niet beschikbaar", "faulty_credentials": "Ongeldige gebruikersreferenties" }, + "flow_title": "Axis apparaat: {naam} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/.translations/nl.json b/homeassistant/components/binary_sensor/.translations/nl.json index 92cadf79aa8373..508a06b38a2a8e 100644 --- a/homeassistant/components/binary_sensor/.translations/nl.json +++ b/homeassistant/components/binary_sensor/.translations/nl.json @@ -6,6 +6,11 @@ "is_connected": "{entity_name} is verbonden", "is_gas": "{entity_name} detecteert gas", "is_hot": "{entity_name} is hot", + "is_light": "{entity_name} detecteert licht", + "is_locked": "{entity_name} is vergrendeld", + "is_moist": "{entity_name} is vochtig", + "is_motion": "{entity_name} detecteert beweging", + "is_moving": "{entity_name} is in beweging", "is_no_gas": "{entity_name} detecteert geen gas", "is_no_light": "{entity_name} detecteert geen licht", "is_no_motion": "{entity_name} detecteert geen beweging", @@ -17,10 +22,21 @@ "is_not_cold": "{entity_name} is niet koud", "is_not_connected": "{entity_name} is niet verbonden", "is_not_hot": "{entity_name} is niet heet", + "is_not_locked": "{entity_name} is ontgrendeld", + "is_not_moist": "{entity_name} is droog", + "is_not_moving": "{entity_name} beweegt niet", "is_not_occupied": "{entity_name} is niet bezet", + "is_not_open": "{entity_name} is gesloten", + "is_not_plugged_in": "{entity_name} is niet aangesloten", + "is_not_powered": "{entity_name} is niet van stroom voorzien...", "is_not_present": "{entity_name} is niet aanwezig", "is_not_unsafe": "{entity_name} is veilig", "is_occupied": "{entity_name} bezet is", + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld", + "is_open": "{entity_name} is open", + "is_plugged_in": "{entity_name} is aangesloten", + "is_powered": "{entity_name} is van stroom voorzien....", "is_present": "{entity_name} is aanwezig", "is_problem": "{entity_name} detecteert een probleem", "is_smoke": "{entity_name} detecteert rook", @@ -31,27 +47,47 @@ "trigger_type": { "bat_low": "{entity_name} batterij bijna leeg", "closed": "{entity_name} gesloten", + "cold": "{entity_name} werd koud", "connected": "{entity_name} verbonden", + "gas": "{entity_name} begon gas te detecteren", + "hot": "{entity_name} werd heet", "light": "{entity_name} begon licht te detecteren", "locked": "{entity_name} vergrendeld", "moist": "{entity_name} werd vochtig", + "moist\u00a7": "{entity_name} werd vochtig", "motion": "{entity_name} begon beweging te detecteren", + "moving": "{entity_name} begon te bewegen", + "no_gas": "{entity_name} is gestopt met het detecteren van gas", + "no_light": "{entity_name} gestopt met het detecteren van licht", + "no_motion": "{entity_name} gestopt met het detecteren van beweging", + "no_problem": "{entity_name} gestopt met het detecteren van het probleem", + "no_smoke": "{entity_name} gestopt met het detecteren van rook", + "no_sound": "{entity_name} gestopt met het detecteren van geluid", + "no_vibration": "{entity_name} gestopt met het detecteren van trillingen", "not_bat_low": "{entity_name} batterij normaal", + "not_cold": "{entity_name} werd niet koud", "not_connected": "{entity_name} verbroken", + "not_hot": "{entity_name} werd niet warm", "not_locked": "{entity_name} ontgrendeld", + "not_moist": "{entity_name} werd droog", + "not_moving": "{entity_name} gestopt met bewegen", "not_occupied": "{entity_name} werd niet bezet", "not_opened": "{entity_name} gesloten", "not_plugged_in": "{entity_name} niet verbonden", "not_powered": "{entity_name} niet ingeschakeld", + "not_present": "{entity_name} is niet aanwezig", + "not_unsafe": "{entity_name} werd veilig", "occupied": "{entity_name} werd bezet", "opened": "{entity_name} geopend", "plugged_in": "{entity_name} aangesloten", "powered": "{entity_name} heeft vermogen", + "present": "{entity_name} aanwezig", "problem": "{entity_name} begonnen met het detecteren van een probleem", "smoke": "{entity_name} begon rook te detecteren", "sound": "{entity_name} begon geluid te detecteren", "turned_off": "{entity_name} uitgeschakeld", "turned_on": "{entity_name} ingeschakeld", + "unsafe": "{entity_name} werd onveilig", "vibration": "{entity_name} begon trillingen te detecteren" } } diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json index a7ecced3f11a05..cce765c8d8444f 100644 --- a/homeassistant/components/binary_sensor/.translations/ru.json +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -74,13 +74,13 @@ "not_occupied": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "not_plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u044d\u043d\u0435\u0440\u0433\u0438\u0438", + "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", "not_present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "not_unsafe": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", "occupied": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "powered": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u044d\u043d\u0435\u0440\u0433\u0438\u0438", + "powered": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", "present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "problem": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", "smoke": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", diff --git a/homeassistant/components/cert_expiry/.translations/nl.json b/homeassistant/components/cert_expiry/.translations/nl.json index d2fe3c76e85001..40316f008d5403 100644 --- a/homeassistant/components/cert_expiry/.translations/nl.json +++ b/homeassistant/components/cert_expiry/.translations/nl.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Kan certificaat niet ophalen van deze combinatie van host en poort", - "connection_timeout": "Timeout bij verbinding maken met deze host", + "connection_timeout": "Time-out bij verbinding maken met deze host", "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd", "resolve_failed": "Deze host kon niet gevonden worden" }, diff --git a/homeassistant/components/ecobee/.translations/nl.json b/homeassistant/components/ecobee/.translations/nl.json index b2e3ce9cdd7ccd..56bb3ace26f578 100644 --- a/homeassistant/components/ecobee/.translations/nl.json +++ b/homeassistant/components/ecobee/.translations/nl.json @@ -4,12 +4,22 @@ "one_instance_only": "Deze integratie ondersteunt momenteel slechts \u00e9\u00e9n ecobee-instantie." }, "error": { + "pin_request_failed": "Fout bij het aanvragen van pincode bij ecobee; Controleer of de API-sleutel correct is.", "token_request_failed": "Fout bij het aanvragen van tokens bij ecobee; probeer het opnieuw." }, "step": { "authorize": { - "description": "Autoriseer deze app op https://www.ecobee.com/consumerportal/index.html met pincode: \n\n {pin} \n \nDruk vervolgens op Submit." + "description": "Autoriseer deze app op https://www.ecobee.com/consumerportal/index.html met pincode: \n\n {pin} \n \nDruk vervolgens op Submit.", + "title": "Autoriseer app op ecobee.com" + }, + "user": { + "data": { + "api_key": "API-sleutel" + }, + "description": "Voer de API-sleutel in die u van ecobee.com hebt gekregen.", + "title": "ecobee API-sleutel" } - } + }, + "title": "ecobee" } } \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/ca.json b/homeassistant/components/glances/.translations/ca.json new file mode 100644 index 00000000000000..edff236623e00f --- /dev/null +++ b/homeassistant/components/glances/.translations/ca.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar amb l'amfitri\u00f3", + "wrong_version": "Versi\u00f3 no compatible (2 o 3 necess\u00e0ria)" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "ssl": "Utilitza SSL/TLS per connectar-te al sistema Glances", + "username": "Nom d'usuari", + "verify_ssl": "Verifica la certificaci\u00f3 del sistema", + "version": "Versi\u00f3 de l'API de Glances (2 o 3)" + }, + "title": "Configuraci\u00f3 de Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + }, + "description": "Opcions de configuraci\u00f3 per a Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/da.json b/homeassistant/components/glances/.translations/da.json new file mode 100644 index 00000000000000..7779c6e40a0492 --- /dev/null +++ b/homeassistant/components/glances/.translations/da.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e6rten er allerede konfigureret." + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", + "wrong_version": "Version underst\u00f8ttes ikke (kun 2 eller 3)" + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt", + "name": "Navn", + "password": "Adgangskode", + "port": "Port", + "ssl": "Brug SSL/TLS til at oprette forbindelse til Glances-systemet", + "username": "Brugernavn", + "verify_ssl": "Bekr\u00e6ft certificering af systemet", + "version": "Glances API version (2 eller 3)" + }, + "title": "Ops\u00e6tning af Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Opdateringsfrekvens" + }, + "description": "Konfigurationsindstillinger for Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/en.json b/homeassistant/components/glances/.translations/en.json index 1bd7275daeff15..ef1a8fb5e31716 100644 --- a/homeassistant/components/glances/.translations/en.json +++ b/homeassistant/components/glances/.translations/en.json @@ -1,36 +1,36 @@ { "config": { - "title": "Glances", + "abort": { + "already_configured": "Host is already configured." + }, + "error": { + "cannot_connect": "Unable to connect to host", + "wrong_version": "Version not supported (2 or 3 only)" + }, "step": { "user": { - "title": "Setup Glances", "data": { - "name": "Name", "host": "Host", - "username": "Username", + "name": "Name", "password": "Password", "port": "Port", - "version": "Glances API Version (2 or 3)", "ssl": "Use SSL/TLS to connect to the Glances system", - "verify_ssl": "Verify the certification of the system" - } + "username": "Username", + "verify_ssl": "Verify the certification of the system", + "version": "Glances API Version (2 or 3)" + }, + "title": "Setup Glances" } }, - "error": { - "cannot_connect": "Unable to connect to host", - "wrong_version": "Version not supported (2 or 3 only)" - }, - "abort": { - "already_configured": "Host is already configured." - } + "title": "Glances" }, "options": { "step": { "init": { - "description": "Configure options for Glances", "data": { "scan_interval": "Update frequency" - } + }, + "description": "Configure options for Glances" } } } diff --git a/homeassistant/components/glances/.translations/fi.json b/homeassistant/components/glances/.translations/fi.json new file mode 100644 index 00000000000000..43ccf405d145fe --- /dev/null +++ b/homeassistant/components/glances/.translations/fi.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nimi", + "password": "Salasana", + "port": "portti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/fr.json b/homeassistant/components/glances/.translations/fr.json new file mode 100644 index 00000000000000..d7b3dc8a4487f6 --- /dev/null +++ b/homeassistant/components/glances/.translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te", + "wrong_version": "Version non prise en charge (2 ou 3 uniquement)" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur", + "verify_ssl": "V\u00e9rifier la certification du syst\u00e8me" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/ko.json b/homeassistant/components/glances/.translations/ko.json new file mode 100644 index 00000000000000..ad19b589d5de97 --- /dev/null +++ b/homeassistant/components/glances/.translations/ko.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "wrong_version": "\ud574\ub2f9 \ubc84\uc804\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4 (2 \ub610\ub294 3\ub9cc \uc9c0\uc6d0)" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec Glances \uc2dc\uc2a4\ud15c\uc5d0 \uc5f0\uacb0", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "verify_ssl": "\uc2dc\uc2a4\ud15c \uc778\uc99d \ud655\uc778", + "version": "Glances API \ubc84\uc804 (2 \ub610\ub294 3)" + }, + "title": "Glances \uc124\uce58" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" + }, + "description": "Glances \uc635\uc158 \uad6c\uc131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/lb.json b/homeassistant/components/glances/.translations/lb.json new file mode 100644 index 00000000000000..06723a4bd12806 --- /dev/null +++ b/homeassistant/components/glances/.translations/lb.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Kann sech net mam Server verbannen.", + "wrong_version": "Versioun net \u00ebnnerst\u00ebtzt (n\u00ebmmen 2 oder 3)" + }, + "step": { + "user": { + "data": { + "host": "Apparat", + "name": "Numm", + "password": "Passwuert", + "port": "Port", + "ssl": "Benotzt SSL/TLS fir sech mam Usiichte System ze verbannen", + "username": "Benotzernumm", + "verify_ssl": "Zertifikatioun vum System iwwerpr\u00e9iwen", + "version": "API Versioun vun den Usiichten (2 oder 3)" + }, + "title": "Usiichten konfigur\u00e9ieren" + } + }, + "title": "Usiichten" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle vun de Mise \u00e0 jour" + }, + "description": "Optioune konfigur\u00e9ieren fir d'Usiichten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/nl.json b/homeassistant/components/glances/.translations/nl.json new file mode 100644 index 00000000000000..7de81bfee98fee --- /dev/null +++ b/homeassistant/components/glances/.translations/nl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Host is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken met host", + "wrong_version": "Versie niet ondersteund (alleen 2 of 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort", + "ssl": "Gebruik SSL / TLS om verbinding te maken met het Glances-systeem", + "username": "Gebruikersnaam", + "verify_ssl": "Controleer de certificering van het systeem", + "version": "Glances API-versie (2 of 3)" + }, + "title": "Glances instellen" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequentie" + }, + "description": "Configureer opties voor Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/no.json b/homeassistant/components/glances/.translations/no.json new file mode 100644 index 00000000000000..7d742cad2626aa --- /dev/null +++ b/homeassistant/components/glances/.translations/no.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Verten er allerede konfigurert." + }, + "error": { + "cannot_connect": "Kan ikke koble til vert", + "wrong_version": "Versjonen st\u00f8ttes ikke (bare 2 eller 3)" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdater frekvens" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/ru.json b/homeassistant/components/glances/.translations/ru.json new file mode 100644 index 00000000000000..597a914a88d312 --- /dev/null +++ b/homeassistant/components/glances/.translations/ru.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", + "wrong_version": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0435\u0440\u0441\u0438\u0438 2 \u0438 3." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL / TLS \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "username": "\u041b\u043e\u0433\u0438\u043d", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u044b", + "version": "\u0412\u0435\u0440\u0441\u0438\u044f API Glances (2 \u0438\u043b\u0438 3)" + }, + "title": "Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/sl.json b/homeassistant/components/glances/.translations/sl.json new file mode 100644 index 00000000000000..b1d0fda94b5d1f --- /dev/null +++ b/homeassistant/components/glances/.translations/sl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Gostitelj je \u017ee konfiguriran." + }, + "error": { + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem", + "wrong_version": "Razli\u010dica ni podprta (samo 2 ali 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Ime", + "password": "Geslo", + "port": "Vrata", + "ssl": "Za povezavo s sistemom Glances uporabite SSL/TLS", + "username": "Uporabni\u0161ko ime", + "verify_ssl": "Preverite veljavnost potrdila sistema", + "version": "Glances API Version (2 ali 3)" + }, + "title": "Nastavite Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Pogostost posodabljanja" + }, + "description": "Konfiguracija mo\u017enosti za Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/th.json b/homeassistant/components/glances/.translations/th.json new file mode 100644 index 00000000000000..718c857c490f52 --- /dev/null +++ b/homeassistant/components/glances/.translations/th.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0e0a\u0e37\u0e48\u0e2d\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/nl.json b/homeassistant/components/izone/.translations/nl.json new file mode 100644 index 00000000000000..979441f728810c --- /dev/null +++ b/homeassistant/components/izone/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen iZone-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van iZone nodig." + }, + "step": { + "confirm": { + "description": "Wilt u iZone instellen?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/nl.json b/homeassistant/components/lock/.translations/nl.json index 6a39f9cbf58d6b..099b7308334615 100644 --- a/homeassistant/components/lock/.translations/nl.json +++ b/homeassistant/components/lock/.translations/nl.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Vergrendel {entity_name}", + "open": "Open {entity_name}", + "unlock": "Ontgrendel {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} is vergrendeld", "is_unlocked": "{entity_name} is ontgrendeld" diff --git a/homeassistant/components/neato/.translations/nl.json b/homeassistant/components/neato/.translations/nl.json index a90009cb7be1c6..4846f0180f1c82 100644 --- a/homeassistant/components/neato/.translations/nl.json +++ b/homeassistant/components/neato/.translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Al geconfigureerd", "invalid_credentials": "Ongeldige gebruikersgegevens" }, "create_entry": { @@ -12,6 +13,12 @@ }, "step": { "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam", + "vendor": "Leverancier" + }, + "description": "Zie [Neato-documentatie] ({docs_url}).", "title": "Neato-account info" } }, diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json index 81f4aa028f1dee..dbed3326b4a248 100644 --- a/homeassistant/components/opentherm_gw/.translations/nl.json +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -1,15 +1,34 @@ { "config": { + "error": { + "already_configured": "Gateway al geconfigureerd", + "id_exists": "Gateway id bestaat al", + "serial_error": "Fout bij het verbinden met het apparaat", + "timeout": "Er is een time-out opgetreden voor de verbindingspoging" + }, "step": { "init": { "data": { "device": "Pad of URL", + "floor_temperature": "Vloertemperatuur", "id": "ID", + "name": "Naam", "precision": "Klimaattemperatuur precisie" }, "title": "OpenTherm Gateway" } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Vloertemperatuur", + "precision": "Precisie" + }, + "description": "Opties voor de OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/nl.json b/homeassistant/components/plex/.translations/nl.json index 413f4ad3207e80..c971ebb4762c7c 100644 --- a/homeassistant/components/plex/.translations/nl.json +++ b/homeassistant/components/plex/.translations/nl.json @@ -1,6 +1,10 @@ { "config": { "abort": { + "all_configured": "Alle gekoppelde servers zijn al geconfigureerd", + "already_configured": "Deze Plex-server is al geconfigureerd", + "already_in_progress": "Plex wordt geconfigureerd", + "discovery_no_file": "Geen legacy configuratiebestand gevonden", "invalid_import": "Ge\u00efmporteerde configuratie is ongeldig", "token_request_timeout": "Time-out verkrijgen van token", "unknown": "Mislukt om onbekende reden" @@ -16,19 +20,42 @@ "data": { "host": "Host", "port": "Poort", - "ssl": "Gebruik SSL" + "ssl": "Gebruik SSL", + "token": "Token (indien nodig)", + "verify_ssl": "Controleer SSL-certificaat" }, "title": "Plex server" }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Meerdere servers beschikbaar, selecteer er een:", + "title": "Selecteer Plex server" + }, "start_website_auth": { "description": "Ga verder met autoriseren bij plex.tv.", "title": "Verbind de Plex server" }, "user": { "data": { - "manual_setup": "Handmatig setup" + "manual_setup": "Handmatig setup", + "token": "Plex token" + }, + "description": "Ga verder met autoriseren bij plex.tv of configureer een server.", + "title": "Verbind de Plex server" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Toon alle bedieningselementen", + "use_episode_art": "Gebruik aflevering kunst" }, - "description": "Ga verder met autoriseren bij plex.tv of configureer een server." + "description": "Opties voor Plex-mediaspelers" } } } diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 9be270a017ce4d..7426e7f95edd24 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -42,7 +42,7 @@ "manual_setup": "Ro\u010dna nastavitev", "token": "Plex \u017eeton" }, - "description": "Vnesite \u017eeton Plex za samodejno nastavitev ali ro\u010dno konfigurirajte stre\u017enik.", + "description": "Nadaljujte z avtorizacijo na plex.tv ali ro\u010dno konfigurirajte stre\u017enik.", "title": "Pove\u017eite stre\u017enik Plex" } }, diff --git a/homeassistant/components/sensor/.translations/ca.json b/homeassistant/components/sensor/.translations/ca.json index 59db5a62f86062..94d95e7ddf844a 100644 --- a/homeassistant/components/sensor/.translations/ca.json +++ b/homeassistant/components/sensor/.translations/ca.json @@ -4,6 +4,7 @@ "is_battery_level": "Nivell de bateria de {entity_name}", "is_humidity": "Humitat de {entity_name}", "is_illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "is_power": "Pot\u00e8ncia de {entity_name}", "is_pressure": "Pressi\u00f3 de {entity_name}", "is_signal_strength": "For\u00e7a del senyal de {entity_name}", "is_temperature": "Temperatura de {entity_name}", @@ -14,6 +15,7 @@ "battery_level": "Nivell de bateria de {entity_name}", "humidity": "Humitat de {entity_name}", "illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "power": "Pot\u00e8ncia de {entity_name}", "pressure": "Pressi\u00f3 de {entity_name}", "signal_strength": "For\u00e7a del senyal de {entity_name}", "temperature": "Temperatura de {entity_name}", diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json index aca2306d90e17e..f9cd8475a4cc33 100644 --- a/homeassistant/components/sensor/.translations/nl.json +++ b/homeassistant/components/sensor/.translations/nl.json @@ -1,7 +1,15 @@ { "device_automation": { "condition_type": { - "is_power": "{entity_name}\nvermogen" + "is_battery_level": "{entity_name} batterijniveau", + "is_humidity": "{entity_name} vochtigheidsgraad", + "is_illuminance": "{entity_name} verlichtingssterkte", + "is_power": "{entity_name}\nvermogen", + "is_pressure": "{entity_name} druk", + "is_signal_strength": "{entity_name} signaalsterkte", + "is_temperature": "{entity_name} temperatuur", + "is_timestamp": "{entity_name} tijdstip", + "is_value": "{entity_name} waarde" }, "trigger_type": { "battery_level": "{entity_name} batterijniveau", @@ -11,7 +19,8 @@ "pressure": "{entity_name} druk", "signal_strength": "{entity_name} signaalsterkte", "temperature": "{entity_name} temperatuur", - "timestamp": "{entity_name} tijdstip" + "timestamp": "{entity_name} tijdstip", + "value": "{entity_name} waarde" } } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/nl.json b/homeassistant/components/soma/.translations/nl.json index 0bf2836c5a1007..c1188b0ac632e6 100644 --- a/homeassistant/components/soma/.translations/nl.json +++ b/homeassistant/components/soma/.translations/nl.json @@ -1,7 +1,23 @@ { "config": { "abort": { - "already_setup": "U kunt slechts \u00e9\u00e9n Soma-account configureren." - } + "already_setup": "U kunt slechts \u00e9\u00e9n Soma-account configureren.", + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De Soma-component is niet geconfigureerd. Gelieve de documentatie te volgen." + }, + "create_entry": { + "default": "Succesvol geverifieerd met Soma." + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort" + }, + "description": "Voer de verbindingsinstellingen van uw SOMA Connect in.", + "title": "SOMA Connect" + } + }, + "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/nl.json b/homeassistant/components/transmission/.translations/nl.json index 6d9d130f85ca9a..fdf3db99ed0d9b 100644 --- a/homeassistant/components/transmission/.translations/nl.json +++ b/homeassistant/components/transmission/.translations/nl.json @@ -1,16 +1,40 @@ { "config": { + "abort": { + "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." + }, "error": { "cannot_connect": "Kan geen verbinding maken met host", "wrong_credentials": "verkeerde gebruikersnaam of wachtwoord" }, "step": { + "options": { + "data": { + "scan_interval": "Update frequentie" + }, + "title": "Configureer opties" + }, "user": { "data": { + "host": "Host", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort", "username": "Gebruikersnaam" }, "title": "Verzendclient instellen" } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequentie" + }, + "description": "Configureer opties voor Transmission" + } } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json index f2277414a3e649..3a62f5d7ebe2f4 100644 --- a/homeassistant/components/zha/.translations/ko.json +++ b/homeassistant/components/zha/.translations/ko.json @@ -47,11 +47,11 @@ "turn_on": "\ucf1c\uae30" }, "trigger_type": { - "device_dropped": "\uc7a5\uce58\ub97c \ub5a8\uc5b4\ub728\ub9bc", + "device_dropped": "\uae30\uae30\ub97c \ub5a8\uad7c", "device_flipped": "\"{subtype}\" \uae30\uae30\ub97c \ub4a4\uc9d1\uc74c", "device_knocked": "\"{subtype}\" \uae30\uae30\ub97c \ub450\ub4dc\ub9bc", "device_rotated": "\"{subtype}\" \uae30\uae30\ub97c \ud68c\uc804", - "device_shaken": "\uae30\uae30 \ud754\ub4e6", + "device_shaken": "\uae30\uae30\ub97c \ud754\ub4e6", "device_slid": "\"{subtype}\" \uae30\uae30\ub97c \uc2ac\ub77c\uc774\ub4dc", "device_tilted": "\uae30\uae30\ub97c \uae30\uc6b8\uc784", "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", diff --git a/homeassistant/components/zha/.translations/nl.json b/homeassistant/components/zha/.translations/nl.json index bfb47c9d7fcf8b..fc7ae970503c99 100644 --- a/homeassistant/components/zha/.translations/nl.json +++ b/homeassistant/components/zha/.translations/nl.json @@ -23,9 +23,22 @@ "warn": "Waarschuwen" }, "trigger_subtype": { + "both_buttons": "Beide knoppen", + "button_1": "Eerste knop", + "button_2": "Tweede knop", + "button_3": "Derde knop", + "button_4": "Vierde knop", + "button_5": "Vijfde knop", + "button_6": "Zesde knop", "close": "Sluiten", "dim_down": "Dim omlaag", "dim_up": "Dim omhoog", + "face_1": "met gezicht 1 geactiveerd", + "face_2": "met gezicht 2 geactiveerd", + "face_3": "met gezicht 3 geactiveerd", + "face_4": "met gezicht 4 geactiveerd", + "face_5": "met gezicht 5 geactiveerd", + "face_6": "met gezicht 6 geactiveerd", "face_any": "Met elk/opgegeven gezicht (en) geactiveerd", "left": "Links", "open": "Open", From 1111e150f447096db5f6958486d584bba1769a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:31:41 +0000 Subject: [PATCH 1163/3953] Move imports in shodan component (#28098) --- homeassistant/components/shodan/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py index 7b1360b0b01fa0..d2a6a28fbe4004 100644 --- a/homeassistant/components/shodan/sensor.py +++ b/homeassistant/components/shodan/sensor.py @@ -1,12 +1,13 @@ """Sensor for displaying the number of result on Shodan.io.""" -import logging from datetime import timedelta +import logging +import shodan import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -32,8 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Shodan sensor.""" - import shodan - api_key = config.get(CONF_API_KEY) name = config.get(CONF_NAME) query = config.get(CONF_QUERY) From d9cb9601aa00f7aa7b3f10e87a5e8b610f2ce4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:31:58 +0000 Subject: [PATCH 1164/3953] Move imports in skybeacon component (#28099) --- homeassistant/components/skybeacon/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 1c098409610bca..cbf394edf47f04 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -3,6 +3,9 @@ import threading from uuid import UUID +from pygatt import BLEAddressType +from pygatt.backends import Characteristic, GATTToolBackend +from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -132,13 +135,8 @@ def __init__(self, hass, mac, name): def run(self): """Thread that keeps connection alive.""" - # pylint: disable=import-error - import pygatt - from pygatt.backends import Characteristic - from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout - cached_char = Characteristic(BLE_TEMP_UUID, BLE_TEMP_HANDLE) - adapter = pygatt.backends.GATTToolBackend() + adapter = GATTToolBackend() while True: try: _LOGGER.debug("Connecting to %s", self.name) @@ -147,7 +145,7 @@ def run(self): # Seems only one connection can be initiated at a time with CONNECT_LOCK: device = adapter.connect( - self.mac, CONNECT_TIMEOUT, pygatt.BLEAddressType.random + self.mac, CONNECT_TIMEOUT, BLEAddressType.random ) if SKIP_HANDLE_LOOKUP: # HACK: inject handle mapping collected offline From 953f31dd55006a01afb72963c2143ac68e8cc73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:32:37 +0000 Subject: [PATCH 1165/3953] Move imports in shiftr component (#28097) --- homeassistant/components/shiftr/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shiftr/__init__.py b/homeassistant/components/shiftr/__init__.py index 8e698d283cfcaa..1c3cddac2563e8 100644 --- a/homeassistant/components/shiftr/__init__.py +++ b/homeassistant/components/shiftr/__init__.py @@ -1,16 +1,17 @@ """Support for Shiftr.io.""" import logging +import paho.mqtt.client as mqtt import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, - EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, + EVENT_STATE_CHANGED, ) from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -33,8 +34,6 @@ def setup(hass, config): """Initialize the Shiftr.io MQTT consumer.""" - import paho.mqtt.client as mqtt - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) From f440259edc02af7ca82a09a0e9e991485b4d5ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:32:53 +0000 Subject: [PATCH 1166/3953] Move imports in seven_segments component (#28096) --- .../seven_segments/image_processing.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py index 4b96cc50ecce73..315b5c39fec0af 100644 --- a/homeassistant/components/seven_segments/image_processing.py +++ b/homeassistant/components/seven_segments/image_processing.py @@ -1,19 +1,21 @@ """Optical character recognition processing of seven segments displays.""" -import logging import io +import logging import os +import subprocess +from PIL import Image import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, - ImageProcessingEntity, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingEntity, ) +from homeassistant.core import split_entity_id +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -120,9 +122,6 @@ def state(self): def process_image(self, image): """Process the image.""" - from PIL import Image - import subprocess - stream = io.BytesIO(image) img = Image.open(stream) img.save(self.filepath, "png") From 828bf1b400092e2f578072fcd2fff573f6f4c62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:33:02 +0000 Subject: [PATCH 1167/3953] Move imports in sesame component (#28095) --- homeassistant/components/sesame/lock.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index f8698ac6bd8860..fa12ff7a1b22f5 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -1,15 +1,17 @@ """Support for Sesame, by CANDY HOUSE.""" from typing import Callable + +import pysesame2 import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONF_API_KEY, STATE_LOCKED, STATE_UNLOCKED, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType ATTR_DEVICE_ID = "device_id" @@ -22,8 +24,6 @@ def setup_platform( hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None ): """Set up the Sesame platform.""" - import pysesame2 - api_key = config.get(CONF_API_KEY) add_entities( From 4a3d6208ae54cec17c169dad8d6d39f65165f9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:33:23 +0000 Subject: [PATCH 1168/3953] Move imports in rpi_pfio component (#28094) --- homeassistant/components/rpi_pfio/__init__.py | 10 ++-------- homeassistant/components/rpi_pfio/binary_sensor.py | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rpi_pfio/__init__.py b/homeassistant/components/rpi_pfio/__init__.py index d51785daf9c8f9..72be34e0f45fce 100644 --- a/homeassistant/components/rpi_pfio/__init__.py +++ b/homeassistant/components/rpi_pfio/__init__.py @@ -1,6 +1,8 @@ """Support for controlling the PiFace Digital I/O module on a RPi.""" import logging +import pifacedigitalio as PFIO + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -12,8 +14,6 @@ def setup(hass, config): """Set up the Raspberry PI PFIO component.""" - import pifacedigitalio as PFIO - pifacedigital = PFIO.PiFaceDigital() hass.data[DATA_PFIO_LISTENER] = PFIO.InputEventListener(chip=pifacedigital) @@ -33,22 +33,16 @@ def prepare_pfio(event): def write_output(port, value): """Write a value to a PFIO.""" - import pifacedigitalio as PFIO - PFIO.digital_write(port, value) def read_input(port): """Read a value from a PFIO.""" - import pifacedigitalio as PFIO - return PFIO.digital_read(port) def edge_detect(hass, port, event_callback, settle): """Add detection for RISING and FALLING events.""" - import pifacedigitalio as PFIO - hass.data[DATA_PFIO_LISTENER].register( port, PFIO.IODIR_BOTH, event_callback, settle_time=settle ) diff --git a/homeassistant/components/rpi_pfio/binary_sensor.py b/homeassistant/components/rpi_pfio/binary_sensor.py index 44da251732b65f..89d44a0e8db301 100644 --- a/homeassistant/components/rpi_pfio/binary_sensor.py +++ b/homeassistant/components/rpi_pfio/binary_sensor.py @@ -3,8 +3,8 @@ import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.components import rpi_pfio +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv From d9b890a4024d04bbab65e3feef3c1f435f0213f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:34:35 +0000 Subject: [PATCH 1169/3953] Move imports in repetier component (#28093) --- homeassistant/components/repetier/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 6f72a6b7ddc545..12975baca91e49 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -1,7 +1,8 @@ """Support for Repetier-Server sensors.""" -import logging from datetime import timedelta +import logging +import pyrepetier import voluptuous as vol from homeassistant.const import ( @@ -160,8 +161,6 @@ def has_all_unique_names(value): def setup(hass, config): """Set up the Repetier Server component.""" - import pyrepetier - hass.data[REPETIER_API] = {} for repetier in config[DOMAIN]: From 3e8f2bf2fce86054bc4bae53e3e723383753d97b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:34:51 +0000 Subject: [PATCH 1170/3953] Move imports in remember_the_milk component (#28092) --- .../components/remember_the_milk/__init__.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index c92a246da14789..fdfbdfd5cdc203 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -3,6 +3,7 @@ import logging import os +from rtmapi import Rtm, RtmRequestFailedException import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME, CONF_TOKEN, STATE_OK @@ -102,8 +103,6 @@ def _create_instance( def _register_new_account( hass, account_name, api_key, shared_secret, stored_rtm_config, component ): - from rtmapi import Rtm - request_id = None configurator = hass.components.configurator api = Rtm(api_key, shared_secret, "write", None) @@ -240,14 +239,12 @@ class RememberTheMilk(Entity): def __init__(self, name, api_key, shared_secret, token, rtm_config): """Create new instance of Remember The Milk component.""" - import rtmapi - self._name = name self._api_key = api_key self._shared_secret = shared_secret self._token = token self._rtm_config = rtm_config - self._rtm_api = rtmapi.Rtm(api_key, shared_secret, "delete", token) + self._rtm_api = Rtm(api_key, shared_secret, "delete", token) self._token_valid = None self._check_token() _LOGGER.debug("Instance created for account %s", self._name) @@ -277,8 +274,6 @@ def create_task(self, call): e.g. "my task #some_tag ^today" will add tag "some_tag" and set the due date to today. """ - import rtmapi - try: task_name = call.data.get(CONF_NAME) hass_id = call.data.get(CONF_ID) @@ -316,7 +311,7 @@ def create_task(self, call): self.name, task_name, ) - except rtmapi.RtmRequestFailedException as rtm_exception: + except RtmRequestFailedException as rtm_exception: _LOGGER.error( "Error creating new Remember The Milk task for " "account %s: %s", self._name, @@ -327,8 +322,6 @@ def create_task(self, call): def complete_task(self, call): """Complete a task that was previously created by this component.""" - import rtmapi - hass_id = call.data.get(CONF_ID) rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id) if rtm_id is None: @@ -352,7 +345,7 @@ def complete_task(self, call): _LOGGER.debug( "Completed task with id %s in account %s", hass_id, self._name ) - except rtmapi.RtmRequestFailedException as rtm_exception: + except RtmRequestFailedException as rtm_exception: _LOGGER.error( "Error creating new Remember The Milk task for " "account %s: %s", self._name, From 40fbfe7a93c712d0ab0be97dc896982ef61e989a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:35:05 +0000 Subject: [PATCH 1171/3953] Move imports in rejseplanen component (#28091) --- homeassistant/components/rejseplanen/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 61cb319fd11ed6..b7d36010714c8e 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -7,17 +7,18 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.rejseplanen/ """ +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime from operator import itemgetter +import rjpl import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -166,8 +167,6 @@ def __init__(self, stop_id, route, direction, departure_type): def update(self): """Get the latest data from rejseplanen.""" - import rjpl - self.info = [] def intersection(lst1, lst2): From 0193207b5c662b0f29c914e6ced2b88f3b5c84d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:35:25 +0000 Subject: [PATCH 1172/3953] Move imports in recollect_waste component (#28089) --- homeassistant/components/recollect_waste/sensor.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 118b6fb370979a..17496f3d361376 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,11 +1,12 @@ """Support for Recollect Waste curbside collection pickup.""" import logging +import recollect_waste import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -29,9 +30,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Recollect Waste platform.""" - import recollect_waste - - # pylint: disable=no-member client = recollect_waste.RecollectWasteClient( config[CONF_PLACE_ID], config[CONF_SERVICE_ID] ) @@ -40,7 +38,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # with given place_id and service_id. try: client.get_next_pickup() - # pylint: disable=no-member except recollect_waste.RecollectWasteException as ex: _LOGGER.error("Recollect Waste platform error. %s", ex) return @@ -85,8 +82,6 @@ def icon(self): def update(self): """Update device state.""" - import recollect_waste - try: pickup_event = self.client.get_next_pickup() self._state = pickup_event.event_date @@ -96,6 +91,5 @@ def update(self): ATTR_AREA_NAME: pickup_event.area_name, } ) - # pylint: disable=no-member except recollect_waste.RecollectWasteException as ex: _LOGGER.error("Recollect Waste platform error. %s", ex) From 2d36e9c08e0b994239e6e888f8954769e9735c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:35:43 +0000 Subject: [PATCH 1173/3953] Move imports in prometheus component (#28086) --- .../components/prometheus/__init__.py | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 82db5f6725f512..8eeb9325bc0a7d 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -3,24 +3,25 @@ import string from aiohttp import web +import prometheus_client import voluptuous as vol from homeassistant import core as hacore from homeassistant.components.climate.const import ATTR_CURRENT_TEMPERATURE from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, - ATTR_DEVICE_CLASS, CONTENT_TYPE_TEXT_PLAIN, EVENT_STATE_CHANGED, - TEMP_FAHRENHEIT, TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) 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 +from homeassistant.util.temperature import fahrenheit_to_celsius _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ def setup(hass, config): """Activate Prometheus component.""" - import prometheus_client - hass.http.register_view(PrometheusView(prometheus_client)) conf = config[DOMAIN] @@ -99,7 +98,7 @@ class PrometheusMetrics: def __init__( self, - prometheus_client, + prometheus_cli, entity_filter, namespace, climate_units, @@ -108,7 +107,7 @@ def __init__( default_metric, ): """Initialize Prometheus Metrics.""" - self.prometheus_client = prometheus_client + self.prometheus_cli = prometheus_cli self._component_config = component_config self._override_metric = override_metric self._default_metric = default_metric @@ -147,9 +146,7 @@ def handle_event(self, event): getattr(self, handler)(state) metric = self._metric( - "state_change", - self.prometheus_client.Counter, - "The number of state changes", + "state_change", self.prometheus_cli.Counter, "The number of state changes" ) metric.labels(**self._labels(state)).inc() @@ -199,7 +196,7 @@ def _battery(self, state): if "battery_level" in state.attributes: metric = self._metric( "battery_level_percent", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "Battery level as a percentage of its capacity", ) try: @@ -211,7 +208,7 @@ def _battery(self, state): def _handle_binary_sensor(self, state): metric = self._metric( "binary_sensor_state", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "State of the binary sensor (0/1)", ) value = self.state_as_number(state) @@ -220,7 +217,7 @@ def _handle_binary_sensor(self, state): def _handle_input_boolean(self, state): metric = self._metric( "input_boolean_state", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "State of the input boolean (0/1)", ) value = self.state_as_number(state) @@ -229,7 +226,7 @@ def _handle_input_boolean(self, state): def _handle_device_tracker(self, state): metric = self._metric( "device_tracker_state", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "State of the device tracker (0/1)", ) value = self.state_as_number(state) @@ -237,14 +234,14 @@ def _handle_device_tracker(self, state): def _handle_person(self, state): metric = self._metric( - "person_state", self.prometheus_client.Gauge, "State of the person (0/1)" + "person_state", self.prometheus_cli.Gauge, "State of the person (0/1)" ) value = self.state_as_number(state) metric.labels(**self._labels(state)).set(value) def _handle_light(self, state): metric = self._metric( - "light_state", self.prometheus_client.Gauge, "Load level of a light (0..1)" + "light_state", self.prometheus_cli.Gauge, "Load level of a light (0..1)" ) try: @@ -259,7 +256,7 @@ def _handle_light(self, state): def _handle_lock(self, state): metric = self._metric( - "lock_state", self.prometheus_client.Gauge, "State of the lock (0/1)" + "lock_state", self.prometheus_cli.Gauge, "State of the lock (0/1)" ) value = self.state_as_number(state) metric.labels(**self._labels(state)).set(value) @@ -271,7 +268,7 @@ def _handle_climate(self, state): temp = fahrenheit_to_celsius(temp) metric = self._metric( "temperature_c", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "Temperature in degrees Celsius", ) metric.labels(**self._labels(state)).set(temp) @@ -282,15 +279,13 @@ def _handle_climate(self, state): current_temp = fahrenheit_to_celsius(current_temp) metric = self._metric( "current_temperature_c", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "Current Temperature in degrees Celsius", ) metric.labels(**self._labels(state)).set(current_temp) metric = self._metric( - "climate_state", - self.prometheus_client.Gauge, - "State of the thermostat (0/1)", + "climate_state", self.prometheus_cli.Gauge, "State of the thermostat (0/1)" ) try: value = self.state_as_number(state) @@ -308,7 +303,7 @@ def _handle_sensor(self, state): if metric is not None: _metric = self._metric( - metric, self.prometheus_client.Gauge, f"Sensor data measured in {unit}" + metric, self.prometheus_cli.Gauge, f"Sensor data measured in {unit}" ) try: @@ -368,7 +363,7 @@ def _unit_string(unit): def _handle_switch(self, state): metric = self._metric( - "switch_state", self.prometheus_client.Gauge, "State of the switch (0/1)" + "switch_state", self.prometheus_cli.Gauge, "State of the switch (0/1)" ) try: @@ -383,7 +378,7 @@ def _handle_zwave(self, state): def _handle_automation(self, state): metric = self._metric( "automation_triggered_count", - self.prometheus_client.Counter, + self.prometheus_cli.Counter, "Count of times an automation has been triggered", ) @@ -396,15 +391,15 @@ class PrometheusView(HomeAssistantView): url = API_ENDPOINT name = "api:prometheus" - def __init__(self, prometheus_client): + def __init__(self, prometheus_cli): """Initialize Prometheus view.""" - self.prometheus_client = prometheus_client + self.prometheus_cli = prometheus_cli async def get(self, request): """Handle request for Prometheus metrics.""" _LOGGER.debug("Received Prometheus metrics request") return web.Response( - body=self.prometheus_client.generate_latest(), + body=self.prometheus_cli.generate_latest(), content_type=CONTENT_TYPE_TEXT_PLAIN, ) From 1313ec4ec8b9de74685c83ecef3a0c97b075ae79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:36:35 +0000 Subject: [PATCH 1174/3953] Move imports in proliphix component (#28085) --- homeassistant/components/proliphix/climate.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 666f8c28241563..44e31e24fb0556 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -1,7 +1,8 @@ """Support for Proliphix NT10e Thermostats.""" +import proliphix import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -9,12 +10,12 @@ SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, PRECISION_TENTHS, TEMP_FAHRENHEIT, - ATTR_TEMPERATURE, ) import homeassistant.helpers.config_validation as cv @@ -35,8 +36,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config.get(CONF_PASSWORD) host = config.get(CONF_HOST) - import proliphix - pdp = proliphix.PDP(host, username, password) add_entities([ProliphixThermostat(pdp)], True) From 87cc6610872ac560ed221c622bb7659931d63d4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:36:46 +0000 Subject: [PATCH 1175/3953] Move imports in pocketcasts component (#28084) --- homeassistant/components/pocketcasts/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index 815e2688009af7..05a8f96bda7ca3 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -1,13 +1,13 @@ """Support for Pocket Casts.""" -import logging - from datetime import timedelta +import logging +import pocketcasts import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -25,8 +25,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the pocketcasts platform for sensors.""" - import pocketcasts - username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) From a8c6b04906d77509e2917978728c6cdf2fcbba03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:37:09 +0000 Subject: [PATCH 1176/3953] Move imports in opencv component (#28042) * Move imports in opencv component * Fix pylint --- .../components/opencv/image_processing.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py index 3c72af4f36866c..4a1b830a324209 100644 --- a/homeassistant/components/opencv/image_processing.py +++ b/homeassistant/components/opencv/image_processing.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import numpy import requests import voluptuous as vol @@ -15,6 +16,15 @@ from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv +try: + # Verify that the OpenCV python package is pre-installed + import cv2 + + CV2_IMPORTED = True +except ImportError: + CV2_IMPORTED = False + + _LOGGER = logging.getLogger(__name__) ATTR_MATCHES = "matches" @@ -86,11 +96,7 @@ def _get_default_classifier(dest_path): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenCV image processing platform.""" - try: - # Verify that the OpenCV python package is pre-installed - # pylint: disable=unused-import,unused-variable - import cv2 # noqa - except ImportError: + if not CV2_IMPORTED: _LOGGER.error( "No OpenCV library found! Install or compile for your system " "following instructions here: http://opencv.org/releases.html" @@ -154,9 +160,6 @@ def state_attributes(self): def process_image(self, image): """Process the image.""" - import cv2 # pylint: disable=import-error - import numpy - cv_image = cv2.imdecode(numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) for name, classifier in self._classifiers.items(): From 1a48c347a415b06a53d0b799e57cbdaf680b1134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:37:31 +0000 Subject: [PATCH 1177/3953] Move imports in mitemp_bt component (#28026) * Move imports in mitemp_bt component * Fix pylint --- homeassistant/components/mitemp_bt/sensor.py | 31 +++++++++----------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index adeba48dbc8517..b536149680dc4e 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -1,21 +1,30 @@ """Support for Xiaomi Mi Temp BLE environmental sensor.""" import logging +import btlewrap +from btlewrap.base import BluetoothBackendException +from mitemp_bt import mitemp_bt_poller import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_FORCE_UPDATE, + CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, - CONF_MAC, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +try: + import bluepy.btle # noqa: F401 pylint: disable=unused-import + BACKEND = btlewrap.BluepyBackend +except ImportError: + BACKEND = btlewrap.GatttoolBackend _LOGGER = logging.getLogger(__name__) @@ -60,17 +69,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MiTempBt sensor.""" - from mitemp_bt import mitemp_bt_poller - - try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - from btlewrap import BluepyBackend - - backend = BluepyBackend - except ImportError: - from btlewrap import GatttoolBackend - - backend = GatttoolBackend + backend = BACKEND _LOGGER.debug("MiTempBt is using %s backend.", backend.__name__) cache = config.get(CONF_CACHE) @@ -152,8 +151,6 @@ def update(self): This uses a rolling median over 3 values to filter out outliers. """ - from btlewrap.base import BluetoothBackendException - try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) From 5d317dc096f147fc8930bd2ce77823678c763b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:37:48 +0000 Subject: [PATCH 1178/3953] Move imports in miflora component (#28025) * Move imports in miflora component * Fix pylint --- homeassistant/components/miflora/sensor.py | 31 +++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 28020a801750f3..a08c4ce5eacbe4 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -1,20 +1,31 @@ """Support for Xiaomi Mi Flora BLE plant sensor.""" from datetime import timedelta import logging + +import btlewrap +from btlewrap import BluetoothBackendException +from miflora import miflora_poller import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_FORCE_UPDATE, + CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, - CONF_MAC, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +try: + import bluepy.btle # noqa: F401 pylint: disable=unused-import + + BACKEND = btlewrap.BluepyBackend +except ImportError: + BACKEND = btlewrap.GatttoolBackend _LOGGER = logging.getLogger(__name__) @@ -53,17 +64,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the MiFlora sensor.""" - from miflora import miflora_poller - - try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - from btlewrap import BluepyBackend - - backend = BluepyBackend - except ImportError: - from btlewrap import GatttoolBackend - - backend = GatttoolBackend + backend = BACKEND _LOGGER.debug("Miflora is using %s backend.", backend.__name__) cache = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL).total_seconds() @@ -152,8 +153,6 @@ def update(self): This uses a rolling median over 3 values to filter out outliers. """ - from btlewrap import BluetoothBackendException - try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) From 6c18bbcf044800d5cbd5813ad8fb2e311eef0deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:38:01 +0000 Subject: [PATCH 1179/3953] Move imports in lastfm component (#28010) * Move imports in lastfm component * Fix pylint --- homeassistant/components/lastfm/sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 736792aefd8123..68d727626cfc9c 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -2,10 +2,12 @@ import logging import re +import pylast as lastfm +from pylast import WSError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -30,9 +32,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Last.fm sensor platform.""" - import pylast as lastfm - from pylast import WSError - api_key = config[CONF_API_KEY] users = config.get(CONF_USERS) @@ -53,11 +52,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class LastfmSensor(Entity): """A class for the Last.fm account.""" - def __init__(self, user, lastfm): + def __init__(self, user, lastfm_api): """Initialize the sensor.""" - self._user = lastfm.get_user(user) + self._user = lastfm_api.get_user(user) self._name = user - self._lastfm = lastfm + self._lastfm = lastfm_api self._state = "Not Scrobbling" self._playcount = None self._lastplayed = None From ff17bb4a56d8b0de56c155c89281ed2440d1ab59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:38:21 +0000 Subject: [PATCH 1180/3953] Move imports in knx component (#28008) * Move imports in knx component * Fix pylint --- homeassistant/components/knx/__init__.py | 28 ++++--------------- homeassistant/components/knx/binary_sensor.py | 4 +-- homeassistant/components/knx/climate.py | 22 ++++++--------- homeassistant/components/knx/cover.py | 5 ++-- homeassistant/components/knx/light.py | 5 ++-- homeassistant/components/knx/notify.py | 5 ++-- homeassistant/components/knx/scene.py | 5 ++-- homeassistant/components/knx/sensor.py | 5 ++-- homeassistant/components/knx/switch.py | 5 ++-- 9 files changed, 29 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 5d0c4be3d07486..00d5d18f0131bd 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -2,6 +2,11 @@ import logging import voluptuous as vol +from xknx import XKNX +from xknx.devices import ActionCallback, DateTime, DateTimeBroadcastType, ExposeSensor +from xknx.exceptions import XKNXException +from xknx.io import DEFAULT_MCAST_PORT, ConnectionConfig, ConnectionType +from xknx.knx import AddressFilter, DPTArray, DPTBinary, GroupAddress, Telegram from homeassistant.const import ( CONF_ENTITY_ID, @@ -90,13 +95,10 @@ async def async_setup(hass, config): """Set up the KNX component.""" - from xknx.exceptions import XKNXException - try: hass.data[DATA_KNX] = KNXModule(hass, config) hass.data[DATA_KNX].async_create_exposures() await hass.data[DATA_KNX].start() - except XKNXException as ex: _LOGGER.warning("Can't connect to KNX interface: %s", ex) hass.components.persistent_notification.async_create( @@ -157,8 +159,6 @@ def __init__(self, hass, config): def init_xknx(self): """Initialize of KNX object.""" - from xknx import XKNX - self.xknx = XKNX( config=self.config_file(), loop=self.hass.loop, @@ -198,8 +198,6 @@ def connection_config(self): def connection_config_routing(self): """Return the connection_config if routing is configured.""" - from xknx.io import ConnectionConfig, ConnectionType - local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get(CONF_KNX_LOCAL_IP) return ConnectionConfig( connection_type=ConnectionType.ROUTING, local_ip=local_ip @@ -207,8 +205,6 @@ def connection_config_routing(self): def connection_config_tunneling(self): """Return the connection_config if tunneling is configured.""" - from xknx.io import ConnectionConfig, ConnectionType, DEFAULT_MCAST_PORT - gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_HOST) gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_PORT) local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_KNX_LOCAL_IP) @@ -224,8 +220,6 @@ def connection_config_tunneling(self): def connection_config_auto(self): """Return the connection_config if auto is configured.""" # pylint: disable=no-self-use - from xknx.io import ConnectionConfig - return ConnectionConfig() def register_callbacks(self): @@ -234,8 +228,6 @@ def register_callbacks(self): CONF_KNX_FIRE_EVENT in self.config[DOMAIN] and self.config[DOMAIN][CONF_KNX_FIRE_EVENT] ): - from xknx.knx import AddressFilter - address_filters = list( map(AddressFilter, self.config[DOMAIN][CONF_KNX_FIRE_EVENT_FILTER]) ) @@ -274,8 +266,6 @@ async def telegram_received_cb(self, telegram): async def service_send_to_knx_bus(self, call): """Service for sending an arbitrary KNX message to the KNX bus.""" - from xknx.knx import Telegram, GroupAddress, DPTBinary, DPTArray - attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD) attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS) @@ -304,9 +294,7 @@ def __init__(self, hass, device, hook, action, counter=1): script_name = "{} turn ON script".format(device.get_name()) self.script = Script(hass, action, script_name) - import xknx - - self.action = xknx.devices.ActionCallback( + self.action = ActionCallback( hass.data[DATA_KNX].xknx, self.script.async_run, hook=hook, counter=counter ) device.actions.append(self.action) @@ -325,8 +313,6 @@ def __init__(self, xknx, expose_type, address): @callback def async_register(self): """Register listener.""" - from xknx.devices import DateTime, DateTimeBroadcastType - broadcast_type_string = self.type.upper() broadcast_type = DateTimeBroadcastType[broadcast_type_string] self.device = DateTime( @@ -350,8 +336,6 @@ def __init__(self, hass, xknx, expose_type, entity_id, address): @callback def async_register(self): """Register listener.""" - from xknx.devices import ExposeSensor - self.device = ExposeSensor( self.xknx, name=self.entity_id, diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index fbe9c4e421ed48..94a171d9c2a75e 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -1,5 +1,6 @@ """Support for KNX/IP binary sensors.""" import voluptuous as vol +from xknx.devices import BinarySensor from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME @@ -70,9 +71,8 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): def async_add_entities_config(hass, config, async_add_entities): """Set up binary senor for KNX platform configured within platform.""" name = config[CONF_NAME] - import xknx - binary_sensor = xknx.devices.BinarySensor( + binary_sensor = BinarySensor( hass.data[DATA_KNX].xknx, name=name, group_address_state=config[CONF_STATE_ADDRESS], diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 014cd8d9ba1360..819fb1794c3896 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,20 +1,22 @@ """Support for KNX/IP climate devices.""" -from typing import Optional, List +from typing import List, Optional import voluptuous as vol +from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode +from xknx.knx import HVACOperationMode from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, + HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_OFF, - HVAC_MODE_COOL, - HVAC_MODE_AUTO, - PRESET_ECO, - PRESET_SLEEP, PRESET_AWAY, PRESET_COMFORT, + PRESET_ECO, + PRESET_SLEEP, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -135,9 +137,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up climate for KNX platform configured within platform.""" - import xknx - - climate_mode = xknx.devices.ClimateMode( + climate_mode = XknxClimateMode( hass.data[DATA_KNX].xknx, name=config[CONF_NAME] + " Mode", group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS), @@ -165,7 +165,7 @@ def async_add_entities_config(hass, config, async_add_entities): ) hass.data[DATA_KNX].xknx.devices.add(climate_mode) - climate = xknx.devices.Climate( + climate = XknxClimate( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_temperature=config[CONF_TEMPERATURE_ADDRESS], @@ -302,8 +302,6 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: elif self.device.supports_on_off and hvac_mode == HVAC_MODE_HEAT: await self.device.turn_on() elif self.device.mode.supports_operation_mode: - from xknx.knx import HVACOperationMode - knx_operation_mode = HVACOperationMode(OPERATION_MODES_INV.get(hvac_mode)) await self.device.mode.set_operation_mode(knx_operation_mode) await self.async_update_ha_state() @@ -337,8 +335,6 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: This method must be run in the event loop and returns a coroutine. """ if self.device.mode.supports_operation_mode: - from xknx.knx import HVACOperationMode - knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode)) await self.device.mode.set_operation_mode(knx_operation_mode) await self.async_update_ha_state() diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 9af7c11678aa1c..976d1286c9f3bc 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -1,5 +1,6 @@ """Support for KNX/IP covers.""" import voluptuous as vol +from xknx.devices import Cover as XknxCover from homeassistant.components.cover import ( ATTR_POSITION, @@ -74,9 +75,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up cover for KNX platform configured within platform.""" - import xknx - - cover = xknx.devices.Cover( + cover = XknxCover( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_long=config.get(CONF_MOVE_LONG_ADDRESS), diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 71a82c6df2a6eb..81bf4ad3c8375d 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -2,6 +2,7 @@ from enum import Enum import voluptuous as vol +from xknx.devices import Light as XknxLight from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -98,8 +99,6 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up light for KNX platform configured within platform.""" - import xknx - group_address_tunable_white = None group_address_tunable_white_state = None group_address_color_temp = None @@ -111,7 +110,7 @@ def async_add_entities_config(hass, config, async_add_entities): group_address_tunable_white = config.get(CONF_COLOR_TEMP_ADDRESS) group_address_tunable_white_state = config.get(CONF_COLOR_TEMP_STATE_ADDRESS) - light = xknx.devices.Light( + light = XknxLight( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_switch=config[CONF_ADDRESS], diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index b83edb89eb132a..64d513b862442f 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -1,5 +1,6 @@ """Support for KNX/IP notification services.""" import voluptuous as vol +from xknx.devices import Notification as XknxNotification from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ADDRESS, CONF_NAME @@ -42,9 +43,7 @@ def async_get_service_discovery(hass, discovery_info): @callback def async_get_service_config(hass, config): """Set up notification for KNX platform configured within platform.""" - import xknx - - notification = xknx.devices.Notification( + notification = XknxNotification( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address=config[CONF_ADDRESS], diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index d635384092fb99..c8c6ac2bcfb86f 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -1,5 +1,6 @@ """Support for KNX scenes.""" import voluptuous as vol +from xknx.devices import Scene as XknxScene from homeassistant.components.scene import CONF_PLATFORM, Scene from homeassistant.const import CONF_ADDRESS, CONF_NAME @@ -42,9 +43,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up scene for KNX platform configured within platform.""" - import xknx - - scene = xknx.devices.Scene( + scene = XknxScene( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address=config[CONF_ADDRESS], diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 9a19ba91b7a301..a0a0f6ea18d016 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,5 +1,6 @@ """Support for KNX/IP sensors.""" import voluptuous as vol +from xknx.devices import Sensor as XknxSensor from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_TYPE @@ -44,9 +45,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up sensor for KNX platform configured within platform.""" - import xknx - - sensor = xknx.devices.Sensor( + sensor = XknxSensor( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_state=config[CONF_STATE_ADDRESS], diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 72a5b5dcdd7dcd..e9a0df5c983688 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -1,5 +1,6 @@ """Support for KNX/IP switches.""" import voluptuous as vol +from xknx.devices import Switch as XknxSwitch from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_ADDRESS, CONF_NAME @@ -41,9 +42,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up switch for KNX platform configured within platform.""" - import xknx - - switch = xknx.devices.Switch( + switch = XknxSwitch( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address=config[CONF_ADDRESS], From 3692c7496e1a3eaa7e67a8639fbe454b4c6dcc88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:38:33 +0000 Subject: [PATCH 1181/3953] Move imports in gtfs component (#27999) * Move imports in gtfs component * Fix pylint --- homeassistant/components/gtfs/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 086545f0c761d5..07b450dd33e157 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -5,6 +5,8 @@ import threading from typing import Any, Callable, Optional +import pygtfs +from sqlalchemy.sql import text import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -129,8 +131,6 @@ def get_next_departure( tomorrow = now + datetime.timedelta(days=1) tomorrow_date = tomorrow.strftime(dt_util.DATE_STR_FORMAT) - from sqlalchemy.sql import text - # Fetch all departures for yesterday, today and optionally tomorrow, # up to an overkill maximum in case of a departure every minute for those # days. @@ -353,8 +353,6 @@ def setup_platform( _LOGGER.error("The given GTFS data file/folder was not found") return - import pygtfs - (gtfs_root, _) = os.path.splitext(data) sqlite_file = f"{gtfs_root}.sqlite?check_same_thread=False" @@ -375,7 +373,7 @@ class GTFSDepartureSensor(Entity): def __init__( self, - pygtfs: Any, + gtfs: Any, name: Optional[Any], origin: Any, destination: Any, @@ -383,7 +381,7 @@ def __init__( include_tomorrow: bool, ) -> None: """Initialize the sensor.""" - self._pygtfs = pygtfs + self._pygtfs = gtfs self.origin = origin self.destination = destination self._include_tomorrow = include_tomorrow From 4a54b130cb83d68cce260d5baa5f245dbe3307b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:39:36 +0000 Subject: [PATCH 1182/3953] Move imports in ptvsd component (#28087) --- homeassistant/components/ptvsd/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ptvsd/__init__.py b/homeassistant/components/ptvsd/__init__.py index 869987bfe4b8be..f12c8004774848 100644 --- a/homeassistant/components/ptvsd/__init__.py +++ b/homeassistant/components/ptvsd/__init__.py @@ -4,10 +4,11 @@ Attach ptvsd debugger by default to port 5678. """ +from asyncio import Event import logging from threading import Thread -from asyncio import Event +import ptvsd import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT @@ -36,8 +37,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up ptvsd debugger.""" - import ptvsd - conf = config[DOMAIN] host = conf[CONF_HOST] port = conf[CONF_PORT] From 77d55a3b155ee82c0f70bca229fe24b04bd9cf9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:40:49 +0000 Subject: [PATCH 1183/3953] Move imports in isy994 component (#28004) --- homeassistant/components/isy994/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 324dcb019b3915..96796e37a6a393 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -3,6 +3,8 @@ import logging from urllib.parse import urlparse +import PyISY +from PyISY.Nodes import Group import voluptuous as vol from homeassistant.const import ( @@ -312,8 +314,6 @@ def _categorize_nodes( # Don't import this node as a device at all continue - from PyISY.Nodes import Group - if isinstance(node, Group): hass.data[ISY994_NODES][SCENE_DOMAIN].append(node) continue @@ -419,8 +419,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.error("isy994 host value in configuration is invalid") return False - import PyISY - # Connect to ISY controller. isy = PyISY.ISY( host.hostname, From acee87bef6edfa83d949a87b553bf92a9592277b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 22 Oct 2019 09:00:58 +0200 Subject: [PATCH 1184/3953] Support to use Whatsapp numbers (fixes ##28065) (#28078) --- homeassistant/components/twilio/__init__.py | 9 ++++----- homeassistant/components/twilio/config_flow.py | 1 - homeassistant/components/twilio/manifest.json | 2 +- homeassistant/components/twilio_call/notify.py | 5 ++--- homeassistant/components/twilio_sms/notify.py | 8 ++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index ea5629e7cab6d9..15c6697b2f7fa5 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -1,9 +1,12 @@ """Support for Twilio.""" +from twilio.rest import Client +from twilio.twiml import TwiML import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv + from .const import DOMAIN CONF_ACCOUNT_SID = "account_sid" @@ -28,8 +31,6 @@ async def async_setup(hass, config): """Set up the Twilio component.""" - from twilio.rest import Client - if DOMAIN not in config: return True @@ -42,8 +43,6 @@ async def async_setup(hass, config): async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook from Twilio for inbound messages and calls.""" - from twilio.twiml import TwiML - data = dict(await request.post()) data["webhook_id"] = webhook_id hass.bus.async_fire(RECEIVED_DATA, dict(data)) diff --git a/homeassistant/components/twilio/config_flow.py b/homeassistant/components/twilio/config_flow.py index dad8e0bf4966a3..1539c1ffadc945 100644 --- a/homeassistant/components/twilio/config_flow.py +++ b/homeassistant/components/twilio/config_flow.py @@ -3,7 +3,6 @@ from .const import DOMAIN - config_entry_flow.register_webhook_flow( DOMAIN, "Twilio Webhook", diff --git a/homeassistant/components/twilio/manifest.json b/homeassistant/components/twilio/manifest.json index 23fac51a3476b2..8f4ed125fb6363 100644 --- a/homeassistant/components/twilio/manifest.json +++ b/homeassistant/components/twilio/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/twilio", "requirements": [ - "twilio==6.19.1" + "twilio==6.32.0" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py index 0672a3d3b9ee42..8270509181447d 100644 --- a/homeassistant/components/twilio_call/notify.py +++ b/homeassistant/components/twilio_call/notify.py @@ -4,14 +4,13 @@ import voluptuous as vol -from homeassistant.components.twilio import DATA_TWILIO -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.components.twilio import DATA_TWILIO +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/twilio_sms/notify.py b/homeassistant/components/twilio_sms/notify.py index bd873f13468560..da5e0e754b9a73 100644 --- a/homeassistant/components/twilio_sms/notify.py +++ b/homeassistant/components/twilio_sms/notify.py @@ -3,15 +3,14 @@ import voluptuous as vol -from homeassistant.components.twilio import DATA_TWILIO -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( + ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, - ATTR_DATA, ) +from homeassistant.components.twilio import DATA_TWILIO +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -26,6 +25,7 @@ r"^\+?[1-9]\d{1,14}$|" r"^(?=.{1,11}$)[a-zA-Z0-9\s]*" r"[a-zA-Z][a-zA-Z0-9\s]*$" + r"^(?:[a-zA-Z]+)\:?\+?[1-9]\d{1,14}$|" ), ) } diff --git a/requirements_all.txt b/requirements_all.txt index 9a7ff3bb14bb33..149ad6234ebe38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1909,7 +1909,7 @@ tuyaha==0.0.4 twentemilieu==0.1.0 # homeassistant.components.twilio -twilio==6.19.1 +twilio==6.32.0 # homeassistant.components.upcloud upcloud-api==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 047ed96bf10cfd..71cbac4de0f16b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -596,7 +596,7 @@ transmissionrpc==0.11 twentemilieu==0.1.0 # homeassistant.components.twilio -twilio==6.19.1 +twilio==6.32.0 # homeassistant.components.uvc uvcclient==0.11.0 From 09b4f65515af4c6111f9a725050acad75f6e6445 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Tue, 22 Oct 2019 11:22:42 -0400 Subject: [PATCH 1185/3953] Add modelnumber for ecobee4 (#28107) --- homeassistant/components/ecobee/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index a6141d874f1647..5022cb71903a1e 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -19,6 +19,7 @@ "corSmart": "Carrier/Bryant Cor", "nikeSmart": "ecobee3 lite Smart", "nikeEms": "ecobee3 lite EMS", + "apolloSmart": "ecobee4 Smart", } ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] From 37bf577284daea88ff84196ae86ee8c03464e7f2 Mon Sep 17 00:00:00 2001 From: Pascal Roeleven Date: Tue, 22 Oct 2019 17:23:39 +0200 Subject: [PATCH 1186/3953] Add support for more Orange Pi devices (#28109) * Bump OPi.GPIO to 0.4.0 * Move imports to top-level --- .../components/orangepi_gpio/__init__.py | 26 ++---------- .../components/orangepi_gpio/const.py | 40 ++++++++++++++++++- .../components/orangepi_gpio/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/orangepi_gpio/__init__.py b/homeassistant/components/orangepi_gpio/__init__.py index 7b686399d0fedc..71d8d65d8b8a5f 100644 --- a/homeassistant/components/orangepi_gpio/__init__.py +++ b/homeassistant/components/orangepi_gpio/__init__.py @@ -6,6 +6,8 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from .const import PIN_MODES + _LOGGER = logging.getLogger(__name__) DOMAIN = "orangepi_gpio" @@ -23,33 +25,13 @@ def prepare_gpio(event): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) - return True def setup_mode(mode): """Set GPIO pin mode.""" - _LOGGER.debug("Setting GPIO pin mode as %s", mode) - if mode == "pc": - import orangepi.pc - - GPIO.setmode(orangepi.pc.BOARD) - elif mode == "zeroplus": - import orangepi.zeroplus - - GPIO.setmode(orangepi.zeroplus.BOARD) - elif mode == "zeroplus2": - import orangepi.zeroplus - - GPIO.setmode(orangepi.zeroplus2.BOARD) - elif mode == "duo": - import nanopi.duo - - GPIO.setmode(nanopi.duo.BOARD) - elif mode == "neocore2": - import nanopi.neocore2 - - GPIO.setmode(nanopi.neocore2.BOARD) + _LOGGER.debug("Setting GPIO pin mode as %s", PIN_MODES[mode]) + GPIO.setmode(PIN_MODES[mode]) def setup_input(port): diff --git a/homeassistant/components/orangepi_gpio/const.py b/homeassistant/components/orangepi_gpio/const.py index 928d75a1f98c22..47ddf5b708568a 100644 --- a/homeassistant/components/orangepi_gpio/const.py +++ b/homeassistant/components/orangepi_gpio/const.py @@ -1,5 +1,23 @@ """Constants for Orange Pi GPIO.""" +from nanopi import duo, neocore2 +from orangepi import ( + lite, + lite2, + one, + oneplus, + pc, + pc2, + pcplus, + pi3, + plus2e, + prime, + r1, + winplus, + zero, + zeroplus, + zeroplus2, +) import voluptuous as vol from homeassistant.helpers import config_validation as cv @@ -8,12 +26,30 @@ CONF_PIN_MODE = "pin_mode" CONF_PORTS = "ports" DEFAULT_INVERT_LOGIC = False -PIN_MODES = ["pc", "zeroplus", "zeroplus2", "deo", "neocore2"] +PIN_MODES = { + "lite": lite.BOARD, + "lite2": lite2.BOARD, + "one": one.BOARD, + "oneplus": oneplus.BOARD, + "pc": pc.BOARD, + "pc2": pc2.BOARD, + "pcplus": pcplus.BOARD, + "pi3": pi3.BOARD, + "plus2e": plus2e.BOARD, + "prime": prime.BOARD, + "r1": r1.BOARD, + "winplus": winplus.BOARD, + "zero": zero.BOARD, + "zeroplus": zeroplus.BOARD, + "zeroplus2": zeroplus2.BOARD, + "duo": duo.BOARD, + "neocore2": neocore2.BOARD, +} _SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) PORT_SCHEMA = { vol.Required(CONF_PORTS): _SENSORS_SCHEMA, - vol.Required(CONF_PIN_MODE): vol.In(PIN_MODES), + vol.Required(CONF_PIN_MODE): vol.In(PIN_MODES.keys()), vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, } diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json index 51bca8fbbbe85c..52c8f8f509f8fd 100644 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -3,7 +3,7 @@ "name": "Orangepi GPIO", "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", "requirements": [ - "OPi.GPIO==0.3.6" + "OPi.GPIO==0.4.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 149ad6234ebe38..884155f3b4c6e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -41,7 +41,7 @@ HAP-python==2.6.0 Mastodon.py==1.5.0 # homeassistant.components.orangepi_gpio -OPi.GPIO==0.3.6 +OPi.GPIO==0.4.0 # homeassistant.components.essent PyEssent==0.13 From c2c9213e9b35d856a85543c5f41f996b1d61a5fc Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 22 Oct 2019 17:28:06 +0200 Subject: [PATCH 1187/3953] Add improved scene support to the counter integration (#28103) * Add improved scene support to the counter integration * Remove comment --- .../components/counter/reproduce_state.py | 71 +++++++++++++++++++ .../counter/test_reproduce_state.py | 71 +++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 homeassistant/components/counter/reproduce_state.py create mode 100644 tests/components/counter/test_reproduce_state.py diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py new file mode 100644 index 00000000000000..ac5045d68e762e --- /dev/null +++ b/homeassistant/components/counter/reproduce_state.py @@ -0,0 +1,71 @@ +"""Reproduce an Counter state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_INITIAL, + ATTR_MAXIMUM, + ATTR_MINIMUM, + ATTR_STEP, + VALUE, + DOMAIN, + SERVICE_CONFIGURE, +) + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 not state.state.isdigit(): + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if ( + cur_state.state == state.state + and cur_state.attributes.get(ATTR_INITIAL) == state.attributes.get(ATTR_INITIAL) + and cur_state.attributes.get(ATTR_MAXIMUM) == state.attributes.get(ATTR_MAXIMUM) + and cur_state.attributes.get(ATTR_MINIMUM) == state.attributes.get(ATTR_MINIMUM) + and cur_state.attributes.get(ATTR_STEP) == state.attributes.get(ATTR_STEP) + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id, VALUE: state.state} + service = SERVICE_CONFIGURE + if ATTR_INITIAL in state.attributes: + service_data[ATTR_INITIAL] = state.attributes[ATTR_INITIAL] + if ATTR_MAXIMUM in state.attributes: + service_data[ATTR_MAXIMUM] = state.attributes[ATTR_MAXIMUM] + if ATTR_MINIMUM in state.attributes: + service_data[ATTR_MINIMUM] = state.attributes[ATTR_MINIMUM] + if ATTR_STEP in state.attributes: + service_data[ATTR_STEP] = state.attributes[ATTR_STEP] + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Counter states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/counter/test_reproduce_state.py b/tests/components/counter/test_reproduce_state.py new file mode 100644 index 00000000000000..aa2c5ddbd9a00d --- /dev/null +++ b/tests/components/counter/test_reproduce_state.py @@ -0,0 +1,71 @@ +"""Test reproduce state for Counter.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Counter states.""" + hass.states.async_set("counter.entity", "5", {}) + hass.states.async_set( + "counter.entity_attr", + "8", + {"initial": 12, "minimum": 5, "maximum": 15, "step": 3}, + ) + + configure_calls = async_mock_service(hass, "counter", "configure") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("counter.entity", "5"), + State( + "counter.entity_attr", + "8", + {"initial": 12, "minimum": 5, "maximum": 15, "step": 3}, + ), + ], + blocking=True, + ) + + assert len(configure_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("counter.entity", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(configure_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("counter.entity", "2"), + State( + "counter.entity_attr", + "7", + {"initial": 10, "minimum": 3, "maximum": 21, "step": 5}, + ), + # Should not raise + State("counter.non_existing", "6"), + ], + blocking=True, + ) + + valid_calls = [ + {"entity_id": "counter.entity", "value": "2"}, + { + "entity_id": "counter.entity_attr", + "value": "7", + "initial": 10, + "minimum": 3, + "maximum": 21, + "step": 5, + }, + ] + assert len(configure_calls) == 2 + for call in configure_calls: + assert call.domain == "counter" + assert call.data in valid_calls + valid_calls.remove(call.data) From 0226b76e0ae40ac8596f2ae1470d60b62faec8a3 Mon Sep 17 00:00:00 2001 From: bastshoes Date: Tue, 22 Oct 2019 18:39:26 +0300 Subject: [PATCH 1188/3953] Add support SQL VACUUM for PostgeSQL (#28106) * Add support SQL VACUUM for PostgeSQL VACUUM PostgreSQL DB if repack is true * Update tests --- homeassistant/components/recorder/purge.py | 4 ++-- tests/components/recorder/test_purge.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 2ac0b38c694cdf..089476245fe672 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -34,8 +34,8 @@ def purge_old_data(instance, purge_days, repack): _LOGGER.debug("Deleted %s events", deleted_rows) # Execute sqlite vacuum command to free up space on disk - if repack and instance.engine.driver == "pysqlite": - _LOGGER.debug("Vacuuming SQLite to free space") + if repack and instance.engine.driver in ("pysqlite", "postgresql"): + _LOGGER.debug("Vacuuming SQL DB to free space") instance.engine.execute("VACUUM") except SQLAlchemyError as err: diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 1c676e203d2b7e..7e06dcd1e5e56e 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -174,5 +174,5 @@ def test_purge_method(self): self.hass.data[DATA_INSTANCE].block_till_done() assert ( mock_logger.debug.mock_calls[3][1][0] - == "Vacuuming SQLite to free space" + == "Vacuuming SQL DB to free space" ) From 04dbe5bc841e1a429873efbd850c35b823ef26ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 16:50:49 +0000 Subject: [PATCH 1189/3953] Move imports in dsmr component (#27974) * Move imports in dsmr component * Review * Fix tests --- homeassistant/components/dsmr/sensor.py | 11 ++++------- tests/components/dsmr/test_sensor.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 82a81118dbdffe..253e8409f1b3a9 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -4,6 +4,9 @@ from functools import partial import logging +from dsmr_parser import obis_references as obis_ref +from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader +import serial import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -52,10 +55,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Suppress logging logging.getLogger("dsmr_parser").setLevel(logging.ERROR) - from dsmr_parser import obis_references as obis_ref - from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader - import serial - dsmr_version = config[CONF_DSMR_VERSION] # Define list of name,obis mappings to generate entities @@ -212,11 +211,9 @@ def icon(self): @property def state(self): """Return the state of sensor, if available, translate if needed.""" - from dsmr_parser import obis_references as obis - value = self.get_dsmr_object_attr("value") - if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: + if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value) try: diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 57dfa183febc51..195345dd489107 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -11,9 +11,11 @@ from unittest.mock import Mock import asynctest +import pytest + from homeassistant.bootstrap import async_setup_component from homeassistant.components.dsmr.sensor import DerivativeDSMREntity -import pytest + from tests.common import assert_setup_component @@ -34,10 +36,11 @@ def connection_factory(*args, **kwargs): # apply the mock to both connection factories monkeypatch.setattr( - "dsmr_parser.clients.protocol.create_dsmr_reader", connection_factory + "homeassistant.components.dsmr.sensor.create_dsmr_reader", connection_factory ) monkeypatch.setattr( - "dsmr_parser.clients.protocol.create_tcp_dsmr_reader", connection_factory + "homeassistant.components.dsmr.sensor.create_tcp_dsmr_reader", + connection_factory, ) return connection_factory, transport, protocol @@ -158,7 +161,8 @@ def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): ) monkeypatch.setattr( - "dsmr_parser.clients.protocol.create_dsmr_reader", first_fail_connection_factory + "homeassistant.components.dsmr.sensor.create_dsmr_reader", + first_fail_connection_factory, ) yield from async_setup_component(hass, "sensor", {"sensor": config}) From 4700d647b01bcc535927713fc5fe5054c2345b7b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Oct 2019 20:40:07 +0200 Subject: [PATCH 1190/3953] Minor tweaks for sensor device automations (#27829) * Minor tweaks for sensor device automations * Change unit_of_measurement to suffix in extra_fields * Address review comment --- .../components/sensor/device_trigger.py | 12 ++----- homeassistant/components/sensor/strings.json | 36 +++++++++---------- .../components/sensor/test_device_trigger.py | 4 +-- 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 7eabc4571610f3..b462124165af17 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -11,7 +11,6 @@ CONF_ENTITY_ID, CONF_FOR, CONF_TYPE, - CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -73,11 +72,6 @@ ), vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), - vol.Optional(CONF_FOR): vol.Any( - vol.All(cv.time_period, cv.positive_timedelta), - cv.template, - cv.template_complex, - ), vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ), @@ -159,12 +153,10 @@ async def async_get_trigger_capabilities(hass, config): "extra_fields": vol.Schema( { vol.Optional( - CONF_ABOVE, - description={CONF_UNIT_OF_MEASUREMENT: unit_of_measurement}, + CONF_ABOVE, description={"suffix": unit_of_measurement} ): vol.Coerce(float), vol.Optional( - CONF_BELOW, - description={CONF_UNIT_OF_MEASUREMENT: unit_of_measurement}, + CONF_BELOW, description={"suffix": unit_of_measurement} ): vol.Coerce(float), vol.Optional(CONF_FOR): cv.positive_time_period_dict, } diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 7df239facde721..a05f57f4584bd5 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} battery level", - "is_humidity": "{entity_name} humidity", - "is_illuminance": "{entity_name} illuminance", - "is_power": "{entity_name} power", - "is_pressure": "{entity_name} pressure", - "is_signal_strength": "{entity_name} signal strength", - "is_temperature": "{entity_name} temperature", - "is_timestamp": "{entity_name} timestamp", - "is_value": "{entity_name} value" + "is_battery_level": "Current {entity_name} battery level", + "is_humidity": "Current {entity_name} humidity", + "is_illuminance": "Current {entity_name} illuminance", + "is_power": "Current {entity_name} power", + "is_pressure": "Current {entity_name} pressure", + "is_signal_strength": "Current {entity_name} signal strength", + "is_temperature": "Current {entity_name} temperature", + "is_timestamp": "Current {entity_name} timestamp", + "is_value": "Current {entity_name} value" }, "trigger_type": { - "battery_level": "{entity_name} battery level", - "humidity": "{entity_name} humidity", - "illuminance": "{entity_name} illuminance", - "power": "{entity_name} power", - "pressure": "{entity_name} pressure", - "signal_strength": "{entity_name} signal strength", - "temperature": "{entity_name} temperature", - "timestamp": "{entity_name} timestamp", - "value": "{entity_name} value" + "battery_level": "{entity_name} battery level changes", + "humidity": "{entity_name} humidity changes", + "illuminance": "{entity_name} illuminance changes", + "power": "{entity_name} power changes", + "pressure": "{entity_name} pressure changes", + "signal_strength": "{entity_name} signal strength changes", + "temperature": "{entity_name} temperature changes", + "timestamp": "{entity_name} timestamp changes", + "value": "{entity_name} value changes" } } } diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 1bc7e5e1ee572f..a21839fcebc292 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -101,13 +101,13 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): expected_capabilities = { "extra_fields": [ { - "description": {"unit_of_measurement": "%"}, + "description": {"suffix": "%"}, "name": "above", "optional": True, "type": "float", }, { - "description": {"unit_of_measurement": "%"}, + "description": {"suffix": "%"}, "name": "below", "optional": True, "type": "float", From adb15286b489fc83ff307e75e2de753b1dfc7dc2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Oct 2019 01:13:34 +0200 Subject: [PATCH 1191/3953] Fix test coverage, reverting top level import ptvsd (#28118) --- homeassistant/components/ptvsd/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ptvsd/__init__.py b/homeassistant/components/ptvsd/__init__.py index f12c8004774848..55cef1405d9269 100644 --- a/homeassistant/components/ptvsd/__init__.py +++ b/homeassistant/components/ptvsd/__init__.py @@ -8,7 +8,6 @@ import logging from threading import Thread -import ptvsd import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT @@ -37,6 +36,12 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up ptvsd debugger.""" + + # This is a local import, since importing this at the top, will cause + # ptvsd to hook into `sys.settrace`. So does `coverage` to generate + # coverage, resulting in a battle and incomplete code test coverage. + import ptvsd # pylint: disable=import-outside-toplevel + conf = config[DOMAIN] host = conf[CONF_HOST] port = conf[CONF_PORT] From dc3aa43f739e321eb1cb6f912489f28365bf6826 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 23 Oct 2019 00:32:15 +0000 Subject: [PATCH 1192/3953] [ci skip] Translation update --- .../components/abode/.translations/es.json | 17 +++++++++ .../components/adguard/.translations/es.json | 2 + .../alarm_control_panel/.translations/es.json | 4 +- .../components/axis/.translations/es.json | 1 + .../binary_sensor/.translations/fr.json | 4 +- .../components/deconz/.translations/es.json | 1 + .../components/glances/.translations/de.json | 31 ++++++++++++++++ .../components/glances/.translations/es.json | 37 +++++++++++++++++++ .../components/glances/.translations/no.json | 14 +++++-- .../glances/.translations/zh-Hant.json | 37 +++++++++++++++++++ .../components/light/.translations/fr.json | 2 +- .../components/lock/.translations/es.json | 5 +++ .../opentherm_gw/.translations/de.json | 10 +++++ .../opentherm_gw/.translations/es.json | 11 ++++++ .../components/sensor/.translations/en.json | 36 +++++++++--------- .../components/sensor/.translations/fr.json | 16 ++++---- .../components/soma/.translations/es.json | 4 +- 17 files changed, 196 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/glances/.translations/de.json create mode 100644 homeassistant/components/glances/.translations/es.json create mode 100644 homeassistant/components/glances/.translations/zh-Hant.json diff --git a/homeassistant/components/abode/.translations/es.json b/homeassistant/components/abode/.translations/es.json index e0c1b6d6a7d607..908e8f0fbc3f36 100644 --- a/homeassistant/components/abode/.translations/es.json +++ b/homeassistant/components/abode/.translations/es.json @@ -1,5 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode." + }, + "error": { + "connection_error": "No se puede conectar a Abode.", + "identifier_exists": "Cuenta ya registrada.", + "invalid_credentials": "Credenciales inv\u00e1lidas." + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Rellene la informaci\u00f3n de acceso Abode" + } + }, "title": "Abode" } } \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/es.json b/homeassistant/components/adguard/.translations/es.json index 5886d8e5c5b6fc..c6946ab61201cd 100644 --- a/homeassistant/components/adguard/.translations/es.json +++ b/homeassistant/components/adguard/.translations/es.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, usted tiene {current_version}. Por favor, actualice su complemento Hass.io AdGuard Home.", + "adguard_home_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, usted tiene {current_version}.", "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.", "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, diff --git a/homeassistant/components/alarm_control_panel/.translations/es.json b/homeassistant/components/alarm_control_panel/.translations/es.json index a704080a2b494b..273efeeaba52f0 100644 --- a/homeassistant/components/alarm_control_panel/.translations/es.json +++ b/homeassistant/components/alarm_control_panel/.translations/es.json @@ -2,8 +2,8 @@ "device_automation": { "action_type": { "arm_away": "Armar {entity_name} exterior", - "arm_home": "Armar {entity_name} casa", - "arm_night": "Armar {entity_name}", + "arm_home": "Armar {entity_name} modo casa", + "arm_night": "Armar {entity_name} por la noche", "disarm": "Desarmar {entity_name}", "trigger": "Lanzar {entity_name}" } diff --git a/homeassistant/components/axis/.translations/es.json b/homeassistant/components/axis/.translations/es.json index d29481a3be96f8..3f7db674fdf44b 100644 --- a/homeassistant/components/axis/.translations/es.json +++ b/homeassistant/components/axis/.translations/es.json @@ -12,6 +12,7 @@ "device_unavailable": "El dispositivo no est\u00e1 disponible", "faulty_credentials": "Credenciales de usuario incorrectas" }, + "flow_title": "Dispositivo Axis: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json index 9a04ea17747f95..4d9bcefbe660d1 100644 --- a/homeassistant/components/binary_sensor/.translations/fr.json +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -9,7 +9,7 @@ "is_light": "{entity_name} d\u00e9tecte de la lumi\u00e8re", "is_locked": "{entity_name} est verrouill\u00e9", "is_moist": "{entity_name} est humide", - "is_motion": "{entity_name} d\u00e9tecte un mouvement", + "is_motion": "{entity_name} d\u00e9tecte du mouvement", "is_moving": "{entity_name} se d\u00e9place", "is_no_gas": "{entity_name} ne d\u00e9tecte pas de gaz", "is_no_light": "{entity_name} ne d\u00e9tecte pas de lumi\u00e8re", @@ -85,7 +85,7 @@ "problem": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter un probl\u00e8me", "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", - "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} activ\u00e9", "unsafe": "{entity_name} est devenu dangereux", "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 04a08d185b30fb..d4f8de9f282ae7 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -11,6 +11,7 @@ "error": { "no_key": "No se pudo obtener una clave API" }, + "flow_title": "pasarela deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/glances/.translations/de.json b/homeassistant/components/glances/.translations/de.json new file mode 100644 index 00000000000000..04fed0fdc49ec8 --- /dev/null +++ b/homeassistant/components/glances/.translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Host ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "wrong_version": "Version nicht unterst\u00fctzt (nur 2 oder 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/es.json b/homeassistant/components/glances/.translations/es.json new file mode 100644 index 00000000000000..1b6b0335192a67 --- /dev/null +++ b/homeassistant/components/glances/.translations/es.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se puede conectar al host", + "wrong_version": "Versi\u00f3n no soportada (s\u00f3lo 2 o 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "ssl": "Utilice SSL/TLS para conectarse al sistema Glances", + "username": "Nombre de usuario", + "verify_ssl": "Verificar la certificaci\u00f3n del sistema", + "version": "Versi\u00f3n API Glances (2 o 3)" + }, + "title": "Configurar Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + }, + "description": "Configurar opciones para Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/no.json b/homeassistant/components/glances/.translations/no.json index 7d742cad2626aa..7cf28cc34d0485 100644 --- a/homeassistant/components/glances/.translations/no.json +++ b/homeassistant/components/glances/.translations/no.json @@ -14,17 +14,23 @@ "name": "Navn", "password": "Passord", "port": "Port", - "username": "Brukernavn" - } + "ssl": "Bruk SSL / TLS for \u00e5 koble til Glances-systemet", + "username": "Brukernavn", + "verify_ssl": "Bekreft sertifiseringen av systemet", + "version": "Glances API-versjon (2 eller 3)" + }, + "title": "Oppsett av Glances" } - } + }, + "title": "Glances" }, "options": { "step": { "init": { "data": { "scan_interval": "Oppdater frekvens" - } + }, + "description": "Konfigurasjonsalternativer for Glances" } } } diff --git a/homeassistant/components/glances/.translations/zh-Hant.json b/homeassistant/components/glances/.translations/zh-Hant.json new file mode 100644 index 00000000000000..12ba7670355d84 --- /dev/null +++ b/homeassistant/components/glances/.translations/zh-Hant.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef", + "wrong_version": "\u7248\u672c\u4e0d\u652f\u63f4\uff08\u50c5 2 \u6216 3\uff09" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u4f7f\u7528 SSL/TLS \u9023\u7dda\u81f3 Glances \u7cfb\u7d71", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u9a57\u8b49\u7cfb\u7d71\u8a8d\u8b49", + "version": "Glances API \u7248\u672c\uff082 \u6216 3\uff09" + }, + "title": "\u8a2d\u5b9a Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387" + }, + "description": "Glances \u8a2d\u5b9a\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json index fd30e9317180ec..4a1dc82bbd6249 100644 --- a/homeassistant/components/light/.translations/fr.json +++ b/homeassistant/components/light/.translations/fr.json @@ -10,7 +10,7 @@ "is_on": "{entity_name} est allum\u00e9" }, "trigger_type": { - "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} activ\u00e9" } } diff --git a/homeassistant/components/lock/.translations/es.json b/homeassistant/components/lock/.translations/es.json index 5c23c270f61c1c..c6ef789e9cba65 100644 --- a/homeassistant/components/lock/.translations/es.json +++ b/homeassistant/components/lock/.translations/es.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Bloquear {entity_name}", + "open": "Abrir {entity_name}", + "unlock": "Desbloquear {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} est\u00e1 bloqueado", "is_unlocked": "{entity_name} est\u00e1 desbloqueado" diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json index 0957e233116468..3b18aa71b6ca39 100644 --- a/homeassistant/components/opentherm_gw/.translations/de.json +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -19,5 +19,15 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Boden-Temperatur", + "precision": "Genauigkeit" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/es.json b/homeassistant/components/opentherm_gw/.translations/es.json index 8ad9d89b07a57d..bb8a8b20f367d9 100644 --- a/homeassistant/components/opentherm_gw/.translations/es.json +++ b/homeassistant/components/opentherm_gw/.translations/es.json @@ -19,5 +19,16 @@ } }, "title": "Gateway OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura del suelo", + "precision": "Precisi\u00f3n" + }, + "description": "Opciones para OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/en.json b/homeassistant/components/sensor/.translations/en.json index 7bbbe660feb94c..07411b885b8130 100644 --- a/homeassistant/components/sensor/.translations/en.json +++ b/homeassistant/components/sensor/.translations/en.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} battery level", - "is_humidity": "{entity_name} humidity", - "is_illuminance": "{entity_name} illuminance", - "is_power": "{entity_name} power", - "is_pressure": "{entity_name} pressure", - "is_signal_strength": "{entity_name} signal strength", - "is_temperature": "{entity_name} temperature", - "is_timestamp": "{entity_name} timestamp", - "is_value": "{entity_name} value" + "is_battery_level": "Current {entity_name} battery level", + "is_humidity": "Current {entity_name} humidity", + "is_illuminance": "Current {entity_name} illuminance", + "is_power": "Current {entity_name} power", + "is_pressure": "Current {entity_name} pressure", + "is_signal_strength": "Current {entity_name} signal strength", + "is_temperature": "Current {entity_name} temperature", + "is_timestamp": "Current {entity_name} timestamp", + "is_value": "Current {entity_name} value" }, "trigger_type": { - "battery_level": "{entity_name} battery level", - "humidity": "{entity_name} humidity", - "illuminance": "{entity_name} illuminance", - "power": "{entity_name} power", - "pressure": "{entity_name} pressure", - "signal_strength": "{entity_name} signal strength", - "temperature": "{entity_name} temperature", - "timestamp": "{entity_name} timestamp", - "value": "{entity_name} value" + "battery_level": "{entity_name} battery level changes", + "humidity": "{entity_name} humidity changes", + "illuminance": "{entity_name} illuminance changes", + "power": "{entity_name} power changes", + "pressure": "{entity_name} pressure changes", + "signal_strength": "{entity_name} signal strength changes", + "temperature": "{entity_name} temperature changes", + "timestamp": "{entity_name} timestamp changes", + "value": "{entity_name} value changes" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json index 676a5aa413fe9c..56725a59e2174d 100644 --- a/homeassistant/components/sensor/.translations/fr.json +++ b/homeassistant/components/sensor/.translations/fr.json @@ -1,24 +1,24 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} niveau batterie", - "is_humidity": "{entity_name} humidit\u00e9", - "is_illuminance": "{entity_name} \u00e9clairement", + "is_battery_level": "Le niveau de la batterie de {entity_name}", + "is_humidity": "L'humidit\u00e9 de {entity_name}", + "is_illuminance": "L'\u00e9clairement de {entity_name}", "is_power": "{entity_name} puissance", "is_pressure": "{entity_name} pression", "is_signal_strength": "{entity_name} force du signal", - "is_temperature": "{entity_name} temp\u00e9rature", + "is_temperature": "La temp\u00e9rature de {entity_name}", "is_timestamp": "{entity_name} horodatage", "is_value": "{entity_name} valeur" }, "trigger_type": { - "battery_level": "{entity_name} niveau batterie", - "humidity": "{entity_name} humidit\u00e9", - "illuminance": "{entity_name} \u00e9clairement", + "battery_level": "Le niveau de la batterie de {entity_name}", + "humidity": "L'humidit\u00e9 de {entity_name}", + "illuminance": "L'\u00e9clairement de {entity_name}", "power": "{entity_name} puissance", "pressure": "{entity_name} pression", "signal_strength": "{entity_name} force du signal", - "temperature": "{entity_name} temp\u00e9rature", + "temperature": "La temp\u00e9rature de {entity_name}", "timestamp": "{entity_name} horodatage", "value": "{entity_name} valeur" } diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json index b539130ea59d29..8692262270455c 100644 --- a/homeassistant/components/soma/.translations/es.json +++ b/homeassistant/components/soma/.translations/es.json @@ -13,7 +13,9 @@ "data": { "host": "Host", "port": "Puerto" - } + }, + "description": "Por favor, introduzca los ajustes de conexi\u00f3n de SOMA Connect.", + "title": "SOMA Connect" } }, "title": "Soma" From da094e09fa270b883abbe1f67256e51254f9dce3 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 23 Oct 2019 01:01:03 -0400 Subject: [PATCH 1193/3953] Implement ToggleController, RangeController, and ModeController in alexa (#27302) * Implement AlexaToggleController, AlexaRangeController, and AlexaModeController interfaces. * Implement AlexaToggleController, AlexaRangeController, and AlexaModeController interfaces. * Unkerfuffled comments to please the pydocstyle gods. * Unkerfuffled comments in Tests to please the pydocstyle gods. * Added additional test for more coverage. * Removed OSCILLATING property check from from ModeController. * Added capability report tests for ModeController, ToggleController, RangeController, PowerLevelController. * Update homeassistant/components/alexa/capabilities.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/alexa/capabilities.py Co-Authored-By: Paulus Schoutsen * Corrected mis-spelling of AlexaCapability class. * Changed instance from method to property in AlexaCapability class. * Refactored to add {entity.domain}.{entity.attribute} to the instance name. * Improved type handling for configuration object. Added additional test for configuration object. * Added Tests for unsupported domains for ModeController and RangeController * Made changes to improve future scaling for other domains. * Split fan range to speed maps into multiple constants. --- .../components/alexa/capabilities.py | 375 ++++++++++++++++-- homeassistant/components/alexa/const.py | 172 +++++++- homeassistant/components/alexa/entities.py | 16 + homeassistant/components/alexa/errors.py | 14 + homeassistant/components/alexa/handlers.py | 194 ++++++++- homeassistant/components/alexa/messages.py | 5 +- tests/components/alexa/__init__.py | 11 +- tests/components/alexa/test_capabilities.py | 69 +++- tests/components/alexa/test_smart_home.py | 339 +++++++++++++++- tests/components/alexa/test_state_report.py | 48 +++ 10 files changed, 1190 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 7be3188fea1fea..f4d930266495ea 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -23,19 +23,20 @@ import homeassistant.util.dt as dt_util from .const import ( + Catalog, API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, DATE_FORMAT, PERCENTAGE_FAN_MAP, + RANGE_FAN_MAP, ) from .errors import UnsupportedProperty - _LOGGER = logging.getLogger(__name__) -class AlexaCapibility: +class AlexaCapability: """Base class for Alexa capability interfaces. The Smart Home Skills API defines a number of "capability interfaces", @@ -45,9 +46,10 @@ class AlexaCapibility: https://developer.amazon.com/docs/device-apis/message-guide.html """ - def __init__(self, entity): - """Initialize an Alexa capibility.""" + def __init__(self, entity, instance=None): + """Initialize an Alexa capability.""" self.entity = entity + self.instance = instance def name(self): """Return the Alexa API name of this interface.""" @@ -68,6 +70,11 @@ def properties_retrievable(): """Return True if properties can be retrieved.""" return False + @staticmethod + def properties_non_controllable(): + """Return True if non controllable.""" + return None + @staticmethod def get_property(name): """Read and return a property. @@ -84,9 +91,14 @@ def supports_deactivation(): """Applicable only to scenes.""" return None + @staticmethod + def capability_resources(): + """Applicable to ToggleController, RangeController, and ModeController interfaces.""" + return [] + @staticmethod def configuration(): - """Applicable only to security control panel.""" + """Return the Configuration object.""" return [] def serialize_discovery(self): @@ -102,15 +114,29 @@ def serialize_discovery(self): }, } + # pylint: disable=assignment-from-none + non_controllable = self.properties_non_controllable() + if non_controllable is not None: + result["properties"]["nonControllable"] = non_controllable + # pylint: disable=assignment-from-none supports_deactivation = self.supports_deactivation() if supports_deactivation is not None: result["supportsDeactivation"] = supports_deactivation + capability_resources = self.serialize_capability_resources() + if capability_resources: + result["capabilityResources"] = capability_resources + configuration = self.configuration() if configuration: result["configuration"] = configuration + # pylint: disable=assignment-from-none + instance = self.instance + if instance is not None: + result["instance"] = instance + return result def serialize_properties(self): @@ -120,16 +146,51 @@ def serialize_properties(self): # pylint: disable=assignment-from-no-return prop_value = self.get_property(prop_name) if prop_value is not None: - yield { + result = { "name": prop_name, "namespace": self.name(), "value": prop_value, "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT), "uncertaintyInMilliseconds": 0, } + instance = self.instance + if instance is not None: + result["instance"] = instance + yield result + + def serialize_capability_resources(self): + """Return capabilityResources friendlyNames serialized for an API response.""" + resources = self.capability_resources() + if resources: + return {"friendlyNames": self.serialize_friendly_names(resources)} + + return None -class AlexaEndpointHealth(AlexaCapibility): + @staticmethod + def serialize_friendly_names(resources): + """Return capabilityResources, ModeResources, or presetResources friendlyNames serialized for an API response.""" + friendly_names = [] + for resource in resources: + if resource["type"] == Catalog.LABEL_ASSET: + friendly_names.append( + { + "@type": Catalog.LABEL_ASSET, + "value": {"assetId": resource["value"]}, + } + ) + else: + friendly_names.append( + { + "@type": Catalog.LABEL_TEXT, + "value": {"text": resource["value"], "locale": "en-US"}, + } + ) + + return friendly_names + + +class AlexaEndpointHealth(AlexaCapability): """Implements Alexa.EndpointHealth. https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it @@ -166,7 +227,7 @@ def get_property(self, name): return {"value": "OK"} -class AlexaPowerController(AlexaCapibility): +class AlexaPowerController(AlexaCapability): """Implements Alexa.PowerController. https://developer.amazon.com/docs/device-apis/alexa-powercontroller.html @@ -202,7 +263,7 @@ def get_property(self, name): return "ON" if is_on else "OFF" -class AlexaLockController(AlexaCapibility): +class AlexaLockController(AlexaCapability): """Implements Alexa.LockController. https://developer.amazon.com/docs/device-apis/alexa-lockcontroller.html @@ -236,7 +297,7 @@ def get_property(self, name): return "JAMMED" -class AlexaSceneController(AlexaCapibility): +class AlexaSceneController(AlexaCapability): """Implements Alexa.SceneController. https://developer.amazon.com/docs/device-apis/alexa-scenecontroller.html @@ -252,7 +313,7 @@ def name(self): return "Alexa.SceneController" -class AlexaBrightnessController(AlexaCapibility): +class AlexaBrightnessController(AlexaCapability): """Implements Alexa.BrightnessController. https://developer.amazon.com/docs/device-apis/alexa-brightnesscontroller.html @@ -283,7 +344,7 @@ def get_property(self, name): return 0 -class AlexaColorController(AlexaCapibility): +class AlexaColorController(AlexaCapability): """Implements Alexa.ColorController. https://developer.amazon.com/docs/device-apis/alexa-colorcontroller.html @@ -315,7 +376,7 @@ def get_property(self, name): } -class AlexaColorTemperatureController(AlexaCapibility): +class AlexaColorTemperatureController(AlexaCapability): """Implements Alexa.ColorTemperatureController. https://developer.amazon.com/docs/device-apis/alexa-colortemperaturecontroller.html @@ -344,7 +405,7 @@ def get_property(self, name): return None -class AlexaPercentageController(AlexaCapibility): +class AlexaPercentageController(AlexaCapability): """Implements Alexa.PercentageController. https://developer.amazon.com/docs/device-apis/alexa-percentagecontroller.html @@ -378,7 +439,7 @@ def get_property(self, name): return 0 -class AlexaSpeaker(AlexaCapibility): +class AlexaSpeaker(AlexaCapability): """Implements Alexa.Speaker. https://developer.amazon.com/docs/device-apis/alexa-speaker.html @@ -389,7 +450,7 @@ def name(self): return "Alexa.Speaker" -class AlexaStepSpeaker(AlexaCapibility): +class AlexaStepSpeaker(AlexaCapability): """Implements Alexa.StepSpeaker. https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html @@ -400,7 +461,7 @@ def name(self): return "Alexa.StepSpeaker" -class AlexaPlaybackController(AlexaCapibility): +class AlexaPlaybackController(AlexaCapability): """Implements Alexa.PlaybackController. https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html @@ -411,7 +472,7 @@ def name(self): return "Alexa.PlaybackController" -class AlexaInputController(AlexaCapibility): +class AlexaInputController(AlexaCapability): """Implements Alexa.InputController. https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html @@ -422,7 +483,7 @@ def name(self): return "Alexa.InputController" -class AlexaTemperatureSensor(AlexaCapibility): +class AlexaTemperatureSensor(AlexaCapability): """Implements Alexa.TemperatureSensor. https://developer.amazon.com/docs/device-apis/alexa-temperaturesensor.html @@ -472,7 +533,7 @@ def get_property(self, name): return {"value": temp, "scale": API_TEMP_UNITS[unit]} -class AlexaContactSensor(AlexaCapibility): +class AlexaContactSensor(AlexaCapability): """Implements Alexa.ContactSensor. The Alexa.ContactSensor interface describes the properties and events used @@ -514,7 +575,7 @@ def get_property(self, name): return "NOT_DETECTED" -class AlexaMotionSensor(AlexaCapibility): +class AlexaMotionSensor(AlexaCapability): """Implements Alexa.MotionSensor. https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html @@ -551,7 +612,7 @@ def get_property(self, name): return "NOT_DETECTED" -class AlexaThermostatController(AlexaCapibility): +class AlexaThermostatController(AlexaCapability): """Implements Alexa.ThermostatController. https://developer.amazon.com/docs/device-apis/alexa-thermostatcontroller.html @@ -631,7 +692,7 @@ def get_property(self, name): return {"value": temp, "scale": API_TEMP_UNITS[unit]} -class AlexaPowerLevelController(AlexaCapibility): +class AlexaPowerLevelController(AlexaCapability): """Implements Alexa.PowerLevelController. https://developer.amazon.com/docs/device-apis/alexa-powerlevelcontroller.html @@ -666,7 +727,7 @@ def get_property(self, name): return None -class AlexaSecurityPanelController(AlexaCapibility): +class AlexaSecurityPanelController(AlexaCapability): """Implements Alexa.SecurityPanelController. https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html @@ -710,9 +771,271 @@ def get_property(self, name): return "DISARMED" def configuration(self): - """Return supported authorization types.""" + """Return configuration object with supported authorization types.""" code_format = self.entity.attributes.get(ATTR_CODE_FORMAT) if code_format == FORMAT_NUMBER: return {"supportedAuthorizationTypes": [{"type": "FOUR_DIGIT_PIN"}]} - return [] + return None + + +class AlexaModeController(AlexaCapability): + """Implements Alexa.ModeController. + + https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html + """ + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ModeController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "mode"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + + def get_property(self, name): + """Read and return a property.""" + if name != "mode": + raise UnsupportedProperty(name) + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + return self.entity.attributes.get(fan.ATTR_DIRECTION) + + return None + + def configuration(self): + """Return configuration with modeResources.""" + return self.serialize_mode_resources() + + def capability_resources(self): + """Return capabilityResources object.""" + capability_resources = [] + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + capability_resources = [ + {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION} + ] + + return capability_resources + + def mode_resources(self): + """Return modeResources object.""" + mode_resources = None + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + mode_resources = { + "ordered": False, + "resources": [ + { + "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", + "friendly_names": [ + {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_FORWARD} + ], + }, + { + "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", + "friendly_names": [ + {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_REVERSE} + ], + }, + ], + } + + return mode_resources + + def serialize_mode_resources(self): + """Return ModeResources, friendlyNames serialized for an API response.""" + mode_resources = [] + resources = self.mode_resources() + ordered = resources["ordered"] + for resource in resources["resources"]: + mode_value = resource["value"] + friendly_names = resource["friendly_names"] + result = { + "value": mode_value, + "modeResources": { + "friendlyNames": self.serialize_friendly_names(friendly_names) + }, + } + mode_resources.append(result) + + return {"ordered": ordered, "supportedModes": mode_resources} + + +class AlexaRangeController(AlexaCapability): + """Implements Alexa.RangeController. + + https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html + """ + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.RangeController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "rangeValue"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "rangeValue": + raise UnsupportedProperty(name) + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + speed = self.entity.attributes.get(fan.ATTR_SPEED) + return RANGE_FAN_MAP.get(speed, 0) + + return None + + def configuration(self): + """Return configuration with presetResources.""" + return self.serialize_preset_resources() + + def capability_resources(self): + """Return capabilityResources object.""" + capability_resources = [] + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + return [{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_FANSPEED}] + + return capability_resources + + def preset_resources(self): + """Return presetResources object.""" + preset_resources = [] + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + preset_resources = { + "minimumValue": 1, + "maximumValue": 3, + "precision": 1, + "presets": [ + { + "rangeValue": 1, + "names": [ + { + "type": Catalog.LABEL_ASSET, + "value": Catalog.VALUE_MINIMUM, + }, + {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_LOW}, + ], + }, + { + "rangeValue": 2, + "names": [ + {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_MEDIUM} + ], + }, + { + "rangeValue": 3, + "names": [ + { + "type": Catalog.LABEL_ASSET, + "value": Catalog.VALUE_MAXIMUM, + }, + {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_HIGH}, + ], + }, + ], + } + + return preset_resources + + def serialize_preset_resources(self): + """Return PresetResources, friendlyNames serialized for an API response.""" + preset_resources = [] + resources = self.preset_resources() + for preset in resources["presets"]: + preset_resources.append( + { + "rangeValue": preset["rangeValue"], + "presetResources": { + "friendlyNames": self.serialize_friendly_names(preset["names"]) + }, + } + ) + + return { + "supportedRange": { + "minimumValue": resources["minimumValue"], + "maximumValue": resources["maximumValue"], + "precision": resources["precision"], + }, + "presets": preset_resources, + } + + +class AlexaToggleController(AlexaCapability): + """Implements Alexa.ToggleController. + + https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html + """ + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ToggleController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "toggleState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "toggleState": + raise UnsupportedProperty(name) + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING)) + return "ON" if is_on else "OFF" + + return None + + def capability_resources(self): + """Return capabilityResources object.""" + capability_resources = [] + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + capability_resources = [ + {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_OSCILLATE}, + {"type": Catalog.LABEL_TEXT, "value": "Rotate"}, + {"type": Catalog.LABEL_TEXT, "value": "Rotation"}, + ] + + return capability_resources diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index cd0cb85a0a5e1b..8d1f0ac95a553d 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -5,7 +5,6 @@ from homeassistant.components.climate import const as climate from homeassistant.components import fan - DOMAIN = "alexa" # Flash briefing constants @@ -69,6 +68,20 @@ fan.SPEED_HIGH: 100, } +RANGE_FAN_MAP = { + fan.SPEED_OFF: 0, + fan.SPEED_LOW: 1, + fan.SPEED_MEDIUM: 2, + fan.SPEED_HIGH: 3, +} + +SPEED_FAN_MAP = { + 0: fan.SPEED_OFF, + 1: fan.SPEED_LOW, + 2: fan.SPEED_MEDIUM, + 3: fan.SPEED_HIGH, +} + class Cause: """Possible causes for property changes. @@ -101,3 +114,160 @@ class Cause: # Indicates that the event was caused by a voice interaction with Alexa. # For example a user speaking to their Echo device. VOICE_INTERACTION = "VOICE_INTERACTION" + + +class Catalog: + """The Global Alexa catalog. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog + + You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units. + This catalog is localized into all the languages that Alexa supports. + + You can reference the following catalog of pre-defined friendly names. + Each item in the following list is an asset identifier followed by its supported friendly names. + The first friendly name for each identifier is the one displayed in the Alexa mobile app. + """ + + LABEL_ASSET = "asset" + LABEL_TEXT = "text" + + # Shower + DEVICENAME_SHOWER = "Alexa.DeviceName.Shower" + + # Washer, Washing Machine + DEVICENAME_WASHER = "Alexa.DeviceName.Washer" + + # Router, Internet Router, Network Router, Wifi Router, Net Router + DEVICENAME_ROUTER = "Alexa.DeviceName.Router" + + # Fan, Blower + DEVICENAME_FAN = "Alexa.DeviceName.Fan" + + # Air Purifier, Air Cleaner,Clean Air Machine + DEVICENAME_AIRPURIFIER = "Alexa.DeviceName.AirPurifier" + + # Space Heater, Portable Heater + DEVICENAME_SPACEHEATER = "Alexa.DeviceName.SpaceHeater" + + # Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet + SHOWER_RAINHEAD = "Alexa.Shower.RainHead" + + # Handheld Shower, Shower Wand, Hand Shower + SHOWER_HANDHELD = "Alexa.Shower.HandHeld" + + # Water Temperature, Water Temp, Water Heat + SETTING_WATERTEMPERATURE = "Alexa.Setting.WaterTemperature" + + # Temperature, Temp + SETTING_TEMPERATURE = "Alexa.Setting.Temperature" + + # Wash Cycle, Wash Preset, Wash setting + SETTING_WASHCYCLE = "Alexa.Setting.WashCycle" + + # 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi + SETTING_2GGUESTWIFI = "Alexa.Setting.2GGuestWiFi" + + # 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi + SETTING_5GGUESTWIFI = "Alexa.Setting.5GGuestWiFi" + + # Guest Wi-fi, Guest Network, Guest Net + SETTING_GUESTWIFI = "Alexa.Setting.GuestWiFi" + + # Auto, Automatic, Automatic Mode, Auto Mode + SETTING_AUTO = "Alexa.Setting.Auto" + + # #Night, Night Mode + SETTING_NIGHT = "Alexa.Setting.Night" + + # Quiet, Quiet Mode, Noiseless, Silent + SETTING_QUIET = "Alexa.Setting.Quiet" + + # Oscillate, Swivel, Oscillation, Spin, Back and forth + SETTING_OSCILLATE = "Alexa.Setting.Oscillate" + + # Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity + SETTING_FANSPEED = "Alexa.Setting.FanSpeed" + + # Preset, Setting + SETTING_PRESET = "Alexa.Setting.Preset" + + # Mode + SETTING_MODE = "Alexa.Setting.Mode" + + # Direction + SETTING_DIRECTION = "Alexa.Setting.Direction" + + # Delicates, Delicate + VALUE_DELICATE = "Alexa.Value.Delicate" + + # Quick Wash, Fast Wash, Wash Quickly, Speed Wash + VALUE_QUICKWASH = "Alexa.Value.QuickWash" + + # Maximum, Max + VALUE_MAXIMUM = "Alexa.Value.Maximum" + + # Minimum, Min + VALUE_MINIMUM = "Alexa.Value.Minimum" + + # High + VALUE_HIGH = "Alexa.Value.High" + + # Low + VALUE_LOW = "Alexa.Value.Low" + + # Medium, Mid + VALUE_MEDIUM = "Alexa.Value.Medium" + + +class Unit: + """Alexa Units of Measure. + + https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#units-of-measure + """ + + ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees" + + ANGLE_RADIANS = "Alexa.Unit.Angle.Radians" + + DISTANCE_FEET = "Alexa.Unit.Distance.Feet" + + DISTANCE_INCHES = "Alexa.Unit.Distance.Inches" + + DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers" + + DISTANCE_METERS = "Alexa.Unit.Distance.Meters" + + DISTANCE_MILES = "Alexa.Unit.Distance.Miles" + + DISTANCE_YARDS = "Alexa.Unit.Distance.Yards" + + MASS_GRAMS = "Alexa.Unit.Mass.Grams" + + MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms" + + PERCENT = "Alexa.Unit.Percent" + + TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius" + + TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees" + + TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit" + + TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin" + + VOLUME_CUBICFEET = "Alexa.Unit.Volume.CubicFeet" + + VOLUME_CUBICMETERS = "Alexa.Unit.Volume.CubicMeters" + + VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons" + + VOLUME_LITERS = "Alexa.Unit.Volume.Liters" + + VOLUME_PINTS = "Alexa.Unit.Volume.Pints" + + VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts" + + WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" + + WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 0f07e525fa9316..dd640aed0a62eb 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -40,17 +40,20 @@ AlexaEndpointHealth, AlexaInputController, AlexaLockController, + AlexaModeController, AlexaMotionSensor, AlexaPercentageController, AlexaPlaybackController, AlexaPowerController, AlexaPowerLevelController, + AlexaRangeController, AlexaSceneController, AlexaSecurityPanelController, AlexaSpeaker, AlexaStepSpeaker, AlexaTemperatureSensor, AlexaThermostatController, + AlexaToggleController, ) ENTITY_ADAPTERS = Registry() @@ -348,6 +351,19 @@ def interfaces(self): if supported & fan.SUPPORT_SET_SPEED: yield AlexaPercentageController(self.entity) yield AlexaPowerLevelController(self.entity) + yield AlexaRangeController( + self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}" + ) + + if supported & fan.SUPPORT_OSCILLATE: + yield AlexaToggleController( + self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}" + ) + if supported & fan.SUPPORT_DIRECTION: + yield AlexaModeController( + self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}" + ) + yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 8e32ed9c7ee715..b0600313fc2286 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -97,3 +97,17 @@ class AlexaSecurityPanelAuthorizationRequired(AlexaError): namespace = "Alexa.SecurityPanelController" error_type = "AUTHORIZATION_REQUIRED" + + +class AlexaAlreadyInOperationError(AlexaError): + """Class to represent AlreadyInOperation errors.""" + + namespace = "Alexa" + error_type = "ALREADY_IN_OPERATION" + + +class AlexaInvalidDirectiveError(AlexaError): + """Class to represent InvalidDirective errors.""" + + namespace = "Alexa" + error_type = "INVALID_DIRECTIVE" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 139defe8313037..64feacb92f5da2 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -36,9 +36,18 @@ from homeassistant.util.decorator import Registry from homeassistant.util.temperature import convert as convert_temperature -from .const import API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause +from .const import ( + API_TEMP_UNITS, + API_THERMOSTAT_MODES, + API_THERMOSTAT_PRESETS, + Cause, + PERCENTAGE_FAN_MAP, + RANGE_FAN_MAP, + SPEED_FAN_MAP, +) from .entities import async_get_entities from .errors import ( + AlexaInvalidDirectiveError, AlexaInvalidValueError, AlexaSecurityPanelAuthorizationRequired, AlexaSecurityPanelUnauthorizedError, @@ -356,15 +365,7 @@ async def async_api_adjust_percentage(hass, config, directive, context): if entity.domain == fan.DOMAIN: service = fan.SERVICE_SET_SPEED speed = entity.attributes.get(fan.ATTR_SPEED) - - if speed == "off": - current = 0 - elif speed == "low": - current = 33 - elif speed == "medium": - current = 66 - elif speed == "high": - current = 100 + current = PERCENTAGE_FAN_MAP.get(speed, 100) # set percentage percentage = max(0, percentage_delta + current) @@ -827,20 +828,11 @@ async def async_api_adjust_power_level(hass, config, directive, context): percentage_delta = int(directive.payload["powerLevelDelta"]) service = None data = {ATTR_ENTITY_ID: entity.entity_id} - current = 0 if entity.domain == fan.DOMAIN: service = fan.SERVICE_SET_SPEED speed = entity.attributes.get(fan.ATTR_SPEED) - - if speed == "off": - current = 0 - elif speed == "low": - current = 33 - elif speed == "medium": - current = 66 - else: - current = 100 + current = PERCENTAGE_FAN_MAP.get(speed, 100) # set percentage percentage = max(0, percentage_delta + current) @@ -928,3 +920,165 @@ async def async_api_disarm(hass, config, directive, context): ) return response + + +@HANDLERS.register(("Alexa.ModeController", "SetMode")) +async def async_api_set_mode(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + mode = directive.payload["mode"] + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + mode, direction = mode.split(".") + if direction in [fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD]: + service = fan.SERVICE_SET_DIRECTION + data[fan.ATTR_DIRECTION] = direction + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.ModeController", "AdjustMode")) +async def async_api_adjust_mode(hass, config, directive, context): + """Process a AdjustMode request. + + Requires modeResources to be ordered. + Only modes that are ordered support the adjustMode directive. + """ + entity = directive.entity + instance = directive.instance + domain = entity.domain + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance is None: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + # No modeResources are currently ordered to support this request. + + return directive.response() + + +@HANDLERS.register(("Alexa.ToggleController", "TurnOn")) +async def async_api_toggle_on(hass, config, directive, context): + """Process a toggle on request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + service = fan.SERVICE_OSCILLATE + data[fan.ATTR_OSCILLATING] = True + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.ToggleController", "TurnOff")) +async def async_api_toggle_off(hass, config, directive, context): + """Process a toggle off request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + service = fan.SERVICE_OSCILLATE + data[fan.ATTR_OSCILLATING] = False + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.RangeController", "SetRangeValue")) +async def async_api_set_range(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + range_value = int(directive.payload["rangeValue"]) + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + service = fan.SERVICE_SET_SPEED + speed = SPEED_FAN_MAP.get(range_value, None) + + if not speed: + msg = "Entity does not support value" + raise AlexaInvalidValueError(msg) + + if speed == fan.SPEED_OFF: + service = fan.SERVICE_TURN_OFF + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue")) +async def async_api_adjust_range(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + range_delta = int(directive.payload["rangeValueDelta"]) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + service = fan.SERVICE_SET_SPEED + + # adjust range + current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0) + speed = SPEED_FAN_MAP.get(max(0, range_delta + current_range), fan.SPEED_OFF) + + if speed == fan.SPEED_OFF: + service = fan.SERVICE_TURN_OFF + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py index 3195656ed091cf..cb78f269f8f3fc 100644 --- a/homeassistant/components/alexa/messages.py +++ b/homeassistant/components/alexa/messages.py @@ -28,7 +28,7 @@ def __init__(self, request): self.payload = self._directive[API_PAYLOAD] self.has_endpoint = API_ENDPOINT in self._directive - self.entity = self.entity_id = self.endpoint = None + self.entity = self.entity_id = self.endpoint = self.instance = None def load_entity(self, hass, config): """Set attributes related to the entity for this request. @@ -38,6 +38,7 @@ def load_entity(self, hass, config): - entity - entity_id - endpoint + - instance (when header includes instance property) Behavior when self.has_endpoint is False is undefined. @@ -52,6 +53,8 @@ def load_entity(self, hass, config): raise AlexaInvalidEndpointError(_endpoint_id) self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity) + if "instance" in self._directive[API_HEADER]: + self.instance = self._directive[API_HEADER]["instance"] def response(self, name="Response", namespace="Alexa", payload=None): """Create an API formatted response. diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 48406a11aef1f2..4fd8bf6f2a9671 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -67,13 +67,22 @@ def get_new_request(namespace, name, endpoint=None): async def assert_request_calls_service( - namespace, name, endpoint, service, hass, response_type="Response", payload=None + namespace, + name, + endpoint, + service, + hass, + response_type="Response", + payload=None, + instance=None, ): """Assert an API request calls a hass service.""" context = Context() request = get_new_request(namespace, name, endpoint) if payload: request["directive"]["payload"] = payload + if instance: + request["directive"]["header"]["instance"] = instance domain, service_name = service.split(".") calls = async_mock_service(hass, domain, service_name) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 280a76dc3f039f..be4a2ba48068d9 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -305,7 +305,7 @@ async def test_report_colored_temp_light_state(hass): async def test_report_fan_speed_state(hass): - """Test PercentageController reports fan speed correctly.""" + """Test PercentageController, PowerLevelController, RangeController reports fan speed correctly.""" hass.states.async_set( "fan.off", "off", @@ -333,15 +333,82 @@ async def test_report_fan_speed_state(hass): properties = await reported_properties(hass, "fan.off") properties.assert_equal("Alexa.PercentageController", "percentage", 0) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 0) + properties.assert_equal("Alexa.RangeController", "rangeValue", 0) properties = await reported_properties(hass, "fan.low_speed") properties.assert_equal("Alexa.PercentageController", "percentage", 33) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 33) + properties.assert_equal("Alexa.RangeController", "rangeValue", 1) properties = await reported_properties(hass, "fan.medium_speed") properties.assert_equal("Alexa.PercentageController", "percentage", 66) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 66) + properties.assert_equal("Alexa.RangeController", "rangeValue", 2) properties = await reported_properties(hass, "fan.high_speed") properties.assert_equal("Alexa.PercentageController", "percentage", 100) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 100) + properties.assert_equal("Alexa.RangeController", "rangeValue", 3) + + +async def test_report_fan_oscillating(hass): + """Test ToggleController reports fan oscillating correctly.""" + hass.states.async_set( + "fan.off", + "off", + {"friendly_name": "Off fan", "speed": "off", "supported_features": 3}, + ) + hass.states.async_set( + "fan.low_speed", + "on", + { + "friendly_name": "Low speed fan", + "speed": "low", + "oscillating": True, + "supported_features": 3, + }, + ) + + properties = await reported_properties(hass, "fan.off") + properties.assert_equal("Alexa.ToggleController", "toggleState", "OFF") + + properties = await reported_properties(hass, "fan.low_speed") + properties.assert_equal("Alexa.ToggleController", "toggleState", "ON") + + +async def test_report_fan_direction(hass): + """Test ModeController reports fan direction correctly.""" + hass.states.async_set( + "fan.off", "off", {"friendly_name": "Off fan", "supported_features": 4} + ) + hass.states.async_set( + "fan.reverse", + "on", + { + "friendly_name": "Fan Reverse", + "direction": "reverse", + "supported_features": 4, + }, + ) + hass.states.async_set( + "fan.forward", + "on", + { + "friendly_name": "Fan Forward", + "direction": "forward", + "supported_features": 4, + }, + ) + + properties = await reported_properties(hass, "fan.off") + properties.assert_not_has_property("Alexa.ModeController", "mode") + + properties = await reported_properties(hass, "fan.reverse") + properties.assert_equal("Alexa.ModeController", "mode", "reverse") + + properties = await reported_properties(hass, "fan.forward") + properties.assert_equal("Alexa.ModeController", "mode", "forward") async def test_report_cover_percentage_state(hass): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 186cb850e341a9..5a39036a30f611 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -310,10 +310,14 @@ async def test_fan(hass): assert appliance["endpointId"] == "fan#test_1" assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" - assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, "Alexa.PowerController", "Alexa.EndpointHealth" ) + power_capability = get_capability(capabilities, "Alexa.PowerController") + assert "capabilityResources" not in power_capability + assert "configuration" not in power_capability + async def test_variable_fan(hass): """Test fan discovery. @@ -336,14 +340,33 @@ async def test_variable_fan(hass): assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 2" - assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, "Alexa.PercentageController", "Alexa.PowerController", "Alexa.PowerLevelController", + "Alexa.RangeController", "Alexa.EndpointHealth", ) + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "fan.speed" + + properties = range_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "rangeValue"} in properties["supported"] + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.FanSpeed"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + call, _ = await assert_request_calls_service( "Alexa.PercentageController", "SetPercentage", @@ -377,7 +400,7 @@ async def test_variable_fan(hass): await assert_percentage_changes( hass, - [("high", "-5"), ("high", "5"), ("low", "-80")], + [("high", "-5"), ("medium", "-50"), ("low", "-80")], "Alexa.PowerLevelController", "AdjustPowerLevel", "fan#test_2", @@ -387,6 +410,251 @@ async def test_variable_fan(hass): ) +async def test_oscillating_fan(hass): + """Test oscillating fan discovery.""" + device = ( + "fan.test_3", + "off", + {"friendly_name": "Test fan 3", "supported_features": 3}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "fan#test_3" + assert appliance["displayCategories"][0] == "FAN" + assert appliance["friendlyName"] == "Test fan 3" + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PercentageController", + "Alexa.PowerController", + "Alexa.PowerLevelController", + "Alexa.RangeController", + "Alexa.ToggleController", + "Alexa.EndpointHealth", + ) + + toggle_capability = get_capability(capabilities, "Alexa.ToggleController") + assert toggle_capability is not None + assert toggle_capability["instance"] == "fan.oscillating" + + properties = toggle_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "toggleState"} in properties["supported"] + + capability_resources = toggle_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Oscillate"}, + } in capability_resources["friendlyNames"] + + call, _ = await assert_request_calls_service( + "Alexa.ToggleController", + "TurnOn", + "fan#test_3", + "fan.oscillate", + hass, + payload={}, + instance="fan.oscillating", + ) + assert call.data["oscillating"] + + call, _ = await assert_request_calls_service( + "Alexa.ToggleController", + "TurnOff", + "fan#test_3", + "fan.oscillate", + hass, + payload={}, + instance="fan.oscillating", + ) + assert not call.data["oscillating"] + + +async def test_direction_fan(hass): + """Test direction fan discovery.""" + device = ( + "fan.test_4", + "on", + { + "friendly_name": "Test fan 4", + "supported_features": 5, + "direction": "forward", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "fan#test_4" + assert appliance["displayCategories"][0] == "FAN" + assert appliance["friendlyName"] == "Test fan 4" + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PercentageController", + "Alexa.PowerController", + "Alexa.PowerLevelController", + "Alexa.RangeController", + "Alexa.ModeController", + "Alexa.EndpointHealth", + ) + + mode_capability = get_capability(capabilities, "Alexa.ModeController") + assert mode_capability is not None + assert mode_capability["instance"] == "fan.direction" + + properties = mode_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "mode"} in properties["supported"] + + capability_resources = mode_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Direction"}, + } in capability_resources["friendlyNames"] + + configuration = mode_capability["configuration"] + assert configuration is not None + assert configuration["ordered"] is False + + supported_modes = configuration["supportedModes"] + assert supported_modes is not None + assert { + "value": "direction.forward", + "modeResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "forward", "locale": "en-US"}} + ] + }, + } in supported_modes + assert { + "value": "direction.reverse", + "modeResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "reverse", "locale": "en-US"}} + ] + }, + } in supported_modes + + call, _ = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "fan#test_4", + "fan.set_direction", + hass, + payload={"mode": "direction.reverse"}, + instance="fan.direction", + ) + assert call.data["direction"] == "reverse" + + # Test for AdjustMode instance=None Error coverage + with pytest.raises(AssertionError): + call, _ = await assert_request_calls_service( + "Alexa.ModeController", + "AdjustMode", + "fan#test_4", + "fan.set_direction", + hass, + payload={}, + instance=None, + ) + assert call.data + + +async def test_fan_range(hass): + """Test fan discovery with range controller. + + This one has variable speed. + """ + device = ( + "fan.test_5", + "off", + { + "friendly_name": "Test fan 5", + "supported_features": 1, + "speed_list": ["low", "medium", "high"], + "speed": "medium", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "fan#test_5" + assert appliance["displayCategories"][0] == "FAN" + assert appliance["friendlyName"] == "Test fan 5" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PercentageController", + "Alexa.PowerController", + "Alexa.PowerLevelController", + "Alexa.RangeController", + "Alexa.EndpointHealth", + ) + + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "fan.speed" + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "fan#test_5", + "fan.set_speed", + hass, + payload={"rangeValue": "1"}, + instance="fan.speed", + ) + assert call.data["speed"] == "low" + + await assert_range_changes( + hass, + [("low", "-1"), ("high", "1"), ("medium", "0")], + "Alexa.RangeController", + "AdjustRangeValue", + "fan#test_5", + False, + "fan.set_speed", + "speed", + instance="fan.speed", + ) + + +async def test_fan_range_off(hass): + """Test fan range controller 0 turns_off fan.""" + device = ( + "fan.test_6", + "off", + { + "friendly_name": "Test fan 6", + "supported_features": 1, + "speed_list": ["low", "medium", "high"], + "speed": "high", + }, + ) + await discovery_test(device, hass) + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "fan#test_6", + "fan.turn_off", + hass, + payload={"rangeValue": "0"}, + instance="fan.speed", + ) + assert call.data["speed"] == "off" + + await assert_range_changes( + hass, + [("off", "-3")], + "Alexa.RangeController", + "AdjustRangeValue", + "fan#test_6", + False, + "fan.turn_off", + "speed", + instance="fan.speed", + ) + + async def test_lock(hass): """Test lock discovery.""" device = ("lock.test", "off", {"friendly_name": "Test lock"}) @@ -729,6 +997,33 @@ async def assert_percentage_changes( assert call.data[changed_parameter] == result_volume +async def assert_range_changes( + hass, + adjustments, + namespace, + name, + endpoint, + delta_default, + service, + changed_parameter, + instance, +): + """Assert an API request making range changes works. + + AdjustRangeValue are examples of such requests. + """ + for result_range, adjustment in adjustments: + payload = { + "rangeValueDelta": adjustment, + "rangeValueDeltaDefault": delta_default, + } + + call, _ = await assert_request_calls_service( + namespace, name, endpoint, service, hass, payload=payload, instance=instance + ) + assert call.data[changed_parameter] == result_range + + async def test_temp_sensor(hass): """Test temperature sensor discovery.""" device = ( @@ -1438,3 +1733,41 @@ async def test_alarm_control_panel_code_arm_required(hass): {"friendly_name": "Test Alarm Control Panel 3", "code_arm_required": True}, ) await discovery_test(device, hass, expected_endpoints=0) + + +async def test_range_unsupported_domain(hass): + """Test rangeController with unsupported domain.""" + device = ("switch.test", "on", {"friendly_name": "Test switch"}) + await discovery_test(device, hass) + + context = Context() + request = get_new_request("Alexa.RangeController", "SetRangeValue", "switch#test") + request["directive"]["payload"] = {"rangeValue": "1"} + request["directive"]["header"]["instance"] = "switch.speed" + + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + +async def test_mode_unsupported_domain(hass): + """Test modeController with unsupported domain.""" + device = ("switch.test", "on", {"friendly_name": "Test switch"}) + await discovery_test(device, hass) + + context = Context() + request = get_new_request("Alexa.ModeController", "SetMode", "switch#test") + request["directive"]["payload"] = {"mode": "testMode"} + request["directive"]["header"]["instance"] = "switch.direction" + + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index c05eed2a89baac..310180ef5d09db 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -37,6 +37,54 @@ async def test_report_state(hass, aioclient_mock): assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_contact" +async def test_report_state_instance(hass, aioclient_mock): + """Test proactive state reports with instance.""" + aioclient_mock.post(TEST_URL, text="", status=202) + + hass.states.async_set( + "fan.test_fan", + "off", + { + "friendly_name": "Test fan", + "supported_features": 3, + "speed": "off", + "oscillating": False, + }, + ) + + await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) + + hass.states.async_set( + "fan.test_fan", + "on", + { + "friendly_name": "Test fan", + "supported_features": 3, + "speed": "high", + "oscillating": True, + }, + ) + + # To trigger event listener + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + assert call_json["event"]["header"]["namespace"] == "Alexa" + assert call_json["event"]["header"]["name"] == "ChangeReport" + + change_reports = call_json["event"]["payload"]["change"]["properties"] + for report in change_reports: + if report["name"] == "toggleState": + assert report["value"] == "ON" + assert report["instance"] == "fan.oscillating" + assert report["namespace"] == "Alexa.ToggleController" + + assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan" + + async def test_send_add_or_update_message(hass, aioclient_mock): """Test sending an AddOrUpdateReport message.""" aioclient_mock.post(TEST_URL, text="") From e3f0c904b05a4a8e90ee8b7ff7b4a702f283df3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Wed, 23 Oct 2019 07:06:21 +0200 Subject: [PATCH 1194/3953] Add option to specify mDNS advertised IP address for HomeKit Bridge (#26791) * Add options to specify advertised IP and MAC for HomeKit Bridge This makes use of HAP-python's new feature in version 2.6.0 that allows to specify the mDNS advertised IP and MAC address. This is a requirement for the following use cases: - Running Home Assistant behind a NAT, e.g. inside Docker. - Running it on a system with multiple interfaces there the default IP address, DNS entry and hostname diverge. The forwarding of the required mDNS packets can be done with an avahi-daemon based gateway, e.g. by using enable-reflector=yes. Specifying the MAC address makes it possible to identify an accessory in case HA is run inside a ephemeral docker container. Whitespace changes were performed due to black and flake8. * Update tests for HomeKit Bridge due to IP and MAC advertising Whitespace changes were performed due to black and flake8. * Remove the possibility to set the MAC address of the HomeKit Bridge Since the MAC address is a random device ID, there is no need for the user to be able to set a custom MAC address value for it. Whitespace changes were performed due to black and flake8. --- homeassistant/components/homekit/__init__.py | 31 +++++++++++++++-- homeassistant/components/homekit/const.py | 1 + tests/components/homekit/test_homekit.py | 35 +++++++++++++++++--- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index d8aafb8e238a50..4c300e0a934e9f 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -31,6 +31,7 @@ from .const import ( BRIDGE_NAME, + CONF_ADVERTISE_IP, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, @@ -89,6 +90,9 @@ ), vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_ADVERTISE_IP): vol.All( + ipaddress.ip_address, cv.string + ), vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, @@ -112,13 +116,21 @@ async def async_setup(hass, config): name = conf[CONF_NAME] port = conf[CONF_PORT] ip_address = conf.get(CONF_IP_ADDRESS) + advertise_ip = conf.get(CONF_ADVERTISE_IP) auto_start = conf[CONF_AUTO_START] safe_mode = conf[CONF_SAFE_MODE] entity_filter = conf[CONF_FILTER] entity_config = conf[CONF_ENTITY_CONFIG] homekit = HomeKit( - hass, name, port, ip_address, entity_filter, entity_config, safe_mode + hass, + name, + port, + ip_address, + entity_filter, + entity_config, + safe_mode, + advertise_ip, ) await hass.async_add_executor_job(homekit.setup) @@ -265,7 +277,15 @@ class HomeKit: """Class to handle all actions between HomeKit and Home Assistant.""" def __init__( - self, hass, name, port, ip_address, entity_filter, entity_config, safe_mode + self, + hass, + name, + port, + ip_address, + entity_filter, + entity_config, + safe_mode, + advertise_ip=None, ): """Initialize a HomeKit object.""" self.hass = hass @@ -275,6 +295,7 @@ def __init__( self._filter = entity_filter self._config = entity_config self._safe_mode = safe_mode + self._advertise_ip = advertise_ip self.status = STATUS_READY self.bridge = None @@ -289,7 +310,11 @@ def setup(self): ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) self.driver = HomeDriver( - self.hass, address=ip_addr, port=self._port, persist_file=path + self.hass, + address=ip_addr, + port=self._port, + persist_file=path, + advertised_address=self._advertise_ip, ) self.bridge = HomeBridge(self.hass, self.driver, self._name) if self._safe_mode: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index d225225237fa0e..82ec296da4bde2 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -10,6 +10,7 @@ ATTR_VALUE = "value" # #### Config #### +CONF_ADVERTISE_IP = "advertise_ip" CONF_AUTO_START = "auto_start" CONF_ENTITY_CONFIG = "entity_config" CONF_FEATURE = "feature" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 61893af7008ada..97838eaa8525c2 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -69,7 +69,7 @@ async def test_setup_min(hass): assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) mock_homekit.assert_any_call( - hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE + hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE, None ) assert mock_homekit().setup.called is True @@ -98,7 +98,7 @@ async def test_setup_auto_start_disabled(hass): assert await setup.async_setup_component(hass, DOMAIN, config) mock_homekit.assert_any_call( - hass, "Test Name", 11111, "172.0.0.0", ANY, {}, DEFAULT_SAFE_MODE + hass, "Test Name", 11111, "172.0.0.0", ANY, {}, DEFAULT_SAFE_MODE, None ) assert mock_homekit().setup.called is True @@ -136,7 +136,11 @@ async def test_homekit_setup(hass, hk_driver): path = hass.config.path(HOMEKIT_FILE) assert isinstance(homekit.bridge, HomeBridge) mock_driver.assert_called_with( - hass, address=IP_ADDRESS, port=DEFAULT_PORT, persist_file=path + hass, + address=IP_ADDRESS, + port=DEFAULT_PORT, + persist_file=path, + advertised_address=None, ) assert homekit.driver.safe_mode is False @@ -153,7 +157,30 @@ async def test_homekit_setup_ip_address(hass, hk_driver): ) as mock_driver: await hass.async_add_job(homekit.setup) mock_driver.assert_called_with( - hass, address="172.0.0.0", port=DEFAULT_PORT, persist_file=ANY + hass, + address="172.0.0.0", + port=DEFAULT_PORT, + persist_file=ANY, + advertised_address=None, + ) + + +async def test_homekit_setup_advertise_ip(hass, hk_driver): + """Test setup with given IP address to advertise.""" + homekit = HomeKit( + hass, BRIDGE_NAME, DEFAULT_PORT, "0.0.0.0", {}, {}, None, "192.168.1.100" + ) + + with patch( + PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver + ) as mock_driver: + await hass.async_add_job(homekit.setup) + mock_driver.assert_called_with( + hass, + address="0.0.0.0", + port=DEFAULT_PORT, + persist_file=ANY, + advertised_address="192.168.1.100", ) From 4cb984842aba70520c4522be3d047286234b7f1b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 23 Oct 2019 01:26:29 -0400 Subject: [PATCH 1195/3953] Support custom source type for MQTT device tracker (#27838) * support custom source type for MQTT device tracker * fix typo * add abbreviation --- .../components/mqtt/abbreviations.py | 1 + .../components/mqtt/device_tracker.py | 13 +++++--- tests/components/mqtt/test_device_tracker.py | 31 ++++++++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 5a5ed4555db536..5e995494a64bc5 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -130,6 +130,7 @@ "spd_stat_t": "speed_state_topic", "spd_val_tpl": "speed_value_template", "spds": "speeds", + "src_type": "source_type", "stat_clsd": "state_closed", "stat_off": "state_off", "stat_on": "state_on", diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index c9cce3ebeda029..d25d7ce21d319b 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -4,7 +4,7 @@ import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_DEVICES, STATE_NOT_HOME, STATE_HOME @@ -15,12 +15,14 @@ CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" +CONF_SOURCE_TYPE = "source_type" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( { vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, + vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), } ) @@ -31,6 +33,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): qos = config[CONF_QOS] payload_home = config[CONF_PAYLOAD_HOME] payload_not_home = config[CONF_PAYLOAD_NOT_HOME] + source_type = config.get(CONF_SOURCE_TYPE) for dev_id, topic in devices.items(): @@ -44,9 +47,11 @@ def async_message_received(msg, dev_id=dev_id): else: location_name = msg.payload - hass.async_create_task( - async_see(dev_id=dev_id, location_name=location_name) - ) + see_args = {"dev_id": dev_id, "location_name": location_name} + if source_type: + see_args["source_type"] = source_type + + hass.async_create_task(async_see(**see_args)) await mqtt.async_subscribe(hass, topic, async_message_received, qos) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 14180d2dcf9b95..71348fcf5cbda4 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -3,7 +3,10 @@ import pytest from homeassistant.components import device_tracker -from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT +from homeassistant.components.device_tracker.const import ( + ENTITY_ID_FORMAT, + SOURCE_TYPE_BLUETOOTH, +) from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component @@ -218,3 +221,29 @@ async def test_not_matching_custom_payload_for_home_and_not_home( await hass.async_block_till_done() assert hass.states.get(entity_id).state != STATE_HOME assert hass.states.get(entity_id).state != STATE_NOT_HOME + + +async def test_matching_source_type(hass, mock_device_tracker_conf): + """Test setting source type.""" + dev_id = "paulus" + entity_id = ENTITY_ID_FORMAT.format(dev_id) + topic = "/location/paulus" + source_type = SOURCE_TYPE_BLUETOOTH + location = "work" + + hass.config.components = set(["mqtt", "zone"]) + assert await async_setup_component( + hass, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: topic}, + "source_type": source_type, + } + }, + ) + + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).attributes["source_type"] == SOURCE_TYPE_BLUETOOTH From 09d8a4204ab907d816b84e54d8f822e90c3e9724 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 23 Oct 2019 07:43:28 +0200 Subject: [PATCH 1196/3953] Add support for resource_template for rest sensor (#27869) * add support for resource_template * fix tests * updated tests and xor(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE) --- homeassistant/components/rest/sensor.py | 23 +++++++++++++- homeassistant/const.py | 1 + tests/components/rest/test_sensor.py | 42 +++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 01d974e7006003..41adb8559036cf 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -16,6 +16,7 @@ CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, + CONF_RESOURCE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_TIMEOUT, @@ -42,7 +43,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_RESOURCE): cv.url, + vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url, + vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template, vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), @@ -62,11 +64,16 @@ } ) +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA +) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RESTful sensor.""" name = config.get(CONF_NAME) resource = config.get(CONF_RESOURCE) + resource_template = config.get(CONF_RESOURCE_TEMPLATE) method = config.get(CONF_METHOD) payload = config.get(CONF_PAYLOAD) verify_ssl = config.get(CONF_VERIFY_SSL) @@ -83,6 +90,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if value_template is not None: value_template.hass = hass + if resource_template is not None: + resource_template.hass = hass + resource = resource_template.render() + if username and password: if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: auth = HTTPDigestAuth(username, password) @@ -108,6 +119,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): value_template, json_attrs, force_update, + resource_template, ) ], True, @@ -127,6 +139,7 @@ def __init__( value_template, json_attrs, force_update, + resource_template, ): """Initialize the REST sensor.""" self._hass = hass @@ -139,6 +152,7 @@ def __init__( self._json_attrs = json_attrs self._attributes = None self._force_update = force_update + self._resource_template = resource_template @property def name(self): @@ -172,6 +186,9 @@ def force_update(self): def update(self): """Get the latest data from REST API and update the state.""" + if self._resource_template is not None: + self.rest.set_url(self._resource_template.render()) + self.rest.update() value = self.rest.data @@ -217,6 +234,10 @@ def __init__( self._timeout = timeout self.data = None + def set_url(self, url): + """Set url.""" + self._request.prepare_url(url, None) + def update(self): """Get the latest data from REST service with provided method.""" _LOGGER.debug("Updating from %s", self._request.url) diff --git a/homeassistant/const.py b/homeassistant/const.py index 592f6b60bc64ae..cac0386b8120bd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -124,6 +124,7 @@ CONF_REGION = "region" CONF_RESOURCE = "resource" CONF_RESOURCES = "resources" +CONF_RESOURCE_TEMPLATE = "resource_template" CONF_RGB = "rgb" CONF_ROOM = "room" CONF_SCAN_INTERVAL = "scan_interval" diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index d117678ccc7e56..50acb0533476e9 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -76,6 +76,40 @@ def test_setup_minimum(self, mock_req): ) assert 2 == mock_req.call_count + @requests_mock.Mocker() + def test_setup_minimum_resource_template(self, mock_req): + """Test setup with minimum configuration (resource_template).""" + mock_req.get("http://localhost", status_code=200) + with assert_setup_component(1, "sensor"): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "rest", + "resource_template": "http://localhost", + } + }, + ) + assert mock_req.call_count == 2 + + @requests_mock.Mocker() + def test_setup_duplicate_resource(self, mock_req): + """Test setup with duplicate resources.""" + mock_req.get("http://localhost", status_code=200) + with assert_setup_component(0, "sensor"): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "rest", + "resource": "http://localhost", + "resource_template": "http://localhost", + } + }, + ) + @requests_mock.Mocker() def test_setup_get(self, mock_req): """Test setup with valid configuration.""" @@ -152,6 +186,7 @@ def setUp(self): self.value_template = template("{{ value_json.key }}") self.value_template.hass = self.hass self.force_update = False + self.resource_template = None self.sensor = rest.RestSensor( self.hass, @@ -162,6 +197,7 @@ def setUp(self): self.value_template, [], self.force_update, + self.resource_template, ) def tearDown(self): @@ -222,6 +258,7 @@ def test_update_with_no_template(self): None, [], self.force_update, + self.resource_template, ) self.sensor.update() assert "plain_state" == self.sensor.state @@ -242,6 +279,7 @@ def test_update_with_json_attrs(self): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert "some_json_value" == self.sensor.device_state_attributes["key"] @@ -261,6 +299,7 @@ def test_update_with_json_attrs_no_data(self, mock_logger): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -282,6 +321,7 @@ def test_update_with_json_attrs_not_dict(self, mock_logger): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -303,6 +343,7 @@ def test_update_with_json_attrs_bad_JSON(self, mock_logger): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -326,6 +367,7 @@ def test_update_with_json_attrs_and_template(self): self.value_template, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() From ab22c617646387af18f088aaf26aba348d606bce Mon Sep 17 00:00:00 2001 From: Matt Kasa Date: Tue, 22 Oct 2019 22:46:18 -0700 Subject: [PATCH 1197/3953] Support SmartStrip type devices (HS300, HS107) in tplink component (#26220) * Add support for SmartStrip type devices (HS300, HS107) to tplink component * Incorporate feedback from @MartinHjelmare using changes suggested by @shbatm - Setting `_state` now uses a list comprehension - `_alias` will use aliases from the Kasa app - `_device_id` will be set to `_mac` for single plugs to retain backwards compatibility --- homeassistant/components/tplink/__init__.py | 4 ++++ homeassistant/components/tplink/common.py | 20 ++++++++++++++--- homeassistant/components/tplink/switch.py | 25 +++++++++++++++++---- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 85258b5e94edc1..7aa261564f3af9 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -14,6 +14,7 @@ CONF_DISCOVERY, CONF_LIGHT, CONF_SWITCH, + CONF_STRIP, SmartDevices, async_discover_devices, get_static_devices, @@ -36,6 +37,9 @@ vol.Optional(CONF_SWITCH, default=[]): vol.All( cv.ensure_list, [TPLINK_HOST_SCHEMA] ), + vol.Optional(CONF_STRIP, default=[]): vol.All( + cv.ensure_list, [TPLINK_HOST_SCHEMA] + ), vol.Optional(CONF_DIMMER, default=[]): vol.All( cv.ensure_list, [TPLINK_HOST_SCHEMA] ), diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 75636c8dc2817a..548edc6822c706 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -4,7 +4,14 @@ import logging from typing import Any, Callable, List -from pyHS100 import Discover, SmartBulb, SmartDevice, SmartDeviceException, SmartPlug +from pyHS100 import ( + Discover, + SmartBulb, + SmartDevice, + SmartDeviceException, + SmartPlug, + SmartStrip, +) from homeassistant.helpers.typing import HomeAssistantType @@ -15,6 +22,7 @@ CONF_DIMMER = "dimmer" CONF_DISCOVERY = "discovery" CONF_LIGHT = "light" +CONF_STRIP = "strip" CONF_SWITCH = "switch" @@ -74,7 +82,10 @@ def process_devices(): if existing_devices.has_device_with_host(dev.host): continue - if isinstance(dev, SmartPlug): + if isinstance(dev, SmartStrip): + for plug in dev.plugs.values(): + switches.append(plug) + elif isinstance(dev, SmartPlug): try: if dev.is_dimmable: # Dimmers act as lights lights.append(dev) @@ -99,7 +110,7 @@ def get_static_devices(config_data) -> SmartDevices: lights = [] switches = [] - for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_DIMMER]: + for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_STRIP, CONF_DIMMER]: for entry in config_data[type_]: host = entry["host"] @@ -107,6 +118,9 @@ def get_static_devices(config_data) -> SmartDevices: lights.append(SmartBulb(host)) elif type_ == CONF_SWITCH: switches.append(SmartPlug(host)) + elif type_ == CONF_STRIP: + for plug in SmartStrip(host).plugs.values(): + switches.append(plug) # Dimmers need to be defined as smart plugs to work correctly. elif type_ == CONF_DIMMER: lights.append(SmartPlug(host)) diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index ebeac984515e63..791d358c509500 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -69,11 +69,12 @@ def __init__(self, smartplug: SmartPlug): self._mac = None self._alias = None self._model = None + self._device_id = None @property def unique_id(self): """Return a unique ID.""" - return self._mac + return self._device_id @property def name(self): @@ -120,10 +121,26 @@ def update(self): if not self._sysinfo: self._sysinfo = self.smartplug.sys_info self._mac = self.smartplug.mac - self._alias = self.smartplug.alias self._model = self.smartplug.model - - self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON + if self.smartplug.context is None: + self._alias = self.smartplug.alias + self._device_id = self._mac + else: + self._alias = [ + child + for child in self.smartplug.sys_info["children"] + if child["id"] == self.smartplug.context + ][0]["alias"] + self._device_id = self.smartplug.context + + if self.smartplug.context is None: + self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON + else: + self._state = [ + child + for child in self.smartplug.sys_info["children"] + if child["id"] == self.smartplug.context + ][0]["state"] == 1 if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() From 25fd930d67ec79e6e9cb09d50451e6677cce24be Mon Sep 17 00:00:00 2001 From: SteveDinn Date: Wed, 23 Oct 2019 02:51:29 -0300 Subject: [PATCH 1198/3953] Add template filters to convert objects to and from JSON strings (#27909) * Added filters to convert objects to and from JSON strings. * Added extra spacing. * Removed try/catch to get native exceptions * Added tests. --- homeassistant/helpers/template.py | 12 ++++++++++++ tests/helpers/test_template.py | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 9af1998e894ebe..1d9ca6914510c1 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -884,6 +884,16 @@ def ordinal(value): ) +def from_json(value): + """Convert a JSON string to an object.""" + return json.loads(value) + + +def to_json(value): + """Convert an object to a JSON string.""" + return json.dumps(value) + + @contextfilter def random_every_time(context, values): """Choose a random value. @@ -916,6 +926,8 @@ def __init__(self, hass): self.filters["timestamp_custom"] = timestamp_custom self.filters["timestamp_local"] = timestamp_local self.filters["timestamp_utc"] = timestamp_utc + self.filters["to_json"] = to_json + self.filters["from_json"] = from_json self.filters["is_defined"] = fail_when_undefined self.filters["max"] = max self.filters["min"] = min diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index cc1f7707df6246..b69fdb17e35a7f 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -501,6 +501,30 @@ def test_timestamp_local(hass): ) +def test_to_json(hass): + """Test the object to JSON string filter.""" + + # Note that we're not testing the actual json.loads and json.dumps methods, + # only the filters, so we don't need to be exhaustive with our sample JSON. + expected_result = '{"Foo": "Bar"}' + actual_result = template.Template( + "{{ {'Foo': 'Bar'} | to_json }}", hass + ).async_render() + assert actual_result == expected_result + + +def test_from_json(hass): + """Test the JSON string to object filter.""" + + # Note that we're not testing the actual json.loads and json.dumps methods, + # only the filters, so we don't need to be exhaustive with our sample JSON. + expected_result = "Bar" + actual_result = template.Template( + '{{ (\'{"Foo": "Bar"}\' | from_json).Foo }}', hass + ).async_render() + assert actual_result == expected_result + + def test_min(hass): """Test the min filter.""" assert template.Template("{{ [1, 2, 3] | min }}", hass).async_render() == "1" From 8bdec13baded6c8f671b9749a1ac5b1f90a16f01 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Wed, 23 Oct 2019 07:58:57 +0200 Subject: [PATCH 1199/3953] Move imports in hue component (#28121) --- homeassistant/components/hue/__init__.py | 8 ++-- homeassistant/components/hue/binary_sensor.py | 16 +++++++- homeassistant/components/hue/helpers.py | 2 +- homeassistant/components/hue/light.py | 7 ++-- homeassistant/components/hue/sensor.py | 26 ++++++++++--- homeassistant/components/hue/sensor_base.py | 37 ++----------------- 6 files changed, 48 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 064e18e7a81b6e..027ec205195279 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -8,11 +8,11 @@ from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.helpers import config_validation as cv, device_registry as dr -from .const import DOMAIN from .bridge import HueBridge - -# Loading the config flow file will register the flow -from .config_flow import configured_hosts +from .config_flow import ( + configured_hosts, +) # Loading the config flow file will register the flow +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index 9c84cb5d61cd59..e4b7dd85e37ed2 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -1,19 +1,31 @@ """Hue binary sensor entities.""" + +from aiohue.sensors import TYPE_ZLL_PRESENCE + from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_MOTION, + BinarySensorDevice, ) from homeassistant.components.hue.sensor_base import ( GenericZLLSensor, + SensorManager, async_setup_entry as shared_async_setup_entry, ) - PRESENCE_NAME_FORMAT = "{} motion" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer binary sensor setup to the shared sensor module.""" + SensorManager.sensor_config_map.update( + { + TYPE_ZLL_PRESENCE: { + "binary": True, + "name_format": PRESENCE_NAME_FORMAT, + "class": HuePresence, + } + } + ) await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=True) diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index 388046bb8cb5c5..971509ab6479c5 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -1,6 +1,6 @@ """Helper functions for Philips Hue.""" -from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg from .const import DOMAIN diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index dcae1cf4f5dab5..041eb76c1d34f0 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -2,8 +2,8 @@ import asyncio from datetime import timedelta import logging -from time import monotonic import random +from time import monotonic import aiohue import async_timeout @@ -14,21 +14,22 @@ ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_TRANSITION, ATTR_HS_COLOR, + ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_RANDOM, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_COLOR, SUPPORT_TRANSITION, Light, ) from homeassistant.util import color + from .helpers import remove_devices SCAN_INTERVAL = timedelta(seconds=5) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 457ed761202bec..f2e02d49ecfcc1 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -1,15 +1,17 @@ """Hue sensor entities.""" +from aiohue.sensors import TYPE_ZLL_LIGHTLEVEL, TYPE_ZLL_TEMPERATURE + +from homeassistant.components.hue.sensor_base import ( + GenericZLLSensor, + SensorManager, + async_setup_entry as shared_async_setup_entry, +) from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, ) from homeassistant.helpers.entity import Entity -from homeassistant.components.hue.sensor_base import ( - GenericZLLSensor, - async_setup_entry as shared_async_setup_entry, -) - LIGHT_LEVEL_NAME_FORMAT = "{} light level" TEMPERATURE_NAME_FORMAT = "{} temperature" @@ -17,6 +19,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Defer sensor setup to the shared sensor module.""" + SensorManager.sensor_config_map.update( + { + TYPE_ZLL_LIGHTLEVEL: { + "binary": False, + "name_format": LIGHT_LEVEL_NAME_FORMAT, + "class": HueLightLevel, + }, + TYPE_ZLL_TEMPERATURE: { + "binary": False, + "name_format": TEMPERATURE_NAME_FORMAT, + "class": HueTemperature, + }, + } + ) await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=False) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 3f202d38bc5e38..7236dfbd886890 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -4,6 +4,8 @@ import logging from time import monotonic +from aiohue import AiohueException +from aiohue.sensors import TYPE_ZLL_PRESENCE import async_timeout from homeassistant.components import hue @@ -53,41 +55,12 @@ class SensorManager: def __init__(self, hass, bridge, config_entry): """Initialize the sensor manager.""" - import aiohue - from .binary_sensor import HuePresence, PRESENCE_NAME_FORMAT - from .sensor import ( - HueLightLevel, - HueTemperature, - LIGHT_LEVEL_NAME_FORMAT, - TEMPERATURE_NAME_FORMAT, - ) - self.hass = hass self.bridge = bridge self.config_entry = config_entry self._component_add_entities = {} self._started = False - self.sensor_config_map.update( - { - aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: { - "binary": False, - "name_format": LIGHT_LEVEL_NAME_FORMAT, - "class": HueLightLevel, - }, - aiohue.sensors.TYPE_ZLL_TEMPERATURE: { - "binary": False, - "name_format": TEMPERATURE_NAME_FORMAT, - "class": HueTemperature, - }, - aiohue.sensors.TYPE_ZLL_PRESENCE: { - "binary": True, - "name_format": PRESENCE_NAME_FORMAT, - "class": HuePresence, - }, - } - ) - def register_component(self, binary, async_add_entities): """Register async_add_entities methods for components.""" self._component_add_entities[binary] = async_add_entities @@ -117,15 +90,13 @@ async def async_update_bridge(now): async def async_update_items(self): """Update sensors from the bridge.""" - import aiohue - api = self.bridge.api.sensors try: start = monotonic() with async_timeout.timeout(4): await api.update() - except (asyncio.TimeoutError, aiohue.AiohueException) as err: + except (asyncio.TimeoutError, AiohueException) as err: _LOGGER.debug("Failed to fetch sensor: %s", err) if not self.bridge.available: @@ -164,7 +135,7 @@ async def async_update_items(self): # finding the remaining ones that may or may not be related to the # presence sensors. for item_id in api: - if api[item_id].type != aiohue.sensors.TYPE_ZLL_PRESENCE: + if api[item_id].type != TYPE_ZLL_PRESENCE: continue primary_sensor_devices[_device_id(api[item_id])] = api[item_id] From 703cd961863dc0ea370642bbc5dbb94117644862 Mon Sep 17 00:00:00 2001 From: Santobert Date: Wed, 23 Oct 2019 08:03:38 +0200 Subject: [PATCH 1200/3953] Add improved scene support to the input_datetime integration (#28105) * input_datetime reproduce state * simplify service decision --- .../input_datetime/reproduce_state.py | 111 ++++++++++++++++++ .../input_datetime/test_reproduce_state.py | 69 +++++++++++ 2 files changed, 180 insertions(+) create mode 100644 homeassistant/components/input_datetime/reproduce_state.py create mode 100644 tests/components/input_datetime/test_reproduce_state.py diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py new file mode 100644 index 00000000000000..09a30e6521052e --- /dev/null +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -0,0 +1,111 @@ +"""Reproduce an Input datetime state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt as dt_util + +from . import ( + ATTR_DATE, + ATTR_DATETIME, + ATTR_TIME, + CONF_HAS_DATE, + CONF_HAS_TIME, + DOMAIN, + SERVICE_SET_DATETIME, +) + +_LOGGER = logging.getLogger(__name__) + + +def is_valid_datetime(string: str) -> bool: + """Test if string dt is a valid datetime.""" + try: + return dt_util.parse_datetime(string) is not None + except ValueError: + return False + + +def is_valid_date(string: str) -> bool: + """Test if string dt is a valid date.""" + try: + return dt_util.parse_date(string) is not None + except ValueError: + return False + + +def is_valid_time(string: str) -> bool: + """Test if string dt is a valid time.""" + try: + return dt_util.parse_time(string) is not None + except ValueError: + return False + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 not ( + ( + is_valid_datetime(state.state) + and cur_state.attributes.get(CONF_HAS_DATE) + and cur_state.attributes.get(CONF_HAS_TIME) + ) + or ( + is_valid_date(state.state) + and cur_state.attributes.get(CONF_HAS_DATE) + and not cur_state.attributes.get(CONF_HAS_TIME) + ) + or ( + is_valid_time(state.state) + and cur_state.attributes.get(CONF_HAS_TIME) + and not cur_state.attributes.get(CONF_HAS_DATE) + ) + ): + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service = SERVICE_SET_DATETIME + service_data = {ATTR_ENTITY_ID: state.entity_id} + + has_time = cur_state.attributes.get(CONF_HAS_TIME) + has_date = cur_state.attributes.get(CONF_HAS_DATE) + + if has_time and has_date: + service_data[ATTR_DATETIME] = state.state + elif has_time: + service_data[ATTR_TIME] = state.state + elif has_date: + service_data[ATTR_DATE] = state.state + else: + _LOGGER.warning("input_datetime needs either has_date or has_time or both") + return + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input datetime states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py new file mode 100644 index 00000000000000..71f0658923c3ba --- /dev/null +++ b/tests/components/input_datetime/test_reproduce_state.py @@ -0,0 +1,69 @@ +"""Test reproduce state for Input datetime.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input datetime states.""" + hass.states.async_set( + "input_datetime.entity_datetime", + "2010-10-10 01:20:00", + {"has_date": True, "has_time": True}, + ) + hass.states.async_set( + "input_datetime.entity_time", "01:20:00", {"has_date": False, "has_time": True} + ) + hass.states.async_set( + "input_datetime.entity_date", + "2010-10-10", + {"has_date": True, "has_time": False}, + ) + + datetime_calls = async_mock_service(hass, "input_datetime", "set_datetime") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("input_datetime.entity_datetime", "2010-10-10 01:20:00"), + State("input_datetime.entity_time", "01:20:00"), + State("input_datetime.entity_date", "2010-10-10"), + ], + blocking=True, + ) + + assert len(datetime_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("input_datetime.entity_datetime", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(datetime_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("input_datetime.entity_datetime", "2011-10-10 02:20:00"), + State("input_datetime.entity_time", "02:20:00"), + State("input_datetime.entity_date", "2011-10-10"), + # Should not raise + State("input_datetime.non_existing", "2010-10-10 01:20:00"), + ], + blocking=True, + ) + + valid_calls = [ + { + "entity_id": "input_datetime.entity_datetime", + "datetime": "2011-10-10 02:20:00", + }, + {"entity_id": "input_datetime.entity_time", "time": "02:20:00"}, + {"entity_id": "input_datetime.entity_date", "date": "2011-10-10"}, + ] + assert len(datetime_calls) == 3 + for call in datetime_calls: + assert call.domain == "input_datetime" + assert call.data in valid_calls + valid_calls.remove(call.data) From 65263bdef98e1d09eb5f33a833415f57175695c7 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 23 Oct 2019 08:08:38 +0200 Subject: [PATCH 1201/3953] Fix #28104 - CalDav support for floating datetimes (#28123) * Fix #28104 - CalDav support for floating datetimes Timzones are optional in CalDav It is possible that an entry contains neither a TZID, nor is an UTC time. When this is the case, it should be treated as a floating date-time value, which represent the same hour, minute, and second value regardless of which time zone is currently being observed. For Home-Assistant the correct timezone therefore is whatever is configured as local time in the settings. See https://www.kanzaki.com/docs/ical/dateTime.html * Revert "Fix #28104 - CalDav support for floating datetimes" This reverts commit cf32a6e39058e340816ae1e3ebd4a2c236b91964. * add test case: floating events fail with error without patch * Fix #28104 - CalDav support for floating datetimes Timzones are optional in CalDav It is possible that an entry contains neither a TZID, nor is an UTC time. When this is the case, it should be treated as a floating date-time value, which represent the same hour, minute, and second value regardless of which time zone is currently being observed. For Home-Assistant the correct timezone therefore is whatever is configured as local time in the settings. See https://www.kanzaki.com/docs/ical/dateTime.html * style fix --- homeassistant/components/caldav/calendar.py | 4 +++ tests/components/caldav/test_calendar.py | 36 +++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 2bbff2a6bc78e3..ad9dac1f7274f8 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -278,6 +278,10 @@ def get_hass_date(obj): def to_datetime(obj): """Return a datetime.""" if isinstance(obj, datetime): + if obj.tzinfo is None: + # floating value, not bound to any time zone in particular + # represent same time regardless of which time zone is currently being observed + return obj.replace(tzinfo=dt.DEFAULT_TIME_ZONE) return obj return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min)) diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 209ab7802652d1..c0be635988a16a 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -111,6 +111,19 @@ DESCRIPTION:Sunny day END:VEVENT END:VCALENDAR +""", + """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Global Corp.//CalDAV Client//EN +BEGIN:VEVENT +UID:8 +DTSTART:20171127T190000 +DTEND:20171127T200000 +SUMMARY:This is a floating Event +LOCATION:Hamburg +DESCRIPTION:What a day +END:VEVENT +END:VCALENDAR """, ] @@ -292,6 +305,29 @@ async def test_ongoing_event_different_tz(mock_now, hass, calendar): } +@patch("homeassistant.util.dt.now", return_value=_local_datetime(19, 10)) +async def test_ongoing_floating_event_returned(mock_now, hass, calendar): + """Test that floating events without timezones work.""" + assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) + await hass.async_block_till_done() + + state = hass.states.get("calendar.private") + print(dt.DEFAULT_TIME_ZONE) + print(state) + assert state.name == calendar.name + assert state.state == STATE_ON + assert dict(state.attributes) == { + "friendly_name": "Private", + "message": "This is a floating Event", + "all_day": False, + "offset_reached": False, + "start_time": "2017-11-27 19:00:00", + "end_time": "2017-11-27 20:00:00", + "location": "Hamburg", + "description": "What a day", + } + + @patch("homeassistant.util.dt.now", return_value=_local_datetime(8, 30)) async def test_ongoing_event_with_offset(mock_now, hass, calendar): """Test that the offset is taken into account.""" From f67813e145baa65b10ebc75a8c76c417bb17c833 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Wed, 23 Oct 2019 02:09:28 -0400 Subject: [PATCH 1202/3953] Fix service descriptions (#28122) --- homeassistant/components/nest/services.yaml | 49 +++++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index 0015c83342d069..e10e626464378e 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -1,16 +1,37 @@ -set_mode: - description: 'Set the home/away mode for a Nest structure. Set to away mode will - also set Estimated Arrival Time if provided. Set ETA will cause the thermostat - to begin warming or cooling the home before the user arrives. After ETA set other - Automation can read ETA sensor as a signal to prepare the home for the user''s - arrival. +# Describes the format for available Nest services - ' +set_away_mode: + description: Set the away mode for a Nest structure. fields: - eta: {description: Optional Estimated Arrival Time from now., example: '0:10'} - eta_window: {description: Optional ETA window. Default is 1 minute., example: '0:5'} - home_mode: {description: home or away, example: home} - structure: {description: Optional structure name. Default set all structures managed - by Home Assistant., example: My Home} - trip_id: {description: Optional identity of a trip. Using the same trip_ID will - update the estimation., example: trip_back_home} + away_mode: + description: New mode to set. Valid modes are "away" or "home". + example: "away" + structure: + description: Name(s) of structure(s) to change. Defaults to all structures if not specified. + example: "Apartment" + +set_eta: + description: Set or update the estimated time of arrival window for a Nest structure. + fields: + eta: + description: Estimated time of arrival from now. + example: "00:10:30" + eta_window: + description: Estimated time of arrival window. Default is 1 minute. + example: "00:05" + trip_id: + description: Unique ID for the trip. Default is auto-generated using a timestamp. + example: "Leave Work" + structure: + description: Name(s) of structure(s) to change. Defaults to all structures if not specified. + example: "Apartment" + +cancel_eta: + description: Cancel an existing estimated time of arrival window for a Nest structure. + fields: + trip_id: + description: Unique ID for the trip. + example: "Leave Work" + structure: + description: Name(s) of structure(s) to change. Defaults to all structures if not specified. + example: "Apartment" From 50e9a9df4f4f59f2e4d85a20a98af58fc5b99c58 Mon Sep 17 00:00:00 2001 From: Santobert Date: Wed, 23 Oct 2019 08:12:17 +0200 Subject: [PATCH 1203/3953] Timer reproduce state (#28117) --- .../components/timer/reproduce_state.py | 70 ++++++++++++++++ .../components/timer/test_reproduce_state.py | 84 +++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 homeassistant/components/timer/reproduce_state.py create mode 100644 tests/components/timer/test_reproduce_state.py diff --git a/homeassistant/components/timer/reproduce_state.py b/homeassistant/components/timer/reproduce_state.py new file mode 100644 index 00000000000000..c765ed7da9c7a2 --- /dev/null +++ b/homeassistant/components/timer/reproduce_state.py @@ -0,0 +1,70 @@ +"""Reproduce an Timer state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_DURATION, + DOMAIN, + SERVICE_CANCEL, + SERVICE_PAUSE, + SERVICE_START, + STATUS_ACTIVE, + STATUS_IDLE, + STATUS_PAUSED, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATUS_IDLE, STATUS_ACTIVE, STATUS_PAUSED} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and cur_state.attributes.get( + ATTR_DURATION + ) == state.attributes.get(ATTR_DURATION): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATUS_ACTIVE: + service = SERVICE_START + if ATTR_DURATION in state.attributes: + service_data[ATTR_DURATION] = state.attributes[ATTR_DURATION] + elif state.state == STATUS_PAUSED: + service = SERVICE_PAUSE + elif state.state == STATUS_IDLE: + service = SERVICE_CANCEL + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Timer states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/timer/test_reproduce_state.py b/tests/components/timer/test_reproduce_state.py new file mode 100644 index 00000000000000..5539d8610c3bd6 --- /dev/null +++ b/tests/components/timer/test_reproduce_state.py @@ -0,0 +1,84 @@ +"""Test reproduce state for Timer.""" +from homeassistant.components.timer import ( + ATTR_DURATION, + SERVICE_CANCEL, + SERVICE_PAUSE, + SERVICE_START, + STATUS_ACTIVE, + STATUS_IDLE, + STATUS_PAUSED, +) +from homeassistant.core import State +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Timer states.""" + hass.states.async_set("timer.entity_idle", STATUS_IDLE, {}) + hass.states.async_set("timer.entity_paused", STATUS_PAUSED, {}) + hass.states.async_set("timer.entity_active", STATUS_ACTIVE, {}) + hass.states.async_set( + "timer.entity_active_attr", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"} + ) + + start_calls = async_mock_service(hass, "timer", SERVICE_START) + pause_calls = async_mock_service(hass, "timer", SERVICE_PAUSE) + cancel_calls = async_mock_service(hass, "timer", SERVICE_CANCEL) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("timer.entity_idle", STATUS_IDLE), + State("timer.entity_paused", STATUS_PAUSED), + State("timer.entity_active", STATUS_ACTIVE), + State( + "timer.entity_active_attr", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"} + ), + ], + blocking=True, + ) + + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(cancel_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("timer.entity_idle", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(cancel_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("timer.entity_idle", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"}), + State("timer.entity_paused", STATUS_ACTIVE), + State("timer.entity_active", STATUS_IDLE), + State("timer.entity_active_attr", STATUS_PAUSED), + # Should not raise + State("timer.non_existing", "on"), + ], + blocking=True, + ) + + valid_start_calls = [ + {"entity_id": "timer.entity_idle", ATTR_DURATION: "00:01:00"}, + {"entity_id": "timer.entity_paused"}, + ] + assert len(start_calls) == 2 + for call in start_calls: + assert call.domain == "timer" + assert call.data in valid_start_calls + valid_start_calls.remove(call.data) + + assert len(pause_calls) == 1 + assert pause_calls[0].domain == "timer" + assert pause_calls[0].data == {"entity_id": "timer.entity_active_attr"} + + assert len(cancel_calls) == 1 + assert cancel_calls[0].domain == "timer" + assert cancel_calls[0].data == {"entity_id": "timer.entity_active"} From c8b2860167f597871a1fb0aae2554b55829c5d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 23 Oct 2019 09:12:57 +0300 Subject: [PATCH 1204/3953] Fix bootstrap dev dependencies message (#28114) https://github.com/home-assistant/home-assistant/pull/28060#discussion_r337701541 --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index ba594cbb341e6d..211f1355b7dce6 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -6,5 +6,5 @@ set -e cd "$(dirname "$0")/.." -echo "Installing test dependencies..." +echo "Installing development dependencies..." python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) From b4054add616af44963bc49add37239c16d106c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Wed, 23 Oct 2019 06:14:52 +0000 Subject: [PATCH 1205/3953] Move imports in wake_on_lan component (#28100) * Move imports in wake_on_lan component * Fix tox tests --- .gitignore | 3 + .../components/wake_on_lan/__init__.py | 2 +- .../components/wake_on_lan/switch.py | 8 +-- tests/components/wake_on_lan/test_init.py | 72 +++++++++---------- 4 files changed, 40 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index 15f0896975da36..2473aeb4bf650d 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ monkeytype.sqlite3 # This is left behind by Azure Restore Cache tmp_cache + +# python-language-server / Rope +.ropeproject diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index 421f6265c0c4b0..b4aad4925b9986 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -3,6 +3,7 @@ import logging import voluptuous as vol +import wakeonlan from homeassistant.const import CONF_MAC import homeassistant.helpers.config_validation as cv @@ -22,7 +23,6 @@ async def async_setup(hass, config): """Set up the wake on LAN component.""" - import wakeonlan async def send_magic_packet(call): """Send magic packet to wake up a device.""" diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 453685b13f60d6..01f69679829677 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -4,6 +4,7 @@ import subprocess as sp import voluptuous as vol +import wakeonlan from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_NAME @@ -48,8 +49,6 @@ class WOLSwitch(SwitchDevice): def __init__(self, hass, name, host, mac_address, off_action, broadcast_address): """Initialize the WOL switch.""" - import wakeonlan - self._hass = hass self._name = name self._host = host @@ -57,7 +56,6 @@ def __init__(self, hass, name, host, mac_address, off_action, broadcast_address) self._broadcast_address = broadcast_address self._off_script = Script(hass, off_action) if off_action else None self._state = False - self._wol = wakeonlan @property def is_on(self): @@ -72,11 +70,11 @@ def name(self): def turn_on(self, **kwargs): """Turn the device on.""" if self._broadcast_address: - self._wol.send_magic_packet( + wakeonlan.send_magic_packet( self._mac_address, ip_address=self._broadcast_address ) else: - self._wol.send_magic_packet(self._mac_address) + wakeonlan.send_magic_packet(self._mac_address) def turn_off(self, **kwargs): """Turn the device off if an off action is present.""" diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py index d71f15e610978b..c2ee0930895e17 100644 --- a/tests/components/wake_on_lan/test_init.py +++ b/tests/components/wake_on_lan/test_init.py @@ -1,52 +1,46 @@ """Tests for Wake On LAN component.""" -import asyncio -from unittest import mock - import pytest import voluptuous as vol -from homeassistant.setup import async_setup_component +from homeassistant.components import wake_on_lan from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET +from homeassistant.setup import async_setup_component - -@pytest.fixture -def mock_wakeonlan(): - """Mock mock_wakeonlan.""" - module = mock.MagicMock() - with mock.patch.dict("sys.modules", {"wakeonlan": module}): - yield module +from tests.common import MockDependency -@asyncio.coroutine -def test_send_magic_packet(hass, caplog, mock_wakeonlan): +async def test_send_magic_packet(hass): """Test of send magic packet service call.""" - mac = "aa:bb:cc:dd:ee:ff" - bc_ip = "192.168.255.255" - - yield from async_setup_component(hass, DOMAIN, {}) - - yield from hass.services.async_call( - DOMAIN, - SERVICE_SEND_MAGIC_PACKET, - {"mac": mac, "broadcast_address": bc_ip}, - blocking=True, - ) - assert len(mock_wakeonlan.mock_calls) == 1 - assert mock_wakeonlan.mock_calls[-1][1][0] == mac - assert mock_wakeonlan.mock_calls[-1][2]["ip_address"] == bc_ip - - with pytest.raises(vol.Invalid): - yield from hass.services.async_call( + with MockDependency("wakeonlan") as mocked_wakeonlan: + mac = "aa:bb:cc:dd:ee:ff" + bc_ip = "192.168.255.255" + + wake_on_lan.wakeonlan = mocked_wakeonlan + + await async_setup_component(hass, DOMAIN, {}) + + await hass.services.async_call( DOMAIN, SERVICE_SEND_MAGIC_PACKET, - {"broadcast_address": bc_ip}, + {"mac": mac, "broadcast_address": bc_ip}, blocking=True, ) - assert len(mock_wakeonlan.mock_calls) == 1 - - yield from hass.services.async_call( - DOMAIN, SERVICE_SEND_MAGIC_PACKET, {"mac": mac}, blocking=True - ) - assert len(mock_wakeonlan.mock_calls) == 2 - assert mock_wakeonlan.mock_calls[-1][1][0] == mac - assert not mock_wakeonlan.mock_calls[-1][2] + assert len(mocked_wakeonlan.mock_calls) == 1 + assert mocked_wakeonlan.mock_calls[-1][1][0] == mac + assert mocked_wakeonlan.mock_calls[-1][2]["ip_address"] == bc_ip + + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SEND_MAGIC_PACKET, + {"broadcast_address": bc_ip}, + blocking=True, + ) + assert len(mocked_wakeonlan.mock_calls) == 1 + + await hass.services.async_call( + DOMAIN, SERVICE_SEND_MAGIC_PACKET, {"mac": mac}, blocking=True + ) + assert len(mocked_wakeonlan.mock_calls) == 2 + assert mocked_wakeonlan.mock_calls[-1][1][0] == mac + assert not mocked_wakeonlan.mock_calls[-1][2] From 62a3dc1a9405d0c21a02656423beecbe9a32fa32 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Wed, 23 Oct 2019 09:17:34 +0300 Subject: [PATCH 1206/3953] Open Hardware Monitor Sensor reconnect (#28052) * raise PlatformNotReady * Don't show errors on reconnect --- homeassistant/components/openhardwaremonitor/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index fc228ee26fbb3e..0729943a7702a5 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -38,6 +39,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Open Hardware Monitor platform.""" data = OpenHardwareMonitorData(config, hass) + if data.data is None: + raise PlatformNotReady add_entities(data.devices, True) @@ -130,7 +133,7 @@ def refresh(self): response = requests.get(data_url, timeout=30) self.data = response.json() except requests.exceptions.ConnectionError: - _LOGGER.error("ConnectionError: Is OpenHardwareMonitor running?") + _LOGGER.debug("ConnectionError: Is OpenHardwareMonitor running?") def initialize(self, now): """Parse of the sensors and adding of devices.""" From 734704c1f7e934e4863c05c0f432ceb6b56f7df2 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Wed, 23 Oct 2019 09:18:00 +0300 Subject: [PATCH 1207/3953] Squeezebox LMS reconnect (#27378) * Fix * Review --- homeassistant/components/squeezebox/media_player.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 6d67f67a3cece6..d8574223307d50 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -41,6 +41,7 @@ ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -126,18 +127,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Get IP of host, to prevent duplication of same host (different DNS names) try: ipaddr = socket.gethostbyname(host) - except (OSError) as error: + except OSError as error: _LOGGER.error("Could not communicate with %s:%d: %s", host, port, error) - return False + raise PlatformNotReady from error if ipaddr in known_servers: return - known_servers.add(ipaddr) _LOGGER.debug("Creating LMS object for %s", ipaddr) lms = LogitechMediaServer(hass, host, port, username, password) players = await lms.create_players() + if players is None: + raise PlatformNotReady + + known_servers.add(ipaddr) hass.data[DATA_SQUEEZEBOX].extend(players) async_add_entities(players) @@ -194,7 +198,7 @@ async def create_players(self): result = [] data = await self.async_query("players", "status") if data is False: - return result + return None for players in data.get("players_loop", []): player = SqueezeBoxDevice(self, players["playerid"], players["name"]) await player.async_update() From 6025630772786e9f6edd72417abd67529da8b810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Wed, 23 Oct 2019 06:19:00 +0000 Subject: [PATCH 1208/3953] Move imports in melissa component (#28021) * Move imports in melissa component * Fix tox tests --- homeassistant/components/melissa/__init__.py | 5 ++--- tests/components/melissa/test_init.py | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/melissa/__init__.py b/homeassistant/components/melissa/__init__.py index 830036b072a6d2..c03939e3e9c839 100644 --- a/homeassistant/components/melissa/__init__.py +++ b/homeassistant/components/melissa/__init__.py @@ -1,9 +1,10 @@ """Support for Melissa climate.""" import logging +import melissa import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -28,8 +29,6 @@ async def async_setup(hass, config): """Set up the Melissa Climate component.""" - import melissa - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) diff --git a/tests/components/melissa/test_init.py b/tests/components/melissa/test_init.py index dfdaf80981f247..892f4d60a44039 100644 --- a/tests/components/melissa/test_init.py +++ b/tests/components/melissa/test_init.py @@ -1,14 +1,15 @@ """The test for the Melissa Climate component.""" -from tests.common import MockDependency, mock_coro_func - from homeassistant.components import melissa +from tests.common import MockDependency, mock_coro_func + VALID_CONFIG = {"melissa": {"username": "********", "password": "********"}} async def test_setup(hass): """Test setting up the Melissa component.""" with MockDependency("melissa") as mocked_melissa: + melissa.melissa = mocked_melissa mocked_melissa.AsyncMelissa().async_connect = mock_coro_func() await melissa.async_setup(hass, VALID_CONFIG) From acc3646ef3867f110a3695238a69320027995d3f Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Wed, 23 Oct 2019 08:31:43 +0200 Subject: [PATCH 1209/3953] Add Solar-Log platform (#27036) * Add Solar-Log sensor * Codeowners update * Update homeassistant/components/solarlog/manifest.json Co-Authored-By: Paulus Schoutsen * remove sunwatcher from gen_requirements_all.py * remove sunwatcher from requirements_test_all.txt * Remove scan_interval as configuration variable I've set it to a fixed scan_interval of 1 minute. Removed the configuration option. * Fix black format * Config flow added (__init__.py) * Config flow added (manifest.json) * Config flow added (const.py) * Config flow added (config_flow.py) * Config flow added (strings.json) * Config flow added (en.json translation) * Config flow added (sensor.py rewritten) * Config flow added (sensor.py) * Config flow added (config_flows.py) * resolve conflict config_flows.py * Add tests * add tests * add tests * Update .coverage to include all files for solarlog * Fix await the unload * Adjust icons, add http:// to default host * Change icons * Add http:// to host if not provided, fix await * Add http:// to host if not provided, fix await * Adjust tests for http:// added to host * remove line * Remove without http:// requirement * Remove without http;// requirement --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/solarlog/.translations/en.json | 21 +++ homeassistant/components/solarlog/__init__.py | 21 +++ .../components/solarlog/config_flow.py | 107 ++++++++++++ homeassistant/components/solarlog/const.py | 89 ++++++++++ .../components/solarlog/manifest.json | 9 + homeassistant/components/solarlog/sensor.py | 159 ++++++++++++++++++ .../components/solarlog/strings.json | 21 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/solarlog/__init__.py | 1 + tests/components/solarlog/test_config_flow.py | 135 +++++++++++++++ 14 files changed, 572 insertions(+) create mode 100644 homeassistant/components/solarlog/.translations/en.json create mode 100644 homeassistant/components/solarlog/__init__.py create mode 100644 homeassistant/components/solarlog/config_flow.py create mode 100644 homeassistant/components/solarlog/const.py create mode 100644 homeassistant/components/solarlog/manifest.json create mode 100644 homeassistant/components/solarlog/sensor.py create mode 100644 homeassistant/components/solarlog/strings.json create mode 100644 tests/components/solarlog/__init__.py create mode 100644 tests/components/solarlog/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index f40b0c30342ae8..f34b0baac17ef0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -620,6 +620,7 @@ omit = homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py + homeassistant/components/solarlog/* homeassistant/components/solax/sensor.py homeassistant/components/soma/cover.py homeassistant/components/soma/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index e1ff7b36ff154a..a5ad222323b879 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -265,6 +265,7 @@ homeassistant/components/smartthings/* @andrewsayre homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/smtp/* @fabaff homeassistant/components/solaredge_local/* @drobtravels @scheric +homeassistant/components/solarlog/* @Ernst79 homeassistant/components/solax/* @squishykid homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne diff --git a/homeassistant/components/solarlog/.translations/en.json b/homeassistant/components/solarlog/.translations/en.json new file mode 100644 index 00000000000000..5399d5176c9366 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Solar-Log", + "step": { + "user": { + "title": "Define your Solar-Log connection", + "data": { + "host": "The hostname or ip-address of your Solar-Log device", + "name": "The prefix to be used for your Solar-Log sensors" + } + } + }, + "error": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect, please verify host address" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py new file mode 100644 index 00000000000000..c8035e1f7e62df --- /dev/null +++ b/homeassistant/components/solarlog/__init__.py @@ -0,0 +1,21 @@ +"""Solar-Log integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + + +async def async_setup(hass, config): + """Component setup, do nothing.""" + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up a config entry for solarlog.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py new file mode 100644 index 00000000000000..5cb2d5deec1a63 --- /dev/null +++ b/homeassistant/components/solarlog/config_flow.py @@ -0,0 +1,107 @@ +"""Config flow for solarlog integration.""" +import logging +from urllib.parse import ParseResult, urlparse + +from requests.exceptions import HTTPError, Timeout +from sunwatcher.solarlog.solarlog import SolarLog +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import slugify + +from .const import DEFAULT_HOST, DEFAULT_NAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@callback +def solarlog_entries(hass: HomeAssistant): + """Return the hosts already configured.""" + return set( + entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class SolarLogConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for solarlog.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self) -> None: + """Initialize the config flow.""" + self._errors = {} + + def _host_in_configuration_exists(self, host) -> bool: + """Return True if host exists in configuration.""" + if host in solarlog_entries(self.hass): + return True + return False + + async def _test_connection(self, host): + """Check if we can connect to the Solar-Log device.""" + try: + await self.hass.async_add_executor_job(SolarLog, host) + return True + except (OSError, HTTPError, Timeout): + self._errors[CONF_HOST] = "cannot_connect" + _LOGGER.error( + "Could not connect to Solar-Log device at %s, check host ip address", + host, + ) + return False + + async def async_step_user(self, user_input=None): + """Step when user intializes a integration.""" + self._errors = {} + if user_input is not None: + # set some defaults in case we need to return to the form + name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) + host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) + + url = urlparse(host_entry, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + host = url.geturl() + + if self._host_in_configuration_exists(host): + self._errors[CONF_HOST] = "already_configured" + else: + if await self._test_connection(host): + return self.async_create_entry(title=name, data={CONF_HOST: host}) + else: + user_input = {} + user_input[CONF_NAME] = DEFAULT_NAME + user_input[CONF_HOST] = DEFAULT_HOST + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): str, + vol.Required( + CONF_HOST, default=user_input.get(CONF_HOST, DEFAULT_HOST) + ): str, + } + ), + errors=self._errors, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) + + url = urlparse(host_entry, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + host = url.geturl() + + if self._host_in_configuration_exists(host): + return self.async_abort(reason="already_configured") + return await self.async_step_user(user_input) diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py new file mode 100644 index 00000000000000..67eb8006cec90c --- /dev/null +++ b/homeassistant/components/solarlog/const.py @@ -0,0 +1,89 @@ +"""Constants for the Solar-Log integration.""" +from datetime import timedelta + +from homeassistant.const import POWER_WATT, ENERGY_KILO_WATT_HOUR + +DOMAIN = "solarlog" + +"""Default config for solarlog.""" +DEFAULT_HOST = "http://solar-log" +DEFAULT_NAME = "solarlog" + +"""Fixed constants.""" +SCAN_INTERVAL = timedelta(seconds=60) + +"""Supported sensor types.""" +SENSOR_TYPES = { + "time": ["TIME", "last update", None, "mdi:calendar-clock"], + "power_ac": ["powerAC", "power AC", POWER_WATT, "mdi:solar-power"], + "power_dc": ["powerDC", "power DC", POWER_WATT, "mdi:solar-power"], + "voltage_ac": ["voltageAC", "voltage AC", "V", "mdi:flash"], + "voltage_dc": ["voltageDC", "voltage DC", "V", "mdi:flash"], + "yield_day": ["yieldDAY", "yield day", ENERGY_KILO_WATT_HOUR, "mdi:solar-power"], + "yield_yesterday": [ + "yieldYESTERDAY", + "yield yesterday", + ENERGY_KILO_WATT_HOUR, + "mdi:solar-power", + ], + "yield_month": [ + "yieldMONTH", + "yield month", + ENERGY_KILO_WATT_HOUR, + "mdi:solar-power", + ], + "yield_year": ["yieldYEAR", "yield year", ENERGY_KILO_WATT_HOUR, "mdi:solar-power"], + "yield_total": [ + "yieldTOTAL", + "yield total", + ENERGY_KILO_WATT_HOUR, + "mdi:solar-power", + ], + "consumption_ac": ["consumptionAC", "consumption AC", POWER_WATT, "mdi:power-plug"], + "consumption_day": [ + "consumptionDAY", + "consumption day", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_yesterday": [ + "consumptionYESTERDAY", + "consumption yesterday", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_month": [ + "consumptionMONTH", + "consumption month", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_year": [ + "consumptionYEAR", + "consumption year", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_total": [ + "consumptionTOTAL", + "consumption total", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "total_power": ["totalPOWER", "total power", "Wp", "mdi:solar-power"], + "alternator_loss": [ + "alternatorLOSS", + "alternator loss", + POWER_WATT, + "mdi:solar-power", + ], + "capacity": ["CAPACITY", "capacity", "%", "mdi:solar-power"], + "efficiency": ["EFFICIENCY", "efficiency", "% W/Wp", "mdi:solar-power"], + "power_available": [ + "powerAVAILABLE", + "power available", + POWER_WATT, + "mdi:solar-power", + ], + "usage": ["USAGE", "usage", None, "mdi:solar-power"], +} diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json new file mode 100644 index 00000000000000..9331628e027893 --- /dev/null +++ b/homeassistant/components/solarlog/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "solarlog", + "name": "Solar-Log", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integration/solarlog", + "dependencies": [], + "codeowners": ["@Ernst79"], + "requirements": ["sunwatcher==0.2.1"] +} diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py new file mode 100644 index 00000000000000..583529ffe87579 --- /dev/null +++ b/homeassistant/components/solarlog/sensor.py @@ -0,0 +1,159 @@ +"""Platform for solarlog sensors.""" +import logging +from urllib.parse import ParseResult, urlparse + +from requests.exceptions import HTTPError, Timeout +from sunwatcher.solarlog.solarlog import SolarLog +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +from .const import DOMAIN, DEFAULT_HOST, DEFAULT_NAME, SCAN_INTERVAL, SENSOR_TYPES + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import YAML configuration when available.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + ) + ) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Add solarlog entry.""" + host_entry = entry.data[CONF_HOST] + + url = urlparse(host_entry, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + host = url.geturl() + + platform_name = entry.title + + try: + api = await hass.async_add_executor_job(SolarLog, host) + _LOGGER.debug("Connected to Solar-Log device, setting up entries") + except (OSError, HTTPError, Timeout): + _LOGGER.error( + "Could not connect to Solar-Log device at %s, check host ip address", host + ) + return + + # Create solarlog data service which will retrieve and update the data. + data = await hass.async_add_executor_job(SolarlogData, hass, api, host) + + # Create a new sensor for each sensor type. + entities = [] + for sensor_key in SENSOR_TYPES: + sensor = SolarlogSensor(platform_name, sensor_key, data) + entities.append(sensor) + + async_add_entities(entities, True) + return True + + +class SolarlogSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, platform_name, sensor_key, data): + """Initialize the sensor.""" + self.platform_name = platform_name + self.sensor_key = sensor_key + self.data = data + self._state = None + + self._json_key = SENSOR_TYPES[self.sensor_key][0] + self._unit_of_measurement = SENSOR_TYPES[self.sensor_key][2] + + @property + def name(self): + """Return the name of the sensor.""" + return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1]) + + @property + def unit_of_measurement(self): + """Return the state of the sensor.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the sensor icon.""" + return SENSOR_TYPES[self.sensor_key][3] + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data from the sensor and update the state.""" + self.data.update() + self._state = self.data.data[self._json_key] + + +class SolarlogData: + """Get and update the latest data.""" + + def __init__(self, hass, api, host): + """Initialize the data object.""" + self.api = api + self.hass = hass + self.host = host + self.update = Throttle(SCAN_INTERVAL)(self._update) + self.data = {} + + def _update(self): + """Update the data from the SolarLog device.""" + try: + self.api = SolarLog(self.host) + response = self.api.time + _LOGGER.debug( + "Connection to Solarlog successful. Retrieving latest Solarlog update of %s", + response, + ) + except (OSError, Timeout, HTTPError): + _LOGGER.error("Connection error, Could not retrieve data, skipping update") + return + + try: + self.data["TIME"] = self.api.time + self.data["powerAC"] = self.api.power_ac + self.data["powerDC"] = self.api.power_dc + self.data["voltageAC"] = self.api.voltage_ac + self.data["voltageDC"] = self.api.voltage_dc + self.data["yieldDAY"] = self.api.yield_day / 1000 + self.data["yieldYESTERDAY"] = self.api.yield_yesterday / 1000 + self.data["yieldMONTH"] = self.api.yield_month / 1000 + self.data["yieldYEAR"] = self.api.yield_year / 1000 + self.data["yieldTOTAL"] = self.api.yield_total / 1000 + self.data["consumptionAC"] = self.api.consumption_ac + self.data["consumptionDAY"] = self.api.consumption_day / 1000 + self.data["consumptionYESTERDAY"] = self.api.consumption_yesterday / 1000 + self.data["consumptionMONTH"] = self.api.consumption_month / 1000 + self.data["consumptionYEAR"] = self.api.consumption_year / 1000 + self.data["consumptionTOTAL"] = self.api.consumption_total / 1000 + self.data["totalPOWER"] = self.api.total_power + self.data["alternatorLOSS"] = self.api.alternator_loss + self.data["CAPACITY"] = round(self.api.capacity * 100, 0) + self.data["EFFICIENCY"] = round(self.api.efficiency * 100, 0) + self.data["powerAVAILABLE"] = self.api.power_available + self.data["USAGE"] = self.api.usage + _LOGGER.debug("Updated Solarlog overview data: %s", self.data) + except AttributeError: + _LOGGER.error("Missing details data in Solarlog response") diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json new file mode 100644 index 00000000000000..5399d5176c9366 --- /dev/null +++ b/homeassistant/components/solarlog/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Solar-Log", + "step": { + "user": { + "title": "Define your Solar-Log connection", + "data": { + "host": "The hostname or ip-address of your Solar-Log device", + "name": "The prefix to be used for your Solar-Log sensors" + } + } + }, + "error": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect, please verify host address" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 60aa610ec073f4..bf63869bc9b48e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -60,6 +60,7 @@ "smartthings", "smhi", "solaredge", + "solarlog", "soma", "somfy", "sonos", diff --git a/requirements_all.txt b/requirements_all.txt index 884155f3b4c6e8..868855ed8513e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1842,6 +1842,9 @@ stringcase==1.2.0 # homeassistant.components.ecovacs sucks==0.9.4 +# homeassistant.components.solarlog +sunwatcher==0.2.1 + # homeassistant.components.swiss_hydrological_data swisshydrodata==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71cbac4de0f16b..2fd91e21d967e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -583,6 +583,9 @@ statsd==3.2.1 # homeassistant.components.traccar stringcase==1.2.0 +# homeassistant.components.solarlog +sunwatcher==0.2.1 + # homeassistant.components.tellduslive tellduslive==0.10.10 diff --git a/tests/components/solarlog/__init__.py b/tests/components/solarlog/__init__.py new file mode 100644 index 00000000000000..9074cab84165b8 --- /dev/null +++ b/tests/components/solarlog/__init__.py @@ -0,0 +1 @@ +"""Tests for the solarlog integration.""" diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py new file mode 100644 index 00000000000000..86f3b05d9755d0 --- /dev/null +++ b/tests/components/solarlog/test_config_flow.py @@ -0,0 +1,135 @@ +"""Test the solarlog config flow.""" +from unittest.mock import patch +import pytest + +from homeassistant import data_entry_flow +from homeassistant import config_entries, setup +from homeassistant.components.solarlog import config_flow +from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME + +from tests.common import MockConfigEntry, mock_coro + +NAME = "Solarlog test 1 2 3" +HOST = "http://1.1.1.1" + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", + return_value=mock_coro({"title": "solarlog test 1 2 3"}), + ), patch( + "homeassistant.components.solarlog.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.solarlog.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": HOST, "name": NAME} + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "solarlog_test_1_2_3" + assert result2["data"] == {"host": "http://1.1.1.1"} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.fixture(name="test_connect") +def mock_controller(): + """Mock a successfull _host_in_configuration_exists.""" + with patch( + "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", + side_effect=lambda *_: mock_coro(True), + ): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.SolarLogConfigFlow() + flow.hass = hass + return flow + + +async def test_user(hass, test_connect): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # tets with all provided + result = await flow.async_step_user({CONF_NAME: NAME, CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + + +async def test_import(hass, test_connect): + """Test import step.""" + flow = init_config_flow(hass) + + # import with only host + result = await flow.async_step_import({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog" + assert result["data"][CONF_HOST] == HOST + + # import with only name + result = await flow.async_step_import({CONF_NAME: NAME}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == DEFAULT_HOST + + # import with host and name + result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + + +async def test_abort_if_already_setup(hass, test_connect): + """Test we abort if the device is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain="solarlog", data={CONF_NAME: NAME, CONF_HOST: HOST} + ).add_to_hass(hass) + + # Should fail, same HOST different NAME (default) + result = await flow.async_step_import( + {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same HOST and NAME + result = await flow.async_step_user({CONF_HOST: HOST, CONF_NAME: NAME}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "already_configured"} + + # SHOULD pass, diff HOST (without http://), different NAME + result = await flow.async_step_import( + {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_7_8_9" + assert result["data"][CONF_HOST] == "http://2.2.2.2" + + # SHOULD pass, diff HOST, same NAME + result = await flow.async_step_import( + {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == "http://2.2.2.2" From a644182b5e200296c97656b038e941e3b9f8b87d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 23 Oct 2019 01:32:57 -0500 Subject: [PATCH 1210/3953] Save client identifier from Plex auth for future use (#27951) * Save client identifier from auth for future use * Bump requirements * Stick with version 1 --- homeassistant/components/plex/config_flow.py | 5 +++++ homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/manifest.json | 2 +- homeassistant/components/plex/server.py | 9 +++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index a11fb9119a68cd..c03b958b2da0d6 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -19,6 +19,7 @@ from .const import ( # pylint: disable=unused-import AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, + CONF_CLIENT_IDENTIFIER, CONF_SERVER, CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, @@ -65,6 +66,7 @@ def __init__(self): self.available_servers = None self.plexauth = None self.token = None + self.client_id = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -116,6 +118,8 @@ async def async_step_server_validate(self, server_config): token = server_config.get(CONF_TOKEN) entry_config = {CONF_URL: url} + if self.client_id: + entry_config[CONF_CLIENT_IDENTIFIER] = self.client_id if token: entry_config[CONF_TOKEN] = token if url.startswith("https"): @@ -216,6 +220,7 @@ async def async_step_obtain_token(self, user_input=None): return self.async_external_step_done(next_step_id="timed_out") self.token = token + self.client_id = self.plexauth.client_identifier return self.async_external_step_done(next_step_id="use_external_token") async def async_step_timed_out(self, user_input=None): diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index d3a3a866361469..0d512101e119c7 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -21,6 +21,7 @@ PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" +CONF_CLIENT_IDENTIFIER = "client_id" CONF_SERVER = "server" CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index d4f2ae0517a296..3c570a0e64cf1d 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ "plexapi==3.0.6", - "plexauth==0.0.4" + "plexauth==0.0.5" ], "dependencies": [ "http" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d7825ae82c3d60..c0461ee0f54e3b 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -12,6 +12,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( + CONF_CLIENT_IDENTIFIER, CONF_SERVER, CONF_SHOW_ALL_CONTROLS, CONF_USE_EPISODE_ART, @@ -33,8 +34,6 @@ plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT plexapi.X_PLEX_VERSION = X_PLEX_VERSION -plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers() -plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() class PlexServer: @@ -52,6 +51,12 @@ def __init__(self, hass, server_config, options=None): self.options = options self.server_choice = None + # Header conditionally added as it is not available in config entry v1 + if CONF_CLIENT_IDENTIFIER in server_config: + plexapi.X_PLEX_IDENTIFIER = server_config[CONF_CLIENT_IDENTIFIER] + plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers() + plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() + def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" diff --git a/requirements_all.txt b/requirements_all.txt index 868855ed8513e0..207b955f2494f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -971,7 +971,7 @@ pizzapi==0.0.3 plexapi==3.0.6 # homeassistant.components.plex -plexauth==0.0.4 +plexauth==0.0.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2fd91e21d967e6..e61732e632ba52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,7 +343,7 @@ pillow==6.2.0 plexapi==3.0.6 # homeassistant.components.plex -plexauth==0.0.4 +plexauth==0.0.5 # homeassistant.components.mhz19 # homeassistant.components.serial_pm From 44bf9e9ddc74458887a0f1376f793a697421e5ed Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 23 Oct 2019 01:34:12 -0500 Subject: [PATCH 1211/3953] Additional SSL validation checks for cert_expiry (#28047) * Additional SSL validation checks * Add validity attribute, log errors on import * Don't log from sensor --- .../components/cert_expiry/config_flow.py | 21 ++++++++++++++++--- .../components/cert_expiry/sensor.py | 20 +++++++++++++----- .../components/cert_expiry/strings.json | 3 ++- .../cert_expiry/test_config_flow.py | 20 ++++++++++++++++-- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 43931fe5830f8f..78450d247b9886 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -1,5 +1,7 @@ """Config flow for the Cert Expiry platform.""" +import logging import socket +import ssl import voluptuous as vol from homeassistant import config_entries @@ -9,6 +11,8 @@ from .const import DOMAIN, DEFAULT_PORT, DEFAULT_NAME from .helper import get_cert +_LOGGER = logging.getLogger(__name__) + @callback def certexpiry_entries(hass: HomeAssistant): @@ -39,17 +43,28 @@ def _prt_in_configuration_exists(self, user_input) -> bool: async def _test_connection(self, user_input=None): """Test connection to the server and try to get the certtificate.""" + host = user_input[CONF_HOST] try: await self.hass.async_add_executor_job( - get_cert, user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT) + get_cert, host, user_input.get(CONF_PORT, DEFAULT_PORT) ) return True except socket.gaierror: + _LOGGER.error("Host cannot be resolved: %s", host) self._errors[CONF_HOST] = "resolve_failed" except socket.timeout: + _LOGGER.error("Timed out connecting to %s", host) self._errors[CONF_HOST] = "connection_timeout" - except OSError: - self._errors[CONF_HOST] = "certificate_fetch_failed" + except ssl.CertificateError as err: + if "doesn't match" in err.args[0]: + _LOGGER.error("Certificate does not match host: %s", host) + self._errors[CONF_HOST] = "wrong_host" + else: + _LOGGER.error("Certificate could not be validated: %s", host) + self._errors[CONF_HOST] = "certificate_error" + except ssl.SSLError: + _LOGGER.error("Certificate could not be validated: %s", host) + self._errors[CONF_HOST] = "certificate_error" return False async def async_step_user(self, user_input=None): diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 2d578ef2c3b72f..3022c7bd42ba1e 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -70,6 +70,7 @@ def __init__(self, sensor_name, server_name, server_port): self._name = sensor_name self._state = None self._available = False + self._valid = False @property def name(self): @@ -122,16 +123,17 @@ def update(self): except socket.gaierror: _LOGGER.error("Cannot resolve hostname: %s", self.server_name) self._available = False + self._valid = False return except socket.timeout: _LOGGER.error("Connection timeout with server: %s", self.server_name) self._available = False + self._valid = False return - except OSError: - _LOGGER.error( - "Cannot fetch certificate from %s", self.server_name, exc_info=1 - ) - self._available = False + except (ssl.CertificateError, ssl.SSLError): + self._available = True + self._state = 0 + self._valid = False return ts_seconds = ssl.cert_time_to_seconds(cert["notAfter"]) @@ -139,3 +141,11 @@ def update(self): expiry = timestamp - datetime.today() self._available = True self._state = expiry.days + self._valid = True + + @property + def device_state_attributes(self): + """Return additional sensor state attributes.""" + attr = {"is_valid": self._valid} + + return attr diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json index 3e2fea2342e260..e5e670d214fc4a 100644 --- a/homeassistant/components/cert_expiry/strings.json +++ b/homeassistant/components/cert_expiry/strings.json @@ -15,7 +15,8 @@ "host_port_exists": "This host and port combination is already configured", "resolve_failed": "This host can not be resolved", "connection_timeout": "Timeout when connecting to this host", - "certificate_fetch_failed": "Can not fetch certificate from this host and port combination" + "certificate_error": "Certificate could not be validated", + "wrong_host": "Certificate does not match hostname" }, "abort": { "host_port_exists": "This host and port combination is already configured" diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index 988f3e971060ac..3754551c230123 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the Cert Expiry config flow.""" import pytest +import ssl import socket from unittest.mock import patch @@ -131,7 +132,22 @@ async def test_abort_on_socket_failed(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {CONF_HOST: "connection_timeout"} - with patch("socket.create_connection", side_effect=OSError()): + with patch( + "socket.create_connection", + side_effect=ssl.CertificateError(f"{HOST} doesn't match somethingelse.com"), + ): + result = await flow.async_step_user({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "wrong_host"} + + with patch( + "socket.create_connection", side_effect=ssl.CertificateError("different error") + ): + result = await flow.async_step_user({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "certificate_error"} + + with patch("socket.create_connection", side_effect=ssl.SSLError()): result = await flow.async_step_user({CONF_HOST: HOST}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_HOST: "certificate_fetch_failed"} + assert result["errors"] == {CONF_HOST: "certificate_error"} From 852cbad965981531c443580631e248312d2244b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per-=C3=98yvind=20Bruun?= Date: Wed, 23 Oct 2019 09:32:14 +0200 Subject: [PATCH 1212/3953] New platform for Microsoft Teams (#27981) * New Microsoft Teams notification service * Updated codeowners * Updated requirements_all * Changed from WEBHOOK_ID to URL * Moved try/except block --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/msteams/__init__.py | 1 + .../components/msteams/manifest.json | 8 +++ homeassistant/components/msteams/notify.py | 67 +++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 81 insertions(+) create mode 100644 homeassistant/components/msteams/__init__.py create mode 100644 homeassistant/components/msteams/manifest.json create mode 100644 homeassistant/components/msteams/notify.py diff --git a/.coveragerc b/.coveragerc index f34b0baac17ef0..83e6971cc6a426 100644 --- a/.coveragerc +++ b/.coveragerc @@ -418,6 +418,7 @@ omit = homeassistant/components/mpchc/media_player.py homeassistant/components/mpd/media_player.py homeassistant/components/mqtt_room/sensor.py + homeassistant/components/msteams/notify.py homeassistant/components/mvglive/sensor.py homeassistant/components/mychevy/* homeassistant/components/mycroft/* diff --git a/CODEOWNERS b/CODEOWNERS index a5ad222323b879..eb29ee28915738 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -190,6 +190,7 @@ homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core +homeassistant/components/msteams/* @peroyvind homeassistant/components/mysensors/* @MartinHjelmare homeassistant/components/mystrom/* @fabaff homeassistant/components/neato/* @dshokouhi @Santobert diff --git a/homeassistant/components/msteams/__init__.py b/homeassistant/components/msteams/__init__.py new file mode 100644 index 00000000000000..42423887fa6b46 --- /dev/null +++ b/homeassistant/components/msteams/__init__.py @@ -0,0 +1 @@ +"""The Microsoft Teams component.""" diff --git a/homeassistant/components/msteams/manifest.json b/homeassistant/components/msteams/manifest.json new file mode 100644 index 00000000000000..f907cf570bb9cc --- /dev/null +++ b/homeassistant/components/msteams/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "msteams", + "name": "Microsoft Teams", + "documentation": "https://www.home-assistant.io/integrations/msteams", + "requirements": ["pymsteams==0.1.12"], + "dependencies": [], + "codeowners": ["@peroyvind"] +} diff --git a/homeassistant/components/msteams/notify.py b/homeassistant/components/msteams/notify.py new file mode 100644 index 00000000000000..c986f1d2363a4f --- /dev/null +++ b/homeassistant/components/msteams/notify.py @@ -0,0 +1,67 @@ +"""Microsoft Teams platform for notify component.""" +import logging + +import pymsteams +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.const import CONF_URL +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +ATTR_FILE_URL = "image_url" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_URL): cv.url}) + + +def get_service(hass, config, discovery_info=None): + """Get the Microsoft Teams notification service.""" + webhook_url = config.get(CONF_URL) + + try: + return MSTeamsNotificationService(webhook_url) + + except RuntimeError as err: + _LOGGER.exception("Error in creating a new Microsoft Teams message: %s", err) + return None + + +class MSTeamsNotificationService(BaseNotificationService): + """Implement the notification service for Microsoft Teams.""" + + def __init__(self, webhook_url): + """Initialize the service.""" + self._webhook_url = webhook_url + self.teams_message = pymsteams.connectorcard(self._webhook_url) + + def send_message(self, message=None, **kwargs): + """Send a message to the webhook.""" + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + data = kwargs.get(ATTR_DATA) + + self.teams_message.title(title) + + self.teams_message.text(message) + + if data is not None: + file_url = data.get(ATTR_FILE_URL) + + if file_url is not None: + if not file_url.startswith("http"): + _LOGGER.error("URL should start with http or https") + return + + message_section = pymsteams.cardsection() + message_section.addImage(file_url) + self.teams_message.addSection(message_section) + try: + self.teams_message.send() + except RuntimeError as err: + _LOGGER.error("Could not send notification. Error: %s", err) diff --git a/requirements_all.txt b/requirements_all.txt index 207b955f2494f1..8f22d229f56110 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1318,6 +1318,9 @@ pymodbus==1.5.2 # homeassistant.components.monoprice pymonoprice==0.3 +# homeassistant.components.msteams +pymsteams==0.1.12 + # homeassistant.components.yamaha_musiccast pymusiccast==0.1.6 From 09b322b8a4d6053d1348b3fd172d30723a608519 Mon Sep 17 00:00:00 2001 From: rolfberkenbosch <30292281+rolfberkenbosch@users.noreply.github.com> Date: Wed, 23 Oct 2019 15:49:47 +0200 Subject: [PATCH 1213/3953] Fix issues with new tile 2020 devices (#28133) * Update meteoalertapi to version 0.1.6 * Fix tile to supporting 2020 tile devices --- homeassistant/components/tile/device_tracker.py | 8 +++++--- homeassistant/components/tile/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index e8ed5b06d27602..924fa913d30993 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -43,7 +43,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Tile scanner.""" - from pytile import Client + from pytile import async_login websession = aiohttp_client.async_get_clientsession(hass) @@ -52,14 +52,16 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): ) config_data = await hass.async_add_job(load_json, config_file) if config_data: - client = Client( + client = await async_login( config[CONF_USERNAME], config[CONF_PASSWORD], websession, client_uuid=config_data["client_uuid"], ) else: - client = Client(config[CONF_USERNAME], config[CONF_PASSWORD], websession) + client = await async_login( + config[CONF_USERNAME], config[CONF_PASSWORD], websession + ) config_data = {"client_uuid": client.client_uuid} await hass.async_add_job(save_json, config_file, config_data) diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 5e40c89369ac14..0dd0b70ef520c5 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "documentation": "https://www.home-assistant.io/integrations/tile", "requirements": [ - "pytile==2.0.6" + "pytile==3.0.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8f22d229f56110..cbe4a93f812c39 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1600,7 +1600,7 @@ python_opendata_transport==0.1.4 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==2.0.6 +pytile==3.0.0 # homeassistant.components.touchline pytouchline==0.7 From b1a374062beca1fee0f3b14a0b81881a7a4ab239 Mon Sep 17 00:00:00 2001 From: Alain Turbide <7193213+Dilbert66@users.noreply.github.com> Date: Wed, 23 Oct 2019 11:28:23 -0400 Subject: [PATCH 1214/3953] Add Alexa.ChannelController functions for media players (#27671) * Added missing Alexa.ChannelController functions. Specifically ChangeChannel and SkipChannel commands. These functions will call the play_media function in a media_player app if it has the capability published and pass on the channel# or channel name. The selected media player can then use this to select the channel on the device it is associated to. Modified the existing Alexa.StepSpeaker Setvolume function to actually do a stepped volume change using the steps sent by Alexa. The Alexa default step of 10 for a simple volume up/down can be changed via an exposed media_player attribute called volume_step_default. The default is set to 1. Any other value then default will be sent as sequential volume up /down to the media_player. * The test code has some weird behaviour with passed boolean values. Had to surround them in quotes for the tests to pass properly. * Reverted test_smart_home.py change. Issue was not the boolean value but the behavior in the handler. The test suite does not like multiple await calls in a loop. Will investigate further. The handler code works though. * Added ChannelController/SkipChannels test in test_smart_home.py Added test for callSign payload attribute. * Modified smart home test to allow more than one call to services * Added more tests for ChannelChange functions for various payload options. Removed name options from metadata payload section. not needed. * Reverted assert call change in alexa test __init__.py back to ==1. Not sure if it was the cause of the pytest's failing on github * Corrected a comment. First commit after a rebase. * Comment line change. Also wanted to force a code check on github. * Added a loop delay in StepSpeaker and SkipChannel functions for safety * Removed uneeded sleep from for loops. Let remote handle delays Moved service type decision out of for loops in ChannelController and StepSpeaker Used constants instead of numeric values for support options in test module * Change media_player const import to be more specific in source * Modifed test_smart_home to use media_play constants instead of hardcode valu * Removed unecessary test volume_step_default attribute from test_smart_home * Removed uneeded comment in StepSpeaker function. Re-ordered constants in test_smart_home.py * Modified call to media_player play_media service to use media_player constant instead of hard coded value. * Changed constant use to be consistant with rest of function. * Correct merge conflicts in handlers.py and capablities.py --- .../components/alexa/capabilities.py | 11 ++ homeassistant/components/alexa/entities.py | 4 + homeassistant/components/alexa/handlers.py | 106 ++++++++++++++++-- tests/components/alexa/test_smart_home.py | 93 ++++++++++++++- 4 files changed, 201 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index f4d930266495ea..246429ad6c9b74 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1039,3 +1039,14 @@ def capability_resources(self): ] return capability_resources + + +class AlexaChannelController(AlexaCapability): + """Implements Alexa.ChannelController. + + https://developer.amazon.com/docs/device-apis/alexa-channelcontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ChannelController" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index dd640aed0a62eb..f6fc9936a024e8 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -34,6 +34,7 @@ from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES from .capabilities import ( AlexaBrightnessController, + AlexaChannelController, AlexaColorController, AlexaColorTemperatureController, AlexaContactSensor, @@ -420,6 +421,9 @@ def interfaces(self): if supported & media_player.SUPPORT_SELECT_SOURCE: yield AlexaInputController(self.entity) + if supported & media_player.const.SUPPORT_PLAY_MEDIA: + yield AlexaChannelController(self.entity) + @ENTITY_ADAPTERS.register(scene.DOMAIN) class SceneCapabilities(AlexaEntity): diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 64feacb92f5da2..331990dc4a44ba 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -521,20 +521,28 @@ async def async_api_adjust_volume_step(hass, config, directive, context): """Process an adjust volume step request.""" # media_player volume up/down service does not support specifying steps # each component handles it differently e.g. via config. - # For now we use the volumeSteps returned to figure out if we - # should step up/down - volume_step = directive.payload["volumeSteps"] + # This workaround will simply call the volume up/Volume down the amount of steps asked for + # When no steps are called in the request, Alexa sends a default of 10 steps which for most + # purposes is too high. The default is set 1 in this case. entity = directive.entity + volume_int = int(directive.payload["volumeSteps"]) + is_default = bool(directive.payload["volumeStepsDefault"]) + default_steps = 1 + + if volume_int < 0: + service_volume = SERVICE_VOLUME_DOWN + if is_default: + volume_int = -default_steps + else: + service_volume = SERVICE_VOLUME_UP + if is_default: + volume_int = default_steps data = {ATTR_ENTITY_ID: entity.entity_id} - if volume_step > 0: - await hass.services.async_call( - entity.domain, SERVICE_VOLUME_UP, data, blocking=False, context=context - ) - elif volume_step < 0: + for _ in range(0, abs(volume_int)): await hass.services.async_call( - entity.domain, SERVICE_VOLUME_DOWN, data, blocking=False, context=context + entity.domain, service_volume, data, blocking=False, context=context ) return directive.response() @@ -546,7 +554,6 @@ async def async_api_set_mute(hass, config, directive, context): """Process a set mute request.""" mute = bool(directive.payload["mute"]) entity = directive.entity - data = { ATTR_ENTITY_ID: entity.entity_id, media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute, @@ -1082,3 +1089,82 @@ async def async_api_adjust_range(hass, config, directive, context): ) return directive.response() + + +@HANDLERS.register(("Alexa.ChannelController", "ChangeChannel")) +async def async_api_changechannel(hass, config, directive, context): + """Process a change channel request.""" + channel = "0" + entity = directive.entity + payload = directive.payload["channel"] + payload_name = "number" + + if "number" in payload: + channel = payload["number"] + payload_name = "number" + elif "callSign" in payload: + channel = payload["callSign"] + payload_name = "callSign" + elif "affiliateCallSign" in payload: + channel = payload["affiliateCallSign"] + payload_name = "affiliateCallSign" + elif "uri" in payload: + channel = payload["uri"] + payload_name = "uri" + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.const.ATTR_MEDIA_CONTENT_ID: channel, + media_player.const.ATTR_MEDIA_CONTENT_TYPE: media_player.const.MEDIA_TYPE_CHANNEL, + } + + await hass.services.async_call( + entity.domain, + media_player.const.SERVICE_PLAY_MEDIA, + data, + blocking=False, + context=context, + ) + + response = directive.response() + + response.add_context_property( + { + "namespace": "Alexa.ChannelController", + "name": "channel", + "value": {payload_name: channel}, + } + ) + + return response + + +@HANDLERS.register(("Alexa.ChannelController", "SkipChannels")) +async def async_api_skipchannel(hass, config, directive, context): + """Process a skipchannel request.""" + channel = int(directive.payload["channelCount"]) + entity = directive.entity + + data = {ATTR_ENTITY_ID: entity.entity_id} + + if channel < 0: + service_media = SERVICE_MEDIA_PREVIOUS_TRACK + else: + service_media = SERVICE_MEDIA_NEXT_TRACK + + for _ in range(0, abs(channel)): + await hass.services.async_call( + entity.domain, service_media, data, blocking=False, context=context + ) + + response = directive.response() + + response.add_context_property( + { + "namespace": "Alexa.ChannelController", + "name": "channel", + "value": {"number": ""}, + } + ) + + return response diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 5a39036a30f611..139c8c9740ba00 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -4,6 +4,19 @@ from homeassistant.core import Context, callback from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.components.alexa import smart_home, messages +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) from homeassistant.helpers import entityfilter from tests.common import async_mock_service @@ -693,7 +706,17 @@ async def test_media_player(hass): "off", { "friendly_name": "Test media player", - "supported_features": 0x59BD, + "supported_features": SUPPORT_NEXT_TRACK + | SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_SELECT_SOURCE + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET, "volume_level": 0.75, }, ) @@ -711,6 +734,7 @@ async def test_media_player(hass): "Alexa.StepSpeaker", "Alexa.PlaybackController", "Alexa.EndpointHealth", + "Alexa.ChannelController", ) await assert_power_controller_works( @@ -824,7 +848,7 @@ async def test_media_player(hass): "media_player#test", "media_player.volume_up", hass, - payload={"volumeSteps": 20}, + payload={"volumeSteps": 1, "volumeStepsDefault": False}, ) call, _ = await assert_request_calls_service( @@ -833,7 +857,69 @@ async def test_media_player(hass): "media_player#test", "media_player.volume_down", hass, - payload={"volumeSteps": -20}, + payload={"volumeSteps": -1, "volumeStepsDefault": False}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.StepSpeaker", + "AdjustVolume", + "media_player#test", + "media_player.volume_up", + hass, + payload={"volumeSteps": 10, "volumeStepsDefault": True}, + ) + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"number": 24}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"callSign": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"affiliateCallSign": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"uri": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "SkipChannels", + "media_player#test", + "media_player.media_next_track", + hass, + payload={"channelCount": 1}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "SkipChannels", + "media_player#test", + "media_player.media_previous_track", + hass, + payload={"channelCount": -1}, ) @@ -862,6 +948,7 @@ async def test_media_player_power(hass): "Alexa.StepSpeaker", "Alexa.PlaybackController", "Alexa.EndpointHealth", + "Alexa.ChannelController", ) await assert_request_calls_service( From 14be60e5bf70d92a8e8a9555e9a0ce42b3cb8318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Wed, 23 Oct 2019 15:30:38 +0000 Subject: [PATCH 1215/3953] Move imports in nuheat component (#28038) * Move imports in nuheat component * Fix tox tests * Fix tox tests * Update tests/components/nuheat/test_init.py @Balloob suggested the change because direct replacement, the mock would never be reverted and impact the other tests. Co-Authored-By: Paulus Schoutsen --- homeassistant/components/nuheat/__init__.py | 8 +++----- homeassistant/components/nuheat/climate.py | 2 +- tests/components/nuheat/test_init.py | 9 +++++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index f83611d3e40657..88e10270d18f4d 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -1,11 +1,11 @@ """Support for NuHeat thermostats.""" import logging +import nuheat import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv, discovery _LOGGER = logging.getLogger(__name__) @@ -29,8 +29,6 @@ def setup(hass, config): """Set up the NuHeat thermostat component.""" - import nuheat - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 19780a35a20934..5a4e4e233d1684 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -9,9 +9,9 @@ HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/nuheat/test_init.py b/tests/components/nuheat/test_init.py index a6be3ac14f58e1..90a209fd897a6b 100644 --- a/tests/components/nuheat/test_init.py +++ b/tests/components/nuheat/test_init.py @@ -1,11 +1,11 @@ """NuHeat component tests.""" import unittest - from unittest.mock import patch -from tests.common import get_test_home_assistant, MockDependency from homeassistant.components import nuheat +from tests.common import MockDependency, get_test_home_assistant + VALID_CONFIG = { "nuheat": {"username": "warm", "password": "feet", "devices": "thermostat123"} } @@ -27,11 +27,12 @@ def tearDown(self): # pylint: disable=invalid-name @patch("homeassistant.helpers.discovery.load_platform") def test_setup(self, mocked_nuheat, mocked_load): """Test setting up the NuHeat component.""" - nuheat.setup(self.hass, self.config) + with patch.object(nuheat, "nuheat", mocked_nuheat): + nuheat.setup(self.hass, self.config) mocked_nuheat.NuHeat.assert_called_with("warm", "feet") assert nuheat.DOMAIN in self.hass.data - assert 2 == len(self.hass.data[nuheat.DOMAIN]) + assert len(self.hass.data[nuheat.DOMAIN]) == 2 assert isinstance( self.hass.data[nuheat.DOMAIN][0], type(mocked_nuheat.NuHeat()) ) From 4d8539e15128d97b5872d384168052b5ff94b2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Wed, 23 Oct 2019 15:53:05 +0000 Subject: [PATCH 1216/3953] Move imports in raspihats component (#28088) * Move imports in raspihats component * Comment require * Disable pylint for import-error * Revert move imports * Remove unnecessary pylint disable error * Update homeassistant/components/raspihats/__init__.py Co-Authored-By: cgtobi * Apply suggestions from code review Co-Authored-By: cgtobi --- .../components/raspihats/__init__.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py index 963c2624362d6e..8b7ea0a38d75a3 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -120,7 +120,9 @@ def register_board(self, board, address): with self._lock: i2c_hat = self._i2c_hats.get(address) if i2c_hat is None: - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error import raspihats.i2c_hats as module constructor = getattr(module, board) @@ -138,7 +140,9 @@ def register_board(self, board, address): def run(self): """Keep alive for I2C-HATs.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException _LOGGER.info(log_message(self, "starting")) @@ -199,7 +203,9 @@ def register_online_callback(self, address, channel, callback): def read_di(self, address, channel): """Read a value from a I2C-HAT digital input.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException with self._lock: @@ -212,7 +218,9 @@ def read_di(self, address, channel): def write_dq(self, address, channel, value): """Write a value to a I2C-HAT digital output.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException with self._lock: @@ -224,7 +232,9 @@ def write_dq(self, address, channel, value): def read_dq(self, address, channel): """Read a value from a I2C-HAT digital output.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException with self._lock: From 7431e2675232e8c84e7aee681aeed45723f5162e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 23 Oct 2019 18:57:51 +0300 Subject: [PATCH 1217/3953] Round system monitor load averages to 2 decimal digits (#27558) On a Raspberry Pi 3, Python 3.7.4: # python3 -c "import os; print(os.getloadavg())" (0.2724609375, 0.3505859375, 0.3515625) --- homeassistant/components/systemmonitor/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index b4621c59798625..53c5c104cd15bf 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -218,8 +218,8 @@ def update(self): dt_util.utc_from_timestamp(psutil.boot_time()) ).isoformat() elif self.type == "load_1m": - self._state = os.getloadavg()[0] + self._state = round(os.getloadavg()[0], 2) elif self.type == "load_5m": - self._state = os.getloadavg()[1] + self._state = round(os.getloadavg()[1], 2) elif self.type == "load_15m": - self._state = os.getloadavg()[2] + self._state = round(os.getloadavg()[2], 2) From efae75103ac124f5391c435328999ac171c13702 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Wed, 23 Oct 2019 18:21:49 +0200 Subject: [PATCH 1218/3953] Cleanup typing and asserts for HomematicIP Cloud (#28144) * Cleanup assert in Homematic IP Cloud Tests * Cleanup typing --- .../components/homematicip_cloud/__init__.py | 7 ++-- .../homematicip_cloud/alarm_control_panel.py | 4 +-- .../homematicip_cloud/binary_sensor.py | 4 +-- .../components/homematicip_cloud/climate.py | 4 +-- .../homematicip_cloud/config_flow.py | 5 +-- .../components/homematicip_cloud/cover.py | 4 +-- .../components/homematicip_cloud/device.py | 6 ++-- .../components/homematicip_cloud/hap.py | 9 +++--- .../components/homematicip_cloud/light.py | 4 +-- .../components/homematicip_cloud/sensor.py | 4 +-- .../components/homematicip_cloud/switch.py | 4 +-- .../components/homematicip_cloud/weather.py | 4 +-- .../components/homematicip_cloud/conftest.py | 32 +++++++++++-------- .../components/homematicip_cloud/test_hap.py | 22 ++++++------- 14 files changed, 59 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 9a3191ac1686cd..9a0eb65aa3fd1b 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -7,11 +7,10 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME -from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import comp_entity_ids -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .config_flow import configured_haps from .const import ( @@ -98,7 +97,7 @@ ) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" hass.data[DOMAIN] = {} @@ -252,7 +251,7 @@ def _get_home(hapid: str): return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up an access point from a config entry.""" hap = HomematicipHAP(hass, entry) hapid = entry.data[HMIPC_HAPID].replace("-", "").upper() diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 653c1ae147bc03..f61bf6f6b56d40 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -12,7 +12,7 @@ STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID from .hap import HomematicipHAP @@ -28,7 +28,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 964ab4d823420d..e308f96c20896f 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -36,7 +36,7 @@ BinarySensorDevice, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_GROUP_MEMBER_UNREACHABLE @@ -82,7 +82,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index f1f414169f672d..74d647c8c33613 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -20,7 +20,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .hap import HomematicipHAP @@ -41,7 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP climate from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index a94ea7b53f147a..1488f02f13b0c5 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -4,7 +4,8 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback +from homeassistant.helpers.typing import HomeAssistantType from .const import ( _LOGGER, @@ -18,7 +19,7 @@ @callback -def configured_haps(hass: HomeAssistant) -> Set[str]: +def configured_haps(hass: HomeAssistantType) -> Set[str]: """Return a set of the configured access points.""" return set( entry.data[HMIPC_HAPID] diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index c5821c4f75e4ec..63ac6f7310cbba 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -10,7 +10,7 @@ CoverDevice, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -28,7 +28,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP cover from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 3d64014883d131..b05c0e0692852d 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -5,11 +5,11 @@ from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup -from homeassistant.components import homematicip_cloud from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import Entity +from .const import DOMAIN as HMIPC_DOMAIN from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -70,13 +70,13 @@ def device_info(self): return { "identifiers": { # Serial numbers of Homematic IP device - (homematicip_cloud.DOMAIN, self._device.id) + (HMIPC_DOMAIN, self._device.id) }, "name": self._device.label, "manufacturer": self._device.oem, "model": self._device.modelType, "sw_version": self._device.firmwareVersion, - "via_device": (homematicip_cloud.DOMAIN, self._device.homeId), + "via_device": (HMIPC_DOMAIN, self._device.homeId), } return None diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 64fbd4fd0799d8..bef04180c6f09b 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -8,9 +8,10 @@ from homematicip.base.enums import EventType from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import HomeAssistantType from .const import COMPONENTS, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN from .errors import HmipcConnectionError @@ -53,7 +54,7 @@ async def async_register(self): except HmipConnectionError: return False - async def get_auth(self, hass: HomeAssistant, hapid, pin): + async def get_auth(self, hass: HomeAssistantType, hapid, pin): """Create a HomematicIP access point object.""" auth = AsyncAuth(hass.loop, async_get_clientsession(hass)) try: @@ -69,7 +70,7 @@ async def get_auth(self, hass: HomeAssistant, hapid, pin): class HomematicipHAP: """Manages HomematicIP HTTP and WebSocket connection.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry) -> None: """Initialize HomematicIP Cloud connection.""" self.hass = hass self.config_entry = config_entry @@ -224,7 +225,7 @@ async def async_reset(self): return True async def get_hap( - self, hass: HomeAssistant, hapid: str, authtoken: str, name: str + self, hass: HomeAssistantType, hapid: str, authtoken: str, name: str ) -> AsyncHome: """Create a HomematicIP access point object.""" home = AsyncHome(hass.loop, async_get_clientsession(hass)) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index bc704e2ef06b3a..46a8d95729ff19 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -21,7 +21,7 @@ Light, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .hap import HomematicipHAP @@ -38,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 18d483f6adf365..9caa72ba15f171 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -31,7 +31,7 @@ POWER_WATT, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE @@ -52,7 +52,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 3b54d3fc2790da..dae6019b3786f0 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -15,7 +15,7 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_GROUP_MEMBER_UNREACHABLE @@ -30,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP switch from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 6b92b639c7afc2..5aa3f28c45d374 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -11,7 +11,7 @@ from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .hap import HomematicipHAP @@ -43,7 +43,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index b2fc53a28ec32a..f60f8d659b54b4 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -12,7 +12,7 @@ const as hmipc, hap as hmip_hap, ) -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeTemplate @@ -20,7 +20,7 @@ @pytest.fixture(name="mock_connection") -def mock_connection_fixture(): +def mock_connection_fixture() -> AsyncConnection: """Return a mocked connection.""" connection = MagicMock(spec=AsyncConnection) @@ -35,7 +35,7 @@ def _rest_call_side_effect(path, body=None): @pytest.fixture(name="hmip_config_entry") -def hmip_config_entry_fixture(): +def hmip_config_entry_fixture() -> config_entries.ConfigEntry: """Create a mock config entriy for homematic ip cloud.""" entry_data = { hmipc.HMIPC_HAPID: HAPID, @@ -57,20 +57,24 @@ def hmip_config_entry_fixture(): @pytest.fixture(name="default_mock_home") -def default_mock_home_fixture(mock_connection): +def default_mock_home_fixture(mock_connection) -> AsyncHome: """Create a fake homematic async home.""" return HomeTemplate(connection=mock_connection).init_home().get_async_home_mock() @pytest.fixture(name="default_mock_hap") async def default_mock_hap_fixture( - hass: HomeAssistant, mock_connection, hmip_config_entry -): + hass: HomeAssistantType, mock_connection, hmip_config_entry +) -> hmip_hap.HomematicipHAP: """Create a mocked homematic access point.""" return await get_mock_hap(hass, mock_connection, hmip_config_entry) -async def get_mock_hap(hass: HomeAssistant, mock_connection, hmip_config_entry): +async def get_mock_hap( + hass: HomeAssistantType, + mock_connection, + hmip_config_entry: config_entries.ConfigEntry, +) -> hmip_hap.HomematicipHAP: """Create a mocked homematic access point.""" hass.config.components.add(HMIPC_DOMAIN) hap = hmip_hap.HomematicipHAP(hass, hmip_config_entry) @@ -81,7 +85,7 @@ async def get_mock_hap(hass: HomeAssistant, mock_connection, hmip_config_entry): .get_async_home_mock() ) with patch.object(hap, "get_hap", return_value=mock_coro(mock_home)): - assert await hap.async_setup() is True + assert await hap.async_setup() mock_home.on_update(hap.async_update) mock_home.on_create(hap.async_create_entity) @@ -93,7 +97,7 @@ async def get_mock_hap(hass: HomeAssistant, mock_connection, hmip_config_entry): @pytest.fixture(name="hmip_config") -def hmip_config_fixture(): +def hmip_config_fixture() -> ConfigType: """Create a config for homematic ip cloud.""" entry_data = { @@ -107,15 +111,15 @@ def hmip_config_fixture(): @pytest.fixture(name="dummy_config") -def dummy_config_fixture(): +def dummy_config_fixture() -> ConfigType: """Create a dummy config.""" return {"blabla": None} @pytest.fixture(name="mock_hap_with_service") async def mock_hap_with_service_fixture( - hass: HomeAssistant, default_mock_hap, dummy_config -): + hass: HomeAssistantType, default_mock_hap, dummy_config +) -> hmip_hap.HomematicipHAP: """Create a fake homematic access point with hass services.""" await hmip_async_setup(hass, dummy_config) await hass.async_block_till_done() @@ -124,7 +128,7 @@ async def mock_hap_with_service_fixture( @pytest.fixture(name="simple_mock_home") -def simple_mock_home_fixture(): +def simple_mock_home_fixture() -> AsyncHome: """Return a simple AsyncHome Mock.""" return Mock( spec=AsyncHome, @@ -139,6 +143,6 @@ def simple_mock_home_fixture(): @pytest.fixture(name="simple_mock_auth") -def simple_mock_auth_fixture(): +def simple_mock_auth_fixture() -> AsyncAuth: """Return a simple AsyncAuth Mock.""" return Mock(spec=AsyncAuth, pin=HAPPIN, create=True) diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 90f557b1f93303..324649ef515046 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -31,7 +31,7 @@ async def test_auth_setup(hass): } hap = hmipc.HomematicipAuth(hass, config) with patch.object(hap, "get_auth", return_value=mock_coro()): - assert await hap.async_setup() is True + assert await hap.async_setup() async def test_auth_setup_connection_error(hass): @@ -43,7 +43,7 @@ async def test_auth_setup_connection_error(hass): } hap = hmipc.HomematicipAuth(hass, config) with patch.object(hap, "get_auth", side_effect=errors.HmipcConnectionError): - assert await hap.async_setup() is False + assert not await hap.async_setup() async def test_auth_auth_check_and_register(hass): @@ -62,7 +62,7 @@ async def test_auth_auth_check_and_register(hass): ), patch.object( hap.auth, "confirmAuthToken", return_value=mock_coro() ): - assert await hap.async_checkbutton() is True + assert await hap.async_checkbutton() assert await hap.async_register() == "ABC" @@ -78,7 +78,7 @@ async def test_auth_auth_check_and_register_with_exception(hass): with patch.object( hap.auth, "isRequestAcknowledged", side_effect=HmipConnectionError ), patch.object(hap.auth, "requestAuthToken", side_effect=HmipConnectionError): - assert await hap.async_checkbutton() is False + assert not await hap.async_checkbutton() assert await hap.async_register() is False @@ -94,7 +94,7 @@ async def test_hap_setup_works(aioclient_mock): } hap = hmipc.HomematicipHAP(hass, entry) with patch.object(hap, "get_hap", return_value=mock_coro(home)): - assert await hap.async_setup() is True + assert await hap.async_setup() assert hap.home is home assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8 @@ -140,7 +140,7 @@ async def test_hap_reset_unloads_entry_if_setup(): } hap = hmipc.HomematicipHAP(hass, entry) with patch.object(hap, "get_hap", return_value=mock_coro(home)): - assert await hap.async_setup() is True + assert await hap.async_setup() assert hap.home is home assert not hass.services.async_register.mock_calls @@ -161,7 +161,7 @@ async def test_hap_create(hass, hmip_config_entry, simple_mock_home): "homeassistant.components.homematicip_cloud.hap.AsyncHome", return_value=simple_mock_home, ), patch.object(hap, "async_connect", return_value=mock_coro(None)): - assert await hap.async_setup() is True + assert await hap.async_setup() async def test_hap_create_exception(hass, hmip_config_entry, simple_mock_home): @@ -197,7 +197,7 @@ async def test_auth_create(hass, simple_mock_auth): "homeassistant.components.homematicip_cloud.hap.AsyncAuth", return_value=simple_mock_auth, ): - assert await hmip_auth.async_setup() is True + assert await hmip_auth.async_setup() await hass.async_block_till_done() assert hmip_auth.auth.pin == HAPPIN @@ -216,12 +216,12 @@ async def test_auth_create_exception(hass, simple_mock_auth): "homeassistant.components.homematicip_cloud.hap.AsyncAuth", return_value=simple_mock_auth, ): - assert await hmip_auth.async_setup() is True + assert await hmip_auth.async_setup() await hass.async_block_till_done() - assert hmip_auth.auth is False + assert not hmip_auth.auth with patch( "homeassistant.components.homematicip_cloud.hap.AsyncAuth", return_value=simple_mock_auth, ): - assert await hmip_auth.get_auth(hass, HAPID, HAPPIN) is False + assert not await hmip_auth.get_auth(hass, HAPID, HAPPIN) From 86700ec1ace0ed6453aa2243b33a9c6de616a1d0 Mon Sep 17 00:00:00 2001 From: Marius Flage Date: Wed, 23 Oct 2019 18:25:00 +0200 Subject: [PATCH 1219/3953] Avoid query operations on a pjlink powered off projector (#28132) --- homeassistant/components/pjlink/media_player.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 44b4055e032f92..6474165a6cd637 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -105,10 +105,12 @@ def update(self): pwstate = projector.get_power() if pwstate in ("on", "warm-up"): self._pwstate = STATE_ON + self._muted = projector.get_mute()[1] + self._current_source = format_input_source(*projector.get_input()) else: self._pwstate = STATE_OFF - self._muted = projector.get_mute()[1] - self._current_source = format_input_source(*projector.get_input()) + self._muted = False + self._current_source = None except KeyError as err: if str(err) == "'OK'": self._pwstate = STATE_OFF From a3f3ea4e2529e3c7de253197b188b10813030aeb Mon Sep 17 00:00:00 2001 From: Jon Gilmore <7232986+JonGilmore@users.noreply.github.com> Date: Wed, 23 Oct 2019 11:29:30 -0500 Subject: [PATCH 1220/3953] Fix Lutron Pico (#27059) * removed a nesting level * Lutron Pico fix * Reverted logging change Was unaware that f-strings aren't used in logging commands, reverted the usage * Reverted logging change Was unaware that f-strings aren't used in logging commands, reverted the usage * fixed logic --- homeassistant/components/lutron/__init__.py | 25 +++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index de3ca40fd1d9c3..09ab0fc747b0be 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -68,18 +68,19 @@ def setup(hass, base_config): hass.data[LUTRON_DEVICES]["switch"].append((area.name, output)) for keypad in area.keypads: for button in keypad.buttons: - # This is the best way to determine if a button does anything - # useful until pylutron is updated to provide information on - # which buttons actually control scenes. - for led in keypad.leds: - if ( - led.number == button.number - and button.name != "Unknown Button" - and button.button_type in ("SingleAction", "Toggle") - ): - hass.data[LUTRON_DEVICES]["scene"].append( - (area.name, keypad.name, button, led) - ) + # If the button has a function assigned to it, add it as a scene + if button.name != "Unknown Button" and button.button_type in ( + "SingleAction", + "Toggle", + ): + # Associate an LED with a button if there is one + led = next( + (led for led in keypad.leds if led.number == button.number), + None, + ) + hass.data[LUTRON_DEVICES]["scene"].append( + (area.name, keypad.name, button, led) + ) hass.data[LUTRON_BUTTONS].append(LutronButton(hass, keypad, button)) if area.occupancy_group is not None: From b8d2c58c6057744b97790ec820303c0db66047d6 Mon Sep 17 00:00:00 2001 From: libots <989623+libots@users.noreply.github.com> Date: Wed, 23 Oct 2019 13:41:58 -0400 Subject: [PATCH 1221/3953] Support for additional Abode timeline events (#28124) * Support for additional timeline events * Update __init__.py * Expose details on user These lines expose apptype and event_by, which can be used to give information on events initiated by keypad users (vs. users on the mobile app, web app, or those initiated from HA through abodepy). --- homeassistant/components/abode/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index aeba437acebd47..6a72ac64145485 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -44,6 +44,8 @@ ATTR_EVENT_UTC = "event_utc" ATTR_SETTING = "setting" ATTR_USER_NAME = "user_name" +ATTR_APP_TYPE = "app_type" +ATTR_EVENT_BY = "event_by" ATTR_VALUE = "value" ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str]) @@ -247,6 +249,8 @@ def event_callback(event, event_json): ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ""), ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ""), ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ""), + ATTR_APP_TYPE: event_json.get(ATTR_APP_TYPE, ""), + ATTR_EVENT_BY: event_json.get(ATTR_EVENT_BY, ""), ATTR_DATE: event_json.get(ATTR_DATE, ""), ATTR_TIME: event_json.get(ATTR_TIME, ""), } @@ -259,6 +263,12 @@ def event_callback(event, event_json): TIMELINE.PANEL_FAULT_GROUP, TIMELINE.PANEL_RESTORE_GROUP, TIMELINE.AUTOMATION_GROUP, + TIMELINE.DISARM_GROUP, + TIMELINE.ARM_GROUP, + TIMELINE.TEST_GROUP, + TIMELINE.CAPTURE_GROUP, + TIMELINE.DEVICE_GROUP, + TIMELINE.AUTOMATION_EDIT_GROUP, ] for event in events: From 85eefe41da44f814477741b20b5b26f4b0f9c4f2 Mon Sep 17 00:00:00 2001 From: Adrien Foulon <6115458+Tofandel@users.noreply.github.com> Date: Wed, 23 Oct 2019 19:44:47 +0200 Subject: [PATCH 1222/3953] Fix supported_features in mqtt cover (#28120) * Correctly compute the supported_features in cover.mqtt * Update homeassistant/components/mqtt/cover.py Co-Authored-By: Paulus Schoutsen * Correctly compute the supported_features in cover.mqtt * Format --- homeassistant/components/mqtt/cover.py | 9 +++++++-- tests/components/mqtt/test_cover.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 66e14ca9a5a741..e6cfab90c26923 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -90,7 +90,7 @@ DEFAULT_TILT_OPEN_POSITION = 100 DEFAULT_TILT_OPTIMISTIC = False -OPEN_CLOSE_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP +OPEN_CLOSE_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE TILT_FEATURES = ( SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT @@ -122,7 +122,9 @@ def validate_options(value): vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string, vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string, - vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, + vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): vol.Any( + cv.string, None + ), vol.Optional(CONF_POSITION_CLOSED, default=DEFAULT_POSITION_CLOSED): int, vol.Optional(CONF_POSITION_OPEN, default=DEFAULT_POSITION_OPEN): int, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, @@ -396,6 +398,9 @@ def supported_features(self): if self._config.get(CONF_COMMAND_TOPIC) is not None: supported_features = OPEN_CLOSE_FEATURES + if self._config.get(CONF_PAYLOAD_STOP) is not None: + supported_features |= SUPPORT_STOP + if self._config.get(CONF_SET_POSITION_TOPIC) is not None: supported_features |= SUPPORT_SET_POSITION diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 12a43030aa8a06..bb734d2c03df65 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -546,6 +546,27 @@ async def test_no_command_topic(hass, mqtt_mock): assert hass.states.get("cover.test").attributes["supported_features"] == 240 +async def test_no_payload_stop(hass, mqtt_mock): + """Test with no stop payload.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": None, + } + }, + ) + + assert hass.states.get("cover.test").attributes["supported_features"] == 3 + + async def test_with_command_topic_and_tilt(hass, mqtt_mock): """Test with command topic and tilt config.""" assert await async_setup_component( From 0771dc3a37d7884e9bcb9733d477d5166e703738 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 23 Oct 2019 20:37:37 +0200 Subject: [PATCH 1223/3953] Downgrade aioHTTP 3.6.2 to 3.6.1 (#28143) --- 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 80357eccf7198d..63b4673b435882 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiohttp==3.6.2 +aiohttp==3.6.1 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index cbe4a93f812c39..297067f522030f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.6.2 +aiohttp==3.6.1 astral==1.10.1 async_timeout==3.0.1 attrs==19.2.0 diff --git a/setup.py b/setup.py index 0e8f8313a98781..d2c4934713be47 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.6.2", + "aiohttp==3.6.1", "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.2.0", From 65d8c703770537d4e7d14d636c4f96a95c9fe3fc Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 23 Oct 2019 14:41:26 -0400 Subject: [PATCH 1224/3953] Rebase Implement Alexa.DoorbellEventSource Interface Controller (#27726) --- .../components/alexa/capabilities.py | 42 ++++++++++-- homeassistant/components/alexa/entities.py | 8 ++- .../components/alexa/state_report.py | 66 ++++++++++++++++++- tests/components/alexa/__init__.py | 2 +- tests/components/alexa/test_smart_home.py | 22 +++++++ tests/components/alexa/test_state_report.py | 31 +++++++++ 6 files changed, 161 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 246429ad6c9b74..deb83813dbc1ca 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -91,6 +91,15 @@ def supports_deactivation(): """Applicable only to scenes.""" return None + @staticmethod + def capability_proactively_reported(): + """Return True if the capability is proactively reported. + + Set properties_proactively_reported() for proactively reported properties. + Applicable to DoorbellEventSource. + """ + return None + @staticmethod def capability_resources(): """Applicable to ToggleController, RangeController, and ModeController interfaces.""" @@ -103,16 +112,20 @@ def configuration(): def serialize_discovery(self): """Serialize according to the Discovery API.""" - result = { - "type": "AlexaInterface", - "interface": self.name(), - "version": "3", - "properties": { + result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} + + properties_supported = self.properties_supported() + if properties_supported: + result["properties"] = { "supported": self.properties_supported(), "proactivelyReported": self.properties_proactively_reported(), "retrievable": self.properties_retrievable(), - }, - } + } + + # pylint: disable=assignment-from-none + proactively_reported = self.capability_proactively_reported() + if proactively_reported is not None: + result["proactivelyReported"] = proactively_reported # pylint: disable=assignment-from-none non_controllable = self.properties_non_controllable() @@ -1050,3 +1063,18 @@ class AlexaChannelController(AlexaCapability): def name(self): """Return the Alexa API name of this interface.""" return "Alexa.ChannelController" + + +class AlexaDoorbellEventSource(AlexaCapability): + """Implements Alexa.DoorbellEventSource. + + https://developer.amazon.com/docs/device-apis/alexa-doorbelleventsource.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.DoorbellEventSource" + + def capability_proactively_reported(self): + """Return True for proactively reported capability.""" + return True diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index f6fc9936a024e8..d84848e9aba43a 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -38,6 +38,7 @@ AlexaColorController, AlexaColorTemperatureController, AlexaContactSensor, + AlexaDoorbellEventSource, AlexaEndpointHealth, AlexaInputController, AlexaLockController, @@ -84,7 +85,7 @@ class DisplayCategory: DOOR = "DOOR" # Indicates a doorbell. - DOOR_BELL = "DOORBELL" + DOORBELL = "DOORBELL" # Indicates a fan. FAN = "FAN" @@ -500,6 +501,11 @@ def interfaces(self): elif sensor_type is self.TYPE_MOTION: yield AlexaMotionSensor(self.hass, self.entity) + entity_conf = self.config.entity_config.get(self.entity.entity_id, {}) + if CONF_DISPLAY_CATEGORIES in entity_conf: + if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL: + yield AlexaDoorbellEventSource(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) def get_type(self): diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 42c16919a45cf1..b5e1b741f0c4e4 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -6,7 +6,8 @@ import aiohttp import async_timeout -from homeassistant.const import MATCH_ALL +import homeassistant.util.dt as dt_util +from homeassistant.const import MATCH_ALL, STATE_ON from .const import API_CHANGE, Cause from .entities import ENTITY_ADAPTERS @@ -45,6 +46,14 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): hass, smart_home_config, alexa_changed_entity ) return + if ( + interface.name() == "Alexa.DoorbellEventSource" + and new_state.state == STATE_ON + ): + await async_send_doorbell_event_message( + hass, smart_home_config, alexa_changed_entity + ) + return return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener @@ -184,3 +193,58 @@ async def async_send_delete_message(hass, config, entity_ids): return await session.post( config.endpoint, headers=headers, json=message_serialized, allow_redirects=True ) + + +async def async_send_doorbell_event_message(hass, config, alexa_entity): + """Send a DoorbellPress event message for an Alexa entity. + + https://developer.amazon.com/docs/smarthome/send-events-to-the-alexa-event-gateway.html + """ + token = await config.async_get_access_token() + + headers = {"Authorization": f"Bearer {token}"} + + endpoint = alexa_entity.alexa_id() + + message = AlexaResponse( + name="DoorbellPress", + namespace="Alexa.DoorbellEventSource", + payload={ + "cause": {"type": Cause.PHYSICAL_INTERACTION}, + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", + }, + ) + + message.set_endpoint_full(token, endpoint) + + message_serialized = message.serialize() + session = hass.helpers.aiohttp_client.async_get_clientsession() + + try: + with async_timeout.timeout(DEFAULT_TIMEOUT): + response = await session.post( + config.endpoint, + headers=headers, + json=message_serialized, + allow_redirects=True, + ) + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Timeout sending report to Alexa.") + return + + response_text = await response.text() + + _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) + _LOGGER.debug("Received (%s): %s", response.status, response_text) + + if response.status == 202: + return + + response_json = json.loads(response_text) + + _LOGGER.error( + "Error when sending DoorbellPress event to Alexa: %s: %s", + response_json["payload"]["code"], + response_json["payload"]["description"], + ) diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 4fd8bf6f2a9671..0fa1961ad61c03 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -13,7 +13,7 @@ class MockConfig(config.AbstractConfig): """Mock Alexa config.""" - entity_config = {} + entity_config = {"binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}} @property def supports_auth(self): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 139c8c9740ba00..c50c0748147f9d 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1196,6 +1196,28 @@ async def test_motion_sensor(hass): properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED") +async def test_doorbell_sensor(hass): + """Test doorbell sensor discovery.""" + device = ( + "binary_sensor.test_doorbell", + "off", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_doorbell" + assert appliance["displayCategories"][0] == "DOORBELL" + assert appliance["friendlyName"] == "Test Doorbell Sensor" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth" + ) + + doorbell_capability = get_capability(capabilities, "Alexa.DoorbellEventSource") + assert doorbell_capability is not None + assert doorbell_capability["proactivelyReported"] is True + + async def test_unknown_sensor(hass): """Test sensors of unknown quantities are not discovered.""" device = ( diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 310180ef5d09db..2c58d1ed45ed17 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -137,3 +137,34 @@ async def test_send_delete_message(hass, aioclient_mock): call_json["event"]["payload"]["endpoints"][0]["endpointId"] == "binary_sensor#test_contact" ) + + +async def test_doorbell_event(hass, aioclient_mock): + """Test doorbell press reports.""" + aioclient_mock.post(TEST_URL, text="", status=202) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "off", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ) + + await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "on", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ) + + # To trigger event listener + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + assert call_json["event"]["header"]["namespace"] == "Alexa.DoorbellEventSource" + assert call_json["event"]["header"]["name"] == "DoorbellPress" + assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION" + assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell" From b6fd191dc427a91aaed81ae72f0e0610e688f145 Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Wed, 23 Oct 2019 21:11:04 +0200 Subject: [PATCH 1225/3953] Add support for SAJ inverters connected via WiFi (#27742) * Add support for SAJ inverters connected via WiFi * Changes after review for saj --- homeassistant/components/saj/manifest.json | 2 +- homeassistant/components/saj/sensor.py | 37 ++++++++++++++++++++-- requirements_all.txt | 2 +- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index e42b37195a42de..2dd701e9c7c65c 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,7 +3,7 @@ "name": "SAJ", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": [ - "pysaj==0.0.9" + "pysaj==0.0.12" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index fa06b2b9125ec7..5605866908e5d7 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -9,6 +9,9 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, + CONF_PASSWORD, + CONF_TYPE, + CONF_USERNAME, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, @@ -31,6 +34,8 @@ UNIT_OF_MEASUREMENT_HOURS = "h" +INVERTER_TYPES = ["ethernet", "wifi"] + SAJ_UNIT_MAPPINGS = { "W": POWER_WATT, "kWh": ENERGY_KILO_WATT_HOUR, @@ -40,16 +45,24 @@ "": None, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_TYPE, default=INVERTER_TYPES[0]): vol.In(INVERTER_TYPES), + vol.Inclusive(CONF_USERNAME, "credentials"): cv.string, + vol.Inclusive(CONF_PASSWORD, "credentials"): cv.string, + } +) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up SAJ sensors.""" remove_interval_update = None + wifi = config[CONF_TYPE] == INVERTER_TYPES[1] # Init all sensors - sensor_def = pysaj.Sensors() + sensor_def = pysaj.Sensors(wifi) # Use all sensors by default hass_sensors = [] @@ -57,7 +70,25 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for sensor in sensor_def: hass_sensors.append(SAJsensor(sensor)) - saj = pysaj.SAJ(config[CONF_HOST]) + kwargs = {} + + if wifi: + kwargs["wifi"] = True + if config.get(CONF_USERNAME) and config.get(CONF_PASSWORD): + kwargs["username"] = config[CONF_USERNAME] + kwargs["password"] = config[CONF_PASSWORD] + + try: + saj = pysaj.SAJ(config[CONF_HOST], **kwargs) + await saj.read(sensor_def) + except pysaj.UnauthorizedException: + _LOGGER.error("Username and/or password is wrong.") + return + except pysaj.UnexpectedResponseException as err: + _LOGGER.error( + "Error in SAJ, please check host/ip address. Original error: %s", err + ) + return async_add_entities(hass_sensors) diff --git a/requirements_all.txt b/requirements_all.txt index 297067f522030f..cafdcae9c9b1d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1420,7 +1420,7 @@ pyrepetier==3.0.5 pysabnzbd==1.1.0 # homeassistant.components.saj -pysaj==0.0.9 +pysaj==0.0.12 # homeassistant.components.sony_projector pysdcp==1 From 1412862f2a29bbc695a515d536b6ca61be799a29 Mon Sep 17 00:00:00 2001 From: On Freund Date: Wed, 23 Oct 2019 22:47:00 +0300 Subject: [PATCH 1226/3953] Config entry and device for Coolmaster integration (#27925) * Config entry and device for Coolmaster integration * Lint/isort/flake/etc... * Black formatting * Code review fixes * Config flow tests for coolmaster * Add pycoolmaster requirement to test * Remove port selection from Coolmaster config flow * Update config_flow.py * More idoimatic hash concat --- .coveragerc | 2 + .../components/coolmaster/__init__.py | 23 +++- .../components/coolmaster/climate.py | 51 ++++----- .../components/coolmaster/config_flow.py | 64 +++++++++++ homeassistant/components/coolmaster/const.py | 25 +++++ .../components/coolmaster/manifest.json | 1 + .../components/coolmaster/strings.json | 23 ++++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/coolmaster/__init__.py | 1 + .../components/coolmaster/test_config_flow.py | 104 ++++++++++++++++++ 11 files changed, 265 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/coolmaster/config_flow.py create mode 100644 homeassistant/components/coolmaster/const.py create mode 100644 homeassistant/components/coolmaster/strings.json create mode 100644 tests/components/coolmaster/__init__.py create mode 100644 tests/components/coolmaster/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 83e6971cc6a426..3645eb00d33b2c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -126,7 +126,9 @@ omit = homeassistant/components/comfoconnect/* homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/binary_sensor.py + homeassistant/components/coolmaster/__init__.py homeassistant/components/coolmaster/climate.py + homeassistant/components/coolmaster/const.py homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/cpuspeed/sensor.py homeassistant/components/crimereports/sensor.py diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index b27ae5f25b4194..530427d33ad1fb 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -1 +1,22 @@ -"""The coolmaster component.""" +"""The Coolmaster integration.""" + + +async def async_setup(hass, config): + """Set up Coolmaster components.""" + return True + + +async def async_setup_entry(hass, entry): + """Set up Coolmaster from a config entry.""" + hass.async_add_job(hass.config_entries.async_forward_entry_setup(entry, "climate")) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a Coolmaster config entry.""" + await hass.async_add_job( + hass.config_entries.async_forward_entry_unload(entry, "climate") + ) + + return True diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 71115a9eebba7b..a52431dd89be22 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -3,9 +3,8 @@ import logging from pycoolmasternet import CoolMasterNet -import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -23,20 +22,10 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -import homeassistant.helpers.config_validation as cv -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE - -DEFAULT_PORT = 10102 +from .const import CONF_SUPPORTED_MODES, DOMAIN -AVAILABLE_MODES = [ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, -] +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE CM_TO_HA_STATE = { "heat": HVAC_MODE_HEAT, @@ -50,17 +39,6 @@ FAN_MODES = ["low", "med", "high", "auto"] -CONF_SUPPORTED_MODES = "supported_modes" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SUPPORTED_MODES, default=AVAILABLE_MODES): vol.All( - cv.ensure_list, [vol.In(AVAILABLE_MODES)] - ), - } -) - _LOGGER = logging.getLogger(__name__) @@ -69,18 +47,17 @@ def _build_entity(device, supported_modes): return CoolmasterClimate(device, supported_modes) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the CoolMasterNet climate platform.""" - - supported_modes = config.get(CONF_SUPPORTED_MODES) - host = config[CONF_HOST] - port = config[CONF_PORT] + supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES) + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] cool = CoolMasterNet(host, port=port) - devices = cool.devices() + devices = await hass.async_add_executor_job(cool.devices) all_devices = [_build_entity(device, supported_modes) for device in devices] - add_entities(all_devices, True) + async_add_devices(all_devices, True) class CoolmasterClimate(ClimateDevice): @@ -118,6 +95,16 @@ def update(self): else: self._unit = TEMP_FAHRENHEIT + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "CoolAutomation", + "model": "CoolMasterNet", + } + @property def unique_id(self): """Return unique ID for this device.""" diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py new file mode 100644 index 00000000000000..543b4c239c8aa4 --- /dev/null +++ b/homeassistant/components/coolmaster/config_flow.py @@ -0,0 +1,64 @@ +"""Config flow to configure Coolmaster.""" + +from pycoolmasternet import CoolMasterNet +import voluptuous as vol + +from homeassistant import core, config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +# pylint: disable=unused-import +from .const import AVAILABLE_MODES, CONF_SUPPORTED_MODES, DEFAULT_PORT, DOMAIN + +MODES_SCHEMA = {vol.Required(mode, default=True): bool for mode in AVAILABLE_MODES} + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA}) + + +async def validate_connection(hass: core.HomeAssistant, host): + """Validate that we can connect to the Coolmaster instance.""" + cool = CoolMasterNet(host, port=DEFAULT_PORT) + devices = await hass.async_add_executor_job(cool.devices) + return len(devices) > 0 + + +class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Coolmaster config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def _async_get_entry(self, data): + supported_modes = [ + key for (key, value) in data.items() if key in AVAILABLE_MODES and value + ] + return self.async_create_entry( + title=data[CONF_HOST], + data={ + CONF_HOST: data[CONF_HOST], + CONF_PORT: DEFAULT_PORT, + CONF_SUPPORTED_MODES: supported_modes, + }, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + + errors = {} + + host = user_input[CONF_HOST] + + try: + result = await validate_connection(self.hass, host) + if not result: + errors["base"] = "no_units" + except (ConnectionRefusedError, TimeoutError): + errors["base"] = "connection_error" + + if errors: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + return self._async_get_entry(user_input) diff --git a/homeassistant/components/coolmaster/const.py b/homeassistant/components/coolmaster/const.py new file mode 100644 index 00000000000000..d4cfea738209e0 --- /dev/null +++ b/homeassistant/components/coolmaster/const.py @@ -0,0 +1,25 @@ +"""Constants for the Coolmaster integration.""" + +from homeassistant.components.climate.const import ( + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, +) + +DOMAIN = "coolmaster" + +DEFAULT_PORT = 10102 + +CONF_SUPPORTED_MODES = "supported_modes" + +AVAILABLE_MODES = [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, +] diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 69ab8ee3c4b199..124a1e4a5b986e 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -1,6 +1,7 @@ { "domain": "coolmaster", "name": "Coolmaster", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", "requirements": [ "pycoolmasternet==0.0.4" diff --git a/homeassistant/components/coolmaster/strings.json b/homeassistant/components/coolmaster/strings.json new file mode 100644 index 00000000000000..d309f8c9c93388 --- /dev/null +++ b/homeassistant/components/coolmaster/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "CoolMasterNet", + "step": { + "user": { + "title": "Setup your CoolMasterNet connection details.", + "data": { + "host": "Host", + "off": "Can be turned off", + "heat": "Support heat mode", + "cool": "Support cool mode", + "heat_cool": "Support automatic heat/cool mode", + "dry": "Support dry mode", + "fan_only": "Support fan only mode" + } + } + }, + "error": { + "connection_error": "Failed to connect to CoolMasterNet instance. Please check your host.", + "no_units": "Could not find any HVAC units in CoolMasterNet host." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index bf63869bc9b48e..4668528fedbac0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -14,6 +14,7 @@ "axis", "cast", "cert_expiry", + "coolmaster", "daikin", "deconz", "dialogflow", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e61732e632ba52..2bdd47cd94661c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -398,6 +398,9 @@ pybotvac==0.0.17 # homeassistant.components.cast pychromecast==4.0.1 +# homeassistant.components.coolmaster +pycoolmasternet==0.0.4 + # homeassistant.components.daikin pydaikin==1.6.1 diff --git a/tests/components/coolmaster/__init__.py b/tests/components/coolmaster/__init__.py new file mode 100644 index 00000000000000..a7e1bf08c99273 --- /dev/null +++ b/tests/components/coolmaster/__init__.py @@ -0,0 +1 @@ +"""Tests for the Coolmaster component.""" diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py new file mode 100644 index 00000000000000..d49858fcf05cf2 --- /dev/null +++ b/tests/components/coolmaster/test_config_flow.py @@ -0,0 +1,104 @@ +"""Test the Coolmaster config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.components.coolmaster.const import DOMAIN, AVAILABLE_MODES + +# from homeassistant.components.coolmaster.config_flow import validate_connection + +from tests.common import mock_coro + + +def _flow_data(): + options = {"host": "1.1.1.1"} + for mode in AVAILABLE_MODES: + options[mode] = True + return options + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.coolmaster.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.coolmaster.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + "host": "1.1.1.1", + "port": 10102, + "supported_modes": AVAILABLE_MODES, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_timeout(hass): + """Test we handle a connection timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + side_effect=TimeoutError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_form_connection_refused(hass): + """Test we handle a connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + side_effect=ConnectionRefusedError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_form_no_units(hass): + """Test we handle no units found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + return_value=mock_coro(False), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "no_units"} From 062ec8a7c29507798816b964ae02caaf66b29954 Mon Sep 17 00:00:00 2001 From: Villhellm Date: Wed, 23 Oct 2019 13:03:52 -0700 Subject: [PATCH 1227/3953] changed STATE_OFF to STATE_STANDBY (#28148) --- homeassistant/components/roku/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index d69b0eddb71d52..12aca14151056c 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -20,7 +20,7 @@ STATE_HOME, STATE_IDLE, STATE_PLAYING, - STATE_OFF, + STATE_STANDBY, ) DEFAULT_PORT = 8060 @@ -98,7 +98,7 @@ def name(self): def state(self): """Return the state of the device.""" if self._power_state == "Off": - return STATE_OFF + return STATE_STANDBY if self.current_app is None: return None From 7cb6607b1f6a86469cb79314594a2920141ac224 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Wed, 23 Oct 2019 23:09:11 +0300 Subject: [PATCH 1228/3953] Allow multiple Transmission clients and add unique_id to entities (#28136) * Allow multiple clients + improvements * remove commented code * fixed test_init.py --- .coveragerc | 1 - .../transmission/.translations/en.json | 37 ++--- .../components/transmission/__init__.py | 153 ++++++++++-------- .../components/transmission/config_flow.py | 46 +++--- .../components/transmission/const.py | 1 - .../components/transmission/sensor.py | 32 ++-- .../components/transmission/strings.json | 13 +- .../components/transmission/switch.py | 37 +++-- .../transmission/test_config_flow.py | 73 +++++---- tests/components/transmission/test_init.py | 123 ++++++++++++++ 10 files changed, 333 insertions(+), 183 deletions(-) create mode 100644 tests/components/transmission/test_init.py diff --git a/.coveragerc b/.coveragerc index 3645eb00d33b2c..f97a7524a21532 100644 --- a/.coveragerc +++ b/.coveragerc @@ -703,7 +703,6 @@ omit = homeassistant/components/tradfri/base_class.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py - homeassistant/components/transmission/__init__.py homeassistant/components/transmission/sensor.py homeassistant/components/transmission/switch.py homeassistant/components/transmission/const.py diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index 67461d1a3e8565..45c16be36e2ca9 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -1,39 +1,34 @@ { "config": { - "abort": { - "one_instance_allowed": "Only a single instance is necessary." - }, - "error": { - "cannot_connect": "Unable to Connect to host", - "wrong_credentials": "Wrong username or password" - }, + "title": "Transmission", "step": { - "options": { - "data": { - "scan_interval": "Update frequency" - }, - "title": "Configure Options" - }, "user": { + "title": "Setup Transmission Client", "data": { - "host": "Host", "name": "Name", + "host": "Host", + "username": "Username", "password": "Password", - "port": "Port", - "username": "Username" - }, - "title": "Setup Transmission Client" + "port": "Port" + } } }, - "title": "Transmission" + "error": { + "name_exists": "Name already exists", + "wrong_credentials": "Wrong username or password", + "cannot_connect": "Unable to Connect to host" + }, + "abort": { + "already_configured": "Host is already configured." + } }, "options": { "step": { "init": { + "title": "Configure options for Transmission", "data": { "scan_interval": "Update frequency" - }, - "description": "Configure options for Transmission" + } } } } diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index e6ddd87bdf56bd..6cfd6bf640ae37 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -19,11 +19,10 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import slugify from .const import ( ATTR_TORRENT, - DATA_TRANSMISSION, - DATA_UPDATED, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, @@ -37,74 +36,77 @@ SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string}) +TRANS_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) +) + CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.All(cv.ensure_list, [TRANS_SCHEMA])}, extra=vol.ALLOW_EXTRA ) async def async_setup(hass, config): """Import the Transmission Component from config.""" - if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + if DOMAIN in config: + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) ) - ) return True async def async_setup_entry(hass, config_entry): """Set up the Transmission Component.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - - if not config_entry.options: - await async_populate_options(hass, config_entry) - client = TransmissionClient(hass, config_entry) - client_id = config_entry.entry_id - hass.data[DOMAIN][client_id] = client + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client + if not await client.async_setup(): return False return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass, config_entry): """Unload Transmission Entry from config_entry.""" - hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) - if hass.data[DOMAIN][entry.entry_id].unsub_timer: - hass.data[DOMAIN][entry.entry_id].unsub_timer() + client = hass.data[DOMAIN][config_entry.entry_id] + hass.services.async_remove(DOMAIN, client.service_name) + if client.unsub_timer: + client.unsub_timer() for component in "sensor", "switch": - await hass.config_entries.async_forward_entry_unload(entry, component) + await hass.config_entries.async_forward_entry_unload(config_entry, component) - del hass.data[DOMAIN] + hass.data[DOMAIN].pop(config_entry.entry_id) return True -async def get_api(hass, host, port, username=None, password=None): +async def get_api(hass, entry): """Get Transmission client.""" + host = entry[CONF_HOST] + port = entry[CONF_PORT] + username = entry.get(CONF_USERNAME) + password = entry.get(CONF_PASSWORD) + try: api = await hass.async_add_executor_job( transmissionrpc.Client, host, port, username, password ) + _LOGGER.debug("Successfully connected to %s", host) return api except TransmissionError as error: @@ -112,20 +114,13 @@ async def get_api(hass, host, port, username=None, password=None): _LOGGER.error("Credentials for Transmission client are not valid") raise AuthenticationError if "111: Connection refused" in str(error): - _LOGGER.error("Connecting to the Transmission client failed") + _LOGGER.error("Connecting to the Transmission client %s failed", host) raise CannotConnect _LOGGER.error(error) raise UnknownError -async def async_populate_options(hass, config_entry): - """Populate default options for Transmission Client.""" - options = {CONF_SCAN_INTERVAL: config_entry.data["options"][CONF_SCAN_INTERVAL]} - - hass.config_entries.async_update_entry(config_entry, options=options) - - class TransmissionClient: """Transmission Client Object.""" @@ -133,33 +128,35 @@ def __init__(self, hass, config_entry): """Initialize the Transmission RPC API.""" self.hass = hass self.config_entry = config_entry - self.scan_interval = self.config_entry.options[CONF_SCAN_INTERVAL] - self.tm_data = None + self._tm_data = None self.unsub_timer = None + @property + def service_name(self): + """Return the service name.""" + return slugify(f"{SERVICE_ADD_TORRENT}_{self.config_entry.data[CONF_NAME]}") + + @property + def api(self): + """Return the tm_data object.""" + return self._tm_data + async def async_setup(self): """Set up the Transmission client.""" - config = { - CONF_HOST: self.config_entry.data[CONF_HOST], - CONF_PORT: self.config_entry.data[CONF_PORT], - CONF_USERNAME: self.config_entry.data.get(CONF_USERNAME), - CONF_PASSWORD: self.config_entry.data.get(CONF_PASSWORD), - } try: - api = await get_api(self.hass, **config) + api = await get_api(self.hass, self.config_entry.data) except CannotConnect: raise ConfigEntryNotReady except (AuthenticationError, UnknownError): return False - self.tm_data = self.hass.data[DOMAIN][DATA_TRANSMISSION] = TransmissionData( - self.hass, self.config_entry, api - ) + self._tm_data = TransmissionData(self.hass, self.config_entry, api) - await self.hass.async_add_executor_job(self.tm_data.init_torrent_list) - await self.hass.async_add_executor_job(self.tm_data.update) - self.set_scan_interval(self.scan_interval) + await self.hass.async_add_executor_job(self._tm_data.init_torrent_list) + await self.hass.async_add_executor_job(self._tm_data.update) + self.add_options() + self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) for platform in ["sensor", "switch"]: self.hass.async_create_task( @@ -181,19 +178,31 @@ def add_torrent(service): ) self.hass.services.async_register( - DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA + DOMAIN, self.service_name, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA ) self.config_entry.add_update_listener(self.async_options_updated) return True + def add_options(self): + """Add options for entry.""" + if not self.config_entry.options: + scan_interval = self.config_entry.data.pop( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + options = {CONF_SCAN_INTERVAL: scan_interval} + + self.hass.config_entries.async_update_entry( + self.config_entry, options=options + ) + def set_scan_interval(self, scan_interval): """Update scan interval.""" - def refresh(event_time): + async def refresh(event_time): """Get the latest data from Transmission.""" - self.tm_data.update() + self._tm_data.update() if self.unsub_timer is not None: self.unsub_timer() @@ -215,6 +224,7 @@ class TransmissionData: def __init__(self, hass, config, api): """Initialize the Transmission RPC API.""" self.hass = hass + self.config = config self.data = None self.torrents = None self.session = None @@ -223,6 +233,16 @@ def __init__(self, hass, config, api): self.completed_torrents = [] self.started_torrents = [] + @property + def host(self): + """Return the host name.""" + return self.config.data[CONF_HOST] + + @property + def signal_options_update(self): + """Option update signal per transmission entry.""" + return f"tm-options-{self.host}" + def update(self): """Get the latest data from Transmission instance.""" try: @@ -232,14 +252,13 @@ def update(self): self.check_completed_torrent() self.check_started_torrent() - _LOGGER.debug("Torrent Data Updated") + _LOGGER.debug("Torrent Data for %s Updated", self.host) self.available = True except TransmissionError: self.available = False - _LOGGER.error("Unable to connect to Transmission client") - - dispatcher_send(self.hass, DATA_UPDATED) + _LOGGER.error("Unable to connect to Transmission client %s", self.host) + dispatcher_send(self.hass, self.signal_options_update) def init_torrent_list(self): """Initialize torrent lists.""" diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index 99376f4b6e0a87..d7b9efb15d8727 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -29,32 +29,32 @@ def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return TransmissionOptionsFlowHandler(config_entry) - def __init__(self): - """Initialize the Transmission flow.""" - self.config = {} - self.errors = {} - async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="one_instance_allowed") + errors = {} if user_input is not None: - self.config[CONF_NAME] = user_input.pop(CONF_NAME) + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == user_input[CONF_HOST]: + return self.async_abort(reason="already_configured") + if entry.data[CONF_NAME] == user_input[CONF_NAME]: + errors[CONF_NAME] = "name_exists" + break + try: - await get_api(self.hass, **user_input) - self.config.update(user_input) - if "options" not in self.config: - self.config["options"] = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} - return self.async_create_entry( - title=self.config[CONF_NAME], data=self.config - ) + await get_api(self.hass, user_input) + except AuthenticationError: - self.errors[CONF_USERNAME] = "wrong_credentials" - self.errors[CONF_PASSWORD] = "wrong_credentials" + errors[CONF_USERNAME] = "wrong_credentials" + errors[CONF_PASSWORD] = "wrong_credentials" except (CannotConnect, UnknownError): - self.errors["base"] = "cannot_connect" + errors["base"] = "cannot_connect" + + if not errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) return self.async_show_form( step_id="user", @@ -67,15 +67,12 @@ async def async_step_user(self, user_input=None): vol.Required(CONF_PORT, default=DEFAULT_PORT): int, } ), - errors=self.errors, + errors=errors, ) async def async_step_import(self, import_config): """Import from Transmission client config.""" - self.config["options"] = { - CONF_SCAN_INTERVAL: import_config.pop(CONF_SCAN_INTERVAL).seconds - } - + import_config[CONF_SCAN_INTERVAL] = import_config[CONF_SCAN_INTERVAL].seconds return await self.async_step_user(user_input=import_config) @@ -95,8 +92,7 @@ async def async_step_init(self, user_input=None): vol.Optional( CONF_SCAN_INTERVAL, default=self.config_entry.options.get( - CONF_SCAN_INTERVAL, - self.config_entry.data["options"][CONF_SCAN_INTERVAL], + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), ): int } diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index e4a8b1490c28bf..472bb32a391cfe 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -21,4 +21,3 @@ SERVICE_ADD_TORRENT = "add_torrent" DATA_UPDATED = "transmission_data_updated" -DATA_TRANSMISSION = "data_transmission" diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 30dfa4a3cbed47..d9fd2b5114476c 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -6,7 +6,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SENSOR_TYPES +from .const import DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -19,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission sensors.""" - transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + tm_client = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data[CONF_NAME] dev = [] @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): dev.append( TransmissionSensor( sensor_type, - transmission_api, + tm_client, name, SENSOR_TYPES[sensor_type][0], SENSOR_TYPES[sensor_type][1], @@ -41,17 +41,12 @@ class TransmissionSensor(Entity): """Representation of a Transmission sensor.""" def __init__( - self, - sensor_type, - transmission_api, - client_name, - sensor_name, - unit_of_measurement, + self, sensor_type, tm_client, client_name, sensor_name, unit_of_measurement ): """Initialize the sensor.""" self._name = sensor_name self._state = None - self._transmission_api = transmission_api + self._tm_client = tm_client self._unit_of_measurement = unit_of_measurement self._data = None self.client_name = client_name @@ -62,6 +57,11 @@ def name(self): """Return the name of the sensor.""" return f"{self.client_name} {self._name}" + @property + def unique_id(self): + """Return the unique id of the entity.""" + return f"{self._tm_client.api.host}-{self.name}" + @property def state(self): """Return the state of the sensor.""" @@ -80,12 +80,14 @@ def unit_of_measurement(self): @property def available(self): """Could the device be accessed during the last update call.""" - return self._transmission_api.available + return self._tm_client.api.available async def async_added_to_hass(self): """Handle entity which will be added.""" async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update + self.hass, + self._tm_client.api.signal_options_update, + self._schedule_immediate_update, ) @callback @@ -94,12 +96,12 @@ def _schedule_immediate_update(self): def update(self): """Get the latest data from Transmission and updates the state.""" - self._data = self._transmission_api.data + self._data = self._tm_client.api.data if self.type == "completed_torrents": - self._state = self._transmission_api.get_completed_torrent_count() + self._state = self._tm_client.api.get_completed_torrent_count() elif self.type == "started_torrents": - self._state = self._transmission_api.get_started_torrent_count() + self._state = self._tm_client.api.get_started_torrent_count() if self.type == "current_status": if self._data: diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 203ed07adb54a0..45c16be36e2ca9 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -11,30 +11,25 @@ "password": "Password", "port": "Port" } - }, - "options": { - "title": "Configure Options", - "data": { - "scan_interval": "Update frequency" - } } }, "error": { + "name_exists": "Name already exists", "wrong_credentials": "Wrong username or password", "cannot_connect": "Unable to Connect to host" }, "abort": { - "one_instance_allowed": "Only a single instance is necessary." + "already_configured": "Host is already configured." } }, "options": { "step": { "init": { - "description": "Configure options for Transmission", + "title": "Configure options for Transmission", "data": { "scan_interval": "Update frequency" } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 0bb43f715aca37..4b93b3f06e205d 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -6,7 +6,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import ToggleEntity -from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SWITCH_TYPES +from .const import DOMAIN, SWITCH_TYPES _LOGGING = logging.getLogger(__name__) @@ -19,12 +19,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission switch.""" - transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + tm_client = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data[CONF_NAME] dev = [] for switch_type, switch_name in SWITCH_TYPES.items(): - dev.append(TransmissionSwitch(switch_type, switch_name, transmission_api, name)) + dev.append(TransmissionSwitch(switch_type, switch_name, tm_client, name)) async_add_entities(dev, True) @@ -32,12 +32,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TransmissionSwitch(ToggleEntity): """Representation of a Transmission switch.""" - def __init__(self, switch_type, switch_name, transmission_api, name): + def __init__(self, switch_type, switch_name, tm_client, name): """Initialize the Transmission switch.""" self._name = switch_name self.client_name = name self.type = switch_type - self._transmission_api = transmission_api + self._tm_client = tm_client self._state = STATE_OFF self._data = None @@ -46,6 +46,11 @@ def name(self): """Return the name of the switch.""" return f"{self.client_name} {self._name}" + @property + def unique_id(self): + """Return the unique id of the entity.""" + return f"{self._tm_client.api.host}-{self.name}" + @property def state(self): """Return the state of the device.""" @@ -64,32 +69,34 @@ def is_on(self): @property def available(self): """Could the device be accessed during the last update call.""" - return self._transmission_api.available + return self._tm_client.api.available def turn_on(self, **kwargs): """Turn the device on.""" if self.type == "on_off": _LOGGING.debug("Starting all torrents") - self._transmission_api.start_torrents() + self._tm_client.api.start_torrents() elif self.type == "turtle_mode": _LOGGING.debug("Turning Turtle Mode of Transmission on") - self._transmission_api.set_alt_speed_enabled(True) - self._transmission_api.update() + self._tm_client.api.set_alt_speed_enabled(True) + self._tm_client.api.update() def turn_off(self, **kwargs): """Turn the device off.""" if self.type == "on_off": _LOGGING.debug("Stoping all torrents") - self._transmission_api.stop_torrents() + self._tm_client.api.stop_torrents() if self.type == "turtle_mode": _LOGGING.debug("Turning Turtle Mode of Transmission off") - self._transmission_api.set_alt_speed_enabled(False) - self._transmission_api.update() + self._tm_client.api.set_alt_speed_enabled(False) + self._tm_client.api.update() async def async_added_to_hass(self): """Handle entity which will be added.""" async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update + self.hass, + self._tm_client.api.signal_options_update, + self._schedule_immediate_update, ) @callback @@ -100,12 +107,12 @@ def update(self): """Get the latest data from Transmission and updates the state.""" active = None if self.type == "on_off": - self._data = self._transmission_api.data + self._data = self._tm_client.api.data if self._data: active = self._data.activeTorrentCount > 0 elif self.type == "turtle_mode": - active = self._transmission_api.get_alt_speed_enabled() + active = self._tm_client.api.get_alt_speed_enabled() if active is None: return diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index e79f5c8ac96a88..28fbed9ff42fb5 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -1,4 +1,4 @@ -"""Tests for Met.no config flow.""" +"""Tests for Transmission config flow.""" from datetime import timedelta from unittest.mock import patch @@ -31,6 +31,14 @@ PORT = 9091 SCAN_INTERVAL = 10 +MOCK_ENTRY = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, +} + @pytest.fixture(name="api") def mock_transmission_api(): @@ -90,18 +98,10 @@ async def test_flow_works(hass, api): assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT - assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL # test with all provided - result = await flow.async_step_user( - { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, - } - ) + result = await flow.async_step_user(MOCK_ENTRY) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == NAME @@ -110,7 +110,7 @@ async def test_flow_works(hass, api): assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD assert result["data"][CONF_PORT] == PORT - assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL async def test_options(hass): @@ -118,14 +118,7 @@ async def test_options(hass): entry = MockConfigEntry( domain=DOMAIN, title=CONF_NAME, - data={ - "name": DEFAULT_NAME, - "host": HOST, - "username": USERNAME, - "password": PASSWORD, - "port": DEFAULT_PORT, - "options": {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, - }, + data=MOCK_ENTRY, options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) flow = init_config_flow(hass) @@ -157,7 +150,7 @@ async def test_import(hass, api): assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT - assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + assert result["data"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL # import with all result = await flow.async_step_import( @@ -177,18 +170,40 @@ async def test_import(hass, api): assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD assert result["data"][CONF_PORT] == PORT - assert result["data"]["options"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL + assert result["data"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL -async def test_integration_already_exists(hass, api): - """Test we only allow a single config flow.""" - MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} +async def test_host_already_configured(hass, api): + """Test host is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_ENTRY, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) + entry.add_to_hass(hass) + flow = init_config_flow(hass) + result = await flow.async_step_user(MOCK_ENTRY) + assert result["type"] == "abort" - assert result["reason"] == "one_instance_allowed" + assert result["reason"] == "already_configured" + + +async def test_name_already_configured(hass, api): + """Test name is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_ENTRY, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + ) + entry.add_to_hass(hass) + + mock_entry = MOCK_ENTRY.copy() + mock_entry[CONF_HOST] = "0.0.0.0" + flow = init_config_flow(hass) + result = await flow.async_step_user(mock_entry) + + assert result["type"] == "form" + assert result["errors"] == {CONF_NAME: "name_exists"} async def test_error_on_wrong_credentials(hass, auth_error): diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py new file mode 100644 index 00000000000000..4baa00de7a7044 --- /dev/null +++ b/tests/components/transmission/test_init.py @@ -0,0 +1,123 @@ +"""Tests for Transmission init.""" + +from unittest.mock import patch + +import pytest +from transmissionrpc.error import TransmissionError + +from homeassistant.components import transmission +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, mock_coro + +MOCK_ENTRY = MockConfigEntry( + domain=transmission.DOMAIN, + data={ + transmission.CONF_NAME: "Transmission", + transmission.CONF_HOST: "0.0.0.0", + transmission.CONF_USERNAME: "user", + transmission.CONF_PASSWORD: "pass", + transmission.CONF_PORT: 9091, + }, +) + + +@pytest.fixture(name="api") +def mock_transmission_api(): + """Mock an api.""" + with patch("transmissionrpc.Client"): + yield + + +@pytest.fixture(name="auth_error") +def mock_api_authentication_error(): + """Mock an api.""" + with patch( + "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") + ): + yield + + +@pytest.fixture(name="unknown_error") +def mock_api_unknown_error(): + """Mock an api.""" + with patch("transmissionrpc.Client", side_effect=TransmissionError): + yield + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything or try to set up a Transmission client.""" + assert await async_setup_component(hass, transmission.DOMAIN, {}) is True + assert transmission.DOMAIN not in hass.data + + +async def test_setup_with_config(hass, api): + """Test that we import the config and setup the client.""" + config = { + transmission.DOMAIN: { + transmission.CONF_NAME: "Transmission", + transmission.CONF_HOST: "0.0.0.0", + transmission.CONF_USERNAME: "user", + transmission.CONF_PASSWORD: "pass", + transmission.CONF_PORT: 9091, + }, + transmission.DOMAIN: { + transmission.CONF_NAME: "Transmission2", + transmission.CONF_HOST: "0.0.0.1", + transmission.CONF_USERNAME: "user", + transmission.CONF_PASSWORD: "pass", + transmission.CONF_PORT: 9091, + }, + } + assert await async_setup_component(hass, transmission.DOMAIN, config) is True + + +async def test_successful_config_entry(hass, api): + """Test that configured transmission is configured successfully.""" + + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + assert await transmission.async_setup_entry(hass, entry) is True + assert entry.options == { + transmission.CONF_SCAN_INTERVAL: transmission.DEFAULT_SCAN_INTERVAL + } + + +async def test_setup_failed(hass): + """Test transmission failed due to an error.""" + + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + # test connection error raising ConfigEntryNotReady + with patch( + "transmissionrpc.Client", + side_effect=TransmissionError("111: Connection refused"), + ), pytest.raises(ConfigEntryNotReady): + + await transmission.async_setup_entry(hass, entry) + + # test Authentication error returning false + + with patch( + "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") + ): + + assert await transmission.async_setup_entry(hass, entry) is False + + +async def test_unload_entry(hass, api): + """Test removing transmission client.""" + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + with patch.object( + hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) + ) as unload_entry: + assert await transmission.async_setup_entry(hass, entry) + + assert await transmission.async_unload_entry(hass, entry) + assert unload_entry.call_count == 2 + assert entry.entry_id not in hass.data[transmission.DOMAIN] From 6a731a68cdcb61801b995895ad3f3cc614163874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 23 Oct 2019 23:18:41 +0300 Subject: [PATCH 1229/3953] Parallelize pylint everywhere (#28149) * Run 2 pylint jobs by default * Run pylint with autodetected number of jobs in Travis Gives a ~25% speedup there at the moment. --- .travis.yml | 2 +- azure-pipelines-ci.yml | 2 +- pylintrc | 3 +++ tox.ini | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7b3765716eb20c..6d5b43c2f0350d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ matrix: - python: "3.6.1" env: TOXENV=lint - python: "3.6.1" - env: TOXENV=pylint + env: TOXENV=pylint PYLINT_ARGS=--jobs=0 - python: "3.6.1" env: TOXENV=typing - python: "3.6.1" diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 1ca834b62136a1..f1abf2ff9dba9b 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -167,7 +167,7 @@ stages: displayName: 'Install Home Assistant' - script: | . venv/bin/activate - pylint -j 2 homeassistant + pylint homeassistant displayName: 'Run pylint' - job: 'Mypy' pool: diff --git a/pylintrc b/pylintrc index 3d69800e5c3120..4aced384b63023 100644 --- a/pylintrc +++ b/pylintrc @@ -1,5 +1,8 @@ [MASTER] ignore=tests +# Use a conservative default here; 2 should speed up most setups and not hurt +# any too bad. Override on command line as appropriate. +jobs=2 [BASIC] good-names=id,i,j,k,ex,Run,_,fp diff --git a/tox.ini b/tox.ini index 0b0c969d7811f6..f6d12fe30f526f 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - pylint {posargs} homeassistant + pylint {env:PYLINT_ARGS} {posargs} homeassistant [testenv:lint] deps = From 2b36fe421c9f210b739194251f1013d8dcb4aa00 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 23 Oct 2019 22:22:07 +0200 Subject: [PATCH 1230/3953] Updated frontend to 20191023.0 (#28150) --- 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 43578420212dc0..ae53b972dcaa94 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191014.0" + "home-assistant-frontend==20191023.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 63b4673b435882..d2f10c891a95f0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191014.0 +home-assistant-frontend==20191023.0 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index cafdcae9c9b1d1..eacaee7d927d33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191014.0 +home-assistant-frontend==20191023.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2bdd47cd94661c..39903b3606a07a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191014.0 +home-assistant-frontend==20191023.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 23289459ca3a59b9329d6bd8a48a4bedac466573 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Oct 2019 13:36:38 -0700 Subject: [PATCH 1231/3953] Update translations --- .../components/adguard/.translations/fr.json | 2 + .../alarm_control_panel/.translations/fr.json | 11 +++++ .../alarm_control_panel/.translations/nl.json | 3 +- .../ambiclimate/.translations/ru.json | 2 +- .../components/axis/.translations/fr.json | 1 + .../components/axis/.translations/ru.json | 4 +- .../cert_expiry/.translations/ca.json | 6 ++- .../cert_expiry/.translations/da.json | 4 +- .../cert_expiry/.translations/en.json | 4 +- .../cert_expiry/.translations/fr.json | 4 +- .../cert_expiry/.translations/nl.json | 4 +- .../cert_expiry/.translations/no.json | 4 +- .../cert_expiry/.translations/ru.json | 6 ++- .../coolmaster/.translations/en.json | 23 +++++++++++ .../components/cover/.translations/fr.json | 10 +++++ .../components/daikin/.translations/ru.json | 2 +- .../components/deconz/.translations/ru.json | 2 +- .../components/glances/.translations/fr.json | 13 ++++-- .../components/glances/.translations/pl.json | 23 +++++++++++ .../components/glances/.translations/ru.json | 2 +- .../components/hangouts/.translations/fr.json | 2 + .../components/heos/.translations/ca.json | 2 +- .../homematicip_cloud/.translations/ru.json | 2 +- .../components/hue/.translations/ru.json | 2 +- .../components/lock/.translations/fr.json | 13 ++++++ .../opentherm_gw/.translations/fr.json | 1 + .../opentherm_gw/.translations/ru.json | 2 +- .../rainmachine/.translations/ru.json | 2 +- .../components/sensor/.translations/nl.json | 2 +- .../components/solarlog/.translations/ca.json | 21 ++++++++++ .../components/solarlog/.translations/da.json | 21 ++++++++++ .../components/solarlog/.translations/en.json | 38 +++++++++--------- .../components/solarlog/.translations/fr.json | 21 ++++++++++ .../components/solarlog/.translations/nl.json | 21 ++++++++++ .../components/solarlog/.translations/no.json | 21 ++++++++++ .../components/solarlog/.translations/ru.json | 21 ++++++++++ .../components/tradfri/.translations/ru.json | 2 +- .../transmission/.translations/en.json | 40 +++++++++++-------- .../components/unifi/.translations/fr.json | 6 +++ .../components/upnp/.translations/fr.json | 4 ++ .../components/zha/.translations/fr.json | 8 ++-- 41 files changed, 316 insertions(+), 66 deletions(-) create mode 100644 homeassistant/components/alarm_control_panel/.translations/fr.json create mode 100644 homeassistant/components/coolmaster/.translations/en.json create mode 100644 homeassistant/components/cover/.translations/fr.json create mode 100644 homeassistant/components/glances/.translations/pl.json create mode 100644 homeassistant/components/lock/.translations/fr.json create mode 100644 homeassistant/components/solarlog/.translations/ca.json create mode 100644 homeassistant/components/solarlog/.translations/da.json create mode 100644 homeassistant/components/solarlog/.translations/fr.json create mode 100644 homeassistant/components/solarlog/.translations/nl.json create mode 100644 homeassistant/components/solarlog/.translations/no.json create mode 100644 homeassistant/components/solarlog/.translations/ru.json diff --git a/homeassistant/components/adguard/.translations/fr.json b/homeassistant/components/adguard/.translations/fr.json index 6543ddd50bc4ec..749ba7d9c03dcb 100644 --- a/homeassistant/components/adguard/.translations/fr.json +++ b/homeassistant/components/adguard/.translations/fr.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Cette int\u00e9gration n\u00e9cessite AdGuard Home {minimal_version} ou une version ult\u00e9rieure, vous disposez de {current_version}. Veuillez mettre \u00e0 jour votre compl\u00e9ment Hass.io AdGuard Home.", + "adguard_home_outdated": "Cette int\u00e9gration n\u00e9cessite AdGuard Home {minimal_version} ou une version ult\u00e9rieure, vous disposez de {current_version}.", "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour.", "single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e." }, diff --git a/homeassistant/components/alarm_control_panel/.translations/fr.json b/homeassistant/components/alarm_control_panel/.translations/fr.json new file mode 100644 index 00000000000000..c3ba6db0c62661 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/fr.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armer {entity_name} mode sortie", + "arm_home": "Armer {entity_name} mode \u00e0 la maison", + "arm_night": "Armer {entity_name} mode nuit", + "disarm": "D\u00e9sarmer {entity_name}", + "trigger": "D\u00e9clencheur {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/nl.json b/homeassistant/components/alarm_control_panel/.translations/nl.json index 9f4e6a4a51c282..86cacad9fd675f 100644 --- a/homeassistant/components/alarm_control_panel/.translations/nl.json +++ b/homeassistant/components/alarm_control_panel/.translations/nl.json @@ -1,7 +1,8 @@ { "device_automation": { "action_type": { - "disarm": "Uitschakelen {entity_name}" + "disarm": "Uitschakelen {entity_name}", + "trigger": "Trigger {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json index 5a816bce1401d5..ba667ea7b9a724 100644 --- a/homeassistant/components/ambiclimate/.translations/ru.json +++ b/homeassistant/components/ambiclimate/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", - "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", + "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambiclimate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { diff --git a/homeassistant/components/axis/.translations/fr.json b/homeassistant/components/axis/.translations/fr.json index 24afb4a226ce7e..608e12d020ac13 100644 --- a/homeassistant/components/axis/.translations/fr.json +++ b/homeassistant/components/axis/.translations/fr.json @@ -12,6 +12,7 @@ "device_unavailable": "L'appareil n'est pas disponible", "faulty_credentials": "Mauvaises informations d'identification de l'utilisateur" }, + "flow_title": "Appareil Axis: {name} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 1128ad30cf5ee8..0345862b865f0b 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis." }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." diff --git a/homeassistant/components/cert_expiry/.translations/ca.json b/homeassistant/components/cert_expiry/.translations/ca.json index 25c0b26fafc285..f1df9a06be1c10 100644 --- a/homeassistant/components/cert_expiry/.translations/ca.json +++ b/homeassistant/components/cert_expiry/.translations/ca.json @@ -4,15 +4,17 @@ "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada" }, "error": { + "certificate_error": "El certificat no ha pogut ser validat", "certificate_fetch_failed": "No s'ha pogut obtenir el certificat des d'aquesta combinaci\u00f3 d'amfitri\u00f3 i port", "connection_timeout": "S'ha acabat el temps d'espera durant la connexi\u00f3 amb l'amfitri\u00f3.", "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada", - "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3" + "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3", + "wrong_host": "El certificat no coincideix amb el nom de l'amfitri\u00f3" }, "step": { "user": { "data": { - "host": "Nom d'amfitri\u00f3 del certificat", + "host": "Nom de l'amfitri\u00f3 del certificat", "name": "Nom del certificat", "port": "Port del certificat" }, diff --git a/homeassistant/components/cert_expiry/.translations/da.json b/homeassistant/components/cert_expiry/.translations/da.json index 667ab5fa4e3d01..c95a56320c985e 100644 --- a/homeassistant/components/cert_expiry/.translations/da.json +++ b/homeassistant/components/cert_expiry/.translations/da.json @@ -4,10 +4,12 @@ "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret" }, "error": { + "certificate_error": "Certifikatet kunne ikke valideres", "certificate_fetch_failed": "Kan ikke hente certifikat fra denne v\u00e6rt- og portkombination", "connection_timeout": "Timeout ved tilslutning til denne v\u00e6rt", "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret", - "resolve_failed": "V\u00e6rten kunne ikke findes" + "resolve_failed": "V\u00e6rten kunne ikke findes", + "wrong_host": "Certifikatet stemmer ikke overens med v\u00e6rtsnavnet" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index 873dfee9a92bb9..19e237a6d0527f 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -4,10 +4,12 @@ "host_port_exists": "This host and port combination is already configured" }, "error": { + "certificate_error": "Certificate could not be validated", "certificate_fetch_failed": "Can not fetch certificate from this host and port combination", "connection_timeout": "Timeout when connecting to this host", "host_port_exists": "This host and port combination is already configured", - "resolve_failed": "This host can not be resolved" + "resolve_failed": "This host can not be resolved", + "wrong_host": "Certificate does not match hostname" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/fr.json b/homeassistant/components/cert_expiry/.translations/fr.json index a3536902c76d26..9e7df5564a21d2 100644 --- a/homeassistant/components/cert_expiry/.translations/fr.json +++ b/homeassistant/components/cert_expiry/.translations/fr.json @@ -4,10 +4,12 @@ "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e" }, "error": { + "certificate_error": "Le certificat n'a pas pu \u00eatre valid\u00e9", "certificate_fetch_failed": "Impossible de r\u00e9cup\u00e9rer le certificat de cette combinaison h\u00f4te / port", "connection_timeout": "D\u00e9lai d'attente lors de la connexion \u00e0 cet h\u00f4te", "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e", - "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu" + "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu", + "wrong_host": "Le certificat ne correspond pas au nom d'h\u00f4te" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/nl.json b/homeassistant/components/cert_expiry/.translations/nl.json index 40316f008d5403..0544c8c02c141e 100644 --- a/homeassistant/components/cert_expiry/.translations/nl.json +++ b/homeassistant/components/cert_expiry/.translations/nl.json @@ -4,10 +4,12 @@ "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd" }, "error": { + "certificate_error": "Certificaat kon niet worden gevalideerd", "certificate_fetch_failed": "Kan certificaat niet ophalen van deze combinatie van host en poort", "connection_timeout": "Time-out bij verbinding maken met deze host", "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd", - "resolve_failed": "Deze host kon niet gevonden worden" + "resolve_failed": "Deze host kon niet gevonden worden", + "wrong_host": "Certificaat komt niet overeen met hostnaam" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/no.json b/homeassistant/components/cert_expiry/.translations/no.json index 73e899106c1063..fc2e98b725d0ca 100644 --- a/homeassistant/components/cert_expiry/.translations/no.json +++ b/homeassistant/components/cert_expiry/.translations/no.json @@ -4,10 +4,12 @@ "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert" }, "error": { + "certificate_error": "Sertifikatet kunne ikke valideres", "certificate_fetch_failed": "Kan ikke hente sertifikat fra denne verts- og portkombinasjonen", "connection_timeout": "Tidsavbrudd n\u00e5r du kobler til denne verten", "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert", - "resolve_failed": "Denne verten kan ikke l\u00f8ses" + "resolve_failed": "Denne verten kan ikke l\u00f8ses", + "wrong_host": "Sertifikatet samsvarer ikke med vertsnavn" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json index f9f9e2063bee54..8c0f230382a382 100644 --- a/homeassistant/components/cert_expiry/.translations/ru.json +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -4,15 +4,17 @@ "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." }, "error": { + "certificate_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442.", "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430.", "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442." + "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442.", + "wrong_host": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u043c\u0443 \u0438\u043c\u0435\u043d\u0438." }, "step": { "user": { "data": { - "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430", + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "port": "\u041f\u043e\u0440\u0442" }, diff --git a/homeassistant/components/coolmaster/.translations/en.json b/homeassistant/components/coolmaster/.translations/en.json new file mode 100644 index 00000000000000..6c30efc594a235 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Failed to connect to CoolMasterNet instance. Please check your host.", + "no_units": "Could not find any HVAC units in CoolMasterNet host." + }, + "step": { + "user": { + "data": { + "cool": "Support cool mode", + "dry": "Support dry mode", + "fan_only": "Support fan only mode", + "heat": "Support heat mode", + "heat_cool": "Support automatic heat/cool mode", + "host": "Host", + "off": "Can be turned off" + }, + "title": "Setup your CoolMasterNet connection details." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/fr.json b/homeassistant/components/cover/.translations/fr.json new file mode 100644 index 00000000000000..95978ed0fa5da7 --- /dev/null +++ b/homeassistant/components/cover/.translations/fr.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est ferm\u00e9", + "is_closing": "{entity_name} se ferme", + "is_open": "{entity_name} est ouvert", + "is_opening": "{entity_name} est en train de s'ouvrir" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index 98ab98e6b170d2..00a517f701fe30 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index d3a8781bb4eef8..2dc3df17aa9069 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", diff --git a/homeassistant/components/glances/.translations/fr.json b/homeassistant/components/glances/.translations/fr.json index d7b3dc8a4487f6..0391012c4cd2c5 100644 --- a/homeassistant/components/glances/.translations/fr.json +++ b/homeassistant/components/glances/.translations/fr.json @@ -14,18 +14,23 @@ "name": "Nom", "password": "Mot de passe", "port": "Port", + "ssl": "V\u00e9rifier la certification du syst\u00e8me", "username": "Nom d'utilisateur", - "verify_ssl": "V\u00e9rifier la certification du syst\u00e8me" - } + "verify_ssl": "V\u00e9rifier la certification du syst\u00e8me", + "version": "Glances API Version (2 ou 3)" + }, + "title": "Installation de Glances" } - } + }, + "title": "Glances" }, "options": { "step": { "init": { "data": { "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" - } + }, + "description": "Configurer les options pour Glances" } } } diff --git a/homeassistant/components/glances/.translations/pl.json b/homeassistant/components/glances/.translations/pl.json new file mode 100644 index 00000000000000..21052c7acdcf3c --- /dev/null +++ b/homeassistant/components/glances/.translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Host jest ju\u017c skonfigurowany." + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem", + "wrong_version": "Wersja nieobs\u0142ugiwana (tylko 2 lub 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z systemem Glances", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/ru.json b/homeassistant/components/glances/.translations/ru.json index 597a914a88d312..8effcc6ab163ef 100644 --- a/homeassistant/components/glances/.translations/ru.json +++ b/homeassistant/components/glances/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", diff --git a/homeassistant/components/hangouts/.translations/fr.json b/homeassistant/components/hangouts/.translations/fr.json index 0b6dbfcbe4435a..13142fee5137cc 100644 --- a/homeassistant/components/hangouts/.translations/fr.json +++ b/homeassistant/components/hangouts/.translations/fr.json @@ -14,6 +14,7 @@ "data": { "2fa": "Code PIN d'authentification \u00e0 2 facteurs" }, + "description": "Vide", "title": "Authentification \u00e0 2 facteurs" }, "user": { @@ -22,6 +23,7 @@ "email": "Adresse e-mail", "password": "Mot de passe" }, + "description": "Vide", "title": "Connexion \u00e0 Google Hangouts" } }, diff --git a/homeassistant/components/heos/.translations/ca.json b/homeassistant/components/heos/.translations/ca.json index 60bd780547c8cc..0987e11430b61b 100644 --- a/homeassistant/components/heos/.translations/ca.json +++ b/homeassistant/components/heos/.translations/ca.json @@ -12,7 +12,7 @@ "access_token": "Amfitri\u00f3", "host": "Amfitri\u00f3" }, - "description": "Introdueix el nom d'amfitri\u00f3 o l'adre\u00e7a IP d'un dispositiu Heos (preferiblement un connectat a la xarxa per cable).", + "description": "Introdueix el nom de l'amfitri\u00f3 o l'adre\u00e7a IP d'un dispositiu Heos (preferiblement un connectat a la xarxa per cable).", "title": "Connexi\u00f3 amb Heos" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 3170f4bf6cc193..57ab265d1c2093 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP.", "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index 08fda906ea96de..c749a498e44eab 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d.", diff --git a/homeassistant/components/lock/.translations/fr.json b/homeassistant/components/lock/.translations/fr.json new file mode 100644 index 00000000000000..748a1e9290c6e1 --- /dev/null +++ b/homeassistant/components/lock/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "V\u00e9rouiller {entity_name}", + "open": "Ouvre {entity_name}", + "unlock": "D\u00e9verrouiller {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} est verrouill\u00e9", + "is_unlocked": "{entity_name} est d\u00e9verrouill\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json index cfdc6b9a738577..edde63d62b47ba 100644 --- a/homeassistant/components/opentherm_gw/.translations/fr.json +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -24,6 +24,7 @@ "step": { "init": { "data": { + "floor_temperature": "Temp\u00e9rature du sol", "precision": "Pr\u00e9cision" }, "description": "Options pour la passerelle OpenTherm" diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json index f38dd669d24f6e..0719857a7d30e5 100644 --- a/homeassistant/components/opentherm_gw/.translations/ru.json +++ b/homeassistant/components/opentherm_gw/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", "serial_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index df9adf2d989cd1..afaa55424d25a8 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "ip_address": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "ip_address": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442" }, diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json index f9cd8475a4cc33..33a7d837d555de 100644 --- a/homeassistant/components/sensor/.translations/nl.json +++ b/homeassistant/components/sensor/.translations/nl.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} batterijniveau", + "is_battery_level": "Huidige batterijniveau {entity_name}", "is_humidity": "{entity_name} vochtigheidsgraad", "is_illuminance": "{entity_name} verlichtingssterkte", "is_power": "{entity_name}\nvermogen", diff --git a/homeassistant/components/solarlog/.translations/ca.json b/homeassistant/components/solarlog/.translations/ca.json new file mode 100644 index 00000000000000..6a041c7ea4f782 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "No s'ha pogut connectar, verifica l'adre\u00e7a de l'amfitri\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP del dispositiu Solar-Log", + "name": "Prefix utilitzat pels sensors de Solar-Log" + }, + "title": "Configuraci\u00f3 de la connexi\u00f3 amb Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/da.json b/homeassistant/components/solarlog/.translations/da.json new file mode 100644 index 00000000000000..a344832c61cbe9 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheden er allerede konfigureret" + }, + "error": { + "already_configured": "Enheden er allerede konfigureret", + "cannot_connect": "Kunne ikke oprette forbindelse, verificer v\u00e6rtsadressen" + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rtsnavnet eller ip-adressen p\u00e5 din Solar-Log-enhed", + "name": "Pr\u00e6fikset, der skal bruges til dine Solar-Log sensorer" + }, + "title": "Angiv dit Solar-Log forbindelse" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/en.json b/homeassistant/components/solarlog/.translations/en.json index 5399d5176c9366..f1396045819cae 100644 --- a/homeassistant/components/solarlog/.translations/en.json +++ b/homeassistant/components/solarlog/.translations/en.json @@ -1,21 +1,21 @@ { - "config": { - "title": "Solar-Log", - "step": { - "user": { - "title": "Define your Solar-Log connection", - "data": { - "host": "The hostname or ip-address of your Solar-Log device", - "name": "The prefix to be used for your Solar-Log sensors" - } - } - }, - "error": { - "already_configured": "Device is already configured", - "cannot_connect": "Failed to connect, please verify host address" - }, - "abort": { - "already_configured": "Device is already configured" + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect, please verify host address" + }, + "step": { + "user": { + "data": { + "host": "The hostname or ip-address of your Solar-Log device", + "name": "The prefix to be used for your Solar-Log sensors" + }, + "title": "Define your Solar-Log connection" + } + }, + "title": "Solar-Log" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/fr.json b/homeassistant/components/solarlog/.translations/fr.json new file mode 100644 index 00000000000000..0f1b4944ed9b9b --- /dev/null +++ b/homeassistant/components/solarlog/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de la connexion, veuillez v\u00e9rifier l'adresse de l'h\u00f4te." + }, + "step": { + "user": { + "data": { + "host": "Le nom d'h\u00f4te ou l'adresse IP de votre p\u00e9riph\u00e9rique Solar-Log", + "name": "Le pr\u00e9fixe \u00e0 utiliser pour vos capteurs Solar-Log" + }, + "title": "D\u00e9finissez votre connexion Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/nl.json b/homeassistant/components/solarlog/.translations/nl.json new file mode 100644 index 00000000000000..3965f71e992d54 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Verbinding mislukt, controleer het host-adres" + }, + "step": { + "user": { + "data": { + "host": "De hostnaam of het IP-adres van uw Solar-Log apparaat", + "name": "Het voorvoegsel dat moet worden gebruikt voor uw Solar-Log sensoren" + }, + "title": "Definieer uw Solar-Log verbinding" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/no.json b/homeassistant/components/solarlog/.translations/no.json new file mode 100644 index 00000000000000..017e886c817c53 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Kunne ikke koble til, vennligst bekreft vertsadresse" + }, + "step": { + "user": { + "data": { + "host": "Vertsnavnet eller ip-adressen til din Solar-Log-enhet", + "name": "Prefikset som skal brukes til dine Solar-Log sensorer" + }, + "title": "Definer din Solar-Log tilkobling" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/ru.json b/homeassistant/components/solarlog/.translations/ru.json new file mode 100644 index 00000000000000..7f40935e5a5bb2 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 Solar-Log" + }, + "title": "Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/ru.json b/homeassistant/components/tradfri/.translations/ru.json index c9121862caf8d8..2e3dc8331be64f 100644 --- a/homeassistant/components/tradfri/.translations/ru.json +++ b/homeassistant/components/tradfri/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430." }, "error": { diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index 45c16be36e2ca9..aa8b99a49142ec 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -1,34 +1,42 @@ { "config": { - "title": "Transmission", + "abort": { + "already_configured": "Host is already configured.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "error": { + "cannot_connect": "Unable to Connect to host", + "name_exists": "Name already exists", + "wrong_credentials": "Wrong username or password" + }, "step": { + "options": { + "data": { + "scan_interval": "Update frequency" + }, + "title": "Configure Options" + }, "user": { - "title": "Setup Transmission Client", "data": { - "name": "Name", "host": "Host", - "username": "Username", + "name": "Name", "password": "Password", - "port": "Port" - } + "port": "Port", + "username": "Username" + }, + "title": "Setup Transmission Client" } }, - "error": { - "name_exists": "Name already exists", - "wrong_credentials": "Wrong username or password", - "cannot_connect": "Unable to Connect to host" - }, - "abort": { - "already_configured": "Host is already configured." - } + "title": "Transmission" }, "options": { "step": { "init": { - "title": "Configure options for Transmission", "data": { "scan_interval": "Update frequency" - } + }, + "description": "Configure options for Transmission", + "title": "Configure options for Transmission" } } } diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index c40b7822073dc9..0a100be0a114db 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -33,6 +33,12 @@ "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" } }, + "init": { + "data": { + "one": "Vide", + "other": "Vide" + } + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau" diff --git a/homeassistant/components/upnp/.translations/fr.json b/homeassistant/components/upnp/.translations/fr.json index a87ea9ec9c79d0..6864658b37940e 100644 --- a/homeassistant/components/upnp/.translations/fr.json +++ b/homeassistant/components/upnp/.translations/fr.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Activer au moins les capteurs ou la cartographie des ports", "single_instance_allowed": "Une seule configuration UPnP / IGD est n\u00e9cessaire." }, + "error": { + "one": "Vide", + "other": "Vide" + }, "step": { "confirm": { "description": "Voulez-vous configurer UPnP / IGD?", diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index b4adac8e997dce..9b1ba025d7c3d6 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -54,14 +54,14 @@ "device_shaken": "Appareil secou\u00e9", "device_slid": "Appareil gliss\u00e9 \"{subtype}\"", "device_tilted": "Dispositif inclin\u00e9", - "remote_button_double_press": "Bouton \"{subtype}\" cliqu\u00e9", + "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9", "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", - "remote_button_quadruple_press": "bouton \" {subtype} \" quadruple clic", - "remote_button_quintuple_press": "bouton \" {subtype} \" quintuple clic", + "remote_button_quadruple_press": "bouton \" {subtype} \" quadruple clics", + "remote_button_quintuple_press": "bouton \" {subtype} \" quintuple clics", "remote_button_short_press": "bouton \" {subtype} \" enfonc\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", - "remote_button_triple_press": "Bouton\"{sous-type}\" \u00e0 trois clics" + "remote_button_triple_press": "Bouton \"{subtype}\" \u00e0 trois clics" } } } \ No newline at end of file From 3b934166a5300c1ac0cc67fe3b87e6542937d02a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Oct 2019 13:37:01 -0700 Subject: [PATCH 1232/3953] Bumped version to 0.101.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cac0386b8120bd..770669b2d82a98 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -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, 1) From 1b216856517933580cd50bd1eb8c1923b73ee44a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Oct 2019 13:38:35 -0700 Subject: [PATCH 1233/3953] Version bump to 102.0.dev0" --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cac0386b8120bd..e1e9757dd02bb6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 101 +MINOR_VERSION = 102 PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) From 160c201be16678c6aa2416a0be20898eb2d86fe8 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 23 Oct 2019 23:13:56 +0200 Subject: [PATCH 1234/3953] Bump songpal to fix a regression (#28115) The new release fixes a single regression from requests to aiohttp conversion. Some devices do not respond with the correct mimetype which was not enforced by requests but is enforced by aiohttp. Related PR https://github.com/rytilahti/python-songpal/pull/59 --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 55b02b66a59bc4..b090a90d719c85 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Songpal", "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ - "python-songpal==0.11.1" + "python-songpal==0.11.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index eacaee7d927d33..eeb9d0cba20f2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1561,7 +1561,7 @@ python-ripple-api==0.0.3 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.11.1 +python-songpal==0.11.2 # homeassistant.components.synologydsm python-synology==0.2.0 From 6a1501b59c4d65ae158213c6a69b65acef06a809 Mon Sep 17 00:00:00 2001 From: Tyler Page Date: Thu, 24 Oct 2019 00:10:57 +0000 Subject: [PATCH 1235/3953] Cover all possible values for venstar operation_mode (#27949) * cover all possible values for operation_mode * Update climate.py * Update climate.py * Update climate.py mapped homeassistant constants to client ones so we don't have to check for both * black + pylint * move mode_map to __init__() * Update climate.py * Update climate.py --- homeassistant/components/venstar/climate.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 81afef975412f3..280e691337dc7b 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -98,6 +98,11 @@ def __init__(self, client, humidifier): """Initialize the thermostat.""" self._client = client self._humidifier = humidifier + self._mode_map = { + HVAC_MODE_HEAT: self._client.MODE_HEAT, + HVAC_MODE_COOL: self._client.MODE_COOL, + HVAC_MODE_AUTO: self._client.MODE_AUTO, + } def update(self): """Update the data from the thermostat.""" @@ -266,20 +271,20 @@ def _set_operation_mode(self, operation_mode): def set_temperature(self, **kwargs): """Set a new target temperature.""" set_temp = True - operation_mode = kwargs.get(ATTR_HVAC_MODE, self._client.mode) + operation_mode = kwargs.get(ATTR_HVAC_MODE) temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) temperature = kwargs.get(ATTR_TEMPERATURE) - if operation_mode != self._client.mode: - set_temp = self._set_operation_mode(operation_mode) + if operation_mode and self._mode_map.get(operation_mode) != self._client.mode: + set_temp = self._set_operation_mode(self._mode_map.get(operation_mode)) if set_temp: - if operation_mode == self._client.MODE_HEAT: + if self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_HEAT: success = self._client.set_setpoints(temperature, self._client.cooltemp) - elif operation_mode == self._client.MODE_COOL: + elif self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_COOL: success = self._client.set_setpoints(self._client.heattemp, temperature) - elif operation_mode == self._client.MODE_AUTO: + elif self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_AUTO: success = self._client.set_setpoints(temp_low, temp_high) else: success = False From 8c31afc31e3f33a30baa5e9817bb8fb8362d1263 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 24 Oct 2019 00:32:18 +0000 Subject: [PATCH 1236/3953] [ci skip] Translation update --- .../components/adguard/.translations/it.json | 2 + .../cert_expiry/.translations/it.json | 4 +- .../coolmaster/.translations/it.json | 23 ++++++++++++ .../components/glances/.translations/it.json | 37 +++++++++++++++++++ .../components/lock/.translations/it.json | 5 +++ .../opentherm_gw/.translations/it.json | 11 ++++++ .../components/sensor/.translations/it.json | 36 +++++++++--------- .../components/solarlog/.translations/it.json | 21 +++++++++++ .../transmission/.translations/it.json | 5 ++- 9 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/coolmaster/.translations/it.json create mode 100644 homeassistant/components/glances/.translations/it.json create mode 100644 homeassistant/components/solarlog/.translations/it.json diff --git a/homeassistant/components/adguard/.translations/it.json b/homeassistant/components/adguard/.translations/it.json index 57f81dc1d99ad5..1b3ce014d905d1 100644 --- a/homeassistant/components/adguard/.translations/it.json +++ b/homeassistant/components/adguard/.translations/it.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo Hass.io AdGuard Home.", + "adguard_home_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}.", "existing_instance_updated": "Configurazione esistente aggiornata.", "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home." }, diff --git a/homeassistant/components/cert_expiry/.translations/it.json b/homeassistant/components/cert_expiry/.translations/it.json index 73749382dd9bca..d95b9cd84a16c4 100644 --- a/homeassistant/components/cert_expiry/.translations/it.json +++ b/homeassistant/components/cert_expiry/.translations/it.json @@ -4,10 +4,12 @@ "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata" }, "error": { + "certificate_error": "Il certificato non pu\u00f2 essere convalidato", "certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta", "connection_timeout": "Tempo scaduto collegandosi a questo host", "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", - "resolve_failed": "Questo host non pu\u00f2 essere risolto" + "resolve_failed": "Questo host non pu\u00f2 essere risolto", + "wrong_host": "Il certificato non corrisponde al nome host" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/it.json b/homeassistant/components/coolmaster/.translations/it.json new file mode 100644 index 00000000000000..b543a10d32d1dc --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Impossibile connettersi all'istanza CoolMasterNet. Controlla il tuo host.", + "no_units": "Impossibile trovare alcuna unit\u00e0 HVAC nell'host CoolMasterNet." + }, + "step": { + "user": { + "data": { + "cool": "Supporta la modalit\u00e0 fresco", + "dry": "Supporta la modalit\u00e0 asciutto", + "fan_only": "Supporta la modalit\u00e0 solo ventilatore", + "heat": "Supporta la modalit\u00e0 di riscaldamento", + "heat_cool": "Supporta la modalit\u00e0 di riscaldamento/raffreddamento automatica", + "host": "Host", + "off": "Pu\u00f2 essere spento" + }, + "title": "Impostare i dettagli della connessione CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/it.json b/homeassistant/components/glances/.translations/it.json new file mode 100644 index 00000000000000..5fbfba547d9bb9 --- /dev/null +++ b/homeassistant/components/glances/.translations/it.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "L'host \u00e8 gi\u00e0 configurato." + }, + "error": { + "cannot_connect": "Impossibile connettersi all'host", + "wrong_version": "Versione non supportata (solo 2 o 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nome", + "password": "Password", + "port": "Porta", + "ssl": "Utilizzare SSL/TLS per connettersi al sistema Glances", + "username": "Nome utente", + "verify_ssl": "Verificare la certificazione del sistema", + "version": "Glances API Version (2 o 3)" + }, + "title": "Impostare Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequenza di aggiornamento" + }, + "description": "Configura le opzioni per Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/it.json b/homeassistant/components/lock/.translations/it.json index f56ef71060bc60..05f0db78cdf43e 100644 --- a/homeassistant/components/lock/.translations/it.json +++ b/homeassistant/components/lock/.translations/it.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Blocca {entity_name}", + "open": "Apri {entity_name}", + "unlock": "Sblocca {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} \u00e8 bloccato", "is_unlocked": "{entity_name} \u00e8 sbloccato" diff --git a/homeassistant/components/opentherm_gw/.translations/it.json b/homeassistant/components/opentherm_gw/.translations/it.json index 9c62686e19083d..73c3a8db970914 100644 --- a/homeassistant/components/opentherm_gw/.translations/it.json +++ b/homeassistant/components/opentherm_gw/.translations/it.json @@ -19,5 +19,16 @@ } }, "title": "Gateway OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura del pavimento", + "precision": "Precisione" + }, + "description": "Opzioni per OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/it.json b/homeassistant/components/sensor/.translations/it.json index 07b20245c1634d..cb643bbdd2929b 100644 --- a/homeassistant/components/sensor/.translations/it.json +++ b/homeassistant/components/sensor/.translations/it.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "Livello della batteria di {entity_name}", - "is_humidity": "Umidit\u00e0 di {entity_name}", - "is_illuminance": "Illuminazione di {entity_name}", - "is_power": "Potenza di {entity_name}", - "is_pressure": "Pressione di {entity_name}", - "is_signal_strength": "Potenza del segnale di {entity_name}", - "is_temperature": "Temperatura di {entity_name}", - "is_timestamp": "Data di {entity_name}", - "is_value": "Valore di {entity_name}" + "is_battery_level": "Livello della batteria attuale di {entity_name}", + "is_humidity": "Umidit\u00e0 attuale di {entity_name}", + "is_illuminance": "Illuminazione attuale di {entity_name}", + "is_power": "Alimentazione attuale di {entity_name}", + "is_pressure": "Pressione attuale di {entity_name}", + "is_signal_strength": "Potenza del segnale attuale di {entity_name}", + "is_temperature": "Temperatura attuale di {entity_name}", + "is_timestamp": "Data e ora attuali di {nome_entit\u00e0}", + "is_value": "Valore attuale di {entity_name}" }, "trigger_type": { - "battery_level": "Livello della batteria di {entity_name}", - "humidity": "Umidit\u00e0 di {entity_name}", - "illuminance": "Illuminazione di {entity_name}", - "power": "Potenza di {entity_name}", - "pressure": "Pressione di {entity_name}", - "signal_strength": "Potenza del segnale di {entity_name}", - "temperature": "Temperatura di {entity_name}", - "timestamp": "Data di {entity_name}", - "value": "Valore di {entity_name}" + "battery_level": "variazioni del livello di batteria di {entity_name} ", + "humidity": "variazioni di umidit\u00e0 di {entity_name} ", + "illuminance": "variazioni dell'illuminazione di {entity_name} ", + "power": "variazioni di alimentazione di {entity_name}", + "pressure": "variazioni della pressione di {entity_name}", + "signal_strength": "variazioni della potenza del segnale di {entity_name} ", + "temperature": "variazioni di temperatura di {entity_name} ", + "timestamp": "variazioni di data e ora di {entity_name} ", + "value": "{entity_name} valori cambiati" } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/it.json b/homeassistant/components/solarlog/.translations/it.json new file mode 100644 index 00000000000000..65c13f052d3428 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi, verifica l'indirizzo host" + }, + "step": { + "user": { + "data": { + "host": "Il nome host o l'indirizzo IP del dispositivo Solar-Log", + "name": "Il prefisso da utilizzare per i sensori Solar-Log" + }, + "title": "Definire la connessione Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/it.json b/homeassistant/components/transmission/.translations/it.json index 17a03b6dba1745..a7c4c675856e76 100644 --- a/homeassistant/components/transmission/.translations/it.json +++ b/homeassistant/components/transmission/.translations/it.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "L'host \u00e8 gi\u00e0 configurato.", "one_instance_allowed": "\u00c8 necessaria solo una singola istanza." }, "error": { "cannot_connect": "Impossibile connettersi all'host", + "name_exists": "Il nome \u00e8 gi\u00e0 esistente", "wrong_credentials": "Nome utente o password non validi" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Frequenza di aggiornamento" }, - "description": "Configurare le opzioni per Trasmissione" + "description": "Configurare le opzioni per Trasmissione", + "title": "Configurare le opzioni per Transmission" } } } From dd9ca70e96465b903cffc5bd2ba693ff57eafcaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Thu, 24 Oct 2019 04:03:25 +0000 Subject: [PATCH 1237/3953] Add onvif local datetime support (#26656) * Update camera.py * Add onvif local datetime support * Correct capture TimeZone * Replace one line if-statement by if-block --- homeassistant/components/onvif/camera.py | 33 +++++++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index c73886c13c03e9..59ee8a8c7ee670 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -192,8 +192,27 @@ async def async_check_date_and_time(self): try: system_date = dt_util.utcnow() device_time = await devicemgmt.GetSystemDateAndTime() - if device_time: + if not device_time: + _LOGGER.debug( + """Couldn't get camera '%s' date/time. + GetSystemDateAndTime() return null/empty""", + self._name, + ) + return + + if device_time.UTCDateTime: + tzone = dt_util.UTC cdate = device_time.UTCDateTime + else: + tzone = ( + dt_util.get_time_zone(device_time.TimeZone) + or dt_util.DEFAULT_TIME_ZONE, + ) + cdate = device_time.LocalDateTime + + if cdate is None: + _LOGGER.warning("Could not retrieve date/time on this camera") + else: cam_date = dt.datetime( cdate.Date.Year, cdate.Date.Month, @@ -202,11 +221,17 @@ async def async_check_date_and_time(self): cdate.Time.Minute, cdate.Time.Second, 0, - dt_util.UTC, + tzone, ) + cam_date_utc = cam_date.astimezone(dt_util.UTC) + + _LOGGER.debug("TimeZone for date/time: %s", tzone) + _LOGGER.debug("Camera date/time: %s", cam_date) + _LOGGER.debug("Camera date/time in UTC: %s", cam_date_utc) + _LOGGER.debug("System date/time: %s", system_date) dt_diff = cam_date - system_date @@ -214,10 +239,10 @@ async def async_check_date_and_time(self): if dt_diff_seconds > 5: _LOGGER.warning( - "The date/time on the camera is '%s', " + "The date/time on the camera (UTC) is '%s', " "which is different from the system '%s', " "this could lead to authentication issues", - cam_date, + cam_date_utc, system_date, ) except ServerDisconnectedError as err: From d44de6dd2bb2853f9a9a61b8a51355c14b40b86b Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Thu, 24 Oct 2019 16:03:29 +0200 Subject: [PATCH 1238/3953] Fix Venstar formatting to restore clean CI (#28171) --- homeassistant/components/venstar/climate.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 280e691337dc7b..9e5450addc591c 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -280,11 +280,20 @@ def set_temperature(self, **kwargs): set_temp = self._set_operation_mode(self._mode_map.get(operation_mode)) if set_temp: - if self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_HEAT: + if ( + self._mode_map.get(operation_mode, self._client.mode) + == self._client.MODE_HEAT + ): success = self._client.set_setpoints(temperature, self._client.cooltemp) - elif self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_COOL: + elif ( + self._mode_map.get(operation_mode, self._client.mode) + == self._client.MODE_COOL + ): success = self._client.set_setpoints(self._client.heattemp, temperature) - elif self._mode_map.get(operation_mode, self._client.mode) == self._client.MODE_AUTO: + elif ( + self._mode_map.get(operation_mode, self._client.mode) + == self._client.MODE_AUTO + ): success = self._client.set_setpoints(temp_low, temp_high) else: success = False From b1fcecd5268f6547bb5ad811c15866701c6f5b21 Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Thu, 24 Oct 2019 17:59:25 +0200 Subject: [PATCH 1239/3953] Add Unifi Led (#27475) * Added Unifi Led * fixed manifest * fixed style issue * removed unused setting * added sugested changes. * fixed order * fixed settings that are required * Fix review issues * fix variable name that was too short * Testing something * Reverted to a previous version for testing * Reverted testing changes. --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/unifiled/__init__.py | 1 + homeassistant/components/unifiled/light.py | 105 ++++++++++++++++++ .../components/unifiled/manifest.json | 8 ++ requirements_all.txt | 3 + 6 files changed, 119 insertions(+) create mode 100644 homeassistant/components/unifiled/__init__.py create mode 100644 homeassistant/components/unifiled/light.py create mode 100644 homeassistant/components/unifiled/manifest.json diff --git a/.coveragerc b/.coveragerc index f97a7524a21532..3350bc359af120 100644 --- a/.coveragerc +++ b/.coveragerc @@ -719,6 +719,7 @@ omit = homeassistant/components/uber/sensor.py homeassistant/components/ubus/device_tracker.py homeassistant/components/ue_smart_radio/media_player.py + homeassistant/components/unifiled/* homeassistant/components/upcloud/* homeassistant/components/upnp/* homeassistant/components/upc_connect/* diff --git a/CODEOWNERS b/CODEOWNERS index eb29ee28915738..809101a5271ec0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -311,6 +311,7 @@ homeassistant/components/twentemilieu/* @frenck homeassistant/components/twilio_call/* @robbiet480 homeassistant/components/twilio_sms/* @robbiet480 homeassistant/components/unifi/* @kane610 +homeassistant/components/unifiled/* @florisvdk homeassistant/components/upc_connect/* @pvizeli homeassistant/components/upcloud/* @scop homeassistant/components/updater/* @home-assistant/core diff --git a/homeassistant/components/unifiled/__init__.py b/homeassistant/components/unifiled/__init__.py new file mode 100644 index 00000000000000..8543cc1a8fdce3 --- /dev/null +++ b/homeassistant/components/unifiled/__init__.py @@ -0,0 +1 @@ +"""Unifi LED Lights integration.""" diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py new file mode 100644 index 00000000000000..3dd1a8d5dc9975 --- /dev/null +++ b/homeassistant/components/unifiled/light.py @@ -0,0 +1,105 @@ +"""Support for Unifi Led lights.""" +import logging + +from unifiled import unifiled +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + Light, +) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +# Validation of the user's configuration +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_PORT, default=20443): vol.All(cv.port, cv.string), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Unifi LED platform.""" + + # Assign configuration variables. + # The configuration check takes care they are present. + host = config[CONF_HOST] + port = config[CONF_PORT] + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + api = unifiled(host, port, username=username, password=password) + + # Verify that passed in configuration works + if not api.getloginstate(): + _LOGGER.error("Could not connect to unifiled controller") + return + + add_entities(UnifiLedLight(light, api) for light in api.getlights()) + + +class UnifiLedLight(Light): + """Representation of an unifiled Light.""" + + def __init__(self, light, api): + """Init Unifi LED Light.""" + + self._api = api + self._light = light + self._name = light["name"] + self._unique_id = light["id"] + self._state = light["status"]["output"] + self._brightness = self._api.convertfrom100to255(light["status"]["led"]) + self._features = SUPPORT_BRIGHTNESS + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def brightness(self): + """Return the brightness name of this light.""" + return self._brightness + + @property + def unique_id(self): + """Return the unique id of this light.""" + return self._unique_id + + @property + def is_on(self): + """Return true if light is on.""" + return self._state + + @property + def supported_features(self): + """Return the supported features of this light.""" + return self._features + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + self._api.setdevicebrightness( + self._unique_id, + str(self._api.convertfrom255to100(kwargs.get(ATTR_BRIGHTNESS, 255))), + ) + self._api.setdeviceoutput(self._unique_id, 1) + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._api.setdeviceoutput(self._unique_id, 0) + + def update(self): + """Update the light states.""" + self._state = self._api.getlightstate(self._unique_id) + self._brightness = self._api.convertfrom100to255( + self._api.getlightbrightness(self._unique_id) + ) diff --git a/homeassistant/components/unifiled/manifest.json b/homeassistant/components/unifiled/manifest.json new file mode 100644 index 00000000000000..fbf05470c6dafe --- /dev/null +++ b/homeassistant/components/unifiled/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "unifiled", + "name": "Unifi LED", + "documentation": "https://www.home-assistant.io/integrations/unifiled", + "dependencies": [], + "codeowners": ["@florisvdk"], + "requirements": ["unifiled==0.10"] +} diff --git a/requirements_all.txt b/requirements_all.txt index eeb9d0cba20f2f..0bb4afc2b8493a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1917,6 +1917,9 @@ twentemilieu==0.1.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.unifiled +unifiled==0.10 + # homeassistant.components.upcloud upcloud-api==0.4.3 From 969322e14a4db0a8edce8d9192e56b60b8f671c5 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 24 Oct 2019 12:23:02 -0400 Subject: [PATCH 1240/3953] Fixes/zha ieee tail (#28160) * Fix ZHA entity_id assignment. * Update tests. --- homeassistant/components/zha/entity.py | 2 +- tests/components/zha/common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 00c3942358e1fe..c11cd405a99402 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -40,7 +40,7 @@ def __init__(self, unique_id, zha_device, channels, skip_entity_id=False, **kwar self._unique_id = unique_id if not skip_entity_id: ieee = zha_device.ieee - ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]]) + ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) self.entity_id = "{}.{}_{}_{}_{}{}".format( self._domain, slugify(zha_device.manufacturer), diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 5f9172749b047c..788faaaec737f1 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -168,7 +168,7 @@ def make_entity_id(domain, device, cluster, use_suffix=True): machine so that we can test state changes. """ ieee = device.ieee - ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]]) + ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) entity_id = "{}.{}_{}_{}_{}{}".format( domain, slugify(device.manufacturer), From fc09702cc33036b46d2b1566eebdabd6696bbd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 24 Oct 2019 19:31:49 +0300 Subject: [PATCH 1241/3953] Modernize Huawei LTE (#26675) * Modernization rework - config entry support, with override support from huawei_lte platform in YAML - device tracker entity registry support - refactor for easier addition of more features - internal code cleanups * Remove log level dependent subscription/data debug hack No longer needed, because pretty much all keys from supported categories are exposed as sensors. Closes https://github.com/home-assistant/home-assistant/issues/23819 * Upgrade huawei-lte-api to 1.4.1 https://github.com/Salamek/huawei-lte-api/releases * Add support for access without username and password * Use subclass init instead of config_entries.HANDLERS * Update huawei-lte-api to 1.4.3 (#27269) * Convert device state attributes to snake_case * Simplify scanner entity initialization * Remove not needed hass reference from Router * Return explicit None from unsupported old device tracker setup * Mark unknown connection errors during config as such * Drop some dead config flow code * Run config flow sync I/O in executor * Parametrize config flow login error tests * Forward entry unload to platforms * Async/sync fixups * Improve data subscription debug logging * Implement on the fly add of new and tracking of seen device tracker entities * Handle device tracker entry unload cleanup in component * Remove unnecessary _async_setup_lte, just have code in async_setup_entry * Remove time tracker on unload * Fix to not use same mutable default subscription set for all routers * Pylint fixes * Remove some redundant defensive device tracker code * Add back explicit get_scanner None return, hush pylint * Adjust approach to set system_options on entry create * Enable some sensors on first add instead of disabling everything * Fix SMS notification recipients default value * Add option to skip new device tracker entities * Fix SMS notification recipient option default * Work around https://github.com/PyCQA/pylint/issues/3202 * Remove unrelated type hint additions * Change async_add_new_entities to a regular function * Remove option to disable polling for new device tracker entries --- .../huawei_lte/.translations/en.json | 39 ++ .../components/huawei_lte/__init__.py | 439 ++++++++++++++---- .../components/huawei_lte/config_flow.py | 208 +++++++++ homeassistant/components/huawei_lte/const.py | 16 + .../components/huawei_lte/device_tracker.py | 193 ++++++-- .../components/huawei_lte/manifest.json | 5 +- homeassistant/components/huawei_lte/notify.py | 48 +- homeassistant/components/huawei_lte/sensor.py | 157 ++++--- .../components/huawei_lte/strings.json | 39 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- .../components/huawei_lte/test_config_flow.py | 140 ++++++ .../huawei_lte/test_device_tracker.py | 20 + tests/components/huawei_lte/test_init.py | 48 -- 15 files changed, 1078 insertions(+), 287 deletions(-) create mode 100644 homeassistant/components/huawei_lte/.translations/en.json create mode 100644 homeassistant/components/huawei_lte/config_flow.py create mode 100644 homeassistant/components/huawei_lte/strings.json create mode 100644 tests/components/huawei_lte/test_config_flow.py create mode 100644 tests/components/huawei_lte/test_device_tracker.py delete mode 100644 tests/components/huawei_lte/test_init.py diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json new file mode 100644 index 00000000000000..8681e3355a46c5 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "This device is already configured" + }, + "error": { + "connection_failed": "Connection failed", + "incorrect_password": "Incorrect password", + "incorrect_username": "Incorrect username", + "incorrect_username_or_password": "Incorrect username or password", + "invalid_url": "Invalid URL", + "login_attempts_exceeded": "Maximum login attempts exceeded, please try again later", + "response_error": "Unknown error from device", + "unknown_connection_error": "Unknown error connecting to device" + }, + "step": { + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "User name" + }, + "description": "Enter device access details. Specifying username and password is optional, but enables support for more integration features. On the other hand, use of an authorized connection may cause problems accessing the device web interface from outside Home Assistant while the integration is active, and the other way around.", + "title": "Configure Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "SMS notification recipients", + "track_new_devices": "Track new devices" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index f09788b7220e6f..18f7035a885268 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -1,34 +1,57 @@ """Support for Huawei LTE routers.""" +from collections import defaultdict from datetime import timedelta -from functools import reduce +from functools import partial from urllib.parse import urlparse import ipaddress import logging -import operator -from typing import Any, Callable +from typing import Any, Callable, Dict, List, Set 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 +from huawei_lte_api.Connection import Connection +from huawei_lte_api.exceptions import ( + ResponseErrorLoginRequiredException, + ResponseErrorNotSupportedException, +) +from url_normalize import url_normalize +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT from homeassistant.const import ( + CONF_PASSWORD, + CONF_RECIPIENT, CONF_URL, CONF_USERNAME, - CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers import config_validation as cv -from homeassistant.util import Throttle +from homeassistant.core import CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, + dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType from .const import ( + ALL_KEYS, + DEFAULT_DEVICE_NAME, DOMAIN, + KEY_DEVICE_BASIC_INFORMATION, KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_WLAN_HOST_LIST, + UPDATE_OPTIONS_SIGNAL, + UPDATE_SIGNAL, ) @@ -38,7 +61,20 @@ # https://github.com/quandyfactory/dicttoxml/issues/60 logging.getLogger("dicttoxml").setLevel(logging.WARNING) -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +DEFAULT_NAME_TEMPLATE = "Huawei {} {}" + +SCAN_INTERVAL = timedelta(seconds=10) + +NOTIFY_SCHEMA = vol.Any( + None, + vol.Schema( + { + vol.Optional(CONF_RECIPIENT): vol.Any( + None, vol.All(cv.ensure_list, [cv.string]) + ) + } + ), +) CONFIG_SCHEMA = vol.Schema( { @@ -48,8 +84,9 @@ vol.Schema( { vol.Required(CONF_URL): cv.url, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(NOTIFY_DOMAIN): NOTIFY_SCHEMA, } ) ], @@ -60,97 +97,136 @@ @attr.s -class RouterData: +class Router: """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) - wlan_host_list = attr.ib(init=False, factory=dict) + connection: Connection = attr.ib() + url: str = attr.ib() + mac: str = attr.ib() + signal_update: CALLBACK_TYPE = attr.ib() + + data: Dict[str, Any] = attr.ib(init=False, factory=dict) + subscriptions: Dict[str, Set[str]] = attr.ib( + init=False, + factory=lambda: defaultdict(set, ((x, {"initial_scan"}) for x in ALL_KEYS)), + ) + unload_handlers: List[CALLBACK_TYPE] = attr.ib(init=False, factory=list) + client: Client + + def __attrs_post_init__(self): + """Set up internal state on init.""" + self.client = Client(self.connection) + + @property + def device_name(self) -> str: + """Get router device name.""" + for key, item in ( + (KEY_DEVICE_BASIC_INFORMATION, "devicename"), + (KEY_DEVICE_INFORMATION, "DeviceName"), + ): + try: + return self.data[key][item] + except (KeyError, TypeError): + pass + return DEFAULT_DEVICE_NAME - _subscriptions = attr.ib(init=False, factory=set) - - def __getitem__(self, path: str): - """ - Get value corresponding to a dotted path. - - The first path component designates a member of this class - such as device_information, device_signal etc, and the remaining - path points to a value in the member's data structure. - """ - root, *rest = path.split(".") - try: - data = getattr(self, root) - except AttributeError as err: - raise KeyError from err - return reduce(operator.getitem, rest, data) - - def subscribe(self, path: str) -> None: - """Subscribe to given router data entries.""" - self._subscriptions.add(path.split(".")[0]) - - def unsubscribe(self, path: str) -> None: - """Unsubscribe from given router data entries.""" - self._subscriptions.discard(path.split(".")[0]) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self) -> None: - """Call API to update data.""" - self._update() - - def _update(self) -> None: - debugging = _LOGGER.isEnabledFor(logging.DEBUG) - - def get_data(path: str, func: Callable[[None], Any]) -> None: - if debugging or path in self._subscriptions: - try: - setattr(self, path, func()) - except ResponseErrorNotSupportedException: - _LOGGER.warning("%s not supported by device", path) - self._subscriptions.discard(path) - finally: - _LOGGER.debug("%s=%s", path, getattr(self, path)) + """Update router data.""" + + def get_data(key: str, func: Callable[[None], Any]) -> None: + if not self.subscriptions[key]: + return + _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) + try: + self.data[key] = func() + except ResponseErrorNotSupportedException: + _LOGGER.info( + "%s not supported by device, excluding from future updates", key + ) + self.subscriptions.pop(key) + except ResponseErrorLoginRequiredException: + _LOGGER.info( + "%s requires authorization, excluding from future updates", key + ) + self.subscriptions.pop(key) + finally: + _LOGGER.debug("%s=%s", key, self.data.get(key)) get_data(KEY_DEVICE_INFORMATION, self.client.device.information) + if self.data.get(KEY_DEVICE_INFORMATION): + # Full information includes everything in basic + self.subscriptions.pop(KEY_DEVICE_BASIC_INFORMATION, None) + get_data(KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information) get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) get_data( KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics ) get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) + self.signal_update() -@attr.s -class HuaweiLteData: - """Shared state.""" - - data = attr.ib(init=False, factory=dict) + def cleanup(self, *_) -> None: + """Clean up resources.""" - def get_data(self, config): - """Get the requested or the only data value.""" - if CONF_URL in config: - return self.data.get(config[CONF_URL]) - if len(self.data) == 1: - return next(iter(self.data.values())) + self.subscriptions.clear() - return None + for handler in self.unload_handlers: + handler() + self.unload_handlers.clear() + if not isinstance(self.connection, AuthorizedConnection): + return + try: + self.client.user.logout() + except ResponseErrorNotSupportedException: + _LOGGER.debug("Logout not supported by device", exc_info=True) + except ResponseErrorLoginRequiredException: + _LOGGER.debug("Logout not supported when not logged in", exc_info=True) + except Exception: # pylint: disable=broad-except + _LOGGER.warning("Logout error", exc_info=True) -def setup(hass, config) -> bool: - """Set up Huawei LTE component.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = HuaweiLteData() - for conf in config.get(DOMAIN, []): - _setup_lte(hass, conf) - return True +@attr.s +class HuaweiLteData: + """Shared state.""" -def _setup_lte(hass, lte_config) -> None: - """Set up Huawei LTE router.""" - url = lte_config[CONF_URL] - username = lte_config[CONF_USERNAME] - password = lte_config[CONF_PASSWORD] + hass_config: dict = attr.ib() + # Our YAML config, keyed by router URL + config: Dict[str, Dict[str, Any]] = attr.ib() + routers: Dict[str, Router] = attr.ib(init=False, factory=dict) + + +async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: + """Set up Huawei LTE component from config entry.""" + url = config_entry.data[CONF_URL] + + # Override settings from YAML config, but only if they're changed in it + # Old values are stored as *_from_yaml in the config entry + yaml_config = hass.data[DOMAIN].config.get(url) + if yaml_config: + # Config values + new_data = {} + for key in CONF_USERNAME, CONF_PASSWORD: + if key in yaml_config: + value = yaml_config[key] + if value != config_entry.data.get(f"{key}_from_yaml"): + new_data[f"{key}_from_yaml"] = value + new_data[key] = value + # Options + new_options = {} + yaml_recipient = yaml_config.get(NOTIFY_DOMAIN, {}).get(CONF_RECIPIENT) + if yaml_recipient is not None and yaml_recipient != config_entry.options.get( + f"{CONF_RECIPIENT}_from_yaml" + ): + new_options[f"{CONF_RECIPIENT}_from_yaml"] = yaml_recipient + new_options[CONF_RECIPIENT] = yaml_recipient + # Update entry if overrides were found + if new_data or new_options: + hass.config_entries.async_update_entry( + config_entry, + data={**config_entry.data, **new_data}, + options={**config_entry.options, **new_options}, + ) # 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 @@ -164,19 +240,194 @@ def _setup_lte(hass, lte_config) -> None: mode = "ip" except ValueError: mode = "hostname" - mac = get_mac_address(**{mode: host}) + mac = await hass.async_add_executor_job(partial(get_mac_address, **{mode: host})) - connection = AuthorizedConnection(url, username=username, password=password) - client = Client(connection) + def get_connection() -> Connection: + """ + Set up a connection. - data = RouterData(client, mac) - hass.data[DOMAIN].data[url] = data + Authorized one if username/pass specified (even if empty), unauthorized one otherwise. + """ + username = config_entry.data.get(CONF_USERNAME) + password = config_entry.data.get(CONF_PASSWORD) + if username or password: + connection = AuthorizedConnection(url, username=username, password=password) + else: + connection = Connection(url) + return connection - def cleanup(event): - """Clean up resources.""" - try: - client.user.logout() - except ResponseErrorNotSupportedException as ex: - _LOGGER.debug("Logout not supported by device", exc_info=ex) + def signal_update() -> None: + """Signal updates to data.""" + dispatcher_send(hass, UPDATE_SIGNAL, url) + + connection = await hass.async_add_executor_job(get_connection) + + # Set up router and store reference to it + router = Router(connection, url, mac, signal_update) + hass.data[DOMAIN].routers[url] = router + + # Do initial data update + await hass.async_add_executor_job(router.update) + + # Clear all subscriptions, enabled entities will push back theirs + router.subscriptions.clear() + + # Forward config entry setup to platforms + for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, domain) + ) + # Notify doesn't support config entry setup yet, load with discovery for now + await discovery.async_load_platform( + hass, + NOTIFY_DOMAIN, + DOMAIN, + {CONF_URL: url, CONF_RECIPIENT: config_entry.options.get(CONF_RECIPIENT)}, + hass.data[DOMAIN].hass_config, + ) + + # Add config entry options update listener + router.unload_handlers.append( + config_entry.add_update_listener(async_signal_options_update) + ) + + def _update_router(*_: Any) -> None: + """ + Update router data. + + Separate passthrough function because lambdas don't work with track_time_interval. + """ + router.update() + + # Set up periodic update + router.unload_handlers.append( + async_track_time_interval(hass, _update_router, SCAN_INTERVAL) + ) + + # Clean up at end + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, router.cleanup) + + return True + + +async def async_unload_entry( + hass: HomeAssistantType, config_entry: ConfigEntry +) -> bool: + """Unload config entry.""" + + # Forward config entry unload to platforms + for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN): + await hass.config_entries.async_forward_entry_unload(config_entry, domain) + + # Forget about the router and invoke its cleanup + router = hass.data[DOMAIN].routers.pop(config_entry.data[CONF_URL]) + await hass.async_add_executor_job(router.cleanup) + + return True + + +async def async_setup(hass: HomeAssistantType, config) -> bool: + """Set up Huawei LTE component.""" + + # Arrange our YAML config to dict with normalized URLs as keys + domain_config = {} + if DOMAIN not in hass.data: + hass.data[DOMAIN] = HuaweiLteData(hass_config=config, config=domain_config) + for router_config in config.get(DOMAIN, []): + domain_config[url_normalize(router_config.pop(CONF_URL))] = router_config + + for url, router_config in domain_config.items(): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_URL: url, + CONF_USERNAME: router_config.get(CONF_USERNAME), + CONF_PASSWORD: router_config.get(CONF_PASSWORD), + }, + ) + ) + + return True + + +async def async_signal_options_update( + hass: HomeAssistantType, config_entry: ConfigEntry +) -> None: + """Handle config entry options update.""" + async_dispatcher_send(hass, UPDATE_OPTIONS_SIGNAL, config_entry) + + +@attr.s +class HuaweiLteBaseEntity(Entity): + """Huawei LTE entity base class.""" + + router: Router = attr.ib() + + _available: bool = attr.ib(init=False, default=True) + _unsub_handlers: List[Callable] = attr.ib(init=False, factory=list) + + @property + def _entity_name(self) -> str: + raise NotImplementedError + + @property + def _device_unique_id(self) -> str: + """Return unique ID for entity within a router.""" + raise NotImplementedError + + @property + def unique_id(self) -> str: + """Return unique ID for entity.""" + return f"{self.router.mac}-{self._device_unique_id}" + + @property + def name(self) -> str: + """Return entity name.""" + return DEFAULT_NAME_TEMPLATE.format(self.router.device_name, self._entity_name) + + @property + def available(self) -> bool: + """Return whether the entity is available.""" + return self._available + + @property + def should_poll(self) -> bool: + """Huawei LTE entities report their state without polling.""" + return False + + async def async_update(self) -> None: + """Update state.""" + raise NotImplementedError + + async def async_update_options(self, config_entry: ConfigEntry) -> None: + """Update config entry options.""" + pass + + async def async_added_to_hass(self) -> None: + """Connect to update signals.""" + self._unsub_handlers.append( + async_dispatcher_connect(self.hass, UPDATE_SIGNAL, self._async_maybe_update) + ) + self._unsub_handlers.append( + async_dispatcher_connect( + self.hass, UPDATE_OPTIONS_SIGNAL, self._async_maybe_update_options + ) + ) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) + async def _async_maybe_update(self, url: str) -> None: + """Update state if the update signal comes from our router.""" + if url == self.router.url: + await self.async_update() + + async def _async_maybe_update_options(self, config_entry: ConfigEntry) -> None: + """Update options if the update signal comes from our router.""" + if config_entry.data[CONF_URL] == self.router.url: + await self.async_update_options(config_entry) + + async def async_will_remove_from_hass(self) -> None: + """Invoke unsubscription handlers.""" + for unsub in self._unsub_handlers: + unsub() + self._unsub_handlers.clear() diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py new file mode 100644 index 00000000000000..52d586d088ac2b --- /dev/null +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -0,0 +1,208 @@ +"""Config flow for the Huawei LTE platform.""" + +from collections import OrderedDict +import logging +from typing import Optional + +from huawei_lte_api.AuthorizedConnection import AuthorizedConnection +from huawei_lte_api.Client import Client +from huawei_lte_api.Connection import Connection +from huawei_lte_api.exceptions import ( + LoginErrorUsernameWrongException, + LoginErrorPasswordWrongException, + LoginErrorUsernamePasswordWrongException, + LoginErrorUsernamePasswordOverrunException, + ResponseErrorException, +) +from url_normalize import url_normalize +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME +from homeassistant.core import callback +from .const import DEFAULT_DEVICE_NAME + +# https://github.com/PyCQA/pylint/issues/3202 +from .const import DOMAIN # pylint: disable=unused-import + + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle Huawei LTE config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get options flow.""" + return OptionsFlowHandler(config_entry) + + async def _async_show_user_form(self, user_input=None, errors=None): + if user_input is None: + user_input = {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + OrderedDict( + ( + ( + vol.Required( + CONF_URL, default=user_input.get(CONF_URL, "") + ), + str, + ), + ( + vol.Optional( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ), + str, + ), + ( + vol.Optional( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ), + str, + ), + ) + ) + ), + errors=errors or {}, + ) + + async def async_step_import(self, user_input=None): + """Handle import initiated config flow.""" + return await self.async_step_user(user_input) + + async def async_step_user(self, user_input=None): + """Handle user initiated config flow.""" + if user_input is None: + return await self._async_show_user_form() + + errors = {} + + # Normalize URL + user_input[CONF_URL] = url_normalize( + user_input[CONF_URL], default_scheme="http" + ) + if "://" not in user_input[CONF_URL]: + errors[CONF_URL] = "invalid_url" + return await self._async_show_user_form( + user_input=user_input, errors=errors + ) + + # See if we already have a router configured with this URL + existing_urls = { # existing entries + url_normalize(entry.data[CONF_URL], default_scheme="http") + for entry in self._async_current_entries() + } + if user_input[CONF_URL] in existing_urls: + return self.async_abort(reason="already_configured") + + conn = None + + def logout(): + if hasattr(conn, "user"): + try: + conn.user.logout() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Could not logout", exc_info=True) + + def try_connect(username: Optional[str], password: Optional[str]) -> Connection: + """Try connecting with given credentials.""" + if username or password: + conn = AuthorizedConnection( + user_input[CONF_URL], username=username, password=password + ) + else: + try: + conn = AuthorizedConnection( + user_input[CONF_URL], username="", password="" + ) + user_input[CONF_USERNAME] = "" + user_input[CONF_PASSWORD] = "" + except ResponseErrorException: + _LOGGER.debug( + "Could not login with empty credentials, proceeding unauthenticated", + exc_info=True, + ) + conn = Connection(user_input[CONF_URL]) + del user_input[CONF_USERNAME] + del user_input[CONF_PASSWORD] + return conn + + def get_router_title(conn: Connection) -> str: + """Get title for router.""" + title = None + client = Client(conn) + try: + info = client.device.basic_information() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Could not get device.basic_information", exc_info=True) + else: + title = info.get("devicename") + if not title: + try: + info = client.device.information() + except Exception: # pylint: disable=broad-except + _LOGGER.debug("Could not get device.information", exc_info=True) + else: + title = info.get("DeviceName") + return title or DEFAULT_DEVICE_NAME + + username = user_input.get(CONF_USERNAME) + password = user_input.get(CONF_PASSWORD) + try: + conn = await self.hass.async_add_executor_job( + try_connect, username, password + ) + except LoginErrorUsernameWrongException: + errors[CONF_USERNAME] = "incorrect_username" + except LoginErrorPasswordWrongException: + errors[CONF_PASSWORD] = "incorrect_password" + except LoginErrorUsernamePasswordWrongException: + errors[CONF_USERNAME] = "incorrect_username_or_password" + except LoginErrorUsernamePasswordOverrunException: + errors["base"] = "login_attempts_exceeded" + except ResponseErrorException: + _LOGGER.warning("Response error", exc_info=True) + errors["base"] = "response_error" + except Exception: # pylint: disable=broad-except + _LOGGER.warning("Unknown error connecting to device", exc_info=True) + errors[CONF_URL] = "unknown_connection_error" + if errors: + await self.hass.async_add_executor_job(logout) + return await self._async_show_user_form( + user_input=user_input, errors=errors + ) + + title = await self.hass.async_add_executor_job(get_router_title, conn) + await self.hass.async_add_executor_job(logout) + + return self.async_create_entry(title=title, data=user_input) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Huawei LTE options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_RECIPIENT, + default=self.config_entry.options.get(CONF_RECIPIENT, ""), + ): str + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 0134417d5fe21c..77126b61c22680 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -2,7 +2,23 @@ DOMAIN = "huawei_lte" +DEFAULT_DEVICE_NAME = "LTE" + +UPDATE_SIGNAL = f"{DOMAIN}_update" +UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" + +KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics" KEY_WLAN_HOST_LIST = "wlan_host_list" + +DEVICE_TRACKER_KEYS = {KEY_WLAN_HOST_LIST} + +SENSOR_KEYS = { + KEY_DEVICE_INFORMATION, + KEY_DEVICE_SIGNAL, + KEY_MONITORING_TRAFFIC_STATISTICS, +} + +ALL_KEYS = DEVICE_TRACKER_KEYS | SENSOR_KEYS diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index bad9253f4e7957..d95d99e71264e2 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -1,63 +1,162 @@ """Support for device tracking of Huawei LTE routers.""" import logging -from typing import Any, Dict, List, Optional +import re +from typing import Any, Dict, Set import attr -import voluptuous as vol +from stringcase import snakecase -import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, DeviceScanner +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN, + SOURCE_TYPE_ROUTER, +) +from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.const import CONF_URL -from . import RouterData -from .const import DOMAIN, KEY_WLAN_HOST_LIST +from homeassistant.helpers import entity_registry +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from . import HuaweiLteBaseEntity +from .const import DOMAIN, KEY_WLAN_HOST_LIST, UPDATE_SIGNAL _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_URL): cv.url}) - -HOSTS_PATH = f"{KEY_WLAN_HOST_LIST}.Hosts.Host" - - -def get_scanner(hass, config): - """Get a Huawei LTE router scanner.""" - data = hass.data[DOMAIN].get_data(config) - data.subscribe(HOSTS_PATH) - return HuaweiLteScanner(data) +_DEVICE_SCAN = f"{DEVICE_TRACKER_DOMAIN}/device_scan" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up from config entry.""" + + # Grab hosts list once to examine whether the initial fetch has got some data for + # us, i.e. if wlan host list is supported. Only set up a subscription and proceed + # with adding and tracking entities if it is. + router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] + try: + _ = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] + except KeyError: + _LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host") + return + + # Initialize already tracked entities + tracked: Set[str] = set() + registry = await entity_registry.async_get_registry(hass) + for entity in registry.entities.values(): + if ( + entity.domain == DEVICE_TRACKER_DOMAIN + and entity.config_entry_id == config_entry.entry_id + ): + tracked.add(entity.unique_id) + async_add_new_entities(hass, router.url, async_add_entities, tracked, True) + + # Tell parent router to poll hosts list to gather new devices + router.subscriptions[KEY_WLAN_HOST_LIST].add(_DEVICE_SCAN) + + async def _async_maybe_add_new_entities(url: str) -> None: + """Add new entities if the update signal comes from our router.""" + if url == router.url: + async_add_new_entities(hass, url, async_add_entities, tracked) + + # Register to handle router data updates + disconnect_dispatcher = async_dispatcher_connect( + hass, UPDATE_SIGNAL, _async_maybe_add_new_entities + ) + router.unload_handlers.append(disconnect_dispatcher) + + # Add new entities from initial scan + async_add_new_entities(hass, router.url, async_add_entities, tracked) + + +def async_add_new_entities( + hass, router_url, async_add_entities, tracked, included: bool = False +): + """Add new entities. + + :param included: if True, setup only items in tracked, and vice versa + """ + router = hass.data[DOMAIN].routers[router_url] + try: + hosts = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] + except KeyError: + _LOGGER.debug("%s[%s][%s] not in data", KEY_WLAN_HOST_LIST, "Hosts", "Host") + return + + new_entities = [] + for host in (x for x in hosts if x.get("MacAddress")): + entity = HuaweiLteScannerEntity(router, host["MacAddress"]) + tracking = entity.unique_id in tracked + if tracking != included: + continue + tracked.add(entity.unique_id) + new_entities.append(entity) + async_add_entities(new_entities, True) + + +def _better_snakecase(text: str) -> str: + if text == text.upper(): + # All uppercase to all lowercase to get http for HTTP, not h_t_t_p + text = text.lower() + else: + # Three or more consecutive uppercase with middle part lowercased + # to get http_response for HTTPResponse, not h_t_t_p_response + text = re.sub( + r"([A-Z])([A-Z]+)([A-Z](?:[^A-Z]|$))", + lambda match: f"{match.group(1)}{match.group(2).lower()}{match.group(3)}", + text, + ) + return snakecase(text) @attr.s -class HuaweiLteScanner(DeviceScanner): - """Huawei LTE router scanner.""" - - data = attr.ib(type=RouterData) +class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): + """Huawei LTE router scanner entity.""" + + mac: str = attr.ib() + + _is_connected: bool = attr.ib(init=False, default=False) + _name: str = attr.ib(init=False, default="device") + _device_state_attributes: Dict[str, Any] = attr.ib(init=False, factory=dict) + + @property + def _entity_name(self) -> str: + return self._name + + @property + def _device_unique_id(self) -> str: + return self.mac + + @property + def source_type(self) -> str: + """Return SOURCE_TYPE_ROUTER.""" + return SOURCE_TYPE_ROUTER + + @property + def is_connected(self) -> bool: + """Get whether the entity is connected.""" + return self._is_connected + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Get additional attributes related to entity state.""" + return self._device_state_attributes + + async def async_update(self) -> None: + """Update state.""" + hosts = self.router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] + host = next((x for x in hosts if x.get("MacAddress") == self.mac), None) + self._is_connected = host is not None + if self._is_connected: + self._name = host.get("HostName", self.mac) + self._device_state_attributes = { + _better_snakecase(k): v + for k, v in host.items() + if k not in ("MacAddress", "HostName") + } - _hosts = attr.ib(init=False, factory=dict) - def scan_devices(self) -> List[str]: - """Scan for devices.""" - self.data.update() - 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]: - """Get name for a device.""" - host = self._hosts.get(device) - return host.get("HostName") or None if host else None - - def get_extra_attributes(self, device: str) -> Dict[str, Any]: - """ - Get extra attributes of a device. - - Some known extra attributes that may be returned in the dict - include MacAddress (MAC address), ID (client ID), IpAddress - (IP address), AssociatedSsid (associated SSID), AssociatedTime - (associated time in seconds), and HostName (host name). - """ - return self._hosts.get(device) or {} +def get_scanner(*args, **kwargs): # pylint: disable=useless-return + """Old no longer used way to set up Huawei LTE device tracker.""" + _LOGGER.warning( + "Loading and configuring as a platform is no longer supported or " + "required, convert to enabling/disabling available entities" + ) + return None diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 5d559cc60c5c3e..b3c4442caa9a65 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -1,10 +1,13 @@ { "domain": "huawei_lte", "name": "Huawei LTE", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.1", - "huawei-lte-api==1.3.0" + "huawei-lte-api==1.4.3", + "stringcase==1.2.0", + "url-normalize==1.4.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index e882509c04c4fb..4b5a63756b5eb6 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -1,58 +1,54 @@ """Support for Huawei LTE router notifications.""" import logging +from typing import Any, List -import voluptuous as vol import attr +from huawei_lte_api.exceptions import ResponseErrorException -from homeassistant.components.notify import ( - BaseNotificationService, - ATTR_TARGET, - PLATFORM_SCHEMA, -) +from homeassistant.components.notify import BaseNotificationService, ATTR_TARGET from homeassistant.const import CONF_RECIPIENT, CONF_URL -import homeassistant.helpers.config_validation as cv +from . import Router from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_URL): cv.url, - vol.Required(CONF_RECIPIENT): vol.All(cv.ensure_list, [cv.string]), - } -) - async def async_get_service(hass, config, discovery_info=None): """Get the notification service.""" - return HuaweiLteSmsNotificationService(hass, config) + if discovery_info is None: + _LOGGER.warning( + "Loading as a platform is no longer supported, convert to use " + "config entries or the huawei_lte component" + ) + return None + + router = hass.data[DOMAIN].routers[discovery_info[CONF_URL]] + default_targets = discovery_info[CONF_RECIPIENT] or [] + + return HuaweiLteSmsNotificationService(router, default_targets) @attr.s class HuaweiLteSmsNotificationService(BaseNotificationService): """Huawei LTE router SMS notification service.""" - hass = attr.ib() - config = attr.ib() + router: Router = attr.ib() + default_targets: List[str] = attr.ib() - def send_message(self, message="", **kwargs): + def send_message(self, message: str = "", **kwargs: Any) -> None: """Send message to target numbers.""" - from huawei_lte_api.exceptions import ResponseErrorException - targets = kwargs.get(ATTR_TARGET, self.config.get(CONF_RECIPIENT)) + targets = kwargs.get(ATTR_TARGET, self.default_targets) if not targets or not message: return - data = self.hass.data[DOMAIN].get_data(self.config) - if not data: - _LOGGER.error("Router not available") - return - try: - resp = data.client.sms.send_sms(phone_numbers=targets, message=message) + resp = self.router.client.sms.send_sms( + phone_numbers=targets, message=message + ) _LOGGER.debug("Sent to %s: %s", targets, resp) except ResponseErrorException as ex: _LOGGER.error("Could not send to %s: %s", targets, ex) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index cb8f5fb5766aab..e5b65c723f043b 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -5,18 +5,15 @@ from typing import Optional import attr -import voluptuous as vol -from homeassistant.const import CONF_URL, CONF_MONITORED_CONDITIONS, STATE_UNKNOWN +from homeassistant.const import CONF_URL, STATE_UNKNOWN from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, DEVICE_CLASS_SIGNAL_STRENGTH, + DOMAIN as SENSOR_DOMAIN, ) from homeassistant.helpers import entity_registry -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv -from . import RouterData +from . import HuaweiLteBaseEntity from .const import ( DOMAIN, KEY_DEVICE_INFORMATION, @@ -27,34 +24,27 @@ _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME_TEMPLATE = "Huawei {} {}" -DEFAULT_DEVICE_NAME = "LTE" - -DEFAULT_SENSORS = [ - f"{KEY_DEVICE_INFORMATION}.WanIPAddress", - f"{KEY_DEVICE_SIGNAL}.rsrq", - f"{KEY_DEVICE_SIGNAL}.rsrp", - f"{KEY_DEVICE_SIGNAL}.rssi", - f"{KEY_DEVICE_SIGNAL}.sinr", -] SENSOR_META = { - f"{KEY_DEVICE_INFORMATION}.SoftwareVersion": dict(name="Software version"), - f"{KEY_DEVICE_INFORMATION}.WanIPAddress": dict( - name="WAN IP address", icon="mdi:ip" + KEY_DEVICE_INFORMATION: dict( + include=re.compile(r"^WanIP.*Address$", re.IGNORECASE) + ), + (KEY_DEVICE_INFORMATION, "SoftwareVersion"): dict(name="Software version"), + (KEY_DEVICE_INFORMATION, "WanIPAddress"): dict( + name="WAN IP address", icon="mdi:ip", enabled_default=True ), - f"{KEY_DEVICE_INFORMATION}.WanIPv6Address": dict( + (KEY_DEVICE_INFORMATION, "WanIPv6Address"): dict( name="WAN IPv6 address", icon="mdi:ip" ), - f"{KEY_DEVICE_SIGNAL}.band": dict(name="Band"), - f"{KEY_DEVICE_SIGNAL}.cell_id": dict(name="Cell ID"), - f"{KEY_DEVICE_SIGNAL}.lac": dict(name="LAC"), - f"{KEY_DEVICE_SIGNAL}.mode": dict( + (KEY_DEVICE_SIGNAL, "band"): dict(name="Band"), + (KEY_DEVICE_SIGNAL, "cell_id"): dict(name="Cell ID"), + (KEY_DEVICE_SIGNAL, "lac"): dict(name="LAC"), + (KEY_DEVICE_SIGNAL, "mode"): dict( name="Mode", formatter=lambda x: ({"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), None), ), - f"{KEY_DEVICE_SIGNAL}.pci": dict(name="PCI"), - f"{KEY_DEVICE_SIGNAL}.rsrq": dict( + (KEY_DEVICE_SIGNAL, "pci"): dict(name="PCI"), + (KEY_DEVICE_SIGNAL, "rsrq"): dict( name="RSRQ", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrq.php @@ -65,8 +55,9 @@ or x < -5 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", + enabled_default=True, ), - f"{KEY_DEVICE_SIGNAL}.rsrp": dict( + (KEY_DEVICE_SIGNAL, "rsrp"): dict( name="RSRP", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrp.php @@ -77,8 +68,9 @@ or x < -80 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", + enabled_default=True, ), - f"{KEY_DEVICE_SIGNAL}.rssi": dict( + (KEY_DEVICE_SIGNAL, "rssi"): dict( name="RSSI", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # https://eyesaas.com/wi-fi-signal-strength/ @@ -89,8 +81,9 @@ or x < -60 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", + enabled_default=True, ), - f"{KEY_DEVICE_SIGNAL}.sinr": dict( + (KEY_DEVICE_SIGNAL, "sinr"): dict( name="SINR", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/sinr.php @@ -101,28 +94,38 @@ or x < 10 and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", + enabled_default=True, + ), + KEY_MONITORING_TRAFFIC_STATISTICS: dict( + exclude=re.compile(r"^showtraffic$", re.IGNORECASE) ), } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_URL): cv.url, - vol.Optional( - CONF_MONITORED_CONDITIONS, default=DEFAULT_SENSORS - ): cv.ensure_list, - } -) - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up Huawei LTE sensor devices.""" - data = hass.data[DOMAIN].get_data(config) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up from config entry.""" + router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] sensors = [] - for path in config.get(CONF_MONITORED_CONDITIONS): - if path == "traffic_statistics": # backwards compatibility - path = KEY_MONITORING_TRAFFIC_STATISTICS - data.subscribe(path) - sensors.append(HuaweiLteSensor(data, path, SENSOR_META.get(path, {}))) + for key in ( + KEY_DEVICE_INFORMATION, + KEY_DEVICE_SIGNAL, + KEY_MONITORING_TRAFFIC_STATISTICS, + ): + items = router.data.get(key) + if not items: + continue + key_meta = SENSOR_META.get(key) + if key_meta: + include = key_meta.get("include") + if include: + items = filter(include.search, items) + exclude = key_meta.get("exclude") + if exclude: + items = [x for x in items if not exclude.search(x)] + for item in items: + sensors.append( + HuaweiLteSensor(router, key, item, SENSOR_META.get((key, item), {})) + ) # 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 @@ -134,7 +137,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if ent.platform != DOMAIN: continue for sensor in sensors: - oldsuf = ".".join(sensor.path) + oldsuf = ".".join(f"{sensor.key}.{sensor.item}") if ent.unique_id.endswith(f"_{oldsuf}"): entreg.async_update_entity(entid, new_unique_id=sensor.unique_id) _LOGGER.debug( @@ -162,30 +165,33 @@ def format_default(value): @attr.s -class HuaweiLteSensor(Entity): +class HuaweiLteSensor(HuaweiLteBaseEntity): """Huawei LTE sensor entity.""" - data = attr.ib(type=RouterData) - path = attr.ib(type=str) - meta = attr.ib(type=dict) + key: str = attr.ib() + item: str = attr.ib() + meta: dict = attr.ib() _state = attr.ib(init=False, default=STATE_UNKNOWN) - _unit = attr.ib(init=False, type=str) + _unit: str = attr.ib(init=False) + + async def async_added_to_hass(self): + """Subscribe to needed data on add.""" + await super().async_added_to_hass() + self.router.subscriptions[self.key].add(f"{SENSOR_DOMAIN}/{self.item}") + + async def async_will_remove_from_hass(self): + """Unsubscribe from needed data on remove.""" + await super().async_will_remove_from_hass() + self.router.subscriptions[self.key].remove(f"{SENSOR_DOMAIN}/{self.item}") @property - def unique_id(self) -> str: - """Return unique ID for sensor.""" - return f"{self.data.mac}-{self.path}" + def _entity_name(self) -> str: + return self.meta.get("name", self.item) @property - def name(self) -> str: - """Return sensor name.""" - try: - dname = self.data[f"{KEY_DEVICE_INFORMATION}.DeviceName"] - except KeyError: - dname = None - vname = self.meta.get("name", self.path) - return DEFAULT_NAME_TEMPLATE.format(dname or DEFAULT_DEVICE_NAME, vname) + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" @property def state(self): @@ -210,18 +216,31 @@ def icon(self): return icon(self.state) return icon - def update(self): - """Update state.""" - self.data.update() + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return bool(self.meta.get("enabled_default")) + async def async_update(self): + """Update state.""" try: - value = self.data[self.path] + value = self.router.data[self.key][self.item] except KeyError: - _LOGGER.debug("%s not in data", self.path) - value = None + _LOGGER.debug("%s[%s] not in data", self.key, self.item) + self._available = False + return + self._available = True formatter = self.meta.get("formatter") if not callable(formatter): formatter = format_default self._state, self._unit = formatter(value) + + +async def async_setup_platform(*args, **kwargs): + """Old no longer used way to set up Huawei LTE sensors.""" + _LOGGER.warning( + "Loading and configuring as a platform is no longer supported or " + "required, convert to enabling/disabling available entities" + ) diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json new file mode 100644 index 00000000000000..8681e3355a46c5 --- /dev/null +++ b/homeassistant/components/huawei_lte/strings.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "This device is already configured" + }, + "error": { + "connection_failed": "Connection failed", + "incorrect_password": "Incorrect password", + "incorrect_username": "Incorrect username", + "incorrect_username_or_password": "Incorrect username or password", + "invalid_url": "Invalid URL", + "login_attempts_exceeded": "Maximum login attempts exceeded, please try again later", + "response_error": "Unknown error from device", + "unknown_connection_error": "Unknown error connecting to device" + }, + "step": { + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "User name" + }, + "description": "Enter device access details. Specifying username and password is optional, but enables support for more integration features. On the other hand, use of an authorized connection may cause problems accessing the device web interface from outside Home Assistant while the integration is active, and the other way around.", + "title": "Configure Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "SMS notification recipients", + "track_new_devices": "Track new devices" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4668528fedbac0..b694af1fb71bea 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -29,6 +29,7 @@ "heos", "homekit_controller", "homematicip_cloud", + "huawei_lte", "hue", "iaqualink", "ifttt", diff --git a/requirements_all.txt b/requirements_all.txt index 0bb4afc2b8493a..73cb1e8bf045b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -665,7 +665,7 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.3.0 +huawei-lte-api==1.4.3 # homeassistant.components.hydrawise hydrawiser==0.1.1 @@ -1837,6 +1837,7 @@ steamodd==4.21 # homeassistant.components.streamlabswater streamlabswater==1.0.1 +# homeassistant.components.huawei_lte # homeassistant.components.solaredge # homeassistant.components.thermoworks_smoke # homeassistant.components.traccar @@ -1923,6 +1924,9 @@ unifiled==0.10 # homeassistant.components.upcloud upcloud-api==0.4.3 +# homeassistant.components.huawei_lte +url-normalize==1.4.1 + # homeassistant.components.uscis uscisstatus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39903b3606a07a..aa907170786701 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -258,7 +258,7 @@ homematicip==0.10.13 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.3.0 +huawei-lte-api==1.4.3 # homeassistant.components.iaqualink iaqualink==0.2.9 @@ -581,6 +581,7 @@ sqlalchemy==1.3.10 # homeassistant.components.statsd statsd==3.2.1 +# homeassistant.components.huawei_lte # homeassistant.components.solaredge # homeassistant.components.thermoworks_smoke # homeassistant.components.traccar @@ -604,6 +605,9 @@ twentemilieu==0.1.0 # homeassistant.components.twilio twilio==6.32.0 +# homeassistant.components.huawei_lte +url-normalize==1.4.1 + # homeassistant.components.uvc uvcclient==0.11.0 diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py new file mode 100644 index 00000000000000..aafa6abd57fb6e --- /dev/null +++ b/tests/components/huawei_lte/test_config_flow.py @@ -0,0 +1,140 @@ +"""Tests for the Huawei LTE config flow.""" + +from huawei_lte_api.enums.client import ResponseCodeEnum +from huawei_lte_api.enums.user import LoginErrorEnum, LoginStateEnum, PasswordTypeEnum +from requests_mock import ANY +from requests.exceptions import ConnectionError +import pytest + +from homeassistant import data_entry_flow +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_URL +from homeassistant.components.huawei_lte.const import DOMAIN +from homeassistant.components.huawei_lte.config_flow import ConfigFlowHandler +from tests.common import MockConfigEntry + + +FIXTURE_USER_INPUT = { + CONF_URL: "http://192.168.1.1/", + CONF_USERNAME: "admin", + CONF_PASSWORD: "secret", +} + + +async def test_show_set_form(hass): + """Test that the setup form is served.""" + flow = ConfigFlowHandler() + 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_urlize_plain_host(hass, requests_mock): + """Test that plain host or IP gets converted to a URL.""" + requests_mock.request(ANY, ANY, exc=ConnectionError()) + flow = ConfigFlowHandler() + flow.hass = hass + host = "192.168.100.1" + user_input = {**FIXTURE_USER_INPUT, CONF_URL: host} + result = await flow.async_step_user(user_input=user_input) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert user_input[CONF_URL] == f"http://{host}/" + + +async def test_already_configured(hass): + """Test we reject already configured devices.""" + MockConfigEntry( + domain=DOMAIN, data=FIXTURE_USER_INPUT, title="Already configured" + ).add_to_hass(hass) + + flow = ConfigFlowHandler() + flow.hass = hass + # Tweak URL a bit to check that doesn't fail duplicate detection + result = await flow.async_step_user( + user_input={ + **FIXTURE_USER_INPUT, + CONF_URL: FIXTURE_USER_INPUT[CONF_URL].replace("http", "HTTP"), + } + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_connection_error(hass, requests_mock): + """Test we show user form on connection error.""" + + requests_mock.request(ANY, ANY, exc=ConnectionError()) + flow = ConfigFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_URL: "unknown_connection_error"} + + +@pytest.fixture +def login_requests_mock(requests_mock): + """Set up a requests_mock with base mocks for login tests.""" + requests_mock.request( + ANY, FIXTURE_USER_INPUT[CONF_URL], text='' + ) + requests_mock.request( + ANY, + f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/state-login", + text=( + f"{LoginStateEnum.LOGGED_OUT}" + f"{PasswordTypeEnum.SHA256}" + ), + ) + return requests_mock + + +@pytest.mark.parametrize( + ("code", "errors"), + ( + (LoginErrorEnum.USERNAME_WRONG, {CONF_USERNAME: "incorrect_username"}), + (LoginErrorEnum.PASSWORD_WRONG, {CONF_PASSWORD: "incorrect_password"}), + ( + LoginErrorEnum.USERNAME_PWD_WRONG, + {CONF_USERNAME: "incorrect_username_or_password"}, + ), + (LoginErrorEnum.USERNAME_PWD_ORERRUN, {"base": "login_attempts_exceeded"}), + (ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}), + ), +) +async def test_login_error(hass, login_requests_mock, code, errors): + """Test we show user form with appropriate error on response failure.""" + login_requests_mock.request( + ANY, + f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", + text=f"{code}", + ) + flow = ConfigFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == errors + + +async def test_success(hass, login_requests_mock): + """Test successful flow provides entry creation data.""" + login_requests_mock.request( + ANY, + f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", + text=f"OK", + ) + flow = ConfigFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] + assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] + assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] diff --git a/tests/components/huawei_lte/test_device_tracker.py b/tests/components/huawei_lte/test_device_tracker.py new file mode 100644 index 00000000000000..143fa5760e3335 --- /dev/null +++ b/tests/components/huawei_lte/test_device_tracker.py @@ -0,0 +1,20 @@ +"""Huawei LTE device tracker tests.""" + +import pytest + +from homeassistant.components.huawei_lte import device_tracker + + +@pytest.mark.parametrize( + ("value", "expected"), + ( + ("HTTP", "http"), + ("ID", "id"), + ("IPAddress", "ip_address"), + ("HTTPResponse", "http_response"), + ("foo_bar", "foo_bar"), + ), +) +def test_better_snakecase(value, expected): + """Test that better snakecase works better.""" + assert device_tracker._better_snakecase(value) == expected diff --git a/tests/components/huawei_lte/test_init.py b/tests/components/huawei_lte/test_init.py deleted file mode 100644 index e7323e1629e200..00000000000000 --- a/tests/components/huawei_lte/test_init.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Huawei LTE component tests.""" -from unittest.mock import Mock - -import pytest - -from homeassistant.components import huawei_lte -from homeassistant.components.huawei_lte.const import KEY_DEVICE_INFORMATION - - -@pytest.fixture(autouse=True) -def routerdata(): - """Set up a router data for testing.""" - rd = huawei_lte.RouterData(Mock(), "de:ad:be:ef:00:00") - rd.device_information = {"SoftwareVersion": "1.0", "nested": {"foo": "bar"}} - return rd - - -async def test_routerdata_get_nonexistent_root(routerdata): - """Test that accessing a nonexistent root element raises KeyError.""" - with pytest.raises(KeyError): # NOT AttributeError - routerdata["nonexistent_root.foo"] - - -async def test_routerdata_get_nonexistent_leaf(routerdata): - """Test that accessing a nonexistent leaf element raises KeyError.""" - with pytest.raises(KeyError): - routerdata[f"{KEY_DEVICE_INFORMATION}.foo"] - - -async def test_routerdata_get_nonexistent_leaf_path(routerdata): - """Test that accessing a nonexistent long path raises KeyError.""" - with pytest.raises(KeyError): - routerdata[f"{KEY_DEVICE_INFORMATION}.long.path.foo"] - - -async def test_routerdata_get_simple(routerdata): - """Test that accessing a short, simple path works.""" - assert routerdata[f"{KEY_DEVICE_INFORMATION}.SoftwareVersion"] == "1.0" - - -async def test_routerdata_get_longer(routerdata): - """Test that accessing a longer path works.""" - assert routerdata[f"{KEY_DEVICE_INFORMATION}.nested.foo"] == "bar" - - -async def test_routerdata_get_dict(routerdata): - """Test that returning an intermediate dict works.""" - assert routerdata[f"{KEY_DEVICE_INFORMATION}.nested"] == {"foo": "bar"} From 15bedd8f2797515eb086c61e9fd31e7afc096403 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Thu, 24 Oct 2019 09:41:04 -0700 Subject: [PATCH 1242/3953] Use latest withings_api module (#27817) * Using latest winthings_api module. Drastically reduced complexity of tests. * Removing import source. * Fixing test requirements. * Using requests_mock instead of responses module. * Updating file formatting. * Removing unused method. * Adding support for new OAuth2 config flow. * Addressing PR feedback. Removing unecessary base_url from config, this is a potential breaking change. * Addressing PR feedback. --- homeassistant/components/withings/__init__.py | 75 ++- homeassistant/components/withings/common.py | 193 ++++--- .../components/withings/config_flow.py | 214 ++----- homeassistant/components/withings/const.py | 35 +- .../components/withings/manifest.json | 2 +- homeassistant/components/withings/sensor.py | 262 ++++----- .../components/withings/strings.json | 10 +- requirements_all.txt | 2 +- requirements_test.txt | 1 + requirements_test_all.txt | 3 +- tests/components/withings/common.py | 528 ++++++++++++------ tests/components/withings/conftest.py | 350 ------------ tests/components/withings/test_common.py | 72 +-- tests/components/withings/test_config_flow.py | 162 ------ tests/components/withings/test_init.py | 345 +++++++++--- tests/components/withings/test_sensor.py | 310 ---------- 16 files changed, 995 insertions(+), 1569 deletions(-) delete mode 100644 tests/components/withings/conftest.py delete mode 100644 tests/components/withings/test_config_flow.py delete mode 100644 tests/components/withings/test_sensor.py diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index baed9300d46408..482c4e96e5cec5 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -4,10 +4,11 @@ For more details about this platform, please refer to the documentation at """ import voluptuous as vol +from withings_api import WithingsAuth -from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from . import config_flow, const from .common import _LOGGER, get_data_manager, NotAuthenticatedError @@ -22,7 +23,6 @@ vol.Required(const.CLIENT_SECRET): vol.All( cv.string, vol.Length(min=1) ), - vol.Optional(const.BASE_URL): cv.url, vol.Required(const.PROFILES): vol.All( cv.ensure_list, vol.Unique(), @@ -36,50 +36,65 @@ ) -async def async_setup(hass: HomeAssistantType, config: ConfigType): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the Withings component.""" - conf = config.get(DOMAIN) + conf = config.get(DOMAIN, {}) if not conf: return True hass.data[DOMAIN] = {const.CONFIG: conf} - base_url = conf.get(const.BASE_URL, hass.config.api.base_url).rstrip("/") - - hass.http.register_view(config_flow.WithingsAuthCallbackView) - - config_flow.register_flow_implementation( + config_flow.WithingsFlowHandler.async_register_implementation( hass, - conf[const.CLIENT_ID], - conf[const.CLIENT_SECRET], - base_url, - conf[const.PROFILES], - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={} - ) + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + const.DOMAIN, + conf[const.CLIENT_ID], + conf[const.CLIENT_SECRET], + f"{WithingsAuth.URL}/oauth2_user/authorize2", + f"{WithingsAuth.URL}/oauth2/token", + ), ) return True -async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up Withings from a config entry.""" - data_manager = get_data_manager(hass, entry) + # Upgrading existing token information to hass managed tokens. + if "auth_implementation" not in entry.data: + _LOGGER.debug("Upgrading existing config entry") + data = entry.data + creds = data.get(const.CREDENTIALS, {}) + hass.config_entries.async_update_entry( + entry, + data={ + "auth_implementation": const.DOMAIN, + "implementation": const.DOMAIN, + "profile": data.get("profile"), + "token": { + "access_token": creds.get("access_token"), + "refresh_token": creds.get("refresh_token"), + "expires_at": int(creds.get("token_expiry")), + "type": creds.get("token_type"), + "userid": creds.get("userid") or creds.get("user_id"), + }, + }, + ) + + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + + data_manager = get_data_manager(hass, entry, implementation) _LOGGER.debug("Confirming we're authenticated") try: await data_manager.check_authenticated() except NotAuthenticatedError: - # Trigger new config flow. - hass.async_create_task( - hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": SOURCE_USER, const.PROFILE: data_manager.profile}, - data={}, - ) + _LOGGER.error( + "Withings auth tokens exired for profile %s, remove and re-add the integration", + data_manager.profile, ) return False @@ -90,6 +105,6 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): return True -async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload Withings config entry.""" return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 9acca6f0cd68e6..911bb08906b1cb 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -1,23 +1,36 @@ """Common code for Withings.""" import datetime +from functools import partial import logging import re import time - -import withings_api as withings -from oauthlib.oauth2.rfc6749.errors import MissingTokenError -from requests_oauthlib import TokenUpdated +from typing import Any, Dict + +from asyncio import run_coroutine_threadsafe +import requests +from withings_api import ( + AbstractWithingsApi, + SleepGetResponse, + MeasureGetMeasResponse, + SleepGetSummaryResponse, +) +from withings_api.common import UnauthorizedException, AuthFailedException from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.config_entry_oauth2_flow import ( + AbstractOAuth2Implementation, + OAuth2Session, +) from homeassistant.util import dt, slugify from . import const _LOGGER = logging.getLogger(const.LOG_NAMESPACE) NOT_AUTHENTICATED_ERROR = re.compile( - ".*(Error Code (100|101|102|200|401)|Missing access token parameter).*", + # ".*(Error Code (100|101|102|200|401)|Missing access token parameter).*", + "^401,.*", re.IGNORECASE, ) @@ -37,40 +50,82 @@ class ServiceError(HomeAssistantError): class ThrottleData: """Throttle data.""" - def __init__(self, interval: int, data): + def __init__(self, interval: int, data: Any): """Constructor.""" self._time = int(time.time()) self._interval = interval self._data = data @property - def time(self): + def time(self) -> int: """Get time created.""" return self._time @property - def interval(self): + def interval(self) -> int: """Get interval.""" return self._interval @property - def data(self): + def data(self) -> Any: """Get data.""" return self._data - def is_expired(self): + def is_expired(self) -> bool: """Is this data expired.""" return int(time.time()) - self.time > self.interval +class ConfigEntryWithingsApi(AbstractWithingsApi): + """Withing API that uses HA resources.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + implementation: AbstractOAuth2Implementation, + ): + """Initialize object.""" + self._hass = hass + self._config_entry = config_entry + self._implementation = implementation + self.session = OAuth2Session(hass, config_entry, implementation) + + def _request( + self, path: str, params: Dict[str, Any], method: str = "GET" + ) -> Dict[str, Any]: + return run_coroutine_threadsafe( + self.async_do_request(path, params, method), self._hass.loop + ).result() + + async def async_do_request( + self, path: str, params: Dict[str, Any], method: str = "GET" + ) -> Dict[str, Any]: + """Perform an async request.""" + await self.session.async_ensure_token_valid() + + response = await self._hass.async_add_executor_job( + partial( + requests.request, + method, + "%s/%s" % (self.URL, path), + params=params, + headers={ + "Authorization": "Bearer %s" + % self._config_entry.data["token"]["access_token"] + }, + ) + ) + + return response.json() + + class WithingsDataManager: """A class representing an Withings cloud service connection.""" service_available = None - def __init__( - self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi - ): + def __init__(self, hass: HomeAssistant, profile: str, api: ConfigEntryWithingsApi): """Constructor.""" self._hass = hass self._api = api @@ -95,27 +150,27 @@ def slug(self) -> str: return self._slug @property - def api(self): + def api(self) -> ConfigEntryWithingsApi: """Get the api object.""" return self._api @property - def measures(self): + def measures(self) -> MeasureGetMeasResponse: """Get the current measures data.""" return self._measures @property - def sleep(self): + def sleep(self) -> SleepGetResponse: """Get the current sleep data.""" return self._sleep @property - def sleep_summary(self): + def sleep_summary(self) -> SleepGetSummaryResponse: """Get the current sleep summary data.""" return self._sleep_summary @staticmethod - def get_throttle_interval(): + def get_throttle_interval() -> int: """Get the throttle interval.""" return const.THROTTLE_INTERVAL @@ -128,22 +183,26 @@ def set_throttle_data(self, domain: str, throttle_data: ThrottleData): self.throttle_data[domain] = throttle_data @staticmethod - def print_service_unavailable(): + def print_service_unavailable() -> bool: """Print the service is unavailable (once) to the log.""" if WithingsDataManager.service_available is not False: _LOGGER.error("Looks like the service is not available at the moment") WithingsDataManager.service_available = False return True + return False + @staticmethod - def print_service_available(): + def print_service_available() -> bool: """Print the service is available (once) to to the log.""" if WithingsDataManager.service_available is not True: _LOGGER.info("Looks like the service is available again") WithingsDataManager.service_available = True return True - async def call(self, function, is_first_call=True, throttle_domain=None): + return False + + async def call(self, function, throttle_domain=None) -> Any: """Call an api method and handle the result.""" throttle_data = self.get_throttle_data(throttle_domain) @@ -167,21 +226,12 @@ async def call(self, function, is_first_call=True, throttle_domain=None): WithingsDataManager.print_service_available() return result - except TokenUpdated: - WithingsDataManager.print_service_available() - if not is_first_call: - raise ServiceError( - "Stuck in a token update loop. This should never happen" - ) - - _LOGGER.info("Token updated, re-running call.") - return await self.call(function, False, throttle_domain) - - except MissingTokenError as ex: - raise NotAuthenticatedError(ex) - except Exception as ex: # pylint: disable=broad-except - # Service error, probably not authenticated. + # Withings api encountered error. + if isinstance(ex, (UnauthorizedException, AuthFailedException)): + raise NotAuthenticatedError(ex) + + # Oauth2 config flow failed to authenticate. if NOT_AUTHENTICATED_ERROR.match(str(ex)): raise NotAuthenticatedError(ex) @@ -189,37 +239,37 @@ async def call(self, function, is_first_call=True, throttle_domain=None): WithingsDataManager.print_service_unavailable() raise PlatformNotReady(ex) - async def check_authenticated(self): + async def check_authenticated(self) -> bool: """Check if the user is authenticated.""" def function(): - return self._api.request("user", "getdevice", version="v2") + return bool(self._api.user_get_device()) return await self.call(function) - async def update_measures(self): + async def update_measures(self) -> MeasureGetMeasResponse: """Update the measures data.""" def function(): - return self._api.get_measures() + return self._api.measure_get_meas() self._measures = await self.call(function, throttle_domain="update_measures") return self._measures - async def update_sleep(self): + async def update_sleep(self) -> SleepGetResponse: """Update the sleep data.""" end_date = int(time.time()) start_date = end_date - (6 * 60 * 60) def function(): - return self._api.get_sleep(startdate=start_date, enddate=end_date) + return self._api.sleep_get(startdate=start_date, enddate=end_date) self._sleep = await self.call(function, throttle_domain="update_sleep") return self._sleep - async def update_sleep_summary(self): + async def update_sleep_summary(self) -> SleepGetSummaryResponse: """Update the sleep summary data.""" now = dt.utcnow() yesterday = now - datetime.timedelta(days=1) @@ -240,7 +290,7 @@ async def update_sleep_summary(self): ) def function(): - return self._api.get_sleep_summary(lastupdate=yesterday_noon.timestamp()) + return self._api.sleep_get_summary(lastupdate=yesterday_noon) self._sleep_summary = await self.call( function, throttle_domain="update_sleep_summary" @@ -250,36 +300,16 @@ def function(): def create_withings_data_manager( - hass: HomeAssistantType, entry: ConfigEntry + hass: HomeAssistant, + config_entry: ConfigEntry, + implementation: AbstractOAuth2Implementation, ) -> WithingsDataManager: """Set up the sensor config entry.""" - entry_creds = entry.data.get(const.CREDENTIALS) or {} - profile = entry.data[const.PROFILE] - credentials = withings.WithingsCredentials( - entry_creds.get("access_token"), - entry_creds.get("token_expiry"), - entry_creds.get("token_type"), - entry_creds.get("refresh_token"), - entry_creds.get("user_id"), - entry_creds.get("client_id"), - entry_creds.get("consumer_secret"), - ) - - def credentials_saver(credentials_param): - _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - - # Sanitizing the data as sometimes a WithingsCredentials object - # is passed through from the API. - cred_data = credentials_param - if not isinstance(credentials_param, dict): - cred_data = credentials_param.__dict__ - - entry.data[const.CREDENTIALS] = cred_data - hass.config_entries.async_update_entry(entry, data={**entry.data}) + profile = config_entry.data.get(const.PROFILE) _LOGGER.debug("Creating withings api instance") - api = withings.WithingsApi( - credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) + api = ConfigEntryWithingsApi( + hass=hass, config_entry=config_entry, implementation=implementation ) _LOGGER.debug("Creating withings data manager for profile: %s", profile) @@ -287,24 +317,25 @@ def credentials_saver(credentials_param): def get_data_manager( - hass: HomeAssistantType, entry: ConfigEntry + hass: HomeAssistant, + entry: ConfigEntry, + implementation: AbstractOAuth2Implementation, ) -> WithingsDataManager: """Get a data manager for a config entry. If the data manager doesn't exist yet, it will be created and cached for later use. """ - profile = entry.data.get(const.PROFILE) + entry_id = entry.entry_id - if not hass.data.get(const.DOMAIN): - hass.data[const.DOMAIN] = {} + hass.data[const.DOMAIN] = hass.data.get(const.DOMAIN, {}) - if not hass.data[const.DOMAIN].get(const.DATA_MANAGER): - hass.data[const.DOMAIN][const.DATA_MANAGER] = {} + domain_dict = hass.data[const.DOMAIN] + domain_dict[const.DATA_MANAGER] = domain_dict.get(const.DATA_MANAGER, {}) - if not hass.data[const.DOMAIN][const.DATA_MANAGER].get(profile): - hass.data[const.DOMAIN][const.DATA_MANAGER][ - profile - ] = create_withings_data_manager(hass, entry) + dm_dict = domain_dict[const.DATA_MANAGER] + dm_dict[entry_id] = dm_dict.get(entry_id) or create_withings_data_manager( + hass, entry, implementation + ) - return hass.data[const.DOMAIN][const.DATA_MANAGER][profile] + return dm_dict[entry_id] diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index c781e785f5e122..cd1e4e4485d7f8 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -1,192 +1,64 @@ """Config flow for Withings.""" -from collections import OrderedDict import logging -from typing import Optional -import aiohttp -import withings_api as withings import voluptuous as vol +from withings_api.common import AuthScope -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.http import HomeAssistantView -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import callback - -from . import const - -DATA_FLOW_IMPL = "withings_flow_implementation" +from homeassistant import config_entries +from homeassistant.components.withings import const +from homeassistant.helpers import config_entry_oauth2_flow _LOGGER = logging.getLogger(__name__) -@callback -def register_flow_implementation(hass, client_id, client_secret, base_url, profiles): - """Register a flow implementation. - - hass: Home assistant object. - client_id: Client id. - client_secret: Client secret. - base_url: Base url of home assistant instance. - profiles: The profiles to work with. - """ - if DATA_FLOW_IMPL not in hass.data: - hass.data[DATA_FLOW_IMPL] = OrderedDict() - - hass.data[DATA_FLOW_IMPL] = { - const.CLIENT_ID: client_id, - const.CLIENT_SECRET: client_secret, - const.BASE_URL: base_url, - const.PROFILES: profiles, - } - - @config_entries.HANDLERS.register(const.DOMAIN) -class WithingsFlowHandler(config_entries.ConfigFlow): +class WithingsFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): """Handle a config flow.""" - VERSION = 1 + DOMAIN = const.DOMAIN CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + _current_data = None + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return { + "scope": ",".join( + [ + AuthScope.USER_INFO.value, + AuthScope.USER_METRICS.value, + AuthScope.USER_ACTIVITY.value, + ] + ) + } - def __init__(self): - """Initialize flow.""" - self.flow_profile = None - self.data = None - - def async_profile_config_entry(self, profile: str) -> Optional[ConfigEntry]: - """Get a profile config entry.""" - entries = self.hass.config_entries.async_entries(const.DOMAIN) - for entry in entries: - if entry.data.get(const.PROFILE) == profile: - return entry - - return None - - def get_auth_client(self, profile: str): - """Get a new auth client.""" - flow = self.hass.data[DATA_FLOW_IMPL] - client_id = flow[const.CLIENT_ID] - client_secret = flow[const.CLIENT_SECRET] - base_url = flow[const.BASE_URL].rstrip("/") - - callback_uri = "{}/{}?flow_id={}&profile={}".format( - base_url.rstrip("/"), - const.AUTH_CALLBACK_PATH.lstrip("/"), - self.flow_id, - profile, - ) - - return withings.WithingsAuth( - client_id, - client_secret, - callback_uri, - scope=",".join(["user.info", "user.metrics", "user.activity"]), - ) - - async def async_step_import(self, user_input=None): - """Create user step.""" - return await self.async_step_user(user_input) - - async def async_step_user(self, user_input=None): - """Create an entry for selecting a profile.""" - flow = self.hass.data.get(DATA_FLOW_IMPL) + async def async_oauth_create_entry(self, data: dict) -> dict: + """Override the create entry so user can select a profile.""" + self._current_data = data + return await self.async_step_profile(data) - if not flow: - return self.async_abort(reason="no_flows") + async def async_step_profile(self, data: dict) -> dict: + """Prompt the user to select a user profile.""" + profile = data.get(const.PROFILE) - if user_input: - return await self.async_step_auth(user_input) + if profile: + new_data = {**self._current_data, **{const.PROFILE: profile}} + self._current_data = None + return await self.async_step_finish(new_data) + profiles = self.hass.data[const.DOMAIN][const.CONFIG][const.PROFILES] return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - {vol.Required(const.PROFILE): vol.In(flow.get(const.PROFILES))} - ), + step_id="profile", + data_schema=vol.Schema({vol.Required(const.PROFILE): vol.In(profiles)}), ) - async def async_step_auth(self, user_input=None): - """Create an entry for auth.""" - if user_input.get(const.CODE): - self.data = user_input - return self.async_external_step_done(next_step_id="finish") - - profile = user_input.get(const.PROFILE) - - auth_client = self.get_auth_client(profile) - - url = auth_client.get_authorize_url() - - return self.async_external_step(step_id="auth", url=url) - - async def async_step_finish(self, user_input=None): - """Received code for authentication.""" - data = user_input or self.data or {} - - _LOGGER.debug( - "Should close all flows below %s", - self.hass.config_entries.flow.async_progress(), - ) - - profile = data[const.PROFILE] - code = data[const.CODE] - - return await self._async_create_session(profile, code) - - async def _async_create_session(self, profile, code): - """Create withings session and entries.""" - auth_client = self.get_auth_client(profile) - - _LOGGER.debug("Requesting credentials with code: %s.", code) - credentials = auth_client.get_credentials(code) - - return self.async_create_entry( - title=profile, - data={const.PROFILE: profile, const.CREDENTIALS: credentials.__dict__}, - ) - - -class WithingsAuthCallbackView(HomeAssistantView): - """Withings Authorization Callback View.""" - - requires_auth = False - url = const.AUTH_CALLBACK_PATH - name = const.AUTH_CALLBACK_NAME - - def __init__(self): - """Constructor.""" - - async def get(self, request): - """Receive authorization code.""" - hass = request.app["hass"] - - code = request.query.get("code") - profile = request.query.get("profile") - flow_id = request.query.get("flow_id") - - if not flow_id: - return aiohttp.web_response.Response( - status=400, text="'flow_id' argument not provided in url." - ) - - if not profile: - return aiohttp.web_response.Response( - status=400, text="'profile' argument not provided in url." - ) - - if not code: - return aiohttp.web_response.Response( - status=400, text="'code' argument not provided in url." - ) - - try: - await hass.config_entries.flow.async_configure( - flow_id, {const.PROFILE: profile, const.CODE: code} - ) - - return aiohttp.web_response.Response( - status=200, - headers={"content-type": "text/html"}, - text="", - ) + async def async_step_finish(self, data: dict) -> dict: + """Finish the flow.""" + self._current_data = None - except data_entry_flow.UnknownFlow: - return aiohttp.web_response.Response(status=400, text="Unknown flow") + return self.async_create_entry(title=data[const.PROFILE], data=data) diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index 79527d9d557be4..856f50ce9adc0c 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -19,6 +19,7 @@ AUTH_CALLBACK_NAME = "withings:authorize" THROTTLE_INTERVAL = 60 +SCAN_INTERVAL = 60 STATE_UNKNOWN = const.STATE_UNKNOWN STATE_AWAKE = "awake" @@ -26,40 +27,6 @@ STATE_LIGHT = "light" STATE_REM = "rem" -MEASURE_TYPE_BODY_TEMP = 71 -MEASURE_TYPE_BONE_MASS = 88 -MEASURE_TYPE_DIASTOLIC_BP = 9 -MEASURE_TYPE_FAT_MASS = 8 -MEASURE_TYPE_FAT_MASS_FREE = 5 -MEASURE_TYPE_FAT_RATIO = 6 -MEASURE_TYPE_HEART_PULSE = 11 -MEASURE_TYPE_HEIGHT = 4 -MEASURE_TYPE_HYDRATION = 77 -MEASURE_TYPE_MUSCLE_MASS = 76 -MEASURE_TYPE_PWV = 91 -MEASURE_TYPE_SKIN_TEMP = 73 -MEASURE_TYPE_SLEEP_DEEP_DURATION = "deepsleepduration" -MEASURE_TYPE_SLEEP_HEART_RATE_AVERAGE = "hr_average" -MEASURE_TYPE_SLEEP_HEART_RATE_MAX = "hr_max" -MEASURE_TYPE_SLEEP_HEART_RATE_MIN = "hr_min" -MEASURE_TYPE_SLEEP_LIGHT_DURATION = "lightsleepduration" -MEASURE_TYPE_SLEEP_REM_DURATION = "remsleepduration" -MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_AVERAGE = "rr_average" -MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MAX = "rr_max" -MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MIN = "rr_min" -MEASURE_TYPE_SLEEP_STATE_AWAKE = 0 -MEASURE_TYPE_SLEEP_STATE_DEEP = 2 -MEASURE_TYPE_SLEEP_STATE_LIGHT = 1 -MEASURE_TYPE_SLEEP_STATE_REM = 3 -MEASURE_TYPE_SLEEP_TOSLEEP_DURATION = "durationtosleep" -MEASURE_TYPE_SLEEP_TOWAKEUP_DURATION = "durationtowakeup" -MEASURE_TYPE_SLEEP_WAKEUP_DURATION = "wakeupduration" -MEASURE_TYPE_SLEEP_WAKUP_COUNT = "wakeupcount" -MEASURE_TYPE_SPO2 = 54 -MEASURE_TYPE_SYSTOLIC_BP = 10 -MEASURE_TYPE_TEMP = 12 -MEASURE_TYPE_WEIGHT = 1 - MEAS_BODY_TEMP_C = "body_temperature_c" MEAS_BONE_MASS_KG = "bone_mass_kg" MEAS_DIASTOLIC_MMHG = "diastolic_blood_pressure_mmhg" diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 7c6e4ec044afaf..ea9845f3e4245c 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "withings-api==2.0.0b8" + "withings-api==2.1.2" ], "dependencies": [ "api", diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 0293784fd3e4ac..17eae93ec0db92 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -1,10 +1,22 @@ """Sensors flow for Withings.""" -import typing as types +from typing import Callable, List, Union + +from withings_api.common import ( + MeasureType, + GetSleepSummaryField, + MeasureGetMeasResponse, + SleepGetResponse, + SleepGetSummaryResponse, + get_measure_value, + MeasureGroupAttribs, + SleepState, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify +from homeassistant.helpers import config_entry_oauth2_flow from . import const from .common import _LOGGER, WithingsDataManager, get_data_manager @@ -16,55 +28,20 @@ async def async_setup_entry( - hass: HomeAssistantType, + hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: types.Callable[[types.List[Entity], bool], None], -): + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: """Set up the sensor config entry.""" - data_manager = get_data_manager(hass, entry) - entities = create_sensor_entities(data_manager) - async_add_entities(entities, True) - - -def get_measures(): - """Get all the measures. - - This function exists to be easily mockable so we can test - one measure at a time. This becomes necessary when integration - testing throttle functionality in the data manager. - """ - return list(WITHINGS_MEASUREMENTS_MAP) - - -def create_sensor_entities(data_manager: WithingsDataManager): - """Create sensor entities.""" - entities = [] - - measures = get_measures() - - for attribute in WITHINGS_ATTRIBUTES: - if attribute.measurement not in measures: - _LOGGER.debug( - "Skipping measurement %s as it is not in the" - "list of measurements to use", - attribute.measurement, - ) - continue - - _LOGGER.debug( - "Creating entity for measurement: %s, measure_type: %s," - "friendly_name: %s, unit_of_measurement: %s", - attribute.measurement, - attribute.measure_type, - attribute.friendly_name, - attribute.unit_of_measurement, - ) - - entity = WithingsHealthSensor(data_manager, attribute) + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) - entities.append(entity) + data_manager = get_data_manager(hass, entry, implementation) + user_id = entry.data["token"]["userid"] - return entities + entities = create_sensor_entities(data_manager, user_id) + async_add_entities(entities, True) class WithingsAttribute: @@ -107,104 +84,104 @@ class WithingsSleepSummaryAttribute(WithingsAttribute): WITHINGS_ATTRIBUTES = [ WithingsMeasureAttribute( const.MEAS_WEIGHT_KG, - const.MEASURE_TYPE_WEIGHT, + MeasureType.WEIGHT, "Weight", const.UOM_MASS_KG, "mdi:weight-kilogram", ), WithingsMeasureAttribute( const.MEAS_FAT_MASS_KG, - const.MEASURE_TYPE_FAT_MASS, + MeasureType.FAT_MASS_WEIGHT, "Fat Mass", const.UOM_MASS_KG, "mdi:weight-kilogram", ), WithingsMeasureAttribute( const.MEAS_FAT_FREE_MASS_KG, - const.MEASURE_TYPE_FAT_MASS_FREE, + MeasureType.FAT_FREE_MASS, "Fat Free Mass", const.UOM_MASS_KG, "mdi:weight-kilogram", ), WithingsMeasureAttribute( const.MEAS_MUSCLE_MASS_KG, - const.MEASURE_TYPE_MUSCLE_MASS, + MeasureType.MUSCLE_MASS, "Muscle Mass", const.UOM_MASS_KG, "mdi:weight-kilogram", ), WithingsMeasureAttribute( const.MEAS_BONE_MASS_KG, - const.MEASURE_TYPE_BONE_MASS, + MeasureType.BONE_MASS, "Bone Mass", const.UOM_MASS_KG, "mdi:weight-kilogram", ), WithingsMeasureAttribute( const.MEAS_HEIGHT_M, - const.MEASURE_TYPE_HEIGHT, + MeasureType.HEIGHT, "Height", const.UOM_LENGTH_M, "mdi:ruler", ), WithingsMeasureAttribute( const.MEAS_TEMP_C, - const.MEASURE_TYPE_TEMP, + MeasureType.TEMPERATURE, "Temperature", const.UOM_TEMP_C, "mdi:thermometer", ), WithingsMeasureAttribute( const.MEAS_BODY_TEMP_C, - const.MEASURE_TYPE_BODY_TEMP, + MeasureType.BODY_TEMPERATURE, "Body Temperature", const.UOM_TEMP_C, "mdi:thermometer", ), WithingsMeasureAttribute( const.MEAS_SKIN_TEMP_C, - const.MEASURE_TYPE_SKIN_TEMP, + MeasureType.SKIN_TEMPERATURE, "Skin Temperature", const.UOM_TEMP_C, "mdi:thermometer", ), WithingsMeasureAttribute( const.MEAS_FAT_RATIO_PCT, - const.MEASURE_TYPE_FAT_RATIO, + MeasureType.FAT_RATIO, "Fat Ratio", const.UOM_PERCENT, None, ), WithingsMeasureAttribute( const.MEAS_DIASTOLIC_MMHG, - const.MEASURE_TYPE_DIASTOLIC_BP, + MeasureType.DIASTOLIC_BLOOD_PRESSURE, "Diastolic Blood Pressure", const.UOM_MMHG, None, ), WithingsMeasureAttribute( const.MEAS_SYSTOLIC_MMGH, - const.MEASURE_TYPE_SYSTOLIC_BP, + MeasureType.SYSTOLIC_BLOOD_PRESSURE, "Systolic Blood Pressure", const.UOM_MMHG, None, ), WithingsMeasureAttribute( const.MEAS_HEART_PULSE_BPM, - const.MEASURE_TYPE_HEART_PULSE, + MeasureType.HEART_RATE, "Heart Pulse", const.UOM_BEATS_PER_MINUTE, "mdi:heart-pulse", ), WithingsMeasureAttribute( - const.MEAS_SPO2_PCT, const.MEASURE_TYPE_SPO2, "SP02", const.UOM_PERCENT, None + const.MEAS_SPO2_PCT, MeasureType.SP02, "SP02", const.UOM_PERCENT, None ), WithingsMeasureAttribute( - const.MEAS_HYDRATION, const.MEASURE_TYPE_HYDRATION, "Hydration", "", "mdi:water" + const.MEAS_HYDRATION, MeasureType.HYDRATION, "Hydration", "", "mdi:water" ), WithingsMeasureAttribute( const.MEAS_PWV, - const.MEASURE_TYPE_PWV, + MeasureType.PULSE_WAVE_VELOCITY, "Pulse Wave Velocity", const.UOM_METERS_PER_SECOND, None, @@ -214,91 +191,91 @@ class WithingsSleepSummaryAttribute(WithingsAttribute): ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_WAKEUP_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_WAKEUP_DURATION, + GetSleepSummaryField.WAKEUP_DURATION.value, "Wakeup time", const.UOM_SECONDS, "mdi:sleep-off", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_LIGHT_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_LIGHT_DURATION, + GetSleepSummaryField.LIGHT_SLEEP_DURATION.value, "Light sleep", const.UOM_SECONDS, "mdi:sleep", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_DEEP_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_DEEP_DURATION, + GetSleepSummaryField.DEEP_SLEEP_DURATION.value, "Deep sleep", const.UOM_SECONDS, "mdi:sleep", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_REM_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_REM_DURATION, + GetSleepSummaryField.REM_SLEEP_DURATION.value, "REM sleep", const.UOM_SECONDS, "mdi:sleep", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_WAKEUP_COUNT, - const.MEASURE_TYPE_SLEEP_WAKUP_COUNT, + GetSleepSummaryField.WAKEUP_COUNT.value, "Wakeup count", const.UOM_FREQUENCY, "mdi:sleep-off", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_TOSLEEP_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_TOSLEEP_DURATION, + GetSleepSummaryField.DURATION_TO_SLEEP.value, "Time to sleep", const.UOM_SECONDS, "mdi:sleep", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS, - const.MEASURE_TYPE_SLEEP_TOWAKEUP_DURATION, + GetSleepSummaryField.DURATION_TO_WAKEUP.value, "Time to wakeup", const.UOM_SECONDS, "mdi:sleep-off", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_HEART_RATE_AVERAGE, - const.MEASURE_TYPE_SLEEP_HEART_RATE_AVERAGE, + GetSleepSummaryField.HR_AVERAGE.value, "Average heart rate", const.UOM_BEATS_PER_MINUTE, "mdi:heart-pulse", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_HEART_RATE_MIN, - const.MEASURE_TYPE_SLEEP_HEART_RATE_MIN, + GetSleepSummaryField.HR_MIN.value, "Minimum heart rate", const.UOM_BEATS_PER_MINUTE, "mdi:heart-pulse", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_HEART_RATE_MAX, - const.MEASURE_TYPE_SLEEP_HEART_RATE_MAX, + GetSleepSummaryField.HR_MAX.value, "Maximum heart rate", const.UOM_BEATS_PER_MINUTE, "mdi:heart-pulse", ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE, - const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_AVERAGE, + GetSleepSummaryField.RR_AVERAGE.value, "Average respiratory rate", const.UOM_BREATHS_PER_MINUTE, None, ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_RESPIRATORY_RATE_MIN, - const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MIN, + GetSleepSummaryField.RR_MIN.value, "Minimum respiratory rate", const.UOM_BREATHS_PER_MINUTE, None, ), WithingsSleepSummaryAttribute( const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, - const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MAX, + GetSleepSummaryField.RR_MAX.value, "Maximum respiratory rate", const.UOM_BREATHS_PER_MINUTE, None, @@ -312,7 +289,10 @@ class WithingsHealthSensor(Entity): """Implementation of a Withings sensor.""" def __init__( - self, data_manager: WithingsDataManager, attribute: WithingsAttribute + self, + data_manager: WithingsDataManager, + attribute: WithingsAttribute, + user_id: str, ) -> None: """Initialize the Withings sensor.""" self._data_manager = data_manager @@ -320,7 +300,7 @@ def __init__( self._state = None self._slug = self._data_manager.slug - self._user_id = self._data_manager.api.get_credentials().user_id + self._user_id = user_id @property def name(self) -> str: @@ -335,7 +315,7 @@ def unique_id(self) -> str: ) @property - def state(self): + def state(self) -> Union[str, int, float, None]: """Return the state of the sensor.""" return self._state @@ -350,7 +330,7 @@ def icon(self) -> str: return self._attribute.icon @property - def device_state_attributes(self): + def device_state_attributes(self) -> None: """Get withings attributes.""" return self._attribute.__dict__ @@ -378,71 +358,45 @@ async def async_update(self) -> None: await self._data_manager.update_sleep_summary() await self.async_update_sleep_summary(self._data_manager.sleep_summary) - async def async_update_measure(self, data) -> None: + async def async_update_measure(self, data: MeasureGetMeasResponse) -> None: """Update the measures data.""" - if data is None: - _LOGGER.error("Provided data is None. Setting state to %s", None) - self._state = None - return - measure_type = self._attribute.measure_type _LOGGER.debug( "Finding the unambiguous measure group with measure_type: %s", measure_type ) - measure_groups = [ - g - for g in data - if (not g.is_ambiguous() and g.get_measure(measure_type) is not None) - ] - - if not measure_groups: - _LOGGER.debug("No measure groups found, setting state to %s", None) - self._state = None - return - - _LOGGER.debug( - "Sorting list of %s measure groups by date created (DESC)", - len(measure_groups), - ) - measure_groups.sort(key=(lambda g: g.created), reverse=True) - self._state = round(measure_groups[0].get_measure(measure_type), 4) + value = get_measure_value(data, measure_type, MeasureGroupAttribs.UNAMBIGUOUS) - async def async_update_sleep_state(self, data) -> None: - """Update the sleep state data.""" - if data is None: - _LOGGER.error("Provided data is None. Setting state to %s", None) + if value is None: + _LOGGER.debug("Could not find a value, setting state to %s", None) self._state = None return + self._state = round(value, 2) + + async def async_update_sleep_state(self, data: SleepGetResponse) -> None: + """Update the sleep state data.""" if not data.series: _LOGGER.debug("No sleep data, setting state to %s", None) self._state = None return - series = sorted(data.series, key=lambda o: o.enddate, reverse=True) + serie = data.series[len(data.series) - 1] + state = None + if serie.state == SleepState.AWAKE: + state = const.STATE_AWAKE + elif serie.state == SleepState.LIGHT: + state = const.STATE_LIGHT + elif serie.state == SleepState.DEEP: + state = const.STATE_DEEP + elif serie.state == SleepState.REM: + state = const.STATE_REM - serie = series[0] - - if serie.state == const.MEASURE_TYPE_SLEEP_STATE_AWAKE: - self._state = const.STATE_AWAKE - elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_LIGHT: - self._state = const.STATE_LIGHT - elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_DEEP: - self._state = const.STATE_DEEP - elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_REM: - self._state = const.STATE_REM - else: - self._state = None + self._state = state - async def async_update_sleep_summary(self, data) -> None: + async def async_update_sleep_summary(self, data: SleepGetSummaryResponse) -> None: """Update the sleep summary data.""" - if data is None: - _LOGGER.error("Provided data is None. Setting state to %s", None) - self._state = None - return - if not data.series: _LOGGER.debug("Sleep data has no series, setting state to %s", None) self._state = None @@ -454,7 +408,59 @@ async def async_update_sleep_summary(self, data) -> None: _LOGGER.debug("Determining total value for: %s", measurement) total = 0 for serie in data.series: - if hasattr(serie, measure_type): - total += getattr(serie, measure_type) + data = serie.data + value = 0 + if measure_type == GetSleepSummaryField.REM_SLEEP_DURATION.value: + value = data.remsleepduration + elif measure_type == GetSleepSummaryField.WAKEUP_DURATION.value: + value = data.wakeupduration + elif measure_type == GetSleepSummaryField.LIGHT_SLEEP_DURATION.value: + value = data.lightsleepduration + elif measure_type == GetSleepSummaryField.DEEP_SLEEP_DURATION.value: + value = data.deepsleepduration + elif measure_type == GetSleepSummaryField.WAKEUP_COUNT.value: + value = data.wakeupcount + elif measure_type == GetSleepSummaryField.DURATION_TO_SLEEP.value: + value = data.durationtosleep + elif measure_type == GetSleepSummaryField.DURATION_TO_WAKEUP.value: + value = data.durationtowakeup + elif measure_type == GetSleepSummaryField.HR_AVERAGE.value: + value = data.hr_average + elif measure_type == GetSleepSummaryField.HR_MIN.value: + value = data.hr_min + elif measure_type == GetSleepSummaryField.HR_MAX.value: + value = data.hr_max + elif measure_type == GetSleepSummaryField.RR_AVERAGE.value: + value = data.rr_average + elif measure_type == GetSleepSummaryField.RR_MIN.value: + value = data.rr_min + elif measure_type == GetSleepSummaryField.RR_MAX.value: + value = data.rr_max + + # Sometimes a None is provided for value, default to 0. + total += value or 0 self._state = round(total, 4) + + +def create_sensor_entities( + data_manager: WithingsDataManager, user_id: str +) -> List[WithingsHealthSensor]: + """Create sensor entities.""" + entities = [] + + for attribute in WITHINGS_ATTRIBUTES: + _LOGGER.debug( + "Creating entity for measurement: %s, measure_type: %s," + "friendly_name: %s, unit_of_measurement: %s", + attribute.measurement, + attribute.measure_type, + attribute.friendly_name, + attribute.unit_of_measurement, + ) + + entity = WithingsHealthSensor(data_manager, attribute, user_id) + + entities.append(entity) + + return entities diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 1a99abc7255651..23be2cd385f1b0 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -2,19 +2,13 @@ "config": { "title": "Withings", "step": { - "user": { + "profile": { "title": "User Profile.", - "description": "Select a user profile to which you want Home Assistant to map with a Withings profile. On the withings page, be sure to select the same user or data will not be labeled correctly.", + "description": "Which profile did you select on the Withings website? It's important the profiles match, otherwise data will be mis-labeled.", "data": { "profile": "Profile" } } - }, - "create_entry": { - "default": "Successfully authenticated with Withings for the selected profile." - }, - "abort": { - "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation." } } } diff --git a/requirements_all.txt b/requirements_all.txt index 73cb1e8bf045b5..4587ed4ed7b6c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1984,7 +1984,7 @@ websockets==6.0 wirelesstagpy==0.4.0 # homeassistant.components.withings -withings-api==2.0.0b8 +withings-api==2.1.2 # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test.txt b/requirements_test.txt index 7af2ec0dde354b..5240946b004750 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -20,3 +20,4 @@ pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==5.2.1 requests_mock==1.7.0 +responses==0.10.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa907170786701..a927b5aace3ea3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -21,6 +21,7 @@ pytest-sugar==0.9.2 pytest-timeout==1.3.3 pytest==5.2.1 requests_mock==1.7.0 +responses==0.10.6 # homeassistant.components.homekit @@ -629,7 +630,7 @@ watchdog==0.8.3 websockets==6.0 # homeassistant.components.withings -withings-api==2.0.0b8 +withings-api==2.1.2 # homeassistant.components.bluesound # homeassistant.components.startca diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index f3839a1be5524c..570d12d79f63ba 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,213 +1,383 @@ """Common data for for the withings component tests.""" +import re import time +from typing import List -import withings_api as withings +import requests_mock +from withings_api import AbstractWithingsApi +from withings_api.common import ( + MeasureGetMeasGroupAttrib, + MeasureGetMeasGroupCategory, + MeasureType, + SleepModel, + SleepState, +) +from homeassistant import data_entry_flow +import homeassistant.components.api as api +import homeassistant.components.http as http import homeassistant.components.withings.const as const +from homeassistant.config import async_process_ha_core_config +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component +from homeassistant.util import slugify -def new_sleep_data(model, series): - """Create simple dict to simulate api data.""" - return {"series": series, "model": model} +def get_entity_id(measure, profile) -> str: + """Get an entity id for a measure and profile.""" + return "sensor.{}_{}_{}".format(const.DOMAIN, measure, slugify(profile)) -def new_sleep_data_serie(startdate, enddate, state): - """Create simple dict to simulate api data.""" - return {"startdate": startdate, "enddate": enddate, "state": state} +def assert_state_equals( + hass: HomeAssistant, profile: str, measure: str, expected +) -> None: + """Assert the state of a withings sensor.""" + entity_id = get_entity_id(measure, profile) + state_obj = hass.states.get(entity_id) + assert state_obj, "Expected entity {} to exist but it did not".format(entity_id) + + assert state_obj.state == str( + expected + ), "Expected {} but was {} for measure {}, {}".format( + expected, state_obj.state, measure, entity_id + ) -def new_sleep_summary(timezone, model, startdate, enddate, date, modified, data): - """Create simple dict to simulate api data.""" - return { - "timezone": timezone, - "model": model, - "startdate": startdate, - "enddate": enddate, - "date": date, - "modified": modified, - "data": data, - } +async def setup_hass(hass: HomeAssistant) -> dict: + """Configure home assistant.""" + profiles = ["Person0", "Person1", "Person2", "Person3", "Person4"] -def new_sleep_summary_detail( - wakeupduration, - lightsleepduration, - deepsleepduration, - remsleepduration, - wakeupcount, - durationtosleep, - durationtowakeup, - hr_average, - hr_min, - hr_max, - rr_average, - rr_min, - rr_max, -): - """Create simple dict to simulate api data.""" - return { - "wakeupduration": wakeupduration, - "lightsleepduration": lightsleepduration, - "deepsleepduration": deepsleepduration, - "remsleepduration": remsleepduration, - "wakeupcount": wakeupcount, - "durationtosleep": durationtosleep, - "durationtowakeup": durationtowakeup, - "hr_average": hr_average, - "hr_min": hr_min, - "hr_max": hr_max, - "rr_average": rr_average, - "rr_min": rr_min, - "rr_max": rr_max, + hass_config = { + "homeassistant": {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, + api.DOMAIN: {"base_url": "http://localhost/"}, + http.DOMAIN: {"server_port": 8080}, + const.DOMAIN: { + const.CLIENT_ID: "my_client_id", + const.CLIENT_SECRET: "my_client_secret", + const.PROFILES: profiles, + }, } + await async_process_ha_core_config(hass, hass_config.get("homeassistant")) + assert await async_setup_component(hass, http.DOMAIN, hass_config) + assert await async_setup_component(hass, api.DOMAIN, hass_config) + assert await async_setup_component(hass, const.DOMAIN, hass_config) + await hass.async_block_till_done() -def new_measure_group( - grpid, attrib, date, created, category, deviceid, more, offset, measures -): - """Create simple dict to simulate api data.""" - return { - "grpid": grpid, - "attrib": attrib, - "date": date, - "created": created, - "category": category, - "deviceid": deviceid, - "measures": measures, - "more": more, - "offset": offset, - "comment": "blah", # deprecated - } + return hass_config -def new_measure(type_str, value, unit): - """Create simple dict to simulate api data.""" - return { - "value": value, - "type": type_str, - "unit": unit, - "algo": -1, # deprecated - "fm": -1, # deprecated - "fw": -1, # deprecated - } +async def configure_integration( + hass: HomeAssistant, + aiohttp_client, + aioclient_mock, + profiles: List[str], + profile_index: int, + get_device_response: dict, + getmeasures_response: dict, + get_sleep_response: dict, + get_sleep_summary_response: dict, +) -> None: + """Configure the integration for a specific profile.""" + selected_profile = profiles[profile_index] + + with requests_mock.mock() as rqmck: + rqmck.get( + re.compile(AbstractWithingsApi.URL + "/v2/user?.*action=getdevice(&.*|$)"), + status_code=200, + json=get_device_response, + ) + + rqmck.get( + re.compile(AbstractWithingsApi.URL + "/v2/sleep?.*action=get(&.*|$)"), + status_code=200, + json=get_sleep_response, + ) + + rqmck.get( + re.compile( + AbstractWithingsApi.URL + "/v2/sleep?.*action=getsummary(&.*|$)" + ), + status_code=200, + json=get_sleep_summary_response, + ) + rqmck.get( + re.compile(AbstractWithingsApi.URL + "/measure?.*action=getmeas(&.*|$)"), + status_code=200, + json=getmeasures_response, + ) + + # Get the withings config flow. + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": SOURCE_USER} + ) + assert result + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, {"flow_id": result["flow_id"]} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + "https://account.withings.com/oauth2_user/authorize2?" + "response_type=code&client_id=my_client_id&" + "redirect_uri=http://127.0.0.1:8080/auth/external/callback&" + f"state={state}" + "&scope=user.info,user.metrics,user.activity" + ) + + # Simulate user being redirected from withings site. + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" -def withings_sleep_response(states): - """Create a sleep response based on states.""" - data = [] - for state in states: - data.append( - new_sleep_data_serie( - "2019-02-01 0{}:00:00".format(str(len(data))), - "2019-02-01 0{}:00:00".format(str(len(data) + 1)), - state, - ) + aioclient_mock.post( + "https://account.withings.com/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "userid": "myuserid", + }, ) - return withings.WithingsSleep(new_sleep_data("aa", data)) + # Present user with a list of profiles to choose from. + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result.get("type") == "form" + assert result.get("step_id") == "profile" + assert result.get("data_schema").schema["profile"].container == profiles + + # Select the user profile. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {const.PROFILE: selected_profile} + ) + + # Finish the config flow by calling it again. + assert result.get("type") == "create_entry" + assert result.get("result") + config_data = result.get("result").data + assert config_data.get(const.PROFILE) == profiles[profile_index] + assert config_data.get("auth_implementation") == const.DOMAIN + assert config_data.get("token") + + # Ensure all the flows are complete. + flows = hass.config_entries.flow.async_progress() + assert not flows + + # Wait for remaining tasks to complete. + await hass.async_block_till_done() + +WITHINGS_GET_DEVICE_RESPONSE_EMPTY = {"status": 0, "body": {"devices": []}} -WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( - { - "updatetime": "", - "timezone": "", + +WITHINGS_GET_DEVICE_RESPONSE = { + "status": 0, + "body": { + "devices": [ + { + "type": "type1", + "model": "model1", + "battery": "battery1", + "deviceid": "deviceid1", + "timezone": "UTC", + } + ] + }, +} + + +WITHINGS_MEASURES_RESPONSE_EMPTY = { + "status": 0, + "body": {"updatetime": "2019-08-01", "timezone": "UTC", "measuregrps": []}, +} + + +WITHINGS_MEASURES_RESPONSE = { + "status": 0, + "body": { + "updatetime": "2019-08-01", + "timezone": "UTC", "measuregrps": [ # Un-ambiguous groups. - new_measure_group( - 1, - 0, - time.time(), - time.time(), - 1, - "DEV_ID", - False, - 0, - [ - new_measure(const.MEASURE_TYPE_WEIGHT, 70, 0), - new_measure(const.MEASURE_TYPE_FAT_MASS, 5, 0), - new_measure(const.MEASURE_TYPE_FAT_MASS_FREE, 60, 0), - new_measure(const.MEASURE_TYPE_MUSCLE_MASS, 50, 0), - new_measure(const.MEASURE_TYPE_BONE_MASS, 10, 0), - new_measure(const.MEASURE_TYPE_HEIGHT, 2, 0), - new_measure(const.MEASURE_TYPE_TEMP, 40, 0), - new_measure(const.MEASURE_TYPE_BODY_TEMP, 35, 0), - new_measure(const.MEASURE_TYPE_SKIN_TEMP, 20, 0), - new_measure(const.MEASURE_TYPE_FAT_RATIO, 70, -3), - new_measure(const.MEASURE_TYPE_DIASTOLIC_BP, 70, 0), - new_measure(const.MEASURE_TYPE_SYSTOLIC_BP, 100, 0), - new_measure(const.MEASURE_TYPE_HEART_PULSE, 60, 0), - new_measure(const.MEASURE_TYPE_SPO2, 95, -2), - new_measure(const.MEASURE_TYPE_HYDRATION, 95, -2), - new_measure(const.MEASURE_TYPE_PWV, 100, 0), + { + "grpid": 1, + "attrib": MeasureGetMeasGroupAttrib.DEVICE_ENTRY_FOR_USER.real, + "date": time.time(), + "created": time.time(), + "category": MeasureGetMeasGroupCategory.REAL.real, + "deviceid": "DEV_ID", + "more": False, + "offset": 0, + "measures": [ + {"type": MeasureType.WEIGHT, "value": 70, "unit": 0}, + {"type": MeasureType.FAT_MASS_WEIGHT, "value": 5, "unit": 0}, + {"type": MeasureType.FAT_FREE_MASS, "value": 60, "unit": 0}, + {"type": MeasureType.MUSCLE_MASS, "value": 50, "unit": 0}, + {"type": MeasureType.BONE_MASS, "value": 10, "unit": 0}, + {"type": MeasureType.HEIGHT, "value": 2, "unit": 0}, + {"type": MeasureType.TEMPERATURE, "value": 40, "unit": 0}, + {"type": MeasureType.BODY_TEMPERATURE, "value": 40, "unit": 0}, + {"type": MeasureType.SKIN_TEMPERATURE, "value": 20, "unit": 0}, + {"type": MeasureType.FAT_RATIO, "value": 70, "unit": -3}, + { + "type": MeasureType.DIASTOLIC_BLOOD_PRESSURE, + "value": 70, + "unit": 0, + }, + { + "type": MeasureType.SYSTOLIC_BLOOD_PRESSURE, + "value": 100, + "unit": 0, + }, + {"type": MeasureType.HEART_RATE, "value": 60, "unit": 0}, + {"type": MeasureType.SP02, "value": 95, "unit": -2}, + {"type": MeasureType.HYDRATION, "value": 95, "unit": -2}, + {"type": MeasureType.PULSE_WAVE_VELOCITY, "value": 100, "unit": 0}, ], - ), + }, # Ambiguous groups (we ignore these) - new_measure_group( - 1, - 1, - time.time(), - time.time(), - 1, - "DEV_ID", - False, - 0, - [ - new_measure(const.MEASURE_TYPE_WEIGHT, 71, 0), - new_measure(const.MEASURE_TYPE_FAT_MASS, 4, 0), - new_measure(const.MEASURE_TYPE_MUSCLE_MASS, 51, 0), - new_measure(const.MEASURE_TYPE_BONE_MASS, 11, 0), - new_measure(const.MEASURE_TYPE_HEIGHT, 201, 0), - new_measure(const.MEASURE_TYPE_TEMP, 41, 0), - new_measure(const.MEASURE_TYPE_BODY_TEMP, 34, 0), - new_measure(const.MEASURE_TYPE_SKIN_TEMP, 21, 0), - new_measure(const.MEASURE_TYPE_FAT_RATIO, 71, -3), - new_measure(const.MEASURE_TYPE_DIASTOLIC_BP, 71, 0), - new_measure(const.MEASURE_TYPE_SYSTOLIC_BP, 101, 0), - new_measure(const.MEASURE_TYPE_HEART_PULSE, 61, 0), - new_measure(const.MEASURE_TYPE_SPO2, 98, -2), - new_measure(const.MEASURE_TYPE_HYDRATION, 96, -2), - new_measure(const.MEASURE_TYPE_PWV, 102, 0), + { + "grpid": 1, + "attrib": MeasureGetMeasGroupAttrib.DEVICE_ENTRY_FOR_USER.real, + "date": time.time(), + "created": time.time(), + "category": MeasureGetMeasGroupCategory.REAL.real, + "deviceid": "DEV_ID", + "more": False, + "offset": 0, + "measures": [ + {"type": MeasureType.WEIGHT, "value": 71, "unit": 0}, + {"type": MeasureType.FAT_MASS_WEIGHT, "value": 4, "unit": 0}, + {"type": MeasureType.FAT_FREE_MASS, "value": 40, "unit": 0}, + {"type": MeasureType.MUSCLE_MASS, "value": 51, "unit": 0}, + {"type": MeasureType.BONE_MASS, "value": 11, "unit": 0}, + {"type": MeasureType.HEIGHT, "value": 201, "unit": 0}, + {"type": MeasureType.TEMPERATURE, "value": 41, "unit": 0}, + {"type": MeasureType.BODY_TEMPERATURE, "value": 34, "unit": 0}, + {"type": MeasureType.SKIN_TEMPERATURE, "value": 21, "unit": 0}, + {"type": MeasureType.FAT_RATIO, "value": 71, "unit": -3}, + { + "type": MeasureType.DIASTOLIC_BLOOD_PRESSURE, + "value": 71, + "unit": 0, + }, + { + "type": MeasureType.SYSTOLIC_BLOOD_PRESSURE, + "value": 101, + "unit": 0, + }, + {"type": MeasureType.HEART_RATE, "value": 61, "unit": 0}, + {"type": MeasureType.SP02, "value": 98, "unit": -2}, + {"type": MeasureType.HYDRATION, "value": 96, "unit": -2}, + {"type": MeasureType.PULSE_WAVE_VELOCITY, "value": 102, "unit": 0}, ], - ), + }, ], - } -) + }, +} -WITHINGS_SLEEP_RESPONSE = withings_sleep_response( - [ - const.MEASURE_TYPE_SLEEP_STATE_AWAKE, - const.MEASURE_TYPE_SLEEP_STATE_LIGHT, - const.MEASURE_TYPE_SLEEP_STATE_REM, - const.MEASURE_TYPE_SLEEP_STATE_DEEP, - ] -) +WITHINGS_SLEEP_RESPONSE_EMPTY = { + "status": 0, + "body": {"model": SleepModel.TRACKER.real, "series": []}, +} + -WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( - { +WITHINGS_SLEEP_RESPONSE = { + "status": 0, + "body": { + "model": SleepModel.TRACKER.real, "series": [ - new_sleep_summary( - "UTC", - 32, - "2019-02-01", - "2019-02-02", - "2019-02-02", - "12345", - new_sleep_summary_detail( - 110, 210, 310, 410, 510, 610, 710, 810, 910, 1010, 1110, 1210, 1310 - ), - ), - new_sleep_summary( - "UTC", - 32, - "2019-02-01", - "2019-02-02", - "2019-02-02", - "12345", - new_sleep_summary_detail( - 210, 310, 410, 510, 610, 710, 810, 910, 1010, 1110, 1210, 1310, 1410 - ), - ), - ] - } -) + { + "startdate": "2019-02-01 00:00:00", + "enddate": "2019-02-01 01:00:00", + "state": SleepState.AWAKE.real, + }, + { + "startdate": "2019-02-01 01:00:00", + "enddate": "2019-02-01 02:00:00", + "state": SleepState.LIGHT.real, + }, + { + "startdate": "2019-02-01 02:00:00", + "enddate": "2019-02-01 03:00:00", + "state": SleepState.REM.real, + }, + { + "startdate": "2019-02-01 03:00:00", + "enddate": "2019-02-01 04:00:00", + "state": SleepState.DEEP.real, + }, + ], + }, +} + + +WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY = { + "status": 0, + "body": {"more": False, "offset": 0, "series": []}, +} + + +WITHINGS_SLEEP_SUMMARY_RESPONSE = { + "status": 0, + "body": { + "more": False, + "offset": 0, + "series": [ + { + "timezone": "UTC", + "model": SleepModel.SLEEP_MONITOR.real, + "startdate": "2019-02-01", + "enddate": "2019-02-02", + "date": "2019-02-02", + "modified": 12345, + "data": { + "wakeupduration": 110, + "lightsleepduration": 210, + "deepsleepduration": 310, + "remsleepduration": 410, + "wakeupcount": 510, + "durationtosleep": 610, + "durationtowakeup": 710, + "hr_average": 810, + "hr_min": 910, + "hr_max": 1010, + "rr_average": 1110, + "rr_min": 1210, + "rr_max": 1310, + }, + }, + { + "timezone": "UTC", + "model": SleepModel.SLEEP_MONITOR.real, + "startdate": "2019-02-01", + "enddate": "2019-02-02", + "date": "2019-02-02", + "modified": 12345, + "data": { + "wakeupduration": 210, + "lightsleepduration": 310, + "deepsleepduration": 410, + "remsleepduration": 510, + "wakeupcount": 610, + "durationtosleep": 710, + "durationtowakeup": 810, + "hr_average": 910, + "hr_min": 1010, + "hr_max": 1110, + "rr_average": 1210, + "rr_min": 1310, + "rr_max": 1410, + }, + }, + ], + }, +} diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py deleted file mode 100644 index 0aa6af0d7c098a..00000000000000 --- a/tests/components/withings/conftest.py +++ /dev/null @@ -1,350 +0,0 @@ -"""Fixtures for withings tests.""" -import time -from typing import Awaitable, Callable, List - -import asynctest -import withings_api as withings -import pytest - -import homeassistant.components.api as api -import homeassistant.components.http as http -import homeassistant.components.withings.const as const -from homeassistant.components.withings import CONFIG_SCHEMA, DOMAIN -from homeassistant.config import async_process_ha_core_config -from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC -from homeassistant.setup import async_setup_component - -from .common import ( - WITHINGS_MEASURES_RESPONSE, - WITHINGS_SLEEP_RESPONSE, - WITHINGS_SLEEP_SUMMARY_RESPONSE, -) - - -class WithingsFactoryConfig: - """Configuration for withings test fixture.""" - - PROFILE_1 = "Person 1" - PROFILE_2 = "Person 2" - - def __init__( - self, - api_config: dict = None, - http_config: dict = None, - measures: List[str] = None, - unit_system: str = None, - throttle_interval: int = const.THROTTLE_INTERVAL, - withings_request_response="DATA", - withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, - withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, - withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, - ) -> None: - """Constructor.""" - self._throttle_interval = throttle_interval - self._withings_request_response = withings_request_response - self._withings_measures_response = withings_measures_response - self._withings_sleep_response = withings_sleep_response - self._withings_sleep_summary_response = withings_sleep_summary_response - self._withings_config = { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: [ - WithingsFactoryConfig.PROFILE_1, - WithingsFactoryConfig.PROFILE_2, - ], - } - - self._api_config = api_config or {"base_url": "http://localhost/"} - self._http_config = http_config or {} - self._measures = measures - - assert self._withings_config, "withings_config must be set." - assert isinstance( - self._withings_config, dict - ), "withings_config must be a dict." - assert isinstance(self._api_config, dict), "api_config must be a dict." - assert isinstance(self._http_config, dict), "http_config must be a dict." - - self._hass_config = { - "homeassistant": {CONF_UNIT_SYSTEM: unit_system or CONF_UNIT_SYSTEM_METRIC}, - api.DOMAIN: self._api_config, - http.DOMAIN: self._http_config, - DOMAIN: self._withings_config, - } - - @property - def withings_config(self): - """Get withings component config.""" - return self._withings_config - - @property - def api_config(self): - """Get api component config.""" - return self._api_config - - @property - def http_config(self): - """Get http component config.""" - return self._http_config - - @property - def measures(self): - """Get the measures.""" - return self._measures - - @property - def hass_config(self): - """Home assistant config.""" - return self._hass_config - - @property - def throttle_interval(self): - """Throttle interval.""" - return self._throttle_interval - - @property - def withings_request_response(self): - """Request response.""" - return self._withings_request_response - - @property - def withings_measures_response(self) -> withings.WithingsMeasures: - """Measures response.""" - return self._withings_measures_response - - @property - def withings_sleep_response(self) -> withings.WithingsSleep: - """Sleep response.""" - return self._withings_sleep_response - - @property - def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: - """Sleep summary response.""" - return self._withings_sleep_summary_response - - -class WithingsFactoryData: - """Data about the configured withing test component.""" - - def __init__( - self, - hass, - flow_id, - withings_auth_get_credentials_mock, - withings_api_request_mock, - withings_api_get_measures_mock, - withings_api_get_sleep_mock, - withings_api_get_sleep_summary_mock, - data_manager_get_throttle_interval_mock, - ): - """Constructor.""" - self._hass = hass - self._flow_id = flow_id - self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock - self._withings_api_request_mock = withings_api_request_mock - self._withings_api_get_measures_mock = withings_api_get_measures_mock - self._withings_api_get_sleep_mock = withings_api_get_sleep_mock - self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock - self._data_manager_get_throttle_interval_mock = ( - data_manager_get_throttle_interval_mock - ) - - @property - def hass(self): - """Get hass instance.""" - return self._hass - - @property - def flow_id(self): - """Get flow id.""" - return self._flow_id - - @property - def withings_auth_get_credentials_mock(self): - """Get auth credentials mock.""" - return self._withings_auth_get_credentials_mock - - @property - def withings_api_request_mock(self): - """Get request mock.""" - return self._withings_api_request_mock - - @property - def withings_api_get_measures_mock(self): - """Get measures mock.""" - return self._withings_api_get_measures_mock - - @property - def withings_api_get_sleep_mock(self): - """Get sleep mock.""" - return self._withings_api_get_sleep_mock - - @property - def withings_api_get_sleep_summary_mock(self): - """Get sleep summary mock.""" - return self._withings_api_get_sleep_summary_mock - - @property - def data_manager_get_throttle_interval_mock(self): - """Get throttle mock.""" - return self._data_manager_get_throttle_interval_mock - - async def configure_user(self): - """Present a form with user profiles.""" - step = await self.hass.config_entries.flow.async_configure(self.flow_id, None) - assert step["step_id"] == "user" - - async def configure_profile(self, profile: str): - """Select the user profile. Present a form with authorization link.""" - print("CONFIG_PROFILE:", profile) - step = await self.hass.config_entries.flow.async_configure( - self.flow_id, {const.PROFILE: profile} - ) - assert step["step_id"] == "auth" - - async def configure_code(self, profile: str, code: str): - """Handle authorization code. Create config entries.""" - step = await self.hass.config_entries.flow.async_configure( - self.flow_id, {const.PROFILE: profile, const.CODE: code} - ) - assert step["type"] == "external_done" - - await self.hass.async_block_till_done() - - step = await self.hass.config_entries.flow.async_configure( - self.flow_id, {const.PROFILE: profile, const.CODE: code} - ) - - assert step["type"] == "create_entry" - - await self.hass.async_block_till_done() - - async def configure_all(self, profile: str, code: str): - """Configure all flow steps.""" - await self.configure_user() - await self.configure_profile(profile) - await self.configure_code(profile, code) - - -WithingsFactory = Callable[[WithingsFactoryConfig], Awaitable[WithingsFactoryData]] - - -@pytest.fixture(name="withings_factory") -def withings_factory_fixture(request, hass) -> WithingsFactory: - """Home assistant platform fixture.""" - patches = [] - - async def factory(config: WithingsFactoryConfig) -> WithingsFactoryData: - CONFIG_SCHEMA(config.hass_config.get(DOMAIN)) - - await async_process_ha_core_config( - hass, config.hass_config.get("homeassistant") - ) - assert await async_setup_component(hass, http.DOMAIN, config.hass_config) - assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - - withings_auth_get_credentials_patch = asynctest.patch( - "withings_api.WithingsAuth.get_credentials", - return_value=withings.WithingsCredentials( - access_token="my_access_token", - token_expiry=time.time() + 600, - token_type="my_token_type", - refresh_token="my_refresh_token", - user_id="my_user_id", - client_id=config.withings_config.get(const.CLIENT_ID), - consumer_secret=config.withings_config.get(const.CLIENT_SECRET), - ), - ) - withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() - - withings_api_request_patch = asynctest.patch( - "withings_api.WithingsApi.request", - return_value=config.withings_request_response, - ) - withings_api_request_mock = withings_api_request_patch.start() - - withings_api_get_measures_patch = asynctest.patch( - "withings_api.WithingsApi.get_measures", - return_value=config.withings_measures_response, - ) - withings_api_get_measures_mock = withings_api_get_measures_patch.start() - - withings_api_get_sleep_patch = asynctest.patch( - "withings_api.WithingsApi.get_sleep", - return_value=config.withings_sleep_response, - ) - withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() - - withings_api_get_sleep_summary_patch = asynctest.patch( - "withings_api.WithingsApi.get_sleep_summary", - return_value=config.withings_sleep_summary_response, - ) - withings_api_get_sleep_summary_mock = ( - withings_api_get_sleep_summary_patch.start() - ) - - data_manager_get_throttle_interval_patch = asynctest.patch( - "homeassistant.components.withings.common.WithingsDataManager" - ".get_throttle_interval", - return_value=config.throttle_interval, - ) - data_manager_get_throttle_interval_mock = ( - data_manager_get_throttle_interval_patch.start() - ) - - get_measures_patch = asynctest.patch( - "homeassistant.components.withings.sensor.get_measures", - return_value=config.measures, - ) - get_measures_patch.start() - - patches.extend( - [ - withings_auth_get_credentials_patch, - withings_api_request_patch, - withings_api_get_measures_patch, - withings_api_get_sleep_patch, - withings_api_get_sleep_summary_patch, - data_manager_get_throttle_interval_patch, - get_measures_patch, - ] - ) - - # Collect the flow id. - tasks = [] - - orig_async_create_task = hass.async_create_task - - def create_task(*args): - task = orig_async_create_task(*args) - tasks.append(task) - return task - - async_create_task_patch = asynctest.patch.object( - hass, "async_create_task", side_effect=create_task - ) - - with async_create_task_patch: - assert await async_setup_component(hass, DOMAIN, config.hass_config) - await hass.async_block_till_done() - - flow_id = tasks[2].result()["flow_id"] - - return WithingsFactoryData( - hass, - flow_id, - withings_auth_get_credentials_mock, - withings_api_request_mock, - withings_api_get_measures_mock, - withings_api_get_sleep_mock, - withings_api_get_sleep_summary_mock, - data_manager_get_throttle_interval_mock, - ) - - def cleanup(): - for patch in patches: - patch.stop() - - request.addfinalizer(cleanup) - - return factory diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 9f2480f9094e55..e513ebb1d2e35e 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,34 +1,33 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import withings_api as withings -from oauthlib.oauth2.rfc6749.errors import MissingTokenError + import pytest -from requests_oauthlib import TokenUpdated +from withings_api import WithingsApi +from withings_api.common import UnauthorizedException, TimeoutException +from homeassistant.exceptions import PlatformNotReady from homeassistant.components.withings.common import ( NotAuthenticatedError, - ServiceError, WithingsDataManager, ) -from homeassistant.exceptions import PlatformNotReady @pytest.fixture(name="withings_api") -def withings_api_fixture(): +def withings_api_fixture() -> WithingsApi: """Provide withings api.""" - withings_api = withings.WithingsApi.__new__(withings.WithingsApi) + withings_api = WithingsApi.__new__(WithingsApi) withings_api.get_measures = MagicMock() withings_api.get_sleep = MagicMock() return withings_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, withings_api: withings.WithingsApi): +def data_manager_fixture(hass, withings_api: WithingsApi) -> WithingsDataManager: """Provide data manager.""" return WithingsDataManager(hass, "My Profile", withings_api) -def test_print_service(): +def test_print_service() -> None: """Test method.""" # Go from None to True WithingsDataManager.service_available = None @@ -57,54 +56,27 @@ def test_print_service(): assert not WithingsDataManager.print_service_unavailable() -async def test_data_manager_call(data_manager): +async def test_data_manager_call(data_manager: WithingsDataManager) -> None: """Test method.""" - # Token refreshed. - def hello_func(): - return "HELLO2" - - function = MagicMock(side_effect=[TokenUpdated("my_token"), hello_func()]) - result = await data_manager.call(function) - assert result == "HELLO2" - assert function.call_count == 2 - - # Too many token refreshes. - function = MagicMock( - side_effect=[TokenUpdated("my_token"), TokenUpdated("my_token")] - ) - try: - result = await data_manager.call(function) - assert False, "This should not have ran." - except ServiceError: - assert True - assert function.call_count == 2 - # Not authenticated 1. - test_function = MagicMock(side_effect=MissingTokenError("Error Code 401")) - try: - result = await data_manager.call(test_function) - assert False, "An exception should have been thrown." - except NotAuthenticatedError: - assert True + test_function = MagicMock(side_effect=UnauthorizedException(401)) + with pytest.raises(NotAuthenticatedError): + await data_manager.call(test_function) # Not authenticated 2. - test_function = MagicMock(side_effect=Exception("Error Code 401")) - try: - result = await data_manager.call(test_function) - assert False, "An exception should have been thrown." - except NotAuthenticatedError: - assert True + test_function = MagicMock(side_effect=TimeoutException(522)) + with pytest.raises(PlatformNotReady): + await data_manager.call(test_function) # Service error. test_function = MagicMock(side_effect=PlatformNotReady()) - try: - result = await data_manager.call(test_function) - assert False, "An exception should have been thrown." - except PlatformNotReady: - assert True + with pytest.raises(PlatformNotReady): + await data_manager.call(test_function) -async def test_data_manager_call_throttle_enabled(data_manager): +async def test_data_manager_call_throttle_enabled( + data_manager: WithingsDataManager +) -> None: """Test method.""" hello_func = MagicMock(return_value="HELLO2") @@ -117,7 +89,9 @@ async def test_data_manager_call_throttle_enabled(data_manager): assert hello_func.call_count == 1 -async def test_data_manager_call_throttle_disabled(data_manager): +async def test_data_manager_call_throttle_disabled( + data_manager: WithingsDataManager +) -> None: """Test method.""" hello_func = MagicMock(return_value="HELLO2") diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py deleted file mode 100644 index 3ae9d11c3b6555..00000000000000 --- a/tests/components/withings/test_config_flow.py +++ /dev/null @@ -1,162 +0,0 @@ -"""Tests for the Withings config flow.""" -from aiohttp.web_request import BaseRequest -from asynctest import CoroutineMock, MagicMock -import pytest - -from homeassistant import data_entry_flow -from homeassistant.components.withings import const -from homeassistant.components.withings.config_flow import ( - register_flow_implementation, - WithingsFlowHandler, - WithingsAuthCallbackView, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType - - -@pytest.fixture(name="flow_handler") -def flow_handler_fixture(hass: HomeAssistantType): - """Provide flow handler.""" - flow_handler = WithingsFlowHandler() - flow_handler.hass = hass - return flow_handler - - -def test_flow_handler_init(flow_handler: WithingsFlowHandler): - """Test the init of the flow handler.""" - assert not flow_handler.flow_profile - - -def test_flow_handler_async_profile_config_entry( - hass: HomeAssistantType, flow_handler: WithingsFlowHandler -): - """Test profile config entry.""" - config_entries = [ - ConfigEntry( - version=1, - domain=const.DOMAIN, - title="AAA", - data={}, - source="source", - connection_class="connection_class", - system_options={}, - ), - ConfigEntry( - version=1, - domain=const.DOMAIN, - title="Person 1", - data={const.PROFILE: "Person 1"}, - source="source", - connection_class="connection_class", - system_options={}, - ), - ConfigEntry( - version=1, - domain=const.DOMAIN, - title="BBB", - data={}, - source="source", - connection_class="connection_class", - system_options={}, - ), - ] - - hass.config_entries.async_entries = MagicMock(return_value=config_entries) - - config_entry = flow_handler.async_profile_config_entry - - assert not config_entry("GGGG") - hass.config_entries.async_entries.assert_called_with(const.DOMAIN) - - assert not config_entry("CCC") - hass.config_entries.async_entries.assert_called_with(const.DOMAIN) - - assert config_entry("Person 1") == config_entries[1] - hass.config_entries.async_entries.assert_called_with(const.DOMAIN) - - -def test_flow_handler_get_auth_client( - hass: HomeAssistantType, flow_handler: WithingsFlowHandler -): - """Test creation of an auth client.""" - register_flow_implementation( - hass, "my_client_id", "my_client_secret", "http://localhost/", ["Person 1"] - ) - - client = flow_handler.get_auth_client("Person 1") - assert client.client_id == "my_client_id" - assert client.consumer_secret == "my_client_secret" - assert client.callback_uri.startswith( - "http://localhost/api/withings/authorize?flow_id=" - ) - assert client.callback_uri.endswith("&profile=Person 1") - assert client.scope == "user.info,user.metrics,user.activity" - - -async def test_auth_callback_view_get(hass: HomeAssistantType): - """Test get api path.""" - view = WithingsAuthCallbackView() - hass.config_entries.flow.async_configure = CoroutineMock(return_value="AAAA") - - request = MagicMock(spec=BaseRequest) - request.app = {"hass": hass} - - # No args - request.query = {} - response = await view.get(request) - assert response.status == 400 - hass.config_entries.flow.async_configure.assert_not_called() - hass.config_entries.flow.async_configure.reset_mock() - - # Checking flow_id - request.query = {"flow_id": "my_flow_id"} - response = await view.get(request) - assert response.status == 400 - hass.config_entries.flow.async_configure.assert_not_called() - hass.config_entries.flow.async_configure.reset_mock() - - # Checking flow_id and profile - request.query = {"flow_id": "my_flow_id", "profile": "my_profile"} - response = await view.get(request) - assert response.status == 400 - hass.config_entries.flow.async_configure.assert_not_called() - hass.config_entries.flow.async_configure.reset_mock() - - # Checking flow_id, profile, code - request.query = { - "flow_id": "my_flow_id", - "profile": "my_profile", - "code": "my_code", - } - response = await view.get(request) - assert response.status == 200 - hass.config_entries.flow.async_configure.assert_called_with( - "my_flow_id", {const.PROFILE: "my_profile", const.CODE: "my_code"} - ) - hass.config_entries.flow.async_configure.reset_mock() - - # Exception thrown - hass.config_entries.flow.async_configure = CoroutineMock( - side_effect=data_entry_flow.UnknownFlow() - ) - request.query = { - "flow_id": "my_flow_id", - "profile": "my_profile", - "code": "my_code", - } - response = await view.get(request) - assert response.status == 400 - hass.config_entries.flow.async_configure.assert_called_with( - "my_flow_id", {const.PROFILE: "my_profile", const.CODE: "my_code"} - ) - hass.config_entries.flow.async_configure.reset_mock() - - -async def test_init_without_config(hass): - """Try initializin a configg flow without it being configured.""" - result = await hass.config_entries.flow.async_init( - "withings", context={"source": "user"} - ) - - assert result["type"] == "abort" - assert result["reason"] == "no_flows" diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 609fc1678ea770..bd4940d9504bef 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -1,29 +1,46 @@ """Tests for the Withings component.""" +import re +import time + from asynctest import MagicMock +import requests_mock import voluptuous as vol +from withings_api import AbstractWithingsApi +from withings_api.common import SleepModel, SleepState -import homeassistant.components.api as api import homeassistant.components.http as http -from homeassistant.components.withings import async_setup, const, CONFIG_SCHEMA - -from .conftest import WithingsFactory, WithingsFactoryConfig +from homeassistant.components.withings import ( + async_setup, + async_setup_entry, + const, + CONFIG_SCHEMA, +) +from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import HomeAssistant -BASE_HASS_CONFIG = { - http.DOMAIN: {}, - api.DOMAIN: {"base_url": "http://localhost/"}, - const.DOMAIN: None, -} +from .common import ( + assert_state_equals, + configure_integration, + setup_hass, + WITHINGS_GET_DEVICE_RESPONSE, + WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + WITHINGS_SLEEP_RESPONSE, + WITHINGS_SLEEP_RESPONSE_EMPTY, + WITHINGS_SLEEP_SUMMARY_RESPONSE, + WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_MEASURES_RESPONSE_EMPTY, +) -def config_schema_validate(withings_config): +def config_schema_validate(withings_config) -> None: """Assert a schema config succeeds.""" - hass_config = BASE_HASS_CONFIG.copy() - hass_config[const.DOMAIN] = withings_config + hass_config = {http.DOMAIN: {}, const.DOMAIN: withings_config} return CONFIG_SCHEMA(hass_config) -def config_schema_assert_fail(withings_config): +def config_schema_assert_fail(withings_config) -> None: """Assert a schema config will fail.""" try: config_schema_validate(withings_config) @@ -32,7 +49,7 @@ def config_schema_assert_fail(withings_config): assert True -def test_config_schema_basic_config(): +def test_config_schema_basic_config() -> None: """Test schema.""" config_schema_validate( { @@ -43,7 +60,7 @@ def test_config_schema_basic_config(): ) -def test_config_schema_client_id(): +def test_config_schema_client_id() -> None: """Test schema.""" config_schema_assert_fail( { @@ -67,7 +84,7 @@ def test_config_schema_client_id(): ) -def test_config_schema_client_secret(): +def test_config_schema_client_secret() -> None: """Test schema.""" config_schema_assert_fail( {const.CLIENT_ID: "my_client_id", const.PROFILES: ["Person 1"]} @@ -88,7 +105,7 @@ def test_config_schema_client_secret(): ) -def test_config_schema_profiles(): +def test_config_schema_profiles() -> None: """Test schema.""" config_schema_assert_fail( {const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret"} @@ -130,67 +147,267 @@ def test_config_schema_profiles(): ) -def test_config_schema_base_url(): - """Test schema.""" - config_schema_validate( - { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.PROFILES: ["Person 1"], - } - ) - config_schema_assert_fail( - { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.BASE_URL: 123, - const.PROFILES: ["Person 1"], - } +async def test_async_setup_no_config(hass: HomeAssistant) -> None: + """Test method.""" + hass.async_create_task = MagicMock() + + await async_setup(hass, {}) + + hass.async_create_task.assert_not_called() + + +async def test_upgrade_token( + hass: HomeAssistant, aiohttp_client, aioclient_mock +) -> None: + """Test upgrading from old config data format to new one.""" + config = await setup_hass(hass) + profiles = config[const.DOMAIN][const.PROFILES] + + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=0, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response=WITHINGS_SLEEP_RESPONSE_EMPTY, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, ) - config_schema_assert_fail( - { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.BASE_URL: "", - const.PROFILES: ["Person 1"], - } + + entries = hass.config_entries.async_entries(const.DOMAIN) + assert entries + + entry = entries[0] + data = entry.data + token = data.get("token") + hass.config_entries.async_update_entry( + entry, + data={ + const.PROFILE: data.get(const.PROFILE), + const.CREDENTIALS: { + "access_token": token.get("access_token"), + "refresh_token": token.get("refresh_token"), + "token_expiry": token.get("expires_at"), + "token_type": token.get("type"), + "userid": token.get("userid"), + "client_id": token.get("my_client_id"), + "consumer_secret": token.get("my_consumer_secret"), + }, + }, ) - config_schema_assert_fail( - { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.BASE_URL: "blah blah", - const.PROFILES: ["Person 1"], - } + + with requests_mock.mock() as rqmck: + rqmck.get( + re.compile(AbstractWithingsApi.URL + "/v2/user?.*action=getdevice(&.*|$)"), + status_code=200, + json=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + ) + + assert await async_setup_entry(hass, entry) + + entries = hass.config_entries.async_entries(const.DOMAIN) + assert entries + + data = entries[0].data + + assert data.get("auth_implementation") == const.DOMAIN + assert data.get("implementation") == const.DOMAIN + assert data.get(const.PROFILE) == profiles[0] + + token = data.get("token") + assert token + assert token.get("access_token") == "mock-access-token" + assert token.get("refresh_token") == "mock-refresh-token" + assert token.get("expires_at") > time.time() + assert token.get("type") == "Bearer" + assert token.get("userid") == "myuserid" + assert not token.get("client_id") + assert not token.get("consumer_secret") + + +async def test_auth_failure( + hass: HomeAssistant, aiohttp_client, aioclient_mock +) -> None: + """Test auth failure.""" + config = await setup_hass(hass) + profiles = config[const.DOMAIN][const.PROFILES] + + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=0, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response=WITHINGS_SLEEP_RESPONSE_EMPTY, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, ) - config_schema_validate( - { - const.CLIENT_ID: "my_client_id", - const.CLIENT_SECRET: "my_client_secret", - const.BASE_URL: "https://www.blah.blah.blah/blah/blah", - const.PROFILES: ["Person 1"], - } + + entries = hass.config_entries.async_entries(const.DOMAIN) + assert entries + + entry = entries[0] + hass.config_entries.async_update_entry( + entry, data={**entry.data, **{"new_item": 1}} ) + with requests_mock.mock() as rqmck: + rqmck.get( + re.compile(AbstractWithingsApi.URL + "/v2/user?.*action=getdevice(&.*|$)"), + status_code=200, + json={"status": 401, "body": {}}, + ) -async def test_async_setup_no_config(hass): - """Test method.""" - hass.async_create_task = MagicMock() + assert not (await async_setup_entry(hass, entry)) - await async_setup(hass, {}) - hass.async_create_task.assert_not_called() +async def test_full_setup(hass: HomeAssistant, aiohttp_client, aioclient_mock) -> None: + """Test the whole component lifecycle.""" + config = await setup_hass(hass) + profiles = config[const.DOMAIN][const.PROFILES] + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=0, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE, + getmeasures_response=WITHINGS_MEASURES_RESPONSE, + get_sleep_response=WITHINGS_SLEEP_RESPONSE, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE, + ) -async def test_async_setup_teardown(withings_factory: WithingsFactory, hass): - """Test method.""" - data = await withings_factory(WithingsFactoryConfig(measures=[const.MEAS_TEMP_C])) + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=1, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response=WITHINGS_SLEEP_RESPONSE_EMPTY, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + ) - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=2, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response={ + "status": 0, + "body": { + "model": SleepModel.TRACKER.real, + "series": [ + { + "startdate": "2019-02-01 00:00:00", + "enddate": "2019-02-01 01:00:00", + "state": SleepState.AWAKE.real, + } + ], + }, + }, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + ) + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=3, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response={ + "status": 0, + "body": { + "model": SleepModel.TRACKER.real, + "series": [ + { + "startdate": "2019-02-01 00:00:00", + "enddate": "2019-02-01 01:00:00", + "state": SleepState.LIGHT.real, + } + ], + }, + }, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + ) + + await configure_integration( + hass=hass, + aiohttp_client=aiohttp_client, + aioclient_mock=aioclient_mock, + profiles=profiles, + profile_index=4, + get_device_response=WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + getmeasures_response=WITHINGS_MEASURES_RESPONSE_EMPTY, + get_sleep_response={ + "status": 0, + "body": { + "model": SleepModel.TRACKER.real, + "series": [ + { + "startdate": "2019-02-01 00:00:00", + "enddate": "2019-02-01 01:00:00", + "state": SleepState.REM.real, + } + ], + }, + }, + get_sleep_summary_response=WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, + ) + + # Test the states of the entities. + expected_states = ( + (profiles[0], const.MEAS_WEIGHT_KG, 70.0), + (profiles[0], const.MEAS_FAT_MASS_KG, 5.0), + (profiles[0], const.MEAS_FAT_FREE_MASS_KG, 60.0), + (profiles[0], const.MEAS_MUSCLE_MASS_KG, 50.0), + (profiles[0], const.MEAS_BONE_MASS_KG, 10.0), + (profiles[0], const.MEAS_HEIGHT_M, 2.0), + (profiles[0], const.MEAS_FAT_RATIO_PCT, 0.07), + (profiles[0], const.MEAS_DIASTOLIC_MMHG, 70.0), + (profiles[0], const.MEAS_SYSTOLIC_MMGH, 100.0), + (profiles[0], const.MEAS_HEART_PULSE_BPM, 60.0), + (profiles[0], const.MEAS_SPO2_PCT, 0.95), + (profiles[0], const.MEAS_HYDRATION, 0.95), + (profiles[0], const.MEAS_PWV, 100.0), + (profiles[0], const.MEAS_SLEEP_WAKEUP_DURATION_SECONDS, 320), + (profiles[0], const.MEAS_SLEEP_LIGHT_DURATION_SECONDS, 520), + (profiles[0], const.MEAS_SLEEP_DEEP_DURATION_SECONDS, 720), + (profiles[0], const.MEAS_SLEEP_REM_DURATION_SECONDS, 920), + (profiles[0], const.MEAS_SLEEP_WAKEUP_COUNT, 1120), + (profiles[0], const.MEAS_SLEEP_TOSLEEP_DURATION_SECONDS, 1320), + (profiles[0], const.MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS, 1520), + (profiles[0], const.MEAS_SLEEP_HEART_RATE_AVERAGE, 1720), + (profiles[0], const.MEAS_SLEEP_HEART_RATE_MIN, 1920), + (profiles[0], const.MEAS_SLEEP_HEART_RATE_MAX, 2120), + (profiles[0], const.MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE, 2320), + (profiles[0], const.MEAS_SLEEP_RESPIRATORY_RATE_MIN, 2520), + (profiles[0], const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, 2720), + (profiles[0], const.MEAS_SLEEP_STATE, const.STATE_DEEP), + (profiles[1], const.MEAS_SLEEP_STATE, STATE_UNKNOWN), + (profiles[1], const.MEAS_HYDRATION, STATE_UNKNOWN), + (profiles[2], const.MEAS_SLEEP_STATE, const.STATE_AWAKE), + (profiles[3], const.MEAS_SLEEP_STATE, const.STATE_LIGHT), + (profiles[3], const.MEAS_FAT_FREE_MASS_KG, STATE_UNKNOWN), + (profiles[4], const.MEAS_SLEEP_STATE, const.STATE_REM), + ) + for (profile, meas, value) in expected_states: + assert_state_equals(hass, profile, meas, value) + + # Tear down setup entries. entries = hass.config_entries.async_entries(const.DOMAIN) assert entries for entry in entries: await hass.config_entries.async_unload(entry.entry_id) + + await hass.async_block_till_done() diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py deleted file mode 100644 index 697d0a8b86413f..00000000000000 --- a/tests/components/withings/test_sensor.py +++ /dev/null @@ -1,310 +0,0 @@ -"""Tests for the Withings component.""" -from unittest.mock import MagicMock, patch - -import asynctest -from withings_api import ( - WithingsApi, - WithingsMeasures, - WithingsSleep, - WithingsSleepSummary, -) -import pytest - -from homeassistant.components.withings import DOMAIN -from homeassistant.components.withings.common import NotAuthenticatedError -import homeassistant.components.withings.const as const -from homeassistant.components.withings.sensor import async_setup_entry -from homeassistant.config_entries import ConfigEntry, SOURCE_USER -from homeassistant.const import STATE_UNKNOWN -from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.util import slugify - -from .common import withings_sleep_response -from .conftest import WithingsFactory, WithingsFactoryConfig - - -def get_entity_id(measure, profile): - """Get an entity id for a measure and profile.""" - return "sensor.{}_{}_{}".format(DOMAIN, measure, slugify(profile)) - - -def assert_state_equals(hass: HomeAssistantType, profile: str, measure: str, expected): - """Assert the state of a withings sensor.""" - entity_id = get_entity_id(measure, profile) - state_obj = hass.states.get(entity_id) - - assert state_obj, "Expected entity {} to exist but it did not".format(entity_id) - - assert state_obj.state == str( - expected - ), "Expected {} but was {} for measure {}".format( - expected, state_obj.state, measure - ) - - -async def test_health_sensor_properties(withings_factory: WithingsFactory): - """Test method.""" - data = await withings_factory(WithingsFactoryConfig(measures=[const.MEAS_HEIGHT_M])) - - await data.configure_all(WithingsFactoryConfig.PROFILE_1, "authorization_code") - - state = data.hass.states.get("sensor.withings_height_m_person_1") - state_dict = state.as_dict() - assert state_dict.get("state") == "2" - assert state_dict.get("attributes") == { - "measurement": "height_m", - "measure_type": 4, - "friendly_name": "Withings height_m person_1", - "unit_of_measurement": "m", - "icon": "mdi:ruler", - } - - -SENSOR_TEST_DATA = [ - (const.MEAS_WEIGHT_KG, 70), - (const.MEAS_FAT_MASS_KG, 5), - (const.MEAS_FAT_FREE_MASS_KG, 60), - (const.MEAS_MUSCLE_MASS_KG, 50), - (const.MEAS_BONE_MASS_KG, 10), - (const.MEAS_HEIGHT_M, 2), - (const.MEAS_FAT_RATIO_PCT, 0.07), - (const.MEAS_DIASTOLIC_MMHG, 70), - (const.MEAS_SYSTOLIC_MMGH, 100), - (const.MEAS_HEART_PULSE_BPM, 60), - (const.MEAS_SPO2_PCT, 0.95), - (const.MEAS_HYDRATION, 0.95), - (const.MEAS_PWV, 100), - (const.MEAS_SLEEP_WAKEUP_DURATION_SECONDS, 320), - (const.MEAS_SLEEP_LIGHT_DURATION_SECONDS, 520), - (const.MEAS_SLEEP_DEEP_DURATION_SECONDS, 720), - (const.MEAS_SLEEP_REM_DURATION_SECONDS, 920), - (const.MEAS_SLEEP_WAKEUP_COUNT, 1120), - (const.MEAS_SLEEP_TOSLEEP_DURATION_SECONDS, 1320), - (const.MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS, 1520), - (const.MEAS_SLEEP_HEART_RATE_AVERAGE, 1720), - (const.MEAS_SLEEP_HEART_RATE_MIN, 1920), - (const.MEAS_SLEEP_HEART_RATE_MAX, 2120), - (const.MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE, 2320), - (const.MEAS_SLEEP_RESPIRATORY_RATE_MIN, 2520), - (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, 2720), -] - - -@pytest.mark.parametrize("measure,expected", SENSOR_TEST_DATA) -async def test_health_sensor_throttled( - withings_factory: WithingsFactory, measure, expected -): - """Test method.""" - data = await withings_factory(WithingsFactoryConfig(measures=measure)) - - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") - - # Checking initial data. - assert_state_equals(data.hass, profile, measure, expected) - - # Encountering a throttled data. - await async_update_entity(data.hass, get_entity_id(measure, profile)) - - assert_state_equals(data.hass, profile, measure, expected) - - -NONE_SENSOR_TEST_DATA = [ - (const.MEAS_WEIGHT_KG, STATE_UNKNOWN), - (const.MEAS_SLEEP_STATE, STATE_UNKNOWN), - (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, STATE_UNKNOWN), -] - - -@pytest.mark.parametrize("measure,expected", NONE_SENSOR_TEST_DATA) -async def test_health_sensor_state_none( - withings_factory: WithingsFactory, measure, expected -): - """Test method.""" - data = await withings_factory( - WithingsFactoryConfig( - measures=measure, - withings_measures_response=None, - withings_sleep_response=None, - withings_sleep_summary_response=None, - ) - ) - - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") - - # Checking initial data. - assert_state_equals(data.hass, profile, measure, expected) - - # Encountering a throttled data. - await async_update_entity(data.hass, get_entity_id(measure, profile)) - - assert_state_equals(data.hass, profile, measure, expected) - - -EMPTY_SENSOR_TEST_DATA = [ - (const.MEAS_WEIGHT_KG, STATE_UNKNOWN), - (const.MEAS_SLEEP_STATE, STATE_UNKNOWN), - (const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, STATE_UNKNOWN), -] - - -@pytest.mark.parametrize("measure,expected", EMPTY_SENSOR_TEST_DATA) -async def test_health_sensor_state_empty( - withings_factory: WithingsFactory, measure, expected -): - """Test method.""" - data = await withings_factory( - WithingsFactoryConfig( - measures=measure, - withings_measures_response=WithingsMeasures({"measuregrps": []}), - withings_sleep_response=WithingsSleep({"series": []}), - withings_sleep_summary_response=WithingsSleepSummary({"series": []}), - ) - ) - - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") - - # Checking initial data. - assert_state_equals(data.hass, profile, measure, expected) - - # Encountering a throttled data. - await async_update_entity(data.hass, get_entity_id(measure, profile)) - - assert_state_equals(data.hass, profile, measure, expected) - - -SLEEP_STATES_TEST_DATA = [ - ( - const.STATE_AWAKE, - [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_AWAKE], - ), - ( - const.STATE_LIGHT, - [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_LIGHT], - ), - ( - const.STATE_REM, - [const.MEASURE_TYPE_SLEEP_STATE_DEEP, const.MEASURE_TYPE_SLEEP_STATE_REM], - ), - ( - const.STATE_DEEP, - [const.MEASURE_TYPE_SLEEP_STATE_LIGHT, const.MEASURE_TYPE_SLEEP_STATE_DEEP], - ), - (const.STATE_UNKNOWN, [const.MEASURE_TYPE_SLEEP_STATE_LIGHT, "blah,"]), -] - - -@pytest.mark.parametrize("expected,sleep_states", SLEEP_STATES_TEST_DATA) -async def test_sleep_state_throttled( - withings_factory: WithingsFactory, expected, sleep_states -): - """Test method.""" - measure = const.MEAS_SLEEP_STATE - - data = await withings_factory( - WithingsFactoryConfig( - measures=[measure], - withings_sleep_response=withings_sleep_response(sleep_states), - ) - ) - - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") - - # Check initial data. - assert_state_equals(data.hass, profile, measure, expected) - - # Encountering a throttled data. - await async_update_entity(data.hass, get_entity_id(measure, profile)) - - assert_state_equals(data.hass, profile, measure, expected) - - -async def test_async_setup_check_credentials( - hass: HomeAssistantType, withings_factory: WithingsFactory -): - """Test method.""" - check_creds_patch = asynctest.patch( - "homeassistant.components.withings.common.WithingsDataManager" - ".check_authenticated", - side_effect=NotAuthenticatedError(), - ) - - async_init_patch = asynctest.patch.object( - hass.config_entries.flow, - "async_init", - wraps=hass.config_entries.flow.async_init, - ) - - with check_creds_patch, async_init_patch as async_init_mock: - data = await withings_factory( - WithingsFactoryConfig(measures=[const.MEAS_HEIGHT_M]) - ) - - profile = WithingsFactoryConfig.PROFILE_1 - await data.configure_all(profile, "authorization_code") - - async_init_mock.assert_called_with( - const.DOMAIN, - context={"source": SOURCE_USER, const.PROFILE: profile}, - data={}, - ) - - -async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): - """Test method.""" - expected_creds = { - "access_token": "my_access_token2", - "refresh_token": "my_refresh_token2", - "token_type": "my_token_type2", - "expires_in": "2", - } - - original_withings_api = WithingsApi - withings_api_instance = None - - def new_withings_api(*args, **kwargs): - nonlocal withings_api_instance - withings_api_instance = original_withings_api(*args, **kwargs) - withings_api_instance.request = MagicMock() - return withings_api_instance - - withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) - session_patch = patch("requests_oauthlib.OAuth2Session") - client_patch = patch("oauthlib.oauth2.WebApplicationClient") - update_entry_patch = patch.object( - hass.config_entries, - "async_update_entry", - wraps=hass.config_entries.async_update_entry, - ) - - with session_patch, client_patch, withings_api_patch, update_entry_patch: - async_add_entities = MagicMock() - hass.config_entries.async_update_entry = MagicMock() - config_entry = ConfigEntry( - version=1, - domain=const.DOMAIN, - title="my title", - data={ - const.PROFILE: "Person 1", - const.CREDENTIALS: { - "access_token": "my_access_token", - "refresh_token": "my_refresh_token", - "token_type": "my_token_type", - "token_expiry": "9999999999", - }, - }, - source="source", - connection_class="conn_class", - system_options={}, - ) - - await async_setup_entry(hass, config_entry, async_add_entities) - - withings_api_instance.set_token(expected_creds) - - new_creds = config_entry.data[const.CREDENTIALS] - assert new_creds["access_token"] == "my_access_token2" From 8791a48328076dd61c1f3e80c10dc54a7c8c9c18 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 24 Oct 2019 20:24:46 +0200 Subject: [PATCH 1243/3953] Bump aioesphomeapi to 2.4.1 (#28170) * Bump aioesphomeapi to 2.4.1 * Update requirements * Bump to 2.4.2 --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b2286b8ab67b15..40691c653f51b6 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.4.0" + "aioesphomeapi==2.4.2" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 4587ed4ed7b6c3..069253d9d23e74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,7 +139,7 @@ aiobotocore==0.10.2 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.4.0 +aioesphomeapi==2.4.2 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a927b5aace3ea3..eb38ac61e2edb5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -71,7 +71,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.2 # homeassistant.components.esphome -aioesphomeapi==2.4.0 +aioesphomeapi==2.4.2 # homeassistant.components.emulated_hue # homeassistant.components.http From b61218f90ebccfc5c5258218c09d1705c0cbc755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 24 Oct 2019 21:31:58 +0200 Subject: [PATCH 1244/3953] Tradfri config flow enhancements (#28179) --- homeassistant/components/tradfri/__init__.py | 5 +++-- homeassistant/components/tradfri/config_flow.py | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index bdfabb4b00a3fd..9d1a43b240f708 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -8,6 +8,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.util.json import load_json from . import config_flow # noqa pylint_disable=unused-import from .const import ( @@ -113,8 +114,8 @@ async def on_hass_stop(event): try: gateway_info = await api(gateway.get_gateway_info()) except RequestError: - _LOGGER.error("Tradfri setup failed.") - return False + await factory.shutdown() + raise ConfigEntryNotReady hass.data.setdefault(KEY_API, {})[entry.entry_id] = api hass.data.setdefault(KEY_GATEWAY, {})[entry.entry_id] = gateway diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index bdb195cf53f514..24c3fbc1876802 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -64,13 +64,17 @@ async def async_step_auth(self, user_input=None): errors[KEY_SECURITY_CODE] = err.code else: errors["base"] = err.code + else: + user_input = {} fields = OrderedDict() if self._host is None: - fields[vol.Required(CONF_HOST)] = str + fields[vol.Required(CONF_HOST, default=user_input.get(CONF_HOST))] = str - fields[vol.Required(KEY_SECURITY_CODE)] = str + fields[ + vol.Required(KEY_SECURITY_CODE, default=user_input.get(KEY_SECURITY_CODE)) + ] = str return self.async_show_form( step_id="auth", data_schema=vol.Schema(fields), errors=errors From 322d8c2dd55b8927ea9fe8118bf22bf817bf5bbf Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 24 Oct 2019 22:36:47 +0200 Subject: [PATCH 1245/3953] Fix ESPHome stacktraces when removing entity and shutting down (#28185) --- homeassistant/components/esphome/__init__.py | 20 +++++++++++++++++-- .../components/esphome/entry_data.py | 7 +++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index dd4ac699089922..a669726ca38969 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -95,8 +95,11 @@ async def on_stop(event: Event) -> None: """Cleanup the socket client on HA stop.""" await _cleanup_instance(hass, entry) + # Use async_listen instead of async_listen_once so that we don't deregister + # the callback twice when shutting down Home Assistant. + # "Unable to remove unknown listener .onetime_listener>" entry_data.cleanup_callbacks.append( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) ) @callback @@ -365,6 +368,7 @@ async def platform_async_setup_entry( """ entry_data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] entry_data.info[component_key] = {} + entry_data.old_info[component_key] = {} entry_data.state[component_key] = {} @callback @@ -390,7 +394,13 @@ def async_list_entities(infos: List[EntityInfo]): # Remove old entities for info in old_infos.values(): entry_data.async_remove_entity(hass, component_key, info.key) + + # First copy the now-old info into the backup object + entry_data.old_info[component_key] = entry_data.info[component_key] + # Then update the actual info entry_data.info[component_key] = new_infos + + # Add entities to Home Assistant async_add_entities(add_entities) signal = DISPATCHER_ON_LIST.format(entry_id=entry.entry_id) @@ -524,7 +534,13 @@ def _entry_data(self) -> RuntimeEntryData: @property def _static_info(self) -> EntityInfo: - return self._entry_data.info[self._component_key][self._key] + # Check if value is in info database. Use a single lookup. + info = self._entry_data.info[self._component_key].get(self._key) + if info is not None: + return info + # This entity is in the removal project and has been removed from .info + # already, look in old_info + return self._entry_data.old_info[self._component_key].get(self._key) @property def _device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index b7f9ad9b347519..d916e1a90c8741 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -56,6 +56,13 @@ class RuntimeEntryData: reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None) state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + + # A second list of EntityInfo objects + # This is necessary for when an entity is being removed. HA requires + # some static info to be accessible during removal (unique_id, maybe others) + # If an entity can't find anything in the info array, it will look for info here. + old_info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + services = attr.ib(type=Dict[int, "UserService"], factory=dict) available = attr.ib(type=bool, default=False) device_info = attr.ib(type=DeviceInfo, default=None) From ec478ab8487f0ccd6ac9e1c488f0d217087adc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 24 Oct 2019 22:38:24 +0200 Subject: [PATCH 1246/3953] Add stop feature to tradfri covers (#28180) * Tradfri cover enhancements * tradfri dependency update * Revert addition of battery attrubite * Remove the supported_features property * Remove unwanted file --- homeassistant/components/tradfri/cover.py | 17 +++++------------ homeassistant/components/tradfri/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 9b831dce0ec59d..ae7d6a09ce3e92 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -1,12 +1,6 @@ """Support for IKEA Tradfri covers.""" -from homeassistant.components.cover import ( - CoverDevice, - ATTR_POSITION, - SUPPORT_OPEN, - SUPPORT_CLOSE, - SUPPORT_SET_POSITION, -) +from homeassistant.components.cover import CoverDevice, ATTR_POSITION from .base_class import TradfriBaseDevice from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID @@ -34,11 +28,6 @@ def __init__(self, device, api, gateway_id): self._refresh(device) - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - @property def current_cover_position(self): """Return current position of cover. @@ -59,6 +48,10 @@ async def async_close_cover(self, **kwargs): """Close cover.""" await self._api(self._device_control.set_state(100)) + async def async_stop_cover(self, **kwargs): + """Close cover.""" + await self._api(self._device_control.trigger_blind()) + @property def is_closed(self): """Return if the cover is closed or not.""" diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index d9fa4ad5696b8e..229db67becd15e 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "Tradfri", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==6.3.1"], + "requirements": ["pytradfri[async]==6.4.0"], "homekit": { "models": ["TRADFRI"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 069253d9d23e74..8ad0f091e66184 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1612,7 +1612,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==6.3.1 +pytradfri[async]==6.4.0 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb38ac61e2edb5..25ca8abdbf3861 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -534,7 +534,7 @@ python_awair==0.0.4 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==6.3.1 +pytradfri[async]==6.4.0 # homeassistant.components.vesync pyvesync==1.1.0 From c96d4c978d078f17b5e207e4febbb6e944a49dd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Thu, 24 Oct 2019 20:39:10 +0000 Subject: [PATCH 1247/3953] Fix tzinfo type for onvif component (#28178) --- homeassistant/components/onvif/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 59ee8a8c7ee670..affbbb62338b57 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -206,7 +206,7 @@ async def async_check_date_and_time(self): else: tzone = ( dt_util.get_time_zone(device_time.TimeZone) - or dt_util.DEFAULT_TIME_ZONE, + or dt_util.DEFAULT_TIME_ZONE ) cdate = device_time.LocalDateTime From 98ac8a217d194bfe238d7a2000eea683350c80c8 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Thu, 24 Oct 2019 22:41:07 +0200 Subject: [PATCH 1248/3953] Adding device_class to samsungtv (#28168) * Adding device_id to samsungtv * Lint * Adding test --- homeassistant/components/samsungtv/media_player.py | 11 ++++++++++- tests/components/samsungtv/test_media_player.py | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 2821a05261b337..fd1da31497e456 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -6,7 +6,11 @@ import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import ( + MediaPlayerDevice, + PLATFORM_SCHEMA, + DEVICE_CLASS_TV, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -227,6 +231,11 @@ def supported_features(self): return SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON return SUPPORT_SAMSUNGTV + @property + def device_class(self): + """Set the device class to TV.""" + return DEVICE_CLASS_TV + def turn_off(self): """Turn off media player.""" self._end_of_power_off = dt_util.utcnow() + timedelta(seconds=15) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 9cd5c782b3f39d..1428ba3b39ba77 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -8,6 +8,7 @@ import pytest import tests.common +from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, MEDIA_TYPE_CHANNEL, @@ -197,6 +198,10 @@ def test_supported_features(self): self.device._mac = "fake" assert SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON == self.device.supported_features + def test_device_class(self): + """Test for device_class property.""" + assert DEVICE_CLASS_TV == self.device.device_class + def test_turn_off(self): """Test for turn_off.""" self.device.send_key = mock.Mock() From 059d2572a28446acc8fd87237d61a9ee0dc314b7 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 24 Oct 2019 12:23:02 -0400 Subject: [PATCH 1249/3953] Fixes/zha ieee tail (#28160) * Fix ZHA entity_id assignment. * Update tests. --- homeassistant/components/zha/entity.py | 2 +- tests/components/zha/common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 00c3942358e1fe..c11cd405a99402 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -40,7 +40,7 @@ def __init__(self, unique_id, zha_device, channels, skip_entity_id=False, **kwar self._unique_id = unique_id if not skip_entity_id: ieee = zha_device.ieee - ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]]) + ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) self.entity_id = "{}.{}_{}_{}_{}{}".format( self._domain, slugify(zha_device.manufacturer), diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 5f9172749b047c..788faaaec737f1 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -168,7 +168,7 @@ def make_entity_id(domain, device, cluster, use_suffix=True): machine so that we can test state changes. """ ieee = device.ieee - ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]]) + ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) entity_id = "{}.{}_{}_{}_{}{}".format( domain, slugify(device.manufacturer), From 8f232f3c692d1042c45ff24faca566bfe519245a Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 24 Oct 2019 20:24:46 +0200 Subject: [PATCH 1250/3953] Bump aioesphomeapi to 2.4.1 (#28170) * Bump aioesphomeapi to 2.4.1 * Update requirements * Bump to 2.4.2 --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b2286b8ab67b15..40691c653f51b6 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.4.0" + "aioesphomeapi==2.4.2" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index eacaee7d927d33..6a6c569b4a1963 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,7 +139,7 @@ aiobotocore==0.10.2 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.4.0 +aioesphomeapi==2.4.2 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39903b3606a07a..51b7ae9d71f798 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -70,7 +70,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.2 # homeassistant.components.esphome -aioesphomeapi==2.4.0 +aioesphomeapi==2.4.2 # homeassistant.components.emulated_hue # homeassistant.components.http From c64fe19260bedc1eba4e0bae5f8144223d57c34e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 24 Oct 2019 22:36:47 +0200 Subject: [PATCH 1251/3953] Fix ESPHome stacktraces when removing entity and shutting down (#28185) --- homeassistant/components/esphome/__init__.py | 20 +++++++++++++++++-- .../components/esphome/entry_data.py | 7 +++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index dd4ac699089922..a669726ca38969 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -95,8 +95,11 @@ async def on_stop(event: Event) -> None: """Cleanup the socket client on HA stop.""" await _cleanup_instance(hass, entry) + # Use async_listen instead of async_listen_once so that we don't deregister + # the callback twice when shutting down Home Assistant. + # "Unable to remove unknown listener .onetime_listener>" entry_data.cleanup_callbacks.append( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) ) @callback @@ -365,6 +368,7 @@ async def platform_async_setup_entry( """ entry_data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] entry_data.info[component_key] = {} + entry_data.old_info[component_key] = {} entry_data.state[component_key] = {} @callback @@ -390,7 +394,13 @@ def async_list_entities(infos: List[EntityInfo]): # Remove old entities for info in old_infos.values(): entry_data.async_remove_entity(hass, component_key, info.key) + + # First copy the now-old info into the backup object + entry_data.old_info[component_key] = entry_data.info[component_key] + # Then update the actual info entry_data.info[component_key] = new_infos + + # Add entities to Home Assistant async_add_entities(add_entities) signal = DISPATCHER_ON_LIST.format(entry_id=entry.entry_id) @@ -524,7 +534,13 @@ def _entry_data(self) -> RuntimeEntryData: @property def _static_info(self) -> EntityInfo: - return self._entry_data.info[self._component_key][self._key] + # Check if value is in info database. Use a single lookup. + info = self._entry_data.info[self._component_key].get(self._key) + if info is not None: + return info + # This entity is in the removal project and has been removed from .info + # already, look in old_info + return self._entry_data.old_info[self._component_key].get(self._key) @property def _device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index b7f9ad9b347519..d916e1a90c8741 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -56,6 +56,13 @@ class RuntimeEntryData: reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None) state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + + # A second list of EntityInfo objects + # This is necessary for when an entity is being removed. HA requires + # some static info to be accessible during removal (unique_id, maybe others) + # If an entity can't find anything in the info array, it will look for info here. + old_info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + services = attr.ib(type=Dict[int, "UserService"], factory=dict) available = attr.ib(type=bool, default=False) device_info = attr.ib(type=DeviceInfo, default=None) From 0a5cde7ac3b33a9e20dfdbd8a1e6a80f7ed54ec2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Oct 2019 13:53:30 -0700 Subject: [PATCH 1252/3953] Bumped version to 0.101.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 770669b2d82a98..ce00e38bc4214e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 63deec3275552f4d43d2c9cfce9f6853d16c4972 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Thu, 24 Oct 2019 23:12:41 +0200 Subject: [PATCH 1253/3953] Bump python-slugify to 4.0.0 (#28186) --- 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 d2f10c891a95f0..0e06690c856ef0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 -python-slugify==3.0.6 +python-slugify==4.0.0 pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8ad0f091e66184..81009711ed7598 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -11,7 +11,7 @@ jinja2>=2.10.1 PyJWT==1.7.1 cryptography==2.8 pip>=8.0.3 -python-slugify==3.0.6 +python-slugify==4.0.0 pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 diff --git a/setup.py b/setup.py index d2c4934713be47..e8b32fd8edfe7b 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ # PyJWT has loose dependency. We want the latest one. "cryptography==2.8", "pip>=8.0.3", - "python-slugify==3.0.6", + "python-slugify==4.0.0", "pytz>=2019.03", "pyyaml==5.1.2", "requests==2.22.0", From 67cf7c26da2b0b7a0dffc1d397cd1396bb6da29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 24 Oct 2019 23:25:47 +0200 Subject: [PATCH 1254/3953] Removes unwanted tradfri battery sensor (#28181) --- homeassistant/components/tradfri/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 68a2c10291b6df..cf797f34e3b410 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -19,6 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if not dev.has_light_control and not dev.has_socket_control and not dev.has_blind_control + and not dev.has_signal_repeater_control ) if devices: async_add_entities(TradfriSensor(device, api, gateway_id) for device in devices) From 32a024c6412df1f45facd5a8afd8cd8dd2f1d596 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 25 Oct 2019 01:41:13 +0200 Subject: [PATCH 1255/3953] Partially revert tensorflow import move (#28184) * Revert "Refactor imports for tensorflow (#27617)" This reverts commit 5a83a92390e8a3255885198c80622556f886b9b3. * move only some imports to top * fix lint * add comments --- .../components/tensorflow/image_processing.py | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 1f49888cb95252..ea73d52fe4a6fe 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -1,23 +1,12 @@ """Support for performing TensorFlow classification on images.""" +import io import logging import os import sys -import io -import voluptuous as vol + from PIL import Image, ImageDraw import numpy as np - -try: - import cv2 -except ImportError: - cv2 = None - -try: - # Verify that the TensorFlow Object Detection API is pre-installed - import tensorflow as tf # noqa - from object_detection.utils import label_map_util # noqa -except ImportError: - label_map_util = None +import voluptuous as vol from homeassistant.components.image_processing import ( CONF_CONFIDENCE, @@ -98,8 +87,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # append custom model path to sys.path sys.path.append(model_dir) - os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" - if label_map_util is None: + try: + # Verify that the TensorFlow Object Detection API is pre-installed + # pylint: disable=unused-import,unused-variable + os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" + # These imports shouldn't be moved to the top, because they depend on code from the model_dir. + # (The model_dir is created during the manual setup process. See integration docs.) + import tensorflow as tf # noqa + from object_detection.utils import label_map_util # noqa + except ImportError: + # pylint: disable=line-too-long _LOGGER.error( "No TensorFlow Object Detection library found! Install or compile " "for your system following instructions here: " @@ -107,7 +104,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) # noqa return - if cv2 is None: + try: + # Display warning that PIL will be used if no OpenCV is found. + # pylint: disable=unused-import,unused-variable + import cv2 # noqa + except ImportError: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " "PIL at reduced resolution" @@ -282,7 +283,13 @@ def _save_image(self, image, matches, paths): def process_image(self, image): """Process the image.""" - if cv2 is None: + try: + import cv2 # pylint: disable=import-error + + img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) + inp = img[:, :, [2, 1, 0]] # BGR->RGB + inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) + except ImportError: img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") img.thumbnail((460, 460), Image.ANTIALIAS) img_width, img_height = img.size @@ -292,10 +299,6 @@ def process_image(self, image): .astype(np.uint8) ) inp_expanded = np.expand_dims(inp, axis=0) - else: - img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) - inp = img[:, :, [2, 1, 0]] # BGR->RGB - inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) image_tensor = self._graph.get_tensor_by_name("image_tensor:0") boxes = self._graph.get_tensor_by_name("detection_boxes:0") From 643b3a98ee68757cda35cada5acfc4002c380b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 25 Oct 2019 02:42:54 +0300 Subject: [PATCH 1256/3953] Huawei LTE sensor metadata update (#28187) --- homeassistant/components/huawei_lte/const.py | 3 ++ homeassistant/components/huawei_lte/sensor.py | 47 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 77126b61c22680..18b8d1a90e13f9 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -7,6 +7,9 @@ UPDATE_SIGNAL = f"{DOMAIN}_update" UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" +UNIT_BYTES = "B" +UNIT_SECONDS = "s" + KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index e5b65c723f043b..99170d4e7c0bb7 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -19,6 +19,8 @@ KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_MONITORING_TRAFFIC_STATISTICS, + UNIT_BYTES, + UNIT_SECONDS, ) @@ -29,7 +31,6 @@ KEY_DEVICE_INFORMATION: dict( include=re.compile(r"^WanIP.*Address$", re.IGNORECASE) ), - (KEY_DEVICE_INFORMATION, "SoftwareVersion"): dict(name="Software version"), (KEY_DEVICE_INFORMATION, "WanIPAddress"): dict( name="WAN IP address", icon="mdi:ip", enabled_default=True ), @@ -38,7 +39,7 @@ ), (KEY_DEVICE_SIGNAL, "band"): dict(name="Band"), (KEY_DEVICE_SIGNAL, "cell_id"): dict(name="Cell ID"), - (KEY_DEVICE_SIGNAL, "lac"): dict(name="LAC"), + (KEY_DEVICE_SIGNAL, "lac"): dict(name="LAC", icon="mdi:map-marker"), (KEY_DEVICE_SIGNAL, "mode"): dict( name="Mode", formatter=lambda x: ({"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), None), @@ -96,9 +97,51 @@ or "mdi:signal-cellular-3", enabled_default=True, ), + (KEY_DEVICE_SIGNAL, "rscp"): dict( + name="RSCP", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + # https://wiki.teltonika.lt/view/RSCP + icon=lambda x: (x is None or x < -95) + and "mdi:signal-cellular-outline" + or x < -85 + and "mdi:signal-cellular-1" + or x < -75 + and "mdi:signal-cellular-2" + or "mdi:signal-cellular-3", + ), + (KEY_DEVICE_SIGNAL, "ecio"): dict( + name="EC/IO", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + # https://wiki.teltonika.lt/view/EC/IO + icon=lambda x: (x is None or x < -20) + and "mdi:signal-cellular-outline" + or x < -10 + and "mdi:signal-cellular-1" + or x < -6 + and "mdi:signal-cellular-2" + or "mdi:signal-cellular-3", + ), KEY_MONITORING_TRAFFIC_STATISTICS: dict( exclude=re.compile(r"^showtraffic$", re.IGNORECASE) ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentConnectTime"): dict( + name="Current connection duration", unit=UNIT_SECONDS, icon="mdi:timer" + ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownload"): dict( + name="Current connection download", unit=UNIT_BYTES, icon="mdi:download" + ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentUpload"): dict( + name="Current connection upload", unit=UNIT_BYTES, icon="mdi:upload" + ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): dict( + name="Total connected duration", unit=UNIT_SECONDS, icon="mdi:timer" + ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): dict( + name="Total download", unit=UNIT_BYTES, icon="mdi:download" + ), + (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalUpload"): dict( + name="Total upload", unit=UNIT_BYTES, icon="mdi:upload" + ), } From 95295791bd4529d0e9435e03a46f4091e884fc04 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 25 Oct 2019 00:32:15 +0000 Subject: [PATCH 1257/3953] [ci skip] Translation update --- .../components/abode/.translations/pt-BR.json | 9 +++++ .../.translations/pt-BR.json | 4 +++ .../components/axis/.translations/pt-BR.json | 1 + .../binary_sensor/.translations/hu.json | 10 +++--- .../binary_sensor/.translations/ru.json | 4 +-- .../cert_expiry/.translations/zh-Hant.json | 4 ++- .../coolmaster/.translations/ca.json | 12 +++++++ .../coolmaster/.translations/da.json | 15 ++++++++ .../coolmaster/.translations/fr.json | 23 ++++++++++++ .../coolmaster/.translations/no.json | 23 ++++++++++++ .../coolmaster/.translations/pt-BR.json | 17 +++++++++ .../coolmaster/.translations/zh-Hant.json | 23 ++++++++++++ .../components/deconz/.translations/ca.json | 2 +- .../dialogflow/.translations/ru.json | 2 +- .../components/geofency/.translations/ru.json | 2 +- .../components/glances/.translations/ca.json | 2 +- .../glances/.translations/pt-BR.json | 20 +++++++++++ .../gpslogger/.translations/ru.json | 2 +- .../huawei_lte/.translations/ca.json | 28 +++++++++++++++ .../components/ifttt/.translations/ru.json | 2 +- .../components/locative/.translations/ru.json | 2 +- .../components/lock/.translations/pt-BR.json | 9 +++++ .../components/mailgun/.translations/ru.json | 2 +- .../mobile_app/.translations/ru.json | 2 +- .../components/nest/.translations/ru.json | 2 +- .../opentherm_gw/.translations/pt-BR.json | 11 ++++++ .../owntracks/.translations/ru.json | 2 +- .../components/plaato/.translations/ru.json | 2 +- .../components/plex/.translations/ca.json | 2 +- .../components/ps4/.translations/ru.json | 2 +- .../components/sensor/.translations/ru.json | 11 ++++++ .../sensor/.translations/zh-Hant.json | 36 +++++++++---------- .../solarlog/.translations/zh-Hant.json | 21 +++++++++++ .../components/soma/.translations/ru.json | 2 +- .../components/somfy/.translations/ru.json | 2 +- .../components/traccar/.translations/ru.json | 2 +- .../transmission/.translations/ca.json | 5 ++- .../transmission/.translations/da.json | 5 ++- .../transmission/.translations/fr.json | 5 ++- .../transmission/.translations/no.json | 5 ++- .../transmission/.translations/pt-BR.json | 5 ++- .../transmission/.translations/zh-Hant.json | 5 ++- .../components/twilio/.translations/ru.json | 2 +- .../components/upnp/.translations/ca.json | 2 +- .../components/withings/.translations/ca.json | 6 ++++ .../components/withings/.translations/en.json | 7 ++++ .../components/zwave/.translations/ru.json | 2 +- 47 files changed, 313 insertions(+), 51 deletions(-) create mode 100644 homeassistant/components/coolmaster/.translations/ca.json create mode 100644 homeassistant/components/coolmaster/.translations/da.json create mode 100644 homeassistant/components/coolmaster/.translations/fr.json create mode 100644 homeassistant/components/coolmaster/.translations/no.json create mode 100644 homeassistant/components/coolmaster/.translations/pt-BR.json create mode 100644 homeassistant/components/coolmaster/.translations/zh-Hant.json create mode 100644 homeassistant/components/glances/.translations/pt-BR.json create mode 100644 homeassistant/components/huawei_lte/.translations/ca.json create mode 100644 homeassistant/components/lock/.translations/pt-BR.json create mode 100644 homeassistant/components/opentherm_gw/.translations/pt-BR.json create mode 100644 homeassistant/components/solarlog/.translations/zh-Hant.json diff --git a/homeassistant/components/abode/.translations/pt-BR.json b/homeassistant/components/abode/.translations/pt-BR.json index 7a117a819931a2..30980103b38cbd 100644 --- a/homeassistant/components/abode/.translations/pt-BR.json +++ b/homeassistant/components/abode/.translations/pt-BR.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "single_instance_allowed": "Somente uma \u00fanica configura\u00e7\u00e3o de Abode \u00e9 permitida." + }, + "error": { + "connection_error": "N\u00e3o foi poss\u00edvel conectar ao Abode.", + "identifier_exists": "Conta j\u00e1 cadastrada.", + "invalid_credentials": "Credenciais inv\u00e1lidas." + }, "step": { "user": { "data": { + "password": "Senha", "username": "Endere\u00e7o de e-mail" } } diff --git a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json index 1f7c994330d4fe..156ede8851bc33 100644 --- a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json +++ b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json @@ -1,6 +1,10 @@ { "device_automation": { "action_type": { + "arm_away": "Armar {entity_name} longe", + "arm_home": "Armar {entity_name} casa", + "arm_night": "Armar {entity_name} noite", + "disarm": "Desarmar {entity_name}", "trigger": "Disparar {entidade_nome}" } } diff --git a/homeassistant/components/axis/.translations/pt-BR.json b/homeassistant/components/axis/.translations/pt-BR.json index 0b8fe8541daeb5..453c8fa364304a 100644 --- a/homeassistant/components/axis/.translations/pt-BR.json +++ b/homeassistant/components/axis/.translations/pt-BR.json @@ -12,6 +12,7 @@ "device_unavailable": "O dispositivo n\u00e3o est\u00e1 dispon\u00edvel", "faulty_credentials": "Credenciais do usu\u00e1rio inv\u00e1lidas" }, + "flow_title": "Eixos do dispositivo: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/.translations/hu.json b/homeassistant/components/binary_sensor/.translations/hu.json index e53d918f98d28e..7ec9b5268e2fff 100644 --- a/homeassistant/components/binary_sensor/.translations/hu.json +++ b/homeassistant/components/binary_sensor/.translations/hu.json @@ -46,13 +46,14 @@ }, "trigger_type": { "bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", - "closed": "{entity_name} be lett z\u00e1rva", + "closed": "{entity_name} be lett csukva", "cold": "{entity_name} hideg lett", - "connected": "{entity_name} csatlakozott", + "connected": "{entity_name} csatlakozik", "gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", - "hot": "{entity_name} felforr\u00f3sodott", + "hot": "{entity_name} felforr\u00f3sodik", "light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", "locked": "{entity_name} be lett z\u00e1rva", + "moist": "{entity_name} nedves lett", "moist\u00a7": "{entity_name} nedves lett", "motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", "moving": "{entity_name} mozog", @@ -65,12 +66,13 @@ "no_vibration": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel rezg\u00e9st", "not_bat_low": "{entity_name} akkufesz\u00fclts\u00e9g megfelel\u0151", "not_cold": "{entity_name} m\u00e1r nem hideg", - "not_connected": "{entity_name} lecsatlakozott", + "not_connected": "{entity_name} lecsatlakozik", "not_hot": "{entity_name} m\u00e1r nem forr\u00f3", "not_locked": "{entity_name} ki lett nyitva", "not_moist": "{entity_name} sz\u00e1raz lett", "not_moving": "{entity_name} m\u00e1r nem mozog", "not_occupied": "{entity_name} m\u00e1r nem foglalt", + "not_opened": "{entity_name} be lett csukva", "not_plugged_in": "{entity_name} m\u00e1r nincs csatlakoztatva", "not_powered": "{entity_name} m\u00e1r nincs fesz\u00fcts\u00e9g alatt", "not_present": "{entity_name} m\u00e1r nincs jelen", diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json index cce765c8d8444f..4c9cfb99a1cc35 100644 --- a/homeassistant/components/binary_sensor/.translations/ru.json +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -28,7 +28,7 @@ "is_not_occupied": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_not_plugged_in": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", + "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0438\u0442\u0430\u043d\u0438\u0435", "is_not_present": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_occupied": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", @@ -36,7 +36,7 @@ "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_plugged_in": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", + "is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0438\u0442\u0430\u043d\u0438\u0435", "is_present": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hant.json b/homeassistant/components/cert_expiry/.translations/zh-Hant.json index 9af730db969f12..c710deae5c1eab 100644 --- a/homeassistant/components/cert_expiry/.translations/zh-Hant.json +++ b/homeassistant/components/cert_expiry/.translations/zh-Hant.json @@ -4,10 +4,12 @@ "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { + "certificate_error": "\u8a8d\u8b49\u7121\u6cd5\u78ba\u8a8d", "certificate_fetch_failed": "\u7121\u6cd5\u81ea\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u7372\u5f97\u8a8d\u8b49", "connection_timeout": "\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef\u903e\u6642", "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "resolve_failed": "\u4e3b\u6a5f\u7aef\u7121\u6cd5\u89e3\u6790" + "resolve_failed": "\u4e3b\u6a5f\u7aef\u7121\u6cd5\u89e3\u6790", + "wrong_host": "\u8a8d\u8b49\u8207\u4e3b\u6a5f\u540d\u7a31\u4e0d\u7b26\u5408" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/ca.json b/homeassistant/components/coolmaster/.translations/ca.json new file mode 100644 index 00000000000000..c79397b0cc5ce4 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/ca.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/da.json b/homeassistant/components/coolmaster/.translations/da.json new file mode 100644 index 00000000000000..8f50a0eb6ddf79 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "heat_cool": "Underst\u00f8t automatisk varm/k\u00f8l tilstand", + "host": "V\u00e6rt", + "off": "Kan slukkes" + }, + "title": "Ops\u00e6t dine CoolMasterNet-forbindelsesdetaljer." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/fr.json b/homeassistant/components/coolmaster/.translations/fr.json new file mode 100644 index 00000000000000..97b1753ddded87 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "\u00c9chec de la connexion \u00e0 l'instance CoolMasterNet. S'il vous pla\u00eet v\u00e9rifier votre h\u00f4te.", + "no_units": "Impossible de trouver des unit\u00e9s HVAC dans l'h\u00f4te CoolMasterNet." + }, + "step": { + "user": { + "data": { + "cool": "Prise en charge du mode refroidissement", + "dry": "Prise en charge du mode d\u00e9shumidification", + "fan_only": "Prise en charge du mode ventilateur uniquement", + "heat": "Prise en charge du mode chauffage", + "heat_cool": "Prise en charge du mode chauffage / refroidissement automatique", + "host": "H\u00f4te", + "off": "Peut \u00eatre \u00e9teint" + }, + "title": "Configurez les d\u00e9tails de votre connexion CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/no.json b/homeassistant/components/coolmaster/.translations/no.json new file mode 100644 index 00000000000000..90c40aaa617c5d --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Kunne ikke koble til CoolMasterNet-forekomsten. Sjekk verten din.", + "no_units": "Kunne ikke finne noen HVAC-enheter i CoolMasterNet vert." + }, + "step": { + "user": { + "data": { + "cool": "St\u00f8tte kj\u00f8lemodus", + "dry": "St\u00f8tt t\u00f8rr modus", + "fan_only": "St\u00f8tt kun modus for vifte", + "heat": "St\u00f8tt varmemodus", + "heat_cool": "St\u00f8tter automatisk varme/kj\u00f8l-modus", + "host": "Vert", + "off": "Kan sl\u00e5s av" + }, + "title": "Konfigurer informasjonen om CoolMasterNet-tilkoblingen." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/pt-BR.json b/homeassistant/components/coolmaster/.translations/pt-BR.json new file mode 100644 index 00000000000000..bb821341818ed9 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cool": "Suporta o modo de resfriamento", + "dry": "Suporta o modo seco", + "fan_only": "Suporte apenas o modo ventilador", + "heat": "Suporta o modo de aquecimento", + "heat_cool": "Suporta o modo de aquecimento/resfriamento autom\u00e1tico", + "host": "Host", + "off": "Pode ser desligado" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/zh-Hant.json b/homeassistant/components/coolmaster/.translations/zh-Hant.json new file mode 100644 index 00000000000000..bc61e82b98ac1a --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "\u9023\u7dda\u81f3 CoolMasterNet \u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u4e3b\u6a5f\u7aef\u3002", + "no_units": "\u7121\u6cd5\u65bc CoolMasterNet \u4e3b\u6a5f\u627e\u5230\u4efb\u4f55 HVAC \u8a2d\u5099\u3002" + }, + "step": { + "user": { + "data": { + "cool": "\u652f\u63f4\u5236\u51b7\u6a21\u5f0f", + "dry": "\u652f\u63f4\u9664\u6fd5\u6a21\u5f0f", + "fan_only": "\u652f\u63f4\u50c5\u9001\u98a8\u6a21\u5f0f", + "heat": "\u652f\u63f4\u4fdd\u6696\u6a21\u5f0f", + "heat_cool": "\u652f\u63f4\u81ea\u52d5\u4fdd\u6696/\u5236\u51b7\u6a21\u5f0f", + "host": "\u4e3b\u6a5f\u7aef", + "off": "\u53ef\u4ee5\u95dc\u9589" + }, + "title": "\u8a2d\u5b9a CoolMasterNet \u9023\u7dda\u8cc7\u8a0a\u3002" + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index a2facf0d7c2e3a..41f257e2a787f9 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -37,7 +37,7 @@ "allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals", "allow_deconz_groups": "Permetre la importaci\u00f3 de grups deCONZ" }, - "title": "Opcions de configuraci\u00f3 addicionals per deCONZ" + "title": "Opcions de configuraci\u00f3 addicionals de deCONZ" } }, "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee" diff --git a/homeassistant/components/dialogflow/.translations/ru.json b/homeassistant/components/dialogflow/.translations/ru.json index d8b7db09a78c62..884053288962eb 100644 --- a/homeassistant/components/dialogflow/.translations/ru.json +++ b/homeassistant/components/dialogflow/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [webhooks \u0434\u043b\u044f Dialogflow]({dialogflow_url}).\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Dialogflow]({dialogflow_url}).\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/geofency/.translations/ru.json b/homeassistant/components/geofency/.translations/ru.json index 6c699d21ce67e9..3663ff0114cea4 100644 --- a/homeassistant/components/geofency/.translations/ru.json +++ b/homeassistant/components/geofency/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Geofency.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Geofency.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/glances/.translations/ca.json b/homeassistant/components/glances/.translations/ca.json index edff236623e00f..2610fe156aab10 100644 --- a/homeassistant/components/glances/.translations/ca.json +++ b/homeassistant/components/glances/.translations/ca.json @@ -30,7 +30,7 @@ "data": { "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" }, - "description": "Opcions de configuraci\u00f3 per a Glances" + "description": "Opcions de configuraci\u00f3 de Glances" } } } diff --git a/homeassistant/components/glances/.translations/pt-BR.json b/homeassistant/components/glances/.translations/pt-BR.json new file mode 100644 index 00000000000000..05ea657c8b3037 --- /dev/null +++ b/homeassistant/components/glances/.translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Nome de usu\u00e1rio" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/ru.json b/homeassistant/components/gpslogger/.translations/ru.json index 366cb1735d59c6..b33bde95aec7fe 100644 --- a/homeassistant/components/gpslogger/.translations/ru.json +++ b/homeassistant/components/gpslogger/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f GPSLogger.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f GPSLogger.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json new file mode 100644 index 00000000000000..5b31875586dfb7 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/ca.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest dispositiu ja est\u00e0 configurat" + }, + "error": { + "connection_failed": "La connexi\u00f3 ha fallat", + "incorrect_password": "Contrasenya incorrecta", + "incorrect_username": "Nom d'usuari incorrecte", + "incorrect_username_or_password": "Nom d'usuari o contrasenya incorrectes", + "invalid_url": "URL inv\u00e0lid", + "login_attempts_exceeded": "Nombre m\u00e0xim d'intents d'inici de sessi\u00f3 superat, torna-ho a provar m\u00e9s tard", + "response_error": "S'ha produ\u00eft un error desconegut del dispositiu", + "unknown_connection_error": "S'ha produ\u00eft un error desconegut en connectar-se al dispositiu" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "url": "URL", + "username": "Nom d'usuari" + }, + "title": "Con de Huawei LTE" + } + }, + "title": "Huawei LTE" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ru.json b/homeassistant/components/ifttt/.translations/ru.json index ae5fdbab3f66c4..128db247150e26 100644 --- a/homeassistant/components/ifttt/.translations/ru.json +++ b/homeassistant/components/ifttt/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"Make a web request\" \u0438\u0437 [IFTTT Webhook applet]({applet_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \"Make a web request\" \u0438\u0437 [IFTTT Webhook applet]({applet_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/locative/.translations/ru.json b/homeassistant/components/locative/.translations/ru.json index 70f08595f3a23d..90fa5253a61fe7 100644 --- a/homeassistant/components/locative/.translations/ru.json +++ b/homeassistant/components/locative/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Locative.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Locative.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/lock/.translations/pt-BR.json b/homeassistant/components/lock/.translations/pt-BR.json new file mode 100644 index 00000000000000..f6bde89a3a645f --- /dev/null +++ b/homeassistant/components/lock/.translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "action_type": { + "lock": "Bloquear {entity_name}", + "open": "Abrir {entity_name}", + "unlock": "Desbloquear {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ru.json b/homeassistant/components/mailgun/.translations/ru.json index 094940e6f905e6..9e8a293b4bf9b6 100644 --- a/homeassistant/components/mailgun/.translations/ru.json +++ b/homeassistant/components/mailgun/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Mailgun]({mailgun_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/mobile_app/.translations/ru.json b/homeassistant/components/mobile_app/.translations/ru.json index 202b73832531d9..28a497ef2195bb 100644 --- a/homeassistant/components/mobile_app/.translations/ru.json +++ b/homeassistant/components/mobile_app/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "\u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0441 Home Assistant. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({apps_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u044b\u0445 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439." + "install_app": "\u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0441 Home Assistant. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({apps_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043f\u0438\u0441\u043a\u0430 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u044b\u0445 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439." }, "step": { "confirm": { diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index ba49b788b9a639..9abdafafa936be 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -22,7 +22,7 @@ }, "link": { "data": { - "code": "\u041f\u0438\u043d-\u043a\u043e\u0434" + "code": "PIN-\u043a\u043e\u0434" }, "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest. \n\n \u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u043f\u0438\u043d-\u043a\u043e\u0434.", "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Nest" diff --git a/homeassistant/components/opentherm_gw/.translations/pt-BR.json b/homeassistant/components/opentherm_gw/.translations/pt-BR.json new file mode 100644 index 00000000000000..96907fd4cdc31f --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "precision": "Precis\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/ru.json b/homeassistant/components/owntracks/.translations/ru.json index 31c3e77279dcfe..0e9479c1ed4db8 100644 --- a/homeassistant/components/owntracks/.translations/ru.json +++ b/homeassistant/components/owntracks/.translations/ru.json @@ -4,7 +4,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/ru.json b/homeassistant/components/plaato/.translations/ru.json index dc06e3ddab0668..fc1255ed9ce90e 100644 --- a/homeassistant/components/plaato/.translations/ru.json +++ b/homeassistant/components/plaato/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 7a8cf7a1424e84..9eb6d16639f4e1 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -55,7 +55,7 @@ "show_all_controls": "Mostra tots els controls", "use_episode_art": "Utilitza imatges de l'episodi" }, - "description": "Opcions per als reproductors multim\u00e8dia Plex" + "description": "Opcions dels reproductors multim\u00e8dia Plex" } } } diff --git a/homeassistant/components/ps4/.translations/ru.json b/homeassistant/components/ps4/.translations/ru.json index b50d4bb838f1ff..bf2484d02543f2 100644 --- a/homeassistant/components/ps4/.translations/ru.json +++ b/homeassistant/components/ps4/.translations/ru.json @@ -25,7 +25,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, - "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/components/ps4/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", + "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/sensor/.translations/ru.json b/homeassistant/components/sensor/.translations/ru.json index 8c70f41fcb71ae..0f2c1bc0e4e31c 100644 --- a/homeassistant/components/sensor/.translations/ru.json +++ b/homeassistant/components/sensor/.translations/ru.json @@ -1,5 +1,16 @@ { "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_humidity": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_illuminance": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_power": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_pressure": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_signal_strength": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_temperature": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_timestamp": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "is_value": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0435\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" + }, "trigger_type": { "battery_level": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", diff --git a/homeassistant/components/sensor/.translations/zh-Hant.json b/homeassistant/components/sensor/.translations/zh-Hant.json index af97681ee764d1..eb8f47a1fd9463 100644 --- a/homeassistant/components/sensor/.translations/zh-Hant.json +++ b/homeassistant/components/sensor/.translations/zh-Hant.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} \u96fb\u91cf", - "is_humidity": "{entity_name} \u6fd5\u5ea6", - "is_illuminance": "{entity_name} \u7167\u5ea6", - "is_power": "{entity_name} \u96fb\u529b", - "is_pressure": "{entity_name} \u58d3\u529b", - "is_signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", - "is_temperature": "{entity_name} \u6eab\u5ea6", - "is_timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", - "is_value": "{entity_name} \u503c" + "is_battery_level": "\u76ee\u524d {entity_name} \u96fb\u91cf", + "is_humidity": "\u76ee\u524d {entity_name} \u6fd5\u5ea6", + "is_illuminance": "\u76ee\u524d {entity_name} \u7167\u5ea6", + "is_power": "\u76ee\u524d {entity_name} \u96fb\u529b", + "is_pressure": "\u76ee\u524d {entity_name} \u58d3\u529b", + "is_signal_strength": "\u76ee\u524d {entity_name} \u8a0a\u865f\u5f37\u5ea6", + "is_temperature": "\u76ee\u524d {entity_name} \u6eab\u5ea6", + "is_timestamp": "\u76ee\u524d {entity_name} \u6642\u9593\u6a19\u8a18", + "is_value": "\u76ee\u524d {entity_name} \u503c" }, "trigger_type": { - "battery_level": "{entity_name} \u96fb\u91cf", - "humidity": "{entity_name} \u6fd5\u5ea6", - "illuminance": "{entity_name} \u7167\u5ea6", - "power": "{entity_name} \u96fb\u529b", - "pressure": "{entity_name} \u58d3\u529b", - "signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", - "temperature": "{entity_name} \u6eab\u5ea6", - "timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", - "value": "{entity_name} \u503c" + "battery_level": "{entity_name} \u96fb\u91cf\u8b8a\u66f4", + "humidity": "{entity_name} \u6fd5\u5ea6\u8b8a\u66f4", + "illuminance": "{entity_name} \u7167\u5ea6\u8b8a\u66f4", + "power": "{entity_name} \u96fb\u529b\u8b8a\u66f4", + "pressure": "{entity_name} \u58d3\u529b\u8b8a\u66f4", + "signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6\u8b8a\u66f4", + "temperature": "{entity_name} \u6eab\u5ea6\u8b8a\u66f4", + "timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18\u8b8a\u66f4", + "value": "{entity_name} \u503c\u8b8a\u66f4" } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/zh-Hant.json b/homeassistant/components/solarlog/.translations/zh-Hant.json new file mode 100644 index 00000000000000..19ec431d2cade9 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u78ba\u8a8d\u4e3b\u6a5f\u4f4d\u5740" + }, + "step": { + "user": { + "data": { + "host": "Solar-Log \u8a2d\u5099\u4e4b\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "name": "Solar-Log \u50b3\u611f\u5668\u6240\u4f7f\u7528\u5b57\u9996" + }, + "title": "\u5b9a\u7fa9 Solar-Log \u9023\u7dda" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json index f7e6574b113854..f28d672d3f28c3 100644 --- a/homeassistant/components/soma/.translations/ru.json +++ b/homeassistant/components/soma/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "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.", - "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439." + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/somfy/.translations/ru.json b/homeassistant/components/somfy/.translations/ru.json index 7251bc990e9f46..0c8778dc2e5afb 100644 --- a/homeassistant/components/somfy/.translations/ru.json +++ b/homeassistant/components/somfy/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "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.", - "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Somfy \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439." + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Somfy \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/traccar/.translations/ru.json b/homeassistant/components/traccar/.translations/ru.json index afaab87efe4e8e..1a215c90d4b30e 100644 --- a/homeassistant/components/traccar/.translations/ru.json +++ b/homeassistant/components/traccar/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Traccar.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f Traccar.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/transmission/.translations/ca.json b/homeassistant/components/transmission/.translations/ca.json index 395f6e2d6810c9..f621574683fc2d 100644 --- a/homeassistant/components/transmission/.translations/ca.json +++ b/homeassistant/components/transmission/.translations/ca.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat.", "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "error": { "cannot_connect": "No s'ha pogut connectar amb l'amfitri\u00f3", + "name_exists": "El nom ja existeix", "wrong_credentials": "Nom d'usuari o contrasenya incorrectes" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" }, - "description": "Opcions de configuraci\u00f3 per a Transmission" + "description": "Opcions de configuraci\u00f3 de Transmission", + "title": "Opcions de configuraci\u00f3 de Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/da.json b/homeassistant/components/transmission/.translations/da.json index 3a619d8154af21..b14fca00c2cdd0 100644 --- a/homeassistant/components/transmission/.translations/da.json +++ b/homeassistant/components/transmission/.translations/da.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "V\u00e6rten er allerede konfigureret.", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." }, "error": { "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", + "name_exists": "Navnet findes allerede", "wrong_credentials": "Ugyldigt brugernavn eller adgangskode" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Opdateringsfrekvens" }, - "description": "Konfigurationsindstillinger for Transmission" + "description": "Konfigurationsindstillinger for Transmission", + "title": "Konfigurationsindstillinger for Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/fr.json b/homeassistant/components/transmission/.translations/fr.json index e2360c016ca304..3c267b36a088d8 100644 --- a/homeassistant/components/transmission/.translations/fr.json +++ b/homeassistant/components/transmission/.translations/fr.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9.", "one_instance_allowed": "Une seule instance est n\u00e9cessaire." }, "error": { "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te", + "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9", "wrong_credentials": "Mauvais nom d'utilisateur ou mot de passe" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" }, - "description": "Configurer les options pour Transmission" + "description": "Configurer les options pour Transmission", + "title": "Configurer les options pour Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/no.json b/homeassistant/components/transmission/.translations/no.json index 94044e692d954e..9cd19cd87f8d93 100644 --- a/homeassistant/components/transmission/.translations/no.json +++ b/homeassistant/components/transmission/.translations/no.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Verten er allerede konfigurert.", "one_instance_allowed": "Bare en enkel instans er n\u00f8dvendig." }, "error": { "cannot_connect": "Kan ikke koble til vert", + "name_exists": "Navnet eksisterer allerede", "wrong_credentials": "Ugyldig brukernavn eller passord" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Oppdater frekvens" }, - "description": "Konfigurer alternativer for Transmission" + "description": "Konfigurer alternativer for Transmission", + "title": "Konfigurer alternativer for Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/pt-BR.json b/homeassistant/components/transmission/.translations/pt-BR.json index cabbb6d91494c1..de854e1273c8c9 100644 --- a/homeassistant/components/transmission/.translations/pt-BR.json +++ b/homeassistant/components/transmission/.translations/pt-BR.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "O host j\u00e1 est\u00e1 configurado.", "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." }, "error": { "cannot_connect": "N\u00e3o foi poss\u00edvel conectar ao host", + "name_exists": "O nome j\u00e1 existe", "wrong_credentials": "Nome de usu\u00e1rio ou senha incorretos" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" }, - "description": "Configurar op\u00e7\u00f5es para transmiss\u00e3o" + "description": "Configurar op\u00e7\u00f5es para transmiss\u00e3o", + "title": "Configurar op\u00e7\u00f5es para Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/zh-Hant.json b/homeassistant/components/transmission/.translations/zh-Hant.json index 479e25c6d8a124..304babc991ef0b 100644 --- a/homeassistant/components/transmission/.translations/zh-Hant.json +++ b/homeassistant/components/transmission/.translations/zh-Hant.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "error": { "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef", + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", "wrong_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "\u66f4\u65b0\u983b\u7387" }, - "description": "Transmission \u8a2d\u5b9a\u9078\u9805" + "description": "Transmission \u8a2d\u5b9a\u9078\u9805", + "title": "Transmission \u8a2d\u5b9a\u9078\u9805" } } } diff --git a/homeassistant/components/twilio/.translations/ru.json b/homeassistant/components/twilio/.translations/ru.json index 1c4e0653496ab4..ba8ed6179d4b0d 100644 --- a/homeassistant/components/twilio/.translations/ru.json +++ b/homeassistant/components/twilio/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c [Webhooks \u0434\u043b\u044f Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Webhook \u0434\u043b\u044f [Twilio]({twilio_url}).\n\n\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0439 \u043f\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043f\u043e\u0441\u0442\u0443\u043f\u0430\u044e\u0449\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445." }, "step": { "user": { diff --git a/homeassistant/components/upnp/.translations/ca.json b/homeassistant/components/upnp/.translations/ca.json index 28ad9ce954d995..85370eec8e6ce4 100644 --- a/homeassistant/components/upnp/.translations/ca.json +++ b/homeassistant/components/upnp/.translations/ca.json @@ -26,7 +26,7 @@ "enable_sensors": "Afegeix sensors de tr\u00e0nsit", "igd": "UPnP/IGD" }, - "title": "Opcions de configuraci\u00f3 per a UPnP/IGD" + "title": "Opcions de configuraci\u00f3 d'UPnP/IGD" } }, "title": "UPnP/IGD" diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json index 2f2fdbe9b3f742..89dae4caa1b8f9 100644 --- a/homeassistant/components/withings/.translations/ca.json +++ b/homeassistant/components/withings/.translations/ca.json @@ -7,6 +7,12 @@ "default": "Autenticaci\u00f3 exitosa amb Withings per al perfil seleccionat." }, "step": { + "profile": { + "data": { + "profile": "Perfil" + }, + "title": "Perfil d'usuari." + }, "user": { "data": { "profile": "Perfil" diff --git a/homeassistant/components/withings/.translations/en.json b/homeassistant/components/withings/.translations/en.json index 16ce491e776d02..987e3347a99e9c 100644 --- a/homeassistant/components/withings/.translations/en.json +++ b/homeassistant/components/withings/.translations/en.json @@ -7,6 +7,13 @@ "default": "Successfully authenticated with Withings for the selected profile." }, "step": { + "profile": { + "data": { + "profile": "Profile" + }, + "description": "Which profile did you select on the Withings website? It's important the profiles match, otherwise data will be mis-labeled.", + "title": "User Profile." + }, "user": { "data": { "profile": "Profile" diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json index 4243f583082818..a1039c2dedc4a5 100644 --- a/homeassistant/components/zwave/.translations/ru.json +++ b/homeassistant/components/zwave/.translations/ru.json @@ -13,7 +13,7 @@ "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", "title": "Z-Wave" } }, From 9661efc31275280540940a5700703778c9d5f3ae Mon Sep 17 00:00:00 2001 From: escoand Date: Fri, 25 Oct 2019 14:32:12 +0200 Subject: [PATCH 1258/3953] Add Samsung TV automatic protocol detection (#27492) * added automatic protocol detection * fix logger tests * fix async tests * add missin const.py * fix log formatting * wait for first update call * migrate first tests * migrated all test functions * started to use state machine * updated all tests to use async_setup_component * slove hints * update tests * get state at correct position * remove impossible tests * fix autodetect tests * use caplog fixture * add test for duplicate * catch concrete exceptions * don't mock samsungctl exceptions * add test for discovery * get state when possible * add test for autodetect without connection --- CODEOWNERS | 1 + .../components/samsungtv/__init__.py | 2 +- homeassistant/components/samsungtv/const.py | 5 + .../components/samsungtv/manifest.json | 4 +- .../components/samsungtv/media_player.py | 76 +- .../components/samsungtv/test_media_player.py | 956 ++++++++++++------ 6 files changed, 700 insertions(+), 344 deletions(-) create mode 100644 homeassistant/components/samsungtv/const.py diff --git a/CODEOWNERS b/CODEOWNERS index 809101a5271ec0..40e37ec46978fb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -247,6 +247,7 @@ homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt homeassistant/components/saj/* @fredericvl +homeassistant/components/samsungtv/* @escoand homeassistant/components/scene/* @home-assistant/core homeassistant/components/scrape/* @fabaff homeassistant/components/script/* @home-assistant/core diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index e43ea1ba984902..6b4f0e31f022a9 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -1 +1 @@ -"""The samsungtv component.""" +"""The Samsung TV integration.""" diff --git a/homeassistant/components/samsungtv/const.py b/homeassistant/components/samsungtv/const.py new file mode 100644 index 00000000000000..83d7474384499a --- /dev/null +++ b/homeassistant/components/samsungtv/const.py @@ -0,0 +1,5 @@ +"""Constants for the Samsung TV integration.""" +import logging + +LOGGER = logging.getLogger(__package__) +DOMAIN = "samsungtv" diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index a080fac112a32d..405d757cbefbbc 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -1,11 +1,11 @@ { "domain": "samsungtv", - "name": "Samsungtv", + "name": "Samsung TV", "documentation": "https://www.home-assistant.io/integrations/samsungtv", "requirements": [ "samsungctl[websocket]==0.7.1", "wakeonlan==1.1.6" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@escoand"] } diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index fd1da31497e456..94e9131ed3232f 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -1,7 +1,6 @@ """Support for interface with an Samsung TV.""" import asyncio from datetime import timedelta -import logging import socket import voluptuous as vol @@ -36,14 +35,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util -_LOGGER = logging.getLogger(__name__) +from .const import LOGGER DEFAULT_NAME = "Samsung TV Remote" -DEFAULT_PORT = 55000 DEFAULT_TIMEOUT = 1 KEY_PRESS_TIMEOUT = 1.2 KNOWN_DEVICES_KEY = "samsungtv_known_devices" +METHODS = ("websocket", "legacy") SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} SUPPORT_SAMSUNGTV = ( @@ -62,7 +61,7 @@ { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_MAC): cv.string, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, } @@ -89,15 +88,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): model = discovery_info.get("model_name") host = discovery_info.get("host") name = f"{tv_name} ({model})" - port = DEFAULT_PORT + if name.startswith("[TV]"): + name = name[4:] + port = None timeout = DEFAULT_TIMEOUT mac = None - udn = discovery_info.get("udn") - if udn and udn.startswith("uuid:"): - uuid = udn[len("uuid:") :] - else: - _LOGGER.warning("Cannot determine device") - return + uuid = discovery_info.get("udn") + if uuid and uuid.startswith("uuid:"): + uuid = uuid[len("uuid:") :] # Only add a device once, so discovered devices do not override manual # config. @@ -105,9 +103,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if ip_addr not in known_devices: known_devices.add(ip_addr) add_entities([SamsungTVDevice(host, port, name, timeout, mac, uuid)]) - _LOGGER.info("Samsung TV %s:%d added as '%s'", host, port, name) + LOGGER.info("Samsung TV %s added as '%s'", host, name) else: - _LOGGER.info("Ignoring duplicate Samsung TV %s:%d", host, port) + LOGGER.info("Ignoring duplicate Samsung TV %s", host) class SamsungTVDevice(MediaPlayerDevice): @@ -140,14 +138,16 @@ def __init__(self, host, port, name, timeout, mac, uuid): "name": "HomeAssistant", "description": name, "id": "ha.component.samsung", + "method": None, "port": port, "host": host, "timeout": timeout, } + # Select method by port number, mainly for fallback if self._config["port"] in (8001, 8002): self._config["method"] = "websocket" - else: + elif self._config["port"] == 55000: self._config["method"] = "legacy" def update(self): @@ -156,16 +156,47 @@ def update(self): def get_remote(self): """Create or return a remote control instance.""" + + # Try to find correct method automatically + if self._config["method"] not in METHODS: + for method in METHODS: + try: + self._config["method"] = method + LOGGER.debug("Try config: %s", self._config) + self._remote = self._remote_class(self._config.copy()) + self._state = STATE_ON + LOGGER.debug("Found working config: %s", self._config) + break + except ( + self._exceptions_class.UnhandledResponse, + self._exceptions_class.AccessDenied, + ): + # We got a response so it's working. + self._state = STATE_ON + LOGGER.debug( + "Found working config without connection: %s", self._config + ) + break + except OSError as err: + LOGGER.debug("Failing config: %s error was: %s", self._config, err) + self._config["method"] = None + + # Unable to find working connection + if self._config["method"] is None: + self._remote = None + self._state = None + return None + if self._remote is None: # We need to create a new instance to reconnect. - self._remote = self._remote_class(self._config) + self._remote = self._remote_class(self._config.copy()) return self._remote def send_key(self, key): """Send a key to the tv and handles exceptions.""" if self._power_off_in_progress() and key not in ("KEY_POWER", "KEY_POWEROFF"): - _LOGGER.info("TV is powering off, not sending command: %s", key) + LOGGER.info("TV is powering off, not sending command: %s", key) return try: # recreate connection if connection was dead @@ -178,6 +209,9 @@ def send_key(self, key): # BrokenPipe can occur when the commands is sent to fast self._remote = None self._state = STATE_ON + except AttributeError: + # Auto-detect could not find working config yet + pass except ( self._exceptions_class.UnhandledResponse, self._exceptions_class.AccessDenied, @@ -185,7 +219,7 @@ def send_key(self, key): # We got a response so it's on. self._state = STATE_ON self._remote = None - _LOGGER.debug("Failed sending command %s", key, exc_info=True) + LOGGER.debug("Failed sending command %s", key, exc_info=True) return except OSError: self._state = STATE_OFF @@ -249,7 +283,7 @@ def turn_off(self): self.get_remote().close() self._remote = None except OSError: - _LOGGER.debug("Could not establish connection.") + LOGGER.debug("Could not establish connection.") def volume_up(self): """Volume up the media player.""" @@ -291,14 +325,14 @@ def media_previous_track(self): async def async_play_media(self, media_type, media_id, **kwargs): """Support changing a channel.""" if media_type != MEDIA_TYPE_CHANNEL: - _LOGGER.error("Unsupported media type") + LOGGER.error("Unsupported media type") return # media_id should only be a channel number try: cv.positive_int(media_id) except vol.Invalid: - _LOGGER.error("Media ID must be positive integer") + LOGGER.error("Media ID must be positive integer") return for digit in media_id: @@ -316,7 +350,7 @@ def turn_on(self): async def async_select_source(self, source): """Select input source.""" if source not in SOURCES: - _LOGGER.error("Unsupported source") + LOGGER.error("Unsupported source") return await self.hass.async_add_job(self.send_key, SOURCES[source]) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 1428ba3b39ba77..c178710e3f944d 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1,309 +1,573 @@ """Tests for samsungtv Components.""" import asyncio -import unittest -from unittest.mock import call, patch, MagicMock - from asynctest import mock - +from datetime import timedelta import pytest +from samsungctl import exceptions +from tests.common import MockDependency, async_fire_time_changed +from unittest.mock import call, patch -import tests.common from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_VOLUME_MUTED, + DOMAIN, + SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOURCE, SUPPORT_TURN_ON, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_URL, ) +from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN from homeassistant.components.samsungtv.media_player import ( - setup_platform, CONF_TIMEOUT, - SamsungTVDevice, SUPPORT_SAMSUNGTV, ) from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, CONF_HOST, + CONF_MAC, CONF_NAME, + CONF_PLATFORM, CONF_PORT, - STATE_ON, - CONF_MAC, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_UP, STATE_OFF, + STATE_ON, + STATE_UNKNOWN, ) -from tests.common import MockDependency -from homeassistant.util import dt as dt_util -from datetime import timedelta +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + + +ENTITY_ID = f"{DOMAIN}.fake" +MOCK_CONFIG = { + DOMAIN: { + CONF_PLATFORM: SAMSUNGTV_DOMAIN, + CONF_HOST: "fake", + CONF_NAME: "fake", + CONF_PORT: 8001, + CONF_TIMEOUT: 10, + CONF_MAC: "fake", + } +} -WORKING_CONFIG = { - CONF_HOST: "fake", - CONF_NAME: "fake", - CONF_PORT: 8001, - CONF_TIMEOUT: 10, - CONF_MAC: "fake", - "uuid": None, +ENTITY_ID_NOMAC = f"{DOMAIN}.fake_nomac" +MOCK_CONFIG_NOMAC = { + DOMAIN: { + CONF_PLATFORM: SAMSUNGTV_DOMAIN, + CONF_HOST: "fake_nomac", + CONF_NAME: "fake_nomac", + CONF_PORT: 55000, + CONF_TIMEOUT: 10, + } } -DISCOVERY_INFO = {"name": "fake", "model_name": "fake", "host": "fake"} - - -class AccessDenied(Exception): - """Dummy Exception.""" - - -class ConnectionClosed(Exception): - """Dummy Exception.""" - - -class UnhandledResponse(Exception): - """Dummy Exception.""" - - -class TestSamsungTv(unittest.TestCase): - """Testing Samsungtv component.""" - - @MockDependency("samsungctl") - @MockDependency("wakeonlan") - def setUp(self, samsung_mock, wol_mock): - """Set up test environment.""" - self.hass = tests.common.get_test_home_assistant() - self.hass.start() - self.hass.block_till_done() - self.device = SamsungTVDevice(**WORKING_CONFIG) - self.device._exceptions_class = mock.Mock() - self.device._exceptions_class.UnhandledResponse = UnhandledResponse - self.device._exceptions_class.AccessDenied = AccessDenied - self.device._exceptions_class.ConnectionClosed = ConnectionClosed - - def tearDown(self): - """Tear down test data.""" - self.hass.stop() - - @MockDependency("samsungctl") - @MockDependency("wakeonlan") - def test_setup(self, samsung_mock, wol_mock): - """Testing setup of platform.""" - with mock.patch("homeassistant.components.samsungtv.media_player.socket"): - add_entities = mock.Mock() - setup_platform(self.hass, WORKING_CONFIG, add_entities) - - @MockDependency("samsungctl") - @MockDependency("wakeonlan") - def test_setup_discovery(self, samsung_mock, wol_mock): - """Testing setup of platform with discovery.""" - with mock.patch("homeassistant.components.samsungtv.media_player.socket"): - add_entities = mock.Mock() - setup_platform(self.hass, {}, add_entities, discovery_info=DISCOVERY_INFO) - - @MockDependency("samsungctl") - @MockDependency("wakeonlan") - @mock.patch("homeassistant.components.samsungtv.media_player._LOGGER.warning") - def test_setup_none(self, samsung_mock, wol_mock, mocked_warn): - """Testing setup of platform with no data.""" - with mock.patch("homeassistant.components.samsungtv.media_player.socket"): - add_entities = mock.Mock() - setup_platform(self.hass, {}, add_entities, discovery_info=None) - mocked_warn.assert_called_once_with("Cannot determine device") - add_entities.assert_not_called() - - def test_update_on(self): - """Testing update tv on.""" - self.device.update() - self.assertEqual(STATE_ON, self.device._state) - - def test_update_off(self): - """Testing update tv off.""" - _remote = mock.Mock() - _remote.control = mock.Mock(side_effect=OSError("Boom")) - self.device.get_remote = mock.Mock(return_value=_remote) - self.device.update() - assert STATE_OFF == self.device._state - - def test_send_key(self): - """Test for send key.""" - self.device.send_key("KEY_POWER") - self.assertEqual(STATE_ON, self.device._state) - - def test_send_key_broken_pipe(self): - """Testing broken pipe Exception.""" - _remote = mock.Mock() - _remote.control = mock.Mock(side_effect=BrokenPipeError("Boom")) - self.device.get_remote = mock.Mock(return_value=_remote) - self.device.send_key("HELLO") - self.assertIsNone(self.device._remote) - self.assertEqual(STATE_ON, self.device._state) - - def test_send_key_connection_closed_retry_succeed(self): - """Test retry on connection closed.""" - _remote = mock.Mock() - _remote.control = mock.Mock( - side_effect=[ - self.device._exceptions_class.ConnectionClosed("Boom"), - mock.DEFAULT, - ] - ) - self.device.get_remote = mock.Mock(return_value=_remote) - command = "HELLO" - self.device.send_key(command) - self.assertEqual(STATE_ON, self.device._state) - # verify that _remote.control() get called twice because of retry logic - expected = [mock.call(command), mock.call(command)] - assert expected == _remote.control.call_args_list - - def test_send_key_unhandled_response(self): - """Testing unhandled response exception.""" - _remote = mock.Mock() - _remote.control = mock.Mock( - side_effect=self.device._exceptions_class.UnhandledResponse("Boom") - ) - self.device.get_remote = mock.Mock(return_value=_remote) - self.device.send_key("HELLO") - self.assertIsNone(self.device._remote) - self.assertEqual(STATE_ON, self.device._state) - - def test_send_key_os_error(self): - """Testing broken pipe Exception.""" - _remote = mock.Mock() - _remote.control = mock.Mock(side_effect=OSError("Boom")) - self.device.get_remote = mock.Mock(return_value=_remote) - self.device.send_key("HELLO") - assert self.device._remote is None - assert STATE_OFF == self.device._state - - def test_power_off_in_progress(self): - """Test for power_off_in_progress.""" - assert not self.device._power_off_in_progress() - self.device._end_of_power_off = dt_util.utcnow() + timedelta(seconds=15) - assert self.device._power_off_in_progress() - - def test_name(self): - """Test for name property.""" - assert "fake" == self.device.name - - def test_state(self): - """Test for state property.""" - self.device._state = STATE_ON - self.assertEqual(STATE_ON, self.device.state) - self.device._state = STATE_OFF - assert STATE_OFF == self.device.state - - def test_is_volume_muted(self): - """Test for is_volume_muted property.""" - self.device._muted = False - assert not self.device.is_volume_muted - self.device._muted = True - assert self.device.is_volume_muted - - def test_supported_features(self): - """Test for supported_features property.""" - self.device._mac = None - assert SUPPORT_SAMSUNGTV == self.device.supported_features - self.device._mac = "fake" - assert SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON == self.device.supported_features - - def test_device_class(self): - """Test for device_class property.""" - assert DEVICE_CLASS_TV == self.device.device_class - - def test_turn_off(self): - """Test for turn_off.""" - self.device.send_key = mock.Mock() - _remote = mock.Mock() - _remote.close = mock.Mock() - self.get_remote = mock.Mock(return_value=_remote) - self.device._end_of_power_off = None - self.device.turn_off() - assert self.device._end_of_power_off is not None - self.device.send_key.assert_called_once_with("KEY_POWER") - self.device.send_key = mock.Mock() - self.device._config["method"] = "legacy" - self.device.turn_off() - self.device.send_key.assert_called_once_with("KEY_POWEROFF") - - @mock.patch("homeassistant.components.samsungtv.media_player._LOGGER.debug") - def test_turn_off_os_error(self, mocked_debug): - """Test for turn_off with OSError.""" - _remote = mock.Mock() - _remote.close = mock.Mock(side_effect=OSError("BOOM")) - self.device.get_remote = mock.Mock(return_value=_remote) - self.device.turn_off() - mocked_debug.assert_called_once_with("Could not establish connection.") - - def test_volume_up(self): - """Test for volume_up.""" - self.device.send_key = mock.Mock() - self.device.volume_up() - self.device.send_key.assert_called_once_with("KEY_VOLUP") - - def test_volume_down(self): - """Test for volume_down.""" - self.device.send_key = mock.Mock() - self.device.volume_down() - self.device.send_key.assert_called_once_with("KEY_VOLDOWN") - - def test_mute_volume(self): - """Test for mute_volume.""" - self.device.send_key = mock.Mock() - self.device.mute_volume(True) - self.device.send_key.assert_called_once_with("KEY_MUTE") - - def test_media_play_pause(self): - """Test for media_next_track.""" - self.device.send_key = mock.Mock() - self.device._playing = False - self.device.media_play_pause() - self.device.send_key.assert_called_once_with("KEY_PLAY") - assert self.device._playing - self.device.send_key = mock.Mock() - self.device.media_play_pause() - self.device.send_key.assert_called_once_with("KEY_PAUSE") - assert not self.device._playing - - def test_media_play(self): - """Test for media_play.""" - self.device.send_key = mock.Mock() - self.device._playing = False - self.device.media_play() - self.device.send_key.assert_called_once_with("KEY_PLAY") - assert self.device._playing - - def test_media_pause(self): - """Test for media_pause.""" - self.device.send_key = mock.Mock() - self.device._playing = True - self.device.media_pause() - self.device.send_key.assert_called_once_with("KEY_PAUSE") - assert not self.device._playing - - def test_media_next_track(self): - """Test for media_next_track.""" - self.device.send_key = mock.Mock() - self.device.media_next_track() - self.device.send_key.assert_called_once_with("KEY_FF") - - def test_media_previous_track(self): - """Test for media_previous_track.""" - self.device.send_key = mock.Mock() - self.device.media_previous_track() - self.device.send_key.assert_called_once_with("KEY_REWIND") - - def test_turn_on(self): - """Test turn on.""" - self.device.send_key = mock.Mock() - self.device._mac = None - self.device.turn_on() - self.device.send_key.assert_called_once_with("KEY_POWERON") - self.device._wol.send_magic_packet = mock.Mock() - self.device._mac = "fake" - self.device.turn_on() - self.device._wol.send_magic_packet.assert_called_once_with("fake") +ENTITY_ID_AUTO = f"{DOMAIN}.fake_auto" +MOCK_CONFIG_AUTO = { + DOMAIN: { + CONF_PLATFORM: SAMSUNGTV_DOMAIN, + CONF_HOST: "fake_auto", + CONF_NAME: "fake_auto", + } +} + +ENTITY_ID_DISCOVERY = f"{DOMAIN}.fake_discovery_fake_model" +MOCK_CONFIG_DISCOVERY = { + "name": "fake_discovery", + "model_name": "fake_model", + "host": "fake_host", + "udn": "fake_uuid", +} + +ENTITY_ID_DISCOVERY_PREFIX = f"{DOMAIN}.fake_discovery_prefix_fake_model_prefix" +MOCK_CONFIG_DISCOVERY_PREFIX = { + "name": "[TV]fake_discovery_prefix", + "model_name": "fake_model_prefix", + "host": "fake_host_prefix", + "udn": "uuid:fake_uuid_prefix", +} + +AUTODETECT_WEBSOCKET = { + "name": "HomeAssistant", + "description": "fake_auto", + "id": "ha.component.samsung", + "method": "websocket", + "port": None, + "host": "fake_auto", + "timeout": 1, +} +AUTODETECT_LEGACY = { + "name": "HomeAssistant", + "description": "fake_auto", + "id": "ha.component.samsung", + "method": "legacy", + "port": None, + "host": "fake_auto", + "timeout": 1, +} + + +@pytest.fixture(name="remote") +def remote_fixture(): + """Patch the samsungctl Remote.""" + with patch("samsungctl.Remote") as remote_class, patch( + "homeassistant.components.samsungtv.media_player.socket" + ) as socket_class: + remote = mock.Mock() + remote_class.return_value = remote + socket = mock.Mock() + socket_class.return_value = socket + yield remote + + +@pytest.fixture(name="wakeonlan") +def wakeonlan_fixture(): + """Patch the wakeonlan Remote.""" + with MockDependency("wakeonlan") as wakeonlan: + yield wakeonlan @pytest.fixture -def samsung_mock(): - """Mock samsungctl.""" - with patch.dict("sys.modules", {"samsungctl": MagicMock()}): - yield +def mock_now(): + """Fixture for dtutil.now.""" + return dt_util.utcnow() + +async def setup_samsungtv(hass, config): + """Set up mock Samsung TV.""" + await async_setup_component(hass, "media_player", config) + await hass.async_block_till_done() -async def test_play_media(hass, samsung_mock): + +async def test_setup_with_mac(hass, remote): + """Test setup of platform.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert hass.states.get(ENTITY_ID) + + +async def test_setup_duplicate(hass, remote, caplog): + """Test duplicate setup of platform.""" + DUPLICATE = {DOMAIN: [MOCK_CONFIG[DOMAIN], MOCK_CONFIG[DOMAIN]]} + await setup_samsungtv(hass, DUPLICATE) + assert "Ignoring duplicate Samsung TV fake" in caplog.text + + +async def test_setup_without_mac(hass, remote): + """Test setup of platform.""" + await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) + assert hass.states.get(ENTITY_ID_NOMAC) + + +async def test_setup_discovery(hass, remote): + """Test setup of platform with discovery.""" + hass.async_create_task( + async_load_platform( + hass, DOMAIN, SAMSUNGTV_DOMAIN, MOCK_CONFIG_DISCOVERY, {DOMAIN: {}} + ) + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID_DISCOVERY) + assert state + assert state.name == "fake_discovery (fake_model)" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get(ENTITY_ID_DISCOVERY) + assert entry + assert entry.unique_id == "fake_uuid" + + +async def test_setup_discovery_prefix(hass, remote): + """Test setup of platform with discovery.""" + hass.async_create_task( + async_load_platform( + hass, DOMAIN, SAMSUNGTV_DOMAIN, MOCK_CONFIG_DISCOVERY_PREFIX, {DOMAIN: {}} + ) + ) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_ID_DISCOVERY_PREFIX) + assert state + assert state.name == "fake_discovery_prefix (fake_model_prefix)" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get(ENTITY_ID_DISCOVERY_PREFIX) + assert entry + assert entry.unique_id == "fake_uuid_prefix" + + +async def test_update_on(hass, remote, mock_now): + """Testing update tv on.""" + await setup_samsungtv(hass, MOCK_CONFIG) + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + +async def test_update_off(hass, remote, mock_now): + """Testing update tv off.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock(side_effect=OSError("Boom")) + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_send_key(hass, remote, wakeonlan): + """Test for send key.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_VOLUP"), call("KEY")] + assert state.state == STATE_ON + + +async def test_send_key_autodetect_websocket(hass, remote): + """Test for send key with autodetection of protocol.""" + with patch("samsungctl.Remote") as remote, patch( + "homeassistant.components.samsungtv.media_player.socket" + ): + await setup_samsungtv(hass, MOCK_CONFIG_AUTO) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True + ) + state = hass.states.get(ENTITY_ID_AUTO) + assert remote.call_count == 1 + assert remote.call_args_list == [call(AUTODETECT_WEBSOCKET)] + assert state.state == STATE_ON + + +async def test_send_key_autodetect_websocket_exception(hass, caplog): + """Test for send key with autodetection of protocol.""" + with patch( + "samsungctl.Remote", side_effect=[exceptions.AccessDenied("Boom"), mock.DEFAULT] + ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): + await setup_samsungtv(hass, MOCK_CONFIG_AUTO) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True + ) + state = hass.states.get(ENTITY_ID_AUTO) + # called 2 times because of the exception and the send key + assert remote.call_count == 2 + assert remote.call_args_list == [ + call(AUTODETECT_WEBSOCKET), + call(AUTODETECT_WEBSOCKET), + ] + assert state.state == STATE_ON + assert "Found working config without connection: " in caplog.text + assert "Failing config: " not in caplog.text + + +async def test_send_key_autodetect_legacy(hass, remote): + """Test for send key with autodetection of protocol.""" + with patch( + "samsungctl.Remote", side_effect=[OSError("Boom"), mock.DEFAULT] + ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): + await setup_samsungtv(hass, MOCK_CONFIG_AUTO) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True + ) + state = hass.states.get(ENTITY_ID_AUTO) + assert remote.call_count == 2 + assert remote.call_args_list == [ + call(AUTODETECT_WEBSOCKET), + call(AUTODETECT_LEGACY), + ] + assert state.state == STATE_ON + + +async def test_send_key_autodetect_none(hass, remote): + """Test for send key with autodetection of protocol.""" + with patch("samsungctl.Remote", side_effect=OSError("Boom")) as remote, patch( + "homeassistant.components.samsungtv.media_player.socket" + ): + await setup_samsungtv(hass, MOCK_CONFIG_AUTO) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True + ) + state = hass.states.get(ENTITY_ID_AUTO) + # 4 calls because of retry + assert remote.call_count == 4 + assert remote.call_args_list == [ + call(AUTODETECT_WEBSOCKET), + call(AUTODETECT_LEGACY), + call(AUTODETECT_WEBSOCKET), + call(AUTODETECT_LEGACY), + ] + assert state.state == STATE_UNKNOWN + + +async def test_send_key_broken_pipe(hass, remote): + """Testing broken pipe Exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock(side_effect=BrokenPipeError("Boom")) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + +async def test_send_key_connection_closed_retry_succeed(hass, remote): + """Test retry on connection closed.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock( + side_effect=[exceptions.ConnectionClosed("Boom"), mock.DEFAULT, mock.DEFAULT] + ) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + # key because of retry two times and update called + assert remote.control.call_count == 3 + assert remote.control.call_args_list == [ + call("KEY_VOLUP"), + call("KEY_VOLUP"), + call("KEY"), + ] + assert state.state == STATE_ON + + +async def test_send_key_unhandled_response(hass, remote): + """Testing unhandled response exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock(side_effect=exceptions.UnhandledResponse("Boom")) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + +async def test_send_key_os_error(hass, remote): + """Testing broken pipe Exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock(side_effect=OSError("Boom")) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_name(hass, remote): + """Test for name property.""" + await setup_samsungtv(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_FRIENDLY_NAME] == "fake" + + +async def test_state_with_mac(hass, remote, wakeonlan): + """Test for state property.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_state_without_mac(hass, remote): + """Test for state property.""" + await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True + ) + state = hass.states.get(ENTITY_ID_NOMAC) + assert state.state == STATE_ON + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True + ) + state = hass.states.get(ENTITY_ID_NOMAC) + assert state.state == STATE_OFF + + +async def test_supported_features_with_mac(hass, remote): + """Test for supported_features property.""" + await setup_samsungtv(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV | SUPPORT_TURN_ON + ) + + +async def test_supported_features_without_mac(hass, remote): + """Test for supported_features property.""" + await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) + state = hass.states.get(ENTITY_ID_NOMAC) + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_SAMSUNGTV + + +async def test_device_class(hass, remote): + """Test for device_class property.""" + await setup_samsungtv(hass, MOCK_CONFIG) + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_TV + + +async def test_turn_off_websocket(hass, remote): + """Test for turn_off.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_POWER")] + + +async def test_turn_off_legacy(hass, remote): + """Test for turn_off.""" + await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True + ) + # key called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY_POWEROFF")] + + +async def test_turn_off_os_error(hass, remote, caplog): + """Test for turn_off with OSError.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.close = mock.Mock(side_effect=OSError("BOOM")) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert "Could not establish connection." in caplog.text + + +async def test_volume_up(hass, remote): + """Test for volume_up.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_VOLUP"), call("KEY")] + + +async def test_volume_down(hass, remote): + """Test for volume_down.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_DOWN, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_VOLDOWN"), call("KEY")] + + +async def test_mute_volume(hass, remote): + """Test for mute_volume.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_MEDIA_VOLUME_MUTED: True}, + True, + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_MUTE"), call("KEY")] + + +async def test_media_play(hass, remote): + """Test for media_play.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_PLAY, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_PLAY"), call("KEY")] + + +async def test_media_pause(hass, remote): + """Test for media_pause.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_PAUSE"), call("KEY")] + + +async def test_media_next_track(hass, remote): + """Test for media_next_track.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_NEXT_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_FF"), call("KEY")] + + +async def test_media_previous_track(hass, remote): + """Test for media_previous_track.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_REWIND"), call("KEY")] + + +async def test_turn_on_with_mac(hass, remote, wakeonlan): + """Test turn on.""" + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert wakeonlan.send_magic_packet.call_count == 1 + assert wakeonlan.send_magic_packet.call_args_list == [call("fake")] + + +async def test_turn_on_without_mac(hass, remote): + """Test turn on.""" + await setup_samsungtv(hass, MOCK_CONFIG_NOMAC) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_NOMAC}, True + ) + # nothing called as not supported feature + assert remote.control.call_count == 0 + + +async def test_play_media(hass, remote): """Test for play_media.""" asyncio_sleep = asyncio.sleep sleeps = [] @@ -312,57 +576,109 @@ async def sleep(duration, loop): sleeps.append(duration) await asyncio_sleep(0, loop=loop) + await setup_samsungtv(hass, MOCK_CONFIG) with patch("asyncio.sleep", new=sleep): - device = SamsungTVDevice(**WORKING_CONFIG) - device.hass = hass - - device.send_key = mock.Mock() - await device.async_play_media(MEDIA_TYPE_CHANNEL, "576") - - exp = [call("KEY_5"), call("KEY_7"), call("KEY_6"), call("KEY_ENTER")] - assert device.send_key.call_args_list == exp + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_ID: "576", + }, + True, + ) + # keys and update called + assert remote.control.call_count == 5 + assert remote.control.call_args_list == [ + call("KEY_5"), + call("KEY_7"), + call("KEY_6"), + call("KEY_ENTER"), + call("KEY"), + ] assert len(sleeps) == 3 -async def test_play_media_invalid_type(hass, samsung_mock): +async def test_play_media_invalid_type(hass, remote): """Test for play_media with invalid media type.""" url = "https://example.com" - device = SamsungTVDevice(**WORKING_CONFIG) - device.send_key = mock.Mock() - await device.async_play_media(MEDIA_TYPE_URL, url) - assert device.send_key.call_count == 0 - - -async def test_play_media_channel_as_string(hass, samsung_mock): + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_URL, + ATTR_MEDIA_CONTENT_ID: url, + }, + True, + ) + # only update called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY")] + + +async def test_play_media_channel_as_string(hass, remote): """Test for play_media with invalid channel as string.""" url = "https://example.com" - device = SamsungTVDevice(**WORKING_CONFIG) - device.send_key = mock.Mock() - await device.async_play_media(MEDIA_TYPE_CHANNEL, url) - assert device.send_key.call_count == 0 - - -async def test_play_media_channel_as_non_positive(hass, samsung_mock): + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_ID: url, + }, + True, + ) + # only update called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY")] + + +async def test_play_media_channel_as_non_positive(hass, remote): """Test for play_media with invalid channel as non positive integer.""" - device = SamsungTVDevice(**WORKING_CONFIG) - device.send_key = mock.Mock() - await device.async_play_media(MEDIA_TYPE_CHANNEL, "-4") - assert device.send_key.call_count == 0 - - -async def test_select_source(hass, samsung_mock): + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + ATTR_MEDIA_CONTENT_ID: "-4", + }, + True, + ) + # only update called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY")] + + +async def test_select_source(hass, remote): """Test for select_source.""" - device = SamsungTVDevice(**WORKING_CONFIG) - device.hass = hass - device.send_key = mock.Mock() - await device.async_select_source("HDMI") - exp = [call("KEY_HDMI")] - assert device.send_key.call_args_list == exp - - -async def test_select_source_invalid_source(hass, samsung_mock): + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "HDMI"}, + True, + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_HDMI"), call("KEY")] + + +async def test_select_source_invalid_source(hass, remote): """Test for select_source with invalid source.""" - device = SamsungTVDevice(**WORKING_CONFIG) - device.send_key = mock.Mock() - await device.async_select_source("INVALID") - assert device.send_key.call_count == 0 + await setup_samsungtv(hass, MOCK_CONFIG) + assert await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "INVALID"}, + True, + ) + # only update called + assert remote.control.call_count == 1 + assert remote.control.call_args_list == [call("KEY")] From 6bfb2460f2e4f6c93b23b07005fc9db5fb1cd0ef Mon Sep 17 00:00:00 2001 From: guillempages Date: Fri, 25 Oct 2019 16:06:52 +0200 Subject: [PATCH 1259/3953] [homematic]Pass channel to light color functions (#27306) The device HmIP-BSL has two independent LEDs on two different channels, so the channel needs to be explictly specified when setting the color. --- homeassistant/components/homematic/light.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 32fa0bb358e5b0..29992bccef3170 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -66,7 +66,7 @@ def hs_color(self): """Return the hue and saturation color value [float, float].""" if not self.supported_features & SUPPORT_COLOR: return None - hue, sat = self._hmdevice.get_hs_color() + hue, sat = self._hmdevice.get_hs_color(self._channel) return hue * 360.0, sat * 100.0 @property @@ -98,6 +98,7 @@ def turn_on(self, **kwargs): self._hmdevice.set_hs_color( hue=kwargs[ATTR_HS_COLOR][0] / 360.0, saturation=kwargs[ATTR_HS_COLOR][1] / 100.0, + channel=self._channel, ) if ATTR_EFFECT in kwargs: self._hmdevice.set_effect(kwargs[ATTR_EFFECT]) From 98cf3f4aa30cb0af02fffcb5561760d1a6dfc732 Mon Sep 17 00:00:00 2001 From: guillempages Date: Fri, 25 Oct 2019 16:08:11 +0200 Subject: [PATCH 1260/3953] [homematic]Add support for HmIP-BSL LEDs (#27307) With this commit, 3 entities are created for the HmIP-BSL device: 2 lights for the two independent LEDs and 1 switch for the relais --- homeassistant/components/homematic/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index cd791434f90072..42fb73f6da2e9d 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -82,6 +82,7 @@ "IPKeySwitchPowermeter", "IPGarage", "IPKeySwitch", + "IPKeySwitchLevel", "IPMultiIO", ], DISCOVER_LIGHTS: [ @@ -90,6 +91,7 @@ "IPKeyDimmer", "IPDimmer", "ColorEffectLight", + "IPKeySwitchLevel", ], DISCOVER_SENSORS: [ "SwitchPowermeter", @@ -671,6 +673,11 @@ def _get_devices(hass, discovery_type, keys, interface): and class_name not in HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS.get(param, []) ): continue + if discovery_type == DISCOVER_SWITCHES and class_name == "IPKeySwitchLevel": + channels.remove(8) + channels.remove(12) + if discovery_type == DISCOVER_LIGHTS and class_name == "IPKeySwitchLevel": + channels.remove(4) # Add devices _LOGGER.debug( From 9153729b2105ffac4738df13a1ac7428eb83cfce Mon Sep 17 00:00:00 2001 From: gngj Date: Fri, 25 Oct 2019 19:02:40 +0300 Subject: [PATCH 1261/3953] move hass-frontend import back down (#28203) --- homeassistant/components/frontend/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 541d1bf473d859..f82350d994d6d5 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -7,7 +7,6 @@ from typing import Any, Dict, Optional, Set, Tuple from aiohttp import hdrs, web, web_urldispatcher -import hass_frontend import jinja2 import voluptuous as vol from yarl import URL @@ -241,6 +240,8 @@ def _frontend_root(dev_repo_path): """Return root path to the frontend files.""" if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" + # Keep import here so that we can import frontend without installing reqs + import hass_frontend return hass_frontend.where() From 43c7b57d1ec2dc45cc8dff09147c205e082389ed Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 25 Oct 2019 11:37:50 -0500 Subject: [PATCH 1262/3953] Update Plex via websockets (#28158) * Save client identifier from auth for future use * Use websocket events to update Plex * Handle websocket disconnections * Use aiohttp, shut down socket cleanly * Bad rebase fix * Don't connect websocket during config_flow validation, fix tests * Move websocket handling to external library * Close websocket session on HA stop * Use external library, revert unnecessary test change * Async & lint fixes * Clean up websocket stopper on entry unload * Setup websocket in component, pass actual needed object to library --- .coveragerc | 1 + homeassistant/components/plex/__init__.py | 45 ++++++++++++++++----- homeassistant/components/plex/const.py | 3 +- homeassistant/components/plex/manifest.json | 3 +- homeassistant/components/plex/server.py | 7 ++++ requirements_all.txt | 3 ++ requirements_test_all.txt | 3 ++ 7 files changed, 52 insertions(+), 13 deletions(-) diff --git a/.coveragerc b/.coveragerc index 3350bc359af120..90efc417e03440 100644 --- a/.coveragerc +++ b/.coveragerc @@ -514,6 +514,7 @@ omit = homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py homeassistant/components/plex/server.py + homeassistant/components/plex/websockets.py homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index b6ed3245115519..1aaa8a8e3aa5fc 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,9 +1,9 @@ """Support to embed Plex.""" import asyncio -from datetime import timedelta import logging import plexapi.exceptions +from plexwebsocket import PlexWebsocket import requests.exceptions import voluptuous as vol @@ -16,9 +16,14 @@ CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .const import ( CONF_USE_EPISODE_ART, @@ -33,8 +38,9 @@ PLATFORMS, PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, - REFRESH_LISTENERS, + PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, + WEBSOCKETS, ) from .server import PlexServer @@ -67,9 +73,7 @@ def setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault( - PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}, DISPATCHERS: {}} - ) + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}}) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: @@ -136,7 +140,6 @@ async def async_setup_entry(hass, entry): ) server_id = plex_server.machine_identifier hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server - hass.data[PLEX_DOMAIN][DISPATCHERS][server_id] = [] for platform in PLATFORMS: hass.async_create_task( @@ -145,9 +148,29 @@ async def async_setup_entry(hass, entry): entry.add_update_listener(async_options_updated) - hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = async_track_time_interval( - hass, lambda now: plex_server.update_platforms(), timedelta(seconds=10) + unsub = async_dispatcher_connect( + hass, + PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id), + plex_server.update_platforms, + ) + hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + def update_plex(): + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + + session = async_get_clientsession(hass) + websocket = PlexWebsocket(plex_server.plex_server, update_plex, session) + hass.loop.create_task(websocket.listen()) + hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket + + def close_websocket_session(_): + websocket.close() + + unsub = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, close_websocket_session ) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) return True @@ -156,8 +179,8 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" server_id = entry.data[CONF_SERVER_IDENTIFIER] - cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id) - cancel() + websocket = hass.data[PLEX_DOMAIN][WEBSOCKETS].pop(server_id) + websocket.close() dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id) for unsub in dispatchers: diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 0d512101e119c7..d3c79e60bc48f5 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -10,8 +10,8 @@ DISPATCHERS = "dispatchers" PLATFORMS = ["media_player", "sensor"] -REFRESH_LISTENERS = "refresh_listeners" SERVERS = "servers" +WEBSOCKETS = "websockets" PLEX_CONFIG_FILE = "plex.conf" PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" @@ -19,6 +19,7 @@ PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" +PLEX_UPDATE_PLATFORMS_SIGNAL = "plex_update_platforms_signal.{}" PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" CONF_CLIENT_IDENTIFIER = "client_id" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 3c570a0e64cf1d..90ae305148ed5c 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -5,7 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ "plexapi==3.0.6", - "plexauth==0.0.5" + "plexauth==0.0.5", + "plexwebsocket==0.0.1" ], "dependencies": [ "http" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index c0461ee0f54e3b..e6f77a310f1727 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -103,6 +103,8 @@ def refresh_entity(self, machine_identifier, device, session): def update_platforms(self): """Update the platform entities.""" + _LOGGER.debug("Updating devices") + available_clients = {} new_clients = set() @@ -164,6 +166,11 @@ def update_platforms(self): sessions, ) + @property + def plex_server(self): + """Return the plexapi PlexServer instance.""" + return self._plex_server + @property def friendly_name(self): """Return name of connected Plex server.""" diff --git a/requirements_all.txt b/requirements_all.txt index 81009711ed7598..32115995df1135 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -973,6 +973,9 @@ plexapi==3.0.6 # homeassistant.components.plex plexauth==0.0.5 +# homeassistant.components.plex +plexwebsocket==0.0.1 + # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25ca8abdbf3861..0b8cfb1f57f898 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -346,6 +346,9 @@ plexapi==3.0.6 # homeassistant.components.plex plexauth==0.0.5 +# homeassistant.components.plex +plexwebsocket==0.0.1 + # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 From 0656f0c62b980d6f47af8b47eace45524e848acd Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 25 Oct 2019 19:39:16 +0300 Subject: [PATCH 1263/3953] Address post-merge coolmaster config flow code review (#28163) * Address post-merge code review comments * Use component path for 3rd party lib --- homeassistant/components/coolmaster/__init__.py | 10 ++++------ homeassistant/components/coolmaster/config_flow.py | 7 +++---- tests/components/coolmaster/test_config_flow.py | 14 ++++++-------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index 530427d33ad1fb..c666c39cfb3bd5 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -8,15 +8,13 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Coolmaster from a config entry.""" - hass.async_add_job(hass.config_entries.async_forward_entry_setup(entry, "climate")) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "climate") + ) return True async def async_unload_entry(hass, entry): """Unload a Coolmaster config entry.""" - await hass.async_add_job( - hass.config_entries.async_forward_entry_unload(entry, "climate") - ) - - return True + return await hass.config_entries.async_forward_entry_unload(entry, "climate") diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index 543b4c239c8aa4..fe52ea17b28bf8 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -14,11 +14,10 @@ DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA}) -async def validate_connection(hass: core.HomeAssistant, host): - """Validate that we can connect to the Coolmaster instance.""" +async def _validate_connection(hass: core.HomeAssistant, host): cool = CoolMasterNet(host, port=DEFAULT_PORT) devices = await hass.async_add_executor_job(cool.devices) - return len(devices) > 0 + return bool(devices) class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -50,7 +49,7 @@ async def async_step_user(self, user_input=None): host = user_input[CONF_HOST] try: - result = await validate_connection(self.hass, host) + result = await _validate_connection(self.hass, host) if not result: errors["base"] = "no_units" except (ConnectionRefusedError, TimeoutError): diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index d49858fcf05cf2..d0126ff2cb64ae 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -4,8 +4,6 @@ from homeassistant import config_entries, setup from homeassistant.components.coolmaster.const import DOMAIN, AVAILABLE_MODES -# from homeassistant.components.coolmaster.config_flow import validate_connection - from tests.common import mock_coro @@ -26,8 +24,8 @@ async def test_form(hass): assert result["errors"] is None with patch( - "homeassistant.components.coolmaster.config_flow.validate_connection", - return_value=mock_coro(True), + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", + return_value=[1], ), patch( "homeassistant.components.coolmaster.async_setup", return_value=mock_coro(True) ) as mock_setup, patch( @@ -57,7 +55,7 @@ async def test_form_timeout(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.validate_connection", + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", side_effect=TimeoutError(), ): result2 = await hass.config_entries.flow.async_configure( @@ -75,7 +73,7 @@ async def test_form_connection_refused(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.validate_connection", + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", side_effect=ConnectionRefusedError(), ): result2 = await hass.config_entries.flow.async_configure( @@ -93,8 +91,8 @@ async def test_form_no_units(hass): ) with patch( - "homeassistant.components.coolmaster.config_flow.validate_connection", - return_value=mock_coro(False), + "homeassistant.components.coolmaster.config_flow.CoolMasterNet.devices", + return_value=[], ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], _flow_data() From 3c4caaaefc4db53fe05c5bcdf6c46871499863e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 25 Oct 2019 20:09:18 +0300 Subject: [PATCH 1264/3953] Add presentation URL to SSDP discovery info (#28196) --- homeassistant/components/ssdp/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index a8591ac042b447..d2382748f30e4f 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -26,6 +26,7 @@ ATTR_MANUFACTURERURL = "manufacturerURL" ATTR_UDN = "udn" ATTR_UPNP_DEVICE_TYPE = "upnp_device_type" +ATTR_PRESENTATIONURL = "presentation_url" _LOGGER = logging.getLogger(__name__) @@ -175,5 +176,6 @@ def info_from_entry(entry, device_info): info[ATTR_MANUFACTURERURL] = device_info.get("manufacturerURL") info[ATTR_UDN] = device_info.get("UDN") info[ATTR_UPNP_DEVICE_TYPE] = device_info.get("deviceType") + info[ATTR_PRESENTATIONURL] = device_info.get("presentationURL") return info From 5c8a9c2815b567c94033ba1ef19ed016faf8bbc0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 25 Oct 2019 19:20:42 +0200 Subject: [PATCH 1265/3953] Updated frontend to 20191025.0 (#28208) --- 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 ae53b972dcaa94..b23d40605dd7a8 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191023.0" + "home-assistant-frontend==20191025.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0e06690c856ef0..fbfa1dbf67b1c8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 32115995df1135..ff633960f821af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b8cfb1f57f898..8f55cb1aa1b184 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -243,7 +243,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 38428308fccdc3c042aa599fca80b0c1d83a8857 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 25 Oct 2019 13:21:22 -0400 Subject: [PATCH 1266/3953] Change Alexa default display category based on media_player device_class (#28191) * Support default display category based one media_player device_class. * Support default display category based one media_player device_class. --- homeassistant/components/alexa/entities.py | 4 ++++ tests/components/alexa/test_smart_home.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index d84848e9aba43a..e52e1b4f87eab6 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -391,6 +391,10 @@ class MediaPlayerCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class == media_player.DEVICE_CLASS_SPEAKER: + return [DisplayCategory.SPEAKER] + return [DisplayCategory.TV] def interfaces(self): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index c50c0748147f9d..1de7d404ef66f1 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -968,6 +968,25 @@ async def test_media_player_power(hass): ) +async def test_media_player_speaker(hass): + """Test media player discovery with device class speaker.""" + device = ( + "media_player.test", + "off", + { + "friendly_name": "Test media player", + "supported_features": 51765, + "volume_level": 0.75, + "device_class": "speaker", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test" + assert appliance["displayCategories"][0] == "SPEAKER" + assert appliance["friendlyName"] == "Test media player" + + async def test_alert(hass): """Test alert discovery.""" device = ("alert.test", "off", {"friendly_name": "Test alert"}) From 2c914e0c59453d73fa3387a6b2b73a4485bbb898 Mon Sep 17 00:00:00 2001 From: Hayley McIldoon Date: Fri, 25 Oct 2019 13:22:39 -0400 Subject: [PATCH 1267/3953] Add device condition support for media_player (#28161) * Add device condition for media_player * Fix typo in strings --- .../media_player/device_condition.py | 117 +++++++++ .../components/media_player/strings.json | 11 + .../media_player/test_device_condition.py | 243 ++++++++++++++++++ 3 files changed, 371 insertions(+) create mode 100644 homeassistant/components/media_player/device_condition.py create mode 100644 homeassistant/components/media_player/strings.json create mode 100644 tests/components/media_player/test_device_condition.py diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py new file mode 100644 index 00000000000000..a67a084a94f799 --- /dev/null +++ b/homeassistant/components/media_player/device_condition.py @@ -0,0 +1,117 @@ +"""Provides device automations for Media player.""" +from typing import Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +CONDITION_TYPES = {"is_on", "is_off", "is_idle", "is_paused", "is_playing"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions for Media player devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add conditions for each entity that belongs to this integration + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_on", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_off", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_idle", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_paused", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_playing", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_playing": + state = STATE_PLAYING + elif config[CONF_TYPE] == "is_idle": + state = STATE_IDLE + elif config[CONF_TYPE] == "is_paused": + state = STATE_PAUSED + elif config[CONF_TYPE] == "is_on": + state = STATE_ON + else: + state = STATE_OFF + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/media_player/strings.json b/homeassistant/components/media_player/strings.json new file mode 100644 index 00000000000000..e9cb812767b2f2 --- /dev/null +++ b/homeassistant/components/media_player/strings.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off", + "is_idle": "{entity_name} is idle", + "is_paused": "{entity_name} is paused", + "is_playing": "{entity_name} is playing" + } + } +} diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py new file mode 100644 index 00000000000000..6216cc0e2b0c85 --- /dev/null +++ b/tests/components/media_player/test_device_condition.py @@ -0,0 +1,243 @@ +"""The tests for Media player device conditions.""" +import pytest + +from homeassistant.components.media_player import DOMAIN +from homeassistant.const import ( + STATE_ON, + STATE_OFF, + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, +) +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a media_player.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_idle", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_paused", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_playing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("media_player.entity", STATE_ON) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": "is_idle", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_idle - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event4"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": "is_paused", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_paused - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event5"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "media_player.entity", + "type": "is_playing", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_playing - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on - event - test_event1" + + hass.states.async_set("media_player.entity", STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off - event - test_event2" + + hass.states.async_set("media_player.entity", STATE_IDLE) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "is_idle - event - test_event3" + + hass.states.async_set("media_player.entity", STATE_PAUSED) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "is_paused - event - test_event4" + + hass.states.async_set("media_player.entity", STATE_PLAYING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + await hass.async_block_till_done() + assert len(calls) == 5 + assert calls[4].data["some"] == "is_playing - event - test_event5" From d28f7ab120ce8a4eabb6b54513e8c441687c99c7 Mon Sep 17 00:00:00 2001 From: gngj Date: Fri, 25 Oct 2019 20:42:23 +0300 Subject: [PATCH 1268/3953] Fix microsoft tts (#28199) * Update pycsspeechtts From 1.0.2 to 1.0.3 as the old one is using an api that doesn't work * Give a option to choose region Api is now region dependent, so gave it a config --- homeassistant/components/microsoft/manifest.json | 2 +- homeassistant/components/microsoft/tts.py | 11 +++++++++-- requirements_all.txt | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 16ae94c212ef62..5834897ee90407 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -3,7 +3,7 @@ "name": "Microsoft", "documentation": "https://www.home-assistant.io/integrations/microsoft", "requirements": [ - "pycsspeechtts==1.0.2" + "pycsspeechtts==1.0.3" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 3536c788bb9d4e..d214f6648ddd4b 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -14,6 +14,7 @@ CONF_VOLUME = "volume" CONF_PITCH = "pitch" CONF_CONTOUR = "contour" +CONF_REGION = "region" _LOGGER = logging.getLogger(__name__) @@ -72,6 +73,7 @@ DEFAULT_VOLUME = 0 DEFAULT_PITCH = "default" DEFAULT_CONTOUR = "" +DEFAULT_REGION = "eastus" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -87,6 +89,7 @@ ), vol.Optional(CONF_PITCH, default=DEFAULT_PITCH): cv.string, vol.Optional(CONF_CONTOUR, default=DEFAULT_CONTOUR): cv.string, + vol.Optional(CONF_REGION, default=DEFAULT_REGION): cv.string, } ) @@ -102,13 +105,16 @@ def get_engine(hass, config): config[CONF_VOLUME], config[CONF_PITCH], config[CONF_CONTOUR], + config[CONF_REGION], ) class MicrosoftProvider(Provider): """The Microsoft speech API provider.""" - def __init__(self, apikey, lang, gender, ttype, rate, volume, pitch, contour): + def __init__( + self, apikey, lang, gender, ttype, rate, volume, pitch, contour, region + ): """Init Microsoft TTS service.""" self._apikey = apikey self._lang = lang @@ -119,6 +125,7 @@ def __init__(self, apikey, lang, gender, ttype, rate, volume, pitch, contour): self._volume = f"{volume}%" self._pitch = pitch self._contour = contour + self._region = region self.name = "Microsoft" @property @@ -138,7 +145,7 @@ def get_tts_audio(self, message, language, options=None): from pycsspeechtts import pycsspeechtts try: - trans = pycsspeechtts.TTSTranslator(self._apikey) + trans = pycsspeechtts.TTSTranslator(self._apikey, self._region) data = trans.speak( language=language, gender=self._gender, diff --git a/requirements_all.txt b/requirements_all.txt index ff633960f821af..d3df0a3ab984ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1132,7 +1132,7 @@ pycomfoconnect==0.3 pycoolmasternet==0.0.4 # homeassistant.components.microsoft -pycsspeechtts==1.0.2 +pycsspeechtts==1.0.3 # homeassistant.components.cups # pycups==1.9.73 From 05ee15c28c90c22e919f3f621fa198e4ff14a436 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 25 Oct 2019 11:37:50 -0500 Subject: [PATCH 1269/3953] Update Plex via websockets (#28158) * Save client identifier from auth for future use * Use websocket events to update Plex * Handle websocket disconnections * Use aiohttp, shut down socket cleanly * Bad rebase fix * Don't connect websocket during config_flow validation, fix tests * Move websocket handling to external library * Close websocket session on HA stop * Use external library, revert unnecessary test change * Async & lint fixes * Clean up websocket stopper on entry unload * Setup websocket in component, pass actual needed object to library --- .coveragerc | 1 + homeassistant/components/plex/__init__.py | 45 ++++++++++++++++----- homeassistant/components/plex/const.py | 3 +- homeassistant/components/plex/manifest.json | 3 +- homeassistant/components/plex/server.py | 7 ++++ requirements_all.txt | 3 ++ requirements_test_all.txt | 3 ++ 7 files changed, 52 insertions(+), 13 deletions(-) diff --git a/.coveragerc b/.coveragerc index f97a7524a21532..748ca511dd5661 100644 --- a/.coveragerc +++ b/.coveragerc @@ -514,6 +514,7 @@ omit = homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py homeassistant/components/plex/server.py + homeassistant/components/plex/websockets.py homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index b6ed3245115519..1aaa8a8e3aa5fc 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,9 +1,9 @@ """Support to embed Plex.""" import asyncio -from datetime import timedelta import logging import plexapi.exceptions +from plexwebsocket import PlexWebsocket import requests.exceptions import voluptuous as vol @@ -16,9 +16,14 @@ CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .const import ( CONF_USE_EPISODE_ART, @@ -33,8 +38,9 @@ PLATFORMS, PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, - REFRESH_LISTENERS, + PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, + WEBSOCKETS, ) from .server import PlexServer @@ -67,9 +73,7 @@ def setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault( - PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}, DISPATCHERS: {}} - ) + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}}) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: @@ -136,7 +140,6 @@ async def async_setup_entry(hass, entry): ) server_id = plex_server.machine_identifier hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server - hass.data[PLEX_DOMAIN][DISPATCHERS][server_id] = [] for platform in PLATFORMS: hass.async_create_task( @@ -145,9 +148,29 @@ async def async_setup_entry(hass, entry): entry.add_update_listener(async_options_updated) - hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = async_track_time_interval( - hass, lambda now: plex_server.update_platforms(), timedelta(seconds=10) + unsub = async_dispatcher_connect( + hass, + PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id), + plex_server.update_platforms, + ) + hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + def update_plex(): + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + + session = async_get_clientsession(hass) + websocket = PlexWebsocket(plex_server.plex_server, update_plex, session) + hass.loop.create_task(websocket.listen()) + hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket + + def close_websocket_session(_): + websocket.close() + + unsub = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, close_websocket_session ) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) return True @@ -156,8 +179,8 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" server_id = entry.data[CONF_SERVER_IDENTIFIER] - cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id) - cancel() + websocket = hass.data[PLEX_DOMAIN][WEBSOCKETS].pop(server_id) + websocket.close() dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id) for unsub in dispatchers: diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 0d512101e119c7..d3c79e60bc48f5 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -10,8 +10,8 @@ DISPATCHERS = "dispatchers" PLATFORMS = ["media_player", "sensor"] -REFRESH_LISTENERS = "refresh_listeners" SERVERS = "servers" +WEBSOCKETS = "websockets" PLEX_CONFIG_FILE = "plex.conf" PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" @@ -19,6 +19,7 @@ PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" +PLEX_UPDATE_PLATFORMS_SIGNAL = "plex_update_platforms_signal.{}" PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" CONF_CLIENT_IDENTIFIER = "client_id" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 3c570a0e64cf1d..90ae305148ed5c 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -5,7 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ "plexapi==3.0.6", - "plexauth==0.0.5" + "plexauth==0.0.5", + "plexwebsocket==0.0.1" ], "dependencies": [ "http" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index c0461ee0f54e3b..e6f77a310f1727 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -103,6 +103,8 @@ def refresh_entity(self, machine_identifier, device, session): def update_platforms(self): """Update the platform entities.""" + _LOGGER.debug("Updating devices") + available_clients = {} new_clients = set() @@ -164,6 +166,11 @@ def update_platforms(self): sessions, ) + @property + def plex_server(self): + """Return the plexapi PlexServer instance.""" + return self._plex_server + @property def friendly_name(self): """Return name of connected Plex server.""" diff --git a/requirements_all.txt b/requirements_all.txt index 6a6c569b4a1963..8f5e83a8e67a97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -973,6 +973,9 @@ plexapi==3.0.6 # homeassistant.components.plex plexauth==0.0.5 +# homeassistant.components.plex +plexwebsocket==0.0.1 + # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51b7ae9d71f798..0af9b338987025 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -345,6 +345,9 @@ plexapi==3.0.6 # homeassistant.components.plex plexauth==0.0.5 +# homeassistant.components.plex +plexwebsocket==0.0.1 + # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 From 4df6b3c76a793495f1425395e215dfdec17c6fbd Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 25 Oct 2019 01:41:13 +0200 Subject: [PATCH 1270/3953] Partially revert tensorflow import move (#28184) * Revert "Refactor imports for tensorflow (#27617)" This reverts commit 5a83a92390e8a3255885198c80622556f886b9b3. * move only some imports to top * fix lint * add comments --- .../components/tensorflow/image_processing.py | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 1f49888cb95252..ea73d52fe4a6fe 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -1,23 +1,12 @@ """Support for performing TensorFlow classification on images.""" +import io import logging import os import sys -import io -import voluptuous as vol + from PIL import Image, ImageDraw import numpy as np - -try: - import cv2 -except ImportError: - cv2 = None - -try: - # Verify that the TensorFlow Object Detection API is pre-installed - import tensorflow as tf # noqa - from object_detection.utils import label_map_util # noqa -except ImportError: - label_map_util = None +import voluptuous as vol from homeassistant.components.image_processing import ( CONF_CONFIDENCE, @@ -98,8 +87,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # append custom model path to sys.path sys.path.append(model_dir) - os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" - if label_map_util is None: + try: + # Verify that the TensorFlow Object Detection API is pre-installed + # pylint: disable=unused-import,unused-variable + os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" + # These imports shouldn't be moved to the top, because they depend on code from the model_dir. + # (The model_dir is created during the manual setup process. See integration docs.) + import tensorflow as tf # noqa + from object_detection.utils import label_map_util # noqa + except ImportError: + # pylint: disable=line-too-long _LOGGER.error( "No TensorFlow Object Detection library found! Install or compile " "for your system following instructions here: " @@ -107,7 +104,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) # noqa return - if cv2 is None: + try: + # Display warning that PIL will be used if no OpenCV is found. + # pylint: disable=unused-import,unused-variable + import cv2 # noqa + except ImportError: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " "PIL at reduced resolution" @@ -282,7 +283,13 @@ def _save_image(self, image, matches, paths): def process_image(self, image): """Process the image.""" - if cv2 is None: + try: + import cv2 # pylint: disable=import-error + + img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) + inp = img[:, :, [2, 1, 0]] # BGR->RGB + inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) + except ImportError: img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") img.thumbnail((460, 460), Image.ANTIALIAS) img_width, img_height = img.size @@ -292,10 +299,6 @@ def process_image(self, image): .astype(np.uint8) ) inp_expanded = np.expand_dims(inp, axis=0) - else: - img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) - inp = img[:, :, [2, 1, 0]] # BGR->RGB - inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) image_tensor = self._graph.get_tensor_by_name("image_tensor:0") boxes = self._graph.get_tensor_by_name("detection_boxes:0") From 637a16799f61f71231dde1e85c8df3b1a8aa2d96 Mon Sep 17 00:00:00 2001 From: gngj Date: Fri, 25 Oct 2019 20:42:23 +0300 Subject: [PATCH 1271/3953] Fix microsoft tts (#28199) * Update pycsspeechtts From 1.0.2 to 1.0.3 as the old one is using an api that doesn't work * Give a option to choose region Api is now region dependent, so gave it a config --- homeassistant/components/microsoft/manifest.json | 2 +- homeassistant/components/microsoft/tts.py | 11 +++++++++-- requirements_all.txt | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 16ae94c212ef62..5834897ee90407 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -3,7 +3,7 @@ "name": "Microsoft", "documentation": "https://www.home-assistant.io/integrations/microsoft", "requirements": [ - "pycsspeechtts==1.0.2" + "pycsspeechtts==1.0.3" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 3536c788bb9d4e..d214f6648ddd4b 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -14,6 +14,7 @@ CONF_VOLUME = "volume" CONF_PITCH = "pitch" CONF_CONTOUR = "contour" +CONF_REGION = "region" _LOGGER = logging.getLogger(__name__) @@ -72,6 +73,7 @@ DEFAULT_VOLUME = 0 DEFAULT_PITCH = "default" DEFAULT_CONTOUR = "" +DEFAULT_REGION = "eastus" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -87,6 +89,7 @@ ), vol.Optional(CONF_PITCH, default=DEFAULT_PITCH): cv.string, vol.Optional(CONF_CONTOUR, default=DEFAULT_CONTOUR): cv.string, + vol.Optional(CONF_REGION, default=DEFAULT_REGION): cv.string, } ) @@ -102,13 +105,16 @@ def get_engine(hass, config): config[CONF_VOLUME], config[CONF_PITCH], config[CONF_CONTOUR], + config[CONF_REGION], ) class MicrosoftProvider(Provider): """The Microsoft speech API provider.""" - def __init__(self, apikey, lang, gender, ttype, rate, volume, pitch, contour): + def __init__( + self, apikey, lang, gender, ttype, rate, volume, pitch, contour, region + ): """Init Microsoft TTS service.""" self._apikey = apikey self._lang = lang @@ -119,6 +125,7 @@ def __init__(self, apikey, lang, gender, ttype, rate, volume, pitch, contour): self._volume = f"{volume}%" self._pitch = pitch self._contour = contour + self._region = region self.name = "Microsoft" @property @@ -138,7 +145,7 @@ def get_tts_audio(self, message, language, options=None): from pycsspeechtts import pycsspeechtts try: - trans = pycsspeechtts.TTSTranslator(self._apikey) + trans = pycsspeechtts.TTSTranslator(self._apikey, self._region) data = trans.speak( language=language, gender=self._gender, diff --git a/requirements_all.txt b/requirements_all.txt index 8f5e83a8e67a97..9b73d46dfbf409 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1132,7 +1132,7 @@ pycomfoconnect==0.3 pycoolmasternet==0.0.4 # homeassistant.components.microsoft -pycsspeechtts==1.0.2 +pycsspeechtts==1.0.3 # homeassistant.components.cups # pycups==1.9.73 From 524f5a7264d11a75ec19e439d1671d0e65a05500 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 25 Oct 2019 19:20:42 +0200 Subject: [PATCH 1272/3953] Updated frontend to 20191025.0 (#28208) --- 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 ae53b972dcaa94..b23d40605dd7a8 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191023.0" + "home-assistant-frontend==20191025.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d2f10c891a95f0..682d9b1e1dbced 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9b73d46dfbf409..9508423927e919 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0af9b338987025..1783f192b24b30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From c456b725fdebd5d32bc29f4367d3e7747c1c8b17 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Oct 2019 10:49:42 -0700 Subject: [PATCH 1273/3953] Bumped version to 0.101.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ce00e38bc4214e..4d858deec8772c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 7ddce1d52b7a967eb0de63f331bdcfd9afe935bb Mon Sep 17 00:00:00 2001 From: Hayley McIldoon Date: Fri, 25 Oct 2019 13:51:35 -0400 Subject: [PATCH 1274/3953] Add device condition support for device_tracker (#28190) --- .../device_tracker/device_condition.py | 81 +++++++++++ .../components/device_tracker/strings.json | 8 ++ .../device_tracker/test_device_condition.py | 126 ++++++++++++++++++ 3 files changed, 215 insertions(+) create mode 100644 homeassistant/components/device_tracker/device_condition.py create mode 100644 homeassistant/components/device_tracker/strings.json create mode 100644 tests/components/device_tracker/test_device_condition.py diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py new file mode 100644 index 00000000000000..6379aca6c0b2a3 --- /dev/null +++ b/homeassistant/components/device_tracker/device_condition.py @@ -0,0 +1,81 @@ +"""Provides device automations for Device tracker.""" +from typing import Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_NOT_HOME, + STATE_HOME, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +CONDITION_TYPES = {"is_home", "is_not_home"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions for Device tracker devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add conditions for each entity that belongs to this integration + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_home", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_not_home", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_home": + state = STATE_HOME + else: + state = STATE_NOT_HOME + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/device_tracker/strings.json b/homeassistant/components/device_tracker/strings.json new file mode 100644 index 00000000000000..7e0691654a09b9 --- /dev/null +++ b/homeassistant/components/device_tracker/strings.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} is home", + "is_not_home": "{entity_name} is not home" + } + } +} \ No newline at end of file diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py new file mode 100644 index 00000000000000..732abccd8ca586 --- /dev/null +++ b/tests/components/device_tracker/test_device_condition.py @@ -0,0 +1,126 @@ +"""The tests for Device tracker device conditions.""" +import pytest + +from homeassistant.components.device_tracker import DOMAIN +from homeassistant.const import STATE_HOME, STATE_NOT_HOME +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a device_tracker.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_not_home", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_home", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("device_tracker.entity", STATE_HOME) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "device_tracker.entity", + "type": "is_home", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_home - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "device_tracker.entity", + "type": "is_not_home", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_not_home - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_home - event - test_event1" + + hass.states.async_set("device_tracker.entity", STATE_NOT_HOME) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_not_home - event - test_event2" From f2d6cc732906d51eccb17350a0b5fd15fb3d080e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20P=C3=B6schl?= Date: Fri, 25 Oct 2019 21:25:27 +0200 Subject: [PATCH 1275/3953] Increased python-eq3bt version to latest (0.1.11) (#28175) --- homeassistant/components/eq3btsmart/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 1065b94c12ac49..e168752d83d3ad 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/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 d3df0a3ab984ba..8e8a72d0181e83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1504,7 +1504,7 @@ python-digitalocean==1.13.2 python-ecobee-api==0.1.4 # homeassistant.components.eq3btsmart -# python-eq3bt==0.1.9 +# python-eq3bt==0.1.11 # homeassistant.components.etherscan python-etherscan-api==0.0.3 From da8a47614282129428e386253ad233f7071d5ced Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 25 Oct 2019 16:34:52 -0400 Subject: [PATCH 1276/3953] Add support for supportedOperations to Alexa.PlaybackController (#28212) * Added support for supportedOperations to Alexa.PlaybackController * Added support for supportedOperations to Alexa.PlaybackController --- .../components/alexa/capabilities.py | 32 +++++++++++++++++++ tests/components/alexa/test_smart_home.py | 9 +++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index deb83813dbc1ca..56a18f1d5211b6 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -17,6 +17,7 @@ STATE_UNKNOWN, ) import homeassistant.components.climate.const as climate +import homeassistant.components.media_player.const as media_player from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER from homeassistant.components import light, fan, cover import homeassistant.util.color as color_util @@ -110,6 +111,11 @@ def configuration(): """Return the Configuration object.""" return [] + @staticmethod + def supported_operations(): + """Return the supportedOperations object.""" + return [] + def serialize_discovery(self): """Serialize according to the Discovery API.""" result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} @@ -150,6 +156,10 @@ def serialize_discovery(self): if instance is not None: result["instance"] = instance + supported_operations = self.supported_operations() + if supported_operations: + result["supportedOperations"] = supported_operations + return result def serialize_properties(self): @@ -484,6 +494,28 @@ def name(self): """Return the Alexa API name of this interface.""" return "Alexa.PlaybackController" + def supported_operations(self): + """Return the supportedOperations object. + + Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind, StartOver, Stop + """ + supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + operations = { + media_player.SUPPORT_NEXT_TRACK: "Next", + media_player.SUPPORT_PAUSE: "Pause", + media_player.SUPPORT_PLAY: "Play", + media_player.SUPPORT_PREVIOUS_TRACK: "Previous", + media_player.SUPPORT_STOP: "Stop", + } + + supported_operations = [] + for operation in operations: + if operation & supported_features: + supported_operations.append(operations[operation]) + + return supported_operations + class AlexaInputController(AlexaCapability): """Implements Alexa.InputController. diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 1de7d404ef66f1..177d52e83de909 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -726,7 +726,7 @@ async def test_media_player(hass): assert appliance["displayCategories"][0] == "TV" assert appliance["friendlyName"] == "Test media player" - assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, "Alexa.InputController", "Alexa.PowerController", @@ -737,6 +737,13 @@ async def test_media_player(hass): "Alexa.ChannelController", ) + playback_capability = get_capability(capabilities, "Alexa.PlaybackController") + assert playback_capability is not None + supported_operations = playback_capability["supportedOperations"] + operations = ["Play", "Pause", "Stop", "Next", "Previous"] + for operation in operations: + assert operation in supported_operations + await assert_power_controller_works( "media_player#test", "media_player.turn_on", "media_player.turn_off", hass ) From f4341c1546eccd1a0999792a85a2e0f58ba9f726 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Oct 2019 04:40:05 +0800 Subject: [PATCH 1277/3953] Fix broken deconz trigger (#28211) --- homeassistant/components/deconz/device_trigger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 27ff6fcd590ad9..2d097d30c0b488 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -235,6 +235,7 @@ async def async_attach_trigger(hass, config, action, automation_info): event_id = deconz_event.serial event_config = { + event.CONF_PLATFORM: "event", event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, } From d44bfa8e8874f6b3c6727998650884f62c8621c7 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 25 Oct 2019 16:42:21 -0400 Subject: [PATCH 1278/3953] Improved Alexa ThermostatController thermostatMode handling (#28176) * Update ThermostatController to map directives to supported modes and add support for CUSTOM mode. * Removed erroneous config value from test. * Removed unnecessary use of a comprehension by dumbing down comment so pylint could comprehend. * Removed erroneous import variable caused by removing erroneous config value from test. * Removed unnecessary use of a comprehension. * Reverted Removal or erroneous import variable and erroneous config value from test. Apparently need for additional tests outside this component. Whoops. --- .../components/alexa/capabilities.py | 27 ++++++++++++ homeassistant/components/alexa/const.py | 3 +- homeassistant/components/alexa/handlers.py | 22 +++++++++- tests/components/alexa/test_capabilities.py | 23 ++++++++--- tests/components/alexa/test_smart_home.py | 41 +++++++++++++++++-- 5 files changed, 105 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 56a18f1d5211b6..52dde74ff3a2a0 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -736,6 +736,33 @@ def get_property(self, name): return {"value": temp, "scale": API_TEMP_UNITS[unit]} + def configuration(self): + """Return configuration object. + + Translates climate HVAC_MODES and PRESETS to supported Alexa ThermostatMode Values. + ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM. + """ + supported_modes = [] + hvac_modes = self.entity.attributes.get(climate.ATTR_HVAC_MODES) + for mode in hvac_modes: + thermostat_mode = API_THERMOSTAT_MODES.get(mode) + if thermostat_mode: + supported_modes.append(thermostat_mode) + + preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES) + for mode in preset_modes: + thermostat_mode = API_THERMOSTAT_PRESETS.get(mode) + if thermostat_mode: + supported_modes.append(thermostat_mode) + + # Return False for supportsScheduling until supported with event listener in handler. + configuration = {"supportsScheduling": False} + + if supported_modes: + configuration["supportedModes"] = supported_modes + + return configuration + class AlexaPowerLevelController(AlexaCapability): """Implements Alexa.PowerLevelController. diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 8d1f0ac95a553d..2a5f9a512b3b40 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -56,9 +56,10 @@ (climate.HVAC_MODE_AUTO, "AUTO"), (climate.HVAC_MODE_OFF, "OFF"), (climate.HVAC_MODE_FAN_ONLY, "OFF"), - (climate.HVAC_MODE_DRY, "OFF"), + (climate.HVAC_MODE_DRY, "CUSTOM"), ] ) +API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} PERCENTAGE_FAN_MAP = { diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 331990dc4a44ba..3dadf51509a807 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -38,6 +38,7 @@ from .const import ( API_TEMP_UNITS, + API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause, @@ -767,11 +768,28 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): raise AlexaUnsupportedThermostatModeError(msg) service = climate.SERVICE_SET_PRESET_MODE - data[climate.ATTR_PRESET_MODE] = climate.PRESET_ECO + data[climate.ATTR_PRESET_MODE] = ha_preset + + elif mode == "CUSTOM": + operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) + custom_mode = directive.payload["thermostatMode"]["customName"] + custom_mode = next( + (k for k, v in API_THERMOSTAT_MODES_CUSTOM.items() if v == custom_mode), + None, + ) + if custom_mode not in operation_list: + msg = ( + f"The requested thermostat mode {mode}: {custom_mode} is not supported" + ) + raise AlexaUnsupportedThermostatModeError(msg) + + service = climate.SERVICE_SET_HVAC_MODE + data[climate.ATTR_HVAC_MODE] = custom_mode else: operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) - ha_mode = next((k for k, v in API_THERMOSTAT_MODES.items() if v == mode), None) + ha_modes = {k: v for k, v in API_THERMOSTAT_MODES.items() if v == mode} + ha_mode = next(iter(set(ha_modes).intersection(operation_list)), None) if ha_mode not in operation_list: msg = f"The requested thermostat mode {mode} is not supported" raise AlexaUnsupportedThermostatModeError(msg) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index be4a2ba48068d9..89815c7254411b 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -472,11 +472,7 @@ async def test_report_climate_state(hass): {"value": 34.0, "scale": "CELSIUS"}, ) - for off_modes in ( - climate.HVAC_MODE_OFF, - climate.HVAC_MODE_FAN_ONLY, - climate.HVAC_MODE_DRY, - ): + for off_modes in (climate.HVAC_MODE_OFF, climate.HVAC_MODE_FAN_ONLY): hass.states.async_set( "climate.downstairs", off_modes, @@ -495,6 +491,23 @@ async def test_report_climate_state(hass): {"value": 34.0, "scale": "CELSIUS"}, ) + # assert dry is reported as CUSTOM + hass.states.async_set( + "climate.downstairs", + "dry", + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 34, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} + ) + hass.states.async_set( "climate.heat", "heat", diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 177d52e83de909..00c762103f30d8 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1267,7 +1267,7 @@ async def test_thermostat(hass): "current_temperature": 75.0, "friendly_name": "Test Thermostat", "supported_features": 1 | 2 | 4 | 128, - "hvac_modes": ["heat", "cool", "auto", "off"], + "hvac_modes": ["off", "heat", "cool", "auto", "dry"], "preset_mode": None, "preset_modes": ["eco"], "min_temp": 50, @@ -1280,7 +1280,7 @@ async def test_thermostat(hass): assert appliance["displayCategories"][0] == "THERMOSTAT" assert appliance["friendlyName"] == "Test Thermostat" - assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, "Alexa.PowerController", "Alexa.ThermostatController", @@ -1299,6 +1299,15 @@ async def test_thermostat(hass): "Alexa.TemperatureSensor", "temperature", {"value": 75.0, "scale": "FAHRENHEIT"} ) + thermostat_capability = get_capability(capabilities, "Alexa.ThermostatController") + assert thermostat_capability is not None + configuration = thermostat_capability["configuration"] + assert configuration["supportsScheduling"] is False + + supported_modes = ["OFF", "HEAT", "COOL", "AUTO", "ECO", "CUSTOM"] + for mode in supported_modes: + assert mode in configuration["supportedModes"] + call, msg = await assert_request_calls_service( "Alexa.ThermostatController", "SetTargetTemperature", @@ -1447,6 +1456,30 @@ async def test_thermostat(hass): properties = ReportedProperties(msg["context"]["properties"]) properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "HEAT") + # Assert we can call custom modes + call, msg = await assert_request_calls_service( + "Alexa.ThermostatController", + "SetThermostatMode", + "climate#test_thermostat", + "climate.set_hvac_mode", + hass, + payload={"thermostatMode": {"value": "CUSTOM", "customName": "DEHUMIDIFY"}}, + ) + assert call.data["hvac_mode"] == "dry" + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + + # assert unsupported custom mode + msg = await assert_request_fails( + "Alexa.ThermostatController", + "SetThermostatMode", + "climate#test_thermostat", + "climate.set_hvac_mode", + hass, + payload={"thermostatMode": {"value": "CUSTOM", "customName": "INVALID"}}, + ) + assert msg["event"]["payload"]["type"] == "UNSUPPORTED_THERMOSTAT_MODE" + msg = await assert_request_fails( "Alexa.ThermostatController", "SetThermostatMode", @@ -1456,7 +1489,6 @@ async def test_thermostat(hass): payload={"thermostatMode": {"value": "INVALID"}}, ) assert msg["event"]["payload"]["type"] == "UNSUPPORTED_THERMOSTAT_MODE" - hass.config.units.temperature_unit = TEMP_CELSIUS call, _ = await assert_request_calls_service( "Alexa.ThermostatController", @@ -1479,6 +1511,9 @@ async def test_thermostat(hass): ) assert call.data["preset_mode"] == "eco" + # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. + hass.config.units.temperature_unit = TEMP_CELSIUS + async def test_exclude_filters(hass): """Test exclusion filters.""" From 7fee44b8c598e5e2b5e5950f5a3bf1078d36cd94 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Oct 2019 04:50:46 +0800 Subject: [PATCH 1279/3953] Add additional device conditions to cover (#27830) * Add additional device conditions to cover * Add default value * Add test * Use numeric_state condition instead of template condition --- .../components/cover/device_condition.py | 210 ++++++-- homeassistant/components/cover/strings.json | 4 +- .../components/cover/test_device_condition.py | 476 +++++++++++++++++- .../custom_components/test/cover.py | 61 +++ 4 files changed, 690 insertions(+), 61 deletions(-) create mode 100644 tests/testing_config/custom_components/test/cover.py diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index 129462047e402a..487f815afb5591 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -4,6 +4,9 @@ from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ABOVE, + CONF_BELOW, CONF_CONDITION, CONF_DOMAIN, CONF_TYPE, @@ -15,20 +18,50 @@ STATE_CLOSING, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers import ( + condition, + config_validation as cv, + entity_registry, + template, +) from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA -from . import DOMAIN +from . import ( + DOMAIN, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, +) -CONDITION_TYPES = {"is_open", "is_closed", "is_opening", "is_closing"} +POSITION_CONDITION_TYPES = {"is_position", "is_tilt_position"} +STATE_CONDITION_TYPES = {"is_open", "is_closed", "is_opening", "is_closing"} -CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( +POSITION_CONDITION_SCHEMA = vol.All( + DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(POSITION_CONDITION_TYPES), + vol.Optional(CONF_ABOVE): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(CONF_BELOW): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + +STATE_CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + vol.Required(CONF_TYPE): vol.In(STATE_CONDITION_TYPES), } ) +CONDITION_SCHEMA = vol.Any(POSITION_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA) + async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: """List device conditions for Cover devices.""" @@ -40,45 +73,92 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict if entry.domain != DOMAIN: continue + state = hass.states.get(entry.entity_id) + if not state or ATTR_SUPPORTED_FEATURES not in state.attributes: + continue + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + supports_open_close = supported_features & (SUPPORT_OPEN | SUPPORT_CLOSE) + # Add conditions for each entity that belongs to this integration - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_open", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_closed", - } - ) - conditions.append( - { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_opening", - } - ) - conditions.append( + if supports_open_close: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_open", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_closed", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_opening", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_closing", + } + ) + if supported_features & SUPPORT_SET_POSITION: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_position", + } + ) + if supported_features & SUPPORT_SET_TILT_POSITION: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_tilt_position", + } + ) + + return conditions + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + if config[CONF_TYPE] not in ["is_position", "is_tilt_position"]: + return {} + + return { + "extra_fields": vol.Schema( { - CONF_CONDITION: "device", - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "is_closing", + vol.Optional(CONF_ABOVE, default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(CONF_BELOW, default=100): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), } ) - - return conditions + } def async_condition_from_config( @@ -87,17 +167,39 @@ def async_condition_from_config( """Create a function to test a device condition.""" if config_validation: config = CONDITION_SCHEMA(config) - if config[CONF_TYPE] == "is_open": - state = STATE_OPEN - elif config[CONF_TYPE] == "is_closed": - state = STATE_CLOSED - elif config[CONF_TYPE] == "is_opening": - state = STATE_OPENING - elif config[CONF_TYPE] == "is_closing": - state = STATE_CLOSING - - def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: - """Test if an entity is a certain state.""" - return condition.state(hass, config[ATTR_ENTITY_ID], state) - - return test_is_state + + if config[CONF_TYPE] in STATE_CONDITION_TYPES: + if config[CONF_TYPE] == "is_open": + state = STATE_OPEN + elif config[CONF_TYPE] == "is_closed": + state = STATE_CLOSED + elif config[CONF_TYPE] == "is_opening": + state = STATE_OPENING + elif config[CONF_TYPE] == "is_closing": + state = STATE_CLOSING + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state + + if config[CONF_TYPE] == "is_position": + position = "current_position" + if config[CONF_TYPE] == "is_tilt_position": + position = "current_tilt_position" + min_pos = config.get(CONF_ABOVE, None) + max_pos = config.get(CONF_BELOW, None) + value_template = template.Template( # type: ignore + f"{{{{ state.attributes.{position} }}}}" + ) + + def template_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: + """Validate template based if-condition.""" + value_template.hass = hass + + return condition.async_numeric_state( + hass, config[ATTR_ENTITY_ID], max_pos, min_pos, value_template + ) + + return template_if diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json index db3ccf9119f3de..e4c72746ee42a4 100644 --- a/homeassistant/components/cover/strings.json +++ b/homeassistant/components/cover/strings.json @@ -4,7 +4,9 @@ "is_open": "{entity_name} is open", "is_closed": "{entity_name} is closed", "is_opening": "{entity_name} is opening", - "is_closing": "{entity_name} is closing" + "is_closing": "{entity_name} is closing", + "is_position": "{entity_name} position is", + "is_tilt_position": "{entity_name} tilt position is" } } } \ No newline at end of file diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 494368f76ff218..8ca912b640ba82 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -2,7 +2,13 @@ import pytest from homeassistant.components.cover import DOMAIN -from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING +from homeassistant.const import ( + CONF_PLATFORM, + STATE_OPEN, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, +) from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry @@ -14,6 +20,7 @@ mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -37,47 +44,298 @@ def calls(hass): async def test_get_conditions(hass, device_reg, entity_reg): """Test we get the expected conditions from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[0] + config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + expected_conditions = [ { "condition": "device", "domain": DOMAIN, "type": "is_open", "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", }, { "condition": "device", "domain": DOMAIN, "type": "is_closed", "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", }, { "condition": "device", "domain": DOMAIN, "type": "is_opening", "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", }, { "condition": "device", "domain": DOMAIN, "type": "is_closing", "device_id": device_entry.id, - "entity_id": f"{DOMAIN}.test_5678", + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", }, ] conditions = await async_get_device_automations(hass, "condition", device_entry.id) assert_lists_same(conditions, expected_conditions) +async def test_get_conditions_set_pos(hass, device_reg, entity_reg): + """Test we get the expected conditions from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_open", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_position", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_get_conditions_set_tilt_pos(hass, device_reg, entity_reg): + """Test we get the expected conditions from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[2] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_open", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_tilt_position", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[0] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert len(conditions) == 4 + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == {"extra_fields": []} + + +async def test_get_condition_capabilities_set_pos(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "name": "above", + "optional": True, + "type": "integer", + "default": 0, + "valueMax": 100, + "valueMin": 0, + }, + { + "name": "below", + "optional": True, + "type": "integer", + "default": 100, + "valueMax": 100, + "valueMin": 0, + }, + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert len(conditions) == 5 + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + if condition["type"] == "is_position": + assert capabilities == expected_capabilities + else: + assert capabilities == {"extra_fields": []} + + +async def test_get_condition_capabilities_set_tilt_pos(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[2] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "name": "above", + "optional": True, + "type": "integer", + "default": 0, + "valueMax": 100, + "valueMin": 0, + }, + { + "name": "below", + "optional": True, + "type": "integer", + "default": 100, + "valueMax": 100, + "valueMin": 0, + }, + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert len(conditions) == 5 + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + if condition["type"] == "is_tilt_position": + assert capabilities == expected_capabilities + else: + assert capabilities == {"extra_fields": []} + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" hass.states.async_set("cover.entity", STATE_OPEN) @@ -188,3 +446,209 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 4 assert calls[3].data["some"] == "is_closing - event - test_event4" + + +async def test_if_position(hass, calls): + """Test for position conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_position", + "above": 45, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_position", + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_position", + "above": 45, + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_gt_45_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[0].data["some"] == "is_pos_gt_45 - event - test_event1" + assert calls[1].data["some"] == "is_pos_lt_90 - event - test_event2" + assert calls[2].data["some"] == "is_pos_gt_45_lt_90 - event - test_event3" + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 45} + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "is_pos_lt_90 - event - test_event2" + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 90} + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 5 + assert calls[4].data["some"] == "is_pos_gt_45 - event - test_event1" + + +async def test_if_tilt_position(hass, calls): + """Test for tilt position conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[2] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_tilt_position", + "above": 45, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_tilt_position", + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "is_tilt_position", + "above": 45, + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_pos_gt_45_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[0].data["some"] == "is_pos_gt_45 - event - test_event1" + assert calls[1].data["some"] == "is_pos_lt_90 - event - test_event2" + assert calls[2].data["some"] == "is_pos_gt_45_lt_90 - event - test_event3" + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 45} + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "is_pos_lt_90 - event - test_event2" + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 90} + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 5 + assert calls[4].data["some"] == "is_pos_gt_45 - event - test_event1" diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py new file mode 100644 index 00000000000000..d7c771e2b2821b --- /dev/null +++ b/tests/testing_config/custom_components/test/cover.py @@ -0,0 +1,61 @@ +""" +Provide a mock cover platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.cover import CoverDevice +from tests.common import MockEntity + + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + [] + if empty + else [ + MockCover(name=f"Simple cover", is_on=True, unique_id=f"unique_cover"), + MockCover( + name=f"Set position cover", + is_on=True, + unique_id=f"unique_set_pos_cover", + current_cover_position=50, + ), + MockCover( + name=f"Set tilt position cover", + is_on=True, + unique_id=f"unique_set_pos_tilt_cover", + current_cover_tilt_position=50, + ), + ] + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(ENTITIES) + + +class MockCover(MockEntity, CoverDevice): + """Mock Cover class.""" + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + return False + + @property + def current_cover_position(self): + """Return current position of cover.""" + return self._handle("current_cover_position") + + @property + def current_cover_tilt_position(self): + """Return current position of cover tilt.""" + return self._handle("current_cover_tilt_position") From d6654eaecb651ba895fa56cb7b917456898c87da Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 25 Oct 2019 17:53:33 -0400 Subject: [PATCH 1280/3953] Implement Alexa.PlaybackStateReporter Interface for alexa (#28215) --- .../components/alexa/capabilities.py | 40 ++++++++++++++++++- homeassistant/components/alexa/entities.py | 2 + tests/components/alexa/test_capabilities.py | 24 +++++++++++ tests/components/alexa/test_smart_home.py | 2 + 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 52dde74ff3a2a0..b5b7fa88ef859c 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -12,9 +12,11 @@ STATE_LOCKED, STATE_OFF, STATE_ON, + STATE_PAUSED, + STATE_PLAYING, STATE_UNAVAILABLE, - STATE_UNLOCKED, STATE_UNKNOWN, + STATE_UNLOCKED, ) import homeassistant.components.climate.const as climate import homeassistant.components.media_player.const as media_player @@ -1137,3 +1139,39 @@ def name(self): def capability_proactively_reported(self): """Return True for proactively reported capability.""" return True + + +class AlexaPlaybackStateReporter(AlexaCapability): + """Implements Alexa.PlaybackStateReporter. + + https://developer.amazon.com/docs/device-apis/alexa-playbackstatereporter.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.PlaybackStateReporter" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "playbackState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "playbackState": + raise UnsupportedProperty(name) + + playback_state = self.entity.state + if playback_state == STATE_PLAYING: + return {"state": "PLAYING"} + if playback_state == STATE_PAUSED: + return {"state": "PAUSED"} + + return {"state": "STOPPED"} diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index e52e1b4f87eab6..ef6c29020530b5 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -46,6 +46,7 @@ AlexaMotionSensor, AlexaPercentageController, AlexaPlaybackController, + AlexaPlaybackStateReporter, AlexaPowerController, AlexaPowerLevelController, AlexaRangeController, @@ -422,6 +423,7 @@ def interfaces(self): ) if supported & playback_features: yield AlexaPlaybackController(self.entity) + yield AlexaPlaybackStateReporter(self.entity) if supported & media_player.SUPPORT_SELECT_SOURCE: yield AlexaInputController(self.entity) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 89815c7254411b..d2f6cddc522c36 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -17,6 +17,11 @@ from homeassistant.components.climate import const as climate from homeassistant.components.alexa import smart_home from homeassistant.components.alexa.errors import UnsupportedProperty +from homeassistant.components.media_player.const import ( + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_STOP, +) from tests.common import async_mock_service from . import ( @@ -642,3 +647,22 @@ async def test_report_alarm_control_panel_state(hass): properties = await reported_properties(hass, "alarm_control_panel.disarmed") properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") + + +async def test_report_playback_state(hass): + """Test PlaybackStateReporter implements playbackState property.""" + hass.states.async_set( + "media_player.test", + "off", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP, + "volume_level": 0.75, + }, + ) + + properties = await reported_properties(hass, "media_player.test") + + properties.assert_equal( + "Alexa.PlaybackStateReporter", "playbackState", {"state": "STOPPED"} + ) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 00c762103f30d8..3994c3d9f5d58c 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -733,6 +733,7 @@ async def test_media_player(hass): "Alexa.Speaker", "Alexa.StepSpeaker", "Alexa.PlaybackController", + "Alexa.PlaybackStateReporter", "Alexa.EndpointHealth", "Alexa.ChannelController", ) @@ -954,6 +955,7 @@ async def test_media_player_power(hass): "Alexa.Speaker", "Alexa.StepSpeaker", "Alexa.PlaybackController", + "Alexa.PlaybackStateReporter", "Alexa.EndpointHealth", "Alexa.ChannelController", ) From 475b43500ae0ef1c56fcdd45cabf8318362bcd22 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Oct 2019 06:55:42 +0800 Subject: [PATCH 1281/3953] Add above and below to sensor condition extra_fields (#27364) * Add above and below to sensor condition extra_fields * Change unit_of_measurement to suffix in extra_fields * Check if sensor has unit when getting capabilities * Improve tests --- .../components/device_automation/__init__.py | 6 +- .../components/sensor/device_condition.py | 27 +++++++ .../components/sensor/device_trigger.py | 8 +- .../sensor/test_device_condition.py | 81 +++++++++++++++++++ .../components/sensor/test_device_trigger.py | 35 ++++++++ 5 files changed, 155 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 0be1c3eb1dd63c..80e6403329531a 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -156,7 +156,11 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom # The device automation has no capabilities return {} - capabilities = await getattr(platform, function_name)(hass, automation) + try: + capabilities = await getattr(platform, function_name)(hass, automation) + except InvalidDeviceAutomationConfig: + return {} + capabilities = capabilities.copy() extra_fields = capabilities.get("extra_fields") diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 26479807991392..259fb5dbab90b9 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -2,6 +2,9 @@ from typing import Dict, List import voluptuous as vol +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.core import HomeAssistant from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -141,3 +144,27 @@ def async_condition_from_config( numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW] return condition.async_numeric_state_from_config(numeric_state_config) + + +async def async_get_condition_capabilities(hass, config): + """List condition capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + raise InvalidDeviceAutomationConfig + + return { + "extra_fields": vol.Schema( + { + vol.Optional( + CONF_ABOVE, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional( + CONF_BELOW, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + } + ) + } diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index b462124165af17..73e55340da9b6a 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -3,6 +3,9 @@ import homeassistant.components.automation.numeric_state as numeric_state_automation from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -146,9 +149,12 @@ async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" state = hass.states.get(config[CONF_ENTITY_ID]) unit_of_measurement = ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else "" + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None ) + if not state or not unit_of_measurement: + raise InvalidDeviceAutomationConfig + return { "extra_fields": vol.Schema( { diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index e28e487f4ef2bd..f3ff15c3ad9386 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -14,6 +14,7 @@ mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES @@ -73,6 +74,86 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["battery"].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "description": {"suffix": "%"}, + "name": "above", + "optional": True, + "type": "float", + }, + { + "description": {"suffix": "%"}, + "name": "below", + "optional": True, + "type": "float", + }, + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert len(conditions) == 1 + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + +async def test_get_condition_capabilities_none(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + conditions = [ + { + "condition": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": "sensor.beer", + "type": "is_battery_level", + }, + { + "condition": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": platform.ENTITIES["none"].entity_id, + "type": "is_battery_level", + }, + ] + + expected_capabilities = {} + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state_not_above_below(hass, calls, caplog): """Test for bad value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index a21839fcebc292..b7a921fff18034 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -124,6 +124,41 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities +async def test_get_trigger_capabilities_none(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + triggers = [ + { + "platform": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": "sensor.beer", + "type": "is_battery_level", + }, + { + "platform": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": platform.ENTITIES["none"].entity_id, + "type": "is_battery_level", + }, + ] + + expected_capabilities = {} + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + async def test_if_fires_not_on_above_below(hass, calls, caplog): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") From 08cc9fd3754c32cb5dc9f65a4c0ef0accb80700a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Oct 2019 16:04:24 -0700 Subject: [PATCH 1282/3953] Add cloud account linking support (#28210) * Add cloud account linking support * Update account_link.py --- homeassistant/bootstrap.py | 2 + homeassistant/components/cloud/__init__.py | 8 +- .../components/cloud/account_link.py | 132 +++++++++++++++ homeassistant/components/cloud/const.py | 1 + homeassistant/components/cloud/manifest.json | 2 +- .../components/somfy/.translations/en.json | 5 + homeassistant/components/somfy/strings.json | 29 ++-- .../helpers/config_entry_oauth2_flow.py | 41 ++++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_account_link.py | 160 ++++++++++++++++++ .../helpers/test_config_entry_oauth2_flow.py | 42 +++++ 13 files changed, 407 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/cloud/account_link.py create mode 100644 tests/components/cloud/test_account_link.py diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 6118f4f2bd734b..312c739cd72034 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -33,6 +33,8 @@ "recorder", # To make sure we forward data to other instances "mqtt_eventstream", + # To provide account link implementations + "cloud", } diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index a2c79fdc0a721b..2d5a2c8b448660 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -20,7 +20,7 @@ from homeassistant.loader import bind_hass from homeassistant.util.aiohttp import MockRequest -from . import http_api +from . import account_link, http_api from .client import CloudClient from .const import ( CONF_ACME_DIRECTORY_SERVER, @@ -38,6 +38,7 @@ CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, CONF_USER_POOL_ID, + CONF_ACCOUNT_LINK_URL, DOMAIN, MODE_DEV, MODE_PROD, @@ -101,6 +102,7 @@ vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(), vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(), + vol.Optional(CONF_ACCOUNT_LINK_URL): vol.Url(), } ) }, @@ -168,7 +170,6 @@ def is_cloudhook_request(request): async def async_setup(hass, config): """Initialize the Home Assistant cloud.""" - # Process configs if DOMAIN in config: kwargs = dict(config[DOMAIN]) @@ -248,4 +249,7 @@ async def _on_connect(): cloud.iot.register_on_connect(_on_connect) await http_api.async_setup(hass) + + account_link.async_setup(hass) + return True diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py new file mode 100644 index 00000000000000..6fbfcc8723ba03 --- /dev/null +++ b/homeassistant/components/cloud/account_link.py @@ -0,0 +1,132 @@ +"""Account linking via the cloud.""" +import asyncio +import logging +from typing import Any + +from hass_nabucasa import account_link + +from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import event, config_entry_oauth2_flow + +from .const import DOMAIN + +DATA_SERVICES = "cloud_account_link_services" +CACHE_TIMEOUT = 3600 +PATCH_VERSION = int(PATCH_VERSION.split(".")[0]) +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_setup(hass: HomeAssistant): + """Set up cloud account link.""" + config_entry_oauth2_flow.async_add_implementation_provider( + hass, DOMAIN, async_provide_implementation + ) + + +async def async_provide_implementation(hass: HomeAssistant, domain: str): + """Provide an implementation for a domain.""" + services = await _get_services(hass) + + for service in services: + if service["service"] == domain and _is_older(service["min_version"]): + return CloudOAuth2Implementation(hass, domain) + + return + + +@callback +def _is_older(version: str) -> bool: + """Test if a version is older than the current HA version.""" + version_parts = version.split(".") + + if len(version_parts) != 3: + return False + + try: + version_parts = [int(val) for val in version_parts] + except ValueError: + return False + + cur_version_parts = [MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION] + + return version_parts <= cur_version_parts + + +async def _get_services(hass): + """Get the available services.""" + services = hass.data.get(DATA_SERVICES) + + if services is not None: + return services + + services = await account_link.async_fetch_available_services(hass.data[DOMAIN]) + + hass.data[DATA_SERVICES] = services + + @callback + def clear_services(_now): + """Clear services cache.""" + hass.data.pop(DATA_SERVICES, None) + + event.async_call_later(hass, CACHE_TIMEOUT, clear_services) + + return services + + +class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implementation): + """Cloud implementation of the OAuth2 flow.""" + + def __init__(self, hass: HomeAssistant, service: str): + """Initialize cloud OAuth2 implementation.""" + self.hass = hass + self.service = service + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Home Assistant Cloud" + + @property + def domain(self) -> str: + """Domain that is providing the implementation.""" + return DOMAIN + + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize.""" + helper = account_link.AuthorizeAccountHelper( + self.hass.data[DOMAIN], self.service + ) + authorize_url = await helper.async_get_authorize_url() + + async def await_tokens(): + """Wait for tokens and pass them on when received.""" + try: + tokens = await helper.async_get_tokens() + + except asyncio.TimeoutError: + _LOGGER.info("Timeout fetching tokens for flow %s", flow_id) + except account_link.AccountLinkException as err: + _LOGGER.info( + "Failed to fetch tokens for flow %s: %s", flow_id, err.code + ) + else: + await self.hass.config_entries.flow.async_configure( + flow_id=flow_id, user_input=tokens + ) + + self.hass.async_create_task(await_tokens()) + + return authorize_url + + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve external data to tokens.""" + # We already passed in tokens + return external_data + + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh a token.""" + return await account_link.async_fetch_access_token( + self.hass.data[DOMAIN], self.service, token["refresh_token"] + ) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 6495cba23b7291..262f84a85e653f 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -37,6 +37,7 @@ CONF_ACME_DIRECTORY_SERVER = "acme_directory_server" CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url" CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url" +CONF_ACCOUNT_LINK_URL = "account_link_url" MODE_DEV = "development" MODE_PROD = "production" diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index c8fa6884563d41..9e9b77287ae53d 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/integrations/cloud", - "requirements": ["hass-nabucasa==0.22"], + "requirements": ["hass-nabucasa==0.23"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/somfy/.translations/en.json b/homeassistant/components/somfy/.translations/en.json index d4155915636c96..3b2f2e6beafe14 100644 --- a/homeassistant/components/somfy/.translations/en.json +++ b/homeassistant/components/somfy/.translations/en.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Successfully authenticated with Somfy." }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/strings.json b/homeassistant/components/somfy/strings.json index d4155915636c96..81308ba18afb8b 100644 --- a/homeassistant/components/somfy/strings.json +++ b/homeassistant/components/somfy/strings.json @@ -1,13 +1,18 @@ { - "config": { - "abort": { - "already_setup": "You can only configure one Somfy account.", - "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Somfy component is not configured. Please follow the documentation." - }, - "create_entry": { - "default": "Successfully authenticated with Somfy." - }, - "title": "Somfy" - } -} \ No newline at end of file + "config": { + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "abort": { + "already_setup": "You can only configure one Somfy account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Somfy component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Somfy." + }, + "title": "Somfy" + } +} diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 7fb954378ee70d..d3db8febcb2cdc 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -8,7 +8,7 @@ import asyncio from abc import ABCMeta, ABC, abstractmethod import logging -from typing import Optional, Any, Dict, cast +from typing import Optional, Any, Dict, cast, Awaitable, Callable import time import async_timeout @@ -28,6 +28,7 @@ DATA_JWT_SECRET = "oauth2_jwt_secret" DATA_VIEW_REGISTERED = "oauth2_view_reg" DATA_IMPLEMENTATIONS = "oauth2_impl" +DATA_PROVIDERS = "oauth2_providers" AUTH_CALLBACK_PATH = "/auth/external/callback" @@ -291,11 +292,23 @@ async def async_get_implementations( hass: HomeAssistant, domain: str ) -> Dict[str, AbstractOAuth2Implementation]: """Return OAuth2 implementations for specified domain.""" - return cast( + registered = cast( Dict[str, AbstractOAuth2Implementation], hass.data.setdefault(DATA_IMPLEMENTATIONS, {}).get(domain, {}), ) + if DATA_PROVIDERS not in hass.data: + return registered + + registered = dict(registered) + + for provider_domain, get_impl in hass.data[DATA_PROVIDERS].items(): + implementation = await get_impl(hass, domain) + if implementation is not None: + registered[provider_domain] = implementation + + return registered + async def async_get_config_entry_implementation( hass: HomeAssistant, config_entry: config_entries.ConfigEntry @@ -310,6 +323,23 @@ async def async_get_config_entry_implementation( return implementation +@callback +def async_add_implementation_provider( + hass: HomeAssistant, + provider_domain: str, + async_provide_implementation: Callable[ + [HomeAssistant, str], Awaitable[Optional[AbstractOAuth2Implementation]] + ], +) -> None: + """Add an implementation provider. + + If no implementation found, return None. + """ + hass.data.setdefault(DATA_PROVIDERS, {})[ + provider_domain + ] = async_provide_implementation + + class OAuth2AuthorizeCallbackView(HomeAssistantView): """OAuth2 Authorization Callback View.""" @@ -355,9 +385,14 @@ def __init__( self.config_entry = config_entry self.implementation = implementation + @property + def token(self) -> dict: + """Return the current token.""" + return cast(dict, self.config_entry.data["token"]) + async def async_ensure_token_valid(self) -> None: """Ensure that the current token is valid.""" - token = self.config_entry.data["token"] + token = self.token if token["expires_at"] > time.time(): return diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fbfa1dbf67b1c8..87878b49615127 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 -hass-nabucasa==0.22 +hass-nabucasa==0.23 home-assistant-frontend==20191025.0 importlib-metadata==0.23 jinja2>=2.10.1 diff --git a/requirements_all.txt b/requirements_all.txt index 8e8a72d0181e83..1039b53f4f3e12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -616,7 +616,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.22 +hass-nabucasa==0.23 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f55cb1aa1b184..db5c1a491cb804 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -225,7 +225,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.22 +hass-nabucasa==0.23 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py new file mode 100644 index 00000000000000..60116895beb408 --- /dev/null +++ b/tests/components/cloud/test_account_link.py @@ -0,0 +1,160 @@ +"""Test account link services.""" +import asyncio +import logging +from time import time +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import data_entry_flow, config_entries +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.components.cloud import account_link +from homeassistant.util.dt import utcnow +from tests.common import mock_coro, async_fire_time_changed, mock_platform + + +TEST_DOMAIN = "oauth2_test" + + +@pytest.fixture +def flow_handler(hass): + """Return a registered config flow.""" + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + class TestFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Test flow handler.""" + + DOMAIN = TEST_DOMAIN + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}): + yield TestFlowHandler + + +async def test_setup_provide_implementation(hass): + """Test that we provide implementations.""" + account_link.async_setup(hass) + + with patch( + "homeassistant.components.cloud.account_link._get_services", + side_effect=lambda _: mock_coro( + [ + {"service": "test", "min_version": "0.1.0"}, + {"service": "too_new", "min_version": "100.0.0"}, + ] + ), + ): + assert ( + await config_entry_oauth2_flow.async_get_implementations( + hass, "non_existing" + ) + == {} + ) + assert ( + await config_entry_oauth2_flow.async_get_implementations(hass, "too_new") + == {} + ) + implementations = await config_entry_oauth2_flow.async_get_implementations( + hass, "test" + ) + + assert "cloud" in implementations + assert implementations["cloud"].domain == "cloud" + assert implementations["cloud"].service == "test" + assert implementations["cloud"].hass is hass + + +async def test_get_services_cached(hass): + """Test that we cache services.""" + hass.data["cloud"] = None + + services = 1 + + with patch.object(account_link, "CACHE_TIMEOUT", 0), patch( + "hass_nabucasa.account_link.async_fetch_available_services", + side_effect=lambda _: mock_coro(services), + ) as mock_fetch: + assert await account_link._get_services(hass) == 1 + + services = 2 + + assert len(mock_fetch.mock_calls) == 1 + assert await account_link._get_services(hass) == 1 + + services = 3 + hass.data.pop(account_link.DATA_SERVICES) + assert await account_link._get_services(hass) == 3 + + services = 4 + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + # Check cache purged + assert await account_link._get_services(hass) == 4 + + +async def test_implementation(hass, flow_handler): + """Test Cloud OAuth2 implementation.""" + hass.data["cloud"] = None + + impl = account_link.CloudOAuth2Implementation(hass, "test") + assert impl.name == "Home Assistant Cloud" + assert impl.domain == "cloud" + + flow_handler.async_register_implementation(hass, impl) + + flow_finished = asyncio.Future() + + helper = Mock( + async_get_authorize_url=Mock(return_value=mock_coro("http://example.com/auth")), + async_get_tokens=Mock(return_value=flow_finished), + ) + + with patch( + "hass_nabucasa.account_link.AuthorizeAccountHelper", return_value=helper + ): + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == "http://example.com/auth" + + flow_finished.set_result( + { + "refresh_token": "mock-refresh", + "access_token": "mock-access", + "expires_in": 10, + "token_type": "bearer", + } + ) + await hass.async_block_till_done() + + # Flow finished! + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == "cloud" + + expires_at = result["data"]["token"].pop("expires_at") + assert round(expires_at - time()) == 10 + + assert result["data"]["token"] == { + "refresh_token": "mock-refresh", + "access_token": "mock-access", + "token_type": "bearer", + "expires_in": 10, + } + + entry = hass.config_entries.async_entries(TEST_DOMAIN)[0] + + assert ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + is impl + ) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index e47dd834bf7dfe..773dfa09375d22 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -264,3 +264,45 @@ async def test_oauth_session(hass, flow_handler, local_impl, aioclient_mock): assert config_entry.data["token"]["expires_in"] == 100 assert config_entry.data["token"]["random_other_data"] == "should_stay" assert round(config_entry.data["token"]["expires_at"] - now) == 100 + + +async def test_implementation_provider(hass, local_impl): + """Test providing an implementation provider.""" + assert ( + await config_entry_oauth2_flow.async_get_implementations(hass, TEST_DOMAIN) + == {} + ) + + mock_domain_with_impl = "some_domain" + + config_entry_oauth2_flow.async_register_implementation( + hass, mock_domain_with_impl, local_impl + ) + + assert await config_entry_oauth2_flow.async_get_implementations( + hass, mock_domain_with_impl + ) == {TEST_DOMAIN: local_impl} + + provider_source = {} + + async def async_provide_implementation(hass, domain): + """Mock implementation provider.""" + return provider_source.get(domain) + + config_entry_oauth2_flow.async_add_implementation_provider( + hass, "cloud", async_provide_implementation + ) + + assert await config_entry_oauth2_flow.async_get_implementations( + hass, mock_domain_with_impl + ) == {TEST_DOMAIN: local_impl} + + provider_source[ + mock_domain_with_impl + ] = config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, "cloud", CLIENT_ID, CLIENT_SECRET, AUTHORIZE_URL, TOKEN_URL + ) + + assert await config_entry_oauth2_flow.async_get_implementations( + hass, mock_domain_with_impl + ) == {TEST_DOMAIN: local_impl, "cloud": provider_source[mock_domain_with_impl]} From 7096826d1d255778bc4fe6c86895d524e82884fd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 26 Oct 2019 00:32:20 +0000 Subject: [PATCH 1283/3953] [ci skip] Translation update --- .../cert_expiry/.translations/lb.json | 4 +- .../coolmaster/.translations/ca.json | 7 +++- .../coolmaster/.translations/lb.json | 23 +++++++++++ .../coolmaster/.translations/ru.json | 23 +++++++++++ .../components/cover/.translations/en.json | 4 +- .../device_tracker/.translations/ca.json | 8 ++++ .../device_tracker/.translations/en.json | 8 ++++ .../device_tracker/.translations/ru.json | 8 ++++ .../homematicip_cloud/.translations/ru.json | 2 +- .../huawei_lte/.translations/ca.json | 11 ++++++ .../huawei_lte/.translations/fr.json | 39 +++++++++++++++++++ .../huawei_lte/.translations/lb.json | 39 +++++++++++++++++++ .../huawei_lte/.translations/ru.json | 39 +++++++++++++++++++ .../media_player/.translations/ca.json | 11 ++++++ .../media_player/.translations/en.json | 11 ++++++ .../media_player/.translations/ru.json | 11 ++++++ .../components/sensor/.translations/nl.json | 2 +- .../components/solarlog/.translations/lb.json | 21 ++++++++++ .../transmission/.translations/lb.json | 5 ++- .../transmission/.translations/ru.json | 5 ++- .../components/withings/.translations/ca.json | 1 + .../components/withings/.translations/fr.json | 7 ++++ .../components/withings/.translations/lb.json | 7 ++++ .../components/withings/.translations/nl.json | 5 +++ .../components/withings/.translations/ru.json | 7 ++++ 25 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/coolmaster/.translations/lb.json create mode 100644 homeassistant/components/coolmaster/.translations/ru.json create mode 100644 homeassistant/components/device_tracker/.translations/ca.json create mode 100644 homeassistant/components/device_tracker/.translations/en.json create mode 100644 homeassistant/components/device_tracker/.translations/ru.json create mode 100644 homeassistant/components/huawei_lte/.translations/fr.json create mode 100644 homeassistant/components/huawei_lte/.translations/lb.json create mode 100644 homeassistant/components/huawei_lte/.translations/ru.json create mode 100644 homeassistant/components/media_player/.translations/ca.json create mode 100644 homeassistant/components/media_player/.translations/en.json create mode 100644 homeassistant/components/media_player/.translations/ru.json create mode 100644 homeassistant/components/solarlog/.translations/lb.json diff --git a/homeassistant/components/cert_expiry/.translations/lb.json b/homeassistant/components/cert_expiry/.translations/lb.json index 9620526e363d9d..14d12967a384ce 100644 --- a/homeassistant/components/cert_expiry/.translations/lb.json +++ b/homeassistant/components/cert_expiry/.translations/lb.json @@ -4,10 +4,12 @@ "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert" }, "error": { + "certificate_error": "Zertifikat konnt net valid\u00e9iert ginn", "certificate_fetch_failed": "Kann keen Zertifikat vun d\u00ebsen Host a Port recuper\u00e9ieren", "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen.", "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert", - "resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn" + "resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn", + "wrong_host": "Zertifikat entspr\u00e9cht net den Numm vum Apparat" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/ca.json b/homeassistant/components/coolmaster/.translations/ca.json index c79397b0cc5ce4..e66d887f3e8a96 100644 --- a/homeassistant/components/coolmaster/.translations/ca.json +++ b/homeassistant/components/coolmaster/.translations/ca.json @@ -1,9 +1,14 @@ { "config": { + "error": { + "connection_error": "No s'ha pogut connectar amb la inst\u00e0ncia de CoolMasterNet. Comprova l'amfitri\u00f3.", + "no_units": "No s'ha pogut trobar cap unitat d'HVAC a l'amfitri\u00f3 de CoolMasterNet." + }, "step": { "user": { "data": { - "host": "Amfitri\u00f3" + "host": "Amfitri\u00f3", + "off": "Es pot apagar" } } }, diff --git a/homeassistant/components/coolmaster/.translations/lb.json b/homeassistant/components/coolmaster/.translations/lb.json new file mode 100644 index 00000000000000..ed54abac03e667 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Feeler beim verbanne mat der CoolMasterNet Instanz. Iwwerpr\u00e9ift w.e.g. \u00e4ren Apparat.", + "no_units": "Konnt keng HVAC Eenheeten am CoolMasterNet Apparat fannen." + }, + "step": { + "user": { + "data": { + "cool": "\u00cbnnerst\u00ebtzt KillModus", + "dry": "\u00cbnnerst\u00ebtzt Dr\u00e9che Modus", + "fan_only": "\u00cbnnerst\u00ebtzt n\u00ebmmen Ventilatiouns Modus", + "heat": "\u00cbnnerst\u00ebtzt H\u00ebtzt Modus", + "heat_cool": "\u00cbnnerst\u00ebtzt automateschen H\u00ebtzt/Kill Modus", + "host": "Apparat", + "off": "Kann ausgeschalt ginn" + }, + "title": "CoolMasterNet Verbindungs Detailer ariichten" + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/ru.json b/homeassistant/components/coolmaster/.translations/ru.json new file mode 100644 index 00000000000000..4c2f74440cd785 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430.", + "no_units": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u044f, \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0438\u0438 \u0438 \u043a\u043e\u043d\u0434\u0438\u0446\u0438\u043e\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f." + }, + "step": { + "user": { + "data": { + "cool": "\u0420\u0435\u0436\u0438\u043c \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u044f", + "dry": "\u0420\u0435\u0436\u0438\u043c \u043e\u0441\u0443\u0448\u0435\u043d\u0438\u044f", + "fan_only": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0438\u0438", + "heat": "\u0420\u0435\u0436\u0438\u043c \u043e\u0431\u043e\u0433\u0440\u0435\u0432\u0430", + "heat_cool": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "host": "\u0425\u043e\u0441\u0442", + "off": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" + }, + "title": "CoolMasterNet" + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/en.json b/homeassistant/components/cover/.translations/en.json index f9f47be3104173..eaac0d557ef8aa 100644 --- a/homeassistant/components/cover/.translations/en.json +++ b/homeassistant/components/cover/.translations/en.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} is closed", "is_closing": "{entity_name} is closing", "is_open": "{entity_name} is open", - "is_opening": "{entity_name} is opening" + "is_opening": "{entity_name} is opening", + "is_position": "{entity_name} position is", + "is_tilt_position": "{entity_name} tilt position is" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/ca.json b/homeassistant/components/device_tracker/.translations/ca.json new file mode 100644 index 00000000000000..de5aed41e3c239 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/ca.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \u00e9s a casa", + "is_not_home": "{entity_name} no \u00e9s a casa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/en.json b/homeassistant/components/device_tracker/.translations/en.json new file mode 100644 index 00000000000000..25045e62b15ca0 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/en.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} is home", + "is_not_home": "{entity_name} is not home" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/ru.json b/homeassistant/components/device_tracker/.translations/ru.json new file mode 100644 index 00000000000000..50a48ce942b431 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/ru.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \u0434\u043e\u043c\u0430", + "is_not_home": "{entity_name} \u043d\u0435 \u0434\u043e\u043c\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 57ab265d1c2093..35f52a7b284b23 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP.", - "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", diff --git a/homeassistant/components/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json index 5b31875586dfb7..6dc88f213235a9 100644 --- a/homeassistant/components/huawei_lte/.translations/ca.json +++ b/homeassistant/components/huawei_lte/.translations/ca.json @@ -20,9 +20,20 @@ "url": "URL", "username": "Nom d'usuari" }, + "description": "Introdueix les dades d\u2019acc\u00e9s del dispositiu. El nom d\u2019usuari i contrasenya s\u00f3n opcionals, per\u00f2 habiliten m\u00e9s funcions de la integraci\u00f3. D'altra banda, (mentre la integraci\u00f3 estigui activa) l'\u00fas d'una connexi\u00f3 autoritzada pot causar problemes per accedir a la interf\u00edcie web del dispositiu des de fora de Home Assistant i viceversa.", "title": "Con de Huawei LTE" } }, "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Destinataris de notificacions SMS", + "track_new_devices": "Segueix dispositius nous" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/fr.json b/homeassistant/components/huawei_lte/.translations/fr.json new file mode 100644 index 00000000000000..19f3330535600a --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/fr.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Cet appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "connection_failed": "La connexion a \u00e9chou\u00e9", + "incorrect_password": "Mot de passe incorrect", + "incorrect_username": "Nom d'utilisateur incorrect", + "incorrect_username_or_password": "identifiant ou mot de passe incorrect", + "invalid_url": "URL invalide", + "login_attempts_exceeded": "Nombre maximal de tentatives de connexion d\u00e9pass\u00e9, veuillez r\u00e9essayer ult\u00e9rieurement", + "response_error": "Erreur inconnue de l'appareil", + "unknown_connection_error": "Erreur inconnue lors de la connexion \u00e0 l'appareil" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "url": "URL", + "username": "Nom d'utilisateur" + }, + "description": "Entrez les d\u00e9tails d'acc\u00e8s au p\u00e9riph\u00e9rique. La sp\u00e9cification du nom d'utilisateur et du mot de passe est facultative, mais permet de prendre en charge davantage de fonctionnalit\u00e9s d'int\u00e9gration. En revanche, l\u2019utilisation d\u2019une connexion autoris\u00e9e peut entra\u00eener des probl\u00e8mes d\u2019acc\u00e8s \u00e0 l\u2019interface Web du p\u00e9riph\u00e9rique depuis l\u2019assistant externe lorsque l\u2019int\u00e9gration est active et inversement.", + "title": "Configurer Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Destinataires des notifications SMS", + "track_new_devices": "Suivre de nouveaux appareils" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/lb.json b/homeassistant/components/huawei_lte/.translations/lb.json new file mode 100644 index 00000000000000..2b90245e929ab7 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/lb.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebsen Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "connection_failed": "Feeler bei der Verbindung", + "incorrect_password": "Ong\u00ebltegt Passwuert", + "incorrect_username": "Ong\u00ebltege Benotzernumm", + "incorrect_username_or_password": "Ong\u00ebltege Benotzernumm oder Passwuert", + "invalid_url": "Ong\u00eblteg URL", + "login_attempts_exceeded": "Maximal Login Versich iwwerschratt, w.e.g. m\u00e9i sp\u00e9it nach eng K\u00e9ier", + "response_error": "Onbekannte Feeler vum Apparat", + "unknown_connection_error": "Onbekannte Feeler beim verbannen mam Apparat" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "url": "URL", + "username": "Benotzernumm" + }, + "description": "Gitt Detailer fir den Acc\u00e8s op den Apparat an. Benotzernumm a Passwuert si fakultativ, erm\u00e9iglecht awer d'\u00cbnnerst\u00ebtzung fir m\u00e9i Integratiouns Optiounen. Op der anerer S\u00e4it kann d'Benotzung vun enger autoris\u00e9ierter Verbindung Problemer mam Acc\u00e8s zum Web Interface vum Apparat ausserhalb vum Home Assistant verursaachen, w\u00e4rend d'Integratioun aktiv ass.", + "title": "Huawei LTE ariichten" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Empf\u00e4nger vun SMS Notifikatioune", + "track_new_devices": "Nei Apparater verfollegen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json new file mode 100644 index 00000000000000..1a0c5cc29ad254 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "connection_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "incorrect_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", + "incorrect_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", + "incorrect_username_or_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "invalid_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", + "login_attempts_exceeded": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0432\u0445\u043e\u0434\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", + "response_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unknown_connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0423\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u043d\u043e \u044d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438. \u0421 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c \u043a \u0432\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437 Home Assistant, \u043a\u043e\u0433\u0434\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442.", + "title": "Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 SMS-\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", + "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/ca.json b/homeassistant/components/media_player/.translations/ca.json new file mode 100644 index 00000000000000..4889c1781c3152 --- /dev/null +++ b/homeassistant/components/media_player/.translations/ca.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} est\u00e0 inactiu", + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s", + "is_paused": "{entity_name} est\u00e0 en pausa", + "is_playing": "{entity_name} est\u00e0 reproduint" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/en.json b/homeassistant/components/media_player/.translations/en.json new file mode 100644 index 00000000000000..472cb98f2836d8 --- /dev/null +++ b/homeassistant/components/media_player/.translations/en.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} is idle", + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on", + "is_paused": "{entity_name} is paused", + "is_playing": "{entity_name} is playing" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/ru.json b/homeassistant/components/media_player/.translations/ru.json new file mode 100644 index 00000000000000..2b459ccab05ced --- /dev/null +++ b/homeassistant/components/media_player/.translations/ru.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f", + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_paused": "{entity_name} \u043d\u0430 \u043f\u0430\u0443\u0437\u0435", + "is_playing": "{entity_name} \u0432\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442 \u043c\u0435\u0434\u0438\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json index 33a7d837d555de..796e9c97071daf 100644 --- a/homeassistant/components/sensor/.translations/nl.json +++ b/homeassistant/components/sensor/.translations/nl.json @@ -9,7 +9,7 @@ "is_signal_strength": "{entity_name} signaalsterkte", "is_temperature": "{entity_name} temperatuur", "is_timestamp": "{entity_name} tijdstip", - "is_value": "{entity_name} waarde" + "is_value": "Huidige {entity_name} waarde" }, "trigger_type": { "battery_level": "{entity_name} batterijniveau", diff --git a/homeassistant/components/solarlog/.translations/lb.json b/homeassistant/components/solarlog/.translations/lb.json new file mode 100644 index 00000000000000..8bfaca69d94d22 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "cannot_connect": "Feeler beim verbannen, iwwerpr\u00e9ift w.e.g d'Adresse vum Apparat" + }, + "step": { + "user": { + "data": { + "host": "De Numm oder IP Adresse vun \u00e4rem Solar-Log Apparat", + "name": "Prefix dee fir \u00e4r Solar-Log Sensoren soll benotz ginn" + }, + "title": "D\u00e9fin\u00e9iert \u00e4r Solar-Log Verbindung" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/lb.json b/homeassistant/components/transmission/.translations/lb.json index 6cc611fcb71550..a012bcd8cde233 100644 --- a/homeassistant/components/transmission/.translations/lb.json +++ b/homeassistant/components/transmission/.translations/lb.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." }, "error": { "cannot_connect": "Kann sech net mam Server verbannen.", + "name_exists": "Numm g\u00ebtt et schonn", "wrong_credentials": "Falsche Benotzernumm oder Passwuert" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Intervalle vun de Mise \u00e0 jour" }, - "description": "Optioune fir Transmission konfigur\u00e9ieren" + "description": "Optioune fir Transmission konfigur\u00e9ieren", + "title": "Optioune fir Transmission konfigur\u00e9ieren" } } } diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json index 23f1ceaaa94e58..222737b90c9af7 100644 --- a/homeassistant/components/transmission/.translations/ru.json +++ b/homeassistant/components/transmission/.translations/ru.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" }, - "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission" + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission", + "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission" } } } diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json index 89dae4caa1b8f9..21c31ccdaaff9f 100644 --- a/homeassistant/components/withings/.translations/ca.json +++ b/homeassistant/components/withings/.translations/ca.json @@ -11,6 +11,7 @@ "data": { "profile": "Perfil" }, + "description": "Quin perfil has seleccionat al lloc web de Withings? \u00c9s important que els perfils coincideixin sin\u00f3, les dades no s\u2019etiquetaran correctament.", "title": "Perfil d'usuari." }, "user": { diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json index ad715d54eb1df5..ed3a43ae2955fd 100644 --- a/homeassistant/components/withings/.translations/fr.json +++ b/homeassistant/components/withings/.translations/fr.json @@ -7,6 +7,13 @@ "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Quel profil avez-vous s\u00e9lectionn\u00e9 sur le site Withings? Il est important que les profils correspondent, sinon les donn\u00e9es seront mal \u00e9tiquet\u00e9es.", + "title": "Profil utilisateur" + }, "user": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json index 5ca969f039102b..e6ef316548b585 100644 --- a/homeassistant/components/withings/.translations/lb.json +++ b/homeassistant/components/withings/.translations/lb.json @@ -7,6 +7,13 @@ "default": "Erfollegr\u00e4ich mam ausgewielte Profile mat Withings authentifiz\u00e9iert." }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "W\u00e9ie Profil hutt dir op der Withings Webs\u00e4it ausgewielt? Et ass wichteg dass Profiller passen, soss ginn Donn\u00e9e\u00eb falsch gekennzeechent.", + "title": "Benotzer Profil." + }, "user": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json index 3776621bec208e..2ca98656db7544 100644 --- a/homeassistant/components/withings/.translations/nl.json +++ b/homeassistant/components/withings/.translations/nl.json @@ -7,6 +7,11 @@ "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." }, "step": { + "profile": { + "data": { + "profile": "Profiel" + } + }, "user": { "data": { "profile": "Profiel" diff --git a/homeassistant/components/withings/.translations/ru.json b/homeassistant/components/withings/.translations/ru.json index c6c621fbdf33ee..750e306c89a507 100644 --- a/homeassistant/components/withings/.translations/ru.json +++ b/homeassistant/components/withings/.translations/ru.json @@ -7,6 +7,13 @@ "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "step": { + "profile": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" + }, + "description": "\u041a\u0430\u043a\u043e\u0439 \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u0412\u044b \u0432\u044b\u0431\u0440\u0430\u043b\u0438 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 Withings? \u0412\u0430\u0436\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0444\u0438\u043b\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u043b\u0438, \u0438\u043d\u0430\u0447\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u044b.", + "title": "Withings" + }, "user": { "data": { "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" From 2baee4ac3ef8ada5e9f2987395f33d9038b2c768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 26 Oct 2019 13:29:36 +0300 Subject: [PATCH 1284/3953] Add Huawei LTE mobile data switch support (#28188) * Add Huawei LTE mobile data switch support * Remove stale comment * Do HA state updates in base entity --- .../components/huawei_lte/__init__.py | 9 +- homeassistant/components/huawei_lte/const.py | 5 +- homeassistant/components/huawei_lte/switch.py | 109 ++++++++++++++++++ 3 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/huawei_lte/switch.py diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 18f7035a885268..e224f45ba90e80 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -23,6 +23,7 @@ from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT from homeassistant.const import ( CONF_PASSWORD, @@ -48,6 +49,7 @@ KEY_DEVICE_BASIC_INFORMATION, KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, + KEY_DIALUP_MOBILE_DATASWITCH, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_WLAN_HOST_LIST, UPDATE_OPTIONS_SIGNAL, @@ -158,6 +160,7 @@ def get_data(key: str, func: Callable[[None], Any]) -> None: self.subscriptions.pop(KEY_DEVICE_BASIC_INFORMATION, None) get_data(KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information) get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) + get_data(KEY_DIALUP_MOBILE_DATASWITCH, self.client.dial_up.mobile_dataswitch) get_data( KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics ) @@ -273,7 +276,7 @@ def signal_update() -> None: router.subscriptions.clear() # Forward config entry setup to platforms - for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN): + for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, domain) ) @@ -316,7 +319,7 @@ async def async_unload_entry( """Unload config entry.""" # Forward config entry unload to platforms - for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN): + for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): await hass.config_entries.async_forward_entry_unload(config_entry, domain) # Forget about the router and invoke its cleanup @@ -419,7 +422,7 @@ async def async_added_to_hass(self) -> None: async def _async_maybe_update(self, url: str) -> None: """Update state if the update signal comes from our router.""" if url == self.router.url: - await self.async_update() + self.async_schedule_update_ha_state(True) async def _async_maybe_update_options(self, config_entry: ConfigEntry) -> None: """Update options if the update signal comes from our router.""" diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 18b8d1a90e13f9..3d99261e6ede82 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -13,6 +13,7 @@ KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" +KEY_DIALUP_MOBILE_DATASWITCH = "dialup_mobile_dataswitch" KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics" KEY_WLAN_HOST_LIST = "wlan_host_list" @@ -24,4 +25,6 @@ KEY_MONITORING_TRAFFIC_STATISTICS, } -ALL_KEYS = DEVICE_TRACKER_KEYS | SENSOR_KEYS +SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH} + +ALL_KEYS = DEVICE_TRACKER_KEYS | SENSOR_KEYS | SWITCH_KEYS diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py new file mode 100644 index 00000000000000..bff82227b80cc4 --- /dev/null +++ b/homeassistant/components/huawei_lte/switch.py @@ -0,0 +1,109 @@ +"""Support for Huawei LTE switches.""" + +import logging +from typing import Optional + +import attr + +from homeassistant.components.switch import ( + DEVICE_CLASS_SWITCH, + DOMAIN as SWITCH_DOMAIN, + SwitchDevice, +) +from homeassistant.const import CONF_URL +from . import HuaweiLteBaseEntity +from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up from config entry.""" + router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] + switches = [] + + if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): + switches.append(HuaweiLteMobileDataSwitch(router)) + + async_add_entities(switches, True) + + +@attr.s +class HuaweiLteBaseSwitch(HuaweiLteBaseEntity, SwitchDevice): + """Huawei LTE switch device base class.""" + + key: str + item: str + _raw_state: Optional[str] = attr.ib(init=False, default=None) + + def _turn(self, state: bool) -> None: + raise NotImplementedError + + def turn_on(self, **kwargs): + """Turn switch on.""" + self._turn(state=True) + + def turn_off(self, **kwargs): + """Turn switch off.""" + self._turn(state=False) + + @property + def device_class(self): + """Return device class.""" + return DEVICE_CLASS_SWITCH + + async def async_added_to_hass(self): + """Subscribe to needed data on add.""" + await super().async_added_to_hass() + self.router.subscriptions[self.key].add(f"{SWITCH_DOMAIN}/{self.item}") + + async def async_will_remove_from_hass(self): + """Unsubscribe from needed data on remove.""" + await super().async_will_remove_from_hass() + self.router.subscriptions[self.key].remove(f"{SWITCH_DOMAIN}/{self.item}") + + async def async_update(self): + """Update state.""" + try: + value = self.router.data[self.key][self.item] + except KeyError: + _LOGGER.debug("%s[%s] not in data", self.key, self.item) + self._available = False + return + self._available = True + self._raw_state = str(value) + + +@attr.s +class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch): + """Huawei LTE mobile data switch device.""" + + def __attrs_post_init__(self): + """Initialize identifiers.""" + self.key = KEY_DIALUP_MOBILE_DATASWITCH + self.item = "dataswitch" + + @property + def _entity_name(self) -> str: + return "Mobile data" + + @property + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" + + @property + def is_on(self) -> bool: + """Return whether the switch is on.""" + return self._raw_state == "1" + + def _turn(self, state: bool) -> None: + value = 1 if state else 0 + self.router.client.dial_up.set_mobile_dataswitch(dataswitch=value) + self._raw_state = str(value) + self.schedule_update_ha_state() + + @property + def icon(self): + """Return switch icon.""" + return "mdi:signal" if self.is_on else "mdi:signal-off" From bb8f139716b24f21b1caca9368c26afef89ca9fa Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 26 Oct 2019 13:45:42 +0200 Subject: [PATCH 1285/3953] Upgrade speedtest-cli to 2.1.2 (#28216) --- homeassistant/components/speedtestdotnet/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index b35df8ee7c8fbd..b32026d86ef7ab 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -3,7 +3,7 @@ "name": "Speedtestdotnet", "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", "requirements": [ - "speedtest-cli==2.1.1" + "speedtest-cli==2.1.2" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 1039b53f4f3e12..a0c4b747f41343 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1813,7 +1813,7 @@ somecomfort==0.5.2 somfy-mylink-synergy==1.0.6 # homeassistant.components.speedtestdotnet -speedtest-cli==2.1.1 +speedtest-cli==2.1.2 # homeassistant.components.spider spiderpy==1.3.1 From 82ed84ba43a6ce9ac950495d1877d7cbb4d1cb3f Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Sat, 26 Oct 2019 16:27:21 -0400 Subject: [PATCH 1286/3953] Bump env_canada to 0.0.27 (#28239) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index c62e1e356b6031..4d1a80946635b4 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/integrations/environment_canada", "requirements": [ - "env_canada==0.0.25" + "env_canada==0.0.27" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9508423927e919..cd4a6fceb85ffb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -456,7 +456,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.25 +env_canada==0.0.27 # homeassistant.components.envirophat # envirophat==0.0.6 From 868f88a4e05a9f22cd58fa317a272f3898e7b0c4 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 27 Oct 2019 00:32:10 +0000 Subject: [PATCH 1287/3953] [ci skip] Translation update --- .../binary_sensor/.translations/fr.json | 2 +- .../cert_expiry/.translations/pl.json | 4 +- .../coolmaster/.translations/ca.json | 8 +++- .../coolmaster/.translations/pl.json | 18 +++++++++ .../components/cover/.translations/ca.json | 4 +- .../components/cover/.translations/ru.json | 4 +- .../cover/.translations/zh-Hant.json | 4 +- .../components/deconz/.translations/ca.json | 1 + .../device_tracker/.translations/zh-Hant.json | 8 ++++ .../components/glances/.translations/pl.json | 18 ++++++++- .../huawei_lte/.translations/zh-Hant.json | 39 +++++++++++++++++++ .../media_player/.translations/zh-Hant.json | 11 ++++++ .../components/sensor/.translations/fr.json | 4 +- .../components/solarlog/.translations/pl.json | 21 ++++++++++ .../components/somfy/.translations/ca.json | 5 +++ .../components/somfy/.translations/fr.json | 5 +++ .../components/somfy/.translations/ru.json | 5 +++ .../somfy/.translations/zh-Hant.json | 5 +++ .../withings/.translations/zh-Hant.json | 7 ++++ 19 files changed, 163 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/coolmaster/.translations/pl.json create mode 100644 homeassistant/components/device_tracker/.translations/zh-Hant.json create mode 100644 homeassistant/components/huawei_lte/.translations/zh-Hant.json create mode 100644 homeassistant/components/media_player/.translations/zh-Hant.json create mode 100644 homeassistant/components/solarlog/.translations/pl.json diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json index 4d9bcefbe660d1..65abfbcd0bd880 100644 --- a/homeassistant/components/binary_sensor/.translations/fr.json +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -86,7 +86,7 @@ "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", - "turned_on": "{entity_name} activ\u00e9", + "turned_on": "{entity_name} est activ\u00e9", "unsafe": "{entity_name} est devenu dangereux", "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" } diff --git a/homeassistant/components/cert_expiry/.translations/pl.json b/homeassistant/components/cert_expiry/.translations/pl.json index 162c8bf8a0aae8..e594ed66a3fadf 100644 --- a/homeassistant/components/cert_expiry/.translations/pl.json +++ b/homeassistant/components/cert_expiry/.translations/pl.json @@ -4,10 +4,12 @@ "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana" }, "error": { + "certificate_error": "Nie mo\u017cna zweryfikowa\u0107 certyfikatu", "certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu", "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z tym hostem", "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", - "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107" + "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107", + "wrong_host": "Certyfikat nie pasuje do nazwy hosta" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/ca.json b/homeassistant/components/coolmaster/.translations/ca.json index e66d887f3e8a96..65816e696fe12a 100644 --- a/homeassistant/components/coolmaster/.translations/ca.json +++ b/homeassistant/components/coolmaster/.translations/ca.json @@ -7,9 +7,15 @@ "step": { "user": { "data": { + "cool": "Suporta mode refredar", + "dry": "Suporta mode assecar", + "fan_only": "Suporta nom\u00e9s mode ventiladoci\u00f3", + "heat": "Suporta mode escalfar", + "heat_cool": "Suporta mode escalfar/refredar autom\u00e0tic", "host": "Amfitri\u00f3", "off": "Es pot apagar" - } + }, + "title": "Configuraci\u00f3 de la connexi\u00f3 amb CoolMasterNet." } }, "title": "CoolMasterNet" diff --git a/homeassistant/components/coolmaster/.translations/pl.json b/homeassistant/components/coolmaster/.translations/pl.json new file mode 100644 index 00000000000000..8568eac2e556ab --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 z CoolMasterNet. Sprawd\u017a adres hosta.", + "no_units": "Nie mo\u017cna znale\u017a\u0107 urz\u0105dze\u0144 HVAC na ho\u015bcie CoolMasterNet." + }, + "step": { + "user": { + "data": { + "cool": "Obs\u0142uga trybu ch\u0142odzenia", + "dry": "Obs\u0142uga trybu osuszania", + "fan_only": "Obs\u0142uga trybu \"tylko wentylator\"", + "heat": "Obs\u0142uga tryb grzania" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ca.json b/homeassistant/components/cover/.translations/ca.json index ffa9ca1a927284..5602ca98ec2cd5 100644 --- a/homeassistant/components/cover/.translations/ca.json +++ b/homeassistant/components/cover/.translations/ca.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} est\u00e0 tancat/da", "is_closing": "{entity_name} est\u00e0 tancan't-se", "is_open": "{entity_name} est\u00e0 obert/a", - "is_opening": "{entity_name} s'est\u00e0 obrint" + "is_opening": "{entity_name} s'est\u00e0 obrint", + "is_position": "La posici\u00f3 de {entity_name} \u00e9s", + "is_tilt_position": "La posici\u00f3 d'inclinaci\u00f3 de {entity_name} \u00e9s" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json index 46456bb9464a2e..5e89cc6e82e541 100644 --- a/homeassistant/components/cover/.translations/ru.json +++ b/homeassistant/components/cover/.translations/ru.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", - "is_opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f" + "is_opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "is_position": "{entity_name} \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438", + "is_tilt_position": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430\u043a\u043b\u043e\u043d\u0430" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/zh-Hant.json b/homeassistant/components/cover/.translations/zh-Hant.json index 9723d1a0dd6743..63910cd208c586 100644 --- a/homeassistant/components/cover/.translations/zh-Hant.json +++ b/homeassistant/components/cover/.translations/zh-Hant.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} \u5df2\u95dc\u9589", "is_closing": "{entity_name} \u6b63\u5728\u95dc\u9589", "is_open": "{entity_name} \u5df2\u958b\u555f", - "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f" + "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f", + "is_position": "{entity_name} \u4f4d\u7f6e\u70ba", + "is_tilt_position": "{entity_name} \u6a19\u984c\u4f4d\u7f6e\u70ba" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 41f257e2a787f9..2900128eedb17b 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -65,6 +65,7 @@ "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades consecutives", "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades consecutives", "remote_button_rotated": "Bot\u00f3 \"{subtype}\" girat", + "remote_button_rotation_stopped": "La rotaci\u00f3 del bot\u00f3 \"{subtype}\" s'ha aturat", "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives", diff --git a/homeassistant/components/device_tracker/.translations/zh-Hant.json b/homeassistant/components/device_tracker/.translations/zh-Hant.json new file mode 100644 index 00000000000000..4092031434ce97 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \u5728\u5bb6", + "is_not_home": "{entity_name} \u4e0d\u5728\u5bb6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/pl.json b/homeassistant/components/glances/.translations/pl.json index 21052c7acdcf3c..f53d4d413e0a45 100644 --- a/homeassistant/components/glances/.translations/pl.json +++ b/homeassistant/components/glances/.translations/pl.json @@ -15,8 +15,22 @@ "password": "Has\u0142o", "port": "Port", "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z systemem Glances", - "username": "Nazwa u\u017cytkownika" - } + "username": "Nazwa u\u017cytkownika", + "verify_ssl": "Sprawd\u017a certyfikacj\u0119 systemu", + "version": "Glances wersja API (2 lub 3)" + }, + "title": "Konfiguracja Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + }, + "description": "Konfiguracja opcji dla Glances" } } } diff --git a/homeassistant/components/huawei_lte/.translations/zh-Hant.json b/homeassistant/components/huawei_lte/.translations/zh-Hant.json new file mode 100644 index 00000000000000..795df4e3d6f862 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/zh-Hant.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "connection_failed": "\u9023\u7dda\u5931\u6557", + "incorrect_password": "\u5bc6\u78bc\u932f\u8aa4", + "incorrect_username": "\u4f7f\u7528\u8005\u540d\u7a31\u932f\u8aa4", + "incorrect_username_or_password": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4", + "invalid_url": "\u7db2\u5740\u7121\u6548", + "login_attempts_exceeded": "\u5df2\u9054\u5617\u8a66\u767b\u5165\u6700\u5927\u6b21\u6578\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66", + "response_error": "\u4f86\u81ea\u8a2d\u5099\u672a\u77e5\u932f\u8aa4", + "unknown_connection_error": "\u9023\u7dda\u81f3\u8a2d\u5099\u672a\u77e5\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "url": "\u7db2\u5740", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165\u8a2d\u5099\u5b58\u53d6\u8a73\u7d30\u8cc7\u6599\u3002\u6307\u5b9a\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u70ba\u9078\u9805\u8f38\u5165\uff0c\u4f46\u958b\u555f\u5c07\u652f\u63f4\u66f4\u591a\u6574\u5408\u529f\u80fd\u3002\u6b64\u5916\uff0c\u4f7f\u7528\u6388\u6b0a\u9023\u7dda\uff0c\u53ef\u80fd\u5c0e\u81f4\u6574\u5408\u555f\u7528\u5f8c\uff0c\u7531\u5916\u90e8\u9023\u7dda\u81f3 Home Assistant \u8a2d\u5099 Web \u4ecb\u9762\u51fa\u73fe\u67d0\u4e9b\u554f\u984c\uff0c\u53cd\u4e4b\u4ea6\u7136\u3002", + "title": "\u8a2d\u5b9a Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", + "track_new_devices": "\u8ffd\u8e64\u65b0\u8a2d\u5099" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/zh-Hant.json b/homeassistant/components/media_player/.translations/zh-Hant.json new file mode 100644 index 00000000000000..abd2f75950b6c1 --- /dev/null +++ b/homeassistant/components/media_player/.translations/zh-Hant.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u9592\u7f6e", + "is_off": "{entity_name} \u95dc\u9589", + "is_on": "{entity_name} \u958b\u555f", + "is_paused": "{entity_name} \u5df2\u66ab\u505c", + "is_playing": "{entity_name} \u6b63\u5728\u64ad\u653e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json index 56725a59e2174d..f0060f151dcc7f 100644 --- a/homeassistant/components/sensor/.translations/fr.json +++ b/homeassistant/components/sensor/.translations/fr.json @@ -9,7 +9,7 @@ "is_signal_strength": "{entity_name} force du signal", "is_temperature": "La temp\u00e9rature de {entity_name}", "is_timestamp": "{entity_name} horodatage", - "is_value": "{entity_name} valeur" + "is_value": "La valeur actuelle de {entity_name} " }, "trigger_type": { "battery_level": "Le niveau de la batterie de {entity_name}", @@ -20,7 +20,7 @@ "signal_strength": "{entity_name} force du signal", "temperature": "La temp\u00e9rature de {entity_name}", "timestamp": "{entity_name} horodatage", - "value": "{entity_name} valeur" + "value": "Changements de valeur de {entity_name} " } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/pl.json b/homeassistant/components/solarlog/.translations/pl.json new file mode 100644 index 00000000000000..251d183b361c79 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, sprawd\u017a adres hosta" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP urz\u0105dzenia Solar-Log", + "name": "Prefiks dla sensor\u00f3w Solar-Log" + }, + "title": "Zdefiniuj po\u0142\u0105czenie z Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/ca.json b/homeassistant/components/somfy/.translations/ca.json index 14f707ac046734..0ca526fde69c53 100644 --- a/homeassistant/components/somfy/.translations/ca.json +++ b/homeassistant/components/somfy/.translations/ca.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Somfy." }, + "step": { + "pick_implementation": { + "title": "Tria del m\u00e8tode d'autenticaci\u00f3" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/fr.json b/homeassistant/components/somfy/.translations/fr.json index ba873c4f029786..29a3eb778532aa 100644 --- a/homeassistant/components/somfy/.translations/fr.json +++ b/homeassistant/components/somfy/.translations/fr.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s avec Somfy." }, + "step": { + "pick_implementation": { + "title": "Choisir la m\u00e9thode d'authentification" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/ru.json b/homeassistant/components/somfy/.translations/ru.json index 0c8778dc2e5afb..0cdb43e4241a4a 100644 --- a/homeassistant/components/somfy/.translations/ru.json +++ b/homeassistant/components/somfy/.translations/ru.json @@ -8,6 +8,11 @@ "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/zh-Hant.json b/homeassistant/components/somfy/.translations/zh-Hant.json index f7f7f4a1d8e33b..3b11e6ef6e0d21 100644 --- a/homeassistant/components/somfy/.translations/zh-Hant.json +++ b/homeassistant/components/somfy/.translations/zh-Hant.json @@ -8,6 +8,11 @@ "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Somfy \u8a2d\u5099\u3002" }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json index f01109b2d621d8..77f3efbd4b90ad 100644 --- a/homeassistant/components/withings/.translations/zh-Hant.json +++ b/homeassistant/components/withings/.translations/zh-Hant.json @@ -7,6 +7,13 @@ "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u8a2d\u5099\u3002" }, "step": { + "profile": { + "data": { + "profile": "\u500b\u4eba\u8a2d\u5b9a" + }, + "description": "\u65bc Withings \u7db2\u7ad9\u6240\u9078\u64c7\u7684\u500b\u4eba\u8a2d\u5b9a\u70ba\u4f55\uff1f\u5047\u5982\u500b\u4eba\u8a2d\u5b9a\u4e0d\u7b26\u5408\u7684\u8a71\uff0c\u8cc7\u6599\u5c07\u6703\u6a19\u793a\u932f\u8aa4\u3002", + "title": "\u500b\u4eba\u8a2d\u5b9a\u3002" + }, "user": { "data": { "profile": "\u500b\u4eba\u8a2d\u5b9a" From 2747f08385ef6560347db54d77a663caa5db423f Mon Sep 17 00:00:00 2001 From: Floris Van der krieken Date: Sun, 27 Oct 2019 05:18:23 +0100 Subject: [PATCH 1288/3953] Add available state to unifiled integration (#28189) * Added Unifi Led * fixed manifest * fixed style issue * removed unused setting * added sugested changes. * fixed order * fixed settings that are required * Fix review issues * fix variable name that was too short * Testing something * Reverted to a previous version for testing * Reverted testing changes. * Add available status and increase version of unifiled package version. * No io in init function. --- homeassistant/components/unifiled/light.py | 7 +++++++ homeassistant/components/unifiled/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py index 3dd1a8d5dc9975..6b0b1e2edf12db 100644 --- a/homeassistant/components/unifiled/light.py +++ b/homeassistant/components/unifiled/light.py @@ -57,6 +57,7 @@ def __init__(self, light, api): self._name = light["name"] self._unique_id = light["id"] self._state = light["status"]["output"] + self._available = light["isOnline"] self._brightness = self._api.convertfrom100to255(light["status"]["led"]) self._features = SUPPORT_BRIGHTNESS @@ -65,6 +66,11 @@ def name(self): """Return the display name of this light.""" return self._name + @property + def available(self): + """Return the available state of this light.""" + return self._available + @property def brightness(self): """Return the brightness name of this light.""" @@ -103,3 +109,4 @@ def update(self): self._brightness = self._api.convertfrom100to255( self._api.getlightbrightness(self._unique_id) ) + self._available = self._api.getlightavailable(self._unique_id) diff --git a/homeassistant/components/unifiled/manifest.json b/homeassistant/components/unifiled/manifest.json index fbf05470c6dafe..927798bd9ce591 100644 --- a/homeassistant/components/unifiled/manifest.json +++ b/homeassistant/components/unifiled/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/unifiled", "dependencies": [], "codeowners": ["@florisvdk"], - "requirements": ["unifiled==0.10"] + "requirements": ["unifiled==0.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index a0c4b747f41343..d02d11d5f5819b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1922,7 +1922,7 @@ twentemilieu==0.1.0 twilio==6.32.0 # homeassistant.components.unifiled -unifiled==0.10 +unifiled==0.11 # homeassistant.components.upcloud upcloud-api==0.4.3 From 7e862e4d92b059963afca01d00f1c4249cd302a7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 Oct 2019 10:04:43 +0100 Subject: [PATCH 1289/3953] Update praw to 6.4.0 (#27324) * Update praw to 6.4.0 * Update requirements_test_all.txt * Fix docstrings * Update tests --- homeassistant/components/reddit/manifest.json | 2 +- homeassistant/components/reddit/sensor.py | 5 +---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/reddit/test_sensor.py | 12 +++++++----- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index 55baecc486cdcb..8ab4b686da74c1 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -3,7 +3,7 @@ "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", "requirements": [ - "praw==6.3.1" + "praw==6.4.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index f9c8140f60db71..82f622b968eec4 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import praw import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -51,8 +52,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Reddit sensor platform.""" - import praw - subreddits = config[CONF_SUBREDDITS] user_agent = "{}_home_assistant_sensor".format(config[CONF_USERNAME]) limit = config[CONF_MAXIMUM] @@ -117,8 +116,6 @@ def icon(self): def update(self): """Update data from Reddit API.""" - import praw - self._subreddit_data = [] try: diff --git a/requirements_all.txt b/requirements_all.txt index d02d11d5f5819b..a4621d0e45fd67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -990,7 +990,7 @@ pocketcasts==0.1 postnl_api==1.0.2 # homeassistant.components.reddit -praw==6.3.1 +praw==6.4.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db5c1a491cb804..69eaf2d29220a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -354,7 +354,7 @@ plexwebsocket==0.0.1 pmsensor==0.4 # homeassistant.components.reddit -praw==6.3.1 +praw==6.4.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index 0a0247e2585ec0..a421f6f417cc0a 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -3,6 +3,7 @@ import unittest from unittest.mock import patch +from homeassistant.components.reddit import sensor as reddit_sensor from homeassistant.components.reddit.sensor import ( DOMAIN, ATTR_SUBREDDIT, @@ -98,7 +99,7 @@ def __init__(self, d): class MockPraw: - """Mock class for tmdbsimple library.""" + """Mock class for Reddit library.""" def __init__( self, @@ -112,7 +113,7 @@ def __init__( self._data = MOCK_RESULTS def subreddit(self, subreddit: str): - """Return an instance of a sunbreddit.""" + """Return an instance of a subreddit.""" return MockSubreddit(subreddit, self._data) @@ -160,8 +161,9 @@ def tearDown(self): # pylint: disable=invalid-name @MockDependency("praw") @patch("praw.Reddit", new=MockPraw) def test_setup_with_valid_config(self, mock_praw): - """Test the platform setup with movie configuration.""" - setup_component(self.hass, "sensor", VALID_CONFIG) + """Test the platform setup with Reddit configuration.""" + with patch.object(reddit_sensor, "praw", mock_praw): + setup_component(self.hass, "sensor", VALID_CONFIG) state = self.hass.states.get("sensor.reddit_worldnews") assert int(state.state) == MOCK_RESULTS_LENGTH @@ -186,6 +188,6 @@ def test_setup_with_valid_config(self, mock_praw): @MockDependency("praw") @patch("praw.Reddit", new=MockPraw) def test_setup_with_invalid_config(self, mock_praw): - """Test the platform setup with invalid movie configuration.""" + """Test the platform setup with invalid Reddit configuration.""" setup_component(self.hass, "sensor", INVALID_SORT_BY_CONFIG) assert not self.hass.states.get("sensor.reddit_worldnews") From a9db2ead3341cc90da482947c9666e6f6c27e326 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 Oct 2019 12:39:36 +0100 Subject: [PATCH 1290/3953] Suppress traceback (fixes #28243) (#28262) --- homeassistant/components/iss/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py index 002b2e958f7a1e..3b8e222c912401 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/binary_sensor.py @@ -120,6 +120,6 @@ def update(self): self.next_rise = iss.next_rise(self.latitude, self.longitude) self.number_of_people_in_space = iss.number_of_people_in_space() self.position = iss.current_location() - except requests.exceptions.HTTPError as error: - _LOGGER.error(error) + except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError): + _LOGGER.error("Unable to retrieve data") return False From 6ac7796fb7599565cf44e90fad1efefa56c43d5a Mon Sep 17 00:00:00 2001 From: ZiroNL Date: Sun, 27 Oct 2019 13:07:44 +0100 Subject: [PATCH 1291/3953] Add charset to imap component. (#28258) --- homeassistant/components/imap/sensor.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index a10fefa1b163b6..db2f528153b734 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -23,6 +23,7 @@ CONF_SERVER = "server" CONF_FOLDER = "folder" CONF_SEARCH = "search" +CONF_CHARSET = "charset" DEFAULT_PORT = 993 @@ -35,6 +36,7 @@ vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_SERVER): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_CHARSET, default="utf-8"): cv.string, vol.Optional(CONF_FOLDER, default="INBOX"): cv.string, vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): cv.string, } @@ -49,6 +51,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= config.get(CONF_PASSWORD), config.get(CONF_SERVER), config.get(CONF_PORT), + config.get(CONF_CHARSET), config.get(CONF_FOLDER), config.get(CONF_SEARCH), ) @@ -62,13 +65,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class ImapSensor(Entity): """Representation of an IMAP sensor.""" - def __init__(self, name, user, password, server, port, folder, search): + def __init__(self, name, user, password, server, port, charset, folder, search): """Initialize the sensor.""" self._name = name or user self._user = user self._password = password self._server = server self._port = port + self._charset = charset self._folder = folder self._email_count = None self._search = search @@ -150,7 +154,9 @@ async def refresh_email_count(self): """Check the number of found emails.""" if self._connection: await self._connection.noop() - result, lines = await self._connection.search(self._search) + result, lines = await self._connection.search( + self._search, charset=self._charset + ) if result == "OK": self._email_count = len(lines[0].split()) From 75f94b914761c7a4ba50e710f117761070242dda Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 28 Oct 2019 01:03:26 +0100 Subject: [PATCH 1292/3953] Reorg and test attributes for HomematicIP Cloud (#28234) * Reorg and test attribute for HomematicIP Cloud * Add dutyCycle check to security_group * Edit test to improve coverage * Add missing flow test * apply suggestion Co-Authored-By: Martin Hjelmare * fix assert condition --- .../homematicip_cloud/binary_sensor.py | 13 +-- .../components/homematicip_cloud/device.py | 20 ++++- .../components/homematicip_cloud/sensor.py | 21 +++++ .../homematicip_cloud/test_binary_sensor.py | 88 ++++++++++++++++--- .../homematicip_cloud/test_config_flow.py | 9 ++ .../homematicip_cloud/test_sensor.py | 35 ++++++++ .../homematicip_cloud/test_switch.py | 2 +- 7 files changed, 169 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index e308f96c20896f..b5b663055a1193 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -39,7 +39,6 @@ from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -48,7 +47,6 @@ ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position" ATTR_ACCELERATION_SENSOR_SENSITIVITY = "acceleration_sensor_sensitivity" ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE = "acceleration_sensor_trigger_angle" -ATTR_LOW_BATTERY = "low_battery" ATTR_MOISTURE_DETECTED = "moisture_detected" ATTR_MOTION_DETECTED = "motion_detected" ATTR_POWER_MAINS_FAILURE = "power_mains_failure" @@ -59,12 +57,10 @@ ATTR_WINDOW_STATE = "window_state" GROUP_ATTRIBUTES = { - "lowBat": ATTR_LOW_BATTERY, "moistureDetected": ATTR_MOISTURE_DETECTED, "motionDetected": ATTR_MOTION_DETECTED, "powerMainsFailure": ATTR_POWER_MAINS_FAILURE, "presenceDetected": ATTR_PRESENCE_DETECTED, - "unreach": ATTR_GROUP_MEMBER_UNREACHABLE, "waterlevelDetected": ATTR_WATER_LEVEL_DETECTED, } @@ -408,17 +404,22 @@ def device_state_attributes(self): def is_on(self) -> bool: """Return true if safety issue detected.""" parent_is_on = super().is_on + if parent_is_on: + return True + if ( - parent_is_on - or self._device.powerMainsFailure + self._device.powerMainsFailure or self._device.moistureDetected or self._device.waterlevelDetected or self._device.lowBat + or self._device.dutyCycle ): return True + if ( self._device.smokeDetectorAlarmType is not None and self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF ): return True + return False diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index b05c0e0692852d..6c81775b6882a1 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -15,6 +15,9 @@ _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" +ATTR_LOW_BATTERY = "low_battery" +ATTR_CONFIG_PENDING = "config_pending" +ATTR_DUTY_CYCLE_REACHED = "duty_cycle_reached" ATTR_ID = "id" ATTR_IS_GROUP = "is_group" # RSSI HAP -> Device @@ -26,27 +29,40 @@ ATTR_DEVICE_OVERHEATED = "device_overheated" ATTR_DEVICE_OVERLOADED = "device_overloaded" ATTR_DEVICE_UNTERVOLTAGE = "device_undervoltage" +ATTR_EVENT_DELAY = "event_delay" DEVICE_ATTRIBUTE_ICONS = { "lowBat": "mdi:battery-outline", - "sabotage": "mdi:alert", + "sabotage": "mdi:shield-alert", + "dutyCycle": "mdi:alert", "deviceOverheated": "mdi:alert", "deviceOverloaded": "mdi:alert", "deviceUndervoltage": "mdi:alert", + "configPending": "mdi:alert-circle", } DEVICE_ATTRIBUTES = { "modelType": ATTR_MODEL_TYPE, "sabotage": ATTR_SABOTAGE, + "dutyCycle": ATTR_DUTY_CYCLE_REACHED, "rssiDeviceValue": ATTR_RSSI_DEVICE, "rssiPeerValue": ATTR_RSSI_PEER, "deviceOverheated": ATTR_DEVICE_OVERHEATED, "deviceOverloaded": ATTR_DEVICE_OVERLOADED, "deviceUndervoltage": ATTR_DEVICE_UNTERVOLTAGE, + "configPending": ATTR_CONFIG_PENDING, + "eventDelay": ATTR_EVENT_DELAY, "id": ATTR_ID, } -GROUP_ATTRIBUTES = {"modelType": ATTR_MODEL_TYPE} +GROUP_ATTRIBUTES = { + "modelType": ATTR_MODEL_TYPE, + "lowBat": ATTR_LOW_BATTERY, + "sabotage": ATTR_SABOTAGE, + "dutyCycle": ATTR_DUTY_CYCLE_REACHED, + "configPending": ATTR_CONFIG_PENDING, + "unreach": ATTR_GROUP_MEMBER_UNREACHABLE, +} class HomematicipGenericDevice(Entity): diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 9caa72ba15f171..acbf72f6ae9943 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -39,12 +39,21 @@ _LOGGER = logging.getLogger(__name__) +ATTR_CURRENT_ILLUMINATION = "current_illumination" +ATTR_LOWEST_ILLUMINATION = "lowest_illumination" +ATTR_HIGHEST_ILLUMINATION = "highest_illumination" ATTR_LEFT_COUNTER = "left_counter" ATTR_RIGHT_COUNTER = "right_counter" ATTR_TEMPERATURE_OFFSET = "temperature_offset" ATTR_WIND_DIRECTION = "wind_direction" ATTR_WIND_DIRECTION_VARIATION = "wind_direction_variation_in_degree" +ILLUMINATION_DEVICE_ATTRIBUTES = { + "currentIllumination": ATTR_CURRENT_ILLUMINATION, + "lowestIllumination": ATTR_LOWEST_ILLUMINATION, + "highestIllumination": ATTR_HIGHEST_ILLUMINATION, +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud sensors devices.""" @@ -273,6 +282,18 @@ def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return "lx" + @property + def device_state_attributes(self): + """Return the state attributes of the wind speed sensor.""" + state_attr = super().device_state_attributes + + for attr, attr_key in ILLUMINATION_DEVICE_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value + + return state_attr + class HomematicipPowerSensor(HomematicipGenericDevice): """Representation of a HomematicIP power measuring device.""" diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 0760518171eab0..38358f9ddff3e1 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -8,8 +8,19 @@ ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION, ATTR_ACCELERATION_SENSOR_SENSITIVITY, ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE, - ATTR_LOW_BATTERY, + ATTR_MOISTURE_DETECTED, ATTR_MOTION_DETECTED, + ATTR_POWER_MAINS_FAILURE, + ATTR_PRESENCE_DETECTED, + ATTR_WATER_LEVEL_DETECTED, + ATTR_WINDOW_STATE, +) +from homeassistant.components.homematicip_cloud.device import ( + ATTR_EVENT_DELAY, + ATTR_GROUP_MEMBER_UNREACHABLE, + ATTR_LOW_BATTERY, + ATTR_RSSI_DEVICE, + ATTR_SABOTAGE, ) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -105,6 +116,13 @@ async def test_hmip_shutter_contact(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + # test common attributes + assert ha_state.attributes[ATTR_RSSI_DEVICE] == -54 + assert not ha_state.attributes.get(ATTR_SABOTAGE) + await async_manipulate_test_data(hass, hmip_device, "sabotage", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_SABOTAGE] + async def test_hmip_motion_detector(hass, default_mock_hap): """Test HomematicipMotionDetector.""" @@ -137,6 +155,11 @@ async def test_hmip_presence_detector(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON + assert not ha_state.attributes.get(ATTR_EVENT_DELAY) + await async_manipulate_test_data(hass, hmip_device, "eventDelay", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_EVENT_DELAY] + async def test_hmip_smoke_detector(hass, default_mock_hap): """Test HomematicipSmokeDetector.""" @@ -267,10 +290,25 @@ async def test_hmip_security_zone_sensor_group(hass, default_mock_hap): ) assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_MOTION_DETECTED) + assert not ha_state.attributes.get(ATTR_PRESENCE_DETECTED) + assert not ha_state.attributes.get(ATTR_GROUP_MEMBER_UNREACHABLE) + assert not ha_state.attributes.get(ATTR_SABOTAGE) + assert not ha_state.attributes.get(ATTR_WINDOW_STATE) + await async_manipulate_test_data(hass, hmip_device, "motionDetected", True) + await async_manipulate_test_data(hass, hmip_device, "presenceDetected", True) + await async_manipulate_test_data(hass, hmip_device, "unreach", True) + await async_manipulate_test_data(hass, hmip_device, "sabotage", True) + await async_manipulate_test_data(hass, hmip_device, "windowState", WindowState.OPEN) ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON - assert ha_state.attributes[ATTR_MOTION_DETECTED] is True + assert ha_state.attributes[ATTR_MOTION_DETECTED] + assert ha_state.attributes[ATTR_PRESENCE_DETECTED] + assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] + assert ha_state.attributes[ATTR_SABOTAGE] + assert ha_state.attributes[ATTR_WINDOW_STATE] == WindowState.OPEN async def test_hmip_security_sensor_group(hass, default_mock_hap): @@ -283,14 +321,6 @@ async def test_hmip_security_sensor_group(hass, default_mock_hap): hass, default_mock_hap, entity_id, entity_name, device_model ) - assert ha_state.state == STATE_OFF - assert not ha_state.attributes.get("low_bat") - await async_manipulate_test_data(hass, hmip_device, "lowBat", True) - ha_state = hass.states.get(entity_id) - assert ha_state.state == STATE_ON - assert ha_state.attributes[ATTR_LOW_BATTERY] is True - - await async_manipulate_test_data(hass, hmip_device, "lowBat", False) await async_manipulate_test_data( hass, hmip_device, @@ -299,7 +329,45 @@ async def test_hmip_security_sensor_group(hass, default_mock_hap): ) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON + assert ( ha_state.attributes["smoke_detector_alarm"] == SmokeDetectorAlarmType.PRIMARY_ALARM ) + await async_manipulate_test_data( + hass, hmip_device, "smokeDetectorAlarmType", SmokeDetectorAlarmType.IDLE_OFF + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + assert not ha_state.attributes.get(ATTR_LOW_BATTERY) + assert not ha_state.attributes.get(ATTR_MOTION_DETECTED) + assert not ha_state.attributes.get(ATTR_PRESENCE_DETECTED) + assert not ha_state.attributes.get(ATTR_POWER_MAINS_FAILURE) + assert not ha_state.attributes.get(ATTR_MOISTURE_DETECTED) + assert not ha_state.attributes.get(ATTR_WATER_LEVEL_DETECTED) + assert not ha_state.attributes.get(ATTR_GROUP_MEMBER_UNREACHABLE) + assert not ha_state.attributes.get(ATTR_SABOTAGE) + assert not ha_state.attributes.get(ATTR_WINDOW_STATE) + + await async_manipulate_test_data(hass, hmip_device, "lowBat", True) + await async_manipulate_test_data(hass, hmip_device, "motionDetected", True) + await async_manipulate_test_data(hass, hmip_device, "presenceDetected", True) + await async_manipulate_test_data(hass, hmip_device, "powerMainsFailure", True) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", True) + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", True) + await async_manipulate_test_data(hass, hmip_device, "unreach", True) + await async_manipulate_test_data(hass, hmip_device, "sabotage", True) + await async_manipulate_test_data(hass, hmip_device, "windowState", WindowState.OPEN) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_LOW_BATTERY] + assert ha_state.attributes[ATTR_MOTION_DETECTED] + assert ha_state.attributes[ATTR_PRESENCE_DETECTED] + assert ha_state.attributes[ATTR_POWER_MAINS_FAILURE] + assert ha_state.attributes[ATTR_MOISTURE_DETECTED] + assert ha_state.attributes[ATTR_WATER_LEVEL_DETECTED] + assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] + assert ha_state.attributes[ATTR_SABOTAGE] + assert ha_state.attributes[ATTR_WINDOW_STATE] == WindowState.OPEN diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index 54cb309755db79..afaf71c67b5b31 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -98,6 +98,15 @@ async def test_init_flow_show_form(hass): assert result["type"] == "form" +async def test_init_flow_user_show_form(hass): + """Test config flow shows up with a form.""" + flow = config_flow.HomematicipCloudFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + assert result["type"] == "form" + + async def test_init_already_configured(hass): """Test accesspoint is already configured.""" MockConfigEntry( diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 8412cd19f4d7a3..f0a81c69074e2f 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -2,8 +2,20 @@ from homematicip.base.enums import ValveState from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.homematicip_cloud.device import ( + ATTR_CONFIG_PENDING, + ATTR_DEVICE_OVERHEATED, + ATTR_DEVICE_OVERLOADED, + ATTR_DEVICE_UNTERVOLTAGE, + ATTR_DUTY_CYCLE_REACHED, + ATTR_RSSI_DEVICE, + ATTR_RSSI_PEER, +) from homeassistant.components.homematicip_cloud.sensor import ( + ATTR_CURRENT_ILLUMINATION, + ATTR_HIGHEST_ILLUMINATION, ATTR_LEFT_COUNTER, + ATTR_LOWEST_ILLUMINATION, ATTR_RIGHT_COUNTER, ATTR_TEMPERATURE_OFFSET, ATTR_WIND_DIRECTION, @@ -92,6 +104,9 @@ async def test_hmip_humidity_sensor(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "humidity", 45) ha_state = hass.states.get(entity_id) assert ha_state.state == "45" + # test common attributes + assert ha_state.attributes[ATTR_RSSI_DEVICE] == -76 + assert ha_state.attributes[ATTR_RSSI_PEER] == -77 async def test_hmip_temperature_sensor1(hass, default_mock_hap): @@ -153,6 +168,23 @@ async def test_hmip_power_sensor(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" + # test common attributes + assert not ha_state.attributes.get(ATTR_DEVICE_OVERHEATED) + assert not ha_state.attributes.get(ATTR_DEVICE_OVERLOADED) + assert not ha_state.attributes.get(ATTR_DEVICE_UNTERVOLTAGE) + assert not ha_state.attributes.get(ATTR_DUTY_CYCLE_REACHED) + assert not ha_state.attributes.get(ATTR_CONFIG_PENDING) + await async_manipulate_test_data(hass, hmip_device, "deviceOverheated", True) + await async_manipulate_test_data(hass, hmip_device, "deviceOverloaded", True) + await async_manipulate_test_data(hass, hmip_device, "deviceUndervoltage", True) + await async_manipulate_test_data(hass, hmip_device, "dutyCycle", True) + await async_manipulate_test_data(hass, hmip_device, "configPending", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_DEVICE_OVERHEATED] + assert ha_state.attributes[ATTR_DEVICE_OVERLOADED] + assert ha_state.attributes[ATTR_DEVICE_UNTERVOLTAGE] + assert ha_state.attributes[ATTR_DUTY_CYCLE_REACHED] + assert ha_state.attributes[ATTR_CONFIG_PENDING] async def test_hmip_illuminance_sensor1(hass, default_mock_hap): @@ -187,6 +219,9 @@ async def test_hmip_illuminance_sensor2(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "averageIllumination", 231) ha_state = hass.states.get(entity_id) assert ha_state.state == "231" + assert ha_state.attributes[ATTR_CURRENT_ILLUMINATION] == 785.2 + assert ha_state.attributes[ATTR_HIGHEST_ILLUMINATION] == 837.1 + assert ha_state.attributes[ATTR_LOWEST_ILLUMINATION] == 785.2 async def test_hmip_windspeed_sensor(hass, default_mock_hap): diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index 9e33d1d9587774..b8ca7b4b67e69f 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -136,7 +136,7 @@ async def test_hmip_group_switch(hass, default_mock_hap): assert not ha_state.attributes.get(ATTR_GROUP_MEMBER_UNREACHABLE) await async_manipulate_test_data(hass, hmip_device, "unreach", True) ha_state = hass.states.get(entity_id) - assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] is True + assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] async def test_hmip_multi_switch(hass, default_mock_hap): From 72dee7dd211a313f9422a1b0fdd040358834f6cc Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 28 Oct 2019 00:32:16 +0000 Subject: [PATCH 1293/3953] [ci skip] Translation update --- .../alarm_control_panel/.translations/ru.json | 4 ++ .../cert_expiry/.translations/ko.json | 4 +- .../coolmaster/.translations/ko.json | 23 +++++++++++ .../components/cover/.translations/fr.json | 4 +- .../components/cover/.translations/ko.json | 4 +- .../device_tracker/.translations/fr.json | 8 ++++ .../device_tracker/.translations/ko.json | 8 ++++ .../huawei_lte/.translations/ko.json | 39 +++++++++++++++++++ .../media_player/.translations/fr.json | 11 ++++++ .../media_player/.translations/ko.json | 11 ++++++ .../components/sensor/.translations/ko.json | 36 ++++++++--------- .../components/solarlog/.translations/ko.json | 21 ++++++++++ .../components/somfy/.translations/ko.json | 5 +++ .../transmission/.translations/ko.json | 5 ++- .../components/withings/.translations/ko.json | 7 ++++ .../components/zha/.translations/ru.json | 7 ++++ 16 files changed, 175 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/coolmaster/.translations/ko.json create mode 100644 homeassistant/components/device_tracker/.translations/fr.json create mode 100644 homeassistant/components/device_tracker/.translations/ko.json create mode 100644 homeassistant/components/huawei_lte/.translations/ko.json create mode 100644 homeassistant/components/media_player/.translations/fr.json create mode 100644 homeassistant/components/media_player/.translations/ko.json create mode 100644 homeassistant/components/solarlog/.translations/ko.json diff --git a/homeassistant/components/alarm_control_panel/.translations/ru.json b/homeassistant/components/alarm_control_panel/.translations/ru.json index acea0ae7551cc0..e573ce709183d7 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ru.json +++ b/homeassistant/components/alarm_control_panel/.translations/ru.json @@ -1,6 +1,10 @@ { "device_automation": { "action_type": { + "arm_away": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "arm_home": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "arm_night": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "disarm": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0445\u0440\u0430\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" } } diff --git a/homeassistant/components/cert_expiry/.translations/ko.json b/homeassistant/components/cert_expiry/.translations/ko.json index a807d32a6fbaaa..25c518f8629a55 100644 --- a/homeassistant/components/cert_expiry/.translations/ko.json +++ b/homeassistant/components/cert_expiry/.translations/ko.json @@ -4,10 +4,12 @@ "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { + "certificate_error": "\uc778\uc99d\uc11c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "certificate_fetch_failed": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uc5d0\uc11c \uc778\uc99d\uc11c\ub97c \uac00\uc838 \uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "connection_timeout": "\ud638\uc2a4\ud2b8 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + "resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "wrong_host": "\uc778\uc99d\uc11c\uac00 \ud638\uc2a4\ud2b8 \uc774\ub984\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/ko.json b/homeassistant/components/coolmaster/.translations/ko.json new file mode 100644 index 00000000000000..ff6ddf0acfe7ae --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "CoolMasterNet \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "no_units": "CoolMasterNet \ud638\uc2a4\ud2b8\uc5d0\uc11c HVAC \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "cool": "\ub0c9\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", + "dry": "\uc81c\uc2b5 \ubaa8\ub4dc \uc9c0\uc6d0", + "fan_only": "\uc1a1\ud48d \ubaa8\ub4dc \uc9c0\uc6d0", + "heat": "\ub09c\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", + "heat_cool": "\uc790\ub3d9 \ub0c9/\ub09c\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", + "host": "\ud638\uc2a4\ud2b8", + "off": "\uc804\uc6d0\uc744 \ub04c \uc218 \uc788\uc2b4" + }, + "title": "CoolMasterNet \uc5f0\uacb0 \uc0c1\uc138\uc815\ubcf4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/fr.json b/homeassistant/components/cover/.translations/fr.json index 95978ed0fa5da7..1f2debe9c04d58 100644 --- a/homeassistant/components/cover/.translations/fr.json +++ b/homeassistant/components/cover/.translations/fr.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} est ferm\u00e9", "is_closing": "{entity_name} se ferme", "is_open": "{entity_name} est ouvert", - "is_opening": "{entity_name} est en train de s'ouvrir" + "is_opening": "{entity_name} est en train de s'ouvrir", + "is_position": "La position de {entity_name} est", + "is_tilt_position": "La position d'inclinaison de {entity_name} est" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json index 02f900a8fe50a6..48f7ba17532d27 100644 --- a/homeassistant/components/cover/.translations/ko.json +++ b/homeassistant/components/cover/.translations/ko.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud799\ub2c8\ub2e4", "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", - "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4" + "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4", + "is_position": "{entity_name} \uac1c\ud3d0 \uc704\uce58\ub294", + "is_tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\ub294" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/fr.json b/homeassistant/components/device_tracker/.translations/fr.json new file mode 100644 index 00000000000000..bf9033170c1551 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/fr.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} est \u00e0 la maison", + "is_not_home": "{entity_name} n'est pas \u00e0 la maison" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/ko.json b/homeassistant/components/device_tracker/.translations/ko.json new file mode 100644 index 00000000000000..34389297f28362 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/ko.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \ub2d8\uc774 \uc9d1\uc5d0 \uc788\uc2b5\ub2c8\ub2e4", + "is_not_home": "{entity_name} \ub2d8\uc774 \uc678\ucd9c\uc911\uc785\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/ko.json b/homeassistant/components/huawei_lte/.translations/ko.json new file mode 100644 index 00000000000000..b21e0aa0a23e4f --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/ko.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "connection_failed": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "incorrect_password": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "incorrect_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "incorrect_username_or_password": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "invalid_url": "URL \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "login_attempts_exceeded": "\ucd5c\ub300 \ub85c\uadf8\uc778 \uc2dc\ub3c4 \ud69f\uc218\ub97c \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", + "response_error": "\uae30\uae30\uc5d0\uc11c \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "unknown_connection_error": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\ub294 \uc911 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "url": "URL \uc8fc\uc18c", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "\uae30\uae30 \uc561\uc138\uc2a4 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc124\uc815\ud558\ub294 \uac83\uc740 \uc120\ud0dd \uc0ac\ud56d\uc774\uc9c0\ub9cc \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc9c0\uc6d0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubc18\uba74, \uc778\uc99d \ub41c \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uba74, \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \ud65c\uc131\ud654 \ub41c \uc0c1\ud0dc\uc5d0\uc11c \ub2e4\ub978 \ubc29\ubc95\uc73c\ub85c Home Assistant \uc758 \uc678\ubd80\uc5d0\uc11c \uae30\uae30\uc758 \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\uc5d0 \uc561\uc138\uc2a4\ud558\ub294 \ub370 \ubb38\uc81c\uac00 \ubc1c\uc0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "Huawei LTE \uc124\uc815" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "SMS \uc54c\ub9bc \uc218\uc2e0\uc790", + "track_new_devices": "\uc0c8\ub85c\uc6b4 \uae30\uae30 \ucd94\uc801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/fr.json b/homeassistant/components/media_player/.translations/fr.json new file mode 100644 index 00000000000000..6be3e6095901ca --- /dev/null +++ b/homeassistant/components/media_player/.translations/fr.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} est inactif", + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9", + "is_paused": "{entity_name} est en pause", + "is_playing": "{entity_name} joue" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/ko.json b/homeassistant/components/media_player/.translations/ko.json new file mode 100644 index 00000000000000..7542154448f620 --- /dev/null +++ b/homeassistant/components/media_player/.translations/ko.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \uc774(\uac00) \uc720\ud734\uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", + "is_paused": "{entity_name} \uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "is_playing": "{entity_name} \uc774(\uac00) \uc7ac\uc0dd\uc911\uc785\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/ko.json b/homeassistant/components/sensor/.translations/ko.json index d24a40583434e9..0e74f3f4f897d3 100644 --- a/homeassistant/components/sensor/.translations/ko.json +++ b/homeassistant/components/sensor/.translations/ko.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", - "is_humidity": "{entity_name} \uc2b5\ub3c4", - "is_illuminance": "{entity_name} \uc870\ub3c4", - "is_power": "{entity_name} \uc18c\ube44 \uc804\ub825", - "is_pressure": "{entity_name} \uc555\ub825", - "is_signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4", - "is_temperature": "{entity_name} \uc628\ub3c4", - "is_timestamp": "{entity_name} \uc2dc\uac01", - "is_value": "{entity_name} \uac12" + "is_battery_level": "\ud604\uc7ac {entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", + "is_humidity": "\ud604\uc7ac {entity_name} \uc2b5\ub3c4", + "is_illuminance": "\ud604\uc7ac {entity_name} \uc870\ub3c4", + "is_power": "\ud604\uc7ac {entity_name} \uc18c\ube44 \uc804\ub825", + "is_pressure": "\ud604\uc7ac {entity_name} \uc555\ub825", + "is_signal_strength": "\ud604\uc7ac {entity_name} \uc2e0\ud638 \uac15\ub3c4", + "is_temperature": "\ud604\uc7ac {entity_name} \uc628\ub3c4", + "is_timestamp": "\ud604\uc7ac {entity_name} \uc2dc\uac01", + "is_value": "\ud604\uc7ac {entity_name} \uac12" }, "trigger_type": { - "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", - "humidity": "{entity_name} \uc2b5\ub3c4", - "illuminance": "{entity_name} \uc870\ub3c4", - "power": "{entity_name} \uc18c\ube44 \uc804\ub825", - "pressure": "{entity_name} \uc555\ub825", - "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4", - "temperature": "{entity_name} \uc628\ub3c4", - "timestamp": "{entity_name} \uc2dc\uac01", - "value": "{entity_name} \uac12" + "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubcc0\ud654", + "humidity": "{entity_name} \uc2b5\ub3c4 \ubcc0\ud654", + "illuminance": "{entity_name} \uc870\ub3c4 \ubcc0\ud654", + "power": "{entity_name} \uc18c\ube44 \uc804\ub825 \ubcc0\ud654", + "pressure": "{entity_name} \uc555\ub825 \ubcc0\ud654", + "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4 \ubcc0\ud654", + "temperature": "{entity_name} \uc628\ub3c4 \ubcc0\ud654", + "timestamp": "{entity_name} \uc2dc\uac01 \ubcc0\ud654", + "value": "{entity_name} \uac12 \ubcc0\ud654" } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/ko.json b/homeassistant/components/solarlog/.translations/ko.json new file mode 100644 index 00000000000000..ea337d1e67542b --- /dev/null +++ b/homeassistant/components/solarlog/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." + }, + "step": { + "user": { + "data": { + "host": "Solar-Log \uae30\uae30\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c", + "name": "Solar-Log \uc13c\uc11c\uc5d0 \uc0ac\uc6a9\ub420 \uc811\ub450\uc0ac" + }, + "title": "Solar-Log \uc5f0\uacb0 \uc815\uc758" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/ko.json b/homeassistant/components/somfy/.translations/ko.json index 72b234cd98b60e..c7fa0e4d293aef 100644 --- a/homeassistant/components/somfy/.translations/ko.json +++ b/homeassistant/components/somfy/.translations/ko.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Somfy \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ko.json b/homeassistant/components/transmission/.translations/ko.json index a9b1b369f9029e..507d4e84789d03 100644 --- a/homeassistant/components/transmission/.translations/ko.json +++ b/homeassistant/components/transmission/.translations/ko.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." }, "error": { "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4", "wrong_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" }, - "description": "Transmission \uc635\uc158 \uc124\uc815" + "description": "Transmission \uc635\uc158 \uc124\uc815", + "title": "Transmission \uc635\uc158 \uc124\uc815" } } } diff --git a/homeassistant/components/withings/.translations/ko.json b/homeassistant/components/withings/.translations/ko.json index 617964e0596aba..4191e03d4404d5 100644 --- a/homeassistant/components/withings/.translations/ko.json +++ b/homeassistant/components/withings/.translations/ko.json @@ -7,6 +7,13 @@ "default": "\uc120\ud0dd\ud55c \ud504\ub85c\ud544\ub85c Withings \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { + "profile": { + "data": { + "profile": "\ud504\ub85c\ud544" + }, + "description": "Withings \uc6f9 \uc0ac\uc774\ud2b8\uc5d0\uc11c \uc5b4\ub5a4 \ud504\ub85c\ud544\uc744 \uc120\ud0dd\ud558\uc168\ub098\uc694? \ud504\ub85c\ud544\uc774 \uc77c\uce58\ud574\uc57c \ud569\ub2c8\ub2e4. \uadf8\ub807\uc9c0 \uc54a\uc73c\uba74, \ub370\uc774\ud130\uc5d0 \ub808\uc774\ube14\uc774 \uc798\ubabb \uc9c0\uc815\ub429\ub2c8\ub2e4.", + "title": "\uc0ac\uc6a9\uc790 \ud504\ub85c\ud544." + }, "user": { "data": { "profile": "\ud504\ub85c\ud544" diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index 1779ed613fc76d..983a69cdc2c201 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -33,6 +33,13 @@ "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "dim_down": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0435\u0442\u0441\u044f", "dim_up": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f", + "face_1": "\u043d\u0430 \u043f\u0435\u0440\u0432\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_2": "\u043d\u0430 \u0432\u0442\u043e\u0440\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_3": "\u043d\u0430 \u0442\u0440\u0435\u0442\u0435\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_4": "\u043d\u0430 \u0447\u0435\u0442\u0432\u0451\u0440\u0442\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_5": "\u043d\u0430 \u043f\u044f\u0442\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_6": "\u043d\u0430 \u0448\u0435\u0441\u0442\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", + "face_any": "\u043d\u0430 \u043b\u044e\u0431\u043e\u0439 \u0433\u0440\u0430\u043d\u0438", "left": "\u041d\u0430\u043b\u0435\u0432\u043e", "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", From edcf476408f495b62ba2b3d7963f129546da9d2d Mon Sep 17 00:00:00 2001 From: fwestenberg <47930023+fwestenberg@users.noreply.github.com> Date: Mon, 28 Oct 2019 07:43:01 +0100 Subject: [PATCH 1294/3953] Add support for Xiaomi Air Quality Monitor (cgllc.airmonitor.b1) (#27735) --- .../components/xiaomi_miio/air_quality.py | 193 ++++++++++++++++++ .../components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/xiaomi_miio/air_quality.py diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py new file mode 100644 index 00000000000000..e96ed074002403 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -0,0 +1,193 @@ +"""Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" +from miio import AirQualityMonitor, DeviceException +import voluptuous as vol + +from homeassistant.components.air_quality import ( + AirQualityEntity, + PLATFORM_SCHEMA, + _LOGGER, + ATTR_PM_2_5, +) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN, ATTR_TEMPERATURE +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv + +DEFAULT_NAME = "Xiaomi Miio Air Quality Monitor" +DATA_KEY = "air_quality.xiaomi_miio" + +ATTR_CO2E = "carbon_dioxide_equivalent" +ATTR_HUMIDITY = "relative_humidity" +ATTR_TVOC = "total_volatile_organic_compounds" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +ATTR_SW_VERSION = "sw_version" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + +PROP_TO_ATTR = { + "carbon_dioxide_equivalent": ATTR_CO2E, + "relative_humidity": ATTR_HUMIDITY, + "particulate_matter_2_5": ATTR_PM_2_5, + "temperature": ATTR_TEMPERATURE, + "total_volatile_organic_compounds": ATTR_TVOC, + "manufacturer": ATTR_MANUFACTURER, + "model": ATTR_MODEL, + "sw_version": ATTR_SW_VERSION, +} + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the sensor from config.""" + + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} + + host = config.get(CONF_HOST) + token = config.get(CONF_TOKEN) + name = config.get(CONF_NAME) + + _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + + try: + device = AirMonitorB1(name, AirQualityMonitor(host, token, model=None)) + + except DeviceException: + raise PlatformNotReady + + hass.data[DATA_KEY][host] = device + async_add_entities([device], update_before_add=True) + + +class AirMonitorB1(AirQualityEntity): + """Air Quality class for Xiaomi cgllc.airmonitor.b1 device.""" + + def __init__(self, name, device): + """Initialize the entity.""" + self._name = name + self._device = device + self._icon = "mdi:cloud" + self._manufacturer = "Xiaomi" + self._unit_of_measurement = "μg/m3" + self._model = None + self._mac_address = None + self._sw_version = None + self._carbon_dioxide_equivalent = None + self._relative_humidity = None + self._particulate_matter_2_5 = None + self._temperature = None + self._total_volatile_organic_compounds = None + + async def async_update(self): + """Fetch state from the miio device.""" + + try: + if self._model is None: + info = await self.hass.async_add_executor_job(self._device.info) + self._model = info.model + self._mac_address = info.mac_address + self._sw_version = info.firmware_version + + state = await self.hass.async_add_executor_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + + self._carbon_dioxide_equivalent = state.co2e + self._relative_humidity = round(state.humidity, 1) + self._particulate_matter_2_5 = round(state.pm25, 1) + self._temperature = round(state.temperature, 1) + self._total_volatile_organic_compounds = round(state.tvoc, 3) + + except DeviceException as ex: + _LOGGER.error("Got exception while fetching the state: %s", ex) + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def device(self): + """Return the name of this entity, if any.""" + return self._device + + @property + def icon(self): + """Return the icon to use for device if any.""" + return self._icon + + @property + def manufacturer(self): + """Return the manufacturer version.""" + return self._manufacturer + + @property + def model(self): + """Return the device model.""" + return self._model + + @property + def sw_version(self): + """Return the software version.""" + return self._sw_version + + @property + def mac_address(self): + """Return the mac address.""" + return self._mac_address + + @property + def unique_id(self): + """Return the unique ID.""" + return f"{self._model}-{self._mac_address}" + + @property + def carbon_dioxide_equivalent(self): + """Return the CO2e (carbon dioxide equivalent) level.""" + return self._carbon_dioxide_equivalent + + @property + def relative_humidity(self): + """Return the humidity percentage.""" + return self._relative_humidity + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._particulate_matter_2_5 + + @property + def temperature(self): + """Return the temperature in °C.""" + return self._temperature + + @property + def total_volatile_organic_compounds(self): + """Return the total volatile organic compounds.""" + return self._total_volatile_organic_compounds + + @property + def state_attributes(self): + """Return the state attributes.""" + data = {} + + for prop, attr in PROP_TO_ATTR.items(): + value = getattr(self, prop) + if value is not None: + data[attr] = value + + return data + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the current state.""" + return self._particulate_matter_2_5 diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index b675e6e6746234..849e4573bbf85c 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": [ "construct==2.9.45", - "python-miio==0.4.6" + "python-miio==0.4.7" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a4621d0e45fd67..b589ef6d918e6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1537,7 +1537,7 @@ python-juicenet==0.0.5 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.4.6 +python-miio==0.4.7 # homeassistant.components.mpd python-mpd2==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69eaf2d29220a3..861e1098664f7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -522,7 +522,7 @@ python-forecastio==1.4.0 python-izone==1.1.1 # homeassistant.components.xiaomi_miio -python-miio==0.4.6 +python-miio==0.4.7 # homeassistant.components.nest python-nest==4.1.0 From 54342d2a4e77eeedd956de3f1613283ede9ce38e Mon Sep 17 00:00:00 2001 From: "J.P. Hutchins" <34154542+JPHutchins@users.noreply.github.com> Date: Mon, 28 Oct 2019 02:20:59 -0700 Subject: [PATCH 1295/3953] Add transmission info about torrents that is accessible with templating (#27111) * Add information about current downloads. * Cleanup: add "Torrent Info" state attribute * Add username to codeowners * Rename state_attributes - device_state_attributes. * Fix snakecase keys, use f-strings, remove redundant method. * Access started_torrent_dict directly * Add return None condition * Remove redundancy. * Add missing comma in codeowners list. * Add missing @ to username. * Update CODEOWNERS with script.hassfest. * Remove transmission_downloading, give started_torrents the info. * Confirm changes. * Actually approve changes. * Resolve conflicts. * Remove leftovers from old torrent_info sensor. * Remove get_started_torrent_info method. Old method to display boolean for the removed torrent_info sensor. --- CODEOWNERS | 2 +- .../components/transmission/__init__.py | 27 +++++++++++++++++++ .../components/transmission/const.py | 1 + .../components/transmission/manifest.json | 5 ++-- .../components/transmission/sensor.py | 10 ++++++- 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 40e37ec46978fb..46ffd1196f7f65 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -306,7 +306,7 @@ homeassistant/components/tplink/* @rytilahti homeassistant/components/traccar/* @ludeeus homeassistant/components/tradfri/* @ggravlingen homeassistant/components/trafikverket_train/* @endor-force -homeassistant/components/transmission/* @engrbm87 +homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/tts/* @robbiet480 homeassistant/components/twentemilieu/* @frenck homeassistant/components/twilio_call/* @robbiet480 diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 6cfd6bf640ae37..be41ca85998f6a 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -232,6 +232,7 @@ def __init__(self, hass, config, api): self._api = api self.completed_torrents = [] self.started_torrents = [] + self.started_torrent_dict = {} @property def host(self): @@ -250,6 +251,7 @@ def update(self): self.torrents = self._api.get_torrents() self.session = self._api.get_session() + self.check_started_torrent_info() self.check_completed_torrent() self.check_started_torrent() _LOGGER.debug("Torrent Data for %s Updated", self.host) @@ -301,6 +303,31 @@ def check_started_torrent(self): self.hass.bus.fire("transmission_started_torrent", {"name": var}) self.started_torrents = actual_started_torrents + def check_started_torrent_info(self): + """Get started torrent info functionality.""" + all_torrents = self._api.get_torrents() + current_down = {} + + for torrent in all_torrents: + if torrent.status == "downloading": + info = self.started_torrent_dict[torrent.name] = { + "added_date": torrent.addedDate, + "percent_done": f"{torrent.percentDone * 100:.2f}", + } + try: + info["eta"] = str(torrent.eta) + except ValueError: + info["eta"] = "unknown" + + current_down[torrent.name] = True + + elif torrent.name in self.started_torrent_dict: + self.started_torrent_dict.pop(torrent.name) + + for torrent in list(self.started_torrent_dict): + if torrent not in current_down: + self.started_torrent_dict.pop(torrent) + def get_started_torrent_count(self): """Get the number of started torrents.""" return len(self.started_torrents) diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 472bb32a391cfe..5540f718ba10f0 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -17,6 +17,7 @@ DEFAULT_PORT = 9091 DEFAULT_SCAN_INTERVAL = 120 +STATE_ATTR_TORRENT_INFO = "torrent_info" ATTR_TORRENT = "torrent" SERVICE_ADD_TORRENT = "add_torrent" diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index c2fa31d7b500c9..9618a5677adef6 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -8,6 +8,7 @@ ], "dependencies": [], "codeowners": [ - "@engrbm87" + "@engrbm87", + "@JPHutchins" ] -} \ No newline at end of file +} diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index d9fd2b5114476c..489582de157292 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -6,7 +6,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DOMAIN, SENSOR_TYPES +from .const import DOMAIN, SENSOR_TYPES, STATE_ATTR_TORRENT_INFO + _LOGGER = logging.getLogger(__name__) @@ -82,6 +83,13 @@ def available(self): """Could the device be accessed during the last update call.""" return self._tm_client.api.available + @property + def device_state_attributes(self): + """Return the state attributes, if any.""" + if self._tm_client.api.started_torrent_dict and self.type == "started_torrents": + return {STATE_ATTR_TORRENT_INFO: self._tm_client.api.started_torrent_dict} + return None + async def async_added_to_hass(self): """Handle entity which will be added.""" async_dispatcher_connect( From 7887850505bbaef83467c732e07b23236e31f1d2 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 28 Oct 2019 14:34:13 +0100 Subject: [PATCH 1296/3953] More header cleanup for websocket proxy (#28288) --- homeassistant/components/hassio/ingress.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 4ecb9a8419f529..3b6fee563545e9 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -211,6 +211,10 @@ def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]: hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE, hdrs.CONTENT_ENCODING, + hdrs.SEC_WEBSOCKET_EXTENSIONS, + hdrs.SEC_WEBSOCKET_PROTOCOL, + hdrs.SEC_WEBSOCKET_VERSION, + hdrs.SEC_WEBSOCKET_KEY, ): continue headers[name] = value From c1d88dd7a42c1b013cee43f1cb1ec03cebbba595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren?= Date: Mon, 28 Oct 2019 14:47:04 +0100 Subject: [PATCH 1297/3953] Bump avea to 1.4 (#28287) * Bump avea to 1.4 * Bump avea to 1.4 #2 --- homeassistant/components/avea/manifest.json | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/avea/manifest.json b/homeassistant/components/avea/manifest.json index 4fb9ab9f4205c7..f6217eeed18223 100644 --- a/homeassistant/components/avea/manifest.json +++ b/homeassistant/components/avea/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/avea", "dependencies": [], "codeowners": ["@pattyland"], - "requirements": ["avea==1.2.8"] -} \ No newline at end of file + "requirements": ["avea==1.4"] +} diff --git a/requirements_all.txt b/requirements_all.txt index b589ef6d918e6b..e30f50f7e147ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -246,7 +246,7 @@ aurorapy==0.2.6 av==6.1.2 # homeassistant.components.avea -avea==1.2.8 +avea==1.4 # homeassistant.components.avion # avion==0.10 From 30f4ee121a0d07e74452fa1ffff413574cd23bd1 Mon Sep 17 00:00:00 2001 From: Eliseo Martelli Date: Mon, 28 Oct 2019 14:54:42 +0100 Subject: [PATCH 1298/3953] Remove GTT component (#28286) * removed GTT component * Removed gtt.py from coveragerc --- .coveragerc | 1 - homeassistant/components/gtt/__init__.py | 1 - homeassistant/components/gtt/manifest.json | 10 -- homeassistant/components/gtt/sensor.py | 114 --------------------- requirements_all.txt | 3 - 5 files changed, 129 deletions(-) delete mode 100644 homeassistant/components/gtt/__init__.py delete mode 100644 homeassistant/components/gtt/manifest.json delete mode 100644 homeassistant/components/gtt/sensor.py diff --git a/.coveragerc b/.coveragerc index 90efc417e03440..e6f09d60effd0c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -274,7 +274,6 @@ omit = homeassistant/components/growatt_server/sensor.py homeassistant/components/gstreamer/media_player.py homeassistant/components/gtfs/sensor.py - homeassistant/components/gtt/sensor.py homeassistant/components/habitica/* homeassistant/components/hangouts/* homeassistant/components/hangouts/__init__.py diff --git a/homeassistant/components/gtt/__init__.py b/homeassistant/components/gtt/__init__.py deleted file mode 100644 index cbb508154dde4d..00000000000000 --- a/homeassistant/components/gtt/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The gtt component.""" diff --git a/homeassistant/components/gtt/manifest.json b/homeassistant/components/gtt/manifest.json deleted file mode 100644 index 217b17555549ba..00000000000000 --- a/homeassistant/components/gtt/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "gtt", - "name": "Gtt", - "documentation": "https://www.home-assistant.io/integrations/gtt", - "requirements": [ - "pygtt==1.1.2" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/gtt/sensor.py b/homeassistant/components/gtt/sensor.py deleted file mode 100644 index cd66a670696eb4..00000000000000 --- a/homeassistant/components/gtt/sensor.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Sensor to get GTT's timetable for a stop.""" -import logging -from datetime import timedelta, datetime - -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import DEVICE_CLASS_TIMESTAMP -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -CONF_STOP = "stop" -CONF_BUS_NAME = "bus_name" - -ICON = "mdi:train" - -SCAN_INTERVAL = timedelta(minutes=2) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_STOP): cv.string, vol.Optional(CONF_BUS_NAME): cv.string} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Gtt platform.""" - stop = config[CONF_STOP] - bus_name = config.get(CONF_BUS_NAME) - - add_entities([GttSensor(stop, bus_name)], True) - - -class GttSensor(Entity): - """Representation of a Gtt Sensor.""" - - def __init__(self, stop, bus_name): - """Initialize the Gtt sensor.""" - self.data = GttData(stop, bus_name) - self._state = None - self._name = f"Stop {stop}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the icon of the sensor.""" - return ICON - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def device_state_attributes(self): - """Return the state attributes of the sensor.""" - attr = {"bus_name": self.data.state_bus["bus_name"]} - return attr - - def update(self): - """Update device state.""" - self.data.get_data() - next_time = get_datetime(self.data.state_bus) - self._state = next_time.isoformat() - - -class GttData: - """Inteface to PyGTT.""" - - def __init__(self, stop, bus_name): - """Initialize the GttData class.""" - from pygtt import PyGTT - - self._pygtt = PyGTT() - self._stop = stop - self._bus_name = bus_name - self.bus_list = {} - self.state_bus = {} - - def get_data(self): - """Get the data from the api.""" - self.bus_list = self._pygtt.get_by_stop(self._stop) - self.bus_list.sort(key=get_datetime) - - if self._bus_name is not None: - self.state_bus = self.get_bus_by_name() - return - - self.state_bus = self.bus_list[0] - - def get_bus_by_name(self): - """Get the bus by name.""" - for bus in self.bus_list: - if bus["bus_name"] == self._bus_name: - return bus - - -def get_datetime(bus): - """Get the datetime from a bus.""" - bustime = datetime.strptime(bus["time"][0]["run"], "%H:%M") - now = datetime.now() - bustime = bustime.replace(year=now.year, month=now.month, day=now.day) - if bustime < now: - bustime = bustime + timedelta(days=1) - return bustime diff --git a/requirements_all.txt b/requirements_all.txt index e30f50f7e147ae..1cbb3d33f38ea5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1222,9 +1222,6 @@ pygogogate2==0.1.1 # homeassistant.components.gtfs pygtfs==0.1.5 -# homeassistant.components.gtt -pygtt==1.1.2 - # homeassistant.components.version pyhaversion==3.1.0 From 549e8cf2c504942348f42ea5f293d2e8691f1d95 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 28 Oct 2019 16:45:08 +0100 Subject: [PATCH 1299/3953] Hue: Create new config flow when auth is lost (#28204) * Hue: Create new config flow when auth is lost * Fix tests * Fix tests * Comments * Lint --- homeassistant/components/hue/bridge.py | 23 +++++++++----- homeassistant/components/hue/helpers.py | 12 +++++++ homeassistant/components/hue/light.py | 18 ++++++++--- homeassistant/components/hue/sensor_base.py | 16 ++++++++-- tests/components/hue/test_bridge.py | 22 +++++++++++++ tests/components/hue/test_light.py | 5 +-- tests/components/hue/test_sensor_base.py | 35 +++++++++++++++++++-- 7 files changed, 112 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 6a654744397626..5015ec669aaa14 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -5,12 +5,12 @@ import async_timeout import voluptuous as vol -from homeassistant import config_entries from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN, LOGGER from .errors import AuthenticationRequired, CannotConnect +from .helpers import create_config_flow SERVICE_HUE_SCENE = "hue_activate_scene" ATTR_GROUP_NAME = "group_name" @@ -30,6 +30,7 @@ def __init__(self, hass, config_entry, allow_unreachable, allow_groups): self.allow_unreachable = allow_unreachable self.allow_groups = allow_groups self.available = True + self.authorized = False self.api = None @property @@ -49,13 +50,7 @@ async def async_setup(self, tries=0): # We are going to fail the config entry setup and initiate a new # linking procedure. When linking succeeds, it will remove the # old config entry. - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": host}, - ) - ) + create_config_flow(hass, host) return False except CannotConnect: @@ -82,6 +77,7 @@ async def async_setup(self, tries=0): DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, schema=SCENE_SCHEMA ) + self.authorized = True return True async def async_reset(self): @@ -155,6 +151,17 @@ async def hue_activate_scene(self, call, updated=False): await group.set_action(scene=scene.id) + async def handle_unauthorized_error(self): + """Create a new config flow when the authorization is no longer valid.""" + if not self.authorized: + # we already created a new config flow, no need to do it again + return + LOGGER.error( + "Unable to authorize to bridge %s, setup the linking again.", self.host + ) + self.authorized = False + create_config_flow(self.hass, self.host) + async def get_bridge(hass, host, username=None): """Create a bridge object and verify authentication.""" diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index 971509ab6479c5..af0f996b537daa 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -1,6 +1,7 @@ """Helper functions for Philips Hue.""" from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg +from homeassistant import config_entries from .const import DOMAIN @@ -31,3 +32,14 @@ async def remove_devices(hass, config_entry, api_ids, current): for item_id in removed_items: del current[item_id] + + +def create_config_flow(hass, host): + """Start a config flow.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"host": host}, + ) + ) diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 041eb76c1d34f0..d58e4608b65582 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -189,6 +189,9 @@ async def async_update_items( progress_waiting, ): """Update either groups or lights from the bridge.""" + if not bridge.authorized: + return + if is_group: api_type = "group" api = bridge.api.groups @@ -200,6 +203,9 @@ async def async_update_items( start = monotonic() with async_timeout.timeout(4): await api.update() + except aiohue.Unauthorized: + await bridge.handle_unauthorized_error() + return except (asyncio.TimeoutError, aiohue.AiohueException) as err: _LOGGER.debug("Failed to fetch %s: %s", api_type, err) @@ -337,10 +343,14 @@ def is_on(self): @property def available(self): """Return if light is available.""" - return self.bridge.available and ( - self.is_group - or self.bridge.allow_unreachable - or self.light.state["reachable"] + return ( + self.bridge.available + and self.bridge.authorized + and ( + self.is_group + or self.bridge.allow_unreachable + or self.light.state["reachable"] + ) ) @property diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 7236dfbd886890..62bd98df3a240a 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -4,7 +4,7 @@ import logging from time import monotonic -from aiohue import AiohueException +from aiohue import AiohueException, Unauthorized from aiohue.sensors import TYPE_ZLL_PRESENCE import async_timeout @@ -80,6 +80,11 @@ async def start(self): async def async_update_bridge(now): """Will update sensors from the bridge.""" + + # don't update when we are not authorized + if not self.bridge.authorized: + return + await self.async_update_items() async_track_point_in_utc_time( @@ -96,6 +101,9 @@ async def async_update_items(self): start = monotonic() with async_timeout.timeout(4): await api.update() + except Unauthorized: + await self.bridge.handle_unauthorized_error() + return except (asyncio.TimeoutError, AiohueException) as err: _LOGGER.debug("Failed to fetch sensor: %s", err) @@ -220,8 +228,10 @@ def name(self): @property def available(self): """Return if sensor is available.""" - return self.bridge.available and ( - self.bridge.allow_unreachable or self.sensor.config["reachable"] + return ( + self.bridge.available + and self.bridge.authorized + and (self.bridge.allow_unreachable or self.sensor.config["reachable"]) ) @property diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index a426d35abf767c..7265b4687143c6 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -91,3 +91,25 @@ async def test_reset_unloads_entry_if_setup(): assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 3 assert len(hass.services.async_remove.mock_calls) == 1 + + +async def test_handle_unauthorized(): + """Test handling an unauthorized error on update.""" + hass = Mock() + entry = Mock() + entry.data = {"host": "1.2.3.4", "username": "mock-username"} + hue_bridge = bridge.HueBridge(hass, entry, False, False) + + with patch.object(bridge, "get_bridge", return_value=mock_coro(Mock())): + assert await hue_bridge.async_setup() is True + + assert hue_bridge.authorized is True + + await hue_bridge.handle_unauthorized_error() + + assert hue_bridge.authorized is False + assert len(hass.async_create_task.mock_calls) == 4 + assert len(hass.config_entries.flow.async_init.mock_calls) == 1 + assert hass.config_entries.flow.async_init.mock_calls[0][2]["data"] == { + "host": "1.2.3.4" + } diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 582cc185bc81d6..88c527a50ca872 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -180,6 +180,7 @@ def mock_bridge(hass): """Mock a Hue bridge.""" bridge = Mock( available=True, + authorized=True, allow_unreachable=False, allow_groups=False, api=Mock(), @@ -598,13 +599,13 @@ async def test_update_timeout(hass, mock_bridge): async def test_update_unauthorized(hass, mock_bridge): - """Test bridge marked as not available if unauthorized during update.""" + """Test bridge marked as not authorized if unauthorized during update.""" mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized) mock_bridge.api.groups.update = Mock(side_effect=aiohue.Unauthorized) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 0 assert len(hass.states.async_all()) == 0 - assert mock_bridge.available is False + assert len(mock_bridge.handle_unauthorized_error.mock_calls) == 1 async def test_light_turn_on_service(hass, mock_bridge): diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index 72ac816483a147..ba259dccf7107c 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -256,6 +256,7 @@ def create_mock_bridge(): """Create a mock Hue bridge.""" bridge = Mock( available=True, + authorized=True, allow_unreachable=False, allow_groups=False, api=Mock(), @@ -425,6 +426,36 @@ async def test_new_sensor_discovered(hass, mock_bridge): assert temperature.state == "17.75" +async def test_sensor_removed(hass, mock_bridge): + """Test if 2nd update has removed sensor.""" + mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 6 + + mock_bridge.mock_sensor_responses.clear() + keys = ("1", "2", "3") + mock_bridge.mock_sensor_responses.append({k: SENSOR_RESPONSE[k] for k in keys}) + + # Force updates to run again + sm_key = hue_sensor_base.SENSOR_MANAGER_FORMAT.format("mock-host") + sm = hass.data[hue.DOMAIN][sm_key] + await sm.async_update_items() + + # To flush out the service call to update the group + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + sensor = hass.states.get("binary_sensor.living_room_sensor_motion") + assert sensor is not None + + removed_sensor = hass.states.get("binary_sensor.kitchen_sensor_motion") + assert removed_sensor is None + + async def test_update_timeout(hass, mock_bridge): """Test bridge marked as not available if timeout error during update.""" mock_bridge.api.sensors.update = Mock(side_effect=asyncio.TimeoutError) @@ -435,9 +466,9 @@ async def test_update_timeout(hass, mock_bridge): async def test_update_unauthorized(hass, mock_bridge): - """Test bridge marked as not available if unauthorized during update.""" + """Test bridge marked as not authorized if unauthorized during update.""" mock_bridge.api.sensors.update = Mock(side_effect=aiohue.Unauthorized) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 0 assert len(hass.states.async_all()) == 0 - assert mock_bridge.available is False + assert len(mock_bridge.handle_unauthorized_error.mock_calls) == 1 From 335872b54d520c3322619a1266045943824bb47e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 28 Oct 2019 16:54:15 +0100 Subject: [PATCH 1300/3953] Revert "More header cleanup for websocket proxy (#28288)" (#28293) This reverts commit 7887850505bbaef83467c732e07b23236e31f1d2. --- homeassistant/components/hassio/ingress.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 3b6fee563545e9..4ecb9a8419f529 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -211,10 +211,6 @@ def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]: hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE, hdrs.CONTENT_ENCODING, - hdrs.SEC_WEBSOCKET_EXTENSIONS, - hdrs.SEC_WEBSOCKET_PROTOCOL, - hdrs.SEC_WEBSOCKET_VERSION, - hdrs.SEC_WEBSOCKET_KEY, ): continue headers[name] = value From 31dd69196c65a550808ad96e52f9923beb5a56e5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 28 Oct 2019 12:39:37 -0500 Subject: [PATCH 1301/3953] Bump library to 0.0.3 (#28294) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 90ae305148ed5c..8edccda75e0d7e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==3.0.6", "plexauth==0.0.5", - "plexwebsocket==0.0.1" + "plexwebsocket==0.0.3" ], "dependencies": [ "http" diff --git a/requirements_all.txt b/requirements_all.txt index 1cbb3d33f38ea5..98c593fc6c041b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,7 +974,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.1 +plexwebsocket==0.0.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 861e1098664f7c..5987c3061b43b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.1 +plexwebsocket==0.0.3 # homeassistant.components.mhz19 # homeassistant.components.serial_pm From f7a64019b60c738cbcfb736a1c4edafb330bf8c0 Mon Sep 17 00:00:00 2001 From: Yann Jajkiewicz Date: Mon, 28 Oct 2019 19:22:15 +0100 Subject: [PATCH 1302/3953] Add support for Somfy Garage door Rollixo IO DiscreteGarageOpenerIOComponent in Tahoma component (#28291) --- homeassistant/components/tahoma/__init__.py | 1 + homeassistant/components/tahoma/cover.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 6bcc783400c8d3..9fc8ca3cf2e0a0 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -47,6 +47,7 @@ "io:VerticalExteriorAwningIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", "io:GarageOpenerIOComponent": "cover", + "io:DiscreteGarageOpenerIOComponent": "cover", "rtds:RTDSContactSensor": "sensor", "rtds:RTDSMotionSensor": "sensor", "rtds:RTDSSmokeSensor": "smoke", diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index 7448eb27ae072f..6c5dcbd807c4e3 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -37,6 +37,7 @@ "io:VerticalExteriorAwningIOComponent": DEVICE_CLASS_AWNING, "io:WindowOpenerVeluxIOComponent": DEVICE_CLASS_WINDOW, "io:GarageOpenerIOComponent": DEVICE_CLASS_GARAGE, + "io:DiscreteGarageOpenerIOComponent": DEVICE_CLASS_GARAGE, "rts:BlindRTSComponent": DEVICE_CLASS_BLIND, "rts:CurtainRTSComponent": DEVICE_CLASS_CURTAIN, "rts:DualCurtainRTSComponent": DEVICE_CLASS_CURTAIN, From 3cedee3feaced5752093b8f48aaf90bf1f027d24 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Oct 2019 06:55:42 +0800 Subject: [PATCH 1303/3953] Add above and below to sensor condition extra_fields (#27364) * Add above and below to sensor condition extra_fields * Change unit_of_measurement to suffix in extra_fields * Check if sensor has unit when getting capabilities * Improve tests --- .../components/device_automation/__init__.py | 6 +- .../components/sensor/device_condition.py | 27 +++++++ .../components/sensor/device_trigger.py | 8 +- .../sensor/test_device_condition.py | 81 +++++++++++++++++++ .../components/sensor/test_device_trigger.py | 35 ++++++++ 5 files changed, 155 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 0be1c3eb1dd63c..80e6403329531a 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -156,7 +156,11 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom # The device automation has no capabilities return {} - capabilities = await getattr(platform, function_name)(hass, automation) + try: + capabilities = await getattr(platform, function_name)(hass, automation) + except InvalidDeviceAutomationConfig: + return {} + capabilities = capabilities.copy() extra_fields = capabilities.get("extra_fields") diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 26479807991392..259fb5dbab90b9 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -2,6 +2,9 @@ from typing import Dict, List import voluptuous as vol +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.core import HomeAssistant from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -141,3 +144,27 @@ def async_condition_from_config( numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW] return condition.async_numeric_state_from_config(numeric_state_config) + + +async def async_get_condition_capabilities(hass, config): + """List condition capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + raise InvalidDeviceAutomationConfig + + return { + "extra_fields": vol.Schema( + { + vol.Optional( + CONF_ABOVE, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional( + CONF_BELOW, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + } + ) + } diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index b462124165af17..73e55340da9b6a 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -3,6 +3,9 @@ import homeassistant.components.automation.numeric_state as numeric_state_automation from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -146,9 +149,12 @@ async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" state = hass.states.get(config[CONF_ENTITY_ID]) unit_of_measurement = ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else "" + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None ) + if not state or not unit_of_measurement: + raise InvalidDeviceAutomationConfig + return { "extra_fields": vol.Schema( { diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index e28e487f4ef2bd..f3ff15c3ad9386 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -14,6 +14,7 @@ mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES @@ -73,6 +74,86 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["battery"].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "description": {"suffix": "%"}, + "name": "above", + "optional": True, + "type": "float", + }, + { + "description": {"suffix": "%"}, + "name": "below", + "optional": True, + "type": "float", + }, + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert len(conditions) == 1 + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + +async def test_get_condition_capabilities_none(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + conditions = [ + { + "condition": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": "sensor.beer", + "type": "is_battery_level", + }, + { + "condition": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": platform.ENTITIES["none"].entity_id, + "type": "is_battery_level", + }, + ] + + expected_capabilities = {} + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state_not_above_below(hass, calls, caplog): """Test for bad value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index a21839fcebc292..b7a921fff18034 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -124,6 +124,41 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities +async def test_get_trigger_capabilities_none(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + triggers = [ + { + "platform": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": "sensor.beer", + "type": "is_battery_level", + }, + { + "platform": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": platform.ENTITIES["none"].entity_id, + "type": "is_battery_level", + }, + ] + + expected_capabilities = {} + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + async def test_if_fires_not_on_above_below(hass, calls, caplog): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") From 4a25bab1b30fcdecc125762ea63548ea608266a9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Oct 2019 04:40:05 +0800 Subject: [PATCH 1304/3953] Fix broken deconz trigger (#28211) --- homeassistant/components/deconz/device_trigger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 27ff6fcd590ad9..2d097d30c0b488 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -235,6 +235,7 @@ async def async_attach_trigger(hass, config, action, automation_info): event_id = deconz_event.serial event_config = { + event.CONF_PLATFORM: "event", event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, } From 0e2b55e60e7fb65bf4a1854b6f1e4acd3a20e883 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 28 Oct 2019 12:39:37 -0500 Subject: [PATCH 1305/3953] Bump library to 0.0.3 (#28294) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 90ae305148ed5c..8edccda75e0d7e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==3.0.6", "plexauth==0.0.5", - "plexwebsocket==0.0.1" + "plexwebsocket==0.0.3" ], "dependencies": [ "http" diff --git a/requirements_all.txt b/requirements_all.txt index cd4a6fceb85ffb..869c6ea90c3dcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,7 +974,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.1 +plexwebsocket==0.0.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1783f192b24b30..72aa75a4676ba7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -346,7 +346,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.1 +plexwebsocket==0.0.3 # homeassistant.components.mhz19 # homeassistant.components.serial_pm From 070790ccc9f613eb1596897031d09cfe4a444b3a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Oct 2019 11:28:45 -0700 Subject: [PATCH 1306/3953] Bumped version to 0.101.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4d858deec8772c..2aa5e97fa94941 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From f88ead597a8da0212d1f34e80b5f9ef1ee1b940a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 28 Oct 2019 22:36:26 +0200 Subject: [PATCH 1307/3953] Type hint improvements (#28260) * Add and improve core and config_entries type hints * Complete and improve config_entries type hints * More entity registry type hints * Complete helpers.event type hints --- homeassistant/components/group/light.py | 5 +- homeassistant/components/switch/light.py | 5 +- homeassistant/config_entries.py | 73 ++++++++++++------- homeassistant/core.py | 2 +- homeassistant/data_entry_flow.py | 6 +- .../helpers/config_entry_oauth2_flow.py | 2 +- homeassistant/helpers/entity_registry.py | 48 ++++++------ homeassistant/helpers/event.py | 70 +++++++++++------- homeassistant/helpers/template.py | 13 +++- 9 files changed, 135 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 858045524948c5..2cd6502813164e 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -16,7 +16,7 @@ STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import State, callback +from homeassistant.core import CALLBACK_TYPE, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -96,7 +96,7 @@ def __init__(self, name: str, entity_ids: List[str]) -> None: self._effect_list: Optional[List[str]] = None self._effect: Optional[str] = None self._supported_features: int = 0 - self._async_unsub_state_changed = None + self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -108,6 +108,7 @@ def async_state_changed_listener( """Handle child updates.""" self.async_schedule_update_ha_state(True) + assert self.hass is not None self._async_unsub_state_changed = async_track_state_change( self.hass, self._entity_ids, async_state_changed_listener ) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index b0abf95799198d..1bdc1d39083ebb 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -12,7 +12,7 @@ STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import State, callback +from homeassistant.core import CALLBACK_TYPE, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change @@ -56,7 +56,7 @@ def __init__(self, name: str, switch_entity_id: str) -> None: self._switch_entity_id = switch_entity_id self._is_on = False self._available = False - self._async_unsub_state_changed = None + self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None @property def name(self) -> str: @@ -113,6 +113,7 @@ def async_state_changed_listener( """Handle child updates.""" self.async_schedule_update_ha_state(True) + assert self.hass is not None self._async_unsub_state_changed = async_track_state_change( self.hass, self._switch_entity_id, async_state_changed_listener ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index aee15d6c0ce9aa..ae7c534adf805b 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3,7 +3,7 @@ import logging import functools import uuid -from typing import Any, Callable, List, Optional, Set +from typing import Any, Callable, Dict, List, Optional, Set, cast import weakref import attr @@ -14,11 +14,11 @@ from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry from homeassistant.helpers import entity_registry +from homeassistant.helpers.event import Event -# mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) -_UNDEF = object() +_UNDEF: dict = {} SOURCE_USER = "user" SOURCE_DISCOVERY = "discovery" @@ -205,7 +205,7 @@ async def async_setup( wait_time, ) - async def setup_again(now): + async def setup_again(now: Any) -> None: """Run setup again.""" self._async_cancel_retry_setup = None await self.async_setup(hass, integration=integration, tries=tries) @@ -357,7 +357,7 @@ def add_update_listener(self, listener: Callable) -> Callable: return lambda: self.update_listeners.remove(weak_listener) - def as_dict(self): + def as_dict(self) -> Dict[str, Any]: """Return dictionary version of this entry.""" return { "entry_id": self.entry_id, @@ -418,7 +418,7 @@ def async_entries(self, domain: Optional[str] = None) -> List[ConfigEntry]: return list(self._entries) return [entry for entry in self._entries if entry.domain == domain] - async def async_remove(self, entry_id): + async def async_remove(self, entry_id: str) -> Dict[str, Any]: """Remove an entry.""" entry = self.async_get_entry(entry_id) @@ -529,8 +529,13 @@ async def async_reload(self, entry_id: str) -> bool: @callback def async_update_entry( - self, entry, *, data=_UNDEF, options=_UNDEF, system_options=_UNDEF - ): + self, + entry: ConfigEntry, + *, + data: dict = _UNDEF, + options: dict = _UNDEF, + system_options: dict = _UNDEF, + ) -> None: """Update a config entry.""" if data is not _UNDEF: entry.data = data @@ -547,7 +552,7 @@ def async_update_entry( self._async_schedule_save() - async def async_forward_entry_setup(self, entry, domain): + async def async_forward_entry_setup(self, entry: ConfigEntry, domain: str) -> bool: """Forward the setup of an entry to a different component. By default an entry is setup with the component it belongs to. If that @@ -567,8 +572,9 @@ async def async_forward_entry_setup(self, entry, domain): integration = await loader.async_get_integration(self.hass, domain) await entry.async_setup(self.hass, integration=integration) + return True - async def async_forward_entry_unload(self, entry, domain): + async def async_forward_entry_unload(self, entry: ConfigEntry, domain: str) -> bool: """Forward the unloading of an entry to a different component.""" # It was never loaded. if domain not in self.hass.config.components: @@ -578,7 +584,9 @@ async def async_forward_entry_unload(self, entry, domain): return await entry.async_unload(self.hass, integration=integration) - async def _async_finish_flow(self, flow, result): + async def _async_finish_flow( + self, flow: "ConfigFlow", result: Dict[str, Any] + ) -> Dict[str, Any]: """Finish a config flow and add an entry.""" # Remove notification if no other discovery config entries in progress if not any( @@ -611,7 +619,9 @@ async def _async_finish_flow(self, flow, result): result["result"] = entry return result - async def _async_create_flow(self, handler_key, *, context, data): + async def _async_create_flow( + self, handler_key: str, *, context: Dict[str, Any], data: Dict[str, Any] + ) -> "ConfigFlow": """Create a flow for specified handler. Handler key is the domain of the component that we want to set up. @@ -654,7 +664,7 @@ async def _async_create_flow(self, handler_key, *, context, data): notification_id=DISCOVERY_NOTIFICATION_ID, ) - flow = handler() + flow = cast(ConfigFlow, handler()) flow.init_step = source return flow @@ -663,12 +673,12 @@ def _async_schedule_save(self) -> None: self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self): + def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]: """Return data to save.""" return {"entries": [entry.as_dict() for entry in self._entries]} -async def _old_conf_migrator(old_config): +async def _old_conf_migrator(old_config: Dict[str, Any]) -> Dict[str, Any]: """Migrate the pre-0.73 config format to the latest version.""" return {"entries": old_config} @@ -686,18 +696,20 @@ def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None: @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> "OptionsFlow": """Get the options flow for this handler.""" raise data_entry_flow.UnknownHandler @callback - def _async_current_entries(self): + def _async_current_entries(self) -> List[ConfigEntry]: """Return current entries.""" + assert self.hass is not None return self.hass.config_entries.async_entries(self.handler) @callback - def _async_in_progress(self): + def _async_in_progress(self) -> List[Dict]: """Return other in progress flows for current domain.""" + assert self.hass is not None return [ flw for flw in self.hass.config_entries.flow.async_progress() @@ -715,29 +727,33 @@ def __init__(self, hass: HomeAssistant) -> None: hass, self._async_create_flow, self._async_finish_flow ) - async def _async_create_flow(self, entry_id, *, context, data): + async def _async_create_flow( + self, entry_id: str, *, context: Dict[str, Any], data: Dict[str, Any] + ) -> Optional["OptionsFlow"]: """Create an options flow for a config entry. Entry_id and flow.handler is the same thing to map entry with flow. """ entry = self.hass.config_entries.async_get_entry(entry_id) if entry is None: - return + return None if entry.domain not in HANDLERS: raise data_entry_flow.UnknownHandler - flow = HANDLERS[entry.domain].async_get_options_flow(entry) + flow = cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry)) return flow - async def _async_finish_flow(self, flow, result): + async def _async_finish_flow( + self, flow: "OptionsFlow", result: Dict[str, Any] + ) -> Optional[Dict[str, Any]]: """Finish an options flow and update options for configuration entry. Flow.handler and entry_id is the same thing to map flow with entry. """ entry = self.hass.config_entries.async_get_entry(flow.handler) if entry is None: - return + return None self.hass.config_entries.async_update_entry(entry, options=result["data"]) result["result"] = True @@ -747,7 +763,7 @@ async def _async_finish_flow(self, flow, result): class OptionsFlow(data_entry_flow.FlowHandler): """Base class for config option flows.""" - pass + handler: str @attr.s(slots=True) @@ -756,11 +772,11 @@ class SystemOptions: disable_new_entities = attr.ib(type=bool, default=False) - def update(self, *, disable_new_entities): + def update(self, *, disable_new_entities: bool) -> None: """Update properties.""" self.disable_new_entities = disable_new_entities - def as_dict(self): + def as_dict(self) -> Dict[str, Any]: """Return dictionary version of this config entrys system options.""" return {"disable_new_entities": self.disable_new_entities} @@ -784,7 +800,7 @@ def async_setup(self) -> None: entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entry_updated ) - async def _handle_entry_updated(self, event): + async def _handle_entry_updated(self, event: Event) -> None: """Handle entity registry entry update.""" if ( event.data["action"] != "update" @@ -811,6 +827,7 @@ async def _handle_entry_updated(self, event): config_entry = self.hass.config_entries.async_get_entry( entity_entry.config_entry_id ) + assert config_entry is not None if config_entry.entry_id not in self.changed and await support_entry_unload( self.hass, config_entry.domain @@ -830,7 +847,7 @@ async def _handle_entry_updated(self, event): self.RELOAD_AFTER_UPDATE_DELAY, self._handle_reload ) - async def _handle_reload(self, _now): + async def _handle_reload(self, _now: Any) -> None: """Handle a reload.""" self._remove_call_later = None to_reload = self.changed diff --git a/homeassistant/core.py b/homeassistant/core.py index ec11b14edaab17..01c5561d939f90 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1283,7 +1283,7 @@ def __init__(self, hass: HomeAssistant) -> None: self.skip_pip: bool = False # List of loaded components - self.components: set = set() + self.components: Set[str] = set() # API (HTTP) server configuration, see components.http.ApiConfig self.api: Optional[Any] = None diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index c06c69d9213a35..58d8e4ea131519 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,6 +1,6 @@ """Classes to help gather user submissions.""" import logging -from typing import Dict, Any, Callable, Hashable, List, Optional +from typing import Dict, Any, Callable, List, Optional import uuid import voluptuous as vol from .core import callback, HomeAssistant @@ -58,7 +58,7 @@ def async_progress(self) -> List[Dict]: ] async def async_init( - self, handler: Hashable, *, context: Optional[Dict] = None, data: Any = None + self, handler: str, *, context: Optional[Dict] = None, data: Any = None ) -> Any: """Start a configuration flow.""" if context is None: @@ -170,7 +170,7 @@ class FlowHandler: # Set by flow manager flow_id: str = None # type: ignore hass: Optional[HomeAssistant] = None - handler: Optional[Hashable] = None + handler: Optional[str] = None cur_step: Optional[Dict[str, str]] = None context: Dict diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index d3db8febcb2cdc..87832f60739791 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -399,7 +399,7 @@ async def async_ensure_token_valid(self) -> None: new_token = await self.implementation.async_refresh_token(token) - self.hass.config_entries.async_update_entry( # type: ignore + self.hass.config_entries.async_update_entry( self.config_entry, data={**self.config_entry.data, "token": new_token} ) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 00671e9c7763ae..08f29a9fb3ecda 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -7,15 +7,15 @@ registered. Registering a new entity while a timer is in progress resets the timer. """ -from asyncio import Event +import asyncio from collections import OrderedDict from itertools import chain import logging -from typing import List, Optional, cast +from typing import Any, Dict, Iterable, List, Optional, cast import attr -from homeassistant.core import callback, split_entity_id, valid_entity_id +from homeassistant.core import Event, callback, split_entity_id, valid_entity_id from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.loader import bind_hass from homeassistant.util import ensure_unique_string, slugify @@ -24,8 +24,7 @@ from .typing import HomeAssistantType -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any +# mypy: allow-untyped-defs, no-check-untyped-defs PATH_REGISTRY = "entity_registry.yaml" DATA_REGISTRY = "entity_registry" @@ -51,7 +50,7 @@ class RegistryEntry: platform = attr.ib(type=str) name = attr.ib(type=str, default=None) device_id = attr.ib(type=str, default=None) - config_entry_id = attr.ib(type=str, default=None) + config_entry_id: Optional[str] = attr.ib(default=None) disabled_by = attr.ib( type=Optional[str], default=None, @@ -68,12 +67,12 @@ class RegistryEntry: domain = attr.ib(type=str, init=False, repr=False) @domain.default - def _domain_default(self): + def _domain_default(self) -> str: """Compute domain value.""" return split_entity_id(self.entity_id)[0] @property - def disabled(self): + def disabled(self) -> bool: """Return if entry is disabled.""" return self.disabled_by is not None @@ -81,17 +80,17 @@ def disabled(self): class EntityRegistry: """Class to hold a registry of entities.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistantType): """Initialize the registry.""" self.hass = hass - self.entities = None + self.entities: Dict[str, RegistryEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self.hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self.async_device_removed ) @callback - def async_is_registered(self, entity_id): + def async_is_registered(self, entity_id: str) -> bool: """Check if an entity_id is currently registered.""" return entity_id in self.entities @@ -116,8 +115,11 @@ def async_get_entity_id( @callback def async_generate_entity_id( - self, domain, suggested_object_id, known_object_ids=None - ): + self, + domain: str, + suggested_object_id: str, + known_object_ids: Optional[Iterable[str]] = None, + ) -> str: """Generate an entity ID that does not conflict. Conflicts checked against registered and currently existing entities. @@ -195,7 +197,7 @@ def async_get_or_create( return entity @callback - def async_remove(self, entity_id): + def async_remove(self, entity_id: str) -> None: """Remove an entity from registry.""" self.entities.pop(entity_id) self.hass.bus.async_fire( @@ -204,7 +206,7 @@ def async_remove(self, entity_id): self.async_schedule_save() @callback - def async_device_removed(self, event): + def async_device_removed(self, event: Event) -> None: """Handle the removal of a device. Remove entities from the registry that are associated to a device when @@ -309,7 +311,7 @@ def _async_update_entity( return new - async def async_load(self): + async def async_load(self) -> None: """Load the entity registry.""" data = await self.hass.helpers.storage.async_migrator( self.hass.config.path(PATH_REGISTRY), @@ -317,7 +319,7 @@ async def async_load(self): old_conf_load_func=load_yaml, old_conf_migrate_func=_async_migrate, ) - entities = OrderedDict() + entities: Dict[str, RegistryEntry] = OrderedDict() if data is not None: for entity in data["entities"]: @@ -334,12 +336,12 @@ async def async_load(self): self.entities = entities @callback - def async_schedule_save(self): + def async_schedule_save(self) -> None: """Schedule saving the entity registry.""" self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self): + def _data_to_save(self) -> Dict[str, Any]: """Return data of entity registry to store in a file.""" data = {} @@ -359,7 +361,7 @@ def _data_to_save(self): return data @callback - def async_clear_config_entry(self, config_entry): + def async_clear_config_entry(self, config_entry: str) -> None: """Clear config entry from registry entries.""" for entity_id in [ entity_id @@ -375,7 +377,7 @@ async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: reg_or_evt = hass.data.get(DATA_REGISTRY) if not reg_or_evt: - evt = hass.data[DATA_REGISTRY] = Event() + evt = hass.data[DATA_REGISTRY] = asyncio.Event() reg = EntityRegistry(hass) await reg.async_load() @@ -384,7 +386,7 @@ async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: evt.set() return reg - if isinstance(reg_or_evt, Event): + if isinstance(reg_or_evt, asyncio.Event): evt = reg_or_evt await evt.wait() return cast(EntityRegistry, hass.data.get(DATA_REGISTRY)) @@ -402,7 +404,7 @@ def async_entries_for_device( ] -async def _async_migrate(entities): +async def _async_migrate(entities: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]: """Migrate the YAML config file to storage helper format.""" return { "entities": [ diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index e819da9873abc7..715344a3969081 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,13 +1,14 @@ """Helpers for listening to events.""" from datetime import datetime, timedelta import functools as ft -from typing import Any, Callable, Iterable, Optional, Union +from typing import Any, Callable, Dict, Iterable, Optional, Union, cast import attr from homeassistant.loader import bind_hass from homeassistant.helpers.sun import get_astral_event_next -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event +from homeassistant.helpers.template import Template +from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event, State from homeassistant.const import ( ATTR_NOW, EVENT_STATE_CHANGED, @@ -21,16 +22,15 @@ from homeassistant.util.async_ import run_callback_threadsafe -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name -def threaded_listener_factory(async_factory): +def threaded_listener_factory(async_factory: Callable[..., Any]) -> CALLBACK_TYPE: """Convert an async event helper to a threaded one.""" @ft.wraps(async_factory) - def factory(*args, **kwargs): + def factory(*args: Any, **kwargs: Any) -> CALLBACK_TYPE: """Call async event helper safely.""" hass = args[0] @@ -41,7 +41,7 @@ def factory(*args, **kwargs): hass.loop, ft.partial(async_factory, *args, **kwargs) ).result() - def remove(): + def remove() -> None: """Threadsafe removal.""" run_callback_threadsafe(hass.loop, async_remove).result() @@ -52,7 +52,13 @@ def remove(): @callback @bind_hass -def async_track_state_change(hass, entity_ids, action, from_state=None, to_state=None): +def async_track_state_change( + hass: HomeAssistant, + entity_ids: Union[str, Iterable[str]], + action: Callable[[str, State, State], None], + from_state: Union[None, str, Iterable[str]] = None, + to_state: Union[None, str, Iterable[str]] = None, +) -> CALLBACK_TYPE: """Track specific state changes. entity_ids, from_state and to_state can be string or list. @@ -74,9 +80,12 @@ def async_track_state_change(hass, entity_ids, action, from_state=None, to_state entity_ids = tuple(entity_id.lower() for entity_id in entity_ids) @callback - def state_change_listener(event): + def state_change_listener(event: Event) -> None: """Handle specific state changes.""" - if entity_ids != MATCH_ALL and event.data.get("entity_id") not in entity_ids: + if ( + entity_ids != MATCH_ALL + and cast(str, event.data.get("entity_id")) not in entity_ids + ): return old_state = event.data.get("old_state") @@ -103,7 +112,12 @@ def state_change_listener(event): @callback @bind_hass -def async_track_template(hass, template, action, variables=None): +def async_track_template( + hass: HomeAssistant, + template: Template, + action: Callable[[str, State, State], None], + variables: Optional[Dict[str, Any]] = None, +) -> CALLBACK_TYPE: """Add a listener that track state changes with template condition.""" from . import condition @@ -111,7 +125,7 @@ def async_track_template(hass, template, action, variables=None): already_triggered = False @callback - def template_condition_listener(entity_id, from_s, to_s): + def template_condition_listener(entity_id: str, from_s: State, to_s: State) -> None: """Check if condition is correct and run action.""" nonlocal already_triggered template_result = condition.async_template(hass, template, variables) @@ -134,18 +148,22 @@ def template_condition_listener(entity_id, from_s, to_s): @callback @bind_hass def async_track_same_state( - hass, period, action, async_check_same_func, entity_ids=MATCH_ALL -): + hass: HomeAssistant, + period: timedelta, + action: Callable[..., None], + async_check_same_func: Callable[[str, State, State], bool], + entity_ids: Union[str, Iterable[str]] = MATCH_ALL, +) -> CALLBACK_TYPE: """Track the state of entities for a period and run an action. If async_check_func is None it use the state of orig_value. Without entity_ids we track all state changes. """ - async_remove_state_for_cancel = None - async_remove_state_for_listener = None + async_remove_state_for_cancel: Optional[CALLBACK_TYPE] = None + async_remove_state_for_listener: Optional[CALLBACK_TYPE] = None @callback - def clear_listener(): + def clear_listener() -> None: """Clear all unsub listener.""" nonlocal async_remove_state_for_cancel, async_remove_state_for_listener @@ -157,7 +175,7 @@ def clear_listener(): async_remove_state_for_cancel = None @callback - def state_for_listener(now): + def state_for_listener(now: Any) -> None: """Fire on state changes after a delay and calls action.""" nonlocal async_remove_state_for_listener async_remove_state_for_listener = None @@ -165,7 +183,9 @@ def state_for_listener(now): hass.async_run_job(action) @callback - def state_for_cancel_listener(entity, from_state, to_state): + def state_for_cancel_listener( + entity: str, from_state: State, to_state: State + ) -> None: """Fire on changes and cancel for listener if changed.""" if not async_check_same_func(entity, from_state, to_state): clear_listener() @@ -193,7 +213,7 @@ def async_track_point_in_time( utc_point_in_time = dt_util.as_utc(point_in_time) @callback - def utc_converter(utc_now): + def utc_converter(utc_now: datetime) -> None: """Convert passed in UTC now to local now.""" hass.async_run_job(action, dt_util.as_local(utc_now)) @@ -213,7 +233,7 @@ def async_track_point_in_utc_time( point_in_time = dt_util.as_utc(point_in_time) @callback - def point_in_time_listener(event): + def point_in_time_listener(event: Event) -> None: """Listen for matching time_changed events.""" now = event.data[ATTR_NOW] @@ -225,7 +245,7 @@ def point_in_time_listener(event): # available to execute this listener it might occur that the # listener gets lined up twice to be executed. This will make # sure the second time it does nothing. - point_in_time_listener.run = True + setattr(point_in_time_listener, "run", True) async_unsub() hass.async_run_job(action, now) @@ -260,12 +280,12 @@ def async_track_time_interval( """Add a listener that fires repetitively at every timedelta interval.""" remove = None - def next_interval(): + def next_interval() -> datetime: """Return the next interval.""" return dt_util.utcnow() + interval @callback - def interval_listener(now): + def interval_listener(now: datetime) -> None: """Handle elapsed intervals.""" nonlocal remove remove = async_track_point_in_utc_time(hass, interval_listener, next_interval()) @@ -273,7 +293,7 @@ def interval_listener(now): remove = async_track_point_in_utc_time(hass, interval_listener, next_interval()) - def remove_listener(): + def remove_listener() -> None: """Remove interval listener.""" remove() @@ -387,7 +407,7 @@ def async_track_utc_time_change( if all(val is None for val in (hour, minute, second)): @callback - def time_change_listener(event): + def time_change_listener(event: Event) -> None: """Fire every time event that comes in.""" hass.async_run_job(action, event.data[ATTR_NOW]) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 1d9ca6914510c1..aa17b2a1fba975 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -7,7 +7,7 @@ import re from datetime import datetime from functools import wraps -from typing import Any, Iterable +from typing import Any, Dict, Iterable, List, Optional, Union import jinja2 from jinja2 import contextfilter, contextfunction @@ -72,7 +72,9 @@ def render_complex(value, variables=None): return value.async_render(variables) -def extract_entities(template, variables=None): +def extract_entities( + template: Optional[str], variables: Optional[Dict[str, Any]] = None +) -> Union[str, List[str]]: """Extract all entities for state_changed listener from template string.""" if template is None or _RE_JINJA_DELIMITERS.search(template) is None: return [] @@ -86,6 +88,7 @@ def extract_entities(template, variables=None): for result in extraction: if ( result[0] == "trigger.entity_id" + and variables and "trigger" in variables and "entity_id" in variables["trigger"] ): @@ -163,7 +166,7 @@ def __init__(self, template, hass=None): if not isinstance(template, str): raise TypeError("Expected template to be a string") - self.template = template + self.template: str = template self._compiled_code = None self._compiled = None self.hass = hass @@ -187,7 +190,9 @@ def ensure_valid(self): except jinja2.exceptions.TemplateSyntaxError as err: raise TemplateError(err) - def extract_entities(self, variables=None): + def extract_entities( + self, variables: Dict[str, Any] = None + ) -> Union[str, List[str]]: """Extract all entities for state_changed listener.""" return extract_entities(self.template, variables) From 0ef99934b70c7c8f27bacf9dc985e12a9705dab5 Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Mon, 28 Oct 2019 16:42:06 -0700 Subject: [PATCH 1308/3953] Add more iaqualink entity properties, fix timeout issues (#28236) * iaqualink: implement some more entity properties * Style fixes --- .../components/iaqualink/__init__.py | 27 ++++++++++++++++--- .../components/iaqualink/manifest.json | 8 ++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 9ce0e04895f2e6..c3fa2bb1eb8d44 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -2,8 +2,8 @@ import asyncio from functools import wraps import logging +from typing import Any, Dict -from aiohttp import ClientTimeout import voluptuous as vol from iaqualink import ( @@ -26,7 +26,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] switches = hass.data[DOMAIN][SWITCH_DOMAIN] = [] - session = async_create_clientsession(hass, timeout=ClientTimeout(total=5)) + session = async_get_clientsession(hass) aqualink = AqualinkClient(username, password, session) try: await aqualink.login() @@ -210,3 +210,24 @@ def should_poll(self) -> bool: def unique_id(self) -> str: """Return a unique identifier for this entity.""" return f"{self.dev.system.serial}_{self.dev.name}" + + @property + def assumed_state(self) -> bool: + """Return whether the state is based on actual reading from the device.""" + return not self.dev.system.last_run_success + + @property + def available(self) -> bool: + """Return whether the device is available or not.""" + return self.dev.system.online + + @property + def device_info(self) -> Dict[str, Any]: + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "model": self.dev.__class__.__name__.replace("Aqualink", ""), + "manufacturer": "Jandy", + "via_device": (DOMAIN, self.dev.system.serial), + } diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index e883aec371c06b..85392e6371bee8 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,10 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "dependencies": [], - "codeowners": [ - "@flz" - ], - "requirements": [ - "iaqualink==0.2.9" - ] + "codeowners": ["@flz"], + "requirements": ["iaqualink==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 98c593fc6c041b..5679d126e2d66c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -676,7 +676,7 @@ hydrawiser==0.1.1 # i2csense==0.0.4 # homeassistant.components.iaqualink -iaqualink==0.2.9 +iaqualink==0.3.0 # homeassistant.components.watson_tts ibm-watson==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5987c3061b43b6..7a13a44288aa87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -262,7 +262,7 @@ httplib2==0.10.3 huawei-lte-api==1.4.3 # homeassistant.components.iaqualink -iaqualink==0.2.9 +iaqualink==0.3.0 # homeassistant.components.influxdb influxdb==5.2.3 From 1e27a1f2b901474523a86218a9b3eee56cc127e3 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Tue, 29 Oct 2019 00:59:13 +0100 Subject: [PATCH 1309/3953] Add keyboard_remote trigger on multiple event types and emulate key hold events (#27761) * convert keyboard_remote to async and add possibility to trigger on multiple event types, as well as emulate key hold events * update requirements * cleanup shutdown handling and config handling as well as address other minor comments * cleanup unused return values and debug message formatting * move start and stop event listen to separate coroutine plus minor cleanup * make setup coroutine a function * fix import order and attribute defined outside of init * add to codeowners * update codeowners --- CODEOWNERS | 1 + .../components/keyboard_remote/__init__.py | 391 +++++++++++------- .../components/keyboard_remote/manifest.json | 4 +- requirements_all.txt | 5 +- 4 files changed, 252 insertions(+), 149 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 46ffd1196f7f65..dac590399350e4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -158,6 +158,7 @@ homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph homeassistant/components/keenetic_ndms2/* @foxel +homeassistant/components/keyboard_remote/* @bendavid homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills homeassistant/components/konnected/* @heythisisnate diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index 8b901dcc61e930..d4ed6128cbe7f7 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -1,12 +1,11 @@ """Receive signals from a keyboard and use it as a remote control.""" # pylint: disable=import-error -import threading import logging -import os -import time +import asyncio +from evdev import InputDevice, categorize, ecodes, list_devices +import aionotify import voluptuous as vol - import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP @@ -26,6 +25,11 @@ KEYBOARD_REMOTE_DISCONNECTED = "keyboard_remote_disconnected" TYPE = "type" +EMULATE_KEY_HOLD = "emulate_key_hold" +EMULATE_KEY_HOLD_DELAY = "emulate_key_hold_delay" +EMULATE_KEY_HOLD_REPEAT = "emulate_key_hold_repeat" + +DEVINPUT = "/dev/input" CONFIG_SCHEMA = vol.Schema( { @@ -36,11 +40,15 @@ { vol.Exclusive(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP): cv.string, vol.Exclusive(DEVICE_NAME, DEVICE_ID_GROUP): cv.string, - vol.Optional(TYPE, default="key_up"): vol.All( - cv.string, vol.Any("key_up", "key_down", "key_hold") + vol.Optional(TYPE, default=["key_up"]): vol.All( + cv.ensure_list, [vol.In(KEY_VALUE)] ), + vol.Optional(EMULATE_KEY_HOLD, default=False): cv.boolean, + vol.Optional(EMULATE_KEY_HOLD_DELAY, default=0.250): float, + vol.Optional(EMULATE_KEY_HOLD_REPEAT, default=0.033): float, } - ) + ), + cv.has_at_least_one_key(DEVICE_DESCRIPTOR, DEVICE_ID_GROUP), ], ) }, @@ -48,165 +56,256 @@ ) -def setup(hass, config): +async def async_setup(hass, config): """Set up the keyboard_remote.""" config = config.get(DOMAIN) - keyboard_remote = KeyboardRemote(hass, config) - - def _start_keyboard_remote(_event): - keyboard_remote.run() - - def _stop_keyboard_remote(_event): - keyboard_remote.stop() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_keyboard_remote) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_keyboard_remote) + remote = KeyboardRemote(hass, config) + remote.setup() return True -class KeyboardRemoteThread(threading.Thread): - """This interfaces with the inputdevice using evdev.""" +class KeyboardRemote: + """Manage device connection/disconnection using inotify to asynchronously monitor.""" - def __init__(self, hass, device_name, device_descriptor, key_value): - """Construct a thread listening for events on one device.""" + def __init__(self, hass, config): + """Create handlers and setup dictionaries to keep track of them.""" self.hass = hass - self.device_name = device_name - self.device_descriptor = device_descriptor - self.key_value = key_value - - if self.device_descriptor: - self.device_id = self.device_descriptor - else: - self.device_id = self.device_name - - self.dev = self._get_keyboard_device() - if self.dev is not None: - _LOGGER.debug("Keyboard connected, %s", self.device_id) - else: - _LOGGER.debug( - "Keyboard not connected, %s. " "Check /dev/input/event* permissions", - self.device_id, - ) - - id_folder = "/dev/input/by-id/" - - if os.path.isdir(id_folder): - from evdev import InputDevice, list_devices - - device_names = [ - InputDevice(file_name).name for file_name in list_devices() - ] - _LOGGER.debug( - "Possible device names are: %s. " - "Possible device descriptors are %s: %s", - device_names, - id_folder, - os.listdir(id_folder), - ) + self.handlers_by_name = {} + self.handlers_by_descriptor = {} + self.active_handlers_by_descriptor = {} + self.watcher = None + self.monitor_task = None - threading.Thread.__init__(self) - self.stopped = threading.Event() - self.hass = hass + for dev_block in config: + handler = self.DeviceHandler(hass, dev_block) + descriptor = dev_block.get(DEVICE_DESCRIPTOR) + if descriptor is not None: + self.handlers_by_descriptor[descriptor] = handler + else: + name = dev_block.get(DEVICE_NAME) + self.handlers_by_name[name] = handler - def _get_keyboard_device(self): - """Get the keyboard device.""" - from evdev import InputDevice, list_devices + def setup(self): + """Listen for Home Assistant start and stop events.""" - if self.device_name: - devices = [InputDevice(file_name) for file_name in list_devices()] - for device in devices: - if self.device_name == device.name: - return device - elif self.device_descriptor: - try: - device = InputDevice(self.device_descriptor) - except OSError: - pass - else: - return device - return None - - def run(self): - """Run the loop of the KeyboardRemote.""" - from evdev import categorize, ecodes - - if self.dev is not None: - self.dev.grab() - _LOGGER.debug("Interface started for %s", self.dev) - - while not self.stopped.isSet(): - # Sleeps to ease load on processor - time.sleep(0.05) - - if self.dev is None: - self.dev = self._get_keyboard_device() - if self.dev is not None: - self.dev.grab() - self.hass.bus.fire( - KEYBOARD_REMOTE_CONNECTED, - { - DEVICE_DESCRIPTOR: self.device_descriptor, - DEVICE_NAME: self.device_name, - }, - ) - _LOGGER.debug("Keyboard re-connected, %s", self.device_id) - else: - continue + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self.async_start_monitoring + ) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.async_stop_monitoring + ) - try: - event = self.dev.read_one() - except OSError: # Keyboard Disconnected - self.dev = None - self.hass.bus.fire( - KEYBOARD_REMOTE_DISCONNECTED, - { - DEVICE_DESCRIPTOR: self.device_descriptor, - DEVICE_NAME: self.device_name, - }, - ) - _LOGGER.debug("Keyboard disconnected, %s", self.device_id) - continue + async def async_start_monitoring(self, event): + """Start monitoring of events and devices. + + Start inotify watching for events, start event monitoring for those already + connected, and start monitoring for device connection/disconnection. + """ + + # start watching + self.watcher = aionotify.Watcher() + self.watcher.watch( + alias="devinput", + path=DEVINPUT, + flags=aionotify.Flags.CREATE + | aionotify.Flags.ATTRIB + | aionotify.Flags.DELETE, + ) + await self.watcher.setup(self.hass.loop) + + # add initial devices (do this AFTER starting watcher in order to + # avoid race conditions leading to missing device connections) + initial_start_monitoring = set() + descriptors = list_devices(DEVINPUT) + for descriptor in descriptors: + dev, handler = self.get_device_handler(descriptor) - if not event: + if handler is None: continue - if event.type is ecodes.EV_KEY and event.value is self.key_value: - _LOGGER.debug(categorize(event)) - self.hass.bus.fire( - KEYBOARD_REMOTE_COMMAND_RECEIVED, - { - KEY_CODE: event.code, - DEVICE_DESCRIPTOR: self.device_descriptor, - DEVICE_NAME: self.device_name, - }, - ) + self.active_handlers_by_descriptor[descriptor] = handler + initial_start_monitoring.add(handler.async_start_monitoring(dev)) + if initial_start_monitoring: + await asyncio.wait(initial_start_monitoring) -class KeyboardRemote: - """Sets up one thread per device.""" + self.monitor_task = self.hass.async_create_task(self.async_monitor_devices()) - def __init__(self, hass, config): - """Construct a KeyboardRemote interface object.""" - self.threads = [] - for dev_block in config: - device_descriptor = dev_block.get(DEVICE_DESCRIPTOR) - device_name = dev_block.get(DEVICE_NAME) - key_value = KEY_VALUE.get(dev_block.get(TYPE, "key_up")) + async def async_stop_monitoring(self, event): + """Stop and cleanup running monitoring tasks.""" + + _LOGGER.debug("Cleanup on shutdown") + + if self.monitor_task is not None: + if not self.monitor_task.done(): + self.monitor_task.cancel() + await self.monitor_task + + handler_stop_monitoring = set() + for handler in self.active_handlers_by_descriptor.values(): + handler_stop_monitoring.add(handler.async_stop_monitoring()) + + if handler_stop_monitoring: + await asyncio.wait(handler_stop_monitoring) + + def get_device_handler(self, descriptor): + """Find the correct device handler given a descriptor (path).""" + + # devices are often added and then correct permissions set after + try: + dev = InputDevice(descriptor) + except (OSError, PermissionError): + return (None, None) + + handler = None + if descriptor in self.handlers_by_descriptor: + handler = self.handlers_by_descriptor[descriptor] + elif dev.name in self.handlers_by_name: + handler = self.handlers_by_name[dev.name] + + return (dev, handler) + + async def async_monitor_devices(self): + """Monitor asynchronously for device connection/disconnection or permissions changes.""" + + try: + while True: + event = await self.watcher.get_event() + descriptor = f"{DEVINPUT}/{event.name}" + + descriptor_active = descriptor in self.active_handlers_by_descriptor + + if (event.flags & aionotify.Flags.DELETE) and descriptor_active: + handler = self.active_handlers_by_descriptor[descriptor] + del self.active_handlers_by_descriptor[descriptor] + await handler.async_stop_monitoring() + elif ( + (event.flags & aionotify.Flags.CREATE) + or (event.flags & aionotify.Flags.ATTRIB) + ) and not descriptor_active: + dev, handler = self.get_device_handler(descriptor) + if handler is None: + continue + self.active_handlers_by_descriptor[descriptor] = handler + await handler.async_start_monitoring(dev) + except asyncio.CancelledError: + return + + class DeviceHandler: + """Manage input events using evdev with asyncio.""" + + def __init__(self, hass, dev_block): + """Fill configuration data.""" - if device_descriptor is not None or device_name is not None: - thread = KeyboardRemoteThread( - hass, device_name, device_descriptor, key_value + self.hass = hass + + key_types = dev_block.get(TYPE) + + self.key_values = set() + for key_type in key_types: + self.key_values.add(KEY_VALUE[key_type]) + + self.emulate_key_hold = dev_block.get(EMULATE_KEY_HOLD) + self.emulate_key_hold_delay = dev_block.get(EMULATE_KEY_HOLD_DELAY) + self.emulate_key_hold_repeat = dev_block.get(EMULATE_KEY_HOLD_REPEAT) + self.monitor_task = None + self.dev = None + + async def async_keyrepeat(self, path, name, code, delay, repeat): + """Emulate keyboard delay/repeat behaviour by sending key events on a timer.""" + + await asyncio.sleep(delay) + while True: + self.hass.bus.async_fire( + KEYBOARD_REMOTE_COMMAND_RECEIVED, + {KEY_CODE: code, DEVICE_DESCRIPTOR: path, DEVICE_NAME: name}, + ) + await asyncio.sleep(repeat) + + async def async_start_monitoring(self, dev): + """Start event monitoring task and issue event.""" + if self.monitor_task is None: + self.dev = dev + self.monitor_task = self.hass.async_create_task( + self.async_monitor_input(dev) ) - self.threads.append(thread) + self.hass.bus.async_fire( + KEYBOARD_REMOTE_CONNECTED, + {DEVICE_DESCRIPTOR: dev.path, DEVICE_NAME: dev.name}, + ) + _LOGGER.debug("Keyboard (re-)connected, %s", dev.name) + + async def async_stop_monitoring(self): + """Stop event monitoring task and issue event.""" + if self.monitor_task is not None: + try: + self.dev.ungrab() + except OSError: + pass + # monitoring of the device form the event loop and closing of the + # device has to occur before cancelling the task to avoid + # triggering unhandled exceptions inside evdev coroutines + asyncio.get_event_loop().remove_reader(self.dev.fileno()) + self.dev.close() + if not self.monitor_task.done(): + self.monitor_task.cancel() + await self.monitor_task + self.monitor_task = None + self.hass.bus.async_fire( + KEYBOARD_REMOTE_DISCONNECTED, + {DEVICE_DESCRIPTOR: self.dev.path, DEVICE_NAME: self.dev.name}, + ) + _LOGGER.debug("Keyboard disconnected, %s", self.dev.name) + self.dev = None - def run(self): - """Run all event listener threads.""" - for thread in self.threads: - thread.start() + async def async_monitor_input(self, dev): + """Event monitoring loop. - def stop(self): - """Stop all event listener threads.""" - for thread in self.threads: - thread.stopped.set() + Monitor one device for new events using evdev with asyncio, + start and stop key hold emulation tasks as needed. + """ + + repeat_tasks = {} + + try: + _LOGGER.debug("Start device monitoring") + dev.grab() + async for event in dev.async_read_loop(): + if event.type is ecodes.EV_KEY: + if event.value in self.key_values: + _LOGGER.debug(categorize(event)) + self.hass.bus.async_fire( + KEYBOARD_REMOTE_COMMAND_RECEIVED, + { + KEY_CODE: event.code, + DEVICE_DESCRIPTOR: dev.path, + DEVICE_NAME: dev.name, + }, + ) + + if ( + event.value == KEY_VALUE["key_down"] + and self.emulate_key_hold + ): + repeat_tasks[event.code] = self.hass.async_create_task( + self.async_keyrepeat( + dev.path, + dev.name, + event.code, + self.emulate_key_hold_delay, + self.emulate_key_hold_repeat, + ) + ) + elif event.value == KEY_VALUE["key_up"]: + if event.code in repeat_tasks: + repeat_tasks[event.code].cancel() + del repeat_tasks[event.code] + except (OSError, PermissionError, asyncio.CancelledError): + # cancel key repeat tasks + for task in repeat_tasks.values(): + task.cancel() + + if repeat_tasks: + await asyncio.wait(repeat_tasks.values()) diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index 6172de132bb8e1..25b8bfa682a1c6 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -3,8 +3,8 @@ "name": "Keyboard remote", "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", "requirements": [ - "evdev==0.6.1" + "evdev==1.1.2", "aionotify==0.2.0" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@bendavid"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5679d126e2d66c..a1db7bbc1f7957 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,6 +169,9 @@ aiolifx==0.6.7 # homeassistant.components.lifx aiolifx_effects==0.2.2 +# homeassistant.components.keyboard_remote +aionotify==0.2.0 + # homeassistant.components.notion aionotion==1.1.0 @@ -477,7 +480,7 @@ epsonprinter==0.0.9 eternalegypt==0.0.10 # homeassistant.components.keyboard_remote -# evdev==0.6.1 +# evdev==1.1.2 # homeassistant.components.evohome evohome-async==0.3.4b1 From 0e4331e922ca1747b6234b206ba704a5f543647a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 29 Oct 2019 00:32:16 +0000 Subject: [PATCH 1310/3953] [ci skip] Translation update --- .../components/abode/.translations/cs.json | 22 +++++++++++ .../alarm_control_panel/.translations/cs.json | 11 ++++++ .../components/axis/.translations/cs.json | 5 +++ .../binary_sensor/.translations/cs.json | 8 ++++ .../cert_expiry/.translations/cs.json | 8 ++++ .../cert_expiry/.translations/pl.json | 2 +- .../coolmaster/.translations/cs.json | 23 +++++++++++ .../coolmaster/.translations/pl.json | 11 ++++-- .../components/cover/.translations/cs.json | 12 ++++++ .../components/cover/.translations/no.json | 4 +- .../components/cover/.translations/pl.json | 4 +- .../components/daikin/.translations/pl.json | 2 +- .../components/deconz/.translations/cs.json | 1 + .../device_tracker/.translations/cs.json | 8 ++++ .../device_tracker/.translations/no.json | 8 ++++ .../device_tracker/.translations/pl.json | 8 ++++ .../dialogflow/.translations/pl.json | 2 +- .../components/geofency/.translations/pl.json | 2 +- .../gpslogger/.translations/pl.json | 2 +- .../huawei_lte/.translations/cs.json | 38 ++++++++++++++++++ .../huawei_lte/.translations/no.json | 39 +++++++++++++++++++ .../huawei_lte/.translations/pl.json | 39 +++++++++++++++++++ .../components/ifttt/.translations/pl.json | 2 +- .../components/locative/.translations/pl.json | 2 +- .../components/lock/.translations/cs.json | 13 +++++++ .../logi_circle/.translations/pl.json | 2 +- .../components/mailgun/.translations/pl.json | 2 +- .../media_player/.translations/cs.json | 11 ++++++ .../media_player/.translations/no.json | 11 ++++++ .../media_player/.translations/pl.json | 11 ++++++ .../components/neato/.translations/cs.json | 7 ++++ .../components/nest/.translations/pl.json | 4 +- .../opentherm_gw/.translations/pl.json | 2 +- .../owntracks/.translations/pl.json | 2 +- .../components/plaato/.translations/pl.json | 2 +- .../components/plex/.translations/cs.json | 13 +++++++ .../components/plex/.translations/pl.json | 2 +- .../components/point/.translations/pl.json | 2 +- .../components/ps4/.translations/pl.json | 2 +- .../components/sensor/.translations/cs.json | 26 +++++++++++++ .../components/solarlog/.translations/cs.json | 21 ++++++++++ .../components/soma/.translations/cs.json | 14 +++++++ .../components/soma/.translations/pl.json | 2 +- .../components/somfy/.translations/cs.json | 9 +++++ .../components/somfy/.translations/no.json | 5 +++ .../components/somfy/.translations/pl.json | 7 +++- .../tellduslive/.translations/pl.json | 2 +- .../components/traccar/.translations/pl.json | 2 +- .../components/tradfri/.translations/pl.json | 2 +- .../transmission/.translations/cs.json | 17 ++++++++ .../transmission/.translations/pl.json | 5 ++- .../components/twilio/.translations/pl.json | 2 +- .../components/unifi/.translations/cs.json | 9 +++++ .../components/withings/.translations/cs.json | 13 +++++++ .../components/withings/.translations/no.json | 7 ++++ .../components/withings/.translations/pl.json | 7 ++++ 56 files changed, 470 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/abode/.translations/cs.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/cs.json create mode 100644 homeassistant/components/axis/.translations/cs.json create mode 100644 homeassistant/components/binary_sensor/.translations/cs.json create mode 100644 homeassistant/components/cert_expiry/.translations/cs.json create mode 100644 homeassistant/components/coolmaster/.translations/cs.json create mode 100644 homeassistant/components/cover/.translations/cs.json create mode 100644 homeassistant/components/device_tracker/.translations/cs.json create mode 100644 homeassistant/components/device_tracker/.translations/no.json create mode 100644 homeassistant/components/device_tracker/.translations/pl.json create mode 100644 homeassistant/components/huawei_lte/.translations/cs.json create mode 100644 homeassistant/components/huawei_lte/.translations/no.json create mode 100644 homeassistant/components/huawei_lte/.translations/pl.json create mode 100644 homeassistant/components/lock/.translations/cs.json create mode 100644 homeassistant/components/media_player/.translations/cs.json create mode 100644 homeassistant/components/media_player/.translations/no.json create mode 100644 homeassistant/components/media_player/.translations/pl.json create mode 100644 homeassistant/components/neato/.translations/cs.json create mode 100644 homeassistant/components/plex/.translations/cs.json create mode 100644 homeassistant/components/sensor/.translations/cs.json create mode 100644 homeassistant/components/solarlog/.translations/cs.json create mode 100644 homeassistant/components/soma/.translations/cs.json create mode 100644 homeassistant/components/somfy/.translations/cs.json create mode 100644 homeassistant/components/transmission/.translations/cs.json create mode 100644 homeassistant/components/withings/.translations/cs.json diff --git a/homeassistant/components/abode/.translations/cs.json b/homeassistant/components/abode/.translations/cs.json new file mode 100644 index 00000000000000..75c65f01e113e6 --- /dev/null +++ b/homeassistant/components/abode/.translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Je povolena pouze jedna konfigurace Abode." + }, + "error": { + "connection_error": "Nelze se p\u0159ipojit k Abode.", + "identifier_exists": "\u00da\u010det je ji\u017e zaregistrov\u00e1n.", + "invalid_credentials": "Neplatn\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje." + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mailov\u00e1 adresa" + }, + "title": "Vypl\u0148te p\u0159ihla\u0161ovac\u00ed \u00fadaje Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/cs.json b/homeassistant/components/alarm_control_panel/.translations/cs.json new file mode 100644 index 00000000000000..247a4e96da4707 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/cs.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Aktivovat {entity_name} v re\u017eimu mimo domov", + "arm_home": "Aktivovat {entity_name} v re\u017eimu doma", + "arm_night": "Aktivovat {entity_name} v re\u017eimu noc", + "disarm": "Deaktivovat {entity_name}", + "trigger": "Spustit {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/cs.json b/homeassistant/components/axis/.translations/cs.json new file mode 100644 index 00000000000000..258f301e432902 --- /dev/null +++ b/homeassistant/components/axis/.translations/cs.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "Za\u0159\u00edzen\u00ed Axis: {name} ({host})" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/cs.json b/homeassistant/components/binary_sensor/.translations/cs.json new file mode 100644 index 00000000000000..cb941e67883ff4 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/cs.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "moist": "{entity_name} se navlh\u010dil", + "not_opened": "{entity_name} uzav\u0159eno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/cs.json b/homeassistant/components/cert_expiry/.translations/cs.json new file mode 100644 index 00000000000000..58a5a281ea2757 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/cs.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "certificate_error": "Certifik\u00e1t nelze ov\u011b\u0159it", + "wrong_host": "Certifik\u00e1t neodpov\u00edd\u00e1 n\u00e1zvu hostitele" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/pl.json b/homeassistant/components/cert_expiry/.translations/pl.json index e594ed66a3fadf..671cbfcd1ffd5c 100644 --- a/homeassistant/components/cert_expiry/.translations/pl.json +++ b/homeassistant/components/cert_expiry/.translations/pl.json @@ -6,7 +6,7 @@ "error": { "certificate_error": "Nie mo\u017cna zweryfikowa\u0107 certyfikatu", "certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu", - "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z tym hostem", + "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z hostem.", "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107", "wrong_host": "Certyfikat nie pasuje do nazwy hosta" diff --git a/homeassistant/components/coolmaster/.translations/cs.json b/homeassistant/components/coolmaster/.translations/cs.json new file mode 100644 index 00000000000000..f1e18f8fcb4bbe --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/cs.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Nepoda\u0159ilo se p\u0159ipojit k instanci CoolMasterNet. Zkontrolujte pros\u00edm sv\u00e9ho hostitele.", + "no_units": "V hostiteli CoolMasterNet nelze naj\u00edt \u017e\u00e1dn\u00e9 jednotky HVAC." + }, + "step": { + "user": { + "data": { + "cool": "Podpora re\u017eimu chlazen\u00ed", + "dry": "Podpora re\u017eimu vysou\u0161en\u00ed", + "fan_only": "Podpora re\u017eimu pouze ventil\u00e1tor", + "heat": "Podpora re\u017eimu topen\u00ed", + "heat_cool": "Podpora automatick\u00e9ho oh\u0159\u00edv\u00e1n\u00ed/chlazen\u00ed", + "host": "Hostitel", + "off": "Lze vypnout" + }, + "title": "Nastavte podrobnosti p\u0159ipojen\u00ed CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/pl.json b/homeassistant/components/coolmaster/.translations/pl.json index 8568eac2e556ab..118c4bc424b595 100644 --- a/homeassistant/components/coolmaster/.translations/pl.json +++ b/homeassistant/components/coolmaster/.translations/pl.json @@ -10,9 +10,14 @@ "cool": "Obs\u0142uga trybu ch\u0142odzenia", "dry": "Obs\u0142uga trybu osuszania", "fan_only": "Obs\u0142uga trybu \"tylko wentylator\"", - "heat": "Obs\u0142uga tryb grzania" - } + "heat": "Obs\u0142uga trybu grzania", + "heat_cool": "Obs\u0142uga automatycznego trybu grzanie/ch\u0142odzenie", + "host": "Host", + "off": "Mo\u017ce by\u0107 wy\u0142\u0105czone" + }, + "title": "Skonfiguruj szczeg\u00f3\u0142y po\u0142\u0105czenia CoolMasterNet." } - } + }, + "title": "CoolMasterNet" } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/cs.json b/homeassistant/components/cover/.translations/cs.json new file mode 100644 index 00000000000000..bed9bc976d39d6 --- /dev/null +++ b/homeassistant/components/cover/.translations/cs.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} je zav\u0159eno", + "is_closing": "{entity_name} se zav\u00edr\u00e1", + "is_open": "{entity_name} je otev\u0159eno", + "is_opening": "{entity_name} se otev\u00edr\u00e1", + "is_position": "pozice {entity_name} je", + "is_tilt_position": "pozice naklon\u011bn\u00ed {entity_name} je" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/no.json b/homeassistant/components/cover/.translations/no.json index af567bcfcfcc3d..901ececec1fa4f 100644 --- a/homeassistant/components/cover/.translations/no.json +++ b/homeassistant/components/cover/.translations/no.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} er stengt", "is_closing": "{entity_name} stenges", "is_open": "{entity_name} er \u00e5pen", - "is_opening": "{entity_name} \u00e5pnes" + "is_opening": "{entity_name} \u00e5pnes", + "is_position": "{entity_name}-posisjonen er", + "is_tilt_position": "{entity_name} vippeposisjon er" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/pl.json b/homeassistant/components/cover/.translations/pl.json index 4adc0c17b54288..a4e4f19712bac8 100644 --- a/homeassistant/components/cover/.translations/pl.json +++ b/homeassistant/components/cover/.translations/pl.json @@ -4,7 +4,9 @@ "is_closed": "pokrywa {entity_name} jest zamkni\u0119ta", "is_closing": "{entity_name} si\u0119 zamyka", "is_open": "pokrywa {entity_name} jest otwarta", - "is_opening": "{entity_name} si\u0119 otwiera" + "is_opening": "{entity_name} si\u0119 otwiera", + "is_position": "pozycja pokrywy {entity_name} to", + "is_tilt_position": "pochylenie pokrywy {entity_name} to" } } } \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/pl.json b/homeassistant/components/daikin/.translations/pl.json index 49c5a4976673b6..5d5448a93dbff9 100644 --- a/homeassistant/components/daikin/.translations/pl.json +++ b/homeassistant/components/daikin/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "device_fail": "Nieoczekiwany b\u0142\u0105d tworzenia urz\u0105dzenia.", - "device_timeout": "Limit czasu pod\u0142\u0105czenia do urz\u0105dzenia." + "device_timeout": "Przekroczono limit czasu \u0142\u0105czenia z urz\u0105dzeniem." }, "step": { "user": { diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json index 42b14833aa0d54..c665690796dd4e 100644 --- a/homeassistant/components/deconz/.translations/cs.json +++ b/homeassistant/components/deconz/.translations/cs.json @@ -8,6 +8,7 @@ "error": { "no_key": "Nelze z\u00edskat kl\u00ed\u010d API" }, + "flow_title": "Br\u00e1na deCONZ ZigBee ({host})", "step": { "init": { "data": { diff --git a/homeassistant/components/device_tracker/.translations/cs.json b/homeassistant/components/device_tracker/.translations/cs.json new file mode 100644 index 00000000000000..778ea0208c48af --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/cs.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} je doma", + "is_not_home": "{entity_name} nen\u00ed doma" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/no.json b/homeassistant/components/device_tracker/.translations/no.json new file mode 100644 index 00000000000000..7034378b0666d7 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/no.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} er hjemme", + "is_not_home": "{entity_name} er ikke hjemme" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/pl.json b/homeassistant/components/device_tracker/.translations/pl.json new file mode 100644 index 00000000000000..8f0f7953a2d516 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/pl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "urz\u0105dzenie {entity_name} jest w domu", + "is_not_home": "urz\u0105dzenie {entity_name} jest poza domem" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/pl.json b/homeassistant/components/dialogflow/.translations/pl.json index ee222c83b5180d..c555a3e09b369b 100644 --- a/homeassistant/components/dialogflow/.translations/pl.json +++ b/homeassistant/components/dialogflow/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Dialogflow?", + "description": "Na pewno chcesz skonfigurowa\u0107 Dialogflow?", "title": "Konfiguracja Dialogflow Webhook" } }, diff --git a/homeassistant/components/geofency/.translations/pl.json b/homeassistant/components/geofency/.translations/pl.json index b2b8b606723fd1..e72e99242c1cba 100644 --- a/homeassistant/components/geofency/.translations/pl.json +++ b/homeassistant/components/geofency/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Geofency?", + "description": "Na pewno chcesz skonfigurowa\u0107 Geofency?", "title": "Konfiguracja Geofency Webhook" } }, diff --git a/homeassistant/components/gpslogger/.translations/pl.json b/homeassistant/components/gpslogger/.translations/pl.json index 726ec2ad9b2784..434fdd2220a487 100644 --- a/homeassistant/components/gpslogger/.translations/pl.json +++ b/homeassistant/components/gpslogger/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Geofency?", + "description": "Na pewno chcesz skonfigurowa\u0107 Geofency?", "title": "Konfiguracja Geofency Webhook" } }, diff --git a/homeassistant/components/huawei_lte/.translations/cs.json b/homeassistant/components/huawei_lte/.translations/cs.json new file mode 100644 index 00000000000000..8d7ac01c55acf7 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/cs.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Toto za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "connection_failed": "P\u0159ipojen\u00ed se nezda\u0159ilo", + "incorrect_password": "Nespr\u00e1vn\u00e9 heslo", + "incorrect_username": "Nespr\u00e1vn\u00e9 u\u017eivatelsk\u00e9 jm\u00e9no", + "incorrect_username_or_password": "Nespr\u00e1vn\u00e9 u\u017eivatelsk\u00e9 jm\u00e9no \u010di heslo", + "invalid_url": "Neplatn\u00e1 adresa URL", + "login_attempts_exceeded": "Maxim\u00e1ln\u00ed pokus o p\u0159ihl\u00e1\u0161en\u00ed byl p\u0159ekro\u010den, zkuste to znovu pozd\u011bji", + "response_error": "Nezn\u00e1m\u00e1 chyba ze za\u0159\u00edzen\u00ed", + "unknown_connection_error": "Nezn\u00e1m\u00e1 chyba p\u0159i p\u0159ipojov\u00e1n\u00ed k za\u0159\u00edzen\u00ed" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "url": "URL", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "title": "Konfigurovat Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "P\u0159\u00edjemci ozn\u00e1men\u00ed SMS", + "track_new_devices": "Sledovat nov\u00e1 za\u0159\u00edzen\u00ed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/no.json b/homeassistant/components/huawei_lte/.translations/no.json new file mode 100644 index 00000000000000..d06a356e9987de --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/no.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Denne enheten er allerede konfigurert" + }, + "error": { + "connection_failed": "Tilkoblingen mislyktes", + "incorrect_password": "feil passord", + "incorrect_username": "Feil brukernavn", + "incorrect_username_or_password": "Feil brukernavn eller passord", + "invalid_url": "Ugyldig URL-adresse", + "login_attempts_exceeded": "Maksimalt antall p\u00e5loggingsfors\u00f8k er overskredet, vennligst pr\u00f8v igjen senere", + "response_error": "Ukjent feil fra enheten", + "unknown_connection_error": "Ukjent feil under tilkobling til enhet" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "url": "URL", + "username": "Brukernavn" + }, + "description": "Angi detaljer for enhetstilgang. Angivelse av brukernavn og passord er valgfritt, men gir st\u00f8tte for flere integreringsfunksjoner. P\u00e5 den annen side kan bruk av en autorisert tilkobling f\u00f8re til problemer med tilgang til enhetens webgrensesnitt utenfor Home Assistant mens integreringen er aktiv, og omvendt.", + "title": "Konfigurer Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Mottakere av SMS-varsling", + "track_new_devices": "Spor nye enheter" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json new file mode 100644 index 00000000000000..5a8c4033436558 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/pl.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "connection_failed": "Po\u0142\u0105czenie nie powiod\u0142o si\u0119", + "incorrect_password": "Nieprawid\u0142owe has\u0142o", + "incorrect_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", + "incorrect_username_or_password": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o", + "invalid_url": "Nieprawid\u0142owy URL", + "login_attempts_exceeded": "Przekroczono maksymaln\u0105 liczb\u0119 pr\u00f3b logowania. Spr\u00f3buj ponownie p\u00f3\u017aniej.", + "response_error": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w urz\u0105dzeniu.", + "unknown_connection_error": "Nieznany b\u0142\u0105d podczas \u0142\u0105czenia z urz\u0105dzeniem" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "url": "URL", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Wprowad\u017a szczeg\u00f3\u0142y dost\u0119pu do urz\u0105dzenia. Okre\u015blenie nazwy u\u017cytkownika i has\u0142a jest opcjonalne, ale umo\u017cliwia obs\u0142ug\u0119 wi\u0119kszej liczby funkcji integracji. Z drugiej strony u\u017cycie autoryzowanego po\u0142\u0105czenia mo\u017ce powodowa\u0107 problemy z dost\u0119pem do interfejsu internetowego urz\u0105dzenia z zewn\u0105trz Home Assistant'a gdy integracja jest aktywna.", + "title": "Konfiguracja Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Odbiorcy powiadomie\u0144 SMS", + "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/pl.json b/homeassistant/components/ifttt/.translations/pl.json index 270e74945a3e10..ca81a510531725 100644 --- a/homeassistant/components/ifttt/.translations/pl.json +++ b/homeassistant/components/ifttt/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 IFTTT?", + "description": "Na pewno chcesz skonfigurowa\u0107 IFTTT?", "title": "Konfiguracja apletu Webhook IFTTT" } }, diff --git a/homeassistant/components/locative/.translations/pl.json b/homeassistant/components/locative/.translations/pl.json index 917744c32fd2ba..9c22a8e3fea594 100644 --- a/homeassistant/components/locative/.translations/pl.json +++ b/homeassistant/components/locative/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy na pewno chcesz skonfigurowa\u0107 Locative Webhook?", + "description": "Na pewno chcesz skonfigurowa\u0107 Locative Webhook?", "title": "Skonfiguruj Locative Webhook" } }, diff --git a/homeassistant/components/lock/.translations/cs.json b/homeassistant/components/lock/.translations/cs.json new file mode 100644 index 00000000000000..3843248a9ee416 --- /dev/null +++ b/homeassistant/components/lock/.translations/cs.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "Zamknout {entity_name}", + "open": "Otev\u0159\u00edt {entity_name}", + "unlock": "Odemknout {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} je uzam\u010deno", + "is_unlocked": "{entity_name} je odem\u010deno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/pl.json b/homeassistant/components/logi_circle/.translations/pl.json index 5d8e6a0607df4a..2266ea841c5fb9 100644 --- a/homeassistant/components/logi_circle/.translations/pl.json +++ b/homeassistant/components/logi_circle/.translations/pl.json @@ -11,7 +11,7 @@ }, "error": { "auth_error": "Autoryzacja API nie powiod\u0142a si\u0119.", - "auth_timeout": "Up\u0142yn\u0105\u0142 limit czasu \u017c\u0105dania tokena dost\u0119pu.", + "auth_timeout": "Przekroczono limit czasu \u017c\u0105dania tokena dost\u0119pu.", "follow_link": "Post\u0119puj zgodnie z linkiem i uwierzytelnij si\u0119 przed naci\u015bni\u0119ciem przycisku Prze\u015blij." }, "step": { diff --git a/homeassistant/components/mailgun/.translations/pl.json b/homeassistant/components/mailgun/.translations/pl.json index ccdc368afffd4e..ddca4c432fa122 100644 --- a/homeassistant/components/mailgun/.translations/pl.json +++ b/homeassistant/components/mailgun/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Mailgun?", + "description": "Na pewno chcesz skonfigurowa\u0107 Mailgun?", "title": "Konfiguracja Mailgun Webhook" } }, diff --git a/homeassistant/components/media_player/.translations/cs.json b/homeassistant/components/media_player/.translations/cs.json new file mode 100644 index 00000000000000..afda756740aa12 --- /dev/null +++ b/homeassistant/components/media_player/.translations/cs.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} je ne\u010dinn\u00fd", + "is_off": "{entity_name} je vypnuto", + "is_on": "{entity_name} je zapnuto", + "is_paused": "{entity_name} je pozastaven", + "is_playing": "{entity_name} p\u0159ehr\u00e1v\u00e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/no.json b/homeassistant/components/media_player/.translations/no.json new file mode 100644 index 00000000000000..a05d907774ff90 --- /dev/null +++ b/homeassistant/components/media_player/.translations/no.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} er inaktiv", + "is_off": "{entity_name} er sl\u00e5tt av", + "is_on": "{entity_name} er sl\u00e5tt p\u00e5", + "is_paused": "{entity_name} er satt p\u00e5 pause", + "is_playing": "{entity_name} spiller" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/pl.json b/homeassistant/components/media_player/.translations/pl.json new file mode 100644 index 00000000000000..29b3beaee638c4 --- /dev/null +++ b/homeassistant/components/media_player/.translations/pl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "odtwarzacz medi\u00f3w (entity_name} jest nieaktywny", + "is_off": "odtwarzacz medi\u00f3w (entity_name} jest wy\u0142\u0105czony", + "is_on": "odtwarzacz medi\u00f3w (entity_name} jest w\u0142\u0105czony", + "is_paused": "odtwarzanie medi\u00f3w na {entity_name} jest wstrzymane", + "is_playing": "{entity_name} odtwarza media" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/cs.json b/homeassistant/components/neato/.translations/cs.json new file mode 100644 index 00000000000000..2a448a26e40ac5 --- /dev/null +++ b/homeassistant/components/neato/.translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unexpected_error": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/pl.json b/homeassistant/components/nest/.translations/pl.json index ec33346cdf8d87..482a67eb221887 100644 --- a/homeassistant/components/nest/.translations/pl.json +++ b/homeassistant/components/nest/.translations/pl.json @@ -3,13 +3,13 @@ "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Nest.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", - "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "no_flows": "Musisz skonfigurowa\u0107 Nest, zanim b\u0119dziesz m\u00f3g\u0142 wykona\u0107 uwierzytelnienie. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Wewn\u0119trzny b\u0142\u0105d sprawdzania poprawno\u015bci kodu", "invalid_code": "Nieprawid\u0142owy kod", - "timeout": "Min\u0105\u0142 limit czasu sprawdzania poprawno\u015bci kodu", + "timeout": "Przekroczono limit czasu sprawdzania poprawno\u015bci kodu.", "unknown": "Nieznany b\u0142\u0105d sprawdzania poprawno\u015bci kodu" }, "step": { diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index e4403420b11af8..180f6a2430dd96 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -4,7 +4,7 @@ "already_configured": "Bramka jest ju\u017c skonfigurowana", "id_exists": "Identyfikator bramki ju\u017c istnieje", "serial_error": "B\u0142\u0105d po\u0142\u0105czenia z urz\u0105dzeniem", - "timeout": "Up\u0142yn\u0105\u0142 limit czasu pr\u00f3by po\u0142\u0105czenia" + "timeout": "Przekroczono limit czasu pr\u00f3by po\u0142\u0105czenia." }, "step": { "init": { diff --git a/homeassistant/components/owntracks/.translations/pl.json b/homeassistant/components/owntracks/.translations/pl.json index fd6cba182374e1..91afa020fc0e26 100644 --- a/homeassistant/components/owntracks/.translations/pl.json +++ b/homeassistant/components/owntracks/.translations/pl.json @@ -8,7 +8,7 @@ }, "step": { "user": { - "description": "Czy na pewno chcesz skonfigurowa\u0107 OwnTracks?", + "description": "Na pewno chcesz skonfigurowa\u0107 OwnTracks?", "title": "Konfiguracja OwnTracks" } }, diff --git a/homeassistant/components/plaato/.translations/pl.json b/homeassistant/components/plaato/.translations/pl.json index aac48ee4774bae..c4402cb8f37f20 100644 --- a/homeassistant/components/plaato/.translations/pl.json +++ b/homeassistant/components/plaato/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy na pewno chcesz skonfigurowa\u0107 Airlock Plaato?", + "description": "Na pewno chcesz skonfigurowa\u0107 Airlock Plaato?", "title": "Konfiguracja Plaato Webhook" } }, diff --git a/homeassistant/components/plex/.translations/cs.json b/homeassistant/components/plex/.translations/cs.json new file mode 100644 index 00000000000000..e033cd5c514e4c --- /dev/null +++ b/homeassistant/components/plex/.translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "discovery_no_file": "Nebyl nalezen \u017e\u00e1dn\u00fd star\u0161\u00ed konfigura\u010dn\u00ed soubor" + }, + "step": { + "start_website_auth": { + "description": "Pokra\u010dujte v autorizaci na plex.tv.", + "title": "P\u0159ipojit server plex" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index 0b94e3eacb6af9..b4ed6134106b6d 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -6,7 +6,7 @@ "already_in_progress": "Plex jest konfigurowany", "discovery_no_file": "Nie znaleziono pliku konfiguracyjnego", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", - "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena", + "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena.", "unknown": "Nieznany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/point/.translations/pl.json b/homeassistant/components/point/.translations/pl.json index ca36001cc1ade8..40acc8b4e496d7 100644 --- a/homeassistant/components/point/.translations/pl.json +++ b/homeassistant/components/point/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko konto Point.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", - "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "external_setup": "Punkt pomy\u015blnie skonfigurowany.", "no_flows": "Musisz skonfigurowa\u0107 Point, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/point/)." }, diff --git a/homeassistant/components/ps4/.translations/pl.json b/homeassistant/components/ps4/.translations/pl.json index 9fb4c73f1d056a..0770116f1c831f 100644 --- a/homeassistant/components/ps4/.translations/pl.json +++ b/homeassistant/components/ps4/.translations/pl.json @@ -8,7 +8,7 @@ "port_997_bind_error": "Nie mo\u017cna powi\u0105za\u0107 z portem 997." }, "error": { - "credential_timeout": "Up\u0142yn\u0105\u0142 limit czasu us\u0142ugi po\u015bwiadcze\u0144. Naci\u015bnij przycisk Prze\u015blij, aby ponowi\u0107.", + "credential_timeout": "Przekroczono limit czasu us\u0142ugi po\u015bwiadcze\u0144. Naci\u015bnij przycisk Prze\u015blij, aby ponowi\u0107.", "login_failed": "Nie uda\u0142o si\u0119 sparowa\u0107 z PlayStation 4. Sprawd\u017a, czy PIN jest poprawny.", "no_ipaddress": "Wprowad\u017a adres IP PlayStation 4, kt\u00f3ry chcesz skonfigurowa\u0107.", "not_ready": "PlayStation 4 nie jest w\u0142\u0105czona lub po\u0142\u0105czona z sieci\u0105." diff --git a/homeassistant/components/sensor/.translations/cs.json b/homeassistant/components/sensor/.translations/cs.json new file mode 100644 index 00000000000000..1b2dbad1a4c89d --- /dev/null +++ b/homeassistant/components/sensor/.translations/cs.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Aktu\u00e1ln\u00ed \u00farove\u0148 nabit\u00ed baterie {entity_name}", + "is_humidity": "Aktu\u00e1ln\u00ed vlhkost {entity_name}", + "is_illuminance": "Aktu\u00e1ln\u00ed osv\u011btlen\u00ed {entity_name}", + "is_power": "Aktu\u00e1ln\u00ed v\u00fdkon {entity_name}", + "is_pressure": "Aktu\u00e1ln\u00ed tlak {entity_name}", + "is_signal_strength": "Aktu\u00e1ln\u00ed s\u00edla sign\u00e1lu {entity_name}", + "is_temperature": "Aktu\u00e1ln\u00ed teplota {entity_name}", + "is_timestamp": "Aktu\u00e1ln\u00ed \u010dasov\u00e9 raz\u00edtko {entity_name}", + "is_value": "Aktu\u00e1ln\u00ed hodnota {entity_name}" + }, + "trigger_type": { + "battery_level": "\u00farove\u0148 baterie {entity_name} se zm\u011bn\u00ed", + "humidity": "vlhkost {entity_name} se zm\u011bn\u00ed", + "illuminance": "osv\u011btlen\u00ed {entity_name} se zm\u011bn\u00ed", + "power": "el. v\u00fdkon {entity_name} se zm\u011bn\u00ed", + "pressure": "tlak {entity_name} se zm\u011bn\u00ed", + "signal_strength": "s\u00edla sign\u00e1lu {entity_name} se zm\u011bn\u00ed", + "temperature": "teplota {entity_name} se zm\u011bn\u00ed", + "timestamp": "\u010dasov\u00e9 raz\u00edtko {entity_name} se zm\u011bn\u00ed", + "value": "hodnota {entity_name} se zm\u011bn\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/cs.json b/homeassistant/components/solarlog/.translations/cs.json new file mode 100644 index 00000000000000..f2294823ebb66a --- /dev/null +++ b/homeassistant/components/solarlog/.translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit, ov\u011b\u0159te pros\u00edm adresu hostitele" + }, + "step": { + "user": { + "data": { + "host": "N\u00e1zev hostitele nebo IP adresa va\u0161eho za\u0159\u00edzen\u00ed Solar-Log", + "name": "Prefix, kter\u00fd se m\u00e1 pou\u017e\u00edt pro va\u0161e senzory Solar-Log" + }, + "title": "Definujte sv\u00e9 p\u0159ipojen\u00ed Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/cs.json b/homeassistant/components/soma/.translations/cs.json new file mode 100644 index 00000000000000..b3922b67795b4b --- /dev/null +++ b/homeassistant/components/soma/.translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + }, + "description": "Zadejte pros\u00edm nastaven\u00ed p\u0159ipojen\u00ed va\u0161eho SOMA Connect.", + "title": "SOMA Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json index 4d783f3f0a05c4..c71e160142e2d2 100644 --- a/homeassistant/components/soma/.translations/pl.json +++ b/homeassistant/components/soma/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", - "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { diff --git a/homeassistant/components/somfy/.translations/cs.json b/homeassistant/components/somfy/.translations/cs.json new file mode 100644 index 00000000000000..7ba035f562e6de --- /dev/null +++ b/homeassistant/components/somfy/.translations/cs.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/no.json b/homeassistant/components/somfy/.translations/no.json index 9d82eea3511e84..f5944cbf9c7c66 100644 --- a/homeassistant/components/somfy/.translations/no.json +++ b/homeassistant/components/somfy/.translations/no.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Vellykket autentisering med Somfy." }, + "step": { + "pick_implementation": { + "title": "Velg autentiseringsmetode" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/pl.json b/homeassistant/components/somfy/.translations/pl.json index cb19fcb793ae1b..e08e921cf1ab89 100644 --- a/homeassistant/components/somfy/.translations/pl.json +++ b/homeassistant/components/somfy/.translations/pl.json @@ -2,12 +2,17 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Somfy.", - "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "missing_configuration": "Komponent Somfy nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Somfy" }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/pl.json b/homeassistant/components/tellduslive/.translations/pl.json index 06391b24b99b56..01d3c7125c3cf8 100644 --- a/homeassistant/components/tellduslive/.translations/pl.json +++ b/homeassistant/components/tellduslive/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "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.", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/traccar/.translations/pl.json b/homeassistant/components/traccar/.translations/pl.json index 66ddbaaa3fdb21..74ff0c089d879f 100644 --- a/homeassistant/components/traccar/.translations/pl.json +++ b/homeassistant/components/traccar/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy na pewno chcesz skonfigurowa\u0107 Traccar?", + "description": "Na pewno chcesz skonfigurowa\u0107 Traccar?", "title": "Skonfiguruj Traccar" } }, diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json index 3a1798e66d995b..fc1152940310f8 100644 --- a/homeassistant/components/tradfri/.translations/pl.json +++ b/homeassistant/components/tradfri/.translations/pl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z bram\u0105.", "invalid_key": "Rejestracja si\u0119 nie powiod\u0142a z podanym kluczem. Je\u015bli tak si\u0119 stanie, spr\u00f3buj ponownie uruchomi\u0107 bramk\u0119.", - "timeout": "Min\u0105\u0142 limit czasu sprawdzania poprawno\u015bci kodu" + "timeout": "Przekroczono limit czasu sprawdzania poprawno\u015bci kodu." }, "step": { "auth": { diff --git a/homeassistant/components/transmission/.translations/cs.json b/homeassistant/components/transmission/.translations/cs.json new file mode 100644 index 00000000000000..bb96d5809e13da --- /dev/null +++ b/homeassistant/components/transmission/.translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Hostitel je ji\u017e nakonfigurov\u00e1n." + }, + "error": { + "name_exists": "Jm\u00e9no ji\u017e existuje" + } + }, + "options": { + "step": { + "init": { + "title": "Nakonfigurujte mo\u017enosti pro Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pl.json b/homeassistant/components/transmission/.translations/pl.json index 9b45e3107672af..a85a3f9b006cf4 100644 --- a/homeassistant/components/transmission/.translations/pl.json +++ b/homeassistant/components/transmission/.translations/pl.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Host jest ju\u017c skonfigurowany.", "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem", + "name_exists": "Nazwa ju\u017c istnieje", "wrong_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" }, - "description": "Konfiguracja opcji dla Transmission" + "description": "Konfiguracja opcji dla Transmission", + "title": "Konfiguracja opcji dla Transmission" } } } diff --git a/homeassistant/components/twilio/.translations/pl.json b/homeassistant/components/twilio/.translations/pl.json index 2b963ff1be506e..c61d22db8804a6 100644 --- a/homeassistant/components/twilio/.translations/pl.json +++ b/homeassistant/components/twilio/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Twilio?", + "description": "Na pewno chcesz skonfigurowa\u0107 Twilio?", "title": "Konfiguracja Twilio Webhook" } }, diff --git a/homeassistant/components/unifi/.translations/cs.json b/homeassistant/components/unifi/.translations/cs.json index 3ea631ec86ccdf..32711da56f7149 100644 --- a/homeassistant/components/unifi/.translations/cs.json +++ b/homeassistant/components/unifi/.translations/cs.json @@ -22,5 +22,14 @@ } }, "title": "UniFi \u0159adi\u010d" + }, + "options": { + "step": { + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Vytvo\u0159it senzory vyu\u017eit\u00ed \u0161\u00ed\u0159ky p\u00e1sma pro s\u00ed\u0165ov\u00e9 klienty" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/cs.json b/homeassistant/components/withings/.translations/cs.json new file mode 100644 index 00000000000000..a8aea1fa08f656 --- /dev/null +++ b/homeassistant/components/withings/.translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Kter\u00fd profil jste vybrali na webu Withings? Je d\u016fle\u017eit\u00e9, aby se profily shodovaly, jinak budou data nespr\u00e1vn\u011b ozna\u010dena.", + "title": "U\u017eivatelsk\u00fd profil." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/no.json b/homeassistant/components/withings/.translations/no.json index d32c9640fd7636..bdde342e7bc1d5 100644 --- a/homeassistant/components/withings/.translations/no.json +++ b/homeassistant/components/withings/.translations/no.json @@ -7,6 +7,13 @@ "default": "Vellykket autentisering for Withings og den valgte profilen." }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Hvilken profil valgte du p\u00e5 Withings nettsted? Det er viktig at profilene samsvarer, ellers blir data feilmerket.", + "title": "Brukerprofil." + }, "user": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json index 3c345a1a788bd6..4f1ee47ab0e449 100644 --- a/homeassistant/components/withings/.translations/pl.json +++ b/homeassistant/components/withings/.translations/pl.json @@ -7,6 +7,13 @@ "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu" }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Kt\u00f3ry profil wybra\u0142e\u015b na stronie Withings? Wa\u017cne jest, aby profile si\u0119 zgadza\u0142y, w przeciwnym razie dane zostan\u0105 b\u0142\u0119dnie oznaczone.", + "title": "Profil u\u017cytkownika" + }, "user": { "data": { "profile": "Profil" From a0f764cf6da52bfce9cf80b5f693fb06df62ebab Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 29 Oct 2019 01:44:26 +0100 Subject: [PATCH 1311/3953] Remove blocking I/O from the event loop (#28305) --- homeassistant/components/xiaomi_miio/fan.py | 2 +- homeassistant/components/xiaomi_miio/light.py | 2 +- homeassistant/components/xiaomi_miio/remote.py | 2 +- homeassistant/components/xiaomi_miio/sensor.py | 2 +- homeassistant/components/xiaomi_miio/switch.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index acac60e108a59b..e50c0102925e80 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -445,7 +445,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if model is None: try: miio_device = Device(host, token) - device_info = miio_device.info() + device_info = await hass.async_add_executor_job(miio_device.info) model = device_info.model unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 3d23f1dfc98c48..cb16fa094ceeae 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -134,7 +134,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if model is None: try: miio_device = Device(host, token) - device_info = miio_device.info() + device_info = await hass.async_add_executor_job(miio_device.info) model = device_info.model unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 311a356870c828..9898f0f88b4eae 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -88,7 +88,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Check that we can communicate with device. try: - device_info = device.info() + device_info = await hass.async_add_executor_job(device.info) model = device_info.model unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 0ebffb06fcd87e..253f2c967dfead 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -50,7 +50,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: air_quality_monitor = AirQualityMonitor(host, token) - device_info = air_quality_monitor.info() + device_info = await hass.async_add_executor_job(air_quality_monitor.info) model = device_info.model unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 7fa1638253cd24..4b9246a54701f1 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -120,7 +120,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if model is None: try: miio_device = Device(host, token) - device_info = miio_device.info() + device_info = await hass.async_add_executor_job(miio_device.info) model = device_info.model unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( From 5b96704c4aec1d90d879a6fc0d75faaa469f061f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 29 Oct 2019 01:45:22 +0100 Subject: [PATCH 1312/3953] Use dict[key] for required config keys (#28304) * Use dict[key] for required config keys * Change CONF_NAME too because it has a default --- homeassistant/components/xiaomi_miio/device_tracker.py | 4 ++-- homeassistant/components/xiaomi_miio/fan.py | 6 +++--- homeassistant/components/xiaomi_miio/light.py | 6 +++--- homeassistant/components/xiaomi_miio/remote.py | 4 ++-- homeassistant/components/xiaomi_miio/sensor.py | 6 +++--- homeassistant/components/xiaomi_miio/switch.py | 6 +++--- homeassistant/components/xiaomi_miio/vacuum.py | 6 +++--- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index 6c2c391034e71c..e2611b52f121cd 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -26,8 +26,8 @@ def get_scanner(hass, config): from miio import WifiRepeater, DeviceException scanner = None - host = config[DOMAIN].get(CONF_HOST) - token = config[DOMAIN].get(CONF_TOKEN) + host = config[DOMAIN][CONF_HOST] + token = config[DOMAIN][CONF_TOKEN] _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index e50c0102925e80..e6c356b7338f54 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -434,9 +434,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] model = config.get(CONF_MODEL) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index cb16fa094ceeae..aa5a0ed42b96de 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -121,9 +121,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] model = config.get(CONF_MODEL) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 9898f0f88b4eae..075dd15e887ae2 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -75,8 +75,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the Xiaomi IR Remote (Chuangmi IR) platform.""" from miio import ChuangmiIr, DeviceException - host = config.get(CONF_HOST) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] # Create handler _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 253f2c967dfead..c19e314acddd8a 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -42,9 +42,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 4b9246a54701f1..97e8ef27c3f228 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -107,9 +107,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] model = config.get(CONF_MODEL) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 61ddef927393f3..aa08693db63456 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -182,9 +182,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] # Create handler _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) From 04ab20846a7a430d6ce38b5fa8c1eeb80addc474 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Oct 2019 07:32:34 +0100 Subject: [PATCH 1313/3953] Bump black to 19.10b0 (#28310) --- .pre-commit-config.yaml | 2 +- homeassistant/auth/mfa_modules/totp.py | 6 ++++- .../components/androidtv/media_player.py | 10 +++++--- homeassistant/components/envirophat/sensor.py | 25 ++++++++++++------- .../components/hangouts/hangouts_bot.py | 7 +++--- .../components/here_travel_time/sensor.py | 2 +- homeassistant/loader.py | 4 +-- homeassistant/util/color.py | 2 +- homeassistant/util/location.py | 2 +- homeassistant/util/yaml/loader.py | 2 +- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ecobee/test_config_flow.py | 4 +-- tests/components/withings/test_common.py | 4 +-- tests/components/zwave/test_cover.py | 14 +++++------ tests/components/zwave/test_init.py | 4 +-- 16 files changed, 54 insertions(+), 38 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 268cff9ea78b85..e2b99393639b8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 19.3b0 + rev: 19.10b0 hooks: - id: black args: diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 9829044a53ece5..9b0f3910e9291f 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -215,7 +215,11 @@ async def async_step_init( else: hass = self._auth_module.hass - self._ota_secret, self._url, self._image = await hass.async_add_executor_job( + ( + self._ota_secret, + self._url, + self._image, + ) = await hass.async_add_executor_job( _generate_secret_and_qr_code, # type: ignore str(self._user.name), ) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 62ae93f96e4b84..60f423c3dee14e 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -455,9 +455,13 @@ def update(self): return # Get the updated state and attributes. - state, self._current_app, self._device, self._is_volume_muted, self._volume_level = ( - self.aftv.update() - ) + ( + state, + self._current_app, + self._device, + self._is_volume_muted, + self._volume_level, + ) = self.aftv.update() self._state = ANDROIDTV_STATES.get(state) if self._state is None: diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 459d21ab698af0..2aaeefa48cf785 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -172,14 +172,18 @@ def update(self): self.envirophat.leds.off() # accelerometer readings in G - self.accelerometer_x, self.accelerometer_y, self.accelerometer_z = ( - self.envirophat.motion.accelerometer() - ) + ( + self.accelerometer_x, + self.accelerometer_y, + self.accelerometer_z, + ) = self.envirophat.motion.accelerometer() # raw magnetometer reading - self.magnetometer_x, self.magnetometer_y, self.magnetometer_z = ( - self.envirophat.motion.magnetometer() - ) + ( + self.magnetometer_x, + self.magnetometer_y, + self.magnetometer_z, + ) = self.envirophat.motion.magnetometer() # temperature resolution of BMP280 sensor: 0.01°C self.temperature = round(self.envirophat.weather.temperature(), 2) @@ -189,6 +193,9 @@ def update(self): self.pressure = round(self.envirophat.weather.pressure() / 100.0, 3) # Voltage sensor, reading between 0-3.3V - self.voltage_0, self.voltage_1, self.voltage_2, self.voltage_3 = ( - self.envirophat.analog.read_all() - ) + ( + self.voltage_0, + self.voltage_1, + self.voltage_2, + self.voltage_3, + ) = self.envirophat.analog.read_all() diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 9fc3e2fa58e8fb..9eee2ca2be3eff 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -308,9 +308,10 @@ async def _async_send_message(self, message, targets, data): async def _async_list_conversations(self): import hangups - self._user_list, self._conversation_list = await hangups.build_user_conversation_list( - self._client - ) + ( + self._user_list, + self._conversation_list, + ) = await hangups.build_user_conversation_list(self._client) conversations = {} for i, conv in enumerate(self._conversation_list.get_all()): users_in_conversation = [] diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index b752b82d08750b..afd12bb01c5531 100755 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -237,7 +237,7 @@ def name(self) -> str: @property def device_state_attributes( - self + self, ) -> Optional[Dict[str, Union[None, float, str, bool]]]: """Return the state attributes.""" if self._here_data.base_time is None: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 272271eefb31af..e32303ecfabb3c 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -64,7 +64,7 @@ def manifest_from_legacy_module(domain: str, module: ModuleType) -> Dict: async def _async_get_custom_components( - hass: "HomeAssistant" + hass: "HomeAssistant", ) -> Dict[str, "Integration"]: """Return list of custom integrations.""" try: @@ -102,7 +102,7 @@ def get_sub_directories(paths: List) -> List: async def async_get_custom_components( - hass: "HomeAssistant" + hass: "HomeAssistant", ) -> Dict[str, "Integration"]: """Return cached list of custom integrations.""" reg_or_evt = hass.data.get(DATA_CUSTOM_COMPONENTS) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 640e5c5540a4d6..837a0e77cd7316 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -441,7 +441,7 @@ def color_temperature_to_hs(color_temperature_kelvin: float) -> Tuple[float, flo def color_temperature_to_rgb( - color_temperature_kelvin: float + color_temperature_kelvin: float, ) -> Tuple[float, float, float]: """ Return an RGB color from a color temperature in Kelvin. diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 7c61a8ab1e9197..b572b3025a0c55 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -46,7 +46,7 @@ async def async_detect_location_info( - session: aiohttp.ClientSession + session: aiohttp.ClientSession, ) -> Optional[LocationInfo]: """Detect location information.""" data = await _get_ipapi(session) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 9822b7c63c2124..bbbe22c2ba8be2 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -225,7 +225,7 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order def _construct_seq(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: """Add line number and file name to Load YAML sequence.""" - obj, = loader.construct_yaml_seq(node) + (obj,) = loader.construct_yaml_seq(node) return _add_reference(obj, loader, node) diff --git a/requirements_test.txt b/requirements_test.txt index 5240946b004750..ad46cfa0741b78 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,7 +4,7 @@ # When updating this file, update .pre-commit-config.yaml too asynctest==0.13.0 -black==19.3b0 +black==19.10b0 codecov==2.0.15 flake8-docstrings==1.5.0 flake8==3.7.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7a13a44288aa87..a4aaeb373ccc89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -5,7 +5,7 @@ # When updating this file, update .pre-commit-config.yaml too asynctest==0.13.0 -black==19.3b0 +black==19.10b0 codecov==2.0.15 flake8-docstrings==1.5.0 flake8==3.7.8 diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 4008e6a17b18b4..64f0e3df0e7850 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -131,7 +131,7 @@ async def test_import_flow_triggered_but_no_ecobee_conf(hass): async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_tokens( - hass + hass, ): """Test expected result if import flow triggers and ecobee.conf exists with valid tokens.""" flow = config_flow.EcobeeFlowHandler() @@ -181,7 +181,7 @@ async def test_import_flow_triggered_with_ecobee_conf_and_invalid_data(hass): async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_stale_tokens( - hass + hass, ): """Test expected result if import flow triggers and ecobee.conf exists with stale tokens.""" flow = config_flow.EcobeeFlowHandler() diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index e513ebb1d2e35e..f6075c0f734cb4 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -75,7 +75,7 @@ async def test_data_manager_call(data_manager: WithingsDataManager) -> None: async def test_data_manager_call_throttle_enabled( - data_manager: WithingsDataManager + data_manager: WithingsDataManager, ) -> None: """Test method.""" hello_func = MagicMock(return_value="HELLO2") @@ -90,7 +90,7 @@ async def test_data_manager_call_throttle_enabled( async def test_data_manager_call_throttle_disabled( - data_manager: WithingsDataManager + data_manager: WithingsDataManager, ) -> None: """Test method.""" hello_func = MagicMock(return_value="HELLO2") diff --git a/tests/components/zwave/test_cover.py b/tests/components/zwave/test_cover.py index a8580604e469e6..848decc2fb533e 100644 --- a/tests/components/zwave/test_cover.py +++ b/tests/components/zwave/test_cover.py @@ -130,17 +130,17 @@ def test_roller_commands(hass, mock_openzwave): device.open_cover() assert mock_network.manager.pressButton.called - value_id, = mock_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == open_value.value_id device.close_cover() assert mock_network.manager.pressButton.called - value_id, = mock_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == close_value.value_id device.stop_cover() assert mock_network.manager.releaseButton.called - value_id, = mock_network.manager.releaseButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.releaseButton.mock_calls.pop(0)[1] assert value_id == open_value.value_id @@ -168,7 +168,7 @@ def test_roller_invert_percent(hass, mock_openzwave): device.open_cover() assert mock_network.manager.pressButton.called - value_id, = mock_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == open_value.value_id @@ -193,17 +193,17 @@ def test_roller_reverse_open_close(hass, mock_openzwave): device.open_cover() assert mock_network.manager.pressButton.called - value_id, = mock_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == close_value.value_id device.close_cover() assert mock_network.manager.pressButton.called - value_id, = mock_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == open_value.value_id device.stop_cover() assert mock_network.manager.releaseButton.called - value_id, = mock_network.manager.releaseButton.mock_calls.pop(0)[1] + (value_id,) = mock_network.manager.releaseButton.mock_calls.pop(0)[1] assert value_id == close_value.value_id diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index f32c97506f8e07..1de69249bfeaad 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -1610,10 +1610,10 @@ def test_reset_node_meters(self): self.hass.block_till_done() assert self.zwave_network.manager.pressButton.called - value_id, = self.zwave_network.manager.pressButton.mock_calls.pop(0)[1] + (value_id,) = self.zwave_network.manager.pressButton.mock_calls.pop(0)[1] assert value_id == reset_value.value_id assert self.zwave_network.manager.releaseButton.called - value_id, = self.zwave_network.manager.releaseButton.mock_calls.pop(0)[1] + (value_id,) = self.zwave_network.manager.releaseButton.mock_calls.pop(0)[1] assert value_id == reset_value.value_id def test_add_association(self): From a4ec4d5a182f74ab741872c6979ad8e101d46fda Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Oct 2019 07:32:57 +0100 Subject: [PATCH 1314/3953] Add source constants for all config entry discovery sources (#28311) --- homeassistant/config_entries.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index ae7c534adf805b..ae3aeebb1ee85e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -20,9 +20,11 @@ _LOGGER = logging.getLogger(__name__) _UNDEF: dict = {} -SOURCE_USER = "user" SOURCE_DISCOVERY = "discovery" SOURCE_IMPORT = "import" +SOURCE_SSDP = "ssdp" +SOURCE_USER = "user" +SOURCE_ZEROCONF = "zeroconf" HANDLERS = Registry() @@ -50,7 +52,7 @@ UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_FAILED_UNLOAD) DISCOVERY_NOTIFICATION_ID = "config_entry_discovery" -DISCOVERY_SOURCES = ("ssdp", "zeroconf", SOURCE_DISCOVERY, SOURCE_IMPORT) +DISCOVERY_SOURCES = (SOURCE_SSDP, SOURCE_ZEROCONF, SOURCE_DISCOVERY, SOURCE_IMPORT) EVENT_FLOW_DISCOVERED = "config_entry_discovered" From 79ac77a93d37bcfae4161c43e6666f6fce68936a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Oct 2019 23:47:31 -0700 Subject: [PATCH 1315/3953] Almond integration (#28282) * Initial Almond integration * Hassfest * Update library * Address comments * Fix inheritance issue py36 * Remove no longer needed check * Fix time --- CODEOWNERS | 1 + homeassistant/auth/__init__.py | 8 +- .../components/almond/.translations/en.json | 8 + homeassistant/components/almond/__init__.py | 230 ++++++++++++++++++ .../components/almond/config_flow.py | 125 ++++++++++ homeassistant/components/almond/const.py | 4 + homeassistant/components/almond/manifest.json | 9 + homeassistant/components/almond/strings.json | 9 + homeassistant/generated/config_flows.py | 1 + .../helpers/config_entry_oauth2_flow.py | 13 +- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/almond/__init__.py | 1 + tests/components/almond/test_config_flow.py | 138 +++++++++++ 14 files changed, 544 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/almond/.translations/en.json create mode 100644 homeassistant/components/almond/__init__.py create mode 100644 homeassistant/components/almond/config_flow.py create mode 100644 homeassistant/components/almond/const.py create mode 100644 homeassistant/components/almond/manifest.json create mode 100644 homeassistant/components/almond/strings.json create mode 100644 tests/components/almond/__init__.py create mode 100644 tests/components/almond/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index dac590399350e4..aed575b5271088 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -19,6 +19,7 @@ homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya homeassistant/components/alarm_control_panel/* @colinodell homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy +homeassistant/components/almond/* @gcampax @balloob homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 921bec71e784c4..3f7dd570400adf 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -261,7 +261,7 @@ async def async_enable_user_mfa( """Enable a multi-factor auth module for user.""" if user.system_generated: raise ValueError( - "System generated users cannot enable " "multi-factor auth module." + "System generated users cannot enable multi-factor auth module." ) module = self.get_auth_mfa_module(mfa_module_id) @@ -276,7 +276,7 @@ async def async_disable_user_mfa( """Disable a multi-factor auth module for user.""" if user.system_generated: raise ValueError( - "System generated users cannot disable " "multi-factor auth module." + "System generated users cannot disable multi-factor auth module." ) module = self.get_auth_mfa_module(mfa_module_id) @@ -320,7 +320,7 @@ async def async_create_refresh_token( if user.system_generated != (token_type == models.TOKEN_TYPE_SYSTEM): raise ValueError( - "System generated users can only have system type " "refresh tokens" + "System generated users can only have system type refresh tokens" ) if token_type == models.TOKEN_TYPE_NORMAL and client_id is None: @@ -330,7 +330,7 @@ async def async_create_refresh_token( token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN and client_name is None ): - raise ValueError("Client_name is required for long-lived access " "token") + raise ValueError("Client_name is required for long-lived access token") if token_type == models.TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN: for token in user.refresh_tokens.values(): diff --git a/homeassistant/components/almond/.translations/en.json b/homeassistant/components/almond/.translations/en.json new file mode 100644 index 00000000000000..cc48b1c28ebf3d --- /dev/null +++ b/homeassistant/components/almond/.translations/en.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Almond account." + }, + "title": "Almond" + } +} diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py new file mode 100644 index 00000000000000..ebdddecdec3d62 --- /dev/null +++ b/homeassistant/components/almond/__init__.py @@ -0,0 +1,230 @@ +"""Support for Almond.""" +import asyncio +from datetime import timedelta +import logging +import time + +import async_timeout +from aiohttp import ClientSession, ClientError +from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI +import voluptuous as vol + +from homeassistant.const import CONF_TYPE, CONF_HOST +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.helpers import ( + config_validation as cv, + config_entry_oauth2_flow, + intent, + aiohttp_client, + storage, +) +from homeassistant import config_entries +from homeassistant.components import conversation + +from . import config_flow +from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 + +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" + +STORAGE_VERSION = 1 +STORAGE_KEY = DOMAIN + +DEFAULT_OAUTH2_HOST = "https://almond.stanford.edu" +DEFAULT_LOCAL_HOST = "http://localhost:3000" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Any( + vol.Schema( + { + vol.Required(CONF_TYPE): TYPE_OAUTH2, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_OAUTH2_HOST): cv.url, + } + ), + vol.Schema( + {vol.Required(CONF_TYPE): TYPE_LOCAL, vol.Required(CONF_HOST): cv.url} + ), + ) + }, + extra=vol.ALLOW_EXTRA, +) +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the Almond component.""" + hass.data[DOMAIN] = {} + + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + host = conf[CONF_HOST] + + if conf[CONF_TYPE] == TYPE_OAUTH2: + config_flow.AlmondFlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + conf[CONF_CLIENT_ID], + conf[CONF_CLIENT_SECRET], + f"{host}/me/api/oauth2/authorize", + f"{host}/me/api/oauth2/token", + ), + ) + return True + + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"type": TYPE_LOCAL, "host": conf[CONF_HOST]}, + ) + ) + return True + + +async def async_setup_entry(hass, entry): + """Set up Almond config entry.""" + websession = aiohttp_client.async_get_clientsession(hass) + if entry.data["type"] == TYPE_LOCAL: + auth = AlmondLocalAuth(entry.data["host"], websession) + + else: + # OAuth2 + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + oauth_session = config_entry_oauth2_flow.OAuth2Session( + hass, entry, implementation + ) + auth = AlmondOAuth(entry.data["host"], websession, oauth_session) + + api = WebAlmondAPI(auth) + agent = AlmondAgent(api) + + # Hass.io does its own configuration of Almond. + if entry.data.get("is_hassio"): + conversation.async_set_agent(hass, agent) + return True + + # Configure Almond to connect to Home Assistant + store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + data = await store.async_load() + + if data is None: + data = {} + + user = None + if "almond_user" in data: + user = await hass.auth.async_get_user(data["almond_user"]) + + if user is None: + user = await hass.auth.async_create_system_user("Almond", [GROUP_ID_ADMIN]) + data["almond_user"] = user.id + await store.async_save(data) + + refresh_token = await hass.auth.async_create_refresh_token( + user, + # Almond will be fine as long as we restart once every 5 years + access_token_expiration=timedelta(days=365 * 5), + ) + + # Create long lived access token + access_token = hass.auth.async_create_access_token(refresh_token) + + # Store token in Almond + try: + with async_timeout.timeout(10): + await api.async_create_device( + { + "kind": "io.home-assistant", + "hassUrl": hass.config.api.base_url, + "accessToken": access_token, + "refreshToken": "", + # 5 years from now in ms. + "accessTokenExpires": (time.time() + 60 * 60 * 24 * 365 * 5) * 1000, + } + ) + except (asyncio.TimeoutError, ClientError) as err: + if isinstance(err, asyncio.TimeoutError): + msg = "Request timeout" + else: + msg = err + _LOGGER.warning("Unable to configure Almond: %s", msg) + await hass.auth.async_remove_refresh_token(refresh_token) + raise ConfigEntryNotReady + + # Clear all other refresh tokens + for token in list(user.refresh_tokens.values()): + if token.id != refresh_token.id: + await hass.auth.async_remove_refresh_token(token) + + conversation.async_set_agent(hass, agent) + return True + + +async def async_unload_entry(hass, entry): + """Unload Almond.""" + conversation.async_set_agent(hass, None) + return True + + +class AlmondOAuth(AbstractAlmondWebAuth): + """Almond Authentication using OAuth2.""" + + def __init__( + self, + host: str, + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ): + """Initialize Almond auth.""" + super().__init__(host, websession) + self._oauth_session = oauth_session + + async def async_get_access_token(self): + """Return a valid access token.""" + if not self._oauth_session.is_valid: + await self._oauth_session.async_ensure_token_valid() + + return self._oauth_session.token + + +class AlmondAgent(conversation.AbstractConversationAgent): + """Almond conversation agent.""" + + def __init__(self, api: WebAlmondAPI): + """Initialize the agent.""" + self.api = api + + async def async_process(self, text: str) -> intent.IntentResponse: + """Process a sentence.""" + response = await self.api.async_converse_text(text) + + buffer = "" + for message in response["messages"]: + if message["type"] == "text": + buffer += "\n" + message["text"] + elif message["type"] == "picture": + buffer += "\n Picture: " + message["url"] + elif message["type"] == "rdl": + buffer += ( + "\n Link: " + + message["rdl"]["displayTitle"] + + " " + + message["rdl"]["webCallback"] + ) + elif message["type"] == "choice": + buffer += "\n Choice: " + message["title"] + + intent_result = intent.IntentResponse() + intent_result.async_set_speech(buffer.strip()) + return intent_result diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py new file mode 100644 index 00000000000000..d79bf6bd6059ae --- /dev/null +++ b/homeassistant/components/almond/config_flow.py @@ -0,0 +1,125 @@ +"""Config flow to connect with Home Assistant.""" +import asyncio +import logging + +import async_timeout +from aiohttp import ClientError +from yarl import URL +import voluptuous as vol +from pyalmond import AlmondLocalAuth, WebAlmondAPI + +from homeassistant import data_entry_flow, config_entries, core +from homeassistant.helpers import config_entry_oauth2_flow, aiohttp_client + +from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 + + +async def async_verify_local_connection(hass: core.HomeAssistant, host: str): + """Verify that a local connection works.""" + websession = aiohttp_client.async_get_clientsession(hass) + api = WebAlmondAPI(AlmondLocalAuth(host, websession)) + + try: + with async_timeout.timeout(10): + await api.async_list_apps() + + return True + except (asyncio.TimeoutError, ClientError): + return False + + +@config_entries.HANDLERS.register(DOMAIN) +class AlmondFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Implementation of the Almond OAuth2 config flow.""" + + DOMAIN = DOMAIN + + host = None + hassio_discovery = None + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": "profile user-read user-read-results user-exec-command"} + + async def async_step_user(self, user_input=None): + """Handle a flow start.""" + # Only allow 1 instance. + if self._async_current_entries(): + return self.async_abort(reason="already_setup") + + return await super().async_step_user(user_input) + + async def async_step_auth(self, user_input=None): + """Handle authorize step.""" + result = await super().async_step_auth(user_input) + + if result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP: + self.host = str(URL(result["url"]).with_path("me")) + + return result + + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an entry for the flow. + + Ok to override if you want to fetch extra info or even add another step. + """ + # pylint: disable=invalid-name + self.CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + data["type"] = TYPE_OAUTH2 + data["host"] = self.host + return self.async_create_entry(title=self.flow_impl.name, data=data) + + async def async_step_import(self, user_input: dict = None) -> dict: + """Import data.""" + # Only allow 1 instance. + if self._async_current_entries(): + return self.async_abort(reason="already_setup") + + if not await async_verify_local_connection(self.hass, user_input["host"]): + self.logger.warning( + "Aborting import of Almond because we're unable to connect" + ) + return self.async_abort(reason="cannot_connect") + + # pylint: disable=invalid-name + self.CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + return self.async_create_entry( + title="Configuration.yaml", + data={"type": TYPE_LOCAL, "host": user_input["host"]}, + ) + + async def async_step_hassio(self, user_input=None): + """Receive a Hass.io discovery.""" + if self._async_current_entries(): + return self.async_abort(reason="already_setup") + + self.hassio_discovery = user_input + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm(self, user_input=None): + """Confirm a Hass.io discovery.""" + data = self.hassio_discovery + + if user_input is not None: + return self.async_create_entry( + title=data["addon"], + data={ + "is_hassio": True, + "type": TYPE_LOCAL, + "host": f"http://{data['host']}:{data['port']}", + }, + ) + + return self.async_show_form( + step_id="hassio_confirm", + description_placeholders={"addon": data["addon"]}, + data_schema=vol.Schema({}), + ) diff --git a/homeassistant/components/almond/const.py b/homeassistant/components/almond/const.py new file mode 100644 index 00000000000000..34dca28e9571ec --- /dev/null +++ b/homeassistant/components/almond/const.py @@ -0,0 +1,4 @@ +"""Constants for the Almond integration.""" +DOMAIN = "almond" +TYPE_OAUTH2 = "oauth2" +TYPE_LOCAL = "local" diff --git a/homeassistant/components/almond/manifest.json b/homeassistant/components/almond/manifest.json new file mode 100644 index 00000000000000..44404b504f6a0f --- /dev/null +++ b/homeassistant/components/almond/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "almond", + "name": "Almond", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/almond", + "dependencies": ["http", "conversation"], + "codeowners": ["@gcampax", "@balloob"], + "requirements": ["pyalmond==0.0.2"] +} diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json new file mode 100644 index 00000000000000..9bc4b0e1b93b45 --- /dev/null +++ b/homeassistant/components/almond/strings.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure one Almond account.", + "cannot_connect": "Unable to connect to the Almond server." + }, + "title": "Almond" + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b694af1fb71bea..22d36fc46c61c4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -9,6 +9,7 @@ "abode", "adguard", "airly", + "almond", "ambiclimate", "ambient_station", "axis", diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 87832f60739791..dc3d3c91f27a76 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -387,17 +387,20 @@ def __init__( @property def token(self) -> dict: - """Return the current token.""" + """Return the token.""" return cast(dict, self.config_entry.data["token"]) + @property + def valid_token(self) -> bool: + """Return if token is still valid.""" + return cast(float, self.token["expires_at"]) > time.time() + async def async_ensure_token_valid(self) -> None: """Ensure that the current token is valid.""" - token = self.token - - if token["expires_at"] > time.time(): + if self.valid_token: return - new_token = await self.implementation.async_refresh_token(token) + new_token = await self.implementation.async_refresh_token(self.token) self.hass.config_entries.async_update_entry( self.config_entry, data={**self.config_entry.data, "token": new_token} diff --git a/requirements_all.txt b/requirements_all.txt index a1db7bbc1f7957..54831270ff652c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1089,6 +1089,9 @@ pyairvisual==3.0.1 # homeassistant.components.alarmdotcom pyalarmdotcom==0.3.2 +# homeassistant.components.almond +pyalmond==0.0.2 + # homeassistant.components.arlo pyarlo==0.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4aaeb373ccc89..280146ec45d882 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -390,6 +390,9 @@ pyRFXtrx==0.23 # homeassistant.components.nextbus py_nextbusnext==0.1.4 +# homeassistant.components.almond +pyalmond==0.0.2 + # homeassistant.components.arlo pyarlo==0.2.3 diff --git a/tests/components/almond/__init__.py b/tests/components/almond/__init__.py new file mode 100644 index 00000000000000..717271c3a6a157 --- /dev/null +++ b/tests/components/almond/__init__.py @@ -0,0 +1 @@ +"""Tests for the Almond integration.""" diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py new file mode 100644 index 00000000000000..afbe25dff5f1c5 --- /dev/null +++ b/tests/components/almond/test_config_flow.py @@ -0,0 +1,138 @@ +"""Test the Almond config flow.""" +import asyncio + +from unittest.mock import patch + + +from homeassistant import config_entries, setup, data_entry_flow +from homeassistant.components.almond.const import DOMAIN +from homeassistant.components.almond import config_flow +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry, mock_coro + +CLIENT_ID_VALUE = "1234" +CLIENT_SECRET_VALUE = "5678" + + +async def test_import(hass): + """Test that we can import a config entry.""" + with patch("pyalmond.WebAlmondAPI.async_list_apps", side_effect=mock_coro): + assert await setup.async_setup_component( + hass, + "almond", + {"almond": {"type": "local", "host": "http://localhost:3000"}}, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data["type"] == "local" + assert entry.data["host"] == "http://localhost:3000" + + +async def test_import_cannot_connect(hass): + """Test that we won't import a config entry if we cannot connect.""" + with patch( + "pyalmond.WebAlmondAPI.async_list_apps", side_effect=asyncio.TimeoutError + ): + assert await setup.async_setup_component( + hass, + "almond", + {"almond": {"type": "local", "host": "http://localhost:3000"}}, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + + +async def test_hassio(hass): + """Test that Hass.io can discover this integration.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "hassio"}, + data={"addon": "Almond add-on", "host": "almond-addon", "port": "1234"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "hassio_confirm" + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data["type"] == "local" + assert entry.data["host"] == "http://almond-addon:1234" + + +async def test_abort_if_existing_entry(hass): + """Check flow abort when an entry already exist.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + flow = config_flow.AlmondFlowHandler() + flow.hass = hass + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + result = await flow.async_step_import() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + result = await flow.async_step_hassio() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "almond", + { + "almond": { + "type": "oauth2", + "client_id": CLIENT_ID_VALUE, + "client_secret": CLIENT_SECRET_VALUE, + }, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "almond", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + "https://almond.stanford.edu/me/api/oauth2/authorize" + f"?response_type=code&client_id={CLIENT_ID_VALUE}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=profile+user-read+user-read-results+user-exec-command" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://almond.stanford.edu/me/api/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data["type"] == "oauth2" + assert entry.data["host"] == "https://almond.stanford.edu/me" From 4dc6d362240a667662d5ecca54a01dbf5969b1c8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Oct 2019 09:37:18 +0100 Subject: [PATCH 1316/3953] Bump pre-commit to 1.20.0 (#28313) --- 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 ad46cfa0741b78..18d2ece04f648a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,7 @@ flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 mypy==0.740 -pre-commit==1.18.3 +pre-commit==1.20.0 pydocstyle==4.0.1 pylint==2.4.3 astroid==2.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 280146ec45d882..8d4714fdeda380 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -11,7 +11,7 @@ flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 mypy==0.740 -pre-commit==1.18.3 +pre-commit==1.20.0 pydocstyle==4.0.1 pylint==2.4.3 astroid==2.3.2 From 756c36171d6ec0d0e8e1cb37789a7da5cc66b225 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 29 Oct 2019 09:37:51 +0100 Subject: [PATCH 1317/3953] Bump youtube_dl to 2019.10.29 (#28312) --- homeassistant/components/media_extractor/manifest.json | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 4fd5470ebdfe30..bb990fc28e7ba4 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,10 +3,10 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.10.22" + "youtube_dl==2019.10.29" ], "dependencies": [ "media_player" ], "codeowners": [] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 54831270ff652c..9d35bb5a8493cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2033,7 +2033,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.10.22 +youtube_dl==2019.10.29 # homeassistant.components.zengge zengge==0.2 From 502f59977af4530b38f8eef6cc037ca7242227fc Mon Sep 17 00:00:00 2001 From: Jonas Janz Date: Tue, 29 Oct 2019 11:02:25 +0100 Subject: [PATCH 1318/3953] Add description for arlo.update service (#28270) --- homeassistant/components/arlo/services.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/arlo/services.yaml b/homeassistant/components/arlo/services.yaml index e69de29bb2d1d6..773bee4430aa7d 100644 --- a/homeassistant/components/arlo/services.yaml +++ b/homeassistant/components/arlo/services.yaml @@ -0,0 +1,4 @@ +# Describes the format for available arlo services + +update: + description: Update the state for all cameras and the base station. \ No newline at end of file From c00b058e531f697912f73cac2a40a7069470a1a6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 29 Oct 2019 12:05:05 +0100 Subject: [PATCH 1319/3953] Cleanup not needed websocket flags for ingress (#28295) --- homeassistant/components/hassio/ingress.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 4ecb9a8419f529..53235f80dcad55 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -167,7 +167,14 @@ def _init_header( # filter flags for name, value in request.headers.items(): - if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING): + if name in ( + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_ENCODING, + hdrs.SEC_WEBSOCKET_EXTENSIONS, + hdrs.SEC_WEBSOCKET_PROTOCOL, + hdrs.SEC_WEBSOCKET_VERSION, + hdrs.SEC_WEBSOCKET_KEY, + ): continue headers[name] = value From 5592eb7709d3a318460245e2124417fb3be1b04c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 29 Oct 2019 16:30:33 +0100 Subject: [PATCH 1320/3953] Updated frontend to 20191025.1 (#28327) --- 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 b23d40605dd7a8..aa7ad8b18f9491 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191025.0" + "home-assistant-frontend==20191025.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 87878b49615127..1933448edda6a6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.23 -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9d35bb5a8493cf..eb1db77263e252 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -649,7 +649,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d4714fdeda380..2f44dab78d8e59 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -243,7 +243,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 1dfb67f0c5adee5a751fd162ccd085027057c39a Mon Sep 17 00:00:00 2001 From: cgtobi Date: Tue, 29 Oct 2019 20:16:05 +0100 Subject: [PATCH 1321/3953] Bump pytest to 5.2.2 (#28230) --- 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 18d2ece04f648a..f2935a423bf5fd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,6 +18,6 @@ pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.1 +pytest==5.2.2 requests_mock==1.7.0 responses==0.10.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2f44dab78d8e59..8166b9713f4f70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.1 +pytest==5.2.2 requests_mock==1.7.0 responses==0.10.6 From 3a9e3ce857160c79d7c8b9e2395d30ea10875e06 Mon Sep 17 00:00:00 2001 From: Renaud Martinet Date: Tue, 29 Oct 2019 20:17:49 +0100 Subject: [PATCH 1322/3953] Add services description for sabnzbd component (#28252) --- homeassistant/components/sabnzbd/services.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/sabnzbd/services.yaml b/homeassistant/components/sabnzbd/services.yaml index e69de29bb2d1d6..654cb50fa1e239 100644 --- a/homeassistant/components/sabnzbd/services.yaml +++ b/homeassistant/components/sabnzbd/services.yaml @@ -0,0 +1,11 @@ +pause: + description: Pauses downloads. +resume: + description: Resumes downloads. +set_speed: + description: Sets the download speed limit. + fields: + speed: + description: Speed limit. If specified as a number with no units, will be interpreted as a percent. If units are provided (e.g., 500K) will be interpreted absolutely. + example: 100 + default: 100 From 6d734a714e6734eece955cf89ac5bf68f5e2595e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 29 Oct 2019 22:17:09 +0100 Subject: [PATCH 1323/3953] Clean up Xiaomi Air Quality Monitor support (cgllc.airmonitor.b1) (#28301) * Clean up Xiaomi Air Quality Monitor support (cgllc.airmonitor.b1) * Remove unused variable * Provide a proper unique_id * Incorporate review * Wrap the method that cause the exception * Undo mistakenly changed file. Fixed in the separate PR. --- .../components/xiaomi_miio/air_quality.py | 102 ++++-------------- 1 file changed, 22 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index e96ed074002403..b80906aa0cb065 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -1,26 +1,20 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" -from miio import AirQualityMonitor, DeviceException +from miio import AirQualityMonitor, Device, DeviceException import voluptuous as vol from homeassistant.components.air_quality import ( AirQualityEntity, PLATFORM_SCHEMA, _LOGGER, - ATTR_PM_2_5, ) -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN, ATTR_TEMPERATURE +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv DEFAULT_NAME = "Xiaomi Miio Air Quality Monitor" -DATA_KEY = "air_quality.xiaomi_miio" ATTR_CO2E = "carbon_dioxide_equivalent" -ATTR_HUMIDITY = "relative_humidity" ATTR_TVOC = "total_volatile_organic_compounds" -ATTR_MANUFACTURER = "manufacturer" -ATTR_MODEL = "model" -ATTR_SW_VERSION = "sw_version" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -32,74 +26,62 @@ PROP_TO_ATTR = { "carbon_dioxide_equivalent": ATTR_CO2E, - "relative_humidity": ATTR_HUMIDITY, - "particulate_matter_2_5": ATTR_PM_2_5, - "temperature": ATTR_TEMPERATURE, "total_volatile_organic_compounds": ATTR_TVOC, - "manufacturer": ATTR_MANUFACTURER, - "model": ATTR_MODEL, - "sw_version": ATTR_SW_VERSION, } async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the sensor from config.""" - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - - host = config.get(CONF_HOST) - token = config.get(CONF_TOKEN) - name = config.get(CONF_NAME) + host = config[CONF_HOST] + token = config[CONF_TOKEN] + name = config[CONF_NAME] _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) - try: - device = AirMonitorB1(name, AirQualityMonitor(host, token, model=None)) + miio_device = Device(host, token) + try: + device_info = await hass.async_add_executor_job(miio_device.info) except DeviceException: raise PlatformNotReady - hass.data[DATA_KEY][host] = device + model = device_info.model + unique_id = f"{model}-{device_info.mac_address}" + _LOGGER.debug( + "%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version, + ) + device = AirMonitorB1(name, AirQualityMonitor(host, token, model), unique_id) + async_add_entities([device], update_before_add=True) class AirMonitorB1(AirQualityEntity): """Air Quality class for Xiaomi cgllc.airmonitor.b1 device.""" - def __init__(self, name, device): + def __init__(self, name, device, unique_id): """Initialize the entity.""" self._name = name self._device = device + self._unique_id = unique_id self._icon = "mdi:cloud" - self._manufacturer = "Xiaomi" self._unit_of_measurement = "μg/m3" - self._model = None - self._mac_address = None - self._sw_version = None self._carbon_dioxide_equivalent = None - self._relative_humidity = None self._particulate_matter_2_5 = None - self._temperature = None self._total_volatile_organic_compounds = None async def async_update(self): """Fetch state from the miio device.""" try: - if self._model is None: - info = await self.hass.async_add_executor_job(self._device.info) - self._model = info.model - self._mac_address = info.mac_address - self._sw_version = info.firmware_version - state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) self._carbon_dioxide_equivalent = state.co2e - self._relative_humidity = round(state.humidity, 1) self._particulate_matter_2_5 = round(state.pm25, 1) - self._temperature = round(state.temperature, 1) self._total_volatile_organic_compounds = round(state.tvoc, 3) except DeviceException as ex: @@ -110,68 +92,33 @@ def name(self): """Return the name of this entity, if any.""" return self._name - @property - def device(self): - """Return the name of this entity, if any.""" - return self._device - @property def icon(self): """Return the icon to use for device if any.""" return self._icon - @property - def manufacturer(self): - """Return the manufacturer version.""" - return self._manufacturer - - @property - def model(self): - """Return the device model.""" - return self._model - - @property - def sw_version(self): - """Return the software version.""" - return self._sw_version - - @property - def mac_address(self): - """Return the mac address.""" - return self._mac_address - @property def unique_id(self): """Return the unique ID.""" - return f"{self._model}-{self._mac_address}" + return self._unique_id @property def carbon_dioxide_equivalent(self): """Return the CO2e (carbon dioxide equivalent) level.""" return self._carbon_dioxide_equivalent - @property - def relative_humidity(self): - """Return the humidity percentage.""" - return self._relative_humidity - @property def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" return self._particulate_matter_2_5 - @property - def temperature(self): - """Return the temperature in °C.""" - return self._temperature - @property def total_volatile_organic_compounds(self): """Return the total volatile organic compounds.""" return self._total_volatile_organic_compounds @property - def state_attributes(self): + def device_state_attributes(self): """Return the state attributes.""" data = {} @@ -186,8 +133,3 @@ def state_attributes(self): def unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement - - @property - def state(self): - """Return the current state.""" - return self._particulate_matter_2_5 From e1eab214ad67a88a2e4a25a1ee82fe1a8c502ecf Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 29 Oct 2019 21:29:39 +0000 Subject: [PATCH 1324/3953] allow multiple heaters per incomfort gateway (#28324) * add multiple heaters per gateway * bump client to handle the above --- homeassistant/components/incomfort/__init__.py | 5 +++-- homeassistant/components/incomfort/binary_sensor.py | 7 ++++--- homeassistant/components/incomfort/climate.py | 6 ++++-- homeassistant/components/incomfort/manifest.json | 2 +- homeassistant/components/incomfort/sensor.py | 10 ++++------ homeassistant/components/incomfort/water_heater.py | 4 ++-- requirements_all.txt | 2 +- 7 files changed, 19 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index adf57e35093e5c..bb11565006148c 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -44,12 +44,13 @@ async def async_setup(hass, hass_config): ) try: - heater = incomfort_data["heater"] = list(await client.heaters)[0] + heaters = incomfort_data["heaters"] = list(await client.heaters) except ClientResponseError as err: _LOGGER.warning("Setup failed, check your configuration, message is: %s", err) return False - await heater.update() + for heater in heaters: + await heater.update() for platform in ["water_heater", "binary_sensor", "sensor", "climate"]: hass.async_create_task( diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index b5dbd8e223d3f8..150515cbbf5494 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -11,9 +11,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return - async_add_entities( - [IncomfortFailed(hass.data[DOMAIN]["client"], hass.data[DOMAIN]["heater"])] - ) + client = hass.data[DOMAIN]["client"] + heaters = hass.data[DOMAIN]["heaters"] + + async_add_entities([IncomfortFailed(client, h) for h in heaters]) class IncomfortFailed(IncomfortChild, BinarySensorDevice): diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 95ccf1863724ca..23bda6b2fdf97a 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -17,9 +17,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return client = hass.data[DOMAIN]["client"] - heater = hass.data[DOMAIN]["heater"] + heaters = hass.data[DOMAIN]["heaters"] - async_add_entities([InComfortClimate(client, heater, r) for r in heater.rooms]) + async_add_entities( + [InComfortClimate(client, h, r) for h in heaters for r in h.rooms] + ) class InComfortClimate(IncomfortChild, ClimateDevice): diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 4bdf43f89578d2..45365c7e354f33 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/integrations/incomfort", "requirements": [ - "incomfort-client==0.3.5" + "incomfort-client==0.4.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index f3170b7b9bba49..4164225b0d79d6 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -29,14 +29,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return client = hass.data[DOMAIN]["client"] - heater = hass.data[DOMAIN]["heater"] + heaters = hass.data[DOMAIN]["heaters"] async_add_entities( - [ - IncomfortPressure(client, heater, INCOMFORT_PRESSURE), - IncomfortTemperature(client, heater, INCOMFORT_HEATER_TEMP), - IncomfortTemperature(client, heater, INCOMFORT_TAP_TEMP), - ] + [IncomfortPressure(client, h, INCOMFORT_PRESSURE) for h in heaters] + + [IncomfortTemperature(client, h, INCOMFORT_HEATER_TEMP) for h in heaters] + + [IncomfortTemperature(client, h, INCOMFORT_TAP_TEMP) for h in heaters] ) diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 0015107b40f0de..9096a7cb72c894 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -22,9 +22,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return client = hass.data[DOMAIN]["client"] - heater = hass.data[DOMAIN]["heater"] + heaters = hass.data[DOMAIN]["heaters"] - async_add_entities([IncomfortWaterHeater(client, heater)]) + async_add_entities([IncomfortWaterHeater(client, h) for h in heaters]) class IncomfortWaterHeater(IncomfortEntity, WaterHeaterDevice): diff --git a/requirements_all.txt b/requirements_all.txt index eb1db77263e252..51f7d0816c7a5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -694,7 +694,7 @@ iglo==1.2.7 ihcsdk==2.3.0 # homeassistant.components.incomfort -incomfort-client==0.3.5 +incomfort-client==0.4.0 # homeassistant.components.influxdb influxdb==5.2.3 From f021e5832a0b7f01892e0281e1072a3bf877c847 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 29 Oct 2019 12:05:05 +0100 Subject: [PATCH 1325/3953] Cleanup not needed websocket flags for ingress (#28295) --- homeassistant/components/hassio/ingress.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 4ecb9a8419f529..53235f80dcad55 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -167,7 +167,14 @@ def _init_header( # filter flags for name, value in request.headers.items(): - if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING): + if name in ( + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_ENCODING, + hdrs.SEC_WEBSOCKET_EXTENSIONS, + hdrs.SEC_WEBSOCKET_PROTOCOL, + hdrs.SEC_WEBSOCKET_VERSION, + hdrs.SEC_WEBSOCKET_KEY, + ): continue headers[name] = value From c104efc18d28acc765fbac6c6b50354dbfed57c4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 29 Oct 2019 16:30:33 +0100 Subject: [PATCH 1326/3953] Updated frontend to 20191025.1 (#28327) --- 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 b23d40605dd7a8..aa7ad8b18f9491 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191025.0" + "home-assistant-frontend==20191025.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 682d9b1e1dbced..85bb00ce6eb299 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 869c6ea90c3dcc..df9cafe7aa005d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72aa75a4676ba7..36f423860d06bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From cf6d11db8d6ab2be890ac06e03765bc28dd085fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 29 Oct 2019 16:17:34 -0700 Subject: [PATCH 1327/3953] Bumped version to 0.101.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2aa5e97fa94941..7367d9723f6e5a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0b4" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From e700384cceba83f7f5e9222eee6dac2161585000 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 30 Oct 2019 00:32:11 +0000 Subject: [PATCH 1328/3953] [ci skip] Translation update --- .../components/almond/.translations/ca.json | 9 ++++++ .../components/almond/.translations/en.json | 15 +++++---- .../components/almond/.translations/fr.json | 9 ++++++ .../components/almond/.translations/lb.json | 9 ++++++ .../components/almond/.translations/ru.json | 9 ++++++ .../almond/.translations/zh-Hant.json | 9 ++++++ .../components/cover/.translations/lb.json | 4 ++- .../components/deconz/.translations/fr.json | 2 +- .../device_tracker/.translations/lb.json | 8 +++++ .../components/glances/.translations/fr.json | 2 +- .../huawei_lte/.translations/fr.json | 2 +- .../media_player/.translations/lb.json | 11 +++++++ .../components/met/.translations/fr.json | 2 +- .../components/plex/.translations/fr.json | 2 +- .../components/ps4/.translations/fr.json | 2 +- .../components/sensor/.translations/nl.json | 32 +++++++++---------- .../components/somfy/.translations/lb.json | 5 +++ 17 files changed, 102 insertions(+), 30 deletions(-) create mode 100644 homeassistant/components/almond/.translations/ca.json create mode 100644 homeassistant/components/almond/.translations/fr.json create mode 100644 homeassistant/components/almond/.translations/lb.json create mode 100644 homeassistant/components/almond/.translations/ru.json create mode 100644 homeassistant/components/almond/.translations/zh-Hant.json create mode 100644 homeassistant/components/device_tracker/.translations/lb.json create mode 100644 homeassistant/components/media_player/.translations/lb.json diff --git a/homeassistant/components/almond/.translations/ca.json b/homeassistant/components/almond/.translations/ca.json new file mode 100644 index 00000000000000..cf4618d227210c --- /dev/null +++ b/homeassistant/components/almond/.translations/ca.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Almond.", + "cannot_connect": "No es pot connectar amb el servidor d'Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/en.json b/homeassistant/components/almond/.translations/en.json index cc48b1c28ebf3d..89134cbb170f26 100644 --- a/homeassistant/components/almond/.translations/en.json +++ b/homeassistant/components/almond/.translations/en.json @@ -1,8 +1,9 @@ { - "config": { - "abort": { - "already_setup": "You can only configure one Almond account." - }, - "title": "Almond" - } -} + "config": { + "abort": { + "already_setup": "You can only configure one Almond account.", + "cannot_connect": "Unable to connect to the Almond server." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/fr.json b/homeassistant/components/almond/.translations/fr.json new file mode 100644 index 00000000000000..b304a596b3ac5e --- /dev/null +++ b/homeassistant/components/almond/.translations/fr.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Almond", + "cannot_connect": "Impossible de se connecter au serveur Almond" + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/lb.json b/homeassistant/components/almond/.translations/lb.json new file mode 100644 index 00000000000000..f74874d283aa1c --- /dev/null +++ b/homeassistant/components/almond/.translations/lb.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Almond Kont konfigur\u00e9ieren.", + "cannot_connect": "Kann sech net mam Almond Server verbannen." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ru.json b/homeassistant/components/almond/.translations/ru.json new file mode 100644 index 00000000000000..b513d5b28d7721 --- /dev/null +++ b/homeassistant/components/almond/.translations/ru.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/zh-Hant.json b/homeassistant/components/almond/.translations/zh-Hant.json new file mode 100644 index 00000000000000..c84b2dd432bf52 --- /dev/null +++ b/homeassistant/components/almond/.translations/zh-Hant.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Almond \u5e33\u865f\u3002", + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002" + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/lb.json b/homeassistant/components/cover/.translations/lb.json index b0c9e1d0d4c3fc..4f7a898c7722c9 100644 --- a/homeassistant/components/cover/.translations/lb.json +++ b/homeassistant/components/cover/.translations/lb.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} ass zou", "is_closing": "{entity_name} g\u00ebtt zougemaach", "is_open": "{entity_name} ass op", - "is_opening": "{entity_name} g\u00ebtt opgemaach" + "is_opening": "{entity_name} g\u00ebtt opgemaach", + "is_position": "{entity_name} positioun ass", + "is_tilt_position": "{entity_name} kipp positioun ass" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index d1fc7fa7286f17..3b29dbf486deb5 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -29,7 +29,7 @@ "title": "Initialiser la passerelle deCONZ" }, "link": { - "description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer aupr\u00e8s de Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"", + "description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer avec Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres avanc\u00e9s du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"", "title": "Lien vers deCONZ" }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/lb.json b/homeassistant/components/device_tracker/.translations/lb.json new file mode 100644 index 00000000000000..98a066ef8e83e7 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/lb.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} ass doheem", + "is_not_home": "{entity_name} ass net doheem" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/fr.json b/homeassistant/components/glances/.translations/fr.json index 0391012c4cd2c5..b65df092b32af2 100644 --- a/homeassistant/components/glances/.translations/fr.json +++ b/homeassistant/components/glances/.translations/fr.json @@ -14,7 +14,7 @@ "name": "Nom", "password": "Mot de passe", "port": "Port", - "ssl": "V\u00e9rifier la certification du syst\u00e8me", + "ssl": "Utiliser SSL / TLS pour se connecter au syst\u00e8me Glances", "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier la certification du syst\u00e8me", "version": "Glances API Version (2 ou 3)" diff --git a/homeassistant/components/huawei_lte/.translations/fr.json b/homeassistant/components/huawei_lte/.translations/fr.json index 19f3330535600a..e0394d525d4acc 100644 --- a/homeassistant/components/huawei_lte/.translations/fr.json +++ b/homeassistant/components/huawei_lte/.translations/fr.json @@ -31,7 +31,7 @@ "init": { "data": { "recipient": "Destinataires des notifications SMS", - "track_new_devices": "Suivre de nouveaux appareils" + "track_new_devices": "Suivre les nouveaux appareils" } } } diff --git a/homeassistant/components/media_player/.translations/lb.json b/homeassistant/components/media_player/.translations/lb.json new file mode 100644 index 00000000000000..99b6f12fd6603a --- /dev/null +++ b/homeassistant/components/media_player/.translations/lb.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} waart", + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un", + "is_paused": "{entity_name} ass paus\u00e9iert", + "is_playing": "{entity_name} spillt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/fr.json b/homeassistant/components/met/.translations/fr.json index 7100cb5e4a778f..164cb13967b71d 100644 --- a/homeassistant/components/met/.translations/fr.json +++ b/homeassistant/components/met/.translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" + "name_exists": "Emplacement d\u00e9j\u00e0 existant" }, "step": { "user": { diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index c06d314ec721dc..2eef7a5e9a2f50 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -42,7 +42,7 @@ "manual_setup": "Installation manuelle", "token": "Jeton plex" }, - "description": "Entrez un jeton Plex pour la configuration automatique.", + "description": "Continuez pour autoriser plex.tv ou configurez manuellement un serveur.", "title": "Connecter un serveur Plex" } }, diff --git a/homeassistant/components/ps4/.translations/fr.json b/homeassistant/components/ps4/.translations/fr.json index 991222d45be78b..5c49723657c34b 100644 --- a/homeassistant/components/ps4/.translations/fr.json +++ b/homeassistant/components/ps4/.translations/fr.json @@ -25,7 +25,7 @@ "name": "Nom", "region": "R\u00e9gion" }, - "description": "Entrez vos informations PlayStation 4. Pour \"Code PIN\", acc\u00e9dez \u00e0 \"Param\u00e8tres\" sur votre console PlayStation 4. Ensuite, acc\u00e9dez \u00e0 \"Param\u00e8tres de connexion de l'application mobile\" et s\u00e9lectionnez \"Ajouter un p\u00e9riph\u00e9rique\". Entrez le code PIN qui est affich\u00e9.", + "description": "Entrez vos informations PlayStation 4. Pour \"Code PIN\", acc\u00e9dez \u00e0 \"Param\u00e8tres\" sur votre console PlayStation 4. Ensuite, acc\u00e9dez \u00e0 \"Param\u00e8tres de connexion de l'application mobile\" et s\u00e9lectionnez \"Ajouter un p\u00e9riph\u00e9rique\". Entrez le code PIN qui est affich\u00e9. Consultez la documentation pour plus d'informations.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json index 796e9c97071daf..03eff2a7f6d7f6 100644 --- a/homeassistant/components/sensor/.translations/nl.json +++ b/homeassistant/components/sensor/.translations/nl.json @@ -2,25 +2,25 @@ "device_automation": { "condition_type": { "is_battery_level": "Huidige batterijniveau {entity_name}", - "is_humidity": "{entity_name} vochtigheidsgraad", - "is_illuminance": "{entity_name} verlichtingssterkte", - "is_power": "{entity_name}\nvermogen", - "is_pressure": "{entity_name} druk", - "is_signal_strength": "{entity_name} signaalsterkte", - "is_temperature": "{entity_name} temperatuur", - "is_timestamp": "{entity_name} tijdstip", + "is_humidity": "Huidige {entity_name} vochtigheidsgraad", + "is_illuminance": "Huidige {entity_name} verlichtingssterkte", + "is_power": "Huidige {entity_name}\nvermogen", + "is_pressure": "Huidige {entity_name} druk", + "is_signal_strength": "Huidige {entity_name} signaalsterkte", + "is_temperature": "Huidige {entity_name} temperatuur", + "is_timestamp": "Huidige {entity_name} tijdstip", "is_value": "Huidige {entity_name} waarde" }, "trigger_type": { - "battery_level": "{entity_name} batterijniveau", - "humidity": "{entity_name} vochtigheidsgraad", - "illuminance": "{entity_name} verlichtingssterkte", - "power": "{entity_name} vermogen", - "pressure": "{entity_name} druk", - "signal_strength": "{entity_name} signaalsterkte", - "temperature": "{entity_name} temperatuur", - "timestamp": "{entity_name} tijdstip", - "value": "{entity_name} waarde" + "battery_level": "{entity_name} batterijniveau gewijzigd", + "humidity": "{entity_name} vochtigheidsgraad gewijzigd", + "illuminance": "{entity_name} verlichtingssterkte gewijzigd", + "power": "{entity_name} vermogen gewijzigd", + "pressure": "{entity_name} druk gewijzigd", + "signal_strength": "{entity_name} signaalsterkte gewijzigd", + "temperature": "{entity_name} temperatuur gewijzigd", + "timestamp": "{entity_name} tijdstip gewijzigd", + "value": "{entity_name} waarde gewijzigd" } } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/lb.json b/homeassistant/components/somfy/.translations/lb.json index 62f588292416fc..0a1cfa83250c37 100644 --- a/homeassistant/components/somfy/.translations/lb.json +++ b/homeassistant/components/somfy/.translations/lb.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Erfollegr\u00e4ich mat Somfy authentifiz\u00e9iert." }, + "step": { + "pick_implementation": { + "title": "Wielt Authentifikatiouns Method aus" + } + }, "title": "Somfy" } } \ No newline at end of file From 24c29f922752bc734b19a7bf10a66be996bea870 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 29 Oct 2019 20:34:03 -0700 Subject: [PATCH 1329/3953] Add OAuth2 config flow scaffold (#28220) * Add OAuth2 scaffold * Generate integration if non-existing domain specified * Update URL --- homeassistant/const.py | 2 + script/scaffold/__main__.py | 50 ++++-- script/scaffold/docs.py | 124 ++++++------- script/scaffold/gather_info.py | 170 ++++++++---------- script/scaffold/generate.py | 78 ++++---- script/scaffold/model.py | 7 + .../config_flow/integration/__init__.py | 49 +++++ .../integration/__init__.py | 49 +++++ .../integration/__init__.py | 94 ++++++++++ .../config_flow_oauth2/integration/api.py | 58 ++++++ .../integration/config_flow.py | 23 +++ .../tests/test_config_flow.py | 60 +++++++ .../integration/integration/__init__.py | 6 +- 13 files changed, 564 insertions(+), 206 deletions(-) create mode 100644 script/scaffold/templates/config_flow/integration/__init__.py create mode 100644 script/scaffold/templates/config_flow_discovery/integration/__init__.py create mode 100644 script/scaffold/templates/config_flow_oauth2/integration/__init__.py create mode 100644 script/scaffold/templates/config_flow_oauth2/integration/api.py create mode 100644 script/scaffold/templates/config_flow_oauth2/integration/config_flow.py create mode 100644 script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py diff --git a/homeassistant/const.py b/homeassistant/const.py index e1e9757dd02bb6..449e7a90087b83 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -40,6 +40,8 @@ CONF_BINARY_SENSORS = "binary_sensors" CONF_BLACKLIST = "blacklist" CONF_BRIGHTNESS = "brightness" +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" CONF_CODE = "code" CONF_COLOR_TEMP = "color_temp" CONF_COMMAND = "command" diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 2258840f430d92..78490b84ba3d9b 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -48,34 +48,46 @@ def main(): args = get_arguments() info = gather_info.gather_info(args) + print() + + # If we are calling scaffold on a non-existing integration, + # We're going to first make it. If we're making an integration, + # we will also make a config flow to go with it. + + if info.is_new: + generate.generate("integration", info) - generate.generate(args.template, info) + # If it's a new integration and it's not a config flow, + # create a config flow too. + if not args.template.startswith("config_flow"): + if info.oauth2: + template = "config_flow_oauth2" + elif info.authentication or not info.discoverable: + template = "config_flow" + else: + template = "config_flow_discovery" - # If creating new integration, create config flow too - if args.template == "integration": - if info.authentication or not info.discoverable: - template = "config_flow" - else: - template = "config_flow_discovery" + generate.generate(template, info) - generate.generate(template, info) + # If we wanted a new integration, we've already done our work. + if args.template != "integration": + generate.generate(args.template, info) + + pipe_null = "" if args.develop else "> /dev/null" print("Running hassfest to pick up new information.") - subprocess.run("python -m script.hassfest", shell=True) + subprocess.run(f"python -m script.hassfest {pipe_null}", shell=True) print() - print("Running tests") - print(f"$ pytest -vvv tests/components/{info.domain}") - if ( - subprocess.run( - f"pytest -vvv tests/components/{info.domain}", shell=True - ).returncode - != 0 - ): - return 1 + print("Running gen_requirements_all to pick up new information.") + subprocess.run(f"python -m script.gen_requirements_all {pipe_null}", shell=True) print() - print(f"Done!") + if args.develop: + print("Running tests") + print(f"$ pytest -vvv tests/components/{info.domain}") + subprocess.run(f"pytest -vvv tests/components/{info.domain}", shell=True) + print() docs.print_relevant_docs(args.template, info) diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index bb119c0e42e4b3..5df663fec0b240 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -2,72 +2,76 @@ from .model import Info -def print_relevant_docs(template: str, info: Info) -> None: - """Print relevant docs.""" - if template == "integration": - print( - f""" -Your integration has been created at {info.integration_dir} . Next step is to fill in the blanks for the code marked with TODO. - -For a breakdown of each file, check the developer documentation at: -https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html -""" - ) +DATA = { + "config_flow": { + "title": "Config Flow", + "docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html", + }, + "config_flow_discovery": { + "title": "Discoverable Config Flow", + "docs": "https://developers.home-assistant.io/docs/en/config_entries_config_flow_handler.html#discoverable-integrations-that-require-no-authentication", + }, + "config_flow_oauth2": { + "title": "OAuth2 Config Flow", + "docs": "https://developers.home-assistant.io/docs/en/next/config_entries_config_flow_handler.html#configuration-via-oauth2", + }, + "device_action": { + "title": "Device Action", + "docs": "https://developers.home-assistant.io/docs/en/device_automation_action.html", + }, + "device_condition": { + "title": "Device Condition", + "docs": "https://developers.home-assistant.io/docs/en/device_automation_condition.html", + }, + "device_trigger": { + "title": "Device Trigger", + "docs": "https://developers.home-assistant.io/docs/en/device_automation_trigger.html", + }, + "integration": { + "title": "Integration", + "docs": "https://developers.home-assistant.io/docs/en/creating_integration_file_structure.html", + }, + "reproduce_state": { + "title": "Reproduce State", + "docs": "https://developers.home-assistant.io/docs/en/reproduce_state_index.html", + "extra": "You will now need to update the code to make sure that every attribute that can occur in the state will cause the right service to be called.", + }, +} - elif template == "config_flow": - print( - f""" -The config flow has been added to the {info.domain} integration. Next step is to fill in the blanks for the code marked with TODO. -""" - ) - - elif template == "reproduce_state": - print( - f""" -Reproduce state code has been added to the {info.domain} integration: - - {info.integration_dir / "reproduce_state.py"} - - {info.tests_dir / "test_reproduce_state.py"} -You will now need to update the code to make sure that every attribute -that can occur in the state will cause the right service to be called. -""" - ) +def print_relevant_docs(template: str, info: Info) -> None: + """Print relevant docs.""" + data = DATA[template] - elif template == "device_trigger": - print( - f""" -Device trigger base has been added to the {info.domain} integration: - - {info.integration_dir / "device_trigger.py"} - - {info.integration_dir / "strings.json"} (translations) - - {info.tests_dir / "test_device_trigger.py"} + print() + print("**************************") + print() + print() + print(f"{data['title']} code has been generated") + print() + if info.files_added: + print("Added the following files:") + for file in info.files_added: + print(f"- {file}") + print() -You will now need to update the code to make sure that relevant triggers -are exposed. -""" - ) + if info.tests_added: + print("Added the following tests:") + for file in info.tests_added: + print(f"- {file}") + print() - elif template == "device_condition": + if info.examples_added: print( - f""" -Device condition base has been added to the {info.domain} integration: - - {info.integration_dir / "device_condition.py"} - - {info.integration_dir / "strings.json"} (translations) - - {info.tests_dir / "test_device_condition.py"} - -You will now need to update the code to make sure that relevant condtions -are exposed. -""" + "Because some files already existed, we added the following example files. Please copy the relevant code to the existing files." ) + for file in info.examples_added: + print(f"- {file}") + print() - elif template == "device_action": - print( - f""" -Device action base has been added to the {info.domain} integration: - - {info.integration_dir / "device_action.py"} - - {info.integration_dir / "strings.json"} (translations) - - {info.tests_dir / "test_device_action.py"} + print( + f"The next step is to look at the files and deal with all areas marked as TODO." + ) -You will now need to update the code to make sure that relevant services -are exposed as actions. -""" - ) + if "extra" in data: + print(data["extra"]) diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index a7263daaf41982..12cb319d188ce0 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -13,36 +13,14 @@ def gather_info(arguments) -> Info: """Gather info.""" - existing = arguments.template != "integration" - - if arguments.develop: + if arguments.integration: + info = {"domain": arguments.integration} + elif arguments.develop: print("Running in developer mode. Automatically filling in info.") print() - - if existing: - if arguments.develop: - return _load_existing_integration("develop") - - if arguments.integration: - return _load_existing_integration(arguments.integration) - - return gather_existing_integration() - - if arguments.develop: - return Info( - domain="develop", - name="Develop Hub", - codeowner="@developer", - requirement="aiodevelop==1.2.3", - ) - - return gather_new_integration() - - -def gather_new_integration() -> Info: - """Gather info about new integration from user.""" - return Info( - **_gather_info( + info = {"domain": "develop"} + else: + info = _gather_info( { "domain": { "prompt": "What is the domain?", @@ -52,84 +30,87 @@ def gather_new_integration() -> Info: "Domains cannot contain spaces or special characters.", lambda value: value == slugify(value), ], - [ - "There already is an integration with this domain.", - lambda value: not (COMPONENT_DIR / value).exists(), - ], ], - }, - "name": { - "prompt": "What is the name of your integration?", - "validators": [CHECK_EMPTY], - }, - "codeowner": { - "prompt": "What is your GitHub handle?", - "validators": [ - CHECK_EMPTY, - [ - 'GitHub handles need to start with an "@"', - lambda value: value.startswith("@"), - ], - ], - }, - "requirement": { - "prompt": "What PyPI package and version do you depend on? Leave blank for none.", - "validators": [ - [ - "Versions should be pinned using '=='.", - lambda value: not value or "==" in value, - ] - ], - }, + } + } + ) + + info["is_new"] = not (COMPONENT_DIR / info["domain"] / "manifest.json").exists() + + if not info["is_new"]: + return _load_existing_integration(info["domain"]) + + if arguments.develop: + info.update( + { + "name": "Develop Hub", + "codeowner": "@developer", + "requirement": "aiodevelop==1.2.3", + "oauth2": True, + } + ) + else: + info.update(gather_new_integration(arguments.template == "integration")) + + return Info(**info) + + +YES_NO = { + "validators": [["Type either 'yes' or 'no'", lambda value: value in ("yes", "no")]], + "convertor": lambda value: value == "yes", +} + + +def gather_new_integration(determine_auth: bool) -> Info: + """Gather info about new integration from user.""" + fields = { + "name": { + "prompt": "What is the name of your integration?", + "validators": [CHECK_EMPTY], + }, + "codeowner": { + "prompt": "What is your GitHub handle?", + "validators": [ + CHECK_EMPTY, + [ + 'GitHub handles need to start with an "@"', + lambda value: value.startswith("@"), + ], + ], + }, + "requirement": { + "prompt": "What PyPI package and version do you depend on? Leave blank for none.", + "validators": [ + [ + "Versions should be pinned using '=='.", + lambda value: not value or "==" in value, + ] + ], + }, + } + + if determine_auth: + fields.update( + { "authentication": { "prompt": "Does Home Assistant need the user to authenticate to control the device/service? (yes/no)", "default": "yes", - "validators": [ - [ - "Type either 'yes' or 'no'", - lambda value: value in ("yes", "no"), - ] - ], - "convertor": lambda value: value == "yes", + **YES_NO, }, "discoverable": { "prompt": "Is the device/service discoverable on the local network? (yes/no)", "default": "no", - "validators": [ - [ - "Type either 'yes' or 'no'", - lambda value: value in ("yes", "no"), - ] - ], - "convertor": lambda value: value == "yes", + **YES_NO, + }, + "oauth2": { + "prompt": "Can the user authenticate the device using OAuth2? (yes/no)", + "default": "no", + **YES_NO, }, } ) - ) - - -def gather_existing_integration() -> Info: - """Gather info about existing integration from user.""" - answers = _gather_info( - { - "domain": { - "prompt": "What is the domain?", - "validators": [ - CHECK_EMPTY, - [ - "Domains cannot contain spaces or special characters.", - lambda value: value == slugify(value), - ], - [ - "This integration does not exist.", - lambda value: (COMPONENT_DIR / value).exists(), - ], - ], - } - } - ) - return _load_existing_integration(answers["domain"]) + return _gather_info(fields) def _load_existing_integration(domain) -> Info: @@ -179,5 +160,4 @@ def _gather_info(fields) -> dict: value = info["convertor"](value) answers[key] = value - print() return answers diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index e16316fd76b129..a04cdb3ef5e0bd 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -1,7 +1,6 @@ """Generate an integration.""" from pathlib import Path -from .error import ExitApp from .model import Info TEMPLATE_DIR = Path(__file__).parent / "templates" @@ -11,8 +10,6 @@ def generate(template: str, info: Info) -> None: """Generate a template.""" - _validate(template, info) - print(f"Scaffolding {template} for the {info.domain} integration...") _ensure_tests_dir_exists(info) _generate(TEMPLATE_DIR / template / "integration", info.integration_dir, info) @@ -21,13 +18,6 @@ def generate(template: str, info: Info) -> None: print() -def _validate(template, info): - """Validate we can run this task.""" - if template == "config_flow": - if (info.integration_dir / "config_flow.py").exists(): - raise ExitApp(f"Integration {info.domain} already has a config flow.") - - def _generate(src_dir, target_dir, info: Info) -> None: """Generate an integration.""" replaces = {"NEW_DOMAIN": info.domain, "NEW_NAME": info.name} @@ -42,6 +32,20 @@ def _generate(src_dir, target_dir, info: Info) -> None: content = content.replace(to_search, to_replace) target_file = target_dir / source_file.relative_to(src_dir) + + # If the target file exists, create our template as EXAMPLE_. + # Exception: If we are creating a new integration, we can end up running integration base + # and a config flows on top of one another. In that case, we want to override the files. + if not info.is_new and target_file.exists(): + new_name = f"EXAMPLE_{target_file.name}" + print(f"File {target_file} already exists, creating {new_name} instead.") + target_file = target_file.parent / new_name + info.examples_added.add(target_file) + elif src_dir.name == "integration": + info.files_added.add(target_file) + else: + info.tests_added.add(target_file) + print(f"Writing {target_file}") target_file.write_text(content) @@ -58,6 +62,11 @@ def _ensure_tests_dir_exists(info: Info) -> None: ) +def _append(path: Path, text): + """Append some text to a path.""" + path.write_text(path.read_text() + text) + + def _custom_tasks(template, info) -> None: """Handle custom tasks for templates.""" if template == "integration": @@ -68,7 +77,7 @@ def _custom_tasks(template, info) -> None: info.update_manifest(**changes) - if template == "device_trigger": + elif template == "device_trigger": info.update_strings( device_automation={ **info.strings().get("device_automation", {}), @@ -79,7 +88,7 @@ def _custom_tasks(template, info) -> None: } ) - if template == "device_condition": + elif template == "device_condition": info.update_strings( device_automation={ **info.strings().get("device_automation", {}), @@ -90,7 +99,7 @@ def _custom_tasks(template, info) -> None: } ) - if template == "device_action": + elif template == "device_action": info.update_strings( device_automation={ **info.strings().get("device_automation", {}), @@ -101,7 +110,7 @@ def _custom_tasks(template, info) -> None: } ) - if template == "config_flow": + elif template == "config_flow": info.update_manifest(config_flow=True) info.update_strings( config={ @@ -118,7 +127,7 @@ def _custom_tasks(template, info) -> None: } ) - if template == "config_flow_discovery": + elif template == "config_flow_discovery": info.update_manifest(config_flow=True) info.update_strings( config={ @@ -136,19 +145,28 @@ def _custom_tasks(template, info) -> None: } ) - if template in ("config_flow", "config_flow_discovery"): - init_file = info.integration_dir / "__init__.py" - init_file.write_text( - init_file.read_text() - + """ - -async def async_setup_entry(hass, entry): - \"\"\"Set up a config entry for NEW_NAME.\"\"\" - # TODO forward the entry for each platform that you want to set up. - # hass.async_create_task( - # hass.config_entries.async_forward_entry_setup(entry, "media_player") - # ) - - return True -""" + elif template == "config_flow_oauth2": + info.update_manifest(config_flow=True) + info.update_strings( + config={ + "title": info.name, + "step": { + "pick_implementation": {"title": "Pick Authentication Method"} + }, + "abort": { + "missing_configuration": "The Somfy component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": f"Successfully authenticated with {info.name}." + }, + } + ) + _append( + info.integration_dir / "const.py", + """ + +# TODO Update with your own urls +OAUTH2_AUTHORIZE = "https://www.example.com/auth/authorize" +OAUTH2_TOKEN = "https://www.example.com/auth/token" +""", ) diff --git a/script/scaffold/model.py b/script/scaffold/model.py index 68ab771122e0ab..bfbcfa52544958 100644 --- a/script/scaffold/model.py +++ b/script/scaffold/model.py @@ -1,6 +1,7 @@ """Models for scaffolding.""" import json from pathlib import Path +from typing import Set import attr @@ -13,10 +14,16 @@ class Info: domain: str = attr.ib() name: str = attr.ib() + is_new: bool = attr.ib() codeowner: str = attr.ib(default=None) requirement: str = attr.ib(default=None) authentication: str = attr.ib(default=None) discoverable: str = attr.ib(default=None) + oauth2: str = attr.ib(default=None) + + files_added: Set[Path] = attr.ib(factory=set) + tests_added: Set[Path] = attr.ib(factory=set) + examples_added: Set[Path] = attr.ib(factory=set) @property def integration_dir(self) -> Path: diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py new file mode 100644 index 00000000000000..403453a1f6b4e6 --- /dev/null +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -0,0 +1,49 @@ +"""The NEW_NAME integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the NEW_NAME component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Somfy from a config entry.""" + # TODO Store an API object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = MyApi(…) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py new file mode 100644 index 00000000000000..403453a1f6b4e6 --- /dev/null +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -0,0 +1,49 @@ +"""The NEW_NAME integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the NEW_NAME component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Somfy from a config entry.""" + # TODO Store an API object for your platforms to access + # hass.data[DOMAIN][entry.entry_id] = MyApi(…) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py new file mode 100644 index 00000000000000..43b4c6f31cd2d9 --- /dev/null +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -0,0 +1,94 @@ +"""The NEW_NAME integration.""" +import asyncio + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.helpers import ( + config_validation as cv, + config_entry_oauth2_flow, + aiohttp_client, +) +from homeassistant.config_entries import ConfigEntry + +from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from . import api, config_flow + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +# TODO List the platforms that you want to support. +# For your initial PR, limit it to 1 platform. +PLATFORMS = ["light"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the NEW_NAME component.""" + hass.data[DOMAIN] = {} + + if DOMAIN not in config: + return True + + config_flow.OAuth2FlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + ), + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Somfy from a config entry.""" + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + + # If using a requests-based API lib + hass.data[DOMAIN][entry.entry_id] = api.ConfigEntryAuth(hass, entry, session) + + # If using an aiohttp-based API lib + hass.data[DOMAIN][entry.entry_id] = api.AsyncConfigEntryAuth( + aiohttp_client.async_get_clientsession(hass), session + ) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/script/scaffold/templates/config_flow_oauth2/integration/api.py b/script/scaffold/templates/config_flow_oauth2/integration/api.py new file mode 100644 index 00000000000000..c5aa4a81ebe0f1 --- /dev/null +++ b/script/scaffold/templates/config_flow_oauth2/integration/api.py @@ -0,0 +1,58 @@ +"""API for NEW_NAME bound to HASS OAuth.""" +from asyncio import run_coroutine_threadsafe + +from aiohttp import ClientSession +import my_pypi_package + +from homeassistant import core, config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +# TODO the following two API examples are based on our suggested best practices +# for libraries using OAuth2 with requests or aiohttp. Delete the one you won't use. +# For more info see the docs at . + + +class ConfigEntryAuth(my_pypi_package.AbstractAuth): + """Provide NEW_NAME authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize NEW_NAME Auth.""" + self.hass = hass + self.config_entry = config_entry + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + super().__init__(self.session.token) + + def refresh_tokens(self) -> dict: + """Refresh and return new NEW_NAME tokens using Home Assistant OAuth2 session.""" + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop + ).result() + + return self.session.token + + +class AsyncConfigEntryAuth(my_pypi_package.AbstractAuth): + """Provide NEW_NAME authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + websession: ClientSession, + oauth_session: config_entry_oauth2_flow.OAuth2Session, + ): + """Initialize NEW_NAME auth.""" + super().__init__(websession) + self._oauth_session = oauth_session + + async def async_get_access_token(self): + """Return a valid access token.""" + if not self._oauth_session.is_valid: + await self._oauth_session.async_ensure_token_valid() + + return self._oauth_session.token diff --git a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py new file mode 100644 index 00000000000000..1112a404e6087b --- /dev/null +++ b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py @@ -0,0 +1,23 @@ +"""Config flow for NEW_NAME.""" +import logging + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_oauth2_flow +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle NEW_NAME OAuth2 authentication.""" + + DOMAIN = DOMAIN + # TODO Pick one from config_entries.CONN_CLASS_* + CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) diff --git a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py new file mode 100644 index 00000000000000..7e61bcbfb1b913 --- /dev/null +++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py @@ -0,0 +1,60 @@ +"""Test the NEW_NAME config flow.""" +from homeassistant import config_entries, setup, data_entry_flow +from homeassistant.components.NEW_DOMAIN.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) +from homeassistant.helpers import config_entry_oauth2_flow + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + + +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "NEW_DOMAIN", + { + "NEW_DOMAIN": { + "type": "oauth2", + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + }, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "NEW_DOMAIN", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data["type"] == "oauth2" diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py index 7ab8b736782f62..c2ae59aaad4417 100644 --- a/script/scaffold/templates/integration/integration/__init__.py +++ b/script/scaffold/templates/integration/integration/__init__.py @@ -1,12 +1,14 @@ """The NEW_NAME integration.""" import voluptuous as vol +from homeassistant.core import HomeAssistant + from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}) +CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}, extra=vol.ALLOW_EXTRA) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: dict): """Set up the NEW_NAME integration.""" return True From f8efc2adc65b883e764d713f29197a2f97bea8e0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 30 Oct 2019 07:57:40 +0100 Subject: [PATCH 1330/3953] Fix KeyError in decora setup (#28279) * Imported homeassistant.util and slugified address if no name is specified * Added a custom validator function in case name is not set in config * Removed logger.debug line only used for testing --- homeassistant/components/decora/light.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 4d2d10ccbd5792..6ca427f2476b5f 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -1,4 +1,5 @@ """Support for Decora dimmers.""" +import copy from functools import wraps import logging import time @@ -15,17 +16,34 @@ ) from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv +import homeassistant.util as util _LOGGER = logging.getLogger(__name__) SUPPORT_DECORA_LED = SUPPORT_BRIGHTNESS + +def _name_validator(config): + """Validate the name.""" + config = copy.deepcopy(config) + for address, device_config in config[CONF_DEVICES].items(): + if CONF_NAME not in device_config: + device_config[CONF_NAME] = util.slugify(address) + + return config + + DEVICE_SCHEMA = vol.Schema( {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} +PLATFORM_SCHEMA = vol.Schema( + vol.All( + PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} + ), + _name_validator, + ) ) From 4350467a00dc7016814e19328204ed244e0b84bc Mon Sep 17 00:00:00 2001 From: ZiroNL Date: Wed, 30 Oct 2019 08:36:53 +0100 Subject: [PATCH 1331/3953] Add services.yaml to local_file component. (#28330) --- homeassistant/components/local_file/services.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/local_file/services.yaml b/homeassistant/components/local_file/services.yaml index e69de29bb2d1d6..b359b411b6a8ab 100644 --- a/homeassistant/components/local_file/services.yaml +++ b/homeassistant/components/local_file/services.yaml @@ -0,0 +1,9 @@ +local_file_update_file_path: + description: Use this service to change the file displayed by the camera. + fields: + entity_id: + description: Name of the entity_id of the camera to update. + example: 'camera.local_file' + file_path: + description: The full path to the new image file to be displayed. + example: '/config/www/images/image.jpg' From bda3aadbcf9694fa7b25bf9120de92e1d6b25a26 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 30 Oct 2019 09:05:13 -0600 Subject: [PATCH 1332/3953] Bump pymyq to 2.0.1 (#28348) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 73265b61c83bf7..115c9cf515bd00 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -3,7 +3,7 @@ "name": "Myq", "documentation": "https://www.home-assistant.io/integrations/myq", "requirements": [ - "pymyq==2.0.0" + "pymyq==2.0.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 51f7d0816c7a5e..ea7fdfdcdb5bda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1331,7 +1331,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.0 +pymyq==2.0.1 # homeassistant.components.mysensors pymysensors==0.18.0 From 8aee92347f12fcb677fbce734ef41b49e3bb13ad Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 30 Oct 2019 07:57:40 +0100 Subject: [PATCH 1333/3953] Fix KeyError in decora setup (#28279) * Imported homeassistant.util and slugified address if no name is specified * Added a custom validator function in case name is not set in config * Removed logger.debug line only used for testing --- homeassistant/components/decora/light.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 4d2d10ccbd5792..6ca427f2476b5f 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -1,4 +1,5 @@ """Support for Decora dimmers.""" +import copy from functools import wraps import logging import time @@ -15,17 +16,34 @@ ) from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv +import homeassistant.util as util _LOGGER = logging.getLogger(__name__) SUPPORT_DECORA_LED = SUPPORT_BRIGHTNESS + +def _name_validator(config): + """Validate the name.""" + config = copy.deepcopy(config) + for address, device_config in config[CONF_DEVICES].items(): + if CONF_NAME not in device_config: + device_config[CONF_NAME] = util.slugify(address) + + return config + + DEVICE_SCHEMA = vol.Schema( {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} +PLATFORM_SCHEMA = vol.Schema( + vol.All( + PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} + ), + _name_validator, + ) ) From 8ae43d2de34412d88a0c6d3f240caa6cf06597f4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Oct 2019 20:49:01 +0100 Subject: [PATCH 1334/3953] Add device triggers to cover (#28063) * Add device triggers to cover * Use numeric_state trigger instead of template trigger * Tweak translations --- .../components/automation/numeric_state.py | 9 +- .../components/cover/device_trigger.py | 210 ++++++ homeassistant/components/cover/strings.json | 28 +- tests/components/cover/test_device_trigger.py | 702 ++++++++++++++++++ 4 files changed, 935 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/cover/device_trigger.py create mode 100644 tests/components/cover/test_device_trigger.py diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 8d88fe9cae6823..0c8ab3d9c8b526 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -4,7 +4,7 @@ import voluptuous as vol from homeassistant import exceptions -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.const import ( CONF_VALUE_TEMPLATE, CONF_PLATFORM, @@ -17,7 +17,8 @@ from homeassistant.helpers import condition, config_validation as cv, template -# mypy: allow-untyped-defs, no-check-untyped-defs +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs TRIGGER_SCHEMA = vol.All( vol.Schema( @@ -42,7 +43,7 @@ async def async_attach_trigger( hass, config, action, automation_info, *, platform_type="numeric_state" -): +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) @@ -52,7 +53,7 @@ async def async_attach_trigger( value_template = config.get(CONF_VALUE_TEMPLATE) unsub_track_same = {} entities_triggered = set() - period = {} + period: dict = {} if value_template is not None: value_template.hass = hass diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py new file mode 100644 index 00000000000000..4f256a87dc5c6c --- /dev/null +++ b/homeassistant/components/cover/device_trigger.py @@ -0,0 +1,210 @@ +"""Provides device automations for Cover.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, + CONF_ABOVE, + CONF_BELOW, + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import ( + state as state_automation, + numeric_state as numeric_state_automation, + AutomationActionType, +) +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import ( + DOMAIN, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, +) + +POSITION_TRIGGER_TYPES = {"position", "tilt_position"} +STATE_TRIGGER_TYPES = {"opened", "closed", "opening", "closing"} + +POSITION_TRIGGER_SCHEMA = vol.All( + TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(POSITION_TRIGGER_TYPES), + vol.Optional(CONF_ABOVE): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(CONF_BELOW): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + +STATE_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(STATE_TRIGGER_TYPES), + } +) + +TRIGGER_SCHEMA = vol.Any(POSITION_TRIGGER_SCHEMA, STATE_TRIGGER_SCHEMA) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Cover devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + if not state or ATTR_SUPPORTED_FEATURES not in state.attributes: + continue + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + supports_open_close = supported_features & (SUPPORT_OPEN | SUPPORT_CLOSE) + + # Add triggers for each entity that belongs to this integration + if supports_open_close: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "opened", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "closed", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "opening", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "closing", + } + ) + if supported_features & SUPPORT_SET_POSITION: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "position", + } + ) + if supported_features & SUPPORT_SET_TILT_POSITION: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "tilt_position", + } + ) + + return triggers + + +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List trigger capabilities.""" + if config[CONF_TYPE] not in ["position", "tilt_position"]: + return {} + + return { + "extra_fields": vol.Schema( + { + vol.Optional(CONF_ABOVE, default=0): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(CONF_BELOW, default=100): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + } + ) + } + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] in STATE_TRIGGER_TYPES: + if config[CONF_TYPE] == "opened": + to_state = STATE_OPEN + elif config[CONF_TYPE] == "closed": + to_state = STATE_CLOSED + elif config[CONF_TYPE] == "opening": + to_state = STATE_OPENING + elif config[CONF_TYPE] == "closing": + to_state = STATE_CLOSING + + state_config = { + state_automation.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_automation.CONF_TO: to_state, + } + state_config = state_automation.TRIGGER_SCHEMA(state_config) + return await state_automation.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + if config[CONF_TYPE] == "position": + position = "current_position" + if config[CONF_TYPE] == "tilt_position": + position = "current_tilt_position" + min_pos = config.get(CONF_ABOVE, -1) + max_pos = config.get(CONF_BELOW, 101) + value_template = f"{{{{ state.attributes.{position} }}}}" + + numeric_state_config = { + numeric_state_automation.CONF_PLATFORM: "numeric_state", + numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + numeric_state_automation.CONF_BELOW: max_pos, + numeric_state_automation.CONF_ABOVE: min_pos, + numeric_state_automation.CONF_VALUE_TEMPLATE: value_template, + } + numeric_state_config = numeric_state_automation.TRIGGER_SCHEMA(numeric_state_config) + return await numeric_state_automation.async_attach_trigger( + hass, numeric_state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json index e4c72746ee42a4..36492cc5ed5515 100644 --- a/homeassistant/components/cover/strings.json +++ b/homeassistant/components/cover/strings.json @@ -1,12 +1,20 @@ { - "device_automation": { - "condition_type": { - "is_open": "{entity_name} is open", - "is_closed": "{entity_name} is closed", - "is_opening": "{entity_name} is opening", - "is_closing": "{entity_name} is closing", - "is_position": "{entity_name} position is", - "is_tilt_position": "{entity_name} tilt position is" - } + "device_automation": { + "condition_type": { + "is_open": "{entity_name} is open", + "is_closed": "{entity_name} is closed", + "is_opening": "{entity_name} is opening", + "is_closing": "{entity_name} is closing", + "is_position": "Current {entity_name} position is", + "is_tilt_position": "Current {entity_name} tilt position is" + }, + "trigger_type": { + "opened": "{entity_name} opened", + "closed": "{entity_name} closed", + "opening": "{entity_name} opening", + "closing": "{entity_name} closing", + "position": "{entity_name} position changes", + "tilt_position": "{entity_name} tilt position changes" } -} \ No newline at end of file + } +} diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py new file mode 100644 index 00000000000000..4f50c0639c03fa --- /dev/null +++ b/tests/components/cover/test_device_trigger.py @@ -0,0 +1,702 @@ +"""The tests for Cover device triggers.""" +import pytest + +from homeassistant.components.cover import DOMAIN +from homeassistant.const import ( + CONF_PLATFORM, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, + async_get_device_automation_capabilities, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[0] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "opened", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_get_triggers_set_pos(hass, device_reg, entity_reg): + """Test we get the expected triggers from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "opened", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "position", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_get_triggers_set_tilt_pos(hass, device_reg, entity_reg): + """Test we get the expected triggers from a cover.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[2] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "opened", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "tilt_position", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_{ent.unique_id}", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[0] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 4 + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == {"extra_fields": []} + + +async def test_get_trigger_capabilities_set_pos(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "name": "above", + "optional": True, + "type": "integer", + "default": 0, + "valueMax": 100, + "valueMin": 0, + }, + { + "name": "below", + "optional": True, + "type": "integer", + "default": 100, + "valueMax": 100, + "valueMin": 0, + }, + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 5 + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + if trigger["type"] == "position": + assert capabilities == expected_capabilities + else: + assert capabilities == {"extra_fields": []} + + +async def test_get_trigger_capabilities_set_tilt_pos(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a cover trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[2] + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, "test", ent.unique_id, device_id=device_entry.id + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "name": "above", + "optional": True, + "type": "integer", + "default": 0, + "valueMax": 100, + "valueMin": 0, + }, + { + "name": "below", + "optional": True, + "type": "integer", + "default": 100, + "valueMax": 100, + "valueMin": 0, + }, + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 5 + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + if trigger["type"] == "tilt_position": + assert capabilities == expected_capabilities + else: + assert capabilities == {"extra_fields": []} + + +async def test_if_fires_on_state_change(hass, calls): + """Test for state triggers firing.""" + hass.states.async_set("cover.entity", STATE_CLOSED) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "opened", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "opened - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "closed", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "closed - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "opening", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "opening - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "closing", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "closing - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is opened. + hass.states.async_set("cover.entity", STATE_OPEN) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "opened - device - {} - closed - open - None".format("cover.entity") + + # Fake that the entity is closed. + hass.states.async_set("cover.entity", STATE_CLOSED) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data[ + "some" + ] == "closed - device - {} - open - closed - None".format("cover.entity") + + # Fake that the entity is opening. + hass.states.async_set("cover.entity", STATE_OPENING) + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data[ + "some" + ] == "opening - device - {} - closed - opening - None".format("cover.entity") + + # Fake that the entity is closing. + hass.states.async_set("cover.entity", STATE_CLOSING) + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data[ + "some" + ] == "closing - device - {} - opening - closing - None".format("cover.entity") + + +async def test_if_fires_on_position(hass, calls): + """Test for position triggers.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "position", + "above": 45, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_gt_45 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "position", + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_lt_90 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "position", + "above": 45, + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_gt_45_lt_90 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 50} + ) + await hass.async_block_till_done() + assert len(calls) == 3 + assert sorted( + [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] + ) == sorted( + [ + "is_pos_gt_45_lt_90 - device - cover.set_position_cover - open - closed - None", + "is_pos_lt_90 - device - cover.set_position_cover - open - closed - None", + "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", + ] + ) + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 95} + ) + await hass.async_block_till_done() + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 45} + ) + await hass.async_block_till_done() + assert len(calls) == 4 + assert ( + calls[3].data["some"] + == "is_pos_lt_90 - device - cover.set_position_cover - closed - closed - None" + ) + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_position": 90} + ) + await hass.async_block_till_done() + assert len(calls) == 5 + assert ( + calls[4].data["some"] + == "is_pos_gt_45 - device - cover.set_position_cover - closed - closed - None" + ) + + +async def test_if_fires_on_tilt_position(hass, calls): + """Test for tilt position triggers.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + ent = platform.ENTITIES[1] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "tilt_position", + "above": 45, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_gt_45 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "tilt_position", + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_lt_90 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": [ + { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent.entity_id, + "type": "tilt_position", + "above": 45, + "below": 90, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "is_pos_gt_45_lt_90 - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 50} + ) + await hass.async_block_till_done() + assert len(calls) == 3 + assert sorted( + [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] + ) == sorted( + [ + "is_pos_gt_45_lt_90 - device - cover.set_position_cover - open - closed - None", + "is_pos_lt_90 - device - cover.set_position_cover - open - closed - None", + "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", + ] + ) + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 95} + ) + await hass.async_block_till_done() + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 45} + ) + await hass.async_block_till_done() + assert len(calls) == 4 + assert ( + calls[3].data["some"] + == "is_pos_lt_90 - device - cover.set_position_cover - closed - closed - None" + ) + + hass.states.async_set( + ent.entity_id, STATE_CLOSED, attributes={"current_tilt_position": 90} + ) + await hass.async_block_till_done() + assert len(calls) == 5 + assert ( + calls[4].data["some"] + == "is_pos_gt_45 - device - cover.set_position_cover - closed - closed - None" + ) From 7eceedea108399a738b81eb57ddf9304630a6296 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 30 Oct 2019 19:50:48 +0000 Subject: [PATCH 1335/3953] Bump version 0.101.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7367d9723f6e5a..f6f1a4f2de2718 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0b4" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From ee24710524e0d4b603891f0e187e0fa2fff5b76c Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 31 Oct 2019 00:32:14 +0000 Subject: [PATCH 1336/3953] [ci skip] Translation update --- homeassistant/components/cover/.translations/en.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/.translations/en.json b/homeassistant/components/cover/.translations/en.json index eaac0d557ef8aa..27710f79436278 100644 --- a/homeassistant/components/cover/.translations/en.json +++ b/homeassistant/components/cover/.translations/en.json @@ -5,8 +5,16 @@ "is_closing": "{entity_name} is closing", "is_open": "{entity_name} is open", "is_opening": "{entity_name} is opening", - "is_position": "{entity_name} position is", - "is_tilt_position": "{entity_name} tilt position is" + "is_position": "Current {entity_name} position is", + "is_tilt_position": "Current {entity_name} tilt position is" + }, + "trigger_type": { + "closed": "{entity_name} closed", + "closing": "{entity_name} closing", + "opened": "{entity_name} opened", + "opening": "{entity_name} opening", + "position": "{entity_name} position changes", + "tilt_position": "{entity_name} tilt position changes" } } } \ No newline at end of file From d3750401c14920e0444050096e3012e436985094 Mon Sep 17 00:00:00 2001 From: Steve M Date: Thu, 31 Oct 2019 04:38:53 -0400 Subject: [PATCH 1337/3953] Bump env_canada to fixed 0.0.29 version (#28360) * Bump env_canada to fixed 0.0.29 version * bump env_canada to 0.29 --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index c62e1e356b6031..7661269073c66c 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/integrations/environment_canada", "requirements": [ - "env_canada==0.0.25" + "env_canada==0.0.29" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ea7fdfdcdb5bda..0966533dc0b9fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -459,7 +459,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.25 +env_canada==0.0.29 # homeassistant.components.envirophat # envirophat==0.0.6 From ef92c5672d020cf4ca03c66fca3ae7d1d7f80a39 Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Thu, 31 Oct 2019 09:39:27 +0100 Subject: [PATCH 1338/3953] Bump pysaj to v0.0.13 (fix for sensor date) (#28351) --- homeassistant/components/saj/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index 2dd701e9c7c65c..4d02ab74840d6e 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,7 +3,7 @@ "name": "SAJ", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": [ - "pysaj==0.0.12" + "pysaj==0.0.13" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0966533dc0b9fe..01c4f879c7b79f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1426,7 +1426,7 @@ pyrepetier==3.0.5 pysabnzbd==1.1.0 # homeassistant.components.saj -pysaj==0.0.12 +pysaj==0.0.13 # homeassistant.components.sony_projector pysdcp==1 From 89df82111314a666811dd24fda704fefc7e27d45 Mon Sep 17 00:00:00 2001 From: Santobert Date: Thu, 31 Oct 2019 09:41:44 +0100 Subject: [PATCH 1339/3953] Flux log with debug instead of info (#28352) --- homeassistant/components/flux/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 7b58ffbe449e9e..404067d41074eb 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -310,7 +310,7 @@ async def async_flux_update(self, utcnow=None): await async_set_lights_xy( self.hass, self._lights, x_val, y_val, brightness, self._transition ) - _LOGGER.info( + _LOGGER.debug( "Lights updated to x:%s y:%s brightness:%s, %s%% " "of %s cycle complete at %s", x_val, @@ -322,7 +322,7 @@ async def async_flux_update(self, utcnow=None): ) elif self._mode == MODE_RGB: await async_set_lights_rgb(self.hass, self._lights, rgb, self._transition) - _LOGGER.info( + _LOGGER.debug( "Lights updated to rgb:%s, %s%% " "of %s cycle complete at %s", rgb, round(percentage_complete * 100), @@ -335,7 +335,7 @@ async def async_flux_update(self, utcnow=None): await async_set_lights_temp( self.hass, self._lights, mired, brightness, self._transition ) - _LOGGER.info( + _LOGGER.debug( "Lights updated to mired:%s brightness:%s, %s%% " "of %s cycle complete at %s", mired, From e11c9d710c917d7c2b71e85024a690a91c40ec94 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Thu, 31 Oct 2019 04:49:38 -0400 Subject: [PATCH 1340/3953] Add modelnumber for ecobee4 (#28278) --- homeassistant/components/ecobee/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 5022cb71903a1e..05c2d22b5940e8 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -20,6 +20,7 @@ "nikeSmart": "ecobee3 lite Smart", "nikeEms": "ecobee3 lite EMS", "apolloSmart": "ecobee4 Smart", + "vulcanSmart": "ecobee4 Smart", } ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] From bfe4a85e9d5255fb8dd64b97ae7222f980c83fe0 Mon Sep 17 00:00:00 2001 From: gngj Date: Thu, 31 Oct 2019 10:51:15 +0200 Subject: [PATCH 1341/3953] Fill services.yaml for duckdns (#28248) * Fill services.yaml for duckdns * Apply suggestions from code review Co-Authored-By: Fabian Affolter --- homeassistant/components/duckdns/services.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/duckdns/services.yaml b/homeassistant/components/duckdns/services.yaml index e69de29bb2d1d6..8c353a0b3cd2db 100644 --- a/homeassistant/components/duckdns/services.yaml +++ b/homeassistant/components/duckdns/services.yaml @@ -0,0 +1,6 @@ +set_txt: + description: Set the TXT record of your DuckDNS subdomain. + fields: + txt: + description: Payload for the TXT record. + example: 'This domain name is reserved for use in documentation' From ff5b070f4b08abe64411ebb5f4d1713a0f5f59cd Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Thu, 31 Oct 2019 05:38:44 -0400 Subject: [PATCH 1342/3953] Implement Alexa.SeekController Interface for media_player in Alexa (#28299) * Implement Alexa.SeekController Interface for Alexa * Added error handling and duration checks. * Split out media_player SeekController tests and added error test. --- .../components/alexa/capabilities.py | 11 ++ homeassistant/components/alexa/entities.py | 4 + homeassistant/components/alexa/errors.py | 7 + homeassistant/components/alexa/handlers.py | 43 ++++++ tests/components/alexa/test_smart_home.py | 132 ++++++++++++++++-- 5 files changed, 189 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index b5b7fa88ef859c..7d74bb3f8cd458 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1175,3 +1175,14 @@ def get_property(self, name): return {"state": "PAUSED"} return {"state": "STOPPED"} + + +class AlexaSeekController(AlexaCapability): + """Implements Alexa.SeekController. + + https://developer.amazon.com/docs/device-apis/alexa-seekcontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.SeekController" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index ef6c29020530b5..6d2f9aef56aa59 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -52,6 +52,7 @@ AlexaRangeController, AlexaSceneController, AlexaSecurityPanelController, + AlexaSeekController, AlexaSpeaker, AlexaStepSpeaker, AlexaTemperatureSensor, @@ -425,6 +426,9 @@ def interfaces(self): yield AlexaPlaybackController(self.entity) yield AlexaPlaybackStateReporter(self.entity) + if supported & media_player.const.SUPPORT_SEEK: + yield AlexaSeekController(self.entity) + if supported & media_player.SUPPORT_SELECT_SOURCE: yield AlexaInputController(self.entity) diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index b0600313fc2286..29643bacc53e58 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -111,3 +111,10 @@ class AlexaInvalidDirectiveError(AlexaError): namespace = "Alexa" error_type = "INVALID_DIRECTIVE" + + +class AlexaVideoActionNotPermittedForContentError(AlexaError): + """Class to represent action not permitted for content errors.""" + + namespace = "Alexa.Video" + error_type = "ACTION_NOT_PERMITTED_FOR_CONTENT" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 3dadf51509a807..c23e01f501fd91 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -54,6 +54,7 @@ AlexaSecurityPanelUnauthorizedError, AlexaTempRangeError, AlexaUnsupportedThermostatModeError, + AlexaVideoActionNotPermittedForContentError, ) from .state_report import async_enable_proactive_mode @@ -1186,3 +1187,45 @@ async def async_api_skipchannel(hass, config, directive, context): ) return response + + +@HANDLERS.register(("Alexa.SeekController", "AdjustSeekPosition")) +async def async_api_seek(hass, config, directive, context): + """Process a seek request.""" + entity = directive.entity + position_delta = int(directive.payload["deltaPositionMilliseconds"]) + + current_position = entity.attributes.get(media_player.ATTR_MEDIA_POSITION) + if not current_position: + msg = f"{entity} did not return the current media position." + raise AlexaVideoActionNotPermittedForContentError(msg) + + seek_position = int(current_position) + int(position_delta / 1000) + + if seek_position < 0: + seek_position = 0 + + media_duration = entity.attributes.get(media_player.ATTR_MEDIA_DURATION) + if media_duration and 0 < int(media_duration) < seek_position: + seek_position = media_duration + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.ATTR_MEDIA_SEEK_POSITION: seek_position, + } + + await hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_MEDIA_SEEK, + data, + blocking=False, + context=context, + ) + + # convert seconds to milliseconds for StateReport. + seek_position = int(seek_position * 1000) + + payload = {"properties": [{"name": "positionMilliseconds", "value": seek_position}]} + return directive.response( + name="StateReport", namespace="Alexa.SeekController", payload=payload + ) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3994c3d9f5d58c..9b901288f26a7d 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -10,6 +10,7 @@ SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -728,14 +729,14 @@ async def test_media_player(hass): capabilities = assert_endpoint_capabilities( appliance, + "Alexa.ChannelController", + "Alexa.EndpointHealth", "Alexa.InputController", + "Alexa.PlaybackController", + "Alexa.PlaybackStateReporter", "Alexa.PowerController", "Alexa.Speaker", "Alexa.StepSpeaker", - "Alexa.PlaybackController", - "Alexa.PlaybackStateReporter", - "Alexa.EndpointHealth", - "Alexa.ChannelController", ) playback_capability = get_capability(capabilities, "Alexa.PlaybackController") @@ -950,14 +951,15 @@ async def test_media_player_power(hass): assert_endpoint_capabilities( appliance, + "Alexa.ChannelController", + "Alexa.EndpointHealth", "Alexa.InputController", + "Alexa.PlaybackController", + "Alexa.PlaybackStateReporter", "Alexa.PowerController", + "Alexa.SeekController", "Alexa.Speaker", "Alexa.StepSpeaker", - "Alexa.PlaybackController", - "Alexa.PlaybackStateReporter", - "Alexa.EndpointHealth", - "Alexa.ChannelController", ) await assert_request_calls_service( @@ -996,6 +998,120 @@ async def test_media_player_speaker(hass): assert appliance["friendlyName"] == "Test media player" +async def test_media_player_seek(hass): + """Test media player seek capability.""" + device = ( + "media_player.test_seek", + "playing", + { + "friendly_name": "Test media player seek", + "supported_features": SUPPORT_SEEK, + "media_position": 300, # 5min + "media_duration": 600, # 10min + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test_seek" + assert appliance["displayCategories"][0] == "TV" + assert appliance["friendlyName"] == "Test media player seek" + + assert_endpoint_capabilities( + appliance, + "Alexa.EndpointHealth", + "Alexa.PowerController", + "Alexa.SeekController", + ) + + # Test seek forward 30 seconds. + call, msg = await assert_request_calls_service( + "Alexa.SeekController", + "AdjustSeekPosition", + "media_player#test_seek", + "media_player.media_seek", + hass, + response_type="StateReport", + payload={"deltaPositionMilliseconds": 30000}, + ) + assert call.data["seek_position"] == 330 + assert "properties" in msg["event"]["payload"] + properties = msg["event"]["payload"]["properties"] + assert {"name": "positionMilliseconds", "value": 330000} in properties + + # Test seek reverse 30 seconds. + call, msg = await assert_request_calls_service( + "Alexa.SeekController", + "AdjustSeekPosition", + "media_player#test_seek", + "media_player.media_seek", + hass, + response_type="StateReport", + payload={"deltaPositionMilliseconds": -30000}, + ) + assert call.data["seek_position"] == 270 + assert "properties" in msg["event"]["payload"] + properties = msg["event"]["payload"]["properties"] + assert {"name": "positionMilliseconds", "value": 270000} in properties + + # Test seek backwards more than current position (5 min.) result = 0. + call, msg = await assert_request_calls_service( + "Alexa.SeekController", + "AdjustSeekPosition", + "media_player#test_seek", + "media_player.media_seek", + hass, + response_type="StateReport", + payload={"deltaPositionMilliseconds": -500000}, + ) + assert call.data["seek_position"] == 0 + assert "properties" in msg["event"]["payload"] + properties = msg["event"]["payload"]["properties"] + assert {"name": "positionMilliseconds", "value": 0} in properties + + # Test seek forward more than current duration (10 min.) result = 600 sec. + call, msg = await assert_request_calls_service( + "Alexa.SeekController", + "AdjustSeekPosition", + "media_player#test_seek", + "media_player.media_seek", + hass, + response_type="StateReport", + payload={"deltaPositionMilliseconds": 800000}, + ) + assert call.data["seek_position"] == 600 + assert "properties" in msg["event"]["payload"] + properties = msg["event"]["payload"]["properties"] + assert {"name": "positionMilliseconds", "value": 600000} in properties + + +async def test_media_player_seek_error(hass): + """Test media player seek capability for media_position Error.""" + device = ( + "media_player.test_seek", + "playing", + {"friendly_name": "Test media player seek", "supported_features": SUPPORT_SEEK}, + ) + await discovery_test(device, hass) + + # Test for media_position error. + with pytest.raises(AssertionError): + call, msg = await assert_request_calls_service( + "Alexa.SeekController", + "AdjustSeekPosition", + "media_player#test_seek", + "media_player.media_seek", + hass, + response_type="StateReport", + payload={"deltaPositionMilliseconds": 30000}, + ) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa.Video" + assert msg["payload"]["type"] == "ACTION_NOT_PERMITTED_FOR_CONTENT" + + async def test_alert(hass): """Test alert discovery.""" device = ("alert.test", "off", {"friendly_name": "Test alert"}) From d1335017357ee5109d8b851fb27fd276f71e140b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 31 Oct 2019 17:29:27 +0100 Subject: [PATCH 1343/3953] Fix Airly asyncio timeout error (#28387) * Raise ConfigEntryNotReady * Better asyncio.TimeoutError handling * Sort imports * Increase asyncio timeout --- homeassistant/components/airly/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index dc2323ddd4e3fb..80f3518c652832 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -1,15 +1,16 @@ """The Airly component.""" import asyncio -import logging from datetime import timedelta +import logging -import async_timeout from aiohttp.client_exceptions import ClientConnectorError from airly import Airly from airly.exceptions import AirlyError +import async_timeout from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle @@ -45,6 +46,9 @@ async def async_setup_entry(hass, config_entry): await airly.async_update() + if not airly.data: + raise ConfigEntryNotReady() + hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_CLIENT] = {} hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly @@ -81,7 +85,7 @@ async def async_update(self): """Update Airly data.""" try: - with async_timeout.timeout(10): + with async_timeout.timeout(20): measurements = self.airly.create_measurements_session_point( self.latitude, self.longitude ) @@ -104,11 +108,8 @@ async def async_update(self): self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"] self.data[ATTR_API_ADVICE] = index["advice"] _LOGGER.debug("Data retrieved from Airly") - except ( - ValueError, - AirlyError, - asyncio.TimeoutError, - ClientConnectorError, - ) as error: + except asyncio.TimeoutError: + _LOGGER.error("Asyncio Timeout Error") + except (ValueError, AirlyError, ClientConnectorError) as error: _LOGGER.error(error) self.data = {} From 89213a4ce83a7f32fd4ee4cd6837b1d0a11c1bdf Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 31 Oct 2019 12:31:06 -0400 Subject: [PATCH 1344/3953] Don't set entity_id in ZHA entities (#28362) * Don't set entity_id on ZHA entities. * Update tests. * Use comma as separator for multiple channel names. * Address PR comments. --- homeassistant/components/zha/entity.py | 18 +++-------- tests/components/zha/common.py | 35 +++++++++------------ tests/components/zha/test_binary_sensor.py | 13 ++++---- tests/components/zha/test_device_tracker.py | 7 +++-- tests/components/zha/test_fan.py | 7 +++-- tests/components/zha/test_light.py | 15 +++++---- tests/components/zha/test_lock.py | 5 +-- tests/components/zha/test_sensor.py | 14 ++++----- tests/components/zha/test_switch.py | 7 +++-- 9 files changed, 55 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index c11cd405a99402..108d8e27a9f88a 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -9,7 +9,6 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.util import slugify from .core.const import ( ATTR_MANUFACTURER, @@ -38,17 +37,10 @@ def __init__(self, unique_id, zha_device, channels, skip_entity_id=False, **kwar self._force_update = False self._should_poll = False self._unique_id = unique_id - if not skip_entity_id: - ieee = zha_device.ieee - ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) - self.entity_id = "{}.{}_{}_{}_{}{}".format( - self._domain, - slugify(zha_device.manufacturer), - slugify(zha_device.model), - ieeetail, - channels[0].cluster.endpoint.endpoint_id, - kwargs.get(ENTITY_SUFFIX, ""), - ) + ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) + ch_names = [ch.cluster.ep_attribute for ch in channels] + ch_names = ", ".join(sorted(ch_names)) + self._name = f"{zha_device.name} {ieeetail} {ch_names}" self._state = None self._device_state_attributes = {} self._zha_device = zha_device @@ -63,7 +55,7 @@ def __init__(self, unique_id, zha_device, channels, skip_entity_id=False, **kwar @property def name(self): """Return Entity's default name.""" - return self.zha_device.name + return self._name @property def unique_id(self) -> str: diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 788faaaec737f1..583b4e0738b94d 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -161,23 +161,22 @@ async def async_setup_entry(hass, config_entry): return True -def make_entity_id(domain, device, cluster, use_suffix=True): - """Make the entity id for the entity under testing. +async def find_entity_id(domain, zha_device, hass): + """Find the entity id under the testing. This is used to get the entity id in order to get the state from the state machine so that we can test state changes. """ - ieee = device.ieee - ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) - entity_id = "{}.{}_{}_{}_{}{}".format( - domain, - slugify(device.manufacturer), - slugify(device.model), - ieeetail, - cluster.endpoint.endpoint_id, - ("", "_{}".format(cluster.cluster_id))[use_suffix], - ) - return entity_id + ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) + head = f"{domain}." + slugify(f"{zha_device.name} {ieeetail}") + + enitiy_ids = hass.states.async_entity_ids(domain) + await hass.async_block_till_done() + + for entity_id in enitiy_ids: + if entity_id.startswith(head): + return entity_id + return None async def async_enable_traffic(hass, zha_gateway, zha_devices): @@ -188,7 +187,7 @@ async def async_enable_traffic(hass, zha_gateway, zha_devices): async def async_test_device_join( - hass, zha_gateway, cluster_id, domain, device_type=None + hass, zha_gateway, cluster_id, entity_id, device_type=None ): """Test a newly joining device. @@ -205,21 +204,15 @@ async def async_test_device_join( "zigpy.zcl.Cluster.bind", return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): - zigpy_device = await async_init_zigpy_device( + await async_init_zigpy_device( hass, [cluster_id, zigpy.zcl.clusters.general.Basic.cluster_id], [], device_type, zha_gateway, ieee="00:0d:6f:00:0a:90:69:f7", - manufacturer="FakeMan{}".format(cluster_id), - model="FakeMod{}".format(cluster_id), is_new_join=True, ) - cluster = zigpy_device.endpoints.get(1).in_clusters[cluster_id] - entity_id = make_entity_id( - domain, zigpy_device, cluster, use_suffix=device_type is None - ) assert hass.states.get(entity_id) is not None diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 89dc1ae25a64c6..2765a465aced7d 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -11,8 +11,8 @@ async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -27,6 +27,7 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): [], None, zha_gateway, + ieee="00:0d:6f:11:9a:90:69:e6", ) zigpy_device_occupancy = await async_init_zigpy_device( @@ -46,15 +47,15 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): # on off binary_sensor zone_cluster = zigpy_device_zone.endpoints.get(1).ias_zone - zone_entity_id = make_entity_id(DOMAIN, zigpy_device_zone, zone_cluster) zone_zha_device = zha_gateway.get_device(zigpy_device_zone.ieee) + zone_entity_id = await find_entity_id(DOMAIN, zone_zha_device, hass) + assert zone_entity_id is not None # occupancy binary_sensor occupancy_cluster = zigpy_device_occupancy.endpoints.get(1).occupancy - occupancy_entity_id = make_entity_id( - DOMAIN, zigpy_device_occupancy, occupancy_cluster - ) occupancy_zha_device = zha_gateway.get_device(zigpy_device_occupancy.ieee) + occupancy_entity_id = await find_entity_id(DOMAIN, occupancy_zha_device, hass) + assert occupancy_entity_id is not None # test that the sensors exist and are in the unavailable state assert hass.states.get(zone_entity_id).state == STATE_UNAVAILABLE @@ -76,7 +77,7 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): # test new sensor join await async_test_device_join( - hass, zha_gateway, measurement.OccupancySensing.cluster_id, DOMAIN + hass, zha_gateway, measurement.OccupancySensing.cluster_id, occupancy_entity_id ) diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 446920eb2f9c2b..bac338ae5e0e37 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -16,8 +16,8 @@ async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -47,8 +47,9 @@ async def test_device_tracker(hass, config_entry, zha_gateway): await hass.async_block_till_done() cluster = zigpy_device.endpoints.get(1).power - entity_id = make_entity_id(DOMAIN, zigpy_device, cluster, use_suffix=False) zha_device = zha_gateway.get_device(zigpy_device.ieee) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None # test that the device tracker was created and that it is unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE @@ -90,6 +91,6 @@ async def test_device_tracker(hass, config_entry, zha_gateway): hass, zha_gateway, general.PowerConfiguration.cluster_id, - DOMAIN, + entity_id, SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, ) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index a196ba50ba7c06..660bff2abac635 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -20,8 +20,8 @@ async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -41,8 +41,9 @@ async def test_fan(hass, config_entry, zha_gateway): await hass.async_block_till_done() cluster = zigpy_device.endpoints.get(1).fan - entity_id = make_entity_id(DOMAIN, zigpy_device, cluster) zha_device = zha_gateway.get_device(zigpy_device.ieee) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None # test that the fan was created and that it is unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE @@ -97,7 +98,7 @@ async def test_fan(hass, config_entry, zha_gateway): assert cluster.write_attributes.call_args == call({"fan_mode": 3}) # test adding new fan to the network and HA - await async_test_device_join(hass, zha_gateway, hvac.Fan.cluster_id, DOMAIN) + await async_test_device_join(hass, zha_gateway, hvac.Fan.cluster_id, entity_id) async def async_turn_on(hass, entity_id, speed=None): diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index f0d9d4913e69e8..5180f8f976eafd 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -14,8 +14,8 @@ async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -35,6 +35,7 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): [], zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, zha_gateway, + ieee="00:0d:6f:11:0a:90:69:e6", ) zigpy_device_level = await async_init_zigpy_device( @@ -58,10 +59,9 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): # on off light on_off_device_on_off_cluster = zigpy_device_on_off.endpoints.get(1).on_off - on_off_entity_id = make_entity_id( - DOMAIN, zigpy_device_on_off, on_off_device_on_off_cluster, use_suffix=False - ) on_off_zha_device = zha_gateway.get_device(zigpy_device_on_off.ieee) + on_off_entity_id = await find_entity_id(DOMAIN, on_off_zha_device, hass) + assert on_off_entity_id is not None # dimmable light level_device_on_off_cluster = zigpy_device_level.endpoints.get(1).on_off @@ -78,10 +78,9 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): ) monkeypatch.setattr(level_device_on_off_cluster, "request", on_off_mock) monkeypatch.setattr(level_device_level_cluster, "request", level_mock) - level_entity_id = make_entity_id( - DOMAIN, zigpy_device_level, level_device_on_off_cluster, use_suffix=False - ) level_zha_device = zha_gateway.get_device(zigpy_device_level.ieee) + level_entity_id = await find_entity_id(DOMAIN, level_zha_device, hass) + assert level_entity_id is not None # test that the lights were created and that they are unavailable assert hass.states.get(on_off_entity_id).state == STATE_UNAVAILABLE @@ -125,7 +124,7 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): hass, zha_gateway, general.OnOff.cluster_id, - DOMAIN, + on_off_entity_id, device_type=zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, ) diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 118526a1d85917..1daef317fed7a4 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -11,8 +11,8 @@ from .common import ( async_enable_traffic, async_init_zigpy_device, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -39,8 +39,9 @@ async def test_lock(hass, config_entry, zha_gateway): await hass.async_block_till_done() cluster = zigpy_device.endpoints.get(1).door_lock - entity_id = make_entity_id(DOMAIN, zigpy_device, cluster) zha_device = zha_gateway.get_device(zigpy_device.ieee) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None # test that the lock was created and that it is unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index dec551f8d627e5..7746c5d422eb30 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -12,8 +12,8 @@ async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -85,7 +85,7 @@ async def test_sensor(hass, config_entry, zha_gateway): # test joining a new temperature sensor to the network await async_test_device_join( - hass, zha_gateway, measurement.TemperatureMeasurement.cluster_id, DOMAIN + hass, zha_gateway, measurement.TemperatureMeasurement.cluster_id, entity_id ) @@ -110,7 +110,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): [], None, zha_gateway, - ieee="{}0:15:8d:00:02:32:4f:32".format(counter), + ieee="00:15:8d:00:02:32:4f:0{}".format(counter), manufacturer="Fake{}".format(cluster_id), model="FakeModel{}".format(cluster_id), ) @@ -126,10 +126,10 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): device_info = device_infos[cluster_id] zigpy_device = device_info["zigpy_device"] device_info["cluster"] = zigpy_device.endpoints.get(1).in_clusters[cluster_id] - device_info["entity_id"] = make_entity_id( - DOMAIN, zigpy_device, device_info["cluster"] - ) - device_info["zha_device"] = zha_gateway.get_device(zigpy_device.ieee) + zha_device = zha_gateway.get_device(zigpy_device.ieee) + device_info["zha_device"] = zha_device + device_info["entity_id"] = await find_entity_id(DOMAIN, zha_device, hass) + await hass.async_block_till_done() return device_infos diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index bf4ff3ed628c1e..11a0b8f3481831 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -11,8 +11,8 @@ async_enable_traffic, async_init_zigpy_device, async_test_device_join, + find_entity_id, make_attribute, - make_entity_id, make_zcl_header, ) @@ -39,8 +39,9 @@ async def test_switch(hass, config_entry, zha_gateway): await hass.async_block_till_done() cluster = zigpy_device.endpoints.get(1).on_off - entity_id = make_entity_id(DOMAIN, zigpy_device, cluster) zha_device = zha_gateway.get_device(zigpy_device.ieee) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None # test that the switch was created and that its state is unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE @@ -93,4 +94,4 @@ async def test_switch(hass, config_entry, zha_gateway): ) # test joining a new switch to the network and HA - await async_test_device_join(hass, zha_gateway, general.OnOff.cluster_id, DOMAIN) + await async_test_device_join(hass, zha_gateway, general.OnOff.cluster_id, entity_id) From 226b2bc3d0bbdcf5b7de257820e106ce69e2d4f0 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Thu, 31 Oct 2019 10:29:10 -0700 Subject: [PATCH 1345/3953] Update withings-api to avoid data parsing bugs. (#28382) --- homeassistant/components/withings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index ea9845f3e4245c..1f2169363e9d9f 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "withings-api==2.1.2" + "withings-api==2.1.3" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 01c4f879c7b79f..deace09aa1ece1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1990,7 +1990,7 @@ websockets==6.0 wirelesstagpy==0.4.0 # homeassistant.components.withings -withings-api==2.1.2 +withings-api==2.1.3 # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8166b9713f4f70..1e46132c30245c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -636,7 +636,7 @@ watchdog==0.8.3 websockets==6.0 # homeassistant.components.withings -withings-api==2.1.2 +withings-api==2.1.3 # homeassistant.components.bluesound # homeassistant.components.startca From 5854eef47b2e0ebfd077fcef2cb3c728881b8c63 Mon Sep 17 00:00:00 2001 From: ZiroNL Date: Thu, 31 Oct 2019 18:57:00 +0100 Subject: [PATCH 1346/3953] Add services.yaml to onvif component (#28349) --- homeassistant/components/onvif/services.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml index e69de29bb2d1d6..667538f056a7fb 100644 --- a/homeassistant/components/onvif/services.yaml +++ b/homeassistant/components/onvif/services.yaml @@ -0,0 +1,16 @@ +onvif_ptz: + description: If your ONVIF camera supports PTZ, you will be able to pan, tilt or zoom your camera. + fields: + entity_id: + description: 'String or list of strings that point at entity_ids of cameras. Else targets all.' + example: 'camera.backyard' + tilt: + description: 'Tilt direction. Allowed values: UP, DOWN, NONE' + example: 'UP' + pan: + description: 'Pan direction. Allowed values: RIGHT, LEFT, NONE' + example: 'RIGHT' + zoom: + description: 'Zoom. Allowed values: ZOOM_IN, ZOOM_OUT, NONE' + example: 'NONE' + From 674860e00e18234184bcb7e5d8b0492b1dfe705d Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 31 Oct 2019 20:16:27 +0200 Subject: [PATCH 1347/3953] Fix hdate spamming homeassistant log (#28392) * Fix hdate spamming homeassistant log * Lower verbosity of another spammy message --- 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 08182daedd05a0..b343963153d705 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/integrations/jewish_calendar", "requirements": [ - "hdate==0.9.1" + "hdate==0.9.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index deace09aa1ece1..daee288d06cd5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -625,7 +625,7 @@ hass-nabucasa==0.23 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.1 +hdate==0.9.3 # homeassistant.components.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e46132c30245c..e757272a7892bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,7 +231,7 @@ hass-nabucasa==0.23 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.1 +hdate==0.9.3 # homeassistant.components.here_travel_time herepy==0.6.3.1 From 70c4b4a4f05ece724e449a15caf10a39ae5de5b3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Oct 2019 11:38:06 -0700 Subject: [PATCH 1348/3953] Check for import errors before validating config (#28395) --- homeassistant/setup.py | 17 +++++++++++------ tests/test_setup.py | 8 ++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 07de3b2942d061..314938feeed42c 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -132,6 +132,17 @@ def log_error(msg: str, link: bool = True) -> None: log_error(str(err)) return False + # Some integrations fail on import because they call functions incorrectly. + # So we do it before validating config to catch these errors. + try: + component = integration.get_component() + except ImportError: + log_error("Unable to import component", False) + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Setup failed for %s: unknown error", domain) + return False + processed_config = await conf_util.async_process_component_config( hass, config, integration ) @@ -143,12 +154,6 @@ def log_error(msg: str, link: bool = True) -> None: start = timer() _LOGGER.info("Setting up %s", domain) - try: - component = integration.get_component() - except ImportError: - log_error("Unable to import component", False) - return False - if hasattr(component, "PLATFORM_SCHEMA"): # Entity components have their own warning warn_task = None diff --git a/tests/test_setup.py b/tests/test_setup.py index 9612c1784ef00a..8fd25091eb6801 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -527,3 +527,11 @@ async def mock_callback(hass, component): setup.async_when_setup(hass, "test", mock_callback) await hass.async_block_till_done() assert calls == ["test", "test"] + + +async def test_setup_import_blows_up(hass): + """Test that we handle it correctly when importing integration blows up.""" + with mock.patch( + "homeassistant.loader.Integration.get_component", side_effect=ValueError + ): + assert not await setup.async_setup_component(hass, "sun", {}) From 631a819bd13a5177f03d4c1ae05548961a9968c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Oct 2019 11:39:26 -0700 Subject: [PATCH 1349/3953] Fix check config (#28393) --- homeassistant/requirements.py | 12 ++++++++++-- tests/test_requirements.py | 21 ++++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 95738084f1f2c6..74469ef2fcd619 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -35,13 +35,21 @@ async def async_get_integration_with_requirements( This can raise IntegrationNotFound if manifest or integration is invalid, RequirementNotFound if there was some type of failure to install requirements. + + Does not handle circular dependencies. """ integration = await async_get_integration(hass, domain) - if hass.config.skip_pip or not integration.requirements: + if hass.config.skip_pip: return integration - await async_process_requirements(hass, integration.domain, integration.requirements) + if integration.requirements: + await async_process_requirements( + hass, integration.domain, integration.requirements + ) + + for dependency in integration.dependencies: + await async_get_integration_with_requirements(hass, dependency) return integration diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 780b175778edb8..548ea645360d94 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -112,7 +112,17 @@ async def test_install_missing_package(hass): 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"])) + mock_integration( + hass, MockModule("test_component_dep", requirements=["test-comp-dep==1.0.0"]) + ) + mock_integration( + hass, + MockModule( + "test_component", + requirements=["test-comp==1.0.0"], + dependencies=["test_component_dep"], + ), + ) with patch( "homeassistant.util.package.is_installed", return_value=False @@ -126,8 +136,13 @@ async def test_get_integration_with_requirements(hass): assert integration assert integration.domain == "test_component" - assert len(mock_is_installed.mock_calls) == 1 - assert len(mock_inst.mock_calls) == 1 + assert len(mock_is_installed.mock_calls) == 2 + assert mock_is_installed.mock_calls[0][1][0] == "test-comp==1.0.0" + assert mock_is_installed.mock_calls[1][1][0] == "test-comp-dep==1.0.0" + + assert len(mock_inst.mock_calls) == 2 + assert mock_inst.mock_calls[0][1][0] == "test-comp==1.0.0" + assert mock_inst.mock_calls[1][1][0] == "test-comp-dep==1.0.0" async def test_install_with_wheels_index(hass): From abbf6595bb6a7d34e2bf50e6e6cb369a2901a9f8 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Thu, 31 Oct 2019 22:07:07 +0300 Subject: [PATCH 1350/3953] Fix (#28369) --- homeassistant/components/telegram_bot/manifest.json | 3 ++- requirements_all.txt | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index afc83879cb0c1e..7fb648e5cb55c5 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -3,7 +3,8 @@ "name": "Telegram bot", "documentation": "https://www.home-assistant.io/integrations/telegram_bot", "requirements": [ - "python-telegram-bot==11.1.0" + "python-telegram-bot==11.1.0", + "PySocks==1.7.1" ], "dependencies": ["http"], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index daee288d06cd5e..135376d5eeedb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -68,6 +68,9 @@ PyQRCode==1.2.1 # homeassistant.components.rmvtransport PyRMVtransport==0.2.9 +# homeassistant.components.telegram_bot +PySocks==1.7.1 + # homeassistant.components.switchbot # PySwitchbot==0.6.2 From 54481598b7f7572a8db3819dd0b6696b659db23a Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 23 Oct 2019 23:13:56 +0200 Subject: [PATCH 1351/3953] Bump songpal to fix a regression (#28115) The new release fixes a single regression from requests to aiohttp conversion. Some devices do not respond with the correct mimetype which was not enforced by requests but is enforced by aiohttp. Related PR https://github.com/rytilahti/python-songpal/pull/59 --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 55b02b66a59bc4..b090a90d719c85 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Songpal", "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ - "python-songpal==0.11.1" + "python-songpal==0.11.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index df9cafe7aa005d..52f124d46d505a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1564,7 +1564,7 @@ python-ripple-api==0.0.3 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.11.1 +python-songpal==0.11.2 # homeassistant.components.synologydsm python-synology==0.2.0 From 5571c0c60a19e7afffa87daf2d5285874b16cde3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 30 Oct 2019 09:05:13 -0600 Subject: [PATCH 1352/3953] Bump pymyq to 2.0.1 (#28348) --- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 73265b61c83bf7..115c9cf515bd00 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -3,7 +3,7 @@ "name": "Myq", "documentation": "https://www.home-assistant.io/integrations/myq", "requirements": [ - "pymyq==2.0.0" + "pymyq==2.0.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 52f124d46d505a..307ee7175716af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1328,7 +1328,7 @@ pymsteams==0.1.12 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==2.0.0 +pymyq==2.0.1 # homeassistant.components.mysensors pymysensors==0.18.0 From 3c9482b2d3679d4947ddf2b6577859eaefc3ae48 Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Thu, 31 Oct 2019 09:39:27 +0100 Subject: [PATCH 1353/3953] Bump pysaj to v0.0.13 (fix for sensor date) (#28351) --- homeassistant/components/saj/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index 2dd701e9c7c65c..4d02ab74840d6e 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,7 +3,7 @@ "name": "SAJ", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": [ - "pysaj==0.0.12" + "pysaj==0.0.13" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 307ee7175716af..276710b520b4fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1423,7 +1423,7 @@ pyrepetier==3.0.5 pysabnzbd==1.1.0 # homeassistant.components.saj -pysaj==0.0.12 +pysaj==0.0.13 # homeassistant.components.sony_projector pysdcp==1 From d92060a461ea35c672ad42f5c7053f3841e99cd4 Mon Sep 17 00:00:00 2001 From: Steve M Date: Thu, 31 Oct 2019 04:38:53 -0400 Subject: [PATCH 1354/3953] Bump env_canada to fixed 0.0.29 version (#28360) * Bump env_canada to fixed 0.0.29 version * bump env_canada to 0.29 --- homeassistant/components/environment_canada/manifest.json | 8 ++------ requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 4d1a80946635b4..7a8313059fae5f 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,11 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": [ - "env_canada==0.0.27" - ], + "requirements": ["env_canada==0.0.29"], "dependencies": [], - "codeowners": [ - "@michaeldavie" - ] + "codeowners": ["@michaeldavie"] } diff --git a/requirements_all.txt b/requirements_all.txt index 276710b520b4fc..60cd7bf74d0efd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -456,7 +456,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.27 +env_canada==0.0.29 # homeassistant.components.envirophat # envirophat==0.0.6 From b71e1affdb6ca40042c45d028bc61ff83052a15d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 31 Oct 2019 17:29:27 +0100 Subject: [PATCH 1355/3953] Fix Airly asyncio timeout error (#28387) * Raise ConfigEntryNotReady * Better asyncio.TimeoutError handling * Sort imports * Increase asyncio timeout --- homeassistant/components/airly/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index dc2323ddd4e3fb..80f3518c652832 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -1,15 +1,16 @@ """The Airly component.""" import asyncio -import logging from datetime import timedelta +import logging -import async_timeout from aiohttp.client_exceptions import ClientConnectorError from airly import Airly from airly.exceptions import AirlyError +import async_timeout from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle @@ -45,6 +46,9 @@ async def async_setup_entry(hass, config_entry): await airly.async_update() + if not airly.data: + raise ConfigEntryNotReady() + hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_CLIENT] = {} hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly @@ -81,7 +85,7 @@ async def async_update(self): """Update Airly data.""" try: - with async_timeout.timeout(10): + with async_timeout.timeout(20): measurements = self.airly.create_measurements_session_point( self.latitude, self.longitude ) @@ -104,11 +108,8 @@ async def async_update(self): self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"] self.data[ATTR_API_ADVICE] = index["advice"] _LOGGER.debug("Data retrieved from Airly") - except ( - ValueError, - AirlyError, - asyncio.TimeoutError, - ClientConnectorError, - ) as error: + except asyncio.TimeoutError: + _LOGGER.error("Asyncio Timeout Error") + except (ValueError, AirlyError, ClientConnectorError) as error: _LOGGER.error(error) self.data = {} From 75b8070c99c366bfbf11a2b0647740c9e6dd2617 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 31 Oct 2019 20:16:27 +0200 Subject: [PATCH 1356/3953] Fix hdate spamming homeassistant log (#28392) * Fix hdate spamming homeassistant log * Lower verbosity of another spammy message --- 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 08182daedd05a0..b343963153d705 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/integrations/jewish_calendar", "requirements": [ - "hdate==0.9.1" + "hdate==0.9.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 60cd7bf74d0efd..6621104f26da18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,7 +622,7 @@ hass-nabucasa==0.22 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.1 +hdate==0.9.3 # homeassistant.components.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36f423860d06bc..4861c5793d6897 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -230,7 +230,7 @@ hass-nabucasa==0.22 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.1 +hdate==0.9.3 # homeassistant.components.here_travel_time herepy==0.6.3.1 From d57fe0334f127d9521497d0fa095da88253ec158 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Oct 2019 11:39:26 -0700 Subject: [PATCH 1357/3953] Fix check config (#28393) --- homeassistant/requirements.py | 12 ++++++++++-- tests/test_requirements.py | 21 ++++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 95738084f1f2c6..74469ef2fcd619 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -35,13 +35,21 @@ async def async_get_integration_with_requirements( This can raise IntegrationNotFound if manifest or integration is invalid, RequirementNotFound if there was some type of failure to install requirements. + + Does not handle circular dependencies. """ integration = await async_get_integration(hass, domain) - if hass.config.skip_pip or not integration.requirements: + if hass.config.skip_pip: return integration - await async_process_requirements(hass, integration.domain, integration.requirements) + if integration.requirements: + await async_process_requirements( + hass, integration.domain, integration.requirements + ) + + for dependency in integration.dependencies: + await async_get_integration_with_requirements(hass, dependency) return integration diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 780b175778edb8..548ea645360d94 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -112,7 +112,17 @@ async def test_install_missing_package(hass): 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"])) + mock_integration( + hass, MockModule("test_component_dep", requirements=["test-comp-dep==1.0.0"]) + ) + mock_integration( + hass, + MockModule( + "test_component", + requirements=["test-comp==1.0.0"], + dependencies=["test_component_dep"], + ), + ) with patch( "homeassistant.util.package.is_installed", return_value=False @@ -126,8 +136,13 @@ async def test_get_integration_with_requirements(hass): assert integration assert integration.domain == "test_component" - assert len(mock_is_installed.mock_calls) == 1 - assert len(mock_inst.mock_calls) == 1 + assert len(mock_is_installed.mock_calls) == 2 + assert mock_is_installed.mock_calls[0][1][0] == "test-comp==1.0.0" + assert mock_is_installed.mock_calls[1][1][0] == "test-comp-dep==1.0.0" + + assert len(mock_inst.mock_calls) == 2 + assert mock_inst.mock_calls[0][1][0] == "test-comp==1.0.0" + assert mock_inst.mock_calls[1][1][0] == "test-comp-dep==1.0.0" async def test_install_with_wheels_index(hass): From 4e2a8fde863857cdad35215d8aaff095b795c36a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Oct 2019 11:38:06 -0700 Subject: [PATCH 1358/3953] Check for import errors before validating config (#28395) --- homeassistant/setup.py | 17 +++++++++++------ tests/test_setup.py | 8 ++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 07de3b2942d061..314938feeed42c 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -132,6 +132,17 @@ def log_error(msg: str, link: bool = True) -> None: log_error(str(err)) return False + # Some integrations fail on import because they call functions incorrectly. + # So we do it before validating config to catch these errors. + try: + component = integration.get_component() + except ImportError: + log_error("Unable to import component", False) + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Setup failed for %s: unknown error", domain) + return False + processed_config = await conf_util.async_process_component_config( hass, config, integration ) @@ -143,12 +154,6 @@ def log_error(msg: str, link: bool = True) -> None: start = timer() _LOGGER.info("Setting up %s", domain) - try: - component = integration.get_component() - except ImportError: - log_error("Unable to import component", False) - return False - if hasattr(component, "PLATFORM_SCHEMA"): # Entity components have their own warning warn_task = None diff --git a/tests/test_setup.py b/tests/test_setup.py index 9612c1784ef00a..8fd25091eb6801 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -527,3 +527,11 @@ async def mock_callback(hass, component): setup.async_when_setup(hass, "test", mock_callback) await hass.async_block_till_done() assert calls == ["test", "test"] + + +async def test_setup_import_blows_up(hass): + """Test that we handle it correctly when importing integration blows up.""" + with mock.patch( + "homeassistant.loader.Integration.get_component", side_effect=ValueError + ): + assert not await setup.async_setup_component(hass, "sun", {}) From 633d006554dc8df87d810435f2e4f92b3a5a1915 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 Oct 2019 12:11:35 -0700 Subject: [PATCH 1359/3953] Bumped version to 0.101.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f6f1a4f2de2718..4e25e8c7dc369b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From b74711793e76185cee650fdf223a410997ee85a8 Mon Sep 17 00:00:00 2001 From: gngj Date: Thu, 31 Oct 2019 21:32:05 +0200 Subject: [PATCH 1360/3953] Fill services.yaml for squeezebox (#28247) * fill services.yaml for squeezebox * Minor fix --- homeassistant/components/squeezebox/services.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml index e69de29bb2d1d6..05c7de07f42b56 100644 --- a/homeassistant/components/squeezebox/services.yaml +++ b/homeassistant/components/squeezebox/services.yaml @@ -0,0 +1,13 @@ +squeezebox_call_method: + description: Call a custom Squeezebox JSONRPC API. + fields: + entity_id: + description: Name(s) of the Squeezebox entities where to run the API method. + example: 'media_player.squeezebox_radio' + command: + description: Command to pass to Logitech Media Server (p0 in the CLI documentation). + example: 'playlist' + parameters: + description: Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). + example: ["loadtracks", "album.titlesearch="] + From ec373d90c1c64ed11ba061c37c33b62120fcc714 Mon Sep 17 00:00:00 2001 From: thoscut Date: Thu, 31 Oct 2019 20:49:33 +0100 Subject: [PATCH 1361/3953] Add file list to attributes of folder sensor (#28338) --- homeassistant/components/folder/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index b06d77d93c9096..e9e4ea680c4bf4 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -64,10 +64,12 @@ def __init__(self, folder_path, filter_term): self._size = None self._name = os.path.split(os.path.split(folder_path)[0])[1] self._unit_of_measurement = "MB" + self._file_list = None def update(self): """Update the sensor.""" files_list = get_files_list(self._folder_path, self._filter_term) + self._file_list = files_list self._number_of_files = len(files_list) self._size = get_size(files_list) @@ -96,6 +98,7 @@ def device_state_attributes(self): "filter": self._filter_term, "number_of_files": self._number_of_files, "bytes": self._size, + "file_list": self._file_list, } return attr From 82729bef70e53eeafeb790bc5b54335c5ce497e7 Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 31 Oct 2019 20:51:35 +0100 Subject: [PATCH 1362/3953] Show all UPNP/IGD sensors in one device (#27517) * show all UPNP/IGD sensors in one device * use device name correctly * Use id of device --- homeassistant/components/upnp/__init__.py | 1 + homeassistant/components/upnp/device.py | 5 +++++ homeassistant/components/upnp/sensor.py | 7 +++++-- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 38356d0af16325..bbb49ebd1d44ae 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -162,6 +162,7 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry): identifiers={(DOMAIN, device.udn)}, name=device.name, manufacturer=device.manufacturer, + model=device.model_name, ) # set up sensors diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 7f7f0f5b93a3a1..de3c93a82ed3a1 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -78,6 +78,11 @@ def manufacturer(self): """Get the manufacturer.""" return self._igd_device.manufacturer + @property + def model_name(self): + """Get the model name.""" + return self._igd_device.model_name + async def async_add_port_mappings(self, ports, local_ip): """Add port mappings.""" if local_ip == "127.0.0.1": diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 40cb7ef2032f85..4c85e904b1db4f 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType @@ -93,9 +94,11 @@ def _upnp_remove_sensor(self, device): def device_info(self): """Get device info.""" return { - "identifiers": {(DOMAIN_UPNP, self.unique_id)}, - "name": self.name, + "connections": {(dr.CONNECTION_UPNP, self._device.udn)}, + "identifiers": {(DOMAIN_UPNP, self._device.udn)}, + "name": self._device.name, "manufacturer": self._device.manufacturer, + "model": self._device.model_name, } From d200c2dca2f3cf49745b3b3dbf965ab1a165e174 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 1 Nov 2019 07:05:42 +1100 Subject: [PATCH 1363/3953] fix feedreader handling unrecognized published date (#28225) --- .../components/feedreader/__init__.py | 7 ++++--- tests/components/feedreader/test_init.py | 6 ++++++ tests/fixtures/feedreader4.xml | 20 +++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/feedreader4.xml diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index e4ec154620e94a..27b164e4edf27e 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -139,9 +139,10 @@ def _filter_entries(self): def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" - # We are lucky, `published_parsed` data available, let's make use of - # it to publish only new available entries since the last run - if "published_parsed" in entry.keys(): + # Check if the entry has a published date. + if "published_parsed" in entry.keys() and entry.published_parsed: + # We are lucky, `published_parsed` data available, let's make use of + # it to publish only new available entries since the last run self._has_published_parsed = True self._last_entry_timestamp = max( entry.published_parsed, self._last_entry_timestamp diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index 3f1fee0188b8b4..eff44c44303c2c 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -163,6 +163,12 @@ def test_feed_without_publication_date_and_title(self): manager, events = self.setup_manager(feed_data) assert len(events) == 3 + def test_feed_with_unrecognized_publication_date(self): + """Test simple feed with entry with unrecognized publication date.""" + feed_data = load_fixture("feedreader4.xml") + manager, events = self.setup_manager(feed_data) + assert len(events) == 1 + def test_feed_invalid_data(self): """Test feed with invalid data.""" feed_data = "INVALID DATA" diff --git a/tests/fixtures/feedreader4.xml b/tests/fixtures/feedreader4.xml new file mode 100644 index 00000000000000..81828ccb6e25e2 --- /dev/null +++ b/tests/fixtures/feedreader4.xml @@ -0,0 +1,20 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 26 Oct 2019 12:00:00 +1000 + Mon, 26 Oct 2019 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + 26.10.2019 - 12:06:24 + + + + From 54361342ba7368f0a5344c7382815435153ddee9 Mon Sep 17 00:00:00 2001 From: Daniyar Yeralin Date: Thu, 31 Oct 2019 19:15:20 -0400 Subject: [PATCH 1364/3953] Introduce SUPPORT_COLOR_TEMP for flux_led component (#26692) * Introduce SUPPORT_COLOR_TEMP for flux_led component * Make black linter happy * Remove duplicate SUPPORT_COLOR_TEMP * Make linter happy --- homeassistant/components/flux_led/light.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 5bd84cd157fb09..7973956848a51d 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -12,12 +12,14 @@ 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, ) @@ -43,6 +45,10 @@ # 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 modes +COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF = 285 + # List of supported effects which aren't already declared in LIGHT EFFECT_RED_FADE = "red_fade" EFFECT_GREEN_FADE = "green_fade" @@ -235,7 +241,7 @@ def hs_color(self): def supported_features(self): """Flag supported features.""" if self._mode == MODE_RGBW: - return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE + return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE | SUPPORT_COLOR_TEMP if self._mode == MODE_WHITE: return SUPPORT_BRIGHTNESS @@ -284,6 +290,17 @@ def turn_on(self, **kwargs): brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) white = kwargs.get(ATTR_WHITE_VALUE) + color_temp = kwargs.get(ATTR_COLOR_TEMP) + + # handle special modes + if color_temp is not None: + if brightness is None: + brightness = self.brightness + if color_temp > COLOR_TEMP_WARM_VS_COLD_WHITE_CUT_OFF: + self._bulb.setRgbw(w=brightness) + else: + self._bulb.setRgbw(w2=brightness) + return # Show warning if effect set with rgb, brightness, or white level if effect and (brightness or white or rgb): From bb6a617a6f75ef79d3d8eae6a8ae7f09d6adfea3 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 1 Nov 2019 00:32:13 +0000 Subject: [PATCH 1365/3953] [ci skip] Translation update --- .../components/almond/.translations/pl.json | 9 +++++ .../components/cover/.translations/ca.json | 8 ++++- .../components/cover/.translations/pl.json | 8 +++++ .../components/cover/.translations/ru.json | 9 ++++- .../cover/.translations/zh-Hant.json | 8 +++++ .../device_tracker/.translations/it.json | 8 +++++ .../huawei_lte/.translations/it.json | 32 +++++++++++++++++ .../media_player/.translations/it.json | 9 +++++ .../opentherm_gw/.translations/pl.json | 4 +-- .../components/sensor/.translations/pl.json | 36 +++++++++---------- .../components/withings/.translations/it.json | 6 ++++ 11 files changed, 115 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/almond/.translations/pl.json create mode 100644 homeassistant/components/device_tracker/.translations/it.json create mode 100644 homeassistant/components/huawei_lte/.translations/it.json create mode 100644 homeassistant/components/media_player/.translations/it.json diff --git a/homeassistant/components/almond/.translations/pl.json b/homeassistant/components/almond/.translations/pl.json new file mode 100644 index 00000000000000..b96d9c09bb24a1 --- /dev/null +++ b/homeassistant/components/almond/.translations/pl.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Almond.", + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ca.json b/homeassistant/components/cover/.translations/ca.json index 5602ca98ec2cd5..da86e4960c0576 100644 --- a/homeassistant/components/cover/.translations/ca.json +++ b/homeassistant/components/cover/.translations/ca.json @@ -2,11 +2,17 @@ "device_automation": { "condition_type": { "is_closed": "{entity_name} est\u00e0 tancat/da", - "is_closing": "{entity_name} est\u00e0 tancan't-se", + "is_closing": "{entity_name} est\u00e0 tancant-se", "is_open": "{entity_name} est\u00e0 obert/a", "is_opening": "{entity_name} s'est\u00e0 obrint", "is_position": "La posici\u00f3 de {entity_name} \u00e9s", "is_tilt_position": "La posici\u00f3 d'inclinaci\u00f3 de {entity_name} \u00e9s" + }, + "trigger_type": { + "closed": "{entity_name} tancat/da", + "closing": "{entity_name} tancant-se", + "opened": "{entity_name} s'ha obert", + "opening": "{entity_name} obrint-se" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/pl.json b/homeassistant/components/cover/.translations/pl.json index a4e4f19712bac8..718c4b86fbdd72 100644 --- a/homeassistant/components/cover/.translations/pl.json +++ b/homeassistant/components/cover/.translations/pl.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} si\u0119 otwiera", "is_position": "pozycja pokrywy {entity_name} to", "is_tilt_position": "pochylenie pokrywy {entity_name} to" + }, + "trigger_type": { + "closed": "nast\u0105pi zamkni\u0119cie {entity_name}", + "closing": "{entity_name} si\u0119 zamyka", + "opened": "nast\u0105pi otwarcie {entity_name}", + "opening": "{entity_name} si\u0119 otwiera", + "position": "zmieni si\u0119 pozycja pokrywy {entity_name}", + "tilt_position": "zmieni si\u0119 pochylenie pokrywy {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json index 5e89cc6e82e541..47608a1ff770d4 100644 --- a/homeassistant/components/cover/.translations/ru.json +++ b/homeassistant/components/cover/.translations/ru.json @@ -6,7 +6,14 @@ "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "is_position": "{entity_name} \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438", - "is_tilt_position": "{entity_name} \u0438\u043c\u0435\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430\u043a\u043b\u043e\u043d\u0430" + "is_tilt_position": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \u043d\u0430\u043a\u043b\u043e\u043d\u0430" + }, + "trigger_type": { + "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "tilt_position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043d\u0430\u043a\u043b\u043e\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/zh-Hant.json b/homeassistant/components/cover/.translations/zh-Hant.json index 63910cd208c586..8197d2146aedcd 100644 --- a/homeassistant/components/cover/.translations/zh-Hant.json +++ b/homeassistant/components/cover/.translations/zh-Hant.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f", "is_position": "{entity_name} \u4f4d\u7f6e\u70ba", "is_tilt_position": "{entity_name} \u6a19\u984c\u4f4d\u7f6e\u70ba" + }, + "trigger_type": { + "closed": "{entity_name} \u5df2\u95dc\u9589", + "closing": "{entity_name} \u6b63\u5728\u95dc\u9589", + "opened": "{entity_name} \u5df2\u958b\u555f", + "opening": "{entity_name} \u6b63\u5728\u958b\u555f", + "position": "{entity_name} \u4f4d\u7f6e\u8b8a\u66f4", + "tilt_position": "{entity_name} \u6a19\u984c\u4f4d\u7f6e\u8b8a\u66f4" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/it.json b/homeassistant/components/device_tracker/.translations/it.json new file mode 100644 index 00000000000000..3030410a97a334 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/it.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \u00e8 a casa", + "is_not_home": "{entity_name} non \u00e8 a casa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/it.json b/homeassistant/components/huawei_lte/.translations/it.json new file mode 100644 index 00000000000000..4bfd3389745851 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Questo dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "connection_failed": "Connessione fallita", + "incorrect_password": "Password errata", + "incorrect_username": "Nome utente errato", + "incorrect_username_or_password": "Nome utente o password errati", + "invalid_url": "URL non valido" + }, + "step": { + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "Nome utente" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "track_new_devices": "Traccia nuovi dispositivi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/it.json b/homeassistant/components/media_player/.translations/it.json new file mode 100644 index 00000000000000..52a9bb3f051909 --- /dev/null +++ b/homeassistant/components/media_player/.translations/it.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} \u00e8 spento", + "is_on": "{entity_name} \u00e8 acceso", + "is_paused": "{entity_name} \u00e8 in pausa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index 180f6a2430dd96..fe8ccfc8975f98 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -10,7 +10,7 @@ "init": { "data": { "device": "\u015acie\u017cka lub adres URL", - "floor_temperature": "Temperatura pod\u0142ogi", + "floor_temperature": "Zaokr\u0105glanie warto\u015bci w d\u00f3\u0142", "id": "Identyfikator", "name": "Nazwa", "precision": "Precyzja temperatury" @@ -24,7 +24,7 @@ "step": { "init": { "data": { - "floor_temperature": "Temperatura pod\u0142ogi", + "floor_temperature": "Zaokr\u0105glanie warto\u015bci w d\u00f3\u0142", "precision": "Precyzja" }, "description": "Opcje dla bramki OpenTherm" diff --git a/homeassistant/components/sensor/.translations/pl.json b/homeassistant/components/sensor/.translations/pl.json index 68a3a0fecfdc3f..7ef982e0d13009 100644 --- a/homeassistant/components/sensor/.translations/pl.json +++ b/homeassistant/components/sensor/.translations/pl.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} poziom na\u0142adowania baterii", - "is_humidity": "{entity_name} wilgotno\u015b\u0107", - "is_illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}", - "is_power": "moc {entity_name}", - "is_pressure": "ci\u015bnienie {entity_name}", - "is_signal_strength": "si\u0142a sygna\u0142u {entity_name}", - "is_temperature": "temperatura {entity_name}", - "is_timestamp": "znacznik czasu {entity_name}", - "is_value": "warto\u015b\u0107 {entity_name}" + "is_battery_level": "obecny poziom na\u0142adowania baterii {entity_name}", + "is_humidity": "obecna wilgotno\u015b\u0107 {entity_name}", + "is_illuminance": "obecne nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "is_power": "obecna moc {entity_name}", + "is_pressure": "obecne ci\u015bnienie {entity_name}", + "is_signal_strength": "obecna si\u0142a sygna\u0142u {entity_name}", + "is_temperature": "obecna temperatura {entity_name}", + "is_timestamp": "obecny znacznik czasu {entity_name}", + "is_value": "obecna warto\u015b\u0107 {entity_name}" }, "trigger_type": { - "battery_level": "poziom baterii {entity_name}", - "humidity": "wilgotno\u015b\u0107 {entity_name}", - "illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}", - "power": "moc {entity_name}", - "pressure": "ci\u015bnienie {entity_name}", - "signal_strength": "si\u0142a sygna\u0142u {entity_name}", - "temperature": "temperatura {entity_name}", - "timestamp": "znacznik czasu {entity_name}", - "value": "warto\u015b\u0107 {entity_name}" + "battery_level": "zmieni si\u0119 poziom baterii {entity_name}", + "humidity": "zmieni si\u0119 wilgotno\u015b\u0107 {entity_name}", + "illuminance": "zmieni si\u0119 nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "power": "zmieni si\u0119 moc {entity_name}", + "pressure": "zmieni si\u0119 ci\u015bnienie {entity_name}", + "signal_strength": "zmieni si\u0119 si\u0142a sygna\u0142u {entity_name}", + "temperature": "zmieni si\u0119 temperatura {entity_name}", + "timestamp": "zmieni si\u0119 znacznik czasu {entity_name}", + "value": "zmieni si\u0119 warto\u015b\u0107 {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json index 51276869ec605c..fbd590697e18dc 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -7,6 +7,12 @@ "default": "Autenticazione completata con Withings per il profilo selezionato." }, "step": { + "profile": { + "data": { + "profile": "Profilo" + }, + "title": "Profilo utente." + }, "user": { "data": { "profile": "Profilo" From 72a17d4c677d1a6faa6058a2ba3f99acac62f320 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Nov 2019 13:09:45 +0100 Subject: [PATCH 1366/3953] Upgrade thingspeak to 1.0.0 (#28424) --- homeassistant/components/thingspeak/__init__.py | 6 ++---- homeassistant/components/thingspeak/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/thingspeak/__init__.py b/homeassistant/components/thingspeak/__init__.py index 1870a317752f66..0eccfbbcd04106 100644 --- a/homeassistant/components/thingspeak/__init__.py +++ b/homeassistant/components/thingspeak/__init__.py @@ -37,20 +37,18 @@ def setup(hass, config): """Set up the Thingspeak environment.""" - conf = config[DOMAIN] api_key = conf.get(CONF_API_KEY) channel_id = conf.get(CONF_ID) entity = conf.get(CONF_WHITELIST) try: - channel = thingspeak.Channel(channel_id, write_key=api_key, timeout=TIMEOUT) + channel = thingspeak.Channel(channel_id, api_key=api_key, timeout=TIMEOUT) channel.get() except RequestException: _LOGGER.error( "Error while accessing the ThingSpeak channel. " - "Please check that the channel exists and your " - "API key is correct" + "Please check that the channel exists and your API key is correct" ) return False diff --git a/homeassistant/components/thingspeak/manifest.json b/homeassistant/components/thingspeak/manifest.json index 689a6678cab505..9fddd941543840 100644 --- a/homeassistant/components/thingspeak/manifest.json +++ b/homeassistant/components/thingspeak/manifest.json @@ -3,7 +3,7 @@ "name": "Thingspeak", "documentation": "https://www.home-assistant.io/integrations/thingspeak", "requirements": [ - "thingspeak==0.4.1" + "thingspeak==1.0.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 135376d5eeedb9..1a3da42e9e84f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1898,7 +1898,7 @@ teslajsonpy==0.0.26 thermoworks_smoke==0.1.8 # homeassistant.components.thingspeak -thingspeak==0.4.1 +thingspeak==1.0.0 # homeassistant.components.tikteck tikteck==0.4 From 083d34cdd94ae0de27eb5ad126c7e44b1c34249e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Nov 2019 13:10:53 +0100 Subject: [PATCH 1367/3953] Upgrade attrs to 19.3.0 (#28421) --- 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 1933448edda6a6..03e43ba139a24e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiohttp==3.6.1 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 -attrs==19.2.0 +attrs==19.3.0 bcrypt==3.1.7 certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" diff --git a/requirements_all.txt b/requirements_all.txt index 1a3da42e9e84f9..4d6e59a69121e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ aiohttp==3.6.1 astral==1.10.1 async_timeout==3.0.1 -attrs==19.2.0 +attrs==19.3.0 bcrypt==3.1.7 certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" diff --git a/setup.py b/setup.py index e8b32fd8edfe7b..612ac7d0ce8f79 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ "aiohttp==3.6.1", "astral==1.10.1", "async_timeout==3.0.1", - "attrs==19.2.0", + "attrs==19.3.0", "bcrypt==3.1.7", "certifi>=2019.9.11", 'contextvars==2.4;python_version<"3.7"', From 6f24d2bb3ba345f80925c36452f28b2bc4987d39 Mon Sep 17 00:00:00 2001 From: Michael Schoonmaker Date: Fri, 1 Nov 2019 06:27:26 -0700 Subject: [PATCH 1368/3953] Add a Services YAML for the Dominos integration (#27289) (#28339) --- homeassistant/components/dominos/services.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/dominos/services.yaml b/homeassistant/components/dominos/services.yaml index e69de29bb2d1d6..93f8b2851f17c9 100644 --- a/homeassistant/components/dominos/services.yaml +++ b/homeassistant/components/dominos/services.yaml @@ -0,0 +1,6 @@ +order: + description: Places a set of orders with Dominos Pizza. + fields: + order_entity_id: + description: The ID (as specified in the configuration) of an order to place. If provided as an array, all of the identified orders will be placed. + example: dominos.medium_pan From 07337badcdc614ace6ba953c63f0effc1c58ffde Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Nov 2019 14:28:39 +0100 Subject: [PATCH 1369/3953] Upgrade pysnmp to 4.4.12 (#28428) --- homeassistant/components/snmp/manifest.json | 2 +- homeassistant/components/snmp/sensor.py | 1 - requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index d3942ab4a327f9..aad7f49c962687 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/integrations/snmp", "requirements": [ - "pysnmp==4.4.11" + "pysnmp==4.4.12" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index b369ec83c58034..9ca0444f7bce8d 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -81,7 +81,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SNMP sensor.""" - name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/requirements_all.txt b/requirements_all.txt index 4d6e59a69121e1..7c12a7580d56d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1462,7 +1462,7 @@ pysmartthings==0.6.9 pysmarty==0.8 # homeassistant.components.snmp -pysnmp==4.4.11 +pysnmp==4.4.12 # homeassistant.components.soma pysoma==0.0.10 From c7d72f55e9e39c481b0e97946ba9c07f39d37ad6 Mon Sep 17 00:00:00 2001 From: Robin Pronk Date: Fri, 1 Nov 2019 15:38:14 +0100 Subject: [PATCH 1370/3953] SNMP switch fix integer support (#28425) --- homeassistant/components/snmp/switch.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index aac43208a1f58b..8d5be1221c4026 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -1,6 +1,8 @@ """Support for SNMP enabled switch.""" import logging +from pyasn1.type.univ import Integer + import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, @@ -190,15 +192,20 @@ def __init__( async def async_turn_on(self, **kwargs): """Turn on the switch.""" - await self._set(self._command_payload_on) + if self._command_payload_on.isdigit(): + await self._set(Integer(self._command_payload_on)) + else: + await self._set(self._command_payload_on) async def async_turn_off(self, **kwargs): """Turn off the switch.""" - await self._set(self._command_payload_off) + if self._command_payload_on.isdigit(): + await self._set(Integer(self._command_payload_off)) + else: + await self._set(self._command_payload_off) async def async_update(self): """Update the state.""" - errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) ) @@ -215,8 +222,12 @@ async def async_update(self): for resrow in restable: if resrow[-1] == self._payload_on: self._state = True + elif resrow[-1] == Integer(self._payload_on): + self._state = True elif resrow[-1] == self._payload_off: self._state = False + elif resrow[-1] == Integer(self._payload_off): + self._state = False else: self._state = None From 44879b323e72c667b86e01eebd6c9c50f1f62798 Mon Sep 17 00:00:00 2001 From: John Mihalic <2854333+mezz64@users.noreply.github.com> Date: Fri, 1 Nov 2019 13:40:35 -0400 Subject: [PATCH 1371/3953] Bump pyEight library to 0.1.2 to update API URL (#28413) --- homeassistant/components/eight_sleep/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 3353f1fa4d962f..25961f15a0a98c 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -3,7 +3,7 @@ "name": "Eight sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", "requirements": [ - "pyeight==0.1.1" + "pyeight==0.1.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7c12a7580d56d8..9723075b98ba16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1180,7 +1180,7 @@ pyeconet==0.0.11 pyedimax==0.1 # homeassistant.components.eight_sleep -pyeight==0.1.1 +pyeight==0.1.2 # homeassistant.components.emby pyemby==1.6 From f8d779e84032416c72ea016829670f5f54747d7c Mon Sep 17 00:00:00 2001 From: phispi Date: Fri, 1 Nov 2019 21:23:23 +0100 Subject: [PATCH 1372/3953] Prevent TypeError when KNX RGB(W) light value contains None (#28358) * Prevent TypeError when KNX RGB(W) light value contains None. * Pylint doesn't like 'w' as variable name, therefore using 'white' instead. * Simplified code as suggested by pvizeli. --- homeassistant/components/knx/light.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 81bf4ad3c8375d..c7292309461440 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -180,13 +180,9 @@ def should_poll(self): @property def brightness(self): """Return the brightness of this light between 0..255.""" - if self.device.supports_brightness: - return self.device.current_brightness - if ( - self.device.supports_color or self.device.supports_rgbw - ) and self.device.current_color: - return max(self.device.current_color) - return None + if not self.device.supports_brightness: + return None + return self.device.current_brightness @property def hs_color(self): From 1fb377e61eeea93336d8ca4d7941b3bceb436f61 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 1 Nov 2019 21:25:33 +0100 Subject: [PATCH 1373/3953] Use defined device class constants for Homematic (#28438) * Use defined device classes for Homematic * Remove not required default None * Missed on None --- .../components/homematic/binary_sensor.py | 32 ++++++++----- homeassistant/components/homematic/sensor.py | 47 ++++++++++++------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 064b6f3e009c7f..cc2907c64fb224 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -1,26 +1,32 @@ """Support for HomeMatic binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PRESENCE, + DEVICE_CLASS_SMOKE, + BinarySensorDevice, +) from homeassistant.components.homematic import ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY -from homeassistant.const import DEVICE_CLASS_BATTERY from . import ATTR_DISCOVER_DEVICES, HMDevice _LOGGER = logging.getLogger(__name__) SENSOR_TYPES_CLASS = { - "IPShutterContact": "opening", - "IPShutterContactSabotage": "opening", - "MaxShutterContact": "opening", - "Motion": "motion", - "MotionV2": "motion", - "PresenceIP": "motion", + "IPShutterContact": DEVICE_CLASS_OPENING, + "IPShutterContactSabotage": DEVICE_CLASS_OPENING, + "MaxShutterContact": DEVICE_CLASS_OPENING, + "Motion": DEVICE_CLASS_MOTION, + "MotionV2": DEVICE_CLASS_MOTION, + "PresenceIP": DEVICE_CLASS_PRESENCE, "Remote": None, "RemoteMotion": None, - "ShutterContact": "opening", - "Smoke": "smoke", - "SmokeV2": "smoke", + "ShutterContact": DEVICE_CLASS_OPENING, + "Smoke": DEVICE_CLASS_SMOKE, + "SmokeV2": DEVICE_CLASS_SMOKE, "TiltSensor": None, "WeatherSensor": None, } @@ -56,8 +62,8 @@ def device_class(self): """Return the class of this sensor from DEVICE_CLASSES.""" # If state is MOTION (Only RemoteMotion working) if self._state == "MOTION": - return "motion" - return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__, None) + return DEVICE_CLASS_MOTION + return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__) def _init_data_struct(self): """Generate the data dictionary (self._data) from metadata.""" diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index a3fc9f7e0fac23..10c402a0dd4395 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -1,7 +1,15 @@ """Support for HomeMatic sensors.""" import logging -from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT, STATE_UNKNOWN +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_WATT_HOUR, + POWER_WATT, + STATE_UNKNOWN, +) from . import ATTR_DISCOVER_DEVICES, HMDevice @@ -48,21 +56,21 @@ "VALUE": "#", } -HM_ICON_HA_CAST = { - "WIND_SPEED": "mdi:weather-windy", - "HUMIDITY": "mdi:water-percent", - "TEMPERATURE": "mdi:thermometer", - "ACTUAL_TEMPERATURE": "mdi:thermometer", - "LUX": "mdi:weather-sunny", - "CURRENT_ILLUMINATION": "mdi:weather-sunny", - "AVERAGE_ILLUMINATION": "mdi:weather-sunny", - "LOWEST_ILLUMINATION": "mdi:weather-sunny", - "HIGHEST_ILLUMINATION": "mdi:weather-sunny", - "BRIGHTNESS": "mdi:invert-colors", - "POWER": "mdi:flash-red-eye", - "CURRENT": "mdi:flash-red-eye", +HM_DEVICE_CLASS_HA_CAST = { + "HUMIDITY": DEVICE_CLASS_HUMIDITY, + "TEMPERATURE": DEVICE_CLASS_TEMPERATURE, + "ACTUAL_TEMPERATURE": DEVICE_CLASS_TEMPERATURE, + "LUX": DEVICE_CLASS_ILLUMINANCE, + "CURRENT_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, + "AVERAGE_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, + "LOWEST_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, + "HIGHEST_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, + "POWER": DEVICE_CLASS_POWER, + "CURRENT": DEVICE_CLASS_POWER, } +HM_ICON_HA_CAST = {"WIND_SPEED": "mdi:weather-windy", "BRIGHTNESS": "mdi:invert-colors"} + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the HomeMatic sensor platform.""" @@ -86,7 +94,7 @@ def state(self): # Does a cast exist for this class? name = self._hmdevice.__class__.__name__ if name in HM_STATE_HA_CAST: - return HM_STATE_HA_CAST[name].get(self._hm_get_state(), None) + return HM_STATE_HA_CAST[name].get(self._hm_get_state()) # No cast, return original value return self._hm_get_state() @@ -94,12 +102,17 @@ def state(self): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - return HM_UNIT_HA_CAST.get(self._state, None) + return HM_UNIT_HA_CAST.get(self._state) + + @property + def device_class(self): + """Return the device class to use in the frontend, if any.""" + return HM_DEVICE_CLASS_HA_CAST.get(self._state) @property def icon(self): """Return the icon to use in the frontend, if any.""" - return HM_ICON_HA_CAST.get(self._state, None) + return HM_ICON_HA_CAST.get(self._state) def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" From 12f1a8f551af6131ecd58da84630cd94dfa9db85 Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 1 Nov 2019 21:36:18 +0100 Subject: [PATCH 1374/3953] Add improved scene support to the alarm_control_panel integration (#28269) * Add improved scene support to the alarm_control_panel integration * Add service description for alarm_arm_custom_bypass --- .../alarm_control_panel/reproduce_state.py | 84 ++++++++++ .../alarm_control_panel/services.yaml | 10 ++ .../test_reproduce_state.py | 148 ++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/reproduce_state.py create mode 100644 tests/components/alarm_control_panel/test_reproduce_state.py diff --git a/homeassistant/components/alarm_control_panel/reproduce_state.py b/homeassistant/components/alarm_control_panel/reproduce_state.py new file mode 100644 index 00000000000000..705bca608a6505 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/reproduce_state.py @@ -0,0 +1,84 @@ +"""Reproduce an Alarm control panel state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = { + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ALARM_ARMED_AWAY: + service = SERVICE_ALARM_ARM_AWAY + elif state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS: + service = SERVICE_ALARM_ARM_CUSTOM_BYPASS + elif state.state == STATE_ALARM_ARMED_HOME: + service = SERVICE_ALARM_ARM_HOME + elif state.state == STATE_ALARM_ARMED_NIGHT: + service = SERVICE_ALARM_ARM_NIGHT + elif state.state == STATE_ALARM_DISARMED: + service = SERVICE_ALARM_DISARM + elif state.state == STATE_ALARM_TRIGGERED: + service = SERVICE_ALARM_TRIGGER + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Alarm control panel states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 7918631464fee3..9abf2189ed3c65 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -10,6 +10,16 @@ alarm_disarm: description: An optional code to disarm the alarm control panel with. example: 1234 +alarm_arm_custom_bypass: + description: Send arm custom bypass command. + fields: + entity_id: + description: Name of alarm control panel to arm custom bypass. + example: 'alarm_control_panel.downstairs' + code: + description: An optional code to arm custom bypass the alarm control panel with. + example: 1234 + alarm_arm_home: description: Send the alarm the command for arm home. fields: diff --git a/tests/components/alarm_control_panel/test_reproduce_state.py b/tests/components/alarm_control_panel/test_reproduce_state.py new file mode 100644 index 00000000000000..61b0e3ccd30293 --- /dev/null +++ b/tests/components/alarm_control_panel/test_reproduce_state.py @@ -0,0 +1,148 @@ +"""Test reproduce state for Alarm control panel.""" +from homeassistant.const import ( + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Alarm control panel states.""" + hass.states.async_set( + "alarm_control_panel.entity_armed_away", STATE_ALARM_ARMED_AWAY, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_armed_custom_bypass", + STATE_ALARM_ARMED_CUSTOM_BYPASS, + {}, + ) + hass.states.async_set( + "alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_HOME, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_NIGHT, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED, {} + ) + hass.states.async_set( + "alarm_control_panel.entity_triggered", STATE_ALARM_TRIGGERED, {} + ) + + arm_away_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_AWAY + ) + arm_custom_bypass_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_CUSTOM_BYPASS + ) + arm_home_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_HOME + ) + arm_night_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_ARM_NIGHT + ) + disarm_calls = async_mock_service(hass, "alarm_control_panel", SERVICE_ALARM_DISARM) + trigger_calls = async_mock_service( + hass, "alarm_control_panel", SERVICE_ALARM_TRIGGER + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("alarm_control_panel.entity_armed_away", STATE_ALARM_ARMED_AWAY), + State( + "alarm_control_panel.entity_armed_custom_bypass", + STATE_ALARM_ARMED_CUSTOM_BYPASS, + ), + State("alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_HOME), + State("alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_NIGHT), + State("alarm_control_panel.entity_disarmed", STATE_ALARM_DISARMED), + State("alarm_control_panel.entity_triggered", STATE_ALARM_TRIGGERED), + ], + blocking=True, + ) + + assert len(arm_away_calls) == 0 + assert len(arm_custom_bypass_calls) == 0 + assert len(arm_home_calls) == 0 + assert len(arm_night_calls) == 0 + assert len(disarm_calls) == 0 + assert len(trigger_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("alarm_control_panel.entity_triggered", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(arm_away_calls) == 0 + assert len(arm_custom_bypass_calls) == 0 + assert len(arm_home_calls) == 0 + assert len(arm_night_calls) == 0 + assert len(disarm_calls) == 0 + assert len(trigger_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("alarm_control_panel.entity_armed_away", STATE_ALARM_TRIGGERED), + State( + "alarm_control_panel.entity_armed_custom_bypass", STATE_ALARM_ARMED_AWAY + ), + State( + "alarm_control_panel.entity_armed_home", STATE_ALARM_ARMED_CUSTOM_BYPASS + ), + State("alarm_control_panel.entity_armed_night", STATE_ALARM_ARMED_HOME), + State("alarm_control_panel.entity_disarmed", STATE_ALARM_ARMED_NIGHT), + State("alarm_control_panel.entity_triggered", STATE_ALARM_DISARMED), + # Should not raise + State("alarm_control_panel.non_existing", "on"), + ], + blocking=True, + ) + + assert len(arm_away_calls) == 1 + assert arm_away_calls[0].domain == "alarm_control_panel" + assert arm_away_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_custom_bypass" + } + + assert len(arm_custom_bypass_calls) == 1 + assert arm_custom_bypass_calls[0].domain == "alarm_control_panel" + assert arm_custom_bypass_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_home" + } + + assert len(arm_home_calls) == 1 + assert arm_home_calls[0].domain == "alarm_control_panel" + assert arm_home_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_night" + } + + assert len(arm_night_calls) == 1 + assert arm_night_calls[0].domain == "alarm_control_panel" + assert arm_night_calls[0].data == { + "entity_id": "alarm_control_panel.entity_disarmed" + } + + assert len(disarm_calls) == 1 + assert disarm_calls[0].domain == "alarm_control_panel" + assert disarm_calls[0].data == {"entity_id": "alarm_control_panel.entity_triggered"} + + assert len(trigger_calls) == 1 + assert trigger_calls[0].domain == "alarm_control_panel" + assert trigger_calls[0].data == { + "entity_id": "alarm_control_panel.entity_armed_away" + } From 07b7d514ac8da2d0cc826c652a0527db2566e63f Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 1 Nov 2019 21:37:34 +0100 Subject: [PATCH 1375/3953] Add improved scene support to the water_heater integration (#28277) --- homeassistant/components/demo/__init__.py | 1 + .../water_heater/reproduce_state.py | 125 ++++++++++++++++++ .../water_heater/test_reproduce_state.py | 124 +++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 homeassistant/components/water_heater/reproduce_state.py create mode 100644 tests/components/water_heater/test_reproduce_state.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 967b7852c6f900..d93d217caa79f1 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -28,6 +28,7 @@ "switch", "tts", "mailbox", + "water_heater", ] diff --git a/homeassistant/components/water_heater/reproduce_state.py b/homeassistant/components/water_heater/reproduce_state.py new file mode 100644 index 00000000000000..2038b4c237b7e6 --- /dev/null +++ b/homeassistant/components/water_heater/reproduce_state.py @@ -0,0 +1,125 @@ +"""Reproduce an Water heater state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_AWAY_MODE, + ATTR_OPERATION_MODE, + ATTR_TEMPERATURE, + DOMAIN, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, + STATE_ECO, + STATE_ELECTRIC, + STATE_GAS, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_PERFORMANCE, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = { + STATE_ECO, + STATE_ELECTRIC, + STATE_GAS, + STATE_HEAT_PUMP, + STATE_HIGH_DEMAND, + STATE_OFF, + STATE_ON, + STATE_PERFORMANCE, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + 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 VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if ( + cur_state.state == state.state + and cur_state.attributes.get(ATTR_TEMPERATURE) + == state.attributes.get(ATTR_TEMPERATURE) + and cur_state.attributes.get(ATTR_AWAY_MODE) + == state.attributes.get(ATTR_AWAY_MODE) + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state != cur_state.state: + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + else: + service = SERVICE_SET_OPERATION_MODE + service_data[ATTR_OPERATION_MODE] = state.state + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + if ( + state.attributes.get(ATTR_TEMPERATURE) + != cur_state.attributes.get(ATTR_TEMPERATURE) + and state.attributes.get(ATTR_TEMPERATURE) is not None + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_TEMPERATURE: state.attributes.get(ATTR_TEMPERATURE), + }, + context=context, + blocking=True, + ) + + if ( + state.attributes.get(ATTR_AWAY_MODE) != cur_state.attributes.get(ATTR_AWAY_MODE) + and state.attributes.get(ATTR_AWAY_MODE) is not None + ): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_AWAY_MODE, + { + ATTR_ENTITY_ID: state.entity_id, + ATTR_AWAY_MODE: state.attributes.get(ATTR_AWAY_MODE), + }, + context=context, + blocking=True, + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Water heater states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/water_heater/test_reproduce_state.py b/tests/components/water_heater/test_reproduce_state.py new file mode 100644 index 00000000000000..0c12d8eb54a6f5 --- /dev/null +++ b/tests/components/water_heater/test_reproduce_state.py @@ -0,0 +1,124 @@ +"""Test reproduce state for Water heater.""" +from homeassistant.components.water_heater import ( + ATTR_AWAY_MODE, + ATTR_OPERATION_MODE, + ATTR_TEMPERATURE, + SERVICE_SET_AWAY_MODE, + SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, + STATE_ECO, + STATE_GAS, +) +from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Water heater states.""" + hass.states.async_set("water_heater.entity_off", STATE_OFF, {}) + hass.states.async_set("water_heater.entity_on", STATE_ON, {ATTR_TEMPERATURE: 45}) + hass.states.async_set("water_heater.entity_away", STATE_ON, {ATTR_AWAY_MODE: True}) + hass.states.async_set("water_heater.entity_gas", STATE_GAS, {}) + hass.states.async_set( + "water_heater.entity_all", + STATE_ECO, + {ATTR_AWAY_MODE: True, ATTR_TEMPERATURE: 45}, + ) + + turn_on_calls = async_mock_service(hass, "water_heater", SERVICE_TURN_ON) + turn_off_calls = async_mock_service(hass, "water_heater", SERVICE_TURN_OFF) + set_op_calls = async_mock_service(hass, "water_heater", SERVICE_SET_OPERATION_MODE) + set_temp_calls = async_mock_service(hass, "water_heater", SERVICE_SET_TEMPERATURE) + set_away_calls = async_mock_service(hass, "water_heater", SERVICE_SET_AWAY_MODE) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("water_heater.entity_off", STATE_OFF), + State("water_heater.entity_on", STATE_ON, {ATTR_TEMPERATURE: 45}), + State("water_heater.entity_away", STATE_ON, {ATTR_AWAY_MODE: True}), + State("water_heater.entity_gas", STATE_GAS, {}), + State( + "water_heater.entity_all", + STATE_ECO, + {ATTR_AWAY_MODE: True, ATTR_TEMPERATURE: 45}, + ), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_op_calls) == 0 + assert len(set_temp_calls) == 0 + assert len(set_away_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("water_heater.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_op_calls) == 0 + assert len(set_temp_calls) == 0 + assert len(set_away_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("water_heater.entity_on", STATE_OFF), + State("water_heater.entity_off", STATE_ON, {ATTR_TEMPERATURE: 45}), + State("water_heater.entity_all", STATE_ECO, {ATTR_AWAY_MODE: False}), + State("water_heater.entity_away", STATE_GAS, {}), + State( + "water_heater.entity_gas", + STATE_ECO, + {ATTR_AWAY_MODE: True, ATTR_TEMPERATURE: 45}, + ), + # Should not raise + State("water_heater.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "water_heater" + assert turn_on_calls[0].data == {"entity_id": "water_heater.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "water_heater" + assert turn_off_calls[0].data == {"entity_id": "water_heater.entity_on"} + + VALID_OP_CALLS = [ + {"entity_id": "water_heater.entity_away", ATTR_OPERATION_MODE: STATE_GAS}, + {"entity_id": "water_heater.entity_gas", ATTR_OPERATION_MODE: STATE_ECO}, + ] + assert len(set_op_calls) == 2 + for call in set_op_calls: + assert call.domain == "water_heater" + assert call.data in VALID_OP_CALLS + VALID_OP_CALLS.remove(call.data) + + VALID_TEMP_CALLS = [ + {"entity_id": "water_heater.entity_off", ATTR_TEMPERATURE: 45}, + {"entity_id": "water_heater.entity_gas", ATTR_TEMPERATURE: 45}, + ] + assert len(set_temp_calls) == 2 + for call in set_temp_calls: + assert call.domain == "water_heater" + assert call.data in VALID_TEMP_CALLS + VALID_TEMP_CALLS.remove(call.data) + + VALID_AWAY_CALLS = [ + {"entity_id": "water_heater.entity_all", ATTR_AWAY_MODE: False}, + {"entity_id": "water_heater.entity_gas", ATTR_AWAY_MODE: True}, + ] + assert len(set_away_calls) == 2 + for call in set_away_calls: + assert call.domain == "water_heater" + assert call.data in VALID_AWAY_CALLS + VALID_AWAY_CALLS.remove(call.data) From 62b09580c4b32812476ba90097c09b34d8bea410 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Nov 2019 22:29:34 +0100 Subject: [PATCH 1376/3953] deCONZ - Add Xiaomi Aqara Cube device trigger support (#27548) * Add Xiaomi Aqara Cube device trigger support --- .../components/deconz/.translations/en.json | 18 ++++- .../components/deconz/device_trigger.py | 67 +++++++++++++++++++ homeassistant/components/deconz/strings.json | 21 +++++- 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index e9c64ffe5faa64..6d9f7236d31afd 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -55,6 +55,12 @@ "left": "Left", "open": "Open", "right": "Right", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6", "turn_off": "Turn off", "turn_on": "Turn on" }, @@ -69,7 +75,17 @@ "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_gyro_activated": "Device shaken" + "remote_double_tap": "Device \"{subtype}\" double tapped", + "remote_awakened": "Device awakened", + "remote_falling": "Device in free fall", + "remote_gyro_activated": "Device shaken", + "remote_moved": "Device moved with \"{subtype}\" up", + "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", + "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", + "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", + "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", + "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", + "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 2d097d30c0b488..919061d9ad8f6b 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -32,7 +32,17 @@ CONF_QUINTUPLE_PRESS = "remote_button_quintuple_press" CONF_ROTATED = "remote_button_rotated" CONF_ROTATION_STOPPED = "remote_button_rotation_stopped" +CONF_AWAKE = "remote_awakened" +CONF_MOVE = "remote_moved" +CONF_DOUBLE_TAP = "remote_double_tap" CONF_SHAKE = "remote_gyro_activated" +CONF_FREE_FALL = "remote_falling" +CONF_ROTATE_FROM_SIDE_1 = "remote_rotate_from_side_1" +CONF_ROTATE_FROM_SIDE_2 = "remote_rotate_from_side_2" +CONF_ROTATE_FROM_SIDE_3 = "remote_rotate_from_side_3" +CONF_ROTATE_FROM_SIDE_4 = "remote_rotate_from_side_4" +CONF_ROTATE_FROM_SIDE_5 = "remote_rotate_from_side_5" +CONF_ROTATE_FROM_SIDE_6 = "remote_rotate_from_side_6" CONF_TURN_ON = "turn_on" CONF_TURN_OFF = "turn_off" @@ -47,6 +57,13 @@ CONF_BUTTON_2 = "button_2" CONF_BUTTON_3 = "button_3" CONF_BUTTON_4 = "button_4" +CONF_SIDE_1 = "side_1" +CONF_SIDE_2 = "side_2" +CONF_SIDE_3 = "side_3" +CONF_SIDE_4 = "side_4" +CONF_SIDE_5 = "side_5" +CONF_SIDE_6 = "side_6" + HUE_DIMMER_REMOTE_MODEL = "RWL021" HUE_DIMMER_REMOTE = { @@ -129,6 +146,55 @@ (CONF_ROTATED, CONF_RIGHT): 2002, } +AQARA_CUBE_MODEL = "lumi.sensor_cube" +AQARA_CUBE = { + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_2): 6002, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_3): 3002, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_4): 4002, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_5): 1002, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_6): 5002, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_1): 2006, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_3): 3006, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_4): 4006, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_5): 1006, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_6): 5006, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_1): 2003, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_2): 6003, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_4): 4003, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_5): 1003, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_6): 5003, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_1): 2004, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_2): 6004, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_3): 3004, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_5): 1004, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_6): 5004, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_1): 2001, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_2): 6001, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_3): 3001, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_4): 4001, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_6): 5001, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_1): 2005, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_2): 6005, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_3): 3005, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_4): 4005, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_5): 1005, + (CONF_MOVE, CONF_SIDE_1): 2000, + (CONF_MOVE, CONF_SIDE_2): 6000, + (CONF_MOVE, CONF_SIDE_3): 3000, + (CONF_MOVE, CONF_SIDE_4): 4000, + (CONF_MOVE, CONF_SIDE_5): 1000, + (CONF_MOVE, CONF_SIDE_6): 5000, + (CONF_DOUBLE_TAP, CONF_SIDE_1): 2002, + (CONF_DOUBLE_TAP, CONF_SIDE_2): 6002, + (CONF_DOUBLE_TAP, CONF_SIDE_3): 3003, + (CONF_DOUBLE_TAP, CONF_SIDE_4): 4004, + (CONF_DOUBLE_TAP, CONF_SIDE_5): 1001, + (CONF_DOUBLE_TAP, CONF_SIDE_6): 5005, + (CONF_AWAKE, ""): 7000, + (CONF_FREE_FALL, ""): 7008, + (CONF_SHAKE, ""): 7007, +} + AQARA_DOUBLE_WALL_SWITCH_MODEL = "lumi.remote.b286acn01" AQARA_DOUBLE_WALL_SWITCH = { (CONF_SHORT_PRESS, CONF_LEFT): 1002, @@ -179,6 +245,7 @@ TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE, TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE, TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER, + AQARA_CUBE_MODEL: AQARA_CUBE, AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH, diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 3571a9e1207c4c..56186feb8b1889 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -65,7 +65,17 @@ "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", - "remote_gyro_activated": "Device shaken" + "remote_falling": "Device in free fall", + "remote_awakened": "Device awakened", + "remote_moved": "Device moved with \"{subtype}\" up", + "remote_double_tap": "Device \"{subtype}\" double tapped", + "remote_gyro_activated": "Device shaken", + "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", + "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", + "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", + "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", + "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", + "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" }, "trigger_subtype": { "turn_on": "Turn on", @@ -80,7 +90,12 @@ "button_1": "First button", "button_2": "Second button", "button_3": "Third button", - "button_4": "Fourth button" - } + "button_4": "Fourth button", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6" } } From 557e585e561f1c907e84ab876db0c8d287f1cf73 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Nov 2019 22:31:22 +0100 Subject: [PATCH 1377/3953] deCONZ - Support creating battery sensor when reported (#27538) --- .../components/deconz/binary_sensor.py | 4 +- homeassistant/components/deconz/climate.py | 4 +- homeassistant/components/deconz/sensor.py | 67 +++++++++++++++++-- tests/components/deconz/test_sensor.py | 23 +++++++ 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index b81ecdc5164a75..1a4d9680c1e0c1 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -26,13 +26,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity_handler = DeconzEntityHandler(gateway) @callback - def async_add_sensor(sensors): + def async_add_sensor(sensors, new=True): """Add binary sensor from deCONZ.""" entities = [] for sensor in sensors: - if sensor.BINARY: + if new and sensor.BINARY: new_sensor = DeconzBinarySensor(sensor, gateway) entity_handler.add_entity(new_sensor) entities.append(new_sensor) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index b7a1ebce22ad48..ba1f1ce846af69 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -31,13 +31,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway = get_gateway_from_config_entry(hass, config_entry) @callback - def async_add_climate(sensors): + def async_add_climate(sensors, new=True): """Add climate devices from deCONZ.""" entities = [] for sensor in sensors: - if sensor.type in Thermostat.ZHATYPE: + if new and sensor.type in Thermostat.ZHATYPE: entities.append(DeconzThermostat(sensor, gateway)) async_add_entities(entities, True) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index cc3f3de3170c66..3a3dbceb46bd6b 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -3,7 +3,10 @@ from homeassistant.const import ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice @@ -25,21 +28,23 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway = get_gateway_from_config_entry(hass, config_entry) batteries = set() + battery_handler = DeconzBatteryHandler(gateway) entity_handler = DeconzEntityHandler(gateway) @callback - def async_add_sensor(sensors): + def async_add_sensor(sensors, new=True): """Add sensors from deCONZ. Create DeconzEvent if part of ZHAType list. Create DeconzSensor if not a ZHAType and not a binary sensor. Create DeconzBattery if sensor has a battery attribute. + If new is false it means an existing sensor has got a battery state reported. """ entities = [] for sensor in sensors: - if sensor.type in Switch.ZHATYPE: + if new and sensor.type in Switch.ZHATYPE: if gateway.option_allow_clip_sensor or not sensor.type.startswith( "CLIP" @@ -48,7 +53,7 @@ def async_add_sensor(sensors): hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) - elif not sensor.BINARY and sensor.type not in Thermostat.ZHATYPE: + elif new and not sensor.BINARY and sensor.type not in Thermostat.ZHATYPE: new_sensor = DeconzSensor(sensor, gateway) entity_handler.add_entity(new_sensor) @@ -59,6 +64,9 @@ def async_add_sensor(sensors): if new_battery.unique_id not in batteries: batteries.add(new_battery.unique_id) entities.append(new_battery) + battery_handler.remove_tracker(sensor) + else: + battery_handler.create_tracker(sensor) async_add_entities(entities, True) @@ -176,3 +184,54 @@ def device_state_attributes(self): attr[ATTR_EVENT_ID] = event.event_id return attr + + +class DeconzSensorStateTracker: + """Track sensors without a battery state and signal when battery state exist.""" + + def __init__(self, sensor, gateway): + """Set up tracker.""" + self.sensor = sensor + self.gateway = gateway + sensor.register_async_callback(self.async_update_callback) + + @callback + def close(self): + """Clean up tracker.""" + self.sensor.remove_callback(self.async_update_callback) + self.gateway = None + self.sensor = None + + @callback + def async_update_callback(self): + """Sensor state updated.""" + if "battery" in self.sensor.changed_keys: + async_dispatcher_send( + self.gateway.hass, + self.gateway.async_signal_new_device(NEW_SENSOR), + [self.sensor], + False, + ) + + +class DeconzBatteryHandler: + """Creates and stores trackers for sensors without a battery state.""" + + def __init__(self, gateway): + """Set up battery handler.""" + self.gateway = gateway + self._trackers = set() + + @callback + def create_tracker(self, sensor): + """Create new tracker for battery state.""" + self._trackers.add(DeconzSensorStateTracker(sensor, self.gateway)) + + @callback + def remove_tracker(self, sensor): + """Remove tracker of battery state.""" + for tracker in self._trackers: + if sensor == tracker.sensor: + tracker.close() + self._trackers.remove(tracker) + break diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 928e527dd0706e..7b6ae41086b8a0 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -233,3 +233,26 @@ async def test_add_new_sensor(hass): light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" + + +async def test_add_battery_later(hass): + """Test that a sensor without an initial battery state creates a battery sensor once state exist.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = {"1": deepcopy(SENSORS["3"])} + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + remote = gateway.api.sensors["1"] + assert len(gateway.deconz_ids) == 0 + assert len(gateway.events) == 1 + assert len(remote._async_callbacks) == 2 + + remote.async_update({"config": {"battery": 50}}) + await hass.async_block_till_done() + + assert len(gateway.deconz_ids) == 1 + assert len(gateway.events) == 1 + assert len(remote._async_callbacks) == 2 + + battery_sensor = hass.states.get("sensor.switch_1_battery_level") + assert battery_sensor is not None From 21d48218aa8a73749f0d051ac587887bebf23be9 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 1 Nov 2019 16:41:26 -0500 Subject: [PATCH 1378/3953] Use server-specific unique_ids for Plex media_players (#28447) --- homeassistant/components/plex/media_player.py | 27 ++++++++++++++++--- homeassistant/components/plex/server.py | 3 ++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 32bf7b65fff6c7..4c32c1e6376de1 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -6,7 +6,7 @@ import plexapi.exceptions import requests.exceptions -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -30,6 +30,7 @@ ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.util import dt as dt_util from .const import ( @@ -56,10 +57,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex media_player from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] + registry = await async_get_registry(hass) def async_new_media_players(new_entities): _async_add_entities( - hass, config_entry, async_add_entities, server_id, new_entities + hass, registry, config_entry, async_add_entities, server_id, new_entities ) unsub = async_dispatcher_connect( @@ -70,7 +72,7 @@ def async_new_media_players(new_entities): @callback def _async_add_entities( - hass, config_entry, async_add_entities, server_id, new_entities + hass, registry, config_entry, async_add_entities, server_id, new_entities ): """Set up Plex media_player entities.""" entities = [] @@ -79,6 +81,19 @@ def _async_add_entities( plex_mp = PlexMediaPlayer(plexserver, **entity_params) entities.append(plex_mp) + # Migration to per-server unique_ids + old_entity_id = registry.async_get_entity_id( + MP_DOMAIN, PLEX_DOMAIN, plex_mp.machine_identifier + ) + if old_entity_id is not None: + new_unique_id = f"{server_id}:{plex_mp.machine_identifier}" + _LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + plex_mp.machine_identifier, + new_unique_id, + ) + registry.async_update_entity(old_entity_id, new_unique_id=new_unique_id) + async_add_entities(entities, True) @@ -126,6 +141,7 @@ def __init__(self, plex_server, device, session=None): async def async_added_to_hass(self): """Run when about to be added to hass.""" server_id = self.plex_server.machine_identifier + unsub = async_dispatcher_connect( self.hass, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id), @@ -315,6 +331,11 @@ def should_poll(self): @property def unique_id(self): """Return the id of this plex client.""" + return f"{self.plex_server.machine_identifier}:{self._machine_identifier}" + + @property + def machine_identifier(self): + """Return the Plex-provided identifier of this plex client.""" return self._machine_identifier @property diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index e6f77a310f1727..28380e714ac790 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -94,9 +94,10 @@ def _connect_with_url(): def refresh_entity(self, machine_identifier, device, session): """Forward refresh dispatch to media_player.""" + unique_id = f"{self.machine_identifier}:{machine_identifier}" dispatcher_send( self._hass, - PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(machine_identifier), + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id), device, session, ) From 6cc947abbfd771cbc480916bf6bf88f1c0ab673e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Nov 2019 23:06:29 +0100 Subject: [PATCH 1379/3953] deCONZ - Add Hue dimmer gen1 (rwl020) support to device triggers(#28450) --- homeassistant/components/deconz/device_trigger.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 919061d9ad8f6b..30fbfdd05ae12d 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -65,7 +65,8 @@ CONF_SIDE_6 = "side_6" -HUE_DIMMER_REMOTE_MODEL = "RWL021" +HUE_DIMMER_REMOTE_MODEL_GEN1 = "RWL020" +HUE_DIMMER_REMOTE_MODEL_GEN2 = "RWL021" HUE_DIMMER_REMOTE = { (CONF_SHORT_PRESS, CONF_TURN_ON): 1000, (CONF_SHORT_RELEASE, CONF_TURN_ON): 1002, @@ -238,7 +239,8 @@ } REMOTES = { - HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, + HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE, + HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER, TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH, From c0e1b97119ff2f128c73cfc7e77357a3b0fc51a4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Nov 2019 23:36:23 +0100 Subject: [PATCH 1380/3953] deCONZ - Improve discovery logging (#28452) --- homeassistant/components/deconz/config_flow.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 5ede8e715b9471..b9a299230ad85d 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -13,6 +13,7 @@ from homeassistant.helpers import aiohttp_client from .const import ( + _LOGGER, CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, @@ -176,6 +177,8 @@ async def async_step_ssdp(self, discovery_info): uuid = discovery_info[ATTR_UUID].replace("uuid:", "") + _LOGGER.debug("deCONZ gateway discovered (%s)", uuid) + for entry in self.hass.config_entries.async_entries(DOMAIN): if uuid == entry.data.get(CONF_UUID): return await self._update_entry(entry, discovery_info[CONF_HOST]) From 6655b7db2c158297bcc2f8e5a1f708188ab6bd2a Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 1 Nov 2019 23:53:42 +0100 Subject: [PATCH 1381/3953] Add scene.create service (#28300) * Initial commit * Fix scene.create --- .../components/homeassistant/scene.py | 21 ++++++++++ homeassistant/components/scene/services.yaml | 14 +++++++ tests/components/homeassistant/test_scene.py | 39 +++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 39b04f6d3eacd1..45560d30edbb30 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -54,6 +54,8 @@ def _convert_states(states): return result +CONF_SCENE_ID = "scene_id" + STATES_SCHEMA = vol.All(dict, _convert_states) PLATFORM_SCHEMA = vol.Schema( @@ -72,7 +74,12 @@ def _convert_states(states): extra=vol.ALLOW_EXTRA, ) +CREATE_SCENE_SCHEMA = vol.Schema( + {vol.Required(CONF_SCENE_ID): cv.slug, vol.Required(CONF_ENTITIES): STATES_SCHEMA} +) + SERVICE_APPLY = "apply" +SERVICE_CREATE = "create" SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) _LOGGER = logging.getLogger(__name__) @@ -129,6 +136,20 @@ async def apply_service(call): vol.Schema({vol.Required(CONF_ENTITIES): STATES_SCHEMA}), ) + async def create_service(call): + """Create a scene.""" + scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], call.data[CONF_ENTITIES]) + entity_id = f"{SCENE_DOMAIN}.{scene_config.name}" + if hass.states.get(entity_id) is not None: + _LOGGER.warning("The scene %s already exists", entity_id) + return + + async_add_entities([HomeAssistantScene(hass, scene_config)]) + + hass.services.async_register( + SCENE_DOMAIN, SERVICE_CREATE, create_service, CREATE_SCENE_SCHEMA + ) + def _process_scenes_config(hass, async_add_entities, config): """Process multiple scenes and add them.""" diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index 0f1e7103aaf6fa..9cf1b9010a8048 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -20,3 +20,17 @@ apply: light.ceiling: state: "on" brightness: 80 + +create: + description: Creates a new scene. + fields: + scene_id: + description: The entity_id of the new scene. + example: all_lights + entities: + description: The entities to control with the scene. + example: + light.tv_back_light: "on" + light.ceiling: + state: "on" + brightness: 200 diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index c7c3f2bc5d50f7..08e40e23d12ad4 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -51,3 +51,42 @@ async def test_apply_service(hass): state = hass.states.get("light.bed_light") assert state.state == "on" assert state.attributes["brightness"] == 50 + + +async def test_create_service(hass, caplog): + """Test the create service.""" + assert await async_setup_component(hass, "scene", {}) + assert hass.states.get("scene.hallo") is None + + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo", + "entities": {"light.bed_light": {"state": "on", "brightness": 50}}, + }, + blocking=True, + ) + + await hass.async_block_till_done() + scene = hass.states.get("scene.hallo") + assert scene is not None + assert scene.domain == "scene" + assert scene.name == "hallo" + assert scene.state == "scening" + assert scene.attributes.get("entity_id") == ["light.bed_light"] + + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo", + "entities": {"light.bed_light": {"state": "on", "brightness": 50}}, + }, + blocking=True, + ) + + await hass.async_block_till_done() + assert "The scene scene.hallo already exists" in caplog.text + assert hass.states.get("scene.hallo") is not None + assert hass.states.get("scene.hallo_2") is None From 50affdf9530630b259f0d95ee7332f92d58d7a0d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Nov 2019 17:21:50 -0700 Subject: [PATCH 1382/3953] Also install after_deps (#28453) --- homeassistant/requirements.py | 8 ++++++-- tests/test_requirements.py | 13 +++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 74469ef2fcd619..a0eec0f442b782 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -48,8 +48,12 @@ async def async_get_integration_with_requirements( hass, integration.domain, integration.requirements ) - for dependency in integration.dependencies: - await async_get_integration_with_requirements(hass, dependency) + deps = integration.dependencies + (integration.after_dependencies or []) + + if deps: + await asyncio.gather( + *[async_get_integration_with_requirements(hass, dep) for dep in deps] + ) return integration diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 548ea645360d94..782b4386552964 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -115,12 +115,19 @@ async def test_get_integration_with_requirements(hass): mock_integration( hass, MockModule("test_component_dep", requirements=["test-comp-dep==1.0.0"]) ) + mock_integration( + hass, + MockModule( + "test_component_after_dep", requirements=["test-comp-after-dep==1.0.0"] + ), + ) mock_integration( hass, MockModule( "test_component", requirements=["test-comp==1.0.0"], dependencies=["test_component_dep"], + partial_manifest={"after_dependencies": ["test_component_after_dep"]}, ), ) @@ -136,13 +143,15 @@ async def test_get_integration_with_requirements(hass): assert integration assert integration.domain == "test_component" - assert len(mock_is_installed.mock_calls) == 2 + assert len(mock_is_installed.mock_calls) == 3 assert mock_is_installed.mock_calls[0][1][0] == "test-comp==1.0.0" assert mock_is_installed.mock_calls[1][1][0] == "test-comp-dep==1.0.0" + assert mock_is_installed.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" - assert len(mock_inst.mock_calls) == 2 + assert len(mock_inst.mock_calls) == 3 assert mock_inst.mock_calls[0][1][0] == "test-comp==1.0.0" assert mock_inst.mock_calls[1][1][0] == "test-comp-dep==1.0.0" + assert mock_inst.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" async def test_install_with_wheels_index(hass): From ad4a960ed2296e4a58713e1d3cfba2da27a3e6ed Mon Sep 17 00:00:00 2001 From: Mister Wil <1091741+MisterWil@users.noreply.github.com> Date: Fri, 1 Nov 2019 17:28:50 -0700 Subject: [PATCH 1383/3953] Change Abode cache file path, add cache path to config flow (#28389) * Changed cache file path * Cache file naming scheme matches original * Restart tests * Adding cache path to config_flow.py * Moved DEFAULT_CACHEDB to consts file * Use correct cache path * Linting issues --- homeassistant/components/abode/__init__.py | 4 +--- homeassistant/components/abode/config_flow.py | 7 +++++-- homeassistant/components/abode/const.py | 2 ++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 6a72ac64145485..76c14d7917ff72 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -23,14 +23,12 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity -from .const import ATTRIBUTION, DOMAIN +from .const import ATTRIBUTION, DOMAIN, DEFAULT_CACHEDB _LOGGER = logging.getLogger(__name__) CONF_POLLING = "polling" -DEFAULT_CACHEDB = "./abodepy_cache.pickle" - SERVICE_SETTINGS = "change_setting" SERVICE_CAPTURE_IMAGE = "capture_image" SERVICE_TRIGGER = "trigger_quick_action" diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index d8d914f7998d63..bf48e4546b301d 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from .const import DOMAIN # pylint: disable=W0611 +from .const import DOMAIN, DEFAULT_CACHEDB # pylint: disable=W0611 CONF_POLLING = "polling" @@ -42,9 +42,12 @@ async def async_step_user(self, user_input=None): username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] polling = user_input.get(CONF_POLLING, False) + cache = self.hass.config.path(DEFAULT_CACHEDB) try: - await self.hass.async_add_executor_job(Abode, username, password, True) + await self.hass.async_add_executor_job( + Abode, username, password, True, True, True, cache + ) except (AbodeException, ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Abode: %s", str(ex)) diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py index 35e74e154cffe4..092843ba2120b5 100644 --- a/homeassistant/components/abode/const.py +++ b/homeassistant/components/abode/const.py @@ -1,3 +1,5 @@ """Constants for the Abode Security System component.""" DOMAIN = "abode" ATTRIBUTION = "Data provided by goabode.com" + +DEFAULT_CACHEDB = "abodepy_cache.pickle" From 15900094a1615b877b2c755098db73b596f9455b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Nov 2019 01:30:37 +0100 Subject: [PATCH 1384/3953] Update MQTT sensor test (#28449) --- tests/components/mqtt/test_sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index f46d8ab7f71fc0..cd55a08482d5f7 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -268,12 +268,12 @@ async def test_setting_sensor_attribute_via_legacy_mqtt_json_message(hass, mqtt_ "name": "test", "state_topic": "test-topic", "unit_of_measurement": "fav unit", - "json_attributes": "val", + "json_attributes_topic": "test-attributes-topic", } }, ) - async_fire_mqtt_message(hass, "test-topic", '{ "val": "100" }') + async_fire_mqtt_message(hass, "test-attributes-topic", '{ "val": "100" }') state = hass.states.get("sensor.test") assert state.attributes.get("val") == "100" @@ -290,12 +290,12 @@ async def test_update_with_legacy_json_attrs_not_dict(hass, mqtt_mock, caplog): "name": "test", "state_topic": "test-topic", "unit_of_measurement": "fav unit", - "json_attributes": "val", + "json_attributes_topic": "test-attributes-topic", } }, ) - async_fire_mqtt_message(hass, "test-topic", '[ "list", "of", "things"]') + async_fire_mqtt_message(hass, "test-attributes-topic", '[ "list", "of", "things"]') state = hass.states.get("sensor.test") assert state.attributes.get("val") is None @@ -313,12 +313,12 @@ async def test_update_with_legacy_json_attrs_bad_JSON(hass, mqtt_mock, caplog): "name": "test", "state_topic": "test-topic", "unit_of_measurement": "fav unit", - "json_attributes": "val", + "json_attributes_topic": "test-attributes-topic", } }, ) - async_fire_mqtt_message(hass, "test-topic", "This is not JSON") + async_fire_mqtt_message(hass, "test-attributes-topic", "This is not JSON") state = hass.states.get("sensor.test") assert state.attributes.get("val") is None From 4863face6908fb4a942695aaa17feb58bad823e5 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 2 Nov 2019 00:31:48 +0000 Subject: [PATCH 1385/3953] [ci skip] Translation update --- .../components/abode/.translations/bg.json | 22 ++++++++++ .../components/adguard/.translations/bg.json | 2 + .../components/airly/.translations/bg.json | 22 ++++++++++ .../alarm_control_panel/.translations/bg.json | 11 +++++ .../components/almond/.translations/bg.json | 9 ++++ .../components/almond/.translations/de.json | 9 ++++ .../components/almond/.translations/it.json | 9 ++++ .../components/almond/.translations/ko.json | 9 ++++ .../components/almond/.translations/no.json | 9 ++++ .../components/axis/.translations/bg.json | 1 + .../binary_sensor/.translations/bg.json | 2 + .../cert_expiry/.translations/bg.json | 4 +- .../coolmaster/.translations/bg.json | 23 ++++++++++ .../components/cover/.translations/bg.json | 20 +++++++++ .../components/cover/.translations/ca.json | 4 +- .../components/cover/.translations/de.json | 6 +++ .../components/cover/.translations/it.json | 12 +++++- .../components/cover/.translations/ko.json | 8 ++++ .../components/cover/.translations/no.json | 8 ++++ .../cover/.translations/zh-Hant.json | 4 +- .../components/deconz/.translations/bg.json | 2 + .../components/deconz/.translations/en.json | 18 +------- .../device_tracker/.translations/bg.json | 8 ++++ .../device_tracker/.translations/it.json | 4 +- .../components/glances/.translations/bg.json | 37 ++++++++++++++++ .../huawei_lte/.translations/bg.json | 39 +++++++++++++++++ .../huawei_lte/.translations/it.json | 13 ++++-- .../components/lock/.translations/bg.json | 13 ++++++ .../media_player/.translations/bg.json | 11 +++++ .../media_player/.translations/it.json | 4 +- .../components/neato/.translations/bg.json | 27 ++++++++++++ .../opentherm_gw/.translations/bg.json | 34 +++++++++++++++ .../components/plex/.translations/bg.json | 17 ++++++++ .../components/sensor/.translations/bg.json | 26 +++++++++++ .../components/solarlog/.translations/bg.json | 21 +++++++++ .../components/soma/.translations/bg.json | 23 ++++++++++ .../components/somfy/.translations/bg.json | 5 +++ .../components/somfy/.translations/it.json | 5 +++ .../transmission/.translations/bg.json | 43 +++++++++++++++++++ .../components/unifi/.translations/bg.json | 5 +++ .../components/withings/.translations/bg.json | 7 +++ .../components/withings/.translations/it.json | 1 + .../components/zha/.translations/bg.json | 4 ++ 43 files changed, 533 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/abode/.translations/bg.json create mode 100644 homeassistant/components/airly/.translations/bg.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/bg.json create mode 100644 homeassistant/components/almond/.translations/bg.json create mode 100644 homeassistant/components/almond/.translations/de.json create mode 100644 homeassistant/components/almond/.translations/it.json create mode 100644 homeassistant/components/almond/.translations/ko.json create mode 100644 homeassistant/components/almond/.translations/no.json create mode 100644 homeassistant/components/coolmaster/.translations/bg.json create mode 100644 homeassistant/components/cover/.translations/bg.json create mode 100644 homeassistant/components/device_tracker/.translations/bg.json create mode 100644 homeassistant/components/glances/.translations/bg.json create mode 100644 homeassistant/components/huawei_lte/.translations/bg.json create mode 100644 homeassistant/components/lock/.translations/bg.json create mode 100644 homeassistant/components/media_player/.translations/bg.json create mode 100644 homeassistant/components/neato/.translations/bg.json create mode 100644 homeassistant/components/opentherm_gw/.translations/bg.json create mode 100644 homeassistant/components/sensor/.translations/bg.json create mode 100644 homeassistant/components/solarlog/.translations/bg.json create mode 100644 homeassistant/components/soma/.translations/bg.json create mode 100644 homeassistant/components/transmission/.translations/bg.json diff --git a/homeassistant/components/abode/.translations/bg.json b/homeassistant/components/abode/.translations/bg.json new file mode 100644 index 00000000000000..29e3f342cf4d53 --- /dev/null +++ b/homeassistant/components/abode/.translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Abode." + }, + "error": { + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Abode.", + "identifier_exists": "\u041f\u0440\u043e\u0444\u0438\u043b\u044a\u0442 \u0435 \u0432\u0435\u0447\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d.", + "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "E-mail \u0430\u0434\u0440\u0435\u0441" + }, + "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0412\u0430\u0448\u0430\u0442\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0432\u0445\u043e\u0434 \u0432 Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/bg.json b/homeassistant/components/adguard/.translations/bg.json index 826244544b5766..398927d370a16d 100644 --- a/homeassistant/components/adguard/.translations/bg.json +++ b/homeassistant/components/adguard/.translations/bg.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "\u0422\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438\u0437\u0438\u0441\u043a\u0432\u0430 AdGuard Home {minimal_version} \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430 {minimal_version}, \u0438\u043c\u0430\u0442\u0435 {current_version}. \u041c\u043e\u043b\u044f, \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 \u0434\u043e\u0431\u0430\u0432\u043a\u0430 \u0437\u0430 Hass.io AdGuard Home.", + "adguard_home_outdated": "\u0422\u0430\u0437\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0438\u0437\u0438\u0441\u043a\u0432\u0430 AdGuard Home {minimal_version} \u0438\u043b\u0438 \u043f\u043e-\u043d\u043e\u0432\u0430 {minimal_version}, \u0438\u043c\u0430\u0442\u0435 {current_version}.", "existing_instance_updated": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f.", "single_instance_allowed": "\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 AdGuard Home." }, diff --git a/homeassistant/components/airly/.translations/bg.json b/homeassistant/components/airly/.translations/bg.json new file mode 100644 index 00000000000000..c91190d9852be2 --- /dev/null +++ b/homeassistant/components/airly/.translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API \u043a\u043b\u044e\u0447\u044a\u0442 \u043d\u0435 \u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d.", + "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430.", + "wrong_location": "\u0412 \u0442\u0430\u0437\u0438 \u043e\u0431\u043b\u0430\u0441\u0442 \u043d\u044f\u043c\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u0442\u0435\u043b\u043d\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447 \u0437\u0430 Airly", + "latitude": "\u0428\u0438\u0440\u0438\u043d\u0430", + "longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430", + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0430 \u0432\u044a\u0437\u0434\u0443\u0445\u0430 Airly \u0417\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 \u043a\u043b\u044e\u0447 \u0437\u0430 API, \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/bg.json b/homeassistant/components/alarm_control_panel/.translations/bg.json new file mode 100644 index 00000000000000..29700793770a78 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/bg.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u0441\u044a\u0441\u0442\u0432\u0438\u0435", + "arm_home": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c \u0432\u043a\u044a\u0449\u0438", + "arm_night": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u043d\u043e\u0449\u0435\u043d \u0440\u0435\u0436\u0438\u043c", + "disarm": "\u0414\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0439 {entity_name}", + "trigger": "\u0417\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0435 {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/bg.json b/homeassistant/components/almond/.translations/bg.json new file mode 100644 index 00000000000000..da5571ad0294b7 --- /dev/null +++ b/homeassistant/components/almond/.translations/bg.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Almond \u0430\u043a\u0430\u0443\u043d\u0442.", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/de.json b/homeassistant/components/almond/.translations/de.json new file mode 100644 index 00000000000000..4e2816cb0011eb --- /dev/null +++ b/homeassistant/components/almond/.translations/de.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Sie k\u00f6nnen nur ein Almond-Konto konfigurieren.", + "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/it.json b/homeassistant/components/almond/.translations/it.json new file mode 100644 index 00000000000000..a7e207e899bea4 --- /dev/null +++ b/homeassistant/components/almond/.translations/it.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account Almond.", + "cannot_connect": "Impossibile connettersi al server Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ko.json b/homeassistant/components/almond/.translations/ko.json new file mode 100644 index 00000000000000..9440242ebbcc62 --- /dev/null +++ b/homeassistant/components/almond/.translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Almond \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/no.json b/homeassistant/components/almond/.translations/no.json new file mode 100644 index 00000000000000..37888debe785e6 --- /dev/null +++ b/homeassistant/components/almond/.translations/no.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan bare konfigurere en Almond konto.", + "cannot_connect": "Kan ikke koble til Almond-serveren." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/bg.json b/homeassistant/components/axis/.translations/bg.json index b8c5bf7609fce2..c56822ba5a47d7 100644 --- a/homeassistant/components/axis/.translations/bg.json +++ b/homeassistant/components/axis/.translations/bg.json @@ -12,6 +12,7 @@ "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u043d\u0430\u043b\u0438\u0447\u043d\u043e", "faulty_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438" }, + "flow_title": "Axis \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/.translations/bg.json b/homeassistant/components/binary_sensor/.translations/bg.json index 9b9741b960196c..373866ecd8c2f0 100644 --- a/homeassistant/components/binary_sensor/.translations/bg.json +++ b/homeassistant/components/binary_sensor/.translations/bg.json @@ -53,6 +53,7 @@ "hot": "{entity_name} \u0441\u0435 \u0441\u0442\u043e\u043f\u043b\u0438", "light": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", "locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "moist": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0432\u043b\u0430\u0436\u0435\u043d", "moist\u00a7": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0432\u043b\u0430\u0436\u0435\u043d", "motion": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", "moving": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0441\u0443\u0445", "not_moving": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u0441\u0435 \u0434\u0432\u0438\u0436\u0438", "not_occupied": "{entity_name} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0435 \u0437\u0430\u0435\u0442", + "not_opened": "{entity_name} \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", "not_plugged_in": "{entity_name} \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", "not_powered": "{entity_name} \u043d\u0435 \u0441\u0435 \u0437\u0430\u0445\u0440\u0430\u043d\u0432\u0430", "not_present": "{entity_name} \u043d\u0435 \u043f\u0440\u0438\u0441\u044a\u0441\u0442\u0432\u0430", diff --git a/homeassistant/components/cert_expiry/.translations/bg.json b/homeassistant/components/cert_expiry/.translations/bg.json index 7c82ef8b9ba8f0..a4a36cb04dcaa2 100644 --- a/homeassistant/components/cert_expiry/.translations/bg.json +++ b/homeassistant/components/cert_expiry/.translations/bg.json @@ -4,10 +4,12 @@ "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" }, "error": { + "certificate_error": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d", "certificate_fetch_failed": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043c\u0438\u0437\u0432\u043b\u0435\u0447\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442", "connection_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0442\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441", "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "resolve_failed": "\u0422\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" + "resolve_failed": "\u0422\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d", + "wrong_host": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u043d\u0435 \u0441\u044a\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0430 \u043d\u0430 \u0438\u043c\u0435\u0442\u043e \u043d\u0430 \u0445\u043e\u0441\u0442\u0430" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/bg.json b/homeassistant/components/coolmaster/.translations/bg.json new file mode 100644 index 00000000000000..9e484f5d38c077 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 CoolMasterNet. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430.", + "no_units": "\u041d\u0435 \u0431\u044f\u0445\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u0447\u043d\u0438/\u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u043d\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u0438\u044f CoolMasterNet \u0430\u0434\u0440\u0435\u0441." + }, + "step": { + "user": { + "data": { + "cool": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", + "dry": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u0438\u0437\u0441\u0443\u0448\u0430\u0432\u0430\u043d\u0435", + "fan_only": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0442\u043e\u0440", + "heat": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", + "heat_cool": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435/\u043e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", + "host": "\u0410\u0434\u0440\u0435\u0441", + "off": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0432\u043e\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/bg.json b/homeassistant/components/cover/.translations/bg.json new file mode 100644 index 00000000000000..4651fb4aebecf7 --- /dev/null +++ b/homeassistant/components/cover/.translations/bg.json @@ -0,0 +1,20 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u0435 \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "is_closing": "{entity_name} \u0441\u0435 \u0437\u0430\u0442\u0432\u0430\u0440\u044f", + "is_open": "{entity_name} \u0435 \u043e\u0442\u0432\u043e\u0440\u0435\u043d", + "is_opening": "{entity_name} \u0441\u0435 \u043e\u0442\u0432\u0430\u0440\u044f", + "is_position": "\u0422\u0435\u043a\u0443\u0449\u0430\u0442\u0430 \u043f\u043e\u0437\u0438\u0446\u0438\u044f \u043d\u0430 {entity_name} \u0435", + "is_tilt_position": "\u0422\u0435\u043a\u0443\u0449\u0430\u0442\u0430 \u043f\u043e\u0437\u0438\u0446\u0438\u044f \u043d\u0430 \u043d\u0430\u043a\u043b\u043e\u043d\u0430 \u043d\u0430 {entity_name} \u0435" + }, + "trigger_type": { + "closed": "{entity_name} \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", + "closing": "{entity_name} \u0441\u0435 \u0437\u0430\u0442\u0432\u0430\u0440\u044f", + "opened": "{entity_name} \u0435 \u043e\u0442\u0432\u043e\u0440\u0435\u043d", + "opening": "{entity_name} \u0441\u0435 \u043e\u0442\u0432\u0430\u0440\u044f", + "position": "{entity_name} \u043f\u0440\u043e\u043c\u0435\u043d\u0438 \u043f\u043e\u0437\u0438\u0446\u0438\u044f\u0442\u0430 \u0441\u0438", + "tilt_position": "{entity_name} \u043f\u0440\u043e\u043c\u0435\u043d\u0438 \u043d\u0430\u043a\u043b\u043e\u043d\u0430 \u0441\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ca.json b/homeassistant/components/cover/.translations/ca.json index da86e4960c0576..b2c2371db5c9f5 100644 --- a/homeassistant/components/cover/.translations/ca.json +++ b/homeassistant/components/cover/.translations/ca.json @@ -12,7 +12,9 @@ "closed": "{entity_name} tancat/da", "closing": "{entity_name} tancant-se", "opened": "{entity_name} s'ha obert", - "opening": "{entity_name} obrint-se" + "opening": "{entity_name} obrint-se", + "position": "Canvia la posici\u00f3 de {entity_name}", + "tilt_position": "Canvia la inclinaci\u00f3 de {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/de.json b/homeassistant/components/cover/.translations/de.json index e9ed497ccc2142..ba692f15e47891 100644 --- a/homeassistant/components/cover/.translations/de.json +++ b/homeassistant/components/cover/.translations/de.json @@ -5,6 +5,12 @@ "is_closing": "{entity_name} wird geschlossen", "is_open": "{entity_name} ist offen", "is_opening": "{entity_name} wird ge\u00f6ffnet" + }, + "trigger_type": { + "closed": "{entity_name} geschlossen", + "closing": "{entity_name} wird geschlossen", + "opened": "{entity_name} ge\u00f6ffnet", + "opening": "{entity_name} wird ge\u00f6ffnet" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/it.json b/homeassistant/components/cover/.translations/it.json index 6a25c0f3f2ffe8..bc9413d4a006c7 100644 --- a/homeassistant/components/cover/.translations/it.json +++ b/homeassistant/components/cover/.translations/it.json @@ -4,7 +4,17 @@ "is_closed": "{entity_name} \u00e8 chiuso", "is_closing": "{entity_name} si sta chiudendo", "is_open": "{entity_name} \u00e8 aperto", - "is_opening": "{entity_name} si sta aprendo" + "is_opening": "{entity_name} si sta aprendo", + "is_position": "La posizione attuale di {entity_name} \u00e8", + "is_tilt_position": "La posizione d'inclinazione attuale di {entity_name} \u00e8" + }, + "trigger_type": { + "closed": "{entity_name} chiuso", + "closing": "{entity_name} in chiusura", + "opened": "{entity_name} aperto", + "opening": "{entity_name} in apertura", + "position": "{entity_name} cambiamenti della posizione", + "tilt_position": "{entity_name} cambiamenti della posizione d'inclinazione" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json index 48f7ba17532d27..4839f8f034f758 100644 --- a/homeassistant/components/cover/.translations/ko.json +++ b/homeassistant/components/cover/.translations/ko.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4", "is_position": "{entity_name} \uac1c\ud3d0 \uc704\uce58\ub294", "is_tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\ub294" + }, + "trigger_type": { + "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", + "closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911", + "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9bc", + "opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911", + "position": "{entity_name} \uac1c\ud3d0 \uc704\uce58 \ubcc0\ud654", + "tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30 \ubcc0\ud654" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/no.json b/homeassistant/components/cover/.translations/no.json index 901ececec1fa4f..cc045e43624f59 100644 --- a/homeassistant/components/cover/.translations/no.json +++ b/homeassistant/components/cover/.translations/no.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} \u00e5pnes", "is_position": "{entity_name}-posisjonen er", "is_tilt_position": "{entity_name} vippeposisjon er" + }, + "trigger_type": { + "closed": "{entity_name} lukket", + "closing": "{entity_name} lukkes", + "opened": "{entity_name} \u00e5pnet", + "opening": "{entity_name} \u00e5pning", + "position": "{entity_name} posisjon endringer", + "tilt_position": "{entity_name} endringer i vippeposisjon" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/zh-Hant.json b/homeassistant/components/cover/.translations/zh-Hant.json index 8197d2146aedcd..f2880a72e614e2 100644 --- a/homeassistant/components/cover/.translations/zh-Hant.json +++ b/homeassistant/components/cover/.translations/zh-Hant.json @@ -5,8 +5,8 @@ "is_closing": "{entity_name} \u6b63\u5728\u95dc\u9589", "is_open": "{entity_name} \u5df2\u958b\u555f", "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f", - "is_position": "{entity_name} \u4f4d\u7f6e\u70ba", - "is_tilt_position": "{entity_name} \u6a19\u984c\u4f4d\u7f6e\u70ba" + "is_position": "\u76ee\u524d {entity_name} \u4f4d\u7f6e\u70ba", + "is_tilt_position": "\u76ee\u524d {entity_name} \u6a19\u984c\u4f4d\u7f6e\u70ba" }, "trigger_type": { "closed": "{entity_name} \u5df2\u95dc\u9589", diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index c9963e496238fd..e8dc5845c134a4 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -11,6 +11,7 @@ "error": { "no_key": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u043f\u043e\u043b\u0443\u0447\u0438 API \u043a\u043b\u044e\u0447" }, + "flow_title": "deCONZ Zigbee \u0448\u043b\u044e\u0437 ({host})", "step": { "hassio_confirm": { "data": { @@ -64,6 +65,7 @@ "remote_button_quadruple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0447\u0435\u0442\u0438\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", "remote_button_quintuple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0435\u0442\u043a\u0440\u0430\u0442\u043d\u043e", "remote_button_rotated": "\u0417\u0430\u0432\u044a\u0440\u0442\u044f\u043d \u0431\u0443\u0442\u043e\u043d \"{subtype}\"", + "remote_button_rotation_stopped": "\u0421\u043f\u0440\u044f \u0432\u044a\u0440\u0442\u0435\u043d\u0435\u0442\u043e \u043d\u0430 \u0431\u0443\u0442\u043e\u043d \"{subtype}\"", "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 6d9f7236d31afd..e9c64ffe5faa64 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -55,12 +55,6 @@ "left": "Left", "open": "Open", "right": "Right", - "side_1": "Side 1", - "side_2": "Side 2", - "side_3": "Side 3", - "side_4": "Side 4", - "side_5": "Side 5", - "side_6": "Side 6", "turn_off": "Turn off", "turn_on": "Turn on" }, @@ -75,17 +69,7 @@ "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_double_tap": "Device \"{subtype}\" double tapped", - "remote_awakened": "Device awakened", - "remote_falling": "Device in free fall", - "remote_gyro_activated": "Device shaken", - "remote_moved": "Device moved with \"{subtype}\" up", - "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", - "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", - "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", - "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", - "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", - "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" + "remote_gyro_activated": "Device shaken" } }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/bg.json b/homeassistant/components/device_tracker/.translations/bg.json new file mode 100644 index 00000000000000..471cbc6a53af2b --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/bg.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} \u0435 \u0443 \u0434\u043e\u043c\u0430", + "is_not_home": "{entity_name} \u043d\u0435 \u0435 \u0443 \u0434\u043e\u043c\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/it.json b/homeassistant/components/device_tracker/.translations/it.json index 3030410a97a334..e2d35296152f41 100644 --- a/homeassistant/components/device_tracker/.translations/it.json +++ b/homeassistant/components/device_tracker/.translations/it.json @@ -1,8 +1,8 @@ { "device_automation": { "condtion_type": { - "is_home": "{entity_name} \u00e8 a casa", - "is_not_home": "{entity_name} non \u00e8 a casa" + "is_home": "{entity_name} \u00e8 in casa", + "is_not_home": "{entity_name} non \u00e8 in casa" } } } \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/bg.json b/homeassistant/components/glances/.translations/bg.json new file mode 100644 index 00000000000000..8604dda565a32f --- /dev/null +++ b/homeassistant/components/glances/.translations/bg.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d." + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0430\u0434\u0440\u0435\u0441\u0430", + "wrong_version": "\u0412\u0435\u0440\u0441\u0438\u044f\u0442\u0430 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 (\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0438 \u0432\u0435\u0440\u0441\u0438\u0438: 2 \u0438\u043b\u0438 3)" + }, + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 SSL/TLS, \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u043a\u044a\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0442\u0430 Glances", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0442\u0430", + "version": "Glances API \u0432\u0435\u0440\u0441\u0438\u044f (2 \u0438\u043b\u0438 3)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0435\u0441\u0442\u043e\u0442\u0430 \u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u043f\u0446\u0438\u0438 \u0437\u0430 Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/bg.json b/homeassistant/components/huawei_lte/.translations/bg.json new file mode 100644 index 00000000000000..de5cbb32b79704 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/bg.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "connection_failed": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "incorrect_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430", + "incorrect_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "incorrect_username_or_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430", + "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0430\u0434\u0440\u0435\u0441", + "login_attempts_exceeded": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0438\u0442\u0435 \u043e\u043f\u0438\u0442\u0438 \u0437\u0430 \u0432\u043b\u0438\u0437\u0430\u043d\u0435 \u0441\u0430 \u043d\u0430\u0434\u0432\u0438\u0448\u0435\u043d\u0438. \u041c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e", + "response_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e", + "unknown_connection_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "url": "URL \u0410\u0434\u0440\u0435\u0441", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f \u0434\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e. \u041f\u043e\u0441\u043e\u0447\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u043d\u0435 \u0435 \u0437\u0430\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e, \u043d\u043e \u0434\u0430\u0432\u0430 \u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u043f\u043e\u0432\u0435\u0447\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0437\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435. \u041e\u0442 \u0434\u0440\u0443\u0433\u0430 \u0441\u0442\u0440\u0430\u043d\u0430, \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0438\u0440\u0430\u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0434\u043e\u0432\u0435\u0434\u0435 \u0434\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0441 \u0434\u043e\u0441\u0442\u044a\u043f\u0430 \u0434\u043e \u0443\u0435\u0431 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043e\u0442\u0432\u044a\u043d Home Assistant, \u0434\u043e\u043a\u0430\u0442\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0438 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0442\u043e.", + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 \u043d\u0430 SMS \u0438\u0437\u0432\u0435\u0441\u0442\u0438\u044f", + "track_new_devices": "\u041f\u0440\u043e\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u043e\u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/it.json b/homeassistant/components/huawei_lte/.translations/it.json index 4bfd3389745851..0be2ed234621a5 100644 --- a/homeassistant/components/huawei_lte/.translations/it.json +++ b/homeassistant/components/huawei_lte/.translations/it.json @@ -8,7 +8,10 @@ "incorrect_password": "Password errata", "incorrect_username": "Nome utente errato", "incorrect_username_or_password": "Nome utente o password errati", - "invalid_url": "URL non valido" + "invalid_url": "URL non valido", + "login_attempts_exceeded": "Superati i tentativi di accesso massimi, riprovare pi\u00f9 tardi", + "response_error": "Errore sconosciuto dal dispositivo", + "unknown_connection_error": "Errore sconosciuto durante la connessione al dispositivo" }, "step": { "user": { @@ -16,14 +19,18 @@ "password": "Password", "url": "URL", "username": "Nome utente" - } + }, + "description": "Immettere i dettagli di accesso al dispositivo. La specifica di nome utente e password \u00e8 facoltativa, ma abilita il supporto per altre funzionalit\u00e0 di integrazione. D'altra parte, l'uso di una connessione autorizzata pu\u00f2 causare problemi di accesso all'interfaccia Web del dispositivo dall'esterno di Home Assistant mentre l'integrazione \u00e8 attiva e viceversa.", + "title": "Configura Huawei LTE" } - } + }, + "title": "Huawei LTE" }, "options": { "step": { "init": { "data": { + "recipient": "Destinatari della notifica SMS", "track_new_devices": "Traccia nuovi dispositivi" } } diff --git a/homeassistant/components/lock/.translations/bg.json b/homeassistant/components/lock/.translations/bg.json new file mode 100644 index 00000000000000..0e77bcf10330d3 --- /dev/null +++ b/homeassistant/components/lock/.translations/bg.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "\u0417\u0430\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "open": "\u041e\u0442\u0432\u0430\u0440\u044f\u043d\u0435 \u043d\u0430 {entity_name}", + "unlock": "\u041e\u0442\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "is_unlocked": "{entity_name} \u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/bg.json b/homeassistant/components/media_player/.translations/bg.json new file mode 100644 index 00000000000000..f6c18cbe11916f --- /dev/null +++ b/homeassistant/components/media_player/.translations/bg.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u0435 \u043d\u0435\u0430\u043a\u0442\u0438\u0432\u0435\u043d", + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", + "is_paused": "{entity_name} \u0435 \u043d\u0430 \u043f\u0430\u0443\u0437\u0430", + "is_playing": "{entity_name} \u0432\u044a\u0437\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0436\u0434\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/it.json b/homeassistant/components/media_player/.translations/it.json index 52a9bb3f051909..93ab26d4585a55 100644 --- a/homeassistant/components/media_player/.translations/it.json +++ b/homeassistant/components/media_player/.translations/it.json @@ -1,9 +1,11 @@ { "device_automation": { "condition_type": { + "is_idle": "{entity_name} \u00e8 inattivo", "is_off": "{entity_name} \u00e8 spento", "is_on": "{entity_name} \u00e8 acceso", - "is_paused": "{entity_name} \u00e8 in pausa" + "is_paused": "{entity_name} \u00e8 in pausa", + "is_playing": "{entity_name} \u00e8 in esecuzione" } } } \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/bg.json b/homeassistant/components/neato/.translations/bg.json new file mode 100644 index 00000000000000..14bc601214e0f3 --- /dev/null +++ b/homeassistant/components/neato/.translations/bg.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438" + }, + "create_entry": { + "default": "\u0412\u0438\u0436\u0442\u0435 [Neato \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f]({docs_url})." + }, + "error": { + "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438", + "unexpected_error": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", + "vendor": "\u0414\u043e\u0441\u0442\u0430\u0432\u0447\u0438\u043a" + }, + "description": "\u0412\u0438\u0436\u0442\u0435 [Neato \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f]({docs_url}).", + "title": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 Neato \u0430\u043a\u0430\u0443\u043d\u0442" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/bg.json b/homeassistant/components/opentherm_gw/.translations/bg.json new file mode 100644 index 00000000000000..cd109579f64501 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/bg.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "already_configured": "\u0428\u043b\u044e\u0437\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440a \u043d\u0430 \u0448\u043b\u044e\u0437\u0430 \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430", + "serial_error": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435" + }, + "step": { + "init": { + "data": { + "device": "\u041f\u044a\u0442 \u0438\u043b\u0438 URL \u0430\u0434\u0440\u0435\u0441", + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u043f\u043e\u0434\u0430", + "id": "ID", + "name": "\u0418\u043c\u0435", + "precision": "\u041f\u0440\u0435\u0446\u0438\u0437\u043d\u043e\u0441\u0442 \u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430\u0442\u0430 \u043d\u0430 \u043a\u043b\u0438\u043c\u0430\u0442\u0430" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u043f\u043e\u0434\u0430", + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442" + }, + "description": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 OpenTherm Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/bg.json b/homeassistant/components/plex/.translations/bg.json index fb77e5da8cb666..9a2ffe299c8cf9 100644 --- a/homeassistant/components/plex/.translations/bg.json +++ b/homeassistant/components/plex/.translations/bg.json @@ -4,7 +4,9 @@ "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0432\u0435\u0447\u0435 \u0441\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", "already_configured": "\u0422\u043e\u0437\u0438 Plex \u0441\u044a\u0440\u0432\u044a\u0440 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", + "discovery_no_file": "\u041d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u0441\u0442\u0430\u0440 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u0444\u0430\u0439\u043b", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430", + "token_request_timeout": "\u0418\u0437\u0442\u0435\u0447\u0435 \u0432\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", "unknown": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0440\u0430\u0434\u0438 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "error": { @@ -31,6 +33,10 @@ "description": "\u041d\u0430\u043b\u0438\u0447\u043d\u0438 \u0441\u0430 \u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u044a\u0440\u0432\u044a\u0440\u0430, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u0438\u043d:", "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 Plex \u0441\u044a\u0440\u0432\u044a\u0440" }, + "start_website_auth": { + "description": "\u041f\u0440\u043e\u0434\u044a\u043b\u0436\u0435\u0442\u0435 \u0441 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 plex.tv.", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 Plex \u0441\u044a\u0440\u0432\u044a\u0440" + }, "user": { "data": { "manual_setup": "\u0420\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", @@ -41,5 +47,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0432\u0441\u0438\u0447\u043a\u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438", + "use_episode_art": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u043b\u0430\u043a\u0430\u0442 \u0437\u0430 \u0435\u043f\u0438\u0437\u043e\u0434\u0430" + }, + "description": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 Plex Media Players" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/bg.json b/homeassistant/components/sensor/.translations/bg.json new file mode 100644 index 00000000000000..fec708034863e2 --- /dev/null +++ b/homeassistant/components/sensor/.translations/bg.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "\u0422\u0435\u043a\u0443\u0449\u043e \u043d\u0438\u0432\u043e \u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u043d\u0430 {entity_name}", + "is_humidity": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}", + "is_illuminance": "\u0422\u0435\u043a\u0443\u0449\u0430 \u043e\u0441\u0432\u0435\u0442\u0435\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}", + "is_power": "\u0422\u0435\u043a\u0443\u0449\u0430 \u043c\u043e\u0449\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}", + "is_pressure": "\u0422\u0435\u043a\u0443\u0449\u043e \u043d\u0430\u043b\u044f\u0433\u0430\u043d\u0435 \u043d\u0430 {entity_name}", + "is_signal_strength": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0441\u0438\u043b\u0430 \u043d\u0430 \u0441\u0438\u0433\u043d\u0430\u043b\u0430 \u043d\u0430 {entity_name}", + "is_temperature": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 {entity_name}", + "is_timestamp": "\u0422\u0435\u043a\u0443\u0449\u043e \u0432\u0440\u0435\u043c\u0435 \u043d\u0430 {entity_name}", + "is_value": "\u0422\u0435\u043a\u0443\u0449\u0430 \u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043d\u0430 {entity_name}" + }, + "trigger_type": { + "battery_level": "{entity_name} \u043d\u0438\u0432\u043e\u0442\u043e \u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u044f", + "humidity": "{entity_name} \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "illuminance": "{entity_name} \u043e\u0441\u0432\u0435\u0442\u0435\u043d\u043e\u0441\u0442\u0442\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "power": "\u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "pressure": "\u043d\u0430\u043b\u044f\u0433\u0430\u043d\u0435\u0442\u043e \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "signal_strength": "\u0441\u0438\u043b\u0430\u0442\u0430 \u043d\u0430 \u0441\u0438\u0433\u043d\u0430\u043b\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "temperature": "\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430\u0442\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "timestamp": "\u0432\u0440\u0435\u043c\u0435\u0442\u043e \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "value": "\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 {entity_name} \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/bg.json b/homeassistant/components/solarlog/.translations/bg.json new file mode 100644 index 00000000000000..6dabc169f12e9c --- /dev/null +++ b/homeassistant/components/solarlog/.translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435, \u043c\u043e\u043b\u044f, \u043f\u043e\u0442\u0432\u044a\u0440\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0430 \u0412\u0430\u0448\u0435\u0442\u043e Solar-Log \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "name": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441\u044a\u0442, \u043a\u043e\u0439\u0442\u043e \u0434\u0430 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0437\u0430 \u0432\u0430\u0448\u0438\u0442\u0435 Solar-Log \u0441\u0435\u043d\u0437\u043e\u0440\u0438" + }, + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430 \u0441\u0438 \u0441\u044a\u0441 Solar-log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/bg.json b/homeassistant/components/soma/.translations/bg.json new file mode 100644 index 00000000000000..0b7dd3b689f254 --- /dev/null +++ b/homeassistant/components/soma/.translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Soma \u0430\u043a\u0430\u0443\u043d\u0442.", + "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.", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 Soma \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441\u044a\u0441 Soma." + }, + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u0442\u0435 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0412\u0430\u0448\u0438\u044f SOMA Connect.", + "title": "SOMA Connect" + } + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/bg.json b/homeassistant/components/somfy/.translations/bg.json index 1e47a7bae458e2..4741ccdba6d234 100644 --- a/homeassistant/components/somfy/.translations/bg.json +++ b/homeassistant/components/somfy/.translations/bg.json @@ -8,6 +8,11 @@ "create_entry": { "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441\u044a\u0441 Somfy." }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/it.json b/homeassistant/components/somfy/.translations/it.json index 06fc8bed40f409..c91536abc773cd 100644 --- a/homeassistant/components/somfy/.translations/it.json +++ b/homeassistant/components/somfy/.translations/it.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Autenticato con successo con Somfy." }, + "step": { + "pick_implementation": { + "title": "Seleziona il metodo di autenticazione" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/bg.json b/homeassistant/components/transmission/.translations/bg.json new file mode 100644 index 00000000000000..98160b89925823 --- /dev/null +++ b/homeassistant/components/transmission/.translations/bg.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d.", + "one_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0430\u0434\u0440\u0435\u0441\u0430", + "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430", + "wrong_credentials": "\u0413\u0440\u0435\u0448\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430" + }, + "step": { + "options": { + "data": { + "scan_interval": "\u0427\u0435\u0441\u0442\u043e\u0442\u0430 \u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" + }, + "title": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435" + }, + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 Transmission \u043a\u043b\u0438\u0435\u043d\u0442" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0435\u0441\u0442\u043e\u0442\u0430 \u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" + }, + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u043f\u0446\u0438\u0438\u0442\u0435 \u0437\u0430 Transmission", + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u043f\u0446\u0438\u0438\u0442\u0435 \u0437\u0430 Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/bg.json b/homeassistant/components/unifi/.translations/bg.json index df5654ff78be5b..4b8b89774914ad 100644 --- a/homeassistant/components/unifi/.translations/bg.json +++ b/homeassistant/components/unifi/.translations/bg.json @@ -32,6 +32,11 @@ "track_devices": "\u041f\u0440\u043e\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (Ubiquiti \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430)", "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0435\u0442\u0435 \u043d\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441 \u043a\u0430\u0431\u0435\u043b" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u0421\u044a\u0437\u0434\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0441\u0435\u043d\u0437\u043e\u0440\u0438 \u0437\u0430 \u0442\u0440\u0430\u0444\u0438\u043a\u0430 \u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0438" + } } } } diff --git a/homeassistant/components/withings/.translations/bg.json b/homeassistant/components/withings/.translations/bg.json index e75860d0e16b6f..4064b21ca6b0b7 100644 --- a/homeassistant/components/withings/.translations/bg.json +++ b/homeassistant/components/withings/.translations/bg.json @@ -7,6 +7,13 @@ "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435 \u0441 Withings \u0437\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b." }, "step": { + "profile": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b" + }, + "description": "\u041a\u043e\u0439 \u043f\u0440\u043e\u0444\u0438\u043b \u0441\u0442\u0435 \u0438\u0437\u0431\u0440\u0430\u043b\u0438 \u043d\u0430 \u0443\u0435\u0431\u0441\u0430\u0439\u0442\u0430 \u043d\u0430 Withings? \u0412\u0430\u0436\u043d\u043e \u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u0438\u0442\u0435 \u0434\u0430 \u0441\u044a\u0432\u043f\u0430\u0434\u0430\u0442, \u0432 \u043f\u0440\u043e\u0442\u0438\u0432\u0435\u043d \u0441\u043b\u0443\u0447\u0430\u0439 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0449\u0435 \u0431\u044a\u0434\u0430\u0442 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043e\u0437\u043d\u0430\u0447\u0435\u043d\u0438.", + "title": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b." + }, "user": { "data": { "profile": "\u041f\u0440\u043e\u0444\u0438\u043b" diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json index fbd590697e18dc..4ac73dde1956af 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -11,6 +11,7 @@ "data": { "profile": "Profilo" }, + "description": "Quale profilo hai selezionato sul sito web di Withings? \u00c8 importante che i profili corrispondano, altrimenti i dati avranno con un'errata etichettatura.", "title": "Profilo utente." }, "user": { diff --git a/homeassistant/components/zha/.translations/bg.json b/homeassistant/components/zha/.translations/bg.json index 2715ef46dc8504..916d09a683060b 100644 --- a/homeassistant/components/zha/.translations/bg.json +++ b/homeassistant/components/zha/.translations/bg.json @@ -18,6 +18,10 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "\u041a\u0432\u0430\u043a", + "warn": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435" + }, "trigger_subtype": { "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", From 84e4e94d8e5dba9562bf806bac674fcc1e52f2fd Mon Sep 17 00:00:00 2001 From: phispi Date: Fri, 1 Nov 2019 21:23:23 +0100 Subject: [PATCH 1386/3953] Prevent TypeError when KNX RGB(W) light value contains None (#28358) * Prevent TypeError when KNX RGB(W) light value contains None. * Pylint doesn't like 'w' as variable name, therefore using 'white' instead. * Simplified code as suggested by pvizeli. --- homeassistant/components/knx/light.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 81bf4ad3c8375d..c7292309461440 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -180,13 +180,9 @@ def should_poll(self): @property def brightness(self): """Return the brightness of this light between 0..255.""" - if self.device.supports_brightness: - return self.device.current_brightness - if ( - self.device.supports_color or self.device.supports_rgbw - ) and self.device.current_color: - return max(self.device.current_color) - return None + if not self.device.supports_brightness: + return None + return self.device.current_brightness @property def hs_color(self): From 9e9537a3d07721781b0bcc84a4f29752337db911 Mon Sep 17 00:00:00 2001 From: Mister Wil <1091741+MisterWil@users.noreply.github.com> Date: Fri, 1 Nov 2019 17:28:50 -0700 Subject: [PATCH 1387/3953] Change Abode cache file path, add cache path to config flow (#28389) * Changed cache file path * Cache file naming scheme matches original * Restart tests * Adding cache path to config_flow.py * Moved DEFAULT_CACHEDB to consts file * Use correct cache path * Linting issues --- homeassistant/components/abode/__init__.py | 4 +--- homeassistant/components/abode/config_flow.py | 7 +++++-- homeassistant/components/abode/const.py | 2 ++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 6a72ac64145485..76c14d7917ff72 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -23,14 +23,12 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity -from .const import ATTRIBUTION, DOMAIN +from .const import ATTRIBUTION, DOMAIN, DEFAULT_CACHEDB _LOGGER = logging.getLogger(__name__) CONF_POLLING = "polling" -DEFAULT_CACHEDB = "./abodepy_cache.pickle" - SERVICE_SETTINGS = "change_setting" SERVICE_CAPTURE_IMAGE = "capture_image" SERVICE_TRIGGER = "trigger_quick_action" diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index d8d914f7998d63..bf48e4546b301d 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from .const import DOMAIN # pylint: disable=W0611 +from .const import DOMAIN, DEFAULT_CACHEDB # pylint: disable=W0611 CONF_POLLING = "polling" @@ -42,9 +42,12 @@ async def async_step_user(self, user_input=None): username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] polling = user_input.get(CONF_POLLING, False) + cache = self.hass.config.path(DEFAULT_CACHEDB) try: - await self.hass.async_add_executor_job(Abode, username, password, True) + await self.hass.async_add_executor_job( + Abode, username, password, True, True, True, cache + ) except (AbodeException, ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Abode: %s", str(ex)) diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py index 35e74e154cffe4..092843ba2120b5 100644 --- a/homeassistant/components/abode/const.py +++ b/homeassistant/components/abode/const.py @@ -1,3 +1,5 @@ """Constants for the Abode Security System component.""" DOMAIN = "abode" ATTRIBUTION = "Data provided by goabode.com" + +DEFAULT_CACHEDB = "abodepy_cache.pickle" From b88c0cf3148ffa66fa5a37ebd5e493fcfdc224fa Mon Sep 17 00:00:00 2001 From: Robin Pronk Date: Fri, 1 Nov 2019 15:38:14 +0100 Subject: [PATCH 1388/3953] SNMP switch fix integer support (#28425) --- homeassistant/components/snmp/switch.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index aac43208a1f58b..8d5be1221c4026 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -1,6 +1,8 @@ """Support for SNMP enabled switch.""" import logging +from pyasn1.type.univ import Integer + import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, @@ -190,15 +192,20 @@ def __init__( async def async_turn_on(self, **kwargs): """Turn on the switch.""" - await self._set(self._command_payload_on) + if self._command_payload_on.isdigit(): + await self._set(Integer(self._command_payload_on)) + else: + await self._set(self._command_payload_on) async def async_turn_off(self, **kwargs): """Turn off the switch.""" - await self._set(self._command_payload_off) + if self._command_payload_on.isdigit(): + await self._set(Integer(self._command_payload_off)) + else: + await self._set(self._command_payload_off) async def async_update(self): """Update the state.""" - errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) ) @@ -215,8 +222,12 @@ async def async_update(self): for resrow in restable: if resrow[-1] == self._payload_on: self._state = True + elif resrow[-1] == Integer(self._payload_on): + self._state = True elif resrow[-1] == self._payload_off: self._state = False + elif resrow[-1] == Integer(self._payload_off): + self._state = False else: self._state = None From fc7d43269c588aca9ea0fa36aa8d192f7e5225c7 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 1 Nov 2019 16:41:26 -0500 Subject: [PATCH 1389/3953] Use server-specific unique_ids for Plex media_players (#28447) --- homeassistant/components/plex/media_player.py | 27 ++++++++++++++++--- homeassistant/components/plex/server.py | 3 ++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 32bf7b65fff6c7..4c32c1e6376de1 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -6,7 +6,7 @@ import plexapi.exceptions import requests.exceptions -from homeassistant.components.media_player import MediaPlayerDevice +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -30,6 +30,7 @@ ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.util import dt as dt_util from .const import ( @@ -56,10 +57,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex media_player from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] + registry = await async_get_registry(hass) def async_new_media_players(new_entities): _async_add_entities( - hass, config_entry, async_add_entities, server_id, new_entities + hass, registry, config_entry, async_add_entities, server_id, new_entities ) unsub = async_dispatcher_connect( @@ -70,7 +72,7 @@ def async_new_media_players(new_entities): @callback def _async_add_entities( - hass, config_entry, async_add_entities, server_id, new_entities + hass, registry, config_entry, async_add_entities, server_id, new_entities ): """Set up Plex media_player entities.""" entities = [] @@ -79,6 +81,19 @@ def _async_add_entities( plex_mp = PlexMediaPlayer(plexserver, **entity_params) entities.append(plex_mp) + # Migration to per-server unique_ids + old_entity_id = registry.async_get_entity_id( + MP_DOMAIN, PLEX_DOMAIN, plex_mp.machine_identifier + ) + if old_entity_id is not None: + new_unique_id = f"{server_id}:{plex_mp.machine_identifier}" + _LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + plex_mp.machine_identifier, + new_unique_id, + ) + registry.async_update_entity(old_entity_id, new_unique_id=new_unique_id) + async_add_entities(entities, True) @@ -126,6 +141,7 @@ def __init__(self, plex_server, device, session=None): async def async_added_to_hass(self): """Run when about to be added to hass.""" server_id = self.plex_server.machine_identifier + unsub = async_dispatcher_connect( self.hass, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id), @@ -315,6 +331,11 @@ def should_poll(self): @property def unique_id(self): """Return the id of this plex client.""" + return f"{self.plex_server.machine_identifier}:{self._machine_identifier}" + + @property + def machine_identifier(self): + """Return the Plex-provided identifier of this plex client.""" return self._machine_identifier @property diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index e6f77a310f1727..28380e714ac790 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -94,9 +94,10 @@ def _connect_with_url(): def refresh_entity(self, machine_identifier, device, session): """Forward refresh dispatch to media_player.""" + unique_id = f"{self.machine_identifier}:{machine_identifier}" dispatcher_send( self._hass, - PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(machine_identifier), + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id), device, session, ) From 3f8dc5ed75c493acd56f587ce00fd3334277070a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Nov 2019 17:21:50 -0700 Subject: [PATCH 1390/3953] Also install after_deps (#28453) --- homeassistant/requirements.py | 8 ++++++-- tests/test_requirements.py | 13 +++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 74469ef2fcd619..a0eec0f442b782 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -48,8 +48,12 @@ async def async_get_integration_with_requirements( hass, integration.domain, integration.requirements ) - for dependency in integration.dependencies: - await async_get_integration_with_requirements(hass, dependency) + deps = integration.dependencies + (integration.after_dependencies or []) + + if deps: + await asyncio.gather( + *[async_get_integration_with_requirements(hass, dep) for dep in deps] + ) return integration diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 548ea645360d94..782b4386552964 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -115,12 +115,19 @@ async def test_get_integration_with_requirements(hass): mock_integration( hass, MockModule("test_component_dep", requirements=["test-comp-dep==1.0.0"]) ) + mock_integration( + hass, + MockModule( + "test_component_after_dep", requirements=["test-comp-after-dep==1.0.0"] + ), + ) mock_integration( hass, MockModule( "test_component", requirements=["test-comp==1.0.0"], dependencies=["test_component_dep"], + partial_manifest={"after_dependencies": ["test_component_after_dep"]}, ), ) @@ -136,13 +143,15 @@ async def test_get_integration_with_requirements(hass): assert integration assert integration.domain == "test_component" - assert len(mock_is_installed.mock_calls) == 2 + assert len(mock_is_installed.mock_calls) == 3 assert mock_is_installed.mock_calls[0][1][0] == "test-comp==1.0.0" assert mock_is_installed.mock_calls[1][1][0] == "test-comp-dep==1.0.0" + assert mock_is_installed.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" - assert len(mock_inst.mock_calls) == 2 + assert len(mock_inst.mock_calls) == 3 assert mock_inst.mock_calls[0][1][0] == "test-comp==1.0.0" assert mock_inst.mock_calls[1][1][0] == "test-comp-dep==1.0.0" + assert mock_inst.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" async def test_install_with_wheels_index(hass): From 969b36a44764afb84c1eb41bc675550b39222128 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Nov 2019 17:32:57 -0700 Subject: [PATCH 1391/3953] Bumped version to 0.101.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4e25e8c7dc369b..575a7d5740f8db 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 512ef2fce46be2e47e6e070f18ce45b076f889a3 Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Sat, 2 Nov 2019 02:32:37 -0700 Subject: [PATCH 1392/3953] Change ps4 state off to state standby (#28261) * Change state off to state standby * update docstring --- homeassistant/components/ps4/media_player.py | 12 +++++------ tests/components/ps4/test_media_player.py | 22 ++++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 3e8b667cd13a3a..91d3a5b13c72a0 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -26,7 +26,7 @@ CONF_REGION, CONF_TOKEN, STATE_IDLE, - STATE_OFF, + STATE_STANDBY, STATE_PLAYING, ) from homeassistant.helpers import device_registry, entity_registry @@ -201,8 +201,8 @@ def _parse_status(self): if self._state != STATE_IDLE: self.idle() else: - if self._state != STATE_OFF: - self.state_off() + if self._state != STATE_STANDBY: + self.state_standby() elif self._retry > DEFAULT_RETRIES: self.state_unknown() @@ -230,10 +230,10 @@ def idle(self): self._state = STATE_IDLE self.schedule_update() - def state_off(self): - """Set states for state off.""" + def state_standby(self): + """Set states for state standby.""" self.reset_title() - self._state = STATE_OFF + self._state = STATE_STANDBY self.schedule_update() def state_unknown(self): diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 7bf93e37777e6c..e2b9e382dc4d49 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -29,7 +29,7 @@ CONF_REGION, CONF_TOKEN, STATE_IDLE, - STATE_OFF, + STATE_STANDBY, STATE_PLAYING, STATE_UNKNOWN, ) @@ -46,7 +46,7 @@ MOCK_HOST_ID = "A0000A0AA000" MOCK_HOST_VERSION = "09879011" MOCK_HOST_TYPE = "PS4" -MOCK_STATUS_STANDBY = "Server Standby" +MOCK_STATUS_REST = "Server Standby" MOCK_STATUS_ON = "Ok" MOCK_STANDBY_CODE = 620 MOCK_ON_CODE = 200 @@ -100,13 +100,13 @@ "system-version": MOCK_HOST_VERSION, } -MOCK_STATUS_OFF = { +MOCK_STATUS_STANDBY = { "host-type": MOCK_HOST_TYPE, "host-ip": MOCK_HOST, "host-request-port": MOCK_TCP_PORT, "host-id": MOCK_HOST_ID, "host-name": MOCK_HOST_NAME, - "status": MOCK_STATUS_STANDBY, + "status": MOCK_STATUS_REST, "status_code": MOCK_STANDBY_CODE, "device-discovery-protocol-version": MOCK_DDP_VERSION, "system-version": MOCK_HOST_VERSION, @@ -183,14 +183,14 @@ async def test_media_player_is_setup_correctly_with_entry(hass): assert mock_state == STATE_UNKNOWN -async def test_state_off_is_set(hass): - """Test that state is set to off.""" +async def test_state_standby_is_set(hass): + """Test that state is set to standby.""" mock_entity_id = await setup_mock_component(hass) with patch(MOCK_SAVE, side_effect=MagicMock()): - await mock_ddp_response(hass, MOCK_STATUS_OFF) + await mock_ddp_response(hass, MOCK_STATUS_STANDBY) - assert hass.states.get(mock_entity_id).state == STATE_OFF + assert hass.states.get(mock_entity_id).state == STATE_STANDBY async def test_state_playing_is_set(hass): @@ -290,13 +290,13 @@ 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_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_OFF): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_STANDBY): mock_entity_id = await setup_mock_component(hass) await hass.async_block_till_done() # Reformat mock status-sw_version for assertion. - mock_version = MOCK_STATUS_OFF["system-version"] + mock_version = MOCK_STATUS_STANDBY["system-version"] mock_version = mock_version[1:4] mock_version = "{}.{}".format(mock_version[0], mock_version[1:]) @@ -306,7 +306,7 @@ async def test_device_info_is_set_from_status_correctly(hass): mock_entry = mock_d_registry.async_get_device( identifiers={(DOMAIN, MOCK_HOST_ID)}, connections={()} ) - assert mock_state == STATE_OFF + assert mock_state == STATE_STANDBY assert len(mock_d_entries) == 1 assert mock_entry.name == MOCK_HOST_NAME From b8fa5367dbbdb2d11f1a0563535e9229b4749025 Mon Sep 17 00:00:00 2001 From: Nash Kaminski Date: Sat, 2 Nov 2019 04:51:30 -0500 Subject: [PATCH 1393/3953] Fix inability to transition between specific presets in Venstar component (#28238) This change addresses a bug where one is unable to change directly between the away and temperature hold presets, as temperature hold cannot be enabled on a Venstar thermostat if away mode is active. Furthermore, this change removes redundant state checks as the set_away and set_schedule calls are idempotent in the venstarcolortouch library. See https://github.com/hpeyerl/venstar_colortouch/blob/master/src/venstarcolortouch/venstarcolortouch.py#L275. --- homeassistant/components/venstar/climate.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 9e5450addc591c..c948772197f665 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -247,6 +247,7 @@ def preset_mode(self): return PRESET_AWAY if self._client.schedule == 0: return HOLD_MODE_TEMPERATURE + return PRESET_NONE @property def preset_modes(self): @@ -332,13 +333,11 @@ def set_preset_mode(self, preset_mode): if preset_mode == PRESET_AWAY: success = self._client.set_away(self._client.AWAY_AWAY) elif preset_mode == HOLD_MODE_TEMPERATURE: - success = self._client.set_schedule(0) + success = self._client.set_away(self._client.AWAY_HOME) + success = success and self._client.set_schedule(0) elif preset_mode == PRESET_NONE: - success = False - if self._client.away: - success = self._client.set_away(self._client.AWAY_HOME) - if self._client.schedule == 0: - success = success and self._client.set_schedule(1) + success = self._client.set_away(self._client.AWAY_HOME) + success = success and self._client.set_schedule(1) else: _LOGGER.error("Unknown hold mode: %s", preset_mode) success = False From a8dff2f2d0b2c33a7e47a06d51248c3d73f6888e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 2 Nov 2019 21:21:09 +0200 Subject: [PATCH 1394/3953] pre-commit: ship default and full configs (#28463) For now, the only difference between the two is mypy. --- .pre-commit-config-all.yaml | 42 +++++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 21 +++++++------------ azure-pipelines-ci.yml | 8 +++---- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- tox.ini | 2 +- 6 files changed, 56 insertions(+), 21 deletions(-) create mode 100644 .pre-commit-config-all.yaml diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml new file mode 100644 index 00000000000000..98829e25fc37fb --- /dev/null +++ b/.pre-commit-config-all.yaml @@ -0,0 +1,42 @@ +# This configuration includes the full set of hooks we use. In +# addition to the defaults (see .pre-commit-config.yaml), this +# includes hooks that require our development and test dependencies +# installed and the virtualenv containing them active by the time +# pre-commit runs to produce correct results. +# +# If this is not a problem for your workflow, using this config is +# recommended, install it with +# pre-commit install --config .pre-commit-config-all.yaml +# Otherwise, see the default .pre-commit-config.yaml for a lighter one. + +repos: +- repo: https://github.com/psf/black + rev: 19.10b0 + hooks: + - id: black + args: + - --safe + - --quiet + files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.8 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.3.1 + - pydocstyle==4.0.0 + files: ^(homeassistant|script|tests)/.+\.py$ +# Using a local "system" mypy instead of the mypy hook, because its +# results depend on what is installed. And the mypy hook runs in a +# virtualenv of its own, meaning we'd need to install and maintain +# another set of our dependencies there... no. Use the "system" one +# and reuse the environment that is set up anyway already instead. +- repo: local + hooks: + - id: mypy + name: mypy + entry: mypy + language: system + types: [python] + require_serial: true + files: ^homeassistant/.+\.py$ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2b99393639b8c..4beff14965b9ce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,10 @@ +# This configuration includes the default, minimal set of hooks to be +# run on all commits. It requires no specific setup and one can just +# start using pre-commit with it. +# +# See .pre-commit-config-all.yaml for a more complete one that comes +# with a better coverage at the cost of some specific setup needed. + repos: - repo: https://github.com/psf/black rev: 19.10b0 @@ -15,17 +22,3 @@ repos: - flake8-docstrings==1.3.1 - pydocstyle==4.0.0 files: ^(homeassistant|script|tests)/.+\.py$ -# Using a local "system" mypy instead of the mypy hook, because its -# results depend on what is installed. And the mypy hook runs in a -# virtualenv of its own, meaning we'd need to install and maintain -# another set of our dependencies there... no. Use the "system" one -# and reuse the environment that is set up anyway already instead. -- repo: local - hooks: - - id: mypy - name: mypy - entry: mypy - language: system - types: [python] - require_serial: true - files: ^homeassistant/.+\.py$ diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index f1abf2ff9dba9b..e80f4b8d0ba8e3 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -45,7 +45,7 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks + pre-commit install-hooks --config .pre-commit-config-all.yaml - script: | . venv/bin/activate pre-commit run flake8 --all-files @@ -84,7 +84,7 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks + pre-commit install-hooks --config .pre-commit-config-all.yaml - script: | . venv/bin/activate pre-commit run black --all-files @@ -182,8 +182,8 @@ stages: . venv/bin/activate pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt - pre-commit install-hooks + pre-commit install-hooks --config .pre-commit-config-all.yaml - script: | . venv/bin/activate - pre-commit run mypy --all-files + pre-commit run --config .pre-commit-config-all.yaml mypy --all-files displayName: 'Run mypy' diff --git a/requirements_test.txt b/requirements_test.txt index f2935a423bf5fd..06a2ef1621d26e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,7 @@ # make new things fail. Manually update these pins when pulling in a # new version -# When updating this file, update .pre-commit-config.yaml too +# When updating this file, update .pre-commit-config*.yaml too asynctest==0.13.0 black==19.10b0 codecov==2.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e757272a7892bf..d7b4e4ffb014a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -3,7 +3,7 @@ # make new things fail. Manually update these pins when pulling in a # new version -# When updating this file, update .pre-commit-config.yaml too +# When updating this file, update .pre-commit-config*.yaml too asynctest==0.13.0 black==19.10b0 codecov==2.0.15 diff --git a/tox.ini b/tox.ini index f6d12fe30f526f..118d0722b7c173 100644 --- a/tox.ini +++ b/tox.ini @@ -41,4 +41,4 @@ deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - pre-commit run mypy {posargs: --all-files} + pre-commit run --config .pre-commit-config-all.yaml mypy {posargs: --all-files} From 1679ec3245c88c8abb52af8cc5bf212bb98e48ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 2 Nov 2019 21:30:09 +0200 Subject: [PATCH 1395/3953] SSDP matching improvements (#28285) * SSDP matching improvements - support multiple match groups per domain - require matches on all, not any item in a group - support matching on all UPnP device description data * Manifest structure fixes --- homeassistant/components/deconz/manifest.json | 10 ++-- homeassistant/components/heos/manifest.json | 12 ++-- homeassistant/components/hue/manifest.json | 10 ++-- homeassistant/components/sonos/manifest.json | 10 ++-- homeassistant/components/ssdp/__init__.py | 31 +++++----- homeassistant/components/wemo/manifest.json | 10 ++-- homeassistant/generated/ssdp.py | 43 ++++++++------ script/hassfest/manifest.py | 6 +- script/hassfest/ssdp.py | 16 ++---- tests/components/ssdp/test_init.py | 57 ++++++++++++++----- 10 files changed, 113 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 63ab17d001acec..64902002600a6d 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -6,11 +6,11 @@ "requirements": [ "pydeconz==64" ], - "ssdp": { - "manufacturer": [ - "Royal Philips Electronics" - ] - }, + "ssdp": [ + { + "manufacturer": "Royal Philips Electronics" + } + ], "dependencies": [], "codeowners": [ "@kane610" diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index fb21a43356f657..684127e519e78d 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -6,13 +6,13 @@ "requirements": [ "pyheos==0.6.0" ], - "ssdp": { - "st": [ - "urn:schemas-denon-com:device:ACT-Denon:1" - ] - }, + "ssdp": [ + { + "st": "urn:schemas-denon-com:device:ACT-Denon:1" + } + ], "dependencies": [], "codeowners": [ "@andrewsayre" ] -} \ No newline at end of file +} diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 9a3e478d108f67..c90b6181559458 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -6,11 +6,11 @@ "requirements": [ "aiohue==1.9.2" ], - "ssdp": { - "manufacturer": [ - "Royal Philips Electronics" - ] - }, + "ssdp": [ + { + "manufacturer": "Royal Philips Electronics" + } + ], "homekit": { "models": [ "BSB002" diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 7b0c041b2a90e1..46723bdcf5fefd 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -7,10 +7,10 @@ "pysonos==0.0.24" ], "dependencies": [], - "ssdp": { - "st": [ - "urn:schemas-upnp-org:device:ZonePlayer:1" - ] - }, + "ssdp": [ + { + "st": "urn:schemas-upnp-org:device:ZonePlayer:1" + } + ], "codeowners": [] } diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index d2382748f30e4f..c4d71e0febd055 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -104,28 +104,27 @@ async def _process_entries(self, entries): async def _process_entry(self, entry): """Process a single entry.""" - domains = set(SSDP["st"].get(entry.st, [])) - xml_location = entry.location + info = {"st": entry.st} - if not xml_location: - if domains: - return (entry, info_from_entry(entry, None), domains) - return None + if entry.location: - # Multiple entries usually share same location. Make sure - # we fetch it only once. - info_req = self._description_cache.get(xml_location) + # Multiple entries usually share same location. Make sure + # we fetch it only once. + info_req = self._description_cache.get(entry.location) - if info_req is None: - info_req = self._description_cache[ - xml_location - ] = self.hass.async_create_task(self._fetch_description(xml_location)) + if info_req is None: + info_req = self._description_cache[ + entry.location + ] = self.hass.async_create_task(self._fetch_description(entry.location)) - info = await info_req + info.update(await info_req) - domains.update(SSDP["manufacturer"].get(info.get("manufacturer"), [])) - domains.update(SSDP["device_type"].get(info.get("deviceType"), [])) + domains = set() + for domain, matchers in SSDP.items(): + for matcher in matchers: + if all(info.get(k) == v for (k, v) in matcher.items()): + domains.add(domain) if domains: return (entry, info_from_entry(entry, info), domains) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index aa863bcff0d4ad..3b43def230fc43 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -6,11 +6,11 @@ "requirements": [ "pywemo==0.4.34" ], - "ssdp": { - "manufacturer": [ - "Belkin International Inc." - ] - }, + "ssdp": [ + { + "manufacturer": "Belkin International Inc." + } + ], "homekit": { "models": [ "Wemo" diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 6d62c47110b2d7..472ad6683ed516 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -6,22 +6,29 @@ # fmt: off SSDP = { - "device_type": {}, - "manufacturer": { - "Belkin International Inc.": [ - "wemo" - ], - "Royal Philips Electronics": [ - "deconz", - "hue" - ] - }, - "st": { - "urn:schemas-denon-com:device:ACT-Denon:1": [ - "heos" - ], - "urn:schemas-upnp-org:device:ZonePlayer:1": [ - "sonos" - ] - } + "deconz": [ + { + "manufacturer": "Royal Philips Electronics" + } + ], + "heos": [ + { + "st": "urn:schemas-denon-com:device:ACT-Denon:1" + } + ], + "hue": [ + { + "manufacturer": "Royal Philips Electronics" + } + ], + "sonos": [ + { + "st": "urn:schemas-upnp-org:device:ZonePlayer:1" + } + ], + "wemo": [ + { + "manufacturer": "Belkin International Inc." + } + ] } diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 5cf8772686e052..16f8b77b5d382d 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -14,11 +14,7 @@ vol.Optional("config_flow"): bool, vol.Optional("zeroconf"): [str], vol.Optional("ssdp"): vol.Schema( - { - vol.Optional("st"): [str], - vol.Optional("manufacturer"): [str], - vol.Optional("device_type"): [str], - } + vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))]) ), vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}), vol.Required("documentation"): str, diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index 3b02ea181514df..d2dd724605e527 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -24,11 +24,8 @@ def sort_dict(value): def generate_and_validate(integrations: Dict[str, Integration]): """Validate and generate ssdp data.""" - data = { - "st": defaultdict(list), - "manufacturer": defaultdict(list), - "device_type": defaultdict(list), - } + + data = defaultdict(list) for domain in sorted(integrations): integration = integrations[domain] @@ -56,14 +53,9 @@ def generate_and_validate(integrations: Dict[str, Integration]): ) continue - for key in "st", "manufacturer", "device_type": - if key not in ssdp: - continue - - for value in ssdp[key]: - data[key][value].append(domain) + for matcher in ssdp: + data[domain].append(sort_dict(matcher)) - data = sort_dict({key: sort_dict(value) for key, value in data.items()}) return BASE.format(json.dumps(data, indent=4)) diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 81c0f886b41191..56b937cf9d9f4a 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -17,7 +17,7 @@ async def test_scan_match_st(hass): with patch( "netdisco.ssdp.scan", return_value=[Mock(st="mock-st", location=None)] - ), patch.dict(gn_ssdp.SSDP["st"], {"mock-st": ["mock-domain"]}), patch.object( + ), patch.dict(gn_ssdp.SSDP, {"mock-domain": [{"st": "mock-st"}]}), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: await scanner.async_scan(None) @@ -27,14 +27,15 @@ async def test_scan_match_st(hass): assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} -async def test_scan_match_manufacturer(hass, aioclient_mock): - """Test matching based on ST.""" +@pytest.mark.parametrize("key", ("manufacturer", "deviceType")) +async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key): + """Test matching based on UPnP device description data.""" aioclient_mock.get( "http://1.1.1.1", - text=""" + text=f""" - Paulus + <{key}>Paulus """, @@ -44,9 +45,7 @@ async def test_scan_match_manufacturer(hass, aioclient_mock): with patch( "netdisco.ssdp.scan", return_value=[Mock(st="mock-st", location="http://1.1.1.1")], - ), patch.dict( - gn_ssdp.SSDP["manufacturer"], {"Paulus": ["mock-domain"]} - ), patch.object( + ), patch.dict(gn_ssdp.SSDP, {"mock-domain": [{key: "Paulus"}]}), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: await scanner.async_scan(None) @@ -56,11 +55,11 @@ async def test_scan_match_manufacturer(hass, aioclient_mock): assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} -async def test_scan_match_device_type(hass, aioclient_mock): - """Test matching based on ST.""" +async def test_scan_not_all_present(hass, aioclient_mock): + """Test match fails if some specified attributes are not present.""" aioclient_mock.get( "http://1.1.1.1", - text=""" + text=f""" Paulus @@ -74,15 +73,43 @@ async def test_scan_match_device_type(hass, aioclient_mock): "netdisco.ssdp.scan", return_value=[Mock(st="mock-st", location="http://1.1.1.1")], ), patch.dict( - gn_ssdp.SSDP["device_type"], {"Paulus": ["mock-domain"]} + gn_ssdp.SSDP, + {"mock-domain": [{"deviceType": "Paulus", "manufacturer": "Paulus"}]}, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: await scanner.async_scan(None) - assert len(mock_init.mock_calls) == 1 - assert mock_init.mock_calls[0][1][0] == "mock-domain" - assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} + assert not mock_init.mock_calls + + +async def test_scan_not_all_match(hass, aioclient_mock): + """Test match fails if some specified attribute values differ.""" + aioclient_mock.get( + "http://1.1.1.1", + text=f""" + + + Paulus + Paulus + + + """, + ) + scanner = ssdp.Scanner(hass) + + with patch( + "netdisco.ssdp.scan", + return_value=[Mock(st="mock-st", location="http://1.1.1.1")], + ), patch.dict( + gn_ssdp.SSDP, + {"mock-domain": [{"deviceType": "Paulus", "manufacturer": "Not-Paulus"}]}, + ), patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + await scanner.async_scan(None) + + assert not mock_init.mock_calls @pytest.mark.parametrize("exc", [asyncio.TimeoutError, aiohttp.ClientError]) From f71527d5dbec1f34f26e55d41cd325c531dbf74a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 3 Nov 2019 00:31:44 +0000 Subject: [PATCH 1396/3953] [ci skip] Translation update --- .../components/almond/.translations/sl.json | 9 +++++ .../cert_expiry/.translations/sl.json | 4 +- .../coolmaster/.translations/sl.json | 23 +++++++++++ .../components/cover/.translations/fr.json | 8 ++++ .../components/cover/.translations/ko.json | 4 +- .../components/cover/.translations/sl.json | 12 +++++- .../device_tracker/.translations/ko.json | 4 +- .../device_tracker/.translations/sl.json | 8 ++++ .../huawei_lte/.translations/nl.json | 10 +++++ .../huawei_lte/.translations/sl.json | 39 +++++++++++++++++++ .../media_player/.translations/sl.json | 11 ++++++ .../components/sensor/.translations/fr.json | 6 +-- .../components/sensor/.translations/sl.json | 36 ++++++++--------- .../components/solarlog/.translations/sl.json | 21 ++++++++++ .../components/somfy/.translations/sl.json | 5 +++ .../transmission/.translations/sl.json | 5 ++- .../components/withings/.translations/sl.json | 7 ++++ 17 files changed, 184 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/almond/.translations/sl.json create mode 100644 homeassistant/components/coolmaster/.translations/sl.json create mode 100644 homeassistant/components/device_tracker/.translations/sl.json create mode 100644 homeassistant/components/huawei_lte/.translations/nl.json create mode 100644 homeassistant/components/huawei_lte/.translations/sl.json create mode 100644 homeassistant/components/media_player/.translations/sl.json create mode 100644 homeassistant/components/solarlog/.translations/sl.json diff --git a/homeassistant/components/almond/.translations/sl.json b/homeassistant/components/almond/.translations/sl.json new file mode 100644 index 00000000000000..f8bf3021db5547 --- /dev/null +++ b/homeassistant/components/almond/.translations/sl.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Konfigurirate lahko samo en ra\u010dun Almond.", + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/sl.json b/homeassistant/components/cert_expiry/.translations/sl.json index 3774956330a52a..d375c626c667b0 100644 --- a/homeassistant/components/cert_expiry/.translations/sl.json +++ b/homeassistant/components/cert_expiry/.translations/sl.json @@ -4,10 +4,12 @@ "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana" }, "error": { + "certificate_error": "Certifikata ni bilo mogo\u010de preveriti", "certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila", "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem je potekla", "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", - "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti" + "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti", + "wrong_host": "Potrdilo se ne ujema z imenom gostitelja" }, "step": { "user": { diff --git a/homeassistant/components/coolmaster/.translations/sl.json b/homeassistant/components/coolmaster/.translations/sl.json new file mode 100644 index 00000000000000..a59b5215e7fb8d --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Povezava s CoolMasterNet ni uspela. Preverite svojega gostitelja.", + "no_units": "V gostitelju CoolMasterNet ni bilo mogo\u010de najti nobenih enot HVAC." + }, + "step": { + "user": { + "data": { + "cool": "Podpira na\u010din hlajenja", + "dry": "Podpira na\u010din su\u0161enja", + "fan_only": "Podpira samo na\u010din ventilacije", + "heat": "Podpira na\u010din ogrevanja", + "heat_cool": "Podpira samodejni na\u010din ogrevanja / hlajenja", + "host": "Gostitelj", + "off": "Lahko se izklopi" + }, + "title": "Nastavite svoje podatke CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/fr.json b/homeassistant/components/cover/.translations/fr.json index 1f2debe9c04d58..3aa877637d966e 100644 --- a/homeassistant/components/cover/.translations/fr.json +++ b/homeassistant/components/cover/.translations/fr.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} est en train de s'ouvrir", "is_position": "La position de {entity_name} est", "is_tilt_position": "La position d'inclinaison de {entity_name} est" + }, + "trigger_type": { + "closed": "{entity_name} ferm\u00e9", + "closing": "{entity_name} fermeture", + "opened": "{entity_name} ouvert", + "opening": "{entity_name} ouverture", + "position": "{entity_name} changement de position", + "tilt_position": "{entity_name} changement d'inclinaison" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json index 4839f8f034f758..6a59bb9f6aea93 100644 --- a/homeassistant/components/cover/.translations/ko.json +++ b/homeassistant/components/cover/.translations/ko.json @@ -5,8 +5,8 @@ "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud799\ub2c8\ub2e4", "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4", - "is_position": "{entity_name} \uac1c\ud3d0 \uc704\uce58\ub294", - "is_tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\ub294" + "is_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uc704\uce58", + "is_tilt_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30" }, "trigger_type": { "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", diff --git a/homeassistant/components/cover/.translations/sl.json b/homeassistant/components/cover/.translations/sl.json index cb5109b5cb05a5..cd3570d39ba146 100644 --- a/homeassistant/components/cover/.translations/sl.json +++ b/homeassistant/components/cover/.translations/sl.json @@ -4,7 +4,17 @@ "is_closed": "{entity_name} je/so zaprt/a", "is_closing": "{entity_name} se zapira/jo", "is_open": "{entity_name} je odprt/a/o", - "is_opening": "{entity_name} se odpira/jo" + "is_opening": "{entity_name} se odpira/jo", + "is_position": "Trenutna pozicija {entity_name} je", + "is_tilt_position": "Trenutni polo\u017eaj nagiba {entity_name} je" + }, + "trigger_type": { + "closed": "{entity_name} se je/so se zaprla", + "closing": "{entity_name} se zapira/jo", + "opened": "{entity_name} se/so je odprla", + "opening": "{entity_name} se odpira/jo", + "position": "{entity_name} spremembe polo\u017eaja", + "tilt_position": "{entity_name} spremembe nagiba" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/ko.json b/homeassistant/components/device_tracker/.translations/ko.json index 34389297f28362..d258f67db22e21 100644 --- a/homeassistant/components/device_tracker/.translations/ko.json +++ b/homeassistant/components/device_tracker/.translations/ko.json @@ -1,8 +1,8 @@ { "device_automation": { "condtion_type": { - "is_home": "{entity_name} \ub2d8\uc774 \uc9d1\uc5d0 \uc788\uc2b5\ub2c8\ub2e4", - "is_not_home": "{entity_name} \ub2d8\uc774 \uc678\ucd9c\uc911\uc785\ub2c8\ub2e4" + "is_home": "{entity_name} \uc774(\uac00) \uc9d1\uc5d0 \uc788\uc2b5\ub2c8\ub2e4", + "is_not_home": "{entity_name} \uc774(\uac00) \uc678\ucd9c\uc911\uc785\ub2c8\ub2e4" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/sl.json b/homeassistant/components/device_tracker/.translations/sl.json new file mode 100644 index 00000000000000..f4784fbc664eb1 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/sl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} je doma", + "is_not_home": "{entity_name} ni doma" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/nl.json b/homeassistant/components/huawei_lte/.translations/nl.json new file mode 100644 index 00000000000000..4e4b63e9391d7e --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/nl.json @@ -0,0 +1,10 @@ +{ + "config": { + "error": { + "incorrect_password": "Onjuist wachtwoord", + "incorrect_username": "Onjuiste gebruikersnaam", + "incorrect_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", + "invalid_url": "Ongeldige URL" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/sl.json b/homeassistant/components/huawei_lte/.translations/sl.json new file mode 100644 index 00000000000000..e23ac72bcca4fd --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/sl.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Ta naprava je \u017ee nastavljena" + }, + "error": { + "connection_failed": "Povezava ni uspela", + "incorrect_password": "Nepravilno geslo", + "incorrect_username": "Nepravilno uporabni\u0161ko ime", + "incorrect_username_or_password": "Nepravilno uporabni\u0161ko ime ali geslo", + "invalid_url": "Neveljaven URL", + "login_attempts_exceeded": "Najve\u010d poskusov prijave prese\u017eeno, prosimo, poskusite znova pozneje", + "response_error": "Neznana napaka iz naprave", + "unknown_connection_error": "Neznana napaka pri povezovanju z napravo" + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "url": "URL", + "username": "Uporabni\u0161ko ime" + }, + "description": "Vnesite podatke za dostop do naprave. Dolo\u010danje uporabni\u0161kega imena in gesla je izbirno, vendar omogo\u010da podporo za ve\u010d funkcij integracije. Po drugi strani pa lahko uporaba poobla\u0161\u010dene povezave povzro\u010di te\u017eave pri dostopu do spletnega vmesnika naprave zunaj Home Assistant-a, medtem ko je integracija aktivna, in obratno.", + "title": "Konfigurirajte Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Prejemniki obvestil SMS", + "track_new_devices": "Sledi novim napravam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/sl.json b/homeassistant/components/media_player/.translations/sl.json new file mode 100644 index 00000000000000..eafc10bd8ab23b --- /dev/null +++ b/homeassistant/components/media_player/.translations/sl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} je nedejaven", + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen", + "is_paused": "{entity_name} je zaustavljen", + "is_playing": "{entity_name} predvaja" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json index f0060f151dcc7f..1ce2592410d163 100644 --- a/homeassistant/components/sensor/.translations/fr.json +++ b/homeassistant/components/sensor/.translations/fr.json @@ -1,9 +1,9 @@ { "device_automation": { "condition_type": { - "is_battery_level": "Le niveau de la batterie de {entity_name}", - "is_humidity": "L'humidit\u00e9 de {entity_name}", - "is_illuminance": "L'\u00e9clairement de {entity_name}", + "is_battery_level": "Niveau de la batterie de {entity_name}", + "is_humidity": "Humidit\u00e9 de {entity_name}", + "is_illuminance": "\u00c9clairement de {entity_name}", "is_power": "{entity_name} puissance", "is_pressure": "{entity_name} pression", "is_signal_strength": "{entity_name} force du signal", diff --git a/homeassistant/components/sensor/.translations/sl.json b/homeassistant/components/sensor/.translations/sl.json index e3bc994b6ea9b5..3f29b62e6658bd 100644 --- a/homeassistant/components/sensor/.translations/sl.json +++ b/homeassistant/components/sensor/.translations/sl.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} raven baterije", - "is_humidity": "{entity_name} vla\u017enost", - "is_illuminance": "{entity_name} osvetlitev", - "is_power": "{entity_name} mo\u010d", - "is_pressure": "{entity_name} pritisk", - "is_signal_strength": "{entity_name} jakost signala", - "is_temperature": "{entity_name} temperatura", - "is_timestamp": "{entity_name} \u010dasovni \u017eig", - "is_value": "{entity_name} vrednost" + "is_battery_level": "Trenutna raven baterije {entity_name}", + "is_humidity": "Trenutna vla\u017enost {entity_name}", + "is_illuminance": "Trenutna svetilnost {entity_name}", + "is_power": "Trenutna mo\u010d {entity_name}", + "is_pressure": "Trenutni tlak {entity_name}", + "is_signal_strength": "Trenutna jakost signala {entity_name}", + "is_temperature": "Trenutna temperatura {entity_name}", + "is_timestamp": "Trenutni {entity_name} \u010dasovni \u017eig", + "is_value": "Trenutna vrednost {entity_name}" }, "trigger_type": { - "battery_level": "{entity_name} raven baterije", - "humidity": "{entity_name} vla\u017enost", - "illuminance": "{entity_name} osvetljenosti", - "power": "{entity_name} mo\u010d", - "pressure": "{entity_name} tlak", - "signal_strength": "{entity_name} jakost signala", - "temperature": "{entity_name} temperatura", - "timestamp": "{entity_name} \u010dasovni \u017eig", - "value": "{entity_name} vrednost" + "battery_level": "{entity_name} spremembe ravni baterije", + "humidity": "{entity_name} spremembe vla\u017enosti", + "illuminance": "{entity_name} spremembe osvetlitve", + "power": "{entity_name} spremembe mo\u010di", + "pressure": "{entity_name} spremembe tlaka", + "signal_strength": "{entity_name} spremembe mo\u010di signala", + "temperature": "{entity_name} spremembe temperatur", + "timestamp": "{entity_name} spremembe \u010dasovnega \u017eiga", + "value": "{entity_name} spremembe vrednosti" } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/sl.json b/homeassistant/components/solarlog/.translations/sl.json new file mode 100644 index 00000000000000..152152eacf7f4a --- /dev/null +++ b/homeassistant/components/solarlog/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "already_configured": "Naprava je \u017ee konfigurirana", + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave, preverite naslov gostitelja" + }, + "step": { + "user": { + "data": { + "host": "Ime gostitelja ali ip naslov va\u0161e naprave Solar-Log", + "name": "Predpona, ki jo \u017eelite uporabiti za senzorje Solar-log" + }, + "title": "Dolo\u010dite povezavo Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/sl.json b/homeassistant/components/somfy/.translations/sl.json index 87e8e33c8147cf..bdfbade2bbe8db 100644 --- a/homeassistant/components/somfy/.translations/sl.json +++ b/homeassistant/components/somfy/.translations/sl.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Uspe\u0161no overjen s Somfy-jem." }, + "step": { + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/sl.json b/homeassistant/components/transmission/.translations/sl.json index 122c332f429d84..37ce27e19f418d 100644 --- a/homeassistant/components/transmission/.translations/sl.json +++ b/homeassistant/components/transmission/.translations/sl.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Gostitelj je \u017ee konfiguriran.", "one_instance_allowed": "Potrebna je samo ena instanca." }, "error": { "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem", + "name_exists": "Ime \u017ee obstaja", "wrong_credentials": "Napa\u010dno uporabni\u0161ko ime ali geslo" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Pogostost posodabljanja" }, - "description": "Nastavite mo\u017enosti za Transmission" + "description": "Nastavite mo\u017enosti za Transmission", + "title": "Nastavite mo\u017enosti za Transmission" } } } diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json index 7f32ade8a0892b..2ee52b29b2d19a 100644 --- a/homeassistant/components/withings/.translations/sl.json +++ b/homeassistant/components/withings/.translations/sl.json @@ -7,6 +7,13 @@ "default": "Uspe\u0161no overjen z Withings za izbrani profil." }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "description": "Kateri profil ste izbrali na spletni strani Withings? Pomembno je, da se profili ujemajo, sicer bodo podatki napa\u010dno ozna\u010deni.", + "title": "Uporabni\u0161ki profil." + }, "user": { "data": { "profile": "Profil" From 67eeb8f2582054ff5b65d361203f119ed66977eb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Nov 2019 21:21:13 -0700 Subject: [PATCH 1397/3953] Fix flaky test --- tests/test_requirements.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 782b4386552964..2627a077a87c74 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -144,14 +144,18 @@ async def test_get_integration_with_requirements(hass): assert integration.domain == "test_component" assert len(mock_is_installed.mock_calls) == 3 - assert mock_is_installed.mock_calls[0][1][0] == "test-comp==1.0.0" - assert mock_is_installed.mock_calls[1][1][0] == "test-comp-dep==1.0.0" - assert mock_is_installed.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" + assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] assert len(mock_inst.mock_calls) == 3 - assert mock_inst.mock_calls[0][1][0] == "test-comp==1.0.0" - assert mock_inst.mock_calls[1][1][0] == "test-comp-dep==1.0.0" - assert mock_inst.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" + assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] async def test_install_with_wheels_index(hass): From 0d432f60e201a5ba650082df749c8c9c428de582 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Sun, 3 Nov 2019 00:21:46 -0400 Subject: [PATCH 1398/3953] Bump env_canada to 0.0.30 (#28487) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 7661269073c66c..8ad13b392513f5 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/integrations/environment_canada", "requirements": [ - "env_canada==0.0.29" + "env_canada==0.0.30" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9723075b98ba16..a582478d132ecc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -462,7 +462,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.29 +env_canada==0.0.30 # homeassistant.components.envirophat # envirophat==0.0.6 From 31752d5736d512e9bfb0481f809e7c51de94d5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Rutkai?= Date: Sun, 3 Nov 2019 05:24:02 +0100 Subject: [PATCH 1399/3953] Fixing #27722 Watson TTS platform (sdk upgrade) (#28468) --- homeassistant/components/watson_tts/manifest.json | 2 +- homeassistant/components/watson_tts/tts.py | 5 ++++- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/watson_tts/manifest.json b/homeassistant/components/watson_tts/manifest.json index 4cde3f764e5080..e5f3efae04cc60 100644 --- a/homeassistant/components/watson_tts/manifest.json +++ b/homeassistant/components/watson_tts/manifest.json @@ -3,7 +3,7 @@ "name": "IBM Watson TTS", "documentation": "https://www.home-assistant.io/integrations/watson_tts", "requirements": [ - "ibm-watson==3.0.3" + "ibm-watson==4.0.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index a30d08f31f3d9e..021767e3d11f49 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -93,8 +93,11 @@ def get_engine(hass, config): """Set up IBM Watson TTS component.""" from ibm_watson import TextToSpeechV1 + from ibm_cloud_sdk_core.authenticators import IAMAuthenticator - service = TextToSpeechV1(url=config[CONF_URL], iam_apikey=config[CONF_APIKEY]) + authenticator = IAMAuthenticator(config[CONF_APIKEY]) + service = TextToSpeechV1(authenticator) + service.set_service_url(config[CONF_URL]) supported_languages = list({s[:5] for s in SUPPORTED_VOICES}) default_voice = config[CONF_VOICE] diff --git a/requirements_all.txt b/requirements_all.txt index a582478d132ecc..cf2c0cb7918fc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -685,7 +685,7 @@ hydrawiser==0.1.1 iaqualink==0.3.0 # homeassistant.components.watson_tts -ibm-watson==3.0.3 +ibm-watson==4.0.1 # homeassistant.components.watson_iot ibmiotf==0.3.4 From 5cbb6607a62ff7dcca3405b3b2ba1c7040ef01ac Mon Sep 17 00:00:00 2001 From: Tim McCormick Date: Sun, 3 Nov 2019 04:25:24 +0000 Subject: [PATCH 1400/3953] Fix missing import (#28460) --- homeassistant/components/sonos/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 94d252e9fee3f7..2baa02d0a5dfce 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -8,6 +8,7 @@ import async_timeout import pysonos +from pysonos import alarms from pysonos.exceptions import SoCoException, SoCoUPnPException import pysonos.snapshot @@ -1163,7 +1164,7 @@ def set_alarm(self, data): """Set the alarm clock on the player.""" alarm = None - for one_alarm in pysonos.alarms.get_alarms(self.soco): + for one_alarm in alarms.get_alarms(self.soco): # pylint: disable=protected-access if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]): alarm = one_alarm From 314c3d0965134ca7aff639a0a56e2ed129519d4c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 3 Nov 2019 05:28:07 +0100 Subject: [PATCH 1401/3953] Use integration name in docstring (#28445) --- script/scaffold/templates/config_flow/integration/__init__.py | 4 ++-- .../templates/config_flow_discovery/integration/__init__.py | 4 ++-- .../templates/config_flow_oauth2/integration/__init__.py | 2 +- .../device_condition/integration/device_condition.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py index 403453a1f6b4e6..04b908952d1c0f 100644 --- a/script/scaffold/templates/config_flow/integration/__init__.py +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -21,9 +21,9 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up Somfy from a config entry.""" + """Set up NEW_NAME from a config entry.""" # TODO Store an API object for your platforms to access - # hass.data[DOMAIN][entry.entry_id] = MyApi(…) + # hass.data[DOMAIN][entry.entry_id] = MyApi(...) for component in PLATFORMS: hass.async_create_task( diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py index 403453a1f6b4e6..04b908952d1c0f 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/__init__.py +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -21,9 +21,9 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up Somfy from a config entry.""" + """Set up NEW_NAME from a config entry.""" # TODO Store an API object for your platforms to access - # hass.data[DOMAIN][entry.entry_id] = MyApi(…) + # hass.data[DOMAIN][entry.entry_id] = MyApi(...) for component in PLATFORMS: hass.async_create_task( diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index 43b4c6f31cd2d9..30e7ad978107bf 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -55,7 +55,7 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up Somfy from a config entry.""" + """Set up NEW_NAME from a config entry.""" implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry ) diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index fa123cff8e00f2..4b7baf68a372d7 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -1,4 +1,4 @@ -"""Provides device automations for NEW_NAME.""" +"""Provide the device automations for NEW_NAME.""" from typing import Dict, List import voluptuous as vol From b904a2c5ada4f2ada097f139909418fb2b1b0217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 3 Nov 2019 06:28:38 +0200 Subject: [PATCH 1402/3953] Handle Huawei LTE timeouts (#28465) --- .../huawei_lte/.translations/en.json | 1 + .../components/huawei_lte/__init__.py | 14 +++++++++++--- .../components/huawei_lte/config_flow.py | 18 ++++++++++++++---- homeassistant/components/huawei_lte/const.py | 2 ++ .../components/huawei_lte/strings.json | 1 + 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 8681e3355a46c5..4e366e0ce31b20 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -5,6 +5,7 @@ }, "error": { "connection_failed": "Connection failed", + "connection_timeout": "Connection timeout", "incorrect_password": "Incorrect password", "incorrect_username": "Incorrect username", "incorrect_username_or_password": "Incorrect username or password", diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index e224f45ba90e80..112fcb6ec52a6b 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -18,6 +18,7 @@ ResponseErrorLoginRequiredException, ResponseErrorNotSupportedException, ) +from requests.exceptions import Timeout from url_normalize import url_normalize from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN @@ -33,6 +34,7 @@ EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CALLBACK_TYPE +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -44,6 +46,7 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ( ALL_KEYS, + CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DOMAIN, KEY_DEVICE_BASIC_INFORMATION, @@ -254,16 +257,21 @@ def get_connection() -> Connection: username = config_entry.data.get(CONF_USERNAME) password = config_entry.data.get(CONF_PASSWORD) if username or password: - connection = AuthorizedConnection(url, username=username, password=password) + connection = AuthorizedConnection( + url, username=username, password=password, timeout=CONNECTION_TIMEOUT + ) else: - connection = Connection(url) + connection = Connection(url, timeout=CONNECTION_TIMEOUT) return connection def signal_update() -> None: """Signal updates to data.""" dispatcher_send(hass, UPDATE_SIGNAL, url) - connection = await hass.async_add_executor_job(get_connection) + try: + connection = await hass.async_add_executor_job(get_connection) + except Timeout as ex: + raise ConfigEntryNotReady from ex # Set up router and store reference to it router = Router(connection, url, mac, signal_update) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 52d586d088ac2b..992dc33a697711 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -14,13 +14,14 @@ LoginErrorUsernamePasswordOverrunException, ResponseErrorException, ) +from requests.exceptions import Timeout from url_normalize import url_normalize import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME from homeassistant.core import callback -from .const import DEFAULT_DEVICE_NAME +from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME # https://github.com/PyCQA/pylint/issues/3202 from .const import DOMAIN # pylint: disable=unused-import @@ -115,12 +116,18 @@ def try_connect(username: Optional[str], password: Optional[str]) -> Connection: """Try connecting with given credentials.""" if username or password: conn = AuthorizedConnection( - user_input[CONF_URL], username=username, password=password + user_input[CONF_URL], + username=username, + password=password, + timeout=CONNECTION_TIMEOUT, ) else: try: conn = AuthorizedConnection( - user_input[CONF_URL], username="", password="" + user_input[CONF_URL], + username="", + password="", + timeout=CONNECTION_TIMEOUT, ) user_input[CONF_USERNAME] = "" user_input[CONF_PASSWORD] = "" @@ -129,7 +136,7 @@ def try_connect(username: Optional[str], password: Optional[str]) -> Connection: "Could not login with empty credentials, proceeding unauthenticated", exc_info=True, ) - conn = Connection(user_input[CONF_URL]) + conn = Connection(user_input[CONF_URL], timeout=CONNECTION_TIMEOUT) del user_input[CONF_USERNAME] del user_input[CONF_PASSWORD] return conn @@ -170,6 +177,9 @@ def get_router_title(conn: Connection) -> str: except ResponseErrorException: _LOGGER.warning("Response error", exc_info=True) errors["base"] = "response_error" + except Timeout: + _LOGGER.warning("Connection timeout", exc_info=True) + errors[CONF_URL] = "connection_timeout" except Exception: # pylint: disable=broad-except _LOGGER.warning("Unknown error connecting to device", exc_info=True) errors[CONF_URL] = "unknown_connection_error" diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 3d99261e6ede82..8dae63f6538b63 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -10,6 +10,8 @@ UNIT_BYTES = "B" UNIT_SECONDS = "s" +CONNECTION_TIMEOUT = 10 + KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 8681e3355a46c5..2e76cf1b343963 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -11,6 +11,7 @@ "invalid_url": "Invalid URL", "login_attempts_exceeded": "Maximum login attempts exceeded, please try again later", "response_error": "Unknown error from device", + "connection_timeout": "Connection timeout", "unknown_connection_error": "Unknown error connecting to device" }, "step": { From ecf2e9c0ab781b1b5b2af68905bf043fea3f3ea2 Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 3 Nov 2019 18:32:01 +0100 Subject: [PATCH 1403/3953] Fix flaky Samsung TV tests (#28503) --- tests/components/samsungtv/test_media_player.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index c178710e3f944d..deb39b4077fe76 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -2,6 +2,7 @@ import asyncio from asynctest import mock from datetime import timedelta +import logging import pytest from samsungctl import exceptions from tests.common import MockDependency, async_fire_time_changed @@ -263,6 +264,7 @@ async def test_send_key_autodetect_websocket(hass, remote): async def test_send_key_autodetect_websocket_exception(hass, caplog): """Test for send key with autodetection of protocol.""" + caplog.set_level(logging.DEBUG) with patch( "samsungctl.Remote", side_effect=[exceptions.AccessDenied("Boom"), mock.DEFAULT] ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): @@ -458,6 +460,7 @@ async def test_turn_off_legacy(hass, remote): async def test_turn_off_os_error(hass, remote, caplog): """Test for turn_off with OSError.""" + caplog.set_level(logging.DEBUG) await setup_samsungtv(hass, MOCK_CONFIG) remote.close = mock.Mock(side_effect=OSError("BOOM")) assert await hass.services.async_call( From 5fd9b474dca3c9eaca6ad6f86625cc54ba88175f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 3 Nov 2019 20:36:02 +0100 Subject: [PATCH 1404/3953] Always provide brightness value (#28228) HA will remove attribute when light is off, but google expect all trait data all the time. --- homeassistant/components/google_assistant/trait.py | 2 ++ tests/components/google_assistant/test_smart_home.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 7d6e79a82372be..6b5530ab2cee0a 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -185,6 +185,8 @@ def query_attributes(self): brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS) if brightness is not None: response["brightness"] = int(100 * (brightness / 255)) + else: + response["brightness"] = 0 return response diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 2f7fdb8e131297..3c3801b35c699f 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -226,7 +226,7 @@ async def test_query_message(hass): "payload": { "devices": { "light.non_existing": {"online": False}, - "light.demo_light": {"on": False, "online": True}, + "light.demo_light": {"on": False, "online": True, "brightness": 0}, "light.another_light": { "on": True, "online": True, From 0768ae2dc80e584c8f14a930248f97646315de37 Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 3 Nov 2019 23:56:08 +0100 Subject: [PATCH 1405/3953] Fix flaky YesssSMS tests on debug messages (#28506) --- tests/components/yessssms/test_notify.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py index 5cc204ccc4d4ca..dbc33b5a388725 100644 --- a/tests/components/yessssms/test_notify.py +++ b/tests/components/yessssms/test_notify.py @@ -2,6 +2,7 @@ import unittest from unittest.mock import patch +import logging import pytest import requests_mock @@ -101,6 +102,7 @@ async def test_false_login_data_error(hass, caplog, valid_settings, invalid_logi async def test_init_success(hass, caplog, valid_settings, valid_login_data): """Test for successful init of yessssms.""" + caplog.set_level(logging.DEBUG) await valid_settings assert hass.services.has_service("notify", "sms") messages = [] @@ -119,6 +121,7 @@ async def test_init_success(hass, caplog, valid_settings, valid_login_data): async def test_connection_error_on_init(hass, caplog, valid_settings, connection_error): """Test for connection error on init.""" + caplog.set_level(logging.DEBUG) await valid_settings assert hass.services.has_service("notify", "sms") for record in caplog.records: From 734e98282268cbd21b5eee7013181ae77d535e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 4 Nov 2019 01:05:01 +0200 Subject: [PATCH 1406/3953] Import CancelledError from asyncio, not .futures (#28511) It's no longer in .futures in Python 3.8.0. --- homeassistant/components/bluesound/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 702cf5ddc30172..762f231b341089 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -1,6 +1,6 @@ """Support for Bluesound devices.""" import asyncio -from asyncio.futures import CancelledError +from asyncio import CancelledError from datetime import timedelta import logging from urllib import parse From 9b038bd10d5945a33ce2ad7079bd71a613a2d658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 4 Nov 2019 01:24:24 +0200 Subject: [PATCH 1407/3953] Don't use deprecated encoding to json.loads (#28509) Will be removed in 3.9, ignored in earlier supported versions. --- tests/components/homematicip_cloud/helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 78c78ec0ab9b3f..494ec2dc90bc0f 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -92,7 +92,7 @@ def __init__(self, connection=None, home_name=""): def init_home(self, json_path=HOME_JSON): """Init template with json.""" - self.init_json_state = json.loads(load_fixture(HOME_JSON), encoding="UTF-8") + self.init_json_state = json.loads(load_fixture(HOME_JSON)) self.update_home(json_state=self.init_json_state, clearConfig=True) return self From 4e40394972fb35da8ba89c74e1ee4b00afa8c2e4 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 4 Nov 2019 00:58:35 +0100 Subject: [PATCH 1408/3953] Fix Airly if more than one config entry (#28498) --- homeassistant/components/airly/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 80f3518c652832..ce165918ac244a 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -31,6 +31,8 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: """Set up configured Airly.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} return True @@ -49,8 +51,6 @@ async def async_setup_entry(hass, config_entry): if not airly.data: raise ConfigEntryNotReady() - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_CLIENT] = {} hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly hass.async_create_task( From 0973f1961d07bf7a1258271c5ec636231256c972 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 4 Nov 2019 00:32:38 +0000 Subject: [PATCH 1409/3953] [ci skip] Translation update --- homeassistant/components/huawei_lte/.translations/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 4e366e0ce31b20..8681e3355a46c5 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -5,7 +5,6 @@ }, "error": { "connection_failed": "Connection failed", - "connection_timeout": "Connection timeout", "incorrect_password": "Incorrect password", "incorrect_username": "Incorrect username", "incorrect_username_or_password": "Incorrect username or password", From cb2f42b3367ca44af5dc5181a3f3d3569ad2d42d Mon Sep 17 00:00:00 2001 From: Kevin McCormack Date: Sun, 3 Nov 2019 22:11:14 -0500 Subject: [PATCH 1410/3953] Update Vivotek component stream source (#27941) * Update Vivotek component Fix building stream URL * Update Vivotek component Make stream path optionally configurable. * Update Vivotek camera integration Use f-string to build stream source URL. This improve readability and I hear it runs faster. --- homeassistant/components/vivotek/camera.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index 012c1e1df34755..c39a9b495bda07 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -20,9 +20,12 @@ CONF_FRAMERATE = "framerate" +CONF_STREAM_PATH = "stream_path" + DEFAULT_CAMERA_BRAND = "Vivotek" DEFAULT_NAME = "Vivotek Camera" DEFAULT_EVENT_0_KEY = "event_i0_enable" +DEFAULT_STREAM_SOURCE = "live.sdp" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -33,12 +36,14 @@ vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, + vol.Optional(CONF_STREAM_PATH, default=DEFAULT_STREAM_SOURCE): cv.string, } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a Vivotek IP Camera.""" + creds = f"{config[CONF_USERNAME]}:{config[CONF_PASSWORD]}" args = dict( config=config, cam=VivotekCamera( @@ -48,12 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): usr=config[CONF_USERNAME], pwd=config[CONF_PASSWORD], ), - stream_source=( - "rtsp://%s:%s@%s:554/live.sdp", - config[CONF_USERNAME], - config[CONF_PASSWORD], - config[CONF_IP_ADDRESS], - ), + stream_source=f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}", ) add_entities([VivotekCam(**args)], True) From 7e6bcb85b73a079f3447bc42efc97a95288c1782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 4 Nov 2019 09:12:15 +0200 Subject: [PATCH 1411/3953] Don't fail tox pylint if PYLINT_ARGS is not set (#28403) Closes https://github.com/home-assistant/home-assistant/issues/28342 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 118d0722b7c173..2898c410b2ad0f 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - pylint {env:PYLINT_ARGS} {posargs} homeassistant + pylint {env:PYLINT_ARGS:} {posargs} homeassistant [testenv:lint] deps = From c01b7bbf283fa633e94f07498ce6b1f95250f099 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Nov 2019 09:03:58 +0100 Subject: [PATCH 1412/3953] Upgrade pillow to 6.2.1 (#28442) --- homeassistant/components/image_processing/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 6a88a358f1d5c9..e986ac6f4ca193 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -3,7 +3,7 @@ "name": "Image processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", "requirements": [ - "pillow==6.2.0" + "pillow==6.2.1" ], "dependencies": [ "camera" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index e3f62514801000..39f7b9064fc69a 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/integrations/proxy", "requirements": [ - "pillow==6.2.0" + "pillow==6.2.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index a3130070cc3518..1f8caa3753a5d1 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/integrations/qrcode", "requirements": [ - "pillow==6.2.0", + "pillow==6.2.1", "pyzbar==0.1.7" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index cf2c0cb7918fc5..38770a24686b64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -968,7 +968,7 @@ pilight==0.1.1 # homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -pillow==6.2.0 +pillow==6.2.1 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7b4e4ffb014a0..89a86a8ce2f8f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ pilight==0.1.1 # homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -pillow==6.2.0 +pillow==6.2.1 # homeassistant.components.plex plexapi==3.0.6 From 7cbd55a8179b54a1559f5c6a21e66bfd365d8918 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 4 Nov 2019 09:55:12 +0100 Subject: [PATCH 1413/3953] Add dump config service to HomematicIP Cloud (#28231) * Add dump config service to HomematicIP Cloud * Mock builin.open * Fix test * reduce SGTIN if anonymize * apply review feedback --- .../components/homematicip_cloud/__init__.py | 50 ++++++++++++++++++- .../homematicip_cloud/services.yaml | 13 ++++- .../components/homematicip_cloud/test_init.py | 15 +++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 9a0eb65aa3fd1b..d6ae05c463a5c9 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,7 +1,9 @@ """Support for HomematicIP Cloud devices.""" import logging +from pathlib import Path from homematicip.aio.group import AsyncHeatingGroup +from homematicip.base.helpers import handle_config import voluptuous as vol from homeassistant import config_entries @@ -26,17 +28,23 @@ _LOGGER = logging.getLogger(__name__) +ATTR_ACCESSPOINT_ID = "accesspoint_id" +ATTR_ANONYMIZE = "anonymize" ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index" +ATTR_CONFIG_OUTPUT_FILE_PREFIX = "config_output_file_prefix" +ATTR_CONFIG_OUTPUT_PATH = "config_output_path" ATTR_DURATION = "duration" ATTR_ENDTIME = "endtime" ATTR_TEMPERATURE = "temperature" -ATTR_ACCESSPOINT_ID = "accesspoint_id" + +DEFAULT_CONFIG_FILE_PREFIX = "hmip-config" SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION = "activate_eco_mode_with_duration" SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD = "activate_eco_mode_with_period" SERVICE_ACTIVATE_VACATION = "activate_vacation" SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode" SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" +SERVICE_DUMP_HAP_CONFIG = "dump_hap_config" SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" CONFIG_SCHEMA = vol.Schema( @@ -96,6 +104,16 @@ } ) +SCHEMA_DUMP_HAP_CONFIG = vol.Schema( + { + vol.Optional(ATTR_CONFIG_OUTPUT_PATH): cv.string, + vol.Optional( + ATTR_CONFIG_OUTPUT_FILE_PREFIX, default=DEFAULT_CONFIG_FILE_PREFIX + ): cv.string, + vol.Optional(ATTR_ANONYMIZE, default=True): cv.boolean, + } +) + async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" @@ -239,6 +257,36 @@ async def _set_active_climate_profile(service): schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, ) + async def _async_dump_hap_config(service): + """Service to dump the configuration of a Homematic IP Access Point.""" + config_path = ( + service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir + ) + config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX] + anonymize = service.data[ATTR_ANONYMIZE] + + for hap in hass.data[DOMAIN].values(): + hap_sgtin = hap.config_entry.title + + if anonymize: + hap_sgtin = hap_sgtin[-4:] + + file_name = f"{config_file_prefix}_{hap_sgtin}.json" + path = Path(config_path) + config_file = path / file_name + + json_state = await hap.home.download_configuration() + json_state = handle_config(json_state, anonymize) + + config_file.write_text(json_state, encoding="utf8") + + hass.services.async_register( + DOMAIN, + SERVICE_DUMP_HAP_CONFIG, + _async_dump_hap_config, + schema=SCHEMA_DUMP_HAP_CONFIG, + ) + def _get_home(hapid: str): """Return a HmIP home.""" hap = hass.data[DOMAIN].get(hapid) diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index f426c9b5d220be..9a7d90eba9c64d 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -57,4 +57,15 @@ set_active_climate_profile: description: The index of the climate profile (1 based) example: 1 - +dump_hap_config: + description: Dump the configuration of the Homematic IP Access Point(s). + fields: + config_output_path: + description: (Default is 'Your home-assistant config directory') Path where to store the config. + example: '/config' + config_output_file_prefix: + description: (Default is 'hmip-config') Name of the config file. The SGTIN of the AP will always be appended. + example: 'hmip-config' + anonymize: + description: (Default is True) Should the Configuration be anonymized? + example: True diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 894db2e691b2df..ba27a619e6aa4f 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -5,7 +5,7 @@ from homeassistant.components import homematicip_cloud as hmipc from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_coro +from tests.common import Mock, MockConfigEntry, mock_coro async def test_config_with_accesspoint_passed_to_config_entry(hass): @@ -150,3 +150,16 @@ async def test_unload_entry(hass): assert await hmipc.async_unload_entry(hass, entry) assert len(mock_hap.return_value.async_reset.mock_calls) == 1 assert hass.data[hmipc.DOMAIN] == {} + + +async def test_hmip_dump_hap_config_services(hass, mock_hap_with_service): + """Test dump configuration services.""" + + with patch("pathlib.Path.write_text", return_value=Mock()) as write_mock: + await hass.services.async_call( + "homematicip_cloud", "dump_hap_config", {"anonymize": True}, blocking=True + ) + home = mock_hap_with_service.home + assert home.mock_calls[-1][0] == "download_configuration" + assert len(home.mock_calls) == 8 # pylint: disable=W0212 + assert len(write_mock.mock_calls) > 0 From 381bf987d24f885166ef20f10e0b8db0214b34ce Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Nov 2019 09:56:27 +0100 Subject: [PATCH 1414/3953] Upgrade TwitterAPI to 2.5.10 (#28401) --- homeassistant/components/twitter/manifest.json | 2 +- homeassistant/components/twitter/notify.py | 3 +-- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index 2713004343ba5e..d63259bcbeeb02 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -3,7 +3,7 @@ "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", "requirements": [ - "TwitterAPI==2.5.9" + "TwitterAPI==2.5.10" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index 2991bea8d2e6d4..39faf987ae0935 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -6,6 +6,7 @@ import mimetypes import os +from TwitterAPI import TwitterAPI import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME @@ -62,8 +63,6 @@ def __init__( username, ): """Initialize the service.""" - from TwitterAPI import TwitterAPI - self.user = username self.hass = hass self.api = TwitterAPI( diff --git a/requirements_all.txt b/requirements_all.txt index 38770a24686b64..d7a8e1be3b7caa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -94,7 +94,7 @@ RtmAPI==0.7.2 TravisPy==0.3.5 # homeassistant.components.twitter -TwitterAPI==2.5.9 +TwitterAPI==2.5.10 # homeassistant.components.tof # VL53L1X2==0.1.5 From e91bb1ab087709954e8c5e880810508070ecc802 Mon Sep 17 00:00:00 2001 From: Tom Robinson Date: Mon, 4 Nov 2019 00:56:36 -0800 Subject: [PATCH 1415/3953] Replace Netatmo CO2 sensor icon (#28520) --- homeassistant/components/netatmo/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 70b6297388cd9c..3d4b5f2fa5151a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -62,7 +62,7 @@ "mdi:thermometer", DEVICE_CLASS_TEMPERATURE, ], - "co2": ["CO2", "ppm", "mdi:cloud", None], + "co2": ["CO2", "ppm", "mdi:periodic-table-co2", None], "pressure": ["Pressure", "mbar", "mdi:gauge", None], "noise": ["Noise", "dB", "mdi:volume-high", None], "humidity": ["Humidity", "%", "mdi:water-percent", DEVICE_CLASS_HUMIDITY], From fe00f3558e4d95d087bd97fec15c6ad37c48299d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Nov 2019 09:56:46 +0100 Subject: [PATCH 1416/3953] Imports twitch (#28517) * Move imports * Add unique_id --- homeassistant/components/twitch/sensor.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index b0051e1c765d23..f4276160d6c472 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -1,11 +1,13 @@ """Support for the Twitch stream status.""" import logging +from requests.exceptions import HTTPError +from twitch import TwitchClient import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -14,6 +16,7 @@ CONF_CHANNELS = "channels" CONF_CLIENT_ID = "client_id" + ICON = "mdi:twitch" STATE_OFFLINE = "offline" @@ -22,18 +25,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CHANNELS, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_CHANNELS): vol.All(cv.ensure_list, [cv.string]), } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Twitch platform.""" - from twitch import TwitchClient - from requests.exceptions import HTTPError - - channels = config.get(CONF_CHANNELS, []) - client = TwitchClient(client_id=config.get(CONF_CLIENT_ID)) + channels = config[CONF_CHANNELS] + client_id = config[CONF_CLIENT_ID] + client = TwitchClient(client_id=client_id) try: client.ingests.get_server_list() @@ -55,8 +56,7 @@ def __init__(self, user, client): self._user = user self._channel = self._user.name self._id = self._user.id - self._state = STATE_OFFLINE - self._preview = self._game = self._title = None + self._state = self._preview = self._game = self._title = None @property def should_poll(self): @@ -84,6 +84,11 @@ def device_state_attributes(self): if self._state == STATE_STREAMING: return {ATTR_GAME: self._game, ATTR_TITLE: self._title} + @property + def unique_id(self): + """Return unique ID for this sensor.""" + return self._id + @property def icon(self): """Icon to use in the frontend, if any.""" From cb19827932219556bda69902136e6048073ece7e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Nov 2019 09:56:58 +0100 Subject: [PATCH 1417/3953] Upgrade paho-mqtt to 1.5.0 (#28423) --- homeassistant/components/mqtt/manifest.json | 2 +- homeassistant/components/shiftr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index c1b6659e1c0733..b8ec38ec100896 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/mqtt", "requirements": [ "hbmqtt==0.9.5", - "paho-mqtt==1.4.0" + "paho-mqtt==1.5.0" ], "dependencies": [ "http" diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json index 282d86ce4195c4..b228909a59e377 100644 --- a/homeassistant/components/shiftr/manifest.json +++ b/homeassistant/components/shiftr/manifest.json @@ -3,7 +3,7 @@ "name": "Shiftr", "documentation": "https://www.home-assistant.io/integrations/shiftr", "requirements": [ - "paho-mqtt==1.4.0" + "paho-mqtt==1.5.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d7a8e1be3b7caa..07095414b0b30e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -933,7 +933,7 @@ orvibo==1.1.1 # homeassistant.components.mqtt # homeassistant.components.shiftr -paho-mqtt==1.4.0 +paho-mqtt==1.5.0 # homeassistant.components.panasonic_bluray panacotta==0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89a86a8ce2f8f5..6499ac046816e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -324,7 +324,7 @@ oauth2client==4.0.0 # homeassistant.components.mqtt # homeassistant.components.shiftr -paho-mqtt==1.4.0 +paho-mqtt==1.5.0 # homeassistant.components.aruba # homeassistant.components.cisco_ios From 60d7f730c316639f9a04b239a4dbb2ca9e769312 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Nov 2019 09:57:20 +0100 Subject: [PATCH 1418/3953] Upgrade jinja2 to >=2.10.3 (#28422) --- 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 03e43ba139a24e..7a7d83319520d3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ distro==1.4.0 hass-nabucasa==0.23 home-assistant-frontend==20191025.1 importlib-metadata==0.23 -jinja2>=2.10.1 +jinja2>=2.10.3 netdisco==2.6.0 pip>=8.0.3 python-slugify==4.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 07095414b0b30e..b831ec7af894e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -7,7 +7,7 @@ bcrypt==3.1.7 certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" importlib-metadata==0.23 -jinja2>=2.10.1 +jinja2>=2.10.3 PyJWT==1.7.1 cryptography==2.8 pip>=8.0.3 diff --git a/setup.py b/setup.py index 612ac7d0ce8f79..99195ee2f5663b 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ "certifi>=2019.9.11", 'contextvars==2.4;python_version<"3.7"', "importlib-metadata==0.23", - "jinja2>=2.10.1", + "jinja2>=2.10.3", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==2.8", From de1799d486bf0c55c14aa8672f6e0c84d454443b Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Mon, 4 Nov 2019 00:58:27 -0800 Subject: [PATCH 1419/3953] iaqualink: better handling of failures (#28514) --- .../components/iaqualink/__init__.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index c3fa2bb1eb8d44..fc1eb3b248ae9e 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -4,6 +4,7 @@ import logging from typing import Any, Dict +import aiohttp.client_exceptions import voluptuous as vol from iaqualink import ( @@ -26,6 +27,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -90,8 +92,14 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None try: await aqualink.login() except AqualinkLoginException as login_exception: - _LOGGER.error("Exception raised while attempting to login: %s", login_exception) + _LOGGER.error("Failed to login: %s", login_exception) return False + except ( + asyncio.TimeoutError, + aiohttp.client_exceptions.ClientConnectorError, + ) as aio_exception: + _LOGGER.warning("Exception raised while attempting to login: %s", aio_exception) + raise ConfigEntryNotReady systems = await aqualink.get_systems() systems = list(systems.values()) @@ -133,7 +141,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None async def _async_systems_update(now): """Refresh internal state for all systems.""" + prev = systems[0].last_run_success + await systems[0].update() + success = systems[0].last_run_success + + if not success and prev: + _LOGGER.warning("Failed to refresh iAqualink state") + elif success and not prev: + _LOGGER.warning("Reconnected to iAqualink") + async_dispatcher_send(hass, DOMAIN) async_track_time_interval(hass, _async_systems_update, UPDATE_INTERVAL) From 40b676c06f0d97504e8a18f4646e39acbeb266bf Mon Sep 17 00:00:00 2001 From: Tobias Efinger Date: Mon, 4 Nov 2019 10:29:29 +0100 Subject: [PATCH 1420/3953] Add services description for ness alarm (#28250) --- .../components/ness_alarm/services.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/ness_alarm/services.yaml b/homeassistant/components/ness_alarm/services.yaml index e69de29bb2d1d6..27f8ba383dde6c 100644 --- a/homeassistant/components/ness_alarm/services.yaml +++ b/homeassistant/components/ness_alarm/services.yaml @@ -0,0 +1,19 @@ +# Describes the format for available ness alarm services + +aux: + description: Trigger an aux output. + fields: + output_id: + description: The aux output you wish to change. A number from 1-4. + example: 1 + state: + description: The On/Off State, represented as true/false. Default is true. If P14xE 8E is enabled then a value of true will pulse output x for the time specified in P14(x+4)E. + example: true + default: true + +panic: + description: Trigger a panic + fields: + code: + description: The user code to use to trigger the panic. + example: 1234 \ No newline at end of file From fadb9bdfb371cdd42e82f0cbd71be48ae27a1964 Mon Sep 17 00:00:00 2001 From: Jonas Janz Date: Mon, 4 Nov 2019 10:58:39 +0100 Subject: [PATCH 1421/3953] Add information to IFTTT services.yaml (#28385) * docs(ifttt): add information to services.yaml * docs(ifttt): start examples lowercase * docs(ifttt): start examples with capital letter * docs(ifttt): end description on period --- homeassistant/components/ifttt/services.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/homeassistant/components/ifttt/services.yaml b/homeassistant/components/ifttt/services.yaml index e69de29bb2d1d6..8669bc07fb4387 100644 --- a/homeassistant/components/ifttt/services.yaml +++ b/homeassistant/components/ifttt/services.yaml @@ -0,0 +1,18 @@ +# Describes the format for available ifttt services + + +trigger: + description: Triggers the configured IFTTT Webhook. + fields: + event: + description: The name of the event to sent. + example: 'MY_HA_EVENT' + value1: + description: Generic field to send data via the event. + example: 'Hello World' + value2: + description: Generic field to send data via the event. + example: 'some additional data' + value3: + description: Generic field to send data via the event. + example: 'even more data' \ No newline at end of file From 6004ef3279ce5ce0dc29ed5a049196bf0f7c21df Mon Sep 17 00:00:00 2001 From: Oscar Tin Lai Date: Mon, 4 Nov 2019 21:10:59 +1100 Subject: [PATCH 1422/3953] Expose set auto mode for all Dyson fans (#28488) Set auto mode should be exposed to all dyson fans (e.g. *Pure Cool Link* and *Pure Hot+Cool Link*) instead of only *Pure Cool*, as it is support in all of the models (i.e. similar to the set night mode). --- homeassistant/components/dyson/fan.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 0165044b839cf4..341919935d0e44 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -151,14 +151,14 @@ def service_handle(service): service_handle, schema=DYSON_SET_NIGHT_MODE_SCHEMA, ) - if has_purecool_devices: - hass.services.register( - DYSON_DOMAIN, - SERVICE_SET_AUTO_MODE, - service_handle, - schema=SET_AUTO_MODE_SCHEMA, - ) + hass.services.register( + DYSON_DOMAIN, + SERVICE_SET_AUTO_MODE, + service_handle, + schema=SET_AUTO_MODE_SCHEMA, + ) + if has_purecool_devices: hass.services.register( DYSON_DOMAIN, SERVICE_SET_ANGLE, service_handle, schema=SET_ANGLE_SCHEMA ) From 9b72a55d60c5ac07c494d2bde17a79e75b348773 Mon Sep 17 00:00:00 2001 From: Luca Zimmermann Date: Mon, 4 Nov 2019 11:11:10 +0100 Subject: [PATCH 1423/3953] Add compatibility for other STBY Codes (#28478) Added PWR2 as valid standby code --- homeassistant/components/pioneer/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 0c6de9803e1837..51f55d4e851ccc 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -169,6 +169,8 @@ def name(self): @property def state(self): """Return the state of the device.""" + if self._pwstate == "PWR2": + return STATE_OFF if self._pwstate == "PWR1": return STATE_OFF if self._pwstate == "PWR0": From 552fbda58b3cd66104a780712ebb6ac1233630de Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Nov 2019 02:12:04 -0800 Subject: [PATCH 1424/3953] Remove legacy reproduce state (#28458) * Remove legacy reproduce state * Fix imports --- .../components/homeassistant/scene.py | 8 +- homeassistant/components/scene/__init__.py | 3 +- homeassistant/helpers/state.py | 144 +----------------- 3 files changed, 12 insertions(+), 143 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 45560d30edbb30..084c950bf1782f 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -14,7 +14,7 @@ STATE_ON, SERVICE_RELOAD, ) -from homeassistant.core import State, DOMAIN +from homeassistant.core import State, DOMAIN as HA_DOMAIN from homeassistant import config as conf_util from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_get_integration @@ -23,7 +23,7 @@ config_validation as cv, entity_platform, ) -from homeassistant.helpers.state import HASS_DOMAIN, async_reproduce_state +from homeassistant.helpers.state import async_reproduce_state from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene @@ -60,7 +60,7 @@ def _convert_states(states): PLATFORM_SCHEMA = vol.Schema( { - vol.Required(CONF_PLATFORM): HASS_DOMAIN, + vol.Required(CONF_PLATFORM): HA_DOMAIN, vol.Required(STATES): vol.All( cv.ensure_list, [ @@ -114,7 +114,7 @@ async def reload_config(call): # 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: + if p_type != HA_DOMAIN: continue _process_scenes_config(hass, async_add_entities, p_config) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index ec2dc3118a911f..63a64f34fe90bb 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -8,7 +8,6 @@ from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON 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 @@ -21,7 +20,7 @@ def _hass_domain_validator(config): """Validate platform in config for homeassistant domain.""" if CONF_PLATFORM not in config: - config = {CONF_PLATFORM: HASS_DOMAIN, STATES: config} + config = {CONF_PLATFORM: HA_DOMAIN, STATES: config} return config diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 4cb7fb85bff29a..abc97bf1f8ae4b 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,35 +1,15 @@ """Helpers that help with state related things.""" import asyncio import datetime as dt -import json import logging from collections import defaultdict from types import ModuleType, TracebackType -from typing import Awaitable, Dict, Iterable, List, Optional, Tuple, Type, Union +from typing import Dict, Iterable, List, Optional, Type, Union 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 -from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION from homeassistant.const import ( - ATTR_ENTITY_ID, - SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_HOME, - SERVICE_ALARM_DISARM, - SERVICE_ALARM_TRIGGER, - SERVICE_LOCK, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - SERVICE_UNLOCK, - SERVICE_OPEN_COVER, - SERVICE_CLOSE_COVER, - SERVICE_SET_COVER_POSITION, - SERVICE_SET_COVER_TILT_POSITION, - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, STATE_CLOSED, STATE_HOME, STATE_LOCKED, @@ -40,36 +20,11 @@ STATE_UNKNOWN, STATE_UNLOCKED, ) -from homeassistant.core import Context, State, DOMAIN as HASS_DOMAIN +from homeassistant.core import Context, State from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) -GROUP_DOMAIN = "group" - -# Update this dict of lists when new services are added to HA. -# Each item is a service with a list of required attributes. -SERVICE_ATTRIBUTES = { - SERVICE_NOTIFY: [ATTR_MESSAGE], - SERVICE_SET_COVER_POSITION: [ATTR_POSITION], - SERVICE_SET_COVER_TILT_POSITION: [ATTR_TILT_POSITION], -} - -# Update this dict when new services are added to HA. -# Each item is a service with a corresponding state. -SERVICE_TO_STATE = { - SERVICE_TURN_ON: STATE_ON, - SERVICE_TURN_OFF: STATE_OFF, - SERVICE_ALARM_ARM_AWAY: STATE_ALARM_ARMED_AWAY, - SERVICE_ALARM_ARM_HOME: STATE_ALARM_ARMED_HOME, - SERVICE_ALARM_DISARM: STATE_ALARM_DISARMED, - SERVICE_ALARM_TRIGGER: STATE_ALARM_TRIGGERED, - SERVICE_LOCK: STATE_LOCKED, - SERVICE_UNLOCK: STATE_UNLOCKED, - SERVICE_OPEN_COVER: STATE_OPEN, - SERVICE_CLOSE_COVER: STATE_CLOSED, -} - class AsyncTrackStates: """ @@ -109,18 +64,6 @@ def get_changed_since( return [state for state in states if state.last_updated >= utc_point_in_time] -@bind_hass -def reproduce_state( - hass: HomeAssistantType, - states: Union[State, Iterable[State]], - blocking: bool = False, -) -> None: - """Reproduce given state.""" - return asyncio.run_coroutine_threadsafe( - async_reproduce_state(hass, states, blocking), hass.loop - ).result() - - @bind_hass async def async_reproduce_state( hass: HomeAssistantType, @@ -149,16 +92,12 @@ async def worker(domain: str, states_by_domain: List[State]) -> None: try: platform: Optional[ModuleType] = integration.get_platform("reproduce_state") except ImportError: - platform = None + _LOGGER.warning("Integration %s does not support reproduce state", domain) + return - if platform: - await platform.async_reproduce_states( # type: ignore - hass, states_by_domain, context=context - ) - else: - await async_reproduce_state_legacy( - hass, domain, states_by_domain, blocking=blocking, context=context - ) + await platform.async_reproduce_states( # type: ignore + hass, states_by_domain, context=context + ) if to_call: # run all domains in parallel @@ -167,75 +106,6 @@ async def worker(domain: str, states_by_domain: List[State]) -> None: ) -@bind_hass -async def async_reproduce_state_legacy( - hass: HomeAssistantType, - domain: str, - states: Iterable[State], - blocking: bool = False, - context: Optional[Context] = None, -) -> None: - """Reproduce given state.""" - to_call: Dict[Tuple[str, str], List[str]] = defaultdict(list) - - if domain == GROUP_DOMAIN: - service_domain = HASS_DOMAIN - else: - service_domain = domain - - for state in states: - - if hass.states.get(state.entity_id) is None: - _LOGGER.warning( - "reproduce_state: Unable to find entity %s", state.entity_id - ) - continue - - domain_services = hass.services.async_services().get(service_domain) - - if not domain_services: - _LOGGER.warning("reproduce_state: Unable to reproduce state %s (1)", state) - continue - - service = None - for _service in domain_services.keys(): - if ( - _service in SERVICE_ATTRIBUTES - and all( - attr in state.attributes for attr in SERVICE_ATTRIBUTES[_service] - ) - or _service in SERVICE_TO_STATE - and SERVICE_TO_STATE[_service] == state.state - ): - service = _service - if ( - _service in SERVICE_TO_STATE - and SERVICE_TO_STATE[_service] == state.state - ): - break - - if not service: - _LOGGER.warning("reproduce_state: Unable to reproduce state %s (2)", state) - continue - - # We group service calls for entities by service call - # json used to create a hashable version of dict with maybe lists in it - key = (service, json.dumps(dict(state.attributes), sort_keys=True)) - to_call[key].append(state.entity_id) - - domain_tasks: List[Awaitable[Optional[bool]]] = [] - for (service, service_data), entity_ids in to_call.items(): - data = json.loads(service_data) - data[ATTR_ENTITY_ID] = entity_ids - - domain_tasks.append( - hass.services.async_call(service_domain, service, data, blocking, context) - ) - - if domain_tasks: - await asyncio.wait(domain_tasks) - - def state_as_number(state: State) -> float: """ Try to coerce our state to a number. From b7296c61bc1f91314af00ce52a5b376657fb9095 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 4 Nov 2019 12:05:39 +0100 Subject: [PATCH 1425/3953] Align attribute naming between light and switch for HomematicIP Cloud (#28271) --- homeassistant/components/homematicip_cloud/light.py | 12 ++++++------ tests/components/homematicip_cloud/test_light.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 46a8d95729ff19..044140e5582344 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -28,8 +28,8 @@ _LOGGER = logging.getLogger(__name__) -ATTR_ENERGY_COUNTER = "energy_counter_kwh" -ATTR_POWER_CONSUMPTION = "power_consumption" +ATTR_TODAY_ENERGY_KWH = "today_energy_kwh" +ATTR_CURRENT_POWER_W = "current_power_w" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -95,11 +95,11 @@ def device_state_attributes(self): """Return the state attributes of the generic device.""" state_attr = super().device_state_attributes - current_power_consumption = self._device.currentPowerConsumption - if current_power_consumption > 0.05: - state_attr[ATTR_POWER_CONSUMPTION] = round(current_power_consumption, 2) + current_power_w = self._device.currentPowerConsumption + if current_power_w > 0.05: + state_attr[ATTR_CURRENT_POWER_W] = round(current_power_w, 2) - state_attr[ATTR_ENERGY_COUNTER] = round(self._device.energyCounter, 2) + state_attr[ATTR_TODAY_ENERGY_KWH] = round(self._device.energyCounter, 2) return state_attr diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index 17e92d9d99d98b..a55d9ea9151b2f 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -3,8 +3,8 @@ from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.light import ( - ATTR_ENERGY_COUNTER, - ATTR_POWER_CONSUMPTION, + ATTR_CURRENT_POWER_W, + ATTR_TODAY_ENERGY_KWH, ) from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -209,8 +209,8 @@ async def test_hmip_light_measuring(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON - assert ha_state.attributes[ATTR_POWER_CONSUMPTION] == 50 - assert ha_state.attributes[ATTR_ENERGY_COUNTER] == 6.33 + assert ha_state.attributes[ATTR_CURRENT_POWER_W] == 50 + assert ha_state.attributes[ATTR_TODAY_ENERGY_KWH] == 6.33 await hass.services.async_call( "light", "turn_off", {"entity_id": entity_id}, blocking=True From 33c8cba30d76f66411642fe2f29cc81f5d9b2bc2 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 4 Nov 2019 12:22:28 +0100 Subject: [PATCH 1426/3953] Enable transition time for HmIP-BSL - HomematicIP Cloud (#28201) * Enable transition time for HmIP-BSL - HomematicIP Cloud harden ACP fix hao device name * update test, initalize instance var --- .../components/homematicip_cloud/__init__.py | 2 +- .../homematicip_cloud/alarm_control_panel.py | 12 ++++-- .../components/homematicip_cloud/light.py | 22 ++++++++-- .../homematicip_cloud/test_light.py | 43 +++++++++++++------ 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index d6ae05c463a5c9..8cd41e0b980a7b 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -312,7 +312,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool device_registry = await dr.async_get_registry(hass) home = hap.home # Add the HAP name from configuration if set. - hapname = home.label if not home.name else f"{home.label} {home.name}" + hapname = home.label if not home.name else f"{home.name} {home.label}" device_registry.async_get_or_create( config_entry_id=home.id, identifiers={(DOMAIN, home.id)}, diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index f61bf6f6b56d40..a7b1beaec93868 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -52,11 +52,13 @@ def __init__(self, hap: HomematicipHAP, security_zones) -> None: """Initialize the alarm control panel.""" self._home = hap.home self.alarm_state = STATE_ALARM_DISARMED + self._internal_alarm_zone = None + self._external_alarm_zone = None for security_zone in security_zones: if security_zone.label == "INTERNAL": self._internal_alarm_zone = security_zone - else: + elif security_zone.label == "EXTERNAL": self._external_alarm_zone = security_zone @property @@ -110,8 +112,10 @@ async def async_alarm_arm_away(self, code=None): async def async_added_to_hass(self): """Register callbacks.""" - self._internal_alarm_zone.on_update(self._async_device_changed) - self._external_alarm_zone.on_update(self._async_device_changed) + if self._internal_alarm_zone: + self._internal_alarm_zone.on_update(self._async_device_changed) + if self._external_alarm_zone: + self._external_alarm_zone.on_update(self._async_device_changed) def _async_device_changed(self, *args, **kwargs): """Handle device state changes.""" @@ -146,7 +150,7 @@ def unique_id(self) -> str: def _get_zone_alarm_state(security_zone) -> bool: - if security_zone.active: + if security_zone and security_zone.active: if ( security_zone.sabotage or security_zone.motionDetected diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 044140e5582344..c262b05d019bde 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -16,6 +16,7 @@ ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_HS_COLOR, + ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, @@ -225,13 +226,28 @@ async def async_turn_on(self, **kwargs): # Minimum brightness is 10, otherwise the led is disabled brightness = max(10, brightness) dim_level = brightness / 255.0 - - await self._device.set_rgb_dim_level(self.channel, simple_rgb_color, dim_level) + transition = kwargs.get(ATTR_TRANSITION, 0.5) + + await self._device.set_rgb_dim_level_with_time( + channelIndex=self.channel, + rgb=simple_rgb_color, + dimLevel=dim_level, + onTime=0, + rampTime=transition, + ) async def async_turn_off(self, **kwargs): """Turn the light off.""" simple_rgb_color = self._func_channel.simpleRGBColorState - await self._device.set_rgb_dim_level(self.channel, simple_rgb_color, 0.0) + transition = kwargs.get(ATTR_TRANSITION, 0.5) + + await self._device.set_rgb_dim_level_with_time( + channelIndex=self.channel, + rgb=simple_rgb_color, + dimLevel=0.0, + onTime=0, + rampTime=transition, + ) def _convert_color(color) -> RGBColorState: diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index a55d9ea9151b2f..632a6aac449e4d 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -79,10 +79,19 @@ async def test_hmip_notification_light(hass, default_mock_hap): # Send all color via service call. await hass.services.async_call( - "light", "turn_on", {"entity_id": entity_id}, blocking=True + "light", + "turn_on", + {"entity_id": entity_id, "brightness_pct": "100", "transition": 100}, + blocking=True, ) - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.RED, 1.0) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level_with_time" + assert hmip_device.mock_calls[-1][2] == { + "channelIndex": 2, + "rgb": "RED", + "dimLevel": 1.0, + "onTime": 0, + "rampTime": 100.0, + } color_list = { RGBColorState.WHITE: [0.0, 0.0], @@ -101,17 +110,17 @@ async def test_hmip_notification_light(hass, default_mock_hap): {"entity_id": entity_id, "hs_color": hs_color}, blocking=True, ) - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - assert hmip_device.mock_calls[-1][1] == (2, color, 0.0392156862745098) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level_with_time" + assert hmip_device.mock_calls[-1][2] == { + "channelIndex": 2, + "dimLevel": 0.0392156862745098, + "onTime": 0, + "rampTime": 0.5, + "rgb": color, + } assert len(hmip_device.mock_calls) == service_call_counter + 8 - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - assert hmip_device.mock_calls[-1][1] == ( - 2, - RGBColorState.PURPLE, - 0.0392156862745098, - ) await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1, 2) await async_manipulate_test_data( hass, hmip_device, "simpleRGBColorState", RGBColorState.PURPLE, 2 @@ -122,11 +131,17 @@ async def test_hmip_notification_light(hass, default_mock_hap): assert ha_state.attributes[ATTR_BRIGHTNESS] == 255 await hass.services.async_call( - "light", "turn_off", {"entity_id": entity_id}, blocking=True + "light", "turn_off", {"entity_id": entity_id, "transition": 100}, blocking=True ) assert len(hmip_device.mock_calls) == service_call_counter + 11 - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.PURPLE, 0.0) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level_with_time" + assert hmip_device.mock_calls[-1][2] == { + "channelIndex": 2, + "dimLevel": 0.0, + "onTime": 0, + "rampTime": 100, + "rgb": "PURPLE", + } await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, 2) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF From 99c0559a0c07e1c8a7b1418247d5f888f6b04d3d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 4 Nov 2019 13:10:42 +0100 Subject: [PATCH 1427/3953] Speech to Text component (#28434) * Initial commit for STT * Fix code review --- CODEOWNERS | 1 + homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/stt.py | 60 ++++++ homeassistant/components/demo/tts.py | 2 +- homeassistant/components/stt/__init__.py | 217 +++++++++++++++++++++ homeassistant/components/stt/const.py | 48 +++++ homeassistant/components/stt/manifest.json | 8 + homeassistant/components/stt/services.yaml | 1 + tests/components/demo/test_stt.py | 69 +++++++ tests/components/stt/__init__.py | 1 + tests/components/stt/test_init.py | 29 +++ 11 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/demo/stt.py create mode 100644 homeassistant/components/stt/__init__.py create mode 100644 homeassistant/components/stt/const.py create mode 100644 homeassistant/components/stt/manifest.json create mode 100644 homeassistant/components/stt/services.yaml create mode 100644 tests/components/demo/test_stt.py create mode 100644 tests/components/stt/__init__.py create mode 100644 tests/components/stt/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index aed575b5271088..77a2ee8355b412 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -280,6 +280,7 @@ homeassistant/components/sql/* @dgomes homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stream/* @hunterjm +homeassistant/components/stt/* @pvizeli homeassistant/components/suez_water/* @ooii homeassistant/components/sun/* @Swamp-Ig homeassistant/components/supla/* @mwegrzynek diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index d93d217caa79f1..93a34794366ddc 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -25,6 +25,7 @@ "media_player", "notify", "sensor", + "stt", "switch", "tts", "mailbox", diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py new file mode 100644 index 00000000000000..90a4cc032964dc --- /dev/null +++ b/homeassistant/components/demo/stt.py @@ -0,0 +1,60 @@ +"""Support for the demo for speech to text service.""" +from typing import List + +from aiohttp import StreamReader + +from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult +from homeassistant.components.stt.const import ( + AudioBitrates, + AudioFormats, + AudioSamplerates, + AudioCodecs, + SpeechResultState, +) + +SUPPORT_LANGUAGES = ["en", "de"] + + +async def async_get_engine(hass, config): + """Set up Demo speech component.""" + return DemoProvider() + + +class DemoProvider(Provider): + """Demo speech API provider.""" + + @property + def supported_languages(self) -> List[str]: + """Return a list of supported languages.""" + return SUPPORT_LANGUAGES + + @property + def supported_formats(self) -> List[AudioFormats]: + """Return a list of supported formats.""" + return [AudioFormats.WAV] + + @property + def supported_codecs(self) -> List[AudioCodecs]: + """Return a list of supported codecs.""" + return [AudioCodecs.PCM] + + @property + def supported_bitrates(self) -> List[AudioBitrates]: + """Return a list of supported bitrates.""" + return [AudioBitrates.BITRATE_16] + + @property + def supported_samplerates(self) -> List[AudioSamplerates]: + """Return a list of supported samplerates.""" + return [AudioSamplerates.SAMPLERATE_16000, AudioSamplerates.SAMPLERATE_44100] + + async def async_process_audio_stream( + self, metadata: SpeechMetadata, stream: StreamReader + ) -> SpeechResult: + """Process an audio stream to STT service.""" + + # Read available data + async for _ in stream.iter_chunked(4096): + pass + + return SpeechResult("Turn the Kitchen Lights on", SpeechResultState.SUCCESS) diff --git a/homeassistant/components/demo/tts.py b/homeassistant/components/demo/tts.py index ae083e50454faf..441b0cc0b3c633 100644 --- a/homeassistant/components/demo/tts.py +++ b/homeassistant/components/demo/tts.py @@ -1,4 +1,4 @@ -"""Support for the demo speech service.""" +"""Support for the demo for text to speech service.""" import os import voluptuous as vol diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py new file mode 100644 index 00000000000000..ea5119b5e24b9a --- /dev/null +++ b/homeassistant/components/stt/__init__.py @@ -0,0 +1,217 @@ +"""Provide functionality to STT.""" +from abc import ABC, abstractmethod +import asyncio +import logging +from typing import Dict, List, Optional + +from aiohttp import StreamReader, web +from aiohttp.hdrs import istr +from aiohttp.web_exceptions import ( + HTTPNotFound, + HTTPUnsupportedMediaType, + HTTPBadRequest, +) +import attr + +from homeassistant.components.http import HomeAssistantView +from homeassistant.core import callback +from homeassistant.helpers import config_per_platform +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_prepare_setup_platform + +from .const import ( + DOMAIN, + AudioBitrates, + AudioCodecs, + AudioFormats, + AudioSamplerates, + SpeechResultState, +) + +# mypy: allow-untyped-defs, no-check-untyped-defs + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config): + """Set up STT.""" + providers = {} + + async def async_setup_platform(p_type, p_config, disc_info=None): + """Set up a TTS platform.""" + platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) + if platform is None: + return + + try: + provider = await platform.async_get_engine(hass, p_config) + if provider is None: + _LOGGER.error("Error setting up platform %s", p_type) + return + + provider.name = p_type + provider.hass = hass + + providers[provider.name] = provider + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error setting up platform: %s", p_type) + return + + setup_tasks = [ + async_setup_platform(p_type, p_config) + for p_type, p_config in config_per_platform(config, DOMAIN) + ] + + if setup_tasks: + await asyncio.wait(setup_tasks) + + hass.http.register_view(SpeechToTextView(providers)) + return True + + +@attr.s +class SpeechMetadata: + """Metadata of audio stream.""" + + language: str = attr.ib() + format: AudioFormats = attr.ib() + codec: AudioCodecs = attr.ib() + bitrate: AudioBitrates = attr.ib(converter=int) + samplerate: AudioSamplerates = attr.ib(converter=int) + + +@attr.s +class SpeechResult: + """Result of audio Speech.""" + + text: str = attr.ib() + result: SpeechResultState = attr.ib() + + +class Provider(ABC): + """Represent a single STT provider.""" + + hass: Optional[HomeAssistantType] = None + name: Optional[str] = None + + @property + @abstractmethod + def supported_languages(self) -> List[str]: + """Return a list of supported languages.""" + + @property + @abstractmethod + def supported_formats(self) -> List[AudioFormats]: + """Return a list of supported formats.""" + + @property + @abstractmethod + def supported_codecs(self) -> List[AudioCodecs]: + """Return a list of supported codecs.""" + + @property + @abstractmethod + def supported_bitrates(self) -> List[AudioBitrates]: + """Return a list of supported bitrates.""" + + @property + @abstractmethod + def supported_samplerates(self) -> List[AudioSamplerates]: + """Return a list of supported samplerates.""" + + @abstractmethod + async def async_process_audio_stream( + self, metadata: SpeechMetadata, stream: StreamReader + ) -> SpeechResult: + """Process an audio stream to STT service. + + Only streaming of content are allow! + """ + + @callback + def check_metadata(self, metadata: SpeechMetadata) -> bool: + """Check if given metadata supported by this provider.""" + if ( + metadata.language not in self.supported_languages + or metadata.format not in self.supported_formats + or metadata.codec not in self.supported_codecs + or metadata.bitrate not in self.supported_bitrates + or metadata.samplerate not in self.supported_samplerates + ): + return False + return True + + +class SpeechToTextView(HomeAssistantView): + """STT view to generate a text from audio stream.""" + + requires_auth = True + url = "/api/stt/{provider}" + name = "api:stt:provider" + + def __init__(self, providers: Dict[str, Provider]) -> None: + """Initialize a tts view.""" + self.providers = providers + + @staticmethod + def _metadata_from_header(request: web.Request) -> Optional[SpeechMetadata]: + """Extract metadata from header. + + X-Speech-Content: format=wav; codec=pcm; samplerate=16000; bitrate=16; language=de_de + """ + try: + data = request.headers[istr("X-Speech-Content")].split(";") + except KeyError: + _LOGGER.warning("Missing X-Speech-Content") + return None + + # Convert Header data + args = dict() + for value in data: + value = value.strip() + args[value.partition("=")[0]] = value.partition("=")[2] + + try: + return SpeechMetadata(**args) + except TypeError as err: + _LOGGER.warning("Wrong format of X-Speech-Content: %s", err) + return None + + async def post(self, request: web.Request, provider: str) -> web.Response: + """Convert Speech (audio) to text.""" + if provider not in self.providers: + raise HTTPNotFound() + stt_provider: Provider = self.providers[provider] + + # Get metadata + metadata = self._metadata_from_header(request) + if not metadata: + raise HTTPBadRequest() + + # Check format + if not stt_provider.check_metadata(metadata): + raise HTTPUnsupportedMediaType() + + # Process audio stream + result = await stt_provider.async_process_audio_stream( + metadata, request.content + ) + + # Return result + return self.json(attr.asdict(result)) + + async def get(self, request: web.Request, provider: str) -> web.Response: + """Return provider specific audio information.""" + if provider not in self.providers: + raise HTTPNotFound() + stt_provider: Provider = self.providers[provider] + + return self.json( + { + "languages": stt_provider.supported_languages, + "formats": stt_provider.supported_formats, + "codecs": stt_provider.supported_codecs, + "samplerates": stt_provider.supported_samplerates, + "bitrates": stt_provider.supported_bitrates, + } + ) diff --git a/homeassistant/components/stt/const.py b/homeassistant/components/stt/const.py new file mode 100644 index 00000000000000..dfdd91d4f9663c --- /dev/null +++ b/homeassistant/components/stt/const.py @@ -0,0 +1,48 @@ +"""STT constante.""" +from enum import Enum + +DOMAIN = "stt" + + +class AudioCodecs(str, Enum): + """Supported Audio codecs.""" + + PCM = "pcm" + OPUS = "opus" + + +class AudioFormats(str, Enum): + """Supported Audio formats.""" + + WAV = "wav" + OGG = "ogg" + + +class AudioBitrates(int, Enum): + """Supported Audio bitrates.""" + + BITRATE_8 = 8 + BITRATE_16 = 16 + BITRATE_24 = 24 + BITRATE_32 = 32 + + +class AudioSamplerates(int, Enum): + """Supported Audio samplerates.""" + + SAMPLERATE_8000 = 8000 + SAMPLERATE_11000 = 11000 + SAMPLERATE_16000 = 16000 + SAMPLERATE_18900 = 18900 + SAMPLERATE_22000 = 22000 + SAMPLERATE_32000 = 32000 + SAMPLERATE_37800 = 37800 + SAMPLERATE_44100 = 44100 + SAMPLERATE_48000 = 48000 + + +class SpeechResultState(str, Enum): + """Result state of speech.""" + + SUCCESS = "success" + ERROR = "error" diff --git a/homeassistant/components/stt/manifest.json b/homeassistant/components/stt/manifest.json new file mode 100644 index 00000000000000..03ea914dec16f4 --- /dev/null +++ b/homeassistant/components/stt/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "stt", + "name": "Stt", + "documentation": "https://www.home-assistant.io/integrations/stt", + "requirements": [], + "dependencies": ["http"], + "codeowners": ["@pvizeli"] +} diff --git a/homeassistant/components/stt/services.yaml b/homeassistant/components/stt/services.yaml new file mode 100644 index 00000000000000..7172061a30a4fa --- /dev/null +++ b/homeassistant/components/stt/services.yaml @@ -0,0 +1 @@ +# Describes the format for available STT services diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py new file mode 100644 index 00000000000000..39fed7658eeec7 --- /dev/null +++ b/tests/components/demo/test_stt.py @@ -0,0 +1,69 @@ +"""The tests for the demo stt component.""" +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components import stt + + +@pytest.fixture(autouse=True) +def setup_comp(hass): + """Set up demo component.""" + hass.loop.run_until_complete( + async_setup_component(hass, stt.DOMAIN, {"stt": {"platform": "demo"}}) + ) + + +async def test_demo_settings(hass_client): + """Test retrieve settings from demo provider.""" + client = await hass_client() + + response = await client.get("/api/stt/demo") + response_data = await response.json() + + assert response.status == 200 + assert response_data == { + "languages": ["en", "de"], + "bitrates": [16], + "samplerates": [16000, 44100], + "formats": ["wav"], + "codecs": ["pcm"], + } + + +async def test_demo_speech_no_metadata(hass_client): + """Test retrieve settings from demo provider.""" + client = await hass_client() + + response = await client.post("/api/stt/demo", data=b"Test") + assert response.status == 400 + + +async def test_demo_speech_wrong_metadata(hass_client): + """Test retrieve settings from demo provider.""" + client = await hass_client() + + response = await client.post( + "/api/stt/demo", + headers={ + "X-Speech-Content": "format=wav; codec=pcm; samplerate=8000; bitrate=16; language=de" + }, + data=b"Test", + ) + assert response.status == 415 + + +async def test_demo_speech(hass_client): + """Test retrieve settings from demo provider.""" + client = await hass_client() + + response = await client.post( + "/api/stt/demo", + headers={ + "X-Speech-Content": "format=wav; codec=pcm; samplerate=16000; bitrate=16; language=de" + }, + data=b"Test", + ) + response_data = await response.json() + + assert response.status == 200 + assert response_data == {"text": "Turn the Kitchen Lights on", "result": "success"} diff --git a/tests/components/stt/__init__.py b/tests/components/stt/__init__.py new file mode 100644 index 00000000000000..b55931a99cf0e8 --- /dev/null +++ b/tests/components/stt/__init__.py @@ -0,0 +1 @@ +"""Speech to text tests.""" diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py new file mode 100644 index 00000000000000..5627d7d3e530cc --- /dev/null +++ b/tests/components/stt/test_init.py @@ -0,0 +1,29 @@ +"""Test STT component setup.""" + +from homeassistant.setup import async_setup_component +from homeassistant.components import stt + + +async def test_setup_comp(hass): + """Set up demo component.""" + assert await async_setup_component(hass, stt.DOMAIN, {"stt": {}}) + + +async def test_demo_settings_not_exists(hass, hass_client): + """Test retrieve settings from demo provider.""" + assert await async_setup_component(hass, stt.DOMAIN, {"stt": {}}) + client = await hass_client() + + response = await client.get("/api/stt/beer") + + assert response.status == 404 + + +async def test_demo_speech_not_exists(hass, hass_client): + """Test retrieve settings from demo provider.""" + assert await async_setup_component(hass, stt.DOMAIN, {"stt": {}}) + client = await hass_client() + + response = await client.post("/api/stt/beer", data=b"test") + + assert response.status == 404 From 5b85456759142c5942c5cd95770acf96be0fcd52 Mon Sep 17 00:00:00 2001 From: Jess Date: Mon, 4 Nov 2019 13:32:33 +0000 Subject: [PATCH 1428/3953] Add switches (on/off zones) to geniushub (#28182) * New switch platform for geniushub * Update to new geniushub-client with support for on/off zones --- .../components/geniushub/__init__.py | 14 ++++- homeassistant/components/geniushub/climate.py | 4 +- .../components/geniushub/manifest.json | 2 +- homeassistant/components/geniushub/switch.py | 55 +++++++++++++++++++ .../components/geniushub/water_heater.py | 4 +- requirements_all.txt | 2 +- 6 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/geniushub/switch.py diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index b34c46a9f26fde..977656149c5a22 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -93,7 +93,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async_track_time_interval(hass, broker.async_update, SCAN_INTERVAL) - for platform in ["climate", "water_heater", "sensor", "binary_sensor"]: + for platform in ["climate", "water_heater", "sensor", "binary_sensor", "switch"]: hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) return True @@ -215,8 +215,6 @@ def __init__(self, broker, zone) -> None: self._zone = zone self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" - self._max_temp = self._min_temp = self._supported_features = None - @property def name(self) -> str: """Return the name of the climate device.""" @@ -228,6 +226,16 @@ def device_state_attributes(self) -> Dict[str, Any]: status = {k: v for k, v in self._zone.data.items() if k in GH_ZONE_ATTRS} return {"status": status} + +class GeniusHeatingZone(GeniusZone): + """Base for Genius Heating Zones.""" + + def __init__(self, broker, zone) -> None: + """Initialize the Zone.""" + super().__init__(broker, zone) + + self._max_temp = self._min_temp = self._supported_features = None + @property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 9a19edd9f8b0e8..2221b8706c855c 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -15,7 +15,7 @@ ) from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusZone +from . import DOMAIN, GeniusHeatingZone # GeniusHub Zones support: Off, Timer, Override/Boost, Footprint & Linked modes HA_HVAC_TO_GH = {HVAC_MODE_OFF: "off", HVAC_MODE_HEAT: "timer"} @@ -45,7 +45,7 @@ async def async_setup_platform( ) -class GeniusClimateZone(GeniusZone, ClimateDevice): +class GeniusClimateZone(GeniusHeatingZone, ClimateDevice): """Representation of a Genius Hub climate device.""" def __init__(self, broker, zone) -> None: diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index f9e8e6eb4f03df..6aa0d792c77303 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/integrations/geniushub", "requirements": [ - "geniushub-client==0.6.28" + "geniushub-client==0.6.30" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py new file mode 100644 index 00000000000000..79d14417dd4431 --- /dev/null +++ b/homeassistant/components/geniushub/switch.py @@ -0,0 +1,55 @@ +"""Support for Genius Hub switch/outlet devices.""" +from homeassistant.components.switch import SwitchDevice, DEVICE_CLASS_OUTLET +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from . import DOMAIN, GeniusZone + +ATTR_DURATION = "duration" + +GH_ON_OFF_ZONE = "on / off" + + +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +) -> None: + """Set up the Genius Hub switch entities.""" + if discovery_info is None: + return + + broker = hass.data[DOMAIN]["broker"] + + async_add_entities( + [ + GeniusSwitch(broker, z) + for z in broker.client.zone_objs + if z.data["type"] == GH_ON_OFF_ZONE + ] + ) + + +class GeniusSwitch(GeniusZone, SwitchDevice): + """Representation of a Genius Hub switch.""" + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_OUTLET + + @property + def is_on(self) -> bool: + """Return the current state of the on/off zone. + + The zone is considered 'on' if & only if it is override/on (e.g. timer/on is 'off'). + """ + return self._zone.data["mode"] == "override" and self._zone.data["setpoint"] + + async def async_turn_off(self, **kwargs) -> None: + """Send the zone to Timer mode. + + The zone is deemed 'off' in this mode, although the plugs may actually be on. + """ + await self._zone.set_mode("timer") + + async def async_turn_on(self, **kwargs) -> None: + """Set the zone to override/on ({'setpoint': true}) for x seconds.""" + await self._zone.set_override(1, kwargs.get(ATTR_DURATION, 3600)) diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 4141e9f8c04562..e7e3278eaf6c19 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -9,7 +9,7 @@ from homeassistant.const import STATE_OFF from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import DOMAIN, GeniusZone +from . import DOMAIN, GeniusHeatingZone STATE_AUTO = "auto" STATE_MANUAL = "manual" @@ -49,7 +49,7 @@ async def async_setup_platform( ) -class GeniusWaterHeater(GeniusZone, WaterHeaterDevice): +class GeniusWaterHeater(GeniusHeatingZone, WaterHeaterDevice): """Representation of a Genius Hub water_heater device.""" def __init__(self, broker, zone) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index b831ec7af894e5..64cc58eeb23e01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.6.28 +geniushub-client==0.6.30 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From f3ea44cd92cdc912cfaace92e07cd820fba8c38e Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Mon, 4 Nov 2019 10:17:32 -0500 Subject: [PATCH 1429/3953] Cleanup Device Registry on Z-Wave Node Removal (#28240) * Remove device from device registry on node removal * Make async_get_registry from entity registry more concise * Lower log level to debug --- homeassistant/components/zwave/__init__.py | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 841b283a98dd1f..97a904c5d994fc 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -13,7 +13,12 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL from homeassistant.helpers.entity_platform import EntityPlatform -from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.entity_registry import ( + async_get_registry as async_get_entity_registry, +) +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_device_registry, +) from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, @@ -376,7 +381,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DATA_DEVICES] = {} hass.data[DATA_ENTITY_VALUES] = [] - registry = await async_get_registry(hass) + registry = await async_get_entity_registry(hass) wsapi.async_load_websocket_api(hass) @@ -479,13 +484,14 @@ def _on_timeout(sec): def node_removed(node): node_id = node.node_id node_key = f"node-{node_id}" - _LOGGER.info("Node Removed: %s", hass.data[DATA_DEVICES][node_key]) for key in list(hass.data[DATA_DEVICES]): + if key is None: + continue if not key.startswith(f"{node_id}-"): continue entity = hass.data[DATA_DEVICES][key] - _LOGGER.info( + _LOGGER.debug( "Removing Entity - value: %s - entity_id: %s", key, entity.entity_id ) hass.add_job(entity.node_removed()) @@ -495,6 +501,16 @@ def node_removed(node): hass.add_job(entity.node_removed()) del hass.data[DATA_DEVICES][node_key] + hass.add_job(_remove_device(node)) + + async def _remove_device(node): + dev_reg = await async_get_device_registry(hass) + identifier, name = node_device_id_and_name(node) + device = dev_reg.async_get_device(identifiers={identifier}, connections=set()) + if device is not None: + _LOGGER.debug("Removing Device - %s - %s", device.id, name) + dev_reg.async_remove_device(device.id) + def network_ready(): """Handle the query of all awake nodes.""" _LOGGER.info( @@ -1208,7 +1224,7 @@ async def value_renamed(self, update_ids=False): self._name = _value_name(self.values.primary) if update_ids: # Update entity ID. - ent_reg = await async_get_registry(self.hass) + ent_reg = await async_get_entity_registry(self.hass) new_entity_id = ent_reg.async_generate_entity_id( self.platform.domain, self._name, From 6a7b5657acc7a201fa4817c9e49f015a327e2e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 4 Nov 2019 19:56:49 +0200 Subject: [PATCH 1430/3953] Support Huawei LTE SSDP discovery (#28214) * Support Huawei LTE SSDP discovery * Avoid KeyError on simultaneous user initiated flow Co-Authored-By: Paulus Schoutsen * Format code * Add already configured check * Initialize context in test flows * Move deviceType match to manifest * Update generated.ssdp * Add SSDP config flow test case * Remove stale debug print from tests --- .../huawei_lte/.translations/en.json | 4 +- .../components/huawei_lte/config_flow.py | 50 +++++++++++-- .../components/huawei_lte/manifest.json | 6 ++ .../components/huawei_lte/strings.json | 4 +- homeassistant/generated/ssdp.py | 6 ++ .../components/huawei_lte/test_config_flow.py | 73 ++++++++++++++----- 6 files changed, 116 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 8681e3355a46c5..0952b05a5cfb43 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "This device is already configured" + "already_configured": "This device has already been configured", + "already_in_progress": "This device is already being configured", + "not_huawei_lte": "Not a Huawei LTE device" }, "error": { "connection_failed": "Connection failed", diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 992dc33a697711..1bc3753bdd7df9 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -19,6 +19,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.ssdp import ATTR_HOST, ATTR_NAME, ATTR_PRESENTATIONURL from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME from homeassistant.core import callback from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME @@ -52,7 +53,14 @@ async def _async_show_user_form(self, user_input=None, errors=None): ( ( vol.Required( - CONF_URL, default=user_input.get(CONF_URL, "") + CONF_URL, + default=user_input.get( + CONF_URL, + # https://github.com/PyCQA/pylint/issues/3167 + self.context.get( # pylint: disable=no-member + CONF_URL, "" + ), + ), ), str, ), @@ -78,6 +86,14 @@ async def async_step_import(self, user_input=None): """Handle import initiated config flow.""" return await self.async_step_user(user_input) + def _already_configured(self, user_input): + """See if we already have a router matching user input configured.""" + existing_urls = { + url_normalize(entry.data[CONF_URL], default_scheme="http") + for entry in self._async_current_entries() + } + return user_input[CONF_URL] in existing_urls + async def async_step_user(self, user_input=None): """Handle user initiated config flow.""" if user_input is None: @@ -95,12 +111,7 @@ async def async_step_user(self, user_input=None): user_input=user_input, errors=errors ) - # See if we already have a router configured with this URL - existing_urls = { # existing entries - url_normalize(entry.data[CONF_URL], default_scheme="http") - for entry in self._async_current_entries() - } - if user_input[CONF_URL] in existing_urls: + if self._already_configured(user_input): return self.async_abort(reason="already_configured") conn = None @@ -194,6 +205,31 @@ def get_router_title(conn: Connection) -> str: return self.async_create_entry(title=title, data=user_input) + async def async_step_ssdp(self, discovery_info): + """Handle SSDP initiated config flow.""" + # Attempt to distinguish from other non-LTE Huawei router devices, at least + # some ones we are interested in have "Mobile Wi-Fi" friendlyName. + if "mobile" not in discovery_info.get(ATTR_NAME, "").lower(): + return self.async_abort(reason="not_huawei_lte") + + # https://github.com/PyCQA/pylint/issues/3167 + url = self.context[CONF_URL] = url_normalize( # pylint: disable=no-member + discovery_info.get( + ATTR_PRESENTATIONURL, f"http://{discovery_info[ATTR_HOST]}/" + ) + ) + + if any( + url == flow["context"].get(CONF_URL) for flow in self._async_in_progress() + ): + return self.async_abort(reason="already_in_progress") + + user_input = {CONF_URL: url} + if self._already_configured(user_input): + return self.async_abort(reason="already_configured") + + return await self._async_show_user_form(user_input) + class OptionsFlowHandler(config_entries.OptionsFlow): """Huawei LTE options flow.""" diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index b3c4442caa9a65..4ea54188688de7 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -9,6 +9,12 @@ "stringcase==1.2.0", "url-normalize==1.4.1" ], + "ssdp": [ + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "Huawei" + } + ], "dependencies": [], "codeowners": [ "@scop" diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 2e76cf1b343963..17684253671bfc 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "This device is already configured" + "already_configured": "This device has already been configured", + "already_in_progress": "This device is already being configured", + "not_huawei_lte": "Not a Huawei LTE device" }, "error": { "connection_failed": "Connection failed", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 472ad6683ed516..adf3a345bbe55f 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -16,6 +16,12 @@ "st": "urn:schemas-denon-com:device:ACT-Denon:1" } ], + "huawei_lte": [ + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "Huawei" + } + ], "hue": [ { "manufacturer": "Royal Philips Electronics" diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index aafa6abd57fb6e..a9f5034fcfe051 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -10,6 +10,21 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_URL from homeassistant.components.huawei_lte.const import DOMAIN from homeassistant.components.huawei_lte.config_flow import ConfigFlowHandler +from homeassistant.components.ssdp import ( + ATTR_HOST, + ATTR_MANUFACTURER, + ATTR_MANUFACTURERURL, + ATTR_MODEL_NAME, + ATTR_MODEL_NUMBER, + ATTR_NAME, + ATTR_PORT, + ATTR_PRESENTATIONURL, + ATTR_SERIAL, + ATTR_ST, + ATTR_UDN, + ATTR_UPNP_DEVICE_TYPE, +) + from tests.common import MockConfigEntry @@ -20,21 +35,26 @@ } -async def test_show_set_form(hass): - """Test that the setup form is served.""" +@pytest.fixture +def flow(hass): + """Get flow to test.""" flow = ConfigFlowHandler() flow.hass = hass + flow.context = {} + return flow + + +async def test_show_set_form(flow): + """Test that the setup form is served.""" 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_urlize_plain_host(hass, requests_mock): +async def test_urlize_plain_host(flow, requests_mock): """Test that plain host or IP gets converted to a URL.""" requests_mock.request(ANY, ANY, exc=ConnectionError()) - flow = ConfigFlowHandler() - flow.hass = hass host = "192.168.100.1" user_input = {**FIXTURE_USER_INPUT, CONF_URL: host} result = await flow.async_step_user(user_input=user_input) @@ -44,14 +64,12 @@ async def test_urlize_plain_host(hass, requests_mock): assert user_input[CONF_URL] == f"http://{host}/" -async def test_already_configured(hass): +async def test_already_configured(flow): """Test we reject already configured devices.""" MockConfigEntry( domain=DOMAIN, data=FIXTURE_USER_INPUT, title="Already configured" - ).add_to_hass(hass) + ).add_to_hass(flow.hass) - flow = ConfigFlowHandler() - flow.hass = hass # Tweak URL a bit to check that doesn't fail duplicate detection result = await flow.async_step_user( user_input={ @@ -64,12 +82,10 @@ async def test_already_configured(hass): assert result["reason"] == "already_configured" -async def test_connection_error(hass, requests_mock): +async def test_connection_error(flow, requests_mock): """Test we show user form on connection error.""" requests_mock.request(ANY, ANY, exc=ConnectionError()) - flow = ConfigFlowHandler() - flow.hass = hass result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -107,15 +123,13 @@ def login_requests_mock(requests_mock): (ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}), ), ) -async def test_login_error(hass, login_requests_mock, code, errors): +async def test_login_error(flow, login_requests_mock, code, errors): """Test we show user form with appropriate error on response failure.""" login_requests_mock.request( ANY, f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", text=f"{code}", ) - flow = ConfigFlowHandler() - flow.hass = hass result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -123,18 +137,41 @@ async def test_login_error(hass, login_requests_mock, code, errors): assert result["errors"] == errors -async def test_success(hass, login_requests_mock): +async def test_success(flow, login_requests_mock): """Test successful flow provides entry creation data.""" login_requests_mock.request( ANY, f"{FIXTURE_USER_INPUT[CONF_URL]}api/user/login", text=f"OK", ) - flow = ConfigFlowHandler() - flow.hass = hass result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + + +async def test_ssdp(flow): + """Test SSDP discovery initiates config properly.""" + url = "http://192.168.100.1/" + result = await flow.async_step_ssdp( + discovery_info={ + ATTR_ST: "upnp:rootdevice", + ATTR_PORT: 60957, + ATTR_HOST: "192.168.100.1", + ATTR_MANUFACTURER: "Huawei", + ATTR_MANUFACTURERURL: "http://www.huawei.com/", + ATTR_MODEL_NAME: "Huawei router", + ATTR_MODEL_NUMBER: "12345678", + ATTR_NAME: "Mobile Wi-Fi", + ATTR_PRESENTATIONURL: url, + ATTR_SERIAL: "00000000", + ATTR_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", + ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + } + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert flow.context[CONF_URL] == url From aaad8eac0a49ef033cc4a4aa8ce3c039e47e0cf3 Mon Sep 17 00:00:00 2001 From: chriscla Date: Mon, 4 Nov 2019 10:39:03 -0800 Subject: [PATCH 1431/3953] Fire an event when nzbget download completes (#27763) * Fire an event when download completes * Rename event and use a set * Use a set comprehension * Renaming method --- homeassistant/components/nzbget/__init__.py | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index ae3ce7c594479f..40a30d31743ed8 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -81,6 +81,7 @@ def setup(hass, config): _LOGGER.debug("Successfully validated NZBGet API connection") nzbget_data = hass.data[DATA_NZBGET] = NZBGetData(hass, nzbget_api) + nzbget_data.init_download_list() nzbget_data.update() def service_handler(service): @@ -127,18 +128,50 @@ def __init__(self, hass, api): self.status = None self.available = True self._api = api + self.downloads = None + self.completed_downloads = set() def update(self): """Get the latest data from NZBGet instance.""" try: self.status = self._api.status() + self.downloads = self._api.history() + + self.check_completed_downloads() + self.available = True dispatcher_send(self.hass, DATA_UPDATED) except pynzbgetapi.NZBGetAPIException as err: self.available = False _LOGGER.error("Unable to refresh NZBGet data: %s", err) + def init_download_list(self): + """Initialize download list.""" + self.downloads = self._api.history() + self.completed_downloads = { + (x["Name"], x["Category"], x["Status"]) for x in self.downloads + } + + def check_completed_downloads(self): + """Check history for newly completed downloads.""" + + actual_completed_downloads = { + (x["Name"], x["Category"], x["Status"]) for x in self.downloads + } + + tmp_completed_downloads = list( + actual_completed_downloads.difference(self.completed_downloads) + ) + + for download in tmp_completed_downloads: + self.hass.bus.fire( + "nzbget_download_complete", + {"name": download[0], "category": download[1], "status": download[2]}, + ) + + self.completed_downloads = actual_completed_downloads + def pause_download(self): """Pause download queue.""" From 06c26f3ffc75d0de1f746caf25971efa7b80730e Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 4 Nov 2019 20:12:43 +0100 Subject: [PATCH 1432/3953] Add heating_type for ViCare integration (#27296) * Add heating_type for ViCare * Add additional gas heating properties * Update requirements_all * Add hvac action * Remove measurements * Remove unused property --- homeassistant/components/vicare/__init__.py | 29 +++++++++++++- homeassistant/components/vicare/climate.py | 38 +++++++++++++++++-- homeassistant/components/vicare/manifest.json | 2 +- .../components/vicare/water_heater.py | 14 ++++++- requirements_all.txt | 2 +- 5 files changed, 77 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 9fec04f23283f3..e091ff99970a50 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -1,8 +1,12 @@ """The ViCare integration.""" +import enum import logging import voluptuous as vol + from PyViCare.PyViCareDevice import Device +from PyViCare.PyViCareGazBoiler import GazBoiler +from PyViCare.PyViCareHeatPump import HeatPump import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME @@ -15,8 +19,20 @@ DOMAIN = "vicare" VICARE_API = "api" VICARE_NAME = "name" +VICARE_HEATING_TYPE = "heating_type" CONF_CIRCUIT = "circuit" +CONF_HEATING_TYPE = "heating_type" +DEFAULT_HEATING_TYPE = "generic" + + +class HeatingType(enum.Enum): + """Possible options for heating type.""" + + generic = "generic" + gas = "gas" + heatpump = "heatpump" + CONFIG_SCHEMA = vol.Schema( { @@ -26,6 +42,9 @@ vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_CIRCUIT): int, vol.Optional(CONF_NAME, default="ViCare"): cv.string, + vol.Optional(CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE): cv.enum( + HeatingType + ), } ) }, @@ -40,8 +59,15 @@ def setup(hass, config): if conf.get(CONF_CIRCUIT) is not None: params["circuit"] = conf[CONF_CIRCUIT] + heating_type = conf[CONF_HEATING_TYPE] + try: - vicare_api = Device(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) + if heating_type == HeatingType.gas: + vicare_api = GazBoiler(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) + elif heating_type == HeatingType.heatpump: + vicare_api = HeatPump(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) + else: + vicare_api = Device(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) except AttributeError: _LOGGER.error( "Failed to create PyViCare API client. Please check your credentials." @@ -51,6 +77,7 @@ def setup(hass, config): hass.data[DOMAIN] = {} hass.data[DOMAIN][VICARE_API] = vicare_api hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME] + hass.data[DOMAIN][VICARE_HEATING_TYPE] = heating_type for platform in VICARE_PLATFORMS: discovery.load_platform(hass, platform, DOMAIN, {}, config) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 0dcb83f758afc8..fe162c0c837316 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -10,12 +10,16 @@ HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_AUTO, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, ) from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE from . import DOMAIN as VICARE_DOMAIN from . import VICARE_API from . import VICARE_NAME +from . import VICARE_HEATING_TYPE +from . import HeatingType _LOGGER = logging.getLogger(__name__) @@ -77,15 +81,22 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] + heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] add_entities( - [ViCareClimate(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", vicare_api)] + [ + ViCareClimate( + f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", + vicare_api, + heating_type, + ) + ] ) class ViCareClimate(ClimateDevice): """Representation of the ViCare heating climate device.""" - def __init__(self, name, api): + def __init__(self, name, api, heating_type): """Initialize the climate device.""" self._name = name self._state = None @@ -95,6 +106,8 @@ def __init__(self, name, api): self._current_mode = None self._current_temperature = None self._current_program = None + self._heating_type = heating_type + self._current_action = None def update(self): """Let HA know there has been an update from the ViCare API.""" @@ -117,7 +130,7 @@ def update(self): self._current_mode = self._api.getActiveMode() - # Update the device attributes + # Update the generic device attributes self._attributes = {} self._attributes["room_temperature"] = _room_temperature self._attributes["supply_temperature"] = _supply_temperature @@ -136,6 +149,18 @@ def update(self): "circulationpump_active" ] = self._api.getCirculationPumpActive() + # Update the specific device attributes + if self._heating_type == HeatingType.gas: + self._current_action = self._api.getBurnerActive() + + self._attributes["burner_modulation"] = self._api.getBurnerModulation() + self._attributes["boiler_temperature"] = self._api.getBoilerTemperature() + + elif self._heating_type == HeatingType.heatpump: + self._current_action = self._api.getCompressorActive() + + self._attributes["return_temperature"] = self._api.getReturnTemperature() + @property def supported_features(self): """Return the list of supported features.""" @@ -183,6 +208,13 @@ def hvac_modes(self): """Return the list of available hvac modes.""" return list(HA_TO_VICARE_HVAC_HEATING) + @property + def hvac_action(self): + """Return the current hvac action.""" + if self._current_action: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + @property def min_temp(self): """Return the minimum temperature.""" diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 9f7c703fe4b514..03bb16fa9bbcf8 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/vicare", "dependencies": [], "codeowners": ["@oischinger"], - "requirements": ["PyViCare==0.1.1"] + "requirements": ["PyViCare==0.1.2"] } diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 71c0f6c2aefe7a..7c4968ad0a4138 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -10,6 +10,7 @@ from . import DOMAIN as VICARE_DOMAIN from . import VICARE_API from . import VICARE_NAME +from . import VICARE_HEATING_TYPE _LOGGER = logging.getLogger(__name__) @@ -46,22 +47,31 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] + heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] add_entities( - [ViCareWater(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Water", vicare_api)] + [ + ViCareWater( + f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Water", + vicare_api, + heating_type, + ) + ] ) class ViCareWater(WaterHeaterDevice): """Representation of the ViCare domestic hot water device.""" - def __init__(self, name, api): + def __init__(self, name, api, heating_type): """Initialize the DHW water_heater device.""" self._name = name self._state = None self._api = api + self._attributes = {} self._target_temperature = None self._current_temperature = None self._current_mode = None + self._heating_type = heating_type def update(self): """Let HA know there has been an update from the ViCare API.""" diff --git a/requirements_all.txt b/requirements_all.txt index 64cc58eeb23e01..8635b19ae06e57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -78,7 +78,7 @@ PySocks==1.7.1 PyTransportNSW==0.1.1 # homeassistant.components.vicare -PyViCare==0.1.1 +PyViCare==0.1.2 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.12.4 From bbe0cf3a0c1c7b474be8f45b128a741d5d532985 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Mon, 4 Nov 2019 20:52:55 +0100 Subject: [PATCH 1433/3953] Bump version for asuswrt to 1.1.22 (#28322) * Bumping version * Fix requirements * Fix requirements --- homeassistant/components/asuswrt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 3fcdcf42ab1b3e..3d8cebce096db0 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -3,7 +3,7 @@ "name": "Asuswrt", "documentation": "https://www.home-assistant.io/integrations/asuswrt", "requirements": [ - "aioasuswrt==1.1.21" + "aioasuswrt==1.1.22" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8635b19ae06e57..7ceb7351eb726f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -130,7 +130,7 @@ aio_geojson_geonetnz_quakes==0.10 aioambient==0.3.2 # homeassistant.components.asuswrt -aioasuswrt==1.1.21 +aioasuswrt==1.1.22 # homeassistant.components.automatic aioautomatic==0.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6499ac046816e9..9395c0cefadbe2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -62,7 +62,7 @@ aio_geojson_geonetnz_quakes==0.10 aioambient==0.3.2 # homeassistant.components.asuswrt -aioasuswrt==1.1.21 +aioasuswrt==1.1.22 # homeassistant.components.automatic aioautomatic==0.6.5 From e4196892294d3cfc0c4d9dc4d22dbbb80348093d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 4 Nov 2019 21:38:18 +0100 Subject: [PATCH 1434/3953] Add config endpoint for scene (#28429) * Add config endpoint for scene * Add scenes to default config * Fix test and add context to websocket service call * Update scene.py * Add tests --- homeassistant/components/config/__init__.py | 1 + homeassistant/components/config/scene.py | 65 ++++++++ .../components/homeassistant/scene.py | 15 +- .../components/websocket_api/commands.py | 4 +- homeassistant/config.py | 6 + tests/components/config/test_scene.py | 144 ++++++++++++++++++ tests/test_config.py | 4 + 7 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/config/scene.py create mode 100644 tests/components/config/test_scene.py diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 569d1de6a02ce8..5a66c1fc5d446d 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -25,6 +25,7 @@ "entity_registry", "group", "script", + "scene", ) ON_DEMAND = ("zwave",) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py new file mode 100644 index 00000000000000..6e77dae08260f8 --- /dev/null +++ b/homeassistant/components/config/scene.py @@ -0,0 +1,65 @@ +"""Provide configuration end points for Scenes.""" +from collections import OrderedDict +import uuid + +from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA +from homeassistant.const import CONF_ID, SERVICE_RELOAD +from homeassistant.config import SCENE_CONFIG_PATH +import homeassistant.helpers.config_validation as cv + +from . import EditIdBasedConfigView + + +async def async_setup(hass): + """Set up the Scene config API.""" + + async def hook(hass): + """post_write_hook for Config View that reloads scenes.""" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + + hass.http.register_view( + EditSceneConfigView( + DOMAIN, + "config", + SCENE_CONFIG_PATH, + cv.string, + PLATFORM_SCHEMA, + post_write_hook=hook, + ) + ) + return True + + +class EditSceneConfigView(EditIdBasedConfigView): + """Edit scene config.""" + + def _write_value(self, hass, data, config_key, new_value): + """Set value.""" + index = None + for index, cur_value in enumerate(data): + # When people copy paste their scenes to the config file, + # they sometimes forget to add IDs. Fix it here. + if CONF_ID not in cur_value: + cur_value[CONF_ID] = uuid.uuid4().hex + + elif cur_value[CONF_ID] == config_key: + break + else: + cur_value = dict() + cur_value[CONF_ID] = config_key + index = len(data) + data.append(cur_value) + + # Iterate through some keys that we want to have ordered in the output + updated_value = OrderedDict() + for key in ("id", "name", "entities"): + if key in cur_value: + updated_value[key] = cur_value[key] + if key in new_value: + updated_value[key] = new_value[key] + + # We cover all current fields above, but just in case we start + # supporting more fields in the future. + updated_value.update(cur_value) + updated_value.update(new_value) + data[index] = updated_value diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 084c950bf1782f..f011dae150f937 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -8,6 +8,7 @@ ATTR_ENTITY_ID, ATTR_STATE, CONF_ENTITIES, + CONF_ID, CONF_NAME, CONF_PLATFORM, STATE_OFF, @@ -160,7 +161,11 @@ def _process_scenes_config(hass, async_add_entities, config): return async_add_entities( - HomeAssistantScene(hass, SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES])) + HomeAssistantScene( + hass, + SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES]), + scene.get(CONF_ID), + ) for scene in scene_config ) @@ -168,8 +173,9 @@ def _process_scenes_config(hass, async_add_entities, config): class HomeAssistantScene(Scene): """A scene is a group of entities and the states we want them to be.""" - def __init__(self, hass, scene_config): + def __init__(self, hass, scene_config, scene_id=None): """Initialize the scene.""" + self._id = scene_id self.hass = hass self.scene_config = scene_config @@ -181,7 +187,10 @@ def name(self): @property def device_state_attributes(self): """Return the scene state attributes.""" - return {ATTR_ENTITY_ID: list(self.scene_config.states)} + attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)} + if self._id is not None: + attributes[CONF_ID] = self._id + return attributes async def async_activate(self): """Activate scene. Try to get entities into requested state.""" diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 9d46238b24104f..f30ee816914bdf 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -132,7 +132,9 @@ async def handle_call_service(hass, connection, msg): blocking, connection.context(msg), ) - connection.send_message(messages.result_message(msg["id"])) + connection.send_message( + messages.result_message(msg["id"], {"context": connection.context(msg)}) + ) except ServiceNotFound as err: if err.domain == msg["domain"] and err.service == msg["service"]: connection.send_message( diff --git a/homeassistant/config.py b/homeassistant/config.py index 9f49889791e93f..864ced6a16aea0 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -69,6 +69,7 @@ GROUP_CONFIG_PATH = "groups.yaml" AUTOMATION_CONFIG_PATH = "automations.yaml" SCRIPT_CONFIG_PATH = "scripts.yaml" +SCENE_CONFIG_PATH = "scenes.yaml" DEFAULT_CONFIG = f""" # Configure a default setup of Home Assistant (frontend, api, etc) @@ -85,6 +86,7 @@ group: !include {GROUP_CONFIG_PATH} automation: !include {AUTOMATION_CONFIG_PATH} script: !include {SCRIPT_CONFIG_PATH} +scene: !include {SCENE_CONFIG_PATH} """ DEFAULT_SECRETS = """ # Use this file to store secrets like usernames and passwords. @@ -261,6 +263,7 @@ def _write_default_config(config_dir: str) -> Optional[str]: group_yaml_path = os.path.join(config_dir, GROUP_CONFIG_PATH) automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH) script_yaml_path = os.path.join(config_dir, SCRIPT_CONFIG_PATH) + scene_yaml_path = os.path.join(config_dir, SCENE_CONFIG_PATH) # Writing files with YAML does not create the most human readable results # So we're hard coding a YAML template. @@ -283,6 +286,9 @@ def _write_default_config(config_dir: str) -> Optional[str]: with open(script_yaml_path, "wt"): pass + with open(scene_yaml_path, "wt"): + pass + return config_path except OSError: diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py new file mode 100644 index 00000000000000..b40c895b620116 --- /dev/null +++ b/tests/components/config/test_scene.py @@ -0,0 +1,144 @@ +"""Test Automation config panel.""" +import json +from unittest.mock import patch + +from homeassistant.bootstrap import async_setup_component +from homeassistant.components import config +from homeassistant.util.yaml import dump + + +async def test_update_scene(hass, hass_client): + """Test updating a scene.""" + with patch.object(config, "SECTIONS", ["scene"]): + await async_setup_component(hass, "config", {}) + + client = await hass_client() + + orig_data = [{"id": "light_on"}, {"id": "light_off"}] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + data = dump(data) + written.append(data) + + with patch("homeassistant.components.config._read", mock_read), patch( + "homeassistant.components.config._write", mock_write + ): + resp = await client.post( + "/api/config/scene/config/light_off", + data=json.dumps( + { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + ), + ) + + assert resp.status == 200 + result = await resp.json() + assert result == {"result": "ok"} + + assert len(written) == 1 + written_yaml = written[0] + assert ( + written_yaml + == """- id: light_on +- id: light_off + name: Lights off + entities: + light.bedroom: + state: 'off' +""" + ) + + +async def test_bad_formatted_scene(hass, hass_client): + """Test that we handle scene without ID.""" + with patch.object(config, "SECTIONS", ["scene"]): + await async_setup_component(hass, "config", {}) + + client = await hass_client() + + orig_data = [ + { + # No ID + "entities": {"light.bedroom": "on"} + }, + {"id": "light_off"}, + ] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch("homeassistant.components.config._read", mock_read), patch( + "homeassistant.components.config._write", mock_write + ): + resp = await client.post( + "/api/config/scene/config/light_off", + data=json.dumps( + { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + ), + ) + + assert resp.status == 200 + result = await resp.json() + assert result == {"result": "ok"} + + # Verify ID added to orig_data + assert "id" in orig_data[0] + + assert orig_data[1] == { + "id": "light_off", + "name": "Lights off", + "entities": {"light.bedroom": {"state": "off"}}, + } + + +async def test_delete_scene(hass, hass_client): + """Test deleting a scene.""" + with patch.object(config, "SECTIONS", ["scene"]): + await async_setup_component(hass, "config", {}) + + client = await hass_client() + + orig_data = [{"id": "light_on"}, {"id": "light_off"}] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch("homeassistant.components.config._read", mock_read), patch( + "homeassistant.components.config._write", mock_write + ): + resp = await client.delete("/api/config/scene/config/light_on") + + assert resp.status == 200 + result = await resp.json() + assert result == {"result": "ok"} + + assert len(written) == 1 + assert written[0][0]["id"] == "light_off" diff --git a/tests/test_config.py b/tests/test_config.py index dab51f59176ca2..1c872369096c60 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -44,6 +44,7 @@ GROUP_PATH = os.path.join(CONFIG_DIR, config_util.GROUP_CONFIG_PATH) AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, config_util.AUTOMATION_CONFIG_PATH) SCRIPTS_PATH = os.path.join(CONFIG_DIR, config_util.SCRIPT_CONFIG_PATH) +SCENES_PATH = os.path.join(CONFIG_DIR, config_util.SCENE_CONFIG_PATH) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE @@ -75,6 +76,9 @@ def teardown(): if os.path.isfile(SCRIPTS_PATH): os.remove(SCRIPTS_PATH) + if os.path.isfile(SCENES_PATH): + os.remove(SCENES_PATH) + async def test_create_default_config(hass): """Test creation of default config.""" From fe749fc0f8eaba9ccea5b389e1b91be2d684d363 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Mon, 4 Nov 2019 12:49:11 -0800 Subject: [PATCH 1435/3953] Fix sensor device in the Abode component (#28516) * Fix for occupancy sensor unique_id * Add check for sensor attributes before adding entity * Fixes temperature key issue * Clean up code with better use of keys * Code clean up --- homeassistant/components/abode/sensor.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index e25921f295f3dc..6ee0cf59cbf723 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -16,9 +16,9 @@ # Sensor types: Name, icon SENSOR_TYPES = { - "temp": ["Temperature", DEVICE_CLASS_TEMPERATURE], - "humidity": ["Humidity", DEVICE_CLASS_HUMIDITY], - "lux": ["Lux", DEVICE_CLASS_ILLUMINANCE], + CONST.TEMP_STATUS_KEY: ["Temperature", DEVICE_CLASS_TEMPERATURE], + CONST.HUMI_STATUS_KEY: ["Humidity", DEVICE_CLASS_HUMIDITY], + CONST.LUX_STATUS_KEY: ["Lux", DEVICE_CLASS_ILLUMINANCE], } @@ -29,15 +29,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for an Abode device.""" - data = hass.data[DOMAIN] + entities = [] - devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): for sensor_type in SENSOR_TYPES: - devices.append(AbodeSensor(data, device, sensor_type)) + if sensor_type not in device.get_value(CONST.STATUSES_KEY): + continue + entities.append(AbodeSensor(data, device, sensor_type)) - async_add_entities(devices) + async_add_entities(entities) class AbodeSensor(AbodeDevice): @@ -62,6 +63,11 @@ def device_class(self): """Return the device class.""" return self._device_class + @property + def unique_id(self): + """Return a unique ID to use for this device.""" + return f"{self._device.device_uuid}-{self._sensor_type}" + @property def state(self): """Return the state of the sensor.""" From 6e58a0c9964e5a1caeb56199d09ba0364532c75b Mon Sep 17 00:00:00 2001 From: Thom Troy Date: Mon, 4 Nov 2019 20:49:53 +0000 Subject: [PATCH 1436/3953] Update ephember library version (#28507) * update ephember library version * update requirements_all.txt for new pyephember version * update imports to top of module --- homeassistant/components/ephember/climate.py | 63 ++++++++++--------- .../components/ephember/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 0e35b8bbee72eb..c189b2d62b821e 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -1,23 +1,35 @@ """Support for the EPH Controls Ember themostats.""" -import logging from datetime import timedelta +import logging + +from pyephember.pyephember import ( + EphEmber, + ZoneMode, + zone_current_temperature, + zone_is_active, + zone_is_boost_active, + zone_is_hot_water, + zone_mode, + zone_name, + zone_target_temperature, +) import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE, - HVAC_MODE_OFF, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, ) from homeassistant.const import ( ATTR_TEMPERATURE, - TEMP_CELSIUS, - CONF_USERNAME, CONF_PASSWORD, + CONF_USERNAME, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv @@ -43,8 +55,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ephember thermostat.""" - from pyephember.pyephember import EphEmber - username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -61,14 +71,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class EphEmberThermostat(ClimateDevice): - """Representation of a HeatmiserV3 thermostat.""" + """Representation of a EphEmber thermostat.""" def __init__(self, ember, zone): """Initialize the thermostat.""" self._ember = ember - self._zone_name = zone["name"] + self._zone_name = zone_name(zone) self._zone = zone - self._hot_water = zone["isHotWater"] + self._hot_water = zone_is_hot_water(zone) @property def supported_features(self): @@ -91,12 +101,12 @@ def temperature_unit(self): @property def current_temperature(self): """Return the current temperature.""" - return self._zone["currentTemperature"] + return zone_current_temperature(self._zone) @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._zone["targetTemperature"] + return zone_target_temperature(self._zone) @property def target_temperature_step(self): @@ -104,12 +114,12 @@ def target_temperature_step(self): if self._hot_water: return None - return 1 + return 0.5 @property def hvac_action(self): """Return current HVAC action.""" - if self._zone["isCurrentlyActive"]: + if zone_is_active(self._zone): return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE @@ -117,9 +127,7 @@ def hvac_action(self): @property def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" - from pyephember.pyephember import ZoneMode - - mode = ZoneMode(self._zone["mode"]) + mode = zone_mode(self._zone) return self.map_mode_eph_hass(mode) @property @@ -138,12 +146,13 @@ def set_hvac_mode(self, hvac_mode): @property def is_aux_heat(self): """Return true if aux heater.""" - return self._zone["isBoostActive"] + + return zone_is_boost_active(self._zone) def turn_aux_heat_on(self): """Turn auxiliary heater on.""" self._ember.activate_boost_by_name( - self._zone_name, self._zone["targetTemperature"] + self._zone_name, zone_target_temperature(self._zone) ) def turn_aux_heat_off(self): @@ -165,24 +174,24 @@ def set_temperature(self, **kwargs): if temperature > self.max_temp or temperature < self.min_temp: return - self._ember.set_target_temperture_by_name(self._zone_name, int(temperature)) + self._ember.set_target_temperture_by_name(self._zone_name, temperature) @property def min_temp(self): """Return the minimum temperature.""" # Hot water temp doesn't support being changed if self._hot_water: - return self._zone["targetTemperature"] + return zone_target_temperature(self._zone) - return 5 + return 5.0 @property def max_temp(self): """Return the maximum temperature.""" if self._hot_water: - return self._zone["targetTemperature"] + return zone_target_temperature(self._zone) - return 35 + return 35.0 def update(self): """Get the latest data.""" @@ -191,8 +200,6 @@ def update(self): @staticmethod def map_mode_hass_eph(operation_mode): """Map from home assistant mode to eph mode.""" - from pyephember.pyephember import ZoneMode - return getattr(ZoneMode, HA_STATE_TO_EPH.get(operation_mode), None) @staticmethod diff --git a/homeassistant/components/ephember/manifest.json b/homeassistant/components/ephember/manifest.json index 7509e627621396..e05d21c0c020e0 100644 --- a/homeassistant/components/ephember/manifest.json +++ b/homeassistant/components/ephember/manifest.json @@ -3,7 +3,7 @@ "name": "Ephember", "documentation": "https://www.home-assistant.io/integrations/ephember", "requirements": [ - "pyephember==0.2.0" + "pyephember==0.3.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7ceb7351eb726f..04a858ce3b979e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1189,7 +1189,7 @@ pyemby==1.6 pyenvisalink==4.0 # homeassistant.components.ephember -pyephember==0.2.0 +pyephember==0.3.1 # homeassistant.components.everlights pyeverlights==0.1.0 From f5fb9fc580e6419907513edfd6b687cbfc090845 Mon Sep 17 00:00:00 2001 From: Marius Flage Date: Mon, 4 Nov 2019 21:54:36 +0100 Subject: [PATCH 1437/3953] Checking state before actually sending a new state change. Some projectors return ERR if you try to turn off a projector that's already off. (#28529) --- homeassistant/components/pjlink/media_player.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 6474165a6cd637..ea35fe7fb75a7d 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -158,13 +158,15 @@ def supported_features(self): def turn_off(self): """Turn projector off.""" - with self.projector() as projector: - projector.set_power("off") + if self._pwstate == STATE_ON: + with self.projector() as projector: + projector.set_power("off") def turn_on(self): """Turn projector on.""" - with self.projector() as projector: - projector.set_power("on") + if self._pwstate == STATE_OFF: + with self.projector() as projector: + projector.set_power("on") def mute_volume(self, mute): """Mute (true) of unmute (false) media player.""" From 83a9f4ddb83ed5f428f124da1d2e1c4ebbf3793b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Nov 2019 14:01:10 -0800 Subject: [PATCH 1438/3953] rate is a separate word (#28535) --- homeassistant/components/demo/stt.py | 16 ++++++++-------- homeassistant/components/stt/__init__.py | 24 ++++++++++++------------ homeassistant/components/stt/const.py | 8 ++++---- tests/components/demo/test_stt.py | 8 ++++---- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py index 90a4cc032964dc..fdd89dc4c9c618 100644 --- a/homeassistant/components/demo/stt.py +++ b/homeassistant/components/demo/stt.py @@ -5,9 +5,9 @@ from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult from homeassistant.components.stt.const import ( - AudioBitrates, + AudioBitRates, AudioFormats, - AudioSamplerates, + AudioSampleRates, AudioCodecs, SpeechResultState, ) @@ -39,14 +39,14 @@ def supported_codecs(self) -> List[AudioCodecs]: return [AudioCodecs.PCM] @property - def supported_bitrates(self) -> List[AudioBitrates]: - """Return a list of supported bitrates.""" - return [AudioBitrates.BITRATE_16] + def supported_bit_rates(self) -> List[AudioBitRates]: + """Return a list of supported bit rates.""" + return [AudioBitRates.BITRATE_16] @property - def supported_samplerates(self) -> List[AudioSamplerates]: - """Return a list of supported samplerates.""" - return [AudioSamplerates.SAMPLERATE_16000, AudioSamplerates.SAMPLERATE_44100] + def supported_sample_rates(self) -> List[AudioSampleRates]: + """Return a list of supported sample rates.""" + return [AudioSampleRates.SAMPLERATE_16000, AudioSampleRates.SAMPLERATE_44100] async def async_process_audio_stream( self, metadata: SpeechMetadata, stream: StreamReader diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index ea5119b5e24b9a..b781c4666ae54b 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -21,10 +21,10 @@ from .const import ( DOMAIN, - AudioBitrates, + AudioBitRates, AudioCodecs, AudioFormats, - AudioSamplerates, + AudioSampleRates, SpeechResultState, ) @@ -76,8 +76,8 @@ class SpeechMetadata: language: str = attr.ib() format: AudioFormats = attr.ib() codec: AudioCodecs = attr.ib() - bitrate: AudioBitrates = attr.ib(converter=int) - samplerate: AudioSamplerates = attr.ib(converter=int) + bit_rate: AudioBitRates = attr.ib(converter=int) + sample_rate: AudioSampleRates = attr.ib(converter=int) @attr.s @@ -111,13 +111,13 @@ def supported_codecs(self) -> List[AudioCodecs]: @property @abstractmethod - def supported_bitrates(self) -> List[AudioBitrates]: - """Return a list of supported bitrates.""" + def supported_bit_rates(self) -> List[AudioBitRates]: + """Return a list of supported bit_rates.""" @property @abstractmethod - def supported_samplerates(self) -> List[AudioSamplerates]: - """Return a list of supported samplerates.""" + def supported_sample_rates(self) -> List[AudioSampleRates]: + """Return a list of supported sample_rates.""" @abstractmethod async def async_process_audio_stream( @@ -135,8 +135,8 @@ def check_metadata(self, metadata: SpeechMetadata) -> bool: metadata.language not in self.supported_languages or metadata.format not in self.supported_formats or metadata.codec not in self.supported_codecs - or metadata.bitrate not in self.supported_bitrates - or metadata.samplerate not in self.supported_samplerates + or metadata.bit_rate not in self.supported_bit_rates + or metadata.sample_rate not in self.supported_sample_rates ): return False return True @@ -211,7 +211,7 @@ async def get(self, request: web.Request, provider: str) -> web.Response: "languages": stt_provider.supported_languages, "formats": stt_provider.supported_formats, "codecs": stt_provider.supported_codecs, - "samplerates": stt_provider.supported_samplerates, - "bitrates": stt_provider.supported_bitrates, + "sample_rates": stt_provider.supported_sample_rates, + "bit_rates": stt_provider.supported_bit_rates, } ) diff --git a/homeassistant/components/stt/const.py b/homeassistant/components/stt/const.py index dfdd91d4f9663c..c653bcc3bd5953 100644 --- a/homeassistant/components/stt/const.py +++ b/homeassistant/components/stt/const.py @@ -18,8 +18,8 @@ class AudioFormats(str, Enum): OGG = "ogg" -class AudioBitrates(int, Enum): - """Supported Audio bitrates.""" +class AudioBitRates(int, Enum): + """Supported Audio bit_rates.""" BITRATE_8 = 8 BITRATE_16 = 16 @@ -27,8 +27,8 @@ class AudioBitrates(int, Enum): BITRATE_32 = 32 -class AudioSamplerates(int, Enum): - """Supported Audio samplerates.""" +class AudioSampleRates(int, Enum): + """Supported Audio sample_rates.""" SAMPLERATE_8000 = 8000 SAMPLERATE_11000 = 11000 diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py index 39fed7658eeec7..782d89688c7089 100644 --- a/tests/components/demo/test_stt.py +++ b/tests/components/demo/test_stt.py @@ -23,8 +23,8 @@ async def test_demo_settings(hass_client): assert response.status == 200 assert response_data == { "languages": ["en", "de"], - "bitrates": [16], - "samplerates": [16000, 44100], + "bit_rates": [16], + "sample_rates": [16000, 44100], "formats": ["wav"], "codecs": ["pcm"], } @@ -45,7 +45,7 @@ async def test_demo_speech_wrong_metadata(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; samplerate=8000; bitrate=16; language=de" + "X-Speech-Content": "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; language=de" }, data=b"Test", ) @@ -59,7 +59,7 @@ async def test_demo_speech(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; samplerate=16000; bitrate=16; language=de" + "X-Speech-Content": "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; language=de" }, data=b"Test", ) From 16a80beb4379b99f681f3cb019be0940efc49430 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 4 Nov 2019 14:14:57 -0800 Subject: [PATCH 1439/3953] Fix scaffold --- script/scaffold/gather_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index 12cb319d188ce0..6a69040a6d7554 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -120,7 +120,7 @@ def _load_existing_integration(domain) -> Info: manifest = json.loads((COMPONENT_DIR / domain / "manifest.json").read_text()) - return Info(domain=domain, name=manifest["name"]) + return Info(domain=domain, name=manifest["name"], is_new=False) def _gather_info(fields) -> dict: From ade60742d4667452a4297c8d664bf06f7fae2d9b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 5 Nov 2019 00:31:48 +0000 Subject: [PATCH 1440/3953] [ci skip] Translation update --- homeassistant/components/huawei_lte/.translations/en.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 0952b05a5cfb43..8681e3355a46c5 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured": "This device has already been configured", - "already_in_progress": "This device is already being configured", - "not_huawei_lte": "Not a Huawei LTE device" + "already_configured": "This device is already configured" }, "error": { "connection_failed": "Connection failed", From fb0e20543e6eae5f945f607017bcde41d678315c Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Tue, 5 Nov 2019 02:58:17 +0100 Subject: [PATCH 1441/3953] Update Plugwise (#28237) * Plugwise update * Fix DEFAULT_NAME * Fix pylint errors * Remove showing of DHW-status * Remove `if not None` where possible * Forgot to remove dhw-related code * Updated w.r.t. comments from MartinHjelmare * Remove the illuminance attribute - move to sensor * Update homeassistant/components/plugwise/climate.py Co-Authored-By: Martin Hjelmare * Breaking lines * Remove thermostat_temperature * Try fix lint errors. * Remove spaces * Remove more spaces --- CODEOWNERS | 2 +- homeassistant/components/plugwise/climate.py | 153 +++++++++++++----- .../components/plugwise/manifest.json | 4 +- requirements_all.txt | 2 +- 4 files changed, 117 insertions(+), 44 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 77a2ee8355b412..ce170df3602e23 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -229,7 +229,7 @@ homeassistant/components/pi_hole/* @fabaff @johnluetke homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren -homeassistant/components/plugwise/* @laetificat @CoMPaTech +homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew homeassistant/components/point/* @fredrike homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index b3b4cf3c1d441c..fa1ac86941b914 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -9,9 +9,11 @@ from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, CURRENT_HVAC_IDLE, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO, - HVAC_MODE_OFF, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -33,6 +35,7 @@ # Configuration directives CONF_MIN_TEMP = "min_temp" CONF_MAX_TEMP = "max_temp" +CONF_LEGACY = "legacy_anna" # Default directives DEFAULT_NAME = "Plugwise Thermostat" @@ -44,7 +47,8 @@ DEFAULT_MAX_TEMP = 30 # HVAC modes -ATTR_HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_OFF] +HVAC_MODES_1 = [HVAC_MODE_HEAT, HVAC_MODE_AUTO] +HVAC_MODES_2 = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO] # Read platform configuration PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -52,6 +56,7 @@ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_LEGACY, default=False): cv.boolean, 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, @@ -67,6 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config[CONF_PASSWORD], config[CONF_HOST], config[CONF_PORT], + config[CONF_LEGACY], ) try: api.ping_anna_thermostat() @@ -92,15 +98,28 @@ def __init__(self, api, name, min_temp, max_temp): self._name = name self._domain_objects = None self._outdoor_temperature = None - self._active_schema = None + self._selected_schema = None self._preset_mode = None - self._hvac_modes = ATTR_HVAC_MODES + self._presets = None + self._presets_list = None + self._heating_status = None + self._cooling_status = None + self._schema_names = None + self._schema_status = None + self._current_temperature = None + self._thermostat_temperature = None + self._boiler_temperature = None + self._water_pressure = None + self._schedule_temperature = None + self._hvac_mode = None @property def hvac_action(self): """Return the current action.""" - if self._api.get_heating_status(self._domain_objects): + if self._heating_status: return CURRENT_HVAC_HEAT + if self._cooling_status: + return CURRENT_HVAC_COOL return CURRENT_HVAC_IDLE @property @@ -122,49 +141,80 @@ def supported_features(self): 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 + if self._outdoor_temperature: + attributes["outdoor_temperature"] = self._outdoor_temperature + attributes["available_schemas"] = self._schema_names + attributes["selected_schema"] = self._selected_schema + if self._boiler_temperature: + attributes["boiler_temperature"] = self._boiler_temperature + if self._water_pressure: + attributes["water_pressure"] = self._water_pressure 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 preset_modes(self): + """Return the available preset modes list. + + And make the presets with their temperatures available. + """ + return self._presets_list + + @property + def hvac_modes(self): + """Return the available hvac modes list.""" + if self._heating_status is not None: + if self._cooling_status is not None: + return HVAC_MODES_2 + return HVAC_MODES_1 + return None @property def hvac_mode(self): """Return current active hvac state.""" - if self._api.get_schema_state(self._domain_objects): + if self._schema_status: return HVAC_MODE_AUTO - return HVAC_MODE_OFF + if self._heating_status: + if self._cooling_status: + return HVAC_MODE_HEAT_COOL + return HVAC_MODE_HEAT + return None @property - def preset_mode(self): - """Return the active preset mode.""" - return self._api.get_current_preset(self._domain_objects) + def target_temperature(self): + """Return the target_temperature. - @property - def preset_modes(self): - """Return the available preset modes list without values.""" - presets = list(self._api.get_presets(self._domain_objects)) - return presets + From the XML the thermostat-value is used because it updates 'immediately' + compared to the target_temperature-value. This way the information on the card + is "immediately" updated after changing the preset, temperature, etc. + """ + return self._thermostat_temperature @property - def hvac_modes(self): - """Return the available hvac modes list.""" - return self._hvac_modes + def preset_mode(self): + """Return the active selected schedule-name. + + Or return the active preset, or return Temporary in case of a manual change + in the set-temperature with a weekschedule active, + or return Manual in case of a manual change and no weekschedule active. + """ + if self._presets: + presets = self._presets + preset_temperature = presets.get(self._preset_mode, "none") + if self.hvac_mode == HVAC_MODE_AUTO: + if self._thermostat_temperature == self._schedule_temperature: + return "{}".format(self._selected_schema) + if self._thermostat_temperature == preset_temperature: + return self._preset_mode + return "Temporary" + if self._thermostat_temperature != preset_temperature: + return "Manual" + return self._preset_mode + return None @property def current_temperature(self): - """Return the current temperature.""" - return self._api.get_temperature(self._domain_objects) + """Return the current room temperature.""" + return self._current_temperature @property def min_temp(self): @@ -176,11 +226,6 @@ 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.""" @@ -203,11 +248,39 @@ def set_hvac_mode(self, hvac_mode): if hvac_mode == HVAC_MODE_AUTO: schema_mode = "true" self._api.set_schema_state( - self._domain_objects, self._active_schema, schema_mode + self._domain_objects, self._selected_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) + + 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._selected_schema = self._api.get_active_schema_name(self._domain_objects) + self._preset_mode = self._api.get_current_preset(self._domain_objects) + self._presets = self._api.get_presets(self._domain_objects) + self._presets_list = list(self._api.get_presets(self._domain_objects)) + self._heating_status = self._api.get_heating_status(self._domain_objects) + self._cooling_status = self._api.get_cooling_status(self._domain_objects) + self._schema_names = self._api.get_schema_names(self._domain_objects) + self._schema_status = self._api.get_schema_state(self._domain_objects) + self._current_temperature = self._api.get_current_temperature( + self._domain_objects + ) + self._thermostat_temperature = self._api.get_thermostat_temperature( + self._domain_objects + ) + self._schedule_temperature = self._api.get_schedule_temperature( + self._domain_objects + ) + self._boiler_temperature = self._api.get_boiler_temperature( + self._domain_objects + ) + self._water_pressure = self._api.get_water_pressure(self._domain_objects) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 1069c0bcdf02bd..e786b6a7f8ed70 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -3,6 +3,6 @@ "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", "dependencies": [], - "codeowners": ["@laetificat","@CoMPaTech"], - "requirements": ["haanna==0.10.1"] + "codeowners": ["@laetificat","@CoMPaTech","@bouwew"], + "requirements": ["haanna==0.13.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 04a858ce3b979e..282d04ac77c66e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -613,7 +613,7 @@ ha-ffmpeg==2.0 ha-philipsjs==0.0.8 # homeassistant.components.plugwise -haanna==0.10.1 +haanna==0.13.5 # homeassistant.components.habitica habitipy==0.2.0 From ef20f0985a318a81b13f6c90c5607eb9460a0c57 Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 5 Nov 2019 06:15:58 +0100 Subject: [PATCH 1442/3953] Improve scene.create service (#28533) * Improve scene.create service * Typo * Improve coverage * Add from_yaml attribute * from_service instead of from_yaml --- .../components/homeassistant/scene.py | 15 ++++--- tests/components/homeassistant/test_scene.py | 39 ++++++++++++++++--- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index f011dae150f937..c505d1534deb9d 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -141,11 +141,13 @@ async def create_service(call): """Create a scene.""" scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], call.data[CONF_ENTITIES]) entity_id = f"{SCENE_DOMAIN}.{scene_config.name}" - if hass.states.get(entity_id) is not None: - _LOGGER.warning("The scene %s already exists", entity_id) - return - - async_add_entities([HomeAssistantScene(hass, scene_config)]) + old = platform.entities.get(entity_id) + if old is not None: + if not old.from_service: + _LOGGER.warning("The scene %s already exists", entity_id) + return + await platform.async_remove_entity(entity_id) + async_add_entities([HomeAssistantScene(hass, scene_config, from_service=True)]) hass.services.async_register( SCENE_DOMAIN, SERVICE_CREATE, create_service, CREATE_SCENE_SCHEMA @@ -173,11 +175,12 @@ def _process_scenes_config(hass, async_add_entities, config): class HomeAssistantScene(Scene): """A scene is a group of entities and the states we want them to be.""" - def __init__(self, hass, scene_config, scene_id=None): + def __init__(self, hass, scene_config, scene_id=None, from_service=False): """Initialize the scene.""" self._id = scene_id self.hass = hass self.scene_config = scene_config + self.from_service = from_service @property def name(self): diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 08e40e23d12ad4..25ce6088a511e6 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -55,8 +55,13 @@ async def test_apply_service(hass): async def test_create_service(hass, caplog): """Test the create service.""" - assert await async_setup_component(hass, "scene", {}) + assert await async_setup_component( + hass, + "scene", + {"scene": {"name": "hallo_2", "entities": {"light.kitchen": "on"}}}, + ) assert hass.states.get("scene.hallo") is None + assert hass.states.get("scene.hallo_2") is not None assert await hass.services.async_call( "scene", @@ -67,8 +72,8 @@ async def test_create_service(hass, caplog): }, blocking=True, ) - await hass.async_block_till_done() + scene = hass.states.get("scene.hallo") assert scene is not None assert scene.domain == "scene" @@ -81,12 +86,34 @@ async def test_create_service(hass, caplog): "create", { "scene_id": "hallo", - "entities": {"light.bed_light": {"state": "on", "brightness": 50}}, + "entities": {"light.kitchen_light": {"state": "on", "brightness": 100}}, }, blocking=True, ) + await hass.async_block_till_done() + + scene = hass.states.get("scene.hallo") + assert scene is not None + assert scene.domain == "scene" + assert scene.name == "hallo" + assert scene.state == "scening" + assert scene.attributes.get("entity_id") == ["light.kitchen_light"] + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo_2", + "entities": {"light.bed_light": {"state": "on", "brightness": 50}}, + }, + blocking=True, + ) await hass.async_block_till_done() - assert "The scene scene.hallo already exists" in caplog.text - assert hass.states.get("scene.hallo") is not None - assert hass.states.get("scene.hallo_2") is None + + assert "The scene scene.hallo_2 already exists" in caplog.text + scene = hass.states.get("scene.hallo_2") + assert scene is not None + assert scene.domain == "scene" + assert scene.name == "hallo_2" + assert scene.state == "scening" + assert scene.attributes.get("entity_id") == ["light.kitchen"] From 804b6bbc0e8fa9fa587c923855be385f7d276b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 5 Nov 2019 07:21:52 +0200 Subject: [PATCH 1443/3953] Reduce test requirements duplication, sync flake8 and related (#28538) * Generate pre-commit test dependencies instead of duplicating * Upgrade/sync to flake8 3.7.9, flake8-docstrings 1.5.0, and pydocstyle 4.0.1 https://flake8.readthedocs.io/en/latest/release-notes/3.7.9.html https://gitlab.com/pycqa/flake8-docstrings/blob/1.4.0/HISTORY.rst https://gitlab.com/pycqa/flake8-docstrings/blob/1.5.0/HISTORY.rst http://www.pydocstyle.org/en/4.0.1/release_notes.html * Include requirements_test.txt from *_all.txt instead of copying --- .pre-commit-config-all.yaml | 6 +++--- .pre-commit-config.yaml | 6 +++--- requirements_test.txt | 6 +----- requirements_test_all.txt | 27 +++------------------------ requirements_test_pre_commit.txt | 6 ++++++ script/gen_requirements_all.py | 31 +++++++++++++++++++++++++++---- 6 files changed, 43 insertions(+), 39 deletions(-) create mode 100644 requirements_test_pre_commit.txt diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml index 98829e25fc37fb..3910835ae9d6a7 100644 --- a/.pre-commit-config-all.yaml +++ b/.pre-commit-config-all.yaml @@ -19,12 +19,12 @@ repos: - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.8 + rev: 3.7.9 hooks: - id: flake8 additional_dependencies: - - flake8-docstrings==1.3.1 - - pydocstyle==4.0.0 + - flake8-docstrings==1.5.0 + - pydocstyle==4.0.1 files: ^(homeassistant|script|tests)/.+\.py$ # Using a local "system" mypy instead of the mypy hook, because its # results depend on what is installed. And the mypy hook runs in a diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4beff14965b9ce..3220ac84866891 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,10 +15,10 @@ repos: - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.8 + rev: 3.7.9 hooks: - id: flake8 additional_dependencies: - - flake8-docstrings==1.3.1 - - pydocstyle==4.0.0 + - flake8-docstrings==1.5.0 + - pydocstyle==4.0.1 files: ^(homeassistant|script|tests)/.+\.py$ diff --git a/requirements_test.txt b/requirements_test.txt index 06a2ef1621d26e..33fab3d6c6af1d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,16 +2,12 @@ # make new things fail. Manually update these pins when pulling in a # new version -# When updating this file, update .pre-commit-config*.yaml too +-r requirements_test_pre_commit.txt asynctest==0.13.0 -black==19.10b0 codecov==2.0.15 -flake8-docstrings==1.5.0 -flake8==3.7.8 mock-open==1.3.1 mypy==0.740 pre-commit==1.20.0 -pydocstyle==4.0.1 pylint==2.4.3 astroid==2.3.2 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9395c0cefadbe2..2a9b30d3e4f6b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1,28 +1,7 @@ -# Home Assistant test -# linters such as flake8 and pylint should be pinned, as new releases -# make new things fail. Manually update these pins when pulling in a -# new version - -# When updating this file, update .pre-commit-config*.yaml too -asynctest==0.13.0 -black==19.10b0 -codecov==2.0.15 -flake8-docstrings==1.5.0 -flake8==3.7.8 -mock-open==1.3.1 -mypy==0.740 -pre-commit==1.20.0 -pydocstyle==4.0.1 -pylint==2.4.3 -astroid==2.3.2 -pytest-aiohttp==0.3.0 -pytest-cov==2.8.1 -pytest-sugar==0.9.2 -pytest-timeout==1.3.3 -pytest==5.2.2 -requests_mock==1.7.0 -responses==0.10.6 +# Home Assistant tests, full dependency set +# Automatically generated by gen_requirements_all.py, do not edit +-r requirements_test.txt # homeassistant.components.homekit HAP-python==2.6.0 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt new file mode 100644 index 00000000000000..29380ca7cd28be --- /dev/null +++ b/requirements_test_pre_commit.txt @@ -0,0 +1,6 @@ +# Automatically generated from .pre-commit-config-all.yaml by gen_requirements_all.py, do not edit + +black==19.10b0 +flake8-docstrings==1.5.0 +flake8==3.7.9 +pydocstyle==4.0.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 930ffa11b5f2cd..9bbe7d379ec29e 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -8,6 +8,8 @@ import re import sys +from homeassistant.util.yaml.loader import load_yaml + from script.hassfest.model import Integration COMMENT_REQUIREMENTS = ( @@ -225,10 +227,11 @@ def requirements_all_output(reqs): def requirements_test_output(reqs): """Generate output for test_requirements.""" output = [] - output.append("# Home Assistant test") - output.append("\n") - output.append(Path("requirements_test.txt").read_text()) - output.append("\n") + output.append("# Home Assistant tests, full dependency set\n") + output.append( + f"# Automatically generated by {Path(__file__).name}, do not edit\n\n" + ) + output.append("-r requirements_test.txt\n") filtered = { requirement: modules @@ -246,6 +249,24 @@ def requirements_test_output(reqs): return "".join(output) +def requirements_pre_commit_output(): + """Generate output for pre-commit dependencies.""" + source = ".pre-commit-config-all.yaml" + pre_commit_conf = load_yaml(source) + reqs = [] + for repo in (x for x in pre_commit_conf["repos"] if x.get("rev")): + for hook in repo["hooks"]: + reqs.append(f"{hook['id']}=={repo['rev']}") + reqs.extend(x for x in hook.get("additional_dependencies", ())) + output = [ + f"# Automatically generated " + f"from {source} by {Path(__file__).name}, do not edit", + "", + ] + output.extend(sorted(reqs)) + return "\n".join(output) + "\n" + + def gather_constraints(): """Construct output for constraint file.""" return ( @@ -285,10 +306,12 @@ def main(validate): reqs_file = requirements_all_output(data) reqs_test_file = requirements_test_output(data) + reqs_pre_commit_file = requirements_pre_commit_output() constraints = gather_constraints() files = ( ("requirements_all.txt", reqs_file), + ("requirements_test_pre_commit.txt", reqs_pre_commit_file), ("requirements_test_all.txt", reqs_test_file), ("homeassistant/package_constraints.txt", constraints), ) From 11efb2c2eb1e235fc9aa5f3e6cc6dab833877b21 Mon Sep 17 00:00:00 2001 From: Zach Date: Tue, 5 Nov 2019 05:43:36 -0500 Subject: [PATCH 1444/3953] Avoid drawing image_processing font text inside the bow line (#27796) * Adjust font text such that it won't be drawn inside the bow line in image_processing.draw_box * Adjust font_height after actually counting the pixels * Thinned out line_width and adjusted font size --- homeassistant/components/image_processing/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index e9621fe6bbe719..4c90441e7f0152 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -85,7 +85,8 @@ def draw_box( the bounding box will be `(40, 10)` to `(180, 50)` (in (x,y) coordinates). """ - line_width = 5 + line_width = 3 + font_height = 8 y_min, x_min, y_max, x_max = box (left, right, top, bottom) = ( x_min * img_width, @@ -99,7 +100,9 @@ def draw_box( fill=color, ) if text: - draw.text((left + line_width, abs(top - line_width)), text, fill=color) + draw.text( + (left + line_width, abs(top - line_width - font_height)), text, fill=color + ) async def async_setup(hass, config): From a43095b2b5f13321c139312bf733864ab1644c4f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 6 Nov 2019 02:24:20 +1300 Subject: [PATCH 1445/3953] Add override switch for juicenet (#28049) * Add override switch for juicenet * Update generated files * Update indentation * Fix indentation * Remove unnecessary else statement * Update homeassistant/components/juicenet/switch.py Co-Authored-By: Fabian Affolter * Update homeassistant/components/juicenet/switch.py Co-Authored-By: Fabian Affolter * Remove state property * Change string formatting * Bump juicenet package version again --- CODEOWNERS | 1 + homeassistant/components/juicenet/__init__.py | 6 ++- .../components/juicenet/manifest.json | 6 ++- homeassistant/components/juicenet/switch.py | 45 +++++++++++++++++++ requirements_all.txt | 2 +- 5 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/juicenet/switch.py diff --git a/CODEOWNERS b/CODEOWNERS index ce170df3602e23..ceb58f370d4ad8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -156,6 +156,7 @@ homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi +homeassistant/components/juicenet/* @jesserockz homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph homeassistant/components/keenetic_ndms2/* @foxel diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 207dac7836a011..55bf91ac398dd1 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -18,6 +18,8 @@ extra=vol.ALLOW_EXTRA, ) +JUICENET_COMPONENTS = ["sensor", "switch"] + def setup(hass, config): """Set up the Juicenet component.""" @@ -26,7 +28,9 @@ def setup(hass, config): access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) hass.data[DOMAIN]["api"] = pyjuicenet.Api(access_token) - discovery.load_platform(hass, "sensor", DOMAIN, {}, config) + for component in JUICENET_COMPONENTS: + discovery.load_platform(hass, component, DOMAIN, {}, config) + return True diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 1ef84b74502a9f..076567573c724d 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -3,8 +3,10 @@ "name": "Juicenet", "documentation": "https://www.home-assistant.io/integrations/juicenet", "requirements": [ - "python-juicenet==0.0.5" + "python-juicenet==0.1.5" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@jesserockz" + ] } diff --git a/homeassistant/components/juicenet/switch.py b/homeassistant/components/juicenet/switch.py new file mode 100644 index 00000000000000..30bb5b22814a1b --- /dev/null +++ b/homeassistant/components/juicenet/switch.py @@ -0,0 +1,45 @@ +"""Support for monitoring juicenet/juicepoint/juicebox based EVSE switches.""" +import logging + +from homeassistant.components.switch import SwitchDevice + +from . import DOMAIN, JuicenetDevice + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Juicenet switch.""" + api = hass.data[DOMAIN]["api"] + + devs = [] + for device in api.get_devices(): + devs.append(JuicenetChargeNowSwitch(device, hass)) + + add_entities(devs) + + +class JuicenetChargeNowSwitch(JuicenetDevice, SwitchDevice): + """Implementation of a Juicenet switch.""" + + def __init__(self, device, hass): + """Initialise the switch.""" + super().__init__(device, "charge_now", hass) + + @property + def name(self): + """Return the name of the device.""" + return f"{self.device.name()} Charge Now" + + @property + def is_on(self): + """Return true if switch is on.""" + return self.device.getOverrideTime() != 0 + + def turn_on(self, **kwargs): + """Charge now.""" + self.device.setOverride(True) + + def turn_off(self, **kwargs): + """Don't charge now.""" + self.device.setOverride(False) diff --git a/requirements_all.txt b/requirements_all.txt index 282d04ac77c66e..99c562870bb0fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1537,7 +1537,7 @@ python-izone==1.1.1 python-join-api==0.0.4 # homeassistant.components.juicenet -python-juicenet==0.0.5 +python-juicenet==0.1.5 # homeassistant.components.lirc # python-lirc==1.2.3 From 136f1f7fe9b1be4b2c3732a007081b3465cb81a6 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 5 Nov 2019 15:04:19 +0100 Subject: [PATCH 1446/3953] Move imports in samsungtv component (#27775) * Move imports in samsungtv component * Fix tests * Fix review 1 * Fix review 2 * wakeonlan is a module --- .../components/samsungtv/media_player.py | 24 +++++------ .../components/samsungtv/test_media_player.py | 42 +++++++++++-------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 94e9131ed3232f..aa6e3ae62d12a9 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -3,7 +3,9 @@ from datetime import timedelta import socket +from samsungctl import exceptions as samsung_exceptions, Remote as SamsungRemote import voluptuous as vol +import wakeonlan from homeassistant.components.media_player import ( MediaPlayerDevice, @@ -113,17 +115,11 @@ class SamsungTVDevice(MediaPlayerDevice): def __init__(self, host, port, name, timeout, mac, uuid): """Initialize the Samsung device.""" - from samsungctl import exceptions - from samsungctl import Remote - import wakeonlan # Save a reference to the imported classes - self._exceptions_class = exceptions - self._remote_class = Remote self._name = name self._mac = mac self._uuid = uuid - self._wol = wakeonlan # Assume that the TV is not muted self._muted = False # Assume that the TV is in Play mode @@ -163,13 +159,13 @@ def get_remote(self): try: self._config["method"] = method LOGGER.debug("Try config: %s", self._config) - self._remote = self._remote_class(self._config.copy()) + self._remote = SamsungRemote(self._config.copy()) self._state = STATE_ON LOGGER.debug("Found working config: %s", self._config) break except ( - self._exceptions_class.UnhandledResponse, - self._exceptions_class.AccessDenied, + samsung_exceptions.UnhandledResponse, + samsung_exceptions.AccessDenied, ): # We got a response so it's working. self._state = STATE_ON @@ -189,7 +185,7 @@ def get_remote(self): if self._remote is None: # We need to create a new instance to reconnect. - self._remote = self._remote_class(self._config.copy()) + self._remote = SamsungRemote(self._config.copy()) return self._remote @@ -205,7 +201,7 @@ def send_key(self, key): try: self.get_remote().control(key) break - except (self._exceptions_class.ConnectionClosed, BrokenPipeError): + except (samsung_exceptions.ConnectionClosed, BrokenPipeError): # BrokenPipe can occur when the commands is sent to fast self._remote = None self._state = STATE_ON @@ -213,8 +209,8 @@ def send_key(self, key): # Auto-detect could not find working config yet pass except ( - self._exceptions_class.UnhandledResponse, - self._exceptions_class.AccessDenied, + samsung_exceptions.UnhandledResponse, + samsung_exceptions.AccessDenied, ): # We got a response so it's on. self._state = STATE_ON @@ -343,7 +339,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): def turn_on(self): """Turn the media player on.""" if self._mac: - self._wol.send_magic_packet(self._mac) + wakeonlan.send_magic_packet(self._mac) else: self.send_key("KEY_POWERON") diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index deb39b4077fe76..2b5e377c617a52 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1,12 +1,13 @@ -"""Tests for samsungtv Components.""" +"""Tests for samsungtv component.""" import asyncio -from asynctest import mock +from unittest.mock import call, patch from datetime import timedelta + import logging +from asynctest import mock import pytest from samsungctl import exceptions -from tests.common import MockDependency, async_fire_time_changed -from unittest.mock import call, patch +from tests.common import async_fire_time_changed from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( @@ -62,7 +63,7 @@ CONF_NAME: "fake", CONF_PORT: 8001, CONF_TIMEOUT: 10, - CONF_MAC: "fake", + CONF_MAC: "38:f9:d3:82:b4:f1", } } @@ -125,7 +126,9 @@ @pytest.fixture(name="remote") def remote_fixture(): """Patch the samsungctl Remote.""" - with patch("samsungctl.Remote") as remote_class, patch( + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote" + ) as remote_class, patch( "homeassistant.components.samsungtv.media_player.socket" ) as socket_class: remote = mock.Mock() @@ -138,8 +141,10 @@ def remote_fixture(): @pytest.fixture(name="wakeonlan") def wakeonlan_fixture(): """Patch the wakeonlan Remote.""" - with MockDependency("wakeonlan") as wakeonlan: - yield wakeonlan + with patch( + "homeassistant.components.samsungtv.media_player.wakeonlan" + ) as wakeonlan_module: + yield wakeonlan_module @pytest.fixture @@ -249,9 +254,9 @@ async def test_send_key(hass, remote, wakeonlan): async def test_send_key_autodetect_websocket(hass, remote): """Test for send key with autodetection of protocol.""" - with patch("samsungctl.Remote") as remote, patch( - "homeassistant.components.samsungtv.media_player.socket" - ): + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote" + ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): await setup_samsungtv(hass, MOCK_CONFIG_AUTO) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True @@ -266,7 +271,8 @@ async def test_send_key_autodetect_websocket_exception(hass, caplog): """Test for send key with autodetection of protocol.""" caplog.set_level(logging.DEBUG) with patch( - "samsungctl.Remote", side_effect=[exceptions.AccessDenied("Boom"), mock.DEFAULT] + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=[exceptions.AccessDenied("Boom"), mock.DEFAULT], ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): await setup_samsungtv(hass, MOCK_CONFIG_AUTO) assert await hass.services.async_call( @@ -287,7 +293,8 @@ async def test_send_key_autodetect_websocket_exception(hass, caplog): async def test_send_key_autodetect_legacy(hass, remote): """Test for send key with autodetection of protocol.""" with patch( - "samsungctl.Remote", side_effect=[OSError("Boom"), mock.DEFAULT] + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=[OSError("Boom"), mock.DEFAULT], ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): await setup_samsungtv(hass, MOCK_CONFIG_AUTO) assert await hass.services.async_call( @@ -304,9 +311,10 @@ async def test_send_key_autodetect_legacy(hass, remote): async def test_send_key_autodetect_none(hass, remote): """Test for send key with autodetection of protocol.""" - with patch("samsungctl.Remote", side_effect=OSError("Boom")) as remote, patch( - "homeassistant.components.samsungtv.media_player.socket" - ): + with patch( + "homeassistant.components.samsungtv.media_player.SamsungRemote", + side_effect=OSError("Boom"), + ) as remote, patch("homeassistant.components.samsungtv.media_player.socket"): await setup_samsungtv(hass, MOCK_CONFIG_AUTO) assert await hass.services.async_call( DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID_AUTO}, True @@ -557,7 +565,7 @@ async def test_turn_on_with_mac(hass, remote, wakeonlan): ) # key and update called assert wakeonlan.send_magic_packet.call_count == 1 - assert wakeonlan.send_magic_packet.call_args_list == [call("fake")] + assert wakeonlan.send_magic_packet.call_args_list == [call("38:f9:d3:82:b4:f1")] async def test_turn_on_without_mac(hass, remote): From 7b86f0f9265dc8cdcf7ed92c0db7340604134569 Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 5 Nov 2019 15:43:50 +0100 Subject: [PATCH 1447/3953] Add deprecated attributes to light.reproduce_state (#28557) * Add deprecated attributes to light.reproduce_state * Add blank line * fix minor bug * Typo --- .../components/light/reproduce_state.py | 42 +++++++++++- .../components/light/test_reproduce_state.py | 66 +++++++++++++++++-- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index ae618f7a8efa7d..c84b3627bed0b1 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -17,10 +17,16 @@ from . import ( DOMAIN, ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_EFFECT, + ATTR_FLASH, ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_PROFILE, ATTR_RGB_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, ) @@ -28,8 +34,36 @@ _LOGGER = logging.getLogger(__name__) VALID_STATES = {STATE_ON, STATE_OFF} -ATTR_GROUP = [ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_WHITE_VALUE] -COLOR_GROUP = [ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_XY_COLOR] + +ATTR_GROUP = [ + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_EFFECT, + ATTR_FLASH, + ATTR_WHITE_VALUE, + ATTR_TRANSITION, +] + +COLOR_GROUP = [ + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_PROFILE, + ATTR_RGB_COLOR, + ATTR_XY_COLOR, +] + +DEPRECATED_GROUP = [ + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, + ATTR_FLASH, + ATTR_KELVIN, + ATTR_PROFILE, + ATTR_TRANSITION, +] + +DEPRECATION_WARNING = "The use of other attributes than device state attributes is deprecated and will be removed in a future release. Read the logs for further details: https://www.home-assistant.io/integrations/scene/" async def _async_reproduce_state( @@ -48,6 +82,10 @@ async def _async_reproduce_state( ) return + # Warn if deprecated attributes are used + if any(attr in DEPRECATED_GROUP for attr in state.attributes): + _LOGGER.warning(DEPRECATION_WARNING) + # Return if we are already at the right state. if cur_state.state == state.state and all( check_attr_equal(cur_state.attributes, state.attributes, attr) diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 92790890a4c25e..250a0fe26a8251 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -1,13 +1,19 @@ """Test reproduce state for Light.""" +from homeassistant.components.light.reproduce_state import DEPRECATION_WARNING from homeassistant.core import State from tests.common import async_mock_service VALID_BRIGHTNESS = {"brightness": 180} VALID_WHITE_VALUE = {"white_value": 200} +VALID_FLASH = {"flash": "short"} VALID_EFFECT = {"effect": "random"} +VALID_TRANSITION = {"transition": 15} +VALID_COLOR_NAME = {"color_name": "red"} VALID_COLOR_TEMP = {"color_temp": 240} VALID_HS_COLOR = {"hs_color": (345, 75)} +VALID_KELVIN = {"kelvin": 4000} +VALID_PROFILE = {"profile": "relax"} VALID_RGB_COLOR = {"rgb_color": (255, 63, 111)} VALID_XY_COLOR = {"xy_color": (0.59, 0.274)} @@ -17,9 +23,14 @@ async def test_reproducing_states(hass, caplog): hass.states.async_set("light.entity_off", "off", {}) hass.states.async_set("light.entity_bright", "on", VALID_BRIGHTNESS) hass.states.async_set("light.entity_white", "on", VALID_WHITE_VALUE) + hass.states.async_set("light.entity_flash", "on", VALID_FLASH) hass.states.async_set("light.entity_effect", "on", VALID_EFFECT) + hass.states.async_set("light.entity_trans", "on", VALID_TRANSITION) + hass.states.async_set("light.entity_name", "on", VALID_COLOR_NAME) hass.states.async_set("light.entity_temp", "on", VALID_COLOR_TEMP) hass.states.async_set("light.entity_hs", "on", VALID_HS_COLOR) + hass.states.async_set("light.entity_kelvin", "on", VALID_KELVIN) + hass.states.async_set("light.entity_profile", "on", VALID_PROFILE) hass.states.async_set("light.entity_rgb", "on", VALID_RGB_COLOR) hass.states.async_set("light.entity_xy", "on", VALID_XY_COLOR) @@ -32,9 +43,14 @@ async def test_reproducing_states(hass, caplog): State("light.entity_off", "off"), State("light.entity_bright", "on", VALID_BRIGHTNESS), State("light.entity_white", "on", VALID_WHITE_VALUE), + State("light.entity_flash", "on", VALID_FLASH), State("light.entity_effect", "on", VALID_EFFECT), + State("light.entity_trans", "on", VALID_TRANSITION), + State("light.entity_name", "on", VALID_COLOR_NAME), State("light.entity_temp", "on", VALID_COLOR_TEMP), State("light.entity_hs", "on", VALID_HS_COLOR), + State("light.entity_kelvin", "on", VALID_KELVIN), + State("light.entity_profile", "on", VALID_PROFILE), State("light.entity_rgb", "on", VALID_RGB_COLOR), State("light.entity_xy", "on", VALID_XY_COLOR), ], @@ -59,16 +75,21 @@ async def test_reproducing_states(hass, caplog): State("light.entity_xy", "off"), State("light.entity_off", "on", VALID_BRIGHTNESS), State("light.entity_bright", "on", VALID_WHITE_VALUE), - State("light.entity_white", "on", VALID_EFFECT), - State("light.entity_effect", "on", VALID_COLOR_TEMP), + State("light.entity_white", "on", VALID_FLASH), + State("light.entity_flash", "on", VALID_EFFECT), + State("light.entity_effect", "on", VALID_TRANSITION), + State("light.entity_trans", "on", VALID_COLOR_NAME), + State("light.entity_name", "on", VALID_COLOR_TEMP), State("light.entity_temp", "on", VALID_HS_COLOR), - State("light.entity_hs", "on", VALID_RGB_COLOR), + State("light.entity_hs", "on", VALID_KELVIN), + State("light.entity_kelvin", "on", VALID_PROFILE), + State("light.entity_profile", "on", VALID_RGB_COLOR), State("light.entity_rgb", "on", VALID_XY_COLOR), ], blocking=True, ) - assert len(turn_on_calls) == 7 + assert len(turn_on_calls) == 12 expected_calls = [] @@ -80,22 +101,42 @@ async def test_reproducing_states(hass, caplog): expected_bright["entity_id"] = "light.entity_bright" expected_calls.append(expected_bright) - expected_white = VALID_EFFECT + expected_white = VALID_FLASH expected_white["entity_id"] = "light.entity_white" expected_calls.append(expected_white) - expected_effect = VALID_COLOR_TEMP + expected_flash = VALID_EFFECT + expected_flash["entity_id"] = "light.entity_flash" + expected_calls.append(expected_flash) + + expected_effect = VALID_TRANSITION expected_effect["entity_id"] = "light.entity_effect" expected_calls.append(expected_effect) + expected_trans = VALID_COLOR_NAME + expected_trans["entity_id"] = "light.entity_trans" + expected_calls.append(expected_trans) + + expected_name = VALID_COLOR_TEMP + expected_name["entity_id"] = "light.entity_name" + expected_calls.append(expected_name) + expected_temp = VALID_HS_COLOR expected_temp["entity_id"] = "light.entity_temp" expected_calls.append(expected_temp) - expected_hs = VALID_RGB_COLOR + expected_hs = VALID_KELVIN expected_hs["entity_id"] = "light.entity_hs" expected_calls.append(expected_hs) + expected_kelvin = VALID_PROFILE + expected_kelvin["entity_id"] = "light.entity_kelvin" + expected_calls.append(expected_kelvin) + + expected_profile = VALID_RGB_COLOR + expected_profile["entity_id"] = "light.entity_profile" + expected_calls.append(expected_profile) + expected_rgb = VALID_XY_COLOR expected_rgb["entity_id"] = "light.entity_rgb" expected_calls.append(expected_rgb) @@ -115,3 +156,14 @@ async def test_reproducing_states(hass, caplog): assert len(turn_off_calls) == 1 assert turn_off_calls[0].domain == "light" assert turn_off_calls[0].data == {"entity_id": "light.entity_xy"} + + +async def test_deprecation_warning(hass, caplog): + """Test deprecation warning.""" + hass.states.async_set("light.entity_off", "off", {}) + turn_on_calls = async_mock_service(hass, "light", "turn_on") + await hass.helpers.state.async_reproduce_state( + [State("light.entity_off", "on", {"brightness_pct": 80})], blocking=True + ) + assert len(turn_on_calls) == 1 + assert DEPRECATION_WARNING in caplog.text From 1618c5c040989a7f99b44c8bb49b5722963b7438 Mon Sep 17 00:00:00 2001 From: Tim McCormick Date: Sun, 3 Nov 2019 04:25:24 +0000 Subject: [PATCH 1448/3953] Fix missing import (#28460) --- homeassistant/components/sonos/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 94d252e9fee3f7..2baa02d0a5dfce 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -8,6 +8,7 @@ import async_timeout import pysonos +from pysonos import alarms from pysonos.exceptions import SoCoException, SoCoUPnPException import pysonos.snapshot @@ -1163,7 +1164,7 @@ def set_alarm(self, data): """Set the alarm clock on the player.""" alarm = None - for one_alarm in pysonos.alarms.get_alarms(self.soco): + for one_alarm in alarms.get_alarms(self.soco): # pylint: disable=protected-access if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]): alarm = one_alarm From f5306f769cda3d3f1b814189db069931383cf499 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 4 Nov 2019 00:58:35 +0100 Subject: [PATCH 1449/3953] Fix Airly if more than one config entry (#28498) --- homeassistant/components/airly/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 80f3518c652832..ce165918ac244a 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -31,6 +31,8 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: """Set up configured Airly.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} return True @@ -49,8 +51,6 @@ async def async_setup_entry(hass, config_entry): if not airly.data: raise ConfigEntryNotReady() - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_CLIENT] = {} hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly hass.async_create_task( From 2814a24f9e12b9bb69ad0bb7e87a9c1fc605df4e Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 5 Nov 2019 15:43:50 +0100 Subject: [PATCH 1450/3953] Add deprecated attributes to light.reproduce_state (#28557) * Add deprecated attributes to light.reproduce_state * Add blank line * fix minor bug * Typo --- .../components/light/reproduce_state.py | 42 +++++++++++- .../components/light/test_reproduce_state.py | 66 +++++++++++++++++-- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index ae618f7a8efa7d..c84b3627bed0b1 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -17,10 +17,16 @@ from . import ( DOMAIN, ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_EFFECT, + ATTR_FLASH, ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_PROFILE, ATTR_RGB_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, ) @@ -28,8 +34,36 @@ _LOGGER = logging.getLogger(__name__) VALID_STATES = {STATE_ON, STATE_OFF} -ATTR_GROUP = [ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_WHITE_VALUE] -COLOR_GROUP = [ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_XY_COLOR] + +ATTR_GROUP = [ + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_EFFECT, + ATTR_FLASH, + ATTR_WHITE_VALUE, + ATTR_TRANSITION, +] + +COLOR_GROUP = [ + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_PROFILE, + ATTR_RGB_COLOR, + ATTR_XY_COLOR, +] + +DEPRECATED_GROUP = [ + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, + ATTR_FLASH, + ATTR_KELVIN, + ATTR_PROFILE, + ATTR_TRANSITION, +] + +DEPRECATION_WARNING = "The use of other attributes than device state attributes is deprecated and will be removed in a future release. Read the logs for further details: https://www.home-assistant.io/integrations/scene/" async def _async_reproduce_state( @@ -48,6 +82,10 @@ async def _async_reproduce_state( ) return + # Warn if deprecated attributes are used + if any(attr in DEPRECATED_GROUP for attr in state.attributes): + _LOGGER.warning(DEPRECATION_WARNING) + # Return if we are already at the right state. if cur_state.state == state.state and all( check_attr_equal(cur_state.attributes, state.attributes, attr) diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 92790890a4c25e..250a0fe26a8251 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -1,13 +1,19 @@ """Test reproduce state for Light.""" +from homeassistant.components.light.reproduce_state import DEPRECATION_WARNING from homeassistant.core import State from tests.common import async_mock_service VALID_BRIGHTNESS = {"brightness": 180} VALID_WHITE_VALUE = {"white_value": 200} +VALID_FLASH = {"flash": "short"} VALID_EFFECT = {"effect": "random"} +VALID_TRANSITION = {"transition": 15} +VALID_COLOR_NAME = {"color_name": "red"} VALID_COLOR_TEMP = {"color_temp": 240} VALID_HS_COLOR = {"hs_color": (345, 75)} +VALID_KELVIN = {"kelvin": 4000} +VALID_PROFILE = {"profile": "relax"} VALID_RGB_COLOR = {"rgb_color": (255, 63, 111)} VALID_XY_COLOR = {"xy_color": (0.59, 0.274)} @@ -17,9 +23,14 @@ async def test_reproducing_states(hass, caplog): hass.states.async_set("light.entity_off", "off", {}) hass.states.async_set("light.entity_bright", "on", VALID_BRIGHTNESS) hass.states.async_set("light.entity_white", "on", VALID_WHITE_VALUE) + hass.states.async_set("light.entity_flash", "on", VALID_FLASH) hass.states.async_set("light.entity_effect", "on", VALID_EFFECT) + hass.states.async_set("light.entity_trans", "on", VALID_TRANSITION) + hass.states.async_set("light.entity_name", "on", VALID_COLOR_NAME) hass.states.async_set("light.entity_temp", "on", VALID_COLOR_TEMP) hass.states.async_set("light.entity_hs", "on", VALID_HS_COLOR) + hass.states.async_set("light.entity_kelvin", "on", VALID_KELVIN) + hass.states.async_set("light.entity_profile", "on", VALID_PROFILE) hass.states.async_set("light.entity_rgb", "on", VALID_RGB_COLOR) hass.states.async_set("light.entity_xy", "on", VALID_XY_COLOR) @@ -32,9 +43,14 @@ async def test_reproducing_states(hass, caplog): State("light.entity_off", "off"), State("light.entity_bright", "on", VALID_BRIGHTNESS), State("light.entity_white", "on", VALID_WHITE_VALUE), + State("light.entity_flash", "on", VALID_FLASH), State("light.entity_effect", "on", VALID_EFFECT), + State("light.entity_trans", "on", VALID_TRANSITION), + State("light.entity_name", "on", VALID_COLOR_NAME), State("light.entity_temp", "on", VALID_COLOR_TEMP), State("light.entity_hs", "on", VALID_HS_COLOR), + State("light.entity_kelvin", "on", VALID_KELVIN), + State("light.entity_profile", "on", VALID_PROFILE), State("light.entity_rgb", "on", VALID_RGB_COLOR), State("light.entity_xy", "on", VALID_XY_COLOR), ], @@ -59,16 +75,21 @@ async def test_reproducing_states(hass, caplog): State("light.entity_xy", "off"), State("light.entity_off", "on", VALID_BRIGHTNESS), State("light.entity_bright", "on", VALID_WHITE_VALUE), - State("light.entity_white", "on", VALID_EFFECT), - State("light.entity_effect", "on", VALID_COLOR_TEMP), + State("light.entity_white", "on", VALID_FLASH), + State("light.entity_flash", "on", VALID_EFFECT), + State("light.entity_effect", "on", VALID_TRANSITION), + State("light.entity_trans", "on", VALID_COLOR_NAME), + State("light.entity_name", "on", VALID_COLOR_TEMP), State("light.entity_temp", "on", VALID_HS_COLOR), - State("light.entity_hs", "on", VALID_RGB_COLOR), + State("light.entity_hs", "on", VALID_KELVIN), + State("light.entity_kelvin", "on", VALID_PROFILE), + State("light.entity_profile", "on", VALID_RGB_COLOR), State("light.entity_rgb", "on", VALID_XY_COLOR), ], blocking=True, ) - assert len(turn_on_calls) == 7 + assert len(turn_on_calls) == 12 expected_calls = [] @@ -80,22 +101,42 @@ async def test_reproducing_states(hass, caplog): expected_bright["entity_id"] = "light.entity_bright" expected_calls.append(expected_bright) - expected_white = VALID_EFFECT + expected_white = VALID_FLASH expected_white["entity_id"] = "light.entity_white" expected_calls.append(expected_white) - expected_effect = VALID_COLOR_TEMP + expected_flash = VALID_EFFECT + expected_flash["entity_id"] = "light.entity_flash" + expected_calls.append(expected_flash) + + expected_effect = VALID_TRANSITION expected_effect["entity_id"] = "light.entity_effect" expected_calls.append(expected_effect) + expected_trans = VALID_COLOR_NAME + expected_trans["entity_id"] = "light.entity_trans" + expected_calls.append(expected_trans) + + expected_name = VALID_COLOR_TEMP + expected_name["entity_id"] = "light.entity_name" + expected_calls.append(expected_name) + expected_temp = VALID_HS_COLOR expected_temp["entity_id"] = "light.entity_temp" expected_calls.append(expected_temp) - expected_hs = VALID_RGB_COLOR + expected_hs = VALID_KELVIN expected_hs["entity_id"] = "light.entity_hs" expected_calls.append(expected_hs) + expected_kelvin = VALID_PROFILE + expected_kelvin["entity_id"] = "light.entity_kelvin" + expected_calls.append(expected_kelvin) + + expected_profile = VALID_RGB_COLOR + expected_profile["entity_id"] = "light.entity_profile" + expected_calls.append(expected_profile) + expected_rgb = VALID_XY_COLOR expected_rgb["entity_id"] = "light.entity_rgb" expected_calls.append(expected_rgb) @@ -115,3 +156,14 @@ async def test_reproducing_states(hass, caplog): assert len(turn_off_calls) == 1 assert turn_off_calls[0].domain == "light" assert turn_off_calls[0].data == {"entity_id": "light.entity_xy"} + + +async def test_deprecation_warning(hass, caplog): + """Test deprecation warning.""" + hass.states.async_set("light.entity_off", "off", {}) + turn_on_calls = async_mock_service(hass, "light", "turn_on") + await hass.helpers.state.async_reproduce_state( + [State("light.entity_off", "on", {"brightness_pct": 80})], blocking=True + ) + assert len(turn_on_calls) == 1 + assert DEPRECATION_WARNING in caplog.text From b2a5c75fbda410c02fab8a2d003d7506f1abb0b7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Nov 2019 09:09:00 -0800 Subject: [PATCH 1451/3953] Bumped version to 0.101.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 575a7d5740f8db..424944f244c17a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From a0443b0238afed2075154ec238b56b7efa448106 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 2 Nov 2019 21:21:13 -0700 Subject: [PATCH 1452/3953] Fix flaky test --- tests/test_requirements.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 782b4386552964..2627a077a87c74 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -144,14 +144,18 @@ async def test_get_integration_with_requirements(hass): assert integration.domain == "test_component" assert len(mock_is_installed.mock_calls) == 3 - assert mock_is_installed.mock_calls[0][1][0] == "test-comp==1.0.0" - assert mock_is_installed.mock_calls[1][1][0] == "test-comp-dep==1.0.0" - assert mock_is_installed.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" + assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] assert len(mock_inst.mock_calls) == 3 - assert mock_inst.mock_calls[0][1][0] == "test-comp==1.0.0" - assert mock_inst.mock_calls[1][1][0] == "test-comp-dep==1.0.0" - assert mock_inst.mock_calls[2][1][0] == "test-comp-after-dep==1.0.0" + assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ + "test-comp-after-dep==1.0.0", + "test-comp-dep==1.0.0", + "test-comp==1.0.0", + ] async def test_install_with_wheels_index(hass): From 1e398a89669599a3c34e8b79df1301998d6e3a08 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 5 Nov 2019 21:12:29 +0100 Subject: [PATCH 1453/3953] Try fix tests (#28470) --- azure-pipelines-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index e80f4b8d0ba8e3..bcd72882df5851 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -127,7 +127,7 @@ stages: set -e . venv/bin/activate - pytest --timeout=9 --durations=10 -n 2 --dist loadfile -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 -n auto --dist=loadfile -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'])) From 10247f6799e75a9e4f8ee9a75d0f37c77b59d4b6 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 5 Nov 2019 21:38:30 +0100 Subject: [PATCH 1454/3953] Fix dev dockerfile --- Dockerfile.dev | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index eb76fe5b16b038..fa90a84fc1e57b 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -24,9 +24,9 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ WORKDIR /workspaces # Install Python dependencies from requirements -COPY requirements_test.txt homeassistant/package_constraints.txt ./ +COPY requirements_test.txt requirements_test_pre_commit.txt homeassistant/package_constraints.txt ./ RUN pip3 install -r requirements_test.txt -c package_constraints.txt \ - && rm -f requirements_test.txt package_constraints.txt + && rm -f requirements_test.txt package_constraints.txt requirements_test_pre_commit.txt # Set the default shell to bash instead of sh ENV SHELL /bin/bash From 925e26b0618247b6eccc5a0fedc6afa2ed9d2a58 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 5 Nov 2019 21:58:35 +0100 Subject: [PATCH 1455/3953] Update azure-pipelines-ci.yml --- azure-pipelines-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index bcd72882df5851..37473b92620128 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -135,7 +135,7 @@ stages: set -e . venv/bin/activate - pytest --timeout=9 --durations=10 -n 2 --dist loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 -n auto --dist=loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests codecov --token $(codecovToken) script/check_dirty displayName: 'Run pytest for python $(python.container) / coverage' From 005a1b2713c92d312fa8c267e25cf4882f5a4a95 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 5 Nov 2019 22:39:15 +0100 Subject: [PATCH 1456/3953] Add additional support over NC (#28527) * Add voice support over NC * Add disocery support for TTS / STT * fix cloud TTS discovery * Fix dev config * Fix discovery * Bump hass-nabucasa 0.25 * Add channel support * Fix lint * Update homeassistant/components/cloud/__init__.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/cloud/tts.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/cloud/tts.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/cloud/tts.py Co-Authored-By: Paulus Schoutsen * bump hass-nabucasa * Update tts.py * fix lint --- homeassistant/components/amazon_polly/tts.py | 2 +- homeassistant/components/baidu/tts.py | 2 +- homeassistant/components/cloud/__init__.py | 19 +++- homeassistant/components/cloud/const.py | 1 + homeassistant/components/cloud/manifest.json | 2 +- homeassistant/components/cloud/stt.py | 106 ++++++++++++++++++ homeassistant/components/cloud/tts.py | 81 +++++++++++++ homeassistant/components/demo/stt.py | 10 +- homeassistant/components/demo/tts.py | 2 +- homeassistant/components/google_cloud/tts.py | 2 +- .../components/google_translate/tts.py | 2 +- homeassistant/components/marytts/tts.py | 2 +- homeassistant/components/microsoft/tts.py | 2 +- homeassistant/components/picotts/tts.py | 2 +- homeassistant/components/stt/__init__.py | 35 ++++-- homeassistant/components/stt/const.py | 11 +- homeassistant/components/tts/__init__.py | 21 +++- homeassistant/components/voicerss/tts.py | 2 +- homeassistant/components/watson_tts/tts.py | 2 +- homeassistant/components/yandextts/tts.py | 2 +- homeassistant/package_constraints.txt | 2 +- pylintrc | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/demo/test_stt.py | 5 +- 25 files changed, 282 insertions(+), 38 deletions(-) create mode 100644 homeassistant/components/cloud/stt.py create mode 100644 homeassistant/components/cloud/tts.py diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 3acfd472320023..3d05236935fb29 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -145,7 +145,7 @@ ) -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up Amazon Polly speech component.""" output_format = config.get(CONF_OUTPUT_FORMAT) sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format]) diff --git a/homeassistant/components/baidu/tts.py b/homeassistant/components/baidu/tts.py index 8d753753e5a25d..4208750b7fcc36 100644 --- a/homeassistant/components/baidu/tts.py +++ b/homeassistant/components/baidu/tts.py @@ -52,7 +52,7 @@ SUPPORTED_OPTIONS = [CONF_PERSON, CONF_PITCH, CONF_SPEED, CONF_VOLUME] -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up Baidu TTS component.""" return BaiduTTSProvider(hass, config) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 2d5a2c8b448660..763f6214185560 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -23,6 +23,7 @@ from . import account_link, http_api from .client import CloudClient from .const import ( + CONF_ACCOUNT_LINK_URL, CONF_ACME_DIRECTORY_SERVER, CONF_ALEXA, CONF_ALEXA_ACCESS_TOKEN_URL, @@ -38,7 +39,7 @@ CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, CONF_USER_POOL_ID, - CONF_ACCOUNT_LINK_URL, + CONF_VOICE_API_URL, DOMAIN, MODE_DEV, MODE_PROD, @@ -103,6 +104,7 @@ vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(), vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(), vol.Optional(CONF_ACCOUNT_LINK_URL): vol.Url(), + vol.Optional(CONF_VOICE_API_URL): vol.Url(), } ) }, @@ -230,21 +232,28 @@ async def _service_handler(service): DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler ) - loaded_binary_sensor = False + loaded = False async def _on_connect(): """Discover RemoteUI binary sensor.""" - nonlocal loaded_binary_sensor + nonlocal loaded - if loaded_binary_sensor: + # Prevent multiple discovery + if loaded: return + loaded = True - loaded_binary_sensor = True hass.async_create_task( hass.helpers.discovery.async_load_platform( "binary_sensor", DOMAIN, {}, config ) ) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("stt", DOMAIN, {}, config) + ) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("tts", DOMAIN, {}, config) + ) cloud.iot.register_on_connect(_on_connect) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 262f84a85e653f..9a2dccf8d7cb60 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -38,6 +38,7 @@ CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url" CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url" CONF_ACCOUNT_LINK_URL = "account_link_url" +CONF_VOICE_API_URL = "voice_api_url" MODE_DEV = "development" MODE_PROD = "production" diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 9e9b77287ae53d..2876ff11b7ea94 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/integrations/cloud", - "requirements": ["hass-nabucasa==0.23"], + "requirements": ["hass-nabucasa==0.26"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py new file mode 100644 index 00000000000000..acca36afae9846 --- /dev/null +++ b/homeassistant/components/cloud/stt.py @@ -0,0 +1,106 @@ +"""Support for the cloud for speech to text service.""" +from typing import List + +from aiohttp import StreamReader +from hass_nabucasa import Cloud +from hass_nabucasa.voice import VoiceError + +from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult +from homeassistant.components.stt.const import ( + AudioBitRates, + AudioChannels, + AudioCodecs, + AudioFormats, + AudioSampleRates, + SpeechResultState, +) + +from .const import DOMAIN + +SUPPORT_LANGUAGES = [ + "da-DK", + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-US", + "es-ES", + "fi-FI", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + "nl-NL", + "pl-PL", + "pt-PT", + "ru-RU", + "sv-SE", + "th-TH", + "zh-CN", + "zh-HK", +] + + +async def async_get_engine(hass, config, discovery_info=None): + """Set up Cloud speech component.""" + cloud: Cloud = hass.data[DOMAIN] + + return CloudProvider(cloud) + + +class CloudProvider(Provider): + """NabuCasa speech API provider.""" + + def __init__(self, cloud: Cloud) -> None: + """Hass NabuCasa Speech to text.""" + self.cloud = cloud + + @property + def supported_languages(self) -> List[str]: + """Return a list of supported languages.""" + return SUPPORT_LANGUAGES + + @property + def supported_formats(self) -> List[AudioFormats]: + """Return a list of supported formats.""" + return [AudioFormats.WAV, AudioFormats.OGG] + + @property + def supported_codecs(self) -> List[AudioCodecs]: + """Return a list of supported codecs.""" + return [AudioCodecs.PCM, AudioCodecs.OPUS] + + @property + def supported_bit_rates(self) -> List[AudioBitRates]: + """Return a list of supported bitrates.""" + return [AudioBitRates.BITRATE_16] + + @property + def supported_sample_rates(self) -> List[AudioSampleRates]: + """Return a list of supported samplerates.""" + return [AudioSampleRates.SAMPLERATE_16000] + + @property + def supported_channels(self) -> List[AudioChannels]: + """Return a list of supported channels.""" + return [AudioChannels.CHANNEL_MONO] + + async def async_process_audio_stream( + self, metadata: SpeechMetadata, stream: StreamReader + ) -> SpeechResult: + """Process an audio stream to STT service.""" + content = f"audio/{metadata.format!s}; codecs=audio/{metadata.codec!s}; samplerate=16000" + + # Process STT + try: + result = await self.cloud.voice.process_stt( + stream, content, metadata.language + ) + except VoiceError: + return SpeechResult(None, SpeechResultState.ERROR) + + # Return Speech as Text + return SpeechResult( + result.text, + SpeechResultState.SUCCESS if result.success else SpeechResultState.ERROR, + ) diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py new file mode 100644 index 00000000000000..338b97d2bd9427 --- /dev/null +++ b/homeassistant/components/cloud/tts.py @@ -0,0 +1,81 @@ +"""Support for the cloud for text to speech service.""" + +from hass_nabucasa.voice import VoiceError +from hass_nabucasa import Cloud +import voluptuous as vol + +from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider + +from .const import DOMAIN + +CONF_GENDER = "gender" + +SUPPORT_LANGUAGES = ["en-US", "de-DE", "es-ES"] +SUPPORT_GENDER = ["male", "female"] + +DEFAULT_LANG = "en-US" +DEFAULT_GENDER = "female" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), + vol.Optional(CONF_GENDER, default=DEFAULT_GENDER): vol.In(SUPPORT_GENDER), + } +) + + +async def async_get_engine(hass, config, discovery_info=None): + """Set up Cloud speech component.""" + cloud: Cloud = hass.data[DOMAIN] + + if discovery_info is not None: + language = DEFAULT_LANG + gender = DEFAULT_GENDER + else: + language = config[CONF_LANG] + gender = config[CONF_GENDER] + + return CloudProvider(cloud, language, gender) + + +class CloudProvider(Provider): + """NabuCasa Cloud speech API provider.""" + + def __init__(self, cloud: Cloud, language: str, gender: str): + """Initialize cloud provider.""" + self.cloud = cloud + self.name = "Cloud" + self._language = language + self._gender = gender + + @property + def default_language(self): + """Return the default language.""" + return self._language + + @property + def supported_languages(self): + """Return list of supported languages.""" + return SUPPORT_LANGUAGES + + @property + def supported_options(self): + """Return list of supported options like voice, emotion.""" + return [CONF_GENDER] + + @property + def default_options(self): + """Return a dict include default options.""" + return {CONF_GENDER: self._gender} + + async def async_get_tts_audio(self, message, language, options=None): + """Load TTS from NabuCasa Cloud.""" + # Process TTS + try: + data = await self.cloud.voice.process_tts( + message, language, gender=options[CONF_GENDER] + ) + except VoiceError: + return (None, None) + + return ("mp3", data) diff --git a/homeassistant/components/demo/stt.py b/homeassistant/components/demo/stt.py index fdd89dc4c9c618..e0367fad6a920c 100644 --- a/homeassistant/components/demo/stt.py +++ b/homeassistant/components/demo/stt.py @@ -6,16 +6,17 @@ from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult from homeassistant.components.stt.const import ( AudioBitRates, + AudioChannels, + AudioCodecs, AudioFormats, AudioSampleRates, - AudioCodecs, SpeechResultState, ) SUPPORT_LANGUAGES = ["en", "de"] -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up Demo speech component.""" return DemoProvider() @@ -48,6 +49,11 @@ def supported_sample_rates(self) -> List[AudioSampleRates]: """Return a list of supported sample rates.""" return [AudioSampleRates.SAMPLERATE_16000, AudioSampleRates.SAMPLERATE_44100] + @property + def supported_channels(self) -> List[AudioChannels]: + """Return a list of supported channels.""" + return [AudioChannels.CHANNEL_STEREO] + async def async_process_audio_stream( self, metadata: SpeechMetadata, stream: StreamReader ) -> SpeechResult: diff --git a/homeassistant/components/demo/tts.py b/homeassistant/components/demo/tts.py index 441b0cc0b3c633..27c9015533edd4 100644 --- a/homeassistant/components/demo/tts.py +++ b/homeassistant/components/demo/tts.py @@ -14,7 +14,7 @@ ) -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up Demo speech component.""" return DemoProvider(config[CONF_LANG]) diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index cf064b3bfa7c2a..942ee0a4e483d1 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -122,7 +122,7 @@ ) -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up Google Cloud TTS component.""" key_file = config.get(CONF_KEY_FILE) if key_file: diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index cdf6dbd402e329..3add45b8cb8e68 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -81,7 +81,7 @@ ) -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up Google speech component.""" return GoogleProvider(hass, config[CONF_LANG]) diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index e5088c5b2df3c3..742b5e87661798 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -38,7 +38,7 @@ ) -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up MaryTTS speech component.""" return MaryTTSProvider(hass, config) diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index d214f6648ddd4b..447d2a4d46a3cc 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -94,7 +94,7 @@ ) -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up Microsoft speech component.""" return MicrosoftProvider( config[CONF_API_KEY], diff --git a/homeassistant/components/picotts/tts.py b/homeassistant/components/picotts/tts.py index d4fe6bc779b1b0..f9c93edb4fc032 100644 --- a/homeassistant/components/picotts/tts.py +++ b/homeassistant/components/picotts/tts.py @@ -20,7 +20,7 @@ ) -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up Pico speech component.""" if shutil.which("pico2wave") is None: _LOGGER.error("'pico2wave' was not found") diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index b781c4666ae54b..b39ab88484bba0 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -7,21 +7,22 @@ from aiohttp import StreamReader, web from aiohttp.hdrs import istr from aiohttp.web_exceptions import ( + HTTPBadRequest, HTTPNotFound, HTTPUnsupportedMediaType, - HTTPBadRequest, ) import attr from homeassistant.components.http import HomeAssistantView from homeassistant.core import callback -from homeassistant.helpers import config_per_platform +from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_prepare_setup_platform from .const import ( DOMAIN, AudioBitRates, + AudioChannels, AudioCodecs, AudioFormats, AudioSampleRates, @@ -37,14 +38,17 @@ async def async_setup(hass: HomeAssistantType, config): """Set up STT.""" providers = {} - async def async_setup_platform(p_type, p_config, disc_info=None): + async def async_setup_platform(p_type, p_config=None, discovery_info=None): """Set up a TTS platform.""" + if p_config is None: + p_config = {} + platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) if platform is None: return try: - provider = await platform.async_get_engine(hass, p_config) + provider = await platform.async_get_engine(hass, p_config, discovery_info) if provider is None: _LOGGER.error("Error setting up platform %s", p_type) return @@ -65,6 +69,13 @@ async def async_setup_platform(p_type, p_config, disc_info=None): if setup_tasks: await asyncio.wait(setup_tasks) + # Add discovery support + async def async_platform_discovered(platform, info): + """Handle for discovered platform.""" + await async_setup_platform(platform, discovery_info=info) + + discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) + hass.http.register_view(SpeechToTextView(providers)) return True @@ -78,13 +89,14 @@ class SpeechMetadata: codec: AudioCodecs = attr.ib() bit_rate: AudioBitRates = attr.ib(converter=int) sample_rate: AudioSampleRates = attr.ib(converter=int) + channel: AudioChannels = attr.ib(converter=int) @attr.s class SpeechResult: """Result of audio Speech.""" - text: str = attr.ib() + text: Optional[str] = attr.ib() result: SpeechResultState = attr.ib() @@ -112,12 +124,17 @@ def supported_codecs(self) -> List[AudioCodecs]: @property @abstractmethod def supported_bit_rates(self) -> List[AudioBitRates]: - """Return a list of supported bit_rates.""" + """Return a list of supported bit rates.""" @property @abstractmethod def supported_sample_rates(self) -> List[AudioSampleRates]: - """Return a list of supported sample_rates.""" + """Return a list of supported sample rates.""" + + @property + @abstractmethod + def supported_channels(self) -> List[AudioChannels]: + """Return a list of supported channels.""" @abstractmethod async def async_process_audio_stream( @@ -137,6 +154,7 @@ def check_metadata(self, metadata: SpeechMetadata) -> bool: or metadata.codec not in self.supported_codecs or metadata.bit_rate not in self.supported_bit_rates or metadata.sample_rate not in self.supported_sample_rates + or metadata.channel not in self.supported_channels ): return False return True @@ -157,7 +175,7 @@ def __init__(self, providers: Dict[str, Provider]) -> None: def _metadata_from_header(request: web.Request) -> Optional[SpeechMetadata]: """Extract metadata from header. - X-Speech-Content: format=wav; codec=pcm; samplerate=16000; bitrate=16; language=de_de + X-Speech-Content: format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de """ try: data = request.headers[istr("X-Speech-Content")].split(";") @@ -213,5 +231,6 @@ async def get(self, request: web.Request, provider: str) -> web.Response: "codecs": stt_provider.supported_codecs, "sample_rates": stt_provider.supported_sample_rates, "bit_rates": stt_provider.supported_bit_rates, + "channels": stt_provider.supported_channels, } ) diff --git a/homeassistant/components/stt/const.py b/homeassistant/components/stt/const.py index c653bcc3bd5953..c111aed82a4d49 100644 --- a/homeassistant/components/stt/const.py +++ b/homeassistant/components/stt/const.py @@ -19,7 +19,7 @@ class AudioFormats(str, Enum): class AudioBitRates(int, Enum): - """Supported Audio bit_rates.""" + """Supported Audio bit rates.""" BITRATE_8 = 8 BITRATE_16 = 16 @@ -28,7 +28,7 @@ class AudioBitRates(int, Enum): class AudioSampleRates(int, Enum): - """Supported Audio sample_rates.""" + """Supported Audio sample rates.""" SAMPLERATE_8000 = 8000 SAMPLERATE_11000 = 11000 @@ -41,6 +41,13 @@ class AudioSampleRates(int, Enum): SAMPLERATE_48000 = 48000 +class AudioChannels(int, Enum): + """Supported Audio channel.""" + + CHANNEL_MONO = 1 + CHANNEL_STEREO = 2 + + class SpeechResultState(str, Enum): """Result state of speech.""" diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 2ce0e18bee5355..d17f64a3a3a4f0 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -25,7 +25,7 @@ from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform +from homeassistant.helpers import config_per_platform, discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_prepare_setup_platform @@ -118,17 +118,24 @@ async def async_setup(hass, config): hass.http.register_view(TextToSpeechView(tts)) hass.http.register_view(TextToSpeechUrlView(tts)) - async def async_setup_platform(p_type, p_config, disc_info=None): + async def async_setup_platform(p_type, p_config=None, discovery_info=None): """Set up a TTS platform.""" + if p_config is None: + p_config = {} + platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type) if platform is None: return try: if hasattr(platform, "async_get_engine"): - provider = await platform.async_get_engine(hass, p_config) + provider = await platform.async_get_engine( + hass, p_config, discovery_info + ) else: - provider = await hass.async_add_job(platform.get_engine, hass, p_config) + provider = await hass.async_add_job( + platform.get_engine, hass, p_config, discovery_info + ) if provider is None: _LOGGER.error("Error setting up platform %s", p_type) @@ -178,6 +185,12 @@ async def async_say_handle(service): if setup_tasks: await asyncio.wait(setup_tasks) + async def async_platform_discovered(platform, info): + """Handle for discovered platform.""" + await async_setup_platform(platform, discovery_info=info) + + discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered) + async def async_clear_cache_handle(service): """Handle clear cache service call.""" await tts.async_clear_cache() diff --git a/homeassistant/components/voicerss/tts.py b/homeassistant/components/voicerss/tts.py index 3eb7baef8eba41..9f87dabf94fe98 100644 --- a/homeassistant/components/voicerss/tts.py +++ b/homeassistant/components/voicerss/tts.py @@ -131,7 +131,7 @@ ) -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up VoiceRSS TTS component.""" return VoiceRSSProvider(hass, config) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 021767e3d11f49..40ebd768a3106c 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -90,7 +90,7 @@ ) -def get_engine(hass, config): +def get_engine(hass, config, discovery_info=None): """Set up IBM Watson TTS component.""" from ibm_watson import TextToSpeechV1 from ibm_cloud_sdk_core.authenticators import IAMAuthenticator diff --git a/homeassistant/components/yandextts/tts.py b/homeassistant/components/yandextts/tts.py index 8541e6560abd9e..b06df4b7a425fc 100644 --- a/homeassistant/components/yandextts/tts.py +++ b/homeassistant/components/yandextts/tts.py @@ -79,7 +79,7 @@ SUPPORTED_OPTIONS = [CONF_CODEC, CONF_VOICE, CONF_EMOTION, CONF_SPEED] -async def async_get_engine(hass, config): +async def async_get_engine(hass, config, discovery_info=None): """Set up VoiceRSS speech component.""" return YandexSpeechKitProvider(hass, config) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7a7d83319520d3..775545c4ff10b1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 -hass-nabucasa==0.23 +hass-nabucasa==0.26 home-assistant-frontend==20191025.1 importlib-metadata==0.23 jinja2>=2.10.3 diff --git a/pylintrc b/pylintrc index 4aced384b63023..ff47af6087b8c0 100644 --- a/pylintrc +++ b/pylintrc @@ -44,6 +44,7 @@ disable= too-many-public-methods, too-many-return-statements, too-many-statements, + too-many-boolean-expressions, unnecessary-pass, unused-argument diff --git a/requirements_all.txt b/requirements_all.txt index 99c562870bb0fb..6250c09c9219b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -622,7 +622,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.23 +hass-nabucasa==0.26 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a9b30d3e4f6b7..f4db717a631b16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -204,7 +204,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.23 +hass-nabucasa==0.26 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py index 782d89688c7089..5933b976460e94 100644 --- a/tests/components/demo/test_stt.py +++ b/tests/components/demo/test_stt.py @@ -27,6 +27,7 @@ async def test_demo_settings(hass_client): "sample_rates": [16000, 44100], "formats": ["wav"], "codecs": ["pcm"], + "channels": [2], } @@ -45,7 +46,7 @@ async def test_demo_speech_wrong_metadata(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; language=de" + "X-Speech-Content": "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; channel=1; language=de" }, data=b"Test", ) @@ -59,7 +60,7 @@ async def test_demo_speech(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; language=de" + "X-Speech-Content": "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=2; language=de" }, data=b"Test", ) From e69cd271ddbc4ecc03efc87c8ed6dddeb065bb1c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 5 Nov 2019 14:40:12 -0700 Subject: [PATCH 1457/3953] Bump pytile and re-order imports (#28570) --- homeassistant/components/tile/device_tracker.py | 10 ++-------- homeassistant/components/tile/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 924fa913d30993..1cb88f67c2fc5e 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -2,6 +2,8 @@ import logging from datetime import timedelta +from pytile import async_login +from pytile.errors import SessionExpiredError, TileError import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA @@ -43,8 +45,6 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Tile scanner.""" - from pytile import async_login - websession = aiohttp_client.async_get_clientsession(hass) config_file = hass.config.path( @@ -89,8 +89,6 @@ def __init__(self, client, hass, async_see, types, show_inactive): async def async_init(self): """Further initialize connection to the Tile servers.""" - from pytile.errors import TileError - try: await self._client.async_init() except TileError as err: @@ -105,10 +103,6 @@ async def async_init(self): async def _async_update(self, now=None): """Update info from Tile.""" - from pytile.errors import SessionExpiredError, TileError - - _LOGGER.debug("Updating Tile data") - try: await self._client.async_init() tiles = await self._client.tiles.all( diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 0dd0b70ef520c5..801e09fd954826 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "documentation": "https://www.home-assistant.io/integrations/tile", "requirements": [ - "pytile==3.0.0" + "pytile==3.0.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 6250c09c9219b3..e36c92471c39c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1609,7 +1609,7 @@ python_opendata_transport==0.1.4 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==3.0.0 +pytile==3.0.1 # homeassistant.components.touchline pytouchline==0.7 From 8ab04d5fc75836e12b0bbcedf2a6a68d01c4c97d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 6 Nov 2019 00:31:46 +0000 Subject: [PATCH 1458/3953] [ci skip] Translation update --- homeassistant/components/auth/.translations/ko.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 6c2e8988d83c58..be160b185ac810 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \uad6c\uc131\ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, From a63e9764961df9b15e07fd8c7e1e10e5c7fc0f99 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Nov 2019 20:33:11 -0800 Subject: [PATCH 1459/3953] Fix invalid JSON in deconz strings.json --- homeassistant/components/deconz/strings.json | 189 ++++++++++--------- 1 file changed, 95 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 56186feb8b1889..a00e10f3e7e654 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -1,101 +1,102 @@ { - "config": { - "title": "deCONZ Zigbee gateway", - "flow_title": "deCONZ Zigbee gateway ({host})", - "step": { - "init": { - "title": "Define deCONZ gateway", - "data": { - "host": "Host", - "port": "Port" - } - }, - "link": { - "title": "Link with deCONZ", - "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button" - }, - "options": { - "title": "Extra configuration options for deCONZ", - "data":{ - "allow_clip_sensor": "Allow importing virtual sensors", - "allow_deconz_groups": "Allow importing deCONZ groups" - } - }, - "hassio_confirm": { - "title": "deCONZ Zigbee gateway via Hass.io add-on", - "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the hass.io add-on {addon}?", - "data": { - "allow_clip_sensor": "Allow importing virtual sensors", - "allow_deconz_groups": "Allow importing deCONZ groups" - } - } - }, - "error": { - "no_key": "Couldn't get an API key" - }, - "abort": { - "already_configured": "Bridge is already configured", - "already_in_progress": "Config flow for bridge is already in progress.", - "no_bridges": "No deCONZ bridges discovered", - "not_deconz_bridge": "Not a deCONZ bridge", - "one_instance_only": "Component only supports one deCONZ instance", - "updated_instance": "Updated deCONZ instance with new host address" + "config": { + "title": "deCONZ Zigbee gateway", + "flow_title": "deCONZ Zigbee gateway ({host})", + "step": { + "init": { + "title": "Define deCONZ gateway", + "data": { + "host": "Host", + "port": "Port" } + }, + "link": { + "title": "Link with deCONZ", + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button" + }, + "options": { + "title": "Extra configuration options for deCONZ", + "data": { + "allow_clip_sensor": "Allow importing virtual sensors", + "allow_deconz_groups": "Allow importing deCONZ groups" + } + }, + "hassio_confirm": { + "title": "deCONZ Zigbee gateway via Hass.io add-on", + "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the hass.io add-on {addon}?", + "data": { + "allow_clip_sensor": "Allow importing virtual sensors", + "allow_deconz_groups": "Allow importing deCONZ groups" + } + } + }, + "error": { + "no_key": "Couldn't get an API key" }, - "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" - } - } + "abort": { + "already_configured": "Bridge is already configured", + "already_in_progress": "Config flow for bridge is already in progress.", + "no_bridges": "No deCONZ bridges discovered", + "not_deconz_bridge": "Not a deCONZ bridge", + "one_instance_only": "Component only supports one deCONZ instance", + "updated_instance": "Updated deCONZ instance with new host address" + } + }, + "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" } + } + } + }, + "device_automation": { + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", + "remote_falling": "Device in free fall", + "remote_awakened": "Device awakened", + "remote_moved": "Device moved with \"{subtype}\" up", + "remote_double_tap": "Device \"{subtype}\" double tapped", + "remote_gyro_activated": "Device shaken", + "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", + "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", + "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", + "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", + "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", + "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" }, - "device_automation": { - "trigger_type": { - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "remote_button_rotated": "Button rotated \"{subtype}\"", - "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", - "remote_falling": "Device in free fall", - "remote_awakened": "Device awakened", - "remote_moved": "Device moved with \"{subtype}\" up", - "remote_double_tap": "Device \"{subtype}\" double tapped", - "remote_gyro_activated": "Device shaken", - "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", - "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", - "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", - "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", - "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", - "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" - }, - "trigger_subtype": { - "turn_on": "Turn on", - "turn_off": "Turn off", - "dim_up": "Dim up", - "dim_down": "Dim down", - "left": "Left", - "right": "Right", - "open": "Open", - "close": "Close", - "both_buttons": "Both buttons", - "button_1": "First button", - "button_2": "Second button", - "button_3": "Third button", - "button_4": "Fourth button", - "side_1": "Side 1", - "side_2": "Side 2", - "side_3": "Side 3", - "side_4": "Side 4", - "side_5": "Side 5", - "side_6": "Side 6" + "trigger_subtype": { + "turn_on": "Turn on", + "turn_off": "Turn off", + "dim_up": "Dim up", + "dim_down": "Dim down", + "left": "Left", + "right": "Right", + "open": "Open", + "close": "Close", + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6" } + } } From f8712b4d7f71204f6bb8bf50bee5788ee6906fb4 Mon Sep 17 00:00:00 2001 From: Grodesh <4070488+Grodesh@users.noreply.github.com> Date: Wed, 6 Nov 2019 01:24:11 -0500 Subject: [PATCH 1460/3953] Update nextbus stop tag to accept strings (#27765) --- homeassistant/components/nextbus/sensor.py | 2 +- tests/components/nextbus/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 661eb75b732663..7622bd133f0820 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -173,7 +173,7 @@ def update(self): """Update sensor with new departures times.""" # Note: using Multi because there is a bug with the single stop impl results = self._client.get_predictions_for_multi_stops( - [{"stop_tag": int(self.stop), "route_tag": self.route}], self.agency + [{"stop_tag": self.stop, "route_tag": self.route}], self.agency ) self._log_debug("Predictions results: %s", results) diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 2aaf622ffc915f..d7c1919dff0664 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -113,7 +113,7 @@ async def test_verify_valid_state( """Verify all attributes are set from a valid response.""" await assert_setup_sensor(hass, CONFIG_BASIC) mock_nextbus_predictions.assert_called_once_with( - [{"stop_tag": int(VALID_STOP), "route_tag": VALID_ROUTE}], VALID_AGENCY + [{"stop_tag": VALID_STOP, "route_tag": VALID_ROUTE}], VALID_AGENCY ) state = hass.states.get(SENSOR_ID_SHORT) From 438ee991751347262357a47ce5e94ed5bc988695 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Tue, 5 Nov 2019 23:02:07 -0800 Subject: [PATCH 1461/3953] Bump adb-shell to 0.0.8 (#28582) * Bump 'adb-shell' to 0.0.8 * Update requirements_test_all.txt * Update manifest.json --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_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 9ec993b9f914e9..c734b525e550a2 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/integrations/androidtv", "requirements": [ - "adb-shell==0.0.7", + "adb-shell==0.0.8", "androidtv==0.0.32" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index e36c92471c39c3..86400df014ce68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,7 +115,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.7 +adb-shell==0.0.8 # homeassistant.components.adguard adguardhome==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4db717a631b16..50f4ae5f2a0b2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -29,7 +29,7 @@ YesssSMS==0.4.1 abodepy==0.16.6 # homeassistant.components.androidtv -adb-shell==0.0.7 +adb-shell==0.0.8 # homeassistant.components.adguard adguardhome==0.3.0 From e99bb8f75e02ca49d067220e02151c327571c788 Mon Sep 17 00:00:00 2001 From: Zach Date: Wed, 6 Nov 2019 02:45:16 -0500 Subject: [PATCH 1462/3953] Fix Doods error when detection labels are specified in list form (#28574) --- homeassistant/components/doods/image_processing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 02d7ce26f1ce18..7f2c3fd1d4276d 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -177,6 +177,7 @@ def __init__(self, hass, camera_entity, name, doods, detector, config): _LOGGER.warning("Detector does not support label %s", label) continue self._label_areas[label] = [0, 0, 1, 1] + self._label_covers[label] = True if label not in dconfig or dconfig[label] > confidence: dconfig[label] = confidence From ac4d8ee07f8164571b5223c90207e84b5a75efbd Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Wed, 6 Nov 2019 09:51:20 +0100 Subject: [PATCH 1463/3953] Upgrade youtube_dl to 2019.11.05 (#28578) --- 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 bb990fc28e7ba4..f413ffd16dbc38 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/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.10.29" + "youtube_dl==2019.11.05" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 86400df014ce68..ead7762d60c1aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2036,7 +2036,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.10.29 +youtube_dl==2019.11.05 # homeassistant.components.zengge zengge==0.2 From 2e1d05560f4fb7d2b4b03dd9208a97e8e6999448 Mon Sep 17 00:00:00 2001 From: temeteke Date: Wed, 6 Nov 2019 21:47:34 +0900 Subject: [PATCH 1464/3953] Reset states when connection to MPC-HC is lost (#27541) * Reset states when connection to MPC-HC is lost * Add the available property of mpchc --- homeassistant/components/mpchc/media_player.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py index ae96704be589e9..580156a565328c 100644 --- a/homeassistant/components/mpchc/media_player.py +++ b/homeassistant/components/mpchc/media_player.py @@ -69,6 +69,7 @@ def __init__(self, name, url): self._name = name self._url = url self._player_variables = dict() + self._available = False def update(self): """Get the latest details.""" @@ -79,8 +80,11 @@ def update(self): for var in mpchc_variables: self._player_variables[var[0]] = var[1].lower() + self._available = True except requests.exceptions.RequestException: _LOGGER.error("Could not connect to MPC-HC at: %s", self._url) + self._player_variables = dict() + self._available = False def _send_command(self, command_id): """Send a command to MPC-HC via its window message ID.""" @@ -111,6 +115,11 @@ def state(self): return STATE_IDLE + @property + def available(self): + """Return True if entity is available.""" + return self._available + @property def media_title(self): """Return the title of current playing media.""" From b7153ca2073d9ae7d73bf9ca642a850c277cca03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kevin=20K=C3=B6ck?= Date: Wed, 6 Nov 2019 13:50:54 +0100 Subject: [PATCH 1465/3953] Add mqtt temp_low/high_template in SCHEMA_BASE (#28257) * fix missing temp_low/high_template in SCHEMA_BASE * temperature_high/low_state_template test * Update test_climate.py * paste error * Update test_climate.py * Update test_climate.py * Update test_climate.py * Update test_climate.py --- homeassistant/components/mqtt/climate.py | 2 ++ tests/components/mqtt/test_climate.py | 33 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 5c1c9286c6edd3..4b163c523fa6d6 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -214,7 +214,9 @@ vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 064b574347886c..3f4fc657186483 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -568,6 +568,39 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert state.state == STATE_UNAVAILABLE +async def test_set_target_temperature_low_high_with_templates(hass, mqtt_mock, caplog): + """Test setting of temperature high/low templates.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["climate"]["temperature_low_state_topic"] = "temperature-state" + config["climate"]["temperature_high_state_topic"] = "temperature-state" + config["climate"]["temperature_low_state_template"] = "{{ value_json.temp_low }}" + config["climate"]["temperature_high_state_template"] = "{{ value_json.temp_high }}" + + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + + # Temperature - with valid value + assert state.attributes.get("target_temp_low") is None + assert state.attributes.get("target_temp_high") is None + + async_fire_mqtt_message( + hass, "temperature-state", '{"temp_low": "1031", "temp_high": "1032"}' + ) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 1031 + assert state.attributes.get("target_temp_high") == 1032 + + # Temperature - with invalid value + async_fire_mqtt_message(hass, "temperature-state", '"-INVALID-"') + state = hass.states.get(ENTITY_CLIMATE) + # make sure, the invalid value gets logged... + assert "Could not parse temperature from" in caplog.text + # ... but the actual value stays unchanged. + assert state.attributes.get("target_temp_low") == 1031 + assert state.attributes.get("target_temp_high") == 1032 + + async def test_set_with_templates(hass, mqtt_mock, caplog): """Test setting of new fan mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) From 9ba3abd1b73f80b17d7e1be552d91d5b0b67e124 Mon Sep 17 00:00:00 2001 From: ssenart <37013755+ssenart@users.noreply.github.com> Date: Wed, 6 Nov 2019 13:52:59 +0100 Subject: [PATCH 1466/3953] Add Netatmo camera services (#27970) * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Add Presence Netatmo Camera services (set_light_auto, set_light_on, set_light_off) to control its internal flood light status. * Add Presence Netatmo Camera services (set_light_auto, set_light_on, set_light_off) to control its internal flood light status. * Netatmo camera : Use new style string formatting. * Make the file compliant with flake8 linter. * Make the file compliant with flake8 linter. * Make it compliant with black formatter. * Make it compliant with black formatter. * Bug fix : Flood light control was not working with VPN url. --- homeassistant/components/netatmo/camera.py | 307 +++++++++++++++++- .../components/netatmo/services.yaml | 24 +- 2 files changed, 312 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index ecc38add3b41cb..f3bf6a6784c87d 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -5,11 +5,20 @@ import requests import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_STREAM -from homeassistant.const import CONF_VERIFY_SSL +from homeassistant.components.camera import ( + PLATFORM_SCHEMA, + Camera, + SUPPORT_STREAM, + CAMERA_SERVICE_SCHEMA, +) +from homeassistant.const import CONF_VERIFY_SSL, STATE_ON, STATE_OFF from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, + async_dispatcher_connect, +) -from .const import DATA_NETATMO_AUTH +from .const import DATA_NETATMO_AUTH, DOMAIN from . import CameraData _LOGGER = logging.getLogger(__name__) @@ -33,6 +42,8 @@ } ) +_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up access to Netatmo cameras.""" @@ -63,6 +74,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): except NoDevice: return None + async def async_service_handler(call): + """Handle service call.""" + _LOGGER.debug( + "Service handler invoked with service=%s and data=%s", + call.service, + call.data, + ) + service = call.service + entity_id = call.data["entity_id"][0] + async_dispatcher_send(hass, f"{service}_{entity_id}") + + hass.services.async_register( + DOMAIN, "set_light_auto", async_service_handler, CAMERA_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, "set_light_on", async_service_handler, CAMERA_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, "set_light_off", async_service_handler, CAMERA_SERVICE_SCHEMA + ) + class NetatmoCamera(Camera): """Representation of the images published from a Netatmo camera.""" @@ -72,16 +104,39 @@ def __init__(self, data, camera_name, home, camera_type, verify_ssl, quality): super().__init__() self._data = data self._camera_name = camera_name - self._verify_ssl = verify_ssl - self._quality = quality + self._home = home if home: self._name = home + " / " + camera_name else: self._name = camera_name - self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( - camera=camera_name - ) self._cameratype = camera_type + self._verify_ssl = verify_ssl + self._quality = quality + + # URLs. + self._vpnurl = None + self._localurl = None + + # Identifier + self._id = None + + # Monitoring status. + self._status = None + + # SD Card status + self._sd_status = None + + # Power status + self._alim_status = None + + # Is local + self._is_local = None + + # VPN URL + self._vpn_url = None + + # Light mode status + self._light_mode_status = None def camera_image(self): """Return a still image response from the camera.""" @@ -112,33 +167,249 @@ def camera_image(self): return None return response.content + # Entity property overrides + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return True + @property def name(self): """Return the name of this Netatmo camera device.""" return self._name @property - def brand(self): - """Return the camera brand.""" - return "Netatmo" + def device_state_attributes(self): + """Return the Netatmo-specific camera state attributes.""" + + _LOGGER.debug("Getting new attributes from camera netatmo '%s'", self._name) + + attr = {} + attr["id"] = self._id + attr["status"] = self._status + attr["sd_status"] = self._sd_status + attr["alim_status"] = self._alim_status + attr["is_local"] = self._is_local + attr["vpn_url"] = self._vpn_url + + if self.model == "Presence": + attr["light_mode_status"] = self._light_mode_status + + _LOGGER.debug("Attributes of '%s' = %s", self._name, attr) + + return attr @property - def model(self): - """Return the camera model.""" - if self._cameratype == "NOC": - return "Presence" - if self._cameratype == "NACamera": - return "Welcome" - return None + def available(self): + """Return True if entity is available.""" + return bool(self._alim_status == "on") @property def supported_features(self): """Return supported features.""" return SUPPORT_STREAM + @property + def is_recording(self): + """Return true if the device is recording.""" + return bool(self._status == "on") + + @property + def brand(self): + """Return the camera brand.""" + return "Netatmo" + + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return bool(self._status == "on") + + @property + def is_on(self): + """Return true if on.""" + return self.is_streaming + async def stream_source(self): """Return the stream source.""" url = "{0}/live/files/{1}/index.m3u8" if self._localurl: return url.format(self._localurl, self._quality) return url.format(self._vpnurl, self._quality) + + @property + def model(self): + """Return the camera model.""" + if self._cameratype == "NOC": + return "Presence" + if self._cameratype == "NACamera": + return "Welcome" + return None + + # Other Entity method overrides + + async def async_added_to_hass(self): + """Subscribe to signals and add camera to list.""" + _LOGGER.debug("Registering services for entity_id=%s", self.entity_id) + async_dispatcher_connect( + self.hass, f"set_light_auto_{self.entity_id}", self.set_light_auto + ) + async_dispatcher_connect( + self.hass, f"set_light_on_{self.entity_id}", self.set_light_on + ) + async_dispatcher_connect( + self.hass, f"set_light_off_{self.entity_id}", self.set_light_off + ) + + def update(self): + """Update entity status.""" + + _LOGGER.debug("Updating camera netatmo '%s'", self._name) + + # Refresh camera data. + self._data.update() + + # URLs. + self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + + # Identifier + self._id = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["id"] + + # Monitoring status. + self._status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["status"] + + _LOGGER.debug("Status of '%s' = %s", self._name, self._status) + + # SD Card status + self._sd_status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["sd_status"] + + # Power status + self._alim_status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["alim_status"] + + # Is local + self._is_local = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["is_local"] + + # VPN URL + self._vpn_url = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["vpn_url"] + + self.is_streaming = self._alim_status == "on" + + if self.model == "Presence": + # Light mode status + self._light_mode_status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["light_mode_status"] + + # Camera method overrides + + def enable_motion_detection(self): + """Enable motion detection in the camera.""" + _LOGGER.debug("Enable motion detection of the camera '%s'", self._name) + self._enable_motion_detection(True) + + def disable_motion_detection(self): + """Disable motion detection in camera.""" + _LOGGER.debug("Disable motion detection of the camera '%s'", self._name) + self._enable_motion_detection(False) + + def _enable_motion_detection(self, enable): + """Enable or disable motion detection.""" + try: + if self._localurl: + requests.get( + f"{self._localurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}", + timeout=10, + ) + elif self._vpnurl: + requests.get( + f"{self._vpnurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}", + timeout=10, + verify=self._verify_ssl, + ) + else: + _LOGGER.error("Welcome/Presence VPN URL is None") + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + except requests.exceptions.RequestException as error: + _LOGGER.error("Welcome/Presence URL changed: %s", error) + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + else: + self.async_schedule_update_ha_state(True) + + # Netatmo Presence specific camera method. + + def set_light_auto(self): + """Set flood light in automatic mode.""" + _LOGGER.debug( + "Set the flood light in automatic mode for the camera '%s'", self._name + ) + self._set_light_mode("auto") + + def set_light_on(self): + """Set flood light on.""" + _LOGGER.debug("Set the flood light on for the camera '%s'", self._name) + self._set_light_mode("on") + + def set_light_off(self): + """Set flood light off.""" + _LOGGER.debug("Set the flood light off for the camera '%s'", self._name) + self._set_light_mode("off") + + def _set_light_mode(self, mode): + """Set light mode ('auto', 'on', 'off').""" + if self.model == "Presence": + try: + config = '{"mode":"' + mode + '"}' + if self._localurl: + requests.get( + f"{self._localurl}/command/floodlight_set_config?config={config}", + timeout=10, + ) + elif self._vpnurl: + requests.get( + f"{self._vpnurl}/command/floodlight_set_config?config={config}", + timeout=10, + verify=self._verify_ssl, + ) + else: + _LOGGER.error("Presence VPN URL is None") + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + except requests.exceptions.RequestException as error: + _LOGGER.error("Presence URL changed: %s", error) + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + else: + self.async_schedule_update_ha_state(True) + else: + _LOGGER.error("Unsupported camera model for light mode") diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 7bb990caf97f39..a928f4765e06ae 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -4,5 +4,27 @@ addwebhook: url: description: URL for which to add the webhook. example: https://yourdomain.com:443/api/webhook/webhook_id + dropwebhook: - description: Drop active webhooks. \ No newline at end of file + description: Drop active webhooks. + +set_light_auto: + description: Set the camera (Presence only) light in automatic mode. + fields: + entity_id: + description: Entity id. + example: 'camera.living_room' + +set_light_on: + description: Set the camera (Netatmo Presence only) light on. + fields: + entity_id: + description: Entity id. + example: 'camera.living_room' + +set_light_off: + description: Set the camera (Netatmo Presence only) light off. + fields: + entity_id: + description: Entity id. + example: 'camera.living_room' From 9a0a889492d06855bdb1f6683aa5c46a10aa4926 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 6 Nov 2019 08:32:33 -0800 Subject: [PATCH 1467/3953] Fix token sent to Almond Web (#28584) --- homeassistant/components/almond/.translations/en.json | 3 ++- homeassistant/components/almond/__init__.py | 6 +++--- homeassistant/components/almond/strings.json | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/almond/.translations/en.json b/homeassistant/components/almond/.translations/en.json index 89134cbb170f26..3ee811b8326413 100644 --- a/homeassistant/components/almond/.translations/en.json +++ b/homeassistant/components/almond/.translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "You can only configure one Almond account.", - "cannot_connect": "Unable to connect to the Almond server." + "cannot_connect": "Unable to connect to the Almond server.", + "missing_configuration": "Please check the documentation on how to set up Almond." }, "title": "Almond" } diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index ebdddecdec3d62..115e0d24de465a 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -111,7 +111,7 @@ async def async_setup_entry(hass, entry): agent = AlmondAgent(api) # Hass.io does its own configuration of Almond. - if entry.data.get("is_hassio"): + if entry.data.get("is_hassio") or entry.data["type"] != TYPE_LOCAL: conversation.async_set_agent(hass, agent) return True @@ -192,10 +192,10 @@ def __init__( async def async_get_access_token(self): """Return a valid access token.""" - if not self._oauth_session.is_valid: + if not self._oauth_session.valid_token: await self._oauth_session.async_ensure_token_valid() - return self._oauth_session.token + return self._oauth_session.token["access_token"] class AlmondAgent(conversation.AbstractConversationAgent): diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json index 9bc4b0e1b93b45..5cfc52044bb04a 100644 --- a/homeassistant/components/almond/strings.json +++ b/homeassistant/components/almond/strings.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "You can only configure one Almond account.", - "cannot_connect": "Unable to connect to the Almond server." + "cannot_connect": "Unable to connect to the Almond server.", + "missing_configuration": "Please check the documentation on how to set up Almond." }, "title": "Almond" } From d9edd425325d39d1a34cfeb56c3c7f46f4d0da85 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 6 Nov 2019 10:55:56 -0800 Subject: [PATCH 1468/3953] Update to latest Somfy changes (#28207) * Update to latest Somfy changes * Update api.py * Update api.py --- homeassistant/components/somfy/__init__.py | 10 ++---- homeassistant/components/somfy/api.py | 37 +++++--------------- homeassistant/components/somfy/cover.py | 8 ++--- homeassistant/components/somfy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 16 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index cd5960bf6b1c70..b767ea834317d9 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -9,6 +9,7 @@ from datetime import timedelta import voluptuous as vol +from requests import HTTPError from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from homeassistant.components.somfy import config_flow @@ -156,13 +157,8 @@ def has_capability(self, capability): @Throttle(SCAN_INTERVAL) async def update_all_devices(hass): """Update all the devices.""" - from requests import HTTPError - from oauthlib.oauth2 import TokenExpiredError - try: data = hass.data[DOMAIN] data[DEVICES] = await hass.async_add_executor_job(data[API].get_devices) - except TokenExpiredError: - _LOGGER.warning("Cannot update devices due to expired token") - except HTTPError: - _LOGGER.warning("Cannot update devices") + except HTTPError as err: + _LOGGER.warning("Cannot update devices: %s", err.response.status_code) diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py index 3e7bcf9deb4cb5..1cfea8ff7d800f 100644 --- a/homeassistant/components/somfy/api.py +++ b/homeassistant/components/somfy/api.py @@ -1,15 +1,14 @@ """API for Somfy bound to HASS OAuth.""" from asyncio import run_coroutine_threadsafe -from functools import partial +from typing import Dict, Union -import requests from pymfy.api import somfy_api from homeassistant import core, config_entries from homeassistant.helpers import config_entry_oauth2_flow -class ConfigEntrySomfyApi(somfy_api.AbstractSomfyApi): +class ConfigEntrySomfyApi(somfy_api.SomfyApi): """Provide a Somfy API tied into an OAuth2 based config entry.""" def __init__( @@ -24,32 +23,12 @@ def __init__( self.session = config_entry_oauth2_flow.OAuth2Session( hass, config_entry, implementation ) + super().__init__(None, None, token=self.session.token) - def get(self, path): - """Fetch a URL from the Somfy API.""" - return run_coroutine_threadsafe( - self._request("get", path), self.hass.loop + def refresh_tokens(self,) -> Dict[str, Union[str, int]]: + """Refresh and return new Somfy tokens using Home Assistant OAuth2 session.""" + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop ).result() - def post(self, path, *, json): - """Post data to the Somfy API.""" - return run_coroutine_threadsafe( - self._request("post", path, json=json), self.hass.loop - ).result() - - async def _request(self, method, path, **kwargs): - """Make a request.""" - await self.session.async_ensure_token_valid() - - return await self.hass.async_add_executor_job( - partial( - requests.request, - method, - f"{self.base_url}{path}", - **kwargs, - headers={ - **kwargs.get("headers", {}), - "authorization": f"Bearer {self.config_entry.data['token']['access_token']}", - }, - ) - ) + return self.session.token diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index 5a6d97dee69f75..d54e7c990018ed 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -4,6 +4,8 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/cover.somfy/ """ +from pymfy.api.devices.category import Category +from pymfy.api.devices.blind import Blind from homeassistant.components.cover import ( CoverDevice, @@ -18,8 +20,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): def get_covers(): """Retrieve covers.""" - from pymfy.api.devices.category import Category - categories = { Category.ROLLER_SHUTTER.value, Category.INTERIOR_BLIND.value, @@ -51,15 +51,11 @@ class SomfyCover(SomfyEntity, CoverDevice): def __init__(self, device, api): """Initialize the Somfy device.""" - from pymfy.api.devices.blind import Blind - super().__init__(device, api) self.cover = Blind(self.device, self.api) async def async_update(self): """Update the device with the latest data.""" - from pymfy.api.devices.blind import Blind - await super().async_update() self.cover = Blind(self.device, self.api) diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index a34023f76ff8a2..f5a17275bcb355 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.6.0"] + "requirements": ["pymfy==0.6.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index ead7762d60c1aa..3f5ac68cc8eca2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1313,7 +1313,7 @@ pymailgunner==1.4 pymediaroom==0.6.4 # homeassistant.components.somfy -pymfy==0.6.0 +pymfy==0.6.1 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 50f4ae5f2a0b2f..ecf8d00d6483a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -439,7 +439,7 @@ pylitejet==0.1 pymailgunner==1.4 # homeassistant.components.somfy -pymfy==0.6.0 +pymfy==0.6.1 # homeassistant.components.mochad pymochad==0.2.0 From bb37bc32e3a61fd521b10265292a4848bc83bc3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 6 Nov 2019 21:38:00 +0200 Subject: [PATCH 1469/3953] Always run flake8 through pre-commit, and with doctests (#28490) * Enable flake8 doctests everywhere * Always run flake8 through pre-commit --- .vscode/tasks.json | 2 +- script/lazytox.py | 2 +- script/lint | 2 +- setup.cfg | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 151868a1663981..1a0bfb16a9be40 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -33,7 +33,7 @@ { "label": "Flake8", "type": "shell", - "command": "flake8 homeassistant tests", + "command": "pre-commit run flake8 --all-files", "group": { "kind": "test", "isDefault": true diff --git a/script/lazytox.py b/script/lazytox.py index 026d4639a065c6..ca8a160e7dced2 100755 --- a/script/lazytox.py +++ b/script/lazytox.py @@ -120,7 +120,7 @@ async def pylint(files): async def flake8(files): """Exec flake8.""" - _, log = await async_exec("flake8", "--doctests", *files) + _, log = await async_exec("pre-commit", "run", "flake8", "--files", *files) res = [] for line in log.splitlines(): line = line.split(":") diff --git a/script/lint b/script/lint index 8ba14d8939ef87..e4bf74cf602d46 100755 --- a/script/lint +++ b/script/lint @@ -15,7 +15,7 @@ printf "%s\n" $files echo "================" echo "LINT with flake8" echo "================" -flake8 --doctests $files +pre-commit run flake8 --files $files echo "================" echo "LINT with pylint" echo "================" diff --git a/setup.cfg b/setup.cfg index 6d0e5378b44de9..bb2b1652ffa20c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,7 @@ norecursedirs = .git testing_config [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build +doctests = True # To work with Black max-line-length = 88 # E501: line too long From 3d2ff841d338421ce79d2f38aa41eaaeb7a303cb Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Wed, 6 Nov 2019 22:46:18 +0100 Subject: [PATCH 1470/3953] Handle exceptions from PyViCare library (#28536) * ViCare: Handle exceptions from PyViCare library (#28072) Sometimes Viessmann server failures or other connection problems may lead to exceptions thrown when updating data. This commit handles those exceptions with some error logging and makes sure that the component does not break completely in that case. * Remove unneeded returns * Remove unneeded returns --- homeassistant/components/vicare/climate.py | 109 ++++++++++-------- .../components/vicare/water_heater.py | 27 +++-- 2 files changed, 79 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index fe162c0c837316..7e330383b30de0 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -1,5 +1,7 @@ """Viessmann ViCare climate device.""" import logging +import requests +import simplejson from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -111,55 +113,64 @@ def __init__(self, name, api, heating_type): def update(self): """Let HA know there has been an update from the ViCare API.""" - _room_temperature = self._api.getRoomTemperature() - _supply_temperature = self._api.getSupplyTemperature() - if _room_temperature is not None and _room_temperature != PYVICARE_ERROR: - self._current_temperature = _room_temperature - elif _supply_temperature != PYVICARE_ERROR: - self._current_temperature = _supply_temperature - else: - self._current_temperature = None - self._current_program = self._api.getActiveProgram() - - # The getCurrentDesiredTemperature call can yield 'error' (str) when the system is in standby - desired_temperature = self._api.getCurrentDesiredTemperature() - if desired_temperature == PYVICARE_ERROR: - desired_temperature = None - - self._target_temperature = desired_temperature - - self._current_mode = self._api.getActiveMode() - - # Update the generic device attributes - self._attributes = {} - self._attributes["room_temperature"] = _room_temperature - self._attributes["supply_temperature"] = _supply_temperature - self._attributes["outside_temperature"] = self._api.getOutsideTemperature() - self._attributes["active_vicare_program"] = self._current_program - self._attributes["active_vicare_mode"] = self._current_mode - self._attributes["heating_curve_slope"] = self._api.getHeatingCurveSlope() - self._attributes["heating_curve_shift"] = self._api.getHeatingCurveShift() - self._attributes[ - "month_since_last_service" - ] = self._api.getMonthSinceLastService() - self._attributes["date_last_service"] = self._api.getLastServiceDate() - self._attributes["error_history"] = self._api.getErrorHistory() - self._attributes["active_error"] = self._api.getActiveError() - self._attributes[ - "circulationpump_active" - ] = self._api.getCirculationPumpActive() - - # Update the specific device attributes - if self._heating_type == HeatingType.gas: - self._current_action = self._api.getBurnerActive() - - self._attributes["burner_modulation"] = self._api.getBurnerModulation() - self._attributes["boiler_temperature"] = self._api.getBoilerTemperature() - - elif self._heating_type == HeatingType.heatpump: - self._current_action = self._api.getCompressorActive() - - self._attributes["return_temperature"] = self._api.getReturnTemperature() + try: + _room_temperature = self._api.getRoomTemperature() + _supply_temperature = self._api.getSupplyTemperature() + if _room_temperature is not None and _room_temperature != PYVICARE_ERROR: + self._current_temperature = _room_temperature + elif _supply_temperature != PYVICARE_ERROR: + self._current_temperature = _supply_temperature + else: + self._current_temperature = None + self._current_program = self._api.getActiveProgram() + + # The getCurrentDesiredTemperature call can yield 'error' (str) when the system is in standby + desired_temperature = self._api.getCurrentDesiredTemperature() + if desired_temperature == PYVICARE_ERROR: + desired_temperature = None + + self._target_temperature = desired_temperature + + self._current_mode = self._api.getActiveMode() + + # Update the generic device attributes + self._attributes = {} + self._attributes["room_temperature"] = _room_temperature + self._attributes["supply_temperature"] = _supply_temperature + self._attributes["outside_temperature"] = self._api.getOutsideTemperature() + self._attributes["active_vicare_program"] = self._current_program + self._attributes["active_vicare_mode"] = self._current_mode + self._attributes["heating_curve_slope"] = self._api.getHeatingCurveSlope() + self._attributes["heating_curve_shift"] = self._api.getHeatingCurveShift() + self._attributes[ + "month_since_last_service" + ] = self._api.getMonthSinceLastService() + self._attributes["date_last_service"] = self._api.getLastServiceDate() + self._attributes["error_history"] = self._api.getErrorHistory() + self._attributes["active_error"] = self._api.getActiveError() + self._attributes[ + "circulationpump_active" + ] = self._api.getCirculationPumpActive() + + # Update the specific device attributes + if self._heating_type == HeatingType.gas: + self._current_action = self._api.getBurnerActive() + + self._attributes["burner_modulation"] = self._api.getBurnerModulation() + self._attributes[ + "boiler_temperature" + ] = self._api.getBoilerTemperature() + + elif self._heating_type == HeatingType.heatpump: + self._current_action = self._api.getCompressorActive() + + self._attributes[ + "return_temperature" + ] = self._api.getReturnTemperature() + except requests.exceptions.ConnectionError: + _LOGGER.error("Unable to retrieve data from ViCare server") + except simplejson.errors.JSONDecodeError: + _LOGGER.error("Unable to decode data from ViCare server") @property def supported_features(self): diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 7c4968ad0a4138..1f56c46dc1ce60 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,5 +1,7 @@ """Viessmann ViCare water_heater device.""" import logging +import requests +import simplejson from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, @@ -41,6 +43,8 @@ OPERATION_MODE_ON: VICARE_MODE_DHW, } +PYVICARE_ERROR = "error" + def setup_platform(hass, config, add_entities, discovery_info=None): """Create the ViCare water_heater devices.""" @@ -75,15 +79,22 @@ def __init__(self, name, api, heating_type): def update(self): """Let HA know there has been an update from the ViCare API.""" - current_temperature = self._api.getDomesticHotWaterStorageTemperature() - if current_temperature is not None and current_temperature != "error": - self._current_temperature = current_temperature - else: - self._current_temperature = None - - self._target_temperature = self._api.getDomesticHotWaterConfiguredTemperature() + try: + current_temperature = self._api.getDomesticHotWaterStorageTemperature() + if current_temperature != PYVICARE_ERROR: + self._current_temperature = current_temperature + else: + self._current_temperature = None + + self._target_temperature = ( + self._api.getDomesticHotWaterConfiguredTemperature() + ) - self._current_mode = self._api.getActiveMode() + self._current_mode = self._api.getActiveMode() + except requests.exceptions.ConnectionError: + _LOGGER.error("Unable to retrieve data from ViCare server") + except simplejson.errors.JSONDecodeError: + _LOGGER.error("Unable to decode data from ViCare server") @property def supported_features(self): From 78b83c653a6ffe8ce6cd62ccea067f3c146163e3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Nov 2019 23:55:39 +0100 Subject: [PATCH 1471/3953] Add WLED integration (#28542) * Add WLED integration * Use f-string for uniq id in sensor platform * Typing improvements * Removes sensor & light platform * Remove PARALLEL_UPDATES from integration level * Correct type in code comment 'themselves' * Use async_track_time_interval in async context * Remove stale code * Remove decorator from Flow handler * Remove unused __init__ from config flow * Move show form methods to sync * Only wrap lines that can raise in try except block * Remove domain and platform from uniq id * Wrap light state in bool object in is_on method * Use async_schedule_update_ha_state in async context * Return empty dict in device state attributes instead of None * Remove unneeded setdefault call in setup entry * Cancel update timer on entry unload * Restructure config flow code * Adjust tests for new uniq id * Correct typo AdGuard Home -> WLED in config flow file comment * Convert internal package imports to be relative * Reformat JSON files with Prettier * Improve tests based on review comments * Add test for zeroconf when no data is provided * Cleanup and extended tests --- CODEOWNERS | 1 + .../components/wled/.translations/en.json | 26 +++ homeassistant/components/wled/__init__.py | 182 +++++++++++++++ homeassistant/components/wled/config_flow.py | 123 ++++++++++ homeassistant/components/wled/const.py | 25 ++ homeassistant/components/wled/light.py | 219 ++++++++++++++++++ homeassistant/components/wled/manifest.json | 10 + homeassistant/components/wled/strings.json | 26 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 3 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/wled/__init__.py | 42 ++++ tests/components/wled/test_config_flow.py | 207 +++++++++++++++++ tests/components/wled/test_init.py | 60 +++++ tests/components/wled/test_light.py | 179 ++++++++++++++ tests/fixtures/wled/rgb.json | 210 +++++++++++++++++ tests/fixtures/wled/rgbw.json | 198 ++++++++++++++++ 18 files changed, 1518 insertions(+) create mode 100644 homeassistant/components/wled/.translations/en.json create mode 100644 homeassistant/components/wled/__init__.py create mode 100644 homeassistant/components/wled/config_flow.py create mode 100644 homeassistant/components/wled/const.py create mode 100644 homeassistant/components/wled/light.py create mode 100644 homeassistant/components/wled/manifest.json create mode 100644 homeassistant/components/wled/strings.json create mode 100644 tests/components/wled/__init__.py create mode 100644 tests/components/wled/test_config_flow.py create mode 100644 tests/components/wled/test_init.py create mode 100644 tests/components/wled/test_light.py create mode 100644 tests/fixtures/wled/rgb.json create mode 100644 tests/fixtures/wled/rgbw.json diff --git a/CODEOWNERS b/CODEOWNERS index ceb58f370d4ad8..27e06d874e1e71 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -339,6 +339,7 @@ homeassistant/components/weblink/* @home-assistant/core homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @sqldiablo homeassistant/components/withings/* @vangorra +homeassistant/components/wled/* @frenck homeassistant/components/worldclock/* @fabaff homeassistant/components/wwlln/* @bachya homeassistant/components/xbox_live/* @MartinHjelmare diff --git a/homeassistant/components/wled/.translations/en.json b/homeassistant/components/wled/.translations/en.json new file mode 100644 index 00000000000000..dde66b8e1225f1 --- /dev/null +++ b/homeassistant/components/wled/.translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "WLED", + "flow_title": "WLED: {name}", + "step": { + "user": { + "title": "Link your WLED", + "description": "Set up your WLED to integrate with Home Assistant.", + "data": { + "host": "Host or IP address" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the WLED named `{name}` to Home Assistant?", + "title": "Discovered WLED device" + } + }, + "error": { + "connection_error": "Failed to connect to WLED device." + }, + "abort": { + "already_configured": "This WLED device is already configured.", + "connection_error": "Failed to connect to WLED device." + } + } +} diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py new file mode 100644 index 00000000000000..62f611b18ec1af --- /dev/null +++ b/homeassistant/components/wled/__init__.py @@ -0,0 +1,182 @@ +"""Support for WLED.""" +from datetime import timedelta +import logging +from typing import Any, Dict, Optional, Union + +from wled import WLED, WLEDConnectionError, WLEDError + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_NAME, CONF_HOST +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util + +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SOFTWARE_VERSION, + DATA_WLED_CLIENT, + DATA_WLED_TIMER, + DATA_WLED_UPDATED, + DOMAIN, +) + +SCAN_INTERVAL = timedelta(seconds=5) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the WLED components.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up WLED from a config entry.""" + + # Create WLED instance for this entry + session = async_get_clientsession(hass) + wled = WLED(entry.data[CONF_HOST], loop=hass.loop, session=session) + + # Ensure we can connect and talk to it + try: + await wled.update() + except WLEDConnectionError as exception: + raise ConfigEntryNotReady from exception + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled} + + # Set up all platforms for this device/entry. + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN) + ) + + async def interval_update(now: dt_util.dt.datetime = None) -> None: + """Poll WLED device function, dispatches event after update.""" + try: + await wled.update() + except WLEDError: + _LOGGER.debug("An error occurred while updating WLED", exc_info=True) + + # Even if the update failed, we still send out the event. + # To allow entities to make themselves unavailable. + async_dispatcher_send(hass, DATA_WLED_UPDATED, entry.entry_id) + + # Schedule update interval + hass.data[DOMAIN][entry.entry_id][DATA_WLED_TIMER] = async_track_time_interval( + hass, interval_update, SCAN_INTERVAL + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload WLED config entry.""" + + # Cancel update timer for this entry/device. + cancel_timer = hass.data[DOMAIN][entry.entry_id][DATA_WLED_TIMER] + cancel_timer() + + # Unload entities for this entry/device. + await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) + + # Cleanup + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + + return True + + +class WLEDEntity(Entity): + """Defines a base WLED entity.""" + + def __init__(self, entry_id: str, wled: WLED, name: str, icon: str) -> None: + """Initialize the WLED entity.""" + self._attributes: Dict[str, Union[str, int, float]] = {} + self._available = True + self._entry_id = entry_id + self._icon = icon + self._name = name + self._unsub_dispatcher = None + self.wled = wled + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def should_poll(self) -> bool: + """Return the polling requirement of the entity.""" + return False + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + return self._attributes + + async def async_added_to_hass(self) -> None: + """Connect to dispatcher listening for entity data notifications.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, DATA_WLED_UPDATED, self._schedule_immediate_update + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect from update signal.""" + self._unsub_dispatcher() + + @callback + def _schedule_immediate_update(self, entry_id: str) -> None: + """Schedule an immediate update of the entity.""" + if entry_id == self._entry_id: + self.async_schedule_update_ha_state(True) + + async def async_update(self) -> None: + """Update WLED entity.""" + if self.wled.device is None: + self._available = False + return + + self._available = True + await self._wled_update() + + async def _wled_update(self) -> None: + """Update WLED entity.""" + raise NotImplementedError() + + +class WLEDDeviceEntity(WLEDEntity): + """Defines a WLED device entity.""" + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this WLED device.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self.wled.device.info.mac_address)}, + ATTR_NAME: self.wled.device.info.name, + ATTR_MANUFACTURER: self.wled.device.info.brand, + ATTR_MODEL: self.wled.device.info.product, + ATTR_SOFTWARE_VERSION: self.wled.device.info.version, + } diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py new file mode 100644 index 00000000000000..7be283874e0ff8 --- /dev/null +++ b/homeassistant/components/wled/config_flow.py @@ -0,0 +1,123 @@ +"""Config flow to configure the WLED integration.""" +import logging +from typing import Any, Dict, Optional + +import voluptuous as vol +from wled import WLED, WLEDConnectionError + +from homeassistant.config_entries import ( + CONN_CLASS_LOCAL_POLL, + SOURCE_ZEROCONF, + ConfigFlow, +) +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.helpers import ConfigType +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN # pylint: disable=W0611 + +_LOGGER = logging.getLogger(__name__) + + +class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a WLED config flow.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + async def async_step_user( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by the user.""" + return await self._handle_config_flow(user_input) + + async def async_step_zeroconf( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle zeroconf discovery.""" + if user_input is None: + return self.async_abort(reason="connection_error") + + # Hostname is format: wled-livingroom.local. + host = user_input["hostname"].rstrip(".") + name, _ = host.rsplit(".") + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update( + {CONF_HOST: host, CONF_NAME: name, "title_placeholders": {"name": name}} + ) + + # Prepare configuration flow + return await self._handle_config_flow(user_input, True) + + async def async_step_zeroconf_confirm( + self, user_input: ConfigType = None + ) -> Dict[str, Any]: + """Handle a flow initiated by zeroconf.""" + return await self._handle_config_flow(user_input) + + async def _handle_config_flow( + self, user_input: Optional[ConfigType] = None, prepare: bool = False + ) -> Dict[str, Any]: + """Config flow handler for WLED.""" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + source = self.context.get("source") + + # Request user input, unless we are preparing discovery flow + if user_input is None and not prepare: + if source == SOURCE_ZEROCONF: + return self._show_confirm_dialog() + return self._show_setup_form() + + if source == SOURCE_ZEROCONF: + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + user_input[CONF_HOST] = self.context.get(CONF_HOST) + + errors = {} + session = async_get_clientsession(self.hass) + wled = WLED(user_input[CONF_HOST], loop=self.hass.loop, session=session) + + try: + device = await wled.update() + except WLEDConnectionError: + if source == SOURCE_ZEROCONF: + return self.async_abort(reason="connection_error") + errors["base"] = "connection_error" + return self._show_setup_form(errors) + + # Check if already configured + mac_address = device.info.mac_address + for entry in self._async_current_entries(): + if entry.data[CONF_MAC] == mac_address: + # This mac address is already configured + return self.async_abort(reason="already_configured") + + title = user_input[CONF_HOST] + if source == SOURCE_ZEROCONF: + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + title = self.context.get(CONF_NAME) + + if prepare: + return await self.async_step_zeroconf_confirm() + + return self.async_create_entry( + title=title, data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: mac_address} + ) + + def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors or {}, + ) + + def _show_confirm_dialog(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the confirm dialog to the user.""" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + name = self.context.get(CONF_NAME) + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={"name": name}, + errors=errors or {}, + ) diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py new file mode 100644 index 00000000000000..9bc5f64a44429f --- /dev/null +++ b/homeassistant/components/wled/const.py @@ -0,0 +1,25 @@ +"""Constants for the WLED integration.""" + +# Integration domain +DOMAIN = "wled" + +# Hass data keys +DATA_WLED_CLIENT = "wled_client" +DATA_WLED_TIMER = "wled_timer" +DATA_WLED_UPDATED = "wled_updated" + +# Attributes +ATTR_COLOR_PRIMARY = "color_primary" +ATTR_DURATION = "duration" +ATTR_IDENTIFIERS = "identifiers" +ATTR_INTENSITY = "intensity" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +ATTR_ON = "on" +ATTR_PALETTE = "palette" +ATTR_PLAYLIST = "playlist" +ATTR_PRESET = "preset" +ATTR_SEGMENT_ID = "segment_id" +ATTR_SOFTWARE_VERSION = "sw_version" +ATTR_SPEED = "speed" +ATTR_TARGET_BRIGHTNESS = "target_brightness" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py new file mode 100644 index 00000000000000..3d2c9d6ef2c94d --- /dev/null +++ b/homeassistant/components/wled/light.py @@ -0,0 +1,219 @@ +"""Support for LED lights.""" +import logging +from typing import Any, Callable, List, Optional, Tuple + +from wled import WLED, Effect, WLEDError + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_TRANSITION, + Light, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.color as color_util + +from . import WLEDDeviceEntity +from .const import ( + ATTR_COLOR_PRIMARY, + ATTR_INTENSITY, + ATTR_ON, + ATTR_PALETTE, + ATTR_PLAYLIST, + ATTR_PRESET, + ATTR_SEGMENT_ID, + ATTR_SPEED, + DATA_WLED_CLIENT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up WLED light based on a config entry.""" + wled: WLED = hass.data[DOMAIN][entry.entry_id][DATA_WLED_CLIENT] + + # Does the WLED device support RGBW + rgbw = wled.device.info.leds.rgbw + + # List of supported effects + effects = wled.device.effects + + # WLED supports splitting a strip in multiple segments + # Each segment will be a separate light in Home Assistant + lights = [] + for light in wled.device.state.segments: + lights.append(WLEDLight(entry.entry_id, wled, light.segment_id, rgbw, effects)) + + async_add_entities(lights, True) + + +class WLEDLight(Light, WLEDDeviceEntity): + """Defines a WLED light.""" + + def __init__( + self, entry_id: str, wled: WLED, segment: int, rgbw: bool, effects: List[Effect] + ): + """Initialize WLED light.""" + self._effects = effects + self._rgbw = rgbw + self._segment = segment + + self._brightness: Optional[int] = None + self._color: Optional[Tuple[float, float]] = None + self._effect: Optional[str] = None + self._state: Optional[bool] = None + + # Only apply the segment ID if it is not the first segment + name = wled.device.info.name + if segment != 0: + name += f" {segment}" + + super().__init__(entry_id, wled, name, "mdi:led-strip-variant") + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return f"{self.wled.device.info.mac_address}_{self._segment}" + + @property + def hs_color(self) -> Optional[Tuple[float, float]]: + """Return the hue and saturation color value [float, float].""" + return self._color + + @property + def effect(self) -> Optional[str]: + """Return the current effect of the light.""" + return self._effect + + @property + def brightness(self) -> Optional[int]: + """Return the brightness of this light between 1..255.""" + return self._brightness + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return ( + SUPPORT_BRIGHTNESS + | SUPPORT_COLOR + | SUPPORT_COLOR_TEMP + | SUPPORT_EFFECT + | SUPPORT_TRANSITION + ) + + @property + def effect_list(self) -> List[str]: + """Return the list of supported effects.""" + return [effect.name for effect in self._effects] + + @property + def is_on(self) -> bool: + """Return the state of the light.""" + return bool(self._state) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + try: + await self.wled.light(on=False) + self._state = False + except WLEDError: + _LOGGER.error("An error occurred while turning off WLED light.") + self._available = False + self.async_schedule_update_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + data = {ATTR_ON: True, ATTR_SEGMENT_ID: self._segment} + + if ATTR_COLOR_TEMP in kwargs: + mireds = color_util.color_temperature_kelvin_to_mired( + kwargs[ATTR_COLOR_TEMP] + ) + data[ATTR_COLOR_PRIMARY] = tuple( + map(int, color_util.color_temperature_to_rgb(mireds)) + ) + + if ATTR_HS_COLOR in kwargs: + hue, sat = kwargs[ATTR_HS_COLOR] + data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100) + + if ATTR_TRANSITION in kwargs: + data[ATTR_TRANSITION] = kwargs[ATTR_TRANSITION] + + if ATTR_BRIGHTNESS in kwargs: + data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] + + if ATTR_EFFECT in kwargs: + data[ATTR_EFFECT] = kwargs[ATTR_EFFECT] + + # Support for RGBW strips + if self._rgbw and any(x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs): + data[ATTR_COLOR_PRIMARY] = color_util.color_rgb_to_rgbw( + *data[ATTR_COLOR_PRIMARY] + ) + + try: + await self.wled.light(**data) + + self._state = True + + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] + + if ATTR_EFFECT in kwargs: + self._effect = kwargs[ATTR_EFFECT] + + if ATTR_HS_COLOR in kwargs: + self._color = kwargs[ATTR_HS_COLOR] + + if ATTR_COLOR_TEMP in kwargs: + self._color = color_util.color_temperature_to_hs(mireds) + + except WLEDError: + _LOGGER.error("An error occurred while turning on WLED light.") + self._available = False + self.async_schedule_update_ha_state() + + async def _wled_update(self) -> None: + """Update WLED entity.""" + self._brightness = self.wled.device.state.brightness + self._effect = self.wled.device.state.segments[self._segment].effect.name + self._state = self.wled.device.state.on + + color = self.wled.device.state.segments[self._segment].color_primary + if self._rgbw: + color = color_util.color_rgbw_to_rgb(*color) + self._color = color_util.color_RGB_to_hs(*color) + + playlist = self.wled.device.state.playlist + if playlist == -1: + playlist = None + + preset = self.wled.device.state.preset + if preset == -1: + preset = None + + self._attributes = { + ATTR_INTENSITY: self.wled.device.state.segments[self._segment].intensity, + ATTR_PALETTE: self.wled.device.state.segments[self._segment].palette.name, + ATTR_PLAYLIST: playlist, + ATTR_PRESET: preset, + ATTR_SPEED: self.wled.device.state.segments[self._segment].speed, + } diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json new file mode 100644 index 00000000000000..97e46998511ba1 --- /dev/null +++ b/homeassistant/components/wled/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "wled", + "name": "WLED", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/wled", + "requirements": ["wled==0.1.0"], + "dependencies": [], + "zeroconf": ["_wled._tcp.local."], + "codeowners": ["@frenck"] +} diff --git a/homeassistant/components/wled/strings.json b/homeassistant/components/wled/strings.json new file mode 100644 index 00000000000000..dde66b8e1225f1 --- /dev/null +++ b/homeassistant/components/wled/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "WLED", + "flow_title": "WLED: {name}", + "step": { + "user": { + "title": "Link your WLED", + "description": "Set up your WLED to integrate with Home Assistant.", + "data": { + "host": "Host or IP address" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the WLED named `{name}` to Home Assistant?", + "title": "Discovered WLED device" + } + }, + "error": { + "connection_error": "Failed to connect to WLED device." + }, + "abort": { + "already_configured": "This WLED device is already configured.", + "connection_error": "Failed to connect to WLED device." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 22d36fc46c61c4..519df86f5e9e80 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -81,6 +81,7 @@ "vesync", "wemo", "withings", + "wled", "wwlln", "zha", "zone", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 6200e2facb0c60..108fe38e64762f 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -20,6 +20,9 @@ ], "_hap._tcp.local.": [ "homekit_controller" + ], + "_wled._tcp.local.": [ + "wled" ] } diff --git a/requirements_all.txt b/requirements_all.txt index 3f5ac68cc8eca2..ac6529ff869146 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1995,6 +1995,9 @@ wirelesstagpy==0.4.0 # homeassistant.components.withings withings-api==2.1.3 +# homeassistant.components.wled +wled==0.1.0 + # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecf8d00d6483a4..605e244ace1e68 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -617,6 +617,9 @@ websockets==6.0 # homeassistant.components.withings withings-api==2.1.3 +# homeassistant.components.wled +wled==0.1.0 + # homeassistant.components.bluesound # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/tests/components/wled/__init__.py b/tests/components/wled/__init__.py new file mode 100644 index 00000000000000..41cbbf01074a80 --- /dev/null +++ b/tests/components/wled/__init__.py @@ -0,0 +1,42 @@ +"""Tests for the WLED integration.""" + +from homeassistant.components.wled.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def init_integration( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + rgbw: bool = False, + skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the WLED integration in Home Assistant.""" + + fixture = "wled/rgb.json" if not rgbw else "wled/rgbw.json" + aioclient_mock.get( + "http://example.local:80/json/", + text=load_fixture(fixture), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.post( + "http://example.local:80/json/state", + json={"success": True}, + headers={"Content-Type": "application/json"}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "example.local", CONF_MAC: "aabbccddeeff"} + ) + + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py new file mode 100644 index 00000000000000..322a068150b8e2 --- /dev/null +++ b/tests/components/wled/test_config_flow.py @@ -0,0 +1,207 @@ +"""Tests for the WLED config flow.""" +import aiohttp + +from homeassistant import data_entry_flow +from homeassistant.components.wled import config_flow +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_show_user_form(hass: HomeAssistant) -> None: + """Test that the user set up form is served.""" + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user(user_input=None) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None: + """Test that the zeroconf confirmation form is served.""" + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF, CONF_NAME: "test"} + result = await flow.async_step_zeroconf_confirm() + + assert result["description_placeholders"] == {CONF_NAME: "test"} + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_show_zerconf_form( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test that the zeroconf confirmation form is served.""" + aioclient_mock.get( + "http://example.local:80/json/", + text=load_fixture("wled/rgb.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf({"hostname": "example.local."}) + + assert flow.context[CONF_HOST] == "example.local" + assert flow.context[CONF_NAME] == "example" + assert result["description_placeholders"] == {CONF_NAME: "example"} + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on WLED connection error.""" + aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user(user_input={CONF_HOST: "example.com"}) + + assert result["errors"] == {"base": "connection_error"} + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_zeroconf_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on WLED connection error.""" + aioclient_mock.get("http://example.local/json/", exc=aiohttp.ClientError) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf(user_input={"hostname": "example.local."}) + + assert result["reason"] == "connection_error" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_confirm_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on WLED connection error.""" + aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = { + "source": SOURCE_ZEROCONF, + CONF_HOST: "example.com", + CONF_NAME: "test", + } + result = await flow.async_step_zeroconf_confirm( + user_input={CONF_HOST: "example.com"} + ) + + assert result["reason"] == "connection_error" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_no_data( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort if zeroconf provides no data.""" + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + result = await flow.async_step_zeroconf() + + assert result["reason"] == "connection_error" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_user_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if WLED device already configured.""" + await init_integration(hass, aioclient_mock) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user({CONF_HOST: "example.local"}) + + assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if WLED device already configured.""" + await init_integration(hass, aioclient_mock) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf({"hostname": "example.local."}) + + assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_full_user_flow_implementation( + hass: HomeAssistant, aioclient_mock +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.get( + "http://example.local:80/json/", + text=load_fixture("wled/rgb.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user(user_input=None) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await flow.async_step_user(user_input={CONF_HOST: "example.local"}) + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_MAC] == "aabbccddeeff" + assert result["title"] == "example.local" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_full_zeroconf_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.get( + "http://example.local:80/json/", + text=load_fixture("wled/rgb.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.WLEDFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf({"hostname": "example.local."}) + + assert flow.context[CONF_HOST] == "example.local" + assert flow.context[CONF_NAME] == "example" + assert result["description_placeholders"] == {CONF_NAME: "example"} + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await flow.async_step_zeroconf_confirm( + user_input={CONF_HOST: "example.local"} + ) + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_MAC] == "aabbccddeeff" + assert result["title"] == "example" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py new file mode 100644 index 00000000000000..a565dcfb1819c8 --- /dev/null +++ b/tests/components/wled/test_init.py @@ -0,0 +1,60 @@ +"""Tests for the WLED integration.""" +import aiohttp +from asynctest import patch + +from homeassistant.components.wled.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant + +from tests.components.wled import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_config_entry_not_ready( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the WLED configuration entry not ready.""" + aioclient_mock.get("http://example.local:80/json/", exc=aiohttp.ClientError) + + entry = await init_integration(hass, aioclient_mock) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_config_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the WLED configuration entry unloading.""" + entry = await init_integration(hass, aioclient_mock) + assert hass.data[DOMAIN] + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert not hass.data.get(DOMAIN) + + +async def test_interval_update( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the WLED configuration entry unloading.""" + entry = await init_integration(hass, aioclient_mock, skip_setup=True) + + interval_action = False + + def async_track_time_interval(hass, action, interval): + nonlocal interval_action + interval_action = action + + with patch( + "homeassistant.components.wled.async_track_time_interval", + new=async_track_time_interval, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert interval_action + await interval_action() # pylint: disable=not-callable + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_ON diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py new file mode 100644 index 00000000000000..185a25b0507117 --- /dev/null +++ b/tests/components/wled/test_light.py @@ -0,0 +1,179 @@ +"""Tests for the WLED light platform.""" +import aiohttp + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.components.wled.const import ( + ATTR_INTENSITY, + ATTR_PALETTE, + ATTR_PLAYLIST, + ATTR_PRESET, + ATTR_SPEED, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from tests.components.wled import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_rgb_light_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the WLED lights.""" + await init_integration(hass, aioclient_mock) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # First segment of the strip + state = hass.states.get("light.wled_rgb_light") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 127 + assert state.attributes.get(ATTR_EFFECT) == "Solid" + assert state.attributes.get(ATTR_HS_COLOR) == (37.412, 100.0) + assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant" + assert state.attributes.get(ATTR_INTENSITY) == 128 + assert state.attributes.get(ATTR_PALETTE) == "Default" + assert state.attributes.get(ATTR_PLAYLIST) is None + assert state.attributes.get(ATTR_PRESET) is None + assert state.attributes.get(ATTR_SPEED) == 32 + assert state.state == STATE_ON + + entry = entity_registry.async_get("light.wled_rgb_light") + assert entry + assert entry.unique_id == "aabbccddeeff_0" + + # Second segment of the strip + state = hass.states.get("light.wled_rgb_light_1") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 127 + assert state.attributes.get(ATTR_EFFECT) == "Blink" + assert state.attributes.get(ATTR_HS_COLOR) == (148.941, 100.0) + assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant" + assert state.attributes.get(ATTR_INTENSITY) == 64 + assert state.attributes.get(ATTR_PALETTE) == "Random Cycle" + assert state.attributes.get(ATTR_PLAYLIST) is None + assert state.attributes.get(ATTR_PRESET) is None + assert state.attributes.get(ATTR_SPEED) == 16 + assert state.state == STATE_ON + + entry = entity_registry.async_get("light.wled_rgb_light_1") + assert entry + assert entry.unique_id == "aabbccddeeff_1" + + +async def test_switch_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the change of state of the WLED switches.""" + await init_integration(hass, aioclient_mock) + + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_ON + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_BRIGHTNESS: 42, + ATTR_EFFECT: "Chase", + ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_RGB_COLOR: [255, 0, 0], + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_BRIGHTNESS) == 42 + assert state.attributes.get(ATTR_EFFECT) == "Chase" + assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_COLOR_TEMP: 400}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) + + +async def test_light_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error handling of the WLED switches.""" + aioclient_mock.post("http://example.local:80/json/state", exc=aiohttp.ClientError) + await init_integration(hass, aioclient_mock) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_UNAVAILABLE + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgb_light_1"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.wled_rgb_light_1") + assert state.state == STATE_UNAVAILABLE + + +async def test_rgbw_light( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test RGBW support for WLED.""" + await init_integration(hass, aioclient_mock, rgbw=True) + + state = hass.states.get("light.wled_rgbw_light") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 64.706) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_COLOR_TEMP: 400}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgbw_light") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) diff --git a/tests/fixtures/wled/rgb.json b/tests/fixtures/wled/rgb.json new file mode 100644 index 00000000000000..70a54f06644bec --- /dev/null +++ b/tests/fixtures/wled/rgb.json @@ -0,0 +1,210 @@ +{ + "state": { + "on": true, + "bri": 127, + "transition": 7, + "ps": -1, + "pl": -1, + "nl": { + "on": false, + "dur": 60, + "fade": true, + "tbri": 0 + }, + "udpn": { + "send": false, + "recv": true + }, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 19, + "len": 20, + "col": [[255, 159, 0], [0, 0, 0], [0, 0, 0]], + "fx": 0, + "sx": 32, + "ix": 128, + "pal": 0, + "sel": true, + "rev": false, + "cln": -1 + }, + { + "id": 1, + "start": 20, + "stop": 30, + "len": 10, + "col": [[0, 255, 123], [0, 0, 0], [0, 0, 0]], + "fx": 1, + "sx": 16, + "ix": 64, + "pal": 1, + "sel": true, + "rev": false, + "cln": -1 + } + ] + }, + "info": { + "ver": "0.8.5", + "vid": 1909122, + "leds": { + "count": 30, + "rgbw": false, + "pin": [2], + "pwr": 470, + "maxpwr": 850, + "maxseg": 10 + }, + "name": "WLED RGB Light", + "udpport": 21324, + "live": false, + "fxcount": 81, + "palcount": 50, + "arch": "esp8266", + "core": "2_4_2", + "freeheap": 14600, + "uptime": 32, + "opt": 119, + "brand": "WLED", + "product": "DIY light", + "btype": "bin", + "mac": "aabbccddeeff" + }, + "effects": [ + "Solid", + "Blink", + "Breathe", + "Wipe", + "Wipe Random", + "Random Colors", + "Sweep", + "Dynamic", + "Colorloop", + "Rainbow", + "Scan", + "Dual Scan", + "Fade", + "Chase", + "Chase Rainbow", + "Running", + "Saw", + "Twinkle", + "Dissolve", + "Dissolve Rnd", + "Sparkle", + "Dark Sparkle", + "Sparkle+", + "Strobe", + "Strobe Rainbow", + "Mega Strobe", + "Blink Rainbow", + "Android", + "Chase", + "Chase Random", + "Chase Rainbow", + "Chase Flash", + "Chase Flash Rnd", + "Rainbow Runner", + "Colorful", + "Traffic Light", + "Sweep Random", + "Running 2", + "Red & Blue", + "Stream", + "Scanner", + "Lighthouse", + "Fireworks", + "Rain", + "Merry Christmas", + "Fire Flicker", + "Gradient", + "Loading", + "In Out", + "In In", + "Out Out", + "Out In", + "Circus", + "Halloween", + "Tri Chase", + "Tri Wipe", + "Tri Fade", + "Lightning", + "ICU", + "Multi Comet", + "Dual Scanner", + "Stream 2", + "Oscillate", + "Pride 2015", + "Juggle", + "Palette", + "Fire 2012", + "Colorwaves", + "BPM", + "Fill Noise", + "Noise 1", + "Noise 2", + "Noise 3", + "Noise 4", + "Colortwinkle", + "Lake", + "Meteor", + "Smooth Meteor", + "Railway", + "Ripple", + "Twinklefox" + ], + "palettes": [ + "Default", + "Random Cycle", + "Primary Color", + "Based on Primary", + "Set Colors", + "Based on Set", + "Party", + "Cloud", + "Lava", + "Ocean", + "Forest", + "Rainbow", + "Rainbow Bands", + "Sunset", + "Rivendell", + "Breeze", + "Red & Blue", + "Yellowout", + "Analogous", + "Splash", + "Pastel", + "Sunset 2", + "Beech", + "Vintage", + "Departure", + "Landscape", + "Beach", + "Sherbet", + "Hult", + "Hult 64", + "Drywet", + "Jul", + "Grintage", + "Rewhi", + "Tertiary", + "Fire", + "Icefire", + "Cyane", + "Light Pink", + "Autumn", + "Magenta", + "Magred", + "Yelmag", + "Yelblu", + "Orange & Teal", + "Tiamat", + "April Night", + "Orangery", + "C9", + "Sakura" + ] +} diff --git a/tests/fixtures/wled/rgbw.json b/tests/fixtures/wled/rgbw.json new file mode 100644 index 00000000000000..0d51dfedd2df73 --- /dev/null +++ b/tests/fixtures/wled/rgbw.json @@ -0,0 +1,198 @@ +{ + "state": { + "on": true, + "bri": 140, + "transition": 7, + "ps": -1, + "pl": -1, + "nl": { + "on": false, + "dur": 60, + "fade": true, + "tbri": 0 + }, + "udpn": { + "send": false, + "recv": true + }, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 13, + "len": 13, + "col": [[255, 0, 0, 139], [0, 0, 0, 0], [0, 0, 0, 0]], + "fx": 9, + "sx": 165, + "ix": 128, + "pal": 0, + "sel": true, + "rev": false, + "cln": -1 + } + ] + }, + "info": { + "ver": "0.8.6", + "vid": 1910255, + "leds": { + "count": 13, + "rgbw": true, + "pin": [2], + "pwr": 208, + "maxpwr": 850, + "maxseg": 10 + }, + "name": "WLED RGBW Light", + "udpport": 21324, + "live": false, + "fxcount": 83, + "palcount": 50, + "arch": "esp8266", + "core": "2_5_2", + "freeheap": 20136, + "uptime": 5591, + "opt": 119, + "brand": "WLED", + "product": "DIY light", + "btype": "bin", + "mac": "aabbccddee11" + }, + "effects": [ + "Solid", + "Blink", + "Breathe", + "Wipe", + "Wipe Random", + "Random Colors", + "Sweep", + "Dynamic", + "Colorloop", + "Rainbow", + "Scan", + "Dual Scan", + "Fade", + "Chase", + "Chase Rainbow", + "Running", + "Saw", + "Twinkle", + "Dissolve", + "Dissolve Rnd", + "Sparkle", + "Dark Sparkle", + "Sparkle+", + "Strobe", + "Strobe Rainbow", + "Mega Strobe", + "Blink Rainbow", + "Android", + "Chase", + "Chase Random", + "Chase Rainbow", + "Chase Flash", + "Chase Flash Rnd", + "Rainbow Runner", + "Colorful", + "Traffic Light", + "Sweep Random", + "Running 2", + "Red & Blue", + "Stream", + "Scanner", + "Lighthouse", + "Fireworks", + "Rain", + "Merry Christmas", + "Fire Flicker", + "Gradient", + "Loading", + "In Out", + "In In", + "Out Out", + "Out In", + "Circus", + "Halloween", + "Tri Chase", + "Tri Wipe", + "Tri Fade", + "Lightning", + "ICU", + "Multi Comet", + "Dual Scanner", + "Stream 2", + "Oscillate", + "Pride 2015", + "Juggle", + "Palette", + "Fire 2012", + "Colorwaves", + "BPM", + "Fill Noise", + "Noise 1", + "Noise 2", + "Noise 3", + "Noise 4", + "Colortwinkles", + "Lake", + "Meteor", + "Smooth Meteor", + "Railway", + "Ripple", + "Twinklefox", + "Twinklecat", + "Halloween Eyes" + ], + "palettes": [ + "Default", + "Random Cycle", + "Primary Color", + "Based on Primary", + "Set Colors", + "Based on Set", + "Party", + "Cloud", + "Lava", + "Ocean", + "Forest", + "Rainbow", + "Rainbow Bands", + "Sunset", + "Rivendell", + "Breeze", + "Red & Blue", + "Yellowout", + "Analogous", + "Splash", + "Pastel", + "Sunset 2", + "Beech", + "Vintage", + "Departure", + "Landscape", + "Beach", + "Sherbet", + "Hult", + "Hult 64", + "Drywet", + "Jul", + "Grintage", + "Rewhi", + "Tertiary", + "Fire", + "Icefire", + "Cyane", + "Light Pink", + "Autumn", + "Magenta", + "Magred", + "Yelmag", + "Yelblu", + "Orange & Teal", + "Tiamat", + "April Night", + "Orangery", + "C9", + "Sakura" + ] +} From 265c390b653a604347f9beaf6e97069e881f86fd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 7 Nov 2019 00:32:16 +0000 Subject: [PATCH 1472/3953] [ci skip] Translation update --- .../alarm_control_panel/.translations/nl.json | 3 ++ .../components/almond/.translations/nl.json | 10 ++++ .../components/almond/.translations/ru.json | 3 +- .../coolmaster/.translations/nl.json | 13 +++++ .../components/cover/.translations/lb.json | 8 ++++ .../components/cover/.translations/nl.json | 12 ++++- .../components/deconz/.translations/ca.json | 8 ++++ .../components/deconz/.translations/en.json | 18 ++++++- .../components/deconz/.translations/lb.json | 18 ++++++- .../components/deconz/.translations/nl.json | 18 ++++++- .../components/deconz/.translations/pl.json | 18 ++++++- .../components/deconz/.translations/ru.json | 38 ++++++++++----- .../deconz/.translations/zh-Hant.json | 18 ++++++- .../device_tracker/.translations/nl.json | 8 ++++ .../huawei_lte/.translations/ca.json | 5 +- .../huawei_lte/.translations/en.json | 5 +- .../huawei_lte/.translations/fr.json | 5 +- .../huawei_lte/.translations/lb.json | 5 +- .../huawei_lte/.translations/nl.json | 34 ++++++++++++- .../huawei_lte/.translations/pl.json | 5 +- .../huawei_lte/.translations/ru.json | 4 +- .../huawei_lte/.translations/zh-Hant.json | 9 ++-- .../media_player/.translations/nl.json | 11 +++++ .../components/somfy/.translations/nl.json | 5 ++ .../transmission/.translations/nl.json | 5 +- .../components/withings/.translations/nl.json | 4 +- .../components/wled/.translations/en.json | 48 +++++++++---------- .../components/zha/.translations/ru.json | 18 +++---- 28 files changed, 293 insertions(+), 63 deletions(-) create mode 100644 homeassistant/components/almond/.translations/nl.json create mode 100644 homeassistant/components/coolmaster/.translations/nl.json create mode 100644 homeassistant/components/device_tracker/.translations/nl.json create mode 100644 homeassistant/components/media_player/.translations/nl.json diff --git a/homeassistant/components/alarm_control_panel/.translations/nl.json b/homeassistant/components/alarm_control_panel/.translations/nl.json index 86cacad9fd675f..9329a089d3204b 100644 --- a/homeassistant/components/alarm_control_panel/.translations/nl.json +++ b/homeassistant/components/alarm_control_panel/.translations/nl.json @@ -1,6 +1,9 @@ { "device_automation": { "action_type": { + "arm_away": "Inschakelen {entity_name} afwezig", + "arm_home": "Inschakelen {entity_name} thuis", + "arm_night": "Inschakelen {entity_name} nacht", "disarm": "Uitschakelen {entity_name}", "trigger": "Trigger {entity_name}" } diff --git a/homeassistant/components/almond/.translations/nl.json b/homeassistant/components/almond/.translations/nl.json new file mode 100644 index 00000000000000..dfe9c238db7b8a --- /dev/null +++ b/homeassistant/components/almond/.translations/nl.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_setup": "U kunt slechts \u00e9\u00e9n Almond-account configureren.", + "cannot_connect": "Kan geen verbinding maken met de Almond-server.", + "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond." + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ru.json b/homeassistant/components/almond/.translations/ru.json index b513d5b28d7721..d15e9a1eeb47d7 100644 --- a/homeassistant/components/almond/.translations/ru.json +++ b/homeassistant/components/almond/.translations/ru.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Almond." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Almond.", + "missing_configuration": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond." }, "title": "Almond" } diff --git a/homeassistant/components/coolmaster/.translations/nl.json b/homeassistant/components/coolmaster/.translations/nl.json new file mode 100644 index 00000000000000..79a1e9fe1e6f72 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "error": { + "connection_error": "Kan geen verbinding maken met CoolMasterNet-instantie. Controleer uw host" + }, + "step": { + "user": { + "title": "Stel uw CoolMasterNet-verbindingsgegevens in." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/lb.json b/homeassistant/components/cover/.translations/lb.json index 4f7a898c7722c9..b2645f3e00167b 100644 --- a/homeassistant/components/cover/.translations/lb.json +++ b/homeassistant/components/cover/.translations/lb.json @@ -7,6 +7,14 @@ "is_opening": "{entity_name} g\u00ebtt opgemaach", "is_position": "{entity_name} positioun ass", "is_tilt_position": "{entity_name} kipp positioun ass" + }, + "trigger_type": { + "closed": "{entity_name} gouf zougemaach", + "closing": "{entity_name} mecht zou", + "opened": "{entity_name} gouf opgemaach", + "opening": "{entity_name} mecht op", + "position": "{entity_name} positioun \u00e4nnert", + "tilt_position": "{entity_name} kipp positioun ge\u00e4nnert" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/nl.json b/homeassistant/components/cover/.translations/nl.json index 93015afbfdd8d8..472583687dd8fa 100644 --- a/homeassistant/components/cover/.translations/nl.json +++ b/homeassistant/components/cover/.translations/nl.json @@ -4,7 +4,17 @@ "is_closed": "{entity_name} is gesloten", "is_closing": "{entity_name} wordt gesloten", "is_open": "{entity_name} is open", - "is_opening": "{entity_name} wordt geopend" + "is_opening": "{entity_name} wordt geopend", + "is_position": "Huidige {entity_name} positie is", + "is_tilt_position": "Huidige {entity_name} kantel positie is" + }, + "trigger_type": { + "closed": "{entity_name} gesloten", + "closing": "{entity_name} wordt gesloten", + "opened": "{entity_name} geopend", + "opening": "{entity_name} wordt geopend", + "position": "{entity_name} positiewijzigingen", + "tilt_position": "{entity_name} kantel positiewijzigingen" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 2900128eedb17b..676641d38c6216 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -55,10 +55,17 @@ "left": "Esquerra", "open": "Obert", "right": "Dreta", + "side_1": "Costat 1", + "side_2": "Costat 2", + "side_3": "Costat 3", + "side_4": "Costat 4", + "side_5": "Costat 5", + "side_6": "Costat 6", "turn_off": "Desactiva", "turn_on": "Activa" }, "trigger_type": { + "remote_awakened": "Dispositiu despertat", "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades consecutives", "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut continuament", "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", @@ -69,6 +76,7 @@ "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives", + "remote_falling": "Dispositiu en caiguda lliure", "remote_gyro_activated": "Dispositiu sacsejat" } }, diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index e9c64ffe5faa64..63798bab52b242 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -55,10 +55,17 @@ "left": "Left", "open": "Open", "right": "Right", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6", "turn_off": "Turn off", "turn_on": "Turn on" }, "trigger_type": { + "remote_awakened": "Device awakened", "remote_button_double_press": "\"{subtype}\" button double clicked", "remote_button_long_press": "\"{subtype}\" button continuously pressed", "remote_button_long_release": "\"{subtype}\" button released after long press", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" button pressed", "remote_button_short_release": "\"{subtype}\" button released", "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_gyro_activated": "Device shaken" + "remote_double_tap": "Device \"{subtype}\" double tapped", + "remote_falling": "Device in free fall", + "remote_gyro_activated": "Device shaken", + "remote_moved": "Device moved with \"{subtype}\" up", + "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", + "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", + "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", + "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", + "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", + "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 49394eb9773bbb..07f88732c6250e 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -55,10 +55,17 @@ "left": "L\u00e9nks", "open": "Op", "right": "Riets", + "side_1": "S\u00e4it 1", + "side_2": "S\u00e4it 2", + "side_3": "S\u00e4it 3", + "side_4": "S\u00e4it 4", + "side_5": "S\u00e4it 5", + "side_6": "S\u00e4it 6", "turn_off": "Ausschalten", "turn_on": "Uschalten" }, "trigger_type": { + "remote_awakened": "Apparat erw\u00e4cht", "remote_button_double_press": "\"{subtype}\" Kn\u00e4ppche zwee mol gedr\u00e9ckt", "remote_button_long_press": "\"{subtype}\" Kn\u00e4ppche permanent gedr\u00e9ckt", "remote_button_long_release": "\"{subtype}\" Kn\u00e4ppche no laangem unhalen lassgelooss", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", - "remote_gyro_activated": "Apparat ger\u00ebselt" + "remote_double_tap": "Apparat \"{subtype}\" zwee mol gedr\u00e9ckt", + "remote_falling": "Apparat am fr\u00e4ie Fall", + "remote_gyro_activated": "Apparat ger\u00ebselt", + "remote_moved": "Apparat beweegt mat \"{subtype}\" erop", + "remote_rotate_from_side_1": "Apparat rot\u00e9iert vun der \"S\u00e4it 1\" op \"{subtype}\"", + "remote_rotate_from_side_2": "Apparat rot\u00e9iert vun der \"S\u00e4it 2\" op \"{subtype}\"", + "remote_rotate_from_side_3": "Apparat rot\u00e9iert vun der \"S\u00e4it 3\" op \"{subtype}\"", + "remote_rotate_from_side_4": "Apparat rot\u00e9iert vun der \"S\u00e4it 4\" op \"{subtype}\"", + "remote_rotate_from_side_5": "Apparat rot\u00e9iert vun der \"S\u00e4it 5\" op \"{subtype}\"", + "remote_rotate_from_side_6": "Apparat rot\u00e9iert vun der \"S\u00e4it\" 6 op \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 7f690f11f1d74c..c0ee391b0c7673 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -55,10 +55,17 @@ "left": "Links", "open": "Open", "right": "Rechts", + "side_1": "Zijde 1", + "side_2": "Zijde 2", + "side_3": "Zijde 3", + "side_4": "Zijde 4", + "side_5": "Zijde 5", + "side_6": "Zijde 6", "turn_off": "Uitschakelen", "turn_on": "Inschakelen" }, "trigger_type": { + "remote_awakened": "Apparaat is gewekt", "remote_button_double_press": "\"{subtype}\" knop dubbel geklikt", "remote_button_long_press": "\" {subtype} \" knop continu ingedrukt", "remote_button_long_release": "\"{subtype}\" knop losgelaten na lang indrukken van de knop", @@ -69,7 +76,16 @@ "remote_button_short_press": "\" {subtype} \" knop ingedrukt", "remote_button_short_release": "\"{subtype}\" knop losgelaten", "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt", - "remote_gyro_activated": "Apparaat geschud" + "remote_double_tap": "Apparaat \"{subtype}\" dubbel getikt", + "remote_falling": "Apparaat in vrije val", + "remote_gyro_activated": "Apparaat geschud", + "remote_moved": "Apparaat verplaatst met \"{subtype}\" omhoog", + "remote_rotate_from_side_1": "Apparaat gedraaid van \"zijde 1\" naar \"{subtype}\"\".", + "remote_rotate_from_side_2": "Apparaat gedraaid van \"zijde 2\" naar \"{subtype}\"\".", + "remote_rotate_from_side_3": "Apparaat gedraaid van \"zijde 3\" naar \" {subtype} \"", + "remote_rotate_from_side_4": "Apparaat gedraaid van \"zijde 4\" naar \" {subtype} \"", + "remote_rotate_from_side_5": "Apparaat gedraaid van \"zijde 5\" naar \" {subtype} \"", + "remote_rotate_from_side_6": "Apparaat gedraaid van \"zijde 6\" naar \" {subtype} \"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index ac9f06f1f17553..eafecf87d03711 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -55,10 +55,17 @@ "left": "w lewo", "open": "otwarcie", "right": "w prawo", + "side_1": "strona 1", + "side_2": "strona 2", + "side_3": "strona 3", + "side_4": "strona 4", + "side_5": "strona 5", + "side_6": "strona 6", "turn_off": "nast\u0105pi wy\u0142\u0105czenie", "turn_on": "nast\u0105pi w\u0142\u0105czenie" }, "trigger_type": { + "remote_awakened": "urz\u0105dzenie si\u0119 obudzi", "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", @@ -69,7 +76,16 @@ "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", - "remote_gyro_activated": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" + "remote_double_tap": "urz\u0105dzenie \"{subtype}\" zostanie dwukrotnie pukni\u0119te", + "remote_falling": "urz\u0105dzenie zarejestruje swobodny spadek", + "remote_gyro_activated": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", + "remote_moved": "urz\u0105dzenie poruszone z \"{subtype}\" w g\u00f3r\u0119", + "remote_rotate_from_side_1": "urz\u0105dzenie obr\u00f3cone ze \"strona 1\" na \"{subtype}\"", + "remote_rotate_from_side_2": "urz\u0105dzenie obr\u00f3cone ze \"strona 2\" na \"{subtype}\"", + "remote_rotate_from_side_3": "urz\u0105dzenie obr\u00f3cone ze \"strona 3\" na \"{subtype}\"", + "remote_rotate_from_side_4": "urz\u0105dzenie obr\u00f3cone ze \"strona 4\" na \"{subtype}\"", + "remote_rotate_from_side_5": "urz\u0105dzenie obr\u00f3cone ze \"strona 5\" na \"{subtype}\"", + "remote_rotate_from_side_6": "urz\u0105dzenie obr\u00f3cone ze \"strona 6\" na \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 2dc3df17aa9069..f0398e530bcbdc 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -55,21 +55,37 @@ "left": "\u041d\u0430\u043b\u0435\u0432\u043e", "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", + "side_1": "\u0413\u0440\u0430\u043d\u044c 1", + "side_2": "\u0413\u0440\u0430\u043d\u044c 2", + "side_3": "\u0413\u0440\u0430\u043d\u044c 3", + "side_4": "\u0413\u0440\u0430\u043d\u044c 4", + "side_5": "\u0413\u0440\u0430\u043d\u044c 5", + "side_6": "\u0413\u0440\u0430\u043d\u044c 6", "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" }, "trigger_type": { - "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", - "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", - "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", - "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430", - "remote_button_rotation_stopped": "\u041f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \"{subtype}\"", - "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", - "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", - "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438" + "remote_awakened": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043b\u0438", + "remote_button_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "remote_button_long_press": "\"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_rotated": "\"{subtype}\" \u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f", + "remote_button_rotation_stopped": "\"{subtype}\" \u043f\u0440\u0435\u043a\u0440\u0430\u0442\u0438\u043b\u0430 \u0432\u0440\u0430\u0449\u0435\u043d\u0438\u0435", + "remote_button_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", + "remote_button_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", + "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \"{subtype}\" \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", + "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u043c \u043f\u0430\u0434\u0435\u043d\u0438\u0438", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438", + "remote_moved": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0434\u0432\u0438\u043d\u0443\u043b\u0438, \u043a\u043e\u0433\u0434\u0430 \"{subtype}\" \u0441\u0432\u0435\u0440\u0445\u0443", + "remote_rotate_from_side_1": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 1 \u043d\u0430 \"{subtype}\"", + "remote_rotate_from_side_2": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 2 \u043d\u0430 \"{subtype}\"", + "remote_rotate_from_side_3": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 3 \u043d\u0430 \"{subtype}\"", + "remote_rotate_from_side_4": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 4 \u043d\u0430 \"{subtype}\"", + "remote_rotate_from_side_5": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 5 \u043d\u0430 \"{subtype}\"", + "remote_rotate_from_side_6": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 6 \u043d\u0430 \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 975600a5745cb3..0a0e40a8d1eb31 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -55,10 +55,17 @@ "left": "\u5de6", "open": "\u958b\u555f", "right": "\u53f3", + "side_1": "\u7b2c 1 \u9762", + "side_2": "\u7b2c 2 \u9762", + "side_3": "\u7b2c 3 \u9762", + "side_4": "\u7b2c 4 \u9762", + "side_5": "\u7b2c 5 \u9762", + "side_6": "\u7b2c 6 \u9762", "turn_off": "\u95dc\u9589", "turn_on": "\u958b\u555f" }, "trigger_type": { + "remote_awakened": "\u8a2d\u5099\u5df2\u559a\u9192", "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", "remote_button_long_release": "\u9577\u6309\u5f8c\u91cb\u653e \"{subtype}\" \u6309\u9215", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", - "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643" + "remote_double_tap": "\u8a2d\u5099 \"{subtype}\" \u96d9\u6572", + "remote_falling": "\u8a2d\u5099\u81ea\u7531\u843d\u4e0b", + "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643", + "remote_moved": "\u8a2d\u5099\u79fb\u52d5\u81f3 \"{subtype}\" \u671d\u4e0a", + "remote_rotate_from_side_1": "\u8a2d\u5099\u7531\u300c\u7b2c 1 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_2": "\u8a2d\u5099\u7531\u300c\u7b2c 2 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_3": "\u8a2d\u5099\u7531\u300c\u7b2c 3 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_4": "\u8a2d\u5099\u7531\u300c\u7b2c 4 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_5": "\u8a2d\u5099\u7531\u300c\u7b2c 5 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_6": "\u8a2d\u5099\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d" } }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/nl.json b/homeassistant/components/device_tracker/.translations/nl.json new file mode 100644 index 00000000000000..d4de8b1f66a010 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/nl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} is thuis", + "is_not_home": "{entity_name} is niet thuis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json index 6dc88f213235a9..b213da018d210c 100644 --- a/homeassistant/components/huawei_lte/.translations/ca.json +++ b/homeassistant/components/huawei_lte/.translations/ca.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Aquest dispositiu ja est\u00e0 configurat" + "already_configured": "Aquest dispositiu ja est\u00e0 configurat", + "already_in_progress": "Aquest dispositiu ja s'est\u00e0 configurant", + "not_huawei_lte": "No \u00e9s un dispositiu Huawei LTE" }, "error": { "connection_failed": "La connexi\u00f3 ha fallat", + "connection_timeout": "S'ha acabat el temps d'espera de la connexi\u00f3", "incorrect_password": "Contrasenya incorrecta", "incorrect_username": "Nom d'usuari incorrecte", "incorrect_username_or_password": "Nom d'usuari o contrasenya incorrectes", diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 8681e3355a46c5..52aaafe595cdc9 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "This device is already configured" + "already_configured": "This device has already been configured", + "already_in_progress": "This device is already being configured", + "not_huawei_lte": "Not a Huawei LTE device" }, "error": { "connection_failed": "Connection failed", + "connection_timeout": "Connection timeout", "incorrect_password": "Incorrect password", "incorrect_username": "Incorrect username", "incorrect_username_or_password": "Incorrect username or password", diff --git a/homeassistant/components/huawei_lte/.translations/fr.json b/homeassistant/components/huawei_lte/.translations/fr.json index e0394d525d4acc..5effea3d003606 100644 --- a/homeassistant/components/huawei_lte/.translations/fr.json +++ b/homeassistant/components/huawei_lte/.translations/fr.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Cet appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Cet appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Ce p\u00e9riph\u00e9rique est d\u00e9j\u00e0 en cours de configuration", + "not_huawei_lte": "Pas un appareil Huawei LTE" }, "error": { "connection_failed": "La connexion a \u00e9chou\u00e9", + "connection_timeout": "D\u00e9lai de connection d\u00e9pass\u00e9", "incorrect_password": "Mot de passe incorrect", "incorrect_username": "Nom d'utilisateur incorrect", "incorrect_username_or_password": "identifiant ou mot de passe incorrect", diff --git a/homeassistant/components/huawei_lte/.translations/lb.json b/homeassistant/components/huawei_lte/.translations/lb.json index 2b90245e929ab7..3c8f0464a55c8e 100644 --- a/homeassistant/components/huawei_lte/.translations/lb.json +++ b/homeassistant/components/huawei_lte/.translations/lb.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "D\u00ebsen Apparat ass scho konfigur\u00e9iert" + "already_configured": "D\u00ebsen Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "D\u00ebsen Apparat g\u00ebtt scho konfigur\u00e9iert", + "not_huawei_lte": "Ken Huawei LTE Apparat" }, "error": { "connection_failed": "Feeler bei der Verbindung", + "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen", "incorrect_password": "Ong\u00ebltegt Passwuert", "incorrect_username": "Ong\u00ebltege Benotzernumm", "incorrect_username_or_password": "Ong\u00ebltege Benotzernumm oder Passwuert", diff --git a/homeassistant/components/huawei_lte/.translations/nl.json b/homeassistant/components/huawei_lte/.translations/nl.json index 4e4b63e9391d7e..6d5e5c3e957b70 100644 --- a/homeassistant/components/huawei_lte/.translations/nl.json +++ b/homeassistant/components/huawei_lte/.translations/nl.json @@ -1,10 +1,42 @@ { "config": { + "abort": { + "already_configured": "Dit apparaat is reeds geconfigureerd", + "already_in_progress": "Dit apparaat wordt al geconfigureerd", + "not_huawei_lte": "Geen Huawei LTE-apparaat" + }, "error": { + "connection_failed": "Verbinding mislukt", + "connection_timeout": "Time-out van de verbinding", "incorrect_password": "Onjuist wachtwoord", "incorrect_username": "Onjuiste gebruikersnaam", "incorrect_username_or_password": "Onjuiste gebruikersnaam of wachtwoord", - "invalid_url": "Ongeldige URL" + "invalid_url": "Ongeldige URL", + "login_attempts_exceeded": "Maximale aanmeldingspogingen overschreden, probeer het later opnieuw.", + "response_error": "Onbekende fout van het apparaat", + "unknown_connection_error": "Onbekende fout bij verbinden met apparaat" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "url": "URL", + "username": "Gebruikersnaam" + }, + "description": "Voer de toegangsgegevens van het apparaat in. Opgeven van gebruikersnaam en wachtwoord is optioneel, maar biedt ondersteuning voor meer integratiefuncties. Aan de andere kant kan het gebruik van een geautoriseerde verbinding problemen veroorzaken bij het openen van het webinterface van het apparaat buiten de Home Assitant, terwijl de integratie actief is en andersom.", + "title": "Configureer Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Ontvangers van sms-berichten", + "track_new_devices": "Volg nieuwe apparaten" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json index 5a8c4033436558..3851d0a409fb5c 100644 --- a/homeassistant/components/huawei_lte/.translations/pl.json +++ b/homeassistant/components/huawei_lte/.translations/pl.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE" }, "error": { "connection_failed": "Po\u0142\u0105czenie nie powiod\u0142o si\u0119", + "connection_timeout": "Przekroczono limit czasu pr\u00f3by po\u0142\u0105czenia.", "incorrect_password": "Nieprawid\u0142owe has\u0142o", "incorrect_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", "incorrect_username_or_password": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o", diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json index 1a0c5cc29ad254..e64018b2a3ce68 100644 --- a/homeassistant/components/huawei_lte/.translations/ru.json +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "connection_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "incorrect_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", "incorrect_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", "incorrect_username_or_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", diff --git a/homeassistant/components/huawei_lte/.translations/zh-Hant.json b/homeassistant/components/huawei_lte/.translations/zh-Hant.json index 795df4e3d6f862..37f1111b77f6bf 100644 --- a/homeassistant/components/huawei_lte/.translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/.translations/zh-Hant.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "\u6b64\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u6b64\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u6b64\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u8a2d\u5099" }, "error": { "connection_failed": "\u9023\u7dda\u5931\u6557", + "connection_timeout": "\u9023\u7dda\u903e\u6642", "incorrect_password": "\u5bc6\u78bc\u932f\u8aa4", "incorrect_username": "\u4f7f\u7528\u8005\u540d\u7a31\u932f\u8aa4", "incorrect_username_or_password": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4", @@ -21,10 +24,10 @@ "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u8f38\u5165\u8a2d\u5099\u5b58\u53d6\u8a73\u7d30\u8cc7\u6599\u3002\u6307\u5b9a\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u70ba\u9078\u9805\u8f38\u5165\uff0c\u4f46\u958b\u555f\u5c07\u652f\u63f4\u66f4\u591a\u6574\u5408\u529f\u80fd\u3002\u6b64\u5916\uff0c\u4f7f\u7528\u6388\u6b0a\u9023\u7dda\uff0c\u53ef\u80fd\u5c0e\u81f4\u6574\u5408\u555f\u7528\u5f8c\uff0c\u7531\u5916\u90e8\u9023\u7dda\u81f3 Home Assistant \u8a2d\u5099 Web \u4ecb\u9762\u51fa\u73fe\u67d0\u4e9b\u554f\u984c\uff0c\u53cd\u4e4b\u4ea6\u7136\u3002", - "title": "\u8a2d\u5b9a Huawei LTE" + "title": "\u8a2d\u5b9a\u83ef\u70ba LTE" } }, - "title": "Huawei LTE" + "title": "\u83ef\u70ba LTE" }, "options": { "step": { diff --git a/homeassistant/components/media_player/.translations/nl.json b/homeassistant/components/media_player/.translations/nl.json new file mode 100644 index 00000000000000..cfd63d190c33d9 --- /dev/null +++ b/homeassistant/components/media_player/.translations/nl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} is niet actief", + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld", + "is_paused": "{entity_name} is gepauzeerd", + "is_playing": "{entity_name} wordt afgespeeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/nl.json b/homeassistant/components/somfy/.translations/nl.json index be50b280c17ad6..b08bc3431cd1bc 100644 --- a/homeassistant/components/somfy/.translations/nl.json +++ b/homeassistant/components/somfy/.translations/nl.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Succesvol geverifieerd met Somfy." }, + "step": { + "pick_implementation": { + "title": "Kies de authenticatie methode" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/nl.json b/homeassistant/components/transmission/.translations/nl.json index fdf3db99ed0d9b..ccb9c5695623cf 100644 --- a/homeassistant/components/transmission/.translations/nl.json +++ b/homeassistant/components/transmission/.translations/nl.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Host is al geconfigureerd.", "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." }, "error": { "cannot_connect": "Kan geen verbinding maken met host", + "name_exists": "Naam bestaat al", "wrong_credentials": "verkeerde gebruikersnaam of wachtwoord" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Update frequentie" }, - "description": "Configureer opties voor Transmission" + "description": "Configureer opties voor Transmission", + "title": "Configureer de opties voor Transmission" } } } diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json index 2ca98656db7544..c831561a439238 100644 --- a/homeassistant/components/withings/.translations/nl.json +++ b/homeassistant/components/withings/.translations/nl.json @@ -10,7 +10,9 @@ "profile": { "data": { "profile": "Profiel" - } + }, + "description": "Welk profiel hebt u op de website van Withings selecteren? Het is belangrijk dat de profielen overeenkomen, anders worden gegevens verkeerd gelabeld.", + "title": "Gebruikersprofiel." }, "user": { "data": { diff --git a/homeassistant/components/wled/.translations/en.json b/homeassistant/components/wled/.translations/en.json index dde66b8e1225f1..0271f7d2b1e38e 100644 --- a/homeassistant/components/wled/.translations/en.json +++ b/homeassistant/components/wled/.translations/en.json @@ -1,26 +1,26 @@ { - "config": { - "title": "WLED", - "flow_title": "WLED: {name}", - "step": { - "user": { - "title": "Link your WLED", - "description": "Set up your WLED to integrate with Home Assistant.", - "data": { - "host": "Host or IP address" - } - }, - "zeroconf_confirm": { - "description": "Do you want to add the WLED named `{name}` to Home Assistant?", - "title": "Discovered WLED device" - } - }, - "error": { - "connection_error": "Failed to connect to WLED device." - }, - "abort": { - "already_configured": "This WLED device is already configured.", - "connection_error": "Failed to connect to WLED device." + "config": { + "abort": { + "already_configured": "This WLED device is already configured.", + "connection_error": "Failed to connect to WLED device." + }, + "error": { + "connection_error": "Failed to connect to WLED device." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Host or IP address" + }, + "description": "Set up your WLED to integrate with Home Assistant.", + "title": "Link your WLED" + }, + "zeroconf_confirm": { + "description": "Do you want to add the WLED named `{name}` to Home Assistant?", + "title": "Discovered WLED device" + } + }, + "title": "WLED" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index 983a69cdc2c201..c0bc7c176a2a73 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -49,19 +49,19 @@ "trigger_type": { "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0431\u0440\u043e\u0441\u0438\u043b\u0438", "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", - "device_knocked": "\u041f\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \"{subtype}\"", + "device_knocked": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \"{subtype}\" ", "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438", "device_slid": "\u0421\u0434\u0432\u0438\u0433 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \"{subtype}\"", "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u043a\u043b\u043e\u043d\u0438\u043b\u0438", - "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", - "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", - "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", - "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", - "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" + "remote_button_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "remote_button_long_press": "\"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", + "remote_button_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } } } \ No newline at end of file From b5587348f5db495d24ad58d06bd78e2c97bfef6b Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 7 Nov 2019 16:37:29 +1100 Subject: [PATCH 1473/3953] update to latest integration library version (#28597) --- 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 f7aa53b0a3ae50..9996e1d1cb386b 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/integrations/geonetnz_quakes", "requirements": [ - "aio_geojson_geonetnz_quakes==0.10" + "aio_geojson_geonetnz_quakes==0.11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ac6529ff869146..a27f5aa4af2cf4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -124,7 +124,7 @@ adguardhome==0.3.0 afsapi==0.0.4 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.10 +aio_geojson_geonetnz_quakes==0.11 # homeassistant.components.ambient_station aioambient==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 605e244ace1e68..186d2576f728c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -35,7 +35,7 @@ adb-shell==0.0.8 adguardhome==0.3.0 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.10 +aio_geojson_geonetnz_quakes==0.11 # homeassistant.components.ambient_station aioambient==0.3.2 From 68fd39e3215154d4cb56255770879308ba244f54 Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Thu, 7 Nov 2019 01:42:42 -0800 Subject: [PATCH 1474/3953] Upgrade greeneye_monitor to 1.0.1 (#28600) This release has a fix for a crash we were seeing occasionally, and a totally revamped packet handler which should be more robust and CPU-efficient. Fixes #25887 --- homeassistant/components/greeneye_monitor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index 1e9569e850921f..eb5f19bc1eefc1 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -3,7 +3,7 @@ "name": "Greeneye monitor", "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", "requirements": [ - "greeneye_monitor==1.0" + "greeneye_monitor==1.0.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index a27f5aa4af2cf4..ca1ae68e4a6d90 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -595,7 +595,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.greeneye_monitor -greeneye_monitor==1.0 +greeneye_monitor==1.0.1 # homeassistant.components.greenwave greenwavereality==0.5.1 From 11cdce3758fab5a87c755abcae284035e8315cae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 03:24:58 -0800 Subject: [PATCH 1475/3953] Add device actions to vacuum (#28554) --- .../components/vacuum/device_action.py | 72 ++++++++++++++ homeassistant/components/vacuum/strings.json | 8 ++ tests/components/vacuum/test_device_action.py | 98 +++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 homeassistant/components/vacuum/device_action.py create mode 100644 homeassistant/components/vacuum/strings.json create mode 100644 tests/components/vacuum/test_device_action.py diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py new file mode 100644 index 00000000000000..e5f8c162fbdae4 --- /dev/null +++ b/homeassistant/components/vacuum/device_action.py @@ -0,0 +1,72 @@ +"""Provides device automations for Vacuum.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN, SERVICE_START, SERVICE_RETURN_TO_BASE + +ACTION_TYPES = {"clean", "dock"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Vacuum devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "clean", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "dock", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "clean": + service = SERVICE_START + elif config[CONF_TYPE] == "dock": + service = SERVICE_RETURN_TO_BASE + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json new file mode 100644 index 00000000000000..461af9d9794d76 --- /dev/null +++ b/homeassistant/components/vacuum/strings.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "clean": "Let {entity_name} clean", + "dock": "Let {entity_name} return to the dock" + } + } +} diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py new file mode 100644 index 00000000000000..a7c0859408f350 --- /dev/null +++ b/tests/components/vacuum/test_device_action.py @@ -0,0 +1,98 @@ +"""The tests for Vacuum device actions.""" +import pytest + +from homeassistant.components.vacuum import DOMAIN +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a vacuum.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "clean", + "device_id": device_entry.id, + "entity_id": "vacuum.test_5678", + }, + { + "domain": DOMAIN, + "type": "dock", + "device_id": device_entry.id, + "entity_id": "vacuum.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass): + """Test for turn_on and turn_off actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event_dock"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "vacuum.entity", + "type": "dock", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event_clean"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "vacuum.entity", + "type": "clean", + }, + }, + ] + }, + ) + + dock_calls = async_mock_service(hass, "vacuum", "return_to_base") + clean_calls = async_mock_service(hass, "vacuum", "start") + + hass.bus.async_fire("test_event_dock") + await hass.async_block_till_done() + assert len(dock_calls) == 1 + assert len(clean_calls) == 0 + + hass.bus.async_fire("test_event_clean") + await hass.async_block_till_done() + assert len(dock_calls) == 1 + assert len(clean_calls) == 1 From 2c607c836f0d12e6175013b555a7b91d9c0273b3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 03:26:10 -0800 Subject: [PATCH 1476/3953] Add device action to fan (#28550) --- homeassistant/components/fan/device_action.py | 74 +++++++++++++ homeassistant/components/fan/strings.json | 8 ++ tests/components/fan/test_device_action.py | 104 ++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 homeassistant/components/fan/device_action.py create mode 100644 homeassistant/components/fan/strings.json create mode 100644 tests/components/fan/test_device_action.py diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py new file mode 100644 index 00000000000000..b26f632a775ea7 --- /dev/null +++ b/homeassistant/components/fan/device_action.py @@ -0,0 +1,74 @@ +"""Provides device automations for Fan.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + SERVICE_TURN_ON, + SERVICE_TURN_OFF, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN + +ACTION_TYPES = {"turn_on", "turn_off"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Fan devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_on", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turn_off", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "turn_on": + service = SERVICE_TURN_ON + elif config[CONF_TYPE] == "turn_off": + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json new file mode 100644 index 00000000000000..8f5709ac155cae --- /dev/null +++ b/homeassistant/components/fan/strings.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}" + } + } +} \ No newline at end of file diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py new file mode 100644 index 00000000000000..928fd353dd55a2 --- /dev/null +++ b/tests/components/fan/test_device_action.py @@ -0,0 +1,104 @@ +"""The tests for Fan device actions.""" +import pytest + +from homeassistant.components.fan import DOMAIN +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a fan.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "turn_on", + "device_id": device_entry.id, + "entity_id": "fan.test_5678", + }, + { + "domain": DOMAIN, + "type": "turn_off", + "device_id": device_entry.id, + "entity_id": "fan.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass): + """Test for turn_on and turn_off actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_turn_off", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "fan.entity", + "type": "turn_off", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_turn_on", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "fan.entity", + "type": "turn_on", + }, + }, + ] + }, + ) + + turn_off_calls = async_mock_service(hass, "fan", "turn_off") + turn_on_calls = async_mock_service(hass, "fan", "turn_on") + + hass.bus.async_fire("test_event_turn_off") + await hass.async_block_till_done() + assert len(turn_off_calls) == 1 + assert len(turn_on_calls) == 0 + + hass.bus.async_fire("test_event_turn_on") + await hass.async_block_till_done() + assert len(turn_off_calls) == 1 + assert len(turn_on_calls) == 1 From 78657bfbafeafa6a0aad0e71a940f0dd32faf476 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 03:26:59 -0800 Subject: [PATCH 1477/3953] Add lock device triggers (#28547) * Add lock device triggers * Lint --- .../components/lock/device_trigger.py | 89 ++++++++++++ homeassistant/components/lock/strings.json | 4 + tests/components/lock/test_device_trigger.py | 132 ++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 homeassistant/components/lock/device_trigger.py create mode 100644 tests/components/lock/test_device_trigger.py diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py new file mode 100644 index 00000000000000..8732cca29f0a0d --- /dev/null +++ b/homeassistant/components/lock/device_trigger.py @@ -0,0 +1,89 @@ +"""Provides device automations for Lock.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_LOCKED, + STATE_UNLOCKED, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import DOMAIN + +TRIGGER_TYPES = {"locked", "unlocked"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Lock devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add triggers for each entity that belongs to this integration + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "locked", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "unlocked", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == "locked": + from_state = STATE_UNLOCKED + to_state = STATE_LOCKED + else: + from_state = STATE_LOCKED + to_state = STATE_UNLOCKED + + state_config = { + state.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + state_config = state.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index 9c8589164763f3..1645b78295dfa7 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} is locked", "is_unlocked": "{entity_name} is unlocked" + }, + "trigger_type": { + "locked": "{entity_name} locked", + "unlocked": "{entity_name} unlocked" } } } diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py new file mode 100644 index 00000000000000..572e28c44a6ea4 --- /dev/null +++ b/tests/components/lock/test_device_trigger.py @@ -0,0 +1,132 @@ +"""The tests for Lock device triggers.""" +import pytest + +from homeassistant.components.lock import DOMAIN +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a lock.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "locked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "unlocked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set("lock.entity", STATE_UNLOCKED) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "locked", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "locked - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "unlocked", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "unlocked - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is turning on. + hass.states.async_set("lock.entity", STATE_LOCKED) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "locked - device - {} - unlocked - locked - None".format("lock.entity") + + # Fake that the entity is turning off. + hass.states.async_set("lock.entity", STATE_UNLOCKED) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data[ + "some" + ] == "unlocked - device - {} - locked - unlocked - None".format("lock.entity") From 5032c5a04c25f5e2b93b55477342e535922b4b07 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 03:38:58 -0800 Subject: [PATCH 1478/3953] Add fan device trigger (#28545) --- .../components/fan/device_trigger.py | 89 ++++++++++++ homeassistant/components/fan/strings.json | 3 + tests/components/fan/test_device_trigger.py | 132 ++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100644 homeassistant/components/fan/device_trigger.py create mode 100644 tests/components/fan/test_device_trigger.py diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py new file mode 100644 index 00000000000000..3e917e0ae792e3 --- /dev/null +++ b/homeassistant/components/fan/device_trigger.py @@ -0,0 +1,89 @@ +"""Provides device automations for Fan.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_ON, + STATE_OFF, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import DOMAIN + +TRIGGER_TYPES = {"turned_on", "turned_off"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Fan devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add triggers for each entity that belongs to this integration + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turned_on", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "turned_off", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == "turned_on": + from_state = STATE_OFF + to_state = STATE_ON + else: + from_state = STATE_ON + to_state = STATE_OFF + + state_config = { + state.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + state_config = state.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index 8f5709ac155cae..dd1bb715ce7320 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -1,5 +1,8 @@ { "device_automation": { + "trigger_type": { + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" "action_type": { "turn_on": "Turn on {entity_name}", "turn_off": "Turn off {entity_name}" diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py new file mode 100644 index 00000000000000..fa41749cf364e3 --- /dev/null +++ b/tests/components/fan/test_device_trigger.py @@ -0,0 +1,132 @@ +"""The tests for Fan device triggers.""" +import pytest + +from homeassistant.components.fan import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a fan.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "turned_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set("fan.entity", STATE_OFF) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "fan.entity", + "type": "turned_on", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "turn_on - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "fan.entity", + "type": "turned_off", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "turn_off - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is turning on. + hass.states.async_set("fan.entity", STATE_ON) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "turn_on - device - {} - off - on - None".format( + "fan.entity" + ) + + # Fake that the entity is turning off. + hass.states.async_set("fan.entity", STATE_OFF) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "turn_off - device - {} - on - off - None".format( + "fan.entity" + ) From c3f07347a199a6ba7ac1392e55c7d8d13650f2bc Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Thu, 7 Nov 2019 22:39:16 +1100 Subject: [PATCH 1479/3953] Fix simple typo: unhasable -> unhashable (#28605) --- tests/util/test_yaml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index c2bf33f9010179..1e5797e33e7cf4 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -39,7 +39,7 @@ def test_simple_dict(): def test_unhashable_key(): - """Test an unhasable key.""" + """Test an unhashable key.""" files = {YAML_CONFIG_FILE: "message:\n {{ states.state }}"} with pytest.raises(HomeAssistantError), patch_yaml_files(files): load_yaml_config_file(YAML_CONFIG_FILE) From 6999a712ef8262736ec03932519a4324e31fd6b0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 04:39:35 -0800 Subject: [PATCH 1480/3953] Add device triggers to vacuum (#28548) * Add device triggers to vacuum * Update strings --- homeassistant/components/automation/state.py | 4 +- homeassistant/components/vacuum/__init__.py | 2 + .../components/vacuum/device_trigger.py | 86 ++++++++++++ homeassistant/components/vacuum/strings.json | 3 + .../components/vacuum/test_device_trigger.py | 131 ++++++++++++++++++ 5 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/vacuum/device_trigger.py create mode 100644 tests/components/vacuum/test_device_trigger.py diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 154394075a02c6..47c44587b08f03 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -27,8 +27,8 @@ vol.Required(CONF_PLATFORM): "state", vol.Required(CONF_ENTITY_ID): cv.entity_ids, # These are str on purpose. Want to catch YAML conversions - vol.Optional(CONF_FROM): str, - vol.Optional(CONF_TO): str, + vol.Optional(CONF_FROM): vol.Any(str, [str]), + vol.Optional(CONF_TO): vol.Any(str, [str]), vol.Optional(CONF_FOR): vol.Any( vol.All(cv.time_period, cv.positive_timedelta), cv.template, diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 55e56415b0deaf..ace3f6106060aa 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -71,6 +71,8 @@ STATE_RETURNING = "returning" STATE_ERROR = "error" +STATES = [STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, STATE_ERROR] + DEFAULT_NAME = "Vacuum cleaner robot" SUPPORT_TURN_ON = 1 diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py new file mode 100644 index 00000000000000..328db54b1b9d66 --- /dev/null +++ b/homeassistant/components/vacuum/device_trigger.py @@ -0,0 +1,86 @@ +"""Provides device automations for Vacuum.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATES + +TRIGGER_TYPES = {"cleaning", "docked"} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Vacuum devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "cleaning", + } + ) + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "docked", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == "cleaning": + from_state = [state for state in STATES if state != STATE_CLEANING] + to_state = STATE_CLEANING + else: + from_state = [state for state in STATES if state != STATE_DOCKED] + to_state = STATE_DOCKED + + state_config = { + state.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + state_config = state.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 461af9d9794d76..5fca1be5197137 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -1,5 +1,8 @@ { "device_automation": { + "trigger_type": { + "cleaning": "{entity_name} started cleaning", + "docked": "{entity_name} docked" "action_type": { "clean": "Let {entity_name} clean", "dock": "Let {entity_name} return to the dock" diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py new file mode 100644 index 00000000000000..680b6482186441 --- /dev/null +++ b/tests/components/vacuum/test_device_trigger.py @@ -0,0 +1,131 @@ +"""The tests for Vacuum device triggers.""" +import pytest + +from homeassistant.components.vacuum import DOMAIN, STATE_DOCKED, STATE_CLEANING +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a vacuum.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "cleaning", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "docked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set("vacuum.entity", STATE_DOCKED) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "vacuum.entity", + "type": "cleaning", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "cleaning - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "vacuum.entity", + "type": "docked", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "docked - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is cleaning + hass.states.async_set("vacuum.entity", STATE_CLEANING) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "cleaning - device - {} - docked - cleaning".format( + "vacuum.entity" + ) + + # Fake that the entity is docked + hass.states.async_set("vacuum.entity", STATE_DOCKED) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "docked - device - {} - cleaning - docked".format( + "vacuum.entity" + ) From 76aae0c23eb4cbc81601f5ec4707c919c5791cd3 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 7 Nov 2019 13:43:43 +0100 Subject: [PATCH 1481/3953] Fix demo TTS (#28608) --- homeassistant/components/demo/tts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/demo/tts.py b/homeassistant/components/demo/tts.py index 27c9015533edd4..b7be6349d98d99 100644 --- a/homeassistant/components/demo/tts.py +++ b/homeassistant/components/demo/tts.py @@ -16,7 +16,7 @@ def get_engine(hass, config, discovery_info=None): """Set up Demo speech component.""" - return DemoProvider(config[CONF_LANG]) + return DemoProvider(config.get(CONF_LANG, DEFAULT_LANG)) class DemoProvider(Provider): From d34caf50f124eee405197665d58994604c8feb73 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 04:44:59 -0800 Subject: [PATCH 1482/3953] Add climate device actions (#28552) --- .../components/climate/device_action.py | 111 +++++++++++ homeassistant/components/climate/strings.json | 8 + .../components/climate/test_device_action.py | 177 ++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 homeassistant/components/climate/device_action.py create mode 100644 homeassistant/components/climate/strings.json create mode 100644 tests/components/climate/test_device_action.py diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py new file mode 100644 index 00000000000000..b53109f69cb7ad --- /dev/null +++ b/homeassistant/components/climate/device_action.py @@ -0,0 +1,111 @@ +"""Provides device automations for Climate.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN, const + +ACTION_TYPES = {"set_hvac_mode", "set_preset_mode"} + +SET_HVAC_MODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): "set_hvac_mode", + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Required(const.ATTR_HVAC_MODE): vol.In(const.HVAC_MODES), + } +) + +SET_PRESET_MODE_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): "set_preset_mode", + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Required(const.ATTR_PRESET_MODE): str, + } +) + +ACTION_SCHEMA = vol.Any(SET_HVAC_MODE_SCHEMA, SET_PRESET_MODE_SCHEMA) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Climate devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the HVAC and preset modes. + if state is None: + continue + + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "set_hvac_mode", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "set_preset_mode", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "set_hvac_mode": + service = const.SERVICE_SET_HVAC_MODE + service_data[const.ATTR_HVAC_MODE] = config[const.ATTR_HVAC_MODE] + elif config[CONF_TYPE] == "set_preset_mode": + service = const.SERVICE_SET_PRESET_MODE + service_data[const.ATTR_PRESET_MODE] = config[const.ATTR_PRESET_MODE] + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) + + +async def async_get_action_capabilities(hass, config): + """List action capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + action_type = config[CONF_TYPE] + + fields = {} + + if action_type == "set_hvac_mode": + hvac_modes = state.attributes[const.ATTR_HVAC_MODES] if state else [] + fields[vol.Required(const.ATTR_HVAC_MODE)] = vol.In(hvac_modes) + elif action_type == "set_preset_mode": + if state: + preset_modes = state.attributes.get(const.ATTR_PRESET_MODES, []) + else: + preset_modes = [] + fields[vol.Required(const.ATTR_PRESET_MODE)] = vol.In(preset_modes) + + return {"extra_fields": vol.Schema(fields)} diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json new file mode 100644 index 00000000000000..7e97c7701c26db --- /dev/null +++ b/homeassistant/components/climate/strings.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Change HVAC mode on {entity_name}", + "set_preset_mode": "Change preset on {entity_name}" + } + } +} diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py new file mode 100644 index 00000000000000..3eb1f38ec41d00 --- /dev/null +++ b/tests/components/climate/test_device_action.py @@ -0,0 +1,177 @@ +"""The tests for Climate device actions.""" +import pytest +import voluptuous_serialize + +from homeassistant.components.climate import DOMAIN, const, device_action +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry, config_validation as cv + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a climate.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + hass.states.async_set("climate.test_5678", const.HVAC_MODE_COOL, {}) + expected_actions = [ + { + "domain": DOMAIN, + "type": "set_hvac_mode", + "device_id": device_entry.id, + "entity_id": "climate.test_5678", + }, + { + "domain": DOMAIN, + "type": "set_preset_mode", + "device_id": device_entry.id, + "entity_id": "climate.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass): + """Test for actions.""" + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF], + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_hvac_mode", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "climate.entity", + "type": "set_hvac_mode", + "hvac_mode": const.HVAC_MODE_OFF, + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_preset_mode", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "climate.entity", + "type": "set_preset_mode", + "preset_mode": const.PRESET_AWAY, + }, + }, + ] + }, + ) + + set_hvac_mode_calls = async_mock_service(hass, "climate", "set_hvac_mode") + set_preset_mode_calls = async_mock_service(hass, "climate", "set_preset_mode") + + hass.bus.async_fire("test_event_set_hvac_mode") + await hass.async_block_till_done() + assert len(set_hvac_mode_calls) == 1 + assert len(set_preset_mode_calls) == 0 + + hass.bus.async_fire("test_event_set_preset_mode") + await hass.async_block_till_done() + assert len(set_hvac_mode_calls) == 1 + assert len(set_preset_mode_calls) == 1 + + +async def test_capabilities(hass): + """Test getting capabilities.""" + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF], + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + + # Set HVAC mode + capabilities = await device_action.async_get_action_capabilities( + hass, + { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "climate.entity", + "type": "set_hvac_mode", + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "hvac_mode", + "options": [("cool", "cool"), ("off", "off")], + "required": True, + "type": "select", + } + ] + + # Set preset mode + capabilities = await device_action.async_get_action_capabilities( + hass, + { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "climate.entity", + "type": "set_preset_mode", + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "preset_mode", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ] From 50f1b6c689cf10513b42f3f094845f982ef71145 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 7 Nov 2019 13:47:26 +0100 Subject: [PATCH 1483/3953] Fix vacuum strings --- homeassistant/components/vacuum/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 5fca1be5197137..2a6fcbbd357767 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -3,6 +3,7 @@ "trigger_type": { "cleaning": "{entity_name} started cleaning", "docked": "{entity_name} docked" + }, "action_type": { "clean": "Let {entity_name} clean", "dock": "Let {entity_name} return to the dock" From abb78a0d132b0cd5072f5b852c171b3c6c0b0b72 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 7 Nov 2019 13:48:23 +0100 Subject: [PATCH 1484/3953] Fix fan strings --- homeassistant/components/fan/strings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index dd1bb715ce7320..d566c76994ce0b 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -3,9 +3,10 @@ "trigger_type": { "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" + }, "action_type": { "turn_on": "Turn on {entity_name}", "turn_off": "Turn off {entity_name}" } } -} \ No newline at end of file +} From af73e54aee51944c72ce2afd54c90cf617273ab4 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Thu, 7 Nov 2019 14:47:44 +0100 Subject: [PATCH 1485/3953] Add azure servicebus notify service (#27566) * Add azure servicebus notify service * files added to .coveragerc * fix: import content type from const * Moved imports to top level * Code review fixes + added code owner + fixed config validation with has at least one + seperate attributes for dto to asb * fixed doc link * async setup instead of sync * rename all the things - removed too many ifs * changed setup back to sync + comment about sync IO in lib * More informative logging * logging exception -> error --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/azure_service_bus/__init__.py | 1 + .../azure_service_bus/manifest.json | 12 ++ .../components/azure_service_bus/notify.py | 106 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 124 insertions(+) create mode 100644 homeassistant/components/azure_service_bus/__init__.py create mode 100644 homeassistant/components/azure_service_bus/manifest.json create mode 100644 homeassistant/components/azure_service_bus/notify.py diff --git a/.coveragerc b/.coveragerc index e6f09d60effd0c..169b73b7899697 100644 --- a/.coveragerc +++ b/.coveragerc @@ -69,6 +69,7 @@ omit = homeassistant/components/avea/light.py homeassistant/components/avion/light.py homeassistant/components/azure_event_hub/* + homeassistant/components/azure_service_bus/* homeassistant/components/baidu/tts.py homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bbb_gpio/* diff --git a/CODEOWNERS b/CODEOWNERS index 27e06d874e1e71..0a02fbc53218d9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -43,6 +43,7 @@ homeassistant/components/awair/* @danielsjf homeassistant/components/aws/* @awarecan @robbiet480 homeassistant/components/axis/* @kane610 homeassistant/components/azure_event_hub/* @eavanvalkenburg +homeassistant/components/azure_service_bus/* @hfurubotten homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria diff --git a/homeassistant/components/azure_service_bus/__init__.py b/homeassistant/components/azure_service_bus/__init__.py new file mode 100644 index 00000000000000..f18dc9eb66c956 --- /dev/null +++ b/homeassistant/components/azure_service_bus/__init__.py @@ -0,0 +1 @@ +"""The Azure Service Bus integration.""" diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json new file mode 100644 index 00000000000000..fa6d1c20b7fdd9 --- /dev/null +++ b/homeassistant/components/azure_service_bus/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "azure_service_bus", + "name": "Azure Service Bus", + "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", + "requirements": [ + "azure-servicebus==0.50.1" + ], + "dependencies": [], + "codeowners": [ + "@hfurubotten" + ] +} \ No newline at end of file diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py new file mode 100644 index 00000000000000..e7c85adede8f20 --- /dev/null +++ b/homeassistant/components/azure_service_bus/notify.py @@ -0,0 +1,106 @@ +"""Support for azure service bus notification.""" +import json +import logging + +from azure.servicebus.aio import Message, ServiceBusClient +from azure.servicebus.common.errors import ( + MessageSendFailed, + ServiceBusConnectionError, + ServiceBusResourceNotFound, +) +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + ATTR_TITLE, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.const import CONTENT_TYPE_JSON +import homeassistant.helpers.config_validation as cv + +CONF_CONNECTION_STRING = "connection_string" +CONF_QUEUE_NAME = "queue" +CONF_TOPIC_NAME = "topic" + +ATTR_ASB_MESSAGE = "message" +ATTR_ASB_TITLE = "title" +ATTR_ASB_TARGET = "target" + +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_QUEUE_NAME, CONF_TOPIC_NAME), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_CONNECTION_STRING): cv.string, + vol.Exclusive( + CONF_QUEUE_NAME, "output", "Can only send to a queue or a topic." + ): cv.string, + vol.Exclusive( + CONF_TOPIC_NAME, "output", "Can only send to a queue or a topic." + ): cv.string, + } + ), +) + +_LOGGER = logging.getLogger(__name__) + + +def get_service(hass, config, discovery_info=None): + """Get the notification service.""" + connection_string = config[CONF_CONNECTION_STRING] + queue_name = config.get(CONF_QUEUE_NAME) + topic_name = config.get(CONF_TOPIC_NAME) + + # Library can do synchronous IO when creating the clients. + # Passes in loop here, but can't run setup on the event loop. + servicebus = ServiceBusClient.from_connection_string( + connection_string, loop=hass.loop + ) + + try: + if queue_name: + client = servicebus.get_queue(queue_name) + else: + client = servicebus.get_topic(topic_name) + except (ServiceBusConnectionError, ServiceBusResourceNotFound) as err: + _LOGGER.error( + "Connection error while creating client for queue/topic '%s'. %s", + queue_name or topic_name, + err, + ) + return None + + return ServiceBusNotificationService(client) + + +class ServiceBusNotificationService(BaseNotificationService): + """Implement the notification service for the service bus service.""" + + def __init__(self, client): + """Initialize the service.""" + self._client = client + + async def async_send_message(self, message, **kwargs): + """Send a message.""" + dto = {ATTR_ASB_MESSAGE: message} + + if ATTR_TITLE in kwargs: + dto[ATTR_ASB_TITLE] = kwargs[ATTR_TITLE] + if ATTR_TARGET in kwargs: + dto[ATTR_ASB_TARGET] = kwargs[ATTR_TARGET] + + data = kwargs.get(ATTR_DATA) + if data: + dto.update(data) + + queue_message = Message(json.dumps(dto)) + queue_message.properties.content_type = CONTENT_TYPE_JSON + try: + await self._client.send(queue_message) + except MessageSendFailed as err: + _LOGGER.error( + "Could not send service bus notification to %s. %s", + self._client.name, + err, + ) diff --git a/requirements_all.txt b/requirements_all.txt index ca1ae68e4a6d90..8843a6693ab176 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -263,6 +263,9 @@ axis==25 # homeassistant.components.azure_event_hub azure-eventhub==1.3.1 +# homeassistant.components.azure_service_bus +azure-servicebus==0.50.1 + # homeassistant.components.baidu baidu-aip==1.6.6 From 48660585f1d699a7825af5c72e00a6f9cd4419ff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 07:28:45 -0800 Subject: [PATCH 1486/3953] Add climate device triggers (#28544) * Add climate device triggers * Test capabilities --- .../components/automation/template.py | 6 +- homeassistant/components/climate/__init__.py | 4 +- homeassistant/components/climate/const.py | 2 +- .../components/climate/device_trigger.py | 192 ++++++++++++++ homeassistant/components/climate/strings.json | 5 + .../components/homekit/type_thermostats.py | 4 +- .../components/climate/test_device_trigger.py | 243 ++++++++++++++++++ tests/components/demo/test_climate.py | 6 +- .../homekit/test_type_thermostats.py | 28 +- tests/components/smartthings/test_climate.py | 8 +- 10 files changed, 470 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/climate/device_trigger.py create mode 100644 tests/components/climate/test_device_trigger.py diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index f2b4134de42282..95b6b857c9de8f 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -28,7 +28,9 @@ ) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="numeric_state" +): """Listen for state changes based on configuration.""" value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = hass @@ -65,7 +67,7 @@ def call_action(): variables = { "trigger": { - "platform": "template", + "platform": platform_type, "entity_id": entity_id, "from_state": from_s, "to_state": to_s, diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index af67be5eccccd1..0c8d6103b12383 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -35,7 +35,7 @@ ATTR_FAN_MODE, ATTR_FAN_MODES, ATTR_HUMIDITY, - ATTR_HVAC_ACTIONS, + ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, @@ -239,7 +239,7 @@ def state_attributes(self) -> Dict[str, Any]: data[ATTR_FAN_MODES] = self.fan_modes if self.hvac_action: - data[ATTR_HVAC_ACTIONS] = self.hvac_action + data[ATTR_HVAC_ACTION] = self.hvac_action if supported_features & SUPPORT_PRESET_MODE: data[ATTR_PRESET_MODE] = self.preset_mode diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index 4012aa8be1bcd3..26cec7efbeb65f 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -97,7 +97,7 @@ ATTR_MIN_HUMIDITY = "min_humidity" ATTR_MAX_TEMP = "max_temp" ATTR_MIN_TEMP = "min_temp" -ATTR_HVAC_ACTIONS = "hvac_action" +ATTR_HVAC_ACTION = "hvac_action" ATTR_HVAC_MODES = "hvac_modes" ATTR_HVAC_MODE = "hvac_mode" ATTR_SWING_MODES = "swing_modes" diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py new file mode 100644 index 00000000000000..e814bdc88de49b --- /dev/null +++ b/homeassistant/components/climate/device_trigger.py @@ -0,0 +1,192 @@ +"""Provides device automations for Climate.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + CONF_DOMAIN, + CONF_TYPE, + CONF_PLATFORM, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + CONF_FOR, + CONF_ABOVE, + CONF_BELOW, +) +from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType +from homeassistant.components.automation import ( + state as state_automation, + numeric_state as numeric_state_automation, + AutomationActionType, +) +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from . import DOMAIN, const + +TRIGGER_TYPES = { + "current_temperature_changed", + "current_humidity_changed", + "hvac_mode_changed", +} + +HVAC_MODE_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): "hvac_mode_changed", + vol.Required(state_automation.CONF_TO): vol.In(const.HVAC_MODES), + } +) + +CURRENT_TRIGGER_SCHEMA = vol.All( + TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In( + ["current_temperature_changed", "current_humidity_changed"] + ), + vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + +TRIGGER_SCHEMA = vol.Any(HVAC_MODE_TRIGGER_SCHEMA, CURRENT_TRIGGER_SCHEMA) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Climate devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + # Add triggers for each entity that belongs to this integration + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "hvac_mode_changed", + } + ) + + if state and const.ATTR_CURRENT_TEMPERATURE in state.attributes: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "current_temperature_changed", + } + ) + + if state and const.ATTR_CURRENT_HUMIDITY in state.attributes: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "current_humidity_changed", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + trigger_type = config[CONF_TYPE] + + if trigger_type == "hvac_mode_changed": + state_config = { + state_automation.CONF_PLATFORM: "state", + state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_automation.CONF_TO: config[state_automation.CONF_TO], + state_automation.CONF_FROM: [ + mode + for mode in const.HVAC_MODES + if mode != config[state_automation.CONF_TO] + ], + } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] + state_config = state_automation.TRIGGER_SCHEMA(state_config) + return await state_automation.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) + + numeric_state_config = { + numeric_state_automation.CONF_PLATFORM: "numeric_state", + numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + } + + if trigger_type == "current_temperature_changed": + numeric_state_config[ + numeric_state_automation.CONF_VALUE_TEMPLATE + ] = "{{ state.attributes.current_temperature }}" + else: + numeric_state_config[ + numeric_state_automation.CONF_VALUE_TEMPLATE + ] = "{{ state.attributes.current_humidity }}" + + if CONF_ABOVE in config: + numeric_state_config[CONF_ABOVE] = config[CONF_ABOVE] + if CONF_BELOW in config: + numeric_state_config[CONF_BELOW] = config[CONF_BELOW] + if CONF_FOR in config: + numeric_state_config[CONF_FOR] = config[CONF_FOR] + + numeric_state_config = numeric_state_automation.TRIGGER_SCHEMA(numeric_state_config) + return await numeric_state_automation.async_attach_trigger( + hass, numeric_state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_trigger_capabilities(hass: HomeAssistant, config): + """List trigger capabilities.""" + trigger_type = config[CONF_TYPE] + + if trigger_type == "hvac_action_changed": + return None + + if trigger_type == "hvac_mode_changed": + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } + + if trigger_type == "current_temperature_changed": + unit_of_measurement = hass.config.units.temperature_unit + else: + unit_of_measurement = "%" + + return { + "extra_fields": vol.Schema( + { + vol.Optional( + CONF_ABOVE, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional( + CONF_BELOW, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ) + } diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index 7e97c7701c26db..84854c713cf71b 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -1,5 +1,10 @@ { "device_automation": { + "trigger_type": { + "current_temperature_changed": "{entity_name} measured temperature changed", + "current_humidity_changed": "{entity_name} measured humidity changed", + "hvac_mode_changed": "{entity_name} HVAC mode changed" + }, "action_type": { "set_hvac_mode": "Change HVAC mode on {entity_name}", "set_preset_mode": "Change preset on {entity_name}" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 63eb688a0c1866..9adc3cc0600bf5 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -5,7 +5,7 @@ from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, - ATTR_HVAC_ACTIONS, + ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, @@ -293,7 +293,7 @@ def update_state(self, new_state): self._flag_heat_cool = False # Set current operation mode for supported thermostats - hvac_action = new_state.attributes.get(ATTR_HVAC_ACTIONS) + hvac_action = new_state.attributes.get(ATTR_HVAC_ACTION) if hvac_action: self.char_current_heat_cool.set_value( HC_HASS_TO_HOMEKIT_ACTION[hvac_action] diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py new file mode 100644 index 00000000000000..3b497912c52bd5 --- /dev/null +++ b/tests/components/climate/test_device_trigger.py @@ -0,0 +1,243 @@ +"""The tests for Climate device triggers.""" +import voluptuous_serialize +import pytest + +from homeassistant.components.climate import DOMAIN, const, device_trigger +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry, config_validation as cv + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a climate device.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + entity_id = f"{DOMAIN}.test_5678" + hass.states.async_set( + entity_id, + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_ACTION: const.CURRENT_HVAC_IDLE, + const.ATTR_CURRENT_HUMIDITY: 23, + const.ATTR_CURRENT_TEMPERATURE: 18, + }, + ) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "hvac_mode_changed", + "device_id": device_entry.id, + "entity_id": entity_id, + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "current_temperature_changed", + "device_id": device_entry.id, + "entity_id": entity_id, + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "current_humidity_changed", + "device_id": device_entry.id, + "entity_id": entity_id, + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_ACTION: const.CURRENT_HVAC_IDLE, + const.ATTR_CURRENT_HUMIDITY: 23, + const.ATTR_CURRENT_TEMPERATURE: 18, + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "hvac_mode_changed", + "to": const.HVAC_MODE_AUTO, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "hvac_mode_changed"}, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "current_temperature_changed", + "above": 20, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "current_temperature_changed"}, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "current_humidity_changed", + "below": 10, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "current_humidity_changed"}, + }, + }, + ] + }, + ) + + # Fake that the HVAC mode is changing + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_AUTO, + { + const.ATTR_HVAC_ACTION: const.CURRENT_HVAC_COOL, + const.ATTR_CURRENT_HUMIDITY: 23, + const.ATTR_CURRENT_TEMPERATURE: 18, + }, + ) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "hvac_mode_changed" + + # Fake that the temperature is changing + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_AUTO, + { + const.ATTR_HVAC_ACTION: const.CURRENT_HVAC_COOL, + const.ATTR_CURRENT_HUMIDITY: 23, + const.ATTR_CURRENT_TEMPERATURE: 23, + }, + ) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "current_temperature_changed" + + # Fake that the humidity is changing + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_AUTO, + { + const.ATTR_HVAC_ACTION: const.CURRENT_HVAC_COOL, + const.ATTR_CURRENT_HUMIDITY: 7, + const.ATTR_CURRENT_TEMPERATURE: 23, + }, + ) + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "current_humidity_changed" + + +async def test_get_trigger_capabilities_hvac_mode(hass): + """Test we get the expected capabilities from a climate trigger.""" + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": "climate", + "type": "hvac_mode_changed", + "entity_id": "climate.upstairs", + "to": "heat", + }, + ) + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [{"name": "for", "optional": True, "type": "positive_time_period_dict"}] + + +@pytest.mark.parametrize( + "type", ["current_temperature_changed", "current_humidity_changed"] +) +async def test_get_trigger_capabilities_temp_humid(hass, type): + """Test we get the expected capabilities from a climate trigger.""" + capabilities = await device_trigger.async_get_trigger_capabilities( + hass, + { + "platform": "device", + "domain": "climate", + "type": "current_temperature_changed", + "entity_id": "climate.upstairs", + "above": "23", + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "description": {"suffix": "°C"}, + "name": "above", + "optional": True, + "type": "float", + }, + { + "description": {"suffix": "°C"}, + "name": "below", + "optional": True, + "type": "float", + }, + {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + ] diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index eef03b2370a5c4..2d1b7a85ff8e68 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -9,7 +9,7 @@ ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_HUMIDITY, - ATTR_HVAC_ACTIONS, + ATTR_HVAC_ACTION, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP, @@ -233,7 +233,7 @@ async def test_set_hvac_bad_attr_and_state(hass): Also check the state. """ state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_HVAC_ACTIONS) == CURRENT_HVAC_COOL + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_COOL assert state.state == HVAC_MODE_COOL with pytest.raises(vol.Invalid): @@ -241,7 +241,7 @@ async def test_set_hvac_bad_attr_and_state(hass): await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get(ATTR_HVAC_ACTIONS) == CURRENT_HVAC_COOL + assert state.attributes.get(ATTR_HVAC_ACTION) == CURRENT_HVAC_COOL assert state.state == HVAC_MODE_COOL diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 8ad46e489d6c20..c896ad211e8685 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -6,7 +6,7 @@ from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, - ATTR_HVAC_ACTIONS, + ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_TEMP, @@ -92,7 +92,7 @@ async def test_thermostat(hass, hk_driver, cls, events): { ATTR_TEMPERATURE: 22.2, ATTR_CURRENT_TEMPERATURE: 17.8, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, }, ) await hass.async_block_till_done() @@ -108,7 +108,7 @@ async def test_thermostat(hass, hk_driver, cls, events): { ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 23.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() @@ -124,7 +124,7 @@ async def test_thermostat(hass, hk_driver, cls, events): { ATTR_TEMPERATURE: 20.0, ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, }, ) await hass.async_block_till_done() @@ -140,7 +140,7 @@ async def test_thermostat(hass, hk_driver, cls, events): { ATTR_TEMPERATURE: 20.0, ATTR_CURRENT_TEMPERATURE: 19.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() @@ -169,7 +169,7 @@ async def test_thermostat(hass, hk_driver, cls, events): ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, }, ) await hass.async_block_till_done() @@ -186,7 +186,7 @@ async def test_thermostat(hass, hk_driver, cls, events): ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, }, ) await hass.async_block_till_done() @@ -203,7 +203,7 @@ async def test_thermostat(hass, hk_driver, cls, events): ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() @@ -265,7 +265,7 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): ATTR_TARGET_TEMP_HIGH: 22.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, }, ) await hass.async_block_till_done() @@ -284,7 +284,7 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 24.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_COOL, + ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, }, ) await hass.async_block_till_done() @@ -303,7 +303,7 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() @@ -349,7 +349,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_HEAT, + ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, }, ) await hass.async_block_till_done() @@ -367,7 +367,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() @@ -381,7 +381,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): ATTR_HVAC_MODE: HVAC_MODE_OFF, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_HVAC_ACTIONS: CURRENT_HVAC_IDLE, + ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, }, ) await hass.async_block_till_done() diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index c366761ea1f1bf..630174a0661396 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -13,7 +13,7 @@ ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_FAN_MODES, - ATTR_HVAC_ACTIONS, + ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_TARGET_TEMP_HIGH, @@ -214,7 +214,7 @@ async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_TEMPERATURE ) - assert state.attributes[ATTR_HVAC_ACTIONS] == CURRENT_HVAC_IDLE + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_AUTO, HVAC_MODE_COOL, @@ -238,7 +238,7 @@ async def test_basic_thermostat_entity_state(hass, basic_thermostat): state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_TEMPERATURE ) - assert ATTR_HVAC_ACTIONS not in state.attributes + assert ATTR_HVAC_ACTION not in state.attributes assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -259,7 +259,7 @@ async def test_thermostat_entity_state(hass, thermostat): | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_TEMPERATURE ) - assert state.attributes[ATTR_HVAC_ACTIONS] == CURRENT_HVAC_IDLE + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_AUTO, HVAC_MODE_COOL, From a80baf2e5fe9a7973c0fc52a0755979c4ce9a57c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 07:29:10 -0800 Subject: [PATCH 1487/3953] Add fan device condition (#28549) --- .../components/fan/device_condition.py | 80 +++++++++++ homeassistant/components/fan/strings.json | 4 + tests/components/fan/test_device_condition.py | 126 ++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 homeassistant/components/fan/device_condition.py create mode 100644 tests/components/fan/test_device_condition.py diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py new file mode 100644 index 00000000000000..8b567fcd4c9935 --- /dev/null +++ b/homeassistant/components/fan/device_condition.py @@ -0,0 +1,80 @@ +"""Provide the device automations for Fan.""" +from typing import Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +CONDITION_TYPES = {"is_on", "is_off"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions for Fan devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_on", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_off", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_on": + state = STATE_ON + else: + state = STATE_OFF + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index d566c76994ce0b..134119f41ffff4 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -1,5 +1,9 @@ { "device_automation": { + "condtion_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" + }, "trigger_type": { "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py new file mode 100644 index 00000000000000..ea87e36b636071 --- /dev/null +++ b/tests/components/fan/test_device_condition.py @@ -0,0 +1,126 @@ +"""The tests for Fan device conditions.""" +import pytest + +from homeassistant.components.fan import DOMAIN +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a fan.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_off", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_on", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("fan.entity", STATE_ON) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "fan.entity", + "type": "is_on", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_on - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "fan.entity", + "type": "is_off", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_on - event - test_event1" + + hass.states.async_set("fan.entity", STATE_OFF) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_off - event - test_event2" From 899306c8ec9e17af1405d4639532929423ba1989 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 07:29:39 -0800 Subject: [PATCH 1488/3953] Add vacuum device conditions (#28551) --- .../components/vacuum/device_condition.py | 79 ++++++++++ homeassistant/components/vacuum/strings.json | 4 + .../vacuum/test_device_condition.py | 138 ++++++++++++++++++ 3 files changed, 221 insertions(+) create mode 100644 homeassistant/components/vacuum/device_condition.py create mode 100644 tests/components/vacuum/test_device_condition.py diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py new file mode 100644 index 00000000000000..6a41fe0490e13e --- /dev/null +++ b/homeassistant/components/vacuum/device_condition.py @@ -0,0 +1,79 @@ +"""Provide the device automations for Vacuum.""" +from typing import Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN, STATE_DOCKED, STATE_CLEANING, STATE_RETURNING + +CONDITION_TYPES = {"is_cleaning", "is_docked"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions for Vacuum devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_cleaning", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_docked", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_docked": + test_states = [STATE_DOCKED] + else: + test_states = [STATE_CLEANING, STATE_RETURNING] + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + state = hass.states.get(config[ATTR_ENTITY_ID]) + return state is not None and state.state in test_states + + return test_is_state diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 2a6fcbbd357767..0300242a506da4 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -1,5 +1,9 @@ { "device_automation": { + "condtion_type": { + "is_docked": "{entity_name} is docked", + "is_cleaning": "{entity_name} is cleaning" + }, "trigger_type": { "cleaning": "{entity_name} started cleaning", "docked": "{entity_name} docked" diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py new file mode 100644 index 00000000000000..80e7b72c36fb34 --- /dev/null +++ b/tests/components/vacuum/test_device_condition.py @@ -0,0 +1,138 @@ +"""The tests for Vacuum device conditions.""" +import pytest + +from homeassistant.components.vacuum import ( + DOMAIN, + STATE_CLEANING, + STATE_DOCKED, + STATE_RETURNING, +) +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a vacuum.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_cleaning", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_docked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("vacuum.entity", STATE_DOCKED) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "vacuum.entity", + "type": "is_cleaning", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_cleaning - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "vacuum.entity", + "type": "is_docked", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_docked - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_docked - event - test_event2" + + hass.states.async_set("vacuum.entity", STATE_CLEANING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_cleaning - event - test_event1" + + # Returning means it's still cleaning + hass.states.async_set("vacuum.entity", STATE_RETURNING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "is_cleaning - event - test_event1" From 9d3d35ad793724a39909de4aabc872bd9845d847 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Thu, 7 Nov 2019 16:41:33 +0100 Subject: [PATCH 1489/3953] Add cool mode to HomematicIP climate (#28525) * Add cool mode to HomematicIP climate * Update test * remove preset_party * Fix profile_names check --- .../components/homematicip_cloud/climate.py | 152 ++++++++++---- .../homematicip_cloud/test_climate.py | 192 ++++++++++++++++-- tests/fixtures/homematicip_cloud.json | 6 +- 3 files changed, 292 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 74d647c8c33613..a8ea424b2072d5 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -4,13 +4,15 @@ from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup -from homematicip.base.enums import AbsenceType, GroupType +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_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, PRESET_ECO, @@ -30,6 +32,9 @@ _LOGGER = logging.getLogger(__name__) +ATTR_PRESET_END_TIME = "preset_end_time" +PERMANENT_END_TIME = "permanent" + HMIP_AUTOMATIC_CM = "AUTOMATIC" HMIP_MANUAL_CM = "MANUAL" HMIP_ECO_CM = "ECO" @@ -55,15 +60,20 @@ async def async_setup_entry( class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): - """Representation of a HomematicIP heating group.""" + """Representation of a HomematicIP heating group. + + Heat mode is supported for all heating devices incl. their defined profiles. + Boost is available for radiator thermostats only. + Cool mode is only available for floor heating systems, if basically enabled in the hmip app. + """ def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None: """Initialize heating group.""" device.modelType = "HmIP-Heating-Group" + super().__init__(hap, device) self._simple_heating = None if device.actualTemperature is None: - self._simple_heating = _get_first_heating_thermostat(device) - super().__init__(hap, device) + self._simple_heating = self._get_first_radiator_thermostat() @property def device_info(self): @@ -105,54 +115,66 @@ def current_humidity(self) -> int: @property def hvac_mode(self) -> str: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ + """Return hvac operation ie.""" + if self._disabled_by_cooling_mode: + return HVAC_MODE_OFF if self._device.boostMode: return HVAC_MODE_HEAT if self._device.controlMode == HMIP_MANUAL_CM: - return HVAC_MODE_HEAT + return HVAC_MODE_HEAT if self._heat_mode_enabled else HVAC_MODE_COOL return HVAC_MODE_AUTO @property def hvac_modes(self): - """Return the list of available hvac operation modes. + """Return the list of available hvac operation modes.""" + if self._disabled_by_cooling_mode: + return [HVAC_MODE_OFF] - Need to be a subset of HVAC_MODES. - """ - return [HVAC_MODE_AUTO, HVAC_MODE_HEAT] + return ( + [HVAC_MODE_AUTO, HVAC_MODE_HEAT] + if self._heat_mode_enabled + else [HVAC_MODE_AUTO, HVAC_MODE_COOL] + ) @property def preset_mode(self): - """Return the current preset mode, e.g., home, away, temp. - - Requires SUPPORT_PRESET_MODE. - """ + """Return the current preset mode.""" if self._device.boostMode: return PRESET_BOOST - if self.hvac_mode == HVAC_MODE_HEAT: + if self.hvac_mode in (HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF): return PRESET_NONE if self._device.controlMode == HMIP_ECO_CM: - absence_type = self._home.get_functionalHome(IndoorClimateHome).absenceType - if absence_type == AbsenceType.VACATION: + if self._indoor_climate.absenceType == AbsenceType.VACATION: return PRESET_AWAY - if absence_type in [ + if self._indoor_climate.absenceType in [ + AbsenceType.PARTY, AbsenceType.PERIOD, AbsenceType.PERMANENT, - AbsenceType.PARTY, ]: return PRESET_ECO - if self._device.activeProfile: - return self._device.activeProfile.name + return ( + self._device.activeProfile.name + if self._device.activeProfile.name in self._device_profile_names + else None + ) @property def preset_modes(self): - """Return a list of available preset modes incl profiles.""" - presets = [PRESET_NONE, PRESET_BOOST] - presets.extend(self._device_profile_names) + """Return a list of available preset modes incl. hmip profiles.""" + # Boost is only available if a radiator thermostat is in the room, + # and heat mode is enabled. + profile_names = self._device_profile_names + + presets = [] + if self._heat_mode_enabled and self._has_radiator_thermostat: + if not profile_names: + presets.append(PRESET_NONE) + presets.append(PRESET_BOOST) + + presets.extend(profile_names) + return presets @property @@ -170,10 +192,15 @@ async def async_set_temperature(self, **kwargs): temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - await self._device.set_point_temperature(temperature) + + if self.min_temp <= temperature <= self.max_temp: + await self._device.set_point_temperature(temperature) async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: """Set new target hvac mode.""" + if hvac_mode not in self.hvac_modes: + return + if hvac_mode == HVAC_MODE_AUTO: await self._device.set_control_mode(HMIP_AUTOMATIC_CM) else: @@ -181,18 +208,44 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: """Set new preset mode.""" + if preset_mode not in self.preset_modes: + return + if self._device.boostMode and preset_mode != PRESET_BOOST: await self._device.set_boost(False) if preset_mode == PRESET_BOOST: await self._device.set_boost() if preset_mode in self._device_profile_names: profile_idx = self._get_profile_idx_by_name(preset_mode) - await self.async_set_hvac_mode(HVAC_MODE_AUTO) + if self._device.controlMode != HMIP_AUTOMATIC_CM: + await self.async_set_hvac_mode(HVAC_MODE_AUTO) await self._device.set_active_profile(profile_idx) + @property + def device_state_attributes(self): + """Return the state attributes of the access point.""" + state_attr = super().device_state_attributes + + if self._device.controlMode == HMIP_ECO_CM: + if self._indoor_climate.absenceType in [ + AbsenceType.PARTY, + AbsenceType.PERIOD, + AbsenceType.VACATION, + ]: + state_attr[ATTR_PRESET_END_TIME] = self._indoor_climate.absenceEndTime + elif self._indoor_climate.absenceType == AbsenceType.PERMANENT: + state_attr[ATTR_PRESET_END_TIME] = PERMANENT_END_TIME + + return state_attr + + @property + def _indoor_climate(self): + """Return the hmip indoor climate functional home of this group.""" + return self._home.get_functionalHome(IndoorClimateHome) + @property def _device_profiles(self): - """Return the relevant profiles of the device.""" + """Return the relevant profiles.""" return [ profile for profile in self._device.profiles @@ -217,18 +270,37 @@ def _get_profile_idx_by_name(self, profile_name): return relevant_index[index_name[0]] + @property + def _heat_mode_enabled(self): + """Return, if heating mode is enabled.""" + return not self._device.cooling + + @property + def _disabled_by_cooling_mode(self): + """Return, if group is disabled by the cooling mode.""" + return self._device.cooling and ( + self._device.coolingIgnored or not self._device.coolingAllowed + ) + @property def _relevant_profile_group(self): """Return the relevant profile groups.""" - return ( - HEATING_PROFILES - if self._device.groupType == GroupType.HEATING - else COOLING_PROFILES - ) + if self._disabled_by_cooling_mode: + return [] + return HEATING_PROFILES if self._heat_mode_enabled else COOLING_PROFILES -def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): - """Return the first HeatingThermostat from a HeatingGroup.""" - for device in heating_group.devices: - if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): - return device + @property + def _has_radiator_thermostat(self) -> bool: + """Return, if a radiator thermostat is in the hmip heating group.""" + return bool(self._get_first_radiator_thermostat()) + + def _get_first_radiator_thermostat(self): + """Return the first radiator thermostat from the hmip heating group.""" + for device in self._device.devices: + if isinstance( + device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact) + ): + return device + + return None diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 80e4e74e4519df..6a05a88086464f 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -10,13 +10,18 @@ ATTR_PRESET_MODE, ATTR_PRESET_MODES, HVAC_MODE_AUTO, + HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, PRESET_ECO, - PRESET_NONE, ) from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.components.homematicip_cloud.climate import ( + ATTR_PRESET_END_TIME, + PERMANENT_END_TIME, +) from homeassistant.setup import async_setup_component from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics @@ -33,7 +38,7 @@ async def test_manually_configured_platform(hass): assert not hass.data.get(HMIPC_DOMAIN) -async def test_hmip_heating_group(hass, default_mock_hap): +async def test_hmip_heating_group_heat(hass, default_mock_hap): """Test HomematicipHeatingGroup.""" entity_id = "climate.badezimmer" entity_name = "Badezimmer" @@ -50,12 +55,7 @@ async def test_hmip_heating_group(hass, default_mock_hap): assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes["current_humidity"] == 47 assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" - assert ha_state.attributes[ATTR_PRESET_MODES] == [ - PRESET_NONE, - PRESET_BOOST, - "STD", - "Winter", - ] + assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_BOOST, "STD", "Winter"] service_call_counter = len(hmip_device.mock_calls) @@ -114,12 +114,12 @@ async def test_hmip_heating_group(hass, default_mock_hap): await hass.services.async_call( "climate", "set_preset_mode", - {"entity_id": entity_id, "preset_mode": PRESET_NONE}, + {"entity_id": entity_id, "preset_mode": "STD"}, blocking=True, ) - assert len(hmip_device.mock_calls) == service_call_counter + 9 - assert hmip_device.mock_calls[-1][0] == "set_boost" - assert hmip_device.mock_calls[-1][1] == (False,) + assert len(hmip_device.mock_calls) == service_call_counter + 11 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (0,) await async_manipulate_test_data(hass, hmip_device, "boostMode", False) ha_state = hass.states.get(entity_id) assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" @@ -132,7 +132,7 @@ async def test_hmip_heating_group(hass, default_mock_hap): blocking=True, ) # No new service call should be in mock_calls. - assert len(hmip_device.mock_calls) == service_call_counter + 10 + assert len(hmip_device.mock_calls) == service_call_counter + 12 # Only fire event from last async_manipulate_test_data available. assert hmip_device.mock_calls[-1][0] == "fire_update_event" @@ -158,7 +158,6 @@ async def test_hmip_heating_group(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO - # Not required for hmip, but a posiblity to send no temperature. await hass.services.async_call( "climate", "set_preset_mode", @@ -166,9 +165,172 @@ async def test_hmip_heating_group(hass, default_mock_hap): blocking=True, ) - assert len(hmip_device.mock_calls) == service_call_counter + 16 + assert len(hmip_device.mock_calls) == service_call_counter + 18 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + + default_mock_hap.home.get_functionalHome( + IndoorClimateHome + ).absenceType = AbsenceType.PERMANENT + await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") + + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_END_TIME] == PERMANENT_END_TIME + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 20 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("MANUAL",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "MANUAL") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_HEAT + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Winter"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 23 assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) + hmip_device.activeProfile = hmip_device.profiles[0] + await async_manipulate_test_data(hass, hmip_device, "controlMode", "AUTOMATIC") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": "dry"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 24 + # Only fire event from last async_manipulate_test_data available. + assert hmip_device.mock_calls[-1][0] == "fire_update_event" + + +async def test_hmip_heating_group_cool(hass, default_mock_hap): + """Test HomematicipHeatingGroup.""" + entity_id = "climate.badezimmer" + entity_name = "Badezimmer" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + hmip_device.activeProfile = hmip_device.profiles[3] + await async_manipulate_test_data(hass, hmip_device, "cooling", True) + await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", True) + await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", False) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes["current_temperature"] == 23.8 + assert ha_state.attributes["min_temp"] == 5.0 + assert ha_state.attributes["max_temp"] == 30.0 + assert ha_state.attributes["temperature"] == 5.0 + assert ha_state.attributes["current_humidity"] == 47 + assert ha_state.attributes[ATTR_PRESET_MODE] == "Cool1" + assert ha_state.attributes[ATTR_PRESET_MODES] == ["Cool1", "Cool2"] + + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_COOL}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("MANUAL",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "MANUAL") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_COOL + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_AUTO}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("AUTOMATIC",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "AUTO") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Cool2"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 6 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (4,) + + hmip_device.activeProfile = hmip_device.profiles[4] + await async_manipulate_test_data(hass, hmip_device, "cooling", True) + await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", False) + await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", False) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == HVAC_MODE_OFF + assert ha_state.attributes[ATTR_PRESET_MODE] == "none" + assert ha_state.attributes[ATTR_PRESET_MODES] == [] + + hmip_device.activeProfile = hmip_device.profiles[4] + await async_manipulate_test_data(hass, hmip_device, "cooling", True) + await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", True) + await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", True) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == HVAC_MODE_OFF + assert ha_state.attributes[ATTR_PRESET_MODE] == "none" + assert ha_state.attributes[ATTR_PRESET_MODES] == [] + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Cool2"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 12 + # fire_update_event shows that set_active_profile has not been called. + assert hmip_device.mock_calls[-1][0] == "fire_update_event" + + hmip_device.activeProfile = hmip_device.profiles[4] + await async_manipulate_test_data(hass, hmip_device, "cooling", True) + await async_manipulate_test_data(hass, hmip_device, "coolingAllowed", True) + await async_manipulate_test_data(hass, hmip_device, "coolingIgnored", False) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes[ATTR_PRESET_MODE] == "Cool2" + assert ha_state.attributes[ATTR_PRESET_MODES] == ["Cool1", "Cool2"] + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Cool2"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 17 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (4,) async def test_hmip_climate_services(hass, mock_hap_with_service): diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index e17df9c2039347..d1ef82f423452c 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -4662,7 +4662,7 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000021", "index": "PROFILE_4", - "name": "", + "name": "Cool1", "profileId": "00000000-0000-0000-0000-000000000041", "visible": true }, @@ -4670,9 +4670,9 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000021", "index": "PROFILE_5", - "name": "", + "name": "Cool2", "profileId": "00000000-0000-0000-0000-000000000042", - "visible": false + "visible": true }, "PROFILE_6": { "enabled": true, From 9b5fa2e67cf5208e9618a79b3e5b5559d1f495fe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 08:03:06 -0800 Subject: [PATCH 1490/3953] Add device conditions to climate (#28553) * Add device conditions to climate * Update strings.json --- .../components/climate/device_condition.py | 117 +++++++++ homeassistant/components/climate/strings.json | 4 + .../climate/test_device_condition.py | 234 ++++++++++++++++++ 3 files changed, 355 insertions(+) create mode 100644 homeassistant/components/climate/device_condition.py create mode 100644 tests/components/climate/test_device_condition.py diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py new file mode 100644 index 00000000000000..c923f3123f1515 --- /dev/null +++ b/homeassistant/components/climate/device_condition.py @@ -0,0 +1,117 @@ +"""Provide the device automations for Climate.""" +from typing import Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN, const + +CONDITION_TYPES = {"is_hvac_mode", "is_preset_mode"} + +HVAC_MODE_CONDITION = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): "is_hvac_mode", + vol.Required(const.ATTR_HVAC_MODE): vol.In(const.HVAC_MODES), + } +) + +PRESET_MODE_CONDITION = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): "is_preset_mode", + vol.Required(const.ATTR_PRESET_MODE): str, + } +) + +CONDITION_SCHEMA = vol.Any(HVAC_MODE_CONDITION, PRESET_MODE_CONDITION) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions for Climate devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_hvac_mode", + } + ) + + if state and const.ATTR_PRESET_MODES in state.attributes: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_preset_mode", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + + if config[CONF_TYPE] == "is_hvac_mode": + attribute = const.ATTR_HVAC_MODE + else: + attribute = const.ATTR_PRESET_MODE + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + state = hass.states.get(config[ATTR_ENTITY_ID]) + return state and state.attributes.get(attribute) == config[attribute] + + return test_is_state + + +async def async_get_condition_capabilities(hass, config): + """List condition capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + condition_type = config[CONF_TYPE] + + fields = {} + + if condition_type == "is_hvac_mode": + hvac_modes = state.attributes[const.ATTR_HVAC_MODES] if state else [] + fields[vol.Required(const.ATTR_HVAC_MODE)] = vol.In(hvac_modes) + + elif condition_type == "is_preset_mode": + if state: + preset_modes = state.attributes.get(const.ATTR_PRESET_MODES, []) + else: + preset_modes = [] + + fields[vol.Required(const.ATTR_PRESET_MODES)] = vol.In(preset_modes) + + return {"extra_fields": vol.Schema(fields)} diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index 84854c713cf71b..a2ceeff2143853 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -1,5 +1,9 @@ { "device_automation": { + "condtion_type": { + "is_hvac_mode": "{entity_name} is set to a specific HVAC mode", + "is_preset_mode": "{entity_name} is set to a specific preset mode" + }, "trigger_type": { "current_temperature_changed": "{entity_name} measured temperature changed", "current_humidity_changed": "{entity_name} measured humidity changed", diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py new file mode 100644 index 00000000000000..82b6f595fb0b62 --- /dev/null +++ b/tests/components/climate/test_device_condition.py @@ -0,0 +1,234 @@ +"""The tests for Climate device conditions.""" +import pytest +import voluptuous_serialize + +from homeassistant.components.climate import DOMAIN, const, device_condition +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry, config_validation as cv + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a climate.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + hass.states.async_set( + f"{DOMAIN}.test_5678", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_hvac_mode", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_preset_mode", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + }, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "is_hvac_mode", + "hvac_mode": "cool", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_hvac_mode - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "is_preset_mode", + "preset_mode": "away", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_preset_mode - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_hvac_mode - event - test_event1" + + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_AUTO, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_AUTO, + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + }, + ) + + # Should not fire + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + + assert len(calls) == 2 + assert calls[1].data["some"] == "is_preset_mode - event - test_event2" + + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_AUTO, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_AUTO, + const.ATTR_PRESET_MODE: const.PRESET_HOME, + }, + ) + + # Should not fire + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + + +async def test_capabilities(hass): + """Bla.""" + hass.states.async_set( + "climate.entity", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + const.ATTR_HVAC_MODES: [const.HVAC_MODE_COOL, const.HVAC_MODE_OFF], + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + + # Test hvac mode + capabilities = await device_condition.async_get_condition_capabilities( + hass, + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "is_hvac_mode", + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "hvac_mode", + "options": [("cool", "cool"), ("off", "off")], + "required": True, + "type": "select", + } + ] + + # Test preset mode + capabilities = await device_condition.async_get_condition_capabilities( + hass, + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "climate.entity", + "type": "is_preset_mode", + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [ + { + "name": "preset_modes", + "options": [("home", "home"), ("away", "away")], + "required": True, + "type": "select", + } + ] From fadb6a39790c688501d8415d94e0047aca40057b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Nov 2019 12:21:12 -0800 Subject: [PATCH 1491/3953] Add support for conversation ID (#28620) --- homeassistant/components/almond/__init__.py | 7 +++++-- homeassistant/components/conversation/__init__.py | 14 +++++++++----- homeassistant/components/conversation/agent.py | 5 ++++- .../components/conversation/default_agent.py | 5 ++++- tests/components/conversation/test_init.py | 14 ++++++++++++-- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 115e0d24de465a..5eb305e6795d4f 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging import time +from typing import Optional import async_timeout from aiohttp import ClientSession, ClientError @@ -205,9 +206,11 @@ def __init__(self, api: WebAlmondAPI): """Initialize the agent.""" self.api = api - async def async_process(self, text: str) -> intent.IntentResponse: + async def async_process( + self, text: str, conversation_id: Optional[str] = None + ) -> intent.IntentResponse: """Process a sentence.""" - response = await self.api.async_converse_text(text) + response = await self.api.async_converse_text(text, conversation_id) buffer = "" for message in response["messages"]: diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 798fc926e0f69c..f875ec2822c271 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -53,7 +53,7 @@ def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent): async def async_setup(hass, config): """Register the process service.""" - async def process(hass, text): + async def process(hass, text, conversation_id): """Process a line of text.""" agent = hass.data.get(DATA_AGENT) @@ -61,14 +61,14 @@ async def process(hass, text): agent = hass.data[DATA_AGENT] = DefaultAgent(hass) await agent.async_initialize(config) - return await agent.async_process(text) + return await agent.async_process(text, conversation_id) async def handle_service(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) try: - await process(hass, text) + await process(hass, text, service.context.id) except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) @@ -91,13 +91,17 @@ def __init__(self, process): """Initialize the conversation process view.""" self._process = process - @RequestDataValidator(vol.Schema({vol.Required("text"): str})) + @RequestDataValidator( + vol.Schema({vol.Required("text"): str, vol.Optional("conversation_id"): str}) + ) async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] try: - intent_result = await self._process(hass, data["text"]) + intent_result = await self._process( + hass, data["text"], data.get("conversation_id") + ) except intent.IntentHandleError as err: intent_result = intent.IntentResponse() intent_result.async_set_speech(str(err)) diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index eae6402530ce8d..1875ab5b9b9b7c 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -1,5 +1,6 @@ """Agent foundation for conversation integration.""" from abc import ABC, abstractmethod +from typing import Optional from homeassistant.helpers import intent @@ -8,5 +9,7 @@ class AbstractConversationAgent(ABC): """Abstract conversation agent.""" @abstractmethod - async def async_process(self, text: str) -> intent.IntentResponse: + async def async_process( + self, text: str, conversation_id: Optional[str] = None + ) -> intent.IntentResponse: """Process a sentence.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index e93afcfaf65ffa..c202cdf1e6591c 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -1,6 +1,7 @@ """Standard conversastion implementation for Home Assistant.""" import logging import re +from typing import Optional from homeassistant import core from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER @@ -107,7 +108,9 @@ def register_utterances(self, component): for intent_type, sentences in UTTERANCES[component].items(): async_register(self.hass, intent_type, sentences) - async def async_process(self, text) -> intent.IntentResponse: + async def async_process( + self, text: str, conversation_id: Optional[str] = None + ) -> intent.IntentResponse: """Process a sentence.""" intents = self.hass.data[DOMAIN] diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index a9116ac0d98b75..ff44eaccc8ea8a 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -266,11 +266,14 @@ async def test_http_api_wrong_data(hass, hass_client): async def test_custom_agent(hass, hass_client): """Test a custom conversation agent.""" + calls = [] + class MyAgent(conversation.AbstractConversationAgent): """Test Agent.""" - async def async_process(self, text): + async def async_process(self, text, conversation_id): """Process some text.""" + calls.append((text, conversation_id)) response = intent.IntentResponse() response.async_set_speech("Test response") return response @@ -281,9 +284,16 @@ async def async_process(self, text): client = await hass_client() - resp = await client.post("/api/conversation/process", json={"text": "Test Text"}) + resp = await client.post( + "/api/conversation/process", + json={"text": "Test Text", "conversation_id": "test-conv-id"}, + ) assert resp.status == 200 assert await resp.json() == { "card": {}, "speech": {"plain": {"extra_data": None, "speech": "Test response"}}, } + + assert len(calls) == 1 + assert calls[0][0] == "Test Text" + assert calls[0][1] == "test-conv-id" From 2bdfa9928bce20ecf238c03944f7fee1eff3c26a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 7 Nov 2019 14:54:48 -0600 Subject: [PATCH 1492/3953] Allow to skip SSL validation on Plex websocket (#28615) --- homeassistant/components/plex/__init__.py | 5 ++++- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 1aaa8a8e3aa5fc..4a575722826ee5 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -160,7 +160,10 @@ def update_plex(): async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) session = async_get_clientsession(hass) - websocket = PlexWebsocket(plex_server.plex_server, update_plex, session) + verify_ssl = server_config.get(CONF_VERIFY_SSL) + websocket = PlexWebsocket( + plex_server.plex_server, update_plex, session=session, verify_ssl=verify_ssl + ) hass.loop.create_task(websocket.listen()) hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 8edccda75e0d7e..b7179174ea496c 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==3.0.6", "plexauth==0.0.5", - "plexwebsocket==0.0.3" + "plexwebsocket==0.0.4" ], "dependencies": [ "http" diff --git a/requirements_all.txt b/requirements_all.txt index 8843a6693ab176..2efb1db7ba052b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -983,7 +983,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.3 +plexwebsocket==0.0.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 186d2576f728c5..f29eee878d9bf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.3 +plexwebsocket==0.0.4 # homeassistant.components.mhz19 # homeassistant.components.serial_pm From 10122157093d9c5787d2eb8200c76573b67874e4 Mon Sep 17 00:00:00 2001 From: "Brett T. Warden" Date: Thu, 7 Nov 2019 14:03:32 -0800 Subject: [PATCH 1493/3953] Match ALARM in NUT UPS status message (#28591) If ups.status contains "ALARM", add "Alarm" to virtual sensor ups.status.display. Fixes #28580 --- homeassistant/components/nut/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index c7865f8c0f179c..db485734777e50 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -133,6 +133,7 @@ "TRIM": "Trimming Voltage", "BOOST": "Boosting Voltage", "FSD": "Forced Shutdown", + "ALARM": "Alarm", } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( From a71d852f16b94d5ac892a5c2e31bcad9140bd410 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 7 Nov 2019 14:04:59 -0800 Subject: [PATCH 1494/3953] Use friendly app names for Fire TV sources (#28417) * Use friendly app names for Fire TV sources * Remove debugging statement * Tests pass * Use 'blocking=True' to patch service calls * Remove parentheses --- .../components/androidtv/media_player.py | 31 +++-- tests/components/androidtv/patchers.py | 12 ++ .../components/androidtv/test_media_player.py | 114 +++++++++++++++++- 3 files changed, 145 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 60f423c3dee14e..7540973ea1999b 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -287,8 +287,11 @@ def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): """Initialize the Android TV / Fire TV device.""" self.aftv = aftv self._name = name - self._apps = APPS.copy() - self._apps.update(apps) + self._app_id_to_name = APPS.copy() + self._app_id_to_name.update(apps) + self._app_name_to_id = { + value: key for key, value in self._app_id_to_name.items() + } self._keys = KEYS self._device_properties = self.aftv.device_properties @@ -328,7 +331,7 @@ def app_id(self): @property def app_name(self): """Return the friendly name of the current app.""" - return self._apps.get(self._current_app, self._current_app) + return self._app_id_to_name.get(self._current_app, self._current_app) @property def available(self): @@ -518,7 +521,7 @@ def __init__( super().__init__(aftv, name, apps, turn_on_command, turn_off_command) self._get_sources = get_sources - self._running_apps = None + self._sources = None @adb_decorator(override_available=True) def update(self): @@ -538,23 +541,28 @@ def update(self): return # Get the `state`, `current_app`, and `running_apps`. - state, self._current_app, self._running_apps = self.aftv.update( - self._get_sources - ) + state, self._current_app, running_apps = self.aftv.update(self._get_sources) self._state = ANDROIDTV_STATES.get(state) if self._state is None: self._available = False + if running_apps: + self._sources = [ + self._app_id_to_name.get(app_id, app_id) for app_id in running_apps + ] + else: + self._sources = None + @property def source(self): """Return the current app.""" - return self._current_app + return self._app_id_to_name.get(self._current_app, self._current_app) @property def source_list(self): """Return a list of running apps.""" - return self._running_apps + return self._sources @property def supported_features(self): @@ -575,6 +583,7 @@ def select_source(self, source): """ if isinstance(source, str): if not source.startswith("!"): - self.aftv.launch_app(source) + self.aftv.launch_app(self._app_name_to_id.get(source, source)) else: - self.aftv.stop_app(source[1:].lstrip()) + source_ = source[1:].lstrip() + self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 5fc6bc754fa1bd..986180bf214ee0 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -140,3 +140,15 @@ def isfile(filepath): PATCH_ISFILE = patch("os.path.isfile", isfile) PATCH_ACCESS = patch("os.access", return_value=True) + + +def patch_firetv_update(state, current_app, running_apps): + """Patch the `FireTV.update()` method.""" + return patch( + "androidtv.firetv.FireTV.update", + return_value=(state, current_app, running_apps), + ) + + +PATCH_LAUNCH_APP = patch("androidtv.firetv.FireTV.launch_app") +PATCH_STOP_APP = patch("androidtv.firetv.FireTV.stop_app") diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 85f562a3500288..04b0bebf447ece 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -6,15 +6,22 @@ ANDROIDTV_DOMAIN, CONF_ADB_SERVER_IP, CONF_ADBKEY, + CONF_APPS, +) +from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + DOMAIN, + SERVICE_SELECT_SOURCE, ) -from homeassistant.components.media_player.const import DOMAIN from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_DEVICE_CLASS, CONF_HOST, CONF_NAME, CONF_PLATFORM, STATE_IDLE, STATE_OFF, + STATE_PLAYING, STATE_UNAVAILABLE, ) @@ -276,3 +283,108 @@ async def test_setup_with_adbkey(hass): state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF + + +async def test_firetv_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Fire TV devices.""" + config = CONFIG_FIRETV_ADB_SERVER.copy() + config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} + patch_key, entity_id = _setup(hass, config) + + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + with patchers.patch_firetv_update( + "playing", "com.app.test1", ["com.app.test1", "com.app.test2"] + ): + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_PLAYING + assert state.attributes["source"] == "TEST 1" + assert state.attributes["source_list"] == ["TEST 1", "com.app.test2"] + + with patchers.patch_firetv_update( + "playing", "com.app.test2", ["com.app.test2", "com.app.test1"] + ): + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_PLAYING + assert state.attributes["source"] == "com.app.test2" + assert state.attributes["source_list"] == ["com.app.test2", "TEST 1"] + + +async def _test_firetv_select_source(hass, source, expected_arg, method_patch): + """Test that the `FireTV.launch_app` and `FireTV.stop_app` methods are called with the right parameter.""" + config = CONFIG_FIRETV_ADB_SERVER.copy() + config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} + patch_key, entity_id = _setup(hass, config) + + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + with method_patch as method_patch_: + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: entity_id, ATTR_INPUT_SOURCE: source}, + blocking=True, + ) + method_patch_.assert_called_with(expected_arg) + + return True + + +async def test_firetv_select_source_launch_app_id(hass): + """Test that an app can be launched using its app ID.""" + assert await _test_firetv_select_source( + hass, "com.app.test1", "com.app.test1", patchers.PATCH_LAUNCH_APP + ) + + +async def test_firetv_select_source_launch_app_name(hass): + """Test that an app can be launched using its friendly name.""" + assert await _test_firetv_select_source( + hass, "TEST 1", "com.app.test1", patchers.PATCH_LAUNCH_APP + ) + + +async def test_firetv_select_source_launch_app_id_no_name(hass): + """Test that an app can be launched using its app ID when it has no friendly name.""" + assert await _test_firetv_select_source( + hass, "com.app.test2", "com.app.test2", patchers.PATCH_LAUNCH_APP + ) + + +async def test_firetv_select_source_stop_app_id(hass): + """Test that an app can be stopped using its app ID.""" + assert await _test_firetv_select_source( + hass, "!com.app.test1", "com.app.test1", patchers.PATCH_STOP_APP + ) + + +async def test_firetv_select_source_stop_app_name(hass): + """Test that an app can be stopped using its friendly name.""" + assert await _test_firetv_select_source( + hass, "!TEST 1", "com.app.test1", patchers.PATCH_STOP_APP + ) + + +async def test_firetv_select_source_stop_app_id_no_name(hass): + """Test that an app can be stopped using its app ID when it has no friendly name.""" + assert await _test_firetv_select_source( + hass, "!com.app.test2", "com.app.test2", patchers.PATCH_STOP_APP + ) From 64166583b3ac75298925dde81beecc1e3444a322 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 8 Nov 2019 00:32:12 +0000 Subject: [PATCH 1495/3953] [ci skip] Translation update --- .../almond/.translations/zh-Hant.json | 3 +- .../components/climate/.translations/en.json | 17 ++++++++++ .../components/climate/.translations/it.json | 17 ++++++++++ .../climate/.translations/zh-Hant.json | 8 +++++ .../coolmaster/.translations/de.json | 11 ++++++ .../components/deconz/.translations/fr.json | 7 ++++ .../components/deconz/.translations/it.json | 14 +++++++- .../device_tracker/.translations/de.json | 8 +++++ .../components/fan/.translations/de.json | 16 +++++++++ .../components/fan/.translations/en.json | 16 +++++++++ .../components/fan/.translations/it.json | 16 +++++++++ .../components/fan/.translations/ru.json | 12 +++++++ .../components/fan/.translations/zh-Hant.json | 12 +++++++ .../huawei_lte/.translations/de.json | 34 +++++++++++++++++++ .../huawei_lte/.translations/it.json | 2 +- .../components/lock/.translations/de.json | 9 +++++ .../components/lock/.translations/en.json | 4 +++ .../components/lock/.translations/it.json | 4 +++ .../components/lock/.translations/ru.json | 4 +++ .../lock/.translations/zh-Hant.json | 4 +++ .../media_player/.translations/de.json | 9 +++++ .../components/sensor/.translations/de.json | 2 +- .../components/somfy/.translations/de.json | 5 +++ .../transmission/.translations/de.json | 2 ++ .../components/vacuum/.translations/de.json | 7 ++++ .../components/vacuum/.translations/en.json | 16 +++++++++ .../components/vacuum/.translations/it.json | 16 +++++++++ .../components/vacuum/.translations/ru.json | 8 +++++ .../vacuum/.translations/zh-Hant.json | 12 +++++++ .../components/withings/.translations/de.json | 6 ++++ .../components/wled/.translations/ca.json | 17 ++++++++++ .../components/wled/.translations/de.json | 6 ++++ .../components/wled/.translations/it.json | 25 ++++++++++++++ .../components/wled/.translations/ru.json | 26 ++++++++++++++ .../wled/.translations/zh-Hant.json | 26 ++++++++++++++ 35 files changed, 397 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/climate/.translations/en.json create mode 100644 homeassistant/components/climate/.translations/it.json create mode 100644 homeassistant/components/climate/.translations/zh-Hant.json create mode 100644 homeassistant/components/coolmaster/.translations/de.json create mode 100644 homeassistant/components/device_tracker/.translations/de.json create mode 100644 homeassistant/components/fan/.translations/de.json create mode 100644 homeassistant/components/fan/.translations/en.json create mode 100644 homeassistant/components/fan/.translations/it.json create mode 100644 homeassistant/components/fan/.translations/ru.json create mode 100644 homeassistant/components/fan/.translations/zh-Hant.json create mode 100644 homeassistant/components/huawei_lte/.translations/de.json create mode 100644 homeassistant/components/media_player/.translations/de.json create mode 100644 homeassistant/components/vacuum/.translations/de.json create mode 100644 homeassistant/components/vacuum/.translations/en.json create mode 100644 homeassistant/components/vacuum/.translations/it.json create mode 100644 homeassistant/components/vacuum/.translations/ru.json create mode 100644 homeassistant/components/vacuum/.translations/zh-Hant.json create mode 100644 homeassistant/components/wled/.translations/ca.json create mode 100644 homeassistant/components/wled/.translations/de.json create mode 100644 homeassistant/components/wled/.translations/it.json create mode 100644 homeassistant/components/wled/.translations/ru.json create mode 100644 homeassistant/components/wled/.translations/zh-Hant.json diff --git a/homeassistant/components/almond/.translations/zh-Hant.json b/homeassistant/components/almond/.translations/zh-Hant.json index c84b2dd432bf52..743835b1046562 100644 --- a/homeassistant/components/almond/.translations/zh-Hant.json +++ b/homeassistant/components/almond/.translations/zh-Hant.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Almond \u5e33\u865f\u3002", - "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002" + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002", + "missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002" }, "title": "Almond" } diff --git a/homeassistant/components/climate/.translations/en.json b/homeassistant/components/climate/.translations/en.json new file mode 100644 index 00000000000000..942d9a2761ff8d --- /dev/null +++ b/homeassistant/components/climate/.translations/en.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Change HVAC mode on {entity_name}", + "set_preset_mode": "Change preset on {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} is set to a specific HVAC mode", + "is_preset_mode": "{entity_name} is set to a specific preset mode" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} measured humidity changed", + "current_temperature_changed": "{entity_name} measured temperature changed", + "hvac_mode_changed": "{entity_name} HVAC mode changed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/it.json b/homeassistant/components/climate/.translations/it.json new file mode 100644 index 00000000000000..34ecbf5e9f2aa2 --- /dev/null +++ b/homeassistant/components/climate/.translations/it.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Cambia modalit\u00e0 HVAC su {entity_name}", + "set_preset_mode": "Modifica preimpostazione su {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 HVAC specifica", + "is_preset_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 preimpostata specifica" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} umidit\u00e0 misurata modificata", + "current_temperature_changed": "{entity_name} temperatura misurata cambiata", + "hvac_mode_changed": "{entity_name} modalit\u00e0 HVAC modificata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/zh-Hant.json b/homeassistant/components/climate/.translations/zh-Hant.json new file mode 100644 index 00000000000000..a1d603c5552130 --- /dev/null +++ b/homeassistant/components/climate/.translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "\u8b8a\u66f4 {entity_name} HVAC \u6a21\u5f0f", + "set_preset_mode": "\u8b8a\u66f4 {entity_name} \u8a2d\u5b9a\u6a21\u5f0f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/de.json b/homeassistant/components/coolmaster/.translations/de.json new file mode 100644 index 00000000000000..66c6911cf105cd --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/de.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "off": "Kann ausgeschaltet werden" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 3b29dbf486deb5..4d49bd18d1e1c5 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -55,10 +55,17 @@ "left": "Gauche", "open": "Ouvert", "right": "Droite", + "side_1": "Face 1", + "side_2": "Face 2", + "side_3": "Face 3", + "side_4": "Face 4", + "side_5": "Face 5", + "side_6": "Face 6", "turn_off": "\u00c9teint", "turn_on": "Allum\u00e9" }, "trigger_type": { + "remote_awakened": "Appareil r\u00e9veill\u00e9", "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9", "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", "remote_button_long_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9 apr\u00e8s appui long", diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 975d69a450f75a..e2e0d52906428c 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -55,10 +55,17 @@ "left": "Sinistra", "open": "Aperto", "right": "Destra", + "side_1": "Lato 1", + "side_2": "Lato 2", + "side_3": "Lato 3", + "side_4": "Lato 4", + "side_5": "Lato 5", + "side_6": "Lato 6", "turn_off": "Spegnere", "turn_on": "Accendere" }, "trigger_type": { + "remote_awakened": "Dispositivo risvegliato", "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", @@ -69,7 +76,12 @@ "remote_button_short_press": "Pulsante \"{subtype}\" premuto", "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", - "remote_gyro_activated": "Dispositivo in vibrazione" + "remote_double_tap": "Dispositivo \"{subtype}\" toccato due volte", + "remote_falling": "Dispositivo in caduta libera", + "remote_gyro_activated": "Dispositivo in vibrazione", + "remote_moved": "Dispositivo spostato con \"{subtype}\" verso l'alto", + "remote_rotate_from_side_1": "Dispositivo ruotato da \"lato 1\" a \"{subtype}\"", + "remote_rotate_from_side_2": "Dispositivo ruotato da \"lato 2\" a \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/de.json b/homeassistant/components/device_tracker/.translations/de.json new file mode 100644 index 00000000000000..7e72bd5595a998 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/de.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} ist Zuhause", + "is_not_home": "{entity_name} ist nicht zu Hause" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/de.json b/homeassistant/components/fan/.translations/de.json new file mode 100644 index 00000000000000..9ac3d9993706b9 --- /dev/null +++ b/homeassistant/components/fan/.translations/de.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Schalte {entity_name} aus.", + "turn_on": "Schalte {entity_name} ein." + }, + "condtion_type": { + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet" + }, + "trigger_type": { + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/en.json b/homeassistant/components/fan/.translations/en.json new file mode 100644 index 00000000000000..b085e7baa45a04 --- /dev/null +++ b/homeassistant/components/fan/.translations/en.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Turn off {entity_name}", + "turn_on": "Turn on {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on" + }, + "trigger_type": { + "turned_off": "{entity_name} turned off", + "turned_on": "{entity_name} turned on" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/it.json b/homeassistant/components/fan/.translations/it.json new file mode 100644 index 00000000000000..b62d80c793b8fe --- /dev/null +++ b/homeassistant/components/fan/.translations/it.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Spegnere {entity_name}", + "turn_on": "Accendere {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} \u00e8 spento", + "is_on": "{entity_name} \u00e8 acceso" + }, + "trigger_type": { + "turned_off": "{entity_name} disattivato", + "turned_on": "{entity_name} attivato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/ru.json b/homeassistant/components/fan/.translations/ru.json new file mode 100644 index 00000000000000..abbecabd13f097 --- /dev/null +++ b/homeassistant/components/fan/.translations/ru.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/zh-Hant.json b/homeassistant/components/fan/.translations/zh-Hant.json new file mode 100644 index 00000000000000..58496f9e9bc3e1 --- /dev/null +++ b/homeassistant/components/fan/.translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u95dc\u9589 {entity_name}", + "turn_on": "\u958b\u555f {entity_name}" + }, + "trigger_type": { + "turned_off": "{entity_name} \u5df2\u95dc\u9589", + "turned_on": "{entity_name} \u5df2\u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/de.json b/homeassistant/components/huawei_lte/.translations/de.json new file mode 100644 index 00000000000000..c3f4025b8b62c1 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert", + "already_in_progress": "Dieses Ger\u00e4t wurde bereits konfiguriert" + }, + "error": { + "connection_failed": "Verbindung fehlgeschlagen.", + "connection_timeout": "Verbindungszeit\u00fcberschreitung", + "incorrect_password": "Ung\u00fcltiges Passwort", + "incorrect_username": "Ung\u00fcltiger Benutzername", + "incorrect_username_or_password": "Ung\u00fcltiger Benutzername oder Kennwort", + "invalid_url": "Ung\u00fcltige URL" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "url": "URL", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "SMS-Benachrichtigungsempf\u00e4nger" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/it.json b/homeassistant/components/huawei_lte/.translations/it.json index 0be2ed234621a5..0646cd4da52982 100644 --- a/homeassistant/components/huawei_lte/.translations/it.json +++ b/homeassistant/components/huawei_lte/.translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Questo dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Questo dispositivo \u00e8 gi\u00e0 stato configurato" }, "error": { "connection_failed": "Connessione fallita", diff --git a/homeassistant/components/lock/.translations/de.json b/homeassistant/components/lock/.translations/de.json index 02c387ff48782e..443c70b68ddf38 100644 --- a/homeassistant/components/lock/.translations/de.json +++ b/homeassistant/components/lock/.translations/de.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "lock": "Sperre {entity_name}", + "open": "\u00d6ffne {entity_name}", + "unlock": "Entsperre {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} ist gesperrt", "is_unlocked": "{entity_name} ist entsperrt" + }, + "trigger_type": { + "locked": "{entity_name} gesperrt", + "unlocked": "{entity_name} entsperrt" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/en.json b/homeassistant/components/lock/.translations/en.json index a9800eecadd77d..262ba27d9516fb 100644 --- a/homeassistant/components/lock/.translations/en.json +++ b/homeassistant/components/lock/.translations/en.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} is locked", "is_unlocked": "{entity_name} is unlocked" + }, + "trigger_type": { + "locked": "{entity_name} locked", + "unlocked": "{entity_name} unlocked" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/it.json b/homeassistant/components/lock/.translations/it.json index 05f0db78cdf43e..72c350dad242d3 100644 --- a/homeassistant/components/lock/.translations/it.json +++ b/homeassistant/components/lock/.translations/it.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} \u00e8 bloccato", "is_unlocked": "{entity_name} \u00e8 sbloccato" + }, + "trigger_type": { + "locked": "{entity_name} \u00e8 bloccato", + "unlocked": "{entity_name} \u00e8 sbloccato" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ru.json b/homeassistant/components/lock/.translations/ru.json index 1610668721f30b..479fa4bee21720 100644 --- a/homeassistant/components/lock/.translations/ru.json +++ b/homeassistant/components/lock/.translations/ru.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_unlocked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" + }, + "trigger_type": { + "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", + "unlocked": "{entity_name} \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/zh-Hant.json b/homeassistant/components/lock/.translations/zh-Hant.json index 7c8abb76e16f71..b5d69a21f9af83 100644 --- a/homeassistant/components/lock/.translations/zh-Hant.json +++ b/homeassistant/components/lock/.translations/zh-Hant.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} \u5df2\u4e0a\u9396", "is_unlocked": "{entity_name} \u5df2\u89e3\u9396" + }, + "trigger_type": { + "locked": "{entity_name} \u5df2\u4e0a\u9396", + "unlocked": "{entity_name} \u5df2\u89e3\u9396" } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/de.json b/homeassistant/components/media_player/.translations/de.json new file mode 100644 index 00000000000000..42fdc22f3773f3 --- /dev/null +++ b/homeassistant/components/media_player/.translations/de.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet", + "is_paused": "{entity_name} ist pausiert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json index bf28653c0ce000..7d5a322ba18720 100644 --- a/homeassistant/components/sensor/.translations/de.json +++ b/homeassistant/components/sensor/.translations/de.json @@ -8,7 +8,7 @@ "is_pressure": "{entity_name} Druck", "is_signal_strength": "{entity_name} Signalst\u00e4rke", "is_temperature": "{entity_name} Temperatur", - "is_timestamp": "{entity_name} Zeitstempel", + "is_timestamp": "Aktueller Zeitstempel von {entity_name}", "is_value": "{entity_name} Wert" }, "trigger_type": { diff --git a/homeassistant/components/somfy/.translations/de.json b/homeassistant/components/somfy/.translations/de.json index 1dd1b7b4448223..b950a439ef3909 100644 --- a/homeassistant/components/somfy/.translations/de.json +++ b/homeassistant/components/somfy/.translations/de.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Erfolgreich mit Somfy authentifiziert." }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json index 1a2fa4a48c06d2..1847c7186dbd51 100644 --- a/homeassistant/components/transmission/.translations/de.json +++ b/homeassistant/components/transmission/.translations/de.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "Host ist bereits konfiguriert.", "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." }, "error": { "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "name_exists": "Name existiert bereits", "wrong_credentials": "Falscher Benutzername oder Kennwort" }, "step": { diff --git a/homeassistant/components/vacuum/.translations/de.json b/homeassistant/components/vacuum/.translations/de.json new file mode 100644 index 00000000000000..060358a0a7a41b --- /dev/null +++ b/homeassistant/components/vacuum/.translations/de.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "condtion_type": { + "is_cleaning": "{entity_name} reinigt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/en.json b/homeassistant/components/vacuum/.translations/en.json new file mode 100644 index 00000000000000..396c6a83be996f --- /dev/null +++ b/homeassistant/components/vacuum/.translations/en.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Let {entity_name} clean", + "dock": "Let {entity_name} return to the dock" + }, + "condtion_type": { + "is_cleaning": "{entity_name} is cleaning", + "is_docked": "{entity_name} is docked" + }, + "trigger_type": { + "cleaning": "{entity_name} started cleaning", + "docked": "{entity_name} docked" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/it.json b/homeassistant/components/vacuum/.translations/it.json new file mode 100644 index 00000000000000..0b879f154fa064 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/it.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Lascia pulire {entity_name}", + "dock": "Lascia che {entity_name} ritorni alla base" + }, + "condtion_type": { + "is_cleaning": "{entity_name} sta pulendo", + "is_docked": "{entity_name} \u00e8 agganciato alla base" + }, + "trigger_type": { + "cleaning": "{entity_name} ha iniziato la pulizia", + "docked": "{entity_name} agganciato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/ru.json b/homeassistant/components/vacuum/.translations/ru.json new file mode 100644 index 00000000000000..3026bac3012fea --- /dev/null +++ b/homeassistant/components/vacuum/.translations/ru.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_cleaning": "{entity_name} \u0434\u0435\u043b\u0430\u0435\u0442 \u0443\u0431\u043e\u0440\u043a\u0443", + "is_docked": "{entity_name} \u0443 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/zh-Hant.json b/homeassistant/components/vacuum/.translations/zh-Hant.json new file mode 100644 index 00000000000000..d6831f81873eb8 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "clean": "\u555f\u52d5 {entity_name} \u6e05\u9664", + "dock": "\u555f\u52d5 {entity_name} \u56de\u5230\u5145\u96fb\u7ad9" + }, + "trigger_type": { + "cleaning": "{entity_name} \u958b\u59cb\u6e05\u6383", + "docked": "{entity_name} \u5df2\u56de\u5145\u96fb\u7ad9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json index dabf184d7ed36a..557518d97e8623 100644 --- a/homeassistant/components/withings/.translations/de.json +++ b/homeassistant/components/withings/.translations/de.json @@ -7,6 +7,12 @@ "default": "Erfolgreiche Authentifizierung mit Withings f\u00fcr das ausgew\u00e4hlte Profil." }, "step": { + "profile": { + "data": { + "profile": "Profil" + }, + "title": "Benutzerprofil" + }, "user": { "data": { "profile": "Profil" diff --git a/homeassistant/components/wled/.translations/ca.json b/homeassistant/components/wled/.translations/ca.json new file mode 100644 index 00000000000000..b86eefd62c83c4 --- /dev/null +++ b/homeassistant/components/wled/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 o adre\u00e7a IP" + }, + "title": "Enlla\u00e7a el teu WLED" + }, + "zeroconf_confirm": { + "title": "Dispositiu WLED descobert" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/de.json b/homeassistant/components/wled/.translations/de.json new file mode 100644 index 00000000000000..f50a24eeac06b8 --- /dev/null +++ b/homeassistant/components/wled/.translations/de.json @@ -0,0 +1,6 @@ +{ + "config": { + "flow_title": "WLED: {name}", + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/it.json b/homeassistant/components/wled/.translations/it.json new file mode 100644 index 00000000000000..03c24101c2a2f8 --- /dev/null +++ b/homeassistant/components/wled/.translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "connection_error": "Impossibile connettersi al dispositivo WLED." + }, + "error": { + "connection_error": "Impossibile connettersi al dispositivo WLED." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Host o indirizzo IP" + }, + "description": "Configura WLED per l'integrazione con Home Assistant.", + "title": "Collega il tuo WLED" + }, + "zeroconf_confirm": { + "description": "Vuoi aggiungere il WLED chiamato `{name}` a Home Assistant?", + "title": "Dispositivo WLED rilevato" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/ru.json b/homeassistant/components/wled/.translations/ru.json new file mode 100644 index 00000000000000..cd4c3c3b0665d5 --- /dev/null +++ b/homeassistant/components/wled/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + }, + "error": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 WLED \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", + "title": "WLED" + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c WLED `{name}`?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 WLED" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/zh-Hant.json b/homeassistant/components/wled/.translations/zh-Hant.json new file mode 100644 index 00000000000000..b72ef3d078c6e6 --- /dev/null +++ b/homeassistant/components/wled/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "WLED \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "connection_error": "WLED \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "error": { + "connection_error": "WLED \u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "flow_title": "WLED\uff1a{name}", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740" + }, + "description": "\u8a2d\u5b9a WLED \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", + "title": "\u9023\u7d50 WLED" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u65b0\u589e WLED \u540d\u7a31\u300c{name}\u300d\u8a2d\u5099\u81f3 Home Assistant\uff1f", + "title": "\u767c\u73fe\u5230 WLED \u8a2d\u5099" + } + }, + "title": "WLED" + } +} \ No newline at end of file From 4f56f4e7e940a6c90f4e5877a551b73712037d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 8 Nov 2019 10:19:59 +0200 Subject: [PATCH 1496/3953] Add Huawei LTE device registry support (#28594) --- .../components/huawei_lte/__init__.py | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 112fcb6ec52a6b..fa1423edcca2c3 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -6,7 +6,7 @@ from urllib.parse import urlparse import ipaddress import logging -from typing import Any, Callable, Dict, List, Set +from typing import Any, Callable, Dict, List, Set, Tuple import voluptuous as vol import attr @@ -36,6 +36,7 @@ from homeassistant.core import CALLBACK_TYPE from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -135,6 +136,11 @@ def device_name(self) -> str: pass return DEFAULT_DEVICE_NAME + @property + def device_connections(self) -> Set[Tuple[str, str]]: + """Get router connections for device registry.""" + return {(dr.CONNECTION_NETWORK_MAC, self.mac)} + def update(self) -> None: """Update router data.""" @@ -283,6 +289,30 @@ def signal_update() -> None: # Clear all subscriptions, enabled entities will push back theirs router.subscriptions.clear() + # Set up device registry + device_data = {} + sw_version = None + if router.data.get(KEY_DEVICE_INFORMATION): + device_info = router.data[KEY_DEVICE_INFORMATION] + serial_number = device_info.get("SerialNumber") + if serial_number: + device_data["identifiers"] = {(DOMAIN, serial_number)} + sw_version = device_info.get("SoftwareVersion") + if device_info.get("DeviceName"): + device_data["model"] = device_info["DeviceName"] + if not sw_version and router.data.get(KEY_DEVICE_BASIC_INFORMATION): + sw_version = router.data[KEY_DEVICE_BASIC_INFORMATION].get("SoftwareVersion") + if sw_version: + device_data["sw_version"] = sw_version + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections=router.device_connections, + name=router.device_name, + manufacturer="Huawei", + **device_data, + ) + # Forward config entry setup to platforms for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): hass.async_create_task( @@ -408,6 +438,11 @@ def should_poll(self) -> bool: """Huawei LTE entities report their state without polling.""" return False + @property + def device_info(self) -> Dict[str, Any]: + """Get info for matching with parent router.""" + return {"connections": self.router.device_connections} + async def async_update(self) -> None: """Update state.""" raise NotImplementedError From e96b5ef2b016e3dbea37ead117c47b1a6a8ffc59 Mon Sep 17 00:00:00 2001 From: akasma74 Date: Fri, 8 Nov 2019 08:25:37 +0000 Subject: [PATCH 1497/3953] Fix generic_thermostat too_hot/too_cold (#27860) * fix for too_hot/too_cold Closes #27802 * too_hot correction --- homeassistant/components/generic_thermostat/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 13e23b962c9a78..b765dbbfda4285 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -412,8 +412,8 @@ async def _async_control_heating(self, time=None, force=False): if not long_enough: return - too_cold = self._target_temp - self._cur_temp >= self._cold_tolerance - too_hot = self._cur_temp - self._target_temp >= self._hot_tolerance + too_cold = self._target_temp >= self._cur_temp + self._cold_tolerance + too_hot = self._cur_temp >= self._target_temp + self._hot_tolerance if self._is_device_active: if (self.ac_mode and too_cold) or (not self.ac_mode and too_hot): _LOGGER.info("Turning off heater %s", self.heater_entity_id) From b2071b81c141eeb88519ded7ee7862e32b061820 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Nov 2019 09:48:46 +0100 Subject: [PATCH 1498/3953] Add switch platform to WLED integration (#28606) * Add switch platform to WLED integration * Use async_schedule_update_ha_state in async context * Process review comments --- homeassistant/components/wled/__init__.py | 14 +- homeassistant/components/wled/const.py | 2 + homeassistant/components/wled/switch.py | 175 ++++++++++++++++++++ tests/components/wled/test_switch.py | 187 ++++++++++++++++++++++ 4 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/wled/switch.py create mode 100644 tests/components/wled/test_switch.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 62f611b18ec1af..054c09eb971929 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -1,4 +1,5 @@ """Support for WLED.""" +import asyncio from datetime import timedelta import logging from typing import Any, Dict, Optional, Union @@ -6,6 +7,7 @@ from wled import WLED, WLEDConnectionError, WLEDError from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_HOST from homeassistant.core import HomeAssistant, callback @@ -58,9 +60,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled} # Set up all platforms for this device/entry. - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN) - ) + for component in LIGHT_DOMAIN, SWITCH_DOMAIN: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) async def interval_update(now: dt_util.dt.datetime = None) -> None: """Poll WLED device function, dispatches event after update.""" @@ -89,7 +92,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: cancel_timer() # Unload entities for this entry/device. - await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) + await asyncio.gather( + hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN), + hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), + ) # Cleanup del hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 9bc5f64a44429f..0836c801632d92 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -11,6 +11,7 @@ # Attributes ATTR_COLOR_PRIMARY = "color_primary" ATTR_DURATION = "duration" +ATTR_FADE = "fade" ATTR_IDENTIFIERS = "identifiers" ATTR_INTENSITY = "intensity" ATTR_MANUFACTURER = "manufacturer" @@ -23,3 +24,4 @@ ATTR_SOFTWARE_VERSION = "sw_version" ATTR_SPEED = "speed" ATTR_TARGET_BRIGHTNESS = "target_brightness" +ATTR_UDP_PORT = "udp_port" diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py new file mode 100644 index 00000000000000..dcb41a1e49b1bf --- /dev/null +++ b/homeassistant/components/wled/switch.py @@ -0,0 +1,175 @@ +"""Support for WLED switches.""" +import logging +from typing import Any, Callable, List + +from wled import WLED, WLEDError + +from homeassistant.components.switch import SwitchDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from . import WLEDDeviceEntity +from .const import ( + ATTR_DURATION, + ATTR_FADE, + ATTR_TARGET_BRIGHTNESS, + ATTR_UDP_PORT, + DATA_WLED_CLIENT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up WLED switch based on a config entry.""" + wled: WLED = hass.data[DOMAIN][entry.entry_id][DATA_WLED_CLIENT] + + switches = [ + WLEDNightlightSwitch(entry.entry_id, wled), + WLEDSyncSendSwitch(entry.entry_id, wled), + WLEDSyncReceiveSwitch(entry.entry_id, wled), + ] + async_add_entities(switches, True) + + +class WLEDSwitch(WLEDDeviceEntity, SwitchDevice): + """Defines a WLED switch.""" + + def __init__( + self, entry_id: str, wled: WLED, name: str, icon: str, key: str + ) -> None: + """Initialize WLED switch.""" + self._key = key + self._state = False + super().__init__(entry_id, wled, name, icon) + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return f"{self.wled.device.info.mac_address}_{self._key}" + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self._state + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + try: + await self._wled_turn_off() + self._state = False + except WLEDError: + _LOGGER.error("An error occurred while turning off WLED switch.") + self._available = False + self.async_schedule_update_ha_state() + + async def _wled_turn_off(self) -> None: + """Turn off the switch.""" + raise NotImplementedError() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + try: + await self._wled_turn_on() + self._state = True + except WLEDError: + _LOGGER.error("An error occurred while turning on WLED switch") + self._available = False + self.async_schedule_update_ha_state() + + async def _wled_turn_on(self) -> None: + """Turn on the switch.""" + raise NotImplementedError() + + +class WLEDNightlightSwitch(WLEDSwitch): + """Defines a WLED nightlight switch.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED nightlight switch.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Nightlight", + "mdi:weather-night", + "nightlight", + ) + + async def _wled_turn_off(self) -> None: + """Turn off the WLED nightlight switch.""" + await self.wled.nightlight(on=False) + + async def _wled_turn_on(self) -> None: + """Turn on the WLED nightlight switch.""" + await self.wled.nightlight(on=True) + + async def _wled_update(self) -> None: + """Update WLED entity.""" + self._state = self.wled.device.state.nightlight.on + self._attributes = { + ATTR_DURATION: self.wled.device.state.nightlight.duration, + ATTR_FADE: self.wled.device.state.nightlight.fade, + ATTR_TARGET_BRIGHTNESS: self.wled.device.state.nightlight.target_brightness, + } + + +class WLEDSyncSendSwitch(WLEDSwitch): + """Defines a WLED sync send switch.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED sync send switch.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Sync Send", + "mdi:upload-network-outline", + "sync_send", + ) + + async def _wled_turn_off(self) -> None: + """Turn off the WLED sync send switch.""" + await self.wled.sync(send=False) + + async def _wled_turn_on(self) -> None: + """Turn on the WLED sync send switch.""" + await self.wled.sync(send=True) + + async def _wled_update(self) -> None: + """Update WLED entity.""" + self._state = self.wled.device.state.sync.send + self._attributes = {ATTR_UDP_PORT: self.wled.device.info.udp_port} + + +class WLEDSyncReceiveSwitch(WLEDSwitch): + """Defines a WLED sync receive switch.""" + + def __init__(self, entry_id: str, wled: WLED): + """Initialize WLED sync receive switch.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Sync Receive", + "mdi:download-network-outline", + "sync_receive", + ) + + async def _wled_turn_off(self) -> None: + """Turn off the WLED sync receive switch.""" + await self.wled.sync(receive=False) + + async def _wled_turn_on(self) -> None: + """Turn on the WLED sync receive switch.""" + await self.wled.sync(receive=True) + + async def _wled_update(self) -> None: + """Update WLED entity.""" + self._state = self.wled.device.state.sync.receive + self._attributes = {ATTR_UDP_PORT: self.wled.device.info.udp_port} diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py new file mode 100644 index 00000000000000..2dc11801712a80 --- /dev/null +++ b/tests/components/wled/test_switch.py @@ -0,0 +1,187 @@ +"""Tests for the WLED switch platform.""" +import aiohttp + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.wled.const import ( + ATTR_DURATION, + ATTR_FADE, + ATTR_TARGET_BRIGHTNESS, + ATTR_UDP_PORT, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from tests.components.wled import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_switch_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the WLED switches.""" + await init_integration(hass, aioclient_mock) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state + assert state.attributes.get(ATTR_DURATION) == 60 + assert state.attributes.get(ATTR_ICON) == "mdi:weather-night" + assert state.attributes.get(ATTR_TARGET_BRIGHTNESS) == 0 + assert state.attributes.get(ATTR_FADE) + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.wled_rgb_light_nightlight") + assert entry + assert entry.unique_id == "aabbccddeeff_nightlight" + + state = hass.states.get("switch.wled_rgb_light_sync_send") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:upload-network-outline" + assert state.attributes.get(ATTR_UDP_PORT) == 21324 + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.wled_rgb_light_sync_send") + assert entry + assert entry.unique_id == "aabbccddeeff_sync_send" + + state = hass.states.get("switch.wled_rgb_light_sync_receive") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:download-network-outline" + assert state.attributes.get(ATTR_UDP_PORT) == 21324 + assert state.state == STATE_ON + + entry = entity_registry.async_get("switch.wled_rgb_light_sync_receive") + assert entry + assert entry.unique_id == "aabbccddeeff_sync_receive" + + +async def test_switch_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the change of state of the WLED switches.""" + await init_integration(hass, aioclient_mock) + + # Nightlight + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state.state == STATE_OFF + + # Sync send + state = hass.states.get("switch.wled_rgb_light_sync_send") + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_sync_send") + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_sync_send") + assert state.state == STATE_OFF + + # Sync receive + state = hass.states.get("switch.wled_rgb_light_sync_receive") + assert state.state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_sync_receive") + assert state.state == STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_sync_receive") + assert state.state == STATE_ON + + +async def test_switch_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error handling of the WLED switches.""" + aioclient_mock.post("http://example.local:80/json/state", exc=aiohttp.ClientError) + await init_integration(hass, aioclient_mock) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state.state == STATE_UNAVAILABLE + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("switch.wled_rgb_light_sync_send") + assert state.state == STATE_UNAVAILABLE + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("switch.wled_rgb_light_sync_receive") + assert state.state == STATE_UNAVAILABLE From bd54ff3c02cdc38cad647e9e658b907897f39f0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Nov 2019 01:06:16 -0800 Subject: [PATCH 1499/3953] Add TT WS API (#28599) * Add TT WS API * Add a test * Correctly convert TT errrors --- homeassistant/components/cloud/http_api.py | 46 ++++++++++------ homeassistant/components/cloud/manifest.json | 2 +- .../components/websocket_api/connection.py | 6 ++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_http_api.py | 53 +++++++++++++++++++ 7 files changed, 93 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 97c96b0a3e88d7..d969612ce8e67b 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -6,7 +6,7 @@ import aiohttp import async_timeout import attr -from hass_nabucasa import Cloud, auth +from hass_nabucasa import Cloud, auth, thingtalk from hass_nabucasa.const import STATE_DISCONNECTED import voluptuous as vol @@ -75,28 +75,29 @@ async def async_setup(hass): """Initialize the HTTP API.""" - hass.components.websocket_api.async_register_command( - WS_TYPE_STATUS, websocket_cloud_status, SCHEMA_WS_STATUS - ) - hass.components.websocket_api.async_register_command( + async_register_command = hass.components.websocket_api.async_register_command + async_register_command(WS_TYPE_STATUS, websocket_cloud_status, SCHEMA_WS_STATUS) + async_register_command( WS_TYPE_SUBSCRIPTION, websocket_subscription, SCHEMA_WS_SUBSCRIPTION ) - hass.components.websocket_api.async_register_command(websocket_update_prefs) - hass.components.websocket_api.async_register_command( + async_register_command(websocket_update_prefs) + async_register_command( WS_TYPE_HOOK_CREATE, websocket_hook_create, SCHEMA_WS_HOOK_CREATE ) - hass.components.websocket_api.async_register_command( + async_register_command( WS_TYPE_HOOK_DELETE, websocket_hook_delete, SCHEMA_WS_HOOK_DELETE ) - hass.components.websocket_api.async_register_command(websocket_remote_connect) - hass.components.websocket_api.async_register_command(websocket_remote_disconnect) + async_register_command(websocket_remote_connect) + async_register_command(websocket_remote_disconnect) + + async_register_command(google_assistant_list) + async_register_command(google_assistant_update) - hass.components.websocket_api.async_register_command(google_assistant_list) - hass.components.websocket_api.async_register_command(google_assistant_update) + async_register_command(alexa_list) + async_register_command(alexa_update) + async_register_command(alexa_sync) - hass.components.websocket_api.async_register_command(alexa_list) - hass.components.websocket_api.async_register_command(alexa_update) - hass.components.websocket_api.async_register_command(alexa_sync) + async_register_command(thingtalk_convert) hass.http.register_view(GoogleActionsSyncView) hass.http.register_view(CloudLoginView) @@ -592,3 +593,18 @@ async def alexa_sync(hass, connection, msg): connection.send_result(msg["id"]) else: connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, "Unknown error") + + +@websocket_api.async_response +@websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str}) +async def thingtalk_convert(hass, connection, msg): + """Convert a query.""" + cloud = hass.data[DOMAIN] + + with async_timeout.timeout(10): + try: + connection.send_result( + msg["id"], await thingtalk.async_convert(cloud, msg["query"]) + ) + except thingtalk.ThingTalkConversionError as err: + connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, str(err)) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 2876ff11b7ea94..2feef55835ed99 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/integrations/cloud", - "requirements": ["hass-nabucasa==0.26"], + "requirements": ["hass-nabucasa==0.29"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 41232b097d14e1..5a0284a34d4c1d 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -108,6 +108,8 @@ def async_close(self): @callback def async_handle_exception(self, msg, err): """Handle an exception while processing a handler.""" + log_handler = self.logger.error + if isinstance(err, Unauthorized): code = const.ERR_UNAUTHORIZED err_message = "Unauthorized" @@ -120,6 +122,8 @@ def async_handle_exception(self, msg, err): else: code = const.ERR_UNKNOWN_ERROR err_message = "Unknown error" + log_handler = self.logger.exception + + log_handler("Error handling message: %s", err_message) - self.logger.exception("Error handling message: %s", err_message) self.send_message(messages.error_message(msg["id"], code, err_message)) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 775545c4ff10b1..90022bb60a0a47 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 -hass-nabucasa==0.26 +hass-nabucasa==0.29 home-assistant-frontend==20191025.1 importlib-metadata==0.23 jinja2>=2.10.3 diff --git a/requirements_all.txt b/requirements_all.txt index 2efb1db7ba052b..80d01e9ce7296e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -625,7 +625,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.26 +hass-nabucasa==0.29 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f29eee878d9bf6..4f4203480cd90c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -204,7 +204,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.26 +hass-nabucasa==0.29 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 314db3a9e885a3..8d05f1a14c37b6 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -7,6 +7,7 @@ from jose import jwt from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED +from hass_nabucasa import thingtalk from homeassistant.core import State from homeassistant.auth.providers import trusted_networks as tn_auth @@ -871,3 +872,55 @@ async def test_enable_alexa_state_report_fail( assert not response["success"] assert response["error"]["code"] == "alexa_relink" + + +async def test_thingtalk_convert(hass, hass_ws_client, setup_api): + """Test that we can convert a query.""" + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.cloud.http_api.thingtalk.async_convert", + return_value=mock_coro({"hello": "world"}), + ): + await client.send_json( + {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} + ) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == {"hello": "world"} + + +async def test_thingtalk_convert_timeout(hass, hass_ws_client, setup_api): + """Test that we can convert a query.""" + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.cloud.http_api.thingtalk.async_convert", + side_effect=asyncio.TimeoutError, + ): + await client.send_json( + {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} + ) + response = await client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "timeout" + + +async def test_thingtalk_convert_internal(hass, hass_ws_client, setup_api): + """Test that we can convert a query.""" + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.cloud.http_api.thingtalk.async_convert", + side_effect=thingtalk.ThingTalkConversionError("Did not understand"), + ): + await client.send_json( + {"id": 5, "type": "cloud/thingtalk/convert", "query": "some-data"} + ) + response = await client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "unknown_error" + assert response["error"]["message"] == "Did not understand" From cffadf919acb11d054263dd3bdbb82f636fd7dc4 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 8 Nov 2019 17:02:44 +0100 Subject: [PATCH 1500/3953] Add turn_on/off to tfiac (#27712) * Add turn_on/off to tfiac * fix ws issue --- homeassistant/components/tfiac/climate.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/tfiac/climate.py b/homeassistant/components/tfiac/climate.py index 7979ffb9cf17be..6d23018e89750b 100644 --- a/homeassistant/components/tfiac/climate.py +++ b/homeassistant/components/tfiac/climate.py @@ -189,3 +189,11 @@ async def async_set_fan_mode(self, fan_mode): async def async_set_swing_mode(self, swing_mode): """Set new swing mode.""" await self._client.set_swing(swing_mode.capitalize()) + + async def async_turn_on(self): + """Turn device on.""" + await self._client.set_state(OPERATION_MODE) + + async def async_turn_off(self): + """Turn device off.""" + await self._client.set_state(ON_MODE, "off") From 4435b3a5c950ca311ddda751d0ee394704366e1b Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 8 Nov 2019 17:12:41 +0100 Subject: [PATCH 1501/3953] Fix issue with multiple Netatmo home coach devices (#28407) * Retrieve more detailed module infos * Switch to using IDs * Bump pyatmo version to 2.3.3 * Update requirements * Undo the change of the unique id * Rename variable --- .../components/netatmo/manifest.json | 2 +- homeassistant/components/netatmo/sensor.py | 70 +++++++++---------- requirements_all.txt | 2 +- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index efb2840216b90c..f6c08faf8fa86d 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/integrations/netatmo", "requirements": [ - "pyatmo==2.3.2" + "pyatmo==2.3.3" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 3d4b5f2fa5151a..f76062035d2fc7 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -6,6 +6,7 @@ import pyatmo import requests +import urllib3 import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -21,7 +22,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import call_later from homeassistant.util import Throttle -from .const import DATA_NETATMO_AUTH +from .const import DATA_NETATMO_AUTH, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -144,37 +145,36 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def find_devices(data): """Find all devices.""" - all_module_names = data.get_module_names() + all_module_infos = data.get_module_infos() + all_module_names = [e["module_name"] for e in all_module_infos.values()] module_names = config.get(CONF_MODULES, all_module_names) - _dev = [] + entities = [] for module_name in module_names: if module_name not in all_module_names: _LOGGER.info("Module %s not found", module_name) + for module in all_module_infos.values(): + if module["module_name"] not in module_names: continue - for condition in data.station_data.monitoredConditions(module_name): + for condition in data.station_data.monitoredConditions( + moduleId=module["id"] + ): _LOGGER.debug( "Adding %s %s", - module_name, - data.station_data.moduleByName( - station=data.station, module=module_name - ), + module["module_name"], + data.station_data.moduleById(mid=module["id"]), ) - _dev.append( - NetatmoSensor( - data, module_name, condition.lower(), data.station - ) - ) - return _dev + entities.append(NetatmoSensor(data, module, condition.lower())) + return entities def _retry(_data): try: - _dev = find_devices(_data) + entities = find_devices(_data) except requests.exceptions.Timeout: return call_later( hass, NETATMO_UPDATE_INTERVAL, lambda _: _retry(_data) ) - if _dev: - add_entities(_dev, True) + if entities: + add_entities(entities, True) for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: try: @@ -197,22 +197,23 @@ def _retry(_data): class NetatmoSensor(Entity): """Implementation of a Netatmo sensor.""" - def __init__(self, netatmo_data, module_name, sensor_type, station): + def __init__(self, netatmo_data, module_info, sensor_type): """Initialize the sensor.""" - self._name = "Netatmo {} {}".format(module_name, SENSOR_TYPES[sensor_type][0]) self.netatmo_data = netatmo_data - self.module_name = module_name + module = self.netatmo_data.station_data.moduleById(mid=module_info["id"]) + if module["type"] == "NHC": + self.module_name = module_info["station_name"] + else: + self.module_name = module_info["module_name"] + self._name = f"{DOMAIN} {self.module_name} {SENSOR_TYPES[sensor_type][0]}" self.type = sensor_type - self.station_name = station self._state = None self._device_class = SENSOR_TYPES[self.type][3] self._icon = SENSOR_TYPES[self.type][2] self._unit_of_measurement = SENSOR_TYPES[self.type][1] - module = self.netatmo_data.station_data.moduleByName( - station=self.station_name, module=module_name - ) self._module_type = module["type"] - self._unique_id = "{}-{}".format(module["_id"], self.type) + self._module_id = module_info["id"] + self._unique_id = f"{self._module_id}-{self.type}" @property def name(self): @@ -254,7 +255,7 @@ def update(self): self._state = None return - data = self.netatmo_data.data.get(self.module_name) + data = self.netatmo_data.data.get(self._module_id) if data is None: _LOGGER.warning("No data found for %s", self.module_name) @@ -543,11 +544,11 @@ def __init__(self, auth, data_class, station): self._next_update = time() self._update_in_progress = threading.Lock() - def get_module_names(self): - """Return all module available on the API as a list.""" + def get_module_infos(self): + """Return all modules available on the API as a dict.""" if self.station is not None: - return self.station_data.modulesNamesList(station=self.station) - return self.station_data.modulesNamesList() + return self.station_data.getModules(station=self.station) + return self.station_data.getModules() def update(self): """Call the Netatmo API to update the data. @@ -567,14 +568,13 @@ def update(self): "No Weather or HomeCoach devices found for %s", str(self.station) ) return - except requests.exceptions.Timeout: + except (requests.exceptions.Timeout, urllib3.exceptions.ReadTimeoutError): _LOGGER.warning("Timed out when connecting to Netatmo server.") return - if self.station is not None: - data = self.station_data.lastData(station=self.station, exclude=3600) - else: - data = self.station_data.lastData(exclude=3600) + data = self.station_data.lastData( + station=self.station, exclude=3600, byId=True + ) if not data: self._next_update = time() + NETATMO_UPDATE_INTERVAL return diff --git a/requirements_all.txt b/requirements_all.txt index 80d01e9ce7296e..a114df06a2a81e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1102,7 +1102,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.3.2 +pyatmo==2.3.3 # homeassistant.components.atome pyatome==0.1.1 From 28c6837f00b32fcf60df2fd0d1536aaf92f39b58 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 8 Nov 2019 18:06:23 +0100 Subject: [PATCH 1502/3953] Add attribution and onboarding commands to conversation and Almond (#28621) * Add attribution and onboarding commands to conversation and Almond * False -> None * Comments * Update __init__.py * Comments + websocket for convert * Lint --- homeassistant/components/almond/__init__.py | 37 +++++- .../components/conversation/__init__.py | 111 +++++++++++++----- .../components/conversation/agent.py | 13 ++ 3 files changed, 128 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 5eb305e6795d4f..9977d48ae9a0c5 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -10,6 +10,7 @@ from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI import voluptuous as vol +from homeassistant import core from homeassistant.const import CONF_TYPE, CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.auth.const import GROUP_ID_ADMIN @@ -95,9 +96,9 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Almond config entry.""" websession = aiohttp_client.async_get_clientsession(hass) + if entry.data["type"] == TYPE_LOCAL: auth = AlmondLocalAuth(entry.data["host"], websession) - else: # OAuth2 implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( @@ -109,7 +110,7 @@ async def async_setup_entry(hass, entry): auth = AlmondOAuth(entry.data["host"], websession, oauth_session) api = WebAlmondAPI(auth) - agent = AlmondAgent(api) + agent = AlmondAgent(hass, api, entry) # Hass.io does its own configuration of Almond. if entry.data.get("is_hassio") or entry.data["type"] != TYPE_LOCAL: @@ -202,9 +203,39 @@ async def async_get_access_token(self): class AlmondAgent(conversation.AbstractConversationAgent): """Almond conversation agent.""" - def __init__(self, api: WebAlmondAPI): + def __init__(self, hass: core.HomeAssistant, api: WebAlmondAPI, entry): """Initialize the agent.""" + self.hass = hass self.api = api + self.entry = entry + + @property + def attribution(self): + """Return the attribution.""" + return {"name": "Powered by Almond", "url": "https://almond.stanford.edu/"} + + async def async_get_onboarding(self): + """Get onboard url if not onboarded.""" + if self.entry.data.get("onboarded"): + return None + + host = self.entry.data["host"] + if self.entry.data.get("is_hassio"): + host = "/core_almond" + elif self.entry.data["type"] != TYPE_LOCAL: + host = f"{host}/me" + return { + "text": "Would you like to opt-in to share your anonymized commands with Stanford to improve Almond's responses?", + "url": f"{host}/conversation", + } + + async def async_set_onboarding(self, shown): + """Set onboarding status.""" + self.hass.config_entries.async_update_entry( + self.entry, data={**self.entry.data, "onboarded": shown} + ) + + return True async def async_process( self, text: str, conversation_id: Optional[str] = None diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index f875ec2822c271..a82034a4237b38 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant import core -from homeassistant.components import http +from homeassistant.components import http, websocket_api from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.helpers import config_validation as cv, intent from homeassistant.loader import bind_hass @@ -21,6 +21,7 @@ REGEX_TYPE = type(re.compile("")) DATA_AGENT = "conversation_agent" +DATA_CONFIG = "conversation_config" SERVICE_PROCESS = "process" @@ -39,7 +40,6 @@ extra=vol.ALLOW_EXTRA, ) - async_register = bind_hass(async_register) # pylint: disable=invalid-name @@ -50,18 +50,19 @@ def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent): hass.data[DATA_AGENT] = agent -async def async_setup(hass, config): - """Register the process service.""" +async def get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: + """Get agent.""" + agent = hass.data.get(DATA_AGENT) + if agent is None: + agent = hass.data[DATA_AGENT] = DefaultAgent(hass) + await agent.async_initialize(hass.data.get(DATA_CONFIG)) + return agent - async def process(hass, text, conversation_id): - """Process a line of text.""" - agent = hass.data.get(DATA_AGENT) - if agent is None: - agent = hass.data[DATA_AGENT] = DefaultAgent(hass) - await agent.async_initialize(config) +async def async_setup(hass, config): + """Register the process service.""" - return await agent.async_process(text, conversation_id) + hass.data[DATA_CONFIG] = config async def handle_service(service): """Parse text into commands.""" @@ -75,39 +76,89 @@ async def handle_service(service): hass.services.async_register( DOMAIN, SERVICE_PROCESS, handle_service, schema=SERVICE_PROCESS_SCHEMA ) - - hass.http.register_view(ConversationProcessView(process)) + hass.http.register_view(ConversationProcessView()) + hass.components.websocket_api.async_register_command(websocket_process) + hass.components.websocket_api.async_register_command(websocket_get_agent_info) + hass.components.websocket_api.async_register_command(websocket_set_onboarding) return True +async def process(hass: core.HomeAssistant, text: str, conversation_id: str): + """Process text and get intent.""" + agent = await get_agent(hass) + return await agent.async_process(text, conversation_id) + + +async def get_intent(hass: core.HomeAssistant, text: str, conversation_id: str): + """Process text and get intent.""" + try: + intent_result = await process(hass, text, conversation_id) + except intent.IntentHandleError as err: + intent_result = intent.IntentResponse() + intent_result.async_set_speech(str(err)) + + if intent_result is None: + intent_result = intent.IntentResponse() + intent_result.async_set_speech("Sorry, I didn't understand that") + + return intent_result + + +@websocket_api.async_response +@websocket_api.websocket_command( + {"type": "conversation/process", "text": str, vol.Optional("conversation_id"): str} +) +async def websocket_process(hass, connection, msg): + """Process text.""" + connection.send_result( + msg["id"], await get_intent(hass, msg["text"], msg.get("conversation_id")) + ) + + +@websocket_api.async_response +@websocket_api.websocket_command({"type": "conversation/agent/info"}) +async def websocket_get_agent_info(hass, connection, msg): + """Do we need onboarding.""" + agent = await get_agent(hass) + + connection.send_result( + msg["id"], + { + "onboarding": await agent.async_get_onboarding(), + "attribution": agent.attribution, + }, + ) + + +@websocket_api.async_response +@websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool}) +async def websocket_set_onboarding(hass, connection, msg): + """Set onboarding status.""" + agent = await get_agent(hass) + + success = await agent.async_set_onboarding(msg["shown"]) + + if success: + connection.send_result(msg["id"]) + else: + connection.send_error(msg["id"]) + + class ConversationProcessView(http.HomeAssistantView): - """View to retrieve shopping list content.""" + """View to process text.""" url = "/api/conversation/process" name = "api:conversation:process" - def __init__(self, process): - """Initialize the conversation process view.""" - self._process = process - @RequestDataValidator( vol.Schema({vol.Required("text"): str, vol.Optional("conversation_id"): str}) ) async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] - - try: - intent_result = await self._process( - hass, data["text"], data.get("conversation_id") - ) - except intent.IntentHandleError as err: - intent_result = intent.IntentResponse() - intent_result.async_set_speech(str(err)) - - if intent_result is None: - intent_result = intent.IntentResponse() - intent_result.async_set_speech("Sorry, I didn't understand that") + intent_result = await get_intent( + hass, data["text"], data.get("conversation_id") + ) return self.json(intent_result) diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 1875ab5b9b9b7c..0c47d6156455d1 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -8,6 +8,19 @@ class AbstractConversationAgent(ABC): """Abstract conversation agent.""" + @property + def attribution(self): + """Return the attribution.""" + return None + + async def async_get_onboarding(self): + """Get onboard data.""" + return None + + async def async_set_onboarding(self, shown): + """Set onboard data.""" + return True + @abstractmethod async def async_process( self, text: str, conversation_id: Optional[str] = None From d0f1e9fc0163f1b30ec612544956d79fd702038b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 8 Nov 2019 18:12:20 +0100 Subject: [PATCH 1503/3953] Updated frontend to 20191108.0 (#28638) --- 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 aa7ad8b18f9491..8f7212209cf7f0 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191025.1" + "home-assistant-frontend==20191108.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 90022bb60a0a47..ade3305edc9f38 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191025.1 +home-assistant-frontend==20191108.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index a114df06a2a81e..308c22145a1e12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.1 +home-assistant-frontend==20191108.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f4203480cd90c..993dd37f43acb6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.1 +home-assistant-frontend==20191108.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From f2c56cff430e32f657a0f15a6ce6a3aaa76b1e5c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 8 Nov 2019 12:12:50 -0500 Subject: [PATCH 1504/3953] Bump ZHA quirks version (#28636) --- 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 9821ec2025bd9f..a8ef269e394277 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.10.0", - "zha-quirks==0.0.26", + "zha-quirks==0.0.27", "zigpy-deconz==0.6.0", "zigpy-homeassistant==0.10.0", "zigpy-xbee-homeassistant==0.6.0", diff --git a/requirements_all.txt b/requirements_all.txt index 308c22145a1e12..29b94d40644704 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2051,7 +2051,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.26 +zha-quirks==0.0.27 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 993dd37f43acb6..ceb63df9896bf4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -637,7 +637,7 @@ yahooweather==0.10 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.26 +zha-quirks==0.0.27 # homeassistant.components.zha zigpy-deconz==0.6.0 From f8d3ea20b6e7495f508673fb797dde72868f605f Mon Sep 17 00:00:00 2001 From: Tomasz Date: Fri, 8 Nov 2019 18:32:44 +0100 Subject: [PATCH 1505/3953] Move imports in xiaomi_miio (#27773) * move imports in xiaomi_miio * reorder imports with isort * fix pylint error * Rename imports --- .../components/xiaomi_miio/device_tracker.py | 5 +- homeassistant/components/xiaomi_miio/fan.py | 90 ++++++++----------- homeassistant/components/xiaomi_miio/light.py | 38 ++------ .../components/xiaomi_miio/remote.py | 24 ++--- .../components/xiaomi_miio/sensor.py | 5 +- .../components/xiaomi_miio/switch.py | 30 ++----- .../components/xiaomi_miio/vacuum.py | 25 ++---- 7 files changed, 76 insertions(+), 141 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index e2611b52f121cd..ef527d0aa407a2 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -1,6 +1,7 @@ """Support for Xiaomi Mi WiFi Repeater 2.""" import logging +from miio import DeviceException, WifiRepeater # pylint: disable=import-error import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -23,8 +24,6 @@ def get_scanner(hass, config): """Return a Xiaomi MiIO device scanner.""" - from miio import WifiRepeater, DeviceException - scanner = None host = config[DOMAIN][CONF_HOST] token = config[DOMAIN][CONF_TOKEN] @@ -56,8 +55,6 @@ def __init__(self, device): async def async_scan_devices(self): """Scan for devices and return a list containing found device IDs.""" - from miio import DeviceException - devices = [] try: station_info = await self.hass.async_add_executor_job(self.device.status) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index e6c356b7338f54..9e496893d56916 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -5,19 +5,39 @@ import logging import voluptuous as vol +from miio import ( # pylint: disable=import-error + AirFresh, + AirHumidifier, + AirPurifier, + Device, + DeviceException, +) + +from miio.airfresh import ( # pylint: disable=import-error; pylint: disable=import-error + LedBrightness as AirfreshLedBrightness, + OperationMode as AirfreshOperationMode, +) +from miio.airhumidifier import ( # pylint: disable=import-error; pylint: disable=import-error + LedBrightness as AirhumidifierLedBrightness, + OperationMode as AirhumidifierOperationMode, +) +from miio.airpurifier import ( # pylint: disable=import-error; pylint: disable=import-error + LedBrightness as AirpurifierLedBrightness, + OperationMode as AirpurifierOperationMode, +) from homeassistant.components.fan import ( - FanEntity, + DOMAIN, PLATFORM_SCHEMA, SUPPORT_SET_SPEED, - DOMAIN, + FanEntity, ) from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_MODE, - CONF_NAME, CONF_HOST, + CONF_NAME, CONF_TOKEN, - ATTR_ENTITY_ID, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -429,8 +449,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the miio fan device from config.""" - from miio import Device, DeviceException - if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -458,18 +476,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= raise PlatformNotReady if model.startswith("zhimi.airpurifier."): - from miio import AirPurifier - air_purifier = AirPurifier(host, token) device = XiaomiAirPurifier(name, air_purifier, model, unique_id) elif model.startswith("zhimi.humidifier."): - from miio import AirHumidifier - air_humidifier = AirHumidifier(host, token, model=model) device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id) elif model.startswith("zhimi.airfresh."): - from miio import AirFresh - air_fresh = AirFresh(host, token) device = XiaomiAirFresh(name, air_fresh, model, unique_id) else: @@ -580,8 +592,6 @@ def _extract_value_from_attribute(state, attribute): async def _try_command(self, mask_error, func, *args, **kwargs): """Call a miio device command handling error messages.""" - from miio import DeviceException - try: result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) @@ -698,8 +708,6 @@ def __init__(self, name, device, model, unique_id): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -731,9 +739,7 @@ def speed_list(self) -> list: def speed(self): """Return the current speed.""" if self._state: - from miio.airpurifier import OperationMode - - return OperationMode(self._state_attrs[ATTR_MODE]).name + return AirpurifierOperationMode(self._state_attrs[ATTR_MODE]).name return None @@ -742,14 +748,12 @@ async def async_set_speed(self, speed: str) -> None: if self.supported_features & SUPPORT_SET_SPEED == 0: return - from miio.airpurifier import OperationMode - _LOGGER.debug("Setting the operation mode to: %s", speed) await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, - OperationMode[speed.title()], + AirpurifierOperationMode[speed.title()], ) async def async_set_led_on(self): @@ -777,12 +781,10 @@ async def async_set_led_brightness(self, brightness: int = 2): if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: return - from miio.airpurifier import LedBrightness - await self._try_command( "Setting the led brightness of the miio device failed.", self._device.set_led_brightness, - LedBrightness(brightness), + AirpurifierLedBrightness(brightness), ) async def async_set_favorite_level(self, level: int = 1): @@ -878,21 +880,23 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): def __init__(self, name, device, model, unique_id): """Initialize the plug switch.""" - from miio.airhumidifier import OperationMode - super().__init__(name, device, model, unique_id) if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB self._speed_list = [ - mode.name for mode in OperationMode if mode is not OperationMode.Strong + mode.name + for mode in AirhumidifierOperationMode + if mode is not AirhumidifierOperationMode.Strong ] else: self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER self._speed_list = [ - mode.name for mode in OperationMode if mode is not OperationMode.Auto + mode.name + for mode in AirhumidifierOperationMode + if mode is not AirhumidifierOperationMode.Auto ] self._state_attrs.update( @@ -901,8 +905,6 @@ def __init__(self, name, device, model, unique_id): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -934,9 +936,7 @@ def speed_list(self) -> list: def speed(self): """Return the current speed.""" if self._state: - from miio.airhumidifier import OperationMode - - return OperationMode(self._state_attrs[ATTR_MODE]).name + return AirhumidifierOperationMode(self._state_attrs[ATTR_MODE]).name return None @@ -945,14 +945,12 @@ async def async_set_speed(self, speed: str) -> None: if self.supported_features & SUPPORT_SET_SPEED == 0: return - from miio.airhumidifier import OperationMode - _LOGGER.debug("Setting the operation mode to: %s", speed) await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, - OperationMode[speed.title()], + AirhumidifierOperationMode[speed.title()], ) async def async_set_led_brightness(self, brightness: int = 2): @@ -960,12 +958,10 @@ async def async_set_led_brightness(self, brightness: int = 2): if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: return - from miio.airhumidifier import LedBrightness - await self._try_command( "Setting the led brightness of the miio device failed.", self._device.set_led_brightness, - LedBrightness(brightness), + AirhumidifierLedBrightness(brightness), ) async def async_set_target_humidity(self, humidity: int = 40): @@ -1018,8 +1014,6 @@ def __init__(self, name, device, model, unique_id): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -1051,9 +1045,7 @@ def speed_list(self) -> list: def speed(self): """Return the current speed.""" if self._state: - from miio.airfresh import OperationMode - - return OperationMode(self._state_attrs[ATTR_MODE]).name + return AirfreshOperationMode(self._state_attrs[ATTR_MODE]).name return None @@ -1062,14 +1054,12 @@ async def async_set_speed(self, speed: str) -> None: if self.supported_features & SUPPORT_SET_SPEED == 0: return - from miio.airfresh import OperationMode - _LOGGER.debug("Setting the operation mode to: %s", speed) await self._try_command( "Setting operation mode of the miio device failed.", self._device.set_mode, - OperationMode[speed.title()], + AirfreshOperationMode[speed.title()], ) async def async_set_led_on(self): @@ -1097,12 +1087,10 @@ async def async_set_led_brightness(self, brightness: int = 2): if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: return - from miio.airfresh import LedBrightness - await self._try_command( "Setting the led brightness of the miio device failed.", self._device.set_led_brightness, - LedBrightness(brightness), + AirfreshLedBrightness(brightness), ) async def async_set_extra_features(self, features: int = 1): diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index aa5a0ed42b96de..5b454512f33524 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -6,13 +6,21 @@ import logging from math import ceil +from miio import ( # pylint: disable=import-error + Ceil, + Device, + DeviceException, + PhilipsBulb, + PhilipsEyecare, + PhilipsMoonlight, +) import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_ENTITY_ID, + ATTR_HS_COLOR, DOMAIN, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, @@ -116,8 +124,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the light from config.""" - from miio import Device, DeviceException - if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -147,8 +153,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= raise PlatformNotReady if model == "philips.light.sread1": - from miio import PhilipsEyecare - light = PhilipsEyecare(host, token) primary_device = XiaomiPhilipsEyecareLamp(name, light, model, unique_id) devices.append(primary_device) @@ -161,15 +165,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # The ambient light doesn't expose additional services. # A hass.data[DATA_KEY] entry isn't needed. elif model in ["philips.light.ceiling", "philips.light.zyceiling"]: - from miio import Ceil - light = Ceil(host, token) device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device elif model == "philips.light.moonlight": - from miio import PhilipsMoonlight - light = PhilipsMoonlight(host, token) device = XiaomiPhilipsMoonlightLamp(name, light, model, unique_id) devices.append(device) @@ -180,15 +180,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "philips.light.candle2", "philips.light.downlight", ]: - from miio import PhilipsBulb - light = PhilipsBulb(host, token) device = XiaomiPhilipsBulb(name, light, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device elif model == "philips.light.mono1": - from miio import PhilipsBulb - light = PhilipsBulb(host, token) device = XiaomiPhilipsGenericLight(name, light, model, unique_id) devices.append(device) @@ -297,8 +293,6 @@ def supported_features(self): async def _try_command(self, mask_error, func, *args, **kwargs): """Call a light command handling error messages.""" - from miio import DeviceException - try: result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) @@ -337,8 +331,6 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -363,8 +355,6 @@ def __init__(self, name, light, model, unique_id): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -521,8 +511,6 @@ async def async_turn_on(self, **kwargs): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -580,8 +568,6 @@ def max_mireds(self): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -626,8 +612,6 @@ def __init__(self, name, light, model, unique_id): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -769,8 +753,6 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: @@ -925,8 +907,6 @@ async def async_turn_on(self, **kwargs): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._light.status) except DeviceException as ex: diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 075dd15e887ae2..0e2ac476e0577c 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -1,28 +1,28 @@ """Support for the Xiaomi IR Remote (Chuangmi IR).""" import asyncio +from datetime import timedelta import logging import time -from datetime import timedelta - +from miio import ChuangmiIr, DeviceException # pylint: disable=import-error import voluptuous as vol from homeassistant.components.remote import ( - PLATFORM_SCHEMA, - DOMAIN, - ATTR_NUM_REPEATS, ATTR_DELAY_SECS, + ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, + DOMAIN, + PLATFORM_SCHEMA, RemoteDevice, ) from homeassistant.const import ( - CONF_NAME, - CONF_HOST, - CONF_TOKEN, - CONF_TIMEOUT, ATTR_ENTITY_ID, ATTR_HIDDEN, CONF_COMMAND, + CONF_HOST, + CONF_NAME, + CONF_TIMEOUT, + CONF_TOKEN, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -73,8 +73,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Xiaomi IR Remote (Chuangmi IR) platform.""" - from miio import ChuangmiIr, DeviceException - host = config[CONF_HOST] token = config[CONF_TOKEN] @@ -226,8 +224,6 @@ def timeout(self): @property def is_on(self): """Return False if device is unreachable, else True.""" - from miio import DeviceException - try: self.device.info() return True @@ -262,8 +258,6 @@ async def async_turn_off(self, **kwargs): def _send_command(self, payload): """Send a command.""" - from miio import DeviceException - _LOGGER.debug("Sending payload: '%s'", payload) try: self.device.play(payload) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index c19e314acddd8a..9f5ea1fa86803b 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -1,6 +1,7 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" import logging +from miio import AirQualityMonitor, DeviceException # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -37,8 +38,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the sensor from config.""" - from miio import AirQualityMonitor, DeviceException - if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -135,8 +134,6 @@ def device_state_attributes(self): async def async_update(self): """Fetch state from the miio device.""" - from miio import DeviceException - try: state = await self.hass.async_add_executor_job(self._device.status) _LOGGER.debug("Got new state: %s", state) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 97e8ef27c3f228..42586cd59700a3 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -3,6 +3,14 @@ from functools import partial import logging +from miio import ( # pylint: disable=import-error + AirConditioningCompanionV3, + ChuangmiPlug, + Device, + DeviceException, + PowerStrip, +) +from miio.powerstrip import PowerMode # pylint: disable=import-error import voluptuous as vol from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchDevice @@ -102,8 +110,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the switch from config.""" - from miio import Device, DeviceException - if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -133,8 +139,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= raise PlatformNotReady if model in ["chuangmi.plug.v1", "chuangmi.plug.v3"]: - from miio import ChuangmiPlug - plug = ChuangmiPlug(host, token, model=model) # The device has two switchable channels (mains and a USB port). @@ -145,8 +149,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass.data[DATA_KEY][host] = device elif model in ["qmi.powerstrip.v1", "zimi.powerstrip.v2"]: - from miio import PowerStrip - plug = PowerStrip(host, token, model=model) device = XiaomiPowerStripSwitch(name, plug, model, unique_id) devices.append(device) @@ -157,15 +159,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "chuangmi.plug.v2", "chuangmi.plug.hmi205", ]: - from miio import ChuangmiPlug - plug = ChuangmiPlug(host, token, model=model) device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device elif model in ["lumi.acpartner.v3"]: - from miio import AirConditioningCompanionV3 - plug = AirConditioningCompanionV3(host, token) device = XiaomiAirConditioningCompanionSwitch(name, plug, model, unique_id) devices.append(device) @@ -268,8 +266,6 @@ def is_on(self): async def _try_command(self, mask_error, func, *args, **kwargs): """Call a plug command handling error messages.""" - from miio import DeviceException - try: result = await self.hass.async_add_executor_job( partial(func, *args, **kwargs) @@ -305,8 +301,6 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -379,8 +373,6 @@ def __init__(self, name, plug, model, unique_id): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -417,8 +409,6 @@ async def async_set_power_mode(self, mode: str): if self._device_features & FEATURE_SET_POWER_MODE == 0: return - from miio.powerstrip import PowerMode - await self._try_command( "Setting the power mode of the power strip failed.", self._plug.set_power_mode, @@ -477,8 +467,6 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False @@ -538,8 +526,6 @@ async def async_turn_off(self, **kwargs): async def async_update(self): """Fetch state from the device.""" - from miio import DeviceException - # On state change the device doesn't provide the new state immediately. if self._skip_update: self._skip_update = False diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index aa08693db63456..b18a54ce97a9b1 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -3,12 +3,19 @@ from functools import partial import logging +from miio import DeviceException, Vacuum # pylint: disable=import-error import voluptuous as vol from homeassistant.components.vacuum import ( ATTR_CLEANED_AREA, DOMAIN, PLATFORM_SCHEMA, + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, @@ -16,16 +23,10 @@ SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, - SUPPORT_STOP, - SUPPORT_STATE, SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STOP, StateVacuumDevice, - STATE_CLEANING, - STATE_DOCKED, - STATE_PAUSED, - STATE_IDLE, - STATE_RETURNING, - STATE_ERROR, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -177,8 +178,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Xiaomi vacuum cleaner robot platform.""" - from miio import Vacuum - if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -348,8 +347,6 @@ def supported_features(self): async def _try_command(self, mask_error, func, *args, **kwargs): """Call a vacuum command handling error messages.""" - from miio import DeviceException - try: await self.hass.async_add_executor_job(partial(func, *args, **kwargs)) return True @@ -450,8 +447,6 @@ async def async_remote_control_move_step( def update(self): """Fetch state from the device.""" - from miio import DeviceException - try: state = self._vacuum.status() self.vacuum_state = state @@ -469,8 +464,6 @@ def update(self): async def async_clean_zone(self, zone, repeats=1): """Clean selected area for the number of repeats indicated.""" - from miio import DeviceException - for _zone in zone: _zone.append(repeats) _LOGGER.debug("Zone with repeats: %s", zone) From caedc14b00044f63c79cc90eb3ef01116c6bc2e7 Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Fri, 8 Nov 2019 18:48:28 +0100 Subject: [PATCH 1506/3953] Added support for multiple SAJ solar inverters (#28612) Changes after review --- homeassistant/components/saj/sensor.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 5605866908e5d7..7542440c102a58 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, + CONF_NAME, CONF_TYPE, CONF_USERNAME, DEVICE_CLASS_POWER, @@ -48,6 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_TYPE, default=INVERTER_TYPES[0]): vol.In(INVERTER_TYPES), vol.Inclusive(CONF_USERNAME, "credentials"): cv.string, vol.Inclusive(CONF_PASSWORD, "credentials"): cv.string, @@ -68,10 +70,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass_sensors = [] for sensor in sensor_def: - hass_sensors.append(SAJsensor(sensor)) + hass_sensors.append(SAJsensor(sensor, inverter_name=config.get(CONF_NAME))) kwargs = {} - if wifi: kwargs["wifi"] = True if config.get(CONF_USERNAME) and config.get(CONF_PASSWORD): @@ -162,14 +163,18 @@ def remove_listener(): class SAJsensor(Entity): """Representation of a SAJ sensor.""" - def __init__(self, pysaj_sensor): + def __init__(self, pysaj_sensor, inverter_name=None): """Initialize the sensor.""" self._sensor = pysaj_sensor + self._inverter_name = inverter_name self._state = self._sensor.value @property def name(self): """Return the name of the sensor.""" + if self._inverter_name: + return f"saj_{self._inverter_name}_{self._sensor.name}" + return f"saj_{self._sensor.name}" @property @@ -230,4 +235,7 @@ def async_update_values(self, unknown_state=False): @property def unique_id(self): """Return a unique identifier for this sensor.""" + if self._inverter_name: + return f"{self._inverter_name}_{self._sensor.name}" + return f"{self._sensor.name}" From 504ad6488cf214da4e45e51889c1241ee0996ddf Mon Sep 17 00:00:00 2001 From: Ari Date: Fri, 8 Nov 2019 13:08:50 -0500 Subject: [PATCH 1507/3953] Add support for Heat Mode detection for ecobee Heat Pumps (#28273) * Add support for Heat Mode detection for Heat Pumps - Fixes #26547 Since the ecobee component started to dynamically set the supported HVAC modes based on querying the device a few releases ago, users with Heat Pumps noticed that the Heat mode was no longer offered as an option by HA. Some of us did not actually notice until the summer was over :). This commit fixes that. For heatpumps, ecobee returns: 'coolStages': 1, 'heatStages': 0, 'hasHeatPump': True, Fix tested on HA 100.1 and 100.3 Fixes bug https://github.com/home-assistant/home-assistant/issues/26547 * changed line formatted with black --- homeassistant/components/ecobee/climate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index e29e23810089d4..c583f9696d24cb 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -267,7 +267,10 @@ def __init__(self, data, thermostat_index): self._last_active_hvac_mode = HVAC_MODE_AUTO self._operation_list = [] - if self.thermostat["settings"]["heatStages"]: + if ( + self.thermostat["settings"]["heatStages"] + or self.thermostat["settings"]["hasHeatPump"] + ): self._operation_list.append(HVAC_MODE_HEAT) if self.thermostat["settings"]["coolStages"]: self._operation_list.append(HVAC_MODE_COOL) From bc53e9d0c824cea2b174584bdaa683e81f0c2d02 Mon Sep 17 00:00:00 2001 From: LeoCal <25389602+LeoCal@users.noreply.github.com> Date: Fri, 8 Nov 2019 20:01:35 +0100 Subject: [PATCH 1508/3953] Fix unhandled exception when Swisscom Internet Box is not responsive (#28618) * Update device_tracker.py From time to time, Swisscom Internet Box fails to respond and this causes an exception, which is currently not handled by the code: Traceback (most recent call last): File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/device_tracker/setup.py", line 164, in async_device_tracker_scan found_devices = await scanner.async_scan_devices() File "/usr/local/lib/python3.7/concurrent/futures/thread.py", line 57, in run result = self.fn(*self.args, **self.kwargs) File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/swisscom/device_tracker.py", line 46, in scan_devices self._update_info() File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/swisscom/device_tracker.py", line 67, in _update_info data = self.get_swisscom_data() File "/srv/homeassistant/lib/python3.7/site-packages/homeassistant/components/swisscom/device_tracker.py", line 83, in get_swisscom_data request = requests.post(url, headers=headers, data=data, timeout=10) File "/srv/homeassistant/lib/python3.7/site-packages/requests/api.py", line 116, in post return request('post', url, data=data, json=json, **kwargs) File "/srv/homeassistant/lib/python3.7/site-packages/requests/api.py", line 60, in request return session.request(method=method, url=url, **kwargs) File "/srv/homeassistant/lib/python3.7/site-packages/requests/sessions.py", line 533, in request resp = self.send(prep, **send_kwargs) File "/srv/homeassistant/lib/python3.7/site-packages/requests/sessions.py", line 686, in send r.content File "/srv/homeassistant/lib/python3.7/site-packages/requests/models.py", line 828, in content self._content = b''.join(self.iter_content(CONTENT_CHUNK_SIZE)) or b'' File "/srv/homeassistant/lib/python3.7/site-packages/requests/models.py", line 757, in generate raise ConnectionError(e) requests.exceptions.ConnectionError: HTTPConnectionPool(host='192.168.1.1', port=80): Read timed out. I've just added a try-except around the post. * Update device_tracker.py Addressed blank line issue reported by flake8 * Update device_tracker.py Fixed alignment to be Black compliant. * Update device_tracker.py Fixed one more alignment issue --- homeassistant/components/swisscom/device_tracker.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index 98965af15136b8..adb018a4b4bea9 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -80,9 +80,18 @@ def get_swisscom_data(self): {"service":"Devices", "method":"get", "parameters":{"expression":"lan and not self"}}""" - request = requests.post(url, headers=headers, data=data, timeout=10) - devices = {} + + try: + request = requests.post(url, headers=headers, data=data, timeout=10) + except ( + requests.exceptions.ConnectionError, + requests.exceptions.Timeout, + requests.exceptions.ConnectTimeout, + ): + _LOGGER.info("No response from Swisscom Internet Box") + return devices + for device in request.json()["status"]: try: devices[device["Key"]] = { From 45b53c8e8232c8c0fd5b3dece2126f3bf7e6d6ae Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 9 Nov 2019 00:32:20 +0000 Subject: [PATCH 1509/3953] [ci skip] Translation update --- .../components/almond/.translations/lb.json | 3 ++- .../components/almond/.translations/no.json | 3 ++- .../components/climate/.translations/ca.json | 8 ++++++ .../components/climate/.translations/lb.json | 17 ++++++++++++ .../components/climate/.translations/no.json | 17 ++++++++++++ .../components/climate/.translations/ru.json | 17 ++++++++++++ .../climate/.translations/zh-Hant.json | 9 +++++++ .../components/deconz/.translations/no.json | 18 ++++++++++++- .../components/fan/.translations/lb.json | 16 ++++++++++++ .../components/fan/.translations/no.json | 16 ++++++++++++ .../components/fan/.translations/ru.json | 4 +++ .../components/fan/.translations/zh-Hant.json | 4 +++ .../huawei_lte/.translations/no.json | 5 +++- .../components/lock/.translations/lb.json | 4 +++ .../components/lock/.translations/no.json | 4 +++ .../components/vacuum/.translations/ca.json | 7 +++++ .../components/vacuum/.translations/lb.json | 16 ++++++++++++ .../components/vacuum/.translations/no.json | 16 ++++++++++++ .../components/vacuum/.translations/ru.json | 8 ++++++ .../vacuum/.translations/zh-Hant.json | 4 +++ .../components/wled/.translations/fr.json | 5 ++++ .../components/wled/.translations/lb.json | 26 +++++++++++++++++++ .../components/wled/.translations/no.json | 26 +++++++++++++++++++ 23 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/climate/.translations/ca.json create mode 100644 homeassistant/components/climate/.translations/lb.json create mode 100644 homeassistant/components/climate/.translations/no.json create mode 100644 homeassistant/components/climate/.translations/ru.json create mode 100644 homeassistant/components/fan/.translations/lb.json create mode 100644 homeassistant/components/fan/.translations/no.json create mode 100644 homeassistant/components/vacuum/.translations/ca.json create mode 100644 homeassistant/components/vacuum/.translations/lb.json create mode 100644 homeassistant/components/vacuum/.translations/no.json create mode 100644 homeassistant/components/wled/.translations/fr.json create mode 100644 homeassistant/components/wled/.translations/lb.json create mode 100644 homeassistant/components/wled/.translations/no.json diff --git a/homeassistant/components/almond/.translations/lb.json b/homeassistant/components/almond/.translations/lb.json index f74874d283aa1c..30cb8b8689148a 100644 --- a/homeassistant/components/almond/.translations/lb.json +++ b/homeassistant/components/almond/.translations/lb.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Almond Kont konfigur\u00e9ieren.", - "cannot_connect": "Kann sech net mam Almond Server verbannen." + "cannot_connect": "Kann sech net mam Almond Server verbannen.", + "missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond." }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/no.json b/homeassistant/components/almond/.translations/no.json index 37888debe785e6..6ea2de635b191c 100644 --- a/homeassistant/components/almond/.translations/no.json +++ b/homeassistant/components/almond/.translations/no.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "Du kan bare konfigurere en Almond konto.", - "cannot_connect": "Kan ikke koble til Almond-serveren." + "cannot_connect": "Kan ikke koble til Almond-serveren.", + "missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond." }, "title": "Almond" } diff --git a/homeassistant/components/climate/.translations/ca.json b/homeassistant/components/climate/.translations/ca.json new file mode 100644 index 00000000000000..480d90310d97b7 --- /dev/null +++ b/homeassistant/components/climate/.translations/ca.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_hvac_mode": "{entity_name} est\u00e0 configurat/ada en un mode HVAC espec\u00edfic", + "is_preset_mode": "{entity_name} est\u00e0 configurat/ada en un mode preestablert espec\u00edfic" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/lb.json b/homeassistant/components/climate/.translations/lb.json new file mode 100644 index 00000000000000..72ab7efc62380d --- /dev/null +++ b/homeassistant/components/climate/.translations/lb.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "HVAC Modus \u00e4nnere fir {entity_name}", + "set_preset_mode": "Preset \u00e4nnere fir {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "\n{entity_name} ass op e spezifesche HVAC Modus gesat", + "is_preset_mode": "{entity_name} ass op e spezifesche preset Modus gesat" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} gemoosse Fiichtegkeet ge\u00e4nnert", + "current_temperature_changed": "{entity_name} gemoossen Temperatur ge\u00e4nnert", + "hvac_mode_changed": "{entity_name} HVAC Modus ge\u00e4nnert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/no.json b/homeassistant/components/climate/.translations/no.json new file mode 100644 index 00000000000000..2d95c63a6aef32 --- /dev/null +++ b/homeassistant/components/climate/.translations/no.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Endre HVAC-modus p\u00e5 {entity_name}", + "set_preset_mode": "Endre forh\u00e5ndsinnstilling p\u00e5 {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} er satt til en spesifikk HVAC-modus", + "is_preset_mode": "{entity_name} er satt til en spesifikk forh\u00e5ndsinnstilt modus" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} m\u00e5lt fuktighet er endret", + "current_temperature_changed": "{entity_name} m\u00e5lt temperatur er endret", + "hvac_mode_changed": "{entity_name} HVAC-modus er endret" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/ru.json b/homeassistant/components/climate/.translations/ru.json new file mode 100644 index 00000000000000..045f96137d2bd9 --- /dev/null +++ b/homeassistant/components/climate/.translations/ru.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \"{entity_name}\"", + "set_preset_mode": "\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430\u0431\u043e\u0440 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \"{entity_name}\"" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043d\u043e\u043c \u0440\u0435\u0436\u0438\u043c\u0435 \u0440\u0430\u0431\u043e\u0442\u044b", + "is_preset_mode": "{entity_name} \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043d\u0430\u0431\u043e\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u043d\u043e\u0439 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u0438", + "current_temperature_changed": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u043d\u043e\u0439 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", + "hvac_mode_changed": "{entity_name} \u043c\u0435\u043d\u044f\u0435\u0442 \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/zh-Hant.json b/homeassistant/components/climate/.translations/zh-Hant.json index a1d603c5552130..1d39eecc056166 100644 --- a/homeassistant/components/climate/.translations/zh-Hant.json +++ b/homeassistant/components/climate/.translations/zh-Hant.json @@ -3,6 +3,15 @@ "action_type": { "set_hvac_mode": "\u8b8a\u66f4 {entity_name} HVAC \u6a21\u5f0f", "set_preset_mode": "\u8b8a\u66f4 {entity_name} \u8a2d\u5b9a\u6a21\u5f0f" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a HVAC \u6a21\u5f0f", + "is_preset_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a\u8a2d\u5b9a\u6a21\u5f0f" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \u91cf\u6e2c\u6fd5\u5ea6\u5df2\u8b8a\u66f4", + "current_temperature_changed": "{entity_name} \u91cf\u6e2c\u6eab\u5ea6\u5df2\u8b8a\u66f4", + "hvac_mode_changed": "{entity_name} HVAC \u6a21\u5f0f\u5df2\u8b8a\u66f4" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 7db8f3f118dc1a..2c1dd687454ec4 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -55,10 +55,17 @@ "left": "Venstre", "open": "\u00c5pen", "right": "H\u00f8yre", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6", "turn_off": "Skru av", "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { + "remote_awakened": "Enheten ble vekket", "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", - "remote_gyro_activated": "Enhet er ristet" + "remote_double_tap": "Enheten \" {subtype} \" dobbeltklikket", + "remote_falling": "Enheten er i fritt fall", + "remote_gyro_activated": "Enhet er ristet", + "remote_moved": "Enheten ble flyttet med \"{under type}\" opp", + "remote_rotate_from_side_1": "Enheten rotert fra \"side 1\" til \" {subtype} \"", + "remote_rotate_from_side_2": "Enheten rotert fra \"side 2\" til \" {subtype} \"", + "remote_rotate_from_side_3": "Enheten rotert fra \"side 3\" til \" {subtype} \"", + "remote_rotate_from_side_4": "Enheten rotert fra \"side 4\" til \" {subtype} \"", + "remote_rotate_from_side_5": "Enheten rotert fra \"side 5\" til \" {subtype} \"", + "remote_rotate_from_side_6": "Enheten rotert fra \"side 6\" til \" {subtype} \"" } }, "options": { diff --git a/homeassistant/components/fan/.translations/lb.json b/homeassistant/components/fan/.translations/lb.json new file mode 100644 index 00000000000000..316a77d471d8ef --- /dev/null +++ b/homeassistant/components/fan/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name} ausschalten", + "turn_on": "{entity_name} uschalten" + }, + "condtion_type": { + "is_off": "{entity_name} ass aus", + "is_on": "{entity_name} ass un" + }, + "trigger_type": { + "turned_off": "{entity_name} gouf ausgeschalt", + "turned_on": "{entity_name} gouf ugeschalt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/no.json b/homeassistant/components/fan/.translations/no.json new file mode 100644 index 00000000000000..73917ac45c4e72 --- /dev/null +++ b/homeassistant/components/fan/.translations/no.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} er av", + "is_on": "{entity_name} er p\u00e5" + }, + "trigger_type": { + "turned_off": "{entity_name} sl\u00e5tt av", + "turned_on": "{entity_name} sl\u00e5tt p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/ru.json b/homeassistant/components/fan/.translations/ru.json index abbecabd13f097..4fd5ebe28c5e60 100644 --- a/homeassistant/components/fan/.translations/ru.json +++ b/homeassistant/components/fan/.translations/ru.json @@ -7,6 +7,10 @@ "condtion_type": { "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" + }, + "trigger_type": { + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/zh-Hant.json b/homeassistant/components/fan/.translations/zh-Hant.json index 58496f9e9bc3e1..4b34f6e0165b22 100644 --- a/homeassistant/components/fan/.translations/zh-Hant.json +++ b/homeassistant/components/fan/.translations/zh-Hant.json @@ -4,6 +4,10 @@ "turn_off": "\u95dc\u9589 {entity_name}", "turn_on": "\u958b\u555f {entity_name}" }, + "condtion_type": { + "is_off": "{entity_name} \u95dc\u9589", + "is_on": "{entity_name} \u958b\u555f" + }, "trigger_type": { "turned_off": "{entity_name} \u5df2\u95dc\u9589", "turned_on": "{entity_name} \u5df2\u958b\u555f" diff --git a/homeassistant/components/huawei_lte/.translations/no.json b/homeassistant/components/huawei_lte/.translations/no.json index d06a356e9987de..35a5d531c5dd20 100644 --- a/homeassistant/components/huawei_lte/.translations/no.json +++ b/homeassistant/components/huawei_lte/.translations/no.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Denne enheten er allerede konfigurert" + "already_configured": "Denne enheten er allerede konfigurert", + "already_in_progress": "Denne enheten blir allerede konfigurert", + "not_huawei_lte": "Ikke en Huawei LTE-enhet" }, "error": { "connection_failed": "Tilkoblingen mislyktes", + "connection_timeout": "Tilkoblingsavbrudd", "incorrect_password": "feil passord", "incorrect_username": "Feil brukernavn", "incorrect_username_or_password": "Feil brukernavn eller passord", diff --git a/homeassistant/components/lock/.translations/lb.json b/homeassistant/components/lock/.translations/lb.json index 90dd7e6087aebd..1bdfa9ac4ec1ff 100644 --- a/homeassistant/components/lock/.translations/lb.json +++ b/homeassistant/components/lock/.translations/lb.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} ass gespaart", "is_unlocked": "{entity_name} ass entspaart" + }, + "trigger_type": { + "locked": "{entity_name} gespaart", + "unlocked": "{entity_name} entspaart" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/no.json b/homeassistant/components/lock/.translations/no.json index 28c809a82d1545..de34f40bc38ef3 100644 --- a/homeassistant/components/lock/.translations/no.json +++ b/homeassistant/components/lock/.translations/no.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} er l\u00e5st", "is_unlocked": "{entity_name} er l\u00e5st opp" + }, + "trigger_type": { + "locked": "{entity_name} l\u00e5st", + "unlocked": "{entity_name} l\u00e5st opp" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/ca.json b/homeassistant/components/vacuum/.translations/ca.json new file mode 100644 index 00000000000000..c004120a8c7ab9 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/ca.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "condtion_type": { + "is_cleaning": "{entity_name} est\u00e0 netejant" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/lb.json b/homeassistant/components/vacuum/.translations/lb.json new file mode 100644 index 00000000000000..6d984b997fa99c --- /dev/null +++ b/homeassistant/components/vacuum/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Looss {entity_name} botzen", + "dock": "Sch\u00e9ck {entity_name} z\u00e9reck zur Statioun" + }, + "condtion_type": { + "is_cleaning": "{entity_name} botzt", + "is_docked": "{entity_name} ass an der Statioun" + }, + "trigger_type": { + "cleaning": "{entity_name} huet ugefaange mam botzen", + "docked": "{entity_name} an der Statioun" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/no.json b/homeassistant/components/vacuum/.translations/no.json new file mode 100644 index 00000000000000..7d6475f8cef298 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/no.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "La {entity_name} rengj\u00f8res", + "dock": "La {entity_name} tilbake til dock" + }, + "condtion_type": { + "is_cleaning": "{entity_name} rengj\u00f8res", + "is_docked": "{entity_name} er docked" + }, + "trigger_type": { + "cleaning": "{entity_name} startet rengj\u00f8ringen", + "docked": "{entity_name} dokket" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/ru.json b/homeassistant/components/vacuum/.translations/ru.json index 3026bac3012fea..c727e8f6ea3d06 100644 --- a/homeassistant/components/vacuum/.translations/ru.json +++ b/homeassistant/components/vacuum/.translations/ru.json @@ -1,8 +1,16 @@ { "device_automation": { + "action_type": { + "clean": "\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c {entity_name} \u0434\u0435\u043b\u0430\u0442\u044c \u0443\u0431\u043e\u0440\u043a\u0443", + "dock": "\u0412\u0435\u0440\u043d\u0443\u0442\u044c {entity_name} \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u044e" + }, "condtion_type": { "is_cleaning": "{entity_name} \u0434\u0435\u043b\u0430\u0435\u0442 \u0443\u0431\u043e\u0440\u043a\u0443", "is_docked": "{entity_name} \u0443 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u0438" + }, + "trigger_type": { + "cleaning": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u0443\u0431\u043e\u0440\u043a\u0443", + "docked": "{entity_name} \u0441\u0442\u044b\u043a\u0443\u0435\u0442\u0441\u044f \u0441 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u0435\u0439" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/zh-Hant.json b/homeassistant/components/vacuum/.translations/zh-Hant.json index d6831f81873eb8..f0ad431afc944a 100644 --- a/homeassistant/components/vacuum/.translations/zh-Hant.json +++ b/homeassistant/components/vacuum/.translations/zh-Hant.json @@ -4,6 +4,10 @@ "clean": "\u555f\u52d5 {entity_name} \u6e05\u9664", "dock": "\u555f\u52d5 {entity_name} \u56de\u5230\u5145\u96fb\u7ad9" }, + "condtion_type": { + "is_cleaning": "{entity_name} \u6b63\u5728\u6e05\u6383", + "is_docked": "{entity_name} \u65bc\u5145\u96fb\u7ad9" + }, "trigger_type": { "cleaning": "{entity_name} \u958b\u59cb\u6e05\u6383", "docked": "{entity_name} \u5df2\u56de\u5145\u96fb\u7ad9" diff --git a/homeassistant/components/wled/.translations/fr.json b/homeassistant/components/wled/.translations/fr.json new file mode 100644 index 00000000000000..a9ef8aa567afad --- /dev/null +++ b/homeassistant/components/wled/.translations/fr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/lb.json b/homeassistant/components/wled/.translations/lb.json new file mode 100644 index 00000000000000..ea23956af42574 --- /dev/null +++ b/homeassistant/components/wled/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebsen WLED Apparat ass scho konfigur\u00e9iert.", + "connection_error": "Feeler beim verbannen mam WLED Apparat." + }, + "error": { + "connection_error": "Feeler beim verbannen mam WLED Apparat." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Numm oder IP Adresse" + }, + "description": "\u00c4ren WLED als Integratioun mam Home Assistant ariichten.", + "title": "\u00c4ren WLED verbannen" + }, + "zeroconf_confirm": { + "description": "W\u00ebllt dir den WLED mam Numm `{name}` am 'Home Assistant dob\u00e4isetzen?", + "title": "Entdeckten WLED Apparat" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/no.json b/homeassistant/components/wled/.translations/no.json new file mode 100644 index 00000000000000..b2dc9cb6547f1d --- /dev/null +++ b/homeassistant/components/wled/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Denne WLED-enheten er allerede konfigurert.", + "connection_error": "Kunne ikke koble til WLED-enheten." + }, + "error": { + "connection_error": "Kunne ikke koble til WLED-enheten." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Vert eller IP-adresse" + }, + "description": "Konfigurer WLED til \u00e5 integreres med Home Assistant.", + "title": "Linken din WLED" + }, + "zeroconf_confirm": { + "description": "Vil du legge til WLED med navnet ' {name} ' i Home Assistant?", + "title": "Oppdaget WLED-enhet" + } + }, + "title": "WLED" + } +} \ No newline at end of file From 97224df4fd9ce72d629bec322a17ef0d47881776 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Fri, 8 Nov 2019 22:35:45 -0800 Subject: [PATCH 1510/3953] Fix Abode capture_image and trigger_quick_action services (#28546) * Fix Abode services * Bump abodepy version * Updated using dispatcher helper * Code review fixes * Removed init method from AbodeQuickActionBinary Sensor --- homeassistant/components/abode/__init__.py | 41 ++++++++++++------- .../components/abode/alarm_control_panel.py | 3 +- .../components/abode/binary_sensor.py | 19 ++++++--- homeassistant/components/abode/camera.py | 16 +++++--- homeassistant/components/abode/const.py | 3 ++ homeassistant/components/abode/cover.py | 8 ++-- homeassistant/components/abode/light.py | 6 +-- homeassistant/components/abode/lock.py | 8 ++-- homeassistant/components/abode/manifest.json | 2 +- homeassistant/components/abode/sensor.py | 3 +- homeassistant/components/abode/switch.py | 8 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 13 files changed, 73 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 76c14d7917ff72..1689576bc7f9e0 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -20,10 +20,17 @@ CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity -from .const import ATTRIBUTION, DOMAIN, DEFAULT_CACHEDB +from .const import ( + ATTRIBUTION, + DOMAIN, + DEFAULT_CACHEDB, + SIGNAL_CAPTURE_IMAGE, + SIGNAL_TRIGGER_QUICK_ACTION, +) _LOGGER = logging.getLogger(__name__) @@ -89,7 +96,7 @@ def __init__(self, abode, polling): self.abode = abode self.polling = polling - self.devices = [] + self.entity_ids = set() self.logout_listener = None @@ -179,27 +186,29 @@ def capture_image(call): """Capture a new image.""" entity_ids = call.data.get(ATTR_ENTITY_ID) - target_devices = [ - device - for device in hass.data[DOMAIN].devices - if device.entity_id in entity_ids + target_entities = [ + entity_id + for entity_id in hass.data[DOMAIN].entity_ids + if entity_id in entity_ids ] - for device in target_devices: - device.capture() + for entity_id in target_entities: + signal = SIGNAL_CAPTURE_IMAGE.format(entity_id) + dispatcher_send(hass, signal) def trigger_quick_action(call): """Trigger a quick action.""" entity_ids = call.data.get(ATTR_ENTITY_ID, None) - target_devices = [ - device - for device in hass.data[DOMAIN].devices - if device.entity_id in entity_ids + target_entities = [ + entity_id + for entity_id in hass.data[DOMAIN].entity_ids + if entity_id in entity_ids ] - for device in target_devices: - device.trigger() + for entity_id in target_entities: + signal = SIGNAL_TRIGGER_QUICK_ACTION.format(entity_id) + dispatcher_send(hass, signal) hass.services.register( DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA @@ -290,6 +299,7 @@ async def async_added_to_hass(self): self._device.device_id, self._update_callback, ) + self.hass.data[DOMAIN].entity_ids.add(self.entity_id) async def async_will_remove_from_hass(self): """Unsubscribe from device events.""" @@ -352,13 +362,14 @@ def __init__(self, data, automation, event=None): self._event = event async def async_added_to_hass(self): - """Subscribe Abode events.""" + """Subscribe to a group of Abode timeline events.""" if self._event: self.hass.async_add_job( self._data.abode.events.add_event_callback, self._event, self._update_callback, ) + self.hass.data[DOMAIN].entity_ids.add(self.entity_id) @property def should_poll(self): diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index f774e773cb527c..f1ff08f3a0ac2e 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -23,9 +23,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up an alarm control panel for an Abode device.""" + """Set up Abode alarm control panel device.""" data = hass.data[DOMAIN] - async_add_entities( [AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))] ) diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 31f744484963f7..56c7bbcc1ff09b 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -5,9 +5,10 @@ import abodepy.helpers.timeline as TIMELINE from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import AbodeAutomation, AbodeDevice -from .const import DOMAIN +from .const import DOMAIN, SIGNAL_TRIGGER_QUICK_ACTION _LOGGER = logging.getLogger(__name__) @@ -18,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up a sensor for an Abode device.""" + """Set up Abode binary sensor devices.""" data = hass.data[DOMAIN] device_types = [ @@ -29,19 +30,19 @@ async def async_setup_entry(hass, config_entry, async_add_entities): CONST.TYPE_OPENING, ] - devices = [] + entities = [] for device in data.abode.get_devices(generic_type=device_types): - devices.append(AbodeBinarySensor(data, device)) + entities.append(AbodeBinarySensor(data, device)) for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION): - devices.append( + entities.append( AbodeQuickActionBinarySensor( data, automation, TIMELINE.AUTOMATION_EDIT_GROUP ) ) - async_add_entities(devices) + async_add_entities(entities) class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): @@ -61,6 +62,12 @@ def device_class(self): class AbodeQuickActionBinarySensor(AbodeAutomation, BinarySensorDevice): """A binary sensor implementation for Abode quick action automations.""" + async def async_added_to_hass(self): + """Subscribe Abode events.""" + await super().async_added_to_hass() + signal = SIGNAL_TRIGGER_QUICK_ACTION.format(self.entity_id) + async_dispatcher_connect(self.hass, signal, self.trigger) + def trigger(self): """Trigger a quick automation.""" self._automation.trigger() diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index e98a59a985ce64..c6f366e0e51a29 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -7,10 +7,11 @@ import requests from homeassistant.components.camera import Camera +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import Throttle from . import AbodeDevice -from .const import DOMAIN +from .const import DOMAIN, SIGNAL_CAPTURE_IMAGE MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) @@ -23,15 +24,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up a camera for an Abode device.""" - + """Set up Abode camera devices.""" data = hass.data[DOMAIN] - devices = [] + entities = [] + for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): - devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) + entities.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) - async_add_entities(devices) + async_add_entities(entities) class AbodeCamera(AbodeDevice, Camera): @@ -54,6 +55,9 @@ async def async_added_to_hass(self): self._capture_callback, ) + signal = SIGNAL_CAPTURE_IMAGE.format(self.entity_id) + async_dispatcher_connect(self.hass, signal, self.capture) + def capture(self): """Request a new image capture.""" return self._device.capture() diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py index 092843ba2120b5..267eb04f72e408 100644 --- a/homeassistant/components/abode/const.py +++ b/homeassistant/components/abode/const.py @@ -3,3 +3,6 @@ ATTRIBUTION = "Data provided by goabode.com" DEFAULT_CACHEDB = "abodepy_cache.pickle" + +SIGNAL_CAPTURE_IMAGE = "abode_camera_capture_{}" +SIGNAL_TRIGGER_QUICK_ACTION = "abode_trigger_quick_action_{}" diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index ebe59ee45c7391..a4fce7e7b8acac 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -18,14 +18,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode cover devices.""" - data = hass.data[DOMAIN] - devices = [] + entities = [] + for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): - devices.append(AbodeCover(data, device)) + entities.append(AbodeCover(data, device)) - async_add_entities(devices) + async_add_entities(entities) class AbodeCover(AbodeDevice, CoverDevice): diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 163982d040eab6..f29270d264c21c 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -33,12 +33,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode light devices.""" data = hass.data[DOMAIN] - devices = [] + entities = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT): - devices.append(AbodeLight(data, device)) + entities.append(AbodeLight(data, device)) - async_add_entities(devices) + async_add_entities(entities) class AbodeLight(AbodeDevice, Light): diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 11f792f88fd8c8..e7ed40849de1aa 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -18,14 +18,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode lock devices.""" - data = hass.data[DOMAIN] - devices = [] + entities = [] + for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): - devices.append(AbodeLock(data, device)) + entities.append(AbodeLock(data, device)) - async_add_entities(devices) + async_add_entities(entities) class AbodeLock(AbodeDevice, LockDevice): diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index b54120c7cbdf33..0a4307ff737dff 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", "requirements": [ - "abodepy==0.16.6" + "abodepy==0.16.7" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 6ee0cf59cbf723..d84bfe52441d8f 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -28,8 +28,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up a sensor for an Abode device.""" + """Set up Abode sensor devices.""" data = hass.data[DOMAIN] + entities = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 7bd7f394d30bee..c092c1ef3f0b48 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -21,17 +21,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode switch devices.""" data = hass.data[DOMAIN] - devices = [] + entities = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH): - devices.append(AbodeSwitch(data, device)) + entities.append(AbodeSwitch(data, device)) for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION): - devices.append( + entities.append( AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP) ) - async_add_entities(devices) + async_add_entities(entities) class AbodeSwitch(AbodeDevice, SwitchDevice): diff --git a/requirements_all.txt b/requirements_all.txt index 29b94d40644704..a34191c4a5c197 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -106,7 +106,7 @@ WazeRouteCalculator==0.10 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.16.6 +abodepy==0.16.7 # homeassistant.components.mcp23017 adafruit-blinka==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ceb63df9896bf4..691e8d413d58c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -26,7 +26,7 @@ RtmAPI==0.7.2 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.16.6 +abodepy==0.16.7 # homeassistant.components.androidtv adb-shell==0.0.8 From 58eeea903f6208dec607ad5b7c68a9429e3cfffe Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Sat, 9 Nov 2019 10:14:46 +0300 Subject: [PATCH 1511/3953] Add pcal9535a integration (#26563) * Support for PCAL9535A chip Signed-off-by: Denis Shulyaka * Code review changes * Code review changes * Fix import order * Fix import order * Apply suggestions from code review --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/pcal9535a/__init__.py | 3 + .../components/pcal9535a/binary_sensor.py | 93 ++++++++++++++++ .../components/pcal9535a/manifest.json | 10 ++ homeassistant/components/pcal9535a/switch.py | 102 ++++++++++++++++++ requirements_all.txt | 3 + 7 files changed, 213 insertions(+) create mode 100644 homeassistant/components/pcal9535a/__init__.py create mode 100644 homeassistant/components/pcal9535a/binary_sensor.py create mode 100644 homeassistant/components/pcal9535a/manifest.json create mode 100644 homeassistant/components/pcal9535a/switch.py diff --git a/.coveragerc b/.coveragerc index 169b73b7899697..b0d6a40c7f7fb3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -499,6 +499,7 @@ omit = homeassistant/components/panasonic_bluray/media_player.py homeassistant/components/panasonic_viera/media_player.py homeassistant/components/pandora/media_player.py + homeassistant/components/pcal9535a/* homeassistant/components/pencom/switch.py homeassistant/components/philips_js/media_player.py homeassistant/components/pi_hole/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 0a02fbc53218d9..cb3d0817d59fea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -225,6 +225,7 @@ homeassistant/components/oru/* @bvlaicu homeassistant/components/owlet/* @oblogic7 homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend +homeassistant/components/pcal9535a/* @Shulyaka homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff @johnluetke diff --git a/homeassistant/components/pcal9535a/__init__.py b/homeassistant/components/pcal9535a/__init__.py new file mode 100644 index 00000000000000..fa1295939be2e1 --- /dev/null +++ b/homeassistant/components/pcal9535a/__init__.py @@ -0,0 +1,3 @@ +"""Support for I2C PCAL9535A chip.""" + +DOMAIN = "pcal9535a" diff --git a/homeassistant/components/pcal9535a/binary_sensor.py b/homeassistant/components/pcal9535a/binary_sensor.py new file mode 100644 index 00000000000000..fd4e92ccf0335d --- /dev/null +++ b/homeassistant/components/pcal9535a/binary_sensor.py @@ -0,0 +1,93 @@ +"""Support for binary sensor using I2C PCAL9535A chip.""" +import logging + +import voluptuous as vol +from pcal9535a import PCAL9535A + +from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.const import DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_INVERT_LOGIC = "invert_logic" +CONF_I2C_ADDRESS = "i2c_address" +CONF_I2C_BUS = "i2c_bus" +CONF_PINS = "pins" +CONF_PULL_MODE = "pull_mode" + +MODE_UP = "UP" +MODE_DOWN = "DOWN" +MODE_DISABLED = "DISABLED" + +DEFAULT_INVERT_LOGIC = False +DEFAULT_I2C_ADDRESS = 0x20 +DEFAULT_I2C_BUS = 1 +DEFAULT_PULL_MODE = MODE_DISABLED + +_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PINS): _SENSORS_SCHEMA, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): vol.All( + vol.Upper, vol.In([MODE_UP, MODE_DOWN, MODE_DISABLED]) + ), + vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), + vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the PCAL9535A binary sensors.""" + pull_mode = config[CONF_PULL_MODE] + invert_logic = config[CONF_INVERT_LOGIC] + i2c_address = config[CONF_I2C_ADDRESS] + bus = config[CONF_I2C_BUS] + + pcal = PCAL9535A(bus, i2c_address) + + binary_sensors = [] + pins = config[CONF_PINS] + + for pin_num, pin_name in pins.items(): + pin = pcal.get_pin(pin_num // 8, pin_num % 8) + binary_sensors.append( + PCAL9535ABinarySensor(pin_name, pin, pull_mode, invert_logic) + ) + + add_entities(binary_sensors, True) + + +class PCAL9535ABinarySensor(BinarySensorDevice): + """Represent a binary sensor that uses PCAL9535A.""" + + def __init__(self, name, pin, pull_mode, invert_logic): + """Initialize the PCAL9535A binary sensor.""" + self._name = name or DEVICE_DEFAULT_NAME + self._pin = pin + self._pin.input = True + self._pin.inverted = invert_logic + if pull_mode == "DISABLED": + self._pin.pullup = 0 + elif pull_mode == "DOWN": + self._pin.pullup = -1 + else: + self._pin.pullup = 1 + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the cached state of the entity.""" + return self._state + + def update(self): + """Update the GPIO state.""" + self._state = self._pin.level diff --git a/homeassistant/components/pcal9535a/manifest.json b/homeassistant/components/pcal9535a/manifest.json new file mode 100644 index 00000000000000..e2fb140c7a91dc --- /dev/null +++ b/homeassistant/components/pcal9535a/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "pcal9535a", + "name": "PCAL9535A I/O Expander", + "documentation": "https://www.home-assistant.io/components/pcal9535a", + "requirements": [ + "pcal9535a==0.7" + ], + "dependencies": [], + "codeowners": ["@Shulyaka"] +} diff --git a/homeassistant/components/pcal9535a/switch.py b/homeassistant/components/pcal9535a/switch.py new file mode 100644 index 00000000000000..faebce5d67e396 --- /dev/null +++ b/homeassistant/components/pcal9535a/switch.py @@ -0,0 +1,102 @@ +"""Support for switch sensor using I2C PCAL9535A chip.""" +import logging + +import voluptuous as vol +from pcal9535a import PCAL9535A + +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import DEVICE_DEFAULT_NAME +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_INVERT_LOGIC = "invert_logic" +CONF_I2C_ADDRESS = "i2c_address" +CONF_I2C_BUS = "i2c_bus" +CONF_PINS = "pins" +CONF_STRENGTH = "strength" + +STRENGTH_025 = "0.25" +STRENGTH_050 = "0.5" +STRENGTH_075 = "0.75" +STRENGTH_100 = "1.0" + +DEFAULT_INVERT_LOGIC = False +DEFAULT_I2C_ADDRESS = 0x20 +DEFAULT_I2C_BUS = 1 +DEFAULT_STRENGTH = STRENGTH_100 + +_SWITCHES_SCHEMA = vol.Schema({cv.positive_int: cv.string}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PINS): _SWITCHES_SCHEMA, + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, + vol.Optional(CONF_STRENGTH, default=DEFAULT_STRENGTH): vol.In( + [STRENGTH_025, STRENGTH_050, STRENGTH_075, STRENGTH_100] + ), + vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): vol.Coerce(int), + vol.Optional(CONF_I2C_BUS, default=DEFAULT_I2C_BUS): cv.positive_int, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the PCAL9535A devices.""" + invert_logic = config[CONF_INVERT_LOGIC] + i2c_address = config[CONF_I2C_ADDRESS] + bus = config[CONF_I2C_BUS] + + pcal = PCAL9535A(bus, i2c_address) + + switches = [] + pins = config[CONF_PINS] + for pin_num, pin_name in pins.items(): + pin = pcal.get_pin(pin_num // 8, pin_num % 8) + switches.append(PCAL9535ASwitch(pin_name, pin, invert_logic)) + + add_entities(switches) + + +class PCAL9535ASwitch(SwitchDevice): + """Representation of a PCAL9535A output pin.""" + + def __init__(self, name, pin, invert_logic): + """Initialize the pin.""" + self._name = name or DEVICE_DEFAULT_NAME + self._pin = pin + self._pin.inverted = invert_logic + self._pin.input = False + self._state = self._pin.level + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def assumed_state(self): + """Return true if optimistic updates are used.""" + return True + + def turn_on(self, **kwargs): + """Turn the device on.""" + self._pin.level = True + self._state = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the device off.""" + self._pin.level = False + self._state = False + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index a34191c4a5c197..6f0b40230edede 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -944,6 +944,9 @@ panacotta==0.1 # homeassistant.components.panasonic_viera panasonic_viera==0.3.2 +# homeassistant.components.pcal9535a +pcal9535a==0.7 + # homeassistant.components.dunehd pdunehd==1.3 From 0954f7d3bef313686686db3ee539086ee441fc1e Mon Sep 17 00:00:00 2001 From: bluestripe Date: Sat, 9 Nov 2019 08:16:53 +0100 Subject: [PATCH 1512/3953] Add bluesound speaker group attribute (#28142) * Added bluesound speaker group attribute. * Changed code to fix failing tests. * Changed condition checking for empty group list. * Investigating CI pipeline error * Changed back to the code that passed CI earlier * Changed condition on existence of list and sorting of bluesound_group * Re-introduced guard on group_name None --- .../components/bluesound/media_player.py | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 762f231b341089..7b2719c1e4e9ee 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -53,6 +53,7 @@ _LOGGER = logging.getLogger(__name__) +ATTR_BLUESOUND_GROUP = "bluesound_group" ATTR_MASTER = "master" DATA_BLUESOUND = "bluesound" @@ -219,6 +220,8 @@ def __init__(self, hass, host, port=None, name=None, init_callback=None): self._master = None self._is_master = False self._group_name = None + self._group_list = [] + self._bluesound_device_name = None self._init_callback = init_callback if self.port is None: @@ -247,6 +250,8 @@ async def force_update_sync_status(self, on_updated_cb=None, raise_timeout=False if not self._name: self._name = self._sync_status.get("@name", self.host) + if not self._bluesound_device_name: + self._bluesound_device_name = self._sync_status.get("@name", self.host) if not self._icon: self._icon = self._sync_status.get("@icon", self.host) @@ -331,7 +336,6 @@ async def send_bluesound_command( self, method, raise_timeout=False, allow_offline=False ): """Send command to the player.""" - if not self._is_online and not allow_offline: return @@ -371,7 +375,6 @@ async def send_bluesound_command( async def async_update_status(self): """Use the poll session to always get the status of the player.""" - response = None url = "Status" @@ -402,6 +405,10 @@ async def async_update_status(self): if group_name != self._group_name: _LOGGER.debug("Group name change detected on device: %s", self.host) self._group_name = group_name + + # rebuild ordered list of entity_ids that are in the group, master is first + self._group_list = self.rebuild_bluesound_group() + # the sleep is needed to make sure that the # devices is synced await asyncio.sleep(1) @@ -659,6 +666,11 @@ def name(self): """Return the name of the device.""" return self._name + @property + def bluesound_device_name(self): + """Return the device name as returned by the device.""" + return self._bluesound_device_name + @property def icon(self): """Return the icon of the device.""" @@ -690,7 +702,6 @@ def source_list(self): @property def source(self): """Name of the current input source.""" - if self._status is None or (self.is_grouped and not self.is_master): return None @@ -831,6 +842,39 @@ async def async_join(self, master): else: _LOGGER.error("Master not found %s", master_device) + @property + def device_state_attributes(self): + """List members in group.""" + attributes = {} + if self._group_list: + attributes = {ATTR_BLUESOUND_GROUP: self._group_list} + + attributes[ATTR_MASTER] = self._is_master + + return attributes + + def rebuild_bluesound_group(self): + """Rebuild the list of entities in speaker group.""" + if self._group_name is None: + return None + + bluesound_group = [] + + device_group = self._group_name.split("+") + + sorted_entities = sorted( + self._hass.data[DATA_BLUESOUND], + key=lambda entity: entity.is_master, + reverse=True, + ) + bluesound_group = [ + entity.name + for entity in sorted_entities + if entity.bluesound_device_name in device_group + ] + + return bluesound_group + async def async_unjoin(self): """Unjoin the player from a group.""" if self._master is None: From fc95a3d0889d7e442d55ff331d5252406ec74e06 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 9 Nov 2019 14:07:01 +0100 Subject: [PATCH 1513/3953] Fix xiaomi vacuum tests (#28658) * Fix xiaomi exceptions test * Fix xiaomi_specific_services test * Fix remaining xiaomi miio vacuum tests * Clean up --- tests/components/xiaomi_miio/test_vacuum.py | 298 ++++++++++---------- 1 file changed, 154 insertions(+), 144 deletions(-) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index ceeb7a92615300..18da270960c045 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -15,11 +15,10 @@ SERVICE_RETURN_TO_BASE, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, - SERVICE_START_PAUSE, + SERVICE_START, SERVICE_STOP, - SERVICE_TOGGLE, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, + STATE_CLEANING, + STATE_ERROR, ) from homeassistant.components.xiaomi_miio.vacuum import ( ATTR_CLEANED_AREA, @@ -55,91 +54,97 @@ PLATFORM = "xiaomi_miio" # calls made when device status is requested -status_calls = [ - mock.call.Vacuum().status(), - mock.call.Vacuum().consumable_status(), - mock.call.Vacuum().clean_history(), - mock.call.Vacuum().dnd_status(), +STATUS_CALLS = [ + mock.call.status(), + mock.call.consumable_status(), + mock.call.clean_history(), + mock.call.dnd_status(), ] -@pytest.fixture -def mock_mirobo_is_off(): +@pytest.fixture(name="mock_mirobo_is_got_error") +def mirobo_is_got_error_fixture(): """Mock mock_mirobo.""" mock_vacuum = mock.MagicMock() - mock_vacuum.Vacuum().status().data = {"test": "raw"} - mock_vacuum.Vacuum().status().is_on = False - mock_vacuum.Vacuum().status().fanspeed = 38 - mock_vacuum.Vacuum().status().got_error = True - mock_vacuum.Vacuum().status().error = "Error message" - mock_vacuum.Vacuum().status().battery = 82 - mock_vacuum.Vacuum().status().clean_area = 123.43218 - mock_vacuum.Vacuum().status().clean_time = timedelta( - hours=2, minutes=35, seconds=34 - ) - mock_vacuum.Vacuum().consumable_status().main_brush_left = timedelta( + mock_vacuum.status().data = {"test": "raw"} + mock_vacuum.status().is_on = False + mock_vacuum.status().fanspeed = 38 + mock_vacuum.status().got_error = True + mock_vacuum.status().error = "Error message" + mock_vacuum.status().battery = 82 + mock_vacuum.status().clean_area = 123.43218 + mock_vacuum.status().clean_time = timedelta(hours=2, minutes=35, seconds=34) + mock_vacuum.consumable_status().main_brush_left = timedelta( hours=12, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().consumable_status().side_brush_left = timedelta( + mock_vacuum.consumable_status().side_brush_left = timedelta( hours=12, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().consumable_status().filter_left = timedelta( + mock_vacuum.consumable_status().filter_left = timedelta( hours=12, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().clean_history().count = "35" - mock_vacuum.Vacuum().clean_history().total_area = 123.43218 - mock_vacuum.Vacuum().clean_history().total_duration = timedelta( + mock_vacuum.clean_history().count = "35" + mock_vacuum.clean_history().total_area = 123.43218 + mock_vacuum.clean_history().total_duration = timedelta( hours=11, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().status().state = "Test Xiaomi Charging" - mock_vacuum.Vacuum().dnd_status().enabled = True - mock_vacuum.Vacuum().dnd_status().start = time(hour=22, minute=0) - mock_vacuum.Vacuum().dnd_status().end = time(hour=6, minute=0) + mock_vacuum.status().state = "Test Xiaomi Charging" + mock_vacuum.dnd_status().enabled = True + mock_vacuum.dnd_status().start = time(hour=22, minute=0) + mock_vacuum.dnd_status().end = time(hour=6, minute=0) - with mock.patch.dict("sys.modules", {"miio": mock_vacuum}): + with mock.patch( + "homeassistant.components.xiaomi_miio.vacuum.Vacuum" + ) as mock_vaccum_cls: + mock_vaccum_cls.return_value = mock_vacuum yield mock_vacuum -@pytest.fixture -def mock_mirobo_is_on(): +@pytest.fixture(name="mock_mirobo_is_on") +def mirobo_is_on_fixture(): """Mock mock_mirobo.""" mock_vacuum = mock.MagicMock() - mock_vacuum.Vacuum().status().data = {"test": "raw"} - mock_vacuum.Vacuum().status().is_on = True - mock_vacuum.Vacuum().status().fanspeed = 99 - mock_vacuum.Vacuum().status().got_error = False - mock_vacuum.Vacuum().status().battery = 32 - mock_vacuum.Vacuum().status().clean_area = 133.43218 - mock_vacuum.Vacuum().status().clean_time = timedelta( - hours=2, minutes=55, seconds=34 - ) - mock_vacuum.Vacuum().consumable_status().main_brush_left = timedelta( + mock_vacuum.status().data = {"test": "raw"} + mock_vacuum.status().is_on = True + mock_vacuum.status().fanspeed = 99 + mock_vacuum.status().got_error = False + mock_vacuum.status().battery = 32 + mock_vacuum.status().clean_area = 133.43218 + mock_vacuum.status().clean_time = timedelta(hours=2, minutes=55, seconds=34) + mock_vacuum.consumable_status().main_brush_left = timedelta( hours=11, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().consumable_status().side_brush_left = timedelta( + mock_vacuum.consumable_status().side_brush_left = timedelta( hours=11, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().consumable_status().filter_left = timedelta( + mock_vacuum.consumable_status().filter_left = timedelta( hours=11, minutes=35, seconds=34 ) - mock_vacuum.Vacuum().clean_history().count = "41" - mock_vacuum.Vacuum().clean_history().total_area = 323.43218 - mock_vacuum.Vacuum().clean_history().total_duration = timedelta( + mock_vacuum.clean_history().count = "41" + mock_vacuum.clean_history().total_area = 323.43218 + mock_vacuum.clean_history().total_duration = timedelta( hours=11, minutes=15, seconds=34 ) - mock_vacuum.Vacuum().status().state = "Test Xiaomi Cleaning" - mock_vacuum.Vacuum().dnd_status().enabled = False + mock_vacuum.status().state = "Test Xiaomi Cleaning" + mock_vacuum.status().state_code = 5 + mock_vacuum.dnd_status().enabled = False - with mock.patch.dict("sys.modules", {"miio": mock_vacuum}): + with mock.patch( + "homeassistant.components.xiaomi_miio.vacuum.Vacuum" + ) as mock_vaccum_cls: + mock_vaccum_cls.return_value = mock_vacuum yield mock_vacuum -@pytest.fixture -def mock_mirobo_errors(): +@pytest.fixture(name="mock_mirobo_errors") +def mirobo_errors_fixture(): """Mock mock_mirobo_errors to simulate a bad vacuum status request.""" mock_vacuum = mock.MagicMock() - mock_vacuum.Vacuum().status.side_effect = OSError() - with mock.patch.dict("sys.modules", {"miio": mock_vacuum}): + mock_vacuum.status.side_effect = OSError() + with mock.patch( + "homeassistant.components.xiaomi_miio.vacuum.Vacuum" + ) as mock_vaccum_cls: + mock_vaccum_cls.return_value = mock_vacuum yield mock_vacuum @@ -159,16 +164,16 @@ def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors): } }, ) + yield from hass.async_block_till_done() assert "Initializing with host 127.0.0.1 (token 12345...)" in caplog.text - assert str(mock_mirobo_errors.mock_calls[-1]) == "call.Vacuum().status()" + assert mock_mirobo_errors.status.call_count == 1 assert "ERROR" in caplog.text assert "Got OSError while fetching the state" in caplog.text @asyncio.coroutine -@pytest.mark.skip(reason="Fails") -def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_off): +def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_1" entity_id = "{}.{}".format(DOMAIN, entity_name) @@ -185,19 +190,20 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_off): } }, ) + yield from hass.async_block_till_done() assert "Initializing with host 127.0.0.1 (token 12345...)" in caplog.text # Check state attributes state = hass.states.get(entity_id) - assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 2047 + assert state.state == STATE_ERROR + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 14204 assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_ON assert state.attributes.get(ATTR_DO_NOT_DISTURB_START) == "22:00:00" assert state.attributes.get(ATTR_DO_NOT_DISTURB_END) == "06:00:00" assert state.attributes.get(ATTR_ERROR) == "Error message" - assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-charging-80" + assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-80" assert state.attributes.get(ATTR_CLEANING_TIME) == 155 assert state.attributes.get(ATTR_CLEANED_AREA) == 123 assert state.attributes.get(ATTR_FAN_SPEED) == "Quiet" @@ -215,96 +221,103 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_off): assert state.attributes.get(ATTR_CLEANING_TOTAL_TIME) == 695 # Call services - yield from hass.services.async_call(DOMAIN, SERVICE_TURN_ON, blocking=True) - - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum.start()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() - - yield from hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().home()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() - - yield from hass.services.async_call(DOMAIN, SERVICE_TOGGLE, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().start()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() - - yield from hass.services.async_call(DOMAIN, SERVICE_STOP, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().stop()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() - - yield from hass.services.async_call(DOMAIN, SERVICE_START_PAUSE, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().pause()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() - - yield from hass.services.async_call(DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().home()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() - - yield from hass.services.async_call(DOMAIN, SERVICE_LOCATE, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().find()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() - - yield from hass.services.async_call(DOMAIN, SERVICE_CLEAN_SPOT, {}, blocking=True) - mock_mirobo_is_off.assert_has_calls([mock.call.Vacuum().spot()], any_order=True) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + yield from hass.services.async_call( + DOMAIN, SERVICE_START, {"entity_id": entity_id}, blocking=True + ) + mock_mirobo_is_got_error.assert_has_calls( + [mock.call.resume_or_start()], any_order=True + ) + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() + + yield from hass.services.async_call( + DOMAIN, SERVICE_STOP, {"entity_id": entity_id}, blocking=True + ) + mock_mirobo_is_got_error.assert_has_calls([mock.call.stop()], any_order=True) + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() + + yield from hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, {"entity_id": entity_id}, blocking=True + ) + mock_mirobo_is_got_error.assert_has_calls([mock.call.home()], any_order=True) + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() + + yield from hass.services.async_call( + DOMAIN, SERVICE_LOCATE, {"entity_id": entity_id}, blocking=True + ) + mock_mirobo_is_got_error.assert_has_calls([mock.call.find()], any_order=True) + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() + + yield from hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, {"entity_id": entity_id}, blocking=True + ) + mock_mirobo_is_got_error.assert_has_calls([mock.call.spot()], any_order=True) + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() # Set speed service: yield from hass.services.async_call( - DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": 60}, blocking=True + DOMAIN, + SERVICE_SET_FAN_SPEED, + {"entity_id": entity_id, "fan_speed": 60}, + blocking=True, ) - mock_mirobo_is_off.assert_has_calls( - [mock.call.Vacuum().set_fan_speed(60)], any_order=True + mock_mirobo_is_got_error.assert_has_calls( + [mock.call.set_fan_speed(60)], any_order=True ) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() yield from hass.services.async_call( - DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": "turbo"}, blocking=True + DOMAIN, + SERVICE_SET_FAN_SPEED, + {"entity_id": entity_id, "fan_speed": "turbo"}, + blocking=True, ) - mock_mirobo_is_off.assert_has_calls( - [mock.call.Vacuum().set_fan_speed(77)], any_order=True + mock_mirobo_is_got_error.assert_has_calls( + [mock.call.set_fan_speed(77)], any_order=True ) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() assert "ERROR" not in caplog.text yield from hass.services.async_call( - DOMAIN, SERVICE_SET_FAN_SPEED, {"fan_speed": "invent"}, blocking=True + DOMAIN, + SERVICE_SET_FAN_SPEED, + {"entity_id": entity_id, "fan_speed": "invent"}, + blocking=True, ) assert "ERROR" in caplog.text yield from hass.services.async_call( - DOMAIN, SERVICE_SEND_COMMAND, {"command": "raw"}, blocking=True + DOMAIN, + SERVICE_SEND_COMMAND, + {"entity_id": entity_id, "command": "raw"}, + blocking=True, ) - mock_mirobo_is_off.assert_has_calls( - [mock.call.Vacuum().raw_command("raw", None)], any_order=True + mock_mirobo_is_got_error.assert_has_calls( + [mock.call.raw_command("raw", None)], any_order=True ) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() yield from hass.services.async_call( DOMAIN, SERVICE_SEND_COMMAND, - {"command": "raw", "params": {"k1": 2}}, + {"entity_id": entity_id, "command": "raw", "params": {"k1": 2}}, blocking=True, ) - mock_mirobo_is_off.assert_has_calls( - [mock.call.Vacuum().raw_command("raw", {"k1": 2})], any_order=True + mock_mirobo_is_got_error.assert_has_calls( + [mock.call.raw_command("raw", {"k1": 2})], any_order=True ) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_got_error.reset_mock() @asyncio.coroutine -@pytest.mark.skip(reason="Fails") def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_2" @@ -322,13 +335,14 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): } }, ) + yield from hass.async_block_till_done() assert "Initializing with host 192.168.1.100 (token 12345" in caplog.text # Check state attributes state = hass.states.get(entity_id) - assert state.state == STATE_ON - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 2047 + assert state.state == STATE_CLEANING + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 14204 assert state.attributes.get(ATTR_DO_NOT_DISTURB) == STATE_OFF assert state.attributes.get(ATTR_ERROR) is None assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-30" @@ -353,47 +367,43 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): DOMAIN, SERVICE_START_REMOTE_CONTROL, {ATTR_ENTITY_ID: entity_id}, blocking=True ) - mock_mirobo_is_on.assert_has_calls( - [mock.call.Vacuum().manual_start()], any_order=True - ) - mock_mirobo_is_on.assert_has_calls(status_calls, any_order=True) + mock_mirobo_is_on.assert_has_calls([mock.call.manual_start()], any_order=True) + mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_on.reset_mock() control = {"duration": 1000, "rotation": -40, "velocity": -0.1} yield from hass.services.async_call( DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, control, blocking=True ) - mock_mirobo_is_on.assert_has_calls( - [mock.call.Vacuum().manual_control(control)], any_order=True + mock_mirobo_is_on.manual_control.assert_has_calls( + [mock.call(**control)], any_order=True ) - mock_mirobo_is_on.assert_has_calls(status_calls, any_order=True) + mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_on.reset_mock() yield from hass.services.async_call( DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True ) - mock_mirobo_is_on.assert_has_calls( - [mock.call.Vacuum().manual_stop()], any_order=True - ) - mock_mirobo_is_on.assert_has_calls(status_calls, any_order=True) + mock_mirobo_is_on.assert_has_calls([mock.call.manual_stop()], any_order=True) + mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_on.reset_mock() control_once = {"duration": 2000, "rotation": 120, "velocity": 0.1} yield from hass.services.async_call( DOMAIN, SERVICE_MOVE_REMOTE_CONTROL_STEP, control_once, blocking=True ) - mock_mirobo_is_on.assert_has_calls( - [mock.call.Vacuum().manual_control_once(control_once)], any_order=True + mock_mirobo_is_on.manual_control_once.assert_has_calls( + [mock.call(**control_once)], any_order=True ) - mock_mirobo_is_on.assert_has_calls(status_calls, any_order=True) + mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_on.reset_mock() control = {"zone": [[123, 123, 123, 123]], "repeats": 2} yield from hass.services.async_call( DOMAIN, SERVICE_CLEAN_ZONE, control, blocking=True ) - mock_mirobo_is_off.assert_has_calls( - [mock.call.Vacuum().zoned_clean([[123, 123, 123, 123, 2]])], any_order=True + mock_mirobo_is_on.zoned_clean.assert_has_calls( + [mock.call([[123, 123, 123, 123, 2]])], any_order=True ) - mock_mirobo_is_off.assert_has_calls(status_calls, any_order=True) - mock_mirobo_is_off.reset_mock() + mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) + mock_mirobo_is_on.reset_mock() From a39cac765e04f36908b77d459cc7691223556db8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Nov 2019 20:18:41 +0100 Subject: [PATCH 1514/3953] Add sensor platform to WLED integration (#28632) * Add sensor platform to WLED integration * Process review comments --- homeassistant/components/wled/__init__.py | 10 +- homeassistant/components/wled/const.py | 6 + homeassistant/components/wled/sensor.py | 141 ++++++++++++++++++++++ tests/components/wled/test_sensor.py | 61 ++++++++++ 4 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/wled/sensor.py create mode 100644 tests/components/wled/test_sensor.py diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 054c09eb971929..cd2c091bc10e98 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -7,6 +7,7 @@ from wled import WLED, WLEDConnectionError, WLEDError from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME, CONF_HOST @@ -34,6 +35,7 @@ ) SCAN_INTERVAL = timedelta(seconds=5) +WLED_COMPONENTS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -60,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled} # Set up all platforms for this device/entry. - for component in LIGHT_DOMAIN, SWITCH_DOMAIN: + for component in WLED_COMPONENTS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) @@ -93,8 +95,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Unload entities for this entry/device. await asyncio.gather( - hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN), - hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), + *( + hass.config_entries.async_forward_entry_unload(entry, component) + for component in WLED_COMPONENTS + ) ) # Cleanup diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 0836c801632d92..5fc24d74d63a25 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -14,7 +14,9 @@ ATTR_FADE = "fade" ATTR_IDENTIFIERS = "identifiers" ATTR_INTENSITY = "intensity" +ATTR_LED_COUNT = "led_count" ATTR_MANUFACTURER = "manufacturer" +ATTR_MAX_POWER = "max_power" ATTR_MODEL = "model" ATTR_ON = "on" ATTR_PALETTE = "palette" @@ -25,3 +27,7 @@ ATTR_SPEED = "speed" ATTR_TARGET_BRIGHTNESS = "target_brightness" ATTR_UDP_PORT = "udp_port" + +# Units of measurement +CURRENT_MA = "mA" +DATA_BYTES = "bytes" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py new file mode 100644 index 00000000000000..f464b27e140fa9 --- /dev/null +++ b/homeassistant/components/wled/sensor.py @@ -0,0 +1,141 @@ +"""Support for WLED sensors.""" +from datetime import timedelta +import logging +from typing import Callable, List, Optional, Union + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.dt import utcnow + +from . import WLED, WLEDDeviceEntity +from .const import ( + ATTR_LED_COUNT, + ATTR_MAX_POWER, + CURRENT_MA, + DATA_BYTES, + DATA_WLED_CLIENT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up WLED sensor based on a config entry.""" + wled: WLED = hass.data[DOMAIN][entry.entry_id][DATA_WLED_CLIENT] + + sensors = [ + WLEDEstimatedCurrentSensor(entry.entry_id, wled), + WLEDUptimeSensor(entry.entry_id, wled), + WLEDFreeHeapSensor(entry.entry_id, wled), + ] + + async_add_entities(sensors, True) + + +class WLEDSensor(WLEDDeviceEntity): + """Defines a WLED sensor.""" + + def __init__( + self, + entry_id: str, + wled: WLED, + name: str, + icon: str, + unit_of_measurement: str, + key: str, + ) -> None: + """Initialize WLED sensor.""" + self._state = None + self._unit_of_measurement = unit_of_measurement + self._key = key + + super().__init__(entry_id, wled, name, icon) + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return f"{self.wled.device.info.mac_address}_{self._key}" + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + +class WLEDEstimatedCurrentSensor(WLEDSensor): + """Defines a WLED estimated current sensor.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED estimated current sensor.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Estimated Current", + "mdi:power", + CURRENT_MA, + "estimated_current", + ) + + async def _wled_update(self) -> None: + """Update WLED entity.""" + self._state = self.wled.device.info.leds.power + self._attributes = { + ATTR_LED_COUNT: self.wled.device.info.leds.count, + ATTR_MAX_POWER: self.wled.device.info.leds.max_power, + } + + +class WLEDUptimeSensor(WLEDSensor): + """Defines a WLED uptime sensor.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED uptime sensor.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Uptime", + "mdi:clock-outline", + None, + "uptime", + ) + + @property + def device_class(self) -> Optional[str]: + """Return the class of this sensor.""" + return DEVICE_CLASS_TIMESTAMP + + async def _wled_update(self) -> None: + """Update WLED uptime sensor.""" + uptime = utcnow() - timedelta(seconds=self.wled.device.info.uptime) + self._state = uptime.replace(microsecond=0).isoformat() + + +class WLEDFreeHeapSensor(WLEDSensor): + """Defines a WLED free heap sensor.""" + + def __init__(self, entry_id: str, wled: WLED) -> None: + """Initialize WLED free heap sensor.""" + super().__init__( + entry_id, + wled, + f"{wled.device.info.name} Free Memory", + "mdi:memory", + DATA_BYTES, + "free_heap", + ) + + async def _wled_update(self) -> None: + """Update WLED uptime sensor.""" + self._state = self.wled.device.info.free_heap diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py new file mode 100644 index 00000000000000..a1247a8c373bce --- /dev/null +++ b/tests/components/wled/test_sensor.py @@ -0,0 +1,61 @@ +"""Tests for the WLED sensor platform.""" +from datetime import datetime + +from asynctest import patch + +from homeassistant.components.wled.const import ( + ATTR_LED_COUNT, + ATTR_MAX_POWER, + CURRENT_MA, + DATA_BYTES, +) +from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from tests.components.wled import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_sensors( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the WLED sensors.""" + + test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=dt_util.UTC) + with patch("homeassistant.components.wled.sensor.utcnow", return_value=test_time): + await init_integration(hass, aioclient_mock) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("sensor.wled_rgb_light_estimated_current") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:power" + assert state.attributes.get(ATTR_LED_COUNT) == 30 + assert state.attributes.get(ATTR_MAX_POWER) == 850 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == CURRENT_MA + assert state.state == "470" + + entry = entity_registry.async_get("sensor.wled_rgb_light_estimated_current") + assert entry + assert entry.unique_id == "aabbccddeeff_estimated_current" + + state = hass.states.get("sensor.wled_rgb_light_uptime") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.state == "2019-11-11T09:10:00+00:00" + + entry = entity_registry.async_get("sensor.wled_rgb_light_uptime") + assert entry + assert entry.unique_id == "aabbccddeeff_uptime" + + state = hass.states.get("sensor.wled_rgb_light_free_memory") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:memory" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_BYTES + assert state.state == "14600" + + entry = entity_registry.async_get("sensor.wled_rgb_light_free_memory") + assert entry + assert entry.unique_id == "aabbccddeeff_free_heap" From 179a2eb187882098b42051ad2b7a5ace779ae730 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 9 Nov 2019 15:41:02 -0700 Subject: [PATCH 1515/3953] Create base entity for SimpliSafe --- .../components/simplisafe/__init__.py | 76 ++++++++++++++++++- .../simplisafe/alarm_control_panel.py | 62 ++------------- .../components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 85 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 0c0595896edfa7..2514c5e9ed9b2d 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -22,7 +22,11 @@ config_validation as cv, device_registry as dr, ) -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import verify_domain_control @@ -240,3 +244,73 @@ async def async_update(self): tasks = [self._update_system(system) for system in self.systems.values()] await asyncio.gather(*tasks) + + +class SimpliSafeEntity(Entity): + """Define a base SimpliSafe entity.""" + + def __init__(self, system, name, *, serial=None): + """Initialize.""" + self._async_unsub_dispatcher_connect = None + self._attrs = {ATTR_SYSTEM_ID: system.system_id} + self._name = name + self._online = True + self._system = system + + if serial: + self._serial = serial + else: + self._serial = system.serial + + @property + def available(self): + """Return whether the entity is available.""" + # We can easily detect if the V3 system is offline, but no simple check exists + # for the V2 system. Therefore, we mark the entity as available if: + # 1. We can verify that the system is online (assuming True if we can't) + # 2. We can verify that the entity is online + system_offline = self._system.version == 3 and self._system.offline + return not system_offline and self._online + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self._system.system_id)}, + "manufacturer": "SimpliSafe", + "model": self._system.version, + "name": self._name, + "via_device": (DOMAIN, self._system.serial), + } + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def name(self): + """Return the name of the entity.""" + return f"{self._system.address} {self._name}" + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return self._serial + + async def async_added_to_hass(self): + """Register callbacks.""" + + @callback + def update(): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, update + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index d44a1c7760aae4..bbd9a5d4e51351 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -2,7 +2,7 @@ import logging import re -from simplipy.sensor import SensorTypes +from simplipy.entity import EntityTypes from simplipy.system import SystemStates from homeassistant.components.alarm_control_panel import ( @@ -16,11 +16,10 @@ STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, ) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import utc_from_timestamp -from .const import DATA_CLIENT, DOMAIN, TOPIC_UPDATE +from . import SimpliSafeEntity +from .const import DATA_CLIENT, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -33,7 +32,6 @@ ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" ATTR_LAST_EVENT_TYPE = "last_event_type" ATTR_RF_JAMMING = "rf_jamming" -ATTR_SYSTEM_ID = "system_id" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" @@ -55,18 +53,16 @@ async def async_setup_entry(hass, entry, async_add_entities): ) -class SimpliSafeAlarm(AlarmControlPanel): +class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): """Representation of a SimpliSafe alarm.""" def __init__(self, simplisafe, system, code): """Initialize the SimpliSafe alarm.""" - self._async_unsub_dispatcher_connect = None - self._attrs = {ATTR_SYSTEM_ID: system.system_id} + super().__init__(system, "Alarm Control Panel") self._changed_by = None self._code = code self._simplisafe = simplisafe self._state = None - self._system = system # Some properties only exist for V2 or V3 systems: for prop in ( @@ -93,39 +89,11 @@ def code_format(self): return FORMAT_NUMBER return FORMAT_TEXT - @property - def device_info(self): - """Return device registry information for this entity.""" - return { - "identifiers": {(DOMAIN, self._system.system_id)}, - "manufacturer": "SimpliSafe", - "model": self._system.version, - # The name should become more dynamic once we deduce a way to - # get various other sensors from SimpliSafe in a reliable manner: - "name": "Keypad", - "via_device": (DOMAIN, self._system.serial), - } - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attrs - - @property - def name(self): - """Return the name of the entity.""" - return self._system.address - @property def state(self): """Return the state of the entity.""" return self._state - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._system.system_id - def _validate_code(self, code, state): """Validate given code.""" check = self._code is None or code == self._code @@ -133,18 +101,6 @@ def _validate_code(self, code, state): _LOGGER.warning("Wrong code entered for %s", state) return check - async def async_added_to_hass(self): - """Register callbacks.""" - - @callback - def update(): - """Update the state.""" - self.async_schedule_update_ha_state(True) - - self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update - ) - async def async_alarm_disarm(self, code=None): """Send disarm command.""" if not self._validate_code(code, "disarming"): @@ -174,6 +130,7 @@ async def async_update(self): self._changed_by = event_data["pinName"] if self._system.state == SystemStates.error: + self._online = False return if self._system.state == SystemStates.off: @@ -195,15 +152,10 @@ async def async_update(self): ATTR_ALARM_ACTIVE: self._system.alarm_going_off, ATTR_LAST_EVENT_INFO: last_event["info"], ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"], - ATTR_LAST_EVENT_SENSOR_TYPE: SensorTypes(last_event["sensorType"]).name, + ATTR_LAST_EVENT_SENSOR_TYPE: EntityTypes(last_event["sensorType"]).name, ATTR_LAST_EVENT_TIMESTAMP: utc_from_timestamp( last_event["eventTimestamp"] ), ATTR_LAST_EVENT_TYPE: last_event["eventType"], } ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect dispatcher listener when removed.""" - if self._async_unsub_dispatcher_connect: - self._async_unsub_dispatcher_connect() diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 96b337def55a6d..254e947ed259bf 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/integrations/simplisafe", "requirements": [ - "simplisafe-python==5.0.1" + "simplisafe-python==5.1.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 6f0b40230edede..ccf732ce2ae107 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1769,7 +1769,7 @@ shodan==1.19.0 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==5.0.1 +simplisafe-python==5.1.0 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 691e8d413d58c1..99b9c744041c38 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -546,7 +546,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.simplisafe -simplisafe-python==5.0.1 +simplisafe-python==5.1.0 # homeassistant.components.sleepiq sleepyq==0.7 From 25f0b709663c21dbf7ad80670a35cbcaa8baec30 Mon Sep 17 00:00:00 2001 From: Gerard Date: Sat, 9 Nov 2019 23:43:04 +0100 Subject: [PATCH 1516/3953] Upgrade bimmer_connected to 0.6.2 (#28651) * Upgrade bimmer_connected to 0.6.1 * Remove time.sleep from library --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 29366715d239a3..a88675ef80fff7 100644 --- 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/integrations/bmw_connected_drive", "requirements": [ - "bimmer_connected==0.6.0" + "bimmer_connected==0.6.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ccf732ce2ae107..ee331d1a32058e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -288,7 +288,7 @@ beewi_smartclim==0.0.7 bellows-homeassistant==0.10.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.6.0 +bimmer_connected==0.6.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 From ef687a36ff36de5a115a6be014909469ac372793 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 9 Nov 2019 17:02:56 -0600 Subject: [PATCH 1517/3953] Add Plex debug logging (#28665) --- homeassistant/components/plex/config_flow.py | 2 +- homeassistant/components/plex/media_player.py | 3 +++ homeassistant/components/plex/sensor.py | 1 + homeassistant/components/plex/server.py | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index c03b958b2da0d6..cb79c08b16e38c 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -102,7 +102,7 @@ async def async_step_server_validate(self, server_config): return await self.async_step_select_server() except Exception as error: # pylint: disable=broad-except - _LOGGER.error("Unknown error connecting to Plex server: %s", error) + _LOGGER.exception("Unknown error connecting to Plex server: %s", error) return self.async_abort(reason="unknown") if errors: diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4c32c1e6376de1..1fe83928d29e9d 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -75,6 +75,7 @@ def _async_add_entities( hass, registry, config_entry, async_add_entities, server_id, new_entities ): """Set up Plex media_player entities.""" + _LOGGER.debug("New entities: %s", new_entities) entities = [] plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] for entity_params in new_entities: @@ -142,6 +143,7 @@ async def async_added_to_hass(self): """Run when about to be added to hass.""" server_id = self.plex_server.machine_identifier + _LOGGER.debug("Added %s [%s]", self.entity_id, self.unique_id) unsub = async_dispatcher_connect( self.hass, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id), @@ -152,6 +154,7 @@ async def async_added_to_hass(self): @callback def async_refresh_media_player(self, device, session): """Set instance objects and trigger an entity state update.""" + _LOGGER.debug("Refreshing %s [%s / %s]", self.entity_id, device, session) self.device = device self.session = session self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 287f0edf39aa09..2a994b08a7bcb7 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -93,6 +93,7 @@ def device_state_attributes(self): def update(self): """Update method for Plex sensor.""" + _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) now_playing = [] for sess in self.sessions: user = sess.usernames[0] diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 28380e714ac790..5df55589bb4749 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -95,6 +95,7 @@ def _connect_with_url(): def refresh_entity(self, machine_identifier, device, session): """Forward refresh dispatch to media_player.""" unique_id = f"{self.machine_identifier}:{machine_identifier}" + _LOGGER.debug("Refreshing %s", unique_id) dispatcher_send( self._hass, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id), From 65bf0a30f0668587b87c41a01e764360335459c2 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 10 Nov 2019 00:32:12 +0000 Subject: [PATCH 1518/3953] [ci skip] Translation update --- .../components/climate/.translations/ca.json | 8 ++++++++ homeassistant/components/fan/.translations/ca.json | 12 ++++++++++++ homeassistant/components/lock/.translations/ca.json | 4 ++++ .../components/vacuum/.translations/ca.json | 3 +++ 4 files changed, 27 insertions(+) create mode 100644 homeassistant/components/fan/.translations/ca.json diff --git a/homeassistant/components/climate/.translations/ca.json b/homeassistant/components/climate/.translations/ca.json index 480d90310d97b7..7aff7383a39d1d 100644 --- a/homeassistant/components/climate/.translations/ca.json +++ b/homeassistant/components/climate/.translations/ca.json @@ -1,8 +1,16 @@ { "device_automation": { + "action_type": { + "set_hvac_mode": "Canvia el mode HVAC de {entity_name}" + }, "condtion_type": { "is_hvac_mode": "{entity_name} est\u00e0 configurat/ada en un mode HVAC espec\u00edfic", "is_preset_mode": "{entity_name} est\u00e0 configurat/ada en un mode preestablert espec\u00edfic" + }, + "trigger_type": { + "current_humidity_changed": "Ha canviat la humitat mesurada per {entity_name}", + "current_temperature_changed": "Ha canviat la temperatura mesurada per {entity_name}", + "hvac_mode_changed": "El mode HVAC de {entity_name} ha canviat" } } } \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/ca.json b/homeassistant/components/fan/.translations/ca.json new file mode 100644 index 00000000000000..1002f8d7dd6f64 --- /dev/null +++ b/homeassistant/components/fan/.translations/ca.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Apaga {entity_name}", + "turn_on": "Enc\u00e9n {entity_name}" + }, + "trigger_type": { + "turned_off": "{entity_name} s'ha apagat", + "turned_on": "{entity_name} s'ha enc\u00e8s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ca.json b/homeassistant/components/lock/.translations/ca.json index 53198a2157381d..69655ac1daab4f 100644 --- a/homeassistant/components/lock/.translations/ca.json +++ b/homeassistant/components/lock/.translations/ca.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} est\u00e0 bloquejat/ada", "is_unlocked": "{entity_name} est\u00e0 desbloquejat/ada" + }, + "trigger_type": { + "locked": "{entity_name} s'ha bloquejat", + "unlocked": "{entity_name} s'ha desbloquejat" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/ca.json b/homeassistant/components/vacuum/.translations/ca.json index c004120a8c7ab9..df2ea439e0e1f1 100644 --- a/homeassistant/components/vacuum/.translations/ca.json +++ b/homeassistant/components/vacuum/.translations/ca.json @@ -2,6 +2,9 @@ "device_automation": { "condtion_type": { "is_cleaning": "{entity_name} est\u00e0 netejant" + }, + "trigger_type": { + "cleaning": "{entity_name} ha comen\u00e7at a netejar" } } } \ No newline at end of file From a9536e4ed1c9831982e23525931329c588dafa31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Sun, 10 Nov 2019 08:25:10 +0100 Subject: [PATCH 1519/3953] verisure autolock service (#27366) --- homeassistant/components/verisure/__init__.py | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index f4313c7c1acfa5..d5cc7f31efb601 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -2,9 +2,9 @@ import logging import threading from datetime import timedelta - from jsonpath import jsonpath import verisure + import voluptuous as vol from homeassistant.const import ( @@ -39,6 +39,8 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) SERVICE_CAPTURE_SMARTCAM = "capture_smartcam" +SERVICE_DISABLE_AUTOLOCK = "disable_autolock" +SERVICE_ENABLE_AUTOLOCK = "enable_autolock" HUB = None @@ -68,7 +70,7 @@ extra=vol.ALLOW_EXTRA, ) -CAPTURE_IMAGE_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_SERIAL): cv.string}) +DEVICE_SERIAL_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_SERIAL): cv.string}) def setup(hass, config): @@ -93,16 +95,44 @@ def setup(hass, config): ): discovery.load_platform(hass, component, DOMAIN, {}, config) - def capture_smartcam(service): + async def capture_smartcam(service): """Capture a new picture from a smartcam.""" - device_id = service.data.get(ATTR_DEVICE_SERIAL) - HUB.smartcam_capture(device_id) - _LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL) + device_id = service.data[ATTR_DEVICE_SERIAL] + try: + await hass.async_add_executor_job(HUB.smartcam_capture, device_id) + _LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL) + except verisure.Error as ex: + _LOGGER.error("Could not capture image, %s", ex) hass.services.register( - DOMAIN, SERVICE_CAPTURE_SMARTCAM, capture_smartcam, schema=CAPTURE_IMAGE_SCHEMA + DOMAIN, SERVICE_CAPTURE_SMARTCAM, capture_smartcam, schema=DEVICE_SERIAL_SCHEMA ) + async def disable_autolock(service): + """Disable autolock on a doorlock.""" + device_id = service.data[ATTR_DEVICE_SERIAL] + try: + await hass.async_add_executor_job(HUB.disable_autolock, device_id) + _LOGGER.debug("Disabling autolock on%s", ATTR_DEVICE_SERIAL) + except verisure.Error as ex: + _LOGGER.error("Could not disable autolock, %s", ex) + + hass.services.register( + DOMAIN, SERVICE_DISABLE_AUTOLOCK, disable_autolock, schema=DEVICE_SERIAL_SCHEMA + ) + + async def enable_autolock(service): + """Enable autolock on a doorlock.""" + device_id = service.data[ATTR_DEVICE_SERIAL] + try: + await hass.async_add_executor_job(HUB.enable_autolock, device_id) + _LOGGER.debug("Enabling autolock on %s", ATTR_DEVICE_SERIAL) + except verisure.Error as ex: + _LOGGER.error("Could not enable autolock, %s", ex) + + hass.services.register( + DOMAIN, SERVICE_ENABLE_AUTOLOCK, enable_autolock, schema=DEVICE_SERIAL_SCHEMA + ) return True @@ -175,6 +205,14 @@ def smartcam_capture(self, device_id): """Capture a new image from a smartcam.""" self.session.capture_image(device_id) + def disable_autolock(self, device_id): + """Disable autolock.""" + self.session.set_lock_config(device_id, auto_lock_enabled=False) + + def enable_autolock(self, device_id): + """Enable autolock.""" + self.session.set_lock_config(device_id, auto_lock_enabled=True) + def get(self, jpath, *args): """Get values from the overview that matches the jsonpath.""" res = jsonpath(self.overview, jpath % args) From 206547a5d8abbcf7bc0463d312cc9f68cdcfce07 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 10 Nov 2019 04:35:12 -0600 Subject: [PATCH 1520/3953] Skip updating idle Plex clients (#28664) * Skip updating idle clients * Different operators --- homeassistant/components/plex/server.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 5df55589bb4749..69838fbf27fb2d 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -44,6 +44,7 @@ def __init__(self, hass, server_config, options=None): self._hass = hass self._plex_server = None self._known_clients = set() + self._known_idle = set() self._url = server_config.get(CONF_URL) self._token = server_config.get(CONF_TOKEN) self._server_name = server_config.get(CONF_SERVER) @@ -123,6 +124,7 @@ def update_platforms(self): return for device in devices: + self._known_idle.discard(device.machineIdentifier) available_clients[device.machineIdentifier] = {"device": device} if device.machineIdentifier not in self._known_clients: @@ -131,6 +133,7 @@ def update_platforms(self): for session in sessions: for player in session.players: + self._known_idle.discard(player.machineIdentifier) available_clients.setdefault( player.machineIdentifier, {"device": player} ) @@ -151,9 +154,12 @@ def update_platforms(self): self._known_clients.update(new_clients) - idle_clients = self._known_clients.difference(available_clients) + idle_clients = (self._known_clients - self._known_idle).difference( + available_clients + ) for client_id in idle_clients: self.refresh_entity(client_id, None, None) + self._known_idle.add(client_id) if new_entity_configs: dispatcher_send( From 66a574eca436872047614e26a7dd21cef3cc9c94 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 10 Nov 2019 20:37:34 +0100 Subject: [PATCH 1521/3953] Hue: store current sensor entities by bridge (#28679) --- homeassistant/components/hue/sensor_base.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 62bd98df3a240a..bf64fed0c9516e 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -15,7 +15,7 @@ from .helpers import remove_devices -CURRENT_SENSORS = "current_sensors" +CURRENT_SENSORS_FORMAT = "{}_current_sensors" SENSOR_MANAGER_FORMAT = "{}_sensor_manager" _LOGGER = logging.getLogger(__name__) @@ -31,8 +31,9 @@ def _device_id(aiohue_sensor): async def async_setup_entry(hass, config_entry, async_add_entities, binary=False): """Set up the Hue sensors from a config entry.""" + sensor_key = CURRENT_SENSORS_FORMAT.format(config_entry.data["host"]) bridge = hass.data[hue.DOMAIN][config_entry.data["host"]] - hass.data[hue.DOMAIN].setdefault(CURRENT_SENSORS, {}) + hass.data[hue.DOMAIN].setdefault(sensor_key, {}) sm_key = SENSOR_MANAGER_FORMAT.format(config_entry.data["host"]) manager = hass.data[hue.DOMAIN].get(sm_key) @@ -127,7 +128,8 @@ async def async_update_items(self): new_sensors = [] new_binary_sensors = [] primary_sensor_devices = {} - current = self.hass.data[hue.DOMAIN][CURRENT_SENSORS] + sensor_key = CURRENT_SENSORS_FORMAT.format(self.config_entry.data["host"]) + current = self.hass.data[hue.DOMAIN][sensor_key] # Physical Hue motion sensors present as three sensors in the API: a # presence sensor, a temperature sensor, and a light level sensor. Of From 65dd7d998be38bd4d8acbbb78e07f4a518677d13 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Sun, 10 Nov 2019 13:51:00 -0800 Subject: [PATCH 1522/3953] #28645: Bump up zm-py to 0.4.0 (#28681) --- homeassistant/components/zoneminder/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index c29a97e857eca6..5e834e2fa4f91d 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -3,7 +3,7 @@ "name": "Zoneminder", "documentation": "https://www.home-assistant.io/integrations/zoneminder", "requirements": [ - "zm-py==0.3.3" + "zm-py==0.4.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ee331d1a32058e..997a218f5e24e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2075,4 +2075,4 @@ zigpy-xbee-homeassistant==0.6.0 zigpy-zigate==0.5.0 # homeassistant.components.zoneminder -zm-py==0.3.3 +zm-py==0.4.0 From 0ac8b297cd89aa6fe444974f1fd17af31bbbdc7e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 11 Nov 2019 00:32:13 +0000 Subject: [PATCH 1523/3953] [ci skip] Translation update --- homeassistant/components/cover/.translations/ru.json | 1 - homeassistant/components/huawei_lte/.translations/ru.json | 3 ++- homeassistant/components/ifttt/.translations/fr.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json index 47608a1ff770d4..043e5a42d2a954 100644 --- a/homeassistant/components/cover/.translations/ru.json +++ b/homeassistant/components/cover/.translations/ru.json @@ -9,7 +9,6 @@ "is_tilt_position": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \u043d\u0430\u043a\u043b\u043e\u043d\u0430" }, "trigger_type": { - "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json index e64018b2a3ce68..ec28325dcddfdc 100644 --- a/homeassistant/components/huawei_lte/.translations/ru.json +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "not_huawei_lte": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Huawei LTE" }, "error": { "connection_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", diff --git a/homeassistant/components/ifttt/.translations/fr.json b/homeassistant/components/ifttt/.translations/fr.json index d083a624d70f93..659b11ae98ab90 100644 --- a/homeassistant/components/ifttt/.translations/fr.json +++ b/homeassistant/components/ifttt/.translations/fr.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Une seule instance est n\u00e9cessaire." }, "create_entry": { - "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez utiliser l'action \"Effectuer une demande Web\" \u00e0 partir de [l'applet IFTTT Webhook] ( {applet_url} ). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation] ( {docs_url} ) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." + "default": "Pour envoyer des \u00e9v\u00e9nements \u00e0 Home Assistant, vous devez utiliser l'action \"Effectuer une demande Web\" \u00e0 partir de [l'applet IFTTT Webhook]({applet_url}). \n\n Remplissez les informations suivantes: \n\n - URL: ` {webhook_url} ` \n - M\u00e9thode: POST \n - Type de contenu: application / json \n\n Voir [la documentation]({docs_url}) pour savoir comment configurer les automatisations pour g\u00e9rer les donn\u00e9es entrantes." }, "step": { "user": { From aea7c1c0ceeec237d4463cf29a6ac0e70c7fbd6f Mon Sep 17 00:00:00 2001 From: Jon Gilmore <7232986+JonGilmore@users.noreply.github.com> Date: Sun, 10 Nov 2019 18:51:24 -0600 Subject: [PATCH 1524/3953] Add codeowner for lutron integration (#28682) * add codeowner for lutron integration * ran hassfest --- CODEOWNERS | 1 + homeassistant/components/lutron/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index cb3d0817d59fea..879a1c8f55d019 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -177,6 +177,7 @@ homeassistant/components/logi_circle/* @evanjd homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luftdaten/* @fabaff +homeassistant/components/lutron/* @JonGilmore homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf homeassistant/components/mcp23017/* @jardiamj diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index cace2770de0bea..f8439e3c1ec594 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -6,5 +6,7 @@ "pylutron==0.2.5" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@JonGilmore" + ] } From b6c79764774389e87df053868cdf6b080d6c6426 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 11 Nov 2019 07:59:07 +0100 Subject: [PATCH 1525/3953] Add xiaomi_miio chuangmi.plug.hmi206 (#28688) Related: https://github.com/rytilahti/python-miio/issues/574 --- homeassistant/components/xiaomi_miio/switch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 42586cd59700a3..023243a1995cb7 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -48,6 +48,7 @@ "chuangmi.plug.v2", "chuangmi.plug.v3", "chuangmi.plug.hmi205", + "chuangmi.plug.hmi206", "lumi.acpartner.v3", ] ), @@ -158,6 +159,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "chuangmi.plug.m3", "chuangmi.plug.v2", "chuangmi.plug.hmi205", + "chuangmi.plug.hmi206", ]: plug = ChuangmiPlug(host, token, model=model) device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) From 7bfde2dd33b4bc4d5464f45729852a7bd1878b01 Mon Sep 17 00:00:00 2001 From: Kevin Lee Date: Mon, 11 Nov 2019 02:07:48 -0500 Subject: [PATCH 1526/3953] Add Lutron hybrid keypad raise/lower buttons (#28674) * Lutron: Add support for hybrid keypad raise/lower buttons * Add MasterRaiseLower --- homeassistant/components/lutron/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 09ab0fc747b0be..ac9a4eab417da4 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -72,6 +72,8 @@ def setup(hass, base_config): if button.name != "Unknown Button" and button.button_type in ( "SingleAction", "Toggle", + "SingleSceneRaiseLower", + "MasterRaiseLower", ): # Associate an LED with a button if there is one led = next( From 90e723e25eb8fc559c7bb67a0cec2f7e1983a5cb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 11 Nov 2019 11:53:57 +0100 Subject: [PATCH 1527/3953] Allow icons to be masked (#28692) --- homeassistant/components/frontend/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f82350d994d6d5..7ef2bd386449d7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -52,6 +52,7 @@ "src": "/static/icons/favicon-{size}x{size}.png".format(size=size), "sizes": "{size}x{size}".format(size=size), "type": "image/png", + "purpose": "maskable any", } for size in (192, 384, 512, 1024) ], From 3ce850234f826acf466e0b631a8f7ce4cfa2e152 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 11 Nov 2019 13:51:26 +0100 Subject: [PATCH 1528/3953] fix typo in comments (#28694) The global is called `PARALLEL_UPDATES` not `PARALLEL_UPDATE`. --- homeassistant/helpers/entity_platform.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 5c59dc6c13e3a0..376a6e23e9a71c 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -272,13 +272,13 @@ async def _async_add_entity( entity.platform = self # Async entity - # PARALLEL_UPDATE == None: entity.parallel_updates = None - # PARALLEL_UPDATE == 0: entity.parallel_updates = None - # PARALLEL_UPDATE > 0: entity.parallel_updates = Semaphore(p) + # PARALLEL_UPDATES == None: entity.parallel_updates = None + # PARALLEL_UPDATES == 0: entity.parallel_updates = None + # PARALLEL_UPDATES > 0: entity.parallel_updates = Semaphore(p) # Sync entity - # PARALLEL_UPDATE == None: entity.parallel_updates = Semaphore(1) - # PARALLEL_UPDATE == 0: entity.parallel_updates = None - # PARALLEL_UPDATE > 0: entity.parallel_updates = Semaphore(p) + # PARALLEL_UPDATES == None: entity.parallel_updates = Semaphore(1) + # PARALLEL_UPDATES == 0: entity.parallel_updates = None + # PARALLEL_UPDATES > 0: entity.parallel_updates = Semaphore(p) if hasattr(entity, "async_update") and not self.parallel_updates: entity.parallel_updates = None elif not hasattr(entity, "async_update") and self.parallel_updates == 0: From cfcacc28279f496e5408b343b560208e632db57d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 11 Nov 2019 12:26:56 -0600 Subject: [PATCH 1529/3953] Bump plexwebsocket to 0.0.5 (#28703) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index b7179174ea496c..eaf3028b39fc43 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==3.0.6", "plexauth==0.0.5", - "plexwebsocket==0.0.4" + "plexwebsocket==0.0.5" ], "dependencies": [ "http" diff --git a/requirements_all.txt b/requirements_all.txt index 997a218f5e24e3..01538784fecd58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -986,7 +986,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.4 +plexwebsocket==0.0.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99b9c744041c38..d352e21b029ed5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.4 +plexwebsocket==0.0.5 # homeassistant.components.mhz19 # homeassistant.components.serial_pm From decab3e15b2ef80e9e9ed3ea3bf20717a8811e51 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 11 Nov 2019 21:30:00 +0100 Subject: [PATCH 1530/3953] Add config flow tests for OwnTracks (#28644) * Add config flow tests for OwnTracks * Fix pylint * Woops, uncomment test * Woops again, logs removed * Review from @MartinHjelmare + fix pylint --- .../components/owntracks/__init__.py | 2 +- .../components/owntracks/config_flow.py | 19 ++- homeassistant/components/owntracks/const.py | 3 + .../components/owntracks/messages.py | 8 +- .../components/owntracks/test_config_flow.py | 128 ++++++++++++++++-- 5 files changed, 134 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/owntracks/const.py diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 7e65ff3d51dba6..d30e667f368e68 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -14,12 +14,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_when_setup +from .const import DOMAIN from .config_flow import CONF_SECRET from .messages import async_handle_message _LOGGER = logging.getLogger(__name__) -DOMAIN = "owntracks" CONF_MAX_GPS_ACCURACY = "max_gps_accuracy" CONF_WAYPOINT_IMPORT = "waypoints" CONF_WAYPOINT_WHITELIST = "waypoint_whitelist" diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 343a6d90b52087..a59cd869c74e61 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -3,6 +3,8 @@ from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.auth.util import generate_secret +from .const import DOMAIN # noqa pylint: disable=unused-import + CONF_SECRET = "secret" CONF_CLOUDHOOK = "cloudhook" @@ -17,8 +19,7 @@ def supports_encryption(): return False -@config_entries.HANDLERS.register("owntracks") -class OwnTracksFlow(config_entries.ConfigFlow): +class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): """Set up OwnTracks.""" VERSION = 1 @@ -36,14 +37,9 @@ async def async_step_user(self, user_input=None): secret = generate_secret(16) if supports_encryption(): - secret_desc = ( - "The encryption key is {} " - "(on Android under preferences -> advanced)".format(secret) - ) + secret_desc = f"The encryption key is {secret} (on Android under preferences -> advanced)" else: - secret_desc = ( - "Encryption is not supported because libsodium is not " "installed." - ) + secret_desc = "Encryption is not supported because nacl is not installed." return self.async_create_entry( title="OwnTracks", @@ -55,8 +51,7 @@ async def async_step_user(self, user_input=None): description_placeholders={ "secret": secret_desc, "webhook_url": webhook_url, - "android_url": "https://play.google.com/store/apps/details?" - "id=org.owntracks.android", + "android_url": "https://play.google.com/store/apps/details?id=org.owntracks.android", "ios_url": "https://itunes.apple.com/us/app/owntracks/id692424691?mt=8", "docs_url": "https://www.home-assistant.io/integrations/owntracks/", }, @@ -64,6 +59,8 @@ async def async_step_user(self, user_input=None): async def async_step_import(self, user_input): """Import a config flow from configuration.""" + if self._async_current_entries(): + return self.async_abort(reason="one_instance_allowed") webhook_id, _webhook_url, cloudhook = await self._get_webhook_id() secret = generate_secret(16) return self.async_create_entry( diff --git a/homeassistant/components/owntracks/const.py b/homeassistant/components/owntracks/const.py new file mode 100644 index 00000000000000..c7caa201ca3dff --- /dev/null +++ b/homeassistant/components/owntracks/const.py @@ -0,0 +1,3 @@ +"""Constants for OwnTracks.""" + +DOMAIN = "owntracks" diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 465d2762f74b98..0cb65c774b5776 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -107,7 +107,7 @@ def _decrypt_payload(secret, topic, ciphertext): try: keylen, decrypt = get_cipher() except OSError: - _LOGGER.warning("Ignoring encrypted payload because libsodium not installed") + _LOGGER.warning("Ignoring encrypted payload because nacl not installed") return None if isinstance(secret, dict): @@ -117,8 +117,7 @@ def _decrypt_payload(secret, topic, ciphertext): if key is None: _LOGGER.warning( - "Ignoring encrypted payload because no decryption key known " - "for topic %s", + "Ignoring encrypted payload because no decryption key known for topic %s", topic, ) return None @@ -134,8 +133,7 @@ def _decrypt_payload(secret, topic, ciphertext): return message except ValueError: _LOGGER.warning( - "Ignoring encrypted payload because unable to decrypt using " - "key for topic %s", + "Ignoring encrypted payload because unable to decrypt using key for topic %s", topic, ) return None diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index c4e2a54f69a0ae..54e33a1ab61b46 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -1,25 +1,135 @@ """Tests for OwnTracks config flow.""" -from unittest.mock import patch +from unittest.mock import Mock, patch +import pytest +from homeassistant import data_entry_flow +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.components.owntracks import config_flow +from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET +from homeassistant.components.owntracks.const import DOMAIN from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.common import mock_coro, MockConfigEntry -async def test_config_flow_import(hass): + +CONF_WEBHOOK_URL = "webhook_url" + +BASE_URL = "http://example.com" +CLOUDHOOK = False +SECRET = "secret" +WEBHOOK_ID = "webhook_id" +WEBHOOK_URL = f"{BASE_URL}/api/webhook/webhook_id" + + +@pytest.fixture(name="webhook_id") +def mock_webhook_id(): + """Mock webhook_id.""" + with patch( + "homeassistant.components.webhook.async_generate_id", return_value=WEBHOOK_ID + ): + yield + + +@pytest.fixture(name="secret") +def mock_secret(): + """Mock secret.""" + with patch("binascii.hexlify", return_value=str.encode(SECRET)): + yield + + +@pytest.fixture(name="not_supports_encryption") +def mock_not_supports_encryption(): + """Mock non successful nacl import.""" + with patch( + "homeassistant.components.owntracks.config_flow.supports_encryption", + return_value=False, + ): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + hass.config.api = Mock(base_url=BASE_URL) + flow = config_flow.OwnTracksFlow() + flow.hass = hass + return flow + + +async def test_user(hass, webhook_id, secret): + """Test user step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await flow.async_step_user({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "OwnTracks" + assert result["data"][CONF_WEBHOOK_ID] == WEBHOOK_ID + assert result["data"][CONF_SECRET] == SECRET + assert result["data"][CONF_CLOUDHOOK] == CLOUDHOOK + assert result["description_placeholders"][CONF_WEBHOOK_URL] == WEBHOOK_URL + + +async def test_import(hass, webhook_id, secret): + """Test import step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_import({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "OwnTracks" + assert result["data"][CONF_WEBHOOK_ID] == WEBHOOK_ID + assert result["data"][CONF_SECRET] == SECRET + assert result["data"][CONF_CLOUDHOOK] == CLOUDHOOK + assert result["description_placeholders"] is None + + +async def test_import_setup(hass): """Test that we automatically create a config flow.""" - assert not hass.config_entries.async_entries("owntracks") - assert await async_setup_component(hass, "owntracks", {"owntracks": {}}) + assert not hass.config_entries.async_entries(DOMAIN) + assert await async_setup_component(hass, DOMAIN, {"owntracks": {}}) await hass.async_block_till_done() - assert hass.config_entries.async_entries("owntracks") + assert hass.config_entries.async_entries(DOMAIN) + + +async def test_abort_if_already_setup(hass): + """Test that we can't add more than one instance.""" + flow = init_config_flow(hass) + + MockConfigEntry(domain=DOMAIN, data={}).add_to_hass(hass) + assert hass.config_entries.async_entries(DOMAIN) + + # Should fail, already setup (import) + result = await flow.async_step_import({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "one_instance_allowed" + + # Should fail, already setup (flow) + result = await flow.async_step_user({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "one_instance_allowed" + + +async def test_user_not_supports_encryption(hass, not_supports_encryption): + """Test user step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + result["description_placeholders"]["secret"] + == "Encryption is not supported because nacl is not installed." + ) -async def test_config_flow_unload(hass): +async def test_unload(hass): """Test unloading a config flow.""" with patch( "homeassistant.config_entries.ConfigEntries" ".async_forward_entry_setup" ) as mock_forward: result = await hass.config_entries.flow.async_init( - "owntracks", context={"source": "import"}, data={} + DOMAIN, context={"source": "import"}, data={} ) assert len(mock_forward.mock_calls) == 1 @@ -51,7 +161,7 @@ async def test_with_cloud_sub(hass): return_value=mock_coro("https://hooks.nabu.casa/ABCD"), ): result = await hass.config_entries.flow.async_init( - "owntracks", context={"source": "user"}, data={} + DOMAIN, context={"source": "user"}, data={} ) entry = result["result"] From a9a1c2b91dd461ab462a1a6b7af6d66a4c01748e Mon Sep 17 00:00:00 2001 From: GaryOkie <37629938+GaryOkie@users.noreply.github.com> Date: Mon, 11 Nov 2019 17:35:09 -0600 Subject: [PATCH 1531/3953] Update Homekit climate.py to remap current mode (#28625) * Update Homekit climate.py to remap current mode This update changes the mapping of Homekit's Current Mode Heating/Cooling State to show the HASS Hvac_action attribute as "idle" instead of "off" when the returned value is 0. * Update climate.py removed imported constant no longer being used (CURRENT_HVAC_OFF) * corrected update to climate.py trying again to remove unused constant. * Update test_climate.py * removed "change" comment The added comment describing the change was not needed and should not be included, as it will already be described via "git annotate" (per @jc2k) --- homeassistant/components/homekit_controller/climate.py | 4 ++-- tests/components/homekit_controller/test_climate.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 194a2b5a42e236..1f9118ff838326 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -13,7 +13,7 @@ HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - CURRENT_HVAC_OFF, + CURRENT_HVAC_IDLE, CURRENT_HVAC_HEAT, CURRENT_HVAC_COOL, SUPPORT_TARGET_TEMPERATURE, @@ -39,7 +39,7 @@ DEFAULT_VALID_MODES = list(MODE_HOMEKIT_TO_HASS) CURRENT_MODE_HOMEKIT_TO_HASS = { - 0: CURRENT_HVAC_OFF, + 0: CURRENT_HVAC_IDLE, 1: CURRENT_HVAC_HEAT, 2: CURRENT_HVAC_COOL, } diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 72c2038f1feeb0..0d3544a6f55883 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -213,7 +213,7 @@ async def test_hvac_mode_vs_hvac_action(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == "heat" - assert state.attributes["hvac_action"] == "off" + assert state.attributes["hvac_action"] == "idle" # Simulate that current temperature is below target temp # Heating might be on and hvac_action currently 'heat' From 25b06312647947c6092b97fd42a85c218f6c7ef6 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 12 Nov 2019 00:32:11 +0000 Subject: [PATCH 1532/3953] [ci skip] Translation update --- .../components/almond/.translations/fr.json | 3 ++- .../components/almond/.translations/it.json | 3 ++- .../components/almond/.translations/sl.json | 3 ++- .../components/climate/.translations/fr.json | 12 +++++++++ .../components/climate/.translations/sl.json | 17 ++++++++++++ .../components/deconz/.translations/it.json | 6 ++++- .../components/deconz/.translations/sl.json | 18 ++++++++++++- .../components/fan/.translations/fr.json | 16 ++++++++++++ .../components/fan/.translations/sl.json | 16 ++++++++++++ .../huawei_lte/.translations/it.json | 5 +++- .../huawei_lte/.translations/sl.json | 5 +++- .../components/lock/.translations/fr.json | 4 +++ .../components/lock/.translations/sl.json | 4 +++ .../components/vacuum/.translations/fr.json | 16 ++++++++++++ .../components/vacuum/.translations/sl.json | 16 ++++++++++++ .../components/wled/.translations/fr.json | 21 +++++++++++++++ .../components/wled/.translations/it.json | 1 + .../components/wled/.translations/sl.json | 26 +++++++++++++++++++ 18 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/climate/.translations/fr.json create mode 100644 homeassistant/components/climate/.translations/sl.json create mode 100644 homeassistant/components/fan/.translations/fr.json create mode 100644 homeassistant/components/fan/.translations/sl.json create mode 100644 homeassistant/components/vacuum/.translations/fr.json create mode 100644 homeassistant/components/vacuum/.translations/sl.json create mode 100644 homeassistant/components/wled/.translations/sl.json diff --git a/homeassistant/components/almond/.translations/fr.json b/homeassistant/components/almond/.translations/fr.json index b304a596b3ac5e..0208366cea1214 100644 --- a/homeassistant/components/almond/.translations/fr.json +++ b/homeassistant/components/almond/.translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Almond", - "cannot_connect": "Impossible de se connecter au serveur Almond" + "cannot_connect": "Impossible de se connecter au serveur Almond", + "missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond." }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/it.json b/homeassistant/components/almond/.translations/it.json index a7e207e899bea4..740535f4f4601d 100644 --- a/homeassistant/components/almond/.translations/it.json +++ b/homeassistant/components/almond/.translations/it.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "\u00c8 possibile configurare un solo account Almond.", - "cannot_connect": "Impossibile connettersi al server Almond." + "cannot_connect": "Impossibile connettersi al server Almond.", + "missing_configuration": "Si prega di controllare la documentazione su come impostare Almond." }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/sl.json b/homeassistant/components/almond/.translations/sl.json index f8bf3021db5547..c809b908b9f580 100644 --- a/homeassistant/components/almond/.translations/sl.json +++ b/homeassistant/components/almond/.translations/sl.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "Konfigurirate lahko samo en ra\u010dun Almond.", - "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond." + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond.", + "missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond." }, "title": "Almond" } diff --git a/homeassistant/components/climate/.translations/fr.json b/homeassistant/components/climate/.translations/fr.json new file mode 100644 index 00000000000000..d82a3644493a09 --- /dev/null +++ b/homeassistant/components/climate/.translations/fr.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "set_preset_mode": "Changer les pr\u00e9r\u00e9glages de {entity_name}" + }, + "trigger_type": { + "current_humidity_changed": "Changement d'humidit\u00e9 mesur\u00e9e pour {entity_name}", + "current_temperature_changed": "Changement de temp\u00e9rature mesur\u00e9e pour {entity_name}", + "hvac_mode_changed": "Mode HVAC chang\u00e9 pour {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/sl.json b/homeassistant/components/climate/.translations/sl.json new file mode 100644 index 00000000000000..4ba4cb02a4b590 --- /dev/null +++ b/homeassistant/components/climate/.translations/sl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Spremeni na\u010din HVAC na {entity_name}", + "set_preset_mode": "Spremenite prednastavitev na {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} je nastavljen na dolo\u010den na\u010din HVAC", + "is_preset_mode": "{entity_name} je nastavljen na dolo\u010den prednastavljeni na\u010din" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} spremenjena izmerjena vla\u017enost", + "current_temperature_changed": "{entity_name} izmerjena temperaturna sprememba", + "hvac_mode_changed": "{entity_name} HVAC na\u010din spremenjen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index e2e0d52906428c..33d49dfca464fa 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -81,7 +81,11 @@ "remote_gyro_activated": "Dispositivo in vibrazione", "remote_moved": "Dispositivo spostato con \"{subtype}\" verso l'alto", "remote_rotate_from_side_1": "Dispositivo ruotato da \"lato 1\" a \"{subtype}\"", - "remote_rotate_from_side_2": "Dispositivo ruotato da \"lato 2\" a \"{subtype}\"" + "remote_rotate_from_side_2": "Dispositivo ruotato da \"lato 2\" a \"{subtype}\"", + "remote_rotate_from_side_3": "Dispositivo ruotato da \"lato 3\" a \"{subtype}\"", + "remote_rotate_from_side_4": "Dispositivo ruotato da \"lato 4\" a \"{subtype}\"", + "remote_rotate_from_side_5": "Dispositivo ruotato da \"lato 5\" a \"{subtype}\"", + "remote_rotate_from_side_6": "Dispositivo ruotato da \"lato 6\" a \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 217007c07d40fc..0edfc11af5558e 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -55,10 +55,17 @@ "left": "Levo", "open": "Odprto", "right": "Desno", + "side_1": "Stran 1", + "side_2": "Stran 2", + "side_3": "Stran 3", + "side_4": "Stran 4", + "side_5": "Stran 5", + "side_6": "Stran 6", "turn_off": "Ugasni", "turn_on": "Pri\u017egi" }, "trigger_type": { + "remote_awakened": "Naprava se je prebudila", "remote_button_double_press": "Dvakrat kliknete gumb \"{subtype}\"", "remote_button_long_press": "\"{subtype}\" gumb neprekinjeno pritisnjen", "remote_button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku", @@ -69,7 +76,16 @@ "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", - "remote_gyro_activated": "Naprava se je pretresla" + "remote_double_tap": "Naprava \"{subtype}\" dvakrat dotaknjena", + "remote_falling": "Naprava v prostem padu", + "remote_gyro_activated": "Naprava se je pretresla", + "remote_moved": "Naprava je premaknjena s \"{subtype}\" navzgor", + "remote_rotate_from_side_1": "Naprava je zasukana iz \"strani 1\" v \"{subtype}\"", + "remote_rotate_from_side_2": "Naprava je zasukana iz \"strani 2\" v \"{subtype}\"", + "remote_rotate_from_side_3": "Naprava je zasukana iz \"strani 3\" v \"{subtype}\"", + "remote_rotate_from_side_4": "Naprava je zasukana iz \"strani 4\" v \"{subtype}\"", + "remote_rotate_from_side_5": "Naprava je zasukana iz \"strani 5\" v \"{subtype}\"", + "remote_rotate_from_side_6": "Naprava je zasukana iz \"strani 6\" v \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/fan/.translations/fr.json b/homeassistant/components/fan/.translations/fr.json new file mode 100644 index 00000000000000..5c5a65b6bcddce --- /dev/null +++ b/homeassistant/components/fan/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u00c9teindre {entity_name}", + "turn_on": "Allumer {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9" + }, + "trigger_type": { + "turned_off": "{entity_name} est \u00e9teint", + "turned_on": "{entity_name} allum\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/sl.json b/homeassistant/components/fan/.translations/sl.json new file mode 100644 index 00000000000000..a5de109f764bc1 --- /dev/null +++ b/homeassistant/components/fan/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Izklopite {entity_name}", + "turn_on": "Vklopite {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} je izklopljen", + "is_on": "{entity_name} je vklopljen" + }, + "trigger_type": { + "turned_off": "{entity_name} izklopljen", + "turned_on": "{entity_name} vklopljen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/it.json b/homeassistant/components/huawei_lte/.translations/it.json index 0646cd4da52982..bcbae3b1b25b35 100644 --- a/homeassistant/components/huawei_lte/.translations/it.json +++ b/homeassistant/components/huawei_lte/.translations/it.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Questo dispositivo \u00e8 gi\u00e0 stato configurato" + "already_configured": "Questo dispositivo \u00e8 gi\u00e0 stato configurato", + "already_in_progress": "Questo dispositivo \u00e8 gi\u00e0 in fase di configurazione", + "not_huawei_lte": "Non \u00e8 un dispositivo Huawei LTE" }, "error": { "connection_failed": "Connessione fallita", + "connection_timeout": "Timeout di connessione", "incorrect_password": "Password errata", "incorrect_username": "Nome utente errato", "incorrect_username_or_password": "Nome utente o password errati", diff --git a/homeassistant/components/huawei_lte/.translations/sl.json b/homeassistant/components/huawei_lte/.translations/sl.json index e23ac72bcca4fd..0b4964069b2e8c 100644 --- a/homeassistant/components/huawei_lte/.translations/sl.json +++ b/homeassistant/components/huawei_lte/.translations/sl.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Ta naprava je \u017ee nastavljena" + "already_configured": "Ta naprava je \u017ee nastavljena", + "already_in_progress": "Ta naprava se \u017ee nastavlja", + "not_huawei_lte": "Ni naprava Huawei LTE" }, "error": { "connection_failed": "Povezava ni uspela", + "connection_timeout": "\u010casovna omejitev povezave", "incorrect_password": "Nepravilno geslo", "incorrect_username": "Nepravilno uporabni\u0161ko ime", "incorrect_username_or_password": "Nepravilno uporabni\u0161ko ime ali geslo", diff --git a/homeassistant/components/lock/.translations/fr.json b/homeassistant/components/lock/.translations/fr.json index 748a1e9290c6e1..cc7e7d6f3e3f84 100644 --- a/homeassistant/components/lock/.translations/fr.json +++ b/homeassistant/components/lock/.translations/fr.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} est verrouill\u00e9", "is_unlocked": "{entity_name} est d\u00e9verrouill\u00e9" + }, + "trigger_type": { + "locked": "{entity_name} verrouill\u00e9", + "unlocked": "{entity_name} d\u00e9verrouill\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/sl.json b/homeassistant/components/lock/.translations/sl.json index d2e32499d2e1db..01cf3feb4a684d 100644 --- a/homeassistant/components/lock/.translations/sl.json +++ b/homeassistant/components/lock/.translations/sl.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} je/so zaklenjen/a", "is_unlocked": "{entity_name} je/so odklenjen/a" + }, + "trigger_type": { + "locked": "{entity_name} zaklenjen/a", + "unlocked": "{entity_name} odklenjen/a" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/fr.json b/homeassistant/components/vacuum/.translations/fr.json new file mode 100644 index 00000000000000..44e7b2887e2717 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Laisser {entity_name} vide", + "dock": "Laisser {entity_name} retourner \u00e0 la base" + }, + "condtion_type": { + "is_cleaning": "{entity_name} nettoie", + "is_docked": "{entity_name} est sur la base" + }, + "trigger_type": { + "cleaning": "{entity_name} commence \u00e0 nettoyer", + "docked": "{entity_name} est sur la base" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/sl.json b/homeassistant/components/vacuum/.translations/sl.json new file mode 100644 index 00000000000000..25de303b157a9e --- /dev/null +++ b/homeassistant/components/vacuum/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Naj {entity_name} \u010disti", + "dock": "Pustite, da se {entity_name} vrne na dok" + }, + "condtion_type": { + "is_cleaning": "{entity_name} \u010disti", + "is_docked": "{entity_name} je priklju\u010den" + }, + "trigger_type": { + "cleaning": "{entity_name} za\u010del \u010di\u0161\u010denje", + "docked": "{entity_name} priklju\u010den" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/fr.json b/homeassistant/components/wled/.translations/fr.json index a9ef8aa567afad..5da30ab6288568 100644 --- a/homeassistant/components/wled/.translations/fr.json +++ b/homeassistant/components/wled/.translations/fr.json @@ -1,5 +1,26 @@ { "config": { + "abort": { + "already_configured": "Cet appareil WLED est d\u00e9j\u00e0 configur\u00e9.", + "connection_error": "\u00c9chec de la connexion au p\u00e9riph\u00e9rique WLED." + }, + "error": { + "connection_error": "\u00c9chec de la connexion au p\u00e9riph\u00e9rique WLED." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "H\u00f4te ou adresse IP" + }, + "description": "Configurer votre WLED pour l'int\u00e9grer \u00e0 Home Assistant.", + "title": "Liez votre WLED" + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter le dispositif WLED nomm\u00e9 '{name}' \u00e0 Home Assistant?", + "title": "Dispositif WLED d\u00e9couvert" + } + }, "title": "WLED" } } \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/it.json b/homeassistant/components/wled/.translations/it.json index 03c24101c2a2f8..300f88ddc4669d 100644 --- a/homeassistant/components/wled/.translations/it.json +++ b/homeassistant/components/wled/.translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Questo dispositivo WLED \u00e8 gi\u00e0 configurato.", "connection_error": "Impossibile connettersi al dispositivo WLED." }, "error": { diff --git a/homeassistant/components/wled/.translations/sl.json b/homeassistant/components/wled/.translations/sl.json new file mode 100644 index 00000000000000..b9ffb347a805cb --- /dev/null +++ b/homeassistant/components/wled/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ta naprava WLED je \u017ee konfigurirana.", + "connection_error": "Povezava z napravo WLED ni uspela." + }, + "error": { + "connection_error": "Povezava z napravo WLED ni uspela." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Gostitelj ali IP naslov" + }, + "description": "Nastavite svoj WLED za integracijo s Home Assistant.", + "title": "Pove\u017eite svoj WLED" + }, + "zeroconf_confirm": { + "description": "Ali \u017eelite dodati WLED z imenom `{name}` v Home Assistant?", + "title": "Odkrite WLED naprave" + } + }, + "title": "WLED" + } +} \ No newline at end of file From cfa689c3a6b139b372283c35e59236ba1727282e Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 11 Nov 2019 20:53:23 -0500 Subject: [PATCH 1533/3953] Bump up ZHA dependencies. (#28711) --- homeassistant/components/zha/manifest.json | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index a8ef269e394277..18e8af7008d154 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.10.0", + "bellows-homeassistant==0.11.0", "zha-quirks==0.0.27", - "zigpy-deconz==0.6.0", - "zigpy-homeassistant==0.10.0", - "zigpy-xbee-homeassistant==0.6.0", + "zigpy-deconz==0.7.0", + "zigpy-homeassistant==0.11.0", + "zigpy-xbee-homeassistant==0.7.0", "zigpy-zigate==0.5.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 01538784fecd58..7859072e2c6d85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -285,7 +285,7 @@ beautifulsoup4==4.8.1 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.10.0 +bellows-homeassistant==0.11.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.6.2 @@ -2063,13 +2063,13 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.6.0 +zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.10.0 +zigpy-homeassistant==0.11.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.6.0 +zigpy-xbee-homeassistant==0.7.0 # homeassistant.components.zha zigpy-zigate==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d352e21b029ed5..589d9d0bc41f51 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,7 +103,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.10.0 +bellows-homeassistant==0.11.0 # homeassistant.components.bom bomradarloop==0.1.3 @@ -640,13 +640,13 @@ zeroconf==0.23.0 zha-quirks==0.0.27 # homeassistant.components.zha -zigpy-deconz==0.6.0 +zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.10.0 +zigpy-homeassistant==0.11.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.6.0 +zigpy-xbee-homeassistant==0.7.0 # homeassistant.components.zha zigpy-zigate==0.5.0 From a89b4011eeeb347a95513329171d31df24cbcfd4 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 11 Nov 2019 18:55:31 -0700 Subject: [PATCH 1534/3953] Ensure SimpliSafe alarm control panels can return from being offline (#28710) --- homeassistant/components/simplisafe/alarm_control_panel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index bbd9a5d4e51351..a63a077ed15b8c 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -133,6 +133,8 @@ async def async_update(self): self._online = False return + self._online = True + if self._system.state == SystemStates.off: self._state = STATE_ALARM_DISARMED elif self._system.state in (SystemStates.home, SystemStates.home_count): From 87606bc12b34876796105ddd2e26fa60ed2cdb48 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 11 Nov 2019 21:30:07 -0600 Subject: [PATCH 1535/3953] Bump plexapi to 3.3.0 (#28709) * Bump plexapi to 3.2.0 * Bump to 3.3.0 --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index eaf3028b39fc43..29bdbf34b60f2e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==3.0.6", + "plexapi==3.3.0", "plexauth==0.0.5", "plexwebsocket==0.0.5" ], diff --git a/requirements_all.txt b/requirements_all.txt index 7859072e2c6d85..aebf0c92b22422 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,7 +980,7 @@ pillow==6.2.1 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==3.0.6 +plexapi==3.3.0 # homeassistant.components.plex plexauth==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 589d9d0bc41f51..2126fed9f6f075 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ pilight==0.1.1 pillow==6.2.1 # homeassistant.components.plex -plexapi==3.0.6 +plexapi==3.3.0 # homeassistant.components.plex plexauth==0.0.5 From 4f11eec1a1bef997ab1ec1413bdbdd7577cf4c19 Mon Sep 17 00:00:00 2001 From: Federico Leoni Date: Tue, 12 Nov 2019 05:53:08 -0300 Subject: [PATCH 1536/3953] Update binary_sensor.py (#28707) * Update binary_sensor.py * Fix style issues --- homeassistant/components/mqtt/binary_sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index bcf398464bc7c0..fe47729561d4c6 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -190,9 +190,11 @@ def state_message_received(msg): self._state = False else: # Payload is not for this entity _LOGGER.warning( - "No matching payload found" " for entity: %s with state_topic: %s", + "No matching payload found for entity: %s with state topic: %s. Payload: %s, with value template %s", self._config[CONF_NAME], self._config[CONF_STATE_TOPIC], + payload, + value_template, ) return From e8348221d488da6086c9db756a1c5f78db21061f Mon Sep 17 00:00:00 2001 From: SukramJ Date: Tue, 12 Nov 2019 11:32:32 +0100 Subject: [PATCH 1537/3953] Allow preset boost for Homematic IP Cloud power switches (#28713) * Allow preset boost for Homematic IP Cloud power switches * Sort Imports * Add test * align test data --- .../components/homematicip_cloud/climate.py | 18 ++++++++++++++--- .../homematicip_cloud/test_climate.py | 20 +++++++++++++++++++ tests/fixtures/homematicip_cloud.json | 16 ++++++++++----- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index a8ea424b2072d5..9673459e820db5 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -5,6 +5,7 @@ from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup from homematicip.base.enums import AbsenceType +from homematicip.device import Switch from homematicip.functionalHomes import IndoorClimateHome from homeassistant.components.climate import ClimateDevice @@ -116,7 +117,7 @@ def current_humidity(self) -> int: @property def hvac_mode(self) -> str: """Return hvac operation ie.""" - if self._disabled_by_cooling_mode: + if self._disabled_by_cooling_mode and not self._has_switch: return HVAC_MODE_OFF if self._device.boostMode: return HVAC_MODE_HEAT @@ -128,7 +129,7 @@ def hvac_mode(self) -> str: @property def hvac_modes(self): """Return the list of available hvac operation modes.""" - if self._disabled_by_cooling_mode: + if self._disabled_by_cooling_mode and not self._has_switch: return [HVAC_MODE_OFF] return ( @@ -168,7 +169,9 @@ def preset_modes(self): profile_names = self._device_profile_names presets = [] - if self._heat_mode_enabled and self._has_radiator_thermostat: + if ( + self._heat_mode_enabled and self._has_radiator_thermostat + ) or self._has_switch: if not profile_names: presets.append(PRESET_NONE) presets.append(PRESET_BOOST) @@ -290,6 +293,15 @@ def _relevant_profile_group(self): return HEATING_PROFILES if self._heat_mode_enabled else COOLING_PROFILES + @property + def _has_switch(self) -> bool: + """Return, if a switch is in the hmip heating group.""" + for device in self._device.devices: + if isinstance(device, Switch): + return True + + return False + @property def _has_radiator_thermostat(self) -> bool: """Return, if a radiator thermostat is in the hmip heating group.""" diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 6a05a88086464f..858fba29563519 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -333,6 +333,26 @@ async def test_hmip_heating_group_cool(hass, default_mock_hap): assert hmip_device.mock_calls[-1][1] == (4,) +async def test_hmip_heating_group_heat_with_switch(hass, default_mock_hap): + """Test HomematicipHeatingGroup.""" + entity_id = "climate.schlafzimmer" + entity_name = "Schlafzimmer" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes["current_temperature"] == 24.7 + assert ha_state.attributes["min_temp"] == 5.0 + assert ha_state.attributes["max_temp"] == 30.0 + assert ha_state.attributes["temperature"] == 5.0 + assert ha_state.attributes["current_humidity"] == 43 + assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" + assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_BOOST, "STD", "P2"] + + async def test_hmip_climate_services(hass, mock_hap_with_service): """Test HomematicipHeatingGroup.""" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index d1ef82f423452c..8cec2462f32dda 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -5,6 +5,12 @@ "id": "00000000-0000-0000-0000-000000000000", "label": "TEST-Client", "clientType": "APP" + }, + "AA000000-0000-0000-0000-000000000000": { + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "AA000000-0000-0000-0000-000000000000", + "label": "REMOVE_ME", + "clientType": "APP" } }, "devices": { @@ -1485,7 +1491,7 @@ "modelType": "HmIP-SMI", "oem": "eQ-3", "permanentlyReachable": true, - "serializedGlobalTradeItemNumber": "3014F7110000000000000011", + "serializedGlobalTradeItemNumber": "3014F711000000000000BB11", "type": "MOTION_DETECTOR_INDOOR", "updateState": "UP_TO_DATE" }, @@ -2202,7 +2208,7 @@ } }, "homeId": "00000000-0000-0000-0000-000000000001", - "id": "3014F7110000000000000011", + "id": "3014F7110000000000000109", "label": "Ausschalter Terrasse Bewegungsmelder", "lastStatusUpdate": 1570366291250, "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", @@ -4075,7 +4081,7 @@ }, { "channelIndex": 1, - "deviceId": "3014F7110000000000000011" + "deviceId": "3014F7110000000000000008" } ], "controlMode": "AUTOMATIC", @@ -4109,7 +4115,7 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000012", "index": "PROFILE_1", - "name": "", + "name": "STD", "profileId": "00000000-0000-0000-0000-000000000023", "visible": true }, @@ -4117,7 +4123,7 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000012", "index": "PROFILE_2", - "name": "", + "name": "P2", "profileId": "00000000-0000-0000-0000-000000000024", "visible": true }, From 1208ab4c76ad3b1390f3e0e4681826d67910256e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 12 Nov 2019 12:28:34 +0100 Subject: [PATCH 1538/3953] Upgrade discogs_client to 2.2.2 (#28723) --- homeassistant/components/discogs/manifest.json | 2 +- homeassistant/components/discogs/sensor.py | 12 +++--------- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/discogs/manifest.json b/homeassistant/components/discogs/manifest.json index 18282f077817a6..5a1c670508a3b5 100644 --- a/homeassistant/components/discogs/manifest.json +++ b/homeassistant/components/discogs/manifest.json @@ -3,7 +3,7 @@ "name": "Discogs", "documentation": "https://www.home-assistant.io/integrations/discogs", "requirements": [ - "discogs_client==2.2.1" + "discogs_client==2.2.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index b3f29fbe75b898..b5e488cc19c624 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -66,7 +66,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Discogs sensor.""" - token = config[CONF_TOKEN] name = config[CONF_NAME] @@ -104,7 +103,7 @@ def __init__(self, discogs_data, name, sensor_type): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, SENSORS[self._type]["name"]) + return f"{self._name} {SENSORS[self._type]['name']}" @property def state(self): @@ -136,10 +135,7 @@ def device_state_attributes(self): return { "cat_no": self._attrs["labels"][0]["catno"], "cover_image": self._attrs["cover_image"], - "format": "{} ({})".format( - self._attrs["formats"][0]["name"], - self._attrs["formats"][0]["descriptions"][0], - ), + "format": f"{self._attrs['formats'][0]['name']} ({self._attrs['formats'][0]['descriptions'][0]})", "label": self._attrs["labels"][0]["name"], "released": self._attrs["year"], ATTR_ATTRIBUTION: ATTRIBUTION, @@ -154,9 +150,7 @@ def get_random_record(self): random_record = collection.releases[random_index].release self._attrs = random_record.data - return "{} - {}".format( - random_record.data["artists"][0]["name"], random_record.data["title"] - ) + return f"{random_record.data['artists'][0]['name']} - {random_record.data['title']}" def update(self): """Set state to the amount of records in user's collection.""" diff --git a/requirements_all.txt b/requirements_all.txt index aebf0c92b22422..c907fd879a2143 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -414,7 +414,7 @@ denonavr==0.7.10 directpy==0.5 # homeassistant.components.discogs -discogs_client==2.2.1 +discogs_client==2.2.2 # homeassistant.components.discord discord.py==1.2.4 From 48fd95c7dbfa0b870dc5f18e315801c6174981cf Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Tue, 12 Nov 2019 15:04:17 +0100 Subject: [PATCH 1539/3953] Fix Here Travel Time unable to find entity on startup (#27237) * add support for entity_id as state of entity * add circular reference detection * voluptuous instead of regex * wait for EVENT_HOMEASSISTANT_START * move delayed_sensor_update to async_added_to_hass * add @callback decorator * remove nested entity resolving --- .../components/here_travel_time/sensor.py | 75 ++++++--- .../here_travel_time/test_sensor.py | 149 +++++++++++++++++- 2 files changed, 198 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index afd12bb01c5531..0b688a770c58f5 100755 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -17,8 +17,9 @@ CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, + EVENT_HOMEASSISTANT_START, ) -from homeassistant.core import HomeAssistant, State +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import location import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -89,8 +90,6 @@ SCAN_INTERVAL = timedelta(minutes=5) -TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] - NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" PLATFORM_SCHEMA = vol.All( @@ -146,15 +145,19 @@ async def async_setup_platform( if config.get(CONF_ORIGIN_LATITUDE) is not None: origin = f"{config[CONF_ORIGIN_LATITUDE]},{config[CONF_ORIGIN_LONGITUDE]}" + origin_entity_id = None else: - origin = config[CONF_ORIGIN_ENTITY_ID] + origin = None + origin_entity_id = config[CONF_ORIGIN_ENTITY_ID] if config.get(CONF_DESTINATION_LATITUDE) is not None: destination = ( f"{config[CONF_DESTINATION_LATITUDE]},{config[CONF_DESTINATION_LONGITUDE]}" ) + destination_entity_id = None else: - destination = config[CONF_DESTINATION_ENTITY_ID] + destination = None + destination_entity_id = config[CONF_DESTINATION_ENTITY_ID] travel_mode = config[CONF_MODE] traffic_mode = config[CONF_TRAFFIC_MODE] @@ -166,9 +169,11 @@ async def async_setup_platform( here_client, travel_mode, traffic_mode, route_mode, units ) - sensor = HERETravelTimeSensor(name, origin, destination, here_data) + sensor = HERETravelTimeSensor( + name, origin, destination, origin_entity_id, destination_entity_id, here_data + ) - async_add_entities([sensor], True) + async_add_entities([sensor]) def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool: @@ -194,31 +199,43 @@ class HERETravelTimeSensor(Entity): """Representation of a HERE travel time sensor.""" def __init__( - self, name: str, origin: str, destination: str, here_data: "HERETravelTimeData" + self, + name: str, + origin: str, + destination: str, + origin_entity_id: str, + destination_entity_id: str, + here_data: "HERETravelTimeData", ) -> None: """Initialize the sensor.""" self._name = name + self._origin_entity_id = origin_entity_id + self._destination_entity_id = destination_entity_id self._here_data = here_data self._unit_of_measurement = UNIT_OF_MEASUREMENT - self._origin_entity_id = None - self._destination_entity_id = None self._attrs = { ATTR_UNIT_SYSTEM: self._here_data.units, ATTR_MODE: self._here_data.travel_mode, ATTR_TRAFFIC_MODE: self._here_data.traffic_mode, } - - # Check if location is a trackable entity - if origin.split(".", 1)[0] in TRACKABLE_DOMAINS: - self._origin_entity_id = origin - else: + if self._origin_entity_id is None: self._here_data.origin = origin - if destination.split(".", 1)[0] in TRACKABLE_DOMAINS: - self._destination_entity_id = destination - else: + if self._destination_entity_id is None: self._here_data.destination = destination + async def async_added_to_hass(self) -> None: + """Delay the sensor update to avoid entity not found warnings.""" + + @callback + def delayed_sensor_update(event): + """Update sensor after homeassistant started.""" + self.async_schedule_update_ha_state(True) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, delayed_sensor_update + ) + @property def state(self) -> Optional[str]: """Return the state of the sensor.""" @@ -309,10 +326,28 @@ async def _get_location_from_entity(self, entity_id: str) -> Optional[str]: ) return self._get_location_from_attributes(zone_entity) - # If zone was not found in state then use the state as the location - if entity_id.startswith("sensor."): + # Check if state is valid coordinate set + if self._entity_state_is_valid_coordinate_set(entity.state): return entity.state + _LOGGER.error( + "The state of %s is not a valid set of coordinates: %s", + entity_id, + entity.state, + ) + return None + + @staticmethod + def _entity_state_is_valid_coordinate_set(state: str) -> bool: + """Check that the given string is a valid set of coordinates.""" + schema = vol.Schema(cv.gps) + try: + coordinates = state.split(",") + schema(coordinates) + return True + except (vol.MultipleInvalid): + return False + @staticmethod def _get_location_from_attributes(entity: State) -> str: """Get the lat/long string from an entities attributes.""" diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 783209690a389c..1d788c93c663d5 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -38,7 +38,7 @@ TRAVEL_MODE_TRUCK, UNIT_OF_MEASUREMENT, ) -from homeassistant.const import ATTR_ICON +from homeassistant.const import ATTR_ICON, EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -176,13 +176,14 @@ async def test_car(hass, requests_mock_car_disabled_response): "app_code": APP_CODE, } } - assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.state == "30" assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT - assert sensor.attributes.get(ATTR_ATTRIBUTION) is None assert sensor.attributes.get(ATTR_DURATION) == 30.05 assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 @@ -241,8 +242,10 @@ async def test_traffic_mode_enabled(hass, requests_mock_credentials_check): } assert await async_setup_component(hass, DOMAIN, config) - sensor = hass.states.get("sensor.test") + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") # Test traffic mode enabled assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( ATTR_DURATION_IN_TRAFFIC @@ -266,6 +269,9 @@ async def test_imperial(hass, requests_mock_car_disabled_response): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.attributes.get(ATTR_DISTANCE) == 14.852635608048994 @@ -295,6 +301,9 @@ async def test_route_mode_shortest(hass, requests_mock_credentials_check): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.attributes.get(ATTR_DISTANCE) == 18.388 @@ -324,6 +333,9 @@ async def test_route_mode_fastest(hass, requests_mock_credentials_check): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.attributes.get(ATTR_DISTANCE) == 23.381 @@ -344,6 +356,10 @@ async def test_truck(hass, requests_mock_truck_response): } } assert await async_setup_component(hass, DOMAIN, config) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) @@ -373,6 +389,9 @@ async def test_public_transport(hass, requests_mock_credentials_check): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.state == "89" assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT @@ -421,6 +440,9 @@ async def test_public_transport_time_table(hass, requests_mock_credentials_check } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.state == "80" assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT @@ -466,8 +488,12 @@ async def test_pedestrian(hass, requests_mock_credentials_check): "mode": TRAVEL_MODE_PEDESTRIAN, } } + assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.state == "211" assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT @@ -517,6 +543,9 @@ async def test_bicycle(hass, requests_mock_credentials_check): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert sensor.state == "55" assert sensor.attributes.get("unit_of_measurement") == UNIT_OF_MEASUREMENT @@ -564,8 +593,6 @@ async def test_location_zone(hass, requests_mock_truck_response): }, ] } - assert await async_setup_component(hass, "zone", zone_config) - config = { DOMAIN: { "platform": PLATFORM, @@ -577,8 +604,12 @@ async def test_location_zone(hass, requests_mock_truck_response): "mode": TRAVEL_MODE_TRUCK, } } + assert await async_setup_component(hass, "zone", zone_config) assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) @@ -616,6 +647,9 @@ async def test_location_sensor(hass, requests_mock_truck_response): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) @@ -662,6 +696,9 @@ async def test_location_person(hass, requests_mock_truck_response): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) @@ -708,6 +745,9 @@ async def test_location_device_tracker(hass, requests_mock_truck_response): } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) @@ -740,6 +780,9 @@ async def test_location_device_tracker_added_after_update( } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert len(caplog.records) == 2 assert "Unable to find entity" in caplog.text @@ -806,6 +849,9 @@ async def test_location_device_tracker_in_zone( } assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") _assert_truck_sensor(sensor) assert ", getting zone location" in caplog.text @@ -836,6 +882,10 @@ async def test_route_not_found(hass, requests_mock_credentials_check, caplog): } } assert await async_setup_component(hass, DOMAIN, config) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(caplog.records) == 1 assert NO_ROUTE_ERROR_MESSAGE in caplog.text @@ -940,8 +990,95 @@ async def test_attribution(hass, requests_mock_credentials_check): } } assert await async_setup_component(hass, DOMAIN, config) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + sensor = hass.states.get("sensor.test") assert ( sensor.attributes.get(ATTR_ATTRIBUTION) == "With the support of HERE Technologies. All information is provided without warranty of any kind." ) + + +async def test_pattern_entity_state(hass, requests_mock_truck_response, caplog): + """Test that pattern matching the state of an entity works.""" + caplog.set_level(logging.ERROR) + hass.states.async_set("sensor.origin", "invalid") + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "sensor.origin", + "destination_latitude": TRUCK_DESTINATION_LATITUDE, + "destination_longitude": TRUCK_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(caplog.records) == 1 + assert "is not a valid set of coordinates" in caplog.text + + +async def test_pattern_entity_state_with_space(hass, requests_mock_truck_response): + """Test that pattern matching the state including a space of an entity works.""" + hass.states.async_set( + "sensor.origin", ", ".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]) + ) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "sensor.origin", + "destination_latitude": TRUCK_DESTINATION_LATITUDE, + "destination_longitude": TRUCK_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + +async def test_delayed_update(hass, requests_mock_truck_response, caplog): + """Test that delayed update does not complain about missing entities.""" + caplog.set_level(logging.WARNING) + + config = { + DOMAIN: { + "platform": PLATFORM, + "name": "test", + "origin_entity_id": "sensor.origin", + "destination_latitude": TRUCK_DESTINATION_LATITUDE, + "destination_longitude": TRUCK_DESTINATION_LONGITUDE, + "app_id": APP_ID, + "app_code": APP_CODE, + "mode": TRAVEL_MODE_TRUCK, + } + } + sensor_config = { + "sensor": { + "platform": "template", + "sensors": [ + {"template_sensor": {"value_template": "{{states('sensor.origin')}}"}} + ], + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert await async_setup_component(hass, "sensor", sensor_config) + hass.states.async_set( + "sensor.origin", ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]) + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert "Unable to find entity" not in caplog.text From 5f177fa42e48f298400b45725926da6f2ffb8e72 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 12 Nov 2019 10:05:30 -0600 Subject: [PATCH 1540/3953] Use library method for season number (#28708) --- homeassistant/components/plex/media_player.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 1fe83928d29e9d..d6720fd9e95051 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -289,12 +289,7 @@ def _set_media_type(self): self._media_content_type = MEDIA_TYPE_TVSHOW # season number (00) - if callable(self.session.season): - self._media_season = str((self.session.season()).index).zfill(2) - elif self.session.parentIndex is not None: - self._media_season = self.session.parentIndex.zfill(2) - else: - self._media_season = None + self._media_season = self.session.seasonNumber # show name self._media_series_title = self.session.grandparentTitle # episode number (00) From fc04b3e31c465348f5242cfebf2776c0d2309301 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Nov 2019 09:22:28 -0800 Subject: [PATCH 1541/3953] Remove choice word when Almond has choices (#28725) --- homeassistant/components/almond/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 9977d48ae9a0c5..a1983288f92a47 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -243,6 +243,7 @@ async def async_process( """Process a sentence.""" response = await self.api.async_converse_text(text, conversation_id) + first_choice = True buffer = "" for message in response["messages"]: if message["type"] == "text": @@ -257,7 +258,11 @@ async def async_process( + message["rdl"]["webCallback"] ) elif message["type"] == "choice": - buffer += "\n Choice: " + message["title"] + if first_choice: + first_choice = False + else: + buffer += "," + buffer += f" {message['title']}" intent_result = intent.IntentResponse() intent_result.async_set_speech(buffer.strip()) From a1f2b6d402ff249a3a27017c20a6439722504f23 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 12 Nov 2019 18:26:46 +0100 Subject: [PATCH 1542/3953] ESPHome fix missing state in certain circumstances (#28729) * Fix ESPHome having missing state in certain situations Fixes https://github.com/esphome/issues/issues/828 * Update requirements_all * Also fix climate preset mode --- homeassistant/components/esphome/binary_sensor.py | 2 ++ homeassistant/components/esphome/climate.py | 2 +- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/sensor.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 4e684638bb7ff1..64506f69283cfa 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -44,6 +44,8 @@ def is_on(self) -> Optional[bool]: return self._entry_data.available if self._state is None: return None + if self._state.missing_state: + return None return self._state.state @property diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 1dfe2184952e81..5fed8da76ef6c2 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -138,7 +138,7 @@ def hvac_mode(self) -> Optional[str]: @esphome_state_property def preset_mode(self): """Return current preset mode.""" - return PRESET_AWAY if self._state.away else None + return PRESET_AWAY if self._state.away else PRESET_HOME @esphome_state_property def current_temperature(self) -> Optional[float]: diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 40691c653f51b6..724946e69841ac 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.4.2" + "aioesphomeapi==2.5.0" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index b6adbf93c41ac3..e50991af6c1376 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -67,6 +67,8 @@ def state(self) -> Optional[str]: """Return the state of the entity.""" if math.isnan(self._state.state): return None + if self._state.missing_state: + return None return "{:.{prec}f}".format( self._state.state, prec=self._static_info.accuracy_decimals ) @@ -96,4 +98,6 @@ def icon(self) -> str: @esphome_state_property def state(self) -> Optional[str]: """Return the state of the entity.""" + if self._state.missing_state: + return None return self._state.state diff --git a/requirements_all.txt b/requirements_all.txt index c907fd879a2143..163918f5180227 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -142,7 +142,7 @@ aiobotocore==0.10.2 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.4.2 +aioesphomeapi==2.5.0 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2126fed9f6f075..90668e3eaabdd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -50,7 +50,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.2 # homeassistant.components.esphome -aioesphomeapi==2.4.2 +aioesphomeapi==2.5.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 5961215e6e52131e0d59cdaf6a8aae9e2a0c0b94 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Nov 2019 11:01:19 -0800 Subject: [PATCH 1543/3953] Set up Almond Web to connect to HA (#28603) * Set up Almond Web to connect to HA * Add missing string * Add type --- homeassistant/components/almond/__init__.py | 69 ++++++++--- homeassistant/components/almond/strings.json | 5 + homeassistant/helpers/network.py | 38 +++++++ tests/components/almond/test_init.py | 113 +++++++++++++++++++ tests/helpers/test_network.py | 34 ++++++ 5 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 homeassistant/helpers/network.py create mode 100644 tests/components/almond/test_init.py create mode 100644 tests/helpers/test_network.py diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index a1983288f92a47..6d4ab31bf17492 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -10,16 +10,18 @@ from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI import voluptuous as vol -from homeassistant import core -from homeassistant.const import CONF_TYPE, CONF_HOST +from homeassistant.core import HomeAssistant, CoreState +from homeassistant.const import CONF_TYPE, CONF_HOST, EVENT_HOMEASSISTANT_START from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.helpers import ( config_validation as cv, config_entry_oauth2_flow, + event, intent, aiohttp_client, storage, + network, ) from homeassistant import config_entries from homeassistant.components import conversation @@ -33,6 +35,8 @@ STORAGE_VERSION = 1 STORAGE_KEY = DOMAIN +ALMOND_SETUP_DELAY = 30 + DEFAULT_OAUTH2_HOST = "https://almond.stanford.edu" DEFAULT_LOCAL_HOST = "http://localhost:3000" @@ -93,7 +97,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): """Set up Almond config entry.""" websession = aiohttp_client.async_get_clientsession(hass) @@ -112,12 +116,50 @@ async def async_setup_entry(hass, entry): api = WebAlmondAPI(auth) agent = AlmondAgent(hass, api, entry) - # Hass.io does its own configuration of Almond. - if entry.data.get("is_hassio") or entry.data["type"] != TYPE_LOCAL: - conversation.async_set_agent(hass, agent) - return True + # Hass.io does its own configuration. + if not entry.data.get("is_hassio"): + # If we're not starting or local, set up Almond right away + if hass.state != CoreState.not_running or entry.data["type"] == TYPE_LOCAL: + await _configure_almond_for_ha(hass, entry, api) + + else: + # OAuth2 implementations can potentially rely on the HA Cloud url. + # This url is not be available until 30 seconds after boot. + + async def configure_almond(_now): + try: + await _configure_almond_for_ha(hass, entry, api) + except ConfigEntryNotReady: + _LOGGER.warning( + "Unable to configure Almond to connect to Home Assistant" + ) + + async def almond_hass_start(_event): + event.async_call_later(hass, ALMOND_SETUP_DELAY, configure_almond) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, almond_hass_start) + + conversation.async_set_agent(hass, agent) + return True - # Configure Almond to connect to Home Assistant + +async def _configure_almond_for_ha( + hass: HomeAssistant, entry: config_entries.ConfigEntry, api: WebAlmondAPI +): + """Configure Almond to connect to HA.""" + + if entry.data["type"] == TYPE_OAUTH2: + # If we're connecting over OAuth2, we will only set up connection + # with Home Assistant if we're remotely accessible. + hass_url = network.async_get_external_url(hass) + else: + hass_url = hass.config.api.base_url + + # If hass_url is None, we're not going to configure Almond to connect to HA. + if hass_url is None: + return + + _LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url) store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) data = await store.async_load() @@ -144,11 +186,11 @@ async def async_setup_entry(hass, entry): # Store token in Almond try: - with async_timeout.timeout(10): + with async_timeout.timeout(30): await api.async_create_device( { "kind": "io.home-assistant", - "hassUrl": hass.config.api.base_url, + "hassUrl": hass_url, "accessToken": access_token, "refreshToken": "", # 5 years from now in ms. @@ -169,9 +211,6 @@ async def async_setup_entry(hass, entry): if token.id != refresh_token.id: await hass.auth.async_remove_refresh_token(token) - conversation.async_set_agent(hass, agent) - return True - async def async_unload_entry(hass, entry): """Unload Almond.""" @@ -203,7 +242,9 @@ async def async_get_access_token(self): class AlmondAgent(conversation.AbstractConversationAgent): """Almond conversation agent.""" - def __init__(self, hass: core.HomeAssistant, api: WebAlmondAPI, entry): + def __init__( + self, hass: HomeAssistant, api: WebAlmondAPI, entry: config_entries.ConfigEntry + ): """Initialize the agent.""" self.hass = hass self.api = api diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json index 5cfc52044bb04a..872367eb86237f 100644 --- a/homeassistant/components/almond/strings.json +++ b/homeassistant/components/almond/strings.json @@ -1,5 +1,10 @@ { "config": { + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, "abort": { "already_setup": "You can only configure one Almond account.", "cannot_connect": "Unable to connect to the Almond server.", diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py new file mode 100644 index 00000000000000..671e7f1fa56e26 --- /dev/null +++ b/homeassistant/helpers/network.py @@ -0,0 +1,38 @@ +"""Network helpers.""" +from typing import Optional, cast +from ipaddress import ip_address + +import yarl + +from homeassistant.core import HomeAssistant, callback +from homeassistant.loader import bind_hass +from homeassistant.util.network import is_local + + +@bind_hass +@callback +def async_get_external_url(hass: HomeAssistant) -> Optional[str]: + """Get external url of this instance. + + Note: currently it takes 30 seconds after Home Assistant starts for + cloud.async_remote_ui_url to work. + """ + if "cloud" in hass.config.components: + try: + return cast(str, hass.components.cloud.async_remote_ui_url()) + except hass.components.cloud.CloudNotAvailable: + pass + + if hass.config.api is None: + return None + + base_url = yarl.URL(hass.config.api.base_url) + + try: + if is_local(ip_address(base_url.host)): + return None + except ValueError: + # ip_address raises ValueError if host is not an IP address + pass + + return str(base_url) diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py new file mode 100644 index 00000000000000..dd44ea1c8f03f5 --- /dev/null +++ b/tests/components/almond/test_init.py @@ -0,0 +1,113 @@ +"""Tests for Almond set up.""" +from unittest.mock import patch +from time import time + +import pytest + +from homeassistant import config_entries, core +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.setup import async_setup_component +from homeassistant.components.almond import const +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, mock_coro, async_fire_time_changed + + +@pytest.fixture(autouse=True) +def patch_hass_state(hass): + """Mock the hass.state to be not_running.""" + hass.state = core.CoreState.not_running + + +async def test_set_up_oauth_remote_url(hass, aioclient_mock): + """Test we set up Almond to connect to HA if we have external url.""" + entry = MockConfigEntry( + domain="almond", + data={ + "type": const.TYPE_OAUTH2, + "auth_implementation": "local", + "host": "http://localhost:9999", + "token": {"expires_at": time() + 1000, "access_token": "abcd"}, + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + return_value=mock_coro(), + ): + assert await async_setup_component(hass, "almond", {}) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + + with patch("homeassistant.components.almond.ALMOND_SETUP_DELAY", 0), patch( + "homeassistant.helpers.network.async_get_external_url", + return_value="https://example.nabu.casa", + ), patch( + "pyalmond.WebAlmondAPI.async_create_device", return_value=mock_coro() + ) as mock_create_device: + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + assert len(mock_create_device.mock_calls) == 1 + + +async def test_set_up_oauth_no_external_url(hass, aioclient_mock): + """Test we do not set up Almond to connect to HA if we have no external url.""" + entry = MockConfigEntry( + domain="almond", + data={ + "type": const.TYPE_OAUTH2, + "auth_implementation": "local", + "host": "http://localhost:9999", + "token": {"expires_at": time() + 1000, "access_token": "abcd"}, + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + return_value=mock_coro(), + ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: + assert await async_setup_component(hass, "almond", {}) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert len(mock_create_device.mock_calls) == 0 + + +async def test_set_up_hassio(hass, aioclient_mock): + """Test we do not set up Almond to connect to HA if we use hassio.""" + entry = MockConfigEntry( + domain="almond", + data={ + "is_hassio": True, + "type": const.TYPE_LOCAL, + "host": "http://localhost:9999", + }, + ) + entry.add_to_hass(hass) + + with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: + assert await async_setup_component(hass, "almond", {}) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert len(mock_create_device.mock_calls) == 0 + + +async def test_set_up_local(hass, aioclient_mock): + """Test we do not set up Almond to connect to HA if we use hassio.""" + entry = MockConfigEntry( + domain="almond", + data={"type": const.TYPE_LOCAL, "host": "http://localhost:9999"}, + ) + entry.add_to_hass(hass) + + with patch( + "pyalmond.WebAlmondAPI.async_create_device", return_value=mock_coro() + ) as mock_create_device: + assert await async_setup_component(hass, "almond", {}) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert len(mock_create_device.mock_calls) == 1 diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py new file mode 100644 index 00000000000000..afb9e88c5a499d --- /dev/null +++ b/tests/helpers/test_network.py @@ -0,0 +1,34 @@ +"""Test network helper.""" +from unittest.mock import Mock, patch + +from homeassistant.helpers import network +from homeassistant.components import cloud + + +async def test_get_external_url(hass): + """Test get_external_url.""" + hass.config.api = Mock(base_url="http://192.168.1.100:8123") + + assert network.async_get_external_url(hass) is None + + hass.config.api = Mock(base_url="http://example.duckdns.org:8123") + + assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123" + + hass.config.components.add("cloud") + + assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123" + + with patch.object( + hass.components.cloud, + "async_remote_ui_url", + side_effect=cloud.CloudNotAvailable, + ): + assert network.async_get_external_url(hass) == "http://example.duckdns.org:8123" + + with patch.object( + hass.components.cloud, + "async_remote_ui_url", + return_value="https://example.nabu.casa", + ): + assert network.async_get_external_url(hass) == "https://example.nabu.casa" From d2d9f09f13e40a8cae9c433e918b53a13f5440a1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 13 Nov 2019 00:32:12 +0000 Subject: [PATCH 1544/3953] [ci skip] Translation update --- .../components/almond/.translations/en.json | 5 +++ .../components/almond/.translations/es.json | 15 +++++++ .../cert_expiry/.translations/es.json | 4 +- .../components/climate/.translations/es.json | 17 ++++++++ .../coolmaster/.translations/es.json | 23 ++++++++++ .../components/cover/.translations/es.json | 12 +++++- .../components/deconz/.translations/es.json | 18 +++++++- .../device_tracker/.translations/es.json | 8 ++++ .../components/fan/.translations/es.json | 16 +++++++ .../huawei_lte/.translations/es.json | 42 +++++++++++++++++++ .../components/lock/.translations/es.json | 4 ++ .../media_player/.translations/es.json | 11 +++++ .../components/sensor/.translations/es.json | 8 ++-- .../components/solarlog/.translations/es.json | 21 ++++++++++ .../components/somfy/.translations/es.json | 5 +++ .../transmission/.translations/es.json | 5 ++- .../components/vacuum/.translations/es.json | 16 +++++++ .../components/withings/.translations/es.json | 7 ++++ .../components/wled/.translations/es.json | 26 ++++++++++++ 19 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/almond/.translations/es.json create mode 100644 homeassistant/components/climate/.translations/es.json create mode 100644 homeassistant/components/coolmaster/.translations/es.json create mode 100644 homeassistant/components/device_tracker/.translations/es.json create mode 100644 homeassistant/components/fan/.translations/es.json create mode 100644 homeassistant/components/huawei_lte/.translations/es.json create mode 100644 homeassistant/components/media_player/.translations/es.json create mode 100644 homeassistant/components/solarlog/.translations/es.json create mode 100644 homeassistant/components/vacuum/.translations/es.json create mode 100644 homeassistant/components/wled/.translations/es.json diff --git a/homeassistant/components/almond/.translations/en.json b/homeassistant/components/almond/.translations/en.json index 3ee811b8326413..3b7b5b9aa636a0 100644 --- a/homeassistant/components/almond/.translations/en.json +++ b/homeassistant/components/almond/.translations/en.json @@ -5,6 +5,11 @@ "cannot_connect": "Unable to connect to the Almond server.", "missing_configuration": "Please check the documentation on how to set up Almond." }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/es.json b/homeassistant/components/almond/.translations/es.json new file mode 100644 index 00000000000000..26eacb834b0ca7 --- /dev/null +++ b/homeassistant/components/almond/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_setup": "S\u00f3lo puede configurar una cuenta de Almond.", + "cannot_connect": "No se puede conectar al servidor Almond.", + "missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond." + }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/es.json b/homeassistant/components/cert_expiry/.translations/es.json index b10518646ac907..4432edac563131 100644 --- a/homeassistant/components/cert_expiry/.translations/es.json +++ b/homeassistant/components/cert_expiry/.translations/es.json @@ -4,10 +4,12 @@ "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" }, "error": { + "certificate_error": "El certificado no pudo ser validado", "certificate_fetch_failed": "No se puede obtener el certificado de esta combinaci\u00f3n de host y puerto", "connection_timeout": "Tiempo de espera agotado al conectar a este host", "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", - "resolve_failed": "Este host no se puede resolver" + "resolve_failed": "Este host no se puede resolver", + "wrong_host": "El certificado no coincide con el nombre de host" }, "step": { "user": { diff --git a/homeassistant/components/climate/.translations/es.json b/homeassistant/components/climate/.translations/es.json new file mode 100644 index 00000000000000..baae9b974367af --- /dev/null +++ b/homeassistant/components/climate/.translations/es.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Cambiar el modo HVAC de {entity_name}.", + "set_preset_mode": "Cambiar la configuraci\u00f3n prefijada de {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico", + "is_preset_mode": "{entity_name} se establece en un modo predeterminado espec\u00edfico" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} humedad medida cambi\u00f3", + "current_temperature_changed": "{entity_name} temperatura medida cambi\u00f3", + "hvac_mode_changed": "{entity_name} Modo HVAC cambiado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/es.json b/homeassistant/components/coolmaster/.translations/es.json new file mode 100644 index 00000000000000..aedd81baccca80 --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Error al conectarse a la instancia de CoolMasterNet. Por favor revise su anfitri\u00f3n.", + "no_units": "No se ha encontrado ninguna unidad HVAC en el host CoolMasterNet." + }, + "step": { + "user": { + "data": { + "cool": "Soporta el modo de enfriamiento", + "dry": "Soporta el modo seco", + "fan_only": "Soporta modo solo ventilador", + "heat": "Soporta modo calor", + "heat_cool": "Soporta el modo autom\u00e1tico de calor/fr\u00edo", + "host": "Host", + "off": "Se puede apagar" + }, + "title": "Configure los detalles de su conexi\u00f3n a CoolMasterNet." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/es.json b/homeassistant/components/cover/.translations/es.json index d0193b939a5db0..490583b54c4bc0 100644 --- a/homeassistant/components/cover/.translations/es.json +++ b/homeassistant/components/cover/.translations/es.json @@ -4,7 +4,17 @@ "is_closed": "{entity_name} est\u00e1 cerrado", "is_closing": "{entity_name} se est\u00e1 cerrando", "is_open": "{entity_name} est\u00e1 abierto", - "is_opening": "{entity_name} se est\u00e1 abriendo" + "is_opening": "{entity_name} se est\u00e1 abriendo", + "is_position": "La posici\u00f3n actual de {entity_name} es", + "is_tilt_position": "La posici\u00f3n de inclinaci\u00f3n actual de {entity_name} es" + }, + "trigger_type": { + "closed": "{entity_name} cerrado", + "closing": "{entity_name} cerrando", + "opened": "abierto {entity_name}", + "opening": "abriendo {entity_name}", + "position": "Posici\u00f3n cambiada de {entity_name}", + "tilt_position": "Cambia la posici\u00f3n de inclinaci\u00f3n de {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index d4f8de9f282ae7..47fd99c48a25f8 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -55,10 +55,17 @@ "left": "Izquierda", "open": "Abierto", "right": "Derecha", + "side_1": "Lado 1", + "side_2": "Lado 2", + "side_3": "Lado 3", + "side_4": "Lado 4", + "side_5": "Lado 5", + "side_6": "Lado 6", "turn_off": "Apagar", "turn_on": "Encender" }, "trigger_type": { + "remote_awakened": "Dispositivo despertado", "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces consecutivas", "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de un rato pulsado", @@ -69,7 +76,16 @@ "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", - "remote_gyro_activated": "Dispositivo sacudido" + "remote_double_tap": "Dispositivo \" {subtype} \" doble pulsaci\u00f3n", + "remote_falling": "Dispositivo en ca\u00edda libre", + "remote_gyro_activated": "Dispositivo sacudido", + "remote_moved": "Dispositivo movido con \"{subtipo}\" hacia arriba", + "remote_rotate_from_side_1": "Dispositivo girado del \"lado 1\" al \" {subtype} \"", + "remote_rotate_from_side_2": "Dispositivo girado del \"lado 2\" al \" {subtype} \"", + "remote_rotate_from_side_3": "Dispositivo girado del \"lado 3\" al \" {subtype} \"", + "remote_rotate_from_side_4": "Dispositivo girado del \"lado 4\" al \" {subtype} \"", + "remote_rotate_from_side_5": "Dispositivo girado del \"lado 5\" al \" {subtype} \"", + "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \" {subtype} \"" } }, "options": { diff --git a/homeassistant/components/device_tracker/.translations/es.json b/homeassistant/components/device_tracker/.translations/es.json new file mode 100644 index 00000000000000..00bda928b56f44 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/es.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} est\u00e1 en casa", + "is_not_home": "{entity_name} no est\u00e1 en casa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/es.json b/homeassistant/components/fan/.translations/es.json new file mode 100644 index 00000000000000..d92153a63021af --- /dev/null +++ b/homeassistant/components/fan/.translations/es.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Desactivar {entity_name}", + "turn_on": "Activar {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} est\u00e1 desactivado", + "is_on": "{entity_name} est\u00e1 activado" + }, + "trigger_type": { + "turned_off": "{entity_name} desactivado", + "turned_on": "{entity_name} activado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/es.json b/homeassistant/components/huawei_lte/.translations/es.json new file mode 100644 index 00000000000000..92ccf8fc0480b5 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/es.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo ya ha sido configurado", + "already_in_progress": "Este dispositivo ya se est\u00e1 configurando", + "not_huawei_lte": "No es un dispositivo Huawei LTE" + }, + "error": { + "connection_failed": "Fallo de conexi\u00f3n", + "connection_timeout": "Tiempo de espera de la conexi\u00f3n superado", + "incorrect_password": "Contrase\u00f1a incorrecta", + "incorrect_username": "Nombre de usuario incorrecto", + "incorrect_username_or_password": "Nombre de usuario o contrase\u00f1a incorrectos", + "invalid_url": "URL no v\u00e1lida", + "login_attempts_exceeded": "Se han superado los intentos de inicio de sesi\u00f3n m\u00e1ximos, int\u00e9ntelo de nuevo m\u00e1s tarde.", + "response_error": "Error desconocido del dispositivo", + "unknown_connection_error": "Error desconocido al conectarse al dispositivo" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "url": "URL", + "username": "Nombre de usuario" + }, + "description": "Introduzca los detalles de acceso al dispositivo. La especificaci\u00f3n del nombre de usuario y la contrase\u00f1a es opcional, pero permite admitir m\u00e1s funciones de integraci\u00f3n. Por otro lado, el uso de una conexi\u00f3n autorizada puede causar problemas para acceder a la interfaz web del dispositivo desde fuera de Home Assistant mientras la integraci\u00f3n est\u00e1 activa, y viceversa.", + "title": "Configurar Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Destinatarios de notificaciones por SMS", + "track_new_devices": "Rastrea nuevos dispositivos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/es.json b/homeassistant/components/lock/.translations/es.json index c6ef789e9cba65..0c352d9608ab05 100644 --- a/homeassistant/components/lock/.translations/es.json +++ b/homeassistant/components/lock/.translations/es.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} est\u00e1 bloqueado", "is_unlocked": "{entity_name} est\u00e1 desbloqueado" + }, + "trigger_type": { + "locked": "{entity_name} bloqueado", + "unlocked": "{entity_name} desbloqueado" } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/es.json b/homeassistant/components/media_player/.translations/es.json new file mode 100644 index 00000000000000..16242dadeb687c --- /dev/null +++ b/homeassistant/components/media_player/.translations/es.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} est\u00e1 inactivo", + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 activado", + "is_paused": "{entity_name} est\u00e1 en pausa", + "is_playing": "{entity_name} est\u00e1 jugando" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/es.json b/homeassistant/components/sensor/.translations/es.json index a9039d2e410cf7..c5641d38cc0c7d 100644 --- a/homeassistant/components/sensor/.translations/es.json +++ b/homeassistant/components/sensor/.translations/es.json @@ -17,10 +17,10 @@ "illuminance": "{entity_name} iluminancia", "power": "{entity_name} alimentaci\u00f3n", "pressure": "{entity_name} presi\u00f3n", - "signal_strength": "{entity_name} intensidad de la se\u00f1al", - "temperature": "{entity_name} temperatura", - "timestamp": "{entity_name} marca de tiempo", - "value": "{entity_name} valor" + "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name} ", + "temperature": "{entity_name} cambios de temperatura", + "timestamp": "{entity_name} cambios de fecha y hora", + "value": "Cambios de valor de la {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/es.json b/homeassistant/components/solarlog/.translations/es.json new file mode 100644 index 00000000000000..2e6778ffbf380f --- /dev/null +++ b/homeassistant/components/solarlog/.translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "Si no se ha conectado, verifique la direcci\u00f3n del host" + }, + "step": { + "user": { + "data": { + "host": "El nombre del host o la direcci\u00f3n IP de su dispositivo Solar-Log", + "name": "El prefijo que se utilizar\u00e1 para los sensores Solar-Log" + }, + "title": "Defina su conexi\u00f3n Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/es.json b/homeassistant/components/somfy/.translations/es.json index fa4156d886df2e..d20eb71cc556a8 100644 --- a/homeassistant/components/somfy/.translations/es.json +++ b/homeassistant/components/somfy/.translations/es.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Autenticado correctamente con Somfy." }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/es.json b/homeassistant/components/transmission/.translations/es.json index f210eb42f8a613..06ea19e72b8c52 100644 --- a/homeassistant/components/transmission/.translations/es.json +++ b/homeassistant/components/transmission/.translations/es.json @@ -1,10 +1,12 @@ { "config": { "abort": { + "already_configured": "El host ya est\u00e1 configurado.", "one_instance_allowed": "S\u00f3lo se necesita una sola instancia." }, "error": { "cannot_connect": "No se puede conectar al host", + "name_exists": "El nombre ya existe", "wrong_credentials": "Nombre de usuario o contrase\u00f1a incorrectos" }, "step": { @@ -33,7 +35,8 @@ "data": { "scan_interval": "Frecuencia de actualizaci\u00f3n" }, - "description": "Configurar opciones para la transmisi\u00f3n" + "description": "Configurar opciones para la transmisi\u00f3n", + "title": "Configurar opciones para la transmisi\u00f3n" } } } diff --git a/homeassistant/components/vacuum/.translations/es.json b/homeassistant/components/vacuum/.translations/es.json new file mode 100644 index 00000000000000..9ecf3ade99c891 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/es.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Deje que {entity_name} limpie", + "dock": "Deje que {entity_name} regrese a la base" + }, + "condtion_type": { + "is_cleaning": "{entity_name} est\u00e1 limpiando", + "is_docked": "{entity_name} est\u00e1 acoplado" + }, + "trigger_type": { + "cleaning": "{entity_name} empez\u00f3 a limpiar", + "docked": "{entity_name} en la base" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/es.json b/homeassistant/components/withings/.translations/es.json index ee0cf5235884c2..c1e969c7f51c64 100644 --- a/homeassistant/components/withings/.translations/es.json +++ b/homeassistant/components/withings/.translations/es.json @@ -7,6 +7,13 @@ "default": "Autenticado correctamente con Withings para el perfil seleccionado." }, "step": { + "profile": { + "data": { + "profile": "Perfil" + }, + "description": "\u00bfQu\u00e9 perfil seleccion\u00f3 en el sitio web de Withings? Es importante que los perfiles coincidan, de lo contrario los datos se etiquetar\u00e1n incorrectamente.", + "title": "Perfil de usuario." + }, "user": { "data": { "profile": "Perfil" diff --git a/homeassistant/components/wled/.translations/es.json b/homeassistant/components/wled/.translations/es.json new file mode 100644 index 00000000000000..7dd388d41afc85 --- /dev/null +++ b/homeassistant/components/wled/.translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo WLED ya est\u00e1 configurado.", + "connection_error": "No se ha podido conectar al dispositivo WLED." + }, + "error": { + "connection_error": "No se ha podido conectar al dispositivo WLED." + }, + "flow_title": "WLED: {nombre}", + "step": { + "user": { + "data": { + "host": "Host o direcci\u00f3n IP" + }, + "description": "Configure su WLED para integrarse con Home Assistant.", + "title": "Vincula tu WLED" + }, + "zeroconf_confirm": { + "description": "\u00bfQuieres a\u00f1adir el WLED llamado `{name}` a Home Assistant?", + "title": "Descubierto dispositivo WLED" + } + }, + "title": "WLED" + } +} \ No newline at end of file From 70e8c582541fa91b6845325be6656f999579cf7b Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 13 Nov 2019 07:42:12 +0100 Subject: [PATCH 1545/3953] version bump pypoint (#28737) --- homeassistant/components/point/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 0f2faef86dfaed..4c29f37e67cf72 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/point", "requirements": [ - "pypoint==1.1.1" + "pypoint==1.1.2" ], "dependencies": [ "webhook" diff --git a/requirements_all.txt b/requirements_all.txt index 163918f5180227..97e34b09fa3ed3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1411,7 +1411,7 @@ pypck==0.6.3 pypjlink2==1.2.0 # homeassistant.components.point -pypoint==1.1.1 +pypoint==1.1.2 # homeassistant.components.ps4 pyps4-2ndscreen==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90668e3eaabdd6..1dcb0323c79e26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -468,7 +468,7 @@ pyotgw==0.5b0 pyotp==2.3.0 # homeassistant.components.point -pypoint==1.1.1 +pypoint==1.1.2 # homeassistant.components.ps4 pyps4-2ndscreen==1.0.1 From d7f45a47f5d53393f8c5a5668f3194f14a60a39d Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Wed, 13 Nov 2019 07:43:38 +0100 Subject: [PATCH 1546/3953] Upgrade async_upnp_client==0.14.12 (#28733) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 008a1293e41c95..ac09b1afb4830a 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "Dlna dmr", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "requirements": [ - "async-upnp-client==0.14.11" + "async-upnp-client==0.14.12" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index d4446b271f9a18..a78fec3610e2dc 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": [ - "async-upnp-client==0.14.11" + "async-upnp-client==0.14.12" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 97e34b09fa3ed3..5b4f0358b0a47f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.upnp -async-upnp-client==0.14.11 +async-upnp-client==0.14.12 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1dcb0323c79e26..f891394db6ced7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ arcam-fmj==0.4.3 # homeassistant.components.dlna_dmr # homeassistant.components.upnp -async-upnp-client==0.14.11 +async-upnp-client==0.14.12 # homeassistant.components.stream av==6.1.2 From 4bcc669d19521d061f0f2db28a366f0a0065ac83 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 13 Nov 2019 07:42:46 +0000 Subject: [PATCH 1547/3953] Add device classes to weather sensors. (#28512) --- homeassistant/components/yr/sensor.py | 43 ++++++++++++++++++--------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index f562f519ab57f1..fc754c9a2579eb 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -19,6 +19,11 @@ CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION, CONF_NAME, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PRESSURE_HPA, + TEMP_CELSIUS, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity @@ -34,20 +39,24 @@ # https://api.met.no/license_data.html SENSOR_TYPES = { - "symbol": ["Symbol", None], - "precipitation": ["Precipitation", "mm"], - "temperature": ["Temperature", "°C"], - "windSpeed": ["Wind speed", "m/s"], - "windGust": ["Wind gust", "m/s"], - "pressure": ["Pressure", "hPa"], - "windDirection": ["Wind direction", "°"], - "humidity": ["Humidity", "%"], - "fog": ["Fog", "%"], - "cloudiness": ["Cloudiness", "%"], - "lowClouds": ["Low clouds", "%"], - "mediumClouds": ["Medium clouds", "%"], - "highClouds": ["High clouds", "%"], - "dewpointTemperature": ["Dewpoint temperature", "°C"], + "symbol": ["Symbol", None, None], + "precipitation": ["Precipitation", "mm", None], + "temperature": ["Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE], + "windSpeed": ["Wind speed", "m/s", None], + "windGust": ["Wind gust", "m/s", None], + "pressure": ["Pressure", PRESSURE_HPA, DEVICE_CLASS_PRESSURE], + "windDirection": ["Wind direction", "°", None], + "humidity": ["Humidity", "%", DEVICE_CLASS_HUMIDITY], + "fog": ["Fog", "%", None], + "cloudiness": ["Cloudiness", "%", None], + "lowClouds": ["Low clouds", "%", None], + "mediumClouds": ["Medium clouds", "%", None], + "highClouds": ["High clouds", "%", None], + "dewpointTemperature": [ + "Dewpoint temperature", + TEMP_CELSIUS, + DEVICE_CLASS_TEMPERATURE, + ], } CONF_FORECAST = "forecast" @@ -103,6 +112,7 @@ def __init__(self, name, sensor_type): self.type = sensor_type self._state = None self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self._device_class = SENSOR_TYPES[self.type][2] @property def name(self): @@ -139,6 +149,11 @@ def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + @property + def device_class(self): + """Return the device class of this entity, if any.""" + return self._device_class + class YrData: """Get the latest data and updates the states.""" From b4cec23adde3d7f90f4670f3bcae1176d2949fac Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 13 Nov 2019 08:43:14 +0100 Subject: [PATCH 1548/3953] Upgrade psutil to 5.6.5 (#28717) --- homeassistant/components/systemmonitor/manifest.json | 2 +- homeassistant/components/systemmonitor/sensor.py | 5 ++--- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index a45a59e410a087..67677bc2572629 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Systemmonitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "requirements": [ - "psutil==5.6.3" + "psutil==5.6.5" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 53c5c104cd15bf..b1a337360835cd 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -7,12 +7,11 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_RESOURCES, STATE_OFF, STATE_ON, CONF_TYPE -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_RESOURCES, CONF_TYPE, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5b4f0358b0a47f..68ac061815d51c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1020,7 +1020,7 @@ prometheus_client==0.7.1 protobuf==3.6.1 # homeassistant.components.systemmonitor -psutil==5.6.3 +psutil==5.6.5 # homeassistant.components.ptvsd ptvsd==4.2.8 From a48d426f182ec859f2ea9f105e921cfa5528193b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 13 Nov 2019 09:50:35 +0200 Subject: [PATCH 1549/3953] Travis and tox config improvements (#28667) * Use travis_wait only with pylint pylint is the only job that is expected to be silent for extended time. For others such a silence is a sign of a problem and using travis_wait just lengthens the wait, and makes things harder to follow and debug, because it also suppresses output in the web UI. * Use pytest-xdist in tox Similarly as in Azure. --- .travis.yml | 4 ++-- tox.ini | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6d5b43c2f0350d..c9638b02a2f6fd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ matrix: - python: "3.6.1" env: TOXENV=lint - python: "3.6.1" - env: TOXENV=pylint PYLINT_ARGS=--jobs=0 + env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30 - python: "3.6.1" env: TOXENV=typing - python: "3.6.1" @@ -33,4 +33,4 @@ cache: - $HOME/.cache/pre-commit install: pip install -U tox language: python -script: travis_wait 50 tox --develop +script: ${TRAVIS_WAIT:+travis_wait $TRAVIS_WAIT} tox --develop diff --git a/tox.ini b/tox.ini index 2898c410b2ad0f..dc2a9f79b90de1 100644 --- a/tox.ini +++ b/tox.ini @@ -5,19 +5,21 @@ skip_missing_interpreters = True [testenv] basepython = {env:PYTHON3_PATH:python3} commands = - pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar {posargs} + pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs} {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt + pytest-xdist [testenv:cov] commands = - pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar --cov --cov-report= {posargs} + pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar --cov --cov-report= {posargs} {toxinidir}/script/check_dirty deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt + pytest-xdist [testenv:pylint] ignore_errors = True From fe942c40a0b4806525da9749d3fd04e34b188f57 Mon Sep 17 00:00:00 2001 From: Thomas Date: Thu, 14 Nov 2019 00:11:09 +1300 Subject: [PATCH 1550/3953] Correct openalpr_local config option name (#28746) Previously was "alp_bin" now "alpr_bin" like it is outlined in the documentation (https://www.home-assistant.io/integrations/openalpr_local/) --- homeassistant/components/openalpr_local/image_processing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index 786f5ca69daf6e..32a08b5316514e 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -47,7 +47,7 @@ "vn2", ] -CONF_ALPR_BIN = "alp_bin" +CONF_ALPR_BIN = "alpr_bin" DEFAULT_BINARY = "alpr" From 349e7b8cd178dac090e6f5a9948afef5137bcb6b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Nov 2019 13:55:02 +0100 Subject: [PATCH 1551/3953] Bumped version to 0.102.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 449e7a90087b83..fa7f753495cdfe 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -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, 1) From ec45e72bea9c72d461737502cd0dcba4a934f63f Mon Sep 17 00:00:00 2001 From: Thomas Germain <12560542+thomasgermain@users.noreply.github.com> Date: Wed, 13 Nov 2019 14:12:36 +0100 Subject: [PATCH 1552/3953] Add verisure ethernet status (#28656) --- .../components/verisure/binary_sensor.py | 36 +++++++++- .../verisure/test_ethernet_status.py | 67 +++++++++++++++++++ tests/components/verisure/test_lock.py | 10 ++- 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 tests/components/verisure/test_ethernet_status.py diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index b7c8cbb49a3e5b..47ec3c536b33ae 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Verisure binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_CONNECTIVITY, +) from . import CONF_DOOR_WINDOW, HUB as hub @@ -22,6 +25,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) ] ) + + sensors.extend([VerisureEthernetStatus()]) add_entities(sensors) @@ -66,3 +71,32 @@ def available(self): def update(self): """Update the state of the sensor.""" hub.update_overview() + + +class VerisureEthernetStatus(BinarySensorDevice): + """Representation of a Verisure VBOX internet status.""" + + @property + def name(self): + """Return the name of the binary sensor.""" + return "Verisure Ethernet status" + + @property + def is_on(self): + """Return the state of the sensor.""" + return hub.get_first("$.ethernetConnectedNow") + + @property + def available(self): + """Return True if entity is available.""" + return hub.get_first("$.ethernetConnectedNow") is not None + + # pylint: disable=no-self-use + def update(self): + """Update the state of the sensor.""" + hub.update_overview() + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_CONNECTIVITY diff --git a/tests/components/verisure/test_ethernet_status.py b/tests/components/verisure/test_ethernet_status.py new file mode 100644 index 00000000000000..71c7df94ae5431 --- /dev/null +++ b/tests/components/verisure/test_ethernet_status.py @@ -0,0 +1,67 @@ +"""Test Verisure ethernet status.""" +from contextlib import contextmanager +from unittest.mock import patch + +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.setup import async_setup_component +from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN + +CONFIG = { + "verisure": { + "username": "test", + "password": "test", + "alarm": False, + "door_window": False, + "hygrometers": False, + "mouse": False, + "smartplugs": False, + "thermometers": False, + "smartcam": False, + } +} + + +@contextmanager +def mock_hub(config, response): + """Extensively mock out a verisure hub.""" + hub_prefix = "homeassistant.components.verisure.binary_sensor.hub" + verisure_prefix = "verisure.Session" + with patch(verisure_prefix) as session, patch(hub_prefix) as hub: + session.login.return_value = True + + hub.config = config["verisure"] + hub.get.return_value = response + hub.get_first.return_value = response.get("ethernetConnectedNow", None) + + yield hub + + +async def setup_verisure(hass, config, response): + """Set up mock verisure.""" + with mock_hub(config, response): + await async_setup_component(hass, VERISURE_DOMAIN, config) + await hass.async_block_till_done() + + +async def test_verisure_no_ethernet_status(hass): + """Test no data from API.""" + await setup_verisure(hass, CONFIG, {}) + assert len(hass.states.async_all()) == 1 + entity_id = hass.states.async_entity_ids()[0] + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_verisure_ethernet_status_disconnected(hass): + """Test disconnected.""" + await setup_verisure(hass, CONFIG, {"ethernetConnectedNow": False}) + assert len(hass.states.async_all()) == 1 + entity_id = hass.states.async_entity_ids()[0] + assert hass.states.get(entity_id).state == "off" + + +async def test_verisure_ethernet_status_connected(hass): + """Test connected.""" + await setup_verisure(hass, CONFIG, {"ethernetConnectedNow": True}) + assert len(hass.states.async_all()) == 1 + entity_id = hass.states.async_entity_ids()[0] + assert hass.states.get(entity_id).state == "on" diff --git a/tests/components/verisure/test_lock.py b/tests/components/verisure/test_lock.py index db277285819cdf..ac03e0d9fb61ce 100644 --- a/tests/components/verisure/test_lock.py +++ b/tests/components/verisure/test_lock.py @@ -50,6 +50,9 @@ def mock_hub(config, get_response=LOCKS[0]): """Extensively mock out a verisure hub.""" hub_prefix = "homeassistant.components.verisure.lock.hub" + # Since there is no conf to disable ethernet status, mock hub for + # binary sensor too + hub_binary_sensor = "homeassistant.components.verisure.binary_sensor.hub" verisure_prefix = "verisure.Session" with patch(verisure_prefix) as session, patch(hub_prefix) as hub: session.login.return_value = True @@ -62,7 +65,8 @@ def mock_hub(config, get_response=LOCKS[0]): } hub.session.get_lock_state_transaction.return_value = {"result": "OK"} - yield hub + with patch(hub_binary_sensor, hub): + yield hub async def setup_verisure_locks(hass, config): @@ -70,8 +74,8 @@ async def setup_verisure_locks(hass, config): with mock_hub(config): await async_setup_component(hass, VERISURE_DOMAIN, config) await hass.async_block_till_done() - # lock.door_lock, group.all_locks - assert len(hass.states.async_all()) == 2 + # lock.door_lock, group.all_locks, ethernet_status + assert len(hass.states.async_all()) == 3 async def test_verisure_no_default_code(hass): From 376e3def1d4fedc75bcfcf0e4230e528a28d9533 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 13 Nov 2019 09:25:45 -0500 Subject: [PATCH 1553/3953] Bump ZHA quirks to 0.0.28 (#28750) --- 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 18e8af7008d154..8781625d326522 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.11.0", - "zha-quirks==0.0.27", + "zha-quirks==0.0.28", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.11.0", "zigpy-xbee-homeassistant==0.7.0", diff --git a/requirements_all.txt b/requirements_all.txt index 68ac061815d51c..9043f944d4202c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2054,7 +2054,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.27 +zha-quirks==0.0.28 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f891394db6ced7..bd57a5490a3de3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -637,7 +637,7 @@ yahooweather==0.10 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.27 +zha-quirks==0.0.28 # homeassistant.components.zha zigpy-deconz==0.7.0 From 15ce73835781f1f3ede80e537afa69895b4f045e Mon Sep 17 00:00:00 2001 From: mvn23 Date: Wed, 13 Nov 2019 15:32:22 +0100 Subject: [PATCH 1554/3953] Bump pyotgw to 0.5b1 (#28751) --- homeassistant/components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index a632096cd75e9a..990df0ba4e3c1a 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -3,7 +3,7 @@ "name": "Opentherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": [ - "pyotgw==0.5b0" + "pyotgw==0.5b1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9043f944d4202c..d9f830fb5ee3aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1388,7 +1388,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.5b0 +pyotgw==0.5b1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd57a5490a3de3..221995b0ac6834 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -460,7 +460,7 @@ pynx584==0.4 pyopenuv==1.0.9 # homeassistant.components.opentherm_gw -pyotgw==0.5b0 +pyotgw==0.5b1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From 15e6278a2e40b839142a702d40751c1fd651ecad Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 13 Nov 2019 16:37:31 +0100 Subject: [PATCH 1555/3953] Add config entry and device support to Demo (#28702) * Add config entry and device support to Demo * Some more devices * Fix tests using demo * Review comments * Update config_flow.py * Revert * Disable pylint --- homeassistant/components/demo/__init__.py | 41 +++++++++++++----- homeassistant/components/demo/air_quality.py | 9 +++- .../components/demo/alarm_control_panel.py | 5 +++ .../components/demo/binary_sensor.py | 33 ++++++++++++--- homeassistant/components/demo/camera.py | 5 +++ homeassistant/components/demo/climate.py | 33 ++++++++++++++- homeassistant/components/demo/config_flow.py | 16 +++++++ homeassistant/components/demo/cover.py | 35 +++++++++++++--- homeassistant/components/demo/fan.py | 9 +++- homeassistant/components/demo/light.py | 32 +++++++++++--- homeassistant/components/demo/lock.py | 9 +++- homeassistant/components/demo/media_player.py | 9 +++- homeassistant/components/demo/remote.py | 5 +++ homeassistant/components/demo/sensor.py | 42 ++++++++++++++++--- homeassistant/components/demo/strings.json | 5 +++ homeassistant/components/demo/switch.py | 33 ++++++++++++--- homeassistant/components/demo/vacuum.py | 5 +++ homeassistant/components/demo/water_heater.py | 9 +++- homeassistant/components/demo/weather.py | 5 +++ .../google_assistant/test_smart_home.py | 7 +++- tests/components/prometheus/test_init.py | 10 +++-- 21 files changed, 304 insertions(+), 53 deletions(-) create mode 100644 homeassistant/components/demo/config_flow.py create mode 100644 homeassistant/components/demo/strings.json diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 93a34794366ddc..05febfad603335 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -3,33 +3,37 @@ import logging import time -from homeassistant import bootstrap +from homeassistant import bootstrap, config_entries import homeassistant.core as ha from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START DOMAIN = "demo" _LOGGER = logging.getLogger(__name__) -COMPONENTS_WITH_DEMO_PLATFORM = [ + +COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ "air_quality", "alarm_control_panel", "binary_sensor", - "calendar", "camera", "climate", "cover", - "device_tracker", "fan", - "image_processing", "light", "lock", "media_player", - "notify", "sensor", - "stt", "switch", + "water_heater", +] + +COMPONENTS_WITH_DEMO_PLATFORM = [ "tts", + "stt", "mailbox", - "water_heater", + "notify", + "image_processing", + "calendar", + "device_tracker", ] @@ -38,8 +42,12 @@ async def async_setup(hass, config): if DOMAIN not in config: return True - config.setdefault(ha.DOMAIN, {}) - config.setdefault(DOMAIN, {}) + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} + ) + ) # Set up demo platforms for component in COMPONENTS_WITH_DEMO_PLATFORM: @@ -47,6 +55,9 @@ async def async_setup(hass, config): hass.helpers.discovery.async_load_platform(component, DOMAIN, {}, config) ) + config.setdefault(ha.DOMAIN, {}) + config.setdefault(DOMAIN, {}) + # Set up sun if not hass.config.latitude: hass.config.latitude = 32.87336 @@ -176,6 +187,16 @@ async def demo_start_listener(_event): return True +async def async_setup_entry(hass, config_entry): + """Set the config entry up.""" + # Set up demo platforms with config entry + for component in COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + return True + + async def finish_setup(hass, config): """Finish set up once demo platforms are set up.""" switches = None diff --git a/homeassistant/components/demo/air_quality.py b/homeassistant/components/demo/air_quality.py index 0b41d9c87e9a74..9fe0f675d9d033 100644 --- a/homeassistant/components/demo/air_quality.py +++ b/homeassistant/components/demo/air_quality.py @@ -2,13 +2,18 @@ from homeassistant.components.air_quality import AirQualityEntity -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Air Quality.""" - add_entities( + async_add_entities( [DemoAirQuality("Home", 14, 23, 100), DemoAirQuality("Office", 4, 16, None)] ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoAirQuality(AirQualityEntity): """Representation of Air Quality data.""" diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index 378ba3b18dd689..d82edadf161497 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -57,3 +57,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) ] ) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index 96c2b66fa5be9c..c1e42807f6d391 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -1,26 +1,49 @@ """Demo platform that has two fake binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice +from . import DOMAIN -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo binary sensor platform.""" - add_entities( + async_add_entities( [ - DemoBinarySensor("Basement Floor Wet", False, "moisture"), - DemoBinarySensor("Movement Backyard", True, "motion"), + DemoBinarySensor("binary_1", "Basement Floor Wet", False, "moisture"), + DemoBinarySensor("binary_2", "Movement Backyard", True, "motion"), ] ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoBinarySensor(BinarySensorDevice): """representation of a Demo binary sensor.""" - def __init__(self, name, state, device_class): + def __init__(self, unique_id, name, state, device_class): """Initialize the demo sensor.""" + self._unique_id = unique_id self._name = name self._state = state self._sensor_type = device_class + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def device_class(self): """Return the class of this sensor.""" diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 0cd77b6112eca8..f639dae9757c75 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -12,6 +12,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([DemoCamera("Demo camera")]) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoCamera(Camera): """The representation of a Demo camera.""" diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index f5117b7986cc49..f4affed7ced10c 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -1,4 +1,5 @@ """Demo platform that offers a fake climate device.""" +import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, @@ -20,15 +21,18 @@ HVAC_MODE_AUTO, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from . import DOMAIN SUPPORT_FLAGS = 0 +_LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo climate devices.""" - add_entities( + async_add_entities( [ DemoClimate( + unique_id="climate_1", name="HeatPump", target_temperature=68, unit_of_measurement=TEMP_FAHRENHEIT, @@ -46,6 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hvac_modes=[HVAC_MODE_HEAT, HVAC_MODE_OFF], ), DemoClimate( + unique_id="climate_2", name="Hvac", target_temperature=21, unit_of_measurement=TEMP_CELSIUS, @@ -63,6 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hvac_modes=[mode for mode in HVAC_MODES if mode != HVAC_MODE_HEAT_COOL], ), DemoClimate( + unique_id="climate_3", name="Ecobee", target_temperature=None, unit_of_measurement=TEMP_CELSIUS, @@ -84,11 +90,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo climate devices config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoClimate(ClimateDevice): """Representation of a demo climate device.""" def __init__( self, + unique_id, name, target_temperature, unit_of_measurement, @@ -107,6 +119,7 @@ def __init__( preset_modes=None, ): """Initialize the climate device.""" + self._unique_id = unique_id self._name = name self._support_flags = SUPPORT_FLAGS if target_temperature is not None: @@ -143,6 +156,22 @@ def __init__( self._target_temperature_high = target_temp_high self._target_temperature_low = target_temp_low + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py new file mode 100644 index 00000000000000..e6b275920c8c16 --- /dev/null +++ b/homeassistant/components/demo/config_flow.py @@ -0,0 +1,16 @@ +"""Config flow to configure demo component.""" + +from homeassistant import config_entries + +# pylint: disable=unused-import +from . import DOMAIN + + +class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Demo configuration flow.""" + + VERSION = 1 + + async def async_step_import(self, import_info): + """Set the config entry up from yaml.""" + return self.async_create_entry(title="Demo", data={}) diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 180312eefa39f5..20a8747aaa5b2d 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -8,17 +8,19 @@ SUPPORT_OPEN, CoverDevice, ) +from . import DOMAIN -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo covers.""" - add_entities( + async_add_entities( [ - DemoCover(hass, "Kitchen Window"), - DemoCover(hass, "Hall Window", 10), - DemoCover(hass, "Living Room Window", 70, 50), + DemoCover(hass, "cover_1", "Kitchen Window"), + DemoCover(hass, "cover_2", "Hall Window", 10), + DemoCover(hass, "cover_3", "Living Room Window", 70, 50), DemoCover( hass, + "cover_4", "Garage Door", device_class="garage", supported_features=(SUPPORT_OPEN | SUPPORT_CLOSE), @@ -27,12 +29,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoCover(CoverDevice): """Representation of a demo cover.""" def __init__( self, hass, + unique_id, name, position=None, tilt_position=None, @@ -41,6 +49,7 @@ def __init__( ): """Initialize the cover.""" self.hass = hass + self._unique_id = unique_id self._name = name self._position = position self._device_class = device_class @@ -59,6 +68,22 @@ def __init__( else: self._closed = self.current_cover_position <= 0 + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + + @property + def unique_id(self): + """Return unique ID for cover.""" + return self._unique_id + @property def name(self): """Return the name of the cover.""" diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index ab8a6f3fae9dc6..500d5f6a5ce5dd 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -15,9 +15,9 @@ LIMITED_SUPPORT = SUPPORT_SET_SPEED -def setup_platform(hass, config, add_entities_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the demo fan platform.""" - add_entities_callback( + async_add_entities( [ DemoFan(hass, "Living Room Fan", FULL_SUPPORT), DemoFan(hass, "Ceiling Fan", LIMITED_SUPPORT), @@ -25,6 +25,11 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoFan(FanEntity): """A demonstration fan component.""" diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index f8b1b5115115ce..a2c06b72986e81 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -15,6 +15,8 @@ Light, ) +from . import DOMAIN + LIGHT_COLORS = [(56, 86), (345, 75)] LIGHT_EFFECT_LIST = ["rainbow", "none"] @@ -30,24 +32,33 @@ ) -def setup_platform(hass, config, add_entities_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the demo light platform.""" - add_entities_callback( + async_add_entities( [ DemoLight( - 1, + "light_1", "Bed Light", False, True, effect_list=LIGHT_EFFECT_LIST, effect=LIGHT_EFFECT_LIST[0], ), - DemoLight(2, "Ceiling Lights", True, True, LIGHT_COLORS[0], LIGHT_TEMPS[1]), - DemoLight(3, "Kitchen Lights", True, True, LIGHT_COLORS[1], LIGHT_TEMPS[0]), + DemoLight( + "light_2", "Ceiling Lights", True, True, LIGHT_COLORS[0], LIGHT_TEMPS[1] + ), + DemoLight( + "light_3", "Kitchen Lights", True, True, LIGHT_COLORS[1], LIGHT_TEMPS[0] + ), ] ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoLight(Light): """Representation of a demo light.""" @@ -76,6 +87,17 @@ def __init__( self._effect = effect self._available = True + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + @property def should_poll(self) -> bool: """No polling needed for a demo light.""" diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index d8fb244b9bcbfd..923469f045c400 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -4,9 +4,9 @@ from homeassistant.components.lock import SUPPORT_OPEN, LockDevice -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo lock platform.""" - add_entities( + async_add_entities( [ DemoLock("Front Door", STATE_LOCKED), DemoLock("Kitchen Door", STATE_UNLOCKED), @@ -15,6 +15,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoLock(LockDevice): """Representation of a Demo lock.""" diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index fb64f8015c0f4b..9d7c3892af8574 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -24,9 +24,9 @@ import homeassistant.util.dt as dt_util -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the media player demo platform.""" - add_entities( + async_add_entities( [ DemoYoutubePlayer( "Living Room", @@ -43,6 +43,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + YOUTUBE_COVER_URL_FORMAT = "https://img.youtube.com/vi/{}/hqdefault.jpg" SOUND_MODE_LIST = ["Dummy Music", "Dummy Movie"] DEFAULT_SOUND_MODE = "Dummy Music" diff --git a/homeassistant/components/demo/remote.py b/homeassistant/components/demo/remote.py index 4a363781e2efb4..70e0d3c8b6e13b 100644 --- a/homeassistant/components/demo/remote.py +++ b/homeassistant/components/demo/remote.py @@ -3,6 +3,11 @@ from homeassistant.const import DEVICE_DEFAULT_NAME +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + setup_platform(hass, {}, async_add_entities) + + def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the demo remotes.""" add_entities_callback( diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index 78bd88b42cf5a6..bf5df94e74c355 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -6,31 +6,63 @@ DEVICE_CLASS_TEMPERATURE, ) from homeassistant.helpers.entity import Entity +from . import DOMAIN -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo sensors.""" - add_entities( + async_add_entities( [ DemoSensor( - "Outside Temperature", 15.6, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 12 + "sensor_1", + "Outside Temperature", + 15.6, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, + 12, + ), + DemoSensor( + "sensor_2", "Outside Humidity", 54, DEVICE_CLASS_HUMIDITY, "%", None ), - DemoSensor("Outside Humidity", 54, DEVICE_CLASS_HUMIDITY, "%", None), ] ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoSensor(Entity): """Representation of a Demo sensor.""" - def __init__(self, name, state, device_class, unit_of_measurement, battery): + def __init__( + self, unique_id, name, state, device_class, unit_of_measurement, battery + ): """Initialize the sensor.""" + self._unique_id = unique_id self._name = name self._state = state self._device_class = device_class self._unit_of_measurement = unit_of_measurement self._battery = battery + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def should_poll(self): """No polling needed for a demo sensor.""" diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json new file mode 100644 index 00000000000000..a2c0103280bee1 --- /dev/null +++ b/homeassistant/components/demo/strings.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 65860867ed63a6..23006cff875a4d 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,29 +1,52 @@ """Demo platform that has two fake switches.""" from homeassistant.components.switch import SwitchDevice from homeassistant.const import DEVICE_DEFAULT_NAME +from . import DOMAIN -def setup_platform(hass, config, add_entities_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the demo switches.""" - add_entities_callback( + async_add_entities( [ - DemoSwitch("Decorative Lights", True, None, True), - DemoSwitch("AC", False, "mdi:air-conditioner", False), + DemoSwitch("swith1", "Decorative Lights", True, None, True), + DemoSwitch("swith2", "AC", False, "mdi:air-conditioner", False), ] ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoSwitch(SwitchDevice): """Representation of a demo switch.""" - def __init__(self, name, state, icon, assumed, device_class=None): + def __init__(self, unique_id, name, state, icon, assumed, device_class=None): """Initialize the Demo switch.""" + self._unique_id = unique_id self._name = name or DEVICE_DEFAULT_NAME self._state = state self._icon = icon self._assumed = assumed self._device_class = device_class + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": { + # Serial numbers are unique identifiers within a specific domain + (DOMAIN, self.unique_id) + }, + "name": self.name, + } + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def should_poll(self): """No polling needed for a demo switch.""" diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index 2ba704d39252bc..fb64f17a4521c0 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -76,6 +76,11 @@ DEMO_VACUUM_STATE = "5_Fifth_floor" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + setup_platform(hass, {}, async_add_entities) + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo vacuums.""" add_entities( diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index d1d53e058c66fc..c3fff26c9922ac 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -13,9 +13,9 @@ ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Demo water_heater devices.""" - add_entities( + async_add_entities( [ DemoWaterHeater("Demo Water Heater", 119, TEMP_FAHRENHEIT, False, "eco"), DemoWaterHeater("Demo Water Heater Celsius", 45, TEMP_CELSIUS, True, "eco"), @@ -23,6 +23,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + class DemoWaterHeater(WaterHeaterDevice): """Representation of a demo water_heater device.""" diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index 2253f261ad2cef..8cc1b2f95fdfc4 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -30,6 +30,11 @@ } +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Demo config entry.""" + setup_platform(hass, {}, async_add_entities) + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Demo weather.""" add_entities( diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 3c3801b35c699f..250d611b60289c 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -498,6 +498,7 @@ async def test_unavailable_state_doesnt_sync(hass): async def test_device_class_switch(hass, device_class, google_type): """Test that a cover entity syncs to the correct device type.""" sensor = DemoSwitch( + None, "Demo Sensor", state=False, icon="mdi:switch", @@ -545,7 +546,9 @@ async def test_device_class_switch(hass, device_class, google_type): ) async def test_device_class_binary_sensor(hass, device_class, google_type): """Test that a binary entity syncs to the correct device type.""" - sensor = DemoBinarySensor("Demo Sensor", state=False, device_class=device_class) + sensor = DemoBinarySensor( + None, "Demo Sensor", state=False, device_class=device_class + ) sensor.hass = hass sensor.entity_id = "binary_sensor.demo_sensor" await sensor.async_update_ha_state() @@ -585,7 +588,7 @@ async def test_device_class_binary_sensor(hass, device_class, google_type): ) async def test_device_class_cover(hass, device_class, google_type): """Test that a binary entity syncs to the correct device type.""" - sensor = DemoCover(hass, "Demo Sensor", device_class=device_class) + sensor = DemoCover(None, hass, "Demo Sensor", device_class=device_class) sensor.hass = hass sensor.entity_id = "cover.demo_sensor" await sensor.async_update_ha_state() diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index 4ec40731c5d206..cf1ff7489f6de8 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -24,24 +24,26 @@ async def prometheus_client(loop, hass, hass_client): hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} ) - sensor1 = DemoSensor("Television Energy", 74, None, ENERGY_KILO_WATT_HOUR, None) + sensor1 = DemoSensor( + None, "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 + None, "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 = DemoSensor(None, "Electricity price", 0.123, None, "SEK/kWh", None) sensor3.hass = hass sensor3.entity_id = "sensor.electricity_price" await sensor3.async_update_ha_state() - sensor4 = DemoSensor("Wind Direction", 25, None, "°", None) + sensor4 = DemoSensor(None, "Wind Direction", 25, None, "°", None) sensor4.hass = hass sensor4.entity_id = "sensor.wind_direction" await sensor4.async_update_ha_state() From 8789da36be901a597c5069fcfbbe01cde8453219 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 13 Nov 2019 12:48:08 -0700 Subject: [PATCH 1556/3953] Add support for SimpliSafe locks (#28672) * Start * Lock init * More stubbing * Final pieces for PR * Fixed incorrect property access * Updaed .coveragerc * Ensure we can handle unknown states * Account for lock's offline property * Account for device online * Unload components concurrently * Handle unknown states more gracefully --- .coveragerc | 1 + .../components/simplisafe/__init__.py | 14 ++-- homeassistant/components/simplisafe/lock.py | 72 +++++++++++++++++++ .../components/simplisafe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/simplisafe/lock.py diff --git a/.coveragerc b/.coveragerc index b0d6a40c7f7fb3..4649f175606f4a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -605,6 +605,7 @@ omit = homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py homeassistant/components/simplisafe/alarm_control_panel.py + homeassistant/components/simplisafe/lock.py homeassistant/components/simulated/sensor.py homeassistant/components/sisyphus/* homeassistant/components/sky_hub/device_tracker.py diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 2514c5e9ed9b2d..d57e7b83fa43d1 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -154,11 +154,10 @@ async def async_setup_entry(hass, config_entry): await simplisafe.async_update() hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( - config_entry, "alarm_control_panel" + for component in ("alarm_control_panel", "lock"): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) ) - ) async def refresh(event_time): """Refresh data from the SimpliSafe account.""" @@ -199,7 +198,12 @@ async def set_pin(call): async def async_unload_entry(hass, entry): """Unload a SimpliSafe config entry.""" - await hass.config_entries.async_forward_entry_unload(entry, "alarm_control_panel") + tasks = [ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in ("alarm_control_panel", "lock") + ] + + await asyncio.gather(*tasks) hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) diff --git a/homeassistant/components/simplisafe/lock.py b/homeassistant/components/simplisafe/lock.py new file mode 100644 index 00000000000000..10c5d310e73eee --- /dev/null +++ b/homeassistant/components/simplisafe/lock.py @@ -0,0 +1,72 @@ +"""Support for SimpliSafe locks.""" +import logging + +from simplipy.lock import LockStates + +from homeassistant.components.lock import LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED + +from . import SimpliSafeEntity +from .const import DATA_CLIENT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +ATTR_LOCK_LOW_BATTERY = "lock_low_battery" +ATTR_JAMMED = "jammed" +ATTR_PIN_PAD_LOW_BATTERY = "pin_pad_low_battery" + +STATE_MAP = { + LockStates.locked: STATE_LOCKED, + LockStates.unknown: STATE_UNKNOWN, + LockStates.unlocked: STATE_UNLOCKED, +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up SimpliSafe locks based on a config entry.""" + simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] + async_add_entities( + [ + SimpliSafeLock(system, lock) + for system in simplisafe.systems.values() + for lock in system.locks.values() + ] + ) + + +class SimpliSafeLock(SimpliSafeEntity, LockDevice): + """Define a SimpliSafe lock.""" + + def __init__(self, system, lock): + """Initialize.""" + super().__init__(system, lock.name, serial=lock.serial) + self._lock = lock + + @property + def is_locked(self): + """Return true if the lock is locked.""" + return STATE_MAP.get(self._lock.state) == STATE_LOCKED + + async def async_lock(self, **kwargs): + """Lock the lock.""" + await self._lock.lock() + + async def async_unlock(self, **kwargs): + """Unlock the lock.""" + await self._lock.unlock() + + async def async_update(self): + """Update lock status.""" + if self._lock.offline or self._lock.disabled: + self._online = False + return + + self._online = True + + self._attrs.update( + { + ATTR_LOCK_LOW_BATTERY: self._lock.lock_low_battery, + ATTR_JAMMED: self._lock.state == LockStates.jammed, + ATTR_PIN_PAD_LOW_BATTERY: self._lock.pin_pad_low_battery, + } + ) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 254e947ed259bf..61a16f8aa44aed 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/integrations/simplisafe", "requirements": [ - "simplisafe-python==5.1.0" + "simplisafe-python==5.2.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d9f830fb5ee3aa..c10b9b46621fb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1769,7 +1769,7 @@ shodan==1.19.0 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==5.1.0 +simplisafe-python==5.2.0 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 221995b0ac6834..80c0a5cd33f09b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -546,7 +546,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.simplisafe -simplisafe-python==5.1.0 +simplisafe-python==5.2.0 # homeassistant.components.sleepiq sleepyq==0.7 From c27b94c0e5e49448aef7920be201ccd0fc4eeb6e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 13 Nov 2019 14:25:49 -0700 Subject: [PATCH 1557/3953] Add small speed improvement when unloading Ambient PWS (#28756) --- homeassistant/components/ambient_station/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index bff03eb422b231..7a805d6b86763d 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -1,4 +1,5 @@ """Support for Ambient Weather Station Service.""" +import asyncio import logging from aioambient import Client @@ -297,8 +298,12 @@ async def async_unload_entry(hass, config_entry): ambient = hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) hass.async_create_task(ambient.ws_disconnect()) - for component in ("binary_sensor", "sensor"): - await hass.config_entries.async_forward_entry_unload(config_entry, component) + tasks = [ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in ("binary_sensor", "sensor") + ] + + await asyncio.gather(*tasks) return True From 31131e4ac689620d214055a689e4f3e4c6e34350 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 13 Nov 2019 14:26:52 -0700 Subject: [PATCH 1558/3953] Add small speed improvement when unloading Notion (#28757) --- homeassistant/components/notion/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 62deb4999d9c36..1e04c4a8e8effa 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -144,8 +144,12 @@ async def async_unload_entry(hass, config_entry): cancel = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) cancel() - for component in ("binary_sensor", "sensor"): - await hass.config_entries.async_forward_entry_unload(config_entry, component) + tasks = [ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in ("binary_sensor", "sensor") + ] + + await asyncio.gather(*tasks) return True From fae8cd48c4cf2fca8cc875149975cf7e4e63e299 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 13 Nov 2019 14:27:15 -0700 Subject: [PATCH 1559/3953] Add small speed improvement when unloading OpenUV (#28758) --- homeassistant/components/openuv/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 62a8c642bc8e4f..16b7a50a4aeaf7 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -233,8 +233,12 @@ async def async_unload_entry(hass, config_entry): """Unload an OpenUV config entry.""" hass.data[DOMAIN][DATA_OPENUV_CLIENT].pop(config_entry.entry_id) - for component in ("binary_sensor", "sensor"): - await hass.config_entries.async_forward_entry_unload(config_entry, component) + tasks = [ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in ("binary_sensor", "sensor") + ] + + await asyncio.gather(*tasks) return True From 3f2c344e4f72e638ce9c5b7025487d7dc2c4d0ab Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 13 Nov 2019 14:27:42 -0700 Subject: [PATCH 1560/3953] Add small speed improvement when unloading RainMachine (#28759) --- homeassistant/components/rainmachine/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 183872087a7240..a530223cb0539d 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -351,8 +351,12 @@ async def async_unload_entry(hass, config_entry): remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) remove_listener() - for component in ("binary_sensor", "sensor", "switch"): - await hass.config_entries.async_forward_entry_unload(config_entry, component) + tasks = [ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in ("binary_sensor", "sensor", "switch") + ] + + await asyncio.gather(*tasks) return True From 08d662c0a5f735000dc4d702ccde138263b621aa Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 14 Nov 2019 00:32:16 +0000 Subject: [PATCH 1561/3953] [ci skip] Translation update --- .../components/abode/.translations/pt.json | 1 + .../components/adguard/.translations/pt.json | 4 ++- .../components/almond/.translations/ca.json | 5 +++ .../components/almond/.translations/no.json | 5 +++ .../components/almond/.translations/pt.json | 10 ++++++ .../components/almond/.translations/ru.json | 5 +++ .../components/cover/.translations/pt.json | 10 +++++- .../components/deconz/.translations/pt.json | 8 ++++- .../components/demo/.translations/en.json | 5 +++ .../components/demo/.translations/no.json | 5 +++ .../components/demo/.translations/pt.json | 5 +++ .../components/demo/.translations/ru.json | 5 +++ .../demo/.translations/zh-Hant.json | 5 +++ .../device_tracker/.translations/pt.json | 8 +++++ .../components/fan/.translations/pt.json | 16 +++++++++ .../components/glances/.translations/pt.json | 12 +++++++ .../homekit_controller/.translations/pt.json | 3 +- .../huawei_lte/.translations/pt.json | 34 +++++++++++++++++++ .../iaqualink/.translations/pt.json | 12 +++++++ .../components/life360/.translations/pt.json | 15 ++++++++ .../components/light/.translations/pt.json | 17 ++++++++++ .../components/linky/.translations/pt.json | 18 ++++++++++ .../components/lock/.translations/pt.json | 8 +++++ .../components/met/.translations/pt.json | 3 ++ .../components/neato/.translations/pt.json | 12 +++++++ .../components/notion/.translations/pt.json | 14 ++++++++ .../components/sensor/.translations/pt.json | 7 +++- .../components/somfy/.translations/ca.json | 2 +- .../transmission/.translations/pt.json | 7 +++- .../components/vacuum/.translations/pt.json | 10 ++++++ .../components/vesync/.translations/pt.json | 16 +++++++++ .../components/withings/.translations/pt.json | 11 ++++++ .../components/wled/.translations/pt.json | 22 ++++++++++++ 33 files changed, 313 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/almond/.translations/pt.json create mode 100644 homeassistant/components/demo/.translations/en.json create mode 100644 homeassistant/components/demo/.translations/no.json create mode 100644 homeassistant/components/demo/.translations/pt.json create mode 100644 homeassistant/components/demo/.translations/ru.json create mode 100644 homeassistant/components/demo/.translations/zh-Hant.json create mode 100644 homeassistant/components/device_tracker/.translations/pt.json create mode 100644 homeassistant/components/fan/.translations/pt.json create mode 100644 homeassistant/components/glances/.translations/pt.json create mode 100644 homeassistant/components/huawei_lte/.translations/pt.json create mode 100644 homeassistant/components/iaqualink/.translations/pt.json create mode 100644 homeassistant/components/life360/.translations/pt.json create mode 100644 homeassistant/components/light/.translations/pt.json create mode 100644 homeassistant/components/linky/.translations/pt.json create mode 100644 homeassistant/components/lock/.translations/pt.json create mode 100644 homeassistant/components/neato/.translations/pt.json create mode 100644 homeassistant/components/notion/.translations/pt.json create mode 100644 homeassistant/components/vacuum/.translations/pt.json create mode 100644 homeassistant/components/vesync/.translations/pt.json create mode 100644 homeassistant/components/withings/.translations/pt.json create mode 100644 homeassistant/components/wled/.translations/pt.json diff --git a/homeassistant/components/abode/.translations/pt.json b/homeassistant/components/abode/.translations/pt.json index 512bf59906c9a9..4a371c706f774e 100644 --- a/homeassistant/components/abode/.translations/pt.json +++ b/homeassistant/components/abode/.translations/pt.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "password": "Palavra-passe", "username": "Endere\u00e7o de e-mail" } } diff --git a/homeassistant/components/adguard/.translations/pt.json b/homeassistant/components/adguard/.translations/pt.json index f681da4210f8a3..77ce7025f70c98 100644 --- a/homeassistant/components/adguard/.translations/pt.json +++ b/homeassistant/components/adguard/.translations/pt.json @@ -4,7 +4,9 @@ "user": { "data": { "host": "Servidor", - "port": "Porta" + "password": "Palavra-passe", + "port": "Porta", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/almond/.translations/ca.json b/homeassistant/components/almond/.translations/ca.json index cf4618d227210c..ee71197757d6d6 100644 --- a/homeassistant/components/almond/.translations/ca.json +++ b/homeassistant/components/almond/.translations/ca.json @@ -4,6 +4,11 @@ "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Almond.", "cannot_connect": "No es pot connectar amb el servidor d'Almond." }, + "step": { + "pick_implementation": { + "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/no.json b/homeassistant/components/almond/.translations/no.json index 6ea2de635b191c..0272a120f21ddf 100644 --- a/homeassistant/components/almond/.translations/no.json +++ b/homeassistant/components/almond/.translations/no.json @@ -5,6 +5,11 @@ "cannot_connect": "Kan ikke koble til Almond-serveren.", "missing_configuration": "Vennligst sjekk dokumentasjonen om hvordan du setter opp Almond." }, + "step": { + "pick_implementation": { + "title": "Velg autentiseringsmetode" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/pt.json b/homeassistant/components/almond/.translations/pt.json new file mode 100644 index 00000000000000..720400e72a5f24 --- /dev/null +++ b/homeassistant/components/almond/.translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ru.json b/homeassistant/components/almond/.translations/ru.json index d15e9a1eeb47d7..39dc41a39952e9 100644 --- a/homeassistant/components/almond/.translations/ru.json +++ b/homeassistant/components/almond/.translations/ru.json @@ -5,6 +5,11 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Almond.", "missing_configuration": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 Almond." }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/pt.json b/homeassistant/components/cover/.translations/pt.json index cb9f85c4a939f6..6234d2685f4b6f 100644 --- a/homeassistant/components/cover/.translations/pt.json +++ b/homeassistant/components/cover/.translations/pt.json @@ -4,7 +4,15 @@ "is_closed": "{entity_name} est\u00e1 fechada", "is_closing": "{entity_name} est\u00e1 a fechar", "is_open": "{entity_name} est\u00e1 aberta", - "is_opening": "{entity_name} est\u00e1 a abrir" + "is_opening": "{entity_name} est\u00e1 a abrir", + "is_position": "A posi\u00e7\u00e3o atual de {entity_name} \u00e9", + "is_tilt_position": "A inclina\u00e7\u00e3o actual de {entity_name} \u00e9" + }, + "trigger_type": { + "closed": "{entity_name} fechou", + "closing": "{entity_name} est\u00e1 a fechar", + "opened": "{entity_name} abriu", + "opening": "{entity_name} est\u00e1 a abrir" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index 63a66595ace417..f24d7692a555e7 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -32,7 +32,13 @@ }, "device_automation": { "trigger_subtype": { - "left": "Esquerda" + "left": "Esquerda", + "side_1": "Lado 1", + "side_2": "Lado 2", + "side_3": "Lado 3", + "side_4": "Lado 4", + "side_5": "Lado 5", + "side_6": "Lado 6" } } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/en.json b/homeassistant/components/demo/.translations/en.json new file mode 100644 index 00000000000000..ef01fcb4f3c35e --- /dev/null +++ b/homeassistant/components/demo/.translations/en.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/no.json b/homeassistant/components/demo/.translations/no.json new file mode 100644 index 00000000000000..ef01fcb4f3c35e --- /dev/null +++ b/homeassistant/components/demo/.translations/no.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/pt.json b/homeassistant/components/demo/.translations/pt.json new file mode 100644 index 00000000000000..8183f28aed3f38 --- /dev/null +++ b/homeassistant/components/demo/.translations/pt.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demonstra\u00e7\u00e3o" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/ru.json b/homeassistant/components/demo/.translations/ru.json new file mode 100644 index 00000000000000..0438252a42991b --- /dev/null +++ b/homeassistant/components/demo/.translations/ru.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\u0414\u0435\u043c\u043e" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/zh-Hant.json b/homeassistant/components/demo/.translations/zh-Hant.json new file mode 100644 index 00000000000000..cfb0fced0c2db3 --- /dev/null +++ b/homeassistant/components/demo/.translations/zh-Hant.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\u5c55\u793a" + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/pt.json b/homeassistant/components/device_tracker/.translations/pt.json new file mode 100644 index 00000000000000..952eb4b1475dfc --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/pt.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_home": "{entity_name} est\u00e1 em casa", + "is_not_home": "{entity_name} n\u00e3o est\u00e1 em casa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/pt.json b/homeassistant/components/fan/.translations/pt.json new file mode 100644 index 00000000000000..a76550cbeddec2 --- /dev/null +++ b/homeassistant/components/fan/.translations/pt.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} est\u00e1 desligada", + "is_on": "{entity_name} est\u00e1 ligada" + }, + "trigger_type": { + "turned_off": "{entity_name} desligou-se", + "turned_on": "{entity_name} ligou-se" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/pt.json b/homeassistant/components/glances/.translations/pt.json new file mode 100644 index 00000000000000..b46423599731a7 --- /dev/null +++ b/homeassistant/components/glances/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/pt.json b/homeassistant/components/homekit_controller/.translations/pt.json index 37f68408ce44fa..c60ed155569f14 100644 --- a/homeassistant/components/homekit_controller/.translations/pt.json +++ b/homeassistant/components/homekit_controller/.translations/pt.json @@ -4,7 +4,8 @@ "pair": { "data": { "pairing_code": "C\u00f3digo de emparelhamento" - } + }, + "title": "Emparelhar com o acess\u00f3rio HomeKit" }, "user": { "data": { diff --git a/homeassistant/components/huawei_lte/.translations/pt.json b/homeassistant/components/huawei_lte/.translations/pt.json new file mode 100644 index 00000000000000..6e3a06ac662c26 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/pt.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo j\u00e1 foi configurado", + "already_in_progress": "Este dispositivo j\u00e1 est\u00e1 a ser configurado" + }, + "error": { + "incorrect_password": "Palavra-passe incorreta", + "incorrect_username": "Nome de Utilizador incorreto", + "incorrect_username_or_password": "Nome de utilizador ou palavra passe incorretos" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "url": "", + "username": "Utilizador" + }, + "title": "Configurar o Huawei LTE" + } + }, + "title": "" + }, + "options": { + "step": { + "init": { + "data": { + "recipient": "Destinat\u00e1rios de notifica\u00e7\u00e3o por SMS", + "track_new_devices": "Seguir novos dispositivos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/pt.json b/homeassistant/components/iaqualink/.translations/pt.json new file mode 100644 index 00000000000000..24825307e767a9 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de utilizador / Endere\u00e7o de e-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/pt.json b/homeassistant/components/life360/.translations/pt.json new file mode 100644 index 00000000000000..9c848bd8ec8810 --- /dev/null +++ b/homeassistant/components/life360/.translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_username": "Nome de utilizador incorreto" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pt.json b/homeassistant/components/light/.translations/pt.json new file mode 100644 index 00000000000000..272516f4c6b9c2 --- /dev/null +++ b/homeassistant/components/light/.translations/pt.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Alternar {entity_name}", + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado" + }, + "trigger_type": { + "turned_off": "{entity_name} foi desligado", + "turned_on": "{entity_name} foi ligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/pt.json b/homeassistant/components/linky/.translations/pt.json new file mode 100644 index 00000000000000..daf1ce751816e4 --- /dev/null +++ b/homeassistant/components/linky/.translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "username_exists": "Conta j\u00e1 configurada" + }, + "error": { + "username_exists": "Conta j\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/pt.json b/homeassistant/components/lock/.translations/pt.json new file mode 100644 index 00000000000000..05bcf44166003e --- /dev/null +++ b/homeassistant/components/lock/.translations/pt.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "locked": "{entity_name} fechada", + "unlocked": "{entity_name} aberta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/pt.json b/homeassistant/components/met/.translations/pt.json index c7081cd694a0af..2ba2911d89042f 100644 --- a/homeassistant/components/met/.translations/pt.json +++ b/homeassistant/components/met/.translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "name_exists": "A localiza\u00e7\u00e3o j\u00e1 existe" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/neato/.translations/pt.json b/homeassistant/components/neato/.translations/pt.json new file mode 100644 index 00000000000000..b46423599731a7 --- /dev/null +++ b/homeassistant/components/neato/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/pt.json b/homeassistant/components/notion/.translations/pt.json new file mode 100644 index 00000000000000..e379229ec3a1b9 --- /dev/null +++ b/homeassistant/components/notion/.translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_credentials": "Nome de utilizador ou palavra passe incorretos" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/pt.json b/homeassistant/components/sensor/.translations/pt.json index 801b22f0c4544b..032d88e02d9e4b 100644 --- a/homeassistant/components/sensor/.translations/pt.json +++ b/homeassistant/components/sensor/.translations/pt.json @@ -1,8 +1,13 @@ { "device_automation": { "condition_type": { + "is_battery_level": "N\u00edvel de bateria atual de {entity_name}", "is_humidity": "humidade {entity_name}", - "is_power": "pot\u00eancia {entity_name}", + "is_illuminance": "Luminancia atual de {entity_name}", + "is_power": "Pot\u00eancia atual de {entity_name}", + "is_pressure": "Press\u00e3o atual de {entity_name}", + "is_signal_strength": "Intensidade atual do sinal de {entity_name}", + "is_temperature": "Temperatura atual de {entity_name}", "is_timestamp": "momento temporal de {entity_name}", "is_value": "valor {entity_name}" }, diff --git a/homeassistant/components/somfy/.translations/ca.json b/homeassistant/components/somfy/.translations/ca.json index 0ca526fde69c53..b3095cd4e9c4da 100644 --- a/homeassistant/components/somfy/.translations/ca.json +++ b/homeassistant/components/somfy/.translations/ca.json @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "Tria del m\u00e8tode d'autenticaci\u00f3" + "title": "Selecci\u00f3 del m\u00e8tode d'autenticaci\u00f3" } }, "title": "Somfy" diff --git a/homeassistant/components/transmission/.translations/pt.json b/homeassistant/components/transmission/.translations/pt.json index f681da4210f8a3..0421228d0f0d6d 100644 --- a/homeassistant/components/transmission/.translations/pt.json +++ b/homeassistant/components/transmission/.translations/pt.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "wrong_credentials": "Nome de utilizador ou palavra passe incorretos" + }, "step": { "user": { "data": { "host": "Servidor", - "port": "Porta" + "password": "Palavra-passe", + "port": "Porta", + "username": "Utilizador" } } } diff --git a/homeassistant/components/vacuum/.translations/pt.json b/homeassistant/components/vacuum/.translations/pt.json new file mode 100644 index 00000000000000..42b8bdabc0f344 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/pt.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "action_type": { + "clean": "Deixar {entity_name} limpar" + }, + "condtion_type": { + "is_cleaning": "{entity_name} est\u00e1 a limpar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/pt.json b/homeassistant/components/vesync/.translations/pt.json new file mode 100644 index 00000000000000..395907056e9cfb --- /dev/null +++ b/homeassistant/components/vesync/.translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_login": "Nome de utilizador ou palavra passe incorretos" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Endere\u00e7o de e-mail" + }, + "title": "Introduza o nome de utilizador e a palavra-passe" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/pt.json b/homeassistant/components/withings/.translations/pt.json new file mode 100644 index 00000000000000..0a1f02335ccb86 --- /dev/null +++ b/homeassistant/components/withings/.translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "profile": { + "data": { + "profile": "Perfil" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/pt.json b/homeassistant/components/wled/.translations/pt.json new file mode 100644 index 00000000000000..521434d11a8bf4 --- /dev/null +++ b/homeassistant/components/wled/.translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "connection_error": "Falha ao ligar ao dispositivo WLED" + }, + "error": { + "connection_error": "Falha ao ligar ao dispositivo WLED" + }, + "step": { + "user": { + "data": { + "host": "Nome servidor ou endere\u00e7o IP" + }, + "title": "Associar WLED" + }, + "zeroconf_confirm": { + "title": "Dispositivo WLED descoberto" + } + }, + "title": "" + } +} \ No newline at end of file From c6d6bbf3e013e23948588a840b4bd63d0879a343 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 14 Nov 2019 12:01:53 +0100 Subject: [PATCH 1562/3953] Upgrade ephem to 3.7.7.0 (#28715) --- homeassistant/components/season/manifest.json | 2 +- homeassistant/components/season/sensor.py | 12 ++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json index 528e9ef35f1f12..445c2bc3827320 100644 --- a/homeassistant/components/season/manifest.json +++ b/homeassistant/components/season/manifest.json @@ -3,7 +3,7 @@ "name": "Season", "documentation": "https://www.home-assistant.io/integrations/season", "requirements": [ - "ephem==3.7.6.0" + "ephem==3.7.7.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 46d2291cf81284..e5f756f0c75176 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -1,27 +1,31 @@ """Support for tracking which astronomical or meteorological season it is.""" -import logging from datetime import datetime +import logging import ephem import voluptuous as vol +from homeassistant import util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_TYPE from homeassistant.helpers.entity import Entity -from homeassistant import util import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +EQUATOR = "equator" + NORTHERN = "northern" + SOUTHERN = "southern" -EQUATOR = "equator" +STATE_AUTUMN = "autumn" STATE_SPRING = "spring" STATE_SUMMER = "summer" -STATE_AUTUMN = "autumn" STATE_WINTER = "winter" + TYPE_ASTRONOMICAL = "astronomical" TYPE_METEOROLOGICAL = "meteorological" + VALID_TYPES = [TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL] HEMISPHERE_SEASON_SWAP = { diff --git a/requirements_all.txt b/requirements_all.txt index c10b9b46621fb9..a06b4b293d87ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -474,7 +474,7 @@ env_canada==0.0.30 envoy_reader==0.8.6 # homeassistant.components.season -ephem==3.7.6.0 +ephem==3.7.7.0 # homeassistant.components.epson epson-projector==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80c0a5cd33f09b..0848d204cec5b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,7 +155,7 @@ eebrightbox==0.0.4 emulated_roku==0.1.8 # homeassistant.components.season -ephem==3.7.6.0 +ephem==3.7.7.0 # homeassistant.components.feedreader feedparser-homeassistant==5.2.2.dev1 From 962c47b66678794f118359ac852353d165119f35 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 14 Nov 2019 14:12:46 +0100 Subject: [PATCH 1563/3953] Updated frontend to 20191114.0 (#28768) --- 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 8f7212209cf7f0..b7506f599ef60e 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191108.0" + "home-assistant-frontend==20191114.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ade3305edc9f38..dfe53ed2e19e15 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191108.0 +home-assistant-frontend==20191114.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index a06b4b293d87ca..7fc128882015ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191108.0 +home-assistant-frontend==20191114.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0848d204cec5b7..960a7bbc887dd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191108.0 +home-assistant-frontend==20191114.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From ea0bae2d1bfbd99297f7209cfcb0f1988f68420a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 14 Nov 2019 14:13:16 +0100 Subject: [PATCH 1564/3953] Added plus sign to safe characters in urllib.parse.quote method (#28763) --- homeassistant/components/squeezebox/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index d8574223307d50..8e03763b709b74 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -545,5 +545,5 @@ def async_call_method(self, command, parameters=None): all_params = [command] if parameters: for parameter in parameters: - all_params.append(urllib.parse.quote(parameter, safe=":=/?")) + all_params.append(urllib.parse.quote(parameter, safe="+:=/?")) return self.async_query(*all_params) From 2cf86a35541d64693ce7f7d60bab3593ff29a316 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Nov 2019 14:34:13 +0100 Subject: [PATCH 1565/3953] Fix account link version check (#28770) --- homeassistant/components/cloud/account_link.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 6fbfcc8723ba03..9ec1fe634d771b 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -13,7 +13,6 @@ DATA_SERVICES = "cloud_account_link_services" CACHE_TIMEOUT = 3600 -PATCH_VERSION = int(PATCH_VERSION.split(".")[0]) _LOGGER = logging.getLogger(__name__) @@ -49,7 +48,20 @@ def _is_older(version: str) -> bool: except ValueError: return False - cur_version_parts = [MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION] + patch_number_str = "" + + for char in PATCH_VERSION: + if char.isnumeric(): + patch_number_str += char + else: + break + + try: + patch_number = int(patch_number_str) + except ValueError: + patch_number = 0 + + cur_version_parts = [MAJOR_VERSION, MINOR_VERSION, patch_number] return version_parts <= cur_version_parts From 4f089aba356fbb76444622a736fc923aa7bf4fdc Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 13 Nov 2019 09:25:45 -0500 Subject: [PATCH 1566/3953] Bump ZHA quirks to 0.0.28 (#28750) --- 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 18e8af7008d154..8781625d326522 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.11.0", - "zha-quirks==0.0.27", + "zha-quirks==0.0.28", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.11.0", "zigpy-xbee-homeassistant==0.7.0", diff --git a/requirements_all.txt b/requirements_all.txt index 68ac061815d51c..9043f944d4202c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2054,7 +2054,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.27 +zha-quirks==0.0.28 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f891394db6ced7..bd57a5490a3de3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -637,7 +637,7 @@ yahooweather==0.10 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.27 +zha-quirks==0.0.28 # homeassistant.components.zha zigpy-deconz==0.7.0 From 520e4296babd17f9005843581c7a981be4fda681 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 14 Nov 2019 14:12:46 +0100 Subject: [PATCH 1567/3953] Updated frontend to 20191114.0 (#28768) --- 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 8f7212209cf7f0..b7506f599ef60e 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191108.0" + "home-assistant-frontend==20191114.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ade3305edc9f38..dfe53ed2e19e15 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191108.0 +home-assistant-frontend==20191114.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9043f944d4202c..058ad143f6f478 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191108.0 +home-assistant-frontend==20191114.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd57a5490a3de3..969107d3707c3d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191108.0 +home-assistant-frontend==20191114.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From af2443fb10b8e028d705701eb5623a1f1b6daa86 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Nov 2019 14:34:13 +0100 Subject: [PATCH 1568/3953] Fix account link version check (#28770) --- homeassistant/components/cloud/account_link.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 6fbfcc8723ba03..9ec1fe634d771b 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -13,7 +13,6 @@ DATA_SERVICES = "cloud_account_link_services" CACHE_TIMEOUT = 3600 -PATCH_VERSION = int(PATCH_VERSION.split(".")[0]) _LOGGER = logging.getLogger(__name__) @@ -49,7 +48,20 @@ def _is_older(version: str) -> bool: except ValueError: return False - cur_version_parts = [MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION] + patch_number_str = "" + + for char in PATCH_VERSION: + if char.isnumeric(): + patch_number_str += char + else: + break + + try: + patch_number = int(patch_number_str) + except ValueError: + patch_number = 0 + + cur_version_parts = [MAJOR_VERSION, MINOR_VERSION, patch_number] return version_parts <= cur_version_parts From f5a9bcdf6d42b31db3767b2946add88333cc2bbb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Nov 2019 14:36:30 +0100 Subject: [PATCH 1569/3953] Bumped version to 0.102.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fa7f753495cdfe..620af2b79db541 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From b2870b6833af0dfd933f3c5e0d88dcf525ea56e1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 14 Nov 2019 19:15:08 +0100 Subject: [PATCH 1570/3953] DECONZ - Add device trigger support for aqara 86sw2 switches to Deconz (#28767) * Add device trigger support for aqara 86sw2 switched to Deconz * Update device_trigger.py * Update naming --- homeassistant/components/deconz/device_trigger.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 30fbfdd05ae12d..9d4a944d6958c7 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -209,6 +209,13 @@ (CONF_DOUBLE_PRESS, CONF_BOTH_BUTTONS): 3004, } +AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL = "lumi.sensor_86sw2" +AQARA_DOUBLE_WALL_SWITCH_WXKG02LM = { + (CONF_SHORT_PRESS, CONF_LEFT): 1002, + (CONF_SHORT_PRESS, CONF_RIGHT): 2002, + (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): 3002, +} + AQARA_MINI_SWITCH_MODEL = "lumi.remote.b1acn01" AQARA_MINI_SWITCH = { (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, @@ -249,6 +256,7 @@ TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER, AQARA_CUBE_MODEL: AQARA_CUBE, AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, + AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_WXKG02LM, AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH, AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, From 0467ddb60ce0f414b3b1a89eb51294690ecb5a3e Mon Sep 17 00:00:00 2001 From: Brendon Baumgartner Date: Thu, 14 Nov 2019 12:07:43 -0800 Subject: [PATCH 1571/3953] Fix amazon dependency conflicts (#28217) * fix amazon dependency conflicts * bump boto3 for route53 --- homeassistant/components/amazon_polly/manifest.json | 4 ++-- homeassistant/components/aws/manifest.json | 2 +- homeassistant/components/route53/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 45e382647f8d38..c07aad079e43dc 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -3,10 +3,10 @@ "name": "Amazon polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "requirements": [ - "boto3==1.9.233" + "boto3==1.9.252" ], "dependencies": [], "codeowners": [ "@robbiet480" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index a4543cc4b0f5b7..b617eb75ee16c1 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -3,7 +3,7 @@ "name": "Aws", "documentation": "https://www.home-assistant.io/integrations/aws", "requirements": [ - "aiobotocore==0.10.2" + "aiobotocore==0.10.4" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 34a296b0f9dece..307132aa01bac1 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -3,7 +3,7 @@ "name": "Route53", "documentation": "https://www.home-assistant.io/integrations/route53", "requirements": [ - "boto3==1.9.233", + "boto3==1.9.252", "ipify==1.0.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 7fc128882015ac..5922be08c0109f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -136,7 +136,7 @@ aioasuswrt==1.1.22 aioautomatic==0.6.5 # homeassistant.components.aws -aiobotocore==0.10.2 +aiobotocore==0.10.4 # homeassistant.components.dnsip aiodns==2.0.0 @@ -317,7 +317,7 @@ bomradarloop==0.1.3 # homeassistant.components.amazon_polly # homeassistant.components.route53 -boto3==1.9.233 +boto3==1.9.252 # homeassistant.components.braviatv braviarc-homeassistant==0.3.7.dev0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 960a7bbc887dd6..ef4298f414c7d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -47,7 +47,7 @@ aioasuswrt==1.1.22 aioautomatic==0.6.5 # homeassistant.components.aws -aiobotocore==0.10.2 +aiobotocore==0.10.4 # homeassistant.components.esphome aioesphomeapi==2.5.0 From f170ac48d22d6d85e38d4fa547f02a98a9cb7767 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 15 Nov 2019 00:32:15 +0000 Subject: [PATCH 1572/3953] [ci skip] Translation update --- .../components/almond/.translations/lb.json | 5 ++++ .../components/almond/.translations/pl.json | 8 +++++- .../components/almond/.translations/sl.json | 5 ++++ .../almond/.translations/zh-Hant.json | 5 ++++ .../ambiclimate/.translations/pl.json | 2 +- .../components/climate/.translations/pl.json | 17 ++++++++++++ .../components/demo/.translations/fr.json | 5 ++++ .../components/demo/.translations/lb.json | 5 ++++ .../components/demo/.translations/pl.json | 5 ++++ .../components/demo/.translations/sl.json | 5 ++++ .../components/fan/.translations/pl.json | 16 ++++++++++++ .../components/hue/.translations/pl.json | 2 +- .../components/ifttt/.translations/pl.json | 2 +- .../components/lock/.translations/pl.json | 4 +++ .../logi_circle/.translations/pl.json | 2 +- .../mobile_app/.translations/pl.json | 2 +- .../components/nest/.translations/pl.json | 2 +- .../components/plaato/.translations/pl.json | 2 +- .../components/point/.translations/pl.json | 2 +- .../components/toon/.translations/pl.json | 2 +- .../components/traccar/.translations/pl.json | 2 +- .../components/vacuum/.translations/pl.json | 16 ++++++++++++ .../components/withings/.translations/pl.json | 4 +-- .../components/wled/.translations/pl.json | 26 +++++++++++++++++++ 24 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/climate/.translations/pl.json create mode 100644 homeassistant/components/demo/.translations/fr.json create mode 100644 homeassistant/components/demo/.translations/lb.json create mode 100644 homeassistant/components/demo/.translations/pl.json create mode 100644 homeassistant/components/demo/.translations/sl.json create mode 100644 homeassistant/components/fan/.translations/pl.json create mode 100644 homeassistant/components/vacuum/.translations/pl.json create mode 100644 homeassistant/components/wled/.translations/pl.json diff --git a/homeassistant/components/almond/.translations/lb.json b/homeassistant/components/almond/.translations/lb.json index 30cb8b8689148a..ca836267d46ccc 100644 --- a/homeassistant/components/almond/.translations/lb.json +++ b/homeassistant/components/almond/.translations/lb.json @@ -5,6 +5,11 @@ "cannot_connect": "Kann sech net mam Almond Server verbannen.", "missing_configuration": "Kuckt w.e.g. Dokumentatioun iwwert d'ariichten vun Almond." }, + "step": { + "pick_implementation": { + "title": "Wielt Authentifikatiouns Method aus" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/pl.json b/homeassistant/components/almond/.translations/pl.json index b96d9c09bb24a1..56aa629e015b19 100644 --- a/homeassistant/components/almond/.translations/pl.json +++ b/homeassistant/components/almond/.translations/pl.json @@ -2,7 +2,13 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Almond.", - "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem Almond." + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem Almond.", + "missing_configuration": "Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105 konfiguracji Almond." + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } }, "title": "Almond" } diff --git a/homeassistant/components/almond/.translations/sl.json b/homeassistant/components/almond/.translations/sl.json index c809b908b9f580..086190590ac88c 100644 --- a/homeassistant/components/almond/.translations/sl.json +++ b/homeassistant/components/almond/.translations/sl.json @@ -5,6 +5,11 @@ "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond.", "missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond." }, + "step": { + "pick_implementation": { + "title": "Izberite na\u010din preverjanja pristnosti" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/zh-Hant.json b/homeassistant/components/almond/.translations/zh-Hant.json index 743835b1046562..4db6e0c936efa3 100644 --- a/homeassistant/components/almond/.translations/zh-Hant.json +++ b/homeassistant/components/almond/.translations/zh-Hant.json @@ -5,6 +5,11 @@ "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Almond \u4f3a\u670d\u5668\u3002", "missing_configuration": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a Almond\u3002" }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/pl.json b/homeassistant/components/ambiclimate/.translations/pl.json index 675c5e18776ca4..18f5d043dbcef0 100644 --- a/homeassistant/components/ambiclimate/.translations/pl.json +++ b/homeassistant/components/ambiclimate/.translations/pl.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu.", "already_setup": "Konto Ambiclimate jest skonfigurowane.", - "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 w nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119] (https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Ambiclimate" diff --git a/homeassistant/components/climate/.translations/pl.json b/homeassistant/components/climate/.translations/pl.json new file mode 100644 index 00000000000000..c5b0c483ca9321 --- /dev/null +++ b/homeassistant/components/climate/.translations/pl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "zmie\u0144 tryb HVAC na {entity_name}", + "set_preset_mode": "zmie\u0144 ustawienia dla {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "na {entity_name} jest ustawiony okre\u015blony tryb HVAC", + "is_preset_mode": "na {entity_name} jest okre\u015blone ustawienie" + }, + "trigger_type": { + "current_humidity_changed": "zmieni si\u0119 zmierzona wilgotno\u015b\u0107 {entity_name}", + "current_temperature_changed": "zmieni si\u0119 zmierzona temperatura {entity_name}", + "hvac_mode_changed": "zmieni si\u0119 tryb HVAC {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/fr.json b/homeassistant/components/demo/.translations/fr.json new file mode 100644 index 00000000000000..bc093330c26dea --- /dev/null +++ b/homeassistant/components/demo/.translations/fr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "D\u00e9mo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/lb.json b/homeassistant/components/demo/.translations/lb.json new file mode 100644 index 00000000000000..ef01fcb4f3c35e --- /dev/null +++ b/homeassistant/components/demo/.translations/lb.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/pl.json b/homeassistant/components/demo/.translations/pl.json new file mode 100644 index 00000000000000..ef01fcb4f3c35e --- /dev/null +++ b/homeassistant/components/demo/.translations/pl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/sl.json b/homeassistant/components/demo/.translations/sl.json new file mode 100644 index 00000000000000..ef01fcb4f3c35e --- /dev/null +++ b/homeassistant/components/demo/.translations/sl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/pl.json b/homeassistant/components/fan/.translations/pl.json new file mode 100644 index 00000000000000..424794a5b64a43 --- /dev/null +++ b/homeassistant/components/fan/.translations/pl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "wy\u0142\u0105cz {entity_name}", + "turn_on": "w\u0142\u0105cz {entity_name}" + }, + "condtion_type": { + "is_off": "wentylator (entity_name} jest wy\u0142\u0105czony", + "is_on": "wentylator (entity_name} jest w\u0142\u0105czony" + }, + "trigger_type": { + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index 33b1ffbfe86c74..3866af9d7fcaa1 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -22,7 +22,7 @@ "title": "Wybierz mostek Hue" }, "link": { - "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant.", + "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue w Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", "title": "Hub Link" } }, diff --git a/homeassistant/components/ifttt/.translations/pl.json b/homeassistant/components/ifttt/.translations/pl.json index ca81a510531725..206702eb593c9a 100644 --- a/homeassistant/components/ifttt/.translations/pl.json +++ b/homeassistant/components/ifttt/.translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wys\u0142a\u0107 zdarzenia do Home Assistant'a, b\u0119dziesz musia\u0142 u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_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}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz u\u017cy\u0107 akcji \"Make a web request\" z [IFTTT Webhook apletu]({applet_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}) na temat konfiguracji automatyzacji, by obs\u0142u\u017cy\u0107 przychodz\u0105ce dane." }, "step": { "user": { diff --git a/homeassistant/components/lock/.translations/pl.json b/homeassistant/components/lock/.translations/pl.json index a3fe7358398c58..a3123919615d08 100644 --- a/homeassistant/components/lock/.translations/pl.json +++ b/homeassistant/components/lock/.translations/pl.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "zamek {entity_name} jest zamkni\u0119ty", "is_unlocked": "zamek {entity_name} jest otwarty" + }, + "trigger_type": { + "locked": "nast\u0105pi zamkni\u0119cie {entity_name}", + "unlocked": "nast\u0105pi otwarcie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/pl.json b/homeassistant/components/logi_circle/.translations/pl.json index 2266ea841c5fb9..333a295ad06d52 100644 --- a/homeassistant/components/logi_circle/.translations/pl.json +++ b/homeassistant/components/logi_circle/.translations/pl.json @@ -4,7 +4,7 @@ "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Logi Circle.", "external_error": "Wyst\u0105pi\u0142 wyj\u0105tek z innego przep\u0142ywu.", "external_setup": "Logi Circle zosta\u0142o pomy\u015blnie skonfigurowane z innego przep\u0142ywu.", - "no_flows": "Musisz skonfigurowa\u0107 Logi Circle, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/logi_circle/)." + "no_flows": "Musisz skonfigurowa\u0107 Logi Circle, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Logi Circle." diff --git a/homeassistant/components/mobile_app/.translations/pl.json b/homeassistant/components/mobile_app/.translations/pl.json index feb00c20779d3c..5fa53384ff0849 100644 --- a/homeassistant/components/mobile_app/.translations/pl.json +++ b/homeassistant/components/mobile_app/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Otw\u00f3rz aplikacj\u0119 mobiln\u0105, aby skonfigurowa\u0107 integracj\u0119 z Home Assistant. Zapoznaj si\u0119 z [dokumentacj\u0105] ({apps_url}), by zobaczy\u0107 list\u0119 kompatybilnych aplikacji." + "install_app": "Otw\u00f3rz aplikacj\u0119 mobiln\u0105, aby skonfigurowa\u0107 integracj\u0119 z Home Assistant. Zapoznaj si\u0119 z [dokumentacj\u0105]({apps_url}), by zobaczy\u0107 list\u0119 kompatybilnych aplikacji." }, "step": { "confirm": { diff --git a/homeassistant/components/nest/.translations/pl.json b/homeassistant/components/nest/.translations/pl.json index 482a67eb221887..d40c872e3006af 100644 --- a/homeassistant/components/nest/.translations/pl.json +++ b/homeassistant/components/nest/.translations/pl.json @@ -4,7 +4,7 @@ "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Nest.", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", - "no_flows": "Musisz skonfigurowa\u0107 Nest, zanim b\u0119dziesz m\u00f3g\u0142 wykona\u0107 uwierzytelnienie. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/nest/)." + "no_flows": "Musisz skonfigurowa\u0107 Nest, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/nest/)." }, "error": { "internal_error": "Wewn\u0119trzny b\u0142\u0105d sprawdzania poprawno\u015bci kodu", diff --git a/homeassistant/components/plaato/.translations/pl.json b/homeassistant/components/plaato/.translations/pl.json index c4402cb8f37f20..dc931b6bd8c498 100644 --- a/homeassistant/components/plaato/.translations/pl.json +++ b/homeassistant/components/plaato/.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 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \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 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/point/.translations/pl.json b/homeassistant/components/point/.translations/pl.json index 40acc8b4e496d7..4de46c84137920 100644 --- a/homeassistant/components/point/.translations/pl.json +++ b/homeassistant/components/point/.translations/pl.json @@ -5,7 +5,7 @@ "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", "external_setup": "Punkt pomy\u015blnie skonfigurowany.", - "no_flows": "Musisz skonfigurowa\u0107 Point, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/point/)." + "no_flows": "Musisz skonfigurowa\u0107 Point, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/point/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono przy u\u017cyciu Minut dla urz\u0105dze\u0144 Point" diff --git a/homeassistant/components/toon/.translations/pl.json b/homeassistant/components/toon/.translations/pl.json index 403be9bc067a54..52da6579a03097 100644 --- a/homeassistant/components/toon/.translations/pl.json +++ b/homeassistant/components/toon/.translations/pl.json @@ -4,7 +4,7 @@ "client_id": "Identyfikator klienta z konfiguracji jest nieprawid\u0142owy.", "client_secret": "Tajny klucz klienta z konfiguracji jest nieprawid\u0142owy.", "no_agreements": "To konto nie posiada wy\u015bwietlaczy Toon.", - "no_app": "Musisz skonfigurowa\u0107 Toon zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. Prosz\u0119 przeczyta\u0107 instrukcj\u0119] (https://www.home-assistant.io/components/toon/).", + "no_app": "Musisz skonfigurowa\u0107 Toon, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z [instrukcj\u0105](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d podczas uwierzytelniania." }, "error": { diff --git a/homeassistant/components/traccar/.translations/pl.json b/homeassistant/components/traccar/.translations/pl.json index 74ff0c089d879f..95b7eb1af00b56 100644 --- a/homeassistant/components/traccar/.translations/pl.json +++ b/homeassistant/components/traccar/.translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Niezb\u0119dna jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wys\u0142a\u0107 wydarzenia do Home Assistant, musisz skonfigurowa\u0107 funkcj\u0119 webhook w Traccar. \n\n U\u017cyj nast\u0119puj\u0105cego URL: ` {webhook_url} ` \n\n Zobacz [dokumentacj\u0119] ( {docs_url} ) w celu uzyskania dalszych szczeg\u00f3\u0142\u00f3w." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 webhook w Traccar. \n\n U\u017cyj nast\u0119puj\u0105cego URL: `{webhook_url}` \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/vacuum/.translations/pl.json b/homeassistant/components/vacuum/.translations/pl.json new file mode 100644 index 00000000000000..e637c26b3edecf --- /dev/null +++ b/homeassistant/components/vacuum/.translations/pl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "niech {entity_name} sprz\u0105ta", + "dock": "niech {entity_name} wr\u00f3ci do bazy" + }, + "condtion_type": { + "is_cleaning": "{entity_name} sprz\u0105ta", + "is_docked": "{entity_name} jest w bazie" + }, + "trigger_type": { + "cleaning": "{entity_name} zacznie sprz\u0105ta\u0107", + "docked": "{entity_name} wr\u00f3ci do bazy" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json index 4f1ee47ab0e449..90fe281c29fec9 100644 --- a/homeassistant/components/withings/.translations/pl.json +++ b/homeassistant/components/withings/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_flows": "Musisz skonfigurowa\u0107 Withings, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Przeczytaj prosz\u0119 dokumentacj\u0119." + "no_flows": "Musisz skonfigurowa\u0107 Withings, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z dokumentacj\u0105." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu" @@ -18,7 +18,7 @@ "data": { "profile": "Profil" }, - "description": "Wybierz profil u\u017cytkownika Withings, na kt\u00f3ry chcesz po\u0142\u0105czy\u0107 z Home Assistant'em. Na stronie Withings wybierz ten sam profil u\u017cytkownika by dane by\u0142y poprawnie oznaczone.", + "description": "Wybierz profil u\u017cytkownika Withings, na kt\u00f3ry chcesz po\u0142\u0105czy\u0107 z Home Assistant'em. Na stronie Withings wybierz ten sam profil u\u017cytkownika, by dane by\u0142y poprawnie oznaczone.", "title": "Profil u\u017cytkownika" } }, diff --git a/homeassistant/components/wled/.translations/pl.json b/homeassistant/components/wled/.translations/pl.json new file mode 100644 index 00000000000000..c10c8ab34d6afb --- /dev/null +++ b/homeassistant/components/wled/.translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "To urz\u0105dzenie WLED jest ju\u017c skonfigurowane", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem WLED." + }, + "error": { + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem WLED." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Konfiguracja WLED w celu integracji z Home Assistant'em.", + "title": "Po\u0142\u0105cz sw\u00f3j WLED" + }, + "zeroconf_confirm": { + "description": "Czy chcesz doda\u0107 WLED o nazwie `{name}` do Home Assistant'a?", + "title": "Wykryto urz\u0105dzenie WLED" + } + }, + "title": "WLED" + } +} \ No newline at end of file From 806b96ef736502ad88de34f2c38dd0428756066d Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 15 Nov 2019 05:14:46 +0100 Subject: [PATCH 1573/3953] Bump pytest to 5.2.3 (#28785) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 33fab3d6c6af1d..f8fba663bcb502 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,6 +14,6 @@ pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.2 +pytest==5.2.3 requests_mock==1.7.0 responses==0.10.6 From 2aad1504195820ad5998674b14661abab078ea04 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 14 Nov 2019 20:15:58 -0800 Subject: [PATCH 1574/3953] Convert Tesla to Async (#28748) * build: bump teslajsonpy to 0.2.0 * feat: add async * perf: convert unnecessary async calls to sync * fix: force real login * Revert change to HVAC_MODE_HEAT * Remove charging rate sensor * Remove tests * Remove tests * Address requested changes * Add missing sensors * Move update to async_setup_scanner * Align wtih prior update behavior --- homeassistant/components/tesla/__init__.py | 62 ++++++++++--------- .../components/tesla/binary_sensor.py | 6 +- homeassistant/components/tesla/climate.py | 16 ++--- homeassistant/components/tesla/const.py | 24 +++++++ .../components/tesla/device_tracker.py | 19 +++--- homeassistant/components/tesla/lock.py | 14 ++--- homeassistant/components/tesla/manifest.json | 2 +- homeassistant/components/tesla/sensor.py | 11 ++-- homeassistant/components/tesla/switch.py | 38 ++++++------ requirements_all.txt | 2 +- 10 files changed, 109 insertions(+), 85 deletions(-) create mode 100644 homeassistant/components/tesla/const.py diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index a08112d66b31f1..a3d45eed01c6b6 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -2,9 +2,8 @@ from collections import defaultdict import logging -import voluptuous as vol from teslajsonpy import Controller as teslaAPI, TeslaException - +import voluptuous as vol from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -12,18 +11,14 @@ CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import aiohttp_client, config_validation as cv, discovery from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -DOMAIN = "tesla" +from .const import DOMAIN, TESLA_COMPONENTS _LOGGER = logging.getLogger(__name__) -TESLA_ID_FORMAT = "{}_{}" -TESLA_ID_LIST_SCHEMA = vol.Schema([int]) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -42,17 +37,8 @@ NOTIFICATION_ID = "tesla_integration_notification" NOTIFICATION_TITLE = "Tesla integration setup" -TESLA_COMPONENTS = [ - "sensor", - "lock", - "climate", - "binary_sensor", - "device_tracker", - "switch", -] - -def setup(hass, base_config): +async def async_setup(hass, base_config): """Set up of Tesla component.""" config = base_config.get(DOMAIN) @@ -61,10 +47,15 @@ def setup(hass, base_config): update_interval = config.get(CONF_SCAN_INTERVAL) if hass.data.get(DOMAIN) is None: try: - hass.data[DOMAIN] = { - "controller": teslaAPI(email, password, update_interval), - "devices": defaultdict(list), - } + websession = aiohttp_client.async_get_clientsession(hass) + controller = teslaAPI( + websession, + email=email, + password=password, + update_interval=update_interval, + ) + await controller.connect(test_login=False) + hass.data[DOMAIN] = {"controller": controller, "devices": defaultdict(list)} _LOGGER.debug("Connected to the Tesla API.") except TeslaException as ex: if ex.code == 401: @@ -85,9 +76,7 @@ def setup(hass, base_config): ) _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) return False - - all_devices = hass.data[DOMAIN]["controller"].list_vehicles() - + all_devices = controller.get_homeassistant_components() if not all_devices: return False @@ -95,8 +84,9 @@ def setup(hass, base_config): hass.data[DOMAIN]["devices"][device.hass_type].append(device) for component in TESLA_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, base_config) - + hass.async_create_task( + discovery.async_load_platform(hass, component, DOMAIN, {}, base_config) + ) return True @@ -104,11 +94,12 @@ class TeslaDevice(Entity): """Representation of a Tesla device.""" def __init__(self, tesla_device, controller): - """Initialise of the Tesla device.""" + """Initialise the Tesla device.""" self.tesla_device = tesla_device self.controller = controller self._name = self.tesla_device.name self.tesla_id = slugify(self.tesla_device.uniq_name) + self._attributes = {} @property def name(self): @@ -128,8 +119,19 @@ def should_poll(self): @property def device_state_attributes(self): """Return the state attributes of the device.""" - attr = {} - + attr = self._attributes if self.tesla_device.has_battery(): attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level() return attr + + async def async_added_to_hass(self): + """Register state update callback.""" + pass + + async def async_will_remove_from_hass(self): + """Prepare for unload.""" + pass + + async def async_update(self): + """Update the state of the device.""" + await self.tesla_device.async_update() diff --git a/homeassistant/components/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py index 2a452dcc832460..738533a9b5685a 100644 --- a/homeassistant/components/tesla/binary_sensor.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -8,7 +8,7 @@ _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla binary sensor.""" devices = [ TeslaBinarySensor(device, hass.data[TESLA_DOMAIN]["controller"], "connectivity") @@ -41,8 +41,8 @@ def is_on(self): """Return the state of the binary sensor.""" return self._state - def update(self): + async def async_update(self): """Update the state of the device.""" _LOGGER.debug("Updating sensor: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = self.tesla_device.get_value() diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index 45858dcf985f0a..85fd8a8e258dda 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -16,7 +16,7 @@ SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF] -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla climate platform.""" devices = [ TeslaThermostat(device, hass.data[TESLA_DOMAIN]["controller"]) @@ -57,10 +57,10 @@ def hvac_modes(self): """ return SUPPORT_HVAC - def update(self): + async def async_update(self): """Call by the Tesla device callback to update state.""" _LOGGER.debug("Updating: %s", self._name) - self.tesla_device.update() + await super().async_update() self._target_temperature = self.tesla_device.get_goal_temp() self._temperature = self.tesla_device.get_current_temp() @@ -83,17 +83,17 @@ def target_temperature(self): """Return the temperature we try to reach.""" return self._target_temperature - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" _LOGGER.debug("Setting temperature for: %s", self._name) temperature = kwargs.get(ATTR_TEMPERATURE) if temperature: - self.tesla_device.set_temperature(temperature) + await self.tesla_device.set_temperature(temperature) - def set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" _LOGGER.debug("Setting mode for: %s", self._name) if hvac_mode == HVAC_MODE_OFF: - self.tesla_device.set_status(False) + await self.tesla_device.set_status(False) elif hvac_mode == HVAC_MODE_HEAT: - self.tesla_device.set_status(True) + await self.tesla_device.set_status(True) diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py new file mode 100644 index 00000000000000..30a58b733edcb3 --- /dev/null +++ b/homeassistant/components/tesla/const.py @@ -0,0 +1,24 @@ +"""Const file for Tesla cars.""" +DOMAIN = "tesla" +DATA_LISTENER = "listener" +TESLA_COMPONENTS = [ + "sensor", + "lock", + "climate", + "binary_sensor", + "device_tracker", + "switch", +] +SENSOR_ICONS = { + "battery sensor": "mdi:battery", + "range sensor": "mdi:gauge", + "mileage sensor": "mdi:counter", + "parking brake sensor": "mdi:car-brake-parking", + "charger sensor": "mdi:ev-station", + "charger switch": "mdi:battery-charging", + "update switch": "mdi:update", + "maxrange switch": "mdi:gauge-full", + "temperature sensor": "mdi:thermometer", + "location tracker": "mdi:crosshairs-gps", + "charging rate sensor": "mdi:speedometer", +} diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py index 9db7bb3eb4893b..c205cc587eba4e 100644 --- a/homeassistant/components/tesla/device_tracker.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -1,7 +1,7 @@ """Support for tracking Tesla cars.""" import logging -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.util import slugify from . import DOMAIN as TESLA_DOMAIN @@ -9,11 +9,13 @@ _LOGGER = logging.getLogger(__name__) -def setup_scanner(hass, config, see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the Tesla tracker.""" - TeslaDeviceTracker( - hass, config, see, hass.data[TESLA_DOMAIN]["devices"]["devices_tracker"] + tracker = TeslaDeviceTracker( + hass, config, async_see, hass.data[TESLA_DOMAIN]["devices"]["devices_tracker"] ) + await tracker.update_info() + async_track_utc_time_change(hass, tracker.update_info, second=range(0, 60, 30)) return True @@ -25,14 +27,11 @@ def __init__(self, hass, config, see, tesla_devices): self.hass = hass self.see = see self.devices = tesla_devices - self._update_info() - track_utc_time_change(self.hass, self._update_info, second=range(0, 60, 30)) - - def _update_info(self, now=None): + async def update_info(self, now=None): """Update the device info.""" for device in self.devices: - device.update() + await device.async_update() name = device.name _LOGGER.debug("Updating device position: %s", name) dev_id = slugify(device.uniq_name) @@ -41,6 +40,6 @@ def _update_info(self, now=None): lat = location["latitude"] lon = location["longitude"] attrs = {"trackr_id": dev_id, "id": dev_id, "name": name} - self.see( + await self.see( dev_id=dev_id, host_name=name, gps=(lat, lon), attributes=attrs ) diff --git a/homeassistant/components/tesla/lock.py b/homeassistant/components/tesla/lock.py index 389d6ee76e3c15..5e97602357df8f 100644 --- a/homeassistant/components/tesla/lock.py +++ b/homeassistant/components/tesla/lock.py @@ -9,7 +9,7 @@ _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla lock platform.""" devices = [ TeslaLock(device, hass.data[TESLA_DOMAIN]["controller"]) @@ -26,23 +26,23 @@ def __init__(self, tesla_device, controller): self._state = None super().__init__(tesla_device, controller) - def lock(self, **kwargs): + async def async_lock(self, **kwargs): """Send the lock command.""" _LOGGER.debug("Locking doors for: %s", self._name) - self.tesla_device.lock() + await self.tesla_device.lock() - def unlock(self, **kwargs): + async def async_unlock(self, **kwargs): """Send the unlock command.""" _LOGGER.debug("Unlocking doors for: %s", self._name) - self.tesla_device.unlock() + await self.tesla_device.unlock() @property def is_locked(self): """Get whether the lock is in locked state.""" return self._state == STATE_LOCKED - def update(self): + async def async_update(self): """Update state of the lock.""" _LOGGER.debug("Updating state for: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = STATE_LOCKED if self.tesla_device.is_locked() else STATE_UNLOCKED diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 87d76c16f05ffd..a2021092413011 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -2,7 +2,7 @@ "domain": "tesla", "name": "Tesla", "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.0.26"], + "requirements": ["teslajsonpy==0.2.0"], "dependencies": [], "codeowners": ["@zabuldon"] } diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index c737b2f0bba576..1cce37f232a5e1 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -1,5 +1,4 @@ """Support for the Tesla sensors.""" -from datetime import timedelta import logging from homeassistant.const import ( @@ -14,10 +13,8 @@ _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=5) - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla sensor platform.""" controller = hass.data[TESLA_DOMAIN]["devices"]["controller"] devices = [] @@ -26,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.bin_type == 0x4: devices.append(TeslaSensor(device, controller, "inside")) devices.append(TeslaSensor(device, controller, "outside")) - else: + elif device.bin_type in [0xA, 0xB, 0x5]: devices.append(TeslaSensor(device, controller)) add_entities(devices, True) @@ -62,10 +59,10 @@ def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" return self._unit - def update(self): + async def async_update(self): """Update the state from the sensor.""" _LOGGER.debug("Updating sensor: %s", self._name) - self.tesla_device.update() + await super().async_update() units = self.tesla_device.measurement if self.tesla_device.bin_type == 0x4: diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 985194f87b2a93..5f432875aebc0b 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -9,7 +9,7 @@ _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla switch platform.""" controller = hass.data[TESLA_DOMAIN]["controller"] devices = [] @@ -30,25 +30,25 @@ def __init__(self, tesla_device, controller): self._state = None super().__init__(tesla_device, controller) - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Send the on command.""" _LOGGER.debug("Enable charging: %s", self._name) - self.tesla_device.start_charge() + await self.tesla_device.start_charge() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable charging for: %s", self._name) - self.tesla_device.stop_charge() + await self.tesla_device.stop_charge() @property def is_on(self): """Get whether the switch is in on state.""" return self._state == STATE_ON - def update(self): + async def async_update(self): """Update the state of the switch.""" _LOGGER.debug("Updating state for: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = STATE_ON if self.tesla_device.is_charging() else STATE_OFF @@ -56,29 +56,29 @@ class RangeSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla max range charging switch.""" def __init__(self, tesla_device, controller): - """Initialise of the switch.""" + """Initialise the switch.""" self._state = None super().__init__(tesla_device, controller) - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Send the on command.""" _LOGGER.debug("Enable max range charging: %s", self._name) - self.tesla_device.set_max() + await self.tesla_device.set_max() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable max range charging: %s", self._name) - self.tesla_device.set_standard() + await self.tesla_device.set_standard() @property def is_on(self): """Get whether the switch is in on state.""" return self._state - def update(self): + async def async_update(self): """Update the state of the switch.""" _LOGGER.debug("Updating state for: %s", self._name) - self.tesla_device.update() + await super().async_update() self._state = bool(self.tesla_device.is_maxrange()) @@ -86,18 +86,19 @@ class UpdateSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla update switch.""" def __init__(self, tesla_device, controller): - """Initialise of the switch.""" + """Initialise the switch.""" self._state = None + tesla_device.type = "update switch" super().__init__(tesla_device, controller) self._name = self._name.replace("charger", "update") self.tesla_id = self.tesla_id.replace("charger", "update") - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Send the on command.""" _LOGGER.debug("Enable updates: %s %s", self._name, self.tesla_device.id()) self.controller.set_updates(self.tesla_device.id(), True) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Send the off command.""" _LOGGER.debug("Disable updates: %s %s", self._name, self.tesla_device.id()) self.controller.set_updates(self.tesla_device.id(), False) @@ -107,8 +108,9 @@ def is_on(self): """Get whether the switch is in on state.""" return self._state - def update(self): + async def async_update(self): """Update the state of the switch.""" car_id = self.tesla_device.id() _LOGGER.debug("Updating state for: %s %s", self._name, car_id) + await super().async_update() self._state = bool(self.controller.get_updates(car_id)) diff --git a/requirements_all.txt b/requirements_all.txt index 5922be08c0109f..407fc45a2471ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1898,7 +1898,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.0.26 +teslajsonpy==0.2.0 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 From bc14e93ae3490126c49e0b57df122e74e0849320 Mon Sep 17 00:00:00 2001 From: Lado Kumsiashvili Date: Fri, 15 Nov 2019 08:56:32 +0100 Subject: [PATCH 1575/3953] =?UTF-8?q?bump=20home=20mitemp=5Fbt=20to=200.0.?= =?UTF-8?q?3.=20It=20has=20an=20important=20bugfix=20for=20reading=20?= =?UTF-8?q?=E2=80=A6=20(#28765)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bump home mitemp_bt to 0.0.3. It has an important bugfix for reading temperatures under 10 grade * adjust the version number for mitemp_bt up to 0.0.3 --- homeassistant/components/mitemp_bt/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index 612e7c19f8bcd9..0f8da91ffd096c 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -3,7 +3,7 @@ "name": "Mitemp bt", "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", "requirements": [ - "mitemp_bt==0.0.1" + "mitemp_bt==0.0.3" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 407fc45a2471ab..c91b6050af3a40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -838,7 +838,7 @@ millheater==0.3.4 minio==4.0.9 # homeassistant.components.mitemp_bt -mitemp_bt==0.0.1 +mitemp_bt==0.0.3 # homeassistant.components.mopar motorparts==1.1.0 From 5e973dd0172f8ddcc640f41d8ad1e68c0e303919 Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Fri, 15 Nov 2019 09:21:46 +0100 Subject: [PATCH 1576/3953] Change unique id for SAJ sensor based on device SN (#28663) * Change unique id for SAJ sensor based on device SN * Add SAJ device name + sn to state attributes * Revert device state attributes (after review) --- homeassistant/components/saj/manifest.json | 2 +- homeassistant/components/saj/sensor.py | 21 +++++++++++---------- requirements_all.txt | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index 4d02ab74840d6e..02d83916d50bab 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,7 +3,7 @@ "name": "SAJ", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": [ - "pysaj==0.0.13" + "pysaj==0.0.14" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 7542440c102a58..2a17d110c6e414 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -69,9 +69,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Use all sensors by default hass_sensors = [] - for sensor in sensor_def: - hass_sensors.append(SAJsensor(sensor, inverter_name=config.get(CONF_NAME))) - kwargs = {} if wifi: kwargs["wifi"] = True @@ -81,7 +78,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: saj = pysaj.SAJ(config[CONF_HOST], **kwargs) - await saj.read(sensor_def) + done = await saj.read(sensor_def) except pysaj.UnauthorizedException: _LOGGER.error("Username and/or password is wrong.") return @@ -91,7 +88,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) return - async_add_entities(hass_sensors) + if done: + for sensor in sensor_def: + hass_sensors.append( + SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) + ) + + async_add_entities(hass_sensors) async def async_saj(): """Update all the SAJ sensors.""" @@ -163,10 +166,11 @@ def remove_listener(): class SAJsensor(Entity): """Representation of a SAJ sensor.""" - def __init__(self, pysaj_sensor, inverter_name=None): + def __init__(self, serialnumber, pysaj_sensor, inverter_name=None): """Initialize the sensor.""" self._sensor = pysaj_sensor self._inverter_name = inverter_name + self._serialnumber = serialnumber self._state = self._sensor.value @property @@ -235,7 +239,4 @@ def async_update_values(self, unknown_state=False): @property def unique_id(self): """Return a unique identifier for this sensor.""" - if self._inverter_name: - return f"{self._inverter_name}_{self._sensor.name}" - - return f"{self._sensor.name}" + return f"{self._serialnumber}_{self._sensor.name}" diff --git a/requirements_all.txt b/requirements_all.txt index c91b6050af3a40..5977b1fef0e545 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1435,7 +1435,7 @@ pyrepetier==3.0.5 pysabnzbd==1.1.0 # homeassistant.components.saj -pysaj==0.0.13 +pysaj==0.0.14 # homeassistant.components.sony_projector pysdcp==1 From b4ccc0202af3a5025d0f16f184d9e91cfa83fe41 Mon Sep 17 00:00:00 2001 From: Paolo Tuninetto Date: Fri, 15 Nov 2019 09:53:16 +0100 Subject: [PATCH 1577/3953] Change keys for Samsung TV next and prev track command (#28213) --- homeassistant/components/samsungtv/media_player.py | 4 ++-- tests/components/samsungtv/test_media_player.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index aa6e3ae62d12a9..65805235f3fbfb 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -312,11 +312,11 @@ def media_pause(self): def media_next_track(self): """Send next track command.""" - self.send_key("KEY_FF") + self.send_key("KEY_CHUP") def media_previous_track(self): """Send the previous track command.""" - self.send_key("KEY_REWIND") + self.send_key("KEY_CHDOWN") async def async_play_media(self, media_type, media_id, **kwargs): """Support changing a channel.""" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 2b5e377c617a52..3409e55a49ceb1 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -543,7 +543,7 @@ async def test_media_next_track(hass, remote): ) # key and update called assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_FF"), call("KEY")] + assert remote.control.call_args_list == [call("KEY_CHUP"), call("KEY")] async def test_media_previous_track(hass, remote): @@ -554,7 +554,7 @@ async def test_media_previous_track(hass, remote): ) # key and update called assert remote.control.call_count == 2 - assert remote.control.call_args_list == [call("KEY_REWIND"), call("KEY")] + assert remote.control.call_args_list == [call("KEY_CHDOWN"), call("KEY")] async def test_turn_on_with_mac(hass, remote, wakeonlan): From 60e7440ec188b1e82e76dcb533820d857ccd3d10 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 15 Nov 2019 09:55:40 +0100 Subject: [PATCH 1578/3953] Fix HomematicIP Cloud Alarm Control Panel support for basic mode (#28778) --- .../homematicip_cloud/alarm_control_panel.py | 67 +++-------------- .../test_alarm_control_panel.py | 72 +++++-------------- .../homematicip_cloud/test_climate.py | 1 + .../components/homematicip_cloud/test_init.py | 4 +- 4 files changed, 31 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index a7b1beaec93868..8ebb35b12c156b 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -1,8 +1,7 @@ """Support for HomematicIP Cloud alarm control panel.""" import logging -from homematicip.aio.group import AsyncSecurityZoneGroup -from homematicip.base.enums import WindowState +from homematicip.functionalHomes import SecurityAndAlarmHome from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.config_entries import ConfigEntry @@ -32,34 +31,15 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] - security_zones = [] - for group in hap.home.groups: - if isinstance(group, AsyncSecurityZoneGroup): - security_zones.append(group) - - if security_zones: - devices.append(HomematicipAlarmControlPanel(hap, security_zones)) - - if devices: - async_add_entities(devices) + async_add_entities([HomematicipAlarmControlPanel(hap)]) class HomematicipAlarmControlPanel(AlarmControlPanel): """Representation of an alarm control panel.""" - def __init__(self, hap: HomematicipHAP, security_zones) -> None: + def __init__(self, hap: HomematicipHAP) -> None: """Initialize the alarm control panel.""" self._home = hap.home - self.alarm_state = STATE_ALARM_DISARMED - self._internal_alarm_zone = None - self._external_alarm_zone = None - - for security_zone in security_zones: - if security_zone.label == "INTERNAL": - self._internal_alarm_zone = security_zone - elif security_zone.label == "EXTERNAL": - self._external_alarm_zone = security_zone @property def device_info(self): @@ -75,28 +55,23 @@ def device_info(self): @property def state(self) -> str: """Return the state of the device.""" + # check for triggered alarm + if self._security_and_alarm.alarmActive: + return STATE_ALARM_TRIGGERED + activation_state = self._home.get_security_zones_activation() # check arm_away if activation_state == (True, True): - if self._internal_alarm_zone_state or self._external_alarm_zone_state: - return STATE_ALARM_TRIGGERED return STATE_ALARM_ARMED_AWAY # check arm_home if activation_state == (False, True): - if self._external_alarm_zone_state: - return STATE_ALARM_TRIGGERED return STATE_ALARM_ARMED_HOME return STATE_ALARM_DISARMED @property - def _internal_alarm_zone_state(self) -> bool: - return _get_zone_alarm_state(self._internal_alarm_zone) - - @property - def _external_alarm_zone_state(self) -> bool: - """Return the state of the device.""" - return _get_zone_alarm_state(self._external_alarm_zone) + def _security_and_alarm(self): + return self._home.get_functionalHome(SecurityAndAlarmHome) async def async_alarm_disarm(self, code=None): """Send disarm command.""" @@ -112,10 +87,7 @@ async def async_alarm_arm_away(self, code=None): async def async_added_to_hass(self): """Register callbacks.""" - if self._internal_alarm_zone: - self._internal_alarm_zone.on_update(self._async_device_changed) - if self._external_alarm_zone: - self._external_alarm_zone.on_update(self._async_device_changed) + self._home.on_update(self._async_device_changed) def _async_device_changed(self, *args, **kwargs): """Handle device state changes.""" @@ -138,26 +110,9 @@ def should_poll(self) -> bool: @property def available(self) -> bool: """Device available.""" - return ( - not self._internal_alarm_zone.unreach - or not self._external_alarm_zone.unreach - ) + return self._home.connected @property def unique_id(self) -> str: """Return a unique ID.""" return f"{self.__class__.__name__}_{self._home.id}" - - -def _get_zone_alarm_state(security_zone) -> bool: - if security_zone and security_zone.active: - if ( - security_zone.sabotage - or security_zone.motionDetected - or security_zone.presenceDetected - or security_zone.windowState == WindowState.OPEN - or security_zone.windowState == WindowState.TILTED - ): - return True - - return False diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 2798a0879b73df..78bc0a09ea507a 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -1,7 +1,4 @@ """Tests for HomematicIP Cloud alarm control panel.""" -from homematicip.base.enums import WindowState -from homematicip.group import SecurityZoneGroup - from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, ) @@ -17,29 +14,24 @@ from .helper import get_and_check_entity_basics -def _get_security_zones(groups): # pylint: disable=W0221 - """Get the security zones.""" - for group in groups: - if isinstance(group, SecurityZoneGroup): - if group.label == "EXTERNAL": - external = group - elif group.label == "INTERNAL": - internal = group - return internal, external - - async def _async_manipulate_security_zones( - hass, home, internal_active, external_active, window_state + hass, home, internal_active=False, external_active=False, alarm_triggered=False ): """Set new values on hmip security zones.""" - internal_zone, external_zone = _get_security_zones(home.groups) + json = home._rawJSONData # pylint: disable=W0212 + json["functionalHomes"]["SECURITY_AND_ALARM"]["alarmActive"] = alarm_triggered + external_zone_id = json["functionalHomes"]["SECURITY_AND_ALARM"]["securityZones"][ + "EXTERNAL" + ] + internal_zone_id = json["functionalHomes"]["SECURITY_AND_ALARM"]["securityZones"][ + "INTERNAL" + ] + external_zone = home.search_group_by_id(external_zone_id) external_zone.active = external_active - external_zone.windowState = window_state + internal_zone = home.search_group_by_id(internal_zone_id) internal_zone.active = internal_active - # Just one call to a security zone is required to refresh the ACP. - internal_zone.fire_update_event() - + home.fire_update_event(json) await hass.async_block_till_done() @@ -70,79 +62,49 @@ async def test_hmip_alarm_control_panel(hass, default_mock_hap): assert not hmip_device home = default_mock_hap.home - service_call_counter = len(home.mock_calls) await hass.services.async_call( "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 1 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (True, True) await _async_manipulate_security_zones( - hass, - home, - internal_active=True, - external_active=True, - window_state=WindowState.CLOSED, + hass, home, internal_active=True, external_active=True ) assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_AWAY await hass.services.async_call( "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 3 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (False, True) - await _async_manipulate_security_zones( - hass, - home, - internal_active=False, - external_active=True, - window_state=WindowState.CLOSED, - ) + await _async_manipulate_security_zones(hass, home, external_active=True) assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_HOME await hass.services.async_call( "alarm_control_panel", "alarm_disarm", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 5 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (False, False) - await _async_manipulate_security_zones( - hass, - home, - internal_active=False, - external_active=False, - window_state=WindowState.CLOSED, - ) + await _async_manipulate_security_zones(hass, home) assert hass.states.get(entity_id).state is STATE_ALARM_DISARMED await hass.services.async_call( "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 7 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (True, True) await _async_manipulate_security_zones( - hass, - home, - internal_active=True, - external_active=True, - window_state=WindowState.OPEN, + hass, home, internal_active=True, external_active=True, alarm_triggered=True ) assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED await hass.services.async_call( "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 9 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (False, True) await _async_manipulate_security_zones( - hass, - home, - internal_active=False, - external_active=True, - window_state=WindowState.OPEN, + hass, home, external_active=True, alarm_triggered=True ) assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 858fba29563519..2b233a6dee2c87 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -343,6 +343,7 @@ async def test_hmip_heating_group_heat_with_switch(hass, default_mock_hap): hass, default_mock_hap, entity_id, entity_name, device_model ) + assert hmip_device assert ha_state.state == HVAC_MODE_AUTO assert ha_state.attributes["current_temperature"] == 24.7 assert ha_state.attributes["min_temp"] == 5.0 diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index ba27a619e6aa4f..eb51c3ece386ff 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -161,5 +161,5 @@ async def test_hmip_dump_hap_config_services(hass, mock_hap_with_service): ) home = mock_hap_with_service.home assert home.mock_calls[-1][0] == "download_configuration" - assert len(home.mock_calls) == 8 # pylint: disable=W0212 - assert len(write_mock.mock_calls) > 0 + assert home.mock_calls + assert write_mock.mock_calls From 4c690eace8707d4b0330739474679ec27fda6066 Mon Sep 17 00:00:00 2001 From: LeoCal <25389602+LeoCal@users.noreply.github.com> Date: Fri, 15 Nov 2019 10:52:15 +0100 Subject: [PATCH 1579/3953] Fix Swisscom empty response received (#28782) --- homeassistant/components/swisscom/device_tracker.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index adb018a4b4bea9..5662212c9e8d90 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -92,6 +92,10 @@ def get_swisscom_data(self): _LOGGER.info("No response from Swisscom Internet Box") return devices + if "status" not in request.json(): + _LOGGER.info("No status in response from Swisscom Internet Box") + return devices + for device in request.json()["status"]: try: devices[device["Key"]] = { From 35000848edf2829dbd4e40bbed456dc06d76c2b9 Mon Sep 17 00:00:00 2001 From: Tyler Page Date: Fri, 15 Nov 2019 11:49:56 +0000 Subject: [PATCH 1580/3953] Fix changing venstar operation_mode (#28754) --- homeassistant/components/venstar/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index c948772197f665..de26d236649767 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -278,7 +278,7 @@ def set_temperature(self, **kwargs): temperature = kwargs.get(ATTR_TEMPERATURE) if operation_mode and self._mode_map.get(operation_mode) != self._client.mode: - set_temp = self._set_operation_mode(self._mode_map.get(operation_mode)) + set_temp = self._set_operation_mode(operation_mode) if set_temp: if ( From d796053d9f67ea436eb781982b1a4676fbe9e562 Mon Sep 17 00:00:00 2001 From: Davide Varricchio <45564538+bannhead@users.noreply.github.com> Date: Fri, 15 Nov 2019 21:22:24 +0100 Subject: [PATCH 1581/3953] Add support for Hisense AEH-W4A1 wifi module (AC remote control) (#28641) * First commit * First working release, but there's a lot to do * Added support for preset_modes * Refined logic * Added translations for config_flow * Updated translations * modified: homeassistant/components/hisense_aehw4a1/climate.py * modified: climate.py * Updated library to latest version * Small changes * Null states when AC off * Minor fixes * Latest updates for TOX * First commit * First working release, but there's a lot to do * new file: requirements_test_all.txt * Added support for preset_modes * Refined logic * Added translations for config_flow * Updated translations * modified: homeassistant/components/hisense_aehw4a1/climate.py * modified: climate.py * Updated library to latest version * Small changes * Null states when AC off * Minor fixes * Latest updates for TOX * new file: requirements_test_all.txt * Fighting with tox * vs Tox round 2 * Isort and updated requirements_test_all.txt * Fighting with lint * Implemented available state * Changed exception type after Travis-ci pylint fails * Support entry in configuration.yaml * Removed commented code * Switched to async * Minor changes * Updated library and fixed pylint errors * Code optimization * Implemented static ip addresses in configuration.yaml * Reverted to existing constant * Corrected pylint wrong-import-order * Recovery from nuke event (messing all while rebase) * Resolved Ci error * Changes for PR * Corrected temp scale for frontend * Added test for config entry from configuration.yaml * Updated dependency * Check on manual config * Imported custom exceptions and modified import config * Optimized * Change based on PR revision * Added logging for failure event on manual config * Tests added but to be corrected * Edited tests * Tests updated to ensure no I/O * Working on tests * Cheanges based on revision for PR * Setting librey exception as direct side_effect in test * Final changes for PR * Redundand on command solved * Improved AC logic --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/hisense_aehw4a1/__init__.py | 81 ++++ .../components/hisense_aehw4a1/climate.py | 438 ++++++++++++++++++ .../components/hisense_aehw4a1/config_flow.py | 22 + .../components/hisense_aehw4a1/const.py | 3 + .../components/hisense_aehw4a1/manifest.json | 13 + .../components/hisense_aehw4a1/strings.json | 15 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/hisense_aehw4a1/__init__.py | 1 + tests/components/hisense_aehw4a1/test_init.py | 89 ++++ 13 files changed, 671 insertions(+) create mode 100644 homeassistant/components/hisense_aehw4a1/__init__.py create mode 100644 homeassistant/components/hisense_aehw4a1/climate.py create mode 100644 homeassistant/components/hisense_aehw4a1/config_flow.py create mode 100644 homeassistant/components/hisense_aehw4a1/const.py create mode 100644 homeassistant/components/hisense_aehw4a1/manifest.json create mode 100644 homeassistant/components/hisense_aehw4a1/strings.json create mode 100644 tests/components/hisense_aehw4a1/__init__.py create mode 100644 tests/components/hisense_aehw4a1/test_init.py diff --git a/.coveragerc b/.coveragerc index 4649f175606f4a..b5939bafec6a8c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -288,6 +288,7 @@ omit = homeassistant/components/heatmiser/climate.py homeassistant/components/hikvision/binary_sensor.py homeassistant/components/hikvisioncam/switch.py + homeassistant/components/hisense_aehw4a1/* homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hive/* homeassistant/components/hlk_sw16/* diff --git a/CODEOWNERS b/CODEOWNERS index 879a1c8f55d019..775fd8be5c196a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -128,6 +128,7 @@ homeassistant/components/heos/* @andrewsayre homeassistant/components/here_travel_time/* @eifinger homeassistant/components/hikvision/* @mezz64 homeassistant/components/hikvisioncam/* @fbradyirl +homeassistant/components/hisense_aehw4a1/* @bannhead homeassistant/components/history/* @home-assistant/core homeassistant/components/history_graph/* @andrey-git homeassistant/components/hive/* @Rendili @KJonline diff --git a/homeassistant/components/hisense_aehw4a1/__init__.py b/homeassistant/components/hisense_aehw4a1/__init__.py new file mode 100644 index 00000000000000..721039d0e1c88c --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/__init__.py @@ -0,0 +1,81 @@ +"""The Hisense AEH-W4A1 integration.""" +import ipaddress +import logging + +from pyaehw4a1.aehw4a1 import AehW4a1 +import pyaehw4a1.exceptions +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.const import CONF_IP_ADDRESS +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def coerce_ip(value): + """Validate that provided value is a valid IP address.""" + if not value: + raise vol.Invalid("Must define an IP address") + try: + ipaddress.IPv4Network(value) + except ValueError: + raise vol.Invalid("Not a valid IP address") + return value + + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: { + CLIMATE_DOMAIN: vol.Schema( + { + vol.Optional(CONF_IP_ADDRESS, default=[]): vol.All( + cv.ensure_list, [vol.All(cv.string, coerce_ip)] + ) + } + ) + } + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Hisense AEH-W4A1 integration.""" + conf = config.get(DOMAIN) + hass.data[DOMAIN] = {} + + if conf is not None: + devices = conf[CONF_IP_ADDRESS][:] + for device in devices: + try: + await AehW4a1(device).check() + except pyaehw4a1.exceptions.ConnectionError: + conf[CONF_IP_ADDRESS].remove(device) + _LOGGER.warning("Hisense AEH-W4A1 at %s not found", device) + if conf[CONF_IP_ADDRESS]: + hass.data[DOMAIN] = conf + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry for Hisense AEH-W4A1.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) + ) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN) diff --git a/homeassistant/components/hisense_aehw4a1/climate.py b/homeassistant/components/hisense_aehw4a1/climate.py new file mode 100644 index 00000000000000..da18419c2642f4 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/climate.py @@ -0,0 +1,438 @@ +"""Pyaehw4a1 platform to control of Hisense AEH-W4A1 Climate Devices.""" + +import logging + +from pyaehw4a1.aehw4a1 import AehW4a1 +import pyaehw4a1.exceptions + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_BOOST, + PRESET_ECO, + PRESET_NONE, + PRESET_SLEEP, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_WHOLE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) + +from . import CONF_IP_ADDRESS, DOMAIN + +SUPPORT_FLAGS = ( + SUPPORT_TARGET_TEMPERATURE + | SUPPORT_FAN_MODE + | SUPPORT_SWING_MODE + | SUPPORT_PRESET_MODE +) + +MIN_TEMP_C = 16 +MAX_TEMP_C = 32 + +MIN_TEMP_F = 61 +MAX_TEMP_F = 90 + +HVAC_MODES = [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, +] + +FAN_MODES = [ + "mute", + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + FAN_AUTO, +] + +SWING_MODES = [ + SWING_OFF, + SWING_VERTICAL, + SWING_HORIZONTAL, + SWING_BOTH, +] + +PRESET_MODES = [ + PRESET_NONE, + PRESET_ECO, + PRESET_BOOST, + PRESET_SLEEP, + "sleep_2", + "sleep_3", + "sleep_4", +] + +AC_TO_HA_STATE = { + "0001": HVAC_MODE_HEAT, + "0010": HVAC_MODE_COOL, + "0011": HVAC_MODE_DRY, + "0000": HVAC_MODE_FAN_ONLY, +} + +HA_STATE_TO_AC = { + HVAC_MODE_OFF: "off", + HVAC_MODE_HEAT: "mode_heat", + HVAC_MODE_COOL: "mode_cool", + HVAC_MODE_DRY: "mode_dry", + HVAC_MODE_FAN_ONLY: "mode_fan", +} + +AC_TO_HA_FAN_MODES = { + "00000000": FAN_AUTO, # fan value for heat mode + "00000001": FAN_AUTO, + "00000010": "mute", + "00000100": FAN_LOW, + "00000110": FAN_MEDIUM, + "00001000": FAN_HIGH, +} + +HA_FAN_MODES_TO_AC = { + "mute": "speed_mute", + FAN_LOW: "speed_low", + FAN_MEDIUM: "speed_med", + FAN_HIGH: "speed_max", + FAN_AUTO: "speed_auto", +} + +AC_TO_HA_SWING = { + "00": SWING_OFF, + "10": SWING_VERTICAL, + "01": SWING_HORIZONTAL, + "11": SWING_BOTH, +} + +_LOGGER = logging.getLogger(__name__) + + +def _build_entity(device): + _LOGGER.debug("Found device at %s", device) + return ClimateAehW4a1(device) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the AEH-W4A1 climate platform.""" + # Priority 1: manual config + if hass.data[DOMAIN].get(CONF_IP_ADDRESS): + devices = hass.data[DOMAIN][CONF_IP_ADDRESS] + else: + # Priority 2: scanned interfaces + devices = await AehW4a1().discovery() + + entities = [_build_entity(device) for device in devices] + async_add_entities(entities, True) + + +class ClimateAehW4a1(ClimateDevice): + """Representation of a Hisense AEH-W4A1 module for climate device.""" + + def __init__(self, device): + """Initialize the climate device.""" + self._unique_id = device + self._device = AehW4a1(device) + self._hvac_modes = HVAC_MODES + self._fan_modes = FAN_MODES + self._swing_modes = SWING_MODES + self._preset_modes = PRESET_MODES + self._available = None + self._on = None + self._temperature_unit = None + self._current_temperature = None + self._target_temperature = None + self._hvac_mode = None + self._fan_mode = None + self._swing_mode = None + self._preset_mode = None + self._previous_state = None + + async def async_update(self): + """Pull state from AEH-W4A1.""" + try: + status = await self._device.command("status_102_0") + except pyaehw4a1.exceptions.ConnectionError as library_error: + _LOGGER.warning( + "Unexpected error of %s: %s", self._unique_id, library_error + ) + self._available = False + return + + self._available = True + + self._on = status["run_status"] + + if status["temperature_Fahrenheit"] == "0": + self._temperature_unit = TEMP_CELSIUS + else: + self._temperature_unit = TEMP_FAHRENHEIT + + self._current_temperature = int(status["indoor_temperature_status"], 2) + + if self._on == "1": + device_mode = status["mode_status"] + self._hvac_mode = AC_TO_HA_STATE[device_mode] + + fan_mode = status["wind_status"] + self._fan_mode = AC_TO_HA_FAN_MODES[fan_mode] + + swing_mode = f'{status["up_down"]}{status["left_right"]}' + self._swing_mode = AC_TO_HA_SWING[swing_mode] + + if self._hvac_mode in (HVAC_MODE_COOL, HVAC_MODE_HEAT): + self._target_temperature = int(status["indoor_temperature_setting"], 2) + else: + self._target_temperature = None + + if status["efficient"] == "1": + self._preset_mode = PRESET_BOOST + elif status["low_electricity"] == "1": + self._preset_mode = PRESET_ECO + elif status["sleep_status"] == "0000001": + self._preset_mode = PRESET_SLEEP + elif status["sleep_status"] == "0000010": + self._preset_mode = "sleep_2" + elif status["sleep_status"] == "0000011": + self._preset_mode = "sleep_3" + elif status["sleep_status"] == "0000100": + self._preset_mode = "sleep_4" + else: + self._preset_mode = PRESET_NONE + else: + self._hvac_mode = HVAC_MODE_OFF + self._fan_mode = None + self._swing_mode = None + self._target_temperature = None + self._preset_mode = None + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def name(self): + """Return the name of the climate device.""" + return self._unique_id + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._temperature_unit + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we are trying to reach.""" + return self._target_temperature + + @property + def hvac_mode(self): + """Return hvac target hvac state.""" + return self._hvac_mode + + @property + def hvac_modes(self): + """Return the list of available operation modes.""" + return self._hvac_modes + + @property + def fan_mode(self): + """Return the fan setting.""" + return self._fan_mode + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + return self._fan_modes + + @property + def preset_mode(self): + """Return the preset mode if on.""" + return self._preset_mode + + @property + def preset_modes(self): + """Return the list of available preset modes.""" + return self._preset_modes + + @property + def swing_mode(self): + """Return swing operation.""" + return self._swing_mode + + @property + def swing_modes(self): + """Return the list of available fan modes.""" + return self._swing_modes + + @property + def min_temp(self): + """Return the minimum temperature.""" + if self._temperature_unit == TEMP_CELSIUS: + return MIN_TEMP_C + return MIN_TEMP_F + + @property + def max_temp(self): + """Return the maximum temperature.""" + if self._temperature_unit == TEMP_CELSIUS: + return MAX_TEMP_C + return MAX_TEMP_F + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return 1 + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + async def async_set_temperature(self, **kwargs): + """Set new target temperatures.""" + if self._on != "1": + _LOGGER.warning( + "AC at %s is off, could not set temperature", self._unique_id + ) + return + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + _LOGGER.debug("Setting temp of %s to %s", self._unique_id, temp) + if self._preset_mode != PRESET_NONE: + await self.async_set_preset_mode(PRESET_NONE) + if self._temperature_unit == TEMP_CELSIUS: + await self._device.command(f"temp_{int(temp)}_C") + else: + await self._device.command(f"temp_{int(temp)}_F") + + async def async_set_fan_mode(self, fan_mode): + """Set new fan mode.""" + if self._on != "1": + _LOGGER.warning("AC at %s is off, could not set fan mode", self._unique_id) + return + if self._hvac_mode in (HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY) and ( + self._hvac_mode != HVAC_MODE_FAN_ONLY or fan_mode != FAN_AUTO + ): + _LOGGER.debug("Setting fan mode of %s to %s", self._unique_id, fan_mode) + await self._device.command(HA_FAN_MODES_TO_AC[fan_mode]) + + async def async_set_swing_mode(self, swing_mode): + """Set new target swing operation.""" + if self._on != "1": + _LOGGER.warning( + "AC at %s is off, could not set swing mode", self._unique_id + ) + return + + _LOGGER.debug("Setting swing mode of %s to %s", self._unique_id, swing_mode) + swing_act = self._swing_mode + + if swing_mode == SWING_OFF and swing_act != SWING_OFF: + if swing_act in (SWING_HORIZONTAL, SWING_BOTH): + await self._device.command("hor_dir") + if swing_act in (SWING_VERTICAL, SWING_BOTH): + await self._device.command("vert_dir") + + if swing_mode == SWING_BOTH and swing_act != SWING_BOTH: + if swing_act in (SWING_OFF, SWING_HORIZONTAL): + await self._device.command("vert_swing") + if swing_act in (SWING_OFF, SWING_VERTICAL): + await self._device.command("hor_swing") + + if swing_mode == SWING_VERTICAL and swing_act != SWING_VERTICAL: + if swing_act in (SWING_OFF, SWING_HORIZONTAL): + await self._device.command("vert_swing") + if swing_act in (SWING_BOTH, SWING_HORIZONTAL): + await self._device.command("hor_dir") + + if swing_mode == SWING_HORIZONTAL and swing_act != SWING_HORIZONTAL: + if swing_act in (SWING_BOTH, SWING_VERTICAL): + await self._device.command("vert_dir") + if swing_act in (SWING_OFF, SWING_VERTICAL): + await self._device.command("hor_swing") + + async def async_set_preset_mode(self, preset_mode): + """Set new preset mode.""" + if self._on != "1": + if preset_mode == PRESET_NONE: + return + await self.async_turn_on() + + _LOGGER.debug("Setting preset mode of %s to %s", self._unique_id, preset_mode) + + if preset_mode == PRESET_ECO: + await self._device.command("energysave_on") + self._previous_state = preset_mode + elif preset_mode == PRESET_BOOST: + await self._device.command("turbo_on") + self._previous_state = preset_mode + elif preset_mode == PRESET_SLEEP: + await self._device.command("sleep_1") + self._previous_state = self._hvac_mode + elif preset_mode == "sleep_2": + await self._device.command("sleep_2") + self._previous_state = self._hvac_mode + elif preset_mode == "sleep_3": + await self._device.command("sleep_3") + self._previous_state = self._hvac_mode + elif preset_mode == "sleep_4": + await self._device.command("sleep_4") + self._previous_state = self._hvac_mode + elif self._previous_state is not None: + if self._previous_state == PRESET_ECO: + await self._device.command("energysave_off") + elif self._previous_state == PRESET_BOOST: + await self._device.command("turbo_off") + elif self._previous_state in HA_STATE_TO_AC: + await self._device.command(HA_STATE_TO_AC[self._previous_state]) + self._previous_state = None + + async def async_set_hvac_mode(self, hvac_mode): + """Set new operation mode.""" + _LOGGER.debug("Setting operation mode of %s to %s", self._unique_id, hvac_mode) + if hvac_mode == HVAC_MODE_OFF: + await self.async_turn_off() + else: + await self._device.command(HA_STATE_TO_AC[hvac_mode]) + if self._on != "1": + await self.async_turn_on() + + async def async_turn_on(self): + """Turn on.""" + _LOGGER.debug("Turning %s on", self._unique_id) + await self._device.command("on") + + async def async_turn_off(self): + """Turn off.""" + _LOGGER.debug("Turning %s off", self._unique_id) + await self._device.command("off") diff --git a/homeassistant/components/hisense_aehw4a1/config_flow.py b/homeassistant/components/hisense_aehw4a1/config_flow.py new file mode 100644 index 00000000000000..52926ba7968792 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow for Hisense AEH-W4A1 integration.""" +import logging + +from pyaehw4a1.aehw4a1 import AehW4a1 + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(hass): + """Return if there are devices that can be discovered.""" + aehw4a1_ip_addresses = await AehW4a1().discovery() + return len(aehw4a1_ip_addresses) > 0 + + +config_entry_flow.register_discovery_flow( + DOMAIN, "Hisense AEH-W4A1", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL +) diff --git a/homeassistant/components/hisense_aehw4a1/const.py b/homeassistant/components/hisense_aehw4a1/const.py new file mode 100644 index 00000000000000..8f381492b62bf4 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/const.py @@ -0,0 +1,3 @@ +"""Constants for the Hisense AEH-W4A1 integration.""" + +DOMAIN = "hisense_aehw4a1" diff --git a/homeassistant/components/hisense_aehw4a1/manifest.json b/homeassistant/components/hisense_aehw4a1/manifest.json new file mode 100644 index 00000000000000..e4bdf581f9c990 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "hisense_aehw4a1", + "name": "Hisense AEH-W4A1", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/hisense_aehw4a1", + "requirements": [ + "pyaehw4a1==0.3.1" + ], + "dependencies": [], + "codeowners": [ + "@bannhead" + ] +} diff --git a/homeassistant/components/hisense_aehw4a1/strings.json b/homeassistant/components/hisense_aehw4a1/strings.json new file mode 100644 index 00000000000000..67031c41710d07 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "title": "Hisense AEH-W4A1", + "step": { + "confirm": { + "title": "Hisense AEH-W4A1", + "description": "Do you want to set up Hisense AEH-W4A1?" + } + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Hisense AEH-W4A1 is possible.", + "no_devices_found": "No Hisense AEH-W4A1 devices found on the network." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 519df86f5e9e80..0cec08d94d9805 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -28,6 +28,7 @@ "gpslogger", "hangouts", "heos", + "hisense_aehw4a1", "homekit_controller", "homematicip_cloud", "huawei_lte", diff --git a/requirements_all.txt b/requirements_all.txt index 5977b1fef0e545..355485cf50d41b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1089,6 +1089,9 @@ py_nextbusnext==0.1.4 # homeassistant.components.ads pyads==3.0.7 +# homeassistant.components.hisense_aehw4a1 +pyaehw4a1==0.3.1 + # homeassistant.components.aftership pyaftership==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef4298f414c7d1..e3a2f2f92a53ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -369,6 +369,9 @@ pyRFXtrx==0.23 # homeassistant.components.nextbus py_nextbusnext==0.1.4 +# homeassistant.components.hisense_aehw4a1 +pyaehw4a1==0.3.1 + # homeassistant.components.almond pyalmond==0.0.2 diff --git a/tests/components/hisense_aehw4a1/__init__.py b/tests/components/hisense_aehw4a1/__init__.py new file mode 100644 index 00000000000000..1365294626eece --- /dev/null +++ b/tests/components/hisense_aehw4a1/__init__.py @@ -0,0 +1 @@ +"""Tests for the hisense_aehw4a1 component.""" diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py new file mode 100644 index 00000000000000..638fbe8f943a23 --- /dev/null +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -0,0 +1,89 @@ +"""Tests for the Hisense AEH-W4A1 init file.""" +from unittest.mock import patch + +from pyaehw4a1 import exceptions + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import hisense_aehw4a1 +from homeassistant.setup import async_setup_component + +from tests.common import mock_coro + + +async def test_creating_entry_sets_up_climate_discovery(hass): + """Test setting up Hisense AEH-W4A1 loads the climate component.""" + with patch( + "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.discovery", + return_value=mock_coro(["1.2.3.4"]), + ): + with patch( + "homeassistant.components.hisense_aehw4a1.climate.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup: + result = await hass.config_entries.flow.async_init( + hisense_aehw4a1.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + 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() + + assert len(mock_setup.mock_calls) == 1 + + +async def test_configuring_hisense_w4a1_create_entry(hass): + """Test that specifying config will create an entry.""" + with patch( + "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.check", + return_value=mock_coro(True), + ): + with patch( + "homeassistant.components.hisense_aehw4a1.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup: + await async_setup_component( + hass, + hisense_aehw4a1.DOMAIN, + {"hisense_aehw4a1": {"ip_address": ["1.2.3.4"]}}, + ) + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 1 + + +async def test_configuring_hisense_w4a1_not_creates_entry_for_device_not_found(hass): + """Test that specifying config will not create an entry.""" + with patch( + "homeassistant.components.hisense_aehw4a1.config_flow.AehW4a1.check", + side_effect=exceptions.ConnectionError, + ): + with patch( + "homeassistant.components.hisense_aehw4a1.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup: + await async_setup_component( + hass, + hisense_aehw4a1.DOMAIN, + {"hisense_aehw4a1": {"ip_address": ["1.2.3.4"]}}, + ) + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 0 + + +async def test_configuring_hisense_w4a1_not_creates_entry_for_empty_import(hass): + """Test that specifying config will not create an entry.""" + with patch( + "homeassistant.components.hisense_aehw4a1.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup: + await async_setup_component(hass, hisense_aehw4a1.DOMAIN, {}) + await hass.async_block_till_done() + + assert len(mock_setup.mock_calls) == 0 From adb3997547fabdde4fa79b6fc573cc5858eaca08 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 16 Nov 2019 00:32:13 +0000 Subject: [PATCH 1582/3953] [ci skip] Translation update --- .../components/almond/.translations/ca.json | 3 ++- .../components/climate/.translations/ca.json | 3 ++- .../components/deconz/.translations/ca.json | 22 +++++++++++++------ .../components/demo/.translations/ca.json | 5 +++++ .../components/fan/.translations/ca.json | 4 ++++ .../hisense_aehw4a1/.translations/ca.json | 15 +++++++++++++ .../hisense_aehw4a1/.translations/en.json | 15 +++++++++++++ .../components/vacuum/.translations/ca.json | 10 +++++++-- .../components/wled/.translations/ca.json | 9 ++++++++ 9 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/demo/.translations/ca.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/ca.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/en.json diff --git a/homeassistant/components/almond/.translations/ca.json b/homeassistant/components/almond/.translations/ca.json index ee71197757d6d6..c626e2795ea5ee 100644 --- a/homeassistant/components/almond/.translations/ca.json +++ b/homeassistant/components/almond/.translations/ca.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte amb Almond.", - "cannot_connect": "No es pot connectar amb el servidor d'Almond." + "cannot_connect": "No es pot connectar amb el servidor d'Almond.", + "missing_configuration": "Consulta la documentaci\u00f3 sobre com configurar Almond." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/climate/.translations/ca.json b/homeassistant/components/climate/.translations/ca.json index 7aff7383a39d1d..743729041ab283 100644 --- a/homeassistant/components/climate/.translations/ca.json +++ b/homeassistant/components/climate/.translations/ca.json @@ -1,7 +1,8 @@ { "device_automation": { "action_type": { - "set_hvac_mode": "Canvia el mode HVAC de {entity_name}" + "set_hvac_mode": "Canvia el mode HVAC de {entity_name}", + "set_preset_mode": "Canvia la configuraci\u00f3 preestablerta de {entity_name}" }, "condtion_type": { "is_hvac_mode": "{entity_name} est\u00e0 configurat/ada en un mode HVAC espec\u00edfic", diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 676641d38c6216..a51bfa056f64e5 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -55,12 +55,12 @@ "left": "Esquerra", "open": "Obert", "right": "Dreta", - "side_1": "Costat 1", - "side_2": "Costat 2", - "side_3": "Costat 3", - "side_4": "Costat 4", - "side_5": "Costat 5", - "side_6": "Costat 6", + "side_1": "cara 1", + "side_2": "cara 2", + "side_3": "cara 3", + "side_4": "cara 4", + "side_5": "cara 5", + "side_6": "cara 6", "turn_off": "Desactiva", "turn_on": "Activa" }, @@ -76,8 +76,16 @@ "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades consecutives", + "remote_double_tap": "Dispositiu \"{subtype}\" tocat dues vegades", "remote_falling": "Dispositiu en caiguda lliure", - "remote_gyro_activated": "Dispositiu sacsejat" + "remote_gyro_activated": "Dispositiu sacsejat", + "remote_moved": "Dispositiu mogut amb la \"{subtype}\" amunt", + "remote_rotate_from_side_1": "Dispositiu rotat de la \"cara 1\" a la \"{subtype}\"", + "remote_rotate_from_side_2": "Dispositiu rotat de la \"cara 2\" a la \"{subtype}\"", + "remote_rotate_from_side_3": "Dispositiu rotat de la \"cara 3\" a la \"{subtype}\"", + "remote_rotate_from_side_4": "Dispositiu rotat de la \"cara 4\" a la \"{subtype}\"", + "remote_rotate_from_side_5": "Dispositiu rotat de la \"cara 5\" a la \"{subtype}\"", + "remote_rotate_from_side_6": "Dispositiu rotat de la \"cara 6\" a la \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/demo/.translations/ca.json b/homeassistant/components/demo/.translations/ca.json new file mode 100644 index 00000000000000..944d358e739131 --- /dev/null +++ b/homeassistant/components/demo/.translations/ca.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demostraci\u00f3" + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/ca.json b/homeassistant/components/fan/.translations/ca.json index 1002f8d7dd6f64..0530ccf5a85a78 100644 --- a/homeassistant/components/fan/.translations/ca.json +++ b/homeassistant/components/fan/.translations/ca.json @@ -4,6 +4,10 @@ "turn_off": "Apaga {entity_name}", "turn_on": "Enc\u00e9n {entity_name}" }, + "condtion_type": { + "is_off": "{entity_name} est\u00e0 apagat", + "is_on": "{entity_name} est\u00e0 enc\u00e8s" + }, "trigger_type": { "turned_off": "{entity_name} s'ha apagat", "turned_on": "{entity_name} s'ha enc\u00e8s" diff --git a/homeassistant/components/hisense_aehw4a1/.translations/ca.json b/homeassistant/components/hisense_aehw4a1/.translations/ca.json new file mode 100644 index 00000000000000..7b237aecdab445 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/ca.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'ha trobat cap dispositiu AEH-W4A1 a la xarxa.", + "single_instance_allowed": "Nom\u00e9s \u00e9s possible una \u00fanica configuraci\u00f3 del AEH-W4A1 de Hisense." + }, + "step": { + "confirm": { + "description": "Vols configurar AEH-W4A1 de Hisense?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/en.json b/homeassistant/components/hisense_aehw4a1/.translations/en.json new file mode 100644 index 00000000000000..b70fc8f05ecdc7 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No Hisense AEH-W4A1 devices found on the network.", + "single_instance_allowed": "Only a single configuration of Hisense AEH-W4A1 is possible." + }, + "step": { + "confirm": { + "description": "Do you want to set up Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/ca.json b/homeassistant/components/vacuum/.translations/ca.json index df2ea439e0e1f1..ee69152ed5c548 100644 --- a/homeassistant/components/vacuum/.translations/ca.json +++ b/homeassistant/components/vacuum/.translations/ca.json @@ -1,10 +1,16 @@ { "device_automation": { + "action_type": { + "clean": "Fes que {entity_name} netegi", + "dock": "Fes que {entity_name} torni a la base" + }, "condtion_type": { - "is_cleaning": "{entity_name} est\u00e0 netejant" + "is_cleaning": "{entity_name} est\u00e0 netejant", + "is_docked": "{entity_name} est\u00e0 acoblada" }, "trigger_type": { - "cleaning": "{entity_name} ha comen\u00e7at a netejar" + "cleaning": "{entity_name} ha comen\u00e7at a netejar", + "docked": "{entity_name} acoblada" } } } \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/ca.json b/homeassistant/components/wled/.translations/ca.json index b86eefd62c83c4..347dc576d91a63 100644 --- a/homeassistant/components/wled/.translations/ca.json +++ b/homeassistant/components/wled/.translations/ca.json @@ -1,14 +1,23 @@ { "config": { + "abort": { + "already_configured": "Aquest dispositiu WLED ja est\u00e0 configurat.", + "connection_error": "No s'ha pogut connectar amb el dispositiu WLED." + }, + "error": { + "connection_error": "No s'ha pogut connectar amb el dispositiu WLED." + }, "flow_title": "WLED: {name}", "step": { "user": { "data": { "host": "Amfitri\u00f3 o adre\u00e7a IP" }, + "description": "Configura el teu WLED per integrar-lo amb Home Assistant.", "title": "Enlla\u00e7a el teu WLED" }, "zeroconf_confirm": { + "description": "Vols afegir el WLED `{name}` a Home Assistant?", "title": "Dispositiu WLED descobert" } }, From 5e20817ea44e47de8e0ce100cbb277ed6467481a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 16 Nov 2019 07:22:06 +0100 Subject: [PATCH 1583/3953] Updated frontend to 20191115.0 (#28797) --- 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 b7506f599ef60e..6c59ea38fe49bb 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191114.0" + "home-assistant-frontend==20191115.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dfe53ed2e19e15..3688d6dfa38580 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191114.0 +home-assistant-frontend==20191115.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 355485cf50d41b..17641c62e31026 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191114.0 +home-assistant-frontend==20191115.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3a2f2f92a53ed..43aa2253d77094 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191114.0 +home-assistant-frontend==20191115.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From cd335bd434e79f22090ff027068675268b977df1 Mon Sep 17 00:00:00 2001 From: Morten Trab Date: Sat, 16 Nov 2019 09:45:43 +0100 Subject: [PATCH 1584/3953] Fix Repetier integration entity indexing (#28766) * Fixed multi extruder/beds/chambers index issue, #28130 * Switched from .format to f style name formatting * Fixed incorrect indexing * Removed VS files * Removed not need temp_id subtraction * Removed VS files * Fixed access mode * Fixed access mode * Fixing access mode - again --- homeassistant/components/repetier/__init__.py | 8 ++++---- homeassistant/components/repetier/sensor.py | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 12975baca91e49..1d6026a8754319 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -108,7 +108,7 @@ def has_all_unique_names(value): SENSOR_TYPES = { - # Type, Unit, Icon + # Type, Unit, Icon, post "bed_temperature": ["temperature", TEMP_CELSIUS, "mdi:thermometer", "_bed_"], "extruder_temperature": [ "temperature", @@ -248,12 +248,12 @@ def _load_entities(self): if prop_data is None: continue for idx, _ in enumerate(prop_data): - info["temp_id"] = idx - sensor_info.append(info) + prop_info = info.copy() + prop_info["temp_id"] = idx + sensor_info.append(prop_info) else: info["temp_id"] = None sensor_info.append(info) - self._known_entities.add(known) if not sensor_info: diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index e692ffc078f2b4..5936b5c33436c4 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -35,11 +35,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): printer_id = info["printer_id"] sensor_type = info["sensor_type"] temp_id = info["temp_id"] - name = info["name"] + name = f"{info['name']}{SENSOR_TYPES[sensor_type][3]}" if temp_id is not None: - name = "{}{}{}".format(name, SENSOR_TYPES[sensor_type][3], temp_id) - else: - name = "{}{}".format(name, SENSOR_TYPES[sensor_type][3]) + _LOGGER.debug("%s Temp_id: %s", sensor_type, temp_id) + name = f"{name}{temp_id}" sensor_class = sensor_map[sensor_type] entity = sensor_class(api, temp_id, name, printer_id, sensor_type) entities.append(entity) From d8ca2e9d710dbd8091baafd03c8445c1457a6820 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 16 Nov 2019 09:56:26 +0100 Subject: [PATCH 1585/3953] Upgrade sqlalchemy to 1.3.11 (#28721) --- 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 1f00cf89f158be..4ad000866db081 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/integrations/recorder", "requirements": [ - "sqlalchemy==1.3.10" + "sqlalchemy==1.3.11" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index fa641adc839e0f..39435524c20cdc 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/integrations/sql", "requirements": [ - "sqlalchemy==1.3.10" + "sqlalchemy==1.3.11" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3688d6dfa38580..72605a3475e2e5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.10 +sqlalchemy==1.3.11 voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index 17641c62e31026..b00928fbff825d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1841,7 +1841,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.10 +sqlalchemy==1.3.11 # homeassistant.components.starlingbank starlingbank==3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43aa2253d77094..2895756d3334f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -565,7 +565,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.10 +sqlalchemy==1.3.11 # homeassistant.components.statsd statsd==3.2.1 From 16f8ae153569587137d016506a8ddb52fb127bca Mon Sep 17 00:00:00 2001 From: John Mihalic <2854333+mezz64@users.noreply.github.com> Date: Sat, 16 Nov 2019 03:57:04 -0500 Subject: [PATCH 1586/3953] Bump pyHik library to 0.2.5 for id, tamper fixes (#28807) --- homeassistant/components/hikvision/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index 11775ed3ae085d..d517189646461a 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -3,7 +3,7 @@ "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", "requirements": [ - "pyhik==0.2.4" + "pyhik==0.2.5" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index b00928fbff825d..e6dbb050c7c6c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1247,7 +1247,7 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.hikvision -pyhik==0.2.4 +pyhik==0.2.5 # homeassistant.components.hive pyhiveapi==0.2.19.3 From 2aee366a1fba5b3ad3d7ad69efbd785c994ab874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 16 Nov 2019 10:59:26 +0200 Subject: [PATCH 1587/3953] Upgrade pylint, tweak config (#28798) * Upgrade pylint to 2.4.4 and astroid to 2.3.3 https://pylint.readthedocs.io/en/latest/whatsnew/changelog.html#what-s-new-in-pylint-2-4-4 https://github.com/PyCQA/astroid/blob/astroid-2.3.3/ChangeLog * Disable pylint score and persistence We don't use scoring, and it can be supposedly incorrect (so worse than ignorable) when using more than one parallel job. --- pylintrc | 3 ++- requirements_test.txt | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pylintrc b/pylintrc index ff47af6087b8c0..7794f61e500d2f 100644 --- a/pylintrc +++ b/pylintrc @@ -3,6 +3,7 @@ ignore=tests # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. jobs=2 +persistent=no [BASIC] good-names=id,i,j,k,ex,Run,_,fp @@ -49,7 +50,7 @@ disable= unused-argument [REPORTS] -reports=no +score=no [TYPECHECK] # For attrs diff --git a/requirements_test.txt b/requirements_test.txt index f8fba663bcb502..dae60b746530cd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,8 +8,8 @@ codecov==2.0.15 mock-open==1.3.1 mypy==0.740 pre-commit==1.20.0 -pylint==2.4.3 -astroid==2.3.2 +pylint==2.4.4 +astroid==2.3.3 pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 From d88ca0f5cb699c83cca21951cbc16eed0778ee28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 16 Nov 2019 11:22:07 +0200 Subject: [PATCH 1588/3953] Lint exclusions (#28655) * Remove malformed pylint disable markers * Remove some unused imports * Remove some unneeded lint exclusions * Remove more unneeded lint exclusions * Add specific codes to all noqa's --- homeassistant/auth/permissions/__init__.py | 16 ++------------ homeassistant/auth/permissions/models.py | 4 ++-- homeassistant/auth/permissions/util.py | 5 +++-- .../components/air_quality/__init__.py | 2 +- .../alarm_control_panel/__init__.py | 2 +- homeassistant/components/aws/__init__.py | 2 +- .../components/binary_sensor/__init__.py | 2 +- homeassistant/components/calendar/__init__.py | 2 +- homeassistant/components/camera/__init__.py | 2 +- homeassistant/components/climate/__init__.py | 2 +- homeassistant/components/cover/__init__.py | 2 +- homeassistant/components/daikin/__init__.py | 2 +- .../components/device_tracker/__init__.py | 4 ++-- .../dlib_face_detect/image_processing.py | 2 +- homeassistant/components/elkm1/__init__.py | 4 ++-- homeassistant/components/esphome/__init__.py | 2 +- homeassistant/components/fan/__init__.py | 2 +- .../components/geo_location/__init__.py | 2 +- homeassistant/components/homekit/__init__.py | 2 +- .../components/homekit_controller/__init__.py | 2 +- homeassistant/components/http/__init__.py | 4 ++-- homeassistant/components/ipma/__init__.py | 4 ++-- homeassistant/components/light/__init__.py | 2 +- homeassistant/components/lock/__init__.py | 2 +- .../components/mailgun/config_flow.py | 2 +- .../components/media_player/__init__.py | 2 +- homeassistant/components/met/__init__.py | 4 ++-- .../components/mobile_app/helpers.py | 2 +- homeassistant/components/mqtt/__init__.py | 2 +- .../components/owntracks/config_flow.py | 2 +- homeassistant/components/point/__init__.py | 2 +- homeassistant/components/remote/__init__.py | 2 +- homeassistant/components/sensor/__init__.py | 2 +- .../components/smartthings/__init__.py | 2 +- homeassistant/components/switch/__init__.py | 2 +- .../components/tellduslive/__init__.py | 2 +- homeassistant/components/tellduslive/const.py | 2 +- .../components/tensorflow/image_processing.py | 10 ++++----- homeassistant/components/toon/__init__.py | 4 ++-- homeassistant/components/tradfri/__init__.py | 2 +- homeassistant/components/tradfri/const.py | 2 +- homeassistant/components/vacuum/__init__.py | 2 +- .../components/water_heater/__init__.py | 2 +- homeassistant/components/weather/__init__.py | 2 +- homeassistant/components/zha/__init__.py | 4 ++-- .../components/zha/core/channels/__init__.py | 22 +++++++++---------- .../components/zha/core/decorators.py | 2 +- .../components/zha/core/registries.py | 2 +- homeassistant/components/zwave/__init__.py | 2 +- homeassistant/core.py | 2 +- homeassistant/exceptions.py | 2 +- homeassistant/loader.py | 4 ++-- homeassistant/monkey_patch.py | 2 +- homeassistant/util/__init__.py | 2 +- homeassistant/util/decorator.py | 2 +- homeassistant/util/yaml/loader.py | 6 ++--- tests/common.py | 4 ++-- tests/components/google_translate/test_tts.py | 2 +- .../logi_circle/test_config_flow.py | 6 ++--- tests/components/marytts/test_tts.py | 2 +- tests/components/mobile_app/test_entity.py | 14 ++++-------- tests/components/mobile_app/test_http_api.py | 5 ++--- tests/components/mobile_app/test_webhook.py | 17 +++++--------- tests/components/onboarding/test_views.py | 4 ++-- tests/components/point/test_config_flow.py | 6 ++--- tests/components/qwikswitch/test_init.py | 4 ++-- tests/components/voicerss/test_tts.py | 2 +- tests/components/yandextts/test_tts.py | 2 +- tests/helpers/test_check_config.py | 1 - tests/helpers/test_service.py | 2 +- tests/scripts/test_check_config.py | 1 - .../test_package/__init__.py | 2 +- tests/util/test_async.py | 2 +- 73 files changed, 113 insertions(+), 142 deletions(-) diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 25253a1601cdeb..f0e093953e1b18 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -1,18 +1,6 @@ """Permissions for Home Assistant.""" import logging -from typing import ( # noqa: F401 - cast, - Any, - Callable, - Dict, - List, - Mapping, - Optional, - Set, - Tuple, - Union, - TYPE_CHECKING, -) +from typing import Any, Callable, Optional import voluptuous as vol @@ -20,7 +8,7 @@ from .models import PermissionLookup from .types import PolicyType from .entities import ENTITY_POLICY_SCHEMA, compile_entities -from .merge import merge_policies # noqa +from .merge import merge_policies # noqa: F401 from .util import test_all diff --git a/homeassistant/auth/permissions/models.py b/homeassistant/auth/permissions/models.py index 31bea635bbe3e1..1224ea07b23a75 100644 --- a/homeassistant/auth/permissions/models.py +++ b/homeassistant/auth/permissions/models.py @@ -5,8 +5,8 @@ if TYPE_CHECKING: # pylint: disable=unused-import - from homeassistant.helpers import entity_registry as ent_reg # noqa - from homeassistant.helpers import device_registry as dev_reg # noqa + from homeassistant.helpers import entity_registry as ent_reg # noqa: F401 + from homeassistant.helpers import device_registry as dev_reg # noqa: F401 @attr.s(slots=True) diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 109a5dc04ae252..4d38e0a639c07f 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -21,8 +21,9 @@ def lookup_all( def compile_policy( policy: CategoryType, subcategories: SubCatLookupType, perm_lookup: PermissionLookup -) -> Callable[[str, str], bool]: # noqa +) -> Callable[[str, str], bool]: """Compile policy into a function that tests policy. + Subcategories are mapping key -> lookup function, ordered by highest priority first. """ @@ -80,7 +81,7 @@ def apply_policy_funcs(object_id: str, key: str) -> bool: def _gen_dict_test_func( perm_lookup: PermissionLookup, lookup_func: LookupFunc, lookup_dict: SubCategoryDict -) -> Callable[[str, str], Optional[bool]]: # noqa +) -> Callable[[str, str], Optional[bool]]: """Generate a lookup function.""" def test_value(object_id: str, key: str) -> Optional[bool]: diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 3d8bb92572a9eb..00308c40b3621f 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -3,7 +3,7 @@ import logging from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index ab0b810ee83ead..6faad5dd51f194 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -14,7 +14,7 @@ SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS, ) -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index 780a65b2d47336..b553b7eafd67d8 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -12,7 +12,7 @@ from homeassistant.helpers import config_validation as cv, discovery # Loading the config flow file will register the flow -from . import config_flow # noqa +from . import config_flow # noqa: F401 from .const import ( CONF_ACCESS_KEY_ID, CONF_CONTEXT, diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 9af6a10c425262..e5f5dc94ff12c8 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -8,7 +8,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 32817242642c5a..ca240925cf5167 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -7,7 +7,7 @@ from homeassistant.components import http from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, time_period_str, diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 68cd1f51dda198..58b6db139f5291 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -24,7 +24,7 @@ from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 0c8d6103b12383..f0d350fc0baae0 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -17,7 +17,7 @@ TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index cfac143a5d80f7..d57bf678a69717 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -9,7 +9,7 @@ from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 390e80d0916b51..7d476e17647f7b 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -15,7 +15,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle -from . import config_flow # noqa pylint_disable=unused-import +from . import config_flow # noqa: F401 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 84bc76e0b04738..25e33d2a2db2fb 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -13,11 +13,11 @@ from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME from . import legacy, setup -from .config_entry import ( # noqa # pylint: disable=unused-import +from .config_entry import ( # noqa: F401 pylint: disable=unused-import async_setup_entry, async_unload_entry, ) -from .legacy import DeviceScanner # noqa # pylint: disable=unused-import +from .legacy import DeviceScanner # noqa: F401 pylint: disable=unused-import from .const import ( ATTR_ATTRIBUTES, ATTR_BATTERY, diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index cdd8bc101b5df8..6e5c49e7aba4bd 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -12,7 +12,7 @@ ) # pylint: disable=unused-import -from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa +from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa: F401 from homeassistant.core import split_entity_id _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index d257c46839cc34..601309590c8c17 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -14,10 +14,10 @@ CONF_TEMPERATURE_UNIT, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant, callback # noqa +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType # noqa +from homeassistant.helpers.typing import ConfigType DOMAIN = "elkm1" diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index a669726ca38969..2ad24e6f75ec43 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -38,7 +38,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType # Import config flow so that it's added to the registry -from .config_flow import EsphomeFlowHandler # noqa +from .config_flow import EsphomeFlowHandler # noqa: F401 from .entry_data import ( DATA_KEY, DISPATCHER_ON_DEVICE_UPDATE, diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 82f4d37938c7d8..4c6cee2927c16e 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -11,7 +11,7 @@ from homeassistant.loader import bind_hass from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 3c270f2c52174e..e5c587f93ede18 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -4,7 +4,7 @@ from typing import Optional from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4c300e0a934e9f..bb525271cec3e7 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -363,7 +363,7 @@ def start(self, *args): self.status = STATUS_WAIT # pylint: disable=unused-import - from . import ( # noqa F401 + from . import ( # noqa: F401 type_covers, type_fans, type_lights, diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 6a6492847225f1..4ca54099d24d09 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -13,7 +13,7 @@ from .config_flow import HomekitControllerFlowHandler # noqa: F401 from .connection import get_accessory_information, HKDevice from .const import CONTROLLER, ENTITY_MAP, KNOWN_DEVICES -from .const import DOMAIN # noqa: pylint: disable=unused-import +from .const import DOMAIN from .storage import EntityMapStorage _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4df606a3c1b29c..4d3985a7af3bb8 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -20,11 +20,11 @@ from .auth import setup_auth from .ban import setup_bans -from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER, KEY_REAL_IP # noqa +from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_HASS_USER, KEY_REAL_IP # noqa: F401 from .cors import setup_cors from .real_ip import setup_real_ip from .static import CACHE_HEADERS, CachingStaticResource -from .view import HomeAssistantView # noqa +from .view import HomeAssistantView # noqa: F401 # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 39cb670999fadb..702e12a8a63358 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,7 +1,7 @@ """Component for the Portuguese weather service - IPMA.""" from homeassistant.core import Config, HomeAssistant -from .config_flow import IpmaFlowHandler # noqa -from .const import DOMAIN # noqa +from .config_flow import IpmaFlowHandler # noqa: F401 +from .const import DOMAIN # noqa: F401 DEFAULT_NAME = "ipma" diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index fbd908a9e45d51..2ca5e496b10aca 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -19,7 +19,7 @@ ) from homeassistant.exceptions import UnknownUser, Unauthorized import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ENTITY_SERVICE_SCHEMA, diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 503bd3a8c78316..341df1bb28a6f7 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -8,7 +8,7 @@ from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, diff --git a/homeassistant/components/mailgun/config_flow.py b/homeassistant/components/mailgun/config_flow.py index e1c6abbc1cab0d..c575b4c0354cd3 100644 --- a/homeassistant/components/mailgun/config_flow.py +++ b/homeassistant/components/mailgun/config_flow.py @@ -7,7 +7,7 @@ DOMAIN, "Mailgun Webhook", { - "mailgun_url": "https://documentation.mailgun.com/en/latest/user_manual.html#webhooks", # noqa: E501 pylint: disable=line-too-long + "mailgun_url": "https://documentation.mailgun.com/en/latest/user_manual.html#webhooks", "docs_url": "https://www.home-assistant.io/integrations/mailgun/", }, ) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 98da19fd98efe0..c783772365bf28 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -39,7 +39,7 @@ ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 63b95b03821abf..305038c3e6a622 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -1,7 +1,7 @@ """The met component.""" from homeassistant.core import Config, HomeAssistant -from .config_flow import MetFlowHandler # noqa -from .const import DOMAIN # noqa +from .config_flow import MetFlowHandler # noqa: F401 +from .const import DOMAIN # noqa: F401 async def async_setup(hass: HomeAssistant, config: Config) -> bool: diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 2fb949720d63c0..cad25f371dda6b 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -111,7 +111,7 @@ def error_response( def supports_encryption() -> bool: """Test if we support encryption.""" try: - import nacl # noqa pylint: disable=unused-import + import nacl # noqa: F401 pylint: disable=unused-import return True except OSError: diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 2fab599ac3f8e0..70523a840a398b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -47,7 +47,7 @@ from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow -from . import config_flow, discovery, server # noqa pylint: disable=unused-import +from . import config_flow, discovery, server # noqa: F401 pylint: disable=unused-import from .const import ( CONF_BROKER, CONF_DISCOVERY, diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index a59cd869c74e61..5034114293f375 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -12,7 +12,7 @@ def supports_encryption(): """Test if we support encryption.""" try: - import nacl # noqa pylint: disable=unused-import + import nacl # noqa: F401 pylint: disable=unused-import return True except OSError: diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index e9885891553b9e..9c5ec4d5529d2f 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import as_local, parse_datetime, utc_from_timestamp -from . import config_flow # noqa pylint_disable=unused-import +from . import config_flow from .const import ( CONF_WEBHOOK_URL, DOMAIN, diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 450b1c123c32c3..71059b98f3567c 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -16,7 +16,7 @@ SERVICE_TOGGLE, ) from homeassistant.components import group -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 9ca11b5266adf1..53e4b0ffcf717f 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -15,7 +15,7 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ) -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 93f7cbb8f32997..9787fb53917c14 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .config_flow import SmartThingsFlowHandler # noqa +from .config_flow import SmartThingsFlowHandler # noqa: F401 from .const import ( CONF_APP_ID, CONF_INSTALLED_APP_ID, diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index aa7459d1d3cec7..26d5658d66864b 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -7,7 +7,7 @@ from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 7234127a15231a..313699e6f1c75a 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later -from . import config_flow # noqa pylint_disable=unused-import +from . import config_flow # noqa: F401 from .const import ( CONF_HOST, DOMAIN, diff --git a/homeassistant/components/tellduslive/const.py b/homeassistant/components/tellduslive/const.py index b352f339d22bfb..8d9f28cc5cf9f5 100644 --- a/homeassistant/components/tellduslive/const.py +++ b/homeassistant/components/tellduslive/const.py @@ -1,7 +1,7 @@ """Consts used by TelldusLive.""" from datetime import timedelta -from homeassistant.const import ( # noqa pylint: disable=unused-import +from homeassistant.const import ( # noqa: F401 pylint: disable=unused-import ATTR_BATTERY_LEVEL, CONF_HOST, CONF_TOKEN, diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index ea73d52fe4a6fe..c9c1b00aae46fb 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -89,25 +89,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: # Verify that the TensorFlow Object Detection API is pre-installed - # pylint: disable=unused-import,unused-variable os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" # These imports shouldn't be moved to the top, because they depend on code from the model_dir. # (The model_dir is created during the manual setup process. See integration docs.) - import tensorflow as tf # noqa - from object_detection.utils import label_map_util # noqa + import tensorflow as tf + from object_detection.utils import label_map_util except ImportError: - # pylint: disable=line-too-long _LOGGER.error( "No TensorFlow Object Detection library found! Install or compile " "for your system following instructions here: " "https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/installation.md" - ) # noqa + ) return try: # Display warning that PIL will be used if no OpenCV is found. # pylint: disable=unused-import,unused-variable - import cv2 # noqa + import cv2 # noqa: F401 except ImportError: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 4a3afb9b87b408..ed627fdc9242bd 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -13,7 +13,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect -from . import config_flow # noqa pylint_disable=unused-import +from . import config_flow # noqa: F401 from .const import ( CONF_CLIENT_ID, CONF_CLIENT_SECRET, @@ -139,7 +139,7 @@ def update(self, now=None): """Update all Toon data and notify entities.""" # Ignore the TTL meganism from client library # It causes a lots of issues, hence we take control over caching - self._toon._clear_cache() # noqa pylint: disable=W0212 + self._toon._clear_cache() # pylint: disable=W0212 # Gather data from client library (single API call) self.gas = self._toon.gas diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index 9d1a43b240f708..bb44564731085f 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -10,7 +10,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.util.json import load_json -from . import config_flow # noqa pylint_disable=unused-import +from . import config_flow # noqa: F401 from .const import ( DOMAIN, CONFIG_FILE, diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 038f0e91c76cca..01d2f18501d836 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -1,6 +1,6 @@ """Consts used by Tradfri.""" from homeassistant.components.light import SUPPORT_TRANSITION, SUPPORT_BRIGHTNESS -from homeassistant.const import CONF_HOST # noqa pylint: disable=unused-import +from homeassistant.const import CONF_HOST # noqa: F401 pylint: disable=unused-import ATTR_DIMMER = "dimmer" ATTR_HUE = "hue" diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index ace3f6106060aa..c1f1131a52f611 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -18,7 +18,7 @@ ) from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index c41381fe5fa5de..6e7b918c289e52 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -9,7 +9,7 @@ from homeassistant.util.temperature import convert as convert_temperature from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index fd122f66ac2b2f..bdeedd4cd6be70 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -3,7 +3,7 @@ import logging from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS -from homeassistant.helpers.config_validation import ( # noqa +from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index ab686a97989b43..be84795d962b79 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -8,7 +8,7 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE # Loading the config flow file will register the flow -from . import config_flow # noqa # pylint: disable=unused-import +from . import config_flow # noqa: F401 pylint: disable=unused-import from . import api from .core import ZHAGateway from .core.const import ( @@ -101,7 +101,7 @@ async def async_setup_entry(hass, config_entry): # needs to be done here so that the ZHA module is finished loading # before zhaquirks is imported # pylint: disable=W0611, W0612 - import zhaquirks # noqa + import zhaquirks # noqa: F401 zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 66a31ff8f21f95..29cecb7784e9a1 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -395,14 +395,14 @@ def cluster_command(self, tsn, command_id, 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 +from . import closures # noqa: F401 +from . import general # noqa: F401 +from . import homeautomation # noqa: F401 +from . import hvac # noqa: F401 +from . import lighting # noqa: F401 +from . import lightlink # noqa: F401 +from . import manufacturerspecific # noqa: F401 +from . import measurement # noqa: F401 +from . import protocol # noqa: F401 +from . import security # noqa: F401 +from . import smartenergy # noqa: F401 diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py index 4148cff6ca9f1e..c416548dbe97e4 100644 --- a/homeassistant/components/zha/core/decorators.py +++ b/homeassistant/components/zha/core/decorators.py @@ -1,7 +1,7 @@ """Decorators for ZHA core registries.""" from typing import Callable, TypeVar, Union -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # noqa pylint: disable=invalid-name +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name class DictRegistry(dict): diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 571e77d4fae302..dd007e125a630c 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -27,7 +27,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH # importing channels updates registries -from . import channels # noqa pylint: disable=wrong-import-position,unused-import +from . import channels # noqa: F401 pylint: disable=wrong-import-position,unused-import from .const import ( CONTROLLER, SENSOR_ACCELERATION, diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 97a904c5d994fc..293cc45273f34a 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -35,7 +35,7 @@ ) from . import const -from . import config_flow # noqa pylint: disable=unused-import +from . import config_flow # noqa: F401 pylint: disable=unused-import from . import websocket_api as wsapi from .const import ( CONF_AUTOHEAL, diff --git a/homeassistant/core.py b/homeassistant/core.py index 01c5561d939f90..f5e9176910756c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -68,7 +68,7 @@ from homeassistant import util import homeassistant.util.dt as dt_util from homeassistant.util import location, slugify -from homeassistant.util.unit_system import ( # NOQA +from homeassistant.util.unit_system import ( UnitSystem, IMPERIAL_SYSTEM, METRIC_SYSTEM, diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 89caf730ad7fa6..0000a4b176ea01 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -5,7 +5,7 @@ # pylint: disable=using-constant-test if TYPE_CHECKING: # pylint: disable=unused-import - from .core import Context # noqa + from .core import Context # noqa: F401 class HomeAssistantError(Exception): diff --git a/homeassistant/loader.py b/homeassistant/loader.py index e32303ecfabb3c..9c8d7d2aecbb44 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -28,9 +28,9 @@ # Typing imports that create a circular dependency # pylint: disable=using-constant-test,unused-import if TYPE_CHECKING: - from homeassistant.core import HomeAssistant # noqa + from homeassistant.core import HomeAssistant -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # noqa pylint: disable=invalid-name +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name DEPENDENCY_BLACKLIST = {"config"} diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py index 2c5227988d316c..c6e2e66ab131a5 100644 --- a/homeassistant/monkey_patch.py +++ b/homeassistant/monkey_patch.py @@ -68,6 +68,6 @@ def find_module(self, fullname: str, path: Any = None) -> None: sys.path.insert(0, AsyncioImportFinder.PATH_TRIGGER) try: - import _asyncio # noqa + import _asyncio # noqa: F401 except ImportError: pass diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 0f4f3c867ad8d2..408d1e370d475b 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -15,7 +15,7 @@ TypeVar, Callable, KeysView, - Union, # noqa + Union, Iterable, Coroutine, ) diff --git a/homeassistant/util/decorator.py b/homeassistant/util/decorator.py index ecf1284b1b1c45..83c63711c7dd36 100644 --- a/homeassistant/util/decorator.py +++ b/homeassistant/util/decorator.py @@ -1,7 +1,7 @@ """Decorator utility functions.""" from typing import Callable, TypeVar -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # noqa pylint: disable=invalid-name +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name class Registry(dict): diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index bbbe22c2ba8be2..47635cd66c3df1 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -76,14 +76,14 @@ def _add_reference( ... -@overload # noqa: F811 +@overload def _add_reference( obj: Union[str, NodeStrClass], loader: yaml.SafeLoader, node: yaml.nodes.Node ) -> NodeStrClass: ... -@overload # noqa: F811 +@overload def _add_reference( obj: DICT_T, loader: yaml.SafeLoader, node: yaml.nodes.Node ) -> DICT_T: @@ -93,7 +93,7 @@ def _add_reference( # pylint: enable=pointless-statement -def _add_reference( # type: ignore # noqa: F811 +def _add_reference( # type: ignore obj, loader: SafeLineLoader, node: yaml.nodes.Node ): """Add file reference information to an object.""" diff --git a/tests/common.py b/tests/common.py index f40019c5d24272..e652e10cc542e7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -56,7 +56,7 @@ from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.components.device_automation import ( # noqa +from homeassistant.components.device_automation import ( # noqa: F401 _async_get_device_automations as async_get_device_automations, _async_get_device_automation_capabilities as async_get_device_automation_capabilities, ) @@ -1084,7 +1084,7 @@ class hashdict(dict): """ - def __key(self): # noqa: D105 no docstring + def __key(self): return tuple(sorted(self.items())) def __repr__(self): # noqa: D105 no docstring diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index e9d19a8f4f1a1c..13f9eb88fce5ef 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -14,7 +14,7 @@ from tests.common import get_test_home_assistant, assert_setup_component, mock_service -from tests.components.tts.test_init import mutagen_mock # noqa +from tests.components.tts.test_init import mutagen_mock # noqa: F401 class TestTTSGooglePlatform: diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index ebfae2af45132b..02a9437a225873 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -76,7 +76,7 @@ async def test_step_import(hass, mock_logi_circle): # pylint: disable=W0621 async def test_full_flow_implementation( hass, mock_logi_circle -): # noqa pylint: disable=W0621 +): # pylint: disable=W0621 """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation( hass, @@ -154,7 +154,7 @@ async def test_abort_if_already_setup(hass): ) async def test_abort_if_authorize_fails( hass, mock_logi_circle, side_effect, error -): # noqa pylint: disable=W0621 +): # pylint: disable=W0621 """Test we abort if authorizing fails.""" flow = init_config_flow(hass) mock_logi_circle.LogiCircle().authorize.side_effect = side_effect @@ -206,7 +206,7 @@ async def test_callback_view_rejects_missing_code(hass): async def test_callback_view_accepts_code( hass, mock_logi_circle -): # noqa pylint: disable=W0621 +): # pylint: disable=W0621 """Test the auth callback view handles requests with auth code.""" init_config_flow(hass) view = LogiCircleAuthCallbackView() diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 275cf17e22afbe..5692d72d388ae8 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -12,7 +12,7 @@ from tests.common import get_test_home_assistant, assert_setup_component, mock_service -from tests.components.tts.test_init import mutagen_mock # noqa +from tests.components.tts.test_init import mutagen_mock # noqa: F401 class TestTTSMaryTTSPlatform: diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index f2693f25585a5b..94a4e76ae848ac 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -1,13 +1,11 @@ """Entity tests for mobile_app.""" -# pylint: disable=redefined-outer-name,unused-import + import logging _LOGGER = logging.getLogger(__name__) -async def test_sensor( - hass, create_registrations, webhook_client -): # noqa: F401, F811, E501 +async def test_sensor(hass, create_registrations, webhook_client): """Test that sensors can be registered and updated.""" webhook_id = create_registrations[1]["webhook_id"] webhook_url = "/api/webhook/{}".format(webhook_id) @@ -67,9 +65,7 @@ async def test_sensor( assert updated_entity.state == "123" -async def test_sensor_must_register( - hass, create_registrations, webhook_client # noqa: F401, F811, E501 -): # noqa: F401, F811, E501 +async def test_sensor_must_register(hass, create_registrations, webhook_client): """Test that sensors must be registered before updating.""" webhook_id = create_registrations[1]["webhook_id"] webhook_url = "/api/webhook/{}".format(webhook_id) @@ -88,9 +84,7 @@ async def test_sensor_must_register( assert json["battery_state"]["error"]["code"] == "not_registered" -async def test_sensor_id_no_dupes( - hass, create_registrations, webhook_client # noqa: F401, F811, E501 -): # noqa: F401, F811, E501 +async def test_sensor_id_no_dupes(hass, create_registrations, webhook_client): """Test that sensors must have a unique ID.""" webhook_id = create_registrations[1]["webhook_id"] webhook_url = "/api/webhook/{}".format(webhook_id) diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py index 3c05f495e403ec..158d3ffe213571 100644 --- a/tests/components/mobile_app/test_http_api.py +++ b/tests/components/mobile_app/test_http_api.py @@ -12,9 +12,8 @@ async def test_registration(hass, hass_client): """Test that registrations happen.""" try: - # pylint: disable=unused-import - from nacl.secret import SecretBox # noqa: F401 - from nacl.encoding import Base64Encoder # noqa: F401 + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder except (ImportError, OSError): pytest.skip("libnacl/libsodium is not installed") return diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 9274aa60a84149..1ea2d50d8d8208 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,5 +1,5 @@ """Webhook tests for mobile_app.""" -# pylint: disable=redefined-outer-name,unused-import + import logging import pytest @@ -29,9 +29,7 @@ async def test_webhook_handle_render_template(create_registrations, webhook_clie assert json == {"one": "Hello world"} -async def test_webhook_handle_call_services( - hass, create_registrations, webhook_client -): # noqa: E501 F811 +async def test_webhook_handle_call_services(hass, create_registrations, webhook_client): """Test that we call services properly.""" calls = async_mock_service(hass, "test", "mobile_app") @@ -68,9 +66,7 @@ def store_event(event): assert events[0].data["hello"] == "yo world" -async def test_webhook_update_registration( - webhook_client, hass_client -): # noqa: E501 F811 +async def test_webhook_update_registration(webhook_client, hass_client): """Test that a we can update an existing registration via webhook.""" authed_api_client = await hass_client() register_resp = await authed_api_client.post( @@ -156,7 +152,7 @@ async def test_webhook_handle_get_config(hass, create_registrations, webhook_cli async def test_webhook_returns_error_incorrect_json( webhook_client, create_registrations, caplog -): # noqa: E501 F811 +): """Test that an error is returned when JSON is invalid.""" resp = await webhook_client.post( "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), data="not json" @@ -171,9 +167,8 @@ async def test_webhook_returns_error_incorrect_json( async def test_webhook_handle_decryption(webhook_client, create_registrations): """Test that we can encrypt/decrypt properly.""" try: - # pylint: disable=unused-import - from nacl.secret import SecretBox # noqa: F401 - from nacl.encoding import Base64Encoder # noqa: F401 + from nacl.secret import SecretBox + from nacl.encoding import Base64Encoder except (ImportError, OSError): pytest.skip("libnacl/libsodium is not installed") return diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index bcb430b828d1c3..7881b75ee99a80 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -9,13 +9,13 @@ from homeassistant.components.onboarding import const, views from tests.common import CLIENT_ID, register_auth_provider -from tests.components.met.conftest import mock_weather # noqa +from tests.components.met.conftest import mock_weather # noqa: F401 from . import mock_storage @pytest.fixture(autouse=True) -def always_mock_weather(mock_weather): # noqa +def always_mock_weather(mock_weather): # noqa: F811 """Mock the Met weather provider.""" pass diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index 664cb412f755dc..ec808b5fd80fa8 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -66,9 +66,7 @@ async def test_abort_if_already_setup(hass): assert result["reason"] == "already_setup" -async def test_full_flow_implementation( - hass, mock_pypoint -): # noqa pylint: disable=W0621 +async def test_full_flow_implementation(hass, mock_pypoint): # pylint: disable=W0621 """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation(hass, "test-other", None, None) flow = init_config_flow(hass) @@ -106,7 +104,7 @@ async def test_step_import(hass, mock_pypoint): # pylint: disable=W0621 @pytest.mark.parametrize("is_authorized", [False]) async def test_wrong_code_flow_implementation( hass, mock_pypoint -): # noqa pylint: disable=W0621 +): # pylint: disable=W0621 """Test wrong code.""" flow = init_config_flow(hass) diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index 126e4c91c39374..e573e8cc293a17 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -57,7 +57,7 @@ def aioclient_mock(): yield mock_session -async def test_binary_sensor_device(hass, aioclient_mock): # noqa +async def test_binary_sensor_device(hass, aioclient_mock): # noqa: F811 """Test a binary sensor device.""" config = { "qwikswitch": { @@ -86,7 +86,7 @@ async def test_binary_sensor_device(hass, aioclient_mock): # noqa assert state_obj.state == "off" -async def test_sensor_device(hass, aioclient_mock): # noqa +async def test_sensor_device(hass, aioclient_mock): # noqa: F811 """Test a sensor device.""" config = { "qwikswitch": { diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index 01ac6049ac5895..0f2a0618096350 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -13,7 +13,7 @@ from tests.common import get_test_home_assistant, assert_setup_component, mock_service -from tests.components.tts.test_init import mutagen_mock # noqa +from tests.components.tts.test_init import mutagen_mock # noqa: F401 class TestTTSVoiceRSSPlatform: diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index 4af259e1355b8c..c532732ccc5213 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -11,7 +11,7 @@ ) from tests.common import get_test_home_assistant, assert_setup_component, mock_service -from tests.components.tts.test_init import mutagen_mock # noqa +from tests.components.tts.test_init import mutagen_mock # noqa: F401 class TestTTSYandexPlatform: diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 5228f0d4882228..0b34b263cafa23 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -1,6 +1,5 @@ """Test check_config helper.""" import logging -import os # noqa: F401 pylint: disable=unused-import from unittest.mock import patch from homeassistant.helpers.check_config import ( diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 22611b9f601c21..de8142a6374614 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -9,7 +9,7 @@ import pytest # To prevent circular import when running just this file -import homeassistant.components # noqa +import homeassistant.components # noqa: F401 from homeassistant import core as ha, exceptions from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID from homeassistant.setup import async_setup_component diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 5199f01807f5ef..8e1ffe63e84533 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,6 +1,5 @@ """Test check_config script.""" import logging -import os # noqa: F401 pylint: disable=unused-import from unittest.mock import patch import homeassistant.scripts.check_config as check_config diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index 0a02ef1e45f215..f5cd2c34edfd3d 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -1,5 +1,5 @@ """Provide a mock package component.""" -from .const import TEST # noqa +from .const import TEST # noqa: F401 DOMAIN = "test_package" diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 8dede61869c39b..9cda40c1b8b4b7 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -165,7 +165,7 @@ def test_run_callback_threadsafe_with_exception(self): def test_run_callback_threadsafe_with_invalid(self): """Test callback submission from thread to event loop on invalid.""" - callback = lambda: self.target_callback(invalid=True) # noqa + callback = lambda: self.target_callback(invalid=True) # noqa: E731 future = self.loop.run_in_executor(None, callback) with self.assertRaises(ValueError) as exc_context: self.loop.run_until_complete(future) From 8a28d5fbee3de131508bd2f5c9701586ccd587cf Mon Sep 17 00:00:00 2001 From: Tommy Larsson <45052383+larssont@users.noreply.github.com> Date: Sat, 16 Nov 2019 10:23:05 +0100 Subject: [PATCH 1589/3953] Add ombi password authentication option (#28742) * Add password authentication option * Add vol.Exclusive for auth config params --- homeassistant/components/ombi/__init__.py | 13 +++++++++---- homeassistant/components/ombi/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py index 860c7d4dcb4df0..750772ce8f5beb 100644 --- a/homeassistant/components/ombi/__init__.py +++ b/homeassistant/components/ombi/__init__.py @@ -7,6 +7,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, + CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME, @@ -57,13 +58,15 @@ def urlbase(value) -> str: { DOMAIN: vol.Schema( { - vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, + vol.Exclusive(CONF_API_KEY, "auth"): cv.string, + vol.Exclusive(CONF_PASSWORD, "auth"): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_URLBASE, default=DEFAULT_URLBASE): urlbase, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - } + }, + cv.has_at_least_one_key("auth"), ) }, extra=vol.ALLOW_EXTRA, @@ -77,12 +80,14 @@ def setup(hass, config): ssl=config[DOMAIN][CONF_SSL], host=config[DOMAIN][CONF_HOST], port=config[DOMAIN][CONF_PORT], - api_key=config[DOMAIN][CONF_API_KEY], - username=config[DOMAIN][CONF_USERNAME], urlbase=config[DOMAIN][CONF_URLBASE], + username=config[DOMAIN][CONF_USERNAME], + password=config[DOMAIN].get(CONF_PASSWORD), + api_key=config[DOMAIN].get(CONF_API_KEY), ) try: + ombi.authenticate() ombi.test_connection() except pyombi.OmbiError as err: _LOGGER.warning("Unable to setup Ombi: %s", err) diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json index fb6daf00f6646b..0407aa5a106dbe 100644 --- a/homeassistant/components/ombi/manifest.json +++ b/homeassistant/components/ombi/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/ombi/", "dependencies": [], "codeowners": ["@larssont"], - "requirements": ["pyombi==0.1.5"] + "requirements": ["pyombi==0.1.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index e6dbb050c7c6c5..4e95af20556cb7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1379,7 +1379,7 @@ pynzbgetapi==0.2.0 pyobihai==1.2.0 # homeassistant.components.ombi -pyombi==0.1.5 +pyombi==0.1.10 # homeassistant.components.openuv pyopenuv==1.0.9 From afbd966ba6714fdebbe8745e2877f9938af4a104 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 16 Nov 2019 10:25:25 +0100 Subject: [PATCH 1590/3953] Move imports in braviatv component (#27855) * Move imports in braviatv component * import braviarc directly * fixed import * directly import BraviaRC --- homeassistant/components/braviatv/media_player.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 5072cb7b74c25b..d0458541f7bee8 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -2,10 +2,11 @@ import ipaddress import logging +from braviarc.braviarc import BraviaRC from getmac import get_mac_address import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -128,12 +129,11 @@ def request_configuration(config, hass, add_entities): def bravia_configuration_callback(data): """Handle the entry of user PIN.""" - from braviarc import braviarc pin = data.get("pin") - braviarc = braviarc.BraviaRC(host) - braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) - if braviarc.is_connected(): + _braviarc = BraviaRC(host) + _braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) + if _braviarc.is_connected(): setup_bravia(config, pin, hass, add_entities) else: request_configuration(config, hass, add_entities) @@ -154,10 +154,9 @@ class BraviaTVDevice(MediaPlayerDevice): def __init__(self, host, mac, name, pin): """Initialize the Sony Bravia device.""" - from braviarc import braviarc self._pin = pin - self._braviarc = braviarc.BraviaRC(host, mac) + self._braviarc = BraviaRC(host, mac) self._name = name self._state = STATE_OFF self._muted = False From 87b13ee732a8451526eae28a6a0d222e190866e5 Mon Sep 17 00:00:00 2001 From: Peter Nijssen Date: Sat, 16 Nov 2019 10:37:58 +0100 Subject: [PATCH 1591/3953] Fix broken postnl sensor (#28794) * fix broken postnl sensor * make sure shipment list is not growing indefinitely --- homeassistant/components/postnl/manifest.json | 2 +- homeassistant/components/postnl/sensor.py | 11 ++++++++--- requirements_all.txt | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/postnl/manifest.json b/homeassistant/components/postnl/manifest.json index d07f9746ee8f28..c45eea0610d380 100644 --- a/homeassistant/components/postnl/manifest.json +++ b/homeassistant/components/postnl/manifest.json @@ -3,7 +3,7 @@ "name": "Postnl", "documentation": "https://www.home-assistant.io/integrations/postnl", "requirements": [ - "postnl_api==1.0.2" + "postnl_api==1.2.2" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/postnl/sensor.py b/homeassistant/components/postnl/sensor.py index cd190c09d87a69..6155f58519a649 100644 --- a/homeassistant/components/postnl/sensor.py +++ b/homeassistant/components/postnl/sensor.py @@ -58,7 +58,7 @@ class PostNLSensor(Entity): def __init__(self, api, name): """Initialize the PostNL sensor.""" self._name = name - self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION, "shipments": []} self._state = None self._api = api @@ -90,6 +90,11 @@ def icon(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update device state.""" - shipments = self._api.get_relevant_shipments() - self._attributes["shipments"] = shipments + shipments = self._api.get_relevant_deliveries() + + self._attributes["shipments"] = [] + + for shipment in shipments: + self._attributes["shipments"].append(vars(shipment)) + self._state = len(shipments) diff --git a/requirements_all.txt b/requirements_all.txt index 4e95af20556cb7..ac41e7b1e46e73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -999,7 +999,7 @@ pmsensor==0.4 pocketcasts==0.1 # homeassistant.components.postnl -postnl_api==1.0.2 +postnl_api==1.2.2 # homeassistant.components.reddit praw==6.4.0 From d736c5f648747d320ec3c3f8023fc262cadb2d8f Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Sat, 16 Nov 2019 04:56:54 -0500 Subject: [PATCH 1592/3953] Truncate sensor state values (#28788) --- homeassistant/components/environment_canada/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 244fda61656f88..a140927c980783 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -125,7 +125,7 @@ def update(self): value = sensor_data.get("value") if isinstance(value, list): - self._state = " | ".join([str(s.get("title")) for s in value]) + self._state = " | ".join([str(s.get("title")) for s in value])[:255] self._attr.update( { ATTR_DETAIL: " | ".join([str(s.get("detail")) for s in value]), @@ -135,7 +135,7 @@ def update(self): elif self.sensor_type == "tendency": self._state = str(value).capitalize() else: - self._state = value + self._state = value[:255] if sensor_data.get("unit") == "C" or self.sensor_type in [ "wind_chill", From 2779d4f66e8d0177c8c394a79a6c42e8ed5430a5 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 16 Nov 2019 11:58:04 +0100 Subject: [PATCH 1593/3953] Upgrade alpha_vantage to 2.1.2 (#28810) --- homeassistant/components/alpha_vantage/__init__.py | 2 +- homeassistant/components/alpha_vantage/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alpha_vantage/__init__.py b/homeassistant/components/alpha_vantage/__init__.py index f8220c2cb811e6..b8da9c190245b9 100644 --- a/homeassistant/components/alpha_vantage/__init__.py +++ b/homeassistant/components/alpha_vantage/__init__.py @@ -1 +1 @@ -"""The alpha_vantage component.""" +"""The Alpha Vantage component.""" diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 1213bb12e74c3c..99498991be26a2 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -1,9 +1,9 @@ { "domain": "alpha_vantage", - "name": "Alpha vantage", + "name": "Alpha Vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "requirements": [ - "alpha_vantage==2.1.1" + "alpha_vantage==2.1.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ac41e7b1e46e73..da43d05b194323 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -200,7 +200,7 @@ aladdin_connect==0.3 alarmdecoder==1.13.2 # homeassistant.components.alpha_vantage -alpha_vantage==2.1.1 +alpha_vantage==2.1.2 # homeassistant.components.ambiclimate ambiclimate==0.2.1 From fbe3d8dade95d390afa93de211738d29f2e030bc Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 16 Nov 2019 11:58:15 +0100 Subject: [PATCH 1594/3953] Upgrade shodan to 1.19.1 (#28809) --- 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 3ef01a44315a01..36348a1975cecd 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/integrations/shodan", "requirements": [ - "shodan==1.19.0" + "shodan==1.19.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index da43d05b194323..a65a86acb7d61e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1766,7 +1766,7 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.19.0 +shodan==1.19.1 # homeassistant.components.simplepush simplepush==1.1.4 From 0996b717ce0023802cac1b3c15324a38a6a04e83 Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 16 Nov 2019 13:27:54 +0100 Subject: [PATCH 1595/3953] Improve coverage of input_datetime/reproduce_state (#28272) * Improve coverage of input_datetime/reproduce_state * Improve tests --- .../components/input_datetime/reproduce_state.py | 10 ++-------- .../components/input_datetime/test_reproduce_state.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py index 09a30e6521052e..17c7fcb9d566b0 100644 --- a/homeassistant/components/input_datetime/reproduce_state.py +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -31,18 +31,12 @@ def is_valid_datetime(string: str) -> bool: def is_valid_date(string: str) -> bool: """Test if string dt is a valid date.""" - try: - return dt_util.parse_date(string) is not None - except ValueError: - return False + return dt_util.parse_date(string) is not None def is_valid_time(string: str) -> bool: """Test if string dt is a valid time.""" - try: - return dt_util.parse_time(string) is not None - except ValueError: - return False + return dt_util.parse_time(string) is not None async def _async_reproduce_state( diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py index 71f0658923c3ba..428b5189117c9a 100644 --- a/tests/components/input_datetime/test_reproduce_state.py +++ b/tests/components/input_datetime/test_reproduce_state.py @@ -36,10 +36,19 @@ async def test_reproducing_states(hass, caplog): # Test invalid state is handled await hass.helpers.state.async_reproduce_state( - [State("input_datetime.entity_datetime", "not_supported")], blocking=True + [ + State("input_datetime.entity_datetime", "not_supported"), + State("input_datetime.entity_datetime", "not-valid-date"), + State("input_datetime.entity_datetime", "not:valid:time"), + State("input_datetime.entity_datetime", "1234-56-78 90:12:34"), + ], + blocking=True, ) assert "not_supported" in caplog.text + assert "not-valid-date" in caplog.text + assert "not:valid:time" in caplog.text + assert "1234-56-78 90:12:34" in caplog.text assert len(datetime_calls) == 0 # Make sure correct services are called From d6e99db38e04a80bb596e38e4e0375fa3351f1de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sat, 16 Nov 2019 15:05:17 +0100 Subject: [PATCH 1596/3953] Fix Comfoconnect errors during startup (#28802) * Add callback registrations to async_added_to_hass * Fix CODEOWNERS * Fix code formatting * Requested changes. * Don't pass unused hass and fix string formatting * Fix import order. --- CODEOWNERS | 1 + .../components/comfoconnect/__init__.py | 5 --- homeassistant/components/comfoconnect/fan.py | 37 +++++++++++-------- .../components/comfoconnect/manifest.json | 2 +- .../components/comfoconnect/sensor.py | 36 +++++++++++------- 5 files changed, 46 insertions(+), 35 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 775fd8be5c196a..ee6a8cd169cb62 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -61,6 +61,7 @@ homeassistant/components/cisco_webex_teams/* @fbradyirl homeassistant/components/ciscospark/* @fbradyirl homeassistant/components/cloud/* @home-assistant/cloud homeassistant/components/cloudflare/* @ludeeus +homeassistant/components/comfoconnect/* @michaelarnauts homeassistant/components/config/* @home-assistant/core homeassistant/components/configurator/* @home-assistant/core homeassistant/components/conversation/* @home-assistant/core diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index aef4bf1deebca7..efdbf020f1adb6 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -102,7 +102,6 @@ class ComfoConnectBridge: def __init__(self, hass, bridge, name, token, friendly_name, pin): """Initialize the ComfoConnect bridge.""" - self.data = {} self.name = name self.hass = hass @@ -136,7 +135,3 @@ def sensor_callback(self, var, value): # Notify listeners that we have received an update dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var) - - def subscribe_sensor(self, sensor_id): - """Subscribe for the specified sensor.""" - self.comfoconnect.register_sensor(sensor_id) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index bbb4b0176bf84c..34e784d61ebec2 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -17,7 +17,7 @@ SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge @@ -30,28 +30,36 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ComfoConnect fan platform.""" ccb = hass.data[DOMAIN] - add_entities([ComfoConnectFan(hass, name=ccb.name, ccb=ccb)], True) + add_entities([ComfoConnectFan(ccb.name, ccb)], True) class ComfoConnectFan(FanEntity): """Representation of the ComfoConnect fan platform.""" - def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None: + def __init__(self, name, ccb: ComfoConnectBridge) -> None: """Initialize the ComfoConnect fan.""" - self._ccb = ccb self._name = name - # Ask the bridge to keep us updated - self._ccb.comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE) - - def _handle_update(var): - if var == SENSOR_FAN_SPEED_MODE: - _LOGGER.debug("Dispatcher update for %s", var) - self.schedule_update_ha_state() + async def async_added_to_hass(self): + """Register for sensor updates.""" + await self.hass.async_add_executor_job( + self._ccb.comfoconnect.register_sensor, SENSOR_FAN_SPEED_MODE + ) + async_dispatcher_connect( + self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update + ) + + def _handle_update(self, var): + """Handle update callbacks.""" + if var == SENSOR_FAN_SPEED_MODE: + _LOGGER.debug("Received update for %s", var) + self.schedule_update_ha_state() - # Register for dispatcher updates - dispatcher_connect(hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False @property def name(self): @@ -71,7 +79,6 @@ def supported_features(self) -> int: @property def speed(self): """Return the current fan mode.""" - try: speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] return SPEED_MAPPING[speed] @@ -95,7 +102,7 @@ def turn_off(self, **kwargs) -> None: def set_speed(self, speed: str): """Set fan speed.""" - _LOGGER.debug("Changing fan speed to %s.", speed) + _LOGGER.debug("Changing fan speed to %s", speed) if speed == SPEED_OFF: self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY) diff --git a/homeassistant/components/comfoconnect/manifest.json b/homeassistant/components/comfoconnect/manifest.json index 57daba7fdbd1dd..091b7f7bcdda4b 100644 --- a/homeassistant/components/comfoconnect/manifest.json +++ b/homeassistant/components/comfoconnect/manifest.json @@ -6,5 +6,5 @@ "pycomfoconnect==0.3" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@michaelarnauts"] } diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 06d0506e2cf39c..a1f16ed9631199 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -11,7 +11,7 @@ ) from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from . import ( @@ -81,13 +81,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_type = resource.lower() if sensor_type not in SENSOR_TYPES: - _LOGGER.warning("Sensor type: %s is not a valid sensor.", sensor_type) + _LOGGER.warning("Sensor type: %s is not a valid sensor", sensor_type) continue sensors.append( ComfoConnectSensor( - hass, - name="%s %s" % (ccb.name, SENSOR_TYPES[sensor_type][0]), + name=f"{ccb.name} {SENSOR_TYPES[sensor_type][0]}", ccb=ccb, sensor_type=sensor_type, ) @@ -99,23 +98,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ComfoConnectSensor(Entity): """Representation of a ComfoConnect sensor.""" - def __init__(self, hass, name, ccb: ComfoConnectBridge, sensor_type) -> None: + def __init__(self, name, ccb: ComfoConnectBridge, sensor_type) -> None: """Initialize the ComfoConnect sensor.""" self._ccb = ccb self._sensor_type = sensor_type self._sensor_id = SENSOR_TYPES[self._sensor_type][3] self._name = name - # Register the requested sensor - self._ccb.comfoconnect.register_sensor(self._sensor_id) - - def _handle_update(var): - if var == self._sensor_id: - _LOGGER.debug("Dispatcher update for %s.", var) - self.schedule_update_ha_state() + async def async_added_to_hass(self): + """Register for sensor updates.""" + await self.hass.async_add_executor_job( + self._ccb.comfoconnect.register_sensor, self._sensor_id + ) + async_dispatcher_connect( + self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update + ) - # Register for dispatcher updates - dispatcher_connect(hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) + def _handle_update(self, var): + """Handle update callbacks.""" + if var == self._sensor_id: + _LOGGER.debug("Received update for %s", var) + self.schedule_update_ha_state() @property def state(self): @@ -125,6 +128,11 @@ def state(self): except KeyError: return None + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + @property def name(self): """Return the name of the sensor.""" From d1241147c87cba194115b4a5f9cfaa58c449a6a2 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 17 Nov 2019 00:32:32 +0000 Subject: [PATCH 1597/3953] [ci skip] Translation update --- .../hisense_aehw4a1/.translations/ru.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/ru.json diff --git a/homeassistant/components/hisense_aehw4a1/.translations/ru.json b/homeassistant/components/hisense_aehw4a1/.translations/ru.json new file mode 100644 index 00000000000000..c65a5277f62cd9 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Hisense AEH-W4A1e \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file From 0a9ac7e655a37007a7cc572a2bcf07eb5a2f8299 Mon Sep 17 00:00:00 2001 From: Jackie Yang Date: Sun, 17 Nov 2019 03:18:53 -0800 Subject: [PATCH 1598/3953] Fix miio air quality sensor (#28828) Fix https://github.com/home-assistant/home-assistant/issues/28827 --- homeassistant/components/xiaomi_miio/air_quality.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index b80906aa0cb065..3824c5b88cd529 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -54,7 +54,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_info.firmware_version, device_info.hardware_version, ) - device = AirMonitorB1(name, AirQualityMonitor(host, token, model), unique_id) + device = AirMonitorB1(name, AirQualityMonitor(host, token, model=model), unique_id) async_add_entities([device], update_before_add=True) From c7f684d3f4339384c23dfbaab554906e0f03333f Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 17 Nov 2019 03:47:14 -0800 Subject: [PATCH 1599/3953] Bump androidtv to 0.0.34 (#28816) * Bump androidtv to 0.0.33; add pure-python-adb requirement * python -m script.gen_requirements_all * Avoid redundant 'available' check * Bump androidtv to 0.0.34 * Update docstrings for the decorator functions --- homeassistant/components/androidtv/manifest.json | 3 ++- homeassistant/components/androidtv/media_player.py | 12 ++++++++---- requirements_all.txt | 5 ++++- requirements_test_all.txt | 5 ++++- tests/components/androidtv/patchers.py | 11 +++++------ 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index c734b525e550a2..8b68f089617b3d 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell==0.0.8", - "androidtv==0.0.32" + "androidtv==0.0.34", + "pure-python-adb==0.2.2.dev0" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 7540973ea1999b..b1cb86f7633e8b 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -252,14 +252,18 @@ def service_adb_command(service): def adb_decorator(override_available=False): - """Send an ADB command if the device is available and catch exceptions.""" + """Wrap ADB methods and catch exceptions. + + Allows for overriding the available status of the ADB connection via the + `override_available` parameter. + """ def _adb_decorator(func): - """Wait if previous ADB commands haven't finished.""" + """Wrap the provided ADB method and catch exceptions.""" @functools.wraps(func) def _adb_exception_catcher(self, *args, **kwargs): - # If the device is unavailable, don't do anything + """Call an ADB-related method and catch exceptions.""" if not self.available and not override_available: return None @@ -319,7 +323,7 @@ def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): # Property attributes self._adb_response = None - self._available = self.aftv.available + self._available = True self._current_app = None self._state = None diff --git a/requirements_all.txt b/requirements_all.txt index a65a86acb7d61e..e4fa1aa04d8585 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -209,7 +209,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.32 +androidtv==0.0.34 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -1028,6 +1028,9 @@ ptvsd==4.2.8 # homeassistant.components.wink pubnubsub-handler==1.0.8 +# homeassistant.components.androidtv +pure-python-adb==0.2.2.dev0 + # homeassistant.components.pushbullet pushbullet.py==0.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2895756d3334f1..bfd97c58fd8371 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.32 +androidtv==0.0.34 # homeassistant.components.apns apns2==0.3.0 @@ -344,6 +344,9 @@ prometheus_client==0.7.1 # homeassistant.components.ptvsd ptvsd==4.2.8 +# homeassistant.components.androidtv +pure-python-adb==0.2.2.dev0 + # homeassistant.components.pushbullet pushbullet.py==0.11.0 diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 986180bf214ee0..0549ad995e18e7 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,6 +1,5 @@ """Define patches used for androidtv tests.""" -from socket import error as socket_error from unittest.mock import mock_open, patch @@ -25,7 +24,7 @@ def shell(self, cmd): class ClientFakeSuccess: - """A fake of the `adb_messenger.client.Client` class when the connection and shell commands succeed.""" + """A fake of the `ppadb.client.Client` class when the connection and shell commands succeed.""" def __init__(self, host="127.0.0.1", port=5037): """Initialize a `ClientFakeSuccess` instance.""" @@ -43,7 +42,7 @@ def device(self, serial): class ClientFakeFail: - """A fake of the `adb_messenger.client.Client` class when the connection and shell commands fail.""" + """A fake of the `ppadb.client.Client` class when the connection and shell commands fail.""" def __init__(self, host="127.0.0.1", port=5037): """Initialize a `ClientFakeFail` instance.""" @@ -59,7 +58,7 @@ def device(self, serial): class DeviceFake: - """A fake of the `adb_messenger.device.Device` class.""" + """A fake of the `ppadb.device.Device` class.""" def __init__(self, host): """Initialize a `DeviceFake` instance.""" @@ -75,7 +74,7 @@ def shell(self, cmd): def patch_connect(success): - """Mock the `adb_shell.adb_device.AdbDevice` and `adb_messenger.client.Client` classes.""" + """Mock the `adb_shell.adb_device.AdbDevice` and `ppadb.client.Client` classes.""" def connect_success_python(self, *args, **kwargs): """Mock the `AdbDeviceFake.connect` method when it succeeds.""" @@ -83,7 +82,7 @@ def connect_success_python(self, *args, **kwargs): def connect_fail_python(self, *args, **kwargs): """Mock the `AdbDeviceFake.connect` method when it fails.""" - raise socket_error + raise OSError if success: return { From f25a3cbfcf6aab34022c731038235b771d0fdda6 Mon Sep 17 00:00:00 2001 From: Jordan Speicher Date: Sun, 17 Nov 2019 05:52:33 -0600 Subject: [PATCH 1600/3953] Add broadcast_address for wake-on-lan (#28793) --- .../components/panasonic_viera/media_player.py | 15 ++++++++++++--- homeassistant/const.py | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index 0b19a8fa55272d..d0615edfc33021 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -21,6 +21,7 @@ SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( + CONF_BROADCAST_ADDRESS, CONF_HOST, CONF_MAC, CONF_NAME, @@ -36,6 +37,7 @@ DEFAULT_NAME = "Panasonic Viera TV" DEFAULT_PORT = 55000 +DEFAULT_BROADCAST_ADDRESS = "255.255.255.255" DEFAULT_APP_POWER = False SUPPORT_VIERATV = ( @@ -55,6 +57,9 @@ { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_MAC): cv.string, + vol.Optional( + CONF_BROADCAST_ADDRESS, default=DEFAULT_BROADCAST_ADDRESS + ): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_APP_POWER, default=DEFAULT_APP_POWER): cv.boolean, @@ -65,6 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Panasonic Viera TV platform.""" mac = config.get(CONF_MAC) + broadcast = config.get(CONF_BROADCAST_ADDRESS) name = config.get(CONF_NAME) port = config.get(CONF_PORT) app_power = config.get(CONF_APP_POWER) @@ -86,14 +92,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) remote = RemoteControl(host, port) - add_entities([PanasonicVieraTVDevice(mac, name, remote, host, app_power)]) + add_entities( + [PanasonicVieraTVDevice(mac, name, remote, host, broadcast, app_power)] + ) return True class PanasonicVieraTVDevice(MediaPlayerDevice): """Representation of a Panasonic Viera TV.""" - def __init__(self, mac, name, remote, host, app_power, uuid=None): + def __init__(self, mac, name, remote, host, broadcast, app_power, uuid=None): """Initialize the Panasonic device.""" # Save a reference to the imported class self._wol = wakeonlan @@ -105,6 +113,7 @@ def __init__(self, mac, name, remote, host, app_power, uuid=None): self._state = None self._remote = remote self._host = host + self._broadcast = broadcast self._volume = 0 self._app_power = app_power @@ -162,7 +171,7 @@ def supported_features(self): def turn_on(self): """Turn on the media player.""" if self._mac: - self._wol.send_magic_packet(self._mac, ip_address=self._host) + self._wol.send_magic_packet(self._mac, ip_address=self._broadcast) self._state = STATE_ON elif self._app_power: self._remote.turn_on() diff --git a/homeassistant/const.py b/homeassistant/const.py index 449e7a90087b83..c15d1e55bd1800 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -40,6 +40,7 @@ CONF_BINARY_SENSORS = "binary_sensors" CONF_BLACKLIST = "blacklist" CONF_BRIGHTNESS = "brightness" +CONF_BROADCAST_ADDRESS = "broadcast_address" CONF_CLIENT_ID = "client_id" CONF_CLIENT_SECRET = "client_secret" CONF_CODE = "code" From cb63a9d7e17af0f243cbc34458b1452098846168 Mon Sep 17 00:00:00 2001 From: Jay Newstrom Date: Sun, 17 Nov 2019 06:17:22 -0600 Subject: [PATCH 1601/3953] Add broadcast address for WOL and samsungtv (#28819) * Add broadcast address for WOL and samsungtv * Fix style * Fix tests, and add new test for new functionality * Fix code style --- .../components/samsungtv/media_player.py | 14 +++++++-- .../components/samsungtv/test_media_player.py | 31 ++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 65805235f3fbfb..1262c3d7d36e57 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -26,6 +26,7 @@ SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( + CONF_BROADCAST_ADDRESS, CONF_HOST, CONF_MAC, CONF_NAME, @@ -41,6 +42,7 @@ DEFAULT_NAME = "Samsung TV Remote" DEFAULT_TIMEOUT = 1 +DEFAULT_BROADCAST_ADDRESS = "255.255.255.255" KEY_PRESS_TIMEOUT = 1.2 KNOWN_DEVICES_KEY = "samsungtv_known_devices" @@ -65,6 +67,9 @@ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_MAC): cv.string, + vol.Optional( + CONF_BROADCAST_ADDRESS, default=DEFAULT_BROADCAST_ADDRESS + ): cv.string, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, } ) @@ -84,6 +89,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = config.get(CONF_PORT) name = config.get(CONF_NAME) mac = config.get(CONF_MAC) + broadcast = config.get(CONF_BROADCAST_ADDRESS) timeout = config.get(CONF_TIMEOUT) elif discovery_info is not None: tv_name = discovery_info.get("name") @@ -95,6 +101,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = None timeout = DEFAULT_TIMEOUT mac = None + broadcast = DEFAULT_BROADCAST_ADDRESS uuid = discovery_info.get("udn") if uuid and uuid.startswith("uuid:"): uuid = uuid[len("uuid:") :] @@ -104,7 +111,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ip_addr = socket.gethostbyname(host) if ip_addr not in known_devices: known_devices.add(ip_addr) - add_entities([SamsungTVDevice(host, port, name, timeout, mac, uuid)]) + add_entities([SamsungTVDevice(host, port, name, timeout, mac, broadcast, uuid)]) LOGGER.info("Samsung TV %s added as '%s'", host, name) else: LOGGER.info("Ignoring duplicate Samsung TV %s", host) @@ -113,12 +120,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SamsungTVDevice(MediaPlayerDevice): """Representation of a Samsung TV.""" - def __init__(self, host, port, name, timeout, mac, uuid): + def __init__(self, host, port, name, timeout, mac, broadcast, uuid): """Initialize the Samsung device.""" # Save a reference to the imported classes self._name = name self._mac = mac + self._broadcast = broadcast self._uuid = uuid # Assume that the TV is not muted self._muted = False @@ -339,7 +347,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): def turn_on(self): """Turn the media player on.""" if self._mac: - wakeonlan.send_magic_packet(self._mac) + wakeonlan.send_magic_packet(self._mac, ip_address=self._broadcast) else: self.send_key("KEY_POWERON") diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 3409e55a49ceb1..8f6014463f507b 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -32,6 +32,7 @@ ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, + CONF_BROADCAST_ADDRESS, CONF_HOST, CONF_MAC, CONF_NAME, @@ -67,6 +68,19 @@ } } +ENTITY_ID_BROADCAST = f"{DOMAIN}.fake_broadcast" +MOCK_CONFIG_BROADCAST = { + DOMAIN: { + CONF_PLATFORM: SAMSUNGTV_DOMAIN, + CONF_HOST: "fake_broadcast", + CONF_NAME: "fake_broadcast", + CONF_PORT: 8001, + CONF_TIMEOUT: 10, + CONF_MAC: "38:f9:d3:82:b4:f1", + CONF_BROADCAST_ADDRESS: "192.168.5.255", + } +} + ENTITY_ID_NOMAC = f"{DOMAIN}.fake_nomac" MOCK_CONFIG_NOMAC = { DOMAIN: { @@ -565,7 +579,22 @@ async def test_turn_on_with_mac(hass, remote, wakeonlan): ) # key and update called assert wakeonlan.send_magic_packet.call_count == 1 - assert wakeonlan.send_magic_packet.call_args_list == [call("38:f9:d3:82:b4:f1")] + assert wakeonlan.send_magic_packet.call_args_list == [ + call("38:f9:d3:82:b4:f1", ip_address="255.255.255.255") + ] + + +async def test_turn_on_with_mac_and_broadcast(hass, remote, wakeonlan): + """Test turn on.""" + await setup_samsungtv(hass, MOCK_CONFIG_BROADCAST) + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID_BROADCAST}, True + ) + # key and update called + assert wakeonlan.send_magic_packet.call_count == 1 + assert wakeonlan.send_magic_packet.call_args_list == [ + call("38:f9:d3:82:b4:f1", ip_address="192.168.5.255") + ] async def test_turn_on_without_mac(hass, remote): From 101ab5c3fad1cb353be01483608c7ea5b6315e6d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 17 Nov 2019 15:53:34 +0100 Subject: [PATCH 1602/3953] Align naming (#28830) * Align naming * Update tests --- homeassistant/components/wake_on_lan/__init__.py | 4 +--- homeassistant/components/wake_on_lan/switch.py | 14 ++++++-------- tests/components/wake_on_lan/test_switch.py | 14 +++++++------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index b4aad4925b9986..d5b8f92a9bc1f4 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -5,15 +5,13 @@ import voluptuous as vol import wakeonlan -from homeassistant.const import CONF_MAC +from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_MAC import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DOMAIN = "wake_on_lan" -CONF_BROADCAST_ADDRESS = "broadcast_address" - SERVICE_SEND_MAGIC_PACKET = "send_magic_packet" WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema( diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 01f69679829677..8200a0309fa7a7 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -7,14 +7,12 @@ import wakeonlan from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_BROADCAST_ADDRESS, CONF_HOST, CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script _LOGGER = logging.getLogger(__name__) -CONF_BROADCAST_ADDRESS = "broadcast_address" -CONF_MAC_ADDRESS = "mac_address" CONF_OFF_ACTION = "turn_off" DEFAULT_NAME = "Wake on LAN" @@ -22,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_MAC_ADDRESS): cv.string, + vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_BROADCAST_ADDRESS): cv.string, vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -35,16 +33,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a wake on lan switch.""" broadcast_address = config.get(CONF_BROADCAST_ADDRESS) host = config.get(CONF_HOST) - mac_address = config.get(CONF_MAC_ADDRESS) - name = config.get(CONF_NAME) + mac_address = config[CONF_MAC] + name = config[CONF_NAME] off_action = config.get(CONF_OFF_ACTION) add_entities( - [WOLSwitch(hass, name, host, mac_address, off_action, broadcast_address)], True + [WolSwitch(hass, name, host, mac_address, off_action, broadcast_address)], True ) -class WOLSwitch(SwitchDevice): +class WolSwitch(SwitchDevice): """Representation of a wake on lan switch.""" def __init__(self, hass, name, host, mac_address, off_action, broadcast_address): diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index 2d319c2b5e7106..1fbcc9cc273ff1 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -30,7 +30,7 @@ def system(): return "Windows" -class TestWOLSwitch(unittest.TestCase): +class TestWolSwitch(unittest.TestCase): """Test the wol switch.""" def setUp(self): @@ -53,7 +53,7 @@ def test_valid_hostname(self): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "validhostname", } }, @@ -89,7 +89,7 @@ def test_valid_hostname_windows(self): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "validhostname", } }, @@ -113,7 +113,7 @@ def test_minimal_config(self): assert setup_component( self.hass, switch.DOMAIN, - {"switch": {"platform": "wake_on_lan", "mac_address": "00-01-02-03-04-05"}}, + {"switch": {"platform": "wake_on_lan", "mac": "00-01-02-03-04-05"}}, ) @patch("wakeonlan.send_magic_packet", new=send_magic_packet) @@ -126,7 +126,7 @@ def test_broadcast_config(self): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "broadcast_address": "255.255.255.255", } }, @@ -150,7 +150,7 @@ def test_off_script(self): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "validhostname", "turn_off": {"service": "shell_command.turn_off_target"}, } @@ -192,7 +192,7 @@ def test_invalid_hostname_windows(self): { "switch": { "platform": "wake_on_lan", - "mac_address": "00-01-02-03-04-05", + "mac": "00-01-02-03-04-05", "host": "invalidhostname", } }, From 0c48b8ec52d384f8ac2e1088b1dacfe776418250 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 17 Nov 2019 19:14:19 -0300 Subject: [PATCH 1603/3953] Esphome climate features (#28786) * add esphome climate fan * adds back compatibility support * fixes, add swing mode * revert client config * sort import * rebase --- homeassistant/components/esphome/climate.py | 96 ++++++++++++++++++- .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 5fed8da76ef6c2..49d284b6de2eae 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -2,7 +2,13 @@ import logging from typing import List, Optional -from aioesphomeapi import ClimateInfo, ClimateMode, ClimateState +from aioesphomeapi import ( + ClimateInfo, + ClimateMode, + ClimateState, + ClimateFanMode, + ClimateSwingMode, +) from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -12,12 +18,29 @@ HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_DRY, + HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE_RANGE, + SUPPORT_FAN_MODE, + SUPPORT_SWING_MODE, PRESET_AWAY, - HVAC_MODE_OFF, PRESET_HOME, + FAN_ON, + FAN_OFF, + FAN_AUTO, + FAN_LOW, + FAN_MEDIUM, + FAN_HIGH, + FAN_MIDDLE, + FAN_FOCUS, + FAN_DIFFUSE, + SWING_OFF, + SWING_BOTH, + SWING_VERTICAL, + SWING_HORIZONTAL, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -57,6 +80,33 @@ def _climate_modes(): ClimateMode.AUTO: HVAC_MODE_HEAT_COOL, ClimateMode.COOL: HVAC_MODE_COOL, ClimateMode.HEAT: HVAC_MODE_HEAT, + ClimateMode.FAN_ONLY: HVAC_MODE_FAN_ONLY, + ClimateMode.DRY: HVAC_MODE_DRY, + } + + +@esphome_map_enum +def _fan_modes(): + return { + ClimateFanMode.ON: FAN_ON, + ClimateFanMode.OFF: FAN_OFF, + ClimateFanMode.AUTO: FAN_AUTO, + ClimateFanMode.LOW: FAN_LOW, + ClimateFanMode.MEDIUM: FAN_MEDIUM, + ClimateFanMode.HIGH: FAN_HIGH, + ClimateFanMode.MIDDLE: FAN_MIDDLE, + ClimateFanMode.FOCUS: FAN_FOCUS, + ClimateFanMode.DIFFUSE: FAN_DIFFUSE, + } + + +@esphome_map_enum +def _swing_modes(): + return { + ClimateSwingMode.OFF: SWING_OFF, + ClimateSwingMode.BOTH: SWING_BOTH, + ClimateSwingMode.VERTICAL: SWING_VERTICAL, + ClimateSwingMode.HORIZONTAL: SWING_HORIZONTAL, } @@ -94,11 +144,27 @@ def hvac_modes(self) -> List[str]: for mode in self._static_info.supported_modes ] + @property + def fan_modes(self): + """Return the list of available fan modes.""" + return [ + _fan_modes.from_esphome(mode) + for mode in self._static_info.supported_fan_modes + ] + @property def preset_modes(self): """Return preset modes.""" return [PRESET_AWAY, PRESET_HOME] if self._static_info.supports_away else [] + @property + def swing_modes(self): + """Return the list of available swing modes.""" + return [ + _swing_modes.from_esphome(mode) + for mode in self._static_info.supported_swing_modes + ] + @property def target_temperature_step(self) -> float: """Return the supported step of target temperature.""" @@ -125,6 +191,10 @@ def supported_features(self) -> int: features |= SUPPORT_TARGET_TEMPERATURE if self._static_info.supports_away: features |= SUPPORT_PRESET_MODE + if self._static_info.supported_fan_modes: + features |= SUPPORT_FAN_MODE + if self._static_info.supported_swing_modes: + features |= SUPPORT_SWING_MODE return features # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property @@ -135,11 +205,21 @@ def hvac_mode(self) -> Optional[str]: """Return current operation ie. heat, cool, idle.""" return _climate_modes.from_esphome(self._state.mode) + @esphome_state_property + def fan_mode(self): + """Return current fan setting.""" + return _fan_modes.from_esphome(self._state.fan_mode) + @esphome_state_property def preset_mode(self): """Return current preset mode.""" return PRESET_AWAY if self._state.away else PRESET_HOME + @esphome_state_property + def swing_mode(self): + """Return current swing mode.""" + return _swing_modes.from_esphome(self._state.swing_mode) + @esphome_state_property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" @@ -183,3 +263,15 @@ async def async_set_preset_mode(self, preset_mode): """Set preset mode.""" away = preset_mode == PRESET_AWAY await self._client.climate_command(key=self._static_info.key, away=away) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new fan mode.""" + await self._client.climate_command( + key=self._static_info.key, fan_mode=_fan_modes.from_hass(fan_mode) + ) + + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new swing mode.""" + await self._client.climate_command( + key=self._static_info.key, swing_mode=_swing_modes.from_hass(swing_mode) + ) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 724946e69841ac..425ecc6ce321ea 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.5.0" + "aioesphomeapi==2.6.0" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index e4fa1aa04d8585..d80cfc27930ec0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -142,7 +142,7 @@ aiobotocore==0.10.4 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.5.0 +aioesphomeapi==2.6.0 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfd97c58fd8371..3870f200c2d801 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -50,7 +50,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.4 # homeassistant.components.esphome -aioesphomeapi==2.5.0 +aioesphomeapi==2.6.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 3d5b007c6bc63be50a4aa5bb8e45e9366bdbd201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Mon, 18 Nov 2019 00:39:49 +0100 Subject: [PATCH 1604/3953] Implement more Comfoconnect sensors (#28817) * Rework Comfoconnect sensor platform * Sort ATTRS and fix icon * Add unique_id to fan * Use a different signal per sensor type * Add more logging * Swap to be sure. * Remove -fan suffix from unique_id --- .../components/comfoconnect/__init__.py | 32 +- homeassistant/components/comfoconnect/fan.py | 24 +- .../components/comfoconnect/sensor.py | 291 +++++++++++++----- 3 files changed, 242 insertions(+), 105 deletions(-) diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index efdbf020f1adb6..f1fd67cc4bbb36 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -1,12 +1,7 @@ """Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging -from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, - SENSOR_TEMPERATURE_OUTDOOR, - Bridge, - ComfoConnect, -) +from pycomfoconnect import Bridge, ComfoConnect import voluptuous as vol from homeassistant.const import ( @@ -24,14 +19,7 @@ DOMAIN = "comfoconnect" -SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = "comfoconnect_update_received" - -ATTR_CURRENT_TEMPERATURE = "current_temperature" -ATTR_CURRENT_HUMIDITY = "current_humidity" -ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" -ATTR_OUTSIDE_HUMIDITY = "outside_humidity" -ATTR_AIR_FLOW_SUPPLY = "air_flow_supply" -ATTR_AIR_FLOW_EXHAUST = "air_flow_exhaust" +SIGNAL_COMFOCONNECT_UPDATE_RECEIVED = "comfoconnect_update_received_{}" CONF_USER_AGENT = "user_agent" @@ -105,6 +93,7 @@ def __init__(self, hass, bridge, name, token, friendly_name, pin): self.data = {} self.name = name self.hass = hass + self.unique_id = bridge.uuid.hex() self.comfoconnect = ComfoConnect( bridge=bridge, @@ -125,13 +114,8 @@ def disconnect(self): self.comfoconnect.disconnect() def sensor_callback(self, var, value): - """Call function for sensor updates.""" - _LOGGER.debug("Got value from bridge: %d = %d", var, value) - - if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]: - self.data[var] = value / 10 - else: - self.data[var] = value - - # Notify listeners that we have received an update - dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var) + """Notify listeners that we have received an update.""" + _LOGGER.debug("Received update for %s: %s", var, value) + dispatcher_send( + self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(var), value + ) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 34e784d61ebec2..432b25ac602b05 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -43,24 +43,34 @@ def __init__(self, name, ccb: ComfoConnectBridge) -> None: async def async_added_to_hass(self): """Register for sensor updates.""" + _LOGGER.debug("Registering for fan speed") + async_dispatcher_connect( + self.hass, + SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(SENSOR_FAN_SPEED_MODE), + self._handle_update, + ) await self.hass.async_add_executor_job( self._ccb.comfoconnect.register_sensor, SENSOR_FAN_SPEED_MODE ) - async_dispatcher_connect( - self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update - ) - def _handle_update(self, var): + def _handle_update(self, value): """Handle update callbacks.""" - if var == SENSOR_FAN_SPEED_MODE: - _LOGGER.debug("Received update for %s", var) - self.schedule_update_ha_state() + _LOGGER.debug( + "Handle update for fan speed (%d): %s", SENSOR_FAN_SPEED_MODE, value + ) + self._ccb.data[SENSOR_FAN_SPEED_MODE] = value + self.schedule_update_ha_state() @property def should_poll(self) -> bool: """Do not poll.""" return False + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._ccb.unique_id + @property def name(self): """Return the name of the fan.""" diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index a1f16ed9631199..3e3507ea48ddc6 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -2,93 +2,214 @@ import logging from pycomfoconnect import ( + SENSOR_BYPASS_STATE, + SENSOR_DAYS_TO_REPLACE_FILTER, + SENSOR_FAN_EXHAUST_DUTY, SENSOR_FAN_EXHAUST_FLOW, + SENSOR_FAN_EXHAUST_SPEED, + SENSOR_FAN_SUPPLY_DUTY, SENSOR_FAN_SUPPLY_FLOW, + SENSOR_FAN_SUPPLY_SPEED, + SENSOR_HUMIDITY_EXHAUST, SENSOR_HUMIDITY_EXTRACT, SENSOR_HUMIDITY_OUTDOOR, + SENSOR_HUMIDITY_SUPPLY, + SENSOR_POWER_CURRENT, + SENSOR_TEMPERATURE_EXHAUST, SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR, + SENSOR_TEMPERATURE_SUPPLY, ) +import voluptuous as vol -from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_RESOURCES, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + POWER_WATT, + TEMP_CELSIUS, +) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import ( - ATTR_AIR_FLOW_EXHAUST, - ATTR_AIR_FLOW_SUPPLY, - ATTR_CURRENT_HUMIDITY, - ATTR_CURRENT_TEMPERATURE, - ATTR_OUTSIDE_HUMIDITY, - ATTR_OUTSIDE_TEMPERATURE, - DOMAIN, - SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, - ComfoConnectBridge, -) +from . import DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge -_LOGGER = logging.getLogger(__name__) +ATTR_AIR_FLOW_EXHAUST = "air_flow_exhaust" +ATTR_AIR_FLOW_SUPPLY = "air_flow_supply" +ATTR_BYPASS_STATE = "bypass_state" +ATTR_CURRENT_HUMIDITY = "current_humidity" +ATTR_CURRENT_TEMPERATURE = "current_temperature" +ATTR_DAYS_TO_REPLACE_FILTER = "days_to_replace_filter" +ATTR_EXHAUST_FAN_DUTY = "exhaust_fan_duty" +ATTR_EXHAUST_FAN_SPEED = "exhaust_fan_speed" +ATTR_EXHAUST_HUMIDITY = "exhaust_humidity" +ATTR_EXHAUST_TEMPERATURE = "exhaust_temperature" +ATTR_OUTSIDE_HUMIDITY = "outside_humidity" +ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" +ATTR_POWER_CURRENT = "power_usage" +ATTR_SUPPLY_FAN_DUTY = "supply_fan_duty" +ATTR_SUPPLY_FAN_SPEED = "supply_fan_speed" +ATTR_SUPPLY_HUMIDITY = "supply_humidity" +ATTR_SUPPLY_TEMPERATURE = "supply_temperature" -SENSOR_TYPES = {} +_LOGGER = logging.getLogger(__name__) +ATTR_ICON = "icon" +ATTR_ID = "id" +ATTR_LABEL = "label" +ATTR_MULTIPLIER = "multiplier" +ATTR_UNIT = "unit" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the ComfoConnect fan platform.""" +SENSOR_TYPES = { + ATTR_CURRENT_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Inside Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_EXTRACT, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_CURRENT_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Inside Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_EXTRACT, + }, + ATTR_OUTSIDE_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Outside Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_OUTDOOR, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_OUTSIDE_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Outside Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_OUTDOOR, + }, + ATTR_SUPPLY_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Supply Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_SUPPLY, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_SUPPLY_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Supply Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_SUPPLY, + }, + ATTR_SUPPLY_FAN_SPEED: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Supply Fan Speed", + ATTR_UNIT: "rpm", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_SUPPLY_SPEED, + }, + ATTR_SUPPLY_FAN_DUTY: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Supply Fan Duty", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_SUPPLY_DUTY, + }, + ATTR_EXHAUST_FAN_SPEED: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Exhaust Fan Speed", + ATTR_UNIT: "rpm", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_EXHAUST_SPEED, + }, + ATTR_EXHAUST_FAN_DUTY: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Exhaust Fan Duty", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_EXHAUST_DUTY, + }, + ATTR_EXHAUST_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Exhaust Temperature", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: "mdi:thermometer", + ATTR_ID: SENSOR_TEMPERATURE_EXHAUST, + ATTR_MULTIPLIER: 0.1, + }, + ATTR_EXHAUST_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_LABEL: "Exhaust Humidity", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:water-percent", + ATTR_ID: SENSOR_HUMIDITY_EXHAUST, + }, + ATTR_AIR_FLOW_SUPPLY: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Supply airflow", + ATTR_UNIT: "m³/h", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_SUPPLY_FLOW, + }, + ATTR_AIR_FLOW_EXHAUST: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Exhaust airflow", + ATTR_UNIT: "m³/h", + ATTR_ICON: "mdi:fan", + ATTR_ID: SENSOR_FAN_EXHAUST_FLOW, + }, + ATTR_BYPASS_STATE: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Bypass State", + ATTR_UNIT: "%", + ATTR_ICON: "mdi:camera-iris", + ATTR_ID: SENSOR_BYPASS_STATE, + }, + ATTR_DAYS_TO_REPLACE_FILTER: { + ATTR_DEVICE_CLASS: None, + ATTR_LABEL: "Days to replace filter", + ATTR_UNIT: "days", + ATTR_ICON: "mdi:calendar", + ATTR_ID: SENSOR_DAYS_TO_REPLACE_FILTER, + }, + ATTR_POWER_CURRENT: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_LABEL: "Power usage", + ATTR_UNIT: POWER_WATT, + ATTR_ICON: "mdi:flash", + ATTR_ID: SENSOR_POWER_CURRENT, + }, +} - global SENSOR_TYPES - SENSOR_TYPES = { - ATTR_CURRENT_TEMPERATURE: [ - "Inside Temperature", - TEMP_CELSIUS, - "mdi:thermometer", - SENSOR_TEMPERATURE_EXTRACT, - ], - ATTR_CURRENT_HUMIDITY: [ - "Inside Humidity", - "%", - "mdi:water-percent", - SENSOR_HUMIDITY_EXTRACT, - ], - ATTR_OUTSIDE_TEMPERATURE: [ - "Outside Temperature", - TEMP_CELSIUS, - "mdi:thermometer", - SENSOR_TEMPERATURE_OUTDOOR, - ], - ATTR_OUTSIDE_HUMIDITY: [ - "Outside Humidity", - "%", - "mdi:water-percent", - SENSOR_HUMIDITY_OUTDOOR, - ], - ATTR_AIR_FLOW_SUPPLY: [ - "Supply airflow", - "m³/h", - "mdi:air-conditioner", - SENSOR_FAN_SUPPLY_FLOW, - ], - ATTR_AIR_FLOW_EXHAUST: [ - "Exhaust airflow", - "m³/h", - "mdi:air-conditioner", - SENSOR_FAN_EXHAUST_FLOW, - ], +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_RESOURCES, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES)] + ), } +) + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the ComfoConnect fan platform.""" ccb = hass.data[DOMAIN] sensors = [] for resource in config[CONF_RESOURCES]: - sensor_type = resource.lower() - - if sensor_type not in SENSOR_TYPES: - _LOGGER.warning("Sensor type: %s is not a valid sensor", sensor_type) - continue - sensors.append( ComfoConnectSensor( - name=f"{ccb.name} {SENSOR_TYPES[sensor_type][0]}", + name=f"{ccb.name} {SENSOR_TYPES[resource][ATTR_LABEL]}", ccb=ccb, - sensor_type=sensor_type, + sensor_type=resource, ) ) @@ -102,23 +223,35 @@ def __init__(self, name, ccb: ComfoConnectBridge, sensor_type) -> None: """Initialize the ComfoConnect sensor.""" self._ccb = ccb self._sensor_type = sensor_type - self._sensor_id = SENSOR_TYPES[self._sensor_type][3] + self._sensor_id = SENSOR_TYPES[self._sensor_type][ATTR_ID] self._name = name async def async_added_to_hass(self): """Register for sensor updates.""" - await self.hass.async_add_executor_job( - self._ccb.comfoconnect.register_sensor, self._sensor_id + _LOGGER.debug( + "Registering for sensor %s (%d)", self._sensor_type, self._sensor_id, ) async_dispatcher_connect( - self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update + self.hass, + SIGNAL_COMFOCONNECT_UPDATE_RECEIVED.format(self._sensor_id), + self._handle_update, + ) + await self.hass.async_add_executor_job( + self._ccb.comfoconnect.register_sensor, self._sensor_id ) - def _handle_update(self, var): + def _handle_update(self, value): """Handle update callbacks.""" - if var == self._sensor_id: - _LOGGER.debug("Received update for %s", var) - self.schedule_update_ha_state() + _LOGGER.debug( + "Handle update for sensor %s (%d): %s", + self._sensor_type, + self._sensor_id, + value, + ) + self._ccb.data[self._sensor_id] = round( + value * SENSOR_TYPES[self._sensor_type].get(ATTR_MULTIPLIER, 1), 2 + ) + self.schedule_update_ha_state() @property def state(self): @@ -133,6 +266,11 @@ def should_poll(self) -> bool: """Do not poll.""" return False + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self._ccb.unique_id}-{self._sensor_type}" + @property def name(self): """Return the name of the sensor.""" @@ -140,10 +278,15 @@ def name(self): @property def icon(self): - """Return the icon to use in the frontend, if any.""" - return SENSOR_TYPES[self._sensor_type][2] + """Return the icon to use in the frontend.""" + return SENSOR_TYPES[self._sensor_type][ATTR_ICON] @property def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return SENSOR_TYPES[self._sensor_type][1] + """Return the unit of measurement of this entity.""" + return SENSOR_TYPES[self._sensor_type][ATTR_UNIT] + + @property + def device_class(self): + """Return the device_class.""" + return SENSOR_TYPES[self._sensor_type][ATTR_DEVICE_CLASS] From aef808d2bf044180d997c0da16b659d4ece96e0e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 18 Nov 2019 00:32:22 +0000 Subject: [PATCH 1605/3953] [ci skip] Translation update --- .../components/almond/.translations/fr.json | 5 ++++ .../components/demo/.translations/es.json | 5 ++++ .../hisense_aehw4a1/.translations/es.json | 15 ++++++++++ .../.translations/zh-Hant.json | 15 ++++++++++ .../huawei_lte/.translations/fr.json | 2 +- .../logi_circle/.translations/es.json | 2 +- .../components/sensor/.translations/es.json | 28 +++++++++---------- .../tellduslive/.translations/es.json | 2 +- .../components/unifi/.translations/es.json | 4 +-- .../components/wled/.translations/fr.json | 2 +- 10 files changed, 60 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/demo/.translations/es.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/es.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/zh-Hant.json diff --git a/homeassistant/components/almond/.translations/fr.json b/homeassistant/components/almond/.translations/fr.json index 0208366cea1214..9ae881d332cdb3 100644 --- a/homeassistant/components/almond/.translations/fr.json +++ b/homeassistant/components/almond/.translations/fr.json @@ -5,6 +5,11 @@ "cannot_connect": "Impossible de se connecter au serveur Almond", "missing_configuration": "Veuillez consulter la documentation pour savoir comment configurer Almond." }, + "step": { + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/es.json b/homeassistant/components/demo/.translations/es.json new file mode 100644 index 00000000000000..ef01fcb4f3c35e --- /dev/null +++ b/homeassistant/components/demo/.translations/es.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/es.json b/homeassistant/components/hisense_aehw4a1/.translations/es.json new file mode 100644 index 00000000000000..69f071bf5d89bd --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/es.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos Hisense AEH-W4A1 en la red.", + "single_instance_allowed": "Solo es posible una \u00fanica configuraci\u00f3n de Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/zh-Hant.json b/homeassistant/components/hisense_aehw4a1/.translations/zh-Hant.json new file mode 100644 index 00000000000000..d4f87905da946d --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/zh-Hant.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u6d77\u4fe1 AEH-W4A1 \u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44\u6d77\u4fe1 AEH-W4A1\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u6d77\u4fe1 AEH-W4A1\uff1f", + "title": "\u6d77\u4fe1 AEH-W4A1" + } + }, + "title": "\u6d77\u4fe1 AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/fr.json b/homeassistant/components/huawei_lte/.translations/fr.json index 5effea3d003606..34db4e93bc4ac1 100644 --- a/homeassistant/components/huawei_lte/.translations/fr.json +++ b/homeassistant/components/huawei_lte/.translations/fr.json @@ -7,7 +7,7 @@ }, "error": { "connection_failed": "La connexion a \u00e9chou\u00e9", - "connection_timeout": "D\u00e9lai de connection d\u00e9pass\u00e9", + "connection_timeout": "D\u00e9lai de connexion d\u00e9pass\u00e9", "incorrect_password": "Mot de passe incorrect", "incorrect_username": "Nom d'utilisateur incorrect", "incorrect_username_or_password": "identifiant ou mot de passe incorrect", diff --git a/homeassistant/components/logi_circle/.translations/es.json b/homeassistant/components/logi_circle/.translations/es.json index 4819ff5cdd77e7..7209bdfefd5a94 100644 --- a/homeassistant/components/logi_circle/.translations/es.json +++ b/homeassistant/components/logi_circle/.translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Solo puedes configurar una cuenta de Logi Circle.", - "external_error": "La excepci\u00f3n se produjo a partir de otro flujo.", + "external_error": "Se produjo una excepci\u00f3n de otro flujo.", "external_setup": "Logi Circle se ha configurado correctamente a partir de otro flujo.", "no_flows": "Es necesario configurar Logi Circle antes de poder autenticarse con \u00e9l. [Echa un vistazo a las instrucciones] (https://www.home-assistant.io/components/logi_circle/)." }, diff --git a/homeassistant/components/sensor/.translations/es.json b/homeassistant/components/sensor/.translations/es.json index c5641d38cc0c7d..7b8ef36efe1268 100644 --- a/homeassistant/components/sensor/.translations/es.json +++ b/homeassistant/components/sensor/.translations/es.json @@ -1,22 +1,22 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} nivel de bater\u00eda", - "is_humidity": "{entity_name} humedad", - "is_illuminance": "{entity_name} iluminancia", - "is_power": "{entity_name} alimentaci\u00f3n", - "is_pressure": "{entity_name} presi\u00f3n", - "is_signal_strength": "{entity_name} intensidad de la se\u00f1al", - "is_temperature": "{entity_name} temperatura", - "is_timestamp": "{entity_name} marca de tiempo", - "is_value": "{entity_name} valor" + "is_battery_level": "Nivel de bater\u00eda actual de {entity_name}", + "is_humidity": "Humedad actual de {entity_name}", + "is_illuminance": "Luminosidad actual de {entity_name}", + "is_power": "Potencia actual de {entity_name}", + "is_pressure": "Presi\u00f3n actual de {entity_name}", + "is_signal_strength": "Intensidad de la se\u00f1al actual de {entity_name}", + "is_temperature": "Temperatura actual de {entity_name}", + "is_timestamp": "Marca de tiempo actual de {entity_name}", + "is_value": "Valor actual de {entity_name}" }, "trigger_type": { - "battery_level": "{entity_name} nivel de bater\u00eda", - "humidity": "{entity_name} humedad", - "illuminance": "{entity_name} iluminancia", - "power": "{entity_name} alimentaci\u00f3n", - "pressure": "{entity_name} presi\u00f3n", + "battery_level": "Cambios de nivel de bater\u00eda de {entity_name}", + "humidity": "Cambios de humedad de {entity_name}", + "illuminance": "Cambios de luminosidad de {entity_name}", + "power": "Cambios de potencia de {entity_name}", + "pressure": "Cambios de presi\u00f3n de {entity_name}", "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name} ", "temperature": "{entity_name} cambios de temperatura", "timestamp": "{entity_name} cambios de fecha y hora", diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json index 677e0389d45b71..0cee7ade0d7683 100644 --- a/homeassistant/components/tellduslive/.translations/es.json +++ b/homeassistant/components/tellduslive/.translations/es.json @@ -11,7 +11,7 @@ }, "step": { "auth": { - "description": "Para vincular tu cuenta de TelldusLivet:\n 1. Pulsa el siguiente enlace\n 2. Inicia sesi\u00f3n en Telldus Live\n 3. Autoriza **{app_name}** (pulsa en **Yes**).\n 4. Vuelve aqu\u00ed y pulsa **ENVIAR**.\n\n [Link TelldusLive account]({auth_url})", + "description": "Para vincular tu cuenta de Telldus Live:\n 1. Pulsa el siguiente enlace\n 2. Inicia sesi\u00f3n en Telldus Live\n 3. Autoriza **{app_name}** (pulsa en **Yes**).\n 4. Vuelve atr\u00e1s y pulsa **ENVIAR**.\n\n [Link TelldusLive account]({auth_url})", "title": "Autenticaci\u00f3n contra TelldusLive" }, "user": { diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 1db6712142d5d8..677899c0958fdc 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -35,8 +35,8 @@ }, "init": { "data": { - "one": "uno", - "other": "otro" + "one": "vac\u00edo", + "other": "vac\u00edo" } }, "statistics_sensors": { diff --git a/homeassistant/components/wled/.translations/fr.json b/homeassistant/components/wled/.translations/fr.json index 5da30ab6288568..6f275ad8199895 100644 --- a/homeassistant/components/wled/.translations/fr.json +++ b/homeassistant/components/wled/.translations/fr.json @@ -17,7 +17,7 @@ "title": "Liez votre WLED" }, "zeroconf_confirm": { - "description": "Voulez-vous ajouter le dispositif WLED nomm\u00e9 '{name}' \u00e0 Home Assistant?", + "description": "Voulez-vous ajouter le dispositif WLED nomm\u00e9 `{name}` \u00e0 Home Assistant?", "title": "Dispositif WLED d\u00e9couvert" } }, From d4c80f160c32d32b20409a5dcfb1ce904ba52cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 18 Nov 2019 10:10:15 +0200 Subject: [PATCH 1606/3953] Add bandit, use to catch known vulnerable XML parsing (#28341) * Add bandit to pre-commit and CI, use to catch known vulnerable XML parsing * Use defusedxml instead of direct xml.etree to parse XML * Move config to tests/bandit.yaml --- .pre-commit-config-all.yaml | 9 +++++++++ .pre-commit-config.yaml | 9 +++++++++ azure-pipelines-ci.yml | 4 ++++ homeassistant/components/ssdp/__init__.py | 2 +- homeassistant/components/ssdp/manifest.json | 1 + homeassistant/package_constraints.txt | 1 + requirements_all.txt | 1 + requirements_test_all.txt | 1 + requirements_test_pre_commit.txt | 1 + tests/bandit.yaml | 11 +++++++++++ tests/components/emulated_hue/test_upnp.py | 2 +- tests/components/rss_feed_template/test_init.py | 2 +- tox.ini | 1 + 13 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 tests/bandit.yaml diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml index 3910835ae9d6a7..abc1afbde788ff 100644 --- a/.pre-commit-config-all.yaml +++ b/.pre-commit-config-all.yaml @@ -26,6 +26,15 @@ repos: - flake8-docstrings==1.5.0 - pydocstyle==4.0.1 files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/PyCQA/bandit + rev: 1.6.2 + hooks: + - id: bandit + args: + - --quiet + - --format=custom + - --configfile=tests/bandit.yaml + files: ^(homeassistant|script|tests)/.+\.py$ # Using a local "system" mypy instead of the mypy hook, because its # results depend on what is installed. And the mypy hook runs in a # virtualenv of its own, meaning we'd need to install and maintain diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3220ac84866891..216bac95f2923b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,3 +22,12 @@ repos: - flake8-docstrings==1.5.0 - pydocstyle==4.0.1 files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/PyCQA/bandit + rev: 1.6.2 + hooks: + - id: bandit + args: + - --quiet + - --format=custom + - --configfile=tests/bandit.yaml + files: ^(homeassistant|script|tests)/.+\.py$ diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 37473b92620128..ca717ca6546004 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -50,6 +50,10 @@ stages: . venv/bin/activate pre-commit run flake8 --all-files displayName: 'Run flake8' + - script: | + . venv/bin/activate + pre-commit run bandit --all-files + displayName: 'Run bandit' - job: 'Validate' pool: vmImage: 'ubuntu-latest' diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index c4d71e0febd055..b9a9d4b46c9f58 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -3,9 +3,9 @@ from datetime import timedelta import logging from urllib.parse import urlparse -from xml.etree import ElementTree import aiohttp +from defusedxml import ElementTree from netdisco import ssdp, util from homeassistant.helpers.event import async_track_time_interval diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 1c3d56fe7fe9a7..1a6bfa36233a3f 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -3,6 +3,7 @@ "name": "SSDP", "documentation": "https://www.home-assistant.io/integrations/ssdp", "requirements": [ + "defusedxml==0.6.0", "netdisco==2.6.0" ], "dependencies": [ diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 72605a3475e2e5..5e266f141cad3e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,6 +9,7 @@ bcrypt==3.1.7 certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" cryptography==2.8 +defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.29 home-assistant-frontend==20191115.0 diff --git a/requirements_all.txt b/requirements_all.txt index d80cfc27930ec0..aea3aa8c9ab194 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -402,6 +402,7 @@ datapoint==0.4.3 # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect +# homeassistant.components.ssdp defusedxml==0.6.0 # homeassistant.components.deluge diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3870f200c2d801..e7be01c8297143 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,6 +137,7 @@ datadog==0.15.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect +# homeassistant.components.ssdp defusedxml==0.6.0 # homeassistant.components.directv diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 29380ca7cd28be..3f4d05a4908111 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,6 @@ # Automatically generated from .pre-commit-config-all.yaml by gen_requirements_all.py, do not edit +bandit==1.6.2 black==19.10b0 flake8-docstrings==1.5.0 flake8==3.7.9 diff --git a/tests/bandit.yaml b/tests/bandit.yaml new file mode 100644 index 00000000000000..79812cba56fc74 --- /dev/null +++ b/tests/bandit.yaml @@ -0,0 +1,11 @@ +# https://bandit.readthedocs.io/en/latest/config.html + +tests: + - B313 + - B314 + - B315 + - B316 + - B317 + - B318 + - B319 + - B320 diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 44f72ba017b95a..ead78ad56ca7eb 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -52,7 +52,7 @@ def tearDownClass(cls): def test_description_xml(self): """Test the description.""" - import xml.etree.ElementTree as ET + import defusedxml.ElementTree as ET result = requests.get(BRIDGE_URL_BASE.format("/description.xml"), timeout=5) diff --git a/tests/components/rss_feed_template/test_init.py b/tests/components/rss_feed_template/test_init.py index 294d84987b2a0b..b07cc8aa9b3c66 100644 --- a/tests/components/rss_feed_template/test_init.py +++ b/tests/components/rss_feed_template/test_init.py @@ -1,7 +1,7 @@ """The tests for the rss_feed_api component.""" import asyncio -from xml.etree import ElementTree +from defusedxml import ElementTree import pytest from homeassistant.setup import async_setup_component diff --git a/tox.ini b/tox.ini index dc2a9f79b90de1..17253e1d1e14ff 100644 --- a/tox.ini +++ b/tox.ini @@ -37,6 +37,7 @@ commands = python -m script.gen_requirements_all validate python -m script.hassfest validate pre-commit run flake8 {posargs: --all-files} + pre-commit run bandit {posargs: --all-files} [testenv:typing] deps = From 90c7d0e56a0bd473fbfb95edff823f46ef612c40 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 18 Nov 2019 09:10:59 +0100 Subject: [PATCH 1607/3953] Update pyatmo to 3.0.1 (#28829) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index f6c08faf8fa86d..9d1178d9d17026 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/integrations/netatmo", "requirements": [ - "pyatmo==2.3.3" + "pyatmo==3.0.1" ], "dependencies": [ "webhook" diff --git a/requirements_all.txt b/requirements_all.txt index aea3aa8c9ab194..7281bdd1474fdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1112,7 +1112,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.3.3 +pyatmo==3.0.1 # homeassistant.components.atome pyatome==0.1.1 From 5731f528d235aa708a6d412bc88644d272dde07d Mon Sep 17 00:00:00 2001 From: Paul Romkes Date: Mon, 18 Nov 2019 09:13:22 +0100 Subject: [PATCH 1608/3953] Make intents end Snips session without speech (#28820) * intents should also end session without speech * Move endSession message to try block * Minor improvement on endSession response --- homeassistant/components/snips/__init__.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 441104211cf233..93e445e8cedfc7 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -135,7 +135,6 @@ async def message_received(msg): intent_type = request["intent"]["intentName"].split("__")[-1] else: intent_type = request["intent"]["intentName"].split(":")[-1] - snips_response = None slots = {} for slot in request.get("slots", []): slots[slot["slotName"]] = {"value": resolve_slot_values(slot)} @@ -148,8 +147,15 @@ async def message_received(msg): intent_response = await intent.async_handle( hass, DOMAIN, intent_type, slots, request["input"] ) + notification = {"sessionId": request.get("sessionId", "default")} + if "plain" in intent_response.speech: - snips_response = intent_response.speech["plain"]["speech"] + notification["text"] = intent_response.speech["plain"]["speech"] + + _LOGGER.debug("send_response %s", json.dumps(notification)) + mqtt.async_publish( + hass, "hermes/dialogueManager/endSession", json.dumps(notification) + ) except intent.UnknownIntent: _LOGGER.warning( "Received unknown intent %s", request["intent"]["intentName"] @@ -157,17 +163,6 @@ async def message_received(msg): except intent.IntentError: _LOGGER.exception("Error while handling intent: %s.", intent_type) - if snips_response: - notification = { - "sessionId": request.get("sessionId", "default"), - "text": snips_response, - } - - _LOGGER.debug("send_response %s", json.dumps(notification)) - mqtt.async_publish( - hass, "hermes/dialogueManager/endSession", json.dumps(notification) - ) - await hass.components.mqtt.async_subscribe(INTENT_TOPIC, message_received) async def snips_say(call): From a4ae9a94ee754462918f35a9b71475a8905b64d5 Mon Sep 17 00:00:00 2001 From: Kevin McCormack Date: Mon, 18 Nov 2019 04:01:36 -0500 Subject: [PATCH 1609/3953] Update Vivotek camera integration (#28841) - Add optional security_level config - Bump libpyvivotek to 0.3.1 --- homeassistant/components/vivotek/camera.py | 5 ++++- homeassistant/components/vivotek/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index c39a9b495bda07..2e604199dd81ed 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -19,12 +19,13 @@ _LOGGER = logging.getLogger(__name__) CONF_FRAMERATE = "framerate" - +CONF_SECURITY_LEVEL = "security_level" CONF_STREAM_PATH = "stream_path" DEFAULT_CAMERA_BRAND = "Vivotek" DEFAULT_NAME = "Vivotek Camera" DEFAULT_EVENT_0_KEY = "event_i0_enable" +DEFAULT_SECURITY_LEVEL = "admin" DEFAULT_STREAM_SOURCE = "live.sdp" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -36,6 +37,7 @@ vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, + vol.Optional(CONF_SECURITY_LEVEL, default=DEFAULT_SECURITY_LEVEL): cv.string, vol.Optional(CONF_STREAM_PATH, default=DEFAULT_STREAM_SOURCE): cv.string, } ) @@ -52,6 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): verify_ssl=config[CONF_VERIFY_SSL], usr=config[CONF_USERNAME], pwd=config[CONF_PASSWORD], + sec_lvl=config[CONF_SECURITY_LEVEL], ), stream_source=f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}", ) diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index ff49899112741c..c97a8461da923e 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -3,7 +3,7 @@ "name": "Vivotek", "documentation": "https://www.home-assistant.io/integrations/vivotek", "requirements": [ - "libpyvivotek==0.2.2" + "libpyvivotek==0.3.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7281bdd1474fdf..36969101d46a99 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -752,7 +752,7 @@ libpurecool==0.5.0 libpyfoscam==1.0 # homeassistant.components.vivotek -libpyvivotek==0.2.2 +libpyvivotek==0.3.1 # homeassistant.components.mikrotik librouteros==2.3.0 From f6b48dec947adb07612793eb68ee54d355edc33d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 18 Nov 2019 15:40:26 +0100 Subject: [PATCH 1610/3953] Updated frontend to 20191118.0 (#28852) --- 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 6c59ea38fe49bb..51906a100cc8fa 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191115.0" + "home-assistant-frontend==20191118.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5e266f141cad3e..7b275684740a9f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191115.0 +home-assistant-frontend==20191118.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 36969101d46a99..49ec2bda97cd38 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -656,7 +656,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191115.0 +home-assistant-frontend==20191118.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e7be01c8297143..4a94664ad0ab54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -223,7 +223,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191115.0 +home-assistant-frontend==20191118.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 326c25a7660ec758dc13aaafbaa0ada822f18b4c Mon Sep 17 00:00:00 2001 From: Brendon Baumgartner Date: Thu, 14 Nov 2019 12:07:43 -0800 Subject: [PATCH 1611/3953] Fix amazon dependency conflicts (#28217) * fix amazon dependency conflicts * bump boto3 for route53 --- homeassistant/components/amazon_polly/manifest.json | 4 ++-- homeassistant/components/aws/manifest.json | 2 +- homeassistant/components/route53/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index 45e382647f8d38..c07aad079e43dc 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -3,10 +3,10 @@ "name": "Amazon polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "requirements": [ - "boto3==1.9.233" + "boto3==1.9.252" ], "dependencies": [], "codeowners": [ "@robbiet480" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index a4543cc4b0f5b7..b617eb75ee16c1 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -3,7 +3,7 @@ "name": "Aws", "documentation": "https://www.home-assistant.io/integrations/aws", "requirements": [ - "aiobotocore==0.10.2" + "aiobotocore==0.10.4" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 34a296b0f9dece..307132aa01bac1 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -3,7 +3,7 @@ "name": "Route53", "documentation": "https://www.home-assistant.io/integrations/route53", "requirements": [ - "boto3==1.9.233", + "boto3==1.9.252", "ipify==1.0.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 058ad143f6f478..65fe34ae7f578c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -136,7 +136,7 @@ aioasuswrt==1.1.22 aioautomatic==0.6.5 # homeassistant.components.aws -aiobotocore==0.10.2 +aiobotocore==0.10.4 # homeassistant.components.dnsip aiodns==2.0.0 @@ -317,7 +317,7 @@ bomradarloop==0.1.3 # homeassistant.components.amazon_polly # homeassistant.components.route53 -boto3==1.9.233 +boto3==1.9.252 # homeassistant.components.braviatv braviarc-homeassistant==0.3.7.dev0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 969107d3707c3d..582d069016849b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -47,7 +47,7 @@ aioasuswrt==1.1.22 aioautomatic==0.6.5 # homeassistant.components.aws -aiobotocore==0.10.2 +aiobotocore==0.10.4 # homeassistant.components.esphome aioesphomeapi==2.5.0 From 1479e7353ba24464b0d238f757f361f6acba60cf Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Fri, 15 Nov 2019 09:21:46 +0100 Subject: [PATCH 1612/3953] Change unique id for SAJ sensor based on device SN (#28663) * Change unique id for SAJ sensor based on device SN * Add SAJ device name + sn to state attributes * Revert device state attributes (after review) --- homeassistant/components/saj/manifest.json | 2 +- homeassistant/components/saj/sensor.py | 21 +++++++++++---------- requirements_all.txt | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index 4d02ab74840d6e..02d83916d50bab 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,7 +3,7 @@ "name": "SAJ", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": [ - "pysaj==0.0.13" + "pysaj==0.0.14" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 7542440c102a58..2a17d110c6e414 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -69,9 +69,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Use all sensors by default hass_sensors = [] - for sensor in sensor_def: - hass_sensors.append(SAJsensor(sensor, inverter_name=config.get(CONF_NAME))) - kwargs = {} if wifi: kwargs["wifi"] = True @@ -81,7 +78,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: saj = pysaj.SAJ(config[CONF_HOST], **kwargs) - await saj.read(sensor_def) + done = await saj.read(sensor_def) except pysaj.UnauthorizedException: _LOGGER.error("Username and/or password is wrong.") return @@ -91,7 +88,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) return - async_add_entities(hass_sensors) + if done: + for sensor in sensor_def: + hass_sensors.append( + SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) + ) + + async_add_entities(hass_sensors) async def async_saj(): """Update all the SAJ sensors.""" @@ -163,10 +166,11 @@ def remove_listener(): class SAJsensor(Entity): """Representation of a SAJ sensor.""" - def __init__(self, pysaj_sensor, inverter_name=None): + def __init__(self, serialnumber, pysaj_sensor, inverter_name=None): """Initialize the sensor.""" self._sensor = pysaj_sensor self._inverter_name = inverter_name + self._serialnumber = serialnumber self._state = self._sensor.value @property @@ -235,7 +239,4 @@ def async_update_values(self, unknown_state=False): @property def unique_id(self): """Return a unique identifier for this sensor.""" - if self._inverter_name: - return f"{self._inverter_name}_{self._sensor.name}" - - return f"{self._sensor.name}" + return f"{self._serialnumber}_{self._sensor.name}" diff --git a/requirements_all.txt b/requirements_all.txt index 65fe34ae7f578c..eb638d9767ebdd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1435,7 +1435,7 @@ pyrepetier==3.0.5 pysabnzbd==1.1.0 # homeassistant.components.saj -pysaj==0.0.13 +pysaj==0.0.14 # homeassistant.components.sony_projector pysdcp==1 From 5f0f5ca5574f6ec824f3191da0ff25aa14b036a1 Mon Sep 17 00:00:00 2001 From: Tyler Page Date: Fri, 15 Nov 2019 11:49:56 +0000 Subject: [PATCH 1613/3953] Fix changing venstar operation_mode (#28754) --- homeassistant/components/venstar/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index c948772197f665..de26d236649767 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -278,7 +278,7 @@ def set_temperature(self, **kwargs): temperature = kwargs.get(ATTR_TEMPERATURE) if operation_mode and self._mode_map.get(operation_mode) != self._client.mode: - set_temp = self._set_operation_mode(self._mode_map.get(operation_mode)) + set_temp = self._set_operation_mode(operation_mode) if set_temp: if ( From fe5f30ba7887db0c2a9a53b3b28591dfcae75a79 Mon Sep 17 00:00:00 2001 From: Morten Trab Date: Sat, 16 Nov 2019 09:45:43 +0100 Subject: [PATCH 1614/3953] Fix Repetier integration entity indexing (#28766) * Fixed multi extruder/beds/chambers index issue, #28130 * Switched from .format to f style name formatting * Fixed incorrect indexing * Removed VS files * Removed not need temp_id subtraction * Removed VS files * Fixed access mode * Fixed access mode * Fixing access mode - again --- homeassistant/components/repetier/__init__.py | 8 ++++---- homeassistant/components/repetier/sensor.py | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 12975baca91e49..1d6026a8754319 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -108,7 +108,7 @@ def has_all_unique_names(value): SENSOR_TYPES = { - # Type, Unit, Icon + # Type, Unit, Icon, post "bed_temperature": ["temperature", TEMP_CELSIUS, "mdi:thermometer", "_bed_"], "extruder_temperature": [ "temperature", @@ -248,12 +248,12 @@ def _load_entities(self): if prop_data is None: continue for idx, _ in enumerate(prop_data): - info["temp_id"] = idx - sensor_info.append(info) + prop_info = info.copy() + prop_info["temp_id"] = idx + sensor_info.append(prop_info) else: info["temp_id"] = None sensor_info.append(info) - self._known_entities.add(known) if not sensor_info: diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py index e692ffc078f2b4..5936b5c33436c4 100644 --- a/homeassistant/components/repetier/sensor.py +++ b/homeassistant/components/repetier/sensor.py @@ -35,11 +35,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): printer_id = info["printer_id"] sensor_type = info["sensor_type"] temp_id = info["temp_id"] - name = info["name"] + name = f"{info['name']}{SENSOR_TYPES[sensor_type][3]}" if temp_id is not None: - name = "{}{}{}".format(name, SENSOR_TYPES[sensor_type][3], temp_id) - else: - name = "{}{}".format(name, SENSOR_TYPES[sensor_type][3]) + _LOGGER.debug("%s Temp_id: %s", sensor_type, temp_id) + name = f"{name}{temp_id}" sensor_class = sensor_map[sensor_type] entity = sensor_class(api, temp_id, name, printer_id, sensor_type) entities.append(entity) From 5ff24ecf77733c3bfa12c0b83629ae512762c0d8 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 15 Nov 2019 09:55:40 +0100 Subject: [PATCH 1615/3953] Fix HomematicIP Cloud Alarm Control Panel support for basic mode (#28778) --- .../homematicip_cloud/alarm_control_panel.py | 67 +++-------------- .../test_alarm_control_panel.py | 72 +++++-------------- .../homematicip_cloud/test_climate.py | 1 + .../components/homematicip_cloud/test_init.py | 4 +- 4 files changed, 31 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index a7b1beaec93868..8ebb35b12c156b 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -1,8 +1,7 @@ """Support for HomematicIP Cloud alarm control panel.""" import logging -from homematicip.aio.group import AsyncSecurityZoneGroup -from homematicip.base.enums import WindowState +from homematicip.functionalHomes import SecurityAndAlarmHome from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.config_entries import ConfigEntry @@ -32,34 +31,15 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] - security_zones = [] - for group in hap.home.groups: - if isinstance(group, AsyncSecurityZoneGroup): - security_zones.append(group) - - if security_zones: - devices.append(HomematicipAlarmControlPanel(hap, security_zones)) - - if devices: - async_add_entities(devices) + async_add_entities([HomematicipAlarmControlPanel(hap)]) class HomematicipAlarmControlPanel(AlarmControlPanel): """Representation of an alarm control panel.""" - def __init__(self, hap: HomematicipHAP, security_zones) -> None: + def __init__(self, hap: HomematicipHAP) -> None: """Initialize the alarm control panel.""" self._home = hap.home - self.alarm_state = STATE_ALARM_DISARMED - self._internal_alarm_zone = None - self._external_alarm_zone = None - - for security_zone in security_zones: - if security_zone.label == "INTERNAL": - self._internal_alarm_zone = security_zone - elif security_zone.label == "EXTERNAL": - self._external_alarm_zone = security_zone @property def device_info(self): @@ -75,28 +55,23 @@ def device_info(self): @property def state(self) -> str: """Return the state of the device.""" + # check for triggered alarm + if self._security_and_alarm.alarmActive: + return STATE_ALARM_TRIGGERED + activation_state = self._home.get_security_zones_activation() # check arm_away if activation_state == (True, True): - if self._internal_alarm_zone_state or self._external_alarm_zone_state: - return STATE_ALARM_TRIGGERED return STATE_ALARM_ARMED_AWAY # check arm_home if activation_state == (False, True): - if self._external_alarm_zone_state: - return STATE_ALARM_TRIGGERED return STATE_ALARM_ARMED_HOME return STATE_ALARM_DISARMED @property - def _internal_alarm_zone_state(self) -> bool: - return _get_zone_alarm_state(self._internal_alarm_zone) - - @property - def _external_alarm_zone_state(self) -> bool: - """Return the state of the device.""" - return _get_zone_alarm_state(self._external_alarm_zone) + def _security_and_alarm(self): + return self._home.get_functionalHome(SecurityAndAlarmHome) async def async_alarm_disarm(self, code=None): """Send disarm command.""" @@ -112,10 +87,7 @@ async def async_alarm_arm_away(self, code=None): async def async_added_to_hass(self): """Register callbacks.""" - if self._internal_alarm_zone: - self._internal_alarm_zone.on_update(self._async_device_changed) - if self._external_alarm_zone: - self._external_alarm_zone.on_update(self._async_device_changed) + self._home.on_update(self._async_device_changed) def _async_device_changed(self, *args, **kwargs): """Handle device state changes.""" @@ -138,26 +110,9 @@ def should_poll(self) -> bool: @property def available(self) -> bool: """Device available.""" - return ( - not self._internal_alarm_zone.unreach - or not self._external_alarm_zone.unreach - ) + return self._home.connected @property def unique_id(self) -> str: """Return a unique ID.""" return f"{self.__class__.__name__}_{self._home.id}" - - -def _get_zone_alarm_state(security_zone) -> bool: - if security_zone and security_zone.active: - if ( - security_zone.sabotage - or security_zone.motionDetected - or security_zone.presenceDetected - or security_zone.windowState == WindowState.OPEN - or security_zone.windowState == WindowState.TILTED - ): - return True - - return False diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 2798a0879b73df..78bc0a09ea507a 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -1,7 +1,4 @@ """Tests for HomematicIP Cloud alarm control panel.""" -from homematicip.base.enums import WindowState -from homematicip.group import SecurityZoneGroup - from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, ) @@ -17,29 +14,24 @@ from .helper import get_and_check_entity_basics -def _get_security_zones(groups): # pylint: disable=W0221 - """Get the security zones.""" - for group in groups: - if isinstance(group, SecurityZoneGroup): - if group.label == "EXTERNAL": - external = group - elif group.label == "INTERNAL": - internal = group - return internal, external - - async def _async_manipulate_security_zones( - hass, home, internal_active, external_active, window_state + hass, home, internal_active=False, external_active=False, alarm_triggered=False ): """Set new values on hmip security zones.""" - internal_zone, external_zone = _get_security_zones(home.groups) + json = home._rawJSONData # pylint: disable=W0212 + json["functionalHomes"]["SECURITY_AND_ALARM"]["alarmActive"] = alarm_triggered + external_zone_id = json["functionalHomes"]["SECURITY_AND_ALARM"]["securityZones"][ + "EXTERNAL" + ] + internal_zone_id = json["functionalHomes"]["SECURITY_AND_ALARM"]["securityZones"][ + "INTERNAL" + ] + external_zone = home.search_group_by_id(external_zone_id) external_zone.active = external_active - external_zone.windowState = window_state + internal_zone = home.search_group_by_id(internal_zone_id) internal_zone.active = internal_active - # Just one call to a security zone is required to refresh the ACP. - internal_zone.fire_update_event() - + home.fire_update_event(json) await hass.async_block_till_done() @@ -70,79 +62,49 @@ async def test_hmip_alarm_control_panel(hass, default_mock_hap): assert not hmip_device home = default_mock_hap.home - service_call_counter = len(home.mock_calls) await hass.services.async_call( "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 1 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (True, True) await _async_manipulate_security_zones( - hass, - home, - internal_active=True, - external_active=True, - window_state=WindowState.CLOSED, + hass, home, internal_active=True, external_active=True ) assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_AWAY await hass.services.async_call( "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 3 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (False, True) - await _async_manipulate_security_zones( - hass, - home, - internal_active=False, - external_active=True, - window_state=WindowState.CLOSED, - ) + await _async_manipulate_security_zones(hass, home, external_active=True) assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_HOME await hass.services.async_call( "alarm_control_panel", "alarm_disarm", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 5 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (False, False) - await _async_manipulate_security_zones( - hass, - home, - internal_active=False, - external_active=False, - window_state=WindowState.CLOSED, - ) + await _async_manipulate_security_zones(hass, home) assert hass.states.get(entity_id).state is STATE_ALARM_DISARMED await hass.services.async_call( "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 7 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (True, True) await _async_manipulate_security_zones( - hass, - home, - internal_active=True, - external_active=True, - window_state=WindowState.OPEN, + hass, home, internal_active=True, external_active=True, alarm_triggered=True ) assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED await hass.services.async_call( "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True ) - assert len(home.mock_calls) == service_call_counter + 9 assert home.mock_calls[-1][0] == "set_security_zones_activation" assert home.mock_calls[-1][1] == (False, True) await _async_manipulate_security_zones( - hass, - home, - internal_active=False, - external_active=True, - window_state=WindowState.OPEN, + hass, home, external_active=True, alarm_triggered=True ) assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 858fba29563519..2b233a6dee2c87 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -343,6 +343,7 @@ async def test_hmip_heating_group_heat_with_switch(hass, default_mock_hap): hass, default_mock_hap, entity_id, entity_name, device_model ) + assert hmip_device assert ha_state.state == HVAC_MODE_AUTO assert ha_state.attributes["current_temperature"] == 24.7 assert ha_state.attributes["min_temp"] == 5.0 diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index ba27a619e6aa4f..eb51c3ece386ff 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -161,5 +161,5 @@ async def test_hmip_dump_hap_config_services(hass, mock_hap_with_service): ) home = mock_hap_with_service.home assert home.mock_calls[-1][0] == "download_configuration" - assert len(home.mock_calls) == 8 # pylint: disable=W0212 - assert len(write_mock.mock_calls) > 0 + assert home.mock_calls + assert write_mock.mock_calls From f8be1512b80c5dea23d1c576b25050d0b4553f1a Mon Sep 17 00:00:00 2001 From: LeoCal <25389602+LeoCal@users.noreply.github.com> Date: Fri, 15 Nov 2019 10:52:15 +0100 Subject: [PATCH 1616/3953] Fix Swisscom empty response received (#28782) --- homeassistant/components/swisscom/device_tracker.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index adb018a4b4bea9..5662212c9e8d90 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -92,6 +92,10 @@ def get_swisscom_data(self): _LOGGER.info("No response from Swisscom Internet Box") return devices + if "status" not in request.json(): + _LOGGER.info("No status in response from Swisscom Internet Box") + return devices + for device in request.json()["status"]: try: devices[device["Key"]] = { From 31cbdbf1dddc25377b8a9afe4c437539ae9d0c17 Mon Sep 17 00:00:00 2001 From: Peter Nijssen Date: Sat, 16 Nov 2019 10:37:58 +0100 Subject: [PATCH 1617/3953] Fix broken postnl sensor (#28794) * fix broken postnl sensor * make sure shipment list is not growing indefinitely --- homeassistant/components/postnl/manifest.json | 2 +- homeassistant/components/postnl/sensor.py | 11 ++++++++--- requirements_all.txt | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/postnl/manifest.json b/homeassistant/components/postnl/manifest.json index d07f9746ee8f28..c45eea0610d380 100644 --- a/homeassistant/components/postnl/manifest.json +++ b/homeassistant/components/postnl/manifest.json @@ -3,7 +3,7 @@ "name": "Postnl", "documentation": "https://www.home-assistant.io/integrations/postnl", "requirements": [ - "postnl_api==1.0.2" + "postnl_api==1.2.2" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/postnl/sensor.py b/homeassistant/components/postnl/sensor.py index cd190c09d87a69..6155f58519a649 100644 --- a/homeassistant/components/postnl/sensor.py +++ b/homeassistant/components/postnl/sensor.py @@ -58,7 +58,7 @@ class PostNLSensor(Entity): def __init__(self, api, name): """Initialize the PostNL sensor.""" self._name = name - self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION, "shipments": []} self._state = None self._api = api @@ -90,6 +90,11 @@ def icon(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update device state.""" - shipments = self._api.get_relevant_shipments() - self._attributes["shipments"] = shipments + shipments = self._api.get_relevant_deliveries() + + self._attributes["shipments"] = [] + + for shipment in shipments: + self._attributes["shipments"].append(vars(shipment)) + self._state = len(shipments) diff --git a/requirements_all.txt b/requirements_all.txt index eb638d9767ebdd..607574b4cfa7eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -999,7 +999,7 @@ pmsensor==0.4 pocketcasts==0.1 # homeassistant.components.postnl -postnl_api==1.0.2 +postnl_api==1.2.2 # homeassistant.components.reddit praw==6.4.0 From b964fcc5b1d5e467ee81da8de0996912116dae11 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 16 Nov 2019 07:22:06 +0100 Subject: [PATCH 1618/3953] Updated frontend to 20191115.0 (#28797) --- 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 b7506f599ef60e..6c59ea38fe49bb 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191114.0" + "home-assistant-frontend==20191115.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dfe53ed2e19e15..3688d6dfa38580 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191114.0 +home-assistant-frontend==20191115.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 607574b4cfa7eb..6f706edc7408a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191114.0 +home-assistant-frontend==20191115.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 582d069016849b..c42188c920ff5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191114.0 +home-assistant-frontend==20191115.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From c6343c9e88ce55b719b544cb647a488849086be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sat, 16 Nov 2019 15:05:17 +0100 Subject: [PATCH 1619/3953] Fix Comfoconnect errors during startup (#28802) * Add callback registrations to async_added_to_hass * Fix CODEOWNERS * Fix code formatting * Requested changes. * Don't pass unused hass and fix string formatting * Fix import order. --- CODEOWNERS | 1 + .../components/comfoconnect/__init__.py | 5 --- homeassistant/components/comfoconnect/fan.py | 37 +++++++++++-------- .../components/comfoconnect/manifest.json | 2 +- .../components/comfoconnect/sensor.py | 36 +++++++++++------- 5 files changed, 46 insertions(+), 35 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 879a1c8f55d019..ec92d918679656 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -61,6 +61,7 @@ homeassistant/components/cisco_webex_teams/* @fbradyirl homeassistant/components/ciscospark/* @fbradyirl homeassistant/components/cloud/* @home-assistant/cloud homeassistant/components/cloudflare/* @ludeeus +homeassistant/components/comfoconnect/* @michaelarnauts homeassistant/components/config/* @home-assistant/core homeassistant/components/configurator/* @home-assistant/core homeassistant/components/conversation/* @home-assistant/core diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index aef4bf1deebca7..efdbf020f1adb6 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -102,7 +102,6 @@ class ComfoConnectBridge: def __init__(self, hass, bridge, name, token, friendly_name, pin): """Initialize the ComfoConnect bridge.""" - self.data = {} self.name = name self.hass = hass @@ -136,7 +135,3 @@ def sensor_callback(self, var, value): # Notify listeners that we have received an update dispatcher_send(self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, var) - - def subscribe_sensor(self, sensor_id): - """Subscribe for the specified sensor.""" - self.comfoconnect.register_sensor(sensor_id) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index bbb4b0176bf84c..34e784d61ebec2 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -17,7 +17,7 @@ SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, ComfoConnectBridge @@ -30,28 +30,36 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ComfoConnect fan platform.""" ccb = hass.data[DOMAIN] - add_entities([ComfoConnectFan(hass, name=ccb.name, ccb=ccb)], True) + add_entities([ComfoConnectFan(ccb.name, ccb)], True) class ComfoConnectFan(FanEntity): """Representation of the ComfoConnect fan platform.""" - def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None: + def __init__(self, name, ccb: ComfoConnectBridge) -> None: """Initialize the ComfoConnect fan.""" - self._ccb = ccb self._name = name - # Ask the bridge to keep us updated - self._ccb.comfoconnect.register_sensor(SENSOR_FAN_SPEED_MODE) - - def _handle_update(var): - if var == SENSOR_FAN_SPEED_MODE: - _LOGGER.debug("Dispatcher update for %s", var) - self.schedule_update_ha_state() + async def async_added_to_hass(self): + """Register for sensor updates.""" + await self.hass.async_add_executor_job( + self._ccb.comfoconnect.register_sensor, SENSOR_FAN_SPEED_MODE + ) + async_dispatcher_connect( + self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update + ) + + def _handle_update(self, var): + """Handle update callbacks.""" + if var == SENSOR_FAN_SPEED_MODE: + _LOGGER.debug("Received update for %s", var) + self.schedule_update_ha_state() - # Register for dispatcher updates - dispatcher_connect(hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False @property def name(self): @@ -71,7 +79,6 @@ def supported_features(self) -> int: @property def speed(self): """Return the current fan mode.""" - try: speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] return SPEED_MAPPING[speed] @@ -95,7 +102,7 @@ def turn_off(self, **kwargs) -> None: def set_speed(self, speed: str): """Set fan speed.""" - _LOGGER.debug("Changing fan speed to %s.", speed) + _LOGGER.debug("Changing fan speed to %s", speed) if speed == SPEED_OFF: self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY) diff --git a/homeassistant/components/comfoconnect/manifest.json b/homeassistant/components/comfoconnect/manifest.json index 57daba7fdbd1dd..091b7f7bcdda4b 100644 --- a/homeassistant/components/comfoconnect/manifest.json +++ b/homeassistant/components/comfoconnect/manifest.json @@ -6,5 +6,5 @@ "pycomfoconnect==0.3" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@michaelarnauts"] } diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 06d0506e2cf39c..a1f16ed9631199 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -11,7 +11,7 @@ ) from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from . import ( @@ -81,13 +81,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_type = resource.lower() if sensor_type not in SENSOR_TYPES: - _LOGGER.warning("Sensor type: %s is not a valid sensor.", sensor_type) + _LOGGER.warning("Sensor type: %s is not a valid sensor", sensor_type) continue sensors.append( ComfoConnectSensor( - hass, - name="%s %s" % (ccb.name, SENSOR_TYPES[sensor_type][0]), + name=f"{ccb.name} {SENSOR_TYPES[sensor_type][0]}", ccb=ccb, sensor_type=sensor_type, ) @@ -99,23 +98,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ComfoConnectSensor(Entity): """Representation of a ComfoConnect sensor.""" - def __init__(self, hass, name, ccb: ComfoConnectBridge, sensor_type) -> None: + def __init__(self, name, ccb: ComfoConnectBridge, sensor_type) -> None: """Initialize the ComfoConnect sensor.""" self._ccb = ccb self._sensor_type = sensor_type self._sensor_id = SENSOR_TYPES[self._sensor_type][3] self._name = name - # Register the requested sensor - self._ccb.comfoconnect.register_sensor(self._sensor_id) - - def _handle_update(var): - if var == self._sensor_id: - _LOGGER.debug("Dispatcher update for %s.", var) - self.schedule_update_ha_state() + async def async_added_to_hass(self): + """Register for sensor updates.""" + await self.hass.async_add_executor_job( + self._ccb.comfoconnect.register_sensor, self._sensor_id + ) + async_dispatcher_connect( + self.hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, self._handle_update + ) - # Register for dispatcher updates - dispatcher_connect(hass, SIGNAL_COMFOCONNECT_UPDATE_RECEIVED, _handle_update) + def _handle_update(self, var): + """Handle update callbacks.""" + if var == self._sensor_id: + _LOGGER.debug("Received update for %s", var) + self.schedule_update_ha_state() @property def state(self): @@ -125,6 +128,11 @@ def state(self): except KeyError: return None + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + @property def name(self): """Return the name of the sensor.""" From fb1fd19aae800f67c2a08a8003ce48e72554425c Mon Sep 17 00:00:00 2001 From: Jackie Yang Date: Sun, 17 Nov 2019 03:18:53 -0800 Subject: [PATCH 1620/3953] Fix miio air quality sensor (#28828) Fix https://github.com/home-assistant/home-assistant/issues/28827 --- homeassistant/components/xiaomi_miio/air_quality.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index b80906aa0cb065..3824c5b88cd529 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -54,7 +54,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_info.firmware_version, device_info.hardware_version, ) - device = AirMonitorB1(name, AirQualityMonitor(host, token, model), unique_id) + device = AirMonitorB1(name, AirQualityMonitor(host, token, model=model), unique_id) async_add_entities([device], update_before_add=True) From d91eddc4f05fc4141c41de5ea0501dcb05f947d7 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 18 Nov 2019 09:10:59 +0100 Subject: [PATCH 1621/3953] Update pyatmo to 3.0.1 (#28829) --- homeassistant/components/netatmo/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index f6c08faf8fa86d..9d1178d9d17026 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/integrations/netatmo", "requirements": [ - "pyatmo==2.3.3" + "pyatmo==3.0.1" ], "dependencies": [ "webhook" diff --git a/requirements_all.txt b/requirements_all.txt index 6f706edc7408a7..4ca7c2ae0f9a69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1105,7 +1105,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.3.3 +pyatmo==3.0.1 # homeassistant.components.atome pyatome==0.1.1 From bf4c81aa5e61b34011aca27d9cf9b4f212938dbe Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 18 Nov 2019 15:40:26 +0100 Subject: [PATCH 1622/3953] Updated frontend to 20191118.0 (#28852) --- 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 6c59ea38fe49bb..51906a100cc8fa 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191115.0" + "home-assistant-frontend==20191118.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3688d6dfa38580..b4ec45e9fdb295 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191115.0 +home-assistant-frontend==20191118.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4ca7c2ae0f9a69..24ab0ce339a2b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191115.0 +home-assistant-frontend==20191118.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c42188c920ff5f..8c8829308cf13f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191115.0 +home-assistant-frontend==20191118.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 7966411274e1d9e7f7be3b7530fa985738f5b166 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 18 Nov 2019 15:44:17 +0100 Subject: [PATCH 1623/3953] Bump version --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 620af2b79db541..0bc6be37dc76a1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From eb23846cfe5a0c92ef11f9bca28426d1492b4bb3 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 18 Nov 2019 16:47:30 +0100 Subject: [PATCH 1624/3953] Move imports in owntracks component (#27780) * Move imports in owntracks component * Fix nacl import * Fix nacl import 2 * Fix nacl import 3 * Add helper.supports_encryption tests * Fix tests helper 1 * Fix tests 2 * Add not_supports_encryption + get_cipher_error tests * Code cov * Fix nacl_not_imported test --- .../components/owntracks/config_flow.py | 11 +----- homeassistant/components/owntracks/helper.py | 10 ++++++ .../components/owntracks/messages.py | 12 +++++-- .../owntracks/test_device_tracker.py | 35 +++++++++++++++++++ tests/components/owntracks/test_helper.py | 29 +++++++++++++++ 5 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/owntracks/helper.py create mode 100644 tests/components/owntracks/test_helper.py diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 5034114293f375..ff4a649e0ce76f 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -4,21 +4,12 @@ from homeassistant.auth.util import generate_secret from .const import DOMAIN # noqa pylint: disable=unused-import +from .helper import supports_encryption CONF_SECRET = "secret" CONF_CLOUDHOOK = "cloudhook" -def supports_encryption(): - """Test if we support encryption.""" - try: - import nacl # noqa: F401 pylint: disable=unused-import - - return True - except OSError: - return False - - class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): """Set up OwnTracks.""" diff --git a/homeassistant/components/owntracks/helper.py b/homeassistant/components/owntracks/helper.py new file mode 100644 index 00000000000000..b6ed307112cc37 --- /dev/null +++ b/homeassistant/components/owntracks/helper.py @@ -0,0 +1,10 @@ +"""Helper for OwnTracks.""" +try: + import nacl +except ImportError: + nacl = None + + +def supports_encryption() -> bool: + """Test if we support encryption.""" + return nacl is not None diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 0cb65c774b5776..7c388f9eb17d73 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -2,6 +2,9 @@ import json import logging +from nacl.secret import SecretBox +from nacl.encoding import Base64Encoder + from homeassistant.components import zone as zone_comp from homeassistant.components.device_tracker import ( SOURCE_TYPE_GPS, @@ -11,6 +14,7 @@ from homeassistant.const import STATE_HOME from homeassistant.util import decorator, slugify +from .helper import supports_encryption _LOGGER = logging.getLogger(__name__) @@ -22,8 +26,6 @@ def get_cipher(): Async friendly. """ - from nacl.secret import SecretBox - from nacl.encoding import Base64Encoder def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" @@ -105,7 +107,11 @@ def _set_gps_from_zone(kwargs, location, zone): def _decrypt_payload(secret, topic, ciphertext): """Decrypt encrypted payload.""" try: - keylen, decrypt = get_cipher() + if supports_encryption(): + keylen, decrypt = get_cipher() + else: + _LOGGER.warning("Ignoring encrypted payload because nacl not installed") + return None except OSError: _LOGGER.warning("Ignoring encrypted payload because nacl not installed") return None diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 5ba68474513a8b..daef624f9c214f 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1406,6 +1406,25 @@ def config_context(hass, setup_comp): patch_save.stop() +@pytest.fixture(name="not_supports_encryption") +def mock_not_supports_encryption(): + """Mock non successful nacl import.""" + with patch( + "homeassistant.components.owntracks.messages.supports_encryption", + return_value=False, + ): + yield + + +@pytest.fixture(name="get_cipher_error") +def mock_get_cipher_error(): + """Mock non successful cipher.""" + with patch( + "homeassistant.components.owntracks.messages.get_cipher", side_effect=OSError() + ): + yield + + @patch("homeassistant.components.owntracks.messages.get_cipher", mock_cipher) async def test_encrypted_payload(hass, setup_comp): """Test encrypted payload.""" @@ -1422,6 +1441,22 @@ async def test_encrypted_payload_topic_key(hass, setup_comp): assert_location_latitude(hass, LOCATION_MESSAGE["lat"]) +async def test_encrypted_payload_not_supports_encryption( + hass, setup_comp, not_supports_encryption +): + """Test encrypted payload with no supported encryption.""" + await setup_owntracks(hass, {CONF_SECRET: TEST_SECRET_KEY}) + await send_message(hass, LOCATION_TOPIC, MOCK_ENCRYPTED_LOCATION_MESSAGE) + assert hass.states.get(DEVICE_TRACKER_STATE) is None + + +async def test_encrypted_payload_get_cipher_error(hass, setup_comp, get_cipher_error): + """Test encrypted payload with no supported encryption.""" + await setup_owntracks(hass, {CONF_SECRET: TEST_SECRET_KEY}) + await send_message(hass, LOCATION_TOPIC, MOCK_ENCRYPTED_LOCATION_MESSAGE) + assert hass.states.get(DEVICE_TRACKER_STATE) is None + + @patch("homeassistant.components.owntracks.messages.get_cipher", mock_cipher) async def test_encrypted_payload_no_key(hass, setup_comp): """Test encrypted payload with no key, .""" diff --git a/tests/components/owntracks/test_helper.py b/tests/components/owntracks/test_helper.py new file mode 100644 index 00000000000000..f870ce82dd385e --- /dev/null +++ b/tests/components/owntracks/test_helper.py @@ -0,0 +1,29 @@ +"""Test the owntracks_http platform.""" +from unittest.mock import patch +import pytest + +from homeassistant.components.owntracks import helper + + +@pytest.fixture(name="nacl_imported") +def mock_nacl_imported(): + """Mock a successful import.""" + with patch("homeassistant.components.owntracks.helper.nacl"): + yield + + +@pytest.fixture(name="nacl_not_imported") +def mock_nacl_not_imported(): + """Mock non successful import.""" + with patch("homeassistant.components.owntracks.helper.nacl", new=None): + yield + + +def test_supports_encryption(nacl_imported): + """Test if env supports encryption.""" + assert helper.supports_encryption() + + +def test_supports_encryption_failed(nacl_not_imported): + """Test if env does not support encryption.""" + assert not helper.supports_encryption() From 1a46294e6d0c0ae55075ecc502baba44dc14383c Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 18 Nov 2019 16:59:07 +0100 Subject: [PATCH 1625/3953] Update roomba integration (#28560) * Updated iRobot integration * added custom delay for periodic mode * new version of roombapy --- homeassistant/components/roomba/manifest.json | 2 +- homeassistant/components/roomba/vacuum.py | 5 +++++ requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 5064357a7df48a..700827c1a65730 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,7 +3,7 @@ "name": "Roomba", "documentation": "https://www.home-assistant.io/integrations/roomba", "requirements": [ - "roombapy==1.3.1" + "roombapy==1.4.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index 291658e19f4b2a..fd74fd190a8913 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -39,9 +39,11 @@ CONF_CERT = "certificate" CONF_CONTINUOUS = "continuous" +CONF_DELAY = "delay" DEFAULT_CERT = "/etc/ssl/certs/ca-certificates.crt" DEFAULT_CONTINUOUS = True +DEFAULT_DELAY = 1 DEFAULT_NAME = "Roomba" PLATFORM = "roomba" @@ -59,6 +61,7 @@ vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_CERT, default=DEFAULT_CERT): cv.string, vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): cv.boolean, + vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int, }, extra=vol.ALLOW_EXTRA, ) @@ -93,6 +96,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= password = config.get(CONF_PASSWORD) certificate = config.get(CONF_CERT) continuous = config.get(CONF_CONTINUOUS) + delay = config.get(CONF_DELAY) roomba = Roomba( address=host, @@ -100,6 +104,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= password=password, cert_name=certificate, continuous=continuous, + delay=delay, ) _LOGGER.debug("Initializing communication with host %s", host) diff --git a/requirements_all.txt b/requirements_all.txt index 49ec2bda97cd38..b70ae848897746 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1728,7 +1728,7 @@ rocketchat-API==0.6.1 roku==3.1 # homeassistant.components.roomba -roombapy==1.3.1 +roombapy==1.4.1 # homeassistant.components.rova rova==0.1.0 From 61bb24c423d98e9183677c1bf7aa79690e14a9a5 Mon Sep 17 00:00:00 2001 From: Andi Date: Mon, 18 Nov 2019 17:03:10 +0100 Subject: [PATCH 1626/3953] Fix Synology camera whitelist (#28822) * Fix Synology camera whitelist If whitelist config is set, not camera is added to HA at all. * Fix Synology Camera whitelist Fix typo in config key. * Update camera.py Access config dict the voluptuous way --- homeassistant/components/synology/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py index 8c176f488034ca..91ee5a98fc328c 100644 --- a/homeassistant/components/synology/camera.py +++ b/homeassistant/components/synology/camera.py @@ -62,7 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # add cameras devices = [] for camera in cameras: - if not config.get(CONF_WHITELIST): + if not config[CONF_WHITELIST] or camera.name in config[CONF_WHITELIST]: device = SynologyCamera(surveillance, camera.camera_id, verify_ssl) devices.append(device) From 8439e597ce165c6dbd2365fcaf94abdb8aa909b1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 19 Nov 2019 00:32:19 +0000 Subject: [PATCH 1627/3953] [ci skip] Translation update --- .../hisense_aehw4a1/.translations/lb.json | 15 +++++++++++++++ .../hisense_aehw4a1/.translations/no.json | 15 +++++++++++++++ .../hisense_aehw4a1/.translations/pl.json | 15 +++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/lb.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/no.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/pl.json diff --git a/homeassistant/components/hisense_aehw4a1/.translations/lb.json b/homeassistant/components/hisense_aehw4a1/.translations/lb.json new file mode 100644 index 00000000000000..33b93348300180 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/lb.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Hisense AEH-W4A1 Apparater am Netzwierk fonnt.", + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun Hisense AEH-W4A1 ass m\u00e9iglech." + }, + "step": { + "confirm": { + "description": "Soll Hisense AEH-W4A1 konfigur\u00e9iert ginn?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/no.json b/homeassistant/components/hisense_aehw4a1/.translations/no.json new file mode 100644 index 00000000000000..e44e818ea60761 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Hisense AEH-W4A1-enheter funnet p\u00e5 nettverket.", + "single_instance_allowed": "Bare en enkelt konfigurasjon av Hisense AEH-W4A1 er mulig." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/pl.json b/homeassistant/components/hisense_aehw4a1/.translations/pl.json new file mode 100644 index 00000000000000..e0ab5cddbda919 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/pl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono w sieci urz\u0105dze\u0144 Hisense AEH-W4A1.", + "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "Chcesz skonfigurowa\u0107 AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file From 4e9e9efa43d057a860f7fec0d3b0f4efc159943e Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 20 Nov 2019 00:00:59 +1100 Subject: [PATCH 1628/3953] GeoNet NZ Volcanic Alert Level sensor (#26901) * first version of new integration * moved icon to shared consts * added unit tests * transformed from geolocation to sensor integration * alert level is now the state of the sensor * adopted unit tests * fixed comment * keep all sensors registered even if the feed update fails intermittently * bumped version of upstream library * bumped version of integration library * regenerated requirements * bumped version of integration library * bumped version of integration library * fixed generated file * removed commented out code * regenerated config flow file * update to latest integration library version * simplified code * removed debug log statement * simplified code structure * defined constant * use core interfaces * moved test and fixture * sorted imports * simplified patching * moved fixture to central config file --- CODEOWNERS | 1 + .../geonetnz_volcano/.translations/en.json | 16 ++ .../components/geonetnz_volcano/__init__.py | 205 ++++++++++++++++++ .../geonetnz_volcano/config_flow.py | 74 +++++++ .../components/geonetnz_volcano/const.py | 19 ++ .../components/geonetnz_volcano/manifest.json | 13 ++ .../components/geonetnz_volcano/sensor.py | 169 +++++++++++++++ .../components/geonetnz_volcano/strings.json | 16 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/geonetnz_volcano/__init__.py | 25 +++ tests/components/geonetnz_volcano/conftest.py | 28 +++ .../geonetnz_volcano/test_config_flow.py | 81 +++++++ .../components/geonetnz_volcano/test_init.py | 22 ++ .../geonetnz_volcano/test_sensor.py | 168 ++++++++++++++ 16 files changed, 844 insertions(+) create mode 100644 homeassistant/components/geonetnz_volcano/.translations/en.json create mode 100644 homeassistant/components/geonetnz_volcano/__init__.py create mode 100644 homeassistant/components/geonetnz_volcano/config_flow.py create mode 100644 homeassistant/components/geonetnz_volcano/const.py create mode 100644 homeassistant/components/geonetnz_volcano/manifest.json create mode 100644 homeassistant/components/geonetnz_volcano/sensor.py create mode 100644 homeassistant/components/geonetnz_volcano/strings.json create mode 100644 tests/components/geonetnz_volcano/__init__.py create mode 100644 tests/components/geonetnz_volcano/conftest.py create mode 100644 tests/components/geonetnz_volcano/test_config_flow.py create mode 100644 tests/components/geonetnz_volcano/test_init.py create mode 100644 tests/components/geonetnz_volcano/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index ee6a8cd169cb62..0daf10d05663fa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -112,6 +112,7 @@ homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte +homeassistant/components/geonetnz_volcano/* @exxamalte homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/gntp/* @robbiet480 diff --git a/homeassistant/components/geonetnz_volcano/.translations/en.json b/homeassistant/components/geonetnz_volcano/.translations/en.json new file mode 100644 index 00000000000000..1175597908e906 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Location already registered" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Fill in your filter details." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py new file mode 100644 index 00000000000000..f0887da9c06fb3 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -0,0 +1,205 @@ +"""The GeoNet NZ Volcano integration.""" +import asyncio +import logging +from datetime import timedelta, datetime +from typing import Optional + +import voluptuous as vol +from aio_geojson_geonetnz_volcano import GeonetnzVolcanoFeedManager + +from homeassistant.core import callback +from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM, + LENGTH_MILES, +) +from homeassistant.helpers import config_validation as cv, aiohttp_client +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from .config_flow import configured_instances +from .const import ( + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + FEED, + SIGNAL_NEW_SENSOR, + SIGNAL_UPDATE_ENTITY, +) + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + 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 Volcano 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) + 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_SCAN_INTERVAL: scan_interval, + }, + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the GeoNet NZ Volcano component as config entry.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN].setdefault(FEED, {}) + + radius = config_entry.data[CONF_RADIUS] + unit_system = config_entry.data[CONF_UNIT_SYSTEM] + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + # Create feed entity manager for all platforms. + manager = GeonetnzVolcanoFeedEntityManager(hass, config_entry, radius, unit_system) + hass.data[DOMAIN][FEED][config_entry.entry_id] = manager + _LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id) + await manager.async_init() + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an GeoNet NZ Volcano component config entry.""" + manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + await manager.async_stop() + await asyncio.wait( + [hass.config_entries.async_forward_entry_unload(config_entry, "sensor")] + ) + return True + + +class GeonetnzVolcanoFeedEntityManager: + """Feed Entity Manager for GeoNet NZ Volcano feed.""" + + def __init__(self, hass, config_entry, radius_in_km, unit_system): + """Initialize the Feed Entity Manager.""" + self._hass = hass + self._config_entry = config_entry + coordinates = ( + config_entry.data[CONF_LATITUDE], + config_entry.data[CONF_LONGITUDE], + ) + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = GeonetnzVolcanoFeedManager( + websession, + self._generate_entity, + self._update_entity, + self._remove_entity, + coordinates, + filter_radius=radius_in_km, + ) + self._config_entry_id = config_entry.entry_id + self._scan_interval = timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]) + self._unit_system = unit_system + self._track_time_remove_callback = None + self.listeners = [] + + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" + + self._hass.async_create_task( + self._hass.config_entries.async_forward_entry_setup( + self._config_entry, "sensor" + ) + ) + + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + 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.""" + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + self.listeners = [] + if self._track_time_remove_callback: + self._track_time_remove_callback() + _LOGGER.debug("Feed entity manager stopped") + + @callback + def async_event_new_entity(self): + """Return manager specific event to signal new entity.""" + return SIGNAL_NEW_SENSOR.format(self._config_entry_id) + + def get_entry(self, external_id): + """Get feed entry by external id.""" + return self._feed_manager.feed_entries.get(external_id) + + def last_update(self) -> Optional[datetime]: + """Return the last update of this feed.""" + return self._feed_manager.last_update + + def last_update_successful(self) -> Optional[datetime]: + """Return the last successful update of this feed.""" + return self._feed_manager.last_update_successful + + async def _generate_entity(self, external_id): + """Generate new entity.""" + async_dispatcher_send( + self._hass, + self.async_event_new_entity(), + self, + external_id, + self._unit_system, + ) + + 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): + """Ignore removing entity.""" diff --git a/homeassistant/components/geonetnz_volcano/config_flow.py b/homeassistant/components/geonetnz_volcano/config_flow.py new file mode 100644 index 00000000000000..7c079c432ddd05 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/config_flow.py @@ -0,0 +1,74 @@ +"""Config flow to configure the GeoNet NZ Volcano 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 DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@callback +def configured_instances(hass): + """Return a set of configured GeoNet NZ Volcano instances.""" + return set( + f"{entry.data[CONF_LATITUDE]}, {entry.data[CONF_LONGITUDE]}" + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class GeonetnzVolcanoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a GeoNet NZ Volcano 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_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.""" + 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 + + return self.async_create_entry(title=identifier, data=user_input) diff --git a/homeassistant/components/geonetnz_volcano/const.py b/homeassistant/components/geonetnz_volcano/const.py new file mode 100644 index 00000000000000..7bc15d3a6a1cd7 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/const.py @@ -0,0 +1,19 @@ +"""Define constants for the GeoNet NZ Volcano integration.""" +from datetime import timedelta + +DOMAIN = "geonetnz_volcano" + +FEED = "feed" + +ATTR_ACTIVITY = "activity" +ATTR_DISTANCE = "distance" +ATTR_EXTERNAL_ID = "external_id" +ATTR_HAZARDS = "hazards" + +# Icon alias "mdi:mountain" not working. +DEFAULT_ICON = "mdi:image-filter-hdr" +DEFAULT_RADIUS = 50.0 +DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) + +SIGNAL_NEW_SENSOR = "geonetnz_volcano_new_sensor_{}" +SIGNAL_UPDATE_ENTITY = "geonetnz_volcano_update_{}" diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json new file mode 100644 index 00000000000000..a80ebdcff65589 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "geonetnz_volcano", + "name": "GeoNet NZ Volcano", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/geonetnz_volcano", + "requirements": [ + "aio_geojson_geonetnz_volcano==0.5" + ], + "dependencies": [], + "codeowners": [ + "@exxamalte" + ] +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py new file mode 100644 index 00000000000000..364ee416be4146 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -0,0 +1,169 @@ +"""Feed Entity Manager Sensor support for GeoNet NZ Volcano Feeds.""" +import logging +from typing import Optional + +from homeassistant.const import ( + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, + ATTR_ATTRIBUTION, + ATTR_LONGITUDE, + ATTR_LATITUDE, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.util import dt +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from .const import ( + ATTR_ACTIVITY, + ATTR_DISTANCE, + ATTR_EXTERNAL_ID, + ATTR_HAZARDS, + DEFAULT_ICON, + DOMAIN, + FEED, + SIGNAL_UPDATE_ENTITY, +) + +_LOGGER = logging.getLogger(__name__) + +ATTR_LAST_UPDATE = "feed_last_update" +ATTR_LAST_UPDATE_SUCCESSFUL = "feed_last_update_successful" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the GeoNet NZ Volcano Feed platform.""" + manager = hass.data[DOMAIN][FEED][entry.entry_id] + + @callback + def async_add_sensor(feed_manager, external_id, unit_system): + """Add sensor entity from feed.""" + new_entity = GeonetnzVolcanoSensor( + entry.entry_id, feed_manager, external_id, unit_system + ) + _LOGGER.debug("Adding sensor %s", new_entity) + async_add_entities([new_entity], True) + + manager.listeners.append( + async_dispatcher_connect( + hass, manager.async_event_new_entity(), async_add_sensor + ) + ) + hass.async_create_task(manager.async_update()) + _LOGGER.debug("Sensor setup done") + + +class GeonetnzVolcanoSensor(Entity): + """This represents an external event with GeoNet NZ Volcano feed data.""" + + def __init__(self, config_entry_id, feed_manager, external_id, unit_system): + """Initialize entity with data from feed entry.""" + self._config_entry_id = config_entry_id + 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._alert_level = None + self._activity = None + self._hazards = None + self._feed_last_update = None + self._feed_last_update_successful = None + self._remove_signal_update = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + 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.""" + if self._remove_signal_update: + self._remove_signal_update() + + @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 Volcano 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) + last_update = self._feed_manager.last_update() + last_update_successful = self._feed_manager.last_update_successful() + if feed_entry: + self._update_from_feed(feed_entry, last_update, last_update_successful) + + def _update_from_feed(self, feed_entry, last_update, last_update_successful): + """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 = round( + IMPERIAL_SYSTEM.length(feed_entry.distance_to_home, LENGTH_KILOMETERS), + 1, + ) + else: + self._distance = round(feed_entry.distance_to_home, 1) + self._latitude = round(feed_entry.coordinates[0], 5) + self._longitude = round(feed_entry.coordinates[1], 5) + self._attribution = feed_entry.attribution + self._alert_level = feed_entry.alert_level + self._activity = feed_entry.activity + self._hazards = feed_entry.hazards + self._feed_last_update = dt.as_utc(last_update) if last_update else None + self._feed_last_update_successful = ( + dt.as_utc(last_update_successful) if last_update_successful else None + ) + + @property + def state(self): + """Return the state of the sensor.""" + return self._alert_level + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEFAULT_ICON + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return f"Volcano {self._title}" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return "alert level" + + @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_ACTIVITY, self._activity), + (ATTR_HAZARDS, self._hazards), + (ATTR_LONGITUDE, self._longitude), + (ATTR_LATITUDE, self._latitude), + (ATTR_DISTANCE, self._distance), + (ATTR_LAST_UPDATE, self._feed_last_update), + (ATTR_LAST_UPDATE_SUCCESSFUL, self._feed_last_update_successful), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/homeassistant/components/geonetnz_volcano/strings.json b/homeassistant/components/geonetnz_volcano/strings.json new file mode 100644 index 00000000000000..93ec8603d03b0a --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "title": "GeoNet NZ Volcano", + "step": { + "user": { + "title": "Fill in your filter details.", + "data": { + "radius": "Radius" + } + } + }, + "error": { + "identifier_exists": "Location already registered" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0cec08d94d9805..c496fc99cf1ecd 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -24,6 +24,7 @@ "esphome", "geofency", "geonetnz_quakes", + "geonetnz_volcano", "glances", "gpslogger", "hangouts", diff --git a/requirements_all.txt b/requirements_all.txt index b70ae848897746..77db871edc1ba2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -126,6 +126,9 @@ afsapi==0.0.4 # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.11 +# homeassistant.components.geonetnz_volcano +aio_geojson_geonetnz_volcano==0.5 + # homeassistant.components.ambient_station aioambient==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a94664ad0ab54..15b480a5547b53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -37,6 +37,9 @@ adguardhome==0.3.0 # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.11 +# homeassistant.components.geonetnz_volcano +aio_geojson_geonetnz_volcano==0.5 + # homeassistant.components.ambient_station aioambient==0.3.2 diff --git a/tests/components/geonetnz_volcano/__init__.py b/tests/components/geonetnz_volcano/__init__.py new file mode 100644 index 00000000000000..708b69e003190d --- /dev/null +++ b/tests/components/geonetnz_volcano/__init__.py @@ -0,0 +1,25 @@ +"""The tests for the GeoNet NZ Volcano Feed integration.""" +from unittest.mock import MagicMock + + +def _generate_mock_feed_entry( + external_id, + title, + alert_level, + distance_to_home, + coordinates, + attribution=None, + activity=None, + hazards=None, +): + """Construct a mock feed entry for testing purposes.""" + feed_entry = MagicMock() + feed_entry.external_id = external_id + feed_entry.title = title + feed_entry.alert_level = alert_level + feed_entry.distance_to_home = distance_to_home + feed_entry.coordinates = coordinates + feed_entry.attribution = attribution + feed_entry.activity = activity + feed_entry.hazards = hazards + return feed_entry diff --git a/tests/components/geonetnz_volcano/conftest.py b/tests/components/geonetnz_volcano/conftest.py new file mode 100644 index 00000000000000..55231cd31207ea --- /dev/null +++ b/tests/components/geonetnz_volcano/conftest.py @@ -0,0 +1,28 @@ +"""Configuration for GeoNet NZ Volcano tests.""" +import pytest + +from homeassistant.components.geonetnz_volcano import DOMAIN +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 Volcano 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, + }, + title="-41.2, 174.7", + ) diff --git a/tests/components/geonetnz_volcano/test_config_flow.py b/tests/components/geonetnz_volcano/test_config_flow.py new file mode 100644 index 00000000000000..8f589aded9032e --- /dev/null +++ b/tests/components/geonetnz_volcano/test_config_flow.py @@ -0,0 +1,81 @@ +"""Define tests for the GeoNet NZ Volcano config flow.""" +from datetime import timedelta + +from homeassistant import data_entry_flow +from homeassistant.components.geonetnz_volcano import config_flow +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, +) + + +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.GeonetnzVolcanoFlowHandler() + 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.GeonetnzVolcanoFlowHandler() + 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_SCAN_INTERVAL: timedelta(minutes=4), + } + + flow = config_flow.GeonetnzVolcanoFlowHandler() + 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_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 240.0, + } + + +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} + + flow = config_flow.GeonetnzVolcanoFlowHandler() + 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_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 300.0, + } diff --git a/tests/components/geonetnz_volcano/test_init.py b/tests/components/geonetnz_volcano/test_init.py new file mode 100644 index 00000000000000..3e2566ffb81711 --- /dev/null +++ b/tests/components/geonetnz_volcano/test_init.py @@ -0,0 +1,22 @@ +"""Define tests for the GeoNet NZ Volcano general setup.""" +from asynctest import CoroutineMock, patch + +from homeassistant.components.geonetnz_volcano import DOMAIN, FEED + + +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_volcano.GeonetnzVolcanoFeedManager.update", + new_callable=CoroutineMock, + ) as mock_feed_manager_update: + # Load config entry. + assert await hass.config_entries.async_setup(config_entry.entry_id) + 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 hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN][FEED].get(config_entry.entry_id) is None diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py new file mode 100644 index 00000000000000..8f71e3c475708b --- /dev/null +++ b/tests/components/geonetnz_volcano/test_sensor.py @@ -0,0 +1,168 @@ +"""The tests for the GeoNet NZ Volcano Feed integration.""" +from asynctest import CoroutineMock, patch + +from homeassistant.components import geonetnz_volcano +from homeassistant.components.geo_location import ATTR_DISTANCE +from homeassistant.components.geonetnz_volcano import DEFAULT_SCAN_INTERVAL +from homeassistant.components.geonetnz_volcano.const import ( + ATTR_ACTIVITY, + ATTR_EXTERNAL_ID, + ATTR_HAZARDS, +) +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_UNIT_OF_MEASUREMENT, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, +) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM + +from tests.common import async_fire_time_changed +from tests.components.geonetnz_volcano import _generate_mock_feed_entry + +CONFIG = {geonetnz_volcano.DOMAIN: {CONF_RADIUS: 200}} + + +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", + 1, + 15.5, + (38.0, -3.0), + attribution="Attribution 1", + activity="Activity 1", + hazards="Hazards 1", + ) + mock_entry_2 = _generate_mock_feed_entry("2345", "Title 2", 0, 20.5, (38.1, -3.1)) + mock_entry_3 = _generate_mock_feed_entry("3456", "Title 3", 2, 25.5, (38.2, -3.2)) + mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 1, 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_volcano.DOMAIN, CONFIG) + # Artificially trigger update and collect events. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + # 3 sensor entities + assert len(all_states) == 3 + + state = hass.states.get("sensor.volcano_title_1") + assert state is not None + assert state.name == "Volcano Title 1" + assert int(state.state) == 1 + assert state.attributes[ATTR_EXTERNAL_ID] == "1234" + assert state.attributes[ATTR_LATITUDE] == 38.0 + assert state.attributes[ATTR_LONGITUDE] == -3.0 + assert state.attributes[ATTR_DISTANCE] == 15.5 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 1" + assert state.attributes[ATTR_ATTRIBUTION] == "Attribution 1" + assert state.attributes[ATTR_ACTIVITY] == "Activity 1" + assert state.attributes[ATTR_HAZARDS] == "Hazards 1" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "alert level" + assert state.attributes[ATTR_ICON] == "mdi:image-filter-hdr" + + state = hass.states.get("sensor.volcano_title_2") + assert state is not None + assert state.name == "Volcano Title 2" + assert int(state.state) == 0 + assert state.attributes[ATTR_EXTERNAL_ID] == "2345" + assert state.attributes[ATTR_LATITUDE] == 38.1 + assert state.attributes[ATTR_LONGITUDE] == -3.1 + assert state.attributes[ATTR_DISTANCE] == 20.5 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 2" + + state = hass.states.get("sensor.volcano_title_3") + assert state is not None + assert state.name == "Volcano Title 3" + assert int(state.state) == 2 + assert state.attributes[ATTR_EXTERNAL_ID] == "3456" + assert state.attributes[ATTR_LATITUDE] == 38.2 + assert state.attributes[ATTR_LONGITUDE] == -3.2 + assert state.attributes[ATTR_DISTANCE] == 25.5 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 3" + + # Simulate an update - two 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) == 4 + + # 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) == 4 + + # Simulate an update - empty data, keep 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) == 4 + + # Simulate an update - regular data for 3 entries + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] + async_fire_time_changed(hass, utcnow + 4 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 4 + + +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", 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__" + ) as mock_feed_init: + mock_feed_update.return_value = "OK", [mock_entry_1] + assert await async_setup_component(hass, geonetnz_volcano.DOMAIN, CONFIG) + # Artificially trigger update and collect events. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + 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("sensor.volcano_title_1") + assert state is not None + assert state.name == "Volcano Title 1" + assert int(state.state) == 1 + assert state.attributes[ATTR_EXTERNAL_ID] == "1234" + assert state.attributes[ATTR_LATITUDE] == 38.0 + assert state.attributes[ATTR_LONGITUDE] == -3.0 + assert state.attributes[ATTR_DISTANCE] == 9.6 + assert state.attributes[ATTR_FRIENDLY_NAME] == "Volcano Title 1" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "alert level" + assert state.attributes[ATTR_ICON] == "mdi:image-filter-hdr" From 475c8ebae20f0bcb79aee23576103051ef13318f Mon Sep 17 00:00:00 2001 From: Steven Impens Date: Tue, 19 Nov 2019 14:05:23 +0100 Subject: [PATCH 1629/3953] Add component VersaSense (#24619) * Add component VersaSense * Updates based on review * Changes based on review * Fixed whitespace * Fixed lines too long * Fixed lines too long * Formatted using black * Added available property * Set unavailable property appropriately * Conversion to f-strings * Load platform only once per platform * Fixed duplicate identifiers across multiple devices * Single call to async_add_entities during setup * Removed unnecessary async/await syntax * Added constants for key-value pairs * Removed async/await syntax * Added breaks in measurement check * Added guard clause for discovery_info --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/versasense/__init__.py | 97 +++++++++++++++ homeassistant/components/versasense/const.py | 11 ++ .../components/versasense/manifest.json | 8 ++ homeassistant/components/versasense/sensor.py | 97 +++++++++++++++ homeassistant/components/versasense/switch.py | 113 ++++++++++++++++++ requirements_all.txt | 3 + 8 files changed, 331 insertions(+) create mode 100644 homeassistant/components/versasense/__init__.py create mode 100644 homeassistant/components/versasense/const.py create mode 100644 homeassistant/components/versasense/manifest.json create mode 100644 homeassistant/components/versasense/sensor.py create mode 100644 homeassistant/components/versasense/switch.py diff --git a/.coveragerc b/.coveragerc index b5939bafec6a8c..ab4e91e5efd722 100644 --- a/.coveragerc +++ b/.coveragerc @@ -742,6 +742,7 @@ omit = homeassistant/components/venstar/climate.py homeassistant/components/vera/* homeassistant/components/verisure/* + homeassistant/components/versasense/* homeassistant/components/vesync/__init__.py homeassistant/components/vesync/common.py homeassistant/components/vesync/const.py diff --git a/CODEOWNERS b/CODEOWNERS index 0daf10d05663fa..c9d9dc129a5846 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -332,6 +332,7 @@ homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes homeassistant/components/velbus/* @cereal2nd homeassistant/components/velux/* @Julius2342 +homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe homeassistant/components/vicare/* @oischinger diff --git a/homeassistant/components/versasense/__init__.py b/homeassistant/components/versasense/__init__.py new file mode 100644 index 00000000000000..4f378f4ab00c9e --- /dev/null +++ b/homeassistant/components/versasense/__init__.py @@ -0,0 +1,97 @@ +"""Support for VersaSense MicroPnP devices.""" +import logging + +import pyversasense as pyv +import voluptuous as vol + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import aiohttp_client +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import async_load_platform + +from .const import ( + PERIPHERAL_CLASS_SENSOR, + PERIPHERAL_CLASS_SENSOR_ACTUATOR, + KEY_IDENTIFIER, + KEY_PARENT_NAME, + KEY_PARENT_MAC, + KEY_UNIT, + KEY_MEASUREMENT, + KEY_CONSUMER, +) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "versasense" + +# Validation of the user's configuration +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA +) + + +async def async_setup(hass, config): + """Set up the versasense component.""" + session = aiohttp_client.async_get_clientsession(hass) + consumer = pyv.Consumer(config[DOMAIN]["host"], session) + + hass.data[DOMAIN] = {KEY_CONSUMER: consumer} + + await _configure_entities(hass, config, consumer) + + # Return boolean to indicate that initialization was successful. + return True + + +async def _configure_entities(hass, config, consumer): + """Fetch all devices with their peripherals for representation.""" + devices = await consumer.fetchDevices() + _LOGGER.debug(devices) + + sensor_info_list = [] + switch_info_list = [] + + for mac, device in devices.items(): + _LOGGER.info("Device connected: %s %s", device.name, mac) + hass.data[DOMAIN][mac] = {} + + for peripheral_id, peripheral in device.peripherals.items(): + hass.data[DOMAIN][mac][peripheral_id] = peripheral + + if peripheral.classification == PERIPHERAL_CLASS_SENSOR: + sensor_info_list = _add_entity_info_to_list( + peripheral, device, sensor_info_list + ) + elif peripheral.classification == PERIPHERAL_CLASS_SENSOR_ACTUATOR: + switch_info_list = _add_entity_info_to_list( + peripheral, device, switch_info_list + ) + + if sensor_info_list: + _load_platform(hass, config, "sensor", sensor_info_list) + + if switch_info_list: + _load_platform(hass, config, "switch", switch_info_list) + + +def _add_entity_info_to_list(peripheral, device, entity_info_list): + """Add info from a peripheral to specified list.""" + for measurement in peripheral.measurements: + entity_info = { + KEY_IDENTIFIER: peripheral.identifier, + KEY_UNIT: measurement.unit, + KEY_MEASUREMENT: measurement.name, + KEY_PARENT_NAME: device.name, + KEY_PARENT_MAC: device.mac, + } + + entity_info_list.append(entity_info) + + return entity_info_list + + +def _load_platform(hass, config, entity_type, entity_info_list): + """Load platform with list of entity info.""" + hass.async_create_task( + async_load_platform(hass, entity_type, DOMAIN, entity_info_list, config) + ) diff --git a/homeassistant/components/versasense/const.py b/homeassistant/components/versasense/const.py new file mode 100644 index 00000000000000..5283f61ac261cd --- /dev/null +++ b/homeassistant/components/versasense/const.py @@ -0,0 +1,11 @@ +"""Constants for versasense.""" +KEY_CONSUMER = "consumer" +KEY_IDENTIFIER = "identifier" +KEY_MEASUREMENT = "measurement" +KEY_PARENT_MAC = "parent_mac" +KEY_PARENT_NAME = "parent_name" +KEY_UNIT = "unit" +PERIPHERAL_CLASS_SENSOR = "sensor" +PERIPHERAL_CLASS_SENSOR_ACTUATOR = "sensor-actuator" +PERIPHERAL_STATE_OFF = "OFF" +PERIPHERAL_STATE_ON = "ON" diff --git a/homeassistant/components/versasense/manifest.json b/homeassistant/components/versasense/manifest.json new file mode 100644 index 00000000000000..3e2be6131d110b --- /dev/null +++ b/homeassistant/components/versasense/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "versasense", + "name": "VersaSense", + "documentation": "https://www.home-assistant.io/components/versasense", + "dependencies": [], + "codeowners": ["@flamm3blemuff1n"], + "requirements": ["pyversasense==0.0.6"] +} diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py new file mode 100644 index 00000000000000..4253bfcbba4cda --- /dev/null +++ b/homeassistant/components/versasense/sensor.py @@ -0,0 +1,97 @@ +"""Support for VersaSense sensor peripheral.""" +import logging + +from homeassistant.helpers.entity import Entity + +from . import DOMAIN +from .const import ( + KEY_IDENTIFIER, + KEY_PARENT_NAME, + KEY_PARENT_MAC, + KEY_UNIT, + KEY_MEASUREMENT, + KEY_CONSUMER, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the sensor platform.""" + if discovery_info is None: + return None + + consumer = hass.data[DOMAIN][KEY_CONSUMER] + + sensor_list = [] + + for entity_info in discovery_info: + peripheral = hass.data[DOMAIN][entity_info[KEY_PARENT_MAC]][ + entity_info[KEY_IDENTIFIER] + ] + parent_name = entity_info[KEY_PARENT_NAME] + unit = entity_info[KEY_UNIT] + measurement = entity_info[KEY_MEASUREMENT] + + sensor_list.append( + VSensor(peripheral, parent_name, unit, measurement, consumer) + ) + + async_add_entities(sensor_list) + + +class VSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, peripheral, parent_name, unit, measurement, consumer): + """Initialize the sensor.""" + self._state = None + self._available = True + self._name = f"{parent_name} {measurement}" + self._parent_mac = peripheral.parentMac + self._identifier = peripheral.identifier + self._unit = unit + self._measurement = measurement + self.consumer = consumer + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return f"{self._parent_mac}/{self._identifier}/{self._measurement}" + + @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 unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def available(self): + """Return if the sensor is available.""" + return self._available + + async def async_update(self): + """Fetch new state data for the sensor.""" + samples = await self.consumer.fetchPeripheralSample( + None, self._identifier, self._parent_mac + ) + + if samples is not None: + for sample in samples: + if sample.measurement == self._measurement: + self._available = True + self._state = sample.value + break + else: + _LOGGER.error("Sample unavailable") + self._available = False + self._state = None diff --git a/homeassistant/components/versasense/switch.py b/homeassistant/components/versasense/switch.py new file mode 100644 index 00000000000000..4ea118a6c97c38 --- /dev/null +++ b/homeassistant/components/versasense/switch.py @@ -0,0 +1,113 @@ +"""Support for VersaSense actuator peripheral.""" +import logging + +from homeassistant.components.switch import SwitchDevice + +from . import DOMAIN +from .const import ( + PERIPHERAL_STATE_ON, + PERIPHERAL_STATE_OFF, + KEY_IDENTIFIER, + KEY_PARENT_NAME, + KEY_PARENT_MAC, + KEY_UNIT, + KEY_MEASUREMENT, + KEY_CONSUMER, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up actuator platform.""" + if discovery_info is None: + return None + + consumer = hass.data[DOMAIN][KEY_CONSUMER] + + actuator_list = [] + + for entity_info in discovery_info: + peripheral = hass.data[DOMAIN][entity_info[KEY_PARENT_MAC]][ + entity_info[KEY_IDENTIFIER] + ] + parent_name = entity_info[KEY_PARENT_NAME] + unit = entity_info[KEY_UNIT] + measurement = entity_info[KEY_MEASUREMENT] + + actuator_list.append( + VActuator(peripheral, parent_name, unit, measurement, consumer) + ) + + async_add_entities(actuator_list) + + +class VActuator(SwitchDevice): + """Representation of an Actuator.""" + + def __init__(self, peripheral, parent_name, unit, measurement, consumer): + """Initialize the sensor.""" + self._is_on = False + self._available = True + self._name = f"{parent_name} {measurement}" + self._parent_mac = peripheral.parentMac + self._identifier = peripheral.identifier + self._unit = unit + self._measurement = measurement + self.consumer = consumer + + @property + def unique_id(self): + """Return the unique id of the actuator.""" + return f"{self._parent_mac}/{self._identifier}/{self._measurement}" + + @property + def name(self): + """Return the name of the actuator.""" + return self._name + + @property + def is_on(self): + """Return the state of the actuator.""" + return self._is_on + + @property + def available(self): + """Return if the actuator is available.""" + return self._available + + async def async_turn_off(self, **kwargs): + """Turn off the actuator.""" + await self.update_state(0) + + async def async_turn_on(self, **kwargs): + """Turn on the actuator.""" + await self.update_state(1) + + async def update_state(self, state): + """Update the state of the actuator.""" + payload = {"id": "state-num", "value": state} + + await self.consumer.actuatePeripheral( + None, self._identifier, self._parent_mac, payload + ) + + async def async_update(self): + """Fetch state data from the actuator.""" + samples = await self.consumer.fetchPeripheralSample( + None, self._identifier, self._parent_mac + ) + + if samples is not None: + for sample in samples: + if sample.measurement == self._measurement: + self._available = True + if sample.value == PERIPHERAL_STATE_OFF: + self._is_on = False + elif sample.value == PERIPHERAL_STATE_ON: + self._is_on = True + break + else: + _LOGGER.error("Sample unavailable") + self._available = False + self._is_on = None diff --git a/requirements_all.txt b/requirements_all.txt index 77db871edc1ba2..b368f35349b0ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1655,6 +1655,9 @@ pyuptimerobot==0.0.5 # homeassistant.components.vera pyvera==0.3.6 +# homeassistant.components.versasense +pyversasense==0.0.6 + # homeassistant.components.vesync pyvesync==1.1.0 From 1f4296f6887692cc479128a8de20a40f1a9ce040 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 19 Nov 2019 14:48:51 +0100 Subject: [PATCH 1630/3953] Updated frontend to 20191119.0 (#28875) --- 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 51906a100cc8fa..d44ddb606792d5 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191118.0" + "home-assistant-frontend==20191119.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7b275684740a9f..3343fa3efedc29 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191118.0 +home-assistant-frontend==20191119.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index b368f35349b0ff..acd3a70cecff1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191118.0 +home-assistant-frontend==20191119.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15b480a5547b53..804054455e0009 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -226,7 +226,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191118.0 +home-assistant-frontend==20191119.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From a4ed1d69b840af1cfbd24d5e845b8e8d8eefe751 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 19 Nov 2019 13:53:12 +0000 Subject: [PATCH 1631/3953] Add init for run inside devcontainer --- script/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 script/__init__.py diff --git a/script/__init__.py b/script/__init__.py new file mode 100644 index 00000000000000..e71d5b3763bfae --- /dev/null +++ b/script/__init__.py @@ -0,0 +1 @@ +"""Home Assistant scripts.""" From b83f92e4a527172326fc292080d444dc8733df86 Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Tue, 19 Nov 2019 07:21:46 -0700 Subject: [PATCH 1632/3953] Bump pyps4-2ndscreen to 1.0.3 (#28874) --- homeassistant/components/ps4/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 361711c400c7af..add52231a07435 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": [ - "pyps4-2ndscreen==1.0.1" + "pyps4-2ndscreen==1.0.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index acd3a70cecff1c..b728905addba12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1424,7 +1424,7 @@ pypjlink2==1.2.0 pypoint==1.1.2 # homeassistant.components.ps4 -pyps4-2ndscreen==1.0.1 +pyps4-2ndscreen==1.0.3 # homeassistant.components.qwikswitch pyqwikswitch==0.93 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 804054455e0009..86c80774581280 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -481,7 +481,7 @@ pyotp==2.3.0 pypoint==1.1.2 # homeassistant.components.ps4 -pyps4-2ndscreen==1.0.1 +pyps4-2ndscreen==1.0.3 # homeassistant.components.qwikswitch pyqwikswitch==0.93 From e915dd0d95d6bc407e4659bfadcef1468c8434f4 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 19 Nov 2019 16:28:12 +0100 Subject: [PATCH 1633/3953] Pulseaudio: Changed default port from 4712 to 4713 (#28857) --- homeassistant/components/pulseaudio_loopback/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index b3bddbf1263456..618d54ab3adb79 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -22,7 +22,7 @@ DEFAULT_BUFFER_SIZE = 1024 DEFAULT_HOST = "localhost" DEFAULT_NAME = "paloopback" -DEFAULT_PORT = 4712 +DEFAULT_PORT = 4713 DEFAULT_TCP_TIMEOUT = 3 IGNORED_SWITCH_WARN = "Switch is already in the desired state. Ignoring." From 5fcfdee6b5f315ca45a9e27064e305fd49aa0e5e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Nov 2019 18:33:09 +0100 Subject: [PATCH 1634/3953] Fix documentation URL in failed platform config check (#28814) * Fix documentation URL in failed platform config check * Replace pop from list by access using negative index * Use of split instead of rsplit --- homeassistant/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 864ced6a16aea0..e6be2b9c7a5557 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -456,9 +456,10 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: ) if domain != CONF_CORE: + integration = domain.split(".")[-1] message += ( "Please check the docs at " - "https://home-assistant.io/integrations/{}/".format(domain) + f"https://home-assistant.io/integrations/{integration}/" ) return message From 5203c72fcade14b6090e766193e0f286772a7dc8 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Tue, 19 Nov 2019 19:10:15 +0100 Subject: [PATCH 1635/3953] Add state dependent icons to moon sensor (#28743) * have sensor.moon use state dependent icons Material design icons have icons for all sensor.states, let's use these natively in the component. Based on the Season icons, tried to change accordingly. * Update sensor.py added state constants * Update sensor.py fixed missing mdi: * order of constants. moon icons and use self.state change order of constants to alphabetical, changed order in Moon_icons to alphabetical, used self.state for icon lookup * replace the strings replace the strings for self.state with the constants * removed quotes * removed spaces, empty line * local Black * Sort imports and move import to file header Co-authored-by: Fabian Affolter --- homeassistant/components/moon/sensor.py | 47 +++++++++++++++++-------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index 39247b096cce2b..dac7d36eda5f62 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -1,19 +1,38 @@ """Support for tracking the moon phases.""" import logging +from astral import Astral import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME -import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Moon" -ICON = "mdi:brightness-3" +STATE_FIRST_QUARTER = "first_quarter" +STATE_FULL_MOON = "full_moon" +STATE_LAST_QUARTER = "last_quarter" +STATE_NEW_MOON = "new_moon" +STATE_WANING_CRESCENT = "waning_crescent" +STATE_WANING_GIBBOUS = "waning_gibbous" +STATE_WAXING_GIBBOUS = "waxing_gibbous" +STATE_WAXING_CRESCENT = "waxing_crescent" + +MOON_ICONS = { + STATE_FIRST_QUARTER: "mdi:moon-first-quarter", + STATE_FULL_MOON: "mdi:moon-full", + STATE_LAST_QUARTER: "mdi:moon-last-quarter", + STATE_NEW_MOON: "mdi:moon-new", + STATE_WANING_CRESCENT: "mdi:moon-waning-crescent", + STATE_WANING_GIBBOUS: "mdi:moon-waning-gibbous", + STATE_WAXING_CRESCENT: "mdi:moon-waxing-crescent", + STATE_WAXING_GIBBOUS: "mdi:moon-waxing-gibbous", +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string} @@ -31,7 +50,7 @@ class MoonSensor(Entity): """Representation of a Moon sensor.""" def __init__(self, name): - """Initialize the sensor.""" + """Initialize the moon sensor.""" self._name = name self._state = None @@ -44,29 +63,27 @@ def name(self): def state(self): """Return the state of the device.""" if self._state == 0: - return "new_moon" + return STATE_NEW_MOON if self._state < 7: - return "waxing_crescent" + return STATE_WAXING_CRESCENT if self._state == 7: - return "first_quarter" + return STATE_FIRST_QUARTER if self._state < 14: - return "waxing_gibbous" + return STATE_WAXING_GIBBOUS if self._state == 14: - return "full_moon" + return STATE_FULL_MOON if self._state < 21: - return "waning_gibbous" + return STATE_WANING_GIBBOUS if self._state == 21: - return "last_quarter" - return "waning_crescent" + return STATE_LAST_QUARTER + return STATE_WANING_CRESCENT @property def icon(self): """Icon to use in the frontend, if any.""" - return ICON + return MOON_ICONS.get(self.state, "mdi:brightness-3") async def async_update(self): """Get the time and updates the states.""" - from astral import Astral - today = dt_util.as_local(dt_util.utcnow()).date() self._state = Astral().moon_phase(today) From 2d4b038b4889e4c7c430bdff9d8eed5c66d71cb7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Nov 2019 20:06:56 +0100 Subject: [PATCH 1636/3953] Move import to top for alarmdecoder (#28862) --- homeassistant/components/alarmdecoder/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 61cb0effe53e2a..93c0746a812e96 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -1,14 +1,17 @@ """Support for AlarmDecoder devices.""" +from datetime import timedelta import logging -from datetime import timedelta +from alarmdecoder import AlarmDecoder +from alarmdecoder.devices import SerialDevice, SocketDevice, USBDevice +from alarmdecoder.util import NoDeviceError import voluptuous as vol +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST from homeassistant.helpers.discovery import load_platform from homeassistant.util import dt as dt_util -from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -109,9 +112,6 @@ def setup(hass, config): """Set up for the AlarmDecoder devices.""" - from alarmdecoder import AlarmDecoder - from alarmdecoder.devices import SocketDevice, SerialDevice, USBDevice - conf = config.get(DOMAIN) restart = False @@ -134,8 +134,6 @@ def stop_alarmdecoder(event): def open_connection(now=None): """Open a connection to AlarmDecoder.""" - from alarmdecoder.util import NoDeviceError - nonlocal restart try: controller.open(baud) From 90029557874db3351d493252c211fdd6e8d86847 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Nov 2019 20:07:17 +0100 Subject: [PATCH 1637/3953] Move import to top for aftership (#28860) --- homeassistant/components/aftership/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index c41e5aec7b51d6..ff5edc92ba07ea 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pyaftership.tracker import Tracking import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -11,6 +12,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -56,8 +58,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the AfterShip sensor platform.""" - from pyaftership.tracker import Tracking - apikey = config[CONF_API_KEY] name = config[CONF_NAME] From 9324845aaa6913326cae39269215aa73018f732f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Nov 2019 20:07:29 +0100 Subject: [PATCH 1638/3953] Move imports to top for serial_pm (#28861) --- homeassistant/components/serial_pm/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index 1d46b05d46e0d6..75587e4eab7981 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -1,6 +1,7 @@ """Support for particulate matter sensors connected to a serial port.""" import logging +from pmsensor import serial_pm as pm import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -24,8 +25,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available PM sensors.""" - from pmsensor import serial_pm as pm - try: coll = pm.PMDataCollector( config.get(CONF_SERIAL_DEVICE), pm.SUPPORTED_SENSORS[config.get(CONF_BRAND)] From 21157f9dacdfd6f7fe495a1e4859b565e3a35940 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Nov 2019 20:09:23 +0100 Subject: [PATCH 1639/3953] Downgrade duplicate YAML key log to warning (#28851) --- homeassistant/util/yaml/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 47635cd66c3df1..3bd466e277172a 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -211,7 +211,7 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order if key in seen: fname = getattr(loader.stream, "name", "") - _LOGGER.error( + _LOGGER.warning( 'YAML file %s contains duplicate key "%s". ' "Check lines %d and %d.", fname, key, From 94675ca5a79e56359849ef9aaab85de54c0c6d29 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Nov 2019 13:42:09 -0600 Subject: [PATCH 1640/3953] Mark entity abc (#28869) * Mark entity abc * Use abstractmethod in climate * Lint --- homeassistant/components/climate/__init__.py | 5 +-- homeassistant/components/heatmiser/climate.py | 23 +++++++++++- homeassistant/helpers/entity.py | 4 +-- pylintrc | 2 ++ tests/components/climate/test_init.py | 36 ++++++++++++++++--- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index f0d350fc0baae0..4d58e1811be143 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -1,4 +1,5 @@ """Provides functionality to interact with climate devices.""" +from abc import abstractmethod from datetime import timedelta import functools as ft import logging @@ -270,20 +271,20 @@ def target_humidity(self) -> Optional[int]: return None @property + @abstractmethod def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode. Need to be one of HVAC_MODE_*. """ - raise NotImplementedError() @property + @abstractmethod def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes. Need to be a subset of HVAC_MODES. """ - raise NotImplementedError() @property def hvac_action(self) -> Optional[str]: diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index c9bed1e9d34fb9..6693e71878a99f 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -1,9 +1,14 @@ """Support for the PRT Heatmiser themostats using the V3 protocol.""" import logging +from typing import List import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import ( + ClimateDevice, + PLATFORM_SCHEMA, + HVAC_MODE_HEAT, +) from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE from homeassistant.const import ( TEMP_CELSIUS, @@ -82,6 +87,22 @@ def temperature_unit(self): """Return the unit of measurement which this thermostat uses.""" return TEMP_CELSIUS + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode. + + Need to be one of HVAC_MODE_*. + """ + return HVAC_MODE_HEAT + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes. + + Need to be a subset of HVAC_MODES. + """ + return [HVAC_MODE_HEAT] + @property def current_temperature(self): """Return the current temperature.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 0d2182f88e1f0a..1805c6bd74b0d8 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1,5 +1,5 @@ """An abstract class for entities.""" - +from abc import ABC import asyncio from datetime import datetime, timedelta import logging @@ -85,7 +85,7 @@ def async_generate_entity_id( return ensure_unique_string(entity_id_format.format(slugify(name)), current_ids) -class Entity: +class Entity(ABC): """An abstract class for Home Assistant entities.""" # SAFE TO OVERWRITE diff --git a/pylintrc b/pylintrc index 7794f61e500d2f..5de08d06794a39 100644 --- a/pylintrc +++ b/pylintrc @@ -24,6 +24,7 @@ good-names=id,i,j,k,ex,Run,_,fp # inconsistent-return-statements - doesn't handle raise # unnecessary-pass - readability for functions which only contain pass # import-outside-toplevel - TODO +# too-many-ancestors - it's too strict. disable= format, abstract-class-little-used, @@ -37,6 +38,7 @@ disable= not-context-manager, redefined-variable-type, too-few-public-methods, + too-many-ancestors, too-many-arguments, too-many-branches, too-many-instance-attributes, diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index c9d9f1f6425040..b044753c891053 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -1,10 +1,16 @@ """The tests for the climate component.""" from unittest.mock import MagicMock +from typing import List import pytest import voluptuous as vol -from homeassistant.components.climate import SET_TEMPERATURE_SCHEMA, ClimateDevice +from homeassistant.components.climate import ( + SET_TEMPERATURE_SCHEMA, + ClimateDevice, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, +) from tests.common import async_mock_service @@ -38,9 +44,29 @@ async def test_set_temp_schema(hass, caplog): assert calls[-1].data == data +class MockClimateDevice(ClimateDevice): + """Mock Climate device to use in tests.""" + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode. + + Need to be one of HVAC_MODE_*. + """ + return HVAC_MODE_HEAT + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes. + + Need to be a subset of HVAC_MODES. + """ + return [HVAC_MODE_OFF, HVAC_MODE_HEAT] + + async def test_sync_turn_on(hass): - """Test if adding turn_on work.""" - climate = ClimateDevice() + """Test if async turn_on calls sync turn_on.""" + climate = MockClimateDevice() climate.hass = hass climate.turn_on = MagicMock() @@ -50,8 +76,8 @@ async def test_sync_turn_on(hass): async def test_sync_turn_off(hass): - """Test if adding turn_on work.""" - climate = ClimateDevice() + """Test if async turn_off calls sync turn_off.""" + climate = MockClimateDevice() climate.hass = hass climate.turn_off = MagicMock() From cd8c281cb6daae3140817b6c8b71fb36cf23b263 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Tue, 19 Nov 2019 11:43:21 -0800 Subject: [PATCH 1641/3953] Add @rohankapoorcom to CODEOWNERS for speedtestdotnet and fastdotcom (#28879) --- CODEOWNERS | 2 ++ homeassistant/components/fastdotcom/manifest.json | 4 +++- homeassistant/components/speedtestdotnet/manifest.json | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c9d9dc129a5846..005c3e3489e9a7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -95,6 +95,7 @@ homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/esphome/* @OttoWinter homeassistant/components/essent/* @TheLastProject homeassistant/components/evohome/* @zxdavb +homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff homeassistant/components/filter/* @dgomes homeassistant/components/fitbit/* @robbiet480 @@ -282,6 +283,7 @@ homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne homeassistant/components/songpal/* @rytilahti homeassistant/components/spaceapi/* @fabaff +homeassistant/components/speedtestdotnet/* @rohankapoorcom homeassistant/components/spider/* @peternijssen homeassistant/components/sql/* @dgomes homeassistant/components/statistics/* @fabaff diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index 3655ce22ba70ee..2e47248d778ffc 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -6,5 +6,7 @@ "fastdotcom==0.0.3" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@rohankapoorcom" + ] } diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index b32026d86ef7ab..821d4158f57182 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -6,5 +6,7 @@ "speedtest-cli==2.1.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@rohankapoorcom" + ] } From c2161b1f5a3be787054806c562c73d168e531837 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 19 Nov 2019 14:47:03 -0500 Subject: [PATCH 1642/3953] Add HTTP view to conversation to handle intents via JSON POST (#28818) --- .../components/conversation/__init__.py | 39 +++++++++++++++- tests/components/conversation/test_init.py | 45 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index a82034a4237b38..b2648d3d1f60f3 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -61,7 +61,6 @@ async def get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: async def async_setup(hass, config): """Register the process service.""" - hass.data[DATA_CONFIG] = config async def handle_service(service): @@ -77,6 +76,7 @@ async def handle_service(service): DOMAIN, SERVICE_PROCESS, handle_service, schema=SERVICE_PROCESS_SCHEMA ) hass.http.register_view(ConversationProcessView()) + hass.http.register_view(ConversationHandleView()) hass.components.websocket_api.async_register_command(websocket_process) hass.components.websocket_api.async_register_command(websocket_get_agent_info) hass.components.websocket_api.async_register_command(websocket_set_onboarding) @@ -162,3 +162,40 @@ async def post(self, request, data): ) return self.json(intent_result) + + +class ConversationHandleView(http.HomeAssistantView): + """View to handle intents from JSON.""" + + url = "/api/conversation/handle" + name = "api:conversation:handle" + + @RequestDataValidator( + vol.Schema( + { + vol.Required("name"): cv.string, + vol.Optional("data"): vol.Schema({cv.string: object}), + } + ) + ) + async def post(self, request, data): + """Handle intent with name/data.""" + hass = request.app["hass"] + + try: + intent_name = data["name"] + slots = { + key: {"value": value} for key, value in data.get("data", {}).items() + } + intent_result = await intent.async_handle( + hass, DOMAIN, intent_name, slots, "" + ) + except intent.IntentHandleError as err: + intent_result = intent.IntentResponse() + intent_result.async_set_speech(str(err)) + + if intent_result is None: + intent_result = intent.IntentResponse() + intent_result.async_set_speech("Sorry, I couldn't handle that") + + return self.json(intent_result) diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index ff44eaccc8ea8a..fc6508159ea3cb 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -124,6 +124,51 @@ async def async_handle(self, intent): } +async def test_http_handle_intent(hass, hass_client): + """Test handle intent via HTTP API.""" + + class TestIntentHandler(intent.IntentHandler): + """Test Intent Handler.""" + + intent_type = "OrderBeer" + + async def async_handle(self, intent): + """Handle the intent.""" + response = intent.create_response() + response.async_set_speech( + "I've ordered a {}!".format(intent.slots["type"]["value"]) + ) + response.async_set_card( + "Beer ordered", "You chose a {}.".format(intent.slots["type"]["value"]) + ) + return response + + intent.async_register(hass, TestIntentHandler()) + + result = await async_setup_component( + hass, + "conversation", + {"conversation": {"intents": {"OrderBeer": ["I would like the {type} beer"]}}}, + ) + assert result + + client = await hass_client() + resp = await client.post( + "/api/conversation/handle", + json={"name": "OrderBeer", "data": {"type": "Belgian"}}, + ) + + assert resp.status == 200 + data = await resp.json() + + assert data == { + "card": { + "simple": {"content": "You chose a Belgian.", "title": "Beer ordered"} + }, + "speech": {"plain": {"extra_data": None, "speech": "I've ordered a Belgian!"}}, + } + + @pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on")) async def test_turn_on_intent(hass, sentence): """Test calling the turn on intent.""" From b92b6c40b7cc12b6880731dac2949001c3fb657b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 19 Nov 2019 23:16:43 +0100 Subject: [PATCH 1643/3953] Updated frontend to 20191119.1 (#28881) --- 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 d44ddb606792d5..cc7722724ea30c 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.0" + "home-assistant-frontend==20191119.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3343fa3efedc29..7dc6eb12bfa464 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191119.0 +home-assistant-frontend==20191119.1 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index b728905addba12..b92a5b952ebcb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.0 +home-assistant-frontend==20191119.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86c80774581280..af8bbbbc107266 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -226,7 +226,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.0 +home-assistant-frontend==20191119.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 8e7a9521dccf4c11a5680b48cb4b32d6019a7eaf Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 19 Nov 2019 23:16:59 +0100 Subject: [PATCH 1644/3953] Fix setting colors while reproducing a lights state (#28871) * Fix setting colors while reproducing state * Reorder list --- homeassistant/components/light/reproduce_state.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index c84b3627bed0b1..90d14c2a19f980 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -45,13 +45,14 @@ ] COLOR_GROUP = [ - ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, ATTR_HS_COLOR, - ATTR_KELVIN, - ATTR_PROFILE, + ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_XY_COLOR, + # The following color attributes are deprecated + ATTR_PROFILE, + ATTR_COLOR_NAME, + ATTR_KELVIN, ] DEPRECATED_GROUP = [ From 6b62328c0ff28f7c02ff0d628167ea6c9025a40b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 20 Nov 2019 00:32:09 +0000 Subject: [PATCH 1645/3953] [ci skip] Translation update --- .../geonetnz_quakes/.translations/ru.json | 4 ++-- .../geonetnz_volcano/.translations/no.json | 16 ++++++++++++++++ .../geonetnz_volcano/.translations/ru.json | 16 ++++++++++++++++ .../hisense_aehw4a1/.translations/sl.json | 15 +++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/geonetnz_volcano/.translations/no.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/ru.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/sl.json diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json index d6763d17e2d0a8..dddb5c47bb9635 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ru.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -9,9 +9,9 @@ "mmi": "MMI", "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" }, - "title": "GeoNet" + "title": "GeoNet NZ Quakes" } }, - "title": "\u0417\u0435\u043c\u043b\u0435\u0442\u0440\u044f\u0441\u0435\u043d\u0438\u044f \u0432 \u041d\u043e\u0432\u043e\u0439 \u0417\u0435\u043b\u0430\u043d\u0434\u0438\u0438 (GeoNet)" + "title": "GeoNet NZ Quakes" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/no.json b/homeassistant/components/geonetnz_volcano/.translations/no.json new file mode 100644 index 00000000000000..d66e0eb6d7d81e --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Beliggenhet er allerede registrert" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Fyll ut filterdetaljene." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ru.json b/homeassistant/components/geonetnz_volcano/.translations/ru.json new file mode 100644 index 00000000000000..6e7411f28b97a4 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." + }, + "step": { + "user": { + "data": { + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "GeoNet NZ Volcano" + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/sl.json b/homeassistant/components/hisense_aehw4a1/.translations/sl.json new file mode 100644 index 00000000000000..3c15eecf6e1895 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/sl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni bilo najdenih naprav Hisense AEH-W4A1.", + "single_instance_allowed": "Mo\u017ena je samo ena konfiguracija Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "Ali \u017eelite nastaviti Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file From e60ae77f1be0a42701d6fe5e6947a5e7d5664bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 20 Nov 2019 00:49:09 +0000 Subject: [PATCH 1646/3953] Add ZHA service to issue group commands (#28823) * add service to ZHA to issue group commands * fix args --- homeassistant/components/zha/api.py | 45 ++++++++++++++++++++ homeassistant/components/zha/core/gateway.py | 4 ++ homeassistant/components/zha/services.yaml | 20 +++++++++ 3 files changed, 69 insertions(+) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 6f24db442dd26f..f2fbc126e6e119 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -57,6 +57,7 @@ DEVICE_INFO = "device_info" ATTR_DURATION = "duration" +ATTR_GROUP = "group" ATTR_IEEE_ADDRESS = "ieee_address" ATTR_IEEE = "ieee" ATTR_SOURCE_IEEE = "source_ieee" @@ -68,6 +69,7 @@ SERVICE_REMOVE = "remove" SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = "set_zigbee_cluster_attribute" SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND = "issue_zigbee_cluster_command" +SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND = "issue_zigbee_group_command" SERVICE_DIRECT_ZIGBEE_BIND = "issue_direct_zigbee_bind" SERVICE_DIRECT_ZIGBEE_UNBIND = "issue_direct_zigbee_unbind" SERVICE_WARNING_DEVICE_SQUAWK = "warning_device_squawk" @@ -143,6 +145,16 @@ vol.Optional(ATTR_MANUFACTURER): cv.positive_int, } ), + SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND: vol.Schema( + { + vol.Required(ATTR_GROUP): cv.positive_int, + vol.Required(ATTR_CLUSTER_ID): cv.positive_int, + vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, + vol.Required(ATTR_COMMAND): cv.positive_int, + vol.Optional(ATTR_ARGS, default=[]): cv.ensure_list, + vol.Optional(ATTR_MANUFACTURER): cv.positive_int, + } + ), } @@ -660,6 +672,38 @@ async def issue_zigbee_cluster_command(service): schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND], ) + async def issue_zigbee_group_command(service): + """Issue command on zigbee cluster on a zigbee group.""" + group_id = service.data.get(ATTR_GROUP) + cluster_id = service.data.get(ATTR_CLUSTER_ID) + command = service.data.get(ATTR_COMMAND) + args = service.data.get(ATTR_ARGS) + manufacturer = service.data.get(ATTR_MANUFACTURER) or None + group = zha_gateway.get_group(group_id) + if cluster_id >= MFG_CLUSTER_ID_START and manufacturer is None: + _LOGGER.error("Missing manufacturer attribute for cluster: %d", cluster_id) + response = None + if group is not None: + cluster = group.endpoint[cluster_id] + response = await cluster.command( + command, *args, manufacturer=manufacturer, expect_reply=True + ) + _LOGGER.debug( + "Issue group command for: %s %s %s %s %s", + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_COMMAND}: [{command}]", + f"{ATTR_ARGS}: [{args}]", + f"{ATTR_MANUFACTURER}: [{manufacturer}]", + f"{RESPONSE}: [{response}]", + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND, + issue_zigbee_group_command, + schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND], + ) + async def warning_device_squawk(service): """Issue the squawk command for an IAS warning device.""" ieee = service.data[ATTR_IEEE] @@ -758,5 +802,6 @@ def async_unload_api(hass): hass.services.async_remove(DOMAIN, SERVICE_REMOVE) hass.services.async_remove(DOMAIN, SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE) hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND) + hass.services.async_remove(DOMAIN, SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND) hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_SQUAWK) hass.services.async_remove(DOMAIN, SERVICE_WARNING_DEVICE_WARN) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 77702c8f3de81c..ef81705ce471f1 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -222,6 +222,10 @@ def get_device(self, ieee): """Return ZHADevice for given ieee.""" return self._devices.get(ieee) + def get_group(self, group_id): + """Return Group for given group id.""" + return self.application_controller.groups[group_id] + def get_entity_reference(self, entity_id): """Return entity reference for given entity_id if found.""" for entity_reference in itertools.chain.from_iterable( diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index d279af46335fe1..d556c279d3caf0 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -83,6 +83,26 @@ issue_zigbee_cluster_command: description: manufacturer code example: 0x00FC +issue_zigbee_group_command: + description: >- + Issue command on the specified cluster on the specified group. + fields: + group: + description: Hexadecimal address of the group + example: 0x0222 + cluster_id: + description: ZCL cluster to send command to + example: 6 + command: + description: id of the command to execute + example: 0 + args: + description: args to pass to the command + example: {} + manufacturer: + description: manufacturer code + example: 0x00FC + warning_device_squawk: description: >- This service uses the WD capabilities to emit a quick audible/visible pulse called a "squawk". The squawk command has no effect if the WD is currently active (warning in progress). From 6db4e975e30e92a88c7db3fa18519df3ddd1a33f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Nov 2019 02:51:25 +0100 Subject: [PATCH 1647/3953] Bump pytest to 5.3.0 (#28883) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index dae60b746530cd..6ccb0168677d47 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,6 +14,6 @@ pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.3 +pytest==5.3.0 requests_mock==1.7.0 responses==0.10.6 From 9fd6afc5fb5843415b434b8a69cefa2033cca3c4 Mon Sep 17 00:00:00 2001 From: foxy82 Date: Wed, 20 Nov 2019 02:11:17 +0000 Subject: [PATCH 1648/3953] Allow connection to rfxtrx using tcp (#28297) --- homeassistant/components/rfxtrx/__init__.py | 38 +++++++++++++------ homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 1515ce33c6e684..ceba82cf544d9d 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -12,6 +12,8 @@ ATTR_STATE, CONF_DEVICE, CONF_DEVICES, + CONF_HOST, + CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, POWER_WATT, @@ -80,17 +82,21 @@ _LOGGER = logging.getLogger(__name__) DATA_RFXOBJECT = "rfxobject" -CONFIG_SCHEMA = vol.Schema( +BASE_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_DEVICE): cv.string, - vol.Optional(CONF_DEBUG, default=False): cv.boolean, - vol.Optional(CONF_DUMMY, default=False): cv.boolean, - } - ) - }, - extra=vol.ALLOW_EXTRA, + vol.Optional(CONF_DEBUG, default=False): cv.boolean, + vol.Optional(CONF_DUMMY, default=False): cv.boolean, + } +) + +DEVICE_SCHEMA = BASE_SCHEMA.extend({vol.Required(CONF_DEVICE): cv.string}) + +PORT_SCHEMA = BASE_SCHEMA.extend( + {vol.Required(CONF_PORT): cv.port, vol.Optional(CONF_HOST): cv.string} +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Any(DEVICE_SCHEMA, PORT_SCHEMA)}, extra=vol.ALLOW_EXTRA ) @@ -115,7 +121,9 @@ def handle_receive(event): for subscriber in RECEIVED_EVT_SUBSCRIBERS: subscriber(event) - device = config[DOMAIN][ATTR_DEVICE] + device = config[DOMAIN].get(ATTR_DEVICE) + host = config[DOMAIN].get(CONF_HOST) + port = config[DOMAIN].get(CONF_PORT) debug = config[DOMAIN][ATTR_DEBUG] dummy_connection = config[DOMAIN][ATTR_DUMMY] @@ -123,6 +131,14 @@ def handle_receive(event): rfx_object = rfxtrxmod.Connect( device, None, debug=debug, transport_protocol=rfxtrxmod.DummyTransport2 ) + elif port is not None: + # If port is set then we create a TCP connection + rfx_object = rfxtrxmod.Connect( + (host, port), + None, + debug=debug, + transport_protocol=rfxtrxmod.PyNetworkTransport, + ) else: rfx_object = rfxtrxmod.Connect(device, None, debug=debug) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index a75a8ba9eb1d0c..e26ceb7ef57ce8 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -3,7 +3,7 @@ "name": "Rfxtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "requirements": [ - "pyRFXtrx==0.23" + "pyRFXtrx==0.24" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index b92a5b952ebcb9..094987042594fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1073,7 +1073,7 @@ pyHS100==0.3.5 pyMetno==0.4.6 # homeassistant.components.rfxtrx -pyRFXtrx==0.23 +pyRFXtrx==0.24 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af8bbbbc107266..484bbd23a43d2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -371,7 +371,7 @@ pyHS100==0.3.5 pyMetno==0.4.6 # homeassistant.components.rfxtrx -pyRFXtrx==0.23 +pyRFXtrx==0.24 # homeassistant.components.nextbus py_nextbusnext==0.1.4 From d9f146d0763b45331f46a1729ee8a4d36c9fb09d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 20 Nov 2019 12:30:32 +0100 Subject: [PATCH 1649/3953] Updated frontend to 20191119.2 (#28896) --- 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 cc7722724ea30c..6e513d55742dde 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.1" + "home-assistant-frontend==20191119.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7dc6eb12bfa464..0eca803d653a91 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191119.1 +home-assistant-frontend==20191119.2 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 094987042594fd..f86f9d3107337a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.1 +home-assistant-frontend==20191119.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 484bbd23a43d2a..1ac20d2cf13ceb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -226,7 +226,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.1 +home-assistant-frontend==20191119.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 89cd5d46cdb09f75bf655148e460170d09f4eddc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Nov 2019 18:33:09 +0100 Subject: [PATCH 1650/3953] Fix documentation URL in failed platform config check (#28814) * Fix documentation URL in failed platform config check * Replace pop from list by access using negative index * Use of split instead of rsplit --- homeassistant/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 864ced6a16aea0..e6be2b9c7a5557 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -456,9 +456,10 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: ) if domain != CONF_CORE: + integration = domain.split(".")[-1] message += ( "Please check the docs at " - "https://home-assistant.io/integrations/{}/".format(domain) + f"https://home-assistant.io/integrations/{integration}/" ) return message From 687b7fc8cbd131bde15452b2277070a514e30786 Mon Sep 17 00:00:00 2001 From: Andi Date: Mon, 18 Nov 2019 17:03:10 +0100 Subject: [PATCH 1651/3953] Fix Synology camera whitelist (#28822) * Fix Synology camera whitelist If whitelist config is set, not camera is added to HA at all. * Fix Synology Camera whitelist Fix typo in config key. * Update camera.py Access config dict the voluptuous way --- homeassistant/components/synology/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py index 8c176f488034ca..91ee5a98fc328c 100644 --- a/homeassistant/components/synology/camera.py +++ b/homeassistant/components/synology/camera.py @@ -62,7 +62,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # add cameras devices = [] for camera in cameras: - if not config.get(CONF_WHITELIST): + if not config[CONF_WHITELIST] or camera.name in config[CONF_WHITELIST]: device = SynologyCamera(surveillance, camera.camera_id, verify_ssl) devices.append(device) From ab413108479fabfca76af06e1773e4a274462c83 Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 19 Nov 2019 23:16:59 +0100 Subject: [PATCH 1652/3953] Fix setting colors while reproducing a lights state (#28871) * Fix setting colors while reproducing state * Reorder list --- homeassistant/components/light/reproduce_state.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index c84b3627bed0b1..90d14c2a19f980 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -45,13 +45,14 @@ ] COLOR_GROUP = [ - ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, ATTR_HS_COLOR, - ATTR_KELVIN, - ATTR_PROFILE, + ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_XY_COLOR, + # The following color attributes are deprecated + ATTR_PROFILE, + ATTR_COLOR_NAME, + ATTR_KELVIN, ] DEPRECATED_GROUP = [ From 69b096023a33addd5c321978ecc09a3cc28f595a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 19 Nov 2019 14:48:51 +0100 Subject: [PATCH 1653/3953] Updated frontend to 20191119.0 (#28875) --- 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 51906a100cc8fa..d44ddb606792d5 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191118.0" + "home-assistant-frontend==20191119.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b4ec45e9fdb295..a13648d2a3bdd0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191118.0 +home-assistant-frontend==20191119.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 24ab0ce339a2b2..13470f4b8f6b00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191118.0 +home-assistant-frontend==20191119.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c8829308cf13f..132fddb58bab9d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191118.0 +home-assistant-frontend==20191119.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 9fb289ad93cb7221b24f7a6514b4e200fa805ca8 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 19 Nov 2019 23:16:43 +0100 Subject: [PATCH 1654/3953] Updated frontend to 20191119.1 (#28881) --- 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 d44ddb606792d5..cc7722724ea30c 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.0" + "home-assistant-frontend==20191119.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a13648d2a3bdd0..f3939120124d27 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191119.0 +home-assistant-frontend==20191119.1 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 13470f4b8f6b00..9790b3c99747f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.0 +home-assistant-frontend==20191119.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 132fddb58bab9d..9f905160ebcf6a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.0 +home-assistant-frontend==20191119.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 98c7ddc0cf66e7aebcc17b59f1adfd2035cd0cf4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 20 Nov 2019 12:30:32 +0100 Subject: [PATCH 1655/3953] Updated frontend to 20191119.2 (#28896) --- 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 cc7722724ea30c..6e513d55742dde 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.1" + "home-assistant-frontend==20191119.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f3939120124d27..a98bb2b46d7215 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191119.1 +home-assistant-frontend==20191119.2 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9790b3c99747f6..833a4d471059a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.1 +home-assistant-frontend==20191119.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f905160ebcf6a..75564e13b94ce1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.1 +home-assistant-frontend==20191119.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From bca93ca2ec6c3adfe50b4a75a41f7173cb11a1cf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 20 Nov 2019 12:33:39 +0100 Subject: [PATCH 1656/3953] Bumped version to 0.102.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0bc6be37dc76a1..cb3b234595cc66 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From b267d54db3b69719c813ebdca105962e71e0bfb5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 20 Nov 2019 07:14:13 -0500 Subject: [PATCH 1657/3953] Update services.yaml for Roku component (#28887) * update services.yaml for roku component * add new line --- homeassistant/components/roku/services.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/roku/services.yaml b/homeassistant/components/roku/services.yaml index e69de29bb2d1d6..956ecb0dd2d293 100644 --- a/homeassistant/components/roku/services.yaml +++ b/homeassistant/components/roku/services.yaml @@ -0,0 +1,2 @@ +roku_scan: + description: Scans the local network for Rokus. All found devices are presented as a persistent notification. From 4ba2bd232c4d02e7c80e7f7efff34a17cd0fe3ab Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 20 Nov 2019 08:34:06 -0500 Subject: [PATCH 1658/3953] Command arguments for issue_zigbee_cluster_commands. (#28885) --- homeassistant/components/zha/api.py | 4 ++-- homeassistant/components/zha/core/device.py | 3 +-- homeassistant/components/zha/services.yaml | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index f2fbc126e6e119..438b93244cf84d 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -141,7 +141,7 @@ 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, + vol.Optional(ATTR_ARGS, default=[]): cv.ensure_list, vol.Optional(ATTR_MANUFACTURER): cv.positive_int, } ), @@ -649,7 +649,7 @@ async def issue_zigbee_cluster_command(service): cluster_id, command, command_type, - args, + *args, cluster_type=cluster_type, manufacturer=manufacturer, ) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index b3be8037ff6118..e5d1678ad6fa31 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -479,7 +479,7 @@ async def issue_cluster_command( cluster_id, command, command_type, - args, + *args, cluster_type=CLUSTER_TYPE_IN, manufacturer=None, ): @@ -487,7 +487,6 @@ async def issue_cluster_command( cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None - response = None if command_type == CLUSTER_COMMAND_SERVER: response = await cluster.command( command, *args, manufacturer=manufacturer, expect_reply=True diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index d556c279d3caf0..ab8e9c195801eb 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -78,7 +78,7 @@ issue_zigbee_cluster_command: example: "server" args: description: args to pass to the command - example: {} + example: '[arg1, arg2, argN]' manufacturer: description: manufacturer code example: 0x00FC @@ -98,7 +98,7 @@ issue_zigbee_group_command: example: 0 args: description: args to pass to the command - example: {} + example: '[arg1, arg2, argN]' manufacturer: description: manufacturer code example: 0x00FC From 288ae8b1e7cdbbdcb6d055eaea09445cc3dc4064 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 20 Nov 2019 09:45:11 -0500 Subject: [PATCH 1659/3953] Update services.yaml for cloudflare component (#28888) * update services.yaml for cloudflare component * Made description more clear --- homeassistant/components/cloudflare/services.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/cloudflare/services.yaml b/homeassistant/components/cloudflare/services.yaml index e69de29bb2d1d6..23ffdd14d5f03d 100644 --- a/homeassistant/components/cloudflare/services.yaml +++ b/homeassistant/components/cloudflare/services.yaml @@ -0,0 +1,2 @@ +update_records: + description: Manually trigger update to Cloudflare records. From 1c630738f76cec1b6ea707af4033bccacae6b089 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 20 Nov 2019 15:40:00 -0500 Subject: [PATCH 1660/3953] update services.yaml for keyboard component (#28889) --- homeassistant/components/keyboard/services.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/homeassistant/components/keyboard/services.yaml b/homeassistant/components/keyboard/services.yaml index e69de29bb2d1d6..a9896c3d6cf1d8 100644 --- a/homeassistant/components/keyboard/services.yaml +++ b/homeassistant/components/keyboard/services.yaml @@ -0,0 +1,17 @@ +volume_up: + description: Simulates a key press of the "Volume Up" button on HomeAssistant's host machine. + +volume_down: + description: Simulates a key press of the "Volume Down" button on HomeAssistant's host machine. + +volume_mute: + description: Simulates a key press of the "Volume Mute" button on HomeAssistant's host machine. + +media_play_pause: + description: Simulates a key press of the "Media Play/Pause" button on HomeAssistant's host machine. + +media_next_track: + description: Simulates a key press of the "Media Next Track" button on HomeAssistant's host machine. + +media_prev_track: + description: Simulates a key press of the "Media Previous Track" button on HomeAssistant's host machine. From ae3cf72fb2bff847e5e2aa65c2328bf5f4f93746 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 20 Nov 2019 22:03:01 +0100 Subject: [PATCH 1661/3953] Fix Almond onboarding url when using cloud (#28908) --- homeassistant/components/almond/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 6d4ab31bf17492..7c1f65f3ac3eac 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -263,8 +263,6 @@ async def async_get_onboarding(self): host = self.entry.data["host"] if self.entry.data.get("is_hassio"): host = "/core_almond" - elif self.entry.data["type"] != TYPE_LOCAL: - host = f"{host}/me" return { "text": "Would you like to opt-in to share your anonymized commands with Stanford to improve Almond's responses?", "url": f"{host}/conversation", From 787aac7cf2a075996005701d177e3845c025e201 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 20 Nov 2019 22:03:01 +0100 Subject: [PATCH 1662/3953] Fix Almond onboarding url when using cloud (#28908) --- homeassistant/components/almond/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 6d4ab31bf17492..7c1f65f3ac3eac 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -263,8 +263,6 @@ async def async_get_onboarding(self): host = self.entry.data["host"] if self.entry.data.get("is_hassio"): host = "/core_almond" - elif self.entry.data["type"] != TYPE_LOCAL: - host = f"{host}/me" return { "text": "Would you like to opt-in to share your anonymized commands with Stanford to improve Almond's responses?", "url": f"{host}/conversation", From 138cee8069327b788f07c9922ffa8072b2f9db1a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 20 Nov 2019 13:05:54 -0800 Subject: [PATCH 1663/3953] Version bump to 0.102.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cb3b234595cc66..ef16de573a00b9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 9f181ac92e55457f0e34b0b6f5e60fc0133eb392 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Wed, 20 Nov 2019 22:37:59 +0100 Subject: [PATCH 1664/3953] bump herepy to 0.6.3.3 (#28907) * bump herepy to 0.6.3.3 * run gen_requirements_all --- homeassistant/components/here_travel_time/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/here_travel_time/test_sensor.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 0f2bde253de5a9..5ef71a249e6d5c 100755 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -3,7 +3,7 @@ "name": "HERE travel time", "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "requirements": [ - "herepy==0.6.3.1" + "herepy==0.6.3.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index f86f9d3107337a..46edbbe23d34e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -641,7 +641,7 @@ hdate==0.9.3 heatmiserV3==0.9.1 # homeassistant.components.here_travel_time -herepy==0.6.3.1 +herepy==0.6.3.3 # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ac20d2cf13ceb..e19d9fee73de0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -217,7 +217,7 @@ hbmqtt==0.9.5 hdate==0.9.3 # homeassistant.components.here_travel_time -herepy==0.6.3.1 +herepy==0.6.3.3 # homeassistant.components.pi_hole hole==0.5.0 diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 1d788c93c663d5..296efbdad532e0 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -71,8 +71,8 @@ def _build_mock_url(origin, destination, modes, app_id, app_code, departure): """Construct a url for HERE.""" base_url = "https://route.cit.api.here.com/routing/7.2/calculateroute.json?" parameters = { - "waypoint0": origin, - "waypoint1": destination, + "waypoint0": f"geo!{origin}", + "waypoint1": f"geo!{destination}", "mode": ";".join(str(herepy.RouteMode[mode]) for mode in modes), "app_id": app_id, "app_code": app_code, From 0e14c3f92a9bc91695a869d43d89a1e04d298b31 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 21 Nov 2019 00:32:11 +0000 Subject: [PATCH 1665/3953] [ci skip] Translation update --- .../components/almond/.translations/it.json | 5 ++++ .../components/demo/.translations/it.json | 5 ++++ .../.translations/zh-Hant.json | 2 +- .../geonetnz_volcano/.translations/ca.json | 16 ++++++++++++ .../geonetnz_volcano/.translations/es.json | 16 ++++++++++++ .../geonetnz_volcano/.translations/it.json | 16 ++++++++++++ .../.translations/zh-Hant.json | 16 ++++++++++++ .../hisense_aehw4a1/.translations/it.json | 15 +++++++++++ .../components/wled/.translations/nl.json | 25 +++++++++++++++++++ 9 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/demo/.translations/it.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/ca.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/es.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/it.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/zh-Hant.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/it.json create mode 100644 homeassistant/components/wled/.translations/nl.json diff --git a/homeassistant/components/almond/.translations/it.json b/homeassistant/components/almond/.translations/it.json index 740535f4f4601d..9d529e5e5c85a9 100644 --- a/homeassistant/components/almond/.translations/it.json +++ b/homeassistant/components/almond/.translations/it.json @@ -5,6 +5,11 @@ "cannot_connect": "Impossibile connettersi al server Almond.", "missing_configuration": "Si prega di controllare la documentazione su come impostare Almond." }, + "step": { + "pick_implementation": { + "title": "Seleziona metodo di autenticazione" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/it.json b/homeassistant/components/demo/.translations/it.json new file mode 100644 index 00000000000000..ef01fcb4f3c35e --- /dev/null +++ b/homeassistant/components/demo/.translations/it.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json index 59b4abf259a12d..487ac9ea8c0d24 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json +++ b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json @@ -12,6 +12,6 @@ "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" } }, - "title": "GeoNet NZ Quakes" + "title": "\u7d10\u897f\u862d GeoNet \u5730\u9707\u9810\u8b66" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ca.json b/homeassistant/components/geonetnz_volcano/.translations/ca.json new file mode 100644 index 00000000000000..2e595b7304046c --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Ubicaci\u00f3 ja registrada" + }, + "step": { + "user": { + "data": { + "radius": "Radi" + }, + "title": "Introdueix els detalls del filtre." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/es.json b/homeassistant/components/geonetnz_volcano/.translations/es.json new file mode 100644 index 00000000000000..c6b92e830898d7 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lugar ya registrado" + }, + "step": { + "user": { + "data": { + "radius": "Radio" + }, + "title": "Complete los detalles de su filtro." + } + }, + "title": "GeoNet NZ Volc\u00e1n" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/it.json b/homeassistant/components/geonetnz_volcano/.translations/it.json new file mode 100644 index 00000000000000..85bfc7297ee24f --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Localit\u00e0 gi\u00e0 registrata" + }, + "step": { + "user": { + "data": { + "radius": "Raggio" + }, + "title": "Inserisci i tuoi dettagli del filtro." + } + }, + "title": "GeoNet NZ Vulcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/zh-Hant.json b/homeassistant/components/geonetnz_volcano/.translations/zh-Hant.json new file mode 100644 index 00000000000000..0f74841fd7b1e0 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "\u5ea7\u6a19\u5df2\u8a3b\u518a" + }, + "step": { + "user": { + "data": { + "radius": "\u534a\u5f91" + }, + "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" + } + }, + "title": "\u7d10\u897f\u862d GeoNet \u706b\u5c71\u9810\u8b66" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/it.json b/homeassistant/components/hisense_aehw4a1/.translations/it.json new file mode 100644 index 00000000000000..b584d18e8bf13d --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/it.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo Hisense AEH-W4A1 trovato sulla rete.", + "single_instance_allowed": "\u00c8 consentita solo una configurazione di Hisense AEH-W4A1" + }, + "step": { + "confirm": { + "description": "Voui configurare Hisense AEH-W4A1", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/nl.json b/homeassistant/components/wled/.translations/nl.json new file mode 100644 index 00000000000000..1bf70b7a0952b7 --- /dev/null +++ b/homeassistant/components/wled/.translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Dit WLED-apparaat is al geconfigureerd.", + "connection_error": "Kan geen verbinding maken met WLED-apparaat." + }, + "error": { + "connection_error": "Kan geen verbinding maken met WLED-apparaat." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Hostnaam of IP-adres" + }, + "title": "Koppel je WLED" + }, + "zeroconf_confirm": { + "description": "Wil je de WLED genaamd `{name}` toevoegen aan Home Assistant?", + "title": "Ontdekt WLED-apparaat" + } + }, + "title": "WLED" + } +} \ No newline at end of file From c7c7a9ad06e2963974d3c47eb3f38c26f6f25cf8 Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Thu, 21 Nov 2019 07:31:40 +0100 Subject: [PATCH 1666/3953] Update __init__.py (#28911) --- homeassistant/components/freebox/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 0bffedd46dc3ad..64c59c3ef2a809 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -2,6 +2,8 @@ import logging import socket +from aiofreepybox import Freepybox +from aiofreepybox.exceptions import HttpRequestError import voluptuous as vol from homeassistant.components.discovery import SERVICE_FREEBOX @@ -49,8 +51,6 @@ async def discovery_dispatch(service, discovery_info): async def async_setup_freebox(hass, config, host, port): """Start up the Freebox component platforms.""" - from aiofreepybox import Freepybox - from aiofreepybox.exceptions import HttpRequestError app_desc = { "app_id": "hass", From 86e581b83dac9eb921b560d3dc4ee97815499e23 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 21 Nov 2019 17:35:03 +0100 Subject: [PATCH 1667/3953] Fix missing Netatmo sensors (#28899) * Update pyatmo to 3.0.1 * Improve sensor name * Fix missing sensors * Update pyatmo module * Update pyatmo to 3.1.0 * Update requirements * Fix method call --- homeassistant/components/netatmo/__init__.py | 2 +- .../components/netatmo/manifest.json | 4 ++-- homeassistant/components/netatmo/sensor.py | 24 ++++++++++++------- requirements_all.txt | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 4b9f0690ac5a4c..de371f97e28277 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -259,4 +259,4 @@ def update(self): @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) def update_event(self): """Call the Netatmo API to update the events.""" - self.camera_data.updateEvent(home=self.home, cameratype=self.camera_type) + self.camera_data.updateEvent(home=self.home, devicetype=self.camera_type) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 9d1178d9d17026..232e99eeae8b2c 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,10 +3,10 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==3.0.1" + "pyatmo==3.1.0" ], "dependencies": [ "webhook" ], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index f76062035d2fc7..fa7aedae739ba5 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -155,14 +155,12 @@ def find_devices(data): for module in all_module_infos.values(): if module["module_name"] not in module_names: continue + _LOGGER.debug( + "Adding module %s %s", module["module_name"], module["id"] + ) for condition in data.station_data.monitoredConditions( moduleId=module["id"] ): - _LOGGER.debug( - "Adding %s %s", - module["module_name"], - data.station_data.moduleById(mid=module["id"]), - ) entities.append(NetatmoSensor(data, module, condition.lower())) return entities @@ -200,18 +198,26 @@ class NetatmoSensor(Entity): def __init__(self, netatmo_data, module_info, sensor_type): """Initialize the sensor.""" self.netatmo_data = netatmo_data - module = self.netatmo_data.station_data.moduleById(mid=module_info["id"]) - if module["type"] == "NHC": + + device = self.netatmo_data.station_data.moduleById(mid=module_info["id"]) + if not device: + # Assume it's a station if module can't be found + device = self.netatmo_data.station_data.stationById(sid=module_info["id"]) + + if device["type"] == "NHC": self.module_name = module_info["station_name"] else: - self.module_name = module_info["module_name"] + self.module_name = ( + f"{module_info['station_name']} {module_info['module_name']}" + ) + self._name = f"{DOMAIN} {self.module_name} {SENSOR_TYPES[sensor_type][0]}" self.type = sensor_type self._state = None self._device_class = SENSOR_TYPES[self.type][3] self._icon = SENSOR_TYPES[self.type][2] self._unit_of_measurement = SENSOR_TYPES[self.type][1] - self._module_type = module["type"] + self._module_type = device["type"] self._module_id = module_info["id"] self._unique_id = f"{self._module_id}-{self.type}" diff --git a/requirements_all.txt b/requirements_all.txt index 46edbbe23d34e1..2663ef1c9b6746 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1115,7 +1115,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==3.0.1 +pyatmo==3.1.0 # homeassistant.components.atome pyatome==0.1.1 From c015f94fa2a759bb01ad61f2a8fbe2e256611305 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 21 Nov 2019 19:05:26 +0100 Subject: [PATCH 1668/3953] Updated frontend to 20191119.5 (#28925) * Updated frontend to 20191119.4 * Updated frontend to 20191119.5 --- 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 6e513d55742dde..41728bc1664041 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.2" + "home-assistant-frontend==20191119.5" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0eca803d653a91..fbdeae6ae56f92 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191119.2 +home-assistant-frontend==20191119.5 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2663ef1c9b6746..10a81e068327b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.2 +home-assistant-frontend==20191119.5 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e19d9fee73de0c..a8449f31049a6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -226,7 +226,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.2 +home-assistant-frontend==20191119.5 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From dba87fd2e1cff8d3308ab02b470ac8cfdf09f515 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 21 Nov 2019 17:35:03 +0100 Subject: [PATCH 1669/3953] Fix missing Netatmo sensors (#28899) * Update pyatmo to 3.0.1 * Improve sensor name * Fix missing sensors * Update pyatmo module * Update pyatmo to 3.1.0 * Update requirements * Fix method call --- homeassistant/components/netatmo/__init__.py | 2 +- .../components/netatmo/manifest.json | 4 ++-- homeassistant/components/netatmo/sensor.py | 24 ++++++++++++------- requirements_all.txt | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 4b9f0690ac5a4c..de371f97e28277 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -259,4 +259,4 @@ def update(self): @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) def update_event(self): """Call the Netatmo API to update the events.""" - self.camera_data.updateEvent(home=self.home, cameratype=self.camera_type) + self.camera_data.updateEvent(home=self.home, devicetype=self.camera_type) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 9d1178d9d17026..232e99eeae8b2c 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,10 +3,10 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==3.0.1" + "pyatmo==3.1.0" ], "dependencies": [ "webhook" ], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index f76062035d2fc7..fa7aedae739ba5 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -155,14 +155,12 @@ def find_devices(data): for module in all_module_infos.values(): if module["module_name"] not in module_names: continue + _LOGGER.debug( + "Adding module %s %s", module["module_name"], module["id"] + ) for condition in data.station_data.monitoredConditions( moduleId=module["id"] ): - _LOGGER.debug( - "Adding %s %s", - module["module_name"], - data.station_data.moduleById(mid=module["id"]), - ) entities.append(NetatmoSensor(data, module, condition.lower())) return entities @@ -200,18 +198,26 @@ class NetatmoSensor(Entity): def __init__(self, netatmo_data, module_info, sensor_type): """Initialize the sensor.""" self.netatmo_data = netatmo_data - module = self.netatmo_data.station_data.moduleById(mid=module_info["id"]) - if module["type"] == "NHC": + + device = self.netatmo_data.station_data.moduleById(mid=module_info["id"]) + if not device: + # Assume it's a station if module can't be found + device = self.netatmo_data.station_data.stationById(sid=module_info["id"]) + + if device["type"] == "NHC": self.module_name = module_info["station_name"] else: - self.module_name = module_info["module_name"] + self.module_name = ( + f"{module_info['station_name']} {module_info['module_name']}" + ) + self._name = f"{DOMAIN} {self.module_name} {SENSOR_TYPES[sensor_type][0]}" self.type = sensor_type self._state = None self._device_class = SENSOR_TYPES[self.type][3] self._icon = SENSOR_TYPES[self.type][2] self._unit_of_measurement = SENSOR_TYPES[self.type][1] - self._module_type = module["type"] + self._module_type = device["type"] self._module_id = module_info["id"] self._unique_id = f"{self._module_id}-{self.type}" diff --git a/requirements_all.txt b/requirements_all.txt index 833a4d471059a1..2b7255b09fa48a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1105,7 +1105,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==3.0.1 +pyatmo==3.1.0 # homeassistant.components.atome pyatome==0.1.1 From 42809ad6a99c2a2e923f0423db1750b763b9106a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 21 Nov 2019 19:05:26 +0100 Subject: [PATCH 1670/3953] Updated frontend to 20191119.5 (#28925) * Updated frontend to 20191119.4 * Updated frontend to 20191119.5 --- 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 6e513d55742dde..41728bc1664041 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.2" + "home-assistant-frontend==20191119.5" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a98bb2b46d7215..1c4aaa1191f426 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191119.2 +home-assistant-frontend==20191119.5 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2b7255b09fa48a..651c089a4fd80d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.2 +home-assistant-frontend==20191119.5 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75564e13b94ce1..8fc004f3363399 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.2 +home-assistant-frontend==20191119.5 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 3391fc660aabcd5c79761626f1d20d64cfb89916 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Nov 2019 12:25:12 -0800 Subject: [PATCH 1671/3953] Bumped version to 0.102.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ef16de573a00b9..3520260481b94c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From a62bd97fa597034c91222dbe4f75f7bae936659c Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 21 Nov 2019 14:39:24 -0600 Subject: [PATCH 1672/3953] Delay Plex websocket connection to avoid race (#28934) --- homeassistant/components/plex/__init__.py | 7 ++++++- homeassistant/components/plex/media_player.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 4a575722826ee5..386be56340a5ab 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -16,6 +16,7 @@ CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv @@ -164,12 +165,16 @@ def update_plex(): websocket = PlexWebsocket( plex_server.plex_server, update_plex, session=session, verify_ssl=verify_ssl ) - hass.loop.create_task(websocket.listen()) hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket + async def async_start_websocket_session(_): + await websocket.listen() + def close_websocket_session(_): websocket.close() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start_websocket_session) + unsub = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, close_websocket_session ) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index d6720fd9e95051..ad5fb2f73f10d1 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -68,6 +68,7 @@ def async_new_media_players(new_entities): hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players ) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + _LOGGER.debug("New entity listener created") @callback From ab9a60f83f20bfe63655a675fe4a691e74c7baf1 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 21 Nov 2019 14:39:24 -0600 Subject: [PATCH 1673/3953] Delay Plex websocket connection to avoid race (#28934) --- homeassistant/components/plex/__init__.py | 7 ++++++- homeassistant/components/plex/media_player.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 4a575722826ee5..386be56340a5ab 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -16,6 +16,7 @@ CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv @@ -164,12 +165,16 @@ def update_plex(): websocket = PlexWebsocket( plex_server.plex_server, update_plex, session=session, verify_ssl=verify_ssl ) - hass.loop.create_task(websocket.listen()) hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket + async def async_start_websocket_session(_): + await websocket.listen() + def close_websocket_session(_): websocket.close() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start_websocket_session) + unsub = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, close_websocket_session ) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index d6720fd9e95051..ad5fb2f73f10d1 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -68,6 +68,7 @@ def async_new_media_players(new_entities): hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players ) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + _LOGGER.debug("New entity listener created") @callback From e3b2a33962e0a01ccc187bd24cfbfe2d7fdc2f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Sandstr=C3=B6m?= Date: Thu, 21 Nov 2019 22:53:02 +0100 Subject: [PATCH 1674/3953] Bump verisure to vsure 1.5.4 and jsonpath 0.82 (#28933) --- homeassistant/components/verisure/__init__.py | 1 + homeassistant/components/verisure/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index d5cc7f31efb601..3ab98a6560e5ec 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -2,6 +2,7 @@ import logging import threading from datetime import timedelta + from jsonpath import jsonpath import verisure diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 38ea8c7314769e..13962e81b7bbbe 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -3,8 +3,8 @@ "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", "requirements": [ - "jsonpath==0.75", - "vsure==1.5.2" + "jsonpath==0.82", + "vsure==1.5.4" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 10a81e068327b6..3fd222ac268b63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -719,7 +719,7 @@ iperf3==0.1.11 ipify==1.0.0 # homeassistant.components.verisure -jsonpath==0.75 +jsonpath==0.82 # homeassistant.components.kodi jsonrpc-async==0.6 @@ -1977,7 +1977,7 @@ volkszaehler==0.1.2 volvooncall==0.8.7 # homeassistant.components.verisure -vsure==1.5.2 +vsure==1.5.4 # homeassistant.components.vasttrafik vtjp==0.1.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a8449f31049a6f..948731eb480c29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -251,7 +251,7 @@ iaqualink==0.3.0 influxdb==5.2.3 # homeassistant.components.verisure -jsonpath==0.75 +jsonpath==0.82 # homeassistant.scripts.keyring keyring==19.2.0 @@ -608,7 +608,7 @@ url-normalize==1.4.1 uvcclient==0.11.0 # homeassistant.components.verisure -vsure==1.5.2 +vsure==1.5.4 # homeassistant.components.vultr vultr==0.1.2 From 1968a88336073ee59538aa287dc408ba02f4af80 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 22 Nov 2019 00:32:10 +0000 Subject: [PATCH 1675/3953] [ci skip] Translation update --- homeassistant/components/abode/.translations/ru.json | 4 ++-- .../components/ambiclimate/.translations/ru.json | 4 ++-- .../components/ambient_station/.translations/ru.json | 2 +- homeassistant/components/axis/.translations/ru.json | 2 +- homeassistant/components/iaqualink/.translations/ru.json | 2 +- homeassistant/components/life360/.translations/ru.json | 8 ++++---- homeassistant/components/linky/.translations/ru.json | 6 +++--- .../components/logi_circle/.translations/ru.json | 2 +- homeassistant/components/neato/.translations/ru.json | 4 ++-- homeassistant/components/notion/.translations/ru.json | 4 ++-- homeassistant/components/plex/.translations/ru.json | 2 +- homeassistant/components/point/.translations/ru.json | 2 +- homeassistant/components/ps4/.translations/ru.json | 2 +- .../components/rainmachine/.translations/ru.json | 4 ++-- homeassistant/components/simplisafe/.translations/ru.json | 2 +- homeassistant/components/toon/.translations/ru.json | 8 ++++---- homeassistant/components/unifi/.translations/ru.json | 2 +- 17 files changed, 30 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/abode/.translations/ru.json b/homeassistant/components/abode/.translations/ru.json index f39e6b1443b0f5..590f7662731323 100644 --- a/homeassistant/components/abode/.translations/ru.json +++ b/homeassistant/components/abode/.translations/ru.json @@ -5,8 +5,8 @@ }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a Abode.", - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json index ba667ea7b9a724..2a99430e436979 100644 --- a/homeassistant/components/ambiclimate/.translations/ru.json +++ b/homeassistant/components/ambiclimate/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", - "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", + "already_setup": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambiclimate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Ambi Climate, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c. \n(\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0433\u043e \u0432\u044b\u0437\u043e\u0432\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 {cb_url})", "title": "Ambi Climate" } }, diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index 3a7c405ea4cd48..438b1cf87a7ff6 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", - "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." + "no_devices": "\u0412 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." }, "step": { "user": { diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 0345862b865f0b..24990bb0f1adab 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -10,7 +10,7 @@ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.", - "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "flow_title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis {name} ({host})", "step": { diff --git a/homeassistant/components/iaqualink/.translations/ru.json b/homeassistant/components/iaqualink/.translations/ru.json index 9a93c19ef20bdb..8c8e30fe067d4c 100644 --- a/homeassistant/components/iaqualink/.translations/ru.json +++ b/homeassistant/components/iaqualink/.translations/ru.json @@ -12,7 +12,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", "title": "Jandy iAqualink" } }, diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index eba3a47ead8088..c3f2601eb99220 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "user_already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "create_entry": { "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "error": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360.", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + "user_already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "step": { "user": { diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index 463343490a7993..da34fbbdb621f4 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443.", "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", + "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { @@ -16,7 +16,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "title": "Linky" } }, diff --git a/homeassistant/components/logi_circle/.translations/ru.json b/homeassistant/components/logi_circle/.translations/ru.json index 40c7c8853daeb4..9cecf3081b68ca 100644 --- a/homeassistant/components/logi_circle/.translations/ru.json +++ b/homeassistant/components/logi_circle/.translations/ru.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Logi Circle, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c.", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Logi Circle, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c.", "title": "Logi Circle" }, "user": { diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json index 999e45880cfdcf..57989a346fa3cd 100644 --- a/homeassistant/components/neato/.translations/ru.json +++ b/homeassistant/components/neato/.translations/ru.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "create_entry": { "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "error": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index 6c1d5f5d8d7715..15b540732a73a4 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -1,9 +1,9 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", + "identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", - "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e." + "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e." }, "step": { "user": { diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index bce55d35baa449..cb4c8247389613 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -11,7 +11,7 @@ }, "error": { "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.", + "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.", "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443.", "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d." }, diff --git a/homeassistant/components/point/.translations/ru.json b/homeassistant/components/point/.translations/ru.json index 487510969481f6..b0fc5a61f724a3 100644 --- a/homeassistant/components/point/.translations/ru.json +++ b/homeassistant/components/point/.translations/ru.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Minut, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c.", + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e [\u0441\u0441\u044b\u043b\u043a\u0435]({authorization_url}) \u0438 \u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u0435 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Minut, \u0437\u0430\u0442\u0435\u043c \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c.", "title": "Minut Point" }, "user": { diff --git a/homeassistant/components/ps4/.translations/ru.json b/homeassistant/components/ps4/.translations/ru.json index bf2484d02543f2..c7ac8d76cf1639 100644 --- a/homeassistant/components/ps4/.translations/ru.json +++ b/homeassistant/components/ps4/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "credential_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445.", + "credential_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445.", "devices_configured": "\u0412\u0441\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 PlayStation 4 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "port_987_bind_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0440\u0442\u0443 987. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/).", diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index afaa55424d25a8..ca535663f5439f 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index 721ba69d67ef61..301eed6d1c1ad8 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 58e6f53986c176..427f717e3adc1d 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -3,12 +3,12 @@ "abort": { "client_id": "Client ID \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "client_secret": "Client secret \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", - "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", + "no_agreements": "\u0423 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435\u0442 \u0434\u0438\u0441\u043f\u043b\u0435\u0435\u0432 Toon.", "no_app": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Toon \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { @@ -18,8 +18,8 @@ "tenant": "\u0412\u043b\u0430\u0434\u0435\u043b\u0435\u0446", "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0432\u043e\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Eneco Toon (\u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430).", - "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Toon" + "description": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0441\u0432\u043e\u0435\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Eneco Toon (\u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0443\u0447\u0451\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430).", + "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Toon" }, "display": { "data": { diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index dbb6efd83432d1..b01cdb84fbf929 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -5,7 +5,7 @@ "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c." }, "error": { - "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430." }, "step": { From 9a23a1c3365f1711db40e2a73980c2cdf39e51de Mon Sep 17 00:00:00 2001 From: kurniawan77 Date: Fri, 22 Nov 2019 13:54:56 +0100 Subject: [PATCH 1676/3953] Add device trigger support for Aqara WXKG11LM 2016 switch to Deconz (#28946) https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Xiaomi-WXKG11LM-2016 --- homeassistant/components/deconz/device_trigger.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 9d4a944d6958c7..62e05724cac2e1 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -245,6 +245,14 @@ (CONF_SHAKE, ""): 1007, } +AQARA_SQUARE_SWITCH_WXKG11LM_2016_MODEL = "lumi.sensor_switch.aq2" +AQARA_SQUARE_SWITCH_WXKG11LM_2016 = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): 1005, + (CONF_QUADRUPLE_PRESS, CONF_TURN_ON): 1006, +} + REMOTES = { HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE, HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE, @@ -260,6 +268,7 @@ AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH, AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, + AQARA_SQUARE_SWITCH_WXKG11LM_2016_MODEL: AQARA_SQUARE_SWITCH_WXKG11LM_2016, } TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( From 051e37eb49f4f48b4b6cf8b8a882346b6cde49d0 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 22 Nov 2019 15:32:05 +0100 Subject: [PATCH 1677/3953] Fix ikea lights on deconz (#28949) * Fix ikea lights on deconz * check for None * Use cleaner code style --- homeassistant/components/deconz/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index bf4b05089a84c0..eda2041b923116 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -152,6 +152,8 @@ async def async_turn_on(self, **kwargs): if ATTR_TRANSITION in kwargs: data["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) + elif "IKEA" in (self._device.manufacturer or ""): + data["transitiontime"] = 0 if ATTR_FLASH in kwargs: if kwargs[ATTR_FLASH] == FLASH_SHORT: From 6d77c15f2805d280c605ed9a669a9f5ecda640b2 Mon Sep 17 00:00:00 2001 From: awkwardDuck <34869622+awkwardDuck@users.noreply.github.com> Date: Fri, 22 Nov 2019 09:33:53 -0500 Subject: [PATCH 1678/3953] fix typo for mediumvioletred (#28941) --- homeassistant/util/color.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 837a0e77cd7316..7361b711dd2c9d 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -107,7 +107,7 @@ "mediumslateblue": (123, 104, 238), "mediumspringgreen": (0, 250, 154), "mediumturquoise": (72, 209, 204), - "mediumvioletredred": (199, 21, 133), + "mediumvioletred": (199, 21, 133), "midnightblue": (25, 25, 112), "mintcream": (245, 255, 250), "mistyrose": (255, 228, 225), From d92f48646a89de03ca532c082fb6d8dc934153ae Mon Sep 17 00:00:00 2001 From: Tobias Perschon Date: Fri, 22 Nov 2019 19:08:41 +0100 Subject: [PATCH 1679/3953] Add round to half template method (#28948) * added round to half method Signed-off-by: Tobias Perschon * testcase for new round method Signed-off-by: Tobias Perschon --- homeassistant/helpers/template.py | 2 ++ tests/helpers/test_template.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index aa17b2a1fba975..e389f95dae124c 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -669,6 +669,8 @@ def forgiving_round(value, precision=0, method="common"): value = math.ceil(float(value) * multiplier) / multiplier elif method == "floor": value = math.floor(float(value) * multiplier) / multiplier + elif method == "half": + value = round(float(value) * 2) / 2 else: # if method is common or something else, use common rounding value = round(float(value), precision) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index b69fdb17e35a7f..f2066ce2c6f0d7 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -227,6 +227,13 @@ def test_rounding_value(hass): == "12.8" ) + assert ( + template.Template( + '{{ states.sensor.temperature.state | round(1, "half") }}', hass + ).async_render() + == "13.0" + ) + def test_rounding_value_get_original_value_on_error(hass): """Test rounding value get original value on error.""" From a977f398ae244d53d85fdafeda8dce7c53d6d608 Mon Sep 17 00:00:00 2001 From: akasma74 Date: Fri, 22 Nov 2019 20:27:40 +0000 Subject: [PATCH 1680/3953] Fix return values of preset_mode(s) properties (#27751) It is incorrect to return None as a result of proprety call because in such a case state_attr call will return None as well, which means "Unknown attribute". Instead for preset_mode(s) PRESET_NONE constant should be used for consistency. --- homeassistant/components/generic_thermostat/climate.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index b765dbbfda4285..5cb4c21c5779ff 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -298,16 +298,12 @@ def hvac_modes(self): @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_AWAY if self._is_away else PRESET_NONE @property def preset_modes(self): - """Return a list of available preset modes.""" - if self._away_temp: - return [PRESET_NONE, PRESET_AWAY] - return None + """Return a list of available preset modes or PRESET_NONE if _away_temp is undefined.""" + return [PRESET_NONE, PRESET_AWAY] if self._away_temp else PRESET_NONE async def async_set_hvac_mode(self, hvac_mode): """Set hvac mode.""" From c35f9ee35f8efb318a8ab59c1e6d9d277a415379 Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 22 Nov 2019 22:21:28 +0100 Subject: [PATCH 1681/3953] Creating a scene by snapshotting entities (#28939) * Initial commit * Add tests * service.yaml * typo * snapshooted -> snapshot * snapshot_entities instead of snapshot * Edit validator * Fix tests * Remove keys() * Improve coverage * Activate scenes * Use pytest.raise * snapshot -> snapshotted --- .../components/homeassistant/scene.py | 46 +++++++++- homeassistant/components/scene/services.yaml | 5 + tests/components/homeassistant/test_scene.py | 92 +++++++++++++++++++ 3 files changed, 140 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index c505d1534deb9d..576bf540e006d3 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -55,7 +55,22 @@ def _convert_states(states): return result +def _ensure_no_intersection(value): + """Validate that entities and snapshot_entities do not overlap.""" + if ( + CONF_SNAPSHOT not in value + or CONF_ENTITIES not in value + or not any( + entity_id in value[CONF_SNAPSHOT] for entity_id in value[CONF_ENTITIES] + ) + ): + return value + + raise vol.Invalid("entities and snapshot_entities must not overlap") + + CONF_SCENE_ID = "scene_id" +CONF_SNAPSHOT = "snapshot_entities" STATES_SCHEMA = vol.All(dict, _convert_states) @@ -75,8 +90,16 @@ def _convert_states(states): extra=vol.ALLOW_EXTRA, ) -CREATE_SCENE_SCHEMA = vol.Schema( - {vol.Required(CONF_SCENE_ID): cv.slug, vol.Required(CONF_ENTITIES): STATES_SCHEMA} +CREATE_SCENE_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_ENTITIES, CONF_SNAPSHOT), + _ensure_no_intersection, + vol.Schema( + { + vol.Required(CONF_SCENE_ID): cv.slug, + vol.Optional(CONF_ENTITIES, default={}): STATES_SCHEMA, + vol.Optional(CONF_SNAPSHOT, default=[]): cv.entity_ids, + } + ), ) SERVICE_APPLY = "apply" @@ -139,7 +162,24 @@ async def apply_service(call): async def create_service(call): """Create a scene.""" - scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], call.data[CONF_ENTITIES]) + snapshot = call.data[CONF_SNAPSHOT] + entities = call.data[CONF_ENTITIES] + + for entity_id in snapshot: + state = hass.states.get(entity_id) + if state is None: + _LOGGER.warning( + "Entity %s does not exist and therefore cannot be snapshotted", + entity_id, + ) + continue + entities[entity_id] = State(entity_id, state.state, state.attributes) + + if not entities: + _LOGGER.warning("Empty scenes are not allowed") + return + + scene_config = SCENECONFIG(call.data[CONF_SCENE_ID], entities) entity_id = f"{SCENE_DOMAIN}.{scene_config.name}" old = platform.entities.get(entity_id) if old is not None: diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index 9cf1b9010a8048..0c261ed60b5041 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -34,3 +34,8 @@ create: light.ceiling: state: "on" brightness: 200 + snapshot_entities: + description: The entities of which a snapshot is to be taken + example: + - light.ceiling + - light.kitchen diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 25ce6088a511e6..d3bbac44df88b5 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -1,8 +1,13 @@ """Test Home Assistant scenes.""" from unittest.mock import patch +import pytest +import voluptuous as vol + from homeassistant.setup import async_setup_component +from tests.common import async_mock_service + async def test_reload_config_service(hass): """Test the reload config service.""" @@ -63,6 +68,16 @@ async def test_create_service(hass, caplog): assert hass.states.get("scene.hallo") is None assert hass.states.get("scene.hallo_2") is not None + assert await hass.services.async_call( + "scene", + "create", + {"scene_id": "hallo", "entities": {}, "snapshot_entities": []}, + blocking=True, + ) + await hass.async_block_till_done() + assert "Empty scenes are not allowed" in caplog.text + assert hass.states.get("scene.hallo") is None + assert await hass.services.async_call( "scene", "create", @@ -117,3 +132,80 @@ async def test_create_service(hass, caplog): assert scene.name == "hallo_2" assert scene.state == "scening" assert scene.attributes.get("entity_id") == ["light.kitchen"] + + +async def test_snapshot_service(hass, caplog): + """Test the snapshot option.""" + assert await async_setup_component(hass, "scene", {"scene": {}}) + hass.states.async_set("light.my_light", "on", {"hs_color": (345, 75)}) + assert hass.states.get("scene.hallo") is None + + assert await hass.services.async_call( + "scene", + "create", + {"scene_id": "hallo", "snapshot_entities": ["light.my_light"]}, + blocking=True, + ) + await hass.async_block_till_done() + scene = hass.states.get("scene.hallo") + assert scene is not None + assert scene.attributes.get("entity_id") == ["light.my_light"] + + hass.states.async_set("light.my_light", "off", {"hs_color": (123, 45)}) + turn_on_calls = async_mock_service(hass, "light", "turn_on") + assert await hass.services.async_call( + "scene", "turn_on", {"entity_id": "scene.hallo"}, blocking=True + ) + await hass.async_block_till_done() + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].data.get("entity_id") == "light.my_light" + assert turn_on_calls[0].data.get("hs_color") == (345, 75) + + assert await hass.services.async_call( + "scene", + "create", + {"scene_id": "hallo_2", "snapshot_entities": ["light.not_existent"]}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("scene.hallo_2") is None + assert ( + "Entity light.not_existent does not exist and therefore cannot be snapshotted" + in caplog.text + ) + + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo_3", + "entities": {"light.bed_light": {"state": "on", "brightness": 50}}, + "snapshot_entities": ["light.my_light"], + }, + blocking=True, + ) + await hass.async_block_till_done() + scene = hass.states.get("scene.hallo_3") + assert scene is not None + assert "light.my_light" in scene.attributes.get("entity_id") + assert "light.bed_light" in scene.attributes.get("entity_id") + + +async def test_ensure_no_intersection(hass): + """Test that entities and snapshot_entities do not overlap.""" + assert await async_setup_component(hass, "scene", {"scene": {}}) + + with pytest.raises(vol.MultipleInvalid) as ex: + assert await hass.services.async_call( + "scene", + "create", + { + "scene_id": "hallo", + "entities": {"light.my_light": {"state": "on", "brightness": 50}}, + "snapshot_entities": ["light.my_light"], + }, + blocking=True, + ) + await hass.async_block_till_done() + assert "entities and snapshot_entities must not overlap" in str(ex.value) + assert hass.states.get("scene.hallo") is None From 829e0a7c4268a643ea31969ae25c30305d58680c Mon Sep 17 00:00:00 2001 From: Jonas <5180118+K4ds3@users.noreply.github.com> Date: Fri, 22 Nov 2019 23:03:41 +0100 Subject: [PATCH 1682/3953] Add Proxmox VE integration (#27315) * Added the Proxmox VE integration * Fixed code as described in PR #27315 * Fixed small linting error * Fix code as described in PR #27315 code review * Improve code as described in PR #27315 --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/proxmoxve/__init__.py | 154 ++++++++++++++++++ .../components/proxmoxve/binary_sensor.py | 112 +++++++++++++ .../components/proxmoxve/manifest.json | 8 + requirements_all.txt | 3 + 6 files changed, 279 insertions(+) create mode 100644 homeassistant/components/proxmoxve/__init__.py create mode 100644 homeassistant/components/proxmoxve/binary_sensor.py create mode 100644 homeassistant/components/proxmoxve/manifest.json diff --git a/.coveragerc b/.coveragerc index ab4e91e5efd722..a241c9ab79c89d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -526,6 +526,7 @@ omit = homeassistant/components/proliphix/climate.py homeassistant/components/prometheus/* homeassistant/components/prowl/notify.py + homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py homeassistant/components/ptvsd/* homeassistant/components/pulseaudio_loopback/switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 005c3e3489e9a7..6b4b3e6a2f24a0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -239,6 +239,7 @@ homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew homeassistant/components/point/* @fredrike +homeassistant/components/proxmoxve/* @k4ds3 homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py new file mode 100644 index 00000000000000..246dc2d48ade0f --- /dev/null +++ b/homeassistant/components/proxmoxve/__init__.py @@ -0,0 +1,154 @@ +"""Support for Proxmox VE.""" +from enum import Enum +import logging +import time + +from proxmoxer import ProxmoxAPI +from proxmoxer.backends.https import AuthenticationError +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "proxmoxve" +PROXMOX_CLIENTS = "proxmox_clients" +CONF_REALM = "realm" +CONF_NODE = "node" +CONF_NODES = "nodes" +CONF_VMS = "vms" +CONF_CONTAINERS = "containers" + +DEFAULT_PORT = 8006 +DEFAULT_REALM = "pam" +DEFAULT_VERIFY_SSL = True + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_REALM, default=DEFAULT_REALM): cv.string, + vol.Optional( + CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL + ): cv.boolean, + vol.Required(CONF_NODES): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_NODE): cv.string, + vol.Optional(CONF_VMS, default=[]): [ + cv.positive_int + ], + vol.Optional(CONF_CONTAINERS, default=[]): [ + cv.positive_int + ], + } + ) + ], + ), + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the component.""" + + # Create API Clients for later use + hass.data[PROXMOX_CLIENTS] = {} + for entry in config[DOMAIN]: + host = entry[CONF_HOST] + port = entry[CONF_PORT] + user = entry[CONF_USERNAME] + realm = entry[CONF_REALM] + password = entry[CONF_PASSWORD] + verify_ssl = entry[CONF_VERIFY_SSL] + + try: + # Construct an API client with the given data for the given host + proxmox_client = ProxmoxClient( + host, port, user, realm, password, verify_ssl + ) + proxmox_client.build_client() + except AuthenticationError: + _LOGGER.warning( + "Invalid credentials for proxmox instance %s:%d", host, port + ) + continue + + hass.data[PROXMOX_CLIENTS][f"{host}:{port}"] = proxmox_client + + if hass.data[PROXMOX_CLIENTS]: + hass.helpers.discovery.load_platform( + "binary_sensor", DOMAIN, {"entries": config[DOMAIN]}, config + ) + return True + + return False + + +class ProxmoxItemType(Enum): + """Represents the different types of machines in Proxmox.""" + + qemu = 0 + lxc = 1 + + +class ProxmoxClient: + """A wrapper for the proxmoxer ProxmoxAPI client.""" + + def __init__(self, host, port, user, realm, password, verify_ssl): + """Initialize the ProxmoxClient.""" + + self._host = host + self._port = port + self._user = user + self._realm = realm + self._password = password + self._verify_ssl = verify_ssl + + self._proxmox = None + self._connection_start_time = None + + def build_client(self): + """Construct the ProxmoxAPI client.""" + + self._proxmox = ProxmoxAPI( + self._host, + port=self._port, + user=f"{self._user}@{self._realm}", + password=self._password, + verify_ssl=self._verify_ssl, + ) + + self._connection_start_time = time.time() + + def get_api_client(self): + """Return the ProxmoxAPI client and rebuild it if necessary.""" + + connection_age = time.time() - self._connection_start_time + + # Workaround for the Proxmoxer bug where the connection stops working after some time + if connection_age > 30 * 60: + self.build_client() + + return self._proxmox diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py new file mode 100644 index 00000000000000..15b1f1483e1f47 --- /dev/null +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -0,0 +1,112 @@ +"""Binary sensor to read Proxmox VE data.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import ATTR_ATTRIBUTION, CONF_HOST, CONF_PORT + +from . import CONF_CONTAINERS, CONF_NODES, CONF_VMS, PROXMOX_CLIENTS, ProxmoxItemType + +ATTRIBUTION = "Data provided by Proxmox VE" +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the sensor platform.""" + + sensors = [] + + for entry in discovery_info["entries"]: + port = entry[CONF_PORT] + + for node in entry[CONF_NODES]: + for virtual_machine in node[CONF_VMS]: + sensors.append( + ProxmoxBinarySensor( + hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], + node["node"], + ProxmoxItemType.qemu, + virtual_machine, + ) + ) + + for container in node[CONF_CONTAINERS]: + sensors.append( + ProxmoxBinarySensor( + hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], + node["node"], + ProxmoxItemType.lxc, + container, + ) + ) + + add_entities(sensors, True) + + +class ProxmoxBinarySensor(BinarySensorDevice): + """A binary sensor for reading Proxmox VE data.""" + + def __init__(self, proxmox_client, item_node, item_type, item_id): + """Initialize the binary sensor.""" + self._proxmox_client = proxmox_client + self._item_node = item_node + self._item_type = item_type + self._item_id = item_id + + self._vmname = None + self._name = None + + self._state = None + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def is_on(self): + """Return true if VM/container is running.""" + return self._state + + @property + def device_state_attributes(self): + """Return device attributes of the entity.""" + return { + "node": self._item_node, + "vmid": self._item_id, + "vmname": self._vmname, + "type": self._item_type.name, + ATTR_ATTRIBUTION: ATTRIBUTION, + } + + def update(self): + """Check if the VM/Container is running.""" + item = self.poll_item() + + if item is None: + _LOGGER.warning("Failed to poll VM/container %s", self._item_id) + return + + self._state = item["status"] == "running" + + def poll_item(self): + """Find the VM/Container with the set item_id.""" + items = ( + self._proxmox_client.get_api_client() + .nodes(self._item_node) + .get(self._item_type.name) + ) + item = next( + (item for item in items if item["vmid"] == str(self._item_id)), None + ) + + if item is None: + _LOGGER.warning("Couldn't find VM/Container with the ID %s", self._item_id) + return None + + if self._vmname is None: + self._vmname = item["name"] + + if self._name is None: + self._name = f"{self._item_node} {self._vmname} running" + + return item diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json new file mode 100644 index 00000000000000..9c03038a630238 --- /dev/null +++ b/homeassistant/components/proxmoxve/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "proxmoxve", + "name": "Proxmox VE", + "documentation": "https://www.home-assistant.io/integrations/proxmoxve", + "dependencies": [], + "codeowners": ["@k4ds3"], + "requirements": ["proxmoxer==1.0.3"] + } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 3fd222ac268b63..7d7852add97976 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1023,6 +1023,9 @@ prometheus_client==0.7.1 # homeassistant.components.tensorflow protobuf==3.6.1 +# homeassistant.components.proxmoxve +proxmoxer==1.0.3 + # homeassistant.components.systemmonitor psutil==5.6.5 From b4caa4ab82012652448b1130ca8ac43fd0201689 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 23 Nov 2019 00:32:17 +0000 Subject: [PATCH 1683/3953] [ci skip] Translation update --- .../geonetnz_volcano/.translations/fr.json | 15 +++++++++++++++ .../geonetnz_volcano/.translations/lb.json | 16 ++++++++++++++++ .../geonetnz_volcano/.translations/pl.json | 16 ++++++++++++++++ .../hisense_aehw4a1/.translations/fr.json | 15 +++++++++++++++ .../components/vacuum/.translations/fr.json | 4 ++-- 5 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/geonetnz_volcano/.translations/fr.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/lb.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/pl.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/fr.json diff --git a/homeassistant/components/geonetnz_volcano/.translations/fr.json b/homeassistant/components/geonetnz_volcano/.translations/fr.json new file mode 100644 index 00000000000000..2692768910c8cd --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "identifier_exists": "Emplacement d\u00e9j\u00e0 enregistr\u00e9" + }, + "step": { + "user": { + "data": { + "radius": "Rayon" + }, + "title": "Remplissez les d\u00e9tails de votre filtre." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/lb.json b/homeassistant/components/geonetnz_volcano/.translations/lb.json new file mode 100644 index 00000000000000..a7ad17e6bd5731 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Standuert ass scho registr\u00e9iert" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "F\u00ebllt \u00e4r Filter D\u00e9tailer aus." + } + }, + "title": "GeoNet NZ Vulkan" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/pl.json b/homeassistant/components/geonetnz_volcano/.translations/pl.json new file mode 100644 index 00000000000000..22bb34e21a602f --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokalizacja ju\u017c zarejestrowana" + }, + "step": { + "user": { + "data": { + "radius": "Promie\u0144" + }, + "title": "Wype\u0142nij szczeg\u00f3\u0142y dotycz\u0105ce filtra." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/fr.json b/homeassistant/components/hisense_aehw4a1/.translations/fr.json new file mode 100644 index 00000000000000..50c753538c78fe --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/fr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun p\u00e9riph\u00e9rique AEH-W4A1 trouv\u00e9 sur le r\u00e9seau.", + "single_instance_allowed": "Une seule configuration de AEH-W4A1 est possible." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/fr.json b/homeassistant/components/vacuum/.translations/fr.json index 44e7b2887e2717..4a0ab7f8de7255 100644 --- a/homeassistant/components/vacuum/.translations/fr.json +++ b/homeassistant/components/vacuum/.translations/fr.json @@ -6,11 +6,11 @@ }, "condtion_type": { "is_cleaning": "{entity_name} nettoie", - "is_docked": "{entity_name} est sur la base" + "is_docked": "{entity_name} est connect\u00e9" }, "trigger_type": { "cleaning": "{entity_name} commence \u00e0 nettoyer", - "docked": "{entity_name} est sur la base" + "docked": "{entity_name} connect\u00e9" } } } \ No newline at end of file From 08432c7c0903d0b695141370f7c7c3eedbc42ae4 Mon Sep 17 00:00:00 2001 From: Chris Mandich Date: Sat, 23 Nov 2019 00:55:46 -0800 Subject: [PATCH 1684/3953] Add flume support (#27235) * Add Flume Sensor Add support for Flume API and sensor * Add support for choosing timezone Timezone is now a required option in configuration.yaml * Add Flume to coveragerc and CODEOWNERS Add flume to be ommited from testing. Add @ChrisMandich as Flume Code owner * Revert "Add Flume to coveragerc and CODEOWNERS" This reverts commit 0b27437a3b13f024332c6498b3bb0d554482e49b. * Update manifest.json Added Documentation URL for Flume to HASS.IO * Update manifest.json Added missing Newline at EOF. * Update sensor.py Update sensor to not required Device ID for configuration. Now loops through all available Type 2 devices and adds them as an entity. * Update Manifest, CODEOWNERS, and manifest.json Resolved errors related to code owners and requirements. Using hassfest and gen_requirements_all * Update sensor.py Implemented recommendations from @Quentame. Including time zone from Home Assistant, Updated variable names, and Consolidated duplicate functions. * Implemented suggested changes from @Quentame This includes: components name, using f-strings instead of concat, snake_case for variables, constants for the addition of future device types, clearer errors, and removed variables no longer in use. * Update sensor.py Restored unit_of_measurement. Updated return to "gal". * Address pylint errors * Update sensor.py Include protected attributes in setup_platform. * Address Pylint errors homeassistant/components/flume/sensor.py:63:11: W0703: Catching too general exception Exception (broad-except) homeassistant/components/flume/sensor.py:133:8: R1720: Unnecessary "else" after "raise" (no-else-raise) homeassistant/components/flume/sensor.py:162:8: R1705: Unnecessary "else" after "return" (no-else-return) homeassistant/components/flume/sensor.py:236:8: R1720: Unnecessary "else" after "raise" (no-else-raise) * Update sensor.py I'm okay with the broad exception clause. homeassistant/components/flume/sensor.py:65:11: W0703: Catching too general exception Exception (broad-except) * Update sensor.py Add more specific exceptions for Try/Except. * Update Flume Sensory.py add requirements, exclude from tests, us pyflume pypi package. * Update sensor.py to support latest pyflume package * Update manifest provide better flow of manifest and easier readibility. * Update manifest.json Reccomended by @balloob as it is already a core requirement * Update sensor.py Add proposed changes from @balloob * Update requirements_all.txt pytz is a core dependency, removing flume's requirement for it. * Update sensor.py Added @MartinHjelmare recommended changes. * Update sensor.py Resolving PyLint error * Update sensor.py Remove `KeyError`. Add length check for flume entity list before adding. * Update sensor.py * Update pyflume version * Update imports with isort * Add line break between standard library and thirdparty imports. * Remove throttle from sensor.py --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/flume/__init__.py | 1 + homeassistant/components/flume/manifest.json | 11 +++ homeassistant/components/flume/sensor.py | 90 ++++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 107 insertions(+) create mode 100644 homeassistant/components/flume/__init__.py create mode 100644 homeassistant/components/flume/manifest.json create mode 100644 homeassistant/components/flume/sensor.py diff --git a/.coveragerc b/.coveragerc index a241c9ab79c89d..20be7adbd9455f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -229,6 +229,7 @@ omit = homeassistant/components/flexit/climate.py homeassistant/components/flic/binary_sensor.py homeassistant/components/flock/notify.py + homeassistant/components/flume/* homeassistant/components/flunearyou/sensor.py homeassistant/components/flux_led/light.py homeassistant/components/folder/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 6b4b3e6a2f24a0..0aca3b9d6fb9f2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -101,6 +101,7 @@ homeassistant/components/filter/* @dgomes homeassistant/components/fitbit/* @robbiet480 homeassistant/components/fixer/* @fabaff homeassistant/components/flock/* @fabaff +homeassistant/components/flume/* @ChrisMandich homeassistant/components/flunearyou/* @bachya homeassistant/components/fortigate/* @kifeo homeassistant/components/fortios/* @kimfrellsen diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py new file mode 100644 index 00000000000000..ab626e1f156040 --- /dev/null +++ b/homeassistant/components/flume/__init__.py @@ -0,0 +1 @@ +"""The Flume component.""" diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json new file mode 100644 index 00000000000000..800751e80ef2ea --- /dev/null +++ b/homeassistant/components/flume/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "flume", + "name": "Flume", + "documentation": "https://www.home-assistant.io/integrations/flume/", + "requirements": [ + "pyflume==0.2.1" + ], + "dependencies": [], + "codeowners": ["@ChrisMandich"] + } + \ No newline at end of file diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py new file mode 100644 index 00000000000000..5fee408e0dcc43 --- /dev/null +++ b/homeassistant/components/flume/sensor.py @@ -0,0 +1,90 @@ +"""Sensor for displaying the number of result from Flume.""" +from datetime import timedelta +import logging + +from pyflume import FlumeData, FlumeDeviceList +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Flume Sensor" + +CONF_CLIENT_ID = "client_id" +CONF_CLIENT_SECRET = "client_secret" +FLUME_TYPE_SENSOR = 2 + +SCAN_INTERVAL = timedelta(minutes=1) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Flume sensor.""" + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + client_id = config[CONF_CLIENT_ID] + client_secret = config[CONF_CLIENT_SECRET] + time_zone = str(hass.config.time_zone) + name = config[CONF_NAME] + flume_entity_list = [] + + flume_devices = FlumeDeviceList(username, password, client_id, client_secret) + + for device in flume_devices.device_list: + if device["type"] == FLUME_TYPE_SENSOR: + flume = FlumeData( + username, + password, + client_id, + client_secret, + device["id"], + time_zone, + SCAN_INTERVAL, + ) + flume_entity_list.append(FlumeSensor(flume, f"{name} {device['id']}")) + + if flume_entity_list: + add_entities(flume_entity_list, True) + + +class FlumeSensor(Entity): + """Representation of the Flume sensor.""" + + def __init__(self, flume, name): + """Initialize the Flume sensor.""" + self.flume = flume + self._name = name + self._state = None + + @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 unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return "gal" + + def update(self): + """Get the latest data and updates the states.""" + self.flume.update() + self._state = self.flume.value diff --git a/requirements_all.txt b/requirements_all.txt index 7d7852add97976..ba69df2f28a428 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1225,6 +1225,9 @@ pyflexit==0.3 # homeassistant.components.flic pyflic-homeassistant==0.4.dev0 +# homeassistant.components.flume +pyflume==0.2.1 + # homeassistant.components.flunearyou pyflunearyou==1.0.3 From 0bd01ba495842a09876548bebd98d63377af5b7b Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 23 Nov 2019 11:00:26 +0100 Subject: [PATCH 1685/3953] Fix manual config (#28956) --- homeassistant/components/netatmo/sensor.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index fa7aedae739ba5..1ae076c6560775 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -547,13 +547,18 @@ def __init__(self, auth, data_class, station): self.data = {} self.station_data = self.data_class(self.auth) self.station = station + self.station_id = None + if station: + station_data = self.station_data.stationByName(self.station) + if station_data: + self.station_id = station_data.get("_id") self._next_update = time() self._update_in_progress = threading.Lock() def get_module_infos(self): """Return all modules available on the API as a dict.""" - if self.station is not None: - return self.station_data.getModules(station=self.station) + if self.station_id is not None: + return self.station_data.getModules(station_id=self.station_id) return self.station_data.getModules() def update(self): @@ -579,7 +584,7 @@ def update(self): return data = self.station_data.lastData( - station=self.station, exclude=3600, byId=True + station=self.station_id, exclude=3600, byId=True ) if not data: self._next_update = time() + NETATMO_UPDATE_INTERVAL From 23737e0225911911f171ba3e673092a21faa88c3 Mon Sep 17 00:00:00 2001 From: Tomasz Date: Sat, 23 Nov 2019 11:02:52 +0100 Subject: [PATCH 1686/3953] Rename rest_command request to response, add exc_info for exceptions (#28521) * rename request to response, isort and black fixes * Log exception details * Add status code to success log, reformat log * new syntax for response * changed info to debug --- .../components/rest_command/__init__.py | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 223dc7da7cc834..10f37b6ac4c95b 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -4,17 +4,16 @@ import aiohttp from aiohttp import hdrs -import async_timeout import voluptuous as vol from homeassistant.const import ( - CONF_TIMEOUT, - CONF_USERNAME, + CONF_HEADERS, + CONF_METHOD, CONF_PASSWORD, - CONF_URL, CONF_PAYLOAD, - CONF_METHOD, - CONF_HEADERS, + CONF_TIMEOUT, + CONF_URL, + CONF_USERNAME, CONF_VERIFY_SSL, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -96,21 +95,32 @@ async def async_service_handler(service): request_url = template_url.async_render(variables=service.data) try: - with async_timeout.timeout(timeout): - request = await getattr(websession, method)( - request_url, data=payload, auth=auth, headers=headers - ) - - if request.status < 400: - _LOGGER.info("Success call %s.", request.url) - else: - _LOGGER.warning("Error %d on call %s.", request.status, request.url) + async with getattr(websession, method)( + request_url, + data=payload, + auth=auth, + headers=headers, + timeout=timeout, + ) as response: + + if response.status < 400: + _LOGGER.debug( + "Success. Url: %s. Status code: %d.", + response.url, + response.status, + ) + else: + _LOGGER.warning( + "Error. Url: %s. Status code %d.", + response.url, + response.status, + ) except asyncio.TimeoutError: - _LOGGER.warning("Timeout call %s.", request.url) + _LOGGER.warning("Timeout call %s.", response.url, exc_info=1) except aiohttp.ClientError: - _LOGGER.error("Client error %s.", request_url) + _LOGGER.error("Client error %s.", request_url, exc_info=1) # register services hass.services.async_register(DOMAIN, name, async_service_handler) From b4f477f4de0f0cf03ade872a270db7549a971353 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Sat, 23 Nov 2019 11:13:44 +0100 Subject: [PATCH 1687/3953] Upgrade youtube_dl to 2019.11.22 (#28964) --- 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 f413ffd16dbc38..5c5aaa0ce22816 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/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.11.05" + "youtube_dl==2019.11.22" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index ba69df2f28a428..88e5893cfbd3c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2064,7 +2064,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.11.05 +youtube_dl==2019.11.22 # homeassistant.components.zengge zengge==0.2 From 0a58853689c5d316026f45f24c2a7d86a996b503 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Sat, 23 Nov 2019 03:29:54 -0800 Subject: [PATCH 1688/3953] Fix temp not being reported properly (#28973) --- homeassistant/components/abode/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index d84bfe52441d8f..573df6d49b4faf 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -72,19 +72,19 @@ def unique_id(self): @property def state(self): """Return the state of the sensor.""" - if self._sensor_type == "temp": + if self._sensor_type == CONST.TEMP_STATUS_KEY: return self._device.temp - if self._sensor_type == "humidity": + if self._sensor_type == CONST.HUMI_STATUS_KEY: return self._device.humidity - if self._sensor_type == "lux": + if self._sensor_type == CONST.LUX_STATUS_KEY: return self._device.lux @property def unit_of_measurement(self): """Return the units of measurement.""" - if self._sensor_type == "temp": + if self._sensor_type == CONST.TEMP_STATUS_KEY: return self._device.temp_unit - if self._sensor_type == "humidity": + if self._sensor_type == CONST.HUMI_STATUS_KEY: return self._device.humidity_unit - if self._sensor_type == "lux": + if self._sensor_type == CONST.LUX_STATUS_KEY: return self._device.lux_unit From 614842bcf2d2d7dc250de50e3d2f04496f4baccc Mon Sep 17 00:00:00 2001 From: majuss Date: Sat, 23 Nov 2019 14:41:52 +0100 Subject: [PATCH 1689/3953] Bumped pypca to 0.0.7 (#28976) --- homeassistant/components/elv/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index b4871a805d2d61..8390fc597f0871 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/pca", "dependencies": [], "codeowners": ["@majuss"], - "requirements": ["pypca==0.0.5"] + "requirements": ["pypca==0.0.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 88e5893cfbd3c2..07d8aed9929136 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1418,7 +1418,7 @@ pyowlet==1.0.3 pyowm==2.10.0 # homeassistant.components.elv -pypca==0.0.5 +pypca==0.0.7 # homeassistant.components.lcn pypck==0.6.3 From 2c00a9ec68eef7f7913849968b51344b8fe37267 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 23 Nov 2019 09:16:21 -0600 Subject: [PATCH 1690/3953] Fix Plex setup race condition v2 (#28943) * Connect websocket when platforms ready, not when HA is ready * Use callbacks from platform setup tasks instead * Convert setup to async * Apply suggestions from code review Co-Authored-By: Martin Hjelmare --- homeassistant/components/plex/__init__.py | 34 +++++++++++++---------- homeassistant/components/plex/const.py | 3 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 386be56340a5ab..971032302888ed 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,5 +1,6 @@ """Support to embed Plex.""" import asyncio +import functools import logging import plexapi.exceptions @@ -16,7 +17,6 @@ CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv @@ -37,6 +37,7 @@ DISPATCHERS, DOMAIN as PLEX_DOMAIN, PLATFORMS, + PLATFORMS_COMPLETED, PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, PLEX_UPDATE_PLATFORMS_SIGNAL, @@ -72,18 +73,21 @@ _LOGGER = logging.getLogger(__package__) -def setup(hass, config): +async def async_setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}}) + hass.data.setdefault( + PLEX_DOMAIN, + {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}, PLATFORMS_COMPLETED: {}}, + ) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: - _setup_plex(hass, plex_config) + _async_setup_plex(hass, plex_config) return True -def _setup_plex(hass, config): +def _async_setup_plex(hass, config): """Pass configuration to a config flow.""" server_config = dict(config) if MP_DOMAIN in server_config: @@ -141,11 +145,7 @@ async def async_setup_entry(hass, entry): ) server_id = plex_server.machine_identifier hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server - - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] = set() entry.add_update_listener(async_options_updated) @@ -167,19 +167,25 @@ def update_plex(): ) hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket - async def async_start_websocket_session(_): - await websocket.listen() + def start_websocket_session(platform, _): + hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id].add(platform) + if hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] == PLATFORMS: + hass.loop.create_task(websocket.listen()) def close_websocket_session(_): websocket.close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start_websocket_session) - unsub = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, close_websocket_session ) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + for platform in PLATFORMS: + task = hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + task.add_done_callback(functools.partial(start_websocket_session, platform)) + return True diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index d3c79e60bc48f5..ad62bade1fd0f9 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -9,7 +9,8 @@ DEFAULT_VERIFY_SSL = True DISPATCHERS = "dispatchers" -PLATFORMS = ["media_player", "sensor"] +PLATFORMS = frozenset(["media_player", "sensor"]) +PLATFORMS_COMPLETED = "platforms_completed" SERVERS = "servers" WEBSOCKETS = "websockets" From f1d4a3cd6ef68257b8b3e605baff631e13fd01c6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 23 Nov 2019 16:46:06 +0100 Subject: [PATCH 1691/3953] Catch Zeroconf exception (#28728) * Catch Zeroconf exception * Make it an error --- homeassistant/components/zeroconf/__init__.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 2f9fb7b4580fa6..41a6c7510f9b27 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -8,7 +8,13 @@ import ipaddress import voluptuous as vol -from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf +from zeroconf import ( + ServiceBrowser, + ServiceInfo, + ServiceStateChange, + Zeroconf, + NonUniqueNameException, +) from homeassistant import util from homeassistant.const import ( @@ -43,7 +49,7 @@ def setup(hass, config): params = { "version": __version__, "base_url": hass.config.api.base_url, - # always needs authentication + # Always needs authentication "requires_api_password": True, } @@ -69,7 +75,12 @@ def zeroconf_hass_start(_event): Wait till started or otherwise HTTP is not up and running. """ _LOGGER.info("Starting Zeroconf broadcast") - zeroconf.register_service(info) + try: + zeroconf.register_service(info) + except NonUniqueNameException: + _LOGGER.error( + "Home Assistant instance with identical name present in the local network" + ) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, zeroconf_hass_start) From ebd38395fbfcc937418d327eaa4afc8cd9e6d19c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 24 Nov 2019 04:46:48 +1300 Subject: [PATCH 1692/3953] Update pyjuicenet dependency (#28958) Closes #28926 --- homeassistant/components/juicenet/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 076567573c724d..fac59cb3b8d658 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -3,7 +3,7 @@ "name": "Juicenet", "documentation": "https://www.home-assistant.io/integrations/juicenet", "requirements": [ - "python-juicenet==0.1.5" + "python-juicenet==0.1.6" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 07d8aed9929136..f43d92dc46c809 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1559,7 +1559,7 @@ python-izone==1.1.1 python-join-api==0.0.4 # homeassistant.components.juicenet -python-juicenet==0.1.5 +python-juicenet==0.1.6 # homeassistant.components.lirc # python-lirc==1.2.3 From 2a8e99d799b3848ef4f63fb6458d5bbfbbe5e177 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 23 Nov 2019 18:01:27 +0100 Subject: [PATCH 1693/3953] Upgrade mutagen to 1.43.0 (#28984) --- homeassistant/components/tts/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index ca2059a4d19e8f..cb780523977724 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -3,7 +3,7 @@ "name": "Tts", "documentation": "https://www.home-assistant.io/integrations/tts", "requirements": [ - "mutagen==1.42.0" + "mutagen==1.43.0" ], "dependencies": [ "http" diff --git a/requirements_all.txt b/requirements_all.txt index f43d92dc46c809..0b348e2c9405e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -848,7 +848,7 @@ mitemp_bt==0.0.3 motorparts==1.1.0 # homeassistant.components.tts -mutagen==1.42.0 +mutagen==1.43.0 # homeassistant.components.mychevy mychevy==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 948731eb480c29..f696bab205240b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -281,7 +281,7 @@ mficlient==0.3.0 minio==4.0.9 # homeassistant.components.tts -mutagen==1.42.0 +mutagen==1.43.0 # homeassistant.components.ness_alarm nessclient==0.9.15 From 9fd8ffdc44fa4c44a0063f934864944935a6817c Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Sun, 24 Nov 2019 03:22:50 +0700 Subject: [PATCH 1694/3953] Bumped keenetic NDMS2 client version to 0.0.11 (#28990) --- homeassistant/components/keenetic_ndms2/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 4613d2d9608022..df78c98aa3ca69 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,7 +3,7 @@ "name": "Keenetic ndms2", "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": [ - "ndms2_client==0.0.10" + "ndms2_client==0.0.11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0b348e2c9405e3..28712f1375fef4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -863,7 +863,7 @@ n26==0.2.7 nad_receiver==0.0.11 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.0.10 +ndms2_client==0.0.11 # homeassistant.components.ness_alarm nessclient==0.9.15 From 6ad74fba301de30a31b83b94081d25219bd60911 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 23 Nov 2019 22:12:56 +0100 Subject: [PATCH 1695/3953] Updated frontend to 20191119.6 (#28996) --- 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 41728bc1664041..b196239dfb1fb5 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.5" + "home-assistant-frontend==20191119.6" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fbdeae6ae56f92..1f393b44967647 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191119.5 +home-assistant-frontend==20191119.6 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 28712f1375fef4..748b204204e11b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -659,7 +659,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.5 +home-assistant-frontend==20191119.6 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f696bab205240b..034ecbb056e0a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -226,7 +226,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.5 +home-assistant-frontend==20191119.6 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 98eae305e23da73a6c883421155e9cdff8762a0b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 23 Nov 2019 23:11:01 +0100 Subject: [PATCH 1696/3953] Upgrade zeroconf to 0.24.0 (#28986) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 39f016e9d0e8ca..ba764300daee2f 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -3,7 +3,7 @@ "name": "Zeroconf", "documentation": "https://www.home-assistant.io/integrations/zeroconf", "requirements": [ - "zeroconf==0.23.0" + "zeroconf==0.24.0" ], "dependencies": [ "api" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1f393b44967647..1edc5184c02a7d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.11 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.23.0 +zeroconf==0.24.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 748b204204e11b..76b476c4ba1a2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2070,7 +2070,7 @@ youtube_dl==2019.11.22 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.23.0 +zeroconf==0.24.0 # homeassistant.components.zha zha-quirks==0.0.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 034ecbb056e0a5..51e193c8e7f452 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -644,7 +644,7 @@ ya_ma==0.3.8 yahooweather==0.10 # homeassistant.components.zeroconf -zeroconf==0.23.0 +zeroconf==0.24.0 # homeassistant.components.zha zha-quirks==0.0.28 From bbca6e3ac2535f33cf07fc6933b5eb7d56c2dcf1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 23 Nov 2019 23:12:02 +0100 Subject: [PATCH 1697/3953] Upgrade shodan to 1.20.0 (#28983) * Upgrade shodan to 1.20.0 * Upgrade mutagen to 1.43.0 --- homeassistant/components/shodan/manifest.json | 2 +- homeassistant/components/tts/__init__.py | 6 +++--- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 36348a1975cecd..36af63da9f8912 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/integrations/shodan", "requirements": [ - "shodan==1.19.1" + "shodan==1.20.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index d17f64a3a3a4f0..8ae06771618eaf 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -1,4 +1,4 @@ -"""Provide functionality to TTS.""" +"""Provide functionality for TTS.""" import asyncio import ctypes import functools as ft @@ -353,7 +353,7 @@ async def async_get_tts_audio(self, engine, key, message, cache, language, optio raise HomeAssistantError(f"No TTS from {engine} for '{message}'") # Create file infos - filename = (f"{key}.{extension}").lower() + filename = f"{key}.{extension}".lower() data = self.write_tags(filename, data, provider, message, language, options) @@ -438,7 +438,7 @@ async def async_read_tts(self, filename): await self.async_file_to_mem(key) content, _ = mimetypes.guess_type(filename) - return (content, self.mem_cache[key][MEM_CACHE_VOICE]) + return content, self.mem_cache[key][MEM_CACHE_VOICE] @staticmethod def write_tags(filename, data, provider, message, language, options): diff --git a/requirements_all.txt b/requirements_all.txt index 76b476c4ba1a2a..c48f932965a457 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1782,7 +1782,7 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.19.1 +shodan==1.20.0 # homeassistant.components.simplepush simplepush==1.1.4 From 7c6b38d30810a87cd14cc02c514a8ba6753474ba Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 24 Nov 2019 00:32:21 +0000 Subject: [PATCH 1698/3953] [ci skip] Translation update --- .../components/almond/.translations/nl.json | 5 +++ .../coolmaster/.translations/nl.json | 3 ++ .../components/demo/.translations/nl.json | 5 +++ .../components/fan/.translations/nl.json | 16 +++++++++ .../geonetnz_volcano/.translations/nl.json | 15 ++++++++ .../components/lock/.translations/nl.json | 4 +++ .../components/sensor/.translations/hu.json | 36 +++++++++---------- .../components/vacuum/.translations/nl.json | 10 ++++++ 8 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/demo/.translations/nl.json create mode 100644 homeassistant/components/fan/.translations/nl.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/nl.json create mode 100644 homeassistant/components/vacuum/.translations/nl.json diff --git a/homeassistant/components/almond/.translations/nl.json b/homeassistant/components/almond/.translations/nl.json index dfe9c238db7b8a..d77fe69f7fa8eb 100644 --- a/homeassistant/components/almond/.translations/nl.json +++ b/homeassistant/components/almond/.translations/nl.json @@ -5,6 +5,11 @@ "cannot_connect": "Kan geen verbinding maken met de Almond-server.", "missing_configuration": "Raadpleeg de documentatie over het instellen van Almond." }, + "step": { + "pick_implementation": { + "title": "Kies de authenticatie methode" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/nl.json b/homeassistant/components/coolmaster/.translations/nl.json index 79a1e9fe1e6f72..02b65cdfff912a 100644 --- a/homeassistant/components/coolmaster/.translations/nl.json +++ b/homeassistant/components/coolmaster/.translations/nl.json @@ -5,6 +5,9 @@ }, "step": { "user": { + "data": { + "off": "Kan uitgeschakeld worden" + }, "title": "Stel uw CoolMasterNet-verbindingsgegevens in." } }, diff --git a/homeassistant/components/demo/.translations/nl.json b/homeassistant/components/demo/.translations/nl.json new file mode 100644 index 00000000000000..ef01fcb4f3c35e --- /dev/null +++ b/homeassistant/components/demo/.translations/nl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/nl.json b/homeassistant/components/fan/.translations/nl.json new file mode 100644 index 00000000000000..706c2c92b19183 --- /dev/null +++ b/homeassistant/components/fan/.translations/nl.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Schakel {entity_name} uit", + "turn_on": "Schakel {entity_name} in" + }, + "condtion_type": { + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld" + }, + "trigger_type": { + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/nl.json b/homeassistant/components/geonetnz_volcano/.translations/nl.json new file mode 100644 index 00000000000000..73c7c1eaab358d --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "identifier_exists": "Locatie al geregistreerd" + }, + "step": { + "user": { + "data": { + "radius": "Straal" + }, + "title": "Vul uw filtergegevens in." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/nl.json b/homeassistant/components/lock/.translations/nl.json index 099b7308334615..61370236a97ac9 100644 --- a/homeassistant/components/lock/.translations/nl.json +++ b/homeassistant/components/lock/.translations/nl.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} is vergrendeld", "is_unlocked": "{entity_name} is ontgrendeld" + }, + "trigger_type": { + "locked": "{entity_name} vergrendeld", + "unlocked": "{entity_name} ontgrendeld" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/hu.json b/homeassistant/components/sensor/.translations/hu.json index 78ea3e5e89befe..a83db67b3e15d3 100644 --- a/homeassistant/components/sensor/.translations/hu.json +++ b/homeassistant/components/sensor/.translations/hu.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} akku szint", - "is_humidity": "{entity_name} p\u00e1ratartalom", - "is_illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s", - "is_power": "{entity_name} teljes\u00edtm\u00e9ny", - "is_pressure": "{entity_name} nyom\u00e1s", - "is_signal_strength": "{entity_name} jeler\u0151ss\u00e9g", - "is_temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet", - "is_timestamp": "{entity_name} id\u0151b\u00e9lyeg", - "is_value": "{entity_name} \u00e9rt\u00e9k" + "is_battery_level": "{entity_name} aktu\u00e1lis akku szintje", + "is_humidity": "{entity_name} aktu\u00e1lis p\u00e1ratartalma", + "is_illuminance": "{entity_name} aktu\u00e1lis megvil\u00e1g\u00edt\u00e1sa", + "is_power": "{entity_name} aktu\u00e1lis teljes\u00edtm\u00e9nye", + "is_pressure": "{entity_name} aktu\u00e1lis nyom\u00e1sa", + "is_signal_strength": "{entity_name} aktu\u00e1lis jeler\u0151ss\u00e9ge", + "is_temperature": "{entity_name} aktu\u00e1lis h\u0151m\u00e9rs\u00e9klete", + "is_timestamp": "{entity_name} aktu\u00e1lis id\u0151b\u00e9lyege", + "is_value": "{entity_name} aktu\u00e1lis \u00e9rt\u00e9ke" }, "trigger_type": { - "battery_level": "{entity_name} akku szint", - "humidity": "{entity_name} p\u00e1ratartalom", - "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s", - "power": "{entity_name} teljes\u00edtm\u00e9ny", - "pressure": "{entity_name} nyom\u00e1s", - "signal_strength": "{entity_name} jeler\u0151ss\u00e9g", - "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet", - "timestamp": "{entity_name} id\u0151b\u00e9lyeg", - "value": "{entity_name} \u00e9rt\u00e9k" + "battery_level": "{entity_name} akku szintje v\u00e1ltozik", + "humidity": "{entity_name} p\u00e1ratartalma v\u00e1ltozik", + "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1sa v\u00e1ltozik", + "power": "{entity_name} teljes\u00edtm\u00e9nye v\u00e1ltozik", + "pressure": "{entity_name} nyom\u00e1sa v\u00e1ltozik", + "signal_strength": "{entity_name} jeler\u0151ss\u00e9ge v\u00e1ltozik", + "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klete v\u00e1ltozik", + "timestamp": "{entity_name} id\u0151b\u00e9lyege v\u00e1ltozik", + "value": "{entity_name} \u00e9rt\u00e9ke v\u00e1ltozik" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/nl.json b/homeassistant/components/vacuum/.translations/nl.json new file mode 100644 index 00000000000000..3032fc22508f34 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/nl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condtion_type": { + "is_cleaning": "{entity_name} is aan het schoonmaken" + }, + "trigger_type": { + "cleaning": "{entity_name} begon met schoonmaken" + } + } +} \ No newline at end of file From 94f8e634b2264e7867c7910095eabeaa9aaf4bf2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 24 Nov 2019 14:35:30 +0100 Subject: [PATCH 1699/3953] Ensure wheel package is present when running Pylint (#29012) --- azure-pipelines-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index ca717ca6546004..cbad0c9af08a1c 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -162,7 +162,7 @@ stages: python -m venv venv . venv/bin/activate - pip install -U pip setuptools + pip install -U pip setuptools wheel pip install -r requirements_all.txt -c homeassistant/package_constraints.txt pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | From ce8e0e54890bd2197556cc3c57cedc32b1e9b785 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 24 Nov 2019 20:31:15 +0100 Subject: [PATCH 1700/3953] Move imports to top for volkszaehler (#29025) --- homeassistant/components/volkszaehler/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index 6a0815a9b140e3..a2da620c1a56de 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from volkszaehler import Volkszaehler +from volkszaehler.exceptions import VolkszaehlerApiConnectionError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -51,7 +53,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Volkszaehler sensors.""" - from volkszaehler import Volkszaehler host = config[CONF_HOST] name = config[CONF_NAME] @@ -130,7 +131,6 @@ def __init__(self, api): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from the Volkszaehler REST API.""" - from volkszaehler.exceptions import VolkszaehlerApiConnectionError try: await self.api.get_data() From 1495df374b77d97c38aa9af94dc627506e3b7f5a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 24 Nov 2019 20:53:26 +0100 Subject: [PATCH 1701/3953] Made spotify playlist name validation less strict (#28684) --- homeassistant/components/spotify/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 236c8b8db89608..ec21a5d7822e60 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -307,7 +307,7 @@ def play_media(self, media_type, media_id, **kwargs): def play_playlist(self, media_id, random_song): """Play random music in a playlist.""" - if not media_id.startswith("spotify:playlist:"): + if not media_id.startswith("spotify:"): _LOGGER.error("media id must be spotify playlist uri") return kwargs = {"context_uri": media_id} From 647595fd677201fe22b3f8da5f0a67aaaf2c16c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 24 Nov 2019 22:47:31 +0100 Subject: [PATCH 1702/3953] Move imports to the top for Tado. (#29016) --- homeassistant/components/tado/__init__.py | 9 ++++----- homeassistant/components/tado/climate.py | 6 +++--- homeassistant/components/tado/device_tracker.py | 12 ++++++------ 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index ad66a594a86bfe..b13ef6856ab867 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -1,13 +1,14 @@ """Support for the (unofficial) Tado API.""" +from datetime import timedelta import logging import urllib -from datetime import timedelta +from PyTado.interface import Tado import voluptuous as vol -from homeassistant.helpers.discovery import load_platform +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers.discovery import load_platform from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -37,8 +38,6 @@ def setup(hass, config): username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] - from PyTado.interface import Tado - try: tado = Tado(username, password) tado.setDebugging(True) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 1108b32af4e07f..da19080e8f7082 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -1,20 +1,20 @@ """Support for Tado to create a climate device for each zone.""" import logging -from typing import Optional, List +from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - CURRENT_HVAC_OFF, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, FAN_HIGH, FAN_LOW, FAN_MIDDLE, FAN_OFF, HVAC_MODE_AUTO, - HVAC_MODE_HEAT, HVAC_MODE_COOL, + HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index 797692bf32c1e0..c63f5061dfa9c3 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -1,22 +1,22 @@ """Support for Tado Smart device trackers.""" -import logging -from datetime import timedelta +import asyncio from collections import namedtuple +from datetime import timedelta +import logging -import asyncio import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from homeassistant.util import Throttle from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_create_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) From 48c289fad3d8726755fd5bb5f5325b85a78465dc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 25 Nov 2019 04:57:18 +0100 Subject: [PATCH 1703/3953] Alexa gracefully handle climate devices without presets (#29010) --- homeassistant/components/alexa/capabilities.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 7d74bb3f8cd458..49b5c5141b6837 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -752,10 +752,11 @@ def configuration(self): supported_modes.append(thermostat_mode) preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES) - for mode in preset_modes: - thermostat_mode = API_THERMOSTAT_PRESETS.get(mode) - if thermostat_mode: - supported_modes.append(thermostat_mode) + if preset_modes: + for mode in preset_modes: + thermostat_mode = API_THERMOSTAT_PRESETS.get(mode) + if thermostat_mode: + supported_modes.append(thermostat_mode) # Return False for supportsScheduling until supported with event listener in handler. configuration = {"supportsScheduling": False} From c38240673535ba03fec4f404926015a074c689e5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 23 Nov 2019 09:16:21 -0600 Subject: [PATCH 1704/3953] Fix Plex setup race condition v2 (#28943) * Connect websocket when platforms ready, not when HA is ready * Use callbacks from platform setup tasks instead * Convert setup to async * Apply suggestions from code review Co-Authored-By: Martin Hjelmare --- homeassistant/components/plex/__init__.py | 34 +++++++++++++---------- homeassistant/components/plex/const.py | 3 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 386be56340a5ab..971032302888ed 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,5 +1,6 @@ """Support to embed Plex.""" import asyncio +import functools import logging import plexapi.exceptions @@ -16,7 +17,6 @@ CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv @@ -37,6 +37,7 @@ DISPATCHERS, DOMAIN as PLEX_DOMAIN, PLATFORMS, + PLATFORMS_COMPLETED, PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, PLEX_UPDATE_PLATFORMS_SIGNAL, @@ -72,18 +73,21 @@ _LOGGER = logging.getLogger(__package__) -def setup(hass, config): +async def async_setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}}) + hass.data.setdefault( + PLEX_DOMAIN, + {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}, PLATFORMS_COMPLETED: {}}, + ) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: - _setup_plex(hass, plex_config) + _async_setup_plex(hass, plex_config) return True -def _setup_plex(hass, config): +def _async_setup_plex(hass, config): """Pass configuration to a config flow.""" server_config = dict(config) if MP_DOMAIN in server_config: @@ -141,11 +145,7 @@ async def async_setup_entry(hass, entry): ) server_id = plex_server.machine_identifier hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server - - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] = set() entry.add_update_listener(async_options_updated) @@ -167,19 +167,25 @@ def update_plex(): ) hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket - async def async_start_websocket_session(_): - await websocket.listen() + def start_websocket_session(platform, _): + hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id].add(platform) + if hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] == PLATFORMS: + hass.loop.create_task(websocket.listen()) def close_websocket_session(_): websocket.close() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start_websocket_session) - unsub = hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, close_websocket_session ) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + for platform in PLATFORMS: + task = hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + task.add_done_callback(functools.partial(start_websocket_session, platform)) + return True diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index d3c79e60bc48f5..ad62bade1fd0f9 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -9,7 +9,8 @@ DEFAULT_VERIFY_SSL = True DISPATCHERS = "dispatchers" -PLATFORMS = ["media_player", "sensor"] +PLATFORMS = frozenset(["media_player", "sensor"]) +PLATFORMS_COMPLETED = "platforms_completed" SERVERS = "servers" WEBSOCKETS = "websockets" From 2e85e3662f06c22324bc28d35bae9b2f25a751ef Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 22 Nov 2019 15:32:05 +0100 Subject: [PATCH 1705/3953] Fix ikea lights on deconz (#28949) * Fix ikea lights on deconz * check for None * Use cleaner code style --- homeassistant/components/deconz/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index bf4b05089a84c0..eda2041b923116 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -152,6 +152,8 @@ async def async_turn_on(self, **kwargs): if ATTR_TRANSITION in kwargs: data["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) + elif "IKEA" in (self._device.manufacturer or ""): + data["transitiontime"] = 0 if ATTR_FLASH in kwargs: if kwargs[ATTR_FLASH] == FLASH_SHORT: From 481f71107bff1dd94897f34e41a34c8e9f73691a Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 23 Nov 2019 11:00:26 +0100 Subject: [PATCH 1706/3953] Fix manual config (#28956) --- homeassistant/components/netatmo/sensor.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index fa7aedae739ba5..1ae076c6560775 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -547,13 +547,18 @@ def __init__(self, auth, data_class, station): self.data = {} self.station_data = self.data_class(self.auth) self.station = station + self.station_id = None + if station: + station_data = self.station_data.stationByName(self.station) + if station_data: + self.station_id = station_data.get("_id") self._next_update = time() self._update_in_progress = threading.Lock() def get_module_infos(self): """Return all modules available on the API as a dict.""" - if self.station is not None: - return self.station_data.getModules(station=self.station) + if self.station_id is not None: + return self.station_data.getModules(station_id=self.station_id) return self.station_data.getModules() def update(self): @@ -579,7 +584,7 @@ def update(self): return data = self.station_data.lastData( - station=self.station, exclude=3600, byId=True + station=self.station_id, exclude=3600, byId=True ) if not data: self._next_update = time() + NETATMO_UPDATE_INTERVAL From a56f7f5d7556cfb16d9c6b4f83ccd7717244785e Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Sat, 23 Nov 2019 03:29:54 -0800 Subject: [PATCH 1707/3953] Fix temp not being reported properly (#28973) --- homeassistant/components/abode/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index d84bfe52441d8f..573df6d49b4faf 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -72,19 +72,19 @@ def unique_id(self): @property def state(self): """Return the state of the sensor.""" - if self._sensor_type == "temp": + if self._sensor_type == CONST.TEMP_STATUS_KEY: return self._device.temp - if self._sensor_type == "humidity": + if self._sensor_type == CONST.HUMI_STATUS_KEY: return self._device.humidity - if self._sensor_type == "lux": + if self._sensor_type == CONST.LUX_STATUS_KEY: return self._device.lux @property def unit_of_measurement(self): """Return the units of measurement.""" - if self._sensor_type == "temp": + if self._sensor_type == CONST.TEMP_STATUS_KEY: return self._device.temp_unit - if self._sensor_type == "humidity": + if self._sensor_type == CONST.HUMI_STATUS_KEY: return self._device.humidity_unit - if self._sensor_type == "lux": + if self._sensor_type == CONST.LUX_STATUS_KEY: return self._device.lux_unit From 2af1ba749260d96fec50e9e1e7c6f290bae0c81c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 23 Nov 2019 22:12:56 +0100 Subject: [PATCH 1708/3953] Updated frontend to 20191119.6 (#28996) --- 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 41728bc1664041..b196239dfb1fb5 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.5" + "home-assistant-frontend==20191119.6" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1c4aaa1191f426..07ebfdec4f9a6b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.29 -home-assistant-frontend==20191119.5 +home-assistant-frontend==20191119.6 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 651c089a4fd80d..e8307161c09339 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.5 +home-assistant-frontend==20191119.6 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8fc004f3363399..2aa46e28f7a1dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.5 +home-assistant-frontend==20191119.6 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From b10d42e33023a24dc51e4b610f5f4f55b1cacdd7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 25 Nov 2019 04:57:18 +0100 Subject: [PATCH 1709/3953] Alexa gracefully handle climate devices without presets (#29010) --- homeassistant/components/alexa/capabilities.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 7d74bb3f8cd458..49b5c5141b6837 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -752,10 +752,11 @@ def configuration(self): supported_modes.append(thermostat_mode) preset_modes = self.entity.attributes.get(climate.ATTR_PRESET_MODES) - for mode in preset_modes: - thermostat_mode = API_THERMOSTAT_PRESETS.get(mode) - if thermostat_mode: - supported_modes.append(thermostat_mode) + if preset_modes: + for mode in preset_modes: + thermostat_mode = API_THERMOSTAT_PRESETS.get(mode) + if thermostat_mode: + supported_modes.append(thermostat_mode) # Return False for supportsScheduling until supported with event listener in handler. configuration = {"supportsScheduling": False} From 8a467bbf5c4bee5938b2b2a6b4a2c135440aafd2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 24 Nov 2019 19:57:50 -0800 Subject: [PATCH 1710/3953] Bumped version to 0.102.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3520260481b94c..8b62d011fda1b7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 8fade79a0a646c527727a4443af6181e08977c8d Mon Sep 17 00:00:00 2001 From: Josh Anderson Date: Mon, 25 Nov 2019 08:13:59 +0000 Subject: [PATCH 1711/3953] Add an early start sensor for heating zones (#28732) --- homeassistant/components/tado/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 7b4bd643f3d123..346b27bec26be5 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -20,6 +20,7 @@ "heating", "tado mode", "overlay", + "early start", ] CLIMATE_COOL_SENSOR_TYPES = [ @@ -252,3 +253,9 @@ def update(self): else: self._state = False self._state_attributes = {} + + elif self.zone_variable == "early start": + if "preparation" in data and data["preparation"] is not None: + self._state = True + else: + self._state = False From 6307f383a74322641f40d8c533b35f49904a3618 Mon Sep 17 00:00:00 2001 From: Philipp Schmitt Date: Mon, 25 Nov 2019 09:40:32 +0100 Subject: [PATCH 1712/3953] Fix #27028 (#29042) --- homeassistant/components/roomba/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 700827c1a65730..92ed16406db6b0 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,7 +3,7 @@ "name": "Roomba", "documentation": "https://www.home-assistant.io/integrations/roomba", "requirements": [ - "roombapy==1.4.1" + "roombapy==1.4.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c48f932965a457..7615cf4b569461 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1740,7 +1740,7 @@ rocketchat-API==0.6.1 roku==3.1 # homeassistant.components.roomba -roombapy==1.4.1 +roombapy==1.4.2 # homeassistant.components.rova rova==0.1.0 From 5822deda1ddd3246c604e0a41a49c263b1bb15a7 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 25 Nov 2019 09:41:37 +0100 Subject: [PATCH 1713/3953] Move philips_js imports at top-level (#29003) --- .../components/philips_js/media_player.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 579dc253603263..fe6d7edf80401b 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -2,11 +2,14 @@ from datetime import timedelta import logging +from haphilipsjs import PhilipsTV import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -14,8 +17,6 @@ SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - MEDIA_TYPE_CHANNEL, - SUPPORT_PLAY_MEDIA, ) from homeassistant.const import ( CONF_API_VERSION, @@ -70,20 +71,18 @@ def _inverted(data): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Philips TV platform.""" - import haphilipsjs - name = config.get(CONF_NAME) host = config.get(CONF_HOST) api_version = config.get(CONF_API_VERSION) turn_on_action = config.get(CONF_ON_ACTION) - tvapi = haphilipsjs.PhilipsTV(host, api_version) + tvapi = PhilipsTV(host, api_version) on_script = Script(hass, turn_on_action) if turn_on_action else None - add_entities([PhilipsTV(tvapi, name, on_script)]) + add_entities([PhilipsTVMediaPlayer(tvapi, name, on_script)]) -class PhilipsTV(MediaPlayerDevice): +class PhilipsTVMediaPlayer(MediaPlayerDevice): """Representation of a Philips TV exposing the JointSpace API.""" def __init__(self, tv, name, on_script): From fe9084bb3bfed8d34a71a22f0039e7601aaa587a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 25 Nov 2019 09:42:37 +0100 Subject: [PATCH 1714/3953] Move imports to top for watson_tts (#29023) --- homeassistant/components/watson_tts/tts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 40ebd768a3106c..520151cf20ebff 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -1,6 +1,8 @@ """Support for IBM Watson TTS integration.""" import logging +from ibm_watson import TextToSpeechV1 +from ibm_cloud_sdk_core.authenticators import IAMAuthenticator import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider @@ -92,8 +94,6 @@ def get_engine(hass, config, discovery_info=None): """Set up IBM Watson TTS component.""" - from ibm_watson import TextToSpeechV1 - from ibm_cloud_sdk_core.authenticators import IAMAuthenticator authenticator = IAMAuthenticator(config[CONF_APIKEY]) service = TextToSpeechV1(authenticator) From ee038999fc0a88f6da98e22aee0bcde3d5454c35 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 25 Nov 2019 09:43:29 +0100 Subject: [PATCH 1715/3953] Move imports to top for todoist (#29022) --- homeassistant/components/todoist/calendar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 1179fd9086836f..2e8b43c749f8ba 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import logging +from todoist.api import TodoistAPI import voluptuous as vol from homeassistant.components.calendar import ( @@ -143,8 +144,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): project_id_lookup = {} label_id_lookup = {} - from todoist.api import TodoistAPI - api = TodoistAPI(token) api.sync() From cc3559d54cd5a7d1db491ad878b766fd848fcb18 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 25 Nov 2019 09:43:59 +0100 Subject: [PATCH 1716/3953] Move imports to top for geizhals (#29021) --- homeassistant/components/geizhals/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/geizhals/sensor.py b/homeassistant/components/geizhals/sensor.py index 28fe10ec5f588c..f04e943964c761 100644 --- a/homeassistant/components/geizhals/sensor.py +++ b/homeassistant/components/geizhals/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +from geizhals import Device, Geizhals import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -45,7 +46,6 @@ class Geizwatch(Entity): def __init__(self, name, description, product_id, domain): """Initialize the sensor.""" - from geizhals import Device, Geizhals # internal self._name = name From 5b64052d6936e592475d81603ef09c9d1f785889 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 25 Nov 2019 09:50:10 +0100 Subject: [PATCH 1717/3953] Upgrade discord.py to 1.2.5 (#28998) --- homeassistant/components/discord/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index d00d26d2b5eda6..b5bf7eab6ccdf5 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -3,7 +3,7 @@ "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", "requirements": [ - "discord.py==1.2.4" + "discord.py==1.2.5" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 7615cf4b569461..9c195ba2a85e2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -421,7 +421,7 @@ directpy==0.5 discogs_client==2.2.2 # homeassistant.components.discord -discord.py==1.2.4 +discord.py==1.2.5 # homeassistant.components.updater distro==1.4.0 From 430f7bdfd453b7f50486bc223a58a713ee13f923 Mon Sep 17 00:00:00 2001 From: escoand Date: Mon, 25 Nov 2019 10:45:50 +0100 Subject: [PATCH 1718/3953] Catch samsungtv websocket exceptions (#28849) * catch websocket exceptions * fix syntax * use exceptions from base class * add comments * add test --- homeassistant/components/samsungtv/media_player.py | 14 +++++++++----- tests/components/samsungtv/test_media_player.py | 12 ++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 1262c3d7d36e57..2488d5ab9132e2 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -6,6 +6,7 @@ from samsungctl import exceptions as samsung_exceptions, Remote as SamsungRemote import voluptuous as vol import wakeonlan +from websocket import WebSocketException from homeassistant.components.media_player import ( MediaPlayerDevice, @@ -209,23 +210,26 @@ def send_key(self, key): try: self.get_remote().control(key) break - except (samsung_exceptions.ConnectionClosed, BrokenPipeError): + except ( + samsung_exceptions.ConnectionClosed, + BrokenPipeError, + WebSocketException, + ): # BrokenPipe can occur when the commands is sent to fast + # WebSocketException can occur when timed out self._remote = None self._state = STATE_ON except AttributeError: # Auto-detect could not find working config yet pass - except ( - samsung_exceptions.UnhandledResponse, - samsung_exceptions.AccessDenied, - ): + except (samsung_exceptions.UnhandledResponse, samsung_exceptions.AccessDenied): # We got a response so it's on. self._state = STATE_ON self._remote = None LOGGER.debug("Failed sending command %s", key, exc_info=True) return except OSError: + # Different reasons, e.g. hostname not resolveable self._state = STATE_OFF self._remote = None if self._power_off_in_progress(): diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 8f6014463f507b..918e30ef4e7a90 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -8,6 +8,7 @@ import pytest from samsungctl import exceptions from tests.common import async_fire_time_changed +from websocket import WebSocketException from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( @@ -387,6 +388,17 @@ async def test_send_key_unhandled_response(hass, remote): assert state.state == STATE_ON +async def test_send_key_websocketexception(hass, remote): + """Testing unhandled response exception.""" + await setup_samsungtv(hass, MOCK_CONFIG) + remote.control = mock.Mock(side_effect=WebSocketException("Boom")) + assert await hass.services.async_call( + DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + async def test_send_key_os_error(hass, remote): """Testing broken pipe Exception.""" await setup_samsungtv(hass, MOCK_CONFIG) From aa00e56e1c98ee9a0a83206fb5b5753651238d12 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 25 Nov 2019 12:15:55 +0100 Subject: [PATCH 1719/3953] Move imports to top for discovery (#29020) --- homeassistant/components/discovery/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 15fcfc15338da2..884f6680d7cd3d 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -10,6 +10,7 @@ from datetime import timedelta import logging +from netdisco.discovery import NetworkDiscovery import voluptuous as vol from homeassistant import config_entries @@ -129,7 +130,6 @@ async def async_setup(hass, config): """Start a discovery service.""" - from netdisco.discovery import NetworkDiscovery logger = logging.getLogger(__name__) netdisco = NetworkDiscovery() From 5690313084bf884028eb3e355ae037894bc92d5a Mon Sep 17 00:00:00 2001 From: Chris Halls Date: Mon, 25 Nov 2019 12:17:08 +0100 Subject: [PATCH 1720/3953] Report device unavailable state through Emulated Hue (#29029) If an entity is in unavailable state in HA, we expose this as unreachable to Hue clients. This helps when troubleshooting Hue issues because you can now receive feedback if there is an issue on the HA side. --- .../components/emulated_hue/hue_api.py | 8 ++++++-- tests/components/emulated_hue/test_hue_api.py | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 5d08af6c5eeb75..51ce3e2e42aab4 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -56,6 +56,7 @@ SERVICE_VOLUME_SET, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.util.network import is_local @@ -578,7 +579,7 @@ def entity_to_json(config, entity, state): HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], HUE_API_STATE_HUE: state[STATE_HUE], HUE_API_STATE_SAT: state[STATE_SATURATION], - "reachable": True, + "reachable": entity.state != STATE_UNAVAILABLE, }, "type": "Dimmable light", "name": config.get_entity_name(entity), @@ -587,7 +588,10 @@ def entity_to_json(config, entity, state): "swversion": "123", } return { - "state": {HUE_API_STATE_ON: state[STATE_ON], "reachable": True}, + "state": { + HUE_API_STATE_ON: state[STATE_ON], + "reachable": entity.state != STATE_UNAVAILABLE, + }, "type": "On/off light", "name": config.get_entity_name(entity), "modelid": "HASS321", diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 02f24f5afba701..9629ae6cf69008 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -232,6 +232,26 @@ def test_light_without_brightness_supported(hass_hue, hue_client): assert light_without_brightness_json["type"] == "On/off light" +@asyncio.coroutine +@pytest.mark.parametrize( + "state,is_reachable", + [ + (const.STATE_UNAVAILABLE, False), + (const.STATE_OK, True), + (const.STATE_UNKNOWN, True), + ], +) +def test_reachable_for_state(hass_hue, hue_client, state, is_reachable): + """Test that an entity is reported as unreachable if in unavailable state.""" + entity_id = "light.ceiling_lights" + + hass_hue.states.async_set(entity_id, state) + + state_json = yield from perform_get_light_state(hue_client, entity_id, 200) + + assert state_json["state"]["reachable"] == is_reachable, state_json + + @asyncio.coroutine def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" From c21c167f4e30d34933de2b197971e770d06a4363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Mon, 25 Nov 2019 14:04:36 +0100 Subject: [PATCH 1721/3953] Use TADO_MODE for temperature overrides in tado climate component. (#29014) * Use TADO_MODE for temperature overrides in tado climate component. * Fix typo in pydoc * Add myself as a code owner * Update CODEOWNERS file --- CODEOWNERS | 1 + homeassistant/components/tado/__init__.py | 2 +- homeassistant/components/tado/climate.py | 20 ++++++++++---------- homeassistant/components/tado/manifest.json | 4 +++- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0aca3b9d6fb9f2..3bf34fc2613f80 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -303,6 +303,7 @@ homeassistant/components/switchmate/* @danielhiversen homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff +homeassistant/components/tado/* @michaelarnauts homeassistant/components/tahoma/* @philklei homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index b13ef6856ab867..1739cbb9254f06 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -116,7 +116,7 @@ def get_capabilities(self, tado_id): return self.tado.getCapabilities(tado_id) def get_me(self): - """Wrap for getMet().""" + """Wrap for getMe().""" return self.tado.getMe() def reset_zone_overlay(self, zone_id): diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index da19080e8f7082..b6a7f3d8bdbfa6 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -48,22 +48,22 @@ HVAC_MAP_TADO_HEAT = { "MANUAL": HVAC_MODE_HEAT, - "TIMER": HVAC_MODE_AUTO, - "TADO_MODE": HVAC_MODE_AUTO, + "TIMER": HVAC_MODE_HEAT, + "TADO_MODE": HVAC_MODE_HEAT, "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, + "TIMER": HVAC_MODE_COOL, + "TADO_MODE": HVAC_MODE_COOL, "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, + "TIMER": HVAC_MODE_HEAT_COOL, + "TADO_MODE": HVAC_MODE_HEAT_COOL, "SMART_SCHEDULE": HVAC_MODE_AUTO, "OFF": HVAC_MODE_OFF, } @@ -325,7 +325,7 @@ def set_temperature(self, **kwargs): if temperature is None: return - self._current_operation = CONST_OVERLAY_MANUAL + self._current_operation = CONST_OVERLAY_TADO_MODE self._overlay_mode = None self._target_temp = temperature self._control_heating() @@ -339,11 +339,11 @@ def set_hvac_mode(self, hvac_mode): elif hvac_mode == HVAC_MODE_AUTO: mode = CONST_MODE_SMART_SCHEDULE elif hvac_mode == HVAC_MODE_HEAT: - mode = CONST_OVERLAY_MANUAL + mode = CONST_OVERLAY_TADO_MODE elif hvac_mode == HVAC_MODE_COOL: - mode = CONST_OVERLAY_MANUAL + mode = CONST_OVERLAY_TADO_MODE elif hvac_mode == HVAC_MODE_HEAT_COOL: - mode = CONST_OVERLAY_MANUAL + mode = CONST_OVERLAY_TADO_MODE self._current_operation = mode self._overlay_mode = None diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 9a884fa010c4a3..4728f1622eda02 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -6,5 +6,7 @@ "python-tado==0.2.9" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@michaelarnauts" + ] } From 3122182ab2015c34472f3d67b618e95d48b9b69a Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 25 Nov 2019 14:05:07 +0100 Subject: [PATCH 1722/3953] Move edimax imports at top-level (#29053) --- homeassistant/components/edimax/switch.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index f1d8f8046efe9a..3d558f6c7708f8 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -1,9 +1,10 @@ """Support for Edimax switches.""" import logging +from pyedimax.smartplug import SmartPlug import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv @@ -25,8 +26,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return Edimax Smart Plugs.""" - from pyedimax.smartplug import SmartPlug - host = config.get(CONF_HOST) auth = (config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) name = config.get(CONF_NAME) From 193769c79155cabef77377ab3ce06dca1abd6183 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 25 Nov 2019 14:05:46 +0100 Subject: [PATCH 1723/3953] Move vizio imports at top-level (#29046) --- homeassistant/components/vizio/media_player.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index f64fd2ca531c67..94601216f23c71 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -2,11 +2,12 @@ from datetime import timedelta import logging -import voluptuous as vol from pyvizio import Vizio +from requests.packages import urllib3 +import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, @@ -110,8 +111,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return if config[CONF_SUPPRESS_WARNING]: - from requests.packages import urllib3 - _LOGGER.warning( "InsecureRequestWarning is disabled " "because of Vizio platform configuration" From 752a4d72214eaf9f85b7f8be67afb50776ecc7f9 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 25 Nov 2019 14:06:39 +0100 Subject: [PATCH 1724/3953] Move dyson imports at top-level (#29047) --- homeassistant/components/dyson/__init__.py | 5 +-- homeassistant/components/dyson/air_quality.py | 9 ++-- homeassistant/components/dyson/climate.py | 8 ++-- homeassistant/components/dyson/fan.py | 45 +++++-------------- homeassistant/components/dyson/sensor.py | 6 ++- homeassistant/components/dyson/vacuum.py | 19 ++------ 6 files changed, 28 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py index 7f247be6bccde0..a5dde58d30ffc2 100644 --- a/homeassistant/components/dyson/__init__.py +++ b/homeassistant/components/dyson/__init__.py @@ -1,11 +1,12 @@ """Support for Dyson Pure Cool Link devices.""" import logging +from libpurecool.dyson import DysonAccount import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -43,8 +44,6 @@ def setup(hass, config): if DYSON_DEVICES not in hass.data: hass.data[DYSON_DEVICES] = [] - from libpurecool.dyson import DysonAccount - dyson_account = DysonAccount( config[DOMAIN].get(CONF_USERNAME), config[DOMAIN].get(CONF_PASSWORD), diff --git a/homeassistant/components/dyson/air_quality.py b/homeassistant/components/dyson/air_quality.py index 0276e47ed611f8..647fb2367074f8 100644 --- a/homeassistant/components/dyson/air_quality.py +++ b/homeassistant/components/dyson/air_quality.py @@ -1,7 +1,11 @@ """Support for Dyson Pure Cool Air Quality Sensors.""" import logging -from homeassistant.components.air_quality import AirQualityEntity, DOMAIN +from libpurecool.dyson_pure_cool import DysonPureCool +from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State + +from homeassistant.components.air_quality import DOMAIN, AirQualityEntity + from . import DYSON_DEVICES ATTRIBUTION = "Dyson purifier air quality sensor" @@ -15,7 +19,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson Sensors.""" - from libpurecool.dyson_pure_cool import DysonPureCool if discovery_info is None: return @@ -47,8 +50,6 @@ async def async_added_to_hass(self): def on_message(self, message): """Handle new messages which are received from the fan.""" - from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State - _LOGGER.debug( "%s: Message received for %s device: %s", DOMAIN, self.name, message ) diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index 90c19e9de88972..df97358d55008d 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -1,20 +1,20 @@ """Support for Dyson Pure Hot+Cool link fan.""" import logging -from libpurecool.const import HeatMode, HeatState, FocusMode, HeatTarget -from libpurecool.dyson_pure_state import DysonPureHotCoolState +from libpurecool.const import FocusMode, HeatMode, HeatState, HeatTarget from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink +from libpurecool.dyson_pure_state import DysonPureHotCoolState from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + FAN_DIFFUSE, + FAN_FOCUS, HVAC_MODE_COOL, HVAC_MODE_HEAT, SUPPORT_FAN_MODE, - FAN_FOCUS, - FAN_DIFFUSE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 341919935d0e44..1fdbed0d20451c 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -5,18 +5,24 @@ """ import logging +from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation +from libpurecool.dyson_pure_cool import DysonPureCool +from libpurecool.dyson_pure_cool_link import DysonPureCoolLink +from libpurecool.dyson_pure_state import DysonPureCoolState +from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, ) from homeassistant.const import ATTR_ENTITY_ID +import homeassistant.helpers.config_validation as cv + from . import DYSON_DEVICES _LOGGER = logging.getLogger(__name__) @@ -88,8 +94,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson fan components.""" - from libpurecool.dyson_pure_cool_link import DysonPureCoolLink - from libpurecool.dyson_pure_cool import DysonPureCool if discovery_info is None: return @@ -197,7 +201,6 @@ async def async_added_to_hass(self): def on_message(self, message): """Call when new messages received from the fan.""" - from libpurecool.dyson_pure_state import DysonPureCoolState if isinstance(message, DysonPureCoolState): _LOGGER.debug("Message received for fan device %s: %s", self.name, message) @@ -215,8 +218,6 @@ def name(self): def set_speed(self, speed: str) -> None: """Set the speed of the fan. Never called ??.""" - from libpurecool.const import FanSpeed, FanMode - _LOGGER.debug("Set fan speed to: %s", speed) if speed == FanSpeed.FAN_SPEED_AUTO.value: @@ -227,8 +228,6 @@ def set_speed(self, speed: str) -> None: def turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the fan.""" - from libpurecool.const import FanSpeed, FanMode - _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) if speed: if speed == FanSpeed.FAN_SPEED_AUTO.value: @@ -244,15 +243,11 @@ def turn_on(self, speed: str = None, **kwargs) -> None: def turn_off(self, **kwargs) -> None: """Turn off the fan.""" - from libpurecool.const import FanMode - _LOGGER.debug("Turn off fan %s", self.name) self._device.set_configuration(fan_mode=FanMode.OFF) def oscillate(self, oscillating: bool) -> None: """Turn on/off oscillating.""" - from libpurecool.const import Oscillation - _LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name) if oscillating: @@ -275,8 +270,6 @@ def is_on(self): @property def speed(self) -> str: """Return the current speed.""" - from libpurecool.const import FanSpeed - if self._device.state: if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: return self._device.state.speed @@ -295,8 +288,6 @@ def night_mode(self): def set_night_mode(self, night_mode: bool) -> None: """Turn fan in night mode.""" - from libpurecool.const import NightMode - _LOGGER.debug("Set %s night mode %s", self.name, night_mode) if night_mode: self._device.set_configuration(night_mode=NightMode.NIGHT_MODE_ON) @@ -310,8 +301,6 @@ def auto_mode(self): def set_auto_mode(self, auto_mode: bool) -> None: """Turn fan in auto mode.""" - from libpurecool.const import FanMode - _LOGGER.debug("Set %s auto mode %s", self.name, auto_mode) if auto_mode: self._device.set_configuration(fan_mode=FanMode.AUTO) @@ -321,8 +310,6 @@ def set_auto_mode(self, auto_mode: bool) -> None: @property def speed_list(self) -> list: """Get the list of available speeds.""" - from libpurecool.const import FanSpeed - supported_speeds = [ FanSpeed.FAN_SPEED_AUTO.value, int(FanSpeed.FAN_SPEED_1.value), @@ -365,8 +352,6 @@ async def async_added_to_hass(self): def on_message(self, message): """Call when new messages received from the fan.""" - from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State - if isinstance(message, DysonPureCoolV2State): _LOGGER.debug("Message received for fan device %s: %s", self.name, message) self.schedule_update_ha_state() @@ -392,8 +377,6 @@ def turn_on(self, speed: str = None, **kwargs) -> None: def set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - from libpurecool.const import FanSpeed - if speed == SPEED_LOW: self._device.set_fan_speed(FanSpeed.FAN_SPEED_4) elif speed == SPEED_MEDIUM: @@ -408,8 +391,6 @@ def turn_off(self, **kwargs): def set_dyson_speed(self, speed: str = None) -> None: """Set the exact speed of the purecool fan.""" - from libpurecool.const import FanSpeed - _LOGGER.debug("Set exact speed for fan %s", self.name) fan_speed = FanSpeed("{0:04d}".format(int(speed))) @@ -487,8 +468,6 @@ def is_on(self): @property def speed(self): """Return the current speed.""" - from libpurecool.const import FanSpeed - speed_map = { FanSpeed.FAN_SPEED_1.value: SPEED_LOW, FanSpeed.FAN_SPEED_2.value: SPEED_LOW, @@ -508,8 +487,6 @@ def speed(self): @property def dyson_speed(self): """Return the current speed.""" - from libpurecool.const import FanSpeed - if self._device.state: if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: return self._device.state.speed @@ -563,8 +540,6 @@ def speed_list(self) -> list: @property def dyson_speed_list(self) -> list: """Get the list of available dyson speeds.""" - from libpurecool.const import FanSpeed - return [ int(FanSpeed.FAN_SPEED_1.value), int(FanSpeed.FAN_SPEED_2.value), diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index 1eb2b79c0735ac..c51f46c77901c2 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -1,8 +1,12 @@ """Support for Dyson Pure Cool Link Sensors.""" import logging +from libpurecool.dyson_pure_cool import DysonPureCool +from libpurecool.dyson_pure_cool_link import DysonPureCoolLink + from homeassistant.const import STATE_OFF, TEMP_CELSIUS from homeassistant.helpers.entity import Entity + from . import DYSON_DEVICES SENSOR_UNITS = { @@ -27,8 +31,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson Sensors.""" - from libpurecool.dyson_pure_cool_link import DysonPureCoolLink - from libpurecool.dyson_pure_cool import DysonPureCool if discovery_info is None: return diff --git a/homeassistant/components/dyson/vacuum.py b/homeassistant/components/dyson/vacuum.py index cef5f0c99611f0..6203b65c9db52a 100644 --- a/homeassistant/components/dyson/vacuum.py +++ b/homeassistant/components/dyson/vacuum.py @@ -1,6 +1,9 @@ """Support for the Dyson 360 eye vacuum cleaner robot.""" import logging +from libpurecool.const import Dyson360EyeMode, PowerMode +from libpurecool.dyson_360_eye import Dyson360Eye + from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_FAN_SPEED, @@ -38,8 +41,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson 360 Eye robot vacuum platform.""" - from libpurecool.dyson_360_eye import Dyson360Eye - _LOGGER.debug("Creating new Dyson 360 Eye robot vacuum") if DYSON_360_EYE_DEVICES not in hass.data: hass.data[DYSON_360_EYE_DEVICES] = [] @@ -86,8 +87,6 @@ def name(self): @property def status(self): """Return the status of the vacuum cleaner.""" - from libpurecool.const import Dyson360EyeMode - dyson_labels = { Dyson360EyeMode.INACTIVE_CHARGING: "Stopped - Charging", Dyson360EyeMode.INACTIVE_CHARGED: "Stopped - Charged", @@ -110,8 +109,6 @@ def battery_level(self): @property def fan_speed(self): """Return the fan speed of the vacuum cleaner.""" - from libpurecool.const import PowerMode - speed_labels = {PowerMode.MAX: "Max", PowerMode.QUIET: "Quiet"} return speed_labels[self._device.state.power_mode] @@ -128,8 +125,6 @@ def device_state_attributes(self): @property def is_on(self) -> bool: """Return True if entity is on.""" - from libpurecool.const import Dyson360EyeMode - return self._device.state.state in [ Dyson360EyeMode.FULL_CLEAN_INITIATED, Dyson360EyeMode.FULL_CLEAN_ABORTED, @@ -149,8 +144,6 @@ def supported_features(self): @property def battery_icon(self): """Return the battery icon for the vacuum cleaner.""" - from libpurecool.const import Dyson360EyeMode - charging = self._device.state.state in [Dyson360EyeMode.INACTIVE_CHARGING] return icon_for_battery_level( battery_level=self.battery_level, charging=charging @@ -158,8 +151,6 @@ def battery_icon(self): def turn_on(self, **kwargs): """Turn the vacuum on.""" - from libpurecool.const import Dyson360EyeMode - _LOGGER.debug("Turn on device %s", self.name) if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: self._device.resume() @@ -178,16 +169,12 @@ def stop(self, **kwargs): def set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" - from libpurecool.const import PowerMode - _LOGGER.debug("Set fan speed %s on device %s", fan_speed, self.name) power_modes = {"Quiet": PowerMode.QUIET, "Max": PowerMode.MAX} self._device.set_power_mode(power_modes[fan_speed]) def start_pause(self, **kwargs): """Start, pause or resume the cleaning task.""" - from libpurecool.const import Dyson360EyeMode - if self._device.state.state in [Dyson360EyeMode.FULL_CLEAN_PAUSED]: _LOGGER.debug("Resume device %s", self.name) self._device.resume() From 63f66785ad90eea350c197e5b64c3f4b7bce17dc Mon Sep 17 00:00:00 2001 From: kuchel77 <52343790+kuchel77@users.noreply.github.com> Date: Tue, 26 Nov 2019 00:08:04 +1100 Subject: [PATCH 1725/3953] Added error checking (#29041) Add in error checking in case disks or volumes are empty. --- .../components/synologydsm/sensor.py | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/synologydsm/sensor.py b/homeassistant/components/synologydsm/sensor.py index e19f6ada809706..df9c4247005867 100644 --- a/homeassistant/components/synologydsm/sensor.py +++ b/homeassistant/components/synologydsm/sensor.py @@ -119,24 +119,26 @@ def run_setup(event): ] # Handle all volumes - for volume in config.get(CONF_VOLUMES, api.storage.volumes): - sensors += [ - SynoNasStorageSensor( - api, name, variable, _STORAGE_VOL_MON_COND[variable], volume - ) - for variable in monitored_conditions - if variable in _STORAGE_VOL_MON_COND - ] + if api.storage.volumes is not None: + for volume in config.get(CONF_VOLUMES, api.storage.volumes): + sensors += [ + SynoNasStorageSensor( + api, name, variable, _STORAGE_VOL_MON_COND[variable], volume + ) + for variable in monitored_conditions + if variable in _STORAGE_VOL_MON_COND + ] # Handle all disks - for disk in config.get(CONF_DISKS, api.storage.disks): - sensors += [ - SynoNasStorageSensor( - api, name, variable, _STORAGE_DSK_MON_COND[variable], disk - ) - for variable in monitored_conditions - if variable in _STORAGE_DSK_MON_COND - ] + if api.storage.disks is not None: + for disk in config.get(CONF_DISKS, api.storage.disks): + sensors += [ + SynoNasStorageSensor( + api, name, variable, _STORAGE_DSK_MON_COND[variable], disk + ) + for variable in monitored_conditions + if variable in _STORAGE_DSK_MON_COND + ] add_entities(sensors, True) From cb5d00a07bd4157cff08bb6047eae6c9709c2379 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 25 Nov 2019 14:08:28 +0100 Subject: [PATCH 1726/3953] Move ebox imports at top-level (#29048) --- homeassistant/components/ebox/sensor.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index 95c5513ecaf94e..55504e8edf7e5d 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -6,23 +6,24 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.ebox/ """ -import logging from datetime import timedelta +import logging +from pyebox import EboxClient +from pyebox.client import PyEboxError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_NAME, CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, ) +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from homeassistant.exceptions import PlatformNotReady - _LOGGER = logging.getLogger(__name__) @@ -75,8 +76,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = config.get(CONF_NAME) - from pyebox.client import PyEboxError - try: await ebox_data.async_update() except PyEboxError as exp: @@ -135,16 +134,12 @@ class EBoxData: def __init__(self, username, password, httpsession): """Initialize the data object.""" - from pyebox import EboxClient - self.client = EboxClient(username, password, REQUESTS_TIMEOUT, httpsession) self.data = {} @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from Ebox.""" - from pyebox.client import PyEboxError - try: await self.client.fetch_data() except PyEboxError as exp: From 71bafba9ec5656d413746341081db00aa2b24e2d Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 25 Nov 2019 14:08:52 +0100 Subject: [PATCH 1727/3953] Move ecoal_boiler imports at top-level (#29049) --- homeassistant/components/ecoal_boiler/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecoal_boiler/__init__.py b/homeassistant/components/ecoal_boiler/__init__.py index 40769c9990aed7..ed8e315bfedcaa 100644 --- a/homeassistant/components/ecoal_boiler/__init__.py +++ b/homeassistant/components/ecoal_boiler/__init__.py @@ -1,15 +1,16 @@ """Support to control ecoal/esterownik.pl coal/wood boiler controller.""" import logging +from ecoaliface.simple import ECoalController import voluptuous as vol from homeassistant.const import ( CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, CONF_MONITORED_CONDITIONS, + CONF_PASSWORD, CONF_SENSORS, CONF_SWITCHES, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -80,7 +81,6 @@ def setup(hass, hass_config): """Set up global ECoalController instance same for sensors and switches.""" - from ecoaliface.simple import ECoalController conf = hass_config[DOMAIN] host = conf[CONF_HOST] From ccb05764b6346184ae30de2c8743ee142a3d732a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 25 Nov 2019 14:11:23 +0100 Subject: [PATCH 1728/3953] Move imports to top for ffmpeg_motion and ffmpeg_noise (#29026) * Move imports to top for ffmpeg_motion and ffmpeg_noise * Fixed tests --- homeassistant/components/ffmpeg_motion/binary_sensor.py | 6 ++++-- homeassistant/components/ffmpeg_noise/binary_sensor.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index 235a9e4b009fcc..54f3981f48a00e 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -1,6 +1,7 @@ """Provides a binary sensor which is a collection of ffmpeg tools.""" import logging +import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol from homeassistant.core import callback @@ -87,10 +88,11 @@ class FFmpegMotion(FFmpegBinarySensor): def __init__(self, hass, manager, config): """Initialize FFmpeg motion binary sensor.""" - from haffmpeg.sensor import SensorMotion super().__init__(config) - self.ffmpeg = SensorMotion(manager.binary, hass.loop, self._async_callback) + self.ffmpeg = ffmpeg_sensor.SensorMotion( + manager.binary, hass.loop, self._async_callback + ) async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index 00e5dbb682f054..7c5f8656410f7a 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -1,6 +1,7 @@ """Provides a binary sensor which is a collection of ffmpeg tools.""" import logging +import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -54,10 +55,11 @@ class FFmpegNoise(FFmpegBinarySensor): def __init__(self, hass, manager, config): """Initialize FFmpeg noise binary sensor.""" - from haffmpeg.sensor import SensorNoise super().__init__(config) - self.ffmpeg = SensorNoise(manager.binary, hass.loop, self._async_callback) + self.ffmpeg = ffmpeg_sensor.SensorNoise( + manager.binary, hass.loop, self._async_callback + ) async def _async_start_ffmpeg(self, entity_ids): """Start a FFmpeg instance. From 5edc45e9af46c2a77ebfb23564e1ffcafe2db643 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 25 Nov 2019 14:12:01 +0100 Subject: [PATCH 1729/3953] Move fibaro imports at top-level (#28995) --- homeassistant/components/fibaro/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index f500b38664307c..d44819e758b5db 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -1,7 +1,9 @@ """Support for the Fibaro devices.""" -import logging from collections import defaultdict +import logging from typing import Optional + +from fiblary3.client.v4.client import Client as FibaroClient, StateHandler import voluptuous as vol from homeassistant.const import ( @@ -109,7 +111,6 @@ class FibaroController: def __init__(self, config): """Initialize the Fibaro controller.""" - from fiblary3.client.v4.client import Client as FibaroClient self._client = FibaroClient( config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] @@ -148,8 +149,6 @@ def connect(self): def enable_state_handler(self): """Start StateHandler thread for monitoring updates.""" - from fiblary3.client.v4.client import StateHandler - self._state_handler = StateHandler(self._client, self._on_state_change) def disable_state_handler(self): From b927f40f00e4348184c66ee29c711d0ccc348798 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 25 Nov 2019 14:13:19 +0100 Subject: [PATCH 1730/3953] Add climate hvac_action for ESPHome (#28993) * Add climate action * Bump aioesphomeapi --- homeassistant/components/esphome/climate.py | 67 +++++++++++++------ .../components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 50 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 49d284b6de2eae..14f684f1e32974 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -3,10 +3,11 @@ from typing import List, Optional from aioesphomeapi import ( + ClimateAction, + ClimateFanMode, ClimateInfo, ClimateMode, ClimateState, - ClimateFanMode, ClimateSwingMode, ) @@ -15,32 +16,38 @@ ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - HVAC_MODE_HEAT_COOL, + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_OFF, + CURRENT_HVAC_IDLE, + FAN_AUTO, + FAN_DIFFUSE, + FAN_FOCUS, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_MIDDLE, + FAN_OFF, + FAN_ON, HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_FAN_ONLY, HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE_RANGE, - SUPPORT_FAN_MODE, - SUPPORT_SWING_MODE, PRESET_AWAY, PRESET_HOME, - FAN_ON, - FAN_OFF, - FAN_AUTO, - FAN_LOW, - FAN_MEDIUM, - FAN_HIGH, - FAN_MIDDLE, - FAN_FOCUS, - FAN_DIFFUSE, - SWING_OFF, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, SWING_BOTH, - SWING_VERTICAL, SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -85,6 +92,18 @@ def _climate_modes(): } +@esphome_map_enum +def _climate_actions(): + return { + ClimateAction.OFF: CURRENT_HVAC_OFF, + ClimateAction.COOLING: CURRENT_HVAC_COOL, + ClimateAction.HEATING: CURRENT_HVAC_HEAT, + ClimateAction.IDLE: CURRENT_HVAC_IDLE, + ClimateAction.DRYING: CURRENT_HVAC_DRY, + ClimateAction.FAN: CURRENT_HVAC_FAN, + } + + @esphome_map_enum def _fan_modes(): return { @@ -205,6 +224,14 @@ def hvac_mode(self) -> Optional[str]: """Return current operation ie. heat, cool, idle.""" return _climate_modes.from_esphome(self._state.mode) + @esphome_state_property + def hvac_action(self) -> Optional[str]: + """Return current action.""" + # HA has no support feature field for hvac_action + if not self._static_info.supports_action: + return None + return _climate_actions.from_esphome(self._state.action) + @esphome_state_property def fan_mode(self): """Return current fan setting.""" diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 425ecc6ce321ea..549a063528f3a7 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.6.0" + "aioesphomeapi==2.6.1" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 9c195ba2a85e2a..82160c08e42582 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -145,7 +145,7 @@ aiobotocore==0.10.4 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.6.0 +aioesphomeapi==2.6.1 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51e193c8e7f452..0a3c5e576a450f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -53,7 +53,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.4 # homeassistant.components.esphome -aioesphomeapi==2.6.0 +aioesphomeapi==2.6.1 # homeassistant.components.emulated_hue # homeassistant.components.http From 1043712c542ff18a78f2a10fe09cf276736c64bc Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 25 Nov 2019 14:17:14 +0100 Subject: [PATCH 1731/3953] Code maintenance for HomematicIP Cloud (#28980) * Add more type hints to Homematic IP Cloud * Rename device to entity in async_setup_entry --- .../components/homematicip_cloud/__init__.py | 20 +++---- .../homematicip_cloud/alarm_control_panel.py | 20 ++++--- .../homematicip_cloud/binary_sensor.py | 45 ++++++++-------- .../components/homematicip_cloud/climate.py | 53 ++++++++++--------- .../homematicip_cloud/config_flow.py | 12 ++--- .../components/homematicip_cloud/cover.py | 30 ++++++----- .../components/homematicip_cloud/device.py | 12 ++--- .../components/homematicip_cloud/hap.py | 26 ++++----- .../components/homematicip_cloud/light.py | 39 +++++++------- .../components/homematicip_cloud/sensor.py | 41 +++++++------- .../components/homematicip_cloud/switch.py | 39 +++++++------- .../components/homematicip_cloud/weather.py | 16 +++--- 12 files changed, 190 insertions(+), 163 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 8cd41e0b980a7b..62f3f9ec5d4d1a 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,8 +1,10 @@ """Support for HomematicIP Cloud devices.""" import logging from pathlib import Path +from typing import Optional from homematicip.aio.group import AsyncHeatingGroup +from homematicip.aio.home import AsyncHome from homematicip.base.helpers import handle_config import voluptuous as vol @@ -135,7 +137,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) ) - async def _async_activate_eco_mode_with_duration(service): + async def _async_activate_eco_mode_with_duration(service) -> None: """Service to activate eco mode with duration.""" duration = service.data[ATTR_DURATION] hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -155,7 +157,7 @@ async def _async_activate_eco_mode_with_duration(service): schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION, ) - async def _async_activate_eco_mode_with_period(service): + async def _async_activate_eco_mode_with_period(service) -> None: """Service to activate eco mode with period.""" endtime = service.data[ATTR_ENDTIME] hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -175,7 +177,7 @@ async def _async_activate_eco_mode_with_period(service): schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD, ) - async def _async_activate_vacation(service): + async def _async_activate_vacation(service) -> None: """Service to activate vacation.""" endtime = service.data[ATTR_ENDTIME] temperature = service.data[ATTR_TEMPERATURE] @@ -196,7 +198,7 @@ async def _async_activate_vacation(service): schema=SCHEMA_ACTIVATE_VACATION, ) - async def _async_deactivate_eco_mode(service): + async def _async_deactivate_eco_mode(service) -> None: """Service to deactivate eco mode.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -215,7 +217,7 @@ async def _async_deactivate_eco_mode(service): schema=SCHEMA_DEACTIVATE_ECO_MODE, ) - async def _async_deactivate_vacation(service): + async def _async_deactivate_vacation(service) -> None: """Service to deactivate vacation.""" hapid = service.data.get(ATTR_ACCESSPOINT_ID) @@ -234,7 +236,7 @@ async def _async_deactivate_vacation(service): schema=SCHEMA_DEACTIVATE_VACATION, ) - async def _set_active_climate_profile(service): + async def _set_active_climate_profile(service) -> None: """Service to set the active climate profile.""" entity_id_list = service.data[ATTR_ENTITY_ID] climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 @@ -257,7 +259,7 @@ async def _set_active_climate_profile(service): schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, ) - async def _async_dump_hap_config(service): + async def _async_dump_hap_config(service) -> None: """Service to dump the configuration of a Homematic IP Access Point.""" config_path = ( service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir @@ -287,7 +289,7 @@ async def _async_dump_hap_config(service): schema=SCHEMA_DUMP_HAP_CONFIG, ) - def _get_home(hapid: str): + def _get_home(hapid: str) -> Optional[AsyncHome]: """Return a HmIP home.""" hap = hass.data[DOMAIN].get(hapid) if hap: @@ -324,7 +326,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Unload a config entry.""" hap = hass.data[DOMAIN].pop(entry.data[HMIPC_HAPID]) return await hap.async_reset() diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 8ebb35b12c156b..f362133034f84a 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud alarm control panel.""" import logging +from typing import Any, Dict from homematicip.functionalHomes import SecurityAndAlarmHome @@ -21,7 +22,9 @@ CONST_ALARM_CONTROL_PANEL_NAME = "HmIP Alarm Control Panel" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud alarm control devices.""" pass @@ -40,9 +43,10 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): def __init__(self, hap: HomematicipHAP) -> None: """Initialize the alarm control panel.""" self._home = hap.home + _LOGGER.info("Setting up %s", self.name) @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" return { "identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")}, @@ -70,26 +74,26 @@ def state(self) -> str: return STATE_ALARM_DISARMED @property - def _security_and_alarm(self): + def _security_and_alarm(self) -> SecurityAndAlarmHome: return self._home.get_functionalHome(SecurityAndAlarmHome) - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code=None) -> None: """Send disarm command.""" await self._home.set_security_zones_activation(False, False) - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code=None) -> None: """Send arm home command.""" await self._home.set_security_zones_activation(False, True) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code=None) -> None: """Send arm away command.""" await self._home.set_security_zones_activation(True, True) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self._home.on_update(self._async_device_changed) - def _async_device_changed(self, *args, **kwargs): + def _async_device_changed(self, *args, **kwargs) -> None: """Handle device state changes.""" _LOGGER.debug("Event %s (%s)", self.name, CONST_ALARM_CONTROL_PANEL_NAME) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index b5b663055a1193..83d48d0a7b1906 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud binary sensor.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncAccelerationSensor, @@ -72,7 +73,9 @@ } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud binary sensor devices.""" pass @@ -82,17 +85,17 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncAccelerationSensor): - devices.append(HomematicipAccelerationSensor(hap, device)) + entities.append(HomematicipAccelerationSensor(hap, device)) if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): - devices.append(HomematicipContactInterface(hap, device)) + entities.append(HomematicipContactInterface(hap, device)) if isinstance( device, (AsyncShutterContact, AsyncShutterContactMagnetic, AsyncRotaryHandleSensor), ): - devices.append(HomematicipShutterContact(hap, device)) + entities.append(HomematicipShutterContact(hap, device)) if isinstance( device, ( @@ -101,31 +104,31 @@ async def async_setup_entry( AsyncMotionDetectorPushButton, ), ): - devices.append(HomematicipMotionDetector(hap, device)) + entities.append(HomematicipMotionDetector(hap, device)) if isinstance(device, AsyncPresenceDetectorIndoor): - devices.append(HomematicipPresenceDetector(hap, device)) + entities.append(HomematicipPresenceDetector(hap, device)) if isinstance(device, AsyncSmokeDetector): - devices.append(HomematicipSmokeDetector(hap, device)) + entities.append(HomematicipSmokeDetector(hap, device)) if isinstance(device, AsyncWaterSensor): - devices.append(HomematicipWaterDetector(hap, device)) + entities.append(HomematicipWaterDetector(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipRainSensor(hap, device)) + entities.append(HomematicipRainSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipStormSensor(hap, device)) - devices.append(HomematicipSunshineSensor(hap, device)) + entities.append(HomematicipStormSensor(hap, device)) + entities.append(HomematicipSunshineSensor(hap, device)) if isinstance(device, AsyncDevice) and device.lowBat is not None: - devices.append(HomematicipBatterySensor(hap, device)) + entities.append(HomematicipBatterySensor(hap, device)) for group in hap.home.groups: if isinstance(group, AsyncSecurityGroup): - devices.append(HomematicipSecuritySensorGroup(hap, group)) + entities.append(HomematicipSecuritySensorGroup(hap, group)) elif isinstance(group, AsyncSecurityZoneGroup): - devices.append(HomematicipSecurityZoneSensorGroup(hap, group)) + entities.append(HomematicipSecurityZoneSensorGroup(hap, group)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorDevice): @@ -142,7 +145,7 @@ def is_on(self) -> bool: return self._device.accelerationSensorTriggered @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the acceleration sensor.""" state_attr = super().device_state_attributes @@ -296,7 +299,7 @@ def is_on(self) -> bool: return self._device.sunshine @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the illuminance sensor.""" state_attr = super().device_state_attributes @@ -346,7 +349,7 @@ def available(self) -> bool: return True @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the security zone group.""" state_attr = super().device_state_attributes @@ -390,7 +393,7 @@ def __init__(self, hap: HomematicipHAP, device) -> None: super().__init__(hap, device, "Sensors") @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the security group.""" state_attr = super().device_state_attributes diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 9673459e820db5..6cc556d5ff2e7d 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -1,6 +1,6 @@ """Support for HomematicIP Cloud climate devices.""" import logging -from typing import Awaitable +from typing import Any, Dict, List, Optional, Union from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup @@ -41,7 +41,9 @@ HMIP_ECO_CM = "ECO" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud climate devices.""" pass @@ -51,13 +53,13 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP climate from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.groups: if isinstance(device, AsyncHeatingGroup): - devices.append(HomematicipHeatingGroup(hap, device)) + entities.append(HomematicipHeatingGroup(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): @@ -74,10 +76,10 @@ def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None: super().__init__(hap, device) self._simple_heating = None if device.actualTemperature is None: - self._simple_heating = self._get_first_radiator_thermostat() + self._simple_heating = self._first_radiator_thermostat @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" return { "identifiers": {(HMIPC_DOMAIN, self._device.id)}, @@ -127,7 +129,7 @@ def hvac_mode(self) -> str: return HVAC_MODE_AUTO @property - def hvac_modes(self): + def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes.""" if self._disabled_by_cooling_mode and not self._has_switch: return [HVAC_MODE_OFF] @@ -139,7 +141,7 @@ def hvac_modes(self): ) @property - def preset_mode(self): + def preset_mode(self) -> Optional[str]: """Return the current preset mode.""" if self._device.boostMode: return PRESET_BOOST @@ -162,7 +164,7 @@ def preset_mode(self): ) @property - def preset_modes(self): + def preset_modes(self) -> List[str]: """Return a list of available preset modes incl. hmip profiles.""" # Boost is only available if a radiator thermostat is in the room, # and heat mode is enabled. @@ -190,7 +192,7 @@ def max_temp(self) -> float: """Return the maximum temperature.""" return self._device.maxTemperature - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: @@ -199,7 +201,7 @@ async def async_set_temperature(self, **kwargs): if self.min_temp <= temperature <= self.max_temp: await self._device.set_point_temperature(temperature) - async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: return @@ -209,7 +211,7 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: else: await self._device.set_control_mode(HMIP_MANUAL_CM) - async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode not in self.preset_modes: return @@ -225,7 +227,7 @@ async def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: await self._device.set_active_profile(profile_idx) @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the access point.""" state_attr = super().device_state_attributes @@ -242,12 +244,12 @@ def device_state_attributes(self): return state_attr @property - def _indoor_climate(self): + def _indoor_climate(self) -> IndoorClimateHome: """Return the hmip indoor climate functional home of this group.""" return self._home.get_functionalHome(IndoorClimateHome) @property - def _device_profiles(self): + def _device_profiles(self) -> List[str]: """Return the relevant profiles.""" return [ profile @@ -258,11 +260,11 @@ def _device_profiles(self): ] @property - def _device_profile_names(self): + def _device_profile_names(self) -> List[str]: """Return a collection of profile names.""" return [profile.name for profile in self._device_profiles] - def _get_profile_idx_by_name(self, profile_name): + def _get_profile_idx_by_name(self, profile_name: str) -> int: """Return a profile index by name.""" relevant_index = self._relevant_profile_group index_name = [ @@ -274,19 +276,19 @@ def _get_profile_idx_by_name(self, profile_name): return relevant_index[index_name[0]] @property - def _heat_mode_enabled(self): + def _heat_mode_enabled(self) -> bool: """Return, if heating mode is enabled.""" return not self._device.cooling @property - def _disabled_by_cooling_mode(self): + def _disabled_by_cooling_mode(self) -> bool: """Return, if group is disabled by the cooling mode.""" return self._device.cooling and ( self._device.coolingIgnored or not self._device.coolingAllowed ) @property - def _relevant_profile_group(self): + def _relevant_profile_group(self) -> List[str]: """Return the relevant profile groups.""" if self._disabled_by_cooling_mode: return [] @@ -305,9 +307,12 @@ def _has_switch(self) -> bool: @property def _has_radiator_thermostat(self) -> bool: """Return, if a radiator thermostat is in the hmip heating group.""" - return bool(self._get_first_radiator_thermostat()) + return bool(self._first_radiator_thermostat) - def _get_first_radiator_thermostat(self): + @property + def _first_radiator_thermostat( + self, + ) -> Optional[Union[AsyncHeatingThermostat, AsyncHeatingThermostatCompact]]: """Return the first radiator thermostat from the hmip heating group.""" for device in self._device.devices: if isinstance( diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index 1488f02f13b0c5..8d85dfda3289e1 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure the HomematicIP Cloud component.""" -from typing import Set +from typing import Any, Dict, Set import voluptuous as vol @@ -34,15 +34,15 @@ class HomematicipCloudFlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH - def __init__(self): + def __init__(self) -> None: """Initialize HomematicIP Cloud config flow.""" self.auth = None - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> Dict[str, Any]: """Handle a flow initialized by the user.""" return await self.async_step_init(user_input) - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input=None) -> Dict[str, Any]: """Handle a flow start.""" errors = {} @@ -69,7 +69,7 @@ async def async_step_init(self, user_input=None): errors=errors, ) - async def async_step_link(self, user_input=None): + async def async_step_link(self, user_input=None) -> Dict[str, Any]: """Attempt to link with the HomematicIP Cloud access point.""" errors = {} @@ -91,7 +91,7 @@ async def async_step_link(self, user_input=None): return self.async_show_form(step_id="link", errors=errors) - async def async_step_import(self, import_info): + async def async_step_import(self, import_info) -> Dict[str, Any]: """Import a new access point as a config entry.""" hapid = import_info[HMIPC_HAPID] authtoken = import_info[HMIPC_AUTHTOKEN] diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 63ac6f7310cbba..ef8cbacfde2a9c 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -22,7 +22,9 @@ HMIP_SLATS_CLOSED = 1 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud cover devices.""" pass @@ -32,15 +34,15 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP cover from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncFullFlushBlind): - devices.append(HomematicipCoverSlats(hap, device)) + entities.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): - devices.append(HomematicipCoverShutter(hap, device)) + entities.append(HomematicipCoverShutter(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): @@ -51,7 +53,7 @@ def current_cover_position(self) -> int: """Return current position of cover.""" return int((1 - self._device.shutterLevel) * 100) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] # HmIP cover is closed:1 -> open:0 @@ -65,15 +67,15 @@ def is_closed(self) -> Optional[bool]: return self._device.shutterLevel == HMIP_COVER_CLOSED return None - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs) -> None: """Open the cover.""" await self._device.set_shutter_level(HMIP_COVER_OPEN) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs) -> None: """Close the cover.""" await self._device.set_shutter_level(HMIP_COVER_CLOSED) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs) -> None: """Stop the device if in motion.""" await self._device.set_shutter_stop() @@ -86,21 +88,21 @@ def current_cover_tilt_position(self) -> int: """Return current tilt position of cover.""" return int((1 - self._device.slatsLevel) * 100) - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs) -> None: """Move the cover to a specific tilt position.""" position = kwargs[ATTR_TILT_POSITION] # HmIP slats is closed:1 -> open:0 level = 1 - position / 100.0 await self._device.set_slats_level(level) - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs) -> None: """Open the slats.""" await self._device.set_slats_level(HMIP_SLATS_OPEN) - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs) -> None: """Close the slats.""" await self._device.set_slats_level(HMIP_SLATS_CLOSED) - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs) -> None: """Stop the device if in motion.""" await self._device.set_shutter_stop() diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 6c81775b6882a1..f35b696767ce89 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -1,6 +1,6 @@ """Generic device for the HomematicIP Cloud component.""" import logging -from typing import Optional +from typing import Any, Dict, Optional from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup @@ -79,7 +79,7 @@ def __init__(self, hap: HomematicipHAP, device, post: Optional[str] = None) -> N _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" # Only physical devices should be HA devices. if isinstance(self._device, AsyncDevice): @@ -96,14 +96,14 @@ def device_info(self): } return None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self._hap.hmip_device_by_entity_id[self.entity_id] = self._device self._device.on_update(self._async_device_changed) self._device.on_remove(self._async_device_removed) @callback - def _async_device_changed(self, *args, **kwargs): + def _async_device_changed(self, *args, **kwargs) -> None: """Handle device state changes.""" # Don't update disabled entities if self.enabled: @@ -152,7 +152,7 @@ async def async_remove_from_registries(self) -> None: entity_registry.async_remove(entity_id) @callback - def _async_device_removed(self, *args, **kwargs): + def _async_device_removed(self, *args, **kwargs) -> None: """Handle hmip device removal.""" # Set marker showing that the HmIP device hase been removed. self.hmip_device_removed = True @@ -193,7 +193,7 @@ def icon(self) -> Optional[str]: return None @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the generic device.""" state_attr = {} diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index bef04180c6f09b..63bdf3166ebf1f 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -22,13 +22,13 @@ class HomematicipAuth: """Manages HomematicIP client registration.""" - def __init__(self, hass, config): + def __init__(self, hass, config) -> None: """Initialize HomematicIP Cloud client registration.""" self.hass = hass self.config = config self.auth = None - async def async_setup(self): + async def async_setup(self) -> bool: """Connect to HomematicIP for registration.""" try: self.auth = await self.get_auth( @@ -38,7 +38,7 @@ async def async_setup(self): except HmipcConnectionError: return False - async def async_checkbutton(self): + async def async_checkbutton(self) -> bool: """Check blue butten has been pressed.""" try: return await self.auth.isRequestAcknowledged() @@ -82,7 +82,7 @@ def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry) -> None: self._accesspoint_connected = True self.hmip_device_by_entity_id = {} - async def async_setup(self, tries: int = 0): + async def async_setup(self, tries: int = 0) -> bool: """Initialize connection.""" try: self.home = await self.get_hap( @@ -108,7 +108,7 @@ async def async_setup(self, tries: int = 0): return True @callback - def async_update(self, *args, **kwargs): + def async_update(self, *args, **kwargs) -> None: """Async update the home device. Triggered when the HMIP HOME_CHANGED event has fired. @@ -141,23 +141,23 @@ def async_update(self, *args, **kwargs): self.home.update_home_only(args[0]) @callback - def async_create_entity(self, *args, **kwargs): + def async_create_entity(self, *args, **kwargs) -> None: """Create a device or a group.""" is_device = EventType(kwargs["event_type"]) == EventType.DEVICE_ADDED self.hass.async_create_task(self.async_create_entity_lazy(is_device)) - async def async_create_entity_lazy(self, is_device=True): + async def async_create_entity_lazy(self, is_device=True) -> None: """Delay entity creation to allow the user to enter a device name.""" if is_device: await asyncio.sleep(30) await self.hass.config_entries.async_reload(self.config_entry.entry_id) - async def get_state(self): + async def get_state(self) -> None: """Update HMIP state and tell Home Assistant.""" await self.home.get_current_state() self.update_all() - def get_state_finished(self, future): + def get_state_finished(self, future) -> None: """Execute when get_state coroutine has finished.""" try: future.result() @@ -167,18 +167,18 @@ def get_state_finished(self, future): _LOGGER.error("Updating state after HMIP access point reconnect failed") self.hass.async_create_task(self.home.disable_events()) - def set_all_to_unavailable(self): + def set_all_to_unavailable(self) -> None: """Set all devices to unavailable and tell Home Assistant.""" for device in self.home.devices: device.unreach = True self.update_all() - def update_all(self): + def update_all(self) -> None: """Signal all devices to update their state.""" for device in self.home.devices: device.fire_update_event() - async def async_connect(self): + async def async_connect(self) -> None: """Start WebSocket connection.""" tries = 0 while True: @@ -210,7 +210,7 @@ async def async_connect(self): except asyncio.CancelledError: break - async def async_reset(self): + async def async_reset(self) -> bool: """Close the websocket connection.""" self._ws_close_requested = True if self._retry_task is not None: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index c262b05d019bde..79083f031ae30d 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud lights.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandDimmer, @@ -33,7 +34,9 @@ ATTR_CURRENT_POWER_W = "current_power_w" -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Old way of setting up HomematicIP Cloud lights.""" pass @@ -43,16 +46,16 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): - devices.append(HomematicipLightMeasuring(hap, device)) + entities.append(HomematicipLightMeasuring(hap, device)) elif isinstance(device, AsyncBrandSwitchNotificationLight): - devices.append(HomematicipLight(hap, device)) - devices.append( + entities.append(HomematicipLight(hap, device)) + entities.append( HomematicipNotificationLight(hap, device, device.topLightChannelIndex) ) - devices.append( + entities.append( HomematicipNotificationLight( hap, device, device.bottomLightChannelIndex ) @@ -61,10 +64,10 @@ async def async_setup_entry( device, (AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer), ): - devices.append(HomematicipDimmer(hap, device)) + entities.append(HomematicipDimmer(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipLight(HomematicipGenericDevice, Light): @@ -79,11 +82,11 @@ def is_on(self) -> bool: """Return true if device is on.""" return self._device.on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the device on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" await self._device.turn_off() @@ -92,7 +95,7 @@ class HomematicipLightMeasuring(HomematicipLight): """Representation of a HomematicIP Cloud measuring light device.""" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the generic device.""" state_attr = super().device_state_attributes @@ -127,14 +130,14 @@ def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_BRIGHTNESS - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: await self._device.set_dim_level(kwargs[ATTR_BRIGHTNESS] / 255.0) else: await self._device.set_dim_level(1) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the light off.""" await self._device.set_dim_level(0) @@ -184,7 +187,7 @@ def hs_color(self) -> tuple: return self._color_switcher.get(simple_rgb_color, [0.0, 0.0]) @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the generic device.""" state_attr = super().device_state_attributes @@ -208,7 +211,7 @@ def unique_id(self) -> str: """Return a unique ID.""" return f"{self.__class__.__name__}_{self.post}_{self._device.id}" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the light on.""" # Use hs_color from kwargs, # if not applicable use current hs_color. @@ -236,7 +239,7 @@ async def async_turn_on(self, **kwargs): rampTime=transition, ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the light off.""" simple_rgb_color = self._func_channel.simpleRGBColorState transition = kwargs.get(ATTR_TRANSITION, 0.5) @@ -250,7 +253,7 @@ async def async_turn_off(self, **kwargs): ) -def _convert_color(color) -> RGBColorState: +def _convert_color(color: tuple) -> RGBColorState: """ Convert the given color to the reduced RGBColorState color. diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index acbf72f6ae9943..a8ca3d17eb9422 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud sensors.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, @@ -55,7 +56,9 @@ } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud sensors devices.""" pass @@ -65,11 +68,11 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [HomematicipAccesspointStatus(hap)] + entities = [HomematicipAccesspointStatus(hap)] for device in hap.home.devices: if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): - devices.append(HomematicipHeatingThermostat(hap, device)) - devices.append(HomematicipTemperatureSensor(hap, device)) + entities.append(HomematicipHeatingThermostat(hap, device)) + entities.append(HomematicipTemperatureSensor(hap, device)) if isinstance( device, ( @@ -81,8 +84,8 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipTemperatureSensor(hap, device)) - devices.append(HomematicipHumiditySensor(hap, device)) + entities.append(HomematicipTemperatureSensor(hap, device)) + entities.append(HomematicipHumiditySensor(hap, device)) if isinstance( device, ( @@ -96,7 +99,7 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipIlluminanceSensor(hap, device)) + entities.append(HomematicipIlluminanceSensor(hap, device)) if isinstance( device, ( @@ -105,18 +108,18 @@ async def async_setup_entry( AsyncFullFlushSwitchMeasuring, ), ): - devices.append(HomematicipPowerSensor(hap, device)) + entities.append(HomematicipPowerSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipWindspeedSensor(hap, device)) + entities.append(HomematicipWindspeedSensor(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipTodayRainSensor(hap, device)) + entities.append(HomematicipTodayRainSensor(hap, device)) if isinstance(device, AsyncPassageDetector): - devices.append(HomematicipPassageDetectorDeltaCounter(hap, device)) + entities.append(HomematicipPassageDetectorDeltaCounter(hap, device)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipAccesspointStatus(HomematicipGenericDevice): @@ -127,7 +130,7 @@ def __init__(self, hap: HomematicipHAP) -> None: super().__init__(hap, hap.home) @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return device specific attributes.""" # Adds a sensor to the existing HAP device return { @@ -158,7 +161,7 @@ def unit_of_measurement(self) -> str: return "%" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the access point.""" state_attr = super().device_state_attributes @@ -246,7 +249,7 @@ def unit_of_measurement(self) -> str: return TEMP_CELSIUS @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the windspeed sensor.""" state_attr = super().device_state_attributes @@ -283,7 +286,7 @@ def unit_of_measurement(self) -> str: return "lx" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the wind speed sensor.""" state_attr = super().device_state_attributes @@ -336,7 +339,7 @@ def unit_of_measurement(self) -> str: return "km/h" @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the wind speed sensor.""" state_attr = super().device_state_attributes @@ -378,7 +381,7 @@ def state(self) -> int: return self._device.leftRightCounterDelta @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the delta counter.""" state_attr = super().device_state_attributes diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index dae6019b3786f0..8e15313a4fe0aa 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -1,5 +1,6 @@ """Support for HomematicIP Cloud switches.""" import logging +from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, @@ -24,7 +25,9 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud switch devices.""" pass @@ -34,7 +37,7 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP switch from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): # BrandSwitchMeasuring inherits PlugableSwitchMeasuring @@ -44,27 +47,27 @@ async def async_setup_entry( elif isinstance( device, (AsyncPlugableSwitchMeasuring, AsyncFullFlushSwitchMeasuring) ): - devices.append(HomematicipSwitchMeasuring(hap, device)) + entities.append(HomematicipSwitchMeasuring(hap, device)) elif isinstance( device, (AsyncPlugableSwitch, AsyncPrintedCircuitBoardSwitchBattery) ): - devices.append(HomematicipSwitch(hap, device)) + entities.append(HomematicipSwitch(hap, device)) elif isinstance(device, AsyncOpenCollector8Module): for channel in range(1, 9): - devices.append(HomematicipMultiSwitch(hap, device, channel)) + entities.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncMultiIOBox): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(hap, device, channel)) + entities.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncPrintedCircuitBoardSwitch2): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(hap, device, channel)) + entities.append(HomematicipMultiSwitch(hap, device, channel)) for group in hap.home.groups: if isinstance(group, AsyncSwitchingGroup): - devices.append(HomematicipGroupSwitch(hap, group)) + entities.append(HomematicipGroupSwitch(hap, group)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): @@ -79,11 +82,11 @@ def is_on(self) -> bool: """Return true if device is on.""" return self._device.on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the device on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" await self._device.turn_off() @@ -111,7 +114,7 @@ def available(self) -> bool: return True @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the switch-group.""" state_attr = super().device_state_attributes @@ -120,11 +123,11 @@ def device_state_attributes(self): return state_attr - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the group on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the group off.""" await self._device.turn_off() @@ -148,7 +151,7 @@ def today_energy_kwh(self) -> int: class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): """Representation of a HomematicIP Cloud multi switch device.""" - def __init__(self, hap: HomematicipHAP, device, channel: int): + def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: """Initialize the multi switch device.""" self.channel = channel super().__init__(hap, device, f"Channel{channel}") @@ -163,10 +166,10 @@ def is_on(self) -> bool: """Return true if device is on.""" return self._device.functionalChannels[self.channel].on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the device on.""" await self._device.turn_on(self.channel) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the device off.""" await self._device.turn_off(self.channel) diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 5aa3f28c45d374..ebc7eacf78ea35 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -37,7 +37,9 @@ } -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Set up the HomematicIP Cloud weather sensor.""" pass @@ -47,17 +49,17 @@ async def async_setup_entry( ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] - devices = [] + entities = [] for device in hap.home.devices: if isinstance(device, AsyncWeatherSensorPro): - devices.append(HomematicipWeatherSensorPro(hap, device)) + entities.append(HomematicipWeatherSensorPro(hap, device)) elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)): - devices.append(HomematicipWeatherSensor(hap, device)) + entities.append(HomematicipWeatherSensor(hap, device)) - devices.append(HomematicipHomeWeather(hap)) + entities.append(HomematicipHomeWeather(hap)) - if devices: - async_add_entities(devices) + if entities: + async_add_entities(entities) class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): From 6da01904c405778b63447214c13b214c31c726f8 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Mon, 25 Nov 2019 14:42:40 +0100 Subject: [PATCH 1732/3953] Fix typo in scaffold generator script (#29058) --- script/scaffold/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index a04cdb3ef5e0bd..59767249d29799 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -154,7 +154,7 @@ def _custom_tasks(template, info) -> None: "pick_implementation": {"title": "Pick Authentication Method"} }, "abort": { - "missing_configuration": "The Somfy component is not configured. Please follow the documentation." + "missing_configuration": "The {info.name} component is not configured. Please follow the documentation." }, "create_entry": { "default": f"Successfully authenticated with {info.name}." From 335473cb715a6b2e083a0d0bb85d090d01a61a9c Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 25 Nov 2019 14:43:17 +0100 Subject: [PATCH 1733/3953] Move hdmi_cec imports at top-level (#29056) --- homeassistant/components/hdmi_cec/__init__.py | 45 +++++++--------- .../components/hdmi_cec/media_player.py | 51 ++++++++----------- 2 files changed, 40 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index d1637f96d95db8..b460020546fa09 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -4,6 +4,25 @@ import logging import multiprocessing +from pycec.cec import CecAdapter +from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand +from pycec.const import ( + ADDR_AUDIOSYSTEM, + ADDR_BROADCAST, + ADDR_UNREGISTERED, + KEY_MUTE_OFF, + KEY_MUTE_ON, + KEY_MUTE_TOGGLE, + KEY_VOLUME_DOWN, + KEY_VOLUME_UP, + POWER_OFF, + POWER_ON, + STATUS_PLAY, + STATUS_STILL, + STATUS_STOP, +) +from pycec.network import HDMINetwork, PhysicalAddress +from pycec.tcp import TcpAdapter import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER @@ -155,8 +174,6 @@ def parse_mapping(mapping, parents=None): parents = [] for addr, val in mapping.items(): if isinstance(addr, (str,)) and isinstance(val, (str,)): - from pycec.network import PhysicalAddress - yield (addr, PhysicalAddress(val)) else: cur = parents + [addr] @@ -168,20 +185,6 @@ def parse_mapping(mapping, parents=None): def setup(hass: HomeAssistant, base_config): """Set up the CEC capability.""" - from pycec.network import HDMINetwork - from pycec.commands import CecCommand, KeyReleaseCommand, KeyPressCommand - from pycec.const import ( - KEY_VOLUME_UP, - KEY_VOLUME_DOWN, - KEY_MUTE_ON, - KEY_MUTE_OFF, - KEY_MUTE_TOGGLE, - ADDR_AUDIOSYSTEM, - ADDR_BROADCAST, - ADDR_UNREGISTERED, - ) - from pycec.cec import CecAdapter - from pycec.tcp import TcpAdapter # Parse configuration into a dict of device name to physical address # represented as a list of four elements. @@ -278,8 +281,6 @@ def _power_on(call): def _select_device(call): """Select the active device.""" - from pycec.network import PhysicalAddress - addr = call.data[ATTR_DEVICE] if not addr: _LOGGER.error("Device not found: %s", call.data[ATTR_DEVICE]) @@ -366,14 +367,6 @@ def __init__(self, device, logical) -> None: def update(self): """Update device status.""" device = self._device - from pycec.const import ( - STATUS_PLAY, - STATUS_STOP, - STATUS_STILL, - POWER_OFF, - POWER_ON, - ) - if device.power_status in [POWER_OFF, 3]: self._state = STATE_OFF elif device.status == STATUS_PLAY: diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index 379105430bc792..42c5f0b456c08d 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -1,6 +1,27 @@ """Support for HDMI CEC devices as media players.""" import logging +from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand +from pycec.const import ( + KEY_BACKWARD, + KEY_FORWARD, + KEY_MUTE_TOGGLE, + KEY_PAUSE, + KEY_PLAY, + KEY_STOP, + KEY_VOLUME_DOWN, + KEY_VOLUME_UP, + POWER_OFF, + POWER_ON, + STATUS_PLAY, + STATUS_STILL, + STATUS_STOP, + TYPE_AUDIO, + TYPE_PLAYBACK, + TYPE_RECORDER, + TYPE_TUNER, +) + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( DOMAIN, @@ -50,8 +71,6 @@ def __init__(self, device, logical) -> None: def send_keypress(self, key): """Send keypress to CEC adapter.""" - from pycec.commands import KeyPressCommand, KeyReleaseCommand - _LOGGER.debug( "Sending keypress %s to device %s", hex(key), hex(self._logical_address) ) @@ -60,20 +79,14 @@ def send_keypress(self, key): def send_playback(self, key): """Send playback status to CEC adapter.""" - from pycec.commands import CecCommand - self._device.async_send_command(CecCommand(key, dst=self._logical_address)) def mute_volume(self, mute): """Mute volume.""" - from pycec.const import KEY_MUTE_TOGGLE - self.send_keypress(KEY_MUTE_TOGGLE) def media_previous_track(self): """Go to previous track.""" - from pycec.const import KEY_BACKWARD - self.send_keypress(KEY_BACKWARD) def turn_on(self): @@ -92,8 +105,6 @@ def turn_off(self): def media_stop(self): """Stop playback.""" - from pycec.const import KEY_STOP - self.send_keypress(KEY_STOP) self._state = STATE_IDLE @@ -103,8 +114,6 @@ def play_media(self, media_type, media_id, **kwargs): def media_next_track(self): """Skip to next track.""" - from pycec.const import KEY_FORWARD - self.send_keypress(KEY_FORWARD) def media_seek(self, position): @@ -117,8 +126,6 @@ def set_volume_level(self, volume): def media_pause(self): """Pause playback.""" - from pycec.const import KEY_PAUSE - self.send_keypress(KEY_PAUSE) self._state = STATE_PAUSED @@ -128,22 +135,16 @@ def select_source(self, source): def media_play(self): """Start playback.""" - from pycec.const import KEY_PLAY - self.send_keypress(KEY_PLAY) self._state = STATE_PLAYING def volume_up(self): """Increase volume.""" - from pycec.const import KEY_VOLUME_UP - _LOGGER.debug("%s: volume up", self._logical_address) self.send_keypress(KEY_VOLUME_UP) def volume_down(self): """Decrease volume.""" - from pycec.const import KEY_VOLUME_DOWN - _LOGGER.debug("%s: volume down", self._logical_address) self.send_keypress(KEY_VOLUME_DOWN) @@ -155,14 +156,6 @@ def state(self) -> str: def update(self): """Update device status.""" device = self._device - from pycec.const import ( - STATUS_PLAY, - STATUS_STOP, - STATUS_STILL, - POWER_OFF, - POWER_ON, - ) - if device.power_status in [POWER_OFF, 3]: self._state = STATE_OFF elif not self.support_pause: @@ -180,8 +173,6 @@ def update(self): @property def supported_features(self): """Flag media player features that are supported.""" - from pycec.const import TYPE_RECORDER, TYPE_PLAYBACK, TYPE_TUNER, TYPE_AUDIO - if self.type_id == TYPE_RECORDER or self.type == TYPE_PLAYBACK: return ( SUPPORT_TURN_ON From 3e4f7fddf261e21c2e3e68677421d2d1c48a5652 Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Mon, 25 Nov 2019 14:46:49 +0100 Subject: [PATCH 1734/3953] Handle offline state of SAJ inverters (fixes #29007) (#29009) * Fix for SAJ issue #29007 * Sort mapping --- homeassistant/components/saj/sensor.py | 27 ++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 2a17d110c6e414..52ae3640a7f6f7 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -24,6 +24,7 @@ TEMP_FAHRENHEIT, ) from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later @@ -38,12 +39,12 @@ INVERTER_TYPES = ["ethernet", "wifi"] SAJ_UNIT_MAPPINGS = { - "W": POWER_WATT, - "kWh": ENERGY_KILO_WATT_HOUR, + "": None, "h": UNIT_OF_MEASUREMENT_HOURS, "kg": MASS_KILOGRAMS, + "kWh": ENERGY_KILO_WATT_HOUR, + "W": POWER_WATT, "°C": TEMP_CELSIUS, - "": None, } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -58,7 +59,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up SAJ sensors.""" + """Set up the SAJ sensors.""" remove_interval_update = None wifi = config[CONF_TYPE] == INVERTER_TYPES[1] @@ -80,7 +81,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= saj = pysaj.SAJ(config[CONF_HOST], **kwargs) done = await saj.read(sensor_def) except pysaj.UnauthorizedException: - _LOGGER.error("Username and/or password is wrong.") + _LOGGER.error("Username and/or password is wrong") return except pysaj.UnexpectedResponseException as err: _LOGGER.error( @@ -88,13 +89,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) return - if done: - for sensor in sensor_def: - hass_sensors.append( - SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) - ) + if not done: + raise PlatformNotReady + + for sensor in sensor_def: + hass_sensors.append( + SAJsensor(saj.serialnumber, sensor, inverter_name=config.get(CONF_NAME)) + ) - async_add_entities(hass_sensors) + async_add_entities(hass_sensors) async def async_saj(): """Update all the SAJ sensors.""" @@ -167,7 +170,7 @@ class SAJsensor(Entity): """Representation of a SAJ sensor.""" def __init__(self, serialnumber, pysaj_sensor, inverter_name=None): - """Initialize the sensor.""" + """Initialize the SAJ sensor.""" self._sensor = pysaj_sensor self._inverter_name = inverter_name self._serialnumber = serialnumber From 9dc63419c441d6dd0d5cfe7c44ef3991043af243 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 25 Nov 2019 16:05:21 +0100 Subject: [PATCH 1735/3953] Bump numpy 1.17.4 / opencv 4.1.2 for Python 3.8 (#29061) --- homeassistant/components/iqvia/manifest.json | 11 +++-------- homeassistant/components/opencv/manifest.json | 7 ++----- homeassistant/components/tensorflow/manifest.json | 8 ++------ homeassistant/components/trend/manifest.json | 6 ++---- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 6 files changed, 12 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 04723a6a1f67fe..7a5eb7e56df386 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,12 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": [ - "numpy==1.17.3", - "pyiqvia==0.2.1" - ], + "requirements": ["numpy==1.17.4", "pyiqvia==0.2.1"], "dependencies": [], - "codeowners": [ - "@bachya" - ] -} \ No newline at end of file + "codeowners": ["@bachya"] +} diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index bd82da000cf10d..4fe6026dfeff08 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,10 +2,7 @@ "domain": "opencv", "name": "Opencv", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": [ - "numpy==1.17.3", - "opencv-python-headless==4.1.1.26" - ], + "requirements": ["numpy==1.17.4", "opencv-python-headless==4.1.2.30"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index e0a8728b2957ae..278a92a481f9f2 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -2,11 +2,7 @@ "domain": "tensorflow", "name": "Tensorflow", "documentation": "https://www.home-assistant.io/integrations/tensorflow", - "requirements": [ - "tensorflow==1.13.2", - "numpy==1.17.3", - "protobuf==3.6.1" - ], + "requirements": ["tensorflow==1.13.2", "numpy==1.17.4", "protobuf==3.6.1"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index cf9333be7c38e0..8842b03b594fad 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,9 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": [ - "numpy==1.17.3" - ], + "requirements": ["numpy==1.17.4"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 82160c08e42582..72e846d4063cc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -897,7 +897,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.3 +numpy==1.17.4 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -915,7 +915,7 @@ onkyo-eiscp==1.2.7 onvif-zeep-async==0.2.0 # homeassistant.components.opencv -# opencv-python-headless==4.1.1.26 +# opencv-python-headless==4.1.2.30 # homeassistant.components.openevse openevsewifi==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a3c5e576a450f..cfeba689189fb0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -300,7 +300,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.3 +numpy==1.17.4 # homeassistant.components.google oauth2client==4.0.0 From f504ac8c65b63bc93dae2b721919c7e489d3272b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 25 Nov 2019 17:06:10 +0200 Subject: [PATCH 1736/3953] Drop < 0.97 Huawei LTE sensor unique id migration workaround (#29060) --- homeassistant/components/huawei_lte/sensor.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 99170d4e7c0bb7..3cc36b30d8ec94 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -11,7 +11,6 @@ DEVICE_CLASS_SIGNAL_STRENGTH, DOMAIN as SENSOR_DOMAIN, ) -from homeassistant.helpers import entity_registry from . import HuaweiLteBaseEntity from .const import ( @@ -170,23 +169,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): HuaweiLteSensor(router, key, item, SENSOR_META.get((key, item), {})) ) - # 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 != DOMAIN: - continue - for sensor in sensors: - oldsuf = ".".join(f"{sensor.key}.{sensor.item}") - 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) From e40dda3ce3e7b018465ad38f01c8aef7b79432bf Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 25 Nov 2019 16:06:37 +0100 Subject: [PATCH 1737/3953] Move econet imports at top-level (#29050) --- homeassistant/components/econet/water_heater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 1c8deae5b99b90..35547c8878ee84 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -2,6 +2,7 @@ import datetime import logging +from pyeconet.api import PyEcoNet import voluptuous as vol from homeassistant.components.water_heater import ( @@ -74,7 +75,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the EcoNet water heaters.""" - from pyeconet.api import PyEcoNet hass.data[ECONET_DATA] = {} hass.data[ECONET_DATA]["water_heaters"] = [] From ea6417bea3ac7b938f1be25952f8e86f722c8475 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 25 Nov 2019 19:07:27 +0100 Subject: [PATCH 1738/3953] track Raspberry Pi 4 image in the version integration (#29059) --- homeassistant/components/version/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 3e00b87e9840d2..fc861d7e3caaba 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -24,6 +24,8 @@ "raspberrypi2", "raspberrypi3", "raspberrypi3-64", + "raspberrypi4", + "raspberrypi4-64", "tinker", "odroid-c2", "odroid-xu", From 9a5dc848c9ab772821b521dd4f89e2b63b21abc7 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Mon, 25 Nov 2019 20:12:01 +0100 Subject: [PATCH 1739/3953] Fix climate device actions (#28660) * limit climate device actions * update test * use supported_features for device_action * Fix tests * user support_features for device_condition --- .../components/climate/device_action.py | 17 +++++----- .../components/climate/device_condition.py | 2 +- .../components/climate/test_device_action.py | 24 ++++++++++++++ .../climate/test_device_condition.py | 33 +++++++++++++++++++ 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index b53109f69cb7ad..836e2277461f7d 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -59,14 +59,15 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: CONF_TYPE: "set_hvac_mode", } ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "set_preset_mode", - } - ) + if state.attributes["supported_features"] & const.SUPPORT_PRESET_MODE: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "set_preset_mode", + } + ) return actions diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index c923f3123f1515..3a0752339429a6 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -61,7 +61,7 @@ async def async_get_conditions( } ) - if state and const.ATTR_PRESET_MODES in state.attributes: + if state and state.attributes["supported_features"] & const.SUPPORT_PRESET_MODE: conditions.append( { CONF_CONDITION: "device", diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 3eb1f38ec41d00..46e8b3395c4c0a 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -39,6 +39,7 @@ async def test_get_actions(hass, device_reg, entity_reg): ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) hass.states.async_set("climate.test_5678", const.HVAC_MODE_COOL, {}) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 17}) expected_actions = [ { "domain": DOMAIN, @@ -57,6 +58,29 @@ async def test_get_actions(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) +async def test_get_action_hvac_only(hass, device_reg, entity_reg): + """Test we get the expected actions from a climate.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + hass.states.async_set("climate.test_5678", const.HVAC_MODE_COOL, {}) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 1}) + expected_actions = [ + { + "domain": DOMAIN, + "type": "set_hvac_mode", + "device_id": device_entry.id, + "entity_id": "climate.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + async def test_action(hass): """Test for actions.""" hass.states.async_set( diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 82b6f595fb0b62..b0a9c6c283a7ef 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -53,6 +53,7 @@ async def test_get_conditions(hass, device_reg, entity_reg): const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], }, ) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 17}) expected_conditions = [ { "condition": "device", @@ -73,6 +74,38 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert_lists_same(conditions, expected_conditions) +async def test_get_conditions_hvac_only(hass, device_reg, entity_reg): + """Test we get the expected conditions from a climate.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + hass.states.async_set( + f"{DOMAIN}.test_5678", + const.HVAC_MODE_COOL, + { + const.ATTR_HVAC_MODE: const.HVAC_MODE_COOL, + const.ATTR_PRESET_MODE: const.PRESET_AWAY, + const.ATTR_PRESET_MODES: [const.PRESET_HOME, const.PRESET_AWAY], + }, + ) + hass.states.async_set("climate.test_5678", "attributes", {"supported_features": 1}) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_hvac_mode", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + } + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" hass.states.async_set( From 3dd5222ec7776dd4771ee9151b3de2d5ab7ff2b6 Mon Sep 17 00:00:00 2001 From: majuss Date: Mon, 25 Nov 2019 20:21:54 +0100 Subject: [PATCH 1740/3953] Bumped lupupy to version 0.0.18 (#28989) * Bumped lupupy * Ran gen req * Ran gen hassfest Co-authored-by: Pascal Vizeli --- CODEOWNERS | 1 + homeassistant/components/lupusec/manifest.json | 4 ++-- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 3bf34fc2613f80..c6cd3cc9e812ef 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -182,6 +182,7 @@ homeassistant/components/logi_circle/* @evanjd homeassistant/components/lovelace/* @home-assistant/frontend homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luftdaten/* @fabaff +homeassistant/components/lupusec/* @majuss homeassistant/components/lutron/* @JonGilmore homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index bb6b18243ec13f..2fea92afd0e249 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -3,8 +3,8 @@ "name": "Lupusec", "documentation": "https://www.home-assistant.io/integrations/lupusec", "requirements": [ - "lupupy==0.0.17" + "lupupy==0.0.18" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@majuss"] } diff --git a/requirements_all.txt b/requirements_all.txt index 72e846d4063cc5..75d0115fa4811d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -800,7 +800,7 @@ london-tube-status==0.2 luftdaten==0.6.3 # homeassistant.components.lupusec -lupupy==0.0.17 +lupupy==0.0.18 # homeassistant.components.lw12wifi lw12==0.9.2 From c4108fec4f879b1f98f8cb0ad7e7959bd8a3557d Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 25 Nov 2019 23:32:35 +0100 Subject: [PATCH 1741/3953] Move yi imports at top-level (#29001) * Move yi imports at top-level * Review 1 : removing useless disable=import-error * Review 2 : removing useless disable=import-error --- homeassistant/components/yi/camera.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/yi/camera.py b/homeassistant/components/yi/camera.py index fb1b46344ca70d..c8417748fd9e93 100644 --- a/homeassistant/components/yi/camera.py +++ b/homeassistant/components/yi/camera.py @@ -2,21 +2,24 @@ import asyncio import logging +from aioftp import Client, StatusCodeError +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PASSWORD, + CONF_PATH, CONF_PORT, CONF_USERNAME, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) @@ -82,8 +85,6 @@ def name(self): async def _get_latest_video_url(self): """Retrieve the latest video file from the customized Yi FTP server.""" - from aioftp import Client, StatusCodeError - ftp = Client() try: await ftp.connect(self.host) @@ -125,8 +126,6 @@ async def _get_latest_video_url(self): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG - url = await self._get_latest_video_url() if url and url != self._last_url: ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) @@ -142,8 +141,6 @@ async def async_camera_image(self): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg - if not self._is_on: return From 66b7dfec53b9ad04d19cd39684295d3ad4dad3b7 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Mon, 25 Nov 2019 23:45:14 +0100 Subject: [PATCH 1742/3953] Nilu integration code owner (#29081) * nilu code owner * gen codeowners file --- CODEOWNERS | 1 + homeassistant/components/nilu/manifest.json | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c6cd3cc9e812ef..7f734e37f670e0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -210,6 +210,7 @@ homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan homeassistant/components/netdata/* @fabaff homeassistant/components/nextbus/* @vividboarder +homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff diff --git a/homeassistant/components/nilu/manifest.json b/homeassistant/components/nilu/manifest.json index fe7a92bc2705bb..77df26312e998e 100644 --- a/homeassistant/components/nilu/manifest.json +++ b/homeassistant/components/nilu/manifest.json @@ -6,5 +6,7 @@ "niluclient==0.1.2" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@hfurubotten" + ] +} \ No newline at end of file From 04754b354c31e8862bb4765a3ce1e34144342389 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Mon, 25 Nov 2019 23:45:33 +0100 Subject: [PATCH 1743/3953] Entur public transport code owner (#29080) * entur code owner * gen codeowners file --- CODEOWNERS | 1 + .../components/entur_public_transport/manifest.json | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7f734e37f670e0..7e011fe46d6ab2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,6 +88,7 @@ homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer +homeassistant/components/entur_public_transport/* @hfurubotten homeassistant/components/environment_canada/* @michaeldavie homeassistant/components/ephember/* @ttroy50 homeassistant/components/epsonworkforce/* @ThaStealth diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index b0910f165363c2..3f00743a1dbf71 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -6,5 +6,7 @@ "enturclient==0.2.0" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@hfurubotten" + ] +} \ No newline at end of file From f971e16749df78a1e4f6e76d59df8941b90efb34 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 25 Nov 2019 23:46:12 +0100 Subject: [PATCH 1744/3953] Move eddystone_temperature imports at top-level (#29052) * Move eddystone_temperature imports at top-level * Fix pylint ? --- .../components/eddystone_temperature/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 67724e9fcf344e..2084e3070294fb 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -9,6 +9,12 @@ """ import logging +# pylint: disable=import-error +from beacontools import ( + BeaconScanner, + EddystoneFilter, + EddystoneTLMFrame, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -150,12 +156,6 @@ def callback(bt_addr, _, packet, additional_info): packet.temperature, ) - from beacontools import ( # pylint: disable=import-error - BeaconScanner, - EddystoneFilter, - EddystoneTLMFrame, - ) - device_filters = [EddystoneFilter(d.namespace, d.instance) for d in devices] self.scanner = BeaconScanner( From dc8c085872c9578cdd3af470aa0141995db35801 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Mon, 25 Nov 2019 17:50:43 -0500 Subject: [PATCH 1745/3953] Support default display category based one switch device_class. (#28221) --- homeassistant/components/alexa/entities.py | 4 ++++ tests/components/alexa/test_smart_home.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 6d2f9aef56aa59..d3bfe1c5d8d78d 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -270,6 +270,10 @@ class SwitchCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class == switch.DEVICE_CLASS_OUTLET: + return [DisplayCategory.SMARTPLUG] + return [DisplayCategory.SWITCH] def interfaces(self): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 9b901288f26a7d..20cae0e10e45ea 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -168,6 +168,23 @@ async def test_switch(hass, events): properties.assert_equal("Alexa.PowerController", "powerState", "ON") +async def test_outlet(hass, events): + """Test switch with device class outlet discovery.""" + device = ( + "switch.test", + "on", + {"friendly_name": "Test switch", "device_class": "outlet"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "switch#test" + assert appliance["displayCategories"][0] == "SMARTPLUG" + assert appliance["friendlyName"] == "Test switch" + assert_endpoint_capabilities( + appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + ) + + async def test_light(hass): """Test light discovery.""" device = ("light.test_1", "on", {"friendly_name": "Test light 1"}) From 5ea5db32d2d56159114bd90404558efcca775258 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Mon, 25 Nov 2019 18:07:33 -0500 Subject: [PATCH 1746/3953] Add Alexa.ModeController to cover entities, adds open/close utterances! (#28309) * Added Alexa.ModeController to cover entities. * Added synonyms for directives. * Updated tests for additional synonyms for directives. * Added Alexa.ModeController to cover entities. * Sacrifice unused variable in split() to please the Pylint gods. * Removed duplicate instance check. * Corrected variable name, clarified definition and consistency. * Changed list to tuple. --- .../components/alexa/capabilities.py | 37 ++++++ homeassistant/components/alexa/entities.py | 9 +- homeassistant/components/alexa/handlers.py | 33 ++++- tests/components/alexa/test_smart_home.py | 119 +++++++++++++++++- 4 files changed, 189 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 49b5c5141b6837..2802dfd3a48788 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -9,9 +9,11 @@ STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_CLOSED, STATE_LOCKED, STATE_OFF, STATE_ON, + STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, @@ -888,6 +890,9 @@ def get_property(self, name): if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": return self.entity.attributes.get(fan.ATTR_DIRECTION) + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + return self.entity.attributes.get(cover.ATTR_POSITION) + return None def configuration(self): @@ -903,6 +908,12 @@ def capability_resources(self): {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION} ] + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + capability_resources = [ + {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_MODE}, + {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_PRESET}, + ] + return capability_resources def mode_resources(self): @@ -927,6 +938,32 @@ def mode_resources(self): ], } + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + mode_resources = { + "ordered": False, + "resources": [ + { + "value": f"{cover.ATTR_POSITION}.{STATE_OPEN}", + "friendly_names": [ + {"type": Catalog.LABEL_TEXT, "value": "open"}, + {"type": Catalog.LABEL_TEXT, "value": "opened"}, + {"type": Catalog.LABEL_TEXT, "value": "raise"}, + {"type": Catalog.LABEL_TEXT, "value": "raised"}, + ], + }, + { + "value": f"{cover.ATTR_POSITION}.{STATE_CLOSED}", + "friendly_names": [ + {"type": Catalog.LABEL_TEXT, "value": "close"}, + {"type": Catalog.LABEL_TEXT, "value": "closed"}, + {"type": Catalog.LABEL_TEXT, "value": "shut"}, + {"type": Catalog.LABEL_TEXT, "value": "lower"}, + {"type": Catalog.LABEL_TEXT, "value": "lowered"}, + ], + }, + ], + } + return mode_resources def serialize_mode_resources(self): diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index d3bfe1c5d8d78d..20376c51223c08 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -311,7 +311,10 @@ class CoverCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" - return [DisplayCategory.DOOR] + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) + if device_class in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_DOOR): + return [DisplayCategory.DOOR] + return [DisplayCategory.OTHER] def interfaces(self): """Yield the supported interfaces.""" @@ -319,6 +322,10 @@ def interfaces(self): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & cover.SUPPORT_SET_POSITION: yield AlexaPercentageController(self.entity) + if supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN): + yield AlexaModeController( + self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" + ) yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index c23e01f501fd91..eae8b4a55207c8 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -9,7 +9,6 @@ ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, - STATE_ALARM_DISARMED, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_NIGHT, @@ -28,6 +27,9 @@ SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, + STATE_ALARM_DISARMED, + STATE_CLOSED, + STATE_OPEN, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -956,23 +958,42 @@ async def async_api_set_mode(hass, config, directive, context): domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - mode = directive.payload["mode"] + capability_mode = directive.payload["mode"] - if domain != fan.DOMAIN: + if domain not in (fan.DOMAIN, cover.DOMAIN): msg = "Entity does not support directive" raise AlexaInvalidDirectiveError(msg) if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - mode, direction = mode.split(".") - if direction in [fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD]: + _, direction = capability_mode.split(".") + if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD): service = fan.SERVICE_SET_DIRECTION data[fan.ATTR_DIRECTION] = direction + if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + _, position = capability_mode.split(".") + + if position == STATE_CLOSED: + service = cover.SERVICE_CLOSE_COVER + + if position == STATE_OPEN: + service = cover.SERVICE_OPEN_COVER + await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ModeController", + "instance": instance, + "name": "mode", + "value": capability_mode, + } + ) + + return response @HANDLERS.register(("Alexa.ModeController", "AdjustMode")) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 20cae0e10e45ea..7738df5cd78c81 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -565,7 +565,7 @@ async def test_direction_fan(hass): }, } in supported_modes - call, _ = await assert_request_calls_service( + call, msg = await assert_request_calls_service( "Alexa.ModeController", "SetMode", "fan#test_4", @@ -575,6 +575,25 @@ async def test_direction_fan(hass): instance="fan.direction", ) assert call.data["direction"] == "reverse" + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "direction.reverse" + + call, msg = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "fan#test_4", + "fan.set_direction", + hass, + payload={"mode": "direction.forward"}, + instance="fan.direction", + ) + assert call.data["direction"] == "forward" + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "direction.forward" # Test for AdjustMode instance=None Error coverage with pytest.raises(AssertionError): @@ -1190,11 +1209,12 @@ async def test_cover(hass): appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "cover#test" - assert appliance["displayCategories"][0] == "DOOR" + assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test cover" assert_endpoint_capabilities( appliance, + "Alexa.ModeController", "Alexa.PercentageController", "Alexa.PowerController", "Alexa.EndpointHealth", @@ -2076,3 +2096,98 @@ async def test_mode_unsupported_domain(hass): assert msg["header"]["name"] == "ErrorResponse" assert msg["header"]["namespace"] == "Alexa" assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + +async def test_cover_position(hass): + """Test cover position mode discovery.""" + device = ( + "cover.test", + "off", + {"friendly_name": "Test cover", "supported_features": 255, "position": 30}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Test cover" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.ModeController", + "Alexa.PercentageController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + ) + + mode_capability = get_capability(capabilities, "Alexa.ModeController") + assert mode_capability is not None + assert mode_capability["instance"] == "cover.position" + + properties = mode_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "mode"} in properties["supported"] + + capability_resources = mode_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Mode"}, + } in capability_resources["friendlyNames"] + + configuration = mode_capability["configuration"] + assert configuration is not None + assert configuration["ordered"] is False + + supported_modes = configuration["supportedModes"] + assert supported_modes is not None + assert { + "value": "position.open", + "modeResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "open", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "opened", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "raise", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "raised", "locale": "en-US"}}, + ] + }, + } in supported_modes + assert { + "value": "position.closed", + "modeResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "close", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "closed", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "shut", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "lower", "locale": "en-US"}}, + {"@type": "text", "value": {"text": "lowered", "locale": "en-US"}}, + ] + }, + } in supported_modes + + call, msg = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "cover#test", + "cover.close_cover", + hass, + payload={"mode": "position.closed"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "position.closed" + + call, msg = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "cover#test", + "cover.open_cover", + hass, + payload={"mode": "position.open"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "position.open" From f53812f2616b4ac3a11350b198dc8f3f812d625d Mon Sep 17 00:00:00 2001 From: mvn23 Date: Tue, 26 Nov 2019 00:08:18 +0100 Subject: [PATCH 1747/3953] Fix opentherm_gw config flow migration (#28474) * Fix opentherm_gw migration to config flow. * Address feedback --- homeassistant/components/opentherm_gw/climate.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 44f143d64da24a..28a0ae98270e30 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -31,6 +31,9 @@ _LOGGER = logging.getLogger(__name__) +DEFAULT_FLOOR_TEMP = False +DEFAULT_PRECISION = None + SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE @@ -54,8 +57,8 @@ def __init__(self, gw_dev, options): """Initialize the device.""" self._gateway = gw_dev self.friendly_name = gw_dev.name - self.floor_temp = options[CONF_FLOOR_TEMP] - self.temp_precision = options.get(CONF_PRECISION) + self.floor_temp = options.get(CONF_FLOOR_TEMP, DEFAULT_FLOOR_TEMP) + self.temp_precision = options.get(CONF_PRECISION, DEFAULT_PRECISION) self._current_operation = None self._current_temperature = None self._hvac_mode = HVAC_MODE_HEAT @@ -70,7 +73,7 @@ def __init__(self, gw_dev, options): def update_options(self, entry): """Update climate entity options.""" self.floor_temp = entry.options[CONF_FLOOR_TEMP] - self.temp_precision = entry.options.get(CONF_PRECISION) + self.temp_precision = entry.options[CONF_PRECISION] self.async_schedule_update_ha_state() async def async_added_to_hass(self): From 970a80216db902ebe3b998c07260f96f4a22d09a Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Mon, 25 Nov 2019 18:17:12 -0500 Subject: [PATCH 1748/3953] Add valid inputs to alexa InputController (#28483) * Add supported Inputs for Alexa.InputController. * Fixed Test. * Added default parameter for get() per @quthla suggestion. * Added additional tests, assets call data. * Added additional tests, asserts call data. * Accounted for space in input name, added tests to handle space. --- .../components/alexa/capabilities.py | 27 +++++ homeassistant/components/alexa/const.py | 81 ++++++++++++++ homeassistant/components/alexa/handlers.py | 20 +++- tests/components/alexa/test_smart_home.py | 103 ++++++++++++++++++ 4 files changed, 225 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 2802dfd3a48788..56622084b4832d 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -35,6 +35,7 @@ DATE_FORMAT, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, + Inputs, ) from .errors import UnsupportedProperty @@ -115,6 +116,11 @@ def configuration(): """Return the Configuration object.""" return [] + @staticmethod + def inputs(): + """Applicable only to media players.""" + return [] + @staticmethod def supported_operations(): """Return the supportedOperations object.""" @@ -164,6 +170,10 @@ def serialize_discovery(self): if supported_operations: result["supportedOperations"] = supported_operations + inputs = self.inputs() + if inputs: + result["inputs"] = inputs + return result def serialize_properties(self): @@ -531,6 +541,23 @@ def name(self): """Return the Alexa API name of this interface.""" return "Alexa.InputController" + def inputs(self): + """Return the list of valid supported inputs.""" + source_list = self.entity.attributes.get( + media_player.ATTR_INPUT_SOURCE_LIST, [] + ) + input_list = [] + for source in source_list: + formatted_source = ( + source.lower().replace("-", "").replace("_", "").replace(" ", "") + ) + if formatted_source in Inputs.VALID_SOURCE_NAME_MAP.keys(): + input_list.append( + {"name": Inputs.VALID_SOURCE_NAME_MAP[formatted_source]} + ) + + return input_list + class AlexaTemperatureSensor(AlexaCapability): """Implements Alexa.TemperatureSensor. diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 2a5f9a512b3b40..1aa9d4f2c1dcc2 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -272,3 +272,84 @@ class Unit: WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" + + +class Inputs: + """Valid names for the InputController. + + https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#input + """ + + VALID_SOURCE_NAME_MAP = { + "aux": "AUX 1", + "aux1": "AUX 1", + "aux2": "AUX 2", + "aux3": "AUX 3", + "aux4": "AUX 4", + "aux5": "AUX 5", + "aux6": "AUX 6", + "aux7": "AUX 7", + "bluray": "BLURAY", + "cable": "CABLE", + "cd": "CD", + "coax": "COAX 1", + "coax1": "COAX 1", + "coax2": "COAX 2", + "composite": "COMPOSITE 1", + "composite1": "COMPOSITE 1", + "dvd": "DVD", + "game": "GAME", + "gameconsole": "GAME", + "hdradio": "HD RADIO", + "hdmi": "HDMI 1", + "hdmi1": "HDMI 1", + "hdmi2": "HDMI 2", + "hdmi3": "HDMI 3", + "hdmi4": "HDMI 4", + "hdmi5": "HDMI 5", + "hdmi6": "HDMI 6", + "hdmi7": "HDMI 7", + "hdmi8": "HDMI 8", + "hdmi9": "HDMI 9", + "hdmi10": "HDMI 10", + "hdmiarc": "HDMI ARC", + "input": "INPUT 1", + "input1": "INPUT 1", + "input2": "INPUT 2", + "input3": "INPUT 3", + "input4": "INPUT 4", + "input5": "INPUT 5", + "input6": "INPUT 6", + "input7": "INPUT 7", + "input8": "INPUT 8", + "input9": "INPUT 9", + "input10": "INPUT 10", + "ipod": "IPOD", + "line": "LINE 1", + "line1": "LINE 1", + "line2": "LINE 2", + "line3": "LINE 3", + "line4": "LINE 4", + "line5": "LINE 5", + "line6": "LINE 6", + "line7": "LINE 7", + "mediaplayer": "MEDIA PLAYER", + "optical": "OPTICAL 1", + "optical1": "OPTICAL 1", + "optical2": "OPTICAL 2", + "phono": "PHONO", + "playstation": "PLAYSTATION", + "playstation3": "PLAYSTATION 3", + "playstation4": "PLAYSTATION 4", + "satellite": "SATELLITE", + "satellitetv": "SATELLITE", + "smartcast": "SMARTCAST", + "tuner": "TUNER", + "tv": "TV", + "usbdac": "USB DAC", + "video": "VIDEO 1", + "video1": "VIDEO 1", + "video2": "VIDEO 2", + "video3": "VIDEO 3", + "xbox": "XBOX", + } diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index eae8b4a55207c8..2e360fba7e2a6c 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -44,6 +44,7 @@ API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause, + Inputs, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, SPEED_FAN_MAP, @@ -461,13 +462,20 @@ async def async_api_select_input(hass, config, directive, context): media_input = directive.payload["input"] entity = directive.entity - # attempt to map the ALL UPPERCASE payload name to a source - source_list = entity.attributes[media_player.const.ATTR_INPUT_SOURCE_LIST] or [] + # Attempt to map the ALL UPPERCASE payload name to a source. + # Strips trailing 1 to match single input devices. + source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST, []) for source in source_list: - # response will always be space separated, so format the source in the - # most likely way to find a match - formatted_source = source.lower().replace("-", " ").replace("_", " ") - if formatted_source in media_input.lower(): + formatted_source = ( + source.lower().replace("-", "").replace("_", "").replace(" ", "") + ) + media_input = media_input.lower().replace(" ", "") + if ( + formatted_source in Inputs.VALID_SOURCE_NAME_MAP.keys() + and formatted_source == media_input + ) or ( + media_input.endswith("1") and formatted_source == media_input.rstrip("1") + ): media_input = source break else: diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 7738df5cd78c81..09d9b6bd7b6a1d 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1015,6 +1015,109 @@ async def test_media_player_power(hass): ) +async def test_media_player_inputs(hass): + """Test media player discovery with source list inputs.""" + device = ( + "media_player.test", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOURCE, + "volume_level": 0.75, + "source_list": [ + "foo", + "foo_2", + "hdmi", + "hdmi_2", + "hdmi-3", + "hdmi4", + "hdmi 5", + "HDMI 6", + "hdmi_arc", + "aux", + "input 1", + "tv", + ], + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test" + assert appliance["displayCategories"][0] == "TV" + assert appliance["friendlyName"] == "Test media player" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.InputController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + ) + + input_capability = get_capability(capabilities, "Alexa.InputController") + assert input_capability is not None + assert {"name": "AUX"} not in input_capability["inputs"] + assert {"name": "AUX 1"} in input_capability["inputs"] + assert {"name": "HDMI 1"} in input_capability["inputs"] + assert {"name": "HDMI 2"} in input_capability["inputs"] + assert {"name": "HDMI 3"} in input_capability["inputs"] + assert {"name": "HDMI 4"} in input_capability["inputs"] + assert {"name": "HDMI 5"} in input_capability["inputs"] + assert {"name": "HDMI 6"} in input_capability["inputs"] + assert {"name": "HDMI ARC"} in input_capability["inputs"] + assert {"name": "FOO 1"} not in input_capability["inputs"] + assert {"name": "TV"} in input_capability["inputs"] + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 1"}, + ) + assert call.data["source"] == "hdmi" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 2"}, + ) + assert call.data["source"] == "hdmi_2" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 5"}, + ) + assert call.data["source"] == "hdmi 5" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "HDMI 6"}, + ) + assert call.data["source"] == "HDMI 6" + + call, _ = await assert_request_calls_service( + "Alexa.InputController", + "SelectInput", + "media_player#test", + "media_player.select_source", + hass, + payload={"input": "TV"}, + ) + assert call.data["source"] == "tv" + + async def test_media_player_speaker(hass): """Test media player discovery with device class speaker.""" device = ( From 12c396fb191a233014b9c6283a6b25bc5823b503 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 00:36:04 +0100 Subject: [PATCH 1749/3953] Move epsonworkforce imports at top-level (#29086) --- homeassistant/components/epsonworkforce/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index b310376e5cc55c..3bb90eb064490a 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -34,8 +35,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the cartridge sensor.""" host = config.get(CONF_HOST) - from epsonprinter_pkg.epsonprinterapi import EpsonPrinterAPI - api = EpsonPrinterAPI(host) if not api.available: raise PlatformNotReady() From 29e22014469039a6f32cf4afaf0fc90a8850a8b4 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 00:36:37 +0100 Subject: [PATCH 1750/3953] Move velux imports at top-level (#29085) --- homeassistant/components/velux/__init__.py | 6 +++--- homeassistant/components/velux/cover.py | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/velux/__init__.py b/homeassistant/components/velux/__init__.py index 51f615e68aaf5a..bac65c969cff30 100644 --- a/homeassistant/components/velux/__init__.py +++ b/homeassistant/components/velux/__init__.py @@ -1,12 +1,12 @@ """Support for VELUX KLF 200 devices.""" import logging + +from pyvlx import PyVLX, PyVLXException import voluptuous as vol -from pyvlx import PyVLX -from pyvlx import PyVLXException +from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP DOMAIN = "velux" DATA_VELUX = "data_velux" diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index a471960b048df5..7d4adc7350c8d7 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -1,4 +1,7 @@ """Support for Velux covers.""" +from pyvlx import OpeningDevice, Position +from pyvlx.opening_device import Awning, Blind, RollerShutter, Window + from homeassistant.components.cover import ( ATTR_POSITION, SUPPORT_CLOSE, @@ -16,7 +19,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up cover(s) for Velux platform.""" entities = [] for node in hass.data[DATA_VELUX].pyvlx.nodes: - from pyvlx import OpeningDevice if isinstance(node, OpeningDevice): entities.append(VeluxCover(node)) @@ -67,8 +69,6 @@ def current_cover_position(self): @property def device_class(self): """Define this cover as either window/blind/awning/shutter.""" - from pyvlx.opening_device import Blind, RollerShutter, Window, Awning - if isinstance(self.node, Window): return "window" if isinstance(self.node, Blind): @@ -96,7 +96,6 @@ async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" if ATTR_POSITION in kwargs: position_percent = 100 - kwargs[ATTR_POSITION] - from pyvlx import Position await self.node.set_position( Position(position_percent=position_percent), wait_for_completion=False From 5015993f3003b0ab1ebb3eb1e258ffb42f9683bb Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 00:37:03 +0100 Subject: [PATCH 1751/3953] Move upnp imports at top-level (#29083) --- homeassistant/components/upnp/__init__.py | 15 ++++++++------- homeassistant/components/upnp/config_flow.py | 3 +-- homeassistant/components/upnp/device.py | 13 +++---------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index bbb49ebd1d44ae..9a7e06738dba68 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -7,11 +7,12 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers import dispatcher -from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + dispatcher, +) +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import get_local_ip from .const import ( @@ -20,10 +21,10 @@ CONF_HASS, CONF_LOCAL_IP, CONF_PORTS, + DOMAIN, + LOGGER as _LOGGER, SIGNAL_REMOVE_SENSOR, ) -from .const import DOMAIN -from .const import LOGGER as _LOGGER from .device import Device NOTIFICATION_ID = "upnp_notification" diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index cdcce76dbc38f5..1601595b6a9f5d 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,11 +1,10 @@ """Config flow for UPNP.""" -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow from .const import DOMAIN from .device import Device - config_entry_flow.register_discovery_flow( DOMAIN, "UPnP/IGD", Device.async_discover, config_entries.CONN_CLASS_LOCAL_POLL ) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index de3c93a82ed3a1..b87c3dc96b636e 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -3,13 +3,14 @@ from ipaddress import IPv4Address import aiohttp +from async_upnp_client import UpnpError, UpnpFactory +from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.profiles.igd import IgdDevice from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType -from .const import LOGGER as _LOGGER -from .const import DOMAIN, CONF_LOCAL_IP +from .const import CONF_LOCAL_IP, DOMAIN, LOGGER as _LOGGER class Device: @@ -48,14 +49,10 @@ async def async_discover(cls, hass: HomeAssistantType): async def async_create_device(cls, hass: HomeAssistantType, ssdp_description: str): """Create UPnP/IGD device.""" # build async_upnp_client requester - from async_upnp_client.aiohttp import AiohttpSessionRequester - session = async_get_clientsession(hass) requester = AiohttpSessionRequester(session, True) # create async_upnp_client device - from async_upnp_client import UpnpFactory - factory = UpnpFactory(requester, disable_state_variable_validation=True) upnp_device = await factory.async_create_device(ssdp_description) @@ -99,8 +96,6 @@ async def async_add_port_mappings(self, ports, local_ip): async def _async_add_port_mapping(self, external_port, local_ip, internal_port): """Add a port mapping.""" # create port mapping - from async_upnp_client import UpnpError - _LOGGER.info( "Creating port mapping %s:%s:%s (TCP)", external_port, @@ -135,8 +130,6 @@ async def async_delete_port_mappings(self): async def _async_delete_port_mapping(self, external_port): """Remove a port mapping.""" - from async_upnp_client import UpnpError - _LOGGER.info("Deleting port mapping %s (TCP)", external_port) try: await self._igd_device.async_delete_port_mapping( From 844eb6b8ef376b91e97a7decfcec8b843d4cc146 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 00:37:19 +0100 Subject: [PATCH 1752/3953] Move dlna_dmr imports at top-level (#29082) --- .../components/dlna_dmr/media_player.py | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 5dd7ab7a88a74d..a9ea9b21d596c1 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -6,9 +6,12 @@ from typing import Optional import aiohttp +from async_upnp_client import UpnpFactory +from async_upnp_client.aiohttp import AiohttpNotifyServer, AiohttpSessionRequester +from async_upnp_client.profiles.dlna import DeviceState, DmrDevice import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, @@ -40,10 +43,10 @@ ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import HomeAssistantType import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import get_local_ip +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -121,8 +124,6 @@ async def async_start_event_handler( return hass_data["event_handler"] # start event handler - from async_upnp_client.aiohttp import AiohttpNotifyServer - server = AiohttpNotifyServer( requester, listen_port=server_port, @@ -163,8 +164,6 @@ async def async_setup_platform( hass.data[DLNA_DMR_DATA]["lock"] = asyncio.Lock() # build upnp/aiohttp requester - from async_upnp_client.aiohttp import AiohttpSessionRequester - session = async_get_clientsession(hass) requester = AiohttpSessionRequester(session, True) @@ -180,8 +179,6 @@ async def async_setup_platform( ) # create upnp device - from async_upnp_client import UpnpFactory - factory = UpnpFactory(requester, disable_state_variable_validation=True) try: upnp_device = await factory.async_create_device(url) @@ -189,8 +186,6 @@ async def async_setup_platform( raise PlatformNotReady() # wrap with DmrDevice - from async_upnp_client.profiles.dlna import DmrDevice - dlna_device = DmrDevice(upnp_device, event_handler) # create our own device @@ -361,8 +356,6 @@ async def async_play_media(self, media_type, media_id, **kwargs): await self._device.async_wait_for_can_play() # If already playing, no need to call Play - from async_upnp_client.profiles.dlna import DeviceState - if self._device.state == DeviceState.PLAYING: return @@ -403,8 +396,6 @@ def state(self): if not self._available: return STATE_OFF - from async_upnp_client.profiles.dlna import DeviceState - if self._device.state is None: return STATE_ON if self._device.state == DeviceState.PLAYING: From d211a29cae3d791a5774a97c1f048627ac42bd65 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 00:39:44 +0100 Subject: [PATCH 1753/3953] Move imports to top for xfinity (#29077) --- homeassistant/components/xfinity/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xfinity/device_tracker.py b/homeassistant/components/xfinity/device_tracker.py index 93603ae5797e21..712d31d46db5bd 100644 --- a/homeassistant/components/xfinity/device_tracker.py +++ b/homeassistant/components/xfinity/device_tracker.py @@ -3,6 +3,7 @@ from requests.exceptions import RequestException import voluptuous as vol +from xfinity_gateway import XfinityGateway import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( @@ -23,7 +24,6 @@ def get_scanner(hass, config): """Validate the configuration and return an Xfinity Gateway scanner.""" - from xfinity_gateway import XfinityGateway gateway = XfinityGateway(config[DOMAIN][CONF_HOST]) scanner = None From a64d04da56199a98edf3478136d03684188d133e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 00:39:54 +0100 Subject: [PATCH 1754/3953] Move imports to top for xeoma (#29076) --- homeassistant/components/xeoma/camera.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/xeoma/camera.py b/homeassistant/components/xeoma/camera.py index 2ca4aab7aff403..bb5febe6bd78c0 100644 --- a/homeassistant/components/xeoma/camera.py +++ b/homeassistant/components/xeoma/camera.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from pyxeoma.xeoma import Xeoma, XeomaError from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -40,7 +41,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Discover and setup Xeoma Cameras.""" - from pyxeoma.xeoma import Xeoma, XeomaError host = config[CONF_HOST] login = config.get(CONF_USERNAME) @@ -111,7 +111,6 @@ def __init__(self, xeoma, image, name, username, password): async def async_camera_image(self): """Return a still image response from the camera.""" - from pyxeoma.xeoma import XeomaError try: image = await self._xeoma.async_get_camera_image( From 729116ff3ae0367614d0b5039c1cd6a1e203fb02 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 00:40:08 +0100 Subject: [PATCH 1755/3953] Move imports to top for version (#29075) --- homeassistant/components/version/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index fc861d7e3caaba..a6e43251b54a58 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -2,6 +2,13 @@ import logging from datetime import timedelta +from pyhaversion import ( + LocalVersion, + DockerVersion, + HassioVersion, + PyPiVersion, + HaIoVersion, +) import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -56,13 +63,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Version sensor platform.""" - from pyhaversion import ( - LocalVersion, - DockerVersion, - HassioVersion, - PyPiVersion, - HaIoVersion, - ) beta = config.get(CONF_BETA) image = config.get(CONF_IMAGE) From c95a01ce3a11da52377e037ccd7110941dcb9e0d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 00:40:25 +0100 Subject: [PATCH 1756/3953] Move imports to top for yale_smart_alarm (#29074) --- .../yale_smart_alarm/alarm_control_panel.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 094263a658b581..c2d0ab01247226 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -2,6 +2,13 @@ import logging import voluptuous as vol +from yalesmartalarmclient.client import ( + YaleSmartAlarmClient, + AuthenticationError, + YALE_STATE_DISARM, + YALE_STATE_ARM_PARTIAL, + YALE_STATE_ARM_FULL, +) from homeassistant.components.alarm_control_panel import ( AlarmControlPanel, @@ -42,8 +49,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config[CONF_PASSWORD] area_id = config[CONF_AREA_ID] - from yalesmartalarmclient.client import YaleSmartAlarmClient, AuthenticationError - try: client = YaleSmartAlarmClient(username, password, area_id) except AuthenticationError: @@ -62,12 +67,6 @@ def __init__(self, name, client): self._client = client self._state = None - from yalesmartalarmclient.client import ( - YALE_STATE_DISARM, - YALE_STATE_ARM_PARTIAL, - YALE_STATE_ARM_FULL, - ) - self._state_map = { YALE_STATE_DISARM: STATE_ALARM_DISARMED, YALE_STATE_ARM_PARTIAL: STATE_ALARM_ARMED_HOME, From 3203cba01f95860b34f1abc2757af2d37e61852a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 00:40:42 +0100 Subject: [PATCH 1757/3953] Move imports to top for xiaomi_tv (#29073) --- homeassistant/components/xiaomi_tv/media_player.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index 352ce0c4835515..c34448ba63b4fd 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import pymitv from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -29,7 +30,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Xiaomi TV platform.""" - from pymitv import Discover # If a hostname is set. Discovery is skipped. host = config.get(CONF_HOST) @@ -37,14 +37,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if host is not None: # Check if there's a valid TV at the IP address. - if not Discover().check_ip(host): + if not pymitv.Discover().check_ip(host): _LOGGER.error("Could not find Xiaomi TV with specified IP: %s", host) else: # Register TV with Home Assistant. add_entities([XiaomiTV(host, name)]) else: # Otherwise, discover TVs on network. - add_entities(XiaomiTV(tv, DEFAULT_NAME) for tv in Discover().scan()) + add_entities(XiaomiTV(tv, DEFAULT_NAME) for tv in pymitv.Discover().scan()) class XiaomiTV(MediaPlayerDevice): @@ -52,11 +52,9 @@ class XiaomiTV(MediaPlayerDevice): def __init__(self, ip, name): """Receive IP address and name to construct class.""" - # Import pymitv library. - from pymitv import TV # Initialize the Xiaomi TV. - self._tv = TV(ip) + self._tv = pymitv.TV(ip) # Default name value, only to be overridden by user. self._name = name self._state = STATE_OFF From 1fde0d18ed25e90b35ecaa14f589761b31dae85a Mon Sep 17 00:00:00 2001 From: SukramJ Date: Tue, 26 Nov 2019 00:42:53 +0100 Subject: [PATCH 1758/3953] Add supported_features to Alarm Control Panel to limit device_actions (#29065) * Add supported_features to Alarm Control Panel * mark supported_features abstract * Add SF to async_register_entity_service * fix test * Add missing SF SUPPORT_ALARM_ARM_CUSTOM_BYPASS * isort * fix async_register_entity_service * Update alarm_control_panel.py --- .../components/abode/alarm_control_panel.py | 9 ++ .../alarm_control_panel/__init__.py | 45 ++++++++-- .../components/alarm_control_panel/const.py | 7 ++ .../alarm_control_panel/device_action.py | 88 ++++++++++++------- .../alarmdecoder/alarm_control_panel.py | 10 +++ .../alarmdotcom/alarm_control_panel.py | 9 ++ .../components/arlo/alarm_control_panel.py | 10 +++ .../components/blink/alarm_control_panel.py | 6 ++ .../components/canary/alarm_control_panel.py | 10 +++ .../concord232/alarm_control_panel.py | 9 ++ .../components/demo/alarm_control_panel.py | 7 +- .../components/egardia/alarm_control_panel.py | 9 ++ .../components/elkm1/alarm_control_panel.py | 10 +++ .../envisalink/alarm_control_panel.py | 16 ++++ .../homekit_controller/alarm_control_panel.py | 10 +++ .../homematicip_cloud/alarm_control_panel.py | 9 ++ .../components/ialarm/alarm_control_panel.py | 9 ++ .../components/ifttt/alarm_control_panel.py | 10 +++ .../components/lupusec/alarm_control_panel.py | 9 ++ .../components/manual/alarm_control_panel.py | 20 ++++- .../manual_mqtt/alarm_control_panel.py | 39 +++++--- .../components/mqtt/alarm_control_panel.py | 10 +++ .../ness_alarm/alarm_control_panel.py | 10 +++ .../components/nx584/alarm_control_panel.py | 9 ++ .../components/point/alarm_control_panel.py | 6 ++ .../satel_integra/alarm_control_panel.py | 13 ++- .../simplisafe/alarm_control_panel.py | 9 ++ .../components/spc/alarm_control_panel.py | 10 +++ .../totalconnect/alarm_control_panel.py | 14 ++- .../verisure/alarm_control_panel.py | 9 ++ .../components/wink/alarm_control_panel.py | 9 ++ .../yale_smart_alarm/alarm_control_panel.py | 13 ++- .../alarm_control_panel/test_device_action.py | 33 +++++++ .../components/device_automation/test_init.py | 3 + .../test/alarm_control_panel.py | 16 ++++ 35 files changed, 449 insertions(+), 66 deletions(-) create mode 100644 homeassistant/components/alarm_control_panel/const.py diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index f1ff08f3a0ac2e..88a072bd79cd51 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -2,6 +2,10 @@ import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, @@ -51,6 +55,11 @@ def state(self): state = None return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def alarm_disarm(self, code=None): """Send disarm command.""" self._device.set_standby() diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 6faad5dd51f194..36cb8650aced00 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -1,4 +1,5 @@ """Component to interface with an alarm control panel.""" +from abc import abstractmethod from datetime import timedelta import logging @@ -7,22 +8,30 @@ from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, - SERVICE_ALARM_TRIGGER, - SERVICE_ALARM_DISARM, - SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from .const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) + DOMAIN = "alarm_control_panel" SCAN_INTERVAL = timedelta(seconds=30) ATTR_CHANGED_BY = "changed_by" @@ -49,21 +58,34 @@ async def async_setup(hass, config): SERVICE_ALARM_DISARM, ALARM_SERVICE_SCHEMA, "async_alarm_disarm" ) component.async_register_entity_service( - SERVICE_ALARM_ARM_HOME, ALARM_SERVICE_SCHEMA, "async_alarm_arm_home" + SERVICE_ALARM_ARM_HOME, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_home", + [SUPPORT_ALARM_ARM_HOME], ) component.async_register_entity_service( - SERVICE_ALARM_ARM_AWAY, ALARM_SERVICE_SCHEMA, "async_alarm_arm_away" + SERVICE_ALARM_ARM_AWAY, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_away", + [SUPPORT_ALARM_ARM_AWAY], ) component.async_register_entity_service( - SERVICE_ALARM_ARM_NIGHT, ALARM_SERVICE_SCHEMA, "async_alarm_arm_night" + SERVICE_ALARM_ARM_NIGHT, + ALARM_SERVICE_SCHEMA, + "async_alarm_arm_night", + [SUPPORT_ALARM_ARM_NIGHT], ) component.async_register_entity_service( SERVICE_ALARM_ARM_CUSTOM_BYPASS, ALARM_SERVICE_SCHEMA, "async_alarm_arm_custom_bypass", + [SUPPORT_ALARM_ARM_CUSTOM_BYPASS], ) component.async_register_entity_service( - SERVICE_ALARM_TRIGGER, ALARM_SERVICE_SCHEMA, "async_alarm_trigger" + SERVICE_ALARM_TRIGGER, + ALARM_SERVICE_SCHEMA, + "async_alarm_trigger", + [SUPPORT_ALARM_TRIGGER], ) return True @@ -164,6 +186,11 @@ def async_alarm_arm_custom_bypass(self, code=None): """ return self.hass.async_add_executor_job(self.alarm_arm_custom_bypass, code) + @property + @abstractmethod + def supported_features(self) -> int: + """Return the list of supported features.""" + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py new file mode 100644 index 00000000000000..77f7846fc3476a --- /dev/null +++ b/homeassistant/components/alarm_control_panel/const.py @@ -0,0 +1,7 @@ +"""Provides the constants needed for component.""" + +SUPPORT_ALARM_ARM_HOME = 1 +SUPPORT_ALARM_ARM_AWAY = 2 +SUPPORT_ALARM_ARM_NIGHT = 4 +SUPPORT_ALARM_TRIGGER = 8 +SUPPORT_ALARM_ARM_CUSTOM_BYPASS = 16 diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index a3c2b4822611e5..81e444ae16f0cb 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -1,5 +1,6 @@ """Provides device automations for Alarm control panel.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( @@ -16,10 +17,17 @@ SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import ATTR_CODE_ARM_REQUIRED, DOMAIN +from .const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) ACTION_TYPES = {"arm_away", "arm_home", "arm_night", "disarm", "trigger"} @@ -42,31 +50,42 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: if entry.domain != DOMAIN: continue + state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the HVAC and preset modes. + if state is None: + continue + + supported_features = state.attributes["supported_features"] + # Add actions for each entity that belongs to this integration - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_away", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_home", - } - ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "arm_night", - } - ) + if supported_features & SUPPORT_ALARM_ARM_AWAY: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_away", + } + ) + if supported_features & SUPPORT_ALARM_ARM_HOME: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_home", + } + ) + if supported_features & SUPPORT_ALARM_ARM_NIGHT: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_night", + } + ) actions.append( { CONF_DEVICE_ID: device_id, @@ -75,14 +94,15 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: CONF_TYPE: "disarm", } ) - actions.append( - { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - CONF_TYPE: "trigger", - } - ) + if supported_features & SUPPORT_ALARM_TRIGGER: + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "trigger", + } + ) return actions diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 288c1dfd1c75a8..f9bff3bfda7b62 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -4,6 +4,11 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_CODE, STATE_ALARM_ARMED_AWAY, @@ -122,6 +127,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/alarmdotcom/alarm_control_panel.py b/homeassistant/components/alarmdotcom/alarm_control_panel.py index 07d69960e0b738..dd6b1272223cea 100644 --- a/homeassistant/components/alarmdotcom/alarm_control_panel.py +++ b/homeassistant/components/alarmdotcom/alarm_control_panel.py @@ -7,6 +7,10 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, CONF_NAME, @@ -95,6 +99,11 @@ def state(self): return STATE_ALARM_ARMED_AWAY return None + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/arlo/alarm_control_panel.py b/homeassistant/components/arlo/alarm_control_panel.py index a56b2a63372cfa..838f319abc1457 100644 --- a/homeassistant/components/arlo/alarm_control_panel.py +++ b/homeassistant/components/arlo/alarm_control_panel.py @@ -7,6 +7,11 @@ PLATFORM_SCHEMA, AlarmControlPanel, ) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, @@ -91,6 +96,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + def update(self): """Update the state of the device.""" _LOGGER.debug("Updating Arlo Alarm Control Panel %s", self.name) diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index b1c9f6a7ec07fd..9b23c1606d4381 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import SUPPORT_ALARM_ARM_AWAY from homeassistant.const import ( ATTR_ATTRIBUTION, STATE_ALARM_ARMED_AWAY, @@ -52,6 +53,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_AWAY + @property def name(self): """Return the name of the panel.""" diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 856ecb9f3a2559..cceb78743d3ee1 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -4,6 +4,11 @@ from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -59,6 +64,11 @@ def state(self): return STATE_ALARM_ARMED_NIGHT return None + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 37bbf05283812c..81a54a182d46e0 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -8,6 +8,10 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, CONF_HOST, @@ -85,6 +89,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Update values from API.""" try: diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index d82edadf161497..0323b68b1b04a5 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -1,16 +1,17 @@ """Demo platform that has two fake alarm control panels.""" import datetime + from homeassistant.components.manual.alarm_control_panel import ManualAlarm from homeassistant.const import ( + CONF_DELAY_TIME, + CONF_PENDING_TIME, + CONF_TRIGGER_TIME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, - CONF_DELAY_TIME, - CONF_PENDING_TIME, - CONF_TRIGGER_TIME, ) diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index 22a458ae9aabc5..2c18be47a1f817 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -4,6 +4,10 @@ import requests import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -79,6 +83,11 @@ def state(self): """Return the state of the device.""" return self._status + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + @property def should_poll(self): """Poll if no report server is enabled.""" diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 38519ab5b3f77a..8c4db6e06a5f02 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -3,6 +3,11 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -143,6 +148,11 @@ def state(self): """Return the state of the element.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Attributes of the area.""" diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 663f19c8ed5806..19703297ccd6b0 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -4,6 +4,12 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, @@ -141,6 +147,16 @@ def state(self): state = STATE_ALARM_DISARMED return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ) + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if code: diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index bb45a6c33d93b0..8cdbe9b2f369de 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -4,6 +4,11 @@ from homekit.model.characteristics import CharacteristicsTypes from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_BATTERY_LEVEL, STATE_ALARM_ARMED_AWAY, @@ -88,6 +93,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + async def async_alarm_disarm(self, code=None): """Send disarm command.""" await self.set_alarm_state(STATE_ALARM_DISARMED, code) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index f362133034f84a..f9a91203426476 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -5,6 +5,10 @@ from homematicip.functionalHomes import SecurityAndAlarmHome from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -77,6 +81,11 @@ def state(self) -> str: def _security_and_alarm(self) -> SecurityAndAlarmHome: return self._home.get_functionalHome(SecurityAndAlarmHome) + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + async def async_alarm_disarm(self, code=None) -> None: """Send disarm command.""" await self._home.set_security_zones_activation(False, False) diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index 845c6b9021f1e2..c3bcf626f0ae93 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -6,6 +6,10 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, CONF_HOST, @@ -91,6 +95,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Return the state of the device.""" status = self._client.get_status() diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index e4d8b6ce654bd3..f740cc8ccc902f 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -6,6 +6,11 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, @@ -127,6 +132,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def assumed_state(self): """Notify that this platform return an assumed state.""" diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py index 245743d0f6537b..c6ad817bfbf6a1 100644 --- a/homeassistant/components/lupusec/alarm_control_panel.py +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -2,6 +2,10 @@ from datetime import timedelta from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -51,6 +55,11 @@ def state(self): state = None return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def alarm_arm_away(self, code=None): """Send arm away command.""" self._device.set_away() diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index ac234dc0ac9b5c..b41da2d51bdb24 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -7,6 +7,13 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( CONF_CODE, CONF_DELAY_TIME, @@ -25,8 +32,8 @@ ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time -import homeassistant.util.dt as dt_util from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -234,6 +241,17 @@ def state(self): return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + | SUPPORT_ALARM_ARM_CUSTOM_BYPASS + ) + @property def _active_state(self): """Get the current state.""" diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index c57fa275516f8e..f11dac357e615e 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -6,30 +6,33 @@ import voluptuous as vol +from homeassistant.components import mqtt import homeassistant.components.alarm_control_panel as alarm -import homeassistant.util.dt as dt_util +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( + CONF_CODE, + CONF_DELAY_TIME, + CONF_DISARM_AFTER_TRIGGER, + CONF_NAME, + CONF_PENDING_TIME, + CONF_PLATFORM, + CONF_TRIGGER_TIME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, - CONF_PLATFORM, - CONF_NAME, - CONF_CODE, - CONF_DELAY_TIME, - CONF_PENDING_TIME, - CONF_TRIGGER_TIME, - CONF_DISARM_AFTER_TRIGGER, ) -from homeassistant.components import mqtt - -from homeassistant.helpers.event import async_track_state_change from homeassistant.core import callback - import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_time +from homeassistant.helpers.event import async_track_state_change, track_point_in_time +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -278,6 +281,16 @@ def state(self): return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ) + @property def _active_state(self): """Get the current state.""" diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 7171a55b2709fd..43d0bb570a8e5a 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -6,6 +6,11 @@ from homeassistant.components import mqtt import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( CONF_CODE, CONF_DEVICE, @@ -223,6 +228,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def code_format(self): """Return one or more digits/characters.""" diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index 1b45c52ab71098..d2feebfb64f4b9 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -3,6 +3,11 @@ import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMING, @@ -62,6 +67,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_TRIGGER + async def async_alarm_disarm(self, code=None): """Send disarm command.""" await self._client.disarm(code) diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index d3d867ff378678..62bc7ae32bba34 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -6,6 +6,10 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -79,6 +83,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Process new events from panel.""" try: diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index f9e725f6c8e5c7..e86b3dd42e8742 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.alarm_control_panel import DOMAIN, AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import SUPPORT_ALARM_ARM_AWAY from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED, @@ -88,6 +89,11 @@ def state(self): """Return state of the device.""" return EVENT_MAP.get(self._home["alarm_status"], STATE_ALARM_ARMED_AWAY) + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_AWAY + @property def changed_by(self): """Return the user the last change was triggered by.""" diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index 2f0e165f21f181..c4321673061a8a 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -1,9 +1,13 @@ """Support for Satel Integra alarm, using ETHM module.""" import asyncio -import logging from collections import OrderedDict +import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -17,8 +21,8 @@ from . import ( CONF_ARM_HOME_MODE, CONF_DEVICE_PARTITIONS, - DATA_SATEL, CONF_ZONE_NAME, + DATA_SATEL, SIGNAL_PANEL_MESSAGE, ) @@ -131,6 +135,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + async def async_alarm_disarm(self, code=None): """Send disarm command.""" if not code: diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index a63a077ed15b8c..96f3fa05f6bc4e 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -10,6 +10,10 @@ FORMAT_TEXT, AlarmControlPanel, ) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( CONF_CODE, STATE_ALARM_ARMED_AWAY, @@ -94,6 +98,11 @@ def state(self): """Return the state of the entity.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def _validate_code(self, code, state): """Validate given code.""" check = self._code is None or code == self._code diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index 8eeccc06515cbd..fa9a9681fff041 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -2,6 +2,11 @@ import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -80,6 +85,11 @@ def state(self): """Return the state of the device.""" return _get_alarm_state(self._area) + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + async def async_alarm_disarm(self, code=None): """Send disarm command.""" from pyspcwebgw.const import AreaMode diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index e5f0b0c8279350..b8b4236806f1ef 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -2,15 +2,20 @@ import logging import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_DISARMED, STATE_ALARM_ARMING, + STATE_ALARM_DISARMED, STATE_ALARM_DISARMING, STATE_ALARM_TRIGGERED, - STATE_ALARM_ARMED_CUSTOM_BYPASS, ) from . import DOMAIN as TOTALCONNECT_DOMAIN @@ -55,6 +60,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property def device_state_attributes(self): """Return the state attributes of the device.""" diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 02f64b6fa9cd52..b7cc822bd43077 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -3,6 +3,10 @@ from time import sleep import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -64,6 +68,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + @property def code_format(self): """Return one or more digits/characters.""" diff --git a/homeassistant/components/wink/alarm_control_panel.py b/homeassistant/components/wink/alarm_control_panel.py index 654252f5ffea84..733022e91b1512 100644 --- a/homeassistant/components/wink/alarm_control_panel.py +++ b/homeassistant/components/wink/alarm_control_panel.py @@ -4,6 +4,10 @@ import pywink import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -52,6 +56,11 @@ def state(self): state = None return state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def alarm_disarm(self, code=None): """Send disarm command.""" self.wink.set_mode("home") diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index c2d0ab01247226..7f2cbc2a33dddb 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -11,13 +11,17 @@ ) from homeassistant.components.alarm_control_panel import ( - AlarmControlPanel, PLATFORM_SCHEMA, + AlarmControlPanel, +) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, ) from homeassistant.const import ( + CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - CONF_NAME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, @@ -83,6 +87,11 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + def update(self): """Return the state of the device.""" armed_status = self._client.get_armed_status() diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index c2dfcbd78b985c..bc489dbe251172 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -46,6 +46,9 @@ async def test_get_actions(hass, device_reg, entity_reg): connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + hass.states.async_set( + "alarm_control_panel.test_5678", "attributes", {"supported_features": 15} + ) expected_actions = [ { "domain": DOMAIN, @@ -82,6 +85,36 @@ async def test_get_actions(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) +async def test_get_actions_arm_night_only(hass, device_reg, entity_reg): + """Test we get the expected actions from a alarm_control_panel.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + hass.states.async_set( + "alarm_control_panel.test_5678", "attributes", {"supported_features": 4} + ) + expected_actions = [ + { + "domain": DOMAIN, + "type": "arm_night", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "disarm", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + async def test_get_action_capabilities(hass, device_reg, entity_reg): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 3c0e3b1eca7ad9..bddef3286ac877 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -184,6 +184,9 @@ async def test_websocket_get_action_capabilities( entity_reg.async_get_or_create( "alarm_control_panel", "test", "5678", device_id=device_entry.id ) + hass.states.async_set( + "alarm_control_panel.test_5678", "attributes", {"supported_features": 15} + ) expected_capabilities = { "arm_away": {"extra_fields": []}, "arm_home": {"extra_fields": []}, diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py index 0e2842f869561a..1ffa52086e7bae 100644 --- a/tests/testing_config/custom_components/test/alarm_control_panel.py +++ b/tests/testing_config/custom_components/test/alarm_control_panel.py @@ -11,6 +11,12 @@ STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from tests.common import MockEntity ENTITIES = {} @@ -64,6 +70,16 @@ def state(self): """Return the state of the device.""" return self._state + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ) + def alarm_arm_away(self, code=None): """Send arm away command.""" self._state = STATE_ALARM_ARMED_AWAY From 112a3f5e9f5df70a705c7bb2c4a1c6fd447f0deb Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 00:44:11 +0100 Subject: [PATCH 1759/3953] Move hangouts imports at top-level (#29055) --- homeassistant/components/hangouts/__init__.py | 10 ++-- .../components/hangouts/config_flow.py | 22 ++++---- .../components/hangouts/hangouts_bot.py | 14 ++--- tests/components/hangouts/test_config_flow.py | 52 ++++++++++++------- 4 files changed, 50 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 953994d6ac0cbd..d4892c66890987 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -1,16 +1,16 @@ """Support for Hangouts.""" import logging +from hangups.auth import GoogleAuthError import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.conversation.util import create_matcher from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import dispatcher, intent import homeassistant.helpers.config_validation as cv -from homeassistant.components.conversation.util import create_matcher # We need an import from .config_flow, without it .config_flow is never loaded. -from .intents import HelpIntent from .config_flow import HangoutsFlowHandler # noqa: F401 from .const import ( CONF_BOT, @@ -32,6 +32,8 @@ SERVICE_UPDATE, TARGETS_SCHEMA, ) +from .hangouts_bot import HangoutsBot +from .intents import HelpIntent _LOGGER = logging.getLogger(__name__) @@ -96,11 +98,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config): """Set up a config entry.""" - from hangups.auth import GoogleAuthError - try: - from .hangouts_bot import HangoutsBot - bot = HangoutsBot( hass, config.data.get(CONF_REFRESH_TOKEN), diff --git a/homeassistant/components/hangouts/config_flow.py b/homeassistant/components/hangouts/config_flow.py index 8e262d8b40f75d..f253df4934194e 100644 --- a/homeassistant/components/hangouts/config_flow.py +++ b/homeassistant/components/hangouts/config_flow.py @@ -1,7 +1,8 @@ """Config flow to configure Google Hangouts.""" import functools -import voluptuous as vol +from hangups import get_auth +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD @@ -9,10 +10,16 @@ from .const import ( CONF_2FA, - CONF_REFRESH_TOKEN, CONF_AUTH_CODE, + CONF_REFRESH_TOKEN, DOMAIN as HANGOUTS_DOMAIN, ) +from .hangups_utils import ( + Google2FAError, + GoogleAuthError, + HangoutsCredentials, + HangoutsRefreshToken, +) @callback @@ -44,14 +51,6 @@ async def async_step_user(self, user_input=None): return self.async_abort(reason="already_configured") if user_input is not None: - from hangups import get_auth - from .hangups_utils import ( - HangoutsCredentials, - HangoutsRefreshToken, - GoogleAuthError, - Google2FAError, - ) - user_email = user_input[CONF_EMAIL] user_password = user_input[CONF_PASSWORD] user_auth_code = user_input.get(CONF_AUTH_CODE) @@ -99,9 +98,6 @@ async def async_step_2fa(self, user_input=None): errors = {} if user_input is not None: - from hangups import get_auth - from .hangups_utils import GoogleAuthError - self._credentials.set_verification_code(user_input[CONF_2FA]) try: await self.hass.async_add_executor_job( diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 9eee2ca2be3eff..8575a547a9c8b5 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -4,6 +4,8 @@ import logging import aiohttp +import hangups +from hangups import ChatMessageEvent, ChatMessageSegment, Client, get_auth, hangouts_pb2 from homeassistant.helpers import dispatcher, intent from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -24,6 +26,7 @@ EVENT_HANGOUTS_MESSAGE_RECEIVED, INTENT_HELP, ) +from .hangups_utils import HangoutsCredentials, HangoutsRefreshToken _LOGGER = logging.getLogger(__name__) @@ -126,8 +129,6 @@ def async_resolve_conversations(self, _): ) async def _async_handle_conversation_event(self, event): - from hangups import ChatMessageEvent - if isinstance(event, ChatMessageEvent): dispatcher.async_dispatcher_send( self.hass, @@ -196,11 +197,6 @@ async def _async_process(self, intents, text, conv_id): async def async_connect(self): """Login to the Google Hangouts.""" - from .hangups_utils import HangoutsRefreshToken, HangoutsCredentials - - from hangups import Client - from hangups import get_auth - session = await self.hass.async_add_executor_job( get_auth, HangoutsCredentials(None, None, None), @@ -252,8 +248,6 @@ async def _async_send_message(self, message, targets, data): if not conversations: return False - from hangups import ChatMessageSegment, hangouts_pb2 - messages = [] for segment in message: if messages: @@ -306,8 +300,6 @@ async def _async_send_message(self, message, targets, data): await conv.send_message(messages, image_file) async def _async_list_conversations(self): - import hangups - ( self._user_list, self._conversation_list, diff --git a/tests/components/hangouts/test_config_flow.py b/tests/components/hangouts/test_config_flow.py index 29585db5f61590..93f909d3bd4dc7 100644 --- a/tests/components/hangouts/test_config_flow.py +++ b/tests/components/hangouts/test_config_flow.py @@ -4,6 +4,10 @@ from homeassistant import data_entry_flow from homeassistant.components.hangouts import config_flow +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +EMAIL = "test@test.com" +PASSWORD = "1232456" async def test_flow_works(hass, aioclient_mock): @@ -12,12 +16,12 @@ async def test_flow_works(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth"): + with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test@test.com" + assert result["title"] == EMAIL async def test_flow_works_with_authcode(hass, aioclient_mock): @@ -26,16 +30,16 @@ async def test_flow_works_with_authcode(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth"): + with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_user( { - "email": "test@test.com", - "password": "1232456", + CONF_EMAIL: EMAIL, + CONF_PASSWORD: PASSWORD, "authorization_code": "c29tZXJhbmRvbXN0cmluZw==", } ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test@test.com" + assert result["title"] == EMAIL async def test_flow_works_with_2fa(hass, aioclient_mock): @@ -46,17 +50,20 @@ async def test_flow_works_with_2fa(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth", side_effect=Google2FAError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=Google2FAError, + ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "2fa" - with patch("hangups.get_auth"): + with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_2fa({"2fa": 123456}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test@test.com" + assert result["title"] == EMAIL async def test_flow_with_unknown_2fa(hass, aioclient_mock): @@ -68,11 +75,11 @@ async def test_flow_with_unknown_2fa(hass, aioclient_mock): flow.hass = hass with patch( - "hangups.get_auth", + "homeassistant.components.hangouts.config_flow.get_auth", side_effect=GoogleAuthError("Unknown verification code input"), ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"]["base"] == "invalid_2fa_method" @@ -86,9 +93,12 @@ async def test_flow_invalid_login(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth", side_effect=GoogleAuthError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=GoogleAuthError, + ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"]["base"] == "invalid_login" @@ -102,14 +112,20 @@ async def test_flow_invalid_2fa(hass, aioclient_mock): flow.hass = hass - with patch("hangups.get_auth", side_effect=Google2FAError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=Google2FAError, + ): result = await flow.async_step_user( - {"email": "test@test.com", "password": "1232456"} + {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "2fa" - with patch("hangups.get_auth", side_effect=Google2FAError): + with patch( + "homeassistant.components.hangouts.config_flow.get_auth", + side_effect=Google2FAError, + ): result = await flow.async_step_2fa({"2fa": 123456}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM From d5db55354e93fee4eb86036e70f4c9e3db2ba1c8 Mon Sep 17 00:00:00 2001 From: Jacob McSwain Date: Mon, 25 Nov 2019 17:47:18 -0600 Subject: [PATCH 1760/3953] Add SSL configuration (#29038) * OpenGarage: Add protocol configuration (#29037) * OpenGarage: Change CONF_PROTOCOL to CONF_SSL Resolves https://github.com/home-assistant/home-assistant/pull/29038#discussion_r350173095 * OpenGarage: Add `verify_ssl` as an option --- homeassistant/components/opengarage/cover.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 1243a9164fd26b..6b5cbc912e6211 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -18,6 +18,8 @@ CONF_COVERS, CONF_HOST, CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, STATE_CLOSING, STATE_OPENING, ) @@ -42,6 +44,8 @@ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, } ) @@ -60,6 +64,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): CONF_NAME: device_config.get(CONF_NAME), CONF_HOST: device_config.get(CONF_HOST), CONF_PORT: device_config.get(CONF_PORT), + CONF_SSL: device_config.get(CONF_SSL), + CONF_VERIFY_SSL: device_config.get(CONF_VERIFY_SSL), CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY), } @@ -73,13 +79,16 @@ class OpenGarageCover(CoverDevice): def __init__(self, args): """Initialize the cover.""" - self.opengarage_url = "http://{}:{}".format(args[CONF_HOST], args[CONF_PORT]) + self.opengarage_url = "{}://{}:{}".format( + "https" if args[CONF_SSL] else "http", args[CONF_HOST], args[CONF_PORT] + ) self._name = args[CONF_NAME] self._device_key = args[CONF_DEVICE_KEY] self._state = None self._state_before_move = None self._device_state_attributes = {} self._available = True + self._verify_ssl = args[CONF_VERIFY_SSL] @property def name(self): @@ -155,7 +164,9 @@ def _push_button(self): result = -1 try: result = requests.get( - f"{self.opengarage_url}/cc?dkey={self._device_key}&click=1", timeout=10 + f"{self.opengarage_url}/cc?dkey={self._device_key}&click=1", + timeout=10, + verify=self._verify_ssl, ).json()["result"] except requests.exceptions.RequestException as ex: _LOGGER.error( From 0088995b980cf866a1b7712d8344e07efcdcf7dd Mon Sep 17 00:00:00 2001 From: Gil Peeters Date: Tue, 26 Nov 2019 11:30:49 +1100 Subject: [PATCH 1761/3953] Refactor template components to extract common routines (#27064) * Added availability_template to Template Cover platform * Added availability_template to Template Binary Sensor platform * Added availability_template to Template Fan platform * Added availability_template to Template Light platform * Added availability_template to Template Sensor platform * Added availability_template to Template Switch platform * Added availability_template to Template Vacuum platform * Added availability_template to Template Lock platform * Added to test for invalid values in availability_template * Black and Lint fix * black formatting * Added to test for invalid values in availability_template * black * Added to test for invalid values in availability_template * Added to test for invalid values in availability_template * simplified exception handler * Fixed Entity discovery big and coverage * Added to test for invalid values in availability_template * flake8 * fixed component ID in test * Added to test for invalid values in availability_template * Added to test for invalid values in availability_template * Made availability_template redering erorr more concise * Cleaned template setup * I'll remember to run black every time one of these days... * Refactored Template initialisation * Refactored Template initialisation * Updated AVAILABILITY_TEMPLATE Rendering error * Updated AVAILABILITY_TEMPLATE Rendering error * Updated AVAILABILITY_TEMPLATE Rendering error * Updated AVAILABILITY_TEMPLATE Rendering error * Updated AVAILABILITY_TEMPLATE Rendering error * Updated AVAILABILITY_TEMPLATE Rendering error * Updated AVAILABILITY_TEMPLATE Rendering error * Updated AVAILABILITY_TEMPLATE Rendering error * Moved const to package Const.py * Moved const to package Const.py * Moved const to package Const.py * Moved const to package Const.py * Moved const to package Const.py * Moved const to package Const.py * Moved const to package Const.py * Moved const to package Const.py * Fix import order (pylint) * Fix import order (pylint) * Fix import order (pylint) * Fix import order (pylint) * Fix import order (pylint) * Fix import order (pylint) * Fix import order (pylint) * Fixed linting issues * Moved availability_template rendering to common loop * Moved availability_template rendering to common loop * Moved availability_template rendering to common loop * Moved availability_template rendering to common loop * Removed 'Magic' string * Removed 'Magic' string and removed duplicate code * Removed 'Magic' string * Removed 'Magic' string * Brought contant into line * Refactored availability_tempalte rendering to common loop * Removed 'Magic' string * Cleaned up const and compare lowercase result to 'true' * Cleaned up const and compare lowercase result to 'true' * Cleaned up const and compare lowercase result to 'true' * Cleaned up const and compare lowercase result to 'true' * Cleaned up const and compare lowercase result to 'true' * Cleaned up const and compare lowercase result to 'true' * Cleaned up const and compare lowercase result to 'true' * reverted _available back to boolean * reverted _available back to boolean * reverted _available back to boolean * reverted _available back to boolean * reverted _available back to boolean * reverted _available back to boolean * reverted _available back to boolean * Fixed tests (magic values and state checks) * Fixed tests (magic values and state checks) * Fixed tests (async, magic values and state checks) * Fixed tests (async, magic values and state checks) * Fixed tests (async, magic values and state checks) * Fixed tests (async, magic values and state checks) * Fixed tests (async, magic values and state checks) * Removed duplicate * Clean up and remove debug * Reverted Dev Container Change --- homeassistant/components/template/__init__.py | 59 +++++++++++++++++++ .../components/template/binary_sensor.py | 52 ++++------------ homeassistant/components/template/cover.py | 52 +++++----------- homeassistant/components/template/fan.py | 37 ++++-------- homeassistant/components/template/light.py | 45 ++++---------- homeassistant/components/template/lock.py | 34 ++++------- homeassistant/components/template/sensor.py | 46 ++++----------- homeassistant/components/template/switch.py | 39 +++--------- homeassistant/components/template/vacuum.py | 48 +++------------ .../components/template/test_binary_sensor.py | 8 +-- tests/components/template/test_lock.py | 9 ++- tests/components/template/test_sensor.py | 53 +++++++++++++++-- 12 files changed, 204 insertions(+), 278 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 0c205a0196c5eb..80421b8e3f88ae 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -1 +1,60 @@ """The template component.""" + +import logging + +from itertools import chain +from homeassistant.const import MATCH_ALL + + +_LOGGER = logging.getLogger(__name__) + + +def initialise_templates(hass, templates, attribute_templates=None): + """Initialise templates and attribute templates.""" + if attribute_templates is None: + attribute_templates = dict() + for template in chain(templates.values(), attribute_templates.values()): + if template is None: + continue + template.hass = hass + + +def extract_entities( + device_name, device_type, manual_entity_ids, templates, attribute_templates=None +): + """Extract entity ids from templates and attribute templates.""" + if attribute_templates is None: + attribute_templates = dict() + entity_ids = set() + if manual_entity_ids is None: + invalid_templates = [] + for template_name, template in chain( + templates.items(), attribute_templates.items() + ): + if template is None: + continue + + template_entity_ids = template.extract_entities() + + if template_entity_ids != MATCH_ALL: + entity_ids |= set(template_entity_ids) + else: + invalid_templates.append(template_name.replace("_template", "")) + + if invalid_templates: + entity_ids = MATCH_ALL + _LOGGER.warning( + "Template %s '%s' has no entity ids configured to track nor" + " were we able to extract the entities to track from the %s " + "template(s). This entity will only be able to be updated " + "manually.", + device_type, + device_name, + ", ".join(invalid_templates), + ) + else: + entity_ids = list(entity_ids) + else: + entity_ids = manual_entity_ids + + return entity_ids diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index d5ade703c97440..116862abc79df2 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,6 +1,5 @@ """Support for exposing a templated binary sensor.""" import logging -from itertools import chain import voluptuous as vol @@ -26,6 +25,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change, async_track_same_state +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -63,11 +63,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) - entity_ids = set() - manual_entity_ids = device_config.get(ATTR_ENTITY_ID) attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) - invalid_templates = [] + friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) + device_class = device_config.get(CONF_DEVICE_CLASS) + delay_on = device_config.get(CONF_DELAY_ON) + delay_off = device_config.get(CONF_DELAY_OFF) templates = { CONF_VALUE_TEMPLATE: value_template, @@ -76,41 +77,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_AVAILABILITY_TEMPLATE: availability_template, } - for tpl_name, template in chain(templates.items(), attribute_templates.items()): - if template is None: - continue - template.hass = hass - - if manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - # Cut off _template from name - invalid_templates.append(tpl_name.replace("_template", "")) - elif entity_ids != MATCH_ALL: - entity_ids |= set(template_entity_ids) - - if manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) - - if invalid_templates: - _LOGGER.warning( - "Template binary sensor %s has no entity ids configured to" - " track nor were we able to extract the entities to track" - " from the %s template(s). This entity will only be able" - " to be updated manually.", - device, - ", ".join(invalid_templates), - ) - - friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) - device_class = device_config.get(CONF_DEVICE_CLASS) - delay_on = device_config.get(CONF_DELAY_ON) - delay_off = device_config.get(CONF_DELAY_OFF) + initialise_templates(hass, templates, attribute_templates) + entity_ids = extract_entities( + device, + "binary sensor", + device_config.get(ATTR_ENTITY_ID), + templates, + attribute_templates, + ) sensors.append( BinarySensorTemplate( diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 483ee1ae8723fb..22035af24ec4c2 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -24,7 +24,6 @@ CONF_FRIENDLY_NAME, CONF_ENTITY_ID, EVENT_HOMEASSISTANT_START, - MATCH_ALL, CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_DEVICE_CLASS, @@ -38,6 +37,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -100,13 +100,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= covers = [] for device, device_config in config[CONF_COVERS].items(): - friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) state_template = device_config.get(CONF_VALUE_TEMPLATE) position_template = device_config.get(CONF_POSITION_TEMPLATE) tilt_template = device_config.get(CONF_TILT_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) + + friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) device_class = device_config.get(CONF_DEVICE_CLASS) open_action = device_config.get(OPEN_ACTION) close_action = device_config.get(CLOSE_ACTION) @@ -121,41 +122,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Must specify at least one of %s" or "%s", OPEN_ACTION, POSITION_ACTION ) continue - template_entity_ids = set() - if state_template is not None: - temp_ids = state_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if position_template is not None: - temp_ids = position_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if tilt_template is not None: - temp_ids = tilt_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if icon_template is not None: - temp_ids = icon_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if entity_picture_template is not None: - temp_ids = entity_picture_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if availability_template is not None: - temp_ids = availability_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if not template_entity_ids: - template_entity_ids = MATCH_ALL - - entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids) + + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_POSITION_TEMPLATE: position_template, + CONF_TILT_TEMPLATE: tilt_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + } + + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "cover", None, templates) covers.append( CoverTemplate( diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 606f18e5fe1923..ebb9bcc8b1427d 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -25,7 +25,6 @@ CONF_ENTITY_ID, STATE_ON, STATE_OFF, - MATCH_ALL, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, ) @@ -33,6 +32,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -98,33 +98,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= speed_list = device_config[CONF_SPEED_LIST] - entity_ids = set() - manual_entity_ids = device_config.get(CONF_ENTITY_ID) - - for template in ( - state_template, - speed_template, - oscillating_template, - direction_template, - availability_template, - ): - if template is None: - continue - template.hass = hass - - if entity_ids == MATCH_ALL or manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - else: - entity_ids |= set(template_entity_ids) + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_SPEED_TEMPLATE: speed_template, + CONF_OSCILLATING_TEMPLATE: oscillating_template, + CONF_DIRECTION_TEMPLATE: direction_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + } - if manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "fan", None, templates) fans.append( TemplateFan( diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 552c21f170dbae..b71aadd0155bfa 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -19,7 +19,6 @@ STATE_ON, STATE_OFF, EVENT_HOMEASSISTANT_START, - MATCH_ALL, CONF_LIGHTS, ) from homeassistant.helpers.config_validation import PLATFORM_SCHEMA @@ -28,6 +27,7 @@ from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -64,46 +64,27 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for device, device_config in config[CONF_LIGHTS].items(): friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) + state_template = device_config.get(CONF_VALUE_TEMPLATE) icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) + level_template = device_config.get(CONF_LEVEL_TEMPLATE) + on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] level_action = device_config.get(CONF_LEVEL_ACTION) - level_template = device_config.get(CONF_LEVEL_TEMPLATE) - - template_entity_ids = set() - - if state_template is not None: - temp_ids = state_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if level_template is not None: - temp_ids = level_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if icon_template is not None: - temp_ids = icon_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if entity_picture_template is not None: - temp_ids = entity_picture_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - - if availability_template is not None: - temp_ids = availability_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) - if not template_entity_ids: - template_entity_ids = MATCH_ALL + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + CONF_LEVEL_TEMPLATE: level_template, + } - entity_ids = device_config.get(CONF_ENTITY_ID, template_entity_ids) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "light", None, templates) lights.append( LightTemplate( diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index aa8cc8b1224e3f..71e9cc6642d595 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -19,6 +19,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -43,39 +44,26 @@ async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Template lock.""" - name = config.get(CONF_NAME) + device = config.get(CONF_NAME) value_template = config.get(CONF_VALUE_TEMPLATE) - value_template.hass = hass - value_template_entity_ids = value_template.extract_entities() - - if value_template_entity_ids == MATCH_ALL: - _LOGGER.warning( - "Template lock '%s' has no entity ids configured to track nor " - "were we able to extract the entities to track from the '%s' " - "template. This entity will only be able to be updated " - "manually.", - name, - CONF_VALUE_TEMPLATE, - ) + availability_template = config.get(CONF_AVAILABILITY_TEMPLATE) - template_entity_ids = set() - template_entity_ids |= set(value_template_entity_ids) + templates = { + CONF_VALUE_TEMPLATE: value_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + } - availability_template = config.get(CONF_AVAILABILITY_TEMPLATE) - if availability_template is not None: - availability_template.hass = hass - temp_ids = availability_template.extract_entities() - if str(temp_ids) != MATCH_ALL: - template_entity_ids |= set(temp_ids) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "lock", None, templates) async_add_devices( [ TemplateLock( hass, - name, + device, value_template, availability_template, - template_entity_ids, + entity_ids, config.get(CONF_LOCK), config.get(CONF_UNLOCK), config.get(CONF_OPTIMISTIC), diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a876819373652f..4ea7daa54f6bcb 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -1,7 +1,6 @@ """Allows the creation of a sensor that breaks out state_attributes.""" import logging from typing import Optional -from itertools import chain import voluptuous as vol @@ -29,6 +28,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.event import async_track_state_change +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" @@ -72,10 +72,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class = device_config.get(CONF_DEVICE_CLASS) attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES] - entity_ids = set() - manual_entity_ids = device_config.get(ATTR_ENTITY_ID) - invalid_templates = [] - templates = { CONF_VALUE_TEMPLATE: state_template, CONF_ICON_TEMPLATE: icon_template, @@ -84,36 +80,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_AVAILABILITY_TEMPLATE: availability_template, } - for tpl_name, template in chain(templates.items(), attribute_templates.items()): - if template is None: - continue - template.hass = hass - - if manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - # Cut off _template from name - invalid_templates.append(tpl_name.replace("_template", "")) - elif entity_ids != MATCH_ALL: - entity_ids |= set(template_entity_ids) - - if invalid_templates: - _LOGGER.warning( - "Template sensor %s has no entity ids configured to track nor" - " were we able to extract the entities to track from the %s " - "template(s). This entity will only be able to be updated " - "manually.", - device, - ", ".join(invalid_templates), - ) - - if manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) + initialise_templates(hass, templates, attribute_templates) + entity_ids = extract_entities( + device, + "sensor", + device_config.get(ATTR_ENTITY_ID), + templates, + attribute_templates, + ) sensors.append( SensorTemplate( @@ -131,7 +105,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= attribute_templates, ) ) + async_add_entities(sensors) + return True diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 2d4dda032ca857..e06ca0c8d54629 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -19,13 +19,13 @@ ATTR_ENTITY_ID, CONF_SWITCHES, EVENT_HOMEASSISTANT_START, - MATCH_ALL, ) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -64,8 +64,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= availability_template = device_config.get(CONF_AVAILABILITY_TEMPLATE) on_action = device_config[ON_ACTION] off_action = device_config[OFF_ACTION] - manual_entity_ids = device_config.get(ATTR_ENTITY_ID) - entity_ids = set() templates = { CONF_VALUE_TEMPLATE: state_template, @@ -73,35 +71,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, CONF_AVAILABILITY_TEMPLATE: availability_template, } - invalid_templates = [] - - for template_name, template in templates.items(): - if template is not None: - template.hass = hass - - if manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - invalid_templates.append(template_name.replace("_template", "")) - entity_ids = MATCH_ALL - elif entity_ids != MATCH_ALL: - entity_ids |= set(template_entity_ids) - if invalid_templates: - _LOGGER.warning( - "Template sensor %s has no entity ids configured to track nor" - " were we able to extract the entities to track from the %s " - "template(s). This entity will only be able to be updated " - "manually.", - device, - ", ".join(invalid_templates), - ) - else: - if manual_entity_ids is None: - entity_ids = list(entity_ids) - else: - entity_ids = manual_entity_ids + + initialise_templates(hass, templates) + entity_ids = extract_entities( + device, "switch", device_config.get(ATTR_ENTITY_ID), templates + ) switches.append( SwitchTemplate( @@ -117,6 +91,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_ids, ) ) + if not switches: _LOGGER.error("No switches added") return False diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 6a6523514c489a..8201842e1312eb 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -43,7 +43,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script - +from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE _LOGGER = logging.getLogger(__name__) @@ -109,45 +109,15 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= fan_speed_list = device_config[CONF_FAN_SPEED_LIST] - entity_ids = set() - manual_entity_ids = device_config.get(CONF_ENTITY_ID) - invalid_templates = [] - - for tpl_name, template in ( - (CONF_VALUE_TEMPLATE, state_template), - (CONF_BATTERY_LEVEL_TEMPLATE, battery_level_template), - (CONF_FAN_SPEED_TEMPLATE, fan_speed_template), - (CONF_AVAILABILITY_TEMPLATE, availability_template), - ): - if template is None: - continue - template.hass = hass - - if manual_entity_ids is not None: - continue - - template_entity_ids = template.extract_entities() - if template_entity_ids == MATCH_ALL: - entity_ids = MATCH_ALL - # Cut off _template from name - invalid_templates.append(tpl_name[:-9]) - elif entity_ids != MATCH_ALL: - entity_ids |= set(template_entity_ids) - - if invalid_templates: - _LOGGER.warning( - "Template vacuum %s has no entity ids configured to track nor" - " were we able to extract the entities to track from the %s " - "template(s). This entity will only be able to be updated " - "manually.", - device, - ", ".join(invalid_templates), - ) + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_BATTERY_LEVEL_TEMPLATE: battery_level_template, + CONF_FAN_SPEED_TEMPLATE: fan_speed_template, + CONF_AVAILABILITY_TEMPLATE: availability_template, + } - if manual_entity_ids is not None: - entity_ids = manual_entity_ids - elif entity_ids != MATCH_ALL: - entity_ids = list(entity_ids) + initialise_templates(hass, templates) + entity_ids = extract_entities(device, "vacuum", None, templates) vacuums.append( TemplateVacuum( diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index 143811da2099e5..b17b98f2f1093f 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -576,22 +576,22 @@ async def test_no_update_template_match_all(hass, caplog): await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 assert ( - "Template binary sensor all_state has no entity ids " + "Template binary sensor 'all_state' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the value template" ) in caplog.text assert ( - "Template binary sensor all_icon has no entity ids " + "Template binary sensor 'all_icon' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the icon template" ) in caplog.text assert ( - "Template binary sensor all_entity_picture has no entity ids " + "Template binary sensor 'all_entity_picture' has no entity ids " "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 " + "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 diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index d1d3020737549b..32a34411b33668 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -254,10 +254,9 @@ def test_no_template_match_all(self, caplog): assert state.state == lock.STATE_UNLOCKED assert ( - "Template lock 'Template Lock' has no entity ids configured " - "to track nor were we able to extract the entities to track " - "from the 'value_template' template. This entity will only " - "be able to be updated manually." + "Template lock 'Template Lock' has no entity ids configured to track " + "nor were we able to extract the entities to track from the value " + "template(s). This entity will only be able to be updated manually" ) in caplog.text self.hass.states.set("lock.template_lock", lock.STATE_LOCKED) @@ -343,7 +342,7 @@ async def test_available_template_with_entities(hass): { "lock": { "platform": "template", - "value_template": "{{ 'on' }}", + "value_template": "{{ states('switch.test_state') }}", "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, "unlock": { "service": "switch.turn_off", diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index b3813da176633c..7ae34b04c00016 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -377,6 +377,49 @@ def test_setup_valid_device_class(self): state = self.hass.states.get("sensor.test2") assert "device_class" not in state.attributes + def test_available_template_with_entities(self): + """Test availability tempalates with values from other entities.""" + + with assert_setup_component(1): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.test_state.state }}", + "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", + } + }, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + # When template returns true.. + self.hass.states.set("availability_boolean.state", STATE_ON) + self.hass.block_till_done() + + # Device State should not be unavailable + assert ( + self.hass.states.get("sensor.test_template_sensor").state + != STATE_UNAVAILABLE + ) + + # When Availability template returns false + self.hass.states.set("availability_boolean.state", STATE_OFF) + self.hass.block_till_done() + + # device state should be unavailable + assert ( + self.hass.states.get("sensor.test_template_sensor").state + == STATE_UNAVAILABLE + ) + async def test_available_template_with_entities(hass): """Test availability tempalates with values from other entities.""" @@ -511,27 +554,27 @@ async def test_no_template_match_all(hass, caplog): await hass.async_block_till_done() assert len(hass.states.async_all()) == 6 assert ( - "Template sensor invalid_state has no entity ids " + "Template sensor 'invalid_state' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the value template" ) in caplog.text assert ( - "Template sensor invalid_icon has no entity ids " + "Template sensor 'invalid_icon' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the icon template" ) in caplog.text assert ( - "Template sensor invalid_entity_picture has no entity ids " + "Template sensor 'invalid_entity_picture' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the entity_picture template" ) in caplog.text assert ( - "Template sensor invalid_friendly_name has no entity ids " + "Template sensor 'invalid_friendly_name' has no entity ids " "configured to track nor were we able to extract the entities to " "track from the friendly_name template" ) in caplog.text assert ( - "Template sensor invalid_attribute has no entity ids " + "Template sensor 'invalid_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 From 807de1aeb33776bd20826d0951b803c043592013 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 26 Nov 2019 00:32:24 +0000 Subject: [PATCH 1762/3953] [ci skip] Translation update --- .../components/climate/.translations/fr.json | 5 +++++ .../components/deconz/.translations/ro.json | 14 ++++++++++++++ .../geonetnz_volcano/.translations/ro.json | 13 +++++++++++++ homeassistant/components/hue/.translations/ro.json | 3 ++- 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/deconz/.translations/ro.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/ro.json diff --git a/homeassistant/components/climate/.translations/fr.json b/homeassistant/components/climate/.translations/fr.json index d82a3644493a09..db29f8424d5fc2 100644 --- a/homeassistant/components/climate/.translations/fr.json +++ b/homeassistant/components/climate/.translations/fr.json @@ -1,8 +1,13 @@ { "device_automation": { "action_type": { + "set_hvac_mode": "Changer le mode HVAC sur {entity_name}.", "set_preset_mode": "Changer les pr\u00e9r\u00e9glages de {entity_name}" }, + "condtion_type": { + "is_hvac_mode": "{entity_name} est d\u00e9fini sur un mode HVAC sp\u00e9cifique", + "is_preset_mode": "{entity_name} est d\u00e9fini sur un mode pr\u00e9d\u00e9fini sp\u00e9cifique" + }, "trigger_type": { "current_humidity_changed": "Changement d'humidit\u00e9 mesur\u00e9e pour {entity_name}", "current_temperature_changed": "Changement de temp\u00e9rature mesur\u00e9e pour {entity_name}", diff --git a/homeassistant/components/deconz/.translations/ro.json b/homeassistant/components/deconz/.translations/ro.json new file mode 100644 index 00000000000000..2d6fc6a39fbccb --- /dev/null +++ b/homeassistant/components/deconz/.translations/ro.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "port": "Port" + } + }, + "link": { + "description": "Debloca\u021bi gateway-ul DECONZ pentru a v\u0103 \u00eenregistra la Home Assistant. \n\n 1. Accesa\u021bi Set\u0103rile deCONZ - > Gateway - > Avansat \n 2. Ap\u0103sa\u021bi butonul \u201eAutentifica\u021bi aplica\u021bia\u201d" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ro.json b/homeassistant/components/geonetnz_volcano/.translations/ro.json new file mode 100644 index 00000000000000..4c0cd317d48be1 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ro.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "radius": "Raz\u0103" + }, + "title": "Completa\u021bi detaliile filtrului." + } + }, + "title": "Vulcanul GeoNet NZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ro.json b/homeassistant/components/hue/.translations/ro.json index a2ecf8964b61e3..9da771a52dcc77 100644 --- a/homeassistant/components/hue/.translations/ro.json +++ b/homeassistant/components/hue/.translations/ro.json @@ -19,6 +19,7 @@ "link": { "description": "Ap\u0103sa\u021bi butonul de pe pod pentru a \u00eenregistra Philips Hue cu Home Assistant. \n\n ! [Loca\u021bia butonului pe pod] (/ static / images / config_philips_hue.jpg)" } - } + }, + "title": "Philips Hue" } } \ No newline at end of file From f5c01cc30da96b5e03ab07b0811dbb42bffc93e2 Mon Sep 17 00:00:00 2001 From: Andrew Onyshchuk Date: Mon, 25 Nov 2019 18:32:37 -0600 Subject: [PATCH 1763/3953] Improve z-wave thermostat support (#27040) * Improve z-wave thermostat support Discover thermostat using COMMAND_CLASS_THERMOSTAT_MODE so that it is a single entitiy in case there are multiple setpoints. z-wave docs mention it is always present. Add support for single/range target temperature depending on the current thermostat mode. * Remove debug print * Refactor Z-Wave dynamic setpoint(s) selection - use explicit mapping between modes and setpoints as defined in Z-Wave specs - add tests for away (2 setpoints) and heat eco (1 setpoint) modes * Add non-standard thermostat mode aliases --- homeassistant/components/zwave/climate.py | 131 ++++-- .../components/zwave/discovery_schemas.py | 61 ++- tests/components/zwave/test_climate.py | 373 ++++++++++++++---- tests/components/zwave/test_init.py | 29 +- 4 files changed, 479 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index b40fff669589c6..e50908783283e3 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -2,6 +2,8 @@ # Because we do not compile openzwave on CI import logging +from typing import Optional + from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, @@ -17,18 +19,23 @@ HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, + PRESET_AWAY, PRESET_BOOST, PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_PRESET_MODE, + ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_HIGH, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ZWaveDeviceEntity _LOGGER = logging.getLogger(__name__) @@ -66,6 +73,33 @@ "auto changeover": HVAC_MODE_HEAT_COOL, } +MODE_SETPOINT_MAPPINGS = { + "off": (), + "heat": ("setpoint_heating",), + "cool": ("setpoint_cooling",), + "auto": ("setpoint_heating", "setpoint_cooling"), + "aux heat": ("setpoint_heating",), + "furnace": ("setpoint_furnace",), + "dry air": ("setpoint_dry_air",), + "moist air": ("setpoint_moist_air",), + "auto changeover": ("setpoint_auto_changeover",), + "heat econ": ("setpoint_eco_heating",), + "cool econ": ("setpoint_eco_cooling",), + "away": ("setpoint_away_heating", "setpoint_away_cooling"), + "full power": ("setpoint_full_power",), + # aliases found in xml configs + "comfort": ("setpoint_heating",), + "heat mode": ("setpoint_heating",), + "heat (default)": ("setpoint_heating",), + "dry floor": ("setpoint_dry_air",), + "heat eco": ("setpoint_eco_heating",), + "energy saving": ("setpoint_eco_heating",), + "energy heat": ("setpoint_eco_heating",), + "vacation": ("setpoint_away_heating", "setpoint_away_cooling"), + # for tests + "heat_cool": ("setpoint_heating", "setpoint_cooling"), +} + HVAC_CURRENT_MAPPINGS = { "idle": CURRENT_HVAC_IDLE, "heat": CURRENT_HVAC_HEAT, @@ -80,6 +114,7 @@ } PRESET_MAPPINGS = { + "away": PRESET_AWAY, "full power": PRESET_BOOST, "manufacturer specific": PRESET_MANUFACTURER_SPECIFIC, } @@ -124,6 +159,7 @@ def __init__(self, values, temp_unit): """Initialize the Z-Wave climate device.""" ZWaveDeviceEntity.__init__(self, values, DOMAIN) self._target_temperature = None + self._target_temperature_range = (None, None) self._current_temperature = None self._hvac_action = None self._hvac_list = None # [zwave_mode] @@ -154,10 +190,20 @@ def __init__(self, values, temp_unit): self._zxt_120 = 1 self.update_properties() + def _current_mode_setpoints(self): + current_mode = str(self.values.primary.data).lower() + setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) + return tuple(getattr(self.values, name, None) for name in setpoints_names) + @property def supported_features(self): """Return the list of supported features.""" support = SUPPORT_TARGET_TEMPERATURE + if HVAC_MODE_HEAT_COOL in self._hvac_list: + support |= SUPPORT_TARGET_TEMPERATURE_RANGE + if PRESET_AWAY in self._preset_list: + support |= SUPPORT_TARGET_TEMPERATURE_RANGE + if self.values.fan_mode: support |= SUPPORT_FAN_MODE if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: @@ -193,13 +239,13 @@ def update_properties(self): def _update_operation_mode(self): """Update hvac and preset modes.""" - if self.values.mode: + if self.values.primary: self._hvac_list = [] self._hvac_mapping = {} self._preset_list = [] self._preset_mapping = {} - mode_list = self.values.mode.data_items + mode_list = self.values.primary.data_items if mode_list: for mode in mode_list: ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) @@ -227,7 +273,7 @@ def _update_operation_mode(self): # Presets are supported self._preset_list.append(PRESET_NONE) - current_mode = self.values.mode.data + current_mode = self.values.primary.data _LOGGER.debug("current_mode=%s", current_mode) _hvac_temp = next( ( @@ -313,15 +359,21 @@ def _update_swing_mode(self): def _update_target_temp(self): """Update target temperature.""" - if self.values.primary.data == 0: - _LOGGER.debug( - "Setpoint is 0, setting default to " "current_temperature=%s", - self._current_temperature, - ) - if self._current_temperature is not None: - self._target_temperature = round((float(self._current_temperature)), 1) - else: - self._target_temperature = round((float(self.values.primary.data)), 1) + current_setpoints = self._current_mode_setpoints() + self._target_temperature = None + self._target_temperature_range = (None, None) + if len(current_setpoints) == 1: + (setpoint,) = current_setpoints + if setpoint is not None: + self._target_temperature = round((float(setpoint.data)), 1) + elif len(current_setpoints) == 2: + (setpoint_low, setpoint_high) = current_setpoints + target_low, target_high = None, None + if setpoint_low is not None: + target_low = round((float(setpoint_low.data)), 1) + if setpoint_high is not None: + target_high = round((float(setpoint_high.data)), 1) + self._target_temperature_range = (target_low, target_high) def _update_operating_state(self): """Update operating state.""" @@ -374,7 +426,7 @@ def hvac_mode(self): Need to be one of HVAC_MODE_*. """ - if self.values.mode: + if self.values.primary: return self._hvac_mode return self._default_hvac_mode @@ -384,7 +436,7 @@ def hvac_modes(self): Need to be a subset of HVAC_MODES. """ - if self.values.mode: + if self.values.primary: return self._hvac_list return [] @@ -401,7 +453,7 @@ 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: + if self.values.primary.data == AUX_HEAT_ZWAVE_MODE: return True return False @@ -411,7 +463,7 @@ def preset_mode(self): Need to be one of PRESET_*. """ - if self.values.mode: + if self.values.primary: return self._preset_mode return PRESET_NONE @@ -421,7 +473,7 @@ def preset_modes(self): Need to be a subset of PRESET_MODES. """ - if self.values.mode: + if self.values.primary: return self._preset_list return [] @@ -430,12 +482,35 @@ def target_temperature(self): """Return the temperature we try to reach.""" return self._target_temperature + @property + def target_temperature_low(self) -> Optional[float]: + """Return the lowbound target temperature we try to reach.""" + return self._target_temperature_range[0] + + @property + def target_temperature_high(self) -> Optional[float]: + """Return the highbound target temperature we try to reach.""" + return self._target_temperature_range[1] + 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) + current_setpoints = self._current_mode_setpoints() + if len(current_setpoints) == 1: + (setpoint,) = current_setpoints + target_temp = kwargs.get(ATTR_TEMPERATURE) + if setpoint is not None and target_temp is not None: + _LOGGER.debug("Set temperature to %s", target_temp) + setpoint.data = target_temp + elif len(current_setpoints) == 2: + (setpoint_low, setpoint_high) = current_setpoints + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if setpoint_low is not None and target_temp_low is not None: + _LOGGER.debug("Set low temperature to %s", target_temp_low) + setpoint_low.data = target_temp_low + if setpoint_high is not None and target_temp_high is not None: + _LOGGER.debug("Set high temperature to %s", target_temp_high) + setpoint_high.data = target_temp_high def set_fan_mode(self, fan_mode): """Set new target fan mode.""" @@ -447,11 +522,11 @@ def set_fan_mode(self, 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: + if not self.values.primary: return operation_mode = self._hvac_mapping.get(hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.mode.data = operation_mode + self.values.primary.data = operation_mode def turn_aux_heat_on(self): """Turn auxillary heater on.""" @@ -459,7 +534,7 @@ def turn_aux_heat_on(self): 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 + self.values.primary.data = operation_mode def turn_aux_heat_off(self): """Turn auxillary heater off.""" @@ -470,23 +545,23 @@ def turn_aux_heat_off(self): 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 + self.values.primary.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: + if not self.values.primary: return if preset_mode == PRESET_NONE: # Activate the current hvac mode self._update_operation_mode() operation_mode = self._hvac_mapping.get(self.hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.mode.data = operation_mode + self.values.primary.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 + self.values.primary.data = operation_mode def set_swing_mode(self, swing_mode): """Set new target swing mode.""" diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index e2254073290e86..2d6f08169eafcf 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -57,17 +57,68 @@ DEFAULT_VALUES_SCHEMA, **{ const.DISC_PRIMARY: { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT] + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE] + }, + "setpoint_heating": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [1], + const.DISC_OPTIONAL: True, + }, + "setpoint_cooling": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [2], + const.DISC_OPTIONAL: True, + }, + "setpoint_furnace": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [7], + const.DISC_OPTIONAL: True, + }, + "setpoint_dry_air": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [8], + const.DISC_OPTIONAL: True, + }, + "setpoint_moist_air": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [9], + const.DISC_OPTIONAL: True, + }, + "setpoint_auto_changeover": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [10], + const.DISC_OPTIONAL: True, + }, + "setpoint_eco_heating": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [11], + const.DISC_OPTIONAL: True, + }, + "setpoint_eco_cooling": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [12], + const.DISC_OPTIONAL: True, + }, + "setpoint_away_heating": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [13], + const.DISC_OPTIONAL: True, + }, + "setpoint_away_cooling": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [14], + const.DISC_OPTIONAL: True, + }, + "setpoint_full_power": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT], + const.DISC_INDEX: [15], + const.DISC_OPTIONAL: True, }, "temperature": { const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE], const.DISC_OPTIONAL: True, }, - "mode": { - const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE], - const.DISC_OPTIONAL: True, - }, "fan_mode": { const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE], const.DISC_OPTIONAL: True, diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 2f13d95fb9f03d..c9fe123af82c8a 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -9,6 +9,7 @@ HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + PRESET_AWAY, PRESET_BOOST, PRESET_ECO, PRESET_NONE, @@ -16,6 +17,9 @@ SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_HIGH, ) from homeassistant.components.zwave import climate from homeassistant.components.zwave.climate import DEFAULT_HVAC_MODES @@ -29,9 +33,7 @@ def device(hass, mock_openzwave): """Fixture to provide a precreated climate device.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -41,6 +43,9 @@ def device(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_action=MockValue(data=7, node=node), @@ -56,9 +61,7 @@ def device_zxt_120(hass, mock_openzwave): node = MockNode(manufacturer_id="5254", product_id="8377") values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -68,6 +71,9 @@ def device_zxt_120(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), fan_action=MockValue(data=7, node=node), @@ -83,13 +89,14 @@ def device_mapping(hass, mock_openzwave): """Fixture to provide a precreated climate device. Test state mapping.""" node = MockNode() values = MockEntityValues( - primary=MockValue(data=1, node=node), - temperature=MockValue(data=5, node=node, units=None), - mode=MockValue( + primary=MockValue( data="Heat", - data_items=["Off", "Cool", "Heat", "Full Power", "heat_cool"], + data_items=["Off", "Cool", "Heat", "Full Power", "Auto"], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="heating", node=node), fan_action=MockValue(data=7, node=node), @@ -104,13 +111,14 @@ 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( + primary=MockValue( data="Heat", data_items=["Off", "Cool", "Heat", "heat_cool", "Abcdefg"], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), fan_action=MockValue(data=7, node=node), @@ -125,9 +133,7 @@ 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( + primary=MockValue( data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -138,6 +144,88 @@ def device_heat_cool(hass, mock_openzwave): ], node=node, ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_cool_range(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Target range mode.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + data=HVAC_MODE_HEAT_COOL, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + ], + node=node, + ), + setpoint_heating=MockValue(data=1, node=node), + setpoint_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_cool_away(hass, mock_openzwave): + """Fixture to provide a precreated climate device. Target range mode.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + data=HVAC_MODE_HEAT_COOL, + data_items=[ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + PRESET_AWAY, + ], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_cooling=MockValue(data=9, node=node), + setpoint_away_heating=MockValue(data=1, node=node), + setpoint_away_cooling=MockValue(data=10, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_heat_eco(hass, mock_openzwave): + """Fixture to provide a precreated climate device. heat/heat eco.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "heat econ"], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_eco_heating=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), fan_action=MockValue(data=7, node=node), @@ -155,7 +243,23 @@ def test_default_hvac_modes(): def test_supported_features(device): """Test supported features flags.""" - assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + ) + + +def test_supported_features_temp_range(device_heat_cool_range): + """Test supported features flags with target temp range.""" + device = device_heat_cool_range + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + ) def test_supported_features_preset_mode(device_mapping): @@ -163,7 +267,10 @@ def test_supported_features_preset_mode(device_mapping): device = device_mapping assert ( device.supported_features - == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_PRESET_MODE + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_PRESET_MODE ) @@ -172,7 +279,10 @@ def test_supported_features_swing_mode(device_zxt_120): device = device_zxt_120 assert ( device.supported_features - == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_SWING_MODE + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_SWING_MODE ) @@ -207,14 +317,6 @@ def test_temperature_unit(device): assert device.temperature_unit == TEMP_CELSIUS -def test_default_target_temperature(device): - """Test default setting of target temperature.""" - assert device.target_temperature == 1 - device.values.primary.data = 0 - value_changed(device.values.primary) - assert device.target_temperature == 5 # Current Temperature - - def test_data_lists(device): """Test data lists from zwave value items.""" assert device.fan_modes == [3, 4, 5] @@ -225,7 +327,7 @@ def test_data_lists(device): HVAC_MODE_HEAT_COOL, ] assert device.preset_modes == [] - device.values.mode = None + device.values.primary = None assert device.preset_modes == [] @@ -234,71 +336,126 @@ def test_data_lists_mapping(device_mapping): device = device_mapping assert device.hvac_modes == ["off", "cool", "heat", "heat_cool"] assert device.preset_modes == ["boost", "none"] - device.values.mode = None + device.values.primary = None assert device.preset_modes == [] def test_target_value_set(device): """Test values changed for climate device.""" - assert device.values.primary.data == 1 + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 device.set_temperature() - assert device.values.primary.data == 1 + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 device.set_temperature(**{ATTR_TEMPERATURE: 2}) - assert device.values.primary.data == 2 + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_hvac_mode(HVAC_MODE_COOL) + value_changed(device.values.primary) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TEMPERATURE: 9}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + + +def test_target_value_set_range(device_heat_cool_range): + """Test values changed for climate device.""" + device = device_heat_cool_range + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature() + assert device.values.setpoint_heating.data == 1 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 2}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 10 + device.set_temperature(**{ATTR_TARGET_TEMP_HIGH: 9}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 3, ATTR_TARGET_TEMP_HIGH: 8}) + assert device.values.setpoint_heating.data == 3 + assert device.values.setpoint_cooling.data == 8 + + +def test_target_value_set_range_away(device_heat_cool_away): + """Test values changed for climate device.""" + device = device_heat_cool_away + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + assert device.values.setpoint_away_heating.data == 1 + assert device.values.setpoint_away_cooling.data == 10 + device.set_preset_mode(PRESET_AWAY) + device.set_temperature(**{ATTR_TARGET_TEMP_LOW: 0, ATTR_TARGET_TEMP_HIGH: 11}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_cooling.data == 9 + assert device.values.setpoint_away_heating.data == 0 + assert device.values.setpoint_away_cooling.data == 11 + + +def test_target_value_set_eco(device_heat_eco): + """Test values changed for climate device.""" + device = device_heat_eco + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_eco_heating.data == 1 + device.set_preset_mode("heat econ") + device.set_temperature(**{ATTR_TEMPERATURE: 0}) + assert device.values.setpoint_heating.data == 2 + assert device.values.setpoint_eco_heating.data == 0 def test_operation_value_set(device): """Test values changed for climate device.""" - assert device.values.mode.data == HVAC_MODE_HEAT + assert device.values.primary.data == HVAC_MODE_HEAT device.set_hvac_mode(HVAC_MODE_COOL) - assert device.values.mode.data == HVAC_MODE_COOL + assert device.values.primary.data == HVAC_MODE_COOL device.set_preset_mode(PRESET_ECO) - assert device.values.mode.data == PRESET_ECO + assert device.values.primary.data == PRESET_ECO device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT_COOL - device.values.mode = None + assert device.values.primary.data == HVAC_MODE_HEAT_COOL + device.values.primary = None device.set_hvac_mode("test_set_failes") - assert device.values.mode is None + assert device.values.primary is None device.set_preset_mode("test_set_failes") - assert device.values.mode is None + assert device.values.primary 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 == "Heat" + assert device.values.primary.data == "Heat" device.set_hvac_mode(HVAC_MODE_COOL) - assert device.values.mode.data == "Cool" + assert device.values.primary.data == "Cool" device.set_hvac_mode(HVAC_MODE_OFF) - assert device.values.mode.data == "Off" + assert device.values.primary.data == "Off" device.set_preset_mode(PRESET_BOOST) - assert device.values.mode.data == "Full Power" + assert device.values.primary.data == "Full Power" device.set_preset_mode(PRESET_ECO) - assert device.values.mode.data == "eco" + assert device.values.primary.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" + assert device.values.primary.data == "Heat" device.set_preset_mode("Abcdefg") - assert device.values.mode.data == "Abcdefg" + assert device.values.primary.data == "Abcdefg" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT_COOL + assert device.values.primary.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 + assert device.values.primary.data == HVAC_MODE_HEAT device.set_preset_mode("Heat Eco") - assert device.values.mode.data == "Heat Eco" + assert device.values.primary.data == "Heat Eco" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_HEAT + assert device.values.primary.data == HVAC_MODE_HEAT device.set_preset_mode("Cool Eco") - assert device.values.mode.data == "Cool Eco" + assert device.values.primary.data == "Cool Eco" device.set_preset_mode(PRESET_NONE) - assert device.values.mode.data == HVAC_MODE_COOL + assert device.values.primary.data == HVAC_MODE_COOL def test_fan_mode_value_set(device): @@ -314,11 +471,81 @@ def test_fan_mode_value_set(device): def test_target_value_changed(device): """Test values changed for climate device.""" assert device.target_temperature == 1 - device.values.primary.data = 2 + device.values.setpoint_heating.data = 2 + value_changed(device.values.setpoint_heating) + assert device.target_temperature == 2 + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) + assert device.target_temperature == 10 + device.values.setpoint_cooling.data = 9 + value_changed(device.values.setpoint_cooling) + assert device.target_temperature == 9 + + +def test_target_range_changed(device_heat_cool_range): + """Test values changed for climate device.""" + device = device_heat_cool_range + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + device.values.setpoint_heating.data = 2 + value_changed(device.values.setpoint_heating) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 10 + device.values.setpoint_cooling.data = 9 + value_changed(device.values.setpoint_cooling) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + + +def test_target_changed_preset_range(device_heat_cool_away): + """Test values changed for climate device.""" + device = device_heat_cool_away + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + device.values.primary.data = PRESET_AWAY + value_changed(device.values.primary) + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + device.values.setpoint_away_heating.data = 0 + value_changed(device.values.setpoint_away_heating) + device.values.setpoint_away_cooling.data = 11 + value_changed(device.values.setpoint_away_cooling) + assert device.target_temperature_low == 0 + assert device.target_temperature_high == 11 + device.values.primary.data = HVAC_MODE_HEAT_COOL + value_changed(device.values.primary) + assert device.target_temperature_low == 2 + assert device.target_temperature_high == 9 + + +def test_target_changed_eco(device_heat_eco): + """Test values changed for climate device.""" + device = device_heat_eco + assert device.target_temperature == 2 + device.values.primary.data = "heat econ" + value_changed(device.values.primary) + assert device.target_temperature == 1 + device.values.setpoint_eco_heating.data = 0 + value_changed(device.values.setpoint_eco_heating) + assert device.target_temperature == 0 + device.values.primary.data = HVAC_MODE_HEAT value_changed(device.values.primary) assert device.target_temperature == 2 +def test_target_changed_with_mode(device): + """Test values changed for climate device.""" + assert device.hvac_mode == HVAC_MODE_HEAT + assert device.target_temperature == 1 + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) + assert device.target_temperature == 10 + device.values.primary.data = HVAC_MODE_HEAT_COOL + value_changed(device.values.primary) + assert device.target_temperature_low == 1 + assert device.target_temperature_high == 10 + + def test_temperature_value_changed(device): """Test values changed for climate device.""" assert device.current_temperature == 5 @@ -331,15 +558,15 @@ def test_operation_value_changed(device): """Test values changed for climate device.""" 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) + device.values.primary.data = HVAC_MODE_COOL + value_changed(device.values.primary) 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) + device.values.primary.data = HVAC_MODE_OFF + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_OFF assert device.preset_mode == PRESET_NONE - device.values.mode = None + device.values.primary = None assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_NONE @@ -349,8 +576,8 @@ def test_operation_value_changed_preset(device_mapping): 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) + device.values.primary.data = PRESET_ECO + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_ECO @@ -360,12 +587,12 @@ def test_operation_value_changed_mapping(device_mapping): device = device_mapping assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Off" - value_changed(device.values.mode) + device.values.primary.data = "Off" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_OFF assert device.preset_mode == PRESET_NONE - device.values.mode.data = "Cool" - value_changed(device.values.mode) + device.values.primary.data = "Cool" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_COOL assert device.preset_mode == PRESET_NONE @@ -375,11 +602,11 @@ def test_operation_value_changed_mapping_preset(device_mapping): 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) + device.values.primary.data = "Full Power" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_BOOST - device.values.mode = None + device.values.primary = None assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == PRESET_NONE @@ -389,8 +616,8 @@ def test_operation_value_changed_unknown(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) + device.values.primary.data = "Abcdefg" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT_COOL assert device.preset_mode == "Abcdefg" @@ -400,12 +627,12 @@ def test_operation_value_changed_heat_cool(device_heat_cool): 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) + device.values.primary.data = "Cool Eco" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_COOL assert device.preset_mode == "Cool Eco" - device.values.mode.data = "Heat Eco" - value_changed(device.values.mode) + device.values.primary.data = "Heat Eco" + value_changed(device.values.primary) assert device.hvac_mode == HVAC_MODE_HEAT assert device.preset_mode == "Heat Eco" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 1de69249bfeaad..7038d6b61147c9 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -574,18 +574,33 @@ def mock_connect(receiver, signal, *args, **kwargs): assert len(mock_receivers) == 1 node = MockNode(node_id=11, generic=const.GENERIC_TYPE_THERMOSTAT) - setpoint = MockValue( + thermostat_mode = MockValue( + data="Heat", + data_items=["Off", "Heat"], + node=node, + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + genre=const.GENRE_USER, + ) + setpoint_heating = MockValue( data=22.0, node=node, - index=12, - instance=13, command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, + index=1, genre=const.GENRE_USER, - units="C", ) - hass.async_add_job(mock_receivers[0], node, setpoint) + + hass.async_add_job(mock_receivers[0], node, thermostat_mode) await hass.async_block_till_done() + def mock_update(self): + self.hass.add_job(self.async_update_ha_state) + + with patch.object( + zwave.node_entity.ZWaveBaseEntity, "maybe_schedule_update", new=mock_update + ): + hass.async_add_job(mock_receivers[0], node, setpoint_heating) + await hass.async_block_till_done() + assert ( hass.states.get("climate.mock_node_mock_value").attributes["temperature"] == 22.0 @@ -597,9 +612,6 @@ def mock_connect(receiver, signal, *args, **kwargs): is None ) - def mock_update(self): - self.hass.add_job(self.async_update_ha_state) - with patch.object( zwave.node_entity.ZWaveBaseEntity, "maybe_schedule_update", new=mock_update ): @@ -607,7 +619,6 @@ def mock_update(self): data=23.5, node=node, index=1, - instance=13, command_class=const.COMMAND_CLASS_SENSOR_MULTILEVEL, genre=const.GENRE_USER, units="C", From 87de5db535523a187c80a9befc639855cdefa96d Mon Sep 17 00:00:00 2001 From: olijouve <17448560+olijouve@users.noreply.github.com> Date: Tue, 26 Nov 2019 02:56:17 +0100 Subject: [PATCH 1764/3953] Fix Onvif setup error: premature end of connection on GetStreamURI (#26781) * Fix Onvif setup error with a premature end of connection on GetStreamUri wsdl call Reconnect to onvif camera after getting profiles to fix this error : [homeassistant.components.onvif.camera] Retrieving stream uri [zeep.asyncio.transport] HTTP Post to http://192.168.1.15/onvif/Media: b'\n Date: Tue, 26 Nov 2019 03:00:58 +0100 Subject: [PATCH 1765/3953] Move esphome imports at top-level (#29064) --- .../components/esphome/binary_sensor.py | 3 --- homeassistant/components/esphome/climate.py | 2 +- .../components/esphome/config_flow.py | 5 +---- homeassistant/components/esphome/cover.py | 6 +----- .../components/esphome/entry_data.py | 10 +++++----- tests/components/esphome/test_config_flow.py | 20 ++++++++----------- 6 files changed, 16 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py index 64506f69283cfa..fe41bb2f7bb6fa 100644 --- a/homeassistant/components/esphome/binary_sensor.py +++ b/homeassistant/components/esphome/binary_sensor.py @@ -1,5 +1,4 @@ """Support for ESPHome binary sensors.""" -import logging from typing import Optional from aioesphomeapi import BinarySensorInfo, BinarySensorState @@ -8,8 +7,6 @@ from . import EsphomeEntity, platform_async_setup_entry -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, entry, async_add_entities): """Set up ESPHome binary sensors based on a config entry.""" diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 14f684f1e32974..960366a8332e76 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -20,8 +20,8 @@ CURRENT_HVAC_DRY, CURRENT_HVAC_FAN, CURRENT_HVAC_HEAT, - CURRENT_HVAC_OFF, CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, FAN_AUTO, FAN_DIFFUSE, FAN_FOCUS, diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 47c00f434635b6..53289799b439e1 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -2,6 +2,7 @@ from collections import OrderedDict from typing import Optional +from aioesphomeapi import APIClient, APIConnectionError import voluptuous as vol from homeassistant import config_entries @@ -147,8 +148,6 @@ async def async_step_authenticate(self, user_input=None, error=None): async def fetch_device_info(self): """Fetch device info from API and return any errors.""" - from aioesphomeapi import APIClient, APIConnectionError - cli = APIClient(self.hass.loop, self._host, self._port, "") try: @@ -165,8 +164,6 @@ async def fetch_device_info(self): async def try_login(self): """Try logging in to device and return any errors.""" - from aioesphomeapi import APIClient, APIConnectionError - cli = APIClient(self.hass.loop, self._host, self._port, self._password) try: diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 980fc936940620..53014991de80e4 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -2,7 +2,7 @@ import logging from typing import Optional -from aioesphomeapi import CoverInfo, CoverState +from aioesphomeapi import CoverInfo, CoverOperation, CoverState from homeassistant.components.cover import ( ATTR_POSITION, @@ -82,15 +82,11 @@ def is_closed(self) -> Optional[bool]: @esphome_state_property def is_opening(self) -> bool: """Return if the cover is opening or not.""" - from aioesphomeapi import CoverOperation - return self._state.current_operation == CoverOperation.IS_OPENING @esphome_state_property def is_closing(self) -> bool: """Return if the cover is closing or not.""" - from aioesphomeapi import CoverOperation - return self._state.current_operation == CoverOperation.IS_CLOSING @esphome_state_property diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index d916e1a90c8741..48f1aea2c2ddd0 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -1,22 +1,22 @@ """Runtime entry data for ESPHome stored in hass.data.""" import asyncio -from typing import Any, Callable, Dict, List, Optional, Tuple, Set +from typing import Any, Callable, Dict, List, Optional, Set, Tuple from aioesphomeapi import ( COMPONENT_TYPE_TO_INFO, - DeviceInfo, - EntityInfo, - EntityState, - UserService, BinarySensorInfo, CameraInfo, ClimateInfo, CoverInfo, + DeviceInfo, + EntityInfo, + EntityState, FanInfo, LightInfo, SensorInfo, SwitchInfo, TextSensorInfo, + UserService, ) import attr diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 8c05b274dd052a..4b951f9a369574 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -4,23 +4,17 @@ import pytest -from homeassistant.components.esphome import config_flow, DATA_KEY -from tests.common import mock_coro, MockConfigEntry - -MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) +from homeassistant.components.esphome import DATA_KEY, config_flow +from tests.common import MockConfigEntry, mock_coro -@pytest.fixture(autouse=True) -def aioesphomeapi_mock(): - """Mock aioesphomeapi.""" - with patch.dict("sys.modules", {"aioesphomeapi": MagicMock()}): - yield +MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) @pytest.fixture def mock_client(): """Mock APIClient.""" - with patch("aioesphomeapi.APIClient") as mock_client: + with patch("homeassistant.components.esphome.config_flow.APIClient") as mock_client: def mock_constructor(loop, host, port, password): """Fake the client constructor.""" @@ -40,7 +34,8 @@ def mock_constructor(loop, host, port, password): def mock_api_connection_error(): """Mock out the try login method.""" with patch( - "aioesphomeapi.APIConnectionError", new_callable=lambda: OSError + "homeassistant.components.esphome.config_flow.APIConnectionError", + new_callable=lambda: OSError, ) as mock_error: yield mock_error @@ -86,7 +81,8 @@ def __init__(self): super().__init__("Error resolving IP address") with patch( - "aioesphomeapi.APIConnectionError", new_callable=lambda: MockResolveError + "homeassistant.components.esphome.config_flow.APIConnectionError", + new_callable=lambda: MockResolveError, ) as exc: mock_client.device_info.side_effect = exc result = await flow.async_step_user( From cc346e57e608afd6af42f4c7da52ee6065fb7b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Tue, 26 Nov 2019 03:01:24 +0100 Subject: [PATCH 1766/3953] Cast the volume_level of a universal media_player to a float (#29045) * Make sure to cast the volume_level of a universal media_player to a float * Fix test that expects the wrong bahaviour * Catch exception instead of checking for None --- homeassistant/components/universal/media_player.py | 5 ++++- tests/components/universal/test_media_player.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 09771b551acd39..63e3ff7448d873 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -255,7 +255,10 @@ def state(self): @property def volume_level(self): """Volume level of entity specified in attributes or active child.""" - return self._override_or_child_attr(ATTR_MEDIA_VOLUME_LEVEL) + try: + return float(self._override_or_child_attr(ATTR_MEDIA_VOLUME_LEVEL)) + except (TypeError, ValueError): + return None @property def is_volume_muted(self): diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 67d826f576b5d7..22ec96c3a21702 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -539,10 +539,10 @@ def test_volume_level_children_and_attr(self): ump = universal.UniversalMediaPlayer(self.hass, **config) - assert "0" == ump.volume_level + assert 0 == ump.volume_level self.hass.states.set(self.mock_volume_id, 100) - assert "100" == ump.volume_level + assert 100 == ump.volume_level def test_is_volume_muted_children_and_attr(self): """Test is volume muted property w/ children and attrs.""" From cc255da038868d6096e36c5a97db0ec977b4acaa Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 03:01:48 +0100 Subject: [PATCH 1767/3953] Move imports to top for ecovacs (#29017) * Moved imports in ecovacs integration * Imported sucks once, reused it multiple times --- homeassistant/components/ecovacs/__init__.py | 3 +-- homeassistant/components/ecovacs/vacuum.py | 27 ++++++++------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ecovacs/__init__.py b/homeassistant/components/ecovacs/__init__.py index 76566912d12c99..964dd7a3f2ac4e 100644 --- a/homeassistant/components/ecovacs/__init__.py +++ b/homeassistant/components/ecovacs/__init__.py @@ -3,6 +3,7 @@ import random import string +from sucks import EcoVacsAPI, VacBot import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP @@ -44,8 +45,6 @@ def setup(hass, config): hass.data[ECOVACS_DEVICES] = [] - from sucks import EcoVacsAPI, VacBot - ecovacs_api = EcoVacsAPI( ECOVACS_API_DEVICEID, config[DOMAIN].get(CONF_USERNAME), diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index fdaf6291be515d..16a9d67bffcd7a 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -1,6 +1,8 @@ """Support for Ecovacs Ecovacs Vaccums.""" import logging +import sucks + from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, @@ -123,9 +125,8 @@ def status(self): def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - from sucks import Charge - self.device.run(Charge()) + self.device.run(sucks.Charge()) @property def battery_icon(self): @@ -150,15 +151,13 @@ def fan_speed(self): @property def fan_speed_list(self): """Get the list of available fan speed steps of the vacuum cleaner.""" - from sucks import FAN_SPEED_NORMAL, FAN_SPEED_HIGH - return [FAN_SPEED_NORMAL, FAN_SPEED_HIGH] + return [sucks.FAN_SPEED_NORMAL, sucks.FAN_SPEED_HIGH] def turn_on(self, **kwargs): """Turn the vacuum on and start cleaning.""" - from sucks import Clean - self.device.run(Clean()) + self.device.run(sucks.Clean()) def turn_off(self, **kwargs): """Turn the vacuum off stopping the cleaning and returning home.""" @@ -166,34 +165,28 @@ def turn_off(self, **kwargs): def stop(self, **kwargs): """Stop the vacuum cleaner.""" - from sucks import Stop - self.device.run(Stop()) + self.device.run(sucks.Stop()) def clean_spot(self, **kwargs): """Perform a spot clean-up.""" - from sucks import Spot - self.device.run(Spot()) + self.device.run(sucks.Spot()) def locate(self, **kwargs): """Locate the vacuum cleaner.""" - from sucks import PlaySound - self.device.run(PlaySound()) + self.device.run(sucks.PlaySound()) def set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" if self.is_on: - from sucks import Clean - self.device.run(Clean(mode=self.device.clean_status, speed=fan_speed)) + self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed)) def send_command(self, command, params=None, **kwargs): """Send a command to a vacuum cleaner.""" - from sucks import VacBotCommand - - self.device.run(VacBotCommand(command, params)) + self.device.run(sucks.VacBotCommand(command, params)) @property def device_state_attributes(self): From db0008e62c8dd53383684c9803a48c03bec69917 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Tue, 26 Nov 2019 03:39:56 +0000 Subject: [PATCH 1768/3953] Allow templates in rest_command headers (#26099) --- .../components/rest_command/__init__.py | 26 +++++++++++---- tests/components/rest_command/test_init.py | 32 +++++++++++++++++-- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 10f37b6ac4c95b..7dfbb964167e60 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -37,7 +37,7 @@ vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.All( vol.Lower, vol.In(SUPPORT_REST_METHODS) ), - vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), + vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.template}), vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, vol.Optional(CONF_PAYLOAD): cv.template, @@ -75,15 +75,15 @@ def async_register_rest_command(name, command_config): template_payload = command_config[CONF_PAYLOAD] template_payload.hass = hass - headers = None + template_headers = None if CONF_HEADERS in command_config: - headers = command_config[CONF_HEADERS] + template_headers = command_config[CONF_HEADERS] + for template_header in template_headers.values(): + template_header.hass = hass + content_type = None if CONF_CONTENT_TYPE in command_config: content_type = command_config[CONF_CONTENT_TYPE] - if headers is None: - headers = {} - headers[hdrs.CONTENT_TYPE] = content_type async def async_service_handler(service): """Execute a shell command service.""" @@ -94,6 +94,20 @@ async def async_service_handler(service): ) request_url = template_url.async_render(variables=service.data) + + headers = None + if template_headers: + headers = {} + for header_name, template_header in template_headers.items(): + headers[header_name] = template_header.async_render( + variables=service.data + ) + + if content_type: + if headers is None: + headers = {} + headers[hdrs.CONTENT_TYPE] = content_type + try: async with getattr(websession, method)( request_url, diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index b7ac5a4be8a282..ba63091041d6cd 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -236,6 +236,19 @@ def test_rest_command_headers(self, aioclient_mock): }, "content_type": "text/plain", }, + "headers_template_test": { + "headers": { + "Accept": "application/json", + "User-Agent": "Mozilla/{{ 3 + 2 }}.0", + } + }, + "headers_and_content_type_override_template_test": { + "headers": { + "Accept": "application/{{ 1 + 1 }}json", + aiohttp.hdrs.CONTENT_TYPE: "application/pdf", + }, + "content_type": "text/json", + }, } } @@ -245,7 +258,7 @@ def test_rest_command_headers(self, aioclient_mock): {"url": self.url, "method": "post", "payload": "test data"} ) - with assert_setup_component(5): + with assert_setup_component(7): setup_component(self.hass, rc.DOMAIN, header_config_variations) # provide post request data @@ -257,11 +270,13 @@ def test_rest_command_headers(self, aioclient_mock): "headers_test", "headers_and_content_type_test", "headers_and_content_type_override_test", + "headers_template_test", + "headers_and_content_type_override_template_test", ]: self.hass.services.call(rc.DOMAIN, test_service, {}) self.hass.block_till_done() - assert len(aioclient_mock.mock_calls) == 5 + assert len(aioclient_mock.mock_calls) == 7 # no_headers_test assert aioclient_mock.mock_calls[0][3] is None @@ -293,3 +308,16 @@ def test_rest_command_headers(self, aioclient_mock): == "text/plain" ) assert aioclient_mock.mock_calls[4][3].get("Accept") == "application/json" + + # headers_template_test + assert len(aioclient_mock.mock_calls[5][3]) == 2 + assert aioclient_mock.mock_calls[5][3].get("Accept") == "application/json" + assert aioclient_mock.mock_calls[5][3].get("User-Agent") == "Mozilla/5.0" + + # headers_and_content_type_override_template_test + assert len(aioclient_mock.mock_calls[6][3]) == 2 + assert ( + aioclient_mock.mock_calls[6][3].get(aiohttp.hdrs.CONTENT_TYPE) + == "text/json" + ) + assert aioclient_mock.mock_calls[6][3].get("Accept") == "application/2json" From 5f4fc271d47743a44ff5c864232935b300c4b887 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 05:27:51 +0100 Subject: [PATCH 1769/3953] Fixed CONFIG_SCHEMA and getting data from the config (#28985) --- homeassistant/components/dovado/__init__.py | 23 +++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index a13c49cc61a1ae..03f12314c5af18 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -21,11 +21,16 @@ CONFIG_SCHEMA = vol.Schema( { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT): cv.port, - } + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + } + ) + }, + extra=vol.ALLOW_EXTRA, ) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) @@ -36,10 +41,10 @@ def setup(hass, config): hass.data[DOMAIN] = DovadoData( dovado.Dovado( - config[CONF_USERNAME], - config[CONF_PASSWORD], - config.get(CONF_HOST), - config.get(CONF_PORT), + config[DOMAIN].get(CONF_USERNAME), + config[DOMAIN].get(CONF_PASSWORD), + config[DOMAIN].get(CONF_HOST), + config[DOMAIN].get(CONF_PORT), ) ) return True From 4881bc04d802b1d130dcbc3cba55c08e3cc298ad Mon Sep 17 00:00:00 2001 From: Dmitry Krasnoukhov Date: Tue, 26 Nov 2019 06:53:37 +0200 Subject: [PATCH 1770/3953] Allow to change MQTT climate hold mode (#28988) --- homeassistant/components/mqtt/climate.py | 12 +++++++----- tests/components/mqtt/test_climate.py | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 4b163c523fa6d6..9b46057a414e1a 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -756,12 +756,14 @@ async def async_set_preset_mode(self, preset_mode): if self._away: optimistic_update = optimistic_update or self._set_away_mode(False) elif preset_mode == PRESET_AWAY: + if self._hold: + self._set_hold_mode(None) optimistic_update = optimistic_update or self._set_away_mode(True) - - if self._hold: - optimistic_update = optimistic_update or self._set_hold_mode(None) - elif preset_mode not in (None, PRESET_AWAY): - optimistic_update = optimistic_update or self._set_hold_mode(preset_mode) + else: + hold_mode = preset_mode + if preset_mode == PRESET_NONE: + hold_mode = None + optimistic_update = optimistic_update or self._set_hold_mode(hold_mode) if optimistic_update: self.async_write_ha_state() diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 3f4fc657186483..648448a6494b75 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -23,6 +23,7 @@ HVAC_MODE_FAN_ONLY, SUPPORT_TARGET_TEMPERATURE_RANGE, PRESET_NONE, + PRESET_ECO, ) from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE @@ -446,6 +447,19 @@ async def test_set_away_mode(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") is None + await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) + mqtt_mock.async_publish.reset_mock() + + await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_has_calls( + [ + unittest.mock.call("hold-topic", "off", 0, False), + unittest.mock.call("away-mode-topic", "AN", 0, False), + ] + ) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == "away" + async def test_set_hvac_action(hass, mqtt_mock): """Test setting of the HVAC action.""" @@ -495,6 +509,12 @@ async def test_set_hold(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "hold-on" + await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with("hold-topic", "eco", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == PRESET_ECO + await common.async_set_preset_mode(hass, PRESET_NONE, ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with("hold-topic", "off", 0, False) state = hass.states.get(ENTITY_CLIMATE) From 6e7b5b71f5ff35dee96713e84f6faf06b004925d Mon Sep 17 00:00:00 2001 From: Adam Cheng <52572642+adamchengtkc@users.noreply.github.com> Date: Tue, 26 Nov 2019 05:59:01 +0100 Subject: [PATCH 1771/3953] Add scale and offset to temperature values to Modbus Climate (#27045) * Add scale and offset to temperature values * fix (no-else-return) * Update manifest.json * Added codeowners --- CODEOWNERS | 1 + homeassistant/components/modbus/climate.py | 80 +++++++++++++++++-- homeassistant/components/modbus/manifest.json | 4 +- 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7e011fe46d6ab2..c9ef0123b22d73 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -198,6 +198,7 @@ homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 +homeassistant/components/modbus/* @adamchengtkc homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 64b45b03c95994..c6764482d9628a 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -7,9 +7,15 @@ from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, - HVAC_MODE_HEAT, + HVAC_MODE_AUTO, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + CONF_SLAVE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) -from homeassistant.const import ATTR_TEMPERATURE, CONF_NAME, CONF_SLAVE import homeassistant.helpers.config_validation as cv from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN @@ -21,12 +27,17 @@ CONF_DATA_TYPE = "data_type" CONF_COUNT = "data_count" CONF_PRECISION = "precision" - +CONF_SCALE = "scale" +CONF_OFFSET = "offset" +CONF_UNIT = "temperature_unit" DATA_TYPE_INT = "int" DATA_TYPE_UINT = "uint" DATA_TYPE_FLOAT = "float" +CONF_MAX_TEMP = "max_temp" +CONF_MIN_TEMP = "min_temp" +CONF_STEP = "temp_step" SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE -HVAC_MODES = [HVAC_MODE_HEAT] +HVAC_MODES = [HVAC_MODE_AUTO] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -40,6 +51,12 @@ ), vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_PRECISION, default=1): cv.positive_int, + vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), + vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), + vol.Optional(CONF_MAX_TEMP, default=5): cv.positive_int, + vol.Optional(CONF_MIN_TEMP, default=35): cv.positive_int, + vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float), + vol.Optional(CONF_UNIT, default="C"): cv.string, } ) @@ -53,6 +70,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data_type = config.get(CONF_DATA_TYPE) count = config.get(CONF_COUNT) precision = config.get(CONF_PRECISION) + scale = config.get(CONF_SCALE) + offset = config.get(CONF_OFFSET) + unit = config.get(CONF_UNIT) + max_temp = config.get(CONF_MAX_TEMP) + min_temp = config.get(CONF_MIN_TEMP) + temp_step = config.get(CONF_STEP) hub_name = config.get(CONF_HUB) hub = hass.data[MODBUS_DOMAIN][hub_name] @@ -67,6 +90,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data_type, count, precision, + scale, + offset, + unit, + max_temp, + min_temp, + temp_step, ) ], True, @@ -86,6 +115,12 @@ def __init__( data_type, count, precision, + scale, + offset, + unit, + max_temp, + min_temp, + temp_step, ): """Initialize the unit.""" self._hub = hub @@ -98,6 +133,12 @@ def __init__( self._data_type = data_type self._count = int(count) self._precision = precision + self._scale = scale + self._offset = offset + self._unit = unit + self._max_temp = max_temp + self._min_temp = min_temp + self._temp_step = temp_step self._structure = ">f" data_types = { @@ -123,7 +164,7 @@ def update(self): @property def hvac_mode(self): """Return the current HVAC mode.""" - return HVAC_MODE_HEAT + return HVAC_MODE_AUTO @property def hvac_modes(self): @@ -145,9 +186,31 @@ def target_temperature(self): """Return the target temperature.""" return self._target_temperature + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT if self._unit == "F" else TEMP_CELSIUS + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._min_temp + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._max_temp + + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + return self._temp_step + def set_temperature(self, **kwargs): """Set new target temperature.""" - target_temperature = kwargs.get(ATTR_TEMPERATURE) + target_temperature = int( + (kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale + ) if target_temperature is None: return byte_string = struct.pack(self._structure, target_temperature) @@ -170,7 +233,10 @@ def read_register(self, register): [x.to_bytes(2, byteorder="big") for x in result.registers] ) val = struct.unpack(self._structure, byte_string)[0] - register_value = format(val, f".{self._precision}f") + register_value = format( + (self._scale * val) + self._offset, f".{self._precision}f" + ) + register_value = float(register_value) return register_value def write_register(self, register, value): diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 8d271d5a95f106..356a9f6a9c0035 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,7 @@ "pymodbus==1.5.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@adamchengtkc" + ] } From 72ccc836516ba561e8cfd6382fb5837d9ed5324d Mon Sep 17 00:00:00 2001 From: Emeric <15244869+Mryck@users.noreply.github.com> Date: Tue, 26 Nov 2019 06:34:13 +0100 Subject: [PATCH 1772/3953] Add uptime and number of reboot for bbox sensor (#28880) * Add uptime and reboot for bbox sensor * Use timestamp for uptime * Fix black format --- homeassistant/components/bbox/sensor.py | 67 ++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index ad6bcc39796e53..7b795a8788e4ee 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -9,9 +9,15 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_MONITORED_VARIABLES, ATTR_ATTRIBUTION +from homeassistant.const import ( + CONF_NAME, + CONF_MONITORED_VARIABLES, + ATTR_ATTRIBUTION, + DEVICE_CLASS_TIMESTAMP, +) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -45,6 +51,8 @@ BANDWIDTH_MEGABITS_SECONDS, "mdi:upload", ], + "uptime": ["Uptime", None, "mdi:clock"], + "number_of_reboots": ["Number of reboot", None, "mdi:restart"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -72,11 +80,61 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for variable in config[CONF_MONITORED_VARIABLES]: - sensors.append(BboxSensor(bbox_data, variable, name)) + if variable == "uptime": + sensors.append(BboxUptimeSensor(bbox_data, variable, name)) + else: + sensors.append(BboxSensor(bbox_data, variable, name)) add_entities(sensors, True) +class BboxUptimeSensor(Entity): + """Bbox uptime sensor.""" + + def __init__(self, bbox_data, sensor_type, name): + """Initialize the sensor.""" + self.client_name = name + self.type = sensor_type + self._name = SENSOR_TYPES[sensor_type][0] + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._icon = SENSOR_TYPES[sensor_type][2] + self.bbox_data = bbox_data + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.client_name} {self._name}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_class(self): + """Return the class of this sensor.""" + return DEVICE_CLASS_TIMESTAMP + + def update(self): + """Get the latest data from Bbox and update the state.""" + self.bbox_data.update() + uptime = utcnow() - timedelta( + seconds=self.bbox_data.router_infos["device"]["uptime"] + ) + self._state = uptime.replace(microsecond=0).isoformat() + + class BboxSensor(Entity): """Implementation of a Bbox sensor.""" @@ -126,6 +184,8 @@ def update(self): self._state = round(self.bbox_data.data["rx"]["bandwidth"] / 1000, 2) elif self.type == "current_up_bandwidth": self._state = round(self.bbox_data.data["tx"]["bandwidth"] / 1000, 2) + elif self.type == "number_of_reboots": + self._state = self.bbox_data.router_infos["device"]["numberofboots"] class BboxData: @@ -134,6 +194,7 @@ class BboxData: def __init__(self): """Initialize the data object.""" self.data = None + self.router_infos = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -142,7 +203,9 @@ def update(self): try: box = pybbox.Bbox() self.data = box.get_ip_stats() + self.router_infos = box.get_bbox_info() except requests.exceptions.HTTPError as error: _LOGGER.error(error) self.data = None + self.router_infos = None return False From 1f72de108c49fd09ab3952fc09d868d92209ef44 Mon Sep 17 00:00:00 2001 From: Shulyaka Date: Tue, 26 Nov 2019 09:12:20 +0300 Subject: [PATCH 1773/3953] google_assistant: support for humidity sensors (#28695) --- .../components/google_assistant/const.py | 1 + .../components/google_assistant/trait.py | 51 +++++++++++++++++++ .../components/google_assistant/test_trait.py | 26 ++++++++++ 3 files changed, 78 insertions(+) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 03253e244fed06..5209f6040c1c3b 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -135,6 +135,7 @@ (media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV, (media_player.DOMAIN, media_player.DEVICE_CLASS_SPEAKER): TYPE_SPEAKER, (sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR, + (sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY): TYPE_SENSOR, } CHALLENGE_ACK_NEEDED = "ackNeeded" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 6b5530ab2cee0a..5b089459d83d32 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -80,6 +80,7 @@ TRAIT_OPENCLOSE = PREFIX_TRAITS + "OpenClose" TRAIT_VOLUME = PREFIX_TRAITS + "Volume" TRAIT_ARMDISARM = PREFIX_TRAITS + "ArmDisarm" +TRAIT_HUMIDITY_SETTING = PREFIX_TRAITS + "HumiditySetting" PREFIX_COMMANDS = "action.devices.commands." COMMAND_ONOFF = PREFIX_COMMANDS + "OnOff" @@ -849,6 +850,56 @@ async def execute(self, command, data, params, challenge): ) +@register_trait +class HumiditySettingTrait(_Trait): + """Trait to offer humidity setting functionality. + + https://developers.google.com/actions/smarthome/traits/humiditysetting + """ + + name = TRAIT_HUMIDITY_SETTING + commands = [] + + @staticmethod + def supported(domain, features, device_class): + """Test if state is supported.""" + return domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_HUMIDITY + + def sync_attributes(self): + """Return humidity attributes for a sync request.""" + response = {} + attrs = self.state.attributes + domain = self.state.domain + if domain == sensor.DOMAIN: + device_class = attrs.get(ATTR_DEVICE_CLASS) + if device_class == sensor.DEVICE_CLASS_HUMIDITY: + response["queryOnlyHumiditySetting"] = True + + return response + + def query_attributes(self): + """Return humidity query attributes.""" + response = {} + attrs = self.state.attributes + domain = self.state.domain + if domain == sensor.DOMAIN: + device_class = attrs.get(ATTR_DEVICE_CLASS) + if device_class == sensor.DEVICE_CLASS_HUMIDITY: + current_humidity = self.state.state + if current_humidity is not None: + response["humidityAmbientPercent"] = round(float(current_humidity)) + + return response + + async def execute(self, command, data, params, challenge): + """Execute a humidity command.""" + domain = self.state.domain + if domain == sensor.DOMAIN: + raise SmartHomeError( + ERR_NOT_SUPPORTED, "Execute is not supported by sensor" + ) + + @register_trait class LockUnlockTrait(_Trait): """Trait to lock or unlock a lock. diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index d6ec24a7867953..6d24aa0942f8ce 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1629,3 +1629,29 @@ async def test_temperature_setting_sensor(hass): assert trt.query_attributes() == {"thermostatTemperatureAmbient": 21.1} hass.config.units.temperature_unit = TEMP_CELSIUS + + +async def test_humidity_setting_sensor(hass): + """Test HumiditySetting trait support for humidity sensor.""" + assert ( + helpers.get_google_type(sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY) is not None + ) + assert not trait.HumiditySettingTrait.supported( + sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE + ) + assert trait.HumiditySettingTrait.supported( + sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY + ) + + trt = trait.HumiditySettingTrait( + hass, + State("sensor.test", "70", {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_HUMIDITY}), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == {"queryOnlyHumiditySetting": True} + assert trt.query_attributes() == {"humidityAmbientPercent": 70} + + with pytest.raises(helpers.SmartHomeError) as err: + await trt.execute(trait.COMMAND_ONOFF, BASIC_DATA, {"on": False}, {}) + assert err.value.code == const.ERR_NOT_SUPPORTED From afaa464142cb403ffa38c0e8dcb3d76eda4f131a Mon Sep 17 00:00:00 2001 From: Barry Williams Date: Tue, 26 Nov 2019 06:34:45 +0000 Subject: [PATCH 1774/3953] If volume disabled do not enable support (#28635) --- .../components/openhome/manifest.json | 4 +--- .../components/openhome/media_player.py | 18 ++++++++---------- requirements_all.txt | 2 +- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 2ec58b86125fb8..7640c388747839 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -2,9 +2,7 @@ "domain": "openhome", "name": "Openhome", "documentation": "https://www.home-assistant.io/integrations/openhome", - "requirements": [ - "openhomedevice==0.4.2" - ], + "requirements": ["openhomedevice==0.6.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 0443187ac63001..fbcd5f3ba021f9 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -17,14 +17,7 @@ ) from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING -SUPPORT_OPENHOME = ( - SUPPORT_SELECT_SOURCE - | SUPPORT_VOLUME_STEP - | SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET - | SUPPORT_TURN_OFF - | SUPPORT_TURN_ON -) +SUPPORT_OPENHOME = SUPPORT_SELECT_SOURCE | SUPPORT_TURN_OFF | SUPPORT_TURN_ON _LOGGER = logging.getLogger(__name__) @@ -79,14 +72,19 @@ def update(self): self._in_standby = self._device.IsInStandby() self._transport_state = self._device.TransportState() self._track_information = self._device.TrackInfo() - self._volume_level = self._device.VolumeLevel() - self._volume_muted = self._device.IsMuted() self._source = self._device.Source() self._name = self._device.Room().decode("utf-8") self._supported_features = SUPPORT_OPENHOME source_index = {} source_names = list() + if self._device.VolumeEnabled(): + self._supported_features |= ( + SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET + ) + self._volume_level = self._device.VolumeLevel() + self._volume_muted = self._device.IsMuted() + for source in self._device.Sources(): source_names.append(source["name"]) source_index[source["name"]] = source["index"] diff --git a/requirements_all.txt b/requirements_all.txt index 75d0115fa4811d..4b3d88d9241cc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -921,7 +921,7 @@ onvif-zeep-async==0.2.0 openevsewifi==0.4 # homeassistant.components.openhome -openhomedevice==0.4.2 +openhomedevice==0.6.3 # homeassistant.components.opensensemap opensensemap-api==0.1.5 From 99c7608fb4dfc5627b0439397ecd53fab64c3ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 26 Nov 2019 08:40:08 +0200 Subject: [PATCH 1775/3953] Lint config cleanups (#28864) * Remove bunch of unneeded lint exclusions * Use symbolic names instead of identifiers in pylint disables * Tighten scope of some pylint disables --- homeassistant/__main__.py | 1 - homeassistant/auth/mfa_modules/__init__.py | 2 +- homeassistant/auth/permissions/__init__.py | 3 -- homeassistant/auth/providers/__init__.py | 2 +- homeassistant/components/abode/config_flow.py | 2 +- .../alarm_control_panel/__init__.py | 1 - .../components/apache_kafka/__init__.py | 2 +- homeassistant/components/brunt/cover.py | 1 - homeassistant/components/cast/helpers.py | 1 - homeassistant/components/cups/sensor.py | 1 - homeassistant/components/decora_wifi/light.py | 2 +- homeassistant/components/doorbird/__init__.py | 1 - homeassistant/components/enocean/__init__.py | 1 - .../components/eq3btsmart/climate.py | 2 +- homeassistant/components/esphome/fan.py | 1 - homeassistant/components/evohome/__init__.py | 6 +--- .../components/google_assistant/helpers.py | 1 - .../components/google_assistant/http.py | 1 - .../components/google_pubsub/__init__.py | 6 ++-- homeassistant/components/group/__init__.py | 1 - homeassistant/components/harmony/remote.py | 1 - homeassistant/components/homekit/__init__.py | 3 +- .../components/homekit_controller/__init__.py | 2 -- homeassistant/components/honeywell/climate.py | 8 ++--- homeassistant/components/http/static.py | 2 -- .../components/input_datetime/__init__.py | 1 - .../components/isy994/binary_sensor.py | 1 - .../components/mobile_app/webhook.py | 2 -- homeassistant/components/mqtt/__init__.py | 1 - homeassistant/components/mqtt/models.py | 1 - homeassistant/components/plex/server.py | 2 +- .../components/python_script/__init__.py | 1 - homeassistant/components/rachio/__init__.py | 1 - homeassistant/components/rainbird/__init__.py | 2 +- homeassistant/components/recorder/util.py | 2 +- homeassistant/components/script/__init__.py | 2 +- .../components/sisyphus/media_player.py | 1 - .../components/sonos/media_player.py | 1 - homeassistant/components/switchmate/switch.py | 2 +- .../components/system_health/__init__.py | 2 +- .../components/telegram_bot/polling.py | 2 +- .../components/tensorflow/image_processing.py | 3 +- homeassistant/components/timer/__init__.py | 1 - homeassistant/components/toon/__init__.py | 2 +- homeassistant/components/updater/__init__.py | 2 +- homeassistant/components/upnp/device.py | 1 - .../verisure/alarm_control_panel.py | 1 - homeassistant/components/withings/common.py | 2 +- homeassistant/components/wled/config_flow.py | 2 +- homeassistant/components/xmpp/notify.py | 1 - homeassistant/components/zeroconf/__init__.py | 4 +-- homeassistant/components/zha/__init__.py | 3 +- .../components/zha/core/registries.py | 2 +- homeassistant/components/zha/core/store.py | 2 +- homeassistant/components/zwave/config_flow.py | 3 +- homeassistant/config.py | 2 -- homeassistant/core.py | 1 - homeassistant/exceptions.py | 4 +-- homeassistant/helpers/__init__.py | 1 - homeassistant/helpers/config_entry_flow.py | 2 +- homeassistant/helpers/script.py | 2 -- homeassistant/helpers/template.py | 2 +- homeassistant/loader.py | 2 +- homeassistant/util/yaml/loader.py | 1 - pylintrc | 2 ++ tests/components/daikin/test_config_flow.py | 2 +- .../components/homematicip_cloud/conftest.py | 4 ++- tests/components/homematicip_cloud/helper.py | 8 +++-- .../test_alarm_control_panel.py | 2 +- .../homematicip_cloud/test_climate.py | 31 +++++++++++-------- .../homematicip_cloud/test_device.py | 2 +- .../logi_circle/test_config_flow.py | 20 +++++++----- .../owntracks/test_device_tracker.py | 3 +- tests/components/point/test_config_flow.py | 12 ++++--- tests/components/smhi/common.py | 2 +- tests/components/smhi/test_config_flow.py | 2 +- tests/components/smhi/test_weather.py | 4 +-- 77 files changed, 92 insertions(+), 126 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index b416b1f98d3d1b..d2903370f4992f 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -272,7 +272,6 @@ def cmdline() -> List[str]: async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: """Set up HASS and run.""" - # pylint: disable=redefined-outer-name from homeassistant import bootstrap, core hass = core.HomeAssistant() diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 9d49f67df82d7d..9020b0b321ecc8 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -42,7 +42,7 @@ def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: self.config = config @property - def id(self) -> str: # pylint: disable=invalid-name + def id(self) -> str: """Return id of the auth module. Default is same as type diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index f0e093953e1b18..36cf9c4f420c83 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -58,15 +58,12 @@ def _entity_func(self) -> Callable[[str, str], bool]: def __eq__(self, other: Any) -> bool: """Equals check.""" - # pylint: disable=protected-access return isinstance(other, PolicyPermissions) and other._policy == self._policy class _OwnerPermissions(AbstractPermissions): """Owner permissions.""" - # pylint: disable=no-self-use - def access_all_entities(self, key: str) -> bool: """Check if we have a certain access to all entities.""" return True diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 3e25003ad00eda..cbce31529022ca 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -48,7 +48,7 @@ def __init__( self.config = config @property - def id(self) -> Optional[str]: # pylint: disable=invalid-name + def id(self) -> Optional[str]: """Return id of the auth provider. Optional, can be None. diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index bf48e4546b301d..b8e8548db3181e 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from .const import DOMAIN, DEFAULT_CACHEDB # pylint: disable=W0611 +from .const import DOMAIN, DEFAULT_CACHEDB # pylint: disable=unused-import CONF_POLLING = "polling" diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 36cb8650aced00..2a335651d96ce8 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -101,7 +101,6 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -# pylint: disable=no-self-use class AlarmControlPanel(Entity): """An abstract class for alarm control devices.""" diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index e0c8b824913e48..7bd23630bd0899 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -64,7 +64,7 @@ class DateTimeJSONEncoder(json.JSONEncoder): Additionally add encoding for datetime objects as isoformat. """ - def default(self, o): # pylint: disable=E0202 + def default(self, o): # pylint: disable=method-hidden """Implement encoding logic.""" if isinstance(o, datetime): return o.isoformat() diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 7d4279cf5b207a..373c33394413c8 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -36,7 +36,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the brunt platform.""" - # pylint: disable=no-name-in-module username = config[CONF_USERNAME] password = config[CONF_PASSWORD] diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index ea5c77ebc1afa3..e82f6c9e4ed186 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -150,7 +150,6 @@ def __init__(self, cast_device, chromecast, mz_mgr): chromecast.register_status_listener(self) chromecast.socket_client.media_controller.register_status_listener(self) chromecast.register_connection_listener(self) - # pylint: disable=protected-access if cast_device._cast_info.is_audio_group: self._mz_mgr.add_multizone(chromecast) else: diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 4af51e911a1cd9..17a246561a50fa 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -306,7 +306,6 @@ def update(self): self._attributes = self.data.attributes -# pylint: disable=no-name-in-module class CupsData: """Get the latest data from CUPS and update the state.""" diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 77f3a6387ed2fd..6171f65ef242ee 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -28,7 +28,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Decora WiFi platform.""" - # pylint: disable=import-error, no-name-in-module + # pylint: disable=import-error from decora_wifi import DecoraWiFiSession from decora_wifi.models.person import Person from decora_wifi.models.residential_account import ResidentialAccount diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index ff0bbd7119407d..3127dabd66d4c1 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -264,7 +264,6 @@ class DoorBirdRequestView(HomeAssistantView): name = API_URL[1:].replace("/", ":") extra_urls = [API_URL + "/{event}"] - # pylint: disable=no-self-use async def get(self, request, event): """Respond to requests from the device.""" from aiohttp import web diff --git a/homeassistant/components/enocean/__init__.py b/homeassistant/components/enocean/__init__.py index b75c8f001c0f33..d01af51c7e37fe 100644 --- a/homeassistant/components/enocean/__init__.py +++ b/homeassistant/components/enocean/__init__.py @@ -84,7 +84,6 @@ def _message_received_callback(self, packet): def value_changed(self, packet): """Update the internal state of the device when a packet arrives.""" - # pylint: disable=no-self-use def send_command(self, data, optional, packet_type): """Send a command via the EnOcean dongle.""" from enocean.protocol.packet import Packet diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 8499a0de5a09f7..cee059972940bf 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -190,7 +190,7 @@ def set_preset_mode(self, preset_mode): def update(self): """Update the data from the thermostat.""" - # pylint: disable=import-error,no-name-in-module + # pylint: disable=import-error from bluepy.btle import BTLEException try: diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index cddb75b41bfa79..8b9b4b4922cf8b 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -81,7 +81,6 @@ async def async_turn_on(self, speed: Optional[str] = None, **kwargs) -> None: data["speed"] = _fan_speeds.from_hass(speed) await self._client.fan_command(**data) - # pylint: disable=arguments-differ async def async_turn_off(self, **kwargs) -> None: """Turn off the fan.""" await self._client.fan_command(key=self._static_info.key, state=False) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 29f89dc08d60c5..3d903e86e30641 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -237,11 +237,7 @@ def __init__(self, hass, client, client_v1, store, params) -> None: loc_idx = params[CONF_LOCATION_IDX] self.config = client.installation_info[loc_idx][GWS][0][TCS][0] - self.tcs = ( - client.locations[loc_idx] # pylint: disable=protected-access - ._gateways[0] - ._control_systems[0] - ) + self.tcs = client.locations[loc_idx]._gateways[0]._control_systems[0] self.temps = None async def save_auth_tokens(self) -> None: diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 96b9b93d70a4e9..0f3a3dcf039cc8 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -77,7 +77,6 @@ def is_local_sdk_active(self): @property def should_report_state(self): """Return if states should be proactively reported.""" - # pylint: disable=no-self-use return False @property diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 90fa1ced157a90..ce5eff7afef40c 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -102,7 +102,6 @@ def secure_devices_pin(self): @property def should_report_state(self): """Return if states should be proactively reported.""" - # pylint: disable=no-self-use return self._config.get(CONF_REPORT_STATE) def should_expose(self, state) -> bool: diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index e108d249797b35..c4136c3b9cbaa1 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -57,7 +57,9 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): service_principal_path ) - topic_path = publisher.topic_path(project_id, topic_name) # pylint: disable=E1101 + topic_path = publisher.topic_path( # pylint: disable=no-member + project_id, topic_name + ) encoder = DateTimeJSONEncoder() @@ -87,7 +89,7 @@ class DateTimeJSONEncoder(json.JSONEncoder): Additionally add encoding for datetime objects as isoformat. """ - def default(self, o): # pylint: disable=E0202 + def default(self, o): # pylint: disable=method-hidden """Implement encoding logic.""" if isinstance(o, datetime.datetime): return o.isoformat() diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 29126c82d44aa7..b4688229073136 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -656,7 +656,6 @@ def _async_update_group_state(self, tr_state=None): if gr_on is None: return - # pylint: disable=too-many-boolean-expressions if tr_state is None or ( (gr_state == gr_on and tr_state.state == gr_off) or (gr_state == gr_off and tr_state.state == gr_on) diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 118af7fe34ad61..d5c1c2d82bc648 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -316,7 +316,6 @@ async def async_turn_off(self, **kwargs): except aioexc.TimeOut: _LOGGER.error("%s: Powering off timed-out", self.name) - # pylint: disable=arguments-differ async def async_send_command(self, command, **kwargs): """Send a list of commands to one device.""" _LOGGER.debug("%s: Send Command", self.name) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index bb525271cec3e7..525c091e177ba8 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -362,8 +362,7 @@ def start(self, *args): return self.status = STATUS_WAIT - # pylint: disable=unused-import - from . import ( # noqa: F401 + from . import ( # noqa: F401 pylint: disable=unused-import type_covers, type_fans, type_lights, diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 4ca54099d24d09..6b53301e87790f 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -109,7 +109,6 @@ def _setup_characteristic(self, char): setup_fn = getattr(self, f"_setup_{setup_fn_name}", None) if not setup_fn: return - # pylint: disable=not-callable setup_fn(char) @callback @@ -132,7 +131,6 @@ def async_state_changed(self): if not update_fn: continue - # pylint: disable=not-callable update_fn(result["value"]) self.async_write_ha_state() diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 15efac87587c30..42f4778eb4f44c 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -160,9 +160,7 @@ def __init__( self._username = username self._password = password - _LOGGER.debug( - "latestData = %s ", device._data # pylint: disable=protected-access - ) + _LOGGER.debug("latestData = %s ", device._data) # not all honeywell HVACs support all modes mappings = [v for k, v in HVAC_MODE_TO_HW_MODE.items() if device.raw_ui_data[k]] @@ -174,13 +172,13 @@ def __init__( | SUPPORT_TARGET_TEMPERATURE_RANGE ) - if device._data["canControlHumidification"]: # pylint: disable=protected-access + if device._data["canControlHumidification"]: self._supported_features |= SUPPORT_TARGET_HUMIDITY if device.raw_ui_data["SwitchEmergencyHeatAllowed"]: self._supported_features |= SUPPORT_AUX_HEAT - if not device._data["hasFan"]: # pylint: disable=protected-access + if not device._data["hasFan"]: return # not all honeywell fans support all modes diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index e6a70c9f64369a..7d84be0f6dd247 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -13,8 +13,6 @@ CACHE_HEADERS = {hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}"} -# https://github.com/PyCQA/astroid/issues/633 -# pylint: disable=duplicate-bases class CachingStaticResource(StaticResource): """Static Resource handler that will add cache headers.""" diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 85fac4130a3e94..c366be90b14f14 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -87,7 +87,6 @@ async def async_set_datetime_service(entity, call): time = call.data.get(ATTR_TIME) date = call.data.get(ATTR_DATE) dttm = call.data.get(ATTR_DATETIME) - # pylint: disable=too-many-boolean-expressions if ( dttm and (date or time) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 0c4fb80be8cdc4..eed5f1a81a0bb2 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -110,7 +110,6 @@ def __init__(self, node) -> None: self._negative_node = None self._heartbeat_device = None self._device_class_from_type = _detect_device_type(self._node) - # pylint: disable=protected-access if _is_val_unknown(self._node.status._val): self._computed_state = None self._status_was_unknown = True diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 66188500fd6a2d..416f6cd2ffebeb 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -148,7 +148,6 @@ async def handle_webhook( blocking=True, context=context, ) - # noqa: E722 pylint: disable=broad-except except (vol.Invalid, ServiceNotFound, Exception) as ex: _LOGGER.error( "Error when calling service during mobile_app " @@ -174,7 +173,6 @@ async def handle_webhook( tpl = item[ATTR_TEMPLATE] attach(hass, tpl) resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) - # noqa: E722 pylint: disable=broad-except except TemplateError as ex: resp[key] = {"error": str(ex)} diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 70523a840a398b..498114732df639 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -335,7 +335,6 @@ def embedded_broker_deprecated(value): ) -# pylint: disable=invalid-name SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 5f014aadd08153..46aaa23732f067 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -3,7 +3,6 @@ import attr -# pylint: disable=invalid-name PublishPayloadType = Union[str, bytes, int, float, None] diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 69838fbf27fb2d..c9aee9e8dad06c 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -192,7 +192,7 @@ def machine_identifier(self): @property def url_in_use(self): """Return URL used for connected Plex server.""" - return self._plex_server._baseurl # pylint: disable=W0212 + return self._plex_server._baseurl # pylint: disable=protected-access @property def use_episode_art(self): diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index af0865bc685a21..046649fce7930b 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -147,7 +147,6 @@ def execute(hass, filename, source, data=None): def protected_getattr(obj, name, default=None): """Restricted method to get attributes.""" - # pylint: disable=too-many-boolean-expressions if name.startswith("async_"): raise ScriptError("Not allowed to access async methods") if ( diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 2030512ab3136e..28614d22110af6 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -284,7 +284,6 @@ class RachioWebhookView(HomeAssistantView): url = WEBHOOK_PATH name = url[1:].replace("/", ":") - # pylint: disable=no-self-use @asyncio.coroutine async def post(self, request) -> web.Response: """Handle webhook calls from the server.""" diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 0b51be1f258018..83c358c480b0e7 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -75,7 +75,7 @@ def _setup_controller(hass, controller_config, config): position = len(hass.data[DATA_RAINBIRD]) try: controller.get_serial_number() - except Exception as exc: # pylint: disable=W0703 + except Exception as exc: # pylint: disable=broad-except _LOGGER.error("Unable to setup controller: %s", exc) return False hass.data[DATA_RAINBIRD].append(controller) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 8cfcafea79d80a..693d88ae7953d1 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -28,7 +28,7 @@ def session_scope(*, hass=None, session=None): if session.transaction: need_rollback = True session.commit() - except Exception as err: # pylint: disable=broad-except + except Exception as err: _LOGGER.error("Error executing query: %s", err) if need_rollback: session.rollback() diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 5a3223a8508f38..9659cc625c01bd 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -207,7 +207,7 @@ async def async_turn_on(self, **kwargs): ) try: await self.script.async_run(kwargs.get(ATTR_VARIABLES), context) - except Exception as err: # pylint: disable=broad-except + except Exception as err: self.script.async_log_exception( _LOGGER, f"Error executing script {self.entity_id}", err ) diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index e06b84b4ac581d..5f02418f7e0d70 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -43,7 +43,6 @@ ) -# pylint: disable=unused-argument async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up a media player entity for a Sisyphus table.""" host = discovery_info[CONF_HOST] diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2baa02d0a5dfce..9ce72d87dfeb34 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -1056,7 +1056,6 @@ def _snapshot_all(entities): def restore(self): """Restore a snapshotted state to a player.""" try: - # pylint: disable=protected-access self._soco_snapshot.restore() except (TypeError, AttributeError, SoCoException) as ex: # Can happen if restoring a coordinator onto a current slave diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index 6abbfd5fae5e26..ddb0db3feee403 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -# pylint: disable=import-error, no-member, no-value-for-parameter +# pylint: disable=import-error, no-member import switchmate import voluptuous as vol diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index c6b483509593f2..778ddf601dcfaf 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -44,7 +44,7 @@ async def _info_wrapper(hass, info_callback): return await info_callback(hass) except asyncio.TimeoutError: return {"error": "Fetching info timed out"} - except Exception as err: # pylint: disable=W0703 + except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Error fetching info") return {"error": str(err)} diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 314cb31a373078..cf3d13d5edc8a6 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -55,7 +55,7 @@ def __init__(self): """Initialize the messages handler instance.""" super().__init__(handler) - def check_update(self, update): # pylint: disable=no-self-use + def check_update(self, update): """Check is update valid.""" return isinstance(update, Update) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index c9c1b00aae46fb..5f576f176e8261 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -104,8 +104,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: # Display warning that PIL will be used if no OpenCV is found. - # pylint: disable=unused-import,unused-variable - import cv2 # noqa: F401 + import cv2 # noqa: F401 pylint: disable=unused-import except ImportError: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 3ac41b84d82ac6..c6cc0a8c3c960e 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -162,7 +162,6 @@ async def async_start(self, duration): event = EVENT_TIMER_RESTARTED self._state = STATUS_ACTIVE - # pylint: disable=redefined-outer-name start = dt_util.utcnow() if self._remaining and newduration is None: self._end = start + self._remaining diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index ed627fdc9242bd..19466ba49c4a10 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -139,7 +139,7 @@ def update(self, now=None): """Update all Toon data and notify entities.""" # Ignore the TTL meganism from client library # It causes a lots of issues, hence we take control over caching - self._toon._clear_cache() # pylint: disable=W0212 + self._toon._clear_cache() # pylint: disable=protected-access # Gather data from client library (single API call) self.gas = self._toon.gas diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 22c11d0c38ef9a..08f08e1bb64fb6 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -2,7 +2,7 @@ import asyncio from datetime import timedelta -# pylint: disable=import-error,no-name-in-module +# pylint: disable=import-error from distutils.version import StrictVersion import json import logging diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index b87c3dc96b636e..fffee57b411870 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -150,7 +150,6 @@ async def async_get_total_bytes_sent(self): async def async_get_total_packets_received(self): """Get total packets received.""" - # pylint: disable=invalid-name return await self._igd_device.async_get_total_packets_received() async def async_get_total_packets_sent(self): diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index b7cc822bd43077..78a09e439d7def 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -37,7 +37,6 @@ def set_arm_state(state, code=None): while "result" not in transaction: sleep(0.5) transaction = hub.session.get_arm_state_transaction(transaction_id) - # pylint: disable=unexpected-keyword-arg hub.update_overview(no_throttle=True) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 911bb08906b1cb..655776ae00438c 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -226,7 +226,7 @@ async def call(self, function, throttle_domain=None) -> Any: WithingsDataManager.print_service_available() return result - except Exception as ex: # pylint: disable=broad-except + except Exception as ex: # Withings api encountered error. if isinstance(ex, (UnauthorizedException, AuthFailedException)): raise NotAuthenticatedError(ex) diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 7be283874e0ff8..c6b11fa1eb6410 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.helpers import ConfigType from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN # pylint: disable=W0611 +from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 5aa9dbfffd11cd..338b0b85c038b8 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -190,7 +190,6 @@ async def send_file(self, timeout=None): message = self.Message(sto=recipient, stype="chat") message["body"] = url - # pylint: disable=invalid-sequence-index message["oob"]["url"] = url try: message.send() diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 41a6c7510f9b27..9f27a5fafc9aea 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,7 +1,5 @@ """Support for exposing Home Assistant via Zeroconf.""" -# PyLint bug confuses absolute/relative imports -# https://github.com/PyCQA/pylint/issues/1931 -# pylint: disable=no-name-in-module + import logging import socket diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index be84795d962b79..ecd27c48839fab 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -100,8 +100,7 @@ async def async_setup_entry(hass, config_entry): 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 - import zhaquirks # noqa: F401 + import zhaquirks # noqa: F401 pylint: disable=unused-import zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index dd007e125a630c..13688a6c4204d3 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -27,7 +27,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH # importing channels updates registries -from . import channels # noqa: F401 pylint: disable=wrong-import-position,unused-import +from . import channels # noqa: F401 pylint: disable=unused-import from .const import ( CONTROLLER, SENSOR_ACCELERATION, diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index cea38517767c85..bcc9b2a42d4e42 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -1,5 +1,5 @@ """Data storage helper for ZHA.""" -# pylint: disable=W0611 +# pylint: disable=unused-import from collections import OrderedDict import logging from typing import MutableMapping diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py index f28502db57fc7a..a264cdea7dabee 100644 --- a/homeassistant/components/zwave/config_flow.py +++ b/homeassistant/components/zwave/config_flow.py @@ -48,8 +48,7 @@ async def async_step_user(self, user_input=None): try: from functools import partial - # pylint: disable=unused-variable - option = await self.hass.async_add_executor_job( # noqa: F841 + option = await self.hass.async_add_executor_job( # noqa: F841 pylint: disable=unused-variable partial( ZWaveOption, user_input[CONF_USB_STICK_PATH], diff --git a/homeassistant/config.py b/homeassistant/config.py index e6be2b9c7a5557..71628be8006e78 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -627,7 +627,6 @@ async def merge_packages_config( _log_pkg_error: Callable = _log_pkg_error, ) -> Dict: """Merge packages into the top-level configuration. Mutate config.""" - # pylint: disable=too-many-nested-blocks PACKAGES_CONFIG_SCHEMA(packages) for pack_name, pack_conf in packages.items(): for comp_name, comp_conf in pack_conf.items(): @@ -766,7 +765,6 @@ async def async_process_component_config( # Validate platform specific schema if hasattr(platform, "PLATFORM_SCHEMA"): - # pylint: disable=no-member try: p_validated = platform.PLATFORM_SCHEMA( # type: ignore p_config diff --git a/homeassistant/core.py b/homeassistant/core.py index f5e9176910756c..2859e0fe1572a1 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -75,7 +75,6 @@ ) # Typing imports that create a circular dependency -# pylint: disable=using-constant-test if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntries from homeassistant.components.http import HomeAssistantHTTP diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 0000a4b176ea01..6147e26c80909e 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -2,10 +2,8 @@ from typing import Optional, Tuple, TYPE_CHECKING import jinja2 -# pylint: disable=using-constant-test if TYPE_CHECKING: - # pylint: disable=unused-import - from .core import Context # noqa: F401 + from .core import Context # noqa: F401 pylint: disable=unused-import class HomeAssistantError(Exception): diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 4c1a9803d754a4..fe60ffc4b33f2c 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -4,7 +4,6 @@ from homeassistant.const import CONF_PLATFORM -# pylint: disable=invalid-name ConfigType = Dict[str, Any] diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 7a1512957a2962..41f90effb89f3f 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -24,7 +24,7 @@ def __init__( self._domain = domain self._title = title self._discovery_function = discovery_function - self.CONNECTION_CLASS = connection_class # pylint: disable=C0103 + self.CONNECTION_CLASS = connection_class # pylint: disable=invalid-name async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1e65c24eaaf4cd..21dd0b71487f84 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -231,7 +231,6 @@ def async_log_exception(self, logger, message_base, exception): Should only be called on exceptions raised by this scripts async_run. """ - # pylint: disable=protected-access step = self._exception_step action = self.sequence[step] action_type = _determine_action(action) @@ -281,7 +280,6 @@ async def _async_delay(self, action, variables, context): @callback def async_script_delay(now): """Handle delay.""" - # pylint: disable=cell-var-from-loop with suppress(ValueError): self._async_listener.remove(unsub) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index e389f95dae124c..f23d9cddfddfe0 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -142,7 +142,7 @@ def _filter_lifecycle(self, entity_id: str) -> bool: def result(self) -> str: """Results of the template computation.""" if self._exception is not None: - raise self._exception # pylint: disable=raising-bad-type + raise self._exception return self._result def _freeze(self) -> None: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 9c8d7d2aecbb44..af76b073cd3973 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -26,7 +26,7 @@ ) # Typing imports that create a circular dependency -# pylint: disable=using-constant-test,unused-import +# pylint: disable=unused-import if TYPE_CHECKING: from homeassistant.core import HomeAssistant diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 3bd466e277172a..fec09a1d6901c8 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -68,7 +68,6 @@ def load_yaml(fname: str) -> JSON_TYPE: raise HomeAssistantError(exc) -# pylint: disable=pointless-statement @overload def _add_reference( obj: Union[list, NodeListClass], loader: yaml.SafeLoader, node: yaml.nodes.Node diff --git a/pylintrc b/pylintrc index 5de08d06794a39..44659ddb376996 100644 --- a/pylintrc +++ b/pylintrc @@ -50,6 +50,8 @@ disable= too-many-boolean-expressions, unnecessary-pass, unused-argument +enable= + use-symbolic-message-instead [REPORTS] score=no diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index f9fba67d554c7b..674d918ed39f42 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,4 +1,4 @@ -# pylint: disable=W0621 +# pylint: disable=redefined-outer-name """Tests for the Daikin config flow.""" import asyncio diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index f60f8d659b54b4..fa19f573c7c13d 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -27,7 +27,9 @@ def mock_connection_fixture() -> AsyncConnection: def _rest_call_side_effect(path, body=None): return path, body - connection._restCall.side_effect = _rest_call_side_effect # pylint: disable=W0212 + connection._restCall.side_effect = ( # pylint: disable=protected-access + _rest_call_side_effect + ) connection.api_call.return_value = mock_coro(True) connection.init.side_effect = mock_coro(True) diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 494ec2dc90bc0f..42ff2061698c8f 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -58,7 +58,9 @@ async def async_manipulate_test_data( fire_target = hmip_device if fire_device is None else fire_device if isinstance(fire_target, AsyncHome): - fire_target.fire_update_event(fire_target._rawJSONData) # pylint: disable=W0212 + fire_target.fire_update_event( + fire_target._rawJSONData # pylint: disable=protected-access + ) else: fire_target.fire_update_event() @@ -136,7 +138,9 @@ def get_async_home_mock(self): def _get_mock(instance): """Create a mock and copy instance attributes over mock.""" if isinstance(instance, Mock): - instance.__dict__.update(instance._mock_wraps.__dict__) # pylint: disable=W0212 + instance.__dict__.update( + instance._mock_wraps.__dict__ # pylint: disable=protected-access + ) return instance mock = Mock(spec=instance, wraps=instance) diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 78bc0a09ea507a..cf85e805143dfc 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -18,7 +18,7 @@ async def _async_manipulate_security_zones( hass, home, internal_active=False, external_active=False, alarm_triggered=False ): """Set new values on hmip security zones.""" - json = home._rawJSONData # pylint: disable=W0212 + json = home._rawJSONData # pylint: disable=protected-access json["functionalHomes"]["SECURITY_AND_ALARM"]["alarmActive"] = alarm_triggered external_zone_id = json["functionalHomes"]["SECURITY_AND_ALARM"]["securityZones"][ "EXTERNAL" diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 2b233a6dee2c87..d40a77751e87ae 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -367,7 +367,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) - assert len(home._connection.mock_calls) == 1 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 1 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -377,7 +377,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) - assert len(home._connection.mock_calls) == 2 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 2 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -387,7 +387,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) - assert len(home._connection.mock_calls) == 3 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 3 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -397,7 +397,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) - assert len(home._connection.mock_calls) == 4 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 4 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -407,7 +407,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) - assert len(home._connection.mock_calls) == 5 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 5 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -417,7 +417,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) - assert len(home._connection.mock_calls) == 6 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 6 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -427,14 +427,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 7 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 7 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", "deactivate_eco_mode", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 8 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 8 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", @@ -444,14 +444,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 9 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 9 # pylint: disable=protected-access await hass.services.async_call( "homematicip_cloud", "deactivate_vacation", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access not_existing_hap_id = "5555F7110000000000000001" await hass.services.async_call( @@ -463,7 +463,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () # There is no further call on connection. - assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 + assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access async def test_hmip_heating_group_services(hass, mock_hap_with_service): @@ -485,7 +485,9 @@ async def test_hmip_heating_group_services(hass, mock_hap_with_service): ) assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) - assert len(hmip_device._connection.mock_calls) == 2 # pylint: disable=W0212 + assert ( + len(hmip_device._connection.mock_calls) == 2 # pylint: disable=protected-access + ) await hass.services.async_call( "homematicip_cloud", @@ -495,4 +497,7 @@ async def test_hmip_heating_group_services(hass, mock_hap_with_service): ) assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) - assert len(hmip_device._connection.mock_calls) == 12 # pylint: disable=W0212 + assert ( + len(hmip_device._connection.mock_calls) # pylint: disable=protected-access + == 12 + ) diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 812f32a33442a0..77f99655c98967 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -105,7 +105,7 @@ async def test_hap_reconnected(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_UNAVAILABLE - default_mock_hap._accesspoint_connected = False # pylint: disable=W0212 + default_mock_hap._accesspoint_connected = False # pylint: disable=protected-access await async_manipulate_test_data(hass, default_mock_hap.home, "connected", True) await hass.async_block_till_done() ha_state = hass.states.get(entity_id) diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 02a9437a225873..6a291d5e7abf4a 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -40,7 +40,7 @@ def init_config_flow(hass): sensors=None, ) flow = config_flow.LogiCircleFlowHandler() - flow._get_authorization_url = Mock( # pylint: disable=W0212 + flow._get_authorization_url = Mock( # pylint: disable=protected-access return_value="http://example.com" ) flow.hass = hass @@ -65,7 +65,9 @@ def mock_logi_circle(): yield mock_logi_circle_ -async def test_step_import(hass, mock_logi_circle): # pylint: disable=W0621 +async def test_step_import( + hass, mock_logi_circle # pylint: disable=redefined-outer-name +): """Test that we trigger import when configuring with client.""" flow = init_config_flow(hass) @@ -75,8 +77,8 @@ async def test_step_import(hass, mock_logi_circle): # pylint: disable=W0621 async def test_full_flow_implementation( - hass, mock_logi_circle -): # pylint: disable=W0621 + hass, mock_logi_circle # pylint: disable=redefined-outer-name +): """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation( hass, @@ -154,7 +156,7 @@ async def test_abort_if_already_setup(hass): ) async def test_abort_if_authorize_fails( hass, mock_logi_circle, side_effect, error -): # pylint: disable=W0621 +): # pylint: disable=redefined-outer-name """Test we abort if authorizing fails.""" flow = init_config_flow(hass) mock_logi_circle.LogiCircle().authorize.side_effect = side_effect @@ -176,7 +178,9 @@ async def test_not_pick_implementation_if_only_one(hass): assert result["step_id"] == "auth" -async def test_gen_auth_url(hass, mock_logi_circle): # pylint: disable=W0621 +async def test_gen_auth_url( + hass, mock_logi_circle +): # pylint: disable=redefined-outer-name """Test generating authorize URL from Logi Circle API.""" config_flow.register_flow_implementation( hass, @@ -192,7 +196,7 @@ async def test_gen_auth_url(hass, mock_logi_circle): # pylint: disable=W0621 flow.flow_impl = "test-auth-url" await async_setup_component(hass, "http", {}) - result = flow._get_authorization_url() # pylint: disable=W0212 + result = flow._get_authorization_url() # pylint: disable=protected-access assert result == "http://authorize.url" @@ -206,7 +210,7 @@ async def test_callback_view_rejects_missing_code(hass): async def test_callback_view_accepts_code( hass, mock_logi_circle -): # pylint: disable=W0621 +): # pylint: disable=redefined-outer-name """Test the auth callback view handles requests with auth code.""" init_config_flow(hass) view = LogiCircleAuthCallbackView() diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index daef624f9c214f..3a7f3c030ed2bd 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1495,8 +1495,7 @@ async def test_encrypted_payload_no_topic_key(hass, setup_comp): async def test_encrypted_payload_libsodium(hass, setup_comp): """Test sending encrypted message payload.""" try: - # pylint: disable=unused-import - import nacl # noqa: F401 + import nacl # noqa: F401 pylint: disable=unused-import except (ImportError, OSError): pytest.skip("PyNaCl/libsodium is not installed") return diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index ec808b5fd80fa8..b7b4cdfb47f71b 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -14,7 +14,7 @@ def init_config_flow(hass, side_effect=None): """Init a configuration flow.""" config_flow.register_flow_implementation(hass, DOMAIN, "id", "secret") flow = config_flow.PointFlowHandler() - flow._get_authorization_url = Mock( # pylint: disable=W0212 + flow._get_authorization_url = Mock( # pylint: disable=protected-access return_value=mock_coro("https://example.com"), side_effect=side_effect ) flow.hass = hass @@ -28,7 +28,7 @@ def is_authorized(): @pytest.fixture -def mock_pypoint(is_authorized): # pylint: disable=W0621 +def mock_pypoint(is_authorized): # pylint: disable=redefined-outer-name """Mock pypoint.""" with MockDependency("pypoint") as mock_pypoint_: mock_pypoint_.PointSession().get_access_token.return_value = { @@ -66,7 +66,9 @@ async def test_abort_if_already_setup(hass): assert result["reason"] == "already_setup" -async def test_full_flow_implementation(hass, mock_pypoint): # pylint: disable=W0621 +async def test_full_flow_implementation( + hass, mock_pypoint # pylint: disable=redefined-outer-name +): """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation(hass, "test-other", None, None) flow = init_config_flow(hass) @@ -92,7 +94,7 @@ async def test_full_flow_implementation(hass, mock_pypoint): # pylint: disable= assert result["data"]["token"] == {"access_token": "boo"} -async def test_step_import(hass, mock_pypoint): # pylint: disable=W0621 +async def test_step_import(hass, mock_pypoint): # pylint: disable=redefined-outer-name """Test that we trigger import when configuring with client.""" flow = init_config_flow(hass) @@ -104,7 +106,7 @@ async def test_step_import(hass, mock_pypoint): # pylint: disable=W0621 @pytest.mark.parametrize("is_authorized", [False]) async def test_wrong_code_flow_implementation( hass, mock_pypoint -): # pylint: disable=W0621 +): # pylint: disable=redefined-outer-name """Test wrong code.""" flow = init_config_flow(hass) diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py index 74c8a99b51fcf7..6f21584032431e 100644 --- a/tests/components/smhi/common.py +++ b/tests/components/smhi/common.py @@ -5,7 +5,7 @@ class AsyncMock(Mock): """Implements Mock async.""" - # pylint: disable=W0235 + # pylint: disable=useless-super-delegation async def __call__(self, *args, **kwargs): """Hack for async support for Mock.""" return super().__call__(*args, **kwargs) diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index ab317fb829ac62..e5e1d392419e18 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant.components.smhi import config_flow -# pylint: disable=W0212 +# pylint: disable=protected-access async def test_homeassistant_location_exists() -> None: """Test if homeassistant location exists it should return True.""" hass = Mock() diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 76b32895758a4a..6cb7d690c1c4e3 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -99,7 +99,7 @@ def test_properties_no_data(hass: HomeAssistant) -> None: assert weather.temperature_unit == TEMP_CELSIUS -# pylint: disable=W0212 +# pylint: disable=protected-access def test_properties_unknown_symbol() -> None: """Test behaviour when unknown symbol from API.""" hass = Mock() @@ -152,7 +152,7 @@ def test_properties_unknown_symbol() -> None: assert forecast[ATTR_FORECAST_CONDITION] is None -# pylint: disable=W0212 +# pylint: disable=protected-access async def test_refresh_weather_forecast_exceeds_retries(hass) -> None: """Test the refresh weather forecast function.""" from smhi.smhi_lib import SmhiForecastException From db425f54326d19009de0c80d18e2a630a22fab36 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Tue, 26 Nov 2019 08:03:38 +0100 Subject: [PATCH 1776/3953] Simplify getting alias from tplink smartplug.context (#28696) * tplink: simplify getting alias from smartplug.context * add a doc-string to private property --- homeassistant/components/tplink/switch.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index 791d358c509500..b6ca69f4ccd287 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -115,6 +115,12 @@ def device_state_attributes(self): """Return the state attributes of the device.""" return self._emeter_params + @property + def _plug_from_context(self): + """Return the plug from the context.""" + children = self.smartplug.sys_info["children"] + return next(c for c in children if c["id"] == self.smartplug.context) + def update(self): """Update the TP-Link switch's state.""" try: @@ -126,21 +132,13 @@ def update(self): self._alias = self.smartplug.alias self._device_id = self._mac else: - self._alias = [ - child - for child in self.smartplug.sys_info["children"] - if child["id"] == self.smartplug.context - ][0]["alias"] + self._alias = self._plug_from_context["alias"] self._device_id = self.smartplug.context if self.smartplug.context is None: self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON else: - self._state = [ - child - for child in self.smartplug.sys_info["children"] - if child["id"] == self.smartplug.context - ][0]["state"] == 1 + self._state = self._plug_from_context["state"] == 1 if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() From b0925e60d70ebdcb499b6755fe7db3c490d7d0a4 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Tue, 26 Nov 2019 02:05:10 -0500 Subject: [PATCH 1777/3953] Explicitly include "Alexa" Interface in discovery response (#28218) * Explicitly include Alexa Interface to discovery response. * Updated cloud component test to reflect additional Alexa interface. * Updated test for recently added SeekController. --- .../components/alexa/capabilities.py | 14 +++++ homeassistant/components/alexa/entities.py | 25 +++++++- tests/components/alexa/test_smart_home.py | 62 ++++++++++++------- tests/components/cloud/test_http_api.py | 2 +- 4 files changed, 78 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 56622084b4832d..1f18cb7a5909d9 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -227,6 +227,20 @@ def serialize_friendly_names(resources): return friendly_names +class Alexa(AlexaCapability): + """Implements Alexa Interface. + + Although endpoints implement this interface implicitly, + The API suggests you should explicitly include this interface. + + https://developer.amazon.com/docs/device-apis/alexa-interface.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa" + + class AlexaEndpointHealth(AlexaCapability): """Implements Alexa.EndpointHealth. diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 20376c51223c08..f9cf0cdf3532f8 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -33,6 +33,7 @@ from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES from .capabilities import ( + Alexa, AlexaBrightnessController, AlexaChannelController, AlexaColorController, @@ -261,6 +262,7 @@ def interfaces(self): return [ AlexaPowerController(self.entity), AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), ] @@ -281,6 +283,7 @@ def interfaces(self): return [ AlexaPowerController(self.entity), AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), ] @@ -303,6 +306,7 @@ def interfaces(self): yield AlexaThermostatController(self.hass, self.entity) yield AlexaTemperatureSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(cover.DOMAIN) @@ -327,6 +331,7 @@ def interfaces(self): self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" ) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(light.DOMAIN) @@ -349,6 +354,7 @@ def interfaces(self): if supported & light.SUPPORT_COLOR_TEMP: yield AlexaColorTemperatureController(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(fan.DOMAIN) @@ -380,6 +386,7 @@ def interfaces(self): ) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(lock.DOMAIN) @@ -395,6 +402,7 @@ def interfaces(self): return [ AlexaLockController(self.entity), AlexaEndpointHealth(self.hass, self.entity), + Alexa(self.hass), ] @@ -412,7 +420,6 @@ def default_display_categories(self): def interfaces(self): """Yield the supported interfaces.""" - yield AlexaEndpointHealth(self.hass, self.entity) yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -446,6 +453,9 @@ def interfaces(self): if supported & media_player.const.SUPPORT_PLAY_MEDIA: yield AlexaChannelController(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + @ENTITY_ADAPTERS.register(scene.DOMAIN) class SceneCapabilities(AlexaEntity): @@ -464,7 +474,10 @@ def default_display_categories(self): def interfaces(self): """Yield the supported interfaces.""" - return [AlexaSceneController(self.entity, supports_deactivation=False)] + return [ + AlexaSceneController(self.entity, supports_deactivation=False), + Alexa(self.hass), + ] @ENTITY_ADAPTERS.register(script.DOMAIN) @@ -478,7 +491,10 @@ def default_display_categories(self): def interfaces(self): """Yield the supported interfaces.""" can_cancel = bool(self.entity.attributes.get("can_cancel")) - return [AlexaSceneController(self.entity, supports_deactivation=can_cancel)] + return [ + AlexaSceneController(self.entity, supports_deactivation=can_cancel), + Alexa(self.hass), + ] @ENTITY_ADAPTERS.register(sensor.DOMAIN) @@ -497,6 +513,7 @@ def interfaces(self): if attrs.get(ATTR_UNIT_OF_MEASUREMENT) in (TEMP_FAHRENHEIT, TEMP_CELSIUS): yield AlexaTemperatureSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) @ENTITY_ADAPTERS.register(binary_sensor.DOMAIN) @@ -528,6 +545,7 @@ def interfaces(self): yield AlexaDoorbellEventSource(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) def get_type(self): """Return the type of binary sensor.""" @@ -551,3 +569,4 @@ def interfaces(self): if not self.entity.attributes.get("code_arm_required"): yield AlexaSecurityPanelController(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 09d9b6bd7b6a1d..214632e2b7f49f 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -157,7 +157,7 @@ async def test_switch(hass, events): assert appliance["displayCategories"][0] == "SWITCH" assert appliance["friendlyName"] == "Test switch" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -194,7 +194,7 @@ async def test_light(hass): assert appliance["displayCategories"][0] == "LIGHT" assert appliance["friendlyName"] == "Test light 1" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -220,6 +220,7 @@ async def test_dimmable_light(hass): "Alexa.BrightnessController", "Alexa.PowerController", "Alexa.EndpointHealth", + "Alexa", ) properties = await reported_properties(hass, "light#test_2") @@ -262,6 +263,7 @@ async def test_color_light(hass): "Alexa.ColorController", "Alexa.ColorTemperatureController", "Alexa.EndpointHealth", + "Alexa", ) # IncreaseColorTemperature and DecreaseColorTemperature have their own @@ -277,8 +279,11 @@ async def test_script(hass): assert appliance["displayCategories"][0] == "ACTIVITY_TRIGGER" assert appliance["friendlyName"] == "Test script" - (capability,) = assert_endpoint_capabilities(appliance, "Alexa.SceneController") - assert not capability["supportsDeactivation"] + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SceneController", "Alexa" + ) + scene_capability = get_capability(capabilities, "Alexa.SceneController") + assert not scene_capability["supportsDeactivation"] await assert_scene_controller_works("script#test", "script.turn_on", None, hass) @@ -293,8 +298,11 @@ async def test_cancelable_script(hass): appliance = await discovery_test(device, hass) assert appliance["endpointId"] == "script#test_2" - (capability,) = assert_endpoint_capabilities(appliance, "Alexa.SceneController") - assert capability["supportsDeactivation"] + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SceneController", "Alexa" + ) + scene_capability = get_capability(capabilities, "Alexa.SceneController") + assert scene_capability["supportsDeactivation"] await assert_scene_controller_works( "script#test_2", "script.turn_on", "script.turn_off", hass @@ -310,7 +318,7 @@ async def test_input_boolean(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test input boolean" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -327,8 +335,11 @@ async def test_scene(hass): assert appliance["displayCategories"][0] == "SCENE_TRIGGER" assert appliance["friendlyName"] == "Test scene" - (capability,) = assert_endpoint_capabilities(appliance, "Alexa.SceneController") - assert not capability["supportsDeactivation"] + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SceneController", "Alexa" + ) + scene_capability = get_capability(capabilities, "Alexa.SceneController") + assert not scene_capability["supportsDeactivation"] await assert_scene_controller_works("scene#test", "scene.turn_on", None, hass) @@ -342,7 +353,7 @@ async def test_fan(hass): assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) power_capability = get_capability(capabilities, "Alexa.PowerController") @@ -378,6 +389,7 @@ async def test_variable_fan(hass): "Alexa.PowerLevelController", "Alexa.RangeController", "Alexa.EndpointHealth", + "Alexa", ) range_capability = get_capability(capabilities, "Alexa.RangeController") @@ -461,6 +473,7 @@ async def test_oscillating_fan(hass): "Alexa.RangeController", "Alexa.ToggleController", "Alexa.EndpointHealth", + "Alexa", ) toggle_capability = get_capability(capabilities, "Alexa.ToggleController") @@ -525,6 +538,7 @@ async def test_direction_fan(hass): "Alexa.RangeController", "Alexa.ModeController", "Alexa.EndpointHealth", + "Alexa", ) mode_capability = get_capability(capabilities, "Alexa.ModeController") @@ -637,6 +651,7 @@ async def test_fan_range(hass): "Alexa.PowerLevelController", "Alexa.RangeController", "Alexa.EndpointHealth", + "Alexa", ) range_capability = get_capability(capabilities, "Alexa.RangeController") @@ -714,7 +729,7 @@ async def test_lock(hass): assert appliance["displayCategories"][0] == "SMARTLOCK" assert appliance["friendlyName"] == "Test lock" assert_endpoint_capabilities( - appliance, "Alexa.LockController", "Alexa.EndpointHealth" + appliance, "Alexa.LockController", "Alexa.EndpointHealth", "Alexa" ) _, msg = await assert_request_calls_service( @@ -765,6 +780,7 @@ async def test_media_player(hass): capabilities = assert_endpoint_capabilities( appliance, + "Alexa", "Alexa.ChannelController", "Alexa.EndpointHealth", "Alexa.InputController", @@ -987,6 +1003,7 @@ async def test_media_player_power(hass): assert_endpoint_capabilities( appliance, + "Alexa", "Alexa.ChannelController", "Alexa.EndpointHealth", "Alexa.InputController", @@ -1157,6 +1174,7 @@ async def test_media_player_seek(hass): assert_endpoint_capabilities( appliance, + "Alexa", "Alexa.EndpointHealth", "Alexa.PowerController", "Alexa.SeekController", @@ -1260,7 +1278,7 @@ async def test_alert(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test alert" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -1277,7 +1295,7 @@ async def test_automation(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test automation" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -1294,7 +1312,7 @@ async def test_group(hass): assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test group" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_power_controller_works( @@ -1321,6 +1339,7 @@ async def test_cover(hass): "Alexa.PercentageController", "Alexa.PowerController", "Alexa.EndpointHealth", + "Alexa", ) await assert_power_controller_works( @@ -1409,7 +1428,7 @@ async def test_temp_sensor(hass): assert appliance["friendlyName"] == "Test Temp Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.TemperatureSensor", "Alexa.EndpointHealth" + appliance, "Alexa.TemperatureSensor", "Alexa.EndpointHealth", "Alexa" ) temp_sensor_capability = get_capability(capabilities, "Alexa.TemperatureSensor") @@ -1438,7 +1457,7 @@ async def test_contact_sensor(hass): assert appliance["friendlyName"] == "Test Contact Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.ContactSensor", "Alexa.EndpointHealth" + appliance, "Alexa.ContactSensor", "Alexa.EndpointHealth", "Alexa" ) contact_sensor_capability = get_capability(capabilities, "Alexa.ContactSensor") @@ -1467,7 +1486,7 @@ async def test_motion_sensor(hass): assert appliance["friendlyName"] == "Test Motion Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.MotionSensor", "Alexa.EndpointHealth" + appliance, "Alexa.MotionSensor", "Alexa.EndpointHealth", "Alexa" ) motion_sensor_capability = get_capability(capabilities, "Alexa.MotionSensor") @@ -1494,7 +1513,7 @@ async def test_doorbell_sensor(hass): assert appliance["friendlyName"] == "Test Doorbell Sensor" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth" + appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth", "Alexa" ) doorbell_capability = get_capability(capabilities, "Alexa.DoorbellEventSource") @@ -1544,6 +1563,7 @@ async def test_thermostat(hass): "Alexa.ThermostatController", "Alexa.TemperatureSensor", "Alexa.EndpointHealth", + "Alexa", ) properties = await reported_properties(hass, "climate#test_thermostat") @@ -1940,7 +1960,7 @@ async def test_entity_config(hass): assert appliance["friendlyName"] == "Config name" assert appliance["description"] == "Config description via Home Assistant" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) scene = msg["payload"]["endpoints"][1] @@ -2057,7 +2077,7 @@ async def test_alarm_control_panel_disarmed(hass): assert appliance["displayCategories"][0] == "SECURITY_PANEL" assert appliance["friendlyName"] == "Test Alarm Control Panel 1" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth", "Alexa" ) security_panel_capability = get_capability( capabilities, "Alexa.SecurityPanelController" @@ -2124,7 +2144,7 @@ async def test_alarm_control_panel_armed(hass): assert appliance["displayCategories"][0] == "SECURITY_PANEL" assert appliance["friendlyName"] == "Test Alarm Control Panel 2" assert_endpoint_capabilities( - appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth", "Alexa" ) properties = await reported_properties(hass, "alarm_control_panel#test_2") diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 8d05f1a14c37b6..b889a89dc5dbac 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -801,7 +801,7 @@ async def test_list_alexa_entities(hass, hass_ws_client, setup_api, mock_cloud_l assert response["result"][0] == { "entity_id": "light.kitchen", "display_categories": ["LIGHT"], - "interfaces": ["Alexa.PowerController", "Alexa.EndpointHealth"], + "interfaces": ["Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"], } From 6c6a5c50a581663c7783c36bea7256e9d2a722a9 Mon Sep 17 00:00:00 2001 From: Tim McCormick Date: Tue, 26 Nov 2019 07:22:11 +0000 Subject: [PATCH 1778/3953] Fix digest auth rest sensors (#28153) * Fix digest auth rest sensors * Refactor to use request() * Fix black complaints * Don't rename data variable Don't rename the data variable, appears several other sensors have been coded to rely on it * Fix tests test_setup_missing_schema: Change the exception to check for to PlatformNotReady. With the change away from prepared statements, we no longer get the MissingSchema error during setup - we get it when we attempt to call the endpoint. Since the result is PlatformNotReady, which is what we want, and the error log clearly contains a note that the schema is incorrect I think this is fine. test_setup_failed_connect & test_setup_timeout: These aren't checking for minimum config, and they're not invoking the correct method to have the default config filled in. Therefore I've just added the correct minimum config so these can continue to test what they're there to test. test_update_request_exception: Testing a request exception with the assert on line 404! Excellent. Anyway, we've moved from using the requests Session object to the requests.request function - so the patch needed to be altered to ensure the RequestException was raised. * Remove no longer needed import * Fix binary sensor test in same way --- homeassistant/components/rest/sensor.py | 33 +++++++++++---------- tests/components/rest/test_binary_sensor.py | 8 ++--- tests/components/rest/test_sensor.py | 10 +++---- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 41adb8559036cf..b89e96a0f34f10 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -227,32 +227,33 @@ def __init__( self, method, resource, auth, headers, data, verify_ssl, timeout=DEFAULT_TIMEOUT ): """Initialize the data object.""" - self._request = requests.Request( - method, resource, headers=headers, auth=auth, data=data - ).prepare() + self._method = method + self._resource = resource + self._auth = auth + self._headers = headers + self._request_data = data self._verify_ssl = verify_ssl self._timeout = timeout self.data = None def set_url(self, url): """Set url.""" - self._request.prepare_url(url, None) + self._resource = url def update(self): """Get the latest data from REST service with provided method.""" - _LOGGER.debug("Updating from %s", self._request.url) + _LOGGER.debug("Updating from %s", self._resource) try: - with requests.Session() as sess: - response = sess.send( - self._request, timeout=self._timeout, verify=self._verify_ssl - ) - + response = requests.request( + self._method, + self._resource, + headers=self._headers, + auth=self._auth, + data=self._request_data, + timeout=self._timeout, + verify=self._verify_ssl, + ) self.data = response.text except requests.exceptions.RequestException as ex: - _LOGGER.error( - "Error fetching data: %s from %s failed with %s", - self._request, - self._request.url, - ex, - ) + _LOGGER.error("Error fetching data: %s failed with %s", self._resource, ex) self.data = None diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 63a7d3ff273f15..8993be6a7a1059 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch, Mock import requests -from requests.exceptions import Timeout, MissingSchema +from requests.exceptions import Timeout import requests_mock from homeassistant.exceptions import PlatformNotReady @@ -47,7 +47,7 @@ def test_setup_missing_config(self): def test_setup_missing_schema(self): """Test setup with resource missing schema.""" - with pytest.raises(MissingSchema): + with pytest.raises(PlatformNotReady): rest.setup_platform( self.hass, {"platform": "rest", "resource": "localhost", "method": "GET"}, @@ -60,7 +60,7 @@ def test_setup_failed_connect(self, mock_req): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, self.add_devices, None, ) @@ -72,7 +72,7 @@ def test_setup_timeout(self, mock_req): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, self.add_devices, None, ) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 50acb0533476e9..c6cfce90a20f9e 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch, Mock import requests -from requests.exceptions import Timeout, MissingSchema, RequestException +from requests.exceptions import Timeout, RequestException import requests_mock from homeassistant.exceptions import PlatformNotReady @@ -37,7 +37,7 @@ def test_setup_missing_config(self): def test_setup_missing_schema(self): """Test setup with resource missing schema.""" - with pytest.raises(MissingSchema): + with pytest.raises(PlatformNotReady): rest.setup_platform( self.hass, {"platform": "rest", "resource": "localhost", "method": "GET"}, @@ -50,7 +50,7 @@ def test_setup_failed_connect(self, mock_req): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, lambda devices, update=True: None, ) @@ -60,7 +60,7 @@ def test_setup_timeout(self, mock_req): with raises(PlatformNotReady): rest.setup_platform( self.hass, - {"platform": "rest", "resource": "http://localhost"}, + {"platform": "rest", "resource": "http://localhost", "method": "GET"}, lambda devices, update=True: None, ) @@ -397,7 +397,7 @@ def test_update(self, mock_req): self.rest.update() assert "test data" == self.rest.data - @patch("requests.Session", side_effect=RequestException) + @patch("requests.request", side_effect=RequestException) def test_update_request_exception(self, mock_req): """Test update when a request exception occurs.""" self.rest.update() From c76f768a823f659f78be0cd58b26ffe008736068 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 26 Nov 2019 02:30:21 -0800 Subject: [PATCH 1779/3953] Make conversation and intent context aware (#28965) * WIP * LINT --- homeassistant/components/almond/__init__.py | 4 +- .../components/conversation/__init__.py | 76 ++++++++++--------- .../components/conversation/agent.py | 3 +- .../components/conversation/default_agent.py | 3 +- homeassistant/components/http/view.py | 7 +- .../components/intent_script/__init__.py | 4 +- homeassistant/components/light/__init__.py | 4 +- homeassistant/helpers/intent.py | 17 ++++- tests/components/conversation/test_init.py | 21 +++-- 9 files changed, 82 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 7c1f65f3ac3eac..66d4b2fc9afb66 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -10,7 +10,7 @@ from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI import voluptuous as vol -from homeassistant.core import HomeAssistant, CoreState +from homeassistant.core import HomeAssistant, CoreState, Context from homeassistant.const import CONF_TYPE, CONF_HOST, EVENT_HOMEASSISTANT_START from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.auth.const import GROUP_ID_ADMIN @@ -277,7 +277,7 @@ async def async_set_onboarding(self, shown): return True async def async_process( - self, text: str, conversation_id: Optional[str] = None + self, text: str, context: Context, conversation_id: Optional[str] = None ) -> intent.IntentResponse: """Process a sentence.""" response = await self.api.async_converse_text(text, conversation_id) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index b2648d3d1f60f3..ba8b211e65a1df 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -50,15 +50,6 @@ def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent): hass.data[DATA_AGENT] = agent -async def get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: - """Get agent.""" - agent = hass.data.get(DATA_AGENT) - if agent is None: - agent = hass.data[DATA_AGENT] = DefaultAgent(hass) - await agent.async_initialize(hass.data.get(DATA_CONFIG)) - return agent - - async def async_setup(hass, config): """Register the process service.""" hass.data[DATA_CONFIG] = config @@ -67,8 +58,9 @@ async def handle_service(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) + agent = await _get_agent(hass) try: - await process(hass, text, service.context.id) + await agent.async_process(text, service.context) except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) @@ -84,27 +76,6 @@ async def handle_service(service): return True -async def process(hass: core.HomeAssistant, text: str, conversation_id: str): - """Process text and get intent.""" - agent = await get_agent(hass) - return await agent.async_process(text, conversation_id) - - -async def get_intent(hass: core.HomeAssistant, text: str, conversation_id: str): - """Process text and get intent.""" - try: - intent_result = await process(hass, text, conversation_id) - except intent.IntentHandleError as err: - intent_result = intent.IntentResponse() - intent_result.async_set_speech(str(err)) - - if intent_result is None: - intent_result = intent.IntentResponse() - intent_result.async_set_speech("Sorry, I didn't understand that") - - return intent_result - - @websocket_api.async_response @websocket_api.websocket_command( {"type": "conversation/process", "text": str, vol.Optional("conversation_id"): str} @@ -112,7 +83,10 @@ async def get_intent(hass: core.HomeAssistant, text: str, conversation_id: str): async def websocket_process(hass, connection, msg): """Process text.""" connection.send_result( - msg["id"], await get_intent(hass, msg["text"], msg.get("conversation_id")) + msg["id"], + await _async_converse( + hass, msg["text"], msg.get("conversation_id"), connection.context(msg) + ), ) @@ -120,7 +94,7 @@ async def websocket_process(hass, connection, msg): @websocket_api.websocket_command({"type": "conversation/agent/info"}) async def websocket_get_agent_info(hass, connection, msg): """Do we need onboarding.""" - agent = await get_agent(hass) + agent = await _get_agent(hass) connection.send_result( msg["id"], @@ -135,7 +109,7 @@ async def websocket_get_agent_info(hass, connection, msg): @websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool}) async def websocket_set_onboarding(hass, connection, msg): """Set onboarding status.""" - agent = await get_agent(hass) + agent = await _get_agent(hass) success = await agent.async_set_onboarding(msg["shown"]) @@ -157,8 +131,9 @@ class ConversationProcessView(http.HomeAssistantView): async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] - intent_result = await get_intent( - hass, data["text"], data.get("conversation_id") + + intent_result = await _async_converse( + hass, data["text"], data.get("conversation_id"), self.context(request) ) return self.json(intent_result) @@ -188,7 +163,7 @@ async def post(self, request, data): key: {"value": value} for key, value in data.get("data", {}).items() } intent_result = await intent.async_handle( - hass, DOMAIN, intent_name, slots, "" + hass, DOMAIN, intent_name, slots, "", self.context(request) ) except intent.IntentHandleError as err: intent_result = intent.IntentResponse() @@ -199,3 +174,30 @@ async def post(self, request, data): intent_result.async_set_speech("Sorry, I couldn't handle that") return self.json(intent_result) + + +async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: + """Get the active conversation agent.""" + agent = hass.data.get(DATA_AGENT) + if agent is None: + agent = hass.data[DATA_AGENT] = DefaultAgent(hass) + await agent.async_initialize(hass.data.get(DATA_CONFIG)) + return agent + + +async def _async_converse( + hass: core.HomeAssistant, text: str, conversation_id: str, context: core.Context +) -> intent.IntentResponse: + """Process text and get intent.""" + agent = await _get_agent(hass) + try: + intent_result = await agent.async_process(text, context, conversation_id) + except intent.IntentHandleError as err: + intent_result = intent.IntentResponse() + intent_result.async_set_speech(str(err)) + + if intent_result is None: + intent_result = intent.IntentResponse() + intent_result.async_set_speech("Sorry, I didn't understand that") + + return intent_result diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 0c47d6156455d1..c9c2ab46cf9fd3 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from typing import Optional +from homeassistant.core import Context from homeassistant.helpers import intent @@ -23,6 +24,6 @@ async def async_set_onboarding(self, shown): @abstractmethod async def async_process( - self, text: str, conversation_id: Optional[str] = None + self, text: str, context: Context, conversation_id: Optional[str] = None ) -> intent.IntentResponse: """Process a sentence.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index c202cdf1e6591c..e562eed7e666a1 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -109,7 +109,7 @@ def register_utterances(self, component): async_register(self.hass, intent_type, sentences) async def async_process( - self, text: str, conversation_id: Optional[str] = None + self, text: str, context: core.Context, conversation_id: Optional[str] = None ) -> intent.IntentResponse: """Process a sentence.""" intents = self.hass.data[DOMAIN] @@ -127,4 +127,5 @@ async def async_process( intent_type, {key: {"value": value} for key, value in match.groupdict().items()}, text, + context, ) diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 804c90d4f96ab8..31f96833667f3e 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -34,8 +34,8 @@ class HomeAssistantView: requires_auth = True cors_allowed = False - # pylint: disable=no-self-use - def context(self, request): + @staticmethod + def context(request): """Generate a context from a request.""" user = request.get("hass_user") if user is None: @@ -43,7 +43,8 @@ def context(self, request): return Context(user_id=user.id) - def json(self, result, status_code=200, headers=None): + @staticmethod + def json(result, status_code=200, headers=None): """Return a JSON response.""" try: msg = json.dumps( diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index 75a0c0e8f976e6..ce4b8b27a51a97 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -80,7 +80,9 @@ async def async_handle(self, intent_obj): if action is not None: if is_async_action: - intent_obj.hass.async_create_task(action.async_run(slots)) + intent_obj.hass.async_create_task( + action.async_run(slots, intent_obj.context) + ) else: await action.async_run(slots) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 2ca5e496b10aca..b33cb29421e21a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -232,7 +232,9 @@ async def async_handle(self, intent_obj): service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] speech_parts.append("{}% brightness".format(slots["brightness"]["value"])) - await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, service_data) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context + ) response = intent_obj.create_response() diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index dc48d825348f04..12b346603f05b1 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.const import ATTR_SUPPORTED_FEATURES -from homeassistant.core import callback, State, T +from homeassistant.core import callback, State, T, Context from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import HomeAssistantType @@ -53,6 +53,7 @@ async def async_handle( intent_type: str, slots: Optional[_SlotsType] = None, text_input: Optional[str] = None, + context: Optional[Context] = None, ) -> "IntentResponse": """Handle an intent.""" handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type) @@ -60,7 +61,10 @@ async def async_handle( if handler is None: raise UnknownIntent(f"Unknown intent {intent_type}") - intent = Intent(hass, platform, intent_type, slots or {}, text_input) + if context is None: + context = Context() + + intent = Intent(hass, platform, intent_type, slots or {}, text_input, context) try: _LOGGER.info("Triggering intent handler %s", handler) @@ -196,7 +200,10 @@ async def async_handle(self, intent_obj: "Intent") -> "IntentResponse": state = async_match_state(hass, slots["name"]["value"]) await hass.services.async_call( - self.domain, self.service, {ATTR_ENTITY_ID: state.entity_id} + self.domain, + self.service, + {ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, ) response = intent_obj.create_response() @@ -207,7 +214,7 @@ async def async_handle(self, intent_obj: "Intent") -> "IntentResponse": class Intent: """Hold the intent.""" - __slots__ = ["hass", "platform", "intent_type", "slots", "text_input"] + __slots__ = ["hass", "platform", "intent_type", "slots", "text_input", "context"] def __init__( self, @@ -216,6 +223,7 @@ def __init__( intent_type: str, slots: _SlotsType, text_input: Optional[str], + context: Context, ) -> None: """Initialize an intent.""" self.hass = hass @@ -223,6 +231,7 @@ def __init__( self.intent_type = intent_type self.slots = slots self.text_input = text_input + self.context = context @callback def create_response(self) -> "IntentResponse": diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index fc6508159ea3cb..3982ed6f69960a 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access import pytest -from homeassistant.core import DOMAIN as HASS_DOMAIN +from homeassistant.core import DOMAIN as HASS_DOMAIN, Context from homeassistant.setup import async_setup_component from homeassistant.components import conversation from homeassistant.components.cover import SERVICE_OPEN_COVER @@ -25,10 +25,13 @@ async def test_calling_intent(hass): ) assert result + context = Context() + await hass.services.async_call( "conversation", "process", {conversation.ATTR_TEXT: "I would like the Grolsch beer"}, + context=context, ) await hass.async_block_till_done() @@ -38,6 +41,7 @@ async def test_calling_intent(hass): assert intent.intent_type == "OrderBeer" assert intent.slots == {"type": {"value": "Grolsch"}} assert intent.text_input == "I would like the Grolsch beer" + assert intent.context is context async def test_register_before_setup(hass): @@ -80,7 +84,7 @@ async def test_register_before_setup(hass): assert intent.text_input == "I would like the Grolsch beer" -async def test_http_processing_intent(hass, hass_client): +async def test_http_processing_intent(hass, hass_client, hass_admin_user): """Test processing intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): @@ -90,6 +94,7 @@ class TestIntentHandler(intent.IntentHandler): async def async_handle(self, intent): """Handle the intent.""" + assert intent.context.user_id == hass_admin_user.id response = intent.create_response() response.async_set_speech( "I've ordered a {}!".format(intent.slots["type"]["value"]) @@ -124,7 +129,7 @@ async def async_handle(self, intent): } -async def test_http_handle_intent(hass, hass_client): +async def test_http_handle_intent(hass, hass_client, hass_admin_user): """Test handle intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): @@ -134,6 +139,7 @@ class TestIntentHandler(intent.IntentHandler): async def async_handle(self, intent): """Handle the intent.""" + assert intent.context.user_id == hass_admin_user.id response = intent.create_response() response.async_set_speech( "I've ordered a {}!".format(intent.slots["type"]["value"]) @@ -308,7 +314,7 @@ async def test_http_api_wrong_data(hass, hass_client): assert resp.status == 400 -async def test_custom_agent(hass, hass_client): +async def test_custom_agent(hass, hass_client, hass_admin_user): """Test a custom conversation agent.""" calls = [] @@ -316,9 +322,9 @@ async def test_custom_agent(hass, hass_client): class MyAgent(conversation.AbstractConversationAgent): """Test Agent.""" - async def async_process(self, text, conversation_id): + async def async_process(self, text, context, conversation_id): """Process some text.""" - calls.append((text, conversation_id)) + calls.append((text, context, conversation_id)) response = intent.IntentResponse() response.async_set_speech("Test response") return response @@ -341,4 +347,5 @@ async def async_process(self, text, conversation_id): assert len(calls) == 1 assert calls[0][0] == "Test Text" - assert calls[0][1] == "test-conv-id" + assert calls[0][1].user_id == hass_admin_user.id + assert calls[0][2] == "test-conv-id" From c538d899a27ad008843e20350e7dc9864de483e5 Mon Sep 17 00:00:00 2001 From: Bendik Brenne Date: Tue, 26 Nov 2019 14:14:09 +0100 Subject: [PATCH 1780/3953] Fix empty data attribute in sinch service call (#28253) * Fix empty data attribute in sinch service call * Simplified the PR as per @MartinHjelmare`s request. --- homeassistant/components/sinch/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py index 173873c0a6c2eb..d7d1f242c67c17 100644 --- a/homeassistant/components/sinch/notify.py +++ b/homeassistant/components/sinch/notify.py @@ -61,7 +61,7 @@ def __init__(self, config): def send_message(self, message="", **kwargs): """Send a message to a user.""" targets = kwargs.get(ATTR_TARGET, self.default_recipients) - data = kwargs.get(ATTR_DATA, {}) + data = kwargs.get(ATTR_DATA) or {} clx_args = {ATTR_MESSAGE: message, ATTR_SENDER: self.sender} From f9571c9637dc9eb0b39e5c6b1f8811cd5802fe88 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 26 Nov 2019 16:17:41 +0100 Subject: [PATCH 1781/3953] Fix Alexa tests (#29100) --- tests/components/alexa/test_smart_home.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 214632e2b7f49f..7cc1638bf2586b 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -181,7 +181,7 @@ async def test_outlet(hass, events): assert appliance["displayCategories"][0] == "SMARTPLUG" assert appliance["friendlyName"] == "Test switch" assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, "Alexa", "Alexa.PowerController", "Alexa.EndpointHealth" ) @@ -1065,6 +1065,7 @@ async def test_media_player_inputs(hass): capabilities = assert_endpoint_capabilities( appliance, + "Alexa", "Alexa.InputController", "Alexa.PowerController", "Alexa.EndpointHealth", @@ -2236,6 +2237,7 @@ async def test_cover_position(hass): capabilities = assert_endpoint_capabilities( appliance, + "Alexa", "Alexa.ModeController", "Alexa.PercentageController", "Alexa.PowerController", From c47ed743f14be184c790115a34b23dd3dfa47365 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Tue, 26 Nov 2019 11:55:33 -0500 Subject: [PATCH 1782/3953] Fix HomeKit linked battery sensor crash (#28974) * fix homekit linked battery sensor crash * simplified missing linked battery * fixed formatting * Make if faster --- .../components/homekit/accessories.py | 12 ++++++++- tests/components/homekit/test_accessories.py | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 84f0b7894c4d58..ddcc795d262519 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -105,8 +105,18 @@ def __init__( battery_found = self.hass.states.get(self.entity_id).attributes.get( ATTR_BATTERY_LEVEL ) + if self.linked_battery_sensor: - battery_found = self.hass.states.get(self.linked_battery_sensor).state + state = self.hass.states.get(self.linked_battery_sensor) + if state is not None: + battery_found = state.state + else: + self.linked_battery_sensor = None + _LOGGER.warning( + "%s: Battery sensor state missing: %s", + self.entity_id, + self.linked_battery_sensor, + ) if battery_found is None: return diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index fcc0e05b570c1a..883d84339e5ce8 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -245,6 +245,33 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): assert acc._char_charging.value == 0 +async def test_missing_linked_battery_sensor(hass, hk_driver, caplog): + """Test battery service with mising linked_battery_sensor.""" + entity_id = "homekit.accessory" + linked_battery = "sensor.battery" + hass.states.async_set(entity_id, "open") + await hass.async_block_till_done() + + acc = HomeAccessory( + hass, + hk_driver, + "Battery Service", + entity_id, + 2, + {CONF_LINKED_BATTERY_SENSOR: linked_battery}, + ) + acc.update_state = lambda x: None + assert not acc.linked_battery_sensor + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert not acc.linked_battery_sensor + assert not hasattr(acc, "_char_battery") + assert not hasattr(acc, "_char_low_battery") + assert not hasattr(acc, "_char_charging") + + async def test_call_service(hass, hk_driver, events): """Test call_service method.""" entity_id = "homekit.accessory" From 78d5184186b6eedf0fb0f51ab9210cd3066db113 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 17:59:42 +0100 Subject: [PATCH 1783/3953] Move icloud imports at top-level (#29089) --- .../components/icloud/device_tracker.py | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 2ecf904314fe4b..f6efb05fd0808c 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,25 +1,31 @@ """Platform that supports scanning iCloud.""" import logging -import random import os +import random +from pyicloud import PyiCloudService +from pyicloud.exceptions import ( + PyiCloudException, + PyiCloudFailedLoginException, + PyiCloudNoDevicesException, +) import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker.const import ( - DOMAIN, ATTR_ATTRIBUTES, + DOMAIN, ENTITY_ID_FORMAT, ) from homeassistant.components.device_tracker.legacy import DeviceScanner from homeassistant.components.zone import async_active_zone -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_utc_time_change from homeassistant.util import slugify +from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util from homeassistant.util.location import distance -from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -214,12 +220,6 @@ def __init__( def reset_account_icloud(self): """Reset an iCloud account.""" - from pyicloud import PyiCloudService - from pyicloud.exceptions import ( - PyiCloudFailedLoginException, - PyiCloudNoDevicesException, - ) - icloud_dir = self.hass.config.path("icloud") if not os.path.exists(icloud_dir): os.makedirs(icloud_dir) @@ -297,8 +297,6 @@ def icloud_need_trusted_device(self): def icloud_verification_callback(self, callback_data): """Handle the chosen trusted device.""" - from pyicloud.exceptions import PyiCloudException - self._verification_code = callback_data.get("code") try: @@ -344,8 +342,6 @@ def keep_alive(self, now): return if self.api.requires_2fa: - from pyicloud.exceptions import PyiCloudException - try: if self._trusted_device is None: self.icloud_need_trusted_device() @@ -436,8 +432,6 @@ def determine_interval(self, devicename, latitude, longitude, battery): def update_device(self, devicename): """Update the device_tracker entity.""" - from pyicloud.exceptions import PyiCloudNoDevicesException - # An entity will not be created by see() when track=false in # 'known_devices.yaml', but we need to see() it at least once entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename)) @@ -503,8 +497,6 @@ def lost_iphone(self, devicename): def update_icloud(self, devicename=None): """Request device information from iCloud and update device_tracker.""" - from pyicloud.exceptions import PyiCloudNoDevicesException - if self.api is None: return From 44e708f72bf87aa3d09fac697fb39652c4b504f2 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 17:59:55 +0100 Subject: [PATCH 1784/3953] Move ee_brightbox imports at top-level (#29054) * Move ee_brightbox imports at top-level * Fix tests * Fix : Commented out code --- homeassistant/components/ee_brightbox/device_tracker.py | 5 +---- tests/components/ee_brightbox/test_device_tracker.py | 9 ++++----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/ee_brightbox/device_tracker.py b/homeassistant/components/ee_brightbox/device_tracker.py index 81dbf9eab1f5b4..845d557e029f10 100644 --- a/homeassistant/components/ee_brightbox/device_tracker.py +++ b/homeassistant/components/ee_brightbox/device_tracker.py @@ -1,6 +1,7 @@ """Support for EE Brightbox router.""" import logging +from eebrightbox import EEBrightBox, EEBrightBoxException import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -46,8 +47,6 @@ def __init__(self, config): def check_config(self): """Check if provided configuration and credentials are correct.""" - from eebrightbox import EEBrightBox, EEBrightBoxException - try: with EEBrightBox(self.config) as ee_brightbox: return bool(ee_brightbox.get_devices()) @@ -57,8 +56,6 @@ def check_config(self): def scan_devices(self): """Scan for devices.""" - from eebrightbox import EEBrightBox - with EEBrightBox(self.config) as ee_brightbox: self.devices = {d["mac"]: d for d in ee_brightbox.get_devices()} diff --git a/tests/components/ee_brightbox/test_device_tracker.py b/tests/components/ee_brightbox/test_device_tracker.py index 6f732896399f37..f862539f1dfb56 100644 --- a/tests/components/ee_brightbox/test_device_tracker.py +++ b/tests/components/ee_brightbox/test_device_tracker.py @@ -2,6 +2,7 @@ from datetime import datetime from asynctest import patch +from eebrightbox import EEBrightBoxException import pytest from homeassistant.components.device_tracker import DOMAIN @@ -41,8 +42,6 @@ def _configure_mock_get_devices(eebrightbox_mock): def _configure_mock_failed_config_check(eebrightbox_mock): - from eebrightbox import EEBrightBoxException - eebrightbox_instance = eebrightbox_mock.return_value eebrightbox_instance.__enter__.side_effect = EEBrightBoxException( "Failed to connect to the router" @@ -55,7 +54,7 @@ def mock_dev_track(mock_device_tracker_conf): pass -@patch("eebrightbox.EEBrightBox") +@patch("homeassistant.components.ee_brightbox.device_tracker.EEBrightBox") async def test_missing_credentials(eebrightbox_mock, hass): """Test missing credentials.""" _configure_mock_get_devices(eebrightbox_mock) @@ -73,7 +72,7 @@ async def test_missing_credentials(eebrightbox_mock, hass): assert hass.states.get("device_tracker.hostnameff") is None -@patch("eebrightbox.EEBrightBox") +@patch("homeassistant.components.ee_brightbox.device_tracker.EEBrightBox") async def test_invalid_credentials(eebrightbox_mock, hass): """Test invalid credentials.""" _configure_mock_failed_config_check(eebrightbox_mock) @@ -93,7 +92,7 @@ async def test_invalid_credentials(eebrightbox_mock, hass): assert hass.states.get("device_tracker.hostnameff") is None -@patch("eebrightbox.EEBrightBox") +@patch("homeassistant.components.ee_brightbox.device_tracker.EEBrightBox") async def test_get_devices(eebrightbox_mock, hass): """Test valid configuration.""" _configure_mock_get_devices(eebrightbox_mock) From 9e971495f78d06a9b58e1de59e42ca335211f3e4 Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Tue, 26 Nov 2019 18:05:06 +0100 Subject: [PATCH 1785/3953] Update WazeRouteCalculator, add config options, fix subscription (#27963) * Update WazeRouteCalculator to 0.11 * Update WazeRouteCalculator to 0.11 * Adding new config options to Waze * Fixing avoid subscription option * Update WazeRouteCalculator to 0.12 There was an error in the underlying lib * Update WazeRouteCalculator to 0.12 --- .../components/waze_travel_time/manifest.json | 2 +- .../components/waze_travel_time/sensor.py | 40 +++++++++++++++++-- requirements_all.txt | 2 +- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 85bcc19032e965..32083ca8ca83c3 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -3,7 +3,7 @@ "name": "Waze travel time", "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", "requirements": [ - "WazeRouteCalculator==0.10" + "WazeRouteCalculator==0.12" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 4392a20d801d84..b9ca64c0970315 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -38,10 +38,16 @@ CONF_REALTIME = "realtime" CONF_UNITS = "units" CONF_VEHICLE_TYPE = "vehicle_type" +CONF_AVOID_TOLL_ROADS = "avoid_toll_roads" +CONF_AVOID_SUBSCRIPTION_ROADS = "avoid_subscription_roads" +CONF_AVOID_FERRIES = "avoid_ferries" DEFAULT_NAME = "Waze Travel Time" DEFAULT_REALTIME = True DEFAULT_VEHICLE_TYPE = "car" +DEFAULT_AVOID_TOLL_ROADS = False +DEFAULT_AVOID_SUBSCRIPTION_ROADS = False +DEFAULT_AVOID_FERRIES = False ICON = "mdi:car" @@ -65,6 +71,13 @@ VEHICLE_TYPES ), vol.Optional(CONF_UNITS): vol.In(UNITS), + vol.Optional( + CONF_AVOID_TOLL_ROADS, default=DEFAULT_AVOID_TOLL_ROADS + ): cv.boolean, + vol.Optional( + CONF_AVOID_SUBSCRIPTION_ROADS, default=DEFAULT_AVOID_SUBSCRIPTION_ROADS + ): cv.boolean, + vol.Optional(CONF_AVOID_FERRIES, default=DEFAULT_AVOID_FERRIES): cv.boolean, } ) @@ -79,10 +92,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): excl_filter = config.get(CONF_EXCL_FILTER) realtime = config.get(CONF_REALTIME) vehicle_type = config.get(CONF_VEHICLE_TYPE) + avoid_toll_roads = config.get(CONF_AVOID_TOLL_ROADS) + avoid_subscription_roads = config.get(CONF_AVOID_SUBSCRIPTION_ROADS) + avoid_ferries = config.get(CONF_AVOID_FERRIES) units = config.get(CONF_UNITS, hass.config.units.name) data = WazeTravelTimeData( - None, None, region, incl_filter, excl_filter, realtime, units, vehicle_type + None, + None, + region, + incl_filter, + excl_filter, + realtime, + units, + vehicle_type, + avoid_toll_roads, + avoid_subscription_roads, + avoid_ferries, ) sensor = WazeTravelTime(name, origin, destination, data) @@ -236,6 +262,9 @@ def __init__( realtime, units, vehicle_type, + avoid_toll_roads, + avoid_subscription_roads, + avoid_ferries, ): """Set up WazeRouteCalculator.""" @@ -251,6 +280,9 @@ def __init__( self.duration = None self.distance = None self.route = None + self.avoid_toll_roads = avoid_toll_roads + self.avoid_subscription_roads = avoid_subscription_roads + self.avoid_ferries = avoid_ferries # Currently WazeRouteCalc only supports PRIVATE, TAXI, MOTORCYCLE. if vehicle_type.upper() == "CAR": @@ -268,7 +300,9 @@ def update(self): self.destination, self.region, self.vehicle_type, - log_lvl=logging.DEBUG, + self.avoid_toll_roads, + self.avoid_subscription_roads, + self.avoid_ferries, ) routes = params.calc_all_routes_info(real_time=self.realtime) @@ -286,7 +320,7 @@ def update(self): if self.exclude.lower() not in k.lower() } - route = sorted(routes, key=(lambda key: routes[key][0]))[0] + route = list(routes)[0] self.duration, distance = routes[route] diff --git a/requirements_all.txt b/requirements_all.txt index 4b3d88d9241cc4..aae71e2028a178 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -100,7 +100,7 @@ TwitterAPI==2.5.10 # VL53L1X2==0.1.5 # homeassistant.components.waze_travel_time -WazeRouteCalculator==0.10 +WazeRouteCalculator==0.12 # homeassistant.components.yessssms YesssSMS==0.4.1 From f8a36499c1d308c98fb1ad67d778784de82b0d92 Mon Sep 17 00:00:00 2001 From: Matthew Donoughe Date: Tue, 26 Nov 2019 12:06:14 -0500 Subject: [PATCH 1786/3953] Add serial to caseta devices (#28886) * add model and serial to caseta devices * use just serial for unique id * add display name for entity registry * remove caseta device model * just store device * state and device are the same --- .../lutron_caseta/.translations/en.json | 5 ++++ .../components/lutron_caseta/__init__.py | 27 +++++++++++++------ .../components/lutron_caseta/cover.py | 14 +++++----- .../components/lutron_caseta/light.py | 12 ++++----- .../components/lutron_caseta/manifest.json | 2 +- .../components/lutron_caseta/strings.json | 5 ++++ .../components/lutron_caseta/switch.py | 10 +++---- requirements_all.txt | 2 +- 8 files changed, 49 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/lutron_caseta/.translations/en.json create mode 100644 homeassistant/components/lutron_caseta/strings.json diff --git a/homeassistant/components/lutron_caseta/.translations/en.json b/homeassistant/components/lutron_caseta/.translations/en.json new file mode 100644 index 00000000000000..cb7ab8c767ebbd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/en.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Caséta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 4d4b2e90fd6bee..54bde9538af526 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -76,28 +76,39 @@ def __init__(self, device, bridge): [:param]device the device metadata [:param]bridge the smartbridge object """ - self._device_id = device["device_id"] - self._device_type = device["type"] - self._device_name = device["name"] - self._device_zone = device["zone"] - self._state = None + self._device = device self._smartbridge = bridge async def async_added_to_hass(self): """Register callbacks.""" self._smartbridge.add_subscriber( - self._device_id, self.async_schedule_update_ha_state + self.device_id, self.async_schedule_update_ha_state ) + @property + def device_id(self): + """Return the device ID used for calling pylutron_caseta.""" + return self._device["device_id"] + @property def name(self): """Return the name of the device.""" - return self._device_name + return self._device["name"] + + @property + def serial(self): + """Return the serial number of the device.""" + return self._device["serial"] + + @property + def unique_id(self): + """Return the unique ID of the device (serial).""" + return str(self.serial) @property def device_state_attributes(self): """Return the state attributes.""" - attr = {"Device ID": self._device_id, "Zone ID": self._device_zone} + attr = {"Device ID": self.device_id, "Zone ID": self._device["zone"]} return attr @property diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index 786e569da32219..afd669153e0cd6 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -38,28 +38,28 @@ def supported_features(self): @property def is_closed(self): """Return if the cover is closed.""" - return self._state["current_state"] < 1 + return self._device["current_state"] < 1 @property def current_cover_position(self): """Return the current position of cover.""" - return self._state["current_state"] + return self._device["current_state"] async def async_close_cover(self, **kwargs): """Close the cover.""" - self._smartbridge.set_value(self._device_id, 0) + self._smartbridge.set_value(self.device_id, 0) async def async_open_cover(self, **kwargs): """Open the cover.""" - self._smartbridge.set_value(self._device_id, 100) + self._smartbridge.set_value(self.device_id, 100) async def async_set_cover_position(self, **kwargs): """Move the shade to a specific position.""" if ATTR_POSITION in kwargs: position = kwargs[ATTR_POSITION] - self._smartbridge.set_value(self._device_id, position) + self._smartbridge.set_value(self.device_id, position) async def async_update(self): """Call when forcing a refresh of the device.""" - self._state = self._smartbridge.get_device_by_id(self._device_id) - _LOGGER.debug(self._state) + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug(self._device) diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index a764ad4b73a29f..af225d2939db29 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -37,23 +37,23 @@ def supported_features(self): @property def brightness(self): """Return the brightness of the light.""" - return to_hass_level(self._state["current_state"]) + return to_hass_level(self._device["current_state"]) async def async_turn_on(self, **kwargs): """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, 255) - self._smartbridge.set_value(self._device_id, to_lutron_level(brightness)) + self._smartbridge.set_value(self.device_id, to_lutron_level(brightness)) async def async_turn_off(self, **kwargs): """Turn the light off.""" - self._smartbridge.set_value(self._device_id, 0) + self._smartbridge.set_value(self.device_id, 0) @property def is_on(self): """Return true if device is on.""" - return self._state["current_state"] > 0 + return self._device["current_state"] > 0 async def async_update(self): """Call when forcing a refresh of the device.""" - self._state = self._smartbridge.get_device_by_id(self._device_id) - _LOGGER.debug(self._state) + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug(self._device) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index d1501a562db4bd..e9df5ad1d46fd4 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -3,7 +3,7 @@ "name": "Lutron caseta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", "requirements": [ - "pylutron-caseta==0.5.0" + "pylutron-caseta==0.5.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json new file mode 100644 index 00000000000000..cb7ab8c767ebbd --- /dev/null +++ b/homeassistant/components/lutron_caseta/strings.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Caséta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index fabd4e7fa76ee6..f6eb846ecfb092 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -27,18 +27,18 @@ class LutronCasetaLight(LutronCasetaDevice, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the switch on.""" - self._smartbridge.turn_on(self._device_id) + self._smartbridge.turn_on(self.device_id) async def async_turn_off(self, **kwargs): """Turn the switch off.""" - self._smartbridge.turn_off(self._device_id) + self._smartbridge.turn_off(self.device_id) @property def is_on(self): """Return true if device is on.""" - return self._state["current_state"] > 0 + return self._device["current_state"] > 0 async def async_update(self): """Update when forcing a refresh of the device.""" - self._state = self._smartbridge.get_device_by_id(self._device_id) - _LOGGER.debug(self._state) + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug(self._device) diff --git a/requirements_all.txt b/requirements_all.txt index aae71e2028a178..808b26f86a4650 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1323,7 +1323,7 @@ pylitejet==0.1 pyloopenergy==0.1.3 # homeassistant.components.lutron_caseta -pylutron-caseta==0.5.0 +pylutron-caseta==0.5.1 # homeassistant.components.lutron pylutron==0.2.5 From a5960830d79d64d6f0d3890a71e5f78c70125a13 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Tue, 26 Nov 2019 21:22:12 +0400 Subject: [PATCH 1787/3953] Add host field to add_torrent service (#28653) * Add host field to add_torrent service * Code cleanup * use name instead of host for service * update add_torrent --- .../transmission/.translations/en.json | 40 ++++++-------- .../components/transmission/__init__.py | 52 +++++++++++-------- .../components/transmission/config_flow.py | 24 ++++----- .../components/transmission/sensor.py | 11 +++- .../components/transmission/services.yaml | 3 ++ .../components/transmission/switch.py | 11 +++- .../transmission/test_config_flow.py | 2 - 7 files changed, 79 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index aa8b99a49142ec..45c16be36e2ca9 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -1,42 +1,34 @@ { "config": { - "abort": { - "already_configured": "Host is already configured.", - "one_instance_allowed": "Only a single instance is necessary." - }, - "error": { - "cannot_connect": "Unable to Connect to host", - "name_exists": "Name already exists", - "wrong_credentials": "Wrong username or password" - }, + "title": "Transmission", "step": { - "options": { - "data": { - "scan_interval": "Update frequency" - }, - "title": "Configure Options" - }, "user": { + "title": "Setup Transmission Client", "data": { - "host": "Host", "name": "Name", + "host": "Host", + "username": "Username", "password": "Password", - "port": "Port", - "username": "Username" - }, - "title": "Setup Transmission Client" + "port": "Port" + } } }, - "title": "Transmission" + "error": { + "name_exists": "Name already exists", + "wrong_credentials": "Wrong username or password", + "cannot_connect": "Unable to Connect to host" + }, + "abort": { + "already_configured": "Host is already configured." + } }, "options": { "step": { "init": { + "title": "Configure options for Transmission", "data": { "scan_interval": "Update frequency" - }, - "description": "Configure options for Transmission", - "title": "Configure options for Transmission" + } } } } diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index be41ca85998f6a..7bbc61a192ffaf 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util import slugify from .const import ( ATTR_TORRENT, @@ -28,13 +27,16 @@ DEFAULT_SCAN_INTERVAL, DOMAIN, SERVICE_ADD_TORRENT, + DATA_UPDATED, ) from .errors import AuthenticationError, CannotConnect, UnknownError _LOGGER = logging.getLogger(__name__) -SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string}) +SERVICE_ADD_TORRENT_SCHEMA = vol.Schema( + {vol.Required(ATTR_TORRENT): cv.string, vol.Required(CONF_NAME): cv.string} +) TRANS_SCHEMA = vol.All( vol.Schema( @@ -55,6 +57,8 @@ {DOMAIN: vol.All(cv.ensure_list, [TRANS_SCHEMA])}, extra=vol.ALLOW_EXTRA ) +PLATFORMS = ["sensor", "switch"] + async def async_setup(hass, config): """Import the Transmission Component from config.""" @@ -82,15 +86,15 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload Transmission Entry from config_entry.""" - client = hass.data[DOMAIN][config_entry.entry_id] - hass.services.async_remove(DOMAIN, client.service_name) + client = hass.data[DOMAIN].pop(config_entry.entry_id) if client.unsub_timer: client.unsub_timer() - for component in "sensor", "switch": - await hass.config_entries.async_forward_entry_unload(config_entry, component) + for platform in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(config_entry, platform) - hass.data[DOMAIN].pop(config_entry.entry_id) + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) return True @@ -128,14 +132,10 @@ def __init__(self, hass, config_entry): """Initialize the Transmission RPC API.""" self.hass = hass self.config_entry = config_entry + self.tm_api = None self._tm_data = None self.unsub_timer = None - @property - def service_name(self): - """Return the service name.""" - return slugify(f"{SERVICE_ADD_TORRENT}_{self.config_entry.data[CONF_NAME]}") - @property def api(self): """Return the tm_data object.""" @@ -145,20 +145,20 @@ async def async_setup(self): """Set up the Transmission client.""" try: - api = await get_api(self.hass, self.config_entry.data) + self.tm_api = await get_api(self.hass, self.config_entry.data) except CannotConnect: raise ConfigEntryNotReady except (AuthenticationError, UnknownError): return False - self._tm_data = TransmissionData(self.hass, self.config_entry, api) + self._tm_data = TransmissionData(self.hass, self.config_entry, self.tm_api) await self.hass.async_add_executor_job(self._tm_data.init_torrent_list) await self.hass.async_add_executor_job(self._tm_data.update) self.add_options() self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - for platform in ["sensor", "switch"]: + for platform in PLATFORMS: self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( self.config_entry, platform @@ -167,18 +167,26 @@ async def async_setup(self): def add_torrent(service): """Add new torrent to download.""" + tm_client = None + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_NAME] == service.data[CONF_NAME]: + tm_client = self.hass.data[DOMAIN][entry.entry_id] + break + if tm_client is None: + _LOGGER.error("Transmission instance is not found") + return torrent = service.data[ATTR_TORRENT] if torrent.startswith( ("http", "ftp:", "magnet:") ) or self.hass.config.is_allowed_path(torrent): - api.add_torrent(torrent) + tm_client.tm_api.add_torrent(torrent) else: _LOGGER.warning( "Could not add torrent: unsupported type or no permission" ) self.hass.services.async_register( - DOMAIN, self.service_name, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA + DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA ) self.config_entry.add_update_listener(self.async_options_updated) @@ -200,7 +208,7 @@ def add_options(self): def set_scan_interval(self, scan_interval): """Update scan interval.""" - async def refresh(event_time): + def refresh(event_time): """Get the latest data from Transmission.""" self._tm_data.update() @@ -240,9 +248,9 @@ def host(self): return self.config.data[CONF_HOST] @property - def signal_options_update(self): - """Option update signal per transmission entry.""" - return f"tm-options-{self.host}" + def signal_update(self): + """Update signal per transmission entry.""" + return f"{DATA_UPDATED}-{self.host}" def update(self): """Get the latest data from Transmission instance.""" @@ -260,7 +268,7 @@ def update(self): except TransmissionError: self.available = False _LOGGER.error("Unable to connect to Transmission client %s", self.host) - dispatcher_send(self.hass, self.signal_options_update) + dispatcher_send(self.hass, self.signal_update) def init_torrent_list(self): """Initialize torrent lists.""" diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index d7b9efb15d8727..193c152d7c10dd 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -16,9 +16,19 @@ from .const import DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN from .errors import AuthenticationError, CannotConnect, UnknownError +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } +) + class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a UniFi config flow.""" + """Handle Tansmission config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @@ -57,17 +67,7 @@ async def async_step_user(self, user_input=None): ) return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required(CONF_HOST): str, - vol.Optional(CONF_USERNAME): str, - vol.Optional(CONF_PASSWORD): str, - vol.Required(CONF_PORT, default=DEFAULT_PORT): int, - } - ), - errors=errors, + step_id="user", data_schema=DATA_SCHEMA, errors=errors, ) async def async_step_import(self, import_config): diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 489582de157292..c51d48eb53259f 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -52,6 +52,7 @@ def __init__( self._data = None self.client_name = client_name self.type = sensor_type + self.unsub_update = None @property def name(self): @@ -92,9 +93,9 @@ def device_state_attributes(self): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_dispatcher_connect( + self.unsub_update = async_dispatcher_connect( self.hass, - self._tm_client.api.signal_options_update, + self._tm_client.api.signal_update, self._schedule_immediate_update, ) @@ -102,6 +103,12 @@ async def async_added_to_hass(self): def _schedule_immediate_update(self): self.async_schedule_update_ha_state(True) + async def will_remove_from_hass(self): + """Unsubscribe from update dispatcher.""" + if self.unsub_update: + self.unsub_update() + self.unsub_update = None + def update(self): """Get the latest data from Transmission and updates the state.""" self._data = self._tm_client.api.data diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index ab383584e83fdc..de3314e20f6494 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -1,6 +1,9 @@ add_torrent: description: Add a new torrent to download (URL, magnet link or Base64 encoded). fields: + name: + description: Instance name as entered during entry config + example: Transmission torrent: description: URL, magnet link or Base64 encoded file. example: http://releases.ubuntu.com/19.04/ubuntu-19.04-desktop-amd64.iso.torrent diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 4b93b3f06e205d..adf94c64fd6a3c 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -40,6 +40,7 @@ def __init__(self, switch_type, switch_name, tm_client, name): self._tm_client = tm_client self._state = STATE_OFF self._data = None + self.unsub_update = None @property def name(self): @@ -93,9 +94,9 @@ def turn_off(self, **kwargs): async def async_added_to_hass(self): """Handle entity which will be added.""" - async_dispatcher_connect( + self.unsub_update = async_dispatcher_connect( self.hass, - self._tm_client.api.signal_options_update, + self._tm_client.api.signal_update, self._schedule_immediate_update, ) @@ -103,6 +104,12 @@ async def async_added_to_hass(self): def _schedule_immediate_update(self): self.async_schedule_update_ha_state(True) + async def will_remove_from_hass(self): + """Unsubscribe from update dispatcher.""" + if self.unsub_update: + self.unsub_update() + self.unsub_update = None + def update(self): """Get the latest data from Transmission and updates the state.""" active = None diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 28fbed9ff42fb5..80e6bd55017397 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -98,7 +98,6 @@ async def test_flow_works(hass, api): assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT - # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL # test with all provided result = await flow.async_step_user(MOCK_ENTRY) @@ -110,7 +109,6 @@ async def test_flow_works(hass, api): assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD assert result["data"][CONF_PORT] == PORT - # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL async def test_options(hass): From 42ce5e8b070408ce5f813311fc387e8e323e2015 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 26 Nov 2019 12:22:55 -0500 Subject: [PATCH 1788/3953] Update service domain for local_file from 'camera' to 'local_file' (#28890) * update service domain for local_file from camera to local_file * remove service.yaml entry in camera component as part of change * fix test * move constants to const.py * add local_file/const.py to .coveragerc * remove local_file/const.py from .coveragerc since component has tests --- homeassistant/components/camera/services.yaml | 10 ---------- homeassistant/components/local_file/camera.py | 14 ++++++++------ homeassistant/components/local_file/const.py | 6 ++++++ homeassistant/components/local_file/services.yaml | 2 +- tests/components/local_file/test_camera.py | 3 +-- 5 files changed, 16 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/local_file/const.py diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index 4c2d89db86d2dd..c50e2926a3fdcc 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -68,16 +68,6 @@ record: description: (Optional) Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream. example: 4 -local_file_update_file_path: - description: Update the file_path for a local_file camera. - fields: - entity_id: - description: Name(s) of entities to update. - example: 'camera.local_file' - file_path: - description: Path to the new image file. - example: '/images/newimage.jpg' - onvif_ptz: description: Pan/Tilt/Zoom service for ONVIF camera. fields: diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index d705cefc3fccb4..9bd476cfb275ca 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -11,15 +11,17 @@ CAMERA_SERVICE_SCHEMA, PLATFORM_SCHEMA, ) -from homeassistant.components.camera.const import DOMAIN from homeassistant.helpers import config_validation as cv -_LOGGER = logging.getLogger(__name__) +from .const import ( + CONF_FILE_PATH, + DATA_LOCAL_FILE, + DEFAULT_NAME, + DOMAIN, + SERVICE_UPDATE_FILE_PATH, +) -CONF_FILE_PATH = "file_path" -DATA_LOCAL_FILE = "local_file_cameras" -DEFAULT_NAME = "Local File" -SERVICE_UPDATE_FILE_PATH = "local_file_update_file_path" +_LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/local_file/const.py b/homeassistant/components/local_file/const.py new file mode 100644 index 00000000000000..5225a70daedc7f --- /dev/null +++ b/homeassistant/components/local_file/const.py @@ -0,0 +1,6 @@ +"""Constants for the Local File Camera component.""" +DOMAIN = "local_file" +SERVICE_UPDATE_FILE_PATH = "update_file_path" +CONF_FILE_PATH = "file_path" +DATA_LOCAL_FILE = "local_file_cameras" +DEFAULT_NAME = "Local File" diff --git a/homeassistant/components/local_file/services.yaml b/homeassistant/components/local_file/services.yaml index b359b411b6a8ab..b8c615f3335055 100644 --- a/homeassistant/components/local_file/services.yaml +++ b/homeassistant/components/local_file/services.yaml @@ -1,4 +1,4 @@ -local_file_update_file_path: +update_file_path: description: Use this service to change the file displayed by the camera. fields: entity_id: diff --git a/tests/components/local_file/test_camera.py b/tests/components/local_file/test_camera.py index ae71954bf4ae35..042b0f76400177 100644 --- a/tests/components/local_file/test_camera.py +++ b/tests/components/local_file/test_camera.py @@ -2,8 +2,7 @@ import asyncio from unittest import mock -from homeassistant.components.camera.const import DOMAIN -from homeassistant.components.local_file.camera import SERVICE_UPDATE_FILE_PATH +from homeassistant.components.local_file.const import DOMAIN, SERVICE_UPDATE_FILE_PATH from homeassistant.setup import async_setup_component from tests.common import mock_registry From d0c47dfee25e74bec42c4913413e061df4fd19c1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 18:49:14 +0100 Subject: [PATCH 1789/3953] Move imports to top for webostv (#29102) --- homeassistant/components/webostv/media_player.py | 12 ++++-------- homeassistant/components/webostv/notify.py | 8 +++----- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 913d193845fb01..3bf0011907d079 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -2,13 +2,15 @@ import asyncio from datetime import timedelta import logging -from urllib.parse import urlparse from typing import Dict +from urllib.parse import urlparse +from pylgtv import PyLGTVPairException, WebOsClient import voluptuous as vol +from websockets.exceptions import ConnectionClosed from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -108,9 +110,6 @@ def setup_tv( host, name, customize, config, timeout, hass, add_entities, turn_on_action ): """Set up a LG WebOS TV based on host parameter.""" - from pylgtv import WebOsClient - from pylgtv import PyLGTVPairException - from websockets.exceptions import ConnectionClosed client = WebOsClient(host, config, timeout) @@ -185,7 +184,6 @@ class LgWebOSDevice(MediaPlayerDevice): def __init__(self, host, name, customize, config, timeout, hass, on_action): """Initialize the webos device.""" - from pylgtv import WebOsClient self._client = WebOsClient(host, config, timeout) self._on_script = Script(hass, on_action) if on_action else None @@ -208,7 +206,6 @@ def __init__(self, host, name, customize, config, timeout, hass, on_action): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): """Retrieve the latest data.""" - from websockets.exceptions import ConnectionClosed try: current_input = self._client.get_input() @@ -331,7 +328,6 @@ def supported_features(self): def turn_off(self): """Turn off media player.""" - from websockets.exceptions import ConnectionClosed self._state = STATE_OFF try: diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index f96c20d49aa78f..f62c41e9a95647 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -1,15 +1,16 @@ """Support for LG WebOS TV notification service.""" import logging +from pylgtv import PyLGTVPairException, WebOsClient import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( ATTR_DATA, - BaseNotificationService, PLATFORM_SCHEMA, + BaseNotificationService, ) from homeassistant.const import CONF_FILENAME, CONF_HOST, CONF_ICON +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -26,8 +27,6 @@ def get_service(hass, config, discovery_info=None): """Return the notify service.""" - from pylgtv import WebOsClient - from pylgtv import PyLGTVPairException path = hass.config.path(config.get(CONF_FILENAME)) client = WebOsClient(config.get(CONF_HOST), key_file_path=path, timeout_connect=8) @@ -55,7 +54,6 @@ def __init__(self, client, icon_path): def send_message(self, message="", **kwargs): """Send a message to the tv.""" - from pylgtv import PyLGTVPairException try: data = kwargs.get(ATTR_DATA) From c1163283ffb57381a6d8d64cd620c4e256e09e0d Mon Sep 17 00:00:00 2001 From: SukramJ Date: Tue, 26 Nov 2019 19:02:30 +0100 Subject: [PATCH 1790/3953] Add hvac_action to HomematicIP Cloud Climate (#28859) * Add hvac_action to HomematicIP Cloud Climate * update test data * limit hvac action to radiator only * add checks * Fix test to match new conditions --- .../components/homematicip_cloud/climate.py | 20 +++++++++++++++++++ .../homematicip_cloud/test_climate.py | 14 +++++++++++++ 2 files changed, 34 insertions(+) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 6cc556d5ff2e7d..e3c922dc5775a5 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -10,6 +10,8 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -140,6 +142,24 @@ def hvac_modes(self) -> List[str]: else [HVAC_MODE_AUTO, HVAC_MODE_COOL] ) + @property + def hvac_action(self) -> Optional[str]: + """ + Return the current hvac_action. + + This is only relevant for radiator thermostats. + """ + if ( + self._device.floorHeatingMode == "RADIATOR" + and self._has_radiator_thermostat + and self._heat_mode_enabled + ): + return ( + CURRENT_HVAC_HEAT if self._device.valvePosition else CURRENT_HVAC_IDLE + ) + + return None + @property def preset_mode(self) -> Optional[str]: """Return the current preset mode.""" diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index d40a77751e87ae..db0529294748cd 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -7,8 +7,11 @@ from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, ATTR_PRESET_MODE, ATTR_PRESET_MODES, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -215,6 +218,17 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap): # Only fire event from last async_manipulate_test_data available. assert hmip_device.mock_calls[-1][0] == "fire_update_event" + await async_manipulate_test_data(hass, hmip_device, "floorHeatingMode", "RADIATOR") + await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + await async_manipulate_test_data(hass, hmip_device, "floorHeatingMode", "RADIATOR") + await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + async def test_hmip_heating_group_cool(hass, default_mock_hap): """Test HomematicipHeatingGroup.""" From 499838d5a733ed284dfc45f9abab9bedb5af358d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 26 Nov 2019 13:07:29 -0500 Subject: [PATCH 1791/3953] move service constants to const.py, move custom services to monoprice domain (#29099) --- .../components/media_player/services.yaml | 14 -------------- homeassistant/components/monoprice/const.py | 5 +++++ homeassistant/components/monoprice/media_player.py | 5 +---- homeassistant/components/monoprice/services.yaml | 13 +++++++++++++ tests/components/monoprice/test_media_player.py | 8 +++++--- 5 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/monoprice/const.py diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 5421085c30804e..92aafda81cb0a4 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -107,20 +107,6 @@ media_seek: description: Position to seek to. The format is platform dependent. example: 100 -monoprice_snapshot: - description: Take a snapshot of the media player zone. - fields: - entity_id: - description: Name(s) of entities that will be snapshot. Platform dependent. - example: 'media_player.living_room' - -monoprice_restore: - description: Restore a snapshot of the media player zone. - fields: - entity_id: - description: Name(s) of entities that will be restored. Platform dependent. - example: 'media_player.living_room' - play_media: description: Send the media player the command for playing media. fields: diff --git a/homeassistant/components/monoprice/const.py b/homeassistant/components/monoprice/const.py new file mode 100644 index 00000000000000..e8d813d252947e --- /dev/null +++ b/homeassistant/components/monoprice/const.py @@ -0,0 +1,5 @@ +"""Constants for the Monoprice 6-Zone Amplifier Media Player component.""" + +DOMAIN = "monoprice" +SERVICE_SNAPSHOT = "snapshot" +SERVICE_RESTORE = "restore" diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 5f2af33e23a27e..1b1d9d2adf472c 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -5,7 +5,6 @@ from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -21,6 +20,7 @@ STATE_ON, ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT _LOGGER = logging.getLogger(__name__) @@ -42,9 +42,6 @@ DATA_MONOPRICE = "monoprice" -SERVICE_SNAPSHOT = "snapshot" -SERVICE_RESTORE = "restore" - # Valid zone ids: 11-16 or 21-26 or 31-36 ZONE_IDS = vol.All( vol.Coerce(int), diff --git a/homeassistant/components/monoprice/services.yaml b/homeassistant/components/monoprice/services.yaml index e69de29bb2d1d6..420270e10aca7c 100644 --- a/homeassistant/components/monoprice/services.yaml +++ b/homeassistant/components/monoprice/services.yaml @@ -0,0 +1,13 @@ +snapshot: + description: Take a snapshot of the media player zone. + fields: + entity_id: + description: Name(s) of entities that will be snapshot. Platform dependent. + example: 'media_player.living_room' + +restore: + description: Restore a snapshot of the media player zone. + fields: + entity_id: + description: Name(s) of entities that will be restored. Platform dependent. + example: 'media_player.living_room' diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 36110e6d909d24..a33b85539083c3 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -5,7 +5,6 @@ from collections import defaultdict from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, @@ -19,10 +18,13 @@ from homeassistant.components.monoprice.media_player import ( DATA_MONOPRICE, PLATFORM_SCHEMA, - SERVICE_SNAPSHOT, - SERVICE_RESTORE, setup_platform, ) +from homeassistant.components.monoprice.const import ( + DOMAIN, + SERVICE_RESTORE, + SERVICE_SNAPSHOT, +) import pytest From 33af72a54f796f2ab4dd870b477e8cd0625c60bb Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 19:07:46 +0100 Subject: [PATCH 1792/3953] Move imports to top for uptimerobot (#29103) * Move imports to top for uptimerobot * Move imports to top for twilio_call * Revert twilio_call --- homeassistant/components/uptimerobot/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/uptimerobot/binary_sensor.py b/homeassistant/components/uptimerobot/binary_sensor.py index 2075d9304946ac..401da496d2f89f 100644 --- a/homeassistant/components/uptimerobot/binary_sensor.py +++ b/homeassistant/components/uptimerobot/binary_sensor.py @@ -1,6 +1,7 @@ """A platform that to monitor Uptime Robot monitors.""" import logging +from pyuptimerobot import UptimeRobot import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -18,7 +19,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Uptime Robot binary_sensors.""" - from pyuptimerobot import UptimeRobot up_robot = UptimeRobot() api_key = config.get(CONF_API_KEY) From 03fe7cb34785f88ce517cfae8356ccfbe2586417 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 19:08:16 +0100 Subject: [PATCH 1793/3953] Move imports to top for twilio_call (#29104) --- homeassistant/components/twilio_call/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py index 8270509181447d..83ca081b26e4fc 100644 --- a/homeassistant/components/twilio_call/notify.py +++ b/homeassistant/components/twilio_call/notify.py @@ -2,6 +2,7 @@ import logging import urllib +from twilio.base.exceptions import TwilioRestException import voluptuous as vol from homeassistant.components.notify import ( @@ -42,7 +43,6 @@ def __init__(self, twilio_client, from_number): def send_message(self, message="", **kwargs): """Call to specified target users.""" - from twilio.base.exceptions import TwilioRestException targets = kwargs.get(ATTR_TARGET) From 21ed87a7736e0f5dd12b1707d6ff24efd9a0de40 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 19:08:34 +0100 Subject: [PATCH 1794/3953] Move imports to top for ubee (#29105) --- homeassistant/components/ubee/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ubee/device_tracker.py b/homeassistant/components/ubee/device_tracker.py index 86b2e3c09af0b5..6fe7e90f4c7fac 100644 --- a/homeassistant/components/ubee/device_tracker.py +++ b/homeassistant/components/ubee/device_tracker.py @@ -1,6 +1,8 @@ """Support for Ubee router.""" import logging + +from pyubee import Ubee import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -36,8 +38,6 @@ def get_scanner(hass, config): password = info[CONF_PASSWORD] model = info[CONF_MODEL] - from pyubee import Ubee - ubee = Ubee(host, username, password, model) if not ubee.login(): _LOGGER.error("Login failed") From a7c4abba980d79652df2da967095f3a7c6db7c40 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 19:08:53 +0100 Subject: [PATCH 1795/3953] Move flexit imports at top-level (#29097) --- homeassistant/components/flexit/climate.py | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 951033849b6e85..34ddd9a8ffa6ed 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -13,26 +13,28 @@ """ import logging from typing import List + +from pyflexit.pyflexit import pyflexit import voluptuous as vol -from homeassistant.const import ( - CONF_NAME, - CONF_SLAVE, - TEMP_CELSIUS, - ATTR_TEMPERATURE, - DEVICE_DEFAULT_NAME, -) -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, HVAC_MODE_COOL, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.components.modbus import ( CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN, ) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + CONF_SLAVE, + DEVICE_DEFAULT_NAME, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -61,8 +63,6 @@ class Flexit(ClimateDevice): def __init__(self, hub, modbus_slave, name): """Initialize the unit.""" - from pyflexit import pyflexit - self._hub = hub self._name = name self._slave = modbus_slave @@ -79,7 +79,7 @@ def __init__(self, hub, modbus_slave, name): self._heating = None self._cooling = None self._alarm = False - self.unit = pyflexit.pyflexit(hub, modbus_slave) + self.unit = pyflexit(hub, modbus_slave) @property def supported_features(self): From 7a04f0c0df8d472923d8a66dca25eab7e44cf10a Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 19:09:10 +0100 Subject: [PATCH 1796/3953] Move flunearyou imports at top-level (#29096) --- homeassistant/components/flunearyou/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 0df61fd24e114e..86a97cce8c74e7 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -1,19 +1,21 @@ """Support for user- and CDC-based flu info sensors from Flu Near You.""" -import logging from datetime import timedelta +import logging +from pyflunearyou import Client +from pyflunearyou.errors import FluNearYouError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_STATE, CONF_LATITUDE, - CONF_MONITORED_CONDITIONS, CONF_LONGITUDE, + CONF_MONITORED_CONDITIONS, ) from homeassistant.helpers import aiohttp_client +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -80,8 +82,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Configure the platform and add the sensors.""" - from pyflunearyou import Client - websession = aiohttp_client.async_get_clientsession(hass) latitude = config.get(CONF_LATITUDE, hass.config.latitude) @@ -219,8 +219,6 @@ def __init__(self, client, latitude, longitude, sensor_types): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Update Flu Near You data.""" - from pyflunearyou.errors import FluNearYouError - for key, method in [ (CATEGORY_CDC_REPORT, self._client.cdc_reports.status_by_coordinates), (CATEGORY_USER_REPORT, self._client.user_reports.status_by_coordinates), From 4107fd9a2501c2871a4d384307d58b697c31cc0a Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 19:09:20 +0100 Subject: [PATCH 1797/3953] Move folder_watcher imports at top-level (#29095) --- homeassistant/components/folder_watcher/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py index b328744aaba776..d99e4928cc5d5d 100644 --- a/homeassistant/components/folder_watcher/__init__.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -3,6 +3,8 @@ import os import voluptuous as vol +from watchdog.events import PatternMatchingEventHandler +from watchdog.observers import Observer from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv @@ -50,7 +52,6 @@ def setup(hass, config): def create_event_handler(patterns, hass): """Return the Watchdog EventHandler object.""" - from watchdog.events import PatternMatchingEventHandler class EventHandler(PatternMatchingEventHandler): """Class for handling Watcher events.""" @@ -99,8 +100,6 @@ class Watcher: def __init__(self, path, patterns, hass): """Initialise the watchdog observer.""" - from watchdog.observers import Observer - self._observer = Observer() self._observer.schedule( create_event_handler(patterns, hass), path, recursive=True From 915d23158daa74c7440e8f882cbf7820ba89340e Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 19:09:32 +0100 Subject: [PATCH 1798/3953] Move foobot imports at top-level (#29094) --- homeassistant/components/foobot/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index 8d3cf6de27de2a..efb74e2cc9a1ab 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -1,26 +1,26 @@ """Support for the Foobot indoor air quality monitor.""" import asyncio -import logging from datetime import timedelta +import logging import aiohttp +from foobot_async import FoobotClient import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( - ATTR_TIME, ATTR_TEMPERATURE, + ATTR_TIME, CONF_TOKEN, CONF_USERNAME, TEMP_CELSIUS, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle - _LOGGER = logging.getLogger(__name__) ATTR_HUMIDITY = "humidity" @@ -51,8 +51,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the devices associated with the account.""" - from foobot_async import FoobotClient - token = config.get(CONF_TOKEN) username = config.get(CONF_USERNAME) From 274508f8fd11743bf95f2a048e6a645a09fee5a3 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 19:09:52 +0100 Subject: [PATCH 1799/3953] Move fortigate imports at top-level (#29093) --- homeassistant/components/fortigate/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fortigate/__init__.py b/homeassistant/components/fortigate/__init__.py index d1f6eb52333d4d..6de55ae3d65703 100644 --- a/homeassistant/components/fortigate/__init__.py +++ b/homeassistant/components/fortigate/__init__.py @@ -1,12 +1,13 @@ """Fortigate integration.""" import logging +from pyFGT.fortigate import FGTConnectionError, FortiGate import voluptuous as vol from homeassistant.const import ( + CONF_API_KEY, CONF_DEVICES, CONF_HOST, - CONF_API_KEY, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) @@ -52,8 +53,6 @@ async def async_setup(hass, config): async def async_setup_fortigate(hass, config, host, user, api_key, devices): """Start up the Fortigate component platforms.""" - from pyFGT.fortigate import FGTConnectionError, FortiGate - fgt = FortiGate(host, user, apikey=api_key, disable_request_warnings=True) try: From c84590b18cb7632503a15a6015a0cd37a545fcd9 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 19:10:05 +0100 Subject: [PATCH 1800/3953] Move free_mobile imports at top-level (#29092) --- homeassistant/components/free_mobile/notify.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/free_mobile/notify.py b/homeassistant/components/free_mobile/notify.py index 5733e3c19c03a3..8b5273f39d114a 100644 --- a/homeassistant/components/free_mobile/notify.py +++ b/homeassistant/components/free_mobile/notify.py @@ -1,13 +1,13 @@ -"""Support for thr Free Mobile SMS platform.""" +"""Support for Free Mobile SMS platform.""" import logging +from freesms import FreeClient import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -25,8 +25,6 @@ class FreeSMSNotificationService(BaseNotificationService): def __init__(self, username, access_token): """Initialize the service.""" - from freesms import FreeClient - self.free_client = FreeClient(username, access_token) def send_message(self, message="", **kwargs): From 9a388e2dd2fe582a728e6397669659ba92d19056 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 26 Nov 2019 19:10:20 +0100 Subject: [PATCH 1801/3953] Move dominos imports at top-level (#29090) --- homeassistant/components/dominos/__init__.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/dominos/__init__.py b/homeassistant/components/dominos/__init__.py index 59869ed0a977d1..78852fa2699ada 100644 --- a/homeassistant/components/dominos/__init__.py +++ b/homeassistant/components/dominos/__init__.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pizzapi import Address, Customer, Order +from pizzapi.address import StoreException import voluptuous as vol from homeassistant.components import http @@ -91,8 +93,6 @@ class Dominos: def __init__(self, hass, config): """Set up main service.""" conf = config[DOMAIN] - from pizzapi import Address, Customer - from pizzapi.address import StoreException self.hass = hass self.customer = Customer( @@ -127,8 +127,6 @@ def handle_order(self, call): @Throttle(MIN_TIME_BETWEEN_STORE_UPDATES) def update_closest_store(self): """Update the shared closest store (if open).""" - from pizzapi.address import StoreException - try: self.closest_store = self.address.closest_store() return True @@ -209,8 +207,6 @@ def state(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the order state and refreshes the store.""" - from pizzapi.address import StoreException - try: self.dominos.update_closest_store() except StoreException: @@ -226,9 +222,6 @@ def update(self): def order(self): """Create the order object.""" - from pizzapi import Order - from pizzapi.address import StoreException - if self.dominos.closest_store is None: raise StoreException @@ -246,8 +239,6 @@ def order(self): def place(self): """Place the order.""" - from pizzapi.address import StoreException - try: order = self.order() order.place() From 825ac36ee7e72fba180a91a8a45e4656fc92993d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 19:11:10 +0100 Subject: [PATCH 1802/3953] Move imports to top for uvc (#29072) * Move imports to top for uvc * Fixed linting error * Renamed parameter in constructor to avoid redefining import --- homeassistant/components/uvc/camera.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 20aae3849ab354..b9a6262cd4f3c5 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -3,12 +3,13 @@ import socket import requests +from uvcclient import camera as uvc_camera, nvr import voluptuous as vol +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_PORT, CONF_SSL -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -39,8 +40,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = config[CONF_PORT] ssl = config[CONF_SSL] - from uvcclient import nvr - try: # Exceptions may be raised in all method calls to the nvr library. nvrconn = nvr.UVCRemote(addr, port, key, ssl=ssl) @@ -76,10 +75,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class UnifiVideoCamera(Camera): """A Ubiquiti Unifi Video Camera.""" - def __init__(self, nvr, uuid, name, password): + def __init__(self, camera, uuid, name, password): """Initialize an Unifi camera.""" super().__init__() - self._nvr = nvr + self._nvr = camera self._uuid = uuid self._name = name self._password = password @@ -118,7 +117,6 @@ def model(self): def _login(self): """Login to the camera.""" - from uvcclient import camera as uvc_camera caminfo = self._nvr.get_camera(self._uuid) if self._connect_addr: @@ -160,7 +158,6 @@ def _login(self): def camera_image(self): """Return the image of this camera.""" - from uvcclient import camera as uvc_camera if not self._camera: if not self._login(): @@ -182,7 +179,6 @@ def _get_image(retry=True): def set_motion_detection(self, mode): """Set motion detection on or off.""" - from uvcclient.nvr import NvrError if mode is True: set_mode = "motion" @@ -192,7 +188,7 @@ def set_motion_detection(self, mode): try: self._nvr.set_recordmode(self._uuid, set_mode) self._motion_status = mode - except NvrError as err: + except nvr.NvrError as err: _LOGGER.error("Unable to set recordmode to %s", set_mode) _LOGGER.debug(err) From 4119ade2af83338b9727a99f2e27feb302084c66 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 19:11:43 +0100 Subject: [PATCH 1803/3953] Move imports to top for travisci (#29107) --- homeassistant/components/travisci/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index b86b62fc1e9518..ba698c2b64d9d1 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -1,17 +1,19 @@ """This component provides HA sensor support for Travis CI framework.""" -import logging from datetime import timedelta +import logging +from travispy import TravisPy +from travispy.errors import TravisError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, - CONF_SCAN_INTERVAL, CONF_MONITORED_CONDITIONS, + CONF_SCAN_INTERVAL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -53,8 +55,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Travis CI sensor.""" - from travispy import TravisPy - from travispy.errors import TravisError token = config.get(CONF_API_KEY) repositories = config.get(CONF_REPOSITORY) From a27d8570c87e4449769c86ba2bb362872e69a6a7 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 19:12:00 +0100 Subject: [PATCH 1804/3953] Move imports to top for unifi_direct (#29106) --- homeassistant/components/unifi_direct/device_tracker.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index a526cc926d3107..558a9981171775 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -1,16 +1,17 @@ """Support for Unifi AP direct access.""" -import logging import json +import logging +from pexpect import exceptions, pxssh import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -74,7 +75,6 @@ def get_device_name(self, device): def _connect(self): """Connect to the Unifi AP SSH server.""" - from pexpect import pxssh, exceptions self.ssh = pxssh.pxssh() try: @@ -98,7 +98,6 @@ def _disconnect(self): self.connected = False def _get_update(self): - from pexpect import pxssh, exceptions try: if not self.connected: From 23d4445de32e50e0ea931d0fa2d673e417a80341 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 19:22:28 +0100 Subject: [PATCH 1805/3953] Move imports to top for trackr (#29109) --- homeassistant/components/trackr/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/trackr/device_tracker.py b/homeassistant/components/trackr/device_tracker.py index 580f49b908fa71..07d3c60e256849 100644 --- a/homeassistant/components/trackr/device_tracker.py +++ b/homeassistant/components/trackr/device_tracker.py @@ -1,10 +1,11 @@ """Support for the TrackR platform.""" import logging +from pytrackr.api import trackrApiInterface import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_utc_time_change from homeassistant.util import slugify @@ -27,7 +28,6 @@ class TrackRDeviceScanner: def __init__(self, hass, config: dict, see) -> None: """Initialize the TrackR device scanner.""" - from pytrackr.api import trackrApiInterface self.hass = hass self.api = trackrApiInterface( From c221fc5d22679b201b184fd57d43897e02f34ca1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 19:22:57 +0100 Subject: [PATCH 1806/3953] Move imports to top for thinkingcleaner (#29110) --- homeassistant/components/thinkingcleaner/sensor.py | 5 +++-- homeassistant/components/thinkingcleaner/switch.py | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/thinkingcleaner/sensor.py b/homeassistant/components/thinkingcleaner/sensor.py index 2c2194f6ace8b1..7a45be7eb61dc3 100644 --- a/homeassistant/components/thinkingcleaner/sensor.py +++ b/homeassistant/components/thinkingcleaner/sensor.py @@ -1,6 +1,8 @@ """Support for ThinkingCleaner sensors.""" -import logging from datetime import timedelta +import logging + +from pythinkingcleaner import Discovery from homeassistant import util from homeassistant.helpers.entity import Entity @@ -46,7 +48,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ThinkingCleaner platform.""" - from pythinkingcleaner import Discovery discovery = Discovery() devices = discovery.discover() diff --git a/homeassistant/components/thinkingcleaner/switch.py b/homeassistant/components/thinkingcleaner/switch.py index aa57077734a0f8..88d87e4e5fe28a 100644 --- a/homeassistant/components/thinkingcleaner/switch.py +++ b/homeassistant/components/thinkingcleaner/switch.py @@ -1,10 +1,12 @@ """Support for ThinkingCleaner switches.""" -import time -import logging from datetime import timedelta +import logging +import time + +from pythinkingcleaner import Discovery from homeassistant import util -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -24,7 +26,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ThinkingCleaner platform.""" - from pythinkingcleaner import Discovery discovery = Discovery() devices = discovery.discover() From 595567ad82966f4b6d7df8b01213e8713cb2834c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 19:23:08 +0100 Subject: [PATCH 1807/3953] Move imports to top for trafikverket_weatherstation (#29108) --- homeassistant/components/trafikverket_weatherstation/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index cb80e8d441bfc4..802bb897b961f9 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -5,6 +5,7 @@ import logging import aiohttp +from pytrafikverket.trafikverket_weather import TrafikverketWeather import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -106,7 +107,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Trafikverket sensor platform.""" - from pytrafikverket.trafikverket_weather import TrafikverketWeather sensor_name = config[CONF_NAME] sensor_api = config[CONF_API_KEY] From 2cdd8ad15e12e6b0d4bb96545b3964f8ac65d6fc Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 26 Nov 2019 11:44:40 -0700 Subject: [PATCH 1808/3953] Add new properties and services for V3 SimpliSafe systems (#28997) * Add new properties and services for V3 SimpliSafe systems * Small semantic change * Updated docstrings * Semantics * Streamlined adding V3 properties * Re-add attribute * Bump to 5.3.5 * Owner comments * Correct coroutine name --- .../components/simplisafe/__init__.py | 168 ++++++++++++++++-- .../simplisafe/alarm_control_panel.py | 40 +++-- .../components/simplisafe/manifest.json | 2 +- .../components/simplisafe/services.yaml | 49 ++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 231 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index d57e7b83fa43d1..94da65ee51a847 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -5,6 +5,7 @@ from simplipy import API from simplipy.errors import InvalidCredentialsError, SimplipyError +from simplipy.system.v3 import LevelMap as V3Volume import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -14,6 +15,7 @@ CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME, + STATE_HOME, ) from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady @@ -35,27 +37,57 @@ _LOGGER = logging.getLogger(__name__) +CONF_ACCOUNTS = "accounts" + +DATA_LISTENER = "listener" + +ATTR_ARMED_LIGHT_STATE = "armed_light_state" +ATTR_ARRIVAL_STATE = "arrival_state" ATTR_PIN_LABEL = "label" ATTR_PIN_LABEL_OR_VALUE = "label_or_pin" ATTR_PIN_VALUE = "pin" +ATTR_SECONDS = "seconds" ATTR_SYSTEM_ID = "system_id" +ATTR_TRANSITION = "transition" +ATTR_VOLUME = "volume" +ATTR_VOLUME_PROPERTY = "volume_property" -CONF_ACCOUNTS = "accounts" +STATE_AWAY = "away" +STATE_ENTRY = "entry" +STATE_EXIT = "exit" -DATA_LISTENER = "listener" +VOLUME_PROPERTY_ALARM = "alarm" +VOLUME_PROPERTY_CHIME = "chime" +VOLUME_PROPERTY_VOICE_PROMPT = "voice_prompt" + +SERVICE_BASE_SCHEMA = vol.Schema({vol.Required(ATTR_SYSTEM_ID): cv.positive_int}) -SERVICE_REMOVE_PIN_SCHEMA = vol.Schema( +SERVICE_REMOVE_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend( + {vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string} +) + +SERVICE_SET_DELAY_SCHEMA = SERVICE_BASE_SCHEMA.extend( { - vol.Required(ATTR_SYSTEM_ID): cv.string, - vol.Required(ATTR_PIN_LABEL_OR_VALUE): cv.string, + vol.Required(ATTR_ARRIVAL_STATE): vol.In((STATE_AWAY, STATE_HOME)), + vol.Required(ATTR_TRANSITION): vol.In((STATE_ENTRY, STATE_EXIT)), + vol.Required(ATTR_SECONDS): cv.positive_int, } ) -SERVICE_SET_PIN_SCHEMA = vol.Schema( +SERVICE_SET_LIGHT_SCHEMA = SERVICE_BASE_SCHEMA.extend( + {vol.Required(ATTR_ARMED_LIGHT_STATE): cv.boolean} +) + +SERVICE_SET_PIN_SCHEMA = SERVICE_BASE_SCHEMA.extend( + {vol.Required(ATTR_PIN_LABEL): cv.string, vol.Required(ATTR_PIN_VALUE): cv.string} +) + +SERVICE_SET_VOLUME_SCHEMA = SERVICE_BASE_SCHEMA.extend( { - vol.Required(ATTR_SYSTEM_ID): cv.string, - vol.Required(ATTR_PIN_LABEL): cv.string, - vol.Required(ATTR_PIN_VALUE): cv.string, + vol.Required(ATTR_VOLUME_PROPERTY): vol.In( + (VOLUME_PROPERTY_ALARM, VOLUME_PROPERTY_CHIME, VOLUME_PROPERTY_VOICE_PROMPT) + ), + vol.Required(ATTR_VOLUME): cv.string, } ) @@ -150,7 +182,7 @@ async def async_setup_entry(hass, config_entry): _async_save_refresh_token(hass, config_entry, api.refresh_token) systems = await api.get_systems() - simplisafe = SimpliSafe(hass, config_entry, systems) + simplisafe = SimpliSafe(hass, api, systems, config_entry) await simplisafe.async_update() hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = simplisafe @@ -175,21 +207,122 @@ async def refresh(event_time): async_register_base_station(hass, system, config_entry.entry_id) ) + @callback + def verify_system_exists(coro): + """Log an error if a service call uses an invalid system ID.""" + + async def decorator(call): + """Decorate.""" + system_id = int(call.data[ATTR_SYSTEM_ID]) + if system_id not in systems: + _LOGGER.error("Unknown system ID in service call: %s", system_id) + return + await coro(call) + + return decorator + + @callback + def v3_only(coro): + """Log an error if the decorated coroutine is called with a v2 system.""" + + async def decorator(call): + """Decorate.""" + system = systems[int(call.data[ATTR_SYSTEM_ID])] + if system.version != 3: + _LOGGER.error("Service only available on V3 systems") + return + await coro(call) + + return decorator + + @verify_system_exists @_verify_domain_control async def remove_pin(call): """Remove a PIN.""" - system = systems[int(call.data[ATTR_SYSTEM_ID])] - await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_alarm_duration(call): + """Set the duration of a running alarm.""" + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.set_alarm_duration(call.data[ATTR_SECONDS]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_delay(call): + """Set the delay duration for entry/exit, away/home (any combo).""" + system = systems[call.data[ATTR_SYSTEM_ID]] + coro = getattr( + system, + f"set_{call.data[ATTR_TRANSITION]}_delay_{call.data[ATTR_ARRIVAL_STATE]}", + ) + + try: + await coro(call.data[ATTR_SECONDS]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_armed_light(call): + """Turn the base station light on/off.""" + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.set_light(call.data[ATTR_ARMED_LIGHT_STATE]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists @_verify_domain_control async def set_pin(call): """Set a PIN.""" - system = systems[int(call.data[ATTR_SYSTEM_ID])] - await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE]) + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + + @verify_system_exists + @v3_only + @_verify_domain_control + async def set_volume_property(call): + """Set a volume parameter in an appropriate service call.""" + system = systems[call.data[ATTR_SYSTEM_ID]] + try: + volume = V3Volume[call.data[ATTR_VOLUME]] + except KeyError: + _LOGGER.error("Unknown volume string: %s", call.data[ATTR_VOLUME]) + return + except SimplipyError as err: + _LOGGER.error("Error during service call: %s", err) + return + else: + coro = getattr(system, f"set_{call.data[ATTR_VOLUME_PROPERTY]}_volume") + await coro(volume) for service, method, schema in [ ("remove_pin", remove_pin, SERVICE_REMOVE_PIN_SCHEMA), + ("set_alarm_duration", set_alarm_duration, SERVICE_SET_DELAY_SCHEMA), + ("set_delay", set_delay, SERVICE_SET_DELAY_SCHEMA), + ("set_armed_light", set_armed_light, SERVICE_SET_LIGHT_SCHEMA), ("set_pin", set_pin, SERVICE_SET_PIN_SCHEMA), + ("set_volume_property", set_volume_property, SERVICE_SET_VOLUME_SCHEMA), ]: hass.services.async_register(DOMAIN, service, method, schema=schema) @@ -215,8 +348,9 @@ async def async_unload_entry(hass, entry): class SimpliSafe: """Define a SimpliSafe API object.""" - def __init__(self, hass, config_entry, systems): + def __init__(self, hass, api, systems, config_entry): """Initialize.""" + self._api = api self._config_entry = config_entry self._hass = hass self.last_event_data = {} @@ -238,9 +372,9 @@ async def _update_system(self, system): self.last_event_data[system.system_id] = latest_event - if system.api.refresh_token_dirty: + if self._api.refresh_token_dirty: _async_save_refresh_token( - self._hass, self._config_entry, system.api.refresh_token + self._hass, self._config_entry, self._api.refresh_token ) async def async_update(self): diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 96f3fa05f6bc4e..9671d56c873483 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -28,14 +28,23 @@ _LOGGER = logging.getLogger(__name__) ATTR_ALARM_ACTIVE = "alarm_active" +ATTR_ALARM_DURATION = "alarm_duration" +ATTR_ALARM_VOLUME = "alarm_volume" ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level" +ATTR_CHIME_VOLUME = "chime_volume" +ATTR_ENTRY_DELAY_AWAY = "entry_delay_away" +ATTR_ENTRY_DELAY_HOME = "entry_delay_home" +ATTR_EXIT_DELAY_AWAY = "exit_delay_away" +ATTR_EXIT_DELAY_HOME = "exit_delay_home" ATTR_GSM_STRENGTH = "gsm_strength" ATTR_LAST_EVENT_INFO = "last_event_info" ATTR_LAST_EVENT_SENSOR_NAME = "last_event_sensor_name" ATTR_LAST_EVENT_SENSOR_TYPE = "last_event_sensor_type" ATTR_LAST_EVENT_TIMESTAMP = "last_event_timestamp" ATTR_LAST_EVENT_TYPE = "last_event_type" +ATTR_LIGHT = "light" ATTR_RF_JAMMING = "rf_jamming" +ATTR_VOICE_PROMPT_VOLUME = "voice_prompt_volume" ATTR_WALL_POWER_LEVEL = "wall_power_level" ATTR_WIFI_STRENGTH = "wifi_strength" @@ -68,16 +77,26 @@ def __init__(self, simplisafe, system, code): self._simplisafe = simplisafe self._state = None - # Some properties only exist for V2 or V3 systems: - for prop in ( - ATTR_BATTERY_BACKUP_POWER_LEVEL, - ATTR_GSM_STRENGTH, - ATTR_RF_JAMMING, - ATTR_WALL_POWER_LEVEL, - ATTR_WIFI_STRENGTH, - ): - if hasattr(system, prop): - self._attrs[prop] = getattr(system, prop) + self._attrs.update({ATTR_ALARM_ACTIVE: self._system.alarm_going_off}) + if self._system.version == 3: + self._attrs.update( + { + ATTR_ALARM_DURATION: self._system.alarm_duration, + ATTR_ALARM_VOLUME: self._system.alarm_volume.name, + ATTR_BATTERY_BACKUP_POWER_LEVEL: self._system.battery_backup_power_level, + ATTR_CHIME_VOLUME: self._system.chime_volume.name, + ATTR_ENTRY_DELAY_AWAY: self._system.entry_delay_away, + ATTR_ENTRY_DELAY_HOME: self._system.entry_delay_home, + ATTR_EXIT_DELAY_AWAY: self._system.exit_delay_away, + ATTR_EXIT_DELAY_HOME: self._system.exit_delay_home, + ATTR_GSM_STRENGTH: self._system.gsm_strength, + ATTR_LIGHT: self._system.light, + ATTR_RF_JAMMING: self._system.rf_jamming, + ATTR_VOICE_PROMPT_VOLUME: self._system.voice_prompt_volume.name, + ATTR_WALL_POWER_LEVEL: self._system.wall_power_level, + ATTR_WIFI_STRENGTH: self._system.wifi_strength, + } + ) @property def changed_by(self): @@ -160,7 +179,6 @@ async def async_update(self): last_event = self._simplisafe.last_event_data[self._system.system_id] self._attrs.update( { - ATTR_ALARM_ACTIVE: self._system.alarm_going_off, ATTR_LAST_EVENT_INFO: last_event["info"], ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"], ATTR_LAST_EVENT_SENSOR_TYPE: EntityTypes(last_event["sensorType"]).name, diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 61a16f8aa44aed..4115ce455b5d46 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/integrations/simplisafe", "requirements": [ - "simplisafe-python==5.2.0" + "simplisafe-python==5.3.5" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/simplisafe/services.yaml b/homeassistant/components/simplisafe/services.yaml index 52e66a435c6626..d8a4973b49edae 100644 --- a/homeassistant/components/simplisafe/services.yaml +++ b/homeassistant/components/simplisafe/services.yaml @@ -10,11 +10,46 @@ remove_pin: label_or_pin: description: The label/value to remove. example: Test PIN +set_alarm_duration: + description: "Set the duration (in seconds) of an active alarm" + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + seconds: + description: The number of seconds to sound the alarm + example: 120 +set_delay: + description: > + Set a duration for how long the base station should delay when transitioning + between states + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + arrival_state: + description: The target "arrival" state (away, home) + example: away + transition: + description: The system state transition to affect (entry, exit) + example: exit + seconds: + description: "The number of seconds to delay" + example: 120 +set_light: + description: "Turn the base station light on/off" + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + armed_light_state: + description: "True for on, False for off" + example: "True" set_pin: description: Set/update a PIN fields: system_id: - description: The SimpliSafe system ID to affect. + description: The SimpliSafe system ID to affect example: 123987 label: description: The label of the PIN @@ -22,3 +57,15 @@ set_pin: pin: description: The value of the PIN example: 1256 +set_volume_property: + description: Set a level for one of the base station's various volumes + fields: + system_id: + description: The SimpliSafe system ID to affect + example: 123987 + volume_property: + description: The volume property to set (alarm, chime, voice_prompt) + example: voice_prompt + volume: + description: "A volume (off, low, medium, high)" + example: low diff --git a/requirements_all.txt b/requirements_all.txt index 808b26f86a4650..477cf1314de3ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1788,7 +1788,7 @@ shodan==1.20.0 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==5.2.0 +simplisafe-python==5.3.5 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfeba689189fb0..06be8aac043e0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -556,7 +556,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.simplisafe -simplisafe-python==5.2.0 +simplisafe-python==5.3.5 # homeassistant.components.sleepiq sleepyq==0.7 From 1c73ac5df88a969c622bd48c788c7fed0d4fbd59 Mon Sep 17 00:00:00 2001 From: rappenze Date: Tue, 26 Nov 2019 19:46:53 +0100 Subject: [PATCH 1809/3953] Fix Vicare imports (#29071) * fix import * Update water_heater.py * use ValueError * use ValueError --- homeassistant/components/vicare/climate.py | 3 +-- homeassistant/components/vicare/water_heater.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 7e330383b30de0..4f6f0cedcd98ee 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -1,7 +1,6 @@ """Viessmann ViCare climate device.""" import logging import requests -import simplejson from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -169,7 +168,7 @@ def update(self): ] = self._api.getReturnTemperature() except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") - except simplejson.errors.JSONDecodeError: + except ValueError: _LOGGER.error("Unable to decode data from ViCare server") @property diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 1f56c46dc1ce60..eefacf99c39674 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,7 +1,6 @@ """Viessmann ViCare water_heater device.""" import logging import requests -import simplejson from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, @@ -93,7 +92,7 @@ def update(self): self._current_mode = self._api.getActiveMode() except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") - except simplejson.errors.JSONDecodeError: + except ValueError: _LOGGER.error("Unable to decode data from ViCare server") @property From 2da37778af673b3df69fb72c91f93ae0bb1f16ff Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 26 Nov 2019 14:06:34 -0500 Subject: [PATCH 1810/3953] Update service domain for bluesound from 'media_player' to 'bluesound' (#29111) * move service constants to const.py, move services to bluesound domain * Remove bluesound services from media_player/services.yaml --- .coveragerc | 2 +- homeassistant/components/bluesound/const.py | 6 ++++ .../components/bluesound/media_player.py | 12 ++++--- .../components/bluesound/services.yaml | 30 ++++++++++++++++++ .../components/media_player/services.yaml | 31 ------------------- 5 files changed, 44 insertions(+), 37 deletions(-) create mode 100644 homeassistant/components/bluesound/const.py diff --git a/.coveragerc b/.coveragerc index 20be7adbd9455f..503cd5ad761ea1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -83,7 +83,7 @@ omit = homeassistant/components/blinkt/light.py homeassistant/components/blockchain/sensor.py homeassistant/components/bloomsky/* - homeassistant/components/bluesound/media_player.py + homeassistant/components/bluesound/* homeassistant/components/bluetooth_le_tracker/device_tracker.py homeassistant/components/bluetooth_tracker/device_tracker.py homeassistant/components/bme280/sensor.py diff --git a/homeassistant/components/bluesound/const.py b/homeassistant/components/bluesound/const.py new file mode 100644 index 00000000000000..af1a8e5187c2c3 --- /dev/null +++ b/homeassistant/components/bluesound/const.py @@ -0,0 +1,6 @@ +"""Constants for the Bluesound HiFi wireless speakers and audio integrations component.""" +DOMAIN = "bluesound" +SERVICE_CLEAR_TIMER = "clear_sleep_timer" +SERVICE_JOIN = "join" +SERVICE_SET_TIMER = "set_sleep_timer" +SERVICE_UNJOIN = "unjoin" diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 7b2719c1e4e9ee..5a9f3561dc9354 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -15,7 +15,6 @@ from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, - DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, @@ -50,6 +49,13 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import Throttle import homeassistant.util.dt as dt_util +from .const import ( + DOMAIN, + SERVICE_CLEAR_TIMER, + SERVICE_JOIN, + SERVICE_SET_TIMER, + SERVICE_UNJOIN, +) _LOGGER = logging.getLogger(__name__) @@ -62,10 +68,6 @@ NODE_OFFLINE_CHECK_TIMEOUT = 180 NODE_RETRY_INITIATION = timedelta(minutes=3) -SERVICE_CLEAR_TIMER = "bluesound_clear_sleep_timer" -SERVICE_JOIN = "bluesound_join" -SERVICE_SET_TIMER = "bluesound_set_sleep_timer" -SERVICE_UNJOIN = "bluesound_unjoin" STATE_GROUPED = "grouped" SYNC_STATUS_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/bluesound/services.yaml b/homeassistant/components/bluesound/services.yaml index e69de29bb2d1d6..6c85c77e961c21 100644 --- a/homeassistant/components/bluesound/services.yaml +++ b/homeassistant/components/bluesound/services.yaml @@ -0,0 +1,30 @@ +join: + description: Group player together. + fields: + master: + description: Entity ID of the player that should become the master of the group. + example: 'media_player.bluesound_livingroom' + entity_id: + description: Name(s) of entities that will coordinate the grouping. Platform dependent. + example: 'media_player.bluesound_livingroom' + +unjoin: + description: Unjoin the player from a group. + fields: + entity_id: + description: Name(s) of entities that will be unjoined from their group. Platform dependent. + example: 'media_player.bluesound_livingroom' + +set_sleep_timer: + description: "Set a Bluesound timer. It will increase timer in steps: 15, 30, 45, 60, 90, 0" + fields: + entity_id: + description: Name(s) of entities that will have a timer set. + example: 'media_player.bluesound_livingroom' + +clear_sleep_timer: + description: Clear a Bluesound timer. + fields: + entity_id: + description: Name(s) of entities that will have the timer cleared. + example: 'media_player.bluesound_livingroom' \ No newline at end of file diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 92aafda81cb0a4..da229ada65cbea 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -244,37 +244,6 @@ yamaha_enable_output: description: Boolean indicating if port should be enabled or not. example: true -bluesound_join: - description: Group player together. - fields: - master: - description: Entity ID of the player that should become the master of the group. - example: 'media_player.bluesound_livingroom' - entity_id: - description: Name(s) of entities that will coordinate the grouping. Platform dependent. - example: 'media_player.bluesound_livingroom' - -bluesound_unjoin: - description: Unjoin the player from a group. - fields: - entity_id: - description: Name(s) of entities that will be unjoined from their group. Platform dependent. - example: 'media_player.bluesound_livingroom' - -bluesound_set_sleep_timer: - description: "Set a Bluesound timer. It will increase timer in steps: 15, 30, 45, 60, 90, 0" - fields: - entity_id: - description: Name(s) of entities that will have a timer set. - example: 'media_player.bluesound_livingroom' - -bluesound_clear_sleep_timer: - description: Clear a Bluesound timer. - fields: - entity_id: - description: Name(s) of entities that will have the timer cleared. - example: 'media_player.bluesound_livingroom' - songpal_set_sound_setting: description: Change sound setting. From 9e01591cacf00c6439bd95886bf0fbf511a9a384 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 26 Nov 2019 14:10:13 -0500 Subject: [PATCH 1811/3953] Update service domain for blackbird from 'media_player' to 'blackbird' (#29112) * move service constants to const.py, update blackbird custom service domain * Readd bluesound services.yaml entries since it should be part of a different branch --- homeassistant/components/blackbird/const.py | 3 +++ homeassistant/components/blackbird/media_player.py | 3 +-- homeassistant/components/blackbird/services.yaml | 10 ++++++++++ homeassistant/components/media_player/services.yaml | 10 ---------- tests/components/blackbird/test_media_player.py | 3 +-- 5 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/blackbird/const.py diff --git a/homeassistant/components/blackbird/const.py b/homeassistant/components/blackbird/const.py new file mode 100644 index 00000000000000..aa8d7e7d514e8a --- /dev/null +++ b/homeassistant/components/blackbird/const.py @@ -0,0 +1,3 @@ +"""Constants for the Monoprice Blackbird Matrix Switch component.""" +DOMAIN = "blackbird" +SERVICE_SETALLZONES = "set_all_zones" diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index e1aa7200c07394..08efc1e664715d 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -8,7 +8,6 @@ from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -23,6 +22,7 @@ STATE_ON, ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_SETALLZONES _LOGGER = logging.getLogger(__name__) @@ -39,7 +39,6 @@ DATA_BLACKBIRD = "blackbird" -SERVICE_SETALLZONES = "blackbird_set_all_zones" ATTR_SOURCE = "source" BLACKBIRD_SETALLZONES_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( diff --git a/homeassistant/components/blackbird/services.yaml b/homeassistant/components/blackbird/services.yaml index e69de29bb2d1d6..d541e21049da16 100644 --- a/homeassistant/components/blackbird/services.yaml +++ b/homeassistant/components/blackbird/services.yaml @@ -0,0 +1,10 @@ +set_all_zones: + description: Set all Blackbird zones to a single source. + fields: + entity_id: + description: Name of any blackbird zone. + example: 'media_player.zone_1' + source: + description: Name of source to switch to. + example: 'Source 1' + diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index da229ada65cbea..704386ea814631 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -258,16 +258,6 @@ songpal_set_sound_setting: description: Value to set. example: 'on' -blackbird_set_all_zones: - description: Set all Blackbird zones to a single source. - fields: - entity_id: - description: Name of any blackbird zone. - example: 'media_player.zone_1' - source: - description: Name of source to switch to. - example: 'Source 1' - epson_select_cmode: description: Select Color mode of Epson projector fields: diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index 34309fdbcf30bc..0b6eda16c152fa 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -5,7 +5,6 @@ from collections import defaultdict from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_SELECT_SOURCE, @@ -16,9 +15,9 @@ from homeassistant.components.blackbird.media_player import ( DATA_BLACKBIRD, PLATFORM_SCHEMA, - SERVICE_SETALLZONES, setup_platform, ) +from homeassistant.components.blackbird.const import DOMAIN, SERVICE_SETALLZONES import pytest From c21650473ae7447e992b205b26f1c1df4dda406a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 26 Nov 2019 14:12:50 -0500 Subject: [PATCH 1812/3953] Update service domain for epson from 'media_player' to 'epson' (#29113) * update .coveragerc, move epson constants to const.py, move epson cutsom service to epson domain * Newline in services.yaml --- .coveragerc | 1 + homeassistant/components/epson/const.py | 10 ++++++++++ homeassistant/components/epson/media_player.py | 17 ++++++++--------- homeassistant/components/epson/services.yaml | 9 +++++++++ .../components/media_player/services.yaml | 10 ---------- 5 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/epson/const.py diff --git a/.coveragerc b/.coveragerc index 503cd5ad761ea1..de5bad9c077dd1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -199,6 +199,7 @@ omit = homeassistant/components/envirophat/sensor.py homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py + homeassistant/components/epson/const.py homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py homeassistant/components/eq3btsmart/climate.py diff --git a/homeassistant/components/epson/const.py b/homeassistant/components/epson/const.py new file mode 100644 index 00000000000000..23f3b081d01352 --- /dev/null +++ b/homeassistant/components/epson/const.py @@ -0,0 +1,10 @@ +"""Constants for the Epson projector component.""" +DOMAIN = "epson" +SERVICE_SELECT_CMODE = "select_cmode" + +ATTR_CMODE = "cmode" + +DATA_EPSON = "epson" +DEFAULT_NAME = "EPSON Projector" + +SUPPORT_CMODE = 33001 diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 638f012ac7ab77..f3428602fadd29 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -29,7 +29,6 @@ from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, @@ -49,17 +48,17 @@ ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from .const import ( + ATTR_CMODE, + DATA_EPSON, + DEFAULT_NAME, + DOMAIN, + SERVICE_SELECT_CMODE, + SUPPORT_CMODE, +) _LOGGER = logging.getLogger(__name__) -ATTR_CMODE = "cmode" - -DATA_EPSON = "epson" -DEFAULT_NAME = "EPSON Projector" - -SERVICE_SELECT_CMODE = "epson_select_cmode" -SUPPORT_CMODE = 33001 - SUPPORT_EPSON = ( SUPPORT_TURN_ON | SUPPORT_TURN_OFF diff --git a/homeassistant/components/epson/services.yaml b/homeassistant/components/epson/services.yaml index e69de29bb2d1d6..6e9724c95f72d7 100644 --- a/homeassistant/components/epson/services.yaml +++ b/homeassistant/components/epson/services.yaml @@ -0,0 +1,9 @@ +select_cmode: + description: Select Color mode of Epson projector + fields: + entity_id: + description: Name of projector + example: 'media_player.epson_projector' + cmode: + description: Name of Cmode + example: 'cinema' diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 704386ea814631..8b7614ddfdbb4c 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -257,13 +257,3 @@ songpal_set_sound_setting: value: description: Value to set. example: 'on' - -epson_select_cmode: - description: Select Color mode of Epson projector - fields: - entity_id: - description: Name of projector - example: 'media_player.epson_projector' - cmode: - description: Name of Cmode - example: 'cinema' From a37260faa99001b80b88c84ecd28f6470b83dd73 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Tue, 26 Nov 2019 22:17:11 +0300 Subject: [PATCH 1813/3953] StarLine integration (#27197) * Device Tracker works * Device Tracker works * Binary Sensor * Sensor * Lock * Switch and service * New switches * Update interval options * WIP * Translation errors * Check online state * WIP * Move to aiohttp * Some checks * CI * CI * .coveragerc * Black * icon_for_signal_level test * update_interval renamed to scan_interval * async logic * Fix cookie read * Requirement starline * Reformat * Requirements updated * ConfigEntryNotReady * Requirement starline * Lint fix * Requirement starline * available status * Translations * Expiration to config * CI * Linter fix * Small renaming * Update slnet token * Starline version bump * Fix updates * Black * Small fix * Removed unused fields * CI * set_scan_interval service * deps updated * Horn switch * Starline lib updated * Starline lib updated * Black * Support multiple integrations * Review * async_will_remove_from_hass * Deps updated * Test config flow * Requirements * CI * Review * Review * Review * Review * Review * CI * pylint fix * Review * Support "mayak" devices * Icons removed * Removed options_flow * Removed options_flow test * Removed options_flow test --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/starline/.translations/en.json | 42 ++++ homeassistant/components/starline/__init__.py | 85 +++++++ homeassistant/components/starline/account.py | 142 +++++++++++ .../components/starline/binary_sensor.py | 58 +++++ .../components/starline/config_flow.py | 229 ++++++++++++++++++ homeassistant/components/starline/const.py | 27 +++ .../components/starline/device_tracker.py | 60 +++++ homeassistant/components/starline/entity.py | 59 +++++ homeassistant/components/starline/lock.py | 72 ++++++ .../components/starline/manifest.json | 13 + homeassistant/components/starline/sensor.py | 95 ++++++++ .../components/starline/services.yaml | 10 + .../components/starline/strings.json | 42 ++++ homeassistant/components/starline/switch.py | 86 +++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/helpers/icon.py | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/starline/__init__.py | 1 + tests/components/starline/test_config_flow.py | 126 ++++++++++ tests/helpers/test_icon.py | 12 + 23 files changed, 1179 insertions(+) create mode 100644 homeassistant/components/starline/.translations/en.json create mode 100644 homeassistant/components/starline/__init__.py create mode 100644 homeassistant/components/starline/account.py create mode 100644 homeassistant/components/starline/binary_sensor.py create mode 100644 homeassistant/components/starline/config_flow.py create mode 100644 homeassistant/components/starline/const.py create mode 100644 homeassistant/components/starline/device_tracker.py create mode 100644 homeassistant/components/starline/entity.py create mode 100644 homeassistant/components/starline/lock.py create mode 100644 homeassistant/components/starline/manifest.json create mode 100644 homeassistant/components/starline/sensor.py create mode 100644 homeassistant/components/starline/services.yaml create mode 100644 homeassistant/components/starline/strings.json create mode 100644 homeassistant/components/starline/switch.py create mode 100644 tests/components/starline/__init__.py create mode 100644 tests/components/starline/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index de5bad9c077dd1..7164dd3d35ed58 100644 --- a/.coveragerc +++ b/.coveragerc @@ -646,6 +646,7 @@ omit = homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/media_player.py homeassistant/components/squeezebox/media_player.py + homeassistant/components/starline/* homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* diff --git a/CODEOWNERS b/CODEOWNERS index c9ef0123b22d73..a489786f48fc84 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -292,6 +292,7 @@ homeassistant/components/spaceapi/* @fabaff homeassistant/components/speedtestdotnet/* @rohankapoorcom homeassistant/components/spider/* @peternijssen homeassistant/components/sql/* @dgomes +homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm homeassistant/components/stream/* @hunterjm diff --git a/homeassistant/components/starline/.translations/en.json b/homeassistant/components/starline/.translations/en.json new file mode 100644 index 00000000000000..afe8f8c732b043 --- /dev/null +++ b/homeassistant/components/starline/.translations/en.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Incorrect application id or secret", + "error_auth_mfa": "Incorrect code", + "error_auth_user": "Incorrect username or password" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App ID", + "app_secret": "Secret" + }, + "description": "Application ID and secret code from StarLine developer account", + "title": "Application credentials" + }, + "auth_captcha": { + "data": { + "captcha_code": "Code from image" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS code" + }, + "description": "Enter the code sent to phone {phone_number}", + "title": "Two-factor authorization" + }, + "auth_user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "StarLine account email and password", + "title": "User credentials" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py new file mode 100644 index 00000000000000..22772282a7c00b --- /dev/null +++ b/homeassistant/components/starline/__init__.py @@ -0,0 +1,85 @@ +"""The StarLine component.""" +import voluptuous as vol +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .account import StarlineAccount +from .const import ( + DOMAIN, + PLATFORMS, + SERVICE_UPDATE_STATE, + SERVICE_SET_SCAN_INTERVAL, + CONF_SCAN_INTERVAL, + DEFAULT_SCAN_INTERVAL, +) + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Set up configured StarLine.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up the StarLine device from a config entry.""" + account = StarlineAccount(hass, config_entry) + await account.update() + if not account.api.available: + raise ConfigEntryNotReady + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + hass.data[DOMAIN][config_entry.entry_id] = account + + device_registry = await hass.helpers.device_registry.async_get_registry() + for device in account.api.devices.values(): + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, **account.device_info(device) + ) + + for domain in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, domain) + ) + + async def async_set_scan_interval(call): + """Service for set scan interval.""" + options = dict(config_entry.options) + options[CONF_SCAN_INTERVAL] = call.data[CONF_SCAN_INTERVAL] + hass.config_entries.async_update_entry(entry=config_entry, options=options) + + hass.services.async_register(DOMAIN, SERVICE_UPDATE_STATE, account.update) + hass.services.async_register( + DOMAIN, + SERVICE_SET_SCAN_INTERVAL, + async_set_scan_interval, + schema=vol.Schema( + { + vol.Required(CONF_SCAN_INTERVAL): vol.All( + vol.Coerce(int), vol.Range(min=10) + ) + } + ), + ) + + config_entry.add_update_listener(async_options_updated) + await async_options_updated(hass, config_entry) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + for domain in PLATFORMS: + await hass.config_entries.async_forward_entry_unload(config_entry, domain) + + account: StarlineAccount = hass.data[DOMAIN][config_entry.entry_id] + account.unload() + return True + + +async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Triggered by config entry options updates.""" + account: StarlineAccount = hass.data[DOMAIN][config_entry.entry_id] + scan_interval = config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + account.set_update_interval(scan_interval) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py new file mode 100644 index 00000000000000..2e7653eb380bb9 --- /dev/null +++ b/homeassistant/components/starline/account.py @@ -0,0 +1,142 @@ +"""StarLine Account.""" +from datetime import timedelta, datetime +from typing import Callable, Optional, Dict, Any +from starline import StarlineApi, StarlineDevice + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + DOMAIN, + LOGGER, + DEFAULT_SCAN_INTERVAL, + DATA_USER_ID, + DATA_SLNET_TOKEN, + DATA_SLID_TOKEN, + DATA_EXPIRES, +) + + +class StarlineAccount: + """StarLine Account class.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): + """Constructor.""" + self._hass: HomeAssistant = hass + self._config_entry: ConfigEntry = config_entry + self._update_interval: int = DEFAULT_SCAN_INTERVAL + self._unsubscribe_auto_updater: Optional[Callable] = None + self._api: StarlineApi = StarlineApi( + config_entry.data[DATA_USER_ID], config_entry.data[DATA_SLNET_TOKEN] + ) + + def _check_slnet_token(self) -> None: + """Check SLNet token expiration and update if needed.""" + now = datetime.now().timestamp() + slnet_token_expires = self._config_entry.data[DATA_EXPIRES] + + if now + self._update_interval > slnet_token_expires: + self._update_slnet_token() + + def _update_slnet_token(self) -> None: + """Update SLNet token.""" + slid_token = self._config_entry.data[DATA_SLID_TOKEN] + + try: + slnet_token, slnet_token_expires, user_id = self._api.get_user_id( + slid_token + ) + self._api.set_slnet_token(slnet_token) + self._api.set_user_id(user_id) + self._hass.config_entries.async_update_entry( + self._config_entry, + data={ + **self._config_entry.data, + DATA_SLNET_TOKEN: slnet_token, + DATA_EXPIRES: slnet_token_expires, + DATA_USER_ID: user_id, + }, + ) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Error updating SLNet token: %s", err) + pass + + def _update_data(self): + """Update StarLine data.""" + self._check_slnet_token() + self._api.update() + + @property + def api(self) -> StarlineApi: + """Return the instance of the API.""" + return self._api + + async def update(self, unused=None): + """Update StarLine data.""" + await self._hass.async_add_executor_job(self._update_data) + + def set_update_interval(self, interval: int) -> None: + """Set StarLine API update interval.""" + LOGGER.debug("Setting update interval: %ds", interval) + self._update_interval = interval + if self._unsubscribe_auto_updater is not None: + self._unsubscribe_auto_updater() + + delta = timedelta(seconds=interval) + self._unsubscribe_auto_updater = async_track_time_interval( + self._hass, self.update, delta + ) + + def unload(self): + """Unload StarLine API.""" + LOGGER.debug("Unloading StarLine API.") + if self._unsubscribe_auto_updater is not None: + self._unsubscribe_auto_updater() + self._unsubscribe_auto_updater = None + + @staticmethod + def device_info(device: StarlineDevice) -> Dict[str, Any]: + """Device information for entities.""" + return { + "identifiers": {(DOMAIN, device.device_id)}, + "manufacturer": "StarLine", + "name": device.name, + "sw_version": device.fw_version, + "model": device.typename, + } + + @staticmethod + def gps_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for device tracker.""" + return { + "updated": datetime.utcfromtimestamp(device.position["ts"]).isoformat(), + "online": device.online, + } + + @staticmethod + def balance_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for balance sensor.""" + return { + "operator": device.balance.get("operator"), + "state": device.balance.get("state"), + "updated": device.balance.get("ts"), + } + + @staticmethod + def gsm_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for GSM sensor.""" + return { + "raw": device.gsm_level, + "imei": device.imei, + "phone": device.phone, + "online": device.online, + } + + @staticmethod + def engine_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for engine switch.""" + return { + "autostart": device.car_state.get("r_start"), + "ignition": device.car_state.get("run"), + } diff --git a/homeassistant/components/starline/binary_sensor.py b/homeassistant/components/starline/binary_sensor.py new file mode 100644 index 00000000000000..fd28ff74cf4c53 --- /dev/null +++ b/homeassistant/components/starline/binary_sensor.py @@ -0,0 +1,58 @@ +"""Reads vehicle status from StarLine API.""" +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_POWER, +) +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + +SENSOR_TYPES = { + "hbrake": ["Hand Brake", DEVICE_CLASS_POWER], + "hood": ["Hood", DEVICE_CLASS_DOOR], + "trunk": ["Trunk", DEVICE_CLASS_DOOR], + "alarm": ["Alarm", DEVICE_CLASS_PROBLEM], + "door": ["Doors", DEVICE_CLASS_LOCK], +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the StarLine sensors.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + for key, value in SENSOR_TYPES.items(): + if key in device.car_state: + sensor = StarlineSensor(account, device, key, *value) + if sensor.is_on is not None: + entities.append(sensor) + async_add_entities(entities) + + +class StarlineSensor(StarlineEntity, BinarySensorDevice): + """Representation of a StarLine binary sensor.""" + + def __init__( + self, + account: StarlineAccount, + device: StarlineDevice, + key: str, + name: str, + device_class: str, + ): + """Constructor.""" + super().__init__(account, device, key, name) + self._device_class = device_class + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return self._device_class + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._device.car_state.get(self._key) diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py new file mode 100644 index 00000000000000..2253cc3cd22ea3 --- /dev/null +++ b/homeassistant/components/starline/config_flow.py @@ -0,0 +1,229 @@ +"""Config flow to configure StarLine component.""" +from typing import Optional +from starline import StarlineAuth +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD + +from .const import ( # pylint: disable=unused-import + DOMAIN, + CONF_APP_ID, + CONF_APP_SECRET, + CONF_MFA_CODE, + CONF_CAPTCHA_CODE, + LOGGER, + ERROR_AUTH_APP, + ERROR_AUTH_USER, + ERROR_AUTH_MFA, + DATA_USER_ID, + DATA_SLNET_TOKEN, + DATA_SLID_TOKEN, + DATA_EXPIRES, +) + + +class StarlineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a StarLine config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize flow.""" + self._app_id: Optional[str] = None + self._app_secret: Optional[str] = None + self._username: Optional[str] = None + self._password: Optional[str] = None + self._mfa_code: Optional[str] = None + + self._app_code = None + self._app_token = None + self._user_slid = None + self._user_id = None + self._slnet_token = None + self._slnet_token_expires = None + self._captcha_image = None + self._captcha_sid = None + self._captcha_code = None + self._phone_number = None + + self._auth = StarlineAuth() + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + return await self.async_step_auth_app(user_input) + + async def async_step_auth_app(self, user_input=None, error=None): + """Authenticate application step.""" + if user_input is not None: + self._app_id = user_input[CONF_APP_ID] + self._app_secret = user_input[CONF_APP_SECRET] + return await self._async_authenticate_app(error) + return self._async_form_auth_app(error) + + async def async_step_auth_user(self, user_input=None, error=None): + """Authenticate user step.""" + if user_input is not None: + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + return await self._async_authenticate_user(error) + return self._async_form_auth_user(error) + + async def async_step_auth_mfa(self, user_input=None, error=None): + """Authenticate mfa step.""" + if user_input is not None: + self._mfa_code = user_input[CONF_MFA_CODE] + return await self._async_authenticate_user(error) + return self._async_form_auth_mfa(error) + + async def async_step_auth_captcha(self, user_input=None, error=None): + """Captcha verification step.""" + if user_input is not None: + self._captcha_code = user_input[CONF_CAPTCHA_CODE] + return await self._async_authenticate_user(error) + return self._async_form_auth_captcha(error) + + def _async_form_auth_app(self, error=None): + """Authenticate application form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_app", + data_schema=vol.Schema( + { + vol.Required( + CONF_APP_ID, default=self._app_id or vol.UNDEFINED + ): str, + vol.Required( + CONF_APP_SECRET, default=self._app_secret or vol.UNDEFINED + ): str, + } + ), + errors=errors, + ) + + def _async_form_auth_user(self, error=None): + """Authenticate user form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_user", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=self._username or vol.UNDEFINED + ): str, + vol.Required( + CONF_PASSWORD, default=self._password or vol.UNDEFINED + ): str, + } + ), + errors=errors, + ) + + def _async_form_auth_mfa(self, error=None): + """Authenticate mfa form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_mfa", + data_schema=vol.Schema( + { + vol.Required( + CONF_MFA_CODE, default=self._mfa_code or vol.UNDEFINED + ): str + } + ), + errors=errors, + description_placeholders={"phone_number": self._phone_number}, + ) + + def _async_form_auth_captcha(self, error=None): + """Captcha verification form.""" + errors = {} + if error is not None: + errors["base"] = error + + return self.async_show_form( + step_id="auth_captcha", + data_schema=vol.Schema( + { + vol.Required( + CONF_CAPTCHA_CODE, default=self._captcha_code or vol.UNDEFINED + ): str + } + ), + errors=errors, + description_placeholders={ + "captcha_img": '' + }, + ) + + async def _async_authenticate_app(self, error=None): + """Authenticate application.""" + try: + self._app_code = self._auth.get_app_code(self._app_id, self._app_secret) + self._app_token = self._auth.get_app_token( + self._app_id, self._app_secret, self._app_code + ) + return self._async_form_auth_user(error) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Error auth StarLine: %s", err) + return self._async_form_auth_app(ERROR_AUTH_APP) + + async def _async_authenticate_user(self, error=None): + """Authenticate user.""" + try: + state, data = self._auth.get_slid_user_token( + self._app_token, + self._username, + self._password, + self._mfa_code, + self._captcha_sid, + self._captcha_code, + ) + + if state == 1: + self._user_slid = data["user_token"] + return await self._async_get_entry() + + if "phone" in data: + self._phone_number = data["phone"] + if state == 0: + error = ERROR_AUTH_MFA + return self._async_form_auth_mfa(error) + + if "captchaSid" in data: + self._captcha_sid = data["captchaSid"] + self._captcha_image = data["captchaImg"] + return self._async_form_auth_captcha(error) + + raise Exception(data) + except Exception as err: # pylint: disable=broad-except + LOGGER.error("Error auth user: %s", err) + return self._async_form_auth_user(ERROR_AUTH_USER) + + async def _async_get_entry(self): + """Create entry.""" + ( + self._slnet_token, + self._slnet_token_expires, + self._user_id, + ) = self._auth.get_user_id(self._user_slid) + + return self.async_create_entry( + title=f"Application {self._app_id}", + data={ + DATA_USER_ID: self._user_id, + DATA_SLNET_TOKEN: self._slnet_token, + DATA_SLID_TOKEN: self._user_slid, + DATA_EXPIRES: self._slnet_token_expires, + }, + ) diff --git a/homeassistant/components/starline/const.py b/homeassistant/components/starline/const.py new file mode 100644 index 00000000000000..d76cd47b100de2 --- /dev/null +++ b/homeassistant/components/starline/const.py @@ -0,0 +1,27 @@ +"""StarLine constants.""" +import logging + +LOGGER = logging.getLogger(__package__) + +DOMAIN = "starline" +PLATFORMS = ["device_tracker", "binary_sensor", "sensor", "lock", "switch"] + +CONF_APP_ID = "app_id" +CONF_APP_SECRET = "app_secret" +CONF_MFA_CODE = "mfa_code" +CONF_CAPTCHA_CODE = "captcha_code" + +CONF_SCAN_INTERVAL = "scan_interval" +DEFAULT_SCAN_INTERVAL = 180 # in seconds + +ERROR_AUTH_APP = "error_auth_app" +ERROR_AUTH_USER = "error_auth_user" +ERROR_AUTH_MFA = "error_auth_mfa" + +DATA_USER_ID = "user_id" +DATA_SLNET_TOKEN = "slnet_token" +DATA_SLID_TOKEN = "slid_token" +DATA_EXPIRES = "expires" + +SERVICE_UPDATE_STATE = "update_state" +SERVICE_SET_SCAN_INTERVAL = "set_scan_interval" diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py new file mode 100644 index 00000000000000..b5254c761d8015 --- /dev/null +++ b/homeassistant/components/starline/device_tracker.py @@ -0,0 +1,60 @@ +"""StarLine device tracker.""" +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.helpers.restore_state import RestoreEntity +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up StarLine entry.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + if device.support_position: + entities.append(StarlineDeviceTracker(account, device)) + async_add_entities(entities) + + +class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): + """StarLine device tracker.""" + + def __init__(self, account: StarlineAccount, device: StarlineDevice): + """Set up StarLine entity.""" + super().__init__(account, device, "location", "Location") + + @property + def device_state_attributes(self): + """Return device specific attributes.""" + return self._account.gps_attrs(self._device) + + @property + def battery_level(self): + """Return the battery level of the device.""" + return self._device.battery_level + + @property + def location_accuracy(self): + """Return the gps accuracy of the device.""" + return self._device.position["r"] if "r" in self._device.position else 0 + + @property + def latitude(self): + """Return latitude value of the device.""" + return self._device.position["x"] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._device.position["y"] + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:map-marker-outline" diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py new file mode 100644 index 00000000000000..b0d948ae2c80c7 --- /dev/null +++ b/homeassistant/components/starline/entity.py @@ -0,0 +1,59 @@ +"""StarLine base entity.""" +from typing import Callable, Optional +from homeassistant.helpers.entity import Entity +from .account import StarlineAccount, StarlineDevice + + +class StarlineEntity(Entity): + """StarLine base entity class.""" + + def __init__( + self, account: StarlineAccount, device: StarlineDevice, key: str, name: str + ): + """Constructor.""" + self._account = account + self._device = device + self._key = key + self._name = name + self._unsubscribe_api: Optional[Callable] = None + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self): + """Return True if entity is available.""" + return self._account.api.available + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return f"starline-{self._key}-{self._device.device_id}" + + @property + def name(self): + """Return the name of the entity.""" + return f"{self._device.name} {self._name}" + + @property + def device_info(self): + """Return the device info.""" + return self._account.device_info(self._device) + + def update(self): + """Read new state data.""" + self.schedule_update_ha_state() + + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + await super().async_added_to_hass() + self._unsubscribe_api = self._account.api.add_update_listener(self.update) + + async def async_will_remove_from_hass(self): + """Call when entity is being removed from Home Assistant.""" + await super().async_will_remove_from_hass() + if self._unsubscribe_api is not None: + self._unsubscribe_api() + self._unsubscribe_api = None diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py new file mode 100644 index 00000000000000..0a20a36ae8bd2b --- /dev/null +++ b/homeassistant/components/starline/lock.py @@ -0,0 +1,72 @@ +"""Support for StarLine lock.""" +from homeassistant.components.lock import LockDevice +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the StarLine lock.""" + + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + if device.support_state: + lock = StarlineLock(account, device) + if lock.is_locked is not None: + entities.append(lock) + async_add_entities(entities) + + +class StarlineLock(StarlineEntity, LockDevice): + """Representation of a StarLine lock.""" + + def __init__(self, account: StarlineAccount, device: StarlineDevice): + """Initialize the lock.""" + super().__init__(account, device, "lock", "Security") + + @property + def available(self): + """Return True if entity is available.""" + return super().available and self._device.online + + @property + def device_state_attributes(self): + """Return the state attributes of the lock. + + Possible dictionary keys: + add_h - Additional sensor alarm status (high level) + add_l - Additional channel alarm status (low level) + door - Doors alarm status + hbrake - Hand brake alarm status + hijack - Hijack mode status + hood - Hood alarm status + ign - Ignition alarm status + pbrake - Brake pedal alarm status + shock_h - Shock sensor alarm status (high level) + shock_l - Shock sensor alarm status (low level) + tilt - Tilt sensor alarm status + trunk - Trunk alarm status + Documentation: https://developer.starline.ru/#api-Device-DeviceState + """ + return self._device.alarm_state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ( + "mdi:shield-check-outline" if self.is_locked else "mdi:shield-alert-outline" + ) + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._device.car_state.get("arm") + + def lock(self, **kwargs): + """Lock the car.""" + self._account.api.set_car_state(self._device.device_id, "arm", True) + + def unlock(self, **kwargs): + """Unlock the car.""" + self._account.api.set_car_state(self._device.device_id, "arm", False) diff --git a/homeassistant/components/starline/manifest.json b/homeassistant/components/starline/manifest.json new file mode 100644 index 00000000000000..ef343aae4ce953 --- /dev/null +++ b/homeassistant/components/starline/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "starline", + "name": "StarLine", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/starline", + "requirements": [ + "starline==0.1.3" + ], + "dependencies": [], + "codeowners": [ + "@anonym-tsk" + ] +} diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py new file mode 100644 index 00000000000000..2507aba4955d12 --- /dev/null +++ b/homeassistant/components/starline/sensor.py @@ -0,0 +1,95 @@ +"""Reads vehicle status from StarLine API.""" +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + +SENSOR_TYPES = { + "battery": ["Battery", None, "V", None], + "balance": ["Balance", None, None, "mdi:cash-multiple"], + "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, None, None], + "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, None, None], + "gsm_lvl": ["GSM Signal", None, "%", None], +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the StarLine sensors.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + for key, value in SENSOR_TYPES.items(): + sensor = StarlineSensor(account, device, key, *value) + if sensor.state is not None: + entities.append(sensor) + async_add_entities(entities) + + +class StarlineSensor(StarlineEntity, Entity): + """Representation of a StarLine sensor.""" + + def __init__( + self, + account: StarlineAccount, + device: StarlineDevice, + key: str, + name: str, + device_class: str, + unit: str, + icon: str, + ): + """Constructor.""" + super().__init__(account, device, key, name) + self._device_class = device_class + self._unit = unit + self._icon = icon + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + if self._key == "battery": + return icon_for_battery_level( + battery_level=self._device.battery_level_percent, + charging=self._device.car_state.get("ign", False), + ) + if self._key == "gsm_lvl": + return icon_for_signal_level(signal_level=self._device.gsm_level_percent) + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + if self._key == "battery": + return self._device.battery_level + if self._key == "balance": + return self._device.balance.get("value") + if self._key == "ctemp": + return self._device.temp_inner + if self._key == "etemp": + return self._device.temp_engine + if self._key == "gsm_lvl": + return self._device.gsm_level_percent + return None + + @property + def unit_of_measurement(self): + """Get the unit of measurement.""" + if self._key == "balance": + return self._device.balance.get("currency") or "₽" + return self._unit + + @property + def device_class(self): + """Return the class of the sensor.""" + return self._device_class + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + if self._key == "balance": + return self._account.balance_attrs(self._device) + if self._key == "gsm_lvl": + return self._account.gsm_attrs(self._device) + return None diff --git a/homeassistant/components/starline/services.yaml b/homeassistant/components/starline/services.yaml new file mode 100644 index 00000000000000..bef3a16803e8d6 --- /dev/null +++ b/homeassistant/components/starline/services.yaml @@ -0,0 +1,10 @@ +update_state: + description: > + Fetch the last state of the devices from the StarLine server. +set_scan_interval: + description: > + Set update frequency. + fields: + scan_interval: + description: Update frequency (in seconds). + example: 180 diff --git a/homeassistant/components/starline/strings.json b/homeassistant/components/starline/strings.json new file mode 100644 index 00000000000000..bf83f652c3c7dd --- /dev/null +++ b/homeassistant/components/starline/strings.json @@ -0,0 +1,42 @@ +{ + "config": { + "title": "StarLine", + "step": { + "auth_app": { + "title": "Application credentials", + "description": "Application ID and secret code from StarLine developer account", + "data": { + "app_id": "App ID", + "app_secret": "Secret" + } + }, + "auth_user": { + "title": "User credentials", + "description": "StarLine account email and password", + "data": { + "username": "Username", + "password": "Password" + } + }, + "auth_mfa": { + "title": "Two-factor authorization", + "description": "Enter the code sent to phone {phone_number}", + "data": { + "mfa_code": "SMS code" + } + }, + "auth_captcha": { + "title": "Captcha", + "description": "{captcha_img}", + "data": { + "captcha_code": "Code from image" + } + } + }, + "error": { + "error_auth_app": "Incorrect application id or secret", + "error_auth_user": "Incorrect username or password", + "error_auth_mfa": "Incorrect code" + } + } +} diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py new file mode 100644 index 00000000000000..92dec10b9d30e2 --- /dev/null +++ b/homeassistant/components/starline/switch.py @@ -0,0 +1,86 @@ +"""Support for StarLine switch.""" +from homeassistant.components.switch import SwitchDevice +from .account import StarlineAccount, StarlineDevice +from .const import DOMAIN +from .entity import StarlineEntity + +SWITCH_TYPES = { + "ign": ["Engine", "mdi:engine-outline", "mdi:engine-off-outline"], + "webasto": ["Webasto", "mdi:radiator", "mdi:radiator-off"], + "out": [ + "Additional Channel", + "mdi:access-point-network", + "mdi:access-point-network-off", + ], + "poke": ["Horn", "mdi:bullhorn-outline", "mdi:bullhorn-outline"], +} + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the StarLine switch.""" + account: StarlineAccount = hass.data[DOMAIN][entry.entry_id] + entities = [] + for device in account.api.devices.values(): + if device.support_state: + for key, value in SWITCH_TYPES.items(): + switch = StarlineSwitch(account, device, key, *value) + if switch.is_on is not None: + entities.append(switch) + async_add_entities(entities) + + +class StarlineSwitch(StarlineEntity, SwitchDevice): + """Representation of a StarLine switch.""" + + def __init__( + self, + account: StarlineAccount, + device: StarlineDevice, + key: str, + name: str, + icon_on: str, + icon_off: str, + ): + """Initialize the switch.""" + super().__init__(account, device, key, name) + self._icon_on = icon_on + self._icon_off = icon_off + + @property + def available(self): + """Return True if entity is available.""" + return super().available and self._device.online + + @property + def device_state_attributes(self): + """Return the state attributes of the switch.""" + if self._key == "ign": + return self._account.engine_attrs(self._device) + return None + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon_on if self.is_on else self._icon_off + + @property + def assumed_state(self): + """Return True if unable to access real state of the entity.""" + return True + + @property + def is_on(self): + """Return True if entity is on.""" + if self._key == "poke": + return False + return self._device.car_state.get(self._key) + + def turn_on(self, **kwargs): + """Turn the entity on.""" + self._account.api.set_car_state(self._device.device_id, self._key, True) + + def turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + if self._key == "poke": + return + self._account.api.set_car_state(self._device.device_id, self._key, False) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c496fc99cf1ecd..8d4be47f5f8674 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -69,6 +69,7 @@ "soma", "somfy", "sonos", + "starline", "tellduslive", "toon", "tplink", diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index 96c3b7e08c16e2..b2a1d58717bce2 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -18,3 +18,14 @@ def icon_for_battery_level( elif 5 < battery_level < 95: icon += "-{}".format(int(round(battery_level / 10 - 0.01)) * 10) return icon + + +def icon_for_signal_level(signal_level: Optional[int] = None) -> str: + """Return a signal icon valid identifier.""" + if signal_level is None or signal_level == 0: + return "mdi:signal-cellular-outline" + if signal_level > 70: + return "mdi:signal-cellular-3" + if signal_level > 30: + return "mdi:signal-cellular-2" + return "mdi:signal-cellular-1" diff --git a/requirements_all.txt b/requirements_all.txt index 477cf1314de3ec..987b231005dbfe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1859,6 +1859,9 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.sql sqlalchemy==1.3.11 +# homeassistant.components.starline +starline==0.1.3 + # homeassistant.components.starlingbank starlingbank==3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06be8aac043e0f..56a48905eb5150 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -574,6 +574,9 @@ somecomfort==0.5.2 # homeassistant.components.sql sqlalchemy==1.3.11 +# homeassistant.components.starline +starline==0.1.3 + # homeassistant.components.statsd statsd==3.2.1 diff --git a/tests/components/starline/__init__.py b/tests/components/starline/__init__.py new file mode 100644 index 00000000000000..58f50c0f1b9d05 --- /dev/null +++ b/tests/components/starline/__init__.py @@ -0,0 +1 @@ +"""Tests for the StarLine component.""" diff --git a/tests/components/starline/test_config_flow.py b/tests/components/starline/test_config_flow.py new file mode 100644 index 00000000000000..31bdf98b404330 --- /dev/null +++ b/tests/components/starline/test_config_flow.py @@ -0,0 +1,126 @@ +"""Tests for StarLine config flow.""" +import requests_mock +from homeassistant.components.starline import config_flow + +TEST_APP_ID = "666" +TEST_APP_SECRET = "appsecret" +TEST_APP_CODE = "appcode" +TEST_APP_TOKEN = "apptoken" +TEST_APP_SLNET = "slnettoken" +TEST_APP_SLID = "slidtoken" +TEST_APP_UID = "123" +TEST_APP_USERNAME = "sluser" +TEST_APP_PASSWORD = "slpassword" + + +async def test_flow_works(hass): + """Test that config flow works.""" + with requests_mock.Mocker() as mock: + mock.get( + "https://id.starline.ru/apiV3/application/getCode/", + text='{"state": 1, "desc": {"code": "' + TEST_APP_CODE + '"}}', + ) + mock.get( + "https://id.starline.ru/apiV3/application/getToken/", + text='{"state": 1, "desc": {"token": "' + TEST_APP_TOKEN + '"}}', + ) + mock.post( + "https://id.starline.ru/apiV3/user/login/", + text='{"state": 1, "desc": {"user_token": "' + TEST_APP_SLID + '"}}', + ) + mock.post( + "https://developer.starline.ru/json/v2/auth.slid", + text='{"code": 200, "user_id": "' + TEST_APP_UID + '"}', + cookies={"slnet": TEST_APP_SLNET}, + ) + mock.get( + "https://developer.starline.ru/json/v2/user/{}/user_info".format( + TEST_APP_UID + ), + text='{"code": 200, "devices": [{"device_id": "123", "imei": "123", "alias": "123", "battery": "123", "ctemp": "123", "etemp": "123", "fw_version": "123", "gsm_lvl": "123", "phone": "123", "status": "1", "ts_activity": "123", "typename": "123", "balance": {}, "car_state": {}, "car_alr_state": {}, "functions": [], "position": {}}], "shared_devices": []}', + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_app" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_APP_ID: TEST_APP_ID, + config_flow.CONF_APP_SECRET: TEST_APP_SECRET, + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + config_flow.CONF_USERNAME: TEST_APP_USERNAME, + config_flow.CONF_PASSWORD: TEST_APP_PASSWORD, + }, + ) + assert result["type"] == "create_entry" + assert result["title"] == "Application {}".format(TEST_APP_ID) + + +async def test_step_auth_app_code_falls(hass): + """Test config flow works when app auth code fails.""" + with requests_mock.Mocker() as mock: + mock.get( + "https://id.starline.ru/apiV3/application/getCode/", text='{"state": 0}}' + ) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "user"}, + data={ + config_flow.CONF_APP_ID: TEST_APP_ID, + config_flow.CONF_APP_SECRET: TEST_APP_SECRET, + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_app" + assert result["errors"] == {"base": "error_auth_app"} + + +async def test_step_auth_app_token_falls(hass): + """Test config flow works when app auth token fails.""" + with requests_mock.Mocker() as mock: + mock.get( + "https://id.starline.ru/apiV3/application/getCode/", + text='{"state": 1, "desc": {"code": "' + TEST_APP_CODE + '"}}', + ) + mock.get( + "https://id.starline.ru/apiV3/application/getToken/", text='{"state": 0}' + ) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "user"}, + data={ + config_flow.CONF_APP_ID: TEST_APP_ID, + config_flow.CONF_APP_SECRET: TEST_APP_SECRET, + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_app" + assert result["errors"] == {"base": "error_auth_app"} + + +async def test_step_auth_user_falls(hass): + """Test config flow works when user fails.""" + with requests_mock.Mocker() as mock: + mock.post("https://id.starline.ru/apiV3/user/login/", text='{"state": 0}') + flow = config_flow.StarlineFlowHandler() + flow.hass = hass + result = await flow.async_step_auth_user( + user_input={ + config_flow.CONF_USERNAME: TEST_APP_USERNAME, + config_flow.CONF_PASSWORD: TEST_APP_PASSWORD, + } + ) + assert result["type"] == "form" + assert result["step_id"] == "auth_user" + assert result["errors"] == {"base": "error_auth_user"} diff --git a/tests/helpers/test_icon.py b/tests/helpers/test_icon.py index ce6e95110c963b..4f1d4cb223fe5e 100644 --- a/tests/helpers/test_icon.py +++ b/tests/helpers/test_icon.py @@ -44,3 +44,15 @@ def test_battery_icon(): postfix = "" assert iconbase + postfix == icon_for_battery_level(level, False) assert iconbase + postfix_charging == icon_for_battery_level(level, True) + + +def test_signal_icon(): + """Test icon generator for signal sensor.""" + from homeassistant.helpers.icon import icon_for_signal_level + + assert icon_for_signal_level(None) == "mdi:signal-cellular-outline" + assert icon_for_signal_level(0) == "mdi:signal-cellular-outline" + assert icon_for_signal_level(5) == "mdi:signal-cellular-1" + assert icon_for_signal_level(40) == "mdi:signal-cellular-2" + assert icon_for_signal_level(80) == "mdi:signal-cellular-3" + assert icon_for_signal_level(100) == "mdi:signal-cellular-3" From 078a7e446dabf92e7838f34ee2b12cff46ec2863 Mon Sep 17 00:00:00 2001 From: sophof Date: Tue, 26 Nov 2019 20:26:40 +0100 Subject: [PATCH 1814/3953] Fix all entities triggering all observations in bayesian sensor (#28979) * change fromkeys to dict comprehension to prevent append working on a list by reference. Now each entity id has its own seperate list * change fromkeys to dict comprehension to prevent append working on a list by reference. Now each entity id has its own seperate list * use get instead of direct keys for dict * change fromkeys to dict comprehension to prevent append working on a list by reference. Now each entity id has its own seperate list * use get instead of direct keys for dict * changed get back to [] --- homeassistant/components/bayesian/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index ffa13a6288ca56..1d3720f6723d14 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -133,7 +133,7 @@ def __init__(self, name, prior, observations, probability_threshold, device_clas to_observe.update(set([obs.get("entity_id")])) if "value_template" in obs: to_observe.update(set(obs.get(CONF_VALUE_TEMPLATE).extract_entities())) - self.entity_obs = dict.fromkeys(to_observe, []) + self.entity_obs = {key: [] for key in to_observe} for ind, obs in enumerate(self._observations): obs["id"] = ind From fe626f566942dc765450e9e0cd8a0b4c27dbcfa6 Mon Sep 17 00:00:00 2001 From: Alex van den Hoogen Date: Tue, 26 Nov 2019 20:31:20 +0100 Subject: [PATCH 1815/3953] Add Post Processing Jobs to NZBGet component (#29027) * Added Post Processing Jobs to NZBGet component In the current implementation of the NZBGet component there is no way of knowing whether NZBGet is still in the Post Processing phase of a download job. This can be very useful information. For instance when you want to suspend the NZBGet service when all queue items have been completed. This change adds that additional sensor. * Renamed Count to Jobs in NZBGet component --- homeassistant/components/nzbget/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 20b49a492f3ae1..3556c88a6da932 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -18,6 +18,7 @@ "download_rate": ["DownloadRate", "Speed", "MB/s"], "download_size": ["DownloadedSizeMB", "Size", "MB"], "free_disk_space": ["FreeDiskSpaceMB", "Disk Free", "MB"], + "post_job_count": ["PostJobCount", "Post Processing Jobs", "Jobs"], "post_paused": ["PostPaused", "Post Processing Paused", None], "remaining_size": ["RemainingSizeMB", "Queue Size", "MB"], "uptime": ["UpTimeSec", "Uptime", "min"], From 4ea47333d84813cf89d993711a92113c2b089390 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 26 Nov 2019 21:31:00 +0100 Subject: [PATCH 1816/3953] Update roomba bin state checking (#29030) * Fixed bin full detection * Refactored bin check * Fix after pylint --- homeassistant/components/roomba/vacuum.py | 27 ++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index fd74fd190a8913..aafa4823d559d8 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -33,7 +33,6 @@ ATTR_POSITION = "position" ATTR_SOFTWARE_VERSION = "software_version" -CAP_BIN_FULL = "bin_full" CAP_POSITION = "position" CAP_CARPET_BOOST = "carpet_boost" @@ -276,18 +275,14 @@ async def async_update(self): # Get the capabilities of our unit capabilities = state.get("cap", {}) - cap_bin_full = capabilities.get("binFullDetect") cap_carpet_boost = capabilities.get("carpetBoost") cap_pos = capabilities.get("pose") # Store capabilities self._capabilities = { - CAP_BIN_FULL: cap_bin_full == 1, CAP_CARPET_BOOST: cap_carpet_boost == 1, CAP_POSITION: cap_pos == 1, } - bin_state = state.get("bin", {}) - # Roomba software version software_version = state.get("softwareVer") @@ -302,10 +297,13 @@ async def async_update(self): # Set properties that are to appear in the GUI self._state_attrs = { - ATTR_BIN_PRESENT: bin_state.get("present"), ATTR_SOFTWARE_VERSION: software_version, } + # Get bin state + bin_state = self._get_bin_state(state) + self._state_attrs.update(bin_state) + # Only add cleaning time and cleaned area attrs when the vacuum is # currently on if self._is_on: @@ -335,10 +333,6 @@ async def async_update(self): position = f"({pos_x}, {pos_y}, {theta})" self._state_attrs[ATTR_POSITION] = position - # Not all Roombas have a bin full sensor - if self._capabilities[CAP_BIN_FULL]: - self._state_attrs[ATTR_BIN_FULL] = bin_state.get("full") - # Fan speed mode (Performance, Automatic or Eco) # Not all Roombas expose carpet boost if self._capabilities[CAP_CARPET_BOOST]: @@ -355,3 +349,16 @@ async def async_update(self): fan_speed = FAN_SPEED_ECO self._fan_speed = fan_speed + + @staticmethod + def _get_bin_state(state): + bin_raw_state = state.get("bin", {}) + bin_state = {} + + if bin_raw_state.get("present") is not None: + bin_state[ATTR_BIN_PRESENT] = bin_raw_state.get("present") + + if bin_raw_state.get("full") is not None: + bin_state[ATTR_BIN_FULL] = bin_raw_state.get("full") + + return bin_state From 0849d42dc68d89a7496d1dc883c8bebbd0142873 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Tue, 26 Nov 2019 15:57:45 -0500 Subject: [PATCH 1817/3953] Remove the alarm_control_panel CODEOWNER (#29114) Although I originally contributed this component, I no longer use it myself and haven't been following the internal development of HA, so I'm no longer a good person to review any further changes to this component. I would therefore like to relinquish my CODEOWNER status over this component. --- CODEOWNERS | 1 - homeassistant/components/alarm_control_panel/manifest.json | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a489786f48fc84..f35c492615d66f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,7 +17,6 @@ homeassistant/components/abode/* @shred86 homeassistant/components/adguard/* @frenck homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya -homeassistant/components/alarm_control_panel/* @colinodell homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/almond/* @gcampax @balloob homeassistant/components/alpha_vantage/* @fabaff diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index 04ef58769dad7b..e877fe90a17cff 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -4,7 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", "requirements": [], "dependencies": [], - "codeowners": [ - "@colinodell" - ] + "codeowners": [] } From b72c6c44242daf1e2bc4b0da687c702bf30cfef0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 26 Nov 2019 22:09:00 +0100 Subject: [PATCH 1818/3953] Update list of supported countries for Workday Sensor (#29000) * Updated list of supported countries * Added Honduras and Polish back to the list * Added HND for Honduras to the list --- homeassistant/components/workday/binary_sensor.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 3ca2afcc749087..f95447c1e72f70 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -13,6 +13,7 @@ _LOGGER = logging.getLogger(__name__) # List of all countries currently supported by holidays +# Source: https://github.com/dr-prodigy/python-holidays#available-countries # There seems to be no way to get the list out at runtime ALL_COUNTRIES = [ "Argentina", @@ -42,6 +43,8 @@ "Denmark", "DK", "England", + "Estonia", + "EE", "EuropeanCentralBank", "ECB", "TAR", @@ -54,7 +57,9 @@ "Hungary", "HU", "Honduras", - "HUD", + "HND", + "Iceland", + "IS", "India", "IND", "Ireland", @@ -64,6 +69,8 @@ "IT", "Japan", "JP", + "Kenya", + "KE", "Lithuania", "LT", "Luxembourg", @@ -77,6 +84,9 @@ "Northern Ireland", "Norway", "NO", + "Peru", + "PE", + "Poland", "Polish", "PL", "Portugal", From 5f1b0fb15cae2480c60a3318f987e02a1b2a2c1f Mon Sep 17 00:00:00 2001 From: gjbadros Date: Tue, 26 Nov 2019 13:10:58 -0800 Subject: [PATCH 1819/3953] Allow rest sensor list responses (#28835) * Make rest sensor a little bit more flexible and allow the response to be a list with 0th element being a dictionary * Black formatter wanted a different formatting * Added test case for a list response with dict as 0th element * Fixed lint error - it thinks a + STRING is an avoidance of using % sequences for log messages; I generally prefer explicit + rather than string juxtaposition for combining string literals, but sounds like that's not the standard * Fixed test case -- I added it to the wrong scenario and need to use the one with json_attrs. --- homeassistant/components/rest/sensor.py | 7 ++++++- tests/components/rest/test_sensor.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index b89e96a0f34f10..6fdf5ce7221c64 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -197,13 +197,18 @@ def update(self): if value: try: json_dict = json.loads(value) + if isinstance(json_dict, list): + json_dict = json_dict[0] if isinstance(json_dict, dict): attrs = { k: json_dict[k] for k in self._json_attrs if k in json_dict } self._attributes = attrs else: - _LOGGER.warning("JSON result was not a dictionary") + _LOGGER.warning( + "JSON result was not a dictionary" + " or list with 0th element a dictionary" + ) except ValueError: _LOGGER.warning("REST result could not be parsed as JSON") _LOGGER.debug("Erroneous JSON: %s", value) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index c6cfce90a20f9e..d770f21a403e4e 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -284,6 +284,26 @@ def test_update_with_json_attrs(self): self.sensor.update() assert "some_json_value" == self.sensor.device_state_attributes["key"] + def test_update_with_json_attrs_list_dict(self): + """Test attributes get extracted from a JSON list[0] result.""" + self.rest.update = Mock( + "rest.RestData.update", + side_effect=self.update_side_effect('[{ "key": "another_value" }]'), + ) + self.sensor = rest.RestSensor( + self.hass, + self.rest, + self.name, + self.unit_of_measurement, + self.device_class, + None, + ["key"], + self.force_update, + self.resource_template, + ) + self.sensor.update() + assert "another_value" == self.sensor.device_state_attributes["key"] + @patch("homeassistant.components.rest.sensor._LOGGER") def test_update_with_json_attrs_no_data(self, mock_logger): """Test attributes when no JSON result fetched.""" From 06231e7ac261c56cb59f8e0952ab46eb0bfa83f7 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 26 Nov 2019 22:47:13 +0100 Subject: [PATCH 1820/3953] Move request sync logic into GoogleConfig (#28227) * Move request sync logic into GoogleConfig * Return http status code for request_sync same as cloud * agent_user_id is not optional for async_sync_entities now * No need in checking parameter here * Adjust some things for cloud tests * Adjust some more stuff for cloud tests * Drop uneccessary else * Black required change * Let async_schedule_google_sync take agent_user_id * Assert return value on api call * Test old api key method * Update homeassistant/components/google_assistant/helpers.py Co-Authored-By: Paulus Schoutsen --- .../components/cloud/google_config.py | 8 +-- homeassistant/components/cloud/http_api.py | 4 +- .../components/google_assistant/__init__.py | 47 ++++---------- .../components/google_assistant/helpers.py | 34 +++++----- .../components/google_assistant/http.py | 65 +++++++++++-------- tests/components/cloud/test_google_config.py | 16 ++++- tests/components/cloud/test_http_api.py | 8 ++- .../components/google_assistant/test_http.py | 31 ++++++++- 8 files changed, 124 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 582fa0075504f8..80d3fedba5b2a7 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -105,7 +105,7 @@ async def async_report_state(self, message): except ErrorResponse as err: _LOGGER.warning("Error reporting state - %s: %s", err.code, err.message) - async def _async_request_sync_devices(self): + async def _async_request_sync_devices(self, agent_user_id: str): """Trigger a sync with Google.""" if self._sync_entities_lock.locked(): return 200 @@ -143,7 +143,7 @@ async def _async_prefs_updated(self, prefs): # State reporting is reported as a property on entities. # So when we change it, we need to sync all entities. - await self.async_sync_entities() + await self.async_sync_entities(self.agent_user_id) # If entity prefs are the same or we have filter in config.yaml, # don't sync. @@ -151,7 +151,7 @@ async def _async_prefs_updated(self, prefs): self._cur_entity_prefs is not prefs.google_entity_configs and self._config["filter"].empty_filter ): - self.async_schedule_google_sync() + self.async_schedule_google_sync(self.agent_user_id) if self.enabled and not self.is_local_sdk_active: self.async_enable_local_sdk() @@ -167,4 +167,4 @@ async def _handle_entity_registry_updated(self, event): # Schedule a sync if a change was made to an entity that Google knows about if self._should_expose_entity_id(entity_id): - await self.async_sync_entities() + await self.async_sync_entities(self.agent_user_id) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index d969612ce8e67b..f795bc2b68c1e9 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -174,7 +174,9 @@ async def post(self, request): """Trigger a Google Actions sync.""" hass = request.app["hass"] cloud: Cloud = hass.data[DOMAIN] - status = await cloud.client.google_config.async_sync_entities() + status = await cloud.client.google_config.async_sync_entities( + cloud.client.google_config.agent_user_id + ) return self.json({}, status_code=status) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index ebf906b6f2ac8f..f0f2ad773887fe 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -1,11 +1,7 @@ """Support for Actions on Google Assistant Smart Home Control.""" -import asyncio import logging from typing import Dict, Any -import aiohttp -import async_timeout - import voluptuous as vol # Typing imports @@ -13,7 +9,6 @@ from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( DOMAIN, @@ -24,7 +19,6 @@ DEFAULT_EXPOSED_DOMAINS, CONF_API_KEY, SERVICE_REQUEST_SYNC, - REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG, CONF_EXPOSE, CONF_ALIASES, @@ -99,37 +93,22 @@ def _check_report_state(data): async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) - api_key = config.get(CONF_API_KEY) - async_register_http(hass, config) + google_config = async_register_http(hass, config) async def request_sync_service_handler(call: ServiceCall): """Handle request sync service calls.""" - websession = async_get_clientsession(hass) - try: - with async_timeout.timeout(15): - agent_user_id = call.data.get("agent_user_id") or call.context.user_id - - if agent_user_id is None: - _LOGGER.warning( - "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." - ) - return - - res = await websession.post( - REQUEST_SYNC_BASE_URL, - params={"key": api_key}, - json={"agent_user_id": agent_user_id}, - ) - _LOGGER.info("Submitted request_sync request to Google") - res.raise_for_status() - except aiohttp.ClientResponseError: - body = await res.read() - _LOGGER.error("request_sync request failed: %d %s", res.status, body) - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Could not contact Google for request_sync") - - # Register service only if api key is provided - if api_key is not None: + agent_user_id = call.data.get("agent_user_id") or call.context.user_id + + if agent_user_id is None: + _LOGGER.warning( + "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." + ) + return + + await google_config.async_sync_entities(agent_user_id) + + # Register service only if key is provided + if CONF_API_KEY in config or CONF_SERVICE_ACCOUNT in config: hass.services.async_register( DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler ) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 0f3a3dcf039cc8..a128b6c9bcb3b2 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -41,7 +41,7 @@ class AbstractConfig: def __init__(self, hass): """Initialize abstract config.""" self.hass = hass - self._google_sync_unsub = None + self._google_sync_unsub = {} self._local_sdk_active = False @property @@ -119,31 +119,29 @@ def async_disable_report_state(self): self._unsub_report_state() self._unsub_report_state = None - async def async_sync_entities(self): + async def async_sync_entities(self, agent_user_id: str): """Sync all entities to Google.""" # Remove any pending sync - if self._google_sync_unsub: - self._google_sync_unsub() - self._google_sync_unsub = None + self._google_sync_unsub.pop(agent_user_id, lambda: None)() - return await self._async_request_sync_devices() - - async def _schedule_callback(self, _now): - """Handle a scheduled sync callback.""" - self._google_sync_unsub = None - await self.async_sync_entities() + return await self._async_request_sync_devices(agent_user_id) @callback - def async_schedule_google_sync(self): + def async_schedule_google_sync(self, agent_user_id: str): """Schedule a sync.""" - if self._google_sync_unsub: - self._google_sync_unsub() - self._google_sync_unsub = async_call_later( - self.hass, SYNC_DELAY, self._schedule_callback + async def _schedule_callback(_now): + """Handle a scheduled sync callback.""" + self._google_sync_unsub.pop(agent_user_id, None) + await self.async_sync_entities(agent_user_id) + + self._google_sync_unsub.pop(agent_user_id, lambda: None)() + + self._google_sync_unsub[agent_user_id] = async_call_later( + self.hass, SYNC_DELAY, _schedule_callback ) - async def _async_request_sync_devices(self) -> int: + async def _async_request_sync_devices(self, agent_user_id: str) -> int: """Trigger a sync with Google. Return value is the HTTP status code of the sync request. @@ -165,7 +163,7 @@ def async_enable_local_sdk(self): return webhook.async_register( - self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook + self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook, ) self._local_sdk_active = True diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index ce5eff7afef40c..164ef8bb4de0d2 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -10,7 +10,7 @@ # Typing imports from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback, ServiceCall +from homeassistant.core import callback from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util @@ -27,12 +27,10 @@ CONF_SERVICE_ACCOUNT, CONF_CLIENT_EMAIL, CONF_PRIVATE_KEY, - DOMAIN, HOMEGRAPH_TOKEN_URL, HOMEGRAPH_SCOPE, REPORT_STATE_BASE_URL, REQUEST_SYNC_BASE_URL, - SERVICE_REQUEST_SYNC, ) from .smart_home import async_handle_message from .helpers import AbstractConfig @@ -133,6 +131,18 @@ def should_2fa(self, state): """If an entity should have 2FA checked.""" return True + async def _async_request_sync_devices(self, agent_user_id: str): + if CONF_API_KEY in self._config: + await self.async_call_homegraph_api_key( + REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} + ) + elif CONF_SERVICE_ACCOUNT in self._config: + await self.async_call_homegraph_api( + REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} + ) + else: + _LOGGER.error("No configuration for request_sync available") + async def _async_update_token(self, force=False): if CONF_SERVICE_ACCOUNT not in self._config: _LOGGER.error("Trying to get homegraph api token without service account") @@ -151,6 +161,25 @@ async def _async_update_token(self, force=False): self._access_token = token["access_token"] self._access_token_renew = now + timedelta(seconds=token["expires_in"]) + async def async_call_homegraph_api_key(self, url, data): + """Call a homegraph api with api key authentication.""" + websession = async_get_clientsession(self.hass) + try: + res = await websession.post( + url, params={"key": self._config.get(CONF_API_KEY)}, json=data + ) + _LOGGER.debug( + "Response on %s with data %s was %s", url, data, await res.text() + ) + res.raise_for_status() + return res.status + except ClientResponseError as error: + _LOGGER.error("Request for %s failed: %d", url, error.status) + return error.status + except (asyncio.TimeoutError, ClientError): + _LOGGER.error("Could not contact %s", url) + return 500 + async def async_call_homegraph_api(self, url, data): """Call a homegraph api with authenticaiton.""" session = async_get_clientsession(self.hass) @@ -165,24 +194,26 @@ async def _call(): "Response on %s with data %s was %s", url, data, await res.text() ) res.raise_for_status() + return res.status try: await self._async_update_token() try: - await _call() + return await _call() except ClientResponseError as error: if error.status == 401: _LOGGER.warning( "Request for %s unauthorized, renewing token and retrying", url ) await self._async_update_token(True) - await _call() - else: - raise + return await _call() + raise except ClientResponseError as error: _LOGGER.error("Request for %s failed: %d", url, error.status) + return error.status except (asyncio.TimeoutError, ClientError): _LOGGER.error("Could not contact %s", url) + return 500 async def async_report_state(self, message): """Send a state report to Google.""" @@ -201,25 +232,7 @@ def async_register_http(hass, cfg): hass.http.register_view(GoogleAssistantView(config)) if config.should_report_state: config.async_enable_report_state() - - async def request_sync_service_handler(call: ServiceCall): - """Handle request sync service calls.""" - agent_user_id = call.data.get("agent_user_id") or call.context.user_id - - if agent_user_id is None: - _LOGGER.warning( - "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." - ) - return - await config.async_call_homegraph_api( - REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} - ) - - # Register service only if api key is provided - if CONF_API_KEY not in cfg and CONF_SERVICE_ACCOUNT in cfg: - hass.services.async_register( - DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler - ) + return config class GoogleAssistantView(HomeAssistantView): diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 43914f489d6eff..7ccb6a33336f23 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -12,7 +12,12 @@ async def test_google_update_report_state(hass, cloud_prefs): """Test Google config responds to updating preference.""" - config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None) + config = CloudGoogleConfig( + hass, + GACTIONS_SCHEMA({}), + cloud_prefs, + Mock(claims={"cognito:username": "abcdefghjkl"}), + ) with patch.object( config, "async_sync_entities", side_effect=mock_coro @@ -39,12 +44,17 @@ async def test_sync_entities(aioclient_mock, hass, cloud_prefs): ), ) - assert await config.async_sync_entities() == 404 + assert await config.async_sync_entities("user") == 404 async def test_google_update_expose_trigger_sync(hass, cloud_prefs): """Test Google config responds to updating exposed entities.""" - config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None) + config = CloudGoogleConfig( + hass, + GACTIONS_SCHEMA({}), + cloud_prefs, + Mock(claims={"cognito:username": "abcdefghjkl"}), + ) with patch.object( config, "async_sync_entities", side_effect=mock_coro diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index b889a89dc5dbac..22d4ddd172ec9d 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -85,14 +85,18 @@ def mock_cognito(): yield mock_cog() -async def test_google_actions_sync(mock_cognito, cloud_client, aioclient_mock): +async def test_google_actions_sync( + mock_cognito, mock_cloud_login, cloud_client, aioclient_mock +): """Test syncing Google Actions.""" aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL) req = await cloud_client.post("/api/cloud/google_actions/sync") assert req.status == 200 -async def test_google_actions_sync_fails(mock_cognito, cloud_client, aioclient_mock): +async def test_google_actions_sync_fails( + mock_cognito, mock_cloud_login, cloud_client, aioclient_mock +): """Test syncing Google Actions gone bad.""" aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL, status=403) req = await cloud_client.post("/api/cloud/google_actions/sync") diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 4b26bbeba7f391..42706a470ab86b 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -106,7 +106,8 @@ async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): aioclient_mock.post(MOCK_URL, status=200, json={}) - await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + res = await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + assert res == 200 assert mock_get_token.call_count == 1 assert aioclient_mock.call_count == 1 @@ -139,6 +140,34 @@ async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): assert call[3] == MOCK_HEADER +async def test_call_homegraph_api_key(hass, aioclient_mock, hass_storage): + """Test the function to call the homegraph api.""" + config = GoogleConfig( + hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}) + ) + aioclient_mock.post(MOCK_URL, status=200, json={}) + + res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) + assert res == 200 + assert aioclient_mock.call_count == 1 + + call = aioclient_mock.mock_calls[0] + assert call[1].query == {"key": "dummy_key"} + assert call[2] == MOCK_JSON + + +async def test_call_homegraph_api_key_fail(hass, aioclient_mock, hass_storage): + """Test the function to call the homegraph api.""" + config = GoogleConfig( + hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}) + ) + aioclient_mock.post(MOCK_URL, status=666, json={}) + + res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) + assert res == 666 + assert aioclient_mock.call_count == 1 + + async def test_report_state(hass, aioclient_mock, hass_storage): """Test the report state function.""" config = GoogleConfig(hass, DUMMY_CONFIG) From 555190990a83f5c80754d40a23a69fab09e5823e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 27 Nov 2019 01:02:36 +0100 Subject: [PATCH 1821/3953] Move imports to top for tank_utility (#29119) --- homeassistant/components/tank_utility/sensor.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 78fcd47099de63..3ab4a027b04f2d 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -4,14 +4,14 @@ import logging import requests +import tank_utility import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_DEVICES, CONF_EMAIL, CONF_PASSWORD +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity - _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = datetime.timedelta(hours=1) @@ -41,14 +41,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tank Utility sensor.""" - from tank_utility import auth email = config.get(CONF_EMAIL) password = config.get(CONF_PASSWORD) devices = config.get(CONF_DEVICES) try: - token = auth.get_token(email, password) + token = tank_utility.auth.get_token(email, password) except requests.exceptions.HTTPError as http_error: if ( http_error.response.status_code @@ -109,19 +108,20 @@ def get_data(self): Flatten dictionary to map device to map of device data. """ - from tank_utility import auth, device data = {} try: - data = device.get_device_data(self._token, self.device) + data = tank_utility.device.get_device_data(self._token, self.device) except requests.exceptions.HTTPError as http_error: if ( http_error.response.status_code == requests.codes.unauthorized # pylint: disable=no-member ): _LOGGER.info("Getting new token") - self._token = auth.get_token(self._email, self._password, force=True) - data = device.get_device_data(self._token, self.device) + self._token = tank_utility.auth.get_token( + self._email, self._password, force=True + ) + data = tank_utility.device.get_device_data(self._token, self.device) else: raise http_error data.update(data.pop("device", {})) From 4e96ca948665258c403face993c7cdec9aff444b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 27 Nov 2019 01:02:50 +0100 Subject: [PATCH 1822/3953] Move imports to top for temper (#29118) --- homeassistant/components/temper/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index a32de3da10fb62..8b782ae4d797cc 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -1,5 +1,7 @@ """Support for getting temperature from TEMPer devices.""" import logging + +from temperusb.temper import TemperHandler import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -24,7 +26,6 @@ def get_temper_devices(): """Scan the Temper devices from temperusb.""" - from temperusb.temper import TemperHandler return TemperHandler().get_devices() From 5eaafcfd7f6b6ec861a977581a5e3ac641c0f4a8 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 27 Nov 2019 01:06:44 +0100 Subject: [PATCH 1823/3953] Move imports to top for touchline (#29117) --- homeassistant/components/touchline/climate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/touchline/climate.py b/homeassistant/components/touchline/climate.py index 46ac30d0d97a73..984e454ae02b13 100644 --- a/homeassistant/components/touchline/climate.py +++ b/homeassistant/components/touchline/climate.py @@ -2,14 +2,15 @@ import logging from typing import List +from pytouchline import PyTouchline import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_HEAT, + SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import CONF_HOST, TEMP_CELSIUS, ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE, CONF_HOST, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Touchline devices.""" - from pytouchline import PyTouchline host = config[CONF_HOST] py_touchline = PyTouchline() From 33c6ee80ad411e1f068f4b38f81ef735926e4b0f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 27 Nov 2019 01:07:12 +0100 Subject: [PATCH 1824/3953] Move imports to top for traccar (#29116) --- homeassistant/components/traccar/__init__.py | 9 ++-- .../components/traccar/config_flow.py | 2 +- .../components/traccar/device_tracker.py | 51 +++++++++---------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 5eb87de0db28f5..7e94ab0a3510ea 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -1,14 +1,15 @@ """Support for Traccar.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, HTTP_OK, CONF_WEBHOOK_ID +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER +from homeassistant.const import CONF_WEBHOOK_ID, HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv 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, diff --git a/homeassistant/components/traccar/config_flow.py b/homeassistant/components/traccar/config_flow.py index 4bd75910163fdd..3702316ffb9065 100644 --- a/homeassistant/components/traccar/config_flow.py +++ b/homeassistant/components/traccar/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Traccar.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index c7fdda013b0aaa..7f23d6cf31e3df 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -2,29 +2,31 @@ from datetime import datetime, timedelta import logging +from pytraccar.api import API +from stringcase import camelcase import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.const import ( + CONF_EVENT, CONF_HOST, + CONF_MONITORED_CONDITIONS, + CONF_PASSWORD, CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SSL, - CONF_VERIFY_SSL, - CONF_PASSWORD, CONF_USERNAME, - CONF_SCAN_INTERVAL, - CONF_MONITORED_CONDITIONS, - CONF_EVENT, + CONF_VERIFY_SSL, ) -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_GPS -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.core import callback from homeassistant.helpers import device_registry +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_time_interval 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 @@ -41,30 +43,29 @@ ATTR_MOTION, ATTR_SPEED, ATTR_STATUS, - ATTR_TRACKER, ATTR_TRACCAR_ID, - EVENT_DEVICE_MOVING, + ATTR_TRACKER, + CONF_MAX_ACCURACY, + CONF_SKIP_ACCURACY_ON, + EVENT_ALARM, + EVENT_ALL_EVENTS, EVENT_COMMAND_RESULT, EVENT_DEVICE_FUEL_DROP, - EVENT_GEOFENCE_ENTER, + EVENT_DEVICE_MOVING, EVENT_DEVICE_OFFLINE, - EVENT_DRIVER_CHANGED, - EVENT_GEOFENCE_EXIT, - EVENT_DEVICE_OVERSPEED, EVENT_DEVICE_ONLINE, + EVENT_DEVICE_OVERSPEED, EVENT_DEVICE_STOPPED, - EVENT_MAINTENANCE, - EVENT_ALARM, - EVENT_TEXT_MESSAGE, EVENT_DEVICE_UNKNOWN, + EVENT_DRIVER_CHANGED, + EVENT_GEOFENCE_ENTER, + EVENT_GEOFENCE_EXIT, EVENT_IGNITION_OFF, EVENT_IGNITION_ON, - EVENT_ALL_EVENTS, - CONF_MAX_ACCURACY, - CONF_SKIP_ACCURACY_ON, + EVENT_MAINTENANCE, + EVENT_TEXT_MESSAGE, ) - _LOGGER = logging.getLogger(__name__) DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) @@ -156,7 +157,6 @@ def _receive_data(device, latitude, longitude, battery, accuracy, attrs): 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 session = async_get_clientsession(hass, config[CONF_VERIFY_SSL]) @@ -199,7 +199,6 @@ def __init__( event_types, ): """Initialize.""" - from stringcase import camelcase self._event_types = {camelcase(evt): evt for evt in event_types} self._custom_attributes = custom_attributes From 2a7aac6afcb11edfb6c7386b16b61c8dee6f4f34 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 27 Nov 2019 00:32:16 +0000 Subject: [PATCH 1825/3953] [ci skip] Translation update --- .../components/almond/.translations/de.json | 8 +++- .../components/climate/.translations/de.json | 8 ++++ .../components/demo/.translations/de.json | 5 +++ .../geonetnz_quakes/.translations/pl.json | 2 +- .../geonetnz_volcano/.translations/de.json | 14 +++++++ .../geonetnz_volcano/.translations/pl.json | 2 +- .../hisense_aehw4a1/.translations/de.json | 15 +++++++ .../locative/.translations/zh-Hant.json | 2 +- .../lutron_caseta/.translations/en.json | 2 +- .../lutron_caseta/.translations/ru.json | 5 +++ .../components/starline/.translations/ca.json | 24 +++++++++++ .../transmission/.translations/en.json | 40 +++++++++++-------- .../components/vacuum/.translations/de.json | 11 ++++- .../components/wled/.translations/de.json | 17 ++++++++ 14 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/climate/.translations/de.json create mode 100644 homeassistant/components/demo/.translations/de.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/de.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/de.json create mode 100644 homeassistant/components/lutron_caseta/.translations/ru.json create mode 100644 homeassistant/components/starline/.translations/ca.json diff --git a/homeassistant/components/almond/.translations/de.json b/homeassistant/components/almond/.translations/de.json index 4e2816cb0011eb..1495cabf9c9111 100644 --- a/homeassistant/components/almond/.translations/de.json +++ b/homeassistant/components/almond/.translations/de.json @@ -2,7 +2,13 @@ "config": { "abort": { "already_setup": "Sie k\u00f6nnen nur ein Almond-Konto konfigurieren.", - "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich." + "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich.", + "missing_configuration": "Bitte \u00fcberpr\u00fcfen Sie die Dokumentation zur Einrichtung von Almond." + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } }, "title": "Almond" } diff --git a/homeassistant/components/climate/.translations/de.json b/homeassistant/components/climate/.translations/de.json new file mode 100644 index 00000000000000..75ffe328fc821b --- /dev/null +++ b/homeassistant/components/climate/.translations/de.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "current_humidity_changed": "Gemessene Luftfeuchtigkeit von {entity_name} ge\u00e4ndert", + "current_temperature_changed": "Gemessene Temperatur von {entity_name} ge\u00e4ndert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/de.json b/homeassistant/components/demo/.translations/de.json new file mode 100644 index 00000000000000..ef01fcb4f3c35e --- /dev/null +++ b/homeassistant/components/demo/.translations/de.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/pl.json b/homeassistant/components/geonetnz_quakes/.translations/pl.json index 427c753f6c1a01..fd82bba43b573e 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/pl.json +++ b/homeassistant/components/geonetnz_quakes/.translations/pl.json @@ -9,7 +9,7 @@ "mmi": "MMI", "radius": "Promie\u0144" }, - "title": "Wype\u0142nij szczeg\u00f3\u0142y dotycz\u0105ce filtra." + "title": "Wprowad\u017a szczeg\u00f3\u0142owe dane filtra." } }, "title": "GeoNet NZ Quakes" diff --git a/homeassistant/components/geonetnz_volcano/.translations/de.json b/homeassistant/components/geonetnz_volcano/.translations/de.json new file mode 100644 index 00000000000000..1a51f1fb4909cb --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "identifier_exists": "Standort bereits registriert" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/pl.json b/homeassistant/components/geonetnz_volcano/.translations/pl.json index 22bb34e21a602f..7d329815f3fd4f 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/pl.json +++ b/homeassistant/components/geonetnz_volcano/.translations/pl.json @@ -8,7 +8,7 @@ "data": { "radius": "Promie\u0144" }, - "title": "Wype\u0142nij szczeg\u00f3\u0142y dotycz\u0105ce filtra." + "title": "Wprowad\u017a szczeg\u00f3\u0142owe dane filtra." } }, "title": "GeoNet NZ Volcano" diff --git a/homeassistant/components/hisense_aehw4a1/.translations/de.json b/homeassistant/components/hisense_aehw4a1/.translations/de.json new file mode 100644 index 00000000000000..8b474ea0418e49 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Es wurden keine Hisense AEH-W4A1-Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Hisense AEH-W4A1 m\u00f6glich." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie Hisense AEH-W4A1 einrichten?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/zh-Hant.json b/homeassistant/components/locative/.translations/zh-Hant.json index 7dd598c8fc217d..5135eb33c9f2da 100644 --- a/homeassistant/components/locative/.translations/zh-Hant.json +++ b/homeassistant/components/locative/.translations/zh-Hant.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" }, "create_entry": { - "default": "\u6b32\u50b3\u9001\u4f4d\u7f6e\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Locative App \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + "default": "\u6b32\u50b3\u9001\u5ea7\u6a19\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Locative App \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" }, "step": { "user": { diff --git a/homeassistant/components/lutron_caseta/.translations/en.json b/homeassistant/components/lutron_caseta/.translations/en.json index cb7ab8c767ebbd..cfc3c290afeebd 100644 --- a/homeassistant/components/lutron_caseta/.translations/en.json +++ b/homeassistant/components/lutron_caseta/.translations/en.json @@ -1,5 +1,5 @@ { "config": { - "title": "Lutron Caséta" + "title": "Lutron Cas\u00e9ta" } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/ru.json b/homeassistant/components/lutron_caseta/.translations/ru.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/ru.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/ca.json b/homeassistant/components/starline/.translations/ca.json new file mode 100644 index 00000000000000..ebfbac0044f841 --- /dev/null +++ b/homeassistant/components/starline/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "auth_captcha": { + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Codi SMS" + }, + "title": "Verificaci\u00f3 en dos passos" + }, + "auth_user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "title": "Credencials d\u2019usuari" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index 45c16be36e2ca9..aa8b99a49142ec 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -1,34 +1,42 @@ { "config": { - "title": "Transmission", + "abort": { + "already_configured": "Host is already configured.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "error": { + "cannot_connect": "Unable to Connect to host", + "name_exists": "Name already exists", + "wrong_credentials": "Wrong username or password" + }, "step": { + "options": { + "data": { + "scan_interval": "Update frequency" + }, + "title": "Configure Options" + }, "user": { - "title": "Setup Transmission Client", "data": { - "name": "Name", "host": "Host", - "username": "Username", + "name": "Name", "password": "Password", - "port": "Port" - } + "port": "Port", + "username": "Username" + }, + "title": "Setup Transmission Client" } }, - "error": { - "name_exists": "Name already exists", - "wrong_credentials": "Wrong username or password", - "cannot_connect": "Unable to Connect to host" - }, - "abort": { - "already_configured": "Host is already configured." - } + "title": "Transmission" }, "options": { "step": { "init": { - "title": "Configure options for Transmission", "data": { "scan_interval": "Update frequency" - } + }, + "description": "Configure options for Transmission", + "title": "Configure options for Transmission" } } } diff --git a/homeassistant/components/vacuum/.translations/de.json b/homeassistant/components/vacuum/.translations/de.json index 060358a0a7a41b..7aed7da23e3f92 100644 --- a/homeassistant/components/vacuum/.translations/de.json +++ b/homeassistant/components/vacuum/.translations/de.json @@ -1,7 +1,16 @@ { "device_automation": { + "action_type": { + "clean": "Lass {entity_name} reinigen", + "dock": "Lass {entity_name} zum Dock zur\u00fcckkehren" + }, "condtion_type": { - "is_cleaning": "{entity_name} reinigt" + "is_cleaning": "{entity_name} reinigt", + "is_docked": "{entity_name} ist angedockt" + }, + "trigger_type": { + "cleaning": "{entity_name} hat mit der Reinigung begonnen", + "docked": "{entity_name} angedockt" } } } \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/de.json b/homeassistant/components/wled/.translations/de.json index f50a24eeac06b8..2d1cc5ef97dc43 100644 --- a/homeassistant/components/wled/.translations/de.json +++ b/homeassistant/components/wled/.translations/de.json @@ -1,6 +1,23 @@ { "config": { + "abort": { + "already_configured": "Dieses WLED-Ger\u00e4t ist bereits konfiguriert.", + "connection_error": "Verbindung zum WLED-Ger\u00e4t fehlgeschlagen." + }, + "error": { + "connection_error": "Verbindung zum WLED-Ger\u00e4t fehlgeschlagen." + }, "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Hostname oder IP-Adresse" + } + }, + "zeroconf_confirm": { + "title": "Gefundenes WLED-Ger\u00e4t" + } + }, "title": "WLED" } } \ No newline at end of file From 738b3363d912c980ab9f074196ad022eba10483a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 27 Nov 2019 12:17:14 +0800 Subject: [PATCH 1826/3953] UpCloud updates (#28646) * Upgrade upcloud-api to 0.4.5 * Fix UpCloud name spelling in manifest * Update data at setup time for better initial states * Clean up signal handlers on remove * Signal data update on server start To keep related binary sensor better up to date. * Improve track_time_interval emulation for initial update --- homeassistant/components/upcloud/__init__.py | 15 +++++++++++++-- homeassistant/components/upcloud/manifest.json | 4 ++-- homeassistant/components/upcloud/switch.py | 4 +++- requirements_all.txt | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index c77b0fe3cddffe..cd2cf5d02c01c6 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -18,6 +18,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval +from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) @@ -82,6 +83,7 @@ def upcloud_update(event_time): dispatcher_send(hass, SIGNAL_UPDATE_UPCLOUD) # Call the UpCloud API to refresh data + upcloud_update(dt.utcnow()) track_time_interval(hass, upcloud_update, scan_interval) return True @@ -108,6 +110,7 @@ def __init__(self, upcloud, uuid): self._upcloud = upcloud self.uuid = uuid self.data = None + self._unsub_handlers = [] @property def unique_id(self) -> str: @@ -124,10 +127,18 @@ def name(self): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_UPCLOUD, self._update_callback + self._unsub_handlers.append( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_UPCLOUD, self._update_callback + ) ) + async def async_will_remove_from_hass(self) -> None: + """Invoke unsubscription handlers.""" + for unsub in self._unsub_handlers: + unsub() + self._unsub_handlers.clear() + @callback def _update_callback(self): """Call update method.""" diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index 62ce608a911f42..0499ce1e9ad8ff 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -1,9 +1,9 @@ { "domain": "upcloud", - "name": "Upcloud", + "name": "UpCloud", "documentation": "https://www.home-assistant.io/integrations/upcloud", "requirements": [ - "upcloud-api==0.4.3" + "upcloud-api==0.4.5" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/upcloud/switch.py b/homeassistant/components/upcloud/switch.py index 66f3d9f42b1781..5cb1d86671e4fb 100644 --- a/homeassistant/components/upcloud/switch.py +++ b/homeassistant/components/upcloud/switch.py @@ -6,8 +6,9 @@ from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import STATE_OFF import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send -from . import CONF_SERVERS, DATA_UPCLOUD, UpCloudServerEntity +from . import CONF_SERVERS, DATA_UPCLOUD, SIGNAL_UPDATE_UPCLOUD, UpCloudServerEntity _LOGGER = logging.getLogger(__name__) @@ -34,6 +35,7 @@ def turn_on(self, **kwargs): """Start the server.""" if self.state == STATE_OFF: self.data.start() + dispatcher_send(self.hass, SIGNAL_UPDATE_UPCLOUD) def turn_off(self, **kwargs): """Stop the server.""" diff --git a/requirements_all.txt b/requirements_all.txt index 987b231005dbfe..c2eabe3a060eba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1959,7 +1959,7 @@ twilio==6.32.0 unifiled==0.11 # homeassistant.components.upcloud -upcloud-api==0.4.3 +upcloud-api==0.4.5 # homeassistant.components.huawei_lte url-normalize==1.4.1 From 5bc8ef962b03e346624031c66e12a5680d1945e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 27 Nov 2019 12:18:24 +0800 Subject: [PATCH 1827/3953] Upgrade huawei-lte-api to 1.4.4 (#29130) https://github.com/Salamek/huawei-lte-api/releases/tag/1.4.4 Closes https://github.com/home-assistant/home-assistant/issues/28922 --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 4ea54188688de7..8fd4ba4bec135c 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.1", - "huawei-lte-api==1.4.3", + "huawei-lte-api==1.4.4", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index c2eabe3a060eba..77e47627a226c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -678,7 +678,7 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.3 +huawei-lte-api==1.4.4 # homeassistant.components.hydrawise hydrawiser==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56a48905eb5150..03a98f0b2889da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ homematicip==0.10.13 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.3 +huawei-lte-api==1.4.4 # homeassistant.components.iaqualink iaqualink==0.3.0 From 8933540950cb755b041535bd628710abb122dde5 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Tue, 26 Nov 2019 23:18:43 -0500 Subject: [PATCH 1828/3953] bumped supported apprise version to 0.8.2 (#29125) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 3e971a96e7eac8..09d840e796acc1 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -3,7 +3,7 @@ "name": "Apprise", "documentation": "https://www.home-assistant.io/components/apprise", "requirements": [ - "apprise==0.8.1" + "apprise==0.8.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 77e47627a226c8..cf6a0a7f8e317f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -227,7 +227,7 @@ apcaccess==0.0.13 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.1 +apprise==0.8.2 # homeassistant.components.aprs aprslib==0.6.46 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03a98f0b2889da..003827d414b04b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -87,7 +87,7 @@ androidtv==0.0.34 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.1 +apprise==0.8.2 # homeassistant.components.aprs aprslib==0.6.46 From fb9f5610521bf59295d11e6ac67602c03a63f38d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:13:27 -0500 Subject: [PATCH 1829/3953] move html5 service to html5 domain (#29145) --- homeassistant/components/html5/const.py | 3 +++ homeassistant/components/html5/notify.py | 5 ++--- homeassistant/components/html5/services.yaml | 9 +++++++++ homeassistant/components/notify/services.yaml | 10 ---------- 4 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/html5/const.py diff --git a/homeassistant/components/html5/const.py b/homeassistant/components/html5/const.py new file mode 100644 index 00000000000000..1d0689511b2f8b --- /dev/null +++ b/homeassistant/components/html5/const.py @@ -0,0 +1,3 @@ +"""Constants for the HTML5 component.""" +DOMAIN = "html5" +SERVICE_DISMISS = "dismiss" diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 18b7ff27ab4b17..481a00e96e13e1 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -34,17 +34,16 @@ ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, - DOMAIN, PLATFORM_SCHEMA, BaseNotificationService, ) +from .const import DOMAIN, SERVICE_DISMISS + _LOGGER = logging.getLogger(__name__) REGISTRATIONS_FILE = "html5_push_registrations.conf" -SERVICE_DISMISS = "html5_dismiss" - ATTR_GCM_SENDER_ID = "gcm_sender_id" ATTR_GCM_API_KEY = "gcm_api_key" ATTR_VAPID_PUB_KEY = "vapid_pub_key" diff --git a/homeassistant/components/html5/services.yaml b/homeassistant/components/html5/services.yaml index e69de29bb2d1d6..5fd068a64dcd51 100644 --- a/homeassistant/components/html5/services.yaml +++ b/homeassistant/components/html5/services.yaml @@ -0,0 +1,9 @@ +dismiss: + description: Dismiss a html5 notification. + fields: + target: + description: An array of targets. Optional. + example: ['my_phone', 'my_tablet'] + data: + description: Extended information of notification. Supports tag. Optional. + example: '{ "tag": "tagname" }' diff --git a/homeassistant/components/notify/services.yaml b/homeassistant/components/notify/services.yaml index 1b7944cc7da0d1..23b1c968c4a390 100644 --- a/homeassistant/components/notify/services.yaml +++ b/homeassistant/components/notify/services.yaml @@ -16,16 +16,6 @@ notify: description: Extended information for notification. Optional depending on the platform. example: platform specific -html5_dismiss: - description: Dismiss a html5 notification. - fields: - target: - description: An array of targets. Optional. - example: ['my_phone', 'my_tablet'] - data: - description: Extended information of notification. Supports tag. Optional. - example: '{ "tag": "tagname" }' - apns_register: description: Registers a device to receive push notifications. fields: From b7896c7b6ff0d8c2bddc152555d9dcd2a648de0a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:14:22 -0500 Subject: [PATCH 1830/3953] move nuki service to nuki domain services.yaml and remove missing service (#29138) --- homeassistant/components/lock/services.yaml | 17 ----------------- homeassistant/components/nuki/services.yaml | 10 ++++++++++ 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index d17e00addd19fd..dad032fd025944 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -20,23 +20,6 @@ get_usercode: description: Code slot to retrieve a code from. example: 1 -nuki_lock_n_go: - description: "Nuki Lock 'n' Go" - fields: - entity_id: - description: Entity id of the Nuki lock. - example: 'lock.front_door' - unlatch: - description: Whether to unlatch the lock. - example: false - -nuki_unlatch: - description: Nuki unlatch. - fields: - entity_id: - description: Entity id of the Nuki lock. - example: 'lock.front_door' - lock: description: Lock all or specified locks. fields: diff --git a/homeassistant/components/nuki/services.yaml b/homeassistant/components/nuki/services.yaml index e69de29bb2d1d6..1300b48e0ddd9c 100644 --- a/homeassistant/components/nuki/services.yaml +++ b/homeassistant/components/nuki/services.yaml @@ -0,0 +1,10 @@ +lock_n_go: + description: "Nuki Lock 'n' Go" + fields: + entity_id: + description: Entity id of the Nuki lock. + example: 'lock.front_door' + unlatch: + description: Whether to unlatch the lock. + example: false + From 975ee0ea7f39df86f7799ce1e092856dcdbf751a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:15:27 -0500 Subject: [PATCH 1831/3953] Update service domain for elkm1 from 'alarm_control_panel' to 'elkm1' (#29128) * move elkm1 services to elkm1 domain * update missed variable references --- .../alarm_control_panel/services.yaml | 52 --------------- homeassistant/components/elkm1/__init__.py | 5 ++ .../components/elkm1/alarm_control_panel.py | 35 +++++++---- homeassistant/components/elkm1/services.yaml | 63 +++++++++++++++++-- 4 files changed, 86 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 9abf2189ed3c65..917600862e3657 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -89,55 +89,3 @@ ifttt_push_alarm_state: state: description: The state to which the alarm control panel has to be set. example: 'armed_night' - -elkm1_alarm_arm_vacation: - description: Arm the ElkM1 in vacation mode. - fields: - entity_id: - description: Name of alarm control panel to arm. - example: 'alarm_control_panel.main' - code: - description: An code to arm the alarm control panel. - example: 1234 - -elkm1_alarm_arm_home_instant: - description: Arm the ElkM1 in home instant mode. - fields: - entity_id: - description: Name of alarm control panel to arm. - example: 'alarm_control_panel.main' - code: - description: An code to arm the alarm control panel. - example: 1234 - -elkm1_alarm_arm_night_instant: - description: Arm the ElkM1 in night instant mode. - fields: - entity_id: - description: Name of alarm control panel to arm. - example: 'alarm_control_panel.main' - code: - description: An code to arm the alarm control panel. - example: 1234 - -elkm1_alarm_display_message: - description: Display a message on all of the ElkM1 keypads for an area. - fields: - entity_id: - description: Name of alarm control panel to display messages on. - example: 'alarm_control_panel.main' - clear: - description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2 - example: 1 - beep: - description: 0=no beep, 1=beep; default 0 - example: 1 - timeout: - description: Time to display message, 0=forever, max 65535, default 0 - example: 4242 - line1: - description: Up to 16 characters of text (truncated if too long). Default blank. - example: The answer to life, - line2: - description: Up to 16 characters of text (truncated if too long). Default blank. - example: the universe, and everything. diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 601309590c8c17..67b84c4f3bf7a3 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -35,6 +35,11 @@ _LOGGER = logging.getLogger(__name__) +SERVICE_ALARM_DISPLAY_MESSAGE = "alarm_display_message" +SERVICE_ALARM_ARM_VACATION = "alarm_arm_vacation" +SERVICE_ALARM_ARM_HOME_INSTANT = "alarm_arm_home_instant" +SERVICE_ALARM_ARM_NIGHT_INSTANT = "alarm_arm_night_instant" + SUPPORTED_DOMAINS = [ "alarm_control_panel", "climate", diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 8c4db6e06a5f02..1e1a8eba9e0ec2 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -2,7 +2,10 @@ from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanel, + FORMAT_NUMBER, +) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -25,7 +28,15 @@ async_dispatcher_send, ) -from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities +from . import ( + create_elk_entities, + DOMAIN, + ElkEntity, + SERVICE_ALARM_ARM_HOME_INSTANT, + SERVICE_ALARM_ARM_NIGHT_INSTANT, + SERVICE_ALARM_ARM_VACATION, + SERVICE_ALARM_DISPLAY_MESSAGE, +) SIGNAL_ARM_ENTITY = "elkm1_arm" SIGNAL_DISPLAY_MESSAGE = "elkm1_display_message" @@ -56,7 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return - elk_datas = hass.data[ELK_DOMAIN] + elk_datas = hass.data[DOMAIN] entities = [] for elk_data in elk_datas.values(): elk = elk_data["elk"] @@ -75,7 +86,7 @@ def _arm_service(service): for service in _arm_services(): hass.services.async_register( - alarm.DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA + DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA ) def _display_message_service(service): @@ -91,8 +102,8 @@ def _display_message_service(service): _dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args) hass.services.async_register( - alarm.DOMAIN, - "elkm1_alarm_display_message", + DOMAIN, + SERVICE_ALARM_DISPLAY_MESSAGE, _display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA, ) @@ -100,13 +111,13 @@ def _display_message_service(service): def _arm_services(): return { - "elkm1_alarm_arm_vacation": ArmLevel.ARMED_VACATION.value, - "elkm1_alarm_arm_home_instant": ArmLevel.ARMED_STAY_INSTANT.value, - "elkm1_alarm_arm_night_instant": ArmLevel.ARMED_NIGHT_INSTANT.value, + SERVICE_ALARM_ARM_VACATION: ArmLevel.ARMED_VACATION.value, + SERVICE_ALARM_ARM_HOME_INSTANT: ArmLevel.ARMED_STAY_INSTANT.value, + SERVICE_ALARM_ARM_NIGHT_INSTANT: ArmLevel.ARMED_NIGHT_INSTANT.value, } -class ElkArea(ElkEntity, alarm.AlarmControlPanel): +class ElkArea(ElkEntity, AlarmControlPanel): """Representation of an Area / Partition within the ElkM1 alarm panel.""" def __init__(self, element, elk, elk_data): @@ -133,7 +144,7 @@ def _watch_keypad(self, keypad, changeset): if keypad.area != self._element.index: return if changeset.get("last_user") is not None: - self._changed_by_entity_id = self.hass.data[ELK_DOMAIN][self._prefix][ + self._changed_by_entity_id = self.hass.data[DOMAIN][self._prefix][ "keypads" ].get(keypad.index, "") self.async_schedule_update_ha_state(True) @@ -141,7 +152,7 @@ def _watch_keypad(self, keypad, changeset): @property def code_format(self): """Return the alarm code format.""" - return alarm.FORMAT_NUMBER + return FORMAT_NUMBER @property def state(self): diff --git a/homeassistant/components/elkm1/services.yaml b/homeassistant/components/elkm1/services.yaml index 405716569630ee..fbcbf7edc6dded 100644 --- a/homeassistant/components/elkm1/services.yaml +++ b/homeassistant/components/elkm1/services.yaml @@ -1,12 +1,65 @@ -speak_word: - description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation. +alarm_arm_home_instant: + description: Arm the ElkM1 in home instant mode. fields: - number: - description: Word number to speak. - example: 142 + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +alarm_arm_night_instant: + description: Arm the ElkM1 in night instant mode. + fields: + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +alarm_arm_vacation: + description: Arm the ElkM1 in vacation mode. + fields: + entity_id: + description: Name of alarm control panel to arm. + example: 'alarm_control_panel.main' + code: + description: An code to arm the alarm control panel. + example: 1234 + +alarm_display_message: + description: Display a message on all of the ElkM1 keypads for an area. + fields: + entity_id: + description: Name of alarm control panel to display messages on. + example: 'alarm_control_panel.main' + clear: + description: 0=clear message, 1=clear message with * key, 2=Display until timeout; default 2 + example: 1 + beep: + description: 0=no beep, 1=beep; default 0 + example: 1 + timeout: + description: Time to display message, 0=forever, max 65535, default 0 + example: 4242 + line1: + description: Up to 16 characters of text (truncated if too long). Default blank. + example: The answer to life, + line2: + description: Up to 16 characters of text (truncated if too long). Default blank. + example: the universe, and everything. + speak_phrase: description: Speak a phrase. See list of phrases in ElkM1 ASCII Protocol documentation. fields: number: description: Phrase number to speak. example: 42 + +speak_word: + description: Speak a word. See list of words in ElkM1 ASCII Protocol documentation. + fields: + number: + description: Word number to speak. + example: 142 From b8f03aa3becc189ebf4631a5c1198a2dc5bc055b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:15:59 -0500 Subject: [PATCH 1832/3953] move neato service to neato domain (#29148) --- homeassistant/components/neato/const.py | 2 ++ homeassistant/components/neato/services.yaml | 18 ++++++++++++++++++ homeassistant/components/neato/vacuum.py | 6 ++---- homeassistant/components/vacuum/services.yaml | 19 ------------------- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index 6dbaeb10d36500..cfe8a2dad9d119 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -11,6 +11,8 @@ SCAN_INTERVAL_MINUTES = 5 +SERVICE_NEATO_CUSTOM_CLEANING = "custom_cleaning" + VALID_VENDORS = ["neato", "vorwerk"] MODE = {1: "Eco", 2: "Turbo"} diff --git a/homeassistant/components/neato/services.yaml b/homeassistant/components/neato/services.yaml index e69de29bb2d1d6..b40edabd77964e 100644 --- a/homeassistant/components/neato/services.yaml +++ b/homeassistant/components/neato/services.yaml @@ -0,0 +1,18 @@ +custom_cleaning: + description: Zone Cleaning service call specific to Neato Botvacs. + fields: + entity_id: + description: Name of the vacuum entity. [Required] + example: 'vacuum.neato' + mode: + description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." + example: 2 + navigation: + description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." + example: 1 + category: + description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." + example: 2 + zone: + description: Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. + example: "Kitchen" \ No newline at end of file diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 40ed79042c760c..ec8ff22d83e9a9 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -7,7 +7,6 @@ from homeassistant.components.vacuum import ( ATTR_STATUS, - DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, @@ -40,6 +39,7 @@ NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES, + SERVICE_NEATO_CUSTOM_CLEANING, ) _LOGGER = logging.getLogger(__name__) @@ -73,8 +73,6 @@ ATTR_CATEGORY = "category" ATTR_ZONE = "zone" -SERVICE_NEATO_CUSTOM_CLEANING = "neato_custom_cleaning" - SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_ids, @@ -126,7 +124,7 @@ def service_to_entities(call): return entities hass.services.async_register( - DOMAIN, + NEATO_DOMAIN, SERVICE_NEATO_CUSTOM_CLEANING, neato_custom_cleaning_service, schema=SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA, diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index fe5bb77cefea41..792658bbdfd074 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -144,22 +144,3 @@ xiaomi_clean_zone: repeats: description: Number of cleaning repeats for each zone between 1 and 3. example: '1' - -neato_custom_cleaning: - description: Zone Cleaning service call specific to Neato Botvacs. - fields: - entity_id: - description: Name of the vacuum entity. [Required] - example: 'vacuum.neato' - mode: - description: "Set the cleaning mode: 1 for eco and 2 for turbo. Defaults to turbo if not set." - example: 2 - navigation: - description: "Set the navigation mode: 1 for normal, 2 for extra care, 3 for deep. Defaults to normal if not set." - example: 1 - category: - description: "Whether to use a persistent map or not for cleaning (i.e. No go lines): 2 for no map, 4 for map. Default to using map if not set (and fallback to no map if no map is found)." - example: 2 - zone: - description: Only supported on the Botvac D7. Name of the zone to clean. Defaults to no zone i.e. complete house cleanup. - example: "Kitchen" From ff4d256893e310dbad5a721b9d31d75e546daaae Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:16:53 -0500 Subject: [PATCH 1833/3953] move icloud services to icloud domain (#29144) --- .coveragerc | 2 +- .../components/device_tracker/services.yaml | 37 ------------------ homeassistant/components/icloud/const.py | 6 +++ .../components/icloud/device_tracker.py | 19 +++++---- homeassistant/components/icloud/services.yaml | 39 +++++++++++++++++++ 5 files changed, 58 insertions(+), 45 deletions(-) create mode 100644 homeassistant/components/icloud/const.py diff --git a/.coveragerc b/.coveragerc index 7164dd3d35ed58..f9e026fefda344 100644 --- a/.coveragerc +++ b/.coveragerc @@ -316,7 +316,7 @@ omit = homeassistant/components/iaqualink/light.py homeassistant/components/iaqualink/sensor.py homeassistant/components/iaqualink/switch.py - homeassistant/components/icloud/device_tracker.py + homeassistant/components/icloud/* homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py homeassistant/components/izone/__init__.py diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 938e9c8e3249f8..51865034b00fbf 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -24,40 +24,3 @@ see: battery: description: Battery level of device. example: '100' - -icloud_lost_iphone: - description: Service to play the lost iphone sound on an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. - example: 'iphonebart' -icloud_set_interval: - description: Service to set the interval of an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. - example: 'iphonebart' - interval: - description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. - example: 1 -icloud_update: - description: Service to ask for an update of an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. - example: 'iphonebart' -icloud_reset_account: - description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. - fields: - account_name: - description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. - example: 'bart' diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py new file mode 100644 index 00000000000000..fe8010df703b43 --- /dev/null +++ b/homeassistant/components/icloud/const.py @@ -0,0 +1,6 @@ +"""Constants for the iCloud component.""" +DOMAIN = "icloud" +SERVICE_LOST_IPHONE = "lost_iphone" +SERVICE_UPDATE = "update" +SERVICE_RESET_ACCOUNT = "reset_account" +SERVICE_SET_INTERVAL = "set_interval" diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index f6efb05fd0808c..3d9fb4715da0b6 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -14,7 +14,6 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker.const import ( ATTR_ATTRIBUTES, - DOMAIN, ENTITY_ID_FORMAT, ) from homeassistant.components.device_tracker.legacy import DeviceScanner @@ -27,6 +26,14 @@ import homeassistant.util.dt as dt_util from homeassistant.util.location import distance +from .const import ( + DOMAIN, + SERVICE_LOST_IPHONE, + SERVICE_RESET_ACCOUNT, + SERVICE_SET_INTERVAL, + SERVICE_UPDATE, +) + _LOGGER = logging.getLogger(__name__) CONF_ACCOUNTNAME = "account_name" @@ -144,7 +151,7 @@ def lost_iphone(call): ICLOUDTRACKERS[account].lost_iphone(devicename) hass.services.register( - DOMAIN, "icloud_lost_iphone", lost_iphone, schema=SERVICE_SCHEMA + DOMAIN, SERVICE_LOST_IPHONE, lost_iphone, schema=SERVICE_SCHEMA ) def update_icloud(call): @@ -155,9 +162,7 @@ def update_icloud(call): if account in ICLOUDTRACKERS: ICLOUDTRACKERS[account].update_icloud(devicename) - hass.services.register( - DOMAIN, "icloud_update", update_icloud, schema=SERVICE_SCHEMA - ) + hass.services.register(DOMAIN, SERVICE_UPDATE, update_icloud, schema=SERVICE_SCHEMA) def reset_account_icloud(call): """Reset an iCloud account.""" @@ -167,7 +172,7 @@ def reset_account_icloud(call): ICLOUDTRACKERS[account].reset_account_icloud() hass.services.register( - DOMAIN, "icloud_reset_account", reset_account_icloud, schema=SERVICE_SCHEMA + DOMAIN, SERVICE_RESET_ACCOUNT, reset_account_icloud, schema=SERVICE_SCHEMA ) def setinterval(call): @@ -180,7 +185,7 @@ def setinterval(call): ICLOUDTRACKERS[account].setinterval(interval, devicename) hass.services.register( - DOMAIN, "icloud_set_interval", setinterval, schema=SERVICE_SCHEMA + DOMAIN, SERVICE_SET_INTERVAL, setinterval, schema=SERVICE_SCHEMA ) # Tells the bootstrapper that the component was successfully initialized diff --git a/homeassistant/components/icloud/services.yaml b/homeassistant/components/icloud/services.yaml index e69de29bb2d1d6..7b2d3b80e8435b 100644 --- a/homeassistant/components/icloud/services.yaml +++ b/homeassistant/components/icloud/services.yaml @@ -0,0 +1,39 @@ +lost_iphone: + description: Service to play the lost iphone sound on an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. + example: 'iphonebart' + +set_interval: + description: Service to set the interval of an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. + example: 'iphonebart' + interval: + description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. + example: 1 + +update: + description: Service to ask for an update of an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. + example: 'iphonebart' + +reset_account: + description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. + fields: + account_name: + description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. + example: 'bart' From 0ff0ec7d3eb038cf5e1adb13dd66d6d407501ba0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:18:21 -0500 Subject: [PATCH 1834/3953] move econet services to econet domain (#29149) --- .coveragerc | 2 +- homeassistant/components/econet/const.py | 5 +++++ homeassistant/components/econet/services.yaml | 19 ++++++++++++++++++ .../components/econet/water_heater.py | 6 ++---- .../components/water_heater/services.yaml | 20 ------------------- 5 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/econet/const.py diff --git a/.coveragerc b/.coveragerc index f9e026fefda344..06a106f1f663ed 100644 --- a/.coveragerc +++ b/.coveragerc @@ -178,7 +178,7 @@ omit = homeassistant/components/ecobee/notify.py homeassistant/components/ecobee/sensor.py homeassistant/components/ecobee/weather.py - homeassistant/components/econet/water_heater.py + homeassistant/components/econet/* homeassistant/components/ecovacs/* homeassistant/components/eddystone_temperature/sensor.py homeassistant/components/edimax/switch.py diff --git a/homeassistant/components/econet/const.py b/homeassistant/components/econet/const.py new file mode 100644 index 00000000000000..88b1b851aa6d2b --- /dev/null +++ b/homeassistant/components/econet/const.py @@ -0,0 +1,5 @@ +"""Constants for Econet integration.""" + +DOMAIN = "econet" +SERVICE_ADD_VACATION = "add_vacation" +SERVICE_DELETE_VACATION = "delete_vacation" diff --git a/homeassistant/components/econet/services.yaml b/homeassistant/components/econet/services.yaml index e69de29bb2d1d6..9f489165c22e96 100644 --- a/homeassistant/components/econet/services.yaml +++ b/homeassistant/components/econet/services.yaml @@ -0,0 +1,19 @@ +add_vacation: + description: Add a vacation to your water heater. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'water_heater.econet' + start_date: + description: The timestamp of when the vacation should start. (Optional, defaults to now) + example: 1513186320 + end_date: + description: The timestamp of when the vacation should end. + example: 1513445520 + +delete_vacation: + description: Delete your existing vacation from your water heater. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'water_heater.econet' \ No newline at end of file diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 35547c8878ee84..26ee7cb8bd422f 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -6,7 +6,6 @@ import voluptuous as vol from homeassistant.components.water_heater import ( - DOMAIN, PLATFORM_SCHEMA, STATE_ECO, STATE_ELECTRIC, @@ -28,6 +27,8 @@ ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_ADD_VACATION, SERVICE_DELETE_VACATION + _LOGGER = logging.getLogger(__name__) ATTR_VACATION_START = "next_vacation_start_date" @@ -41,9 +42,6 @@ SUPPORT_FLAGS_HEATER = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE -SERVICE_ADD_VACATION = "econet_add_vacation" -SERVICE_DELETE_VACATION = "econet_delete_vacation" - ADD_VACATION_SCHEMA = vol.Schema( { vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, diff --git a/homeassistant/components/water_heater/services.yaml b/homeassistant/components/water_heater/services.yaml index 72a3f909fbbd24..7a26e5bc0d4bec 100644 --- a/homeassistant/components/water_heater/services.yaml +++ b/homeassistant/components/water_heater/services.yaml @@ -29,23 +29,3 @@ set_operation_mode: operation_mode: description: New value of operation mode. example: eco - -econet_add_vacation: - description: Add a vacation to your water heater. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'water_heater.econet' - start_date: - description: The timestamp of when the vacation should start. (Optional, defaults to now) - example: 1513186320 - end_date: - description: The timestamp of when the vacation should end. - example: 1513445520 - -econet_delete_vacation: - description: Delete your existing vacation from your water heater. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'water_heater.econet' \ No newline at end of file From c72e2304329571e0adaddec377fbaeabb5e60caf Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:19:01 -0500 Subject: [PATCH 1835/3953] move songpal service to songpal domain (#29143) --- .coveragerc | 2 +- .../components/media_player/services.yaml | 14 -------------- homeassistant/components/songpal/const.py | 3 +++ homeassistant/components/songpal/media_player.py | 5 ++--- homeassistant/components/songpal/services.yaml | 13 +++++++++++++ 5 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/songpal/const.py diff --git a/.coveragerc b/.coveragerc index 06a106f1f663ed..e859e05f6ee8e3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -637,7 +637,7 @@ omit = homeassistant/components/somfy/* homeassistant/components/somfy_mylink/* homeassistant/components/sonarr/sensor.py - homeassistant/components/songpal/media_player.py + homeassistant/components/songpal/* homeassistant/components/sonos/* homeassistant/components/sony_projector/switch.py homeassistant/components/spc/* diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 8b7614ddfdbb4c..f30848e0078ab7 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -243,17 +243,3 @@ yamaha_enable_output: enabled: description: Boolean indicating if port should be enabled or not. example: true - -songpal_set_sound_setting: - description: Change sound setting. - - fields: - entity_id: - description: Target device. - example: 'media_player.my_soundbar' - name: - description: Name of the setting. - example: 'nightMode' - value: - description: Value to set. - example: 'on' diff --git a/homeassistant/components/songpal/const.py b/homeassistant/components/songpal/const.py new file mode 100644 index 00000000000000..6a19e316a9f38c --- /dev/null +++ b/homeassistant/components/songpal/const.py @@ -0,0 +1,3 @@ +"""Constants for the Songpal component.""" +DOMAIN = "songpal" +SET_SOUND_SETTING = "set_sound_setting" diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 0567cd0ea6a297..681c97a7710754 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -15,7 +15,6 @@ from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -33,6 +32,8 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SET_SOUND_SETTING + _LOGGER = logging.getLogger(__name__) CONF_ENDPOINT = "endpoint" @@ -42,8 +43,6 @@ PLATFORM = "songpal" -SET_SOUND_SETTING = "songpal_set_sound_setting" - SUPPORT_SONGPAL = ( SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP diff --git a/homeassistant/components/songpal/services.yaml b/homeassistant/components/songpal/services.yaml index e69de29bb2d1d6..8cf1a664276eda 100644 --- a/homeassistant/components/songpal/services.yaml +++ b/homeassistant/components/songpal/services.yaml @@ -0,0 +1,13 @@ +set_sound_setting: + description: Change sound setting. + + fields: + entity_id: + description: Target device. + example: 'media_player.my_soundbar' + name: + description: Name of the setting. + example: 'nightMode' + value: + description: Value to set. + example: 'on' From 5120181e0e217c47cf1c291852710700b5ad11b0 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:20:24 -0500 Subject: [PATCH 1836/3953] move facebox service to facebox domain (#29151) --- homeassistant/components/facebox/const.py | 4 ++++ .../components/facebox/image_processing.py | 4 ++-- homeassistant/components/facebox/services.yaml | 12 ++++++++++++ .../components/image_processing/services.yaml | 13 ------------- tests/components/facebox/test_image_processing.py | 8 ++++---- 5 files changed, 22 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/facebox/const.py diff --git a/homeassistant/components/facebox/const.py b/homeassistant/components/facebox/const.py new file mode 100644 index 00000000000000..991ec925a9817a --- /dev/null +++ b/homeassistant/components/facebox/const.py @@ -0,0 +1,4 @@ +"""Constants for the Facebox component.""" + +DOMAIN = "facebox" +SERVICE_TEACH_FACE = "teach_face" diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index 228cae2f19d54f..ba53ac1ac7d065 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -15,7 +15,6 @@ CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, - DOMAIN, ) from homeassistant.const import ( CONF_IP_ADDRESS, @@ -27,6 +26,8 @@ HTTP_UNAUTHORIZED, ) +from .const import DOMAIN, SERVICE_TEACH_FACE + _LOGGER = logging.getLogger(__name__) ATTR_BOUNDING_BOX = "bounding_box" @@ -38,7 +39,6 @@ CLASSIFIER = "facebox" DATA_FACEBOX = "facebox_classifiers" FILE_PATH = "file_path" -SERVICE_TEACH_FACE = "facebox_teach_face" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/facebox/services.yaml b/homeassistant/components/facebox/services.yaml index e69de29bb2d1d6..c6b686efb8517c 100644 --- a/homeassistant/components/facebox/services.yaml +++ b/homeassistant/components/facebox/services.yaml @@ -0,0 +1,12 @@ +teach_face: + description: Teach facebox a face using a file. + fields: + entity_id: + description: The facebox entity to teach. + example: 'image_processing.facebox' + name: + description: The name of the face to teach. + example: 'my_name' + file_path: + description: The path to the image file. + example: '/images/my_image.jpg' diff --git a/homeassistant/components/image_processing/services.yaml b/homeassistant/components/image_processing/services.yaml index 0689c34c1a3eec..1f1fa347dc9b5f 100644 --- a/homeassistant/components/image_processing/services.yaml +++ b/homeassistant/components/image_processing/services.yaml @@ -6,16 +6,3 @@ scan: entity_id: description: Name(s) of entities to scan immediately. example: 'image_processing.alpr_garage' - -facebox_teach_face: - description: Teach facebox a face using a file. - fields: - entity_id: - description: The facebox entity to teach. - example: 'image_processing.facebox' - name: - description: The name of the face to teach. - example: 'my_name' - file_path: - description: The path to the image file. - example: '/images/my_image.jpg' diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py index edad90e41c7184..9b4610e84b0d3e 100644 --- a/tests/components/facebox/test_image_processing.py +++ b/tests/components/facebox/test_image_processing.py @@ -261,7 +261,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() @@ -275,7 +275,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert "AuthenticationError on facebox" in caplog.text @@ -290,7 +290,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert MOCK_ERROR_NO_FACE in caplog.text @@ -305,7 +305,7 @@ async def test_teach_service( fb.FILE_PATH: MOCK_FILE_PATH, } await hass.services.async_call( - ip.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data + fb.DOMAIN, fb.SERVICE_TEACH_FACE, service_data=data ) await hass.async_block_till_done() assert "ConnectionError: Is facebox running?" in caplog.text From 9e882ef6b4384c618c99212fa78e5df58e805851 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:21:52 -0500 Subject: [PATCH 1837/3953] move wink service definitions from lock to wink domain (#29137) --- homeassistant/components/lock/services.yaml | 63 -------------------- homeassistant/components/wink/lock.py | 12 ++-- homeassistant/components/wink/services.yaml | 65 ++++++++++++++++++++- 3 files changed, 70 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index dad032fd025944..fea02bb025e2f7 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -62,66 +62,3 @@ unlock: code: description: An optional code to unlock the lock with. example: 1234 - -wink_set_lock_vacation_mode: - description: Set vacation mode for all or specified locks. Disables all user codes. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - enabled: - description: enable or disable. true or false. - example: true - -wink_set_lock_alarm_mode: - description: Set alarm mode for all or specified locks. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - mode: - description: One of tamper, activity, or forced_entry. - example: tamper - -wink_set_lock_alarm_sensitivity: - description: Set alarm sensitivity for all or specified locks. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - sensitivity: - description: One of low, medium_low, medium, medium_high, high. - example: medium - -wink_set_lock_alarm_state: - description: Set alarm state. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - enabled: - description: enable or disable. true or false. - example: true - -wink_set_lock_beeper_state: - description: Set beeper state. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - enabled: - description: enable or disable. true or false. - example: true - -wink_add_new_lock_key_code: - description: Add a new user key code. - fields: - entity_id: - description: Name of lock to unlock. - example: 'lock.front_door' - name: - description: name of the new key code. - example: Bob - code: - description: new key code, length must match length of other codes. Default length is 4. - example: 1234 diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py index 37b27c0d500b80..57cf9d304ec9ee 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -18,12 +18,12 @@ _LOGGER = logging.getLogger(__name__) -SERVICE_SET_VACATION_MODE = "wink_set_lock_vacation_mode" -SERVICE_SET_ALARM_MODE = "wink_set_lock_alarm_mode" -SERVICE_SET_ALARM_SENSITIVITY = "wink_set_lock_alarm_sensitivity" -SERVICE_SET_ALARM_STATE = "wink_set_lock_alarm_state" -SERVICE_SET_BEEPER_STATE = "wink_set_lock_beeper_state" -SERVICE_ADD_KEY = "wink_add_new_lock_key_code" +SERVICE_SET_VACATION_MODE = "set_lock_vacation_mode" +SERVICE_SET_ALARM_MODE = "set_lock_alarm_mode" +SERVICE_SET_ALARM_SENSITIVITY = "set_lock_alarm_sensitivity" +SERVICE_SET_ALARM_STATE = "set_lock_alarm_state" +SERVICE_SET_BEEPER_STATE = "set_lock_beeper_state" +SERVICE_ADD_KEY = "add_new_lock_key_code" ATTR_ENABLED = "enabled" ATTR_SENSITIVITY = "sensitivity" diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml index a3b489f9cf54ba..93d53159702ae4 100644 --- a/homeassistant/components/wink/services.yaml +++ b/homeassistant/components/wink/services.yaml @@ -151,4 +151,67 @@ set_nimbus_dial_state: example: 250 labels: description: The values shown on the dial labels ["Dial 1", "test"] the first value is what is shown by default the second value is shown when the nimbus is pressed - example: ["example", "test"] \ No newline at end of file + example: ["example", "test"] + +set_lock_vacation_mode: + description: Set vacation mode for all or specified locks. Disables all user codes. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + +set_lock_alarm_mode: + description: Set alarm mode for all or specified locks. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + mode: + description: One of tamper, activity, or forced_entry. + example: tamper + +set_lock_alarm_sensitivity: + description: Set alarm sensitivity for all or specified locks. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + sensitivity: + description: One of low, medium_low, medium, medium_high, high. + example: medium + +set_lock_alarm_state: + description: Set alarm state. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + +set_lock_beeper_state: + description: Set beeper state. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + enabled: + description: enable or disable. true or false. + example: true + +add_new_lock_key_code: + description: Add a new user key code. + fields: + entity_id: + description: Name of lock to unlock. + example: 'lock.front_door' + name: + description: name of the new key code. + example: Bob + code: + description: new key code, length must match length of other codes. Default length is 4. + example: 1234 \ No newline at end of file From 1f13809c6d70b557dc105ff2d6f687d77b6712e5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:23:07 -0500 Subject: [PATCH 1838/3953] Update service domain for lifx from 'light' to 'lifx' (#29136) * update service domain for lifx custom services * fix service name --- homeassistant/components/lifx/light.py | 23 +++--- homeassistant/components/lifx/services.yaml | 77 +++++++++++++++++++ homeassistant/components/light/services.yaml | 78 -------------------- 3 files changed, 90 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 50e36e8db0ac16..7b33e0c1d86f02 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -60,7 +60,7 @@ MESSAGE_RETRIES = 8 UNAVAILABLE_GRACE = 90 -SERVICE_LIFX_SET_STATE = "lifx_set_state" +SERVICE_LIFX_SET_STATE = "set_state" ATTR_INFRARED = "infrared" ATTR_ZONES = "zones" @@ -74,9 +74,9 @@ } ) -SERVICE_EFFECT_PULSE = "lifx_effect_pulse" -SERVICE_EFFECT_COLORLOOP = "lifx_effect_colorloop" -SERVICE_EFFECT_STOP = "lifx_effect_stop" +SERVICE_EFFECT_PULSE = "effect_pulse" +SERVICE_EFFECT_COLORLOOP = "effect_colorloop" +SERVICE_EFFECT_STOP = "effect_stop" ATTR_POWER_ON = "power_on" ATTR_PERIOD = "period" @@ -282,7 +282,7 @@ def cleanup(self, event=None): SERVICE_EFFECT_PULSE, SERVICE_EFFECT_COLORLOOP, ]: - self.hass.services.async_remove(DOMAIN, service) + self.hass.services.async_remove(LIFX_DOMAIN, service) def register_set_state(self): """Register the LIFX set_state service call.""" @@ -298,7 +298,7 @@ async def service_handler(service): await asyncio.wait(tasks) self.hass.services.async_register( - DOMAIN, + LIFX_DOMAIN, SERVICE_LIFX_SET_STATE, service_handler, schema=LIFX_SET_STATE_SCHEMA, @@ -314,21 +314,24 @@ async def service_handler(service): await self.start_effect(entities, service.service, **service.data) self.hass.services.async_register( - DOMAIN, + LIFX_DOMAIN, SERVICE_EFFECT_PULSE, service_handler, schema=LIFX_EFFECT_PULSE_SCHEMA, ) self.hass.services.async_register( - DOMAIN, + LIFX_DOMAIN, SERVICE_EFFECT_COLORLOOP, service_handler, schema=LIFX_EFFECT_COLORLOOP_SCHEMA, ) self.hass.services.async_register( - DOMAIN, SERVICE_EFFECT_STOP, service_handler, schema=LIFX_EFFECT_STOP_SCHEMA + LIFX_DOMAIN, + SERVICE_EFFECT_STOP, + service_handler, + schema=LIFX_EFFECT_STOP_SCHEMA, ) async def start_effect(self, entities, service, **kwargs): @@ -652,7 +655,7 @@ async def default_effect(self, **kwargs): """Start an effect with default parameters.""" service = kwargs[ATTR_EFFECT] data = {ATTR_ENTITY_ID: self.entity_id} - await self.hass.services.async_call(DOMAIN, service, data) + await self.hass.services.async_call(LIFX_DOMAIN, service, data) async def async_update(self): """Update bulb status.""" diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml index e69de29bb2d1d6..ebf2032a9a51be 100644 --- a/homeassistant/components/lifx/services.yaml +++ b/homeassistant/components/lifx/services.yaml @@ -0,0 +1,77 @@ +set_state: + description: Set a color/brightness and possibliy turn the light on/off. + fields: + entity_id: + description: Name(s) of entities to set a state on. + example: "light.garage" + "...": + description: All turn_on parameters can be used to specify a color. + infrared: + description: Automatic infrared level (0..255) when light brightness is low. + example: 255 + zones: + description: List of zone numbers to affect (8 per LIFX Z, starts at 0). + example: "[0,5]" + transition: + description: Duration in seconds it takes to get to the final state. + example: 10 + power: + description: Turn the light on (True) or off (False). Leave out to keep the power as it is. + example: True + +effect_pulse: + description: Run a flash effect by changing to a color and back. + fields: + entity_id: + description: Name(s) of entities to run the effect on. + example: "light.kitchen" + mode: + description: "Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid." + example: strobe + brightness: + description: Number between 0..255 indicating brightness of the temporary color. + example: 120 + color_name: + description: A human readable color name. + example: "red" + rgb_color: + description: The temporary color in RGB-format. + example: "[255, 100, 100]" + period: + description: Duration of the effect in seconds (default 1.0). + example: 3 + cycles: + description: Number of times the effect should run (default 1.0). + example: 2 + power_on: + description: Powered off lights are temporarily turned on during the effect (default True). + example: False + +effect_colorloop: + description: Run an effect with looping colors. + fields: + entity_id: + description: Name(s) of entities to run the effect on. + example: "light.disco1, light.disco2, light.disco3" + brightness: + description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. + example: 120 + period: + description: Duration (in seconds) between color changes (default 60). + example: 180 + change: + description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20). + example: 45 + spread: + description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30). + example: 0 + power_on: + description: Powered off lights are temporarily turned on during the effect (default True). + example: False + +effect_stop: + description: Stop a running effect. + fields: + entity_id: + description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. + example: "light.bedroom" diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 9173f79f9647c7..439c45962db263 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -120,84 +120,6 @@ toggle: - colorloop - random -lifx_set_state: - description: Set a color/brightness and possibliy turn the light on/off. - fields: - entity_id: - description: Name(s) of entities to set a state on. - example: "light.garage" - "...": - description: All turn_on parameters can be used to specify a color. - infrared: - description: Automatic infrared level (0..255) when light brightness is low. - example: 255 - zones: - description: List of zone numbers to affect (8 per LIFX Z, starts at 0). - example: "[0,5]" - transition: - description: Duration in seconds it takes to get to the final state. - example: 10 - power: - description: Turn the light on (True) or off (False). Leave out to keep the power as it is. - example: True - -lifx_effect_pulse: - description: Run a flash effect by changing to a color and back. - fields: - entity_id: - description: Name(s) of entities to run the effect on. - example: "light.kitchen" - mode: - description: "Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid." - example: strobe - brightness: - description: Number between 0..255 indicating brightness of the temporary color. - example: 120 - color_name: - description: A human readable color name. - example: "red" - rgb_color: - description: The temporary color in RGB-format. - example: "[255, 100, 100]" - period: - description: Duration of the effect in seconds (default 1.0). - example: 3 - cycles: - description: Number of times the effect should run (default 1.0). - example: 2 - power_on: - description: Powered off lights are temporarily turned on during the effect (default True). - example: False - -lifx_effect_colorloop: - description: Run an effect with looping colors. - fields: - entity_id: - description: Name(s) of entities to run the effect on. - example: "light.disco1, light.disco2, light.disco3" - brightness: - description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. - example: 120 - period: - description: Duration (in seconds) between color changes (default 60). - example: 180 - change: - description: Hue movement per period, in degrees on a color wheel (ranges from 0 to 360, default 20). - example: 45 - spread: - description: Maximum hue difference between participating lights, in degrees on a color wheel (ranges from 0 to 360, default 30). - example: 0 - power_on: - description: Powered off lights are temporarily turned on during the effect (default True). - example: False - -lifx_effect_stop: - description: Stop a running effect. - fields: - entity_id: - description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. - example: "light.bedroom" - xiaomi_miio_set_scene: description: Set a fixed scene. fields: From ec61a8667869ee2aef97648423664472b6874c35 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:23:52 -0500 Subject: [PATCH 1839/3953] move service constants to const.py and move channels services to channels domain (#29139) --- .coveragerc | 2 +- homeassistant/components/channels/const.py | 5 ++++ .../components/channels/media_player.py | 6 ++--- .../components/channels/services.yaml | 23 ++++++++++++++++++ .../components/media_player/services.yaml | 24 ------------------- 5 files changed, 31 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/channels/const.py diff --git a/.coveragerc b/.coveragerc index e859e05f6ee8e3..73b203a181ad8d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -109,7 +109,7 @@ omit = homeassistant/components/cast/* homeassistant/components/cert_expiry/sensor.py homeassistant/components/cert_expiry/helper.py - homeassistant/components/channels/media_player.py + homeassistant/components/channels/* homeassistant/components/cisco_ios/device_tracker.py homeassistant/components/cisco_mobility_express/device_tracker.py homeassistant/components/cisco_webex_teams/notify.py diff --git a/homeassistant/components/channels/const.py b/homeassistant/components/channels/const.py new file mode 100644 index 00000000000000..5ae7fdebb0b19f --- /dev/null +++ b/homeassistant/components/channels/const.py @@ -0,0 +1,5 @@ +"""Constants for the Channels component.""" +DOMAIN = "channels" +SERVICE_SEEK_FORWARD = "seek_forward" +SERVICE_SEEK_BACKWARD = "seek_backward" +SERVICE_SEEK_BY = "seek_by" diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 6d978a5451e354..e4acc2f907c862 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -6,7 +6,6 @@ from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_EPISODE, MEDIA_TYPE_MOVIE, @@ -31,6 +30,8 @@ ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_SEEK_BACKWARD, SERVICE_SEEK_BY, SERVICE_SEEK_FORWARD + _LOGGER = logging.getLogger(__name__) DATA_CHANNELS = "channels" @@ -56,9 +57,6 @@ } ) -SERVICE_SEEK_FORWARD = "channels_seek_forward" -SERVICE_SEEK_BACKWARD = "channels_seek_backward" -SERVICE_SEEK_BY = "channels_seek_by" # Service call validation schemas ATTR_SECONDS = "seconds" diff --git a/homeassistant/components/channels/services.yaml b/homeassistant/components/channels/services.yaml index e69de29bb2d1d6..cbb1dd201a67b4 100644 --- a/homeassistant/components/channels/services.yaml +++ b/homeassistant/components/channels/services.yaml @@ -0,0 +1,23 @@ +seek_forward: + description: Seek forward by a set number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + +seek_backward: + description: Seek backward by a set number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + +seek_by: + description: Seek by an inputted number of seconds. + fields: + entity_id: + description: Name of entity for the instance of Channels to seek in. + example: 'media_player.family_room_channels' + seconds: + description: Number of seconds to seek by. Negative numbers seek backwards. + example: 120 diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index f30848e0078ab7..e0ec1661e9fcfd 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -157,30 +157,6 @@ shuffle_set: description: True/false for enabling/disabling shuffle. example: true -channels_seek_forward: - description: Seek forward by a set number of seconds. - fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: 'media_player.family_room_channels' - -channels_seek_backward: - description: Seek backward by a set number of seconds. - fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: 'media_player.family_room_channels' - -channels_seek_by: - description: Seek by an inputted number of seconds. - fields: - entity_id: - description: Name of entity for the instance of Channels to seek in. - example: 'media_player.family_room_channels' - seconds: - description: Number of seconds to seek by. Negative numbers seek backwards. - example: 120 - soundtouch_play_everywhere: description: Play on all Bose Soundtouch devices. fields: From 004476a1f8fb4dd7292c67174fdf964eb0617635 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Nov 2019 02:25:43 -0800 Subject: [PATCH 1840/3953] Add intent integration to expose intent handle API (#29124) * Add intent integration to expose intent handle API. * Run hassfest + fix scaffolding * Update __init__.py --- CODEOWNERS | 1 + .../components/conversation/__init__.py | 38 ------------- homeassistant/components/intent/__init__.py | 54 +++++++++++++++++++ homeassistant/components/intent/const.py | 3 ++ homeassistant/components/intent/manifest.json | 11 ++++ .../integration/integration/manifest.json | 3 +- tests/components/conversation/test_init.py | 46 ---------------- tests/components/intent/__init__.py | 1 + tests/components/intent/test_init.py | 44 +++++++++++++++ 9 files changed, 116 insertions(+), 85 deletions(-) create mode 100644 homeassistant/components/intent/__init__.py create mode 100644 homeassistant/components/intent/const.py create mode 100644 homeassistant/components/intent/manifest.json create mode 100644 tests/components/intent/__init__.py create mode 100644 tests/components/intent/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index f35c492615d66f..74916740253b13 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -156,6 +156,7 @@ homeassistant/components/input_number/* @home-assistant/core homeassistant/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core homeassistant/components/integration/* @dgomes +homeassistant/components/intent/* @home-assistant/core homeassistant/components/ios/* @robbiet480 homeassistant/components/ipma/* @dgomes homeassistant/components/iqvia/* @bachya diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index ba8b211e65a1df..ec5868e86feab8 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -68,7 +68,6 @@ async def handle_service(service): DOMAIN, SERVICE_PROCESS, handle_service, schema=SERVICE_PROCESS_SCHEMA ) hass.http.register_view(ConversationProcessView()) - hass.http.register_view(ConversationHandleView()) hass.components.websocket_api.async_register_command(websocket_process) hass.components.websocket_api.async_register_command(websocket_get_agent_info) hass.components.websocket_api.async_register_command(websocket_set_onboarding) @@ -139,43 +138,6 @@ async def post(self, request, data): return self.json(intent_result) -class ConversationHandleView(http.HomeAssistantView): - """View to handle intents from JSON.""" - - url = "/api/conversation/handle" - name = "api:conversation:handle" - - @RequestDataValidator( - vol.Schema( - { - vol.Required("name"): cv.string, - vol.Optional("data"): vol.Schema({cv.string: object}), - } - ) - ) - async def post(self, request, data): - """Handle intent with name/data.""" - hass = request.app["hass"] - - try: - intent_name = data["name"] - slots = { - key: {"value": value} for key, value in data.get("data", {}).items() - } - intent_result = await intent.async_handle( - hass, DOMAIN, intent_name, slots, "", self.context(request) - ) - except intent.IntentHandleError as err: - intent_result = intent.IntentResponse() - intent_result.async_set_speech(str(err)) - - if intent_result is None: - intent_result = intent.IntentResponse() - intent_result.async_set_speech("Sorry, I couldn't handle that") - - return self.json(intent_result) - - async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: """Get the active conversation agent.""" agent = hass.data.get(DATA_AGENT) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py new file mode 100644 index 00000000000000..758b77ba108d27 --- /dev/null +++ b/homeassistant/components/intent/__init__.py @@ -0,0 +1,54 @@ +"""The Intent integration.""" +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.components import http +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.helpers import config_validation as cv, intent + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Intent component.""" + hass.http.register_view(IntentHandleView()) + return True + + +class IntentHandleView(http.HomeAssistantView): + """View to handle intents from JSON.""" + + url = "/api/intent/handle" + name = "api:intent:handle" + + @RequestDataValidator( + vol.Schema( + { + vol.Required("name"): cv.string, + vol.Optional("data"): vol.Schema({cv.string: object}), + } + ) + ) + async def post(self, request, data): + """Handle intent with name/data.""" + hass = request.app["hass"] + + try: + intent_name = data["name"] + slots = { + key: {"value": value} for key, value in data.get("data", {}).items() + } + intent_result = await intent.async_handle( + hass, DOMAIN, intent_name, slots, "", self.context(request) + ) + except intent.IntentHandleError as err: + intent_result = intent.IntentResponse() + intent_result.async_set_speech(str(err)) + + if intent_result is None: + intent_result = intent.IntentResponse() + intent_result.async_set_speech("Sorry, I couldn't handle that") + + return self.json(intent_result) diff --git a/homeassistant/components/intent/const.py b/homeassistant/components/intent/const.py new file mode 100644 index 00000000000000..61b97c205378cd --- /dev/null +++ b/homeassistant/components/intent/const.py @@ -0,0 +1,3 @@ +"""Constants for the Intent integration.""" + +DOMAIN = "intent" diff --git a/homeassistant/components/intent/manifest.json b/homeassistant/components/intent/manifest.json new file mode 100644 index 00000000000000..005abde47d68e5 --- /dev/null +++ b/homeassistant/components/intent/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "intent", + "name": "Intent", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/intent", + "requirements": [], + "ssdp": [], + "homekit": {}, + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"] +} diff --git a/script/scaffold/templates/integration/integration/manifest.json b/script/scaffold/templates/integration/integration/manifest.json index 0bc54519ce9de8..a95991abef8f71 100644 --- a/script/scaffold/templates/integration/integration/manifest.json +++ b/script/scaffold/templates/integration/integration/manifest.json @@ -4,7 +4,8 @@ "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/NEW_DOMAIN", "requirements": [], - "ssdp": {}, + "ssdp": [], + "zeroconf": [], "homekit": {}, "dependencies": [], "codeowners": [] diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 3982ed6f69960a..6d318deacdd72d 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -129,52 +129,6 @@ async def async_handle(self, intent): } -async def test_http_handle_intent(hass, hass_client, hass_admin_user): - """Test handle intent via HTTP API.""" - - class TestIntentHandler(intent.IntentHandler): - """Test Intent Handler.""" - - intent_type = "OrderBeer" - - async def async_handle(self, intent): - """Handle the intent.""" - assert intent.context.user_id == hass_admin_user.id - response = intent.create_response() - response.async_set_speech( - "I've ordered a {}!".format(intent.slots["type"]["value"]) - ) - response.async_set_card( - "Beer ordered", "You chose a {}.".format(intent.slots["type"]["value"]) - ) - return response - - intent.async_register(hass, TestIntentHandler()) - - result = await async_setup_component( - hass, - "conversation", - {"conversation": {"intents": {"OrderBeer": ["I would like the {type} beer"]}}}, - ) - assert result - - client = await hass_client() - resp = await client.post( - "/api/conversation/handle", - json={"name": "OrderBeer", "data": {"type": "Belgian"}}, - ) - - assert resp.status == 200 - data = await resp.json() - - assert data == { - "card": { - "simple": {"content": "You chose a Belgian.", "title": "Beer ordered"} - }, - "speech": {"plain": {"extra_data": None, "speech": "I've ordered a Belgian!"}}, - } - - @pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on")) async def test_turn_on_intent(hass, sentence): """Test calling the turn on intent.""" diff --git a/tests/components/intent/__init__.py b/tests/components/intent/__init__.py new file mode 100644 index 00000000000000..463f53d921c176 --- /dev/null +++ b/tests/components/intent/__init__.py @@ -0,0 +1 @@ +"""Tests for the Intent integration.""" diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py new file mode 100644 index 00000000000000..e0e5f44873d665 --- /dev/null +++ b/tests/components/intent/test_init.py @@ -0,0 +1,44 @@ +"""Tests for Intent component.""" +from homeassistant.setup import async_setup_component +from homeassistant.helpers import intent + + +async def test_http_handle_intent(hass, hass_client, hass_admin_user): + """Test handle intent via HTTP API.""" + + class TestIntentHandler(intent.IntentHandler): + """Test Intent Handler.""" + + intent_type = "OrderBeer" + + async def async_handle(self, intent): + """Handle the intent.""" + assert intent.context.user_id == hass_admin_user.id + response = intent.create_response() + response.async_set_speech( + "I've ordered a {}!".format(intent.slots["type"]["value"]) + ) + response.async_set_card( + "Beer ordered", "You chose a {}.".format(intent.slots["type"]["value"]) + ) + return response + + intent.async_register(hass, TestIntentHandler()) + + result = await async_setup_component(hass, "intent", {}) + assert result + + client = await hass_client() + resp = await client.post( + "/api/intent/handle", json={"name": "OrderBeer", "data": {"type": "Belgian"}} + ) + + assert resp.status == 200 + data = await resp.json() + + assert data == { + "card": { + "simple": {"content": "You chose a Belgian.", "title": "Beer ordered"} + }, + "speech": {"plain": {"extra_data": None, "speech": "I've ordered a Belgian!"}}, + } From 1681d36637e130fe71569e014d11c266d29a73b3 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:26:59 -0500 Subject: [PATCH 1841/3953] Update service domain for todoist from 'calendar' to 'todoist' (#29131) * move todoist constants to const.py and update service domain * update .coveragerc * remove unused variable * save file --- .coveragerc | 1 + .../components/calendar/services.yaml | 24 ---- homeassistant/components/todoist/calendar.py | 119 ++++++------------ homeassistant/components/todoist/const.py | 79 ++++++++++++ 4 files changed, 115 insertions(+), 108 deletions(-) create mode 100644 homeassistant/components/todoist/const.py diff --git a/.coveragerc b/.coveragerc index 73b203a181ad8d..5e8f5e04850d2f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -692,6 +692,7 @@ omit = homeassistant/components/tile/device_tracker.py homeassistant/components/time_date/sensor.py homeassistant/components/todoist/calendar.py + homeassistant/components/todoist/const.py homeassistant/components/tof/sensor.py homeassistant/components/tomato/device_tracker.py homeassistant/components/toon/* diff --git a/homeassistant/components/calendar/services.yaml b/homeassistant/components/calendar/services.yaml index ebf0c7b1591ab0..d8a0575bcedd26 100644 --- a/homeassistant/components/calendar/services.yaml +++ b/homeassistant/components/calendar/services.yaml @@ -1,26 +1,2 @@ # Describes the format for available calendar services -todoist_new_task: - description: Create a new task and add it to a project. - fields: - content: - description: The name of the task. - example: Pick up the mail - project: - description: The name of the project this task should belong to. Defaults to Inbox. - example: Errands - labels: - description: Any labels that you want to apply to this task, separated by a comma. - example: Chores,Deliveries - priority: - description: The priority of this task, from 1 (normal) to 4 (urgent). - example: 2 - due_date_string: - description: The day this task is due, in natural language. - example: "tomorrow" - due_date_lang: - description: The language of due_date_string. - example: "en" - due_date: - description: The day this task is due, in format YYYY-MM-DD. - example: "2018-04-01" diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 2e8b43c749f8ba..eabec37a0539a4 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -5,96 +5,47 @@ from todoist.api import TodoistAPI import voluptuous as vol -from homeassistant.components.calendar import ( - DOMAIN, - PLATFORM_SCHEMA, - CalendarEventDevice, -) +from homeassistant.components.calendar import PLATFORM_SCHEMA, CalendarEventDevice from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util import Throttle, dt -_LOGGER = logging.getLogger(__name__) +from .const import ( + ALL_DAY, + ALL_TASKS, + CHECKED, + COMPLETED, + CONF_PROJECT_DUE_DATE, + CONF_EXTRA_PROJECTS, + CONF_PROJECT_LABEL_WHITELIST, + CONF_PROJECT_WHITELIST, + CONTENT, + DATETIME, + DESCRIPTION, + DOMAIN, + DUE, + DUE_DATE, + DUE_DATE_LANG, + DUE_DATE_STRING, + DUE_DATE_VALID_LANGS, + DUE_TODAY, + END, + ID, + LABELS, + NAME, + OVERDUE, + PRIORITY, + PROJECT_ID, + PROJECT_NAME, + PROJECTS, + SERVICE_NEW_TASK, + START, + SUMMARY, + TASKS, +) -CONF_EXTRA_PROJECTS = "custom_projects" -CONF_PROJECT_DUE_DATE = "due_date_days" -CONF_PROJECT_LABEL_WHITELIST = "labels" -CONF_PROJECT_WHITELIST = "include_projects" - -# Calendar Platform: Does this calendar event last all day? -ALL_DAY = "all_day" -# Attribute: All tasks in this project -ALL_TASKS = "all_tasks" -# Todoist API: "Completed" flag -- 1 if complete, else 0 -CHECKED = "checked" -# Attribute: Is this task complete? -COMPLETED = "completed" -# Todoist API: What is this task about? -# Service Call: What is this task about? -CONTENT = "content" -# Calendar Platform: Get a calendar event's description -DESCRIPTION = "description" -# Calendar Platform: Used in the '_get_date()' method -DATETIME = "dateTime" -DUE = "due" -# Service Call: When is this task due (in natural language)? -DUE_DATE_STRING = "due_date_string" -# Service Call: The language of DUE_DATE_STRING -DUE_DATE_LANG = "due_date_lang" -# Service Call: The available options of DUE_DATE_LANG -DUE_DATE_VALID_LANGS = [ - "en", - "da", - "pl", - "zh", - "ko", - "de", - "pt", - "ja", - "it", - "fr", - "sv", - "ru", - "es", - "nl", -] -# Attribute: When is this task due? -# Service Call: When is this task due? -DUE_DATE = "due_date" -# Todoist API: Look up a task's due date -DUE_DATE_UTC = "due_date_utc" -# Attribute: Is this task due today? -DUE_TODAY = "due_today" -# Calendar Platform: When a calendar event ends -END = "end" -# Todoist API: Look up a Project/Label/Task ID -ID = "id" -# Todoist API: Fetch all labels -# Service Call: What are the labels attached to this task? -LABELS = "labels" -# Todoist API: "Name" value -NAME = "name" -# Attribute: Is this task overdue? -OVERDUE = "overdue" -# Attribute: What is this task's priority? -# Todoist API: Get a task's priority -# Service Call: What is this task's priority? -PRIORITY = "priority" -# Todoist API: Look up the Project ID a Task belongs to -PROJECT_ID = "project_id" -# Service Call: What Project do you want a Task added to? -PROJECT_NAME = "project" -# Todoist API: Fetch all Projects -PROJECTS = "projects" -# Calendar Platform: When does a calendar event start? -START = "start" -# Calendar Platform: What is the next calendar event about? -SUMMARY = "summary" -# Todoist API: Fetch all Tasks -TASKS = "items" - -SERVICE_NEW_TASK = "todoist_new_task" +_LOGGER = logging.getLogger(__name__) NEW_TASK_SERVICE_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/todoist/const.py b/homeassistant/components/todoist/const.py new file mode 100644 index 00000000000000..a1e37bf0292d96 --- /dev/null +++ b/homeassistant/components/todoist/const.py @@ -0,0 +1,79 @@ +"""Constants for the Todoist component.""" +CONF_EXTRA_PROJECTS = "custom_projects" +CONF_PROJECT_DUE_DATE = "due_date_days" +CONF_PROJECT_LABEL_WHITELIST = "labels" +CONF_PROJECT_WHITELIST = "include_projects" + +# Calendar Platform: Does this calendar event last all day? +ALL_DAY = "all_day" +# Attribute: All tasks in this project +ALL_TASKS = "all_tasks" +# Todoist API: "Completed" flag -- 1 if complete, else 0 +CHECKED = "checked" +# Attribute: Is this task complete? +COMPLETED = "completed" +# Todoist API: What is this task about? +# Service Call: What is this task about? +CONTENT = "content" +# Calendar Platform: Get a calendar event's description +DESCRIPTION = "description" +# Calendar Platform: Used in the '_get_date()' method +DATETIME = "dateTime" +DUE = "due" +# Service Call: When is this task due (in natural language)? +DUE_DATE_STRING = "due_date_string" +# Service Call: The language of DUE_DATE_STRING +DUE_DATE_LANG = "due_date_lang" +# Service Call: The available options of DUE_DATE_LANG +DUE_DATE_VALID_LANGS = [ + "en", + "da", + "pl", + "zh", + "ko", + "de", + "pt", + "ja", + "it", + "fr", + "sv", + "ru", + "es", + "nl", +] +# Attribute: When is this task due? +# Service Call: When is this task due? +DUE_DATE = "due_date" +# Attribute: Is this task due today? +DUE_TODAY = "due_today" +# Calendar Platform: When a calendar event ends +END = "end" +# Todoist API: Look up a Project/Label/Task ID +ID = "id" +# Todoist API: Fetch all labels +# Service Call: What are the labels attached to this task? +LABELS = "labels" +# Todoist API: "Name" value +NAME = "name" +# Attribute: Is this task overdue? +OVERDUE = "overdue" +# Attribute: What is this task's priority? +# Todoist API: Get a task's priority +# Service Call: What is this task's priority? +PRIORITY = "priority" +# Todoist API: Look up the Project ID a Task belongs to +PROJECT_ID = "project_id" +# Service Call: What Project do you want a Task added to? +PROJECT_NAME = "project" +# Todoist API: Fetch all Projects +PROJECTS = "projects" +# Calendar Platform: When does a calendar event start? +START = "start" +# Calendar Platform: What is the next calendar event about? +SUMMARY = "summary" +# Todoist API: Fetch all Tasks +TASKS = "items" + +DOMAIN = "todoist" + +SERVICE_NEW_TASK = "new_task" From 2d2ab452ca9c5fba5355bd0e641743a4b3c5ab5c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 05:27:48 -0500 Subject: [PATCH 1842/3953] update envisalink service domain (#29126) --- .../components/alarm_control_panel/services.yaml | 10 ---------- .../components/envisalink/alarm_control_panel.py | 14 +++++++++----- homeassistant/components/envisalink/services.yaml | 10 ++++++++++ 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 917600862e3657..9cd964141c36f2 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -60,16 +60,6 @@ alarm_trigger: description: An optional code to trigger the alarm control panel with. example: 1234 -envisalink_alarm_keypress: - description: Send custom keypresses to the alarm. - fields: - entity_id: - description: Name of the alarm control panel to trigger. - example: 'alarm_control_panel.downstairs' - keypress: - description: 'String to send to the alarm panel (1-6 characters).' - example: '*71' - alarmdecoder_alarm_toggle_chime: description: Send the alarm the toggle chime command. fields: diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 19703297ccd6b0..00f39bece9fa3f 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -3,7 +3,10 @@ import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanel, + FORMAT_NUMBER, +) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -29,6 +32,7 @@ CONF_PANIC, CONF_PARTITIONNAME, DATA_EVL, + DOMAIN, PARTITION_SCHEMA, SIGNAL_KEYPAD_UPDATE, SIGNAL_PARTITION_UPDATE, @@ -37,7 +41,7 @@ _LOGGER = logging.getLogger(__name__) -SERVICE_ALARM_KEYPRESS = "envisalink_alarm_keypress" +SERVICE_ALARM_KEYPRESS = "alarm_keypress" ATTR_KEYPRESS = "keypress" ALARM_KEYPRESS_SCHEMA = vol.Schema( { @@ -83,7 +87,7 @@ def alarm_keypress_handler(service): device.async_alarm_keypress(keypress) hass.services.async_register( - alarm.DOMAIN, + DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, schema=ALARM_KEYPRESS_SCHEMA, @@ -92,7 +96,7 @@ def alarm_keypress_handler(service): return True -class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): +class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanel): """Representation of an Envisalink-based alarm panel.""" def __init__( @@ -124,7 +128,7 @@ def code_format(self): """Regex for code format or None if no code is required.""" if self._code: return None - return alarm.FORMAT_NUMBER + return FORMAT_NUMBER @property def state(self): diff --git a/homeassistant/components/envisalink/services.yaml b/homeassistant/components/envisalink/services.yaml index e31aa804059df2..2a5f91791df46f 100644 --- a/homeassistant/components/envisalink/services.yaml +++ b/homeassistant/components/envisalink/services.yaml @@ -1,5 +1,15 @@ # Describes the format for available Envisalink services. +alarm_keypress: + description: Send custom keypresses to the alarm. + fields: + entity_id: + description: Name of the alarm control panel to trigger. + example: 'alarm_control_panel.downstairs' + keypress: + description: 'String to send to the alarm panel (1-6 characters).' + example: '*71' + invoke_custom_function: description: > Allows users with DSC panels to trigger a PGM output (1-4). From c80f284ca483f065cac9b280c75b56cf540d9785 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 07:31:40 -0500 Subject: [PATCH 1843/3953] =?UTF-8?q?Update=20service=20domain=20for=20mys?= =?UTF-8?q?ensors=20from=20'switch'=20to=20'mysensor=E2=80=A6=20(#29147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/mysensors/const.py | 2 ++ homeassistant/components/mysensors/services.yaml | 9 +++++++++ homeassistant/components/mysensors/switch.py | 5 +++-- homeassistant/components/switch/services.yaml | 10 ---------- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 45f603a2cb44f8..ccb646eb47eec6 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -25,6 +25,8 @@ TYPE = "type" UPDATE_DELAY = 0.1 +SERVICE_SEND_IR_CODE = "send_ir_code" + BINARY_SENSOR_TYPES = { "S_DOOR": {"V_TRIPPED"}, "S_MOTION": {"V_TRIPPED"}, diff --git a/homeassistant/components/mysensors/services.yaml b/homeassistant/components/mysensors/services.yaml index e69de29bb2d1d6..74a5ff0f183ffe 100644 --- a/homeassistant/components/mysensors/services.yaml +++ b/homeassistant/components/mysensors/services.yaml @@ -0,0 +1,9 @@ +send_ir_code: + description: Set an IR code as a state attribute for a MySensors IR device switch and turn the switch on. + fields: + entity_id: + description: Name(s) of entities that should have the IR code set and be turned on. Platform dependent. + example: 'switch.living_room_1_1' + V_IR_SEND: + description: IR code to send. + example: '0xC284' \ No newline at end of file diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index c624aaafa343f4..fecec53370bcba 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -6,8 +6,9 @@ from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from .const import DOMAIN as MYSENSORS_DOMAIN, SERVICE_SEND_IR_CODE + ATTR_IR_CODE = "V_IR_SEND" -SERVICE_SEND_IR_CODE = "mysensors_send_ir_code" SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema( {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_IR_CODE): cv.string} @@ -64,7 +65,7 @@ async def async_send_ir_code_service(service): await device.async_turn_on(**kwargs) hass.services.async_register( - DOMAIN, + MYSENSORS_DOMAIN, SERVICE_SEND_IR_CODE, async_send_ir_code_service, schema=SEND_IR_CODE_SERVICE_SCHEMA, diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index 46b1237f57c240..3a30a0855f6007 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -21,16 +21,6 @@ toggle: description: Name(s) of entities to toggle. example: 'switch.living_room' -mysensors_send_ir_code: - description: Set an IR code as a state attribute for a MySensors IR device switch and turn the switch on. - fields: - entity_id: - description: Name(s) of entities that should have the IR code set and be turned on. Platform dependent. - example: 'switch.living_room_1_1' - V_IR_SEND: - description: IR code to send. - example: '0xC284' - xiaomi_miio_set_wifi_led_on: description: Turn the wifi led on. fields: From 8f05388bc51477ac972c9fb96063d6d60f4f0606 Mon Sep 17 00:00:00 2001 From: Tom Brien Date: Wed, 27 Nov 2019 12:35:38 +0000 Subject: [PATCH 1844/3953] Remove ios warning for no devices subscribed to ios.notify (#29153) Since 2019.1 has launched there is a perfectly reasonable (in fact the standard) case where ios component is loaded (i.e. actionable notifications) but no devices are registed for the now defunct `ios.notify` service. As such, the log warning relating to this case is no longer relevant. --- homeassistant/components/ios/notify.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index ee74b369629b75..80dbad5336d478 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -48,12 +48,6 @@ def get_service(hass, config, discovery_info=None): hass.config.components.add("notify.ios") if not ios.devices_with_push(hass): - _LOGGER.error( - "The notify.ios platform was loaded but no " - "devices exist! Please check the documentation at " - "https://home-assistant.io/ecosystem/ios/notifications" - "/ for more information" - ) return None return iOSNotificationService() From a08df53dbc86dfb2182d843807f7e98f952205c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Wed, 27 Nov 2019 12:46:13 +0000 Subject: [PATCH 1845/3953] Add "gentle" mode for Xiaomi Roborock vacuum (#29004) * Add "gentle" mode for Xiaomi Roborock vacuum * fix tests --- homeassistant/components/xiaomi_miio/vacuum.py | 2 +- tests/components/xiaomi_miio/test_vacuum.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index b18a54ce97a9b1..0320a2e49060d7 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -58,7 +58,7 @@ SERVICE_STOP_REMOTE_CONTROL = "xiaomi_remote_control_stop" SERVICE_CLEAN_ZONE = "xiaomi_clean_zone" -FAN_SPEEDS = {"Quiet": 38, "Balanced": 60, "Turbo": 77, "Max": 90} +FAN_SPEEDS = {"Quiet": 38, "Balanced": 60, "Turbo": 77, "Max": 90, "Gentle": 105} ATTR_CLEAN_START = "clean_start" ATTR_CLEAN_STOP = "clean_stop" diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 18da270960c045..040e04b3c832d2 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -212,6 +212,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): "Balanced", "Turbo", "Max", + "Gentle", ] assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 12 assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 12 @@ -354,6 +355,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): "Balanced", "Turbo", "Max", + "Gentle", ] assert state.attributes.get(ATTR_MAIN_BRUSH_LEFT) == 11 assert state.attributes.get(ATTR_SIDE_BRUSH_LEFT) == 11 From 59939004bf332db468685fc9188969b1c1c31e1a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 07:47:38 -0500 Subject: [PATCH 1846/3953] Update service domain for mill from 'climate' to 'mill' (#29132) --- .coveragerc | 1 + .../components/climate/services.yaml | 16 --------------- homeassistant/components/mill/climate.py | 20 ++++++++++--------- homeassistant/components/mill/const.py | 10 ++++++++++ homeassistant/components/mill/services.yaml | 15 ++++++++++++++ 5 files changed, 37 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/mill/const.py diff --git a/.coveragerc b/.coveragerc index 5e8f5e04850d2f..81a1efec6181f8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -412,6 +412,7 @@ omit = homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/* homeassistant/components/mill/climate.py + homeassistant/components/mill/const.py homeassistant/components/minio/* homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index f10e1b4bd69d71..59188bb03d1a49 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -72,22 +72,6 @@ set_swing_mode: swing_mode: description: New value of swing mode. -mill_set_room_temperature: - description: Set Mill room temperatures. - fields: - room_name: - description: Name of room to change. - example: 'kitchen' - away_temp: - description: Away temp. - example: 12 - comfort_temp: - description: Comfort temp. - example: 22 - sleep_temp: - description: Sleep temp. - example: 17 - nuheat_resume_program: description: Resume the programmed schedule. fields: diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index 1166bd451c0c0e..b08015fe548047 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -6,7 +6,6 @@ from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - DOMAIN, HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_FAN_MODE, @@ -22,15 +21,18 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -_LOGGER = logging.getLogger(__name__) +from .const import ( + ATTR_AWAY_TEMP, + ATTR_COMFORT_TEMP, + ATTR_ROOM_NAME, + ATTR_SLEEP_TEMP, + DOMAIN, + MAX_TEMP, + MIN_TEMP, + SERVICE_SET_ROOM_TEMP, +) -ATTR_AWAY_TEMP = "away_temp" -ATTR_COMFORT_TEMP = "comfort_temp" -ATTR_ROOM_NAME = "room_name" -ATTR_SLEEP_TEMP = "sleep_temp" -MAX_TEMP = 35 -MIN_TEMP = 5 -SERVICE_SET_ROOM_TEMP = "mill_set_room_temperature" +_LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE diff --git a/homeassistant/components/mill/const.py b/homeassistant/components/mill/const.py new file mode 100644 index 00000000000000..65c67b72b6e675 --- /dev/null +++ b/homeassistant/components/mill/const.py @@ -0,0 +1,10 @@ +"""Constants for the Mill heater component.""" + +ATTR_AWAY_TEMP = "away_temp" +ATTR_COMFORT_TEMP = "comfort_temp" +ATTR_ROOM_NAME = "room_name" +ATTR_SLEEP_TEMP = "sleep_temp" +MAX_TEMP = 35 +MIN_TEMP = 5 +DOMAIN = "mill" +SERVICE_SET_ROOM_TEMP = "set_room_temperature" diff --git a/homeassistant/components/mill/services.yaml b/homeassistant/components/mill/services.yaml index e69de29bb2d1d6..e9e59b50170f75 100644 --- a/homeassistant/components/mill/services.yaml +++ b/homeassistant/components/mill/services.yaml @@ -0,0 +1,15 @@ +set_room_temperature: + description: Set Mill room temperatures. + fields: + room_name: + description: Name of room to change. + example: 'kitchen' + away_temp: + description: Away temp. + example: 12 + comfort_temp: + description: Comfort temp. + example: 22 + sleep_temp: + description: Sleep temp. + example: 17 \ No newline at end of file From 12ae8b025fe184632a5308501835be85b10133ef Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 07:54:00 -0500 Subject: [PATCH 1847/3953] move squeezebox service to squeezebox domain (#29141) --- .coveragerc | 2 +- homeassistant/components/media_player/services.yaml | 13 ------------- homeassistant/components/squeezebox/const.py | 3 +++ homeassistant/components/squeezebox/media_player.py | 5 ++--- homeassistant/components/squeezebox/services.yaml | 3 +-- 5 files changed, 7 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/squeezebox/const.py diff --git a/.coveragerc b/.coveragerc index 81a1efec6181f8..a1e7e068ed59c7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -646,7 +646,7 @@ omit = homeassistant/components/spider/* homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/media_player.py - homeassistant/components/squeezebox/media_player.py + homeassistant/components/squeezebox/* homeassistant/components/starline/* homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index e0ec1661e9fcfd..279fed37522fdf 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -194,19 +194,6 @@ soundtouch_remove_zone_slave: description: Name of slaves entities to remove from the existing zone. example: 'media_player.soundtouch_bedroom' -squeezebox_call_method: - description: 'Call a Squeezebox JSON/RPC API method.' - fields: - entity_id: - description: Name(s) of the Squeexebox entities where to run the API method. - example: 'media_player.squeezebox_radio' - command: - description: Name of the Squeezebox command. - example: 'playlist' - parameters: - description: Optional array of parameters to be appended to the command. See 'Command Line Interface' official help page from Logitech for details. - example: '["loadtracks", "track.titlesearch=highway to hell"]' - yamaha_enable_output: description: Enable or disable an output port fields: diff --git a/homeassistant/components/squeezebox/const.py b/homeassistant/components/squeezebox/const.py new file mode 100644 index 00000000000000..1e8fd6f3a2a873 --- /dev/null +++ b/homeassistant/components/squeezebox/const.py @@ -0,0 +1,3 @@ +"""Constants for the Squeezebox component.""" +DOMAIN = "squeezebox" +SERVICE_CALL_METHOD = "call_method" diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 8e03763b709b74..b3fb82591c9fe3 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -12,7 +12,6 @@ from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, - DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, @@ -44,6 +43,8 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.util.dt import utcnow +from .const import DOMAIN, SERVICE_CALL_METHOD + _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 9000 @@ -75,8 +76,6 @@ } ) -SERVICE_CALL_METHOD = "squeezebox_call_method" - DATA_SQUEEZEBOX = "squeezebox" KNOWN_SERVERS = "squeezebox_known_servers" diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml index 05c7de07f42b56..0c81c369e73171 100644 --- a/homeassistant/components/squeezebox/services.yaml +++ b/homeassistant/components/squeezebox/services.yaml @@ -1,4 +1,4 @@ -squeezebox_call_method: +call_method: description: Call a custom Squeezebox JSONRPC API. fields: entity_id: @@ -10,4 +10,3 @@ squeezebox_call_method: parameters: description: Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). example: ["loadtracks", "album.titlesearch="] - From fdf0793fddf9a1974c3ba45eb77f4d0d293d424e Mon Sep 17 00:00:00 2001 From: mvn23 Date: Wed, 27 Nov 2019 14:14:41 +0100 Subject: [PATCH 1848/3953] Add opentherm_gw device support (#28722) * Add opentherm_gw device support * Add support for enabling/disabling of opentherm_gw entities * Disable sensors by default, base climate entity_id on gw_id instead of friendly_name to guarantee uniqueness * Remove platform name from unique_id --- .../components/opentherm_gw/__init__.py | 30 +++++++++++++---- .../components/opentherm_gw/binary_sensor.py | 32 ++++++++++++++++++- .../components/opentherm_gw/climate.py | 30 +++++++++++++++-- .../components/opentherm_gw/sensor.py | 30 ++++++++++++++++- 4 files changed, 110 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 643f80ae8f9db5..3a1255e3697866 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,4 +1,5 @@ """Support for OpenTherm Gateway devices.""" +import asyncio import logging from datetime import datetime, date @@ -344,6 +345,18 @@ async def set_setback_temp(call): ) +async def async_unload_entry(hass, entry): + """Cleanup and disconnect from gateway.""" + await asyncio.gather( + hass.config_entries.async_forward_entry_unload(entry, COMP_BINARY_SENSOR), + hass.config_entries.async_forward_entry_unload(entry, COMP_CLIMATE), + hass.config_entries.async_forward_entry_unload(entry, COMP_SENSOR), + ) + gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]] + await gateway.cleanup() + return True + + class OpenThermGatewayDevice: """OpenTherm Gateway device class.""" @@ -358,18 +371,21 @@ def __init__(self, hass, config_entry): self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update" self.gateway = pyotgw.pyotgw() + self.gw_version = None + + async def cleanup(self, event=None): + """Reset overrides on the gateway.""" + await self.gateway.set_control_setpoint(0) + await self.gateway.set_max_relative_mod("-") + await self.gateway.disconnect() async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" - await self.gateway.connect(self.hass.loop, self.device_path) + self.status = await self.gateway.connect(self.hass.loop, self.device_path) _LOGGER.debug("Connected to OpenTherm Gateway at %s", self.device_path) + self.gw_version = self.status.get(gw_vars.OTGW_BUILD) - async def cleanup(event): - """Reset overrides on the gateway.""" - await self.gateway.set_control_setpoint(0) - await self.gateway.set_max_relative_mod("-") - - self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, cleanup) + self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.cleanup) async def handle_report(status): """Handle reports from the OpenTherm Gateway.""" diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 36867feda61e52..39fd78f5fe8f23 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id +from . import DOMAIN from .const import BINARY_SENSOR_INFO, DATA_GATEWAYS, DATA_OPENTHERM_GW @@ -44,14 +45,27 @@ def __init__(self, gw_dev, var, device_class, friendly_name_format): self._state = None self._device_class = device_class self._friendly_name = friendly_name_format.format(gw_dev.name) + self._unsub_updates = None async def async_added_to_hass(self): """Subscribe to updates from the component.""" _LOGGER.debug("Added OpenTherm Gateway binary sensor %s", self._friendly_name) - async_dispatcher_connect( + self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) + async def async_will_remove_from_hass(self): + """Unsubscribe from updates from the component.""" + _LOGGER.debug( + "Removing OpenTherm Gateway binary sensor %s", self._friendly_name + ) + self._unsub_updates() + + @property + def entity_registry_enabled_default(self): + """Disable binary_sensors by default.""" + return False + @callback def receive_report(self, status): """Handle status updates from the component.""" @@ -63,6 +77,22 @@ def name(self): """Return the friendly name.""" return self._friendly_name + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._gateway.gw_id)}, + "name": self._gateway.name, + "manufacturer": "Schelte Bron", + "model": "OpenTherm Gateway", + "sw_version": self._gateway.gw_version, + } + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._gateway.gw_id}-{self._var}" + @property def is_on(self): """Return true if the binary sensor is on.""" diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 28a0ae98270e30..8c21c6560c1497 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -3,7 +3,7 @@ from pyotgw import vars as gw_vars -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, @@ -25,7 +25,9 @@ ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import async_generate_entity_id +from . import DOMAIN from .const import CONF_FLOOR_TEMP, CONF_PRECISION, DATA_GATEWAYS, DATA_OPENTHERM_GW @@ -56,6 +58,9 @@ class OpenThermClimate(ClimateDevice): def __init__(self, gw_dev, options): """Initialize the device.""" self._gateway = gw_dev + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, gw_dev.gw_id, hass=gw_dev.hass + ) self.friendly_name = gw_dev.name self.floor_temp = options.get(CONF_FLOOR_TEMP, DEFAULT_FLOOR_TEMP) self.temp_precision = options.get(CONF_PRECISION, DEFAULT_PRECISION) @@ -68,6 +73,8 @@ def __init__(self, gw_dev, options): self._away_mode_b = None self._away_state_a = False self._away_state_b = False + self._unsub_options = None + self._unsub_updates = None @callback def update_options(self, entry): @@ -79,13 +86,19 @@ def update_options(self, entry): async def async_added_to_hass(self): """Connect to the OpenTherm Gateway device.""" _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name) - async_dispatcher_connect( + self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) - async_dispatcher_connect( + self._unsub_options = async_dispatcher_connect( self.hass, self._gateway.options_update_signal, self.update_options ) + async def async_will_remove_from_hass(self): + """Unsubscribe from updates from the component.""" + _LOGGER.debug("Removing OpenTherm Gateway climate %s", self.friendly_name) + self._unsub_options() + self._unsub_updates() + @callback def receive_report(self, status): """Receive and handle a new report from the Gateway.""" @@ -139,6 +152,17 @@ def name(self): """Return the friendly name.""" return self.friendly_name + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._gateway.gw_id)}, + "name": self._gateway.name, + "manufacturer": "Schelte Bron", + "model": "OpenTherm Gateway", + "sw_version": self._gateway.gw_version, + } + @property def unique_id(self): """Return a unique ID.""" diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index c77a73cd18032b..cd9ce9fb095a35 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -7,6 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, async_generate_entity_id +from . import DOMAIN from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW, SENSOR_INFO @@ -47,14 +48,25 @@ def __init__(self, gw_dev, var, device_class, unit, friendly_name_format): self._device_class = device_class self._unit = unit self._friendly_name = friendly_name_format.format(gw_dev.name) + self._unsub_updates = None async def async_added_to_hass(self): """Subscribe to updates from the component.""" _LOGGER.debug("Added OpenTherm Gateway sensor %s", self._friendly_name) - async_dispatcher_connect( + self._unsub_updates = async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) + async def async_will_remove_from_hass(self): + """Unsubscribe from updates from the component.""" + _LOGGER.debug("Removing OpenTherm Gateway sensor %s", self._friendly_name) + self._unsub_updates() + + @property + def entity_registry_enabled_default(self): + """Disable sensors by default.""" + return False + @callback def receive_report(self, status): """Handle status updates from the component.""" @@ -69,6 +81,22 @@ def name(self): """Return the friendly name of the sensor.""" return self._friendly_name + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._gateway.gw_id)}, + "name": self._gateway.name, + "manufacturer": "Schelte Bron", + "model": "OpenTherm Gateway", + "sw_version": self._gateway.gw_version, + } + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._gateway.gw_id}-{self._var}" + @property def device_class(self): """Return the device class.""" From ef21fd2536d56213c13876ac648ded7098785e39 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 09:20:40 -0500 Subject: [PATCH 1849/3953] Update services.yaml for nuheat (#29133) * update services.yaml for nuheat * update tests * black formatting --- homeassistant/components/climate/services.yaml | 13 ++++++------- homeassistant/components/nuheat/climate.py | 6 +++--- homeassistant/components/nuheat/services.yaml | 6 ++++++ tests/components/nuheat/test_climate.py | 10 ++++------ 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 59188bb03d1a49..34e89d57346899 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -9,6 +9,7 @@ set_aux_heat: aux_heat: description: New value of axillary heater. example: true + set_preset_mode: description: Set preset mode for climate device. fields: @@ -18,6 +19,7 @@ set_preset_mode: preset_mode: description: New value of preset mode example: 'away' + set_temperature: description: Set target temperature of climate device. fields: @@ -36,6 +38,7 @@ set_temperature: hvac_mode: description: HVAC operation mode to set temperature to. example: 'heat' + set_humidity: description: Set target humidity of climate device. fields: @@ -45,6 +48,7 @@ set_humidity: humidity: description: New target humidity for climate device. example: 60 + set_fan_mode: description: Set fan operation for climate device. fields: @@ -54,6 +58,7 @@ set_fan_mode: fan_mode: description: New value of fan mode. example: On Low + set_hvac_mode: description: Set HVAC operation mode for climate device. fields: @@ -63,6 +68,7 @@ set_hvac_mode: hvac_mode: description: New value of operation mode. example: heat + set_swing_mode: description: Set swing operation for climate device. fields: @@ -72,13 +78,6 @@ set_swing_mode: swing_mode: description: New value of swing mode. -nuheat_resume_program: - description: Resume the programmed schedule. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.kitchen' - turn_on: description: Turn climate device on. fields: diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 5a4e4e233d1684..5cf9bd6fc58007 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -from . import DOMAIN as NUHEAT_DOMAIN +from . import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -52,7 +52,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return temperature_unit = hass.config.units.temperature_unit - api, serial_numbers = hass.data[NUHEAT_DOMAIN] + api, serial_numbers = hass.data[DOMAIN] thermostats = [ NuHeatThermostat(api, serial_number, temperature_unit) for serial_number in serial_numbers @@ -75,7 +75,7 @@ def resume_program_set_service(service): thermostat.schedule_update_ha_state(True) hass.services.register( - NUHEAT_DOMAIN, + DOMAIN, SERVICE_RESUME_PROGRAM, resume_program_set_service, schema=RESUME_PROGRAM_SCHEMA, diff --git a/homeassistant/components/nuheat/services.yaml b/homeassistant/components/nuheat/services.yaml index e69de29bb2d1d6..6639fcd98986e0 100644 --- a/homeassistant/components/nuheat/services.yaml +++ b/homeassistant/components/nuheat/services.yaml @@ -0,0 +1,6 @@ +resume_program: + description: Resume the programmed schedule. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index 1ab791dfd374cf..c35497968acee2 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -67,7 +67,7 @@ def test_setup_platform(self, mocked_thermostat): thermostat = mocked_thermostat(self.api, "12345", "F") thermostats = [thermostat] - self.hass.data[nuheat.NUHEAT_DOMAIN] = (self.api, ["12345"]) + self.hass.data[nuheat.DOMAIN] = (self.api, ["12345"]) config = {} add_entities = Mock() @@ -85,12 +85,12 @@ def test_resume_program_service(self, mocked_thermostat): thermostat.schedule_update_ha_state = Mock() thermostat.entity_id = "climate.master_bathroom" - self.hass.data[nuheat.NUHEAT_DOMAIN] = (self.api, ["12345"]) + self.hass.data[nuheat.DOMAIN] = (self.api, ["12345"]) nuheat.setup_platform(self.hass, {}, Mock(), {}) # Explicit entity self.hass.services.call( - nuheat.NUHEAT_DOMAIN, + nuheat.DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {"entity_id": "climate.master_bathroom"}, True, @@ -103,9 +103,7 @@ def test_resume_program_service(self, mocked_thermostat): thermostat.schedule_update_ha_state.reset_mock() # All entities - self.hass.services.call( - nuheat.NUHEAT_DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {}, True - ) + self.hass.services.call(nuheat.DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {}, True) thermostat.resume_program.assert_called_with() thermostat.schedule_update_ha_state.assert_called_with(True) From d5778681900fbfac85d9edbbdb6f2718c466ed81 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:13:54 -0500 Subject: [PATCH 1850/3953] move yamaha service to yamaha domain (#29142) --- .coveragerc | 1 - .../components/media_player/services.yaml | 50 ------------------- homeassistant/components/yamaha/const.py | 3 ++ .../components/yamaha/media_player.py | 5 +- homeassistant/components/yamaha/services.yaml | 12 +++++ 5 files changed, 17 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/yamaha/const.py diff --git a/.coveragerc b/.coveragerc index a1e7e068ed59c7..91b856dc6d80d7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -790,7 +790,6 @@ omit = homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* homeassistant/components/yale_smart_alarm/alarm_control_panel.py - homeassistant/components/yamaha/media_player.py homeassistant/components/yamaha_musiccast/media_player.py homeassistant/components/yandex_transport/* homeassistant/components/yeelight/* diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 279fed37522fdf..ad6b8a78957bd4 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -156,53 +156,3 @@ shuffle_set: shuffle: description: True/false for enabling/disabling shuffle. example: true - -soundtouch_play_everywhere: - description: Play on all Bose Soundtouch devices. - fields: - master: - description: Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices - example: 'media_player.soundtouch_home' - -soundtouch_create_zone: - description: Create a Sountouch multi-room zone. - fields: - master: - description: Name of the master entity that will coordinate the multi-room zone. Platform dependent. - example: 'media_player.soundtouch_home' - slaves: - description: Name of slaves entities to add to the new zone. - example: 'media_player.soundtouch_bedroom' - -soundtouch_add_zone_slave: - description: Add a slave to a Sountouch multi-room zone. - fields: - master: - description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. - example: 'media_player.soundtouch_home' - slaves: - description: Name of slaves entities to add to the existing zone. - example: 'media_player.soundtouch_bedroom' - -soundtouch_remove_zone_slave: - description: Remove a slave from the Sounttouch multi-room zone. - fields: - master: - description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. - example: 'media_player.soundtouch_home' - slaves: - description: Name of slaves entities to remove from the existing zone. - example: 'media_player.soundtouch_bedroom' - -yamaha_enable_output: - description: Enable or disable an output port - fields: - entity_id: - description: Name(s) of entites to enable/disable port on. - example: 'media_player.yamaha' - port: - description: Name of port to enable/disable. - example: 'hdmi1' - enabled: - description: Boolean indicating if port should be enabled or not. - example: true diff --git a/homeassistant/components/yamaha/const.py b/homeassistant/components/yamaha/const.py new file mode 100644 index 00000000000000..e2a0c5eceeaf60 --- /dev/null +++ b/homeassistant/components/yamaha/const.py @@ -0,0 +1,3 @@ +"""Constants for the Yamaha component.""" +DOMAIN = "yamaha" +SERVICE_ENABLE_OUTPUT = "enable_output" diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index eabb1ef34f1360..fa2c68dce88b49 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -7,7 +7,6 @@ from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( - DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -34,6 +33,8 @@ ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_ENABLE_OUTPUT + _LOGGER = logging.getLogger(__name__) ATTR_ENABLED = "enabled" @@ -53,8 +54,6 @@ {vol.Required(ATTR_ENABLED): cv.boolean, vol.Required(ATTR_PORT): cv.string} ) -SERVICE_ENABLE_OUTPUT = "yamaha_enable_output" - SUPPORT_YAMAHA = ( SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml index e69de29bb2d1d6..592a1d1342e340 100644 --- a/homeassistant/components/yamaha/services.yaml +++ b/homeassistant/components/yamaha/services.yaml @@ -0,0 +1,12 @@ +enable_output: + description: Enable or disable an output port + fields: + entity_id: + description: Name(s) of entites to enable/disable port on. + example: 'media_player.yamaha' + port: + description: Name of port to enable/disable. + example: 'hdmi1' + enabled: + description: Boolean indicating if port should be enabled or not. + example: true \ No newline at end of file From b274fcba051eee36fcd1fed2badf402b6aeeed5d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:14:46 -0500 Subject: [PATCH 1851/3953] Update service domain for harmony from 'remote' to 'harmony' (#29146) * move harmoney services to harmony domain * update service names --- .coveragerc | 2 +- homeassistant/components/harmony/const.py | 4 ++++ homeassistant/components/harmony/remote.py | 6 ++---- homeassistant/components/harmony/services.yaml | 16 ++++++++++++++++ homeassistant/components/remote/services.yaml | 18 ------------------ 5 files changed, 23 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/harmony/const.py diff --git a/.coveragerc b/.coveragerc index 91b856dc6d80d7..f1ba55054b567b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -284,7 +284,7 @@ omit = homeassistant/components/hangouts/hangouts_bot.py homeassistant/components/hangouts/hangups_utils.py homeassistant/components/harman_kardon_avr/media_player.py - homeassistant/components/harmony/remote.py + homeassistant/components/harmony/* homeassistant/components/haveibeenpwned/sensor.py homeassistant/components/hdmi_cec/* homeassistant/components/heatmiser/climate.py diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py new file mode 100644 index 00000000000000..12e710506657e9 --- /dev/null +++ b/homeassistant/components/harmony/const.py @@ -0,0 +1,4 @@ +"""Constants for the Harmony component.""" +DOMAIN = "harmony" +SERVICE_SYNC = "sync" +SERVICE_CHANGE_CHANNEL = "change_channel" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index d5c1c2d82bc648..7f4d03ccbb08a7 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -19,7 +19,6 @@ ATTR_HOLD_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, - DOMAIN, PLATFORM_SCHEMA, ) from homeassistant.const import ( @@ -33,6 +32,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify +from .const import DOMAIN, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC + _LOGGER = logging.getLogger(__name__) ATTR_CHANNEL = "channel" @@ -42,9 +43,6 @@ DEVICES = [] CONF_DEVICE_CACHE = "harmony_device_cache" -SERVICE_SYNC = "harmony_sync" -SERVICE_CHANGE_CHANNEL = "harmony_change_channel" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(ATTR_ACTIVITY): cv.string, diff --git a/homeassistant/components/harmony/services.yaml b/homeassistant/components/harmony/services.yaml index e69de29bb2d1d6..1b9ae225c7f802 100644 --- a/homeassistant/components/harmony/services.yaml +++ b/homeassistant/components/harmony/services.yaml @@ -0,0 +1,16 @@ +sync: + description: Syncs the remote's configuration. + fields: + entity_id: + description: Name(s) of entities to sync. + example: 'remote.family_room' + +change_channel: + description: Sends change channel command to the Harmony HUB + fields: + entity_id: + description: Name(s) of Harmony remote entities to send change channel command to + example: 'remote.family_room' + channel: + description: Channel number to change to + example: '200' \ No newline at end of file diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index a551ba18ed4587..f728b5033c87b8 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -65,24 +65,6 @@ learn_command: description: Timeout, in seconds, for the command to be learned. example: '30' - -harmony_sync: - description: Syncs the remote's configuration. - fields: - entity_id: - description: Name(s) of entities to sync. - example: 'remote.family_room' - -harmony_change_channel: - description: Sends change channel command to the Harmony HUB - fields: - entity_id: - description: Name(s) of Harmony remote entities to send change channel command to - example: 'remote.family_room' - channel: - description: Channel number to change to - example: '200' - xiaomi_miio_learn_command: description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' fields: From 82fe409961dfabab7aa28007465dc6bbf2e56135 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:15:15 -0500 Subject: [PATCH 1852/3953] add services.yaml description for matrix service (#29159) --- homeassistant/components/matrix/services.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index e69de29bb2d1d6..1cf83de2c332cf 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -0,0 +1,9 @@ +send_message: + description: Send message to target room(s) + fields: + message: + description: The message to be sent + example: 'This is a message I am sending to matrix' + target: + description: A list of room(s) to send the message to + example: '#hasstest:matrix.org' \ No newline at end of file From 8ef8a314df6c8f6be02db3f7caee44581cae927e Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 12:17:56 -0500 Subject: [PATCH 1853/3953] Update service domain for ifttt from 'alarm_control_panel' to 'ifttt' (#29129) * update service domain for ifttt * update missed variable references --- .../alarm_control_panel/services.yaml | 10 ---------- homeassistant/components/ifttt/__init__.py | 1 + .../components/ifttt/alarm_control_panel.py | 20 ++++++++++--------- homeassistant/components/ifttt/services.yaml | 11 +++++++++- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 9cd964141c36f2..4b8f54c3d17fe8 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -69,13 +69,3 @@ alarmdecoder_alarm_toggle_chime: code: description: A required code to toggle the alarm control panel chime with. example: 1234 - -ifttt_push_alarm_state: - description: Update the alarm state to the specified value. - fields: - entity_id: - description: Name of the alarm control panel which state has to be updated. - example: 'alarm_control_panel.downstairs' - state: - description: The state to which the alarm control panel has to be set. - example: 'armed_night' diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 05d773e9fd687a..362b01bb5d80b1 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -23,6 +23,7 @@ CONF_KEY = "key" +SERVICE_PUSH_ALARM_STATE = "push_alarm_state" SERVICE_TRIGGER = "trigger" SERVICE_TRIGGER_SCHEMA = vol.Schema( diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index f740cc8ccc902f..9c9ec88ccc7184 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -4,8 +4,12 @@ import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import DOMAIN, PLATFORM_SCHEMA +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanel, + FORMAT_NUMBER, + FORMAT_TEXT, +) +from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -24,7 +28,7 @@ ) import homeassistant.helpers.config_validation as cv -from . import ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER +from . import ATTR_EVENT, DOMAIN, SERVICE_PUSH_ALARM_STATE, SERVICE_TRIGGER _LOGGER = logging.getLogger(__name__) @@ -60,8 +64,6 @@ } ) -SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state" - PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema( {vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_STATE): cv.string} ) @@ -106,7 +108,7 @@ async def push_state_update(service): ) -class IFTTTAlarmPanel(alarm.AlarmControlPanel): +class IFTTTAlarmPanel(AlarmControlPanel): """Representation of an alarm control panel controlled through IFTTT.""" def __init__( @@ -148,8 +150,8 @@ def code_format(self): if self._code is None: return None if isinstance(self._code, str) and re.search("^\\d+$", self._code): - return alarm.FORMAT_NUMBER - return alarm.FORMAT_TEXT + return FORMAT_NUMBER + return FORMAT_TEXT def alarm_disarm(self, code=None): """Send disarm command.""" @@ -179,7 +181,7 @@ def set_alarm_state(self, event, state): """Call the IFTTT trigger service to change the alarm state.""" data = {ATTR_EVENT: event} - self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data) + self.hass.services.call(DOMAIN, SERVICE_TRIGGER, data) _LOGGER.debug("Called IFTTT integration to trigger event %s", event) if self._optimistic: self._state = state diff --git a/homeassistant/components/ifttt/services.yaml b/homeassistant/components/ifttt/services.yaml index 8669bc07fb4387..693c654f258299 100644 --- a/homeassistant/components/ifttt/services.yaml +++ b/homeassistant/components/ifttt/services.yaml @@ -1,5 +1,14 @@ # Describes the format for available ifttt services +push_alarm_state: + description: Update the alarm state to the specified value. + fields: + entity_id: + description: Name of the alarm control panel which state has to be updated. + example: 'alarm_control_panel.downstairs' + state: + description: The state to which the alarm control panel has to be set. + example: 'armed_night' trigger: description: Triggers the configured IFTTT Webhook. @@ -15,4 +24,4 @@ trigger: example: 'some additional data' value3: description: Generic field to send data via the event. - example: 'even more data' \ No newline at end of file + example: 'even more data' From fa1622fe8b25235649775b0f88afc8c82bc47ed8 Mon Sep 17 00:00:00 2001 From: "Brett T. Warden" Date: Wed, 27 Nov 2019 09:19:10 -0800 Subject: [PATCH 1854/3953] Use roku.poweron method for media_player.turn_on (#29123) Instead of using the roku.power method, which toggles power, implement the media_player.turn_on command for the roku component by calling the new roku.poweron method. Fixes #28961, but depends on upstream https://github.com/jcarbaugh/python-roku/pull/53 --- homeassistant/components/roku/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 12aca14151056c..84a1982fb49e6d 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -174,7 +174,7 @@ def source_list(self): def turn_on(self): """Turn on the Roku.""" - self.roku.power() + self.roku.poweron() def turn_off(self): """Turn off the Roku.""" From d7a66e6e4c3529fb9c9c872a2c0c9a785522ca6e Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Wed, 27 Nov 2019 21:52:03 +0200 Subject: [PATCH 1855/3953] Return Jewish Calendar holiday type id support (#29168) * Bring back holiday type id as a attribute to the holiday sensor * Add test for holiday type id attribute --- homeassistant/components/jewish_calendar/sensor.py | 3 ++- tests/components/jewish_calendar/test_sensor.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 54a3d1497aacdd..d0376694a4404a 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -122,8 +122,9 @@ def get_state(self, after_shkia_date, after_tzais_date): # Compute the weekly portion based on the upcoming shabbat. return after_tzais_date.upcoming_shabbat.parasha if self._type == "holiday": - self._holiday_attrs["type"] = after_shkia_date.holiday_type.name self._holiday_attrs["id"] = after_shkia_date.holiday_name + self._holiday_attrs["type"] = after_shkia_date.holiday_type.name + self._holiday_attrs["type_id"] = after_shkia_date.holiday_type.value return after_shkia_date.holiday_description if self._type == "omer_count": return after_shkia_date.omer_day diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 07e0b7cb192aab..60ffdea8c70810 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -180,8 +180,9 @@ async def test_jewish_calendar_sensor( assert sensor_object.state == str(result) if sensor == "holiday": - assert sensor_object.attributes.get("type") == "YOM_TOV" assert sensor_object.attributes.get("id") == "rosh_hashana_i" + assert sensor_object.attributes.get("type") == "YOM_TOV" + assert sensor_object.attributes.get("type_id") == 1 SHABBAT_PARAMS = [ From acc10c296e870382b89ac45aea6fd586c82950e2 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 15:26:54 -0500 Subject: [PATCH 1856/3953] move import to top and add service definition for pilight (#29163) * move import to top and add service definition for pilight * move import above HA imports --- homeassistant/components/pilight/__init__.py | 3 ++- homeassistant/components/pilight/services.yaml | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 2688b15e837c1d..e8cc0862a53fbb 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -7,6 +7,8 @@ import voluptuous as vol +from pilight import pilight + from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.util import dt as dt_util import homeassistant.helpers.config_validation as cv @@ -59,7 +61,6 @@ def setup(hass, config): """Set up the Pilight component.""" - from pilight import pilight host = config[DOMAIN][CONF_HOST] port = config[DOMAIN][CONF_PORT] diff --git a/homeassistant/components/pilight/services.yaml b/homeassistant/components/pilight/services.yaml index e69de29bb2d1d6..cc6141fdd91e61 100644 --- a/homeassistant/components/pilight/services.yaml +++ b/homeassistant/components/pilight/services.yaml @@ -0,0 +1,6 @@ +send: + description: Send RF code to Pilight device + fields: + protocol: + description: 'Protocol that Pilight recognizes. See https://manual.pilight.org/protocols/index.html for supported protocols and additional parameters that each protocol supports' + example: 'lirc' From 88441d5f68a9bc908febd6ef0d688845b1b5fd36 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 27 Nov 2019 16:14:01 -0600 Subject: [PATCH 1857/3953] Reduce connection timeout to avoid config flow timeouts (#29172) --- homeassistant/components/plex/server.py | 2 +- tests/components/plex/mock_classes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index c9aee9e8dad06c..46602cf6552e6e 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -77,7 +77,7 @@ def _connect_with_token(): self.server_choice = ( self._server_name if self._server_name else available_servers[0][0] ) - self._plex_server = account.resource(self.server_choice).connect() + self._plex_server = account.resource(self.server_choice).connect(timeout=10) def _connect_with_url(): session = None diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 69e6a84df63622..1a680e6af0f029 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -30,7 +30,7 @@ def __init__(self, index): self.provides = ["server"] self._mock_plex_server = MockPlexServer(index) - def connect(self): + def connect(self, timeout): """Mock the resource connect method.""" return self._mock_plex_server From bdb3eb16836d32480e17d18bf5434b178178d841 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 27 Nov 2019 23:14:19 +0100 Subject: [PATCH 1858/3953] Handle case when device can be None (#29171) --- homeassistant/components/deconz/device_trigger.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 62e05724cac2e1..e1f6c4fa135758 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -299,7 +299,11 @@ async def async_validate_trigger_config(hass, config): trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - if device.model not in REMOTES or trigger not in REMOTES[device.model]: + if ( + not device + or device.model not in REMOTES + or trigger not in REMOTES[device.model] + ): raise InvalidDeviceAutomationConfig return config From b6d94bcc96953bd1ed4631da75c17ddacc38124c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 17:15:25 -0500 Subject: [PATCH 1859/3953] Change apns service domain and remove unused variable (#29165) * change apns service domain and remove unused variable * fix tests * move DOMAIN to const.py and import DEVICE_TRACKER_DOMAIN from base component --- homeassistant/components/apns/const.py | 2 ++ homeassistant/components/apns/notify.py | 6 +++--- tests/components/apns/test_notify.py | 8 ++++---- 3 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/apns/const.py diff --git a/homeassistant/components/apns/const.py b/homeassistant/components/apns/const.py new file mode 100644 index 00000000000000..a8dc1204aa1949 --- /dev/null +++ b/homeassistant/components/apns/const.py @@ -0,0 +1,2 @@ +"""Constants for the apns component.""" +DOMAIN = "apns" diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index c24c9cc16052ae..ce761b502ac414 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -9,7 +9,6 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - DOMAIN, PLATFORM_SCHEMA, BaseNotificationService, ) @@ -18,13 +17,14 @@ from homeassistant.helpers import template as template_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_state_change +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN + +from .const import DOMAIN APNS_DEVICES = "apns.yaml" CONF_CERTFILE = "cert_file" CONF_TOPIC = "topic" CONF_SANDBOX = "sandbox" -DEVICE_TRACKER_DOMAIN = "device_tracker" -SERVICE_REGISTER = "apns_register" ATTR_PUSH_ID = "push_id" diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py index 78f597c58adef4..19d869ea678313 100644 --- a/tests/components/apns/test_notify.py +++ b/tests/components/apns/test_notify.py @@ -121,7 +121,7 @@ def fake_write(_out, device): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, + apns.DOMAIN, "apns_test_app", {"push_id": "1234", "name": "test device"}, blocking=True, @@ -153,7 +153,7 @@ def fake_write(_out, device): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, "apns_test_app", {"push_id": "1234"}, blocking=True + apns.DOMAIN, "apns_test_app", {"push_id": "1234"}, blocking=True ) devices = {dev.push_id: dev for dev in written_devices} @@ -183,7 +183,7 @@ def fake_write(_out, device): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, + apns.DOMAIN, "apns_test_app", {"push_id": "1234", "name": "updated device 1"}, blocking=True, @@ -222,7 +222,7 @@ def fake_write(_out, device): self._setup_notify() assert self.hass.services.call( - notify.DOMAIN, + apns.DOMAIN, "apns_test_app", {"push_id": "1234", "name": "updated device 1"}, blocking=True, From ceb1528b50604855a7b09a430f734f3aef4e1a31 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 17:18:01 -0500 Subject: [PATCH 1860/3953] Update bluetooth_tracker service name and domain from 'device_tracker' to 'bluetooth_tracker' (#29161) * move service constants to const.py, update service domain, add service description * update .coveragerc --- .coveragerc | 2 +- homeassistant/components/bluetooth_tracker/const.py | 3 +++ .../components/bluetooth_tracker/device_tracker.py | 6 ++---- homeassistant/components/bluetooth_tracker/services.yaml | 2 ++ 4 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/bluetooth_tracker/const.py diff --git a/.coveragerc b/.coveragerc index f1ba55054b567b..1107f22e5ef2f1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -85,7 +85,7 @@ omit = homeassistant/components/bloomsky/* homeassistant/components/bluesound/* homeassistant/components/bluetooth_le_tracker/device_tracker.py - homeassistant/components/bluetooth_tracker/device_tracker.py + homeassistant/components/bluetooth_tracker/* homeassistant/components/bme280/sensor.py homeassistant/components/bme680/sensor.py homeassistant/components/bmw_connected_drive/* diff --git a/homeassistant/components/bluetooth_tracker/const.py b/homeassistant/components/bluetooth_tracker/const.py new file mode 100644 index 00000000000000..b481efa296f783 --- /dev/null +++ b/homeassistant/components/bluetooth_tracker/const.py @@ -0,0 +1,3 @@ +"""Constants for the Bluetooth Tracker component.""" +DOMAIN = "bluetooth_tracker" +SERVICE_UPDATE = "update" diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 6a26775b0a8ac7..102c8e494aa44a 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -13,7 +13,6 @@ CONF_SCAN_INTERVAL, CONF_TRACK_NEW, DEFAULT_TRACK_NEW, - DOMAIN, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH, ) @@ -25,6 +24,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType +from .const import DOMAIN, SERVICE_UPDATE _LOGGER = logging.getLogger(__name__) @@ -184,8 +184,6 @@ async def handle_manual_update_bluetooth(call): hass.async_create_task(update_bluetooth()) async_track_time_interval(hass, update_bluetooth, interval) - hass.services.async_register( - DOMAIN, "bluetooth_tracker_update", handle_manual_update_bluetooth - ) + hass.services.async_register(DOMAIN, SERVICE_UPDATE, handle_manual_update_bluetooth) return True diff --git a/homeassistant/components/bluetooth_tracker/services.yaml b/homeassistant/components/bluetooth_tracker/services.yaml index e69de29bb2d1d6..b48c48a896808b 100644 --- a/homeassistant/components/bluetooth_tracker/services.yaml +++ b/homeassistant/components/bluetooth_tracker/services.yaml @@ -0,0 +1,2 @@ +update: + description: Trigger manual tracker update \ No newline at end of file From 7ecd5b5e5a4204ceab92705494cb8f8364f9120c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 17:18:21 -0500 Subject: [PATCH 1861/3953] add services.yaml description for service (#29160) --- .../components/nuimo_controller/services.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/homeassistant/components/nuimo_controller/services.yaml b/homeassistant/components/nuimo_controller/services.yaml index e69de29bb2d1d6..ba537544a3b902 100644 --- a/homeassistant/components/nuimo_controller/services.yaml +++ b/homeassistant/components/nuimo_controller/services.yaml @@ -0,0 +1,18 @@ +led_matrix: + description: Sends an LED Matrix to your display + fields: + matrix: + description: "A string representation of the matrix to be displayed. See the SDK documentation for more info: https://github.com/getSenic/nuimo-linux-python#write-to-nuimos-led-matrix" + example: + '........ + 0000000. + .000000. + ..00000. + .0.0000. + .00.000. + .000000. + .000000. + ........' + interval: + description: Display interval in seconds + example: 0.5 \ No newline at end of file From f6dd51a7fb257400781b752aa02815c426420e2a Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 28 Nov 2019 00:32:18 +0000 Subject: [PATCH 1862/3953] [ci skip] Translation update --- .../components/almond/.translations/ko.json | 3 +- .../components/deconz/.translations/ko.json | 18 +++++++- .../huawei_lte/.translations/ko.json | 5 ++- .../lutron_caseta/.translations/ca.json | 5 +++ .../lutron_caseta/.translations/it.json | 5 +++ .../lutron_caseta/.translations/no.json | 5 +++ .../lutron_caseta/.translations/pl.json | 5 +++ .../media_player/.translations/no.json | 2 +- .../components/sensor/.translations/no.json | 36 ++++++++-------- .../components/starline/.translations/ca.json | 17 ++++++++ .../components/starline/.translations/fr.json | 38 +++++++++++++++++ .../components/starline/.translations/it.json | 42 +++++++++++++++++++ .../components/starline/.translations/no.json | 42 +++++++++++++++++++ .../components/starline/.translations/pl.json | 42 +++++++++++++++++++ .../components/starline/.translations/ru.json | 42 +++++++++++++++++++ .../components/starline/.translations/sl.json | 32 ++++++++++++++ .../components/wled/.translations/ko.json | 19 +++++++++ 17 files changed, 336 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/lutron_caseta/.translations/ca.json create mode 100644 homeassistant/components/lutron_caseta/.translations/it.json create mode 100644 homeassistant/components/lutron_caseta/.translations/no.json create mode 100644 homeassistant/components/lutron_caseta/.translations/pl.json create mode 100644 homeassistant/components/starline/.translations/fr.json create mode 100644 homeassistant/components/starline/.translations/it.json create mode 100644 homeassistant/components/starline/.translations/no.json create mode 100644 homeassistant/components/starline/.translations/pl.json create mode 100644 homeassistant/components/starline/.translations/ru.json create mode 100644 homeassistant/components/starline/.translations/sl.json create mode 100644 homeassistant/components/wled/.translations/ko.json diff --git a/homeassistant/components/almond/.translations/ko.json b/homeassistant/components/almond/.translations/ko.json index 9440242ebbcc62..2a2346aaf501a9 100644 --- a/homeassistant/components/almond/.translations/ko.json +++ b/homeassistant/components/almond/.translations/ko.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_setup": "\ud558\ub098\uc758 Almond \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." }, "title": "Almond" } diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 61725316b137b9..fede936b964d65 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -55,10 +55,17 @@ "left": "\uc67c\ucabd", "open": "\uc5f4\uae30", "right": "\uc624\ub978\ucabd", + "side_1": "\uba74 1", + "side_2": "\uba74 2", + "side_3": "\uba74 3", + "side_4": "\uba74 4", + "side_5": "\uba74 5", + "side_6": "\uba74 6", "turn_off": "\ub044\uae30", "turn_on": "\ucf1c\uae30" }, "trigger_type": { + "remote_awakened": "\uae30\uae30 \uc808\uc804 \ubaa8\ub4dc \ud574\uc81c\ub428", "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984", - "remote_gyro_activated": "\uae30\uae30 \ud754\ub4e6" + "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \ub354\ube14\ud0ed \ub428", + "remote_falling": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc9d0", + "remote_gyro_activated": "\uae30\uae30 \ud754\ub4e6", + "remote_moved": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc784", + "remote_rotate_from_side_1": "\"\uba74 1\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", + "remote_rotate_from_side_2": "\"\uba74 2\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", + "remote_rotate_from_side_3": "\"\uba74 3\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", + "remote_rotate_from_side_4": "\"\uba74 4\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", + "remote_rotate_from_side_5": "\"\uba74 5\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", + "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428" } }, "options": { diff --git a/homeassistant/components/huawei_lte/.translations/ko.json b/homeassistant/components/huawei_lte/.translations/ko.json index b21e0aa0a23e4f..a9ac8d7f62c9e6 100644 --- a/homeassistant/components/huawei_lte/.translations/ko.json +++ b/homeassistant/components/huawei_lte/.translations/ko.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_in_progress": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "not_huawei_lte": "\ud654\uc6e8\uc774 LTE \uae30\uae30\uac00 \uc544\ub2d8" }, "error": { "connection_failed": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "connection_timeout": "\uc811\uc18d \uc2dc\uac04 \ucd08\uacfc", "incorrect_password": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "incorrect_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "incorrect_username_or_password": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/lutron_caseta/.translations/ca.json b/homeassistant/components/lutron_caseta/.translations/ca.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/ca.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/it.json b/homeassistant/components/lutron_caseta/.translations/it.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/it.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/no.json b/homeassistant/components/lutron_caseta/.translations/no.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/no.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/pl.json b/homeassistant/components/lutron_caseta/.translations/pl.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/pl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/no.json b/homeassistant/components/media_player/.translations/no.json index a05d907774ff90..388143b53ec4d6 100644 --- a/homeassistant/components/media_player/.translations/no.json +++ b/homeassistant/components/media_player/.translations/no.json @@ -5,7 +5,7 @@ "is_off": "{entity_name} er sl\u00e5tt av", "is_on": "{entity_name} er sl\u00e5tt p\u00e5", "is_paused": "{entity_name} er satt p\u00e5 pause", - "is_playing": "{entity_name} spiller" + "is_playing": "{entity_name} spiller n\u00e5" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json index 6709e4eb28c40f..1c0fc108510ba0 100644 --- a/homeassistant/components/sensor/.translations/no.json +++ b/homeassistant/components/sensor/.translations/no.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} batteriniv\u00e5", - "is_humidity": "{entity_name} fuktighet", - "is_illuminance": "{entity_name} belysningsstyrke", - "is_power": "{entity_name} effekt", - "is_pressure": "{entity_name} trykk", - "is_signal_strength": "{entity_name} signalstyrke", - "is_temperature": "{entity_name} temperatur", - "is_timestamp": "{entity_name} tidsstempel", - "is_value": "{entity_name} verdi" + "is_battery_level": "Gjeldende {entity_name} batteriniv\u00e5", + "is_humidity": "Gjeldende {entity_name} fuktighet", + "is_illuminance": "Gjeldende {entity_name} belysningsstyrke", + "is_power": "Gjeldende {entity_name} str\u00f8m", + "is_pressure": "Gjeldende {entity_name} trykk", + "is_signal_strength": "Gjeldende {entity_name} signalstyrke", + "is_temperature": "Gjeldende {entity_name} temperatur", + "is_timestamp": "Gjeldende {entity_name} tidsstempel", + "is_value": "Gjeldende {entity_name} verdi" }, "trigger_type": { - "battery_level": "{entity_name} batteriniv\u00e5", - "humidity": "{entity_name} fuktighet", - "illuminance": "{entity_name} belysningsstyrke", - "power": "{entity_name} str\u00f8m", - "pressure": "{entity_name} trykk", - "signal_strength": "{entity_name} signalstyrke", - "temperature": "{entity_name} temperatur", - "timestamp": "{entity_name} tidsstempel", - "value": "{entity_name} verdi" + "battery_level": "{entity_name} batteriniv\u00e5 endres", + "humidity": "{entity_name} fuktighets endringer", + "illuminance": "{entity_name} belysningsstyrke endringer", + "power": "{entity_name} str\u00f8m endringer", + "pressure": "{entity_name} trykk endringer", + "signal_strength": "{entity_name} signalstyrkeendringer", + "temperature": "{entity_name} temperaturendringer", + "timestamp": "{entity_name} tidsstempel endringer", + "value": "{entity_name} verdi endringer" } } } \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/ca.json b/homeassistant/components/starline/.translations/ca.json index ebfbac0044f841..04426c2acfa18e 100644 --- a/homeassistant/components/starline/.translations/ca.json +++ b/homeassistant/components/starline/.translations/ca.json @@ -1,7 +1,22 @@ { "config": { + "error": { + "error_auth_app": "ID d'aplicaci\u00f3 o secret incorrectes", + "error_auth_mfa": "Codi incorrecte", + "error_auth_user": "Nom d'usuari o contrasenya incorrectes" + }, "step": { + "auth_app": { + "data": { + "app_id": "ID d'aplicaci\u00f3", + "app_secret": "Secret" + }, + "title": "Credencials d'aplicaci\u00f3" + }, "auth_captcha": { + "data": { + "captcha_code": "Codi des de imatge" + }, "description": "{captcha_img}", "title": "Captcha" }, @@ -9,6 +24,7 @@ "data": { "mfa_code": "Codi SMS" }, + "description": "Introdueix el codi rebut al n\u00famero {phone_number}", "title": "Verificaci\u00f3 en dos passos" }, "auth_user": { @@ -16,6 +32,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, + "description": "Correu electr\u00f2nic i contrasenya del compte StarLine", "title": "Credencials d\u2019usuari" } }, diff --git a/homeassistant/components/starline/.translations/fr.json b/homeassistant/components/starline/.translations/fr.json new file mode 100644 index 00000000000000..07563bdb857d51 --- /dev/null +++ b/homeassistant/components/starline/.translations/fr.json @@ -0,0 +1,38 @@ +{ + "config": { + "error": { + "error_auth_mfa": "code incorrect", + "error_auth_user": "identifiant ou mot de passe incorrect" + }, + "step": { + "auth_app": { + "data": { + "app_secret": "Secret" + }, + "title": "Informations d'identification de l'application" + }, + "auth_captcha": { + "data": { + "captcha_code": "Code de l'image" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Code SMS" + }, + "title": "Autorisation \u00e0 deux facteurs" + }, + "auth_user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Adresse e-mail et mot de passe du compte StarLine", + "title": "Informations d'identification de l'utilisateur" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/it.json b/homeassistant/components/starline/.translations/it.json new file mode 100644 index 00000000000000..f68732354c6669 --- /dev/null +++ b/homeassistant/components/starline/.translations/it.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "ID applicazione o Segreto errati", + "error_auth_mfa": "Codice errato", + "error_auth_user": "Nome utente o password errati" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID applicazione", + "app_secret": "Segreto" + }, + "description": "ID applicazione e codice segreto da Account sviluppatore StarLine ", + "title": "Credenziali dell'applicazione" + }, + "auth_captcha": { + "data": { + "captcha_code": "Codice dall'immagine" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Codice SMS" + }, + "description": "Inserisci il codice inviato al telefono {phone_number}.", + "title": "Autenticazione a due fattori" + }, + "auth_user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Email e password dell'account StarLine", + "title": "Credenziali utente" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/no.json b/homeassistant/components/starline/.translations/no.json new file mode 100644 index 00000000000000..37d55aea194b7c --- /dev/null +++ b/homeassistant/components/starline/.translations/no.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Feil applikasjons-ID eller hemmelighet", + "error_auth_mfa": "Feil kode", + "error_auth_user": "Ugyldig brukernavn eller passord" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App-ID", + "app_secret": "Hemmelig" + }, + "description": "S\u00f8knads-ID og hemmelig kode fra StarLine utviklerkonto ", + "title": "Bruksanvisning" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kode fra bilde" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS-kode" + }, + "description": "Skriv inn koden som er sendt til telefonen {phone_number}", + "title": "Tofaktorautentisering" + }, + "auth_user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "E-postadresse og passord for StarLine-kontoen", + "title": "Brukerlegitimasjon" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/pl.json b/homeassistant/components/starline/.translations/pl.json new file mode 100644 index 00000000000000..71d54bbbcd1b39 --- /dev/null +++ b/homeassistant/components/starline/.translations/pl.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Niepoprawny identyfikator aplikacji lub tajny kod", + "error_auth_mfa": "Niepoprawny kod", + "error_auth_user": "Niepoprawna nazwa u\u017cytkownika lub has\u0142o" + }, + "step": { + "auth_app": { + "data": { + "app_id": "Identyfikator aplikacji", + "app_secret": "Tajny kod" + }, + "description": "Identyfikator aplikacji i tajny kod z konta programisty StarLine", + "title": "Po\u015bwiadczenia aplikacji" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kod z obrazka" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "Kod SMS" + }, + "description": "Wprowad\u017a kod wys\u0142any na numer telefonu {phone_number}", + "title": "Uwierzytelnianie dwusk\u0142adnikowe" + }, + "auth_user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Adres e-mail i has\u0142o do konta StarLine", + "title": "Po\u015bwiadczenia u\u017cytkownika" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/ru.json b/homeassistant/components/starline/.translations/ru.json new file mode 100644 index 00000000000000..aa84c36772b07e --- /dev/null +++ b/homeassistant/components/starline/.translations/ru.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434.", + "error_auth_mfa": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434.", + "error_auth_user": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", + "app_secret": "\u0421\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434" + }, + "description": "ID \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043e\u0434 \u0438\u0437 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 StarLine", + "title": "\u0423\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u041a\u043e\u0434 \u0441 \u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438" + }, + "description": "{captcha_img}", + "title": "CAPTCHA" + }, + "auth_mfa": { + "data": { + "mfa_code": "\u041a\u043e\u0434 \u0438\u0437 SMS" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043d\u0430 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0430 {phone_number}", + "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "auth_user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 StarLine", + "title": "\u0423\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/sl.json b/homeassistant/components/starline/.translations/sl.json new file mode 100644 index 00000000000000..2cc83f1227a1b8 --- /dev/null +++ b/homeassistant/components/starline/.translations/sl.json @@ -0,0 +1,32 @@ +{ + "config": { + "step": { + "auth_app": { + "title": "Poverilnice programa" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u0160ifra iz slike" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS koda" + }, + "description": "Vnesite kodo, poslano na telefon {phone_number}", + "title": "2-faktorska avtorizacija" + }, + "auth_user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime" + }, + "description": "StarLine e-po\u0161tni ra\u010dun in geslo", + "title": "Uporabni\u0161ke poverilnice" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/ko.json b/homeassistant/components/wled/.translations/ko.json new file mode 100644 index 00000000000000..bee9c2a6204be4 --- /dev/null +++ b/homeassistant/components/wled/.translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "WLED \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "connection_error": "WLED \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "WLED \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c" + } + } + } + } +} \ No newline at end of file From 9fd058aa33807dea138c4c86b4a918a08db33491 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 19:47:45 -0500 Subject: [PATCH 1863/3953] move soundtouch services to soundtouch domain (#29140) --- homeassistant/components/soundtouch/const.py | 6 ++++ .../components/soundtouch/media_player.py | 14 ++++---- .../components/soundtouch/services.yaml | 36 +++++++++++++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/soundtouch/const.py diff --git a/homeassistant/components/soundtouch/const.py b/homeassistant/components/soundtouch/const.py new file mode 100644 index 00000000000000..37bf1d8cc2b9ad --- /dev/null +++ b/homeassistant/components/soundtouch/const.py @@ -0,0 +1,6 @@ +"""Constants for the Bose Soundtouch component.""" +DOMAIN = "soundtouch" +SERVICE_PLAY_EVERYWHERE = "play_everywhere" +SERVICE_CREATE_ZONE = "create_zone" +SERVICE_ADD_ZONE_SLAVE = "add_zone_slave" +SERVICE_REMOVE_ZONE_SLAVE = "remove_zone_slave" diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index f613ba22dfa406..a9f6e05011fced 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -6,7 +6,6 @@ from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -29,12 +28,15 @@ ) import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) +from .const import ( + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + SERVICE_CREATE_ZONE, + SERVICE_PLAY_EVERYWHERE, + SERVICE_REMOVE_ZONE_SLAVE, +) -SERVICE_PLAY_EVERYWHERE = "soundtouch_play_everywhere" -SERVICE_CREATE_ZONE = "soundtouch_create_zone" -SERVICE_ADD_ZONE_SLAVE = "soundtouch_add_zone_slave" -SERVICE_REMOVE_ZONE_SLAVE = "soundtouch_remove_zone_slave" +_LOGGER = logging.getLogger(__name__) MAP_STATUS = { "PLAY_STATE": STATE_PLAYING, diff --git a/homeassistant/components/soundtouch/services.yaml b/homeassistant/components/soundtouch/services.yaml index e69de29bb2d1d6..fd848b76b2db0b 100644 --- a/homeassistant/components/soundtouch/services.yaml +++ b/homeassistant/components/soundtouch/services.yaml @@ -0,0 +1,36 @@ +play_everywhere: + description: Play on all Bose Soundtouch devices. + fields: + master: + description: Name of the master entity that will coordinate the grouping. Platform dependent. It is a shortcut for creating a multi-room zone with all devices + example: 'media_player.soundtouch_home' + +create_zone: + description: Create a Sountouch multi-room zone. + fields: + master: + description: Name of the master entity that will coordinate the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to add to the new zone. + example: 'media_player.soundtouch_bedroom' + +add_zone_slave: + description: Add a slave to a Sountouch multi-room zone. + fields: + master: + description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to add to the existing zone. + example: 'media_player.soundtouch_bedroom' + +remove_zone_slave: + description: Remove a slave from the Sounttouch multi-room zone. + fields: + master: + description: Name of the master entity that is coordinating the multi-room zone. Platform dependent. + example: 'media_player.soundtouch_home' + slaves: + description: Name of slaves entities to remove from the existing zone. + example: 'media_player.soundtouch_bedroom' From 88376bf363100a96b4e08772a5196f2f17050e4a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 19:49:12 -0500 Subject: [PATCH 1864/3953] move wemo services to wemo domain (#29135) --- .coveragerc | 1 - homeassistant/components/fan/services.yaml | 17 ----------------- homeassistant/components/wemo/__init__.py | 3 +-- homeassistant/components/wemo/const.py | 5 +++++ homeassistant/components/wemo/fan.py | 6 +----- homeassistant/components/wemo/services.yaml | 16 ++++++++++++++++ homeassistant/components/wemo/switch.py | 5 +++-- 7 files changed, 26 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/wemo/const.py diff --git a/.coveragerc b/.coveragerc index 1107f22e5ef2f1..74fb5c11cece7d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -770,7 +770,6 @@ omit = homeassistant/components/waze_travel_time/sensor.py homeassistant/components/webostv/* homeassistant/components/wemo/* - homeassistant/components/wemo/fan.py homeassistant/components/whois/sensor.py homeassistant/components/wink/* homeassistant/components/wirelesstag/* diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 0e3978690e69d1..3cb3db6dfe0bff 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -194,20 +194,3 @@ xiaomi_miio_set_dry_off: entity_id: description: Name of the xiaomi miio entity. example: 'fan.xiaomi_miio_device' - -wemo_set_humidity: - description: Set the target humidity of WeMo humidifier devices. - fields: - entity_id: - description: Names of the WeMo humidifier entities (1 or more entity_ids are required). - example: 'fan.wemo_humidifier' - target_humidity: - description: Target humidity. This is a float value between 0 and 100, but will be mapped to the humidity levels that WeMo humidifiers support (45, 50, 55, 60, and 100/Max) by rounding the value down to the nearest supported value. - example: 56.5 - -wemo_reset_filter_life: - description: Reset the WeMo Humidifier's filter life to 100%. - fields: - entity_id: - description: Names of the WeMo humidifier entities (1 or more entity_ids are required). - example: 'fan.wemo_humidifier' diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index df2d8ed1f314f9..fe63f10aebae29 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -11,8 +11,7 @@ from homeassistant.helpers import discovery from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP - -DOMAIN = "wemo" +from .const import DOMAIN # Mapping from Wemo model_name to component. WEMO_MODEL_DISPATCH = { diff --git a/homeassistant/components/wemo/const.py b/homeassistant/components/wemo/const.py new file mode 100644 index 00000000000000..e9272d39bddb66 --- /dev/null +++ b/homeassistant/components/wemo/const.py @@ -0,0 +1,5 @@ +"""Constants for the Belkin Wemo component.""" +DOMAIN = "wemo" + +SERVICE_SET_HUMIDITY = "set_humidity" +SERVICE_RESET_FILTER_LIFE = "reset_filter_life" diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 91273fa033fc30..4a8be4fba81f62 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -10,7 +10,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( - DOMAIN, SUPPORT_SET_SPEED, FanEntity, SPEED_OFF, @@ -22,6 +21,7 @@ from homeassistant.const import ATTR_ENTITY_ID from . import SUBSCRIPTION_REGISTRY +from .const import DOMAIN, SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY SCAN_INTERVAL = timedelta(seconds=10) DATA_KEY = "fan.wemo" @@ -79,8 +79,6 @@ if k not in [WEMO_FAN_LOW, WEMO_FAN_HIGH] } -SERVICE_SET_HUMIDITY = "wemo_set_humidity" - SET_HUMIDITY_SCHEMA = vol.Schema( { vol.Required(ATTR_ENTITY_ID): cv.entity_ids, @@ -90,8 +88,6 @@ } ) -SERVICE_RESET_FILTER_LIFE = "wemo_reset_filter_life" - RESET_FILTER_LIFE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) diff --git a/homeassistant/components/wemo/services.yaml b/homeassistant/components/wemo/services.yaml index e69de29bb2d1d6..c2415265c62cc0 100644 --- a/homeassistant/components/wemo/services.yaml +++ b/homeassistant/components/wemo/services.yaml @@ -0,0 +1,16 @@ +set_humidity: + description: Set the target humidity of WeMo humidifier devices. + fields: + entity_id: + description: Names of the WeMo humidifier entities (1 or more entity_ids are required). + example: 'fan.wemo_humidifier' + target_humidity: + description: Target humidity. This is a float value between 0 and 100, but will be mapped to the humidity levels that WeMo humidifiers support (45, 50, 55, 60, and 100/Max) by rounding the value down to the nearest supported value. + example: 56.5 + +reset_filter_life: + description: Reset the WeMo Humidifier's filter life to 100%. + fields: + entity_id: + description: Names of the WeMo humidifier entities (1 or more entity_ids are required). + example: 'fan.wemo_humidifier' diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index c1d07a069021ea..1c0606b489dbb7 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -12,7 +12,8 @@ from homeassistant.util import convert from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN -from . import SUBSCRIPTION_REGISTRY, DOMAIN as WEMO_DOMAIN +from . import SUBSCRIPTION_REGISTRY +from .const import DOMAIN SCAN_INTERVAL = timedelta(seconds=10) @@ -96,7 +97,7 @@ def name(self): @property def device_info(self): """Return the device info.""" - return {"name": self._name, "identifiers": {(WEMO_DOMAIN, self._serialnumber)}} + return {"name": self._name, "identifiers": {(DOMAIN, self._serialnumber)}} @property def device_state_attributes(self): From f1a4e212cc68ea3840301eb32c731a48b246fbee Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 19:51:40 -0500 Subject: [PATCH 1865/3953] Update service domain for alarmdecoder from 'alarm_control_panel' to 'alarmdecoder' (#29127) * update alarmdecoder service domain * update alarmdecoder service name * black formatting --- .../alarm_control_panel/services.yaml | 10 ---------- .../alarmdecoder/alarm_control_panel.py | 17 ++++++++++------- .../components/alarmdecoder/services.yaml | 10 ++++++++++ 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 4b8f54c3d17fe8..b31cb718b3f8e4 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -59,13 +59,3 @@ alarm_trigger: code: description: An optional code to trigger the alarm control panel with. example: 1234 - -alarmdecoder_alarm_toggle_chime: - description: Send the alarm the toggle chime command. - fields: - entity_id: - description: Name of the alarm control panel to trigger. - example: 'alarm_control_panel.downstairs' - code: - description: A required code to toggle the alarm control panel chime with. - example: 1234 diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index f9bff3bfda7b62..d2e9fd136a8270 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -3,7 +3,10 @@ import voluptuous as vol -import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanel, + FORMAT_NUMBER, +) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, @@ -18,11 +21,11 @@ ) import homeassistant.helpers.config_validation as cv -from . import DATA_AD, DOMAIN as DOMAIN_ALARMDECODER, SIGNAL_PANEL_MESSAGE +from . import DATA_AD, DOMAIN, SIGNAL_PANEL_MESSAGE _LOGGER = logging.getLogger(__name__) -SERVICE_ALARM_TOGGLE_CHIME = "alarmdecoder_alarm_toggle_chime" +SERVICE_ALARM_TOGGLE_CHIME = "alarm_toggle_chime" ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string}) SERVICE_ALARM_KEYPRESS = "alarm_keypress" @@ -41,7 +44,7 @@ def alarm_toggle_chime_handler(service): device.alarm_toggle_chime(code) hass.services.register( - alarm.DOMAIN, + DOMAIN, SERVICE_ALARM_TOGGLE_CHIME, alarm_toggle_chime_handler, schema=ALARM_TOGGLE_CHIME_SCHEMA, @@ -53,14 +56,14 @@ def alarm_keypress_handler(service): device.alarm_keypress(keypress) hass.services.register( - DOMAIN_ALARMDECODER, + DOMAIN, SERVICE_ALARM_KEYPRESS, alarm_keypress_handler, schema=ALARM_KEYPRESS_SCHEMA, ) -class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): +class AlarmDecoderAlarmPanel(AlarmControlPanel): """Representation of an AlarmDecoder-based alarm panel.""" def __init__(self): @@ -120,7 +123,7 @@ def should_poll(self): @property def code_format(self): """Return one or more digits/characters.""" - return alarm.FORMAT_NUMBER + return FORMAT_NUMBER @property def state(self): diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml index 55451d42f13768..12268d48bb7121 100644 --- a/homeassistant/components/alarmdecoder/services.yaml +++ b/homeassistant/components/alarmdecoder/services.yaml @@ -7,3 +7,13 @@ alarm_keypress: keypress: description: 'String to send to the alarm panel.' example: '*71' + +alarm_toggle_chime: + description: Send the alarm the toggle chime command. + fields: + entity_id: + description: Name of the alarm control panel to trigger. + example: 'alarm_control_panel.downstairs' + code: + description: A required code to toggle the alarm control panel chime with. + example: 1234 From 665613e395efb79bb9f426170fd27281328124b5 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Wed, 27 Nov 2019 20:45:36 -0500 Subject: [PATCH 1866/3953] Add fan support to lutron_caseta (#29033) * Adding lutron_caseta fan controller. Updated to pylutron 0.5.1 * Accidental import remain * updating for black formatting * Fix blank spaces * Moving third party import * update to 0.5.1 to pass checks * fix deletion of new line * Update fan.py * Update with dev branch, update from comments, added mediumhigh --- .../components/lutron_caseta/__init__.py | 2 +- homeassistant/components/lutron_caseta/fan.py | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lutron_caseta/fan.py diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 54bde9538af526..f5ed0742cf7cee 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -32,7 +32,7 @@ extra=vol.ALLOW_EXTRA, ) -LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene"] +LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene", "fan"] async def async_setup(hass, base_config): diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py new file mode 100644 index 00000000000000..486a0aed401979 --- /dev/null +++ b/homeassistant/components/lutron_caseta/fan.py @@ -0,0 +1,96 @@ +"""Support for Lutron Caseta fans.""" +import logging + +from pylutron_caseta import FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH + +from homeassistant.components.fan import ( + SUPPORT_SET_SPEED, + FanEntity, + DOMAIN, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, +) + +from . import LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice + +_LOGGER = logging.getLogger(__name__) + +VALUE_TO_SPEED = { + None: SPEED_OFF, + FAN_OFF: SPEED_OFF, + FAN_LOW: SPEED_LOW, + FAN_MEDIUM: SPEED_MEDIUM, + FAN_MEDIUM_HIGH: SPEED_MEDIUM, + FAN_HIGH: SPEED_HIGH, +} + +SPEED_TO_VALUE = { + SPEED_OFF: FAN_OFF, + SPEED_LOW: FAN_LOW, + SPEED_MEDIUM: FAN_MEDIUM, + SPEED_HIGH: FAN_HIGH, +} + +FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up Lutron fan.""" + entities = [] + bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + fan_devices = bridge.get_devices_by_domain(DOMAIN) + + for fan_device in fan_devices: + entity = LutronCasetaFan(fan_device, bridge) + entities.append(entity) + + async_add_entities(entities, True) + + +class LutronCasetaFan(LutronCasetaDevice, FanEntity): + """Representation of a Lutron Caseta fan. Including Fan Speed.""" + + @property + def speed(self) -> str: + """Return the current speed.""" + return VALUE_TO_SPEED[self._device["fan_speed"]] + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return FAN_SPEEDS + + @property + def supported_features(self) -> int: + """Flag supported features. Speed Only.""" + return SUPPORT_SET_SPEED + + async def async_turn_on(self, speed: str = None, **kwargs): + """Turn the fan on.""" + if speed is None: + speed = SPEED_MEDIUM + await self.async_set_speed(speed) + + async def async_turn_off(self, **kwargs): + """Turn the fan off.""" + await self.async_set_speed(SPEED_OFF) + + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + self._smartbridge.set_fan(self.device_id, SPEED_TO_VALUE[speed]) + + @property + def is_on(self): + """Return true if device is on.""" + return VALUE_TO_SPEED[self._device["fan_speed"]] in [ + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + ] + + async def async_update(self): + """Update when forcing a refresh of the device.""" + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug("State of this lutron fan device is %s", self._device) From 4cfd24a03a87f4326a634974e9b5b75692c7d349 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Nov 2019 20:27:22 -0800 Subject: [PATCH 1867/3953] Fix mobile app device identifiers (#29173) --- homeassistant/components/mobile_app/__init__.py | 7 +------ tests/components/mobile_app/test_entity.py | 5 +++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index ca2a58d1f96111..56594f3e2c30d4 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -76,14 +76,9 @@ async def async_setup_entry(hass, entry): device_registry = await dr.async_get_registry(hass) - identifiers = { - (ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]), - (CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID]), - } - device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, - identifiers=identifiers, + identifiers={(DOMAIN, registration[ATTR_DEVICE_ID])}, manufacturer=registration[ATTR_MANUFACTURER], model=registration[ATTR_MODEL], name=registration[ATTR_DEVICE_NAME], diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index 94a4e76ae848ac..0db9d42048fc82 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -2,6 +2,8 @@ import logging +from homeassistant.helpers import device_registry + _LOGGER = logging.getLogger(__name__) @@ -64,6 +66,9 @@ async def test_sensor(hass, create_registrations, webhook_client): updated_entity = hass.states.get("sensor.battery_state") assert updated_entity.state == "123" + dev_reg = await device_registry.async_get_registry(hass) + assert len(dev_reg.devices) == len(create_registrations) + async def test_sensor_must_register(hass, create_registrations, webhook_client): """Test that sensors must be registered before updating.""" From 3ecf5596ffcbdc6039c23646386dc6feace74130 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Nov 2019 23:36:40 -0500 Subject: [PATCH 1868/3953] update demo service domain and service description (#29164) * update demo service domain and service description * move DOMAIN and service name to const.py * update per balloob's service name suggestion * update service name in services.yaml * update service name constant's name --- homeassistant/components/demo/const.py | 3 +++ homeassistant/components/demo/device_tracker.py | 4 ++-- homeassistant/components/demo/services.yaml | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/demo/const.py diff --git a/homeassistant/components/demo/const.py b/homeassistant/components/demo/const.py new file mode 100644 index 00000000000000..e11b0b0731a3b5 --- /dev/null +++ b/homeassistant/components/demo/const.py @@ -0,0 +1,3 @@ +"""Constants for the Demo component.""" +DOMAIN = "demo" +SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA = "randomize_device_tracker_data" diff --git a/homeassistant/components/demo/device_tracker.py b/homeassistant/components/demo/device_tracker.py index fba8095efd6420..02864111527b6f 100644 --- a/homeassistant/components/demo/device_tracker.py +++ b/homeassistant/components/demo/device_tracker.py @@ -1,7 +1,7 @@ """Demo platform for the Device tracker component.""" import random -from homeassistant.components.device_tracker import DOMAIN +from .const import DOMAIN, SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA def setup_scanner(hass, config, see, discovery_info=None): @@ -36,6 +36,6 @@ def observe(call=None): battery=53, ) - hass.services.register(DOMAIN, "demo", observe) + hass.services.register(DOMAIN, SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA, observe) return True diff --git a/homeassistant/components/demo/services.yaml b/homeassistant/components/demo/services.yaml index e69de29bb2d1d6..a8a96b21c192b5 100644 --- a/homeassistant/components/demo/services.yaml +++ b/homeassistant/components/demo/services.yaml @@ -0,0 +1,2 @@ +randomize_device_tracker_data: + description: Demonstrates using a device tracker to see where devices are located \ No newline at end of file From 69991bf3a2c4f09d874946ab10d101e1bacf49e7 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 28 Nov 2019 05:45:43 +0100 Subject: [PATCH 1869/3953] Move GoogleConfig initialization into setup of component (#29170) --- homeassistant/components/google_assistant/__init__.py | 9 +++++++-- homeassistant/components/google_assistant/http.py | 11 ----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index f0f2ad773887fe..c156bf0176b6e1 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -32,7 +32,7 @@ ) from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 from .const import EVENT_QUERY_RECEIVED # noqa: F401 -from .http import async_register_http +from .http import GoogleAssistantView, GoogleConfig _LOGGER = logging.getLogger(__name__) @@ -93,7 +93,12 @@ def _check_report_state(data): async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) - google_config = async_register_http(hass, config) + google_config = GoogleConfig(hass, config) + + hass.http.register_view(GoogleAssistantView(google_config)) + + if google_config.should_report_state: + google_config.async_enable_report_state() async def request_sync_service_handler(call: ServiceCall): """Handle request sync service calls.""" diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 164ef8bb4de0d2..abf931bd969421 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -10,7 +10,6 @@ # Typing imports from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util @@ -225,16 +224,6 @@ async def async_report_state(self, message): await self.async_call_homegraph_api(REPORT_STATE_BASE_URL, data) -@callback -def async_register_http(hass, cfg): - """Register HTTP views for Google Assistant.""" - config = GoogleConfig(hass, cfg) - hass.http.register_view(GoogleAssistantView(config)) - if config.should_report_state: - config.async_enable_report_state() - return config - - class GoogleAssistantView(HomeAssistantView): """Handle Google Assistant requests.""" From 9c9e9bc92afd64a2bcfae93548b0985b6ed74567 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 28 Nov 2019 00:40:08 -0600 Subject: [PATCH 1870/3953] Bump plexwebsocket to 0.0.6 (#29175) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 29bdbf34b60f2e..922a9c142884b7 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==3.3.0", "plexauth==0.0.5", - "plexwebsocket==0.0.5" + "plexwebsocket==0.0.6" ], "dependencies": [ "http" diff --git a/requirements_all.txt b/requirements_all.txt index cf6a0a7f8e317f..33693732c23797 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -990,7 +990,7 @@ plexapi==3.3.0 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.5 +plexwebsocket==0.0.6 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 003827d414b04b..0f22fe8c2322ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -330,7 +330,7 @@ plexapi==3.3.0 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.5 +plexwebsocket==0.0.6 # homeassistant.components.mhz19 # homeassistant.components.serial_pm From 4e107a2bcf0ed202f4fcb2d6e020564cd2a0d52a Mon Sep 17 00:00:00 2001 From: tetienne Date: Thu, 28 Nov 2019 10:42:17 +0100 Subject: [PATCH 1871/3953] Add support for Somfy Camera Shutter (#29057) --- homeassistant/components/somfy/__init__.py | 2 +- homeassistant/components/somfy/cover.py | 16 +------ homeassistant/components/somfy/manifest.json | 2 +- homeassistant/components/somfy/switch.py | 49 ++++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/somfy/switch.py diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index b767ea834317d9..184e32f1e6de69 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -48,7 +48,7 @@ extra=vol.ALLOW_EXTRA, ) -SOMFY_COMPONENTS = ["cover"] +SOMFY_COMPONENTS = ["cover", "switch"] async def async_setup(hass, config): diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index d54e7c990018ed..3a8557ea4841b3 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -1,9 +1,4 @@ -""" -Support for Somfy Covers. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/cover.somfy/ -""" +"""Support for Somfy Covers.""" from pymfy.api.devices.category import Category from pymfy.api.devices.blind import Blind @@ -37,15 +32,6 @@ def get_covers(): async_add_entities(await hass.async_add_executor_job(get_covers), True) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Old way of setting up platform. - - Can only be called when a user accidentally mentions the platform in their - config. But even in that case it would have been ignored. - """ - pass - - class SomfyCover(SomfyEntity, CoverDevice): """Representation of a Somfy cover device.""" diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index f5a17275bcb355..82e62e7dd08eee 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.6.1"] + "requirements": ["pymfy==0.7.1"] } diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py new file mode 100644 index 00000000000000..d3294954851542 --- /dev/null +++ b/homeassistant/components/somfy/switch.py @@ -0,0 +1,49 @@ +"""Support for Somfy Camera Shutter.""" +from pymfy.api.devices.camera_protect import CameraProtect +from pymfy.api.devices.category import Category + +from homeassistant.components.somfy import DOMAIN, SomfyEntity, DEVICES, API +from homeassistant.helpers.entity import ToggleEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Somfy switch platform.""" + + def get_shutters(): + """Retrieve switches.""" + devices = hass.data[DOMAIN][DEVICES] + + return [ + SomfyCameraShutter(device, hass.data[DOMAIN][API]) + for device in devices + if Category.CAMERA.value in device.categories + ] + + async_add_entities(await hass.async_add_executor_job(get_shutters), True) + + +class SomfyCameraShutter(SomfyEntity, ToggleEntity): + """Representation of a Somfy Camera Shutter device.""" + + def __init__(self, device, api): + """Initialize the Somfy device.""" + super().__init__(device, api) + self.shutter = CameraProtect(self.device, self.api) + + async def async_update(self): + """Update the device with the latest data.""" + await super().async_update() + self.shutter = CameraProtect(self.device, self.api) + + def turn_on(self, **kwargs) -> None: + """Turn the entity on.""" + self.shutter.open_shutter() + + def turn_off(self, **kwargs): + """Turn the entity off.""" + self.shutter.close_shutter() + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self.shutter.get_shutter_position() == "opened" diff --git a/requirements_all.txt b/requirements_all.txt index 33693732c23797..7d0c08e6a28670 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1335,7 +1335,7 @@ pymailgunner==1.4 pymediaroom==0.6.4 # homeassistant.components.somfy -pymfy==0.6.1 +pymfy==0.7.1 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f22fe8c2322ef..d8d13ab8908f92 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -449,7 +449,7 @@ pylitejet==0.1 pymailgunner==1.4 # homeassistant.components.somfy -pymfy==0.6.1 +pymfy==0.7.1 # homeassistant.components.mochad pymochad==0.2.0 From 5d5d053bced20dfd53d43c1bd3bcc0ffce493273 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 28 Nov 2019 13:40:29 +0100 Subject: [PATCH 1872/3953] Use github instead gitlab --- .pre-commit-config-all.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml index abc1afbde788ff..f7b29cddc4fbe1 100644 --- a/.pre-commit-config-all.yaml +++ b/.pre-commit-config-all.yaml @@ -18,7 +18,7 @@ repos: - --safe - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ -- repo: https://gitlab.com/pycqa/flake8 +- repo: https://github.com/PyCQA/flake8 rev: 3.7.9 hooks: - id: flake8 From b847d55077ffa529e46f19256f6d720c12ff6e5e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Nov 2019 05:23:59 -0800 Subject: [PATCH 1873/3953] Only create cloud user if cloud in use (#29150) * Only create cloud user if cloud in use * Pass context to alexa * Update requirements * Fix handing & design pattern for 0.30 * fix tests * Fix lint & tests * rename internal user --- homeassistant/components/cloud/__init__.py | 14 ---- homeassistant/components/cloud/client.py | 43 ++++++---- homeassistant/components/cloud/const.py | 1 + .../components/cloud/google_config.py | 12 ++- homeassistant/components/cloud/http_api.py | 14 ++-- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/components/cloud/prefs.py | 78 ++++++++++++++---- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cloud/test_client.py | 26 +++++- tests/components/cloud/test_google_config.py | 5 +- tests/components/cloud/test_http_api.py | 25 ++---- tests/components/cloud/test_init.py | 60 +------------- tests/components/cloud/test_prefs.py | 80 +++++++++++++++++++ 15 files changed, 221 insertions(+), 145 deletions(-) create mode 100644 tests/components/cloud/test_prefs.py diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 763f6214185560..6d9b70051f5dab 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -4,7 +4,6 @@ from hass_nabucasa import Cloud import voluptuous as vol -from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.alexa import const as alexa_const from homeassistant.components.google_assistant import const as ga_c from homeassistant.const import ( @@ -186,19 +185,6 @@ async def async_setup(hass, config): prefs = CloudPreferences(hass) await prefs.async_initialize() - # Cloud user - user = None - if prefs.cloud_user: - # Fetch the user. It can happen that the user no longer exists if - # an image was restored without restoring the cloud prefs. - user = await hass.auth.async_get_user(prefs.cloud_user) - - if user is None: - user = await hass.auth.async_create_system_user( - "Home Assistant Cloud", [GROUP_ID_ADMIN] - ) - await prefs.async_update(cloud_user=user.id) - # Initialize Cloud websession = hass.helpers.aiohttp_client.async_get_clientsession() client = CloudClient(hass, prefs, websession, alexa_conf, google_conf) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index c7626777943da1..00acf930f86c7c 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -7,7 +7,7 @@ import aiohttp from hass_nabucasa.client import CloudClient as Interface -from homeassistant.core import callback +from homeassistant.core import callback, Context from homeassistant.components.google_assistant import smart_home as ga from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -44,7 +44,6 @@ def __init__( self.alexa_user_config = alexa_user_config self._alexa_config = None self._google_config = None - self.cloud = None @property def base_path(self) -> Path: @@ -92,23 +91,22 @@ def alexa_config(self) -> alexa_config.AlexaConfig: return self._alexa_config - @property - def google_config(self) -> google_config.CloudGoogleConfig: + async def get_google_config(self) -> google_config.CloudGoogleConfig: """Return Google config.""" if not self._google_config: assert self.cloud is not None + + cloud_user = await self._prefs.get_cloud_user() + self._google_config = google_config.CloudGoogleConfig( - self._hass, self.google_user_config, self._prefs, self.cloud + self._hass, self.google_user_config, cloud_user, self._prefs, self.cloud ) return self._google_config - async def async_initialize(self, cloud) -> None: - """Initialize the client.""" - self.cloud = cloud - - if not self.cloud.is_logged_in: - return + async def logged_in(self) -> None: + """When user logs in.""" + await self.prefs.async_set_username(self.cloud.username) if self.alexa_config.enabled and self.alexa_config.should_report_state: try: @@ -116,14 +114,18 @@ async def async_initialize(self, cloud) -> None: except alexa_errors.NoTokenAvailable: pass - if self.google_config.enabled: - self.google_config.async_enable_local_sdk() + if self._prefs.google_enabled: + gconf = await self.get_google_config() - if self.google_config.should_report_state: - self.google_config.async_enable_report_state() + gconf.async_enable_local_sdk() + + if gconf.should_report_state: + gconf.async_enable_report_state() async def cleanups(self) -> None: """Cleanup some stuff after logout.""" + await self.prefs.async_set_username(None) + self._google_config = None @callback @@ -141,8 +143,13 @@ def dispatcher_message(self, identifier: str, data: Any = None) -> None: async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud alexa message to client.""" + cloud_user = await self._prefs.get_cloud_user() return await alexa_sh.async_handle_message( - self._hass, self.alexa_config, payload, enabled=self._prefs.alexa_enabled + self._hass, + self.alexa_config, + payload, + context=Context(user_id=cloud_user), + enabled=self._prefs.alexa_enabled, ) async def async_google_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: @@ -150,8 +157,10 @@ async def async_google_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: if not self._prefs.google_enabled: return ga.turned_off_response(payload) + gconf = await self.get_google_config() + return await ga.async_handle_message( - self._hass, self.google_config, self.prefs.cloud_user, payload + self._hass, gconf, gconf.cloud_user, payload ) async def async_webhook_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 9a2dccf8d7cb60..406263c85f8900 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -17,6 +17,7 @@ PREF_ALIASES = "aliases" PREF_SHOULD_EXPOSE = "should_expose" PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id" +PREF_USERNAME = "username" DEFAULT_SHOULD_EXPOSE = True DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 80d3fedba5b2a7..3d6511ffc3da5a 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -23,10 +23,11 @@ class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, hass, config, prefs, cloud): + def __init__(self, hass, config, cloud_user, prefs, cloud): """Initialize the Google config.""" super().__init__(hass) self._config = config + self._user = cloud_user self._prefs = prefs self._cloud = cloud self._cur_entity_prefs = self._prefs.google_entity_configs @@ -46,7 +47,7 @@ def enabled(self): @property def agent_user_id(self): """Return Agent User Id to use for query responses.""" - return self._cloud.claims["cognito:username"] + return self._cloud.username @property def entity_config(self): @@ -74,7 +75,12 @@ def local_sdk_webhook_id(self): @property def local_sdk_user_id(self): """Return the user ID to be used for actions received via the local SDK.""" - return self._prefs.cloud_user + return self._user + + @property + def cloud_user(self): + """Return Cloud User account.""" + return self._user def should_expose(self, state): """If a state object should be exposed.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index f795bc2b68c1e9..d808fe72d392fd 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -174,9 +174,8 @@ async def post(self, request): """Trigger a Google Actions sync.""" hass = request.app["hass"] cloud: Cloud = hass.data[DOMAIN] - status = await cloud.client.google_config.async_sync_entities( - cloud.client.google_config.agent_user_id - ) + gconf = await cloud.client.get_google_config() + status = await gconf.async_sync_entities(gconf.agent_user_id) return self.json({}, status_code=status) @@ -194,11 +193,7 @@ async def post(self, request, data): """Handle login request.""" hass = request.app["hass"] cloud = hass.data[DOMAIN] - - with async_timeout.timeout(REQUEST_TIMEOUT): - await hass.async_add_job(cloud.auth.login, data["email"], data["password"]) - - hass.async_add_job(cloud.iot.connect) + await cloud.login(data["email"], data["password"]) return self.json({"success": True}) @@ -479,7 +474,8 @@ async def websocket_remote_disconnect(hass, connection, msg): async def google_assistant_list(hass, connection, msg): """List all google assistant entities.""" cloud = hass.data[DOMAIN] - entities = google_helpers.async_get_entities(hass, cloud.client.google_config) + gconf = await cloud.client.get_google_config() + entities = google_helpers.async_get_entities(hass, gconf) result = [] diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 2feef55835ed99..ec9a556af0ac5c 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/integrations/cloud", - "requirements": ["hass-nabucasa==0.29"], + "requirements": ["hass-nabucasa==0.30"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 0599b00a8bd8ee..e96ee9527fb95c 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -1,7 +1,10 @@ """Preference management for cloud.""" from ipaddress import ip_address +from typing import Optional from homeassistant.core import callback +from homeassistant.auth.models import User +from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.util.logging import async_create_catching_coro from .const import ( @@ -19,6 +22,7 @@ PREF_SHOULD_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS, PREF_ALEXA_REPORT_STATE, + PREF_USERNAME, DEFAULT_ALEXA_REPORT_STATE, PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_LOCAL_WEBHOOK_ID, @@ -47,16 +51,7 @@ async def async_initialize(self): prefs = await self._store.async_load() if prefs is None: - prefs = { - PREF_ENABLE_ALEXA: True, - PREF_ENABLE_GOOGLE: True, - PREF_ENABLE_REMOTE: False, - PREF_GOOGLE_SECURE_DEVICES_PIN: None, - PREF_GOOGLE_ENTITY_CONFIGS: {}, - PREF_ALEXA_ENTITY_CONFIGS: {}, - PREF_CLOUDHOOKS: {}, - PREF_CLOUD_USER: None, - } + prefs = self._empty_config("") self._prefs = prefs @@ -166,6 +161,27 @@ async def async_update_alexa_entity_config( updated_entities = {**entities, entity_id: updated_entity} await self.async_update(alexa_entity_configs=updated_entities) + async def async_set_username(self, username): + """Set the username that is logged in.""" + # Logging out. + if username is None: + user = await self._load_cloud_user() + + if user is not None: + await self._hass.auth.async_remove_user(user) + await self._save_prefs({**self._prefs, PREF_CLOUD_USER: None}) + return + + cur_username = self._prefs.get(PREF_USERNAME) + + if cur_username == username: + return + + if cur_username is None: + await self._save_prefs({**self._prefs, PREF_USERNAME: username}) + else: + await self._save_prefs(self._empty_config(username)) + def as_dict(self): """Return dictionary version.""" return { @@ -178,7 +194,6 @@ def as_dict(self): PREF_ALEXA_REPORT_STATE: self.alexa_report_state, PREF_GOOGLE_REPORT_STATE: self.google_report_state, PREF_CLOUDHOOKS: self.cloudhooks, - PREF_CLOUD_USER: self.cloud_user, } @property @@ -239,10 +254,29 @@ def cloudhooks(self): """Return the published cloud webhooks.""" return self._prefs.get(PREF_CLOUDHOOKS, {}) - @property - def cloud_user(self) -> str: + async def get_cloud_user(self) -> str: """Return ID from Home Assistant Cloud system user.""" - return self._prefs.get(PREF_CLOUD_USER) + user = await self._load_cloud_user() + + if user: + return user.id + + user = await self._hass.auth.async_create_system_user( + "Home Assistant Cloud", [GROUP_ID_ADMIN] + ) + await self.async_update(cloud_user=user.id) + return user.id + + async def _load_cloud_user(self) -> Optional[User]: + """Load cloud user if available.""" + user_id = self._prefs.get(PREF_CLOUD_USER) + + if user_id is None: + return None + + # Fetch the user. It can happen that the user no longer exists if + # an image was restored without restoring the cloud prefs. + return await self._hass.auth.async_get_user(user_id) @property def _has_local_trusted_network(self) -> bool: @@ -283,3 +317,19 @@ async def _save_prefs(self, prefs): for listener in self._listeners: self._hass.async_create_task(async_create_catching_coro(listener(self))) + + @callback + def _empty_config(self, username): + """Return an empty config.""" + return { + PREF_ENABLE_ALEXA: True, + PREF_ENABLE_GOOGLE: True, + PREF_ENABLE_REMOTE: False, + PREF_GOOGLE_SECURE_DEVICES_PIN: None, + PREF_GOOGLE_ENTITY_CONFIGS: {}, + PREF_ALEXA_ENTITY_CONFIGS: {}, + PREF_CLOUDHOOKS: {}, + PREF_CLOUD_USER: None, + PREF_USERNAME: username, + PREF_GOOGLE_LOCAL_WEBHOOK_ID: self._hass.components.webhook.async_generate_id(), + } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1edc5184c02a7d..0e336835c4d073 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 -hass-nabucasa==0.29 +hass-nabucasa==0.30 home-assistant-frontend==20191119.6 importlib-metadata==0.23 jinja2>=2.10.3 diff --git a/requirements_all.txt b/requirements_all.txt index 7d0c08e6a28670..20eb48e3bdbc43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -629,7 +629,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.29 +hass-nabucasa==0.30 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d8d13ab8908f92..3b6b65d7e4d43b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -208,7 +208,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.29 +hass-nabucasa==0.30 # homeassistant.components.mqtt hbmqtt==0.9.5 diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 054b38daffc2ed..a9c4ade668d96d 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -7,6 +7,7 @@ from homeassistant.core import State from homeassistant.setup import async_setup_component from homeassistant.components.cloud import DOMAIN +from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE from tests.components.alexa import test_smart_home as test_alexa from tests.common import mock_coro @@ -187,25 +188,42 @@ async def test_google_config_expose_entity(hass, mock_cloud_setup, mock_cloud_lo """Test Google config exposing entity method uses latest config.""" cloud_client = hass.data[DOMAIN].client state = State("light.kitchen", "on") + gconf = await cloud_client.get_google_config() - assert cloud_client.google_config.should_expose(state) + assert gconf.should_expose(state) await cloud_client.prefs.async_update_google_entity_config( entity_id="light.kitchen", should_expose=False ) - assert not cloud_client.google_config.should_expose(state) + assert not gconf.should_expose(state) async def test_google_config_should_2fa(hass, mock_cloud_setup, mock_cloud_login): """Test Google config disabling 2FA method uses latest config.""" cloud_client = hass.data[DOMAIN].client + gconf = await cloud_client.get_google_config() state = State("light.kitchen", "on") - assert cloud_client.google_config.should_2fa(state) + assert gconf.should_2fa(state) await cloud_client.prefs.async_update_google_entity_config( entity_id="light.kitchen", disable_2fa=True ) - assert not cloud_client.google_config.should_2fa(state) + assert not gconf.should_2fa(state) + + +async def test_set_username(hass): + """Test we set username during loggin.""" + prefs = MagicMock( + alexa_enabled=False, + google_enabled=False, + async_set_username=MagicMock(return_value=mock_coro()), + ) + client = CloudClient(hass, prefs, None, {}, {}) + client.cloud = MagicMock(is_logged_in=True, username="mock-username") + await client.logged_in() + + assert len(prefs.async_set_username.mock_calls) == 1 + assert prefs.async_set_username.mock_calls[0][1][0] == "mock-username" diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 7ccb6a33336f23..0284a2c385134f 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -15,6 +15,7 @@ async def test_google_update_report_state(hass, cloud_prefs): config = CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), + "mock-user-id", cloud_prefs, Mock(claims={"cognito:username": "abcdefghjkl"}), ) @@ -37,6 +38,7 @@ async def test_sync_entities(aioclient_mock, hass, cloud_prefs): config = CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), + "mock-user-id", cloud_prefs, Mock( google_actions_sync_url="http://example.com", @@ -52,6 +54,7 @@ async def test_google_update_expose_trigger_sync(hass, cloud_prefs): config = CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), + "mock-user-id", cloud_prefs, Mock(claims={"cognito:username": "abcdefghjkl"}), ) @@ -90,7 +93,7 @@ async def test_google_update_expose_trigger_sync(hass, cloud_prefs): async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): """Test Google config responds to entity registry.""" config = CloudGoogleConfig( - hass, GACTIONS_SCHEMA({}), cloud_prefs, hass.data["cloud"] + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] ) with patch.object( diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 22d4ddd172ec9d..440ad7a9c89081 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -103,32 +103,18 @@ async def test_google_actions_sync_fails( assert req.status == 403 -async def test_login_view(hass, cloud_client, mock_cognito): +async def test_login_view(hass, cloud_client): """Test logging in.""" - mock_cognito.id_token = jwt.encode( - {"email": "hello@home-assistant.io", "custom:sub-exp": "2018-01-03"}, "test" - ) - mock_cognito.access_token = "access_token" - mock_cognito.refresh_token = "refresh_token" + hass.data["cloud"] = MagicMock(login=MagicMock(return_value=mock_coro())) - with patch("hass_nabucasa.iot.CloudIoT.connect") as mock_connect, patch( - "hass_nabucasa.auth.CognitoAuth._authenticate", return_value=mock_cognito - ) as mock_auth: - req = await cloud_client.post( - "/api/cloud/login", json={"email": "my_username", "password": "my_password"} - ) + req = await cloud_client.post( + "/api/cloud/login", json={"email": "my_username", "password": "my_password"} + ) assert req.status == 200 result = await req.json() assert result == {"success": True} - assert len(mock_connect.mock_calls) == 1 - - assert len(mock_auth.mock_calls) == 1 - result_user, result_pass = mock_auth.mock_calls[0][1] - assert result_user == "my_username" - assert result_pass == "my_password" - async def test_login_view_random_exception(cloud_client): """Try logging in with invalid JSON.""" @@ -351,7 +337,6 @@ async def test_websocket_status( "cloud": "connected", "prefs": { "alexa_enabled": True, - "cloud_user": None, "cloudhooks": {}, "google_enabled": True, "google_entity_configs": {}, diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index e160ea8826ab7e..d039cdd1b0b802 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -5,7 +5,6 @@ from homeassistant.core import Context from homeassistant.exceptions import Unauthorized -from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import cloud from homeassistant.components.cloud.const import DOMAIN from homeassistant.components.cloud.prefs import STORAGE_KEY @@ -142,68 +141,11 @@ async def test_setup_existing_cloud_user(hass, hass_storage): assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] == user.id -async def test_setup_invalid_cloud_user(hass, hass_storage): - """Test setup with API push default data.""" - hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": "non-existing"}} - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): - result = await async_setup_component( - hass, - "cloud", - { - "http": {}, - "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, - "cognito_client_id": "test-cognito_client_id", - "user_pool_id": "test-user_pool_id", - "region": "test-region", - "relayer": "test-relayer", - }, - }, - ) - assert result - - assert hass_storage[STORAGE_KEY]["data"]["cloud_user"] != "non-existing" - cloud_user = await hass.auth.async_get_user( - hass_storage[STORAGE_KEY]["data"]["cloud_user"] - ) - - assert cloud_user - assert cloud_user.groups[0].id == GROUP_ID_ADMIN - - -async def test_setup_setup_cloud_user(hass, hass_storage): - """Test setup with API push default data.""" - hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": None}} - with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): - result = await async_setup_component( - hass, - "cloud", - { - "http": {}, - "cloud": { - cloud.CONF_MODE: cloud.MODE_DEV, - "cognito_client_id": "test-cognito_client_id", - "user_pool_id": "test-user_pool_id", - "region": "test-region", - "relayer": "test-relayer", - }, - }, - ) - assert result - - cloud_user = await hass.auth.async_get_user( - hass_storage[STORAGE_KEY]["data"]["cloud_user"] - ) - - assert cloud_user - assert cloud_user.groups[0].id == GROUP_ID_ADMIN - - async def test_on_connect(hass, mock_cloud_fixture): """Test cloud on connect triggers.""" cl = hass.data["cloud"] - assert len(cl.iot._on_connect) == 4 + assert len(cl.iot._on_connect) == 3 assert len(hass.states.async_entity_ids("binary_sensor")) == 0 diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py new file mode 100644 index 00000000000000..1678757e52cf87 --- /dev/null +++ b/tests/components/cloud/test_prefs.py @@ -0,0 +1,80 @@ +"""Test Cloud preferences.""" +from unittest.mock import patch + +from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.components.cloud.prefs import CloudPreferences, STORAGE_KEY + + +async def test_set_username(hass): + """Test we clear config if we set different username.""" + prefs = CloudPreferences(hass) + await prefs.async_initialize() + + assert prefs.google_enabled + + await prefs.async_update(google_enabled=False) + + assert not prefs.google_enabled + + await prefs.async_set_username("new-username") + + assert prefs.google_enabled + + +async def test_set_username_migration(hass): + """Test we not clear config if we had no username.""" + prefs = CloudPreferences(hass) + + with patch.object(prefs, "_empty_config", return_value=prefs._empty_config(None)): + await prefs.async_initialize() + + assert prefs.google_enabled + + await prefs.async_update(google_enabled=False) + + assert not prefs.google_enabled + + await prefs.async_set_username("new-username") + + assert not prefs.google_enabled + + +async def test_load_invalid_cloud_user(hass, hass_storage): + """Test loading cloud user with invalid storage.""" + hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": "non-existing"}} + + prefs = CloudPreferences(hass) + await prefs.async_initialize() + + cloud_user_id = await prefs.get_cloud_user() + + assert cloud_user_id != "non-existing" + + cloud_user = await hass.auth.async_get_user( + hass_storage[STORAGE_KEY]["data"]["cloud_user"] + ) + + assert cloud_user + assert cloud_user.groups[0].id == GROUP_ID_ADMIN + + +async def test_setup_remove_cloud_user(hass, hass_storage): + """Test creating and removing cloud user.""" + hass_storage[STORAGE_KEY] = {"version": 1, "data": {"cloud_user": None}} + + prefs = CloudPreferences(hass) + await prefs.async_initialize() + await prefs.async_set_username("user1") + + cloud_user = await hass.auth.async_get_user(await prefs.get_cloud_user()) + + assert cloud_user + assert cloud_user.groups[0].id == GROUP_ID_ADMIN + + await prefs.async_set_username("user2") + + cloud_user2 = await hass.auth.async_get_user(await prefs.get_cloud_user()) + + assert cloud_user2 + assert cloud_user2.groups[0].id == GROUP_ID_ADMIN + assert cloud_user2.id != cloud_user.id From daed31458538fb0b022903f6983cfcdbda8f584b Mon Sep 17 00:00:00 2001 From: Wim Haanstra Date: Thu, 28 Nov 2019 20:30:24 +0100 Subject: [PATCH 1874/3953] Dsmr reader (#28701) * Added DSMR Reader platform - Added DSMR Reader platform - Updated codeowners for other components I added earlier * Move sensor definitions to new file * Sensor definitions in new file * Add energy prices from MQTT * lint fixes * Black formatted some files * Update .coveragerc * Support transform methods on definitions * Manifest mangled by Black, fixed * Visual studio code reformatting dictionary error * Fix issues with code, remove unrelated manifest changes * Update CODEOWNERS --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/dsmr_reader/__init__.py | 1 + .../components/dsmr_reader/definitions.py | 243 ++++++++++++++++++ .../components/dsmr_reader/manifest.json | 12 + .../components/dsmr_reader/sensor.py | 87 +++++++ 6 files changed, 345 insertions(+) create mode 100755 homeassistant/components/dsmr_reader/__init__.py create mode 100644 homeassistant/components/dsmr_reader/definitions.py create mode 100755 homeassistant/components/dsmr_reader/manifest.json create mode 100755 homeassistant/components/dsmr_reader/sensor.py diff --git a/.coveragerc b/.coveragerc index 74fb5c11cece7d..33cbb0ed450733 100644 --- a/.coveragerc +++ b/.coveragerc @@ -163,6 +163,7 @@ omit = homeassistant/components/doorbird/* homeassistant/components/dovado/* homeassistant/components/downloader/* + homeassistant/components/dsmr_reader/* homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/duke_energy/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 74916740253b13..d7155e8f91796b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -78,6 +78,7 @@ homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/digital_ocean/* @fabaff homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 +homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dweet/* @fabaff homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT diff --git a/homeassistant/components/dsmr_reader/__init__.py b/homeassistant/components/dsmr_reader/__init__.py new file mode 100755 index 00000000000000..946be91d1a5611 --- /dev/null +++ b/homeassistant/components/dsmr_reader/__init__.py @@ -0,0 +1 @@ +"""The DSMR Reader component.""" diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py new file mode 100644 index 00000000000000..f4a36ebc34682c --- /dev/null +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -0,0 +1,243 @@ +"""Definitions for DSMR Reader sensors added to MQTT.""" + + +def dsmr_transform(value): + """Transform DSMR version value to right format.""" + return float(value) / 10 + + +def tariff_transform(value): + """Transform tariff from number to description.""" + if value == "1": + return "low" + return "high" + + +DEFINITIONS = { + "dsmr/reading/electricity_delivered_1": { + "name": "Low tariff usage", + "icon": "mdi:flash", + "unit": "kWh", + }, + "dsmr/reading/electricity_returned_1": { + "name": "Low tariff returned", + "icon": "mdi:flash-outline", + "unit": "kWh", + }, + "dsmr/reading/electricity_delivered_2": { + "name": "High tariff usage", + "icon": "mdi:flash", + "unit": "kWh", + }, + "dsmr/reading/electricity_returned_2": { + "name": "High tariff returned", + "icon": "mdi:flash-outline", + "unit": "kWh", + }, + "dsmr/reading/electricity_currently_delivered": { + "name": "Current power usage", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/electricity_currently_returned": { + "name": "Current power return", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/phase_currently_delivered_l1": { + "name": "Current power usage L1", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/phase_currently_delivered_l2": { + "name": "Current power usage L2", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/phase_currently_delivered_l3": { + "name": "Current power usage L3", + "icon": "mdi:flash", + "unit": "kW", + }, + "dsmr/reading/phase_currently_returned_l1": { + "name": "Current power return L1", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/phase_currently_returned_l2": { + "name": "Current power return L2", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/phase_currently_returned_l3": { + "name": "Current power return L3", + "icon": "mdi:flash-outline", + "unit": "kW", + }, + "dsmr/reading/extra_device_delivered": { + "name": "Gas meter usage", + "icon": "mdi:fire", + "unit": "m3", + }, + "dsmr/reading/phase_voltage_l1": { + "name": "Current voltage L1", + "icon": "mdi:flash", + "unit": "V", + }, + "dsmr/reading/phase_voltage_l2": { + "name": "Current voltage L2", + "icon": "mdi:flash", + "unit": "V", + }, + "dsmr/reading/phase_voltage_l3": { + "name": "Current voltage L3", + "icon": "mdi:flash", + "unit": "V", + }, + "dsmr/consumption/gas/delivered": { + "name": "Gas usage", + "icon": "mdi:fire", + "unit": "m3", + }, + "dsmr/consumption/gas/currently_delivered": { + "name": "Current gas usage", + "icon": "mdi:fire", + "unit": "m3", + }, + "dsmr/consumption/gas/read_at": { + "name": "Gas meter read", + "icon": "mdi:clock", + "unit": "", + }, + "dsmr/day-consumption/electricity1": { + "name": "Low tariff usage", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity2": { + "name": "High tariff usage", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity1_returned": { + "name": "Low tariff return", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity2_returned": { + "name": "High tariff return", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity_merged": { + "name": "Power usage total", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity_returned_merged": { + "name": "Power return total", + "icon": "mdi:counter", + "unit": "kWh", + }, + "dsmr/day-consumption/electricity1_cost": { + "name": "Low tariff cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/electricity2_cost": { + "name": "High tariff cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/electricity_cost_merged": { + "name": "Power total cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/gas": { + "name": "Gas usage", + "icon": "mdi:counter", + "unit": "m3", + }, + "dsmr/day-consumption/gas_cost": { + "name": "Gas cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/total_cost": { + "name": "Total cost", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_delivered_1": { + "name": "Low tariff delivered price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_delivered_2": { + "name": "High tariff delivered price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_returned_1": { + "name": "Low tariff returned price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_electricity_returned_2": { + "name": "High tariff returned price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/day-consumption/energy_supplier_price_gas": { + "name": "Gas price", + "icon": "mdi:currency-eur", + "unit": "€", + }, + "dsmr/meter-stats/dsmr_version": { + "name": "DSMR version", + "icon": "mdi:alert-circle", + "transform": dsmr_transform, + }, + "dsmr/meter-stats/electricity_tariff": { + "name": "Electricity tariff", + "icon": "mdi:flash", + "transform": tariff_transform, + }, + "dsmr/meter-stats/power_failure_count": { + "name": "Power failure count", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/long_power_failure_count": { + "name": "Long power failure count", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_sag_count_l1": { + "name": "Voltage sag L1", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_sag_count_l2": { + "name": "Voltage sag L2", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_sag_count_l3": { + "name": "Voltage sag L3", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_swell_count_l1": { + "name": "Voltage swell L1", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_swell_count_l2": { + "name": "Voltage swell L2", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/voltage_swell_count_l3": { + "name": "Voltage swell L3", + "icon": "mdi:flash", + }, + "dsmr/meter-stats/rejected_telegrams": { + "name": "Rejected telegrams", + "icon": "mdi:flash", + }, +} diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json new file mode 100755 index 00000000000000..aa108d8bf9aafd --- /dev/null +++ b/homeassistant/components/dsmr_reader/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "dsmr_reader", + "name": "DSMR Reader", + "documentation": "https://www.home-assistant.io/components/dsmr_reader", + "requirements": [], + "dependencies": [ + "mqtt" + ], + "codeowners": [ + "@depl0y" + ] +} diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py new file mode 100755 index 00000000000000..c7efba9ddde283 --- /dev/null +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -0,0 +1,87 @@ +"""Support for DSMR Reader through MQTT.""" +import logging + +from homeassistant.components import mqtt +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify + +from .definitions import DEFINITIONS + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "dsmr_reader" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up DSMR Reader sensors.""" + + sensors = [] + for topic in DEFINITIONS: + sensors.append(DSMRSensor(topic)) + + async_add_entities(sensors) + + +class DSMRSensor(Entity): + """Representation of a DSMR sensor that is updated via MQTT.""" + + def __init__(self, topic): + """Initialize the sensor.""" + + self._definition = DEFINITIONS[topic] + + self._entity_id = slugify(topic.replace("/", "_")) + self._topic = topic + + self._name = self._definition["name"] + self._unit_of_measurement = ( + self._definition["unit"] if "unit" in self._definition else "" + ) + self._icon = self._definition["icon"] if "icon" in self._definition else None + self._transform = ( + self._definition["transform"] if "transform" in self._definition else None + ) + + self._state = None + + async def async_added_to_hass(self): + """Subscribe to MQTT events.""" + + @callback + def message_received(message): + """Handle new MQTT messages.""" + + if self._transform is not None: + self._state = self._transform(message.payload) + else: + self._state = message.payload + + self.async_schedule_update_ha_state() + + return await mqtt.async_subscribe(self.hass, self._topic, message_received, 1) + + @property + def name(self): + """Return the name of the sensor supplied in constructor.""" + return self._name + + @property + def entity_id(self): + """Return the entity ID for this sensor.""" + return f"sensor.{self._entity_id}" + + @property + def state(self): + """Return the current state of the entity.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of this sensor.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon of this sensor.""" + return self._icon From 26e674b4c30bf2d791c638b5aaf2ddd3cd6f672e Mon Sep 17 00:00:00 2001 From: guillempages Date: Thu, 28 Nov 2019 21:14:20 +0100 Subject: [PATCH 1875/3953] Resolve hosts for fritzbox_callmonitor (#28761) * Resolve hosts for fritzbox_callmonitor If the configuration supplied "host" is not an IP address, try resolving it * always use gethostbyname Instead of just checking whether it is an IP and if it isn't try to resolve; just resolve it; IPs will be returned unchanged, and hostnames will be resolved. * Catch error if the hostname cannot be resolved * Don't fallback to default host If the hostname cannot be resolved; don't try to fallback; just print the error message. * Fail setup if hostname cannot be resolved If the hostname cannot be resolved, log an error and stop the setup; no entities will be then created. --- homeassistant/components/fritzbox_callmonitor/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index b1d601ce382287..600420db8591c5 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -60,6 +60,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Fritz!Box call monitor sensor platform.""" name = config.get(CONF_NAME) host = config.get(CONF_HOST) + # Try to resolve a hostname; if it is already an IP, it will be returned as-is + try: + host = socket.gethostbyname(host) + except socket.error: + _LOGGER.error("Could not resolve hostname %s", host) + return port = config.get(CONF_PORT) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) From 103b917bb21b8c32d2c3daa04fadb28fb7e645a6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 28 Nov 2019 23:20:00 +0100 Subject: [PATCH 1876/3953] Upgrade psutil to 5.6.6 (#29192) --- homeassistant/components/systemmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 67677bc2572629..9d03e287d13457 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Systemmonitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "requirements": [ - "psutil==5.6.5" + "psutil==5.6.6" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 20eb48e3bdbc43..3ed99608946267 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1027,7 +1027,7 @@ protobuf==3.6.1 proxmoxer==1.0.3 # homeassistant.components.systemmonitor -psutil==5.6.5 +psutil==5.6.6 # homeassistant.components.ptvsd ptvsd==4.2.8 From 8dc366c75b7fa3c15c0391a06e5ca57cfa62795b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 29 Nov 2019 00:32:07 +0000 Subject: [PATCH 1877/3953] [ci skip] Translation update --- .../components/almond/.translations/nn.json | 5 +++ .../geonetnz_volcano/.translations/sl.json | 16 +++++++ .../components/glances/.translations/nn.json | 5 +++ .../huawei_lte/.translations/nn.json | 5 +++ .../iaqualink/.translations/lb.json | 2 +- .../lutron_caseta/.translations/lb.json | 5 +++ .../lutron_caseta/.translations/sl.json | 5 +++ .../lutron_caseta/.translations/zh-Hant.json | 5 +++ .../components/solarlog/.translations/nn.json | 5 +++ .../components/starline/.translations/lb.json | 42 +++++++++++++++++++ .../components/starline/.translations/nn.json | 10 +++++ .../components/starline/.translations/sl.json | 10 +++++ .../starline/.translations/zh-Hant.json | 42 +++++++++++++++++++ .../components/vesync/.translations/lb.json | 2 +- .../components/wled/.translations/nn.json | 6 +++ .../components/zha/.translations/nn.json | 5 +++ 16 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/almond/.translations/nn.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/sl.json create mode 100644 homeassistant/components/glances/.translations/nn.json create mode 100644 homeassistant/components/huawei_lte/.translations/nn.json create mode 100644 homeassistant/components/lutron_caseta/.translations/lb.json create mode 100644 homeassistant/components/lutron_caseta/.translations/sl.json create mode 100644 homeassistant/components/lutron_caseta/.translations/zh-Hant.json create mode 100644 homeassistant/components/solarlog/.translations/nn.json create mode 100644 homeassistant/components/starline/.translations/lb.json create mode 100644 homeassistant/components/starline/.translations/nn.json create mode 100644 homeassistant/components/starline/.translations/zh-Hant.json create mode 100644 homeassistant/components/wled/.translations/nn.json diff --git a/homeassistant/components/almond/.translations/nn.json b/homeassistant/components/almond/.translations/nn.json new file mode 100644 index 00000000000000..a25f5dc157451a --- /dev/null +++ b/homeassistant/components/almond/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/sl.json b/homeassistant/components/geonetnz_volcano/.translations/sl.json new file mode 100644 index 00000000000000..e31f473c26f2df --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokacija je \u017ee registrirana" + }, + "step": { + "user": { + "data": { + "radius": "Radij" + }, + "title": "Izpolnite podrobnosti filtra." + } + }, + "title": "GeoNet NZ vulkan" + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/nn.json b/homeassistant/components/glances/.translations/nn.json new file mode 100644 index 00000000000000..2c9acc227bd900 --- /dev/null +++ b/homeassistant/components/glances/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Glances" + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/nn.json b/homeassistant/components/huawei_lte/.translations/nn.json new file mode 100644 index 00000000000000..1a5c63f10f8634 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Huawei LTE" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/lb.json b/homeassistant/components/iaqualink/.translations/lb.json index 4beb11214bc2c8..db8d67eea75549 100644 --- a/homeassistant/components/iaqualink/.translations/lb.json +++ b/homeassistant/components/iaqualink/.translations/lb.json @@ -12,7 +12,7 @@ "password": "Passwuert", "username": "Benotzernumm / E-Mail Adresse" }, - "description": "Gitt den Benotznumm an d'Passwuert fir \u00e4ren iAqualink Kont un.", + "description": "Gitt den Benotzernumm an d'Passwuert fir \u00e4ren iAqualink Kont un.", "title": "Mat iAqualink verbannen" } }, diff --git a/homeassistant/components/lutron_caseta/.translations/lb.json b/homeassistant/components/lutron_caseta/.translations/lb.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/lb.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/sl.json b/homeassistant/components/lutron_caseta/.translations/sl.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/sl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/zh-Hant.json b/homeassistant/components/lutron_caseta/.translations/zh-Hant.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/zh-Hant.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/nn.json b/homeassistant/components/solarlog/.translations/nn.json new file mode 100644 index 00000000000000..3ce86b4e10a952 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/lb.json b/homeassistant/components/starline/.translations/lb.json new file mode 100644 index 00000000000000..527add9920b903 --- /dev/null +++ b/homeassistant/components/starline/.translations/lb.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Ong\u00ebltege Applikatioun's ID oder Schl\u00ebssel", + "error_auth_mfa": "Ong\u00ebltegte Code", + "error_auth_user": "Ong\u00ebltege Benotzernumm oder Passwuert" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App ID", + "app_secret": "Schl\u00ebssel" + }, + "description": "Applikatioun's ID an Schl\u00ebssel vum StarLine Developpeur's Kont", + "title": "Login Informatioune vun der Applikatioun" + }, + "auth_captcha": { + "data": { + "captcha_code": "Cod vum Bild" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS Code" + }, + "description": "Gitt de Code an deen un d'Telefondnummer {phone_number} gesch\u00e9ckt gouf", + "title": "2-Faktor-Authentifikatioun" + }, + "auth_user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "description": "StarLine Konto Email a Passwuert", + "title": "Login Informatiounen" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/nn.json b/homeassistant/components/starline/.translations/nn.json new file mode 100644 index 00000000000000..88b146144fbc2e --- /dev/null +++ b/homeassistant/components/starline/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "auth_captcha": { + "description": "{captcha_img}" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/sl.json b/homeassistant/components/starline/.translations/sl.json index 2cc83f1227a1b8..3cdc5bc4bac9f9 100644 --- a/homeassistant/components/starline/.translations/sl.json +++ b/homeassistant/components/starline/.translations/sl.json @@ -1,7 +1,17 @@ { "config": { + "error": { + "error_auth_app": "Nepravilen ID ali skrivnost", + "error_auth_mfa": "Napa\u010dna koda", + "error_auth_user": "Nepravilno uporabni\u0161ko ime ali geslo" + }, "step": { "auth_app": { + "data": { + "app_id": "ID aplikacije", + "app_secret": "Skrivnost" + }, + "description": "ID aplikacije in tajna koda iz ra\u010duna za razvijalce StarLine ", "title": "Poverilnice programa" }, "auth_captcha": { diff --git a/homeassistant/components/starline/.translations/zh-Hant.json b/homeassistant/components/starline/.translations/zh-Hant.json new file mode 100644 index 00000000000000..0bd69d54ec6a66 --- /dev/null +++ b/homeassistant/components/starline/.translations/zh-Hant.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "\u61c9\u7528\u7a0b\u5f0f ID \u932f\u8aa4\u6216\u4e0d\u6b63\u78ba", + "error_auth_mfa": "\u5bc6\u78bc\u932f\u8aa4", + "error_auth_user": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App ID", + "app_secret": "\u5bc6\u78bc" + }, + "description": "Application ID and secret code \u7531 StarLine \u958b\u767c\u8005\u5e33\u865f \u6240\u53d6\u5f97\u7684\u61c9\u7528\u7a0b\u5f0f ID \u8207\u5bc6\u78bc", + "title": "\u61c9\u7528\u6191\u8b49" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u5716\u50cf\u5bc6\u78bc" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "\u7c21\u8a0a\u5bc6\u78bc" + }, + "description": "\u8f38\u5165\u50b3\u9001\u81f3 {phone_number} \u7684\u9a57\u8b49\u78bc", + "title": "\u5169\u968e\u6bb5\u8a8d\u8b49" + }, + "auth_user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "StarLine \u5e33\u865f\u90f5\u4ef6\u8207\u5bc6\u78bc", + "title": "\u4f7f\u7528\u8005\u6191\u8b49" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/lb.json b/homeassistant/components/vesync/.translations/lb.json index cfccd8b1dbb49a..0825bd0805d6f8 100644 --- a/homeassistant/components/vesync/.translations/lb.json +++ b/homeassistant/components/vesync/.translations/lb.json @@ -12,7 +12,7 @@ "password": "Passwuert", "username": "E-Mail Adresse" }, - "title": "Benotznumm a Passwuert aginn" + "title": "Benotzernumm a Passwuert aginn" } }, "title": "VeSync" diff --git a/homeassistant/components/wled/.translations/nn.json b/homeassistant/components/wled/.translations/nn.json new file mode 100644 index 00000000000000..f50a24eeac06b8 --- /dev/null +++ b/homeassistant/components/wled/.translations/nn.json @@ -0,0 +1,6 @@ +{ + "config": { + "flow_title": "WLED: {name}", + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nn.json b/homeassistant/components/zha/.translations/nn.json index ad2c240baf18f2..392018bb1f1c66 100644 --- a/homeassistant/components/zha/.translations/nn.json +++ b/homeassistant/components/zha/.translations/nn.json @@ -6,5 +6,10 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Squawk" + } } } \ No newline at end of file From 430061a159e29931d4e154ef215b0ca42905738d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 01:53:49 +0100 Subject: [PATCH 1878/3953] Move imports to top for usgs_earthquakes_feed (#29202) --- .../components/usgs_earthquakes_feed/geo_location.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 7890243c1e0acc..37934db30525cb 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -3,6 +3,9 @@ import logging from typing import Optional +from geojson_client.usgs_earthquake_hazards_program_feed import ( + UsgsEarthquakeHazardsProgramFeedManager, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -122,9 +125,6 @@ def __init__( minimum_magnitude, ): """Initialize the Feed Entity Manager.""" - from geojson_client.usgs_earthquake_hazards_program_feed import ( - UsgsEarthquakeHazardsProgramFeedManager, - ) self._hass = hass self._feed_manager = UsgsEarthquakeHazardsProgramFeedManager( From e1fece48e1147da5d76fc4ee883e878bdccdad84 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 01:54:42 +0100 Subject: [PATCH 1879/3953] Move imports to top for syncthru (#29206) --- homeassistant/components/syncthru/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 1258732223b01c..e981154a81ac01 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -1,13 +1,15 @@ """Support for Samsung Printers with SyncThru web interface.""" import logging + +from pysyncthru import SyncThru import voluptuous as vol -from homeassistant.const import CONF_RESOURCE, CONF_HOST, CONF_NAME +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_RESOURCE from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -33,7 +35,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SyncThru component.""" - from pysyncthru import SyncThru if discovery_info is not None: _LOGGER.info( From 5001dbdcfafc18ed3178ce3993e0985c897a74fd Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 01:55:52 +0100 Subject: [PATCH 1880/3953] Move imports to top for tapsaff (#29205) --- homeassistant/components/tapsaff/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index fe6b01ced4e170..e54bc7298b0a79 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from tapsaff import TapsAff import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -62,7 +63,6 @@ class TapsAffData: def __init__(self, location): """Initialize the data object.""" - from tapsaff import TapsAff self._is_taps_aff = None self.taps_aff = TapsAff(location) From 7b33e57e8668126ebe8a22b227bb43f39d4ecc78 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 01:56:39 +0100 Subject: [PATCH 1881/3953] Move imports to top for tautulli (#29204) --- homeassistant/components/tautulli/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 14b67838906289..b800bf6af1e0c1 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pytautulli import Tautulli import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -10,10 +11,10 @@ CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, + CONF_PATH, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, - CONF_PATH, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -50,7 +51,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the Tautulli sensor.""" - from pytautulli import Tautulli name = config.get(CONF_NAME) host = config[CONF_HOST] From 0e4920d6a14d8d9cbc37dc6f90922f5e3b752dea Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 01:58:56 +0100 Subject: [PATCH 1882/3953] Move imports to top for tof (#29203) * Move imports to top for tof * Isorted imports --- homeassistant/components/tof/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tof/sensor.py b/homeassistant/components/tof/sensor.py index d9e85b1e22b11d..58f50f4899ecfc 100644 --- a/homeassistant/components/tof/sensor.py +++ b/homeassistant/components/tof/sensor.py @@ -1,15 +1,16 @@ """Platform for Time of Flight sensor VL53L1X from STMicroelectronics.""" import asyncio -import logging from functools import partial +import logging +from VL53L1X2 import VL53L1X # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.components import rpi_gpio +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,6 @@ def init_tof_1(xshut): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Reset and initialize the VL53L1X ToF Sensor from STMicroelectronics.""" - from VL53L1X2 import VL53L1X # pylint: disable=import-error name = config.get(CONF_NAME) bus_number = config.get(CONF_I2C_BUS) From 243e80459dc11fc33cfa15b9f4f82b22297ee379 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 01:59:48 +0100 Subject: [PATCH 1883/3953] Move imports to top for volvooncall (#29201) --- homeassistant/components/volvooncall/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index c41c72020c42aa..c621a12943b8cb 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -1,10 +1,10 @@ """Support for Volvo On Call.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from volvooncall import Connection -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, @@ -14,6 +14,7 @@ ) from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -115,8 +116,6 @@ async def async_setup(hass, config): """Set up the Volvo On Call component.""" session = async_get_clientsession(hass) - from volvooncall import Connection - connection = Connection( session=session, username=config[DOMAIN].get(CONF_USERNAME), From 25ad933d2d2409e6c642f4e47aba195f898c7bc9 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 02:00:40 +0100 Subject: [PATCH 1884/3953] Move imports to top for vultr (#29200) --- homeassistant/components/vultr/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vultr/__init__.py b/homeassistant/components/vultr/__init__.py index 50e77c01c43663..9b26c4a75b3d2e 100644 --- a/homeassistant/components/vultr/__init__.py +++ b/homeassistant/components/vultr/__init__.py @@ -1,12 +1,13 @@ """Support for Vultr.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from vultr import Vultr as VultrAPI from homeassistant.const import CONF_API_KEY -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -69,7 +70,6 @@ class Vultr: def __init__(self, api_key): """Initialize the Vultr connection.""" - from vultr import Vultr as VultrAPI self._api_key = api_key self.data = None From 0d30a6bcac8eb33ad513fa1425c021fbb3d82f0b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 29 Nov 2019 02:02:01 +0100 Subject: [PATCH 1885/3953] Upgrade youtube_dl to 2019.11.28 (#29199) --- 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 5c5aaa0ce22816..16f491f0caefd3 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/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.11.22" + "youtube_dl==2019.11.28" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 3ed99608946267..650de67eee5ccd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2067,7 +2067,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.11.22 +youtube_dl==2019.11.28 # homeassistant.components.zengge zengge==0.2 From 66aae61fd5bfde385321d6d6dfba3f6831f11d9c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 02:03:14 +0100 Subject: [PATCH 1886/3953] Move imports to top for wirelesstag (#29198) --- homeassistant/components/wirelesstag/__init__.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 5e0da881076f65..1bc971f1372794 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -1,18 +1,20 @@ """Support for Wireless Sensor Tags.""" import logging -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol +from wirelesstagpy import NotificationConfig as NC + +from homeassistant import util from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, - CONF_USERNAME, CONF_PASSWORD, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv -from homeassistant import util -from homeassistant.helpers.entity import Entity from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -96,7 +98,6 @@ def make_notifications(self, binary_sensors, mac): configs.extend(bi_sensor.event.build_notifications(bi_url, mac)) update_url = self.update_callback_url - from wirelesstagpy import NotificationConfig as NC update_config = NC.make_config_for_update_event(update_url, mac) From fe34ea18655434c3e75ac7ded8bfe1d919c4a42e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 02:07:52 +0100 Subject: [PATCH 1887/3953] Move imports to top for zabbix (#29195) --- homeassistant/components/zabbix/__init__.py | 6 +++--- homeassistant/components/zabbix/sensor.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zabbix/__init__.py b/homeassistant/components/zabbix/__init__.py index f1c5dbffead3fb..0926f35af383c0 100644 --- a/homeassistant/components/zabbix/__init__.py +++ b/homeassistant/components/zabbix/__init__.py @@ -2,13 +2,14 @@ import logging from urllib.parse import urljoin +from pyzabbix import ZabbixAPI, ZabbixAPIException import voluptuous as vol from homeassistant.const import ( - CONF_PATH, CONF_HOST, - CONF_SSL, CONF_PASSWORD, + CONF_PATH, + CONF_SSL, CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv @@ -37,7 +38,6 @@ def setup(hass, config): """Set up the Zabbix component.""" - from pyzabbix import ZabbixAPI, ZabbixAPIException conf = config[DOMAIN] if conf[CONF_SSL]: diff --git a/homeassistant/components/zabbix/sensor.py b/homeassistant/components/zabbix/sensor.py index 6b2c06eab2f4a8..3fa29a0789688b 100644 --- a/homeassistant/components/zabbix/sensor.py +++ b/homeassistant/components/zabbix/sensor.py @@ -4,9 +4,9 @@ import voluptuous as vol from homeassistant.components import zabbix -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) From e5aa050def978647d1b481f200d7848f5994d2db Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 02:09:03 +0100 Subject: [PATCH 1888/3953] Move imports to top for zhong_hong (#29194) --- homeassistant/components/zhong_hong/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index f1a363cfedecd5..b94e19d6dbdb10 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -2,6 +2,8 @@ import logging import voluptuous as vol +from zhong_hong_hvac.hub import ZhongHongGateway +from zhong_hong_hvac.hvac import HVAC as ZhongHongHVAC from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( @@ -71,7 +73,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ZhongHong HVAC platform.""" - from zhong_hong_hvac.hub import ZhongHongGateway host = config.get(CONF_HOST) port = config.get(CONF_PORT) @@ -117,9 +118,8 @@ class ZhongHongClimate(ClimateDevice): def __init__(self, hub, addr_out, addr_in): """Set up the ZhongHong climate devices.""" - from zhong_hong_hvac.hvac import HVAC - self._device = HVAC(hub, addr_out, addr_in) + self._device = ZhongHongHVAC(hub, addr_out, addr_in) self._hub = hub self._current_operation = None self._current_temperature = None From 1c824f5ca7f3e2e1e422ab36547a084bb89bbd23 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 02:10:08 +0100 Subject: [PATCH 1889/3953] Move imports to top for ziggo_mediabox_xl (#29193) --- homeassistant/components/ziggo_mediabox_xl/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index a5f8b38ac3775a..83a7dbbaba9233 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -3,8 +3,9 @@ import socket import voluptuous as vol +from ziggo_mediabox_xl import ZiggoMediaboxXL -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -44,7 +45,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Ziggo Mediabox XL platform.""" - from ziggo_mediabox_xl import ZiggoMediaboxXL hass.data[DATA_KNOWN_DEVICES] = known_devices = set() From 1322661ee0d16b2dd0dbf7e30c4925ff6dd04667 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Thu, 28 Nov 2019 22:13:21 -0500 Subject: [PATCH 1890/3953] Handle None when trucating long Environment Canada state values (#29208) * Handle None when trucating long state values, add info message * Black --- homeassistant/components/environment_canada/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index a140927c980783..b3e4d7ae3df668 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -134,8 +134,11 @@ def update(self): ) elif self.sensor_type == "tendency": self._state = str(value).capitalize() - else: + elif value is not None and len(value) > 255: self._state = value[:255] + _LOGGER.info("Value for %s truncated to 255 characters", self._unique_id) + else: + self._state = value if sensor_data.get("unit") == "C" or self.sensor_type in [ "wind_chill", From e63bca4f326dd9d29a84111cf3f56876166bf370 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 08:09:10 +0100 Subject: [PATCH 1891/3953] Move imports to top for xiaomi_aqara (#29196) --- homeassistant/components/xiaomi_aqara/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 7a337dcc497a81..ae032a8b35f2ab 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -1,9 +1,9 @@ """Support for Xiaomi Gateways.""" -import logging - from datetime import timedelta +import logging import voluptuous as vol +from xiaomi_gateway import XiaomiGatewayDiscovery from homeassistant.components.discovery import SERVICE_XIAOMI_GW from homeassistant.const import ( @@ -135,8 +135,6 @@ async def xiaomi_gw_discovered(service, discovery_info): discovery.listen(hass, SERVICE_XIAOMI_GW, xiaomi_gw_discovered) - from xiaomi_gateway import XiaomiGatewayDiscovery - xiaomi = hass.data[PY_XIAOMI_GATEWAY] = XiaomiGatewayDiscovery( hass.add_job, gateways, interface ) From 364d1cff401452a58c4ede8e2c4540485c5d2e65 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 08:09:36 +0100 Subject: [PATCH 1892/3953] Move imports to top for xiaomi (#29197) --- homeassistant/components/xiaomi/camera.py | 12 ++++++------ homeassistant/components/xiaomi/device_tracker.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 363c17fe4a9cc5..cc85f17146b25f 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -1,20 +1,23 @@ """This component provides support for Xiaomi Cameras.""" import asyncio +from ftplib import FTP, error_perm import logging +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA -from homeassistant.exceptions import TemplateError +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PASSWORD, + CONF_PATH, CONF_PORT, CONF_USERNAME, ) +from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream @@ -88,7 +91,6 @@ def model(self): def get_latest_video_url(self, host): """Retrieve the latest video file from the Xiaomi Camera FTP server.""" - from ftplib import FTP, error_perm ftp = FTP(host) try: @@ -140,7 +142,6 @@ def get_latest_video_url(self, host): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG try: host = self.host.async_render() @@ -162,7 +163,6 @@ async def async_camera_image(self): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) await stream.open_camera(self._last_url, extra_cmd=self._extra_arguments) diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index dbc647f49827d1..df16b13b931193 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -4,13 +4,13 @@ import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) From 6126d059353e7b2663f72f2d4a73e95eaafeef04 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 29 Nov 2019 22:06:21 +1100 Subject: [PATCH 1893/3953] Migrate NSW Rural Fire Service integration to async library (#29181) * use async integration library * adapted unit tests * removed unused constants * relocated constants * simplified generation of new entries * small code fixes * increased test coverage and removed unused code * fixed comment * simplified patch code --- .../geo_location.py | 100 ++++++++++++------ .../nsw_rural_fire_service_feed/manifest.json | 2 +- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- .../test_geo_location.py | 40 ++++--- 5 files changed, 101 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index 9a9679f957511f..a04d2bd69b2134 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -3,6 +3,7 @@ import logging from typing import Optional +from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -14,11 +15,16 @@ CONF_RADIUS, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers import ConfigType, aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) @@ -58,7 +64,9 @@ ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +): """Set up the NSW Rural Fire Service Feed platform.""" scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) coordinates = ( @@ -68,30 +76,40 @@ def setup_platform(hass, config, add_entities, discovery_info=None): radius_in_km = config[CONF_RADIUS] categories = config.get(CONF_CATEGORIES) # Initialize the entity manager. - feed = NswRuralFireServiceFeedEntityManager( - hass, add_entities, scan_interval, coordinates, radius_in_km, categories + manager = NswRuralFireServiceFeedEntityManager( + hass, async_add_entities, scan_interval, coordinates, radius_in_km, categories ) - def start_feed_manager(event): + async def start_feed_manager(event): """Start feed manager.""" - feed.startup() + await manager.async_init() + + async def stop_feed_manager(event): + """Stop feed manager.""" + await manager.async_stop() - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_feed_manager) + hass.async_create_task(manager.async_update()) class NswRuralFireServiceFeedEntityManager: """Feed Entity Manager for NSW Rural Fire Service GeoJSON feed.""" def __init__( - self, hass, add_entities, scan_interval, coordinates, radius_in_km, categories + self, + hass, + async_add_entities, + scan_interval, + coordinates, + radius_in_km, + categories, ): """Initialize the Feed Entity Manager.""" - from geojson_client.nsw_rural_fire_service_feed import ( - NswRuralFireServiceFeedManager, - ) - self._hass = hass - self._feed_manager = NswRuralFireServiceFeedManager( + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = NswRuralFireServiceIncidentsFeedManager( + websession, self._generate_entity, self._update_entity, self._remove_entity, @@ -99,37 +117,52 @@ def __init__( filter_radius=radius_in_km, filter_categories=categories, ) - self._add_entities = add_entities + self._async_add_entities = async_add_entities self._scan_interval = scan_interval + self._track_time_remove_callback = None - def startup(self): - """Start up this manager.""" - self._feed_manager.update() - self._init_regular_updates() + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" - def _init_regular_updates(self): - """Schedule regular updates at the specified interval.""" - track_time_interval( - self._hass, lambda now: self._feed_manager.update(), self._scan_interval + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + 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) - def _generate_entity(self, external_id): + async def _generate_entity(self, external_id): """Generate new entity.""" new_entity = NswRuralFireServiceLocationEvent(self, external_id) # Add new entities to HA. - self._add_entities([new_entity], True) + self._async_add_entities([new_entity], True) - def _update_entity(self, external_id): + async def _update_entity(self, external_id): """Update entity.""" - dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - def _remove_entity(self, external_id): + async def _remove_entity(self, external_id): """Remove entity.""" - dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) + async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) class NswRuralFireServiceLocationEvent(GeolocationEvent): @@ -169,11 +202,14 @@ async def async_added_to_hass(self): 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._remove_signal_delete() - self._remove_signal_update() self.hass.async_create_task(self.async_remove()) @callback diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index 3d16f0a57e34cc..7dd7d10d6beb64 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -3,7 +3,7 @@ "name": "Nsw rural fire service feed", "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", "requirements": [ - "geojson_client==0.4" + "aio_geojson_nsw_rfs_incidents==0.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 650de67eee5ccd..c774e45fbd7c8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -129,6 +129,9 @@ aio_geojson_geonetnz_quakes==0.11 # homeassistant.components.geonetnz_volcano aio_geojson_geonetnz_volcano==0.5 +# homeassistant.components.nsw_rural_fire_service_feed +aio_geojson_nsw_rfs_incidents==0.1 + # homeassistant.components.ambient_station aioambient==0.3.2 @@ -550,7 +553,6 @@ geizhals==0.0.9 geniushub-client==0.6.30 # homeassistant.components.geo_json_events -# homeassistant.components.nsw_rural_fire_service_feed # homeassistant.components.usgs_earthquakes_feed geojson_client==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b6b65d7e4d43b..3f47fd5317ed2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -40,6 +40,9 @@ aio_geojson_geonetnz_quakes==0.11 # homeassistant.components.geonetnz_volcano aio_geojson_geonetnz_volcano==0.5 +# homeassistant.components.nsw_rural_fire_service_feed +aio_geojson_nsw_rfs_incidents==0.1 + # homeassistant.components.ambient_station aioambient==0.3.2 @@ -171,7 +174,6 @@ foobot_async==0.3.1 gTTS-token==1.1.3 # homeassistant.components.geo_json_events -# homeassistant.components.nsw_rural_fire_service_feed # homeassistant.components.usgs_earthquakes_feed geojson_client==0.4 diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index f5f88087010a66..274ef3d37431f3 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -1,5 +1,8 @@ -"""The tests for the geojson platform.""" +"""The tests for the NSW Rural Fire Service Feeds platform.""" import datetime +from unittest.mock import ANY + +from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeed from asynctest.mock import patch, MagicMock, call from homeassistant.components import geo_location @@ -20,6 +23,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, @@ -27,7 +31,7 @@ CONF_LONGITUDE, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - ATTR_ICON, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, async_fire_time_changed @@ -110,12 +114,12 @@ async def test_setup(hass): mock_entry_3 = _generate_mock_feed_entry("3456", "Title 3", 25.5, (-31.2, 150.2)) mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 12.5, (-31.3, 150.3)) - utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "geojson_client.nsw_rural_fire_service_feed." "NswRuralFireServiceFeed" - ) as mock_feed: - mock_feed.return_value.update.return_value = ( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_2, mock_entry_3], ) @@ -187,7 +191,7 @@ async def test_setup(hass): # Simulate an update - one existing, one new entry, # one outdated entry - mock_feed.return_value.update.return_value = ( + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_4, mock_entry_3], ) @@ -199,7 +203,7 @@ async def test_setup(hass): # Simulate an update - empty data, but successful update, # so no changes to entities. - mock_feed.return_value.update.return_value = "OK_NO_DATA", None + mock_feed_update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -207,13 +211,18 @@ async def test_setup(hass): assert len(all_states) == 3 # Simulate an update - empty data, removes all entities - mock_feed.return_value.update.return_value = "ERROR", None + mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * SCAN_INTERVAL) await hass.async_block_till_done() all_states = hass.states.async_all() assert len(all_states) == 0 + # Artificially trigger update. + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + # Collect events. + await hass.async_block_till_done() + async def test_setup_with_custom_location(hass): """Test the setup with a custom location.""" @@ -221,9 +230,12 @@ async def test_setup_with_custom_location(hass): mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 20.5, (-31.1, 150.1)) with patch( - "geojson_client.nsw_rural_fire_service_feed." "NswRuralFireServiceFeed" - ) as mock_feed: - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + "aio_geojson_nsw_rfs_incidents.feed_manager.NswRuralFireServiceIncidentsFeed", + wraps=NswRuralFireServiceIncidentsFeed, + ) as mock_feed_manager, patch( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component( @@ -238,6 +250,6 @@ async def test_setup_with_custom_location(hass): all_states = hass.states.async_all() assert len(all_states) == 1 - assert mock_feed.call_args == call( - (15.1, 25.2), filter_categories=[], filter_radius=200.0 + assert mock_feed_manager.call_args == call( + ANY, (15.1, 25.2), filter_categories=[], filter_radius=200.0 ) From 12155cb66eebaf41e6dac547007f5a3cae7be727 Mon Sep 17 00:00:00 2001 From: tetienne Date: Fri, 29 Nov 2019 12:08:20 +0100 Subject: [PATCH 1894/3953] Fix somfy switch inherit from SwitchDevice instead of ToggleEntity (#29182) --- homeassistant/components/somfy/cover.py | 2 +- homeassistant/components/somfy/switch.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index 3a8557ea4841b3..12f12676f9247f 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -7,7 +7,7 @@ ATTR_POSITION, ATTR_TILT_POSITION, ) -from homeassistant.components.somfy import DOMAIN, SomfyEntity, DEVICES, API +from . import DOMAIN, SomfyEntity, DEVICES, API async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py index d3294954851542..58ad2e5905d3ea 100644 --- a/homeassistant/components/somfy/switch.py +++ b/homeassistant/components/somfy/switch.py @@ -2,8 +2,8 @@ from pymfy.api.devices.camera_protect import CameraProtect from pymfy.api.devices.category import Category -from homeassistant.components.somfy import DOMAIN, SomfyEntity, DEVICES, API -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.switch import SwitchDevice +from . import DOMAIN, SomfyEntity, DEVICES, API async def async_setup_entry(hass, config_entry, async_add_entities): @@ -22,7 +22,7 @@ def get_shutters(): async_add_entities(await hass.async_add_executor_job(get_shutters), True) -class SomfyCameraShutter(SomfyEntity, ToggleEntity): +class SomfyCameraShutter(SomfyEntity, SwitchDevice): """Representation of a Somfy Camera Shutter device.""" def __init__(self, device, api): From 392cdf49e60e819dbfbb54776769894d1be50780 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 16:29:39 +0100 Subject: [PATCH 1895/3953] Allow controlling Tado Hot Water Devices with temperature control (#29191) --- homeassistant/components/tado/climate.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index b6a7f3d8bdbfa6..0431522d9118d0 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -103,6 +103,7 @@ def create_climate_device(tado, hass, zone, name, zone_id): unit = TEMP_CELSIUS ac_device = capabilities["type"] == "AIR_CONDITIONING" + hot_water_device = capabilities["type"] == "HOT_WATER" ac_support_heat = False if ac_device: @@ -134,6 +135,7 @@ def create_climate_device(tado, hass, zone, name, zone_id): hass.config.units.temperature(max_temp, unit), step, ac_device, + hot_water_device, ac_support_heat, ) @@ -157,6 +159,7 @@ def __init__( max_temp, step, ac_device, + hot_water_device, ac_support_heat, tolerance=0.3, ): @@ -168,6 +171,7 @@ def __init__( self.zone_id = zone_id self._ac_device = ac_device + self._hot_water_device = hot_water_device self._ac_support_heat = ac_support_heat self._cooling = False @@ -519,6 +523,21 @@ def _control_heating(self): "AIR_CONDITIONING", "COOL", ) + elif self._hot_water_device: + _LOGGER.info( + "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - HOT_WATER", + 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, + "HOT_WATER", + ) else: _LOGGER.info( "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - HEATING", From 4065c460469e149244bc3f17bb8121d8c1fff2f1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 29 Nov 2019 18:29:45 +0100 Subject: [PATCH 1896/3953] Fix smartthings cloud webhook (#29219) * Fix smartthings cloud webhook * Update smartapp.py --- homeassistant/components/smartthings/smartapp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index ecd4da5dcabc3c..6acd29397ae6a6 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -234,7 +234,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): and not hass.config_entries.async_entries(DOMAIN) ): cloudhook_url = await hass.components.cloud.async_create_cloudhook( - hass, config[CONF_WEBHOOK_ID] + config[CONF_WEBHOOK_ID] ) config[CONF_CLOUDHOOK_URL] = cloudhook_url await store.async_save(config) @@ -287,7 +287,7 @@ async def unload_smartapp_endpoint(hass: HomeAssistantType): and hass.components.cloud.async_is_logged_in() ): await hass.components.cloud.async_delete_cloudhook( - hass, hass.data[DOMAIN][CONF_WEBHOOK_ID] + hass.data[DOMAIN][CONF_WEBHOOK_ID] ) # Remove cloudhook from storage store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) From 2d5d40ce0ea34312717b066bd179b8b75fffa66c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 29 Nov 2019 18:29:45 +0100 Subject: [PATCH 1897/3953] Fix smartthings cloud webhook (#29219) * Fix smartthings cloud webhook * Update smartapp.py --- homeassistant/components/smartthings/smartapp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index ecd4da5dcabc3c..6acd29397ae6a6 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -234,7 +234,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): and not hass.config_entries.async_entries(DOMAIN) ): cloudhook_url = await hass.components.cloud.async_create_cloudhook( - hass, config[CONF_WEBHOOK_ID] + config[CONF_WEBHOOK_ID] ) config[CONF_CLOUDHOOK_URL] = cloudhook_url await store.async_save(config) @@ -287,7 +287,7 @@ async def unload_smartapp_endpoint(hass: HomeAssistantType): and hass.components.cloud.async_is_logged_in() ): await hass.components.cloud.async_delete_cloudhook( - hass, hass.data[DOMAIN][CONF_WEBHOOK_ID] + hass.data[DOMAIN][CONF_WEBHOOK_ID] ) # Remove cloudhook from storage store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) From ec008ddbe441e800f3170e605cd4e85c82b7bfe8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 29 Nov 2019 17:48:08 +0000 Subject: [PATCH 1898/3953] Bump version 0.102.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8b62d011fda1b7..927739486d6989 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 102 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From e405398ca40e31aa548a4c02965f0416247e2323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Fri, 29 Nov 2019 19:22:22 +0100 Subject: [PATCH 1899/3953] Allow turning off a tado water heater (#29221) --- homeassistant/components/tado/climate.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 0431522d9118d0..2baf1f380b5928 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -497,6 +497,15 @@ def _control_heating(self): self._store.set_zone_off( self.zone_id, CONST_OVERLAY_MANUAL, "AIR_CONDITIONING" ) + elif self._hot_water_device: + _LOGGER.info( + "Switching mytado.com to OFF for zone %s (%d) - HOT_WATER", + self.zone_name, + self.zone_id, + ) + self._store.set_zone_off( + self.zone_id, CONST_OVERLAY_MANUAL, "HOT_WATER" + ) else: _LOGGER.info( "Switching mytado.com to OFF for zone %s (%d) - HEATING", From 04bad4bc74dd9f43182a744b36827235f6ba8af3 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 19:34:40 +0100 Subject: [PATCH 1900/3953] Move imports to top for switchbot (#29229) --- homeassistant/components/switchbot/switch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index bc1fc34a890707..55e2a8b9641fde 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -2,11 +2,13 @@ import logging from typing import Any, Dict +# pylint: disable=import-error, no-member +import switchbot import voluptuous as vol +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_MAC from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) @@ -33,8 +35,6 @@ class SwitchBot(SwitchDevice, RestoreEntity): def __init__(self, mac, name) -> None: """Initialize the Switchbot.""" - # pylint: disable=import-error, no-member - import switchbot self._state = None self._last_run_success = None From 11331ad26e380f9e30d2b67710436f181943eb71 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 21:18:52 +0100 Subject: [PATCH 1901/3953] Move imports to top for starlingbank (#29233) --- homeassistant/components/starlingbank/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index 24ca7d4809cc38..1e04619234789b 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -2,6 +2,7 @@ import logging import requests +from starlingbank import StarlingAccount import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -40,7 +41,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Sterling Bank sensor platform.""" - from starlingbank import StarlingAccount sensors = [] for account in config[CONF_ACCOUNTS]: From e4b26dc02b8964722238d8b5e884fc5630cfbd26 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 21:19:49 +0100 Subject: [PATCH 1902/3953] Move imports to top for tahoma (#29232) --- homeassistant/components/tahoma/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 9fc8ca3cf2e0a0..02cdba5c46acea 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -1,12 +1,13 @@ """Support for Tahoma devices.""" from collections import defaultdict import logging -import voluptuous as vol + from requests.exceptions import RequestException +from tahoma_api import Action, TahomaApi +import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_EXCLUDE -from homeassistant.helpers import discovery -from homeassistant.helpers import config_validation as cv +from homeassistant.const import CONF_EXCLUDE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -63,7 +64,6 @@ def setup(hass, config): """Activate Tahoma component.""" - from tahoma_api import TahomaApi conf = config[DOMAIN] username = conf.get(CONF_USERNAME) @@ -133,7 +133,6 @@ def device_state_attributes(self): def apply_action(self, cmd_name, *args): """Apply Action to Device.""" - from tahoma_api import Action action = Action(self.tahoma_device.url) action.add_command(cmd_name, *args) From c30dfac4ed001058046d60654e6e397dc5995ac1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 21:21:00 +0100 Subject: [PATCH 1903/3953] Move imports to top for synologdsm (#29231) --- homeassistant/components/synologydsm/sensor.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/synologydsm/sensor.py b/homeassistant/components/synologydsm/sensor.py index df9c4247005867..d415d009252be5 100644 --- a/homeassistant/components/synologydsm/sensor.py +++ b/homeassistant/components/synologydsm/sensor.py @@ -1,24 +1,25 @@ """Support for Synology NAS Sensors.""" -import logging from datetime import timedelta +import logging +from SynologyDSM import SynologyDSM import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, + ATTR_ATTRIBUTION, + CONF_DISKS, CONF_HOST, - CONF_USERNAME, + CONF_MONITORED_CONDITIONS, + CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, - ATTR_ATTRIBUTION, - TEMP_CELSIUS, - CONF_MONITORED_CONDITIONS, + CONF_USERNAME, EVENT_HOMEASSISTANT_START, - CONF_DISKS, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -151,7 +152,6 @@ class SynoApi: def __init__(self, host, port, username, password, temp_unit, use_ssl): """Initialize the API wrapper class.""" - from SynologyDSM import SynologyDSM self.temp_unit = temp_unit From bbecd55c098850a0f509cecf241a8d69cffc9d80 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 21:22:03 +0100 Subject: [PATCH 1904/3953] Move imports to top for synology (#29230) --- homeassistant/components/synology/camera.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py index 91ee5a98fc328c..c144a251608d02 100644 --- a/homeassistant/components/synology/camera.py +++ b/homeassistant/components/synology/camera.py @@ -2,18 +2,19 @@ import logging import requests +from synology.surveillance_station import SurveillanceStation import voluptuous as vol +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import ( CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, + CONF_TIMEOUT, CONF_URL, - CONF_WHITELIST, + CONF_USERNAME, CONF_VERIFY_SSL, - CONF_TIMEOUT, + CONF_WHITELIST, ) -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_web, async_get_clientsession, @@ -44,8 +45,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= timeout = config.get(CONF_TIMEOUT) try: - from synology.surveillance_station import SurveillanceStation - surveillance = SurveillanceStation( config.get(CONF_URL), config.get(CONF_USERNAME), From 93bb1a681645e5229f45be435eaa52b43558dc32 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 21:26:23 +0100 Subject: [PATCH 1905/3953] Move imports to top for supla (#29226) --- homeassistant/components/supla/__init__.py | 2 +- homeassistant/components/supla/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 4293f187f5bb7c..fd60254cd0adf4 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -2,6 +2,7 @@ import logging from typing import Optional +from pysupla import SuplaAPI import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN @@ -38,7 +39,6 @@ def setup(hass, base_config): """Set up the Supla component.""" - from pysupla import SuplaAPI server_confs = base_config[DOMAIN][CONF_SERVERS] diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py index 5e7a54699505b9..725771e21e8036 100644 --- a/homeassistant/components/supla/switch.py +++ b/homeassistant/components/supla/switch.py @@ -2,8 +2,8 @@ import logging from pprint import pformat -from homeassistant.components.switch import SwitchDevice from homeassistant.components.supla import SuplaChannel +from homeassistant.components.switch import SwitchDevice _LOGGER = logging.getLogger(__name__) From 7d90b1f9ec899b4a444ff658dcab31805fc0d8ab Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 21:27:00 +0100 Subject: [PATCH 1906/3953] Move imports to top for streamlabswater (#29225) --- homeassistant/components/streamlabswater/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index 68097794d07650..836bc9b41830dd 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -1,6 +1,7 @@ """Support for Streamlabs Water Monitor devices.""" import logging +from streamlabswater import streamlabswater import voluptuous as vol from homeassistant.const import CONF_API_KEY @@ -39,7 +40,6 @@ def setup(hass, config): """Set up the streamlabs water component.""" - from streamlabswater import streamlabswater conf = config[DOMAIN] api_key = conf.get(CONF_API_KEY) From 39bf1b5f5d1ecb710e7457edee78cdeb44327247 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 21:32:03 +0100 Subject: [PATCH 1907/3953] Move imports to top for stiebel_eltron (#29224) --- homeassistant/components/stiebel_eltron/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/stiebel_eltron/__init__.py b/homeassistant/components/stiebel_eltron/__init__.py index 20cb6607ea142c..956e629dd9db92 100644 --- a/homeassistant/components/stiebel_eltron/__init__.py +++ b/homeassistant/components/stiebel_eltron/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pystiebeleltron import pystiebeleltron import voluptuous as vol from homeassistant.components.modbus import ( @@ -55,7 +56,6 @@ class StiebelEltronData: def __init__(self, name, modbus_client): """Init the STIEBEL ELTRON data object.""" - from pystiebeleltron import pystiebeleltron self.api = pystiebeleltron.StiebelEltronAPI(modbus_client, 1) From fbfbae8ad9c52968d2867ab4ab57e43bbae580e0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 21:50:09 +0100 Subject: [PATCH 1908/3953] Move imports to top for swiss_hydrological_data (#29227) --- homeassistant/components/swiss_hydrological_data/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index 2d5d0e8de3f3af..c8e7b9d6fc264e 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from swisshydrodata import SwissHydroData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -167,7 +168,6 @@ def __init__(self, station): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" - from swisshydrodata import SwissHydroData shd = SwissHydroData() self.data = shd.get_station(self.station) From 4bec14b0f6f73bf8e2e963cec21e3faa0d3b25c6 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 29 Nov 2019 22:16:14 +0100 Subject: [PATCH 1909/3953] Move imports to top for swiss_public_transport (#29228) * Move imports to top for swiss_public_transport * Remove import of exceptions since only one exception is used --- homeassistant/components/swiss_public_transport/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index 3cf8babf554889..be967247dc751a 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from opendata_transport import OpendataTransport +from opendata_transport.exceptions import OpendataTransportError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -45,7 +47,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Swiss public transport sensor.""" - from opendata_transport import OpendataTransport, exceptions name = config.get(CONF_NAME) start = config.get(CONF_START) @@ -56,7 +57,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: await opendata.async_get_data() - except exceptions.OpendataTransportError: + except OpendataTransportError: _LOGGER.error( "Check at http://transport.opendata.ch/examples/stationboard.html " "if your station names are valid" @@ -122,7 +123,6 @@ def icon(self): async def async_update(self): """Get the latest data from opendata.ch and update the states.""" - from opendata_transport.exceptions import OpendataTransportError try: if self._remaining_time.total_seconds() < 0: From 8ae0d891e45bdc79e8fc16eb311d807b7a080441 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 30 Nov 2019 00:32:19 +0000 Subject: [PATCH 1910/3953] [ci skip] Translation update --- .../components/starline/.translations/de.json | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 homeassistant/components/starline/.translations/de.json diff --git a/homeassistant/components/starline/.translations/de.json b/homeassistant/components/starline/.translations/de.json new file mode 100644 index 00000000000000..657e6c08b1a26f --- /dev/null +++ b/homeassistant/components/starline/.translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "error_auth_mfa": "Ung\u00fcltiger Code" + }, + "step": { + "auth_captcha": { + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS Code" + }, + "title": "2-Faktor-Authentifizierung" + }, + "auth_user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Anmeldeinformationen" + } + } + } +} \ No newline at end of file From bde453be47d2d52aa38cd32329bdae0d53ce694c Mon Sep 17 00:00:00 2001 From: Wim Haanstra Date: Sat, 30 Nov 2019 16:23:03 +0100 Subject: [PATCH 1911/3953] Address DSMR Reader review notes (#29209) * Fix review issues * Remove None from get method * No longer need logging import * Give definition without name a default name --- .../components/dsmr_reader/manifest.json | 2 +- .../components/dsmr_reader/sensor.py | 19 +++++-------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json index aa108d8bf9aafd..f1c52e02c830bd 100755 --- a/homeassistant/components/dsmr_reader/manifest.json +++ b/homeassistant/components/dsmr_reader/manifest.json @@ -1,7 +1,7 @@ { "domain": "dsmr_reader", "name": "DSMR Reader", - "documentation": "https://www.home-assistant.io/components/dsmr_reader", + "documentation": "https://www.home-assistant.io/integrations/dsmr_reader", "requirements": [], "dependencies": [ "mqtt" diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index c7efba9ddde283..01c010c4971ad0 100755 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -1,6 +1,4 @@ """Support for DSMR Reader through MQTT.""" -import logging - from homeassistant.components import mqtt from homeassistant.core import callback from homeassistant.helpers.entity import Entity @@ -8,8 +6,6 @@ from .definitions import DEFINITIONS -_LOGGER = logging.getLogger(__name__) - DOMAIN = "dsmr_reader" @@ -34,15 +30,10 @@ def __init__(self, topic): self._entity_id = slugify(topic.replace("/", "_")) self._topic = topic - self._name = self._definition["name"] - self._unit_of_measurement = ( - self._definition["unit"] if "unit" in self._definition else "" - ) - self._icon = self._definition["icon"] if "icon" in self._definition else None - self._transform = ( - self._definition["transform"] if "transform" in self._definition else None - ) - + self._name = self._definition.get("name", topic.split("/")[-1]) + self._unit_of_measurement = self._definition.get("unit") + self._icon = self._definition.get("icon") + self._transform = self._definition.get("transform") self._state = None async def async_added_to_hass(self): @@ -59,7 +50,7 @@ def message_received(message): self.async_schedule_update_ha_state() - return await mqtt.async_subscribe(self.hass, self._topic, message_received, 1) + await mqtt.async_subscribe(self.hass, self._topic, message_received, 1) @property def name(self): From 5dbfa16e04fa44ed1435970b3ab8879d978988ca Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 01:16:05 +0100 Subject: [PATCH 1912/3953] Move imports to top for simplisafe (#29262) --- homeassistant/components/simplisafe/__init__.py | 2 +- homeassistant/components/simplisafe/config_flow.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 94da65ee51a847..63ac0ca973c604 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -1,7 +1,7 @@ """Support for SimpliSafe alarm systems.""" import asyncio -import logging from datetime import timedelta +import logging from simplipy import API from simplipy.errors import InvalidCredentialsError, SimplipyError diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0e3af0ae03c87d..6e1082948d358f 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,10 +1,11 @@ """Config flow to configure the SimpliSafe component.""" from collections import OrderedDict +from simplipy import API +from simplipy.errors import SimplipyError import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import ( CONF_CODE, CONF_PASSWORD, @@ -12,6 +13,7 @@ CONF_TOKEN, CONF_USERNAME, ) +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import DEFAULT_SCAN_INTERVAL, DOMAIN @@ -53,8 +55,6 @@ async def async_step_import(self, import_config): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from simplipy import API - from simplipy.errors import SimplipyError if not user_input: return await self._show_form() From aa9514b7741148a4279edeaa417bc391e07393f0 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 1 Dec 2019 00:34:59 +0000 Subject: [PATCH 1913/3953] [ci skip] Translation update --- .../components/geonetnz_volcano/.translations/fr.json | 3 ++- homeassistant/components/lutron_caseta/.translations/fr.json | 5 +++++ homeassistant/components/starline/.translations/fr.json | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lutron_caseta/.translations/fr.json diff --git a/homeassistant/components/geonetnz_volcano/.translations/fr.json b/homeassistant/components/geonetnz_volcano/.translations/fr.json index 2692768910c8cd..c93ae906a46619 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/fr.json +++ b/homeassistant/components/geonetnz_volcano/.translations/fr.json @@ -10,6 +10,7 @@ }, "title": "Remplissez les d\u00e9tails de votre filtre." } - } + }, + "title": "GeoNet NZ Volcano" } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/fr.json b/homeassistant/components/lutron_caseta/.translations/fr.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/fr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/fr.json b/homeassistant/components/starline/.translations/fr.json index 07563bdb857d51..67a7dae4afcd02 100644 --- a/homeassistant/components/starline/.translations/fr.json +++ b/homeassistant/components/starline/.translations/fr.json @@ -7,6 +7,7 @@ "step": { "auth_app": { "data": { + "app_id": "ID de l'application", "app_secret": "Secret" }, "title": "Informations d'identification de l'application" From 96a28e59bca9bdfee44d522b36a9851f896ec93c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:19:28 +0100 Subject: [PATCH 1914/3953] Move imports to top for route53 (#29273) --- homeassistant/components/route53/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py index 3dffc3ffd9e381..a84475ab8a195d 100644 --- a/homeassistant/components/route53/__init__.py +++ b/homeassistant/components/route53/__init__.py @@ -3,6 +3,8 @@ import logging from typing import List +import boto3 +from ipify import exceptions, get_ip import voluptuous as vol from homeassistant.const import CONF_DOMAIN, CONF_TTL, CONF_ZONE @@ -72,10 +74,6 @@ def _update_route53( records: List[str], ttl: int, ): - import boto3 - from ipify import get_ip - from ipify import exceptions - _LOGGER.debug("Starting update for zone %s", zone) client = boto3.client( From 7ad139bb63f91ec41e9b6f54985aa9cb388f0208 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:20:17 +0100 Subject: [PATCH 1915/3953] Move imports to top for roomba (#29272) --- homeassistant/components/roomba/vacuum.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index aafa4823d559d8..172a494b602eaf 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -3,12 +3,14 @@ import logging import async_timeout +from roomba import Roomba import voluptuous as vol from homeassistant.components.vacuum import ( PLATFORM_SCHEMA, SUPPORT_BATTERY, SUPPORT_FAN_SPEED, + SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, @@ -16,7 +18,6 @@ SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_LOCATE, VacuumDevice, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -84,7 +85,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the iRobot Roomba vacuum cleaner platform.""" - from roomba import Roomba if PLATFORM not in hass.data: hass.data[PLATFORM] = {} @@ -296,9 +296,7 @@ async def async_update(self): self._is_on = self._status in ["Running"] # Set properties that are to appear in the GUI - self._state_attrs = { - ATTR_SOFTWARE_VERSION: software_version, - } + self._state_attrs = {ATTR_SOFTWARE_VERSION: software_version} # Get bin state bin_state = self._get_bin_state(state) From 869e71f45575c68445e11b0827bedc565701dd1e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:20:33 +0100 Subject: [PATCH 1916/3953] Move imports to top for rpi_rf (#29271) --- homeassistant/components/rpi_rf/switch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rpi_rf/switch.py b/homeassistant/components/rpi_rf/switch.py index 18e4a28d5c866c..5c09111c1cbcf3 100644 --- a/homeassistant/components/rpi_rf/switch.py +++ b/homeassistant/components/rpi_rf/switch.py @@ -1,10 +1,11 @@ """Support for a switch using a 433MHz module via GPIO on a Raspberry Pi.""" import importlib import logging +from threading import RLock import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME, CONF_SWITCHES, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv @@ -44,7 +45,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return switches controlled by a generic RF device via GPIO.""" rpi_rf = importlib.import_module("rpi_rf") - from threading import RLock gpio = config.get(CONF_GPIO) rfdevice = rpi_rf.RFDevice(gpio) From 82d8c9c2ca92c9c21d968f3a99ea3bd5d5d24229 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:21:08 +0100 Subject: [PATCH 1917/3953] Move imports to top for rova (#29269) --- homeassistant/components/rova/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index fe0b5dead84d15..86a04829c75071 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -3,6 +3,8 @@ from datetime import datetime, timedelta import logging +from requests.exceptions import ConnectTimeout, HTTPError +from rova.rova import Rova import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -49,8 +51,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Create the Rova data service and sensors.""" - from rova.rova import Rova - from requests.exceptions import HTTPError, ConnectTimeout zip_code = config[CONF_ZIP_CODE] house_number = config[CONF_HOUSE_NUMBER] @@ -132,7 +132,6 @@ def __init__(self, api): @Throttle(UPDATE_DELAY) def update(self): """Update the data from the Rova API.""" - from requests.exceptions import HTTPError, ConnectTimeout try: items = self.api.get_calendar_items() From db2f53dfc157bbc0eaefb7ea4816b90503dee0de Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:21:28 +0100 Subject: [PATCH 1918/3953] Move imports to top for russound_rnet (#29268) --- homeassistant/components/russound_rnet/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/russound_rnet/media_player.py b/homeassistant/components/russound_rnet/media_player.py index e62e0a6b3af075..70ed12123638fa 100644 --- a/homeassistant/components/russound_rnet/media_player.py +++ b/homeassistant/components/russound_rnet/media_player.py @@ -1,9 +1,10 @@ """Support for interfacing with Russound via RNET Protocol.""" import logging +from russound import russound import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -51,8 +52,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Invalid config. Expected %s and %s", CONF_HOST, CONF_PORT) return False - from russound import russound - russ = russound.Russound(host, port) russ.connect() From 453569a469d0e2d29d994f4d16a007a1d773bf9a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:21:40 +0100 Subject: [PATCH 1919/3953] Move imports to top for russound_rio (#29267) --- homeassistant/components/russound_rio/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 69967c21fd5965..fdcd308618adac 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -1,6 +1,7 @@ """Support for Russound multizone controllers using RIO Protocol.""" import logging +from russound_rio import Russound import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -44,7 +45,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Russound RIO platform.""" - from russound_rio import Russound host = config.get(CONF_HOST) port = config.get(CONF_PORT) From 3aff90c0ac8144965a112e0e50ef9d46505cea56 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:21:46 +0100 Subject: [PATCH 1920/3953] Move imports to top for rpi_gpio_pwm (#29270) --- .../components/rpi_gpio_pwm/light.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rpi_gpio_pwm/light.py b/homeassistant/components/rpi_gpio_pwm/light.py index 27dd4da80ac29a..aededbc676cf05 100644 --- a/homeassistant/components/rpi_gpio_pwm/light.py +++ b/homeassistant/components/rpi_gpio_pwm/light.py @@ -1,22 +1,28 @@ """Support for LED lights that can be controlled using PWM.""" import logging +from pwmled import Color +from pwmled.driver.gpio import GpioDriver +from pwmled.driver.pca9685 import Pca9685Driver +from pwmled.led import SimpleLed +from pwmled.led.rgb import RgbLed +from pwmled.led.rgbw import RgbwLed import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_ON, CONF_ADDRESS from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, - PLATFORM_SCHEMA, + Light, ) +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE, STATE_ON import homeassistant.helpers.config_validation as cv -import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -61,11 +67,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PWM LED lights.""" - from pwmled.led import SimpleLed - from pwmled.led.rgb import RgbLed - from pwmled.led.rgbw import RgbwLed - from pwmled.driver.gpio import GpioDriver - from pwmled.driver.pca9685 import Pca9685Driver leds = [] for led_conf in config[CONF_LEDS]: @@ -240,7 +241,6 @@ def _from_hass_brightness(brightness): def _from_hass_color(color): """Convert Home Assistant RGB list to Color tuple.""" - from pwmled import Color rgb = color_util.color_hs_to_RGB(*color) return Color(*tuple(rgb)) From 4ceddc6d35431d562cfc76abcfbc8e9db4cfd16a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:22:05 +0100 Subject: [PATCH 1921/3953] Move imports to top for sabnzbd (#29266) --- homeassistant/components/sabnzbd/__init__.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index bf5e90e21f1445..f436bcb8a72297 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -1,14 +1,14 @@ """Support for monitoring an SABnzbd NZB client.""" -import logging from datetime import timedelta +import logging +from pysabnzbd import SabnzbdApi, SabnzbdApiException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.discovery import SERVICE_SABNZBD from homeassistant.const import ( - CONF_HOST, CONF_API_KEY, + CONF_HOST, CONF_NAME, CONF_PATH, CONF_PORT, @@ -18,6 +18,7 @@ from homeassistant.core import callback from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.json import load_json, save_json @@ -86,7 +87,6 @@ async def async_check_sabnzbd(sab_api): """Check if we can reach SABnzbd.""" - from pysabnzbd import SabnzbdApiException try: await sab_api.check_available() @@ -100,7 +100,6 @@ async def async_configure_sabnzbd( hass, config, use_ssl, name=DEFAULT_NAME, api_key=None ): """Try to configure Sabnzbd and request api key if configuration fails.""" - from pysabnzbd import SabnzbdApi host = config[CONF_HOST] port = config[CONF_PORT] @@ -174,7 +173,6 @@ async def async_service_handler(service): async def async_update_sabnzbd(now): """Refresh SABnzbd queue data.""" - from pysabnzbd import SabnzbdApiException try: await sab_api.refresh_data() @@ -188,7 +186,6 @@ async def async_update_sabnzbd(now): @callback def async_request_configuration(hass, config, host, web_root): """Request configuration steps from the user.""" - from pysabnzbd import SabnzbdApi configurator = hass.components.configurator # We got an error if this method is called while we are configuring @@ -239,7 +236,6 @@ def __init__(self, sab_api, name, sensors): async def async_pause_queue(self): """Pause Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException try: return await self.sab_api.pause_queue() @@ -249,7 +245,6 @@ async def async_pause_queue(self): async def async_resume_queue(self): """Resume Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException try: return await self.sab_api.resume_queue() @@ -259,7 +254,6 @@ async def async_resume_queue(self): async def async_set_queue_speed(self, limit): """Set speed limit for the Sabnzbd queue.""" - from pysabnzbd import SabnzbdApiException try: return await self.sab_api.set_speed_limit(limit) From 11b274989a8af4113ab489000ad5ba549bb7a603 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:22:18 +0100 Subject: [PATCH 1922/3953] Move imports to top for satel_integra (#29263) --- homeassistant/components/satel_integra/__init__.py | 5 ++--- .../components/satel_integra/alarm_control_panel.py | 3 ++- homeassistant/components/satel_integra/binary_sensor.py | 2 +- homeassistant/components/satel_integra/switch.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/satel_integra/__init__.py b/homeassistant/components/satel_integra/__init__.py index a657f6239d1eeb..1972eefd6b507a 100644 --- a/homeassistant/components/satel_integra/__init__.py +++ b/homeassistant/components/satel_integra/__init__.py @@ -2,9 +2,10 @@ import collections import logging +from satel_integra.satel_integra import AsyncSatel import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -102,8 +103,6 @@ async def async_setup(hass, config): port = conf.get(CONF_PORT) partitions = conf.get(CONF_DEVICE_PARTITIONS) - from satel_integra.satel_integra import AsyncSatel - monitored_outputs = collections.OrderedDict( list(outputs.items()) + list(switchable_outputs.items()) ) diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index c4321673061a8a..d4294788fdda66 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -3,6 +3,8 @@ from collections import OrderedDict import logging +from satel_integra.satel_integra import AlarmState + import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -82,7 +84,6 @@ def _update_alarm_status(self): def _read_alarm_state(self): """Read current status of the alarm and translate it into HA status.""" - from satel_integra.satel_integra import AlarmState # Default - disarmed: hass_alarm_status = STATE_ALARM_DISARMED diff --git a/homeassistant/components/satel_integra/binary_sensor.py b/homeassistant/components/satel_integra/binary_sensor.py index 1e4877229b9944..cbe760c06bf1e2 100644 --- a/homeassistant/components/satel_integra/binary_sensor.py +++ b/homeassistant/components/satel_integra/binary_sensor.py @@ -10,9 +10,9 @@ CONF_ZONE_NAME, CONF_ZONE_TYPE, CONF_ZONES, + DATA_SATEL, SIGNAL_OUTPUTS_UPDATED, SIGNAL_ZONES_UPDATED, - DATA_SATEL, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index 5b5e4f3095bb89..c20f30cf87151d 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -9,8 +9,8 @@ CONF_DEVICE_CODE, CONF_SWITCHABLE_OUTPUTS, CONF_ZONE_NAME, - SIGNAL_OUTPUTS_UPDATED, DATA_SATEL, + SIGNAL_OUTPUTS_UPDATED, ) _LOGGER = logging.getLogger(__name__) From 221db3a2dd1db9ee22fd4984c23a840347713d42 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:22:33 +0100 Subject: [PATCH 1923/3953] Move imports to top for simplepush (#29261) --- homeassistant/components/simplepush/notify.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 0b3b09fe11b10c..63bcd31935ebec 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -1,17 +1,17 @@ """Simplepush notification service.""" import logging +from simplepush import send, send_encrypted import voluptuous as vol -from homeassistant.const import CONF_PASSWORD -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_PASSWORD +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -48,7 +48,6 @@ def __init__(self, config): def send_message(self, message="", **kwargs): """Send a message to a Simplepush user.""" - from simplepush import send, send_encrypted title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) From f2b06d9abd50c07f03be1156717c1fefc19cd585 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:22:53 +0100 Subject: [PATCH 1924/3953] Move imports to top for sht31 (#29260) --- homeassistant/components/sht31/sensor.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sht31/sensor.py b/homeassistant/components/sht31/sensor.py index 7f8b6ecdc521bb..8a520377896016 100644 --- a/homeassistant/components/sht31/sensor.py +++ b/homeassistant/components/sht31/sensor.py @@ -4,17 +4,21 @@ import logging import math +from Adafruit_SHT31 import SHT31 import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_NAME, CONF_MONITORED_CONDITIONS +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, + CONF_NAME, + PRECISION_TENTHS, + TEMP_CELSIUS, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import display_temp -from homeassistant.const import PRECISION_TENTHS from homeassistant.util import Throttle - _LOGGER = logging.getLogger(__name__) CONF_I2C_ADDRESS = "i2c_address" @@ -43,7 +47,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the sensor platform.""" - from Adafruit_SHT31 import SHT31 i2c_address = config.get(CONF_I2C_ADDRESS) sensor = SHT31(address=i2c_address) From 93150f6f94290af31aa2f7c07368b108e011e3a1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:23:09 +0100 Subject: [PATCH 1925/3953] Move imports to top for sensehat (#29259) --- homeassistant/components/sensehat/light.py | 8 ++++---- homeassistant/components/sensehat/sensor.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensehat/light.py b/homeassistant/components/sensehat/light.py index dcfc9b925e5769..462c4245cd4a08 100644 --- a/homeassistant/components/sensehat/light.py +++ b/homeassistant/components/sensehat/light.py @@ -1,18 +1,19 @@ """Support for Sense Hat LEDs.""" import logging +from sense_hat import SenseHat import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -28,7 +29,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Sense Hat Light platform.""" - from sense_hat import SenseHat sensehat = SenseHat() diff --git a/homeassistant/components/sensehat/sensor.py b/homeassistant/components/sensehat/sensor.py index 7a7af09b4eb08e..980c23f8555efe 100644 --- a/homeassistant/components/sensehat/sensor.py +++ b/homeassistant/components/sensehat/sensor.py @@ -1,12 +1,13 @@ """Support for Sense HAT sensors.""" -import os -import logging from datetime import timedelta +import logging +import os +from sense_hat import SenseHat import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_DISPLAY_OPTIONS, CONF_NAME +from homeassistant.const import CONF_DISPLAY_OPTIONS, CONF_NAME, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -117,7 +118,6 @@ def __init__(self, is_hat_attached): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Sense HAT.""" - from sense_hat import SenseHat sense = SenseHat() temp_from_h = sense.get_temperature_from_humidity() From 9fbb345ce0022596c95f140474c236db3edfdc92 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:23:39 +0100 Subject: [PATCH 1926/3953] Move imports to top for tradfri (#29247) --- homeassistant/components/tradfri/__init__.py | 27 ++++++++++--------- .../components/tradfri/base_class.py | 1 + .../components/tradfri/config_flow.py | 13 +++++---- homeassistant/components/tradfri/const.py | 2 +- homeassistant/components/tradfri/cover.py | 5 ++-- homeassistant/components/tradfri/light.py | 13 ++++----- homeassistant/components/tradfri/sensor.py | 3 ++- homeassistant/components/tradfri/switch.py | 3 ++- 8 files changed, 36 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index bb44564731085f..a797607e243322 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -1,32 +1,33 @@ """Support for IKEA Tradfri.""" import logging -import voluptuous as vol from pytradfri import Gateway, RequestError from pytradfri.api.aiocoap_api import APIFactory +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json + from . import config_flow # noqa: F401 from .const import ( - DOMAIN, - CONFIG_FILE, - KEY_GATEWAY, - KEY_API, - CONF_ALLOW_TRADFRI_GROUPS, - DEFAULT_ALLOW_TRADFRI_GROUPS, - TRADFRI_DEVICE_TYPES, - ATTR_TRADFRI_MANUFACTURER, ATTR_TRADFRI_GATEWAY, ATTR_TRADFRI_GATEWAY_MODEL, - CONF_IMPORT_GROUPS, - CONF_IDENTITY, + ATTR_TRADFRI_MANUFACTURER, + CONF_ALLOW_TRADFRI_GROUPS, + CONF_GATEWAY_ID, CONF_HOST, + CONF_IDENTITY, + CONF_IMPORT_GROUPS, CONF_KEY, - CONF_GATEWAY_ID, + CONFIG_FILE, + DEFAULT_ALLOW_TRADFRI_GROUPS, + DOMAIN, + KEY_API, + KEY_GATEWAY, + TRADFRI_DEVICE_TYPES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index ba90fe05d1e3cd..358056d7ef6011 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -5,6 +5,7 @@ from homeassistant.core import callback from homeassistant.helpers.entity import Entity + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 24c3fbc1876802..048541b5402545 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -4,15 +4,18 @@ from uuid import uuid4 import async_timeout +from pytradfri import Gateway, RequestError +from pytradfri.api.aiocoap_api import APIFactory import voluptuous as vol from homeassistant import config_entries + from .const import ( - CONF_IMPORT_GROUPS, - CONF_IDENTITY, + CONF_GATEWAY_ID, CONF_HOST, + CONF_IDENTITY, + CONF_IMPORT_GROUPS, CONF_KEY, - CONF_GATEWAY_ID, KEY_SECURITY_CODE, ) @@ -153,8 +156,6 @@ async def _entry_from_data(self, data): async def authenticate(hass, host, security_code): """Authenticate with a Tradfri hub.""" - from pytradfri.api.aiocoap_api import APIFactory - from pytradfri import RequestError identity = uuid4().hex @@ -173,8 +174,6 @@ async def authenticate(hass, host, security_code): async def get_gateway_info(hass, host, identity, key): """Return info for the gateway.""" - from pytradfri.api.aiocoap_api import APIFactory - from pytradfri import Gateway, RequestError try: factory = APIFactory(host, psk_id=identity, psk=key, loop=hass.loop) diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index 01d2f18501d836..88225d3282a66f 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -1,5 +1,5 @@ """Consts used by Tradfri.""" -from homeassistant.components.light import SUPPORT_TRANSITION, SUPPORT_BRIGHTNESS +from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION from homeassistant.const import CONF_HOST # noqa: F401 pylint: disable=unused-import ATTR_DIMMER = "dimmer" diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index ae7d6a09ce3e92..d978e51292041a 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -1,8 +1,9 @@ """Support for IKEA Tradfri covers.""" -from homeassistant.components.cover import CoverDevice, ATTR_POSITION +from homeassistant.components.cover import ATTR_POSITION, CoverDevice + from .base_class import TradfriBaseDevice -from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID +from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 9ee3c5d6a8cc63..0fe826be9af425 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -1,29 +1,30 @@ """Support for IKEA Tradfri lights.""" import logging -import homeassistant.util.color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + Light, ) -from .base_class import TradfriBaseDevice, TradfriBaseClass +import homeassistant.util.color as color_util + +from .base_class import TradfriBaseClass, TradfriBaseDevice from .const import ( ATTR_DIMMER, ATTR_HUE, ATTR_SAT, ATTR_TRANSITION_TIME, - SUPPORTED_LIGHT_FEATURES, - SUPPORTED_GROUP_FEATURES, CONF_GATEWAY_ID, CONF_IMPORT_GROUPS, - KEY_GATEWAY, KEY_API, + KEY_GATEWAY, + SUPPORTED_GROUP_FEATURES, + SUPPORTED_LIGHT_FEATURES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index cf797f34e3b410..c3a08ab167535d 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,8 +1,9 @@ """Support for IKEA Tradfri sensors.""" from homeassistant.const import DEVICE_CLASS_BATTERY + from .base_class import TradfriBaseDevice -from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID +from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index e1c549a1805f7f..fffbf320c7e9e4 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,7 +1,8 @@ """Support for IKEA Tradfri switches.""" from homeassistant.components.switch import SwitchDevice + from .base_class import TradfriBaseDevice -from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID +from .const import CONF_GATEWAY_ID, KEY_API, KEY_GATEWAY async def async_setup_entry(hass, config_entry, async_add_entities): From 6726c4c76baa59b2533f6eead86b93a49dfe7162 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:23:48 +0100 Subject: [PATCH 1927/3953] Move imports to top for spider (#29249) --- homeassistant/components/spider/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py index 0d5e1606b53cb6..125799b394a46a 100644 --- a/homeassistant/components/spider/__init__.py +++ b/homeassistant/components/spider/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from spiderpy.spiderapi import SpiderApi, UnauthorizedException import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME @@ -32,8 +33,6 @@ def setup(hass, config): """Set up Spider Component.""" - from spiderpy.spiderapi import SpiderApi - from spiderpy.spiderapi import UnauthorizedException username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] From bea5d18c4aca7a2f7db636868413043997e74bb7 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:23:59 +0100 Subject: [PATCH 1928/3953] Move imports to top for sochain (#29250) --- homeassistant/components/sochain/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sochain/sensor.py b/homeassistant/components/sochain/sensor.py index cc5a477652be95..608405dd1b400a 100644 --- a/homeassistant/components/sochain/sensor.py +++ b/homeassistant/components/sochain/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pysochain import ChainSo import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -31,7 +32,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the sochain sensors.""" - from pysochain import ChainSo address = config.get(CONF_ADDRESS) network = config.get(CONF_NETWORK) From 8da7f407365a00aedccc2f6a2e28c5e2cca44ee9 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:24:17 +0100 Subject: [PATCH 1929/3953] Move imports to top for smarty (#29251) --- homeassistant/components/smarty/__init__.py | 6 +++--- homeassistant/components/smarty/binary_sensor.py | 3 ++- homeassistant/components/smarty/fan.py | 2 +- homeassistant/components/smarty/sensor.py | 7 ++++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/smarty/__init__.py b/homeassistant/components/smarty/__init__.py index ad824055126ed5..22987673005b32 100644 --- a/homeassistant/components/smarty/__init__.py +++ b/homeassistant/components/smarty/__init__.py @@ -1,12 +1,13 @@ """Support to control a Salda Smarty XP/XV ventilation unit.""" from datetime import timedelta - import ipaddress import logging + +from pysmarty import Smarty import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send @@ -36,7 +37,6 @@ def setup(hass, config): """Set up the smarty environment.""" - from pysmarty import Smarty conf = config[DOMAIN] diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index 8723f0248d34a8..a86b3548e959a9 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -2,9 +2,10 @@ import logging -from homeassistant.core import callback from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import DOMAIN, SIGNAL_UPDATE_SMARTY _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index 81edac80cb04c2..bb6b76237791a7 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -2,7 +2,6 @@ import logging -from homeassistant.core import callback from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -11,6 +10,7 @@ SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN, SIGNAL_UPDATE_SMARTY diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index bf647777b52801..f5cd1fbb404dcc 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -3,15 +3,16 @@ import datetime as dt import logging -from homeassistant.core import callback from homeassistant.const import ( - TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, + TEMP_CELSIUS, ) -import homeassistant.util.dt as dt_util +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util + from . import DOMAIN, SIGNAL_UPDATE_SMARTY _LOGGER = logging.getLogger(__name__) From 1560d84cd7d8d2912f005ec2d17c1f1485ba4020 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:24:38 +0100 Subject: [PATCH 1930/3953] Move imports to top for sisyphus (#29252) --- homeassistant/components/sisyphus/__init__.py | 3 +-- homeassistant/components/sisyphus/media_player.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sisyphus/__init__.py b/homeassistant/components/sisyphus/__init__.py index 771641c9b1dd25..5ad59da5dee347 100644 --- a/homeassistant/components/sisyphus/__init__.py +++ b/homeassistant/components/sisyphus/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +from sisyphus_control import Table import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP @@ -29,7 +30,6 @@ async def async_setup(hass, config): """Set up the sisyphus component.""" - from sisyphus_control import Table class SocketIONoiseFilter(logging.Filter): """Filters out excessively verbose logs from SocketIO.""" @@ -105,7 +105,6 @@ async def get_table(self): return await self._table_task async def _connect_table(self): - from sisyphus_control import Table self._table = await Table.connect(self._host, self._session) if self._name is None: diff --git a/homeassistant/components/sisyphus/media_player.py b/homeassistant/components/sisyphus/media_player.py index 5f02418f7e0d70..e708504ff7ed4b 100644 --- a/homeassistant/components/sisyphus/media_player.py +++ b/homeassistant/components/sisyphus/media_player.py @@ -2,6 +2,7 @@ import logging import aiohttp +from sisyphus_control import Track from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -140,7 +141,6 @@ def supported_features(self): @property def media_image_url(self): """Return the URL for a thumbnail image of the current track.""" - from sisyphus_control import Track if self._table.active_track: return self._table.active_track.get_thumbnail_url(Track.ThumbnailSize.LARGE) From bb46918d2ebd9e7616d17187936357826231911f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:24:54 +0100 Subject: [PATCH 1931/3953] Move imports to top for scsgate (#29257) --- homeassistant/components/scsgate/__init__.py | 10 ++++------ homeassistant/components/scsgate/cover.py | 13 ++++++------- homeassistant/components/scsgate/light.py | 5 ++--- homeassistant/components/scsgate/switch.py | 9 ++++----- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/scsgate/__init__.py b/homeassistant/components/scsgate/__init__.py index 739a2949d17a5d..21e3608a51b5de 100644 --- a/homeassistant/components/scsgate/__init__.py +++ b/homeassistant/components/scsgate/__init__.py @@ -2,6 +2,10 @@ import logging from threading import Lock +from scsgate.connection import Connection +from scsgate.messages import ScenarioTriggeredMessage, StateMessage +from scsgate.reactor import Reactor +from scsgate.tasks import GetStatusTask import voluptuous as vol from homeassistant.const import CONF_DEVICE, CONF_NAME @@ -61,12 +65,8 @@ def __init__(self, device, logger): self._device_being_registered = None self._device_being_registered_lock = Lock() - from scsgate.connection import Connection - connection = Connection(device=device, logger=self._logger) - from scsgate.reactor import Reactor - self._reactor = Reactor( connection=connection, logger=self._logger, @@ -75,7 +75,6 @@ def __init__(self, device, logger): def handle_message(self, message): """Handle a messages seen on the bus.""" - from scsgate.messages import StateMessage, ScenarioTriggeredMessage self._logger.debug(f"Received message {message}") if not isinstance(message, StateMessage) and not isinstance( @@ -132,7 +131,6 @@ def add_devices_to_register(self, devices): def _activate_next_device(self): """Start the activation of the first device.""" - from scsgate.tasks import GetStatusTask with self._devices_to_register_lock: while self._devices_to_register: diff --git a/homeassistant/components/scsgate/cover.py b/homeassistant/components/scsgate/cover.py index 9aa19e3f668111..9d034c146ee2a1 100644 --- a/homeassistant/components/scsgate/cover.py +++ b/homeassistant/components/scsgate/cover.py @@ -1,10 +1,15 @@ """Support for SCSGate covers.""" import logging +from scsgate.tasks import ( + HaltRollerShutterTask, + LowerRollerShutterTask, + RaiseRollerShutterTask, +) import voluptuous as vol from homeassistant.components import scsgate -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice from homeassistant.const import CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -69,20 +74,14 @@ def is_closed(self): def open_cover(self, **kwargs): """Move the cover.""" - from scsgate.tasks import RaiseRollerShutterTask - scsgate.SCSGATE.append_task(RaiseRollerShutterTask(target=self._scs_id)) def close_cover(self, **kwargs): """Move the cover down.""" - from scsgate.tasks import LowerRollerShutterTask - scsgate.SCSGATE.append_task(LowerRollerShutterTask(target=self._scs_id)) def stop_cover(self, **kwargs): """Stop the cover.""" - from scsgate.tasks import HaltRollerShutterTask - scsgate.SCSGATE.append_task(HaltRollerShutterTask(target=self._scs_id)) def process_event(self, message): diff --git a/homeassistant/components/scsgate/light.py b/homeassistant/components/scsgate/light.py index c183fc6a3f88e9..a04dfdc7e7ae79 100644 --- a/homeassistant/components/scsgate/light.py +++ b/homeassistant/components/scsgate/light.py @@ -1,10 +1,11 @@ """Support for SCSGate lights.""" import logging +from scsgate.tasks import ToggleStatusTask import voluptuous as vol from homeassistant.components import scsgate -from homeassistant.components.light import Light, PLATFORM_SCHEMA +from homeassistant.components.light import PLATFORM_SCHEMA, Light from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -70,7 +71,6 @@ def is_on(self): def turn_on(self, **kwargs): """Turn the device on.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task(ToggleStatusTask(target=self._scs_id, toggled=True)) @@ -79,7 +79,6 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn the device off.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task( ToggleStatusTask(target=self._scs_id, toggled=False) diff --git a/homeassistant/components/scsgate/switch.py b/homeassistant/components/scsgate/switch.py index 75e55e259a6b81..b2043d3a4c3954 100644 --- a/homeassistant/components/scsgate/switch.py +++ b/homeassistant/components/scsgate/switch.py @@ -1,11 +1,13 @@ """Support for SCSGate switches.""" import logging +from scsgate.messages import ScenarioTriggeredMessage, StateMessage +from scsgate.tasks import ToggleStatusTask import voluptuous as vol from homeassistant.components import scsgate -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_DEVICES +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import ATTR_ENTITY_ID, ATTR_STATE, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv ATTR_SCENARIO_ID = "scenario_id" @@ -105,7 +107,6 @@ def is_on(self): def turn_on(self, **kwargs): """Turn the device on.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task(ToggleStatusTask(target=self._scs_id, toggled=True)) @@ -114,7 +115,6 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn the device off.""" - from scsgate.tasks import ToggleStatusTask scsgate.SCSGATE.append_task( ToggleStatusTask(target=self._scs_id, toggled=False) @@ -172,7 +172,6 @@ def name(self): def process_event(self, message): """Handle a SCSGate message related with this switch.""" - from scsgate.messages import StateMessage, ScenarioTriggeredMessage if isinstance(message, StateMessage): scenario_id = message.bytes[4] From c0619944faaab4fb95a7eb92456b53d9414cd551 Mon Sep 17 00:00:00 2001 From: Santobert Date: Sun, 1 Dec 2019 06:26:44 +0100 Subject: [PATCH 1932/3953] Neato reduce API calls (#29156) * initial commit * Fix a bug where some values are not available * Workaround if git_robot_info() is not available --- homeassistant/components/neato/__init__.py | 3 +-- homeassistant/components/neato/sensor.py | 10 +------- homeassistant/components/neato/switch.py | 10 +------- homeassistant/components/neato/vacuum.py | 27 +++++++++------------- 4 files changed, 14 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index ddf9789f678fa2..5a697e7b9ad108 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -21,7 +21,6 @@ NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, - SCAN_INTERVAL_MINUTES, VALID_VENDORS, ) @@ -161,7 +160,7 @@ def login(self): self.logged_in = True _LOGGER.debug("Successfully connected to Neato API") - @Throttle(timedelta(minutes=SCAN_INTERVAL_MINUTES)) + @Throttle(timedelta(minutes=1)) def update_robots(self): """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS)) diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 36175151e0e736..fd5d8036f5fa66 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -41,22 +41,14 @@ class NeatoSensor(Entity): def __init__(self, neato, robot): """Initialize Neato sensor.""" self.robot = robot - self.neato = neato - self._available = self.neato.logged_in if self.neato is not None else False + self._available = neato.logged_in if neato is not None else False self._robot_name = f"{self.robot.name} {BATTERY}" self._robot_serial = self.robot.serial self._state = None def update(self): """Update Neato Sensor.""" - if self.neato is None: - _LOGGER.error("Error while updating sensor") - self._state = None - self._available = False - return - try: - self.neato.update_robots() self._state = self.robot.state except NeatoRobotException as ex: if self._available: diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 8536af63945ecd..6aa0e11a43efda 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -45,8 +45,7 @@ def __init__(self, neato, robot, switch_type): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self.neato = neato - self._available = self.neato.logged_in if self.neato is not None else False + self._available = neato.logged_in if neato is not None else False self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None self._schedule_state = None @@ -55,15 +54,8 @@ def __init__(self, neato, robot, switch_type): def update(self): """Update the states of Neato switches.""" - if self.neato is None: - _LOGGER.error("Error while updating switches") - self._state = None - self._available = False - return - _LOGGER.debug("Running switch update") try: - self.neato.update_robots() self._state = self.robot.state except NeatoRobotException as ex: if self._available: # Print only once when available diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index ec8ff22d83e9a9..9cac0cd24ceb79 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -137,8 +137,7 @@ class NeatoConnectedVacuum(StateVacuumDevice): def __init__(self, neato, robot, mapdata, persistent_maps): """Initialize the Neato Connected Vacuum.""" self.robot = robot - self.neato = neato - self._available = self.neato.logged_in if self.neato is not None else False + self._available = neato.logged_in if neato is not None else False self._mapdata = mapdata self._name = f"{self.robot.name}" self._robot_has_map = self.robot.has_persistent_maps @@ -163,17 +162,14 @@ def __init__(self, neato, robot, mapdata, persistent_maps): def update(self): """Update the states of Neato Vacuums.""" - if self.neato is None: - _LOGGER.error("Error while updating vacuum") - self._state = None - self._available = False - return - _LOGGER.debug("Running Neato Vacuums update") try: if self._robot_stats is None: self._robot_stats = self.robot.get_robot_info().json() - self.neato.update_robots() + except NeatoRobotException: + _LOGGER.warning("Couldn't fetch robot information of %s", self._name) + + try: self._state = self.robot.state except NeatoRobotException as ex: if self._available: # print only once when available @@ -321,13 +317,12 @@ def device_state_attributes(self): @property def device_info(self): """Device info for neato robot.""" - return { - "identifiers": {(NEATO_DOMAIN, self._robot_serial)}, - "name": self._name, - "manufacturer": self._robot_stats["data"]["mfg_name"], - "model": self._robot_stats["data"]["modelName"], - "sw_version": self._state["meta"]["firmware"], - } + info = {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}, "name": self._name} + if self._robot_stats: + info["manufacturer"] = self._robot_stats["data"]["mfg_name"] + info["model"] = self._robot_stats["data"]["modelName"] + if self._state: + info["sw_version"] = self._state["meta"]["firmware"] def start(self): """Start cleaning or resume cleaning.""" From d5efd0b35264da3edc16e4d918accb2ed7672fdc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 1 Dec 2019 06:28:42 +0100 Subject: [PATCH 1933/3953] Fix Espalexa being detected as Hue Bridge (#29237) --- homeassistant/components/hue/config_flow.py | 11 +++++++---- tests/components/hue/test_config_flow.py | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index ebd71ba7c1cf9a..375042c88351fb 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -16,6 +16,7 @@ from .errors import AuthenticationRequired, CannotConnect HUE_MANUFACTURERURL = "http://www.philips.com" +HUE_IGNORED_BRIDGE_NAMES = ["HASS Bridge", "Espalexa"] @callback @@ -133,14 +134,16 @@ async def async_step_ssdp(self, discovery_info): This flow is triggered by the SSDP component. It will check if the host is already configured and delegate to the import step if not. """ - from homeassistant.components.ssdp import ATTR_MANUFACTURERURL + from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL: return self.async_abort(reason="not_hue_bridge") - # Filter out emulated Hue - if "HASS Bridge" in discovery_info.get("name", ""): - return self.async_abort(reason="already_configured") + if any( + name in discovery_info.get(ATTR_NAME, "") + for name in HUE_IGNORED_BRIDGE_NAMES + ): + return self.async_abort(reason="not_hue_bridge") host = self.context["host"] = discovery_info.get("host") diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 54082464a7c3b5..a6d221ef323446 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -230,6 +230,26 @@ async def test_bridge_ssdp_emulated_hue(hass): ) assert result["type"] == "abort" + assert result["reason"] == "not_hue_bridge" + + +async def test_bridge_ssdp_espalexa(hass): + """Test if discovery info is from an Espalexa based device.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + flow.context = {} + + result = await flow.async_step_ssdp( + { + "name": "Espalexa (0.0.0.0)", + "host": "0.0.0.0", + "serial": "1234", + "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + } + ) + + assert result["type"] == "abort" + assert result["reason"] == "not_hue_bridge" async def test_bridge_ssdp_already_configured(hass): From 8908dba5b49656204ee12239c2e8b8c1859b8c28 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:30:51 +0100 Subject: [PATCH 1934/3953] Add device trigger for alarm_control_panel (#29068) * Added device trigger to alarm control panel * Added supported features to check that device has certain trigger * Fixed tests for device_trigger * Fixed pylint error * Removed pending trigger and removed trigger condition --- .../alarm_control_panel/device_trigger.py | 151 ++++++++++ .../alarm_control_panel/strings.json | 7 + .../test_device_trigger.py | 257 ++++++++++++++++++ 3 files changed, 415 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/device_trigger.py create mode 100644 tests/components/alarm_control_panel/test_device_trigger.py diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py new file mode 100644 index 00000000000000..95ae17aaaf5621 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -0,0 +1,151 @@ +"""Provides device automations for Alarm control panel.""" +from typing import List + +import voluptuous as vol + +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN + +TRIGGER_TYPES = { + "triggered", + "disarmed", + "armed_home", + "armed_away", + "armed_night", +} + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } +) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Alarm control panel devices.""" + registry = await entity_registry.async_get_registry(hass) + triggers = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + entity_state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the HVAC and preset modes. + if entity_state is None: + continue + + supported_features = entity_state.attributes["supported_features"] + + # Add triggers for each entity that belongs to this integration + triggers += [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "disarmed", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "triggered", + }, + ] + if supported_features & SUPPORT_ALARM_ARM_HOME: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "armed_home", + } + ) + if supported_features & SUPPORT_ALARM_ARM_AWAY: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "armed_away", + } + ) + if supported_features & SUPPORT_ALARM_ARM_NIGHT: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "armed_night", + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + + if config[CONF_TYPE] == "triggered": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_TRIGGERED + elif config[CONF_TYPE] == "disarmed": + from_state = STATE_ALARM_TRIGGERED + to_state = STATE_ALARM_DISARMED + elif config[CONF_TYPE] == "armed_home": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_ARMED_HOME + elif config[CONF_TYPE] == "armed_away": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_ARMED_AWAY + elif config[CONF_TYPE] == "armed_night": + from_state = STATE_ALARM_PENDING + to_state = STATE_ALARM_ARMED_NIGHT + + state_config = { + state.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + state_config = state.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json index f67635776dd8a7..cbca15c8cf6654 100644 --- a/homeassistant/components/alarm_control_panel/strings.json +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -6,6 +6,13 @@ "arm_night": "Arm {entity_name} night", "disarm": "Disarm {entity_name}", "trigger": "Trigger {entity_name}" + }, + "trigger_type": { + "triggered": "{entity_name} triggered", + "disarmed": "{entity_name} disarmed", + "armed_home": "{entity_name} armed home", + "armed_away": "{entity_name} armed away", + "armed_night": "{entity_name} armed night" } } } \ No newline at end of file diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py new file mode 100644 index 00000000000000..ec14cefc2915a3 --- /dev/null +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -0,0 +1,257 @@ +"""The tests for Alarm control panel device triggers.""" +import pytest + +from homeassistant.components.alarm_control_panel import DOMAIN +import homeassistant.components.automation as automation +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, +) +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a alarm_control_panel.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + hass.states.async_set( + "alarm_control_panel.test_5678", "attributes", {"supported_features": 15} + ) + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": "disarmed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "triggered", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "armed_home", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "armed_away", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "platform": "device", + "domain": DOMAIN, + "type": "armed_night", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, calls): + """Test for turn_on and turn_off triggers firing.""" + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "triggered", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "triggered - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "disarmed", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "disarmed - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "armed_home", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "armed_home - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "armed_away", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "armed_away - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "armed_night", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": ( + "armed_night - {{ trigger.platform}} - " + "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " + "{{ trigger.to_state.state}} - {{ trigger.for }}" + ) + }, + }, + }, + ] + }, + ) + + # Fake that the entity is triggered. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_TRIGGERED) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + "some" + ] == "triggered - device - {} - pending - triggered - None".format( + "alarm_control_panel.entity" + ) + + # Fake that the entity is disarmed. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_DISARMED) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data[ + "some" + ] == "disarmed - device - {} - triggered - disarmed - None".format( + "alarm_control_panel.entity" + ) + + # Fake that the entity is armed home. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING) + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_HOME) + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data[ + "some" + ] == "armed_home - device - {} - pending - armed_home - None".format( + "alarm_control_panel.entity" + ) + + # Fake that the entity is armed away. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING) + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_AWAY) + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data[ + "some" + ] == "armed_away - device - {} - pending - armed_away - None".format( + "alarm_control_panel.entity" + ) + + # Fake that the entity is armed night. + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_PENDING) + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_NIGHT) + await hass.async_block_till_done() + assert len(calls) == 5 + assert calls[4].data[ + "some" + ] == "armed_night - device - {} - pending - armed_night - None".format( + "alarm_control_panel.entity" + ) From 2bdf7fc8f5ebaddc242fe1b010b6125e8b68c7ae Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 1 Dec 2019 06:33:11 +0100 Subject: [PATCH 1935/3953] Fix Hue linking with non ASCII chars in location (#29213) * Fix Hue linking with non ASCII chars in location * Use slugify --- homeassistant/components/hue/bridge.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 5015ec669aaa14..9b7559a4efed4e 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -3,6 +3,7 @@ import aiohue import async_timeout +import slugify as unicode_slug import voluptuous as vol from homeassistant.exceptions import ConfigEntryNotReady @@ -173,7 +174,11 @@ 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(f"home-assistant#{hass.config.location_name}") + device_name = unicode_slug.slugify( + hass.config.location_name, max_length=19 + ) + await bridge.create_user(f"home-assistant#{device_name}") + # Initialize bridge (and validate our username) await bridge.initialize() From c4c8a1ba2d7f4612ddc22a593f2a0d08faa164bd Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 06:35:45 +0100 Subject: [PATCH 1936/3953] Move imports to top for sense (#29258) --- homeassistant/components/sense/__init__.py | 12 ++++++------ homeassistant/components/sense/binary_sensor.py | 2 +- homeassistant/components/sense/sensor.py | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index f905c369d72380..ce0d3bce5dc974 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -1,7 +1,12 @@ """Support for monitoring a Sense energy sensor.""" -import logging from datetime import timedelta +import logging +from sense_energy import ( + ASyncSenseable, + SenseAPITimeoutException, + SenseAuthenticationException, +) import voluptuous as vol from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT @@ -36,11 +41,6 @@ async def async_setup(hass, config): """Set up the Sense sensor.""" - from sense_energy import ( - ASyncSenseable, - SenseAuthenticationException, - SenseAPITimeoutException, - ) username = config[DOMAIN][CONF_EMAIL] password = config[DOMAIN][CONF_PASSWORD] diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index ffc3ecb3cab0bb..81f1b64c864388 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -2,8 +2,8 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import SENSE_DATA, SENSE_DEVICE_UPDATE diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 36474620b03f2b..d177a480ddf200 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from sense_energy import SenseAPITimeoutException + from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -114,7 +116,6 @@ def icon(self): async def async_update(self): """Get the latest data, update state.""" - from sense_energy import SenseAPITimeoutException try: await self.update_sensor() From 6a02c6540e2a5349a4186d2878fec71e61edc440 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 1 Dec 2019 00:07:12 -0600 Subject: [PATCH 1937/3953] Stop Plex config flow imports where more user input needed (#29241) * Abort imports that require user interaction, update logs and tests * Disable lint --- homeassistant/components/plex/config_flow.py | 15 +++++++++++ homeassistant/components/plex/strings.json | 1 + tests/components/plex/test_config_flow.py | 26 +++++++++++++++++--- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index cb79c08b16e38c..350f1b3d5773ac 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -80,12 +80,17 @@ async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" errors = {} self.current_login = server_config + is_importing = ( + self.context["source"] # pylint: disable=no-member + == config_entries.SOURCE_IMPORT + ) plex_server = PlexServer(self.hass, server_config) try: await self.hass.async_add_executor_job(plex_server.connect) except NoServersFound: + _LOGGER.error("No servers linked to Plex account") errors["base"] = "no_servers" except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): _LOGGER.error("Invalid credentials provided, config not created") @@ -98,6 +103,11 @@ async def async_step_server_validate(self, server_config): errors["base"] = "not_found" except ServerNotSpecified as available_servers: + if is_importing: + _LOGGER.warning( + "Imported configuration has multiple available Plex servers. Specify server in configuration or add a new Integration." + ) + return self.async_abort(reason="non-interactive") self.available_servers = available_servers.args[0] return await self.async_step_select_server() @@ -106,12 +116,17 @@ async def async_step_server_validate(self, server_config): return self.async_abort(reason="unknown") if errors: + if is_importing: + return self.async_abort(reason="non-interactive") return self.async_show_form(step_id="start_website_auth", errors=errors) server_id = plex_server.machine_identifier for entry in self._async_current_entries(): if entry.data[CONF_SERVER_IDENTIFIER] == server_id: + _LOGGER.debug( + "Plex server already configured: %s", entry.data[CONF_SERVER] + ) return self.async_abort(reason="already_configured") url = plex_server.url_in_use diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index aff79acc2ed4cf..b6491db350cf90 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -25,6 +25,7 @@ "already_in_progress": "Plex is being configured", "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", + "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" } diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index c0d14f1efdcb9c..668ac3b2a17275 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -178,9 +178,8 @@ async def test_import_bad_hostname(hass): CONF_URL: f"http://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", }, ) - assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" - assert result["errors"]["base"] == "not_found" + assert result["type"] == "abort" + assert result["reason"] == "non-interactive" async def test_unknown_exception(hass): @@ -384,12 +383,14 @@ async def test_already_configured(hass): mock_plex_server = MockPlexServer() flow = init_config_flow(hass) + flow.context = {"source": "import"} MockConfigEntry( domain=config_flow.DOMAIN, data={ + config_flow.CONF_SERVER: MOCK_SERVERS[0][config_flow.CONF_SERVER], config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][ config_flow.CONF_SERVER_IDENTIFIER - ] + ], }, ).add_to_hass(hass) @@ -530,3 +531,20 @@ async def test_callback_view(hass, aiohttp_client): resp = await client.get(forward_url) assert resp.status == 200 + + +async def test_multiple_servers_with_import(hass): + """Test importing a config with multiple servers available.""" + + with patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "import"}, + data={CONF_TOKEN: MOCK_TOKEN}, + ) + assert result["type"] == "abort" + assert result["reason"] == "non-interactive" From 5c8a8a631cbccbc1f7f404acbe51b88e540adbeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 1 Dec 2019 14:09:08 +0800 Subject: [PATCH 1938/3953] Add Huawei LTE binary sensor support, mobile connection sensor (#28226) * Add Huawei LTE binary sensor support, mobile connection sensor * Improve mobile connection sensor icon docstring * Remove device class for permission to use a more descriptive icon --- .../components/huawei_lte/__init__.py | 14 +- .../components/huawei_lte/binary_sensor.py | 122 ++++++++++++++++++ homeassistant/components/huawei_lte/const.py | 5 +- 3 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/huawei_lte/binary_sensor.py diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index fa1423edcca2c3..c4c251aef506e6 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -21,6 +21,7 @@ from requests.exceptions import Timeout from url_normalize import url_normalize +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -54,6 +55,7 @@ KEY_DEVICE_INFORMATION, KEY_DEVICE_SIGNAL, KEY_DIALUP_MOBILE_DATASWITCH, + KEY_MONITORING_STATUS, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_WLAN_HOST_LIST, UPDATE_OPTIONS_SIGNAL, @@ -101,6 +103,13 @@ extra=vol.ALLOW_EXTRA, ) +CONFIG_ENTRY_PLATFORMS = ( + BINARY_SENSOR_DOMAIN, + DEVICE_TRACKER_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, +) + @attr.s class Router: @@ -170,6 +179,7 @@ def get_data(key: str, func: Callable[[None], Any]) -> None: get_data(KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information) get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) get_data(KEY_DIALUP_MOBILE_DATASWITCH, self.client.dial_up.mobile_dataswitch) + get_data(KEY_MONITORING_STATUS, self.client.monitoring.status) get_data( KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics ) @@ -314,7 +324,7 @@ def signal_update() -> None: ) # Forward config entry setup to platforms - for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): + for domain in CONFIG_ENTRY_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, domain) ) @@ -357,7 +367,7 @@ async def async_unload_entry( """Unload config entry.""" # Forward config entry unload to platforms - for domain in (DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN): + for domain in CONFIG_ENTRY_PLATFORMS: await hass.config_entries.async_forward_entry_unload(config_entry, domain) # Forget about the router and invoke its cleanup diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py new file mode 100644 index 00000000000000..4fcb400c32a750 --- /dev/null +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -0,0 +1,122 @@ +"""Support for Huawei LTE binary sensors.""" + +import logging +from typing import Optional + +import attr +from huawei_lte_api.enums.cradle import ConnectionStatusEnum + +from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorDevice, +) +from homeassistant.const import CONF_URL +from . import HuaweiLteBaseEntity +from .const import DOMAIN, KEY_MONITORING_STATUS + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up from config entry.""" + router = hass.data[DOMAIN].routers[config_entry.data[CONF_URL]] + entities = [] + + if router.data.get(KEY_MONITORING_STATUS): + entities.append(HuaweiLteMobileConnectionBinarySensor(router)) + + async_add_entities(entities, True) + + +@attr.s +class HuaweiLteBaseBinarySensor(HuaweiLteBaseEntity, BinarySensorDevice): + """Huawei LTE binary sensor device base class.""" + + key: str + item: str + _raw_state: Optional[str] = attr.ib(init=False, default=None) + + async def async_added_to_hass(self): + """Subscribe to needed data on add.""" + await super().async_added_to_hass() + self.router.subscriptions[self.key].add(f"{BINARY_SENSOR_DOMAIN}/{self.item}") + + async def async_will_remove_from_hass(self): + """Unsubscribe from needed data on remove.""" + await super().async_will_remove_from_hass() + self.router.subscriptions[self.key].remove( + f"{BINARY_SENSOR_DOMAIN}/{self.item}" + ) + + async def async_update(self): + """Update state.""" + try: + value = self.router.data[self.key][self.item] + except KeyError: + _LOGGER.debug("%s[%s] not in data", self.key, self.item) + self._available = False + return + self._available = True + self._raw_state = str(value) + + +CONNECTION_STATE_ATTRIBUTES = { + str(ConnectionStatusEnum.CONNECTING): "Connecting", + str(ConnectionStatusEnum.DISCONNECTING): "Disconnecting", + str(ConnectionStatusEnum.CONNECT_FAILED): "Connect failed", + str(ConnectionStatusEnum.CONNECT_STATUS_NULL): "Status not available", + str(ConnectionStatusEnum.CONNECT_STATUS_ERROR): "Status error", +} + + +@attr.s +class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor): + """Huawei LTE mobile connection binary sensor.""" + + def __attrs_post_init__(self): + """Initialize identifiers.""" + self.key = KEY_MONITORING_STATUS + self.item = "ConnectionStatus" + + @property + def _entity_name(self) -> str: + return "Mobile connection" + + @property + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" + + @property + def is_on(self) -> bool: + """Return whether the binary sensor is on.""" + return self._raw_state and int(self._raw_state) in ( + ConnectionStatusEnum.CONNECTED, + ConnectionStatusEnum.DISCONNECTING, + ) + + @property + def assumed_state(self) -> bool: + """Return True if real state is assumed, not known.""" + return not self._raw_state or int(self._raw_state) not in ( + ConnectionStatusEnum.CONNECT_FAILED, + ConnectionStatusEnum.CONNECTED, + ConnectionStatusEnum.DISCONNECTED, + ) + + @property + def icon(self): + """Return mobile connectivity sensor icon.""" + return "mdi:signal" if self.is_on else "mdi:signal-off" + + @property + def device_state_attributes(self): + """Get additional attributes related to connection status.""" + attributes = super().device_state_attributes + if self._raw_state in CONNECTION_STATE_ATTRIBUTES: + if attributes is None: + attributes = {} + attributes["additional_state"] = CONNECTION_STATE_ATTRIBUTES[ + self._raw_state + ] + return attributes diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 8dae63f6538b63..b6e079576ac132 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -16,9 +16,12 @@ KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" KEY_DIALUP_MOBILE_DATASWITCH = "dialup_mobile_dataswitch" +KEY_MONITORING_STATUS = "monitoring_status" KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics" KEY_WLAN_HOST_LIST = "wlan_host_list" +BINARY_SENSOR_KEYS = {KEY_MONITORING_STATUS} + DEVICE_TRACKER_KEYS = {KEY_WLAN_HOST_LIST} SENSOR_KEYS = { @@ -29,4 +32,4 @@ SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH} -ALL_KEYS = DEVICE_TRACKER_KEYS | SENSOR_KEYS | SWITCH_KEYS +ALL_KEYS = BINARY_SENSOR_KEYS | DEVICE_TRACKER_KEYS | SENSOR_KEYS | SWITCH_KEYS From 9f649ed3455d52c6c5e13c485497f97790778b95 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 12:03:51 +0100 Subject: [PATCH 1939/3953] Move imports to top for roku (#29289) --- homeassistant/components/roku/__init__.py | 3 +-- homeassistant/components/roku/media_player.py | 7 ++++--- homeassistant/components/roku/remote.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index aa13814ee6b7a3..b84b6dd1e63fe5 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,6 +1,7 @@ """Support for Roku.""" import logging +from roku import Roku, RokuException import voluptuous as vol from homeassistant.components.discovery import SERVICE_ROKU @@ -64,7 +65,6 @@ def roku_discovered(service, info): def scan_for_rokus(hass): """Scan for devices and present a notification of the ones found.""" - from roku import Roku, RokuException rokus = Roku.discover() @@ -94,7 +94,6 @@ def scan_for_rokus(hass): def _setup_roku(hass, hass_config, roku_config): """Set up a Roku.""" - from roku import Roku host = roku_config[CONF_HOST] diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 84a1982fb49e6d..a785d7b18ff931 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -1,6 +1,8 @@ """Support for the Roku media player.""" import logging + import requests.exceptions +from roku import Roku from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -10,10 +12,10 @@ SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, ) from homeassistant.const import ( CONF_HOST, @@ -54,7 +56,6 @@ class RokuDevice(MediaPlayerDevice): def __init__(self, host): """Initialize the Roku device.""" - from roku import Roku self.roku = Roku(host) self.ip_address = host diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index f443b7e8e74edc..c953d9ba734ed3 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -1,5 +1,6 @@ """Support for the Roku remote.""" import requests.exceptions +from roku import Roku from homeassistant.components import remote from homeassistant.const import CONF_HOST @@ -19,7 +20,6 @@ class RokuRemote(remote.RemoteDevice): def __init__(self, host): """Initialize the Roku device.""" - from roku import Roku self.roku = Roku(host) self._device_info = {} From 84d6a5369f83c9d28c4149e5e788b2ce8b384cf7 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 12:04:12 +0100 Subject: [PATCH 1940/3953] Move imports to top for raincloud (#29283) --- homeassistant/components/raincloud/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/raincloud/__init__.py b/homeassistant/components/raincloud/__init__.py index 77bdcc5aa2f96f..dd851c0b3e3029 100644 --- a/homeassistant/components/raincloud/__init__.py +++ b/homeassistant/components/raincloud/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from raincloudy.core import RainCloudy from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol @@ -96,8 +97,6 @@ def setup(hass, config): scan_interval = conf.get(CONF_SCAN_INTERVAL) try: - from raincloudy.core import RainCloudy - raincloud = RainCloudy(username=username, password=password) if not raincloud.is_connected: raise HTTPError From 8fbfb6bf8cf82383aba96d48f06b1759a6f02885 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 12:04:47 +0100 Subject: [PATCH 1941/3953] Move imports to top for rocketchat (#29288) --- homeassistant/components/rocketchat/notify.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/rocketchat/notify.py b/homeassistant/components/rocketchat/notify.py index 8657d0f9450529..2b5b8dcd235fdc 100644 --- a/homeassistant/components/rocketchat/notify.py +++ b/homeassistant/components/rocketchat/notify.py @@ -1,16 +1,20 @@ """Rocket.Chat notification service.""" import logging +from rocketchat_API.APIExceptions.RocketExceptions import ( + RocketAuthenticationException, + RocketConnectionException, +) +from rocketchat_API.rocketchat import RocketChat import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_ROOM, CONF_URL, CONF_USERNAME -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_PASSWORD, CONF_ROOM, CONF_URL, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -27,10 +31,6 @@ def get_service(hass, config, discovery_info=None): """Return the notify service.""" - from rocketchat_API.APIExceptions.RocketExceptions import ( - RocketConnectionException, - RocketAuthenticationException, - ) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -54,7 +54,6 @@ class RocketChatNotificationService(BaseNotificationService): def __init__(self, url, username, password, room): """Initialize the service.""" - from rocketchat_API.rocketchat import RocketChat self._room = room self._server = RocketChat(username, password, server_url=url) From 3c9d2b552e5380878dd3feb7974d10e15abff9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 1 Dec 2019 14:20:27 +0100 Subject: [PATCH 1942/3953] Upgrade Tibber library (#29290) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 11c5a676bf6b5c..99c1335517e4cd 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", "requirements": [ - "pyTibber==0.11.7" + "pyTibber==0.12.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c774e45fbd7c8d..0c5cb1fe513e2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1084,7 +1084,7 @@ pyRFXtrx==0.24 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.11.7 +pyTibber==0.12.0 # homeassistant.components.dlink pyW215==0.6.0 From e2e53be5acf4c3a1fb6451945df5efbe5232c56e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 15:23:18 +0100 Subject: [PATCH 1943/3953] Move imports to top for ripple (#29287) --- homeassistant/components/ripple/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ripple/sensor.py b/homeassistant/components/ripple/sensor.py index ebbcec708c3a51..ab0da77b173b04 100644 --- a/homeassistant/components/ripple/sensor.py +++ b/homeassistant/components/ripple/sensor.py @@ -1,6 +1,7 @@ """Support for Ripple sensors.""" from datetime import timedelta +from pyripple import get_balance import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -62,7 +63,6 @@ def device_state_attributes(self): def update(self): """Get the latest state of the sensor.""" - from pyripple import get_balance balance = get_balance(self.address) if balance is not None: From d0ed9b32ac3ceb3b19230753bce05b54cb710e5e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 15:24:11 +0100 Subject: [PATCH 1944/3953] Move imports to top for remote_rpi_gpio (#29286) --- homeassistant/components/remote_rpi_gpio/__init__.py | 7 +++---- homeassistant/components/remote_rpi_gpio/binary_sensor.py | 8 +++----- homeassistant/components/remote_rpi_gpio/switch.py | 5 ++--- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/remote_rpi_gpio/__init__.py b/homeassistant/components/remote_rpi_gpio/__init__.py index 33356d0e3b82cc..e1b66128e3f4f2 100644 --- a/homeassistant/components/remote_rpi_gpio/__init__.py +++ b/homeassistant/components/remote_rpi_gpio/__init__.py @@ -1,6 +1,9 @@ """Support for controlling GPIO pins of a Raspberry Pi.""" import logging +from gpiozero import LED, Button +from gpiozero.pins.pigpio import PiGPIOFactory + _LOGGER = logging.getLogger(__name__) CONF_BOUNCETIME = "bouncetime" @@ -21,8 +24,6 @@ def setup(hass, config): def setup_output(address, port, invert_logic): """Set up a GPIO as output.""" - from gpiozero import LED - from gpiozero.pins.pigpio import PiGPIOFactory try: return LED(port, active_high=invert_logic, pin_factory=PiGPIOFactory(address)) @@ -32,8 +33,6 @@ def setup_output(address, port, invert_logic): def setup_input(address, port, pull_mode, bouncetime): """Set up a GPIO as input.""" - from gpiozero import Button - from gpiozero.pins.pigpio import PiGPIOFactory if pull_mode == "UP": pull_gpio_up = True diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index e12d83324fd756..862bd30ae432da 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -1,19 +1,17 @@ """Support for binary sensor using RPi GPIO.""" import logging -import voluptuous as vol - import requests +import voluptuous as vol +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_HOST -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA - import homeassistant.helpers.config_validation as cv from . import ( CONF_BOUNCETIME, - CONF_PULL_MODE, CONF_INVERT_LOGIC, + CONF_PULL_MODE, DEFAULT_BOUNCETIME, DEFAULT_INVERT_LOGIC, DEFAULT_PULL_MODE, diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index 8240de7951d710..a5b255179cde38 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -3,9 +3,8 @@ import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_HOST - +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv from . import CONF_INVERT_LOGIC, DEFAULT_INVERT_LOGIC From a0a348a200b88210cd3b20fa19f7e1def916cd38 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 15:24:48 +0100 Subject: [PATCH 1945/3953] Move imports to top for recswitch (#29285) --- homeassistant/components/recswitch/switch.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/recswitch/switch.py b/homeassistant/components/recswitch/switch.py index aa93693a36d43a..c242f23dfddb07 100644 --- a/homeassistant/components/recswitch/switch.py +++ b/homeassistant/components/recswitch/switch.py @@ -2,13 +2,13 @@ import logging +from pyrecswitch import RSNetwork, RSNetworkError import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "RecSwitch {0}" @@ -26,7 +26,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the device.""" - from pyrecswitch import RSNetwork host = config[CONF_HOST] mac_address = config[CONF_MAC] @@ -78,7 +77,6 @@ async def async_turn_off(self, **kwargs): async def async_set_gpio_status(self, status): """Set the switch status.""" - from pyrecswitch import RSNetworkError try: ret = await self.device.set_gpio_status(status) @@ -88,7 +86,6 @@ async def async_set_gpio_status(self, status): async def async_update(self): """Update the current switch status.""" - from pyrecswitch import RSNetworkError try: ret = await self.device.get_gpio_status() From 6c9291c7a6fce34dd3abdb8c1c6e7dafd1079c9b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 15:25:57 +0100 Subject: [PATCH 1946/3953] Move imports to top for raspyrfm (#29284) --- homeassistant/components/raspyrfm/switch.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index 53cb1dbdcb5020..ec07119b96a330 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -1,6 +1,15 @@ """Support for switches that can be controlled using the RaspyRFM rc module.""" import logging +from raspyrfm_client import RaspyRFMClient +from raspyrfm_client.device_implementations.controlunit.actions import Action +from raspyrfm_client.device_implementations.controlunit.controlunit_constants import ( + ControlUnitModel, +) +from raspyrfm_client.device_implementations.gateway.manufacturer.gateway_constants import ( + GatewayModel, +) +from raspyrfm_client.device_implementations.manufacturer_constants import Manufacturer import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -46,16 +55,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RaspyRFM switch.""" - from raspyrfm_client import RaspyRFMClient - from raspyrfm_client.device_implementations.controlunit.controlunit_constants import ( - ControlUnitModel, - ) - from raspyrfm_client.device_implementations.gateway.manufacturer.gateway_constants import ( - GatewayModel, - ) - from raspyrfm_client.device_implementations.manufacturer_constants import ( - Manufacturer, - ) gateway_manufacturer = config.get( CONF_GATEWAY_MANUFACTURER, Manufacturer.SEEGEL_SYSTEME.value @@ -123,7 +122,6 @@ def is_on(self): def turn_on(self, **kwargs): """Turn the switch on.""" - from raspyrfm_client.device_implementations.controlunit.actions import Action self._raspyrfm_client.send(self._gateway, self._controlunit, Action.ON) self._state = True @@ -131,7 +129,6 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn the switch off.""" - from raspyrfm_client.device_implementations.controlunit.actions import Action if Action.OFF in self._controlunit.get_supported_actions(): self._raspyrfm_client.send(self._gateway, self._controlunit, Action.OFF) From 64628c1f70d686f086575f823a6e89bbd7c23252 Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Sun, 1 Dec 2019 17:46:12 +0100 Subject: [PATCH 1947/3953] Add ATEN PE component for ATEN eco PDUs (#27960) * Add ATEN PE component for ATEN eco PDUs * aten_pe: Require host configuration * aten_pe: Do not import from other integrations * aten_pe: Include unnamed outlets * aten_pe: Avoid get() for config entries having default values * aten_pe: Fix documentation URI * aten_pe: Remove unused return value * aten_pe: Update atenpdu lib to 0.2.0 for asyncio * aten_pe: Raise exception if initialization fails * aten_pe: Update atenpdu lib to 0.3.0 for improved exception handling --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/aten_pe/__init__.py | 1 + .../components/aten_pe/manifest.json | 12 ++ homeassistant/components/aten_pe/switch.py | 122 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 140 insertions(+) create mode 100644 homeassistant/components/aten_pe/__init__.py create mode 100644 homeassistant/components/aten_pe/manifest.json create mode 100644 homeassistant/components/aten_pe/switch.py diff --git a/.coveragerc b/.coveragerc index 33cbb0ed450733..600701d792b4f6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -62,6 +62,7 @@ omit = homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* homeassistant/components/asuswrt/device_tracker.py + homeassistant/components/aten_pe/* homeassistant/components/atome/* homeassistant/components/august/* homeassistant/components/aurora_abb_powerone/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index d7155e8f91796b..b76909de9d6b8b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -32,6 +32,7 @@ homeassistant/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff homeassistant/components/arest/* @fabaff homeassistant/components/asuswrt/* @kennedyshead +homeassistant/components/aten_pe/* @mtdcr homeassistant/components/atome/* @baqs homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core diff --git a/homeassistant/components/aten_pe/__init__.py b/homeassistant/components/aten_pe/__init__.py new file mode 100644 index 00000000000000..2a0fb277a48c32 --- /dev/null +++ b/homeassistant/components/aten_pe/__init__.py @@ -0,0 +1 @@ +"""The ATEN PE component.""" diff --git a/homeassistant/components/aten_pe/manifest.json b/homeassistant/components/aten_pe/manifest.json new file mode 100644 index 00000000000000..4f6416dd76c6da --- /dev/null +++ b/homeassistant/components/aten_pe/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "aten_pe", + "name": "ATEN eco PDUs", + "documentation": "https://www.home-assistant.io/integrations/aten_pe", + "requirements": [ + "atenpdu==0.3.0" + ], + "dependencies": [], + "codeowners": [ + "@mtdcr" + ] +} diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py new file mode 100644 index 00000000000000..2ec6ec4b83d544 --- /dev/null +++ b/homeassistant/components/aten_pe/switch.py @@ -0,0 +1,122 @@ +"""The ATEN PE switch component.""" + +import logging + +from atenpdu import AtenPE, AtenPEError +import voluptuous as vol + +from homeassistant.components.switch import ( + DEVICE_CLASS_OUTLET, + PLATFORM_SCHEMA, + SwitchDevice, +) +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_USERNAME +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_AUTH_KEY = "auth_key" +CONF_COMMUNITY = "community" +CONF_PRIV_KEY = "priv_key" +DEFAULT_COMMUNITY = "private" +DEFAULT_PORT = "161" +DEFAULT_USERNAME = "administrator" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_AUTH_KEY): cv.string, + vol.Optional(CONF_PRIV_KEY): cv.string, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the ATEN PE switch.""" + node = config[CONF_HOST] + serv = config[CONF_PORT] + + dev = AtenPE( + node=node, + serv=serv, + community=config[CONF_COMMUNITY], + username=config[CONF_USERNAME], + authkey=config.get(CONF_AUTH_KEY), + privkey=config.get(CONF_PRIV_KEY), + ) + + try: + await hass.async_add_executor_job(dev.initialize) + mac = await dev.deviceMAC() + outlets = dev.outlets() + except AtenPEError as exc: + _LOGGER.error("Failed to initialize %s:%s: %s", node, serv, str(exc)) + raise PlatformNotReady + + switches = [] + async for outlet in outlets: + switches.append(AtenSwitch(dev, mac, outlet.id, outlet.name)) + + async_add_entities(switches) + + +class AtenSwitch(SwitchDevice): + """Represents an ATEN PE switch.""" + + def __init__(self, device, mac, outlet, name): + """Initialize an ATEN PE switch.""" + self._device = device + self._mac = mac + self._outlet = outlet + self._name = name or f"Outlet {outlet}" + self._enabled = False + self._outlet_power = 0.0 + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._mac}-{self._outlet}" + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def device_class(self) -> str: + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_OUTLET + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self._enabled + + @property + def current_power_w(self) -> float: + """Return the current power usage in W.""" + return self._outlet_power + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + await self._device.setOutletStatus(self._outlet, "on") + self._enabled = True + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + await self._device.setOutletStatus(self._outlet, "off") + self._enabled = False + + async def async_update(self): + """Process update from entity.""" + status = await self._device.displayOutletStatus(self._outlet) + if status == "on": + self._enabled = True + self._outlet_power = await self._device.outletPower(self._outlet) + elif status == "off": + self._enabled = False + self._outlet_power = 0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0c5cb1fe513e2b..60b8dbb43cab6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -251,6 +251,9 @@ asterisk_mbox==0.5.0 # homeassistant.components.upnp async-upnp-client==0.14.12 +# homeassistant.components.aten_pe +atenpdu==0.3.0 + # homeassistant.components.aurora_abb_powerone aurorapy==0.2.6 From 9927f6c17d047653fbf9f1a844b37ae819d43c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 2 Dec 2019 00:58:26 +0800 Subject: [PATCH 1948/3953] Upgrade mypy to 0.750 (#29294) https://mypy-lang.blogspot.com/2019/11/mypy-0.html --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 6ccb0168677d47..9d63b59f62ad68 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ asynctest==0.13.0 codecov==2.0.15 mock-open==1.3.1 -mypy==0.740 +mypy==0.750 pre-commit==1.20.0 pylint==2.4.4 astroid==2.3.3 From 22225cea4d92972604b2e122158ff81d08bc457b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 1 Dec 2019 18:19:17 +0100 Subject: [PATCH 1949/3953] Move imports to top for seventeentrack (#29264) * Move imports to top for seventeentrack * Updated patch target path in test_sensor.py --- homeassistant/components/seventeentrack/sensor.py | 9 ++++----- tests/components/seventeentrack/test_sensor.py | 5 ++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 33abe2f1f861df..167f4347c0cc61 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -1,7 +1,9 @@ """Support for package tracking sensors from 17track.net.""" -import logging from datetime import timedelta +import logging +from py17track import Client as SeventeenTrackClient +from py17track.errors import SeventeenTrackError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -61,12 +63,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Configure the platform and add the sensors.""" - from py17track import Client - from py17track.errors import SeventeenTrackError websession = aiohttp_client.async_get_clientsession(hass) - client = Client(websession) + client = SeventeenTrackClient(websession) try: login_result = await client.profile.login( @@ -290,7 +290,6 @@ def __init__( async def _async_update(self): """Get updated data from 17track.net.""" - from py17track.errors import SeventeenTrackError try: packages = await self._client.profile.packages( diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index dff3cc40b9e2ee..45ab8a62225426 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -119,7 +119,10 @@ def fixture_mock_py17track(): @pytest.fixture(autouse=True, name="mock_client") def fixture_mock_client(mock_py17track): """Mock py17track client.""" - with mock.patch("py17track.Client", new=ClientMock): + with mock.patch( + "homeassistant.components.seventeentrack.sensor.SeventeenTrackClient", + new=ClientMock, + ): yield ProfileMock.reset() From a9baa24fda1ee3689acfa757d7e3b9ef1cc17968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 2 Dec 2019 03:58:31 +0800 Subject: [PATCH 1950/3953] Improve naming and attrs of hostnameless Huawei LTE device tracker entities (#29281) --- homeassistant/components/huawei_lte/device_tracker.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index d95d99e71264e2..29d7f437b5f54a 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -145,11 +145,10 @@ async def async_update(self) -> None: host = next((x for x in hosts if x.get("MacAddress") == self.mac), None) self._is_connected = host is not None if self._is_connected: - self._name = host.get("HostName", self.mac) + # HostName may be present with explicit None value + self._name = host.get("HostName") or self.mac self._device_state_attributes = { - _better_snakecase(k): v - for k, v in host.items() - if k not in ("MacAddress", "HostName") + _better_snakecase(k): v for k, v in host.items() if k != "HostName" } From d91dd68b31d5642e17c9fffff2f161f673113482 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 1 Dec 2019 22:24:16 +0100 Subject: [PATCH 1951/3953] Limit parallel requests to Philips Hue (#29189) * Limit parallel requests to Philips Hue * Fix tests * Remove loop * Update homeassistant/components/hue/bridge.py Co-Authored-By: Paulus Schoutsen --- homeassistant/components/hue/bridge.py | 11 +++++++++++ homeassistant/components/hue/light.py | 10 +++++----- homeassistant/components/hue/sensor_base.py | 2 +- tests/components/hue/test_light.py | 4 ++++ tests/components/hue/test_sensor_base.py | 4 ++++ 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 9b7559a4efed4e..5a5e55773a5da0 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -33,6 +33,7 @@ def __init__(self, hass, config_entry, allow_unreachable, allow_groups): self.available = True self.authorized = False self.api = None + self.parallel_updates_semaphore = None @property def host(self): @@ -78,9 +79,19 @@ async def async_setup(self, tries=0): DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, schema=SCENE_SCHEMA ) + self.parallel_updates_semaphore = asyncio.Semaphore( + 3 if self.api.config.modelid == "BSB001" else 10 + ) + self.authorized = True return True + async def async_request_call(self, coro): + """Process request batched.""" + + async with self.parallel_updates_semaphore: + return await coro + async def async_reset(self): """Reset this bridge to default state. diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index d58e4608b65582..ad511639d57804 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -202,7 +202,7 @@ async def async_update_items( try: start = monotonic() with async_timeout.timeout(4): - await api.update() + await bridge.async_request_call(api.update()) except aiohue.Unauthorized: await bridge.handle_unauthorized_error() return @@ -434,9 +434,9 @@ async def async_turn_on(self, **kwargs): command["effect"] = "none" if self.is_group: - await self.light.set_action(**command) + await self.bridge.async_request_call(self.light.set_action(**command)) else: - await self.light.set_state(**command) + await self.bridge.async_request_call(self.light.set_state(**command)) async def async_turn_off(self, **kwargs): """Turn the specified or all lights off.""" @@ -457,9 +457,9 @@ async def async_turn_off(self, **kwargs): command["alert"] = "none" if self.is_group: - await self.light.set_action(**command) + await self.bridge.async_request_call(self.light.set_action(**command)) else: - await self.light.set_state(**command) + await self.bridge.async_request_call(self.light.set_state(**command)) async def async_update(self): """Synchronize state with bridge.""" diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index bf64fed0c9516e..f7882b102c0742 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -101,7 +101,7 @@ async def async_update_items(self): try: start = monotonic() with async_timeout.timeout(4): - await api.update() + await self.bridge.async_request_call(api.update()) except Unauthorized: await self.bridge.handle_unauthorized_error() return diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 88c527a50ca872..5b10ff9446cdd7 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -204,6 +204,10 @@ async def mock_request(method, path, **kwargs): return bridge.mock_group_responses.popleft() return None + async def async_request_call(coro): + await coro + + bridge.async_request_call = async_request_call bridge.api.config.apiversion = "9.9.9" bridge.api.lights = Lights({}, mock_request) bridge.api.groups = Groups({}, mock_request) diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index ba259dccf7107c..ad927767c3075b 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -277,6 +277,10 @@ async def mock_request(method, path, **kwargs): return bridge.mock_sensor_responses.popleft() return None + async def async_request_call(coro): + await coro + + bridge.async_request_call = async_request_call bridge.api.config.apiversion = "9.9.9" bridge.api.sensors = Sensors({}, mock_request) return bridge From d1aa0cea974194136d626fe5ce5818b380b06f30 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Dec 2019 14:12:57 -0800 Subject: [PATCH 1952/3953] Move intent registration to own integration (#29280) * Move intent registration to own integration. * Lint --- .../components/conversation/default_agent.py | 12 ++- homeassistant/components/cover/__init__.py | 14 --- homeassistant/components/cover/intent.py | 22 +++++ homeassistant/components/intent/__init__.py | 33 +++++++ homeassistant/components/light/__init__.py | 64 -------------- homeassistant/components/light/intent.py | 84 ++++++++++++++++++ .../components/shopping_list/__init__.py | 49 ----------- .../components/shopping_list/intent.py | 55 ++++++++++++ tests/components/conversation/test_init.py | 28 ------ .../cover/{test_init.py => test_intent.py} | 13 +-- tests/components/intent/test_init.py | 32 +++++++ tests/components/light/test_init.py | 84 ------------------ tests/components/light/test_intent.py | 88 +++++++++++++++++++ tests/components/shopping_list/conftest.py | 23 +++++ tests/components/shopping_list/test_init.py | 55 +++--------- tests/components/shopping_list/test_intent.py | 22 +++++ 16 files changed, 389 insertions(+), 289 deletions(-) create mode 100644 homeassistant/components/cover/intent.py create mode 100644 homeassistant/components/light/intent.py create mode 100644 homeassistant/components/shopping_list/intent.py rename tests/components/cover/{test_init.py => test_intent.py} (83%) create mode 100644 tests/components/light/test_intent.py create mode 100644 tests/components/shopping_list/conftest.py create mode 100644 tests/components/shopping_list/test_intent.py diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index e562eed7e666a1..2f09cba2eb196b 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -3,9 +3,12 @@ import re from typing import Optional -from homeassistant import core -from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER -from homeassistant.components.shopping_list import INTENT_ADD_ITEM, INTENT_LAST_ITEMS +from homeassistant import core, setup +from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER +from homeassistant.components.shopping_list.intent import ( + INTENT_ADD_ITEM, + INTENT_LAST_ITEMS, +) from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import callback from homeassistant.helpers import intent @@ -58,6 +61,9 @@ def __init__(self, hass: core.HomeAssistant): async def async_initialize(self, config): """Initialize the default agent.""" + if "intent" not in self.hass.config.components: + await setup.async_setup_component(self.hass, "intent", {}) + config = config.get(DOMAIN, {}) intents = self.hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index d57bf678a69717..a3c28a77cbe64a 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -15,7 +15,6 @@ ) from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.components import group -from homeassistant.helpers import intent from homeassistant.const import ( SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, @@ -83,8 +82,6 @@ ATTR_POSITION = "position" ATTR_TILT_POSITION = "tilt_position" -INTENT_OPEN_COVER = "HassOpenCover" -INTENT_CLOSE_COVER = "HassCloseCover" COVER_SET_COVER_POSITION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( {vol.Required(ATTR_POSITION): vol.All(vol.Coerce(int), vol.Range(min=0, max=100))} @@ -158,17 +155,6 @@ async def async_setup(hass, config): SERVICE_TOGGLE_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_toggle_tilt" ) - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, "Opened {}" - ) - ) - hass.helpers.intent.async_register( - intent.ServiceIntentHandler( - INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, "Closed {}" - ) - ) - return True diff --git a/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py new file mode 100644 index 00000000000000..f8d13e6a90ebe5 --- /dev/null +++ b/homeassistant/components/cover/intent.py @@ -0,0 +1,22 @@ +"""Intents for the cover integration.""" +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from . import DOMAIN, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER + +INTENT_OPEN_COVER = "HassOpenCover" +INTENT_CLOSE_COVER = "HassCloseCover" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the cover intents.""" + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, "Opened {}" + ) + ) + hass.helpers.intent.async_register( + intent.ServiceIntentHandler( + INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, "Closed {}" + ) + ) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 758b77ba108d27..31ab36ecc89f65 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -1,22 +1,55 @@ """The Intent integration.""" +import asyncio +import logging + import voluptuous as vol from homeassistant.core import HomeAssistant +from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.setup import ATTR_COMPONENT from homeassistant.components import http from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.helpers import config_validation as cv, intent +from homeassistant.loader import async_get_integration, IntegrationNotFound from .const import DOMAIN CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) +_LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: dict): """Set up the Intent component.""" hass.http.register_view(IntentHandleView()) + + tasks = [_async_process_intent(hass, comp) for comp in hass.config.components] + + async def async_component_loaded(event): + """Handle a new component loaded.""" + await _async_process_intent(hass, event.data[ATTR_COMPONENT]) + + hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_component_loaded) + + if tasks: + await asyncio.gather(*tasks) + return True +async def _async_process_intent(hass: HomeAssistant, component_name: str): + """Process the intents of a component.""" + try: + integration = await async_get_integration(hass, component_name) + platform = integration.get_platform(DOMAIN) + except (IntegrationNotFound, ImportError): + return + + try: + await platform.async_setup_intents(hass) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error setting up intents for %s", component_name) + + class IntentHandleView(http.HomeAssistantView): """View to handle intents from JSON.""" diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b33cb29421e21a..0d60f3f6b52337 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -26,7 +26,6 @@ ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers import intent from homeassistant.loader import bind_hass import homeassistant.util.color as color_util @@ -141,8 +140,6 @@ vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte)) ) -INTENT_SET = "HassLightSet" - _LOGGER = logging.getLogger(__name__) @@ -196,65 +193,6 @@ def preprocess_turn_off(params): return (False, None) # Light should be turned on -class SetIntentHandler(intent.IntentHandler): - """Handle set color intents.""" - - intent_type = INTENT_SET - slot_schema = { - vol.Required("name"): cv.string, - vol.Optional("color"): color_util.color_name_to_rgb, - vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)), - } - - async def async_handle(self, intent_obj): - """Handle the hass intent.""" - hass = intent_obj.hass - slots = self.async_validate_slots(intent_obj.slots) - state = hass.helpers.intent.async_match_state( - slots["name"]["value"], - [state for state in hass.states.async_all() if state.domain == DOMAIN], - ) - - service_data = {ATTR_ENTITY_ID: state.entity_id} - speech_parts = [] - - if "color" in slots: - intent.async_test_feature(state, SUPPORT_COLOR, "changing colors") - service_data[ATTR_RGB_COLOR] = slots["color"]["value"] - # Use original passed in value of the color because we don't have - # human readable names for that internally. - speech_parts.append( - "the color {}".format(intent_obj.slots["color"]["value"]) - ) - - if "brightness" in slots: - intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness") - service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] - speech_parts.append("{}% brightness".format(slots["brightness"]["value"])) - - await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context - ) - - response = intent_obj.create_response() - - if not speech_parts: # No attributes changed - speech = f"Turned on {state.name}" - else: - parts = [f"Changed {state.name} to"] - for index, part in enumerate(speech_parts): - if index == 0: - parts.append(f" {part}") - elif index != len(speech_parts) - 1: - parts.append(f", {part}") - else: - parts.append(f" and {part}") - speech = "".join(parts) - - response.async_set_speech(speech) - return response - - async def async_setup(hass, config): """Expose light control via state machine and services.""" component = hass.data[DOMAIN] = EntityComponent( @@ -341,8 +279,6 @@ async def async_handle_light_on_service(service): SERVICE_TOGGLE, LIGHT_TOGGLE_SCHEMA, "async_toggle" ) - hass.helpers.intent.async_register(SetIntentHandler()) - return True diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py new file mode 100644 index 00000000000000..93b9748fc4a3da --- /dev/null +++ b/homeassistant/components/light/intent.py @@ -0,0 +1,84 @@ +"""Intents for the light integration.""" +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent +import homeassistant.util.color as color_util +import homeassistant.helpers.config_validation as cv + +from . import ( + ATTR_ENTITY_ID, + SUPPORT_COLOR, + ATTR_RGB_COLOR, + ATTR_BRIGHTNESS_PCT, + SUPPORT_BRIGHTNESS, + DOMAIN, + SERVICE_TURN_ON, +) + + +INTENT_SET = "HassLightSet" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the light intents.""" + hass.helpers.intent.async_register(SetIntentHandler()) + + +class SetIntentHandler(intent.IntentHandler): + """Handle set color intents.""" + + intent_type = INTENT_SET + slot_schema = { + vol.Required("name"): cv.string, + vol.Optional("color"): color_util.color_name_to_rgb, + vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)), + } + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + """Handle the hass intent.""" + hass = intent_obj.hass + slots = self.async_validate_slots(intent_obj.slots) + state = hass.helpers.intent.async_match_state( + slots["name"]["value"], + [state for state in hass.states.async_all() if state.domain == DOMAIN], + ) + + service_data = {ATTR_ENTITY_ID: state.entity_id} + speech_parts = [] + + if "color" in slots: + intent.async_test_feature(state, SUPPORT_COLOR, "changing colors") + service_data[ATTR_RGB_COLOR] = slots["color"]["value"] + # Use original passed in value of the color because we don't have + # human readable names for that internally. + speech_parts.append( + "the color {}".format(intent_obj.slots["color"]["value"]) + ) + + if "brightness" in slots: + intent.async_test_feature(state, SUPPORT_BRIGHTNESS, "changing brightness") + service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] + speech_parts.append("{}% brightness".format(slots["brightness"]["value"])) + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context + ) + + response = intent_obj.create_response() + + if not speech_parts: # No attributes changed + speech = f"Turned on {state.name}" + else: + parts = [f"Changed {state.name} to"] + for index, part in enumerate(speech_parts): + if index == 0: + parts.append(f" {part}") + elif index != len(speech_parts) - 1: + parts.append(f", {part}") + else: + parts.append(f" and {part}") + speech = "".join(parts) + + response.async_set_speech(speech) + return response diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index a5e901b8c6e7be..850b06332f8899 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -9,7 +9,6 @@ from homeassistant.core import callback from homeassistant.components import http from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json from homeassistant.components import websocket_api @@ -20,8 +19,6 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA) EVENT = "shopping_list_updated" -INTENT_ADD_ITEM = "HassShoppingListAddItem" -INTENT_LAST_ITEMS = "HassShoppingListLastItems" ITEM_UPDATE_SCHEMA = vol.Schema({"complete": bool, ATTR_NAME: str}) PERSISTENCE = ".shopping_list.json" @@ -86,9 +83,6 @@ def complete_item_service(call): data = hass.data[DOMAIN] = ShoppingData(hass) yield from data.async_load() - intent.async_register(hass, AddItemIntent()) - intent.async_register(hass, ListTopItemsIntent()) - hass.services.async_register( DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA ) @@ -175,49 +169,6 @@ def save(self): save_json(self.hass.config.path(PERSISTENCE), self.items) -class AddItemIntent(intent.IntentHandler): - """Handle AddItem intents.""" - - intent_type = INTENT_ADD_ITEM - slot_schema = {"item": cv.string} - - @asyncio.coroutine - def async_handle(self, intent_obj): - """Handle the intent.""" - slots = self.async_validate_slots(intent_obj.slots) - item = slots["item"]["value"] - intent_obj.hass.data[DOMAIN].async_add(item) - - response = intent_obj.create_response() - response.async_set_speech(f"I've added {item} to your shopping list") - intent_obj.hass.bus.async_fire(EVENT) - return response - - -class ListTopItemsIntent(intent.IntentHandler): - """Handle AddItem intents.""" - - intent_type = INTENT_LAST_ITEMS - slot_schema = {"item": cv.string} - - @asyncio.coroutine - def async_handle(self, intent_obj): - """Handle the intent.""" - items = intent_obj.hass.data[DOMAIN].items[-5:] - response = intent_obj.create_response() - - if not items: - response.async_set_speech("There are no items on your shopping list") - else: - response.async_set_speech( - "These are the top {} items on your shopping list: {}".format( - min(len(items), 5), - ", ".join(itm["name"] for itm in reversed(items)), - ) - ) - return response - - class ShoppingListView(http.HomeAssistantView): """View to retrieve shopping list content.""" diff --git a/homeassistant/components/shopping_list/intent.py b/homeassistant/components/shopping_list/intent.py new file mode 100644 index 00000000000000..21ae7181e895bf --- /dev/null +++ b/homeassistant/components/shopping_list/intent.py @@ -0,0 +1,55 @@ +"""Intents for the Shopping List integration.""" +from homeassistant.helpers import intent +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN, EVENT + +INTENT_ADD_ITEM = "HassShoppingListAddItem" +INTENT_LAST_ITEMS = "HassShoppingListLastItems" + + +async def async_setup_intents(hass): + """Set up the Shopping List intents.""" + intent.async_register(hass, AddItemIntent()) + intent.async_register(hass, ListTopItemsIntent()) + + +class AddItemIntent(intent.IntentHandler): + """Handle AddItem intents.""" + + intent_type = INTENT_ADD_ITEM + slot_schema = {"item": cv.string} + + async def async_handle(self, intent_obj): + """Handle the intent.""" + slots = self.async_validate_slots(intent_obj.slots) + item = slots["item"]["value"] + intent_obj.hass.data[DOMAIN].async_add(item) + + response = intent_obj.create_response() + response.async_set_speech(f"I've added {item} to your shopping list") + intent_obj.hass.bus.async_fire(EVENT) + return response + + +class ListTopItemsIntent(intent.IntentHandler): + """Handle AddItem intents.""" + + intent_type = INTENT_LAST_ITEMS + slot_schema = {"item": cv.string} + + async def async_handle(self, intent_obj): + """Handle the intent.""" + items = intent_obj.hass.data[DOMAIN].items[-5:] + response = intent_obj.create_response() + + if not items: + response.async_set_speech("There are no items on your shopping list") + else: + response.async_set_speech( + "These are the top {} items on your shopping list: {}".format( + min(len(items), 5), + ", ".join(itm["name"] for itm in reversed(items)), + ) + ) + return response diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 6d318deacdd72d..45008ef9447600 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1,11 +1,9 @@ """The tests for the Conversation component.""" -# pylint: disable=protected-access import pytest from homeassistant.core import DOMAIN as HASS_DOMAIN, Context from homeassistant.setup import async_setup_component from homeassistant.components import conversation -from homeassistant.components.cover import SERVICE_OPEN_COVER from homeassistant.helpers import intent from tests.common import async_mock_intent, async_mock_service @@ -153,32 +151,6 @@ async def test_turn_on_intent(hass, sentence): assert call.data == {"entity_id": "light.kitchen"} -async def test_cover_intents_loading(hass): - """Test Cover Intents Loading.""" - with pytest.raises(intent.UnknownIntent): - await intent.async_handle( - hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} - ) - - result = await async_setup_component(hass, "cover", {}) - assert result - - hass.states.async_set("cover.garage_door", "closed") - calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) - - response = await intent.async_handle( - hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} - ) - await hass.async_block_till_done() - - assert response.speech["plain"]["speech"] == "Opened garage door" - assert len(calls) == 1 - call = calls[0] - assert call.domain == "cover" - assert call.service == "open_cover" - assert call.data == {"entity_id": "cover.garage_door"} - - @pytest.mark.parametrize("sentence", ("turn off kitchen", "turn kitchen off")) async def test_turn_off_intent(hass, sentence): """Test calling the turn on intent.""" diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_intent.py similarity index 83% rename from tests/components/cover/test_init.py rename to tests/components/cover/test_intent.py index d1ca17d18f3525..ce01e882941bde 100644 --- a/tests/components/cover/test_init.py +++ b/tests/components/cover/test_intent.py @@ -1,15 +1,17 @@ """The tests for the cover platform.""" -from homeassistant.components.cover import SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER +from homeassistant.components.cover import ( + SERVICE_OPEN_COVER, + SERVICE_CLOSE_COVER, + intent as cover_intent, +) from homeassistant.helpers import intent -import homeassistant.components as comps from tests.common import async_mock_service async def test_open_cover_intent(hass): """Test HassOpenCover intent.""" - result = await comps.cover.async_setup(hass, {}) - assert result + await cover_intent.async_setup_intents(hass) hass.states.async_set("cover.garage_door", "closed") calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) @@ -29,8 +31,7 @@ async def test_open_cover_intent(hass): async def test_close_cover_intent(hass): """Test HassCloseCover intent.""" - result = await comps.cover.async_setup(hass, {}) - assert result + await cover_intent.async_setup_intents(hass) hass.states.async_set("cover.garage_door", "open") calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER) diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index e0e5f44873d665..76a0399c68876a 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -1,6 +1,11 @@ """Tests for Intent component.""" +import pytest + from homeassistant.setup import async_setup_component from homeassistant.helpers import intent +from homeassistant.components.cover import SERVICE_OPEN_COVER + +from tests.common import async_mock_service async def test_http_handle_intent(hass, hass_client, hass_admin_user): @@ -42,3 +47,30 @@ async def async_handle(self, intent): }, "speech": {"plain": {"extra_data": None, "speech": "I've ordered a Belgian!"}}, } + + +async def test_cover_intents_loading(hass): + """Test Cover Intents Loading.""" + assert await async_setup_component(hass, "intent", {}) + + with pytest.raises(intent.UnknownIntent): + await intent.async_handle( + hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} + ) + + assert await async_setup_component(hass, "cover", {}) + + hass.states.async_set("cover.garage_door", "closed") + calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + + response = await intent.async_handle( + hass, "test", "HassOpenCover", {"name": {"value": "garage door"}} + ) + await hass.async_block_till_done() + + assert response.speech["plain"]["speech"] == "Opened garage door" + assert len(calls) == 1 + call = calls[0] + assert call.domain == "cover" + assert call.service == "open_cover" + assert call.data == {"entity_id": "cover.garage_door"} diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 8ceda6cbd3efa7..3539e109a66770 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -18,13 +18,10 @@ SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, - ATTR_SUPPORTED_FEATURES, ) from homeassistant.components import light -from homeassistant.helpers.intent import IntentHandleError from tests.common import ( - async_mock_service, mock_service, get_test_home_assistant, mock_storage, @@ -433,87 +430,6 @@ def _mock_open(path, *args, **kwargs): assert {light.ATTR_HS_COLOR: (50.353, 100), light.ATTR_BRIGHTNESS: 100} == data -async def test_intent_set_color(hass): - """Test the set color intent.""" - hass.states.async_set( - "light.hello_2", "off", {ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR} - ) - hass.states.async_set("switch.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - result = await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, - ) - await hass.async_block_till_done() - - assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue" - - assert len(calls) == 1 - call = calls[0] - assert call.domain == light.DOMAIN - assert call.service == SERVICE_TURN_ON - assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" - assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) - - -async def test_intent_set_color_tests_feature(hass): - """Test the set color intent.""" - hass.states.async_set("light.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - try: - await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, - ) - assert False, "handling intent should have raised" - except IntentHandleError as err: - assert str(err) == "Entity hello does not support changing colors" - - assert len(calls) == 0 - - -async def test_intent_set_color_and_brightness(hass): - """Test the set color intent.""" - hass.states.async_set( - "light.hello_2", - "off", - {ATTR_SUPPORTED_FEATURES: (light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS)}, - ) - hass.states.async_set("switch.hello", "off") - calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) - hass.helpers.intent.async_register(light.SetIntentHandler()) - - result = await hass.helpers.intent.async_handle( - "test", - light.INTENT_SET, - { - "name": {"value": "Hello"}, - "color": {"value": "blue"}, - "brightness": {"value": "20"}, - }, - ) - await hass.async_block_till_done() - - assert ( - result.speech["plain"]["speech"] - == "Changed hello 2 to the color blue and 20% brightness" - ) - - assert len(calls) == 1 - call = calls[0] - assert call.domain == light.DOMAIN - assert call.service == SERVICE_TURN_ON - assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" - assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) - assert call.data.get(light.ATTR_BRIGHTNESS_PCT) == 20 - - async def test_light_context(hass, hass_admin_user): """Test that light context works.""" assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py new file mode 100644 index 00000000000000..594c9a5d1fc77e --- /dev/null +++ b/tests/components/light/test_intent.py @@ -0,0 +1,88 @@ +"""Tests for the light intents.""" +from homeassistant.helpers.intent import IntentHandleError + +from homeassistant.const import ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON, ATTR_ENTITY_ID +from homeassistant.components import light +from homeassistant.components.light import intent +from tests.common import async_mock_service + + +async def test_intent_set_color(hass): + """Test the set color intent.""" + hass.states.async_set( + "light.hello_2", "off", {ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR} + ) + hass.states.async_set("switch.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + ) + await hass.async_block_till_done() + + assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue" + + assert len(calls) == 1 + call = calls[0] + assert call.domain == light.DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" + assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) + + +async def test_intent_set_color_tests_feature(hass): + """Test the set color intent.""" + hass.states.async_set("light.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + try: + await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + ) + assert False, "handling intent should have raised" + except IntentHandleError as err: + assert str(err) == "Entity hello does not support changing colors" + + assert len(calls) == 0 + + +async def test_intent_set_color_and_brightness(hass): + """Test the set color intent.""" + hass.states.async_set( + "light.hello_2", + "off", + {ATTR_SUPPORTED_FEATURES: (light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS)}, + ) + hass.states.async_set("switch.hello", "off") + calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + await intent.async_setup_intents(hass) + + result = await hass.helpers.intent.async_handle( + "test", + intent.INTENT_SET, + { + "name": {"value": "Hello"}, + "color": {"value": "blue"}, + "brightness": {"value": "20"}, + }, + ) + await hass.async_block_till_done() + + assert ( + result.speech["plain"]["speech"] + == "Changed hello 2 to the color blue and 20% brightness" + ) + + assert len(calls) == 1 + call = calls[0] + assert call.domain == light.DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data.get(ATTR_ENTITY_ID) == "light.hello_2" + assert call.data.get(light.ATTR_RGB_COLOR) == (0, 0, 255) + assert call.data.get(light.ATTR_BRIGHTNESS_PCT) == 20 diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py new file mode 100644 index 00000000000000..646b3bee4c01c9 --- /dev/null +++ b/tests/components/shopping_list/conftest.py @@ -0,0 +1,23 @@ +"""Shopping list test helpers.""" +from unittest.mock import patch + +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components.shopping_list import intent as sl_intent + + +@pytest.fixture(autouse=True) +def mock_shopping_list_io(): + """Stub out the persistence.""" + with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( + "homeassistant.components.shopping_list." "ShoppingData.async_load" + ): + yield + + +@pytest.fixture +async def sl_setup(hass): + """Set up the shopping list.""" + assert await async_setup_component(hass, "shopping_list", {}) + await sl_intent.async_setup_intents(hass) diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index 1d42fa60d9c26a..4394a835f494c3 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,27 +1,13 @@ """Test shopping list component.""" import asyncio -from unittest.mock import patch -import pytest - -from homeassistant.bootstrap import async_setup_component from homeassistant.helpers import intent from homeassistant.components.websocket_api.const import TYPE_RESULT -@pytest.fixture(autouse=True) -def mock_shopping_list_io(): - """Stub out the persistence.""" - with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( - "homeassistant.components.shopping_list." "ShoppingData.async_load" - ): - yield - - @asyncio.coroutine -def test_add_item(hass): +def test_add_item(hass, sl_setup): """Test adding an item intent.""" - yield from async_setup_component(hass, "shopping_list", {}) response = yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -31,9 +17,8 @@ def test_add_item(hass): @asyncio.coroutine -def test_recent_items_intent(hass): +def test_recent_items_intent(hass, sl_setup): """Test recent items.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -54,9 +39,8 @@ def test_recent_items_intent(hass): @asyncio.coroutine -def test_deprecated_api_get_all(hass, hass_client): +def test_deprecated_api_get_all(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -77,9 +61,8 @@ def test_deprecated_api_get_all(hass, hass_client): assert not data[1]["complete"] -async def test_ws_get_items(hass, hass_ws_client): +async def test_ws_get_items(hass, hass_ws_client, sl_setup): """Test get shopping_list items websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -106,9 +89,8 @@ async def test_ws_get_items(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_update(hass, hass_client): +def test_deprecated_api_update(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -142,9 +124,8 @@ def test_deprecated_api_update(hass, hass_client): assert wine == {"id": wine_id, "name": "wine", "complete": True} -async def test_ws_update_item(hass, hass_ws_client): +async def test_ws_update_item(hass, hass_ws_client, sl_setup): """Test update shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -186,9 +167,8 @@ async def test_ws_update_item(hass, hass_ws_client): @asyncio.coroutine -def test_api_update_fails(hass, hass_client): +def test_api_update_fails(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -209,9 +189,8 @@ def test_api_update_fails(hass, hass_client): assert resp.status == 400 -async def test_ws_update_item_fail(hass, hass_ws_client): +async def test_ws_update_item_fail(hass, hass_ws_client, sl_setup): """Test failure of update shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -234,9 +213,8 @@ async def test_ws_update_item_fail(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_clear_completed(hass, hass_client): +def test_deprecated_api_clear_completed(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) yield from intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} @@ -265,9 +243,8 @@ def test_deprecated_api_clear_completed(hass, hass_client): assert items[0] == {"id": wine_id, "name": "wine", "complete": False} -async def test_ws_clear_items(hass, hass_ws_client): +async def test_ws_clear_items(hass, hass_ws_client, sl_setup): """Test clearing shopping_list items websocket command.""" - await async_setup_component(hass, "shopping_list", {}) await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) @@ -296,9 +273,8 @@ async def test_ws_clear_items(hass, hass_ws_client): @asyncio.coroutine -def test_deprecated_api_create(hass, hass_client): +def test_deprecated_api_create(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) client = yield from hass_client() resp = yield from client.post("/api/shopping_list/item", json={"name": "soda"}) @@ -315,9 +291,8 @@ def test_deprecated_api_create(hass, hass_client): @asyncio.coroutine -def test_deprecated_api_create_fail(hass, hass_client): +def test_deprecated_api_create_fail(hass, hass_client, sl_setup): """Test the API.""" - yield from async_setup_component(hass, "shopping_list", {}) client = yield from hass_client() resp = yield from client.post("/api/shopping_list/item", json={"name": 1234}) @@ -326,9 +301,8 @@ def test_deprecated_api_create_fail(hass, hass_client): assert len(hass.data["shopping_list"].items) == 0 -async def test_ws_add_item(hass, hass_ws_client): +async def test_ws_add_item(hass, hass_ws_client, sl_setup): """Test adding shopping_list item websocket command.""" - await async_setup_component(hass, "shopping_list", {}) client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": "soda"}) msg = await client.receive_json() @@ -342,9 +316,8 @@ async def test_ws_add_item(hass, hass_ws_client): assert items[0]["complete"] is False -async def test_ws_add_item_fail(hass, hass_ws_client): +async def test_ws_add_item_fail(hass, hass_ws_client, sl_setup): """Test adding shopping_list item failure websocket command.""" - await async_setup_component(hass, "shopping_list", {}) client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": 123}) msg = await client.receive_json() diff --git a/tests/components/shopping_list/test_intent.py b/tests/components/shopping_list/test_intent.py new file mode 100644 index 00000000000000..d0bcb1d837c2aa --- /dev/null +++ b/tests/components/shopping_list/test_intent.py @@ -0,0 +1,22 @@ +"""Test Shopping List intents.""" +from homeassistant.helpers import intent + + +async def test_recent_items_intent(hass, sl_setup): + """Test recent items.""" + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "soda"}} + ) + + response = await intent.async_handle(hass, "test", "HassShoppingListLastItems") + + assert ( + response.speech["plain"]["speech"] + == "These are the top 3 items on your shopping list: soda, wine, beer" + ) From 19241f421bb56d321456e66472b9e8668ed613f1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 2 Dec 2019 00:32:37 +0000 Subject: [PATCH 1953/3953] [ci skip] Translation update --- .../components/alarm_control_panel/.translations/ca.json | 7 +++++++ .../components/alarm_control_panel/.translations/en.json | 7 +++++++ .../components/alarm_control_panel/.translations/ru.json | 3 +++ .../alarm_control_panel/.translations/zh-Hant.json | 7 +++++++ homeassistant/components/plex/.translations/ca.json | 1 + homeassistant/components/plex/.translations/en.json | 1 + homeassistant/components/plex/.translations/ru.json | 1 + homeassistant/components/plex/.translations/zh-Hant.json | 1 + 8 files changed, 28 insertions(+) diff --git a/homeassistant/components/alarm_control_panel/.translations/ca.json b/homeassistant/components/alarm_control_panel/.translations/ca.json index 8d95d5f648593e..d60cf3173c7f95 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ca.json +++ b/homeassistant/components/alarm_control_panel/.translations/ca.json @@ -6,6 +6,13 @@ "arm_night": "Activa {entity_name} nocturn", "disarm": "Desactiva {entity_name}", "trigger": "Dispara {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} activada en mode a fora", + "armed_home": "{entity_name} activada en mode a casa", + "armed_night": "{entity_name} activada en mode nocturn", + "disarmed": "{entity_name} desactivada", + "triggered": "{entity_name} disparat/ada" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/en.json b/homeassistant/components/alarm_control_panel/.translations/en.json index b8eeb1d2e8c5b1..a00e81feb921f8 100644 --- a/homeassistant/components/alarm_control_panel/.translations/en.json +++ b/homeassistant/components/alarm_control_panel/.translations/en.json @@ -6,6 +6,13 @@ "arm_night": "Arm {entity_name} night", "disarm": "Disarm {entity_name}", "trigger": "Trigger {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} armed away", + "armed_home": "{entity_name} armed home", + "armed_night": "{entity_name} armed night", + "disarmed": "{entity_name} disarmed", + "triggered": "{entity_name} triggered" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/ru.json b/homeassistant/components/alarm_control_panel/.translations/ru.json index e573ce709183d7..79d04c8855f736 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ru.json +++ b/homeassistant/components/alarm_control_panel/.translations/ru.json @@ -6,6 +6,9 @@ "arm_night": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "disarm": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0445\u0440\u0430\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" + }, + "trigger_type": { + "triggered": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json index c52288802d1175..72c0b65436dd8f 100644 --- a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json +++ b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json @@ -6,6 +6,13 @@ "arm_night": "\u8a2d\u5b9a {entity_name} \u591c\u9593\u6a21\u5f0f", "disarm": "\u89e3\u9664 {entity_name}", "trigger": "\u89f8\u767c {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} \u8a2d\u5b9a\u5916\u51fa", + "armed_home": "{entity_name} \u8a2d\u5b9a\u5728\u5bb6", + "armed_night": "{entity_name} \u8a2d\u5b9a\u591c\u9593", + "disarmed": "{entity_name} \u5df2\u89e3\u9664", + "triggered": "{entity_name} \u5df2\u89f8\u767c" } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 9eb6d16639f4e1..63cf65b8d6c1cf 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -6,6 +6,7 @@ "already_in_progress": "S\u2019est\u00e0 configurant Plex", "discovery_no_file": "No s'ha trobat cap fitxer de configuraci\u00f3 heretat", "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", + "non-interactive": "Importaci\u00f3 no interactiva", "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del testimoni.", "unknown": "Ha fallat per motiu desconegut" }, diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index bf927b7f1be07b..31211182f4717e 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex is being configured", "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", + "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" }, diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index cb4c8247389613..334a4e353d4c6a 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -6,6 +6,7 @@ "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", "discovery_no_file": "\u0421\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430.", + "non-interactive": "\u041d\u0435\u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439 \u0438\u043c\u043f\u043e\u0440\u0442.", "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430.", "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435." }, diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index 2d4ce1ea6aa2c5..5c05d2104f9b0a 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", "discovery_no_file": "\u627e\u4e0d\u5230\u820a\u7248\u8a2d\u5b9a\u6a94\u6848", "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", + "non-interactive": "\u7121\u4e92\u52d5\u532f\u5165", "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" }, From 3f2b6bfaa4acc333a5a3caaea3955ab564f49ab2 Mon Sep 17 00:00:00 2001 From: NobleKangaroo <34781835+NobleKangaroo@users.noreply.github.com> Date: Mon, 2 Dec 2019 00:00:22 -0500 Subject: [PATCH 1954/3953] Overhaul Emulated Hue (#28317) * Emulated Hue Overhaul * Fix erroneous merge * Remove unused code * Modernize string format --- .../components/emulated_hue/hue_api.py | 397 ++++++++++-------- tests/components/emulated_hue/test_hue_api.py | 42 +- 2 files changed, 254 insertions(+), 185 deletions(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 51ce3e2e42aab4..e7f15e7fc53524 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,7 +1,6 @@ """Support for a Hue API to control Home Assistant.""" import logging - -from aiohttp import web +import hashlib from homeassistant import core from homeassistant.components import ( @@ -36,8 +35,10 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, + ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, ) from homeassistant.components.media_player.const import ( ATTR_MEDIA_VOLUME_LEVEL, @@ -48,6 +49,7 @@ ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, HTTP_BAD_REQUEST, + HTTP_UNAUTHORIZED, HTTP_NOT_FOUND, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, @@ -62,18 +64,30 @@ _LOGGER = logging.getLogger(__name__) +STATE_BRIGHTNESS = "bri" +STATE_COLORMODE = "colormode" +STATE_HUE = "hue" +STATE_SATURATION = "sat" +STATE_COLOR_TEMP = "ct" + +# Hue API states, defined separately in case they change HUE_API_STATE_ON = "on" HUE_API_STATE_BRI = "bri" +HUE_API_STATE_COLORMODE = "colormode" HUE_API_STATE_HUE = "hue" HUE_API_STATE_SAT = "sat" +HUE_API_STATE_CT = "ct" +HUE_API_STATE_EFFECT = "effect" -HUE_API_STATE_HUE_MAX = 65535.0 -HUE_API_STATE_SAT_MAX = 254.0 -HUE_API_STATE_BRI_MAX = 255.0 - -STATE_BRIGHTNESS = HUE_API_STATE_BRI -STATE_HUE = HUE_API_STATE_HUE -STATE_SATURATION = HUE_API_STATE_SAT +# Hue API min/max values - https://developers.meethue.com/develop/hue-api/lights-api/ +HUE_API_STATE_BRI_MIN = 1 # Brightness +HUE_API_STATE_BRI_MAX = 254 +HUE_API_STATE_HUE_MIN = 0 # Hue +HUE_API_STATE_HUE_MAX = 65535 +HUE_API_STATE_SAT_MIN = 0 # Saturation +HUE_API_STATE_SAT_MAX = 254 +HUE_API_STATE_CT_MIN = 153 # Color temp +HUE_API_STATE_CT_MAX = 500 class HueUsernameView(HomeAssistantView): @@ -86,6 +100,9 @@ class HueUsernameView(HomeAssistantView): async def post(self, request): """Handle a POST request.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) + try: data = await request.json() except ValueError: @@ -94,14 +111,11 @@ async def post(self, request): if "devicetype" not in data: return self.json_message("devicetype not specified", HTTP_BAD_REQUEST) - if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) - return self.json([{"success": {"username": "12345678901234567890"}}]) class HueAllGroupsStateView(HomeAssistantView): - """Group handler.""" + """Handle requests for getting info about entity groups.""" url = "/api/{username}/groups" name = "emulated_hue:all_groups:state" @@ -115,7 +129,7 @@ def __init__(self, config): def get(self, request, username): """Process a request to make the Brilliant Lightpad work.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json({}) @@ -135,7 +149,7 @@ def __init__(self, config): def put(self, request, username): """Process a request to make the Logitech Pop working.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) return self.json( [ @@ -151,7 +165,7 @@ def put(self, request, username): class HueAllLightsStateView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for getting info about all entities.""" url = "/api/{username}/lights" name = "emulated_hue:lights:state" @@ -165,23 +179,21 @@ def __init__(self, config): def get(self, request, username): """Process a request to get the list of available lights.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) hass = request.app["hass"] json_response = {} for entity in hass.states.async_all(): if self.config.is_entity_exposed(entity): - state = get_entity_state(self.config, entity) - number = self.config.entity_id_to_number(entity.entity_id) - json_response[number] = entity_to_json(self.config, entity, state) + json_response[number] = entity_to_json(self.config, entity) return self.json(json_response) class HueOneLightStateView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for getting info about a single entity.""" url = "/api/{username}/lights/{entity_id}" name = "emulated_hue:light:state" @@ -195,7 +207,7 @@ def __init__(self, config): def get(self, request, username, entity_id): """Process a request to get the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) hass = request.app["hass"] hass_entity_id = self.config.number_to_entity_id(entity_id) @@ -205,27 +217,25 @@ def get(self, request, username, entity_id): "Unknown entity number: %s not found in emulated_hue_ids.json", entity_id, ) - return web.Response(text="Entity not found", status=404) + return self.json_message("Entity not found", HTTP_NOT_FOUND) entity = hass.states.get(hass_entity_id) if entity is None: _LOGGER.error("Entity not found: %s", hass_entity_id) - return web.Response(text="Entity not found", status=404) + return self.json_message("Entity not found", HTTP_NOT_FOUND) if not self.config.is_entity_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) - return web.Response(text="Entity not exposed", status=404) - - state = get_entity_state(self.config, entity) + return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED) - json_response = entity_to_json(self.config, entity, state) + json_response = entity_to_json(self.config, entity) return self.json(json_response) class HueOneLightChangeView(HomeAssistantView): - """Handle requests for getting and setting info about entities.""" + """Handle requests for setting info about entities.""" url = "/api/{username}/lights/{entity_number}/state" name = "emulated_hue:light:state" @@ -238,7 +248,7 @@ def __init__(self, config): async def put(self, request, username, entity_number): """Process a request to set the state of an individual light.""" if not is_local(request[KEY_REAL_IP]): - return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) + return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) config = self.config hass = request.app["hass"] @@ -256,7 +266,7 @@ async def put(self, request, username, entity_number): if not config.is_entity_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) - return web.Response(text="Entity not exposed", status=404) + return self.json_message("Entity not exposed", HTTP_UNAUTHORIZED) try: request_json = await request.json() @@ -264,12 +274,60 @@ async def put(self, request, username, entity_number): _LOGGER.error("Received invalid json") return self.json_message("Invalid JSON", HTTP_BAD_REQUEST) - # Parse the request into requested "on" status and brightness - parsed = parse_hue_api_put_light_body(request_json, entity) + # Get the entity's supported features + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + # Parse the request + parsed = { + STATE_ON: False, + STATE_BRIGHTNESS: None, + STATE_HUE: None, + STATE_SATURATION: None, + STATE_COLOR_TEMP: None, + } - if parsed is None: - _LOGGER.error("Unable to parse data: %s", request_json) - return web.Response(text="Bad request", status=400) + if HUE_API_STATE_ON in request_json: + if not isinstance(request_json[HUE_API_STATE_ON], bool): + _LOGGER.error("Unable to parse data: %s", request_json) + return self.json_message("Bad request", HTTP_BAD_REQUEST) + parsed[STATE_ON] = request_json[HUE_API_STATE_ON] + else: + parsed[STATE_ON] = entity.state != STATE_OFF + + for (key, attr) in ( + (HUE_API_STATE_BRI, STATE_BRIGHTNESS), + (HUE_API_STATE_HUE, STATE_HUE), + (HUE_API_STATE_SAT, STATE_SATURATION), + (HUE_API_STATE_CT, STATE_COLOR_TEMP), + ): + if key in request_json: + try: + parsed[attr] = int(request_json[key]) + except ValueError: + _LOGGER.error("Unable to parse data (2): %s", request_json) + return self.json_message("Bad request", HTTP_BAD_REQUEST) + + if HUE_API_STATE_BRI in request_json: + if entity.domain == light.DOMAIN: + parsed[STATE_ON] = parsed[STATE_BRIGHTNESS] > 0 + if not entity_features & SUPPORT_BRIGHTNESS: + parsed[STATE_BRIGHTNESS] = None + + elif entity.domain == scene.DOMAIN: + parsed[STATE_BRIGHTNESS] = None + parsed[STATE_ON] = True + + elif entity.domain in [ + script.DOMAIN, + media_player.DOMAIN, + fan.DOMAIN, + cover.DOMAIN, + climate.DOMAIN, + ]: + # Convert 0-255 to 0-100 + level = (parsed[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 + parsed[STATE_BRIGHTNESS] = round(level) + parsed[STATE_ON] = True # Choose general HA domain domain = core.DOMAIN @@ -283,29 +341,37 @@ async def put(self, request, username, entity_number): # Construct what we need to send to the service data = {ATTR_ENTITY_ID: entity_id} - # Make sure the entity actually supports brightness - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - + # If the requested entity is a light, set the brightness, hue, + # saturation and color temp if entity.domain == light.DOMAIN: if parsed[STATE_ON]: if entity_features & SUPPORT_BRIGHTNESS: if parsed[STATE_BRIGHTNESS] is not None: data[ATTR_BRIGHTNESS] = parsed[STATE_BRIGHTNESS] + if entity_features & SUPPORT_COLOR: - if parsed[STATE_HUE] is not None: - if parsed[STATE_SATURATION]: + if any((parsed[STATE_HUE], parsed[STATE_SATURATION])): + if parsed[STATE_HUE] is not None: + hue = parsed[STATE_HUE] + else: + hue = 0 + + if parsed[STATE_SATURATION] is not None: sat = parsed[STATE_SATURATION] else: sat = 0 - hue = parsed[STATE_HUE] # Convert hs values to hass hs values - sat = int((sat / HUE_API_STATE_SAT_MAX) * 100) hue = int((hue / HUE_API_STATE_HUE_MAX) * 360) + sat = int((sat / HUE_API_STATE_SAT_MAX) * 100) data[ATTR_HS_COLOR] = (hue, sat) - # If the requested entity is a script add some variables + if entity_features & SUPPORT_COLOR_TEMP: + if parsed[STATE_COLOR_TEMP] is not None: + data[ATTR_COLOR_TEMP] = parsed[STATE_COLOR_TEMP] + + # If the requested entity is a script, add some variables elif entity.domain == script.DOMAIN: data["variables"] = { "requested_state": STATE_ON if parsed[STATE_ON] else STATE_OFF @@ -366,8 +432,8 @@ async def put(self, request, username, entity_number): elif 66.6 < brightness <= 100: data[ATTR_SPEED] = SPEED_HIGH + # Map the off command to on if entity.domain in config.off_maps_to_on_domains: - # Map the off command to on service = SERVICE_TURN_ON # Caching is required because things like scripts and scenes won't @@ -393,141 +459,65 @@ async def put(self, request, username, entity_number): hass.services.async_call(domain, service, data, blocking=True) ) + # Create success responses for all received keys json_response = [ create_hue_success_response(entity_id, HUE_API_STATE_ON, parsed[STATE_ON]) ] - if parsed[STATE_BRIGHTNESS] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_BRI, parsed[STATE_BRIGHTNESS] - ) - ) - if parsed[STATE_HUE] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_HUE, parsed[STATE_HUE] + for (key, val) in ( + (STATE_BRIGHTNESS, HUE_API_STATE_BRI), + (STATE_HUE, HUE_API_STATE_HUE), + (STATE_SATURATION, HUE_API_STATE_SAT), + (STATE_COLOR_TEMP, HUE_API_STATE_CT), + ): + if parsed[key] is not None: + json_response.append( + create_hue_success_response(entity_id, val, parsed[key]) ) - ) - if parsed[STATE_SATURATION] is not None: - json_response.append( - create_hue_success_response( - entity_id, HUE_API_STATE_SAT, parsed[STATE_SATURATION] - ) - ) return self.json(json_response) -def parse_hue_api_put_light_body(request_json, entity): - """Parse the body of a request to change the state of a light.""" - data = { - STATE_BRIGHTNESS: None, - STATE_HUE: None, - STATE_ON: False, - STATE_SATURATION: None, - } - - # Make sure the entity actually supports brightness - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - - if HUE_API_STATE_ON in request_json: - if not isinstance(request_json[HUE_API_STATE_ON], bool): - return None - - if request_json[HUE_API_STATE_ON]: - # Echo requested device be turned on - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = True - else: - # Echo requested device be turned off - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = False - - if HUE_API_STATE_HUE in request_json: - try: - # Clamp brightness from 0 to 65535 - data[STATE_HUE] = max( - 0, min(int(request_json[HUE_API_STATE_HUE]), HUE_API_STATE_HUE_MAX) - ) - except ValueError: - return None - - if HUE_API_STATE_SAT in request_json: - try: - # Clamp saturation from 0 to 254 - data[STATE_SATURATION] = max( - 0, min(int(request_json[HUE_API_STATE_SAT]), HUE_API_STATE_SAT_MAX) - ) - except ValueError: - return None - - if HUE_API_STATE_BRI in request_json: - try: - # Clamp brightness from 0 to 255 - data[STATE_BRIGHTNESS] = max( - 0, min(int(request_json[HUE_API_STATE_BRI]), HUE_API_STATE_BRI_MAX) - ) - except ValueError: - return None - - if entity.domain == light.DOMAIN: - data[STATE_ON] = data[STATE_BRIGHTNESS] > 0 - if not entity_features & SUPPORT_BRIGHTNESS: - data[STATE_BRIGHTNESS] = None - - elif entity.domain == scene.DOMAIN: - data[STATE_BRIGHTNESS] = None - data[STATE_ON] = True - - elif entity.domain in [ - script.DOMAIN, - media_player.DOMAIN, - fan.DOMAIN, - cover.DOMAIN, - climate.DOMAIN, - ]: - # Convert 0-255 to 0-100 - level = (data[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 - data[STATE_BRIGHTNESS] = round(level) - data[STATE_ON] = True - - return data - - def get_entity_state(config, entity): """Retrieve and convert state and brightness values for an entity.""" cached_state = config.cached_states.get(entity.entity_id, None) data = { + STATE_ON: False, STATE_BRIGHTNESS: None, STATE_HUE: None, - STATE_ON: False, STATE_SATURATION: None, + STATE_COLOR_TEMP: None, } if cached_state is None: data[STATE_ON] = entity.state != STATE_OFF + if data[STATE_ON]: data[STATE_BRIGHTNESS] = entity.attributes.get(ATTR_BRIGHTNESS, 0) hue_sat = entity.attributes.get(ATTR_HS_COLOR, None) if hue_sat is not None: hue = hue_sat[0] sat = hue_sat[1] - # convert hass hs values back to hue hs values + # Convert hass hs values back to hue hs values data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX) data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX) + else: + data[STATE_HUE] = HUE_API_STATE_HUE_MIN + data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN + data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0) + else: data[STATE_BRIGHTNESS] = 0 data[STATE_HUE] = 0 data[STATE_SATURATION] = 0 + data[STATE_COLOR_TEMP] = 0 - # Make sure the entity actually supports brightness + # Get the entity's supported features entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if entity.domain == light.DOMAIN: if entity_features & SUPPORT_BRIGHTNESS: pass - elif entity.domain == climate.DOMAIN: temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) # Convert 0-100 to 0-255 @@ -537,7 +527,7 @@ def get_entity_state(config, entity): ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0 ) # Convert 0.0-1.0 to 0-255 - data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX) + data[STATE_BRIGHTNESS] = round(min(1.0, level) * 255) elif entity.domain == fan.DOMAIN: speed = entity.attributes.get(ATTR_SPEED, 0) # Convert 0.0-1.0 to 0-255 @@ -550,12 +540,13 @@ def get_entity_state(config, entity): data[STATE_BRIGHTNESS] = 255 elif entity.domain == cover.DOMAIN: level = entity.attributes.get(ATTR_CURRENT_POSITION, 0) - data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX) + data[STATE_BRIGHTNESS] = round(level / 100 * 255) else: data = cached_state # Make sure brightness is valid if data[STATE_BRIGHTNESS] is None: data[STATE_BRIGHTNESS] = 255 if data[STATE_ON] else 0 + # Make sure hue/saturation are valid if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None): data[STATE_HUE] = 0 @@ -566,39 +557,117 @@ def get_entity_state(config, entity): data[STATE_HUE] = 0 data[STATE_SATURATION] = 0 + # Clamp brightness, hue, saturation, and color temp to valid values + for (key, v_min, v_max) in ( + (STATE_BRIGHTNESS, HUE_API_STATE_BRI_MIN, HUE_API_STATE_BRI_MAX), + (STATE_HUE, HUE_API_STATE_HUE_MIN, HUE_API_STATE_HUE_MAX), + (STATE_SATURATION, HUE_API_STATE_SAT_MIN, HUE_API_STATE_SAT_MAX), + (STATE_COLOR_TEMP, HUE_API_STATE_CT_MIN, HUE_API_STATE_CT_MAX), + ): + if data[key] is not None: + data[key] = max(v_min, min(data[key], v_max)) + return data -def entity_to_json(config, entity, state): +def entity_to_json(config, entity): """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": entity.state != STATE_UNAVAILABLE, - }, - "type": "Dimmable light", - "name": config.get_entity_name(entity), - "modelid": "HASS123", - "uniqueid": entity.entity_id, - "swversion": "123", - } - return { + unique_id = hashlib.md5(entity.entity_id.encode()).hexdigest() + unique_id = "00:{}:{}:{}:{}:{}:{}:{}-{}".format( + unique_id[0:2], + unique_id[2:4], + unique_id[4:6], + unique_id[6:8], + unique_id[8:10], + unique_id[10:12], + unique_id[12:14], + unique_id[14:16], + ) + + state = get_entity_state(config, entity) + + retval = { "state": { HUE_API_STATE_ON: state[STATE_ON], "reachable": entity.state != STATE_UNAVAILABLE, + "mode": "homeautomation", }, - "type": "On/off light", "name": config.get_entity_name(entity), - "modelid": "HASS321", - "uniqueid": entity.entity_id, + "uniqueid": unique_id, + "manufacturername": "Home Assistant", "swversion": "123", } + if ( + (entity_features & SUPPORT_BRIGHTNESS) + and (entity_features & SUPPORT_COLOR) + and (entity_features & SUPPORT_COLOR_TEMP) + ): + # Extended Color light (ZigBee Device ID: 0x0210) + # Same as Color light, but which supports additional setting of color temperature + retval["type"] = "Extended color light" + retval["modelid"] = "HASS231" + retval["state"].update( + { + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + HUE_API_STATE_CT: state[STATE_COLOR_TEMP], + HUE_API_STATE_EFFECT: "none", + } + ) + if state[STATE_HUE] > 0 or state[STATE_SATURATION] > 0: + retval["state"][HUE_API_STATE_COLORMODE] = "hs" + else: + retval["state"][HUE_API_STATE_COLORMODE] = "ct" + elif (entity_features & SUPPORT_BRIGHTNESS) and (entity_features & SUPPORT_COLOR): + # Color light (ZigBee Device ID: 0x0200) + # Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY) + retval["type"] = "Color light" + retval["modelid"] = "HASS213" + retval["state"].update( + { + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_COLORMODE: "hs", + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + HUE_API_STATE_EFFECT: "none", + } + ) + elif (entity_features & SUPPORT_BRIGHTNESS) and ( + entity_features & SUPPORT_COLOR_TEMP + ): + # Color temperature light (ZigBee Device ID: 0x0220) + # Supports groups, scenes, on/off, dimming, and setting of a color temperature + retval["type"] = "Color temperature light" + retval["modelid"] = "HASS312" + retval["state"].update( + {HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_CT: state[STATE_COLOR_TEMP]} + ) + elif ( + entity_features + & ( + SUPPORT_BRIGHTNESS + | SUPPORT_SET_POSITION + | SUPPORT_SET_SPEED + | SUPPORT_VOLUME_SET + | SUPPORT_TARGET_TEMPERATURE + ) + ) or entity.domain == script.DOMAIN: + # Dimmable light (ZigBee Device ID: 0x0100) + # Supports groups, scenes, on/off and dimming + retval["type"] = "Dimmable light" + retval["modelid"] = "HASS123" + retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) + else: + # On/off light (ZigBee Device ID: 0x0000) + # Supports groups, scenes and on/off control + retval["type"] = "On/off light" + retval["modelid"] = "HASS321" + + return retval + def create_hue_success_response(entity_id, attr, value): """Create a success response for an attribute set on a light.""" diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 9629ae6cf69008..4f0d70d0046957 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -205,20 +205,20 @@ def test_discover_lights(hue_client): devices = set(val["uniqueid"] for val in result_json.values()) # Make sure the lights we added to the config are there - assert "light.ceiling_lights" in devices - assert "light.bed_light" not in devices - assert "script.set_kitchen_light" in devices - assert "light.kitchen_lights" not in devices - assert "media_player.living_room" in devices - assert "media_player.bedroom" in devices - assert "media_player.walkman" in devices - assert "media_player.lounge_room" in devices - assert "fan.living_room_fan" in devices - assert "fan.ceiling_fan" not in devices - assert "cover.living_room_window" in devices - assert "climate.hvac" in devices - assert "climate.heatpump" in devices - assert "climate.ecobee" not in devices + assert "00:2f:d2:31:ce:c5:55:cc-ee" in devices # light.ceiling_lights + assert "00:b6:14:77:34:b7:bb:06-e8" not in devices # light.bed_light + assert "00:95:b7:51:16:58:6c:c0-c5" in devices # script.set_kitchen_light + assert "00:64:7b:e4:96:c3:fe:90-c3" not in devices # light.kitchen_lights + assert "00:7e:8a:42:35:66:db:86-c5" in devices # media_player.living_room + assert "00:05:44:c2:d6:0a:e5:17-b7" in devices # media_player.bedroom + assert "00:f3:5f:fa:31:f3:32:21-a8" in devices # media_player.walkman + assert "00:b4:06:2e:91:95:23:97-fb" in devices # media_player.lounge_room + assert "00:b2:bd:f9:2c:ad:22:ae-58" in devices # fan.living_room_fan + assert "00:77:4c:8a:23:7d:27:4b-7f" not in devices # fan.ceiling_fan + assert "00:02:53:b9:d5:1a:b3:67-b2" in devices # cover.living_room_window + assert "00:42:03:fe:97:58:2d:b1-50" in devices # climate.hvac + assert "00:7b:2a:c7:08:d6:66:bf-80" in devices # climate.heatpump + assert "00:57:77:a1:6a:8e:ef:b3-6c" not in devices # climate.ecobee @asyncio.coroutine @@ -300,15 +300,15 @@ def test_get_light_state(hass_hue, hue_client): ) assert office_json["state"][HUE_API_STATE_ON] is False - assert office_json["state"][HUE_API_STATE_BRI] == 0 + # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 assert office_json["state"][HUE_API_STATE_HUE] == 0 assert office_json["state"][HUE_API_STATE_SAT] == 0 # Make sure bedroom light isn't accessible - yield from perform_get_light_state(hue_client, "light.bed_light", 404) + yield from perform_get_light_state(hue_client, "light.bed_light", 401) # Make sure kitchen light isn't accessible - yield from perform_get_light_state(hue_client, "light.kitchen_lights", 404) + yield from perform_get_light_state(hue_client, "light.kitchen_lights", 401) @asyncio.coroutine @@ -365,7 +365,7 @@ def test_put_light_state(hass_hue, hue_client): ceiling_json = yield from perform_get_light_state( hue_client, "light.ceiling_lights", 200 ) - assert ceiling_json["state"][HUE_API_STATE_BRI] == 0 + # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 assert ceiling_json["state"][HUE_API_STATE_HUE] == 0 assert ceiling_json["state"][HUE_API_STATE_SAT] == 0 @@ -373,7 +373,7 @@ def test_put_light_state(hass_hue, hue_client): bedroom_result = yield from perform_put_light_state( hass_hue, hue_client, "light.bed_light", True ) - assert bedroom_result.status == 404 + assert bedroom_result.status == 401 # Make sure we can't change the kitchen light state kitchen_result = yield from perform_put_light_state( @@ -434,7 +434,7 @@ def test_put_light_state_climate_set_temperature(hass_hue, hue_client): ecobee_result = yield from perform_put_light_state( hass_hue, hue_client, "climate.ecobee", True ) - assert ecobee_result.status == 404 + assert ecobee_result.status == 401 @asyncio.coroutine @@ -769,4 +769,4 @@ async def test_external_ip_blocked(hue_client): ): result = await hue_client.get("/api/username/lights") - assert result.status == 400 + assert result.status == 401 From b28bc1d6fb5a23a7c2cb9bcb0f818e32afe034c7 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 2 Dec 2019 12:00:07 +0100 Subject: [PATCH 1955/3953] Add name option to season sensor (#29302) * Add name option to season sensor * Changed DEFAULT_NAME from season to Season --- homeassistant/components/season/sensor.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index e5f756f0c75176..8c237c1da19f44 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -7,12 +7,15 @@ from homeassistant import util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_TYPE +from homeassistant.const import CONF_NAME, CONF_TYPE +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +DEFAULT_NAME = "Season" + EQUATOR = "equator" NORTHERN = "northern" @@ -44,7 +47,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_TYPE, default=TYPE_ASTRONOMICAL): vol.In(VALID_TYPES)} + { + vol.Optional(CONF_TYPE, default=TYPE_ASTRONOMICAL): vol.In(VALID_TYPES), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } ) @@ -56,6 +62,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): latitude = util.convert(hass.config.latitude, float) _type = config.get(CONF_TYPE) + name = config.get(CONF_NAME) if latitude < 0: hemisphere = SOUTHERN @@ -65,7 +72,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hemisphere = EQUATOR _LOGGER.debug(_type) - add_entities([Season(hass, hemisphere, _type)]) + add_entities([Season(hass, hemisphere, _type, name)]) return True @@ -105,9 +112,10 @@ def get_season(date, hemisphere, season_tracking_type): class Season(Entity): """Representation of the current season.""" - def __init__(self, hass, hemisphere, season_tracking_type): + def __init__(self, hass, hemisphere, season_tracking_type, name): """Initialize the season.""" self.hass = hass + self._name = name self.hemisphere = hemisphere self.datetime = dt_util.utcnow().replace(tzinfo=None) self.type = season_tracking_type @@ -116,7 +124,7 @@ def __init__(self, hass, hemisphere, season_tracking_type): @property def name(self): """Return the name.""" - return "Season" + return self._name @property def state(self): From 9771826ed6d7351f0410703e3edc128229418ec9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Dec 2019 11:15:50 -0800 Subject: [PATCH 1956/3953] Add capability-attributes (#29306) --- homeassistant/components/light/__init__.py | 44 +++++++++++++--------- homeassistant/helpers/entity.py | 13 ++++++- tests/helpers/test_entity.py | 22 ++++++++++- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 0d60f3f6b52337..7bc2306d4180f1 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -398,8 +398,8 @@ def effect(self): return None @property - def state_attributes(self): - """Return optional state attributes.""" + def capability_attributes(self): + """Return capability attributes.""" data = {} supported_features = self.supported_features @@ -410,25 +410,35 @@ def state_attributes(self): if supported_features & SUPPORT_EFFECT: data[ATTR_EFFECT_LIST] = self.effect_list - if self.is_on: - if supported_features & SUPPORT_BRIGHTNESS: - data[ATTR_BRIGHTNESS] = self.brightness + return data + + @property + def state_attributes(self): + """Return state attributes.""" + if not self.is_on: + return None + + data = {} + supported_features = self.supported_features - if supported_features & SUPPORT_COLOR_TEMP: - data[ATTR_COLOR_TEMP] = self.color_temp + if supported_features & SUPPORT_BRIGHTNESS: + data[ATTR_BRIGHTNESS] = self.brightness - if supported_features & SUPPORT_COLOR and self.hs_color: - # pylint: disable=unsubscriptable-object,not-an-iterable - hs_color = self.hs_color - data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) - data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) - data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) + if supported_features & SUPPORT_COLOR_TEMP: + data[ATTR_COLOR_TEMP] = self.color_temp - if supported_features & SUPPORT_WHITE_VALUE: - data[ATTR_WHITE_VALUE] = self.white_value + if supported_features & SUPPORT_COLOR and self.hs_color: + # pylint: disable=unsubscriptable-object,not-an-iterable + hs_color = self.hs_color + data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) + data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) + data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) - if supported_features & SUPPORT_EFFECT: - data[ATTR_EFFECT] = self.effect + if supported_features & SUPPORT_WHITE_VALUE: + data[ATTR_WHITE_VALUE] = self.white_value + + if supported_features & SUPPORT_EFFECT: + data[ATTR_EFFECT] = self.effect return {key: val for key, val in data.items() if val is not None} diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 1805c6bd74b0d8..ed6560614016f8 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -144,6 +144,17 @@ def state(self) -> Union[None, str, int, float]: """Return the state of the entity.""" return STATE_UNKNOWN + @property + def capability_attributes(self) -> Optional[Dict[str, Any]]: + """Return the capability attributes. + + Attributes that explain the capabilities of an entity. + + Implemented by component base class. Convention for attribute names + is lowercase snake_case. + """ + return None + @property def state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes. @@ -302,7 +313,7 @@ def _async_write_ha_state(self): start = timer() - attr = {} + attr = self.capability_attributes or {} if not self.available: state = STATE_UNAVAILABLE else: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 9d05920f78b792..cd852f5bfc0688 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -9,7 +9,7 @@ from homeassistant.helpers import entity, entity_registry from homeassistant.core import Context -from homeassistant.const import ATTR_HIDDEN, ATTR_DEVICE_CLASS +from homeassistant.const import ATTR_HIDDEN, ATTR_DEVICE_CLASS, STATE_UNAVAILABLE from homeassistant.config import DATA_CUSTOMIZE from homeassistant.helpers.entity_values import EntityValues @@ -641,3 +641,23 @@ async def test_disabled_in_entity_registry(hass): assert entry3 != entry2 assert ent.registry_entry == entry3 assert ent.enabled is False + + +async def test_capability_attrs(hass): + """Test we still include capabilities even when unavailable.""" + with patch.object( + entity.Entity, "available", PropertyMock(return_value=False) + ), patch.object( + entity.Entity, + "capability_attributes", + PropertyMock(return_value={"always": "there"}), + ): + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.async_write_ha_state() + + state = hass.states.get("hello.world") + assert state is not None + assert state.state == STATE_UNAVAILABLE + assert state.attributes["always"] == "there" From 1804c6edc5b80ed955a804c13662b6d35754877d Mon Sep 17 00:00:00 2001 From: Christian Ferbar <5595808+ferbar@users.noreply.github.com> Date: Mon, 2 Dec 2019 20:24:16 +0100 Subject: [PATCH 1957/3953] Clear miflora sensor state on exception (#29276) * Clear state on exception Clear state if querying the device fails. The state is then set to unknown, so it can be tracked if a miflora device isn't responding any more. * Add available() Signal valid data via available() --- homeassistant/components/miflora/sensor.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index a08c4ce5eacbe4..815a6e97bb8c0b 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -106,6 +106,7 @@ def __init__(self, poller, parameter, name, unit, icon, force_update, median): self._icon = icon self._name = name self._state = None + self._available = False self.data = [] self._force_update = force_update # Median is used to filter out outliers. median of 3 will filter @@ -132,6 +133,11 @@ def state(self): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return True if entity is available.""" + return self._available + @property def unit_of_measurement(self): """Return the units of measurement.""" @@ -156,15 +162,14 @@ def update(self): try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) - except OSError as ioerr: - _LOGGER.info("Polling error %s", ioerr) - return - except BluetoothBackendException as bterror: - _LOGGER.info("Polling error %s", bterror) + except (OSError, BluetoothBackendException) as err: + _LOGGER.info("Polling error %s: %s", type(err).__name__, err) + self._available = False return if data is not None: _LOGGER.debug("%s = %s", self.name, data) + self._available = True self.data.append(data) else: _LOGGER.info("Did not receive any data from Mi Flora sensor %s", self.name) From ffaa0e572a377e04465ca0e00b2446ceb097a4c8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Dec 2019 11:32:02 -0800 Subject: [PATCH 1958/3953] Inline MQTT paho imports (#29177) * Inline MQTT paho imports * Address comments * Fix patch paths * Move other imports inline * Fix test --- homeassistant/components/mqtt/__init__.py | 19 +++++++++++++++++-- homeassistant/components/mqtt/config_flow.py | 3 ++- homeassistant/components/mqtt/server.py | 8 ++++++-- tests/components/mqtt/test_server.py | 16 ++++------------ 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 498114732df639..ad9166e24106a9 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -16,8 +16,6 @@ import attr import requests.certs import voluptuous as vol -import paho.mqtt.client as mqtt -from paho.mqtt.matcher import MQTTMatcher from homeassistant import config_entries from homeassistant.components import websocket_api @@ -725,6 +723,11 @@ def __init__( tls_version: Optional[int], ) -> None: """Initialize Home Assistant MQTT client.""" + # We don't import them on the top because some integrations + # should be able to optionally rely on MQTT. + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + self.hass = hass self.broker = broker self.port = port @@ -786,6 +789,9 @@ async def async_connect(self) -> str: This method is a coroutine. """ + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + result: int = None try: result = await self.hass.async_add_job( @@ -877,6 +883,9 @@ def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: Resubscribe to all topics we were subscribed to and publish birth message. """ + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + if result_code != mqtt.CONNACK_ACCEPTED: _LOGGER.error( "Unable to connect to the MQTT broker: %s", @@ -968,6 +977,9 @@ def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: def _raise_on_error(result_code: int) -> None: """Raise error if error result.""" + # pylint: disable=import-outside-toplevel + import paho.mqtt.client as mqtt + if result_code != 0: raise HomeAssistantError( "Error talking to MQTT: {}".format(mqtt.error_string(result_code)) @@ -976,6 +988,9 @@ def _raise_on_error(result_code: int) -> None: def _match_topic(subscription: str, topic: str) -> bool: """Test if topic matches subscription.""" + # pylint: disable=import-outside-toplevel + from paho.mqtt.matcher import MQTTMatcher + matcher = MQTTMatcher() matcher[subscription] = True try: diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index a8a378e723c098..d3c6ee819b5f5f 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3,7 +3,6 @@ import queue import voluptuous as vol -import paho.mqtt.client as mqtt from homeassistant import config_entries from homeassistant.const import ( @@ -126,6 +125,8 @@ async def async_step_hassio_confirm(self, user_input=None): def try_connection(broker, port, username, password, protocol="3.1"): """Test if we can connect to an MQTT broker.""" + import paho.mqtt.client as mqtt + if protocol == "3.1": proto = mqtt.MQTTv31 else: diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index f5d369a75c799c..3ed2fb71b14d2b 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -4,8 +4,6 @@ import tempfile import voluptuous as vol -from hbmqtt.broker import Broker, BrokerException -from passlib.apps import custom_app_context from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv @@ -37,6 +35,9 @@ def async_start(hass, password, server_config): This method is a coroutine. """ + # pylint: disable=import-outside-toplevel + from hbmqtt.broker import Broker, BrokerException + passwd = tempfile.NamedTemporaryFile() gen_server_config, client_config = generate_config(hass, passwd, password) @@ -65,6 +66,9 @@ def async_shutdown_mqtt_server(event): def generate_config(hass, passwd, password): """Generate a configuration based on current Home Assistant instance.""" + # pylint: disable=import-outside-toplevel + from passlib.apps import custom_app_context + config = { "listeners": { "default": { diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 71dff7ef3acc87..3627c95040e07c 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -19,13 +19,9 @@ def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() - @patch( - "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") - ) + @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch( - "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock()) - ) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): @@ -45,13 +41,9 @@ def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): assert mock_mqtt.mock_calls[1][2]["username"] == "homeassistant" assert mock_mqtt.mock_calls[1][2]["password"] == password - @patch( - "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") - ) + @patch("passlib.apps.custom_app_context", Mock(return_value="")) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch( - "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock()) - ) + @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): From 040fd9c258b422f7f17c3dc11c3c403f86117f25 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 2 Dec 2019 13:49:39 -0600 Subject: [PATCH 1959/3953] Update service domains for xiaomi_miio from base domains to xiaomi_miio domain (#29134) * move service constants to const.py, move all custom xiaomi_miio services to xiaomi_miio domain * update service names * try to fix black error * try black formatting again * final black formatting attempt * update service names to reflect platform * fix typo --- homeassistant/components/fan/services.yaml | 141 -------- homeassistant/components/light/services.yaml | 20 -- homeassistant/components/remote/services.yaml | 13 - homeassistant/components/switch/services.yaml | 31 -- homeassistant/components/vacuum/services.yaml | 59 ---- homeassistant/components/xiaomi_miio/const.py | 48 +++ homeassistant/components/xiaomi_miio/fan.py | 48 ++- homeassistant/components/xiaomi_miio/light.py | 22 +- .../components/xiaomi_miio/remote.py | 4 +- .../components/xiaomi_miio/services.yaml | 308 ++++++++++++++++++ .../components/xiaomi_miio/switch.py | 15 +- .../components/xiaomi_miio/vacuum.py | 16 +- tests/components/xiaomi_miio/test_vacuum.py | 14 +- 13 files changed, 420 insertions(+), 319 deletions(-) create mode 100644 homeassistant/components/xiaomi_miio/const.py diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 3cb3db6dfe0bff..ee478950095b2d 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -53,144 +53,3 @@ set_direction: direction: description: The direction to rotate. Either 'forward' or 'reverse' example: 'forward' - -xiaomi_miio_set_buzzer_on: - description: Turn the buzzer on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_buzzer_off: - description: Turn the buzzer off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_led_on: - description: Turn the led on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_led_off: - description: Turn the led off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_child_lock_on: - description: Turn the child lock on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_child_lock_off: - description: Turn the child lock off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_favorite_level: - description: Set the favorite level. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - level: - description: Level, between 0 and 16. - example: 1 - -xiaomi_miio_set_led_brightness: - description: Set the led brightness. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - brightness: - description: Brightness (0 = Bright, 1 = Dim, 2 = Off) - example: 1 - -xiaomi_miio_set_auto_detect_on: - description: Turn the auto detect on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_auto_detect_off: - description: Turn the auto detect off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_learn_mode_on: - description: Turn the learn mode on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_learn_mode_off: - description: Turn the learn mode off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_volume: - description: Set the sound volume. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - volume: - description: Volume, between 0 and 100. - example: 50 - -xiaomi_miio_reset_filter: - description: Reset the filter lifetime and usage. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_extra_features: - description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - features: - description: Integer, known values are 0 (default) and 1 (turbo mode). - example: 1 - -xiaomi_miio_set_target_humidity: - description: Set the target humidity. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - humidity: - description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80. - example: 50 - -xiaomi_miio_set_dry_on: - description: Turn the dry mode on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' - -xiaomi_miio_set_dry_off: - description: Turn the dry mode off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 439c45962db263..449e5ea5aaf6d1 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -119,23 +119,3 @@ toggle: values: - colorloop - random - -xiaomi_miio_set_scene: - description: Set a fixed scene. - fields: - entity_id: - description: Name of the light entity. - example: "light.xiaomi_miio" - scene: - description: Number of the fixed scene, between 1 and 4. - example: 1 - -xiaomi_miio_set_delayed_turn_off: - description: Delayed turn off. - fields: - entity_id: - description: Name of the light entity. - example: "light.xiaomi_miio" - time_period: - description: Time period for the delayed turn off. - example: "5, '0:05', {'minutes': 5}" diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml index f728b5033c87b8..1d712a8f2850c9 100644 --- a/homeassistant/components/remote/services.yaml +++ b/homeassistant/components/remote/services.yaml @@ -64,16 +64,3 @@ learn_command: timeout: description: Timeout, in seconds, for the command to be learned. example: '30' - -xiaomi_miio_learn_command: - description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' - fields: - entity_id: - description: 'Name of the entity to learn command from.' - example: 'remote.xiaomi_miio' - slot: - description: 'Define the slot used to save the IR command (Value from 1 to 1000000)' - example: '1' - timeout: - description: 'Define the timeout in seconds, before which the command must be learned.' - example: '30' diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index 3a30a0855f6007..352ffb6feec637 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -20,34 +20,3 @@ toggle: entity_id: description: Name(s) of entities to toggle. example: 'switch.living_room' - -xiaomi_miio_set_wifi_led_on: - description: Turn the wifi led on. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' -xiaomi_miio_set_wifi_led_off: - description: Turn the wifi led off. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' -xiaomi_miio_set_power_price: - description: Set the power price. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' - mode: - description: Power price, between 0 and 999. - example: 31 -xiaomi_miio_set_power_mode: - description: Set the power mode. - fields: - entity_id: - description: Name of the xiaomi miio entity. - example: 'switch.xiaomi_miio_device' - mode: - description: Power mode, valid values are 'normal' and 'green'. - example: 'green' diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index 792658bbdfd074..7db70c5cd5160d 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -85,62 +85,3 @@ set_fan_speed: fan_speed: description: Platform dependent vacuum cleaner fan speed, with speed steps, like 'medium' or by percentage, between 0 and 100. example: 'low' - -xiaomi_remote_control_start: - description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - -xiaomi_remote_control_stop: - description: Stop remote control mode of the vacuum cleaner. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - -xiaomi_remote_control_move: - description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - velocity: - description: Speed, between -0.29 and 0.29. - example: '0.2' - rotation: - description: Rotation, between -179 degrees and 179 degrees. - example: '90' - duration: - description: Duration of the movement. - example: '1500' - -xiaomi_remote_control_move_step: - description: Remote control the vacuum cleaner, only makes one move and then stops. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - velocity: - description: Speed, between -0.29 and 0.29. - example: '0.2' - rotation: - description: Rotation, between -179 degrees and 179 degrees. - example: '90' - duration: - description: Duration of the movement. - example: '1500' - -xiaomi_clean_zone: - description: Start the cleaning operation in the selected areas for the number of repeats indicated. - fields: - entity_id: - description: Name of the vacuum entity. - example: 'vacuum.xiaomi_vacuum_cleaner' - zone: - description: Array of zones. Each zone is an array of 4 integer values. - example: '[[23510,25311,25110,26362]]' - repeats: - description: Number of cleaning repeats for each zone between 1 and 3. - example: '1' diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py new file mode 100644 index 00000000000000..f8be37b313c8bf --- /dev/null +++ b/homeassistant/components/xiaomi_miio/const.py @@ -0,0 +1,48 @@ +"""Constants for the Xiaomi Miio component.""" +DOMAIN = "xiaomi_miio" + +# Fan Services +SERVICE_SET_BUZZER_ON = "fan_set_buzzer_on" +SERVICE_SET_BUZZER_OFF = "fan_set_buzzer_off" +SERVICE_SET_LED_ON = "fan_set_led_on" +SERVICE_SET_LED_OFF = "fan_set_led_off" +SERVICE_SET_CHILD_LOCK_ON = "fan_set_child_lock_on" +SERVICE_SET_CHILD_LOCK_OFF = "fan_set_child_lock_off" +SERVICE_SET_LED_BRIGHTNESS = "fan_set_led_brightness" +SERVICE_SET_FAVORITE_LEVEL = "fan_set_favorite_level" +SERVICE_SET_AUTO_DETECT_ON = "fan_set_auto_detect_on" +SERVICE_SET_AUTO_DETECT_OFF = "fan_set_auto_detect_off" +SERVICE_SET_LEARN_MODE_ON = "fan_set_learn_mode_on" +SERVICE_SET_LEARN_MODE_OFF = "fan_set_learn_mode_off" +SERVICE_SET_VOLUME = "fan_set_volume" +SERVICE_RESET_FILTER = "fan_reset_filter" +SERVICE_SET_EXTRA_FEATURES = "fan_set_extra_features" +SERVICE_SET_TARGET_HUMIDITY = "fan_set_target_humidity" +SERVICE_SET_DRY_ON = "fan_set_dry_on" +SERVICE_SET_DRY_OFF = "fan_set_dry_off" + +# Light Services +SERVICE_SET_SCENE = "light_set_scene" +SERVICE_SET_DELAYED_TURN_OFF = "light_set_delayed_turn_off" +SERVICE_REMINDER_ON = "light_reminder_on" +SERVICE_REMINDER_OFF = "light_reminder_off" +SERVICE_NIGHT_LIGHT_MODE_ON = "light_night_light_mode_on" +SERVICE_NIGHT_LIGHT_MODE_OFF = "light_night_light_mode_off" +SERVICE_EYECARE_MODE_ON = "light_eyecare_mode_on" +SERVICE_EYECARE_MODE_OFF = "light_eyecare_mode_off" + +# Remote Services +SERVICE_LEARN = "remote_learn_command" + +# Switch Services +SERVICE_SET_WIFI_LED_ON = "switch_set_wifi_led_on" +SERVICE_SET_WIFI_LED_OFF = "switch_set_wifi_led_off" +SERVICE_SET_POWER_MODE = "switch_set_power_mode" +SERVICE_SET_POWER_PRICE = "switch_set_power_price" + +# Vacuum Services +SERVICE_MOVE_REMOTE_CONTROL = "vacuum_remote_control_move" +SERVICE_MOVE_REMOTE_CONTROL_STEP = "vacuum_remote_control_move_step" +SERVICE_START_REMOTE_CONTROL = "vacuum_remote_control_start" +SERVICE_STOP_REMOTE_CONTROL = "vacuum_remote_control_stop" +SERVICE_CLEAN_ZONE = "vacuum_clean_zone" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 9e496893d56916..91b18aaf3644f4 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -26,12 +26,7 @@ OperationMode as AirpurifierOperationMode, ) -from homeassistant.components.fan import ( - DOMAIN, - PLATFORM_SCHEMA, - SUPPORT_SET_SPEED, - FanEntity, -) +from homeassistant.components.fan import PLATFORM_SCHEMA, SUPPORT_SET_SPEED, FanEntity from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -42,6 +37,28 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from .const import ( + DOMAIN, + SERVICE_SET_BUZZER_ON, + SERVICE_SET_BUZZER_OFF, + SERVICE_SET_LED_ON, + SERVICE_SET_LED_OFF, + SERVICE_SET_CHILD_LOCK_ON, + SERVICE_SET_CHILD_LOCK_OFF, + SERVICE_SET_LED_BRIGHTNESS, + SERVICE_SET_FAVORITE_LEVEL, + SERVICE_SET_AUTO_DETECT_ON, + SERVICE_SET_AUTO_DETECT_OFF, + SERVICE_SET_LEARN_MODE_ON, + SERVICE_SET_LEARN_MODE_OFF, + SERVICE_SET_VOLUME, + SERVICE_RESET_FILTER, + SERVICE_SET_EXTRA_FEATURES, + SERVICE_SET_TARGET_HUMIDITY, + SERVICE_SET_DRY_ON, + SERVICE_SET_DRY_OFF, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Miio Device" @@ -368,25 +385,6 @@ | FEATURE_SET_EXTRA_FEATURES ) -SERVICE_SET_BUZZER_ON = "xiaomi_miio_set_buzzer_on" -SERVICE_SET_BUZZER_OFF = "xiaomi_miio_set_buzzer_off" -SERVICE_SET_LED_ON = "xiaomi_miio_set_led_on" -SERVICE_SET_LED_OFF = "xiaomi_miio_set_led_off" -SERVICE_SET_CHILD_LOCK_ON = "xiaomi_miio_set_child_lock_on" -SERVICE_SET_CHILD_LOCK_OFF = "xiaomi_miio_set_child_lock_off" -SERVICE_SET_LED_BRIGHTNESS = "xiaomi_miio_set_led_brightness" -SERVICE_SET_FAVORITE_LEVEL = "xiaomi_miio_set_favorite_level" -SERVICE_SET_AUTO_DETECT_ON = "xiaomi_miio_set_auto_detect_on" -SERVICE_SET_AUTO_DETECT_OFF = "xiaomi_miio_set_auto_detect_off" -SERVICE_SET_LEARN_MODE_ON = "xiaomi_miio_set_learn_mode_on" -SERVICE_SET_LEARN_MODE_OFF = "xiaomi_miio_set_learn_mode_off" -SERVICE_SET_VOLUME = "xiaomi_miio_set_volume" -SERVICE_RESET_FILTER = "xiaomi_miio_reset_filter" -SERVICE_SET_EXTRA_FEATURES = "xiaomi_miio_set_extra_features" -SERVICE_SET_TARGET_HUMIDITY = "xiaomi_miio_set_target_humidity" -SERVICE_SET_DRY_ON = "xiaomi_miio_set_dry_on" -SERVICE_SET_DRY_OFF = "xiaomi_miio_set_dry_off" - AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) SERVICE_SCHEMA_LED_BRIGHTNESS = AIRPURIFIER_SERVICE_SCHEMA.extend( diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 5b454512f33524..2343a6787c29c2 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -21,7 +21,6 @@ ATTR_COLOR_TEMP, ATTR_ENTITY_ID, ATTR_HS_COLOR, - DOMAIN, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, @@ -33,6 +32,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import color, dt +from .const import ( + DOMAIN, + SERVICE_SET_SCENE, + SERVICE_SET_DELAYED_TURN_OFF, + SERVICE_REMINDER_ON, + SERVICE_REMINDER_OFF, + SERVICE_NIGHT_LIGHT_MODE_ON, + SERVICE_NIGHT_LIGHT_MODE_OFF, + SERVICE_EYECARE_MODE_ON, + SERVICE_EYECARE_MODE_OFF, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Philips Light" @@ -85,15 +96,6 @@ ATTR_BRAND_SLEEP = "brand_sleep" ATTR_BRAND = "brand" -SERVICE_SET_SCENE = "xiaomi_miio_set_scene" -SERVICE_SET_DELAYED_TURN_OFF = "xiaomi_miio_set_delayed_turn_off" -SERVICE_REMINDER_ON = "xiaomi_miio_reminder_on" -SERVICE_REMINDER_OFF = "xiaomi_miio_reminder_off" -SERVICE_NIGHT_LIGHT_MODE_ON = "xiaomi_miio_night_light_mode_on" -SERVICE_NIGHT_LIGHT_MODE_OFF = "xiaomi_miio_night_light_mode_off" -SERVICE_EYECARE_MODE_ON = "xiaomi_miio_eyecare_mode_on" -SERVICE_EYECARE_MODE_OFF = "xiaomi_miio_eyecare_mode_off" - XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) SERVICE_SCHEMA_SET_SCENE = XIAOMI_MIIO_SERVICE_SCHEMA.extend( diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 0e2ac476e0577c..1e7cada1a7b3b8 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -11,7 +11,6 @@ ATTR_DELAY_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, - DOMAIN, PLATFORM_SCHEMA, RemoteDevice, ) @@ -28,9 +27,10 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow +from .const import DOMAIN, SERVICE_LEARN + _LOGGER = logging.getLogger(__name__) -SERVICE_LEARN = "xiaomi_miio_learn_command" DATA_KEY = "remote.xiaomi_miio" CONF_SLOT = "slot" diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index e69de29bb2d1d6..36dcbc950be3ae 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -0,0 +1,308 @@ +fan_set_buzzer_on: + description: Turn the buzzer on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_buzzer_off: + description: Turn the buzzer off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_led_on: + description: Turn the led on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_led_off: + description: Turn the led off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_child_lock_on: + description: Turn the child lock on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_child_lock_off: + description: Turn the child lock off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_favorite_level: + description: Set the favorite level. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + level: + description: Level, between 0 and 16. + example: 1 + +fan_set_led_brightness: + description: Set the led brightness. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + brightness: + description: Brightness (0 = Bright, 1 = Dim, 2 = Off) + example: 1 + +fan_set_auto_detect_on: + description: Turn the auto detect on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_auto_detect_off: + description: Turn the auto detect off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_learn_mode_on: + description: Turn the learn mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_learn_mode_off: + description: Turn the learn mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_volume: + description: Set the sound volume. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + volume: + description: Volume, between 0 and 100. + example: 50 + +fan_reset_filter: + description: Reset the filter lifetime and usage. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_extra_features: + description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + features: + description: Integer, known values are 0 (default) and 1 (turbo mode). + example: 1 + +fan_set_target_humidity: + description: Set the target humidity. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + humidity: + description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80. + example: 50 + +fan_set_dry_on: + description: Turn the dry mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +fan_set_dry_off: + description: Turn the dry mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +light_set_scene: + description: Set a fixed scene. + fields: + entity_id: + description: Name of the light entity. + example: "light.xiaomi_miio" + scene: + description: Number of the fixed scene, between 1 and 4. + example: 1 + +light_set_delayed_turn_off: + description: Delayed turn off. + fields: + entity_id: + description: Name of the light entity. + example: "light.xiaomi_miio" + time_period: + description: Time period for the delayed turn off. + example: "5, '0:05', {'minutes': 5}" + +light_reminder_on: + description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_reminder_off: + description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_night_light_mode_on: + description: Turn the eyecare mode on (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_night_light_mode_off: + description: Turn the eyecare mode fan_set_dry_off (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_eyecare_mode_on: + description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +light_eyecare_mode_off: + description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). + fields: + entity_id: + description: 'Name of the entity to act on.' + example: 'light.xiaomi_miio' + +remote_learn_command: + description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' + fields: + entity_id: + description: 'Name of the entity to learn command from.' + example: 'remote.xiaomi_miio' + slot: + description: 'Define the slot used to save the IR command (Value from 1 to 1000000)' + example: '1' + timeout: + description: 'Define the timeout in seconds, before which the command must be learned.' + example: '30' + +switch_set_wifi_led_on: + description: Turn the wifi led on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + +switch_set_wifi_led_off: + description: Turn the wifi led off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + +switch_set_power_price: + description: Set the power price. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power price, between 0 and 999. + example: 31 + +switch_set_power_mode: + description: Set the power mode. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power mode, valid values are 'normal' and 'green'. + example: 'green' + +vacuum_remote_control_start: + description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + +vacuum_remote_control_stop: + description: Stop remote control mode of the vacuum cleaner. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + +vacuum_remote_control_move: + description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + velocity: + description: Speed, between -0.29 and 0.29. + example: '0.2' + rotation: + description: Rotation, between -179 degrees and 179 degrees. + example: '90' + duration: + description: Duration of the movement. + example: '1500' + +vacuum_remote_control_move_step: + description: Remote control the vacuum cleaner, only makes one move and then stops. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + velocity: + description: Speed, between -0.29 and 0.29. + example: '0.2' + rotation: + description: Rotation, between -179 degrees and 179 degrees. + example: '90' + duration: + description: Duration of the movement. + example: '1500' + +vacuum_clean_zone: + description: Start the cleaning operation in the selected areas for the number of repeats indicated. + fields: + entity_id: + description: Name of the vacuum entity. + example: 'vacuum.xiaomi_vacuum_cleaner' + zone: + description: Array of zones. Each zone is an array of 4 integer values. + example: '[[23510,25311,25110,26362]]' + repeats: + description: Number of cleaning repeats for each zone between 1 and 3. + example: '1' diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 023243a1995cb7..f9a06924b5c334 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -13,7 +13,7 @@ from miio.powerstrip import PowerMode # pylint: disable=import-error import voluptuous as vol -from homeassistant.components.switch import DOMAIN, PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, @@ -24,6 +24,14 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from .const import ( + DOMAIN, + SERVICE_SET_WIFI_LED_ON, + SERVICE_SET_WIFI_LED_OFF, + SERVICE_SET_POWER_MODE, + SERVICE_SET_POWER_PRICE, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Miio Switch" @@ -80,11 +88,6 @@ FEATURE_FLAGS_PLUG_V3 = FEATURE_SET_WIFI_LED -SERVICE_SET_WIFI_LED_ON = "xiaomi_miio_set_wifi_led_on" -SERVICE_SET_WIFI_LED_OFF = "xiaomi_miio_set_wifi_led_off" -SERVICE_SET_POWER_MODE = "xiaomi_miio_set_power_mode" -SERVICE_SET_POWER_PRICE = "xiaomi_miio_set_power_price" - SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) SERVICE_SCHEMA_POWER_MODE = SERVICE_SCHEMA.extend( diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 0320a2e49060d7..f1845f534bb1ad 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -8,7 +8,6 @@ from homeassistant.components.vacuum import ( ATTR_CLEANED_AREA, - DOMAIN, PLATFORM_SCHEMA, STATE_CLEANING, STATE_DOCKED, @@ -38,6 +37,15 @@ ) import homeassistant.helpers.config_validation as cv +from .const import ( + DOMAIN, + SERVICE_MOVE_REMOTE_CONTROL, + SERVICE_MOVE_REMOTE_CONTROL_STEP, + SERVICE_START_REMOTE_CONTROL, + SERVICE_STOP_REMOTE_CONTROL, + SERVICE_CLEAN_ZONE, +) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Vacuum cleaner" @@ -52,12 +60,6 @@ extra=vol.ALLOW_EXTRA, ) -SERVICE_MOVE_REMOTE_CONTROL = "xiaomi_remote_control_move" -SERVICE_MOVE_REMOTE_CONTROL_STEP = "xiaomi_remote_control_move_step" -SERVICE_START_REMOTE_CONTROL = "xiaomi_remote_control_start" -SERVICE_STOP_REMOTE_CONTROL = "xiaomi_remote_control_stop" -SERVICE_CLEAN_ZONE = "xiaomi_clean_zone" - FAN_SPEEDS = {"Quiet": 38, "Balanced": 60, "Turbo": 77, "Max": 90, "Gentle": 105} ATTR_CLEAN_START = "clean_start" diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 040e04b3c832d2..5b6ce578c8b5a1 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -36,6 +36,7 @@ CONF_HOST, CONF_NAME, CONF_TOKEN, + DOMAIN as XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, SERVICE_MOVE_REMOTE_CONTROL_STEP, SERVICE_START_REMOTE_CONTROL, @@ -366,7 +367,10 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): # Xiaomi vacuum specific services: yield from hass.services.async_call( - DOMAIN, SERVICE_START_REMOTE_CONTROL, {ATTR_ENTITY_ID: entity_id}, blocking=True + XIAOMI_DOMAIN, + SERVICE_START_REMOTE_CONTROL, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, ) mock_mirobo_is_on.assert_has_calls([mock.call.manual_start()], any_order=True) @@ -375,7 +379,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): control = {"duration": 1000, "rotation": -40, "velocity": -0.1} yield from hass.services.async_call( - DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, control, blocking=True + XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, control, blocking=True ) mock_mirobo_is_on.manual_control.assert_has_calls( [mock.call(**control)], any_order=True @@ -384,7 +388,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): mock_mirobo_is_on.reset_mock() yield from hass.services.async_call( - DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True + XIAOMI_DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True ) mock_mirobo_is_on.assert_has_calls([mock.call.manual_stop()], any_order=True) mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) @@ -392,7 +396,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): control_once = {"duration": 2000, "rotation": 120, "velocity": 0.1} yield from hass.services.async_call( - DOMAIN, SERVICE_MOVE_REMOTE_CONTROL_STEP, control_once, blocking=True + XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL_STEP, control_once, blocking=True ) mock_mirobo_is_on.manual_control_once.assert_has_calls( [mock.call(**control_once)], any_order=True @@ -402,7 +406,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): control = {"zone": [[123, 123, 123, 123]], "repeats": 2} yield from hass.services.async_call( - DOMAIN, SERVICE_CLEAN_ZONE, control, blocking=True + XIAOMI_DOMAIN, SERVICE_CLEAN_ZONE, control, blocking=True ) mock_mirobo_is_on.zoned_clean.assert_has_calls( [mock.call([[123, 123, 123, 123, 2]])], any_order=True From 67498595e430cd258119f52eda581deacb83d3c6 Mon Sep 17 00:00:00 2001 From: NobleKangaroo <34781835+NobleKangaroo@users.noreply.github.com> Date: Mon, 2 Dec 2019 15:29:31 -0500 Subject: [PATCH 1960/3953] Add Emulated Hue code owner (#29319) --- CODEOWNERS | 1 + homeassistant/components/emulated_hue/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index b76909de9d6b8b..cb86e5c0c901fb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -87,6 +87,7 @@ homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 +homeassistant/components/emulated_hue/* @NobleKangaroo homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer homeassistant/components/entur_public_transport/* @hfurubotten diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index 9b3b00d20b21e9..ddd39443886abd 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -6,5 +6,7 @@ "aiohttp_cors==0.7.0" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@NobleKangaroo" + ] } From 5a24dbf599862c5b04e1f288e1a537353c8cdb39 Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 2 Dec 2019 18:20:36 -0300 Subject: [PATCH 1961/3953] Broadlink remote (#26528) * Add broadlink remote control platform * Fix order of the imports * Add remote.py to .coveragerc * Optimize MAC address validation * Use storage helper class and improve code readability * Add me to the manifest as a code owner * Fix dosctring * Add me to the code owners * Remove storage schemas, rename storage keys and improve readability --- .coveragerc | 1 + CODEOWNERS | 2 +- .../components/broadlink/__init__.py | 27 ++ .../components/broadlink/manifest.json | 3 +- homeassistant/components/broadlink/remote.py | 349 ++++++++++++++++++ 5 files changed, 380 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/broadlink/remote.py diff --git a/.coveragerc b/.coveragerc index 600701d792b4f6..7f519f8970a079 100644 --- a/.coveragerc +++ b/.coveragerc @@ -94,6 +94,7 @@ omit = homeassistant/components/bom/sensor.py homeassistant/components/bom/weather.py homeassistant/components/braviatv/media_player.py + homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/switch.py homeassistant/components/brottsplatskartan/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index cb86e5c0c901fb..e859a9d0eac941 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -50,7 +50,7 @@ homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blink/* @fronzbot homeassistant/components/bmw_connected_drive/* @gerard33 homeassistant/components/braviatv/* @robbiet480 -homeassistant/components/broadlink/* @danielhiversen +homeassistant/components/broadlink/* @danielhiversen @felipediel homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 589da62feaa13b..521cd68780c6ef 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -1,7 +1,9 @@ """The broadlink component.""" import asyncio from base64 import b64decode, b64encode +from binascii import unhexlify import logging +import re import socket from datetime import timedelta @@ -27,6 +29,31 @@ def data_packet(value): return b64decode(value) +def hostname(value): + """Validate a hostname.""" + host = str(value).lower() + if len(host) > 253: + raise ValueError + if host[-1] == ".": + host = host[:-1] + allowed = re.compile(r"(?!-)[a-z\d-]{1,63}(? Date: Tue, 3 Dec 2019 00:53:04 +0100 Subject: [PATCH 1962/3953] Move imports to top for python_script (#29331) --- .../components/python_script/__init__.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 046649fce7930b..e36ac397c0fa7a 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -5,6 +5,15 @@ import os import time +from RestrictedPython import compile_restricted_exec +from RestrictedPython.Eval import default_guarded_getitem +from RestrictedPython.Guards import ( + full_write_guard, + guarded_iter_unpack_sequence, + guarded_unpack_sequence, + safe_builtins, +) +from RestrictedPython.Utilities import utility_builtins import voluptuous as vol from homeassistant.const import SERVICE_RELOAD @@ -12,8 +21,8 @@ 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 +from homeassistant.util.yaml.loader import load_yaml _LOGGER = logging.getLogger(__name__) @@ -122,15 +131,6 @@ def execute_script(hass, name, data=None): @bind_hass def execute(hass, filename, source, data=None): """Execute Python source.""" - from RestrictedPython import compile_restricted_exec - from RestrictedPython.Guards import ( - safe_builtins, - full_write_guard, - guarded_iter_unpack_sequence, - guarded_unpack_sequence, - ) - from RestrictedPython.Utilities import utility_builtins - from RestrictedPython.Eval import default_guarded_getitem compiled = compile_restricted_exec(source, filename=filename) From 98681618218e5d402e01ce7ae7b28becfff7350a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 00:53:42 +0100 Subject: [PATCH 1963/3953] Move imports to top for postnl (#29330) --- homeassistant/components/postnl/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/postnl/sensor.py b/homeassistant/components/postnl/sensor.py index 6155f58519a649..2e1f8176835b4c 100644 --- a/homeassistant/components/postnl/sensor.py +++ b/homeassistant/components/postnl/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from postnl_api import PostNL_API, UnauthorizedException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -36,7 +37,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the PostNL sensor platform.""" - from postnl_api import PostNL_API, UnauthorizedException username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) From 372aa312e1df1b63582daf2d87b6d7d61f10fd67 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 00:54:17 +0100 Subject: [PATCH 1964/3953] Move imports to top for prezzibenzina (#29329) --- homeassistant/components/prezzibenzina/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/prezzibenzina/sensor.py b/homeassistant/components/prezzibenzina/sensor.py index f1f41ba46bad0a..c985f96e6c6063 100644 --- a/homeassistant/components/prezzibenzina/sensor.py +++ b/homeassistant/components/prezzibenzina/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging +from prezzibenzina import PrezziBenzinaPy import voluptuous as vol from homeassistant.const import ATTR_ATTRIBUTION, ATTR_TIME, CONF_NAME @@ -43,7 +44,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the PrezziBenzina sensor platform.""" - from prezzibenzina import PrezziBenzinaPy station = config[CONF_STATION] name = config.get(CONF_NAME) From 1adcdad5a4dec73918f4a0909ae7c7658e3aa22e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 00:56:08 +0100 Subject: [PATCH 1965/3953] Move imports to top for quantum_gateway (#29327) --- homeassistant/components/quantum_gateway/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index ea3979e77570e6..97eb8eedfd3915 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -1,6 +1,7 @@ """Support for Verizon FiOS Quantum Gateways.""" import logging +from quantum_gateway import QuantumGatewayScanner from requests.exceptions import RequestException import voluptuous as vol @@ -37,7 +38,6 @@ class QuantumGatewayDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from quantum_gateway import QuantumGatewayScanner self.host = config[CONF_HOST] self.password = config[CONF_PASSWORD] From 61cb0924db179b33f83b3de952b7f4427f456c75 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 00:56:58 +0100 Subject: [PATCH 1966/3953] Move imports to top for qnap (#29326) --- homeassistant/components/qnap/sensor.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index efbb1ac26ca53a..c3863bd0077c69 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -1,26 +1,27 @@ """Support for QNAP NAS Sensors.""" -import logging from datetime import timedelta +import logging +from qnapstats import QNAPStats import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity from homeassistant.const import ( + ATTR_NAME, CONF_HOST, - CONF_USERNAME, + CONF_MONITORED_CONDITIONS, CONF_PASSWORD, CONF_PORT, CONF_SSL, - ATTR_NAME, - CONF_VERIFY_SSL, CONF_TIMEOUT, - CONF_MONITORED_CONDITIONS, + CONF_USERNAME, + CONF_VERIFY_SSL, TEMP_CELSIUS, ) -from homeassistant.util import Throttle from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -170,7 +171,6 @@ class QNAPStatsAPI: def __init__(self, config): """Initialize the API wrapper.""" - from qnapstats import QNAPStats protocol = "https" if config.get(CONF_SSL) else "http" self._api = QNAPStats( From 55ba956d3de7811f737b2c568e1c2f00e182d1c3 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 00:57:47 +0100 Subject: [PATCH 1967/3953] Move imports to top for qbittorrent (#29325) --- homeassistant/components/qbittorrent/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index f00b392065cb7b..0299277059b484 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -1,9 +1,9 @@ """Support for monitoring the qBittorrent API.""" import logging -import voluptuous as vol - +from qbittorrent.client import Client, LoginRequired from requests.exceptions import RequestException +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( @@ -13,9 +13,9 @@ CONF_USERNAME, STATE_IDLE, ) -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -43,7 +43,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the qBittorrent sensors.""" - from qbittorrent.client import Client, LoginRequired try: client = Client(config[CONF_URL]) From bacc5495767e79fb81483370c79150cac666a835 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 00:58:18 +0100 Subject: [PATCH 1968/3953] Move imports to top for plum_lightpad (#29324) --- homeassistant/components/plum_lightpad/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index 67a3b60e8badb1..bfdf67a0f400f9 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +from plumlightpad import Plum import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP @@ -30,7 +31,6 @@ async def async_setup(hass, config): """Plum Lightpad Platform initialization.""" - from plumlightpad import Plum conf = config[DOMAIN] plum = Plum(conf[CONF_USERNAME], conf[CONF_PASSWORD]) From 9160d43a08f5acbefaf9ec36078ec5d0b90271a7 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 00:59:13 +0100 Subject: [PATCH 1969/3953] Move imports to top for rachio (#29323) --- homeassistant/components/rachio/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 28614d22110af6..4d67175ecd56d9 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -4,11 +4,13 @@ from typing import Optional from aiohttp import web +from rachiopy import Rachio import voluptuous as vol + from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API -from homeassistant.helpers import discovery, config_validation as cv +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) @@ -102,7 +104,6 @@ def setup(hass, config) -> bool: """Set up the Rachio component.""" - from rachiopy import Rachio # Listen for incoming webhook connections hass.http.register_view(RachioWebhookView()) From 9811d63d78f3b474b3e900ba359b3cb716848bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Tue, 3 Dec 2019 00:02:17 +0000 Subject: [PATCH 1970/3953] Improve Alexa interface selection for binary sensors (#29120) * Improve Alexa interface selection for binary sensors This allows the sensor to work correctly as a contact or motion sensor in alexa, if the user overrides its display category as such. * add tests --- homeassistant/components/alexa/entities.py | 4 ++ tests/components/alexa/__init__.py | 6 ++- tests/components/alexa/test_smart_home.py | 58 ++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index f9cf0cdf3532f8..f9463949b58606 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -543,6 +543,10 @@ def interfaces(self): if CONF_DISPLAY_CATEGORIES in entity_conf: if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL: yield AlexaDoorbellEventSource(self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CONTACT_SENSOR: + yield AlexaContactSensor(self.hass, self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.MOTION_SENSOR: + yield AlexaMotionSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 0fa1961ad61c03..3752ad7d48fe66 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -13,7 +13,11 @@ class MockConfig(config.AbstractConfig): """Mock Alexa config.""" - entity_config = {"binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}} + entity_config = { + "binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}, + "binary_sensor.test_contact_forced": {"display_categories": "CONTACT_SENSOR"}, + "binary_sensor.test_motion_forced": {"display_categories": "MOTION_SENSOR"}, + } @property def supports_auth(self): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 7cc1638bf2586b..b720182e4d25dd 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1473,6 +1473,35 @@ async def test_contact_sensor(hass): properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) +async def test_forced_contact_sensor(hass): + """Test contact sensor discovery with specified display_category.""" + device = ( + "binary_sensor.test_contact_forced", + "on", + {"friendly_name": "Test Contact Sensor With DisplayCategory"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_contact_forced" + assert appliance["displayCategories"][0] == "CONTACT_SENSOR" + assert appliance["friendlyName"] == "Test Contact Sensor With DisplayCategory" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.ContactSensor", "Alexa.EndpointHealth", "Alexa" + ) + + contact_sensor_capability = get_capability(capabilities, "Alexa.ContactSensor") + assert contact_sensor_capability is not None + properties = contact_sensor_capability["properties"] + assert properties["retrievable"] is True + assert {"name": "detectionState"} in properties["supported"] + + properties = await reported_properties(hass, "binary_sensor#test_contact_forced") + properties.assert_equal("Alexa.ContactSensor", "detectionState", "DETECTED") + + properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) + + async def test_motion_sensor(hass): """Test motion sensor discovery.""" device = ( @@ -1500,6 +1529,35 @@ async def test_motion_sensor(hass): properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED") +async def test_forced_motion_sensor(hass): + """Test motion sensor discovery with specified display_category.""" + device = ( + "binary_sensor.test_motion_forced", + "on", + {"friendly_name": "Test Motion Sensor With DisplayCategory"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_motion_forced" + assert appliance["displayCategories"][0] == "MOTION_SENSOR" + assert appliance["friendlyName"] == "Test Motion Sensor With DisplayCategory" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.MotionSensor", "Alexa.EndpointHealth", "Alexa" + ) + + motion_sensor_capability = get_capability(capabilities, "Alexa.MotionSensor") + assert motion_sensor_capability is not None + properties = motion_sensor_capability["properties"] + assert properties["retrievable"] is True + assert {"name": "detectionState"} in properties["supported"] + + properties = await reported_properties(hass, "binary_sensor#test_motion_forced") + properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED") + + properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) + + async def test_doorbell_sensor(hass): """Test doorbell sensor discovery.""" device = ( From 83b21651ceddcea9da6e5d4a0d1e1ac7ad288609 Mon Sep 17 00:00:00 2001 From: Alain Turbide <7193213+Dilbert66@users.noreply.github.com> Date: Mon, 2 Dec 2019 19:10:44 -0500 Subject: [PATCH 1971/3953] Add alexa check for "name" channelMetadata attribute (#29279) * Added check for "name" channelMetada attribute * Added/changed smart home media player tests for added value name in chanelMetadata payload section * Since Alexa only expects a number/callSign/affiliate in the returned response, returning "name" is not technically valid. Modified to return the value in the callSign field instead since it's a text value. Since there is no returned channel number, cannot return a true numeric value in "channel" field --- homeassistant/components/alexa/handlers.py | 22 +++++++++++++--------- tests/components/alexa/test_smart_home.py | 20 ++++++++++++++++---- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 2e360fba7e2a6c..f1aa260e88ea44 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1144,21 +1144,25 @@ async def async_api_changechannel(hass, config, directive, context): """Process a change channel request.""" channel = "0" entity = directive.entity - payload = directive.payload["channel"] + channel_payload = directive.payload["channel"] + metadata_payload = directive.payload["channelMetadata"] payload_name = "number" - if "number" in payload: - channel = payload["number"] + if "number" in channel_payload: + channel = channel_payload["number"] payload_name = "number" - elif "callSign" in payload: - channel = payload["callSign"] + elif "callSign" in channel_payload: + channel = channel_payload["callSign"] payload_name = "callSign" - elif "affiliateCallSign" in payload: - channel = payload["affiliateCallSign"] + elif "affiliateCallSign" in channel_payload: + channel = channel_payload["affiliateCallSign"] payload_name = "affiliateCallSign" - elif "uri" in payload: - channel = payload["uri"] + elif "uri" in channel_payload: + channel = channel_payload["uri"] payload_name = "uri" + elif "name" in metadata_payload: + channel = metadata_payload["name"] + payload_name = "callSign" data = { ATTR_ENTITY_ID: entity.entity_id, diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index b720182e4d25dd..9a2eba21c0e270 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -935,7 +935,7 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"number": 24}}, + payload={"channel": {"number": "24"}, "channelMetadata": {"name": ""}}, ) call, _ = await assert_request_calls_service( @@ -944,7 +944,7 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"callSign": "ABC"}}, + payload={"channel": {"callSign": "ABC"}, "channelMetadata": {"name": ""}}, ) call, _ = await assert_request_calls_service( @@ -953,7 +953,7 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"affiliateCallSign": "ABC"}}, + payload={"channel": {"number": ""}, "channelMetadata": {"name": "ABC"}}, ) call, _ = await assert_request_calls_service( @@ -962,7 +962,19 @@ async def test_media_player(hass): "media_player#test", "media_player.play_media", hass, - payload={"channel": {"uri": "ABC"}}, + payload={ + "channel": {"affiliateCallSign": "ABC"}, + "channelMetadata": {"name": ""}, + }, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"uri": "ABC"}, "channelMetadata": {"name": ""}}, ) call, _ = await assert_request_calls_service( From 141fd1bffd767bb4ce958ea143210ca96ef0a9e3 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 01:11:10 +0100 Subject: [PATCH 1972/3953] Move imports to top for qwikswitch (#29328) --- homeassistant/components/qwikswitch/__init__.py | 4 ++-- homeassistant/components/qwikswitch/binary_sensor.py | 3 ++- homeassistant/components/qwikswitch/sensor.py | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index 1ae92b0a18ad05..33392c51be8e0c 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -1,6 +1,8 @@ """Support for Qwikswitch devices.""" import logging +from pyqwikswitch.async_ import QSUsb +from pyqwikswitch.qwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, SENSORS, QSType import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA @@ -128,8 +130,6 @@ async def async_turn_off(self, **_): async def async_setup(hass, config): """Qwiskswitch component setup.""" - from pyqwikswitch.async_ import QSUsb - from pyqwikswitch.qwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType, SENSORS # Add cmd's to in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] diff --git a/homeassistant/components/qwikswitch/binary_sensor.py b/homeassistant/components/qwikswitch/binary_sensor.py index a5b142e19aede4..054028b5629e82 100644 --- a/homeassistant/components/qwikswitch/binary_sensor.py +++ b/homeassistant/components/qwikswitch/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Qwikswitch Binary Sensors.""" import logging +from pyqwikswitch.qwikswitch import SENSORS + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback @@ -27,7 +29,6 @@ class QSBinarySensor(QSEntity, BinarySensorDevice): def __init__(self, sensor): """Initialize the sensor.""" - from pyqwikswitch.qwikswitch import SENSORS super().__init__(sensor["id"], sensor["name"]) self.channel = sensor["channel"] diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 01964fc7831e04..4674da420b22b9 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -1,6 +1,8 @@ """Support for Qwikswitch Sensors.""" import logging +from pyqwikswitch.qwikswitch import SENSORS + from homeassistant.core import callback from . import DOMAIN as QWIKSWITCH, QSEntity @@ -26,7 +28,6 @@ class QSSensor(QSEntity): def __init__(self, sensor): """Initialize the sensor.""" - from pyqwikswitch.qwikswitch import SENSORS super().__init__(sensor["id"], sensor["name"]) self.channel = sensor["channel"] From 8f608608ed2323a4272455b244eb7632894d554c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 01:11:22 +0100 Subject: [PATCH 1973/3953] Move imports to top for radarr (#29322) --- homeassistant/components/radarr/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 0dcdcf1514fda7..79e45ffd9a82be 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -1,21 +1,22 @@ """Support for Radarr.""" +from datetime import datetime, timedelta import logging import time -from datetime import datetime, timedelta +from pytz import timezone import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, CONF_HOST, - CONF_PORT, CONF_MONITORED_CONDITIONS, + CONF_PORT, CONF_SSL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -79,7 +80,6 @@ class RadarrSensor(Entity): def __init__(self, hass, conf, sensor_type): """Create Radarr entity.""" - from pytz import timezone self.conf = conf self.host = conf.get(CONF_HOST) From 76aaf8b560027dbacef6aaf205126076a6e747ff Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Dec 2019 01:12:18 +0100 Subject: [PATCH 1974/3953] Improve WLED white value support for RGBW strips (#29312) --- homeassistant/components/wled/light.py | 42 +++++++++++++++++++++----- tests/components/wled/test_light.py | 18 ++++++++++- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 3d2c9d6ef2c94d..8bc1a56b2051d2 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -10,11 +10,13 @@ ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_TRANSITION, + SUPPORT_WHITE_VALUE, Light, ) from homeassistant.config_entries import ConfigEntry @@ -79,6 +81,7 @@ def __init__( self._color: Optional[Tuple[float, float]] = None self._effect: Optional[str] = None self._state: Optional[bool] = None + self._white_value: Optional[int] = None # Only apply the segment ID if it is not the first segment name = wled.device.info.name @@ -107,10 +110,15 @@ def brightness(self) -> Optional[int]: """Return the brightness of this light between 1..255.""" return self._brightness + @property + def white_value(self) -> Optional[int]: + """Return the white value of this light between 0..255.""" + return self._white_value + @property def supported_features(self) -> int: """Flag supported features.""" - return ( + flags = ( SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP @@ -118,6 +126,11 @@ def supported_features(self) -> int: | SUPPORT_TRANSITION ) + if self._rgbw: + flags |= SUPPORT_WHITE_VALUE + + return flags + @property def effect_list(self) -> List[str]: """Return the list of supported effects.""" @@ -163,11 +176,21 @@ async def async_turn_on(self, **kwargs: Any) -> None: if ATTR_EFFECT in kwargs: data[ATTR_EFFECT] = kwargs[ATTR_EFFECT] - # Support for RGBW strips - if self._rgbw and any(x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs): - data[ATTR_COLOR_PRIMARY] = color_util.color_rgb_to_rgbw( - *data[ATTR_COLOR_PRIMARY] - ) + # Support for RGBW strips, adds white value + if self._rgbw and any( + x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_WHITE_VALUE) for x in kwargs + ): + # WLED cannot just accept a white value, it needs the color. + # We use the last know color in case just the white value changes. + if not any(x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs): + hue, sat = self._color + data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100) + + # Add requested or last known white value + if ATTR_WHITE_VALUE in kwargs: + data[ATTR_COLOR_PRIMARY] += (kwargs[ATTR_WHITE_VALUE],) + else: + data[ATTR_COLOR_PRIMARY] += (self._white_value,) try: await self.wled.light(**data) @@ -186,6 +209,9 @@ async def async_turn_on(self, **kwargs: Any) -> None: if ATTR_COLOR_TEMP in kwargs: self._color = color_util.color_temperature_to_hs(mireds) + if ATTR_WHITE_VALUE in kwargs: + self._white_value = kwargs[ATTR_WHITE_VALUE] + except WLEDError: _LOGGER.error("An error occurred while turning on WLED light.") self._available = False @@ -198,9 +224,9 @@ async def _wled_update(self) -> None: self._state = self.wled.device.state.on color = self.wled.device.state.segments[self._segment].color_primary + self._color = color_util.color_RGB_to_hs(*color[:3]) if self._rgbw: - color = color_util.color_rgbw_to_rgb(*color) - self._color = color_util.color_RGB_to_hs(*color) + self._white_value = color[-1] playlist = self.wled.device.state.playlist if playlist == -1: diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 185a25b0507117..037081608af422 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -8,6 +8,7 @@ ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.components.wled.const import ( @@ -164,7 +165,8 @@ async def test_rgbw_light( state = hass.states.get("light.wled_rgbw_light") assert state.state == STATE_ON - assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 64.706) + assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0) + assert state.attributes.get(ATTR_WHITE_VALUE) == 139 await hass.services.async_call( LIGHT_DOMAIN, @@ -177,3 +179,17 @@ async def test_rgbw_light( state = hass.states.get("light.wled_rgbw_light") assert state.state == STATE_ON assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) + assert state.attributes.get(ATTR_WHITE_VALUE) == 139 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_WHITE_VALUE: 100}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgbw_light") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) + assert state.attributes.get(ATTR_WHITE_VALUE) == 100 From 9587afc5ced570600a9f2955fc4f18923fadf519 Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Tue, 3 Dec 2019 01:19:56 +0100 Subject: [PATCH 1975/3953] delete incorrect "mdi:brightness-3" (#29309) delete incorrect 'mdi:brightness-3' in final ``` def icon(self): """Icon to use in the frontend, if any.""" return MOON_ICONS.get(self.state, "mdi:brightness-3") ``` which still shows in the sensors attributes --- homeassistant/components/moon/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index dac7d36eda5f62..3a7dd9e20844d2 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -81,7 +81,7 @@ def state(self): @property def icon(self): """Icon to use in the frontend, if any.""" - return MOON_ICONS.get(self.state, "mdi:brightness-3") + return MOON_ICONS.get(self.state) async def async_update(self): """Get the time and updates the states.""" From 02d9ed5e36257490726a75c8440b4a2b61bc7fe4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Dec 2019 16:23:12 -0800 Subject: [PATCH 1976/3953] Do not select all entities when omitting entity ID in service call (#29178) * Do not select all entities when omitting entity ID * Address comments Matthew * Require either area_id or entity_id * Fix tests * Fix test --- .../alarm_control_panel/__init__.py | 6 +- .../components/automation/__init__.py | 14 ++- homeassistant/components/climate/__init__.py | 81 ++++++++--------- homeassistant/components/counter/__init__.py | 32 +++---- homeassistant/components/cover/__init__.py | 47 ++++------ homeassistant/components/fan/__init__.py | 35 +++----- homeassistant/components/group/__init__.py | 50 +++++------ .../components/image_processing/__init__.py | 4 +- .../components/input_boolean/__init__.py | 13 +-- .../components/input_datetime/__init__.py | 17 ++-- .../components/input_number/__init__.py | 17 ++-- .../components/input_select/__init__.py | 32 +++---- .../components/input_text/__init__.py | 7 +- homeassistant/components/lifx/light.py | 18 ++-- homeassistant/components/light/__init__.py | 77 ++++++++-------- homeassistant/components/lock/__init__.py | 4 +- .../components/media_extractor/__init__.py | 10 ++- .../components/media_player/__init__.py | 90 ++++++------------- homeassistant/components/remote/__init__.py | 42 ++++----- homeassistant/components/script/__init__.py | 4 +- homeassistant/components/timer/__init__.py | 25 +++--- .../components/utility_meter/__init__.py | 16 ++-- homeassistant/components/vacuum/__init__.py | 57 ++++-------- homeassistant/components/wink/__init__.py | 22 ++--- homeassistant/helpers/config_validation.py | 23 +++-- homeassistant/helpers/entity_component.py | 18 ++-- homeassistant/helpers/service.py | 14 +-- .../components/alarm_control_panel/common.py | 25 +++--- tests/components/automation/common.py | 9 +- tests/components/camera/common.py | 15 ++-- tests/components/climate/common.py | 33 +++---- tests/components/demo/test_light.py | 8 +- tests/components/fan/common.py | 17 ++-- tests/components/image_processing/common.py | 6 +- tests/components/light/common.py | 13 +-- tests/components/lock/common.py | 13 +-- tests/components/media_player/common.py | 35 ++++---- tests/components/mqtt/test_state_vacuum.py | 49 +++++++--- tests/components/remote/common.py | 25 ++++-- tests/components/scene/common.py | 4 +- tests/components/smartthings/test_climate.py | 8 +- tests/components/smartthings/test_cover.py | 10 ++- tests/components/switch/common.py | 15 ++-- tests/components/vacuum/common.py | 49 +++++----- tests/components/water_heater/common.py | 10 ++- tests/helpers/test_entity_component.py | 12 ++- tests/helpers/test_service.py | 23 +++-- 47 files changed, 533 insertions(+), 621 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 2a335651d96ce8..dfac0fd192f93c 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -17,7 +17,7 @@ ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, + make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -41,9 +41,7 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" -ALARM_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Optional(ATTR_CODE): cv.string} -) +ALARM_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) async def async_setup(hass, config): diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 3409ce832ddb33..3863ab0c88df54 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -24,7 +24,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, extract_domain_configs, script import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -106,7 +106,7 @@ def _platform_validator(config): } ) -TRIGGER_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +TRIGGER_SERVICE_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_VARIABLES, default={}): dict} ) @@ -184,12 +184,18 @@ async def reload_service_handler(service_call): ) hass.services.async_register( - DOMAIN, SERVICE_TOGGLE, toggle_service_handler, schema=ENTITY_SERVICE_SCHEMA + DOMAIN, + SERVICE_TOGGLE, + toggle_service_handler, + schema=make_entity_service_schema({}), ) for service in (SERVICE_TURN_ON, SERVICE_TURN_OFF): hass.services.async_register( - DOMAIN, service, turn_onoff_service_handler, schema=ENTITY_SERVICE_SCHEMA + DOMAIN, + service, + turn_onoff_service_handler, + schema=make_entity_service_schema({}), ) return True diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 4d58e1811be143..6006b2a9a3b13b 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -19,7 +19,7 @@ ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, + make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -84,38 +84,19 @@ _LOGGER = logging.getLogger(__name__) -SET_AUX_HEAT_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_AUX_HEAT): cv.boolean} -) -SET_TEMPERATURE_SCHEMA = vol.Schema( - vol.All( - cv.has_at_least_one_key( - ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW - ), - ENTITY_SERVICE_SCHEMA.extend( - { - vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), - vol.Inclusive(ATTR_TARGET_TEMP_HIGH, "temperature"): vol.Coerce(float), - vol.Inclusive(ATTR_TARGET_TEMP_LOW, "temperature"): vol.Coerce(float), - vol.Optional(ATTR_HVAC_MODE): vol.In(HVAC_MODES), - } - ), - ) -) -SET_FAN_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_FAN_MODE): cv.string} -) -SET_PRESET_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_PRESET_MODE): cv.string} -) -SET_HVAC_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_HVAC_MODE): vol.In(HVAC_MODES)} -) -SET_HUMIDITY_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_HUMIDITY): vol.Coerce(float)} -) -SET_SWING_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_SWING_MODE): cv.string} + +SET_TEMPERATURE_SCHEMA = vol.All( + cv.has_at_least_one_key( + ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW + ), + make_entity_service_schema( + { + vol.Exclusive(ATTR_TEMPERATURE, "temperature"): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_HIGH, "temperature"): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_LOW, "temperature"): vol.Coerce(float), + vol.Optional(ATTR_HVAC_MODE): vol.In(HVAC_MODES), + } + ), ) @@ -126,32 +107,40 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: ) await component.async_setup(config) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" - ) - component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" - ) - component.async_register_entity_service( - SERVICE_SET_HVAC_MODE, SET_HVAC_MODE_SCHEMA, "async_set_hvac_mode" + SERVICE_SET_HVAC_MODE, + {vol.Required(ATTR_HVAC_MODE): vol.In(HVAC_MODES)}, + "async_set_hvac_mode", ) component.async_register_entity_service( - SERVICE_SET_PRESET_MODE, SET_PRESET_MODE_SCHEMA, "async_set_preset_mode" + SERVICE_SET_PRESET_MODE, + {vol.Required(ATTR_PRESET_MODE): cv.string}, + "async_set_preset_mode", ) component.async_register_entity_service( - SERVICE_SET_AUX_HEAT, SET_AUX_HEAT_SCHEMA, async_service_aux_heat + SERVICE_SET_AUX_HEAT, + {vol.Required(ATTR_AUX_HEAT): cv.boolean}, + async_service_aux_heat, ) component.async_register_entity_service( - SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, async_service_temperature_set + SERVICE_SET_TEMPERATURE, SET_TEMPERATURE_SCHEMA, async_service_temperature_set, ) component.async_register_entity_service( - SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, "async_set_humidity" + SERVICE_SET_HUMIDITY, + {vol.Required(ATTR_HUMIDITY): vol.Coerce(float)}, + "async_set_humidity", ) component.async_register_entity_service( - SERVICE_SET_FAN_MODE, SET_FAN_MODE_SCHEMA, "async_set_fan_mode" + SERVICE_SET_FAN_MODE, + {vol.Required(ATTR_FAN_MODE): cv.string}, + "async_set_fan_mode", ) component.async_register_entity_service( - SERVICE_SET_SWING_MODE, SET_SWING_MODE_SCHEMA, "async_set_swing_mode" + SERVICE_SET_SWING_MODE, + {vol.Required(ATTR_SWING_MODE): cv.string}, + "async_set_swing_mode", ) return True diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index aca3461b4f7fbe..c2f61d0c1b43da 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -6,7 +6,6 @@ from homeassistant.const import CONF_ICON, CONF_NAME, CONF_MAXIMUM, CONF_MINIMUM import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -33,15 +32,6 @@ SERVICE_RESET = "reset" SERVICE_CONFIGURE = "configure" -SERVICE_SCHEMA_CONFIGURE = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), - vol.Optional(ATTR_STEP): cv.positive_int, - vol.Optional(ATTR_INITIAL): cv.positive_int, - vol.Optional(VALUE): cv.positive_int, - } -) CONFIG_SCHEMA = vol.Schema( { @@ -95,17 +85,19 @@ async def async_setup(hass, config): if not entities: return False + component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") + component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") + component.async_register_entity_service(SERVICE_RESET, {}, "async_reset") component.async_register_entity_service( - SERVICE_INCREMENT, ENTITY_SERVICE_SCHEMA, "async_increment" - ) - component.async_register_entity_service( - SERVICE_DECREMENT, ENTITY_SERVICE_SCHEMA, "async_decrement" - ) - component.async_register_entity_service( - SERVICE_RESET, ENTITY_SERVICE_SCHEMA, "async_reset" - ) - component.async_register_entity_service( - SERVICE_CONFIGURE, SERVICE_SCHEMA_CONFIGURE, "async_configure" + SERVICE_CONFIGURE, + { + vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_STEP): cv.positive_int, + vol.Optional(ATTR_INITIAL): cv.positive_int, + vol.Optional(VALUE): cv.positive_int, + }, + "async_configure", ) await component.async_add_entities(entities) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index a3c28a77cbe64a..d7fc0c49742032 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -13,7 +13,6 @@ PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.components import group from homeassistant.const import ( SERVICE_OPEN_COVER, @@ -83,18 +82,6 @@ ATTR_TILT_POSITION = "tilt_position" -COVER_SET_COVER_POSITION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_POSITION): vol.All(vol.Coerce(int), vol.Range(min=0, max=100))} -) - -COVER_SET_COVER_TILT_POSITION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_TILT_POSITION): vol.All( - vol.Coerce(int), vol.Range(min=0, max=100) - ) - } -) - @bind_hass def is_closed(hass, entity_id=None): @@ -111,48 +98,50 @@ async def async_setup(hass, config): await component.async_setup(config) - component.async_register_entity_service( - SERVICE_OPEN_COVER, ENTITY_SERVICE_SCHEMA, "async_open_cover" - ) + component.async_register_entity_service(SERVICE_OPEN_COVER, {}, "async_open_cover") component.async_register_entity_service( - SERVICE_CLOSE_COVER, ENTITY_SERVICE_SCHEMA, "async_close_cover" + SERVICE_CLOSE_COVER, {}, "async_close_cover" ) component.async_register_entity_service( SERVICE_SET_COVER_POSITION, - COVER_SET_COVER_POSITION_SCHEMA, + { + vol.Required(ATTR_POSITION): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + }, "async_set_cover_position", ) - component.async_register_entity_service( - SERVICE_STOP_COVER, ENTITY_SERVICE_SCHEMA, "async_stop_cover" - ) + component.async_register_entity_service(SERVICE_STOP_COVER, {}, "async_stop_cover") - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" - ) + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") component.async_register_entity_service( - SERVICE_OPEN_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_open_cover_tilt" + SERVICE_OPEN_COVER_TILT, {}, "async_open_cover_tilt" ) component.async_register_entity_service( - SERVICE_CLOSE_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_close_cover_tilt" + SERVICE_CLOSE_COVER_TILT, {}, "async_close_cover_tilt" ) component.async_register_entity_service( - SERVICE_STOP_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_stop_cover_tilt" + SERVICE_STOP_COVER_TILT, {}, "async_stop_cover_tilt" ) component.async_register_entity_service( SERVICE_SET_COVER_TILT_POSITION, - COVER_SET_COVER_TILT_POSITION_SCHEMA, + { + vol.Required(ATTR_TILT_POSITION): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + }, "async_set_cover_tilt_position", ) component.async_register_entity_service( - SERVICE_TOGGLE_COVER_TILT, ENTITY_SERVICE_SCHEMA, "async_toggle_tilt" + SERVICE_TOGGLE_COVER_TILT, {}, "async_toggle_tilt" ) return True diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 4c6cee2927c16e..51aecc3e7c2097 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -12,7 +12,6 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -57,20 +56,6 @@ "current_direction": ATTR_DIRECTION, } -FAN_SET_SPEED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_SPEED): cv.string} -) - -FAN_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({vol.Optional(ATTR_SPEED): cv.string}) - -FAN_OSCILLATE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_OSCILLATING): cv.boolean} -) - -FAN_SET_DIRECTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Optional(ATTR_DIRECTION): cv.string} -) - @bind_hass def is_on(hass, entity_id: Optional[str] = None) -> bool: @@ -89,22 +74,22 @@ async def async_setup(hass, config: dict): await component.async_setup(config) component.async_register_entity_service( - SERVICE_TURN_ON, FAN_TURN_ON_SCHEMA, "async_turn_on" - ) - component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" - ) - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" + SERVICE_TURN_ON, {vol.Optional(ATTR_SPEED): cv.string}, "async_turn_on" ) + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") component.async_register_entity_service( - SERVICE_SET_SPEED, FAN_SET_SPEED_SCHEMA, "async_set_speed" + SERVICE_SET_SPEED, {vol.Required(ATTR_SPEED): cv.string}, "async_set_speed" ) component.async_register_entity_service( - SERVICE_OSCILLATE, FAN_OSCILLATE_SCHEMA, "async_oscillate" + SERVICE_OSCILLATE, + {vol.Required(ATTR_OSCILLATING): cv.boolean}, + "async_oscillate", ) component.async_register_entity_service( - SERVICE_SET_DIRECTION, FAN_SET_DIRECTION_SCHEMA, "async_set_direction" + SERVICE_SET_DIRECTION, + {vol.Optional(ATTR_DIRECTION): cv.string}, + "async_set_direction", ) return True diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index b4688229073136..ba12e22b53e663 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -32,7 +32,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.typing import HomeAssistantType @@ -63,28 +63,6 @@ CONTROL_TYPES = vol.In(["hidden", None]) -SET_VISIBILITY_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_VISIBLE): cv.boolean} -) - -RELOAD_SERVICE_SCHEMA = vol.Schema({}) - -SET_SERVICE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_OBJECT_ID): cv.slug, - vol.Optional(ATTR_NAME): cv.string, - vol.Optional(ATTR_VIEW): cv.boolean, - vol.Optional(ATTR_ICON): cv.string, - vol.Optional(ATTR_CONTROL): CONTROL_TYPES, - vol.Optional(ATTR_VISIBLE): cv.boolean, - vol.Optional(ATTR_ALL): cv.boolean, - vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids, - vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids, - } -) - -REMOVE_SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_OBJECT_ID): cv.slug}) - _LOGGER = logging.getLogger(__name__) @@ -227,7 +205,7 @@ async def reload_service_handler(service): await component.async_add_entities(auto) hass.services.async_register( - DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA + DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=vol.Schema({}) ) service_lock = asyncio.Lock() @@ -319,11 +297,29 @@ async def groups_service_handler(service): await component.async_remove_entity(entity_id) hass.services.async_register( - DOMAIN, SERVICE_SET, locked_service_handler, schema=SET_SERVICE_SCHEMA + DOMAIN, + SERVICE_SET, + locked_service_handler, + schema=vol.Schema( + { + vol.Required(ATTR_OBJECT_ID): cv.slug, + vol.Optional(ATTR_NAME): cv.string, + vol.Optional(ATTR_VIEW): cv.boolean, + vol.Optional(ATTR_ICON): cv.string, + vol.Optional(ATTR_CONTROL): CONTROL_TYPES, + vol.Optional(ATTR_VISIBLE): cv.boolean, + vol.Optional(ATTR_ALL): cv.boolean, + vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids, + vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids, + } + ), ) hass.services.async_register( - DOMAIN, SERVICE_REMOVE, groups_service_handler, schema=REMOVE_SERVICE_SCHEMA + DOMAIN, + SERVICE_REMOVE, + groups_service_handler, + schema=vol.Schema({vol.Required(ATTR_OBJECT_ID): cv.slug}), ) async def visibility_service_handler(service): @@ -344,7 +340,7 @@ async def visibility_service_handler(service): DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, - schema=SET_VISIBILITY_SERVICE_SCHEMA, + schema=make_entity_service_schema({vol.Required(ATTR_VISIBLE): cv.boolean}), ) return True diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 4c90441e7f0152..78ae15eb5372b4 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -11,7 +11,7 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util.async_ import run_callback_threadsafe @@ -124,7 +124,7 @@ async def async_scan_service(service): await asyncio.wait(update_tasks) hass.services.async_register( - DOMAIN, SERVICE_SCAN, async_scan_service, schema=ENTITY_SERVICE_SCHEMA + DOMAIN, SERVICE_SCAN, async_scan_service, schema=make_entity_service_schema({}) ) return True diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 6027b0b3da1a04..4a32ad16797319 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -13,7 +13,6 @@ ) from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -68,17 +67,11 @@ async def async_setup(hass, config): if not entities: return False - component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" - ) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") - component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" - ) + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" - ) + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") await component.async_add_entities(entities) return True diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index c366be90b14f14..36180ed2bad18a 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -6,7 +6,6 @@ from homeassistant.const import ATTR_DATE, ATTR_TIME, CONF_ICON, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import dt as dt_util @@ -27,14 +26,6 @@ SERVICE_SET_DATETIME = "set_datetime" -SERVICE_SET_DATETIME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Optional(ATTR_DATE): cv.date, - vol.Optional(ATTR_TIME): cv.time, - vol.Optional(ATTR_DATETIME): cv.datetime, - } -) - def has_date_or_time(conf): """Check at least date or time is true.""" @@ -108,7 +99,13 @@ async def async_set_datetime_service(entity, call): entity.async_set_datetime(date, time) component.async_register_entity_service( - SERVICE_SET_DATETIME, SERVICE_SET_DATETIME_SCHEMA, async_set_datetime_service + SERVICE_SET_DATETIME, + { + vol.Optional(ATTR_DATE): cv.date, + vol.Optional(ATTR_TIME): cv.time, + vol.Optional(ATTR_DATETIME): cv.datetime, + }, + async_set_datetime_service, ) await component.async_add_entities(entities) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 9b4d5a961ba3a0..77625ffa7f8e2d 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -4,7 +4,6 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, ATTR_MODE, @@ -38,10 +37,6 @@ SERVICE_INCREMENT = "increment" SERVICE_DECREMENT = "decrement" -SERVICE_SET_VALUE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_VALUE): vol.Coerce(float)} -) - def _cv_input_number(cfg): """Configure validation helper for input number (voluptuous).""" @@ -110,16 +105,14 @@ async def async_setup(hass, config): return False component.async_register_entity_service( - SERVICE_SET_VALUE, SERVICE_SET_VALUE_SCHEMA, "async_set_value" + SERVICE_SET_VALUE, + {vol.Required(ATTR_VALUE): vol.Coerce(float)}, + "async_set_value", ) - component.async_register_entity_service( - SERVICE_INCREMENT, ENTITY_SERVICE_SCHEMA, "async_increment" - ) + component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") - component.async_register_entity_service( - SERVICE_DECREMENT, ENTITY_SERVICE_SCHEMA, "async_decrement" - ) + component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") await component.async_add_entities(entities) return True diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 8cb3001c52e3e2..ae609e0927153b 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -5,7 +5,6 @@ from homeassistant.const import CONF_ICON, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -22,9 +21,6 @@ SERVICE_SELECT_OPTION = "select_option" -SERVICE_SELECT_OPTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_OPTION): cv.string} -) SERVICE_SELECT_NEXT = "select_next" @@ -32,14 +28,6 @@ SERVICE_SET_OPTIONS = "set_options" -SERVICE_SET_OPTIONS_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_OPTIONS): vol.All( - cv.ensure_list, vol.Length(min=1), [cv.string] - ) - } -) - def _cv_input_select(cfg): """Configure validation helper for input select (voluptuous).""" @@ -92,23 +80,27 @@ async def async_setup(hass, config): return False component.async_register_entity_service( - SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION_SCHEMA, "async_select_option" + SERVICE_SELECT_OPTION, + {vol.Required(ATTR_OPTION): cv.string}, + "async_select_option", ) component.async_register_entity_service( - SERVICE_SELECT_NEXT, - ENTITY_SERVICE_SCHEMA, - lambda entity, call: entity.async_offset_index(1), + SERVICE_SELECT_NEXT, {}, lambda entity, call: entity.async_offset_index(1), ) component.async_register_entity_service( - SERVICE_SELECT_PREVIOUS, - ENTITY_SERVICE_SCHEMA, - lambda entity, call: entity.async_offset_index(-1), + SERVICE_SELECT_PREVIOUS, {}, lambda entity, call: entity.async_offset_index(-1), ) component.async_register_entity_service( - SERVICE_SET_OPTIONS, SERVICE_SET_OPTIONS_SCHEMA, "async_set_options" + SERVICE_SET_OPTIONS, + { + vol.Required(ATTR_OPTIONS): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ) + }, + "async_set_options", ) await component.async_add_entities(entities) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 1b4670cf1e6443..d43e47c11ca730 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -4,7 +4,6 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, ATTR_MODE, @@ -34,10 +33,6 @@ SERVICE_SET_VALUE = "set_value" -SERVICE_SET_VALUE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_VALUE): cv.string} -) - def _cv_input_text(cfg): """Configure validation helper for input box (voluptuous).""" @@ -111,7 +106,7 @@ async def async_setup(hass, config): return False component.async_register_entity_service( - SERVICE_SET_VALUE, SERVICE_SET_VALUE_SCHEMA, "async_set_value" + SERVICE_SET_VALUE, {vol.Required(ATTR_VALUE): cv.string}, "async_set_value" ) await component.async_add_entities(entities) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 7b33e0c1d86f02..1fb614f856fb5f 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -66,8 +66,9 @@ ATTR_ZONES = "zones" ATTR_POWER = "power" -LIFX_SET_STATE_SCHEMA = LIGHT_TURN_ON_SCHEMA.extend( +LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( { + **LIGHT_TURN_ON_SCHEMA, ATTR_INFRARED: vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)), ATTR_ZONES: vol.All(cv.ensure_list, [cv.positive_int]), ATTR_POWER: cv.boolean, @@ -369,16 +370,11 @@ async def start_effect(self, entities, service, **kwargs): async def async_service_to_entities(self, service): """Return the known entities that a service call mentions.""" entity_ids = await async_extract_entity_ids(self.hass, service) - if entity_ids: - entities = [ - entity - for entity in self.entities.values() - if entity.entity_id in entity_ids - ] - else: - entities = list(self.entities.values()) - - return entities + return [ + entity + for entity in self.entities.values() + if entity.entity_id in entity_ids + ] @callback def register(self, bulb): diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 7bc2306d4180f1..f01258e2ab477e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, - ENTITY_SERVICE_SCHEMA, + make_entity_service_schema, ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent @@ -93,49 +93,37 @@ VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) -LIGHT_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, - ATTR_TRANSITION: VALID_TRANSITION, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, - vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) - ), - vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) - ), - vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence( - ( - vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), - ) - ), - vol.Coerce(tuple), - ), - vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): vol.All( - vol.Coerce(int), vol.Range(min=0) - ), - ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), - ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), - ATTR_EFFECT: cv.string, - } -) - - -LIGHT_TURN_OFF_SCHEMA = { +LIGHT_TURN_ON_SCHEMA = { + vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, ATTR_TRANSITION: VALID_TRANSITION, + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, + vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + ), + vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) + ), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + ) + ), + vol.Coerce(tuple), + ), + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): vol.All(vol.Coerce(int), vol.Range(min=0)), + ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), + ATTR_EFFECT: cv.string, } -LIGHT_TOGGLE_SCHEMA = LIGHT_TURN_ON_SCHEMA - PROFILE_SCHEMA = vol.Schema( vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte)) ) @@ -268,15 +256,20 @@ async def async_handle_light_on_service(service): DOMAIN, SERVICE_TURN_ON, async_handle_light_on_service, - schema=LIGHT_TURN_ON_SCHEMA, + schema=cv.make_entity_service_schema(LIGHT_TURN_ON_SCHEMA), ) component.async_register_entity_service( - SERVICE_TURN_OFF, LIGHT_TURN_OFF_SCHEMA, "async_turn_off" + SERVICE_TURN_OFF, + { + ATTR_TRANSITION: VALID_TRANSITION, + ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), + }, + "async_turn_off", ) component.async_register_entity_service( - SERVICE_TOGGLE, LIGHT_TOGGLE_SCHEMA, "async_toggle" + SERVICE_TOGGLE, LIGHT_TURN_ON_SCHEMA, "async_toggle" ) return True diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 341df1bb28a6f7..c443a5219d7dd2 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -9,7 +9,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, + make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -40,7 +40,7 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -LOCK_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({vol.Optional(ATTR_CODE): cv.string}) +LOCK_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) # Bitfield of features supported by the lock entity SUPPORT_OPEN = 1 diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 97cb7d9978a037..7dc05368dcde60 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -2,6 +2,8 @@ import logging import voluptuous as vol +from youtube_dl import YoutubeDL +from youtube_dl.utils import DownloadError, ExtractorError from homeassistant.components.media_player import MEDIA_PLAYER_PLAY_MEDIA_SCHEMA from homeassistant.components.media_player.const import ( @@ -44,7 +46,10 @@ def play_media(call): MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send() hass.services.register( - DOMAIN, SERVICE_PLAY_MEDIA, play_media, schema=MEDIA_PLAYER_PLAY_MEDIA_SCHEMA + DOMAIN, + SERVICE_PLAY_MEDIA, + play_media, + schema=cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA), ) return True @@ -98,9 +103,6 @@ def extract_and_send(self): def get_stream_selector(self): """Return format selector for the media URL.""" - from youtube_dl import YoutubeDL - from youtube_dl.utils import DownloadError, ExtractorError - ydl = YoutubeDL({"quiet": True, "logger": _LOGGER}) try: diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index c783772365bf28..a6cd4dda4ead8e 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -40,7 +40,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -123,42 +122,12 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) -# Service call validation schemas -MEDIA_PLAYER_SET_VOLUME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float} -) - -MEDIA_PLAYER_MUTE_VOLUME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean} -) - -MEDIA_PLAYER_MEDIA_SEEK_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_MEDIA_SEEK_POSITION): vol.All( - vol.Coerce(float), vol.Range(min=0) - ) - } -) - -MEDIA_PLAYER_SELECT_SOURCE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_INPUT_SOURCE): cv.string} -) -MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_SOUND_MODE): cv.string} -) - -MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, - vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, - } -) - -MEDIA_PLAYER_SET_SHUFFLE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MEDIA_SHUFFLE): cv.boolean} -) +MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { + vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, + vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, + vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, +} ATTR_TO_PROPERTY = [ ATTR_MEDIA_VOLUME_LEVEL, @@ -223,65 +192,56 @@ async def async_setup(hass, config): await component.async_setup(config) component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on", [SUPPORT_TURN_ON] + SERVICE_TURN_ON, {}, "async_turn_on", [SUPPORT_TURN_ON] ) component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off", [SUPPORT_TURN_OFF] + SERVICE_TURN_OFF, {}, "async_turn_off", [SUPPORT_TURN_OFF] ) component.async_register_entity_service( - SERVICE_TOGGLE, - ENTITY_SERVICE_SCHEMA, - "async_toggle", - [SUPPORT_TURN_OFF | SUPPORT_TURN_ON], + SERVICE_TOGGLE, {}, "async_toggle", [SUPPORT_TURN_OFF | SUPPORT_TURN_ON], ) component.async_register_entity_service( SERVICE_VOLUME_UP, - ENTITY_SERVICE_SCHEMA, + {}, "async_volume_up", [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP], ) component.async_register_entity_service( SERVICE_VOLUME_DOWN, - ENTITY_SERVICE_SCHEMA, + {}, "async_volume_down", [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP], ) component.async_register_entity_service( SERVICE_MEDIA_PLAY_PAUSE, - ENTITY_SERVICE_SCHEMA, + {}, "async_media_play_pause", [SUPPORT_PLAY | SUPPORT_PAUSE], ) component.async_register_entity_service( - SERVICE_MEDIA_PLAY, ENTITY_SERVICE_SCHEMA, "async_media_play", [SUPPORT_PLAY] + SERVICE_MEDIA_PLAY, {}, "async_media_play", [SUPPORT_PLAY] ) component.async_register_entity_service( - SERVICE_MEDIA_PAUSE, ENTITY_SERVICE_SCHEMA, "async_media_pause", [SUPPORT_PAUSE] + SERVICE_MEDIA_PAUSE, {}, "async_media_pause", [SUPPORT_PAUSE] ) component.async_register_entity_service( - SERVICE_MEDIA_STOP, ENTITY_SERVICE_SCHEMA, "async_media_stop", [SUPPORT_STOP] + SERVICE_MEDIA_STOP, {}, "async_media_stop", [SUPPORT_STOP] ) component.async_register_entity_service( - SERVICE_MEDIA_NEXT_TRACK, - ENTITY_SERVICE_SCHEMA, - "async_media_next_track", - [SUPPORT_NEXT_TRACK], + SERVICE_MEDIA_NEXT_TRACK, {}, "async_media_next_track", [SUPPORT_NEXT_TRACK], ) component.async_register_entity_service( SERVICE_MEDIA_PREVIOUS_TRACK, - ENTITY_SERVICE_SCHEMA, + {}, "async_media_previous_track", [SUPPORT_PREVIOUS_TRACK], ) component.async_register_entity_service( - SERVICE_CLEAR_PLAYLIST, - ENTITY_SERVICE_SCHEMA, - "async_clear_playlist", - [SUPPORT_CLEAR_PLAYLIST], + SERVICE_CLEAR_PLAYLIST, {}, "async_clear_playlist", [SUPPORT_CLEAR_PLAYLIST], ) component.async_register_entity_service( SERVICE_VOLUME_SET, - MEDIA_PLAYER_SET_VOLUME_SCHEMA, + {vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float}, lambda entity, call: entity.async_set_volume_level( volume=call.data[ATTR_MEDIA_VOLUME_LEVEL] ), @@ -289,7 +249,7 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_VOLUME_MUTE, - MEDIA_PLAYER_MUTE_VOLUME_SCHEMA, + {vol.Required(ATTR_MEDIA_VOLUME_MUTED): cv.boolean}, lambda entity, call: entity.async_mute_volume( mute=call.data[ATTR_MEDIA_VOLUME_MUTED] ), @@ -297,7 +257,11 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_MEDIA_SEEK, - MEDIA_PLAYER_MEDIA_SEEK_SCHEMA, + { + vol.Required(ATTR_MEDIA_SEEK_POSITION): vol.All( + vol.Coerce(float), vol.Range(min=0) + ) + }, lambda entity, call: entity.async_media_seek( position=call.data[ATTR_MEDIA_SEEK_POSITION] ), @@ -305,13 +269,13 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_SELECT_SOURCE, - MEDIA_PLAYER_SELECT_SOURCE_SCHEMA, + {vol.Required(ATTR_INPUT_SOURCE): cv.string}, "async_select_source", [SUPPORT_SELECT_SOURCE], ) component.async_register_entity_service( SERVICE_SELECT_SOUND_MODE, - MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA, + {vol.Required(ATTR_SOUND_MODE): cv.string}, "async_select_sound_mode", [SUPPORT_SELECT_SOUND_MODE], ) @@ -327,7 +291,7 @@ async def async_setup(hass, config): ) component.async_register_entity_service( SERVICE_SHUFFLE_SET, - MEDIA_PLAYER_SET_SHUFFLE_SCHEMA, + {vol.Required(ATTR_MEDIA_SHUFFLE): cv.boolean}, "async_set_shuffle", [SUPPORT_SHUFFLE_SET], ) diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 71059b98f3567c..af653165ee387d 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -17,7 +17,7 @@ ) from homeassistant.components import group from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, + make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -56,29 +56,10 @@ SUPPORT_LEARN_COMMAND = 1 -REMOTE_SERVICE_ACTIVITY_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_ACTIVITY): cv.string} ) -REMOTE_SERVICE_SEND_COMMAND_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_DEVICE): cv.string, - vol.Optional(ATTR_NUM_REPEATS, default=DEFAULT_NUM_REPEATS): cv.positive_int, - vol.Optional(ATTR_DELAY_SECS): vol.Coerce(float), - vol.Optional(ATTR_HOLD_SECS, default=DEFAULT_HOLD_SECS): vol.Coerce(float), - } -) - -REMOTE_SERVICE_LEARN_COMMAND_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Optional(ATTR_DEVICE): cv.string, - vol.Optional(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ALTERNATIVE): cv.boolean, - vol.Optional(ATTR_TIMEOUT): cv.positive_int, - } -) - @bind_hass def is_on(hass, entity_id=None): @@ -107,12 +88,27 @@ async def async_setup(hass, config): ) component.async_register_entity_service( - SERVICE_SEND_COMMAND, REMOTE_SERVICE_SEND_COMMAND_SCHEMA, "async_send_command" + SERVICE_SEND_COMMAND, + { + vol.Required(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_DEVICE): cv.string, + vol.Optional( + ATTR_NUM_REPEATS, default=DEFAULT_NUM_REPEATS + ): cv.positive_int, + vol.Optional(ATTR_DELAY_SECS): vol.Coerce(float), + vol.Optional(ATTR_HOLD_SECS, default=DEFAULT_HOLD_SECS): vol.Coerce(float), + }, + "async_send_command", ) component.async_register_entity_service( SERVICE_LEARN_COMMAND, - REMOTE_SERVICE_LEARN_COMMAND_SCHEMA, + { + vol.Optional(ATTR_DEVICE): cv.string, + vol.Optional(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ALTERNATIVE): cv.boolean, + vol.Optional(ATTR_TIMEOUT): cv.positive_int, + }, "async_learn_command", ) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 9659cc625c01bd..cb9cb5194ba193 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -19,7 +19,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.config_validation import make_entity_service_schema from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.script import Script @@ -60,7 +60,7 @@ ) SCRIPT_SERVICE_SCHEMA = vol.Schema(dict) -SCRIPT_TURN_ONOFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SCRIPT_TURN_ONOFF_SCHEMA = make_entity_service_schema( {vol.Optional(ATTR_VARIABLES): dict} ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index c6cc0a8c3c960e..de60752eada9ee 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -6,7 +6,6 @@ from homeassistant.const import CONF_ICON, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity @@ -37,10 +36,6 @@ SERVICE_CANCEL = "cancel" SERVICE_FINISH = "finish" -SERVICE_SCHEMA_DURATION = ENTITY_SERVICE_SCHEMA.extend( - {vol.Optional(ATTR_DURATION, default=timedelta(DEFAULT_DURATION)): cv.time_period} -) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( @@ -80,17 +75,17 @@ async def async_setup(hass, config): return False component.async_register_entity_service( - SERVICE_START, SERVICE_SCHEMA_DURATION, "async_start" - ) - component.async_register_entity_service( - SERVICE_PAUSE, ENTITY_SERVICE_SCHEMA, "async_pause" - ) - component.async_register_entity_service( - SERVICE_CANCEL, ENTITY_SERVICE_SCHEMA, "async_cancel" - ) - component.async_register_entity_service( - SERVICE_FINISH, ENTITY_SERVICE_SCHEMA, "async_finish" + SERVICE_START, + { + vol.Optional( + ATTR_DURATION, default=timedelta(DEFAULT_DURATION) + ): cv.time_period + }, + "async_start", ) + component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause") + component.async_register_entity_service(SERVICE_CANCEL, {}, "async_cancel") + component.async_register_entity_service(SERVICE_FINISH, {}, "async_finish") await component.async_add_entities(entities) return True diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 17eacc326d3b58..04e472a7828619 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -7,7 +7,6 @@ from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -39,11 +38,8 @@ DEFAULT_OFFSET = timedelta(hours=0) -SERVICE_SELECT_TARIFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_TARIFF): cv.string} -) -METER_CONFIG_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +METER_CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_SOURCE_SENSOR): cv.entity_id, vol.Optional(CONF_NAME): cv.string, @@ -110,16 +106,16 @@ async def async_setup(hass, config): register_services = True if register_services: - component.async_register_entity_service( - SERVICE_RESET, ENTITY_SERVICE_SCHEMA, "async_reset_meters" - ) + component.async_register_entity_service(SERVICE_RESET, {}, "async_reset_meters") component.async_register_entity_service( - SERVICE_SELECT_TARIFF, SERVICE_SELECT_TARIFF_SCHEMA, "async_select_tariff" + SERVICE_SELECT_TARIFF, + {vol.Required(ATTR_TARIFF): cv.string}, + "async_select_tariff", ) component.async_register_entity_service( - SERVICE_SELECT_NEXT_TARIFF, ENTITY_SERVICE_SCHEMA, "async_next_tariff" + SERVICE_SELECT_NEXT_TARIFF, {}, "async_next_tariff" ) return True diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index c1f1131a52f611..85b3d665e170c0 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -19,7 +19,7 @@ from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - ENTITY_SERVICE_SCHEMA, + make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -55,16 +55,6 @@ SERVICE_PAUSE = "pause" SERVICE_STOP = "stop" -VACUUM_SET_FAN_SPEED_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_FAN_SPEED): cv.string} -) - -VACUUM_SEND_COMMAND_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - { - vol.Required(ATTR_COMMAND): cv.string, - vol.Optional(ATTR_PARAMS): vol.Any(dict, cv.ensure_list), - } -) STATE_CLEANING = "cleaning" STATE_DOCKED = "docked" @@ -106,43 +96,32 @@ async def async_setup(hass, config): await component.async_setup(config) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" - ) - component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" - ) - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" - ) - component.async_register_entity_service( - SERVICE_START_PAUSE, ENTITY_SERVICE_SCHEMA, "async_start_pause" - ) - component.async_register_entity_service( - SERVICE_START, ENTITY_SERVICE_SCHEMA, "async_start" - ) - component.async_register_entity_service( - SERVICE_PAUSE, ENTITY_SERVICE_SCHEMA, "async_pause" - ) - component.async_register_entity_service( - SERVICE_RETURN_TO_BASE, ENTITY_SERVICE_SCHEMA, "async_return_to_base" - ) - component.async_register_entity_service( - SERVICE_CLEAN_SPOT, ENTITY_SERVICE_SCHEMA, "async_clean_spot" - ) - component.async_register_entity_service( - SERVICE_LOCATE, ENTITY_SERVICE_SCHEMA, "async_locate" + SERVICE_START_PAUSE, {}, "async_start_pause" ) + component.async_register_entity_service(SERVICE_START, {}, "async_start") + component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause") component.async_register_entity_service( - SERVICE_STOP, ENTITY_SERVICE_SCHEMA, "async_stop" + SERVICE_RETURN_TO_BASE, {}, "async_return_to_base" ) + component.async_register_entity_service(SERVICE_CLEAN_SPOT, {}, "async_clean_spot") + component.async_register_entity_service(SERVICE_LOCATE, {}, "async_locate") + component.async_register_entity_service(SERVICE_STOP, {}, "async_stop") component.async_register_entity_service( SERVICE_SET_FAN_SPEED, - VACUUM_SET_FAN_SPEED_SERVICE_SCHEMA, + {vol.Required(ATTR_FAN_SPEED): cv.string}, "async_set_fan_speed", ) component.async_register_entity_service( - SERVICE_SEND_COMMAND, VACUUM_SEND_COMMAND_SERVICE_SCHEMA, "async_send_command" + SERVICE_SEND_COMMAND, + { + vol.Required(ATTR_COMMAND): cv.string, + vol.Optional(ATTR_PARAMS): vol.Any(dict, cv.ensure_list), + }, + "async_send_command", ) return True diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index e2eb98938bb73c..3bda8b314f2fdd 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import track_time_interval @@ -131,11 +131,11 @@ extra=vol.ALLOW_EXTRA, ) -RENAME_DEVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +RENAME_DEVICE_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_NAME): cv.string}, extra=vol.ALLOW_EXTRA ) -DELETE_DEVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +DELETE_DEVICE_SCHEMA = make_entity_service_schema({}, extra=vol.ALLOW_EXTRA) SET_PAIRING_MODE_SCHEMA = vol.Schema( { @@ -146,31 +146,31 @@ extra=vol.ALLOW_EXTRA, ) -SET_VOLUME_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_VOLUME_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_VOLUME): vol.In(VOLUMES)} ) -SET_SIREN_TONE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_SIREN_TONE_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_TONE): vol.In(TONES)} ) -SET_CHIME_MODE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_CHIME_MODE_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_TONE): vol.In(CHIME_TONES)} ) -SET_AUTO_SHUTOFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_AUTO_SHUTOFF_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_AUTO_SHUTOFF): vol.In(AUTO_SHUTOFF_TIMES)} ) -SET_STROBE_ENABLED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +SET_STROBE_ENABLED_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_ENABLED): cv.boolean} ) -ENABLED_SIREN_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +ENABLED_SIREN_SCHEMA = make_entity_service_schema( {vol.Required(ATTR_ENABLED): cv.boolean} ) -DIAL_CONFIG_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +DIAL_CONFIG_SCHEMA = make_entity_service_schema( { vol.Optional(ATTR_MIN_VALUE): vol.Coerce(int), vol.Optional(ATTR_MAX_VALUE): vol.Coerce(int), @@ -182,7 +182,7 @@ } ) -DIAL_STATE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( +DIAL_STATE_SCHEMA = make_entity_service_schema( { vol.Required(ATTR_VALUE): vol.Coerce(int), vol.Optional(ATTR_LABELS): cv.ensure_list(cv.string), diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7ca5a7e86f923b..c34b104e0f765c 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -715,12 +715,23 @@ def custom_serializer(schema): PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) -ENTITY_SERVICE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, - vol.Optional(ATTR_AREA_ID): vol.All(ensure_list, [str]), - } -) + +def make_entity_service_schema( + schema: dict, *, extra: int = vol.PREVENT_EXTRA +) -> vol.All: + """Create an entity service schema.""" + return vol.All( + vol.Schema( + { + **schema, + vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, + vol.Optional(ATTR_AREA_ID): vol.All(ensure_list, [str]), + }, + extra=extra, + ), + has_at_least_one_key(ATTR_ENTITY_ID, ATTR_AREA_ID), + ) + EVENT_SCHEMA = vol.Schema( { diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 42b19da889ece1..63d1b21fc9aa81 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -15,7 +15,7 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.loader import bind_hass, async_get_integration from homeassistant.util import slugify @@ -173,24 +173,16 @@ async def async_unload_entry(self, config_entry): async def async_extract_from_service(self, service, expand_group=True): """Extract all known and available entities from a service call. - Will return all entities if no entities specified in call. Will return an empty list if entities specified but unknown. This method must be run in the event loop. """ data_ent_id = service.data.get(ATTR_ENTITY_ID) - if data_ent_id in (None, ENTITY_MATCH_ALL): - if data_ent_id is None: - self.logger.warning( - "Not passing an entity ID to a service to target all " - "entities is deprecated. Update your call to %s.%s to be " - "instead: entity_id: %s", - service.domain, - service.service, - ENTITY_MATCH_ALL, - ) + if data_ent_id is None: + return [] + if data_ent_id == ENTITY_MATCH_ALL: return [entity for entity in self.entities if entity.available] entity_ids = await async_extract_entity_ids(self.hass, service, expand_group) @@ -204,7 +196,7 @@ async def async_extract_from_service(self, service, expand_group=True): def async_register_entity_service(self, name, schema, func, required_features=None): """Register an entity service.""" if isinstance(schema, dict): - schema = ENTITY_SERVICE_SCHEMA.extend(schema) + schema = make_entity_service_schema(schema) async def handle_service(call): """Handle the service.""" diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index e177c86c65c971..45393dc04860ce 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -260,19 +260,7 @@ async def entity_service_call( else: entity_perms = None - # Are we trying to target all entities - if ATTR_ENTITY_ID in call.data: - target_all_entities = call.data[ATTR_ENTITY_ID] == ENTITY_MATCH_ALL - else: - # Remove the service_name parameter along with this warning - _LOGGER.warning( - "Not passing an entity ID to a service to target all " - "entities is deprecated. Update your call to %s to be " - "instead: entity_id: %s", - service_name, - ENTITY_MATCH_ALL, - ) - target_all_entities = True + target_all_entities = call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL if not target_all_entities: # A set of entities we're trying to target. diff --git a/tests/components/alarm_control_panel/common.py b/tests/components/alarm_control_panel/common.py index 216f226ef14b9c..d06939dce9b322 100644 --- a/tests/components/alarm_control_panel/common.py +++ b/tests/components/alarm_control_panel/common.py @@ -13,11 +13,12 @@ SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass -async def async_alarm_disarm(hass, code=None, entity_id=None): +async def async_alarm_disarm(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -29,7 +30,7 @@ async def async_alarm_disarm(hass, code=None, entity_id=None): @bind_hass -def alarm_disarm(hass, code=None, entity_id=None): +def alarm_disarm(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -40,7 +41,7 @@ def alarm_disarm(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) -async def async_alarm_arm_home(hass, code=None, entity_id=None): +async def async_alarm_arm_home(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -52,7 +53,7 @@ async def async_alarm_arm_home(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_home(hass, code=None, entity_id=None): +def alarm_arm_home(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm home.""" data = {} if code: @@ -63,7 +64,7 @@ def alarm_arm_home(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) -async def async_alarm_arm_away(hass, code=None, entity_id=None): +async def async_alarm_arm_away(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -75,7 +76,7 @@ async def async_alarm_arm_away(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_away(hass, code=None, entity_id=None): +def alarm_arm_away(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm away.""" data = {} if code: @@ -86,7 +87,7 @@ def alarm_arm_away(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) -async def async_alarm_arm_night(hass, code=None, entity_id=None): +async def async_alarm_arm_night(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -98,7 +99,7 @@ async def async_alarm_arm_night(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_night(hass, code=None, entity_id=None): +def alarm_arm_night(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm night.""" data = {} if code: @@ -109,7 +110,7 @@ def alarm_arm_night(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data) -async def async_alarm_trigger(hass, code=None, entity_id=None): +async def async_alarm_trigger(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -121,7 +122,7 @@ async def async_alarm_trigger(hass, code=None, entity_id=None): @bind_hass -def alarm_trigger(hass, code=None, entity_id=None): +def alarm_trigger(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for trigger.""" data = {} if code: @@ -132,7 +133,7 @@ def alarm_trigger(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) -async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=None): +async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for disarm.""" data = {} if code: @@ -146,7 +147,7 @@ async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=None): @bind_hass -def alarm_arm_custom_bypass(hass, code=None, entity_id=None): +def alarm_arm_custom_bypass(hass, code=None, entity_id=ENTITY_MATCH_ALL): """Send the alarm the command for arm custom bypass.""" data = {} if code: diff --git a/tests/components/automation/common.py b/tests/components/automation/common.py index 6fadbd14199d6c..c7aa8f1eced95c 100644 --- a/tests/components/automation/common.py +++ b/tests/components/automation/common.py @@ -10,33 +10,34 @@ SERVICE_TURN_OFF, SERVICE_TOGGLE, SERVICE_RELOAD, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass @bind_hass -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data) @bind_hass -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) @bind_hass -async def async_toggle(hass, entity_id=None): +async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data) @bind_hass -async def async_trigger(hass, entity_id=None): +async def async_trigger(hass, entity_id=ENTITY_MATCH_ALL): """Trigger specified automation or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TRIGGER, data) diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py index 9d5d2dcd6fd4af..971d723d2d5411 100644 --- a/tests/components/camera/common.py +++ b/tests/components/camera/common.py @@ -13,20 +13,25 @@ DATA_CAMERA_PREFS, PREF_PRELOAD_STREAM, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + ENTITY_MATCH_ALL, +) from homeassistant.core import callback from homeassistant.loader import bind_hass @bind_hass -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off camera.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data) @bind_hass -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on camera, and set operation mode.""" data = {} if entity_id is not None: @@ -36,7 +41,7 @@ async def async_turn_on(hass, entity_id=None): @bind_hass -def enable_motion_detection(hass, entity_id=None): +def enable_motion_detection(hass, entity_id=ENTITY_MATCH_ALL): """Enable Motion Detection.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_ENABLE_MOTION, data)) @@ -44,7 +49,7 @@ def enable_motion_detection(hass, entity_id=None): @bind_hass @callback -def async_snapshot(hass, filename, entity_id=None): +def async_snapshot(hass, filename, entity_id=ENTITY_MATCH_ALL): """Make a snapshot from a camera.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_FILENAME] = filename diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index 9f1ef8b084a636..a5ea182f2b61c2 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -27,11 +27,12 @@ ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass -async def async_set_preset_mode(hass, preset_mode, entity_id=None): +async def async_set_preset_mode(hass, preset_mode, entity_id=ENTITY_MATCH_ALL): """Set new preset mode.""" data = {ATTR_PRESET_MODE: preset_mode} @@ -42,7 +43,7 @@ async def async_set_preset_mode(hass, preset_mode, entity_id=None): @bind_hass -def set_preset_mode(hass, preset_mode, entity_id=None): +def set_preset_mode(hass, preset_mode, entity_id=ENTITY_MATCH_ALL): """Set new preset mode.""" data = {ATTR_PRESET_MODE: preset_mode} @@ -52,7 +53,7 @@ def set_preset_mode(hass, preset_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_PRESET_MODE, data) -async def async_set_aux_heat(hass, aux_heat, entity_id=None): +async def async_set_aux_heat(hass, aux_heat, entity_id=ENTITY_MATCH_ALL): """Turn all or specified climate devices auxiliary heater on.""" data = {ATTR_AUX_HEAT: aux_heat} @@ -63,7 +64,7 @@ async def async_set_aux_heat(hass, aux_heat, entity_id=None): @bind_hass -def set_aux_heat(hass, aux_heat, entity_id=None): +def set_aux_heat(hass, aux_heat, entity_id=ENTITY_MATCH_ALL): """Turn all or specified climate devices auxiliary heater on.""" data = {ATTR_AUX_HEAT: aux_heat} @@ -76,7 +77,7 @@ def set_aux_heat(hass, aux_heat, entity_id=None): async def async_set_temperature( hass, temperature=None, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, target_temp_high=None, target_temp_low=None, hvac_mode=None, @@ -103,7 +104,7 @@ async def async_set_temperature( def set_temperature( hass, temperature=None, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, target_temp_high=None, target_temp_low=None, hvac_mode=None, @@ -124,7 +125,7 @@ def set_temperature( hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs) -async def async_set_humidity(hass, humidity, entity_id=None): +async def async_set_humidity(hass, humidity, entity_id=ENTITY_MATCH_ALL): """Set new target humidity.""" data = {ATTR_HUMIDITY: humidity} @@ -135,7 +136,7 @@ async def async_set_humidity(hass, humidity, entity_id=None): @bind_hass -def set_humidity(hass, humidity, entity_id=None): +def set_humidity(hass, humidity, entity_id=ENTITY_MATCH_ALL): """Set new target humidity.""" data = {ATTR_HUMIDITY: humidity} @@ -145,7 +146,7 @@ def set_humidity(hass, humidity, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data) -async def async_set_fan_mode(hass, fan, entity_id=None): +async def async_set_fan_mode(hass, fan, entity_id=ENTITY_MATCH_ALL): """Set all or specified climate devices fan mode on.""" data = {ATTR_FAN_MODE: fan} @@ -156,7 +157,7 @@ async def async_set_fan_mode(hass, fan, entity_id=None): @bind_hass -def set_fan_mode(hass, fan, entity_id=None): +def set_fan_mode(hass, fan, entity_id=ENTITY_MATCH_ALL): """Set all or specified climate devices fan mode on.""" data = {ATTR_FAN_MODE: fan} @@ -166,7 +167,7 @@ def set_fan_mode(hass, fan, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) -async def async_set_hvac_mode(hass, hvac_mode, entity_id=None): +async def async_set_hvac_mode(hass, hvac_mode, entity_id=ENTITY_MATCH_ALL): """Set new target operation mode.""" data = {ATTR_HVAC_MODE: hvac_mode} @@ -177,7 +178,7 @@ async def async_set_hvac_mode(hass, hvac_mode, entity_id=None): @bind_hass -def set_operation_mode(hass, hvac_mode, entity_id=None): +def set_operation_mode(hass, hvac_mode, entity_id=ENTITY_MATCH_ALL): """Set new target operation mode.""" data = {ATTR_HVAC_MODE: hvac_mode} @@ -187,7 +188,7 @@ def set_operation_mode(hass, hvac_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_HVAC_MODE, data) -async def async_set_swing_mode(hass, swing_mode, entity_id=None): +async def async_set_swing_mode(hass, swing_mode, entity_id=ENTITY_MATCH_ALL): """Set new target swing mode.""" data = {ATTR_SWING_MODE: swing_mode} @@ -198,7 +199,7 @@ async def async_set_swing_mode(hass, swing_mode, entity_id=None): @bind_hass -def set_swing_mode(hass, swing_mode, entity_id=None): +def set_swing_mode(hass, swing_mode, entity_id=ENTITY_MATCH_ALL): """Set new target swing mode.""" data = {ATTR_SWING_MODE: swing_mode} @@ -208,7 +209,7 @@ def set_swing_mode(hass, swing_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on device.""" data = {} @@ -218,7 +219,7 @@ async def async_turn_on(hass, entity_id=None): await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off device.""" data = {} diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index 10b407688af424..34b340b600acba 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -62,10 +62,14 @@ async def test_turn_off(hass): async def test_turn_off_without_entity_id(hass): """Test light turn off all lights.""" - await hass.services.async_call("light", "turn_on", {}, blocking=True) + await hass.services.async_call( + "light", "turn_on", {"entity_id": "all"}, blocking=True + ) assert light.is_on(hass, ENTITY_LIGHT) - await hass.services.async_call("light", "turn_off", {}, blocking=True) + await hass.services.async_call( + "light", "turn_off", {"entity_id": "all"}, blocking=True + ) assert not light.is_on(hass, ENTITY_LIGHT) diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index 24a6868a372fca..de645dac42e6d7 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -12,10 +12,15 @@ SERVICE_SET_DIRECTION, SERVICE_SET_SPEED, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_ON, + SERVICE_TURN_OFF, + ENTITY_MATCH_ALL, +) -async def async_turn_on(hass, entity_id: str = None, speed: str = None) -> None: +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) -> None: """Turn all or specified fan on.""" data = { key: value @@ -26,7 +31,7 @@ async def async_turn_on(hass, entity_id: str = None, speed: str = None) -> None: await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) -async def async_turn_off(hass, entity_id: str = None) -> None: +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: """Turn all or specified fan off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} @@ -34,7 +39,7 @@ async def async_turn_off(hass, entity_id: str = None) -> None: async def async_oscillate( - hass, entity_id: str = None, should_oscillate: bool = True + hass, entity_id=ENTITY_MATCH_ALL, should_oscillate: bool = True ) -> None: """Set oscillation on all or specified fan.""" data = { @@ -49,7 +54,7 @@ async def async_oscillate( await hass.services.async_call(DOMAIN, SERVICE_OSCILLATE, data, blocking=True) -async def async_set_speed(hass, entity_id: str = None, speed: str = None) -> None: +async def async_set_speed(hass, entity_id=ENTITY_MATCH_ALL, speed: str = None) -> None: """Set speed for all or specified fan.""" data = { key: value @@ -61,7 +66,7 @@ async def async_set_speed(hass, entity_id: str = None, speed: str = None) -> Non async def async_set_direction( - hass, entity_id: str = None, direction: str = None + hass, entity_id=ENTITY_MATCH_ALL, direction: str = None ) -> None: """Set direction for all or specified fan.""" data = { diff --git a/tests/components/image_processing/common.py b/tests/components/image_processing/common.py index b767884503dced..8522353d3f29a2 100644 --- a/tests/components/image_processing/common.py +++ b/tests/components/image_processing/common.py @@ -4,20 +4,20 @@ components. Instead call the service directly. """ from homeassistant.components.image_processing import DOMAIN, SERVICE_SCAN -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL from homeassistant.core import callback from homeassistant.loader import bind_hass @bind_hass -def scan(hass, entity_id=None): +def scan(hass, entity_id=ENTITY_MATCH_ALL): """Force process of all cameras or given entity.""" hass.add_job(async_scan, hass, entity_id) @callback @bind_hass -def async_scan(hass, entity_id=None): +def async_scan(hass, entity_id=ENTITY_MATCH_ALL): """Force process of all cameras or given entity.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_SCAN, data)) diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 416c02884dcc77..32678bf4daa0da 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -24,6 +24,7 @@ SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass @@ -31,7 +32,7 @@ @bind_hass def turn_on( hass, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, transition=None, brightness=None, brightness_pct=None, @@ -69,7 +70,7 @@ def turn_on( async def async_turn_on( hass, - entity_id=None, + entity_id=ENTITY_MATCH_ALL, transition=None, brightness=None, brightness_pct=None, @@ -110,12 +111,12 @@ async def async_turn_on( @bind_hass -def turn_off(hass, entity_id=None, transition=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Turn all or specified light off.""" hass.add_job(async_turn_off, hass, entity_id, transition) -async def async_turn_off(hass, entity_id=None, transition=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Turn all or specified light off.""" data = { key: value @@ -127,12 +128,12 @@ async def async_turn_off(hass, entity_id=None, transition=None): @bind_hass -def toggle(hass, entity_id=None, transition=None): +def toggle(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Toggle all or specified light.""" hass.add_job(async_toggle, hass, entity_id, transition) -async def async_toggle(hass, entity_id=None, transition=None): +async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL, transition=None): """Toggle all or specified light.""" data = { key: value diff --git a/tests/components/lock/common.py b/tests/components/lock/common.py index 2ad8f075bce5d5..a658befca55d5f 100644 --- a/tests/components/lock/common.py +++ b/tests/components/lock/common.py @@ -10,12 +10,13 @@ SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass @bind_hass -def lock(hass, entity_id=None, code=None): +def lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: @@ -26,7 +27,7 @@ def lock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_LOCK, data) -async def async_lock(hass, entity_id=None, code=None): +async def async_lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: @@ -38,7 +39,7 @@ async def async_lock(hass, entity_id=None, code=None): @bind_hass -def unlock(hass, entity_id=None, code=None): +def unlock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Unlock all or specified locks.""" data = {} if code: @@ -49,7 +50,7 @@ def unlock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_UNLOCK, data) -async def async_unlock(hass, entity_id=None, code=None): +async def async_unlock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: @@ -61,7 +62,7 @@ async def async_unlock(hass, entity_id=None, code=None): @bind_hass -def open_lock(hass, entity_id=None, code=None): +def open_lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Open all or specified locks.""" data = {} if code: @@ -72,7 +73,7 @@ def open_lock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_OPEN, data) -async def async_open_lock(hass, entity_id=None, code=None): +async def async_open_lock(hass, entity_id=ENTITY_MATCH_ALL, code=None): """Lock all or specified locks.""" data = {} if code: diff --git a/tests/components/media_player/common.py b/tests/components/media_player/common.py index 1d1f811c91ec92..177f8169ff95fd 100644 --- a/tests/components/media_player/common.py +++ b/tests/components/media_player/common.py @@ -32,47 +32,48 @@ SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, entity_id=None): +def turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn on specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_TURN_ON, data) @bind_hass -def turn_off(hass, entity_id=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn off specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_TURN_OFF, data) @bind_hass -def toggle(hass, entity_id=None): +def toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle specified media player or all.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_TOGGLE, data) @bind_hass -def volume_up(hass, entity_id=None): +def volume_up(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for volume up.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_VOLUME_UP, data) @bind_hass -def volume_down(hass, entity_id=None): +def volume_down(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for volume down.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_VOLUME_DOWN, data) @bind_hass -def mute_volume(hass, mute, entity_id=None): +def mute_volume(hass, mute, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for muting the volume.""" data = {ATTR_MEDIA_VOLUME_MUTED: mute} @@ -83,7 +84,7 @@ def mute_volume(hass, mute, entity_id=None): @bind_hass -def set_volume_level(hass, volume, entity_id=None): +def set_volume_level(hass, volume, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for setting the volume.""" data = {ATTR_MEDIA_VOLUME_LEVEL: volume} @@ -94,49 +95,49 @@ def set_volume_level(hass, volume, entity_id=None): @bind_hass -def media_play_pause(hass, entity_id=None): +def media_play_pause(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for play/pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, data) @bind_hass -def media_play(hass, entity_id=None): +def media_play(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for play/pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PLAY, data) @bind_hass -def media_pause(hass, entity_id=None): +def media_pause(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for pause.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PAUSE, data) @bind_hass -def media_stop(hass, entity_id=None): +def media_stop(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for stop.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_STOP, data) @bind_hass -def media_next_track(hass, entity_id=None): +def media_next_track(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for next track.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_NEXT_TRACK, data) @bind_hass -def media_previous_track(hass, entity_id=None): +def media_previous_track(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for prev track.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, data) @bind_hass -def media_seek(hass, position, entity_id=None): +def media_seek(hass, position, entity_id=ENTITY_MATCH_ALL): """Send the media player the command to seek in current playing media.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_MEDIA_SEEK_POSITION] = position @@ -144,7 +145,7 @@ def media_seek(hass, position, entity_id=None): @bind_hass -def play_media(hass, media_type, media_id, entity_id=None, enqueue=None): +def play_media(hass, media_type, media_id, entity_id=ENTITY_MATCH_ALL, enqueue=None): """Send the media player the command for playing media.""" data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id} @@ -158,7 +159,7 @@ def play_media(hass, media_type, media_id, entity_id=None, enqueue=None): @bind_hass -def select_source(hass, source, entity_id=None): +def select_source(hass, source, entity_id=ENTITY_MATCH_ALL): """Send the media player the command to select input source.""" data = {ATTR_INPUT_SOURCE: source} @@ -169,7 +170,7 @@ def select_source(hass, source, entity_id=None): @bind_hass -def clear_playlist(hass, entity_id=None): +def clear_playlist(hass, entity_id=ENTITY_MATCH_ALL): """Send the media player the command for clear playlist.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} hass.services.call(DOMAIN, SERVICE_CLEAR_PLAYLIST, data) diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 572c3b05752401..7919e07767d50f 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -28,6 +28,7 @@ CONF_PLATFORM, STATE_UNAVAILABLE, STATE_UNKNOWN, + ENTITY_MATCH_ALL, ) from homeassistant.setup import async_setup_component @@ -75,29 +76,41 @@ async def test_all_commands(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.services.async_call(DOMAIN, SERVICE_START, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "start", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_STOP, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_STOP, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "stop", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_PAUSE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_PAUSE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "pause", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_LOCATE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_LOCATE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with(COMMAND_TOPIC, "locate", 0, False) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_CLEAN_SPOT, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, "clean_spot", 0, False ) mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, "return_to_base", 0, False ) @@ -134,27 +147,39 @@ async def test_commands_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - await hass.services.async_call(DOMAIN, SERVICE_START, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_PAUSE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_PAUSE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_STOP, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_STOP, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_LOCATE, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_LOCATE, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - await hass.services.async_call(DOMAIN, SERVICE_CLEAN_SPOT, blocking=True) + await hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, {"entity_id": ENTITY_MATCH_ALL}, blocking=True + ) mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() diff --git a/tests/components/remote/common.py b/tests/components/remote/common.py index a35489f1780fea..d972640487aa15 100644 --- a/tests/components/remote/common.py +++ b/tests/components/remote/common.py @@ -15,12 +15,17 @@ SERVICE_LEARN_COMMAND, SERVICE_SEND_COMMAND, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + ENTITY_MATCH_ALL, +) from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, activity=None, entity_id=None): +def turn_on(hass, activity=None, entity_id=ENTITY_MATCH_ALL): """Turn all or specified remote on.""" data = { key: value @@ -31,7 +36,7 @@ def turn_on(hass, activity=None, entity_id=None): @bind_hass -def turn_off(hass, activity=None, entity_id=None): +def turn_off(hass, activity=None, entity_id=ENTITY_MATCH_ALL): """Turn all or specified remote off.""" data = {} if activity: @@ -45,7 +50,12 @@ def turn_off(hass, activity=None, entity_id=None): @bind_hass def send_command( - hass, command, entity_id=None, device=None, num_repeats=None, delay_secs=None + hass, + command, + entity_id=ENTITY_MATCH_ALL, + device=None, + num_repeats=None, + delay_secs=None, ): """Send a command to a device.""" data = {ATTR_COMMAND: command} @@ -66,7 +76,12 @@ def send_command( @bind_hass def learn_command( - hass, entity_id=None, device=None, command=None, alternative=None, timeout=None + hass, + entity_id=ENTITY_MATCH_ALL, + device=None, + command=None, + alternative=None, + timeout=None, ): """Learn a command from a device.""" data = {} diff --git a/tests/components/scene/common.py b/tests/components/scene/common.py index 4f8123ca6380a4..5da0cc21db0838 100644 --- a/tests/components/scene/common.py +++ b/tests/components/scene/common.py @@ -4,12 +4,12 @@ components. Instead call the service directly. """ from homeassistant.components.scene import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, ENTITY_MATCH_ALL from homeassistant.loader import bind_hass @bind_hass -def activate(hass, entity_id=None): +def activate(hass, entity_id=ENTITY_MATCH_ALL): """Activate a scene.""" data = {} diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 630174a0661396..79919a376cd067 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -551,7 +551,9 @@ async def test_set_turn_off(hass, air_conditioner): await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_HEAT_COOL - await hass.services.async_call(CLIMATE_DOMAIN, SERVICE_TURN_OFF, blocking=True) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_TURN_OFF, {"entity_id": "all"}, blocking=True + ) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_OFF @@ -562,7 +564,9 @@ async def test_set_turn_on(hass, air_conditioner): await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_OFF - await hass.services.async_call(CLIMATE_DOMAIN, SERVICE_TURN_ON, blocking=True) + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_TURN_ON, {"entity_id": "all"}, blocking=True + ) state = hass.states.get("climate.air_conditioner") assert state.state == HVAC_MODE_HEAT_COOL diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 19a2ec3463a6fc..26b68c0cb1f6d8 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -114,7 +114,10 @@ async def test_set_cover_position(hass, device_factory): await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.services.async_call( - COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {ATTR_POSITION: 50}, blocking=True + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_POSITION: 50, "entity_id": "all"}, + blocking=True, ) state = hass.states.get("cover.shade") @@ -136,7 +139,10 @@ async def test_set_cover_position_unsupported(hass, device_factory): await setup_platform(hass, COVER_DOMAIN, devices=[device]) # Act await hass.services.async_call( - COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {ATTR_POSITION: 50}, blocking=True + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {"entity_id": "all", ATTR_POSITION: 50}, + blocking=True, ) state = hass.states.get("cover.shade") diff --git a/tests/components/switch/common.py b/tests/components/switch/common.py index 4491b07cd73a6e..2c6b1ccd0d2b41 100644 --- a/tests/components/switch/common.py +++ b/tests/components/switch/common.py @@ -4,29 +4,34 @@ components. Instead call the service directly. """ from homeassistant.components.switch import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + ENTITY_MATCH_ALL, +) from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, entity_id=None): +def turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch on.""" hass.add_job(async_turn_on, hass, entity_id) -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) @bind_hass -def turn_off(hass, entity_id=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch off.""" hass.add_job(async_turn_off, hass, entity_id) -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified switch off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) diff --git a/tests/components/vacuum/common.py b/tests/components/vacuum/common.py index 7d9f645449f924..59f600590b1946 100644 --- a/tests/components/vacuum/common.py +++ b/tests/components/vacuum/common.py @@ -23,137 +23,138 @@ SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass @bind_hass -def turn_on(hass, entity_id=None): +def turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum on.""" hass.add_job(async_turn_on, hass, entity_id) -async def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data, blocking=True) @bind_hass -def turn_off(hass, entity_id=None): +def turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum off.""" hass.add_job(async_turn_off, hass, entity_id) -async def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL): """Turn all or specified vacuum off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data, blocking=True) @bind_hass -def toggle(hass, entity_id=None): +def toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle all or specified vacuum.""" hass.add_job(async_toggle, hass, entity_id) -async def async_toggle(hass, entity_id=None): +async def async_toggle(hass, entity_id=ENTITY_MATCH_ALL): """Toggle all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_TOGGLE, data, blocking=True) @bind_hass -def locate(hass, entity_id=None): +def locate(hass, entity_id=ENTITY_MATCH_ALL): """Locate all or specified vacuum.""" hass.add_job(async_locate, hass, entity_id) -async def async_locate(hass, entity_id=None): +async def async_locate(hass, entity_id=ENTITY_MATCH_ALL): """Locate all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_LOCATE, data, blocking=True) @bind_hass -def clean_spot(hass, entity_id=None): +def clean_spot(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to perform a spot clean-up.""" hass.add_job(async_clean_spot, hass, entity_id) -async def async_clean_spot(hass, entity_id=None): +async def async_clean_spot(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to perform a spot clean-up.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_CLEAN_SPOT, data, blocking=True) @bind_hass -def return_to_base(hass, entity_id=None): +def return_to_base(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to return to base.""" hass.add_job(async_return_to_base, hass, entity_id) -async def async_return_to_base(hass, entity_id=None): +async def async_return_to_base(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to return to base.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_RETURN_TO_BASE, data, blocking=True) @bind_hass -def start_pause(hass, entity_id=None): +def start_pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or pause the current task.""" hass.add_job(async_start_pause, hass, entity_id) -async def async_start_pause(hass, entity_id=None): +async def async_start_pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_START_PAUSE, data, blocking=True) @bind_hass -def start(hass, entity_id=None): +def start(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or resume the current task.""" hass.add_job(async_start, hass, entity_id) -async def async_start(hass, entity_id=None): +async def async_start(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or specified vacuum to start or resume the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_START, data, blocking=True) @bind_hass -def pause(hass, entity_id=None): +def pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or the specified vacuum to pause the current task.""" hass.add_job(async_pause, hass, entity_id) -async def async_pause(hass, entity_id=None): +async def async_pause(hass, entity_id=ENTITY_MATCH_ALL): """Tell all or the specified vacuum to pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_PAUSE, data, blocking=True) @bind_hass -def stop(hass, entity_id=None): +def stop(hass, entity_id=ENTITY_MATCH_ALL): """Stop all or specified vacuum.""" hass.add_job(async_stop, hass, entity_id) -async def async_stop(hass, entity_id=None): +async def async_stop(hass, entity_id=ENTITY_MATCH_ALL): """Stop all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None await hass.services.async_call(DOMAIN, SERVICE_STOP, data, blocking=True) @bind_hass -def set_fan_speed(hass, fan_speed, entity_id=None): +def set_fan_speed(hass, fan_speed, entity_id=ENTITY_MATCH_ALL): """Set fan speed for all or specified vacuum.""" hass.add_job(async_set_fan_speed, hass, fan_speed, entity_id) -async def async_set_fan_speed(hass, fan_speed, entity_id=None): +async def async_set_fan_speed(hass, fan_speed, entity_id=ENTITY_MATCH_ALL): """Set fan speed for all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_FAN_SPEED] = fan_speed @@ -161,12 +162,12 @@ async def async_set_fan_speed(hass, fan_speed, entity_id=None): @bind_hass -def send_command(hass, command, params=None, entity_id=None): +def send_command(hass, command, params=None, entity_id=ENTITY_MATCH_ALL): """Send command to all or specified vacuum.""" hass.add_job(async_send_command, hass, command, params, entity_id) -async def async_send_command(hass, command, params=None, entity_id=None): +async def async_send_command(hass, command, params=None, entity_id=ENTITY_MATCH_ALL): """Send command to all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_COMMAND] = command diff --git a/tests/components/water_heater/common.py b/tests/components/water_heater/common.py index 3fb010ab55ccee..0808e3e3daca8e 100644 --- a/tests/components/water_heater/common.py +++ b/tests/components/water_heater/common.py @@ -12,12 +12,12 @@ SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, ENTITY_MATCH_ALL from homeassistant.loader import bind_hass @bind_hass -def set_away_mode(hass, away_mode, entity_id=None): +def set_away_mode(hass, away_mode, entity_id=ENTITY_MATCH_ALL): """Turn all or specified water_heater devices away mode on.""" data = {ATTR_AWAY_MODE: away_mode} @@ -28,7 +28,9 @@ def set_away_mode(hass, away_mode, entity_id=None): @bind_hass -def set_temperature(hass, temperature=None, entity_id=None, operation_mode=None): +def set_temperature( + hass, temperature=None, entity_id=ENTITY_MATCH_ALL, operation_mode=None +): """Set new target temperature.""" kwargs = { key: value @@ -44,7 +46,7 @@ def set_temperature(hass, temperature=None, entity_id=None, operation_mode=None) @bind_hass -def set_operation_mode(hass, operation_mode, entity_id=None): +def set_operation_mode(hass, operation_mode, entity_id=ENTITY_MATCH_ALL): """Set new target operation mode.""" data = {ATTR_OPERATION_MODE: operation_mode} diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 0d52f430ff5d23..350aa88b6af2f3 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -9,6 +9,7 @@ import pytest import homeassistant.core as ha +from homeassistant.const import ENTITY_MATCH_ALL from homeassistant.exceptions import PlatformNotReady from homeassistant.components import group from homeassistant.helpers.entity_component import EntityComponent @@ -194,7 +195,7 @@ async def test_extract_from_service_available_device(hass): ] ) - call_1 = ha.ServiceCall("test", "service") + call_1 = ha.ServiceCall("test", "service", data={"entity_id": ENTITY_MATCH_ALL}) assert ["test_domain.test_1", "test_domain.test_3"] == sorted( ent.entity_id for ent in (await component.async_extract_from_service(call_1)) @@ -250,7 +251,7 @@ async def test_platform_not_ready(hass): assert "test_domain.mod1" in hass.config.components -async def test_extract_from_service_returns_all_if_no_entity_id(hass): +async def test_extract_from_service_fails_if_no_entity_id(hass): """Test the extraction of everything from service.""" component = EntityComponent(_LOGGER, DOMAIN, hass) await component.async_add_entities( @@ -259,7 +260,7 @@ async def test_extract_from_service_returns_all_if_no_entity_id(hass): call = ha.ServiceCall("test", "service") - assert ["test_domain.test_1", "test_domain.test_2"] == sorted( + assert [] == sorted( ent.entity_id for ent in (await component.async_extract_from_service(call)) ) @@ -445,12 +446,9 @@ async def test_extract_all_omit_entity_id(hass, caplog): call = ha.ServiceCall("test", "service") - assert ["test_domain.test_1", "test_domain.test_2"] == sorted( + assert [] == sorted( ent.entity_id for ent in await component.async_extract_from_service(call) ) - assert ( - "Not passing an entity ID to a service to target all entities is " "deprecated" - ) in caplog.text async def test_extract_all_use_match_all(hass, caplog): diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index de8142a6374614..20be7db42f5402 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -11,7 +11,7 @@ # To prevent circular import when running just this file import homeassistant.components # noqa: F401 from homeassistant import core as ha, exceptions -from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID +from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID, ENTITY_MATCH_ALL from homeassistant.setup import async_setup_component import homeassistant.helpers.config_validation as cv from homeassistant.auth.permissions import PolicyPermissions @@ -334,7 +334,10 @@ async def test_call_context_target_all(hass, mock_service_platform_call, mock_en [Mock(entities=mock_entities)], Mock(), ha.ServiceCall( - "test_domain", "test_service", context=ha.Context(user_id="mock-id") + "test_domain", + "test_service", + data={"entity_id": ENTITY_MATCH_ALL}, + context=ha.Context(user_id="mock-id"), ), ) @@ -407,7 +410,9 @@ async def test_call_no_context_target_all( hass, [Mock(entities=mock_entities)], Mock(), - ha.ServiceCall("test_domain", "test_service"), + ha.ServiceCall( + "test_domain", "test_service", data={"entity_id": ENTITY_MATCH_ALL} + ), ) assert len(mock_service_platform_call.mock_calls) == 1 @@ -458,9 +463,9 @@ async def test_call_with_match_all( async def test_call_with_omit_entity_id( - hass, mock_service_platform_call, mock_entities, caplog + hass, mock_service_platform_call, mock_entities ): - """Check we only target allowed entities if targetting all.""" + """Check service call if we do not pass an entity ID.""" await service.entity_service_call( hass, [Mock(entities=mock_entities)], @@ -470,13 +475,7 @@ async def test_call_with_omit_entity_id( assert len(mock_service_platform_call.mock_calls) == 1 entities = mock_service_platform_call.mock_calls[0][1][2] - assert entities == [ - mock_entities["light.kitchen"], - mock_entities["light.living_room"], - ] - assert ( - "Not passing an entity ID to a service to target " "all entities is deprecated" - ) in caplog.text + assert entities == [] async def test_register_admin_service(hass, hass_read_only_user, hass_admin_user): From d5ee34e5045fe73a18ea510fadc6a6ff3413babb Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 3 Dec 2019 00:32:23 +0000 Subject: [PATCH 1977/3953] [ci skip] Translation update --- .../components/alarm_control_panel/.translations/lb.json | 7 +++++++ .../components/alarm_control_panel/.translations/no.json | 7 +++++++ .../components/alarm_control_panel/.translations/sl.json | 7 +++++++ homeassistant/components/huawei_lte/.translations/sl.json | 2 +- homeassistant/components/plex/.translations/fr.json | 1 + homeassistant/components/plex/.translations/lb.json | 1 + homeassistant/components/plex/.translations/no.json | 1 + homeassistant/components/plex/.translations/sl.json | 1 + 8 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alarm_control_panel/.translations/lb.json b/homeassistant/components/alarm_control_panel/.translations/lb.json index ff265a52c388cc..add11f5b8fe62c 100644 --- a/homeassistant/components/alarm_control_panel/.translations/lb.json +++ b/homeassistant/components/alarm_control_panel/.translations/lb.json @@ -6,6 +6,13 @@ "arm_night": "{entity_name} fir Nuecht uschalten", "disarm": "{entity_name} entsch\u00e4rfen", "trigger": "{entity_name} ausl\u00e9isen" + }, + "trigger_type": { + "armed_away": "{entity_name} ugeschalt fir Ennerwee", + "armed_home": "{entity_name} ugeschalt fir Doheem", + "armed_night": "{entity_name} ugeschalt fir Nuecht", + "disarmed": "{entity_name} entsch\u00e4rft", + "triggered": "{entity_name} ausgel\u00e9ist" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/no.json b/homeassistant/components/alarm_control_panel/.translations/no.json index 93833f33d41539..108d273a0f077f 100644 --- a/homeassistant/components/alarm_control_panel/.translations/no.json +++ b/homeassistant/components/alarm_control_panel/.translations/no.json @@ -6,6 +6,13 @@ "arm_night": "Aktiver {entity_name} natt", "disarm": "Deaktiver {entity_name}", "trigger": "Utl\u00f8ser {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} borte sikkring ", + "armed_home": "{entity_name} hjemme sikkring", + "armed_night": "{entity_name} natt sikkring", + "disarmed": "{entity_name} deaktivert", + "triggered": "{entity_name} utl\u00f8st" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/sl.json b/homeassistant/components/alarm_control_panel/.translations/sl.json index 9bf01fc62de920..855c50ab8273cc 100644 --- a/homeassistant/components/alarm_control_panel/.translations/sl.json +++ b/homeassistant/components/alarm_control_panel/.translations/sl.json @@ -6,6 +6,13 @@ "arm_night": "Vklju\u010di {entity_name} no\u010d", "disarm": "Razoro\u017ei {entity_name}", "trigger": "Spro\u017ei {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} oboro\u017een - zdoma", + "armed_home": "{entity_name} oboro\u017een - dom", + "armed_night": "{entity_name} oboro\u017een - no\u010d", + "disarmed": "{entity_name} razoro\u017een", + "triggered": "{entity_name} spro\u017een" } } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/sl.json b/homeassistant/components/huawei_lte/.translations/sl.json index 0b4964069b2e8c..5022e358ca72d3 100644 --- a/homeassistant/components/huawei_lte/.translations/sl.json +++ b/homeassistant/components/huawei_lte/.translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ta naprava je \u017ee nastavljena", + "already_configured": "Ta naprava je \u017ee konfigurirana", "already_in_progress": "Ta naprava se \u017ee nastavlja", "not_huawei_lte": "Ni naprava Huawei LTE" }, diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 2eef7a5e9a2f50..bcd53d2ffae457 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex en cours de configuration", "discovery_no_file": "Aucun fichier de configuration h\u00e9rit\u00e9 trouv\u00e9", "invalid_import": "La configuration import\u00e9e est invalide", + "non-interactive": "Importation non interactive", "token_request_timeout": "D\u00e9lai d'obtention du jeton", "unknown": "\u00c9chec pour une raison inconnue" }, diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 7b0f7232976a67..c6fcabc40d7ced 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", "discovery_no_file": "Kee Konfiguratioun Fichier am ale Format fonnt.", "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", + "non-interactive": "Net interaktiven Import", "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", "unknown": "Onbekannte Feeler opgetrueden" }, diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index 8ebd2b69bb93e8..cc6dac8a35bb03 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex blir konfigurert", "discovery_no_file": "Ingen eldre konfigurasjonsfil ble funnet", "invalid_import": "Den importerte konfigurasjonen er ugyldig", + "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Mislyktes av ukjent \u00e5rsak" }, diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 7426e7f95edd24..1ff93cff650b6a 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex se konfigurira", "discovery_no_file": "Podatkovne konfiguracijske datoteke ni bilo mogo\u010de najti", "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", + "non-interactive": "Neinteraktivni uvoz", "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", "unknown": "Ni uspelo iz neznanega razloga" }, From 434b783b4ca8876247c49ba39c8f6dafd66020d4 Mon Sep 17 00:00:00 2001 From: Andy Loughran Date: Tue, 3 Dec 2019 05:48:51 +0000 Subject: [PATCH 1978/3953] Update heatmiserv3 integration (#29006) * Updated heatmiserV3 initial commit * Fixing heatmiser component * Updated codeowners and heatmiserV3 version * Updating files as part of PR process * Removed extra _LOGGER statements. * Added in HVAC_MODE_OFF to allowed states to track whether heating on/off * Handling PR comments * Removed legacy tests * fixing pylint errors * Update homeassistant/components/heatmiser/climate.py Removed .get from config Co-Authored-By: Martin Hjelmare * Update homeassistant/components/heatmiser/climate.py Removed .get from config Co-Authored-By: Martin Hjelmare * Update homeassistant/components/heatmiser/climate.py Removed .get from config Co-Authored-By: Martin Hjelmare * Updated climate based on latest feedback * Removed cast to int and update requirements * Update requirements * Updated heatmiser deps --- CODEOWNERS | 1 + homeassistant/components/heatmiser/climate.py | 81 ++++++++++++------- .../components/heatmiser/manifest.json | 8 +- requirements_all.txt | 2 +- 4 files changed, 58 insertions(+), 34 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e859a9d0eac941..66332531bd8323 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -131,6 +131,7 @@ homeassistant/components/growatt_server/* @indykoning homeassistant/components/gtfs/* @robbiet480 homeassistant/components/harmony/* @ehendrix23 homeassistant/components/hassio/* @home-assistant/hass-io +homeassistant/components/heatmiser/* @andylockran homeassistant/components/heos/* @andrewsayre homeassistant/components/here_travel_time/* @eifinger homeassistant/components/hikvision/* @mezz64 diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index 6693e71878a99f..1954749c21bf79 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -4,56 +4,62 @@ import voluptuous as vol +from heatmiserV3 import heatmiser, connection from homeassistant.components.climate import ( ClimateDevice, PLATFORM_SCHEMA, HVAC_MODE_HEAT, + HVAC_MODE_OFF, ) from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE from homeassistant.const import ( TEMP_CELSIUS, + TEMP_FAHRENHEIT, ATTR_TEMPERATURE, + CONF_HOST, CONF_PORT, - CONF_NAME, CONF_ID, + CONF_NAME, ) import homeassistant.helpers.config_validation as cv + _LOGGER = logging.getLogger(__name__) -CONF_IPADDRESS = "ipaddress" -CONF_TSTATS = "tstats" +CONF_THERMOSTATS = "tstats" TSTATS_SCHEMA = vol.Schema( - {vol.Required(CONF_ID): cv.string, vol.Required(CONF_NAME): cv.string} + vol.All( + cv.ensure_list, + [{vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string}], + ) ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_IPADDRESS): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_TSTATS, default={}): vol.Schema({cv.string: TSTATS_SCHEMA}), + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.string, + vol.Optional(CONF_THERMOSTATS, default=[]): TSTATS_SCHEMA, } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the heatmiser thermostat.""" - from heatmiserV3 import heatmiser, connection - ipaddress = config.get(CONF_IPADDRESS) - port = str(config.get(CONF_PORT)) - tstats = config.get(CONF_TSTATS) + heatmiser_v3_thermostat = heatmiser.HeatmiserThermostat + + host = config[CONF_HOST] + port = config[CONF_PORT] + + thermostats = config[CONF_THERMOSTATS] - serport = connection.connection(ipaddress, port) - serport.open() + uh1_hub = connection.HeatmiserUH1(host, port) add_entities( [ - HeatmiserV3Thermostat( - heatmiser, tstat.get(CONF_ID), tstat.get(CONF_NAME), serport - ) - for tstat in tstats.values() + HeatmiserV3Thermostat(heatmiser_v3_thermostat, thermostat, uh1_hub) + for thermostat in thermostats ], True, ) @@ -62,15 +68,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class HeatmiserV3Thermostat(ClimateDevice): """Representation of a HeatmiserV3 thermostat.""" - def __init__(self, heatmiser, device, name, serport): + def __init__(self, therm, device, uh1): """Initialize the thermostat.""" - self.heatmiser = heatmiser - self.serport = serport + self.therm = therm(device[CONF_ID], "prt", uh1) + self.uh1 = uh1 + self._name = device[CONF_NAME] self._current_temperature = None self._target_temperature = None - self._name = name self._id = device self.dcb = None + self._hvac_mode = HVAC_MODE_HEAT + self._temperature_unit = None @property def supported_features(self): @@ -85,7 +93,7 @@ def name(self): @property def temperature_unit(self): """Return the unit of measurement which this thermostat uses.""" - return TEMP_CELSIUS + return self._temperature_unit @property def hvac_mode(self) -> str: @@ -93,7 +101,7 @@ def hvac_mode(self) -> str: Need to be one of HVAC_MODE_*. """ - return HVAC_MODE_HEAT + return self._hvac_mode @property def hvac_modes(self) -> List[str]: @@ -101,7 +109,7 @@ def hvac_modes(self) -> List[str]: Need to be a subset of HVAC_MODES. """ - return [HVAC_MODE_HEAT] + return [HVAC_MODE_HEAT, HVAC_MODE_OFF] @property def current_temperature(self): @@ -116,12 +124,25 @@ def target_temperature(self): def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) - self.heatmiser.hmSendAddress(self._id, 18, temperature, 1, self.serport) + self._target_temperature = int(temperature) + self.therm.set_target_temp(self._target_temperature) def update(self): """Get the latest data.""" - self.dcb = self.heatmiser.hmReadAddress(self._id, "prt", self.serport) - low = self.dcb.get("floortemplow ") - high = self.dcb.get("floortemphigh") - self._current_temperature = (high * 256 + low) / 10.0 - self._target_temperature = int(self.dcb.get("roomset")) + self.uh1.reopen() + if not self.uh1.status: + _LOGGER.error("Failed to update device %s", self._name) + return + self.dcb = self.therm.read_dcb() + self._temperature_unit = ( + TEMP_CELSIUS + if (self.therm.get_temperature_format() == "C") + else TEMP_FAHRENHEIT + ) + self._current_temperature = int(self.therm.get_floor_temp()) + self._target_temperature = int(self.therm.get_target_temp()) + self._hvac_mode = ( + HVAC_MODE_OFF + if (int(self.therm.get_current_state()) == 0) + else HVAC_MODE_HEAT + ) diff --git a/homeassistant/components/heatmiser/manifest.json b/homeassistant/components/heatmiser/manifest.json index b3882c63c51f28..89bcec081253d8 100644 --- a/homeassistant/components/heatmiser/manifest.json +++ b/homeassistant/components/heatmiser/manifest.json @@ -3,8 +3,10 @@ "name": "Heatmiser", "documentation": "https://www.home-assistant.io/integrations/heatmiser", "requirements": [ - "heatmiserV3==0.9.1" + "heatmiserV3==1.1.18" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@andylockran" + ] +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 60b8dbb43cab6f..75fc2dd5bbde4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -643,7 +643,7 @@ hbmqtt==0.9.5 hdate==0.9.3 # homeassistant.components.heatmiser -heatmiserV3==0.9.1 +heatmiserV3==1.1.18 # homeassistant.components.here_travel_time herepy==0.6.3.3 From 2569c4ae37c493dcde560d9ce64245d8fea9f15b Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 3 Dec 2019 07:05:59 +0100 Subject: [PATCH 1979/3953] Google assistant storage of connected agents (#29158) * Make async_report_state take agent_user_id * Attempt to store synced agents * Drop now not needed initialization * Make sure cloud uses the all sync on changed preferences * Some more places to use all version of sync * Get the agent_user_id from the request context if available * Minor cleanup * Remove the old fixed agent_user_id for cloud Instead pass along cloud_user where appropriate. * async_delay_save takes a function * Adjust test for delayed store * Remove unused save function * Add login check. --- homeassistant/components/cloud/client.py | 1 + .../components/cloud/google_config.py | 24 ++--- homeassistant/components/cloud/http_api.py | 2 +- .../components/google_assistant/__init__.py | 2 + .../components/google_assistant/const.py | 2 + .../components/google_assistant/helpers.py | 92 ++++++++++++++++-- .../components/google_assistant/http.py | 9 +- .../google_assistant/report_state.py | 4 +- .../components/google_assistant/smart_home.py | 15 +-- tests/components/cloud/test_client.py | 9 +- tests/components/cloud/test_google_config.py | 6 ++ tests/components/google_assistant/__init__.py | 13 +++ .../google_assistant/test_helpers.py | 96 ++++++++++++++++++- .../components/google_assistant/test_http.py | 29 +++--- .../google_assistant/test_report_state.py | 10 +- .../google_assistant/test_smart_home.py | 29 +++--- 16 files changed, 260 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 00acf930f86c7c..956d35caf2d7ae 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -101,6 +101,7 @@ async def get_google_config(self) -> google_config.CloudGoogleConfig: self._google_config = google_config.CloudGoogleConfig( self._hass, self.google_user_config, cloud_user, self._prefs, self.cloud ) + await self._google_config.async_initialize() return self._google_config diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 3d6511ffc3da5a..3df06c140a05ed 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -42,12 +42,7 @@ def __init__(self, hass, config, cloud_user, prefs, cloud): @property def enabled(self): """Return if Google is enabled.""" - return self._prefs.google_enabled - - @property - def agent_user_id(self): - """Return Agent User Id to use for query responses.""" - return self._cloud.username + return self._cloud.is_logged_in and self._prefs.google_enabled @property def entity_config(self): @@ -62,7 +57,7 @@ def secure_devices_pin(self): @property def should_report_state(self): """Return if states should be proactively reported.""" - return self._prefs.google_report_state + return self._cloud.is_logged_in and self._prefs.google_report_state @property def local_sdk_webhook_id(self): @@ -104,7 +99,7 @@ def should_2fa(self, state): entity_config = entity_configs.get(state.entity_id, {}) return not entity_config.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA) - async def async_report_state(self, message): + async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" try: await self._cloud.google_report_state.async_send_message(message) @@ -132,13 +127,6 @@ async def _async_request_sync_devices(self, agent_user_id: str): _LOGGER.debug("Finished requesting syncing: %s", req.status) return req.status - async def async_deactivate_report_state(self): - """Turn off report state and disable further state reporting. - - Called when the user disconnects their account from Google. - """ - await self._prefs.async_update(google_report_state=False) - async def _async_prefs_updated(self, prefs): """Handle updated preferences.""" if self.should_report_state != self.is_reporting_state: @@ -149,7 +137,7 @@ async def _async_prefs_updated(self, prefs): # State reporting is reported as a property on entities. # So when we change it, we need to sync all entities. - await self.async_sync_entities(self.agent_user_id) + await self.async_sync_entities_all() # If entity prefs are the same or we have filter in config.yaml, # don't sync. @@ -157,7 +145,7 @@ async def _async_prefs_updated(self, prefs): self._cur_entity_prefs is not prefs.google_entity_configs and self._config["filter"].empty_filter ): - self.async_schedule_google_sync(self.agent_user_id) + self.async_schedule_google_sync_all() if self.enabled and not self.is_local_sdk_active: self.async_enable_local_sdk() @@ -173,4 +161,4 @@ async def _handle_entity_registry_updated(self, event): # Schedule a sync if a change was made to an entity that Google knows about if self._should_expose_entity_id(entity_id): - await self.async_sync_entities(self.agent_user_id) + await self.async_sync_entities_all() diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index d808fe72d392fd..c68f24172f035c 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -175,7 +175,7 @@ async def post(self, request): hass = request.app["hass"] cloud: Cloud = hass.data[DOMAIN] gconf = await cloud.client.get_google_config() - status = await gconf.async_sync_entities(gconf.agent_user_id) + status = await gconf.async_sync_entities(gconf.cloud_user) return self.json({}, status_code=status) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index c156bf0176b6e1..ecb6d76781713d 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -93,7 +93,9 @@ def _check_report_state(data): async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) + google_config = GoogleConfig(hass, config) + await google_config.async_initialize() hass.http.register_view(GoogleAssistantView(google_config)) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 5209f6040c1c3b..35a04e0e08ec1e 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -141,3 +141,5 @@ CHALLENGE_ACK_NEEDED = "ackNeeded" CHALLENGE_PIN_NEEDED = "pinNeeded" CHALLENGE_FAILED_PIN_NEEDED = "challengeFailedPinNeeded" + +STORE_AGENT_USER_IDS = "agent_user_ids" diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index a128b6c9bcb3b2..bc076533c69db9 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -10,6 +10,7 @@ from homeassistant.core import Context, callback, HomeAssistant, State from homeassistant.helpers.event import async_call_later from homeassistant.components import webhook +from homeassistant.helpers.storage import Store from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, @@ -26,6 +27,7 @@ ERR_FUNCTION_NOT_SUPPORTED, DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT, + STORE_AGENT_USER_IDS, ) from .error import SmartHomeError @@ -41,19 +43,20 @@ class AbstractConfig: def __init__(self, hass): """Initialize abstract config.""" self.hass = hass + self._store = None self._google_sync_unsub = {} self._local_sdk_active = False + async def async_initialize(self): + """Perform async initialization of config.""" + self._store = GoogleConfigStore(self.hass) + await self._store.async_load() + @property def enabled(self): """Return if Google is enabled.""" return False - @property - def agent_user_id(self): - """Return Agent User Id to use for query responses.""" - return None - @property def entity_config(self): """Return entity config.""" @@ -101,10 +104,18 @@ def should_2fa(self, state): # pylint: disable=no-self-use return True - async def async_report_state(self, message): + async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" raise NotImplementedError + async def async_report_state_all(self, message): + """Send a state report to Google for all previously synced users.""" + jobs = [ + self.async_report_state(message, agent_user_id) + for agent_user_id in self._store.agent_user_ids + ] + await gather(*jobs) + def async_enable_report_state(self): """Enable proactive mode.""" # Circular dep @@ -123,9 +134,18 @@ async def async_sync_entities(self, agent_user_id: str): """Sync all entities to Google.""" # Remove any pending sync self._google_sync_unsub.pop(agent_user_id, lambda: None)() - return await self._async_request_sync_devices(agent_user_id) + async def async_sync_entities_all(self): + """Sync all entities to Google for all registered agents.""" + res = await gather( + *[ + self.async_sync_entities(agent_user_id) + for agent_user_id in self._store.agent_user_ids + ] + ) + return max(res, default=204) + @callback def async_schedule_google_sync(self, agent_user_id: str): """Schedule a sync.""" @@ -141,6 +161,12 @@ async def _schedule_callback(_now): self.hass, SYNC_DELAY, _schedule_callback ) + @callback + def async_schedule_google_sync_all(self): + """Schedule a sync for all registered agents.""" + for agent_user_id in self._store.agent_user_ids: + self.async_schedule_google_sync(agent_user_id) + async def _async_request_sync_devices(self, agent_user_id: str) -> int: """Trigger a sync with Google. @@ -148,11 +174,19 @@ async def _async_request_sync_devices(self, agent_user_id: str) -> int: """ raise NotImplementedError - async def async_deactivate_report_state(self): + async def async_connect_agent_user(self, agent_user_id: str): + """Add an synced and known agent_user_id. + + Called when a completed sync response have been sent to Google. + """ + self._store.add_agent_user_id(agent_user_id) + + async def async_disconnect_agent_user(self, agent_user_id: str): """Turn off report state and disable further state reporting. Called when the user disconnects their account from Google. """ + self._store.pop_agent_user_id(agent_user_id) @callback def async_enable_local_sdk(self): @@ -199,6 +233,44 @@ async def _handle_local_webhook(self, hass, webhook_id, request): return json_response(result) +class GoogleConfigStore: + """A configuration store for google assistant.""" + + _STORAGE_VERSION = 1 + _STORAGE_KEY = DOMAIN + + def __init__(self, hass): + """Initialize a configuration store.""" + self._hass = hass + self._store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY) + self._data = {STORE_AGENT_USER_IDS: {}} + + @property + def agent_user_ids(self): + """Return a list of connected agent user_ids.""" + return self._data[STORE_AGENT_USER_IDS] + + @callback + def add_agent_user_id(self, agent_user_id): + """Add an agent user id to store.""" + if agent_user_id not in self._data[STORE_AGENT_USER_IDS]: + self._data[STORE_AGENT_USER_IDS][agent_user_id] = {} + self._store.async_delay_save(lambda: self._data, 1.0) + + @callback + def pop_agent_user_id(self, agent_user_id): + """Remove agent user id from store.""" + if agent_user_id in self._data[STORE_AGENT_USER_IDS]: + self._data[STORE_AGENT_USER_IDS].pop(agent_user_id, None) + self._store.async_delay_save(lambda: self._data, 1.0) + + async def async_load(self): + """Store current configuration to disk.""" + data = await self._store.async_load() + if data: + self._data = data + + class RequestData: """Hold data associated with a particular request.""" @@ -278,7 +350,7 @@ def might_2fa(self) -> bool: trait.might_2fa(domain, features, device_class) for trait in self.traits() ) - async def sync_serialize(self): + async def sync_serialize(self, agent_user_id): """Serialize entity for a SYNC response. https://developers.google.com/actions/smarthome/create-app#actiondevicessync @@ -314,7 +386,7 @@ async def sync_serialize(self): "webhookId": self.config.local_sdk_webhook_id, "httpPort": self.hass.config.api.port, "httpSSL": self.hass.config.api.use_ssl, - "proxyDeviceId": self.config.agent_user_id, + "proxyDeviceId": agent_user_id, } for trt in traits: diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index abf931bd969421..c3d0dd493a8120 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -81,11 +81,6 @@ def enabled(self): """Return if Google is enabled.""" return True - @property - def agent_user_id(self): - """Return Agent User Id to use for query responses.""" - return None - @property def entity_config(self): """Return entity config.""" @@ -214,11 +209,11 @@ async def _call(): _LOGGER.error("Could not contact %s", url) return 500 - async def async_report_state(self, message): + async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" data = { "requestId": uuid4().hex, - "agentUserId": (await self.hass.auth.async_get_owner()).id, + "agentUserId": agent_user_id, "payload": message, } await self.async_call_homegraph_api(REPORT_STATE_BASE_URL, data) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index aacb90e9d2bd36..78a0f50e2773ce 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -45,7 +45,7 @@ async def async_entity_state_listener(changed_entity, old_state, new_state): if entity_data == old_entity.query_serialize(): return - await google_config.async_report_state( + await google_config.async_report_state_all( {"devices": {"states": {changed_entity: entity_data}}} ) @@ -62,7 +62,7 @@ async def inital_report(_now): except SmartHomeError: continue - await google_config.async_report_state({"devices": {"states": entities}}) + await google_config.async_report_state_all({"devices": {"states": entities}}) async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 0944c9532effdb..0e5037ce13a170 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -79,18 +79,19 @@ async def async_devices_sync(hass, data, payload): EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context ) + agent_user_id = data.context.user_id + devices = await asyncio.gather( *( - entity.sync_serialize() + entity.sync_serialize(agent_user_id) for entity in async_get_entities(hass, data.config) if entity.should_expose() ) ) - response = { - "agentUserId": data.config.agent_user_id or data.context.user_id, - "devices": devices, - } + response = {"agentUserId": agent_user_id, "devices": devices} + + await data.config.async_connect_agent_user(agent_user_id) return response @@ -197,7 +198,7 @@ async def async_devices_disconnect(hass, data: RequestData, payload): https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT """ - await data.config.async_deactivate_report_state() + await data.config.async_disconnect_agent_user(data.context.user_id) return None @@ -209,7 +210,7 @@ async def async_devices_identify(hass, data: RequestData, payload): """ return { "device": { - "id": data.config.agent_user_id, + "id": data.context.user_id, "isLocalOnly": True, "isProxy": True, "deviceInfo": { diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index a9c4ade668d96d..955923c1e68ebb 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -102,16 +102,13 @@ async def test_handler_google_actions(hass): reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} - with patch( - "hass_nabucasa.Cloud._decode_claims", - return_value={"cognito:username": "myUserName"}, - ): - resp = await cloud.client.async_google_message(data) + config = await cloud.client.get_google_config() + resp = await cloud.client.async_google_message(data) assert resp["requestId"] == reqid payload = resp["payload"] - assert payload["agentUserId"] == "myUserName" + assert payload["agentUserId"] == config.cloud_user devices = payload["devices"] assert len(devices) == 1 diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 0284a2c385134f..3510b4b8abd203 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -19,6 +19,8 @@ async def test_google_update_report_state(hass, cloud_prefs): cloud_prefs, Mock(claims={"cognito:username": "abcdefghjkl"}), ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro @@ -58,6 +60,8 @@ async def test_google_update_expose_trigger_sync(hass, cloud_prefs): cloud_prefs, Mock(claims={"cognito:username": "abcdefghjkl"}), ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro @@ -95,6 +99,8 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): config = CloudGoogleConfig( hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] ) + await config.async_initialize() + await config.async_connect_agent_user("mock-user-id") with patch.object( config, "async_sync_entities", side_effect=mock_coro diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 09522e9c86fc33..657bf930ed6007 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,7 +1,18 @@ """Tests for the Google Assistant integration.""" +from asynctest.mock import MagicMock from homeassistant.components.google_assistant import helpers +def mock_google_config_store(agent_user_ids=None): + """Fake a storage for google assistant.""" + store = MagicMock(spec=helpers.GoogleConfigStore) + if agent_user_ids is not None: + store.agent_user_ids = agent_user_ids + else: + store.agent_user_ids = {} + return store + + class MockConfig(helpers.AbstractConfig): """Fake config that always exposes everything.""" @@ -15,6 +26,7 @@ def __init__( local_sdk_webhook_id=None, local_sdk_user_id=None, enabled=True, + agent_user_ids=None, ): """Initialize config.""" super().__init__(hass) @@ -24,6 +36,7 @@ def __init__( self._local_sdk_webhook_id = local_sdk_webhook_id self._local_sdk_user_id = local_sdk_user_id self._enabled = enabled + self._store = mock_google_config_store(agent_user_ids) @property def enabled(self): diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 497b7b1f0ae8ad..eb479a3b6b52c6 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -1,11 +1,18 @@ """Test Google Assistant helpers.""" -from unittest.mock import Mock +from asynctest.mock import Mock, patch, call +from datetime import timedelta +import pytest from homeassistant.setup import async_setup_component from homeassistant.components.google_assistant import helpers from homeassistant.components.google_assistant.const import EVENT_COMMAND_RECEIVED +from homeassistant.util import dt from . import MockConfig -from tests.common import async_capture_events, async_mock_service +from tests.common import ( + async_capture_events, + async_mock_service, + async_fire_time_changed, +) async def test_google_entity_sync_serialize_with_local_sdk(hass): @@ -19,13 +26,13 @@ async def test_google_entity_sync_serialize_with_local_sdk(hass): ) entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights")) - serialized = await entity.sync_serialize() + serialized = await entity.sync_serialize(None) assert "otherDeviceIds" not in serialized assert "customData" not in serialized config.async_enable_local_sdk() - serialized = await entity.sync_serialize() + serialized = await entity.sync_serialize(None) assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] assert serialized["customData"] == { "httpPort": 1234, @@ -128,3 +135,84 @@ async def test_config_local_sdk_if_disabled(hass, hass_client): resp = await client.post("/api/webhook/mock-webhook-id") assert resp.status == 200 assert await resp.read() == b"" + + +async def test_agent_user_id_storage(hass, hass_storage): + """Test a disconnect message.""" + + hass_storage["google_assistant"] = { + "version": 1, + "key": "google_assistant", + "data": {"agent_user_ids": {"agent_1": {}}}, + } + + store = helpers.GoogleConfigStore(hass) + await store.async_load() + + assert hass_storage["google_assistant"] == { + "version": 1, + "key": "google_assistant", + "data": {"agent_user_ids": {"agent_1": {}}}, + } + + async def _check_after_delay(data): + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=2)) + await hass.async_block_till_done() + + assert hass_storage["google_assistant"] == { + "version": 1, + "key": "google_assistant", + "data": data, + } + + store.add_agent_user_id("agent_2") + await _check_after_delay({"agent_user_ids": {"agent_1": {}, "agent_2": {}}}) + + store.pop_agent_user_id("agent_1") + await _check_after_delay({"agent_user_ids": {"agent_2": {}}}) + + +async def test_agent_user_id_connect(): + """Test the connection and disconnection of users.""" + config = MockConfig() + store = config._store + + await config.async_connect_agent_user("agent_2") + assert store.add_agent_user_id.call_args == call("agent_2") + + await config.async_connect_agent_user("agent_1") + assert store.add_agent_user_id.call_args == call("agent_1") + + await config.async_disconnect_agent_user("agent_2") + assert store.pop_agent_user_id.call_args == call("agent_2") + + await config.async_disconnect_agent_user("agent_1") + assert store.pop_agent_user_id.call_args == call("agent_1") + + +@pytest.mark.parametrize("agents", [{}, {"1"}, {"1", "2"}]) +async def test_report_state_all(agents): + """Test a disconnect message.""" + config = MockConfig(agent_user_ids=agents) + data = {} + with patch.object(config, "async_report_state") as mock: + await config.async_report_state_all(data) + assert sorted(mock.mock_calls) == sorted( + [call(data, agent) for agent in agents] + ) + + +@pytest.mark.parametrize( + "agents, result", [({}, 204), ({"1": 200}, 200), ({"1": 200, "2": 300}, 300)], +) +async def test_sync_entities_all(agents, result): + """Test sync entities .""" + config = MockConfig(agent_user_ids=set(agents.keys())) + with patch.object( + config, + "async_sync_entities", + side_effect=lambda agent_user_id: agents[agent_user_id], + ) as mock: + res = await config.async_sync_entities_all() + assert sorted(mock.mock_calls) == sorted([call(agent) for agent in agents]) + assert res == result diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 42706a470ab86b..86ffcc87ac0a16 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -12,7 +12,6 @@ REPORT_STATE_BASE_URL, HOMEGRAPH_TOKEN_URL, ) -from homeassistant.auth.models import User DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA( { @@ -67,6 +66,7 @@ async def test_update_access_token(hass): jwt = "dummyjwt" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() base_time = datetime(2019, 10, 14, tzinfo=timezone.utc) with patch( @@ -99,6 +99,8 @@ async def test_update_access_token(hass): async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): """Test the function to call the homegraph api.""" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() + with patch( "homeassistant.components.google_assistant.http._get_homegraph_token" ) as mock_get_token: @@ -120,6 +122,8 @@ async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): """Test the that the calls get retried with new token on 401.""" config = GoogleConfig(hass, DUMMY_CONFIG) + await config.async_initialize() + with patch( "homeassistant.components.google_assistant.http._get_homegraph_token" ) as mock_get_token: @@ -143,8 +147,10 @@ async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): async def test_call_homegraph_api_key(hass, aioclient_mock, hass_storage): """Test the function to call the homegraph api.""" config = GoogleConfig( - hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}) + hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), ) + await config.async_initialize() + aioclient_mock.post(MOCK_URL, status=200, json={}) res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) @@ -159,8 +165,10 @@ async def test_call_homegraph_api_key(hass, aioclient_mock, hass_storage): async def test_call_homegraph_api_key_fail(hass, aioclient_mock, hass_storage): """Test the function to call the homegraph api.""" config = GoogleConfig( - hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}) + hass, GOOGLE_ASSISTANT_SCHEMA({"project_id": "1234", "api_key": "dummy_key"}), ) + await config.async_initialize() + aioclient_mock.post(MOCK_URL, status=666, json={}) res = await config.async_call_homegraph_api_key(MOCK_URL, MOCK_JSON) @@ -170,17 +178,16 @@ async def test_call_homegraph_api_key_fail(hass, aioclient_mock, hass_storage): async def test_report_state(hass, aioclient_mock, hass_storage): """Test the report state function.""" + agent_user_id = "user" config = GoogleConfig(hass, DUMMY_CONFIG) - message = {"devices": {}} - owner = User(name="Test User", perm_lookup=None, groups=[], is_owner=True) + await config.async_initialize() - with patch.object(config, "async_call_homegraph_api") as mock_call, patch.object( - hass.auth, "async_get_owner" - ) as mock_get_owner: - mock_get_owner.return_value = owner + await config.async_connect_agent_user(agent_user_id) + message = {"devices": {}} - await config.async_report_state(message) + with patch.object(config, "async_call_homegraph_api") as mock_call: + await config.async_report_state(message, agent_user_id) mock_call.assert_called_once_with( REPORT_STATE_BASE_URL, - {"requestId": ANY, "agentUserId": owner.id, "payload": message}, + {"requestId": ANY, "agentUserId": agent_user_id, "payload": message}, ) diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index 6ab88286a69593..ce624c9ca95ed2 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -16,7 +16,7 @@ async def test_report_state(hass, caplog): hass.states.async_set("switch.ac", "on") with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): unsub = report_state.async_enable_report_state(hass, BASIC_CONFIG) @@ -35,7 +35,7 @@ async def test_report_state(hass, caplog): } with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() @@ -48,7 +48,7 @@ async def test_report_state(hass, caplog): # Test that state changes that change something that Google doesn't care about # do not trigger a state report. with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set( "light.kitchen", "on", {"irrelevant": "should_be_ignored"} @@ -59,7 +59,7 @@ async def test_report_state(hass, caplog): # Test that entities that we can't query don't report a state with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report, patch( "homeassistant.components.google_assistant.report_state.GoogleEntity.query_serialize", side_effect=error.SmartHomeError("mock-error", "mock-msg"), @@ -73,7 +73,7 @@ async def test_report_state(hass, caplog): unsub() with patch.object( - BASIC_CONFIG, "async_report_state", side_effect=mock_coro + BASIC_CONFIG, "async_report_state_all", side_effect=mock_coro ) as mock_report: hass.states.async_set("light.kitchen", "on") await hass.async_block_till_done() diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 250d611b60289c..3bd3831521830c 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -455,7 +455,7 @@ async def test_serialize_input_boolean(hass): state = State("input_boolean.bla", "on") # pylint: disable=protected-access entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) - result = await entity.sync_serialize() + result = await entity.sync_serialize(None) assert result == { "id": "input_boolean.bla", "attributes": {}, @@ -664,8 +664,8 @@ async def test_query_disconnect(hass): config.async_enable_report_state() assert config._unsub_report_state is not None with patch.object( - config, "async_deactivate_report_state", side_effect=mock_coro - ) as mock_deactivate: + config, "async_disconnect_agent_user", side_effect=mock_coro + ) as mock_disconnect: result = await sh.async_handle_message( hass, config, @@ -673,7 +673,7 @@ async def test_query_disconnect(hass): {"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID}, ) assert result is None - assert len(mock_deactivate.mock_calls) == 1 + assert len(mock_disconnect.mock_calls) == 1 async def test_trait_execute_adding_query_data(hass): @@ -741,10 +741,12 @@ async def test_trait_execute_adding_query_data(hass): async def test_identify(hass): """Test identify message.""" + user_agent_id = "mock-user-id" + proxy_device_id = user_agent_id result = await sh.async_handle_message( hass, BASIC_CONFIG, - None, + user_agent_id, { "requestId": REQ_ID, "inputs": [ @@ -778,7 +780,7 @@ async def test_identify(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, } @@ -790,7 +792,7 @@ async def test_identify(hass): "requestId": REQ_ID, "payload": { "device": { - "id": BASIC_CONFIG.agent_user_id, + "id": proxy_device_id, "isLocalOnly": True, "isProxy": True, "deviceInfo": { @@ -822,10 +824,13 @@ async def test_reachable_devices(hass): should_expose=lambda state: state.entity_id != "light.not_expose" ) + user_agent_id = "mock-user-id" + proxy_device_id = user_agent_id + result = await sh.async_handle_message( hass, config, - None, + user_agent_id, { "requestId": REQ_ID, "inputs": [ @@ -834,7 +839,7 @@ async def test_reachable_devices(hass): "payload": { "device": { "proxyDevice": { - "id": "6a04f0f7-6125-4356-a846-861df7e01497", + "id": proxy_device_id, "customData": "{}", "proxyData": "{}", } @@ -849,7 +854,7 @@ async def test_reachable_devices(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, }, @@ -858,11 +863,11 @@ async def test_reachable_devices(hass): "customData": { "httpPort": 8123, "httpSSL": False, - "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "proxyDeviceId": proxy_device_id, "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", }, }, - {"id": BASIC_CONFIG.agent_user_id, "customData": {}}, + {"id": proxy_device_id, "customData": {}}, ], }, ) From 32e04e1dcefe3f55be32c5f99049fae15474b788 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 Dec 2019 22:08:19 -0800 Subject: [PATCH 1980/3953] Lint --- homeassistant/components/cover/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index d7fc0c49742032..0b8fbfa9dd2b48 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -82,7 +82,6 @@ ATTR_TILT_POSITION = "tilt_position" - @bind_hass def is_closed(hass, entity_id=None): """Return if the cover is closed based on the statemachine.""" From 4191d9ca8d0e27e435ef3c45183cf50347cab3f3 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 3 Dec 2019 07:14:16 +0100 Subject: [PATCH 1981/3953] Report unavailable entities to google (#28501) * Report unavailable entites to google. Entities should only removed when removed from HA. Removing a temporarily unavailable entity from google causes it to need to re-configured once it become available again. * Fix test for unavailable entities --- .../components/google_assistant/helpers.py | 2 +- .../google_assistant/test_smart_home.py | 38 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index bc076533c69db9..09859c5d3d0bda 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -336,7 +336,7 @@ def should_expose(self): @callback def is_supported(self) -> bool: """Return if the entity is supported by Google.""" - return self.state.state != STATE_UNAVAILABLE and bool(self.traits()) + return bool(self.traits()) @callback def might_2fa(self) -> bool: diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 3bd3831521830c..c144ffee6def51 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -466,14 +466,17 @@ async def test_serialize_input_boolean(hass): } -async def test_unavailable_state_doesnt_sync(hass): - """Test that an unavailable entity does not sync over.""" - light = DemoLight(None, "Demo Light", state=False) +async def test_unavailable_state_does_sync(hass): + """Test that an unavailable entity does sync over.""" + light = DemoLight(None, "Demo Light", state=False, hs_color=(180, 75)) light.hass = hass light.entity_id = "light.demo_light" light._available = False # pylint: disable=protected-access await light.async_update_ha_state() + events = [] + hass.bus.async_listen(EVENT_SYNC_RECEIVED, events.append) + result = await sh.async_handle_message( hass, BASIC_CONFIG, @@ -483,8 +486,35 @@ async def test_unavailable_state_doesnt_sync(hass): assert result == { "requestId": REQ_ID, - "payload": {"agentUserId": "test-agent", "devices": []}, + "payload": { + "agentUserId": "test-agent", + "devices": [ + { + "id": "light.demo_light", + "name": {"name": "Demo Light"}, + "traits": [ + trait.TRAIT_BRIGHTNESS, + trait.TRAIT_ONOFF, + trait.TRAIT_COLOR_SETTING, + ], + "type": const.TYPE_LIGHT, + "willReportState": False, + "attributes": { + "colorModel": "hsv", + "colorTemperatureRange": { + "temperatureMinK": 2000, + "temperatureMaxK": 6535, + }, + }, + } + ], + }, } + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].event_type == EVENT_SYNC_RECEIVED + assert events[0].data == {"request_id": REQ_ID} @pytest.mark.parametrize( From 26b63e73ad1fb6826d52179b589a9578782ab6b6 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 3 Dec 2019 07:52:25 +0100 Subject: [PATCH 1982/3953] Add initial test suite for arcam_fmj integration (#29335) * Add initial tests * Adjust test * Typo --- tests/components/arcam_fmj/conftest.py | 52 +++ .../components/arcam_fmj/test_config_flow.py | 72 ++-- .../components/arcam_fmj/test_media_player.py | 348 ++++++++++++++++++ 3 files changed, 434 insertions(+), 38 deletions(-) create mode 100644 tests/components/arcam_fmj/conftest.py create mode 100644 tests/components/arcam_fmj/test_media_player.py diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py new file mode 100644 index 00000000000000..10405dbeb11de8 --- /dev/null +++ b/tests/components/arcam_fmj/conftest.py @@ -0,0 +1,52 @@ +"""Tests for the arcam_fmj component.""" +from arcam.fmj.client import Client +from arcam.fmj.state import State +from asynctest import Mock +import pytest + +from homeassistant.components.arcam_fmj import DEVICE_SCHEMA +from homeassistant.components.arcam_fmj.const import DOMAIN +from homeassistant.components.arcam_fmj.media_player import ArcamFmj +from homeassistant.const import CONF_HOST, CONF_PORT + +MOCK_HOST = "127.0.0.1" +MOCK_PORT = 1234 +MOCK_TURN_ON = { + "service": "switch.turn_on", + "data": {"entity_id": "switch.test"}, +} +MOCK_NAME = "dummy" +MOCK_CONFIG = DEVICE_SCHEMA({CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}) + + +@pytest.fixture(name="config") +def config_fixture(): + """Create hass config fixture.""" + return {DOMAIN: [{CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}]} + + +@pytest.fixture(name="client") +def client_fixture(): + """Get a mocked client.""" + client = Mock(Client) + client.host = MOCK_HOST + client.port = MOCK_PORT + return client + + +@pytest.fixture(name="state") +def state_fixture(client): + """Get a mocked state.""" + state = Mock(State) + state.client = client + state.zn = 1 + state.get_power.return_value = True + return state + + +@pytest.fixture(name="player") +def player_fixture(hass, state): + """Get standard player.""" + player = ArcamFmj(state, MOCK_NAME, None) + player.async_schedule_update_ha_state = Mock() + return player diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 54fb34443a5357..6df280fa92efb3 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -1,41 +1,37 @@ """Tests for the Arcam FMJ config flow module.""" + import pytest + from homeassistant import data_entry_flow -from homeassistant.const import CONF_HOST, CONF_PORT - -from tests.common import MockConfigEntry, MockDependency - -with MockDependency("arcam"), MockDependency("arcam.fmj"), MockDependency( - "arcam.fmj.client" -): - from homeassistant.components.arcam_fmj import DEVICE_SCHEMA - from homeassistant.components.arcam_fmj.config_flow import ArcamFmjFlowHandler - from homeassistant.components.arcam_fmj.const import DOMAIN - - MOCK_HOST = "127.0.0.1" - MOCK_PORT = 1234 - MOCK_NAME = "Arcam FMJ" - MOCK_CONFIG = DEVICE_SCHEMA({CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}) - - @pytest.fixture(name="config_entry") - def config_entry_fixture(): - """Create a mock HEOS config entry.""" - return MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME) - - async def test_single_import_only(hass, config_entry): - """Test form is shown when host not provided.""" - config_entry.add_to_hass(hass) - flow = ArcamFmjFlowHandler() - flow.hass = hass - result = await flow.async_step_import(MOCK_CONFIG) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_setup" - - async def test_import(hass): - """Test form is shown when host not provided.""" - flow = ArcamFmjFlowHandler() - flow.hass = hass - result = await flow.async_step_import(MOCK_CONFIG) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == MOCK_NAME - assert result["data"] == MOCK_CONFIG +from homeassistant.components.arcam_fmj.config_flow import ArcamFmjFlowHandler +from homeassistant.components.arcam_fmj.const import DOMAIN + +from .conftest import MOCK_CONFIG, MOCK_NAME + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(): + """Create a mock Arcam config entry.""" + return MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, title=MOCK_NAME) + + +async def test_single_import_only(hass, config_entry): + """Test form is shown when host not provided.""" + config_entry.add_to_hass(hass) + flow = ArcamFmjFlowHandler() + flow.hass = hass + result = await flow.async_step_import(MOCK_CONFIG) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + +async def test_import(hass): + """Test form is shown when host not provided.""" + flow = ArcamFmjFlowHandler() + flow.hass = hass + result = await flow.async_step_import(MOCK_CONFIG) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Arcam FMJ" + assert result["data"] == MOCK_CONFIG diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py new file mode 100644 index 00000000000000..2d2c14f8e18812 --- /dev/null +++ b/tests/components/arcam_fmj/test_media_player.py @@ -0,0 +1,348 @@ +"""Tests for arcam fmj receivers.""" +from math import isclose + +from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes +from asynctest.mock import ANY, MagicMock, Mock, PropertyMock, patch +import pytest + +from homeassistant.components.arcam_fmj.const import ( + DOMAIN, + SIGNAL_CLIENT_DATA, + SIGNAL_CLIENT_STARTED, + SIGNAL_CLIENT_STOPPED, +) +from homeassistant.components.arcam_fmj.media_player import ArcamFmj +from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + +from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT + +MOCK_TURN_ON = { + "service": "switch.turn_on", + "data": {"entity_id": "switch.test"}, +} + + +async def test_properties(player, state): + """Test standard properties.""" + assert player.unique_id is None + assert player.device_info == { + "identifiers": {(DOMAIN, MOCK_HOST, MOCK_PORT)}, + "model": "FMJ", + "manufacturer": "Arcam", + } + assert not player.should_poll + + +async def test_powered_off(player, state): + """Test properties in powered off state.""" + state.get_source.return_value = None + state.get_power.return_value = None + assert player.source is None + assert player.state == STATE_OFF + + +async def test_powered_on(player, state): + """Test properties in powered on state.""" + state.get_source.return_value = SourceCodes.PVR + state.get_power.return_value = True + assert player.source == "PVR" + assert player.state == STATE_ON + + +async def test_supported_features_no_service(player, state): + """Test support when turn on service exist.""" + state.get_power.return_value = None + assert player.supported_features == 68876 + + state.get_power.return_value = False + assert player.supported_features == 69004 + + +async def test_supported_features_service(hass, state): + """Test support when turn on service exist.""" + player = ArcamFmj(state, "dummy", MOCK_TURN_ON) + state.get_power.return_value = None + assert player.supported_features == 69004 + + state.get_power.return_value = False + assert player.supported_features == 69004 + + +async def test_turn_on_without_service(player, state): + """Test turn on service.""" + state.get_power.return_value = None + await player.async_turn_on() + state.set_power.assert_not_called() + + state.get_power.return_value = False + await player.async_turn_on() + state.set_power.assert_called_with(True) + + +async def test_turn_on_with_service(hass, state): + """Test support when turn on service exist.""" + player = ArcamFmj(state, "dummy", MOCK_TURN_ON) + player.hass = Mock(HomeAssistant) + with patch( + "homeassistant.components.arcam_fmj.media_player.async_call_from_config" + ) as async_call_from_config: + + state.get_power.return_value = None + await player.async_turn_on() + state.set_power.assert_not_called() + async_call_from_config.assert_called_with( + player.hass, + MOCK_TURN_ON, + variables=None, + blocking=True, + validate_config=False, + ) + + +async def test_turn_off(player, state): + """Test command to turn off.""" + await player.async_turn_off() + state.set_power.assert_called_with(False) + + +@pytest.mark.parametrize("mute", [True, False]) +async def test_mute_volume(player, state, mute): + """Test mute functionallity.""" + await player.async_mute_volume(mute) + state.set_mute.assert_called_with(mute) + player.async_schedule_update_ha_state.assert_called_with() + + +async def test_name(player): + """Test name.""" + assert player.name == MOCK_NAME + + +async def test_update(player, state): + """Test update.""" + await player.async_update() + state.update.assert_called_with() + + +@pytest.mark.parametrize( + "fmt, result", + [ + (None, True), + (IncomingAudioFormat.PCM, True), + (IncomingAudioFormat.ANALOGUE_DIRECT, True), + (IncomingAudioFormat.DOLBY_DIGITAL, False), + ], +) +async def test_2ch(player, state, fmt, result): + """Test selection of 2ch mode.""" + state.get_incoming_audio_format.return_value = (fmt, None) + assert player._get_2ch() == result # pylint: disable=W0212 + + +@pytest.mark.parametrize( + "source, value", + [("PVR", SourceCodes.PVR), ("BD", SourceCodes.BD), ("INVALID", None)], +) +async def test_select_source(player, state, source, value): + """Test selection of source.""" + await player.async_select_source(source) + if value: + state.set_source.assert_called_with(value) + else: + state.set_source.assert_not_called() + + +async def test_source_list(player, state): + """Test source list.""" + state.get_source_list.return_value = [SourceCodes.BD] + assert player.source_list == ["BD"] + + +@pytest.mark.parametrize( + "mode, mode_sel, mode_2ch, mode_mch", + [ + ("STEREO", True, DecodeMode2CH.STEREO, None), + ("STEREO", False, None, None), + ("STEREO", False, None, None), + ], +) +async def test_select_sound_mode(player, state, mode, mode_sel, mode_2ch, mode_mch): + """Test selection sound mode.""" + player._get_2ch = Mock(return_value=mode_sel) # pylint: disable=W0212 + + await player.async_select_sound_mode(mode) + if mode_2ch: + state.set_decode_mode_2ch.assert_called_with(mode_2ch) + else: + state.set_decode_mode_2ch.assert_not_called() + + if mode_mch: + state.set_decode_mode_mch.assert_called_with(mode_mch) + else: + state.set_decode_mode_mch.assert_not_called() + + +async def test_volume_up(player, state): + """Test mute functionallity.""" + await player.async_volume_up() + state.inc_volume.assert_called_with() + player.async_schedule_update_ha_state.assert_called_with() + + +async def test_volume_down(player, state): + """Test mute functionallity.""" + await player.async_volume_down() + state.dec_volume.assert_called_with() + player.async_schedule_update_ha_state.assert_called_with() + + +@pytest.mark.parametrize( + "mode, mode_sel, mode_2ch, mode_mch", + [ + ("STEREO", True, DecodeMode2CH.STEREO, None), + ("STEREO_DOWNMIX", False, None, DecodeModeMCH.STEREO_DOWNMIX), + (None, False, None, None), + ], +) +async def test_sound_mode(player, state, mode, mode_sel, mode_2ch, mode_mch): + """Test selection sound mode.""" + player._get_2ch = Mock(return_value=mode_sel) # pylint: disable=W0212 + state.get_decode_mode_2ch.return_value = mode_2ch + state.get_decode_mode_mch.return_value = mode_mch + + assert player.sound_mode == mode + + +async def test_sound_mode_list(player, state): + """Test sound mode list.""" + player._get_2ch = Mock(return_value=True) # pylint: disable=W0212 + assert sorted(player.sound_mode_list) == sorted([x.name for x in DecodeMode2CH]) + player._get_2ch = Mock(return_value=False) # pylint: disable=W0212 + assert sorted(player.sound_mode_list) == sorted([x.name for x in DecodeModeMCH]) + + +async def test_sound_mode_zone_x(player, state): + """Test second zone sound mode.""" + state.zn = 2 + assert player.sound_mode is None + assert player.sound_mode_list is None + + +async def test_is_volume_muted(player, state): + """Test muted.""" + state.get_mute.return_value = True + assert player.is_volume_muted is True # pylint: disable=singleton-comparison + state.get_mute.return_value = False + assert player.is_volume_muted is False # pylint: disable=singleton-comparison + state.get_mute.return_value = None + assert player.is_volume_muted is None + + +async def test_volume_level(player, state): + """Test volume.""" + state.get_volume.return_value = 0 + assert isclose(player.volume_level, 0.0) + state.get_volume.return_value = 50 + assert isclose(player.volume_level, 50.0 / 99) + state.get_volume.return_value = 99 + assert isclose(player.volume_level, 1.0) + state.get_volume.return_value = None + assert player.volume_level is None + + +@pytest.mark.parametrize("volume, call", [(0.0, 0), (0.5, 50), (1.0, 99)]) +async def test_set_volume_level(player, state, volume, call): + """Test setting volume.""" + await player.async_set_volume_level(volume) + state.set_volume.assert_called_with(call) + + +@pytest.mark.parametrize( + "source, media_content_type", + [ + (SourceCodes.DAB, MEDIA_TYPE_MUSIC), + (SourceCodes.FM, MEDIA_TYPE_MUSIC), + (SourceCodes.PVR, None), + (None, None), + ], +) +async def test_media_content_type(player, state, source, media_content_type): + """Test content type deduction.""" + state.get_source.return_value = source + assert player.media_content_type == media_content_type + + +@pytest.mark.parametrize( + "source, dab, rds, channel", + [ + (SourceCodes.DAB, "dab", "rds", "dab"), + (SourceCodes.DAB, None, None, None), + (SourceCodes.FM, "dab", "rds", "rds"), + (SourceCodes.FM, None, None, None), + (SourceCodes.PVR, "dab", "rds", None), + ], +) +async def test_media_channel(player, state, source, dab, rds, channel): + """Test media channel.""" + state.get_dab_station.return_value = dab + state.get_rds_information.return_value = rds + state.get_source.return_value = source + assert player.media_channel == channel + + +@pytest.mark.parametrize( + "source, dls, artist", + [ + (SourceCodes.DAB, "dls", "dls"), + (SourceCodes.FM, "dls", None), + (SourceCodes.DAB, None, None), + ], +) +async def test_media_artist(player, state, source, dls, artist): + """Test media artist.""" + state.get_dls_pdt.return_value = dls + state.get_source.return_value = source + assert player.media_artist == artist + + +@pytest.mark.parametrize( + "source, channel, title", + [ + (SourceCodes.DAB, "channel", "DAB - channel"), + (SourceCodes.DAB, None, "DAB"), + (None, None, None), + ], +) +async def test_media_title(player, state, source, channel, title): + """Test media title.""" + state.get_source.return_value = source + with patch.object( + ArcamFmj, "media_channel", new_callable=PropertyMock + ) as media_channel: + media_channel.return_value = channel + assert player.media_title == title + + +async def test_added_to_hass(player, state): + """Test addition to hass.""" + connectors = {} + + def _connect(signal, fun): + connectors[signal] = fun + + player.hass = MagicMock() + player.hass.helpers.dispatcher.async_dispatcher_connect.side_effects = _connect + + await player.async_added_to_hass() + state.start.assert_called_with() + player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( + SIGNAL_CLIENT_DATA, ANY + ) + player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( + SIGNAL_CLIENT_STARTED, ANY + ) + player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( + SIGNAL_CLIENT_STOPPED, ANY + ) From 841ce8ade98a515aaed46f19edf9c46e97213d85 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Dec 2019 11:08:28 +0100 Subject: [PATCH 1983/3953] Fix uvloop warning (#29341) --- homeassistant/components/stream/__init__.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index a83f05820e2ea3..9304257f853b9f 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -23,11 +23,6 @@ from .core import PROVIDERS from .hls import async_setup_hls -try: - import uvloop -except ImportError: - uvloop = None - _LOGGER = logging.getLogger(__name__) @@ -42,7 +37,6 @@ 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) @@ -53,21 +47,6 @@ 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 = {} From e9647f88147b9ebb504b1571f73464ace569a67d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 3 Dec 2019 10:51:46 +0000 Subject: [PATCH 1984/3953] Update ozw 0.1.6 --- homeassistant/components/zwave/manifest.json | 9 ++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index 9268a50a14d78c..78362b25462e9b 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -3,12 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", - "requirements": [ - "homeassistant-pyozw==0.1.4", - "pydispatcher==2.0.5" - ], + "requirements": ["homeassistant-pyozw==0.1.6", "pydispatcher==2.0.5"], "dependencies": [], - "codeowners": [ - "@home-assistant/z-wave" - ] + "codeowners": ["@home-assistant/z-wave"] } diff --git a/requirements_all.txt b/requirements_all.txt index 75fc2dd5bbde4a..1302b2aeb78b0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,7 @@ holidays==0.9.11 home-assistant-frontend==20191119.6 # homeassistant.components.zwave -homeassistant-pyozw==0.1.4 +homeassistant-pyozw==0.1.6 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f47fd5317ed2e..bfe48e243778e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,7 +231,7 @@ holidays==0.9.11 home-assistant-frontend==20191119.6 # homeassistant.components.zwave -homeassistant-pyozw==0.1.4 +homeassistant-pyozw==0.1.6 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 078e90717853790b75d1e6c01483631347ea7936 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 12:23:41 +0100 Subject: [PATCH 1985/3953] Move imports to top for pushetta (#29332) * Move imports to top for pushetta * Make Pushetta.exceptions import lowercase and snakecase --- homeassistant/components/pushetta/notify.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/pushetta/notify.py b/homeassistant/components/pushetta/notify.py index b8911039f3f050..c9b008524d686a 100644 --- a/homeassistant/components/pushetta/notify.py +++ b/homeassistant/components/pushetta/notify.py @@ -1,17 +1,17 @@ """Pushetta platform for notify component.""" import logging +from pushetta import Pushetta, exceptions as pushetta_exceptions import voluptuous as vol -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -44,7 +44,6 @@ class PushettaNotificationService(BaseNotificationService): def __init__(self, api_key, channel_name, send_test_msg): """Initialize the service.""" - from pushetta import Pushetta self._api_key = api_key self._channel_name = channel_name @@ -56,15 +55,14 @@ def __init__(self, api_key, channel_name, send_test_msg): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from pushetta import exceptions title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) try: self.pushetta.pushMessage(self._channel_name, f"{title} {message}") - except exceptions.TokenValidationError: + except pushetta_exceptions.TokenValidationError: _LOGGER.error("Please check your access token") self.is_valid = False - except exceptions.ChannelNotFoundError: + except pushetta_exceptions.ChannelNotFoundError: _LOGGER.error("Channel '%s' not found", self._channel_name) self.is_valid = False From 25b674046662278a1c271725e7b0be79c9d53352 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 19:02:09 +0100 Subject: [PATCH 1986/3953] Move imports to top for pencom (#29348) --- homeassistant/components/pencom/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pencom/switch.py b/homeassistant/components/pencom/switch.py index 60e7ef30837f01..36266feaa6e945 100644 --- a/homeassistant/components/pencom/switch.py +++ b/homeassistant/components/pencom/switch.py @@ -5,10 +5,11 @@ """ import logging +from pencompy.pencompy import Pencompy import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_NAME +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -39,7 +40,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Pencom relay platform (pencompy).""" - from pencompy.pencompy import Pencompy # Assign configuration variables. host = config[CONF_HOST] From cae802f7ee2788769c047ae4f3bd7e5aaf555be0 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Tue, 3 Dec 2019 21:51:45 +0100 Subject: [PATCH 1987/3953] Include telegram_bot message id for all messages (#29315) * Include telegram_bot message id for all messages * Update __init__.py --- homeassistant/components/telegram_bot/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 7acf4985def3b5..d365060e204705 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -731,6 +731,8 @@ def _get_message_data(self, msg_data): ATTR_USER_ID: msg_data["from"]["id"], ATTR_FROM_FIRST: msg_data["from"]["first_name"], } + if "message_id" in msg_data: + data[ATTR_MSGID] = msg_data["message_id"] if "last_name" in msg_data["from"]: data[ATTR_FROM_LAST] = msg_data["from"]["last_name"] if "chat" in msg_data: @@ -752,6 +754,9 @@ def process_message(self, data): if event_data is None: return message_ok + if ATTR_MSGID in data: + event_data[ATTR_MSGID] = data[ATTR_MSGID] + if "text" in data: if data["text"][0] == "/": pieces = data["text"].split(" ") From 4f8200d15a60fb0ea8a3695863e048cbc0a776e6 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 21:54:25 +0100 Subject: [PATCH 1988/3953] Ignore state of climate entities in prometheus (#29346) --- homeassistant/components/prometheus/__init__.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 8eeb9325bc0a7d..71d56cda18a1ee 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -284,15 +284,6 @@ def _handle_climate(self, state): ) metric.labels(**self._labels(state)).set(current_temp) - metric = self._metric( - "climate_state", self.prometheus_cli.Gauge, "State of the thermostat (0/1)" - ) - try: - value = self.state_as_number(state) - metric.labels(**self._labels(state)).set(value) - except ValueError: - pass - def _handle_sensor(self, state): unit = self._unit_string(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) From b1b784484e1b767f7bbf48850a12b15491e47e2d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 3 Dec 2019 23:13:37 +0100 Subject: [PATCH 1989/3953] Move imports to top for owlet (#29352) --- homeassistant/components/owlet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py index f9543c7fa6e6d8..afde50cae49a02 100644 --- a/homeassistant/components/owlet/__init__.py +++ b/homeassistant/components/owlet/__init__.py @@ -1,6 +1,7 @@ """Support for Owlet baby monitors.""" import logging +from pyowlet.PyOwlet import PyOwlet import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -41,7 +42,6 @@ def setup(hass, config): """Set up owlet component.""" - from pyowlet.PyOwlet import PyOwlet username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] From eef91f843db7885bf0e1961a070aae7e19048a53 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Dec 2019 14:15:45 -0800 Subject: [PATCH 1990/3953] Rendering complex template objects to leave non-template values alone (#29353) --- homeassistant/helpers/config_validation.py | 5 +++-- homeassistant/helpers/template.py | 4 +++- tests/helpers/test_config_validation.py | 6 +++++- tests/helpers/test_template.py | 7 +++++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index c34b104e0f765c..948fb017d9d1f2 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -489,8 +489,9 @@ def template_complex(value): for key, element in return_value.items(): return_value[key] = template_complex(element) return return_value - - return template(value) + if isinstance(value, str): + return template(value) + return value def datetime(value): diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index f23d9cddfddfe0..7dcf08ebf92145 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -69,7 +69,9 @@ def render_complex(value, variables=None): return [render_complex(item, variables) for item in value] if isinstance(value, dict): return {key: render_complex(item, variables) for key, item in value.items()} - return value.async_render(variables) + if isinstance(value, Template): + return value.async_render(variables) + return value def extract_entities( diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 1f5d6ddfc402e7..57554d37bb16a1 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -395,7 +395,7 @@ def test_template_complex(): """Test template_complex validator.""" schema = vol.Schema(cv.template_complex) - for value in (None, "{{ partial_print }", "{% if True %}Hello"): + for value in ("{{ partial_print }", "{% if True %}Hello"): with pytest.raises(vol.MultipleInvalid): schema(value) @@ -420,6 +420,10 @@ def test_template_complex(): ["{{ beer }}", 1], ) + # Ensure we don't mutate non-string types that cannot be templates. + for value in (1, True, None): + assert schema(value) == value + def test_time_zone(): """Test time zone validation.""" diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index f2066ce2c6f0d7..f463149bc2820c 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1796,3 +1796,10 @@ def test_length_of_states(hass): tpl = template.Template("{{ states.sensor | length }}", hass) assert tpl.async_render() == "2" + + +def test_render_complex_handling_non_template_values(hass): + """Test that we can render non-template fields.""" + assert template.render_complex( + {True: 1, False: template.Template("{{ hello }}", hass)}, {"hello": 2} + ) == {True: 1, False: "2"} From f8ffcd6cafdca9e0e21cea52521d165eebf62626 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:15:42 +0100 Subject: [PATCH 1991/3953] Move imports to top for opple (#29372) --- homeassistant/components/opple/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/opple/light.py b/homeassistant/components/opple/light.py index 5a6657a323d429..9ee53704d10897 100644 --- a/homeassistant/components/opple/light.py +++ b/homeassistant/components/opple/light.py @@ -1,6 +1,7 @@ """Support for the Opple light.""" import logging +from pyoppleio.OppleLightDevice import OppleLightDevice import voluptuous as vol from homeassistant.components.light import ( @@ -46,7 +47,6 @@ class OppleLight(Light): def __init__(self, name, host): """Initialize an Opple light.""" - from pyoppleio.OppleLightDevice import OppleLightDevice self._device = OppleLightDevice(host) From f220c73e8f1d63ab80afbff693465edf773fd2cf Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:16:57 +0100 Subject: [PATCH 1992/3953] Move imports to top for orvibo (#29371) --- homeassistant/components/orvibo/switch.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/orvibo/switch.py b/homeassistant/components/orvibo/switch.py index 38d0d2c05d4b2d..75a95e053ae166 100644 --- a/homeassistant/components/orvibo/switch.py +++ b/homeassistant/components/orvibo/switch.py @@ -1,15 +1,16 @@ """Support for Orvibo S20 Wifi Smart Switches.""" import logging +from orvibo.s20 import S20, S20Exception, discover import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( + CONF_DISCOVERY, CONF_HOST, + CONF_MAC, CONF_NAME, CONF_SWITCHES, - CONF_MAC, - CONF_DISCOVERY, ) import homeassistant.helpers.config_validation as cv @@ -37,7 +38,6 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up S20 switches.""" - from orvibo.s20 import discover, S20, S20Exception switch_data = {} switches = [] @@ -67,7 +67,6 @@ class S20Switch(SwitchDevice): def __init__(self, name, s20): """Initialize the S20 device.""" - from orvibo.s20 import S20Exception self._name = name self._s20 = s20 From 655780447c775fc957f9875ea078d4238d0db1ba Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:19:43 +0100 Subject: [PATCH 1993/3953] Move imports to top for nut (#29368) --- homeassistant/components/nut/sensor.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index db485734777e50..34e3bfaf08611e 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -1,25 +1,26 @@ """Provides a sensor to track various status aspects of a UPS.""" -import logging from datetime import timedelta +import logging +from pynut2.nut2 import PyNUTClient, PyNUTError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( + ATTR_STATE, + CONF_ALIAS, CONF_HOST, - CONF_PORT, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, - TEMP_CELSIUS, + CONF_PORT, CONF_RESOURCES, - CONF_ALIAS, - ATTR_STATE, - STATE_UNKNOWN, + CONF_USERNAME, POWER_WATT, + STATE_UNKNOWN, + TEMP_CELSIUS, ) from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -270,7 +271,6 @@ class PyNUTData: def __init__(self, host, port, alias, username, password): """Initialize the data object.""" - from pynut2.nut2 import PyNUTClient, PyNUTError self._host = host self._port = port From bbf99c61fa8f58f6878611081252e76ee745c9a0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:42:21 +0100 Subject: [PATCH 1994/3953] Move imports to top for mycroft (#29355) --- homeassistant/components/mycroft/notify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mycroft/notify.py b/homeassistant/components/mycroft/notify.py index 93b724f97cda53..335eff875461fc 100644 --- a/homeassistant/components/mycroft/notify.py +++ b/homeassistant/components/mycroft/notify.py @@ -1,8 +1,9 @@ """Mycroft AI notification platform.""" import logging -from homeassistant.components.notify import BaseNotificationService +from mycroftapi import MycroftAPI +from homeassistant.components.notify import BaseNotificationService _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ def __init__(self, mycroft_ip): def send_message(self, message="", **kwargs): """Send a message mycroft to speak on instance.""" - from mycroftapi import MycroftAPI text = message mycroft = MycroftAPI(self.mycroft_ip) From 98b6905738b4041ff1f162d6cd8b7fe5879c5f5a Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Wed, 4 Dec 2019 00:42:45 +0100 Subject: [PATCH 1995/3953] Upgrade enturclient to 0.2.1 (#29375) --- homeassistant/components/entur_public_transport/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index 3f00743a1dbf71..6396ff8e678d17 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -3,7 +3,7 @@ "name": "Entur public transport", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", "requirements": [ - "enturclient==0.2.0" + "enturclient==0.2.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 1302b2aeb78b0a..48144e7c8393b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -472,7 +472,7 @@ emulated_roku==0.1.8 enocean==0.50 # homeassistant.components.entur_public_transport -enturclient==0.2.0 +enturclient==0.2.1 # homeassistant.components.environment_canada env_canada==0.0.30 From e26eebfc1915d778f70aaeadcc2d96e99115fc62 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Dec 2019 15:43:48 -0800 Subject: [PATCH 1996/3953] Remove cloud dependency from mobile_app (#29373) --- .../components/mobile_app/http_api.py | 23 ++++++++++--------- .../components/mobile_app/manifest.json | 14 +++-------- .../components/mobile_app/webhook.py | 10 ++++---- .../components/mobile_app/websocket_api.py | 3 +-- 4 files changed, 21 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index ee69f15fb11edc..11ca39e8b60014 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -6,11 +6,7 @@ from nacl.secret import SecretBox from homeassistant.auth.util import generate_secret -from homeassistant.components.cloud import ( - CloudNotAvailable, - async_create_cloudhook, - async_remote_ui_url, -) + from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED @@ -41,8 +37,12 @@ async def post(self, request: Request, data: Dict) -> Response: webhook_id = generate_secret() - if hass.components.cloud.async_active_subscription(): - data[CONF_CLOUDHOOK_URL] = await async_create_cloudhook(hass, webhook_id) + cloud_loaded = "cloud" in hass.config.components + + if cloud_loaded and hass.components.cloud.async_active_subscription(): + data[ + CONF_CLOUDHOOK_URL + ] = await hass.components.cloud.async_create_cloudhook(webhook_id) data[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "") @@ -59,10 +59,11 @@ async def post(self, request: Request, data: Dict) -> Response: ) remote_ui_url = None - try: - remote_ui_url = async_remote_ui_url(hass) - except CloudNotAvailable: - pass + if cloud_loaded: + try: + remote_ui_url = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass return self.json( { diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index ab140b4148e435..29ee35e002c0ad 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -3,15 +3,7 @@ "name": "Home Assistant Mobile App Support", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mobile_app", - "requirements": [ - "PyNaCl==1.3.0" - ], - "dependencies": [ - "cloud", - "http", - "webhook" - ], - "codeowners": [ - "@robbiet480" - ] + "requirements": ["PyNaCl==1.3.0"], + "dependencies": ["http", "webhook"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 416f6cd2ffebeb..98687e6658fdc7 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -4,7 +4,6 @@ from aiohttp.web import HTTPBadRequest, Request, Response import voluptuous as vol -from homeassistant.components.cloud import CloudNotAvailable, async_remote_ui_url from homeassistant.components.frontend import MANIFEST_JSON from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( @@ -310,9 +309,10 @@ async def handle_webhook( if CONF_CLOUDHOOK_URL in registration: resp[CONF_CLOUDHOOK_URL] = registration[CONF_CLOUDHOOK_URL] - try: - resp[CONF_REMOTE_UI_URL] = async_remote_ui_url(hass) - except CloudNotAvailable: - pass + if "cloud" in hass.config.components: + try: + resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass return webhook_response(resp, registration=registration, headers=headers) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index 813d0a9cf8930b..bc5305c36fa6bc 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -1,7 +1,6 @@ """Websocket API for mobile_app.""" import voluptuous as vol -from homeassistant.components.cloud import async_delete_cloudhook from homeassistant.components.websocket_api import ( ActiveConnection, async_register_command, @@ -117,6 +116,6 @@ async def websocket_delete_registration( return error_message(msg["id"], "internal_error", "Error deleting registration") if CONF_CLOUDHOOK_URL in registration and "cloud" in hass.config.components: - await async_delete_cloudhook(hass, webhook_id) + await hass.components.cloud.async_delete_cloudhook(webhook_id) connection.send_message(result_message(msg["id"], "ok")) From 9d213e70f006d27f04b5e4c69b8fec607eea015f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:44:04 +0100 Subject: [PATCH 1997/3953] Move imports to top for opensensemap (#29370) --- homeassistant/components/opensensemap/air_quality.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/opensensemap/air_quality.py b/homeassistant/components/opensensemap/air_quality.py index d525e807aed266..cf27f86cc9ffff 100644 --- a/homeassistant/components/opensensemap/air_quality.py +++ b/homeassistant/components/opensensemap/air_quality.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from opensensemap_api import OpenSenseMap +from opensensemap_api.exceptions import OpenSenseMapError import voluptuous as vol from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity @@ -26,7 +28,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the openSenseMap air quality platform.""" - from opensensemap_api import OpenSenseMap name = config.get(CONF_NAME) station_id = config[CONF_STATION_ID] @@ -88,7 +89,6 @@ def __init__(self, api): @Throttle(SCAN_INTERVAL) async def async_update(self): """Get the latest data from the Pi-hole.""" - from opensensemap_api.exceptions import OpenSenseMapError try: await self.api.get_data() From be316af303c865b64cbffa7905f7713e2f1708a5 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:44:50 +0100 Subject: [PATCH 1998/3953] Move imports to top for mystrom (#29356) --- homeassistant/components/mystrom/light.py | 19 ++++++++----------- homeassistant/components/mystrom/switch.py | 4 ++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index d878ee60302c2f..56fe369144bb71 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -1,21 +1,23 @@ """Support for myStrom Wifi bulbs.""" import logging +from pymystrom.bulb import MyStromBulb +from pymystrom.exceptions import MyStromConnectionError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - Light, - PLATFORM_SCHEMA, ATTR_BRIGHTNESS, + ATTR_EFFECT, + ATTR_HS_COLOR, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_EFFECT, - ATTR_EFFECT, SUPPORT_FLASH, - SUPPORT_COLOR, - ATTR_HS_COLOR, + Light, ) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -39,8 +41,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the myStrom Light platform.""" - from pymystrom.bulb import MyStromBulb - from pymystrom.exceptions import MyStromConnectionError host = config.get(CONF_HOST) mac = config.get(CONF_MAC) @@ -107,7 +107,6 @@ def is_on(self): def turn_on(self, **kwargs): """Turn on the light.""" - from pymystrom.exceptions import MyStromConnectionError brightness = kwargs.get(ATTR_BRIGHTNESS, 255) effect = kwargs.get(ATTR_EFFECT) @@ -136,7 +135,6 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn off the bulb.""" - from pymystrom.exceptions import MyStromConnectionError try: self._bulb.set_off() @@ -145,7 +143,6 @@ def turn_off(self, **kwargs): def update(self): """Fetch new state data for this light.""" - from pymystrom.exceptions import MyStromConnectionError try: self._state = self._bulb.get_status() diff --git a/homeassistant/components/mystrom/switch.py b/homeassistant/components/mystrom/switch.py index 0eca5598cc9f1f..3a045e0391dc63 100644 --- a/homeassistant/components/mystrom/switch.py +++ b/homeassistant/components/mystrom/switch.py @@ -3,8 +3,8 @@ import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv DEFAULT_NAME = "myStrom Switch" From c9c41260b22d4dedd09e5c4a6d1b3712a0019e2b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:45:24 +0100 Subject: [PATCH 1999/3953] Move imports to top for nello (#29361) --- homeassistant/components/nello/lock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nello/lock.py b/homeassistant/components/nello/lock.py index 3efe0a9cc5fc1b..19f8e7aa14c940 100644 --- a/homeassistant/components/nello/lock.py +++ b/homeassistant/components/nello/lock.py @@ -2,11 +2,12 @@ from itertools import filterfalse import logging +from pynello.private import Nello import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nello lock platform.""" - from pynello.private import Nello nello = Nello(config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) add_entities([NelloLock(lock) for lock in nello.locations], True) From 4989d1e7a988379bc60fc6cd29a944fabec1f0b2 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:45:36 +0100 Subject: [PATCH 2000/3953] Move imports to top for nederlandse_spoorwegen (#29360) --- homeassistant/components/nederlandse_spoorwegen/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 0741ed4cb496df..0b8239623730e5 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import logging +import ns_api import requests import voluptuous as vol @@ -46,7 +47,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the departure sensor.""" - import ns_api nsapi = ns_api.NSAPI(config.get(CONF_EMAIL), config.get(CONF_PASSWORD)) try: From 0e223662a96f2fa8b510e85a27398edaea946729 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:45:46 +0100 Subject: [PATCH 2001/3953] Move imports to top for nanoleaf (#29359) --- homeassistant/components/nanoleaf/light.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index e3f3cfbeab14b4..4b08d0b9751a40 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -1,6 +1,7 @@ """Support for Nanoleaf Lights.""" import logging +from pynanoleaf import Nanoleaf, Unavailable import voluptuous as vol from homeassistant.components.light import ( @@ -54,7 +55,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nanoleaf light.""" - from pynanoleaf import Nanoleaf, Unavailable if DATA_NANOLEAF not in hass.data: hass.data[DATA_NANOLEAF] = dict() @@ -222,7 +222,6 @@ def turn_off(self, **kwargs): def update(self): """Fetch new state data for this light.""" - from pynanoleaf import Unavailable try: self._available = self._light.available From 01bc1e4c662d5e4828d84ed16df411561aeec83e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:45:56 +0100 Subject: [PATCH 2002/3953] Move imports to top for nad (#29358) --- homeassistant/components/nad/media_player.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index 61003d980e1fd1..0c29aac427f0aa 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -1,10 +1,10 @@ """Support for interfacing with NAD receivers through RS-232.""" import logging +from nad_receiver import NADReceiver, NADReceiverTCP, NADReceiverTelnet import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -13,7 +13,8 @@ SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NAD platform.""" if config.get(CONF_TYPE) == "RS232": - from nad_receiver import NADReceiver - add_entities( [ NAD( @@ -79,8 +78,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): True, ) elif config.get(CONF_TYPE) == "Telnet": - from nad_receiver import NADReceiverTelnet - add_entities( [ NAD( @@ -94,8 +91,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): True, ) else: - from nad_receiver import NADReceiverTCP - add_entities( [ NADtcp( From b462c539eb89cc46f4e6d20b4d66421a041f13db Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:46:07 +0100 Subject: [PATCH 2003/3953] Move imports to top for n26 (#29357) --- homeassistant/components/n26/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/n26/__init__.py b/homeassistant/components/n26/__init__.py index e89d78a76f942c..f8379cb310f188 100644 --- a/homeassistant/components/n26/__init__.py +++ b/homeassistant/components/n26/__init__.py @@ -2,9 +2,9 @@ from datetime import datetime, timedelta, timezone import logging -import voluptuous as vol - from n26 import api as n26_api, config as n26_config +from requests import HTTPError +import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME import homeassistant.helpers.config_validation as cv @@ -51,8 +51,6 @@ def setup(hass, config): api = n26_api.Api(n26_config.Config(user, password)) - from requests import HTTPError - try: api.get_token() except HTTPError as err: From 4130f2ff1a756d7829e80c6b77c2ad9dd918dc0b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:46:16 +0100 Subject: [PATCH 2004/3953] Move imports to top for netdata (#29362) --- homeassistant/components/netdata/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index aab901506a8f40..edabef9535c4a3 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from netdata import Netdata +from netdata.exceptions import NetdataError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -53,7 +55,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Netdata sensor.""" - from netdata import Netdata name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -154,7 +155,6 @@ def __init__(self, api): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from the Netdata REST API.""" - from netdata.exceptions import NetdataError try: await self.api.get_allmetrics() From bc635120fa8b07fdf77f91e89a15d0a00353bde1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:46:26 +0100 Subject: [PATCH 2005/3953] Move imports to top for netio (#29363) --- homeassistant/components/netio/switch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/netio/switch.py b/homeassistant/components/netio/switch.py index 77af9a34d68769..4c9b6343f2b905 100644 --- a/homeassistant/components/netio/switch.py +++ b/homeassistant/components/netio/switch.py @@ -1,22 +1,23 @@ """The Netio switch component.""" -import logging from collections import namedtuple from datetime import timedelta +import logging +from pynetio import Netio import voluptuous as vol -from homeassistant.core import callback from homeassistant import util from homeassistant.components.http import HomeAssistantView +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, + CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP, STATE_ON, ) -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -50,7 +51,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Netio platform.""" - from pynetio import Netio host = config.get(CONF_HOST) username = config.get(CONF_USERNAME) From 564c468c268706b1a5a32c138f96fda1ccf60d8b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:46:38 +0100 Subject: [PATCH 2006/3953] Move imports to top for nmap_tracker (#29364) --- homeassistant/components/nmap_tracker/device_tracker.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 7998f8758269be..d41e80f17a2f09 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -1,19 +1,20 @@ """Support for scanning a network with nmap.""" -import logging from collections import namedtuple from datetime import timedelta +import logging from getmac import get_mac_address +from nmap import PortScanner, PortScannerError import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOSTS +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -91,8 +92,6 @@ def _update_info(self): """ _LOGGER.debug("Scanning...") - from nmap import PortScanner, PortScannerError - scanner = PortScanner() options = self._options From 3ddd20159cc181a1705c8e822111675df6fdfc8f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:46:48 +0100 Subject: [PATCH 2007/3953] Move imports to top for nmbs (#29365) --- homeassistant/components/nmbs/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 8b2182665f64ef..26802808c0a0f0 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -1,6 +1,7 @@ """Get ride details and liveboard details for NMBS (Belgian railway).""" import logging +from pyrail import iRail import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -64,7 +65,6 @@ def get_ride_duration(departure_time, arrival_time, delay=0): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NMBS sensor with iRail API.""" - from pyrail import iRail api_client = iRail() From bd1e5fce27369f1f0cfbd3d8eb093856f71b3396 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:47:00 +0100 Subject: [PATCH 2008/3953] Move imports to top for noaa_tides (#29366) --- homeassistant/components/noaa_tides/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index e5f31dba1568b3..063a163a8ab2f5 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import logging +from py_noaa import coops # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -109,7 +110,6 @@ def state(self): def update(self): """Get the latest data from NOAA Tides and Currents API.""" - from py_noaa import coops # pylint: disable=import-error begin = datetime.now() delta = timedelta(days=2) From 3205afe74e10b45ae49f0eac7f3edd2ef52eadca Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:47:11 +0100 Subject: [PATCH 2009/3953] Move imports to top for nuimo_controller (#29367) --- homeassistant/components/nuimo_controller/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nuimo_controller/__init__.py b/homeassistant/components/nuimo_controller/__init__.py index 8fa3897b735776..013c2caf23d6c4 100644 --- a/homeassistant/components/nuimo_controller/__init__.py +++ b/homeassistant/components/nuimo_controller/__init__.py @@ -3,10 +3,12 @@ import threading import time +# pylint: disable=import-error +from nuimo import NuimoController, NuimoDiscoveryManager import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -104,8 +106,6 @@ def stop(self, event): def _attach(self): """Create a Nuimo object from MAC address or discovery.""" - # pylint: disable=import-error - from nuimo import NuimoController, NuimoDiscoveryManager if self._nuimo: self._nuimo.disconnect() From 0416e5b0fc5e15a2bb2fcfb0566e2ac9a9c9e574 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 00:47:22 +0100 Subject: [PATCH 2010/3953] Move imports to top for openhome (#29369) --- homeassistant/components/openhome/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index fbcd5f3ba021f9..222c1d87ec0b9b 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -1,6 +1,8 @@ """Support for Openhome Devices.""" import logging +from openhomedevice.Device import Device + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, @@ -26,7 +28,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Openhome platform.""" - from openhomedevice.Device import Device if not discovery_info: return True From a1a131334a34e0756921e19510a5ee1e4a0cb23e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 4 Dec 2019 00:32:11 +0000 Subject: [PATCH 2011/3953] [ci skip] Translation update --- .../alarm_control_panel/.translations/bg.json | 7 ++++ .../alarm_control_panel/.translations/it.json | 7 ++++ .../alarm_control_panel/.translations/pl.json | 7 ++++ .../alarm_control_panel/.translations/ru.json | 4 ++ .../components/almond/.translations/bg.json | 8 +++- .../components/climate/.translations/bg.json | 17 ++++++++ .../components/deconz/.translations/bg.json | 24 +++++++++-- .../components/demo/.translations/bg.json | 5 +++ .../components/fan/.translations/bg.json | 16 +++++++ .../geonetnz_volcano/.translations/bg.json | 16 +++++++ .../hisense_aehw4a1/.translations/bg.json | 15 +++++++ .../homekit_controller/.translations/bg.json | 2 +- .../huawei_lte/.translations/bg.json | 5 ++- .../components/hue/.translations/bg.json | 2 +- .../components/lock/.translations/bg.json | 4 ++ .../lutron_caseta/.translations/bg.json | 5 +++ .../lutron_caseta/.translations/es.json | 5 +++ .../components/met/.translations/bg.json | 2 +- .../components/plex/.translations/bg.json | 3 +- .../components/plex/.translations/it.json | 1 + .../components/plex/.translations/pl.json | 1 + .../components/ps4/.translations/bg.json | 6 +-- .../components/starline/.translations/bg.json | 42 +++++++++++++++++++ .../components/starline/.translations/es.json | 42 +++++++++++++++++++ .../components/vacuum/.translations/bg.json | 16 +++++++ .../components/wled/.translations/bg.json | 26 ++++++++++++ 26 files changed, 275 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/climate/.translations/bg.json create mode 100644 homeassistant/components/demo/.translations/bg.json create mode 100644 homeassistant/components/fan/.translations/bg.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/bg.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/bg.json create mode 100644 homeassistant/components/lutron_caseta/.translations/bg.json create mode 100644 homeassistant/components/lutron_caseta/.translations/es.json create mode 100644 homeassistant/components/starline/.translations/bg.json create mode 100644 homeassistant/components/starline/.translations/es.json create mode 100644 homeassistant/components/vacuum/.translations/bg.json create mode 100644 homeassistant/components/wled/.translations/bg.json diff --git a/homeassistant/components/alarm_control_panel/.translations/bg.json b/homeassistant/components/alarm_control_panel/.translations/bg.json index 29700793770a78..a9342c8c477278 100644 --- a/homeassistant/components/alarm_control_panel/.translations/bg.json +++ b/homeassistant/components/alarm_control_panel/.translations/bg.json @@ -6,6 +6,13 @@ "arm_night": "\u0421\u043b\u043e\u0436\u0438 {entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 \u0432 \u043d\u043e\u0449\u0435\u043d \u0440\u0435\u0436\u0438\u043c", "disarm": "\u0414\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0439 {entity_name}", "trigger": "\u0417\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0435 {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430", + "armed_home": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u0432\u043a\u044a\u0449\u0438", + "armed_night": "{entity_name} \u043f\u043e\u0434 \u043e\u0445\u0440\u0430\u043d\u0430 - \u043d\u043e\u0449", + "disarmed": "{entity_name} \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0430", + "triggered": "{entity_name} \u0437\u0430\u0434\u0435\u0439\u0441\u0442\u0432\u0430\u043d\u0430" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/it.json b/homeassistant/components/alarm_control_panel/.translations/it.json index e39967e9dacc09..78a3f0b07e5fe3 100644 --- a/homeassistant/components/alarm_control_panel/.translations/it.json +++ b/homeassistant/components/alarm_control_panel/.translations/it.json @@ -6,6 +6,13 @@ "arm_night": "Armare {entity_name} notte", "disarm": "Disarmare {entity_name}", "trigger": "Attivazione {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} armata modalit\u00e0 fuori casa", + "armed_home": "{entity_name} armata modalit\u00e0 a casa", + "armed_night": "{entity_name} armata modalit\u00e0 notte", + "disarmed": "{entity_name} disarmato", + "triggered": "{entity_name} attivato" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pl.json b/homeassistant/components/alarm_control_panel/.translations/pl.json index a5dc326c267353..024a0861c1c03f 100644 --- a/homeassistant/components/alarm_control_panel/.translations/pl.json +++ b/homeassistant/components/alarm_control_panel/.translations/pl.json @@ -6,6 +6,13 @@ "arm_night": "uzbr\u00f3j (noc) {entity_name}", "disarm": "rozbr\u00f3j {entity_name}", "trigger": "wyzw\u00f3l {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} zostanie uzbrojony (poza domem)", + "armed_home": "{entity_name} zostanie uzbrojony (w domu)", + "armed_night": "{entity_name} zostanie uzbrojony (noc)", + "disarmed": "{entity_name} zostanie rozbrojony", + "triggered": "{entity_name} zostanie wyzwolony" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/ru.json b/homeassistant/components/alarm_control_panel/.translations/ru.json index 79d04c8855f736..f9a0e859e1113c 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ru.json +++ b/homeassistant/components/alarm_control_panel/.translations/ru.json @@ -8,6 +8,10 @@ "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" }, "trigger_type": { + "armed_away": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "armed_home": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "armed_night": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "disarmed": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "triggered": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" } } diff --git a/homeassistant/components/almond/.translations/bg.json b/homeassistant/components/almond/.translations/bg.json index da5571ad0294b7..3327e34e76581e 100644 --- a/homeassistant/components/almond/.translations/bg.json +++ b/homeassistant/components/almond/.translations/bg.json @@ -2,7 +2,13 @@ "config": { "abort": { "already_setup": "\u041c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u0438\u043d Almond \u0430\u043a\u0430\u0443\u043d\u0442.", - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430." + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430.", + "missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond." + }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + } }, "title": "Almond" } diff --git a/homeassistant/components/climate/.translations/bg.json b/homeassistant/components/climate/.translations/bg.json new file mode 100644 index 00000000000000..d7901d2988443a --- /dev/null +++ b/homeassistant/components/climate/.translations/bg.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "\u041f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u043d\u0430 \u041e\u0412\u041a \u043d\u0430 {entity_name}", + "set_preset_mode": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438 \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043d\u0430 {entity_name}" + }, + "condtion_type": { + "is_hvac_mode": "{entity_name} \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0447\u0435\u043d \u041e\u0412\u041a \u0440\u0435\u0436\u0438\u043c", + "is_preset_mode": "{entity_name} \u0435 \u0432 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u043d \u0440\u0435\u0436\u0438\u043c" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0430\u0442\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "current_temperature_changed": "{entity_name} \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0430\u0442\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438", + "hvac_mode_changed": "{entity_name} \u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u041e\u0412\u041a \u0441\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index e8dc5845c134a4..fb75fc81f5fe01 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -24,12 +24,12 @@ "init": { "data": { "host": "\u0410\u0434\u0440\u0435\u0441", - "port": "\u041f\u043e\u0440\u0442 (\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: '80')" + "port": "\u041f\u043e\u0440\u0442" }, "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0448\u043b\u044e\u0437" }, "link": { - "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 deCONZ\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Unlock Gateway\"", + "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 deCONZ Settings -> Gateway -> Advanced\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Authenticate app\"", "title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ" }, "options": { @@ -40,7 +40,7 @@ "title": "\u0414\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0438 \u043e\u043f\u0446\u0438\u0438 \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ" } }, - "title": "deCONZ" + "title": "deCONZ Zigbee \u0448\u043b\u044e\u0437" }, "device_automation": { "trigger_subtype": { @@ -55,10 +55,17 @@ "left": "\u041b\u044f\u0432\u043e", "open": "\u041e\u0442\u0432\u0430\u0440\u044f\u043d\u0435", "right": "\u0414\u044f\u0441\u043d\u043e", + "side_1": "\u0421\u0442\u0440\u0430\u043d\u0430 1", + "side_2": "\u0421\u0442\u0440\u0430\u043d\u0430 2", + "side_3": "\u0421\u0442\u0440\u0430\u043d\u0430 3", + "side_4": "\u0421\u0442\u0440\u0430\u043d\u0430 4", + "side_5": "\u0421\u0442\u0440\u0430\u043d\u0430 5", + "side_6": "\u0421\u0442\u0440\u0430\u043d\u0430 6", "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438" }, "trigger_type": { + "remote_awakened": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0441\u0435 \u0441\u044a\u0431\u0443\u0434\u0438", "remote_button_double_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0434\u0432\u0443\u043a\u0440\u0430\u0442\u043d\u043e", "remote_button_long_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e", "remote_button_long_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442 \u0441\u043b\u0435\u0434 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435\u043b\u043d\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u0435", @@ -69,7 +76,16 @@ "remote_button_short_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442", "remote_button_short_release": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043e\u0442\u043f\u0443\u0441\u043d\u0430\u0442", "remote_button_triple_press": "\"{subtype}\" \u0431\u0443\u0442\u043e\u043d\u044a\u0442 \u0431\u0435\u0448\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0430\u0442 \u0442\u0440\u0438\u043a\u0440\u0430\u0442\u043d\u043e", - "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e" + "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \"{subtype}\" \u0435 \u043f\u043e\u0447\u0443\u043a\u0430\u043d\u043e \u0434\u0432\u0430 \u043f\u044a\u0442\u0438", + "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043f\u0430\u0434\u0430", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0440\u0430\u0437\u043a\u043b\u0430\u0442\u0435\u043d\u043e", + "remote_moved": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u043f\u0440\u0435\u043c\u0435\u0441\u0442\u0435\u043d\u043e \u0441 \"{subtype}\" \u043d\u0430\u0433\u043e\u0440\u0435", + "remote_rotate_from_side_1": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 1\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_2": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 2\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_3": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 3\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_4": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 4\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_5": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 5\" \u043a\u044a\u043c \" {subtype} \"", + "remote_rotate_from_side_6": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0435 \u0437\u0430\u0432\u044a\u0440\u0442\u044f\u043d\u043e \u043e\u0442 \"\u0441\u0442\u0440\u0430\u043d\u0430 6\" \u043a\u044a\u043c \" {subtype} \"" } }, "options": { diff --git a/homeassistant/components/demo/.translations/bg.json b/homeassistant/components/demo/.translations/bg.json new file mode 100644 index 00000000000000..3b1f5f8a8d235a --- /dev/null +++ b/homeassistant/components/demo/.translations/bg.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u044f" + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/bg.json b/homeassistant/components/fan/.translations/bg.json new file mode 100644 index 00000000000000..62452e67179f2a --- /dev/null +++ b/homeassistant/components/fan/.translations/bg.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" + }, + "condtion_type": { + "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d" + }, + "trigger_type": { + "turned_off": "{entity_name} \u0431\u044a\u0434\u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "turned_on": "{entity_name} \u0431\u044a\u0434\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/bg.json b/homeassistant/components/geonetnz_volcano/.translations/bg.json new file mode 100644 index 00000000000000..f895d282902557 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/bg.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0437\u0430 \u0444\u0438\u043b\u0442\u044a\u0440\u0430 \u0441\u0438." + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/bg.json b/homeassistant/components/hisense_aehw4a1/.translations/bg.json new file mode 100644 index 00000000000000..c758e9cc20d715 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/bg.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0412 \u043c\u0440\u0435\u0436\u0430\u0442\u0430 \u043d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Hisense AEH-W4A1.", + "single_instance_allowed": "\u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/bg.json b/homeassistant/components/homekit_controller/.translations/bg.json index f8ce05b4bbe2e7..b1909ca2ec00cc 100644 --- a/homeassistant/components/homekit_controller/.translations/bg.json +++ b/homeassistant/components/homekit_controller/.translations/bg.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "\u041a\u043e\u0434 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, - "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 HomeKit \u043a\u043e\u0434\u0430 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 HomeKit \u043a\u043e\u0434\u0430 \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 (\u0432\u044a\u0432 \u0444\u043e\u0440\u043c\u0430\u0442 XXX-XX-XXX) \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0442\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 HomeKit \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "user": { diff --git a/homeassistant/components/huawei_lte/.translations/bg.json b/homeassistant/components/huawei_lte/.translations/bg.json index de5cbb32b79704..44746468b351ad 100644 --- a/homeassistant/components/huawei_lte/.translations/bg.json +++ b/homeassistant/components/huawei_lte/.translations/bg.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "already_in_progress": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0435\u0447\u0435 \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", + "not_huawei_lte": "\u041d\u0435 \u0435 Huawei LTE \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "error": { "connection_failed": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e", + "connection_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435", "incorrect_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u0430 \u043f\u0430\u0440\u043e\u043b\u0430", "incorrect_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "incorrect_username_or_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/hue/.translations/bg.json b/homeassistant/components/hue/.translations/bg.json index 04ee6d13831448..5f28f4bde40ad8 100644 --- a/homeassistant/components/hue/.translations/bg.json +++ b/homeassistant/components/hue/.translations/bg.json @@ -26,6 +26,6 @@ "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0445\u044a\u0431" } }, - "title": "\u0411\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/bg.json b/homeassistant/components/lock/.translations/bg.json index 0e77bcf10330d3..54b80842f4f277 100644 --- a/homeassistant/components/lock/.translations/bg.json +++ b/homeassistant/components/lock/.translations/bg.json @@ -8,6 +8,10 @@ "condition_type": { "is_locked": "{entity_name} \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", "is_unlocked": "{entity_name} \u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d" + }, + "trigger_type": { + "locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", + "unlocked": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/bg.json b/homeassistant/components/lutron_caseta/.translations/bg.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/bg.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/es.json b/homeassistant/components/lutron_caseta/.translations/es.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/es.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/bg.json b/homeassistant/components/met/.translations/bg.json index aabb1aeda3f400..aa85bed1d13055 100644 --- a/homeassistant/components/met/.translations/bg.json +++ b/homeassistant/components/met/.translations/bg.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" + "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430" }, "step": { "user": { diff --git a/homeassistant/components/plex/.translations/bg.json b/homeassistant/components/plex/.translations/bg.json index 9a2ffe299c8cf9..adfdd98ebaff79 100644 --- a/homeassistant/components/plex/.translations/bg.json +++ b/homeassistant/components/plex/.translations/bg.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", "discovery_no_file": "\u041d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u0441\u0442\u0430\u0440 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u0444\u0430\u0439\u043b", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430", + "non-interactive": "\u041d\u0435\u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u0435\u043d \u0438\u043c\u043f\u043e\u0440\u0442", "token_request_timeout": "\u0418\u0437\u0442\u0435\u0447\u0435 \u0432\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", "unknown": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0440\u0430\u0434\u0438 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, @@ -42,7 +43,7 @@ "manual_setup": "\u0420\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "token": "Plex \u043a\u043e\u0434" }, - "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043a\u043e\u0434 \u0437\u0430 Plex \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043b\u0438 \u0440\u044a\u0447\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u044a\u0440\u0432\u044a\u0440.", + "description": "\u041f\u0440\u043e\u0434\u044a\u043b\u0436\u0435\u0442\u0435 \u0441 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 plex.tv \u0438\u043b\u0438 \u0440\u044a\u0447\u043d\u043e \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u044a\u0440\u0432\u044a\u0440.", "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 Plex \u0441\u044a\u0440\u0432\u044a\u0440" } }, diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 8f61f968dba8c4..06c20660fef7d7 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex \u00e8 in fase di configurazione", "discovery_no_file": "Nessun file di configurazione legacy trovato", "invalid_import": "La configurazione importata non \u00e8 valida", + "non-interactive": "Importazione non interattiva", "token_request_timeout": "Timeout per l'ottenimento del token", "unknown": "Non riuscito per motivo sconosciuto" }, diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index b4ed6134106b6d..d752899b9f0e76 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex jest konfigurowany", "discovery_no_file": "Nie znaleziono pliku konfiguracyjnego", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", + "non-interactive": "Nieinteraktywny import", "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena.", "unknown": "Nieznany b\u0142\u0105d" }, diff --git a/homeassistant/components/ps4/.translations/bg.json b/homeassistant/components/ps4/.translations/bg.json index 4bea40b206a6e0..fabd9032dc0c0a 100644 --- a/homeassistant/components/ps4/.translations/bg.json +++ b/homeassistant/components/ps4/.translations/bg.json @@ -4,8 +4,8 @@ "credential_error": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0438\u0437\u0432\u043b\u0438\u0447\u0430\u043d\u0435 \u043d\u0430 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438.", "devices_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441\u0430 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438.", "no_devices_found": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 PlayStation 4 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430.", - "port_987_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 987.", - "port_997_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 997." + "port_987_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 987. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438\u0442\u0435] (https://www.home-assistant.io/components/ps4/) \u0437\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f.", + "port_997_bind_error": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0440\u0435\u0437\u0435\u0440\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0440\u0442 997. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430](https://www.home-assistant.io/components/ps4/) \u0437\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f." }, "error": { "credential_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0443\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \"\u0418\u0437\u043f\u0440\u0430\u0449\u0430\u043d\u0435\" \u0437\u0430 \u0434\u0430 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0442\u0435.", @@ -25,7 +25,7 @@ "name": "\u0418\u043c\u0435", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, - "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f. \u0417\u0430 \u201ePIN\u201c \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u0432 \u201eSettings\u201c \u043d\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u043a\u043e\u043d\u0437\u043e\u043b\u0430. \u0421\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u043f\u0440\u0435\u043c\u0438\u043d\u0435\u0442\u0435 \u043a\u044a\u043c \u201eMobile App Connection Settings\u201c \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u201eAdd Device\u201c. \u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f PIN \u043a\u043e\u0434.", + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f. \u0417\u0430 \u201ePIN\u201c \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u0432 \u201eSettings\u201c \u043d\u0430 \u0412\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u043a\u043e\u043d\u0437\u043e\u043b\u0430. \u0421\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u043f\u0440\u0435\u043c\u0438\u043d\u0435\u0442\u0435 \u043a\u044a\u043c \u201eMobile App Connection Settings\u201c \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u201eAdd Device\u201c. \u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f PIN \u043a\u043e\u0434. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\u0442\u0435 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430](https://www.home-assistant.io/components/ps4/) \u0437\u0430 \u0434\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/starline/.translations/bg.json b/homeassistant/components/starline/.translations/bg.json new file mode 100644 index 00000000000000..702c061c6294c5 --- /dev/null +++ b/homeassistant/components/starline/.translations/bg.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e ID \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438\u043b\u0438 \u0442\u0430\u0439\u043d\u0430", + "error_auth_mfa": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u043a\u043e\u0434", + "error_auth_user": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "app_secret": "\u0422\u0430\u0439\u043d\u0430" + }, + "description": "\u0418\u0414 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0438 \u0442\u0430\u0435\u043d \u043a\u043e\u0434 \u043e\u0442 StarLine \u0430\u043a\u0430\u0443\u043d\u0442 \u043d\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a", + "title": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u041a\u043e\u0434 \u043e\u0442 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435\u0442\u043e" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS \u043a\u043e\u0434" + }, + "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043a\u043e\u0434\u0430, \u0438\u0437\u043f\u0440\u0430\u0442\u0435\u043d \u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0435\u043d \u043d\u043e\u043c\u0435\u0440 {phone_number}", + "title": "\u0414\u0432\u0443\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" + }, + "auth_user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "\u0418\u043c\u0435\u0439\u043b \u0438 \u043f\u0430\u0440\u043e\u043b\u0430 \u0437\u0430 \u0430\u043a\u0430\u0443\u043d\u0442 \u0432 StarLine", + "title": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/es.json b/homeassistant/components/starline/.translations/es.json new file mode 100644 index 00000000000000..bc881ced6a2eaf --- /dev/null +++ b/homeassistant/components/starline/.translations/es.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Id de aplicaci\u00f3n o secreto incorrectos", + "error_auth_mfa": "C\u00f3digo incorrecto", + "error_auth_user": "Nombre de usuario o contrase\u00f1a incorrectos" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID de la aplicaci\u00f3n", + "app_secret": "Secreto" + }, + "description": "ID de la aplicaci\u00f3n y c\u00f3digo secreto de la cuenta de desarrollador de StarLine", + "title": "Credenciales de la aplicaci\u00f3n" + }, + "auth_captcha": { + "data": { + "captcha_code": "C\u00f3digo de la imagen" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "C\u00f3digo SMS" + }, + "description": "Introduce el c\u00f3digo enviado al tel\u00e9fono {phone_number}", + "title": "Autorizaci\u00f3n de dos factores" + }, + "auth_user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "description": "Correo electr\u00f3nico y contrase\u00f1a de la cuenta StarLine", + "title": "Credenciales de usuario" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/bg.json b/homeassistant/components/vacuum/.translations/bg.json new file mode 100644 index 00000000000000..2d422284a38915 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/bg.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "\u041d\u0435\u043a\u0430 {entity_name} \u043f\u043e\u0447\u0438\u0441\u0442\u0438", + "dock": "\u041d\u0435\u043a\u0430 {entity_name} \u0434\u0430 \u0441\u0435 \u0432\u044a\u0440\u043d\u0435 \u0432 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + }, + "condtion_type": { + "is_cleaning": "{entity_name} \u043f\u043e\u0447\u0438\u0441\u0442\u0432\u0430", + "is_docked": "{entity_name} \u0435 \u0432 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + }, + "trigger_type": { + "cleaning": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043f\u043e\u0447\u0438\u0441\u0442\u0432\u0430\u043d\u0435", + "docked": "{entity_name} \u0432 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/bg.json b/homeassistant/components/wled/.translations/bg.json new file mode 100644 index 00000000000000..d99df20187f732 --- /dev/null +++ b/homeassistant/components/wled/.translations/bg.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0422\u043e\u0432\u0430 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e.", + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + }, + "error": { + "connection_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "\u0410\u0434\u0440\u0435\u0441" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0432\u0430\u0448\u0438\u044f WLED \u0434\u0430 \u0441\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430 \u0441 Home Assistant.", + "title": "\u0421\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0412\u0430\u0448\u0438\u044f WLED" + }, + "zeroconf_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u0435 WLED \u0441 \u0438\u043c\u0435 {name} `\u043a\u044a\u043c Home Assistant?", + "title": "\u041e\u0442\u043a\u0440\u0438\u0442\u043e \u0435 WLED \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + }, + "title": "WLED" + } +} \ No newline at end of file From f6780c1fa209f117efdc7c533bb578b09b109edf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Dec 2019 22:39:12 -0800 Subject: [PATCH 2012/3953] Allow negative altitude in location updates (#29381) --- homeassistant/components/mobile_app/const.py | 2 +- tests/components/mobile_app/conftest.py | 4 +++- tests/components/mobile_app/test_webhook.py | 20 ++++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 0b6a93a39ea4cb..318076d5fd933d 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -160,7 +160,7 @@ vol.Required(ATTR_GPS_ACCURACY): cv.positive_int, vol.Optional(ATTR_BATTERY): cv.positive_int, vol.Optional(ATTR_SPEED): cv.positive_int, - vol.Optional(ATTR_ALTITUDE): cv.positive_int, + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), vol.Optional(ATTR_COURSE): cv.positive_int, vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, } diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index 33af3c2b4a76fd..1d653b73ba3a6c 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -18,7 +18,7 @@ def registry(hass): @pytest.fixture -async def create_registrations(authed_api_client): +async def create_registrations(hass, authed_api_client): """Return two new registrations.""" enc_reg = await authed_api_client.post( "/api/mobile_app/registrations", json=REGISTER @@ -34,6 +34,8 @@ async def create_registrations(authed_api_client): assert clear_reg.status == 201 clear_reg_json = await clear_reg.json() + await hass.async_block_till_done() + return (enc_reg_json, clear_reg_json) diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 1ea2d50d8d8208..6e8efe15dd0d2e 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -216,3 +216,23 @@ async def test_webhook_requires_encryption(webhook_client, create_registrations) assert "error" in webhook_json assert webhook_json["success"] is False assert webhook_json["error"]["code"] == "encryption_required" + + +async def test_webhook_update_location(hass, webhook_client, create_registrations): + """Test that encrypted registrations only accept encrypted data.""" + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + json={ + "type": "update_location", + "data": {"gps": [1, 2], "gps_accuracy": 10, "altitude": -10}, + }, + ) + + assert resp.status == 200 + + state = hass.states.get("device_tracker.test_1_2") + assert state is not None + assert state.attributes["latitude"] == 1.0 + assert state.attributes["longitude"] == 2.0 + assert state.attributes["gps_accuracy"] == 10 + assert state.attributes["altitude"] == -10 From 09e2be02d3702d291793521eaa8fe2c60c0fbf71 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 10:49:33 +0100 Subject: [PATCH 2013/3953] Move imports to top for lametric (#29406) --- homeassistant/components/lametric/__init__.py | 2 +- homeassistant/components/lametric/notify.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index a24ad79104d080..9281affa492aa5 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -1,6 +1,7 @@ """Support for LaMetric time.""" import logging +from lmnotify import LaMetricManager import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -50,7 +51,6 @@ class HassLaMetricManager: def __init__(self, client_id, client_secret): """Initialize HassLaMetricManager and connect to LaMetric.""" - from lmnotify import LaMetricManager _LOGGER.debug("Connecting to LaMetric") self.manager = LaMetricManager(client_id, client_secret) diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index 901fb07fc558d8..b8dd610b1a0f8c 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -1,6 +1,8 @@ """Support for LaMetric notifications.""" import logging +from lmnotify import Model, SimpleFrame, Sound +from oauthlib.oauth2 import TokenExpiredError from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol @@ -59,8 +61,6 @@ def __init__(self, hasslametricmanager, icon, lifetime, cycles, priority): def send_message(self, message="", **kwargs): """Send a message to some LaMetric device.""" - from lmnotify import SimpleFrame, Sound, Model - from oauthlib.oauth2 import TokenExpiredError targets = kwargs.get(ATTR_TARGET) data = kwargs.get(ATTR_DATA) From 2df189b58e340c9da256b6fca8cc13c57005338a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 10:49:55 +0100 Subject: [PATCH 2014/3953] Move imports to top for lacrosse (#29405) --- homeassistant/components/lacrosse/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index ccfd647d746baf..b8bde797b39b93 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import pylacrosse +from serial import SerialException import voluptuous as vol from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA @@ -61,8 +63,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LaCrosse sensors.""" - import pylacrosse - from serial import SerialException usb_device = config.get(CONF_DEVICE) baud = int(config.get(CONF_BAUD)) From fd107f2b2ead5da495dd28fc1013301013d3152e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 10:50:22 +0100 Subject: [PATCH 2015/3953] Move imports to top for kwb (#29404) --- homeassistant/components/kwb/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index 49815faf7aed44..b7872ca1ab4c5c 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -1,18 +1,19 @@ """Support for KWB Easyfire.""" import logging +from pykwb import kwb import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, - CONF_PORT, CONF_DEVICE, + CONF_HOST, CONF_NAME, + CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -56,8 +57,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): raw = config.get(CONF_RAW) client_name = config.get(CONF_NAME) - from pykwb import kwb - if connection_type == "serial": easyfire = kwb.KWBEasyfire(MODE_SERIAL, "", 0, device) elif connection_type == "tcp": From 1d65670a71d32ee41f0780c9a807ece5d7ecd885 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 10:50:44 +0100 Subject: [PATCH 2016/3953] Move imports to top for kiwi (#29403) --- homeassistant/components/kiwi/lock.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 4949ceeb1d823d..b13906b44f5cb0 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -1,21 +1,22 @@ """Support for the KIWI.KI lock platform.""" import logging +from kiwiki import KiwiClient, KiwiException import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, ATTR_ID, - ATTR_LONGITUDE, ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_PASSWORD, + CONF_USERNAME, STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.helpers.event import async_call_later from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_call_later _LOGGER = logging.getLogger(__name__) @@ -32,7 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the KIWI lock platform.""" - from kiwiki import KiwiClient, KiwiException try: kiwi = KiwiClient(config[CONF_USERNAME], config[CONF_PASSWORD]) @@ -98,7 +98,6 @@ def clear_unlock_state(self, _): def unlock(self, **kwargs): """Unlock the device.""" - from kiwiki import KiwiException try: self._client.open_door(self.lock_id) From c0c1d688588d0e6155984ef6d6763021e4ac4bc4 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:10:28 +0100 Subject: [PATCH 2017/3953] Move imports to top for iota (#29399) --- homeassistant/components/iota/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iota/__init__.py b/homeassistant/components/iota/__init__.py index 900dd028e6a473..497e94a08d6bf8 100644 --- a/homeassistant/components/iota/__init__.py +++ b/homeassistant/components/iota/__init__.py @@ -1,7 +1,8 @@ """Support for IOTA wallets.""" -import logging from datetime import timedelta +import logging +from iota import Iota import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -77,6 +78,5 @@ def device_state_attributes(self): @property def api(self): """Construct API object for interaction with the IRI node.""" - from iota import Iota return Iota(adapter=self.iri, seed=self._seed) From c3453be17b3e6b2fb13d8e761f9185b30bda4219 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:11:21 +0100 Subject: [PATCH 2018/3953] Move imports to top for lauch_library (#29383) --- homeassistant/components/launch_library/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index 14a75704312db9..32335526194bca 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -2,13 +2,14 @@ from datetime import timedelta import logging +from pylaunches.api import Launches import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME -from homeassistant.helpers.entity import Entity from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -25,7 +26,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the launch sensor.""" - from pylaunches.api import Launches name = config[CONF_NAME] From 3dc629db08d11cbc4913a41cc4511dcd431754c1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:11:42 +0100 Subject: [PATCH 2019/3953] Move imports to top for lg_netcast (#29384) --- homeassistant/components/lg_netcast/media_player.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lg_netcast/media_player.py b/homeassistant/components/lg_netcast/media_player.py index d11887d8a8a4fe..0be51c337e865a 100644 --- a/homeassistant/components/lg_netcast/media_player.py +++ b/homeassistant/components/lg_netcast/media_player.py @@ -2,11 +2,12 @@ from datetime import timedelta import logging +from pylgnetcast import LgNetCastClient, LgNetCastError from requests import RequestException import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -57,7 +58,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LG TV platform.""" - from pylgnetcast import LgNetCastClient host = config.get(CONF_HOST) access_token = config.get(CONF_ACCESS_TOKEN) @@ -87,7 +87,6 @@ def __init__(self, client, name): def send_command(self, command): """Send remote control commands to the TV.""" - from pylgnetcast import LgNetCastError try: with self._client as client: @@ -98,7 +97,6 @@ def send_command(self, command): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): """Retrieve the latest data from the LG TV.""" - from pylgnetcast import LgNetCastError try: with self._client as client: From cfe920079c0feef68373c8e6026fad29927ba10e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:12:03 +0100 Subject: [PATCH 2020/3953] Move imports to top for lightwave (#29385) --- homeassistant/components/lightwave/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lightwave/__init__.py b/homeassistant/components/lightwave/__init__.py index f3445a3c94ab63..4a27d4a7f4a2c4 100644 --- a/homeassistant/components/lightwave/__init__.py +++ b/homeassistant/components/lightwave/__init__.py @@ -1,7 +1,9 @@ """Support for device connected via Lightwave WiFi-link hub.""" +from lightwave.lightwave import LWLink import voluptuous as vol -import homeassistant.helpers.config_validation as cv + from homeassistant.const import CONF_HOST, CONF_LIGHTS, CONF_NAME, CONF_SWITCHES +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform LIGHTWAVE_LINK = "lightwave_link" @@ -32,7 +34,6 @@ async def async_setup(hass, config): """Try to start embedded Lightwave broker.""" - from lightwave.lightwave import LWLink host = config[DOMAIN][CONF_HOST] hass.data[LIGHTWAVE_LINK] = LWLink(host) From 7aab0d0aa03be9c1c357be70586e6af7a5adc9db Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:12:32 +0100 Subject: [PATCH 2021/3953] Move imports to top for limitlessled (#29386) --- .../components/limitlessled/light.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index be320ee4307773..e0ef635ae87bc5 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -1,9 +1,16 @@ """Support for LimitlessLED bulbs.""" import logging +from limitlessled import Color +from limitlessled.bridge import Bridge +from limitlessled.group.dimmer import DimmerGroup +from limitlessled.group.rgbw import RgbwGroup +from limitlessled.group.rgbww import RgbwwGroup +from limitlessled.group.white import WhiteGroup +from limitlessled.pipeline import Pipeline +from limitlessled.presets import COLORLOOP import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT, CONF_TYPE, STATE_ON from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -14,18 +21,19 @@ EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_COLOR, SUPPORT_TRANSITION, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TYPE, STATE_ON import homeassistant.helpers.config_validation as cv -from homeassistant.util.color import color_temperature_mired_to_kelvin, color_hs_to_RGB from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util.color import color_hs_to_RGB, color_temperature_mired_to_kelvin _LOGGER = logging.getLogger(__name__) @@ -137,7 +145,6 @@ def rewrite_legacy(config): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the LimitlessLED lights.""" - from limitlessled.bridge import Bridge # Two legacy configuration formats are supported to maintain backwards # compatibility. @@ -172,7 +179,6 @@ def decorator(function): # pylint: disable=protected-access def wrapper(self, **kwargs): """Wrap a group state change.""" - from limitlessled.pipeline import Pipeline pipeline = Pipeline() transition_time = DEFAULT_TRANSITION @@ -199,10 +205,6 @@ class LimitlessLEDGroup(Light, RestoreEntity): def __init__(self, group, config): """Initialize a group.""" - from limitlessled.group.rgbw import RgbwGroup - from limitlessled.group.white import WhiteGroup - from limitlessled.group.dimmer import DimmerGroup - from limitlessled.group.rgbww import RgbwwGroup if isinstance(group, WhiteGroup): self._supported = SUPPORT_LIMITLESSLED_WHITE @@ -366,8 +368,6 @@ def turn_on(self, transition_time, pipeline, **kwargs): # Add effects. if ATTR_EFFECT in kwargs and self._effect_list: if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: - from limitlessled.presets import COLORLOOP - self._effect = EFFECT_COLORLOOP pipeline.append(COLORLOOP) if kwargs[ATTR_EFFECT] == EFFECT_WHITE: @@ -389,6 +389,5 @@ def limitlessled_brightness(self): def limitlessled_color(self): """Convert Home Assistant HS list to RGB Color tuple.""" - from limitlessled import Color return Color(*color_hs_to_RGB(*tuple(self._color))) From 695ec0c219bd69c1c71fc62c9e33ca6eaed53778 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:13:07 +0100 Subject: [PATCH 2022/3953] Move imports to top for london_underground (#29387) --- homeassistant/components/london_underground/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index 07881fce40f64a..12f40f7b461328 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from london_tube_status import TubeData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -43,7 +44,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tube sensor.""" - from london_tube_status import TubeData data = TubeData() data.update() From 49232332a18ea2fb30bfa1d173885ca8c28fc087 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:13:22 +0100 Subject: [PATCH 2023/3953] Move imports to top for luci (#29388) --- homeassistant/components/luci/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 59c3251a437a8d..9d71b3d263a743 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -1,6 +1,7 @@ """Support for OpenWRT (luci) routers.""" import logging +from openwrt_luci_rpc import OpenWrtRpc import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -45,7 +46,6 @@ class LuciDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from openwrt_luci_rpc import OpenWrtRpc self.router = OpenWrtRpc( config[CONF_HOST], From 1a51590711548fa1c935b80d9a7b30a6210d8f08 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:13:46 +0100 Subject: [PATCH 2024/3953] Move imports to top for lutron (#29389) --- homeassistant/components/lutron/__init__.py | 5 ++--- homeassistant/components/lutron/binary_sensor.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index ac9a4eab417da4..ac6ffa46b27c82 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -1,11 +1,12 @@ """Component for interacting with a Lutron RadioRA 2 system.""" import logging +from pylutron import Button, Lutron import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ATTR_ID, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify @@ -36,7 +37,6 @@ def setup(hass, base_config): """Set up the Lutron component.""" - from pylutron import Lutron hass.data[LUTRON_BUTTONS] = [] hass.data[LUTRON_CONTROLLER] = None @@ -147,7 +147,6 @@ def __init__(self, hass, keypad, button): def button_callback(self, button, context, event, params): """Fire an event about a button being pressed or released.""" - from pylutron import Button # Events per button type: # RaiseLower -> pressed/released diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py index a86d56c325fb6b..866c82a7b2acf1 100644 --- a/homeassistant/components/lutron/binary_sensor.py +++ b/homeassistant/components/lutron/binary_sensor.py @@ -2,8 +2,8 @@ from pylutron import OccupancyGroup from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_OCCUPANCY, + BinarySensorDevice, ) from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice From 025c1a39a7ae3d360f511f3aef5be66f754f2be3 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:14:07 +0100 Subject: [PATCH 2025/3953] Move imports to top for lutron_caseta (#29390) --- homeassistant/components/lutron_caseta/__init__.py | 4 ++-- homeassistant/components/lutron_caseta/fan.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index f5ed0742cf7cee..aaac06a6bd5add 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -1,11 +1,12 @@ """Component for interacting with a Lutron Caseta system.""" import logging +from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_HOST from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -37,7 +38,6 @@ async def async_setup(hass, base_config): """Set up the Lutron component.""" - from pylutron_caseta.smartbridge import Smartbridge config = base_config.get(DOMAIN) keyfile = hass.config.path(config[CONF_KEYFILE]) diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 486a0aed401979..1227371ac0735e 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -1,16 +1,16 @@ """Support for Lutron Caseta fans.""" import logging -from pylutron_caseta import FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH +from pylutron_caseta import FAN_HIGH, FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_OFF from homeassistant.components.fan import ( - SUPPORT_SET_SPEED, - FanEntity, DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, + SUPPORT_SET_SPEED, + FanEntity, ) from . import LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice From c6fd8582a9a8d084358b55f4810e8d145cc1deb0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:14:35 +0100 Subject: [PATCH 2026/3953] Move imports to top for lyft (#29391) --- homeassistant/components/lyft/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py index 339b996c5d847b..1b90d66398e831 100644 --- a/homeassistant/components/lyft/sensor.py +++ b/homeassistant/components/lyft/sensor.py @@ -1,13 +1,16 @@ """Support for the Lyft API.""" -import logging from datetime import timedelta +import logging +from lyft_rides.auth import ClientCredentialGrant +from lyft_rides.client import LyftRidesClient +from lyft_rides.errors import APIError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -38,8 +41,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Lyft sensor.""" - from lyft_rides.auth import ClientCredentialGrant - from lyft_rides.errors import APIError auth_flow = ClientCredentialGrant( client_id=config.get(CONF_CLIENT_ID), @@ -208,7 +209,6 @@ def __init__( @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest product info and estimates from the Lyft API.""" - from lyft_rides.errors import APIError try: self.fetch_data() @@ -217,7 +217,6 @@ def update(self): def fetch_data(self): """Get the latest product info and estimates from the Lyft API.""" - from lyft_rides.client import LyftRidesClient client = LyftRidesClient(self._session) From d79d9e0bfbd92d3dcd680e457cbcac1fd260d9b1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:15:29 +0100 Subject: [PATCH 2027/3953] Move imports to top for matrix (#29392) --- homeassistant/components/matrix/__init__.py | 21 ++++++++------------- homeassistant/components/matrix/notify.py | 4 ++-- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 4a9435808bbf66..71735bd7e51b6b 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -1,22 +1,23 @@ """The matrix bot component.""" +from functools import partial import logging import os -from functools import partial +from matrix_client.client import MatrixClient, MatrixRequestError import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ATTR_TARGET, ATTR_MESSAGE +from homeassistant.components.notify import ATTR_MESSAGE, ATTR_TARGET from homeassistant.const import ( - CONF_USERNAME, + CONF_NAME, CONF_PASSWORD, + CONF_USERNAME, CONF_VERIFY_SSL, - CONF_NAME, - EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.util.json import load_json, save_json from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.util.json import load_json, save_json _LOGGER = logging.getLogger(__name__) @@ -76,7 +77,6 @@ def setup(hass, config): """Set up the Matrix bot component.""" - from matrix_client.client import MatrixRequestError config = config[DOMAIN] @@ -249,7 +249,6 @@ def _join_or_get_room(self, room_id_or_alias): def _join_rooms(self): """Join the rooms that we listen for commands in.""" - from matrix_client.client import MatrixRequestError for room_id in self._listening_rooms: try: @@ -287,7 +286,6 @@ def _store_auth_token(self, token): def _login(self): """Login to the matrix homeserver and return the client instance.""" - from matrix_client.client import MatrixRequestError # Attempt to generate a valid client using either of the two possible # login methods: @@ -328,7 +326,6 @@ def _login(self): def _login_by_token(self): """Login using authentication token and return the client.""" - from matrix_client.client import MatrixClient return MatrixClient( base_url=self._homeserver, @@ -339,7 +336,6 @@ def _login_by_token(self): def _login_by_password(self): """Login using password authentication and return the client.""" - from matrix_client.client import MatrixClient _client = MatrixClient( base_url=self._homeserver, valid_cert_check=self._verify_tls @@ -353,7 +349,6 @@ def _login_by_password(self): def _send_message(self, message, target_rooms): """Send the message to the matrix server.""" - from matrix_client.client import MatrixRequestError for target_room in target_rooms: try: diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py index 44a0587ba6d22d..06e9712becc173 100644 --- a/homeassistant/components/matrix/notify.py +++ b/homeassistant/components/matrix/notify.py @@ -3,13 +3,13 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( + ATTR_MESSAGE, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, - ATTR_MESSAGE, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) From 849d8c885deaefb3792e75741b8e46e0298beae8 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:15:53 +0100 Subject: [PATCH 2028/3953] Move imports to top for maxcube (#29393) --- homeassistant/components/maxcube/__init__.py | 8 ++++---- homeassistant/components/maxcube/climate.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/maxcube/__init__.py b/homeassistant/components/maxcube/__init__.py index 65a969bbcb8cfd..1b65cb161e12bb 100644 --- a/homeassistant/components/maxcube/__init__.py +++ b/homeassistant/components/maxcube/__init__.py @@ -1,14 +1,16 @@ """Support for the MAX! Cube LAN Gateway.""" import logging -import time from socket import timeout from threading import Lock +import time +from maxcube.connection import MaxCubeConnection +from maxcube.cube import MaxCube import voluptuous as vol +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -46,8 +48,6 @@ def setup(hass, config): """Establish connection to MAX! Cube.""" - from maxcube.connection import MaxCubeConnection - from maxcube.cube import MaxCube if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index e09dfc2d99f49b..ff4b219ec21f16 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -4,16 +4,16 @@ from maxcube.device import ( MAX_DEVICE_MODE_AUTOMATIC, + MAX_DEVICE_MODE_BOOST, MAX_DEVICE_MODE_MANUAL, MAX_DEVICE_MODE_VACATION, - MAX_DEVICE_MODE_BOOST, ) from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS From 518ca3afa7a9fefd23175f2a21f8527b96350347 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:16:16 +0100 Subject: [PATCH 2029/3953] Move imports to top for meteo_france (#29394) --- homeassistant/components/meteo_france/__init__.py | 8 +++----- homeassistant/components/meteo_france/sensor.py | 8 ++++---- homeassistant/components/meteo_france/weather.py | 2 +- homeassistant/components/meteoalarm/binary_sensor.py | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index cfcd78400bd5e8..73b8dbb0e3963d 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -2,6 +2,8 @@ import datetime import logging +from meteofrance.client import meteofranceClient, meteofranceError +from vigilancemeteo import VigilanceMeteoError, VigilanceMeteoFranceProxy import voluptuous as vol from homeassistant.const import CONF_MONITORED_CONDITIONS @@ -9,7 +11,7 @@ from homeassistant.helpers.discovery import load_platform from homeassistant.util import Throttle -from .const import DOMAIN, CONF_CITY, SENSOR_TYPES, DATA_METEO_FRANCE +from .const import CONF_CITY, DATA_METEO_FRANCE, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -61,7 +63,6 @@ def setup(hass, config): # 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: @@ -79,8 +80,6 @@ def setup(hass, config): city = location[CONF_CITY] - from meteofrance.client import meteofranceClient, meteofranceError - try: client = meteofranceClient(city) except meteofranceError as exp: @@ -127,7 +126,6 @@ def get_data(self): @Throttle(SCAN_INTERVAL) def update(self): """Get the latest data from Meteo-France.""" - from meteofrance.client import meteofranceError try: self._client.update() diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 8c2bd32048fc00..f0c08ac18220a3 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,6 +1,8 @@ """Support for Meteo-France raining forecast sensor.""" import logging +from vigilancemeteo import DepartmentWeatherAlert + from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity @@ -8,11 +10,11 @@ ATTRIBUTION, CONF_CITY, DATA_METEO_FRANCE, - SENSOR_TYPES, + SENSOR_TYPE_CLASS, SENSOR_TYPE_ICON, SENSOR_TYPE_NAME, SENSOR_TYPE_UNIT, - SENSOR_TYPE_CLASS, + SENSOR_TYPES, ) _LOGGER = logging.getLogger(__name__) @@ -31,8 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): client = hass.data[DATA_METEO_FRANCE][city] weather_alert_client = hass.data[DATA_METEO_FRANCE]["weather_alert_client"] - from vigilancemeteo import DepartmentWeatherAlert - alert_watcher = None if "weather_alert" in monitored_conditions: datas = hass.data[DATA_METEO_FRANCE][city].get_data() diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 00da55809ffb64..c96080808e97e8 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -9,8 +9,8 @@ ATTR_FORECAST_TIME, WeatherEntity, ) -import homeassistant.util.dt as dt_util from homeassistant.const import TEMP_CELSIUS +import homeassistant.util.dt as dt_util from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py index 55041f59cf2393..7d3bea4c995997 100644 --- a/homeassistant/components/meteoalarm/binary_sensor.py +++ b/homeassistant/components/meteoalarm/binary_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from meteoalertapi import Meteoalert import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -33,7 +34,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MeteoAlarm binary sensor platform.""" - from meteoalertapi import Meteoalert country = config[CONF_COUNTRY] province = config[CONF_PROVINCE] From 048068307d33a1914d60844d220345a85440ac65 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:16:35 +0100 Subject: [PATCH 2030/3953] Move imports to top for microsoft (#29395) --- homeassistant/components/microsoft/tts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 447d2a4d46a3cc..074605e07fe5d9 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -2,6 +2,7 @@ from http.client import HTTPException import logging +from pycsspeechtts import pycsspeechtts import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider @@ -142,7 +143,6 @@ def get_tts_audio(self, message, language, options=None): """Load TTS from Microsoft.""" if language is None: language = self._lang - from pycsspeechtts import pycsspeechtts try: trans = pycsspeechtts.TTSTranslator(self._apikey, self._region) From a24e7832b5c8bcd299ac9cb375d0bca0368b1f72 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:16:58 +0100 Subject: [PATCH 2031/3953] Move imports to top for modem_callerid (#29396) --- homeassistant/components/modem_callerid/sensor.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 7acb345e27e4a7..7ffda3e61243fd 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -1,15 +1,18 @@ """A sensor for incoming calls using a USB modem that supports caller ID.""" import logging + +from basicmodem.basicmodem import BasicModem as bm import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - STATE_IDLE, - EVENT_HOMEASSISTANT_STOP, - CONF_NAME, CONF_DEVICE, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, ) -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Modem CallerID" @@ -29,7 +32,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up modem caller ID sensor platform.""" - from basicmodem.basicmodem import BasicModem as bm name = config.get(CONF_NAME) port = config.get(CONF_DEVICE) From 45c7c7a439f32937384112cfc4aa9c2d7ec3d927 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:17:20 +0100 Subject: [PATCH 2032/3953] Move imports to top for keenetic-ndms2 (#29400) --- homeassistant/components/keenetic_ndms2/device_tracker.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index faab0dde62d3f3..598e29cf58305a 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -1,15 +1,16 @@ """Support for Zyxel Keenetic NDMS2 based routers.""" import logging +from ndms2_client import Client, ConnectionException, TelnetConnection import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,7 +46,6 @@ class KeeneticNDMS2DeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from ndms2_client import Client, TelnetConnection self.last_results = [] @@ -88,8 +88,6 @@ def _update_info(self): """Get ARP from keenetic router.""" _LOGGER.debug("Fetching devices from router...") - from ndms2_client import ConnectionException - try: self.last_results = [ dev From e04a208af0b823f33a33e0ee3c816c198661e503 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:17:38 +0100 Subject: [PATCH 2033/3953] Move imports to top for irish_rail_transport (#29401) --- homeassistant/components/irish_rail_transport/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/irish_rail_transport/sensor.py b/homeassistant/components/irish_rail_transport/sensor.py index 6f2bbae5ebad64..883f4ed7b397f0 100644 --- a/homeassistant/components/irish_rail_transport/sensor.py +++ b/homeassistant/components/irish_rail_transport/sensor.py @@ -1,12 +1,13 @@ """Support for Irish Rail RTPI information.""" -import logging from datetime import timedelta +import logging +from pyirishrail.pyirishrail import IrishRailRTPI import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -47,7 +48,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Irish Rail transport sensor.""" - from pyirishrail.pyirishrail import IrishRailRTPI station = config.get(CONF_STATION) direction = config.get(CONF_DIRECTION) From 73e25296ca59b1891f8cb689a579f48da66bd2c6 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 11:18:05 +0100 Subject: [PATCH 2034/3953] Move imports to top for joaoapps_join (#29402) --- .../components/joaoapps_join/__init__.py | 20 +++++++++---------- .../components/joaoapps_join/notify.py | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/joaoapps_join/__init__.py b/homeassistant/components/joaoapps_join/__init__.py index b988411762eb1e..10cbcf6b5c0684 100644 --- a/homeassistant/components/joaoapps_join/__init__.py +++ b/homeassistant/components/joaoapps_join/__init__.py @@ -1,10 +1,19 @@ """Support for Joaoapps Join services.""" import logging +from pyjoin import ( + get_devices, + ring_device, + send_file, + send_notification, + send_sms, + send_url, + set_wallpaper, +) import voluptuous as vol +from homeassistant.const import CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME, CONF_API_KEY _LOGGER = logging.getLogger(__name__) @@ -35,14 +44,6 @@ def register_device(hass, api_key, name, device_id, device_ids, device_names): """Register services for each join device listed.""" - from pyjoin import ( - ring_device, - set_wallpaper, - send_sms, - send_file, - send_url, - send_notification, - ) def ring_service(service): """Service to ring devices.""" @@ -114,7 +115,6 @@ def send_sms_service(service): def setup(hass, config): """Set up the Join services.""" - from pyjoin import get_devices for device in config[DOMAIN]: api_key = device.get(CONF_API_KEY) diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py index 2e6b3d1c67a3fe..14b8fe1a814d77 100644 --- a/homeassistant/components/joaoapps_join/notify.py +++ b/homeassistant/components/joaoapps_join/notify.py @@ -1,6 +1,9 @@ """Support for Join notifications.""" import logging + +from pyjoin import get_devices, send_notification import voluptuous as vol + from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, @@ -34,8 +37,6 @@ def get_service(hass, config, discovery_info=None): device_ids = config.get(CONF_DEVICE_IDS) device_names = config.get(CONF_DEVICE_NAMES) if api_key: - from pyjoin import get_devices - if not get_devices(api_key): _LOGGER.error("Error connecting to Join. Check the API key") return False @@ -60,7 +61,6 @@ def __init__(self, api_key, device_id, device_ids, device_names): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from pyjoin import send_notification title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) or {} From 992d9273bb68690fadfb2232d47df15f6314956d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 4 Dec 2019 11:45:56 +0100 Subject: [PATCH 2035/3953] Upgrade psutil to 5.6.7 (#29407) --- homeassistant/components/systemmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 9d03e287d13457..0614ef4b91ca17 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Systemmonitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", "requirements": [ - "psutil==5.6.6" + "psutil==5.6.7" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 48144e7c8393b2..d9ff2a90946667 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1032,7 +1032,7 @@ protobuf==3.6.1 proxmoxer==1.0.3 # homeassistant.components.systemmonitor -psutil==5.6.6 +psutil==5.6.7 # homeassistant.components.ptvsd ptvsd==4.2.8 From 56b60577de2292969b3ddc515c98698460e97679 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:07:36 +0100 Subject: [PATCH 2036/3953] Move imports to top for iglo (#29424) --- homeassistant/components/iglo/light.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iglo/light.py b/homeassistant/components/iglo/light.py index d93ebcb920aa2f..59e6db2a81f1d7 100644 --- a/homeassistant/components/iglo/light.py +++ b/homeassistant/components/iglo/light.py @@ -2,6 +2,7 @@ import logging import math +from iglo import Lamp import voluptuous as vol from homeassistant.components.light import ( @@ -9,11 +10,11 @@ ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - PLATFORM_SCHEMA, Light, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT @@ -47,7 +48,6 @@ class IGloLamp(Light): def __init__(self, name, host, port): """Initialize the light.""" - from iglo import Lamp self._name = name self._lamp = Lamp(0, host, port) From 6c45c1969b7d5fbbd7360c66198e9b60102ce039 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:08:01 +0100 Subject: [PATCH 2037/3953] Move imports to top for idteck_prox (#29423) --- homeassistant/components/idteck_prox/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/idteck_prox/__init__.py b/homeassistant/components/idteck_prox/__init__.py index 089347a0f73058..9cc4f3de9d6093 100644 --- a/homeassistant/components/idteck_prox/__init__.py +++ b/homeassistant/components/idteck_prox/__init__.py @@ -1,15 +1,16 @@ """Component for interfacing RFK101 proximity card readers.""" import logging +from rfk101py.rfk101py import rfk101py import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, - CONF_PORT, CONF_NAME, + CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -68,7 +69,6 @@ def __init__(self, hass, host, port, name): def connect(self): """Connect to the reader.""" - from rfk101py.rfk101py import rfk101py self._connection = rfk101py(self._host, self._port, self._callback) From c159ad52a496c4a381c51905ae60114c6a17712c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:09:04 +0100 Subject: [PATCH 2038/3953] Move imports to top for gogogate2 (#29411) * Move imports to top for gogogate2 * Isorted imports --- homeassistant/components/gogogate2/cover.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 26aecee250495a..fcb0182ec0e4d6 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -1,15 +1,16 @@ """Support for Gogogate2 garage Doors.""" import logging +from pygogogate2 import Gogogate2API as pygogogate2 import voluptuous as vol -from homeassistant.components.cover import CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverDevice from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - STATE_CLOSED, CONF_IP_ADDRESS, CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + STATE_CLOSED, ) import homeassistant.helpers.config_validation as cv @@ -32,7 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gogogate2 component.""" - from pygogogate2 import Gogogate2API as pygogogate2 ip_address = config.get(CONF_IP_ADDRESS) name = config.get(CONF_NAME) From f4f4c695d9994e7717418c247b2a302cb528fc36 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:09:35 +0100 Subject: [PATCH 2039/3953] Move imports to top for hydrawise (#29421) --- homeassistant/components/hydrawise/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index e111c157460c38..57ed29d9780e43 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -2,12 +2,13 @@ from datetime import timedelta import logging +from hydrawiser.core import Hydrawiser from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant.const import ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL -import homeassistant.helpers.config_validation as cv from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval @@ -73,8 +74,6 @@ def setup(hass, config): scan_interval = conf.get(CONF_SCAN_INTERVAL) try: - from hydrawiser.core import Hydrawiser - hydrawise = Hydrawiser(user_token=access_token) hass.data[DATA_HYDRAWISE] = HydrawiseHub(hydrawise) except (ConnectTimeout, HTTPError) as ex: From 497674835b81713d58b2a4f27195e1d82ca2d80d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:09:57 +0100 Subject: [PATCH 2040/3953] Move imports to top for ihc (#29425) --- homeassistant/components/ihc/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index a55b94eb26a5eb..b246943b6addc8 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -2,6 +2,8 @@ import logging import os.path +from defusedxml import ElementTree +from ihcsdk.ihccontroller import IHCController import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA @@ -214,7 +216,6 @@ def setup(hass, config): def ihc_setup(hass, config, conf, controller_id): """Set up the IHC component.""" - from ihcsdk.ihccontroller import IHCController url = conf[CONF_URL] username = conf[CONF_USERNAME] @@ -272,7 +273,6 @@ def autosetup_ihc_products( hass: HomeAssistantType, config, ihc_controller, controller_id ): """Auto setup of IHC products from the IHC project file.""" - from defusedxml import ElementTree project_xml = ihc_controller.get_project() if not project_xml: From 6f5b59da150b34619b1c31fdbbe04906029f628f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:10:15 +0100 Subject: [PATCH 2041/3953] Move imports to top for ialarm (#29422) --- homeassistant/components/ialarm/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index c3bcf626f0ae93..24ab2bc7a80b56 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -2,6 +2,7 @@ import logging import re +from pyialarm import IAlarm import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm @@ -66,7 +67,6 @@ class IAlarmPanel(alarm.AlarmControlPanel): def __init__(self, name, code, username, password, url): """Initialize the iAlarm status.""" - from pyialarm import IAlarm self._name = name self._code = str(code) if code else None From e6f7cbe53e4142090cc90fdd5bdf2593547faa4b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:10:47 +0100 Subject: [PATCH 2042/3953] Move imports to top for gitter (#29412) --- homeassistant/components/gitter/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index f124849a19385f..4f1eeca7d71901 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -1,6 +1,8 @@ """Support for displaying details about a Gitter.im chat room.""" import logging +from gitterpy.client import GitterClient +from gitterpy.errors import GitterRoomError, GitterTokenError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -30,8 +32,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gitter sensor.""" - from gitterpy.client import GitterClient - from gitterpy.errors import GitterTokenError name = config.get(CONF_NAME) api_key = config.get(CONF_API_KEY) @@ -91,7 +91,6 @@ def icon(self): def update(self): """Get the latest data and updates the state.""" - from gitterpy.errors import GitterRoomError try: data = self._data.user.unread_items(self._room) From 95bc97fb6a8fd9ef424366a9688191894bbe74aa Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:11:14 +0100 Subject: [PATCH 2043/3953] Move imports to top for homeworks (#29418) --- homeassistant/components/homeworks/__init__.py | 3 +-- homeassistant/components/homeworks/light.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index bd40336b8ba894..55357acdad4467 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -1,6 +1,7 @@ """Support for Lutron Homeworks Series 4 and 8 systems.""" import logging +from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED, Homeworks import voluptuous as vol from homeassistant.const import ( @@ -65,7 +66,6 @@ def setup(hass, base_config): """Start Homeworks controller.""" - from pyhomeworks.pyhomeworks import Homeworks def hw_callback(msg_type, values): """Dispatch state changes.""" @@ -138,7 +138,6 @@ def __init__(self, hass, addr, name): @callback def _update_callback(self, msg_type, values): """Fire events if button is pressed or released.""" - from pyhomeworks.pyhomeworks import HW_BUTTON_PRESSED, HW_BUTTON_RELEASED if msg_type == HW_BUTTON_PRESSED: event = EVENT_BUTTON_PRESS diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index d1854b4dbf3bea..2c0034ee9862c2 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -1,6 +1,8 @@ """Support for Lutron Homeworks lights.""" import logging +from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED + from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light from homeassistant.const import CONF_NAME from homeassistant.core import callback @@ -93,7 +95,6 @@ def is_on(self): @callback def _update_callback(self, msg_type, values): """Process device specific messages.""" - from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED if msg_type == HW_LIGHT_CHANGED: self._level = int((values[1] * 255.0) / 100.0) From 88c1a630d8afd31ebd157fd5baa49d17ffda9552 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:11:47 +0100 Subject: [PATCH 2044/3953] Move imports to top for gpmdp (#29413) --- homeassistant/components/gpmdp/media_player.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py index e6df8b0fe8b1be..e7b18aacc15c6f 100644 --- a/homeassistant/components/gpmdp/media_player.py +++ b/homeassistant/components/gpmdp/media_player.py @@ -5,8 +5,9 @@ import time import voluptuous as vol +from websocket import _exceptions, create_connection -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -65,8 +66,6 @@ def request_configuration(hass, config, url, add_entities_callback): ) return - from websocket import create_connection - websocket = create_connection((url), timeout=1) websocket.send( json.dumps( @@ -81,7 +80,6 @@ def request_configuration(hass, config, url, add_entities_callback): def gpmdp_configuration_callback(callback_data): """Handle configuration changes.""" while True: - from websocket import _exceptions try: msg = json.loads(websocket.recv()) @@ -174,7 +172,6 @@ class GPMDP(MediaPlayerDevice): def __init__(self, name, url, code): """Initialize the media player.""" - from websocket import create_connection self._connection = create_connection self._url = url @@ -210,7 +207,6 @@ def get_ws(self): def send_gpmdp_msg(self, namespace, method, with_id=True): """Send ws messages to GPMDP and verify request id in response.""" - from websocket import _exceptions try: websocket = self.get_ws() From 4a7004b79156ae08e03fa7c7c2869c11ff35c23c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:12:07 +0100 Subject: [PATCH 2045/3953] Move imports to top for greeneye_monitor (#29414) --- homeassistant/components/greeneye_monitor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/greeneye_monitor/__init__.py b/homeassistant/components/greeneye_monitor/__init__.py index cb67ac7faa4cd4..4f5899f6a4a6d4 100644 --- a/homeassistant/components/greeneye_monitor/__init__.py +++ b/homeassistant/components/greeneye_monitor/__init__.py @@ -1,6 +1,7 @@ """Support for monitoring a GreenEye Monitor energy monitor.""" import logging +from greeneye import Monitors import voluptuous as vol from homeassistant.const import ( @@ -110,7 +111,6 @@ async def async_setup(hass, config): """Set up the GreenEye Monitor component.""" - from greeneye import Monitors monitors = Monitors() hass.data[DATA_GREENEYE_MONITOR] = monitors From d8dcf17cb5c4f79201a6a19856bdb49e699f78f2 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:12:27 +0100 Subject: [PATCH 2046/3953] Move imports to top for gstreamer (#29415) --- homeassistant/components/gstreamer/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index a213587bc0e69c..9b371bfffcaec8 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -1,9 +1,10 @@ """Play media via gstreamer.""" import logging +from gsp import GstreamerPlayer import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -36,7 +37,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gstreamer platform.""" - from gsp import GstreamerPlayer name = config.get(CONF_NAME) pipeline = config.get(CONF_PIPELINE) From c79e99318d2b42af99c412d9f523060514b022dd Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:14:19 +0100 Subject: [PATCH 2047/3953] Move imports to top for habitica (#29416) --- homeassistant/components/habitica/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index f94a09e4da522c..52326555aab7bf 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -2,6 +2,7 @@ from collections import namedtuple import logging +from habitipy.aio import HabitipyAsync import voluptuous as vol from homeassistant.const import ( @@ -92,7 +93,6 @@ def has_all_unique_users_names(value): async def async_setup(hass, config): """Set up the Habitica service.""" - from habitipy.aio import HabitipyAsync conf = config[DOMAIN] data = hass.data[DOMAIN] = {} From 46c306685dc8719ae272f4048366b256f40ccc19 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:14:49 +0100 Subject: [PATCH 2048/3953] Move imports to top for hikvision (#29417) --- .../components/hikvision/binary_sensor.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index b898f5d860c2bb..9db9121730023d 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -1,24 +1,26 @@ """Support for Hikvision event stream events represented as binary sensors.""" -import logging from datetime import timedelta +import logging + +from pyhik.hikvision import HikCamera import voluptuous as vol -from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.util.dt import utcnow -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( + ATTR_LAST_TRIP_TIME, + CONF_CUSTOMIZE, CONF_HOST, - CONF_PORT, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, + CONF_PORT, CONF_SSL, - EVENT_HOMEASSISTANT_STOP, + CONF_USERNAME, EVENT_HOMEASSISTANT_START, - ATTR_LAST_TRIP_TIME, - CONF_CUSTOMIZE, + EVENT_HOMEASSISTANT_STOP, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -135,7 +137,6 @@ class HikvisionData: def __init__(self, hass, url, port, name, username, password): """Initialize the data object.""" - from pyhik.hikvision import HikCamera self._url = url self._port = port From c880f0971476b2d677e76db5f0659784a08a3980 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:15:17 +0100 Subject: [PATCH 2049/3953] Move imports to top for horizon (#29419) * Move imports to top for horizon * Fix Pylint redefining name keys --- homeassistant/components/horizon/media_player.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/horizon/media_player.py b/homeassistant/components/horizon/media_player.py index 8bed30e88f3278..44e93d26a405fa 100644 --- a/homeassistant/components/horizon/media_player.py +++ b/homeassistant/components/horizon/media_player.py @@ -2,10 +2,12 @@ from datetime import timedelta import logging +from horimote import Client, keys +from horimote.exceptions import AuthenticationError import voluptuous as vol from homeassistant import util -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -56,8 +58,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Horizon platform.""" - from horimote import Client, keys - from horimote.exceptions import AuthenticationError host = config[CONF_HOST] name = config[CONF_NAME] @@ -81,12 +81,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class HorizonDevice(MediaPlayerDevice): """Representation of a Horizon HD Recorder.""" - def __init__(self, client, name, keys): + def __init__(self, client, name, remote_keys): """Initialize the remote.""" self._client = client self._name = name self._state = None - self._keys = keys + self._keys = remote_keys @property def name(self): @@ -177,7 +177,6 @@ def _send_key(self, key): def _send(self, key=None, channel=None): """Send a key to the Horizon device.""" - from horimote.exceptions import AuthenticationError try: if key: From bef1c00b4959717f25fcad9b661659329bc27d6d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 14:15:39 +0100 Subject: [PATCH 2050/3953] Move imports to top for hunterdouglas_powerview (#29420) --- .../components/hunterdouglas_powerview/scene.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py index d55a970f1e4118..3f2ac79306c381 100644 --- a/homeassistant/components/hunterdouglas_powerview/scene.py +++ b/homeassistant/components/hunterdouglas_powerview/scene.py @@ -1,12 +1,16 @@ """Support for Powerview scenes from a Powerview hub.""" import logging +from aiopvapi.helpers.aiorequest import AioRequest +from aiopvapi.resources.scene import Scene as PvScene +from aiopvapi.rooms import Rooms +from aiopvapi.scenes import Scenes import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.scene import Scene, DOMAIN +from homeassistant.components.scene import DOMAIN, Scene from homeassistant.const import CONF_PLATFORM from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id _LOGGER = logging.getLogger(__name__) @@ -33,11 +37,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up home assistant scene entries.""" - # from aiopvapi.hub import Hub - from aiopvapi.helpers.aiorequest import AioRequest - from aiopvapi.scenes import Scenes - from aiopvapi.rooms import Rooms - from aiopvapi.resources.scene import Scene as PvScene hub_address = config.get(HUB_ADDRESS) websession = async_get_clientsession(hass) From 9bb6bcad707c8fde7515d70e5d0af58a900c88b4 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:14:46 +0100 Subject: [PATCH 2051/3953] Move imports to top for gearbest (#29432) --- homeassistant/components/gearbest/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gearbest/sensor.py b/homeassistant/components/gearbest/sensor.py index bad9b335e7383d..b9b2a35b89d839 100644 --- a/homeassistant/components/gearbest/sensor.py +++ b/homeassistant/components/gearbest/sensor.py @@ -1,15 +1,16 @@ """Parse prices of an item from gearbest.""" -import logging from datetime import timedelta +import logging +from gearbest_parser import CurrencyConverter, GearbestParser import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_CURRENCY, CONF_ID, CONF_NAME, CONF_URL import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval -from homeassistant.const import CONF_NAME, CONF_ID, CONF_URL, CONF_CURRENCY +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -41,7 +42,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Gearbest sensor.""" - from gearbest_parser import CurrencyConverter currency = config.get(CONF_CURRENCY) @@ -71,7 +71,6 @@ class GearbestSensor(Entity): def __init__(self, converter, item, currency): """Initialize the sensor.""" - from gearbest_parser import GearbestParser self._name = item.get(CONF_NAME) self._parser = GearbestParser() From c6ad57eb2bfcb01029967dc314b41708e6e4b7e3 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:15:30 +0100 Subject: [PATCH 2052/3953] Move imports to top for fleetgo (#29431) --- homeassistant/components/fleetgo/device_tracker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index 0561530345c76a..5a922ed4b92436 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -2,11 +2,12 @@ import logging import requests +from ritassist import API import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_utc_time_change _LOGGER = logging.getLogger(__name__) @@ -40,7 +41,6 @@ class FleetGoDeviceScanner: def __init__(self, config, see): """Initialize FleetGoDeviceScanner.""" - from ritassist import API self._include = config.get(CONF_INCLUDE) self._see = see From 12791a687bdd1139a0c501cedfa4669e585603bc Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:19:03 +0100 Subject: [PATCH 2053/3953] Move imports to top for fints (#29429) --- homeassistant/components/fints/sensor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 376ea2c0f9d4fc..d81f353c222f8c 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -3,10 +3,13 @@ from collections import namedtuple from datetime import timedelta import logging + +from fints.client import FinTS3PinTanClient +from fints.dialog import FinTSDialogError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PIN, CONF_URL, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PIN, CONF_URL, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -118,7 +121,6 @@ def client(self): the client objects. If that ever changes, consider caching the client object and also think about potential concurrency problems. """ - from fints.client import FinTS3PinTanClient return FinTS3PinTanClient( self._credentials.blz, @@ -129,7 +131,6 @@ def client(self): def detect_accounts(self): """Identify the accounts of the bank.""" - from fints.dialog import FinTSDialogError balance_accounts = [] holdings_accounts = [] From 4b6a2496c7f31b2772bcb9a5d0902797f78e3c69 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:20:14 +0100 Subject: [PATCH 2054/3953] Move imports to top for fastdotcom (#29428) --- homeassistant/components/fastdotcom/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index b070eef031010c..e0a4782493e4e8 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -1,11 +1,12 @@ """Support for testing internet speed via Fast.com.""" -import logging from datetime import timedelta +import logging +from fastdotcom import fast_com import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_SCAN_INTERVAL +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -63,7 +64,6 @@ def __init__(self, hass): def update(self, now=None): """Get the latest data from fast.com.""" - from fastdotcom import fast_com _LOGGER.debug("Executing fast.com speedtest") self.data = {"download": fast_com()} From af18c668d259335fedadc74d4071b3ba0d3ccbb0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:20:50 +0100 Subject: [PATCH 2055/3953] Move imports to top for familyhub (#29427) --- homeassistant/components/familyhub/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/familyhub/camera.py b/homeassistant/components/familyhub/camera.py index 546d95f24d12b5..2e4e7085927fac 100644 --- a/homeassistant/components/familyhub/camera.py +++ b/homeassistant/components/familyhub/camera.py @@ -1,9 +1,10 @@ """Family Hub camera for Samsung Refrigerators.""" import logging +from pyfamilyhublocal import FamilyHubCam import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -22,7 +23,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Family Hub Camera.""" - from pyfamilyhublocal import FamilyHubCam address = config.get(CONF_IP_ADDRESS) name = config.get(CONF_NAME) From 0d1a38960283dc44fde383ed306685d346b2d711 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 15:21:18 +0100 Subject: [PATCH 2056/3953] Move imports to top for etherscan (#29426) --- homeassistant/components/etherscan/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 9cabb2762b03cc..1c14ce578c16fc 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -1,6 +1,7 @@ """Support for Etherscan sensors.""" from datetime import timedelta +from pyetherscan import get_balance import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -75,7 +76,6 @@ def device_state_attributes(self): def update(self): """Get the latest state of the sensor.""" - from pyetherscan import get_balance if self._token_address: self._state = get_balance(self._address, self._token_address) From f8d607bdf5da3c9c20093e58912810f0b5c62897 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 17:08:23 +0100 Subject: [PATCH 2057/3953] Move imports to top for fixer (#29430) * Move imports to top for fixer * Only import FixerioException instead of all exceptions --- homeassistant/components/fixer/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index a97f77138dbbd3..e3dfd432a416a9 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from fixerio import Fixerio +from fixerio.exceptions import FixerioException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -35,7 +37,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Fixer.io sensor.""" - from fixerio import Fixerio, exceptions api_key = config.get(CONF_API_KEY) name = config.get(CONF_NAME) @@ -43,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: Fixerio(symbols=[target], access_key=api_key).latest() - except exceptions.FixerioException: + except FixerioException: _LOGGER.error("One of the given currencies is not supported") return @@ -102,7 +103,6 @@ class ExchangeData: def __init__(self, target_currency, api_key): """Initialize the data object.""" - from fixerio import Fixerio self.api_key = api_key self.rate = None From a2a3c50555bfaf8d950b047e0aeff24ceb12bae8 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 4 Dec 2019 20:27:51 +0100 Subject: [PATCH 2058/3953] Move imports to top for danfoss_air (#29435) --- homeassistant/components/danfoss_air/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index adfb13e722c759..b1dbf890eb970f 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pydanfossair.commands import ReadCommand +from pydanfossair.danfossclient import DanfossClient import voluptuous as vol from homeassistant.const import CONF_HOST @@ -40,8 +42,6 @@ def __init__(self, host): """Initialize the Danfoss Air CCM connection.""" self._data = {} - from pydanfossair.danfossclient import DanfossClient - self._client = DanfossClient(host) def get_value(self, item): @@ -56,7 +56,6 @@ def update_state(self, command, state_command): def update(self): """Use the data from Danfoss Air API.""" _LOGGER.debug("Fetching data from Danfoss Air CCM module") - from pydanfossair.commands import ReadCommand self._data[ReadCommand.exhaustTemperature] = self._client.command( ReadCommand.exhaustTemperature From b829c46e18382f5e2e59205abecf0a443f6d4447 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Dec 2019 20:28:23 +0100 Subject: [PATCH 2059/3953] Updated frontend to 20191204.0 (#29461) --- 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 b196239dfb1fb5..03d2d7899b07ba 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191119.6" + "home-assistant-frontend==20191204.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0e336835c4d073..e0a823da44e733 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.30 -home-assistant-frontend==20191119.6 +home-assistant-frontend==20191204.0 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index d9ff2a90946667..970b844d8c96d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -664,7 +664,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.6 +home-assistant-frontend==20191204.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfe48e243778e6..4fffcce129061e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,7 +228,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191119.6 +home-assistant-frontend==20191204.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.6 From 557350450894569636493c9c0d8510e1bab53a5f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:08:22 +0100 Subject: [PATCH 2060/3953] Move imports to top for litejet (#29481) --- homeassistant/components/litejet/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index 6df79c7c968a8e..1d3ab4f6b19a49 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -1,11 +1,12 @@ """Support for the LiteJet lighting system.""" import logging +from pylitejet import LiteJet import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery from homeassistant.const import CONF_PORT +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -30,7 +31,6 @@ def setup(hass, config): """Set up the LiteJet component.""" - from pylitejet import LiteJet url = config[DOMAIN].get(CONF_PORT) From 72995f6ce63634e96cdc785d4b717d5d00f84422 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:11:13 +0100 Subject: [PATCH 2061/3953] Move imports to top for tuya (#29467) --- homeassistant/components/tuya/__init__.py | 9 +++++---- homeassistant/components/tuya/climate.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 4d6506d950d807..dffd66265a6864 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -1,13 +1,15 @@ """Support for Tuya Smart devices.""" from datetime import timedelta import logging + +from tuyaha import TuyaApi import voluptuous as vol +from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM from homeassistant.helpers import discovery -from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_time_interval @@ -50,7 +52,6 @@ def setup(hass, config): """Set up Tuya Component.""" - from tuyaha import TuyaApi tuya = TuyaApi() username = config[DOMAIN][CONF_USERNAME] diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 02e8a1bf821810..6450920b806427 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -5,9 +5,9 @@ HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, - HVAC_MODE_OFF, ) from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ( From 96cf20ad815087d7b26c5b13067af46844eb7b92 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:11:30 +0100 Subject: [PATCH 2062/3953] Move imports to top for zoneminder (#29468) --- homeassistant/components/zoneminder/__init__.py | 8 ++++---- homeassistant/components/zoneminder/sensor.py | 2 +- homeassistant/components/zoneminder/switch.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index a116cc31891e5d..3007c981480f52 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -2,18 +2,19 @@ import logging import voluptuous as vol +from zoneminder.zm import ZoneMinder -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( + ATTR_ID, + ATTR_NAME, CONF_HOST, CONF_PASSWORD, CONF_PATH, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - ATTR_NAME, - ATTR_ID, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,6 @@ def setup(hass, config): """Set up the ZoneMinder component.""" - from zoneminder.zm import ZoneMinder hass.data[DOMAIN] = {} diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index bfcfcb8f907a8e..75531e79e13b81 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from zoneminder.monitor import TimePeriod from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_MONITORED_CONDITIONS @@ -95,7 +96,6 @@ class ZMSensorEvents(Entity): def __init__(self, monitor, include_archived, sensor_type): """Initialize event sensor.""" - from zoneminder.monitor import TimePeriod self._monitor = monitor self._include_archived = include_archived diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index d2d761aab1e60c..5eaf2ed4901dd2 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from zoneminder.monitor import MonitorState from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_COMMAND_OFF, CONF_COMMAND_ON @@ -21,7 +22,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ZoneMinder switch platform.""" - from zoneminder.monitor import MonitorState on_state = MonitorState(config.get(CONF_COMMAND_ON)) off_state = MonitorState(config.get(CONF_COMMAND_OFF)) From 43665466f58188a8249681f3899eaa0afa5b3117 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:12:09 +0100 Subject: [PATCH 2063/3953] Move imports to top for point (#29470) --- homeassistant/components/point/__init__.py | 2 +- homeassistant/components/point/config_flow.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 9c5ec4d5529d2f..9abae9ab025474 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging +from pypoint import PointSession import voluptuous as vol from homeassistant import config_entries @@ -71,7 +72,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Point from a config entry.""" - from pypoint import PointSession def token_saver(token): _LOGGER.debug("Saving updated token") diff --git a/homeassistant/components/point/config_flow.py b/homeassistant/components/point/config_flow.py index f354411ab42dc7..3312931085ef62 100644 --- a/homeassistant/components/point/config_flow.py +++ b/homeassistant/components/point/config_flow.py @@ -4,6 +4,7 @@ import logging import async_timeout +from pypoint import PointSession import voluptuous as vol from homeassistant import config_entries @@ -109,7 +110,6 @@ async def async_step_auth(self, user_input=None): async def _get_authorization_url(self): """Create Minut Point session and get authorization url.""" - from pypoint import PointSession flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] client_id = flow[CLIENT_ID] @@ -138,7 +138,6 @@ async def async_step_code(self, code=None): async def _async_create_session(self, code): """Create point session and entries.""" - from pypoint import PointSession flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] client_id = flow[CLIENT_ID] From c6b8d35c16da58e59f1be735dbce2095c494cbe4 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:12:26 +0100 Subject: [PATCH 2064/3953] Move imports to top for ps4 (#29471) --- homeassistant/components/ps4/__init__.py | 4 ++-- homeassistant/components/ps4/media_player.py | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 205059be608f99..05e3422fe7466a 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -2,9 +2,9 @@ import logging import os -import voluptuous as vol from pyps4_2ndscreen.ddp import async_create_ddp_endpoint from pyps4_2ndscreen.media_art import COUNTRIES +import voluptuous as vol from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, @@ -20,7 +20,7 @@ ) from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry, config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import location from homeassistant.util.json import load_json, save_json diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 91d3a5b13c72a0..35cdbab253406f 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -1,19 +1,18 @@ """Support for PlayStation 4 consoles.""" -import logging import asyncio +import logging +from pyps4_2ndscreen.errors import NotReady, PSDataIncomplete import pyps4_2ndscreen.ps4 as pyps4 -from pyps4_2ndscreen.errors import NotReady -from homeassistant.core import callback from homeassistant.components.media_player import ENTITY_IMAGE_URL, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, - MEDIA_TYPE_GAME, MEDIA_TYPE_APP, - SUPPORT_SELECT_SOURCE, + MEDIA_TYPE_GAME, SUPPORT_PAUSE, + SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -26,9 +25,10 @@ CONF_REGION, CONF_TOKEN, STATE_IDLE, - STATE_STANDBY, STATE_PLAYING, + STATE_STANDBY, ) +from homeassistant.core import callback from homeassistant.helpers import device_registry, entity_registry from .const import ( @@ -254,7 +254,6 @@ def reset_title(self): async def async_get_title_data(self, title_id, name): """Get PS Store Data.""" - from pyps4_2ndscreen.errors import PSDataIncomplete app_name = None art = None From d9661b408b2d8958be3caf1e8a80d912fbd9ef80 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:12:44 +0100 Subject: [PATCH 2065/3953] Move imports to top for rainmachine (#29472) --- homeassistant/components/rainmachine/__init__.py | 9 ++++----- homeassistant/components/rainmachine/config_flow.py | 6 +++--- homeassistant/components/rainmachine/switch.py | 8 ++------ 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index a530223cb0539d..5dffecb0488e5d 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -1,8 +1,10 @@ """Support for RainMachine devices.""" import asyncio -import logging from datetime import timedelta +import logging +from regenmaschine import login +from regenmaschine.errors import RainMachineError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -10,12 +12,12 @@ ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, + CONF_MONITORED_CONDITIONS, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SSL, - CONF_MONITORED_CONDITIONS, CONF_SWITCHES, ) from homeassistant.exceptions import ConfigEntryNotReady @@ -211,8 +213,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up RainMachine as config entry.""" - from regenmaschine import login - from regenmaschine.errors import RainMachineError _verify_domain_control = verify_domain_control(hass, DOMAIN) @@ -377,7 +377,6 @@ def __init__( async def async_update(self): """Update sensor/binary sensor data.""" - from regenmaschine.errors import RainMachineError tasks = {} diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 3600324cb12024..4753335da7838a 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -2,10 +2,11 @@ from collections import OrderedDict +from regenmaschine import login +from regenmaschine.errors import RainMachineError import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PASSWORD, @@ -13,6 +14,7 @@ CONF_SCAN_INTERVAL, CONF_SSL, ) +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_SSL, DOMAIN @@ -55,8 +57,6 @@ async def async_step_import(self, import_config): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from regenmaschine import login - from regenmaschine.errors import RainMachineError if not user_input: return await self._show_form() diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 870c7317f257e1..69c0e4da52dc0d 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -2,6 +2,8 @@ from datetime import datetime import logging +from regenmaschine.errors import RequestError + from homeassistant.components.switch import SwitchDevice from homeassistant.const import ATTR_ID from homeassistant.core import callback @@ -181,7 +183,6 @@ async def async_added_to_hass(self): async def async_turn_off(self, **kwargs) -> None: """Turn the program off.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.programs.stop(self._rainmachine_entity_id) @@ -193,7 +194,6 @@ async def async_turn_off(self, **kwargs) -> None: async def async_turn_on(self, **kwargs) -> None: """Turn the program on.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.programs.start(self._rainmachine_entity_id) @@ -205,7 +205,6 @@ async def async_turn_on(self, **kwargs) -> None: async def async_update(self) -> None: """Update info for the program.""" - from regenmaschine.errors import RequestError try: self._obj = await self.rainmachine.client.programs.get( @@ -265,7 +264,6 @@ async def async_added_to_hass(self): async def async_turn_off(self, **kwargs) -> None: """Turn the zone off.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.zones.stop(self._rainmachine_entity_id) @@ -274,7 +272,6 @@ async def async_turn_off(self, **kwargs) -> None: async def async_turn_on(self, **kwargs) -> None: """Turn the zone on.""" - from regenmaschine.errors import RequestError try: await self.rainmachine.client.zones.start( @@ -285,7 +282,6 @@ async def async_turn_on(self, **kwargs) -> None: async def async_update(self) -> None: """Update info for the zone.""" - from regenmaschine.errors import RequestError try: self._obj = await self.rainmachine.client.zones.get( From c02d551cd591c246a92f3d9a3767754053338d18 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:13:05 +0100 Subject: [PATCH 2066/3953] Move imports to top for random (#29473) --- homeassistant/components/random/binary_sensor.py | 10 +++++----- homeassistant/components/random/sensor.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/random/binary_sensor.py b/homeassistant/components/random/binary_sensor.py index 8c2608ad81dd38..e502439b28c32d 100644 --- a/homeassistant/components/random/binary_sensor.py +++ b/homeassistant/components/random/binary_sensor.py @@ -1,15 +1,16 @@ """Support for showing random states.""" import logging +from random import getrandbits import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import ( - BinarySensorDevice, - PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorDevice, ) -from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -57,6 +58,5 @@ def device_class(self): async def async_update(self): """Get new state and update the sensor's state.""" - from random import getrandbits self._state = bool(getrandbits(1)) diff --git a/homeassistant/components/random/sensor.py b/homeassistant/components/random/sensor.py index 5d4e3d0d57a0e8..4ebd710f1031c9 100644 --- a/homeassistant/components/random/sensor.py +++ b/homeassistant/components/random/sensor.py @@ -1,5 +1,6 @@ """Support for showing random numbers.""" import logging +from random import randrange import voluptuous as vol @@ -82,6 +83,5 @@ def device_state_attributes(self): async def async_update(self): """Get a new number and updates the states.""" - from random import randrange self._state = randrange(self._minimum, self._maximum + 1) From c6066d8b9877e067f2fe9cd9a4f48ca24b663db1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:13:28 +0100 Subject: [PATCH 2067/3953] Move imports to top for ring (#29474) --- homeassistant/components/ring/__init__.py | 11 +++++------ homeassistant/components/ring/camera.py | 8 ++++---- homeassistant/components/ring/light.py | 5 +++-- homeassistant/components/ring/sensor.py | 4 ++-- homeassistant/components/ring/switch.py | 5 +++-- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 006b3ae3e9abdf..a68749b2c67a73 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -1,14 +1,15 @@ """Support for Ring Doorbell/Chimes.""" +from datetime import timedelta import logging -from datetime import timedelta from requests.exceptions import ConnectTimeout, HTTPError +from ring_doorbell import Ring import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL -from homeassistant.helpers.event import track_time_interval -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) @@ -50,8 +51,6 @@ def setup(hass, config): scan_interval = conf[CONF_SCAN_INTERVAL] try: - from ring_doorbell import Ring - cache = hass.config.path(DEFAULT_CACHEDB) ring = Ring(username=username, password=password, cache_file=cache) if not ring.is_connected: diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 461b3a199d7721..1d2fe6ff67b0a5 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -3,16 +3,18 @@ from datetime import timedelta import logging +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.util import dt as dt_util from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.core import callback +from homeassistant.util import dt as dt_util from . import ( ATTRIBUTION, @@ -122,7 +124,6 @@ def device_state_attributes(self): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) @@ -140,7 +141,6 @@ async def async_camera_image(self): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg if self._video_url is None: return diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 697be4d1579220..bc86e5b5fd1925 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -1,9 +1,10 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" -import logging from datetime import timedelta +import logging + from homeassistant.components.light import Light -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 6a64226a053e97..b54c750664efc1 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -9,11 +9,11 @@ CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.core import callback from . import ( ATTRIBUTION, diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 413d2a70aae2ba..16fc4a6717fe15 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -1,9 +1,10 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" -import logging from datetime import timedelta +import logging + from homeassistant.components.switch import SwitchDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING From 653e0078a4581e8a486b3e38092a93fef73939d5 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:13:49 +0100 Subject: [PATCH 2068/3953] Move imports to top for skybell (#29475) --- homeassistant/components/skybell/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index fd01b6d22c9d35..a4e4263d360274 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -1,10 +1,11 @@ """Support for the Skybell HD Doorbell.""" import logging -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError +from skybellpy import Skybell import voluptuous as vol -from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -39,8 +40,6 @@ def setup(hass, config): password = conf.get(CONF_PASSWORD) try: - from skybellpy import Skybell - cache = hass.config.path(DEFAULT_CACHEDB) skybell = Skybell( username=username, password=password, get_devices=True, cache_path=cache From 7df4c1e676cd50d193347d446cffadae6d507213 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:14:03 +0100 Subject: [PATCH 2069/3953] Move imports to top for mhz19 (#29478) --- homeassistant/components/mhz19/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py index 460decd41b6739..36f3a4d426de81 100644 --- a/homeassistant/components/mhz19/sensor.py +++ b/homeassistant/components/mhz19/sensor.py @@ -1,20 +1,21 @@ """Support for CO2 sensor connected to a serial port.""" -import logging from datetime import timedelta +import logging +from pmsensor import co2sensor import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_TEMPERATURE, - CONF_NAME, CONF_MONITORED_CONDITIONS, + CONF_NAME, TEMP_FAHRENHEIT, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.util.temperature import celsius_to_fahrenheit +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from homeassistant.util.temperature import celsius_to_fahrenheit _LOGGER = logging.getLogger(__name__) @@ -41,7 +42,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available CO2 sensors.""" - from pmsensor import co2sensor try: co2sensor.read_mh_z19(config.get(CONF_SERIAL_DEVICE)) From 12e1c695cae5f4b8c8f7964b50a893a57421c566 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:14:26 +0100 Subject: [PATCH 2070/3953] Move imports to top for mfi (#29479) --- homeassistant/components/mfi/sensor.py | 17 ++++++++--------- homeassistant/components/mfi/switch.py | 9 ++++----- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index 5d9b3be738a6aa..671a52bbf01635 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -1,23 +1,24 @@ """Support for Ubiquiti mFi sensors.""" import logging +from mficlient.client import FailedToLogin, MFiClient import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_PASSWORD, - CONF_USERNAME, - TEMP_CELSIUS, - STATE_ON, - STATE_OFF, CONF_HOST, + CONF_PASSWORD, + CONF_PORT, CONF_SSL, + CONF_USERNAME, CONF_VERIFY_SSL, - CONF_PORT, + STATE_OFF, + STATE_ON, + TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -57,8 +58,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): default_port = 6443 if use_tls else 6080 port = int(config.get(CONF_PORT, default_port)) - from mficlient.client import FailedToLogin, MFiClient - try: client = MFiClient( host, username, password, port=port, use_tls=use_tls, verify=verify_tls diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 1da09e7f78cdc0..00cb23a102ef65 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -1,16 +1,17 @@ """Support for Ubiquiti mFi switches.""" import logging +from mficlient.client import FailedToLogin, MFiClient import requests import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, - CONF_PORT, CONF_PASSWORD, - CONF_USERNAME, + CONF_PORT, CONF_SSL, + CONF_USERNAME, CONF_VERIFY_SSL, ) import homeassistant.helpers.config_validation as cv @@ -44,8 +45,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): default_port = 6443 if use_tls else 6080 port = int(config.get(CONF_PORT, default_port)) - from mficlient.client import FailedToLogin, MFiClient - try: client = MFiClient( host, username, password, port=port, use_tls=use_tls, verify=verify_tls From 81e4e9e26b6f6b2079eb647a9a2c023ff36f10b7 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:14:39 +0100 Subject: [PATCH 2071/3953] Move imports to top for mailgun (#29480) --- homeassistant/components/mailgun/__init__.py | 3 +-- homeassistant/components/mailgun/config_flow.py | 2 +- homeassistant/components/mailgun/notify.py | 9 ++++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py index 4bcca0848f43f9..57c83d8c20cfe2 100644 --- a/homeassistant/components/mailgun/__init__.py +++ b/homeassistant/components/mailgun/__init__.py @@ -6,13 +6,12 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_API_KEY, CONF_DOMAIN, CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) CONF_SANDBOX = "sandbox" diff --git a/homeassistant/components/mailgun/config_flow.py b/homeassistant/components/mailgun/config_flow.py index c575b4c0354cd3..6fe87e7cbf4aef 100644 --- a/homeassistant/components/mailgun/config_flow.py +++ b/homeassistant/components/mailgun/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Mailgun.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/mailgun/notify.py b/homeassistant/components/mailgun/notify.py index efa5a17430ca94..c2222cfd742b59 100644 --- a/homeassistant/components/mailgun/notify.py +++ b/homeassistant/components/mailgun/notify.py @@ -1,6 +1,12 @@ """Support for the Mailgun mail notifications.""" import logging +from pymailgunner import ( + Client, + MailgunCredentialsError, + MailgunDomainError, + MailgunError, +) import voluptuous as vol from homeassistant.components.notify import ( @@ -58,7 +64,6 @@ def __init__(self, domain, sandbox, api_key, sender, recipient): def initialize_client(self): """Initialize the connection to Mailgun.""" - from pymailgunner import Client self._client = Client(self._api_key, self._domain, self._sandbox) _LOGGER.debug("Mailgun domain: %s", self._client.domain) @@ -68,7 +73,6 @@ def initialize_client(self): def connection_is_valid(self): """Check whether the provided credentials are valid.""" - from pymailgunner import MailgunCredentialsError, MailgunDomainError try: self.initialize_client() @@ -82,7 +86,6 @@ def connection_is_valid(self): def send_message(self, message="", **kwargs): """Send a mail to the recipient.""" - from pymailgunner import MailgunError subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) data = kwargs.get(ATTR_DATA) From 3e634aaf5d53072543caadf984a315d913d0a5af Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:14:57 +0100 Subject: [PATCH 2072/3953] Move imports to top for logi_circle (#29482) --- homeassistant/components/logi_circle/__init__.py | 6 +++--- homeassistant/components/logi_circle/config_flow.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index f7ed3a73fce033..b77f17101a8621 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -2,7 +2,10 @@ import asyncio import logging +from aiohttp.client_exceptions import ClientResponseError import async_timeout +from logi_circle import LogiCircle +from logi_circle.exception import AuthorizationFailed import voluptuous as vol from homeassistant import config_entries @@ -116,9 +119,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Logi Circle from a config entry.""" - from logi_circle import LogiCircle - from logi_circle.exception import AuthorizationFailed - from aiohttp.client_exceptions import ClientResponseError logi_circle = LogiCircle( client_id=entry.data[CONF_CLIENT_ID], diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index 2a25c5f00a49fd..ce8460233d6148 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -3,6 +3,8 @@ from collections import OrderedDict import async_timeout +from logi_circle import LogiCircle +from logi_circle.exception import AuthorizationFailed import voluptuous as vol from homeassistant import config_entries @@ -120,7 +122,6 @@ async def async_step_auth(self, user_input=None): def _get_authorization_url(self): """Create temporary Circle session and generate authorization url.""" - from logi_circle import LogiCircle flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] client_id = flow[CONF_CLIENT_ID] @@ -148,8 +149,6 @@ async def async_step_code(self, code=None): async def _async_create_session(self, code): """Create Logi Circle session and entries.""" - from logi_circle import LogiCircle - from logi_circle.exception import AuthorizationFailed flow = self.hass.data[DATA_FLOW_IMPL][DOMAIN] client_id = flow[CONF_CLIENT_ID] From 95de802fbd335d78fb313de2b0af71ba59582761 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:15:18 +0100 Subject: [PATCH 2073/3953] Move imports to top for hlk_sw16 (#29483) --- homeassistant/components/hlk_sw16/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index f174b00613b33d..e7264c4e0dd93b 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -1,23 +1,24 @@ """Support for HLK-SW16 relay switches.""" import logging +from hlk_sw16 import create_hlk_sw16_connection import voluptuous as vol from homeassistant.const import ( CONF_HOST, + CONF_NAME, CONF_PORT, - EVENT_HOMEASSISTANT_STOP, CONF_SWITCHES, - CONF_NAME, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -59,7 +60,6 @@ async def async_setup(hass, config): """Set up the HLK-SW16 switch.""" # Allow platform to specify function to register new unknown devices - from hlk_sw16 import create_hlk_sw16_connection hass.data[DATA_DEVICE_REGISTER] = {} From bbe57d6673047ab1bd6930ba5e12b04e8d324f2b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:15:39 +0100 Subject: [PATCH 2074/3953] Move imports to top for history (#29484) --- homeassistant/components/history/__init__.py | 22 +++++++------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 65607d0f8bfa04..133151c7f735f3 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -5,22 +5,23 @@ import logging import time +from sqlalchemy import and_, func import voluptuous as vol +from homeassistant.components import recorder, script +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.recorder.models import States +from homeassistant.components.recorder.util import execute, session_scope from homeassistant.const import ( - HTTP_BAD_REQUEST, + ATTR_HIDDEN, CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, + HTTP_BAD_REQUEST, ) -import homeassistant.util.dt as dt_util -from homeassistant.components import recorder, script -from homeassistant.components.http import HomeAssistantView -from homeassistant.const import ATTR_HIDDEN -from homeassistant.components.recorder.util import session_scope, execute import homeassistant.helpers.config_validation as cv - +import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs @@ -58,7 +59,6 @@ def get_significant_states( thermostat so that we get current temperature in our graphs). """ timer_start = time.perf_counter() - from homeassistant.components.recorder.models import States with session_scope(hass=hass) as session: query = session.query(States).filter( @@ -94,7 +94,6 @@ def get_significant_states( def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): """Return states changes during UTC period start_time - end_time.""" - from homeassistant.components.recorder.models import States with session_scope(hass=hass) as session: query = session.query(States).filter( @@ -117,7 +116,6 @@ def state_changes_during_period(hass, start_time, end_time=None, entity_id=None) def get_last_state_changes(hass, number_of_states, entity_id): """Return the last number_of_states.""" - from homeassistant.components.recorder.models import States start_time = dt_util.utcnow() @@ -142,7 +140,6 @@ def get_last_state_changes(hass, number_of_states, entity_id): def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): """Return the states at a specific point in time.""" - from homeassistant.components.recorder.models import States if run is None: run = recorder.run_information(hass, utc_point_in_time) @@ -151,8 +148,6 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None) if run is None: return [] - from sqlalchemy import and_, func - with session_scope(hass=hass) as session: query = session.query(States) @@ -386,7 +381,6 @@ def apply(self, query, entity_ids=None): * if include and exclude is defined - select the entities specified in the include and filter out the ones from the exclude list. """ - from homeassistant.components.recorder.models import States # specific entities requested - do not in/exclude anything if entity_ids is not None: From f5550509d4f7f23f3bd3824b348edc62f4a22068 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:16:25 +0100 Subject: [PATCH 2075/3953] Move imports to top for geo_json_events (#29486) --- homeassistant/components/geo_json_events/geo_location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index 2bf309e2450136..2f881232495d0e 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -3,6 +3,7 @@ import logging from typing import Optional +from geojson_client.generic_feed import GenericFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -71,7 +72,6 @@ def __init__( self, hass, add_entities, scan_interval, coordinates, url, radius_in_km ): """Initialize the GeoJSON Feed Manager.""" - from geojson_client.generic_feed import GenericFeedManager self._hass = hass self._feed_manager = GenericFeedManager( From 8647ba3dd573c59e491dd92e6d791eede257f042 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:16:51 +0100 Subject: [PATCH 2076/3953] Move imports to top for emulated_hue (#29488) --- homeassistant/components/emulated_roku/binding.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/emulated_roku/binding.py b/homeassistant/components/emulated_roku/binding.py index 4c98af69848bdc..a44effff55a0ed 100644 --- a/homeassistant/components/emulated_roku/binding.py +++ b/homeassistant/components/emulated_roku/binding.py @@ -1,6 +1,8 @@ """Bridge between emulated_roku and Home Assistant.""" import logging +from emulated_roku import EmulatedRokuCommandHandler, EmulatedRokuServer + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState, EventOrigin @@ -51,7 +53,6 @@ def __init__( async def setup(self): """Start the emulated_roku server.""" - from emulated_roku import EmulatedRokuServer, EmulatedRokuCommandHandler class EventCommandHandler(EmulatedRokuCommandHandler): """emulated_roku command handler to turn commands into events.""" From 89c76292153a397e84af0121067c6a000fb68b61 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:17:18 +0100 Subject: [PATCH 2077/3953] Move imports to top for deconz (#29489) --- homeassistant/components/deconz/binary_sensor.py | 2 +- homeassistant/components/deconz/config_flow.py | 7 +++---- homeassistant/components/deconz/cover.py | 4 ++-- homeassistant/components/deconz/device_trigger.py | 1 - homeassistant/components/deconz/gateway.py | 7 +++---- homeassistant/components/deconz/light.py | 2 +- homeassistant/components/deconz/sensor.py | 2 +- homeassistant/components/deconz/services.py | 2 +- 8 files changed, 12 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 1a4d9680c1e0c1..0fdc5904c2db53 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -8,7 +8,7 @@ from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry, DeconzEntityHandler +from .gateway import DeconzEntityHandler, get_gateway_from_config_entry ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index b9a299230ad85d..75e33ea6cafb87 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -2,12 +2,12 @@ import asyncio import async_timeout -import voluptuous as vol - -from pydeconz.errors import ResponseError, RequestError +from pydeconz.errors import RequestError, ResponseError from pydeconz.utils import async_discovery, async_get_api_key, async_get_gateway_config +import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -170,7 +170,6 @@ async def _update_entry(self, entry, host): async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" - from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL: return self.async_abort(reason="not_deconz_bridge") diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index bcd408c25a7591..6e5e616fbb896a 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -1,11 +1,11 @@ """Support for deCONZ covers.""" from homeassistant.components.cover import ( ATTR_POSITION, - CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, - SUPPORT_STOP, SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverDevice, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index e1f6c4fa135758..b6691548b877fb 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -2,7 +2,6 @@ import voluptuous as vol import homeassistant.components.automation.event as event - from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 75898b0fdab819..0c77285a6fe598 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -1,12 +1,12 @@ """Representation of a deCONZ gateway.""" import asyncio -import async_timeout +import async_timeout from pydeconz import DeconzSession, errors -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_HOST from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import ( @@ -14,8 +14,8 @@ async_dispatcher_send, ) from homeassistant.helpers.entity_registry import ( - async_get_registry, DISABLED_CONFIG_ENTRY, + async_get_registry, ) from .const import ( @@ -30,7 +30,6 @@ NEW_DEVICE, SUPPORTED_PLATFORMS, ) - from .errors import AuthenticationRequired, CannotConnect diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index eda2041b923116..af708a15391798 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -29,7 +29,7 @@ SWITCH_TYPES, ) from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry, DeconzEntityHandler +from .gateway import DeconzEntityHandler, get_gateway_from_config_entry async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 3a3dbceb46bd6b..4c854a0ec11578 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -11,7 +11,7 @@ from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice from .deconz_event import DeconzEvent -from .gateway import get_gateway_from_config_entry, DeconzEntityHandler +from .gateway import DeconzEntityHandler, get_gateway_from_config_entry ATTR_CURRENT = "current" ATTR_POWER = "power" diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 3498b46d879c8f..8efdc2718bcc97 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -4,7 +4,7 @@ from homeassistant.helpers import config_validation as cv from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, DOMAIN, _LOGGER +from .const import _LOGGER, CONF_BRIDGEID, DOMAIN DECONZ_SERVICES = "deconz_services" From 270d97d7894d1d7c92ac4e359013cf12a044c0cc Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:17:33 +0100 Subject: [PATCH 2078/3953] Move imports to top for daikin (#29490) --- homeassistant/components/daikin/__init__.py | 2 +- homeassistant/components/daikin/climate.py | 2 +- homeassistant/components/daikin/config_flow.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 7d476e17647f7b..209bf71e594fbb 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -5,6 +5,7 @@ from aiohttp import ClientConnectionError from async_timeout import timeout +from pydaikin.appliance import Appliance import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -87,7 +88,6 @@ async def async_unload_entry(hass, config_entry): async def daikin_api_setup(hass, host): """Create a Daikin instance only once.""" - from pydaikin.appliance import Appliance session = hass.helpers.aiohttp_client.async_get_clientsession() try: diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index ddc5353250ce57..d46ea26d4870db 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -1,6 +1,7 @@ """Support for the Daikin HVAC.""" import logging +from pydaikin import appliance import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice @@ -91,7 +92,6 @@ class DaikinClimate(ClimateDevice): def __init__(self, api): """Initialize the climate device.""" - from pydaikin import appliance self._api = api self._list = { diff --git a/homeassistant/components/daikin/config_flow.py b/homeassistant/components/daikin/config_flow.py index 36d8ef0d3835eb..bd90a87db86e25 100644 --- a/homeassistant/components/daikin/config_flow.py +++ b/homeassistant/components/daikin/config_flow.py @@ -4,6 +4,7 @@ from aiohttp import ClientError from async_timeout import timeout +from pydaikin.appliance import Appliance import voluptuous as vol from homeassistant import config_entries @@ -32,7 +33,6 @@ async def _create_entry(self, host, mac): async def _create_device(self, host): """Create device.""" - from pydaikin.appliance import Appliance try: device = Appliance( From dbd231b3a04c2a6bd07e0ae39f9f36bec5196a03 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:17:45 +0100 Subject: [PATCH 2079/3953] Move imports to top for somfy_mylink (#29464) --- homeassistant/components/somfy_mylink/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index 394de5980ea464..6c6333af5a666f 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -1,6 +1,7 @@ """Component for the Somfy MyLink device supporting the Synergy API.""" import logging +from somfy_mylink_synergy import SomfyMyLinkSynergy import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT @@ -48,7 +49,6 @@ def validate_entity_config(values): async def async_setup(hass, config): """Set up the MyLink platform.""" - from somfy_mylink_synergy import SomfyMyLinkSynergy host = config[DOMAIN][CONF_HOST] port = config[DOMAIN][CONF_PORT] From 24878f109c3ae2c8d2d4c5166623624ebaac6d74 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:18:12 +0100 Subject: [PATCH 2080/3953] Move imports to top for solaredge (#29463) --- homeassistant/components/solaredge/__init__.py | 2 +- homeassistant/components/solaredge/config_flow.py | 4 ++-- homeassistant/components/solaredge/const.py | 2 +- homeassistant/components/solaredge/sensor.py | 7 ++++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 8909b970aafd2b..bafc6b67f1c828 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -6,7 +6,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType -from .const import DEFAULT_NAME, DOMAIN, CONF_SITE_ID +from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 67f05d83aa0f17..7c8c9380522b20 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -1,14 +1,14 @@ """Config flow for the SolarEdge platform.""" +from requests.exceptions import ConnectTimeout, HTTPError import solaredge import voluptuous as vol -from requests.exceptions import HTTPError, ConnectTimeout from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify -from .const import DOMAIN, DEFAULT_NAME, CONF_SITE_ID +from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN @callback diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 0d3d1a0cb5fff7..6fec88c42d5520 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -1,7 +1,7 @@ """Constants for the SolarEdge Monitoring API.""" from datetime import timedelta -from homeassistant.const import POWER_WATT, ENERGY_WATT_HOUR +from homeassistant.const import ENERGY_WATT_HOUR, POWER_WATT DOMAIN = "solaredge" diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 896596a2a34d01..f0f1660a821a36 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -1,17 +1,19 @@ """Support for SolarEdge Monitoring API.""" import logging + +from requests.exceptions import ConnectTimeout, HTTPError import solaredge +from stringcase import snakecase -from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.const import CONF_API_KEY from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from .const import ( CONF_SITE_ID, - OVERVIEW_UPDATE_DELAY, DETAILS_UPDATE_DELAY, INVENTORY_UPDATE_DELAY, + OVERVIEW_UPDATE_DELAY, POWER_FLOW_UPDATE_DELAY, SENSOR_TYPES, ) @@ -262,7 +264,6 @@ def __init__(self, api, site_id): @Throttle(DETAILS_UPDATE_DELAY) def update(self): """Update the data from the SolarEdge Monitoring API.""" - from stringcase import snakecase try: data = self.api.get_details(self.site_id) From a4ac5dc6b86730fbea989b4658ca7da7e7bcef0e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:18:29 +0100 Subject: [PATCH 2081/3953] Move imports to top for entur_public_transport (#29459) --- homeassistant/components/entur_public_transport/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 0f8324ded9e099..2ecae21824eeb5 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta import logging +from enturclient import EnturPublicTransportData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -87,7 +88,6 @@ def due_in_minutes(timestamp: datetime) -> int: async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Entur public transport sensor.""" - from enturclient import EnturPublicTransportData expand = config.get(CONF_EXPAND_PLATFORMS) line_whitelist = config.get(CONF_WHITELIST_LINES) From 58ba6052e21f05265d56c1d9862954be32d0c043 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:18:52 +0100 Subject: [PATCH 2082/3953] Move imports to top for environment_canada (#29458) --- homeassistant/components/environment_canada/camera.py | 8 ++++---- homeassistant/components/environment_canada/sensor.py | 10 +++++----- homeassistant/components/environment_canada/weather.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 2a23fb95a18f0b..4ef3e17fc4683e 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -7,17 +7,18 @@ import datetime import logging +from env_canada import ECRadar import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import ( - CONF_NAME, + ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ATTRIBUTION, + CONF_NAME, ) -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -46,7 +47,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Environment Canada camera.""" - from env_canada import ECRadar if config.get(CONF_STATION): radar_object = ECRadar( diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index b3e4d7ae3df668..1568ba19d6b7b2 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -8,18 +8,19 @@ import logging import re +from env_canada import ECData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_CELSIUS, - CONF_LATITUDE, - CONF_LONGITUDE, ATTR_ATTRIBUTION, ATTR_LOCATION, + CONF_LATITUDE, + CONF_LONGITUDE, + TEMP_CELSIUS, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -56,7 +57,6 @@ def validate_station(station): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Environment Canada sensor.""" - from env_canada import ECData if config.get(CONF_STATION): ec_data = ECData( diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index a4fad083d2a6e0..572543e39c4c19 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -20,8 +20,8 @@ WeatherEntity, ) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS -import homeassistant.util.dt as dt import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt _LOGGER = logging.getLogger(__name__) From 6c863a8948fa5da923b67414e01b2156d97e9296 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:18:57 +0100 Subject: [PATCH 2083/3953] Move imports to top for sonarr (#29462) --- homeassistant/components/sonarr/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index 47738521bf06dd..82bcdad6ef4599 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -1,20 +1,21 @@ """Support for Sonarr.""" +from datetime import datetime import logging import time -from datetime import datetime +from pytz import timezone import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, CONF_HOST, - CONF_PORT, CONF_MONITORED_CONDITIONS, + CONF_PORT, CONF_SSL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -80,7 +81,6 @@ class SonarrSensor(Entity): def __init__(self, hass, conf, sensor_type): """Create Sonarr entity.""" - from pytz import timezone self.conf = conf self.host = conf.get(CONF_HOST) From ebc9d175581b28f95757685a76dfe54c8619f132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 5 Dec 2019 07:19:25 +0200 Subject: [PATCH 2084/3953] Fix setup of Huawei LTE for which we can't get a MAC address (#29455) Closes https://github.com/home-assistant/home-assistant/issues/29188 --- homeassistant/components/huawei_lte/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index c4c251aef506e6..ada1f0a8abdcdc 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -148,7 +148,7 @@ def device_name(self) -> str: @property def device_connections(self) -> Set[Tuple[str, str]]: """Get router connections for device registry.""" - return {(dr.CONNECTION_NETWORK_MAC, self.mac)} + return {(dr.CONNECTION_NETWORK_MAC, self.mac)} if self.mac else set() def update(self) -> None: """Update router data.""" From 06a156c091cde4aecacd07b931c8a67fc607e356 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:19:43 +0100 Subject: [PATCH 2085/3953] Move imports to top for emby (#29453) --- homeassistant/components/emby/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index d8a98a96585903..57f781deceb747 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -1,9 +1,10 @@ """Support to interface with the Emby API.""" import logging +from pyemby import EmbyServer import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -70,7 +71,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Emby platform.""" - from pyemby import EmbyServer host = config.get(CONF_HOST) key = config.get(CONF_API_KEY) From f422cdbfef1ab029018869d90f4f3e9dd50a7ef9 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:20:01 +0100 Subject: [PATCH 2086/3953] Move imports to top for eq3btsmart (#29456) --- homeassistant/components/eq3btsmart/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index cee059972940bf..d0b60c74443da1 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -1,6 +1,8 @@ """Support for eQ-3 Bluetooth Smart thermostats.""" import logging +# pylint: disable=import-error +from bluepy.btle import BTLEException import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol @@ -11,9 +13,9 @@ HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -190,8 +192,6 @@ def set_preset_mode(self, preset_mode): def update(self): """Update the data from the thermostat.""" - # pylint: disable=import-error - from bluepy.btle import BTLEException try: self._thermostat.update() From cdf53aed9315a06ff019befd550aa2896f18952e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:20:11 +0100 Subject: [PATCH 2087/3953] Move imports to top for enigma2 (#29452) --- .../components/enigma2/media_player.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 5b0e705f3929da..85dec4abd94f92 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -1,35 +1,36 @@ """Support for Enigma2 media players.""" import logging +from openwebif.api import CreateDevice import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( + MEDIA_TYPE_TVSHOW, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, - SUPPORT_TURN_ON, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_STOP, - SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_STEP, - MEDIA_TYPE_TVSHOW, ) from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, + CONF_PORT, CONF_SSL, + CONF_USERNAME, STATE_OFF, STATE_ON, STATE_PLAYING, - CONF_PORT, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -101,8 +102,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config[CONF_DEEP_STANDBY] = DEFAULT_DEEP_STANDBY config[CONF_SOURCE_BOUQUET] = DEFAULT_SOURCE_BOUQUET - from openwebif.api import CreateDevice - device = CreateDevice( host=config[CONF_HOST], port=config.get(CONF_PORT), From 5586f40ab0f40f125dd7da0b620625ea431bd988 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:20:26 +0100 Subject: [PATCH 2088/3953] Move imports to top for enocean (#29451) --- homeassistant/components/enocean/__init__.py | 9 ++++----- homeassistant/components/enocean/sensor.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/enocean/__init__.py b/homeassistant/components/enocean/__init__.py index d01af51c7e37fe..876c7a1f05baab 100644 --- a/homeassistant/components/enocean/__init__.py +++ b/homeassistant/components/enocean/__init__.py @@ -1,11 +1,14 @@ """Support for EnOcean devices.""" import logging +from enocean.communicators.serialcommunicator import SerialCommunicator +from enocean.protocol.packet import Packet, RadioPacket +from enocean.utils import combine_hex import voluptuous as vol from homeassistant.const import CONF_DEVICE -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -34,7 +37,6 @@ class EnOceanDongle: def __init__(self, hass, ser): """Initialize the EnOcean dongle.""" - from enocean.communicators.serialcommunicator import SerialCommunicator self.__communicator = SerialCommunicator(port=ser, callback=self.callback) self.__communicator.start() @@ -53,7 +55,6 @@ def callback(self, packet): This is the callback function called by python-enocan whenever there is an incoming packet. """ - from enocean.protocol.packet import RadioPacket if isinstance(packet, RadioPacket): _LOGGER.debug("Received radio packet: %s", packet) @@ -76,7 +77,6 @@ async def async_added_to_hass(self): def _message_received_callback(self, packet): """Handle incoming packets.""" - from enocean.utils import combine_hex if packet.sender_int == combine_hex(self.dev_id): self.value_changed(packet) @@ -86,7 +86,6 @@ def value_changed(self, packet): def send_command(self, data, optional, packet_type): """Send a command via the EnOcean dongle.""" - from enocean.protocol.packet import Packet packet = Packet(packet_type, data=data, optional=optional) self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_SEND_MESSAGE, packet) diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 2e6b5bdb986da6..cfab52b3665954 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -11,8 +11,8 @@ CONF_NAME, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - TEMP_CELSIUS, POWER_WATT, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv From 0158f29e4eb5d9b54a4cd47344334a86617c11d3 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:21:12 +0100 Subject: [PATCH 2089/3953] Move imports to top for envisalink (#29457) --- homeassistant/components/envisalink/__init__.py | 6 +++--- homeassistant/components/envisalink/alarm_control_panel.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 6cdedf897446d2..14113537de6e98 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -2,14 +2,15 @@ import asyncio import logging +from pyenvisalink import EnvisalinkAlarmPanel import voluptuous as vol +from homeassistant.const import CONF_HOST, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_TIMEOUT, CONF_HOST -from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -98,7 +99,6 @@ async def async_setup(hass, config): """Set up for Envisalink devices.""" - from pyenvisalink import EnvisalinkAlarmPanel conf = config.get(DOMAIN) diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 00f39bece9fa3f..7630169dcadaf0 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -4,8 +4,8 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - AlarmControlPanel, FORMAT_NUMBER, + AlarmControlPanel, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, From 10759b7ca603e92d93a9d6a273ab0cb796efb808 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:21:25 +0100 Subject: [PATCH 2090/3953] Move imports to top for enphase_envoy (#29450) --- homeassistant/components/enphase_envoy/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 13784e24d77fc9..3977326c06ddf9 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,19 +1,19 @@ """Support for Enphase Envoy solar energy monitor.""" import logging +from envoy_reader.envoy_reader import EnvoyReader import voluptuous as vol -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, CONF_NAME, - POWER_WATT, ENERGY_WATT_HOUR, + POWER_WATT, ) - +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -52,7 +52,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Enphase Envoy sensor.""" - from envoy_reader.envoy_reader import EnvoyReader ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] @@ -118,7 +117,6 @@ def icon(self): async def async_update(self): """Get the energy production data from the Enphase Envoy.""" - from envoy_reader.envoy_reader import EnvoyReader if self._type != "inverters": _state = await getattr(EnvoyReader(self._ip_address), self._type)() From 85e188db52222faa21393df82a278e4e74c36d43 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:21:40 +0100 Subject: [PATCH 2091/3953] Move imports to top for egardia (#29448) --- homeassistant/components/egardia/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py index 9e11f522dd53d0..efe477364791d9 100644 --- a/homeassistant/components/egardia/__init__.py +++ b/homeassistant/components/egardia/__init__.py @@ -1,6 +1,7 @@ """Interfaces with Egardia/Woonveilig alarm control panel.""" import logging +from pythonegardia import egardiadevice, egardiaserver import requests import voluptuous as vol @@ -78,8 +79,6 @@ def setup(hass, config): """Set up the Egardia platform.""" - from pythonegardia import egardiadevice - from pythonegardia import egardiaserver conf = config[DOMAIN] username = conf.get(CONF_USERNAME) From b2cce05405c93b31799d65e2ceedc54ed6368642 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:21:50 +0100 Subject: [PATCH 2092/3953] Move imports to top for eight_sleep (#29447) --- homeassistant/components/eight_sleep/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 923c3f7d309124..a8a5a6e1fccd01 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -1,23 +1,24 @@ """Support for Eight smart mattress covers and mattresses.""" -import logging from datetime import timedelta +import logging +from pyeight.eight import EightSleep import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( - CONF_USERNAME, + ATTR_ENTITY_ID, + CONF_BINARY_SENSORS, CONF_PASSWORD, CONF_SENSORS, - CONF_BINARY_SENSORS, - ATTR_ENTITY_ID, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time @@ -90,7 +91,6 @@ async def async_setup(hass, config): """Set up the Eight Sleep component.""" - from pyeight.eight import EightSleep conf = config.get(DOMAIN) user = conf.get(CONF_USERNAME) From 94297e96cb6743c0defa1d68a3c3dae695c8054c Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 5 Dec 2019 06:22:03 +0100 Subject: [PATCH 2093/3953] Update eternalegypt to 0.0.11 (#29446) --- homeassistant/components/netgear_lte/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 7e085d063077aa..f8c4c39cb83ada 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/integrations/netgear_lte", "requirements": [ - "eternalegypt==0.0.10" + "eternalegypt==0.0.11" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 970b844d8c96d0..c3b266c8110b06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -493,7 +493,7 @@ epson-projector==0.1.3 epsonprinter==0.0.9 # homeassistant.components.netgear_lte -eternalegypt==0.0.10 +eternalegypt==0.0.11 # homeassistant.components.keyboard_remote # evdev==1.1.2 From f2a08c438b225e5473da89a91f396668d1c3f355 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:22:14 +0100 Subject: [PATCH 2094/3953] Move imports to top for duke_energy (#29445) --- homeassistant/components/duke_energy/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duke_energy/sensor.py b/homeassistant/components/duke_energy/sensor.py index 998809decc02f4..cd30ae96caf334 100644 --- a/homeassistant/components/duke_energy/sensor.py +++ b/homeassistant/components/duke_energy/sensor.py @@ -1,12 +1,13 @@ """Support for Duke Energy Gas and Electric meters.""" import logging +from pydukeenergy.api import DukeEnergy, DukeEnergyException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up all Duke Energy meters.""" - from pydukeenergy.api import DukeEnergy, DukeEnergyException try: duke = DukeEnergy( From 50ee0c6727a67ae4561f835ff448117c31804056 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:22:27 +0100 Subject: [PATCH 2095/3953] Move imports to top for dunehd (#29444) --- homeassistant/components/dunehd/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index 95e8cac3dbda17..bb32bff2a15a2a 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -1,7 +1,8 @@ """DuneHD implementation of the media player.""" +from pdunehd import DuneHDPlayer import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -46,7 +47,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the DuneHD media player platform.""" - from pdunehd import DuneHDPlayer sources = config.get(CONF_SOURCES, {}) host = config.get(CONF_HOST) From 27200865eef518ce5de8566c8a7c76a11a080bcf Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:22:37 +0100 Subject: [PATCH 2096/3953] Move imports to top for dlink (#29443) --- homeassistant/components/dlink/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 25091e14dbd344..7fa391e8060619 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -3,6 +3,7 @@ import logging import urllib +from pyW215.pyW215 import SmartPlug import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -42,7 +43,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a D-Link Smart Plug.""" - from pyW215.pyW215 import SmartPlug host = config.get(CONF_HOST) username = config.get(CONF_USERNAME) From 5ee20b61b341a38bf2f5c1034d80a163d4e17723 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:22:51 +0100 Subject: [PATCH 2097/3953] Move imports to top for doorbird (#29442) --- homeassistant/components/doorbird/__init__.py | 2 +- homeassistant/components/doorbird/camera.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 3127dabd66d4c1..d92ff3d36924b3 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -2,6 +2,7 @@ import logging from urllib.error import HTTPError +from doorbirdpy import DoorBird import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -51,7 +52,6 @@ def setup(hass, config): """Set up the DoorBird component.""" - from doorbirdpy import DoorBird # Provide an endpoint for the doorstations to call to trigger events hass.http.register_view(DoorBirdRequestView) diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 457c319d9e1654..d9a802f071f5ca 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -6,7 +6,7 @@ import aiohttp import async_timeout -from homeassistant.components.camera import Camera, SUPPORT_STREAM +from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util From 277332813483527d840c07507b1cb1fccb434e13 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:23:05 +0100 Subject: [PATCH 2098/3953] Move imports to top for directv (#29441) --- homeassistant/components/directv/media_player.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 2be2544cec1cd3..5dd673ca93f9fe 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -1,9 +1,11 @@ """Support for the DirecTV receivers.""" import logging + +from DirectPy import DIRECTV import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -99,8 +101,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # Attempt to discover additional RVU units _LOGGER.debug("Doing discovery of DirecTV devices on %s", host) - from DirectPy import DIRECTV - dtv = DIRECTV(host, DEFAULT_PORT) try: resp = dtv.get_locations() @@ -156,7 +156,6 @@ class DirecTvDevice(MediaPlayerDevice): def __init__(self, name, host, port, device): """Initialize the device.""" - from DirectPy import DIRECTV self.dtv = DIRECTV(host, port, device) self._name = name From 4c0f73a2b9b238737b460050eb9a1c15d87b001f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:23:17 +0100 Subject: [PATCH 2099/3953] Move imports to top for datadog (#29440) --- homeassistant/components/datadog/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/datadog/__init__.py b/homeassistant/components/datadog/__init__.py index 5517e41d5c66df..adb8bb1f95c795 100644 --- a/homeassistant/components/datadog/__init__.py +++ b/homeassistant/components/datadog/__init__.py @@ -1,6 +1,7 @@ """Support for sending data to Datadog.""" import logging +from datadog import initialize, statsd import voluptuous as vol from homeassistant.const import ( @@ -42,7 +43,6 @@ def setup(hass, config): """Set up the Datadog component.""" - from datadog import initialize, statsd conf = config[DOMAIN] host = conf.get(CONF_HOST) From 69ae469148c178b3c80578d702e20acc1c7953f3 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:23:56 +0100 Subject: [PATCH 2100/3953] Move imports to top for deluge (#29438) --- homeassistant/components/deluge/sensor.py | 13 ++++++------- homeassistant/components/deluge/switch.py | 9 ++++----- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 098484cf7ae6e2..7df87490c60930 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -1,21 +1,22 @@ """Support for monitoring the Deluge BitTorrent client API.""" import logging +from deluge_client import DelugeRPCClient, FailedToReconnectException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, + CONF_MONITORED_VARIABLES, CONF_NAME, + CONF_PASSWORD, CONF_PORT, - CONF_MONITORED_VARIABLES, + CONF_USERNAME, STATE_IDLE, ) -from homeassistant.helpers.entity import Entity from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _THROTTLED_REFRESH = None @@ -46,7 +47,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Deluge sensors.""" - from deluge_client import DelugeRPCClient name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -103,7 +103,6 @@ def unit_of_measurement(self): def update(self): """Get the latest data from Deluge and updates the state.""" - from deluge_client import FailedToReconnectException try: self.data = self.client.call( diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index 981ef129b473b3..7ac98f284c8a99 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -1,21 +1,22 @@ """Support for setting the Deluge BitTorrent client in Pause.""" import logging +from deluge_client import DelugeRPCClient, FailedToReconnectException import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA -from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PORT, CONF_PASSWORD, + CONF_PORT, CONF_USERNAME, STATE_OFF, STATE_ON, ) -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Deluge switch.""" - from deluge_client import DelugeRPCClient name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -95,7 +95,6 @@ def turn_off(self, **kwargs): def update(self): """Get the latest data from deluge and updates the state.""" - from deluge_client import FailedToReconnectException try: torrent_list = self.deluge_client.call( From f874a9df13c53bed4c8552a0e7f58468dfe74367 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:24:08 +0100 Subject: [PATCH 2101/3953] Move imports to top for clementine (#29437) --- homeassistant/components/clementine/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index 37ed97915c78b8..9e05b831359fdd 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -3,9 +3,10 @@ import logging import time +from clementineremote import ClementineRemote import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -56,7 +57,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Clementine platform.""" - from clementineremote import ClementineRemote host = config.get(CONF_HOST) port = config.get(CONF_PORT) From d70ad369de63d4ecba685426a5884bcad37a8ecb Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:24:20 +0100 Subject: [PATCH 2102/3953] Move imports to top for asuswrt (#29436) --- homeassistant/components/asuswrt/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index e0c6830adfe93c..64d2d7c7a4bf15 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -1,15 +1,16 @@ """Support for ASUSWRT devices.""" import logging +from aioasuswrt.asuswrt import AsusWrt import voluptuous as vol from homeassistant.const import ( CONF_HOST, + CONF_MODE, CONF_PASSWORD, - CONF_USERNAME, CONF_PORT, - CONF_MODE, CONF_PROTOCOL, + CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -53,7 +54,6 @@ async def async_setup(hass, config): """Set up the asuswrt component.""" - from aioasuswrt.asuswrt import AsusWrt conf = config[DOMAIN] From 52926f19618ce8049bb184c30a839a5c04f479a1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 06:30:22 +0100 Subject: [PATCH 2103/3953] Move imports to top for canary (#29449) --- homeassistant/components/canary/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 6bb01c9d114804..67654c99f3eb83 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,4 +1,5 @@ """Support for Canary sensors.""" +from canary.api import SensorType from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity @@ -103,8 +104,6 @@ def update(self): """Get the latest state of the sensor.""" self._data.update() - from canary.api import SensorType - canary_sensor_type = None if self._sensor_type[0] == "air_quality": canary_sensor_type = SensorType.AIR_QUALITY From 41440d4f1bbba61260b64e48f275e5fc96d6719e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Dec 2019 21:47:35 -0800 Subject: [PATCH 2104/3953] Fix litejet tests --- tests/components/automation/test_litejet.py | 2 +- tests/components/litejet/test_light.py | 2 +- tests/components/litejet/test_scene.py | 2 +- tests/components/litejet/test_switch.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/automation/test_litejet.py b/tests/components/automation/test_litejet.py index 2e6f578ef4c7e6..4c916d8ed96501 100644 --- a/tests/components/automation/test_litejet.py +++ b/tests/components/automation/test_litejet.py @@ -33,7 +33,7 @@ def get_switch_name(number): @pytest.fixture def mock_lj(hass): """Initialize components.""" - with mock.patch("pylitejet.LiteJet") as mock_pylitejet: + with mock.patch("homeassistant.components.litejet.LiteJet") as mock_pylitejet: mock_lj = mock_pylitejet.return_value mock_lj.switch_pressed_callbacks = {} diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py index 1fc6f1df94e41b..e4ca1c2106ea5f 100644 --- a/tests/components/litejet/test_light.py +++ b/tests/components/litejet/test_light.py @@ -21,7 +21,7 @@ class TestLiteJetLight(unittest.TestCase): """Test the litejet component.""" - @mock.patch("pylitejet.LiteJet") + @mock.patch("homeassistant.components.litejet.LiteJet") def setup_method(self, method, mock_pylitejet): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() diff --git a/tests/components/litejet/test_scene.py b/tests/components/litejet/test_scene.py index d9ab561b6e145e..0f42ac40cdf8da 100644 --- a/tests/components/litejet/test_scene.py +++ b/tests/components/litejet/test_scene.py @@ -20,7 +20,7 @@ class TestLiteJetScene(unittest.TestCase): """Test the litejet component.""" - @mock.patch("pylitejet.LiteJet") + @mock.patch("homeassistant.components.litejet.LiteJet") def setup_method(self, method, mock_pylitejet): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() diff --git a/tests/components/litejet/test_switch.py b/tests/components/litejet/test_switch.py index b7f980872548db..a9cf54dc1f68b2 100644 --- a/tests/components/litejet/test_switch.py +++ b/tests/components/litejet/test_switch.py @@ -21,7 +21,7 @@ class TestLiteJetSwitch(unittest.TestCase): """Test the litejet component.""" - @mock.patch("pylitejet.LiteJet") + @mock.patch("homeassistant.components.litejet.LiteJet") def setup_method(self, method, mock_pylitejet): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() From 28e9f1d0b8f1d9a2362cd146141e6000a55445f1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Dec 2019 21:57:08 -0800 Subject: [PATCH 2105/3953] Fix deconz import --- homeassistant/components/deconz/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 75e33ea6cafb87..b757f1f4d03045 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -170,6 +169,9 @@ async def _update_entry(self, entry, host): async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" + # Import it here, because only now do we know ssdp integration loaded. + # pylint: disable=import-outside-toplevel + from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL: return self.async_abort(reason="not_deconz_bridge") From dad11f8208e83a57bf03fec20e3fe385c8f6f1d0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Dec 2019 22:47:40 -0800 Subject: [PATCH 2106/3953] Fix tests & lint --- homeassistant/components/mhz19/sensor.py | 4 +-- .../components/asuswrt/test_device_tracker.py | 15 +++++---- tests/components/daikin/test_config_flow.py | 13 ++++---- tests/components/datadog/test_init.py | 26 +++++++-------- tests/components/directv/test_media_player.py | 23 +++++++++---- .../components/emulated_roku/test_binding.py | 4 ++- tests/components/emulated_roku/test_init.py | 8 ++--- tests/components/light/test_init.py | 4 +++ .../logi_circle/test_config_flow.py | 33 ++++++++----------- tests/components/mfi/test_sensor.py | 17 +++++----- tests/components/mfi/test_switch.py | 13 ++++++-- tests/components/point/test_config_flow.py | 16 ++++----- .../rainmachine/test_config_flow.py | 17 +++++++--- tests/components/random/test_binary_sensor.py | 6 ++-- 14 files changed, 112 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/mhz19/sensor.py b/homeassistant/components/mhz19/sensor.py index 36f3a4d426de81..aedd5ea9b09221 100644 --- a/homeassistant/components/mhz19/sensor.py +++ b/homeassistant/components/mhz19/sensor.py @@ -116,9 +116,9 @@ def device_state_attributes(self): class MHZClient: """Get the latest data from the MH-Z sensor.""" - def __init__(self, co2sensor, serial): + def __init__(self, co2sens, serial): """Initialize the sensor.""" - self.co2sensor = co2sensor + self.co2sensor = co2sens self._serial = serial self.data = dict() diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index a3fde3a6855561..de999362f51e3d 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -1,4 +1,5 @@ """The tests for the ASUSWRT device tracker platform.""" +from unittest.mock import patch from homeassistant.setup import async_setup_component from homeassistant.components.asuswrt import ( @@ -10,7 +11,7 @@ ) from homeassistant.const import CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST -from tests.common import MockDependency, mock_coro_func +from tests.common import mock_coro_func FAKEFILE = None @@ -28,9 +29,9 @@ async def test_password_or_pub_key_required(hass): """Test creating an AsusWRT scanner without a pass or pubkey.""" - with MockDependency("aioasuswrt.asuswrt") as mocked_asus: - mocked_asus.AsusWrt().connection.async_connect = mock_coro_func() - mocked_asus.AsusWrt().is_connected = False + with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: + AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().is_connected = False result = await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} ) @@ -39,9 +40,9 @@ async def test_password_or_pub_key_required(hass): async def test_get_scanner_with_password_no_pubkey(hass): """Test creating an AsusWRT scanner with a password and no pubkey.""" - with MockDependency("aioasuswrt.asuswrt") as mocked_asus: - mocked_asus.AsusWrt().connection.async_connect = mock_coro_func() - mocked_asus.AsusWrt().connection.async_get_connected_devices = mock_coro_func( + with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: + AsusWrt().connection.async_connect = mock_coro_func() + AsusWrt().connection.async_get_connected_devices = mock_coro_func( return_value={} ) result = await async_setup_component( diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 674d918ed39f42..aea78f17564af7 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,6 +1,7 @@ # pylint: disable=redefined-outer-name """Tests for the Daikin config flow.""" import asyncio +from unittest.mock import patch import pytest @@ -9,7 +10,7 @@ from homeassistant.components.daikin.const import KEY_IP, KEY_MAC from homeassistant.const import CONF_HOST -from tests.common import MockConfigEntry, MockDependency +from tests.common import MockConfigEntry MAC = "AABBCCDDEEFF" HOST = "127.0.0.1" @@ -30,10 +31,10 @@ async def mock_daikin_init(): """Mock the init function in pydaikin.""" pass - with MockDependency("pydaikin.appliance") as mock_daikin_: - mock_daikin_.Appliance().values.get.return_value = "AABBCCDDEEFF" - mock_daikin_.Appliance().init = mock_daikin_init - yield mock_daikin_ + with patch("homeassistant.components.daikin.config_flow.Appliance") as Appliance: + Appliance().values.get.return_value = "AABBCCDDEEFF" + Appliance().init = mock_daikin_init + yield Appliance async def test_user(hass, mock_daikin): @@ -94,7 +95,7 @@ async def test_discovery(hass, mock_daikin): async def test_device_abort(hass, mock_daikin, s_effect, reason): """Test device abort.""" flow = init_config_flow(hass) - mock_daikin.Appliance.side_effect = s_effect + mock_daikin.side_effect = s_effect result = await flow.async_step_user({CONF_HOST: HOST}) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index 56d70b18d91260..30541c8137a89d 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -12,7 +12,7 @@ import homeassistant.components.datadog as datadog import homeassistant.core as ha -from tests.common import assert_setup_component, get_test_home_assistant, MockDependency +from tests.common import assert_setup_component, get_test_home_assistant class TestDatadog(unittest.TestCase): @@ -33,11 +33,11 @@ def test_invalid_config(self): self.hass, datadog.DOMAIN, {datadog.DOMAIN: {"host1": "host1"}} ) - @MockDependency("datadog", "beer") - def test_datadog_setup_full(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_datadog_setup_full(self, mock_connection, mock_client): """Test setup with all data.""" self.hass.bus.listen = mock.MagicMock() - mock_connection = mock_datadog.initialize assert setup_component( self.hass, @@ -54,11 +54,11 @@ def test_datadog_setup_full(self, mock_datadog): assert EVENT_LOGBOOK_ENTRY == self.hass.bus.listen.call_args_list[0][0][0] assert EVENT_STATE_CHANGED == self.hass.bus.listen.call_args_list[1][0][0] - @MockDependency("datadog") - def test_datadog_setup_defaults(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_datadog_setup_defaults(self, mock_connection, mock_client): """Test setup with defaults.""" self.hass.bus.listen = mock.MagicMock() - mock_connection = mock_datadog.initialize assert setup_component( self.hass, @@ -78,11 +78,11 @@ def test_datadog_setup_defaults(self, mock_datadog): ) assert self.hass.bus.listen.called - @MockDependency("datadog") - def test_logbook_entry(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_logbook_entry(self, mock_connection, mock_client): """Test event listener.""" self.hass.bus.listen = mock.MagicMock() - mock_client = mock_datadog.statsd assert setup_component( self.hass, @@ -110,11 +110,11 @@ def test_logbook_entry(self, mock_datadog): mock_client.event.reset_mock() - @MockDependency("datadog") - def test_state_changed(self, mock_datadog): + @mock.patch("homeassistant.components.datadog.statsd") + @mock.patch("homeassistant.components.datadog.initialize") + def test_state_changed(self, mock_connection, mock_client): """Test event listener.""" self.hass.bus.listen = mock.MagicMock() - mock_client = mock_datadog.statsd assert setup_component( self.hass, diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 85916cf6159f2e..7f802e1a94b7f7 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -58,7 +58,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import MockDependency, async_fire_time_changed +from tests.common import async_fire_time_changed CLIENT_ENTITY_ID = "media_player.client_dvr" MAIN_ENTITY_ID = "media_player.main_dvr" @@ -179,8 +179,9 @@ def platforms(hass, dtv_side_effect, mock_now): ] } - with MockDependency("DirectPy"), patch( - "DirectPy.DIRECTV", side_effect=dtv_side_effect + with patch( + "homeassistant.components.directv.media_player.DIRECTV", + side_effect=dtv_side_effect, ), patch("homeassistant.util.dt.utcnow", return_value=mock_now): hass.loop.run_until_complete(async_setup_component(hass, DOMAIN, config)) hass.loop.run_until_complete(hass.async_block_till_done()) @@ -309,7 +310,9 @@ def tune_channel(self, source): async def test_setup_platform_config(hass): """Test setting up the platform from configuration.""" - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() @@ -321,7 +324,9 @@ async def test_setup_platform_config(hass): async def test_setup_platform_discover(hass): """Test setting up the platform from discovery.""" - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): hass.async_create_task( async_load_platform( @@ -337,7 +342,9 @@ async def test_setup_platform_discover(hass): async def test_setup_platform_discover_duplicate(hass): """Test setting up the platform from discovery.""" - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() @@ -358,7 +365,9 @@ async def test_setup_platform_discover_client(hass): LOCATIONS.append({"locationName": "Client 1", "clientAddr": "1"}) LOCATIONS.append({"locationName": "Client 2", "clientAddr": "2"}) - with MockDependency("DirectPy"), patch("DirectPy.DIRECTV", new=MockDirectvClass): + with patch( + "homeassistant.components.directv.media_player.DIRECTV", new=MockDirectvClass + ): await async_setup_component(hass, DOMAIN, WORKING_CONFIG) await hass.async_block_till_done() diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 19b014a5782392..712c35f5a104ef 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -44,7 +44,9 @@ def instantiate( def listener(event): events.append(event) - with patch("emulated_roku.EmulatedRokuServer", instantiate): + with patch( + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", instantiate + ): hass.bus.async_listen(EVENT_ROKU_COMMAND, listener) assert await binding.setup() is True diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index f83bd2330c4b80..92524f24d97a08 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -10,7 +10,7 @@ async def test_config_required_fields(hass): """Test that configuration is successful with required fields.""" with patch.object(emulated_roku, "configured_servers", return_value=[]), patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ): assert ( @@ -35,7 +35,7 @@ async def test_config_required_fields(hass): async def test_config_already_registered_not_configured(hass): """Test that an already registered name causes the entry to be ignored.""" with patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ) as instantiate, patch.object( emulated_roku, "configured_servers", return_value=["Emulated Roku Test"] @@ -74,7 +74,7 @@ async def test_setup_entry_successful(hass): } with patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ) as instantiate: assert await emulated_roku.async_setup_entry(hass, entry) is True @@ -98,7 +98,7 @@ async def test_unload_entry(hass): entry.data = {"name": "Emulated Roku Test", "listen_port": 8060} with patch( - "emulated_roku.EmulatedRokuServer", + "homeassistant.components.emulated_roku.binding.EmulatedRokuServer", return_value=Mock(start=mock_coro_func(), close=mock_coro_func()), ): assert await emulated_roku.async_setup_entry(hass, entry) is True diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 3539e109a66770..2cf13369bd9857 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -432,6 +432,8 @@ def _mock_open(path, *args, **kwargs): async def test_light_context(hass, hass_admin_user): """Test that light context works.""" + platform = getattr(hass.components, "test.light") + platform.init() assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) state = hass.states.get("light.ceiling") @@ -453,6 +455,8 @@ async def test_light_context(hass, hass_admin_user): async def test_light_turn_on_auth(hass, hass_admin_user): """Test that light context works.""" + platform = getattr(hass.components, "test.light") + platform.init() assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) state = hass.states.get("light.ceiling") diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 6a291d5e7abf4a..0e2ef29da94e3d 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -9,14 +9,11 @@ from homeassistant.components.logi_circle.config_flow import ( DOMAIN, LogiCircleAuthCallbackView, + AuthorizationFailed, ) from homeassistant.setup import async_setup_component -from tests.common import MockDependency, mock_coro - - -class AuthorizationFailed(Exception): - """Dummy Exception.""" +from tests.common import mock_coro class MockRequest: @@ -50,19 +47,15 @@ def init_config_flow(hass): @pytest.fixture def mock_logi_circle(): """Mock logi_circle.""" - with MockDependency("logi_circle", "exception") as mock_logi_circle_: - mock_logi_circle_.exception.AuthorizationFailed = AuthorizationFailed - mock_logi_circle_.LogiCircle().authorize = Mock( - return_value=mock_coro(return_value=True) - ) - mock_logi_circle_.LogiCircle().close = Mock( - return_value=mock_coro(return_value=True) - ) - mock_logi_circle_.LogiCircle().account = mock_coro( - return_value={"accountId": "testId"} - ) - mock_logi_circle_.LogiCircle().authorize_url = "http://authorize.url" - yield mock_logi_circle_ + with patch( + "homeassistant.components.logi_circle.config_flow.LogiCircle" + ) as logi_circle: + LogiCircle = logi_circle() + LogiCircle.authorize = Mock(return_value=mock_coro(return_value=True)) + LogiCircle.close = Mock(return_value=mock_coro(return_value=True)) + LogiCircle.account = mock_coro(return_value={"accountId": "testId"}) + LogiCircle.authorize_url = "http://authorize.url" + yield LogiCircle async def test_step_import( @@ -159,7 +152,7 @@ async def test_abort_if_authorize_fails( ): # pylint: disable=redefined-outer-name """Test we abort if authorizing fails.""" flow = init_config_flow(hass) - mock_logi_circle.LogiCircle().authorize.side_effect = side_effect + mock_logi_circle.authorize.side_effect = side_effect result = await flow.async_step_code("123ABC") assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -219,4 +212,4 @@ async def test_callback_view_accepts_code( assert resp.status == 200 await hass.async_block_till_done() - mock_logi_circle.LogiCircle.return_value.authorize.assert_called_with("456") + mock_logi_circle.authorize.assert_called_with("456") diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index da472308fc2f6f..6849578bbb9ac0 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -3,6 +3,7 @@ import unittest.mock as mock import requests +from mficlient.client import FailedToLogin from homeassistant.setup import setup_component import homeassistant.components.sensor as sensor @@ -38,28 +39,26 @@ def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_missing_config(self, mock_client): """Test setup with missing configuration.""" config = {"sensor": {"platform": "mfi"}} assert setup_component(self.hass, "sensor", config) assert not mock_client.called - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_failed_login(self, mock_client): """Test setup with login failure.""" - from mficlient.client import FailedToLogin - mock_client.side_effect = FailedToLogin assert not self.PLATFORM.setup_platform(self.hass, dict(self.GOOD_CONFIG), None) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_failed_connect(self, mock_client): """Test setup with connection failure.""" mock_client.side_effect = requests.exceptions.ConnectionError assert not self.PLATFORM.setup_platform(self.hass, dict(self.GOOD_CONFIG), None) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_minimum(self, mock_client): """Test setup with minimum configuration.""" config = dict(self.GOOD_CONFIG) @@ -70,7 +69,7 @@ def test_setup_minimum(self, mock_client): "foo", "user", "pass", port=6443, use_tls=True, verify=True ) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_with_port(self, mock_client): """Test setup with port.""" config = dict(self.GOOD_CONFIG) @@ -81,7 +80,7 @@ def test_setup_with_port(self, mock_client): "foo", "user", "pass", port=6123, use_tls=True, verify=True ) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") def test_setup_with_tls_disabled(self, mock_client): """Test setup without TLS.""" config = dict(self.GOOD_CONFIG) @@ -94,7 +93,7 @@ def test_setup_with_tls_disabled(self, mock_client): "foo", "user", "pass", port=6080, use_tls=False, verify=False ) - @mock.patch("mficlient.client.MFiClient") + @mock.patch("homeassistant.components.mfi.sensor.MFiClient") @mock.patch("homeassistant.components.mfi.sensor.MfiSensor") def test_setup_adds_proper_devices(self, mock_sensor, mock_client): """Test if setup adds devices.""" diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index 11a6c402ad69df..ebddc8c5bc25a3 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -5,12 +5,11 @@ from homeassistant.setup import setup_component import homeassistant.components.switch as switch import homeassistant.components.mfi.switch as mfi -from tests.components.mfi import test_sensor as test_mfi_sensor from tests.common import get_test_home_assistant -class TestMfiSwitchSetup(test_mfi_sensor.TestMfiSensorSetup): +class TestMfiSwitchSetup(unittest.TestCase): """Test the mFi switch.""" PLATFORM = mfi @@ -28,7 +27,15 @@ class TestMfiSwitchSetup(test_mfi_sensor.TestMfiSensorSetup): } } - @mock.patch("mficlient.client.MFiClient") + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + @mock.patch("homeassistant.components.mfi.switch.MFiClient") @mock.patch("homeassistant.components.mfi.switch.MfiSwitch") def test_setup_adds_proper_devices(self, mock_switch, mock_client): """Test if setup adds devices.""" diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index b7b4cdfb47f71b..c1c705e752de32 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant import data_entry_flow from homeassistant.components.point import DOMAIN, config_flow -from tests.common import MockDependency, mock_coro +from tests.common import mock_coro def init_config_flow(hass, side_effect=None): @@ -30,15 +30,15 @@ def is_authorized(): @pytest.fixture def mock_pypoint(is_authorized): # pylint: disable=redefined-outer-name """Mock pypoint.""" - with MockDependency("pypoint") as mock_pypoint_: - mock_pypoint_.PointSession().get_access_token.return_value = { + with patch( + "homeassistant.components.point.config_flow.PointSession" + ) as PointSession: + PointSession.return_value.get_access_token.return_value = { "access_token": "boo" } - mock_pypoint_.PointSession().is_authorized = is_authorized - mock_pypoint_.PointSession().user.return_value = { - "email": "john.doe@example.com" - } - yield mock_pypoint_ + PointSession.return_value.is_authorized = is_authorized + PointSession.return_value.user.return_value = {"email": "john.doe@example.com"} + yield PointSession async def test_abort_if_no_implementation_registered(hass): diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index f888f9d4642bc0..cfde51408220d8 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -1,6 +1,8 @@ """Define tests for the OpenUV config flow.""" from unittest.mock import patch +from regenmaschine.errors import RainMachineError + from homeassistant import data_entry_flow from homeassistant.components.rainmachine import DOMAIN, config_flow from homeassistant.const import ( @@ -33,8 +35,6 @@ async def test_duplicate_error(hass): async def test_invalid_password(hass): """Test that an invalid password throws an error.""" - from regenmaschine.errors import RainMachineError - conf = { CONF_IP_ADDRESS: "192.168.1.100", CONF_PASSWORD: "bad_password", @@ -46,7 +46,8 @@ async def test_invalid_password(hass): flow.hass = hass with patch( - "regenmaschine.login", return_value=mock_coro(exception=RainMachineError) + "homeassistant.components.rainmachine.config_flow.login", + return_value=mock_coro(exception=RainMachineError), ): result = await flow.async_step_user(user_input=conf) assert result["errors"] == {CONF_PASSWORD: "invalid_credentials"} @@ -75,7 +76,10 @@ async def test_step_import(hass): flow = config_flow.RainMachineFlowHandler() flow.hass = hass - with patch("regenmaschine.login", return_value=mock_coro(True)): + with patch( + "homeassistant.components.rainmachine.config_flow.login", + return_value=mock_coro(True), + ): result = await flow.async_step_import(import_config=conf) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -101,7 +105,10 @@ async def test_step_user(hass): flow = config_flow.RainMachineFlowHandler() flow.hass = hass - with patch("regenmaschine.login", return_value=mock_coro(True)): + with patch( + "homeassistant.components.rainmachine.config_flow.login", + return_value=mock_coro(True), + ): result = await flow.async_step_user(user_input=conf) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/random/test_binary_sensor.py b/tests/components/random/test_binary_sensor.py index 5bc332e6da3cc8..a11b571dd83b6e 100644 --- a/tests/components/random/test_binary_sensor.py +++ b/tests/components/random/test_binary_sensor.py @@ -18,7 +18,7 @@ def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() - @patch("random.getrandbits", return_value=1) + @patch("homeassistant.components.random.binary_sensor.getrandbits", return_value=1) def test_random_binary_sensor_on(self, mocked): """Test the Random binary sensor.""" config = {"binary_sensor": {"platform": "random", "name": "test"}} @@ -29,7 +29,9 @@ def test_random_binary_sensor_on(self, mocked): assert state.state == "on" - @patch("random.getrandbits", return_value=False) + @patch( + "homeassistant.components.random.binary_sensor.getrandbits", return_value=False + ) def test_random_binary_sensor_off(self, mocked): """Test the Random binary sensor.""" config = {"binary_sensor": {"platform": "random", "name": "test"}} From b2d5de6a798a28537410f0ced6e9c55aab558c7b Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Wed, 4 Dec 2019 22:49:26 -0800 Subject: [PATCH 2107/3953] Switch iperf3 to generate a new client every time it runs a test (#29495) * Switch iperf3 to generate a new client every time it runs a test * Add myself to CODEOWNERS * Fix imperative mood --- CODEOWNERS | 1 + homeassistant/components/iperf3/__init__.py | 38 ++++++++++--------- homeassistant/components/iperf3/manifest.json | 4 +- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 66332531bd8323..8078aadf6419ac 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -162,6 +162,7 @@ homeassistant/components/input_text/* @home-assistant/core homeassistant/components/integration/* @dgomes homeassistant/components/intent/* @home-assistant/core homeassistant/components/ios/* @robbiet480 +homeassistant/components/iperf3/* @rohankapoorcom homeassistant/components/ipma/* @dgomes homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 753ea60efa4fe3..9272a725bb7f03 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -85,17 +85,7 @@ async def async_setup(hass, config): conf = config[DOMAIN] for host in conf[CONF_HOSTS]: - host_name = host[CONF_HOST] - - client = iperf3.Client() - client.duration = host[CONF_DURATION] - client.server_hostname = host_name - client.port = host[CONF_PORT] - client.num_streams = host[CONF_PARALLEL] - client.protocol = host[CONF_PROTOCOL] - client.verbose = False - - data = hass.data[DOMAIN][host_name] = Iperf3Data(hass, client) + data = hass.data[DOMAIN][host[CONF_HOST]] = Iperf3Data(hass, host) if not conf[CONF_MANUAL]: async_track_time_interval(hass, data.update, conf[CONF_SCAN_INTERVAL]) @@ -123,26 +113,37 @@ def update(call): class Iperf3Data: """Get the latest data from iperf3.""" - def __init__(self, hass, client): + def __init__(self, hass, host): """Initialize the data object.""" self._hass = hass - self._client = client + self._host = host self.data = {ATTR_DOWNLOAD: None, ATTR_UPLOAD: None, ATTR_VERSION: None} + def create_client(self): + """Create a new iperf3 client to use for measurement.""" + client = iperf3.Client() + client.duration = self._host[CONF_DURATION] + client.server_hostname = self._host[CONF_HOST] + client.port = self._host[CONF_PORT] + client.num_streams = self._host[CONF_PARALLEL] + client.protocol = self._host[CONF_PROTOCOL] + client.verbose = False + return client + @property def protocol(self): """Return the protocol used for this connection.""" - return self._client.protocol + return self._host[CONF_PROTOCOL] @property def host(self): """Return the host connected to.""" - return self._client.server_hostname + return self._host[CONF_HOST] @property def port(self): """Return the port on the host connected to.""" - return self._client.port + return self._host[CONF_PORT] def update(self, now=None): """Get the latest data from iperf3.""" @@ -165,9 +166,10 @@ def update(self, now=None): def _run_test(self, test_type): """Run and return the iperf3 data.""" - self._client.reverse = test_type == ATTR_DOWNLOAD + client = self.create_client() + client.reverse = test_type == ATTR_DOWNLOAD try: - result = self._client.run() + result = client.run() except (AttributeError, OSError, ValueError) as error: _LOGGER.error("Iperf3 error: %s", error) return None diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index c3b1e27c77acc9..6b7cadfd5ded8c 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -6,5 +6,7 @@ "iperf3==0.1.11" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@rohankapoorcom" + ] } From c6ac2a5705d1c8f912df2ac1d407723d562d74af Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 5 Dec 2019 07:50:18 +0100 Subject: [PATCH 2108/3953] Update pyhomematic (#29477) --- homeassistant/components/homematic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 5db547e3f0a438..8a86fd19c7d335 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -3,7 +3,7 @@ "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", "requirements": [ - "pyhomematic==0.1.61" + "pyhomematic==0.1.62" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c3b266c8110b06..c6dad0573ca838 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1271,7 +1271,7 @@ pyhik==0.2.5 pyhiveapi==0.2.19.3 # homeassistant.components.homematic -pyhomematic==0.1.61 +pyhomematic==0.1.62 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4fffcce129061e..484dba217a8fa6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -427,7 +427,7 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.homematic -pyhomematic==0.1.61 +pyhomematic==0.1.62 # homeassistant.components.ipma pyipma==1.2.1 From 1646aab36d4498183779dbb7397dc2407a9cfaf3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Dec 2019 22:51:53 -0800 Subject: [PATCH 2109/3953] Version bump to 0.103.0dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c15d1e55bd1800..fab722ea5e6b3a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 102 +MINOR_VERSION = 103 PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) From f10076a4ad599c317eb214c81c764d49ffdd313b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Dec 2019 22:52:20 -0800 Subject: [PATCH 2110/3953] Bumped version to 0.103.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fab722ea5e6b3a..07a4284c491853 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 103 -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, 1) From 957e5018f4445cd83de01e3da07717f7afa23909 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 Dec 2019 22:52:58 -0800 Subject: [PATCH 2111/3953] Version bump to 0.104.0dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fab722ea5e6b3a..dc937f81482c6f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 103 +MINOR_VERSION = 104 PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) From e99184bf68cf4d475624d4b77eccca74ae5fa267 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Dec 2019 00:28:56 -0800 Subject: [PATCH 2112/3953] Install requirements of after_dependencies when loading integrations (#29491) * Install requirements of after_dependencies when loading integrations * Fix smartthings test --- homeassistant/components/auth/manifest.json | 9 +++----- homeassistant/components/camera/manifest.json | 7 +----- homeassistant/components/cast/manifest.json | 1 + .../components/google_assistant/manifest.json | 1 + .../components/mobile_app/http_api.py | 13 +++++------ .../components/mobile_app/manifest.json | 1 + .../components/mobile_app/webhook.py | 9 ++++---- .../components/mobile_app/websocket_api.py | 2 +- .../components/onboarding/manifest.json | 9 ++------ .../components/owntracks/__init__.py | 2 +- .../components/owntracks/config_flow.py | 5 +---- .../components/owntracks/manifest.json | 12 +++------- homeassistant/components/proxy/camera.py | 22 +++++++++++-------- homeassistant/components/proxy/manifest.json | 4 +--- .../components/smartthings/manifest.json | 14 ++++-------- .../components/smartthings/smartapp.py | 18 +++------------ homeassistant/setup.py | 4 ++-- script/hassfest/dependencies.py | 13 +++++------ .../smartthings/test_config_flow.py | 9 ++++---- 19 files changed, 57 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index 1b0ab33f38155b..2f3e724b5831cd 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -3,10 +3,7 @@ "name": "Auth", "documentation": "https://www.home-assistant.io/integrations/auth", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["http"], + "after_dependencies": ["onboarding"], + "codeowners": ["@home-assistant/core"] } diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index a3395965e4f749..1bd4bb7caeb065 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,11 +3,6 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "requirements": [], - "dependencies": [ - "http" - ], - "after_dependencies": [ - "stream" - ], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index b6776a17f7cba0..8ad6f8fdb8dcc0 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==4.0.1"], "dependencies": [], + "after_dependencies": ["cloud"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": [] } diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index f97977a74001bf..94dd3b7f079047 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/google_assistant", "requirements": [], "dependencies": ["http"], + "after_dependencies": ["camera"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 11ca39e8b60014..5be2d19789e420 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -37,9 +37,7 @@ async def post(self, request: Request, data: Dict) -> Response: webhook_id = generate_secret() - cloud_loaded = "cloud" in hass.config.components - - if cloud_loaded and hass.components.cloud.async_active_subscription(): + if hass.components.cloud.async_active_subscription(): data[ CONF_CLOUDHOOK_URL ] = await hass.components.cloud.async_create_cloudhook(webhook_id) @@ -59,11 +57,10 @@ async def post(self, request: Request, data: Dict) -> Response: ) remote_ui_url = None - if cloud_loaded: - try: - remote_ui_url = hass.components.cloud.async_remote_ui_url() - except hass.components.cloud.CloudNotAvailable: - pass + try: + remote_ui_url = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass return self.json( { diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 29ee35e002c0ad..230a60fdf256ea 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": ["PyNaCl==1.3.0"], "dependencies": ["http", "webhook"], + "after_dependencies": ["cloud"], "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 98687e6658fdc7..46f17b401bcdbf 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -309,10 +309,9 @@ async def handle_webhook( if CONF_CLOUDHOOK_URL in registration: resp[CONF_CLOUDHOOK_URL] = registration[CONF_CLOUDHOOK_URL] - if "cloud" in hass.config.components: - try: - resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() - except hass.components.cloud.CloudNotAvailable: - pass + try: + resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass return webhook_response(resp, registration=registration, headers=headers) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index bc5305c36fa6bc..a18e5247bfa50f 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -115,7 +115,7 @@ async def websocket_delete_registration( except HomeAssistantError: return error_message(msg["id"], "internal_error", "Error deleting registration") - if CONF_CLOUDHOOK_URL in registration and "cloud" in hass.config.components: + if CONF_CLOUDHOOK_URL in registration: await hass.components.cloud.async_delete_cloudhook(webhook_id) connection.send_message(result_message(msg["id"], "ok")) diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index 2febfc481e01f9..8e525ff0baad1b 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -3,11 +3,6 @@ "name": "Onboarding", "documentation": "https://www.home-assistant.io/integrations/onboarding", "requirements": [], - "dependencies": [ - "auth", - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["auth", "http", "person"], + "codeowners": ["@home-assistant/core"] } diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index d30e667f368e68..8556e8a7556057 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -118,7 +118,7 @@ async def async_unload_entry(hass, entry): async def async_remove_entry(hass, entry): """Remove an OwnTracks config entry.""" - if not entry.data.get("cloudhook") or "cloud" not in hass.config.components: + if not entry.data.get("cloudhook"): return await hass.components.cloud.async_delete_cloudhook(entry.data[CONF_WEBHOOK_ID]) diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index ff4a649e0ce76f..1a8bb838e181f7 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -66,10 +66,7 @@ async def async_step_import(self, user_input): async def _get_webhook_id(self): """Generate webhook ID.""" webhook_id = self.hass.components.webhook.async_generate_id() - if ( - "cloud" in self.hass.config.components - and self.hass.components.cloud.async_active_subscription() - ): + if self.hass.components.cloud.async_active_subscription(): webhook_url = await self.hass.components.cloud.async_create_cloudhook( webhook_id ) diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 529d7990a8644e..63fdfb94cf7779 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -3,14 +3,8 @@ "name": "Owntracks", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/owntracks", - "requirements": [ - "PyNaCl==1.3.0" - ], - "dependencies": [ - "webhook" - ], - "after_dependencies": [ - "mqtt" - ], + "requirements": ["PyNaCl==1.3.0"], + "dependencies": ["webhook"], + "after_dependencies": ["mqtt", "cloud"], "codeowners": [] } diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 90487120ffe0a7..893fadfe178a81 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -7,7 +7,13 @@ from PIL import Image import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA, + Camera, + async_get_image, + async_get_mjpeg_stream, + async_get_still_stream, +) from homeassistant.const import CONF_ENTITY_ID, CONF_MODE, CONF_NAME from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -227,7 +233,7 @@ async def async_camera_image(self): return self._last_image self._last_image_time = now - image = await self.hass.components.camera.async_get_image(self._proxied_camera) + image = await async_get_image(self.hass, self._proxied_camera) if not image: _LOGGER.error("Error getting original camera image") return self._last_image @@ -247,12 +253,12 @@ async def async_camera_image(self): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from camera images.""" if not self._stream_opts: - return await self.hass.components.camera.async_get_mjpeg_stream( - request, self._proxied_camera + return await async_get_mjpeg_stream( + self.hass, request, self._proxied_camera ) - return await self.hass.components.camera.async_get_still_stream( - request, self._async_stream_image, self.content_type, self.frame_interval + return await async_get_still_stream( + request, self._async_stream_image, self.content_type, self.frame_interval, ) @property @@ -263,9 +269,7 @@ def name(self): async def _async_stream_image(self): """Return a still image response from the camera.""" try: - image = await self.hass.components.camera.async_get_image( - self._proxied_camera - ) + image = await async_get_image(self.hass, self._proxied_camera) if not image: return None except HomeAssistantError: diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 39f7b9064fc69a..5344ea6fe835d7 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,9 +2,7 @@ "domain": "proxy", "name": "Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": [ - "pillow==6.2.1" - ], + "requirements": ["pillow==6.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 8b5bf65afa1349..0ab71382fad590 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -3,14 +3,8 @@ "name": "Smartthings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smartthings", - "requirements": [ - "pysmartapp==0.3.2", - "pysmartthings==0.6.9" - ], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@andrewsayre" - ] + "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.6.9"], + "dependencies": ["webhook"], + "after_dependencies": ["cloud"], + "codeowners": ["@andrewsayre"] } diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 6acd29397ae6a6..9b67df21491011 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -88,10 +88,7 @@ async def validate_installed_app(api, installed_app_id: str): def validate_webhook_requirements(hass: HomeAssistantType) -> bool: """Ensure HASS is setup properly to receive webhooks.""" - if ( - "cloud" in hass.config.components - and hass.components.cloud.async_active_subscription() - ): + if hass.components.cloud.async_active_subscription(): return True if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None: return True @@ -105,11 +102,7 @@ def get_webhook_url(hass: HomeAssistantType) -> str: Return the cloudhook if available, otherwise local webhook. """ cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if ( - "cloud" in hass.config.components - and hass.components.cloud.async_active_subscription() - and cloudhook_url is not None - ): + if hass.components.cloud.async_active_subscription() and cloudhook_url is not None: return cloudhook_url return webhook.async_generate_url(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) @@ -229,7 +222,6 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): cloudhook_url = config.get(CONF_CLOUDHOOK_URL) if ( cloudhook_url is None - and "cloud" in hass.config.components and hass.components.cloud.async_active_subscription() and not hass.config_entries.async_entries(DOMAIN) ): @@ -281,11 +273,7 @@ async def unload_smartapp_endpoint(hass: HomeAssistantType): return # Remove the cloudhook if it was created cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if ( - cloudhook_url - and "cloud" in hass.config.components - and hass.components.cloud.async_is_logged_in() - ): + if cloudhook_url and hass.components.cloud.async_is_logged_in(): await hass.components.cloud.async_delete_cloudhook( hass.data[DOMAIN][CONF_WEBHOOK_ID] ) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 314938feeed42c..42296a4935d657 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -288,8 +288,8 @@ async def async_process_deps_reqs( raise HomeAssistantError("Could not set up all dependencies.") if not hass.config.skip_pip and integration.requirements: - await requirements.async_process_requirements( - hass, integration.domain, integration.requirements + await requirements.async_get_integration_with_requirements( + hass, integration.domain ) processed.add(integration.domain) diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index e9933995715425..e47deb76ad5f7e 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -43,15 +43,12 @@ def validate_dependencies(integration: Integration): if referenced: for domain in sorted(referenced): - print( - "Warning: {} references integration {} but it's not a " - "dependency".format(integration.domain, domain) + integration.add_error( + "dependencies", + "Using component {} but it's not in 'dependencies' or 'after_dependencies'".format( + domain + ), ) - # Not enforced yet. - # integration.add_error( - # 'dependencies', - # "Using component {} but it's not a dependency".format(domain) - # ) def validate(integrations: Dict[str, Integration], config): diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 521f1c6a6a8972..82a24f38287139 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -7,7 +7,6 @@ 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 from homeassistant.components.smartthings.const import ( @@ -18,7 +17,7 @@ DOMAIN, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_step_user(hass): @@ -211,9 +210,11 @@ async def test_cloudhook_app_created_then_show_wait_form( await smartapp.unload_smartapp_endpoint(hass) with patch.object( - cloud, "async_active_subscription", return_value=True + hass.components.cloud, "async_active_subscription", return_value=True ), patch.object( - cloud, "async_create_cloudhook", return_value="http://cloud.test" + hass.components.cloud, + "async_create_cloudhook", + return_value=mock_coro("http://cloud.test"), ) as mock_create_cloudhook: await smartapp.setup_smartapp_endpoint(hass) From f6d1eb97a3390a75240c9665113fc786555f55ce Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 09:56:17 +0100 Subject: [PATCH 2113/3953] Move imports to top for decora_wifi (#29439) --- homeassistant/components/decora_wifi/light.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 6171f65ef242ee..7d8aa104bb0317 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -2,17 +2,22 @@ import logging +# pylint: disable=import-error +from decora_wifi import DecoraWiFiSession +from decora_wifi.models.person import Person +from decora_wifi.models.residence import Residence +from decora_wifi.models.residential_account import ResidentialAccount import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, - Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, + Light, ) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -28,11 +33,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Decora WiFi platform.""" - # pylint: disable=import-error - from decora_wifi import DecoraWiFiSession - from decora_wifi.models.person import Person - from decora_wifi.models.residential_account import ResidentialAccount - from decora_wifi.models.residence import Residence email = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) From a050d5484754030b13e34be2fd636bcd4af91804 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Dec 2019 01:15:28 -0800 Subject: [PATCH 2114/3953] Make hassfest stricter (#29494) * Make hassfest stricter * Update manifest.json --- .../components/ambiclimate/manifest.json | 10 +-- homeassistant/components/apns/manifest.json | 5 +- homeassistant/components/camera/manifest.json | 1 + homeassistant/components/cloud/manifest.json | 1 + .../components/doorbird/manifest.json | 10 +-- .../components/hikvisioncam/switch.py | 5 +- homeassistant/components/html5/manifest.json | 10 +-- .../components/logbook/manifest.json | 5 +- .../components/logi_circle/manifest.json | 2 +- homeassistant/components/melissa/climate.py | 19 +++--- .../components/nsw_fuel_station/sensor.py | 2 +- homeassistant/components/person/manifest.json | 1 + homeassistant/components/plant/manifest.json | 10 +-- homeassistant/components/point/manifest.json | 12 +--- homeassistant/components/rachio/manifest.json | 6 +- .../components/statistics/manifest.json | 5 +- .../components/telegram_bot/__init__.py | 5 +- homeassistant/components/tts/manifest.json | 13 ++-- homeassistant/components/wink/manifest.json | 7 +-- .../components/workday/binary_sensor.py | 3 +- .../components/worxlandroid/sensor.py | 2 +- script/hassfest/dependencies.py | 62 ++++++++++++++++++- 22 files changed, 112 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index 3d175165abd1fc..151b761dff8656 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -3,11 +3,7 @@ "name": "Ambiclimate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambiclimate", - "requirements": [ - "ambiclimate==0.2.1" - ], - "dependencies": [], - "codeowners": [ - "@danielhiversen" - ] + "requirements": ["ambiclimate==0.2.1"], + "dependencies": ["http"], + "codeowners": ["@danielhiversen"] } diff --git a/homeassistant/components/apns/manifest.json b/homeassistant/components/apns/manifest.json index 4845c45a963dce..3c38238f7ebf61 100644 --- a/homeassistant/components/apns/manifest.json +++ b/homeassistant/components/apns/manifest.json @@ -2,9 +2,8 @@ "domain": "apns", "name": "Apns", "documentation": "https://www.home-assistant.io/integrations/apns", - "requirements": [ - "apns2==0.3.0" - ], + "requirements": ["apns2==0.3.0"], "dependencies": [], + "after_dependencies": ["device_tracker"], "codeowners": [] } diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index 1bd4bb7caeb065..32cd7c3fe47138 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/camera", "requirements": [], "dependencies": ["http"], + "after_dependencies": ["stream", "media_player"], "codeowners": [] } diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index ec9a556af0ac5c..accc4a0c0f9971 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/cloud", "requirements": ["hass-nabucasa==0.30"], "dependencies": ["http", "webhook"], + "after_dependencies": ["alexa", "google_assistant"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index c9cdb32e18af31..61225b86a445c7 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -2,11 +2,7 @@ "domain": "doorbird", "name": "Doorbird", "documentation": "https://www.home-assistant.io/integrations/doorbird", - "requirements": [ - "doorbirdpy==2.0.8" - ], - "dependencies": [], - "codeowners": [ - "@oblogic7" - ] + "requirements": ["doorbirdpy==2.0.8"], + "dependencies": ["http"], + "codeowners": ["@oblogic7"] } diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index 020b894c0f76bd..f86853a5468ae3 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -5,7 +5,7 @@ from hikvision.error import HikvisionError, MissingParamError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -16,7 +16,6 @@ STATE_ON, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import ToggleEntity # This is the last working version, please test before updating @@ -60,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([HikvisionMotionSwitch(name, hikvision_cam)]) -class HikvisionMotionSwitch(ToggleEntity): +class HikvisionMotionSwitch(SwitchDevice): """Representation of a switch to toggle on/off motion detection.""" def __init__(self, name, hikvision_cam): diff --git a/homeassistant/components/html5/manifest.json b/homeassistant/components/html5/manifest.json index 667a57891821a7..dd794ae03866b7 100644 --- a/homeassistant/components/html5/manifest.json +++ b/homeassistant/components/html5/manifest.json @@ -2,11 +2,7 @@ "domain": "html5", "name": "HTML5 Notifications", "documentation": "https://www.home-assistant.io/integrations/html5", - "requirements": [ - "pywebpush==1.9.2" - ], - "dependencies": ["frontend"], - "codeowners": [ - "@robbiet480" - ] + "requirements": ["pywebpush==1.9.2"], + "dependencies": ["http"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index e8e3ad8ac2e580..08083ea4024579 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -3,9 +3,6 @@ "name": "Logbook", "documentation": "https://www.home-assistant.io/integrations/logbook", "requirements": [], - "dependencies": [ - "frontend", - "recorder" - ], + "dependencies": ["frontend", "http", "recorder"], "codeowners": [] } diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index 22502956e06aa5..bd6dc8a8d27bc2 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/logi_circle", "requirements": ["logi_circle==0.2.2"], - "dependencies": ["ffmpeg"], + "dependencies": ["ffmpeg", "http"], "codeowners": ["@evanjd"] } diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 38f4977c96abc5..c09203c3e333e6 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -11,8 +11,11 @@ HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, + FAN_AUTO, + FAN_HIGH, + FAN_MEDIUM, + FAN_LOW, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS from . import DATA_MELISSA @@ -29,7 +32,7 @@ HVAC_MODE_OFF, ] -FAN_MODES = [HVAC_MODE_AUTO, SPEED_HIGH, SPEED_MEDIUM, SPEED_LOW] +FAN_MODES = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW] async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -200,11 +203,11 @@ def melissa_fan_to_hass(self, fan): if fan == self._api.FAN_AUTO: return HVAC_MODE_AUTO if fan == self._api.FAN_LOW: - return SPEED_LOW + return FAN_LOW if fan == self._api.FAN_MEDIUM: - return SPEED_MEDIUM + return FAN_MEDIUM if fan == self._api.FAN_HIGH: - return SPEED_HIGH + return FAN_HIGH _LOGGER.warning("Fan mode %s could not be mapped to hass", fan) return None @@ -224,10 +227,10 @@ def hass_fan_to_melissa(self, fan): """Translate hass fan modes to melissa modes.""" if fan == HVAC_MODE_AUTO: return self._api.FAN_AUTO - if fan == SPEED_LOW: + if fan == FAN_LOW: return self._api.FAN_LOW - if fan == SPEED_MEDIUM: + if fan == FAN_MEDIUM: return self._api.FAN_MEDIUM - if fan == SPEED_HIGH: + if fan == FAN_HIGH: return self._api.FAN_HIGH _LOGGER.warning("Melissa have no setting for %s fan mode", fan) diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index a84aa554be910c..3c900b46be039e 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -6,7 +6,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.light import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index cf50b8029c2e91..afcd428d6afd76 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/person", "requirements": [], "dependencies": [], + "after_dependencies": ["device_tracker"], "codeowners": [] } diff --git a/homeassistant/components/plant/manifest.json b/homeassistant/components/plant/manifest.json index 721a57e782248a..c1e009ccec3a4b 100644 --- a/homeassistant/components/plant/manifest.json +++ b/homeassistant/components/plant/manifest.json @@ -3,11 +3,7 @@ "name": "Plant", "documentation": "https://www.home-assistant.io/integrations/plant", "requirements": [], - "dependencies": [ - "group", - "zone" - ], - "codeowners": [ - "@ChristianKuehnel" - ] + "dependencies": ["group", "zone"], + "after_dependencies": ["recorder"], + "codeowners": ["@ChristianKuehnel"] } diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 4c29f37e67cf72..1c74052ee7e278 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -3,13 +3,7 @@ "name": "Point", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/point", - "requirements": [ - "pypoint==1.1.2" - ], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@fredrike" - ] + "requirements": ["pypoint==1.1.2"], + "dependencies": ["webhook", "http"], + "codeowners": ["@fredrike"] } diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index 79e3677d65e821..fae640f926268b 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -2,9 +2,7 @@ "domain": "rachio", "name": "Rachio", "documentation": "https://www.home-assistant.io/integrations/rachio", - "requirements": [ - "rachiopy==0.1.3" - ], - "dependencies": [], + "requirements": ["rachiopy==0.1.3"], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/statistics/manifest.json b/homeassistant/components/statistics/manifest.json index 3dab05942b947b..17ade1283ceb7e 100644 --- a/homeassistant/components/statistics/manifest.json +++ b/homeassistant/components/statistics/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/statistics", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "after_dependencies": ["recorder"], + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index d365060e204705..12bde6c72d8496 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -19,7 +19,6 @@ from telegram.utils.request import Request import voluptuous as vol -from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE from homeassistant.const import ( ATTR_COMMAND, ATTR_LATITUDE, @@ -35,6 +34,10 @@ _LOGGER = logging.getLogger(__name__) +ATTR_DATA = "data" +ATTR_MESSAGE = "message" +ATTR_TITLE = "title" + ATTR_ARGS = "args" ATTR_AUTHENTICATION = "authentication" ATTR_CALLBACK_QUERY = "callback_query" diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index cb780523977724..b57d5c36112d38 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -2,13 +2,8 @@ "domain": "tts", "name": "Tts", "documentation": "https://www.home-assistant.io/integrations/tts", - "requirements": [ - "mutagen==1.43.0" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@robbiet480" - ] + "requirements": ["mutagen==1.43.0"], + "dependencies": ["http"], + "after_dependencies": ["media_player"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index acf9c38e6322c1..a1bae6482927d2 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -2,10 +2,7 @@ "domain": "wink", "name": "Wink", "documentation": "https://www.home-assistant.io/integrations/wink", - "requirements": [ - "pubnubsub-handler==1.0.8", - "python-wink==1.10.5" - ], - "dependencies": ["configurator"], + "requirements": ["pubnubsub-handler==1.0.8", "python-wink==1.10.5"], + "dependencies": ["configurator", "http"], "codeowners": [] } diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index f95447c1e72f70..efa8b6ad77b487 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -5,9 +5,8 @@ import holidays import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, WEEKDAYS -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 4e9bf0a6a4a913..ad583d6d943dbc 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -10,7 +10,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PIN, CONF_TIMEOUT from homeassistant.helpers.aiohttp_client import async_get_clientsession diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index e47deb76ad5f7e..42a31f206105c2 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -16,7 +16,19 @@ def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> Set[ continue for match in pattern.finditer(fil.read_text()): - found.add(match.groups()[0]) + integration = match.groups()[1] + + if ( + # If it's importing something from itself + integration == path.name + # Platform file + or (path / f"{integration}.py").exists() + # Dir for platform + or (path / integration).exists() + ): + continue + + found.add(match.groups()[1]) return found @@ -30,19 +42,65 @@ def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> Set[ "hassio", "system_health", "websocket_api", + "automation", + "device_automation", + "zone", + "homeassistant", + "system_log", + "person", + # Discovery + "ssdp", + "discovery", + # Other + "mjpeg", # base class, has no reqs or component to load. } +IGNORE_VIOLATIONS = [ + # Has same requirement, gets defaults. + ("sql", "recorder"), + # Sharing a base class + ("openalpr_cloud", "openalpr_local"), + ("lutron_caseta", "lutron"), + ("ffmpeg_noise", "ffmpeg_motion"), + # Demo + ("demo", "manual"), + ("demo", "openalpr_local"), + # This should become a helper method that integrations can submit data to + ("websocket_api", "lovelace"), + # Expose HA to external systems + "homekit", + "alexa", + "google_assistant", + "emulated_hue", + "prometheus", + "conversation", + "logbook", + # These should be extracted to external package + "pvoutput", + "dwd_weather_warnings", + # Should be rewritten to use own data fetcher + "scrape", +] + def validate_dependencies(integration: Integration): """Validate all dependencies.""" # Find usage of hass.components - referenced = grep_dir(integration.path, "**/*.py", r"hass\.components\.(\w+)") + referenced = grep_dir( + integration.path, "**/*.py", r"(hass|homeassistant)\.components\.(\w+)" + ) referenced -= ALLOWED_USED_COMPONENTS referenced -= set(integration.manifest["dependencies"]) referenced -= set(integration.manifest.get("after_dependencies", [])) if referenced: for domain in sorted(referenced): + if ( + integration.domain in IGNORE_VIOLATIONS + or (integration.domain, domain) in IGNORE_VIOLATIONS + ): + continue + integration.add_error( "dependencies", "Using component {} but it's not in 'dependencies' or 'after_dependencies'".format( From 564fed787986f43fafae707be62ca8890010637a Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 12:50:53 +0100 Subject: [PATCH 2115/3953] Move imports to top for ambient_station (#29497) --- homeassistant/components/ambient_station/__init__.py | 2 +- homeassistant/components/ambient_station/config_flow.py | 4 ++-- tests/components/ambient_station/test_config_flow.py | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 7a805d6b86763d..58389dd183189e 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -8,8 +8,8 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( - ATTR_NAME, ATTR_LOCATION, + ATTR_NAME, CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, ) diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py index 256e55ba4020ab..c20b43598ca4f8 100644 --- a/homeassistant/components/ambient_station/config_flow.py +++ b/homeassistant/components/ambient_station/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the Ambient PWS component.""" +from aioambient import Client +from aioambient.errors import AmbientError import voluptuous as vol from homeassistant import config_entries @@ -40,8 +42,6 @@ async def async_step_import(self, import_config): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from aioambient import Client - from aioambient.errors import AmbientError if not user_input: return await self._show_form() diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index 701a6dacb98e34..c94a51be94e719 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -3,12 +3,13 @@ import aioambient import pytest +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.ambient_station import CONF_APP_KEY, DOMAIN, config_flow from homeassistant.const import CONF_API_KEY -from tests.common import load_fixture, MockConfigEntry, MockDependency, mock_coro +from tests.common import load_fixture, MockConfigEntry, mock_coro @pytest.fixture @@ -20,9 +21,9 @@ def get_devices_response(): @pytest.fixture def mock_aioambient(get_devices_response): """Mock the aioambient library.""" - with MockDependency("aioambient") as mock_aioambient_: - mock_aioambient_.Client().api.get_devices.return_value = get_devices_response - yield mock_aioambient_ + with patch("homeassistant.components.ambient_station.config_flow.Client") as Client: + Client().api.get_devices.return_value = get_devices_response + yield Client async def test_duplicate_error(hass): From 086d1f432d89957df836c0a19233482aeba739d7 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 12:59:59 +0100 Subject: [PATCH 2116/3953] Move imports to top for google_pubsub (#29498) --- homeassistant/components/google_pubsub/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index c4136c3b9cbaa1..bc7811a7a8f0cf 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -5,6 +5,7 @@ import os from typing import Any, Dict +from google.cloud import pubsub_v1 import voluptuous as vol from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN @@ -38,7 +39,6 @@ def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Pub/Sub component.""" - from google.cloud import pubsub_v1 config = yaml_config[DOMAIN] project_id = config[CONF_PROJECT_ID] From 009e4df6efc9d743cec5493fb28f20b8d4096362 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:19:50 +0100 Subject: [PATCH 2117/3953] Move imports to top for hue (#29501) --- homeassistant/components/hue/__init__.py | 4 ++-- homeassistant/components/hue/config_flow.py | 1 + homeassistant/components/hue/helpers.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 027ec205195279..f2b9bd1a229f45 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -9,9 +9,9 @@ from homeassistant.helpers import config_validation as cv, device_registry as dr from .bridge import HueBridge -from .config_flow import ( +from .config_flow import ( # Loading the config flow file will register the flow configured_hosts, -) # Loading the config flow file will register the flow +) from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 375042c88351fb..84b435d02eda28 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -134,6 +134,7 @@ async def async_step_ssdp(self, discovery_info): This flow is triggered by the SSDP component. It will check if the host is already configured and delegate to the import step if not. """ + # pylint: disable=import-outside-toplevel from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL: diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index af0f996b537daa..8a5fa973e4f2da 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -1,7 +1,7 @@ """Helper functions for Philips Hue.""" +from homeassistant import config_entries from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg -from homeassistant import config_entries from .const import DOMAIN From e9917c6a81fbf8601717185884a42208d993b2f3 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:22:01 +0100 Subject: [PATCH 2118/3953] Move imports to top for google_translate (#29499) --- homeassistant/components/google_translate/tts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index 3add45b8cb8e68..e35a229ab98eb3 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -6,6 +6,7 @@ import aiohttp from aiohttp.hdrs import REFERER, USER_AGENT import async_timeout +from gtts_token import gtts_token import voluptuous as vol import yarl @@ -115,7 +116,6 @@ def supported_languages(self): async def async_get_tts_audio(self, message, language, options=None): """Load TTS from google.""" - from gtts_token import gtts_token token = gtts_token.Token() websession = async_get_clientsession(self.hass) From 04722fdd63a80f55e87e4e06b9c8857acd10b02d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:42:09 +0100 Subject: [PATCH 2119/3953] Move imports to top for http (#29500) --- homeassistant/components/http/__init__.py | 1 - homeassistant/components/http/auth.py | 1 - homeassistant/components/http/ban.py | 1 - homeassistant/components/http/cors.py | 4 ++-- homeassistant/components/http/data_validator.py | 1 - homeassistant/components/http/real_ip.py | 1 - homeassistant/components/http/static.py | 3 +-- 7 files changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4d3985a7af3bb8..c720d134c9fcbf 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -26,7 +26,6 @@ from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa: F401 - # mypy: allow-untyped-defs, no-check-untyped-defs DOMAIN = "http" diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 97bd9b7d4bcee7..3866e770de0373 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -11,7 +11,6 @@ from .const import KEY_AUTHENTICATED, KEY_HASS_USER, KEY_REAL_IP - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 7d1e24f369800f..553d3657160367 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -17,7 +17,6 @@ from .const import KEY_REAL_IP - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index de4547f4782715..2d99a049e4bbdf 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,11 +1,10 @@ """Provide CORS support for the HTTP component.""" +from aiohttp.hdrs import ACCEPT, AUTHORIZATION, CONTENT_TYPE, ORIGIN from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource -from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback - # mypy: allow-untyped-defs, no-check-untyped-defs ALLOWED_CORS_HEADERS = [ @@ -23,6 +22,7 @@ def setup_cors(app, origins): """Set up CORS.""" # This import should remain here. That way the HTTP integration can always # be imported by other integrations without it's requirements being installed. + # pylint: disable=import-outside-toplevel import aiohttp_cors cors = aiohttp_cors.setup( diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 5945a4ca402fb8..017644a4d3695d 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -4,7 +4,6 @@ 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 f327c86a4c1681..f2334ce0a2ff81 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -8,7 +8,6 @@ from .const import KEY_REAL_IP - # mypy: allow-untyped-defs diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 7d84be0f6dd247..a5fe686a651bd0 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -3,10 +3,9 @@ from aiohttp import hdrs from aiohttp.web import FileResponse -from aiohttp.web_exceptions import HTTPNotFound, HTTPForbidden +from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound from aiohttp.web_urldispatcher import StaticResource - # mypy: allow-untyped-defs CACHE_TIME = 31 * 86400 # = 1 month From ed464a75b205dde77668c5cef5d5026a24c9d049 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:42:56 +0100 Subject: [PATCH 2120/3953] Move imports to top for system_log (#29465) --- homeassistant/components/system_log/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 68561d45f8ff81..44ff9c49a0190e 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -8,8 +8,8 @@ from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.http import HomeAssistantView -import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv CONF_MAX_ENTRIES = "max_entries" CONF_FIRE_EVENT = "fire_event" @@ -57,6 +57,7 @@ def _figure_out_source(record, call_stack, hass): paths = [HOMEASSISTANT_PATH[0], hass.config.config_dir] try: # If netdisco is installed check its path too. + # pylint: disable=import-outside-toplevel from netdisco import __path__ as netdisco_path paths.append(netdisco_path[0]) From ed5cdb528c3c2a810843ec2fbc5a983b46676094 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:44:11 +0100 Subject: [PATCH 2121/3953] Move imports to top for group (#29485) * Move imports to top for group * Fix failing test for group --- homeassistant/components/group/__init__.py | 21 +++++++------- homeassistant/components/group/cover.py | 24 ++++++++-------- homeassistant/components/group/light.py | 28 +++++++++---------- homeassistant/components/group/notify.py | 6 ++-- .../components/group/reproduce_state.py | 5 ++-- .../components/group/test_reproduce_state.py | 4 ++- 6 files changed, 42 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index ba12e22b53e663..16094ed48320ca 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -7,34 +7,33 @@ from homeassistant import core as ha from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, + ATTR_ICON, + ATTR_NAME, CONF_ICON, CONF_NAME, + SERVICE_RELOAD, STATE_CLOSED, STATE_HOME, + STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, + STATE_OK, STATE_ON, STATE_OPEN, - STATE_LOCKED, - STATE_UNLOCKED, - STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, - ATTR_ASSUMED_STATE, - SERVICE_RELOAD, - ATTR_NAME, - ATTR_ICON, + STATE_UNLOCKED, ) from homeassistant.core import callback -from homeassistant.loader import bind_hass +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.typing import HomeAssistantType - +from homeassistant.loader import bind_hass # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index f7a9643e5c8cb3..d9efdfa53c692c 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -4,18 +4,6 @@ import voluptuous as vol -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - CONF_ENTITIES, - CONF_NAME, - STATE_CLOSED, -) -from homeassistant.core import callback, State -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change - from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, @@ -41,7 +29,17 @@ SUPPORT_STOP_TILT, CoverDevice, ) - +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_NAME, + STATE_CLOSED, +) +from homeassistant.core import State, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_state_change # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 2cd6502813164e..f0c816964694b3 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -8,20 +8,6 @@ import voluptuous as vol from homeassistant.components import light -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - CONF_ENTITIES, - CONF_NAME, - STATE_ON, - STATE_UNAVAILABLE, -) -from homeassistant.core import CALLBACK_TYPE, State, callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.util import color as color_util - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -42,7 +28,19 @@ SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, ) - +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_NAME, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import CALLBACK_TYPE, State, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import color as color_util # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index e17990690faadb..2209e0e233326c 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -6,9 +6,6 @@ import voluptuous as vol -from homeassistant.const import ATTR_SERVICE -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, @@ -16,7 +13,8 @@ PLATFORM_SCHEMA, BaseNotificationService, ) - +from homeassistant.const import ATTR_SERVICE +import homeassistant.helpers.config_validation as cv # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py index 827e9bb1dcb572..787907019343b3 100644 --- a/homeassistant/components/group/reproduce_state.py +++ b/homeassistant/components/group/reproduce_state.py @@ -2,15 +2,16 @@ from typing import Iterable, Optional from homeassistant.core import Context, State +from homeassistant.helpers.state import async_reproduce_state from homeassistant.helpers.typing import HomeAssistantType +from . import get_entity_ids + async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None ) -> None: """Reproduce component states.""" - from . import get_entity_ids - from homeassistant.helpers.state import async_reproduce_state states_copy = [] for state in states: diff --git a/tests/components/group/test_reproduce_state.py b/tests/components/group/test_reproduce_state.py index 502ea9e51fc84f..f3a56a4647255b 100644 --- a/tests/components/group/test_reproduce_state.py +++ b/tests/components/group/test_reproduce_state.py @@ -21,7 +21,9 @@ def clone_state(state, entity_id): context=state.context, ) - with patch("homeassistant.helpers.state.async_reproduce_state") as fun: + with patch( + "homeassistant.components.group.reproduce_state.async_reproduce_state" + ) as fun: fun.return_value = Future() fun.return_value.set_result(None) From 204ca3f3a6e24ef11ece2e2ee490a8d77553c147 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 13:44:59 +0100 Subject: [PATCH 2122/3953] Move imports to top for frontend (#29487) --- homeassistant/components/frontend/__init__.py | 1 + homeassistant/components/frontend/storage.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7ef2bd386449d7..efb1c34653b3b7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -242,6 +242,7 @@ def _frontend_root(dev_repo_path): if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" # Keep import here so that we can import frontend without installing reqs + # pylint: disable=import-outside-toplevel import hass_frontend return hass_frontend.where() diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index 75b7b356ef9a12..2f68c5f8e017ca 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -1,10 +1,10 @@ """API for persistent storage for the frontend.""" from functools import wraps + import voluptuous as vol from homeassistant.components import websocket_api - # mypy: allow-untyped-calls, allow-untyped-defs DATA_STORAGE = "frontend_storage" From 2a92eb1962a54b9b0cf9e349fe00bd0b5028b6b7 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 16:56:01 +0100 Subject: [PATCH 2123/3953] Move imports to top for ipma (#29507) --- homeassistant/components/ipma/__init__.py | 1 + homeassistant/components/ipma/weather.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 702e12a8a63358..a00941624f550c 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,5 +1,6 @@ """Component for the Portuguese weather service - IPMA.""" from homeassistant.core import Config, HomeAssistant + from .config_flow import IpmaFlowHandler # noqa: F401 from .const import DOMAIN # noqa: F401 diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 9f1836c73896fd..c088d76d16573e 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -1,22 +1,23 @@ """Support for IPMA weather service.""" -import logging from datetime import timedelta +import logging import async_timeout +from pyipma import Station import voluptuous as vol from homeassistant.components.weather import ( - WeatherEntity, - PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, + PLATFORM_SCHEMA, + WeatherEntity, ) -from homeassistant.const import CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -84,7 +85,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_get_station(hass, latitude, longitude): """Retrieve weather station, station name to be used as the entity name.""" - from pyipma import Station websession = async_get_clientsession(hass) with async_timeout.timeout(10): From 76f455cea9b6df03b41c29679348e7bf44b06e76 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 16:56:53 +0100 Subject: [PATCH 2124/3953] Move imports to top for mediaroom (#29509) --- .../components/mediaroom/media_player.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 8e02ee56a7520e..539138783eead7 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -1,9 +1,10 @@ """Support for the Mediaroom Set-up-box.""" import logging +from pymediaroom import PyMediaroomError, Remote, State, install_mediaroom_protocol import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -99,7 +100,6 @@ def callback_notify(notify): async_add_entities([new_stb]) if not config[CONF_OPTIMISTIC]: - from pymediaroom import install_mediaroom_protocol already_installed = hass.data.get(DISCOVERY_MEDIAROOM, None) if not already_installed: @@ -123,7 +123,6 @@ class MediaroomDevice(MediaPlayerDevice): def set_state(self, mediaroom_state): """Map pymediaroom state to HA state.""" - from pymediaroom import State state_map = { State.OFF: STATE_OFF, @@ -139,7 +138,6 @@ def set_state(self, mediaroom_state): def __init__(self, host, device_id, optimistic=False, timeout=DEFAULT_TIMEOUT): """Initialize the device.""" - from pymediaroom import Remote self.host = host self.stb = Remote(host) @@ -184,7 +182,6 @@ async def async_notify_received(notify): async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" - from pymediaroom import PyMediaroomError _LOGGER.debug( "STB(%s) Play media: %s (%s)", self.stb.stb_ip, media_id, media_type @@ -237,7 +234,6 @@ def media_channel(self): async def async_turn_on(self): """Turn on the receiver.""" - from pymediaroom import PyMediaroomError try: self.set_state(await self.stb.turn_on()) @@ -250,7 +246,6 @@ async def async_turn_on(self): async def async_turn_off(self): """Turn off the receiver.""" - from pymediaroom import PyMediaroomError try: self.set_state(await self.stb.turn_off()) @@ -263,7 +258,6 @@ async def async_turn_off(self): async def async_media_play(self): """Send play command.""" - from pymediaroom import PyMediaroomError try: _LOGGER.debug("media_play()") @@ -277,7 +271,6 @@ async def async_media_play(self): async def async_media_pause(self): """Send pause command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("PlayPause") @@ -290,7 +283,6 @@ async def async_media_pause(self): async def async_media_stop(self): """Send stop command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("Stop") @@ -303,7 +295,6 @@ async def async_media_stop(self): async def async_media_previous_track(self): """Send Program Down command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("ProgDown") @@ -316,7 +307,6 @@ async def async_media_previous_track(self): async def async_media_next_track(self): """Send Program Up command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("ProgUp") @@ -329,7 +319,6 @@ async def async_media_next_track(self): async def async_volume_up(self): """Send volume up command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("VolUp") @@ -340,7 +329,6 @@ async def async_volume_up(self): async def async_volume_down(self): """Send volume up command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("VolDown") @@ -350,7 +338,6 @@ async def async_volume_down(self): async def async_mute_volume(self, mute): """Send mute command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("Mute") From 19893b8f3cf5162593f38c67b787f322ae899927 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 16:58:14 +0100 Subject: [PATCH 2125/3953] Move imports to top for islamic_prayer_times (#29506) * Move imports to top for islamic_prayer_times * Fix test_sensor.py for islamic_prayer_times * Format test_sensor.py with black * Fix tests for islamic prayer times sensor --- .../components/islamic_prayer_times/sensor.py | 8 ++--- .../islamic_prayer_times/test_sensor.py | 31 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 88cbd2cb4319e8..3f7de535407bf0 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -1,15 +1,16 @@ """Platform to retrieve Islamic prayer times information for Home Assistant.""" -import logging from datetime import datetime, timedelta +import logging +from prayer_times_calculator import PrayerTimesCalculator import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import DEVICE_CLASS_TIMESTAMP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_time +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -148,7 +149,6 @@ def __init__(self, latitude, longitude, calc_method): def get_new_prayer_times(self): """Fetch prayer times for today.""" - from prayer_times_calculator import PrayerTimesCalculator today = datetime.today().strftime("%Y-%m-%d") diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 734b82076c2964..ad229404a300fd 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -3,7 +3,6 @@ from unittest.mock import patch from homeassistant.setup import async_setup_component from homeassistant.components.islamic_prayer_times.sensor import IslamicPrayerTimesData -from tests.common import MockDependency import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed @@ -34,8 +33,10 @@ async def test_islamic_prayer_times_min_config(hass): """Test minimum Islamic prayer times configuration.""" min_config_sensors = ["fajr", "dhuhr", "asr", "maghrib", "isha"] - with MockDependency("prayer_times_calculator") as mock_pt_calc: - mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( + with patch( + "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" + ) as PrayerTimesCalculator: + PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( PRAYER_TIMES ) @@ -63,8 +64,10 @@ async def test_islamic_prayer_times_multiple_sensors(hass): "midnight", ] - with MockDependency("prayer_times_calculator") as mock_pt_calc: - mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( + with patch( + "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" + ) as PrayerTimesCalculator: + PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( PRAYER_TIMES ) @@ -87,8 +90,10 @@ async def test_islamic_prayer_times_with_calculation_method(hass): """Test Islamic prayer times configuration with calculation method.""" sensors = ["fajr", "maghrib"] - with MockDependency("prayer_times_calculator") as mock_pt_calc: - mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( + with patch( + "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" + ) as PrayerTimesCalculator: + PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( PRAYER_TIMES ) @@ -113,8 +118,10 @@ async def test_islamic_prayer_times_with_calculation_method(hass): async def test_islamic_prayer_times_data_get_prayer_times(hass): """Test Islamic prayer times data fetcher.""" - with MockDependency("prayer_times_calculator") as mock_pt_calc: - mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( + with patch( + "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" + ) as PrayerTimesCalculator: + PrayerTimesCalculator.return_value.fetch_prayer_times.return_value = ( PRAYER_TIMES ) @@ -138,8 +145,10 @@ async def test_islamic_prayer_times_sensor_update(hass): "Midnight": "00:45", } - with MockDependency("prayer_times_calculator") as mock_pt_calc: - mock_pt_calc.PrayerTimesCalculator.return_value.fetch_prayer_times.side_effect = [ + with patch( + "homeassistant.components.islamic_prayer_times.sensor.PrayerTimesCalculator" + ) as PrayerTimesCalculator: + PrayerTimesCalculator.return_value.fetch_prayer_times.side_effect = [ PRAYER_TIMES, new_prayer_times, ] From 1846b45cb58b04a4db95a3bd84289e875c9295ec Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 16:59:31 +0100 Subject: [PATCH 2126/3953] Move imports to top for mobile_app (#29511) --- homeassistant/components/mobile_app/helpers.py | 2 +- homeassistant/components/mobile_app/http_api.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index cad25f371dda6b..400ff31be89e30 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -111,7 +111,7 @@ def error_response( def supports_encryption() -> bool: """Test if we support encryption.""" try: - import nacl # noqa: F401 pylint: disable=unused-import + import nacl # noqa: F401 pylint: disable=unused-import, import-outside-toplevel return True except OSError: diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 5be2d19789e420..ede18528f81f75 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -6,7 +6,6 @@ from nacl.secret import SecretBox from homeassistant.auth.util import generate_secret - from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED From 5bdb20098e4a1c7e20d6ca6525f62cc22c2ff430 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 16:59:56 +0100 Subject: [PATCH 2127/3953] Move imports to top for meteoalarm (#29510) From 98d2eadb768b36ae4bccee6249c89af726d13865 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 18:49:28 +0100 Subject: [PATCH 2128/3953] Move imports to top for statistics (#29223) * Move imports to top for statistics * Added recorder to manifest.json * Deleted recorder from manifest.json, moved import back into method, added pylint disable comment * Moved recorder util imports away from the top * Move recorder imports to top, add recorder as after_dependency to manifest.json --- homeassistant/components/statistics/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 51868c6d0a85f1..6e042b1536f89e 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -1,25 +1,26 @@ """Support for statistics for sensor values.""" +from collections import deque import logging import statistics -from collections import deque import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.components.recorder.models import States +from homeassistant.components.recorder.util import execute, session_scope from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, + ATTR_UNIT_OF_MEASUREMENT, CONF_ENTITY_ID, + CONF_NAME, EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN, STATE_UNAVAILABLE, - ATTR_UNIT_OF_MEASUREMENT, + STATE_UNKNOWN, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change from homeassistant.util import dt as dt_util -from homeassistant.components.recorder.util import session_scope, execute _LOGGER = logging.getLogger(__name__) @@ -275,7 +276,6 @@ async def _async_initialize_from_database(self): If MaxAge is provided then query will restrict to entries younger then current datetime - MaxAge. """ - from homeassistant.components.recorder.models import States _LOGGER.debug("%s: initializing values from the database", self.entity_id) From 4e7b9eaed01b2d56a2b8b82641a2a184078c1a36 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 19:16:16 +0100 Subject: [PATCH 2129/3953] Move imports to top for monoprice (#29516) * Move imports to top for monoprice * Format test with black --- homeassistant/components/monoprice/media_player.py | 8 ++++---- tests/components/monoprice/test_media_player.py | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 1b1d9d2adf472c..20b2ecebcf4bfc 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -1,9 +1,11 @@ """Support for interfacing with Monoprice 6 zone home audio controller.""" import logging +from pymonoprice import get_monoprice +from serial import SerialException import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -20,6 +22,7 @@ STATE_ON, ) import homeassistant.helpers.config_validation as cv + from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT _LOGGER = logging.getLogger(__name__) @@ -68,9 +71,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Monoprice 6-zone amplifier platform.""" port = config.get(CONF_PORT) - from serial import SerialException - from pymonoprice import get_monoprice - try: monoprice = get_monoprice(port) except SerialException: diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index a33b85539083c3..cb064048d7b829 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -174,7 +174,10 @@ def setUp(self): self.hass = tests.common.get_test_home_assistant() self.hass.start() # Note, source dictionary is unsorted! - with mock.patch("pymonoprice.get_monoprice", new=lambda *a: self.monoprice): + with mock.patch( + "homeassistant.components.monoprice.media_player.get_monoprice", + new=lambda *a: self.monoprice, + ): setup_platform( self.hass, { From ec8ea022731b762481c2c88e1b14b4c9561c0b19 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Dec 2019 10:40:05 -0800 Subject: [PATCH 2130/3953] Fix recursion --- homeassistant/requirements.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index a0eec0f442b782..eb8aeeecfae896 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -3,7 +3,7 @@ from pathlib import Path import logging import os -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Set from homeassistant.exceptions import HomeAssistantError import homeassistant.util.package as pkg_util @@ -28,16 +28,19 @@ def __init__(self, domain: str, requirements: List) -> None: async def async_get_integration_with_requirements( - hass: HomeAssistant, domain: str + hass: HomeAssistant, domain: str, done: Set[str] = None ) -> 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. - - Does not handle circular dependencies. """ + if done is None: + done = {domain} + else: + done.add(domain) + integration = await async_get_integration(hass, domain) if hass.config.skip_pip: @@ -48,11 +51,18 @@ async def async_get_integration_with_requirements( hass, integration.domain, integration.requirements ) - deps = integration.dependencies + (integration.after_dependencies or []) + deps_to_check = [ + dep + for dep in integration.dependencies + (integration.after_dependencies or []) + if dep not in done + ] - if deps: + if deps_to_check: await asyncio.gather( - *[async_get_integration_with_requirements(hass, dep) for dep in deps] + *[ + async_get_integration_with_requirements(hass, dep, done) + for dep in deps_to_check + ] ) return integration From b4fda5faab72ed8bf9678067c3be17ec9b806cd1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 19:54:43 +0100 Subject: [PATCH 2131/3953] Move imports to top for mysensors (#29517) --- homeassistant/components/mysensors/__init__.py | 2 +- homeassistant/components/mysensors/climate.py | 2 +- homeassistant/components/mysensors/device.py | 2 +- homeassistant/components/mysensors/gateway.py | 2 +- homeassistant/components/mysensors/handler.py | 2 +- homeassistant/components/mysensors/light.py | 2 +- homeassistant/components/mysensors/sensor.py | 4 ++-- homeassistant/components/mysensors/switch.py | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index cbedd947843c38..a528be15e1475b 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -25,7 +25,7 @@ MYSENSORS_GATEWAYS, ) from .device import get_mysensors_devices -from .gateway import get_mysensors_gateway, setup_gateways, finish_setup +from .gateway import finish_setup, get_mysensors_gateway, setup_gateways _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index dc053e60de15e7..4939c0c83e566e 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -8,10 +8,10 @@ HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - HVAC_MODE_OFF, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 6d766530b04b0a..e5853fce5ca7f1 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -1,6 +1,6 @@ """Handle MySensors devices.""" -import logging from functools import partial +import logging from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON from homeassistant.core import callback diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 366692205a761a..903ec069b51414 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -6,6 +6,7 @@ import sys import async_timeout +from mysensors import mysensors import voluptuous as vol from homeassistant.const import CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP @@ -84,7 +85,6 @@ async def setup_gateways(hass, config): async def _get_gateway(hass, config, gateway_conf, persistence_file): """Return gateway after setup of the gateway.""" - from mysensors import mysensors conf = config[DOMAIN] persistence = conf[CONF_PERSISTENCE] diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index 0923b6bc8ded32..31e836f476737a 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -5,7 +5,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import decorator -from .const import MYSENSORS_GATEWAY_READY, CHILD_CALLBACK, NODE_CALLBACK +from .const import CHILD_CALLBACK, MYSENSORS_GATEWAY_READY, NODE_CALLBACK from .device import get_mysensors_devices from .helpers import discover_mysensors_platform, validate_set_msg diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 8f0d0906311c68..19eb8e9e92c2e6 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -11,8 +11,8 @@ Light, ) from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.util.color import rgb_hex_to_rgb_list import homeassistant.util.color as color_util +from homeassistant.util.color import rgb_hex_to_rgb_list SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index a7d1cad98fa9b9..ddad451d20fded 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -2,10 +2,10 @@ from homeassistant.components import mysensors from homeassistant.components.sensor import DOMAIN from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - POWER_WATT, - ENERGY_KILO_WATT_HOUR, ) SENSORS = { diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index fecec53370bcba..ec28649d70f60d 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -1,10 +1,10 @@ """Support for MySensors switches.""" import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components import mysensors from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +import homeassistant.helpers.config_validation as cv from .const import DOMAIN as MYSENSORS_DOMAIN, SERVICE_SEND_IR_CODE From 97cb8a37454e2bb4f6ee9af23778e4e3ae38ea02 Mon Sep 17 00:00:00 2001 From: tetienne Date: Thu, 5 Dec 2019 21:33:56 +0100 Subject: [PATCH 2132/3953] Increase somfy SCAN_INTERVAL (#29524) - There was too many errors 504 --- homeassistant/components/somfy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 184e32f1e6de69..1368725777bf5f 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=30) DOMAIN = "somfy" From 42688a6e4aa99bd66e8a3aee2a936b1c4eff3e28 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 21:55:48 +0100 Subject: [PATCH 2133/3953] Move imports to top for ign_sismologia (#29523) --- homeassistant/components/ign_sismologia/geo_location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index 8ad045c9f7a249..deecc389e7e1da 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -3,6 +3,7 @@ import logging from typing import Optional +from georss_ign_sismologia_client import IgnSismologiaFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -87,7 +88,6 @@ def __init__( minimum_magnitude, ): """Initialize the Feed Entity Manager.""" - from georss_ign_sismologia_client import IgnSismologiaFeedManager self._hass = hass self._feed_manager = IgnSismologiaFeedManager( From 20fdcbadff35891de6dc1965cf81b3954fe71bab Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 5 Dec 2019 21:56:42 +0100 Subject: [PATCH 2134/3953] Move imports to top for nextbus (#29520) * Move imports to top for nextbus * Fix test_sensor.py failed tests --- homeassistant/components/nextbus/sensor.py | 10 ++++----- tests/components/nextbus/test_sensor.py | 24 +++++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 7622bd133f0820..983c2272adcca1 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -1,13 +1,13 @@ """NextBus sensor.""" -import logging from itertools import chain +import logging +from py_nextbus import NextBusClient 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.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.const import CONF_NAME, DEVICE_CLASS_TIMESTAMP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util.dt import utc_from_timestamp @@ -94,8 +94,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): stop = config[CONF_STOP] name = config.get(CONF_NAME) - from py_nextbus import NextBusClient - client = NextBusClient(output_format="json") # Ensures that the tags provided are valid, also logs out valid values diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index d7c1919dff0664..4bac317102ac6b 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -2,11 +2,12 @@ from copy import deepcopy import pytest +from unittest.mock import patch import homeassistant.components.sensor as sensor import homeassistant.components.nextbus.sensor as nextbus -from tests.common import assert_setup_component, async_setup_component, MockDependency +from tests.common import assert_setup_component, async_setup_component VALID_AGENCY = "sf-muni" @@ -54,14 +55,16 @@ async def assert_setup_sensor(hass, config, count=1): @pytest.fixture def mock_nextbus(): """Create a mock py_nextbus module.""" - with MockDependency("py_nextbus") as py_nextbus: - yield py_nextbus + with patch( + "homeassistant.components.nextbus.sensor.NextBusClient" + ) as NextBusClient: + yield NextBusClient @pytest.fixture def mock_nextbus_predictions(mock_nextbus): """Create a mock of NextBusClient predictions.""" - instance = mock_nextbus.NextBusClient.return_value + instance = mock_nextbus.return_value instance.get_predictions_for_multi_stops.return_value = BASIC_RESULTS yield instance.get_predictions_for_multi_stops @@ -70,7 +73,7 @@ def mock_nextbus_predictions(mock_nextbus): @pytest.fixture def mock_nextbus_lists(mock_nextbus): """Mock all list functions in nextbus to test validate logic.""" - instance = mock_nextbus.NextBusClient.return_value + instance = mock_nextbus.return_value instance.get_agency_list.return_value = { "agency": [{"tag": "sf-muni", "title": "San Francisco Muni"}] } @@ -94,17 +97,18 @@ async def test_invalid_config(hass, mock_nextbus, mock_nextbus_lists): async def test_validate_tags(hass, mock_nextbus, mock_nextbus_lists): """Test that additional validation against the API is successful.""" - client = mock_nextbus.NextBusClient() # with self.subTest('Valid everything'): - assert nextbus.validate_tags(client, VALID_AGENCY, VALID_ROUTE, VALID_STOP) + assert nextbus.validate_tags(mock_nextbus(), VALID_AGENCY, VALID_ROUTE, VALID_STOP) # with self.subTest('Invalid agency'): - assert not nextbus.validate_tags(client, "not-valid", VALID_ROUTE, VALID_STOP) + assert not nextbus.validate_tags( + mock_nextbus(), "not-valid", VALID_ROUTE, VALID_STOP + ) # with self.subTest('Invalid route'): - assert not nextbus.validate_tags(client, VALID_AGENCY, "0", VALID_STOP) + assert not nextbus.validate_tags(mock_nextbus(), VALID_AGENCY, "0", VALID_STOP) # with self.subTest('Invalid stop'): - assert not nextbus.validate_tags(client, VALID_AGENCY, VALID_ROUTE, 0) + assert not nextbus.validate_tags(mock_nextbus(), VALID_AGENCY, VALID_ROUTE, 0) async def test_verify_valid_state( From 173966f45921365c6e2687780b29c1df55f5be75 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 00:20:07 +0100 Subject: [PATCH 2135/3953] Move imports to top for switcher_kis (#29530) * Move imports to top for switcher_kis * Disabled ungrouped imports if TYPE_CHECKING is true --- .../components/switcher_kis/__init__.py | 4 +- .../components/switcher_kis/switch.py | 25 +++++------ tests/components/switcher_kis/conftest.py | 44 +++++++++++++++---- 3 files changed, 48 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 9f4347d61d2d4d..e7e8d2d270c349 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -5,6 +5,8 @@ from logging import getLogger from typing import Dict, Optional +from aioswitcher.api import SwitcherV2Api +from aioswitcher.bridge import SwitcherV2Bridge import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_EDIT @@ -88,7 +90,6 @@ async def _validate_edit_permission( async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: """Set up the switcher component.""" - from aioswitcher.bridge import SwitcherV2Bridge phone_id = config[DOMAIN][CONF_PHONE_ID] device_id = config[DOMAIN][CONF_DEVICE_ID] @@ -122,7 +123,6 @@ async def async_switch_platform_discovered( async def async_set_auto_off_service(service: ServiceCallType) -> None: """Use for handling setting device auto-off service calls.""" - from aioswitcher.api import SwitcherV2Api await _validate_edit_permission( hass, service.context, service.data[CONF_ENTITY_ID] diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 454baca4eef849..c8eaddcb5bd7aa 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -1,7 +1,16 @@ """Home Assistant Switcher Component Switch platform.""" from logging import getLogger -from typing import Callable, Dict, TYPE_CHECKING +from typing import TYPE_CHECKING, Callable, Dict + +from aioswitcher.api import SwitcherV2Api +from aioswitcher.consts import ( + COMMAND_OFF, + COMMAND_ON, + STATE_OFF as SWITCHER_STATE_OFF, + STATE_ON as SWITCHER_STATE_ON, + WAITING_TEXT, +) from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -16,6 +25,7 @@ SIGNAL_SWITCHER_DEVICE_UPDATE, ) +# pylint: disable=ungrouped-imports if TYPE_CHECKING: from aioswitcher.devices import SwitcherV2Device from aioswitcher.api.messages import SwitcherV2ControlResponseMSG @@ -70,7 +80,6 @@ def unique_id(self) -> str: @property def is_on(self) -> bool: """Return True if entity is on.""" - from aioswitcher.consts import STATE_ON as SWITCHER_STATE_ON return self._state == SWITCHER_STATE_ON @@ -82,7 +91,6 @@ def current_power_w(self) -> int: @property def device_state_attributes(self) -> Dict: """Return the optional state attributes.""" - from aioswitcher.consts import WAITING_TEXT attribs = {} @@ -96,10 +104,6 @@ def device_state_attributes(self) -> Dict: @property def available(self) -> bool: """Return True if entity is available.""" - from aioswitcher.consts import ( - STATE_OFF as SWITCHER_STATE_OFF, - STATE_ON as SWITCHER_STATE_ON, - ) return self._state in [SWITCHER_STATE_ON, SWITCHER_STATE_OFF] @@ -135,13 +139,6 @@ async def async_turn_off(self, **kwargs: Dict) -> None: async def _control_device(self, send_on: bool) -> None: """Turn the entity on or off.""" - from aioswitcher.api import SwitcherV2Api - from aioswitcher.consts import ( - COMMAND_OFF, - COMMAND_ON, - STATE_OFF as SWITCHER_STATE_OFF, - STATE_ON as SWITCHER_STATE_ON, - ) response: "SwitcherV2ControlResponseMSG" = None async with SwitcherV2Api( diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index 888ffd46c3b136..2b0150cae67085 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -103,10 +103,22 @@ async def mock_queue(): mock_bridge = CoroutineMock() patchers = [ - patch("aioswitcher.bridge.SwitcherV2Bridge.start", new=mock_bridge), - patch("aioswitcher.bridge.SwitcherV2Bridge.stop", new=mock_bridge), - patch("aioswitcher.bridge.SwitcherV2Bridge.queue", get=mock_queue), - patch("aioswitcher.bridge.SwitcherV2Bridge.running", return_value=True), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.start", + new=mock_bridge, + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.stop", + new=mock_bridge, + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.queue", + get=mock_queue, + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.running", + return_value=True, + ), ] for patcher in patchers: @@ -127,9 +139,18 @@ async def mock_queue(): raise RuntimeError patchers = [ - patch("aioswitcher.bridge.SwitcherV2Bridge.start", return_value=None), - patch("aioswitcher.bridge.SwitcherV2Bridge.stop", return_value=None), - patch("aioswitcher.bridge.SwitcherV2Bridge.queue", get=mock_queue), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.start", + return_value=None, + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.stop", + return_value=None, + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Bridge.queue", + get=mock_queue, + ), ] for patcher in patchers: @@ -147,8 +168,13 @@ def mock_api_fixture() -> Generator[CoroutineMock, Any, None]: mock_api = CoroutineMock() patchers = [ - patch("aioswitcher.api.SwitcherV2Api.connect", new=mock_api), - patch("aioswitcher.api.SwitcherV2Api.disconnect", new=mock_api), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Api.connect", new=mock_api + ), + patch( + "homeassistant.components.switcher_kis.SwitcherV2Api.disconnect", + new=mock_api, + ), ] for patcher in patchers: From 9ba9b3339b32c205d78eeecc0b9a4cf6abfaba55 Mon Sep 17 00:00:00 2001 From: orrpan Date: Fri, 6 Dec 2019 00:23:54 +0100 Subject: [PATCH 2136/3953] Add full state view for emulated_hue (apps using emulated_hue, 'sleep cycle' and 'sleep as android') (#26650) * Add full state view for emulated_hue * clean and support updated sleep cycle * emulated hue add reuasable logic and cleanup code * emulated hue correct typos * Update hue_api.py * correct error message and update test_hue_api.py * cleanup test_hue_api.py --- .../components/emulated_hue/__init__.py | 4 ++ .../components/emulated_hue/hue_api.py | 69 +++++++++++++++++-- tests/components/emulated_hue/test_hue_api.py | 66 +++++++++++++++++- tests/components/emulated_hue/test_upnp.py | 25 +++++++ 4 files changed, 155 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 791085b46f3c86..41c23707a036f9 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -14,11 +14,13 @@ from .hue_api import ( HueUsernameView, + HueUnauthorizedUser, HueAllLightsStateView, HueOneLightStateView, HueOneLightChangeView, HueGroupView, HueAllGroupsStateView, + HueFullStateView, ) from .upnp import DescriptionXmlView, UPNPResponderThread @@ -113,11 +115,13 @@ async def async_setup(hass, yaml_config): DescriptionXmlView(config).register(app, app.router) HueUsernameView().register(app, app.router) + HueUnauthorizedUser().register(app, app.router) HueAllLightsStateView(config).register(app, app.router) HueOneLightStateView(config).register(app, app.router) HueOneLightChangeView(config).register(app, app.router) HueAllGroupsStateView(config).register(app, app.router) HueGroupView(config).register(app, app.router) + HueFullStateView(config).register(app, app.router) upnp_listener = UPNPResponderThread( config.host_ip_addr, diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index e7f15e7fc53524..d7db6bb2fe343e 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -89,6 +89,24 @@ HUE_API_STATE_CT_MIN = 153 # Color temp HUE_API_STATE_CT_MAX = 500 +HUE_API_USERNAME = "12345678901234567890" +UNAUTHORIZED_USER = [ + {"error": {"address": "/", "description": "unauthorized user", "type": "1"}} +] + + +class HueUnauthorizedUser(HomeAssistantView): + """Handle requests to find the emulated hue bridge.""" + + url = "/api" + name = "emulated_hue:api:unauthorized_user" + extra_urls = ["/api/"] + requires_auth = False + + async def get(self, request): + """Handle a GET request.""" + return self.json(UNAUTHORIZED_USER) + class HueUsernameView(HomeAssistantView): """Handle requests to create a username for the emulated hue bridge.""" @@ -111,7 +129,7 @@ async def post(self, request): if "devicetype" not in data: return self.json_message("devicetype not specified", HTTP_BAD_REQUEST) - return self.json([{"success": {"username": "12345678901234567890"}}]) + return self.json([{"success": {"username": HUE_API_USERNAME}}]) class HueAllGroupsStateView(HomeAssistantView): @@ -181,13 +199,37 @@ def get(self, request, username): if not is_local(request[KEY_REAL_IP]): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) - hass = request.app["hass"] - json_response = {} + return self.json(create_list_of_entities(self.config, request)) + + +class HueFullStateView(HomeAssistantView): + """Return full state view of emulated hue.""" + + url = "/api/{username}" + name = "emulated_hue:username:state" + requires_auth = False - for entity in hass.states.async_all(): - if self.config.is_entity_exposed(entity): - number = self.config.entity_id_to_number(entity.entity_id) - json_response[number] = entity_to_json(self.config, entity) + def __init__(self, config): + """Initialize the instance of the view.""" + self.config = config + + @core.callback + def get(self, request, username): + """Process a request to get the list of available lights.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED) + if username != HUE_API_USERNAME: + return self.json(UNAUTHORIZED_USER) + + json_response = { + "lights": create_list_of_entities(self.config, request), + "config": { + "mac": "00:00:00:00:00:00", + "swversion": "01003542", + "whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}}, + "ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}", + }, + } return self.json(json_response) @@ -673,3 +715,16 @@ def create_hue_success_response(entity_id, attr, value): """Create a success response for an attribute set on a light.""" success_key = f"/lights/{entity_id}/state/{attr}" return {"success": {success_key: value}} + + +def create_list_of_entities(config, request): + """Create a list of all entites.""" + hass = request.app["hass"] + json_response = {} + + for entity in hass.states.async_all(): + if config.is_entity_exposed(entity): + number = config.entity_id_to_number(entity.entity_id) + json_response[number] = entity_to_json(config, entity) + + return json_response diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 4f0d70d0046957..749493a6ca8177 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -25,11 +25,13 @@ HUE_API_STATE_BRI, HUE_API_STATE_HUE, HUE_API_STATE_SAT, + HUE_API_USERNAME, HueUsernameView, HueOneLightStateView, HueAllLightsStateView, HueOneLightChangeView, HueAllGroupsStateView, + HueFullStateView, ) from homeassistant.const import STATE_ON, STATE_OFF @@ -188,6 +190,7 @@ def hue_client(loop, hass_hue, aiohttp_client): HueOneLightStateView(config).register(web_app, web_app.router) HueOneLightChangeView(config).register(web_app, web_app.router) HueAllGroupsStateView(config).register(web_app, web_app.router) + HueFullStateView(config).register(web_app, web_app.router) return loop.run_until_complete(aiohttp_client(web_app)) @@ -252,6 +255,49 @@ def test_reachable_for_state(hass_hue, hue_client, state, is_reachable): assert state_json["state"]["reachable"] == is_reachable, state_json +@asyncio.coroutine +def test_discover_full_state(hue_client): + """Test the discovery of full state.""" + result = yield from hue_client.get("/api/" + HUE_API_USERNAME) + + assert result.status == 200 + assert "application/json" in result.headers["content-type"] + + result_json = yield from result.json() + + # Make sure array has correct content + assert "lights" in result_json + assert "lights" not in result_json["config"] + assert "config" in result_json + assert "config" not in result_json["lights"] + + lights_json = result_json["lights"] + config_json = result_json["config"] + + # Make sure array is correct size + assert len(result_json) == 2 + assert len(config_json) == 4 + assert len(lights_json) >= 1 + + # Make sure the config wrapper added to the config is there + assert "mac" in config_json + assert "00:00:00:00:00:00" in config_json["mac"] + + # Make sure the correct version in config + assert "swversion" in config_json + assert "01003542" in config_json["swversion"] + + # Make sure the correct username in config + assert "whitelist" in config_json + assert HUE_API_USERNAME in config_json["whitelist"] + assert "name" in config_json["whitelist"][HUE_API_USERNAME] + assert "HASS BRIDGE" in config_json["whitelist"][HUE_API_USERNAME]["name"] + + # Make sure the correct ip in config + assert "ipaddress" in config_json + assert "127.0.0.1:8300" in config_json["ipaddress"] + + @asyncio.coroutine def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" @@ -763,10 +809,26 @@ def perform_put_light_state( async def test_external_ip_blocked(hue_client): """Test external IP blocked.""" + getUrls = [ + "/api/username/groups", + "/api/username", + "/api/username/lights", + "/api/username/lights/light.ceiling_lights", + ] + postUrls = ["/api"] + putUrls = ["/api/username/lights/light.ceiling_lights/state"] with patch( "homeassistant.components.http.real_ip.ip_address", return_value=ip_address("45.45.45.45"), ): - result = await hue_client.get("/api/username/lights") + for getUrl in getUrls: + result = await hue_client.get(getUrl) + assert result.status == 401 + + for postUrl in postUrls: + result = await hue_client.post(postUrl) + assert result.status == 401 - assert result.status == 401 + for putUrl in putUrls: + result = await hue_client.put(putUrl) + assert result.status == 401 diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index ead78ad56ca7eb..4cbc79f68a507e 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -82,6 +82,31 @@ def test_create_username(self): assert "success" in success_json assert "username" in success_json["success"] + def test_unauthorized_view(self): + """Test unauthorized view.""" + request_json = {"devicetype": "my_device"} + + result = requests.get( + BRIDGE_URL_BASE.format("/api/unauthorized"), + data=json.dumps(request_json), + timeout=5, + ) + + assert result.status_code == 200 + assert "application/json" in result.headers["content-type"] + + resp_json = result.json() + assert len(resp_json) == 1 + success_json = resp_json[0] + assert len(success_json) == 1 + + assert "error" in success_json + error_json = success_json["error"] + assert len(error_json) == 3 + assert "/" in error_json["address"] + assert "unauthorized user" in error_json["description"] + assert "1" in error_json["type"] + def test_valid_username_request(self): """Test request with a valid username.""" request_json = {"invalid_key": "my_device"} From 3b6bc9067f5066c68936cef7e34dd54bb364d13e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 02:02:34 +0100 Subject: [PATCH 2137/3953] Move imports to top for mochad (#29514) * Move imports to top for mochad * Fix test test_light.py for mochad * Fix test test_switch.py for mochad * Make intra package imports relative in switch and light --- homeassistant/components/mochad/__init__.py | 13 +++++------ homeassistant/components/mochad/light.py | 23 ++++++++++---------- homeassistant/components/mochad/switch.py | 24 ++++++++++----------- tests/components/mochad/test_light.py | 4 ++-- tests/components/mochad/test_switch.py | 5 ++--- 5 files changed, 35 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/mochad/__init__.py b/homeassistant/components/mochad/__init__.py index 77426e8ae2c911..683681b50a0978 100644 --- a/homeassistant/components/mochad/__init__.py +++ b/homeassistant/components/mochad/__init__.py @@ -2,11 +2,16 @@ import logging import threading +from pymochad import controller, exceptions import voluptuous as vol +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.const import CONF_HOST, CONF_PORT _LOGGER = logging.getLogger(__name__) @@ -37,8 +42,6 @@ def setup(hass, config): host = conf.get(CONF_HOST) port = conf.get(CONF_PORT) - from pymochad import exceptions - global CONTROLLER try: CONTROLLER = MochadCtrl(host, port) @@ -68,8 +71,6 @@ def __init__(self, host, port): self._host = host self._port = port - from pymochad import controller - self.ctrl = controller.PyMochad(server=self._host, port=self._port) @property diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 899908c34bdfc4..871caadd95ca19 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -1,30 +1,32 @@ """Support for X10 dimmer over Mochad.""" import logging +from pymochad import device import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) -from homeassistant.components import mochad -from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_DEVICES, CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_validation as cv +from . import CONF_COMM_TYPE, CONTROLLER, DOMAIN, REQ_LOCK + _LOGGER = logging.getLogger(__name__) CONF_BRIGHTNESS_LEVELS = "brightness_levels" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_PLATFORM): mochad.DOMAIN, + vol.Required(CONF_PLATFORM): DOMAIN, CONF_DEVICES: [ { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): cv.x10_address, - vol.Optional(mochad.CONF_COMM_TYPE): cv.string, + vol.Optional(CONF_COMM_TYPE): cv.string, vol.Optional(CONF_BRIGHTNESS_LEVELS, default=32): vol.All( vol.Coerce(int), vol.In([32, 64, 256]) ), @@ -37,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up X10 dimmers over a mochad controller.""" devs = config.get(CONF_DEVICES) - add_entities([MochadLight(hass, mochad.CONTROLLER.ctrl, dev) for dev in devs]) + add_entities([MochadLight(hass, CONTROLLER.ctrl, dev) for dev in devs]) return True @@ -46,12 +48,11 @@ class MochadLight(Light): def __init__(self, hass, ctrl, dev): """Initialize a Mochad Light Device.""" - from pymochad import device self._controller = ctrl self._address = dev[CONF_ADDRESS] self._name = dev.get(CONF_NAME, f"x10_light_dev_{self._address}") - self._comm_type = dev.get(mochad.CONF_COMM_TYPE, "pl") + self._comm_type = dev.get(CONF_COMM_TYPE, "pl") self.light = device.Device(ctrl, self._address, comm_type=self._comm_type) self._brightness = 0 self._state = self._get_device_status() @@ -64,7 +65,7 @@ def brightness(self): def _get_device_status(self): """Get the status of the light from mochad.""" - with mochad.REQ_LOCK: + with REQ_LOCK: status = self.light.get_status().rstrip() return status == "on" @@ -106,7 +107,7 @@ def _adjust_brightness(self, brightness): def turn_on(self, **kwargs): """Send the command to turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, 255) - with mochad.REQ_LOCK: + with REQ_LOCK: if self._brightness_levels > 32: out_brightness = self._calculate_brightness_value(brightness) self.light.send_cmd(f"xdim {out_brightness}") @@ -124,7 +125,7 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Send the command to turn the light on.""" - with mochad.REQ_LOCK: + with REQ_LOCK: self.light.send_cmd("off") self._controller.read_data() # There is no persistence for X10 modules so we need to prepare diff --git a/homeassistant/components/mochad/switch.py b/homeassistant/components/mochad/switch.py index 0713d50eb4bcb3..14fb601f91942b 100644 --- a/homeassistant/components/mochad/switch.py +++ b/homeassistant/components/mochad/switch.py @@ -1,24 +1,27 @@ """Support for X10 switch over Mochad.""" import logging +from pymochad import device +from pymochad.exceptions import MochadException import voluptuous as vol -from homeassistant.components import mochad from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_NAME, CONF_DEVICES, CONF_PLATFORM, CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_validation as cv +from . import CONF_COMM_TYPE, CONTROLLER, DOMAIN, REQ_LOCK + _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = vol.Schema( { - vol.Required(CONF_PLATFORM): mochad.DOMAIN, + vol.Required(CONF_PLATFORM): DOMAIN, CONF_DEVICES: [ { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): cv.x10_address, - vol.Optional(mochad.CONF_COMM_TYPE): cv.string, + vol.Optional(CONF_COMM_TYPE): cv.string, } ], } @@ -28,7 +31,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up X10 switches over a mochad controller.""" devs = config.get(CONF_DEVICES) - add_entities([MochadSwitch(hass, mochad.CONTROLLER.ctrl, dev) for dev in devs]) + add_entities([MochadSwitch(hass, CONTROLLER.ctrl, dev) for dev in devs]) return True @@ -37,12 +40,11 @@ class MochadSwitch(SwitchDevice): def __init__(self, hass, ctrl, dev): """Initialize a Mochad Switch Device.""" - from pymochad import device self._controller = ctrl self._address = dev[CONF_ADDRESS] self._name = dev.get(CONF_NAME, "x10_switch_dev_%s" % self._address) - self._comm_type = dev.get(mochad.CONF_COMM_TYPE, "pl") + self._comm_type = dev.get(CONF_COMM_TYPE, "pl") self.switch = device.Device(ctrl, self._address, comm_type=self._comm_type) # Init with false to avoid locking HA for long on CM19A (goes from rf # to pl via TM751, but not other way around) @@ -58,10 +60,9 @@ def name(self): def turn_on(self, **kwargs): """Turn the switch on.""" - from pymochad.exceptions import MochadException _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) - with mochad.REQ_LOCK: + with REQ_LOCK: try: # Recycle socket on new command to recover mochad connection self._controller.reconnect() @@ -75,10 +76,9 @@ def turn_on(self, **kwargs): def turn_off(self, **kwargs): """Turn the switch off.""" - from pymochad.exceptions import MochadException _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) - with mochad.REQ_LOCK: + with REQ_LOCK: try: # Recycle socket on new command to recover mochad connection self._controller.reconnect() @@ -92,7 +92,7 @@ def turn_off(self, **kwargs): def _get_device_status(self): """Get the status of the switch from mochad.""" - with mochad.REQ_LOCK: + with REQ_LOCK: status = self.switch.get_status().rstrip() return status == "on" diff --git a/tests/components/mochad/test_light.py b/tests/components/mochad/test_light.py index 7cb4ecb3cbcc00..631c5b407343bf 100644 --- a/tests/components/mochad/test_light.py +++ b/tests/components/mochad/test_light.py @@ -14,8 +14,8 @@ @pytest.fixture(autouse=True) def pymochad_mock(): """Mock pymochad.""" - with mock.patch.dict("sys.modules", {"pymochad": mock.MagicMock()}): - yield + with mock.patch("homeassistant.components.mochad.light.device") as device: + yield device class TestMochadSwitchSetup(unittest.TestCase): diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index 5fc3d4ee415199..8c0dc3554dbb14 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -14,9 +14,8 @@ @pytest.fixture(autouse=True) def pymochad_mock(): """Mock pymochad.""" - with mock.patch.dict( - "sys.modules", - {"pymochad": mock.MagicMock(), "pymochad.exceptions": mock.MagicMock()}, + with mock.patch("homeassistant.components.mochad.switch.device"), mock.patch( + "homeassistant.components.mochad.switch.MochadException" ): yield From dc911ecc5bce9778d163c285d848daf3ffbb6f3d Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 5 Dec 2019 23:31:18 -0500 Subject: [PATCH 2138/3953] Add input_boolean reload service (#29379) * Add input_boolean reload service. * Add reload test. * Address comments. Register reload service as an admin service Setup platform even if there're no entities --- .../components/input_boolean/__init__.py | 53 +++++++++--- .../components/input_boolean/services.yaml | 3 + tests/components/input_boolean/test_init.py | 80 +++++++++++++++++-- 3 files changed, 118 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 4a32ad16797319..326244a7e4c7f8 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -6,16 +6,18 @@ from homeassistant.const import ( CONF_ICON, CONF_NAME, + SERVICE_RELOAD, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_TOGGLE, STATE_ON, ) -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service +from homeassistant.loader import bind_hass DOMAIN = "input_boolean" @@ -41,6 +43,8 @@ extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) + @bind_hass def is_on(hass, entity_id): @@ -52,6 +56,39 @@ async def async_setup(hass, config): """Set up an input boolean.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(config) + + async def reload_service_handler(service_call): + """Remove all input booleans and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + + if entities: + await component.async_add_entities(entities) + + return True + + +async def _async_process_config(config): + """Process config and create list of entities.""" entities = [] for object_id, cfg in config[DOMAIN].items(): @@ -64,17 +101,7 @@ async def async_setup(hass, config): entities.append(InputBoolean(object_id, name, initial, icon)) - if not entities: - return False - - component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") - - component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") - - component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") - - await component.async_add_entities(entities) - return True + return entities class InputBoolean(ToggleEntity, RestoreEntity): diff --git a/homeassistant/components/input_boolean/services.yaml b/homeassistant/components/input_boolean/services.yaml index e49d46c9b86049..e391c15d3a8f1a 100644 --- a/homeassistant/components/input_boolean/services.yaml +++ b/homeassistant/components/input_boolean/services.yaml @@ -10,3 +10,6 @@ turn_on: description: Turns on an input boolean. fields: entity_id: {description: Entity id of the input boolean to turn on., example: input_boolean.notify_alerts} +reload: + description: Reload the input_boolean configuration. + diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index def8db9b35cc43..ed5e927c2ca45d 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -2,20 +2,22 @@ # pylint: disable=protected-access import asyncio import logging +from unittest.mock import patch -from homeassistant.core import CoreState, State, Context -from homeassistant.setup import async_setup_component -from homeassistant.components.input_boolean import is_on, CONF_INITIAL, DOMAIN +from homeassistant.components.input_boolean import CONF_INITIAL, DOMAIN, is_on from homeassistant.const import ( - STATE_ON, - STATE_OFF, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON, + SERVICE_RELOAD, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) +from homeassistant.core import Context, CoreState, State +from homeassistant.setup import async_setup_component from tests.common import mock_component, mock_restore_cache @@ -165,3 +167,71 @@ async def test_input_boolean_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_reload(hass, hass_admin_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + _LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_1": None, + "test_2": {"name": "Hello World", "icon": "mdi:work", "initial": True}, + } + }, + ) + + _LOGGER.debug("ENTITIES: %s", hass.states.async_entity_ids()) + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_boolean.test_1") + state_2 = hass.states.get("input_boolean.test_2") + state_3 = hass.states.get("input_boolean.test_3") + + assert state_1 is not None + assert state_2 is not None + assert state_3 is None + assert STATE_ON == state_2.state + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_2": { + "name": "Hello World reloaded", + "icon": "mdi:work_reloaded", + "initial": False, + }, + "test_3": None, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_boolean.test_1") + state_2 = hass.states.get("input_boolean.test_2") + state_3 = hass.states.get("input_boolean.test_3") + + assert state_1 is None + assert state_2 is not None + assert state_3 is not None + + assert STATE_OFF == state_2.state + assert "Hello World reloaded" == state_2.attributes.get(ATTR_FRIENDLY_NAME) + assert "mdi:work_reloaded" == state_2.attributes.get(ATTR_ICON) From 8def0326dd326ccdd6a117d20ccc895c46c8290d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 06:08:08 +0100 Subject: [PATCH 2139/3953] Move imports to top for izone (#29508) * Move imports to top for izone * Isorted all imports, fixed tests for config_flow --- homeassistant/components/izone/__init__.py | 2 +- homeassistant/components/izone/climate.py | 26 +++++++++---------- homeassistant/components/izone/config_flow.py | 7 +++-- homeassistant/components/izone/discovery.py | 5 ++-- tests/components/izone/test_config_flow.py | 6 ++--- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 6fecbc1f3a67cf..0e5dcddbc488b4 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import IZONE, DATA_CONFIG +from .const import DATA_CONFIG, IZONE from .discovery import async_start_discovery_service, async_stop_discovery_service _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index c932c66627bcae..b80dfc2542ffb2 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -1,22 +1,21 @@ """Support for the iZone HVAC.""" import logging -from typing import Optional, List +from typing import List, Optional -from pizone import Zone, Controller +from pizone import Controller, Zone -from homeassistant.core import callback from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT_COOL, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - FAN_LOW, - FAN_MEDIUM, - FAN_HIGH, - FAN_AUTO, PRESET_ECO, PRESET_NONE, SUPPORT_FAN_MODE, @@ -25,23 +24,24 @@ ) from homeassistant.const import ( ATTR_TEMPERATURE, + CONF_EXCLUDE, PRECISION_HALVES, TEMP_CELSIUS, - CONF_EXCLUDE, ) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( + DATA_CONFIG, DATA_DISCOVERY_SERVICE, - IZONE, - DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_RECONNECTED, DISPATCH_CONTROLLER_UPDATE, DISPATCH_ZONE_UPDATE, - DATA_CONFIG, + IZONE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py index eb57a36a2bb5f7..add1bb47a5400c 100644 --- a/homeassistant/components/izone/config_flow.py +++ b/homeassistant/components/izone/config_flow.py @@ -1,7 +1,7 @@ """Config flow for izone.""" -import logging import asyncio +import logging from async_timeout import timeout @@ -9,14 +9,13 @@ from homeassistant.helpers import config_entry_flow from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import IZONE, TIMEOUT_DISCOVERY, DISPATCH_CONTROLLER_DISCOVERED - +from .const import DISPATCH_CONTROLLER_DISCOVERED, IZONE, TIMEOUT_DISCOVERY +from .discovery import async_start_discovery_service, async_stop_discovery_service _LOGGER = logging.getLogger(__name__) async def _async_has_devices(hass): - from .discovery import async_start_discovery_service, async_stop_discovery_service controller_ready = asyncio.Event() async_dispatcher_connect( diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py index 3630c28605bb7f..c49144f1db9fc9 100644 --- a/homeassistant/components/izone/discovery.py +++ b/homeassistant/components/izone/discovery.py @@ -1,17 +1,18 @@ """Internal discovery service for iZone AC.""" import logging + import pizone from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType from .const import ( DATA_DISCOVERY_SERVICE, - DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_RECONNECTED, DISPATCH_CONTROLLER_UPDATE, DISPATCH_ZONE_UPDATE, diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index faa920271e385b..b5f9aa41c80b17 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -33,9 +33,9 @@ async def test_not_found(hass, mock_disco): """Test not finding iZone controller.""" with patch( - "homeassistant.components.izone.discovery.async_start_discovery_service" + "homeassistant.components.izone.config_flow.async_start_discovery_service" ) as start_disco, patch( - "homeassistant.components.izone.discovery.async_stop_discovery_service", + "homeassistant.components.izone.config_flow.async_stop_discovery_service", return_value=mock_coro(), ) as stop_disco: start_disco.side_effect = _mock_start_discovery(hass, mock_disco) @@ -62,7 +62,7 @@ async def test_found(hass, mock_disco): "homeassistant.components.izone.climate.async_setup_entry", return_value=mock_coro(True), ) as mock_setup, patch( - "homeassistant.components.izone.discovery.async_start_discovery_service" + "homeassistant.components.izone.config_flow.async_start_discovery_service" ) as start_disco, patch( "homeassistant.components.izone.async_start_discovery_service", return_value=mock_coro(), From fcf18aca3403b553fee6aa456fd8abc1c5a74480 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 06:10:29 +0100 Subject: [PATCH 2140/3953] Move imports to top for modbus (#29515) * Move imports to top for modbus * Include imports for TCP and UDP ModbusClients --- homeassistant/components/modbus/__init__.py | 19 ++++++------------- homeassistant/components/modbus/climate.py | 2 +- homeassistant/components/modbus/sensor.py | 2 +- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index a6e901af749e6c..823703ac4c9f67 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -2,6 +2,8 @@ import logging import threading +from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient +from pymodbus.transaction import ModbusRtuFramer import voluptuous as vol from homeassistant.const import ( @@ -91,9 +93,7 @@ def setup_client(client_config): client_type = client_config[CONF_TYPE] if client_type == "serial": - from pymodbus.client.sync import ModbusSerialClient as ModbusClient - - return ModbusClient( + return ModbusSerialClient( method=client_config[CONF_METHOD], port=client_config[CONF_PORT], baudrate=client_config[CONF_BAUDRATE], @@ -103,27 +103,20 @@ def setup_client(client_config): timeout=client_config[CONF_TIMEOUT], ) if client_type == "rtuovertcp": - from pymodbus.client.sync import ModbusTcpClient as ModbusClient - from pymodbus.transaction import ModbusRtuFramer - - return ModbusClient( + return ModbusTcpClient( host=client_config[CONF_HOST], port=client_config[CONF_PORT], framer=ModbusRtuFramer, timeout=client_config[CONF_TIMEOUT], ) if client_type == "tcp": - from pymodbus.client.sync import ModbusTcpClient as ModbusClient - - return ModbusClient( + return ModbusTcpClient( host=client_config[CONF_HOST], port=client_config[CONF_PORT], timeout=client_config[CONF_TIMEOUT], ) if client_type == "udp": - from pymodbus.client.sync import ModbusUdpClient as ModbusClient - - return ModbusClient( + return ModbusUdpClient( host=client_config[CONF_HOST], port=client_config[CONF_PORT], timeout=client_config[CONF_TIMEOUT], diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index c6764482d9628a..99ea686543de95 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -6,8 +6,8 @@ from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_AUTO, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_TEMPERATURE, diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 1a5c71812d610b..86f6445b8d6972 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -1,8 +1,8 @@ """Support for Modbus Register sensors.""" import logging import struct - from typing import Any, Union + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA From 33542f0e5e61374ccc01622f8baf8581bbf55b42 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 6 Dec 2019 16:55:42 +1100 Subject: [PATCH 2141/3953] Bump georss_generic_client to 0.3 (#29532) * bump version of georss_generic_client library * updated requirements --- homeassistant/components/geo_rss_events/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index c681807ad0113d..b8949286dea6c5 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -3,7 +3,7 @@ "name": "Geo RSS events", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": [ - "georss_generic_client==0.2" + "georss_generic_client==0.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c6dad0573ca838..1fd51ad5d0019c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -563,7 +563,7 @@ geojson_client==0.4 geopy==1.19.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.2 +georss_generic_client==0.3 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 484dba217a8fa6..6b5bcf96d2d392 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -181,7 +181,7 @@ geojson_client==0.4 geopy==1.19.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.2 +georss_generic_client==0.3 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 From e2a9c652ab9222a11d6b1b5e2b6ce03493960f58 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 6 Dec 2019 09:08:06 +0100 Subject: [PATCH 2142/3953] Bump pytest to 5.3.1 (#29535) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9d63b59f62ad68..70e78222644bef 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,6 +14,6 @@ pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.3.0 +pytest==5.3.1 requests_mock==1.7.0 responses==0.10.6 From 0aace1da5519dd46ab97fdc266daae4349292adb Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:08:54 +0100 Subject: [PATCH 2143/3953] Move imports to top for nx584 (#29537) --- homeassistant/components/nx584/alarm_control_panel.py | 2 +- homeassistant/components/nx584/binary_sensor.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 62bc7ae32bba34..7a064ef0d007e5 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -1,6 +1,7 @@ """Support for NX584 alarm control panels.""" import logging +from nx584 import client import requests import voluptuous as vol @@ -56,7 +57,6 @@ class NX584Alarm(alarm.AlarmControlPanel): def __init__(self, hass, url, name): """Init the nx584 alarm panel.""" - from nx584 import client self._hass = hass self._name = name diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 6f1c66d8d879b6..f6006ff2de486c 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -3,13 +3,14 @@ import threading import time +from nx584 import client as nx584_client import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, - BinarySensorDevice, PLATFORM_SCHEMA, + BinarySensorDevice, ) from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -39,7 +40,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NX584 binary sensor platform.""" - from nx584 import client as nx584_client host = config.get(CONF_HOST) port = config.get(CONF_PORT) From a8571485601114b908204f9d73af2b5dd30c9ed1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:09:45 +0100 Subject: [PATCH 2144/3953] Move imports to top for pjlink (#29540) --- homeassistant/components/pjlink/media_player.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index ea35fe7fb75a7d..e93e6e5fb20283 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -1,9 +1,11 @@ """Support for controlling projector via the PJLink protocol.""" import logging +from pypjlink import MUTE_AUDIO, Projector +from pypjlink.projector import ProjectorError import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -90,7 +92,6 @@ def __init__(self, host, port, name, encoding, password): def projector(self): """Create PJLink Projector instance.""" - from pypjlink import Projector projector = Projector.from_address(self._host, self._port, self._encoding) projector.authenticate(self._password) @@ -98,7 +99,6 @@ def projector(self): def update(self): """Get the latest state from the device.""" - from pypjlink.projector import ProjectorError with self.projector() as projector: try: @@ -171,8 +171,6 @@ def turn_on(self): def mute_volume(self, mute): """Mute (true) of unmute (false) media player.""" with self.projector() as projector: - from pypjlink import MUTE_AUDIO - projector.set_mute(MUTE_AUDIO, mute) def select_source(self, source): From 1bb499aec21f34688f1d17eab771458a34af3e83 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:11:07 +0100 Subject: [PATCH 2145/3953] Move imports to top for smhi (#29545) --- homeassistant/components/smhi/config_flow.py | 2 +- homeassistant/components/smhi/weather.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 3b60cb66165015..2c04896497a1fc 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure SMHI component.""" +from smhi.smhi_lib import Smhi, SmhiForecastException import voluptuous as vol from homeassistant import config_entries @@ -96,7 +97,6 @@ async def _show_config_form( async def _check_location(self, longitude: str, latitude: str) -> bool: """Return true if location is ok.""" - from smhi.smhi_lib import Smhi, SmhiForecastException try: session = aiohttp_client.async_get_clientsession(self.hass) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 5f6722b72a6df3..574b8d8576725c 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -6,6 +6,8 @@ import aiohttp import async_timeout +from smhi import Smhi +from smhi.smhi_lib import SmhiForecastException from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -90,7 +92,6 @@ def __init__( session: aiohttp.ClientSession = None, ) -> None: """Initialize the SMHI weather entity.""" - from smhi import Smhi self._name = name self._latitude = latitude @@ -107,7 +108,6 @@ def unique_id(self) -> str: @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Refresh the forecast data from SMHI weather API.""" - from smhi.smhi_lib import SmhiForecastException def fail(): """Postpone updates.""" From d5419b77f9c245e5af006143eb55ae4dda3f174e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 09:13:44 +0100 Subject: [PATCH 2146/3953] Move imports to top for sleepiq (#29544) --- homeassistant/components/sleepiq/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index 7035299709dbb8..2b4d9d010a3a6b 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -1,13 +1,14 @@ """Support for SleepIQ from SleepNumber.""" -import logging from datetime import timedelta +import logging +from sleepyq import Sleepyq import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.util import Throttle DOMAIN = "sleepiq" @@ -47,8 +48,6 @@ def setup(hass, config): """ global DATA - from sleepyq import Sleepyq - username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] client = Sleepyq(username, password) From ec3ffe309ac29da3cc527a73ef0e63ab5a6a1aea Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 10:40:38 +0100 Subject: [PATCH 2147/3953] Move imports to top for toon (#29553) --- homeassistant/components/toon/__init__.py | 12 ++++++------ homeassistant/components/toon/binary_sensor.py | 6 +++--- homeassistant/components/toon/climate.py | 6 +++--- homeassistant/components/toon/config_flow.py | 17 ++++++++--------- homeassistant/components/toon/sensor.py | 8 ++++---- tests/components/toon/test_config_flow.py | 16 ++++++++-------- 6 files changed, 32 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 19466ba49c4a10..348826a12645d1 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -1,17 +1,18 @@ """Support for Toon van Eneco devices.""" +from functools import partial import logging from typing import Any, Dict -from functools import partial +from toonapilib import Toon import voluptuous as vol +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import callback -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import config_flow # noqa: F401 from .const import ( @@ -19,10 +20,10 @@ CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, + DATA_TOON, DATA_TOON_CLIENT, DATA_TOON_CONFIG, DATA_TOON_UPDATED, - DATA_TOON, DEFAULT_SCAN_INTERVAL, DOMAIN, ) @@ -63,7 +64,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: """Set up Toon from a config entry.""" - from toonapilib import Toon conf = hass.data.get(DATA_TOON_CONFIG) diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index 9962e2c32d3563..7cf52919efed1c 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -8,11 +8,11 @@ from homeassistant.helpers.typing import HomeAssistantType from . import ( - ToonData, - ToonEntity, - ToonDisplayDeviceEntity, ToonBoilerDeviceEntity, ToonBoilerModuleDeviceEntity, + ToonData, + ToonDisplayDeviceEntity, + ToonEntity, ) from .const import DATA_TOON, DOMAIN diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index cfe07adfda39ae..9ce9991c37143f 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -5,6 +5,8 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_HEAT, PRESET_AWAY, PRESET_COMFORT, @@ -12,8 +14,6 @@ PRESET_SLEEP, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -21,8 +21,8 @@ from . import ToonData, ToonDisplayDeviceEntity from .const import ( - DATA_TOON_CLIENT, DATA_TOON, + DATA_TOON_CLIENT, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index 62c141b003af6d..ce4f347eaf2ac8 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -1,8 +1,15 @@ """Config flow to configure the Toon component.""" from collections import OrderedDict -import logging from functools import partial +import logging +from toonapilib import Toon +from toonapilib.toonapilibexceptions import ( + AgreementsRetrievalError, + InvalidConsumerKey, + InvalidConsumerSecret, + InvalidCredentials, +) import voluptuous as vol from homeassistant import config_entries @@ -67,13 +74,6 @@ async def _show_authenticaticate_form(self, errors=None): async def async_step_authenticate(self, user_input=None): """Attempt to authenticate with the Toon account.""" - from toonapilib import Toon - from toonapilib.toonapilibexceptions import ( - InvalidConsumerSecret, - InvalidConsumerKey, - InvalidCredentials, - AgreementsRetrievalError, - ) if user_input is None: return await self._show_authenticaticate_form() @@ -129,7 +129,6 @@ async def _show_display_form(self, errors=None): async def async_step_display(self, user_input=None): """Select Toon display to add.""" - from toonapilib import Toon if not self.displays: return self.async_abort(reason="no_displays") diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index f82bcb7ac1b978..79a8fa28540cfa 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -2,18 +2,18 @@ import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT +from homeassistant.helpers.typing import HomeAssistantType from . import ( + ToonBoilerDeviceEntity, ToonData, - ToonEntity, ToonElectricityMeterDeviceEntity, + ToonEntity, ToonGasMeterDeviceEntity, ToonSolarDeviceEntity, - ToonBoilerDeviceEntity, ) -from .const import CURRENCY_EUR, DATA_TOON, DOMAIN, VOLUME_CM3, VOLUME_M3, RATIO_PERCENT +from .const import CURRENCY_EUR, DATA_TOON, DOMAIN, RATIO_PERCENT, VOLUME_CM3, VOLUME_M3 _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index a4d7c760ca1366..45d16908446ba6 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -22,7 +22,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, MockDependency +from tests.common import MockConfigEntry FIXTURE_APP = { DOMAIN: {CONF_CLIENT_ID: "1234567890abcdef", CONF_CLIENT_SECRET: "1234567890abcdef"} @@ -40,9 +40,9 @@ @pytest.fixture def mock_toonapilib(): """Mock toonapilib.""" - with MockDependency("toonapilib") as mock_toonapilib_: - mock_toonapilib_.Toon().display_names = [FIXTURE_DISPLAY[CONF_DISPLAY]] - yield mock_toonapilib_ + with patch("homeassistant.components.toon.config_flow.Toon") as Toon: + Toon().display_names = [FIXTURE_DISPLAY[CONF_DISPLAY]] + yield Toon async def setup_component(hass): @@ -90,7 +90,7 @@ async def test_toon_abort(hass, mock_toonapilib, side_effect, reason): flow = config_flow.ToonFlowHandler() flow.hass = hass - mock_toonapilib.Toon.side_effect = side_effect + mock_toonapilib.side_effect = side_effect result = await flow.async_step_authenticate(user_input=FIXTURE_CREDENTIALS) @@ -100,7 +100,7 @@ async def test_toon_abort(hass, mock_toonapilib, side_effect, reason): async def test_invalid_credentials(hass, mock_toonapilib): """Test we show authentication form on Toon auth error.""" - mock_toonapilib.Toon.side_effect = InvalidCredentials + mock_toonapilib.side_effect = InvalidCredentials await setup_component(hass) @@ -140,7 +140,7 @@ async def test_no_displays(hass, mock_toonapilib): """Test abort when there are no displays.""" await setup_component(hass) - mock_toonapilib.Toon().display_names = [] + mock_toonapilib().display_names = [] flow = config_flow.ToonFlowHandler() flow.hass = hass @@ -177,7 +177,7 @@ async def test_abort_last_minute_fail(hass, mock_toonapilib): flow.hass = hass await flow.async_step_user(user_input=FIXTURE_CREDENTIALS) - mock_toonapilib.Toon.side_effect = Exception + mock_toonapilib.side_effect = Exception result = await flow.async_step_display(user_input=FIXTURE_DISPLAY) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT From 27530be46fd7550edba2b1725ea6b790b74328cc Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 13:05:35 +0100 Subject: [PATCH 2148/3953] Move imports to top for influxdb (#29513) --- homeassistant/components/influxdb/__init__.py | 11 +++++------ homeassistant/components/influxdb/sensor.py | 2 +- tests/components/influxdb/test_init.py | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 86d489621eaad3..48852f279109ed 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -1,11 +1,12 @@ """Support for sending data to an Influx database.""" import logging -import re +import math import queue +import re import threading import time -import math +from influxdb import InfluxDBClient, exceptions import requests.exceptions import voluptuous as vol @@ -20,12 +21,12 @@ CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, + EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.helpers import state as state_helper, event as event_helper +from homeassistant.helpers import event as event_helper, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues @@ -118,7 +119,6 @@ def setup(hass, config): """Set up the InfluxDB component.""" - from influxdb import InfluxDBClient, exceptions conf = config[DOMAIN] @@ -341,7 +341,6 @@ def get_events_json(self): def write_to_influxdb(self, json): """Write preprocessed events to influxdb, with retry.""" - from influxdb import exceptions for retry in range(self.max_tries + 1): try: diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index e94824c9abbb94..58fbc5605db50f 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from influxdb import InfluxDBClient, exceptions import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -94,7 +95,6 @@ class InfluxSensor(Entity): def __init__(self, hass, influx_conf, query): """Initialize the sensor.""" - from influxdb import InfluxDBClient, exceptions self._name = query.get(CONF_NAME) self._unit_of_measurement = query.get(CONF_UNIT_OF_MEASUREMENT) diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 26f3c9bcd0b798..3e0e1dcf7066a9 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -10,7 +10,7 @@ from tests.common import get_test_home_assistant -@mock.patch("influxdb.InfluxDBClient") +@mock.patch("homeassistant.components.influxdb.InfluxDBClient") @mock.patch( "homeassistant.components.influxdb.InfluxThread.batch_timeout", mock.Mock(return_value=0), From c5f4872aea24b42c4fb7a5ee990fccdf655b96f5 Mon Sep 17 00:00:00 2001 From: David K <142583+neffs@users.noreply.github.com> Date: Fri, 6 Dec 2019 14:07:45 +0100 Subject: [PATCH 2149/3953] Limit available heat/cool modes for HomeKit thermostats (#28586) * Limit available heat/cool modes for HomeKit thermostats. The Home app only shows appropriate modes (heat/cool/auto) for the device. Depending on the climate integration, disabling the auto start might be needed. * Include improved mapping for HVAC modes in tests --- .../components/homekit/type_thermostats.py | 57 ++++++++++-- .../homekit/test_type_thermostats.py | 87 ++++++++++++++----- 2 files changed, 115 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 9adc3cc0600bf5..b6e1e75d3c6529 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -7,6 +7,7 @@ ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, + ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TARGET_TEMP_HIGH, @@ -23,6 +24,8 @@ HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_FAN_ONLY, SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -60,13 +63,18 @@ _LOGGER = logging.getLogger(__name__) +HC_HOMEKIT_VALID_MODES_WATER_HEATER = { + "Heat": 1, +} UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()} HC_HASS_TO_HOMEKIT = { HVAC_MODE_OFF: 0, HVAC_MODE_HEAT: 1, HVAC_MODE_COOL: 2, + HVAC_MODE_AUTO: 3, HVAC_MODE_HEAT_COOL: 3, + HVAC_MODE_FAN_ONLY: 2, } HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} @@ -97,9 +105,9 @@ def __init__(self, *args): # Add additional characteristics if auto mode is supported self.chars = [] - features = self.hass.states.get(self.entity_id).attributes.get( - ATTR_SUPPORTED_FEATURES, 0 - ) + state = self.hass.states.get(self.entity_id) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & SUPPORT_TARGET_TEMPERATURE_RANGE: self.chars.extend( (CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) @@ -107,12 +115,44 @@ def __init__(self, *args): serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) - # Current and target mode characteristics + # Current mode characteristics self.char_current_heat_cool = serv_thermostat.configure_char( CHAR_CURRENT_HEATING_COOLING, value=0 ) + + # Target mode characteristics + hc_modes = state.attributes.get(ATTR_HVAC_MODES, None) + if hc_modes is None: + _LOGGER.error( + "%s: HVAC modes not yet available. Please disable auto start for homekit.", + self.entity_id, + ) + hc_modes = ( + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + ) + + # determine available modes for this entity, prefer AUTO over HEAT_COOL and COOL over FAN_ONLY + self.hc_homekit_to_hass = { + c: s + for s, c in HC_HASS_TO_HOMEKIT.items() + if ( + s in hc_modes + and not ( + (s == HVAC_MODE_HEAT_COOL and HVAC_MODE_AUTO in hc_modes) + or (s == HVAC_MODE_FAN_ONLY and HVAC_MODE_COOL in hc_modes) + ) + ) + } + hc_valid_values = {k: v for v, k in self.hc_homekit_to_hass.items()} + self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=0, setter_callback=self.set_heat_cool + CHAR_TARGET_HEATING_COOLING, + value=0, + setter_callback=self.set_heat_cool, + valid_values=hc_valid_values, ) # Current and target temperature characteristics @@ -185,7 +225,7 @@ def set_heat_cool(self, value): """Change operation mode to value if call came from HomeKit.""" _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) self._flag_heat_cool = True - hass_value = HC_HOMEKIT_TO_HASS[value] + hass_value = self.hc_homekit_to_hass[value] params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HVAC_MODE: hass_value} self.call_service( DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE_THERMOSTAT, params, hass_value @@ -318,7 +358,10 @@ def __init__(self, *args): CHAR_CURRENT_HEATING_COOLING, value=1 ) self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=1, setter_callback=self.set_heat_cool + CHAR_TARGET_HEATING_COOLING, + value=1, + setter_callback=self.set_heat_cool, + valid_values=HC_HOMEKIT_VALID_MODES_WATER_HEATER, ) self.char_current_temp = serv_thermostat.configure_char( diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index c896ad211e8685..9f9ebcdfd32ee3 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -24,6 +24,8 @@ HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_AUTO, ) from homeassistant.components.homekit.const import ( ATTR_VALUE, @@ -64,7 +66,20 @@ async def test_thermostat(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" - hass.states.async_set(entity_id, HVAC_MODE_OFF) + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) await hass.async_add_job(acc.run) @@ -120,7 +135,7 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, { ATTR_TEMPERATURE: 20.0, ATTR_CURRENT_TEMPERATURE: 25.0, @@ -164,9 +179,8 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, @@ -183,7 +197,6 @@ async def test_thermostat(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 25.0, ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, @@ -198,9 +211,8 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -226,14 +238,23 @@ async def test_thermostat(hass, hk_driver, cls, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "19.0°C" - await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 2) await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT - assert acc.char_target_heat_cool.value == 1 + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + assert acc.char_target_heat_cool.value == 2 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == HVAC_MODE_HEAT + assert events[-1].data[ATTR_VALUE] == HVAC_MODE_COOL + + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3) + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_AUTO + assert acc.char_target_heat_cool.value == 3 + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] == HVAC_MODE_AUTO async def test_thermostat_auto(hass, hk_driver, cls, events): @@ -261,7 +282,6 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 22.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, @@ -278,9 +298,8 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 24.0, @@ -291,15 +310,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): assert acc.char_heating_thresh_temp.value == 19.0 assert acc.char_cooling_thresh_temp.value == 23.0 assert acc.char_current_heat_cool.value == 2 - assert acc.char_target_heat_cool.value == 3 + assert acc.char_target_heat_cool.value == 2 assert acc.char_current_temp.value == 24.0 assert acc.char_display_units.value == 0 hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, @@ -346,7 +364,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): HVAC_MODE_HEAT, { ATTR_SUPPORTED_FEATURES: 4096, - ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, @@ -364,7 +381,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): entity_id, HVAC_MODE_OFF, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -378,7 +394,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): entity_id, HVAC_MODE_OFF, { - ATTR_HVAC_MODE: HVAC_MODE_OFF, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -423,7 +438,6 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 75.2, ATTR_TARGET_TEMP_LOW: 68.1, ATTR_TEMPERATURE: 71.6, @@ -503,6 +517,34 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): assert acc.char_target_temp.properties[PROP_MIN_STEP] == 1.0 +async def test_thermostat_hvac_modes(hass, hk_driver, cls): + """Test if unsupported HVAC modes are deactivated in HomeKit.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, HVAC_MODE_OFF, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]} + ) + + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 3) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 0 + + await hass.async_add_job(acc.char_target_heat_cool.set_value, 1) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 2) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + async def test_water_heater(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "water_heater.test" @@ -571,7 +613,8 @@ async def test_water_heater(hass, hk_driver, cls, events): await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 - await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3) + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 3) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 From f3717421c0cca59395cfec4574441020f8469f2f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 14:59:32 +0100 Subject: [PATCH 2150/3953] Move imports to top for heatmiser (#29562) --- homeassistant/components/heatmiser/climate.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index 1954749c21bf79..553ae8f4bc3b83 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -2,28 +2,27 @@ import logging from typing import List +from heatmiserV3 import connection, heatmiser import voluptuous as vol -from heatmiserV3 import heatmiser, connection from homeassistant.components.climate import ( - ClimateDevice, - PLATFORM_SCHEMA, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PLATFORM_SCHEMA, + ClimateDevice, ) from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE from homeassistant.const import ( - TEMP_CELSIUS, - TEMP_FAHRENHEIT, ATTR_TEMPERATURE, CONF_HOST, - CONF_PORT, CONF_ID, CONF_NAME, + CONF_PORT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) CONF_THERMOSTATS = "tstats" From d9b52ef98c61854ca5f42d34b353a94758dec151 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 15:00:54 +0100 Subject: [PATCH 2151/3953] Move imports to top for plant (#29543) --- homeassistant/components/plant/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index a516e06d55bce4..cc405dcad1f643 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant.components import group +from homeassistant.components.recorder.models import States from homeassistant.components.recorder.util import execute, session_scope from homeassistant.const import ( ATTR_TEMPERATURE, @@ -288,7 +289,6 @@ async def _load_history_from_db(self): This only needs to be done once during startup. """ - from homeassistant.components.recorder.models import States start_date = datetime.now() - timedelta(days=self._conf_check_days) entity_id = self._readingmap.get(READING_BRIGHTNESS) From 606d310ea346ee28c39101c22f535ea7743173b0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 15:40:04 +0100 Subject: [PATCH 2152/3953] Move imports to top for spc (#29547) * Move imports to top for spc * Fix pylint error by removing duplicate import --- homeassistant/components/spc/__init__.py | 10 +++++----- homeassistant/components/spc/alarm_control_panel.py | 7 ++----- homeassistant/components/spc/binary_sensor.py | 3 ++- tests/components/spc/test_init.py | 10 ++++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/spc/__init__.py b/homeassistant/components/spc/__init__.py index b5db4b685ae808..1601090463f442 100644 --- a/homeassistant/components/spc/__init__.py +++ b/homeassistant/components/spc/__init__.py @@ -1,11 +1,14 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" import logging +from pyspcwebgw import SpcWebGateway +from pyspcwebgw.area import Area +from pyspcwebgw.zone import Zone import voluptuous as vol -from homeassistant.helpers import discovery, aiohttp_client -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers import aiohttp_client, discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) @@ -33,11 +36,8 @@ async def async_setup(hass, config): """Set up the SPC component.""" - from pyspcwebgw import SpcWebGateway async def async_upate_callback(spc_object): - from pyspcwebgw.area import Area - from pyspcwebgw.zone import Zone if isinstance(spc_object, Area): async_dispatcher_send(hass, SIGNAL_UPDATE_ALARM.format(spc_object.id)) diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index fa9a9681fff041..ca5d77b2a828a5 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" import logging +from pyspcwebgw.const import AreaMode + import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -24,7 +26,6 @@ def _get_alarm_state(area): """Get the alarm state.""" - from pyspcwebgw.const import AreaMode if area.verified_alarm: return STATE_ALARM_TRIGGERED @@ -92,24 +93,20 @@ def supported_features(self) -> int: async def async_alarm_disarm(self, code=None): """Send disarm command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.UNSET) async def async_alarm_arm_home(self, code=None): """Send arm home command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_A) async def async_alarm_arm_night(self, code=None): """Send arm home command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_B) async def async_alarm_arm_away(self, code=None): """Send arm away command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.FULL_SET) diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index 1ce02af390f267..2104f931c0a857 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" import logging +from pyspcwebgw.const import ZoneInput + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -60,7 +62,6 @@ def name(self): @property def is_on(self): """Whether the device is switched on.""" - from pyspcwebgw.const import ZoneInput return self._zone.input == ZoneInput.OPEN diff --git a/tests/components/spc/test_init.py b/tests/components/spc/test_init.py index 25b9515e44deb9..f726a064dd1bdc 100644 --- a/tests/components/spc/test_init.py +++ b/tests/components/spc/test_init.py @@ -13,7 +13,8 @@ async def test_valid_device_config(hass, monkeypatch): config = {"spc": {"api_url": "http://localhost/", "ws_url": "ws://localhost/"}} with patch( - "pyspcwebgw.SpcWebGateway.async_load_parameters", return_value=mock_coro(True) + "homeassistant.components.spc.SpcWebGateway.async_load_parameters", + return_value=mock_coro(True), ): assert await async_setup_component(hass, "spc", config) is True @@ -23,7 +24,8 @@ async def test_invalid_device_config(hass, monkeypatch): config = {"spc": {"api_url": "http://localhost/"}} with patch( - "pyspcwebgw.SpcWebGateway.async_load_parameters", return_value=mock_coro(True) + "homeassistant.components.spc.SpcWebGateway.async_load_parameters", + return_value=mock_coro(True), ): assert await async_setup_component(hass, "spc", config) is False @@ -45,11 +47,11 @@ async def test_update_alarm_device(hass): area_mock.verified_alarm = False with patch( - "pyspcwebgw.SpcWebGateway.areas", new_callable=PropertyMock + "homeassistant.components.spc.SpcWebGateway.areas", new_callable=PropertyMock ) as mock_areas: mock_areas.return_value = {"1": area_mock} with patch( - "pyspcwebgw.SpcWebGateway.async_load_parameters", + "homeassistant.components.spc.SpcWebGateway.async_load_parameters", return_value=mock_coro(True), ): assert await async_setup_component(hass, "spc", config) is True From 48aba426a9813175df6bb41aaf5f4617f46cf8c3 Mon Sep 17 00:00:00 2001 From: Martin Rowan Date: Fri, 6 Dec 2019 16:40:59 +0000 Subject: [PATCH 2153/3953] Bump lightwave to 0.17 (#29566) --- homeassistant/components/lightwave/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lightwave/manifest.json b/homeassistant/components/lightwave/manifest.json index 4b2456f0df5814..c39a8a6bae8caf 100644 --- a/homeassistant/components/lightwave/manifest.json +++ b/homeassistant/components/lightwave/manifest.json @@ -3,7 +3,7 @@ "name": "Lightwave", "documentation": "https://www.home-assistant.io/integrations/lightwave", "requirements": [ - "lightwave==0.15" + "lightwave==0.17" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 1fd51ad5d0019c..a9ac029884db89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -778,7 +778,7 @@ liffylights==0.9.4 lightify==1.0.7.2 # homeassistant.components.lightwave -lightwave==0.15 +lightwave==0.17 # homeassistant.components.limitlessled limitlessled==1.1.3 From b8434fdcfda3a93ef568d7f31ad669baa65272d9 Mon Sep 17 00:00:00 2001 From: 1v0dev <56637685+1v0dev@users.noreply.github.com> Date: Fri, 6 Dec 2019 18:45:27 +0200 Subject: [PATCH 2154/3953] Add service to set netatmo home heating schedule (#29244) * Add service to set netatmo home heating schedule. * handle NoDevice exeption; add service argument constant --- homeassistant/components/netatmo/__init__.py | 24 +++++++++++++++++++ .../components/netatmo/services.yaml | 7 ++++++ 2 files changed, 31 insertions(+) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index de371f97e28277..9edeb0937a138e 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -30,6 +30,7 @@ SERVICE_ADDWEBHOOK = "addwebhook" SERVICE_DROPWEBHOOK = "dropwebhook" +SERVICE_SETSCHEDULE = "set_schedule" NETATMO_AUTH = None NETATMO_WEBHOOK_URL = None @@ -63,6 +64,7 @@ ATTR_FACE_URL = "face_url" ATTR_SNAPSHOT_URL = "snapshot_url" ATTR_VIGNETTE_URL = "vignette_url" +ATTR_SCHEDULE = "schedule" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) @@ -87,6 +89,8 @@ SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({}) +SCHEMA_SERVICE_SETSCHEDULE = vol.Schema({vol.Required(ATTR_SCHEDULE): cv.string}) + def setup(hass, config): """Set up the Netatmo devices.""" @@ -106,6 +110,12 @@ def setup(hass, config): _LOGGER.error("Unable to connect to Netatmo API") return False + try: + home_data = pyatmo.HomeData(auth) + except pyatmo.NoDevice: + home_data = None + _LOGGER.debug("No climate device. Disable %s service", SERVICE_SETSCHEDULE) + # Store config to be used during entry setup hass.data[DATA_NETATMO_AUTH] = auth @@ -151,6 +161,20 @@ def _service_dropwebhook(service): schema=SCHEMA_SERVICE_DROPWEBHOOK, ) + def _service_setschedule(service): + """Service to change current home schedule.""" + schedule_name = service.data.get(ATTR_SCHEDULE) + home_data.switchHomeSchedule(schedule=schedule_name) + _LOGGER.info("Set home schedule to %s", schedule_name) + + if home_data is not None: + hass.services.register( + DOMAIN, + SERVICE_SETSCHEDULE, + _service_setschedule, + schema=SCHEMA_SERVICE_SETSCHEDULE, + ) + return True diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index a928f4765e06ae..d8fa223780a187 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -28,3 +28,10 @@ set_light_off: entity_id: description: Entity id. example: 'camera.living_room' + +set_schedule: + description: Set the home heating schedule + fields: + schedule: + description: Schedule name + example: Standard \ No newline at end of file From c9415ab75d84aedf5a1e667e40c49c3383fcbe91 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 17:46:24 +0100 Subject: [PATCH 2155/3953] Move imports to top for homematic (#29558) --- homeassistant/components/homematic/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 42fb73f6da2e9d..828e170c67de19 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -1,8 +1,9 @@ """Support for HomeMatic devices.""" -from datetime import timedelta, datetime +from datetime import datetime, timedelta from functools import partial import logging +from pyhomematic import HMConnection import voluptuous as vol from homeassistant.const import ( @@ -366,7 +367,6 @@ def setup(hass, config): """Set up the Homematic component.""" - from pyhomematic import HMConnection conf = config[DOMAIN] hass.data[DATA_CONF] = remotes = {} From d257fff9cec9af2dca86451c59bf796fc4294c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 6 Dec 2019 19:54:11 +0200 Subject: [PATCH 2156/3953] Use "kB" and "s" as UPnP/IGD units (#29552) For consistency with various existing components, and they're more commonly used and compact than "kbyte" and "sec". --- homeassistant/components/upnp/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 4c85e904b1db4f..7b5071aabd3e05 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -189,7 +189,7 @@ def icon(self) -> str: @property def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" - return f"{self.unit}/sec" + return f"{self.unit}/s" def _is_overflowed(self, new_value) -> bool: """Check if value has overflowed.""" @@ -222,7 +222,7 @@ class KBytePerSecondUPnPIGDSensor(PerSecondUPnPIGDSensor): @property def unit(self) -> str: """Get unit we are measuring in.""" - return "kbyte" + return "kB" async def _async_fetch_value(self) -> float: """Fetch value from device.""" From 8afe13e81840b23c2cd0b241ace1c233481dbf57 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 7 Dec 2019 00:39:18 +0530 Subject: [PATCH 2157/3953] Upgrade certifi to >=2019.11.28 (#29571) --- 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 e0a823da44e733..372ff66bffd7e9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 -certifi>=2019.9.11 +certifi>=2019.11.28 contextvars==2.4;python_version<"3.7" cryptography==2.8 defusedxml==0.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index a9ac029884db89..f7e608cc1fdfb7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,7 +4,7 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 -certifi>=2019.9.11 +certifi>=2019.11.28 contextvars==2.4;python_version<"3.7" importlib-metadata==0.23 jinja2>=2.10.3 diff --git a/setup.py b/setup.py index 99195ee2f5663b..c153ad1f763260 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ "async_timeout==3.0.1", "attrs==19.3.0", "bcrypt==3.1.7", - "certifi>=2019.9.11", + "certifi>=2019.11.28", 'contextvars==2.4;python_version<"3.7"', "importlib-metadata==0.23", "jinja2>=2.10.3", From 6af30bc232a7139af3a334015f1f7b6d0eab9c3b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 20:40:00 +0100 Subject: [PATCH 2158/3953] Move imports to top for notion (#29539) * Move imports to top for notion * Fix mocking library in test_config_flow.py --- homeassistant/components/notion/binary_sensor.py | 1 - homeassistant/components/notion/config_flow.py | 4 ++-- tests/components/notion/test_config_flow.py | 7 ++++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 85495c040fa451..5079348e82136b 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -17,7 +17,6 @@ SENSOR_WINDOW_HINGED_VERTICAL, NotionEntity, ) - from .const import DATA_CLIENT, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index affda29e4d61a3..2af231d582e4d8 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the Notion integration.""" +from aionotion import async_get_client +from aionotion.errors import NotionError import voluptuous as vol from homeassistant import config_entries @@ -40,8 +42,6 @@ async def async_step_import(self, import_config): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from aionotion import async_get_client - from aionotion.errors import NotionError if not user_input: return await self._show_form() diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 42b28d2c0e154d..aa942a8905d069 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -1,12 +1,13 @@ """Define tests for the Notion config flow.""" import aionotion +from unittest.mock import patch import pytest from homeassistant import data_entry_flow from homeassistant.components.notion import DOMAIN, config_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.common import MockConfigEntry, MockDependency, mock_coro +from tests.common import MockConfigEntry, mock_coro @pytest.fixture @@ -18,8 +19,8 @@ def mock_client_coro(): @pytest.fixture def mock_aionotion(mock_client_coro): """Mock the aionotion library.""" - with MockDependency("aionotion") as mock_: - mock_.async_get_client.return_value = mock_client_coro + with patch("homeassistant.components.notion.config_flow.async_get_client") as mock_: + mock_.return_value = mock_client_coro yield mock_ From 74d86dfff95fef5ea28fd7d4af072dcd40b256da Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 21:11:02 +0100 Subject: [PATCH 2159/3953] Move imports to top for soundtouch (#29546) * Move imports to top for soundtouch * Format with black * Fix pyling error by renaming variable * Rename entity instances to include entity instead of device --- .../components/soundtouch/media_player.py | 14 +- .../soundtouch/test_media_player.py | 137 ++++++++++++++---- 2 files changed, 116 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index a9f6e05011fced..4a0f6b55b227bc 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -2,6 +2,7 @@ import logging import re +from libsoundtouch import soundtouch_device import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice @@ -100,9 +101,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return remote_config = {"id": "ha.component.soundtouch", "host": host, "port": port} - soundtouch_device = SoundTouchDevice(None, remote_config) - hass.data[DATA_SOUNDTOUCH].append(soundtouch_device) - add_entities([soundtouch_device]) + bose_soundtouch_entity = SoundTouchDevice(None, remote_config) + hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) + add_entities([bose_soundtouch_entity]) else: name = config.get(CONF_NAME) remote_config = { @@ -110,9 +111,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "port": config.get(CONF_PORT), "host": config.get(CONF_HOST), } - soundtouch_device = SoundTouchDevice(name, remote_config) - hass.data[DATA_SOUNDTOUCH].append(soundtouch_device) - add_entities([soundtouch_device]) + bose_soundtouch_entity = SoundTouchDevice(name, remote_config) + hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) + add_entities([bose_soundtouch_entity]) def service_handle(service): """Handle the applying of a service.""" @@ -184,7 +185,6 @@ class SoundTouchDevice(MediaPlayerDevice): def __init__(self, name, config): """Create Soundtouch Entity.""" - from libsoundtouch import soundtouch_device self._device = soundtouch_device(config["host"], config["port"]) if name is None: diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index bf6d2f72b4abce..f9921b5bcb2e81 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -148,7 +148,10 @@ def tearDown(self): # pylint: disable=invalid-name logging.disable(logging.NOTSET) self.hass.stop() - @mock.patch("libsoundtouch.soundtouch_device", side_effect=None) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=None, + ) def test_ensure_setup_config(self, mocked_soundtouch_device): """Test setup OK with custom config.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) @@ -158,7 +161,10 @@ def test_ensure_setup_config(self, mocked_soundtouch_device): assert all_devices[0].config["port"] == 8090 assert mocked_soundtouch_device.call_count == 1 - @mock.patch("libsoundtouch.soundtouch_device", side_effect=None) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=None, + ) def test_ensure_setup_discovery(self, mocked_soundtouch_device): """Test setup with discovery.""" new_device = { @@ -174,7 +180,10 @@ def test_ensure_setup_discovery(self, mocked_soundtouch_device): assert all_devices[0].config["host"] == "192.168.1.1" assert mocked_soundtouch_device.call_count == 1 - @mock.patch("libsoundtouch.soundtouch_device", side_effect=None) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=None, + ) def test_ensure_setup_discovery_no_duplicate(self, mocked_soundtouch_device): """Test setup OK if device already exists.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) @@ -203,7 +212,10 @@ def test_ensure_setup_discovery_no_duplicate(self, mocked_soundtouch_device): @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_update(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test update device state.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) @@ -218,7 +230,10 @@ def test_update(self, mocked_soundtouch_device, mocked_status, mocked_volume): @mock.patch( "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusPlaying ) - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_playing_media( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -240,7 +255,10 @@ def test_playing_media( @mock.patch( "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusUnknown ) - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_playing_unknown_media( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -257,7 +275,10 @@ def test_playing_unknown_media( "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusPlayingRadio, ) - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_playing_radio( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -277,7 +298,10 @@ def test_playing_radio( @mock.patch("libsoundtouch.device.SoundTouchDevice.volume", side_effect=MockVolume) @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_get_volume_level( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -293,7 +317,10 @@ def test_get_volume_level( @mock.patch( "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusStandby ) - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_get_state_off( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -309,7 +336,10 @@ def test_get_state_off( @mock.patch( "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusPause ) - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_get_state_pause( self, mocked_soundtouch_device, mocked_status, mocked_volume ): @@ -325,7 +355,10 @@ def test_get_state_pause( "libsoundtouch.device.SoundTouchDevice.volume", side_effect=MockVolumeMuted ) @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_is_muted(self, mocked_soundtouch_device, mocked_status, mocked_volume): """Test device volume is muted.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) @@ -335,7 +368,7 @@ def test_is_muted(self, mocked_soundtouch_device, mocked_status, mocked_volume): all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] assert all_devices[0].is_volume_muted is True - @mock.patch("libsoundtouch.soundtouch_device") + @mock.patch("homeassistant.components.soundtouch.media_player.soundtouch_device") def test_media_commands(self, mocked_soundtouch_device): """Test supported media commands.""" soundtouch.setup_platform(self.hass, default_component(), mock.MagicMock()) @@ -346,7 +379,10 @@ def test_media_commands(self, mocked_soundtouch_device): @mock.patch("libsoundtouch.device.SoundTouchDevice.power_off") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_should_turn_off( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_power_off ): @@ -362,7 +398,10 @@ def test_should_turn_off( @mock.patch("libsoundtouch.device.SoundTouchDevice.power_on") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_should_turn_on( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_power_on ): @@ -378,7 +417,10 @@ def test_should_turn_on( @mock.patch("libsoundtouch.device.SoundTouchDevice.volume_up") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_volume_up( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_volume_up ): @@ -394,7 +436,10 @@ def test_volume_up( @mock.patch("libsoundtouch.device.SoundTouchDevice.volume_down") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_volume_down( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_volume_down ): @@ -410,7 +455,10 @@ def test_volume_down( @mock.patch("libsoundtouch.device.SoundTouchDevice.set_volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_set_volume_level( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_set_volume ): @@ -426,7 +474,10 @@ def test_set_volume_level( @mock.patch("libsoundtouch.device.SoundTouchDevice.mute") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_mute( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_mute ): @@ -442,7 +493,10 @@ def test_mute( @mock.patch("libsoundtouch.device.SoundTouchDevice.play") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_play( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play ): @@ -458,7 +512,10 @@ def test_play( @mock.patch("libsoundtouch.device.SoundTouchDevice.pause") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_pause( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_pause ): @@ -474,7 +531,10 @@ def test_pause( @mock.patch("libsoundtouch.device.SoundTouchDevice.play_pause") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_play_pause_play( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play_pause ): @@ -491,7 +551,10 @@ def test_play_pause_play( @mock.patch("libsoundtouch.device.SoundTouchDevice.next_track") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_next_previous_track( self, mocked_soundtouch_device, @@ -519,7 +582,10 @@ def test_next_previous_track( ) @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_play_media( self, mocked_soundtouch_device, @@ -544,7 +610,10 @@ def test_play_media( @mock.patch("libsoundtouch.device.SoundTouchDevice.play_url") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_play_media_url( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_play_url ): @@ -560,7 +629,10 @@ def test_play_media_url( @mock.patch("libsoundtouch.device.SoundTouchDevice.create_zone") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_play_everywhere( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_create_zone ): @@ -605,7 +677,10 @@ def test_play_everywhere( @mock.patch("libsoundtouch.device.SoundTouchDevice.create_zone") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_create_zone( self, mocked_soundtouch_device, mocked_status, mocked_volume, mocked_create_zone ): @@ -649,7 +724,10 @@ def test_create_zone( @mock.patch("libsoundtouch.device.SoundTouchDevice.remove_zone_slave") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_remove_zone_slave( self, mocked_soundtouch_device, @@ -697,7 +775,10 @@ def test_remove_zone_slave( @mock.patch("libsoundtouch.device.SoundTouchDevice.add_zone_slave") @mock.patch("libsoundtouch.device.SoundTouchDevice.volume") @mock.patch("libsoundtouch.device.SoundTouchDevice.status") - @mock.patch("libsoundtouch.soundtouch_device", side_effect=_mock_soundtouch_device) + @mock.patch( + "homeassistant.components.soundtouch.media_player.soundtouch_device", + side_effect=_mock_soundtouch_device, + ) def test_add_zone_slave( self, mocked_soundtouch_device, From 31c71989e99531b410d2c2118fcdd132c6c26e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 6 Dec 2019 22:53:26 +0200 Subject: [PATCH 2160/3953] Huawei LTE device tracker fixes (#29551) * Include MAC address in device state attributes for absent devices too * Use MAC address as default name whether device is connected or not * Fix initialization of known entities Closes https://github.com/home-assistant/home-assistant/issues/29354 --- .../components/huawei_lte/device_tracker.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 29d7f437b5f54a..f5f834fa18674a 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -2,7 +2,7 @@ import logging import re -from typing import Any, Dict, Set +from typing import Any, Dict, List, Optional, Set import attr from stringcase import snakecase @@ -40,13 +40,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Initialize already tracked entities tracked: Set[str] = set() registry = await entity_registry.async_get_registry(hass) + known_entities: List[HuaweiLteScannerEntity] = [] for entity in registry.entities.values(): if ( entity.domain == DEVICE_TRACKER_DOMAIN and entity.config_entry_id == config_entry.entry_id ): tracked.add(entity.unique_id) - async_add_new_entities(hass, router.url, async_add_entities, tracked, True) + known_entities.append( + HuaweiLteScannerEntity(router, entity.unique_id.partition("-")[2]) + ) + async_add_entities(known_entities, True) # Tell parent router to poll hosts list to gather new devices router.subscriptions[KEY_WLAN_HOST_LIST].add(_DEVICE_SCAN) @@ -66,13 +70,8 @@ async def _async_maybe_add_new_entities(url: str) -> None: async_add_new_entities(hass, router.url, async_add_entities, tracked) -def async_add_new_entities( - hass, router_url, async_add_entities, tracked, included: bool = False -): - """Add new entities. - - :param included: if True, setup only items in tracked, and vice versa - """ +def async_add_new_entities(hass, router_url, async_add_entities, tracked): + """Add new entities that are not already being tracked.""" router = hass.data[DOMAIN].routers[router_url] try: hosts = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] @@ -83,8 +82,7 @@ def async_add_new_entities( new_entities = [] for host in (x for x in hosts if x.get("MacAddress")): entity = HuaweiLteScannerEntity(router, host["MacAddress"]) - tracking = entity.unique_id in tracked - if tracking != included: + if entity.unique_id in tracked: continue tracked.add(entity.unique_id) new_entities.append(entity) @@ -113,12 +111,16 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): mac: str = attr.ib() _is_connected: bool = attr.ib(init=False, default=False) - _name: str = attr.ib(init=False, default="device") + _hostname: Optional[str] = attr.ib(init=False, default=None) _device_state_attributes: Dict[str, Any] = attr.ib(init=False, factory=dict) + def __attrs_post_init__(self): + """Initialize internal state.""" + self._device_state_attributes["mac_address"] = self.mac + @property def _entity_name(self) -> str: - return self._name + return self._hostname or self.mac @property def _device_unique_id(self) -> str: @@ -145,8 +147,7 @@ async def async_update(self) -> None: host = next((x for x in hosts if x.get("MacAddress") == self.mac), None) self._is_connected = host is not None if self._is_connected: - # HostName may be present with explicit None value - self._name = host.get("HostName") or self.mac + self._hostname = host.get("HostName") self._device_state_attributes = { _better_snakecase(k): v for k, v in host.items() if k != "HostName" } From fb66a6cf81666ab2a7a25017d7faec36d6486087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 6 Dec 2019 22:58:32 +0200 Subject: [PATCH 2161/3953] Treat BaseException as over-general (#29573) To follow pylint's defaults. --- homeassistant/components/whois/sensor.py | 4 ++-- pylintrc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 3c78d80ba92442..dc9da1100f0120 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "WHOIS lookup for %s didn't contain an expiration date", domain ) return - except whois.BaseException as ex: + except whois.BaseException as ex: # pylint: disable=broad-except _LOGGER.error("Exception %s occurred during WHOIS lookup for %s", ex, domain) return @@ -96,7 +96,7 @@ def update(self): """Get the current WHOIS data for the domain.""" try: response = self.whois(self._domain) - except whois.BaseException as ex: + except whois.BaseException as ex: # pylint: disable=broad-except _LOGGER.error("Exception %s occurred during WHOIS lookup", ex) self._empty_state_and_attributes() return diff --git a/pylintrc b/pylintrc index 44659ddb376996..3235d583865a8b 100644 --- a/pylintrc +++ b/pylintrc @@ -64,4 +64,4 @@ ignored-classes=_CountingAttr expected-line-ending-format=LF [EXCEPTIONS] -overgeneral-exceptions=Exception,HomeAssistantError +overgeneral-exceptions=BaseException,Exception,HomeAssistantError From 1ee80576621933e3264e9c9fb8cbd12ba61e175c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 22:12:46 +0100 Subject: [PATCH 2162/3953] Move imports to top for zha (#29555) * Move imports to top for zha * Move back some imports, add annotation for disabling import-outside-toplevel * Move import config_flow before import api --- homeassistant/components/zha/__init__.py | 2 +- homeassistant/components/zha/core/channels/__init__.py | 2 +- homeassistant/components/zha/core/channels/hvac.py | 2 +- .../components/zha/core/channels/manufacturerspecific.py | 1 - homeassistant/components/zha/core/channels/security.py | 2 +- homeassistant/components/zha/core/device.py | 2 +- homeassistant/components/zha/core/store.py | 3 +-- homeassistant/components/zha/device_trigger.py | 2 +- 8 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index ecd27c48839fab..07eb3c53a95d12 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -100,7 +100,7 @@ async def async_setup_entry(hass, config_entry): if config.get(CONF_ENABLE_QUIRKS, True): # needs to be done here so that the ZHA module is finished loading # before zhaquirks is imported - import zhaquirks # noqa: F401 pylint: disable=unused-import + import zhaquirks # noqa: F401 pylint: disable=unused-import, import-outside-toplevel zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 29cecb7784e9a1..2d24c4ce045a15 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -394,7 +394,7 @@ def cluster_command(self, tsn, command_id, args): ) -# pylint: disable=wrong-import-position +# pylint: disable=wrong-import-position, import-outside-toplevel from . import closures # noqa: F401 from . import general # noqa: F401 from . import homeautomation # noqa: F401 diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 14d982ab1e8e9f..db4745d51c382b 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -6,6 +6,7 @@ """ import logging +from zigpy.exceptions import DeliveryError import zigpy.zcl.clusters.hvac as hvac from homeassistant.core import callback @@ -35,7 +36,6 @@ class FanChannel(ZigbeeChannel): async def async_set_speed(self, value) -> None: """Set the speed of the fan.""" - from zigpy.exceptions import DeliveryError try: await self.cluster.write_attributes({"fan_mode": value}) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 31dd5cd63d14ea..39f45f6c4a2791 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -18,7 +18,6 @@ SIGNAL_ATTR_UPDATED, ) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index e4840dae86dabc..69e4ea1a27ad68 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -6,6 +6,7 @@ """ import logging +from zigpy.exceptions import DeliveryError import zigpy.zcl.clusters.security as security from homeassistant.core import callback @@ -149,7 +150,6 @@ async def async_configure(self): if self._zha_device.manufacturer == "LUMI": self.debug("finished IASZoneChannel configuration") return - from zigpy.exceptions import DeliveryError self.debug("started IASZoneChannel configuration") diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e5d1678ad6fa31..9ace477d6212e5 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -11,8 +11,8 @@ import time import zigpy.exceptions -import zigpy.quirks from zigpy.profiles import zha, zll +import zigpy.quirks from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index bcc9b2a42d4e42..46fef76b6564ba 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -2,8 +2,7 @@ # pylint: disable=unused-import from collections import OrderedDict import logging -from typing import MutableMapping -from typing import cast +from typing import MutableMapping, cast import attr diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index cdd62b11d1e544..b7c46e5a40ae9f 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -2,11 +2,11 @@ import voluptuous as vol import homeassistant.components.automation.event as event +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from . import DOMAIN from .core.helpers import async_get_zha_device From 23fb36407614fd5baff41bd8793a64a6a4453910 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 6 Dec 2019 22:13:43 +0100 Subject: [PATCH 2163/3953] Move imports to top for openuv (#29541) * Move imports to top for openuv * Renamed mock_pyopenuv_ to MockClient in test_config_flow --- homeassistant/components/openuv/__init__.py | 8 +++----- homeassistant/components/openuv/config_flow.py | 5 ++--- tests/components/openuv/test_config_flow.py | 9 +++++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 16b7a50a4aeaf7..167fcdcd0e610e 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -1,7 +1,9 @@ """Support for UV data from openuv.io.""" -import logging import asyncio +import logging +from pyopenuv import Client +from pyopenuv.errors import OpenUvError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -164,8 +166,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up OpenUV as config entry.""" - from pyopenuv import Client - from pyopenuv.errors import OpenUvError _verify_domain_control = verify_domain_control(hass, DOMAIN) @@ -255,7 +255,6 @@ def __init__(self, client, binary_sensor_conditions, sensor_conditions): async def async_update_protection_data(self): """Update binary sensor (protection window) data.""" - from pyopenuv.errors import OpenUvError if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: try: @@ -268,7 +267,6 @@ async def async_update_protection_data(self): async def async_update_uv_index_data(self): """Update sensor (uv index, etc) data.""" - from pyopenuv.errors import OpenUvError if any(c in self.sensor_conditions for c in SENSORS): try: diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 40ec2abf2fe9d1..7dd8ed45a79657 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure the OpenUV component.""" - +from pyopenuv import Client +from pyopenuv.errors import OpenUvError import voluptuous as vol from homeassistant import config_entries @@ -59,8 +60,6 @@ async def async_step_import(self, import_config): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from pyopenuv import Client - from pyopenuv.errors import OpenUvError if not user_input: return await self._show_form() diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index c57e22e44e2ef1..43dd5924a72146 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -1,6 +1,7 @@ """Define tests for the OpenUV config flow.""" import pytest from pyopenuv.errors import OpenUvError +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.openuv import DOMAIN, config_flow @@ -11,7 +12,7 @@ CONF_LONGITUDE, ) -from tests.common import MockConfigEntry, MockDependency, mock_coro +from tests.common import MockConfigEntry, mock_coro @pytest.fixture @@ -23,9 +24,9 @@ def uv_index_response(): @pytest.fixture def mock_pyopenuv(uv_index_response): """Mock the pyopenuv library.""" - with MockDependency("pyopenuv") as mock_pyopenuv_: - mock_pyopenuv_.Client().uv_index.return_value = uv_index_response - yield mock_pyopenuv_ + with patch("homeassistant.components.openuv.config_flow.Client") as MockClient: + MockClient().uv_index.return_value = uv_index_response + yield MockClient async def test_duplicate_error(hass): From b4c95421d3c5cdde6323702aa4b6a7a5634ef451 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 7 Dec 2019 00:32:13 +0000 Subject: [PATCH 2164/3953] [ci skip] Translation update --- .../components/alarm_control_panel/.translations/pt-BR.json | 5 +++++ homeassistant/components/plex/.translations/pt-BR.json | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json index 156ede8851bc33..f72ae0e820ed86 100644 --- a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json +++ b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json @@ -6,6 +6,11 @@ "arm_night": "Armar {entity_name} noite", "disarm": "Desarmar {entity_name}", "trigger": "Disparar {entidade_nome}" + }, + "trigger_type": { + "armed_night": "{entity_name} armadado para noite", + "disarmed": "{entity_name} desarmado", + "triggered": "{entity_name} acionado" } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pt-BR.json b/homeassistant/components/plex/.translations/pt-BR.json index 9a759e309c2a10..be97c7fdcb728d 100644 --- a/homeassistant/components/plex/.translations/pt-BR.json +++ b/homeassistant/components/plex/.translations/pt-BR.json @@ -1,4 +1,9 @@ { + "config": { + "abort": { + "non-interactive": "Importa\u00e7\u00e3o n\u00e3o interativa" + } + }, "options": { "step": { "plex_mp_settings": { From e2adfc3979901e4e1b4c5e2acf0a9c0b4d99a2e1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 7 Dec 2019 07:25:15 +0100 Subject: [PATCH 2165/3953] Move imports to top for onboarding (#29542) --- homeassistant/components/onboarding/__init__.py | 7 +++---- homeassistant/components/onboarding/views.py | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 5527f5f2abe85a..bedfa703a9b0d2 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -1,9 +1,10 @@ """Support to help onboard new users.""" from homeassistant.core import callback -from homeassistant.loader import bind_hass from homeassistant.helpers.storage import Store +from homeassistant.loader import bind_hass -from .const import DOMAIN, STEP_USER, STEPS, STEP_INTEGRATION, STEP_CORE_CONFIG +from . import views +from .const import DOMAIN, STEP_CORE_CONFIG, STEP_INTEGRATION, STEP_USER, STEPS STORAGE_KEY = DOMAIN STORAGE_VERSION = 3 @@ -64,8 +65,6 @@ async def async_setup(hass, config): hass.data[DOMAIN] = data - from . import views - await views.async_setup(hass, data, store) return True diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 2e79393fe4236d..8eac430ac49687 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -8,12 +8,12 @@ from homeassistant.core import callback from .const import ( + DEFAULT_AREAS, DOMAIN, + STEP_CORE_CONFIG, + STEP_INTEGRATION, STEP_USER, STEPS, - DEFAULT_AREAS, - STEP_INTEGRATION, - STEP_CORE_CONFIG, ) From 977f51a9e45e7afed3230d3a2e197b3844ca0e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 7 Dec 2019 08:34:46 +0200 Subject: [PATCH 2166/3953] Update Travis dist to bionic (#29575) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c9638b02a2f6fd..e35787bb1e818f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ sudo: false -dist: xenial +dist: bionic addons: apt: sources: From 9d7799c0af4de52e24fa1c40bfffb5d25ecc01b0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 7 Dec 2019 13:36:55 +0530 Subject: [PATCH 2167/3953] Upgrade pyyaml to 5.2.0 (#29586) --- 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 372ff66bffd7e9..07b9b4c80e8b30 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ netdisco==2.6.0 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.1.2 +pyyaml==5.2.0 requests==2.22.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.11 diff --git a/requirements_all.txt b/requirements_all.txt index f7e608cc1fdfb7..094728834df874 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -13,7 +13,7 @@ cryptography==2.8 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.1.2 +pyyaml==5.2.0 requests==2.22.0 ruamel.yaml==0.15.100 voluptuous==0.11.7 diff --git a/setup.py b/setup.py index c153ad1f763260..73cc893cbe2244 100755 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ "pip>=8.0.3", "python-slugify==4.0.0", "pytz>=2019.03", - "pyyaml==5.1.2", + "pyyaml==5.2.0", "requests==2.22.0", "ruamel.yaml==0.15.100", "voluptuous==0.11.7", From ee657f3c2ff84b8649cc3fa32e923ab11ee67e37 Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Sat, 7 Dec 2019 12:09:43 +0100 Subject: [PATCH 2168/3953] Add service to reboot the Freebox (#29525) --- homeassistant/components/freebox/__init__.py | 6 ++++++ homeassistant/components/freebox/services.yaml | 5 +++++ 2 files changed, 11 insertions(+) create mode 100644 homeassistant/components/freebox/services.yaml diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 64c59c3ef2a809..96874ddeb94284 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -71,6 +71,12 @@ async def async_setup_freebox(hass, config, host, port): else: hass.data[DATA_FREEBOX] = fbx + async def async_freebox_reboot(call): + """Handle reboot service call.""" + await fbx.system.reboot() + + hass.services.async_register(DOMAIN, "reboot", async_freebox_reboot) + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) hass.async_create_task( async_load_platform(hass, "device_tracker", DOMAIN, {}, config) diff --git a/homeassistant/components/freebox/services.yaml b/homeassistant/components/freebox/services.yaml new file mode 100644 index 00000000000000..be7afa60562bf5 --- /dev/null +++ b/homeassistant/components/freebox/services.yaml @@ -0,0 +1,5 @@ +# Freebox service entries description. + +reboot: + # Description of the service + description: Reboots the Freebox. From d838a56c1d0b4f04a522fa846f7752a5b58b4004 Mon Sep 17 00:00:00 2001 From: butako <1004460+butako@users.noreply.github.com> Date: Sat, 7 Dec 2019 15:14:09 +0000 Subject: [PATCH 2169/3953] Improve Tahoma Velux support (#27920) * Improved Velux support. Added Velux Solar Roller Blind. Fixed Velux Integra Window. * fix indentation * black formatting * added new devices in correct sorted order --- homeassistant/components/tahoma/__init__.py | 1 + homeassistant/components/tahoma/cover.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) mode change 100644 => 100755 homeassistant/components/tahoma/cover.py diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 02cdba5c46acea..640cc6418d0f01 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -46,6 +46,7 @@ "io:SomfyBasicContactIOSystemSensor": "sensor", "io:SomfyContactIOSystemSensor": "sensor", "io:VerticalExteriorAwningIOComponent": "cover", + "io:VerticalInteriorBlindVeluxIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", "io:GarageOpenerIOComponent": "cover", "io:DiscreteGarageOpenerIOComponent": "cover", diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py old mode 100644 new mode 100755 index 6c5dcbd807c4e3..e11c2f4cdf59ce --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -35,6 +35,7 @@ "io:RollerShutterVeluxIOComponent": DEVICE_CLASS_SHUTTER, "io:RollerShutterWithLowSpeedManagementIOComponent": DEVICE_CLASS_SHUTTER, "io:VerticalExteriorAwningIOComponent": DEVICE_CLASS_AWNING, + "io:VerticalInteriorBlindVeluxIOComponent": DEVICE_CLASS_BLIND, "io:WindowOpenerVeluxIOComponent": DEVICE_CLASS_WINDOW, "io:GarageOpenerIOComponent": DEVICE_CLASS_GARAGE, "io:DiscreteGarageOpenerIOComponent": DEVICE_CLASS_GARAGE, @@ -163,10 +164,15 @@ def current_cover_position(self): def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" + if self.tahoma_device.type == "io:WindowOpenerVeluxIOComponent": + command = "setClosure" + else: + command = "setPosition" + if self.tahoma_device.type == HORIZONTAL_AWNING: - self.apply_action("setPosition", kwargs.get(ATTR_POSITION, 0)) + self.apply_action(command, kwargs.get(ATTR_POSITION, 0)) else: - self.apply_action("setPosition", 100 - kwargs.get(ATTR_POSITION, 0)) + self.apply_action(command, 100 - kwargs.get(ATTR_POSITION, 0)) @property def is_closed(self): @@ -235,6 +241,8 @@ def stop_cover(self, **kwargs): HORIZONTAL_AWNING, "io:RollerShutterGenericIOComponent", "io:VerticalExteriorAwningIOComponent", + "io:VerticalInteriorBlindVeluxIOComponent", + "io:WindowOpenerVeluxIOComponent", ): self.apply_action("stop") else: From ccb0fd5e32d8ed5a430dd42e7b24bc980facc881 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 7 Dec 2019 15:17:30 -0500 Subject: [PATCH 2170/3953] Register automation.reload service as an admin service. (#29582) * homeassistant/components/automation/__init__.py * Register automation.reload as an admin service. --- .../components/automation/__init__.py | 9 +++++-- tests/components/automation/common.py | 6 +++-- tests/components/automation/test_init.py | 25 +++++++++++-------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 3863ab0c88df54..2b775e3a602f41 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -27,6 +27,7 @@ from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass @@ -179,8 +180,12 @@ async def reload_service_handler(service_call): DOMAIN, SERVICE_TRIGGER, trigger_service_handler, schema=TRIGGER_SERVICE_SCHEMA ) - hass.services.async_register( - DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA + async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, ) hass.services.async_register( diff --git a/tests/components/automation/common.py b/tests/components/automation/common.py index c7aa8f1eced95c..729a6bb7212946 100644 --- a/tests/components/automation/common.py +++ b/tests/components/automation/common.py @@ -44,6 +44,8 @@ async def async_trigger(hass, entity_id=ENTITY_MATCH_ALL): @bind_hass -async def async_reload(hass): +async def async_reload(hass, context=None): """Reload the automation from config.""" - await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + await hass.services.async_call( + DOMAIN, SERVICE_RELOAD, blocking=True, context=context + ) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index a0573ce7c1b88e..d5498c04814b5f 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,28 +1,28 @@ """The tests for the automation component.""" from datetime import timedelta -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest -from homeassistant.core import State, CoreState, Context -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.const import ( - ATTR_NAME, ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, - EVENT_HOMEASSISTANT_START, + ATTR_NAME, EVENT_AUTOMATION_TRIGGERED, + EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, ) -from homeassistant.exceptions import HomeAssistantError +from homeassistant.core import Context, CoreState, State +from homeassistant.exceptions import HomeAssistantError, Unauthorized +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, async_fire_time_changed, - mock_restore_cache, async_mock_service, + mock_restore_cache, ) from tests.components.automation import common @@ -445,7 +445,7 @@ async def test_services(hass, calls): assert automation.is_on(hass, entity_id) -async def test_reload_config_service(hass, calls): +async def test_reload_config_service(hass, calls, hass_admin_user, hass_read_only_user): """Test the reload config service.""" assert await async_setup_component( hass, @@ -488,7 +488,10 @@ async def test_reload_config_service(hass, calls): }, ): with patch("homeassistant.config.find_config_file", return_value=""): - await common.async_reload(hass) + with pytest.raises(Unauthorized): + await common.async_reload(hass, Context(user_id=hass_read_only_user.id)) + await hass.async_block_till_done() + await common.async_reload(hass, Context(user_id=hass_admin_user.id)) await hass.async_block_till_done() # De-flake ?! await hass.async_block_till_done() From 256056430ea4a06f46312403cc6eb41b69673c0c Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 7 Dec 2019 15:24:56 -0500 Subject: [PATCH 2171/3953] Add input_datetime reload service. (#29581) * Add input_datetime reload service. * Add reload service test. --- .../components/input_datetime/__init__.py | 63 +++++++++++----- .../components/input_datetime/services.yaml | 3 + tests/components/input_datetime/test_init.py | 71 ++++++++++++++++++- 3 files changed, 117 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 36180ed2bad18a..654f3547ad67f6 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -1,16 +1,22 @@ """Support to select a date and/or a time.""" -import logging import datetime +import logging import voluptuous as vol -from homeassistant.const import ATTR_DATE, ATTR_TIME, CONF_ICON, CONF_NAME +from homeassistant.const import ( + ATTR_DATE, + ATTR_TIME, + CONF_ICON, + CONF_NAME, + SERVICE_RELOAD, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service from homeassistant.util import dt as dt_util - _LOGGER = logging.getLogger(__name__) DOMAIN = "input_datetime" @@ -52,26 +58,31 @@ def has_date_or_time(conf): }, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input datetime.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - entities = [] - - for object_id, cfg in config[DOMAIN].items(): - name = cfg.get(CONF_NAME) - has_time = cfg.get(CONF_HAS_TIME) - has_date = cfg.get(CONF_HAS_DATE) - icon = cfg.get(CONF_ICON) - initial = cfg.get(CONF_INITIAL) - entities.append( - InputDatetime(object_id, name, has_date, has_time, icon, initial) - ) + entities = await _async_process_config(config) - if not entities: - return False + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) async def async_set_datetime_service(entity, call): """Handle a call to the input datetime 'set datetime' service.""" @@ -108,10 +119,28 @@ async def async_set_datetime_service(entity, call): async_set_datetime_service, ) - await component.async_add_entities(entities) + if entities: + await component.async_add_entities(entities) return True +async def _async_process_config(config): + """Process config and create list of entities.""" + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + name = cfg.get(CONF_NAME) + has_time = cfg.get(CONF_HAS_TIME) + has_date = cfg.get(CONF_HAS_DATE) + icon = cfg.get(CONF_ICON) + initial = cfg.get(CONF_INITIAL) + entities.append( + InputDatetime(object_id, name, has_date, has_time, icon, initial) + ) + + return entities + + class InputDatetime(RestoreEntity): """Representation of a datetime input.""" diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml index 8a40be47acdc7a..472bd1b83b9888 100644 --- a/homeassistant/components/input_datetime/services.yaml +++ b/homeassistant/components/input_datetime/services.yaml @@ -9,3 +9,6 @@ set_datetime: example: '"time": "05:30:00"'} datetime: {description: The target date & time the entity should be set to. Do not use with date or time., example: '"datetime": "2019-04-22 05:30:00"'} + +reload: + description: Reload the input_datetime configuration. diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 2ddeddbefacf81..427433e22d2ffa 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -2,20 +2,23 @@ # pylint: disable=protected-access import asyncio import datetime +from unittest.mock import patch import pytest import voluptuous as vol -from homeassistant.core import CoreState, State, Context -from homeassistant.setup import async_setup_component from homeassistant.components.input_datetime import ( - DOMAIN, ATTR_DATE, ATTR_DATETIME, ATTR_TIME, + DOMAIN, + SERVICE_RELOAD, SERVICE_SET_DATETIME, ) from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, CoreState, State +from homeassistant.exceptions import Unauthorized +from homeassistant.setup import async_setup_component from tests.common import mock_restore_cache @@ -310,3 +313,65 @@ async def test_input_datetime_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "dt1": {"has_time": False, "has_date": True, "initial": "2019-1-1"}, + } + }, + ) + + assert count_start + 1 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_datetime.dt1") + state_2 = hass.states.get("input_datetime.dt2") + + dt_obj = datetime.datetime(2019, 1, 1, 0, 0) + assert state_1 is not None + assert state_2 is None + assert str(dt_obj.date()) == state_1.state + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "dt1": {"has_time": True, "has_date": False, "initial": "23:32"}, + "dt2": {"has_time": True, "has_date": True}, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_datetime.dt1") + state_2 = hass.states.get("input_datetime.dt2") + + dt_obj = datetime.datetime(2019, 1, 1, 23, 32) + assert state_1 is not None + assert state_2 is not None + assert str(dt_obj.time()) == state_1.state + assert str(datetime.datetime(1970, 1, 1, 0, 0)) == state_2.state From e360b1265f988ad192e782c981f96722a637a9e8 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 7 Dec 2019 15:26:06 -0500 Subject: [PATCH 2172/3953] Add input_number.reload admin service. (#29584) * Add input_number reload service. * Add test. * Allow platform setup without entities. We can reload and add entities later. --- .../components/input_number/__init__.py | 61 +++++++++++++------ .../components/input_number/services.yaml | 2 + tests/components/input_number/test_init.py | 61 ++++++++++++++++++- 3 files changed, 105 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 77625ffa7f8e2d..a4438020886485 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -3,16 +3,18 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_MODE, + ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, - CONF_NAME, CONF_MODE, + CONF_NAME, + SERVICE_RELOAD, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service _LOGGER = logging.getLogger(__name__) @@ -77,12 +79,49 @@ def _cv_input_number(cfg): required=True, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input slider.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(config) + + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + component.async_register_entity_service( + SERVICE_SET_VALUE, + {vol.Required(ATTR_VALUE): vol.Coerce(float)}, + "async_set_value", + ) + + component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") + + component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") + + if entities: + await component.async_add_entities(entities) + return True + + +async def _async_process_config(config): + """Process config and create list of entities.""" entities = [] for object_id, cfg in config[DOMAIN].items(): @@ -101,21 +140,7 @@ async def async_setup(hass, config): ) ) - if not entities: - return False - - component.async_register_entity_service( - SERVICE_SET_VALUE, - {vol.Required(ATTR_VALUE): vol.Coerce(float)}, - "async_set_value", - ) - - component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") - - component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") - - await component.async_add_entities(entities) - return True + return entities class InputNumber(RestoreEntity): diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml index 650abc056a97c2..9cd1b913ccd328 100644 --- a/homeassistant/components/input_number/services.yaml +++ b/homeassistant/components/input_number/services.yaml @@ -14,3 +14,5 @@ set_value: entity_id: {description: Entity id of the input number to set the new value., example: input_number.threshold} value: {description: The target value the entity should be set to., example: 42} +reload: + description: Reload the input_number configuration. diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 02d59c367c97a8..a3b46212daf556 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -1,16 +1,21 @@ """The tests for the Input number component.""" # pylint: disable=protected-access import asyncio +from unittest.mock import patch + +import pytest -from homeassistant.core import CoreState, State, Context from homeassistant.components.input_number import ( ATTR_VALUE, DOMAIN, SERVICE_DECREMENT, SERVICE_INCREMENT, + SERVICE_RELOAD, SERVICE_SET_VALUE, ) from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, CoreState, State +from homeassistant.exceptions import Unauthorized from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component @@ -254,3 +259,57 @@ async def test_input_number_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {"test_1": {"initial": 50, "min": 0, "max": 51}}} + ) + + assert count_start + 1 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_number.test_1") + state_2 = hass.states.get("input_number.test_2") + + assert state_1 is not None + assert state_2 is None + assert 50 == float(state_1.state) + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_1": {"initial": 40, "min": 0, "max": 51}, + "test_2": {"initial": 20, "min": 10, "max": 30}, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_number.test_1") + state_2 = hass.states.get("input_number.test_2") + + assert state_1 is not None + assert state_2 is not None + assert 40 == float(state_1.state) + assert 20 == float(state_2.state) From 0cdc315038e9621326c72ec79879cac2fb5ac2ad Mon Sep 17 00:00:00 2001 From: Marius <33354141+Mariusthvdb@users.noreply.github.com> Date: Sun, 8 Dec 2019 06:25:39 +0100 Subject: [PATCH 2173/3953] change icon for partly-cloudy-night (#29601) to the available mdi:weather-night-partly-cloudy --- homeassistant/components/darksky/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index cd8417e3e8467b..82aaccc9590aaf 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -385,7 +385,7 @@ ], "partly-cloudy-night": [ "/static/images/darksky/weather-cloudy.svg", - "mdi:weather-partly-cloudy", + "mdi:weather-night-partly-cloudy", ], } From 7f4baab3f6f4a43991cedb47f808d71edb727a67 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sun, 8 Dec 2019 09:20:13 +0100 Subject: [PATCH 2174/3953] Add additional Magic Cube Model (#29598) --- homeassistant/components/deconz/device_trigger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index b6691548b877fb..d057de23d02b20 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -147,6 +147,7 @@ } AQARA_CUBE_MODEL = "lumi.sensor_cube" +AQARA_CUBE_MODEL_ALT1 = "lumi.sensor_cube.aqgl01" AQARA_CUBE = { (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_2): 6002, (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_3): 3002, @@ -262,6 +263,7 @@ TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE, TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER, AQARA_CUBE_MODEL: AQARA_CUBE, + AQARA_CUBE_MODEL_ALT1: AQARA_CUBE, AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_WXKG02LM, AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, From cc9589cff2f8447674f39d06a7322abd6aa16c27 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 8 Dec 2019 09:26:31 +0100 Subject: [PATCH 2175/3953] Add Elgato Key Light integration (#29592) * Add Elgato Key Light integration * Remove passing in of hass loop * Tweaks a comment * Tweaks a function name * Ensure domain namespace in data exists in entry setup --- CODEOWNERS | 1 + .../components/elgato/.translations/en.json | 27 ++ homeassistant/components/elgato/__init__.py | 55 ++++ .../components/elgato/config_flow.py | 146 +++++++++++ homeassistant/components/elgato/const.py | 17 ++ homeassistant/components/elgato/light.py | 158 ++++++++++++ homeassistant/components/elgato/manifest.json | 10 + homeassistant/components/elgato/strings.json | 27 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 3 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/elgato/__init__.py | 49 ++++ tests/components/elgato/test_config_flow.py | 238 ++++++++++++++++++ tests/components/elgato/test_init.py | 33 +++ tests/components/elgato/test_light.py | 104 ++++++++ tests/fixtures/elgato/info.json | 9 + tests/fixtures/elgato/state.json | 10 + 18 files changed, 894 insertions(+) create mode 100644 homeassistant/components/elgato/.translations/en.json create mode 100644 homeassistant/components/elgato/__init__.py create mode 100644 homeassistant/components/elgato/config_flow.py create mode 100644 homeassistant/components/elgato/const.py create mode 100644 homeassistant/components/elgato/light.py create mode 100644 homeassistant/components/elgato/manifest.json create mode 100644 homeassistant/components/elgato/strings.json create mode 100644 tests/components/elgato/__init__.py create mode 100644 tests/components/elgato/test_config_flow.py create mode 100644 tests/components/elgato/test_init.py create mode 100644 tests/components/elgato/test_light.py create mode 100644 tests/fixtures/elgato/info.json create mode 100644 tests/fixtures/elgato/state.json diff --git a/CODEOWNERS b/CODEOWNERS index 8078aadf6419ac..7f5ff17a043980 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -85,6 +85,7 @@ homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 +homeassistant/components/elgato/* @frenck homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 homeassistant/components/emulated_hue/* @NobleKangaroo diff --git a/homeassistant/components/elgato/.translations/en.json b/homeassistant/components/elgato/.translations/en.json new file mode 100644 index 00000000000000..03c46f02efcab3 --- /dev/null +++ b/homeassistant/components/elgato/.translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Elgato Key Light", + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "title": "Link your Elgato Key Light", + "description": "Set up your Elgato Key Light to integrate with Home Assistant.", + "data": { + "host": "Host or IP address", + "port": "Port number" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?", + "title": "Discovered Elgato Key Light device" + } + }, + "error": { + "connection_error": "Failed to connect to Elgato Key Light device." + }, + "abort": { + "already_configured": "This Elgato Key Light device is already configured.", + "connection_error": "Failed to connect to Elgato Key Light device." + } + } +} diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py new file mode 100644 index 00000000000000..993748033b53ab --- /dev/null +++ b/homeassistant/components/elgato/__init__.py @@ -0,0 +1,55 @@ +"""Support for Elgato Key Lights.""" +import logging + +from elgato import Elgato, ElgatoConnectionError + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType + +from .const import DATA_ELGATO_CLIENT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Elgato Key Light components.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Elgato Key Light from a config entry.""" + session = async_get_clientsession(hass) + elgato = Elgato(entry.data[CONF_HOST], port=entry.data[CONF_PORT], session=session,) + + # Ensure we can connect to it + try: + await elgato.info() + except ElgatoConnectionError as exception: + raise ConfigEntryNotReady from exception + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {DATA_ELGATO_CLIENT: elgato} + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Elgato Key Light config entry.""" + # Unload entities for this entry/device. + await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) + + # Cleanup + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + + return True diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py new file mode 100644 index 00000000000000..1d14fca18d2dcb --- /dev/null +++ b/homeassistant/components/elgato/config_flow.py @@ -0,0 +1,146 @@ +"""Config flow to configure the Elgato Key Light integration.""" +import logging +from typing import Any, Dict, Optional + +from elgato import Elgato, ElgatoError, Info +import voluptuous as vol + +from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers import ConfigType +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_SERIAL_NUMBER, DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a Elgato Key Light config flow.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + async def async_step_user( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_setup_form() + + try: + info = await self._get_elgato_info( + user_input[CONF_HOST], user_input[CONF_PORT] + ) + except ElgatoError: + return self._show_setup_form({"base": "connection_error"}) + + # Check if already configured + if await self._device_already_configured(info): + # This serial number is already configured + return self.async_abort(reason="already_configured") + + return self.async_create_entry( + title=info.serial_number, + data={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + CONF_SERIAL_NUMBER: info.serial_number, + }, + ) + + async def async_step_zeroconf( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle zeroconf discovery.""" + if user_input is None: + return self.async_abort(reason="connection_error") + + # Hostname is format: my-ke.local. + host = user_input["hostname"].rstrip(".") + try: + info = await self._get_elgato_info(host, user_input[CONF_PORT]) + except ElgatoError: + return self.async_abort(reason="connection_error") + + # Check if already configured + if await self._device_already_configured(info): + # This serial number is already configured + return self.async_abort(reason="already_configured") + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update( + { + CONF_HOST: host, + CONF_PORT: user_input[CONF_PORT], + CONF_SERIAL_NUMBER: info.serial_number, + "title_placeholders": {"serial_number": info.serial_number}, + } + ) + + # Prepare configuration flow + return self._show_confirm_dialog() + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + async def async_step_zeroconf_confirm( + self, user_input: ConfigType = None + ) -> Dict[str, Any]: + """Handle a flow initiated by zeroconf.""" + if user_input is None: + return self._show_confirm_dialog() + + try: + info = await self._get_elgato_info( + self.context.get(CONF_HOST), self.context.get(CONF_PORT) + ) + except ElgatoError: + return self.async_abort(reason="connection_error") + + # Check if already configured + if await self._device_already_configured(info): + # This serial number is already configured + return self.async_abort(reason="already_configured") + + return self.async_create_entry( + title=self.context.get(CONF_SERIAL_NUMBER), + data={ + CONF_HOST: self.context.get(CONF_HOST), + CONF_PORT: self.context.get(CONF_PORT), + CONF_SERIAL_NUMBER: self.context.get(CONF_SERIAL_NUMBER), + }, + ) + + def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=9123): int, + } + ), + errors=errors or {}, + ) + + def _show_confirm_dialog(self) -> Dict[str, Any]: + """Show the confirm dialog to the user.""" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + serial_number = self.context.get(CONF_SERIAL_NUMBER) + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={"serial_number": serial_number}, + ) + + async def _get_elgato_info(self, host: str, port: int) -> Info: + """Get device information from an Elgato Key Light device.""" + session = async_get_clientsession(self.hass) + elgato = Elgato(host, port=port, session=session,) + return await elgato.info() + + async def _device_already_configured(self, info: Info) -> bool: + """Return if a Elgato Key Light is already configured.""" + for entry in self._async_current_entries(): + if entry.data[CONF_SERIAL_NUMBER] == info.serial_number: + return True + return False diff --git a/homeassistant/components/elgato/const.py b/homeassistant/components/elgato/const.py new file mode 100644 index 00000000000000..4983608f899461 --- /dev/null +++ b/homeassistant/components/elgato/const.py @@ -0,0 +1,17 @@ +"""Constants for the Elgato Key Light integration.""" + +# Integration domain +DOMAIN = "elgato" + +# Hass data keys +DATA_ELGATO_CLIENT = "elgato_client" + +# Attributes +ATTR_IDENTIFIERS = "identifiers" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +ATTR_ON = "on" +ATTR_SOFTWARE_VERSION = "sw_version" +ATTR_TEMPERATURE = "temperature" + +CONF_SERIAL_NUMBER = "serial_number" diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py new file mode 100644 index 00000000000000..99bca1ba20e010 --- /dev/null +++ b/homeassistant/components/elgato/light.py @@ -0,0 +1,158 @@ +"""Support for LED lights.""" +from datetime import timedelta +import logging +from typing import Any, Callable, Dict, List, Optional + +from elgato import Elgato, ElgatoError, Info, State + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, + Light, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_NAME +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_ON, + ATTR_SOFTWARE_VERSION, + ATTR_TEMPERATURE, + DATA_ELGATO_CLIENT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 1 +SCAN_INTERVAL = timedelta(seconds=10) + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Elgato Key Light based on a config entry.""" + elgato: Elgato = hass.data[DOMAIN][entry.entry_id][DATA_ELGATO_CLIENT] + info = await elgato.info() + async_add_entities([ElgatoLight(entry.entry_id, elgato, info)], True) + + +class ElgatoLight(Light): + """Defines a Elgato Key Light.""" + + def __init__( + self, entry_id: str, elgato: Elgato, info: Info, + ): + """Initialize Elgato Key Light.""" + self._brightness: Optional[int] = None + self._info: Info = info + self._state: Optional[bool] = None + self._temperature: Optional[int] = None + self._available = True + self.elgato = elgato + + @property + def name(self) -> str: + """Return the name of the entity.""" + # Return the product name, if display name is not set + if not self._info.display_name: + return self._info.product_name + return self._info.display_name + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return self._info.serial_number + + @property + def brightness(self) -> Optional[int]: + """Return the brightness of this light between 1..255.""" + return self._brightness + + @property + def color_temp(self): + """Return the CT color value in mireds.""" + return self._temperature + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return 143 + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return 344 + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + + @property + def is_on(self) -> bool: + """Return the state of the light.""" + return bool(self._state) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + await self.async_turn_on(on=False) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + data = {} + + data[ATTR_ON] = True + if ATTR_ON in kwargs: + data[ATTR_ON] = kwargs[ATTR_ON] + + if ATTR_COLOR_TEMP in kwargs: + data[ATTR_TEMPERATURE] = kwargs[ATTR_COLOR_TEMP] + + if ATTR_BRIGHTNESS in kwargs: + data[ATTR_BRIGHTNESS] = round((kwargs[ATTR_BRIGHTNESS] / 255) * 100) + + try: + await self.elgato.light(**data) + except ElgatoError: + _LOGGER.error("An error occurred while updating the Elgato Key Light") + self._available = False + + async def async_update(self) -> None: + """Update Elgato entity.""" + try: + state: State = await self.elgato.state() + except ElgatoError: + if self._available: + _LOGGER.error("An error occurred while updating the Elgato Key Light") + self._available = False + return + + self._available = True + self._brightness = round((state.brightness * 255) / 100) + self._state = state.on + self._temperature = state.temperature + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this Elgato Key Light.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._info.serial_number)}, + ATTR_NAME: self._info.product_name, + ATTR_MANUFACTURER: "Elgato", + ATTR_MODEL: self._info.product_name, + ATTR_SOFTWARE_VERSION: f"{self._info.firmware_version} ({self._info.firmware_build_number})", + } diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json new file mode 100644 index 00000000000000..bed28364fa152b --- /dev/null +++ b/homeassistant/components/elgato/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "elgato", + "name": "Elgato Key Light", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/elgato", + "requirements": ["elgato==0.1.0"], + "dependencies": [], + "zeroconf": ["_elg._tcp.local."], + "codeowners": ["@frenck"] +} diff --git a/homeassistant/components/elgato/strings.json b/homeassistant/components/elgato/strings.json new file mode 100644 index 00000000000000..03c46f02efcab3 --- /dev/null +++ b/homeassistant/components/elgato/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Elgato Key Light", + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "title": "Link your Elgato Key Light", + "description": "Set up your Elgato Key Light to integrate with Home Assistant.", + "data": { + "host": "Host or IP address", + "port": "Port number" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?", + "title": "Discovered Elgato Key Light device" + } + }, + "error": { + "connection_error": "Failed to connect to Elgato Key Light device." + }, + "abort": { + "already_configured": "This Elgato Key Light device is already configured.", + "connection_error": "Failed to connect to Elgato Key Light device." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8d4be47f5f8674..cf1c4b55e1924a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -20,6 +20,7 @@ "deconz", "dialogflow", "ecobee", + "elgato", "emulated_roku", "esphome", "geofency", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 108fe38e64762f..306b3850a1b709 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -12,6 +12,9 @@ "_coap._udp.local.": [ "tradfri" ], + "_elg._tcp.local.": [ + "elgato" + ], "_esphomelib._tcp.local.": [ "esphome" ], diff --git a/requirements_all.txt b/requirements_all.txt index 094728834df874..2d8a2b881d4b56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -459,6 +459,9 @@ ecoaliface==0.4.0 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 +# homeassistant.components.elgato +elgato==0.1.0 + # homeassistant.components.eliqonline eliqonline==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b5bcf96d2d392..08174f8787471b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,6 +158,9 @@ dsmr_parser==0.12 # homeassistant.components.ee_brightbox eebrightbox==0.0.4 +# homeassistant.components.elgato +elgato==0.1.0 + # homeassistant.components.emulated_roku emulated_roku==0.1.8 diff --git a/tests/components/elgato/__init__.py b/tests/components/elgato/__init__.py new file mode 100644 index 00000000000000..1dae6cb1dac9a5 --- /dev/null +++ b/tests/components/elgato/__init__.py @@ -0,0 +1,49 @@ +"""Tests for the Elgato Key Light integration.""" + +from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER, DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def init_integration( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the Elgato Key Light integration in Home Assistant.""" + + aioclient_mock.get( + "http://example.local:9123/elgato/accessory-info", + text=load_fixture("elgato/info.json"), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.put( + "http://example.local:9123/elgato/lights", + text=load_fixture("elgato/state.json"), + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.get( + "http://example.local:9123/elgato/lights", + text=load_fixture("elgato/state.json"), + headers={"Content-Type": "application/json"}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "example.local", + CONF_PORT: 9123, + CONF_SERIAL_NUMBER: "CN11A1A00001", + }, + ) + + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py new file mode 100644 index 00000000000000..f84b82527a27ca --- /dev/null +++ b/tests/components/elgato/test_config_flow.py @@ -0,0 +1,238 @@ +"""Tests for the Elgato Key Light config flow.""" +import aiohttp + +from homeassistant import data_entry_flow +from homeassistant.components.elgato import config_flow +from homeassistant.components.elgato.const import CONF_SERIAL_NUMBER +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_show_user_form(hass: HomeAssistant) -> None: + """Test that the user set up form is served.""" + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user(user_input=None) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None: + """Test that the zeroconf confirmation form is served.""" + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF, CONF_SERIAL_NUMBER: "12345"} + result = await flow.async_step_zeroconf_confirm() + + assert result["description_placeholders"] == {CONF_SERIAL_NUMBER: "12345"} + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_show_zerconf_form( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test that the zeroconf confirmation form is served.""" + aioclient_mock.get( + "http://example.local:9123/elgato/accessory-info", + text=load_fixture("elgato/info.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf( + {"hostname": "example.local.", "port": 9123} + ) + + assert flow.context[CONF_HOST] == "example.local" + assert flow.context[CONF_PORT] == 9123 + assert flow.context[CONF_SERIAL_NUMBER] == "CN11A1A00001" + assert result["description_placeholders"] == {CONF_SERIAL_NUMBER: "CN11A1A00001"} + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on Elgato Key Light connection error.""" + aioclient_mock.get( + "http://example.local/elgato/accessory-info", exc=aiohttp.ClientError + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user( + user_input={CONF_HOST: "example.local", CONF_PORT: 9123} + ) + + assert result["errors"] == {"base": "connection_error"} + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_zeroconf_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on Elgato Key Light connection error.""" + aioclient_mock.get( + "http://example.local/elgato/accessory-info", exc=aiohttp.ClientError + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf( + user_input={"hostname": "example.local.", "port": 9123} + ) + + assert result["reason"] == "connection_error" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_confirm_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on Elgato Key Light connection error.""" + aioclient_mock.get( + "http://example.local/elgato/accessory-info", exc=aiohttp.ClientError + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = { + "source": SOURCE_ZEROCONF, + CONF_HOST: "example.local", + CONF_PORT: 9123, + } + result = await flow.async_step_zeroconf_confirm( + user_input={CONF_HOST: "example.local", CONF_PORT: 9123} + ) + + assert result["reason"] == "connection_error" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_no_data( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort if zeroconf provides no data.""" + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + result = await flow.async_step_zeroconf() + + assert result["reason"] == "connection_error" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_user_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if Elgato Key Light device already configured.""" + await init_integration(hass, aioclient_mock) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user({CONF_HOST: "example.local", CONF_PORT: 9123}) + + assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_zeroconf_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if Elgato Key Light device already configured.""" + await init_integration(hass, aioclient_mock) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf( + {"hostname": "example.local.", "port": 9123} + ) + + assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + flow.context = {"source": SOURCE_ZEROCONF, CONF_HOST: "example.local", "port": 9123} + result = await flow.async_step_zeroconf_confirm( + {"hostname": "example.local.", "port": 9123} + ) + + assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_full_user_flow_implementation( + hass: HomeAssistant, aioclient_mock +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.get( + "http://example.local:9123/elgato/accessory-info", + text=load_fixture("elgato/info.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_USER} + result = await flow.async_step_user(user_input=None) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await flow.async_step_user( + user_input={CONF_HOST: "example.local", CONF_PORT: 9123} + ) + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_PORT] == 9123 + assert result["data"][CONF_SERIAL_NUMBER] == "CN11A1A00001" + assert result["title"] == "CN11A1A00001" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_full_zeroconf_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.get( + "http://example.local:9123/elgato/accessory-info", + text=load_fixture("elgato/info.json"), + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.ElgatoFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + result = await flow.async_step_zeroconf( + {"hostname": "example.local.", "port": 9123} + ) + + assert flow.context[CONF_HOST] == "example.local" + assert flow.context[CONF_PORT] == 9123 + assert flow.context[CONF_SERIAL_NUMBER] == "CN11A1A00001" + assert result["description_placeholders"] == {CONF_SERIAL_NUMBER: "CN11A1A00001"} + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await flow.async_step_zeroconf_confirm( + user_input={CONF_HOST: "example.local"} + ) + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_PORT] == 9123 + assert result["data"][CONF_SERIAL_NUMBER] == "CN11A1A00001" + assert result["title"] == "CN11A1A00001" + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/elgato/test_init.py b/tests/components/elgato/test_init.py new file mode 100644 index 00000000000000..fd2f86fe2eae33 --- /dev/null +++ b/tests/components/elgato/test_init.py @@ -0,0 +1,33 @@ +"""Tests for the Elgato Key Light integration.""" +import aiohttp + +from homeassistant.components.elgato.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.core import HomeAssistant + +from tests.components.elgato import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_config_entry_not_ready( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the Elgato Key Light configuration entry not ready.""" + aioclient_mock.get( + "http://example.local:9123/elgato/accessory-info", exc=aiohttp.ClientError + ) + + entry = await init_integration(hass, aioclient_mock) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_config_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the Elgato Key Light configuration entry unloading.""" + entry = await init_integration(hass, aioclient_mock) + assert hass.data[DOMAIN] + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert not hass.data.get(DOMAIN) diff --git a/tests/components/elgato/test_light.py b/tests/components/elgato/test_light.py new file mode 100644 index 00000000000000..13898dad757932 --- /dev/null +++ b/tests/components/elgato/test_light.py @@ -0,0 +1,104 @@ +"""Tests for the Elgato Key Light light platform.""" +from unittest.mock import patch + +from homeassistant.components.elgato.light import ElgatoError +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from tests.common import mock_coro +from tests.components.elgato import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_light_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the Elgato Key Lights.""" + await init_integration(hass, aioclient_mock) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # First segment of the strip + state = hass.states.get("light.frenck") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 54 + assert state.attributes.get(ATTR_COLOR_TEMP) == 297 + assert state.state == STATE_ON + + entry = entity_registry.async_get("light.frenck") + assert entry + assert entry.unique_id == "CN11A1A00001" + + +async def test_light_change_state( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the change of state of a Elgato Key Light device.""" + await init_integration(hass, aioclient_mock) + + state = hass.states.get("light.frenck") + assert state.state == STATE_ON + + with patch( + "homeassistant.components.elgato.light.Elgato.light", return_value=mock_coro(), + ) as mock_light: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.frenck", + ATTR_BRIGHTNESS: 255, + ATTR_COLOR_TEMP: 100, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_light.mock_calls) == 1 + mock_light.assert_called_with(on=True, brightness=100, temperature=100) + + with patch( + "homeassistant.components.elgato.light.Elgato.light", return_value=mock_coro(), + ) as mock_light: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.frenck"}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_light.mock_calls) == 1 + mock_light.assert_called_with(on=False) + + +async def test_light_unavailable( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error/unavailable handling of an Elgato Key Light.""" + await init_integration(hass, aioclient_mock) + with patch( + "homeassistant.components.elgato.light.Elgato.light", side_effect=ElgatoError, + ): + with patch( + "homeassistant.components.elgato.light.Elgato.state", + side_effect=ElgatoError, + ): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.frenck"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.frenck") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/fixtures/elgato/info.json b/tests/fixtures/elgato/info.json new file mode 100644 index 00000000000000..e2a816df26e4bc --- /dev/null +++ b/tests/fixtures/elgato/info.json @@ -0,0 +1,9 @@ +{ + "productName": "Elgato Key Light", + "hardwareBoardType": 53, + "firmwareBuildNumber": 192, + "firmwareVersion": "1.0.3", + "serialNumber": "CN11A1A00001", + "displayName": "Frenck", + "features": ["lights"] +} diff --git a/tests/fixtures/elgato/state.json b/tests/fixtures/elgato/state.json new file mode 100644 index 00000000000000..f6180e14238cc8 --- /dev/null +++ b/tests/fixtures/elgato/state.json @@ -0,0 +1,10 @@ +{ + "numberOfLights": 1, + "lights": [ + { + "on": 1, + "brightness": 21, + "temperature": 297 + } + ] +} From b759d50900f025ebadec0197e2ef357a3c48395e Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 8 Dec 2019 09:45:13 +0100 Subject: [PATCH 2176/3953] Move imports to top for google_assistant (#29561) * Move imports to top for google_assistant * Fix pylint error caused by isorting the imports with noqa: F401 * Move back an import because of circular dependency, add annotations --- .../components/google_assistant/__init__.py | 27 +++++----- .../components/google_assistant/const.py | 2 +- .../components/google_assistant/helpers.py | 21 ++++---- .../components/google_assistant/http.py | 18 +++---- .../google_assistant/report_state.py | 4 +- .../components/google_assistant/smart_home.py | 9 ++-- .../components/google_assistant/trait.py | 53 ++++++++++--------- 7 files changed, 68 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index ecb6d76781713d..f34a8e342c4db6 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -1,34 +1,33 @@ """Support for Actions on Google Assistant Smart Home Control.""" import logging -from typing import Dict, Any +from typing import Any, Dict import voluptuous as vol # Typing imports -from homeassistant.core import HomeAssistant, ServiceCall - from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from .const import ( - DOMAIN, - CONF_PROJECT_ID, - CONF_EXPOSE_BY_DEFAULT, - DEFAULT_EXPOSE_BY_DEFAULT, - CONF_EXPOSED_DOMAINS, - DEFAULT_EXPOSED_DOMAINS, + CONF_ALIASES, + CONF_ALLOW_UNLOCK, CONF_API_KEY, - SERVICE_REQUEST_SYNC, + CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, - CONF_ALIASES, + CONF_EXPOSE_BY_DEFAULT, + CONF_EXPOSED_DOMAINS, + CONF_PRIVATE_KEY, + CONF_PROJECT_ID, CONF_REPORT_STATE, CONF_ROOM_HINT, - CONF_ALLOW_UNLOCK, CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, - CONF_CLIENT_EMAIL, - CONF_PRIVATE_KEY, + DEFAULT_EXPOSE_BY_DEFAULT, + DEFAULT_EXPOSED_DOMAINS, + DOMAIN, + SERVICE_REQUEST_SYNC, ) from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 from .const import EVENT_QUERY_RECEIVED # noqa: F401 diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 35a04e0e08ec1e..dcb87d1d93d3aa 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -1,5 +1,6 @@ """Constants for Google Assistant.""" from homeassistant.components import ( + alarm_control_panel, binary_sensor, camera, climate, @@ -15,7 +16,6 @@ sensor, switch, vacuum, - alarm_control_panel, ) DOMAIN = "google_assistant" diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 09859c5d3d0bda..8a847eca7057a2 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -7,26 +7,26 @@ from aiohttp.web import json_response -from homeassistant.core import Context, callback, HomeAssistant, State -from homeassistant.helpers.event import async_call_later from homeassistant.components import webhook -from homeassistant.helpers.storage import Store from homeassistant.const import ( - CONF_NAME, - STATE_UNAVAILABLE, - ATTR_SUPPORTED_FEATURES, ATTR_DEVICE_CLASS, + ATTR_SUPPORTED_FEATURES, CLOUD_NEVER_EXPOSED_ENTITIES, + CONF_NAME, + STATE_UNAVAILABLE, ) +from homeassistant.core import Context, HomeAssistant, State, callback +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.storage import Store from . import trait from .const import ( + CONF_ALIASES, + CONF_ROOM_HINT, + DEVICE_CLASS_TO_GOOGLE_TYPES, DOMAIN, DOMAIN_TO_GOOGLE_TYPES, - CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED, - DEVICE_CLASS_TO_GOOGLE_TYPES, - CONF_ROOM_HINT, STORE_AGENT_USER_IDS, ) from .error import SmartHomeError @@ -119,6 +119,7 @@ async def async_report_state_all(self, message): def async_enable_report_state(self): """Enable proactive mode.""" # Circular dep + # pylint: disable=import-outside-toplevel from .report_state import async_enable_report_state if self._unsub_report_state is None: @@ -213,6 +214,8 @@ def async_disable_local_sdk(self): async def _handle_local_webhook(self, hass, webhook_id, request): """Handle an incoming local SDK message.""" + # Circular dep + # pylint: disable=import-outside-toplevel from . import smart_home payload = await request.json() diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index c3d0dd493a8120..233923e97a91af 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -3,10 +3,10 @@ from datetime import timedelta import logging from uuid import uuid4 -import jwt -from aiohttp import ClientResponseError, ClientError +from aiohttp import ClientError, ClientResponseError from aiohttp.web import Request, Response +import jwt # Typing imports from homeassistant.components.http import HomeAssistantView @@ -15,24 +15,24 @@ from homeassistant.util import dt as dt_util from .const import ( - GOOGLE_ASSISTANT_API_ENDPOINT, CONF_API_KEY, - CONF_EXPOSE_BY_DEFAULT, - CONF_EXPOSED_DOMAINS, + CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, + CONF_EXPOSE_BY_DEFAULT, + CONF_EXPOSED_DOMAINS, + CONF_PRIVATE_KEY, CONF_REPORT_STATE, CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, - CONF_CLIENT_EMAIL, - CONF_PRIVATE_KEY, - HOMEGRAPH_TOKEN_URL, + GOOGLE_ASSISTANT_API_ENDPOINT, HOMEGRAPH_SCOPE, + HOMEGRAPH_TOKEN_URL, REPORT_STATE_BASE_URL, REQUEST_SYNC_BASE_URL, ) -from .smart_home import async_handle_message from .helpers import AbstractConfig +from .smart_home import async_handle_message _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 78a0f50e2773ce..1e8b6c020de1ef 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,12 +1,12 @@ """Google Report State implementation.""" import logging -from homeassistant.core import HomeAssistant, callback from homeassistant.const import MATCH_ALL +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_call_later -from .helpers import AbstractConfig, GoogleEntity, async_get_entities from .error import SmartHomeError +from .helpers import AbstractConfig, GoogleEntity, async_get_entities # Time to wait until the homegraph updates # https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 0e5037ce13a170..b111e6dc9425f4 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -3,20 +3,19 @@ from itertools import product import logging -from homeassistant.util.decorator import Registry - from homeassistant.const import ATTR_ENTITY_ID, __version__ +from homeassistant.util.decorator import Registry from .const import ( - ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, + ERR_PROTOCOL_ERROR, ERR_UNKNOWN_ERROR, EVENT_COMMAND_RECEIVED, - EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED, + EVENT_SYNC_RECEIVED, ) -from .helpers import RequestData, GoogleEntity, async_get_entities from .error import SmartHomeError +from .helpers import GoogleEntity, RequestData, async_get_entities HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 5b089459d83d32..40def0cb464f03 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2,66 +2,67 @@ import logging from homeassistant.components import ( + alarm_control_panel, binary_sensor, camera, cover, - group, fan, + group, input_boolean, - media_player, light, lock, + media_player, scene, script, sensor, switch, vacuum, - alarm_control_panel, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( - ATTR_ENTITY_ID, + ATTR_ASSUMED_STATE, + ATTR_CODE, ATTR_DEVICE_CLASS, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - STATE_LOCKED, - STATE_OFF, - STATE_ON, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, - ATTR_ASSUMED_STATE, - SERVICE_ALARM_DISARM, - SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, - STATE_ALARM_ARMED_HOME, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, STATE_ALARM_PENDING, - ATTR_CODE, + STATE_ALARM_TRIGGERED, + STATE_LOCKED, + STATE_OFF, + STATE_ON, STATE_UNKNOWN, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.util import color as color_util, temperature as temp_util + from .const import ( - ERR_VALUE_OUT_OF_RANGE, - ERR_NOT_SUPPORTED, - ERR_FUNCTION_NOT_SUPPORTED, - ERR_CHALLENGE_NOT_SETUP, CHALLENGE_ACK_NEEDED, - CHALLENGE_PIN_NEEDED, CHALLENGE_FAILED_PIN_NEEDED, - ERR_ALREADY_DISARMED, + CHALLENGE_PIN_NEEDED, ERR_ALREADY_ARMED, + ERR_ALREADY_DISARMED, + ERR_CHALLENGE_NOT_SETUP, + ERR_FUNCTION_NOT_SUPPORTED, + ERR_NOT_SUPPORTED, + ERR_VALUE_OUT_OF_RANGE, ) -from .error import SmartHomeError, ChallengeNeeded +from .error import ChallengeNeeded, SmartHomeError _LOGGER = logging.getLogger(__name__) From 957a2e99fdccaf8c8cf28915c5d9433398c97bad Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 8 Dec 2019 09:48:08 +0100 Subject: [PATCH 2177/3953] Move imports to top for tellduslive (#29550) --- .../components/tellduslive/__init__.py | 8 ++++---- .../components/tellduslive/config_flow.py | 3 +-- homeassistant/components/tellduslive/entry.py | 3 ++- .../tellduslive/test_config_flow.py | 20 +++++++++++-------- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 313699e6f1c75a..917b927691e07a 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -1,15 +1,17 @@ """Support for Telldus Live.""" import asyncio -import logging from functools import partial +import logging +from tellduslive import DIM, TURNON, UP, Session import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import CONF_SCAN_INTERVAL +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later + from . import config_flow # noqa: F401 from .const import ( CONF_HOST, @@ -51,7 +53,6 @@ async def async_setup_entry(hass, entry): """Create a tellduslive session.""" - from tellduslive import Session conf = entry.data[KEY_SESSION] @@ -159,7 +160,6 @@ def identify_device(device): """Find out what type of HA component to create.""" if device.is_sensor: return "sensor" - from tellduslive import DIM, UP, TURNON if device.methods & DIM: return "light" diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index 19f82dd18f43fe..893f3b80456769 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -4,6 +4,7 @@ import os import async_timeout +from tellduslive import Session, supports_local_api import voluptuous as vol from homeassistant import config_entries @@ -43,7 +44,6 @@ def __init__(self): self._scan_interval = SCAN_INTERVAL def _get_auth_url(self): - from tellduslive import Session self._session = Session( public_key=PUBLIC_KEY, @@ -116,7 +116,6 @@ async def async_step_auth(self, user_input=None): async def async_step_discovery(self, user_input): """Run when a Tellstick is discovered.""" - from tellduslive import supports_local_api _LOGGER.info("Discovered tellstick device: %s", user_input) if supports_local_api(user_input[1]): diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index ecd428d3b153ae..50a219bf7a15c9 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -2,6 +2,8 @@ from datetime import datetime import logging +from tellduslive import BATTERY_LOW, BATTERY_OK, BATTERY_UNKNOWN + from homeassistant.const import ATTR_BATTERY_LEVEL, DEVICE_DEFAULT_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -91,7 +93,6 @@ def device_state_attributes(self): @property def _battery_level(self): """Return the battery level of a device.""" - from tellduslive import BATTERY_LOW, BATTERY_UNKNOWN, BATTERY_OK if self.device.battery == BATTERY_LOW: return 1 diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index c615c8e6aea275..f4972ada2c7942 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -15,7 +15,7 @@ ) from homeassistant.const import CONF_HOST -from tests.common import MockConfigEntry, MockDependency, mock_coro +from tests.common import MockConfigEntry, mock_coro def init_config_flow(hass, side_effect=None): @@ -42,13 +42,17 @@ def authorize(): @pytest.fixture def mock_tellduslive(supports_local_api, authorize): """Mock tellduslive.""" - with MockDependency("tellduslive") as mock_tellduslive_: - mock_tellduslive_.supports_local_api.return_value = supports_local_api - mock_tellduslive_.Session().authorize.return_value = authorize - mock_tellduslive_.Session().access_token = "token" - mock_tellduslive_.Session().access_token_secret = "token_secret" - mock_tellduslive_.Session().authorize_url = "https://example.com" - yield mock_tellduslive_ + with patch( + "homeassistant.components.tellduslive.config_flow.Session" + ) as Session, patch( + "homeassistant.components.tellduslive.config_flow.supports_local_api" + ) as tellduslive_supports_local_api: + tellduslive_supports_local_api.return_value = supports_local_api + Session().authorize.return_value = authorize + Session().access_token = "token" + Session().access_token_secret = "token_secret" + Session().authorize_url = "https://example.com" + yield Session, tellduslive_supports_local_api async def test_abort_if_already_setup(hass): From 94b6cbc571c41426ed725c6cc1a7b1617ac9f64f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 8 Dec 2019 09:48:28 +0100 Subject: [PATCH 2178/3953] Move imports to top for stream (#29548) --- homeassistant/components/stream/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 9304257f853b9f..2bfd37a2641130 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -23,7 +23,6 @@ from .core import PROVIDERS from .hls import async_setup_hls - _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -83,6 +82,7 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N async def async_setup(hass, config): """Set up stream.""" # Keep import here so that we can import stream integration without installing reqs + # pylint: disable=import-outside-toplevel from .recorder import async_setup_recorder hass.data[DOMAIN] = {} @@ -163,6 +163,7 @@ def check_idle(self): def start(self): """Start a stream.""" # Keep import here so that we can import stream integration without installing reqs + # pylint: disable=import-outside-toplevel from .worker import stream_worker if self._thread is None or not self._thread.isAlive(): From de0db1601b0b25ce1c2873c12a528cf68235c72e Mon Sep 17 00:00:00 2001 From: Andrew McRae <37428808+aamcrae@users.noreply.github.com> Date: Sun, 8 Dec 2019 19:49:18 +1100 Subject: [PATCH 2179/3953] Add quarterly cycle for utility_meter component (#29534) * Add quarterly tariff period to utility_meter Many tariff cycles in Australia are 3 monthly (quarterly). Add quarterly tariff cycle handling to the utility_meter component. * Add quarterly tariff period to utility_meter Many tariff cycles in Australia are 3 monthly (quarterly). Add quarterly tariff cycle handling to the utility_meter component. * Change date for test for utility_meter * Add quarterly tariff period to utility_meter Many tariff cycles in Australia are 3 monthly (quarterly). Add quarterly tariff cycle handling to the utility_meter component. --- homeassistant/components/utility_meter/const.py | 3 ++- homeassistant/components/utility_meter/sensor.py | 9 ++++++++- tests/components/utility_meter/test_sensor.py | 7 +++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 87721dfbf81719..23d39204f9c328 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -5,9 +5,10 @@ DAILY = "daily" WEEKLY = "weekly" MONTHLY = "monthly" +QUARTERLY = "quarterly" YEARLY = "yearly" -METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY] +METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY] DATA_UTILITY = "utility_meter_data" diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 1ad4300b28b74f..41fe2cbcc0a078 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -25,6 +25,7 @@ DAILY, WEEKLY, MONTHLY, + QUARTERLY, YEARLY, CONF_SOURCE_SENSOR, CONF_METER_TYPE, @@ -184,6 +185,12 @@ async def _async_reset_meter(self, event): and now != date(now.year, now.month, 1) + self._period_offset ): return + if ( + self._period == QUARTERLY + and now + != date(now.year, (((now.month - 1) // 3) * 3 + 1), 1) + self._period_offset + ): + return if self._period == YEARLY and now != date(now.year, 1, 1) + self._period_offset: return await self.async_reset_meter(self._tariff_entity) @@ -209,7 +216,7 @@ async def async_added_to_hass(self): minute=self._period_offset.seconds // 60, second=self._period_offset.seconds % 60, ) - elif self._period in [DAILY, WEEKLY, MONTHLY, YEARLY]: + elif self._period in [DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY]: async_track_time_change( self.hass, self._async_reset_meter, diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 1bdaa01c2e6b02..7bf10875b778b4 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -240,6 +240,13 @@ async def test_self_reset_monthly(hass): ) +async def test_self_reset_quarterly(hass): + """Test quarterly reset of meter.""" + await _test_self_reset( + hass, gen_config("quarterly"), "2017-03-31T23:59:00.000000+00:00" + ) + + async def test_self_reset_yearly(hass): """Test yearly reset of meter.""" await _test_self_reset( From 21cf6777bb254513734e42f3b62d7e55a680559f Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Sun, 8 Dec 2019 09:49:43 +0100 Subject: [PATCH 2180/3953] bump freebox api version (#29527) --- homeassistant/components/freebox/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 96874ddeb94284..58426334dea6f6 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -60,7 +60,7 @@ async def async_setup_freebox(hass, config, host, port): } token_file = hass.config.path(FREEBOX_CONFIG_FILE) - api_version = "v4" + api_version = "v6" fbx = Freepybox(app_desc=app_desc, token_file=token_file, api_version=api_version) From 6de8072e8ac1d90dcc16653593eda29e73553c78 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 8 Dec 2019 12:19:15 +0100 Subject: [PATCH 2181/3953] Move imports to top for websocket_api (#29556) * Move imports to top for websocket_api * Move back an import because of circular dependency, add annotations --- .../components/websocket_api/__init__.py | 1 - homeassistant/components/websocket_api/auth.py | 3 +-- .../components/websocket_api/commands.py | 11 ++++++----- .../components/websocket_api/connection.py | 3 +-- homeassistant/components/websocket_api/const.py | 1 + .../components/websocket_api/decorators.py | 1 - homeassistant/components/websocket_api/http.py | 15 +++++++-------- .../components/websocket_api/messages.py | 1 - .../components/websocket_api/permissions.py | 12 ++++++------ homeassistant/components/websocket_api/sensor.py | 3 +-- 10 files changed, 23 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 1ec758ebd4d1a5..2beb2aa2788086 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -4,7 +4,6 @@ from . import commands, connection, const, decorators, http, messages - # mypy: allow-untyped-calls, allow-untyped-defs DOMAIN = const.DOMAIN diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 3971d39ee73d3c..9e33ed74fd4065 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -3,13 +3,12 @@ from voluptuous.humanize import humanize_error from homeassistant.auth.models import RefreshToken, User -from homeassistant.components.http.ban import process_wrong_login, process_success_login +from homeassistant.components.http.ban import process_success_login, process_wrong_login from homeassistant.const import __version__ from .connection import ActiveConnection from .error import Disconnect - # mypy: allow-untyped-calls, allow-untyped-defs TYPE_AUTH = "auth" diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index f30ee816914bdf..93f926b537a80c 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -2,16 +2,15 @@ import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_READ -from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED -from homeassistant.core import callback, DOMAIN as HASS_DOMAIN -from homeassistant.exceptions import Unauthorized, ServiceNotFound, HomeAssistantError +from homeassistant.const import EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL +from homeassistant.core import DOMAIN as HASS_DOMAIN, callback +from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, Unauthorized 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 homeassistant.helpers.service import async_get_all_descriptions from . import const, decorators, messages - # mypy: allow-untyped-calls, allow-untyped-defs @@ -45,6 +44,8 @@ def handle_subscribe_events(hass, connection, msg): Async friendly. """ + # Circular dep + # pylint: disable=import-outside-toplevel from .permissions import SUBSCRIBE_WHITELIST event_type = msg["event_type"] diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 5a0284a34d4c1d..ed24a70519d919 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -4,12 +4,11 @@ import voluptuous as vol -from homeassistant.core import callback, Context +from homeassistant.core import Context, callback from homeassistant.exceptions import Unauthorized from . import const, messages - # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index fe9792c4ab3fc0..8ad9443a4d643d 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -3,6 +3,7 @@ from concurrent import futures from functools import partial import json + from homeassistant.helpers.json import JSONEncoder DOMAIN = "websocket_api" diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 025131643e894a..1a1330242bc0f5 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -7,7 +7,6 @@ from . import messages - # mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index be1830aa07bdfc..3921413fd28fef 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -4,28 +4,27 @@ import logging from typing import Optional -from aiohttp import web, WSMsgType +from aiohttp import WSMsgType, web import async_timeout +from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView +from .auth import AuthPhase, auth_required_message from .const import ( - MAX_PENDING_MSG, CANCELLATION_ERRORS, - URL, + DATA_CONNECTIONS, ERR_UNKNOWN_ERROR, + JSON_DUMP, + MAX_PENDING_MSG, SIGNAL_WEBSOCKET_CONNECTED, SIGNAL_WEBSOCKET_DISCONNECTED, - DATA_CONNECTIONS, - JSON_DUMP, + URL, ) -from .auth import AuthPhase, auth_required_message from .error import Disconnect from .messages import error_message - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index c8c760a6549560..27d557e8110d33 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -6,7 +6,6 @@ from . import const - # mypy: allow-untyped-defs # Minimal requirements of a message diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index ffbb80fa19ec7b..c270c0f0cccfc5 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -2,22 +2,22 @@ Separate file to avoid circular imports. """ +from homeassistant.components.frontend import EVENT_PANELS_UPDATED +from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED +from homeassistant.components.persistent_notification import ( + EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, +) from homeassistant.const import ( EVENT_COMPONENT_LOADED, + EVENT_CORE_CONFIG_UPDATE, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, - EVENT_CORE_CONFIG_UPDATE, -) -from homeassistant.components.persistent_notification import ( - EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, ) -from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED -from homeassistant.components.frontend import EVENT_PANELS_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index f8f1257aefca4a..4ae39787335adf 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -4,12 +4,11 @@ from homeassistant.helpers.entity import Entity from .const import ( + DATA_CONNECTIONS, SIGNAL_WEBSOCKET_CONNECTED, SIGNAL_WEBSOCKET_DISCONNECTED, - DATA_CONNECTIONS, ) - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs From d752fe303311043367e9bfef688509212279b158 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 8 Dec 2019 12:20:53 +0100 Subject: [PATCH 2182/3953] Move imports to top for fido (#29557) * Move imports to top for fido * Fix tests for fido by using patch --- homeassistant/components/fido/sensor.py | 14 ++++----- tests/components/fido/test_sensor.py | 42 +++++++++++-------------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 8814a2406c52a9..086ae87a529996 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -7,21 +7,23 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fido/ """ -import logging from datetime import timedelta +import logging +from pyfido import FidoClient +from pyfido.client import PyFidoError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_NAME, CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -147,7 +149,6 @@ class FidoData: def __init__(self, username, password, httpsession): """Initialize the data object.""" - from pyfido import FidoClient self.client = FidoClient(username, password, REQUESTS_TIMEOUT, httpsession) self.data = {} @@ -155,7 +156,6 @@ def __init__(self, username, password, httpsession): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from Fido.""" - from pyfido.client import PyFidoError try: await self.client.fetch_data() diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index 510d432124342c..010896e086bada 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -2,7 +2,7 @@ import asyncio import logging import sys -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components.fido import sensor as fido @@ -66,29 +66,25 @@ def fake_async_add_entities(component, update_before_add=False): @asyncio.coroutine def test_fido_sensor(loop, hass): """Test the Fido number sensor.""" - sys.modules["pyfido"] = MagicMock() - sys.modules["pyfido.client"] = MagicMock() - sys.modules["pyfido.client.PyFidoError"] = PyFidoErrorMock - import pyfido.client - - pyfido.FidoClient = FidoClientMock - pyfido.client.PyFidoError = PyFidoErrorMock - config = { - "sensor": { - "platform": "fido", - "name": "fido", - "username": "myusername", - "password": "password", - "monitored_variables": ["balance", "data_remaining"], + with patch( + "homeassistant.components.fido.sensor.FidoClient", new=FidoClientMock + ), patch("homeassistant.components.fido.sensor.PyFidoError", new=PyFidoErrorMock): + config = { + "sensor": { + "platform": "fido", + "name": "fido", + "username": "myusername", + "password": "password", + "monitored_variables": ["balance", "data_remaining"], + } } - } - with assert_setup_component(1): - yield from async_setup_component(hass, "sensor", config) - state = hass.states.get("sensor.fido_1112223344_balance") - assert state.state == "160.12" - assert state.attributes.get("number") == "1112223344" - state = hass.states.get("sensor.fido_1112223344_data_remaining") - assert state.state == "100.33" + with assert_setup_component(1): + yield from async_setup_component(hass, "sensor", config) + state = hass.states.get("sensor.fido_1112223344_balance") + assert state.state == "160.12" + assert state.attributes.get("number") == "1112223344" + state = hass.states.get("sensor.fido_1112223344_data_remaining") + assert state.state == "100.33" @asyncio.coroutine From ef4515ed70e8d92ae471e348a5d094d3879911fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 8 Dec 2019 13:21:48 +0200 Subject: [PATCH 2183/3953] Add Huawei LTE reboot and clear traffic statistics services (#29594) * Add clear traffic statistics service * Add reboot service * Register services as admin ones * Make URL optional when there's only one router configured * Eliminate one if/else indent level * Remove unnecessary .keys() with sorted() --- .../components/huawei_lte/__init__.py | 37 +++++++++++++++++++ homeassistant/components/huawei_lte/const.py | 3 ++ .../components/huawei_lte/services.yaml | 13 +++++++ 3 files changed, 53 insertions(+) create mode 100644 homeassistant/components/huawei_lte/services.yaml diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index ada1f0a8abdcdc..30b8eb1bc6d1dd 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -58,6 +58,8 @@ KEY_MONITORING_STATUS, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_WLAN_HOST_LIST, + SERVICE_CLEAR_TRAFFIC_STATISTICS, + SERVICE_REBOOT, UPDATE_OPTIONS_SIGNAL, UPDATE_SIGNAL, ) @@ -103,6 +105,8 @@ extra=vol.ALLOW_EXTRA, ) +SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_URL): cv.url}) + CONFIG_ENTRY_PLATFORMS = ( BINARY_SENSOR_DOMAIN, DEVICE_TRACKER_DOMAIN, @@ -387,6 +391,39 @@ async def async_setup(hass: HomeAssistantType, config) -> bool: for router_config in config.get(DOMAIN, []): domain_config[url_normalize(router_config.pop(CONF_URL))] = router_config + def service_handler(service) -> None: + """Apply a service.""" + url = service.data.get(CONF_URL) + routers = hass.data[DOMAIN].routers + if url: + router = routers.get(url) + elif len(routers) == 1: + router = next(iter(routers.values())) + else: + _LOGGER.error( + "%s: more than one router configured, must specify one of URLs %s", + service.service, + sorted(routers), + ) + return + if not router: + _LOGGER.error("%s: router %s unavailable", service.service, url) + return + + if service.service == SERVICE_CLEAR_TRAFFIC_STATISTICS: + result = router.client.monitoring.set_clear_traffic() + _LOGGER.debug("%s: %s", service.service, result) + elif service.service == SERVICE_REBOOT: + result = router.client.device.reboot() + _LOGGER.debug("%s: %s", service.service, result) + else: + _LOGGER.error("%s: unsupported service", service.service) + + for service in (SERVICE_CLEAR_TRAFFIC_STATISTICS, SERVICE_REBOOT): + hass.helpers.service.async_register_admin_service( + DOMAIN, service, service_handler, schema=SERVICE_SCHEMA, + ) + for url, router_config in domain_config.items(): hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index b6e079576ac132..c71b51435e1a4b 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -12,6 +12,9 @@ CONNECTION_TIMEOUT = 10 +SERVICE_CLEAR_TRAFFIC_STATISTICS = "clear_traffic_statistics" +SERVICE_REBOOT = "reboot" + KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" diff --git a/homeassistant/components/huawei_lte/services.yaml b/homeassistant/components/huawei_lte/services.yaml new file mode 100644 index 00000000000000..428745ee33e2ec --- /dev/null +++ b/homeassistant/components/huawei_lte/services.yaml @@ -0,0 +1,13 @@ +clear_traffic_statistics: + description: Clear traffic statistics. + fields: + url: + description: URL of router to clear; optional when only one is configured. + example: http://192.168.100.1/ + +reboot: + description: Reboot router. + fields: + url: + description: URL of router to reboot; optional when only one is configured. + example: http://192.168.100.1/ From 700cecc8ef14c067e509be1ead206761b224ed7b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 13:41:51 +0100 Subject: [PATCH 2184/3953] sort imports according to PEP8 for airly (#29615) --- homeassistant/components/airly/air_quality.py | 4 ++-- homeassistant/components/airly/config_flow.py | 6 +++--- tests/components/airly/test_config_flow.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index 082344c14e3b94..e22fa7939c2ea1 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -1,9 +1,9 @@ """Support for the Airly air_quality service.""" from homeassistant.components.air_quality import ( - AirQualityEntity, ATTR_AQI, - ATTR_PM_10, ATTR_PM_2_5, + ATTR_PM_10, + AirQualityEntity, ) from homeassistant.const import CONF_NAME diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index b361930fa7da0a..31cfec7e7aacbc 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -1,14 +1,14 @@ """Adds config flow for Airly.""" -import async_timeout -import voluptuous as vol from airly import Airly from airly.exceptions import AirlyError +import async_timeout +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from .const import DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 8b615b34c2aa13..a5ca3981a5a77d 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -5,11 +5,11 @@ from asynctest import patch from homeassistant import data_entry_flow -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.components.airly import config_flow from homeassistant.components.airly.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from tests.common import load_fixture, MockConfigEntry +from tests.common import MockConfigEntry, load_fixture CONFIG = { CONF_NAME: "abcd", From d020486929e66fd6fffe044556212bddbe13848d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 13:42:31 +0100 Subject: [PATCH 2185/3953] sort imports according to PEP8 for alarm_control_panel (#29616) --- .../components/alarm_control_panel/__init__.py | 2 +- tests/components/alarm_control_panel/common.py | 10 +++++----- .../alarm_control_panel/test_device_action.py | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index dfac0fd192f93c..5fb44a18a0be6d 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -17,9 +17,9 @@ ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + make_entity_service_schema, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/tests/components/alarm_control_panel/common.py b/tests/components/alarm_control_panel/common.py index d06939dce9b322..ce0bde0517c60c 100644 --- a/tests/components/alarm_control_panel/common.py +++ b/tests/components/alarm_control_panel/common.py @@ -7,13 +7,13 @@ from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, - SERVICE_ALARM_TRIGGER, - SERVICE_ALARM_DISARM, - SERVICE_ALARM_ARM_HOME, + ENTITY_MATCH_ALL, SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS, - ENTITY_MATCH_ALL, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, ) from homeassistant.loader import bind_hass diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index bc489dbe251172..72754c3c96f360 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components.alarm_control_panel import DOMAIN +import homeassistant.components.automation as automation from homeassistant.const import ( CONF_PLATFORM, STATE_ALARM_ARMED_AWAY, @@ -11,17 +12,16 @@ STATE_ALARM_TRIGGERED, STATE_UNKNOWN, ) -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automation_capabilities, + async_get_device_automations, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) From e510c4ea1dcbdaff7924d981cfbf586390d1f30e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 13:42:55 +0100 Subject: [PATCH 2186/3953] sort imports according to PEP8 for air_quality (#29614) --- homeassistant/components/air_quality/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 00308c40b3621f..29c6756260c56e 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -2,12 +2,12 @@ from datetime import timedelta import logging -from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent _LOGGER = logging.getLogger(__name__) From f5a1b32be0b1aa2eef86df3cce7b616268e1f050 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 13:43:32 +0100 Subject: [PATCH 2187/3953] sort imports according to PEP8 for ads (#29613) --- homeassistant/components/ads/__init__.py | 13 ++++++------- homeassistant/components/ads/binary_sensor.py | 2 +- homeassistant/components/ads/cover.py | 14 +++++++------- homeassistant/components/ads/light.py | 2 +- homeassistant/components/ads/sensor.py | 2 +- homeassistant/components/ads/switch.py | 4 ++-- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index ba4762da84a4ae..adaaaa08b7f7b3 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -1,14 +1,13 @@ """Support for Automation Device Specification (ADS).""" -import threading -import struct -import logging -import ctypes -from collections import namedtuple import asyncio -import async_timeout +from collections import namedtuple +import ctypes +import logging +import struct +import threading +import async_timeout import pyads - import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index 2afb163fc32f05..fd6d77873b5f77 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME import homeassistant.helpers.config_validation as cv -from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE +from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index b21c064e94125c..0fdcbc16ef8d21 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -4,25 +4,25 @@ import voluptuous as vol from homeassistant.components.cover import ( + ATTR_POSITION, + DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, - SUPPORT_OPEN, SUPPORT_CLOSE, - SUPPORT_STOP, + SUPPORT_OPEN, SUPPORT_SET_POSITION, - ATTR_POSITION, - DEVICE_CLASSES_SCHEMA, + SUPPORT_STOP, CoverDevice, ) -from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME import homeassistant.helpers.config_validation as cv from . import ( CONF_ADS_VAR, CONF_ADS_VAR_POSITION, DATA_ADS, - AdsEntity, - STATE_KEY_STATE, STATE_KEY_POSITION, + STATE_KEY_STATE, + AdsEntity, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py index f1e78ea132e9d4..b9626b9e969fd8 100644 --- a/homeassistant/components/ads/light.py +++ b/homeassistant/components/ads/light.py @@ -16,9 +16,9 @@ CONF_ADS_VAR, CONF_ADS_VAR_BRIGHTNESS, DATA_ADS, - AdsEntity, STATE_KEY_BRIGHTNESS, STATE_KEY_STATE, + AdsEntity, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 1d956b1fba2888..3bdcbfc95f811d 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -8,7 +8,7 @@ from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT import homeassistant.helpers.config_validation as cv -from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, AdsEntity, STATE_KEY_STATE +from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, STATE_KEY_STATE, AdsEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py index 1b5a40debb6738..3590b6af88e5e2 100644 --- a/homeassistant/components/ads/switch.py +++ b/homeassistant/components/ads/switch.py @@ -3,11 +3,11 @@ import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE +from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity _LOGGER = logging.getLogger(__name__) From a885670a9aede98b7074b0b29a070edda1adf2e7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 13:44:12 +0100 Subject: [PATCH 2188/3953] sort imports according to PEP8 for abode (#29610) --- homeassistant/components/abode/__init__.py | 4 ++-- homeassistant/components/abode/config_flow.py | 2 +- tests/components/abode/test_config_flow.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 1689576bc7f9e0..6d23701d088822 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -20,14 +20,14 @@ CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.entity import Entity from .const import ( ATTRIBUTION, - DOMAIN, DEFAULT_CACHEDB, + DOMAIN, SIGNAL_CAPTURE_IMAGE, SIGNAL_TRIGGER_QUICK_ACTION, ) diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index b8e8548db3181e..89b389798f6326 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -10,7 +10,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from .const import DOMAIN, DEFAULT_CACHEDB # pylint: disable=unused-import +from .const import DEFAULT_CACHEDB, DOMAIN # pylint: disable=unused-import CONF_POLLING = "polling" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index c3f5d17076753e..5e32c923245069 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -6,6 +6,7 @@ from homeassistant import data_entry_flow from homeassistant.components.abode import config_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + from tests.common import MockConfigEntry CONF_POLLING = "polling" From cfe68d7e0068d6efcb52337c7c2a12436e7943f6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 13:44:44 +0100 Subject: [PATCH 2189/3953] sort imports according to PEP8 for actiontec (#29612) --- homeassistant/components/actiontec/device_tracker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index e07dd2622beb06..302a8d56173e98 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -1,18 +1,19 @@ """Support for Actiontec MI424WR (Verizon FIOS) routers.""" +from collections import namedtuple import logging import re import telnetlib -from collections import namedtuple + import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) From b4bcd477f817e2c2ce670a5f05e164690b8b639c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 13:45:33 +0100 Subject: [PATCH 2190/3953] sort imports according to PEP8 for acer_projector (#29611) --- homeassistant/components/acer_projector/switch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 39a79636c9360a..b28d67562d4bda 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -1,17 +1,17 @@ """Use serial protocol of Acer projector to obtain state of the projector.""" import logging import re -import serial +import serial import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( - STATE_ON, + CONF_FILENAME, + CONF_NAME, STATE_OFF, + STATE_ON, STATE_UNKNOWN, - CONF_NAME, - CONF_FILENAME, ) import homeassistant.helpers.config_validation as cv From 74c0219d0c81c17cd7c29795bdaa02a62ca9ebd0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 13:46:04 +0100 Subject: [PATCH 2191/3953] sort imports according to PEP8 for components (#29609) --- homeassistant/components/__init__.py | 1 - tests/components/conftest.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 7bb572dcf6b4c5..90e0f32226c33c 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -11,7 +11,6 @@ from homeassistant.core import split_entity_id - # mypy: allow-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 4f1f3e64e025c3..a589839c03f3b6 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -3,13 +3,13 @@ import pytest -from homeassistant.setup import async_setup_component -from homeassistant.components.websocket_api.http import URL from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED, ) +from homeassistant.components.websocket_api.http import URL +from homeassistant.setup import async_setup_component from tests.common import mock_coro From 09ff0a5ac6fb28f69122e63f4d288253d558ee92 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 13:50:02 +0100 Subject: [PATCH 2192/3953] sort imports according to PEP8 for yweather (#29608) --- tests/components/yweather/test_sensor.py | 3 +-- tests/components/yweather/test_weather.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/components/yweather/test_sensor.py b/tests/components/yweather/test_sensor.py index f5f5852229ba52..25cb26d7c83564 100644 --- a/tests/components/yweather/test_sensor.py +++ b/tests/components/yweather/test_sensor.py @@ -1,12 +1,11 @@ """The tests for the Yahoo weather sensor component.""" import json - import unittest from unittest.mock import patch from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, load_fixture, MockDependency +from tests.common import MockDependency, get_test_home_assistant, load_fixture VALID_CONFIG_MINIMAL = { "sensor": {"platform": "yweather", "monitored_conditions": ["weather"]} diff --git a/tests/components/yweather/test_weather.py b/tests/components/yweather/test_weather.py index 2c0435672e6184..c9bf7bc89b668d 100644 --- a/tests/components/yweather/test_weather.py +++ b/tests/components/yweather/test_weather.py @@ -1,6 +1,5 @@ """The tests for the Yahoo weather component.""" import json - import unittest from unittest.mock import patch @@ -11,10 +10,10 @@ ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, ) -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.setup import setup_component +from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.common import get_test_home_assistant, load_fixture, MockDependency +from tests.common import MockDependency, get_test_home_assistant, load_fixture def _yql_queryMock(yql): # pylint: disable=invalid-name From fa00808f6c5caa7befd0ec62f5bce9518301b8c6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 8 Dec 2019 18:21:54 +0530 Subject: [PATCH 2193/3953] Upgrade keyring to 19.3.0 and keyrings.alt to 3.2.0 (#29607) --- homeassistant/scripts/keyring.py | 4 +--- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 0c5623a50ad1c5..63b4fd1a37e736 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,10 +5,8 @@ from homeassistant.util.yaml import _SECRET_NAMESPACE - # mypy: allow-untyped-defs - -REQUIREMENTS = ["keyring==19.2.0", "keyrings.alt==3.1.1"] +REQUIREMENTS = ["keyring==19.3.0", "keyrings.alt==3.2.0"] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 2d8a2b881d4b56..992069c5045f1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -742,10 +742,10 @@ kaiterra-async-client==0.0.2 keba-kecontact==0.2.0 # homeassistant.scripts.keyring -keyring==19.2.0 +keyring==19.3.0 # homeassistant.scripts.keyring -keyrings.alt==3.1.1 +keyrings.alt==3.2.0 # homeassistant.components.kiwi kiwiki-client==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08174f8787471b..5fe6c4f6143190 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -259,10 +259,10 @@ influxdb==5.2.3 jsonpath==0.82 # homeassistant.scripts.keyring -keyring==19.2.0 +keyring==19.3.0 # homeassistant.scripts.keyring -keyrings.alt==3.1.1 +keyrings.alt==3.2.0 # homeassistant.components.dyson libpurecool==0.5.0 From 6ad3b6426a46ed26e37eee35dce700572d765ddc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 14:55:23 +0100 Subject: [PATCH 2194/3953] sort imports according to PEP8 for auth (#29619) --- homeassistant/components/auth/__init__.py | 14 ++++++-------- homeassistant/components/auth/indieauth.py | 6 +++--- homeassistant/components/auth/login_flow.py | 5 +++-- homeassistant/components/auth/mfa_setup_flow.py | 2 +- tests/components/auth/__init__.py | 1 - tests/components/auth/test_init.py | 6 +++--- tests/components/auth/test_mfa_setup_flow.py | 2 +- 7 files changed, 17 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index d0da9d39fe8724..888ef98a582a11 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -114,31 +114,29 @@ } """ +from datetime import timedelta import logging import uuid -from datetime import timedelta from aiohttp import web import voluptuous as vol from homeassistant.auth.models import ( - User, - Credentials, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + Credentials, + User, ) -from homeassistant.loader import bind_hass from homeassistant.components import websocket_api from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http.auth import async_sign_path from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util -from . import indieauth -from . import login_flow -from . import mfa_setup_flow +from . import indieauth, login_flow, mfa_setup_flow DOMAIN = "auth" WS_TYPE_CURRENT_USER = "auth/current_user" diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 6a0a516bee2a82..c845f230bf3b56 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -1,9 +1,9 @@ """Helpers to resolve client ID/secret.""" -import logging import asyncio -from ipaddress import ip_address from html.parser import HTMLParser -from urllib.parse import urlparse, urljoin +from ipaddress import ip_address +import logging +from urllib.parse import urljoin, urlparse import aiohttp diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index d6844396ce7811..6f8d2751018107 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -73,12 +73,13 @@ from homeassistant import data_entry_flow from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http.ban import ( - process_wrong_login, - process_success_login, log_invalid_auth, + process_success_login, + process_wrong_login, ) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView + from . import indieauth diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 271e9ae163417c..92926e2e7c5f0a 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -6,7 +6,7 @@ from homeassistant import data_entry_flow from homeassistant.components import websocket_api -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback WS_TYPE_SETUP_MFA = "auth/setup_mfa" SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index 5114e18889b842..7ce6596408628d 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -4,7 +4,6 @@ from tests.common import ensure_auth_manager_loaded - BASE_CONFIG = [ { "name": "Example", diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index de91613b74b12e..162569aa0e8e36 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -3,15 +3,15 @@ from unittest.mock import patch from homeassistant.auth.models import Credentials +from homeassistant.components import auth from homeassistant.components.auth import RESULT_TYPE_USER from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from homeassistant.components import auth - -from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser from . import async_setup_auth +from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser + async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): """Test logging in with new user and refreshing tokens.""" diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py index ffce9266e5729c..3569d7d5233b78 100644 --- a/tests/components/auth/test_mfa_setup_flow.py +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -4,7 +4,7 @@ from homeassistant.components.auth import mfa_setup_flow from homeassistant.setup import async_setup_component -from tests.common import MockUser, CLIENT_ID, ensure_auth_manager_loaded +from tests.common import CLIENT_ID, MockUser, ensure_auth_manager_loaded async def test_ws_setup_depose_mfa(hass, hass_ws_client): From ba34922b03f40917041dff15be7ecbdb34128aa2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 14:56:42 +0100 Subject: [PATCH 2195/3953] sort imports according to PEP8 for alexa (#29618) --- homeassistant/components/alexa/__init__.py | 11 +++---- homeassistant/components/alexa/auth.py | 3 +- .../components/alexa/capabilities.py | 10 +++--- homeassistant/components/alexa/const.py | 4 +-- homeassistant/components/alexa/entities.py | 26 +++++++-------- .../components/alexa/flash_briefings.py | 2 +- homeassistant/components/alexa/handlers.py | 8 ++--- homeassistant/components/alexa/smart_home.py | 2 +- .../components/alexa/smart_home_http.py | 2 +- .../components/alexa/state_report.py | 2 +- tests/components/alexa/__init__.py | 2 +- tests/components/alexa/test_auth.py | 1 + tests/components/alexa/test_capabilities.py | 33 ++++++++++--------- tests/components/alexa/test_entities.py | 3 +- .../components/alexa/test_flash_briefings.py | 4 +-- tests/components/alexa/test_intent.py | 4 +-- tests/components/alexa/test_smart_home.py | 18 +++++----- .../components/alexa/test_smart_home_http.py | 2 +- tests/components/alexa/test_state_report.py | 3 +- 19 files changed, 72 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index cb0d093bb4802f..e9bcccb3587309 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -3,25 +3,24 @@ import voluptuous as vol -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import entityfilter from homeassistant.const import CONF_NAME +from homeassistant.helpers import config_validation as cv, entityfilter from . import flash_briefings, intent, smart_home_http from .const import ( CONF_AUDIO, CONF_CLIENT_ID, CONF_CLIENT_SECRET, + CONF_DESCRIPTION, + CONF_DISPLAY_CATEGORIES, CONF_DISPLAY_URL, CONF_ENDPOINT, + CONF_ENTITY_CONFIG, + CONF_FILTER, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, - CONF_FILTER, - CONF_ENTITY_CONFIG, - CONF_DESCRIPTION, - CONF_DISPLAY_CATEGORIES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 9f87a6d954e95b..33c25b73d7e73b 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -1,8 +1,9 @@ """Support for Alexa skill auth.""" import asyncio +from datetime import timedelta import json import logging -from datetime import timedelta + import aiohttp import async_timeout diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 1f18cb7a5909d9..38f769b7cd4a0f 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,6 +1,10 @@ """Alexa capabilities.""" import logging +from homeassistant.components import cover, fan, light +from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER +import homeassistant.components.climate.const as climate +import homeassistant.components.media_player.const as media_player from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, @@ -20,21 +24,17 @@ STATE_UNKNOWN, STATE_UNLOCKED, ) -import homeassistant.components.climate.const as climate -import homeassistant.components.media_player.const as media_player -from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER -from homeassistant.components import light, fan, cover import homeassistant.util.color as color_util import homeassistant.util.dt as dt_util from .const import ( - Catalog, API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, DATE_FORMAT, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, + Catalog, Inputs, ) from .errors import UnsupportedProperty diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 1aa9d4f2c1dcc2..2c62e1a485a6a6 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -1,9 +1,9 @@ """Constants for the Alexa integration.""" from collections import OrderedDict -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.components.climate import const as climate from homeassistant.components import fan +from homeassistant.components.climate import const as climate +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT DOMAIN = "alexa" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index f9463949b58606..733ea73f9980a7 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -1,18 +1,6 @@ """Alexa entity adapters.""" from typing import List -from homeassistant.core import callback -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, - CLOUD_NEVER_EXPOSED_ENTITIES, - CONF_NAME, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -from homeassistant.util.decorator import Registry -from homeassistant.components.climate import const as climate from homeassistant.components import ( alarm_control_panel, alert, @@ -30,8 +18,19 @@ sensor, switch, ) +from homeassistant.components.climate import const as climate +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, + CLOUD_NEVER_EXPOSED_ENTITIES, + CONF_NAME, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import callback +from homeassistant.util.decorator import Registry -from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES from .capabilities import ( Alexa, AlexaBrightnessController, @@ -60,6 +59,7 @@ AlexaThermostatController, AlexaToggleController, ) +from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES ENTITY_ADAPTERS = Registry() diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 0b5c1243764ce2..45d31d6088afc7 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -3,10 +3,10 @@ import logging import uuid -import homeassistant.util.dt as dt_util from homeassistant.components import http from homeassistant.core import callback from homeassistant.helpers import template +import homeassistant.util.dt as dt_util from .const import ( ATTR_MAIN_TEXT, diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index f1aa260e88ea44..efb4f59514dc79 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -34,20 +34,20 @@ TEMP_FAHRENHEIT, ) import homeassistant.util.color as color_util -import homeassistant.util.dt as dt_util from homeassistant.util.decorator import Registry +import homeassistant.util.dt as dt_util from homeassistant.util.temperature import convert as convert_temperature from .const import ( API_TEMP_UNITS, - API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_MODES, + API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_PRESETS, - Cause, - Inputs, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, SPEED_FAN_MAP, + Cause, + Inputs, ) from .entities import async_get_entities from .errors import ( diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 2c34542e25cd8d..9b0955f8fcacac 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -4,7 +4,7 @@ import homeassistant.core as ha from .const import API_DIRECTIVE, API_HEADER -from .errors import AlexaError, AlexaBridgeUnreachableError +from .errors import AlexaBridgeUnreachableError, AlexaError from .handlers import HANDLERS from .messages import AlexaDirective diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index ada00e8a326820..08d33ffa09c660 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -13,8 +13,8 @@ CONF_ENTITY_CONFIG, CONF_FILTER, ) -from .state_report import async_enable_proactive_mode from .smart_home import async_handle_message +from .state_report import async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index b5e1b741f0c4e4..44e1b7f4f554a7 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -6,8 +6,8 @@ import aiohttp import async_timeout -import homeassistant.util.dt as dt_util from homeassistant.const import MATCH_ALL, STATE_ON +import homeassistant.util.dt as dt_util from .const import API_CHANGE, Cause from .entities import ENTITY_ADAPTERS diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 3752ad7d48fe66..79fdb86c3efdcd 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -1,8 +1,8 @@ """Tests for the Alexa integration.""" from uuid import uuid4 -from homeassistant.core import Context from homeassistant.components.alexa import config, smart_home +from homeassistant.core import Context from tests.common import async_mock_service diff --git a/tests/components/alexa/test_auth.py b/tests/components/alexa/test_auth.py index ecd8f1eb4b3a29..9d14fffe7e478a 100644 --- a/tests/components/alexa/test_auth.py +++ b/tests/components/alexa/test_auth.py @@ -1,5 +1,6 @@ """Test Alexa auth endpoints.""" from homeassistant.components.alexa.auth import Auth + from . import TEST_TOKEN_URL diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index d2f6cddc522c36..952af543aabfc4 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -1,37 +1,38 @@ """Test Alexa capabilities.""" import pytest -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS, - STATE_LOCKED, - STATE_UNLOCKED, - STATE_UNKNOWN, - STATE_UNAVAILABLE, - STATE_ALARM_DISARMED, - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_CUSTOM_BYPASS, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, -) -from homeassistant.components.climate import const as climate from homeassistant.components.alexa import smart_home from homeassistant.components.alexa.errors import UnsupportedProperty +from homeassistant.components.climate import const as climate from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_STOP, ) -from tests.common import async_mock_service +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_LOCKED, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + STATE_UNLOCKED, + TEMP_CELSIUS, +) from . import ( DEFAULT_CONFIG, - get_new_request, assert_request_calls_service, assert_request_fails, + get_new_request, reported_properties, ) +from tests.common import async_mock_service + @pytest.mark.parametrize("result,adjust", [(25, "-5"), (35, "5"), (0, "-80")]) async def test_api_adjust_brightness(hass, result, adjust): diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 7436306fe25f31..8cae4fb9b77bde 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -1,6 +1,7 @@ """Test Alexa entity representation.""" from homeassistant.components.alexa import smart_home -from . import get_new_request, DEFAULT_CONFIG + +from . import DEFAULT_CONFIG, get_new_request async def test_unsupported_domain(hass): diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index 4c94046d41a169..7a20c05ed86b3c 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -5,10 +5,10 @@ import pytest -from homeassistant.core import callback -from homeassistant.setup import async_setup_component from homeassistant.components import alexa from homeassistant.components.alexa import const +from homeassistant.core import callback +from homeassistant.setup import async_setup_component SESSION_ID = "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000" APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 0b7fc5f22c8d4b..ac41e6d3b4d1c2 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -5,10 +5,10 @@ import pytest -from homeassistant.core import callback -from homeassistant.setup import async_setup_component from homeassistant.components import alexa from homeassistant.components.alexa import intent +from homeassistant.core import callback +from homeassistant.setup import async_setup_component SESSION_ID = "amzn1.echo-api.session.0000000-0000-0000-0000-00000000000" APPLICATION_ID = "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe" diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 9a2eba21c0e270..ea1e6f50fcf9a8 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,9 +1,7 @@ """Test for smart home alexa support.""" import pytest -from homeassistant.core import Context, callback -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.components.alexa import smart_home, messages +from homeassistant.components.alexa import messages, smart_home from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -18,22 +16,24 @@ SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import Context, callback from homeassistant.helpers import entityfilter -from tests.common import async_mock_service - from . import ( - get_new_request, - MockConfig, DEFAULT_CONFIG, - assert_request_calls_service, - assert_request_fails, + MockConfig, ReportedProperties, assert_power_controller_works, + assert_request_calls_service, + assert_request_fails, assert_scene_controller_works, + get_new_request, reported_properties, ) +from tests.common import async_mock_service + @pytest.fixture def events(hass): diff --git a/tests/components/alexa/test_smart_home_http.py b/tests/components/alexa/test_smart_home_http.py index 845c375be6790e..f242a421eac2a2 100644 --- a/tests/components/alexa/test_smart_home_http.py +++ b/tests/components/alexa/test_smart_home_http.py @@ -1,8 +1,8 @@ """Test Smart Home HTTP endpoints.""" import json -from homeassistant.setup import async_setup_component from homeassistant.components.alexa import DOMAIN, smart_home_http +from homeassistant.setup import async_setup_component from . import get_new_request diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 2c58d1ed45ed17..4cd2a18a8333f9 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -1,6 +1,7 @@ """Test report state.""" from homeassistant.components.alexa import state_report -from . import TEST_URL, DEFAULT_CONFIG + +from . import DEFAULT_CONFIG, TEST_URL async def test_report_state(hass, aioclient_mock): From 663e2eaaffe48bec0987fc18d9b2ba9ff3548284 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 14:59:21 +0100 Subject: [PATCH 2196/3953] sort imports according to PEP8 for buienradar (#29623) --- homeassistant/components/buienradar/camera.py | 5 +---- homeassistant/components/buienradar/sensor.py | 2 -- homeassistant/components/buienradar/util.py | 5 +---- homeassistant/components/buienradar/weather.py | 2 +- tests/components/buienradar/test_camera.py | 4 ++-- tests/components/buienradar/test_sensor.py | 3 +-- tests/components/buienradar/test_weather.py | 1 - 7 files changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index cdf202bbafd685..3d30e330bc9ef4 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -1,7 +1,7 @@ """Provide animated GIF loops of Buienradar imagery.""" import asyncio -import logging from datetime import datetime, timedelta +import logging from typing import Optional import aiohttp @@ -9,13 +9,10 @@ from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_NAME - from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession - from homeassistant.util import dt as dt_util - CONF_DIMENSION = "dimension" CONF_DELTA = "delta" diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 5fe97b6fb38854..4011a928dfe970 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -33,11 +33,9 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util - from .const import DEFAULT_TIMEFRAME from .util import BrData - _LOGGER = logging.getLogger(__name__) MEASURED_LABEL = "Measured" diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 579b341827128e..2ef0713713bbdf 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -5,7 +5,6 @@ import aiohttp import async_timeout - from buienradar.buienradar import parse_data from buienradar.constants import ( ATTRIBUTION, @@ -31,9 +30,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util - -from .const import SCHEDULE_OK, SCHEDULE_NOK - +from .const import SCHEDULE_NOK, SCHEDULE_OK _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index c95e57807c4aba..98cbb2f5e43e42 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -28,8 +28,8 @@ from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation -from .util import BrData from .const import DEFAULT_TIMEFRAME +from .util import BrData _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index dcbbb4ae076551..2dd63583741910 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -1,10 +1,10 @@ """The tests for generic camera component.""" import asyncio -from aiohttp.client_exceptions import ClientResponseError -from homeassistant.util import dt as dt_util +from aiohttp.client_exceptions import ClientResponseError from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util # An infinitesimally small time-delta. EPSILON_DELTA = 0.0000000001 diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py index c1569e4576be29..0c1cbd2a158cdf 100644 --- a/tests/components/buienradar/test_sensor.py +++ b/tests/components/buienradar/test_sensor.py @@ -1,7 +1,6 @@ """The tests for the Buienradar sensor platform.""" -from homeassistant.setup import async_setup_component from homeassistant.components import sensor - +from homeassistant.setup import async_setup_component CONDITIONS = ["stationname", "temperature"] BASE_CONFIG = { diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py index 1a8c94e1712ccd..081616a1406476 100644 --- a/tests/components/buienradar/test_weather.py +++ b/tests/components/buienradar/test_weather.py @@ -2,7 +2,6 @@ from homeassistant.components import weather from homeassistant.setup import async_setup_component - # Example config snippet from documentation. BASE_CONFIG = { "weather": [ From c78773970b8ee1fe084c5cc76b71639d6c594cd1 Mon Sep 17 00:00:00 2001 From: James Nimmo Date: Mon, 9 Dec 2019 03:09:16 +1300 Subject: [PATCH 2197/3953] Add IntesisHome Climate Platform (#25364) * Add IntesisHome Climate Platform * Add support for IntesisHome and Airconwithme devices * Implement requested changes from PR review * Improve error handling for IntesisHome component * Fix snake-case naming style * Update exception logging --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/intesishome/__init__.py | 1 + .../components/intesishome/climate.py | 407 ++++++++++++++++++ .../components/intesishome/manifest.json | 8 + requirements_all.txt | 3 + 6 files changed, 421 insertions(+) create mode 100755 homeassistant/components/intesishome/__init__.py create mode 100755 homeassistant/components/intesishome/climate.py create mode 100755 homeassistant/components/intesishome/manifest.json diff --git a/.coveragerc b/.coveragerc index 7f519f8970a079..778af8db89ad82 100644 --- a/.coveragerc +++ b/.coveragerc @@ -332,6 +332,7 @@ omit = homeassistant/components/influxdb/sensor.py homeassistant/components/insteon/* homeassistant/components/incomfort/* + homeassistant/components/intesishome/* homeassistant/components/ios/* homeassistant/components/iota/* homeassistant/components/iperf3/* diff --git a/CODEOWNERS b/CODEOWNERS index 7f5ff17a043980..472798f72a6b2c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -162,6 +162,7 @@ homeassistant/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core homeassistant/components/integration/* @dgomes homeassistant/components/intent/* @home-assistant/core +homeassistant/components/intesishome/* @jnimmo homeassistant/components/ios/* @robbiet480 homeassistant/components/iperf3/* @rohankapoorcom homeassistant/components/ipma/* @dgomes diff --git a/homeassistant/components/intesishome/__init__.py b/homeassistant/components/intesishome/__init__.py new file mode 100755 index 00000000000000..fd4ae1f03e341b --- /dev/null +++ b/homeassistant/components/intesishome/__init__.py @@ -0,0 +1 @@ +"""Intesishome platform.""" diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py new file mode 100755 index 00000000000000..669d1155d80467 --- /dev/null +++ b/homeassistant/components/intesishome/climate.py @@ -0,0 +1,407 @@ +"""Support for IntesisHome and airconwithme Smart AC Controllers.""" +import logging +from random import randrange + +from pyintesishome import IHAuthenticationError, IHConnectionError, IntesisHome +import voluptuous as vol + +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_DEVICE, + CONF_PASSWORD, + CONF_USERNAME, + TEMP_CELSIUS, +) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_call_later + +_LOGGER = logging.getLogger(__name__) + +IH_DEVICE_INTESISHOME = "IntesisHome" +IH_DEVICE_AIRCONWITHME = "airconwithme" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_DEVICE, default=IH_DEVICE_INTESISHOME): vol.In( + [IH_DEVICE_AIRCONWITHME, IH_DEVICE_INTESISHOME] + ), + } +) + +MAP_IH_TO_HVAC_MODE = { + "auto": HVAC_MODE_HEAT_COOL, + "cool": HVAC_MODE_COOL, + "dry": HVAC_MODE_DRY, + "fan": HVAC_MODE_FAN_ONLY, + "heat": HVAC_MODE_HEAT, + "off": HVAC_MODE_OFF, +} + +MAP_HVAC_MODE_TO_IH = {v: k for k, v in MAP_IH_TO_HVAC_MODE.items()} + +MAP_STATE_ICONS = { + HVAC_MODE_COOL: "mdi:snowflake", + HVAC_MODE_DRY: "mdi:water-off", + HVAC_MODE_FAN_ONLY: "mdi:fan", + HVAC_MODE_HEAT: "mdi:white-balance-sunny", + HVAC_MODE_HEAT_COOL: "mdi:cached", +} + +IH_HVAC_MODES = [ + HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, +] + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Create the IntesisHome climate devices.""" + ih_user = config[CONF_USERNAME] + ih_pass = config[CONF_PASSWORD] + device_type = config[CONF_DEVICE] + + controller = IntesisHome( + ih_user, + ih_pass, + hass.loop, + websession=async_get_clientsession(hass), + device_type=device_type, + ) + try: + await controller.poll_status() + except IHAuthenticationError: + _LOGGER.error("Invalid username or password") + return + except IHConnectionError: + _LOGGER.error("Error connecting to the %s server", device_type) + raise PlatformNotReady + + ih_devices = controller.get_devices() + if ih_devices: + async_add_entities( + [ + IntesisAC(ih_device_id, device, controller) + for ih_device_id, device in ih_devices.items() + ], + True, + ) + else: + _LOGGER.error( + "Error getting device list from %s API: %s", + device_type, + controller.error_message, + ) + await controller.stop() + + +class IntesisAC(ClimateDevice): + """Represents an Intesishome air conditioning device.""" + + def __init__(self, ih_device_id, ih_device, controller): + """Initialize the thermostat.""" + self._controller = controller + self._device_id = ih_device_id + self._ih_device = ih_device + self._device_name = ih_device.get("name") + self._device_type = controller.device_type + self._connected = None + self._setpoint_step = 1 + self._current_temp = None + self._max_temp = None + self._min_temp = None + self._target_temp = None + self._outdoor_temp = None + self._run_hours = None + self._rssi = None + self._swing = None + self._swing_list = None + self._vvane = None + self._hvane = None + self._power = False + self._fan_speed = None + self._hvac_mode = None + self._fan_modes = controller.get_fan_speed_list(ih_device_id) + self._support = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE + self._swing_list = [SWING_OFF] + + if ih_device.get("config_vertical_vanes"): + self._swing_list.append(SWING_VERTICAL) + if ih_device.get("config_horizontal_vanes"): + self._swing_list.append(SWING_HORIZONTAL) + if len(self._swing_list) == 3: + self._swing_list.append(SWING_BOTH) + self._support |= SUPPORT_SWING_MODE + elif len(self._swing_list) == 2: + self._support |= SUPPORT_SWING_MODE + + async def async_added_to_hass(self): + """Subscribe to event updates.""" + _LOGGER.debug("Added climate device with state: %s", repr(self._ih_device)) + await self._controller.add_update_callback(self.async_update_callback) + try: + await self._controller.connect() + except IHConnectionError as ex: + _LOGGER.error("Exception connecting to IntesisHome: %s", ex) + + @property + def name(self): + """Return the name of the AC device.""" + return self._device_name + + @property + def temperature_unit(self): + """Intesishome API uses celsius on the backend.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + attrs = {} + if len(self._swing_list) > 1: + attrs["vertical_vane"] = self._vvane + attrs["horizontal_vane"] = self._hvane + if self._outdoor_temp: + attrs["outdoor_temp"] = self._outdoor_temp + return attrs + + @property + def unique_id(self): + """Return unique ID for this device.""" + return self._device_id + + @property + def target_temperature_step(self) -> float: + """Return whether setpoint should be whole or half degree precision.""" + return self._setpoint_step + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + hvac_mode = kwargs.get(ATTR_HVAC_MODE) + + if hvac_mode: + await self.async_set_hvac_mode(hvac_mode) + + if temperature: + _LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature) + await self._controller.set_temperature(self._device_id, temperature) + self._target_temp = temperature + + # Write updated temperature to HA state to avoid flapping (API confirmation is slow) + self.async_write_ha_state() + + async def async_set_hvac_mode(self, hvac_mode): + """Set operation mode.""" + _LOGGER.debug("Setting %s to %s mode", self._device_type, hvac_mode) + if hvac_mode == HVAC_MODE_OFF: + self._power = False + await self._controller.set_power_off(self._device_id) + # Write changes to HA, API can be slow to push changes + self.async_write_ha_state() + return + + # First check device is turned on + if not self._controller.is_on(self._device_id): + self._power = True + await self._controller.set_power_on(self._device_id) + + # Set the mode + await self._controller.set_mode(self._device_id, MAP_HVAC_MODE_TO_IH[hvac_mode]) + + # Send the temperature again in case changing modes has changed it + if self._target_temp: + await self._controller.set_temperature(self._device_id, self._target_temp) + + # Updates can take longer than 2 seconds, so update locally + self._hvac_mode = hvac_mode + self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode): + """Set fan mode (from quiet, low, medium, high, auto).""" + await self._controller.set_fan_speed(self._device_id, fan_mode) + + # Updates can take longer than 2 seconds, so update locally + self._fan_speed = fan_mode + self.async_write_ha_state() + + async def async_set_swing_mode(self, swing_mode): + """Set the vertical vane.""" + if swing_mode == SWING_OFF: + await self._controller.set_vertical_vane(self._device_id, "auto/stop") + await self._controller.set_horizontal_vane(self._device_id, "auto/stop") + elif swing_mode == SWING_BOTH: + await self._controller.set_vertical_vane(self._device_id, "swing") + await self._controller.set_horizontal_vane(self._device_id, "swing") + elif swing_mode == SWING_HORIZONTAL: + await self._controller.set_vertical_vane(self._device_id, "auto/stop") + await self._controller.set_horizontal_vane(self._device_id, "swing") + elif swing_mode == SWING_VERTICAL: + await self._controller.set_vertical_vane(self._device_id, "swing") + await self._controller.set_horizontal_vane(self._device_id, "auto/stop") + self._swing = swing_mode + + async def async_update(self): + """Copy values from controller dictionary to climate device.""" + # Update values from controller's device dictionary + self._connected = self._controller.is_connected + self._current_temp = self._controller.get_temperature(self._device_id) + self._fan_speed = self._controller.get_fan_speed(self._device_id) + self._power = self._controller.is_on(self._device_id) + self._min_temp = self._controller.get_min_setpoint(self._device_id) + self._max_temp = self._controller.get_max_setpoint(self._device_id) + self._rssi = self._controller.get_rssi(self._device_id) + self._run_hours = self._controller.get_run_hours(self._device_id) + self._target_temp = self._controller.get_setpoint(self._device_id) + self._outdoor_temp = self._controller.get_outdoor_temperature(self._device_id) + + # Operation mode + mode = self._controller.get_mode(self._device_id) + self._hvac_mode = MAP_IH_TO_HVAC_MODE.get(mode) + + # Swing mode + # Climate module only supports one swing setting. + self._vvane = self._controller.get_vertical_swing(self._device_id) + self._hvane = self._controller.get_horizontal_swing(self._device_id) + + if self._vvane == "swing" and self._hvane == "swing": + self._swing = SWING_BOTH + elif self._vvane == "swing": + self._swing = SWING_VERTICAL + elif self._hvane == "swing": + self._swing = SWING_HORIZONTAL + else: + self._swing = SWING_OFF + + async def async_will_remove_from_hass(self): + """Shutdown the controller when the device is being removed.""" + await self._controller.stop() + + @property + def icon(self): + """Return the icon for the current state.""" + icon = None + if self._power: + icon = MAP_STATE_ICONS.get(self._hvac_mode) + return icon + + async def async_update_callback(self, device_id=None): + """Let HA know there has been an update from the controller.""" + # Track changes in connection state + if not self._controller.is_connected and self._connected: + # Connection has dropped + self._connected = False + reconnect_minutes = 1 + randrange(10) + _LOGGER.error( + "Connection to %s API was lost. Reconnecting in %i minutes", + self._device_type, + reconnect_minutes, + ) + # Schedule reconnection + async_call_later( + self.hass, reconnect_minutes * 60, self._controller.connect() + ) + + if self._controller.is_connected and not self._connected: + # Connection has been restored + self._connected = True + _LOGGER.debug("Connection to %s API was restored", self._device_type) + + if not device_id or self._device_id == device_id: + # Update all devices if no device_id was specified + _LOGGER.debug( + "%s API sent a status update for device %s", + self._device_type, + device_id, + ) + self.async_schedule_update_ha_state(True) + + @property + def min_temp(self): + """Return the minimum temperature for the current mode of operation.""" + return self._min_temp + + @property + def max_temp(self): + """Return the maximum temperature for the current mode of operation.""" + return self._max_temp + + @property + def should_poll(self): + """Poll for updates if pyIntesisHome doesn't have a socket open.""" + return False + + @property + def hvac_modes(self): + """List of available operation modes.""" + return IH_HVAC_MODES + + @property + def fan_mode(self): + """Return whether the fan is on.""" + return self._fan_speed + + @property + def swing_mode(self): + """Return current swing mode.""" + return self._swing + + @property + def fan_modes(self): + """List of available fan modes.""" + return self._fan_modes + + @property + def swing_modes(self): + """List of available swing positions.""" + return self._swing_list + + @property + def available(self) -> bool: + """If the device hasn't been able to connect, mark as unavailable.""" + return self._connected or self._connected is None + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temp + + @property + def hvac_mode(self): + """Return the current mode of operation if unit is on.""" + if self._power: + return self._hvac_mode + return HVAC_MODE_OFF + + @property + def target_temperature(self): + """Return the current setpoint temperature if unit is on.""" + return self._target_temp + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._support diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json new file mode 100755 index 00000000000000..025d08ac548e88 --- /dev/null +++ b/homeassistant/components/intesishome/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "intesishome", + "name": "IntesisHome", + "documentation": "https://www.home-assistant.io/integrations/intesishome", + "dependencies": [], + "codeowners": ["@jnimmo"], + "requirements": ["pyintesishome==1.5"] +} diff --git a/requirements_all.txt b/requirements_all.txt index 992069c5045f1c..a50325ffd19c18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1285,6 +1285,9 @@ pyialarm==0.3 # homeassistant.components.icloud pyicloud==0.9.1 +# homeassistant.components.intesishome +pyintesishome==1.5 + # homeassistant.components.ipma pyipma==1.2.1 From 94dec483e91e8a0f606cd1b2575322e7a2c07bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Beye?= Date: Sun, 8 Dec 2019 15:18:46 +0100 Subject: [PATCH 2198/3953] Don't escape command parameters (#29504) * Don't escape command parameters Escaping should only be done when using the tcp socket cli interface which we aren't. * Updated comment to reflect the changes --- homeassistant/components/squeezebox/media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index b3fb82591c9fe3..72a5772a14d9a0 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -538,11 +538,11 @@ def async_call_method(self, command, parameters=None): """ Call Squeezebox JSON/RPC method. - Escaped optional parameters are added to the command to form the list - of positional parameters (p0, p1..., pN) passed to JSON/RPC server. + Additional parameters are added to the command to form the list of + positional parameters (p0, p1..., pN) passed to JSON/RPC server. """ all_params = [command] if parameters: for parameter in parameters: - all_params.append(urllib.parse.quote(parameter, safe="+:=/?")) + all_params.append(parameter) return self.async_query(*all_params) From c4794572d43c5f420fd7ea7548a0b097ca7513d5 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 8 Dec 2019 20:13:10 +0530 Subject: [PATCH 2199/3953] Fix file permission (#29635) --- homeassistant/components/dsmr_reader/__init__.py | 0 homeassistant/components/dsmr_reader/manifest.json | 0 homeassistant/components/dsmr_reader/sensor.py | 0 homeassistant/components/here_travel_time/__init__.py | 0 homeassistant/components/here_travel_time/manifest.json | 0 homeassistant/components/here_travel_time/sensor.py | 0 homeassistant/components/tahoma/cover.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 homeassistant/components/dsmr_reader/__init__.py mode change 100755 => 100644 homeassistant/components/dsmr_reader/manifest.json mode change 100755 => 100644 homeassistant/components/dsmr_reader/sensor.py mode change 100755 => 100644 homeassistant/components/here_travel_time/__init__.py mode change 100755 => 100644 homeassistant/components/here_travel_time/manifest.json mode change 100755 => 100644 homeassistant/components/here_travel_time/sensor.py mode change 100755 => 100644 homeassistant/components/tahoma/cover.py diff --git a/homeassistant/components/dsmr_reader/__init__.py b/homeassistant/components/dsmr_reader/__init__.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json old mode 100755 new mode 100644 diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json old mode 100755 new mode 100644 diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py old mode 100755 new mode 100644 From b0d0060643699f3f0b1c28b62bf5ff45be4b21dc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 15:44:04 +0100 Subject: [PATCH 2200/3953] sort imports according to PEP8 for axis (#29621) --- homeassistant/components/axis/device.py | 3 +-- tests/components/axis/test_binary_sensor.py | 3 +-- tests/components/axis/test_camera.py | 4 +--- tests/components/axis/test_config_flow.py | 2 +- tests/components/axis/test_device.py | 4 ++-- tests/components/axis/test_init.py | 4 ++-- tests/components/axis/test_switch.py | 5 ++--- 7 files changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index e42a758f3c414c..b05c5b2fed0c41 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -1,8 +1,8 @@ """Axis network device abstraction.""" import asyncio -import async_timeout +import async_timeout import axis from axis.streammanager import SIGNAL_PLAYING @@ -21,7 +21,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import CONF_CAMERA, CONF_EVENTS, CONF_MODEL, DOMAIN, LOGGER - from .errors import AuthenticationRequired, CannotConnect diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 8e5a6f9675df56..ca3e984c993d18 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -4,9 +4,8 @@ from homeassistant import config_entries from homeassistant.components import axis -from homeassistant.setup import async_setup_component - import homeassistant.components.binary_sensor as binary_sensor +from homeassistant.setup import async_setup_component EVENTS = [ { diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 027dc42748e396..67ca7e3690a137 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -4,10 +4,8 @@ from homeassistant import config_entries from homeassistant.components import axis -from homeassistant.setup import async_setup_component - import homeassistant.components.camera as camera - +from homeassistant.setup import async_setup_component ENTRY_CONFIG = { axis.CONF_DEVICE: { diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 5aec416961d644..a29c270e0b8c46 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -4,7 +4,7 @@ from homeassistant.components import axis from homeassistant.components.axis import config_flow -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_configured_devices(hass): diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index b15e9e68adaca5..a9f38cc4f3ab3b 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -3,11 +3,11 @@ import pytest -from tests.common import mock_coro, MockConfigEntry - from homeassistant.components.axis import device, errors from homeassistant.components.axis.camera import AxisCamera +from tests.common import MockConfigEntry, mock_coro + DEVICE_DATA = { device.CONF_HOST: "1.2.3.4", device.CONF_USERNAME: "username", diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index b1317eb88e9859..831d1d6ea080e7 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -1,10 +1,10 @@ """Test Axis component setup process.""" from unittest.mock import Mock, patch -from homeassistant.setup import async_setup_component from homeassistant.components import axis +from homeassistant.setup import async_setup_component -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_setup(hass): diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 3469106c436637..406e3170ab203c 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,12 +1,11 @@ """Axis switch platform tests.""" -from unittest.mock import call as mock_call, Mock +from unittest.mock import Mock, call as mock_call from homeassistant import config_entries from homeassistant.components import axis -from homeassistant.setup import async_setup_component - import homeassistant.components.switch as switch +from homeassistant.setup import async_setup_component EVENTS = [ { From 00dc7216093abd099b1e4a58a14876cc9ed2896e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 16:33:22 +0100 Subject: [PATCH 2201/3953] sort imports according to PEP8 for hassio (#29634) --- homeassistant/components/hassio/__init__.py | 4 ++-- homeassistant/components/hassio/addon_panel.py | 2 +- homeassistant/components/hassio/auth.py | 6 +++--- homeassistant/components/hassio/discovery.py | 2 +- homeassistant/components/hassio/http.py | 2 +- homeassistant/components/hassio/ingress.py | 2 +- tests/components/hassio/conftest.py | 7 ++++--- tests/components/hassio/test_addon_panel.py | 2 +- tests/components/hassio/test_auth.py | 2 +- tests/components/hassio/test_discovery.py | 4 ++-- tests/components/hassio/test_init.py | 7 +++---- 11 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e0c0a57375a36b..dab0fadd922470 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -10,9 +10,9 @@ import homeassistant.config as conf_util from homeassistant.const import ( ATTR_NAME, + EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, - EVENT_CORE_CONFIG_UPDATE, ) from homeassistant.core import DOMAIN as HASS_DOMAIN, callback from homeassistant.exceptions import HomeAssistantError @@ -20,8 +20,8 @@ from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow -from .auth import async_setup_auth_view from .addon_panel import async_setup_addon_panel +from .auth import async_setup_auth_view from .discovery import async_setup_discovery_view from .handler import HassIO, HassioAPIError from .http import HassIOView diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index b60c864a893acf..cb509cb19a1210 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -7,7 +7,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.helpers.typing import HomeAssistantType -from .const import ATTR_PANELS, ATTR_TITLE, ATTR_ICON, ATTR_ADMIN, ATTR_ENABLE +from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_ICON, ATTR_PANELS, ATTR_TITLE from .handler import HassioAPIError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 19e4c63b5a5c32..800801b435073e 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -1,18 +1,18 @@ """Implement the auth feature from Hass.io for Add-ons.""" +from ipaddress import ip_address import logging import os -from ipaddress import ip_address -import voluptuous as vol from aiohttp import web from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 55336735133cea..fc6efbe0e58154 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -5,9 +5,9 @@ from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable +from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView from .const import ( ATTR_ADDON, diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 3b1b83745105ba..ddb9269219b9d3 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -7,7 +7,7 @@ import aiohttp from aiohttp import web -from aiohttp.hdrs import CONTENT_TYPE, CONTENT_LENGTH +from aiohttp.hdrs import CONTENT_LENGTH, CONTENT_TYPE from aiohttp.web_exceptions import HTTPBadGateway import async_timeout diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 53235f80dcad55..c69d2078468fd7 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -1,8 +1,8 @@ """Hass.io Add-on ingress service.""" import asyncio +from ipaddress import ip_address import logging import os -from ipaddress import ip_address from typing import Dict, Union import aiohttp diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 0e246cf1b46f2c..d7ef853012e4a9 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -1,16 +1,17 @@ """Fixtures for Hass.io.""" import os -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest +from homeassistant.components.hassio.handler import HassIO, HassioAPIError from homeassistant.core import CoreState from homeassistant.setup import async_setup_component -from homeassistant.components.hassio.handler import HassIO, HassioAPIError -from tests.common import mock_coro from . import HASSIO_TOKEN +from tests.common import mock_coro + @pytest.fixture def hassio_env(): diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index 480df5089682fd..d2ad673111d8ad 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -1,5 +1,5 @@ """Test add-on panel.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index 1fb6d32ccf73eb..c7fe3459e41ade 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,5 +1,5 @@ """The tests for the hassio component.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch from homeassistant.exceptions import HomeAssistantError diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index a1b4ae2e900cfc..2a2fdc4deaa435 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,9 +1,9 @@ """Test config flow.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch -from homeassistant.setup import async_setup_component from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.setup import async_setup_component from tests.common import mock_coro diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 3d27931896f04d..c1e3d7ab2bf038 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1,18 +1,17 @@ """The tests for the hassio component.""" import asyncio import os -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest from homeassistant.auth.const import GROUP_ID_ADMIN -from homeassistant.setup import async_setup_component -from homeassistant.components.hassio import STORAGE_KEY from homeassistant.components import frontend +from homeassistant.components.hassio import STORAGE_KEY +from homeassistant.setup import async_setup_component from tests.common import mock_coro - MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} From 57a3f7d5c8ad3dc71b6c29767bd625096767e7d5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 8 Dec 2019 16:53:34 +0100 Subject: [PATCH 2202/3953] Pass in parameters explicitly to DeconzSession (#29617) Dont pass in loop to DeconzSession Services will use new refresh state method --- homeassistant/components/deconz/const.py | 8 +-- homeassistant/components/deconz/gateway.py | 45 ++++++------ homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/deconz/services.py | 22 ++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_binary_sensor.py | 13 +++- tests/components/deconz/test_climate.py | 72 ++++++++++++------- tests/components/deconz/test_cover.py | 27 ++++--- tests/components/deconz/test_gateway.py | 14 ++-- tests/components/deconz/test_init.py | 4 +- tests/components/deconz/test_light.py | 43 ++++++----- tests/components/deconz/test_scene.py | 6 +- tests/components/deconz/test_sensor.py | 23 ++++-- tests/components/deconz/test_services.py | 28 ++++---- tests/components/deconz/test_switch.py | 45 ++++++++---- 16 files changed, 219 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index ad23a564272d9c..a663f99bf736f3 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -26,10 +26,10 @@ "switch", ] -NEW_GROUP = "group" -NEW_LIGHT = "light" -NEW_SCENE = "scene" -NEW_SENSOR = "sensor" +NEW_GROUP = "groups" +NEW_LIGHT = "lights" +NEW_SCENE = "scenes" +NEW_SENSOR = "sensors" NEW_DEVICE = { NEW_GROUP: "deconz_new_group_{}", diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 0c77285a6fe598..083af2dca6fdc7 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -4,7 +4,7 @@ import async_timeout from pydeconz import DeconzSession, errors -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -42,13 +42,13 @@ def get_gateway_from_config_entry(hass, config_entry): class DeconzGateway: """Manages a single deCONZ gateway.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry) -> None: """Initialize the system.""" self.hass = hass self.config_entry = config_entry + self.available = True self.api = None - self.deconz_ids = {} self.events = [] self.listeners = [] @@ -77,7 +77,7 @@ def option_allow_deconz_groups(self) -> bool: CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS ) - async def async_update_device_registry(self): + async def async_update_device_registry(self) -> None: """Update device registry.""" device_registry = await self.hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( @@ -90,7 +90,7 @@ async def async_update_device_registry(self): sw_version=self.api.config.swversion, ) - async def async_setup(self): + async def async_setup(self) -> bool: """Set up a deCONZ gateway.""" hass = self.hass @@ -105,8 +105,8 @@ async def async_setup(self): except CannotConnect: raise ConfigEntryNotReady - except Exception: # pylint: disable=broad-except - _LOGGER.error("Error connecting with deCONZ gateway") + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Error connecting with deCONZ gateway: %s", err) return False for component in SUPPORTED_PLATFORMS: @@ -124,7 +124,7 @@ async def async_setup(self): return True @staticmethod - async def async_new_address(hass, entry): + async def async_new_address(hass, entry) -> None: """Handle signals of gateway getting new address. This is a static method because a class method (bound method), @@ -137,23 +137,23 @@ async def async_new_address(hass, entry): gateway.api.start() @property - def signal_reachable(self): + def signal_reachable(self) -> str: """Gateway specific event to signal a change in connection status.""" return f"deconz-reachable-{self.bridgeid}" @callback - def async_connection_status_callback(self, available): + def async_connection_status_callback(self, available) -> None: """Handle signals of gateway connection status.""" self.available = available async_dispatcher_send(self.hass, self.signal_reachable, True) @property - def signal_options_update(self): + def signal_options_update(self) -> str: """Event specific per deCONZ entry to signal new options.""" return f"deconz-options-{self.bridgeid}" @staticmethod - async def async_options_updated(hass, entry): + async def async_options_updated(hass, entry) -> None: """Triggered by config entry options updates.""" gateway = get_gateway_from_config_entry(hass, entry) @@ -161,12 +161,12 @@ async def async_options_updated(hass, entry): async_dispatcher_send(hass, gateway.signal_options_update, registry) @callback - def async_signal_new_device(self, device_type): + def async_signal_new_device(self, device_type) -> str: """Gateway specific event to signal new device.""" return NEW_DEVICE[device_type].format(self.bridgeid) @callback - def async_add_device_callback(self, device_type, device): + def async_add_device_callback(self, device_type, device) -> None: """Handle event of new device creation in deCONZ.""" if not isinstance(device, list): device = [device] @@ -175,7 +175,7 @@ def async_add_device_callback(self, device_type, device): ) @callback - def shutdown(self, event): + def shutdown(self, event) -> None: """Wrap the call to deconz.close. Used as an argument to EventBus.async_listen_once. @@ -206,20 +206,21 @@ async def async_reset(self): async def get_gateway( hass, config, async_add_device_callback, async_connection_status_callback -): +) -> DeconzSession: """Create a gateway object and verify configuration.""" session = aiohttp_client.async_get_clientsession(hass) deconz = DeconzSession( - hass.loop, session, - **config, + config[CONF_HOST], + config[CONF_PORT], + config[CONF_API_KEY], async_add_device=async_add_device_callback, connection_status=async_connection_status_callback, ) try: with async_timeout.timeout(10): - await deconz.async_load_parameters() + await deconz.initialize() return deconz except errors.Unauthorized: @@ -234,7 +235,7 @@ async def get_gateway( class DeconzEntityHandler: """Platform entity handler to help with updating disabled by.""" - def __init__(self, gateway): + def __init__(self, gateway) -> None: """Create an entity handler.""" self.gateway = gateway self._entities = [] @@ -246,12 +247,12 @@ def __init__(self, gateway): ) @callback - def add_entity(self, entity): + def add_entity(self, entity) -> None: """Add a new entity to handler.""" self._entities.append(entity) @callback - def update_entity_registry(self, entity_registry): + def update_entity_registry(self, entity_registry) -> None: """Update entity registry disabled by status.""" for entity in self._entities: diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 64902002600a6d..30b00600331535 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==64" + "pydeconz==65" ], "ssdp": [ { diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 8efdc2718bcc97..9d133acdb1dfb5 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -4,7 +4,15 @@ from homeassistant.helpers import config_validation as cv from .config_flow import get_master_gateway -from .const import _LOGGER, CONF_BRIDGEID, DOMAIN +from .const import ( + _LOGGER, + CONF_BRIDGEID, + DOMAIN, + NEW_GROUP, + NEW_LIGHT, + NEW_SCENE, + NEW_SENSOR, +) DECONZ_SERVICES = "deconz_services" @@ -105,7 +113,7 @@ async def async_configure_service(hass, data): _LOGGER.error("Could not find the entity %s", entity_id) return - await gateway.api.async_put_state(field, data) + await gateway.api.request("put", field, json=data) async def async_refresh_devices_service(hass, data): @@ -119,10 +127,10 @@ async def async_refresh_devices_service(hass, data): scenes = set(gateway.api.scenes.keys()) sensors = set(gateway.api.sensors.keys()) - await gateway.api.async_load_parameters() + await gateway.api.refresh_state() gateway.async_add_device_callback( - "group", + NEW_GROUP, [ group for group_id, group in gateway.api.groups.items() @@ -131,7 +139,7 @@ async def async_refresh_devices_service(hass, data): ) gateway.async_add_device_callback( - "light", + NEW_LIGHT, [ light for light_id, light in gateway.api.lights.items() @@ -140,7 +148,7 @@ async def async_refresh_devices_service(hass, data): ) gateway.async_add_device_callback( - "scene", + NEW_SCENE, [ scene for scene_id, scene in gateway.api.scenes.items() @@ -149,7 +157,7 @@ async def async_refresh_devices_service(hass, data): ) gateway.async_add_device_callback( - "sensor", + NEW_SENSOR, [ sensor for sensor_id, sensor in gateway.api.sensors.items() diff --git a/requirements_all.txt b/requirements_all.txt index a50325ffd19c18..360898df055640 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1180,7 +1180,7 @@ pydaikin==1.6.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==64 +pydeconz==65 # homeassistant.components.delijn pydelijn==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5fe6c4f6143190..fff9af5b32df7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -406,7 +406,7 @@ pycoolmasternet==0.0.4 pydaikin==1.6.1 # homeassistant.components.deconz -pydeconz==64 +pydeconz==65 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 2f42193291c8dd..c340018b641889 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -95,7 +95,14 @@ async def test_binary_sensors(hass): vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") assert vibration_sensor.state == "on" - gateway.api.sensors["1"].async_update({"state": {"presence": True}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"presence": True}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() presence_sensor = hass.states.get("binary_sensor.presence_sensor") @@ -143,14 +150,14 @@ async def test_add_new_binary_sensor(hass): ) assert len(gateway.deconz_ids) == 0 - state_added = { + state_added_event = { "t": "event", "e": "added", "r": "sensors", "id": "1", "sensor": deepcopy(SENSORS["1"]), } - gateway.api.async_event_handler(state_added) + gateway.api.async_event_handler(state_added_event) await hass.async_block_till_done() assert "binary_sensor.presence_sensor" in gateway.deconz_ids diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index cee91f00c4283b..9536929ff3cda5 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -94,21 +94,41 @@ async def test_climate_devices(hass): clip_thermostat = hass.states.get("climate.clip_thermostat") assert clip_thermostat is None - thermostat_device = gateway.api.sensors["1"] - - thermostat_device.async_update({"config": {"mode": "off"}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "config": {"mode": "off"}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "off" - thermostat_device.async_update({"config": {"mode": "other"}, "state": {"on": True}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "config": {"mode": "other"}, + "state": {"on": True}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "heat" - thermostat_device.async_update({"state": {"on": False}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"on": False}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") @@ -116,9 +136,9 @@ async def test_climate_devices(hass): # Verify service calls - with patch.object( - thermostat_device, "_async_set_callback", return_value=True - ) as set_callback: + thermostat_device = gateway.api.sensors["1"] + + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: await hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE, @@ -126,11 +146,11 @@ async def test_climate_devices(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/sensors/1/config", {"mode": "auto"}) + set_callback.assert_called_with( + "put", "/sensors/1/config", json={"mode": "auto"} + ) - with patch.object( - thermostat_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: await hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE, @@ -138,29 +158,31 @@ async def test_climate_devices(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/sensors/1/config", {"mode": "heat"}) + set_callback.assert_called_with( + "put", "/sensors/1/config", json={"mode": "heat"} + ) - with patch.object( - thermostat_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: await hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE, {"entity_id": "climate.thermostat", "hvac_mode": "off"}, blocking=True, ) - set_callback.assert_called_with("/sensors/1/config", {"mode": "off"}) + set_callback.assert_called_with( + "put", "/sensors/1/config", json={"mode": "off"} + ) - with patch.object( - thermostat_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(thermostat_device, "_request", return_value=True) as set_callback: await hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_TEMPERATURE, {"entity_id": "climate.thermostat", "temperature": 20}, blocking=True, ) - set_callback.assert_called_with("/sensors/1/config", {"heatsetpoint": 2000.0}) + set_callback.assert_called_with( + "put", "/sensors/1/config", json={"heatsetpoint": 2000.0} + ) await gateway.async_reset() @@ -212,14 +234,14 @@ async def test_verify_state_update(hass): thermostat = hass.states.get("climate.thermostat") assert thermostat.state == "auto" - state_update = { + state_changed_event = { "t": "event", "e": "changed", "r": "sensors", "id": "1", "state": {"on": False}, } - gateway.api.async_event_handler(state_update) + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() thermostat = hass.states.get("climate.thermostat") @@ -235,14 +257,14 @@ async def test_add_new_climate_device(hass): ) assert len(gateway.deconz_ids) == 0 - state_added = { + state_added_event = { "t": "event", "e": "added", "r": "sensors", "id": "1", "sensor": deepcopy(SENSORS["1"]), } - gateway.api.async_event_handler(state_added) + gateway.api.async_event_handler(state_added_event) await hass.async_block_till_done() assert "climate.thermostat" in gateway.deconz_ids diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 5c7ee48a78a2eb..31819888404d81 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -73,16 +73,23 @@ async def test_cover(hass): level_controllable_cover = hass.states.get("cover.level_controllable_cover") assert level_controllable_cover.state == "open" - level_controllable_cover_device = gateway.api.lights["1"] - - level_controllable_cover_device.async_update({"state": {"on": True}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "1", + "state": {"on": True}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() level_controllable_cover = hass.states.get("cover.level_controllable_cover") assert level_controllable_cover.state == "closed" + level_controllable_cover_device = gateway.api.lights["1"] + with patch.object( - level_controllable_cover_device, "_async_set_callback", return_value=True + level_controllable_cover_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( cover.DOMAIN, @@ -91,10 +98,10 @@ async def test_cover(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"on": False}) + set_callback.assert_called_with("put", "/lights/1/state", json={"on": False}) with patch.object( - level_controllable_cover_device, "_async_set_callback", return_value=True + level_controllable_cover_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( cover.DOMAIN, @@ -103,10 +110,12 @@ async def test_cover(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"on": True, "bri": 255}) + set_callback.assert_called_with( + "put", "/lights/1/state", json={"on": True, "bri": 255} + ) with patch.object( - level_controllable_cover_device, "_async_set_callback", return_value=True + level_controllable_cover_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( cover.DOMAIN, @@ -115,7 +124,7 @@ async def test_cover(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"bri_inc": 0}) + set_callback.assert_called_with("put", "/lights/1/state", json={"bri_inc": 0}) await gateway.async_reset() diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index b98681b6fc9697..f9290cc0e055b9 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -51,8 +51,12 @@ async def setup_deconz_integration(hass, config, options, get_state_response): entry_id="1", ) + for resource in ("groups", "lights", "sensors"): + if resource not in get_state_response: + get_state_response[resource] = {} + with patch( - "pydeconz.DeconzSession.async_get_state", return_value=get_state_response + "pydeconz.DeconzSession.request", return_value=get_state_response ), patch("pydeconz.DeconzSession.start", return_value=True): await deconz.async_setup_entry(hass, config_entry) await hass.async_block_till_done() @@ -172,15 +176,14 @@ async def test_reset_after_successful_setup(hass): async def test_get_gateway(hass): """Successful call.""" - with patch("pydeconz.DeconzSession.async_load_parameters", return_value=True): + with patch("pydeconz.DeconzSession.initialize", return_value=True): assert await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) async def test_get_gateway_fails_unauthorized(hass): """Failed call.""" with patch( - "pydeconz.DeconzSession.async_load_parameters", - side_effect=pydeconz.errors.Unauthorized, + "pydeconz.DeconzSession.initialize", side_effect=pydeconz.errors.Unauthorized, ), pytest.raises(deconz.errors.AuthenticationRequired): assert ( await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) @@ -191,8 +194,7 @@ async def test_get_gateway_fails_unauthorized(hass): async def test_get_gateway_fails_cannot_connect(hass): """Failed call.""" with patch( - "pydeconz.DeconzSession.async_load_parameters", - side_effect=pydeconz.errors.RequestError, + "pydeconz.DeconzSession.initialize", side_effect=pydeconz.errors.RequestError, ), pytest.raises(deconz.errors.CannotConnect): assert ( await deconz.gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 986e01a1599bc9..d21859197be49c 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -41,7 +41,7 @@ async def test_setup_entry_fails(hass): deconz.config_flow.CONF_PORT: ENTRY1_PORT, deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, } - with patch("pydeconz.DeconzSession.async_load_parameters", side_effect=Exception): + with patch("pydeconz.DeconzSession.initialize", side_effect=Exception): await deconz.async_setup_entry(hass, entry) @@ -54,7 +54,7 @@ async def test_setup_entry_no_available_bridge(hass): deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, } with patch( - "pydeconz.DeconzSession.async_load_parameters", side_effect=asyncio.TimeoutError + "pydeconz.DeconzSession.initialize", side_effect=asyncio.TimeoutError ), pytest.raises(ConfigEntryNotReady): await deconz.async_setup_entry(hass, entry) diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 14dc5cc8eac1c7..438b2209b1da2f 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -117,17 +117,22 @@ async def test_lights_and_groups(hass): empty_group = hass.states.get("light.empty_group") assert empty_group is None - rgb_light_device = gateway.api.lights["1"] - - rgb_light_device.async_update({"state": {"on": False}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "1", + "state": {"on": False}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() rgb_light = hass.states.get("light.rgb_light") assert rgb_light.state == "off" - with patch.object( - rgb_light_device, "_async_set_callback", return_value=True - ) as set_callback: + rgb_light_device = gateway.api.lights["1"] + + with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: await hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_ON, @@ -143,8 +148,9 @@ async def test_lights_and_groups(hass): ) await hass.async_block_till_done() set_callback.assert_called_with( + "put", "/lights/1/state", - { + json={ "ct": 2500, "bri": 200, "transitiontime": 50, @@ -153,9 +159,7 @@ async def test_lights_and_groups(hass): }, ) - with patch.object( - rgb_light_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: await hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_ON, @@ -169,13 +173,12 @@ async def test_lights_and_groups(hass): ) await hass.async_block_till_done() set_callback.assert_called_with( + "put", "/lights/1/state", - {"xy": (0.411, 0.351), "alert": "lselect", "effect": "none"}, + json={"xy": (0.411, 0.351), "alert": "lselect", "effect": "none"}, ) - with patch.object( - rgb_light_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: await hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_OFF, @@ -184,12 +187,12 @@ async def test_lights_and_groups(hass): ) await hass.async_block_till_done() set_callback.assert_called_with( - "/lights/1/state", {"bri": 0, "transitiontime": 50, "alert": "select"} + "put", + "/lights/1/state", + json={"bri": 0, "transitiontime": 50, "alert": "select"}, ) - with patch.object( - rgb_light_device, "_async_set_callback", return_value=True - ) as set_callback: + with patch.object(rgb_light_device, "_request", return_value=True) as set_callback: await hass.services.async_call( light.DOMAIN, light.SERVICE_TURN_OFF, @@ -197,7 +200,9 @@ async def test_lights_and_groups(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"alert": "lselect"}) + set_callback.assert_called_with( + "put", "/lights/1/state", json={"alert": "lselect"} + ) await gateway.async_reset() diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index dcc8ba500c32f7..ca7493a41f1e2d 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -60,14 +60,12 @@ async def test_scenes(hass): group_scene = gateway.api.groups["1"].scenes["1"] - with patch.object( - group_scene, "_async_set_state_callback", return_value=True - ) as set_callback: + with patch.object(group_scene, "_request", return_value=True) as set_callback: await hass.services.async_call( "scene", "turn_on", {"entity_id": "scene.light_group_scene"}, blocking=True ) await hass.async_block_till_done() - set_callback.assert_called_with("/groups/1/scenes/1/recall", {}) + set_callback.assert_called_with("put", "/groups/1/scenes/1/recall", json={}) await gateway.async_reset() diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 7b6ae41086b8a0..b678ee136440a4 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -143,8 +143,23 @@ async def test_sensors(hass): consumption_sensor = hass.states.get("sensor.consumption_sensor") assert consumption_sensor.state == "0.002" - gateway.api.sensors["1"].async_update({"state": {"lightlevel": 2000}}) - gateway.api.sensors["4"].async_update({"config": {"battery": 75}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"lightlevel": 2000}, + } + gateway.api.async_event_handler(state_changed_event) + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "4", + "config": {"battery": 75}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() light_level_sensor = hass.states.get("sensor.light_level_sensor") @@ -219,14 +234,14 @@ async def test_add_new_sensor(hass): ) assert len(gateway.deconz_ids) == 0 - state_added = { + state_added_event = { "t": "event", "e": "added", "r": "sensors", "id": "1", "sensor": deepcopy(SENSORS["1"]), } - gateway.api.async_event_handler(state_added) + gateway.api.async_event_handler(state_added_event) await hass.async_block_till_done() assert "sensor.light_level_sensor" in gateway.deconz_ids diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 533d85eef7cbe3..fae0ed41d9304a 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -104,14 +104,14 @@ async def test_configure_service_with_field(hass): deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - with patch( - "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) - ) as put_state: + with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state: await hass.services.async_call( deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data ) await hass.async_block_till_done() - put_state.assert_called_with("/light/2", {"on": True, "attr1": 10, "attr2": 20}) + put_state.assert_called_with( + "put", "/light/2", json={"on": True, "attr1": 10, "attr2": 20} + ) async def test_configure_service_with_entity(hass): @@ -127,14 +127,14 @@ async def test_configure_service_with_entity(hass): deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - with patch( - "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) - ) as put_state: + with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state: await hass.services.async_call( deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data ) await hass.async_block_till_done() - put_state.assert_called_with("/light/1", {"on": True, "attr1": 10, "attr2": 20}) + put_state.assert_called_with( + "put", "/light/1", json={"on": True, "attr1": 10, "attr2": 20} + ) async def test_configure_service_with_entity_and_field(hass): @@ -151,15 +151,13 @@ async def test_configure_service_with_entity_and_field(hass): deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } - with patch( - "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) - ) as put_state: + with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state: await hass.services.async_call( deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data ) await hass.async_block_till_done() put_state.assert_called_with( - "/light/1/state", {"on": True, "attr1": 10, "attr2": 20} + "put", "/light/1/state", json={"on": True, "attr1": 10, "attr2": 20} ) @@ -191,9 +189,7 @@ async def test_configure_service_with_faulty_entity(hass): deconz.services.SERVICE_DATA: {}, } - with patch( - "pydeconz.DeconzSession.async_put_state", return_value=Mock(True) - ) as put_state: + with patch("pydeconz.DeconzSession.request", return_value=Mock(True)) as put_state: await hass.services.async_call( deconz.DOMAIN, deconz.services.SERVICE_CONFIGURE_DEVICE, service_data=data ) @@ -211,7 +207,7 @@ async def test_service_refresh_devices(hass): data = {deconz.CONF_BRIDGEID: BRIDGEID} with patch( - "pydeconz.DeconzSession.async_get_state", + "pydeconz.DeconzSession.request", return_value={"groups": GROUP, "lights": LIGHT, "sensors": SENSOR}, ): await hass.services.async_call( diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 262bd7001f5846..0c66ace56401d6 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -85,11 +85,22 @@ async def test_switches(hass): warning_device = hass.states.get("switch.warning_device") assert warning_device.state == "on" - on_off_switch_device = gateway.api.lights["1"] - warning_device_device = gateway.api.lights["3"] - - on_off_switch_device.async_update({"state": {"on": False}}) - warning_device_device.async_update({"state": {"alert": None}}) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "1", + "state": {"on": False}, + } + gateway.api.async_event_handler(state_changed_event) + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "3", + "state": {"alert": None}, + } + gateway.api.async_event_handler(state_changed_event) await hass.async_block_till_done() on_off_switch = hass.states.get("switch.on_off_switch") @@ -98,8 +109,10 @@ async def test_switches(hass): warning_device = hass.states.get("switch.warning_device") assert warning_device.state == "off" + on_off_switch_device = gateway.api.lights["1"] + with patch.object( - on_off_switch_device, "_async_set_callback", return_value=True + on_off_switch_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( switch.DOMAIN, @@ -108,10 +121,10 @@ async def test_switches(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"on": True}) + set_callback.assert_called_with("put", "/lights/1/state", json={"on": True}) with patch.object( - on_off_switch_device, "_async_set_callback", return_value=True + on_off_switch_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( switch.DOMAIN, @@ -120,10 +133,12 @@ async def test_switches(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/1/state", {"on": False}) + set_callback.assert_called_with("put", "/lights/1/state", json={"on": False}) + + warning_device_device = gateway.api.lights["3"] with patch.object( - warning_device_device, "_async_set_callback", return_value=True + warning_device_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( switch.DOMAIN, @@ -132,10 +147,12 @@ async def test_switches(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/3/state", {"alert": "lselect"}) + set_callback.assert_called_with( + "put", "/lights/3/state", json={"alert": "lselect"} + ) with patch.object( - warning_device_device, "_async_set_callback", return_value=True + warning_device_device, "_request", return_value=True ) as set_callback: await hass.services.async_call( switch.DOMAIN, @@ -144,7 +161,9 @@ async def test_switches(hass): blocking=True, ) await hass.async_block_till_done() - set_callback.assert_called_with("/lights/3/state", {"alert": "none"}) + set_callback.assert_called_with( + "put", "/lights/3/state", json={"alert": "none"} + ) await gateway.async_reset() From a38f3ac9c650ca8050ffdc51d974d3135a330ff0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 16:58:47 +0100 Subject: [PATCH 2203/3953] use isort to sort imports according to PEP8 for fan (#29632) --- homeassistant/components/fan/__init__.py | 10 +++++----- homeassistant/components/fan/device_action.py | 12 +++++++----- homeassistant/components/fan/device_condition.py | 8 +++++--- homeassistant/components/fan/device_trigger.py | 16 +++++++++------- homeassistant/components/fan/reproduce_state.py | 6 +++--- tests/components/fan/common.py | 6 +++--- tests/components/fan/test_device_action.py | 6 +++--- tests/components/fan/test_device_condition.py | 8 ++++---- tests/components/fan/test_device_trigger.py | 8 ++++---- tests/components/fan/test_init.py | 3 ++- 10 files changed, 45 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 51aecc3e7c2097..c3111a3aeb7b66 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -7,15 +7,15 @@ import voluptuous as vol from homeassistant.components import group -from homeassistant.const import SERVICE_TURN_ON, SERVICE_TOGGLE, SERVICE_TURN_OFF -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py index b26f632a775ea7..a5d35d741b640e 100644 --- a/homeassistant/components/fan/device_action.py +++ b/homeassistant/components/fan/device_action.py @@ -1,19 +1,21 @@ """Provides device automations for Fan.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - SERVICE_TURN_ON, + CONF_TYPE, SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import DOMAIN ACTION_TYPES = {"turn_on", "turn_off"} diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py index 8b567fcd4c9935..c69f28c10e9f2f 100644 --- a/homeassistant/components/fan/device_condition.py +++ b/homeassistant/components/fan/device_condition.py @@ -1,21 +1,23 @@ """Provide the device automations for Fan.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, STATE_OFF, STATE_ON, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_on", "is_off"} diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py index 3e917e0ae792e3..3bfeb5ee36b8b7 100644 --- a/homeassistant/components/fan/device_trigger.py +++ b/homeassistant/components/fan/device_trigger.py @@ -1,21 +1,23 @@ """Provides device automations for Fan.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - STATE_ON, + CONF_PLATFORM, + CONF_TYPE, STATE_OFF, + STATE_ON, ) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType -from homeassistant.components.automation import state, AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN TRIGGER_TYPES = {"turned_on", "turned_off"} diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index 1053861e2bf547..2692ac7ee5cb30 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -6,19 +6,19 @@ from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType from . import ( - DOMAIN, ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_SPEED, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_SPEED, diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index de645dac42e6d7..70a2c7e43d3aeb 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -5,8 +5,8 @@ """ from homeassistant.components.fan import ( ATTR_DIRECTION, - ATTR_SPEED, ATTR_OSCILLATING, + ATTR_SPEED, DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, @@ -14,9 +14,9 @@ ) from homeassistant.const import ( ATTR_ENTITY_ID, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py index 928fd353dd55a2..70da4bd1fca62e 100644 --- a/tests/components/fan/test_device_action.py +++ b/tests/components/fan/test_device_action.py @@ -1,18 +1,18 @@ """The tests for Fan device actions.""" import pytest -from homeassistant.components.fan import DOMAIN -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.fan import DOMAIN from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index ea87e36b636071..e665f9d5ddc26b 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -1,19 +1,19 @@ """The tests for Fan device conditions.""" import pytest -from homeassistant.components.fan import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.fan import DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index fa41749cf364e3..3d4f4229965660 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -1,19 +1,19 @@ """The tests for Fan device triggers.""" import pytest -from homeassistant.components.fan import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.fan import DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index fec4d5b2495e4f..316504381ece05 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -2,9 +2,10 @@ import unittest -from homeassistant.components.fan import FanEntity import pytest +from homeassistant.components.fan import FanEntity + class BaseFan(FanEntity): """Implementation of the abstract FanEntity.""" From d3f67c3841a586eefe8b69beb85e4a3f6f1755bf Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 17:29:39 +0100 Subject: [PATCH 2204/3953] use isort to sort imports according to PEP8 for automation (#29620) --- homeassistant/components/automation/__init__.py | 3 +-- homeassistant/components/automation/config.py | 2 +- homeassistant/components/automation/device.py | 1 - homeassistant/components/automation/event.py | 3 +-- homeassistant/components/automation/geo_location.py | 3 +-- .../components/automation/homeassistant.py | 5 ++--- homeassistant/components/automation/litejet.py | 5 ++--- homeassistant/components/automation/mqtt.py | 5 ++--- .../components/automation/numeric_state.py | 13 ++++++------- .../components/automation/reproduce_state.py | 4 ++-- homeassistant/components/automation/state.py | 7 +++---- homeassistant/components/automation/sun.py | 5 ++--- homeassistant/components/automation/template.py | 8 +++----- homeassistant/components/automation/time.py | 3 +-- homeassistant/components/automation/time_pattern.py | 3 +-- homeassistant/components/automation/webhook.py | 3 +-- homeassistant/components/automation/zone.py | 9 ++++----- tests/components/automation/common.py | 8 ++++---- tests/components/automation/test_event.py | 5 ++--- tests/components/automation/test_geo_location.py | 3 +-- tests/components/automation/test_homeassistant.py | 4 ++-- tests/components/automation/test_litejet.py | 5 +++-- tests/components/automation/test_mqtt.py | 10 ++++++---- tests/components/automation/test_numeric_state.py | 7 ++++--- tests/components/automation/test_state.py | 12 ++++++++---- tests/components/automation/test_sun.py | 8 ++++---- tests/components/automation/test_template.py | 10 +++++++--- tests/components/automation/test_time.py | 10 +++++++--- tests/components/automation/test_time_pattern.py | 5 ++--- tests/components/automation/test_zone.py | 4 ++-- 30 files changed, 85 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 2b775e3a602f41..4441b0285658c5 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -27,13 +27,12 @@ from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime, utcnow - # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 5733cd2e83ec2a..d11472a21289dc 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -7,8 +7,8 @@ from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.const import CONF_PLATFORM from homeassistant.config import async_log_exception, config_without_domain +from homeassistant.const import CONF_PLATFORM from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, config_per_platform, script from homeassistant.loader import IntegrationNotFound diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index ced8f65cbf5379..b2892d1abaa835 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -7,7 +7,6 @@ ) from homeassistant.const import CONF_DOMAIN - # mypy: allow-untyped-defs, no-check-untyped-defs TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 26dacac974d559..9fc78746a7c82b 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -3,11 +3,10 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv - # mypy: allow-untyped-defs CONF_EVENT_TYPE = "event_type" diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 0ef0884d329e64..5dc4f3c80f6e88 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -2,7 +2,6 @@ import voluptuous as vol from homeassistant.components.geo_location import DOMAIN -from homeassistant.core import callback from homeassistant.const import ( CONF_EVENT, CONF_PLATFORM, @@ -10,10 +9,10 @@ CONF_ZONE, EVENT_STATE_CHANGED, ) +from homeassistant.core import callback 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" diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index e4eb029d5aa778..743b169c86cc13 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -3,9 +3,8 @@ import voluptuous as vol -from homeassistant.core import callback, CoreState -from homeassistant.const import CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP - +from homeassistant.const import CONF_EVENT, CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import CoreState, callback # mypy: allow-untyped-defs diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 9512db8261dbbf..466fc941a9ae02 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -3,12 +3,11 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_point_in_utc_time - +import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 135a421f72e1fb..fb0073c78d539a 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -3,12 +3,11 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt -from homeassistant.const import CONF_PLATFORM, CONF_PAYLOAD +from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv - # mypy: allow-untyped-defs CONF_ENCODING = "encoding" diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 0c8ab3d9c8b526..e944b66751bc01 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -4,18 +4,17 @@ import voluptuous as vol from homeassistant import exceptions -from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.const import ( - CONF_VALUE_TEMPLATE, - CONF_PLATFORM, - CONF_ENTITY_ID, - CONF_BELOW, CONF_ABOVE, + CONF_BELOW, + CONF_ENTITY_ID, CONF_FOR, + CONF_PLATFORM, + CONF_VALUE_TEMPLATE, ) -from homeassistant.helpers.event import async_track_state_change, async_track_same_state +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import condition, config_validation as cv, template - +from homeassistant.helpers.event import async_track_same_state, async_track_state_change # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py index 553d6871087729..4cfe519d585a0e 100644 --- a/homeassistant/components/automation/reproduce_state.py +++ b/homeassistant/components/automation/reproduce_state.py @@ -5,10 +5,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 47c44587b08f03..fc3fff475148f7 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -6,11 +6,10 @@ import voluptuous as vol from homeassistant import exceptions -from homeassistant.core import HomeAssistant, CALLBACK_TYPE, callback -from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR +from homeassistant.const import CONF_FOR, CONF_PLATFORM, MATCH_ALL +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.event import async_track_state_change, async_track_same_state - +from homeassistant.helpers.event import async_track_same_state, async_track_state_change # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 66892784a54d1d..c416742f397c75 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -4,16 +4,15 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE, ) -from homeassistant.helpers.event import async_track_sunrise, async_track_sunset +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv - +from homeassistant.helpers.event import async_track_sunrise, async_track_sunset # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 95b6b857c9de8f..ee4484410cd4c5 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -3,13 +3,11 @@ import voluptuous as vol -from homeassistant.core import callback -from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_FOR from homeassistant import exceptions -from homeassistant.helpers import condition +from homeassistant.const import CONF_FOR, CONF_PLATFORM, CONF_VALUE_TEMPLATE +from homeassistant.core import callback +from homeassistant.helpers import condition, config_validation as cv, template 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 diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 231bc346e141c0..5f461952960227 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -3,12 +3,11 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_AT, CONF_PLATFORM +from homeassistant.core import callback 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__) diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py index ee092916112024..65d44f5b1caf46 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -3,12 +3,11 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM +from homeassistant.core import callback 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" diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index bbcf9bd9ddcad2..5d01c6454a80ae 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -5,13 +5,12 @@ from aiohttp import hdrs import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from . import DOMAIN as AUTOMATION_DOMAIN - # mypy: allow-untyped-defs DEPENDENCIES = ("webhook",) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 535ef298a2a78a..3dba1a4df355b6 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -1,17 +1,16 @@ """Offer zone automation rules.""" import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( - CONF_EVENT, CONF_ENTITY_ID, + CONF_EVENT, + CONF_PLATFORM, CONF_ZONE, MATCH_ALL, - CONF_PLATFORM, ) -from homeassistant.helpers.event import async_track_state_change +from homeassistant.core import callback from homeassistant.helpers import condition, config_validation as cv, location - +from homeassistant.helpers.event import async_track_state_change # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/tests/components/automation/common.py b/tests/components/automation/common.py index 729a6bb7212946..95b156bcb149a9 100644 --- a/tests/components/automation/common.py +++ b/tests/components/automation/common.py @@ -6,11 +6,11 @@ from homeassistant.components.automation import DOMAIN, SERVICE_TRIGGER from homeassistant.const import ( ATTR_ENTITY_ID, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - SERVICE_TOGGLE, - SERVICE_RELOAD, ENTITY_MATCH_ALL, + SERVICE_RELOAD, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) from homeassistant.loader import bind_hass diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index e8d6089f500789..26d19d6fa47f57 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -1,13 +1,12 @@ """The tests for the Event automation.""" import pytest +import homeassistant.components.automation as automation from homeassistant.core import Context from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation -from tests.common import mock_component +from tests.common import async_mock_service, mock_component from tests.components.automation import common -from tests.common import async_mock_service @pytest.fixture diff --git a/tests/components/automation/test_geo_location.py b/tests/components/automation/test_geo_location.py index d1ded8da1c650e..05e30458ef36ff 100644 --- a/tests/components/automation/test_geo_location.py +++ b/tests/components/automation/test_geo_location.py @@ -5,9 +5,8 @@ from homeassistant.core import Context from homeassistant.setup import async_setup_component -from tests.common import mock_component +from tests.common import async_mock_service, mock_component from tests.components.automation import common -from tests.common import async_mock_service @pytest.fixture diff --git a/tests/components/automation/test_homeassistant.py b/tests/components/automation/test_homeassistant.py index 003e900babcb23..d5bd4c6dd5bc82 100644 --- a/tests/components/automation/test_homeassistant.py +++ b/tests/components/automation/test_homeassistant.py @@ -1,9 +1,9 @@ """The tests for the Event automation.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch +import homeassistant.components.automation as automation from homeassistant.core import CoreState from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from tests.common import async_mock_service, mock_coro diff --git a/tests/components/automation/test_litejet.py b/tests/components/automation/test_litejet.py index 4c916d8ed96501..75fbc03a5899f8 100644 --- a/tests/components/automation/test_litejet.py +++ b/tests/components/automation/test_litejet.py @@ -1,13 +1,14 @@ """The tests for the litejet component.""" +from datetime import timedelta import logging from unittest import mock -from datetime import timedelta + import pytest from homeassistant import setup -import homeassistant.util.dt as dt_util from homeassistant.components import litejet import homeassistant.components.automation as automation +import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, async_mock_service diff --git a/tests/components/automation/test_mqtt.py b/tests/components/automation/test_mqtt.py index 7c6db978f5c70b..9dbe93a7998d40 100644 --- a/tests/components/automation/test_mqtt.py +++ b/tests/components/automation/test_mqtt.py @@ -1,14 +1,16 @@ """The tests for the MQTT automation.""" -import pytest from unittest import mock -from homeassistant.setup import async_setup_component +import pytest + import homeassistant.components.automation as automation +from homeassistant.setup import async_setup_component + from tests.common import ( async_fire_mqtt_message, - mock_component, - async_mock_service, async_mock_mqtt_component, + async_mock_service, + mock_component, ) from tests.components.automation import common diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index 3cb8e2588fc5ee..c6c1fd83184bdb 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -1,18 +1,19 @@ """The tests for numeric state automation.""" from datetime import timedelta -import pytest from unittest.mock import patch +import pytest + import homeassistant.components.automation as automation from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( - mock_component, - async_fire_time_changed, assert_setup_component, + async_fire_time_changed, async_mock_service, + mock_component, ) from tests.components.automation import common diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 9d84fb3e8cef53..b6f9a50cf9d2e1 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -1,17 +1,21 @@ """The test for state automation.""" from datetime import timedelta +from unittest.mock import patch import pytest -from unittest.mock import patch +import homeassistant.components.automation as automation from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -import homeassistant.components.automation as automation -from tests.common import async_fire_time_changed, assert_setup_component, mock_component +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + async_mock_service, + mock_component, +) from tests.components.automation import common -from tests.common import async_mock_service @pytest.fixture diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 2668ac97053917..3468c9e9480ca6 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -1,16 +1,16 @@ """The tests for the sun automation.""" from datetime import datetime +from unittest.mock import patch import pytest -from unittest.mock import patch -from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET -from homeassistant.setup import async_setup_component from homeassistant.components import sun import homeassistant.components.automation as automation +from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, mock_component, async_mock_service +from tests.common import async_fire_time_changed, async_mock_service, mock_component from tests.components.automation import common ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/automation/test_template.py b/tests/components/automation/test_template.py index d7726b7ffd8ea9..d9566b8f464364 100644 --- a/tests/components/automation/test_template.py +++ b/tests/components/automation/test_template.py @@ -4,14 +4,18 @@ import pytest +import homeassistant.components.automation as automation from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -import homeassistant.components.automation as automation -from tests.common import async_fire_time_changed, assert_setup_component, mock_component +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + async_mock_service, + mock_component, +) from tests.components.automation import common -from tests.common import async_mock_service @pytest.fixture diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index fa931d06bfcd14..e12ce6684d2387 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -4,12 +4,16 @@ import pytest +import homeassistant.components.automation as automation from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -import homeassistant.components.automation as automation -from tests.common import async_fire_time_changed, assert_setup_component, mock_component -from tests.common import async_mock_service +from tests.common import ( + assert_setup_component, + async_fire_time_changed, + async_mock_service, + mock_component, +) @pytest.fixture diff --git a/tests/components/automation/test_time_pattern.py b/tests/components/automation/test_time_pattern.py index 479bab1c78e001..70d647a124182f 100644 --- a/tests/components/automation/test_time_pattern.py +++ b/tests/components/automation/test_time_pattern.py @@ -1,13 +1,12 @@ """The tests for the time_pattern automation.""" import pytest +import homeassistant.components.automation as automation from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -import homeassistant.components.automation as automation -from tests.common import async_fire_time_changed, mock_component +from tests.common import async_fire_time_changed, async_mock_service, mock_component from tests.components.automation import common -from tests.common import async_mock_service @pytest.fixture diff --git a/tests/components/automation/test_zone.py b/tests/components/automation/test_zone.py index 07be09a84543f5..44ad20e16f0133 100644 --- a/tests/components/automation/test_zone.py +++ b/tests/components/automation/test_zone.py @@ -1,12 +1,12 @@ """The tests for the location automation.""" import pytest +from homeassistant.components import automation, zone from homeassistant.core import Context from homeassistant.setup import async_setup_component -from homeassistant.components import automation, zone -from tests.components.automation import common from tests.common import async_mock_service, mock_component +from tests.components.automation import common @pytest.fixture From f5288db93c91d6910005fc66dfbc5116e6fa42dc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 17:30:57 +0100 Subject: [PATCH 2205/3953] use isort to sort imports according to PEP8 for cast (#29624) --- homeassistant/components/cast/discovery.py | 2 +- .../components/cast/home_assistant_cast.py | 3 +-- homeassistant/components/cast/media_player.py | 16 ++++++++-------- .../components/cast/test_home_assistant_cast.py | 1 + tests/components/cast/test_init.py | 2 +- tests/components/cast/test_media_player.py | 8 ++++---- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index d3097b3cc29fb3..54f165889afe1e 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -9,9 +9,9 @@ from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( + INTERNAL_DISCOVERY_RUNNING_KEY, KNOWN_CHROMECAST_INFO_KEY, SIGNAL_CAST_DISCOVERED, - INTERNAL_DISCOVERY_RUNNING_KEY, SIGNAL_CAST_REMOVED, ) from .helpers import ChromecastInfo, ChromeCastZeroconf diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index d5d35ba7c9f933..0b8633e1916267 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -1,9 +1,8 @@ """Home Assistant Cast integration for Cast.""" from typing import Optional -import voluptuous as vol - from pychromecast.controllers.homeassistant import HomeAssistantController +import voluptuous as vol from homeassistant import auth, config_entries, core from homeassistant.const import ATTR_ENTITY_ID diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index c2d847fd09bb2b..0317413450203c 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -4,12 +4,12 @@ from typing import Optional import pychromecast +from pychromecast.controllers.homeassistant import HomeAssistantController +from pychromecast.controllers.multizone import MultizoneManager from pychromecast.socket_client import ( CONNECTION_STATUS_CONNECTED, CONNECTION_STATUS_DISCONNECTED, ) -from pychromecast.controllers.multizone import MultizoneManager -from pychromecast.controllers.homeassistant import HomeAssistantController import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice @@ -46,22 +46,22 @@ from homeassistant.util.logging import async_create_catching_coro from .const import ( - DOMAIN as CAST_DOMAIN, ADDED_CAST_DEVICES_KEY, - SIGNAL_CAST_DISCOVERED, - KNOWN_CHROMECAST_INFO_KEY, CAST_MULTIZONE_MANAGER_KEY, DEFAULT_PORT, + DOMAIN as CAST_DOMAIN, + KNOWN_CHROMECAST_INFO_KEY, + SIGNAL_CAST_DISCOVERED, SIGNAL_CAST_REMOVED, SIGNAL_HASS_CAST_SHOW_VIEW, ) +from .discovery import discover_chromecast, setup_internal_discovery from .helpers import ( - ChromecastInfo, CastStatusListener, - DynamicGroupCastStatusListener, + ChromecastInfo, ChromeCastZeroconf, + DynamicGroupCastStatusListener, ) -from .discovery import setup_internal_discovery, discover_chromecast _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 8db6fd4609e850..10dd253704ea32 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,5 +1,6 @@ """Test Home Assistant Cast.""" from unittest.mock import Mock, patch + from homeassistant.components.cast import home_assistant_cast from tests.common import MockConfigEntry, async_mock_signal diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 9062e521bef3fb..6971c071353d5f 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow -from homeassistant.setup import async_setup_component from homeassistant.components import cast +from homeassistant.setup import async_setup_component from tests.common import MockDependency, mock_coro diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 8f33709fb2d1a4..fd565ab59d9b6c 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -2,18 +2,18 @@ # pylint: disable=protected-access import asyncio from typing import Optional -from unittest.mock import patch, MagicMock, Mock +from unittest.mock import MagicMock, Mock, patch from uuid import UUID import attr import pytest -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.components.cast import media_player as cast from homeassistant.components.cast.media_player import ChromecastInfo from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.components.cast import media_player as cast +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_coro From 8f5e8c72c60bc04202a1c075dc7c3ecf117bd09d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 17:32:08 +0100 Subject: [PATCH 2206/3953] use isort to sort imports according to PEP8 for binary_sensor (#29622) --- .../components/binary_sensor/__init__.py | 7 +++---- .../components/binary_sensor/device_condition.py | 5 +++-- .../components/binary_sensor/device_trigger.py | 5 ++--- .../binary_sensor/test_device_condition.py | 15 ++++++++------- .../binary_sensor/test_device_trigger.py | 13 +++++++------ tests/components/binary_sensor/test_init.py | 2 +- 6 files changed, 24 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index e5f5dc94ff12c8..73d5e0be4584a1 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -5,14 +5,13 @@ import voluptuous as vol -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) - +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 0766d82c727b09..842790e017852a 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -1,10 +1,11 @@ """Implemenet device conditions for binary sensor.""" from typing import Dict, List + import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import ( async_entries_for_device, @@ -13,7 +14,6 @@ from homeassistant.helpers.typing import ConfigType from . import ( - DOMAIN, DEVICE_CLASS_BATTERY, DEVICE_CLASS_COLD, DEVICE_CLASS_CONNECTIVITY, @@ -37,6 +37,7 @@ DEVICE_CLASS_SOUND, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, + DOMAIN, ) DEVICE_CLASS_NONE = "none" diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index c51b9749288bb5..288cc101d930ca 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -8,11 +8,10 @@ CONF_TURNED_ON, ) from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE -from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_device from . import ( - DOMAIN, DEVICE_CLASS_BATTERY, DEVICE_CLASS_COLD, DEVICE_CLASS_CONNECTIVITY, @@ -36,9 +35,9 @@ DEVICE_CLASS_SOUND, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, + DOMAIN, ) - # mypy: allow-untyped-defs, no-check-untyped-defs DEVICE_CLASS_NONE = "none" diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 34cf4030a50192..ecf5e86bdad79f 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -1,23 +1,24 @@ """The test for binary_sensor device automation.""" from datetime import timedelta -import pytest from unittest.mock import patch -from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES -from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component +import pytest + import homeassistant.components.automation as automation +from homeassistant.components.binary_sensor import DEVICE_CLASSES, DOMAIN +from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 9bab1ff1f363e9..404def664919bb 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -1,23 +1,24 @@ """The test for binary_sensor device automation.""" from datetime import timedelta + import pytest -from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES -from homeassistant.components.binary_sensor.device_trigger import ENTITY_TRIGGERS -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.binary_sensor import DEVICE_CLASSES, DOMAIN +from homeassistant.components.binary_sensor.device_trigger import ENTITY_TRIGGERS +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, async_fire_time_changed, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 0aa1a798487d2f..9759d3281e62ac 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -3,7 +3,7 @@ from unittest import mock from homeassistant.components import binary_sensor -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON class TestBinarySensor(unittest.TestCase): From 3b5da9c44a0370f9b615f5fbc63bb630e2b6b95b Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 8 Dec 2019 17:50:57 +0100 Subject: [PATCH 2207/3953] Move imports to top for homekit_controller (#29564) * Move imports to top for homekit_controller * Fix IpPairing mock in two test files --- .../components/homekit_controller/__init__.py | 7 +++---- .../components/homekit_controller/climate.py | 14 +++++++------- .../components/homekit_controller/config_flow.py | 7 +++---- .../components/homekit_controller/connection.py | 7 +++---- .../components/homekit_controller/cover.py | 2 +- .../components/homekit_controller/storage.py | 2 +- tests/components/homekit_controller/common.py | 2 +- .../specific_devices/test_ecobee3.py | 2 +- .../homekit_controller/test_config_flow.py | 16 ++++++++++++---- 9 files changed, 32 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 6b53301e87790f..c863da14a3a409 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -5,15 +5,14 @@ from homekit.model.characteristics import CharacteristicsTypes from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import Entity # We need an import from .config_flow, without it .config_flow is never loaded. from .config_flow import HomekitControllerFlowHandler # noqa: F401 -from .connection import get_accessory_information, HKDevice -from .const import CONTROLLER, ENTITY_MAP, KNOWN_DEVICES -from .const import DOMAIN +from .connection import HKDevice, get_accessory_information +from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES from .storage import EntityMapStorage _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 1f9118ff838326..d0ab7bd2e990d5 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -4,20 +4,20 @@ from homekit.model.characteristics import CharacteristicsTypes from homeassistant.components.climate import ( - ClimateDevice, - DEFAULT_MIN_HUMIDITY, DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + ClimateDevice, ) from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT_COOL, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 40bf87d6f0a911..f4eb1190727ab4 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -1,17 +1,17 @@ """Config flow to configure homekit_controller.""" -import os import json import logging +import os import homekit +from homekit.controller.ip_implementation import IpPairing import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback +from .connection import get_accessory_name, get_bridge_information from .const import DOMAIN, KNOWN_DEVICES -from .connection import get_bridge_information, get_accessory_name - HOMEKIT_IGNORE = ["Home Assistant Bridge"] HOMEKIT_DIR = ".homekit" @@ -194,7 +194,6 @@ async def async_step_zeroconf(self, discovery_info): async def async_import_legacy_pairing(self, discovery_props, pairing_data): """Migrate a legacy pairing to config entries.""" - from homekit.controller.ip_implementation import IpPairing hkid = discovery_props["id"] diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 1cb2131fb8f374..3ccfa8b01397db 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -3,18 +3,18 @@ import datetime import logging +from homekit.controller.ip_implementation import IpPairing from homekit.exceptions import ( AccessoryDisconnectedError, AccessoryNotFoundError, EncryptionError, ) -from homekit.model.services import ServicesTypes from homekit.model.characteristics import CharacteristicsTypes +from homekit.model.services import ServicesTypes from homeassistant.helpers.event import async_track_time_interval -from .const import DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP - +from .const import DOMAIN, ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds @@ -57,7 +57,6 @@ class HKDevice: def __init__(self, hass, config_entry, pairing_data): """Initialise a generic HomeKit device.""" - from homekit.controller.ip_implementation import IpPairing self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 0606778acb5768..7e5591d9505ea5 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -11,8 +11,8 @@ SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, - SUPPORT_STOP, SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, CoverDevice, ) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 46d095b5631823..ffc2da5fbf213c 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -1,7 +1,7 @@ """Helpers for HomeKit data stored in HA storage.""" -from homeassistant.helpers.storage import Store from homeassistant.core import callback +from homeassistant.helpers.storage import Store from .const import DOMAIN diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 2e1cfd7a77d9f1..9a404701c54fb9 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -250,7 +250,7 @@ async def setup_test_accessories(hass, accessories): config_entry.add_to_hass(hass) - pairing_cls_loc = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_loc = "homeassistant.components.homekit_controller.connection.IpPairing" with mock.patch(pairing_cls_loc) as pairing_cls: pairing_cls.return_value = pairing await config_entry.async_setup(hass) diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 8473d235278265..8531712a5d3973 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -154,7 +154,7 @@ async def test_ecobee3_setup_connection_failure(hass): # make sure the IpPairing mock is in place or we'll try to connect to # a real device. Normally this mocking is done by the helper in # setup_test_accessories. - pairing_cls_loc = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_loc = "homeassistant.components.homekit_controller.connection.IpPairing" with mock.patch(pairing_cls_loc) as pairing_cls: pairing_cls.return_value = pairing await time_changed(hass, 5 * 60) diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index a8428375ae6ddf..81d5ac9a7516ae 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -554,7 +554,9 @@ async def test_import_works(hass): flow = _setup_flow_handler(hass) - pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_imp = ( + "homeassistant.components.homekit_controller.config_flow.IpPairing" + ) with mock.patch(pairing_cls_imp) as pairing_cls: pairing_cls.return_value = pairing @@ -694,7 +696,9 @@ async def test_parse_new_homekit_json(hass): flow = _setup_flow_handler(hass) - pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_imp = ( + "homeassistant.components.homekit_controller.config_flow.IpPairing" + ) with mock.patch(pairing_cls_imp) as pairing_cls: pairing_cls.return_value = pairing @@ -742,7 +746,9 @@ async def test_parse_old_homekit_json(hass): flow = _setup_flow_handler(hass) - pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_imp = ( + "homeassistant.components.homekit_controller.config_flow.IpPairing" + ) with mock.patch(pairing_cls_imp) as pairing_cls: pairing_cls.return_value = pairing @@ -798,7 +804,9 @@ async def test_parse_overlapping_homekit_json(hass): flow = _setup_flow_handler(hass) - pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" + pairing_cls_imp = ( + "homeassistant.components.homekit_controller.config_flow.IpPairing" + ) with mock.patch(pairing_cls_imp) as pairing_cls: pairing_cls.return_value = pairing From 954813b47832ab91d9dc400862769abef3817b3e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 17:55:01 +0100 Subject: [PATCH 2208/3953] use isort to sort imports according to PEP8 for climate (#29625) --- homeassistant/components/climate/__init__.py | 4 +-- .../components/climate/device_action.py | 10 ++++--- .../components/climate/device_condition.py | 8 +++--- .../components/climate/device_trigger.py | 26 ++++++++++--------- .../components/climate/reproduce_state.py | 16 ++++++------ tests/components/climate/common.py | 2 +- .../components/climate/test_device_action.py | 6 ++--- .../climate/test_device_condition.py | 6 ++--- .../components/climate/test_device_trigger.py | 8 +++--- tests/components/climate/test_init.py | 7 ++--- .../climate/test_reproduce_state.py | 2 +- 11 files changed, 51 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 6006b2a9a3b13b..e2bf555cc49e16 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -19,9 +19,9 @@ ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + make_entity_service_schema, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -68,8 +68,8 @@ SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY, - SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) DEFAULT_MIN_TEMP = 7 diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index 836e2277461f7d..6f7725ac83577a 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -1,17 +1,19 @@ """Provides device automations for Climate.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import DOMAIN, const ACTION_TYPES = {"set_hvac_mode", "set_preset_mode"} diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 3a0752339429a6..cf393a035ec9e5 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -1,19 +1,21 @@ """Provide the device automations for Climate.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN, const CONDITION_TYPES = {"is_hvac_mode", "is_preset_mode"} diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index e814bdc88de49b..4c5dcb0ee0439a 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -1,26 +1,28 @@ """Provides device automations for Climate.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + numeric_state as numeric_state_automation, + state as state_automation, +) +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, + CONF_ABOVE, + CONF_BELOW, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, CONF_FOR, - CONF_ABOVE, - CONF_BELOW, + CONF_PLATFORM, + CONF_TYPE, ) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType -from homeassistant.components.automation import ( - state as state_automation, - numeric_state as numeric_state_automation, - AutomationActionType, -) -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN, const TRIGGER_TYPES = { diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index 34e72a27c92d58..82ca4f4e85c86a 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -8,20 +8,20 @@ from .const import ( ATTR_AUX_HEAT, - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, - ATTR_PRESET_MODE, + ATTR_HUMIDITY, ATTR_HVAC_MODE, + ATTR_PRESET_MODE, ATTR_SWING_MODE, - ATTR_HUMIDITY, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + DOMAIN, HVAC_MODES, SERVICE_SET_AUX_HEAT, - SERVICE_SET_TEMPERATURE, - SERVICE_SET_PRESET_MODE, + SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, - SERVICE_SET_HUMIDITY, - DOMAIN, + SERVICE_SET_TEMPERATURE, ) diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index a5ea182f2b61c2..5b75dc98e69d95 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -25,9 +25,9 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, + ENTITY_MATCH_ALL, SERVICE_TURN_OFF, SERVICE_TURN_ON, - ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index 46e8b3395c4c0a..ff78b837591a9e 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -2,18 +2,18 @@ import pytest import voluptuous_serialize +import homeassistant.components.automation as automation from homeassistant.components.climate import DOMAIN, const, device_action +from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation -from homeassistant.helpers import device_registry, config_validation as cv from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index b0a9c6c283a7ef..c8aaf0e19677f1 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -2,18 +2,18 @@ import pytest import voluptuous_serialize +import homeassistant.components.automation as automation from homeassistant.components.climate import DOMAIN, const, device_condition +from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation -from homeassistant.helpers import device_registry, config_validation as cv from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 3b497912c52bd5..d9bfd6d5ba4fe1 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -1,19 +1,19 @@ """The tests for Climate device triggers.""" -import voluptuous_serialize import pytest +import voluptuous_serialize +import homeassistant.components.automation as automation from homeassistant.components.climate import DOMAIN, const, device_trigger +from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation -from homeassistant.helpers import device_registry, config_validation as cv from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index b044753c891053..4345ecedcf7691 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -1,16 +1,17 @@ """The tests for the climate component.""" -from unittest.mock import MagicMock from typing import List +from unittest.mock import MagicMock import pytest import voluptuous as vol from homeassistant.components.climate import ( - SET_TEMPERATURE_SCHEMA, - ClimateDevice, HVAC_MODE_HEAT, HVAC_MODE_OFF, + SET_TEMPERATURE_SCHEMA, + ClimateDevice, ) + from tests.common import async_mock_service diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index fe9958688408fa..df0b6314d63331 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -2,7 +2,6 @@ import pytest -from homeassistant.components.climate.reproduce_state import async_reproduce_states from homeassistant.components.climate.const import ( ATTR_AUX_HEAT, ATTR_HUMIDITY, @@ -21,6 +20,7 @@ SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, ) +from homeassistant.components.climate.reproduce_state import async_reproduce_states from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import Context, State From 73c373a0f2fc880516821853eb09acbd872c3d54 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 17:55:57 +0100 Subject: [PATCH 2209/3953] use isort to sort imports according to PEP8 for command_line (#29627) --- homeassistant/components/command_line/cover.py | 4 ++-- homeassistant/components/command_line/notify.py | 3 +-- homeassistant/components/command_line/switch.py | 13 ++++++------- tests/components/command_line/test_binary_sensor.py | 2 +- tests/components/command_line/test_cover.py | 2 +- tests/components/command_line/test_notify.py | 3 ++- tests/components/command_line/test_sensor.py | 3 ++- tests/components/command_line/test_switch.py | 6 +++--- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index c4413e78a00114..1d996614caa00a 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -4,15 +4,15 @@ import voluptuous as vol -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice from homeassistant.const import ( CONF_COMMAND_CLOSE, CONF_COMMAND_OPEN, CONF_COMMAND_STATE, CONF_COMMAND_STOP, CONF_COVERS, - CONF_VALUE_TEMPLATE, CONF_FRIENDLY_NAME, + CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index e2581c8f065198..21653171f34d99 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -4,11 +4,10 @@ import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_COMMAND, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 937e859197a742..62dcbe2f15a267 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -4,21 +4,20 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv - from homeassistant.components.switch import ( - SwitchDevice, - PLATFORM_SCHEMA, ENTITY_ID_FORMAT, + PLATFORM_SCHEMA, + SwitchDevice, ) from homeassistant.const import ( - CONF_FRIENDLY_NAME, - CONF_SWITCHES, - CONF_VALUE_TEMPLATE, CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_COMMAND_STATE, + CONF_FRIENDLY_NAME, + CONF_SWITCHES, + CONF_VALUE_TEMPLATE, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index 333404cf904e01..33c28b7d65ac72 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -1,8 +1,8 @@ """The tests for the Command line Binary sensor platform.""" import unittest -from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.command_line import binary_sensor as command_line +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import template from tests.common import get_test_home_assistant diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 05b77a1c85d07f..662ab0c969cc54 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -5,8 +5,8 @@ import pytest -from homeassistant.components.cover import DOMAIN import homeassistant.components.command_line.cover as cmd_rs +from homeassistant.components.cover import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 964a053f403e46..8bdc4ba0a015db 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -4,8 +4,9 @@ import unittest from unittest.mock import patch -from homeassistant.setup import setup_component import homeassistant.components.notify as notify +from homeassistant.setup import setup_component + from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 58a170cb37ae25..e51c0187460df8 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -2,8 +2,9 @@ import unittest from unittest.mock import patch -from homeassistant.helpers.template import Template from homeassistant.components.command_line import sensor as command_line +from homeassistant.helpers.template import Template + from tests.common import get_test_home_assistant diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index ef010081958539..497fb0c252359d 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -4,10 +4,10 @@ import tempfile import unittest -from homeassistant.setup import setup_component -from homeassistant.const import STATE_ON, STATE_OFF -import homeassistant.components.switch as switch import homeassistant.components.command_line.switch as command_line +import homeassistant.components.switch as switch +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant from tests.components.switch import common From f355570f17c05798e4380565e00b278ca01df625 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 17:57:28 +0100 Subject: [PATCH 2210/3953] use isort to sort imports according to PEP8 for config (#29628) --- homeassistant/components/config/__init__.py | 4 ++-- homeassistant/components/config/area_registry.py | 1 - homeassistant/components/config/auth.py | 1 - .../components/config/auth_provider_homeassistant.py | 1 - homeassistant/components/config/automation.py | 2 +- homeassistant/components/config/core.py | 4 ++-- homeassistant/components/config/entity_registry.py | 4 ++-- homeassistant/components/config/group.py | 2 +- homeassistant/components/config/scene.py | 2 +- homeassistant/components/config/script.py | 2 +- tests/components/config/test_area_registry.py | 1 + tests/components/config/test_auth.py | 2 +- tests/components/config/test_config_entries.py | 6 +++--- tests/components/config/test_core.py | 1 + tests/components/config/test_device_registry.py | 1 + tests/components/config/test_entity_registry.py | 5 +++-- tests/components/config/test_group.py | 3 +-- tests/components/config/test_init.py | 6 +++--- tests/components/config/test_zwave.py | 3 +-- 19 files changed, 25 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 5a66c1fc5d446d..5873cdc32712dd 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -6,11 +6,11 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID +from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import ATTR_COMPONENT -from homeassistant.util.yaml import load_yaml, dump +from homeassistant.util.yaml import dump, load_yaml DOMAIN = "config" SECTIONS = ( diff --git a/homeassistant/components/config/area_registry.py b/homeassistant/components/config/area_registry.py index 9c8853ac7825e3..81daf35339e881 100644 --- a/homeassistant/components/config/area_registry.py +++ b/homeassistant/components/config/area_registry.py @@ -9,7 +9,6 @@ from homeassistant.core import callback from homeassistant.helpers.area_registry import async_get_registry - WS_TYPE_LIST = "config/area_registry/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( {vol.Required("type"): WS_TYPE_LIST} diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 977bae3608322d..361367ffb4d526 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -3,7 +3,6 @@ from homeassistant.components import websocket_api - WS_TYPE_LIST = "config/auth/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( {vol.Required("type"): WS_TYPE_LIST} diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 817675db238b59..dec7fb24d27f79 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -4,7 +4,6 @@ from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.components import websocket_api - WS_TYPE_CREATE = "config/auth_provider/homeassistant/create" SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( { diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 0e9b4053b7b587..d7bb1ef9883160 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -4,8 +4,8 @@ from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.automation.config import async_validate_config_item -from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.config import AUTOMATION_CONFIG_PATH +from homeassistant.const import CONF_ID, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditIdBasedConfigView diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index 073f8f23d6cb1a..e9ceb7eac57e41 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -2,10 +2,10 @@ import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.config import async_check_ha_config_file -from homeassistant.components import websocket_api -from homeassistant.const import CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL +from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC from homeassistant.helpers import config_validation as cv from homeassistant.util import location diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 125b2260f08b81..458a9dd3ecb741 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -1,15 +1,15 @@ """HTTP views to interact with the entity registry.""" import voluptuous as vol -from homeassistant.core import callback -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.components import websocket_api from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.websocket_api.decorators import ( async_response, require_admin, ) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_registry import async_get_registry async def async_setup(hass): diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index d104cd2e1df6f6..d95891af6556c1 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -1,7 +1,7 @@ """Provide configuration end points for Groups.""" from homeassistant.components.group import DOMAIN, GROUP_SCHEMA -from homeassistant.const import SERVICE_RELOAD from homeassistant.config import GROUP_CONFIG_PATH +from homeassistant.const import SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 6e77dae08260f8..79a30177e470fd 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -3,8 +3,8 @@ import uuid from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA -from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.config import SCENE_CONFIG_PATH +from homeassistant.const import CONF_ID, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditIdBasedConfigView diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index e63651d8f2a9bf..032774de47343d 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,7 +1,7 @@ """Provide configuration end points for scripts.""" from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA -from homeassistant.const import SERVICE_RELOAD from homeassistant.config import SCRIPT_CONFIG_PATH +from homeassistant.const import SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView diff --git a/tests/components/config/test_area_registry.py b/tests/components/config/test_area_registry.py index 0505597c6ffade..f66e16e606fc2e 100644 --- a/tests/components/config/test_area_registry.py +++ b/tests/components/config/test_area_registry.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components.config import area_registry + from tests.common import mock_area_registry diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index 4d90f345657311..b07df39a8fee96 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -4,7 +4,7 @@ from homeassistant.auth import models as auth_models from homeassistant.components.config import auth as auth_config -from tests.common import MockGroup, MockUser, CLIENT_ID +from tests.common import CLIENT_ID, MockGroup, MockUser @pytest.fixture(autouse=True) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 3d22d3ac1a7773..6176bd73c52c64 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -8,18 +8,18 @@ import voluptuous as vol from homeassistant import config_entries as core_ce, data_entry_flow +from homeassistant.components.config import config_entries from homeassistant.config_entries import HANDLERS from homeassistant.core import callback -from homeassistant.setup import async_setup_component -from homeassistant.components.config import config_entries from homeassistant.generated import config_flows +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, MockModule, mock_coro_func, - mock_integration, mock_entity_platform, + mock_integration, ) diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 050190d8dbea37..8caa0f3e6fbe80 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -8,6 +8,7 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.util import dt as dt_util, location + from tests.common import mock_coro ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index cd305e5b567979..a3710d48b940ba 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components.config import device_registry + from tests.common import mock_device_registry diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 9472d8882540c9..133c88d9ceb96b 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -3,9 +3,10 @@ import pytest -from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.components.config import entity_registry -from tests.common import mock_registry, MockEntity, MockEntityPlatform +from homeassistant.helpers.entity_registry import RegistryEntry + +from tests.common import MockEntity, MockEntityPlatform, mock_registry @pytest.fixture diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index 3240dbe9c13c2d..3a4a145105af65 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -1,12 +1,11 @@ """Test Group config panel.""" import asyncio import json -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config - VIEW_NAME = "api:config:group:config" diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 8e5fab494eb0fe..21af45a120387b 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -2,11 +2,11 @@ import asyncio from unittest.mock import patch -from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.setup import async_setup_component, ATTR_COMPONENT from homeassistant.components import config +from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.setup import ATTR_COMPONENT, async_setup_component -from tests.common import mock_coro, mock_component +from tests.common import mock_component, mock_coro @asyncio.coroutine diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index 9f62c3f9ac23bd..c2490de23ead9b 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -7,10 +7,9 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components import config - from homeassistant.components.zwave import DATA_NETWORK, const -from tests.mock.zwave import MockNode, MockValue, MockEntityValues +from tests.mock.zwave import MockEntityValues, MockNode, MockValue VIEW_NAME = "api:config:zwave:device_config" From a3b605bb7db37eb51d512766a022f9917786dd15 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 17:58:00 +0100 Subject: [PATCH 2211/3953] use isort to sort imports according to PEP8 for cover (#29629) --- homeassistant/components/cover/__init__.py | 29 +++++++++---------- .../components/cover/device_condition.py | 12 ++++---- .../components/cover/device_trigger.py | 22 +++++++------- homeassistant/components/cover/intent.py | 2 +- .../components/cover/test_device_condition.py | 12 ++++---- tests/components/cover/test_device_trigger.py | 8 ++--- tests/components/cover/test_intent.py | 3 +- .../components/cover/test_reproduce_state.py | 1 + 8 files changed, 47 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 0b8fbfa9dd2b48..3c842067ccaf5a 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -6,31 +6,30 @@ import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) from homeassistant.components import group from homeassistant.const import ( - SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, - SERVICE_TOGGLE, - SERVICE_OPEN_COVER_TILT, - SERVICE_CLOSE_COVER_TILT, SERVICE_STOP_COVER_TILT, - SERVICE_SET_COVER_TILT_POSITION, + SERVICE_TOGGLE, SERVICE_TOGGLE_COVER_TILT, - STATE_OPEN, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) - +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index 487f815afb5591..ec6da84e5f6ee0 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -1,5 +1,6 @@ """Provides device automations for Cover.""" from typing import Any, Dict, List + import voluptuous as vol from homeassistant.const import ( @@ -8,14 +9,14 @@ CONF_ABOVE, CONF_BELOW, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - STATE_OPEN, + CONF_TYPE, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import ( @@ -24,8 +25,9 @@ entity_registry, template, ) -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import ( DOMAIN, SUPPORT_CLOSE, diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index 4f256a87dc5c6c..988427003e7a0c 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -1,30 +1,32 @@ """Provides device automations for Cover.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + numeric_state as numeric_state_automation, + state as state_automation, +) +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_ABOVE, CONF_BELOW, - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType -from homeassistant.components.automation import ( - state as state_automation, - numeric_state as numeric_state_automation, - AutomationActionType, -) -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import ( DOMAIN, SUPPORT_CLOSE, diff --git a/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py index f8d13e6a90ebe5..36402025bfa38e 100644 --- a/homeassistant/components/cover/intent.py +++ b/homeassistant/components/cover/intent.py @@ -2,7 +2,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import intent -from . import DOMAIN, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER +from . import DOMAIN, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER INTENT_OPEN_COVER = "HassOpenCover" INTENT_CLOSE_COVER = "HassCloseCover" diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 8ca912b640ba82..13c6fd8701fa11 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -1,26 +1,26 @@ """The tests for Cover device conditions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.cover import DOMAIN from homeassistant.const import ( CONF_PLATFORM, - STATE_OPEN, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 4f50c0639c03fa..3f82babc2ed103 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -1,6 +1,7 @@ """The tests for Cover device triggers.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.cover import DOMAIN from homeassistant.const import ( CONF_PLATFORM, @@ -9,18 +10,17 @@ STATE_OPEN, STATE_OPENING, ) -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/cover/test_intent.py b/tests/components/cover/test_intent.py index ce01e882941bde..29d3378a0f95bd 100644 --- a/tests/components/cover/test_intent.py +++ b/tests/components/cover/test_intent.py @@ -1,11 +1,12 @@ """The tests for the cover platform.""" from homeassistant.components.cover import ( - SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, intent as cover_intent, ) from homeassistant.helpers import intent + from tests.common import async_mock_service diff --git a/tests/components/cover/test_reproduce_state.py b/tests/components/cover/test_reproduce_state.py index 39fdf3d3992d2f..2e2d0f634670e6 100644 --- a/tests/components/cover/test_reproduce_state.py +++ b/tests/components/cover/test_reproduce_state.py @@ -16,6 +16,7 @@ STATE_OPEN, ) from homeassistant.core import State + from tests.common import async_mock_service From e4e9cdce7303f4412c5213a7db6ac5d61f0bde47 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 17:59:27 +0100 Subject: [PATCH 2212/3953] use isort to sort imports according to PEP8 for demo (#29630) --- homeassistant/components/demo/__init__.py | 2 +- .../components/demo/binary_sensor.py | 1 + homeassistant/components/demo/calendar.py | 3 +-- homeassistant/components/demo/climate.py | 4 +++- homeassistant/components/demo/cover.py | 4 ++-- homeassistant/components/demo/fan.py | 3 +-- homeassistant/components/demo/geo_location.py | 3 +-- .../components/demo/image_processing.py | 6 ++--- homeassistant/components/demo/lock.py | 3 +-- homeassistant/components/demo/sensor.py | 3 ++- homeassistant/components/demo/switch.py | 1 + homeassistant/components/demo/water_heater.py | 3 +-- tests/components/demo/test_camera.py | 2 +- tests/components/demo/test_cover.py | 14 +++++------ tests/components/demo/test_fan.py | 2 +- tests/components/demo/test_geo_location.py | 7 +++--- tests/components/demo/test_init.py | 2 +- tests/components/demo/test_light.py | 2 +- tests/components/demo/test_lock.py | 2 +- tests/components/demo/test_media_player.py | 4 ++-- tests/components/demo/test_notify.py | 4 ++-- tests/components/demo/test_remote.py | 4 ++-- tests/components/demo/test_stt.py | 2 +- tests/components/demo/test_vacuum.py | 23 +++++++++---------- tests/components/demo/test_water_heater.py | 5 ++-- 25 files changed, 54 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 05febfad603335..b6845d9d6a424a 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -4,8 +4,8 @@ import time from homeassistant import bootstrap, config_entries -import homeassistant.core as ha from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START +import homeassistant.core as ha DOMAIN = "demo" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index c1e42807f6d391..0f6dfa9f357707 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -1,5 +1,6 @@ """Demo platform that has two fake binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice + from . import DOMAIN diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 4ae836466f075e..42cb2b137a113a 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -1,9 +1,8 @@ """Demo platform that has two fake binary sensors.""" import copy -import homeassistant.util.dt as dt_util - from homeassistant.components.calendar import CalendarEventDevice, get_date +import homeassistant.util.dt as dt_util def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index f4affed7ced10c..0edcf618ba6346 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -1,11 +1,13 @@ """Demo platform that offers a fake climate device.""" import logging + from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, + HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, @@ -18,9 +20,9 @@ SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - HVAC_MODE_AUTO, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT + from . import DOMAIN SUPPORT_FLAGS = 0 diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 20a8747aaa5b2d..20e3a52aa8d255 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -1,6 +1,4 @@ """Demo platform for the cover component.""" -from homeassistant.helpers.event import track_utc_time_change - from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -8,6 +6,8 @@ SUPPORT_OPEN, CoverDevice, ) +from homeassistant.helpers.event import track_utc_time_change + from . import DOMAIN diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 500d5f6a5ce5dd..966ba51cacb795 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -1,6 +1,4 @@ """Demo fan platform that has a fake fan.""" -from homeassistant.const import STATE_OFF - from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -10,6 +8,7 @@ SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.const import STATE_OFF FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION LIMITED_SUPPORT = SUPPORT_SET_SPEED diff --git a/homeassistant/components/demo/geo_location.py b/homeassistant/components/demo/geo_location.py index 6a7aa7ddce1088..6fc8e9c2e89f45 100644 --- a/homeassistant/components/demo/geo_location.py +++ b/homeassistant/components/demo/geo_location.py @@ -5,9 +5,8 @@ import random from typing import Optional -from homeassistant.helpers.event import track_time_interval - from homeassistant.components.geo_location import GeolocationEvent +from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index 348045e47b20e7..9183609509e535 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -1,10 +1,10 @@ """Support for the demo image processing.""" from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, - ATTR_CONFIDENCE, - ATTR_NAME, ATTR_AGE, + ATTR_CONFIDENCE, ATTR_GENDER, + ATTR_NAME, + ImageProcessingFaceEntity, ) from homeassistant.components.openalpr_local.image_processing import ( ImageProcessingAlprEntity, diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index 923469f045c400..5074741d83d436 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -1,7 +1,6 @@ """Demo lock platform that has two fake locks.""" -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED - from homeassistant.components.lock import SUPPORT_OPEN, LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index bf5df94e74c355..d2b2464468bdef 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -1,11 +1,12 @@ """Demo platform that has a couple of fake sensors.""" from homeassistant.const import ( ATTR_BATTERY_LEVEL, - TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, ) from homeassistant.helpers.entity import Entity + from . import DOMAIN diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 23006cff875a4d..5c651198f5c3a8 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,6 +1,7 @@ """Demo platform that has two fake switches.""" from homeassistant.components.switch import SwitchDevice from homeassistant.const import DEVICE_DEFAULT_NAME + from . import DOMAIN diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index c3fff26c9922ac..f9aca141245656 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -1,12 +1,11 @@ """Demo platform that offers a fake water heater device.""" -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT - from homeassistant.components.water_heater import ( SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, WaterHeaterDevice, ) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT SUPPORT_FLAGS_HEATER = ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index 7a3cf426daca9d..286a1c8ca22462 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -4,7 +4,7 @@ import pytest from homeassistant.components import camera -from homeassistant.components.camera import STATE_STREAMING, STATE_IDLE +from homeassistant.components.camera import STATE_IDLE, STATE_STREAMING from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component diff --git a/tests/components/demo/test_cover.py b/tests/components/demo/test_cover.py index 45a194eab7c146..e2478f64f3a2f7 100644 --- a/tests/components/demo/test_cover.py +++ b/tests/components/demo/test_cover.py @@ -4,29 +4,29 @@ import pytest from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN, ) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - STATE_OPEN, - STATE_OPENING, - STATE_CLOSED, - STATE_CLOSING, - SERVICE_TOGGLE, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, - SERVICE_TOGGLE_COVER_TILT, SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, + SERVICE_TOGGLE, + SERVICE_TOGGLE_COVER_TILT, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index 3139c2a45db962..71ec5c385dc848 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -1,9 +1,9 @@ """Test cases around the demo fan platform.""" import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import fan from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component from tests.components.fan import common diff --git a/tests/components/demo/test_geo_location.py b/tests/components/demo/test_geo_location.py index a95d7e4e807a25..b7b11a7b46d49a 100644 --- a/tests/components/demo/test_geo_location.py +++ b/tests/components/demo/test_geo_location.py @@ -4,17 +4,18 @@ from homeassistant.components import geo_location from homeassistant.components.demo.geo_location import ( - NUMBER_OF_DEMO_DEVICES, DEFAULT_UNIT_OF_MEASUREMENT, DEFAULT_UPDATE_INTERVAL, + NUMBER_OF_DEMO_DEVICES, ) from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + from tests.common import ( - get_test_home_assistant, assert_setup_component, fire_time_changed, + get_test_home_assistant, ) -import homeassistant.util.dt as dt_util CONFIG = {geo_location.DOMAIN: [{"platform": "demo"}]} diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 5a420f768825b6..422ca55b399df4 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -4,10 +4,10 @@ import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import demo from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.helpers.json import JSONEncoder +from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index 34b340b600acba..48409d6cc377b3 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -1,8 +1,8 @@ """The tests for the demo light component.""" import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import light +from homeassistant.setup import async_setup_component from tests.components.light import common diff --git a/tests/components/demo/test_lock.py b/tests/components/demo/test_lock.py index 1c4264f1b535bd..279bd35d12a0a7 100644 --- a/tests/components/demo/test_lock.py +++ b/tests/components/demo/test_lock.py @@ -1,8 +1,8 @@ """The tests for the Demo lock platform.""" import unittest -from homeassistant.setup import setup_component from homeassistant.components import lock +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_service from tests.components.lock import common diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 8ff96082f2510c..60402844d24832 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -1,14 +1,14 @@ """The tests for the Demo Media player platform.""" +import asyncio import unittest from unittest.mock import patch -import asyncio import pytest import voluptuous as vol -from homeassistant.setup import setup_component, async_setup_component import homeassistant.components.media_player as mp from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant from tests.components.media_player import common diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index becfb49d2c15b3..30fb49be47d356 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -5,11 +5,11 @@ import pytest import voluptuous as vol -import homeassistant.components.notify as notify -from homeassistant.setup import setup_component import homeassistant.components.demo.notify as demo +import homeassistant.components.notify as notify from homeassistant.core import callback from homeassistant.helpers import discovery, script +from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant from tests.components.notify import common diff --git a/tests/components/demo/test_remote.py b/tests/components/demo/test_remote.py index a1a0e541b05a02..b83eaca4c9c8db 100644 --- a/tests/components/demo/test_remote.py +++ b/tests/components/demo/test_remote.py @@ -2,9 +2,9 @@ # pylint: disable=protected-access import unittest -from homeassistant.setup import setup_component import homeassistant.components.remote as remote -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant from tests.components.remote import common diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py index 5933b976460e94..3fe4e22396124c 100644 --- a/tests/components/demo/test_stt.py +++ b/tests/components/demo/test_stt.py @@ -1,8 +1,8 @@ """The tests for the demo stt component.""" import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import stt +from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) diff --git a/tests/components/demo/test_vacuum.py b/tests/components/demo/test_vacuum.py index c18241847fc5f6..13f1b1e352c777 100644 --- a/tests/components/demo/test_vacuum.py +++ b/tests/components/demo/test_vacuum.py @@ -2,6 +2,15 @@ import unittest from homeassistant.components import vacuum +from homeassistant.components.demo.vacuum import ( + DEMO_VACUUM_BASIC, + DEMO_VACUUM_COMPLETE, + DEMO_VACUUM_MINIMAL, + DEMO_VACUUM_MOST, + DEMO_VACUUM_NONE, + DEMO_VACUUM_STATE, + FAN_SPEEDS, +) from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, ATTR_COMMAND, @@ -13,21 +22,12 @@ ENTITY_ID_ALL_VACUUMS, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, - STATE_DOCKED, STATE_CLEANING, - STATE_PAUSED, + STATE_DOCKED, STATE_IDLE, + STATE_PAUSED, STATE_RETURNING, ) -from homeassistant.components.demo.vacuum import ( - DEMO_VACUUM_BASIC, - DEMO_VACUUM_COMPLETE, - DEMO_VACUUM_MINIMAL, - DEMO_VACUUM_MOST, - DEMO_VACUUM_NONE, - DEMO_VACUUM_STATE, - FAN_SPEEDS, -) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -40,7 +40,6 @@ from tests.common import get_test_home_assistant, mock_service from tests.components.vacuum import common - ENTITY_VACUUM_BASIC = "{}.{}".format(DOMAIN, DEMO_VACUUM_BASIC).lower() ENTITY_VACUUM_COMPLETE = "{}.{}".format(DOMAIN, DEMO_VACUUM_COMPLETE).lower() ENTITY_VACUUM_MINIMAL = "{}.{}".format(DOMAIN, DEMO_VACUUM_MINIMAL).lower() diff --git a/tests/components/demo/test_water_heater.py b/tests/components/demo/test_water_heater.py index 8a57c3c61f0a48..97efd48be4af08 100644 --- a/tests/components/demo/test_water_heater.py +++ b/tests/components/demo/test_water_heater.py @@ -4,14 +4,13 @@ import pytest import voluptuous as vol -from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from homeassistant.setup import setup_component from homeassistant.components import water_heater +from homeassistant.setup import setup_component +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from tests.common import get_test_home_assistant from tests.components.water_heater import common - ENTITY_WATER_HEATER = "water_heater.demo_water_heater" ENTITY_WATER_HEATER_CELSIUS = "water_heater.demo_water_heater_celsius" From fbd4cf1089d4459a9ddacf7db6ee877d842119ff Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 18:01:12 +0100 Subject: [PATCH 2213/3953] use isort to sort imports according to PEP8 for cloud (#29626) --- .../components/cloud/account_link.py | 2 +- .../components/cloud/alexa_config.py | 15 +++++------ .../components/cloud/binary_sensor.py | 1 - homeassistant/components/cloud/client.py | 17 ++++++------ .../components/cloud/google_config.py | 8 +++--- homeassistant/components/cloud/prefs.py | 26 +++++++++---------- homeassistant/components/cloud/tts.py | 2 +- homeassistant/components/cloud/utils.py | 2 +- tests/components/cloud/__init__.py | 2 +- tests/components/cloud/test_account_link.py | 6 ++--- tests/components/cloud/test_alexa_config.py | 7 ++--- tests/components/cloud/test_binary_sensor.py | 2 +- tests/components/cloud/test_client.py | 13 +++++----- tests/components/cloud/test_google_config.py | 8 +++--- tests/components/cloud/test_http_api.py | 18 ++++++------- tests/components/cloud/test_init.py | 5 ++-- tests/components/cloud/test_prefs.py | 2 +- 17 files changed, 68 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 9ec1fe634d771b..1d0de26918d8fe 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -7,7 +7,7 @@ from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import event, config_entry_oauth2_flow +from homeassistant.helpers import config_entry_oauth2_flow, event from .const import DOMAIN diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index a1432f196bf5d9..45e1fab1101df7 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -7,24 +7,23 @@ import async_timeout from hass_nabucasa import cloud_api -from homeassistant.core import callback -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES -from homeassistant.helpers import entity_registry -from homeassistant.helpers.event import async_call_later -from homeassistant.util.dt import utcnow from homeassistant.components.alexa import ( config as alexa_config, - errors as alexa_errors, entities as alexa_entities, + errors as alexa_errors, state_report as alexa_state_report, ) - +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.core import callback +from homeassistant.helpers import entity_registry +from homeassistant.helpers.event import async_call_later +from homeassistant.util.dt import utcnow from .const import ( CONF_ENTITY_CONFIG, CONF_FILTER, - PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, + PREF_SHOULD_EXPOSE, RequireRelink, ) diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 2192eec89230ea..056105f8071014 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -6,7 +6,6 @@ from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN - WAIT_UNTIL_CHANGE = 3 diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 956d35caf2d7ae..24947ed795216c 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -1,27 +1,26 @@ """Interface implementation for cloud client.""" import asyncio +import logging from pathlib import Path from typing import Any, Dict -import logging import aiohttp from hass_nabucasa.client import CloudClient as Interface -from homeassistant.core import callback, Context -from homeassistant.components.google_assistant import smart_home as ga -from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.util.aiohttp import MockRequest from homeassistant.components.alexa import ( - smart_home as alexa_sh, errors as alexa_errors, + smart_home as alexa_sh, ) +from homeassistant.components.google_assistant import smart_home as ga +from homeassistant.core import Context, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.aiohttp import MockRequest -from . import utils, alexa_config, google_config +from . import alexa_config, google_config, utils from .const import DISPATCHER_REMOTE_UPDATE from .prefs import CloudPreferences - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 3df06c140a05ed..1ff87bf95f501d 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -5,16 +5,16 @@ import async_timeout from hass_nabucasa.google_report_state import ErrorResponse -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.components.google_assistant.helpers import AbstractConfig +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers import entity_registry from .const import ( - PREF_SHOULD_EXPOSE, - DEFAULT_SHOULD_EXPOSE, CONF_ENTITY_CONFIG, - PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA, + DEFAULT_SHOULD_EXPOSE, + PREF_DISABLE_2FA, + PREF_SHOULD_EXPOSE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index e96ee9527fb95c..a7d1b59fd39884 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -2,31 +2,31 @@ from ipaddress import ip_address from typing import Optional -from homeassistant.core import callback -from homeassistant.auth.models import User from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.auth.models import User +from homeassistant.core import callback from homeassistant.util.logging import async_create_catching_coro from .const import ( + DEFAULT_ALEXA_REPORT_STATE, + DEFAULT_GOOGLE_REPORT_STATE, DOMAIN, + PREF_ALEXA_ENTITY_CONFIGS, + PREF_ALEXA_REPORT_STATE, + PREF_ALIASES, + PREF_CLOUD_USER, + PREF_CLOUDHOOKS, + PREF_DISABLE_2FA, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, - PREF_GOOGLE_SECURE_DEVICES_PIN, - PREF_CLOUDHOOKS, - PREF_CLOUD_USER, PREF_GOOGLE_ENTITY_CONFIGS, + PREF_GOOGLE_LOCAL_WEBHOOK_ID, + PREF_GOOGLE_REPORT_STATE, + PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_OVERRIDE_NAME, - PREF_DISABLE_2FA, - PREF_ALIASES, PREF_SHOULD_EXPOSE, - PREF_ALEXA_ENTITY_CONFIGS, - PREF_ALEXA_REPORT_STATE, PREF_USERNAME, - DEFAULT_ALEXA_REPORT_STATE, - PREF_GOOGLE_REPORT_STATE, - PREF_GOOGLE_LOCAL_WEBHOOK_ID, - DEFAULT_GOOGLE_REPORT_STATE, InvalidTrustedNetworks, InvalidTrustedProxies, ) diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 338b97d2bd9427..ea769c6a054df0 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -1,7 +1,7 @@ """Support for the cloud for text to speech service.""" -from hass_nabucasa.voice import VoiceError from hass_nabucasa import Cloud +from hass_nabucasa.voice import VoiceError import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider diff --git a/homeassistant/components/cloud/utils.py b/homeassistant/components/cloud/utils.py index 5040baada9a172..36599b42ad336a 100644 --- a/homeassistant/components/cloud/utils.py +++ b/homeassistant/components/cloud/utils.py @@ -1,7 +1,7 @@ """Helper functions for cloud components.""" from typing import Any, Dict -from aiohttp import web, payload +from aiohttp import payload, web def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]: diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 45ea4e43ee4347..571b73e8d09abf 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1,9 +1,9 @@ """Tests for the cloud component.""" from unittest.mock import patch -from homeassistant.setup import async_setup_component from homeassistant.components import cloud from homeassistant.components.cloud import const +from homeassistant.setup import async_setup_component from tests.common import mock_coro diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 60116895beb408..a8c247cc9852cb 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -6,12 +6,12 @@ import pytest -from homeassistant import data_entry_flow, config_entries -from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.cloud import account_link +from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow -from tests.common import mock_coro, async_fire_time_changed, mock_platform +from tests.common import async_fire_time_changed, mock_coro, mock_platform TEST_DOMAIN = "oauth2_test" diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index a7c8898659a432..508626b43f0136 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -1,11 +1,12 @@ """Test Alexa config.""" import contextlib -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config -from homeassistant.util.dt import utcnow from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED -from tests.common import mock_coro, async_fire_time_changed +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed, mock_coro async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index 99ae2f43bc50d5..24b0563890bf5a 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -1,8 +1,8 @@ """Tests for the cloud binary sensor.""" from unittest.mock import Mock -from homeassistant.setup import async_setup_component from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE +from homeassistant.setup import async_setup_component async def test_remote_connection_sensor(hass): diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 955923c1e68ebb..b3bfebb0ee7c7d 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -1,18 +1,19 @@ """Test the cloud.iot module.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from aiohttp import web import pytest -from homeassistant.core import State -from homeassistant.setup import async_setup_component from homeassistant.components.cloud import DOMAIN from homeassistant.components.cloud.client import CloudClient from homeassistant.components.cloud.const import PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE -from tests.components.alexa import test_smart_home as test_alexa -from tests.common import mock_coro +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +from . import mock_cloud, mock_cloud_prefs -from . import mock_cloud_prefs, mock_cloud +from tests.common import mock_coro +from tests.components.alexa import test_smart_home as test_alexa @pytest.fixture diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 3510b4b8abd203..830751029d72af 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -1,13 +1,13 @@ """Test the Cloud Google Config.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch -from homeassistant.components.google_assistant import helpers as ga_helpers from homeassistant.components.cloud import GACTIONS_SCHEMA from homeassistant.components.cloud.google_config import CloudGoogleConfig -from homeassistant.util.dt import utcnow +from homeassistant.components.google_assistant import helpers as ga_helpers from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED +from homeassistant.util.dt import utcnow -from tests.common import mock_coro, async_fire_time_changed +from tests.common import async_fire_time_changed, mock_coro async def test_google_update_report_state(hass, cloud_prefs): diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 440ad7a9c89081..515489035fb373 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,26 +1,26 @@ """Tests for the HTTP API for the cloud component.""" import asyncio -from unittest.mock import patch, MagicMock from ipaddress import ip_network +from unittest.mock import MagicMock, patch -import pytest -from jose import jwt +from hass_nabucasa import thingtalk from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED -from hass_nabucasa import thingtalk +from jose import jwt +import pytest -from homeassistant.core import State from homeassistant.auth.providers import trusted_networks as tn_auth +from homeassistant.components.alexa import errors as alexa_errors +from homeassistant.components.alexa.entities import LightCapabilities from homeassistant.components.cloud.const import DOMAIN, RequireRelink from homeassistant.components.google_assistant.helpers import GoogleEntity -from homeassistant.components.alexa.entities import LightCapabilities -from homeassistant.components.alexa import errors as alexa_errors +from homeassistant.core import State + +from . import mock_cloud, mock_cloud_prefs from tests.common import mock_coro from tests.components.google_assistant import MockConfig -from . import mock_cloud, mock_cloud_prefs - GOOGLE_ACTIONS_SYNC_URL = "https://api-test.hass.io/google_actions_sync" SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/subscription_info" diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index d039cdd1b0b802..5d0ba76f80b337 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -3,13 +3,14 @@ import pytest -from homeassistant.core import Context -from homeassistant.exceptions import Unauthorized from homeassistant.components import cloud from homeassistant.components.cloud.const import DOMAIN from homeassistant.components.cloud.prefs import STORAGE_KEY from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Context +from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component + from tests.common import mock_coro diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index 1678757e52cf87..d1b6f9ed867dab 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant.auth.const import GROUP_ID_ADMIN -from homeassistant.components.cloud.prefs import CloudPreferences, STORAGE_KEY +from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences async def test_set_username(hass): From 0d5de6a464dcdbc1b5768d6c7b140aac5b2429bc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 18:01:50 +0100 Subject: [PATCH 2214/3953] use isort to sort imports according to PEP8 for ecobee (#29631) --- homeassistant/components/ecobee/__init__.py | 6 ++-- .../components/ecobee/binary_sensor.py | 4 +-- homeassistant/components/ecobee/climate.py | 34 +++++++++---------- .../components/ecobee/config_flow.py | 9 +++-- homeassistant/components/ecobee/notify.py | 2 +- homeassistant/components/ecobee/sensor.py | 2 +- homeassistant/components/ecobee/util.py | 1 + homeassistant/components/ecobee/weather.py | 2 +- tests/components/ecobee/test_climate.py | 3 +- tests/components/ecobee/test_config_flow.py | 3 +- 10 files changed, 34 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index eb65a7ed4269bc..80c3be7954b4ac 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -1,9 +1,9 @@ """Support for ecobee.""" import asyncio from datetime import timedelta -import voluptuous as vol -from pyecobee import Ecobee, ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, ExpiredTokenError +from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, Ecobee, ExpiredTokenError +import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY @@ -11,11 +11,11 @@ from homeassistant.util import Throttle from .const import ( + _LOGGER, CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, ECOBEE_PLATFORMS, - _LOGGER, ) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 06289572aeaacb..f7a24886b84826 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -1,10 +1,10 @@ """Support for Ecobee binary sensors.""" from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_OCCUPANCY, + BinarySensorDevice, ) -from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER +from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index c583f9696d24cb..83a1453a23ae4b 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -6,37 +6,37 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + FAN_AUTO, + FAN_ON, + HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, - HVAC_MODE_AUTO, HVAC_MODE_OFF, - ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_HIGH, - SUPPORT_TARGET_TEMPERATURE, + PRESET_AWAY, + PRESET_NONE, SUPPORT_AUX_HEAT, - SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_FAN_MODE, - PRESET_AWAY, - FAN_AUTO, - FAN_ON, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, SUPPORT_PRESET_MODE, - PRESET_NONE, - CURRENT_HVAC_FAN, - CURRENT_HVAC_DRY, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, ATTR_TEMPERATURE, + STATE_ON, TEMP_FAHRENHEIT, ) -from homeassistant.util.temperature import convert import homeassistant.helpers.config_validation as cv +from homeassistant.util.temperature import convert -from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER +from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER from .util import ecobee_date, ecobee_time ATTR_COOL_TEMP = "cool_temp" diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py index 56ce13f7701b99..bb406d81e3a3a9 100644 --- a/homeassistant/components/ecobee/config_flow.py +++ b/homeassistant/components/ecobee/config_flow.py @@ -1,19 +1,18 @@ """Config flow to configure ecobee.""" -import voluptuous as vol - from pyecobee import ( - Ecobee, - ECOBEE_CONFIG_FILENAME, ECOBEE_API_KEY, + ECOBEE_CONFIG_FILENAME, ECOBEE_REFRESH_TOKEN, + Ecobee, ) +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistantError from homeassistant.util.json import load_json -from .const import CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, _LOGGER +from .const import _LOGGER, CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index c7b3f47d29c17f..a8f53a027b370c 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -1,8 +1,8 @@ """Support for Ecobee Send Message service.""" import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import BaseNotificationService, PLATFORM_SCHEMA from .const import CONF_INDEX, DOMAIN diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 76945080bfa685..37201ec2121a93 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -8,7 +8,7 @@ ) from homeassistant.helpers.entity import Entity -from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER +from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER SENSOR_TYPES = { "temperature": ["Temperature", TEMP_FAHRENHEIT], diff --git a/homeassistant/components/ecobee/util.py b/homeassistant/components/ecobee/util.py index 3acc3e5676d8d6..2f5d194fec04d0 100644 --- a/homeassistant/components/ecobee/util.py +++ b/homeassistant/components/ecobee/util.py @@ -1,5 +1,6 @@ """Validation utility functions for ecobee services.""" from datetime import datetime + import voluptuous as vol diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 7b057f09a0cbb3..a571e854f73586 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -15,11 +15,11 @@ from homeassistant.const import TEMP_FAHRENHEIT from .const import ( + _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, ECOBEE_WEATHER_SYMBOL_TO_HASS, MANUFACTURER, - _LOGGER, ) diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 90a9a6417765a7..0c0ca7850267c9 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -1,8 +1,9 @@ """The test for the Ecobee thermostat module.""" import unittest from unittest import mock -import homeassistant.const as const + from homeassistant.components.ecobee import climate as ecobee +import homeassistant.const as const from homeassistant.const import STATE_OFF diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 64f0e3df0e7850..6b53af5daa87ed 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -1,8 +1,8 @@ """Tests for the ecobee config flow.""" -import pytest from unittest.mock import patch from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN +import pytest from homeassistant import data_entry_flow from homeassistant.components.ecobee import config_flow @@ -12,6 +12,7 @@ DOMAIN, ) from homeassistant.const import CONF_API_KEY + from tests.common import MockConfigEntry, mock_coro From d2c1e5d45c748ec645c88a14755ae8a9e6b4bde6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 18:14:26 +0100 Subject: [PATCH 2215/3953] Sort imports according to PEP8 for homekit_controller (#29646) --- tests/components/homekit_controller/common.py | 13 +++++++------ tests/components/homekit_controller/test_climate.py | 8 ++++---- .../homekit_controller/test_config_flow.py | 2 +- tests/components/homekit_controller/test_light.py | 1 - tests/components/homekit_controller/test_storage.py | 10 +++++----- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 9a404701c54fb9..b743f84f73c0f6 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -1,28 +1,29 @@ """Code to support homekit_controller tests.""" +from datetime import timedelta import json import os -from datetime import timedelta from unittest import mock -from homekit.model.services import AbstractService, ServicesTypes +from homekit.exceptions import AccessoryNotFoundError +from homekit.model import Accessory, get_id from homekit.model.characteristics import ( AbstractCharacteristic, CharacteristicPermissions, CharacteristicsTypes, ) -from homekit.model import Accessory, get_id -from homekit.exceptions import AccessoryNotFoundError +from homekit.model.services import AbstractService, ServicesTypes from homeassistant import config_entries +from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import ( CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, ) -from homeassistant.components.homekit_controller import config_flow from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed, load_fixture, MockConfigEntry + +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture class FakePairing: diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 0d3544a6f55883..e076b2975e2c71 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -1,16 +1,16 @@ """Basic checks for HomeKitclimate.""" from homeassistant.components.climate.const import ( DOMAIN, - SERVICE_SET_HVAC_MODE, - SERVICE_SET_TEMPERATURE, - HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SERVICE_SET_HUMIDITY, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, ) -from tests.components.homekit_controller.common import FakeService, setup_test_component +from tests.components.homekit_controller.common import FakeService, setup_test_component HEATING_COOLING_TARGET = ("thermostat", "heating-cooling.target") HEATING_COOLING_CURRENT = ("thermostat", "heating-cooling.current") diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 81d5ac9a7516ae..22b486e1d5193f 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -7,6 +7,7 @@ from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES + from tests.common import MockConfigEntry from tests.components.homekit_controller.common import ( Accessory, @@ -14,7 +15,6 @@ setup_platform, ) - PAIRING_START_FORM_ERRORS = [ (homekit.BusyError, "busy_error"), (homekit.MaxTriesError, "max_tries_error"), diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index 1608f097ed0c9e..b558160a9f233d 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -3,7 +3,6 @@ from tests.components.homekit_controller.common import FakeService, setup_test_component - LIGHT_ON = ("lightbulb", "on") LIGHT_BRIGHTNESS = ("lightbulb", "brightness") LIGHT_HUE = ("lightbulb", "hue") diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index 4fcf035ae4865e..39b0d9d8250565 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -1,15 +1,15 @@ """Basic checks for entity map storage.""" +from homeassistant import config_entries +from homeassistant.components.homekit_controller import async_remove_entry +from homeassistant.components.homekit_controller.const import ENTITY_MAP + from tests.common import flush_store from tests.components.homekit_controller.common import ( FakeService, - setup_test_component, setup_platform, + setup_test_component, ) -from homeassistant import config_entries -from homeassistant.components.homekit_controller import async_remove_entry -from homeassistant.components.homekit_controller.const import ENTITY_MAP - async def test_load_from_storage(hass, hass_storage): """Test that entity map can be correctly loaded from cache.""" From ce5072fc9171aed7b27eecffb29a8ed1f977486c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 18:15:26 +0100 Subject: [PATCH 2216/3953] use isort to sort imports according to PEP8 for mqtt (#29649) --- homeassistant/components/mqtt/__init__.py | 12 ++++++------ homeassistant/components/mqtt/camera.py | 2 +- homeassistant/components/mqtt/climate.py | 4 ++-- homeassistant/components/mqtt/device_tracker.py | 2 +- homeassistant/components/mqtt/models.py | 2 +- tests/components/mqtt/test_binary_sensor.py | 1 - tests/components/mqtt/test_camera.py | 2 +- tests/components/mqtt/test_climate.py | 16 ++++++++-------- tests/components/mqtt/test_cover.py | 4 ++-- tests/components/mqtt/test_discovery.py | 1 - tests/components/mqtt/test_init.py | 2 +- tests/components/mqtt/test_sensor.py | 2 +- tests/components/mqtt/test_state_vacuum.py | 2 +- 13 files changed, 25 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ad9166e24106a9..d8dc584ae30e61 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,6 +1,5 @@ """Support for MQTT message handling.""" import asyncio -import sys from functools import partial, wraps import inspect from itertools import groupby @@ -10,6 +9,7 @@ import os import socket import ssl +import sys import time from typing import Any, Callable, List, Optional, Union @@ -32,9 +32,9 @@ ) from homeassistant.core import Event, ServiceCall, callback from homeassistant.exceptions import ( + ConfigEntryNotReady, HomeAssistantError, Unauthorized, - ConfigEntryNotReady, ) from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -47,16 +47,16 @@ # Loading the config flow file will register the flow from . import config_flow, discovery, server # noqa: F401 pylint: disable=unused-import from .const import ( + ATTR_DISCOVERY_HASH, CONF_BROKER, CONF_DISCOVERY, - DEFAULT_DISCOVERY, CONF_STATE_TOPIC, - ATTR_DISCOVERY_HASH, - PROTOCOL_311, + DEFAULT_DISCOVERY, DEFAULT_QOS, + PROTOCOL_311, ) from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash -from .models import PublishPayloadType, Message, MessageCallbackType +from .models import Message, MessageCallbackType, PublishPayloadType from .subscription import async_subscribe_topics, async_unsubscribe_topics _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index f3ae36c5746855..831c47c3621a55 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -7,7 +7,7 @@ from homeassistant.components import camera, mqtt from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_NAME, CONF_DEVICE +from homeassistant.const import CONF_DEVICE, CONF_NAME from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 9b46057a414e1a..a51590e0b59cf1 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -20,14 +20,14 @@ HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_AWAY, SUPPORT_TARGET_TEMPERATURE_RANGE, - PRESET_NONE, ) from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ( diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index d25d7ce21d319b..bcc969f0354137 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -5,9 +5,9 @@ from homeassistant.components import mqtt from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES +from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_DEVICES, STATE_NOT_HOME, STATE_HOME from . import CONF_QOS diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 46aaa23732f067..cfdecd3383d120 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -1,5 +1,5 @@ """Modesl used by multiple MQTT modules.""" -from typing import Union, Callable +from typing import Callable, Union import attr diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 28f1a7e9720091..3e8f342ea9424c 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1,7 +1,6 @@ """The tests for the MQTT binary sensor platform.""" from datetime import datetime, timedelta import json - from unittest.mock import ANY, patch from homeassistant.components import binary_sensor, mqtt diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 70b5e941fe3868..0e7d8ada759cad 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,6 +1,6 @@ """The tests for mqtt camera component.""" -from unittest.mock import ANY import json +from unittest.mock import ANY from homeassistant.components import camera, mqtt from homeassistant.components.mqtt.discovery import async_start diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 648448a6494b75..2db368d0311e16 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -11,19 +11,19 @@ from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.components.climate.const import ( DOMAIN as CLIMATE_DOMAIN, - SUPPORT_AUX_HEAT, - SUPPORT_PRESET_MODE, - SUPPORT_FAN_MODE, - SUPPORT_SWING_MODE, - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_AUTO, HVAC_MODE_COOL, - HVAC_MODE_HEAT, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, - SUPPORT_TARGET_TEMPERATURE_RANGE, - PRESET_NONE, + HVAC_MODE_HEAT, PRESET_ECO, + PRESET_NONE, + SUPPORT_AUX_HEAT, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index bb734d2c03df65..b15518961a4a98 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -16,11 +16,11 @@ SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + SERVICE_TOGGLE, + SERVICE_TOGGLE_COVER_TILT, STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, - SERVICE_TOGGLE, - SERVICE_TOGGLE_COVER_TILT, STATE_UNKNOWN, ) from homeassistant.setup import async_setup_component diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 860ef52a98a68b..2b6c65b919e220 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1,7 +1,6 @@ """The tests for the MQTT discovery.""" from pathlib import Path import re - from unittest.mock import patch from homeassistant.components import mqtt diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 6f2a87d1caa997..3d8e261fdb2378 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -15,8 +15,8 @@ EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback -from homeassistant.setup import async_setup_component from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index cd55a08482d5f7..4e8e5f9bfd0197 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -14,8 +14,8 @@ from tests.common import ( MockConfigEntry, async_fire_mqtt_message, - async_mock_mqtt_component, async_fire_time_changed, + async_mock_mqtt_component, mock_registry, ) diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 7919e07767d50f..eb3071eb1205cf 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -26,9 +26,9 @@ from homeassistant.const import ( CONF_NAME, CONF_PLATFORM, + ENTITY_MATCH_ALL, STATE_UNAVAILABLE, STATE_UNKNOWN, - ENTITY_MATCH_ALL, ) from homeassistant.setup import async_setup_component From aeff27680bfb975fce948f33381f631c7d6413b3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 18:16:23 +0100 Subject: [PATCH 2217/3953] use isort to sort imports according to PEP8 for light (#29648) --- homeassistant/components/light/__init__.py | 3 +- .../components/light/device_action.py | 7 ++-- .../components/light/device_condition.py | 7 ++-- .../components/light/device_trigger.py | 5 +-- homeassistant/components/light/intent.py | 9 +++-- .../components/light/reproduce_state.py | 6 ++-- .../components/mqtt/light/__init__.py | 3 +- .../components/mqtt/light/schema_basic.py | 34 +++++++++---------- .../components/mqtt/light/schema_template.py | 23 +++++++------ tests/components/light/common.py | 2 +- tests/components/light/test_device_action.py | 8 ++--- .../components/light/test_device_condition.py | 13 +++---- tests/components/light/test_device_trigger.py | 11 +++--- tests/components/light/test_init.py | 24 ++++++------- tests/components/light/test_intent.py | 6 ++-- 15 files changed, 81 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index f01258e2ab477e..8a2a61d042152c 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -17,7 +17,7 @@ SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.exceptions import UnknownUser, Unauthorized +from homeassistant.exceptions import Unauthorized, UnknownUser import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -29,7 +29,6 @@ from homeassistant.loader import bind_hass import homeassistant.util.color as color_util - # mypy: allow-untyped-defs, no-check-untyped-defs DOMAIN = "light" diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 9d8ef6bceafb32..c436ce7886ae69 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -1,13 +1,14 @@ """Provides device actions for lights.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, Context from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import TemplateVarsType, ConfigType -from . import DOMAIN +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from . import DOMAIN ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index e87ae3bf9450de..d27953749f67d2 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -1,14 +1,15 @@ """Provides device conditions for lights.""" from typing import Dict, List + import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import ConfigType +from homeassistant.core import HomeAssistant from homeassistant.helpers.condition import ConditionCheckerType -from . import DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 432d24d3c14d90..066d1f4c0205b7 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -1,14 +1,15 @@ """Provides device trigger for lights.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, CALLBACK_TYPE from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import DOMAIN +from . import DOMAIN TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index 93b9748fc4a3da..ea8899c44fc1d6 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -3,20 +3,19 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import intent -import homeassistant.util.color as color_util import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util from . import ( + ATTR_BRIGHTNESS_PCT, ATTR_ENTITY_ID, - SUPPORT_COLOR, ATTR_RGB_COLOR, - ATTR_BRIGHTNESS_PCT, - SUPPORT_BRIGHTNESS, DOMAIN, SERVICE_TURN_ON, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, ) - INTENT_SET = "HassLightSet" diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 90d14c2a19f980..59a4b0306d08ef 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -6,16 +6,15 @@ from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType from . import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, @@ -29,6 +28,7 @@ ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + DOMAIN, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 95a850fb9e864c..a72008c059f10a 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -15,7 +15,8 @@ clear_discovery_hash, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 829809dd9c31d7..ff57db7c8c1d50 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -8,7 +8,6 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -16,12 +15,24 @@ ATTR_EFFECT, ATTR_HS_COLOR, ATTR_WHITE_VALUE, - Light, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - SUPPORT_COLOR, SUPPORT_WHITE_VALUE, + Light, +) +from homeassistant.components.mqtt import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + CONF_UNIQUE_ID, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, ) from homeassistant.const import ( CONF_BRIGHTNESS, @@ -33,27 +44,16 @@ CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, - STATE_ON, CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY, + STATE_ON, ) -from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - CONF_UNIQUE_ID, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index c80ab2f95a7c26..dd69a8e87d68d5 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -5,9 +5,9 @@ https://home-assistant.io/components/light.mqtt_template/ """ import logging + import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -17,21 +17,14 @@ ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, - Light, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, -) -from homeassistant.const import ( - CONF_DEVICE, - CONF_NAME, - CONF_OPTIMISTIC, - STATE_ON, - STATE_OFF, + Light, ) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, @@ -45,9 +38,17 @@ MqttEntityDeviceInfo, subscription, ) +from homeassistant.const import ( + CONF_DEVICE, + CONF_NAME, + CONF_OPTIMISTIC, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.color as color_util from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 32678bf4daa0da..aa1e62db5bf09d 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -21,10 +21,10 @@ ) from homeassistant.const import ( ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index bb50778db52f70..a3cf57a7dbe96c 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -1,18 +1,18 @@ """The test for light device automation.""" import pytest -from homeassistant.components.light import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.light import DOMAIN +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index a9f4adddfabfe1..7a560dd781d7f6 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -1,22 +1,23 @@ """The test for light device automation.""" from datetime import timedelta -import pytest from unittest.mock import patch -from homeassistant.components.light import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component +import pytest + import homeassistant.components.automation as automation +from homeassistant.components.light import DOMAIN +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index a6437ef9ee0a64..dd8320c166e4a7 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -1,22 +1,23 @@ """The test for light device automation.""" from datetime import timedelta + import pytest -from homeassistant.components.light import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.light import DOMAIN +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, async_fire_time_changed, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 2cf13369bd9857..18cc032bd984bd 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1,31 +1,27 @@ """The tests for the Light component.""" # pylint: disable=protected-access +from io import StringIO +import os import unittest import unittest.mock as mock -import os -from io import StringIO import pytest from homeassistant import core -from homeassistant.exceptions import Unauthorized -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.components import light from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, CONF_PLATFORM, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) -from homeassistant.components import light +from homeassistant.exceptions import Unauthorized +from homeassistant.setup import async_setup_component, setup_component -from tests.common import ( - mock_service, - get_test_home_assistant, - mock_storage, -) +from tests.common import get_test_home_assistant, mock_service, mock_storage from tests.components.light import common diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py index 594c9a5d1fc77e..4adba921d5ed36 100644 --- a/tests/components/light/test_intent.py +++ b/tests/components/light/test_intent.py @@ -1,9 +1,9 @@ """Tests for the light intents.""" -from homeassistant.helpers.intent import IntentHandleError - -from homeassistant.const import ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON, ATTR_ENTITY_ID from homeassistant.components import light from homeassistant.components.light import intent +from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON +from homeassistant.helpers.intent import IntentHandleError + from tests.common import async_mock_service From b731ddabde71b54fe295e76c26896b884b71ea9e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 18:16:49 +0100 Subject: [PATCH 2218/3953] Sort imports according to PEP8 for homekit (#29645) --- homeassistant/components/homekit/__init__.py | 3 +-- .../components/homekit/type_media_players.py | 20 +++++++++---------- .../components/homekit/type_thermostats.py | 4 ++-- tests/components/homekit/conftest.py | 3 +-- tests/components/homekit/test_accessories.py | 8 ++++---- .../homekit/test_get_accessories.py | 10 +++++----- tests/components/homekit/test_homekit.py | 14 ++++++------- tests/components/homekit/test_type_lights.py | 4 ++-- .../homekit/test_type_media_players.py | 2 +- tests/components/homekit/test_type_sensors.py | 4 ++-- .../homekit/test_type_thermostats.py | 4 ++-- 11 files changed, 36 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 525c091e177ba8..cfd59ad61a4f60 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -45,8 +45,8 @@ DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START, SERVICE_HOMEKIT_RESET_ACCESSORY, + SERVICE_HOMEKIT_START, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, @@ -54,7 +54,6 @@ TYPE_SWITCH, TYPE_VALVE, ) - from .util import ( show_setup_message, validate_entity_config, diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index d9b24782610dbb..450ae818ec87e0 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -6,16 +6,16 @@ from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, - ATTR_MEDIA_VOLUME_MUTED, ATTR_MEDIA_VOLUME_LEVEL, - SERVICE_SELECT_SOURCE, + ATTR_MEDIA_VOLUME_MUTED, DOMAIN, + SERVICE_SELECT_SOURCE, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - SUPPORT_SELECT_SOURCE, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -26,13 +26,13 @@ SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_VOLUME_MUTE, - SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, STATE_OFF, - STATE_PLAYING, STATE_PAUSED, + STATE_PLAYING, STATE_UNKNOWN, ) @@ -46,23 +46,23 @@ CHAR_IDENTIFIER, CHAR_INPUT_SOURCE_TYPE, CHAR_IS_CONFIGURED, - CHAR_NAME, - CHAR_SLEEP_DISCOVER_MODE, CHAR_MUTE, + CHAR_NAME, CHAR_ON, CHAR_REMOTE_KEY, + CHAR_SLEEP_DISCOVER_MODE, + CHAR_VOLUME, CHAR_VOLUME_CONTROL_TYPE, CHAR_VOLUME_SELECTOR, - CHAR_VOLUME, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, + SERV_INPUT_SOURCE, SERV_SWITCH, SERV_TELEVISION, SERV_TELEVISION_SPEAKER, - SERV_INPUT_SOURCE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index b6e1e75d3c6529..79a9d156f101fb 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -20,12 +20,12 @@ DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE, + HVAC_MODE_AUTO, HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - HVAC_MODE_AUTO, - HVAC_MODE_FAN_ONLY, SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, SUPPORT_TARGET_TEMPERATURE_RANGE, diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index c7e749fbad969a..ef534d0e47286f 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -1,13 +1,12 @@ """HomeKit session fixtures.""" from unittest.mock import patch +from pyhap.accessory_driver import AccessoryDriver import pytest from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED from homeassistant.core import callback as ha_callback -from pyhap.accessory_driver import AccessoryDriver - @pytest.fixture(scope="session") def hk_driver(): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 883d84339e5ce8..0c5810a5b10415 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -3,15 +3,15 @@ This includes tests for all mock object types. """ from datetime import datetime, timedelta -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest from homeassistant.components.homekit.accessories import ( - debounce, HomeAccessory, HomeBridge, HomeDriver, + debounce, ) from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, @@ -30,13 +30,13 @@ SERV_ACCESSORY_INFO, ) from homeassistant.const import ( - __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, - ATTR_SERVICE, ATTR_NOW, + ATTR_SERVICE, EVENT_TIME_CHANGED, + __version__, ) import homeassistant.util.dt as dt_util diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index eb15b46146118e..e6bf185c93b932 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,13 +1,11 @@ """Package to test the get_accessory method.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest -from homeassistant.core import State -import homeassistant.components.cover as cover import homeassistant.components.climate as climate -import homeassistant.components.media_player.const as media_player_c -from homeassistant.components.homekit import get_accessory, TYPES +import homeassistant.components.cover as cover +from homeassistant.components.homekit import TYPES, get_accessory from homeassistant.components.homekit.const import ( CONF_FEATURE_LIST, FEATURE_ON_OFF, @@ -18,6 +16,7 @@ TYPE_SWITCH, TYPE_VALVE, ) +import homeassistant.components.media_player.const as media_player_c from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, @@ -28,6 +27,7 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import State def test_not_supported(caplog): diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 97838eaa8525c2..de6aaf0f11eb58 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,35 +1,34 @@ """Tests for the HomeKit component.""" -from unittest.mock import patch, ANY, Mock +from unittest.mock import ANY, Mock, patch import pytest from homeassistant import setup - from homeassistant.components.homekit import ( - generate_aid, - HomeKit, MAX_DEVICES, STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT, + HomeKit, + generate_aid, ) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( + BRIDGE_NAME, CONF_AUTO_START, CONF_SAFE_MODE, - BRIDGE_NAME, DEFAULT_PORT, DEFAULT_SAFE_MODE, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START, SERVICE_HOMEKIT_RESET_ACCESSORY, + SERVICE_HOMEKIT_START, ) from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_NAME, CONF_IP_ADDRESS, + CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, @@ -39,7 +38,6 @@ from tests.components.homekit.common import patch_debounce - IP_ADDRESS = "127.0.0.1" PATH_HOMEKIT = "homeassistant.components.homekit" diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 784a6c82346174..3444b2778f6b57 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -11,14 +11,14 @@ ATTR_HS_COLOR, DOMAIN, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, ) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - STATE_ON, STATE_OFF, + STATE_ON, STATE_UNKNOWN, ) diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index f2682fc86b09f3..aa007b4d04c955 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -8,11 +8,11 @@ FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, ) -from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.homekit.type_media_players import ( MediaPlayer, TelevisionMediaPlayer, ) +from homeassistant.components.media_player import DEVICE_CLASS_TV from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index c2ed48732861ba..43533840cc6ae9 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -5,14 +5,14 @@ THRESHOLD_CO2, ) from homeassistant.components.homekit.type_sensors import ( + BINARY_SENSOR_SERVICE_MAP, AirQualitySensor, BinarySensor, - CarbonMonoxideSensor, CarbonDioxideSensor, + CarbonMonoxideSensor, HumiditySensor, LightSensor, TemperatureSensor, - BINARY_SENSOR_SERVICE_MAP, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 9f9ebcdfd32ee3..174b72f780a772 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -20,12 +20,12 @@ DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE, + HVAC_MODE_AUTO, HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_AUTO, ) from homeassistant.components.homekit.const import ( ATTR_VALUE, From 0a4979549d9e7a803589883c1715a1a8c982fb83 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 18:17:01 +0100 Subject: [PATCH 2219/3953] use isort to sort imports according to PEP8 for vacuum (#29650) --- .../components/mqtt/vacuum/__init__.py | 5 ++- .../components/mqtt/vacuum/schema_legacy.py | 19 ++++----- .../components/mqtt/vacuum/schema_state.py | 41 +++++++++---------- homeassistant/components/vacuum/__init__.py | 9 ++-- .../components/vacuum/device_action.py | 12 +++--- .../components/vacuum/device_condition.py | 10 +++-- .../components/vacuum/device_trigger.py | 14 ++++--- tests/components/vacuum/common.py | 4 +- tests/components/vacuum/test_device_action.py | 6 +-- .../vacuum/test_device_condition.py | 6 +-- .../components/vacuum/test_device_trigger.py | 6 +-- 11 files changed, 68 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 12fd4c51693457..84f564e5c7ea16 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -8,14 +8,15 @@ import voluptuous as vol -from homeassistant.components.vacuum import DOMAIN from homeassistant.components.mqtt import ATTR_DISCOVERY_HASH from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash, ) +from homeassistant.components.vacuum import DOMAIN from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .schema import CONF_SCHEMA, LEGACY, STATE, MQTT_VACUUM_SCHEMA + +from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index d770cfbb7f83d9..6c08b18bc9c399 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -1,10 +1,18 @@ """Support for Legacy MQTT vacuum.""" -import logging import json +import logging import voluptuous as vol from homeassistant.components import mqtt +from homeassistant.components.mqtt import ( + CONF_UNIQUE_ID, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, @@ -24,15 +32,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.components.mqtt import ( - CONF_UNIQUE_ID, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) - from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 40b3eeb752cd37..9dd5053d019277 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -1,27 +1,39 @@ """Support for a State MQTT vacuum.""" -import logging import json +import logging import voluptuous as vol from homeassistant.components import mqtt +from homeassistant.components.mqtt import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + CONF_UNIQUE_ID, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) from homeassistant.components.vacuum import ( + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, - SUPPORT_START, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, + SUPPORT_START, SUPPORT_STATUS, SUPPORT_STOP, - STATE_CLEANING, - STATE_DOCKED, - STATE_PAUSED, - STATE_IDLE, - STATE_RETURNING, - STATE_ERROR, StateVacuumDevice, ) from homeassistant.const import ( @@ -33,19 +45,6 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.components.mqtt import ( - CONF_UNIQUE_ID, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, - CONF_COMMAND_TOPIC, - CONF_RETAIN, - CONF_STATE_TOPIC, - CONF_QOS, -) - from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 85b3d665e170c0..62eac6e39f5a03 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -12,21 +12,20 @@ SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_IDLE, STATE_ON, STATE_PAUSED, - STATE_IDLE, ) -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + make_entity_service_schema, ) +from homeassistant.helpers.entity import Entity, ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity, Entity from homeassistant.helpers.icon import icon_for_battery_level - +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py index e5f8c162fbdae4..ed25289da10ede 100644 --- a/homeassistant/components/vacuum/device_action.py +++ b/homeassistant/components/vacuum/device_action.py @@ -1,18 +1,20 @@ """Provides device automations for Vacuum.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv -from . import DOMAIN, SERVICE_START, SERVICE_RETURN_TO_BASE + +from . import DOMAIN, SERVICE_RETURN_TO_BASE, SERVICE_START ACTION_TYPES = {"clean", "dock"} diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index 6a41fe0490e13e..5a2eefd94f25ab 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -1,20 +1,22 @@ """Provide the device automations for Vacuum.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA -from . import DOMAIN, STATE_DOCKED, STATE_CLEANING, STATE_RETURNING +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from . import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_RETURNING CONDITION_TYPES = {"is_cleaning", "is_docked"} diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 328db54b1b9d66..ee225ab3caa74f 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -1,19 +1,21 @@ """Provides device automations for Vacuum.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, ) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType -from homeassistant.components.automation import state, AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATES TRIGGER_TYPES = {"cleaning", "docked"} diff --git a/tests/components/vacuum/common.py b/tests/components/vacuum/common.py index 59f600590b1946..6cecbda9968156 100644 --- a/tests/components/vacuum/common.py +++ b/tests/components/vacuum/common.py @@ -10,20 +10,20 @@ SERVICE_CLEAN_SPOT, SERVICE_LOCATE, SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, SERVICE_SEND_COMMAND, SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_START_PAUSE, SERVICE_STOP, - SERVICE_RETURN_TO_BASE, ) from homeassistant.const import ( ATTR_COMMAND, ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py index a7c0859408f350..47ce5423f7da68 100644 --- a/tests/components/vacuum/test_device_action.py +++ b/tests/components/vacuum/test_device_action.py @@ -1,18 +1,18 @@ """The tests for Vacuum device actions.""" import pytest -from homeassistant.components.vacuum import DOMAIN -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.vacuum import DOMAIN from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/vacuum/test_device_condition.py b/tests/components/vacuum/test_device_condition.py index 80e7b72c36fb34..7be944305da8d2 100644 --- a/tests/components/vacuum/test_device_condition.py +++ b/tests/components/vacuum/test_device_condition.py @@ -1,23 +1,23 @@ """The tests for Vacuum device conditions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.vacuum import ( DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_RETURNING, ) -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 680b6482186441..1f9666b17740ed 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -1,18 +1,18 @@ """The tests for Vacuum device triggers.""" import pytest -from homeassistant.components.vacuum import DOMAIN, STATE_DOCKED, STATE_CLEANING -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.vacuum import DOMAIN, STATE_CLEANING, STATE_DOCKED from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) From b832749e3c68a8e731ab0c686a8211d5539fa7d6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 8 Dec 2019 23:11:48 +0530 Subject: [PATCH 2220/3953] Fix file permission (#29660) --- homeassistant/components/intesishome/__init__.py | 0 homeassistant/components/intesishome/climate.py | 0 homeassistant/components/intesishome/manifest.json | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 homeassistant/components/intesishome/__init__.py mode change 100755 => 100644 homeassistant/components/intesishome/climate.py mode change 100755 => 100644 homeassistant/components/intesishome/manifest.json diff --git a/homeassistant/components/intesishome/__init__.py b/homeassistant/components/intesishome/__init__.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json old mode 100755 new mode 100644 From 3d6440589629c5f6bd2c025fc16c127ee20ac1ce Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 18:48:18 +0100 Subject: [PATCH 2221/3953] Sort imports according to PEP8 for recorder (#29652) --- homeassistant/components/recorder/__init__.py | 8 ++++---- homeassistant/components/recorder/migration.py | 2 +- homeassistant/components/recorder/models.py | 4 ++-- homeassistant/components/recorder/purge.py | 2 +- tests/components/recorder/models_original.py | 4 ++-- tests/components/recorder/test_init.py | 8 ++++---- tests/components/recorder/test_migrate.py | 5 +++-- tests/components/recorder/test_models.py | 6 +++--- tests/components/recorder/test_purge.py | 5 +++-- tests/components/recorder/test_util.py | 3 ++- 10 files changed, 25 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 10b1d04304fd63..7ae1cb1a2204a8 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -5,18 +5,19 @@ from datetime import datetime, timedelta import logging import queue +from sqlite3 import Connection import threading import time from typing import Any, Dict, Optional -from sqlite3 import Connection -import voluptuous as vol -from sqlalchemy import exc, create_engine +from sqlalchemy import create_engine, exc from sqlalchemy.engine import Engine from sqlalchemy.event import listens_for from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.pool import StaticPool +import voluptuous as vol +from homeassistant.components import persistent_notification from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DOMAINS, @@ -29,7 +30,6 @@ EVENT_TIME_CHANGED, MATCH_ALL, ) -from homeassistant.components import persistent_notification from homeassistant.core import CoreState, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 33a01ea1ac07f9..3a5ef2729bedad 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -6,7 +6,7 @@ from sqlalchemy.engine import reflection from sqlalchemy.exc import OperationalError, SQLAlchemyError -from .models import SchemaChanges, SCHEMA_VERSION, Base +from .models import SCHEMA_VERSION, Base, SchemaChanges from .util import session_scope _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index b512bfc8204ec1..f3e80a9a7394b9 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,6 +1,6 @@ """Models for SQLAlchemy.""" -import json from datetime import datetime +import json import logging from sqlalchemy import ( @@ -17,9 +17,9 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.session import Session -import homeassistant.util.dt as dt_util from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util # SQLAlchemy Schema # pylint: disable=invalid-name diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 089476245fe672..b4b1f612fac368 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -5,8 +5,8 @@ from sqlalchemy.exc import SQLAlchemyError import homeassistant.util.dt as dt_util -from .models import Events, States +from .models import Events, States from .util import session_scope _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/recorder/models_original.py b/tests/components/recorder/models_original.py index 526116aae7bcc1..25978ef6d5567b 100644 --- a/tests/components/recorder/models_original.py +++ b/tests/components/recorder/models_original.py @@ -4,8 +4,8 @@ implemented. It is used to test the schema migration logic. """ -import json from datetime import datetime +import json import logging from sqlalchemy import ( @@ -21,9 +21,9 @@ ) from sqlalchemy.ext.declarative import declarative_base -import homeassistant.util.dt as dt_util from homeassistant.core import Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util # SQLAlchemy Schema # pylint: disable=invalid-name diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 0f5ee24e6e874c..2ee3126b9fad31 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -5,13 +5,13 @@ import pytest -from homeassistant.core import callback -from homeassistant.const import MATCH_ALL -from homeassistant.setup import async_setup_component from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.models import Events, States from homeassistant.components.recorder.util import session_scope -from homeassistant.components.recorder.models import States, Events +from homeassistant.const import MATCH_ALL +from homeassistant.core import callback +from homeassistant.setup import async_setup_component from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 81e0423a72392b..7947ba5ccef074 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -1,13 +1,14 @@ """The tests for the Recorder component.""" # pylint: disable=protected-access -from unittest.mock import patch, call +from unittest.mock import call, patch import pytest from sqlalchemy import create_engine from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component -from homeassistant.components.recorder import migration, const, models +from homeassistant.components.recorder import const, migration, models + from tests.components.recorder import models_original diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 5d4ac46102ebc0..276194b5d6c999 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -1,14 +1,14 @@ """The tests for the Recorder component.""" -import unittest from datetime import datetime +import unittest from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker -import homeassistant.core as ha +from homeassistant.components.recorder.models import Base, Events, RecorderRuns, States from homeassistant.const import EVENT_STATE_CHANGED +import homeassistant.core as ha from homeassistant.util import dt -from homeassistant.components.recorder.models import Base, Events, States, RecorderRuns ENGINE = None SESSION = None diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 7e06dcd1e5e56e..e0993b8cffc3d2 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -1,14 +1,15 @@ """Test data purging.""" -import json from datetime import datetime, timedelta +import json import unittest from unittest.mock import patch from homeassistant.components import recorder from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.models import Events, States from homeassistant.components.recorder.purge import purge_old_data -from homeassistant.components.recorder.models import States, Events from homeassistant.components.recorder.util import session_scope + from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 8dcfac3c00113a..47f2bf4becad50 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -1,10 +1,11 @@ """Test util methods.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest from homeassistant.components.recorder import util from homeassistant.components.recorder.const import DATA_INSTANCE + from tests.common import get_test_home_assistant, init_recorder_component From e0b82440ad9f64919f3bddb2c2c487666fda12a1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 18:50:17 +0100 Subject: [PATCH 2222/3953] Sort imports according to PEP8 for switch (#29654) --- homeassistant/components/switch/__init__.py | 21 +++++++++---------- .../components/switch/device_action.py | 7 ++++--- .../components/switch/device_condition.py | 7 ++++--- .../components/switch/device_trigger.py | 5 +++-- homeassistant/components/switch/light.py | 6 ++---- .../components/switch/reproduce_state.py | 4 ++-- tests/components/switch/common.py | 2 +- tests/components/switch/test_device_action.py | 6 +++--- .../switch/test_device_condition.py | 13 ++++++------ .../components/switch/test_device_trigger.py | 11 +++++----- tests/components/switch/test_init.py | 4 ++-- tests/components/switch/test_light.py | 1 + 12 files changed, 45 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 26d5658d66864b..78c57e001aa7f3 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -4,21 +4,20 @@ import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components import group +from homeassistant.const import ( + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.const import ( - STATE_ON, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - SERVICE_TOGGLE, -) -from homeassistant.components import group - +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py index a65c1acc5124b8..a50131f094c281 100644 --- a/homeassistant/components/switch/device_action.py +++ b/homeassistant/components/switch/device_action.py @@ -1,13 +1,14 @@ """Provides device actions for switches.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, Context from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import TemplateVarsType, ConfigType -from . import DOMAIN +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from . import DOMAIN ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 56f8f6c196ea4c..87aefdb616d5a0 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -1,14 +1,15 @@ """Provides device conditions for switches.""" from typing import Dict, List + import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import ConfigType +from homeassistant.core import HomeAssistant from homeassistant.helpers.condition import ConditionCheckerType -from . import DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 7f0458b3e9f28d..cb5d5f7aa0e3c4 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -1,14 +1,15 @@ """Provides device triggers for switches.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, CALLBACK_TYPE from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import DOMAIN +from . import DOMAIN TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 1bdc1d39083ebb..5486b8d880c88a 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,10 +1,11 @@ """Light support for switch entities.""" import logging -from typing import cast, Callable, Dict, Optional, Sequence +from typing import Callable, Dict, Optional, Sequence, cast import voluptuous as vol from homeassistant.components import switch +from homeassistant.components.light import PLATFORM_SCHEMA, Light from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, @@ -18,9 +19,6 @@ from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.components.light import PLATFORM_SCHEMA, Light - - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/reproduce_state.py b/homeassistant/components/switch/reproduce_state.py index 7ed1f70cb97804..d2bfc56995627e 100644 --- a/homeassistant/components/switch/reproduce_state.py +++ b/homeassistant/components/switch/reproduce_state.py @@ -5,10 +5,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/tests/components/switch/common.py b/tests/components/switch/common.py index 2c6b1ccd0d2b41..1123b1de6c112f 100644 --- a/tests/components/switch/common.py +++ b/tests/components/switch/common.py @@ -6,9 +6,9 @@ from homeassistant.components.switch import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, SERVICE_TURN_OFF, SERVICE_TURN_ON, - ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index 888e06e0214db8..06ad7323eadeab 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -1,14 +1,14 @@ """The test for switch device automation.""" import pytest -from homeassistant.components.switch import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.components.device_automation import ( _async_get_device_automations as async_get_device_automations, ) +from homeassistant.components.switch import DOMAIN +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index e673527fadaedc..d51a00ddf79870 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -1,22 +1,23 @@ """The test for switch device automation.""" from datetime import timedelta -import pytest from unittest.mock import patch -from homeassistant.components.switch import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component +import pytest + import homeassistant.components.automation as automation +from homeassistant.components.switch import DOMAIN +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 31fb6d30f60994..19588ebfba0722 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -1,22 +1,23 @@ """The test for switch device automation.""" from datetime import timedelta + import pytest -from homeassistant.components.switch import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.components.switch import DOMAIN +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, async_fire_time_changed, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index a9463cb78f4fea..bebebafc763da0 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -2,10 +2,10 @@ # pylint: disable=protected-access import unittest -from homeassistant.setup import setup_component, async_setup_component from homeassistant import core from homeassistant.components import switch -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant, mock_entity_platform from tests.components.switch import common diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index e5c5e5c0aedb51..3034877c6d684a 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -1,6 +1,7 @@ """The tests for the Light Switch platform.""" from homeassistant.setup import async_setup_component + from tests.components.light import common from tests.components.switch import common as switch_common From 415176e350d64ebe0523bedd5b4101c194a5f7de Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 21:05:08 +0100 Subject: [PATCH 2223/3953] Sort imports according to PEP8 for template (#29655) --- homeassistant/components/template/__init__.py | 3 +- .../components/template/binary_sensor.py | 17 +++++----- homeassistant/components/template/cover.py | 33 ++++++++++--------- homeassistant/components/template/fan.py | 27 +++++++-------- homeassistant/components/template/light.py | 19 ++++++----- homeassistant/components/template/lock.py | 12 +++---- homeassistant/components/template/sensor.py | 16 ++++----- homeassistant/components/template/switch.py | 15 +++++---- homeassistant/components/template/vacuum.py | 23 ++++++------- .../components/template/test_binary_sensor.py | 12 +++---- tests/components/template/test_cover.py | 11 ++++--- tests/components/template/test_fan.py | 16 ++++----- tests/components/template/test_light.py | 6 ++-- tests/components/template/test_lock.py | 7 ++-- tests/components/template/test_sensor.py | 14 +++++--- tests/components/template/test_switch.py | 6 ++-- tests/components/template/test_vacuum.py | 5 +-- 17 files changed, 126 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 80421b8e3f88ae..f100d663d8c50f 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -1,11 +1,10 @@ """The template component.""" +from itertools import chain import logging -from itertools import chain from homeassistant.const import MATCH_ALL - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 116862abc79df2..3ca25a33d6437f 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -3,28 +3,29 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.binary_sensor import ( - BinarySensorDevice, + DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - DEVICE_CLASSES_SCHEMA, + BinarySensorDevice, ) from homeassistant.const import ( - ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, + ATTR_FRIENDLY_NAME, + CONF_DEVICE_CLASS, CONF_ENTITY_PICTURE_TEMPLATE, + CONF_ICON_TEMPLATE, CONF_SENSORS, - CONF_DEVICE_CLASS, + CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_START, MATCH_ALL, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.event import async_track_state_change, async_track_same_state +from homeassistant.helpers.event import async_track_same_state, async_track_state_change + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 22035af24ec4c2..c55c3f97df5150 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -3,40 +3,41 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.cover import ( + ATTR_POSITION, + ATTR_TILT_POSITION, + DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, - CoverDevice, PLATFORM_SCHEMA, - DEVICE_CLASSES_SCHEMA, - SUPPORT_OPEN_TILT, + SUPPORT_CLOSE, SUPPORT_CLOSE_TILT, - SUPPORT_STOP_TILT, - SUPPORT_SET_TILT_POSITION, SUPPORT_OPEN, - SUPPORT_CLOSE, - SUPPORT_STOP, + SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, - ATTR_POSITION, - ATTR_TILT_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, + SUPPORT_STOP_TILT, + CoverDevice, ) from homeassistant.const import ( - CONF_FRIENDLY_NAME, - CONF_ENTITY_ID, - EVENT_HOMEASSISTANT_START, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, CONF_DEVICE_CLASS, + CONF_ENTITY_ID, CONF_ENTITY_PICTURE_TEMPLATE, + CONF_FRIENDLY_NAME, + CONF_ICON_TEMPLATE, CONF_OPTIMISTIC, - STATE_OPEN, + CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, STATE_CLOSED, + STATE_OPEN, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index ebb9bcc8b1427d..89f54444376022 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -3,35 +3,36 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( + ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_SPEED, + DIRECTION_FORWARD, + DIRECTION_REVERSE, + ENTITY_ID_FORMAT, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, - SUPPORT_SET_SPEED, + SUPPORT_DIRECTION, SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, FanEntity, - ATTR_SPEED, - ATTR_OSCILLATING, - ENTITY_ID_FORMAT, - SUPPORT_DIRECTION, - DIRECTION_FORWARD, - DIRECTION_REVERSE, - ATTR_DIRECTION, ) from homeassistant.const import ( + CONF_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, - CONF_ENTITY_ID, - STATE_ON, - STATE_OFF, EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, STATE_UNKNOWN, ) from homeassistant.core import callback from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index b71aadd0155bfa..e18833aae3943e 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -3,30 +3,31 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.light import ( ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, - Light, SUPPORT_BRIGHTNESS, + Light, ) from homeassistant.const import ( - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, - CONF_ENTITY_PICTURE_TEMPLATE, CONF_ENTITY_ID, + CONF_ENTITY_PICTURE_TEMPLATE, CONF_FRIENDLY_NAME, - STATE_ON, - STATE_OFF, - EVENT_HOMEASSISTANT_START, + CONF_ICON_TEMPLATE, CONF_LIGHTS, + CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, ) -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 71e9cc6642d595..f4a6b55dd18f2c 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -3,22 +3,22 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv - -from homeassistant.core import callback -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_START, - STATE_ON, - STATE_LOCKED, MATCH_ALL, + STATE_LOCKED, + STATE_ON, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 4ea7daa54f6bcb..a4e28265e1d92f 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -4,30 +4,30 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.sensor import ( + DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - DEVICE_CLASSES_SCHEMA, ) from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, + CONF_DEVICE_CLASS, CONF_ENTITY_PICTURE_TEMPLATE, - ATTR_ENTITY_ID, + CONF_FRIENDLY_NAME_TEMPLATE, + CONF_ICON_TEMPLATE, CONF_SENSORS, + CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_START, - CONF_FRIENDLY_NAME_TEMPLATE, MATCH_ALL, - CONF_DEVICE_CLASS, ) - +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.event import async_track_state_change + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index e06ca0c8d54629..f44f7256e199af 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -3,28 +3,29 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.switch import ( ENTITY_ID_FORMAT, - SwitchDevice, PLATFORM_SCHEMA, + SwitchDevice, ) from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, - STATE_OFF, - STATE_ON, - ATTR_ENTITY_ID, + CONF_ICON_TEMPLATE, CONF_SWITCHES, + CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 8201842e1312eb..4946d54edc37de 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -3,7 +3,6 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.vacuum import ( ATTR_FAN_SPEED, DOMAIN, @@ -14,35 +13,37 @@ SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, - SUPPORT_STOP, - SUPPORT_STATE, SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STOP, StateVacuumDevice, - STATE_CLEANING, - STATE_DOCKED, - STATE_PAUSED, - STATE_IDLE, - STATE_RETURNING, - STATE_ERROR, ) from homeassistant.const import ( + CONF_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, - CONF_ENTITY_ID, - MATCH_ALL, EVENT_HOMEASSISTANT_START, + MATCH_ALL, STATE_UNKNOWN, ) from homeassistant.core import callback from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index b17b98f2f1093f..b024bbc311f88f 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -3,24 +3,24 @@ import unittest from unittest import mock +from homeassistant import setup +from homeassistant.components.template import binary_sensor as template from homeassistant.const import ( - MATCH_ALL, EVENT_HOMEASSISTANT_START, - STATE_UNAVAILABLE, - STATE_ON, + MATCH_ALL, STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, ) -from homeassistant import setup -from homeassistant.components.template import binary_sensor as template from homeassistant.exceptions import TemplateError from homeassistant.helpers import template as template_hlpr from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as dt_util from tests.common import ( - get_test_home_assistant, assert_setup_component, async_fire_time_changed, + get_test_home_assistant, ) diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index d3be01cbdc3ae8..c3e1f2843fd8ea 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -1,5 +1,6 @@ """The tests the cover command line platform.""" import logging + import pytest from homeassistant import setup @@ -8,18 +9,18 @@ ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, - SERVICE_TOGGLE, - SERVICE_TOGGLE_COVER_TILT, SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + SERVICE_TOGGLE, + SERVICE_TOGGLE_COVER_TILT, STATE_CLOSED, - STATE_UNAVAILABLE, - STATE_OPEN, - STATE_ON, STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_UNAVAILABLE, ) from tests.common import assert_setup_component, async_mock_service diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 5753684795b127..981b87ff43e685 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -1,23 +1,23 @@ """The tests for the Template fan platform.""" import logging -import pytest +import pytest import voluptuous as vol from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.fan import ( - ATTR_SPEED, - ATTR_OSCILLATING, - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_SPEED, DIRECTION_FORWARD, DIRECTION_REVERSE, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, ) +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE -from tests.common import async_mock_service, assert_setup_component +from tests.common import assert_setup_component, async_mock_service from tests.components.fan import common _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index c2dd49a76fbd8a..62d377a93370a7 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -1,12 +1,12 @@ """The tests for the Template light platform.""" import logging -from homeassistant.core import callback from homeassistant import setup from homeassistant.components.light import ATTR_BRIGHTNESS -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import callback -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant from tests.components.light import common _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 32a34411b33668..10f959efed8959 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -1,13 +1,12 @@ """The tests for the Template lock platform.""" import logging -from homeassistant.core import callback from homeassistant import setup from homeassistant.components import lock -from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import callback -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 7ae34b04c00016..e9033b3dfe3bf3 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1,9 +1,13 @@ """The test for the Template sensor platform.""" -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.setup import setup_component, async_setup_component - -from tests.common import get_test_home_assistant, assert_setup_component -from homeassistant.const import STATE_UNAVAILABLE, STATE_ON, STATE_OFF +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.setup import async_setup_component, setup_component + +from tests.common import assert_setup_component, get_test_home_assistant class TestTemplateSensor: diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index 3adc5dcad46db9..a66028a318f723 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -1,9 +1,9 @@ """The tests for the Template switch platform.""" -from homeassistant.core import callback from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import callback -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant from tests.components.switch import common diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index da0e8e59ededb8..4080b75f46af08 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -1,9 +1,9 @@ """The tests for the Template vacuum platform.""" import logging + import pytest from homeassistant import setup -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNKNOWN, STATE_UNAVAILABLE from homeassistant.components.vacuum import ( ATTR_BATTERY_LEVEL, STATE_CLEANING, @@ -12,8 +12,9 @@ STATE_PAUSED, STATE_RETURNING, ) +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN -from tests.common import async_mock_service, assert_setup_component +from tests.common import assert_setup_component, async_mock_service from tests.components.vacuum import common _LOGGER = logging.getLogger(__name__) From 51ece97e0d2c60c94ba63b25e9207e3f2c2daca2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 21:06:05 +0100 Subject: [PATCH 2224/3953] Sort imports according to PEP8 for hive (#29669) --- homeassistant/components/hive/binary_sensor.py | 2 +- homeassistant/components/hive/climate.py | 9 ++++----- homeassistant/components/hive/light.py | 2 +- homeassistant/components/hive/sensor.py | 2 +- homeassistant/components/hive/switch.py | 2 +- homeassistant/components/hive/water_heater.py | 3 ++- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index ce7e53b77a5263..fa91d6862a21a0 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,7 +1,7 @@ """Support for the Hive binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice -from . import DOMAIN, DATA_HIVE, HiveEntity +from . import DATA_HIVE, DOMAIN, HiveEntity DEVICETYPE_DEVICE_CLASS = {"motionsensor": "motion", "contactsensor": "opening"} diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index ed13e3019ce13f..202cea7bf8e477 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,6 +1,9 @@ """Support for the Hive climate devices.""" from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, @@ -8,14 +11,10 @@ PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - CURRENT_HVAC_HEAT, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS - -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system HIVE_TO_HASS_STATE = { "SCHEDULE": HVAC_MODE_AUTO, diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 41fc286d13b0e7..33175de543da16 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -10,7 +10,7 @@ ) import homeassistant.util.color as color_util -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index ccd635015de320..360fb61bfbee82 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -2,7 +2,7 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from . import DOMAIN, DATA_HIVE, HiveEntity +from . import DATA_HIVE, DOMAIN, HiveEntity FRIENDLY_NAMES = { "Hub_OnlineStatus": "Hive Hub Status", diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 1447f5483a4a0e..53e1ec6a069b3a 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,7 +1,7 @@ """Support for the Hive switches.""" from homeassistant.components.switch import SwitchDevice -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index c60a9ec01d1fcf..d7d98426df5eae 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -7,7 +7,8 @@ WaterHeaterDevice, ) from homeassistant.const import TEMP_CELSIUS -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system + +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE From 55559f3e306d9b50e5a02b9cbbd7f61ab07b4e58 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 21:09:48 +0100 Subject: [PATCH 2225/3953] Sort imports according to PEP8 for starline (#29653) --- homeassistant/components/starline/__init__.py | 7 ++++--- homeassistant/components/starline/account.py | 15 ++++++++------- .../components/starline/binary_sensor.py | 5 +++-- .../components/starline/config_flow.py | 19 ++++++++++--------- .../components/starline/device_tracker.py | 1 + homeassistant/components/starline/entity.py | 2 ++ homeassistant/components/starline/lock.py | 1 + homeassistant/components/starline/sensor.py | 1 + homeassistant/components/starline/switch.py | 1 + tests/components/starline/test_config_flow.py | 1 + 10 files changed, 32 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 22772282a7c00b..303507b1491468 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -1,17 +1,18 @@ """The StarLine component.""" import voluptuous as vol + from homeassistant.config_entries import ConfigEntry from homeassistant.core import Config, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .account import StarlineAccount from .const import ( + CONF_SCAN_INTERVAL, + DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS, - SERVICE_UPDATE_STATE, SERVICE_SET_SCAN_INTERVAL, - CONF_SCAN_INTERVAL, - DEFAULT_SCAN_INTERVAL, + SERVICE_UPDATE_STATE, ) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index 2e7653eb380bb9..aee88c0bd3f54f 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -1,6 +1,7 @@ """StarLine Account.""" -from datetime import timedelta, datetime -from typing import Callable, Optional, Dict, Any +from datetime import datetime, timedelta +from typing import Any, Callable, Dict, Optional + from starline import StarlineApi, StarlineDevice from homeassistant.config_entries import ConfigEntry @@ -8,13 +9,13 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( + DATA_EXPIRES, + DATA_SLID_TOKEN, + DATA_SLNET_TOKEN, + DATA_USER_ID, + DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, - DEFAULT_SCAN_INTERVAL, - DATA_USER_ID, - DATA_SLNET_TOKEN, - DATA_SLID_TOKEN, - DATA_EXPIRES, ) diff --git a/homeassistant/components/starline/binary_sensor.py b/homeassistant/components/starline/binary_sensor.py index fd28ff74cf4c53..21074069135249 100644 --- a/homeassistant/components/starline/binary_sensor.py +++ b/homeassistant/components/starline/binary_sensor.py @@ -1,11 +1,12 @@ """Reads vehicle status from StarLine API.""" from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_DOOR, DEVICE_CLASS_LOCK, - DEVICE_CLASS_PROBLEM, DEVICE_CLASS_POWER, + DEVICE_CLASS_PROBLEM, + BinarySensorDevice, ) + from .account import StarlineAccount, StarlineDevice from .const import DOMAIN from .entity import StarlineEntity diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index 2253cc3cd22ea3..fa559f6291350d 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -1,25 +1,26 @@ """Config flow to configure StarLine component.""" from typing import Optional + from starline import StarlineAuth import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import ( # pylint: disable=unused-import - DOMAIN, CONF_APP_ID, CONF_APP_SECRET, - CONF_MFA_CODE, CONF_CAPTCHA_CODE, - LOGGER, + CONF_MFA_CODE, + DATA_EXPIRES, + DATA_SLID_TOKEN, + DATA_SLNET_TOKEN, + DATA_USER_ID, + DOMAIN, ERROR_AUTH_APP, - ERROR_AUTH_USER, ERROR_AUTH_MFA, - DATA_USER_ID, - DATA_SLNET_TOKEN, - DATA_SLID_TOKEN, - DATA_EXPIRES, + ERROR_AUTH_USER, + LOGGER, ) diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index b5254c761d8015..6f202bbae52c47 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -2,6 +2,7 @@ from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.helpers.restore_state import RestoreEntity + from .account import StarlineAccount, StarlineDevice from .const import DOMAIN from .entity import StarlineEntity diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py index b0d948ae2c80c7..31d1c79b9f0664 100644 --- a/homeassistant/components/starline/entity.py +++ b/homeassistant/components/starline/entity.py @@ -1,6 +1,8 @@ """StarLine base entity.""" from typing import Callable, Optional + from homeassistant.helpers.entity import Entity + from .account import StarlineAccount, StarlineDevice diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py index 0a20a36ae8bd2b..804e8c8df2d705 100644 --- a/homeassistant/components/starline/lock.py +++ b/homeassistant/components/starline/lock.py @@ -1,5 +1,6 @@ """Support for StarLine lock.""" from homeassistant.components.lock import LockDevice + from .account import StarlineAccount, StarlineDevice from .const import DOMAIN from .entity import StarlineEntity diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 2507aba4955d12..0629a03e148c0d 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -2,6 +2,7 @@ from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level + from .account import StarlineAccount, StarlineDevice from .const import DOMAIN from .entity import StarlineEntity diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py index 92dec10b9d30e2..920fe686d9afed 100644 --- a/homeassistant/components/starline/switch.py +++ b/homeassistant/components/starline/switch.py @@ -1,5 +1,6 @@ """Support for StarLine switch.""" from homeassistant.components.switch import SwitchDevice + from .account import StarlineAccount, StarlineDevice from .const import DOMAIN from .entity import StarlineEntity diff --git a/tests/components/starline/test_config_flow.py b/tests/components/starline/test_config_flow.py index 31bdf98b404330..3ca52f849bc42c 100644 --- a/tests/components/starline/test_config_flow.py +++ b/tests/components/starline/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for StarLine config flow.""" import requests_mock + from homeassistant.components.starline import config_flow TEST_APP_ID = "666" From 3d25ed7994ec604e03acd21a6ac73ba39adfd864 Mon Sep 17 00:00:00 2001 From: Gerald Hansen Date: Sun, 8 Dec 2019 22:27:18 +0100 Subject: [PATCH 2226/3953] Change state values for Worx Landroid sensor (#27453) The obj["state"] contains already several named return states as following: "grass cutting", "trapped recovery", "searching wire", "following wire", "searching home", "home", "idle" And with the batteryChargerState also the "charging" Fixes #455 --- homeassistant/components/worxlandroid/sensor.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index ad583d6d943dbc..dd74c5b7d17220 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -141,16 +141,9 @@ def get_state(self, obj): state = self.get_error(obj) if state is None: - state_obj = obj["settaggi"] - - if state_obj[14] == 1: - return "manual-stop" - if state_obj[5] == 1 and state_obj[13] == 0: - return "charging" - if state_obj[5] == 1 and state_obj[13] == 1: - return "charging-complete" - if state_obj[15] == 1: - return "going-home" - return "mowing" + if obj["batteryChargerState"] == "charging": + return obj["batteryChargerState"] + + return obj["state"] return state From e6eed4755f6851d7b44c6ef8989a9ab319cfc0a7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 23:31:56 +0100 Subject: [PATCH 2227/3953] Sort imports according to PEP8 for plex (#29708) --- homeassistant/components/plex/__init__.py | 4 ++-- homeassistant/components/plex/config_flow.py | 12 ++++++------ tests/components/plex/mock_classes.py | 2 +- tests/components/plex/test_config_flow.py | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 971032302888ed..89659769192599 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -27,10 +27,10 @@ ) from .const import ( - CONF_USE_EPISODE_ART, - CONF_SHOW_ALL_CONTROLS, CONF_SERVER, CONF_SERVER_IDENTIFIER, + CONF_SHOW_ALL_CONTROLS, + CONF_USE_EPISODE_ART, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 350f1b3d5773ac..d38d13c847e6be 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -8,12 +8,12 @@ import requests.exceptions import voluptuous as vol -from homeassistant.components.http.view import HomeAssistantView -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant import config_entries +from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.const import CONF_SSL, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json from .const import ( # pylint: disable=unused-import @@ -22,16 +22,16 @@ CONF_CLIENT_IDENTIFIER, CONF_SERVER, CONF_SERVER_IDENTIFIER, - CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, + CONF_USE_EPISODE_ART, DEFAULT_VERIFY_SSL, DOMAIN, PLEX_CONFIG_FILE, PLEX_SERVER_CONFIG, X_PLEX_DEVICE_NAME, - X_PLEX_VERSION, - X_PLEX_PRODUCT, X_PLEX_PLATFORM, + X_PLEX_PRODUCT, + X_PLEX_VERSION, ) from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 1a680e6af0f029..de6ffa51170589 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -1,6 +1,6 @@ """Mock classes used in tests.""" -from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.components.plex.const import CONF_SERVER, CONF_SERVER_IDENTIFIER +from homeassistant.const import CONF_HOST, CONF_PORT MOCK_SERVERS = [ { diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 668ac3b2a17275..0fb1f850809600 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -9,10 +9,10 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_TOKEN, CONF_URL from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry - from .mock_classes import MOCK_SERVERS, MockPlexAccount, MockPlexServer +from tests.common import MockConfigEntry + MOCK_TOKEN = "secret_token" MOCK_FILE_CONTENTS = { f"{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}": { From cce3077df369aa28ad41ec45e3980dca784157b2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 23:32:50 +0100 Subject: [PATCH 2228/3953] Sort imports according to PEP8 for cert_expiry (#29705) --- homeassistant/components/cert_expiry/config_flow.py | 5 +++-- homeassistant/components/cert_expiry/sensor.py | 10 +++++----- tests/components/cert_expiry/test_config_flow.py | 7 ++++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 78450d247b9886..14532aea65f9ba 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -2,13 +2,14 @@ import logging import socket import ssl + import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN, DEFAULT_PORT, DEFAULT_NAME +from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN from .helper import get_cert _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 3022c7bd42ba1e..3a76575dfddd35 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -1,24 +1,24 @@ """Counter for the days until an HTTPS (TLS) certificate will expire.""" +from datetime import datetime, timedelta import logging import socket import ssl -from datetime import datetime, timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( - CONF_NAME, CONF_HOST, + CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_START, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT +from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN from .helper import get_cert _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index 3754551c230123..bcd1482195d907 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -1,13 +1,14 @@ """Tests for the Cert Expiry config flow.""" -import pytest -import ssl import socket +import ssl from unittest.mock import patch +import pytest + from homeassistant import data_entry_flow from homeassistant.components.cert_expiry import config_flow from homeassistant.components.cert_expiry.const import DEFAULT_NAME, DEFAULT_PORT -from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from tests.common import MockConfigEntry, mock_coro From 6a1753d6db8dbef2c76c9a706ead70034ac10093 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 23:33:42 +0100 Subject: [PATCH 2229/3953] Sort imports according to PEP8 for geonetnz_volcano (#29716) --- .../components/geonetnz_volcano/__init__.py | 12 ++++++------ homeassistant/components/geonetnz_volcano/sensor.py | 6 +++--- tests/components/geonetnz_volcano/conftest.py | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py index f0887da9c06fb3..e24de7fdc5da9e 100644 --- a/homeassistant/components/geonetnz_volcano/__init__.py +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -1,27 +1,27 @@ """The GeoNet NZ Volcano integration.""" import asyncio +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime from typing import Optional -import voluptuous as vol from aio_geojson_geonetnz_volcano import GeonetnzVolcanoFeedManager +import voluptuous as vol -from homeassistant.core import callback -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_SCAN_INTERVAL, - CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_MILES, ) -from homeassistant.helpers import config_validation as cv, aiohttp_client +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.unit_system import METRIC_SYSTEM from .config_flow import configured_instances from .const import ( diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index 364ee416be4146..f87ea88fc1cba8 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -3,11 +3,11 @@ from typing import Optional from homeassistant.const import ( - CONF_UNIT_SYSTEM_IMPERIAL, - LENGTH_KILOMETERS, ATTR_ATTRIBUTION, - ATTR_LONGITUDE, ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/tests/components/geonetnz_volcano/conftest.py b/tests/components/geonetnz_volcano/conftest.py index 55231cd31207ea..33a299eeb79605 100644 --- a/tests/components/geonetnz_volcano/conftest.py +++ b/tests/components/geonetnz_volcano/conftest.py @@ -6,9 +6,10 @@ CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, - CONF_UNIT_SYSTEM, CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, ) + from tests.common import MockConfigEntry From 81a482332d6bd90f0cef193ebb6b8a79a6d09ca3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 23:34:18 +0100 Subject: [PATCH 2230/3953] Sort imports according to PEP8 for eufy (#29715) --- homeassistant/components/eufy/__init__.py | 2 +- homeassistant/components/eufy/light.py | 7 +++---- homeassistant/components/eufy/switch.py | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index 191d6ab5315c8e..eca637ec371838 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -1,7 +1,7 @@ """Support for Eufy devices.""" import logging -import lakeside +import lakeside import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index 21c26606bdd7ae..570f690307f228 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -1,5 +1,6 @@ """Support for Eufy lights.""" import logging + import lakeside from homeassistant.components.light import ( @@ -7,16 +8,14 @@ ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, Light, ) - import homeassistant.util.color as color_util - from homeassistant.util.color import ( - color_temperature_mired_to_kelvin as mired_to_kelvin, color_temperature_kelvin_to_mired as kelvin_to_mired, + color_temperature_mired_to_kelvin as mired_to_kelvin, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index 2e13886dd2a5a7..cbc09f4101c9c9 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -1,5 +1,6 @@ """Support for Eufy switches.""" import logging + import lakeside from homeassistant.components.switch import SwitchDevice From 39887c46c09c45f3db99944797e838a190dba8fe Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 23:40:06 +0100 Subject: [PATCH 2231/3953] Sort imports according to PEP8 for dialogflow (#29714) --- homeassistant/components/dialogflow/__init__.py | 5 ++--- homeassistant/components/dialogflow/config_flow.py | 2 +- tests/components/dialogflow/test_init.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index 45fee0f867e120..ae3c0288aed58c 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -1,16 +1,15 @@ """Support for Dialogflow webhook.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import intent, template, config_entry_flow +from homeassistant.helpers import config_entry_flow, intent, template from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) SOURCE = "Home Assistant Dialogflow" diff --git a/homeassistant/components/dialogflow/config_flow.py b/homeassistant/components/dialogflow/config_flow.py index 4f785392ffcf17..fee99898ccc179 100644 --- a/homeassistant/components/dialogflow/config_flow.py +++ b/homeassistant/components/dialogflow/config_flow.py @@ -1,7 +1,7 @@ """Config flow for DialogFlow.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 18a03ff2603a54..aaec1ee67cffad 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -1,6 +1,6 @@ """The tests for the Dialogflow component.""" -import json import copy +import json from unittest.mock import Mock import pytest From 076e0273a2cda5a9b46ceb7abbfa4f0e86c161e0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 8 Dec 2019 23:42:04 +0100 Subject: [PATCH 2232/3953] Sort imports according to PEP8 for kodi (#29721) --- homeassistant/components/kodi/__init__.py | 6 +++--- homeassistant/components/kodi/media_player.py | 10 ++++------ homeassistant/components/kodi/notify.py | 16 +++++++--------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index 5bbffc5df1de5e..1f2d3cb5cd0956 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -1,13 +1,13 @@ """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 - +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.helpers import config_validation as cv SERVICE_ADD_MEDIA = "add_to_playlist" SERVICE_CALL_METHOD = "call_method" diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 9b2ba01e90ada2..9721ea2d31f557 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -7,15 +7,14 @@ import urllib import aiohttp -import jsonrpc_base import jsonrpc_async +import jsonrpc_base import jsonrpc_websocket - 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 import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -52,12 +51,11 @@ STATE_PLAYING, ) from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import script +from homeassistant.helpers import config_validation as cv, script from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.template import Template -from homeassistant.util.yaml import dump import homeassistant.util.dt as dt_util +from homeassistant.util.yaml import dump _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index 1072cf1b7329b9..6f370ffad98241 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -3,9 +3,15 @@ import aiohttp import jsonrpc_async - import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( ATTR_ICON, CONF_HOST, @@ -17,14 +23,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 8080 From e577f047f70c074149febc0bc36467348e5db4e8 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Sun, 8 Dec 2019 15:19:38 -0800 Subject: [PATCH 2233/3953] Add tests for vera component (#28340) * Adding tests for vera component. Fixing update bug in the vera climate platform. * Updating requrements file. * Moving vera stop to a job. Sorting imports. * Addressing simple PR feedback. * Splitting tests into platforms. * Mocking controller instead of using requests_mock. * Updating pyvera to use version that stops threads quickly. * Updating requirements files. * Mocking the pyvera module, not the API. * Addressing PR feedback. Handling start/stop of patch in fixture. Removing unecessary code. * Using generator --- .coveragerc | 1 - homeassistant/components/vera/climate.py | 6 + homeassistant/components/vera/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/vera/__init__.py | 1 + tests/components/vera/common.py | 61 ++++++ tests/components/vera/conftest.py | 13 ++ tests/components/vera/test_binary_sensor.py | 38 ++++ tests/components/vera/test_climate.py | 155 +++++++++++++++ tests/components/vera/test_cover.py | 76 ++++++++ tests/components/vera/test_init.py | 78 ++++++++ tests/components/vera/test_light.py | 76 ++++++++ tests/components/vera/test_lock.py | 49 +++++ tests/components/vera/test_scene.py | 27 +++ tests/components/vera/test_sensor.py | 198 ++++++++++++++++++++ tests/components/vera/test_switch.py | 48 +++++ 17 files changed, 831 insertions(+), 3 deletions(-) create mode 100644 tests/components/vera/__init__.py create mode 100644 tests/components/vera/common.py create mode 100644 tests/components/vera/conftest.py create mode 100644 tests/components/vera/test_binary_sensor.py create mode 100644 tests/components/vera/test_climate.py create mode 100644 tests/components/vera/test_cover.py create mode 100644 tests/components/vera/test_init.py create mode 100644 tests/components/vera/test_light.py create mode 100644 tests/components/vera/test_lock.py create mode 100644 tests/components/vera/test_scene.py create mode 100644 tests/components/vera/test_sensor.py create mode 100644 tests/components/vera/test_switch.py diff --git a/.coveragerc b/.coveragerc index 778af8db89ad82..e85c97ef80c411 100644 --- a/.coveragerc +++ b/.coveragerc @@ -750,7 +750,6 @@ omit = homeassistant/components/velbus/switch.py homeassistant/components/velux/* homeassistant/components/venstar/climate.py - homeassistant/components/vera/* homeassistant/components/verisure/* homeassistant/components/versasense/* homeassistant/components/vesync/__init__.py diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 50c897d0fc19fb..60e73d48978cdb 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -92,6 +92,8 @@ def set_fan_mode(self, fan_mode): else: self.vera_device.fan_auto() + self.schedule_update_ha_state() + @property def current_power_w(self): """Return the current power usage in W.""" @@ -129,6 +131,8 @@ def set_temperature(self, **kwargs): if kwargs.get(ATTR_TEMPERATURE) is not None: self.vera_device.set_temperature(kwargs.get(ATTR_TEMPERATURE)) + self.schedule_update_ha_state() + def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_OFF: @@ -139,3 +143,5 @@ def set_hvac_mode(self, hvac_mode): self.vera_device.turn_cool_on() elif hvac_mode == HVAC_MODE_HEAT: self.vera_device.turn_heat_on() + + self.schedule_update_ha_state() diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 120ec241d60f7b..70abc09843182d 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/integrations/vera", "requirements": [ - "pyvera==0.3.6" + "pyvera==0.3.7" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 360898df055640..87ca2a46177f4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1670,7 +1670,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.6 +pyvera==0.3.7 # homeassistant.components.versasense pyversasense==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fff9af5b32df7c..405e632c0ccb02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -536,6 +536,9 @@ pytraccar==0.9.0 # homeassistant.components.tradfri pytradfri[async]==6.4.0 +# homeassistant.components.vera +pyvera==0.3.7 + # homeassistant.components.vesync pyvesync==1.1.0 diff --git a/tests/components/vera/__init__.py b/tests/components/vera/__init__.py new file mode 100644 index 00000000000000..4e0919be042b38 --- /dev/null +++ b/tests/components/vera/__init__.py @@ -0,0 +1 @@ +"""Tests for the Vera component.""" diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py new file mode 100644 index 00000000000000..d78a536e95c645 --- /dev/null +++ b/tests/components/vera/common.py @@ -0,0 +1,61 @@ +"""Common code for tests.""" + +from typing import Callable, NamedTuple, Tuple + +from mock import MagicMock +from pyvera import VeraController, VeraDevice, VeraScene + +from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +ComponentData = NamedTuple("ComponentData", (("controller", VeraController),)) + + +class ComponentFactory: + """Factory class.""" + + def __init__(self, init_controller_mock): + """Constructor.""" + self.init_controller_mock = init_controller_mock + + async def configure_component( + self, + hass: HomeAssistant, + devices: Tuple[VeraDevice] = (), + scenes: Tuple[VeraScene] = (), + setup_callback: Callable[[VeraController], None] = None, + ) -> ComponentData: + """Configure the component with specific mock data.""" + controller_url = "http://127.0.0.1:123" + + hass_config = { + DOMAIN: {CONF_CONTROLLER: controller_url}, + } + + controller = MagicMock(spec=VeraController) # type: VeraController + controller.base_url = controller_url + controller.register = MagicMock() + controller.get_devices = MagicMock(return_value=devices or ()) + controller.get_scenes = MagicMock(return_value=scenes or ()) + + for vera_obj in controller.get_devices() + controller.get_scenes(): + vera_obj.vera_controller = controller + + controller.get_devices.reset_mock() + controller.get_scenes.reset_mock() + + if setup_callback: + setup_callback(controller, hass_config) + + def init_controller(base_url: str) -> list: + nonlocal controller + return [controller, True] + + self.init_controller_mock.side_effect = init_controller + + # Setup home assistant. + assert await async_setup_component(hass, DOMAIN, hass_config) + await hass.async_block_till_done() + + return ComponentData(controller=controller) diff --git a/tests/components/vera/conftest.py b/tests/components/vera/conftest.py new file mode 100644 index 00000000000000..b94a40135d8b09 --- /dev/null +++ b/tests/components/vera/conftest.py @@ -0,0 +1,13 @@ +"""Fixtures for tests.""" + +from mock import patch +import pytest + +from .common import ComponentFactory + + +@pytest.fixture() +def vera_component_factory(): + """Return a factory for initializing the vera component.""" + with patch("pyvera.init_controller") as init_controller_mock: + yield ComponentFactory(init_controller_mock) diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py new file mode 100644 index 00000000000000..2c2e2b8638818a --- /dev/null +++ b/tests/components/vera/test_binary_sensor.py @@ -0,0 +1,38 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import VeraBinarySensor + +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_binary_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraBinarySensor) # type: VeraBinarySensor + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.is_tripped = False + entity_id = "binary_sensor.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,) + ) + controller = component_data.controller + + update_callback = controller.register.call_args_list[0][0][1] + + vera_device.is_tripped = False + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "off" + controller.register.reset_mock() + + vera_device.is_tripped = True + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "on" + controller.register.reset_mock() diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py new file mode 100644 index 00000000000000..c27a72865fd995 --- /dev/null +++ b/tests/components/vera/test_climate.py @@ -0,0 +1,155 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import CATEGORY_THERMOSTAT, VeraController, VeraThermostat + +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_ON, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, +) +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_climate( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraThermostat) # type: VeraThermostat + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_THERMOSTAT + vera_device.power = 10 + vera_device.get_current_temperature.return_value = 71 + vera_device.get_hvac_mode.return_value = "Off" + vera_device.get_current_goal_temperature.return_value = 72 + entity_id = "climate.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + assert hass.states.get(entity_id).state == HVAC_MODE_OFF + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_COOL}, + ) + await hass.async_block_till_done() + vera_device.turn_cool_on.assert_called() + vera_device.get_hvac_mode.return_value = "CoolOn" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == HVAC_MODE_COOL + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_HEAT}, + ) + await hass.async_block_till_done() + vera_device.turn_heat_on.assert_called() + vera_device.get_hvac_mode.return_value = "HeatOn" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == HVAC_MODE_HEAT + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_HEAT_COOL}, + ) + await hass.async_block_till_done() + vera_device.turn_auto_on.assert_called() + vera_device.get_hvac_mode.return_value = "AutoChangeOver" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == HVAC_MODE_HEAT_COOL + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_OFF}, + ) + await hass.async_block_till_done() + vera_device.turn_auto_on.assert_called() + vera_device.get_hvac_mode.return_value = "Off" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == HVAC_MODE_OFF + + await hass.services.async_call( + "climate", "set_fan_mode", {"entity_id": entity_id, "fan_mode": "on"}, + ) + await hass.async_block_till_done() + vera_device.turn_auto_on.assert_called() + vera_device.get_fan_mode.return_value = "ContinuousOn" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).attributes["fan_mode"] == FAN_ON + + await hass.services.async_call( + "climate", "set_fan_mode", {"entity_id": entity_id, "fan_mode": "off"}, + ) + await hass.async_block_till_done() + vera_device.turn_auto_on.assert_called() + vera_device.get_fan_mode.return_value = "Auto" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).attributes["fan_mode"] == FAN_AUTO + + await hass.services.async_call( + "climate", "set_temperature", {"entity_id": entity_id, "temperature": 30}, + ) + await hass.async_block_till_done() + vera_device.set_temperature.assert_called_with(30) + vera_device.get_current_goal_temperature.return_value = 30 + vera_device.get_current_temperature.return_value = 25 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).attributes["current_temperature"] == 25 + assert hass.states.get(entity_id).attributes["temperature"] == 30 + + +async def test_climate_f( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraThermostat) # type: VeraThermostat + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_THERMOSTAT + vera_device.power = 10 + vera_device.get_current_temperature.return_value = 71 + vera_device.get_hvac_mode.return_value = "Off" + vera_device.get_current_goal_temperature.return_value = 72 + entity_id = "climate.dev1_1" + + def setup_callback(controller: VeraController, hass_config: dict) -> None: + controller.temperature_units = "F" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), setup_callback=setup_callback + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + await hass.services.async_call( + "climate", "set_temperature", {"entity_id": entity_id, "temperature": 30}, + ) + await hass.async_block_till_done() + vera_device.set_temperature.assert_called_with(86) + vera_device.get_current_goal_temperature.return_value = 30 + vera_device.get_current_temperature.return_value = 25 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).attributes["current_temperature"] == -3.9 + assert hass.states.get(entity_id).attributes["temperature"] == -1.1 diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py new file mode 100644 index 00000000000000..79cb4adedfbc92 --- /dev/null +++ b/tests/components/vera/test_cover.py @@ -0,0 +1,76 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import CATEGORY_CURTAIN, VeraCurtain + +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_cover( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraCurtain) # type: VeraCurtain + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_CURTAIN + vera_device.is_closed = False + vera_device.get_level.return_value = 0 + entity_id = "cover.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + assert hass.states.get(entity_id).state == "closed" + assert hass.states.get(entity_id).attributes["current_position"] == 0 + + await hass.services.async_call( + "cover", "open_cover", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.open.assert_called() + vera_device.is_open.return_value = True + vera_device.get_level.return_value = 100 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "open" + assert hass.states.get(entity_id).attributes["current_position"] == 100 + + await hass.services.async_call( + "cover", "set_cover_position", {"entity_id": entity_id, "position": 50}, + ) + await hass.async_block_till_done() + vera_device.set_level.assert_called_with(50) + vera_device.is_open.return_value = True + vera_device.get_level.return_value = 50 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "open" + assert hass.states.get(entity_id).attributes["current_position"] == 50 + + await hass.services.async_call( + "cover", "stop_cover", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.stop.assert_called() + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "open" + assert hass.states.get(entity_id).attributes["current_position"] == 50 + + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.close.assert_called() + vera_device.is_open.return_value = False + vera_device.get_level.return_value = 00 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "closed" + assert hass.states.get(entity_id).attributes["current_position"] == 00 diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py new file mode 100644 index 00000000000000..9ff6cb4058b3c0 --- /dev/null +++ b/tests/components/vera/test_init.py @@ -0,0 +1,78 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import ( + VeraArmableDevice, + VeraBinarySensor, + VeraController, + VeraCurtain, + VeraDevice, + VeraDimmer, + VeraLock, + VeraScene, + VeraSceneController, + VeraSensor, + VeraSwitch, + VeraThermostat, +) + +from homeassistant.components.vera import ( + CONF_EXCLUDE, + CONF_LIGHTS, + DOMAIN, + VERA_DEVICES, +) +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +def new_vera_device(cls, device_id: int) -> VeraDevice: + """Create new mocked vera device..""" + vera_device = MagicMock(spec=cls) # type: VeraDevice + vera_device.device_id = device_id + vera_device.name = f"dev${device_id}" + return vera_device + + +def assert_hass_vera_devices(hass: HomeAssistant, platform: str, arr_len: int) -> None: + """Assert vera devices are present..""" + assert hass.data[VERA_DEVICES][platform] + assert len(hass.data[VERA_DEVICES][platform]) == arr_len + + +async def test_init( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + + def setup_callback(controller: VeraController, hass_config: dict) -> None: + hass_config[DOMAIN][CONF_EXCLUDE] = [11] + hass_config[DOMAIN][CONF_LIGHTS] = [10] + + await vera_component_factory.configure_component( + hass=hass, + devices=( + new_vera_device(VeraDimmer, 1), + new_vera_device(VeraBinarySensor, 2), + new_vera_device(VeraSensor, 3), + new_vera_device(VeraArmableDevice, 4), + new_vera_device(VeraLock, 5), + new_vera_device(VeraThermostat, 6), + new_vera_device(VeraCurtain, 7), + new_vera_device(VeraSceneController, 8), + new_vera_device(VeraSwitch, 9), + new_vera_device(VeraSwitch, 10), + new_vera_device(VeraSwitch, 11), + ), + scenes=(MagicMock(spec=VeraScene),), + setup_callback=setup_callback, + ) + + assert_hass_vera_devices(hass, "light", 2) + assert_hass_vera_devices(hass, "binary_sensor", 1) + assert_hass_vera_devices(hass, "sensor", 2) + assert_hass_vera_devices(hass, "switch", 2) + assert_hass_vera_devices(hass, "lock", 1) + assert_hass_vera_devices(hass, "climate", 1) + assert_hass_vera_devices(hass, "cover", 1) diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py new file mode 100644 index 00000000000000..fa63ce63454444 --- /dev/null +++ b/tests/components/vera/test_light.py @@ -0,0 +1,76 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import CATEGORY_DIMMER, VeraDimmer + +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_HS_COLOR +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_light( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraDimmer) # type: VeraDimmer + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_DIMMER + vera_device.is_switched_on = MagicMock(return_value=False) + vera_device.get_brightness = MagicMock(return_value=0) + vera_device.get_color = MagicMock(return_value=[0, 0, 0]) + vera_device.is_dimmable = True + entity_id = "light.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + assert hass.states.get(entity_id).state == "off" + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.switch_on.assert_called() + vera_device.is_switched_on.return_value = True + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "on" + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id, ATTR_HS_COLOR: [300, 70]}, + ) + await hass.async_block_till_done() + vera_device.set_color.assert_called_with((255, 76, 255)) + vera_device.is_switched_on.return_value = True + vera_device.get_color.return_value = (255, 76, 255) + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "on" + assert hass.states.get(entity_id).attributes["hs_color"] == (300.0, 70.196) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id, ATTR_BRIGHTNESS: 55}, + ) + await hass.async_block_till_done() + vera_device.set_brightness.assert_called_with(55) + vera_device.is_switched_on.return_value = True + vera_device.get_brightness.return_value = 55 + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "on" + assert hass.states.get(entity_id).attributes["brightness"] == 55 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.switch_off.assert_called() + vera_device.is_switched_on.return_value = False + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "off" diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py new file mode 100644 index 00000000000000..362bdbeddc0fde --- /dev/null +++ b/tests/components/vera/test_lock.py @@ -0,0 +1,49 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import CATEGORY_LOCK, VeraLock + +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_lock( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraLock) # type: VeraLock + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_LOCK + vera_device.is_locked = MagicMock(return_value=False) + entity_id = "lock.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + assert hass.states.get(entity_id).state == STATE_UNLOCKED + + await hass.services.async_call( + "lock", "lock", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.lock.assert_called() + vera_device.is_locked.return_value = True + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_LOCKED + + await hass.services.async_call( + "lock", "unlock", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.unlock.assert_called() + vera_device.is_locked.return_value = False + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNLOCKED diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py new file mode 100644 index 00000000000000..136227ffa7126e --- /dev/null +++ b/tests/components/vera/test_scene.py @@ -0,0 +1,27 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import VeraScene + +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_scene( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_scene = MagicMock(spec=VeraScene) # type: VeraScene + vera_scene.scene_id = 1 + vera_scene.name = "dev1" + entity_id = "scene.dev1_1" + + await vera_component_factory.configure_component( + hass=hass, scenes=(vera_scene,), + ) + + await hass.services.async_call( + "scene", "turn_on", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py new file mode 100644 index 00000000000000..da28fc225e0bac --- /dev/null +++ b/tests/components/vera/test_sensor.py @@ -0,0 +1,198 @@ +"""Vera tests.""" +from typing import Any, Callable, Tuple +from unittest.mock import MagicMock + +from pyvera import ( + CATEGORY_HUMIDITY_SENSOR, + CATEGORY_LIGHT_SENSOR, + CATEGORY_POWER_METER, + CATEGORY_SCENE_CONTROLLER, + CATEGORY_TEMPERATURE_SENSOR, + CATEGORY_UV_SENSOR, + VeraController, + VeraSensor, +) + +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def run_sensor_test( + hass: HomeAssistant, + vera_component_factory: ComponentFactory, + category: int, + class_property: str, + assert_states: Tuple[Tuple[Any, Any]], + assert_unit_of_measurement: str = None, + setup_callback: Callable[[VeraController], None] = None, +) -> None: + """Test generic sensor.""" + vera_device = MagicMock(spec=VeraSensor) # type: VeraSensor + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = category + setattr(vera_device, class_property, "33") + entity_id = "sensor.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), setup_callback=setup_callback + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + for (initial_value, state_value) in assert_states: + setattr(vera_device, class_property, initial_value) + update_callback(vera_device) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == state_value + if assert_unit_of_measurement: + assert state.attributes["unit_of_measurement"] == assert_unit_of_measurement + + +async def test_temperature_sensor_f( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + + def setup_callback(controller: VeraController, hass_config: dict) -> None: + controller.temperature_units = "F" + + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_TEMPERATURE_SENSOR, + class_property="temperature", + assert_states=(("33", "1"), ("44", "7"),), + setup_callback=setup_callback, + ) + + +async def test_temperature_sensor_c( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_TEMPERATURE_SENSOR, + class_property="temperature", + assert_states=(("33", "33"), ("44", "44"),), + ) + + +async def test_light_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_LIGHT_SENSOR, + class_property="light", + assert_states=(("12", "12"), ("13", "13"),), + assert_unit_of_measurement="lx", + ) + + +async def test_uv_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_UV_SENSOR, + class_property="light", + assert_states=(("12", "12"), ("13", "13"),), + assert_unit_of_measurement="level", + ) + + +async def test_humidity_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_HUMIDITY_SENSOR, + class_property="humidity", + assert_states=(("12", "12"), ("13", "13"),), + assert_unit_of_measurement="%", + ) + + +async def test_power_meter_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=CATEGORY_POWER_METER, + class_property="power", + assert_states=(("12", "12"), ("13", "13"),), + assert_unit_of_measurement="watts", + ) + + +async def test_trippable_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + + def setup_callback(controller: VeraController, hass_config: dict) -> None: + controller.get_devices()[0].is_trippable = True + + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=999, + class_property="is_tripped", + assert_states=((True, "Tripped"), (False, "Not Tripped"), (True, "Tripped"),), + setup_callback=setup_callback, + ) + + +async def test_unknown_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + + def setup_callback(controller: VeraController, hass_config: dict) -> None: + controller.get_devices()[0].is_trippable = False + + await run_sensor_test( + hass=hass, + vera_component_factory=vera_component_factory, + category=999, + class_property="is_tripped", + assert_states=((True, "Unknown"), (False, "Unknown"), (True, "Unknown"),), + setup_callback=setup_callback, + ) + + +async def test_scene_controller_sensor( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraSensor) # type: VeraSensor + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_SCENE_CONTROLLER + vera_device.get_last_scene_id = MagicMock(return_value="id0") + vera_device.get_last_scene_time = MagicMock(return_value="0000") + entity_id = "sensor.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + vera_device.get_last_scene_time = "1111" + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "id0" diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py new file mode 100644 index 00000000000000..ba09068e7e602f --- /dev/null +++ b/tests/components/vera/test_switch.py @@ -0,0 +1,48 @@ +"""Vera tests.""" +from unittest.mock import MagicMock + +from pyvera import CATEGORY_SWITCH, VeraSwitch + +from homeassistant.core import HomeAssistant + +from .common import ComponentFactory + + +async def test_switch( + hass: HomeAssistant, vera_component_factory: ComponentFactory +) -> None: + """Test function.""" + vera_device = MagicMock(spec=VeraSwitch) # type: VeraSwitch + vera_device.device_id = 1 + vera_device.name = "dev1" + vera_device.category = CATEGORY_SWITCH + vera_device.is_switched_on = MagicMock(return_value=False) + entity_id = "switch.dev1_1" + + component_data = await vera_component_factory.configure_component( + hass=hass, devices=(vera_device,), + ) + controller = component_data.controller + update_callback = controller.register.call_args_list[0][0][1] + + assert hass.states.get(entity_id).state == "off" + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.switch_on.assert_called() + vera_device.is_switched_on.return_value = True + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "on" + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, + ) + await hass.async_block_till_done() + vera_device.switch_off.assert_called() + vera_device.is_switched_on.return_value = False + update_callback(vera_device) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == "off" From d451e54e3418b7210c93b11315526895ed0488a3 Mon Sep 17 00:00:00 2001 From: Bernhard B Date: Mon, 9 Dec 2019 00:27:06 +0100 Subject: [PATCH 2234/3953] Add Signal Messenger integration (#28537) * added signalmessenger integration * allows to send a message (with an attachment) to one or more recipients * added signalmessenger documentation to manifest file * remove debug logging from signalmessenger integration * add signalmessenger to .coveragerc * fixed typo in signalmessenger manifes * moved service specific code to own pypi library * updated pysignalclirestapi dependeny in manifest.json * added pysignalclirestapi requirement for signalmessenger component * fixed typo in codeowners * reworked signalmessenger integration based on code review input * updated requirements for signalmessenger * small code improvements in signalmessenger integration * no need to use the get() method to access dict parameters that are required * small changes in signalmessenger integration * re-ordered import statements * removed empty "requirements" list (not needed) * changed import order in signalmessenger integration according to PEP 8 * used isort to order includes in signalmessenger integration * renamed signalmessenger to signal_messenger * renamed signalmessenger to signal_messenger in CODEOWNERS file * changed documentation url in signal_messenger integration to new name * changed signal messenger naming in .coveragerc --- .coveragerc | 2 + CODEOWNERS | 1 + .../components/signal_messenger/__init__.py | 1 + .../components/signal_messenger/manifest.json | 10 +++ .../components/signal_messenger/notify.py | 71 +++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 88 insertions(+) create mode 100644 homeassistant/components/signal_messenger/__init__.py create mode 100644 homeassistant/components/signal_messenger/manifest.json create mode 100644 homeassistant/components/signal_messenger/notify.py diff --git a/.coveragerc b/.coveragerc index e85c97ef80c411..b974356e0fe0aa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -611,6 +611,8 @@ omit = homeassistant/components/shodan/sensor.py homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py + homeassistant/components/signal_messenger/__init__.py + homeassistant/components/signal_messenger/notify.py homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py homeassistant/components/simplisafe/alarm_control_panel.py diff --git a/CODEOWNERS b/CODEOWNERS index 472798f72a6b2c..6723244c089ef6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -281,6 +281,7 @@ homeassistant/components/seventeentrack/* @bachya homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff +homeassistant/components/signal_messenger/* @bbernhard homeassistant/components/simplisafe/* @bachya homeassistant/components/sinch/* @bendikrb homeassistant/components/slide/* @ualex73 diff --git a/homeassistant/components/signal_messenger/__init__.py b/homeassistant/components/signal_messenger/__init__.py new file mode 100644 index 00000000000000..045eb95f1b31af --- /dev/null +++ b/homeassistant/components/signal_messenger/__init__.py @@ -0,0 +1 @@ +"""The signalmessenger component.""" diff --git a/homeassistant/components/signal_messenger/manifest.json b/homeassistant/components/signal_messenger/manifest.json new file mode 100644 index 00000000000000..b3494ce8bab889 --- /dev/null +++ b/homeassistant/components/signal_messenger/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "signal_messenger", + "name": "signal_messenger", + "documentation": "https://www.home-assistant.io/integrations/signal_messenger", + "dependencies": [], + "codeowners": ["@bbernhard"], + "requirements": [ + "pysignalclirestapi==0.1.4" + ] +} diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py new file mode 100644 index 00000000000000..8fbf9c708734d8 --- /dev/null +++ b/homeassistant/components/signal_messenger/notify.py @@ -0,0 +1,71 @@ +"""Signal Messenger for notify component.""" +import logging + +from pysignalclirestapi import SignalCliRestApi, SignalCliRestApiError +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + PLATFORM_SCHEMA, + BaseNotificationService, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_SENDER_NR = "number" +CONF_RECP_NR = "recipients" +CONF_SIGNAL_CLI_REST_API = "url" +ATTR_FILENAME = "attachment" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_SENDER_NR): cv.string, + vol.Required(CONF_SIGNAL_CLI_REST_API): cv.string, + vol.Required(CONF_RECP_NR): vol.All(cv.ensure_list, [cv.string]), + } +) + + +def get_service(hass, config, discovery_info=None): + """Get the SignalMessenger notification service.""" + + sender_nr = config[CONF_SENDER_NR] + recp_nrs = config[CONF_RECP_NR] + signal_cli_rest_api_url = config[CONF_SIGNAL_CLI_REST_API] + + signal_cli_rest_api = SignalCliRestApi( + signal_cli_rest_api_url, sender_nr, api_version=1 + ) + + return SignalNotificationService(recp_nrs, signal_cli_rest_api) + + +class SignalNotificationService(BaseNotificationService): + """Implement the notification service for SignalMessenger.""" + + def __init__(self, recp_nrs, signal_cli_rest_api): + """Initialize the service.""" + + self._recp_nrs = recp_nrs + self._signal_cli_rest_api = signal_cli_rest_api + + def send_message(self, message="", **kwargs): + """Send a message to a one or more recipients. + + Additionally a file can be attached. + """ + + _LOGGER.debug("Sending signal message") + + data = kwargs.get(ATTR_DATA) + + filename = None + if data is not None and ATTR_FILENAME in data: + filename = data[ATTR_FILENAME] + + try: + self._signal_cli_rest_api.send_message(message, self._recp_nrs, filename) + except SignalCliRestApiError as ex: + _LOGGER.error("%s", ex) + raise ex diff --git a/requirements_all.txt b/requirements_all.txt index 87ca2a46177f4b..e5983972243952 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1482,6 +1482,9 @@ pysesame2==1.0.1 # homeassistant.components.goalfeed pysher==1.0.1 +# homeassistant.components.signal_messenger +pysignalclirestapi==0.1.4 + # homeassistant.components.sma pysma==0.3.4 From 8dea7f0f98df8c93d4a628d4cba470a64b263877 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 9 Dec 2019 00:32:08 +0000 Subject: [PATCH 2235/3953] [ci skip] Translation update --- .../components/elgato/.translations/en.json | 50 +++++++++---------- .../components/elgato/.translations/no.json | 11 ++++ .../elgato/.translations/zh-Hant.json | 27 ++++++++++ 3 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/elgato/.translations/no.json create mode 100644 homeassistant/components/elgato/.translations/zh-Hant.json diff --git a/homeassistant/components/elgato/.translations/en.json b/homeassistant/components/elgato/.translations/en.json index 03c46f02efcab3..d52003d10e162e 100644 --- a/homeassistant/components/elgato/.translations/en.json +++ b/homeassistant/components/elgato/.translations/en.json @@ -1,27 +1,27 @@ { - "config": { - "title": "Elgato Key Light", - "flow_title": "Elgato Key Light: {serial_number}", - "step": { - "user": { - "title": "Link your Elgato Key Light", - "description": "Set up your Elgato Key Light to integrate with Home Assistant.", - "data": { - "host": "Host or IP address", - "port": "Port number" - } - }, - "zeroconf_confirm": { - "description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?", - "title": "Discovered Elgato Key Light device" - } - }, - "error": { - "connection_error": "Failed to connect to Elgato Key Light device." - }, - "abort": { - "already_configured": "This Elgato Key Light device is already configured.", - "connection_error": "Failed to connect to Elgato Key Light device." + "config": { + "abort": { + "already_configured": "This Elgato Key Light device is already configured.", + "connection_error": "Failed to connect to Elgato Key Light device." + }, + "error": { + "connection_error": "Failed to connect to Elgato Key Light device." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Host or IP address", + "port": "Port number" + }, + "description": "Set up your Elgato Key Light to integrate with Home Assistant.", + "title": "Link your Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?", + "title": "Discovered Elgato Key Light device" + } + }, + "title": "Elgato Key Light" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/no.json b/homeassistant/components/elgato/.translations/no.json new file mode 100644 index 00000000000000..df7d6db26216e1 --- /dev/null +++ b/homeassistant/components/elgato/.translations/no.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Portnummer" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/zh-Hant.json b/homeassistant/components/elgato/.translations/zh-Hant.json new file mode 100644 index 00000000000000..b187abc5ccd734 --- /dev/null +++ b/homeassistant/components/elgato/.translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Elgato Key \u7167\u660e\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "connection_error": "Elgato Key \u7167\u660e\u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "error": { + "connection_error": "Elgato Key \u7167\u660e\u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "flow_title": "Elgato Key \u7167\u660e\uff1a{serial_number}", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a Elgato Key \u7167\u660e\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", + "title": "\u9023\u7d50 Elgato Key \u7167\u660e\u3002" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u5c07 Elgato Key \u7167\u660e\u5e8f\u865f `{serial_number}` \u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u767c\u73fe\u5230 Elgato Key \u7167\u660e\u8a2d\u5099" + } + }, + "title": "Elgato Key \u7167\u660e" + } +} \ No newline at end of file From 6996ad35415c0ec6e8ea5fe0efadab1471da4efd Mon Sep 17 00:00:00 2001 From: gjbadros Date: Mon, 9 Dec 2019 00:14:55 -0800 Subject: [PATCH 2236/3953] Protect Doorbird platform from failing when individual doorbird fails (#29374) * Wrap connection attempt by try/except block so an individual downed doorbird does not fail the whole platform * Fixed lint warning * Disable too-broad-exception pylint warning since the whole point of this is to catch all exceptions and log them while letting processing continue; I don't disable a lint warning lightly, but this is the entire intent of this PR. * Fixed name of pylint warning to broad-except * Use _LOGGER.exception to get the stack trace too, per PEP8 recommendation for a bare Exception being caught. * per @balloob, switch to just catching OSError on a narrow block; I'm not sure this protects all problems with Doorbird device startup, but it protects against the primary one I experience. --- homeassistant/components/doorbird/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index d92ff3d36924b3..680ee1354eb236 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -67,8 +67,14 @@ def setup(hass, config): token = doorstation_config.get(CONF_TOKEN) name = doorstation_config.get(CONF_NAME) or "DoorBird {}".format(index + 1) - device = DoorBird(device_ip, username, password) - status = device.ready() + try: + device = DoorBird(device_ip, username, password) + status = device.ready() + except OSError as oserr: + _LOGGER.error( + "Failed to setup doorbird at %s: %s; not retrying", device_ip, oserr + ) + continue if status[0]: doorstation = ConfiguredDoorBird(device, name, events, custom_url, token) From 8c1a8b502d319d2f8f3452db0215116c9ec2f481 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 09:36:42 +0100 Subject: [PATCH 2237/3953] Sort imports according to PEP8 for velbus (#29676) --- homeassistant/components/velbus/__init__.py | 5 +++-- homeassistant/components/velbus/binary_sensor.py | 2 +- homeassistant/components/velbus/climate.py | 2 +- homeassistant/components/velbus/config_flow.py | 2 +- homeassistant/components/velbus/cover.py | 4 ++-- homeassistant/components/velbus/sensor.py | 2 +- tests/components/velbus/test_config_flow.py | 4 ++-- 7 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 9946f06446f059..317c305254be96 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -1,13 +1,14 @@ """Support for Velbus devices.""" import asyncio import logging + import velbus import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PORT, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.exceptions import ConfigEntryNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/velbus/binary_sensor.py b/homeassistant/components/velbus/binary_sensor.py index 9230632e442e27..505303ded240b2 100644 --- a/homeassistant/components/velbus/binary_sensor.py +++ b/homeassistant/components/velbus/binary_sensor.py @@ -3,8 +3,8 @@ from homeassistant.components.binary_sensor import BinarySensorDevice -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index eb5ed00c3955f4..812e4605d9546d 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -10,8 +10,8 @@ ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index e9cbe14ce25be6..9325acf0608003 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -3,7 +3,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PORT, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index cf73af593b8737..aea02331ead42f 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -4,14 +4,14 @@ from velbus.util import VelbusException from homeassistant.components.cover import ( - CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, + CoverDevice, ) -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 3b7f2b6f5bc61e..8af5df9e165baa 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -1,8 +1,8 @@ """Support for Velbus sensors.""" import logging -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 271f0b3dd3ad39..66273e01f4360f 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for the Velbus config flow.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest from homeassistant import data_entry_flow from homeassistant.components.velbus import config_flow -from homeassistant.const import CONF_PORT, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from tests.common import MockConfigEntry From 942f654d926c484ebd25f5fcb11a5a9b64145268 Mon Sep 17 00:00:00 2001 From: Bruno Filipe Date: Mon, 9 Dec 2019 05:37:15 -0300 Subject: [PATCH 2238/3953] Proactively report Alexa Endpoint Health properties (#29736) --- homeassistant/components/alexa/capabilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 38f769b7cd4a0f..9f217d2e9c9586 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -262,7 +262,7 @@ def properties_supported(self): def properties_proactively_reported(self): """Return True if properties asynchronously reported.""" - return False + return True def properties_retrievable(self): """Return True if properties can be retrieved.""" From 236a21c76ea7f223fa3b1f83ab4dc06e68b07cf8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 09:38:14 +0100 Subject: [PATCH 2239/3953] Sort imports according to PEP8 for pi_hole (#29726) --- homeassistant/components/pi_hole/__init__.py | 8 ++++---- homeassistant/components/pi_hole/sensor.py | 4 ++-- tests/components/pi_hole/test_init.py | 4 +++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 95351083b5abfe..8ee21af7858c8f 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -1,31 +1,31 @@ """The pi_hole component.""" import logging -import voluptuous as vol from hole import Hole from hole.exceptions import HoleError +import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( + CONF_API_KEY, CONF_HOST, CONF_NAME, - CONF_API_KEY, CONF_SSL, CONF_VERIFY_SSL, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN 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.util import Throttle from .const import ( - DOMAIN, CONF_LOCATION, DEFAULT_HOST, DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, DEFAULT_VERIFY_SSL, + DOMAIN, MIN_TIME_BETWEEN_UPDATES, SERVICE_DISABLE, SERVICE_DISABLE_ATTR_DURATION, diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 4e80e9767a6fb1..f76e756aec49f3 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -4,10 +4,10 @@ from homeassistant.helpers.entity import Entity from .const import ( - DOMAIN as PIHOLE_DOMAIN, ATTR_BLOCKED_DOMAINS, - SENSOR_LIST, + DOMAIN as PIHOLE_DOMAIN, SENSOR_DICT, + SENSOR_LIST, ) LOGGER = logging.getLogger(__name__) diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index f30422bfea99f7..b2f4b3f28aff18 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -1,11 +1,13 @@ """Test pi_hole component.""" +from unittest.mock import patch + from asynctest import CoroutineMock from hole import Hole from homeassistant.components import pi_hole + from tests.common import async_setup_component -from unittest.mock import patch def mock_pihole_data_call(Hole): From 2abc9005ccefb0e942bd03df31aa5c1bb4485033 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 09:38:24 +0100 Subject: [PATCH 2240/3953] use isort to sort imports according to PEP8 for homeassistant (#29718) --- .../components/homeassistant/__init__.py | 21 ++++++------ .../components/homeassistant/scene.py | 10 +++--- tests/components/homeassistant/test_init.py | 34 +++++++++---------- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index d2d6abdadb5f07..8aa1d7e020aca0 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -6,23 +6,22 @@ import voluptuous as vol -import homeassistant.core as ha import homeassistant.config as conf_util -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.service import async_extract_entity_ids -from homeassistant.helpers import intent from homeassistant.const import ( ATTR_ENTITY_ID, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - SERVICE_TOGGLE, - SERVICE_HOMEASSISTANT_STOP, - SERVICE_HOMEASSISTANT_RESTART, - RESTART_EXIT_CODE, ATTR_LATITUDE, ATTR_LONGITUDE, + RESTART_EXIT_CODE, + SERVICE_HOMEASSISTANT_RESTART, + SERVICE_HOMEASSISTANT_STOP, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) -from homeassistant.helpers import config_validation as cv +import homeassistant.core as ha +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers.service import async_extract_entity_ids _LOGGER = logging.getLogger(__name__) DOMAIN = ha.DOMAIN diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 576bf540e006d3..af271a069fe141 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -4,6 +4,8 @@ import voluptuous as vol +from homeassistant import config as conf_util +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, @@ -11,21 +13,19 @@ CONF_ID, CONF_NAME, CONF_PLATFORM, + SERVICE_RELOAD, STATE_OFF, STATE_ON, - SERVICE_RELOAD, ) -from homeassistant.core import State, DOMAIN as HA_DOMAIN -from homeassistant import config as conf_util +from homeassistant.core import DOMAIN as HA_DOMAIN, State 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.helpers.state import async_reproduce_state -from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene +from homeassistant.loader import async_get_integration def _convert_states(states): diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 7a97de0f68e42f..6c2b7f78e2491f 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -2,40 +2,40 @@ # pylint: disable=protected-access import asyncio import unittest -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import yaml -import homeassistant.core as ha from homeassistant import config +import homeassistant.components as comps +from homeassistant.components.homeassistant import ( + SERVICE_CHECK_CONFIG, + SERVICE_RELOAD_CORE_CONFIG, +) from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, + EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, SERVICE_TOGGLE, - EVENT_CORE_CONFIG_UPDATE, -) -import homeassistant.components as comps -from homeassistant.setup import async_setup_component -from homeassistant.components.homeassistant import ( - SERVICE_CHECK_CONFIG, - SERVICE_RELOAD_CORE_CONFIG, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) -import homeassistant.helpers.intent as intent +import homeassistant.core as ha from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity +import homeassistant.helpers.intent as intent +from homeassistant.setup import async_setup_component from tests.common import ( + async_capture_events, + async_mock_service, get_test_home_assistant, + mock_coro, mock_service, patch_yaml_files, - mock_coro, - async_mock_service, - async_capture_events, ) From 179003676760d44dbcd353c921229502606a059b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 09:38:38 +0100 Subject: [PATCH 2241/3953] use isort to sort imports according to PEP8 for zone (#29712) --- homeassistant/components/zone/__init__.py | 17 ++++++++--------- homeassistant/components/zone/config_flow.py | 7 +++---- tests/components/zone/test_config_flow.py | 4 ++-- tests/components/zone/test_init.py | 3 +-- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 6ae62be3eb9b2d..357d4eac172923 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -4,29 +4,28 @@ import voluptuous as vol -from homeassistant.core import callback, State -from homeassistant.loader import bind_hass -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_NAME, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_ICON, CONF_LATITUDE, CONF_LONGITUDE, - CONF_ICON, + CONF_NAME, CONF_RADIUS, EVENT_CORE_CONFIG_UPDATE, ) +from homeassistant.core import State, callback from homeassistant.helpers import config_per_platform +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.loader import bind_hass from homeassistant.util import slugify -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.util.location import distance - from .config_flow import configured_zones -from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE, ATTR_PASSIVE, ATTR_RADIUS +from .const import ATTR_PASSIVE, ATTR_RADIUS, CONF_PASSIVE, DOMAIN, HOME_ZONE from .zone import Zone - # mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index 3963375477294f..4531ff7b834776 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -4,22 +4,21 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import ( - CONF_NAME, + CONF_ICON, CONF_LATITUDE, CONF_LONGITUDE, - CONF_ICON, + CONF_NAME, CONF_RADIUS, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE - # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/tests/components/zone/test_config_flow.py b/tests/components/zone/test_config_flow.py index 1e1bca9cdea277..5f57e8b4064d1d 100644 --- a/tests/components/zone/test_config_flow.py +++ b/tests/components/zone/test_config_flow.py @@ -3,10 +3,10 @@ from homeassistant.components.zone import config_flow from homeassistant.components.zone.const import CONF_PASSIVE, DOMAIN, HOME_ZONE from homeassistant.const import ( - CONF_NAME, + CONF_ICON, CONF_LATITUDE, CONF_LONGITUDE, - CONF_ICON, + CONF_NAME, CONF_RADIUS, ) diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index b5b975ae4bd3d4..d4a76463c18de8 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -6,8 +6,7 @@ from homeassistant import setup from homeassistant.components import zone -from tests.common import get_test_home_assistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, get_test_home_assistant async def test_setup_entry_successful(hass): From d0c7db548f9cffebe895fd7cdc54abf784de29b5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 09:39:26 +0100 Subject: [PATCH 2242/3953] use isort to sort imports according to PEP8 for group (#29713) --- tests/components/group/test_cover.py | 6 +++--- tests/components/group/test_init.py | 20 +++++++++---------- tests/components/group/test_notify.py | 6 +++--- .../components/group/test_reproduce_state.py | 1 + 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index e687f105e20c6b..42345536120a4a 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -19,16 +19,16 @@ CONF_ENTITIES, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, - SERVICE_TOGGLE, - SERVICE_TOGGLE_COVER_TILT, SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, - STATE_OPEN, + SERVICE_TOGGLE, + SERVICE_TOGGLE_COVER_TILT, STATE_CLOSED, + STATE_OPEN, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 44f5448ec8f461..5c826dbe85d375 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -5,21 +5,21 @@ import unittest from unittest.mock import patch -from homeassistant.setup import setup_component, async_setup_component +import homeassistant.components.group as group from homeassistant.const import ( - STATE_ON, - STATE_OFF, - STATE_HOME, - STATE_UNKNOWN, - ATTR_ICON, - ATTR_HIDDEN, ATTR_ASSUMED_STATE, - STATE_NOT_HOME, ATTR_FRIENDLY_NAME, + ATTR_HIDDEN, + ATTR_ICON, + STATE_HOME, + STATE_NOT_HOME, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, ) -import homeassistant.components.group as group +from homeassistant.setup import async_setup_component, setup_component -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant from tests.components.group import common diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index d7b7496573bebd..f029ec9d2fa9e0 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -3,10 +3,10 @@ import unittest from unittest.mock import MagicMock, patch -from homeassistant.setup import setup_component -import homeassistant.components.notify as notify -import homeassistant.components.group.notify as group import homeassistant.components.demo.notify as demo +import homeassistant.components.group.notify as group +import homeassistant.components.notify as notify +from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/group/test_reproduce_state.py b/tests/components/group/test_reproduce_state.py index f3a56a4647255b..5cc1c862cd332c 100644 --- a/tests/components/group/test_reproduce_state.py +++ b/tests/components/group/test_reproduce_state.py @@ -2,6 +2,7 @@ from asyncio import Future from unittest.mock import patch + from homeassistant.components.group.reproduce_state import async_reproduce_states from homeassistant.core import Context, State From c399fe2837b38a99e949da07ed33a747522e47ff Mon Sep 17 00:00:00 2001 From: Santobert Date: Mon, 9 Dec 2019 10:46:25 +0100 Subject: [PATCH 2243/3953] Change source of device_info (#29570) --- homeassistant/components/neato/vacuum.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 9cac0cd24ceb79..d8a3e4ded45239 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -165,7 +165,7 @@ def update(self): _LOGGER.debug("Running Neato Vacuums update") try: if self._robot_stats is None: - self._robot_stats = self.robot.get_robot_info().json() + self._robot_stats = self.robot.get_general_info().json().get("data") except NeatoRobotException: _LOGGER.warning("Couldn't fetch robot information of %s", self._name) @@ -319,10 +319,9 @@ def device_info(self): """Device info for neato robot.""" info = {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}, "name": self._name} if self._robot_stats: - info["manufacturer"] = self._robot_stats["data"]["mfg_name"] - info["model"] = self._robot_stats["data"]["modelName"] - if self._state: - info["sw_version"] = self._state["meta"]["firmware"] + info["manufacturer"] = self._robot_stats["battery"]["vendor"] + info["model"] = self._robot_stats["model"] + info["sw_version"] = self._robot_stats["firmware"] def start(self): """Start cleaning or resume cleaning.""" From 791dc5809ff657659994498e79fa1d296d2824ec Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Mon, 9 Dec 2019 12:56:57 +0300 Subject: [PATCH 2244/3953] Fix unit_of_measurement for Starline temperature sensors (#29740) --- homeassistant/components/starline/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 0629a03e148c0d..874fc5a9a7eda4 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -1,5 +1,6 @@ """Reads vehicle status from StarLine API.""" from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level @@ -10,8 +11,8 @@ SENSOR_TYPES = { "battery": ["Battery", None, "V", None], "balance": ["Balance", None, None, "mdi:cash-multiple"], - "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, None, None], - "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, None, None], + "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], + "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], "gsm_lvl": ["GSM Signal", None, "%", None], } From 9df71ecae2a8750ac0e32f5cc8e02327674c69e7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:05:22 +0100 Subject: [PATCH 2245/3953] Sort imports according to PEP8 for neato (#29724) --- homeassistant/components/neato/__init__.py | 4 ++-- tests/components/neato/test_config_flow.py | 2 +- tests/components/neato/test_init.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 5a697e7b9ad108..ad4eb02eccce7c 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,11 +1,11 @@ """Support for Neato botvac connected vacuum cleaners.""" import asyncio -import logging from datetime import timedelta +import logging -import voluptuous as vol from pybotvac import Account, Neato, Vorwerk from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException +import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 3f4bd90d0c1b0b..59db79c1052ac8 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -1,8 +1,8 @@ """Tests for the Neato config flow.""" -import pytest from unittest.mock import patch from pybotvac.exceptions import NeatoLoginException, NeatoRobotException +import pytest from homeassistant import data_entry_flow from homeassistant.components.neato import config_flow diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py index 444cbe8cc5d462..8fa6ad059450d9 100644 --- a/tests/components/neato/test_init.py +++ b/tests/components/neato/test_init.py @@ -1,8 +1,8 @@ """Tests for the Neato init file.""" -import pytest from unittest.mock import patch from pybotvac.exceptions import NeatoLoginException +import pytest from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME From cbf59fb33dfbe23fda43b9e7b40a2028561edf57 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:06:05 +0100 Subject: [PATCH 2246/3953] Sort imports according to PEP8 for input_text (#29719) --- homeassistant/components/input_text/__init__.py | 6 +++--- homeassistant/components/input_text/reproduce_state.py | 2 +- tests/components/input_text/test_init.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index d43e47c11ca730..2d5a23e2a76d8e 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -3,14 +3,14 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_MODE, + ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, - CONF_NAME, CONF_MODE, + CONF_NAME, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py index f64c5c019f65dd..28a2f27ee84653 100644 --- a/homeassistant/components/input_text/reproduce_state.py +++ b/homeassistant/components/input_text/reproduce_state.py @@ -7,7 +7,7 @@ from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE +from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index 4888994d78868b..b758b245092dba 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -4,7 +4,7 @@ from homeassistant.components.input_text import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.core import CoreState, State, Context +from homeassistant.core import Context, CoreState, State from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component From 6a67532a2dc531aadc6fa9514bf376225936f21a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:07:50 +0100 Subject: [PATCH 2247/3953] Sort imports according to PEP8 for linky (#29722) --- homeassistant/components/linky/config_flow.py | 2 +- homeassistant/components/linky/sensor.py | 5 ++--- tests/components/linky/test_config_flow.py | 5 +++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/linky/config_flow.py b/homeassistant/components/linky/config_flow.py index 3b882eed2ad9a3..8a2d307ceabed0 100644 --- a/homeassistant/components/linky/config_flow.py +++ b/homeassistant/components/linky/config_flow.py @@ -1,7 +1,6 @@ """Config flow to configure the Linky integration.""" import logging -import voluptuous as vol from pylinky.client import LinkyClient from pylinky.exceptions import ( PyLinkyAccessException, @@ -9,6 +8,7 @@ PyLinkyException, PyLinkyWrongLoginException, ) +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index 489e66c2b12482..4b5f9ab6cad290 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -1,10 +1,9 @@ """Support for Linky.""" +from datetime import timedelta import json import logging -from datetime import timedelta -from pylinky.client import DAILY, MONTHLY, YEARLY, LinkyClient -from pylinky.client import PyLinkyException +from pylinky.client import DAILY, MONTHLY, YEARLY, LinkyClient, PyLinkyException from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/tests/components/linky/test_config_flow.py b/tests/components/linky/test_config_flow.py index f18ce72c1c37d6..2b90c778a8f488 100644 --- a/tests/components/linky/test_config_flow.py +++ b/tests/components/linky/test_config_flow.py @@ -1,16 +1,17 @@ """Tests for the Linky config flow.""" -import pytest from unittest.mock import patch + from pylinky.exceptions import ( PyLinkyAccessException, PyLinkyEnedisException, PyLinkyException, PyLinkyWrongLoginException, ) +import pytest from homeassistant import data_entry_flow from homeassistant.components.linky import config_flow -from homeassistant.components.linky.const import DOMAIN, DEFAULT_TIMEOUT +from homeassistant.components.linky.const import DEFAULT_TIMEOUT, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from tests.common import MockConfigEntry From 7128396f1a632494620521f99fc539ee68ce8d28 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:08:29 +0100 Subject: [PATCH 2248/3953] Sort imports according to PEP8 for minio (#29723) --- homeassistant/components/minio/__init__.py | 4 ++-- homeassistant/components/minio/minio_helper.py | 4 ++-- tests/components/minio/test_minio.py | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index d411d913082f89..4f5159ed9d50ed 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -1,8 +1,8 @@ """Minio component.""" import logging import os -import threading from queue import Queue +import threading from typing import List import voluptuous as vol @@ -10,7 +10,7 @@ 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 +from .minio_helper import MinioEventThread, create_minio_client _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index bd7b15d27d4f58..2aaba9d408528c 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -1,11 +1,11 @@ """Minio helper methods.""" -import time from collections.abc import Iterable import json import logging +from queue import Queue import re import threading -from queue import Queue +import time from typing import Iterator, List from urllib.parse import unquote diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py index 836b456dc9b5c6..e9b5759097c4c9 100644 --- a/tests/components/minio/test_minio.py +++ b/tests/components/minio/test_minio.py @@ -3,19 +3,19 @@ import json from unittest.mock import MagicMock +from asynctest import call, patch import pytest -from asynctest import patch, call from homeassistant.components.minio import ( - QueueListener, - DOMAIN, + CONF_ACCESS_KEY, CONF_HOST, + CONF_LISTEN, + CONF_LISTEN_BUCKET, CONF_PORT, - CONF_ACCESS_KEY, CONF_SECRET_KEY, CONF_SECURE, - CONF_LISTEN, - CONF_LISTEN_BUCKET, + DOMAIN, + QueueListener, ) from homeassistant.core import callback from homeassistant.setup import async_setup_component From c3d7ab6a7fb9011ed456f618d8574694a8c61db7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:13:37 +0100 Subject: [PATCH 2249/3953] Sort imports according to PEP8 for netgear_lte (#29725) --- homeassistant/components/netgear_lte/__init__.py | 8 ++++---- homeassistant/components/netgear_lte/notify.py | 2 +- homeassistant/components/netgear_lte/sensor.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 4758a13c391291..ac36cc1eb44dee 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -8,6 +8,9 @@ import eternalegypt import voluptuous as vol +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_HOST, CONF_MONITORED_CONDITIONS, @@ -17,14 +20,11 @@ EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index 9700ee3c715396..3e91394aa4f679 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -4,7 +4,7 @@ import attr import eternalegypt -from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService, DOMAIN +from homeassistant.components.notify import ATTR_TARGET, DOMAIN, BaseNotificationService from . import CONF_RECIPIENT, DATA_KEY diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py index 5435df88727ad6..49301a61e4f0c9 100644 --- a/homeassistant/components/netgear_lte/sensor.py +++ b/homeassistant/components/netgear_lte/sensor.py @@ -5,7 +5,7 @@ from homeassistant.exceptions import PlatformNotReady from . import CONF_MONITORED_CONDITIONS, DATA_KEY, LTEEntity -from .sensor_types import SENSOR_SMS, SENSOR_SMS_TOTAL, SENSOR_USAGE, SENSOR_UNITS +from .sensor_types import SENSOR_SMS, SENSOR_SMS_TOTAL, SENSOR_UNITS, SENSOR_USAGE _LOGGER = logging.getLogger(__name__) From c5316dbc39a73f300a9e8c4e48764222a077ee1c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:14:08 +0100 Subject: [PATCH 2250/3953] Sort imports according to PEP8 for iqvia (#29720) --- homeassistant/components/iqvia/__init__.py | 3 +-- homeassistant/components/iqvia/config_flow.py | 2 +- homeassistant/components/iqvia/sensor.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index e3add21c3a41d0..397cffe6d8cf99 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -4,8 +4,7 @@ import logging from pyiqvia import Client -from pyiqvia.errors import IQVIAError, InvalidZipError - +from pyiqvia.errors import InvalidZipError, IQVIAError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT diff --git a/homeassistant/components/iqvia/config_flow.py b/homeassistant/components/iqvia/config_flow.py index f6776a833eeaa8..abec8eff09a037 100644 --- a/homeassistant/components/iqvia/config_flow.py +++ b/homeassistant/components/iqvia/config_flow.py @@ -1,10 +1,10 @@ """Config flow to configure the IQVIA component.""" from collections import OrderedDict -import voluptuous as vol from pyiqvia import Client from pyiqvia.errors import InvalidZipError +import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 90aa89f06d1699..21c31bbff08809 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -9,8 +9,8 @@ DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, - TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_INDEX, + TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ASTHMA_FORECAST, From ebb2722d039b9167b2259ff73b7b1688e75cb39e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:15:02 +0100 Subject: [PATCH 2251/3953] Sort imports according to PEP8 for gpslogger (#29717) --- .../components/gpslogger/__init__.py | 19 +++++++++++-------- .../components/gpslogger/config_flow.py | 2 +- .../components/gpslogger/device_tracker.py | 6 +++--- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 3ac09457d81541..aa95d17cbfc125 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -1,30 +1,33 @@ """Support for GPSLogger.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import ATTR_BATTERY +from homeassistant.components.device_tracker import ( + ATTR_BATTERY, + DOMAIN as DEVICE_TRACKER, +) from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, - HTTP_OK, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, + HTTP_OK, + HTTP_UNPROCESSABLE_ENTITY, ) from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER + from .const import ( - DOMAIN, - ATTR_ALTITUDE, ATTR_ACCURACY, ATTR_ACTIVITY, + ATTR_ALTITUDE, ATTR_DEVICE, ATTR_DIRECTION, ATTR_PROVIDER, ATTR_SPEED, + DOMAIN, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/gpslogger/config_flow.py b/homeassistant/components/gpslogger/config_flow.py index 5173c02e7ffd80..ef90a8d16074f2 100644 --- a/homeassistant/components/gpslogger/config_flow.py +++ b/homeassistant/components/gpslogger/config_flow.py @@ -1,7 +1,7 @@ """Config flow for GPSLogger.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index c9dbbfee026e76..d8afc377d400bb 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,15 +1,15 @@ """Support for the GPSLogger device tracking.""" import logging -from homeassistant.core import callback +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, ) -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.core import callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity From 6a11e6aa72bf2789fd1b674ab847d9f2dd099ffe Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:19:23 +0100 Subject: [PATCH 2252/3953] Sort imports according to PEP8 for soma (#29709) --- homeassistant/components/soma/__init__.py | 10 ++++------ homeassistant/components/soma/config_flow.py | 3 ++- homeassistant/components/soma/cover.py | 5 ++--- tests/components/soma/test_config_flow.py | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index b4daa28b5b2595..93ee4fc9b8faa4 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -1,20 +1,18 @@ """Support for Soma Smartshades.""" import logging -import voluptuous as vol from api.soma_api import SomaApi from requests import RequestException +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.const import CONF_HOST, CONF_PORT - -from .const import DOMAIN, HOST, PORT, API - +from .const import API, DOMAIN, HOST, PORT DEVICES = "devices" diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py index e2f89273520da4..f46066e23cacef 100644 --- a/homeassistant/components/soma/config_flow.py +++ b/homeassistant/components/soma/config_flow.py @@ -1,12 +1,13 @@ """Config flow for Soma.""" import logging -import voluptuous as vol from api.soma_api import SomaApi from requests import RequestException +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index 1577b7f2911f03..d23cc9ec5d0d3c 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -2,9 +2,8 @@ import logging -from homeassistant.components.cover import CoverDevice, ATTR_POSITION -from homeassistant.components.soma import DOMAIN, SomaEntity, DEVICES, API - +from homeassistant.components.cover import ATTR_POSITION, CoverDevice +from homeassistant.components.soma import API, DEVICES, DOMAIN, SomaEntity _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 764a18d1b8b98a..15f90516c170a9 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -5,9 +5,9 @@ from requests import RequestException from homeassistant import data_entry_flow -from homeassistant.components.soma import config_flow, DOMAIN -from tests.common import MockConfigEntry +from homeassistant.components.soma import DOMAIN, config_flow +from tests.common import MockConfigEntry MOCK_HOST = "123.45.67.89" MOCK_PORT = 3000 From 9228ed7c4060148c7b04baa3384705802334b11c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:21:16 +0100 Subject: [PATCH 2253/3953] Sort imports according to PEP8 for verisure (#29711) --- homeassistant/components/verisure/__init__.py | 5 ++--- homeassistant/components/verisure/binary_sensor.py | 2 +- tests/components/verisure/test_ethernet_status.py | 2 +- tests/components/verisure/test_lock.py | 8 ++++---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 3ab98a6560e5ec..32735bf06c13a6 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -1,11 +1,10 @@ """Support for Verisure devices.""" +from datetime import timedelta import logging import threading -from datetime import timedelta from jsonpath import jsonpath import verisure - import voluptuous as vol from homeassistant.const import ( @@ -15,8 +14,8 @@ EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 47ec3c536b33ae..bbdd9f54e834fb 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -2,8 +2,8 @@ import logging from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_CONNECTIVITY, + BinarySensorDevice, ) from . import CONF_DOOR_WINDOW, HUB as hub diff --git a/tests/components/verisure/test_ethernet_status.py b/tests/components/verisure/test_ethernet_status.py index 71c7df94ae5431..611adde19d9fd3 100644 --- a/tests/components/verisure/test_ethernet_status.py +++ b/tests/components/verisure/test_ethernet_status.py @@ -2,9 +2,9 @@ from contextlib import contextmanager from unittest.mock import patch +from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import async_setup_component -from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN CONFIG = { "verisure": { diff --git a/tests/components/verisure/test_lock.py b/tests/components/verisure/test_lock.py index ac03e0d9fb61ce..2f69c183d7d3e2 100644 --- a/tests/components/verisure/test_lock.py +++ b/tests/components/verisure/test_lock.py @@ -1,16 +1,16 @@ """Tests for the Verisure platform.""" from contextlib import contextmanager -from unittest.mock import patch, call -from homeassistant.const import STATE_UNLOCKED -from homeassistant.setup import async_setup_component +from unittest.mock import call, patch + from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, SERVICE_LOCK, SERVICE_UNLOCK, ) from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN - +from homeassistant.const import STATE_UNLOCKED +from homeassistant.setup import async_setup_component NO_DEFAULT_LOCK_CODE_CONFIG = { "verisure": { From f9e9a5e4cb9e3423825467630b374db3aec7a677 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:22:04 +0100 Subject: [PATCH 2254/3953] Sort imports according to PEP8 for darksky (#29706) --- homeassistant/components/darksky/sensor.py | 8 ++++---- tests/components/darksky/test_sensor.py | 7 +++---- tests/components/darksky/test_weather.py | 7 +++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 82aaccc9590aaf..5b6da5d11bb53d 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -1,12 +1,11 @@ """Support for Dark Sky weather service.""" -import logging from datetime import timedelta +import logging import forecastio -import voluptuous as vol from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -15,9 +14,10 @@ CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_NAME, - UNIT_UV_INDEX, CONF_SCAN_INTERVAL, + UNIT_UV_INDEX, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index be66b74c186d3d..bb716ed17ec68e 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -1,18 +1,17 @@ """The tests for the Dark Sky platform.""" +from datetime import timedelta import re import unittest from unittest.mock import MagicMock, patch -from datetime import timedelta +import forecastio from requests.exceptions import HTTPError import requests_mock -import forecastio - from homeassistant.components.darksky import sensor as darksky from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant, MockDependency +from tests.common import MockDependency, get_test_home_assistant, load_fixture VALID_CONFIG_MINIMAL = { "sensor": { diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py index ca328f458397b9..09ffe7bdc90a75 100644 --- a/tests/components/darksky/test_weather.py +++ b/tests/components/darksky/test_weather.py @@ -4,15 +4,14 @@ from unittest.mock import patch import forecastio -import requests_mock - from requests.exceptions import ConnectionError +import requests_mock from homeassistant.components import weather -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.setup import setup_component +from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.common import load_fixture, get_test_home_assistant +from tests.common import get_test_home_assistant, load_fixture class TestDarkSky(unittest.TestCase): From ea39d5b42868db6c5041374f92fbc29deceb7753 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:23:21 +0100 Subject: [PATCH 2255/3953] Sort imports according to PEP8 for aws (#29704) --- homeassistant/components/aws/__init__.py | 3 +-- homeassistant/components/aws/notify.py | 3 ++- tests/components/aws/test_init.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index b553b7eafd67d8..600874b0d25d34 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -1,10 +1,9 @@ """Support for Amazon Web Services (AWS).""" import asyncio -import logging from collections import OrderedDict +import logging import aiobotocore - import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index 2afa9a3a4024ff..13fa189a318235 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -12,8 +12,9 @@ ATTR_TITLE_DEFAULT, BaseNotificationService, ) -from homeassistant.const import CONF_PLATFORM, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers.json import JSONEncoder + from .const import ( CONF_CONTEXT, CONF_CREDENTIAL_NAME, diff --git a/tests/components/aws/test_init.py b/tests/components/aws/test_init.py index a9701ec7ff92ce..c7fa9d0a5c1a81 100644 --- a/tests/components/aws/test_init.py +++ b/tests/components/aws/test_init.py @@ -1,5 +1,5 @@ """Tests for the aws component config and setup.""" -from asynctest import patch as async_patch, MagicMock, CoroutineMock +from asynctest import CoroutineMock, MagicMock, patch as async_patch from homeassistant.components import aws from homeassistant.setup import async_setup_component From 2cd55bbb87bd5c537f1f3eada86a44ea1cd33424 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:24:49 +0100 Subject: [PATCH 2256/3953] Sort imports according to PEP8 for device_automation (#29707) --- homeassistant/components/device_automation/__init__.py | 7 +++---- .../components/device_automation/toggle_entity.py | 9 +++++---- tests/components/device_automation/test_init.py | 5 ++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 80e6403329531a..872a4af6cd626a 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,22 +1,21 @@ """Helpers for device automations.""" import asyncio import logging -from typing import Any, List, MutableMapping from types import ModuleType +from typing import Any, List, MutableMapping import voluptuous as vol import voluptuous_serialize -from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.components import websocket_api +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.loader import async_get_integration, IntegrationNotFound +from homeassistant.loader import IntegrationNotFound, async_get_integration from .exceptions import InvalidDeviceAutomationConfig - # mypy: allow-untyped-calls, allow-untyped-defs DOMAIN = "device_automation" diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 5f01f4d9d71ace..7d84eb921e907c 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,11 +1,11 @@ """Device automation helpers for toggle entity.""" from typing import Any, Dict, List + import voluptuous as vol -from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE from homeassistant.components.automation import ( - state as state_automation, AutomationActionType, + state as state_automation, ) from homeassistant.components.device_automation.const import ( CONF_IS_OFF, @@ -24,11 +24,12 @@ CONF_PLATFORM, CONF_TYPE, ) -from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import TRIGGER_BASE_SCHEMA +from . import TRIGGER_BASE_SCHEMA # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index bddef3286ac877..5d997a485a596a 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -1,12 +1,11 @@ """The test for light device automation.""" import pytest -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry - +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, From 852996700fcb3533b2c7d757f07d0f0b095c7356 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:25:29 +0100 Subject: [PATCH 2257/3953] Sort imports according to PEP8 for arest (#29703) --- homeassistant/components/arest/binary_sensor.py | 10 +++++----- homeassistant/components/arest/sensor.py | 10 +++++----- homeassistant/components/arest/switch.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 669a28b707800b..caabe3333f8994 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -1,18 +1,18 @@ """Support for an exposed aREST RESTful API of a device.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( - BinarySensorDevice, - PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorDevice, ) -from homeassistant.const import CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS -from homeassistant.util import Throttle +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_PIN, CONF_RESOURCE import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 2416eeb0ebb08f..270a3cda269f6e 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -1,22 +1,22 @@ """Support for an exposed aREST RESTful API of a device.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE, - CONF_RESOURCE, CONF_MONITORED_VARIABLES, CONF_NAME, + CONF_RESOURCE, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, ) from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index e1a7edacb7e59b..b3db6684cf2a12 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -5,7 +5,7 @@ import requests import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME, CONF_RESOURCE import homeassistant.helpers.config_validation as cv From 69f790f6cc0fafd5fff677f47225725d27a8b149 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:26:03 +0100 Subject: [PATCH 2258/3953] Sort imports according to PEP8 for arduino (#29702) --- homeassistant/components/arduino/__init__.py | 10 ++++++---- homeassistant/components/arduino/sensor.py | 4 ++-- homeassistant/components/arduino/switch.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py index f973ec136e320f..61b03a3160de66 100644 --- a/homeassistant/components/arduino/__init__.py +++ b/homeassistant/components/arduino/__init__.py @@ -1,13 +1,15 @@ """Support for Arduino boards running with the Firmata firmware.""" import logging +from PyMata.pymata import PyMata import serial import voluptuous as vol -from PyMata.pymata import PyMata - -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.const import CONF_PORT +from homeassistant.const import ( + CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/arduino/sensor.py b/homeassistant/components/arduino/sensor.py index a92432537ca7b2..c5863475512a05 100644 --- a/homeassistant/components/arduino/sensor.py +++ b/homeassistant/components/arduino/sensor.py @@ -3,11 +3,11 @@ import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components import arduino +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/arduino/switch.py b/homeassistant/components/arduino/switch.py index 63d83c8575ece1..5b5b161a24a5fd 100644 --- a/homeassistant/components/arduino/switch.py +++ b/homeassistant/components/arduino/switch.py @@ -4,7 +4,7 @@ import voluptuous as vol from homeassistant.components import arduino -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv From 790881fa7ba99a638a9c9921b8cd8214cc9449a0 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:27:16 +0100 Subject: [PATCH 2259/3953] Sort imports according to PEP8 for almond (#29688) --- homeassistant/components/almond/__init__.py | 20 +++++++++---------- .../components/almond/config_flow.py | 10 +++++----- tests/components/almond/test_config_flow.py | 6 ++---- tests/components/almond/test_init.py | 6 +++--- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 66d4b2fc9afb66..8877107b9847c5 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -5,26 +5,26 @@ import time from typing import Optional +from aiohttp import ClientError, ClientSession import async_timeout -from aiohttp import ClientSession, ClientError -from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI +from pyalmond import AbstractAlmondWebAuth, AlmondLocalAuth, WebAlmondAPI import voluptuous as vol -from homeassistant.core import HomeAssistant, CoreState, Context -from homeassistant.const import CONF_TYPE, CONF_HOST, EVENT_HOMEASSISTANT_START -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant import config_entries from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.components import conversation +from homeassistant.const import CONF_HOST, CONF_TYPE, EVENT_HOMEASSISTANT_START +from homeassistant.core import Context, CoreState, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( - config_validation as cv, + aiohttp_client, config_entry_oauth2_flow, + config_validation as cv, event, intent, - aiohttp_client, - storage, network, + storage, ) -from homeassistant import config_entries -from homeassistant.components import conversation from . import config_flow from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py index d79bf6bd6059ae..42f9318a06f76b 100644 --- a/homeassistant/components/almond/config_flow.py +++ b/homeassistant/components/almond/config_flow.py @@ -2,14 +2,14 @@ import asyncio import logging -import async_timeout from aiohttp import ClientError -from yarl import URL -import voluptuous as vol +import async_timeout from pyalmond import AlmondLocalAuth, WebAlmondAPI +import voluptuous as vol +from yarl import URL -from homeassistant import data_entry_flow, config_entries, core -from homeassistant.helpers import config_entry_oauth2_flow, aiohttp_client +from homeassistant import config_entries, core, data_entry_flow +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index afbe25dff5f1c5..0b402ed407ddc0 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -1,12 +1,10 @@ """Test the Almond config flow.""" import asyncio - from unittest.mock import patch - -from homeassistant import config_entries, setup, data_entry_flow -from homeassistant.components.almond.const import DOMAIN +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.almond import config_flow +from homeassistant.components.almond.const import DOMAIN from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry, mock_coro diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py index dd44ea1c8f03f5..f13ad7dd859367 100644 --- a/tests/components/almond/test_init.py +++ b/tests/components/almond/test_init.py @@ -1,16 +1,16 @@ """Tests for Almond set up.""" -from unittest.mock import patch from time import time +from unittest.mock import patch import pytest from homeassistant import config_entries, core +from homeassistant.components.almond import const from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component -from homeassistant.components.almond import const from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry, mock_coro, async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed, mock_coro @pytest.fixture(autouse=True) From 425a1814d9dd4d93a71458a1ed57233604f2f64a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:28:07 +0100 Subject: [PATCH 2260/3953] Sort imports according to PEP8 for geonetnz_quakes (#29668) --- .../components/geonetnz_quakes/__init__.py | 14 ++++++------- .../components/geonetnz_quakes/config_flow.py | 4 ++-- .../geonetnz_quakes/geo_location.py | 2 +- .../geonetnz_quakes/test_config_flow.py | 13 ++++++------ .../geonetnz_quakes/test_geo_location.py | 21 ++++++++++--------- .../components/geonetnz_quakes/test_sensor.py | 19 +++++++++-------- 6 files changed, 38 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index 069c9ab7daa165..141d05068473e3 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -1,30 +1,29 @@ """The GeoNet NZ Quakes integration.""" import asyncio -import logging from datetime import timedelta +import logging -import voluptuous as vol from aio_geojson_geonetnz_quakes import GeonetnzQuakesFeedManager +import voluptuous as vol -from homeassistant.core import callback -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_SCAN_INTERVAL, - CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_MILES, ) -from homeassistant.helpers import config_validation as cv, aiohttp_client +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.unit_system import METRIC_SYSTEM from .config_flow import configured_instances from .const import ( - PLATFORMS, CONF_MINIMUM_MAGNITUDE, CONF_MMI, DEFAULT_FILTER_TIME_INTERVAL, @@ -34,6 +33,7 @@ DEFAULT_SCAN_INTERVAL, DOMAIN, FEED, + PLATFORMS, SIGNAL_DELETE_ENTITY, SIGNAL_NEW_GEOLOCATION, SIGNAL_STATUS, diff --git a/homeassistant/components/geonetnz_quakes/config_flow.py b/homeassistant/components/geonetnz_quakes/config_flow.py index bd93f08c72be6f..cc40f31f1fbeef 100644 --- a/homeassistant/components/geonetnz_quakes/config_flow.py +++ b/homeassistant/components/geonetnz_quakes/config_flow.py @@ -17,13 +17,13 @@ from homeassistant.helpers import config_validation as cv from .const import ( + CONF_MINIMUM_MAGNITUDE, CONF_MMI, + DEFAULT_MINIMUM_MAGNITUDE, DEFAULT_MMI, DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, - DEFAULT_MINIMUM_MAGNITUDE, - CONF_MINIMUM_MAGNITUDE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 1ee7c287c6170e..ae8b8fef48d95a 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -5,10 +5,10 @@ from homeassistant.components.geo_location import GeolocationEvent from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_TIME, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, - ATTR_TIME, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py index 2d8e3750648f6d..494ceaa542d97a 100644 --- a/tests/components/geonetnz_quakes/test_config_flow.py +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -1,26 +1,27 @@ """Define tests for the GeoNet NZ Quakes config flow.""" from datetime import timedelta +from asynctest import CoroutineMock, patch 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, + CONF_MMI, DOMAIN, - async_unload_entry, FEED, + async_setup_entry, + async_unload_entry, + config_flow, ) from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, - CONF_UNIT_SYSTEM, CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, ) + from tests.common import MockConfigEntry diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 04bbdc9dcf03c9..0132a07c7458e1 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -1,34 +1,35 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime -from asynctest import patch, CoroutineMock +from asynctest import CoroutineMock, patch 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_DEPTH, ATTR_EXTERNAL_ID, - ATTR_MAGNITUDE, ATTR_LOCALITY, + ATTR_MAGNITUDE, ATTR_MMI, - ATTR_DEPTH, ATTR_QUALITY, ) from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - CONF_RADIUS, + ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_FRIENDLY_NAME, - ATTR_UNIT_OF_MEASUREMENT, - ATTR_ATTRIBUTION, ATTR_TIME, - ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM + from tests.common import async_fire_time_changed -import homeassistant.util.dt as dt_util from tests.components.geonetnz_quakes import _generate_mock_feed_entry CONFIG = {geonetnz_quakes.DOMAIN: {CONF_RADIUS: 200}} diff --git a/tests/components/geonetnz_quakes/test_sensor.py b/tests/components/geonetnz_quakes/test_sensor.py index 518e08f02bb7b1..aecd012ba1c9d7 100644 --- a/tests/components/geonetnz_quakes/test_sensor.py +++ b/tests/components/geonetnz_quakes/test_sensor.py @@ -1,27 +1,28 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime -from asynctest import patch, CoroutineMock +from asynctest import CoroutineMock, patch from homeassistant.components import geonetnz_quakes from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL from homeassistant.components.geonetnz_quakes.sensor import ( - ATTR_STATUS, - ATTR_LAST_UPDATE, ATTR_CREATED, - ATTR_UPDATED, - ATTR_REMOVED, + ATTR_LAST_UPDATE, ATTR_LAST_UPDATE_SUCCESSFUL, + ATTR_REMOVED, + ATTR_STATUS, + ATTR_UPDATED, ) from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - CONF_RADIUS, - ATTR_UNIT_OF_MEASUREMENT, ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import async_setup_component -from tests.common import async_fire_time_changed import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed from tests.components.geonetnz_quakes import _generate_mock_feed_entry CONFIG = {geonetnz_quakes.DOMAIN: {CONF_RADIUS: 200}} From 202522fbca4b7d668e64dfde95a3b3c0a5fc29f4 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 9 Dec 2019 11:29:36 +0100 Subject: [PATCH 2261/3953] Move imports to top for nsw_fuel_station (#29538) * Move imports to top for nsw_fuel_station * Correct patch path in test_sensor.py * Fix tests by removing the unused argument mock_nsw_fuel --- .../components/nsw_fuel_station/sensor.py | 5 ++--- .../components/nsw_fuel_station/test_sensor.py | 18 +++++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 3c900b46be039e..b4cd7bd161ed42 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -3,11 +3,12 @@ import logging from typing import Optional +from nsw_fuel import FuelCheckClient, FuelCheckError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -52,7 +53,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NSW Fuel Station sensor.""" - from nsw_fuel import FuelCheckClient station_id = config[CONF_STATION_ID] fuel_types = config[CONF_FUEL_TYPES] @@ -97,7 +97,6 @@ def __init__(self, client, station_id: int) -> None: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the internal data using the API client.""" - from nsw_fuel import FuelCheckError if self._reference_data is None: try: diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py index c65c1fe5091cbf..4f1753c05186e3 100644 --- a/tests/components/nsw_fuel_station/test_sensor.py +++ b/tests/components/nsw_fuel_station/test_sensor.py @@ -4,7 +4,7 @@ from homeassistant.components import sensor from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, MockDependency +from tests.common import get_test_home_assistant, assert_setup_component VALID_CONFIG = { "platform": "nsw_fuel_station", @@ -83,9 +83,11 @@ def tearDown(self): """Stop everything that was started.""" self.hass.stop() - @MockDependency("nsw_fuel") - @patch("nsw_fuel.FuelCheckClient", new=FuelCheckClientMock) - def test_setup(self, mock_nsw_fuel): + @patch( + "homeassistant.components.nsw_fuel_station.sensor.FuelCheckClient", + new=FuelCheckClientMock, + ) + def test_setup(self): """Test the setup with custom settings.""" with assert_setup_component(1, sensor.DOMAIN): assert setup_component(self.hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) @@ -96,9 +98,11 @@ def test_setup(self, mock_nsw_fuel): state = self.hass.states.get("sensor.{}".format(entity_id)) assert state is not None - @MockDependency("nsw_fuel") - @patch("nsw_fuel.FuelCheckClient", new=FuelCheckClientMock) - def test_sensor_values(self, mock_nsw_fuel): + @patch( + "homeassistant.components.nsw_fuel_station.sensor.FuelCheckClient", + new=FuelCheckClientMock, + ) + def test_sensor_values(self): """Test retrieval of sensor values.""" assert setup_component(self.hass, sensor.DOMAIN, {"sensor": VALID_CONFIG}) From 0fbb450838aa78185186c550d66b78715ecd1f77 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:30:31 +0100 Subject: [PATCH 2262/3953] Sort imports according to PEP8 for jewish_calendar (#29697) * use isort to sort imports according to PEP8 for jewish_calendar * fix order somehow isort did the wrong thing --- homeassistant/components/jewish_calendar/__init__.py | 5 ++--- tests/components/jewish_calendar/__init__.py | 3 +-- .../components/jewish_calendar/test_binary_sensor.py | 11 +++++------ tests/components/jewish_calendar/test_sensor.py | 10 +++++----- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index bbe0c1d24fd5f9..21c19da7b35f17 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -1,13 +1,12 @@ """The jewish_calendar component.""" import logging -import voluptuous as vol import hdate +import voluptuous as vol from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.helpers.discovery import async_load_platform import homeassistant.helpers.config_validation as cv - +from homeassistant.helpers.discovery import async_load_platform _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index 54589a640cc0cd..592086461d3610 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -1,13 +1,12 @@ """Tests for the jewish_calendar component.""" -from datetime import datetime from collections import namedtuple from contextlib import contextmanager +from datetime import datetime from unittest.mock import patch from homeassistant.components import jewish_calendar import homeassistant.util.dt as dt_util - _LatLng = namedtuple("_LatLng", ["lat", "lng"]) NYC_LATLNG = _LatLng(40.7128, -74.0060) diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index 64745d8929f7e7..0daa7c68993904 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -1,17 +1,16 @@ """The tests for the Jewish calendar binary sensors.""" -from datetime import timedelta -from datetime import datetime as dt +from datetime import datetime as dt, timedelta import pytest -from homeassistant.const import STATE_ON, STATE_OFF -import homeassistant.util.dt as dt_util -from homeassistant.setup import async_setup_component from homeassistant.components import jewish_calendar +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed -from . import alter_time, make_nyc_test_params, make_jerusalem_test_params +from . import alter_time, make_jerusalem_test_params, make_nyc_test_params MELACHA_PARAMS = [ make_nyc_test_params(dt(2018, 9, 1, 16, 0), STATE_ON), diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 60ffdea8c70810..7ed3fdccb6269c 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -1,15 +1,15 @@ """The tests for the Jewish calendar sensors.""" -from datetime import timedelta -from datetime import datetime as dt +from datetime import datetime as dt, timedelta import pytest -import homeassistant.util.dt as dt_util -from homeassistant.setup import async_setup_component from homeassistant.components import jewish_calendar +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + from tests.common import async_fire_time_changed -from . import alter_time, make_nyc_test_params, make_jerusalem_test_params +from . import alter_time, make_jerusalem_test_params, make_nyc_test_params async def test_jewish_calendar_min_config(hass): From 0e71c29e008fbb5ea62c2381637378ca6fe14936 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:34:06 +0100 Subject: [PATCH 2263/3953] Sort imports according to PEP8 for opentherm_gw (#29671) --- homeassistant/components/opentherm_gw/__init__.py | 12 +++++------- .../components/opentherm_gw/binary_sensor.py | 1 - homeassistant/components/opentherm_gw/climate.py | 5 ++--- homeassistant/components/opentherm_gw/config_flow.py | 3 +-- homeassistant/components/opentherm_gw/sensor.py | 1 - tests/components/opentherm_gw/test_config_flow.py | 11 ++++++----- 6 files changed, 14 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 3a1255e3697866..c6cf14bfdcecf8 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,16 +1,16 @@ """Support for OpenTherm Gateway devices.""" import asyncio +from datetime import date, datetime import logging -from datetime import datetime, date import pyotgw import pyotgw.vars as gw_vars import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR from homeassistant.components.climate import DOMAIN as COMP_CLIMATE from homeassistant.components.sensor import DOMAIN as COMP_SENSOR +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_DATE, ATTR_ID, @@ -25,14 +25,13 @@ PRECISION_TENTHS, PRECISION_WHOLE, ) -from homeassistant.helpers.dispatcher import async_dispatcher_send - import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( + ATTR_DHW_OVRD, ATTR_GW_ID, ATTR_LEVEL, - ATTR_DHW_OVRD, CONF_CLIMATE, CONF_FLOOR_TEMP, CONF_PRECISION, @@ -42,15 +41,14 @@ SERVICE_RESET_GATEWAY, SERVICE_SET_CLOCK, SERVICE_SET_CONTROL_SETPOINT, - SERVICE_SET_HOT_WATER_OVRD, SERVICE_SET_GPIO_MODE, + SERVICE_SET_HOT_WATER_OVRD, SERVICE_SET_LED_MODE, SERVICE_SET_MAX_MOD, SERVICE_SET_OAT, SERVICE_SET_SB_TEMP, ) - _LOGGER = logging.getLogger(__name__) CLIMATE_SCHEMA = vol.Schema( diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 39fd78f5fe8f23..eff11554a397e5 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -10,7 +10,6 @@ from . import DOMAIN from .const import BINARY_SENSOR_INFO, DATA_GATEWAYS, DATA_OPENTHERM_GW - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 8c21c6560c1497..2db20662a774d2 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -3,17 +3,17 @@ from pyotgw import vars as gw_vars -from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT +from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, HVAC_MODE_COOL, HVAC_MODE_HEAT, - SUPPORT_TARGET_TEMPERATURE, PRESET_AWAY, PRESET_NONE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -30,7 +30,6 @@ from . import DOMAIN from .const import CONF_FLOOR_TEMP, CONF_PRECISION, DATA_GATEWAYS, DATA_OPENTHERM_GW - _LOGGER = logging.getLogger(__name__) DEFAULT_FLOOR_TEMP = False diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 2d7a65bbd845fa..b52641105e4445 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -1,8 +1,8 @@ """OpenTherm Gateway config flow.""" import asyncio -from serial import SerialException import pyotgw +from serial import SerialException import voluptuous as vol from homeassistant import config_entries @@ -15,7 +15,6 @@ PRECISION_WHOLE, ) from homeassistant.core import callback - import homeassistant.helpers.config_validation as cv from . import DOMAIN diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index cd9ce9fb095a35..3739f77e69dfc5 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -10,7 +10,6 @@ from . import DOMAIN from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW, SENSOR_INFO - _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 89f2783cf71f28..26048543a22749 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -1,18 +1,19 @@ """Test the Opentherm Gateway config flow.""" import asyncio -from serial import SerialException from unittest.mock import patch +from pyotgw import OTGW_ABOUT +from serial import SerialException + from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES from homeassistant.components.opentherm_gw.const import ( - DOMAIN, CONF_FLOOR_TEMP, CONF_PRECISION, + DOMAIN, ) +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES -from pyotgw import OTGW_ABOUT -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_form_user(hass): From c7b2c09a613b757b73a6e3bd9ece16fe1e3b3694 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:42:18 +0100 Subject: [PATCH 2264/3953] Sort imports according to PEP8 for xiaomi_miio (#29677) --- .../components/xiaomi_miio/air_quality.py | 4 +-- homeassistant/components/xiaomi_miio/fan.py | 36 +++++++++---------- homeassistant/components/xiaomi_miio/light.py | 14 ++++---- .../components/xiaomi_miio/switch.py | 4 +-- .../components/xiaomi_miio/vacuum.py | 2 +- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 3824c5b88cd529..f5e7e476ac5cc0 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -3,9 +3,9 @@ import voluptuous as vol from homeassistant.components.air_quality import ( - AirQualityEntity, - PLATFORM_SCHEMA, _LOGGER, + PLATFORM_SCHEMA, + AirQualityEntity, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 91b18aaf3644f4..bf3691eb48613e 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -4,7 +4,6 @@ from functools import partial import logging -import voluptuous as vol from miio import ( # pylint: disable=import-error AirFresh, AirHumidifier, @@ -12,19 +11,19 @@ Device, DeviceException, ) - -from miio.airfresh import ( # pylint: disable=import-error; pylint: disable=import-error +from miio.airfresh import ( # pylint: disable=import-error, import-error LedBrightness as AirfreshLedBrightness, OperationMode as AirfreshOperationMode, ) -from miio.airhumidifier import ( # pylint: disable=import-error; pylint: disable=import-error +from miio.airhumidifier import ( # pylint: disable=import-error, import-error LedBrightness as AirhumidifierLedBrightness, OperationMode as AirhumidifierOperationMode, ) -from miio.airpurifier import ( # pylint: disable=import-error; pylint: disable=import-error +from miio.airpurifier import ( # pylint: disable=import-error, import-error LedBrightness as AirpurifierLedBrightness, OperationMode as AirpurifierOperationMode, ) +import voluptuous as vol from homeassistant.components.fan import PLATFORM_SCHEMA, SUPPORT_SET_SPEED, FanEntity from homeassistant.const import ( @@ -39,26 +38,27 @@ from .const import ( DOMAIN, - SERVICE_SET_BUZZER_ON, + SERVICE_RESET_FILTER, + SERVICE_SET_AUTO_DETECT_OFF, + SERVICE_SET_AUTO_DETECT_ON, SERVICE_SET_BUZZER_OFF, - SERVICE_SET_LED_ON, - SERVICE_SET_LED_OFF, - SERVICE_SET_CHILD_LOCK_ON, + SERVICE_SET_BUZZER_ON, SERVICE_SET_CHILD_LOCK_OFF, - SERVICE_SET_LED_BRIGHTNESS, + SERVICE_SET_CHILD_LOCK_ON, + SERVICE_SET_DRY_OFF, + SERVICE_SET_DRY_ON, + SERVICE_SET_EXTRA_FEATURES, SERVICE_SET_FAVORITE_LEVEL, - SERVICE_SET_AUTO_DETECT_ON, - SERVICE_SET_AUTO_DETECT_OFF, - SERVICE_SET_LEARN_MODE_ON, SERVICE_SET_LEARN_MODE_OFF, - SERVICE_SET_VOLUME, - SERVICE_RESET_FILTER, - SERVICE_SET_EXTRA_FEATURES, + SERVICE_SET_LEARN_MODE_ON, + SERVICE_SET_LED_BRIGHTNESS, + SERVICE_SET_LED_OFF, + SERVICE_SET_LED_ON, SERVICE_SET_TARGET_HUMIDITY, - SERVICE_SET_DRY_ON, - SERVICE_SET_DRY_OFF, + SERVICE_SET_VOLUME, ) + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Miio Device" diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 2343a6787c29c2..5a7b743b3622ff 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -34,14 +34,14 @@ from .const import ( DOMAIN, - SERVICE_SET_SCENE, - SERVICE_SET_DELAYED_TURN_OFF, - SERVICE_REMINDER_ON, - SERVICE_REMINDER_OFF, - SERVICE_NIGHT_LIGHT_MODE_ON, - SERVICE_NIGHT_LIGHT_MODE_OFF, - SERVICE_EYECARE_MODE_ON, SERVICE_EYECARE_MODE_OFF, + SERVICE_EYECARE_MODE_ON, + SERVICE_NIGHT_LIGHT_MODE_OFF, + SERVICE_NIGHT_LIGHT_MODE_ON, + SERVICE_REMINDER_OFF, + SERVICE_REMINDER_ON, + SERVICE_SET_DELAYED_TURN_OFF, + SERVICE_SET_SCENE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index f9a06924b5c334..63229b851d06b9 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -26,10 +26,10 @@ from .const import ( DOMAIN, - SERVICE_SET_WIFI_LED_ON, - SERVICE_SET_WIFI_LED_OFF, SERVICE_SET_POWER_MODE, SERVICE_SET_POWER_PRICE, + SERVICE_SET_WIFI_LED_OFF, + SERVICE_SET_WIFI_LED_ON, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index f1845f534bb1ad..bc703c769a556c 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -39,11 +39,11 @@ from .const import ( DOMAIN, + SERVICE_CLEAN_ZONE, SERVICE_MOVE_REMOTE_CONTROL, SERVICE_MOVE_REMOTE_CONTROL_STEP, SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL, - SERVICE_CLEAN_ZONE, ) _LOGGER = logging.getLogger(__name__) From 642655b6d7cd9114cbb86ebb4c818bd6108d5ac6 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:43:00 +0100 Subject: [PATCH 2265/3953] Sort imports according to PEP8 for met (#29699) --- homeassistant/components/met/__init__.py | 1 + homeassistant/components/met/config_flow.py | 2 +- homeassistant/components/met/weather.py | 4 ++-- tests/components/met/test_config_flow.py | 6 +++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 305038c3e6a622..7ef3c5f8796328 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -1,5 +1,6 @@ """The met component.""" from homeassistant.core import Config, HomeAssistant + from .config_flow import MetFlowHandler # noqa: F401 from .const import DOMAIN # noqa: F401 diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index c7ff4973c7d2aa..759f7f6fc89994 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -6,7 +6,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, HOME_LOCATION_NAME, CONF_TRACK_HOME +from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME @callback diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 2f9ddc5a67c78f..d99573a985e94e 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -5,16 +5,16 @@ import metno import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity from homeassistant.const import ( CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - TEMP_CELSIUS, EVENT_CORE_CONFIG_UPDATE, + TEMP_CELSIUS, ) +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.event import async_call_later diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 32f3be676e0514..73c6c81981755e 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for Met.no config flow.""" from unittest.mock import Mock, patch -from tests.common import MockConfigEntry, mock_coro - -from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.components.met import config_flow +from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import MockConfigEntry, mock_coro async def test_show_config_form(): From a78fe25871f90bc988ac89e5a8a0653f5f45182f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:43:46 +0100 Subject: [PATCH 2266/3953] Sort imports according to PEP8 for locative (#29698) --- homeassistant/components/locative/__init__.py | 10 +++++----- homeassistant/components/locative/config_flow.py | 2 +- homeassistant/components/locative/device_tracker.py | 2 +- tests/components/locative/test_init.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index ed8bcb6e7e5cfb..ea36aa9f7fbc99 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -2,21 +2,21 @@ import logging from typing import Dict -import voluptuous as vol from aiohttp import web +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, + ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, - STATE_NOT_HOME, CONF_WEBHOOK_ID, - ATTR_ID, HTTP_OK, + HTTP_UNPROCESSABLE_ENTITY, + STATE_NOT_HOME, ) from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/locative/config_flow.py b/homeassistant/components/locative/config_flow.py index b4fb43d5e4e56a..a1ac8263416b47 100644 --- a/homeassistant/components/locative/config_flow.py +++ b/homeassistant/components/locative/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Locative.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index c92847930a0ee2..ef247954171764 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -1,9 +1,9 @@ """Support for the Locative platform.""" import logging -from homeassistant.core import callback from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN as LT_DOMAIN, TRACKER_UPDATE diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 1abbf25d4334c6..009a3d469c56a3 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -1,5 +1,5 @@ """The tests the for Locative device tracker platform.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest From fa6b75f2c1de8cf66027d9189ffbc8b0aac4595f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:45:11 +0100 Subject: [PATCH 2267/3953] Sort imports according to PEP8 for file (#29694) --- homeassistant/components/file/notify.py | 7 +++---- homeassistant/components/file/sensor.py | 6 +++--- tests/components/file/test_notify.py | 2 +- tests/components/file/test_sensor.py | 2 +- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index b190bf5d12187a..4cd83e64a83b5d 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -4,16 +4,15 @@ import voluptuous as vol -from homeassistant.const import CONF_FILENAME -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util - from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_FILENAME +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util CONF_TIMESTAMP = "timestamp" diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 60f04b18f2469a..96ae885ca77fc0 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -1,12 +1,12 @@ """Support for sensor value(s) stored in local files.""" -import os import logging +import os import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index 16f4c72658ea7b..52524d5b189968 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -3,9 +3,9 @@ import unittest from unittest.mock import call, mock_open, patch -from homeassistant.setup import setup_component import homeassistant.components.notify as notify from homeassistant.components.notify import ATTR_TITLE_DEFAULT +from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index 912f11aab3be2e..3afdd8284fc496 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -6,8 +6,8 @@ # https://bugs.python.org/issue23004 from mock_open import MockOpen -from homeassistant.setup import setup_component from homeassistant.const import STATE_UNKNOWN +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_registry From 2511f5edb40880286ca20ff95cd1b4e664d70271 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:46:08 +0100 Subject: [PATCH 2268/3953] Sort imports according to PEP8 for ifttt (#29696) --- homeassistant/components/ifttt/__init__.py | 1 + homeassistant/components/ifttt/alarm_control_panel.py | 4 ++-- homeassistant/components/ifttt/config_flow.py | 2 +- tests/components/ifttt/test_init.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 362b01bb5d80b1..3011f5a2a0a48c 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -9,6 +9,7 @@ from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index 9c9ec88ccc7184..2c281e58c4885d 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -5,11 +5,11 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - AlarmControlPanel, FORMAT_NUMBER, FORMAT_TEXT, + PLATFORM_SCHEMA, + AlarmControlPanel, ) -from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, diff --git a/homeassistant/components/ifttt/config_flow.py b/homeassistant/components/ifttt/config_flow.py index ae9be6b698ca14..dc28f6bbaa2a4c 100644 --- a/homeassistant/components/ifttt/config_flow.py +++ b/homeassistant/components/ifttt/config_flow.py @@ -1,7 +1,7 @@ """Config flow for IFTTT.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index a71e2921888084..74d12ba44f4707 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import patch from homeassistant import data_entry_flow -from homeassistant.core import callback from homeassistant.components import ifttt +from homeassistant.core import callback async def test_config_flow_registers_webhook(hass, aiohttp_client): From 2261bb60e09eec318017283699402d7305d8f6ee Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:46:49 +0100 Subject: [PATCH 2269/3953] Sort imports according to PEP8 for geofency (#29695) --- homeassistant/components/geofency/__init__.py | 2 +- homeassistant/components/geofency/config_flow.py | 2 +- homeassistant/components/geofency/device_tracker.py | 6 +++--- tests/components/geofency/test_init.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 9d8e0b29f5d191..9afc9a8bfacbaa 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -18,8 +18,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import slugify -from .const import DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geofency/config_flow.py b/homeassistant/components/geofency/config_flow.py index 1a87502df2a211..2d8bce86d741dc 100644 --- a/homeassistant/components/geofency/config_flow.py +++ b/homeassistant/components/geofency/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Geofency.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 09e9d46ce6d63d..49bd70192ef04c 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,13 +1,13 @@ """Support for the Geofency device tracker platform.""" import logging -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE -from homeassistant.core import callback from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import callback +from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers import device_registry from . import DOMAIN as GF_DOMAIN, TRACKER_UPDATE diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 6254fd4a5045c9..319a79966fdbe7 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -1,6 +1,6 @@ """The tests for the Geofency device tracker platform.""" # pylint: disable=redefined-outer-name -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest From 38657c0055fbab6f1b4ce285cf093a7fa678429d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:47:35 +0100 Subject: [PATCH 2270/3953] Sort imports according to PEP8 for counter (#29692) --- homeassistant/components/counter/__init__.py | 3 +-- homeassistant/components/counter/reproduce_state.py | 2 +- tests/components/counter/common.py | 2 +- tests/components/counter/test_init.py | 1 + 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index c2f61d0c1b43da..98329bc417a359 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -3,8 +3,7 @@ import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME, CONF_MAXIMUM, CONF_MINIMUM - +from homeassistant.const import CONF_ICON, CONF_MAXIMUM, CONF_MINIMUM, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py index ac5045d68e762e..b37fcea719e175 100644 --- a/homeassistant/components/counter/reproduce_state.py +++ b/homeassistant/components/counter/reproduce_state.py @@ -12,9 +12,9 @@ ATTR_MAXIMUM, ATTR_MINIMUM, ATTR_STEP, - VALUE, DOMAIN, SERVICE_CONFIGURE, + VALUE, ) _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/counter/common.py b/tests/components/counter/common.py index 0f735e52f9f929..5f47e4faa77831 100644 --- a/tests/components/counter/common.py +++ b/tests/components/counter/common.py @@ -3,13 +3,13 @@ All containing methods are legacy helpers that should not be used by new components. Instead call the service directly. """ -from homeassistant.const import ATTR_ENTITY_ID from homeassistant.components.counter import ( DOMAIN, SERVICE_DECREMENT, SERVICE_INCREMENT, SERVICE_RESET, ) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback from homeassistant.loader import bind_hass diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 8ce90e164b69e0..35512129aedccf 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -14,6 +14,7 @@ from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import Context, CoreState, State from homeassistant.setup import async_setup_component + from tests.common import mock_restore_cache from tests.components.counter.common import ( async_decrement, From 9c1236b6de49a11e8c9d35c91b251c4b44183e5d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:48:22 +0100 Subject: [PATCH 2271/3953] Sort imports according to PEP8 for ffmpeg (#29693) --- homeassistant/components/ffmpeg/__init__.py | 8 ++++---- homeassistant/components/ffmpeg/camera.py | 6 +++--- tests/components/ffmpeg/test_init.py | 4 ++-- tests/components/ffmpeg/test_sensor.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 673a34230fc2f6..bc402b46fb21a8 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -2,20 +2,20 @@ import logging import re -import voluptuous as vol from haffmpeg.tools import FFVersion +import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity DOMAIN = "ffmpeg" diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 0f5001769339d8..db3eb5621ff7bc 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -2,11 +2,11 @@ import asyncio import logging -import voluptuous as vol from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import ImageFrame, IMAGE_JPEG +from haffmpeg.tools import IMAGE_JPEG, ImageFrame +import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_STREAM +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream import homeassistant.helpers.config_validation as cv diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index e0b68cd61b15d8..1b21f07cd3651f 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -11,9 +11,9 @@ ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.setup import async_setup_component, setup_component -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant @callback diff --git a/tests/components/ffmpeg/test_sensor.py b/tests/components/ffmpeg/test_sensor.py index 175da3a1ab0dcb..5a89daa624c196 100644 --- a/tests/components/ffmpeg/test_sensor.py +++ b/tests/components/ffmpeg/test_sensor.py @@ -3,7 +3,7 @@ from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, mock_coro +from tests.common import assert_setup_component, get_test_home_assistant, mock_coro class TestFFmpegNoiseSetup: From bfa58f671a825d2175ecc47fa423af35ba974337 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:48:52 +0100 Subject: [PATCH 2272/3953] use isort to sort imports according to PEP8 for broadlink (#29690) --- homeassistant/components/broadlink/__init__.py | 2 +- homeassistant/components/broadlink/sensor.py | 9 ++++----- homeassistant/components/broadlink/switch.py | 3 +-- tests/components/broadlink/test_init.py | 6 +++--- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 521cd68780c6ef..3f9b5cd4597c19 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -2,11 +2,11 @@ import asyncio from base64 import b64decode, b64encode from binascii import unhexlify +from datetime import timedelta import logging import re import socket -from datetime import timedelta import voluptuous as vol from homeassistant.const import CONF_HOST diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 6374f35c503e16..9f3087335c892e 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -1,23 +1,22 @@ """Support for the Broadlink RM2 Pro (only temperature) and A1 devices.""" import binascii -import logging from datetime import timedelta +import logging import broadlink - import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, - TEMP_CELSIUS, - CONF_TIMEOUT, CONF_SCAN_INTERVAL, + CONF_TIMEOUT, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index bfb6dc4f42e370..78738870aaa32f 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -5,7 +5,6 @@ import socket import broadlink - import voluptuous as vol from homeassistant.components.switch import ( @@ -25,8 +24,8 @@ STATE_ON, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle, slugify from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import Throttle, slugify from . import async_setup_service, data_packet diff --git a/tests/components/broadlink/test_init.py b/tests/components/broadlink/test_init.py index c15ef12125f621..d4e3c993cd027c 100644 --- a/tests/components/broadlink/test_init.py +++ b/tests/components/broadlink/test_init.py @@ -1,13 +1,13 @@ """The tests for the broadlink component.""" -from datetime import timedelta from base64 import b64decode -from unittest.mock import MagicMock, patch, call +from datetime import timedelta +from unittest.mock import MagicMock, call, patch import pytest -from homeassistant.util.dt import utcnow from homeassistant.components.broadlink import async_setup_service, data_packet from homeassistant.components.broadlink.const import DOMAIN, SERVICE_LEARN, SERVICE_SEND +from homeassistant.util.dt import utcnow DUMMY_IR_PACKET = ( "JgBGAJKVETkRORA6ERQRFBEUERQRFBE5ETkQOhAVEBUQFREUEBUQ" From 0b4ca9ecacacc8e2710bc2bfc6a0a7f7455c94a2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:49:35 +0100 Subject: [PATCH 2273/3953] Sort imports according to PEP8 for ambiclimate (#29689) --- homeassistant/components/ambiclimate/__init__.py | 2 +- homeassistant/components/ambiclimate/climate.py | 5 +++-- homeassistant/components/ambiclimate/config_flow.py | 3 ++- tests/components/ambiclimate/test_config_flow.py | 6 ++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ambiclimate/__init__.py b/homeassistant/components/ambiclimate/__init__.py index 962c8c8a82d86e..e15f6dea2ec449 100644 --- a/homeassistant/components/ambiclimate/__init__.py +++ b/homeassistant/components/ambiclimate/__init__.py @@ -4,10 +4,10 @@ import voluptuous as vol from homeassistant.helpers import config_validation as cv + from . import config_flow from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN - _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index bb3e5ab2b25d85..a8ed166903e6b0 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -7,13 +7,14 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, - HVAC_MODE_OFF, HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession + from .const import ( ATTR_VALUE, CONF_CLIENT_ID, diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 99563dcb97de1a..4996a458a1f348 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -7,14 +7,15 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession + from .const import ( AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN, - STORAGE_VERSION, STORAGE_KEY, + STORAGE_VERSION, ) DATA_AMBICLIMATE_IMPL = "ambiclimate_flow_implementation" diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index c0940fcc3549ed..acf3717b89809d 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -1,11 +1,13 @@ """Tests for the Ambiclimate config flow.""" -import ambiclimate from unittest.mock import Mock, patch +import ambiclimate + +from homeassistant import data_entry_flow from homeassistant.components.ambiclimate import config_flow from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp -from homeassistant import data_entry_flow + from tests.common import mock_coro From 4c5c34919d535ef3e2f21dc6a668c4a270ee1758 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:50:48 +0100 Subject: [PATCH 2274/3953] Sort imports according to PEP8 for camera (#29691) --- homeassistant/components/camera/__init__.py | 53 ++++++++++----------- homeassistant/components/camera/prefs.py | 1 - tests/components/camera/common.py | 4 +- tests/components/camera/test_init.py | 14 +++--- 4 files changed, 35 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 58b6db139f5291..b3d5935784fdd2 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -4,55 +4,54 @@ import collections from contextlib import suppress from datetime import timedelta -import logging import hashlib +import logging from random import SystemRandom -import attr from aiohttp import web import async_timeout +import attr import voluptuous as vol -from homeassistant.core import callback -from homeassistant.const import ( - ATTR_ENTITY_ID, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - CONF_FILENAME, -) -from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) -from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED +from homeassistant.components import websocket_api +from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - SERVICE_PLAY_MEDIA, DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, ) from homeassistant.components.stream import request_stream from homeassistant.components.stream.const import ( - OUTPUT_FORMATS, - FORMAT_CONTENT_TYPE, - CONF_STREAM_SOURCE, - CONF_LOOKBACK, CONF_DURATION, - SERVICE_RECORD, + CONF_LOOKBACK, + CONF_STREAM_SOURCE, DOMAIN as DOMAIN_STREAM, + FORMAT_CONTENT_TYPE, + OUTPUT_FORMATS, + SERVICE_RECORD, ) -from homeassistant.components import websocket_api +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_FILENAME, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass from homeassistant.setup import async_when_setup -from .const import DOMAIN, DATA_CAMERA_PREFS +from .const import DATA_CAMERA_PREFS, DOMAIN from .prefs import CameraPreferences - # mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index d83e0b55c968fd..ae182c62dc6382 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,7 +1,6 @@ """Preference management for camera component.""" from .const import DOMAIN, PREF_PRELOAD_STREAM - # mypy: allow-untyped-defs, no-check-untyped-defs STORAGE_KEY = DOMAIN diff --git a/tests/components/camera/common.py b/tests/components/camera/common.py index 971d723d2d5411..8c05295b5d1de4 100644 --- a/tests/components/camera/common.py +++ b/tests/components/camera/common.py @@ -9,15 +9,15 @@ SERVICE_SNAPSHOT, ) from homeassistant.components.camera.const import ( - DOMAIN, DATA_CAMERA_PREFS, + DOMAIN, PREF_PRELOAD_STREAM, ) from homeassistant.const import ( ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, SERVICE_TURN_OFF, SERVICE_TURN_ON, - ENTITY_MATCH_ALL, ) from homeassistant.core import callback from homeassistant.loader import bind_hass diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 17bcaadb92b6ab..89a19d0458a3f7 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -2,26 +2,26 @@ import asyncio import base64 import io -from unittest.mock import patch, mock_open, PropertyMock +from unittest.mock import PropertyMock, mock_open, patch import pytest -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.components import camera, http +from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM +from homeassistant.components.camera.prefs import CameraEntityPreferences +from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, EVENT_HOMEASSISTANT_START, ) -from homeassistant.components import camera, http -from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM -from homeassistant.components.camera.prefs import CameraEntityPreferences -from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component, setup_component from tests.common import ( + assert_setup_component, get_test_home_assistant, get_test_instance_port, - assert_setup_component, mock_coro, ) from tests.components.camera import common From c54135486e771ed6f7e8703e39a575b6143c9ac1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:50:59 +0100 Subject: [PATCH 2275/3953] use isort to sort imports according to PEP8 for netatmo (#29682) --- homeassistant/components/netatmo/__init__.py | 8 ++++---- .../components/netatmo/binary_sensor.py | 2 +- homeassistant/components/netatmo/camera.py | 10 +++++----- homeassistant/components/netatmo/climate.py | 18 +++++++++--------- homeassistant/components/netatmo/sensor.py | 11 ++++++----- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 9edeb0937a138e..6becedde61149c 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,6 +1,6 @@ """Support for the Netatmo devices.""" -import logging from datetime import timedelta +import logging from urllib.error import HTTPError import pyatmo @@ -8,17 +8,17 @@ from homeassistant.const import ( CONF_API_KEY, - CONF_PASSWORD, - CONF_USERNAME, CONF_DISCOVERY, + CONF_PASSWORD, CONF_URL, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -from .const import DOMAIN, DATA_NETATMO_AUTH +from .const import DATA_NETATMO_AUTH, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 1a40d3952e9456..06097ed852d9fa 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -8,8 +8,8 @@ from homeassistant.const import CONF_TIMEOUT from homeassistant.helpers import config_validation as cv -from .const import DATA_NETATMO_AUTH from . import CameraData +from .const import DATA_NETATMO_AUTH _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index f3bf6a6784c87d..1713265a014c9a 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -6,20 +6,20 @@ import voluptuous as vol from homeassistant.components.camera import ( + CAMERA_SERVICE_SCHEMA, PLATFORM_SCHEMA, - Camera, SUPPORT_STREAM, - CAMERA_SERVICE_SCHEMA, + Camera, ) -from homeassistant.const import CONF_VERIFY_SSL, STATE_ON, STATE_OFF +from homeassistant.const import CONF_VERIFY_SSL, STATE_OFF, STATE_ON from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) -from .const import DATA_NETATMO_AUTH, DOMAIN from . import CameraData +from .const import DATA_NETATMO_AUTH, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 8ba13a03889d2c..9e320c303c89b9 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -1,34 +1,34 @@ """Support for Netatmo Smart thermostats.""" from datetime import timedelta import logging -from typing import Optional, List +from typing import List, Optional import pyatmo import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + DEFAULT_MIN_TEMP, HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, - DEFAULT_MIN_TEMP, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( - TEMP_CELSIUS, + ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, CONF_NAME, PRECISION_HALVES, STATE_OFF, - ATTR_BATTERY_LEVEL, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from .const import DATA_NETATMO_AUTH diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 1ae076c6560775..d4d624061f5df5 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,7 +1,7 @@ """Support for the Netatmo Weather Service.""" +from datetime import timedelta import logging import threading -from datetime import timedelta from time import time import pyatmo @@ -9,19 +9,20 @@ import urllib3 import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_MODE, - TEMP_CELSIUS, + CONF_NAME, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import call_later from homeassistant.util import Throttle + from .const import DATA_NETATMO_AUTH, DOMAIN _LOGGER = logging.getLogger(__name__) From 776d8cfdc9886968d038aae9f42811b642de1d48 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:51:36 +0100 Subject: [PATCH 2276/3953] Sort imports according to PEP8 for specific_devices (#29687) --- .../specific_devices/test_aqara_gateway.py | 3 ++- .../homekit_controller/specific_devices/test_ecobee3.py | 7 +++---- .../specific_devices/test_hue_bridge.py | 2 +- .../specific_devices/test_koogeek_ls1.py | 9 +++++---- .../specific_devices/test_lennox_e30.py | 3 ++- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py index 6e41a88b2997dc..292c416968800a 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py @@ -5,10 +5,11 @@ """ from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_COLOR + from tests.components.homekit_controller.common import ( + Helper, setup_accessories_from_file, setup_test_accessories, - Helper, ) diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 8531712a5d3973..bb7695840f0ec4 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -8,19 +8,18 @@ from homekit import AccessoryDisconnectedError -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, ) - +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from tests.components.homekit_controller.common import ( FakePairing, + Helper, device_config_changed, setup_accessories_from_file, setup_test_accessories, - Helper, time_changed, ) diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index f3e4baa4b1f3d1..0b6ebc00eba8ce 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -1,9 +1,9 @@ """Tests for handling accessories on a Hue bridge via HomeKit.""" from tests.components.homekit_controller.common import ( + Helper, setup_accessories_from_file, setup_test_accessories, - Helper, ) diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py index 89c5bb4de3ceeb..52339bb6635ad5 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -3,17 +3,18 @@ from datetime import timedelta from unittest import mock +from homekit.exceptions import AccessoryDisconnectedError, EncryptionError import pytest -from homekit.exceptions import AccessoryDisconnectedError, EncryptionError -import homeassistant.util.dt as dt_util from homeassistant.components.light import SUPPORT_BRIGHTNESS, SUPPORT_COLOR +import homeassistant.util.dt as dt_util + from tests.common import async_fire_time_changed from tests.components.homekit_controller.common import ( - setup_accessories_from_file, - setup_test_accessories, FakePairing, Helper, + setup_accessories_from_file, + setup_test_accessories, ) LIGHT_ON = ("lightbulb", "on") diff --git a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py index ef0d35166db931..3209139ae1e631 100644 --- a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py +++ b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py @@ -5,10 +5,11 @@ """ from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE + from tests.components.homekit_controller.common import ( + Helper, setup_accessories_from_file, setup_test_accessories, - Helper, ) From b6cd6135c2a217800fe1fb9a9efc537d7ecd56c9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:53:51 +0100 Subject: [PATCH 2277/3953] Sort imports according to PEP8 for wemo (#29685) --- homeassistant/components/wemo/__init__.py | 5 ++--- homeassistant/components/wemo/config_flow.py | 2 +- homeassistant/components/wemo/fan.py | 14 +++++++------- homeassistant/components/wemo/light.py | 6 +++--- homeassistant/components/wemo/switch.py | 4 ++-- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index fe63f10aebae29..9b1c4cd465f293 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -7,10 +7,9 @@ from homeassistant import config_entries from homeassistant.components.discovery import SERVICE_WEMO -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery - from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import config_validation as cv, discovery + from .const import DOMAIN # Mapping from Wemo model_name to component. diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py index 21c911a66ce944..9ad7dda10ba59c 100644 --- a/homeassistant/components/wemo/config_flow.py +++ b/homeassistant/components/wemo/config_flow.py @@ -2,8 +2,8 @@ import pywemo -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow from . import DOMAIN diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 4a8be4fba81f62..5974a9eae8ce34 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -1,24 +1,24 @@ """Support for WeMo humidifier.""" import asyncio -import logging from datetime import timedelta +import logging import async_timeout from pywemo import discovery import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( - SUPPORT_SET_SPEED, - FanEntity, - SPEED_OFF, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, + SPEED_OFF, + SUPPORT_SET_SPEED, + FanEntity, ) -from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from . import SUBSCRIPTION_REGISTRY from .const import DOMAIN, SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index dab96eb8c948cd..37113a09bd1cb4 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -1,7 +1,7 @@ """Support for Belkin WeMo lights.""" import asyncio -import logging from datetime import timedelta +import logging import async_timeout from pywemo import discovery @@ -9,15 +9,15 @@ from homeassistant import util from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, + Light, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.util.color as color_util diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 1c0606b489dbb7..432a0ddf2cc428 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -1,16 +1,16 @@ """Support for WeMo switches.""" import asyncio -import logging from datetime import datetime, timedelta +import logging import async_timeout from pywemo import discovery import requests from homeassistant.components.switch import SwitchDevice +from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN from homeassistant.exceptions import PlatformNotReady from homeassistant.util import convert -from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN from . import SUBSCRIPTION_REGISTRY from .const import DOMAIN From e4e4f78eb0aec339a030d65fbbe59ae346a82ba8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:54:56 +0100 Subject: [PATCH 2278/3953] Sort imports according to PEP8 for utility_meter (#29710) --- .../components/utility_meter/__init__.py | 25 ++++++++------- .../components/utility_meter/sensor.py | 31 ++++++++++--------- tests/components/utility_meter/test_init.py | 13 ++++---- tests/components/utility_meter/test_sensor.py | 18 +++++------ 4 files changed, 44 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 04e472a7828619..ef9d9b1ddcee90 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -1,33 +1,34 @@ """Support for tracking consumption over given periods of time.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_NAME -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN + from .const import ( - DOMAIN, - SIGNAL_RESET_METER, - METER_TYPES, - CONF_METER_TYPE, - CONF_METER_OFFSET, + ATTR_TARIFF, + CONF_METER, CONF_METER_NET_CONSUMPTION, + CONF_METER_OFFSET, + CONF_METER_TYPE, CONF_SOURCE_SENSOR, - CONF_TARIFF_ENTITY, CONF_TARIFF, + CONF_TARIFF_ENTITY, CONF_TARIFFS, - CONF_METER, DATA_UTILITY, + DOMAIN, + METER_TYPES, SERVICE_RESET, - SERVICE_SELECT_TARIFF, SERVICE_SELECT_NEXT_TARIFF, - ATTR_TARIFF, + SERVICE_SELECT_TARIFF, + SIGNAL_RESET_METER, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 41fe2cbcc0a078..3dab92b89f8028 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -1,39 +1,40 @@ """Utility meter from sensors providing raw data.""" -import logging from datetime import date, timedelta from decimal import Decimal, DecimalException +import logging -import homeassistant.util.dt as dt_util from homeassistant.const import ( - CONF_NAME, ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( async_track_state_change, async_track_time_change, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.dt as dt_util + from .const import ( + CONF_METER, + CONF_METER_NET_CONSUMPTION, + CONF_METER_OFFSET, + CONF_METER_TYPE, + CONF_SOURCE_SENSOR, + CONF_TARIFF, + CONF_TARIFF_ENTITY, + DAILY, DATA_UTILITY, - SIGNAL_RESET_METER, HOURLY, - DAILY, - WEEKLY, MONTHLY, QUARTERLY, + SIGNAL_RESET_METER, + WEEKLY, YEARLY, - CONF_SOURCE_SENSOR, - CONF_METER_TYPE, - CONF_METER_OFFSET, - CONF_METER_NET_CONSUMPTION, - CONF_TARIFF, - CONF_TARIFF_ENTITY, - CONF_METER, ) _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index 784a3647b8dc0f..719ea9445cc58e 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -1,20 +1,19 @@ """The tests for the utility_meter component.""" -import logging - from datetime import timedelta +import logging from unittest.mock import patch -from homeassistant.const import EVENT_HOMEASSISTANT_START, ATTR_ENTITY_ID +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( + ATTR_TARIFF, + DOMAIN, SERVICE_RESET, - SERVICE_SELECT_TARIFF, SERVICE_SELECT_NEXT_TARIFF, - ATTR_TARIFF, + SERVICE_SELECT_TARIFF, ) +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from homeassistant.components.utility_meter.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 7bf10875b778b4..fcfe97804e418d 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1,20 +1,20 @@ """The tests for the utility_meter sensor platform.""" -import logging - +from contextlib import contextmanager from datetime import timedelta +import logging from unittest.mock import patch -from contextlib import contextmanager -from tests.common import async_fire_time_changed -from homeassistant.const import EVENT_HOMEASSISTANT_START, ATTR_ENTITY_ID -from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( + ATTR_TARIFF, DOMAIN, SERVICE_SELECT_TARIFF, - ATTR_TARIFF, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) From b54c8641b4de7916d1a23b1566a7abcf6a52cea7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:56:02 +0100 Subject: [PATCH 2279/3953] Sort imports according to PEP8 for vesync (#29684) --- homeassistant/components/vesync/__init__.py | 15 +++++++++------ homeassistant/components/vesync/common.py | 2 ++ homeassistant/components/vesync/config_flow.py | 9 ++++++--- homeassistant/components/vesync/switch.py | 6 ++++-- tests/components/vesync/test_config_flow.py | 6 ++++-- 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 9ed71dbc5ee5aa..0f905b8d7ef775 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -1,20 +1,23 @@ """Etekcity VeSync integration.""" import logging -import voluptuous as vol + from pyvesync import VeSync -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.config_entries import SOURCE_IMPORT + from .common import async_process_devices from .config_flow import configured_instances from .const import ( DOMAIN, - VS_DISPATCHERS, - VS_DISCOVERY, - VS_SWITCHES, SERVICE_UPDATE_DEVS, + VS_DISCOVERY, + VS_DISPATCHERS, VS_MANAGER, + VS_SWITCHES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index 361b3913283744..d2ffa5281e97f0 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -1,6 +1,8 @@ """Common utilities for VeSync Component.""" import logging + from homeassistant.helpers.entity import ToggleEntity + from .const import VS_SWITCHES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/config_flow.py b/homeassistant/components/vesync/config_flow.py index 168a3568392ade..8b0e8ae6781852 100644 --- a/homeassistant/components/vesync/config_flow.py +++ b/homeassistant/components/vesync/config_flow.py @@ -1,11 +1,14 @@ """Config flow utilities.""" -import logging from collections import OrderedDict -import voluptuous as vol +import logging + from pyvesync import VeSync +import voluptuous as vol + from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index 5ca76a772546dd..6ab5c0c436833e 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -1,10 +1,12 @@ """Support for Etekcity VeSync switches.""" import logging -from homeassistant.core import callback + from homeassistant.components.switch import SwitchDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import VS_DISCOVERY, VS_DISPATCHERS, VS_SWITCHES, DOMAIN + from .common import VeSyncDevice +from .const import DOMAIN, VS_DISCOVERY, VS_DISPATCHERS, VS_SWITCHES _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index 205ce80b4b1804..39b847effc5645 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -1,8 +1,10 @@ """Test for vesync config flow.""" from unittest.mock import patch + from homeassistant import data_entry_flow -from homeassistant.components.vesync import config_flow, DOMAIN -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.components.vesync import DOMAIN, config_flow +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + from tests.common import MockConfigEntry From 9cf3ff319e34c04839929d2f639293f13119e643 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:56:51 +0100 Subject: [PATCH 2280/3953] Sort imports according to PEP8 for iaqualink (#29681) --- homeassistant/components/iaqualink/__init__.py | 6 ++---- homeassistant/components/iaqualink/binary_sensor.py | 2 +- homeassistant/components/iaqualink/climate.py | 2 +- homeassistant/components/iaqualink/config_flow.py | 3 +-- tests/components/iaqualink/test_config_flow.py | 1 + 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index fc1eb3b248ae9e..16c8deac72e7bd 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -5,8 +5,6 @@ from typing import Any, Dict import aiohttp.client_exceptions -import voluptuous as vol - from iaqualink import ( AqualinkBinarySensor, AqualinkClient, @@ -17,6 +15,7 @@ AqualinkThermostat, AqualinkToggle, ) +import voluptuous as vol from homeassistant import config_entries from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -29,18 +28,17 @@ from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import DOMAIN, UPDATE_INTERVAL - _LOGGER = logging.getLogger(__name__) ATTR_CONFIG = "config" diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py index 09c9322a58764b..30d419c1bce058 100644 --- a/homeassistant/components/iaqualink/binary_sensor.py +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_COLD, DOMAIN, + BinarySensorDevice, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index f41d17837c2f6c..36f3303774af2a 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -22,7 +22,7 @@ from homeassistant.helpers.typing import HomeAssistantType from . import AqualinkEntity, refresh_system -from .const import DOMAIN as AQUALINK_DOMAIN, CLIMATE_SUPPORTED_MODES +from .const import CLIMATE_SUPPORTED_MODES, DOMAIN as AQUALINK_DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index ec83477d253a04..d577fe448aa01e 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -1,9 +1,8 @@ """Config flow to configure zone component.""" from typing import Optional -import voluptuous as vol - from iaqualink import AqualinkClient, AqualinkLoginException +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/tests/components/iaqualink/test_config_flow.py b/tests/components/iaqualink/test_config_flow.py index 5c4d75ee3c155f..d2fa4633d8076b 100644 --- a/tests/components/iaqualink/test_config_flow.py +++ b/tests/components/iaqualink/test_config_flow.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components.iaqualink import config_flow + from tests.common import MockConfigEntry, mock_coro DATA = {"username": "test@example.com", "password": "pass"} From df85a50f3bc19e17d5ce8ed3d4c1d17287c9e1c8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:57:49 +0100 Subject: [PATCH 2281/3953] Sort imports according to PEP8 for sensor (#29683) --- homeassistant/components/sensor/__init__.py | 1 - homeassistant/components/sensor/device_condition.py | 6 +++--- homeassistant/components/sensor/device_trigger.py | 3 +-- tests/components/sensor/test_device_condition.py | 10 +++++----- tests/components/sensor/test_device_trigger.py | 11 ++++++----- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 53e4b0ffcf717f..83711a07759243 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -21,7 +21,6 @@ ) from homeassistant.helpers.entity_component import EntityComponent - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 259fb5dbab90b9..7417765f9f4b5b 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -1,11 +1,11 @@ """Provides device conditions for sensors.""" from typing import Dict, List + import voluptuous as vol from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.core import HomeAssistant from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -22,16 +22,16 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import ( async_entries_for_device, async_get_registry, ) -from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.typing import ConfigType from . import DOMAIN - # mypy: allow-untyped-defs, no-check-untyped-defs DEVICE_CLASS_NONE = "none" diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 73e55340da9b6a..1af8a5e4ab08d9 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -23,12 +23,11 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ) -from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import 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 DEVICE_CLASS_NONE = "none" diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index f3ff15c3ad9386..bd6a6ce4928191 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -1,20 +1,20 @@ """The test for sensor device automation.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS -from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation +from homeassistant.const import CONF_PLATFORM, STATE_UNKNOWN from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index b7a921fff18034..7bb69388c1d5ed 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -1,23 +1,24 @@ """The test for sensor device automation.""" from datetime import timedelta + import pytest +import homeassistant.components.automation as automation from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS -from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation +from homeassistant.const import CONF_PLATFORM, STATE_UNKNOWN from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, async_fire_time_changed, + async_get_device_automation_capabilities, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, - async_get_device_automation_capabilities, ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES From c7d61279bd6abce2223ab436d2614c98728c7c23 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:58:40 +0100 Subject: [PATCH 2282/3953] Sort imports according to PEP8 for ring (#29680) --- tests/components/ring/common.py | 2 +- tests/components/ring/conftest.py | 5 +++-- tests/components/ring/test_binary_sensor.py | 5 +++-- tests/components/ring/test_init.py | 4 +++- tests/components/ring/test_light.py | 4 +++- tests/components/ring/test_sensor.py | 6 ++++-- tests/components/ring/test_switch.py | 4 +++- 7 files changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index 1228f998618864..e5042a935d633d 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -1,6 +1,6 @@ """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.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.setup import async_setup_component diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index 14a29f78aaef0e..b61840769a2c29 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -1,8 +1,9 @@ """Configuration for Ring tests.""" -import requests_mock +from asynctest import patch import pytest +import requests_mock + from tests.common import load_fixture -from asynctest import patch @pytest.fixture(name="ring_mock") diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index d144a6308638ee..c0b538b8effff6 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,13 +1,14 @@ """The tests for the Ring binary sensor platform.""" import os import unittest + import requests_mock -from homeassistant.components.ring import binary_sensor as ring from homeassistant.components import ring as base_ring +from homeassistant.components.ring import binary_sensor as ring -from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture +from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG class TestRingBinarySensorSetup(unittest.TestCase): diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index 1a0d0dfec1623b..4d3fede89a932c 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -1,9 +1,11 @@ """The tests for the Ring component.""" from copy import deepcopy +from datetime import timedelta import os import unittest + import requests_mock -from datetime import timedelta + from homeassistant import setup import homeassistant.components.ring as ring diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py index e07867c19b24ae..56d39173d63581 100644 --- a/tests/components/ring/test_light.py +++ b/tests/components/ring/test_light.py @@ -1,8 +1,10 @@ """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 +from tests.common import load_fixture + async def test_entity_registry(hass, requests_mock): """Tests that the devices are registed in the entity registry.""" diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 8559f1acc683bb..dd9d36f80a120c 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -1,13 +1,15 @@ """The tests for the Ring sensor platform.""" import os import unittest + import requests_mock -import homeassistant.components.ring.sensor as ring from homeassistant.components import ring as base_ring +import homeassistant.components.ring.sensor as ring from homeassistant.helpers.icon import icon_for_battery_level -from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG + from tests.common import get_test_config_dir, get_test_home_assistant, load_fixture +from tests.components.ring.test_init import ATTRIBUTION, VALID_CONFIG class TestRingSensorSetup(unittest.TestCase): diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py index 864d16466daf11..15f4dd86a3958d 100644 --- a/tests/components/ring/test_switch.py +++ b/tests/components/ring/test_switch.py @@ -1,8 +1,10 @@ """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 +from tests.common import load_fixture + async def test_entity_registry(hass, requests_mock): """Tests that the devices are registed in the entity registry.""" From 3d10bb3647d050f6006f4725685fc5ad8f85ad59 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 11:59:38 +0100 Subject: [PATCH 2283/3953] Sort imports according to PEP8 for http (#29679) --- tests/components/http/__init__.py | 1 - tests/components/http/test_auth.py | 7 ++++--- tests/components/http/test_ban.py | 13 ++++++------- tests/components/http/test_cors.py | 5 ++--- tests/components/http/test_init.py | 5 ++--- tests/components/http/test_real_ip.py | 5 +++-- tests/components/http/test_view.py | 2 +- 7 files changed, 18 insertions(+), 20 deletions(-) diff --git a/tests/components/http/__init__.py b/tests/components/http/__init__.py index db5e1ea5c7a004..e96f4a7fcf2418 100644 --- a/tests/components/http/__init__.py +++ b/tests/components/http/__init__.py @@ -5,7 +5,6 @@ from homeassistant.components.http.const import KEY_REAL_IP - # Relic from the past. Kept here so we can run negative tests. HTTP_HEADER_HA_AUTH = "X-HA-access" diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 499ceab1556fd5..3617690eb3baad 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -3,16 +3,17 @@ from ipaddress import ip_network from unittest.mock import patch -import pytest from aiohttp import BasicAuth, web from aiohttp.web_exceptions import HTTPUnauthorized +import pytest from homeassistant.auth.providers import trusted_networks -from homeassistant.components.http.auth import setup_auth, async_sign_path +from homeassistant.components.http.auth import async_sign_path, setup_auth from homeassistant.components.http.const import KEY_AUTHENTICATED from homeassistant.components.http.real_ip import setup_real_ip from homeassistant.setup import async_setup_component -from . import mock_real_ip, HTTP_HEADER_HA_AUTH + +from . import HTTP_HEADER_HA_AUTH, mock_real_ip API_PASSWORD = "test-password" diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index f50afcef8a83f0..8d9d19b6a12eaf 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -1,29 +1,28 @@ """The tests for the Home Assistant HTTP component.""" # pylint: disable=protected-access from ipaddress import ip_address -from unittest.mock import patch, mock_open, Mock +from unittest.mock import Mock, mock_open, patch from aiohttp import web from aiohttp.web_exceptions import HTTPUnauthorized from aiohttp.web_middlewares import middleware -from homeassistant.components.http import KEY_AUTHENTICATED -from homeassistant.components.http.view import request_handler_factory -from homeassistant.setup import async_setup_component import homeassistant.components.http as http +from homeassistant.components.http import KEY_AUTHENTICATED from homeassistant.components.http.ban import ( - IpBan, IP_BANS_FILE, - setup_bans, KEY_BANNED_IPS, KEY_FAILED_LOGIN_ATTEMPTS, + IpBan, + setup_bans, ) +from homeassistant.components.http.view import request_handler_factory +from homeassistant.setup import async_setup_component from . import mock_real_ip from tests.common import mock_coro - BANNED_IPS = ["200.201.202.203", "100.64.0.2"] diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 1cea900d9719cb..04447191fd5a8a 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -4,8 +4,8 @@ from aiohttp import web from aiohttp.hdrs import ( - ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_HEADERS, + ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AUTHORIZATION, @@ -13,13 +13,12 @@ ) import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.http.cors import setup_cors from homeassistant.components.http.view import HomeAssistantView +from homeassistant.setup import async_setup_component from . import HTTP_HEADER_HA_AUTH - TRUSTED_ORIGIN = "https://home-assistant.io" diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index ad8e3ac10fd638..212ae7499ab0ad 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -3,10 +3,9 @@ import unittest from unittest.mock import patch -from homeassistant.setup import async_setup_component - import homeassistant.components.http as http -from homeassistant.util.ssl import server_context_modern, server_context_intermediate +from homeassistant.setup import async_setup_component +from homeassistant.util.ssl import server_context_intermediate, server_context_modern class TestView(http.HomeAssistantView): diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py index 581624e0c0a072..2cb74df3176790 100644 --- a/tests/components/http/test_real_ip.py +++ b/tests/components/http/test_real_ip.py @@ -1,10 +1,11 @@ """Test real IP middleware.""" +from ipaddress import ip_network + from aiohttp import web from aiohttp.hdrs import X_FORWARDED_FOR -from ipaddress import ip_network -from homeassistant.components.http.real_ip import setup_real_ip from homeassistant.components.http.const import KEY_REAL_IP +from homeassistant.components.http.real_ip import setup_real_ip async def mock_handler(request): diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index 1cbe4c5a030168..414ad4e8cb0e2d 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -2,8 +2,8 @@ from unittest.mock import Mock from aiohttp.web_exceptions import ( - HTTPInternalServerError, HTTPBadRequest, + HTTPInternalServerError, HTTPUnauthorized, ) import pytest From 69b113c539e52fc64f0aa295ca21647a7f5ca29b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:04:40 +0100 Subject: [PATCH 2284/3953] Sort imports according to PEP8 for dyson (#29678) --- tests/components/dyson/test_air_quality.py | 6 +++--- tests/components/dyson/test_climate.py | 3 ++- tests/components/dyson/test_fan.py | 15 ++++++++------- tests/components/dyson/test_init.py | 1 + tests/components/dyson/test_sensor.py | 3 ++- tests/components/dyson/test_vacuum.py | 1 + 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/components/dyson/test_air_quality.py b/tests/components/dyson/test_air_quality.py index d1ef0c73a513f9..ed2fbed34f3d8f 100644 --- a/tests/components/dyson/test_air_quality.py +++ b/tests/components/dyson/test_air_quality.py @@ -6,14 +6,14 @@ from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State -import homeassistant.components.dyson.air_quality as dyson from homeassistant.components import dyson as dyson_parent from homeassistant.components.air_quality import ( - DOMAIN as AIQ_DOMAIN, + ATTR_NO2, ATTR_PM_2_5, ATTR_PM_10, - ATTR_NO2, + DOMAIN as AIQ_DOMAIN, ) +import homeassistant.components.dyson.air_quality as dyson from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index c3786f47507889..dbc477203a1a24 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -9,8 +9,9 @@ from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import climate as dyson -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.setup import async_setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 03fa09110a8686..5f6b124a3d593a 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -4,27 +4,28 @@ from unittest import mock import asynctest -from libpurecool.const import FanSpeed, FanMode, NightMode, Oscillation +from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink from libpurecool.dyson_pure_state import DysonPureCoolState from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State -import homeassistant.components.dyson.fan as dyson from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import DYSON_DEVICES +import homeassistant.components.dyson.fan as dyson from homeassistant.components.fan import ( - DOMAIN, - ATTR_SPEED, ATTR_OSCILLATING, + ATTR_SPEED, + DOMAIN, + SERVICE_OSCILLATE, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, - SERVICE_OSCILLATE, ) -from homeassistant.const import SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/dyson/test_init.py b/tests/components/dyson/test_init.py index fc4ede75e321c3..067f6a360ed6d0 100644 --- a/tests/components/dyson/test_init.py +++ b/tests/components/dyson/test_init.py @@ -3,6 +3,7 @@ from unittest import mock from homeassistant.components import dyson + from tests.common import get_test_home_assistant diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py index 8ea9d1ff5ec008..442ea913b46fe1 100644 --- a/tests/components/dyson/test_sensor.py +++ b/tests/components/dyson/test_sensor.py @@ -8,9 +8,10 @@ from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import sensor as dyson -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_OFF +from homeassistant.const import STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/dyson/test_vacuum.py b/tests/components/dyson/test_vacuum.py index 3e11e929b32480..fc801cbe649efe 100644 --- a/tests/components/dyson/test_vacuum.py +++ b/tests/components/dyson/test_vacuum.py @@ -7,6 +7,7 @@ from homeassistant.components.dyson import vacuum as dyson from homeassistant.components.dyson.vacuum import Dyson360EyeDevice + from tests.common import get_test_home_assistant From ad58e607dfe96e65fe2d18d08bc8ae5d265451e3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:07:10 +0100 Subject: [PATCH 2285/3953] Sort imports according to PEP8 for somfy (#29675) --- homeassistant/components/somfy/__init__.py | 6 +++--- homeassistant/components/somfy/api.py | 2 +- homeassistant/components/somfy/config_flow.py | 1 + homeassistant/components/somfy/cover.py | 7 ++++--- homeassistant/components/somfy/switch.py | 3 ++- tests/components/somfy/test_config_flow.py | 4 ++-- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 1368725777bf5f..365c68393002cf 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -5,15 +5,15 @@ https://home-assistant.io/integrations/somfy/ """ import asyncio -import logging from datetime import timedelta +import logging -import voluptuous as vol from requests import HTTPError +import voluptuous as vol -from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from homeassistant.components.somfy import config_flow from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py index 1cfea8ff7d800f..b2516cb36c4077 100644 --- a/homeassistant/components/somfy/api.py +++ b/homeassistant/components/somfy/api.py @@ -4,7 +4,7 @@ from pymfy.api import somfy_api -from homeassistant import core, config_entries +from homeassistant import config_entries, core from homeassistant.helpers import config_entry_oauth2_flow diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index cb180d4e2470a2..2d143fbd196327 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -3,6 +3,7 @@ from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index 12f12676f9247f..5eabe7ee07aa71 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -1,13 +1,14 @@ """Support for Somfy Covers.""" -from pymfy.api.devices.category import Category from pymfy.api.devices.blind import Blind +from pymfy.api.devices.category import Category from homeassistant.components.cover import ( - CoverDevice, ATTR_POSITION, ATTR_TILT_POSITION, + CoverDevice, ) -from . import DOMAIN, SomfyEntity, DEVICES, API + +from . import API, DEVICES, DOMAIN, SomfyEntity async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py index 58ad2e5905d3ea..bc31d68ec1dc70 100644 --- a/homeassistant/components/somfy/switch.py +++ b/homeassistant/components/somfy/switch.py @@ -3,7 +3,8 @@ from pymfy.api.devices.category import Category from homeassistant.components.switch import SwitchDevice -from . import DOMAIN, SomfyEntity, DEVICES, API + +from . import API, DEVICES, DOMAIN, SomfyEntity async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index d42e7b8e367e4f..f195b640240a19 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -4,8 +4,8 @@ import pytest -from homeassistant import data_entry_flow, setup, config_entries -from homeassistant.components.somfy import config_flow, DOMAIN +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.somfy import DOMAIN, config_flow from homeassistant.helpers import config_entry_oauth2_flow from tests.common import MockConfigEntry From 186799794d39ecab4df5bd92b5c771986305712c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:08:51 +0100 Subject: [PATCH 2286/3953] Sort imports according to PEP8 for nest (#29670) --- homeassistant/components/nest/__init__.py | 8 ++++---- homeassistant/components/nest/camera.py | 4 ++-- homeassistant/components/nest/climate.py | 14 +++++++------- homeassistant/components/nest/config_flow.py | 1 - homeassistant/components/nest/local_auth.py | 3 ++- tests/components/nest/test_config_flow.py | 2 +- tests/components/nest/test_local_auth.py | 4 ++-- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 32bbd009417f13..e2a1479595e419 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -1,11 +1,11 @@ """Support for Nest devices.""" +from datetime import datetime, timedelta import logging import socket -from datetime import datetime, timedelta import threading from nest import Nest -from nest.nest import AuthorizationError, APIError +from nest.nest import APIError, AuthorizationError import voluptuous as vol from homeassistant import config_entries @@ -20,11 +20,11 @@ ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity -from .const import DOMAIN from . import local_auth +from .const import DOMAIN _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index efc0bfbc8227e1..34b4f6c5693e15 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -1,11 +1,11 @@ """Support for Nest Cameras.""" -import logging from datetime import timedelta +import logging import requests from homeassistant.components import nest -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_ON_OFF +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_ON_OFF, Camera from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 795ce5c80e97cb..f75e3a692f3112 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -8,22 +8,22 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, FAN_AUTO, FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - SUPPORT_PRESET_MODE, - SUPPORT_FAN_MODE, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, PRESET_AWAY, PRESET_ECO, PRESET_NONE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_COOL, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ( ATTR_TEMPERATURE, diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index b78896b0499759..b8fa2ad93f58f9 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -14,7 +14,6 @@ from .const import DOMAIN - DATA_FLOW_IMPL = "nest_flow_implementation" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index 38d1827326da37..8b0af5011ec02b 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -2,9 +2,10 @@ import asyncio from functools import partial -from nest.nest import NestAuth, AUTHORIZE_URL, AuthorizationError +from nest.nest import AUTHORIZE_URL, AuthorizationError, NestAuth from homeassistant.core import callback + from . import config_flow from .const import DOMAIN diff --git a/tests/components/nest/test_config_flow.py b/tests/components/nest/test_config_flow.py index 4018a94b66622e..ec6218fb0d767a 100644 --- a/tests/components/nest/test_config_flow.py +++ b/tests/components/nest/test_config_flow.py @@ -3,8 +3,8 @@ from unittest.mock import Mock, patch from homeassistant import data_entry_flow +from homeassistant.components.nest import DOMAIN, config_flow from homeassistant.setup import async_setup_component -from homeassistant.components.nest import config_flow, DOMAIN from tests.common import mock_coro diff --git a/tests/components/nest/test_local_auth.py b/tests/components/nest/test_local_auth.py index bf3c3e4230a8ef..491b9bd9e0701e 100644 --- a/tests/components/nest/test_local_auth.py +++ b/tests/components/nest/test_local_auth.py @@ -1,11 +1,11 @@ """Test Nest local auth.""" -from homeassistant.components.nest import const, config_flow, local_auth from urllib.parse import parse_qsl import pytest - import requests_mock as rmock +from homeassistant.components.nest import config_flow, const, local_auth + @pytest.fixture def registered_flow(hass): From 04225ba802fa70267ee6974956c7f8906bbae0c2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:09:49 +0100 Subject: [PATCH 2287/3953] Sort imports according to PEP8 for rest (#29674) --- homeassistant/components/rest/notify.py | 17 ++++++++--------- homeassistant/components/rest/sensor.py | 14 +++++++------- homeassistant/components/rest/switch.py | 6 +++--- tests/components/rest/test_binary_sensor.py | 14 +++++++------- tests/components/rest/test_sensor.py | 16 ++++++++-------- tests/components/rest/test_switch.py | 5 +++-- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index f2bfcf3aba7934..4f3de14b731321 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -4,6 +4,14 @@ import requests import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_MESSAGE, + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_AUTHENTICATION, CONF_HEADERS, @@ -18,15 +26,6 @@ ) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_TARGET, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - ATTR_MESSAGE, - PLATFORM_SCHEMA, - BaseNotificationService, -) - CONF_DATA = "data" CONF_DATA_TEMPLATE = "data_template" CONF_MESSAGE_PARAMETER_NAME = "message_param_name" diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 6fdf5ce7221c64..51120cb350ca11 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -1,34 +1,34 @@ """Support for RESTful API sensors.""" -import logging import json +import logging -import voluptuous as vol import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA +from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA from homeassistant.const import ( CONF_AUTHENTICATION, + CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, CONF_HEADERS, - CONF_NAME, CONF_METHOD, + CONF_NAME, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, + CONF_TIMEOUT, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, - CONF_TIMEOUT, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, - CONF_DEVICE_CLASS, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index a02a8507194532..fe409a46be74ec 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -6,15 +6,15 @@ import async_timeout import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HEADERS, + CONF_METHOD, CONF_NAME, + CONF_PASSWORD, CONF_RESOURCE, CONF_TIMEOUT, - CONF_METHOD, CONF_USERNAME, - CONF_PASSWORD, CONF_VERIFY_SSL, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 8993be6a7a1059..a4850793ca765b 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -1,21 +1,21 @@ """The tests for the REST binary sensor platform.""" import unittest -from pytest import raises -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch +import pytest +from pytest import raises import requests from requests.exceptions import Timeout import requests_mock -from homeassistant.exceptions import PlatformNotReady -from homeassistant.setup import setup_component import homeassistant.components.binary_sensor as binary_sensor import homeassistant.components.rest.binary_sensor as rest -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import template +from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component -import pytest +from tests.common import assert_setup_component, get_test_home_assistant class TestRestBinarySensorSetup(unittest.TestCase): diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index d770f21a403e4e..7edbfa065ad418 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,20 +1,20 @@ """The tests for the REST sensor platform.""" import unittest -from pytest import raises -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch +import pytest +from pytest import raises import requests -from requests.exceptions import Timeout, RequestException +from requests.exceptions import RequestException, Timeout import requests_mock -from homeassistant.exceptions import PlatformNotReady -from homeassistant.setup import setup_component -import homeassistant.components.sensor as sensor import homeassistant.components.rest.sensor as rest +import homeassistant.components.sensor as sensor +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.config_validation import template +from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component -import pytest +from tests.common import assert_setup_component, get_test_home_assistant class TestRestSensorSetup(unittest.TestCase): diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 81430cff349db4..d1e4ac05514b7b 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -4,9 +4,10 @@ import aiohttp import homeassistant.components.rest.switch as rest -from homeassistant.setup import setup_component from homeassistant.helpers.template import Template -from tests.common import get_test_home_assistant, assert_setup_component +from homeassistant.setup import setup_component + +from tests.common import assert_setup_component, get_test_home_assistant class TestRestSwitchSetup: From d62993c5af2e43d8639c388412b6c68040b62a00 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:10:38 +0100 Subject: [PATCH 2288/3953] Sort imports according to PEP8 for pilight (#29673) --- homeassistant/components/pilight/__init__.py | 19 +++++++++---------- .../components/pilight/binary_sensor.py | 2 +- homeassistant/components/pilight/sensor.py | 6 +++--- homeassistant/components/pilight/switch.py | 10 +++++----- tests/components/pilight/test_init.py | 8 ++++---- tests/components/pilight/test_sensor.py | 6 +++--- 6 files changed, 25 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index e8cc0862a53fbb..50ee1b248b0c92 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -1,25 +1,24 @@ """Component to create an interface to a Pilight daemon.""" -import logging +from datetime import timedelta import functools +import logging import socket import threading -from datetime import timedelta - -import voluptuous as vol from pilight import pilight +import voluptuous as vol -from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.util import dt as dt_util -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT, - CONF_WHITELIST, CONF_PROTOCOL, + CONF_WHITELIST, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pilight/binary_sensor.py b/homeassistant/components/pilight/binary_sensor.py index a8f7d84b9b17bd..ae6d562725d612 100644 --- a/homeassistant/components/pilight/binary_sensor.py +++ b/homeassistant/components/pilight/binary_sensor.py @@ -3,6 +3,7 @@ import logging import voluptuous as vol + from homeassistant.components import pilight from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( @@ -16,7 +17,6 @@ from homeassistant.helpers.event import track_point_in_time from homeassistant.util import dt as dt_util - _LOGGER = logging.getLogger(__name__) CONF_VARIABLE = "variable" diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py index 006bebf74bb36b..e8c7b4bd4b6673 100644 --- a/homeassistant/components/pilight/sensor.py +++ b/homeassistant/components/pilight/sensor.py @@ -3,11 +3,11 @@ import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_PAYLOAD -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity from homeassistant.components import pilight +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_PAYLOAD, CONF_UNIT_OF_MEASUREMENT import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pilight/switch.py b/homeassistant/components/pilight/switch.py index fb95b91dfd83c8..8be199921dcb74 100644 --- a/homeassistant/components/pilight/switch.py +++ b/homeassistant/components/pilight/switch.py @@ -3,17 +3,17 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components import pilight -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( - CONF_NAME, CONF_ID, - CONF_SWITCHES, - CONF_STATE, + CONF_NAME, CONF_PROTOCOL, + CONF_STATE, + CONF_SWITCHES, STATE_ON, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index e45c7ddc02f575..2a2342a90dcb56 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -1,18 +1,18 @@ """The tests for the pilight component.""" +from datetime import timedelta import logging +import socket import unittest from unittest.mock import patch -import socket -from datetime import timedelta import pytest from homeassistant import core as ha -from homeassistant.setup import setup_component from homeassistant.components import pilight +from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/pilight/test_sensor.py b/tests/components/pilight/test_sensor.py index 721b04386fee41..4bc1e80b07abb2 100644 --- a/tests/components/pilight/test_sensor.py +++ b/tests/components/pilight/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the Pilight sensor platform.""" import logging -from homeassistant.setup import setup_component -import homeassistant.components.sensor as sensor from homeassistant.components import pilight +import homeassistant.components.sensor as sensor +from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, mock_component +from tests.common import assert_setup_component, get_test_home_assistant, mock_component HASS = None From 05daa817f5899b18e3c8018acb8ec05aa03b5e1f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:11:27 +0100 Subject: [PATCH 2289/3953] Sort imports according to PEP8 for owntracks (#29672) --- .../components/owntracks/__init__.py | 2 +- .../components/owntracks/config_flow.py | 2 +- .../components/owntracks/device_tracker.py | 19 ++++++++++--------- .../components/owntracks/messages.py | 5 ++--- .../components/owntracks/test_config_flow.py | 6 +++--- tests/components/owntracks/test_helper.py | 1 + tests/components/owntracks/test_init.py | 5 +++-- 7 files changed, 21 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 8556e8a7556057..b75be465aa19aa 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -14,8 +14,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_when_setup -from .const import DOMAIN from .config_flow import CONF_SECRET +from .const import DOMAIN from .messages import async_handle_message _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 1a8bb838e181f7..82f89de6363742 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -1,7 +1,7 @@ """Config flow for OwnTracks.""" from homeassistant import config_entries -from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.auth.util import generate_secret +from homeassistant.const import CONF_WEBHOOK_ID from .const import DOMAIN # noqa pylint: disable=unused-import from .helper import supports_encryption diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 6d3254eda991cd..00fa023d6c1f41 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -1,21 +1,22 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" import logging -from homeassistant.core import callback +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker.const import ( + ATTR_SOURCE_TYPE, + ENTITY_ID_FORMAT, + SOURCE_TYPE_GPS, +) from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_BATTERY_LEVEL, ) -from homeassistant.components.device_tracker.const import ( - ENTITY_ID_FORMAT, - ATTR_SOURCE_TYPE, - SOURCE_TYPE_GPS, -) -from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.core import callback from homeassistant.helpers import device_registry +from homeassistant.helpers.restore_state import RestoreEntity + from . import DOMAIN as OT_DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 7c388f9eb17d73..d357843c42eb81 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -2,15 +2,14 @@ import json import logging -from nacl.secret import SecretBox from nacl.encoding import Base64Encoder +from nacl.secret import SecretBox from homeassistant.components import zone as zone_comp from homeassistant.components.device_tracker import ( - SOURCE_TYPE_GPS, SOURCE_TYPE_BLUETOOTH_LE, + SOURCE_TYPE_GPS, ) - from homeassistant.const import STATE_HOME from homeassistant.util import decorator, slugify diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 54e33a1ab61b46..d48c3c43a25cdc 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -1,16 +1,16 @@ """Tests for OwnTracks config flow.""" from unittest.mock import Mock, patch + import pytest from homeassistant import data_entry_flow -from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.components.owntracks import config_flow from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET from homeassistant.components.owntracks.const import DOMAIN +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.setup import async_setup_component -from tests.common import mock_coro, MockConfigEntry - +from tests.common import MockConfigEntry, mock_coro CONF_WEBHOOK_URL = "webhook_url" diff --git a/tests/components/owntracks/test_helper.py b/tests/components/owntracks/test_helper.py index f870ce82dd385e..2c06ac0c4e7f8b 100644 --- a/tests/components/owntracks/test_helper.py +++ b/tests/components/owntracks/test_helper.py @@ -1,5 +1,6 @@ """Test the owntracks_http platform.""" from unittest.mock import patch + import pytest from homeassistant.components.owntracks import helper diff --git a/tests/components/owntracks/test_init.py b/tests/components/owntracks/test_init.py index aab772d64e3305..78ae687e2084ef 100644 --- a/tests/components/owntracks/test_init.py +++ b/tests/components/owntracks/test_init.py @@ -3,9 +3,10 @@ import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import owntracks -from tests.common import mock_component, MockConfigEntry +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, mock_component MINIMAL_LOCATION_MESSAGE = { "_type": "location", From 27bd6ca1db80d6883b6e26cbc5288451d9fe3396 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:12:41 +0100 Subject: [PATCH 2290/3953] Sort imports according to PEP8 for emulated_hue (#29667) --- .../components/emulated_hue/__init__.py | 16 +++++----- .../components/emulated_hue/hue_api.py | 6 ++-- homeassistant/components/emulated_hue/upnp.py | 4 +-- tests/components/emulated_hue/test_hue_api.py | 29 +++++++++---------- tests/components/emulated_hue/test_init.py | 2 +- tests/components/emulated_hue/test_upnp.py | 8 ++--- 6 files changed, 32 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 41c23707a036f9..c40a22df4510c4 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -5,22 +5,22 @@ import voluptuous as vol from homeassistant import util +from homeassistant.components.http import real_ip from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.deprecation import get_deprecated import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.deprecation import get_deprecated from homeassistant.util.json import load_json, save_json -from homeassistant.components.http import real_ip from .hue_api import ( - HueUsernameView, - HueUnauthorizedUser, - HueAllLightsStateView, - HueOneLightStateView, - HueOneLightChangeView, - HueGroupView, HueAllGroupsStateView, + HueAllLightsStateView, HueFullStateView, + HueGroupView, + HueOneLightChangeView, + HueOneLightStateView, + HueUnauthorizedUser, + HueUsernameView, ) from .upnp import DescriptionXmlView, UPNPResponderThread diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index d7db6bb2fe343e..41c6329446c2c2 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,6 +1,6 @@ """Support for a Hue API to control Home Assistant.""" -import logging import hashlib +import logging from homeassistant import core from homeassistant.components import ( @@ -34,8 +34,8 @@ from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -49,8 +49,8 @@ ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, HTTP_BAD_REQUEST, - HTTP_UNAUTHORIZED, HTTP_NOT_FOUND, + HTTP_UNAUTHORIZED, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_TURN_OFF, diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index 412dfdd673e5ac..0583ae9a1b68ea 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -1,8 +1,8 @@ """Support UPNP discovery method that mimics Hue hubs.""" -import threading -import socket import logging import select +import socket +import threading from aiohttp import web diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 749493a6ca8177..32543602f89ced 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1,43 +1,42 @@ """The tests for the emulated Hue component.""" import asyncio -import json +from datetime import timedelta from ipaddress import ip_address +import json from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE import pytest -from tests.common import get_test_instance_port from homeassistant import const, setup from homeassistant.components import ( + climate, + cover, + emulated_hue, fan, http, light, - script, - emulated_hue, media_player, - cover, - climate, + script, ) from homeassistant.components.emulated_hue import Config from homeassistant.components.emulated_hue.hue_api import ( - HUE_API_STATE_ON, HUE_API_STATE_BRI, HUE_API_STATE_HUE, + HUE_API_STATE_ON, HUE_API_STATE_SAT, HUE_API_USERNAME, - HueUsernameView, - HueOneLightStateView, - HueAllLightsStateView, - HueOneLightChangeView, HueAllGroupsStateView, + HueAllLightsStateView, HueFullStateView, + HueOneLightChangeView, + HueOneLightStateView, + HueUsernameView, ) -from homeassistant.const import STATE_ON, STATE_OFF - +from homeassistant.const import STATE_OFF, STATE_ON import homeassistant.util.dt as dt_util -from datetime import timedelta -from tests.common import async_fire_time_changed + +from tests.common import async_fire_time_changed, get_test_instance_port HTTP_SERVER_PORT = get_test_instance_port() BRIDGE_SERVER_PORT = get_test_instance_port() diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index a8798daeba27d2..09c0731e4cdc8c 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,5 +1,5 @@ """Test the Emulated Hue component.""" -from unittest.mock import patch, Mock, MagicMock +from unittest.mock import MagicMock, Mock, patch from homeassistant.components.emulated_hue import Config diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 4cbc79f68a507e..2fc9d903d3b605 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -1,15 +1,15 @@ """The tests for the emulated Hue component.""" import json - import unittest from unittest.mock import patch -import requests + from aiohttp.hdrs import CONTENT_TYPE +import requests -from homeassistant import setup, const +from homeassistant import const, setup from homeassistant.components import emulated_hue, http -from tests.common import get_test_instance_port, get_test_home_assistant +from tests.common import get_test_home_assistant, get_test_instance_port HTTP_SERVER_PORT = get_test_instance_port() BRIDGE_SERVER_PORT = get_test_instance_port() From 01d651c67d35fba0f00e0fae7970b6d63a227df7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:14:38 +0100 Subject: [PATCH 2291/3953] Sort imports according to PEP8 for device_tracker (#29666) --- .../components/device_tracker/__init__.py | 11 +++++------ .../components/device_tracker/config_entry.py | 6 +++--- .../components/device_tracker/device_condition.py | 10 ++++++---- homeassistant/components/device_tracker/setup.py | 15 +++++++-------- tests/components/device_tracker/common.py | 6 +++--- .../device_tracker/test_device_condition.py | 6 +++--- tests/components/device_tracker/test_entities.py | 5 +++-- 7 files changed, 30 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 25e33d2a2db2fb..a160e580c57700 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -3,21 +3,19 @@ import voluptuous as vol -from homeassistant.loader import bind_hass from homeassistant.components import group +from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType - from homeassistant.helpers.event import async_track_utc_time_change -from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME +from homeassistant.helpers.typing import ConfigType, GPSType, HomeAssistantType +from homeassistant.loader import bind_hass from . import legacy, setup from .config_entry import ( # noqa: F401 pylint: disable=unused-import async_setup_entry, async_unload_entry, ) -from .legacy import DeviceScanner # noqa: F401 pylint: disable=unused-import from .const import ( ATTR_ATTRIBUTES, ATTR_BATTERY, @@ -38,11 +36,12 @@ DEFAULT_TRACK_NEW, DOMAIN, PLATFORM_TYPE_LEGACY, - SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_BLUETOOTH, + SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, ) +from .legacy import DeviceScanner # noqa: F401 pylint: disable=unused-import ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format("all_devices") diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 9e53c2e0cea24e..6c5cacac591733 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -3,12 +3,12 @@ from homeassistant.components import zone from homeassistant.const import ( - STATE_NOT_HOME, - STATE_HOME, + ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_BATTERY_LEVEL, + STATE_HOME, + STATE_NOT_HOME, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index 6379aca6c0b2a3..9bdfc12db39910 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -1,21 +1,23 @@ """Provides device automations for Device tracker.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - STATE_NOT_HOME, + CONF_TYPE, STATE_HOME, + STATE_NOT_HOME, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_home", "is_not_home"} diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py index 6c9f05dead7349..42751b1a7845d9 100644 --- a/homeassistant/components/device_tracker/setup.py +++ b/homeassistant/components/device_tracker/setup.py @@ -1,27 +1,26 @@ """Device tracker helpers.""" import asyncio -from typing import Dict, Any, Callable, Optional from types import ModuleType +from typing import Any, Callable, Dict, Optional import attr +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import callback -from homeassistant.setup import async_prepare_setup_platform -from homeassistant.helpers import config_per_platform from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers import config_per_platform from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.setup import async_prepare_setup_platform from homeassistant.util import dt as dt_util -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE - from .const import ( + CONF_SCAN_INTERVAL, DOMAIN, + LOGGER, PLATFORM_TYPE_LEGACY, - CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_ROUTER, - LOGGER, ) diff --git a/tests/components/device_tracker/common.py b/tests/components/device_tracker/common.py index 3a2df751d6ffcc..1326174c6be134 100644 --- a/tests/components/device_tracker/common.py +++ b/tests/components/device_tracker/common.py @@ -4,15 +4,15 @@ components. Instead call the service directly. """ from homeassistant.components.device_tracker import ( - DOMAIN, ATTR_ATTRIBUTES, ATTR_BATTERY, + ATTR_DEV_ID, ATTR_GPS, ATTR_GPS_ACCURACY, + ATTR_HOST_NAME, ATTR_LOCATION_NAME, ATTR_MAC, - ATTR_DEV_ID, - ATTR_HOST_NAME, + DOMAIN, SERVICE_SEE, ) from homeassistant.core import callback diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 732abccd8ca586..15cd28e8fae93f 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -1,19 +1,19 @@ """The tests for Device tracker device conditions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import STATE_HOME, STATE_NOT_HOME -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/device_tracker/test_entities.py b/tests/components/device_tracker/test_entities.py index 031e961cd2d1dc..a0b2553543dc50 100644 --- a/tests/components/device_tracker/test_entities.py +++ b/tests/components/device_tracker/test_entities.py @@ -6,11 +6,12 @@ ScannerEntity, ) from homeassistant.components.device_tracker.const import ( - SOURCE_TYPE_ROUTER, ATTR_SOURCE_TYPE, DOMAIN, + SOURCE_TYPE_ROUTER, ) -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, ATTR_BATTERY_LEVEL +from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_HOME, STATE_NOT_HOME + from tests.common import MockConfigEntry From eb47c2b1487f0e99a577b41532b0ffd3501d1edc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:17:41 +0100 Subject: [PATCH 2292/3953] Sort imports according to PEP8 for media_player (#29665) --- homeassistant/components/media_player/__init__.py | 1 - .../components/media_player/device_condition.py | 10 ++++++---- .../components/media_player/reproduce_state.py | 15 +++++++-------- tests/components/media_player/common.py | 2 +- .../components/media_player/test_async_helpers.py | 10 +++++----- .../media_player/test_device_condition.py | 10 +++++----- tests/components/media_player/test_init.py | 2 +- .../media_player/test_reproduce_state.py | 2 +- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index a6cd4dda4ead8e..1375a0ed429ba3 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -97,7 +97,6 @@ SUPPORT_VOLUME_STEP, ) - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index a67a084a94f799..a8091a6aed81b7 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -1,24 +1,26 @@ """Provides device automations for Media player.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, + STATE_IDLE, STATE_OFF, STATE_ON, - STATE_IDLE, STATE_PAUSED, STATE_PLAYING, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_on", "is_off", "is_idle", "is_paused", "is_playing"} diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index dac08afe471dea..dc9078d3ffd577 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -21,21 +21,20 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ( - ATTR_MEDIA_VOLUME_LEVEL, - ATTR_MEDIA_VOLUME_MUTED, - ATTR_MEDIA_SEEK_POSITION, ATTR_INPUT_SOURCE, - ATTR_SOUND_MODE, - ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_ENQUEUE, + ATTR_MEDIA_SEEK_POSITION, + ATTR_MEDIA_VOLUME_LEVEL, + ATTR_MEDIA_VOLUME_MUTED, + ATTR_SOUND_MODE, + DOMAIN, SERVICE_PLAY_MEDIA, - SERVICE_SELECT_SOURCE, SERVICE_SELECT_SOUND_MODE, - DOMAIN, + SERVICE_SELECT_SOURCE, ) - # mypy: allow-untyped-defs diff --git a/tests/components/media_player/common.py b/tests/components/media_player/common.py index 177f8169ff95fd..5be9a29282992f 100644 --- a/tests/components/media_player/common.py +++ b/tests/components/media_player/common.py @@ -18,6 +18,7 @@ ) from homeassistant.const import ( ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, @@ -32,7 +33,6 @@ SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, - ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 4a2e4fed6c565d..2e1ded3f08492e 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -1,14 +1,14 @@ """The tests for the Async Media player helper functions.""" -import unittest import asyncio +import unittest import homeassistant.components.media_player as mp from homeassistant.const import ( - STATE_PLAYING, - STATE_PAUSED, - STATE_ON, - STATE_OFF, STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, ) from tests.common import get_test_home_assistant diff --git a/tests/components/media_player/test_device_condition.py b/tests/components/media_player/test_device_condition.py index 6216cc0e2b0c85..333cc4a2b13b58 100644 --- a/tests/components/media_player/test_device_condition.py +++ b/tests/components/media_player/test_device_condition.py @@ -1,25 +1,25 @@ """The tests for Media player device conditions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.media_player import DOMAIN from homeassistant.const import ( - STATE_ON, - STATE_OFF, STATE_IDLE, + STATE_OFF, + STATE_ON, STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 7e36206a635d12..3db92cda42d0cc 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -2,8 +2,8 @@ import base64 from unittest.mock import patch -from homeassistant.setup import async_setup_component from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.setup import async_setup_component from tests.common import mock_coro diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py index ddc5d6cf0ca68f..cbc5684f5d5042 100644 --- a/tests/components/media_player/test_reproduce_state.py +++ b/tests/components/media_player/test_reproduce_state.py @@ -2,7 +2,6 @@ import pytest -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, @@ -17,6 +16,7 @@ SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, ) +from homeassistant.components.media_player.reproduce_state import async_reproduce_states from homeassistant.const import ( SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, From 3b0f29fe957f349d414fcc479e4a5f5c35464abb Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:19:11 +0100 Subject: [PATCH 2293/3953] sort imports according to PEP8 for lock (#29663) --- homeassistant/components/lock/__init__.py | 27 +++++++++---------- .../components/lock/device_action.py | 10 ++++--- .../components/lock/device_condition.py | 8 +++--- .../components/lock/device_trigger.py | 14 +++++----- .../components/lock/reproduce_state.py | 4 +-- tests/components/lock/common.py | 4 +-- tests/components/lock/test_device_action.py | 6 ++--- .../components/lock/test_device_condition.py | 6 ++--- tests/components/lock/test_device_trigger.py | 6 ++--- 9 files changed, 45 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index c443a5219d7dd2..a50f687238fbe5 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -5,26 +5,25 @@ import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import ( # noqa: F401 - make_entity_service_schema, - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.components import group from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, - STATE_LOCKED, - STATE_UNLOCKED, SERVICE_LOCK, - SERVICE_UNLOCK, SERVICE_OPEN, + SERVICE_UNLOCK, + STATE_LOCKED, + STATE_UNLOCKED, ) -from homeassistant.components import group - +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, + make_entity_service_schema, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py index c678bfc17cf28a..efdb5e352cfe31 100644 --- a/homeassistant/components/lock/device_action.py +++ b/homeassistant/components/lock/device_action.py @@ -1,21 +1,23 @@ """Provides device automations for Lock.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import DOMAIN, SUPPORT_OPEN ACTION_TYPES = {"lock", "unlock", "open"} diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index 328da6ad450e1c..447913206691dd 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -1,21 +1,23 @@ """Provides device automations for Lock.""" from typing import List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, STATE_LOCKED, STATE_UNLOCKED, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_locked", "is_unlocked"} diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index 8732cca29f0a0d..9db2822a5916ec 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -1,21 +1,23 @@ """Provides device automations for Lock.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType -from homeassistant.components.automation import state, AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN TRIGGER_TYPES = {"locked", "unlocked"} diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py index dc1bee85839dab..b8b469f943fa70 100644 --- a/homeassistant/components/lock/reproduce_state.py +++ b/homeassistant/components/lock/reproduce_state.py @@ -5,10 +5,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_LOCKED, - STATE_UNLOCKED, SERVICE_LOCK, SERVICE_UNLOCK, + STATE_LOCKED, + STATE_UNLOCKED, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/tests/components/lock/common.py b/tests/components/lock/common.py index a658befca55d5f..ec477cba78d14c 100644 --- a/tests/components/lock/common.py +++ b/tests/components/lock/common.py @@ -7,10 +7,10 @@ from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, SERVICE_LOCK, - SERVICE_UNLOCK, SERVICE_OPEN, - ENTITY_MATCH_ALL, + SERVICE_UNLOCK, ) from homeassistant.loader import bind_hass diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index 2006f9b3ff1431..0fc98d9460e69d 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -1,19 +1,19 @@ """The tests for Lock device actions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.lock import DOMAIN from homeassistant.const import CONF_PLATFORM -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py index 675f402e770cb5..638a7edf5d745d 100644 --- a/tests/components/lock/test_device_condition.py +++ b/tests/components/lock/test_device_condition.py @@ -1,19 +1,19 @@ """The tests for Lock device conditions.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.lock import DOMAIN from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 572e28c44a6ea4..781ed03307ba69 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -1,19 +1,19 @@ """The tests for Lock device triggers.""" import pytest +import homeassistant.components.automation as automation from homeassistant.components.lock import DOMAIN from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED -from homeassistant.setup import async_setup_component -import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) From 2da3848f892b9f9d56c7572edf956e3094d65d38 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:19:34 +0100 Subject: [PATCH 2294/3953] Sort imports according to PEP8 for unifi (#29656) --- homeassistant/components/unifi/__init__.py | 3 +-- homeassistant/components/unifi/config_flow.py | 4 ++-- homeassistant/components/unifi/controller.py | 16 +++++++--------- homeassistant/components/unifi/device_tracker.py | 3 +-- homeassistant/components/unifi/switch.py | 2 +- tests/components/unifi/test_config_flow.py | 4 +--- tests/components/unifi/test_controller.py | 5 ++--- tests/components/unifi/test_device_tracker.py | 3 +-- tests/components/unifi/test_init.py | 4 +--- tests/components/unifi/test_sensor.py | 3 +-- tests/components/unifi/test_switch.py | 3 +-- 11 files changed, 19 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 4f3edf9ce79eac..65015b357a7138 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -3,9 +3,8 @@ from homeassistant.const import CONF_HOST from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC - import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .config_flow import get_controller_id_from_config_entry from .const import ( diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 01b97a78366116..52ecab08856fb5 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -2,7 +2,6 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -10,6 +9,7 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.core import callback from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -21,10 +21,10 @@ CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_DETECTION_TIME, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, - DEFAULT_DETECTION_TIME, DOMAIN, LOGGER, ) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 3deb2e9040aa15..826491f6ba6df4 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -1,16 +1,14 @@ """UniFi Controller abstraction.""" -from datetime import timedelta - import asyncio +from datetime import timedelta import ssl -import async_timeout from aiohttp import CookieJar - import aiounifi +import async_timeout -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_HOST +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -22,19 +20,19 @@ CONF_DONT_TRACK_CLIENTS, CONF_DONT_TRACK_DEVICES, CONF_DONT_TRACK_WIRED_CLIENTS, + CONF_SITE_ID, + CONF_SSID_FILTER, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, - CONF_SITE_ID, - CONF_SSID_FILTER, CONTROLLER_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_BLOCK_CLIENTS, + DEFAULT_DETECTION_TIME, + DEFAULT_SSID_FILTER, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, - DEFAULT_DETECTION_TIME, - DEFAULT_SSID_FILTER, DOMAIN, LOGGER, UNIFI_CONFIG, diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b92211c4eae5a2..0806a2c04e3f4e 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,16 +1,15 @@ """Track devices using UniFi controllers.""" import logging -from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.core import callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY - import homeassistant.util.dt as dt_util from .const import ATTR_MANUFACTURER diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 82aa6f0384de00..18bfbb4066e146 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,8 +1,8 @@ """Support for devices connected to UniFi POE.""" import logging -from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.switch import SwitchDevice +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.core import callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index aea4d565f3d36b..1b973aee9a507c 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -1,10 +1,10 @@ """Test UniFi config flow.""" +import aiounifi from asynctest import patch from homeassistant.components import unifi from homeassistant.components.unifi import config_flow from homeassistant.components.unifi.const import CONF_CONTROLLER, CONF_SITE_ID - from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -15,8 +15,6 @@ from tests.common import MockConfigEntry -import aiounifi - async def test_flow_works(hass, aioclient_mock): """Test config flow.""" diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 2b64e56cd996e9..c86e2f11538982 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -2,12 +2,11 @@ from collections import deque from datetime import timedelta +import aiounifi from asynctest import Mock, patch - import pytest from homeassistant import config_entries -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components import unifi from homeassistant.components.unifi.const import ( CONF_CONTROLLER, @@ -22,7 +21,7 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) -import aiounifi +from homeassistant.exceptions import ConfigEntryNotReady CONTROLLER_HOST = { "hostname": "controller_host", diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 29b165537571b5..634247a5283d5a 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -4,6 +4,7 @@ from homeassistant import config_entries from homeassistant.components import unifi +import homeassistant.components.device_tracker as device_tracker from homeassistant.components.unifi.const import ( CONF_SSID_FILTER, CONF_TRACK_DEVICES, @@ -12,8 +13,6 @@ from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component - -import homeassistant.components.device_tracker as device_tracker import homeassistant.util.dt as dt_util from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 6b17b803390488..1f5a3852e164ac 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -2,11 +2,9 @@ from unittest.mock import Mock, patch from homeassistant.components import unifi - from homeassistant.setup import async_setup_component - -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_setup_with_no_config(hass): diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index f591801a966d4c..8819f2f33ae3e0 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -2,9 +2,8 @@ from copy import deepcopy from homeassistant.components import unifi -from homeassistant.setup import async_setup_component - import homeassistant.components.sensor as sensor +from homeassistant.setup import async_setup_component from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 3d754fb5dffb14..30f9e990421180 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -3,11 +3,10 @@ from homeassistant import config_entries from homeassistant.components import unifi +import homeassistant.components.switch as switch from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -import homeassistant.components.switch as switch - from .test_controller import ( CONTROLLER_HOST, ENTRY_CONFIG, From 41cd678f00a0d3331a71903bafb2de8d66f762a2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:25:35 +0100 Subject: [PATCH 2295/3953] Sort imports according to PEP8 for deconz (#29659) --- tests/components/deconz/test_binary_sensor.py | 5 ++--- tests/components/deconz/test_climate.py | 5 ++--- tests/components/deconz/test_config_flow.py | 6 +++--- tests/components/deconz/test_cover.py | 5 ++--- tests/components/deconz/test_deconz_event.py | 2 +- tests/components/deconz/test_device_trigger.py | 4 ++-- tests/components/deconz/test_gateway.py | 7 ++----- tests/components/deconz/test_init.py | 3 +-- tests/components/deconz/test_light.py | 5 ++--- tests/components/deconz/test_scene.py | 5 ++--- tests/components/deconz/test_sensor.py | 5 ++--- tests/components/deconz/test_services.py | 3 +-- tests/components/deconz/test_switch.py | 5 ++--- 13 files changed, 24 insertions(+), 36 deletions(-) diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index c340018b641889..297be46dd271ee 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -2,11 +2,10 @@ from copy import deepcopy from homeassistant.components import deconz -from homeassistant.setup import async_setup_component - import homeassistant.components.binary_sensor as binary_sensor +from homeassistant.setup import async_setup_component -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration SENSORS = { "1": { diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 9536929ff3cda5..d905c80b2cdeb4 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -4,11 +4,10 @@ from asynctest import patch from homeassistant.components import deconz -from homeassistant.setup import async_setup_component - import homeassistant.components.climate as climate +from homeassistant.setup import async_setup_component -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration SENSORS = { "1": { diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 4045201bd18084..b95743c95ad250 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,13 +1,13 @@ """Tests for deCONZ config flow.""" +import asyncio from unittest.mock import Mock, patch -import asyncio +import pydeconz from homeassistant.components.deconz import config_flow from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL -from tests.common import MockConfigEntry -import pydeconz +from tests.common import MockConfigEntry async def test_flow_works(hass, aioclient_mock): diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 31819888404d81..f6f5f3a23cad03 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -4,11 +4,10 @@ from asynctest import patch from homeassistant.components import deconz -from homeassistant.setup import async_setup_component - import homeassistant.components.cover as cover +from homeassistant.setup import async_setup_component -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration COVERS = { "1": { diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index ade9aa02ad4afb..d4f461762086d9 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -5,7 +5,7 @@ from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration SENSORS = { "1": { diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 91714e647bd9d3..0d86fc3073063b 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -3,9 +3,9 @@ from homeassistant.components.deconz import device_trigger -from tests.common import assert_lists_same, async_get_device_automations +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from tests.common import assert_lists_same, async_get_device_automations SENSORS = { "1": { diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index f9290cc0e055b9..109bb325dac432 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -2,17 +2,14 @@ from copy import deepcopy from asynctest import Mock, patch - +import pydeconz import pytest from homeassistant import config_entries -from homeassistant.components import deconz -from homeassistant.components import ssdp +from homeassistant.components import deconz, ssdp from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect -import pydeconz - BRIDGEID = "0123456789" ENTRY_CONFIG = { diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index d21859197be49c..3c6e02ab41cae8 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -2,11 +2,10 @@ import asyncio from asynctest import Mock, patch - import pytest -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components import deconz +from homeassistant.exceptions import ConfigEntryNotReady from tests.common import MockConfigEntry diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 438b2209b1da2f..0ba4463ab81a41 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -4,11 +4,10 @@ from asynctest import patch from homeassistant.components import deconz -from homeassistant.setup import async_setup_component - import homeassistant.components.light as light +from homeassistant.setup import async_setup_component -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration GROUPS = { "1": { diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index ca7493a41f1e2d..2b7cbaa55f5af3 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -4,11 +4,10 @@ from asynctest import patch from homeassistant.components import deconz -from homeassistant.setup import async_setup_component - import homeassistant.components.scene as scene +from homeassistant.setup import async_setup_component -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration GROUPS = { "1": { diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index b678ee136440a4..c42cd82b3d2544 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -2,11 +2,10 @@ from copy import deepcopy from homeassistant.components import deconz -from homeassistant.setup import async_setup_component - import homeassistant.components.sensor as sensor +from homeassistant.setup import async_setup_component -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration SENSORS = { "1": { diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index fae0ed41d9304a..fad5444aa00729 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -2,7 +2,6 @@ from copy import deepcopy from asynctest import Mock, patch - import pytest import voluptuous as vol @@ -10,8 +9,8 @@ from .test_gateway import ( BRIDGEID, - ENTRY_CONFIG, DECONZ_WEB_REQUEST, + ENTRY_CONFIG, setup_deconz_integration, ) diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 0c66ace56401d6..352ec84c5028a5 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -4,11 +4,10 @@ from asynctest import patch from homeassistant.components import deconz -from homeassistant.setup import async_setup_component - import homeassistant.components.switch as switch +from homeassistant.setup import async_setup_component -from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration SWITCHES = { "1": { From 16a7408f233538d410f0487b283a659e7ee2b9ea Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:29:12 +0100 Subject: [PATCH 2296/3953] Sort imports according to PEP8 for zwave (#29658) --- homeassistant/components/zwave/__init__.py | 52 +++++++++---------- .../components/zwave/binary_sensor.py | 10 ++-- homeassistant/components/zwave/climate.py | 12 ++--- homeassistant/components/zwave/config_flow.py | 2 +- homeassistant/components/zwave/cover.py | 16 +++--- homeassistant/components/zwave/fan.py | 9 ++-- homeassistant/components/zwave/light.py | 13 ++--- homeassistant/components/zwave/lock.py | 5 +- homeassistant/components/zwave/node_entity.py | 18 +++---- homeassistant/components/zwave/sensor.py | 8 +-- homeassistant/components/zwave/switch.py | 4 +- tests/components/zwave/conftest.py | 2 +- tests/components/zwave/test_binary_sensor.py | 5 +- tests/components/zwave/test_climate.py | 8 +-- tests/components/zwave/test_cover.py | 8 +-- tests/components/zwave/test_fan.py | 8 +-- tests/components/zwave/test_init.py | 2 +- tests/components/zwave/test_light.py | 10 ++-- tests/components/zwave/test_lock.py | 4 +- tests/components/zwave/test_node_entity.py | 9 ++-- tests/components/zwave/test_sensor.py | 2 +- tests/components/zwave/test_switch.py | 2 +- tests/components/zwave/test_websocket_api.py | 3 +- tests/components/zwave/test_workaround.py | 1 + 24 files changed, 111 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 293cc45273f34a..6dd007fb8a8b2f 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -8,62 +8,60 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback, CoreState +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import CoreState, callback from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_device_registry, +) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from homeassistant.helpers.device_registry import ( - async_get_registry as async_get_device_registry, -) -from homeassistant.const import ( - ATTR_ENTITY_ID, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, -) from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.event import async_track_time_change from homeassistant.util import convert import homeassistant.util.dt as dt_util -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from . import const from . import config_flow # noqa: F401 pylint: disable=unused-import -from . import websocket_api as wsapi +from . import const, websocket_api as wsapi, workaround from .const import ( CONF_AUTOHEAL, + CONF_CONFIG_PATH, CONF_DEBUG, + CONF_NETWORK_KEY, CONF_POLLING_INTERVAL, CONF_USB_STICK_PATH, - CONF_CONFIG_PATH, - CONF_NETWORK_KEY, + DATA_DEVICES, + DATA_ENTITY_VALUES, + DATA_NETWORK, + DATA_ZWAVE_CONFIG, DEFAULT_CONF_AUTOHEAL, DEFAULT_CONF_USB_STICK_PATH, - DEFAULT_POLLING_INTERVAL, DEFAULT_DEBUG, + DEFAULT_POLLING_INTERVAL, DOMAIN, - DATA_DEVICES, - DATA_NETWORK, - DATA_ENTITY_VALUES, - DATA_ZWAVE_CONFIG, ) -from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity -from . import workaround from .discovery_schemas import DISCOVERY_SCHEMAS +from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity from .util import ( + check_has_unique_id, check_node_schema, check_value_schema, - node_name, - check_has_unique_id, is_node_parsed, node_device_id_and_name, + node_name, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/binary_sensor.py b/homeassistant/components/zwave/binary_sensor.py index a28f53f93d463a..68df3313de31f0 100644 --- a/homeassistant/components/zwave/binary_sensor.py +++ b/homeassistant/components/zwave/binary_sensor.py @@ -1,12 +1,14 @@ """Support for Z-Wave binary sensors.""" -import logging import datetime -import homeassistant.util.dt as dt_util +import logging + +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import track_point_in_time -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice -from . import workaround, ZWaveDeviceEntity +import homeassistant.util.dt as dt_util + +from . import ZWaveDeviceEntity, workaround from .const import COMMAND_CLASS_SENSOR_BINARY _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index e50908783283e3..81b37aa5cb661f 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -1,11 +1,12 @@ """Support for Z-Wave climate devices.""" # Because we do not compile openzwave on CI import logging - from typing import Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_FAN, CURRENT_HVAC_HEAT, @@ -14,28 +15,25 @@ DOMAIN, HVAC_MODE_AUTO, HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - SUPPORT_PRESET_MODE, - ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_HIGH, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect - from . import ZWaveDeviceEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py index a264cdea7dabee..b570e31c128b89 100644 --- a/homeassistant/components/zwave/config_flow.py +++ b/homeassistant/components/zwave/config_flow.py @@ -7,8 +7,8 @@ from homeassistant import config_entries from .const import ( - CONF_USB_STICK_PATH, CONF_NETWORK_KEY, + CONF_USB_STICK_PATH, DEFAULT_CONF_USB_STICK_PATH, DOMAIN, ) diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py index 3a389fbf2bbe83..95cc994e4ff7ec 100644 --- a/homeassistant/components/zwave/cover.py +++ b/homeassistant/components/zwave/cover.py @@ -1,24 +1,26 @@ """Support for Z-Wave covers.""" import logging -from homeassistant.core import callback + from homeassistant.components.cover import ( + ATTR_POSITION, DOMAIN, - SUPPORT_OPEN, SUPPORT_CLOSE, - ATTR_POSITION, + SUPPORT_OPEN, + CoverDevice, ) -from homeassistant.components.cover import CoverDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ( - ZWaveDeviceEntity, CONF_INVERT_OPENCLOSE_BUTTONS, CONF_INVERT_PERCENT, + ZWaveDeviceEntity, workaround, ) from .const import ( - COMMAND_CLASS_SWITCH_MULTILEVEL, - COMMAND_CLASS_SWITCH_BINARY, COMMAND_CLASS_BARRIER_OPERATOR, + COMMAND_CLASS_SWITCH_BINARY, + COMMAND_CLASS_SWITCH_MULTILEVEL, DATA_NETWORK, ) diff --git a/homeassistant/components/zwave/fan.py b/homeassistant/components/zwave/fan.py index a52a7613d7295a..b77ab8dcf68229 100644 --- a/homeassistant/components/zwave/fan.py +++ b/homeassistant/components/zwave/fan.py @@ -2,17 +2,18 @@ import logging import math -from homeassistant.core import callback from homeassistant.components.fan import ( DOMAIN, - FanEntity, - SPEED_OFF, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, + SPEED_OFF, SUPPORT_SET_SPEED, + FanEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ZWaveDeviceEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index bdb9021c02c581..e941b2a97dc6a0 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -1,26 +1,27 @@ """Support for Z-Wave lights.""" import logging - from threading import Timer -from homeassistant.core import callback + from homeassistant.components.light import ( - ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, + DOMAIN, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, - DOMAIN, Light, ) from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util -from . import CONF_REFRESH_VALUE, CONF_REFRESH_DELAY, const, ZWaveDeviceEntity + +from . import CONF_REFRESH_DELAY, CONF_REFRESH_VALUE, ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index ae5640e812d698..9d7b2bffddd44a 100644 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -3,10 +3,11 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.lock import DOMAIN, LockDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 44241e91daf942..3b94991312a63a 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -1,26 +1,26 @@ """Entity class that represents Z-Wave node.""" -import logging from itertools import count +import logging +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_WAKEUP from homeassistant.core import callback -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_WAKEUP, ATTR_ENTITY_ID -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_registry import async_get_registry from .const import ( + ATTR_BASIC_LEVEL, ATTR_NODE_ID, - COMMAND_CLASS_WAKE_UP, - ATTR_SCENE_ID, ATTR_SCENE_DATA, - ATTR_BASIC_LEVEL, - EVENT_NODE_EVENT, - EVENT_SCENE_ACTIVATED, + ATTR_SCENE_ID, COMMAND_CLASS_CENTRAL_SCENE, COMMAND_CLASS_VERSION, + COMMAND_CLASS_WAKE_UP, DOMAIN, + EVENT_NODE_EVENT, + EVENT_SCENE_ACTIVATED, ) -from .util import node_name, is_node_parsed, node_device_id_and_name +from .util import is_node_parsed, node_device_id_and_name, node_name _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index 0820feb8d0f4d8..08ee54415ad63d 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -1,10 +1,12 @@ """Support for Z-Wave sensors.""" import logging -from homeassistant.core import callback -from homeassistant.components.sensor import DOMAIN, DEVICE_CLASS_BATTERY + +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, DOMAIN from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import const, ZWaveDeviceEntity + +from . import ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/switch.py b/homeassistant/components/zwave/switch.py index 4b13b06d2b9905..3592f5340747d3 100644 --- a/homeassistant/components/zwave/switch.py +++ b/homeassistant/components/zwave/switch.py @@ -1,9 +1,11 @@ """Support for Z-Wave switches.""" import logging import time -from homeassistant.core import callback + from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ZWaveDeviceEntity, workaround _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/zwave/conftest.py b/tests/components/zwave/conftest.py index cc9dc5c72ba20b..f80c55f776793d 100644 --- a/tests/components/zwave/conftest.py +++ b/tests/components/zwave/conftest.py @@ -1,5 +1,5 @@ """Fixtures for Z-Wave tests.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest diff --git a/tests/components/zwave/test_binary_sensor.py b/tests/components/zwave/test_binary_sensor.py index b68f4208f7a6c2..54270cdc3f4d19 100644 --- a/tests/components/zwave/test_binary_sensor.py +++ b/tests/components/zwave/test_binary_sensor.py @@ -1,11 +1,10 @@ """Test Z-Wave binary sensors.""" import datetime - from unittest.mock import patch -from homeassistant.components.zwave import const, binary_sensor +from homeassistant.components.zwave import binary_sensor, const -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_none(mock_openzwave): diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index c9fe123af82c8a..b820e496226162 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -2,13 +2,15 @@ import pytest from homeassistant.components.climate.const import ( - CURRENT_HVAC_HEAT, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, - HVAC_MODES, + CURRENT_HVAC_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + HVAC_MODES, PRESET_AWAY, PRESET_BOOST, PRESET_ECO, @@ -18,8 +20,6 @@ SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_HIGH, ) from homeassistant.components.zwave import climate from homeassistant.components.zwave.climate import DEFAULT_HVAC_MODES diff --git a/tests/components/zwave/test_cover.py b/tests/components/zwave/test_cover.py index 848decc2fb533e..e8b784feefe730 100644 --- a/tests/components/zwave/test_cover.py +++ b/tests/components/zwave/test_cover.py @@ -1,15 +1,15 @@ """Test Z-Wave cover devices.""" from unittest.mock import MagicMock -from homeassistant.components.cover import SUPPORT_OPEN, SUPPORT_CLOSE +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.zwave import ( - const, - cover, CONF_INVERT_OPENCLOSE_BUTTONS, CONF_INVERT_PERCENT, + const, + cover, ) -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_none(hass, mock_openzwave): diff --git a/tests/components/zwave/test_fan.py b/tests/components/zwave/test_fan.py index 2252a42a064797..e5dac881ba2533 100644 --- a/tests/components/zwave/test_fan.py +++ b/tests/components/zwave/test_fan.py @@ -1,14 +1,14 @@ """Test Z-Wave fans.""" -from homeassistant.components.zwave import fan from homeassistant.components.fan import ( - SPEED_OFF, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, + SPEED_OFF, SUPPORT_SET_SPEED, ) +from homeassistant.components.zwave import fan -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_fan(mock_openzwave): diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 7038d6b61147c9..8f717b2903cc15 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -19,8 +19,8 @@ ) from homeassistant.components.zwave.binary_sensor import get_device from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.setup import setup_component from tests.common import ( diff --git a/tests/components/zwave/test_light.py b/tests/components/zwave/test_light.py index 55070b9081df34..10efed24bf2daa 100644 --- a/tests/components/zwave/test_light.py +++ b/tests/components/zwave/test_light.py @@ -1,22 +1,22 @@ """Test Z-Wave lights.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from homeassistant.components import zwave -from homeassistant.components.zwave import const, light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, - SUPPORT_TRANSITION, SUPPORT_COLOR, - ATTR_WHITE_VALUE, SUPPORT_COLOR_TEMP, + SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, ) +from homeassistant.components.zwave import const, light -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed class MockLightValues(MockEntityValues): diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index 4a32c3fb07cd29..d5b6d0a0d275dd 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -1,10 +1,10 @@ """Test Z-Wave locks.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from homeassistant import config_entries from homeassistant.components.zwave import const, lock -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_lock(mock_openzwave): diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index dba187d7b966a0..9136b53ff0b3a4 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -1,11 +1,14 @@ """Test Z-Wave node entity.""" import unittest -from unittest.mock import patch, MagicMock -import tests.mock.zwave as mock_zwave +from unittest.mock import MagicMock, patch + import pytest -from homeassistant.components.zwave import node_entity, const + +from homeassistant.components.zwave import const, node_entity from homeassistant.const import ATTR_ENTITY_ID +import tests.mock.zwave as mock_zwave + async def test_maybe_schedule_update(hass, mock_openzwave): """Test maybe schedule update.""" diff --git a/tests/components/zwave/test_sensor.py b/tests/components/zwave/test_sensor.py index cec93f5af4ac64..74e1ef2cd03ec4 100644 --- a/tests/components/zwave/test_sensor.py +++ b/tests/components/zwave/test_sensor.py @@ -2,7 +2,7 @@ from homeassistant.components.zwave import const, sensor import homeassistant.const -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_none(mock_openzwave): diff --git a/tests/components/zwave/test_switch.py b/tests/components/zwave/test_switch.py index a275e4b9e018a6..4293a4a23fd04a 100644 --- a/tests/components/zwave/test_switch.py +++ b/tests/components/zwave/test_switch.py @@ -3,7 +3,7 @@ from homeassistant.components.zwave import switch -from tests.mock.zwave import MockNode, MockValue, MockEntityValues, value_changed +from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed def test_get_device_detects_switch(mock_openzwave): diff --git a/tests/components/zwave/test_websocket_api.py b/tests/components/zwave/test_websocket_api.py index b55024eb3f0832..978fc09a10da59 100644 --- a/tests/components/zwave/test_websocket_api.py +++ b/tests/components/zwave/test_websocket_api.py @@ -1,10 +1,9 @@ """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, + CONF_USB_STICK_PATH, ) from homeassistant.components.zwave.websocket_api import ID, TYPE diff --git a/tests/components/zwave/test_workaround.py b/tests/components/zwave/test_workaround.py index 0825b8f47f9c74..ec708d38e43dbc 100644 --- a/tests/components/zwave/test_workaround.py +++ b/tests/components/zwave/test_workaround.py @@ -1,5 +1,6 @@ """Test Z-Wave workarounds.""" from homeassistant.components.zwave import const, workaround + from tests.mock.zwave import MockNode, MockValue From 3df40c7a16ef49377a342fece6f983c76c28418c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 12:30:23 +0100 Subject: [PATCH 2297/3953] Sort imports according to PEP8 for websocket_api (#29657) --- tests/components/websocket_api/conftest.py | 4 ++-- tests/components/websocket_api/test_auth.py | 11 +++++------ tests/components/websocket_api/test_commands.py | 6 +++--- tests/components/websocket_api/test_init.py | 2 +- tests/components/websocket_api/test_sensor.py | 3 ++- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py index 382de3142e87d8..65b9232821f13a 100644 --- a/tests/components/websocket_api/conftest.py +++ b/tests/components/websocket_api/conftest.py @@ -1,9 +1,9 @@ """Fixtures for websocket tests.""" import pytest -from homeassistant.setup import async_setup_component -from homeassistant.components.websocket_api.http import URL from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED +from homeassistant.components.websocket_api.http import URL +from homeassistant.setup import async_setup_component @pytest.fixture diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 0038750602024c..ccc033ccc72812 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -1,18 +1,17 @@ """Test auth of websocket API.""" from unittest.mock import patch -from homeassistant.components.websocket_api.const import ( - URL, - SIGNAL_WEBSOCKET_CONNECTED, - SIGNAL_WEBSOCKET_DISCONNECTED, -) from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_INVALID, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED, ) - +from homeassistant.components.websocket_api.const import ( + SIGNAL_WEBSOCKET_CONNECTED, + SIGNAL_WEBSOCKET_DISCONNECTED, + URL, +) from homeassistant.setup import async_setup_component from tests.common import mock_coro diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 1de5b8bb2c1bb0..58d904c8f4b08b 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1,14 +1,14 @@ """Tests for WebSocket API commands.""" from async_timeout import timeout -from homeassistant.core import callback -from homeassistant.components.websocket_api.const import URL +from homeassistant.components.websocket_api import const from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, TYPE_AUTH_REQUIRED, ) -from homeassistant.components.websocket_api import const +from homeassistant.components.websocket_api.const import URL +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index ed3319ed4f4a0f..7cda3200e7b170 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -1,6 +1,6 @@ """Tests for the Home Assistant Websocket API.""" import asyncio -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch from aiohttp import WSMsgType import pytest diff --git a/tests/components/websocket_api/test_sensor.py b/tests/components/websocket_api/test_sensor.py index 84b730606987a7..2c7117378516c2 100644 --- a/tests/components/websocket_api/test_sensor.py +++ b/tests/components/websocket_api/test_sensor.py @@ -2,9 +2,10 @@ from homeassistant.bootstrap import async_setup_component -from tests.common import assert_setup_component from .test_auth import test_auth_active_with_token +from tests.common import assert_setup_component + async def test_websocket_api( hass, no_auth_websocket_client, hass_access_token, legacy_auth From e9b428781b342b9aeb3083d2e024d7dc8e4eb2c2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:06:25 +0100 Subject: [PATCH 2298/3953] Sort imports according to PEP8 for pushbullet (#29748) --- homeassistant/components/pushbullet/notify.py | 9 +++------ homeassistant/components/pushbullet/sensor.py | 6 ++---- tests/components/pushbullet/test_notify.py | 3 ++- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py index 76c1e14e5a5188..28cb08cc69c480 100644 --- a/homeassistant/components/pushbullet/notify.py +++ b/homeassistant/components/pushbullet/notify.py @@ -2,14 +2,9 @@ import logging import mimetypes -from pushbullet import PushBullet -from pushbullet import InvalidKeyError -from pushbullet import PushError +from pushbullet import InvalidKeyError, PushBullet, PushError import voluptuous as vol -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -18,6 +13,8 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 600b38b6eaf4d4..771ba55586cd8b 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -2,13 +2,11 @@ import logging import threading -from pushbullet import PushBullet -from pushbullet import InvalidKeyError -from pushbullet import Listener +from pushbullet import InvalidKeyError, Listener, PushBullet import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/tests/components/pushbullet/test_notify.py b/tests/components/pushbullet/test_notify.py index d95a942d25b66f..4c731c1f704e67 100644 --- a/tests/components/pushbullet/test_notify.py +++ b/tests/components/pushbullet/test_notify.py @@ -6,8 +6,9 @@ from pushbullet import PushBullet import requests_mock -from homeassistant.setup import setup_component import homeassistant.components.notify as notify +from homeassistant.setup import setup_component + from tests.common import assert_setup_component, get_test_home_assistant, load_fixture From c4a6f265e8570c8f84b5c7db08e4b6cd0dbe04b5 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:08:05 +0100 Subject: [PATCH 2299/3953] Sort imports according to PEP8 for versasense (#29753) --- homeassistant/components/versasense/__init__.py | 10 +++++----- homeassistant/components/versasense/sensor.py | 6 +++--- homeassistant/components/versasense/switch.py | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/versasense/__init__.py b/homeassistant/components/versasense/__init__.py index 4f378f4ab00c9e..d2081d715d50ee 100644 --- a/homeassistant/components/versasense/__init__.py +++ b/homeassistant/components/versasense/__init__.py @@ -10,14 +10,14 @@ from homeassistant.helpers.discovery import async_load_platform from .const import ( - PERIPHERAL_CLASS_SENSOR, - PERIPHERAL_CLASS_SENSOR_ACTUATOR, + KEY_CONSUMER, KEY_IDENTIFIER, - KEY_PARENT_NAME, + KEY_MEASUREMENT, KEY_PARENT_MAC, + KEY_PARENT_NAME, KEY_UNIT, - KEY_MEASUREMENT, - KEY_CONSUMER, + PERIPHERAL_CLASS_SENSOR, + PERIPHERAL_CLASS_SENSOR_ACTUATOR, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py index 4253bfcbba4cda..e598093cd37164 100644 --- a/homeassistant/components/versasense/sensor.py +++ b/homeassistant/components/versasense/sensor.py @@ -5,12 +5,12 @@ from . import DOMAIN from .const import ( + KEY_CONSUMER, KEY_IDENTIFIER, - KEY_PARENT_NAME, + KEY_MEASUREMENT, KEY_PARENT_MAC, + KEY_PARENT_NAME, KEY_UNIT, - KEY_MEASUREMENT, - KEY_CONSUMER, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/versasense/switch.py b/homeassistant/components/versasense/switch.py index 4ea118a6c97c38..4b44cb7aa2a577 100644 --- a/homeassistant/components/versasense/switch.py +++ b/homeassistant/components/versasense/switch.py @@ -5,14 +5,14 @@ from . import DOMAIN from .const import ( - PERIPHERAL_STATE_ON, - PERIPHERAL_STATE_OFF, + KEY_CONSUMER, KEY_IDENTIFIER, - KEY_PARENT_NAME, + KEY_MEASUREMENT, KEY_PARENT_MAC, + KEY_PARENT_NAME, KEY_UNIT, - KEY_MEASUREMENT, - KEY_CONSUMER, + PERIPHERAL_STATE_OFF, + PERIPHERAL_STATE_ON, ) _LOGGER = logging.getLogger(__name__) From f81e608cc14a728d28c080bf2e1dd5879565f95f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:09:16 +0100 Subject: [PATCH 2300/3953] Sort imports according to PEP8 for remote (#29749) --- homeassistant/components/remote/__init__.py | 19 +++++++++---------- tests/components/remote/common.py | 2 +- tests/components/remote/test_init.py | 10 +++++----- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index af653165ee387d..b88629ea46861a 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -5,23 +5,22 @@ import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity -import homeassistant.helpers.config_validation as cv +from homeassistant.components import group from homeassistant.const import ( - STATE_ON, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, ) -from homeassistant.components import group +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + make_entity_service_schema, ) - +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/tests/components/remote/common.py b/tests/components/remote/common.py index d972640487aa15..1f4a5268440f34 100644 --- a/tests/components/remote/common.py +++ b/tests/components/remote/common.py @@ -17,9 +17,9 @@ ) from homeassistant.const import ( ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, SERVICE_TURN_OFF, SERVICE_TURN_ON, - ENTITY_MATCH_ALL, ) from homeassistant.loader import bind_hass diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py index 723f38baced453..392f0e6fa613dc 100644 --- a/tests/components/remote/test_init.py +++ b/tests/components/remote/test_init.py @@ -3,17 +3,17 @@ import unittest +import homeassistant.components.remote as remote from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, CONF_PLATFORM, - SERVICE_TURN_ON, SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) -import homeassistant.components.remote as remote -from tests.common import mock_service, get_test_home_assistant +from tests.common import get_test_home_assistant, mock_service from tests.components.remote import common TEST_PLATFORM = {remote.DOMAIN: {CONF_PLATFORM: "test"}} From 3f469eac28ed6ab98e7090ef1a2ccca334709880 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:10:24 +0100 Subject: [PATCH 2301/3953] Sort imports according to PEP8 for yeelight (#29755) --- homeassistant/components/yeelight/__init__.py | 15 ++--- .../components/yeelight/binary_sensor.py | 3 +- homeassistant/components/yeelight/light.py | 63 ++++++++++--------- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index c899c811a47d35..ddd3cf2a0535da 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -1,24 +1,25 @@ """Support for Xiaomi Yeelight WiFi color bulb.""" -import logging from datetime import timedelta +import logging import voluptuous as vol from yeelight import Bulb, BulbException + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.discovery import SERVICE_YEELIGHT +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_DEVICES, + CONF_HOST, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_HOST, - ATTR_ENTITY_ID, ) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.helpers import discovery -from homeassistant.helpers.discovery import load_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send, dispatcher_connect +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import dispatcher_connect, dispatcher_send from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index da39152e9cae63..29e24b510e5520 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -4,7 +4,8 @@ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_YEELIGHT, DATA_UPDATED + +from . import DATA_UPDATED, DATA_YEELIGHT _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 772fb00977baac..3e98403587f20f 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -4,60 +4,61 @@ import voluptuous as vol import yeelight from yeelight import ( + BulbException, + Flow, RGBTransition, SleepTransition, - Flow, - BulbException, transitions as yee_transitions, ) -from yeelight.enums import PowerMode, LightType, BulbType, SceneClass +from yeelight.enums import BulbType, LightType, PowerMode, SceneClass -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.service import extract_entity_ids -import homeassistant.helpers.config_validation as cv -from homeassistant.util.color import ( - color_temperature_mired_to_kelvin as mired_to_kelvin, - color_temperature_kelvin_to_mired as kelvin_to_mired, -) -from homeassistant.const import CONF_HOST, ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME -from homeassistant.core import callback from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_TRANSITION, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_FLASH, - FLASH_SHORT, + ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, FLASH_LONG, - ATTR_EFFECT, + FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, - SUPPORT_FLASH, SUPPORT_EFFECT, + SUPPORT_FLASH, + SUPPORT_TRANSITION, Light, - ATTR_RGB_COLOR, - ATTR_KELVIN, ) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, CONF_HOST, CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.service import extract_entity_ids import homeassistant.util.color as color_util +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired as kelvin_to_mired, + color_temperature_mired_to_kelvin as mired_to_kelvin, +) + from . import ( - CONF_TRANSITION, - DATA_YEELIGHT, + ACTION_RECOVER, + ATTR_ACTION, + ATTR_COUNT, + ATTR_TRANSITIONS, + CONF_CUSTOM_EFFECTS, + CONF_FLOW_PARAMS, CONF_MODE_MUSIC, + CONF_NIGHTLIGHT_SWITCH_TYPE, CONF_SAVE_ON_CHANGE, - CONF_CUSTOM_EFFECTS, + CONF_TRANSITION, DATA_UPDATED, - YEELIGHT_SERVICE_SCHEMA, + DATA_YEELIGHT, DOMAIN, - ATTR_TRANSITIONS, - YEELIGHT_FLOW_TRANSITION_SCHEMA, - ACTION_RECOVER, - CONF_FLOW_PARAMS, - ATTR_ACTION, - ATTR_COUNT, NIGHTLIGHT_SWITCH_TYPE_LIGHT, - CONF_NIGHTLIGHT_SWITCH_TYPE, + YEELIGHT_FLOW_TRANSITION_SCHEMA, + YEELIGHT_SERVICE_SCHEMA, ) _LOGGER = logging.getLogger(__name__) From 127d84edd1101e1216cd6c5a40735f5095419ff3 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:11:37 +0100 Subject: [PATCH 2302/3953] Sort imports according to PEP8 for solarlog (#29752) --- homeassistant/components/solarlog/const.py | 2 +- homeassistant/components/solarlog/sensor.py | 6 +++--- tests/components/solarlog/test_config_flow.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index 67eb8006cec90c..933f801409061a 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -1,7 +1,7 @@ """Constants for the Solar-Log integration.""" from datetime import timedelta -from homeassistant.const import POWER_WATT, ENERGY_KILO_WATT_HOUR +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT DOMAIN = "solarlog" diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 583529ffe87579..85ab9eb913e9d5 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -6,14 +6,14 @@ from sunwatcher.solarlog.solarlog import SolarLog import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import DOMAIN, DEFAULT_HOST, DEFAULT_NAME, SCAN_INTERVAL, SENSOR_TYPES +from .const import DEFAULT_HOST, DEFAULT_NAME, DOMAIN, SCAN_INTERVAL, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 86f3b05d9755d0..cd05cf13185d49 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -1,9 +1,9 @@ """Test the solarlog config flow.""" from unittest.mock import patch + import pytest -from homeassistant import data_entry_flow -from homeassistant import config_entries, setup +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.solarlog import config_flow from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME From 76debf4c8834e393d0beb031004199f01a84f76f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:12:43 +0100 Subject: [PATCH 2303/3953] Sort imports according to PEP8 for scene (#29750) --- homeassistant/components/scene/__init__.py | 3 +-- tests/components/scene/common.py | 2 +- tests/components/scene/test_init.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 63a64f34fe90bb..75ec2bfd87546c 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -4,12 +4,11 @@ import voluptuous as vol -from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON +from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent - # mypy: allow-untyped-defs, no-check-untyped-defs DOMAIN = "scene" diff --git a/tests/components/scene/common.py b/tests/components/scene/common.py index 5da0cc21db0838..cdf124add29e35 100644 --- a/tests/components/scene/common.py +++ b/tests/components/scene/common.py @@ -4,7 +4,7 @@ components. Instead call the service directly. """ from homeassistant.components.scene import DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, ENTITY_MATCH_ALL +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_ON from homeassistant.loader import bind_hass diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 5c8d46cb727753..f26189eec6c92a 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -2,8 +2,8 @@ import io import unittest -from homeassistant.setup import setup_component from homeassistant.components import light, scene +from homeassistant.setup import setup_component from homeassistant.util.yaml import loader as yaml_loader from tests.common import get_test_home_assistant From f281069c8cb8d256dbde40b0dcbbc77372a06749 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:13:33 +0100 Subject: [PATCH 2304/3953] Sort imports according to PEP8 for vicare (#29754) --- homeassistant/components/vicare/__init__.py | 7 ++--- homeassistant/components/vicare/climate.py | 31 ++++++++++--------- .../components/vicare/water_heater.py | 8 ++--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index e091ff99970a50..282e234811ada4 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -2,15 +2,14 @@ import enum import logging -import voluptuous as vol - from PyViCare.PyViCareDevice import Device from PyViCare.PyViCareGazBoiler import GazBoiler from PyViCare.PyViCareHeatPump import HeatPump +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 4f6f0cedcd98ee..1b101cc7612527 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -1,26 +1,29 @@ """Viessmann ViCare climate device.""" import logging + import requests from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE, - PRESET_ECO, - PRESET_COMFORT, - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_AUTO, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_COMFORT, + PRESET_ECO, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS + +from . import ( + DOMAIN as VICARE_DOMAIN, + VICARE_API, + VICARE_HEATING_TYPE, + VICARE_NAME, + HeatingType, ) -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE - -from . import DOMAIN as VICARE_DOMAIN -from . import VICARE_API -from . import VICARE_NAME -from . import VICARE_HEATING_TYPE -from . import HeatingType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index eefacf99c39674..f31e4f6517096b 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,17 +1,15 @@ """Viessmann ViCare water_heater device.""" import logging + import requests from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, WaterHeaterDevice, ) -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS -from . import DOMAIN as VICARE_DOMAIN -from . import VICARE_API -from . import VICARE_NAME -from . import VICARE_HEATING_TYPE +from . import DOMAIN as VICARE_DOMAIN, VICARE_API, VICARE_HEATING_TYPE, VICARE_NAME _LOGGER = logging.getLogger(__name__) From d1b38c0c7992ed41c589aff89e17ee1976dec0d4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:14:08 +0100 Subject: [PATCH 2305/3953] Sort imports according to PEP8 for plaato (#29747) --- homeassistant/components/plaato/__init__.py | 1 + homeassistant/components/plaato/config_flow.py | 1 + homeassistant/components/plaato/sensor.py | 6 ++++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 49b749b8de6f54..0dd57f758122dc 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -15,6 +15,7 @@ ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 59cb270c616af4..3c616c822fb984 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -1,5 +1,6 @@ """Config flow for GPSLogger.""" from homeassistant.helpers import config_entry_flow + from .const import DOMAIN config_entry_flow.register_webhook_flow( diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index f8e6a3e9fa7b48..e7c8033f2ac578 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -2,8 +2,10 @@ import logging -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from . import ( From 991834f337cbdc4e8420cfb89b23c9d6d7dd07ba Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:14:49 +0100 Subject: [PATCH 2306/3953] Sort imports according to PEP8 for shopping_list (#29751) --- homeassistant/components/shopping_list/__init__.py | 7 +++---- tests/components/shopping_list/conftest.py | 2 +- tests/components/shopping_list/test_init.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 850b06332f8899..856ea0784ba866 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -5,13 +5,12 @@ import voluptuous as vol -from homeassistant.const import HTTP_NOT_FOUND, HTTP_BAD_REQUEST -from homeassistant.core import callback -from homeassistant.components import http +from homeassistant.components import http, websocket_api from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.const import HTTP_BAD_REQUEST, HTTP_NOT_FOUND +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json -from homeassistant.components import websocket_api ATTR_NAME = "name" diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py index 646b3bee4c01c9..5026f19302e4ec 100644 --- a/tests/components/shopping_list/conftest.py +++ b/tests/components/shopping_list/conftest.py @@ -3,8 +3,8 @@ import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.shopping_list import intent as sl_intent +from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index 4394a835f494c3..74c354848a3282 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,8 +1,8 @@ """Test shopping list component.""" import asyncio -from homeassistant.helpers import intent from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.helpers import intent @asyncio.coroutine From 60e1789557001cf4c713eacfebf359a20c617d6b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:20:51 +0100 Subject: [PATCH 2307/3953] Sort imports according to PEP8 for emulated_roku (#29756) --- tests/components/emulated_roku/test_binding.py | 10 +++++----- tests/components/emulated_roku/test_config_flow.py | 1 + tests/components/emulated_roku/test_init.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 712c35f5a104ef..53b6217fcbccaa 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -2,16 +2,16 @@ from unittest.mock import Mock, patch from homeassistant.components.emulated_roku.binding import ( - EmulatedRoku, - EVENT_ROKU_COMMAND, - ATTR_SOURCE_NAME, + ATTR_APP_ID, ATTR_COMMAND_TYPE, ATTR_KEY, - ATTR_APP_ID, - ROKU_COMMAND_KEYPRESS, + ATTR_SOURCE_NAME, + EVENT_ROKU_COMMAND, ROKU_COMMAND_KEYDOWN, + ROKU_COMMAND_KEYPRESS, ROKU_COMMAND_KEYUP, ROKU_COMMAND_LAUNCH, + EmulatedRoku, ) from tests.common import mock_coro_func diff --git a/tests/components/emulated_roku/test_config_flow.py b/tests/components/emulated_roku/test_config_flow.py index 3cb25f5a7fce9f..879d95d0cfc671 100644 --- a/tests/components/emulated_roku/test_config_flow.py +++ b/tests/components/emulated_roku/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for emulated_roku config flow.""" from homeassistant.components.emulated_roku import config_flow + from tests.common import MockConfigEntry diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index 92524f24d97a08..efdf330a87655b 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -1,8 +1,8 @@ """Test emulated_roku component setup process.""" from unittest.mock import Mock, patch -from homeassistant.setup import async_setup_component from homeassistant.components import emulated_roku +from homeassistant.setup import async_setup_component from tests.common import mock_coro_func From 53012a548b6f908e603b70405db499582949fe48 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:21:45 +0100 Subject: [PATCH 2308/3953] Sort imports according to PEP8 for sleepiq (#29759) --- tests/components/sleepiq/test_binary_sensor.py | 4 ++-- tests/components/sleepiq/test_init.py | 2 +- tests/components/sleepiq/test_sensor.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py index a3bf31134c5a6e..b8c3a2cd2e8dc4 100644 --- a/tests/components/sleepiq/test_binary_sensor.py +++ b/tests/components/sleepiq/test_binary_sensor.py @@ -4,11 +4,11 @@ import requests_mock -from homeassistant.setup import setup_component from homeassistant.components.sleepiq import binary_sensor as sleepiq +from homeassistant.setup import setup_component -from tests.components.sleepiq.test_init import mock_responses from tests.common import get_test_home_assistant +from tests.components.sleepiq.test_init import mock_responses class TestSleepIQBinarySensorSetup(unittest.TestCase): diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index a418253d40909d..67fe19da45a707 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -7,7 +7,7 @@ from homeassistant import setup import homeassistant.components.sleepiq as sleepiq -from tests.common import load_fixture, get_test_home_assistant +from tests.common import get_test_home_assistant, load_fixture def mock_responses(mock, single=False): diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index 9dbba3a8b0a4e3..a049dfd2fbf663 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -4,11 +4,11 @@ import requests_mock -from homeassistant.setup import setup_component import homeassistant.components.sleepiq.sensor as sleepiq +from homeassistant.setup import setup_component -from tests.components.sleepiq.test_init import mock_responses from tests.common import get_test_home_assistant +from tests.components.sleepiq.test_init import mock_responses class TestSleepIQSensorSetup(unittest.TestCase): From abfcc18004c5ae99a5c9cfb7501b9c8d4d551e03 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:22:42 +0100 Subject: [PATCH 2309/3953] Sort imports according to PEP8 for mobile_app (#29758) --- tests/components/mobile_app/conftest.py | 7 +++---- tests/components/mobile_app/test_notify.py | 3 +-- tests/components/mobile_app/test_webhook.py | 5 +++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index 1d653b73ba3a6c..cd819a9891c549 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -2,14 +2,13 @@ # pylint: disable=redefined-outer-name,unused-import import pytest -from tests.common import mock_device_registry - -from homeassistant.setup import async_setup_component - from homeassistant.components.mobile_app.const import DOMAIN +from homeassistant.setup import async_setup_component from .const import REGISTER, REGISTER_CLEARTEXT +from tests.common import mock_device_registry + @pytest.fixture def registry(hass): diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py index 83a2a5e0766f38..860f3d9f81ff87 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -2,9 +2,8 @@ # pylint: disable=redefined-outer-name import pytest -from homeassistant.setup import async_setup_component - from homeassistant.components.mobile_app.const import DOMAIN +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 6e8efe15dd0d2e..6a41b5f054dd0a 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,6 +1,7 @@ """Webhook tests for mobile_app.""" import logging + import pytest from homeassistant.components.mobile_app.const import CONF_SECRET @@ -9,10 +10,10 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component -from tests.common import async_mock_service - from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE +from tests.common import async_mock_service + _LOGGER = logging.getLogger(__name__) From c7cf1b820c83fd00fd88cf393ffba8e51d14b99a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:25:04 +0100 Subject: [PATCH 2310/3953] Sort imports according to PEP8 for hue (#29757) --- tests/components/hue/test_bridge.py | 2 +- tests/components/hue/test_init.py | 4 ++-- tests/components/hue/test_light.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 7265b4687143c6..b66733e7c76a91 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -3,8 +3,8 @@ import pytest -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.components.hue import bridge, errors +from homeassistant.exceptions import ConfigEntryNotReady from tests.common import mock_coro diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index f6ff112cc37d1b..58f004ec540225 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -1,10 +1,10 @@ """Test Hue setup process.""" from unittest.mock import Mock, patch -from homeassistant.setup import async_setup_component from homeassistant.components import hue +from homeassistant.setup import async_setup_component -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_setup_with_no_config(hass): diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 5b10ff9446cdd7..c218e729255129 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -5,8 +5,8 @@ from unittest.mock import Mock import aiohue -from aiohue.lights import Lights from aiohue.groups import Groups +from aiohue.lights import Lights import pytest from homeassistant import config_entries From 9b27e5b86cdd6132fe66ec0421a002d04803cbd2 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:33:08 +0100 Subject: [PATCH 2311/3953] Sort imports according to PEP8 for vultr (#29760) --- tests/components/vultr/test_binary_sensor.py | 10 +++++----- tests/components/vultr/test_sensor.py | 6 +++--- tests/components/vultr/test_switch.py | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/components/vultr/test_binary_sensor.py b/tests/components/vultr/test_binary_sensor.py index e59c7b4e46c29b..f57926f30c843c 100644 --- a/tests/components/vultr/test_binary_sensor.py +++ b/tests/components/vultr/test_binary_sensor.py @@ -3,25 +3,25 @@ import unittest from unittest.mock import patch -import requests_mock import pytest +import requests_mock import voluptuous as vol -from homeassistant.components.vultr import binary_sensor as vultr from homeassistant.components import vultr as base_vultr from homeassistant.components.vultr import ( ATTR_ALLOWED_BANDWIDTH, ATTR_AUTO_BACKUPS, - ATTR_IPV4_ADDRESS, ATTR_COST_PER_MONTH, ATTR_CREATED_AT, + ATTR_IPV4_ADDRESS, ATTR_SUBSCRIPTION_ID, CONF_SUBSCRIPTION, + binary_sensor as vultr, ) -from homeassistant.const import CONF_PLATFORM, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PLATFORM -from tests.components.vultr.test_init import VALID_CONFIG from tests.common import get_test_home_assistant, load_fixture +from tests.components.vultr.test_init import VALID_CONFIG class TestVultrBinarySensorSetup(unittest.TestCase): diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 6e2969dd2e0bd3..4da60783c44cc1 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -7,13 +7,13 @@ import requests_mock import voluptuous as vol -import homeassistant.components.vultr.sensor as vultr from homeassistant.components import vultr as base_vultr from homeassistant.components.vultr import CONF_SUBSCRIPTION -from homeassistant.const import CONF_NAME, CONF_MONITORED_CONDITIONS, CONF_PLATFORM +import homeassistant.components.vultr.sensor as vultr +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PLATFORM -from tests.components.vultr.test_init import VALID_CONFIG from tests.common import get_test_home_assistant, load_fixture +from tests.components.vultr.test_init import VALID_CONFIG class TestVultrSensorSetup(unittest.TestCase): diff --git a/tests/components/vultr/test_switch.py b/tests/components/vultr/test_switch.py index 0d5055cd6a56d5..4f61291c4add9f 100644 --- a/tests/components/vultr/test_switch.py +++ b/tests/components/vultr/test_switch.py @@ -3,25 +3,25 @@ import unittest from unittest.mock import patch -import requests_mock import pytest +import requests_mock import voluptuous as vol -from homeassistant.components.vultr import switch as vultr from homeassistant.components import vultr as base_vultr from homeassistant.components.vultr import ( ATTR_ALLOWED_BANDWIDTH, ATTR_AUTO_BACKUPS, - ATTR_IPV4_ADDRESS, ATTR_COST_PER_MONTH, ATTR_CREATED_AT, + ATTR_IPV4_ADDRESS, ATTR_SUBSCRIPTION_ID, CONF_SUBSCRIPTION, + switch as vultr, ) -from homeassistant.const import CONF_PLATFORM, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PLATFORM -from tests.components.vultr.test_init import VALID_CONFIG from tests.common import get_test_home_assistant, load_fixture +from tests.components.vultr.test_init import VALID_CONFIG class TestVultrSwitchSetup(unittest.TestCase): From 4bb670cdf7b43e8b0d5c3a782bcce61b22fdb06a Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 9 Dec 2019 13:33:28 +0100 Subject: [PATCH 2312/3953] HomeAssistant-pyozw 0.1.7 (#29743) --- homeassistant/components/zwave/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index 78362b25462e9b..c781a493b55088 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", - "requirements": ["homeassistant-pyozw==0.1.6", "pydispatcher==2.0.5"], + "requirements": ["homeassistant-pyozw==0.1.7", "pydispatcher==2.0.5"], "dependencies": [], "codeowners": ["@home-assistant/z-wave"] } diff --git a/requirements_all.txt b/requirements_all.txt index e5983972243952..23449f1a59c1f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -670,7 +670,7 @@ holidays==0.9.11 home-assistant-frontend==20191204.0 # homeassistant.components.zwave -homeassistant-pyozw==0.1.6 +homeassistant-pyozw==0.1.7 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 405e632c0ccb02..9f30a3cfcc7580 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -234,7 +234,7 @@ holidays==0.9.11 home-assistant-frontend==20191204.0 # homeassistant.components.zwave -homeassistant-pyozw==0.1.6 +homeassistant-pyozw==0.1.7 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From df74272ba6311d527fd07198929c80a45d9fed15 Mon Sep 17 00:00:00 2001 From: tetienne Date: Mon, 9 Dec 2019 13:35:14 +0100 Subject: [PATCH 2313/3953] Remove Tahoma component #29744 (#29745) --- CODEOWNERS | 1 - homeassistant/components/tahoma/__init__.py | 140 ---------- .../components/tahoma/binary_sensor.py | 95 ------- homeassistant/components/tahoma/cover.py | 249 ------------------ homeassistant/components/tahoma/manifest.json | 12 - homeassistant/components/tahoma/scene.py | 41 --- homeassistant/components/tahoma/sensor.py | 106 -------- homeassistant/components/tahoma/switch.py | 109 -------- requirements_all.txt | 3 - 9 files changed, 756 deletions(-) delete mode 100644 homeassistant/components/tahoma/__init__.py delete mode 100644 homeassistant/components/tahoma/binary_sensor.py delete mode 100644 homeassistant/components/tahoma/cover.py delete mode 100644 homeassistant/components/tahoma/manifest.json delete mode 100644 homeassistant/components/tahoma/scene.py delete mode 100644 homeassistant/components/tahoma/sensor.py delete mode 100644 homeassistant/components/tahoma/switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 6723244c089ef6..afea92c8847b40 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -317,7 +317,6 @@ homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff homeassistant/components/tado/* @michaelarnauts -homeassistant/components/tahoma/* @philklei homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike homeassistant/components/template/* @PhracturedBlue diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py deleted file mode 100644 index 640cc6418d0f01..00000000000000 --- a/homeassistant/components/tahoma/__init__.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Support for Tahoma devices.""" -from collections import defaultdict -import logging - -from requests.exceptions import RequestException -from tahoma_api import Action, TahomaApi -import voluptuous as vol - -from homeassistant.const import CONF_EXCLUDE, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.entity import Entity - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "tahoma" - -TAHOMA_ID_FORMAT = "{}_{}" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_EXCLUDE, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -TAHOMA_COMPONENTS = ["scene", "sensor", "cover", "switch", "binary_sensor"] - -TAHOMA_TYPES = { - "io:ExteriorVenetianBlindIOComponent": "cover", - "io:HorizontalAwningIOComponent": "cover", - "io:LightIOSystemSensor": "sensor", - "io:OnOffIOComponent": "switch", - "io:OnOffLightIOComponent": "switch", - "io:RollerShutterGenericIOComponent": "cover", - "io:RollerShutterUnoIOComponent": "cover", - "io:RollerShutterVeluxIOComponent": "cover", - "io:RollerShutterWithLowSpeedManagementIOComponent": "cover", - "io:SomfyBasicContactIOSystemSensor": "sensor", - "io:SomfyContactIOSystemSensor": "sensor", - "io:VerticalExteriorAwningIOComponent": "cover", - "io:VerticalInteriorBlindVeluxIOComponent": "cover", - "io:WindowOpenerVeluxIOComponent": "cover", - "io:GarageOpenerIOComponent": "cover", - "io:DiscreteGarageOpenerIOComponent": "cover", - "rtds:RTDSContactSensor": "sensor", - "rtds:RTDSMotionSensor": "sensor", - "rtds:RTDSSmokeSensor": "smoke", - "rts:BlindRTSComponent": "cover", - "rts:CurtainRTSComponent": "cover", - "rts:DualCurtainRTSComponent": "cover", - "rts:ExteriorVenetianBlindRTSComponent": "cover", - "rts:GarageDoor4TRTSComponent": "switch", - "rts:RollerShutterRTSComponent": "cover", - "rts:VenetianBlindRTSComponent": "cover", -} - - -def setup(hass, config): - """Activate Tahoma component.""" - - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - exclude = conf.get(CONF_EXCLUDE) - try: - api = TahomaApi(username, password) - except RequestException: - _LOGGER.exception("Error when trying to log in to the Tahoma API") - return False - - try: - api.get_setup() - devices = api.get_devices() - scenes = api.get_action_groups() - except RequestException: - _LOGGER.exception("Error when getting devices from the Tahoma API") - return False - - hass.data[DOMAIN] = {"controller": api, "devices": defaultdict(list), "scenes": []} - - for device in devices: - _device = api.get_device(device) - if all(ext not in _device.type for ext in exclude): - device_type = map_tahoma_device(_device) - if device_type is None: - _LOGGER.warning( - "Unsupported type %s for Tahoma device %s", - _device.type, - _device.label, - ) - continue - hass.data[DOMAIN]["devices"][device_type].append(_device) - - for scene in scenes: - hass.data[DOMAIN]["scenes"].append(scene) - - for component in TAHOMA_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) - - return True - - -def map_tahoma_device(tahoma_device): - """Map Tahoma device types to Home Assistant components.""" - return TAHOMA_TYPES.get(tahoma_device.type) - - -class TahomaDevice(Entity): - """Representation of a Tahoma device entity.""" - - def __init__(self, tahoma_device, controller): - """Initialize the device.""" - self.tahoma_device = tahoma_device - self.controller = controller - self._name = self.tahoma_device.label - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def device_state_attributes(self): - """Return the state attributes of the device.""" - return {"tahoma_device_id": self.tahoma_device.url} - - def apply_action(self, cmd_name, *args): - """Apply Action to Device.""" - - action = Action(self.tahoma_device.url) - action.add_command(cmd_name, *args) - self.controller.apply_actions("HomeAssistant", [action]) diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py deleted file mode 100644 index 81078ab480babd..00000000000000 --- a/homeassistant/components/tahoma/binary_sensor.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Support for Tahoma binary sensors.""" -from datetime import timedelta -import logging - -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON - -from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=120) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tahoma controller devices.""" - _LOGGER.debug("Setup Tahoma Binary sensor platform") - controller = hass.data[TAHOMA_DOMAIN]["controller"] - devices = [] - for device in hass.data[TAHOMA_DOMAIN]["devices"]["smoke"]: - devices.append(TahomaBinarySensor(device, controller)) - add_entities(devices, True) - - -class TahomaBinarySensor(TahomaDevice, BinarySensorDevice): - """Representation of a Tahoma Binary Sensor.""" - - def __init__(self, tahoma_device, controller): - """Initialize the sensor.""" - super().__init__(tahoma_device, controller) - - self._state = None - self._icon = None - self._battery = None - self._available = False - - @property - def is_on(self): - """Return the state of the sensor.""" - return bool(self._state == STATE_ON) - - @property - def device_class(self): - """Return the class of the device.""" - if self.tahoma_device.type == "rtds:RTDSSmokeSensor": - return "smoke" - return None - - @property - def icon(self): - """Icon for device by its type.""" - return self._icon - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - attr = {} - super_attr = super().device_state_attributes - if super_attr is not None: - attr.update(super_attr) - - if self._battery is not None: - attr[ATTR_BATTERY_LEVEL] = self._battery - return attr - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - def update(self): - """Update the state.""" - self.controller.get_states([self.tahoma_device]) - if self.tahoma_device.type == "rtds:RTDSSmokeSensor": - if self.tahoma_device.active_states["core:SmokeState"] == "notDetected": - self._state = STATE_OFF - else: - self._state = STATE_ON - - if "core:SensorDefectState" in self.tahoma_device.active_states: - # 'lowBattery' for low battery warning. 'dead' for not available. - self._battery = self.tahoma_device.active_states["core:SensorDefectState"] - self._available = bool(self._battery != "dead") - else: - self._battery = None - self._available = True - - if self._state == STATE_ON: - self._icon = "mdi:fire" - elif self._battery == "lowBattery": - self._icon = "mdi:battery-alert" - else: - self._icon = None - - _LOGGER.debug("Update %s, state: %s", self._name, self._state) diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py deleted file mode 100644 index e11c2f4cdf59ce..00000000000000 --- a/homeassistant/components/tahoma/cover.py +++ /dev/null @@ -1,249 +0,0 @@ -"""Support for Tahoma cover - shutters etc.""" -from datetime import timedelta -import logging - -from homeassistant.components.cover import ( - ATTR_POSITION, - DEVICE_CLASS_AWNING, - DEVICE_CLASS_BLIND, - DEVICE_CLASS_CURTAIN, - DEVICE_CLASS_GARAGE, - DEVICE_CLASS_SHUTTER, - DEVICE_CLASS_WINDOW, - CoverDevice, -) -from homeassistant.util.dt import utcnow - -from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice - -_LOGGER = logging.getLogger(__name__) - -ATTR_MEM_POS = "memorized_position" -ATTR_RSSI_LEVEL = "rssi_level" -ATTR_LOCK_START_TS = "lock_start_ts" -ATTR_LOCK_END_TS = "lock_end_ts" -ATTR_LOCK_LEVEL = "lock_level" -ATTR_LOCK_ORIG = "lock_originator" - -HORIZONTAL_AWNING = "io:HorizontalAwningIOComponent" - -TAHOMA_DEVICE_CLASSES = { - "io:ExteriorVenetianBlindIOComponent": DEVICE_CLASS_BLIND, - HORIZONTAL_AWNING: DEVICE_CLASS_AWNING, - "io:RollerShutterGenericIOComponent": DEVICE_CLASS_SHUTTER, - "io:RollerShutterUnoIOComponent": DEVICE_CLASS_SHUTTER, - "io:RollerShutterVeluxIOComponent": DEVICE_CLASS_SHUTTER, - "io:RollerShutterWithLowSpeedManagementIOComponent": DEVICE_CLASS_SHUTTER, - "io:VerticalExteriorAwningIOComponent": DEVICE_CLASS_AWNING, - "io:VerticalInteriorBlindVeluxIOComponent": DEVICE_CLASS_BLIND, - "io:WindowOpenerVeluxIOComponent": DEVICE_CLASS_WINDOW, - "io:GarageOpenerIOComponent": DEVICE_CLASS_GARAGE, - "io:DiscreteGarageOpenerIOComponent": DEVICE_CLASS_GARAGE, - "rts:BlindRTSComponent": DEVICE_CLASS_BLIND, - "rts:CurtainRTSComponent": DEVICE_CLASS_CURTAIN, - "rts:DualCurtainRTSComponent": DEVICE_CLASS_CURTAIN, - "rts:ExteriorVenetianBlindRTSComponent": DEVICE_CLASS_BLIND, - "rts:RollerShutterRTSComponent": DEVICE_CLASS_SHUTTER, - "rts:VenetianBlindRTSComponent": DEVICE_CLASS_BLIND, -} - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tahoma covers.""" - controller = hass.data[TAHOMA_DOMAIN]["controller"] - devices = [] - for device in hass.data[TAHOMA_DOMAIN]["devices"]["cover"]: - devices.append(TahomaCover(device, controller)) - add_entities(devices, True) - - -class TahomaCover(TahomaDevice, CoverDevice): - """Representation a Tahoma Cover.""" - - def __init__(self, tahoma_device, controller): - """Initialize the device.""" - super().__init__(tahoma_device, controller) - - self._closure = 0 - # 100 equals open - self._position = 100 - self._closed = False - self._rssi_level = None - self._icon = None - # Can be 0 and bigger - self._lock_timer = 0 - self._lock_start_ts = None - self._lock_end_ts = None - # Can be 'comfortLevel1', 'comfortLevel2', 'comfortLevel3', - # 'comfortLevel4', 'environmentProtection', 'humanProtection', - # 'userLevel1', 'userLevel2' - self._lock_level = None - # Can be 'LSC', 'SAAC', 'SFC', 'UPS', 'externalGateway', 'localUser', - # 'myself', 'rain', 'security', 'temperature', 'timer', 'user', 'wind' - self._lock_originator = None - - def update(self): - """Update method.""" - self.controller.get_states([self.tahoma_device]) - - # For vertical covers - self._closure = self.tahoma_device.active_states.get("core:ClosureState") - # For horizontal covers - if self._closure is None: - self._closure = self.tahoma_device.active_states.get("core:DeploymentState") - - # For all, if available - if "core:PriorityLockTimerState" in self.tahoma_device.active_states: - old_lock_timer = self._lock_timer - self._lock_timer = self.tahoma_device.active_states[ - "core:PriorityLockTimerState" - ] - # Derive timestamps from _lock_timer, only if not already set or - # something has changed - if self._lock_timer > 0: - _LOGGER.debug("Update %s, lock_timer: %d", self._name, self._lock_timer) - if self._lock_start_ts is None: - self._lock_start_ts = utcnow() - if self._lock_end_ts is None or old_lock_timer != self._lock_timer: - self._lock_end_ts = utcnow() + timedelta(seconds=self._lock_timer) - else: - self._lock_start_ts = None - self._lock_end_ts = None - else: - self._lock_timer = 0 - self._lock_start_ts = None - self._lock_end_ts = None - - self._lock_level = self.tahoma_device.active_states.get( - "io:PriorityLockLevelState" - ) - - self._lock_originator = self.tahoma_device.active_states.get( - "io:PriorityLockOriginatorState" - ) - - self._rssi_level = self.tahoma_device.active_states.get("core:RSSILevelState") - - # Define which icon to use - if self._lock_timer > 0: - if self._lock_originator == "wind": - self._icon = "mdi:weather-windy" - else: - self._icon = "mdi:lock-alert" - else: - self._icon = None - - # Define current position. - # _position: 0 is closed, 100 is fully open. - # 'core:ClosureState': 100 is closed, 0 is fully open. - if self._closure is not None: - if self.tahoma_device.type == HORIZONTAL_AWNING: - self._position = self._closure - else: - self._position = 100 - self._closure - if self._position <= 5: - self._position = 0 - if self._position >= 95: - self._position = 100 - self._closed = self._position == 0 - else: - self._position = None - if "core:OpenClosedState" in self.tahoma_device.active_states: - self._closed = ( - self.tahoma_device.active_states["core:OpenClosedState"] == "closed" - ) - else: - self._closed = False - - _LOGGER.debug("Update %s, position: %d", self._name, self._position) - - @property - def current_cover_position(self): - """Return current position of cover.""" - return self._position - - def set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - if self.tahoma_device.type == "io:WindowOpenerVeluxIOComponent": - command = "setClosure" - else: - command = "setPosition" - - if self.tahoma_device.type == HORIZONTAL_AWNING: - self.apply_action(command, kwargs.get(ATTR_POSITION, 0)) - else: - self.apply_action(command, 100 - kwargs.get(ATTR_POSITION, 0)) - - @property - def is_closed(self): - """Return if the cover is closed.""" - return self._closed - - @property - def device_class(self): - """Return the class of the device.""" - return TAHOMA_DEVICE_CLASSES.get(self.tahoma_device.type) - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - attr = {} - super_attr = super().device_state_attributes - if super_attr is not None: - attr.update(super_attr) - - if "core:Memorized1PositionState" in self.tahoma_device.active_states: - attr[ATTR_MEM_POS] = self.tahoma_device.active_states[ - "core:Memorized1PositionState" - ] - if self._rssi_level is not None: - attr[ATTR_RSSI_LEVEL] = self._rssi_level - if self._lock_start_ts is not None: - attr[ATTR_LOCK_START_TS] = self._lock_start_ts.isoformat() - if self._lock_end_ts is not None: - attr[ATTR_LOCK_END_TS] = self._lock_end_ts.isoformat() - if self._lock_level is not None: - attr[ATTR_LOCK_LEVEL] = self._lock_level - if self._lock_originator is not None: - attr[ATTR_LOCK_ORIG] = self._lock_originator - return attr - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return self._icon - - def open_cover(self, **kwargs): - """Open the cover.""" - self.apply_action("open") - - def close_cover(self, **kwargs): - """Close the cover.""" - self.apply_action("close") - - def stop_cover(self, **kwargs): - """Stop the cover.""" - if ( - self.tahoma_device.type - == "io:RollerShutterWithLowSpeedManagementIOComponent" - ): - self.apply_action("setPosition", "secured") - elif self.tahoma_device.type in ( - "rts:BlindRTSComponent", - "io:ExteriorVenetianBlindIOComponent", - "rts:VenetianBlindRTSComponent", - "rts:DualCurtainRTSComponent", - "rts:ExteriorVenetianBlindRTSComponent", - "rts:BlindRTSComponent", - ): - self.apply_action("my") - elif self.tahoma_device.type in ( - HORIZONTAL_AWNING, - "io:RollerShutterGenericIOComponent", - "io:VerticalExteriorAwningIOComponent", - "io:VerticalInteriorBlindVeluxIOComponent", - "io:WindowOpenerVeluxIOComponent", - ): - self.apply_action("stop") - else: - self.apply_action("stopIdentify") diff --git a/homeassistant/components/tahoma/manifest.json b/homeassistant/components/tahoma/manifest.json deleted file mode 100644 index 1e99d4b288d725..00000000000000 --- a/homeassistant/components/tahoma/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "tahoma", - "name": "Tahoma", - "documentation": "https://www.home-assistant.io/integrations/tahoma", - "requirements": [ - "tahoma-api==0.0.14" - ], - "dependencies": [], - "codeowners": [ - "@philklei" - ] -} diff --git a/homeassistant/components/tahoma/scene.py b/homeassistant/components/tahoma/scene.py deleted file mode 100644 index e54ff91a0f6e90..00000000000000 --- a/homeassistant/components/tahoma/scene.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Support for Tahoma scenes.""" -import logging - -from homeassistant.components.scene import Scene - -from . import DOMAIN as TAHOMA_DOMAIN - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Tahoma scenes.""" - controller = hass.data[TAHOMA_DOMAIN]["controller"] - scenes = [] - for scene in hass.data[TAHOMA_DOMAIN]["scenes"]: - scenes.append(TahomaScene(scene, controller)) - add_entities(scenes, True) - - -class TahomaScene(Scene): - """Representation of a Tahoma scene entity.""" - - def __init__(self, tahoma_scene, controller): - """Initialize the scene.""" - self.tahoma_scene = tahoma_scene - self.controller = controller - self._name = self.tahoma_scene.name - - def activate(self): - """Activate the scene.""" - self.controller.launch_action_group(self.tahoma_scene.oid) - - @property - def name(self): - """Return the name of the scene.""" - return self._name - - @property - def device_state_attributes(self): - """Return the state attributes of the scene.""" - return {"tahoma_scene_oid": self.tahoma_scene.oid} diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py deleted file mode 100644 index 5279b160d9c540..00000000000000 --- a/homeassistant/components/tahoma/sensor.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Support for Tahoma sensors.""" -from datetime import timedelta -import logging - -from homeassistant.const import ATTR_BATTERY_LEVEL -from homeassistant.helpers.entity import Entity - -from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=60) - -ATTR_RSSI_LEVEL = "rssi_level" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tahoma controller devices.""" - controller = hass.data[TAHOMA_DOMAIN]["controller"] - devices = [] - for device in hass.data[TAHOMA_DOMAIN]["devices"]["sensor"]: - devices.append(TahomaSensor(device, controller)) - add_entities(devices, True) - - -class TahomaSensor(TahomaDevice, Entity): - """Representation of a Tahoma Sensor.""" - - def __init__(self, tahoma_device, controller): - """Initialize the sensor.""" - self.current_value = None - self._available = False - super().__init__(tahoma_device, controller) - - @property - def state(self): - """Return the name of the sensor.""" - return self.current_value - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - if self.tahoma_device.type == "Temperature Sensor": - return None - if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": - return None - if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": - return None - if self.tahoma_device.type == "io:LightIOSystemSensor": - return "lx" - if self.tahoma_device.type == "Humidity Sensor": - return "%" - if self.tahoma_device.type == "rtds:RTDSContactSensor": - return None - if self.tahoma_device.type == "rtds:RTDSMotionSensor": - return None - - def update(self): - """Update the state.""" - self.controller.get_states([self.tahoma_device]) - if self.tahoma_device.type == "io:LightIOSystemSensor": - self.current_value = self.tahoma_device.active_states["core:LuminanceState"] - self._available = bool( - self.tahoma_device.active_states.get("core:StatusState") == "available" - ) - if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": - self.current_value = self.tahoma_device.active_states["core:ContactState"] - self._available = bool( - self.tahoma_device.active_states.get("core:StatusState") == "available" - ) - if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": - self.current_value = self.tahoma_device.active_states["core:ContactState"] - self._available = bool( - self.tahoma_device.active_states.get("core:StatusState") == "available" - ) - if self.tahoma_device.type == "rtds:RTDSContactSensor": - self.current_value = self.tahoma_device.active_states["core:ContactState"] - self._available = True - if self.tahoma_device.type == "rtds:RTDSMotionSensor": - self.current_value = self.tahoma_device.active_states["core:OccupancyState"] - self._available = True - - _LOGGER.debug("Update %s, value: %d", self._name, self.current_value) - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - attr = {} - super_attr = super().device_state_attributes - if super_attr is not None: - attr.update(super_attr) - - if "core:RSSILevelState" in self.tahoma_device.active_states: - attr[ATTR_RSSI_LEVEL] = self.tahoma_device.active_states[ - "core:RSSILevelState" - ] - if "core:SensorDefectState" in self.tahoma_device.active_states: - attr[ATTR_BATTERY_LEVEL] = self.tahoma_device.active_states[ - "core:SensorDefectState" - ] - return attr - - @property - def available(self): - """Return True if entity is available.""" - return self._available diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py deleted file mode 100644 index a0a95ab47ce056..00000000000000 --- a/homeassistant/components/tahoma/switch.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Support for Tahoma switches.""" -import logging - -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_OFF, STATE_ON - -from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice - -_LOGGER = logging.getLogger(__name__) - -ATTR_RSSI_LEVEL = "rssi_level" - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Tahoma switches.""" - controller = hass.data[TAHOMA_DOMAIN]["controller"] - devices = [] - for switch in hass.data[TAHOMA_DOMAIN]["devices"]["switch"]: - devices.append(TahomaSwitch(switch, controller)) - add_entities(devices, True) - - -class TahomaSwitch(TahomaDevice, SwitchDevice): - """Representation a Tahoma Switch.""" - - def __init__(self, tahoma_device, controller): - """Initialize the switch.""" - super().__init__(tahoma_device, controller) - self._state = STATE_OFF - self._skip_update = False - self._available = False - - def update(self): - """Update method.""" - # Postpone the immediate state check for changes that take time. - if self._skip_update: - self._skip_update = False - return - - self.controller.get_states([self.tahoma_device]) - - if self.tahoma_device.type == "io:OnOffLightIOComponent": - if self.tahoma_device.active_states.get("core:OnOffState") == "on": - self._state = STATE_ON - else: - self._state = STATE_OFF - - self._available = bool( - self.tahoma_device.active_states.get("core:StatusState") == "available" - ) - - _LOGGER.debug("Update %s, state: %s", self._name, self._state) - - @property - def device_class(self): - """Return the class of the device.""" - if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": - return "garage" - return None - - def turn_on(self, **kwargs): - """Send the on command.""" - _LOGGER.debug("Turn on: %s", self._name) - if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": - self.toggle() - else: - self.apply_action("on") - self._skip_update = True - self._state = STATE_ON - - def turn_off(self, **kwargs): - """Send the off command.""" - _LOGGER.debug("Turn off: %s", self._name) - if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": - return - - self.apply_action("off") - self._skip_update = True - self._state = STATE_OFF - - def toggle(self, **kwargs): - """Click the switch.""" - self.apply_action("cycle") - - @property - def is_on(self): - """Get whether the switch is in on state.""" - if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": - return False - return bool(self._state == STATE_ON) - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - attr = {} - super_attr = super().device_state_attributes - if super_attr is not None: - attr.update(super_attr) - - if "core:RSSILevelState" in self.tahoma_device.active_states: - attr[ATTR_RSSI_LEVEL] = self.tahoma_device.active_states[ - "core:RSSILevelState" - ] - return attr - - @property - def available(self): - """Return True if entity is available.""" - return self._available diff --git a/requirements_all.txt b/requirements_all.txt index 23449f1a59c1f0..0e163f775951af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1906,9 +1906,6 @@ swisshydrodata==0.0.3 # homeassistant.components.synology_srm synology-srm==0.0.7 -# homeassistant.components.tahoma -tahoma-api==0.0.14 - # homeassistant.components.tank_utility tank_utility==1.4.0 From 73de69896b7b78e558e024d9d84c7a508c30285c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:52:18 +0100 Subject: [PATCH 2314/3953] Sort imports according to PEP8 for components starting with "B" (#29762) --- .../components/bbb_gpio/binary_sensor.py | 4 ++-- homeassistant/components/bbb_gpio/switch.py | 6 ++--- .../components/bbox/device_tracker.py | 1 - homeassistant/components/bbox/sensor.py | 11 ++++----- .../components/beewi_smartclim/sensor.py | 8 +++---- homeassistant/components/bh1750/sensor.py | 5 ++-- homeassistant/components/bizkaibus/sensor.py | 7 +++--- .../components/blackbird/media_player.py | 1 + homeassistant/components/blinkt/light.py | 6 ++--- .../components/bloomsky/binary_sensor.py | 2 +- homeassistant/components/bloomsky/sensor.py | 4 ++-- .../components/bluesound/media_player.py | 1 + .../bluetooth_le_tracker/device_tracker.py | 12 +++++----- .../bluetooth_tracker/device_tracker.py | 2 +- homeassistant/components/bme280/sensor.py | 5 ++-- homeassistant/components/bme680/sensor.py | 2 +- homeassistant/components/bom/sensor.py | 10 ++++---- .../components/brottsplatskartan/sensor.py | 1 - homeassistant/components/browser/__init__.py | 1 + .../bt_home_hub_5/device_tracker.py | 3 +-- .../components/bayesian/test_binary_sensor.py | 2 +- .../components/blackbird/test_media_player.py | 23 ++++++++++--------- tests/components/bom/test_sensor.py | 1 + 23 files changed, 58 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/bbb_gpio/binary_sensor.py b/homeassistant/components/bbb_gpio/binary_sensor.py index 105015da720a13..3ef13c117a219b 100644 --- a/homeassistant/components/bbb_gpio/binary_sensor.py +++ b/homeassistant/components/bbb_gpio/binary_sensor.py @@ -4,8 +4,8 @@ import voluptuous as vol from homeassistant.components import bbb_gpio -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bbb_gpio/switch.py b/homeassistant/components/bbb_gpio/switch.py index 45f95609758319..eb75c6f374cd57 100644 --- a/homeassistant/components/bbb_gpio/switch.py +++ b/homeassistant/components/bbb_gpio/switch.py @@ -3,11 +3,11 @@ import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.components import bbb_gpio -from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 122016ecf96d2b..8097c11eb89b92 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -5,7 +5,6 @@ from typing import List import pybbox - import voluptuous as vol from homeassistant.components.device_tracker import ( diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 7b795a8788e4ee..f5e5865f6f00f1 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -1,20 +1,19 @@ """Support for Bbox Bouygues Modem Router.""" -import logging from datetime import timedelta +import logging -import requests import pybbox - +import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, - CONF_MONITORED_VARIABLES, ATTR_ATTRIBUTION, + CONF_MONITORED_VARIABLES, + CONF_NAME, DEVICE_CLASS_TIMESTAMP, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.dt import utcnow diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 7bfa8883013724..be1697e4f88521 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -5,15 +5,15 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_NAME, CONF_MAC, - TEMP_CELSIUS, + CONF_NAME, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index cc91fa48bae443..924bfcd55076ad 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -2,14 +2,13 @@ from functools import partial import logging -import smbus # pylint: disable=import-error from i2csense.bh1750 import BH1750 # pylint: disable=import-error - +import smbus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index c54a61c66b1205..931fbbb834ddc1 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -2,15 +2,14 @@ import logging -import voluptuous as vol from bizkaibus.bizkaibus import BizkaibusData -import homeassistant.helpers.config_validation as cv +import voluptuous as vol -from homeassistant.const import CONF_NAME from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity - _LOGGER = logging.getLogger(__name__) ATTR_DUE_IN = "Due in" diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index 08efc1e664715d..a0ea369bb9b3b0 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -22,6 +22,7 @@ STATE_ON, ) import homeassistant.helpers.config_validation as cv + from .const import DOMAIN, SERVICE_SETALLZONES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py index e626a73d287c64..0fedc2b794bac7 100644 --- a/homeassistant/components/blinkt/light.py +++ b/homeassistant/components/blinkt/light.py @@ -4,16 +4,16 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 99951fcf5c54e8..cc6562a0bc1250 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -3,7 +3,7 @@ import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 18f60036397b9f..84871b7b30e5d8 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -4,9 +4,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from . import BLOOMSKY diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 5a9f3561dc9354..04ba21555d4848 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -49,6 +49,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import Throttle import homeassistant.util.dt as dt_util + from .const import ( DOMAIN, SERVICE_CLEAR_TIMER, diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 18edd750639526..40f25f2fc43c55 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -4,18 +4,18 @@ import pygatt # pylint: disable=import-error -from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.components.device_tracker.legacy import ( - YAML_DEVICES, - async_load_config, -) from homeassistant.components.device_tracker.const import ( - CONF_TRACK_NEW, CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH_LE, ) +from homeassistant.components.device_tracker.legacy import ( + YAML_DEVICES, + async_load_config, +) from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.event import track_point_in_utc_time import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 102c8e494aa44a..d833f60c84f20f 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -1,7 +1,7 @@ """Tracking for bluetooth devices.""" import asyncio import logging -from typing import List, Set, Tuple, Optional +from typing import List, Optional, Set, Tuple # pylint: disable=import-error import bluetooth diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index b9bc18e6abf437..e1e33210c9b176 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -3,14 +3,13 @@ from functools import partial import logging -import smbus # pylint: disable=import-error from i2csense.bme280 import BME280 # pylint: disable=import-error - +import smbus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 5a1e9fd120ff90..65c87890242ed9 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -3,8 +3,8 @@ import threading from time import sleep, time -from smbus import SMBus # pylint: disable=import-error import bme680 # pylint: disable=import-error +from smbus import SMBus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index ed22be003ad4fa..7d951968cb2bea 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -12,19 +12,19 @@ import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, - TEMP_CELSIUS, - CONF_NAME, ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _RESOURCE = "http://www.bom.gov.au/fwo/{}/{}.{}.json" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index d8592f44fff7c0..282433aa7a46ff 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -5,7 +5,6 @@ import uuid import brottsplatskartan - import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py index b7612def70152c..fc0e9eccb3a097 100644 --- a/homeassistant/components/browser/__init__.py +++ b/homeassistant/components/browser/__init__.py @@ -1,5 +1,6 @@ """Support for launching a web browser on the host machine.""" import webbrowser + import voluptuous as vol ATTR_URL = "url" diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 20ad909c44e4e4..32b8e2aa0507ce 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -2,16 +2,15 @@ import logging import bthomehub5_devicelist - import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 0ea92b143bec39..d9341bb327167b 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -1,8 +1,8 @@ """The test for the bayesian sensor platform.""" import unittest -from homeassistant.setup import setup_component from homeassistant.components.bayesian import binary_sensor as bayesian +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index 0b6eda16c152fa..b090368a4ce677 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -1,24 +1,25 @@ """The tests for the Monoprice Blackbird media player platform.""" +from collections import defaultdict import unittest from unittest import mock -import voluptuous as vol -from collections import defaultdict -from homeassistant.components.media_player.const import ( - SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, - SUPPORT_SELECT_SOURCE, -) -from homeassistant.const import STATE_ON, STATE_OFF +import pytest +import voluptuous as vol -import tests.common +from homeassistant.components.blackbird.const import DOMAIN, SERVICE_SETALLZONES from homeassistant.components.blackbird.media_player import ( DATA_BLACKBIRD, PLATFORM_SCHEMA, setup_platform, ) -from homeassistant.components.blackbird.const import DOMAIN, SERVICE_SETALLZONES -import pytest +from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, +) +from homeassistant.const import STATE_OFF, STATE_ON + +import tests.common class AttrDict(dict): diff --git a/tests/components/bom/test_sensor.py b/tests/components/bom/test_sensor.py index 66d00e50796cf2..6d452f7a6a3881 100644 --- a/tests/components/bom/test_sensor.py +++ b/tests/components/bom/test_sensor.py @@ -10,6 +10,7 @@ from homeassistant.components import sensor from homeassistant.components.bom.sensor import BOMCurrentData from homeassistant.setup import setup_component + from tests.common import assert_setup_component, get_test_home_assistant, load_fixture VALID_CONFIG = { From 96961b9bccebe66b2ad18567fceb70e01db59854 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 13:57:24 +0100 Subject: [PATCH 2315/3953] Sort imports according to PEP8 for components starting with "A" (#29761) --- .../alarmdecoder/alarm_control_panel.py | 2 +- homeassistant/components/alert/__init__.py | 20 +++++++++--------- .../components/alpha_vantage/sensor.py | 4 ++-- homeassistant/components/amazon_polly/tts.py | 2 +- homeassistant/components/amcrest/__init__.py | 4 ++-- .../components/amcrest/binary_sensor.py | 4 ++-- .../components/androidtv/media_player.py | 6 +++--- .../components/anthemav/media_player.py | 3 +-- .../components/apcupsd/binary_sensor.py | 4 ++-- homeassistant/components/api/__init__.py | 6 +++--- homeassistant/components/apns/notify.py | 2 +- homeassistant/components/apple_tv/__init__.py | 2 +- homeassistant/components/apprise/notify.py | 6 ++---- .../components/aprs/device_tracker.py | 6 ++---- .../components/aquostv/media_player.py | 3 +-- .../components/arcam_fmj/__init__.py | 21 ++++++++++--------- .../components/arcam_fmj/media_player.py | 10 ++++----- homeassistant/components/arwn/sensor.py | 2 +- .../components/asterisk_cdr/mailbox.py | 12 ++++++----- homeassistant/components/atome/sensor.py | 13 ++++++------ .../components/aurora/binary_sensor.py | 2 +- .../components/aurora_abb_powerone/sensor.py | 2 +- .../components/automatic/device_tracker.py | 3 +-- homeassistant/components/avea/light.py | 2 +- .../components/azure_event_hub/__init__.py | 2 +- tests/components/alert/test_init.py | 10 ++++----- .../ambient_station/test_config_flow.py | 4 ++-- .../components/androidtv/test_media_player.py | 3 +-- tests/components/apns/test_notify.py | 6 +++--- tests/components/apprise/test_notify.py | 3 +-- tests/components/arlo/test_sensor.py | 11 +++++----- .../components/asuswrt/test_device_tracker.py | 8 +++---- tests/components/aurora/test_binary_sensor.py | 3 ++- .../automatic/test_device_tracker.py | 5 +++-- tests/components/awair/test_sensor.py | 2 +- 35 files changed, 97 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index d2e9fd136a8270..66960ca3034351 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -4,8 +4,8 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - AlarmControlPanel, FORMAT_NUMBER, + AlarmControlPanel, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 420d730933ca7a..09e2883c332f8c 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -1,30 +1,30 @@ """Support for repeating alerts when conditions are met.""" import asyncio -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( + ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE, - ATTR_DATA, DOMAIN as DOMAIN_NOTIFY, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_ENTITY_ID, - STATE_IDLE, CONF_NAME, CONF_STATE, - STATE_ON, - STATE_OFF, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, SERVICE_TOGGLE, - ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_ON, ) -from homeassistant.helpers import service, event +from homeassistant.helpers import event, service +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.util.dt import now diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index da29e4e25e1baa..7d871c286e57c0 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -2,9 +2,9 @@ from datetime import timedelta import logging -import voluptuous as vol -from alpha_vantage.timeseries import TimeSeries from alpha_vantage.foreignexchange import ForeignExchange +from alpha_vantage.timeseries import TimeSeries +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 3d05236935fb29..bcb4a24e95b92c 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -1,7 +1,7 @@ """Support for the Amazon Polly text to speech service.""" import logging -import boto3 +import boto3 import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index d49104a0b2624b..3420f42f9c9954 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -1,6 +1,6 @@ """Support for Amcrest IP cameras.""" -import logging from datetime import timedelta +import logging import threading import aiohttp @@ -36,7 +36,7 @@ from .binary_sensor import BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSORS from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST -from .const import CAMERAS, DOMAIN, DATA_AMCREST, DEVICES, SERVICE_UPDATE +from .const import CAMERAS, DATA_AMCREST, DEVICES, DOMAIN, SERVICE_UPDATE from .helpers import service_signal from .sensor import SENSOR_MOTION_DETECTOR, SENSORS from .switch import SWITCHES diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index f8b50d1114e48c..ac16f0664aa43c 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -5,11 +5,11 @@ from amcrest import AmcrestError from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOTION, + BinarySensorDevice, ) -from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS +from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index b1cb86f7633e8b..8b7f18802649a7 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -2,7 +2,6 @@ import functools import logging import os -import voluptuous as vol from adb_shell.auth.keygen import keygen from adb_shell.exceptions import ( @@ -11,10 +10,11 @@ InvalidResponseError, TcpTimeoutException, ) -from androidtv import setup, ha_state_detection_rules_validator +from androidtv import ha_state_detection_rules_validator, setup from androidtv.constants import APPS, KEYS +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index d472af6104ef26..f7b385d80a2210 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -2,10 +2,9 @@ import logging import anthemav - import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 62f0c90a4479ee..29825fd695ede0 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -1,10 +1,10 @@ """Support for tracking the online status of a UPS.""" import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components import apcupsd +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components import apcupsd DEFAULT_NAME = "UPS Online Status" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index d4faa55ed8c058..fc2f01d418d919 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -8,6 +8,7 @@ import async_timeout import voluptuous as vol +from homeassistant.auth.permissions.const import POLICY_READ from homeassistant.bootstrap import DATA_LOGGING from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( @@ -31,12 +32,11 @@ __version__, ) import homeassistant.core as ha -from homeassistant.auth.permissions.const import POLICY_READ -from homeassistant.exceptions import TemplateError, Unauthorized, ServiceNotFound +from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized from homeassistant.helpers import template +from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.state import AsyncTrackStates -from homeassistant.helpers.json import JSONEncoder _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index ce761b502ac414..990598508afddb 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -6,6 +6,7 @@ from apns2.payload import Payload import voluptuous as vol +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -17,7 +18,6 @@ from homeassistant.helpers import template as template_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_state_change -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from .const import DOMAIN diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 38d520f73daa35..e11b246fd5e4ca 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -7,11 +7,11 @@ from pyatv.exceptions import DeviceAuthenticationError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.discovery import SERVICE_APPLE_TV from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index 662cc9c1ab6ef0..0c8c5b26eeca4b 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -1,11 +1,8 @@ """Apprise platform for notify component.""" import logging -import voluptuous as vol - import apprise - -import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant.components.notify import ( ATTR_TARGET, @@ -14,6 +11,7 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index 0d23cedb4eeef9..6258b470ebb8d0 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -3,11 +3,9 @@ import logging import threading -import geopy.distance import aprslib -from aprslib import ConnectionError as AprsConnectionError -from aprslib import LoginError - +from aprslib import ConnectionError as AprsConnectionError, LoginError +import geopy.distance import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index d8770592c9fdca..f71f41dc293902 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -2,10 +2,9 @@ import logging import sharp_aquos_rc - import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index bdb3bf67bbe41d..d818414753fd08 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -1,31 +1,32 @@ """Arcam component.""" -import logging import asyncio +import logging -import voluptuous as vol -import async_timeout -from arcam.fmj.client import Client from arcam.fmj import ConnectionFailed +from arcam.fmj.client import Client +import async_timeout +import voluptuous as vol from homeassistant import config_entries -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SCAN_INTERVAL, CONF_ZONE, + EVENT_HOMEASSISTANT_STOP, SERVICE_TURN_ON, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + from .const import ( - DOMAIN, - DOMAIN_DATA_ENTRIES, - DOMAIN_DATA_CONFIG, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, + DOMAIN, + DOMAIN_DATA_CONFIG, + DOMAIN_DATA_ENTRIES, SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_STARTED, SIGNAL_CLIENT_STOPPED, diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 231e9821dc6ddc..8a54c745695ec4 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -6,14 +6,13 @@ from arcam.fmj.state import State from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, @@ -25,15 +24,16 @@ STATE_OFF, STATE_ON, ) -from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.core import callback from homeassistant.helpers.service import async_call_from_config +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( + DOMAIN, + DOMAIN_DATA_ENTRIES, SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_STARTED, SIGNAL_CLIENT_STOPPED, - DOMAIN_DATA_ENTRIES, - DOMAIN, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 23cd811a3e0eaf..685e5d90f53669 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -3,8 +3,8 @@ import logging from homeassistant.components import mqtt +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback -from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from homeassistant.util import slugify diff --git a/homeassistant/components/asterisk_cdr/mailbox.py b/homeassistant/components/asterisk_cdr/mailbox.py index 4146ca9ddf9cb1..0bae6ebf3adf62 100644 --- a/homeassistant/components/asterisk_cdr/mailbox.py +++ b/homeassistant/components/asterisk_cdr/mailbox.py @@ -1,12 +1,14 @@ """Support for the Asterisk CDR interface.""" -import logging -import hashlib import datetime +import hashlib +import logging -from homeassistant.core import callback -from homeassistant.components.asterisk_mbox import SIGNAL_CDR_UPDATE -from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN +from homeassistant.components.asterisk_mbox import ( + DOMAIN as ASTERISK_DOMAIN, + SIGNAL_CDR_UPDATE, +) from homeassistant.components.mailbox import Mailbox +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index c98b634bb2111d..f9dd6b2dd61290 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -1,23 +1,22 @@ """Linky Atome.""" -import logging from datetime import timedelta +import logging +from pyatome.client import AtomeClient, PyAtomeError import voluptuous as vol -from pyatome.client import AtomeClient -from pyatome.client import PyAtomeError +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - CONF_NAME, DEVICE_CLASS_POWER, - POWER_WATT, ENERGY_KILO_WATT_HOUR, + POWER_WATT, ) -from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index a69433c418617f..d76884d289573f 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 05ed5fa99bf2b1..a2645e5d7cb35b 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -2,8 +2,8 @@ import logging +from aurorapy.client import AuroraError, AuroraSerialClient import voluptuous as vol -from aurorapy.client import AuroraSerialClient, AuroraError from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( diff --git a/homeassistant/components/automatic/device_tracker.py b/homeassistant/components/automatic/device_tracker.py index fbb823dd329270..bb403687963086 100644 --- a/homeassistant/components/automatic/device_tracker.py +++ b/homeassistant/components/automatic/device_tracker.py @@ -5,9 +5,8 @@ import logging import os -from aiohttp import web import aioautomatic - +from aiohttp import web import voluptuous as vol from homeassistant.components.device_tracker import ( diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py index e6ceedcf96d23b..92d66a554daad1 100644 --- a/homeassistant/components/avea/light.py +++ b/homeassistant/components/avea/light.py @@ -1,5 +1,6 @@ """Support for the Elgato Avea lights.""" import logging + import avea from homeassistant.components.light import ( @@ -12,7 +13,6 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.util.color as color_util - _LOGGER = logging.getLogger(__name__) SUPPORT_AVEA = SUPPORT_BRIGHTNESS | SUPPORT_COLOR diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index 371b8d1ea8dd89..7e141cd8060cf8 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -3,8 +3,8 @@ import logging from typing import Any, Dict -import voluptuous as vol from azure.eventhub import EventData, EventHubClientAsync +import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index bcdb2058a423fa..55a3112c32f7d5 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -1,26 +1,26 @@ """The tests for the Alert component.""" -import unittest - # pylint: disable=protected-access from copy import deepcopy +import unittest import homeassistant.components.alert as alert -import homeassistant.components.notify as notify from homeassistant.components.alert import DOMAIN +import homeassistant.components.notify as notify from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, - STATE_IDLE, CONF_NAME, CONF_STATE, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_ON, + STATE_IDLE, STATE_OFF, + STATE_ON, ) from homeassistant.core import callback from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant NAME = "alert_test" diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index c94a51be94e719..25e4609000951d 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -1,15 +1,15 @@ """Define tests for the Ambient PWS config flow.""" import json +from unittest.mock import patch import aioambient import pytest -from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.ambient_station import CONF_APP_KEY, DOMAIN, config_flow from homeassistant.const import CONF_API_KEY -from tests.common import load_fixture, MockConfigEntry, mock_coro +from tests.common import MockConfigEntry, load_fixture, mock_coro @pytest.fixture diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 04b0bebf447ece..d7a6a8c1ce682d 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,7 +1,6 @@ """The tests for the androidtv platform.""" import logging -from homeassistant.setup import async_setup_component from homeassistant.components.androidtv.media_player import ( ANDROIDTV_DOMAIN, CONF_ADB_SERVER_IP, @@ -24,10 +23,10 @@ STATE_PLAYING, STATE_UNAVAILABLE, ) +from homeassistant.setup import async_setup_component from . import patchers - # Android TV device with Python ADB implementation CONFIG_ANDROIDTV_PYTHON_ADB = { DOMAIN: { diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py index 19d869ea678313..61092899e248d9 100644 --- a/tests/components/apns/test_notify.py +++ b/tests/components/apns/test_notify.py @@ -1,15 +1,15 @@ """The tests for the APNS component.""" import io import unittest -from unittest.mock import Mock, patch, mock_open +from unittest.mock import Mock, mock_open, patch from apns2.errors import Unregistered import yaml -import homeassistant.components.notify as notify -from homeassistant.setup import setup_component import homeassistant.components.apns.notify as apns +import homeassistant.components.notify as notify from homeassistant.core import State +from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index 237f99de676faa..a275e57653dc2a 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -1,6 +1,5 @@ """The tests for the apprise notification platform.""" -from unittest.mock import patch -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from homeassistant.setup import async_setup_component diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index d06b48ac3a258b..ac64ad8f272eb9 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -1,14 +1,15 @@ """The tests for the Netgear Arlo sensors.""" from collections import namedtuple -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + import pytest + +from homeassistant.components.arlo import DATA_ARLO, sensor as arlo from homeassistant.const import ( - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_HUMIDITY, ATTR_ATTRIBUTION, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, ) -from homeassistant.components.arlo import sensor as arlo -from homeassistant.components.arlo import DATA_ARLO def _get_named_tuple(input_dict): diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index de999362f51e3d..2ecab9c1d37366 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -1,15 +1,15 @@ """The tests for the ASUSWRT device tracker platform.""" from unittest.mock import patch -from homeassistant.setup import async_setup_component from homeassistant.components.asuswrt import ( - CONF_PROTOCOL, CONF_MODE, - DOMAIN, CONF_PORT, + CONF_PROTOCOL, DATA_ASUSWRT, + DOMAIN, ) -from homeassistant.const import CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME +from homeassistant.setup import async_setup_component from tests.common import mock_coro_func diff --git a/tests/components/aurora/test_binary_sensor.py b/tests/components/aurora/test_binary_sensor.py index 72cef0b44ca5b8..1683e1951a073c 100644 --- a/tests/components/aurora/test_binary_sensor.py +++ b/tests/components/aurora/test_binary_sensor.py @@ -5,7 +5,8 @@ import requests_mock from homeassistant.components.aurora import binary_sensor as aurora -from tests.common import load_fixture, get_test_home_assistant + +from tests.common import get_test_home_assistant, load_fixture class TestAuroraSensorSetUp(unittest.TestCase): diff --git a/tests/components/automatic/test_device_tracker.py b/tests/components/automatic/test_device_tracker.py index 4186316e7ac8ad..3611a5ae0e31d4 100644 --- a/tests/components/automatic/test_device_tracker.py +++ b/tests/components/automatic/test_device_tracker.py @@ -2,11 +2,12 @@ import asyncio from datetime import datetime import logging -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + import aioautomatic -from homeassistant.setup import async_setup_component from homeassistant.components.automatic.device_tracker import async_setup_scanner +from homeassistant.setup import async_setup_component _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index ee4e49068262a0..ded1520718f685 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -6,7 +6,6 @@ import logging from unittest.mock import patch -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.awair.sensor import ( ATTR_LAST_API_UPDATE, ATTR_TIMESTAMP, @@ -15,6 +14,7 @@ DEVICE_CLASS_SCORE, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, From f9e06ca2fd819f76965028c56bec99374119e252 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:10:04 +0100 Subject: [PATCH 2316/3953] Sort imports according to PEP8 for components starting with "E" (#29765) --- .../components/eddystone_temperature/sensor.py | 6 +----- homeassistant/components/efergy/sensor.py | 2 +- .../components/elkm1/alarm_control_panel.py | 6 +++--- homeassistant/components/elv/__init__.py | 2 +- homeassistant/components/elv/switch.py | 2 +- homeassistant/components/emoncms/sensor.py | 14 +++++++------- .../components/emoncms_history/__init__.py | 10 +++++----- homeassistant/components/envirophat/sensor.py | 4 ++-- homeassistant/components/epson/media_player.py | 12 ++++++------ tests/components/efergy/test_sensor.py | 2 +- 10 files changed, 28 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 2084e3070294fb..22d3533d32fbbc 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -10,11 +10,7 @@ import logging # pylint: disable=import-error -from beacontools import ( - BeaconScanner, - EddystoneFilter, - EddystoneTLMFrame, -) +from beacontools import BeaconScanner, EddystoneFilter, EddystoneTLMFrame import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 43c3b67457a755..3be962fea2f501 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_CURRENCY, POWER_WATT, ENERGY_KILO_WATT_HOUR +from homeassistant.const import CONF_CURRENCY, ENERGY_KILO_WATT_HOUR, POWER_WATT import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 1e1a8eba9e0ec2..de1cb62234c789 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -3,8 +3,8 @@ import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - AlarmControlPanel, FORMAT_NUMBER, + AlarmControlPanel, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -29,13 +29,13 @@ ) from . import ( - create_elk_entities, DOMAIN, - ElkEntity, SERVICE_ALARM_ARM_HOME_INSTANT, SERVICE_ALARM_ARM_NIGHT_INSTANT, SERVICE_ALARM_ARM_VACATION, SERVICE_ALARM_DISPLAY_MESSAGE, + ElkEntity, + create_elk_entities, ) SIGNAL_ARM_ENTITY = "elkm1_arm" diff --git a/homeassistant/components/elv/__init__.py b/homeassistant/components/elv/__init__.py index b6097737414e91..b776c7f54530c4 100644 --- a/homeassistant/components/elv/__init__.py +++ b/homeassistant/components/elv/__init__.py @@ -4,8 +4,8 @@ import voluptuous as vol -from homeassistant.helpers import discovery from homeassistant.const import CONF_DEVICE +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index 362424c7faca48..a77d21cf173a8d 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -4,7 +4,7 @@ import pypca from serial import SerialException -from homeassistant.components.switch import SwitchDevice, ATTR_CURRENT_POWER_W +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchDevice from homeassistant.const import EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 5f9d31697b86f8..34063e4c253f4f 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -2,23 +2,23 @@ from datetime import timedelta import logging -import voluptuous as vol import requests +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, - CONF_URL, - CONF_VALUE_TEMPLATE, - CONF_UNIT_OF_MEASUREMENT, CONF_ID, CONF_SCAN_INTERVAL, - STATE_UNKNOWN, + CONF_UNIT_OF_MEASUREMENT, + CONF_URL, + CONF_VALUE_TEMPLATE, POWER_WATT, + STATE_UNKNOWN, ) -from homeassistant.helpers.entity import Entity from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index 3b30a29960b3fd..fd38da1cac17e5 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -1,20 +1,20 @@ """Support for sending data to Emoncms.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_API_KEY, - CONF_WHITELIST, + CONF_SCAN_INTERVAL, CONF_URL, - STATE_UNKNOWN, + CONF_WHITELIST, STATE_UNAVAILABLE, - CONF_SCAN_INTERVAL, + STATE_UNKNOWN, ) from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 2aaeefa48cf785..ce1f154f911aa8 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -1,12 +1,12 @@ """Support for Enviro pHAT sensors.""" +from datetime import timedelta import importlib import logging -from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_DISPLAY_OPTIONS, CONF_NAME +from homeassistant.const import CONF_DISPLAY_OPTIONS, CONF_NAME, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index f3428602fadd29..b39722c39f3440 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -1,8 +1,7 @@ """Support for Epson projector.""" import logging -import voluptuous as vol - +import epson_projector as epson from epson_projector.const import ( BACK, BUSY, @@ -19,15 +18,15 @@ POWER, SOURCE, SOURCE_LIST, - TURN_ON, TURN_OFF, - VOLUME, + TURN_ON, VOL_DOWN, VOL_UP, + VOLUME, ) -import epson_projector as epson +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, @@ -48,6 +47,7 @@ ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv + from .const import ( ATTR_CMODE, DATA_EPSON, diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index cbba49d0ed0c54..18a00005dd5aaa 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -5,7 +5,7 @@ from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant +from tests.common import get_test_home_assistant, load_fixture token = "9p6QGJ7dpZfO3fqPTBk1fyEmjV1cGoLT" multi_sensor_token = "9r6QGF7dpZfO3fqPTBl1fyRmjV1cGoLT" From ca0fad2cbb0544125d87d21ffe308cf3addcde5a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:14:40 +0100 Subject: [PATCH 2317/3953] Sort imports according to PEP8 for components starting with "F" (#29766) --- homeassistant/components/facebook/notify.py | 5 ++-- .../components/facebox/image_processing.py | 15 ++++++------ homeassistant/components/fail2ban/sensor.py | 11 ++++----- .../components/feedreader/__init__.py | 8 +++---- .../components/ffmpeg_motion/binary_sensor.py | 12 +++++----- .../components/ffmpeg_noise/binary_sensor.py | 10 ++++---- homeassistant/components/filesize/sensor.py | 4 ++-- homeassistant/components/filter/sensor.py | 24 +++++++++---------- homeassistant/components/fitbit/sensor.py | 12 ++++------ .../components/flic/binary_sensor.py | 8 +++---- homeassistant/components/flock/notify.py | 3 +-- homeassistant/components/flux/switch.py | 12 +++++----- homeassistant/components/flux_led/light.py | 14 +++++------ homeassistant/components/folder/sensor.py | 4 ++-- .../components/fortios/device_tracker.py | 7 +++--- homeassistant/components/foscam/camera.py | 18 +++++++------- .../components/foursquare/__init__.py | 4 ++-- homeassistant/components/freedns/__init__.py | 4 ++-- homeassistant/components/fritzbox/climate.py | 6 ++--- homeassistant/components/fronius/sensor.py | 7 +++--- tests/components/facebook/test_notify.py | 1 + .../facebox/test_image_processing.py | 10 ++++---- tests/components/fail2ban/test_sensor.py | 10 ++++---- tests/components/feedreader/test_init.py | 19 ++++++++------- tests/components/fido/test_sensor.py | 2 +- tests/components/filesize/test_sensor.py | 4 ++-- tests/components/filter/test_sensor.py | 9 +++---- tests/components/flux/test_switch.py | 6 ++--- tests/components/folder/test_sensor.py | 4 ++-- tests/components/folder_watcher/test_init.py | 3 ++- tests/components/foobot/test_sensor.py | 7 +++--- tests/components/freedns/test_init.py | 3 ++- tests/components/frontend/test_init.py | 11 ++++----- tests/components/frontend/test_storage.py | 2 +- 34 files changed, 139 insertions(+), 140 deletions(-) diff --git a/homeassistant/components/facebook/notify.py b/homeassistant/components/facebook/notify.py index 452b81c0f16a06..b75f26280331be 100644 --- a/homeassistant/components/facebook/notify.py +++ b/homeassistant/components/facebook/notify.py @@ -6,15 +6,14 @@ import requests import voluptuous as vol -from homeassistant.const import CONTENT_TYPE_JSON -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONTENT_TYPE_JSON +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index ba53ac1ac7d065..ee6e4d8a6fa50e 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -5,26 +5,27 @@ import requests import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME -from homeassistant.core import split_entity_id -import homeassistant.helpers.config_validation as cv from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, - ImageProcessingFaceEntity, ATTR_CONFIDENCE, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingFaceEntity, ) from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_NAME, CONF_IP_ADDRESS, - CONF_PORT, CONF_PASSWORD, + CONF_PORT, CONF_USERNAME, HTTP_BAD_REQUEST, HTTP_OK, HTTP_UNAUTHORIZED, ) +from homeassistant.core import split_entity_id +import homeassistant.helpers.config_validation as cv from .const import DOMAIN, SERVICE_TEACH_FACE diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 2dc528b2cff776..692b48d9db5d80 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -5,17 +5,16 @@ https://home-assistant.io/components/sensor.fail2ban/ """ -import os -import logging - from datetime import timedelta - +import logging +import os import re + import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_FILE_PATH +from homeassistant.const import CONF_FILE_PATH, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 27b164e4edf27e..bf1e55370bcaa1 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -2,15 +2,15 @@ from datetime import datetime, timedelta from logging import getLogger from os.path import exists -from threading import Lock import pickle +from threading import Lock -import voluptuous as vol import feedparser +import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL -from homeassistant.helpers.event import track_time_interval +from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_time_interval _LOGGER = getLogger(__name__) diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index 54f3981f48a00e..294fcc2518fa2c 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -4,17 +4,17 @@ import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.components.ffmpeg import ( - FFmpegBase, - DATA_FFMPEG, - CONF_INPUT, CONF_EXTRA_ARGUMENTS, CONF_INITIAL_STATE, + CONF_INPUT, + DATA_FFMPEG, + FFmpegBase, ) from homeassistant.const import CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index 7c5f8656410f7a..6ada2bb274896d 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -4,17 +4,17 @@ import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import PLATFORM_SCHEMA -from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor from homeassistant.components.ffmpeg import ( - DATA_FFMPEG, - CONF_INPUT, - CONF_OUTPUT, CONF_EXTRA_ARGUMENTS, CONF_INITIAL_STATE, + CONF_INPUT, + CONF_OUTPUT, + DATA_FFMPEG, ) +from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index af9375aad053d4..8c6cd30b1186fa 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -5,9 +5,9 @@ import voluptuous as vol -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 81c4623c53f52a..eeb0d32f51c745 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -1,31 +1,31 @@ """Allows the creation of a sensor that filters state property.""" -import logging -import statistics -from collections import deque, Counter -from numbers import Number -from functools import partial +from collections import Counter, deque from copy import copy from datetime import timedelta +from functools import partial +import logging +from numbers import Number +import statistics from typing import Optional import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components import history from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, - CONF_ENTITY_ID, - ATTR_UNIT_OF_MEASUREMENT, ATTR_ENTITY_ID, ATTR_ICON, - STATE_UNKNOWN, + ATTR_UNIT_OF_MEASUREMENT, + CONF_ENTITY_ID, + CONF_NAME, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.util.decorator import Registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change -from homeassistant.components import history +from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 0d4b8d61e7a7a7..5ddb63ef899a3f 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -1,7 +1,7 @@ """Support for the Fitbit API.""" -import os -import logging import datetime +import logging +import os import time from fitbit import Fitbit @@ -9,17 +9,15 @@ from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.const import CONF_UNIT_SYSTEM +from homeassistant.const import ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level -import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json - _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index 416d39e53326e6..4f2f229977fc03 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -3,24 +3,24 @@ import threading from pyflic import ( - FlicClient, ButtonConnectionChannel, ClickType, ConnectionStatus, + FlicClient, ScanWizard, ScanWizardResult, ) import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( + CONF_DISCOVERY, CONF_HOST, CONF_PORT, - CONF_DISCOVERY, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index ba52d3b4beb03e..a71601ea2c4786 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -5,12 +5,11 @@ import async_timeout import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://api.flock.com/hooks/sendMessage/" diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 404067d41074eb..a02b1b2504b264 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -11,9 +11,7 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, @@ -22,29 +20,31 @@ ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION, + is_on, ) from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_NAME, - CONF_PLATFORM, CONF_LIGHTS, CONF_MODE, + CONF_NAME, + CONF_PLATFORM, SERVICE_TURN_ON, STATE_ON, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify from homeassistant.util.color import ( - color_temperature_to_rgb, color_RGB_to_xy_brightness, color_temperature_kelvin_to_mired, + color_temperature_to_rgb, ) -from homeassistant.util.dt import utcnow as dt_utcnow, as_local +from homeassistant.util.dt import as_local, utcnow as dt_utcnow _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 7973956848a51d..16db60abbc0188 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -1,28 +1,28 @@ """Support for Flux lights.""" import logging -import socket import random +import socket from flux_led import BulbScanner, WifiLedBulb import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL, ATTR_MODE from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, + ATTR_COLOR_TEMP, ATTR_EFFECT, + ATTR_HS_COLOR, ATTR_WHITE_VALUE, - ATTR_COLOR_TEMP, EFFECT_COLORLOOP, EFFECT_RANDOM, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, SUPPORT_COLOR, - SUPPORT_WHITE_VALUE, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_WHITE_VALUE, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import ATTR_MODE, CONF_DEVICES, CONF_NAME, CONF_PROTOCOL import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index e9e4ea680c4bf4..a706ab2a0b5cf4 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -6,9 +6,9 @@ import voluptuous as vol -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index 51bce5429f66db..2b2d14f60e04fd 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -5,17 +5,16 @@ """ import logging -import voluptuous as vol from fortiosapi import FortiOSAPI +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_TOKEN -from homeassistant.const import CONF_VERIFY_SSL +from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_VERIFY_SSL +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_VERIFY_SSL = False diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 0e2ca4073bfa51..f4ec655689475b 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -1,26 +1,26 @@ """This component provides basic support for Foscam IP cameras.""" -import logging import asyncio +import logging from libpyfoscam import FoscamCamera - import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, CONF_PORT, - ATTR_ENTITY_ID, + CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_extract_entity_ids -from .const import DOMAIN as FOSCAM_DOMAIN -from .const import DATA as FOSCAM_DATA -from .const import ENTITIES as FOSCAM_ENTITIES - +from .const import ( + DATA as FOSCAM_DATA, + DOMAIN as FOSCAM_DOMAIN, + ENTITIES as FOSCAM_ENTITIES, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py index 7efb989e14223d..3f0578cf5b4750 100644 --- a/homeassistant/components/foursquare/__init__.py +++ b/homeassistant/components/foursquare/__init__.py @@ -4,9 +4,9 @@ import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST from homeassistant.components.http import HomeAssistantView +from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index 30f80280c1f03c..7aa34c8780e881 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -1,14 +1,14 @@ """Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org.""" import asyncio -import logging from datetime import timedelta +import logging import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL, CONF_URL +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 40c16930206598..115f7f8e644fda 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -7,11 +7,11 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, HVAC_MODE_HEAT, - PRESET_ECO, - PRESET_COMFORT, - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_OFF, + PRESET_COMFORT, + PRESET_ECO, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_BATTERY_LEVEL, diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index ff0694afaab38c..27e2531c9f911b 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -2,24 +2,23 @@ import copy from datetime import timedelta import logging -import voluptuous as vol from pyfronius import Fronius +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_RESOURCE, - CONF_SENSOR_TYPE, CONF_DEVICE, CONF_MONITORED_CONDITIONS, + CONF_RESOURCE, CONF_SCAN_INTERVAL, + CONF_SENSOR_TYPE, ) 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__) CONF_SCOPE = "scope" diff --git a/tests/components/facebook/test_notify.py b/tests/components/facebook/test_notify.py index 5cd5f89ef1f66e..e23cc4f09826cf 100644 --- a/tests/components/facebook/test_notify.py +++ b/tests/components/facebook/test_notify.py @@ -1,5 +1,6 @@ """The test for the Facebook notify module.""" import unittest + import requests_mock # import homeassistant.components.facebook as facebook diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py index 9b4610e84b0d3e..d82f70e7ca1a94 100644 --- a/tests/components/facebox/test_image_processing.py +++ b/tests/components/facebox/test_image_processing.py @@ -5,23 +5,23 @@ import requests import requests_mock -from homeassistant.core import callback +import homeassistant.components.facebox.image_processing as fb +import homeassistant.components.image_processing as ip from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, CONF_FRIENDLY_NAME, - CONF_PASSWORD, - CONF_USERNAME, CONF_IP_ADDRESS, + CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, HTTP_BAD_REQUEST, HTTP_OK, HTTP_UNAUTHORIZED, STATE_UNKNOWN, ) +from homeassistant.core import callback from homeassistant.setup import async_setup_component -import homeassistant.components.image_processing as ip -import homeassistant.components.facebox.image_processing as fb MOCK_IP = "192.168.0.1" MOCK_PORT = "8080" diff --git a/tests/components/fail2ban/test_sensor.py b/tests/components/fail2ban/test_sensor.py index 55195958169a37..796ddd93d26cb3 100644 --- a/tests/components/fail2ban/test_sensor.py +++ b/tests/components/fail2ban/test_sensor.py @@ -4,15 +4,15 @@ from mock_open import MockOpen -from homeassistant.setup import setup_component from homeassistant.components.fail2ban.sensor import ( - BanSensor, - BanLogParser, - STATE_CURRENT_BANS, STATE_ALL_BANS, + STATE_CURRENT_BANS, + BanLogParser, + BanSensor, ) +from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant def fake_log(log_key): diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index eff44c44303c2c..d74bbd79d804b1 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -1,27 +1,28 @@ """The tests for the feedreader component.""" -import time from datetime import timedelta - -import unittest -from genericpath import exists from logging import getLogger from os import remove +import time +import unittest from unittest import mock from unittest.mock import patch +from genericpath import exists + from homeassistant.components import feedreader from homeassistant.components.feedreader import ( + CONF_MAX_ENTRIES, CONF_URLS, + DEFAULT_MAX_ENTRIES, + DEFAULT_SCAN_INTERVAL, + EVENT_FEEDREADER, FeedManager, StoredData, - EVENT_FEEDREADER, - DEFAULT_SCAN_INTERVAL, - CONF_MAX_ENTRIES, - DEFAULT_MAX_ENTRIES, ) -from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL +from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START from homeassistant.core import callback from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant, load_fixture _LOGGER = getLogger(__name__) diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index 010896e086bada..1f67a1e2e114b1 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -6,8 +6,8 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components.fido import sensor as fido -from tests.common import assert_setup_component +from tests.common import assert_setup_component CONTRACT = "123456789" diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 2246a74abe58e2..29bd6a7fb1f0f7 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the filesize sensor.""" -import unittest import os +import unittest from homeassistant.components.filesize.sensor import CONF_FILE_PATHS from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant TEST_DIR = os.path.join(os.path.dirname(__file__)) TEST_FILE = os.path.join(TEST_DIR, "mock_file_test_filesize.txt") diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index e9c8f4c35e2ba5..17e5bd8fd5d4f4 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -6,17 +6,18 @@ from homeassistant.components.filter.sensor import ( LowPassFilter, OutlierFilter, + RangeFilter, ThrottleFilter, TimeSMAFilter, - RangeFilter, TimeThrottleFilter, ) -import homeassistant.util.dt as dt_util -from homeassistant.setup import setup_component import homeassistant.core as ha +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + from tests.common import ( - get_test_home_assistant, assert_setup_component, + get_test_home_assistant, init_recorder_component, ) diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index 91871666f467a7..b3d0a00896179a 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -2,15 +2,15 @@ from asynctest.mock import patch import pytest -from homeassistant.setup import async_setup_component -from homeassistant.components import switch, light +from homeassistant.components import light, switch from homeassistant.const import ( CONF_PLATFORM, - STATE_ON, SERVICE_TURN_ON, + STATE_ON, SUN_EVENT_SUNRISE, ) from homeassistant.core import State +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( diff --git a/tests/components/folder/test_sensor.py b/tests/components/folder/test_sensor.py index 97c933fdd23294..fc7de1f59c0838 100644 --- a/tests/components/folder/test_sensor.py +++ b/tests/components/folder/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the folder sensor.""" -import unittest import os +import unittest from homeassistant.components.folder.sensor import CONF_FOLDER_PATHS from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant CWD = os.path.join(os.path.dirname(__file__)) TEST_FOLDER = "test_folder" diff --git a/tests/components/folder_watcher/test_init.py b/tests/components/folder_watcher/test_init.py index 69dea7f51ec514..dee97afa4689a9 100644 --- a/tests/components/folder_watcher/test_init.py +++ b/tests/components/folder_watcher/test_init.py @@ -1,9 +1,10 @@ """The tests for the folder_watcher component.""" -from unittest.mock import Mock, patch import os +from unittest.mock import Mock, patch from homeassistant.components import folder_watcher from homeassistant.setup import async_setup_component + from tests.common import MockDependency diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 6c601c5ad69a5a..9c6a17264ebadb 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -1,16 +1,17 @@ """The tests for the Foobot sensor platform.""" -import re import asyncio +import re from unittest.mock import MagicMock -import pytest +import pytest -import homeassistant.components.sensor as sensor from homeassistant.components.foobot import sensor as foobot +import homeassistant.components.sensor as sensor from homeassistant.const import TEMP_CELSIUS from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component + from tests.common import load_fixture VALID_CONFIG = { diff --git a/tests/components/freedns/test_init.py b/tests/components/freedns/test_init.py index 6441a418bfaa4f..b9e59de9ff1339 100644 --- a/tests/components/freedns/test_init.py +++ b/tests/components/freedns/test_init.py @@ -1,9 +1,10 @@ """Test the FreeDNS component.""" import asyncio + import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import freedns +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index b38c55cd5839db..f8fd5f1d7e3e6c 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -5,19 +5,18 @@ import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( - DOMAIN, - CONF_JS_VERSION, - CONF_THEMES, CONF_EXTRA_HTML_URL, CONF_EXTRA_HTML_URL_ES5, + CONF_JS_VERSION, + CONF_THEMES, + DOMAIN, EVENT_PANELS_UPDATED, ) from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.setup import async_setup_component -from tests.common import mock_coro, async_capture_events - +from tests.common import async_capture_events, mock_coro CONFIG_THEMES = {DOMAIN: {CONF_THEMES: {"happy": {"primary-color": "red"}}}} diff --git a/tests/components/frontend/test_storage.py b/tests/components/frontend/test_storage.py index 76296f743c5bd5..d907f69bbf9073 100644 --- a/tests/components/frontend/test_storage.py +++ b/tests/components/frontend/test_storage.py @@ -1,8 +1,8 @@ """The tests for frontend storage.""" import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.frontend import storage +from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) From d58e6e924a271fa446f6267d598ad18269e7d164 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:17:36 +0100 Subject: [PATCH 2318/3953] Sort imports according to PEP8 for components starting with "G" (#29767) --- homeassistant/components/garadget/cover.py | 14 ++++++------- homeassistant/components/geizhals/sensor.py | 6 +++--- homeassistant/components/generic/camera.py | 20 +++++++++---------- .../components/generic_thermostat/climate.py | 2 +- homeassistant/components/geniushub/switch.py | 2 +- .../components/geo_location/__init__.py | 1 - .../components/geo_rss_events/sensor.py | 10 +++++----- homeassistant/components/github/sensor.py | 1 + homeassistant/components/google/__init__.py | 13 ++++++------ homeassistant/components/google_cloud/tts.py | 4 ++-- .../components/google_domains/__init__.py | 2 +- .../components/google_wifi/sensor.py | 11 +++++----- homeassistant/components/graphite/__init__.py | 2 +- .../components/growatt_server/sensor.py | 12 +++++------ tests/components/generic/test_camera.py | 1 - .../geo_json_events/test_geo_location.py | 17 ++++++++-------- .../components/geo_rss_events/test_sensor.py | 11 +++++----- tests/components/google/test_calendar.py | 1 - tests/components/google_domains/test_init.py | 2 +- tests/components/google_translate/test_tts.py | 7 +++---- tests/components/google_wifi/test_sensor.py | 8 ++++---- tests/components/graphite/test_init.py | 9 +++++---- 22 files changed, 77 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index d487c39db6bfa5..0eeb5f2b8f9b68 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -4,19 +4,19 @@ import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice from homeassistant.const import ( - CONF_DEVICE, - CONF_USERNAME, - CONF_PASSWORD, CONF_ACCESS_TOKEN, + CONF_COVERS, + CONF_DEVICE, CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, STATE_CLOSED, STATE_OPEN, - CONF_COVERS, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_utc_time_change _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geizhals/sensor.py b/homeassistant/components/geizhals/sensor.py index f04e943964c761..9d5605cc404543 100644 --- a/homeassistant/components/geizhals/sensor.py +++ b/homeassistant/components/geizhals/sensor.py @@ -1,15 +1,15 @@ """Parse prices of a device from geizhals.""" -import logging from datetime import timedelta +import logging from geizhals import Device, Geizhals import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity -from homeassistant.const import CONF_NAME +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 01d2fb948eda1e..3d39d75ff4a49d 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -8,24 +8,24 @@ from requests.auth import HTTPDigestAuth import voluptuous as vol +from homeassistant.components.camera import ( + DEFAULT_CONTENT_TYPE, + PLATFORM_SCHEMA, + SUPPORT_STREAM, + Camera, +) from homeassistant.const import ( + CONF_AUTHENTICATION, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, - CONF_AUTHENTICATION, + CONF_USERNAME, + CONF_VERIFY_SSL, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, - CONF_VERIFY_SSL, ) from homeassistant.exceptions import TemplateError -from homeassistant.components.camera import ( - PLATFORM_SCHEMA, - DEFAULT_CONTENT_TYPE, - SUPPORT_STREAM, - Camera, -) -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 5cb4c21c5779ff..cb5ae275df77cc 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -15,9 +15,9 @@ HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index 79d14417dd4431..b73c9a89041c09 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -1,5 +1,5 @@ """Support for Genius Hub switch/outlet devices.""" -from homeassistant.components.switch import SwitchDevice, DEVICE_CLASS_OUTLET +from homeassistant.components.switch import DEVICE_CLASS_OUTLET, SwitchDevice from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import DOMAIN, GeniusZone diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index e5c587f93ede18..6142fa222095d7 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -11,7 +11,6 @@ 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__) diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index 39e6c5c7e824a1..b8891cdef0d2b5 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -8,23 +8,23 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.geo_rss_events/ """ -import logging from datetime import timedelta +import logging -import voluptuous as vol from georss_client import UPDATE_OK, UPDATE_OK_NO_DATA from georss_client.generic_feed import GenericFeed +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_UNIT_OF_MEASUREMENT, - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, + CONF_NAME, CONF_RADIUS, + CONF_UNIT_OF_MEASUREMENT, CONF_URL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 5e8200b41ab4b8..c77cf7930b83d4 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -1,6 +1,7 @@ """Support for GitHub.""" from datetime import timedelta import logging + import github import voluptuous as vol diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 9cb9be0fa4fad7..0e7ccd33b33206 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,23 +1,22 @@ """Support for Google - Calendar Event Devices.""" -from datetime import timedelta, datetime +from datetime import datetime, timedelta import logging import os -import yaml +from googleapiclient import discovery as google_discovery import httplib2 from oauth2client.client import ( - OAuth2WebServerFlow, - OAuth2DeviceCodeError, FlowExchangeError, + OAuth2DeviceCodeError, + OAuth2WebServerFlow, ) from oauth2client.file import Storage -from googleapiclient import discovery as google_discovery - import voluptuous as vol from voluptuous.error import Error as VoluptuousError +import yaml -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import track_time_change from homeassistant.util import convert, dt diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 942ee0a4e483d1..6721520d130c2e 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -1,11 +1,11 @@ """Support for the Google Cloud TTS service.""" +import asyncio import logging import os -import asyncio import async_timeout -import voluptuous as vol from google.cloud import texttospeech +import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index 8f975db6fd8347..d440567d9ad937 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -7,8 +7,8 @@ import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index 1d4ed8d84f812c..9d6f3ea3d58d10 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -1,21 +1,20 @@ """Support for retrieving status info from Google Wifi/OnHub routers.""" -import logging from datetime import timedelta +import logging -import voluptuous as vol import requests +import voluptuous as vol -from homeassistant.util import dt -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_MONITORED_CONDITIONS, + CONF_NAME, STATE_UNKNOWN, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 3809249bea698f..bf34bc3ddeaac3 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -7,7 +7,6 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -17,6 +16,7 @@ EVENT_STATE_CHANGED, ) from homeassistant.helpers import state +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 3b7109222a4abb..2816b86be843c4 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -1,17 +1,17 @@ """Read status of growatt inverters.""" -import re +import datetime import json import logging -import datetime +import re import growattServer import voluptuous as vol -from homeassistant.util import Throttle -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 4708725fd3b893..c25b4ce9f3d17e 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -1,6 +1,5 @@ """The tests for generic camera component.""" import asyncio - from unittest import mock from homeassistant.setup import async_setup_component diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index 7e356cc03f6dc2..38c7200cce1ccf 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -1,30 +1,31 @@ """The tests for the geojson platform.""" -from asynctest.mock import patch, MagicMock, call +from asynctest.mock import MagicMock, call, patch from homeassistant.components import geo_location -from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.geo_json_events.geo_location import ( - SCAN_INTERVAL, ATTR_EXTERNAL_ID, + SCAN_INTERVAL, SIGNAL_DELETE_ENTITY, SIGNAL_UPDATE_ENTITY, ) +from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.const import ( - CONF_URL, - EVENT_HOMEASSISTANT_START, - CONF_RADIUS, + ATTR_FRIENDLY_NAME, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_LATITUDE, CONF_LONGITUDE, + CONF_RADIUS, + CONF_URL, + EVENT_HOMEASSISTANT_START, ) from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_fire_time_changed import homeassistant.util.dt as dt_util +from tests.common import assert_setup_component, async_fire_time_changed + URL = "http://geo.json.local/geo_json_events.json" CONFIG = { geo_location.DOMAIN: [ diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index 492290b9519202..25243afea78984 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -4,20 +4,21 @@ from unittest.mock import MagicMock, patch from homeassistant.components import sensor +import homeassistant.components.geo_rss_events.sensor as geo_rss_events from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_FRIENDLY_NAME, - EVENT_HOMEASSISTANT_START, ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + from tests.common import ( - get_test_home_assistant, assert_setup_component, fire_time_changed, + get_test_home_assistant, ) -import homeassistant.components.geo_rss_events.sensor as geo_rss_events -import homeassistant.util.dt as dt_util URL = "http://geo.rss.local/geo_rss_events.xml" VALID_CONFIG_WITH_CATEGORIES = { diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 496570ca468fe2..4aace6f5484e8d 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -25,7 +25,6 @@ from tests.common import async_mock_service - GOOGLE_CONFIG = {CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret"} TEST_ENTITY = "calendar.we_are_we_are_a_test_calendar" TEST_ENTITY_NAME = "We are, we are, a... Test Calendar" diff --git a/tests/components/google_domains/test_init.py b/tests/components/google_domains/test_init.py index 1334e46b96f241..80844063b00734 100644 --- a/tests/components/google_domains/test_init.py +++ b/tests/components/google_domains/test_init.py @@ -4,8 +4,8 @@ import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import google_domains +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 13f9eb88fce5ef..15e84b384c08c8 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -4,16 +4,15 @@ import shutil from unittest.mock import patch -import homeassistant.components.tts as tts from homeassistant.components.media_player.const import ( - SERVICE_PLAY_MEDIA, ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, ) +import homeassistant.components.tts as tts from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, mock_service - +from tests.common import assert_setup_component, get_test_home_assistant, mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index a9883c6be66987..8a529f93f721c2 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -1,17 +1,17 @@ """The tests for the Google Wifi platform.""" -import unittest -from unittest.mock import patch, Mock from datetime import datetime, timedelta +import unittest +from unittest.mock import Mock, patch import requests_mock from homeassistant import core as ha -from homeassistant.setup import setup_component import homeassistant.components.google_wifi.sensor as google_wifi from homeassistant.const import STATE_UNKNOWN +from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant NAME = "foo" diff --git a/tests/components/graphite/test_init.py b/tests/components/graphite/test_init.py index f6484f082453d8..dd60f03cd589f2 100644 --- a/tests/components/graphite/test_init.py +++ b/tests/components/graphite/test_init.py @@ -4,16 +4,17 @@ from unittest import mock from unittest.mock import patch -from homeassistant.setup import setup_component -import homeassistant.core as ha import homeassistant.components.graphite as graphite from homeassistant.const import ( - EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - STATE_ON, + EVENT_STATE_CHANGED, STATE_OFF, + STATE_ON, ) +import homeassistant.core as ha +from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant From 8b39957c56b449b984e7349e1e6f334a34eaf46d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:19:48 +0100 Subject: [PATCH 2319/3953] Sort imports according to PEP8 for components starting with "H" (#29768) --- .../components/haveibeenpwned/sensor.py | 2 +- homeassistant/components/hddtemp/sensor.py | 10 +++---- .../components/history_graph/__init__.py | 2 +- .../components/history_stats/sensor.py | 8 ++--- .../components/hitron_coda/device_tracker.py | 6 ++-- homeassistant/components/honeywell/climate.py | 30 +++++++++---------- homeassistant/components/hook/switch.py | 10 +++---- homeassistant/components/html5/notify.py | 22 +++++++------- .../huawei_router/device_tracker.py | 4 +-- tests/components/hddtemp/test_sensor.py | 1 - tests/components/history/test_init.py | 6 ++-- tests/components/history_graph/test_init.py | 3 +- tests/components/history_stats/test_sensor.py | 7 +++-- tests/components/homematic/test_notify.py | 3 +- tests/components/honeywell/test_climate.py | 18 +++++------ tests/components/html5/test_notify.py | 7 +++-- 16 files changed, 69 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 7fa3f422300fb5..a0f30dd1a8b2c4 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_EMAIL, CONF_API_KEY, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_EMAIL import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_point_in_time diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index d0dd5018dca8b6..a1052b0440a11a 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -1,21 +1,21 @@ """Support for getting the disk temperature of a host.""" -import logging from datetime import timedelta -from telnetlib import Telnet +import logging import socket +from telnetlib import Telnet import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, + CONF_DISKS, CONF_HOST, + CONF_NAME, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_DISKS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/history_graph/__init__.py b/homeassistant/components/history_graph/__init__.py index ad8398c75f5b89..2b89556818fb99 100644 --- a/homeassistant/components/history_graph/__init__.py +++ b/homeassistant/components/history_graph/__init__.py @@ -3,8 +3,8 @@ import voluptuous as vol +from homeassistant.const import ATTR_ENTITY_ID, CONF_ENTITIES, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_ENTITIES, CONF_NAME, ATTR_ENTITY_ID from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 5c59b5f8e976ce..0bded03a29cb75 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -5,21 +5,21 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import history -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_ENTITY_ID, + CONF_NAME, CONF_STATE, CONF_TYPE, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index 2f3526d45b6dbc..12b03acbcc503e 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -1,17 +1,17 @@ """Support for the Hitron CODA-4582U, provided by Rogers.""" -import logging from collections import namedtuple +import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TYPE, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 42f4778eb4f44c..f8537bfe96a3cd 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -1,43 +1,43 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" import datetime import logging -from typing import Any, Dict, Optional, List +from typing import Any, Dict, List, Optional import requests -import voluptuous as vol import somecomfort +import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, FAN_AUTO, FAN_DIFFUSE, FAN_ON, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_FAN, - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - PRESET_AWAY, - PRESET_NONE, ) from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_PASSWORD, + CONF_REGION, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_TEMPERATURE, - CONF_REGION, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/hook/switch.py b/homeassistant/components/hook/switch.py index d26f35e2dfcae9..14c4d4ba662991 100644 --- a/homeassistant/components/hook/switch.py +++ b/homeassistant/components/hook/switch.py @@ -1,13 +1,13 @@ """Support Hook, available at hooksmarthome.com.""" -import logging import asyncio +import logging -import voluptuous as vol -import async_timeout import aiohttp +import async_timeout +import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_TOKEN +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 481a00e96e13e1..6d6fcd5c37793b 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -1,23 +1,30 @@ """HTML5 Push Messaging notification service.""" from datetime import datetime, timedelta - from functools import partial -from urllib.parse import urlparse import json import logging import time +from urllib.parse import urlparse import uuid from aiohttp.hdrs import AUTHORIZATION import jwt -from pywebpush import WebPusher from py_vapid import Vapid +from pywebpush import WebPusher import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.components import websocket_api from homeassistant.components.frontend import add_manifest_json_key from homeassistant.components.http import HomeAssistantView +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, @@ -29,15 +36,6 @@ from homeassistant.util import ensure_unique_string from homeassistant.util.json import load_json, save_json -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - from .const import DOMAIN, SERVICE_DISMISS _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_router/device_tracker.py b/homeassistant/components/huawei_router/device_tracker.py index b7b5731dfd3b87..4b52060e425ea8 100644 --- a/homeassistant/components/huawei_router/device_tracker.py +++ b/homeassistant/components/huawei_router/device_tracker.py @@ -1,19 +1,19 @@ """Support for HUAWEI routers.""" import base64 +from collections import namedtuple import logging import re -from collections import namedtuple import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index aeb4ed2ab9b26b..0b4d2d0d4c6c48 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -1,6 +1,5 @@ """The tests for the hddtemp platform.""" import socket - import unittest from unittest.mock import patch diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 68bc9c5371fc4e..051024999e4edc 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -4,15 +4,15 @@ import unittest from unittest.mock import patch, sentinel -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.components import history, recorder import homeassistant.core as ha +from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util -from homeassistant.components import history, recorder from tests.common import ( + get_test_home_assistant, init_recorder_component, mock_state_change_event, - get_test_home_assistant, ) diff --git a/tests/components/history_graph/test_init.py b/tests/components/history_graph/test_init.py index d46bdd02843d1e..ef41f70aaa7b5e 100644 --- a/tests/components/history_graph/test_init.py +++ b/tests/components/history_graph/test_init.py @@ -3,7 +3,8 @@ import unittest from homeassistant.setup import setup_component -from tests.common import init_recorder_component, get_test_home_assistant + +from tests.common import get_test_home_assistant, init_recorder_component class TestGraph(unittest.TestCase): diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 178abf2b152421..492f928c9f0bcc 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -3,17 +3,18 @@ from datetime import datetime, timedelta import unittest from unittest.mock import patch + import pytest import pytz -from homeassistant.const import STATE_UNKNOWN -from homeassistant.setup import setup_component from homeassistant.components.history_stats.sensor import HistoryStatsSensor +from homeassistant.const import STATE_UNKNOWN import homeassistant.core as ha from homeassistant.helpers.template import Template +from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from tests.common import init_recorder_component, get_test_home_assistant +from tests.common import get_test_home_assistant, init_recorder_component class TestHistoryStatsSensor(unittest.TestCase): diff --git a/tests/components/homematic/test_notify.py b/tests/components/homematic/test_notify.py index 967e217f1a76a9..411be41eb39459 100644 --- a/tests/components/homematic/test_notify.py +++ b/tests/components/homematic/test_notify.py @@ -2,8 +2,9 @@ import unittest -from homeassistant.setup import setup_component import homeassistant.components.notify as notify_comp +from homeassistant.setup import setup_component + from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index 9c93eb9a7c7e79..feba4f6410e2d4 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -2,25 +2,23 @@ import unittest from unittest import mock -import voluptuous as vol +import pytest import requests.exceptions import somecomfort -import pytest +import voluptuous as vol -from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) from homeassistant.components.climate.const import ( ATTR_FAN_MODE, ATTR_FAN_MODES, ATTR_HVAC_MODES, ) - import homeassistant.components.honeywell.climate as honeywell - +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) pytestmark = pytest.mark.skip("Need to be fixed!") diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index 481d7a010c94bf..a9fd998f003b81 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -1,11 +1,12 @@ """Test HTML5 notify platform.""" import json -from unittest.mock import patch, MagicMock, mock_open +from unittest.mock import MagicMock, mock_open, patch + from aiohttp.hdrs import AUTHORIZATION -from homeassistant.setup import async_setup_component -from homeassistant.exceptions import HomeAssistantError import homeassistant.components.html5.notify as html5 +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component CONFIG_FILE = "file.conf" From 710680d60454275b06d1387d98b0f158b0fa418d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:20:40 +0100 Subject: [PATCH 2320/3953] use isort to sort imports for components starting with 'm' (#29772) --- homeassistant/components/mailbox/__init__.py | 1 - .../components/mcp23017/binary_sensor.py | 6 ++--- homeassistant/components/mcp23017/switch.py | 6 ++--- homeassistant/components/melissa/climate.py | 8 +++--- .../components/meraki/device_tracker.py | 9 ++++--- .../components/microsoft_face/__init__.py | 2 +- homeassistant/components/mikrotik/__init__.py | 25 ++++++++++--------- .../components/mikrotik/device_tracker.py | 13 +++++----- homeassistant/components/mill/climate.py | 2 +- homeassistant/components/min_max/sensor.py | 8 +++--- homeassistant/components/mjpeg/camera.py | 14 +++++------ .../components/mold_indicator/sensor.py | 7 +++--- .../components/mpchc/media_player.py | 2 +- .../components/mqtt_eventstream/__init__.py | 3 +-- .../components/mqtt_json/device_tracker.py | 10 ++++---- homeassistant/components/mqtt_room/sensor.py | 8 +++--- .../components/mqtt_statestream/__init__.py | 4 +-- homeassistant/components/myq/cover.py | 2 +- .../manual/test_alarm_control_panel.py | 16 ++++++------ .../manual_mqtt/test_alarm_control_panel.py | 14 +++++------ tests/components/marytts/test_tts.py | 9 +++---- .../components/meraki/test_device_tracker.py | 9 ++++--- tests/components/mfi/test_sensor.py | 6 ++--- tests/components/mfi/test_switch.py | 4 +-- tests/components/mhz19/test_sensor.py | 9 ++++--- tests/components/microsoft_face/test_init.py | 4 +-- .../test_image_processing.py | 10 ++++---- .../test_image_processing.py | 10 ++++---- tests/components/min_max/test_sensor.py | 7 +++--- tests/components/mochad/test_switch.py | 2 +- tests/components/modbus/test_modbus_sensor.py | 18 +++++++------ .../components/mold_indicator/test_sensor.py | 6 ++--- .../components/monoprice/test_media_player.py | 25 ++++++++++--------- tests/components/moon/test_sensor.py | 4 +-- .../components/mqtt_eventstream/test_init.py | 6 ++--- .../mqtt_json/test_device_tracker.py | 9 ++++--- tests/components/mqtt_room/test_sensor.py | 6 ++--- .../components/mqtt_statestream/test_init.py | 2 +- tests/components/mythicbeastsdns/test_init.py | 3 ++- 39 files changed, 160 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 1252036e1b29cc..0381d9323285a7 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -16,7 +16,6 @@ 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__) diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py index 088052c469e042..e95b91389cd53b 100644 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ b/homeassistant/components/mcp23017/binary_sensor.py @@ -1,13 +1,13 @@ """Support for binary sensor using I2C MCP23017 chip.""" import logging -import voluptuous as vol +import adafruit_mcp230xx # pylint: disable=import-error import board # pylint: disable=import-error import busio # pylint: disable=import-error -import adafruit_mcp230xx # pylint: disable=import-error import digitalio # pylint: disable=import-error +import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py index 399ed17c44b51b..8506106b705be2 100644 --- a/homeassistant/components/mcp23017/switch.py +++ b/homeassistant/components/mcp23017/switch.py @@ -1,16 +1,16 @@ """Support for switch sensor using I2C MCP23017 chip.""" import logging -import voluptuous as vol +import adafruit_mcp230xx # pylint: disable=import-error import board # pylint: disable=import-error import busio # pylint: disable=import-error -import adafruit_mcp230xx # pylint: disable=import-error import digitalio # pylint: disable=import-error +import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index c09203c3e333e6..4b033811f43186 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -3,6 +3,10 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -11,10 +15,6 @@ HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, - FAN_AUTO, - FAN_HIGH, - FAN_MEDIUM, - FAN_LOW, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index 3f50caa2c8ba7e..1aa1485922e32c 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -5,15 +5,16 @@ https://home-assistant.io/components/device_tracker.meraki/ """ -import logging import json +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv + +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER +from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_BAD_REQUEST, HTTP_UNPROCESSABLE_ENTITY from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER +import homeassistant.helpers.config_validation as cv CONF_VALIDATOR = "validator" CONF_SECRET = "secret" diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 5d0c50e536a554..244c8a0e8ee7c1 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -8,7 +8,7 @@ import async_timeout import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT, ATTR_NAME +from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index aacd3c65b3eb64..9b533288d86295 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -2,34 +2,35 @@ import logging import ssl -import voluptuous as vol import librouteros from librouteros.login import login_plain, login_token +import voluptuous as vol +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.const import ( CONF_HOST, + CONF_METHOD, CONF_PASSWORD, - CONF_USERNAME, CONF_PORT, CONF_SSL, - CONF_METHOD, + CONF_USERNAME, ) 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, + CONF_ARP_PING, + CONF_ENCODING, + CONF_LOGIN_METHOD, + CONF_TRACK_DEVICES, + DEFAULT_ENCODING, DOMAIN, HOSTS, - MTK_LOGIN_PLAIN, - MTK_LOGIN_TOKEN, - DEFAULT_ENCODING, IDENTITY, - CONF_TRACK_DEVICES, - CONF_ENCODING, - CONF_ARP_PING, - CONF_LOGIN_METHOD, MIKROTIK_SERVICES, + MTK_LOGIN_PLAIN, + MTK_LOGIN_TOKEN, + NAME, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 6c3fb559cba750..92fcfac4ae4d90 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -5,18 +5,19 @@ DOMAIN as DEVICE_TRACKER, DeviceScanner, ) -from homeassistant.util import slugify from homeassistant.const import CONF_METHOD +from homeassistant.util import slugify + from .const import ( + ARP, + ATTR_DEVICE_TRACKER, + CAPSMAN, + CONF_ARP_PING, + DHCP, HOSTS, MIKROTIK, - CONF_ARP_PING, MIKROTIK_SERVICES, - CAPSMAN, WIRELESS, - DHCP, - ARP, - ATTR_DEVICE_TRACKER, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index b08015fe548047..875d217247c96b 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -6,11 +6,11 @@ from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + FAN_ON, HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, - FAN_ON, ) from homeassistant.const import ( ATTR_TEMPERATURE, diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 519bba464deb97..977ee51cd1c871 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -3,16 +3,16 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, - STATE_UNKNOWN, - STATE_UNAVAILABLE, CONF_TYPE, - ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index b7cfa20a163d6c..ab0409694d1b12 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -1,7 +1,7 @@ """Support for IP Cameras.""" import asyncio -import logging from contextlib import closing +import logging import aiohttp import async_timeout @@ -9,21 +9,21 @@ from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import ( + CONF_AUTHENTICATION, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, - CONF_AUTHENTICATION, + CONF_USERNAME, + CONF_VERIFY_SSL, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, - CONF_VERIFY_SSL, ) -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import ( - async_get_clientsession, async_aiohttp_proxy_web, + async_get_clientsession, ) -from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index d0e5c7d51dd348..15f8b80a5ab9e8 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -6,20 +6,19 @@ from homeassistant import util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.core import callback from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_NAME, ) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change -import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) ATTR_CRITICAL_TEMP = "estimated_critical_temp" diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py index 580156a565328c..a3f2c500030e45 100644 --- a/homeassistant/components/mpchc/media_player.py +++ b/homeassistant/components/mpchc/media_player.py @@ -5,7 +5,7 @@ import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py index ce11c7cb9331af..48448355df8995 100644 --- a/homeassistant/components/mqtt_eventstream/__init__.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -4,7 +4,6 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic from homeassistant.const import ( ATTR_SERVICE_DATA, @@ -13,7 +12,7 @@ EVENT_TIME_CHANGED, MATCH_ALL, ) -from homeassistant.core import EventOrigin, State +from homeassistant.core import EventOrigin, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.json import JSONEncoder diff --git a/homeassistant/components/mqtt_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index a7dda18e7e6084..8f64636b817423 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -5,17 +5,17 @@ import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.core import callback -from homeassistant.components.mqtt import CONF_QOS from homeassistant.components.device_tracker import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv +from homeassistant.components.mqtt import CONF_QOS from homeassistant.const import ( - CONF_DEVICES, + ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_BATTERY_LEVEL, + CONF_DEVICES, ) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 5323383e8929d7..d8dfa65f7990b7 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -1,16 +1,16 @@ """Support for MQTT room presence detection.""" -import logging -import json from datetime import timedelta +import json +import logging import voluptuous as vol from homeassistant.components import mqtt -import homeassistant.helpers.config_validation as cv from homeassistant.components.mqtt import CONF_STATE_TOPIC from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_TIMEOUT, STATE_NOT_HOME, ATTR_ID +from homeassistant.const import ATTR_ID, CONF_NAME, CONF_TIMEOUT, STATE_NOT_HOME from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import dt, slugify diff --git a/homeassistant/components/mqtt_statestream/__init__.py b/homeassistant/components/mqtt_statestream/__init__.py index f99b341df5b3e2..e35f26532837ca 100644 --- a/homeassistant/components/mqtt_statestream/__init__.py +++ b/homeassistant/components/mqtt_statestream/__init__.py @@ -3,6 +3,7 @@ import voluptuous as vol +from homeassistant.components.mqtt import valid_publish_topic from homeassistant.const import ( CONF_DOMAINS, CONF_ENTITIES, @@ -11,11 +12,10 @@ MATCH_ALL, ) from homeassistant.core import callback -from homeassistant.components.mqtt import valid_publish_topic +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.json import JSONEncoder -import homeassistant.helpers.config_validation as cv CONF_BASE_TOPIC = "base_topic" CONF_PUBLISH_ATTRIBUTES = "publish_attributes" diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index b6da7174f05227..8a83f398e64e9a 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -6,10 +6,10 @@ import voluptuous as vol from homeassistant.components.cover import ( - CoverDevice, PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, + CoverDevice, ) from homeassistant.const import ( CONF_PASSWORD, diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index 666d906ff03c54..1b06477750b9c9 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -1,22 +1,24 @@ """The tests for the manual Alarm Control Panel component.""" from datetime import timedelta -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + +from homeassistant.components import alarm_control_panel from homeassistant.components.demo import alarm_control_panel as demo -from homeassistant.setup import async_setup_component from homeassistant.const import ( - STATE_ALARM_DISARMED, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.components import alarm_control_panel +from homeassistant.core import CoreState, State +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util + from tests.common import async_fire_time_changed, mock_component, mock_restore_cache from tests.components.alarm_control_panel import common -from homeassistant.core import State, CoreState CODE = "HELLO_CODE" diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 80a1aa8495dc43..91e976855883e2 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -1,26 +1,26 @@ """The tests for the manual_mqtt Alarm Control Panel component.""" from datetime import timedelta import unittest -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch -from homeassistant.setup import setup_component +from homeassistant.components import alarm_control_panel from homeassistant.const import ( - STATE_ALARM_DISARMED, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) -from homeassistant.components import alarm_control_panel +from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util from tests.common import ( + assert_setup_component, + fire_mqtt_message, fire_time_changed, get_test_home_assistant, mock_mqtt_component, - fire_mqtt_message, - assert_setup_component, ) from tests.components.alarm_control_panel import common diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 5692d72d388ae8..65d4ab7e39cff5 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -3,15 +3,14 @@ import os import shutil -import homeassistant.components.tts as tts -from homeassistant.setup import setup_component from homeassistant.components.media_player.const import ( - SERVICE_PLAY_MEDIA, DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, ) +import homeassistant.components.tts as tts +from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, mock_service - +from tests.common import assert_setup_component, get_test_home_assistant, mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 diff --git a/tests/components/meraki/test_device_tracker.py b/tests/components/meraki/test_device_tracker.py index 9efd9037b6786e..360eb67a1ff9c9 100644 --- a/tests/components/meraki/test_device_tracker.py +++ b/tests/components/meraki/test_device_tracker.py @@ -4,11 +4,14 @@ import pytest -from homeassistant.components.meraki.device_tracker import CONF_VALIDATOR, CONF_SECRET -from homeassistant.setup import async_setup_component import homeassistant.components.device_tracker as device_tracker +from homeassistant.components.meraki.device_tracker import ( + CONF_SECRET, + CONF_VALIDATOR, + URL, +) from homeassistant.const import CONF_PLATFORM -from homeassistant.components.meraki.device_tracker import URL +from homeassistant.setup import async_setup_component @pytest.fixture diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index 6849578bbb9ac0..05f175fc191483 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -2,13 +2,13 @@ import unittest import unittest.mock as mock -import requests from mficlient.client import FailedToLogin +import requests -from homeassistant.setup import setup_component -import homeassistant.components.sensor as sensor import homeassistant.components.mfi.sensor as mfi +import homeassistant.components.sensor as sensor from homeassistant.const import TEMP_CELSIUS +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index ebddc8c5bc25a3..42469b1b5acd37 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -2,9 +2,9 @@ import unittest import unittest.mock as mock -from homeassistant.setup import setup_component -import homeassistant.components.switch as switch import homeassistant.components.mfi.switch as mfi +import homeassistant.components.switch as switch +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/mhz19/test_sensor.py b/tests/components/mhz19/test_sensor.py index 06a1f1462378fa..5eab93a30ffad5 100644 --- a/tests/components/mhz19/test_sensor.py +++ b/tests/components/mhz19/test_sensor.py @@ -1,12 +1,13 @@ """Tests for MH-Z19 sensor.""" import unittest -from unittest.mock import patch, DEFAULT, Mock +from unittest.mock import DEFAULT, Mock, patch -from homeassistant.setup import setup_component -from homeassistant.components.sensor import DOMAIN import homeassistant.components.mhz19.sensor as mhz19 +from homeassistant.components.sensor import DOMAIN from homeassistant.const import TEMP_FAHRENHEIT -from tests.common import get_test_home_assistant, assert_setup_component +from homeassistant.setup import setup_component + +from tests.common import assert_setup_component, get_test_home_assistant class TestMHZ19Sensor(unittest.TestCase): diff --git a/tests/components/microsoft_face/test_init.py b/tests/components/microsoft_face/test_init.py index 26a4777860374b..24d67f56fb5acc 100644 --- a/tests/components/microsoft_face/test_init.py +++ b/tests/components/microsoft_face/test_init.py @@ -19,10 +19,10 @@ from homeassistant.setup import setup_component from tests.common import ( - get_test_home_assistant, assert_setup_component, - mock_coro, + get_test_home_assistant, load_fixture, + mock_coro, ) diff --git a/tests/components/microsoft_face_detect/test_image_processing.py b/tests/components/microsoft_face_detect/test_image_processing.py index b25b3453825b83..1b01ee7434c07d 100644 --- a/tests/components/microsoft_face_detect/test_image_processing.py +++ b/tests/components/microsoft_face_detect/test_image_processing.py @@ -1,15 +1,15 @@ """The tests for the microsoft face detect platform.""" -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch -from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_PICTURE -from homeassistant.setup import setup_component import homeassistant.components.image_processing as ip import homeassistant.components.microsoft_face as mf +from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.core import callback +from homeassistant.setup import setup_component from tests.common import ( - get_test_home_assistant, assert_setup_component, + get_test_home_assistant, load_fixture, mock_coro, ) diff --git a/tests/components/microsoft_face_identify/test_image_processing.py b/tests/components/microsoft_face_identify/test_image_processing.py index 95f314dbc21ee1..311d463bc1de5e 100644 --- a/tests/components/microsoft_face_identify/test_image_processing.py +++ b/tests/components/microsoft_face_identify/test_image_processing.py @@ -1,15 +1,15 @@ """The tests for the microsoft face identify platform.""" -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch -from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_PICTURE, STATE_UNKNOWN -from homeassistant.setup import setup_component import homeassistant.components.image_processing as ip import homeassistant.components.microsoft_face as mf +from homeassistant.const import ATTR_ENTITY_PICTURE, STATE_UNKNOWN +from homeassistant.core import callback +from homeassistant.setup import setup_component from tests.common import ( - get_test_home_assistant, assert_setup_component, + get_test_home_assistant, load_fixture, mock_coro, ) diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 7fa0dee5aa3f76..fcc9a72cf7b219 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -1,14 +1,15 @@ """The test for the min/max sensor platform.""" import unittest -from homeassistant.setup import setup_component from homeassistant.const import ( - STATE_UNKNOWN, - STATE_UNAVAILABLE, ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index 8c0dc3554dbb14..aa6ce354a32a50 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -4,9 +4,9 @@ import pytest -from homeassistant.setup import setup_component from homeassistant.components import switch from homeassistant.components.mochad import switch as mochad +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index 82d0b4bd5f0dac..9f13cba8907042 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -1,14 +1,9 @@ """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, -) +import pytest + from homeassistant.components.modbus import DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN from homeassistant.components.modbus.sensor import ( CONF_COUNT, @@ -26,9 +21,16 @@ REGISTER_TYPE_INPUT, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + CONF_NAME, + CONF_OFFSET, + CONF_PLATFORM, + CONF_SCAN_INTERVAL, +) 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 + +from tests.common import MockModule, async_fire_time_changed, mock_integration @pytest.fixture() diff --git a/tests/components/mold_indicator/test_sensor.py b/tests/components/mold_indicator/test_sensor.py index d0a08dd25d42ee..ce0450d3304f05 100644 --- a/tests/components/mold_indicator/test_sensor.py +++ b/tests/components/mold_indicator/test_sensor.py @@ -1,13 +1,13 @@ """The tests for the MoldIndicator sensor.""" import unittest -from homeassistant.setup import setup_component -import homeassistant.components.sensor as sensor from homeassistant.components.mold_indicator.sensor import ( - ATTR_DEWPOINT, ATTR_CRITICAL_TEMP, + ATTR_DEWPOINT, ) +import homeassistant.components.sensor as sensor from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index cb064048d7b829..e6747dfc4bf726 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -1,31 +1,32 @@ """The tests for Monoprice Media player platform.""" +from collections import defaultdict import unittest from unittest import mock + +import pytest import voluptuous as vol -from collections import defaultdict from homeassistant.components.media_player.const import ( - SUPPORT_TURN_ON, + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - SUPPORT_SELECT_SOURCE, -) -from homeassistant.const import STATE_ON, STATE_OFF - -import tests.common -from homeassistant.components.monoprice.media_player import ( - DATA_MONOPRICE, - PLATFORM_SCHEMA, - setup_platform, ) from homeassistant.components.monoprice.const import ( DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT, ) -import pytest +from homeassistant.components.monoprice.media_player import ( + DATA_MONOPRICE, + PLATFORM_SCHEMA, + setup_platform, +) +from homeassistant.const import STATE_OFF, STATE_ON + +import tests.common class AttrDict(dict): diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 95b1a1c305ff09..1e19d0a4d83cc1 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -1,10 +1,10 @@ """The test for the moon sensor platform.""" -import unittest from datetime import datetime +import unittest from unittest.mock import patch -import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index b5ebe3c1ae1868..f4062458d91e99 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -2,19 +2,19 @@ import json from unittest.mock import ANY, patch -from homeassistant.setup import setup_component import homeassistant.components.mqtt_eventstream as eventstream from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.core import State, callback from homeassistant.helpers.json import JSONEncoder +from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util from tests.common import ( + fire_mqtt_message, + fire_time_changed, get_test_home_assistant, mock_mqtt_component, - fire_mqtt_message, mock_state_change_event, - fire_time_changed, ) diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index 00be001840b01d..7f3f806da5268f 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -2,18 +2,19 @@ import json import logging import os + from asynctest import patch import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.device_tracker.legacy import ( - YAML_DEVICES, - ENTITY_ID_FORMAT, DOMAIN as DT_DOMAIN, + ENTITY_ID_FORMAT, + YAML_DEVICES, ) from homeassistant.const import CONF_PLATFORM +from homeassistant.setup import async_setup_component -from tests.common import async_mock_mqtt_component, async_fire_mqtt_message +from tests.common import async_fire_mqtt_message, async_mock_mqtt_component _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index 07b8f89ef3fd26..e8a9e62403f7a9 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -1,12 +1,12 @@ """The tests for the MQTT room presence sensor.""" -import json import datetime +import json from unittest.mock import patch -from homeassistant.setup import async_setup_component +from homeassistant.components.mqtt import CONF_QOS, CONF_STATE_TOPIC, DEFAULT_QOS import homeassistant.components.sensor as sensor -from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS, DEFAULT_QOS from homeassistant.const import CONF_NAME, CONF_PLATFORM +from homeassistant.setup import async_setup_component from homeassistant.util import dt from tests.common import async_fire_mqtt_message, async_mock_mqtt_component diff --git a/tests/components/mqtt_statestream/test_init.py b/tests/components/mqtt_statestream/test_init.py index 280b61a490ccd9..ffab0e0846f083 100644 --- a/tests/components/mqtt_statestream/test_init.py +++ b/tests/components/mqtt_statestream/test_init.py @@ -1,9 +1,9 @@ """The tests for the MQTT statestream component.""" from unittest.mock import ANY, call, patch -from homeassistant.setup import setup_component import homeassistant.components.mqtt_statestream as statestream from homeassistant.core import State +from homeassistant.setup import setup_component from tests.common import ( get_test_home_assistant, diff --git a/tests/components/mythicbeastsdns/test_init.py b/tests/components/mythicbeastsdns/test_init.py index c62b7241510692..ee037a029edf48 100644 --- a/tests/components/mythicbeastsdns/test_init.py +++ b/tests/components/mythicbeastsdns/test_init.py @@ -1,9 +1,10 @@ """Test the Mythic Beasts DNS component.""" import logging + import asynctest -from homeassistant.setup import async_setup_component from homeassistant.components import mythicbeastsdns +from homeassistant.setup import async_setup_component _LOGGER = logging.getLogger(__name__) From 14779ce3d0955c2f7f98ce5fd5c665ac24586fdc Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:21:24 +0100 Subject: [PATCH 2321/3953] Sort imports according to PEP8 for components starting with "I" (#29769) --- .../components/imap_email_content/sensor.py | 12 +++++------ .../input_boolean/reproduce_state.py | 4 ++-- .../input_number/reproduce_state.py | 2 +- .../input_select/reproduce_state.py | 4 ++-- .../components/integration/sensor.py | 9 ++++---- homeassistant/components/intent/__init__.py | 8 +++---- .../components/intent_script/__init__.py | 2 +- homeassistant/components/ios/config_flow.py | 4 ++-- .../components/itunes/media_player.py | 4 ++-- .../ign_sismologia/test_geo_location.py | 21 ++++++++++--------- .../components/image_processing/test_init.py | 12 +++++------ .../imap_email_content/test_sensor.py | 6 +++--- tests/components/influxdb/test_init.py | 2 +- tests/components/input_select/test_init.py | 6 +++--- tests/components/intent/test_init.py | 4 ++-- tests/components/ios/test_init.py | 2 +- tests/components/ipma/test_config_flow.py | 6 +++--- tests/components/ipma/test_weather.py | 4 ++-- .../islamic_prayer_times/test_sensor.py | 4 +++- tests/components/izone/test_config_flow.py | 2 +- 20 files changed, 60 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index 62dceae0dadcb5..307d5a22c1e4ea 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -1,24 +1,24 @@ """Email sensor support.""" -import logging +from collections import deque import datetime import email -from collections import deque - import imaplib +import logging + import voluptuous as vol -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_DATE, CONF_NAME, + CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - CONF_PASSWORD, CONF_VALUE_TEMPLATE, CONTENT_TYPE_TEXT_PLAIN, - ATTR_DATE, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/input_boolean/reproduce_state.py b/homeassistant/components/input_boolean/reproduce_state.py index b8bc18edfac9a7..558d57ae862072 100644 --- a/homeassistant/components/input_boolean/reproduce_state.py +++ b/homeassistant/components/input_boolean/reproduce_state.py @@ -4,11 +4,11 @@ from typing import Iterable, Optional from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_ON, STATE_OFF, - ATTR_ENTITY_ID, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py index 97a4837d3711de..22a91f7400077f 100644 --- a/homeassistant/components/input_number/reproduce_state.py +++ b/homeassistant/components/input_number/reproduce_state.py @@ -7,7 +7,7 @@ from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE +from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py index 657f518cd3d5d6..818510bee4a370 100644 --- a/homeassistant/components/input_select/reproduce_state.py +++ b/homeassistant/components/input_select/reproduce_state.py @@ -9,11 +9,11 @@ from homeassistant.helpers.typing import HomeAssistantType from . import ( + ATTR_OPTION, + ATTR_OPTIONS, DOMAIN, SERVICE_SELECT_OPTION, SERVICE_SET_OPTIONS, - ATTR_OPTION, - ATTR_OPTIONS, ) ATTR_GROUP = [ATTR_OPTION, ATTR_OPTIONS] diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 236a996794a7ab..560a7cbd33ceb8 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -1,22 +1,21 @@ """Numeric integration of data coming from a source sensor over time.""" +from decimal import Decimal, DecimalException import logging -from decimal import Decimal, DecimalException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, ATTR_UNIT_OF_MEASUREMENT, - STATE_UNKNOWN, + CONF_NAME, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv 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__) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 31ab36ecc89f65..53960851f6cc41 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -4,13 +4,13 @@ import voluptuous as vol -from homeassistant.core import HomeAssistant -from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.setup import ATTR_COMPONENT from homeassistant.components import http from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, intent -from homeassistant.loader import async_get_integration, IntegrationNotFound +from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.setup import ATTR_COMPONENT from .const import DOMAIN diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index ce4b8b27a51a97..38f93ed3506125 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -4,7 +4,7 @@ import voluptuous as vol -from homeassistant.helpers import intent, template, script, config_validation as cv +from homeassistant.helpers import config_validation as cv, intent, script, template DOMAIN = "intent_script" diff --git a/homeassistant/components/ios/config_flow.py b/homeassistant/components/ios/config_flow.py index 511e350aae3a0d..9eaca389ba1120 100644 --- a/homeassistant/components/ios/config_flow.py +++ b/homeassistant/components/ios/config_flow.py @@ -1,8 +1,8 @@ """Config flow for iOS.""" -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries -from .const import DOMAIN +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN config_entry_flow.register_discovery_flow( DOMAIN, "Home Assistant iOS", lambda *_: True, config_entries.CONN_CLASS_CLOUD_PUSH diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index aebe16ffa26616..112a9c609d836a 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -4,7 +4,7 @@ import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -14,11 +14,11 @@ SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SHUFFLE_SET, ) from homeassistant.const import ( CONF_HOST, diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index 4babbb6a4254a4..2d869c1a0622b5 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -1,34 +1,35 @@ """The tests for the IGN Sismologia (Earthquakes) Feed platform.""" import datetime -from unittest.mock import patch, MagicMock, call +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.ign_sismologia.geo_location import ( ATTR_EXTERNAL_ID, - SCAN_INTERVAL, - ATTR_REGION, - ATTR_MAGNITUDE, ATTR_IMAGE_URL, + ATTR_MAGNITUDE, ATTR_PUBLICATION_DATE, + ATTR_REGION, ATTR_TITLE, + SCAN_INTERVAL, ) from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - CONF_RADIUS, + ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ICON, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_fire_time_changed import homeassistant.util.dt as dt_util +from tests.common import assert_setup_component, async_fire_time_changed + CONFIG = {geo_location.DOMAIN: [{"platform": "ign_sismologia", CONF_RADIUS: 200}]} CONFIG_WITH_CUSTOM_LOCATION = { diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 88c870c78fb1db..3503fcfb9a2ef6 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -1,17 +1,17 @@ """The tests for the image_processing component.""" -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch -from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_PICTURE -from homeassistant.setup import setup_component -from homeassistant.exceptions import HomeAssistantError import homeassistant.components.http as http import homeassistant.components.image_processing as ip +from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import setup_component from tests.common import ( + assert_setup_component, get_test_home_assistant, get_test_instance_port, - assert_setup_component, ) from tests.components.image_processing import common diff --git a/tests/components/imap_email_content/test_sensor.py b/tests/components/imap_email_content/test_sensor.py index fcb9da6ddf3c91..ee39bac51ef0dc 100644 --- a/tests/components/imap_email_content/test_sensor.py +++ b/tests/components/imap_email_content/test_sensor.py @@ -1,14 +1,14 @@ """The tests for the IMAP email content sensor platform.""" from collections import deque +import datetime import email from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText -import datetime import unittest -from homeassistant.helpers.template import Template -from homeassistant.helpers.event import track_state_change from homeassistant.components.imap_email_content import sensor as imap_email_content +from homeassistant.helpers.event import track_state_change +from homeassistant.helpers.template import Template from tests.common import get_test_home_assistant diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 3e0e1dcf7066a9..1dd2681b7f2cf9 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -3,9 +3,9 @@ import unittest from unittest import mock -from homeassistant.setup import setup_component import homeassistant.components.influxdb as influxdb from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, STATE_STANDBY +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index cbf9bd5f4ee60c..6c5d8501239d94 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -2,18 +2,18 @@ # pylint: disable=protected-access import asyncio -from homeassistant.loader import bind_hass from homeassistant.components.input_select import ( ATTR_OPTION, ATTR_OPTIONS, DOMAIN, - SERVICE_SET_OPTIONS, SERVICE_SELECT_NEXT, SERVICE_SELECT_OPTION, SERVICE_SELECT_PREVIOUS, + SERVICE_SET_OPTIONS, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON -from homeassistant.core import State, Context +from homeassistant.core import Context, State +from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component from tests.common import mock_restore_cache diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 76a0399c68876a..56344b6affe5a1 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -1,9 +1,9 @@ """Tests for Intent component.""" import pytest -from homeassistant.setup import async_setup_component -from homeassistant.helpers import intent from homeassistant.components.cover import SERVICE_OPEN_COVER +from homeassistant.helpers import intent +from homeassistant.setup import async_setup_component from tests.common import async_mock_service diff --git a/tests/components/ios/test_init.py b/tests/components/ios/test_init.py index 9a1699872983a0..31eb43fc611b86 100644 --- a/tests/components/ios/test_init.py +++ b/tests/components/ios/test_init.py @@ -4,8 +4,8 @@ import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.setup import async_setup_component from homeassistant.components import ios +from homeassistant.setup import async_setup_component from tests.common import mock_component, mock_coro diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index 0850a15d620d8f..fd44f8b2a58742 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,10 +1,10 @@ """Tests for IPMA config flow.""" from unittest.mock import Mock, patch -from tests.common import mock_coro - -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.components.ipma import config_flow +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import mock_coro async def test_show_config_form(): diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 5e63f6fa5c7170..de13d3c94b25be 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -1,6 +1,6 @@ """The tests for the IPMA weather component.""" -from unittest.mock import patch from collections import namedtuple +from unittest.mock import patch from homeassistant.components import weather from homeassistant.components.weather import ( @@ -11,9 +11,9 @@ ATTR_WEATHER_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, ) +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_coro -from homeassistant.setup import async_setup_component TEST_CONFIG = {"name": "HomeTown", "latitude": "40.00", "longitude": "-8.00"} diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index ad229404a300fd..389fa43945eea8 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -1,9 +1,11 @@ """The tests for the Islamic prayer times sensor platform.""" from datetime import datetime, timedelta from unittest.mock import patch -from homeassistant.setup import async_setup_component + from homeassistant.components.islamic_prayer_times.sensor import IslamicPrayerTimesData +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util + from tests.common import async_fire_time_changed LATITUDE = 41 diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index b5f9aa41c80b17..5deafeb08a7b48 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -5,7 +5,7 @@ import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components.izone.const import IZONE, DISPATCH_CONTROLLER_DISCOVERED +from homeassistant.components.izone.const import DISPATCH_CONTROLLER_DISCOVERED, IZONE from tests.common import mock_coro From 1dea0c9e340a4f03b4ec71f8112e99e63221ab50 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:22:30 +0100 Subject: [PATCH 2322/3953] Sort imports according to PEP8 for components starting with "L" (#29771) --- homeassistant/components/lannouncer/notify.py | 5 ++- homeassistant/components/lcn/__init__.py | 2 +- homeassistant/components/lcn/services.py | 2 +- homeassistant/components/lifx_cloud/scene.py | 2 +- .../linksys_smart/device_tracker.py | 2 +- .../components/liveboxplaytv/media_player.py | 2 +- .../components/llamalab_automate/notify.py | 3 +- homeassistant/components/local_file/camera.py | 4 +-- homeassistant/components/lockitron/lock.py | 4 +-- .../components/logentries/__init__.py | 4 +-- homeassistant/components/logger/__init__.py | 2 +- tests/components/litejet/test_init.py | 1 + tests/components/logbook/test_init.py | 36 +++++++++---------- tests/components/logentries/test_init.py | 4 +-- tests/components/logger/test_init.py | 2 +- .../logi_circle/test_config_flow.py | 2 +- tests/components/london_air/test_sensor.py | 4 ++- tests/components/lovelace/test_init.py | 4 +-- 18 files changed, 42 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/lannouncer/notify.py b/homeassistant/components/lannouncer/notify.py index 9512a75047bd10..9421eb16f51eef 100644 --- a/homeassistant/components/lannouncer/notify.py +++ b/homeassistant/components/lannouncer/notify.py @@ -5,14 +5,13 @@ import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_PORT -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv ATTR_METHOD = "method" ATTR_METHOD_DEFAULT = "speak" diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index f7170340f1b333..14f25be70b0d88 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -4,7 +4,6 @@ import pypck import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.const import ( CONF_ADDRESS, @@ -22,6 +21,7 @@ TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index aba29e55176eda..3c775224623c9d 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -2,13 +2,13 @@ import pypck import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_ADDRESS, CONF_BRIGHTNESS, CONF_STATE, CONF_UNIT_OF_MEASUREMENT, ) +import homeassistant.helpers.config_validation as cv from .const import ( CONF_CONNECTIONS, diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index ac4e0201fb87f5..4068ff20fe297c 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.scene import Scene -from homeassistant.const import CONF_TOKEN, CONF_TIMEOUT, CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM, CONF_TIMEOUT, CONF_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index 1af84a4c4ab216..a2a8e317133d39 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -4,13 +4,13 @@ import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv DEFAULT_TIMEOUT = 10 diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py index 996b4f33b5094e..66fb383d677b2a 100644 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ b/homeassistant/components/liveboxplaytv/media_player.py @@ -7,7 +7,7 @@ import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, diff --git a/homeassistant/components/llamalab_automate/notify.py b/homeassistant/components/llamalab_automate/notify.py index ab6a7032208d08..5a3d4e0df38b68 100644 --- a/homeassistant/components/llamalab_automate/notify.py +++ b/homeassistant/components/llamalab_automate/notify.py @@ -4,11 +4,10 @@ import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_API_KEY, CONF_DEVICE from homeassistant.helpers import config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://llamalab.com/automate/cloud/message" diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index 9bd476cfb275ca..1d06efeb708554 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -5,12 +5,12 @@ import voluptuous as vol -from homeassistant.const import CONF_NAME, ATTR_ENTITY_ID from homeassistant.components.camera import ( - Camera, CAMERA_SERVICE_SCHEMA, PLATFORM_SCHEMA, + Camera, ) +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME from homeassistant.helpers import config_validation as cv from .const import ( diff --git a/homeassistant/components/lockitron/lock.py b/homeassistant/components/lockitron/lock.py index b993f644ecd599..5840c7f553752b 100644 --- a/homeassistant/components/lockitron/lock.py +++ b/homeassistant/components/lockitron/lock.py @@ -4,9 +4,9 @@ import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ID +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/logentries/__init__.py b/homeassistant/components/logentries/__init__.py index 3601ee275b852b..55d1ab7aae6cf0 100644 --- a/homeassistant/components/logentries/__init__.py +++ b/homeassistant/components/logentries/__init__.py @@ -1,13 +1,13 @@ """Support for sending data to Logentries webhook endpoint.""" import json import logging -import requests +import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_TOKEN, EVENT_STATE_CHANGED from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index a2ed19f92b1c0d..8043469d43b396 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -1,6 +1,6 @@ """Support for settting the level of logging for components.""" -import logging from collections import OrderedDict +import logging import voluptuous as vol diff --git a/tests/components/litejet/test_init.py b/tests/components/litejet/test_init.py index 8dd1440a7bc534..3861e7a058ebb5 100644 --- a/tests/components/litejet/test_init.py +++ b/tests/components/litejet/test_init.py @@ -3,6 +3,7 @@ import unittest from homeassistant.components import litejet + from tests.common import get_test_home_assistant _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 6ff043dff281a1..1b48f301529b90 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1,41 +1,39 @@ """The tests for the logbook component.""" # pylint: disable=protected-access,invalid-name +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime import unittest import pytest import voluptuous as vol -from homeassistant.components import sun -import homeassistant.core as ha +from homeassistant.components import logbook, recorder, sun +from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME +from homeassistant.components.homekit.const import ( + ATTR_DISPLAY_NAME, + ATTR_VALUE, + DOMAIN as DOMAIN_HOMEKIT, + EVENT_HOMEKIT_CHANGED, +) from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_SERVICE, + ATTR_HIDDEN, ATTR_NAME, - EVENT_STATE_CHANGED, + ATTR_SERVICE, + EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED, - ATTR_HIDDEN, + EVENT_STATE_CHANGED, STATE_NOT_HOME, - STATE_ON, STATE_OFF, + STATE_ON, ) +import homeassistant.core as ha +from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util -from homeassistant.components import logbook, recorder -from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME -from homeassistant.components.homekit.const import ( - ATTR_DISPLAY_NAME, - ATTR_VALUE, - DOMAIN as DOMAIN_HOMEKIT, - EVENT_HOMEKIT_CHANGED, -) -from homeassistant.setup import setup_component, async_setup_component - -from tests.common import init_recorder_component, get_test_home_assistant +from tests.common import get_test_home_assistant, init_recorder_component _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/logentries/test_init.py b/tests/components/logentries/test_init.py index 8c69ec5ff87dde..7125822e93e06b 100644 --- a/tests/components/logentries/test_init.py +++ b/tests/components/logentries/test_init.py @@ -3,9 +3,9 @@ import unittest from unittest import mock -from homeassistant.setup import setup_component import homeassistant.components.logentries as logentries -from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index eac6060a5bc0ab..00fa5aa35580ee 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -3,8 +3,8 @@ import logging import unittest -from homeassistant.setup import setup_component from homeassistant.components import logger +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 0e2ef29da94e3d..7ba3816b5e2b74 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -8,8 +8,8 @@ from homeassistant.components.logi_circle import config_flow from homeassistant.components.logi_circle.config_flow import ( DOMAIN, - LogiCircleAuthCallbackView, AuthorizationFailed, + LogiCircleAuthCallbackView, ) from homeassistant.setup import async_setup_component diff --git a/tests/components/london_air/test_sensor.py b/tests/components/london_air/test_sensor.py index cd1ee32f2231dc..83405095f2ef35 100644 --- a/tests/components/london_air/test_sensor.py +++ b/tests/components/london_air/test_sensor.py @@ -1,10 +1,12 @@ """The tests for the tube_state platform.""" import unittest + import requests_mock from homeassistant.components.london_air.sensor import CONF_LOCATIONS, URL from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant + +from tests.common import get_test_home_assistant, load_fixture VALID_CONFIG = {"platform": "london_air", CONF_LOCATIONS: ["Merton"]} diff --git a/tests/components/lovelace/test_init.py b/tests/components/lovelace/test_init.py index 16aab911fb0abd..e8d041e9dfaef6 100644 --- a/tests/components/lovelace/test_init.py +++ b/tests/components/lovelace/test_init.py @@ -1,10 +1,10 @@ """Test the Lovelace initialization.""" from unittest.mock import patch -from homeassistant.setup import async_setup_component from homeassistant.components import frontend, lovelace +from homeassistant.setup import async_setup_component -from tests.common import get_system_health_info, async_capture_events +from tests.common import async_capture_events, get_system_health_info async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage): From c49e423c78c425ea0906669fde391e7f9673fac7 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:25:18 +0100 Subject: [PATCH 2323/3953] Sort imports according to PEP8 for components starting with "K" (#29770) --- homeassistant/components/kankun/switch.py | 8 ++++---- homeassistant/components/keba/binary_sensor.py | 4 ++-- homeassistant/components/keba/sensor.py | 3 +-- homeassistant/components/keyboard_remote/__init__.py | 7 ++++--- tests/components/kira/test_init.py | 3 +-- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py index 63f289862f636e..4f7ba5c8b06004 100644 --- a/homeassistant/components/kankun/switch.py +++ b/homeassistant/components/kankun/switch.py @@ -4,15 +4,15 @@ import requests import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PORT, - CONF_PATH, - CONF_USERNAME, CONF_PASSWORD, + CONF_PATH, + CONF_PORT, CONF_SWITCHES, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py index 8c0503a2020f0b..ac7326cc92eaf5 100644 --- a/homeassistant/components/keba/binary_sensor.py +++ b/homeassistant/components/keba/binary_sensor.py @@ -1,12 +1,12 @@ """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_PLUG, DEVICE_CLASS_POWER, DEVICE_CLASS_SAFETY, + BinarySensorDevice, ) from . import DOMAIN diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index f46b2f0cf9023d..dfa04f95c65a3f 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -1,9 +1,8 @@ """Support for KEBA charging station sensors.""" import logging -from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR from homeassistant.helpers.entity import Entity -from homeassistant.const import DEVICE_CLASS_POWER from . import DOMAIN diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index d4ed6128cbe7f7..24889a3f82095a 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -1,13 +1,14 @@ """Receive signals from a keyboard and use it as a remote control.""" # pylint: disable=import-error -import logging import asyncio +import logging -from evdev import InputDevice, categorize, ecodes, list_devices import aionotify +from evdev import InputDevice, categorize, ecodes, list_devices import voluptuous as vol -import homeassistant.helpers.config_validation as cv + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index 9588c2bc1fabc6..e505623512778e 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -3,9 +3,8 @@ import os import shutil import tempfile - import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import homeassistant.components.kira as kira from homeassistant.setup import setup_component From 5cdaff5405e3dbac64a1d2b52ae54180bdd9801a Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:26:53 +0100 Subject: [PATCH 2324/3953] Sort imports according to PEP8 for components starting with "O" (#29774) --- homeassistant/components/obihai/sensor.py | 9 +++------ homeassistant/components/octoprint/__init__.py | 10 +++++----- homeassistant/components/onewire/sensor.py | 8 ++++---- homeassistant/components/onkyo/media_player.py | 8 ++++---- .../openalpr_cloud/image_processing.py | 12 ++++++------ .../openalpr_local/image_processing.py | 16 ++++++++-------- .../components/openexchangerates/sensor.py | 4 ++-- homeassistant/components/opengarage/cover.py | 10 +++++----- homeassistant/components/opensky/sensor.py | 15 +++++++-------- homeassistant/components/oru/sensor.py | 6 ++---- tests/components/onboarding/test_init.py | 8 ++++---- tests/components/onboarding/test_views.py | 6 +++--- .../openalpr_cloud/test_image_processing.py | 8 ++++---- .../openalpr_local/test_image_processing.py | 8 ++++---- .../openhardwaremonitor/test_sensor.py | 5 ++++- tests/components/openuv/test_config_flow.py | 5 +++-- 16 files changed, 68 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 89bfee7d4eebfe..13d09de05425ef 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -1,10 +1,9 @@ """Support for Obihai Sensors.""" -import logging - from datetime import timedelta -import voluptuous as vol +import logging from pyobihai import PyObihai +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( @@ -13,10 +12,8 @@ CONF_USERNAME, DEVICE_CLASS_TIMESTAMP, ) - -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv - +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index bc71b1a59116f0..7564330e499ade 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -2,23 +2,23 @@ import logging import time +from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol -from aiohttp.hdrs import CONTENT_TYPE from homeassistant.components.discovery import SERVICE_OCTOPRINT from homeassistant.const import ( CONF_API_KEY, + CONF_BINARY_SENSORS, CONF_HOST, - CONTENT_TYPE_JSON, + CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PATH, CONF_PORT, + CONF_SENSORS, CONF_SSL, + CONTENT_TYPE_JSON, TEMP_CELSIUS, - CONF_MONITORED_CONDITIONS, - CONF_SENSORS, - CONF_BINARY_SENSORS, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index e0a47a45b25559..6e90178c5d3537 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -1,15 +1,15 @@ """Support for 1-Wire environment sensors.""" +from glob import glob +import logging import os import time -import logging -from glob import glob import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.const import TEMP_CELSIUS -from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 86f0f418c3f674..93107b2eb48c2e 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -2,12 +2,13 @@ import logging from typing import List -import voluptuous as vol import eiscp from eiscp import eISCP +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( + DOMAIN, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, @@ -16,14 +17,13 @@ SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - DOMAIN, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, - ATTR_ENTITY_ID, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 66081d9b2718ba..64ba0d83844183 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -1,26 +1,26 @@ """Component that will help set the OpenALPR cloud for ALPR processing.""" import asyncio -import logging from base64 import b64encode +import logging import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.core import split_entity_id -from homeassistant.const import CONF_API_KEY from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, CONF_CONFIDENCE, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, ) from homeassistant.components.openalpr_local.image_processing import ( ImageProcessingAlprEntity, ) +from homeassistant.const import CONF_API_KEY +from homeassistant.core import split_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index 32a08b5316514e..df7b235a22401b 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -6,19 +6,19 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.core import split_entity_id, callback -from homeassistant.const import CONF_REGION from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, - ImageProcessingEntity, + ATTR_CONFIDENCE, + ATTR_ENTITY_ID, CONF_CONFIDENCE, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, - ATTR_ENTITY_ID, - ATTR_CONFIDENCE, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingEntity, ) +from homeassistant.const import CONF_REGION +from homeassistant.core import callback, split_entity_id +import homeassistant.helpers.config_validation as cv from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 9b79eb564e0d70..cc6da709dff56a 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -7,11 +7,11 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_API_KEY, - CONF_NAME, CONF_BASE, + CONF_NAME, CONF_QUOTE, - ATTR_ATTRIBUTION, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 6b5cbc912e6211..26a69fa11afa0c 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -5,22 +5,22 @@ import voluptuous as vol from homeassistant.components.cover import ( - CoverDevice, DEVICE_CLASS_GARAGE, PLATFORM_SCHEMA, - SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_OPEN, + CoverDevice, ) from homeassistant.const import ( - CONF_NAME, - STATE_CLOSED, - STATE_OPEN, CONF_COVERS, CONF_HOST, + CONF_NAME, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, + STATE_CLOSED, STATE_CLOSING, + STATE_OPEN, STATE_OPENING, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 0c17daa0ab4686..cef99902d23821 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -1,26 +1,25 @@ """Sensor for the Open Sky Network.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_RADIUS, ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_RADIUS, LENGTH_KILOMETERS, LENGTH_METERS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import distance as util_distance -from homeassistant.util import location as util_location +from homeassistant.util import distance as util_distance, location as util_location _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index e68d8e1c45a964..32eb5b7569b3fb 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -2,14 +2,12 @@ from datetime import timedelta import logging +from oru import Meter, MeterError import voluptuous as vol -from oru import Meter -from oru import MeterError - from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import ENERGY_KILO_WATT_HOUR +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/onboarding/test_init.py b/tests/components/onboarding/test_init.py index a7fc1ad43f492d..e347a07e73f042 100644 --- a/tests/components/onboarding/test_init.py +++ b/tests/components/onboarding/test_init.py @@ -1,13 +1,13 @@ """Tests for the init.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch -from homeassistant.setup import async_setup_component from homeassistant.components import onboarding - -from tests.common import mock_coro, MockUser +from homeassistant.setup import async_setup_component from . import mock_storage +from tests.common import MockUser, mock_coro + # Temporarily: if auth not active, always set onboarded=True diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 7881b75ee99a80..6d2c6e4c08fc30 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -4,15 +4,15 @@ import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import onboarding from homeassistant.components.onboarding import const, views +from homeassistant.setup import async_setup_component + +from . import mock_storage from tests.common import CLIENT_ID, register_auth_provider from tests.components.met.conftest import mock_weather # noqa: F401 -from . import mock_storage - @pytest.fixture(autouse=True) def always_mock_weather(mock_weather): # noqa: F811 diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index e559b6adc45452..4aec9e68709e58 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -1,15 +1,15 @@ """The tests for the openalpr cloud platform.""" import asyncio -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch -from homeassistant.core import callback -from homeassistant.setup import setup_component from homeassistant.components import camera, image_processing as ip from homeassistant.components.openalpr_cloud.image_processing import OPENALPR_API_URL +from homeassistant.core import callback +from homeassistant.setup import setup_component from tests.common import ( - get_test_home_assistant, assert_setup_component, + get_test_home_assistant, load_fixture, mock_coro, ) diff --git a/tests/components/openalpr_local/test_image_processing.py b/tests/components/openalpr_local/test_image_processing.py index bc29c227b0cfc6..4c34abca1d4685 100644 --- a/tests/components/openalpr_local/test_image_processing.py +++ b/tests/components/openalpr_local/test_image_processing.py @@ -1,13 +1,13 @@ """The tests for the openalpr local platform.""" import asyncio -from unittest.mock import patch, PropertyMock, MagicMock +from unittest.mock import MagicMock, PropertyMock, patch -from homeassistant.core import callback +import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE +from homeassistant.core import callback from homeassistant.setup import setup_component -import homeassistant.components.image_processing as ip -from tests.common import get_test_home_assistant, assert_setup_component, load_fixture +from tests.common import assert_setup_component, get_test_home_assistant, load_fixture from tests.components.image_processing import common diff --git a/tests/components/openhardwaremonitor/test_sensor.py b/tests/components/openhardwaremonitor/test_sensor.py index 909f9ab27320b3..3fb93cb1375355 100644 --- a/tests/components/openhardwaremonitor/test_sensor.py +++ b/tests/components/openhardwaremonitor/test_sensor.py @@ -1,8 +1,11 @@ """The tests for the Open Hardware Monitor platform.""" import unittest + import requests_mock + from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant + +from tests.common import get_test_home_assistant, load_fixture class TestOpenHardwareMonitorSetup(unittest.TestCase): diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 43dd5924a72146..3aa67abdc4f37c 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -1,8 +1,9 @@ """Define tests for the OpenUV config flow.""" -import pytest -from pyopenuv.errors import OpenUvError from unittest.mock import patch +from pyopenuv.errors import OpenUvError +import pytest + from homeassistant import data_entry_flow from homeassistant.components.openuv import DOMAIN, config_flow from homeassistant.const import ( From 21816eeed4a3366c7880724e10929965617cc710 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:29:39 +0100 Subject: [PATCH 2325/3953] Sort imports according to PEP8 for components starting with "P" (#29775) --- .../components/pcal9535a/binary_sensor.py | 4 ++-- homeassistant/components/pcal9535a/switch.py | 2 +- .../components/persistent_notification/__init__.py | 1 - homeassistant/components/person/__init__.py | 12 ++++++------ homeassistant/components/ping/binary_sensor.py | 8 ++++---- homeassistant/components/ping/device_tracker.py | 7 +++---- homeassistant/components/pioneer/media_player.py | 2 +- homeassistant/components/plugwise/climate.py | 8 ++++---- homeassistant/components/prowl/notify.py | 7 +++---- homeassistant/components/proximity/__init__.py | 1 - .../components/pulseaudio_loopback/switch.py | 6 +++--- homeassistant/components/push/camera.py | 10 +++++----- homeassistant/components/pushover/notify.py | 7 +++---- homeassistant/components/pushsafer/notify.py | 3 +-- homeassistant/components/pvoutput/sensor.py | 14 +++++++------- homeassistant/components/pyload/sensor.py | 6 +++--- .../persistent_notification/test_init.py | 4 ++-- tests/components/plant/test_init.py | 12 ++++++------ tests/components/prometheus/test_init.py | 6 +++--- tests/components/proximity/test_init.py | 2 +- tests/components/ps4/test_init.py | 3 ++- tests/components/ps4/test_media_player.py | 4 ++-- tests/components/ptvsd/test_ptvsd.py | 3 ++- tests/components/push/test_camera.py | 3 +-- tests/components/python_script/test_init.py | 4 ++-- 25 files changed, 67 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/pcal9535a/binary_sensor.py b/homeassistant/components/pcal9535a/binary_sensor.py index fd4e92ccf0335d..236fd47af73543 100644 --- a/homeassistant/components/pcal9535a/binary_sensor.py +++ b/homeassistant/components/pcal9535a/binary_sensor.py @@ -1,10 +1,10 @@ """Support for binary sensor using I2C PCAL9535A chip.""" import logging -import voluptuous as vol from pcal9535a import PCAL9535A +import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/pcal9535a/switch.py b/homeassistant/components/pcal9535a/switch.py index faebce5d67e396..87c8ced1b0d406 100644 --- a/homeassistant/components/pcal9535a/switch.py +++ b/homeassistant/components/pcal9535a/switch.py @@ -1,8 +1,8 @@ """Support for switch sensor using I2C PCAL9535A chip.""" import logging -import voluptuous as vol from pcal9535a import PCAL9535A +import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import DEVICE_DEFAULT_NAME diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 33f17b18a806f7..0311bd4d30d2e2 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -14,7 +14,6 @@ from homeassistant.util import slugify import homeassistant.util.dt as dt_util - # mypy: allow-untyped-calls, allow-untyped-defs ATTR_CREATED_AT = "created_at" diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 832853c670d827..2e347cf4d49d83 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -7,27 +7,27 @@ import voluptuous as vol +from homeassistant.auth import EVENT_USER_REMOVED from homeassistant.components import websocket_api from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN, ATTR_SOURCE_TYPE, + DOMAIN as DEVICE_TRACKER_DOMAIN, SOURCE_TYPE_GPS, ) from homeassistant.const import ( + ATTR_GPS_ACCURACY, ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_GPS_ACCURACY, CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN, - STATE_UNAVAILABLE, STATE_HOME, STATE_NOT_HOME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) -from homeassistant.core import callback, Event, State -from homeassistant.auth import EVENT_USER_REMOVED +from homeassistant.core import Event, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index fe4c12d6738c04..4d9a99c678e6c3 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -1,15 +1,15 @@ """Tracks the latency of a host by sending ICMP echo requests (ping).""" +from datetime import timedelta import logging -import subprocess import re +import subprocess import sys -from datetime import timedelta import voluptuous as vol +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 9bdc38e065b8da..c4d88f6061c72f 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -1,20 +1,19 @@ """Tracks devices by sending a ICMP echo request (ping).""" +from datetime import timedelta import logging import subprocess import sys -from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant import const, util from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_ROUTER, ) -from homeassistant import util -from homeassistant import const +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 51f55d4e851ccc..3e71b54c9fab09 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -4,7 +4,7 @@ import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index fa1ac86941b914..bc303caeca883a 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -2,18 +2,17 @@ import logging -import voluptuous as vol import haanna +import voluptuous as vol -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_COOL, + CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, - HVAC_MODE_AUTO, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -27,6 +26,7 @@ TEMP_CELSIUS, ) from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index ab6d1b498a02a6..9690e748887ec2 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -5,10 +5,6 @@ import async_timeout import voluptuous as vol -from homeassistant.const import CONF_API_KEY -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, @@ -16,6 +12,9 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://api.prowlapp.com/publicapi/" diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 1f86958d08e065..45a1c19c29e3c9 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -10,7 +10,6 @@ from homeassistant.util.distance import convert from homeassistant.util.location import distance - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index 618d54ab3adb79..a10c5995d630d6 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -1,14 +1,14 @@ """Switch logic for loading/unloading pulseaudio loopback modules.""" +from datetime import timedelta import logging import re import socket -from datetime import timedelta import voluptuous as vol from homeassistant import util -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 9d53bd7b0331a7..f78966253b7ccd 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -1,22 +1,22 @@ """Camera platform that receives images through HTTP POST.""" -import logging import asyncio - from collections import deque from datetime import timedelta -import voluptuous as vol +import logging + import aiohttp import async_timeout +import voluptuous as vol from homeassistant.components.camera import ( - Camera, PLATFORM_SCHEMA, STATE_IDLE, STATE_RECORDING, + Camera, ) from homeassistant.components.camera.const import DOMAIN -from homeassistant.core import callback from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 3f78897838d293..064ad91b6b9aa1 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -1,12 +1,9 @@ """Pushover platform for notify component.""" import logging +from pushover import Client, InitError, RequestError import requests import voluptuous as vol -from pushover import InitError, Client, RequestError - -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( ATTR_DATA, @@ -16,6 +13,8 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pushsafer/notify.py b/homeassistant/components/pushsafer/notify.py index 758a3390286f65..436191ab8647c9 100644 --- a/homeassistant/components/pushsafer/notify.py +++ b/homeassistant/components/pushsafer/notify.py @@ -7,8 +7,6 @@ from requests.auth import HTTPBasicAuth import voluptuous as vol -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -17,6 +15,7 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://www.pushsafer.com/api" diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 90084ab799913d..169086af3fc57c 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -1,22 +1,22 @@ """Support for getting collected information from PVOutput.""" -import logging from collections import namedtuple from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.rest.sensor import RestData +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_API_KEY, - CONF_NAME, ATTR_DATE, + ATTR_TEMPERATURE, ATTR_TIME, ATTR_VOLTAGE, + CONF_API_KEY, + CONF_NAME, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _ENDPOINT = "http://pvoutput.org/service/r2/getstatus.jsp" diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 8ffe1ece4a2ab1..fd4461e3e1b1eb 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -8,14 +8,14 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_SSL, CONF_HOST, + CONF_MONITORED_VARIABLES, CONF_NAME, - CONF_PORT, CONF_PASSWORD, + CONF_PORT, + CONF_SSL, CONF_USERNAME, CONTENT_TYPE_JSON, - CONF_MONITORED_VARIABLES, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/tests/components/persistent_notification/test_init.py b/tests/components/persistent_notification/test_init.py index 286d8a701eddbd..daf671bd52b335 100644 --- a/tests/components/persistent_notification/test_init.py +++ b/tests/components/persistent_notification/test_init.py @@ -1,7 +1,7 @@ """The tests for the persistent notification component.""" -from homeassistant.components.websocket_api.const import TYPE_RESULT -from homeassistant.setup import setup_component, async_setup_component import homeassistant.components.persistent_notification as pn +from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/plant/test_init.py b/tests/components/plant/test_init.py index fdbf43618aee2b..fb919d56607395 100644 --- a/tests/components/plant/test_init.py +++ b/tests/components/plant/test_init.py @@ -1,23 +1,23 @@ """Unit tests for platform/plant.py.""" import asyncio +from datetime import datetime, timedelta import unittest + import pytest -from datetime import datetime, timedelta +from homeassistant.components import recorder +import homeassistant.components.plant as plant from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + STATE_OK, + STATE_PROBLEM, STATE_UNAVAILABLE, STATE_UNKNOWN, - STATE_PROBLEM, - STATE_OK, ) -from homeassistant.components import recorder -import homeassistant.components.plant as plant from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, init_recorder_component - GOOD_DATA = { "moisture": 50, "battery": 90, diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index cf1ff7489f6de8..dd5b673e8441a8 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -1,14 +1,14 @@ """The tests for the Prometheus exporter.""" import asyncio -import pytest -from homeassistant.const import ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_POWER +import pytest 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 +from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR +from homeassistant.setup import async_setup_component @pytest.fixture diff --git a/tests/components/proximity/test_init.py b/tests/components/proximity/test_init.py index 41e9ea98ebdb02..a01d625d8c4aa2 100644 --- a/tests/components/proximity/test_init.py +++ b/tests/components/proximity/test_init.py @@ -3,8 +3,8 @@ from homeassistant.components import proximity from homeassistant.components.proximity import DOMAIN - from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 5b6d6f87cd573a..028c1643ff0ad8 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -26,8 +26,9 @@ CONF_TOKEN, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.util import location from homeassistant.setup import async_setup_component +from homeassistant.util import location + from tests.common import MockConfigEntry, mock_coro, mock_registry MOCK_HOST = "192.168.0.1" diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index e2b9e382dc4d49..56a659aa152ef5 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -29,13 +29,13 @@ CONF_REGION, CONF_TOKEN, STATE_IDLE, - STATE_STANDBY, STATE_PLAYING, + STATE_STANDBY, STATE_UNKNOWN, ) from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry, mock_device_registry, mock_registry, mock_coro +from tests.common import MockConfigEntry, mock_coro, mock_device_registry, mock_registry MOCK_CREDS = "123412341234abcd12341234abcd12341234abcd12341234abcd12341234abcd" MOCK_NAME = "ha_ps4_name" diff --git a/tests/components/ptvsd/test_ptvsd.py b/tests/components/ptvsd/test_ptvsd.py index e7d9c97be22c90..d4a2aa1ab94ffd 100644 --- a/tests/components/ptvsd/test_ptvsd.py +++ b/tests/components/ptvsd/test_ptvsd.py @@ -1,12 +1,13 @@ """Tests for PTVSD Debugger.""" from unittest.mock import patch + from asynctest import CoroutineMock from pytest import mark +from homeassistant.bootstrap import _async_set_up_integrations import homeassistant.components.ptvsd as ptvsd_component from homeassistant.setup import async_setup_component -from homeassistant.bootstrap import _async_set_up_integrations @mark.skip("causes code cover to fail") diff --git a/tests/components/push/test_camera.py b/tests/components/push/test_camera.py index c48f0c4322a220..b5803b96889fea 100644 --- a/tests/components/push/test_camera.py +++ b/tests/components/push/test_camera.py @@ -1,7 +1,6 @@ """The tests for generic camera component.""" -import io - from datetime import timedelta +import io from homeassistant import core as ha from homeassistant.setup import async_setup_component diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index d7732c00f94fe1..75616c7400a701 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -1,11 +1,11 @@ """Test the python_script component.""" import asyncio import logging -from unittest.mock import patch, mock_open +from unittest.mock import mock_open, patch +from homeassistant.components.python_script import DOMAIN, FOLDER, execute from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component -from homeassistant.components.python_script import DOMAIN, execute, FOLDER from tests.common import patch_yaml_files From 23b92b2a562d528dc8333f5cda59496f73016039 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:38:01 +0100 Subject: [PATCH 2326/3953] Sort imports according to PEP8 for components starting with "S" (#29777) --- homeassistant/components/saj/sensor.py | 2 +- .../components/samsungtv/media_player.py | 6 +++--- homeassistant/components/scrape/sensor.py | 14 +++++++------- homeassistant/components/script/__init__.py | 19 +++++++++---------- homeassistant/components/sendgrid/notify.py | 16 +++++++--------- homeassistant/components/sensibo/climate.py | 4 ++-- .../components/shell_command/__init__.py | 2 +- homeassistant/components/sigfox/sensor.py | 4 ++-- homeassistant/components/simulated/sensor.py | 2 +- homeassistant/components/sinch/notify.py | 10 +++++----- .../components/sky_hub/device_tracker.py | 2 +- homeassistant/components/slide/__init__.py | 15 ++++++++------- homeassistant/components/slide/cover.py | 7 ++++--- homeassistant/components/smtp/notify.py | 15 +++++++-------- homeassistant/components/snips/__init__.py | 6 +++--- homeassistant/components/snmp/switch.py | 1 - .../components/solaredge_local/sensor.py | 8 ++++---- homeassistant/components/solax/sensor.py | 7 +++---- .../components/songpal/media_player.py | 14 +++++++------- homeassistant/components/splunk/__init__.py | 4 ++-- .../components/squeezebox/media_player.py | 2 +- homeassistant/components/ssdp/__init__.py | 2 +- homeassistant/components/suez_water/sensor.py | 5 ++--- homeassistant/components/sun/__init__.py | 5 ++--- .../components/supervisord/sensor.py | 2 +- .../components/synology_chat/notify.py | 5 ++--- .../components/samsungtv/test_media_player.py | 10 +++++----- tests/components/script/test_init.py | 9 ++++----- tests/components/season/test_sensor.py | 5 ++--- .../components/seventeentrack/test_sensor.py | 5 +++-- tests/components/shell_command/test_init.py | 4 ++-- tests/components/sigfox/test_sensor.py | 4 +++- .../components/simplisafe/test_config_flow.py | 4 ++-- tests/components/simulated/test_sensor.py | 8 ++++---- .../smartthings/test_config_flow.py | 2 +- tests/components/smartthings/test_init.py | 2 +- tests/components/smhi/test_config_flow.py | 6 +++--- tests/components/smhi/test_weather.py | 17 ++++++++--------- tests/components/smtp/test_notify.py | 2 +- tests/components/snips/test_init.py | 3 ++- .../components/solaredge/test_config_flow.py | 7 ++++--- tests/components/sonarr/test_sensor.py | 4 ++-- .../soundtouch/test_media_player.py | 4 +++- tests/components/spaceapi/test_init.py | 3 ++- tests/components/spc/test_init.py | 2 +- tests/components/splunk/test_init.py | 6 +++--- tests/components/ssdp/test_init.py | 4 ++-- tests/components/startca/test_sensor.py | 1 + tests/components/statistics/test_sensor.py | 16 ++++++++-------- tests/components/stt/test_init.py | 2 +- tests/components/sun/test_init.py | 4 ++-- tests/components/switcher_kis/test_init.py | 14 +++++++------- tests/components/system_log/test_init.py | 2 +- 53 files changed, 164 insertions(+), 165 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 52ae3640a7f6f7..704e9996d2d53a 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -9,8 +9,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, - CONF_PASSWORD, CONF_NAME, + CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, DEVICE_CLASS_POWER, diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 2488d5ab9132e2..56b947ba9adbf9 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -3,15 +3,15 @@ from datetime import timedelta import socket -from samsungctl import exceptions as samsung_exceptions, Remote as SamsungRemote +from samsungctl import Remote as SamsungRemote, exceptions as samsung_exceptions import voluptuous as vol import wakeonlan from websocket import WebSocketException from homeassistant.components.media_player import ( - MediaPlayerDevice, - PLATFORM_SCHEMA, DEVICE_CLASS_TV, + PLATFORM_SCHEMA, + MediaPlayerDevice, ) from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 0bfb7351c881e6..13d99a0cb8f1b6 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -2,27 +2,27 @@ import logging from bs4 import BeautifulSoup -import voluptuous as vol from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.rest.sensor import RestData +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_HEADERS, CONF_NAME, + CONF_PASSWORD, CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, + CONF_USERNAME, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, - CONF_USERNAME, - CONF_HEADERS, - CONF_PASSWORD, - CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, ) -from homeassistant.helpers.entity import Entity from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index cb9cb5194ba193..a8d78beae950ce 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -6,23 +6,22 @@ from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_NAME, + CONF_ALIAS, + EVENT_SCRIPT_STARTED, + SERVICE_RELOAD, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_TOGGLE, - SERVICE_RELOAD, STATE_ON, - CONF_ALIAS, - EVENT_SCRIPT_STARTED, - ATTR_NAME, ) -from homeassistant.loader import bind_hass -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 make_entity_service_schema -from homeassistant.helpers.service import async_set_service_schema - +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.script import Script +from homeassistant.helpers.service import async_set_service_schema +from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index f16758a53559c2..6dbf4d5c2b73a1 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -1,10 +1,15 @@ """SendGrid notification service.""" import logging -import voluptuous as vol - from sendgrid import SendGridAPIClient +import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -13,13 +18,6 @@ ) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - _LOGGER = logging.getLogger(__name__) CONF_SENDER_NAME = "sender_name" diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index a14bdb49133868..2431b223f09ef1 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -5,16 +5,16 @@ import aiohttp import async_timeout -import voluptuous as vol import pysensibo +import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 420574078147ed..89a1a20e8e4290 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -5,8 +5,8 @@ import voluptuous as vol -from homeassistant.exceptions import TemplateError from homeassistant.core import ServiceCall +from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType, HomeAssistantType diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index b890880389c232..27e2fe9b5636ab 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -1,15 +1,15 @@ """Sensor for SigFox devices.""" -import logging import datetime import json +import logging from urllib.parse import urljoin import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py index f6ed54e5191bb2..d05448a82c7fd4 100644 --- a/homeassistant/components/simulated/sensor.py +++ b/homeassistant/components/simulated/sensor.py @@ -1,8 +1,8 @@ """Adds a simulated sensor.""" +from datetime import datetime import logging import math from random import Random -from datetime import datetime import voluptuous as vol diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py index d7d1f242c67c17..c0092f013c49f4 100644 --- a/homeassistant/components/sinch/notify.py +++ b/homeassistant/components/sinch/notify.py @@ -1,25 +1,25 @@ """Support for Sinch notifications.""" import logging -import voluptuous as vol from clx.xms.api import MtBatchTextSmsResult from clx.xms.client import Client from clx.xms.exceptions import ( ErrorResponseException, - UnexpectedResponseException, - UnauthorizedException, NotFoundException, + UnauthorizedException, + UnexpectedResponseException, ) +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_MESSAGE, ATTR_DATA, + ATTR_MESSAGE, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_SENDER +import homeassistant.helpers.config_validation as cv DOMAIN = "sinch" diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index 109c410c16d0b8..f7760a59eed596 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -5,13 +5,13 @@ import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _MAC_REGEX = re.compile(r"(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})") diff --git a/homeassistant/components/slide/__init__.py b/homeassistant/components/slide/__init__.py index 54154ae863ee83..49e50e601dd50f 100644 --- a/homeassistant/components/slide/__init__.py +++ b/homeassistant/components/slide/__init__.py @@ -1,24 +1,25 @@ """Component for the Go Slide API.""" -import logging from datetime import timedelta +import logging -import voluptuous as vol from goslideapi import GoSlideCloud, goslideapi +import voluptuous as vol from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, - STATE_OPEN, + CONF_USERNAME, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.event import async_track_time_interval, async_call_later -from .const import DOMAIN, SLIDES, API, COMPONENT, DEFAULT_RETRY +from homeassistant.helpers.event import async_call_later, async_track_time_interval + +from .const import API, COMPONENT, DEFAULT_RETRY, DOMAIN, SLIDES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py index 1c4e6da5aaca6b..a567a9bf61b4a4 100644 --- a/homeassistant/components/slide/cover.py +++ b/homeassistant/components/slide/cover.py @@ -2,15 +2,16 @@ import logging -from homeassistant.const import ATTR_ID from homeassistant.components.cover import ( ATTR_POSITION, + DEVICE_CLASS_CURTAIN, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, - DEVICE_CLASS_CURTAIN, + STATE_OPENING, CoverDevice, ) +from homeassistant.const import ATTR_ID + from .const import API, DOMAIN, SLIDES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index d592f25a61dc11..82b0f96f785282 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -10,6 +10,13 @@ import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, @@ -21,14 +28,6 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - _LOGGER = logging.getLogger(__name__) ATTR_IMAGES = "images" # optional embedded image file attachments diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 93e445e8cedfc7..65015bd723c685 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -1,13 +1,13 @@ """Support for Snips on-device ASR and NLU.""" +from datetime import timedelta import json import logging -from datetime import timedelta import voluptuous as vol -from homeassistant.core import callback -from homeassistant.helpers import intent, config_validation as cv from homeassistant.components import mqtt +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv, intent DOMAIN = "snips" CONF_INTENTS = "intents" diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 8d5be1221c4026..578b97c801eb91 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -2,7 +2,6 @@ import logging from pyasn1.type.univ import Integer - import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 917fb86ddcb776..ecf9dfde8b15f0 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -1,10 +1,10 @@ """Support for SolarEdge-local Monitoring API.""" -import logging +from copy import deepcopy from datetime import timedelta +import logging import statistics -from copy import deepcopy -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError from solaredge_local import SolarEdge import voluptuous as vol @@ -12,8 +12,8 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, - POWER_WATT, ENERGY_WATT_HOUR, + POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index a5b4547b344894..8eb61560e63490 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -1,6 +1,5 @@ """Support for Solax inverter via local API.""" import asyncio - from datetime import timedelta import logging @@ -8,11 +7,11 @@ from solax.inverter import InverterError import voluptuous as vol -from homeassistant.const import TEMP_CELSIUS, CONF_IP_ADDRESS, CONF_PORT -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS from homeassistant.exceptions import PlatformNotReady +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__) diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 681c97a7710754..27a81b2a667d5b 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -1,19 +1,19 @@ """Support for Songpal-enabled (Sony) media devices.""" import asyncio -import logging from collections import OrderedDict +import logging -import voluptuous as vol from songpal import ( + ConnectChange, + ContentChange, Device, + PowerChange, SongpalException, VolumeChange, - ContentChange, - PowerChange, - ConnectChange, ) +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -25,9 +25,9 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, + EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON, - EVENT_HOMEASSISTANT_STOP, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/splunk/__init__.py b/homeassistant/components/splunk/__init__.py index c483d7fae8735c..1d5d39416a3f87 100644 --- a/homeassistant/components/splunk/__init__.py +++ b/homeassistant/components/splunk/__init__.py @@ -7,12 +7,12 @@ import voluptuous as vol from homeassistant.const import ( - CONF_SSL, - CONF_VERIFY_SSL, CONF_HOST, CONF_NAME, CONF_PORT, + CONF_SSL, CONF_TOKEN, + CONF_VERIFY_SSL, EVENT_STATE_CHANGED, ) from homeassistant.helpers import state as state_helper diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 72a5772a14d9a0..94c497e4db6f97 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -38,9 +38,9 @@ STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import PlatformNotReady from homeassistant.util.dt import utcnow from .const import DOMAIN, SERVICE_CALL_METHOD diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index b9a9d4b46c9f58..1a97b1721fcde0 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -8,8 +8,8 @@ from defusedxml import ElementTree from netdisco import ssdp, util -from homeassistant.helpers.event import async_track_time_interval from homeassistant.generated.ssdp import SSDP +from homeassistant.helpers.event import async_track_time_interval DOMAIN = "ssdp" SCAN_INTERVAL = timedelta(seconds=60) diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 05f82183e46b2c..bfa529adb34412 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -2,10 +2,9 @@ from datetime import timedelta import logging -import voluptuous as vol - -from pysuez.client import PySuezError from pysuez import SuezClient +from pysuez.client import PySuezError +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, VOLUME_LITERS diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index e848449e61e529..704f9432a0fdfa 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -1,12 +1,12 @@ """Support for functionality to keep track of the sun.""" -import logging from datetime import timedelta +import logging from homeassistant.const import ( CONF_ELEVATION, + EVENT_CORE_CONFIG_UPDATE, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, - EVENT_CORE_CONFIG_UPDATE, ) from homeassistant.core import callback from homeassistant.helpers.entity import Entity @@ -17,7 +17,6 @@ ) from homeassistant.util import dt as dt_util - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index e1a816f91aea72..c79c09248b4380 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -6,8 +6,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_URL -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/synology_chat/notify.py b/homeassistant/components/synology_chat/notify.py index c67ef79f5d592b..3e1aeb4ce1383a 100644 --- a/homeassistant/components/synology_chat/notify.py +++ b/homeassistant/components/synology_chat/notify.py @@ -5,14 +5,13 @@ import requests import voluptuous as vol -from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL +import homeassistant.helpers.config_validation as cv ATTR_FILE_URL = "file_url" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 918e30ef4e7a90..bb40dc28445626 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1,13 +1,12 @@ """Tests for samsungtv component.""" import asyncio -from unittest.mock import call, patch from datetime import timedelta - import logging +from unittest.mock import call, patch + from asynctest import mock import pytest from samsungctl import exceptions -from tests.common import async_fire_time_changed from websocket import WebSocketException from homeassistant.components.media_player import DEVICE_CLASS_TV @@ -17,11 +16,11 @@ ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_VOLUME_MUTED, DOMAIN, + MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_URL, SERVICE_PLAY_MEDIA, SERVICE_SELECT_SOURCE, SUPPORT_TURN_ON, - MEDIA_TYPE_CHANNEL, - MEDIA_TYPE_URL, ) from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN from homeassistant.components.samsungtv.media_player import ( @@ -56,6 +55,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake" MOCK_CONFIG = { diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index d675034e7442ae..697154c46b2d7c 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -1,7 +1,7 @@ """The tests for the Script component.""" # pylint: disable=protected-access import unittest -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest @@ -10,21 +10,20 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, + EVENT_SCRIPT_STARTED, SERVICE_RELOAD, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - EVENT_SCRIPT_STARTED, ) from homeassistant.core import Context, callback, split_entity_id +from homeassistant.exceptions import ServiceNotFound 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 +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant - ENTITY_ID = "script.test" diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index 9d891fe015589d..2acc5f6573ff3d 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -1,14 +1,13 @@ """The tests for the Season sensor platform.""" # pylint: disable=protected-access -import unittest from datetime import datetime +import unittest -from homeassistant.setup import setup_component import homeassistant.components.season.sensor as season +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant - HEMISPHERE_NORTHERN = { "homeassistant": {"latitude": "48.864716", "longitude": "2.349014"}, "sensor": {"platform": "season", "type": "astronomical"}, diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index 45ab8a62225426..10ec22f8b67db5 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -2,17 +2,18 @@ import datetime from typing import Union -import pytest import mock from py17track.package import Package +import pytest from homeassistant.components.seventeentrack.sensor import ( CONF_SHOW_ARCHIVED, CONF_SHOW_DELIVERED, ) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component from homeassistant.util import utcnow + from tests.common import MockDependency, async_fire_time_changed VALID_CONFIG_MINIMAL = { diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index 13899da9a3e10a..a54bd9f778795e 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -2,12 +2,12 @@ import asyncio import os import tempfile -import unittest from typing import Tuple +import unittest from unittest.mock import Mock, patch -from homeassistant.setup import setup_component from homeassistant.components import shell_command +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/sigfox/test_sensor.py b/tests/components/sigfox/test_sensor.py index eac1e6c258234a..35534a3a12680a 100644 --- a/tests/components/sigfox/test_sensor.py +++ b/tests/components/sigfox/test_sensor.py @@ -1,14 +1,16 @@ """Tests for the sigfox sensor.""" import re -import requests_mock import unittest +import requests_mock + from homeassistant.components.sigfox.sensor import ( API_URL, CONF_API_LOGIN, CONF_API_PASSWORD, ) from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant TEST_API_LOGIN = "foo" diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index c0920a738eee25..a7a21c577d6343 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,7 +1,7 @@ """Define tests for the SimpliSafe config flow.""" -import json from datetime import timedelta -from unittest.mock import mock_open, patch, MagicMock, PropertyMock +import json +from unittest.mock import MagicMock, PropertyMock, mock_open, patch from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN, config_flow diff --git a/tests/components/simulated/test_sensor.py b/tests/components/simulated/test_sensor.py index 14d839ee656374..09e77f7b283a08 100644 --- a/tests/components/simulated/test_sensor.py +++ b/tests/components/simulated/test_sensor.py @@ -1,28 +1,28 @@ """The tests for the simulated sensor.""" import unittest -from tests.common import get_test_home_assistant - from homeassistant.components.simulated.sensor import ( CONF_AMP, CONF_FWHM, CONF_MEAN, CONF_PERIOD, CONF_PHASE, + CONF_RELATIVE_TO_EPOCH, CONF_SEED, CONF_UNIT, - CONF_RELATIVE_TO_EPOCH, DEFAULT_AMP, DEFAULT_FWHM, DEFAULT_MEAN, DEFAULT_NAME, DEFAULT_PHASE, - DEFAULT_SEED, DEFAULT_RELATIVE_TO_EPOCH, + DEFAULT_SEED, ) from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant + class TestSimulatedSensor(unittest.TestCase): """Test the simulated sensor.""" diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 82a24f38287139..f299727b948ff9 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -6,7 +6,6 @@ from pysmartthings import APIResponseError from homeassistant import data_entry_flow -from homeassistant.setup import async_setup_component from homeassistant.components.smartthings import smartapp from homeassistant.components.smartthings.config_flow import SmartThingsFlowHandler from homeassistant.components.smartthings.const import ( @@ -16,6 +15,7 @@ CONF_REFRESH_TOKEN, DOMAIN, ) +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_coro diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index b8cd65f5a0b9ca..0c9d889d558e9c 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -6,7 +6,6 @@ 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, @@ -20,6 +19,7 @@ ) from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index e5e1d392419e18..ceccd75e08d405 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -3,10 +3,10 @@ from smhi.smhi_lib import Smhi as SmhiApi, SmhiForecastException -from tests.common import mock_coro - -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.components.smhi import config_flow +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import mock_coro # pylint: disable=protected-access diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 6cb7d690c1c4e3..92557f9d5439e9 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -1,31 +1,30 @@ """Test for the smhi weather entity.""" import asyncio -import logging from datetime import datetime +import logging from unittest.mock import Mock, patch +from homeassistant.components.smhi import weather as weather_smhi +from homeassistant.components.smhi.const import ATTR_SMHI_CLOUDINESS from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, - ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, - ATTR_FORECAST_TEMP_LOW, + ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_VISIBILITY, - ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - ATTR_FORECAST_PRECIPITATION, DOMAIN as WEATHER_DOMAIN, ) -from homeassistant.components.smhi import weather as weather_smhi from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant -from tests.common import load_fixture, MockConfigEntry - -from homeassistant.components.smhi.const import ATTR_SMHI_CLOUDINESS +from tests.common import MockConfigEntry, load_fixture _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index daef7ef130e1ae..c79633dd02da06 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -1,11 +1,11 @@ """The tests for the notify smtp platform.""" +import re import unittest from unittest.mock import patch from homeassistant.components.smtp.notify import MailNotificationService from tests.common import get_test_home_assistant -import re class MockSMTP(MailNotificationService): diff --git a/tests/components/snips/test_init.py b/tests/components/snips/test_init.py index fa6fbe0b2542fd..40fb30ddd19b41 100644 --- a/tests/components/snips/test_init.py +++ b/tests/components/snips/test_init.py @@ -9,11 +9,12 @@ from homeassistant.components.mqtt import MQTT_PUBLISH_SCHEMA import homeassistant.components.snips as snips from homeassistant.helpers.intent import ServiceIntentHandler, async_register + from tests.common import ( async_fire_mqtt_message, async_mock_intent, - async_mock_service, async_mock_mqtt_component, + async_mock_service, ) diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index c1183147bac5e2..46f40dd80efe49 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -1,12 +1,13 @@ """Tests for the SolarEdge config flow.""" +from unittest.mock import Mock, patch + import pytest -from requests.exceptions import HTTPError, ConnectTimeout -from unittest.mock import patch, Mock +from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import data_entry_flow from homeassistant.components.solaredge import config_flow from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME -from homeassistant.const import CONF_NAME, CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_NAME from tests.common import MockConfigEntry diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 43a53eeadcc309..38382dc70ab00b 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -1,7 +1,7 @@ """The tests for the Sonarr platform.""" -import unittest -import time from datetime import datetime +import time +import unittest import pytest diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index f9921b5bcb2e81..8789db1ca1f23b 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -2,10 +2,12 @@ import logging import unittest from unittest import mock -from libsoundtouch.device import SoundTouchDevice as STD, Status, Volume, Preset, Config + +from libsoundtouch.device import Config, Preset, SoundTouchDevice as STD, Status, Volume from homeassistant.components.soundtouch import media_player as soundtouch from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING + from tests.common import get_test_home_assistant diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index 58c417831a966f..840931d073b22d 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -3,11 +3,12 @@ from unittest.mock import patch import pytest -from tests.common import mock_coro from homeassistant.components.spaceapi import DOMAIN, SPACEAPI_VERSION, URL_API_SPACEAPI from homeassistant.setup import async_setup_component +from tests.common import mock_coro + CONFIG = { DOMAIN: { "space": "Home", diff --git a/tests/components/spc/test_init.py b/tests/components/spc/test_init.py index f726a064dd1bdc..f08abac261f8da 100644 --- a/tests/components/spc/test_init.py +++ b/tests/components/spc/test_init.py @@ -1,5 +1,5 @@ """Tests for Vanderbilt SPC component.""" -from unittest.mock import patch, PropertyMock, Mock +from unittest.mock import Mock, PropertyMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components.spc import DATA_API diff --git a/tests/components/splunk/test_init.py b/tests/components/splunk/test_init.py index 1fe4c6061cdcee..256c78af5026b4 100644 --- a/tests/components/splunk/test_init.py +++ b/tests/components/splunk/test_init.py @@ -3,12 +3,12 @@ import unittest from unittest import mock -from homeassistant.setup import setup_component import homeassistant.components.splunk as splunk -from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON +from homeassistant.core import State from homeassistant.helpers import state as state_helper +from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from homeassistant.core import State from tests.common import get_test_home_assistant, mock_state_change_event diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 56b937cf9d9f4a..e224842b5abc16 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -1,12 +1,12 @@ """Test the SSDP integration.""" import asyncio -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import aiohttp import pytest -from homeassistant.generated import ssdp as gn_ssdp from homeassistant.components import ssdp +from homeassistant.generated import ssdp as gn_ssdp from tests.common import mock_coro diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index 9ecfed3ce81e30..ab043c44d11ad1 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the Start.ca sensor platform.""" import asyncio + from homeassistant.bootstrap import async_setup_component from homeassistant.components.startca.sensor import StartcaData from homeassistant.helpers.aiohttp_client import async_get_clientsession diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 2a28876f5527b0..6a38ea6c391af2 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -1,18 +1,18 @@ """The test for the statistics sensor platform.""" -import unittest +from datetime import datetime, timedelta import statistics +import unittest +from unittest.mock import patch import pytest -from homeassistant.setup import setup_component +from homeassistant.components import recorder from homeassistant.components.statistics.sensor import StatisticsSensor -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, STATE_UNKNOWN +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS +from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util -from tests.common import get_test_home_assistant -from unittest.mock import patch -from datetime import datetime, timedelta -from tests.common import init_recorder_component -from homeassistant.components import recorder + +from tests.common import get_test_home_assistant, init_recorder_component class TestStatisticsSensor(unittest.TestCase): diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index 5627d7d3e530cc..1e69fa2494ad51 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -1,7 +1,7 @@ """Test STT component setup.""" -from homeassistant.setup import async_setup_component from homeassistant.components import stt +from homeassistant.setup import async_setup_component async def test_setup_comp(hass): diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index 5346f97308f0f5..e04de7e2578ff5 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -5,10 +5,10 @@ from pytest import mark import homeassistant.components.sun as sun -import homeassistant.core as ha -import homeassistant.util.dt as dt_util from homeassistant.const import EVENT_STATE_CHANGED +import homeassistant.core as ha from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util async def test_setting_rising(hass): diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py index ad6bafd0643a67..9e262fafa7eccb 100644 --- a/tests/components/switcher_kis/test_init.py +++ b/tests/components/switcher_kis/test_init.py @@ -1,28 +1,26 @@ """Test cases for the switcher_kis component.""" from datetime import timedelta -from typing import Any, Generator, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Generator from pytest import raises -from homeassistant.const import CONF_ENTITY_ID from homeassistant.components.switcher_kis import ( CONF_AUTO_OFF, - DOMAIN, DATA_DEVICE, + DOMAIN, SERVICE_SET_AUTO_OFF_NAME, SERVICE_SET_AUTO_OFF_SCHEMA, SIGNAL_SWITCHER_DEVICE_UPDATE, ) -from homeassistant.core import callback, Context +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import Context, callback +from homeassistant.exceptions import Unauthorized, UnknownUser from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.exceptions import Unauthorized, UnknownUser from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.common import async_mock_service, async_fire_time_changed - from .consts import ( DUMMY_AUTO_OFF_SET, DUMMY_DEVICE_ID, @@ -38,6 +36,8 @@ SWITCH_ENTITY_ID, ) +from tests.common import async_fire_time_changed, async_mock_service + if TYPE_CHECKING: from tests.common import MockUser from aioswitcher.devices import SwitcherV2Device diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 55d69db8fb40f6..c21d4842b456b7 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -2,9 +2,9 @@ import logging from unittest.mock import MagicMock, patch -from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log +from homeassistant.core import callback _LOGGER = logging.getLogger("test_logger") BASIC_CONFIG = {"system_log": {"max_entries": 2}} From 485761bbaf8d359cf210156c45b3a86395da2675 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:39:51 +0100 Subject: [PATCH 2327/3953] Sort imports according to PEP8 for components starting with "R" (#29776) --- .../components/radiotherm/climate.py | 20 ++++++++-------- homeassistant/components/reddit/sensor.py | 2 +- .../components/rmvtransport/sensor.py | 4 ++-- homeassistant/components/rpi_camera/camera.py | 8 +++---- homeassistant/components/rpi_gpio/switch.py | 4 ++-- .../components/rss_feed_template/__init__.py | 2 +- homeassistant/components/rtorrent/sensor.py | 8 +++---- .../components/russound_rio/media_player.py | 2 +- .../rainmachine/test_config_flow.py | 2 +- tests/components/reddit/test_sensor.py | 23 +++++++++---------- .../components/remember_the_milk/test_init.py | 4 ++-- tests/components/rest_command/test_init.py | 2 +- tests/components/rmvtransport/test_sensor.py | 1 - 13 files changed, 40 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index a007dd673ace3a..5ac64fd64e91b9 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -1,32 +1,32 @@ """Support for Radio Thermostat wifi-enabled home thermostats.""" import logging -import voluptuous as vol import radiotherm +import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + FAN_OFF, + FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - FAN_ON, - FAN_OFF, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, PRECISION_HALVES, - TEMP_FAHRENHEIT, STATE_ON, + TEMP_FAHRENHEIT, ) -from homeassistant.util import dt as dt_util import homeassistant.helpers.config_validation as cv +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 82f622b968eec4..ed24dfe47dff2c 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_MAXIMUM +from homeassistant.const import CONF_MAXIMUM, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 190274518cd5f9..8df1191a420b09 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -1,14 +1,14 @@ """Support for departure information for Rhein-Main public transport.""" import asyncio -import logging from datetime import timedelta +import logging from RMVtransport import RMVtransport from RMVtransport.rmvtransport import RMVtransportApiConnectionError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index d486f406e41ce1..bf04e0ef492372 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -1,14 +1,14 @@ """Camera platform that has a Raspberry Pi camera.""" -import os -import subprocess import logging +import os import shutil +import subprocess from tempfile import NamedTemporaryFile import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_FILE_PATH, EVENT_HOMEASSISTANT_STOP +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.const import CONF_FILE_PATH, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rpi_gpio/switch.py b/homeassistant/components/rpi_gpio/switch.py index e9a46978eaf29c..03cb9f083ce56d 100644 --- a/homeassistant/components/rpi_gpio/switch.py +++ b/homeassistant/components/rpi_gpio/switch.py @@ -3,11 +3,11 @@ import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.components import rpi_gpio +from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py index 440482ac31ad7f..69bc947426791a 100644 --- a/homeassistant/components/rss_feed_template/__init__.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -1,7 +1,7 @@ """Support to export sensor values via RSS feed.""" from html import escape -from aiohttp import web +from aiohttp import web import voluptuous as vol from homeassistant.components.http import HomeAssistantView diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index ed16331e91288b..4ae272ca9bd36e 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -6,14 +6,14 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_URL, - CONF_NAME, CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_URL, STATE_IDLE, ) -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index fdcd308618adac..c954553160e7f4 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -4,7 +4,7 @@ from russound_rio import Russound import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOURCE, diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index cfde51408220d8..9e43f647301835 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -9,8 +9,8 @@ CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, - CONF_SSL, CONF_SCAN_INTERVAL, + CONF_SSL, ) from tests.common import MockConfigEntry, mock_coro diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index a421f6f417cc0a..c44c62fe080509 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -5,23 +5,22 @@ from homeassistant.components.reddit import sensor as reddit_sensor from homeassistant.components.reddit.sensor import ( - DOMAIN, - ATTR_SUBREDDIT, - ATTR_POSTS, - CONF_SORT_BY, - ATTR_ID, - ATTR_URL, - ATTR_TITLE, - ATTR_SCORE, + ATTR_BODY, ATTR_COMMENTS_NUMBER, ATTR_CREATED, - ATTR_BODY, + ATTR_ID, + ATTR_POSTS, + ATTR_SCORE, + ATTR_SUBREDDIT, + ATTR_TITLE, + ATTR_URL, + CONF_SORT_BY, + DOMAIN, ) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_MAXIMUM +from homeassistant.const import CONF_MAXIMUM, CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, MockDependency - +from tests.common import MockDependency, get_test_home_assistant VALID_CONFIG = { "sensor": { diff --git a/tests/components/remember_the_milk/test_init.py b/tests/components/remember_the_milk/test_init.py index f27e41451a3bec..e09e1e01dab977 100644 --- a/tests/components/remember_the_milk/test_init.py +++ b/tests/components/remember_the_milk/test_init.py @@ -1,9 +1,9 @@ """Tests for the Remember The Milk component.""" -import logging import json +import logging import unittest -from unittest.mock import patch, mock_open, Mock +from unittest.mock import Mock, mock_open, patch import homeassistant.components.remember_the_milk as rtm diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index ba63091041d6cd..0aee8ccfbccf01 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -6,7 +6,7 @@ import homeassistant.components.rest_command as rc from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant class TestRestCommandSetup: diff --git a/tests/components/rmvtransport/test_sensor.py b/tests/components/rmvtransport/test_sensor.py index 7cb94b281d1f8d..b34ba3d122914c 100644 --- a/tests/components/rmvtransport/test_sensor.py +++ b/tests/components/rmvtransport/test_sensor.py @@ -6,7 +6,6 @@ from tests.common import mock_coro - VALID_CONFIG_MINIMAL = { "sensor": {"platform": "rmvtransport", "next_departure": [{"station": "3000010"}]} } From de915e1bf0a0378dfb6149beaba437e61216ac49 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:41:48 +0100 Subject: [PATCH 2328/3953] Sort imports according to PEP8 for components starting with "T" (#29778) --- homeassistant/components/tcp/sensor.py | 8 ++--- homeassistant/components/teksavvy/sensor.py | 2 +- homeassistant/components/telegram/notify.py | 3 +- .../components/telegram_bot/__init__.py | 8 ++--- .../components/telegram_bot/polling.py | 4 +-- homeassistant/components/telnet/switch.py | 2 +- .../components/thomson/device_tracker.py | 2 +- .../components/tile/device_tracker.py | 4 +-- homeassistant/components/time_date/sensor.py | 6 ++-- homeassistant/components/todoist/calendar.py | 2 +- .../components/tomato/device_tracker.py | 6 ++-- homeassistant/components/torque/sensor.py | 4 +-- .../components/totalconnect/__init__.py | 7 ++--- homeassistant/components/tplink/__init__.py | 2 +- .../components/trafikverket_train/sensor.py | 2 +- .../components/transmission/__init__.py | 2 +- .../components/transmission/sensor.py | 1 - .../components/transport_nsw/sensor.py | 6 ++-- homeassistant/components/twitter/notify.py | 7 ++--- tests/components/teksavvy/test_sensor.py | 1 + .../threshold/test_binary_sensor.py | 2 +- tests/components/timer/test_init.py | 30 +++++++++---------- .../components/timer/test_reproduce_state.py | 1 + tests/components/tod/test_binary_sensor.py | 12 ++++---- .../components/tomato/test_device_tracker.py | 5 ++-- tests/components/tradfri/test_config_flow.py | 2 +- tests/components/tradfri/test_light.py | 3 +- tests/components/trend/test_binary_sensor.py | 2 +- tests/components/tts/test_init.py | 14 ++++----- tests/components/twilio/test_init.py | 1 + 30 files changed, 76 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index a387b3fc0bb7f7..2732f2d6bd19fc 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -1,23 +1,23 @@ """Support for TCP socket based sensors.""" import logging -import socket import select +import socket import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, - CONF_PORT, + CONF_NAME, CONF_PAYLOAD, + CONF_PORT, CONF_TIMEOUT, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) from homeassistant.exceptions import TemplateError -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py index dc8b16b8ce18fc..fe183129eaadbf 100644 --- a/homeassistant/components/teksavvy/sensor.py +++ b/homeassistant/components/teksavvy/sensor.py @@ -1,8 +1,8 @@ """Support for TekSavvy Bandwidth Monitor.""" from datetime import timedelta import logging -import async_timeout +import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index 23c36e3bafa9c8..ceb660d9e1dfb0 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -3,8 +3,6 @@ import voluptuous as vol -from homeassistant.const import ATTR_LOCATION - from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, @@ -13,6 +11,7 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import ATTR_LOCATION _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 12bde6c72d8496..b91c37b35dee05 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -1,8 +1,8 @@ """Support to send and receive Telegram messages.""" -import io -from ipaddress import ip_network from functools import partial import importlib +import io +from ipaddress import ip_network import logging import requests @@ -26,11 +26,11 @@ CONF_API_KEY, CONF_PLATFORM, CONF_TIMEOUT, - HTTP_DIGEST_AUTHENTICATION, CONF_URL, + HTTP_DIGEST_AUTHENTICATION, ) -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index cf3d13d5edc8a6..8bdeef25118c75 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -2,8 +2,8 @@ import logging from telegram import Update -from telegram.error import TelegramError, TimedOut, NetworkError, RetryAfter -from telegram.ext import Updater, Handler +from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut +from telegram.ext import Handler, Updater from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index 87fb70bb8886a6..a99fe044c46923 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -16,9 +16,9 @@ CONF_COMMAND_STATE, CONF_NAME, CONF_PORT, - CONF_TIMEOUT, CONF_RESOURCE, CONF_SWITCHES, + CONF_TIMEOUT, CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/thomson/device_tracker.py b/homeassistant/components/thomson/device_tracker.py index 214c1b8cfb4edb..1f3fda6cc72c75 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -5,13 +5,13 @@ import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 1cb88f67c2fc5e..8bc4fb11cdfae2 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -1,13 +1,13 @@ """Support for Tile® Bluetooth trackers.""" -import logging from datetime import timedelta +import logging from pytile import async_login from pytile.errors import SessionExpiredError, TileError import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_MONITORED_VARIABLES, CONF_PASSWORD +from homeassistant.const import CONF_MONITORED_VARIABLES, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index cbe4c85ace31d1..9edbdd3a9a1901 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -4,13 +4,13 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_DISPLAY_OPTIONS -from homeassistant.helpers.entity import Entity +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index eabec37a0539a4..ed6476af229d0e 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -16,8 +16,8 @@ ALL_TASKS, CHECKED, COMPLETED, - CONF_PROJECT_DUE_DATE, CONF_EXTRA_PROJECTS, + CONF_PROJECT_DUE_DATE, CONF_PROJECT_LABEL_WHITELIST, CONF_PROJECT_WHITELIST, CONTENT, diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index 57348c9710a9d4..d53b5ab6cf0dd7 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -6,7 +6,6 @@ import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, @@ -14,12 +13,13 @@ ) from homeassistant.const import ( CONF_HOST, + CONF_PASSWORD, CONF_PORT, CONF_SSL, - CONF_VERIFY_SSL, - CONF_PASSWORD, CONF_USERNAME, + CONF_VERIFY_SSL, ) +import homeassistant.helpers.config_validation as cv CONF_HTTP_ID = "http_id" diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 10161856a47a71..f084c135e474f6 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -4,12 +4,12 @@ import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_EMAIL, CONF_NAME -from homeassistant.helpers.entity import Entity +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index c14ac36057e2ff..020f2d9c07fb0f 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -1,13 +1,12 @@ """The totalconnect component.""" import logging -import voluptuous as vol from total_connect_client import TotalConnectClient +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery from homeassistant.const import CONF_PASSWORD, CONF_USERNAME - +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 7aa261564f3af9..764060135a2132 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -13,8 +13,8 @@ CONF_DIMMER, CONF_DISCOVERY, CONF_LIGHT, - CONF_SWITCH, CONF_STRIP, + CONF_SWITCH, SmartDevices, async_discover_devices, get_static_devices, diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index e6789ca5aee7ee..12f3cf73e50dc1 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -11,8 +11,8 @@ CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, - WEEKDAYS, DEVICE_CLASS_TIMESTAMP, + WEEKDAYS, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 7bbc61a192ffaf..3e6f2407d176b2 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -22,12 +22,12 @@ from .const import ( ATTR_TORRENT, + DATA_UPDATED, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN, SERVICE_ADD_TORRENT, - DATA_UPDATED, ) from .errors import AuthenticationError, CannotConnect, UnknownError diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index c51d48eb53259f..6bedc793ed917c 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -8,7 +8,6 @@ from .const import DOMAIN, SENSOR_TYPES, STATE_ATTR_TORRENT_INFO - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 79df41ac4899d0..7c6990de08560d 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -2,13 +2,13 @@ from datetime import timedelta import logging -import voluptuous as vol from TransportNSW import TransportNSW +import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_MODE, CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_MODE, CONF_NAME, CONF_API_KEY, ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index 39faf987ae0935..768e1ee731662d 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -9,15 +9,14 @@ from TwitterAPI import TwitterAPI import voluptuous as vol -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_point_in_time - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_point_in_time _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/teksavvy/test_sensor.py b/tests/components/teksavvy/test_sensor.py index 9e2714f0388533..723cf07f173d41 100644 --- a/tests/components/teksavvy/test_sensor.py +++ b/tests/components/teksavvy/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the TekSavvy sensor platform.""" import asyncio + from homeassistant.bootstrap import async_setup_component from homeassistant.components.teksavvy.sensor import TekSavvyData from homeassistant.helpers.aiohttp_client import async_get_clientsession diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py index 147e2e37cb4985..3eb6299b3bedf7 100644 --- a/tests/components/threshold/test_binary_sensor.py +++ b/tests/components/threshold/test_binary_sensor.py @@ -1,8 +1,8 @@ """The test for the threshold sensor platform.""" import unittest -from homeassistant.setup import setup_component from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 5d57cd2f2d899e..39648b68fd7921 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -1,31 +1,31 @@ """The tests for the timer component.""" # pylint: disable=protected-access import asyncio -import logging from datetime import timedelta +import logging -from homeassistant.core import CoreState -from homeassistant.setup import async_setup_component from homeassistant.components.timer import ( - DOMAIN, + ATTR_DURATION, CONF_DURATION, - CONF_NAME, - STATUS_ACTIVE, - STATUS_IDLE, - STATUS_PAUSED, CONF_ICON, - ATTR_DURATION, - EVENT_TIMER_FINISHED, + CONF_NAME, + DOMAIN, EVENT_TIMER_CANCELLED, - EVENT_TIMER_STARTED, - EVENT_TIMER_RESTARTED, + EVENT_TIMER_FINISHED, EVENT_TIMER_PAUSED, - SERVICE_START, - SERVICE_PAUSE, + EVENT_TIMER_RESTARTED, + EVENT_TIMER_STARTED, SERVICE_CANCEL, SERVICE_FINISH, + SERVICE_PAUSE, + SERVICE_START, + STATUS_ACTIVE, + STATUS_IDLE, + STATUS_PAUSED, ) -from homeassistant.const import ATTR_ICON, ATTR_FRIENDLY_NAME, CONF_ENTITY_ID +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON, CONF_ENTITY_ID +from homeassistant.core import CoreState +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed diff --git a/tests/components/timer/test_reproduce_state.py b/tests/components/timer/test_reproduce_state.py index 5539d8610c3bd6..80205a40f5ddb0 100644 --- a/tests/components/timer/test_reproduce_state.py +++ b/tests/components/timer/test_reproduce_state.py @@ -9,6 +9,7 @@ STATUS_PAUSED, ) from homeassistant.core import State + from tests.common import async_mock_service diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index 2ef361e1dac343..03581d16c094b6 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -1,16 +1,18 @@ """Test Times of the Day Binary Sensor.""" +from datetime import datetime, timedelta import unittest from unittest.mock import patch -from datetime import timedelta, datetime + import pytz from homeassistant import setup -import homeassistant.core as ha from homeassistant.const import STATE_OFF, STATE_ON -import homeassistant.util.dt as dt_util -from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component +import homeassistant.core as ha from homeassistant.helpers.sun import get_astral_event_date, get_astral_event_next +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + +from tests.common import assert_setup_component, get_test_home_assistant class TestBinarySensorTod(unittest.TestCase): diff --git a/tests/components/tomato/test_device_tracker.py b/tests/components/tomato/test_device_tracker.py index 89ca2a6e1aa45b..9695780546679e 100644 --- a/tests/components/tomato/test_device_tracker.py +++ b/tests/components/tomato/test_device_tracker.py @@ -1,5 +1,6 @@ """The tests for the Tomato device tracker platform.""" from unittest import mock + import pytest import requests import requests_mock @@ -9,11 +10,11 @@ import homeassistant.components.tomato.device_tracker as tomato from homeassistant.const import ( CONF_HOST, - CONF_USERNAME, CONF_PASSWORD, + CONF_PLATFORM, CONF_PORT, CONF_SSL, - CONF_PLATFORM, + CONF_USERNAME, CONF_VERIFY_SSL, ) diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index cc5c6be4c7292b..151607b1ed8ed1 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -6,7 +6,7 @@ from homeassistant import data_entry_flow from homeassistant.components.tradfri import config_flow -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro @pytest.fixture diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index 4c691f66af872c..90449120aaf21f 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -1,7 +1,7 @@ """Tradfri lights platform tests.""" from copy import deepcopy -from unittest.mock import Mock, MagicMock, patch, PropertyMock +from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest from pytradfri.device import Device @@ -12,7 +12,6 @@ from tests.common import MockConfigEntry - DEFAULT_TEST_FEATURES = { "can_set_dimmer": False, "can_set_color": False, diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index e4b5cf5df6cd99..d78cf793d2fc0b 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -5,7 +5,7 @@ from homeassistant import setup import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant, assert_setup_component +from tests.common import assert_setup_component, get_test_home_assistant class TestTrendBinarySensor: diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 1107aace1330ff..389f9478ad3fde 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -2,27 +2,27 @@ import ctypes import os import shutil -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch import pytest import requests -import homeassistant.components.http as http -import homeassistant.components.tts as tts from homeassistant.components.demo.tts import DemoProvider +import homeassistant.components.http as http from homeassistant.components.media_player.const import ( - SERVICE_PLAY_MEDIA, - MEDIA_TYPE_MUSIC, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP, + MEDIA_TYPE_MUSIC, + SERVICE_PLAY_MEDIA, ) -from homeassistant.setup import setup_component, async_setup_component +import homeassistant.components.tts as tts +from homeassistant.setup import async_setup_component, setup_component from tests.common import ( + assert_setup_component, get_test_home_assistant, get_test_instance_port, - assert_setup_component, mock_service, ) diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 5795967e492b5f..196d0e99991ddb 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -4,6 +4,7 @@ from homeassistant import data_entry_flow from homeassistant.components import twilio from homeassistant.core import callback + from tests.common import MockDependency From ee1cc3b3dda3268bfe9421b5d623e02b0af078c4 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:42:53 +0100 Subject: [PATCH 2329/3953] Sort imports according to PEP8 for components starting with "V" (#29780) --- homeassistant/components/vasttrafik/sensor.py | 4 ++-- homeassistant/components/venstar/climate.py | 20 ++++++++--------- homeassistant/components/vera/__init__.py | 15 ++++++------- homeassistant/components/version/sensor.py | 10 ++++----- homeassistant/components/vivotek/camera.py | 4 ++-- homeassistant/components/vlc/media_player.py | 5 ++--- .../components/vlc_telnet/media_player.py | 22 +++++++++---------- .../components/volkszaehler/sensor.py | 4 ++-- .../components/volumio/media_player.py | 4 ++-- tests/components/voicerss/test_tts.py | 7 +++--- 10 files changed, 46 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index d13383a083265e..54fd0a5503ed3c 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -5,9 +5,9 @@ import vasttrafik import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.dt import now diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index de26d236649767..df7dfcacb20f9e 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -4,27 +4,27 @@ from venstarcolortouch import VenstarColorTouch import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + FAN_AUTO, + FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, + PRESET_AWAY, + PRESET_NONE, SUPPORT_FAN_MODE, - FAN_ON, - FAN_AUTO, - SUPPORT_TARGET_HUMIDITY, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, - PRESET_AWAY, - PRESET_NONE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ( diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 8fcc8a4a2fea1b..1c9d412d974ddd 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -1,25 +1,24 @@ """Support for Vera devices.""" -import logging from collections import defaultdict +import logging import pyvera as veraApi -import voluptuous as vol from requests.exceptions import RequestException +import voluptuous as vol -from homeassistant.util.dt import utc_from_timestamp -from homeassistant.util import convert, slugify -from homeassistant.helpers import discovery -from homeassistant.helpers import config_validation as cv from homeassistant.const import ( ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED, - EVENT_HOMEASSISTANT_STOP, - CONF_LIGHTS, CONF_EXCLUDE, + CONF_LIGHTS, + EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity +from homeassistant.util import convert, slugify +from homeassistant.util.dt import utc_from_timestamp _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index a6e43251b54a58..636e564b816394 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -1,20 +1,20 @@ """Sensor that can display the current Home Assistant versions.""" -import logging from datetime import timedelta +import logging from pyhaversion import ( - LocalVersion, DockerVersion, + HaIoVersion, HassioVersion, + LocalVersion, PyPiVersion, - HaIoVersion, ) import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_SOURCE +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index 2e604199dd81ed..665db37344044f 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -2,9 +2,10 @@ import logging -import voluptuous as vol from libpyvivotek import VivotekCamera +import voluptuous as vol +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, @@ -13,7 +14,6 @@ CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index 30b316cb4e8fcb..c7a3d49fabc698 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -1,10 +1,10 @@ """Provide functionality to interact with vlc devices on the network.""" import logging -import voluptuous as vol import vlc +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, @@ -18,7 +18,6 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util - _LOGGER = logging.getLogger(__name__) CONF_ARGUMENTS = "arguments" diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 1adb4eaebb30c8..45b0971ad9fa2f 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -1,33 +1,33 @@ """Provide functionality to interact with the vlc telnet interface.""" import logging -import voluptuous as vol -from python_telnet_vlc import VLCTelnet, ConnectionError as ConnErr +from python_telnet_vlc import ConnectionError as ConnErr, VLCTelnet +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, + SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, - SUPPORT_STOP, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, - SUPPORT_NEXT_TRACK, - SUPPORT_CLEAR_PLAYLIST, SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, ) from homeassistant.const import ( + CONF_HOST, CONF_NAME, + CONF_PASSWORD, + CONF_PORT, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, - CONF_HOST, - CONF_PORT, - CONF_PASSWORD, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index a2da620c1a56de..a418780c165b80 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -9,11 +9,11 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, + CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PORT, - CONF_MONITORED_CONDITIONS, - POWER_WATT, ENERGY_WATT_HOUR, + POWER_WATT, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 7c13488c3f573e..b25520c3f383cb 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -14,7 +14,7 @@ import aiohttp import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, @@ -25,11 +25,11 @@ SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, + SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - SUPPORT_SHUFFLE_SET, ) from homeassistant.const import ( CONF_HOST, diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index 0f2a0618096350..d2a7197fe1a006 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -3,16 +3,15 @@ import os import shutil -import homeassistant.components.tts as tts from homeassistant.components.media_player.const import ( - SERVICE_PLAY_MEDIA, ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, ) +import homeassistant.components.tts as tts from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, mock_service - +from tests.common import assert_setup_component, get_test_home_assistant, mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 From 1ab18083074e7388bddec59d4baad4202685b19f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:46:24 +0100 Subject: [PATCH 2330/3953] Sort imports according to PEP8 for components starting with "N" (#29773) --- .../components/ness_alarm/__init__.py | 6 ++--- .../components/netgear/device_tracker.py | 8 +++--- .../components/nfandroidtv/notify.py | 5 ++-- .../components/nissan_leaf/__init__.py | 3 ++- homeassistant/components/no_ip/__init__.py | 4 +-- homeassistant/components/notify/__init__.py | 9 +++---- homeassistant/components/nws/weather.py | 12 ++++----- tests/components/namecheapdns/test_init.py | 2 +- tests/components/ness_alarm/test_init.py | 19 +++++++------- tests/components/nextbus/test_sensor.py | 5 ++-- tests/components/no_ip/test_init.py | 2 +- tests/components/notion/test_config_flow.py | 3 ++- .../nsw_fuel_station/test_sensor.py | 3 ++- .../test_geo_location.py | 17 +++++++------ tests/components/nws/test_weather.py | 25 ++++++++----------- tests/components/nx584/test_binary_sensor.py | 4 +-- 16 files changed, 63 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index 328d3506a9749b..cc6fad1346d9f7 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -1,7 +1,7 @@ """Support for Ness D8X/D16X devices.""" +from collections import namedtuple import datetime import logging -from collections import namedtuple import voluptuous as vol @@ -9,9 +9,9 @@ from homeassistant.const import ( ATTR_CODE, ATTR_STATE, - EVENT_HOMEASSISTANT_STOP, - CONF_SCAN_INTERVAL, CONF_HOST, + CONF_SCAN_INTERVAL, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 2e20f6423a5aac..d556e83ca13707 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -4,21 +4,21 @@ from pynetgear import Netgear import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( + CONF_DEVICES, + CONF_EXCLUDE, CONF_HOST, CONF_PASSWORD, - CONF_USERNAME, CONF_PORT, CONF_SSL, - CONF_DEVICES, - CONF_EXCLUDE, + CONF_USERNAME, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index 36eed0a11db8a1..52f0af607bcf1f 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -7,9 +7,6 @@ from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol -from homeassistant.const import CONF_TIMEOUT, CONF_HOST -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, @@ -17,6 +14,8 @@ PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_HOST, CONF_TIMEOUT +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 0c72f4f43ea5e9..e5b4f34812aafa 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -1,8 +1,9 @@ """Support for the Nissan Leaf Carwings/Nissan Connect API.""" -from datetime import datetime, timedelta import asyncio +from datetime import datetime, timedelta import logging import sys + from pycarwings2 import CarwingsError, Session import voluptuous as vol diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 70ac7099d30727..12d0fb08ad30ec 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -5,11 +5,11 @@ import logging import aiohttp -from aiohttp.hdrs import USER_AGENT, AUTHORIZATION +from aiohttp.hdrs import AUTHORIZATION, USER_AGENT import async_timeout import voluptuous as vol -from homeassistant.const import CONF_DOMAIN, CONF_TIMEOUT, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 6ede7f18da732b..8211fdc08281b9 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -1,20 +1,19 @@ """Provides functionality to notify people.""" import asyncio -import logging from functools import partial +import logging from typing import Optional import voluptuous as vol -from homeassistant.setup import async_prepare_setup_platform -from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_PLATFORM +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_prepare_setup_platform from homeassistant.util import slugify - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 23cf84411a3788..c22700f1cf8c69 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -9,32 +9,32 @@ import voluptuous as vol from homeassistant.components.weather import ( - WeatherEntity, - PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_SPEED, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, + PLATFORM_SCHEMA, + WeatherEntity, ) from homeassistant.const import ( CONF_API_KEY, - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, + CONF_NAME, LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_MILES, PRESSURE_HPA, - PRESSURE_PA, PRESSURE_INHG, + PRESSURE_PA, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure diff --git a/tests/components/namecheapdns/test_init.py b/tests/components/namecheapdns/test_init.py index e3ccb48b6da5bb..1e771b11ea8aed 100644 --- a/tests/components/namecheapdns/test_init.py +++ b/tests/components/namecheapdns/test_init.py @@ -4,8 +4,8 @@ import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import namecheapdns +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed diff --git a/tests/components/ness_alarm/test_init.py b/tests/components/ness_alarm/test_init.py index 3e5ccdd5b66780..31b173f9be665c 100644 --- a/tests/components/ness_alarm/test_init.py +++ b/tests/components/ness_alarm/test_init.py @@ -1,36 +1,37 @@ """Tests for the ness_alarm component.""" from enum import Enum +from asynctest import MagicMock, patch import pytest -from asynctest import patch, MagicMock from homeassistant.components import alarm_control_panel from homeassistant.components.ness_alarm import ( - DOMAIN, + ATTR_CODE, + ATTR_OUTPUT_ID, CONF_DEVICE_PORT, + CONF_ZONE_ID, CONF_ZONE_NAME, CONF_ZONES, - CONF_ZONE_ID, + DOMAIN, SERVICE_AUX, SERVICE_PANIC, - ATTR_CODE, - ATTR_OUTPUT_ID, ) from homeassistant.const import ( - STATE_ALARM_ARMING, - SERVICE_ALARM_DISARM, ATTR_ENTITY_ID, + CONF_HOST, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_DISARM, SERVICE_ALARM_TRIGGER, - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMING, + STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, - CONF_HOST, ) from homeassistant.setup import async_setup_component + from tests.common import MockDependency VALID_CONFIG = { diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 4bac317102ac6b..bc74ebcbe1e0aa 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -1,15 +1,14 @@ """The tests for the nexbus sensor component.""" from copy import deepcopy +from unittest.mock import patch import pytest -from unittest.mock import patch -import homeassistant.components.sensor as sensor import homeassistant.components.nextbus.sensor as nextbus +import homeassistant.components.sensor as sensor from tests.common import assert_setup_component, async_setup_component - VALID_AGENCY = "sf-muni" VALID_ROUTE = "F" VALID_STOP = "5650" diff --git a/tests/components/no_ip/test_init.py b/tests/components/no_ip/test_init.py index 07b3eabae1041a..50063ab3fffb6e 100644 --- a/tests/components/no_ip/test_init.py +++ b/tests/components/no_ip/test_init.py @@ -4,8 +4,8 @@ import pytest -from homeassistant.setup import async_setup_component from homeassistant.components import no_ip +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index aa942a8905d069..f7651a570cffe7 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -1,6 +1,7 @@ """Define tests for the Notion config flow.""" -import aionotion from unittest.mock import patch + +import aionotion import pytest from homeassistant import data_entry_flow diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py index 4f1753c05186e3..babdd0cf1c3193 100644 --- a/tests/components/nsw_fuel_station/test_sensor.py +++ b/tests/components/nsw_fuel_station/test_sensor.py @@ -4,7 +4,8 @@ from homeassistant.components import sensor from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component + +from tests.common import assert_setup_component, get_test_home_assistant VALID_CONFIG = { "platform": "nsw_fuel_station", diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index 274ef3d37431f3..1915706ca1adca 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -3,22 +3,22 @@ from unittest.mock import ANY from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeed -from asynctest.mock import patch, MagicMock, call +from asynctest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.nsw_rural_fire_service_feed.geo_location import ( - ATTR_EXTERNAL_ID, - SCAN_INTERVAL, ATTR_CATEGORY, + ATTR_COUNCIL_AREA, + ATTR_EXTERNAL_ID, ATTR_FIRE, ATTR_LOCATION, - ATTR_COUNCIL_AREA, + ATTR_PUBLICATION_DATE, + ATTR_RESPONSIBLE_AGENCY, + ATTR_SIZE, ATTR_STATUS, ATTR_TYPE, - ATTR_SIZE, - ATTR_RESPONSIBLE_AGENCY, - ATTR_PUBLICATION_DATE, + SCAN_INTERVAL, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -34,9 +34,10 @@ EVENT_HOMEASSISTANT_STOP, ) from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_fire_time_changed import homeassistant.util.dt as dt_util +from tests.common import assert_setup_component, async_fire_time_changed + CONFIG = { geo_location.DOMAIN: [{"platform": "nsw_rural_fire_service_feed", CONF_RADIUS: 200}] } diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index 0e450f06238d94..adda88a789d828 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -1,13 +1,5 @@ """Tests for the NWS weather component.""" from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB -from homeassistant.components.weather import ( - ATTR_WEATHER_HUMIDITY, - ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_TEMPERATURE, - ATTR_WEATHER_VISIBILITY, - ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED, -) from homeassistant.components.weather import ( ATTR_FORECAST, ATTR_FORECAST_CONDITION, @@ -15,25 +7,30 @@ ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, ) - from homeassistant.const import ( LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_MILES, + PRESSURE_HPA, PRESSURE_INHG, PRESSURE_PA, - PRESSURE_HPA, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.setup import async_setup_component from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.setup import async_setup_component +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from tests.common import load_fixture, assert_setup_component +from tests.common import assert_setup_component, load_fixture EXP_OBS_IMP = { ATTR_WEATHER_TEMPERATURE: round( diff --git a/tests/components/nx584/test_binary_sensor.py b/tests/components/nx584/test_binary_sensor.py index 3736d97bbcdad4..24823e9046a122 100644 --- a/tests/components/nx584/test_binary_sensor.py +++ b/tests/components/nx584/test_binary_sensor.py @@ -1,15 +1,15 @@ """The tests for the nx584 sensor platform.""" -import requests import unittest from unittest import mock from nx584 import client as nx584_client +import pytest +import requests from homeassistant.components.nx584 import binary_sensor as nx584 from homeassistant.setup import setup_component from tests.common import get_test_home_assistant -import pytest class StopMe(Exception): From fbf1836997b64c574af24bb78ab2b8abaeaf5c59 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:47:53 +0100 Subject: [PATCH 2331/3953] Sort imports according to PEP8 for components starting with "W" (#29781) --- homeassistant/components/w800rf32/__init__.py | 3 +- .../components/w800rf32/binary_sensor.py | 2 +- homeassistant/components/waqi/sensor.py | 8 ++--- .../components/water_heater/__init__.py | 29 +++++++++---------- .../components/waterfurnace/__init__.py | 7 ++--- homeassistant/components/watson_tts/tts.py | 2 +- homeassistant/components/weather/__init__.py | 1 - homeassistant/components/webhook/__init__.py | 6 ++-- homeassistant/components/weblink/__init__.py | 4 +-- homeassistant/components/wink/__init__.py | 2 +- homeassistant/components/wink/climate.py | 4 +-- .../components/workday/binary_sensor.py | 4 +-- homeassistant/components/worldclock/sensor.py | 4 +-- .../components/worxlandroid/sensor.py | 8 ++--- homeassistant/components/wsdot/sensor.py | 10 +++---- .../components/wunderground/sensor.py | 16 +++++----- .../components/wunderlist/__init__.py | 2 +- homeassistant/components/wwlln/config_flow.py | 2 +- tests/components/wake_on_lan/test_switch.py | 5 ++-- tests/components/water_heater/common.py | 2 +- tests/components/weather/test_weather.py | 12 ++++---- tests/components/weblink/test_init.py | 2 +- .../components/workday/test_binary_sensor.py | 3 +- tests/components/worldclock/test_sensor.py | 3 +- tests/components/wsdot/test_sensor.py | 3 +- tests/components/wunderground/test_sensor.py | 7 +++-- 26 files changed, 73 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/w800rf32/__init__.py b/homeassistant/components/w800rf32/__init__.py index 805cca47023d42..bbd3fdac23d5fc 100644 --- a/homeassistant/components/w800rf32/__init__.py +++ b/homeassistant/components/w800rf32/__init__.py @@ -1,15 +1,14 @@ """Support for w800rf32 devices.""" import logging -import voluptuous as vol import W800rf32 as w800 +import voluptuous as vol from homeassistant.const import ( CONF_DEVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) - import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index e08111da8ba08f..9c83dbce804ff4 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -1,8 +1,8 @@ """Support for w800rf32 binary sensors.""" import logging -import voluptuous as vol import W800rf32 as w800 +import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index b53723a29b62da..8f16c216b37106 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -1,21 +1,21 @@ """Support for the World Air Quality Index service.""" import asyncio -import logging from datetime import timedelta +import logging import aiohttp import voluptuous as vol from waqiasync import WaqiClient -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_TIME, ATTR_TEMPERATURE, + ATTR_TIME, CONF_TOKEN, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 6e7b918c289e52..c5ba009717ccdf 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -1,32 +1,31 @@ """Support for water heater devices.""" from datetime import timedelta -import logging import functools as ft +import logging import voluptuous as vol -from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import ( # noqa: F401 - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, - SERVICE_TURN_ON, + PRECISION_TENTHS, + PRECISION_WHOLE, SERVICE_TURN_OFF, - STATE_ON, + SERVICE_TURN_ON, STATE_OFF, + STATE_ON, TEMP_CELSIUS, - PRECISION_WHOLE, - PRECISION_TENTHS, TEMP_FAHRENHEIT, ) - +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.temperature import display_temp as show_temp +from homeassistant.util.temperature import convert as convert_temperature # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/waterfurnace/__init__.py b/homeassistant/components/waterfurnace/__init__.py index b6eb22c89ae248..942ab8a14ac4db 100644 --- a/homeassistant/components/waterfurnace/__init__.py +++ b/homeassistant/components/waterfurnace/__init__.py @@ -1,16 +1,15 @@ """Support for Waterfurnaces.""" from datetime import timedelta import logging -import time import threading +import time import voluptuous as vol from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv, discovery _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 520151cf20ebff..74803464484bfb 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -1,8 +1,8 @@ """Support for IBM Watson TTS integration.""" import logging -from ibm_watson import TextToSpeechV1 from ibm_cloud_sdk_core.authenticators import IAMAuthenticator +from ibm_watson import TextToSpeechV1 import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index bdeedd4cd6be70..01ca8ed67904bb 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -11,7 +11,6 @@ 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__) diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 5a41bfa9851404..d51771d0b16778 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -1,14 +1,14 @@ """Webhooks for Home Assistant.""" import logging -from aiohttp.web import Response, Request +from aiohttp.web import Request, Response import voluptuous as vol -from homeassistant.core import callback -from homeassistant.loader import bind_hass from homeassistant.auth.util import generate_secret from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView +from homeassistant.core import callback +from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weblink/__init__.py b/homeassistant/components/weblink/__init__.py index a6ee72fa147581..be6814da30c4a7 100644 --- a/homeassistant/components/weblink/__init__.py +++ b/homeassistant/components/weblink/__init__.py @@ -3,10 +3,10 @@ import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_ICON, CONF_URL +from homeassistant.const import CONF_ICON, CONF_NAME, CONF_URL +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 3bda8b314f2fdd..b3ae01c8f6733b 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -6,8 +6,8 @@ import time from aiohttp.web import Response -import pywink from pubnubsubhandler import PubNubSubscriptionHandler +import pywink import voluptuous as vol from homeassistant.components.http import HomeAssistantView diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index 6323fa7bbfe8aa..85d477313f164a 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -23,12 +23,12 @@ HVAC_MODE_OFF, PRESET_AWAY, PRESET_ECO, + PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - SUPPORT_PRESET_MODE, - PRESET_NONE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.helpers.temperature import display_temp as show_temp diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index efa8b6ad77b487..866d87d62401f3 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -1,12 +1,12 @@ """Sensor to indicate whether the current day is a workday.""" -import logging from datetime import datetime, timedelta +import logging import holidays import voluptuous as vol +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_NAME, WEEKDAYS -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index 103c38819bb056..1709ac7d23e296 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -5,9 +5,9 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_TIME_ZONE -import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index dd74c5b7d17220..fa2cae53f52cd8 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -1,18 +1,16 @@ """Support for Worx Landroid mower.""" -import logging import asyncio +import logging import aiohttp import async_timeout - import voluptuous as vol -import homeassistant.helpers.config_validation as cv - -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PIN, CONF_TIMEOUT from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 2b6c0b73563524..5afa3a3efcf38f 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -1,21 +1,21 @@ """Support for Washington State Department of Transportation (WSDOT) data.""" +from datetime import datetime, timedelta, timezone import logging import re -from datetime import datetime, timezone, timedelta import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, ATTR_ATTRIBUTION, - CONF_ID, ATTR_NAME, + CONF_API_KEY, + CONF_ID, + CONF_NAME, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 5272b33ccb5673..84c870af54d55e 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -9,27 +9,27 @@ import async_timeout import voluptuous as vol -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components import sensor from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, + ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, + CONF_MONITORED_CONDITIONS, + LENGTH_FEET, LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, - LENGTH_FEET, - ATTR_ATTRIBUTION, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import Throttle _RESOURCE = "http://api.wunderground.com/api/{}/{}/{}/q/" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py index 122d09feaa4040..4d9ff6e2235743 100644 --- a/homeassistant/components/wunderlist/__init__.py +++ b/homeassistant/components/wunderlist/__init__.py @@ -4,8 +4,8 @@ import voluptuous as vol from wunderpy2 import WunderApi +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME, CONF_ACCESS_TOKEN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wwlln/config_flow.py b/homeassistant/components/wwlln/config_flow.py index 3e43ba932788c8..f9cd022f25503c 100644 --- a/homeassistant/components/wwlln/config_flow.py +++ b/homeassistant/components/wwlln/config_flow.py @@ -2,7 +2,6 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.helpers import config_validation as cv from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -12,6 +11,7 @@ CONF_UNIT_SYSTEM_METRIC, ) from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv from .const import CONF_WINDOW, DEFAULT_RADIUS, DEFAULT_WINDOW, DOMAIN diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index 1fbcc9cc273ff1..e0f12f9c7f8ee2 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -2,14 +2,13 @@ import unittest from unittest.mock import patch -from homeassistant.setup import setup_component -from homeassistant.const import STATE_ON, STATE_OFF import homeassistant.components.switch as switch +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_service from tests.components.switch import common - TEST_STATE = None diff --git a/tests/components/water_heater/common.py b/tests/components/water_heater/common.py index 0808e3e3daca8e..04fd345577ef1b 100644 --- a/tests/components/water_heater/common.py +++ b/tests/components/water_heater/common.py @@ -9,8 +9,8 @@ ATTR_OPERATION_MODE, DOMAIN, SERVICE_SET_AWAY_MODE, - SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, + SERVICE_SET_TEMPERATURE, ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, ENTITY_MATCH_ALL from homeassistant.loader import bind_hass diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py index bbfd07fb551d33..fd960b594a07de 100644 --- a/tests/components/weather/test_weather.py +++ b/tests/components/weather/test_weather.py @@ -3,6 +3,11 @@ from homeassistant.components import weather from homeassistant.components.weather import ( + ATTR_FORECAST, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, @@ -10,14 +15,9 @@ ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - ATTR_FORECAST, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ) -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.setup import setup_component +from homeassistant.util.unit_system import METRIC_SYSTEM from tests.common import get_test_home_assistant diff --git a/tests/components/weblink/test_init.py b/tests/components/weblink/test_init.py index ae519b95ada70e..5f803107c4629a 100644 --- a/tests/components/weblink/test_init.py +++ b/tests/components/weblink/test_init.py @@ -1,8 +1,8 @@ """The tests for the weblink component.""" import unittest -from homeassistant.setup import setup_component from homeassistant.components import weblink +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index eeb0707ea50456..19da0cbfd87706 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -5,8 +5,7 @@ from homeassistant.components.workday.binary_sensor import day_to_string from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component - +from tests.common import assert_setup_component, get_test_home_assistant FUNCTION_PATH = "homeassistant.components.workday.binary_sensor.get_date" diff --git a/tests/components/worldclock/test_sensor.py b/tests/components/worldclock/test_sensor.py index 27cf574f7f8db6..b0e8119035da9c 100644 --- a/tests/components/worldclock/test_sensor.py +++ b/tests/components/worldclock/test_sensor.py @@ -2,9 +2,10 @@ import unittest from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant import homeassistant.util.dt as dt_util +from tests.common import get_test_home_assistant + class TestWorldClockSensor(unittest.TestCase): """Test the World clock sensor.""" diff --git a/tests/components/wsdot/test_sensor.py b/tests/components/wsdot/test_sensor.py index 96e2c66a1631a9..b548c099f40231 100644 --- a/tests/components/wsdot/test_sensor.py +++ b/tests/components/wsdot/test_sensor.py @@ -4,7 +4,6 @@ import unittest import requests_mock -from tests.common import get_test_home_assistant, load_fixture import homeassistant.components.wsdot.sensor as wsdot from homeassistant.components.wsdot.sensor import ( @@ -19,6 +18,8 @@ ) from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant, load_fixture + class TestWSDOT(unittest.TestCase): """Test the WSDOT platform.""" diff --git a/tests/components/wunderground/test_sensor.py b/tests/components/wunderground/test_sensor.py index 54072e04ab57a0..70264249157040 100644 --- a/tests/components/wunderground/test_sensor.py +++ b/tests/components/wunderground/test_sensor.py @@ -1,14 +1,15 @@ """The tests for the WUnderground platform.""" import asyncio -import aiohttp +import aiohttp from pytest import raises import homeassistant.components.wunderground.sensor as wunderground -from homeassistant.const import TEMP_CELSIUS, LENGTH_INCHES, STATE_UNKNOWN +from homeassistant.const import LENGTH_INCHES, STATE_UNKNOWN, TEMP_CELSIUS from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component -from tests.common import load_fixture, assert_setup_component + +from tests.common import assert_setup_component, load_fixture VALID_CONFIG_PWS = { "platform": "wunderground", From 4035fda659160af9a0ad0675f3a47714357f810c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:56:20 +0100 Subject: [PATCH 2332/3953] Sort imports according to PEP8 for components starting with "Q" (#29785) --- tests/components/qld_bushfire/test_geo_location.py | 5 +++-- tests/components/qwikswitch/test_init.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 59de4643cb8e8b..86ab16c5f1bcce 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -1,6 +1,6 @@ """The tests for the Queensland Bushfire Alert Feed platform.""" import datetime -from unittest.mock import patch, MagicMock, call +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -25,9 +25,10 @@ EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_fire_time_changed import homeassistant.util.dt as dt_util +from tests.common import assert_setup_component, async_fire_time_changed + CONFIG = {geo_location.DOMAIN: [{"platform": "qld_bushfire", CONF_RADIUS: 200}]} CONFIG_WITH_CUSTOM_LOCATION = { diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index e573e8cc293a17..d9c2a8d0ba63a2 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -1,14 +1,14 @@ """Test qwikswitch sensors.""" import logging +from aiohttp.client_exceptions import ClientError import pytest -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH from homeassistant.bootstrap import async_setup_component -from tests.test_util.aiohttp import mock_aiohttp_client -from aiohttp.client_exceptions import ClientError +from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH +from homeassistant.const import EVENT_HOMEASSISTANT_START +from tests.test_util.aiohttp import mock_aiohttp_client _LOGGER = logging.getLogger(__name__) From 9bcd4653e0f51d666081c9762446ffe7eef6f134 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:57:42 +0100 Subject: [PATCH 2333/3953] Sort imports according to PEP8 for components starting with "Y" (#29783) --- .../yale_smart_alarm/alarm_control_panel.py | 8 ++++---- homeassistant/components/yamaha/media_player.py | 5 ++--- .../components/yamaha_musiccast/media_player.py | 4 ++-- homeassistant/components/yandex_transport/sensor.py | 8 ++++---- homeassistant/components/yeelightsunflower/light.py | 12 ++++++------ homeassistant/components/yessssms/notify.py | 7 ++----- homeassistant/components/yr/sensor.py | 11 +++++------ tests/components/yamaha/test_media_player.py | 5 +++-- .../yandex_transport/test_yandex_transport_sensor.py | 6 ++++-- tests/components/yandextts/test_tts.py | 8 ++++---- tests/components/yessssms/test_notify.py | 7 +++---- tests/components/yr/test_sensor.py | 2 +- 12 files changed, 40 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 7f2cbc2a33dddb..05823a511dd730 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -3,11 +3,11 @@ import voluptuous as vol from yalesmartalarmclient.client import ( - YaleSmartAlarmClient, - AuthenticationError, - YALE_STATE_DISARM, - YALE_STATE_ARM_PARTIAL, YALE_STATE_ARM_FULL, + YALE_STATE_ARM_PARTIAL, + YALE_STATE_DISARM, + AuthenticationError, + YaleSmartAlarmClient, ) from homeassistant.components.alarm_control_panel import ( diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index fa2c68dce88b49..7ab7d5b3a47222 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -5,7 +5,7 @@ import rxv import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -13,15 +13,14 @@ SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOUND_MODE, ) - from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 18b80cc40855b0..ae5b78b9116beb 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -1,11 +1,11 @@ """Support for Yamaha MusicCast Receivers.""" import logging - import socket + import pymusiccast import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 4bf634a61f407a..54db4882e74b2d 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -1,16 +1,16 @@ """Service for obtaining information about closer bus from Transport Yandex Service.""" -import logging from datetime import timedelta +import logging import voluptuous as vol from ya_ma import YandexMapsRequester -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 3424014e8f4c73..c49c874dc21ddf 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -1,19 +1,19 @@ """Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).""" import logging -import yeelightsunflower import voluptuous as vol +import yeelightsunflower -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - Light, - ATTR_HS_COLOR, - SUPPORT_COLOR, ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, + ATTR_HS_COLOR, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + Light, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yessssms/notify.py b/homeassistant/components/yessssms/notify.py index 1c1eed0e89d18c..70c85f7bdb345e 100644 --- a/homeassistant/components/yessssms/notify.py +++ b/homeassistant/components/yessssms/notify.py @@ -1,16 +1,13 @@ """Support for the YesssSMS platform.""" import logging -import voluptuous as vol - from YesssSMS import YesssSMS +import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - - from .const import CONF_PROVIDER _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index fc754c9a2579eb..f8fbc97962f6f0 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -1,23 +1,21 @@ """Support for Yr.no weather service.""" import asyncio import logging - from random import randrange from xml.parsers.expat import ExpatError import aiohttp import async_timeout -import xmltodict import voluptuous as vol +import xmltodict -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, - CONF_ELEVATION, CONF_MONITORED_CONDITIONS, - ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -26,8 +24,9 @@ TEMP_CELSIUS, ) 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_utc_time_change, async_call_later +from homeassistant.helpers.event import async_call_later, async_track_utc_time_change from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 7e5b04f0269eb9..6b101167c85c20 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -1,10 +1,11 @@ """The tests for the Yamaha Media player platform.""" import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch -from homeassistant.setup import setup_component import homeassistant.components.media_player as mp from homeassistant.components.yamaha import media_player as yamaha +from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index 7997d01bd13a16..a67108dc93b708 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -1,15 +1,17 @@ """Tests for the yandex transport platform.""" import json + import pytest import homeassistant.components.sensor as sensor -import homeassistant.util.dt as dt_util from homeassistant.const import CONF_NAME +import homeassistant.util.dt as dt_util + from tests.common import ( + MockDependency, assert_setup_component, async_setup_component, - MockDependency, load_fixture, ) diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index c532732ccc5213..edd5c058f12615 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -3,14 +3,14 @@ import os import shutil -import homeassistant.components.tts as tts -from homeassistant.setup import setup_component from homeassistant.components.media_player.const import ( - SERVICE_PLAY_MEDIA, DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, ) -from tests.common import get_test_home_assistant, assert_setup_component, mock_service +import homeassistant.components.tts as tts +from homeassistant.setup import setup_component +from tests.common import assert_setup_component, get_test_home_assistant, mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py index dbc33b5a388725..e5ef24ac15060a 100644 --- a/tests/components/yessssms/test_notify.py +++ b/tests/components/yessssms/test_notify.py @@ -1,16 +1,15 @@ """The tests for the notify yessssms platform.""" +import logging import unittest from unittest.mock import patch -import logging import pytest import requests_mock -from homeassistant.setup import async_setup_component -import homeassistant.components.yessssms.notify as yessssms from homeassistant.components.yessssms.const import CONF_PROVIDER - +import homeassistant.components.yessssms.notify as yessssms from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME +from homeassistant.setup import async_setup_component @pytest.fixture(name="config") diff --git a/tests/components/yr/test_sensor.py b/tests/components/yr/test_sensor.py index dce387b2c8cbb4..1e8282a58ffaae 100644 --- a/tests/components/yr/test_sensor.py +++ b/tests/components/yr/test_sensor.py @@ -5,8 +5,8 @@ from homeassistant.bootstrap import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import assert_setup_component, load_fixture +from tests.common import assert_setup_component, load_fixture NOW = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) From 96a6a44411d822249df90343f183e2cbd9732274 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 14:58:51 +0100 Subject: [PATCH 2334/3953] Sort imports according to PEP8 for components starting with "X" (#29782) --- homeassistant/components/x10/light.py | 6 +++--- homeassistant/components/xbox_live/sensor.py | 2 +- homeassistant/components/xeoma/camera.py | 2 +- .../components/xfinity/device_tracker.py | 2 +- .../components/xiaomi_tv/media_player.py | 4 ++-- homeassistant/components/xmpp/notify.py | 15 +++++++-------- homeassistant/components/xs1/climate.py | 2 +- tests/components/xiaomi/test_device_tracker.py | 4 ++-- 8 files changed, 18 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/x10/light.py b/homeassistant/components/x10/light.py index bb2d2d89456b9e..1f74326d544c09 100644 --- a/homeassistant/components/x10/light.py +++ b/homeassistant/components/x10/light.py @@ -1,16 +1,16 @@ """Support for X10 lights.""" import logging -from subprocess import check_output, CalledProcessError, STDOUT +from subprocess import STDOUT, CalledProcessError, check_output import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_ID, CONF_DEVICES from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index e837cc4bbbc0fe..41ebf69126af65 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -1,6 +1,6 @@ """Sensor for Xbox Live account status.""" -import logging from datetime import timedelta +import logging import voluptuous as vol from xboxapi import xbox_api diff --git a/homeassistant/components/xeoma/camera.py b/homeassistant/components/xeoma/camera.py index bb5febe6bd78c0..d6f313c0382170 100644 --- a/homeassistant/components/xeoma/camera.py +++ b/homeassistant/components/xeoma/camera.py @@ -1,8 +1,8 @@ """Support for Xeoma Cameras.""" import logging -import voluptuous as vol from pyxeoma.xeoma import Xeoma, XeomaError +import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME diff --git a/homeassistant/components/xfinity/device_tracker.py b/homeassistant/components/xfinity/device_tracker.py index 712d31d46db5bd..524929ae42d442 100644 --- a/homeassistant/components/xfinity/device_tracker.py +++ b/homeassistant/components/xfinity/device_tracker.py @@ -5,13 +5,13 @@ import voluptuous as vol from xfinity_gateway import XfinityGateway -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index c34448ba63b4fd..c82708852c2bbc 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -1,10 +1,10 @@ """Add support for the Xiaomi TVs.""" import logging -import voluptuous as vol import pymitv +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 338b0b85c038b8..d26b1eed151360 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -9,14 +9,20 @@ import requests import slixmpp from slixmpp.exceptions import IqError, IqTimeout, XMPPError -from slixmpp.xmlstream.xmlstream import NotConnectedError from slixmpp.plugins.xep_0363.http_upload import ( FileTooBig, FileUploadError, UploadServiceNotFound, ) +from slixmpp.xmlstream.xmlstream import NotConnectedError import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_PASSWORD, CONF_RECIPIENT, @@ -27,13 +33,6 @@ import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper -from homeassistant.components.notify import ( - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - _LOGGER = logging.getLogger(__name__) ATTR_DATA = "data" diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index 95ad10539bd1a3..33c778c0d3df2d 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -5,8 +5,8 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_HEAT, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 60e13ab3493722..76788d0c3e89bf 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -1,13 +1,13 @@ """The tests for the Xiaomi router device tracker platform.""" import logging -from asynctest import mock, patch +from asynctest import mock, patch import requests from homeassistant.components.device_tracker import DOMAIN import homeassistant.components.xiaomi.device_tracker as xiaomi from homeassistant.components.xiaomi.device_tracker import get_scanner -from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME _LOGGER = logging.getLogger(__name__) From bb3fa6990a958d74e4f653c2137554800301b2a1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 9 Dec 2019 15:10:03 +0100 Subject: [PATCH 2335/3953] Updated frontend to 20191204.1 (#29787) --- 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 03d2d7899b07ba..75d02baaeeb23b 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191204.0" + "home-assistant-frontend==20191204.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 07b9b4c80e8b30..afc92f92ef790f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.30 -home-assistant-frontend==20191204.0 +home-assistant-frontend==20191204.1 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0e163f775951af..e5af926a51e707 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191204.0 +home-assistant-frontend==20191204.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f30a3cfcc7580..4cb7ca26a87035 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,7 +231,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191204.0 +home-assistant-frontend==20191204.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.7 From 72f336a2ddfaf7dc6af65b6604af892c20bb284c Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 9 Dec 2019 16:10:02 +0100 Subject: [PATCH 2336/3953] Move imports to top for homekit (#29560) * Move imports to top for homekit * Moved back a couple imports, added annotation to disable import-outside-toplevel * Fix all tests in test_homekit.py --- homeassistant/components/homekit/__init__.py | 4 ++-- tests/components/homekit/test_homekit.py | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index cfd59ad61a4f60..9a2b2d5a851ea5 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -29,6 +29,7 @@ from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry +from .accessories import HomeBridge, HomeDriver from .const import ( BRIDGE_NAME, CONF_ADVERTISE_IP, @@ -302,7 +303,6 @@ def __init__( def setup(self): """Set up bridge and accessory driver.""" - from .accessories import HomeBridge, HomeDriver self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) @@ -361,7 +361,7 @@ def start(self, *args): return self.status = STATUS_WAIT - from . import ( # noqa: F401 pylint: disable=unused-import + from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel type_covers, type_fans, type_lights, diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index de6aaf0f11eb58..c845370e278499 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -126,7 +126,7 @@ async def test_homekit_setup(hass, hk_driver): homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, DEFAULT_SAFE_MODE) with patch( - PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver + PATH_HOMEKIT + ".HomeDriver", return_value=hk_driver ) as mock_driver, patch("homeassistant.util.get_local_ip") as mock_ip: mock_ip.return_value = IP_ADDRESS await hass.async_add_job(homekit.setup) @@ -150,9 +150,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver): """Test setup with given IP address.""" homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, "172.0.0.0", {}, {}, None) - with patch( - PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver - ) as mock_driver: + with patch(PATH_HOMEKIT + ".HomeDriver", return_value=hk_driver) as mock_driver: await hass.async_add_job(homekit.setup) mock_driver.assert_called_with( hass, @@ -169,9 +167,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver): hass, BRIDGE_NAME, DEFAULT_PORT, "0.0.0.0", {}, {}, None, "192.168.1.100" ) - with patch( - PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver - ) as mock_driver: + with patch(PATH_HOMEKIT + ".HomeDriver", return_value=hk_driver) as mock_driver: await hass.async_add_job(homekit.setup) mock_driver.assert_called_with( hass, @@ -186,7 +182,7 @@ async def test_homekit_setup_safe_mode(hass, hk_driver): """Test if safe_mode flag is set.""" homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, True) - with patch(PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver): + with patch(PATH_HOMEKIT + ".HomeDriver", return_value=hk_driver): await hass.async_add_job(homekit.setup) assert homekit.driver.safe_mode is True From 3a28361bebbe11d124793a0ec269de74ef180665 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 9 Dec 2019 16:21:12 +0100 Subject: [PATCH 2337/3953] Cleanup removed component (#29788) --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index b974356e0fe0aa..8f872a93d8dae6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -675,7 +675,6 @@ omit = homeassistant/components/systemmonitor/sensor.py homeassistant/components/tado/* homeassistant/components/tado/device_tracker.py - homeassistant/components/tahoma/* homeassistant/components/tank_utility/sensor.py homeassistant/components/tapsaff/binary_sensor.py homeassistant/components/tautulli/sensor.py From 5cb6d1b21fcfd46e517f89c801cd925f2f4b9498 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 16:24:03 +0100 Subject: [PATCH 2338/3953] Sort imports according to PEP8 for 'script' folder (#29790) --- script/gen_requirements_all.py | 4 ++-- script/hassfest/__main__.py | 2 +- script/hassfest/codeowners.py | 2 +- script/hassfest/config_flow.py | 2 +- script/hassfest/dependencies.py | 2 +- script/hassfest/manifest.py | 1 - script/hassfest/manifest_helper.py | 1 - script/hassfest/model.py | 4 ++-- script/hassfest/services.py | 2 +- script/hassfest/ssdp.py | 2 +- script/hassfest/zeroconf.py | 2 +- script/inspect_schemas.py | 2 +- script/lazytox.py | 6 +++--- script/scaffold/__main__.py | 3 +-- script/scaffold/docs.py | 1 - script/scaffold/gather_info.py | 3 +-- .../config_flow/integration/__init__.py | 2 +- .../config_flow/integration/config_flow.py | 2 +- .../config_flow/tests/test_config_flow.py | 2 +- .../integration/__init__.py | 2 +- .../integration/config_flow.py | 3 ++- .../config_flow_oauth2/integration/__init__.py | 10 +++++----- .../config_flow_oauth2/integration/api.py | 2 +- .../integration/config_flow.py | 1 + .../config_flow_oauth2/tests/test_config_flow.py | 2 +- .../device_action/integration/device_action.py | 12 +++++++----- .../device_action/tests/test_device_action.py | 4 ++-- .../integration/device_condition.py | 8 +++++--- .../tests/test_device_condition.py | 6 +++--- .../device_trigger/integration/device_trigger.py | 16 +++++++++------- .../device_trigger/tests/test_device_trigger.py | 6 +++--- .../integration/integration/__init__.py | 1 - .../integration/reproduce_state.py | 4 ++-- script/translations_download_split.py | 2 +- script/translations_upload_merge.py | 2 +- script/version_bump.py | 2 +- 36 files changed, 65 insertions(+), 63 deletions(-) diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9bbe7d379ec29e..c18b51ba5d1aae 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -8,10 +8,10 @@ import re import sys -from homeassistant.util.yaml.loader import load_yaml - from script.hassfest.model import Integration +from homeassistant.util.yaml.loader import load_yaml + COMMENT_REQUIREMENTS = ( "Adafruit_BBIO", "Adafruit-DHT", diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index a1168b15f7deb3..78b46b8db57d15 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -2,8 +2,8 @@ import pathlib import sys -from .model import Integration, Config from . import codeowners, config_flow, dependencies, manifest, services, ssdp, zeroconf +from .model import Config, Integration PLUGINS = [codeowners, config_flow, dependencies, manifest, services, ssdp, zeroconf] diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 6f63fab3fdb8ba..f6970b50a3cd12 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -1,7 +1,7 @@ """Generate CODEOWNERS.""" from typing import Dict -from .model import Integration, Config +from .model import Config, Integration BASE = """ # This file is generated by script/hassfest/codeowners.py diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 4384399f4db393..83d495e1bf2b69 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -2,7 +2,7 @@ import json from typing import Dict -from .model import Integration, Config +from .model import Config, Integration BASE = """ \"\"\"Automatically generated by hassfest. diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 42a31f206105c2..71936411b75d47 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -1,7 +1,7 @@ """Validate dependencies.""" import pathlib import re -from typing import Set, Dict +from typing import Dict, Set from .model import Integration diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 16f8b77b5d382d..acc5e9af8328d5 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -6,7 +6,6 @@ from .model import Integration - MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, diff --git a/script/hassfest/manifest_helper.py b/script/hassfest/manifest_helper.py index 251a29398070da..0c2a1456ec6ac4 100644 --- a/script/hassfest/manifest_helper.py +++ b/script/hassfest/manifest_helper.py @@ -2,7 +2,6 @@ import json import pathlib - component_dir = pathlib.Path("homeassistant/components") diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 77683d659619b7..faa1c26262c2dc 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -1,8 +1,8 @@ """Models for manifest validator.""" +import importlib import json -from typing import List, Dict, Any import pathlib -import importlib +from typing import Any, Dict, List import attr diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 801ee10e43a8ce..08cde60f5b5e21 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -1,8 +1,8 @@ """Validate dependencies.""" import pathlib +import re from typing import Dict -import re import voluptuous as vol from voluptuous.humanize import humanize_error diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index d2dd724605e527..7578d52ed4fd57 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -3,7 +3,7 @@ import json from typing import Dict -from .model import Integration, Config +from .model import Config, Integration BASE = """ \"\"\"Automatically generated by hassfest. diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index 3d93d363086162..f864d3e0327d17 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -3,7 +3,7 @@ import json from typing import Dict -from .model import Integration, Config +from .model import Config, Integration BASE = """ \"\"\"Automatically generated by hassfest. diff --git a/script/inspect_schemas.py b/script/inspect_schemas.py index e46165bfaa25a0..4f305a4fb9cc73 100755 --- a/script/inspect_schemas.py +++ b/script/inspect_schemas.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Inspect all component SCHEMAS.""" -import os import importlib +import os import pkgutil from homeassistant.config import _identify_config_schema diff --git a/script/lazytox.py b/script/lazytox.py index ca8a160e7dced2..d1b09618998e5d 100755 --- a/script/lazytox.py +++ b/script/lazytox.py @@ -4,12 +4,12 @@ This is NOT a tox replacement, only a quick check during development. """ -import os import asyncio -import sys +from collections import namedtuple +import os import re import shlex -from collections import namedtuple +import sys try: from colorlog.escape_codes import escape_codes diff --git a/script/scaffold/__main__.py b/script/scaffold/__main__.py index 78490b84ba3d9b..94ac009fd9c698 100644 --- a/script/scaffold/__main__.py +++ b/script/scaffold/__main__.py @@ -4,10 +4,9 @@ import subprocess import sys -from . import gather_info, generate, error, docs +from . import docs, error, gather_info, generate from .const import COMPONENT_DIR - TEMPLATES = [ p.name for p in (Path(__file__).parent / "templates").glob("*") if p.is_dir() ] diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index 5df663fec0b240..8186b857e80958 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -1,7 +1,6 @@ """Print links to relevant docs.""" from .model import Info - DATA = { "config_flow": { "title": "Config Flow", diff --git a/script/scaffold/gather_info.py b/script/scaffold/gather_info.py index 6a69040a6d7554..48d0a20ea73321 100644 --- a/script/scaffold/gather_info.py +++ b/script/scaffold/gather_info.py @@ -4,9 +4,8 @@ from homeassistant.util import slugify from .const import COMPONENT_DIR -from .model import Info from .error import ExitApp - +from .model import Info CHECK_EMPTY = ["Cannot be empty", lambda value: value] diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py index 04b908952d1c0f..4a206981c3c9a5 100644 --- a/script/scaffold/templates/config_flow/integration/__init__.py +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -3,8 +3,8 @@ import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from .const import DOMAIN diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py index e08851f47a0358..e2452b5324ddb9 100644 --- a/script/scaffold/templates/config_flow/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -3,7 +3,7 @@ import voluptuous as vol -from homeassistant import core, config_entries, exceptions +from homeassistant import config_entries, core, exceptions from .const import DOMAIN # pylint:disable=unused-import diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py index 35d8a96ab2b796..b68adc897bb89f 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -2,8 +2,8 @@ from unittest.mock import patch from homeassistant import config_entries, setup -from homeassistant.components.NEW_DOMAIN.const import DOMAIN from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth +from homeassistant.components.NEW_DOMAIN.const import DOMAIN from tests.common import mock_coro diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py index 04b908952d1c0f..4a206981c3c9a5 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/__init__.py +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -3,8 +3,8 @@ import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from .const import DOMAIN diff --git a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py index 16d13aaa99ffc0..db5f719ce3db4a 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py @@ -1,8 +1,9 @@ """Config flow for NEW_NAME.""" import my_pypi_dependency -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + from .const import DOMAIN diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index 30e7ad978107bf..d561f284caf252 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -3,17 +3,17 @@ import voluptuous as vol -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant from homeassistant.helpers import ( - config_validation as cv, - config_entry_oauth2_flow, aiohttp_client, + config_entry_oauth2_flow, + config_validation as cv, ) -from homeassistant.config_entries import ConfigEntry -from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from . import api, config_flow +from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN CONFIG_SCHEMA = vol.Schema( { diff --git a/script/scaffold/templates/config_flow_oauth2/integration/api.py b/script/scaffold/templates/config_flow_oauth2/integration/api.py index c5aa4a81ebe0f1..f1a1f6a7ec4958 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/api.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/api.py @@ -4,7 +4,7 @@ from aiohttp import ClientSession import my_pypi_package -from homeassistant import core, config_entries +from homeassistant import config_entries, core from homeassistant.helpers import config_entry_oauth2_flow # TODO the following two API examples are based on our suggested best practices diff --git a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py index 1112a404e6087b..2343e1d79f81f6 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py @@ -3,6 +3,7 @@ from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py index 7e61bcbfb1b913..50540594a3424b 100644 --- a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py @@ -1,5 +1,5 @@ """Test the NEW_NAME config flow.""" -from homeassistant import config_entries, setup, data_entry_flow +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.NEW_DOMAIN.const import ( DOMAIN, OAUTH2_AUTHORIZE, diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py index d5674f01b2de4a..3861ee8ebe991c 100644 --- a/script/scaffold/templates/device_action/integration/device_action.py +++ b/script/scaffold/templates/device_action/integration/device_action.py @@ -1,19 +1,21 @@ """Provides device automations for NEW_NAME.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - SERVICE_TURN_ON, + CONF_TYPE, SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import DOMAIN # TODO specify your supported action types. diff --git a/script/scaffold/templates/device_action/tests/test_device_action.py b/script/scaffold/templates/device_action/tests/test_device_action.py index b65c8257531cd4..3c7c7bb71a431f 100644 --- a/script/scaffold/templates/device_action/tests/test_device_action.py +++ b/script/scaffold/templates/device_action/tests/test_device_action.py @@ -2,17 +2,17 @@ import pytest from homeassistant.components.NEW_DOMAIN import DOMAIN -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index 4b7baf68a372d7..1414636474d414 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -1,21 +1,23 @@ """Provide the device automations for NEW_NAME.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, STATE_OFF, STATE_ON, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN # TODO specify your supported condition types. diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py index 1ae4df5f1b76b4..d58957030dc50f 100644 --- a/script/scaffold/templates/device_condition/tests/test_device_condition.py +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -2,18 +2,18 @@ import pytest from homeassistant.components.NEW_DOMAIN import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index e0741734d5f7ff..a4f918684dc4aa 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -1,21 +1,23 @@ """Provides device automations for NEW_NAME.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - STATE_ON, + CONF_PLATFORM, + CONF_TYPE, STATE_OFF, + STATE_ON, ) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType -from homeassistant.components.automation import state, AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN # TODO specify your supported trigger types. diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py index 99e1f8937afd96..0ea584f474d3c8 100644 --- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -2,18 +2,18 @@ import pytest from homeassistant.components.NEW_DOMAIN import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, assert_lists_same, + async_get_device_automations, async_mock_service, mock_device_registry, mock_registry, - async_get_device_automations, ) diff --git a/script/scaffold/templates/integration/integration/__init__.py b/script/scaffold/templates/integration/integration/__init__.py index c2ae59aaad4417..0ab65cb7da8b59 100644 --- a/script/scaffold/templates/integration/integration/__init__.py +++ b/script/scaffold/templates/integration/integration/__init__.py @@ -5,7 +5,6 @@ from .const import DOMAIN - CONFIG_SCHEMA = vol.Schema({vol.Optional(DOMAIN): {}}, extra=vol.ALLOW_EXTRA) diff --git a/script/scaffold/templates/reproduce_state/integration/reproduce_state.py b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py index 3449009818b6ab..871142a5b002d0 100644 --- a/script/scaffold/templates/reproduce_state/integration/reproduce_state.py +++ b/script/scaffold/templates/reproduce_state/integration/reproduce_state.py @@ -5,10 +5,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/script/translations_download_split.py b/script/translations_download_split.py index 375a9490e4e7fd..86944ec9f90e33 100755 --- a/script/translations_download_split.py +++ b/script/translations_download_split.py @@ -4,7 +4,7 @@ import json import os import re -from typing import Union, List, Dict +from typing import Dict, List, Union FILENAME_FORMAT = re.compile(r"strings\.(?P\w+)\.json") diff --git a/script/translations_upload_merge.py b/script/translations_upload_merge.py index c44727f690fee1..86de1f1842bfbc 100755 --- a/script/translations_upload_merge.py +++ b/script/translations_upload_merge.py @@ -5,7 +5,7 @@ import json import os import re -from typing import Union, List, Dict +from typing import Dict, List, Union FILENAME_FORMAT = re.compile(r"strings\.(?P\w+)\.json") diff --git a/script/version_bump.py b/script/version_bump.py index de6638df30ba8c..13dfe499f5e864 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """Helper script to bump the current version.""" import argparse +from datetime import datetime import re import subprocess -from datetime import datetime from packaging.version import Version From 29ec17d50df10aab61c67c0a52260321910cb3b1 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 16:30:16 +0100 Subject: [PATCH 2339/3953] use isort to sort imports for "setup.py" (#29792) --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 73cc893cbe2244..f9cb24d1e91b8e 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 """Home Assistant setup script.""" from datetime import datetime as dt -from setuptools import setup, find_packages + +from setuptools import find_packages, setup import homeassistant.const as hass_const From 67c56c860da42c531e93fa420a558023c22e8dce Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 16:42:10 +0100 Subject: [PATCH 2340/3953] Sort imports according to PEP8 for 'homeassistant' folder (#29789) Components are already done --- homeassistant/__main__.py | 4 +- homeassistant/auth/__init__.py | 8 ++-- homeassistant/auth/auth_store.py | 2 +- homeassistant/auth/mfa_modules/__init__.py | 2 +- .../auth/mfa_modules/insecure_example.py | 4 +- homeassistant/auth/mfa_modules/notify.py | 8 ++-- homeassistant/auth/mfa_modules/totp.py | 6 +-- homeassistant/auth/permissions/__init__.py | 5 +-- homeassistant/auth/permissions/entities.py | 5 +-- homeassistant/auth/permissions/merge.py | 4 +- .../auth/permissions/system_policies.py | 2 +- homeassistant/auth/permissions/util.py | 1 - homeassistant/auth/providers/__init__.py | 2 +- homeassistant/auth/providers/command_line.py | 6 +-- homeassistant/auth/providers/homeassistant.py | 7 +--- .../auth/providers/insecure_example.py | 5 +-- .../auth/providers/legacy_api_password.py | 4 +- .../auth/providers/trusted_networks.py | 7 ++-- homeassistant/bootstrap.py | 8 ++-- homeassistant/config.py | 42 +++++++++---------- homeassistant/config_entries.py | 13 +++--- homeassistant/core.py | 38 +++++++---------- homeassistant/data_entry_flow.py | 6 ++- homeassistant/exceptions.py | 3 +- homeassistant/helpers/__init__.py | 2 +- homeassistant/helpers/aiohttp_client.py | 11 +++-- homeassistant/helpers/area_registry.py | 7 ++-- homeassistant/helpers/check_config.py | 16 ++++--- homeassistant/helpers/condition.py | 10 ++--- homeassistant/helpers/config_entry_flow.py | 4 +- .../helpers/config_entry_oauth2_flow.py | 11 +++-- homeassistant/helpers/config_validation.py | 27 ++++++------ homeassistant/helpers/data_entry_flow.py | 3 +- homeassistant/helpers/device_registry.py | 5 +-- homeassistant/helpers/discovery.py | 6 +-- homeassistant/helpers/dispatcher.py | 2 +- homeassistant/helpers/entity.py | 18 ++++---- homeassistant/helpers/entity_component.py | 8 ++-- homeassistant/helpers/entity_platform.py | 5 +-- homeassistant/helpers/entity_registry.py | 1 - homeassistant/helpers/event.py | 11 +++-- homeassistant/helpers/intent.py | 5 +-- homeassistant/helpers/logging.py | 1 - homeassistant/helpers/network.py | 2 +- homeassistant/helpers/restore_state.py | 15 ++++--- homeassistant/helpers/script.py | 15 ++++--- homeassistant/helpers/service.py | 7 ++-- homeassistant/helpers/signal.py | 2 +- homeassistant/helpers/state.py | 7 ++-- homeassistant/helpers/storage.py | 7 ++-- homeassistant/helpers/sun.py | 5 ++- homeassistant/helpers/system_info.py | 1 + homeassistant/helpers/temperature.py | 2 +- homeassistant/helpers/template.py | 5 +-- homeassistant/helpers/translation.py | 3 +- homeassistant/helpers/typing.py | 2 +- homeassistant/loader.py | 10 ++--- homeassistant/requirements.py | 6 +-- homeassistant/scripts/__init__.py | 3 +- homeassistant/scripts/auth.py | 3 +- homeassistant/scripts/benchmark/__init__.py | 1 - homeassistant/scripts/check_config.py | 9 ++-- homeassistant/scripts/credstash.py | 1 - homeassistant/scripts/ensure_config.py | 3 +- homeassistant/scripts/macos/__init__.py | 1 - homeassistant/setup.py | 6 +-- homeassistant/util/__init__.py | 16 +++---- homeassistant/util/aiohttp.py | 2 +- homeassistant/util/async_.py | 12 +++--- homeassistant/util/color.py | 4 +- homeassistant/util/distance.py | 6 +-- homeassistant/util/dt.py | 2 +- homeassistant/util/json.py | 5 +-- homeassistant/util/location.py | 2 +- homeassistant/util/package.py | 5 +-- homeassistant/util/pressure.py | 6 +-- homeassistant/util/ruamel_yaml.py | 8 ++-- homeassistant/util/temperature.py | 2 +- homeassistant/util/unit_system.py | 36 ++++++++-------- homeassistant/util/volume.py | 9 ++-- homeassistant/util/yaml/__init__.py | 3 +- homeassistant/util/yaml/dumper.py | 2 +- homeassistant/util/yaml/loader.py | 16 +++---- 83 files changed, 279 insertions(+), 318 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index d2903370f4992f..2cecd1217f901c 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -5,10 +5,10 @@ import subprocess import sys import threading -from typing import List, Dict, Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, List from homeassistant import monkey_patch -from homeassistant.const import __version__, REQUIRED_PYTHON_VER, RESTART_EXIT_CODE +from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ if TYPE_CHECKING: from homeassistant import core diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 3f7dd570400adf..e4437bea840d37 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -1,21 +1,21 @@ """Provide an authentication layer for Home Assistant.""" import asyncio -import logging from collections import OrderedDict from datetime import timedelta +import logging from typing import Any, Dict, List, Optional, Tuple, cast import jwt from homeassistant import data_entry_flow from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util from . import auth_store, models from .const import GROUP_ID_ADMIN -from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule -from .providers import auth_provider_from_config, AuthProvider, LoginFlow +from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config +from .providers import AuthProvider, LoginFlow, auth_provider_from_config EVENT_USER_ADDED = "user_added" EVENT_USER_REMOVED = "user_removed" diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 4c64730edda646..57ec9ee63dcc41 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -11,7 +11,7 @@ from homeassistant.util import dt as dt_util from . import models -from .const import GROUP_ID_ADMIN, GROUP_ID_USER, GROUP_ID_READ_ONLY +from .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY, GROUP_ID_USER from .permissions import PermissionLookup, system_policies from .permissions.types import PolicyType diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 9020b0b321ecc8..fd9e61b9d17117 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -7,7 +7,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant import requirements, data_entry_flow +from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant/auth/mfa_modules/insecure_example.py index a3f0d58c6b30a9..45cc07ae5810c4 100644 --- a/homeassistant/auth/mfa_modules/insecure_example.py +++ b/homeassistant/auth/mfa_modules/insecure_example.py @@ -7,9 +7,9 @@ from homeassistant.core import HomeAssistant from . import ( - MultiFactorAuthModule, - MULTI_FACTOR_AUTH_MODULES, MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, SetupFlow, ) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index b14f5fedc22f55..46cc634bcaeecc 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -3,9 +3,9 @@ Sending HOTP through notify service """ import asyncio -import logging from collections import OrderedDict -from typing import Any, Dict, Optional, List +import logging +from typing import Any, Dict, List, Optional import attr import voluptuous as vol @@ -16,9 +16,9 @@ from homeassistant.helpers import config_validation as cv from . import ( - MultiFactorAuthModule, - MULTI_FACTOR_AUTH_MODULES, MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, SetupFlow, ) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 9b0f3910e9291f..6abddd2123f09a 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -1,7 +1,7 @@ """Time-based One Time Password auth module.""" import asyncio -import logging from io import BytesIO +import logging from typing import Any, Dict, Optional, Tuple import voluptuous as vol @@ -10,9 +10,9 @@ from homeassistant.core import HomeAssistant from . import ( - MultiFactorAuthModule, - MULTI_FACTOR_AUTH_MODULES, MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, SetupFlow, ) diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 36cf9c4f420c83..92d02c75b91be0 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -5,13 +5,12 @@ import voluptuous as vol from .const import CAT_ENTITIES -from .models import PermissionLookup -from .types import PolicyType from .entities import ENTITY_POLICY_SCHEMA, compile_entities from .merge import merge_policies # noqa: F401 +from .models import PermissionLookup +from .types import PolicyType from .util import test_all - POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA}) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/auth/permissions/entities.py b/homeassistant/auth/permissions/entities.py index add9913abf3370..be30c7bf69aef5 100644 --- a/homeassistant/auth/permissions/entities.py +++ b/homeassistant/auth/permissions/entities.py @@ -4,11 +4,10 @@ import voluptuous as vol -from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT +from .const import POLICY_CONTROL, POLICY_EDIT, POLICY_READ, SUBCAT_ALL from .models import PermissionLookup from .types import CategoryType, SubCategoryDict, ValueType - -from .util import SubCatLookupType, lookup_all, compile_policy +from .util import SubCatLookupType, compile_policy, lookup_all SINGLE_ENTITY_SCHEMA = vol.Any( True, diff --git a/homeassistant/auth/permissions/merge.py b/homeassistant/auth/permissions/merge.py index 3cf02e0577134f..fad98b3f22a3f7 100644 --- a/homeassistant/auth/permissions/merge.py +++ b/homeassistant/auth/permissions/merge.py @@ -1,7 +1,7 @@ """Merging of policies.""" -from typing import cast, Dict, List, Set +from typing import Dict, List, Set, cast -from .types import PolicyType, CategoryType +from .types import CategoryType, PolicyType def merge_policies(policies: List[PolicyType]) -> PolicyType: diff --git a/homeassistant/auth/permissions/system_policies.py b/homeassistant/auth/permissions/system_policies.py index b40400304cc504..b45984653fb37e 100644 --- a/homeassistant/auth/permissions/system_policies.py +++ b/homeassistant/auth/permissions/system_policies.py @@ -1,5 +1,5 @@ """System policies.""" -from .const import CAT_ENTITIES, SUBCAT_ALL, POLICY_READ +from .const import CAT_ENTITIES, POLICY_READ, SUBCAT_ALL ADMIN_POLICY = {CAT_ENTITIES: True} diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 4d38e0a639c07f..11bbd878eb23d1 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -1,6 +1,5 @@ """Helpers to deal with permissions.""" from functools import wraps - from typing import Callable, Dict, List, Optional, cast from .const import SUBCAT_ALL diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index cbce31529022ca..bb0fc55b5c4dd0 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -8,8 +8,8 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements -from homeassistant.core import callback, HomeAssistant from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.util import dt as dt_util from homeassistant.util.decorator import Registry diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 58a2cac1fc532c..203bc191193d6b 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -1,20 +1,18 @@ """Auth provider that validates credentials via an external command.""" -from typing import Any, Dict, Optional, cast - import asyncio.subprocess import collections import logging import os +from typing import Any, Dict, Optional, cast import voluptuous as vol from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta - CONF_COMMAND = "command" CONF_ARGS = "args" CONF_META = "meta" diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 265a24a4b288c3..9ddbf4189f79b2 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -3,21 +3,18 @@ import base64 from collections import OrderedDict import logging - from typing import Any, Dict, List, Optional, Set, cast import bcrypt import voluptuous as vol from homeassistant.const import CONF_ID -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow - +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta - STORAGE_VERSION = 1 STORAGE_KEY = "auth_provider.homeassistant" diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index 37859f5ed0ec07..70014a236cdae2 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -5,13 +5,12 @@ import voluptuous as vol -from homeassistant.exceptions import HomeAssistantError from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta - USER_SCHEMA = vol.Schema( { vol.Required("username"): str, diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 018886388df8ab..15ba1dfc14c5c3 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -12,9 +12,9 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from .. import AuthManager -from ..models import Credentials, UserMeta, User +from ..models import Credentials, User, UserMeta AUTH_PROVIDER_TYPE = "legacy_api_password" CONF_API_PASSWORD = "api_password" diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index f71be436acf919..bc995368fec8f8 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -3,15 +3,16 @@ It shows list of users if access from trusted network. Abort login flow if not access from trusted network. """ -from ipaddress import ip_network, IPv4Address, IPv6Address, IPv4Network, IPv6Network +from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network from typing import Any, Dict, List, Optional, Union, cast import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +import homeassistant.helpers.config_validation as cv + +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta IPAddress = Union[IPv4Address, IPv6Address] diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 312c739cd72034..79de0b2545398f 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,22 +1,22 @@ """Provide methods to bootstrap a Home Assistant instance.""" import asyncio +from collections import OrderedDict import logging import logging.handlers import os import sys from time import time -from collections import OrderedDict -from typing import Any, Optional, Dict, Set +from typing import Any, Dict, Optional, Set import voluptuous as vol -from homeassistant import core, config as conf_util, config_entries, loader +from homeassistant import config as conf_util, config_entries, core, loader from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util.logging import AsyncHandler from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.yaml import clear_secret_cache -from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/config.py b/homeassistant/config.py index 71628be8006e78..353a717778b008 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,59 +1,59 @@ """Module to help with parsing and generating configuration files.""" -from collections import OrderedDict - # pylint: disable=no-name-in-module +from collections import OrderedDict from distutils.version import LooseVersion # pylint: disable=import-error import logging import os import re import shutil -from typing import Any, Tuple, Optional, Dict, Union, Callable, Sequence, Set from types import ModuleType +from typing import Any, Callable, Dict, Optional, Sequence, Set, Tuple, Union + import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant import auth from homeassistant.auth import ( - providers as auth_providers, mfa_modules as auth_mfa_modules, + providers as auth_providers, ) from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, - ATTR_ASSUMED_STATE, + CONF_AUTH_MFA_MODULES, + CONF_AUTH_PROVIDERS, + CONF_CUSTOMIZE, + CONF_CUSTOMIZE_DOMAIN, + CONF_CUSTOMIZE_GLOB, + CONF_ELEVATION, + CONF_ID, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, - CONF_UNIT_SYSTEM, + CONF_TEMPERATURE_UNIT, CONF_TIME_ZONE, - CONF_ELEVATION, + CONF_TYPE, + CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, - CONF_TEMPERATURE_UNIT, + CONF_WHITELIST_EXTERNAL_DIRS, TEMP_CELSIUS, __version__, - CONF_CUSTOMIZE, - CONF_CUSTOMIZE_DOMAIN, - CONF_CUSTOMIZE_GLOB, - CONF_WHITELIST_EXTERNAL_DIRS, - CONF_AUTH_PROVIDERS, - CONF_AUTH_MFA_MODULES, - CONF_TYPE, - CONF_ID, ) from homeassistant.core import DOMAIN as CONF_CORE, SOURCE_YAML, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform, extract_domain_configs +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_values import EntityValues from homeassistant.loader import Integration, IntegrationNotFound from homeassistant.requirements import ( - async_get_integration_with_requirements, RequirementsNotFound, + async_get_integration_with_requirements, ) -from homeassistant.util.yaml import load_yaml, SECRET_YAML from homeassistant.util.package import is_docker_env -import homeassistant.helpers.config_validation as cv from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from homeassistant.helpers.entity_values import EntityValues -from homeassistant.helpers import config_per_platform, extract_domain_configs +from homeassistant.util.yaml import SECRET_YAML, load_yaml _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index ae3aeebb1ee85e..07a287c387cb96 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1,21 +1,20 @@ """Manage config entries in Home Assistant.""" import asyncio -import logging import functools -import uuid +import logging from typing import Any, Callable, Dict, List, Optional, Set, cast +import uuid 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 +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.event import Event - +from homeassistant.setup import async_process_deps_reqs, async_setup_component +from homeassistant.util.decorator import Registry _LOGGER = logging.getLogger(__name__) _UNDEF: dict = {} diff --git a/homeassistant/core.py b/homeassistant/core.py index 2859e0fe1572a1..0002019fdfaaf8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -14,65 +14,59 @@ import pathlib import threading from time import monotonic -import uuid - from types import MappingProxyType from typing import ( - Optional, + TYPE_CHECKING, Any, + Awaitable, Callable, - List, - TypeVar, - Dict, Coroutine, - Set, - TYPE_CHECKING, - Awaitable, + Dict, + List, Mapping, + Optional, + Set, + TypeVar, ) +import uuid from async_timeout import timeout import attr import voluptuous as vol +from homeassistant import loader, util from homeassistant.const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, + ATTR_SECONDS, ATTR_SERVICE, ATTR_SERVICE_DATA, - ATTR_SECONDS, CONF_UNIT_SYSTEM_IMPERIAL, EVENT_CALL_SERVICE, EVENT_CORE_CONFIG_UPDATE, + EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_CLOSE, - EVENT_SERVICE_REMOVED, EVENT_SERVICE_REGISTERED, + EVENT_SERVICE_REMOVED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, MATCH_ALL, __version__, ) -from homeassistant import loader from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError, InvalidStateError, - Unauthorized, ServiceNotFound, + Unauthorized, ) -from homeassistant.util.async_ import run_callback_threadsafe, fire_coroutine_threadsafe -from homeassistant import util -import homeassistant.util.dt as dt_util from homeassistant.util import location, slugify -from homeassistant.util.unit_system import ( - UnitSystem, - IMPERIAL_SYSTEM, - METRIC_SYSTEM, -) +from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe +import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem # Typing imports that create a circular dependency if TYPE_CHECKING: diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 58d8e4ea131519..e7432cd52f75d6 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,9 +1,11 @@ """Classes to help gather user submissions.""" import logging -from typing import Dict, Any, Callable, List, Optional +from typing import Any, Callable, Dict, List, Optional import uuid + import voluptuous as vol -from .core import callback, HomeAssistant + +from .core import HomeAssistant, callback from .exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 6147e26c80909e..745d80d386b68e 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -1,5 +1,6 @@ """The exceptions used by Home Assistant.""" -from typing import Optional, Tuple, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Tuple + import jinja2 if TYPE_CHECKING: diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index fe60ffc4b33f2c..125d90e1162b96 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -1,6 +1,6 @@ """Helper methods for components within Home Assistant.""" import re -from typing import Any, Iterable, Tuple, Sequence, Dict +from typing import Any, Dict, Iterable, Sequence, Tuple from homeassistant.const import CONF_PLATFORM diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 7f1579cd2c67aa..eee891b7f8894c 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -1,18 +1,17 @@ """Helper for aiohttp webclient stuff.""" import asyncio -import sys from ssl import SSLContext -from typing import Any, Awaitable, Optional, cast -from typing import Union +import sys +from typing import Any, Awaitable, Optional, Union, cast import aiohttp -from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE from aiohttp import web -from aiohttp.web_exceptions import HTTPGatewayTimeout, HTTPBadGateway +from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT +from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout -from homeassistant.core import callback, Event from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ +from homeassistant.core import Event, callback from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index e75b195d386519..58abecffb8b0a8 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -1,10 +1,9 @@ """Provide a way to connect devices to one physical location.""" -import logging -import uuid from asyncio import Event from collections import OrderedDict -from typing import MutableMapping -from typing import Iterable, Optional, cast +import logging +from typing import Iterable, MutableMapping, Optional, cast +import uuid import attr diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 4052a94b9de9b7..81e654247b72aa 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -6,26 +6,24 @@ import voluptuous as vol from homeassistant import loader -from homeassistant.core import HomeAssistant from homeassistant.config import ( CONF_CORE, - CORE_CONFIG_SCHEMA, CONF_PACKAGES, - merge_packages_config, + CORE_CONFIG_SCHEMA, _format_config_error, + config_per_platform, + extract_domain_configs, find_config_file, load_yaml_config_file, - extract_domain_configs, - config_per_platform, + merge_packages_config, ) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.requirements import ( - async_get_integration_with_requirements, RequirementsNotFound, + async_get_integration_with_requirements, ) - import homeassistant.util.yaml.loader as yaml_loader -from homeassistant.exceptions import HomeAssistantError - # mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index df82ba6076fd83..c02c49ce311208 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -6,9 +6,6 @@ import sys from typing import Callable, Container, Optional, Union, cast -from homeassistant.helpers.template import Template -from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from homeassistant.core import HomeAssistant, State from homeassistant.components import zone as zone_cmp from homeassistant.components.device_automation import ( async_get_device_automation_platform, @@ -34,11 +31,14 @@ SUN_EVENT_SUNSET, WEEKDAYS, ) -from homeassistant.exceptions import TemplateError, HomeAssistantError +from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError, TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.sun import get_astral_event_date -import homeassistant.util.dt as dt_util +from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.util.async_ import run_callback_threadsafe +import homeassistant.util.dt as dt_util FROM_CONFIG_FORMAT = "{}_from_config" ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config" diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 41f90effb89f3f..323c6907411530 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -1,6 +1,8 @@ """Helpers for data entry flows for config entries.""" -from typing import Callable, Awaitable, Union +from typing import Awaitable, Callable, Union + from homeassistant import config_entries + from .typing import HomeAssistantType # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index dc3d3c91f27a76..4ea82f5f04749b 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -5,26 +5,25 @@ - OAuth2 implementation that works with local provided client ID/secret """ +from abc import ABC, ABCMeta, abstractmethod import asyncio -from abc import ABCMeta, ABC, abstractmethod import logging -from typing import Optional, Any, Dict, cast, Awaitable, Callable import time +from typing import Any, Awaitable, Callable, Dict, Optional, cast +from aiohttp import client, web import async_timeout -from aiohttp import web, client import jwt import voluptuous as vol from yarl import URL -from homeassistant.auth.util import generate_secret -from homeassistant.core import HomeAssistant, callback from homeassistant import config_entries +from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView +from homeassistant.core import HomeAssistant, callback from .aiohttp_client import async_get_clientsession - DATA_JWT_SECRET = "oauth2_jwt_secret" DATA_VIEW_REGISTERED = "oauth2_view_reg" DATA_IMPLEMENTATIONS = "oauth2_impl" diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 948fb017d9d1f2..0f5b4e13dc21ea 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,17 +1,17 @@ """Helpers for config validation using voluptuous.""" -import inspect -import logging -import os -import re from datetime import ( - timedelta, + date as date_sys, datetime as datetime_sys, time as time_sys, - date as date_sys, + timedelta, ) -from socket import _GLOBAL_DEFAULT_TIMEOUT +import inspect +import logging from numbers import Number -from typing import Any, Union, TypeVar, Callable, List, Dict, Optional +import os +import re +from socket import _GLOBAL_DEFAULT_TIMEOUT +from typing import Any, Callable, Dict, List, Optional, TypeVar, Union from urllib.parse import urlparse from uuid import UUID @@ -19,8 +19,9 @@ import voluptuous as vol import voluptuous_serialize -import homeassistant.util.dt as dt_util from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_ENTITY_ID, CONF_ABOVE, CONF_ALIAS, CONF_BELOW, @@ -33,10 +34,10 @@ CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_STATE, + CONF_TIMEOUT, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, CONF_VALUE_TEMPLATE, - CONF_TIMEOUT, ENTITY_MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, @@ -44,14 +45,12 @@ TEMP_FAHRENHEIT, WEEKDAYS, __version__, - ATTR_AREA_ID, - ATTR_ENTITY_ID, ) -from homeassistant.core import valid_entity_id, split_entity_id +from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers.logging import KeywordStyleAdapter from homeassistant.util import slugify as util_slugify - +import homeassistant.util.dt as dt_util # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 1d47105247486f..ac5fb6086756a4 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -2,11 +2,10 @@ import voluptuous as vol -from homeassistant import data_entry_flow, config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator - # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 456678edac779a..2ff444da89f384 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1,9 +1,9 @@ """Provide a way to connect entities belonging to one device.""" -import logging -import uuid from asyncio import Event from collections import OrderedDict +import logging from typing import List, Optional, cast +import uuid import attr @@ -12,7 +12,6 @@ from .typing import HomeAssistantType - # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index bc1094613bb749..8e4def77440a8b 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -5,14 +5,12 @@ - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ -from homeassistant import setup, core -from homeassistant.loader import bind_hass +from homeassistant import core, setup from homeassistant.const import ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import DEPENDENCY_BLACKLIST +from homeassistant.loader import DEPENDENCY_BLACKLIST, bind_hass from homeassistant.util.async_ import run_callback_threadsafe - # mypy: allow-untyped-defs, no-check-untyped-defs EVENT_LOAD_PLATFORM = "load_platform.{}" diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 81582f4fa54dd7..a4e624f119fe16 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -6,8 +6,8 @@ from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception -from .typing import HomeAssistantType +from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) DATA_DISPATCHER = "dispatcher" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index ed6560614016f8..531444b9d1eb65 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -2,16 +2,20 @@ from abc import ABC import asyncio from datetime import datetime, timedelta -import logging import functools as ft +import logging from timeit import default_timer as timer from typing import Any, Dict, Iterable, List, Optional, Union +from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( ATTR_ASSUMED_STATE, + ATTR_DEVICE_CLASS, + ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON, + ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, STATE_OFF, @@ -20,22 +24,16 @@ STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_ENTITY_PICTURE, - ATTR_SUPPORTED_FEATURES, - ATTR_DEVICE_CLASS, ) +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback +from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, ) -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Context -from homeassistant.config import DATA_CUSTOMIZE -from homeassistant.exceptions import NoEntitySpecifiedError -from homeassistant.util import ensure_unique_string, slugify +from homeassistant.util import dt as dt_util, ensure_unique_string, slugify from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.util import dt as dt_util - # mypy: allow-untyped-defs, no-check-untyped-defs, no-warn-return-any diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 63d1b21fc9aa81..becd96bf5f3807 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -5,11 +5,10 @@ import logging from homeassistant import config as conf_util -from homeassistant.setup import async_prepare_setup_platform from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, + CONF_SCAN_INTERVAL, ENTITY_MATCH_ALL, ) from homeassistant.core import callback @@ -17,10 +16,11 @@ from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.service import async_extract_entity_ids -from homeassistant.loader import bind_hass, async_get_integration +from homeassistant.loader import async_get_integration, bind_hass +from homeassistant.setup import async_prepare_setup_platform from homeassistant.util import slugify -from .entity_platform import EntityPlatform +from .entity_platform import EntityPlatform # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 376a6e23e9a71c..133d1a5841f5b9 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -4,13 +4,12 @@ from typing import Optional from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import callback, valid_entity_id, split_entity_id +from homeassistant.core import callback, split_entity_id, valid_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.util.async_ import run_callback_threadsafe from .entity_registry import DISABLED_INTEGRATION -from .event import async_track_time_interval, async_call_later - +from .event import async_call_later, async_track_time_interval # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 08f29a9fb3ecda..a5bd62d973c8ef 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -23,7 +23,6 @@ from .typing import HomeAssistantType - # mypy: allow-untyped-defs, no-check-untyped-defs PATH_REGISTRY = "entity_registry.yaml" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 715344a3969081..b3c8af6f50cdd2 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -5,23 +5,22 @@ import attr -from homeassistant.loader import bind_hass -from homeassistant.helpers.sun import get_astral_event_next -from homeassistant.helpers.template import Template -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event, State from homeassistant.const import ( ATTR_NOW, + EVENT_CORE_CONFIG_UPDATE, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, - EVENT_CORE_CONFIG_UPDATE, ) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback +from homeassistant.helpers.sun import get_astral_event_next +from homeassistant.helpers.template import Template +from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe - # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 12b346603f05b1..181d1baebc0b09 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -5,13 +5,12 @@ import voluptuous as vol -from homeassistant.const import ATTR_SUPPORTED_FEATURES -from homeassistant.core import callback, State, T, Context +from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.core import Context, State, T, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass -from homeassistant.const import ATTR_ENTITY_ID _LOGGER = logging.getLogger(__name__) _SlotsType = Dict[str, Any] diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index dd9e383380191c..7b2507d9e05e61 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -2,7 +2,6 @@ import inspect import logging - # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 671e7f1fa56e26..a446b5750776c6 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -1,6 +1,6 @@ """Network helpers.""" -from typing import Optional, cast from ipaddress import ip_address +from typing import Optional, cast import yarl diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 5d47f34b0021a1..4a67193734e2d0 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -1,24 +1,23 @@ """Support for restoring entity states on startup.""" import asyncio +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime -from typing import Any, Dict, List, Set, Optional +from typing import Any, Dict, List, Optional, Set +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( + CoreState, HomeAssistant, - callback, State, - CoreState, + callback, valid_entity_id, ) -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -import homeassistant.util.dt as dt_util +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import Store - +import homeassistant.util.dt as dt_util # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 21dd0b71487f84..8e0faa2ce4d1a7 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,16 +1,16 @@ """Helpers to execute scripts.""" import asyncio -import logging from contextlib import suppress from datetime import datetime from itertools import islice -from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple, Any +import logging +from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple import voluptuous as vol +from homeassistant import exceptions import homeassistant.components.device_automation as device_automation import homeassistant.components.scene as scene -from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, @@ -19,21 +19,20 @@ CONF_TIMEOUT, SERVICE_TURN_ON, ) -from homeassistant import exceptions +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.helpers import ( - service, condition, - template as template, config_validation as cv, + service, + template as template, ) from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_template, ) from homeassistant.helpers.typing import ConfigType -import homeassistant.util.dt as date_util from homeassistant.util.async_ import run_callback_threadsafe - +import homeassistant.util.dt as date_util # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 45393dc04860ce..5381f7659934f4 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ATTR_AREA_ID +from homeassistant.const import ATTR_AREA_ID, ATTR_ENTITY_ID, ENTITY_MATCH_ALL import homeassistant.core as ha from homeassistant.exceptions import ( HomeAssistantError, @@ -16,12 +16,11 @@ UnknownUser, ) from homeassistant.helpers import template, typing +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType 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.helpers.typing import HomeAssistantType - # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/helpers/signal.py b/homeassistant/helpers/signal.py index 12792918742cf3..53802a2a1196a3 100644 --- a/homeassistant/helpers/signal.py +++ b/homeassistant/helpers/signal.py @@ -4,8 +4,8 @@ import sys from types import FrameType -from homeassistant.core import callback, HomeAssistant from homeassistant.const import RESTART_EXIT_CODE +from homeassistant.core import HomeAssistant, callback from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index abc97bf1f8ae4b..60e6acc8797ff4 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,13 +1,11 @@ """Helpers that help with state related things.""" import asyncio +from collections import defaultdict import datetime as dt import logging -from collections import defaultdict from types import ModuleType, TracebackType from typing import Dict, Iterable, List, Optional, Type, Union -from homeassistant.loader import bind_hass, async_get_integration, IntegrationNotFound -import homeassistant.util.dt as dt_util from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON from homeassistant.const import ( STATE_CLOSED, @@ -21,6 +19,9 @@ STATE_UNLOCKED, ) from homeassistant.core import Context, State +from homeassistant.loader import IntegrationNotFound, async_get_integration, bind_hass +import homeassistant.util.dt as dt_util + from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index bd18eebfb25c53..aed6da37518f2c 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -3,14 +3,13 @@ from json import JSONEncoder import logging import os -from typing import Dict, List, Optional, Callable, Union, Any, Type +from typing import Any, Callable, Dict, List, Optional, Type, Union from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.event import async_call_later from homeassistant.loader import bind_hass from homeassistant.util import json as json_util -from homeassistant.helpers.event import async_call_later - # mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any # mypy: no-check-untyped-defs diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index 9fa6e074bdd945..45ff06f16dea47 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -1,11 +1,12 @@ """Helpers for sun events.""" import datetime -from typing import Optional, Union, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Union from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET from homeassistant.core import callback -from homeassistant.util import dt as dt_util from homeassistant.loader import bind_hass +from homeassistant.util import dt as dt_util + from .typing import HomeAssistantType if TYPE_CHECKING: diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index b552a634d4b768..7d1d6f2b3e7e17 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -6,6 +6,7 @@ from homeassistant.const import __version__ as current_version from homeassistant.loader import bind_hass from homeassistant.util.package import is_virtual_env + from .typing import HomeAssistantType diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py index 30b428a9e17991..e0846d6f893cac 100644 --- a/homeassistant/helpers/temperature.py +++ b/homeassistant/helpers/temperature.py @@ -2,9 +2,9 @@ from numbers import Number from typing import Optional +from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS from homeassistant.core import HomeAssistant from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS def display_temp( diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 7dcf08ebf92145..4bdee7500348ed 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,12 +1,12 @@ """Template helper methods for rendering strings with Home Assistant data.""" import base64 +from datetime import datetime +from functools import wraps import json import logging import math import random import re -from datetime import datetime -from functools import wraps from typing import Any, Dict, Iterable, List, Optional, Union import jinja2 @@ -30,7 +30,6 @@ from homeassistant.util import convert, dt as dt_util, location as loc_util from homeassistant.util.async_ import run_callback_threadsafe - # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index b9fd24c95e0908..fe254ab8907075 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -3,11 +3,12 @@ from typing import Any, Dict, Iterable, Optional from homeassistant.loader import ( + async_get_config_flows, async_get_integration, bind_hass, - async_get_config_flows, ) from homeassistant.util.json import load_json + from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index f084c5fddbe6a9..6e31301066c6ca 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,5 +1,5 @@ """Typing Helpers for Home Assistant.""" -from typing import Dict, Any, Tuple, Optional +from typing import Any, Dict, Optional, Tuple import homeassistant.core diff --git a/homeassistant/loader.py b/homeassistant/loader.py index af76b073cd3973..0e1ee8ae7567e9 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -13,14 +13,14 @@ import sys from types import ModuleType from typing import ( - Optional, - Set, TYPE_CHECKING, - Callable, Any, - TypeVar, - List, + Callable, Dict, + List, + Optional, + Set, + TypeVar, Union, cast, ) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index eb8aeeecfae896..bd52253cdb1365 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -1,14 +1,14 @@ """Module to handle installing requirements.""" import asyncio -from pathlib import Path import logging import os +from pathlib import Path from typing import Any, Dict, List, Optional, Set +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.loader import Integration, async_get_integration 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" diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py index ecac61895c53de..b47450ab9ddfb7 100644 --- a/homeassistant/scripts/__init__.py +++ b/homeassistant/scripts/__init__.py @@ -10,8 +10,7 @@ from homeassistant.bootstrap import async_mount_local_lib_path from homeassistant.config import get_default_config_dir from homeassistant.requirements import pip_kwargs -from homeassistant.util.package import install_package, is_virtual_env, is_installed - +from homeassistant.util.package import install_package, is_installed, is_virtual_env # mypy: allow-untyped-defs, no-warn-return-any diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py index e0a8b5cf117a0a..66baf555306fec 100644 --- a/homeassistant/scripts/auth.py +++ b/homeassistant/scripts/auth.py @@ -6,9 +6,8 @@ from homeassistant.auth import auth_manager_from_config from homeassistant.auth.providers import homeassistant as hass_auth -from homeassistant.core import HomeAssistant from homeassistant.config import get_default_config_dir - +from homeassistant.core import HomeAssistant # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 480f6fc9fdeefa..58125bc4829a68 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -11,7 +11,6 @@ from homeassistant.const import ATTR_NOW, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED from homeassistant.util import dt as dt_util - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 3ac023115a102a..46724fc7c102ce 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -1,19 +1,18 @@ """Script to check the configuration file.""" import argparse -import logging -import os from collections import OrderedDict from glob import glob -from typing import Dict, List, Sequence, Any, Tuple, Callable +import logging +import os +from typing import Any, Callable, Dict, List, Sequence, Tuple from unittest.mock import patch from homeassistant import bootstrap, core from homeassistant.config import get_default_config_dir +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.check_config import async_check_ha_config_file import homeassistant.util.yaml.loader as yaml_loader -from homeassistant.exceptions import HomeAssistantError - # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/scripts/credstash.py b/homeassistant/scripts/credstash.py index e77ae326cd7e92..f90ab5f793e26d 100644 --- a/homeassistant/scripts/credstash.py +++ b/homeassistant/scripts/credstash.py @@ -4,7 +4,6 @@ from homeassistant.util.yaml import _SECRET_NAMESPACE - # mypy: allow-untyped-defs REQUIREMENTS = ["credstash==1.15.0"] diff --git a/homeassistant/scripts/ensure_config.py b/homeassistant/scripts/ensure_config.py index c5cf69283e6f85..cb2c408804972e 100644 --- a/homeassistant/scripts/ensure_config.py +++ b/homeassistant/scripts/ensure_config.py @@ -2,9 +2,8 @@ import argparse import os -from homeassistant.core import HomeAssistant import homeassistant.config as config_util - +from homeassistant.core import HomeAssistant # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/scripts/macos/__init__.py b/homeassistant/scripts/macos/__init__.py index ceb3609dbdb1b7..9d9c5cd824842f 100644 --- a/homeassistant/scripts/macos/__init__.py +++ b/homeassistant/scripts/macos/__init__.py @@ -2,7 +2,6 @@ import os import time - # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 42296a4935d657..01b13629e7b60d 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -2,16 +2,14 @@ import asyncio import logging.handlers from timeit import default_timer as timer - from types import ModuleType -from typing import Awaitable, Callable, Optional, Dict, List +from typing import Awaitable, Callable, Dict, List, Optional -from homeassistant import requirements, core, loader, config as conf_util +from homeassistant import config as conf_util, core, loader, requirements from homeassistant.config import async_notify_setup_error from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError - _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = "component" diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 408d1e370d475b..f39fa5f1e5593b 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -1,23 +1,23 @@ """Helper methods for various modules.""" import asyncio from datetime import datetime, timedelta -import threading -import re import enum -import socket +from functools import wraps import random +import re +import socket import string -from functools import wraps +import threading from types import MappingProxyType from typing import ( Any, - Optional, - TypeVar, Callable, + Coroutine, + Iterable, KeysView, + Optional, + TypeVar, Union, - Iterable, - Coroutine, ) import slugify as unicode_slug diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index 1e36d2d4875615..69911986f57fdf 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -1,7 +1,7 @@ """Utilities to help with aiohttp.""" import json -from urllib.parse import parse_qsl from typing import Any, Dict, Optional +from urllib.parse import parse_qsl from multidict import CIMultiDict, MultiDict diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 64bedfe2501676..b1d3a7dd8e7cd1 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -1,13 +1,11 @@ """Asyncio backports for Python 3.6 compatibility.""" +import asyncio +from asyncio import coroutines, ensure_future +from asyncio.events import AbstractEventLoop import concurrent.futures -import threading import logging -from asyncio import coroutines -from asyncio.events import AbstractEventLoop - -import asyncio -from asyncio import ensure_future -from typing import Any, Coroutine, Callable, TypeVar, Awaitable +import threading +from typing import Any, Awaitable, Callable, Coroutine, TypeVar _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 7361b711dd2c9d..b56ecbbaa89175 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -1,8 +1,8 @@ """Color util methods.""" -import math import colorsys +import math +from typing import List, Optional, Tuple -from typing import Tuple, List, Optional import attr # Official CSS3 colors from w3.org: diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index b9cce45cb5bf4c..4fdc40bde2ffd3 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -4,12 +4,12 @@ from numbers import Number from homeassistant.const import ( - LENGTH_KILOMETERS, - LENGTH_MILES, + LENGTH, LENGTH_FEET, + LENGTH_KILOMETERS, LENGTH_METERS, + LENGTH_MILES, UNIT_NOT_RECOGNIZED_TEMPLATE, - LENGTH, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 1abb429439876c..791b36a4236df4 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -1,7 +1,7 @@ """Helper methods to handle the time in Home Assistant.""" import datetime as dt import re -from typing import Any, Union, Optional, Tuple, List, cast, Dict +from typing import Any, Dict, List, Optional, Tuple, Union, cast import pytz import pytz.exceptions as pytzexceptions diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 5b2ee316376967..e975c87867210a 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -1,10 +1,9 @@ """JSON utility functions.""" -import logging -from typing import Union, List, Dict, Optional, Type - import json +import logging import os import tempfile +from typing import Dict, List, Optional, Type, Union from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index b572b3025a0c55..a617eba50f93af 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -6,7 +6,7 @@ import asyncio import collections import math -from typing import Any, Optional, Tuple, Dict +from typing import Any, Dict, Optional, Tuple import aiohttp diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 58a3db31bd385a..24cf83092281c1 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -2,15 +2,14 @@ import asyncio import logging import os +from pathlib import Path from subprocess import PIPE, Popen import sys from typing import Optional from urllib.parse import urlparse -from pathlib import Path +from importlib_metadata import PackageNotFoundError, version import pkg_resources -from importlib_metadata import version, PackageNotFoundError - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index e394076800c542..df791fd02353bc 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -4,13 +4,13 @@ from numbers import Number from homeassistant.const import ( - PRESSURE_PA, + PRESSURE, PRESSURE_HPA, - PRESSURE_MBAR, PRESSURE_INHG, + PRESSURE_MBAR, + PRESSURE_PA, PRESSURE_PSI, UNIT_NOT_RECOGNIZED_TEMPLATE, - PRESSURE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index b7e8927888c734..e5ffbb94afe0fd 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -1,18 +1,18 @@ """ruamel.yaml utility functions.""" +from collections import OrderedDict import logging import os from os import O_CREAT, O_TRUNC, O_WRONLY, stat_result -from collections import OrderedDict -from typing import Union, List, Dict, Optional +from typing import Dict, List, Optional, Union import ruamel.yaml from ruamel.yaml import YAML +from ruamel.yaml.compat import StringIO from ruamel.yaml.constructor import SafeConstructor from ruamel.yaml.error import YAMLError -from ruamel.yaml.compat import StringIO -from homeassistant.util.yaml import secret_yaml from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.yaml import secret_yaml _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 909cdac60d9685..0b3edc6ef57b1d 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -2,8 +2,8 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, - UNIT_NOT_RECOGNIZED_TEMPLATE, TEMPERATURE, + UNIT_NOT_RECOGNIZED_TEMPLATE, ) diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 23ac8f05025617..a79c022be4538a 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -1,35 +1,37 @@ """Unit system helper class and methods.""" import logging -from typing import Optional from numbers import Number +from typing import Optional from homeassistant.const import ( - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - LENGTH_MILES, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, + LENGTH, LENGTH_KILOMETERS, - PRESSURE_PA, - PRESSURE_PSI, - VOLUME_LITERS, - VOLUME_GALLONS, + LENGTH_MILES, + MASS, MASS_GRAMS, MASS_KILOGRAMS, MASS_OUNCES, MASS_POUNDS, - CONF_UNIT_SYSTEM_METRIC, - CONF_UNIT_SYSTEM_IMPERIAL, - LENGTH, - MASS, PRESSURE, - VOLUME, + PRESSURE_PA, + PRESSURE_PSI, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE, + VOLUME, + VOLUME_GALLONS, + VOLUME_LITERS, +) +from homeassistant.util import ( + distance as distance_util, + pressure as pressure_util, + temperature as temperature_util, + volume as volume_util, ) -from homeassistant.util import temperature as temperature_util -from homeassistant.util import distance as distance_util -from homeassistant.util import pressure as pressure_util -from homeassistant.util import volume as volume_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index 5a05b663522410..2e033beb35cb5b 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -2,13 +2,14 @@ import logging from numbers import Number + from homeassistant.const import ( + UNIT_NOT_RECOGNIZED_TEMPLATE, + VOLUME, + VOLUME_FLUID_OUNCE, + VOLUME_GALLONS, VOLUME_LITERS, VOLUME_MILLILITERS, - VOLUME_GALLONS, - VOLUME_FLUID_OUNCE, - VOLUME, - UNIT_NOT_RECOGNIZED_TEMPLATE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/yaml/__init__.py b/homeassistant/util/yaml/__init__.py index 9cf33a401d3925..106bdbf8ef5d0f 100644 --- a/homeassistant/util/yaml/__init__.py +++ b/homeassistant/util/yaml/__init__.py @@ -1,9 +1,8 @@ """YAML utility functions.""" -from .const import SECRET_YAML, _SECRET_NAMESPACE +from .const import _SECRET_NAMESPACE, SECRET_YAML from .dumper import dump, save_yaml from .loader import clear_secret_cache, load_yaml, secret_yaml - __all__ = [ "SECRET_YAML", "_SECRET_NAMESPACE", diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index a53dc0cdd02030..ffcd4917363f9a 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -1,10 +1,10 @@ """Custom dumper and representers.""" from collections import OrderedDict + import yaml from .objects import NodeListClass - # mypy: allow-untyped-calls, no-warn-return-any diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index fec09a1d6901c8..65422f231ba573 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -1,13 +1,18 @@ """Custom loader.""" +from collections import OrderedDict +import fnmatch import logging import os import sys -import fnmatch -from collections import OrderedDict -from typing import Union, List, Dict, Iterator, overload, TypeVar +from typing import Dict, Iterator, List, TypeVar, Union, overload import yaml +from homeassistant.exceptions import HomeAssistantError + +from .const import _SECRET_NAMESPACE, SECRET_YAML +from .objects import NodeListClass, NodeStrClass + try: import keyring except ImportError: @@ -18,11 +23,6 @@ except ImportError: credstash = None -from homeassistant.exceptions import HomeAssistantError - -from .const import _SECRET_NAMESPACE, SECRET_YAML -from .objects import NodeListClass, NodeStrClass - # mypy: allow-untyped-calls, no-warn-return-any From f60125b5c9dc08e58a8346da7825833e65c8f958 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 16:52:24 +0100 Subject: [PATCH 2341/3953] Sort imports according to PEP8 for 'tests' (#29791) --- .../auth/mfa_modules/test_insecure_example.py | 1 + tests/auth/mfa_modules/test_notify.py | 3 +- tests/auth/mfa_modules/test_totp.py | 3 +- tests/auth/permissions/test_entities.py | 6 ++-- .../auth/permissions/test_system_policies.py | 2 +- tests/auth/providers/test_command_line.py | 4 +-- tests/auth/providers/test_insecure_example.py | 2 +- tests/auth/test_init.py | 5 +-- tests/common.py | 32 ++++++++--------- tests/conftest.py | 17 ++++----- tests/helpers/test_aiohttp_client.py | 2 +- tests/helpers/test_area_registry.py | 3 +- tests/helpers/test_check_config.py | 5 +-- tests/helpers/test_config_entry_flow.py | 5 +-- .../helpers/test_config_entry_oauth2_flow.py | 6 ++-- tests/helpers/test_deprecation.py | 4 +-- tests/helpers/test_device_registry.py | 3 +- tests/helpers/test_discovery.py | 4 +-- tests/helpers/test_dispatcher.py | 2 +- tests/helpers/test_entity.py | 10 +++--- tests/helpers/test_entity_component.py | 19 +++++----- tests/helpers/test_entity_platform.py | 17 +++++---- tests/helpers/test_entity_registry.py | 5 ++- tests/helpers/test_entity_values.py | 1 + tests/helpers/test_entityfilter.py | 2 +- tests/helpers/test_event.py | 8 ++--- tests/helpers/test_intent.py | 5 ++- tests/helpers/test_network.py | 2 +- tests/helpers/test_restore_state.py | 7 ++-- tests/helpers/test_script.py | 10 +++--- tests/helpers/test_service.py | 19 +++++----- tests/helpers/test_state.py | 21 +++++------ tests/helpers/test_storage.py | 3 +- tests/helpers/test_sun.py | 4 +-- tests/helpers/test_temperature.py | 6 ++-- tests/helpers/test_template.py | 4 +-- tests/helpers/test_translation.py | 3 +- tests/scripts/test_auth.py | 2 +- tests/scripts/test_check_config.py | 3 +- tests/test_bootstrap.py | 8 ++--- tests/test_config.py | 28 +++++++-------- tests/test_config_entries.py | 10 +++--- tests/test_core.py | 36 +++++++++---------- tests/test_loader.py | 2 +- tests/test_main.py | 2 +- tests/test_requirements.py | 11 +++--- tests/test_setup.py | 18 +++++----- tests/test_util/aiohttp.py | 3 +- tests/test_util/test_aiohttp.py | 4 +-- .../test/alarm_control_panel.py | 13 +++---- .../custom_components/test/binary_sensor.py | 4 +-- .../custom_components/test/cover.py | 2 +- .../custom_components/test/light.py | 4 +-- .../custom_components/test/lock.py | 3 +- .../custom_components/test/sensor.py | 2 +- .../custom_components/test/switch.py | 4 +-- .../test_package/__init__.py | 1 - tests/util/test_async.py | 2 +- tests/util/test_distance.py | 4 +-- tests/util/test_init.py | 2 +- tests/util/test_json.py | 7 ++-- tests/util/test_location.py | 2 +- tests/util/test_package.py | 3 +- tests/util/test_pressure.py | 4 +-- tests/util/test_ruamel_yaml.py | 3 +- tests/util/test_unit_system.py | 12 +++---- tests/util/test_volume.py | 6 ++-- tests/util/test_yaml.py | 7 ++-- 68 files changed, 235 insertions(+), 232 deletions(-) diff --git a/tests/auth/mfa_modules/test_insecure_example.py b/tests/auth/mfa_modules/test_insecure_example.py index dc233cb53ff9ed..5384ebee4bd6de 100644 --- a/tests/auth/mfa_modules/test_insecure_example.py +++ b/tests/auth/mfa_modules/test_insecure_example.py @@ -2,6 +2,7 @@ from homeassistant import auth, data_entry_flow from homeassistant.auth.mfa_modules import auth_mfa_module_from_config from homeassistant.auth.models import Credentials + from tests.common import MockUser diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index 20ed104b375590..bc4ecaab712b65 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -3,9 +3,10 @@ from unittest.mock import patch from homeassistant import data_entry_flow -from homeassistant.auth import models as auth_models, auth_manager_from_config +from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config from homeassistant.components.notify import NOTIFY_SERVICE_SCHEMA + from tests.common import MockUser, async_mock_service MOCK_CODE = "123456" diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index e53e3030cd2ec5..d0a4f3cf3acaf9 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -3,8 +3,9 @@ from unittest.mock import patch from homeassistant import data_entry_flow -from homeassistant.auth import models as auth_models, auth_manager_from_config +from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config + from tests.common import MockUser MOCK_CODE = "123456" diff --git a/tests/auth/permissions/test_entities.py b/tests/auth/permissions/test_entities.py index c2305b5c203fea..a929984d152235 100644 --- a/tests/auth/permissions/test_entities.py +++ b/tests/auth/permissions/test_entities.py @@ -3,14 +3,14 @@ import voluptuous as vol from homeassistant.auth.permissions.entities import ( - compile_entities, ENTITY_POLICY_SCHEMA, + compile_entities, ) from homeassistant.auth.permissions.models import PermissionLookup -from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.entity_registry import RegistryEntry -from tests.common import mock_registry, mock_device_registry +from tests.common import mock_device_registry, mock_registry def test_entities_none(): diff --git a/tests/auth/permissions/test_system_policies.py b/tests/auth/permissions/test_system_policies.py index 2710fdd68c3f48..58515e2ad2aec1 100644 --- a/tests/auth/permissions/test_system_policies.py +++ b/tests/auth/permissions/test_system_policies.py @@ -1,8 +1,8 @@ """Test system policies.""" from homeassistant.auth.permissions import ( + POLICY_SCHEMA, PolicyPermissions, system_policies, - POLICY_SCHEMA, ) diff --git a/tests/auth/providers/test_command_line.py b/tests/auth/providers/test_command_line.py index 33b1735457f5d4..abcf124b9c4686 100644 --- a/tests/auth/providers/test_command_line.py +++ b/tests/auth/providers/test_command_line.py @@ -1,13 +1,13 @@ """Tests for the command_line auth provider.""" -from unittest.mock import Mock import os +from unittest.mock import Mock import uuid import pytest from homeassistant import data_entry_flow -from homeassistant.auth import auth_store, models as auth_models, AuthManager +from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import command_line from homeassistant.const import CONF_TYPE diff --git a/tests/auth/providers/test_insecure_example.py b/tests/auth/providers/test_insecure_example.py index 43f5aff290eda3..c5b3a8db038395 100644 --- a/tests/auth/providers/test_insecure_example.py +++ b/tests/auth/providers/test_insecure_example.py @@ -4,7 +4,7 @@ import pytest -from homeassistant.auth import auth_store, models as auth_models, AuthManager +from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import insecure_example from tests.common import mock_coro diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 73b383fb0fec19..aa047a12d54528 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -7,11 +7,12 @@ import voluptuous as vol from homeassistant import auth, data_entry_flow -from homeassistant.auth import models as auth_models, auth_store, const as auth_const +from homeassistant.auth import auth_store, const as auth_const, models as auth_models from homeassistant.auth.const import MFA_SESSION_EXPIRATION from homeassistant.core import callback from homeassistant.util import dt as dt_util -from tests.common import MockUser, ensure_auth_manager_loaded, flush_store, CLIENT_ID + +from tests.common import CLIENT_ID, MockUser, ensure_auth_manager_loaded, flush_store @pytest.fixture diff --git a/tests/common.py b/tests/common.py index e652e10cc542e7..a54b3899698f36 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,32 +1,32 @@ """Test the helper method for writing tests.""" import asyncio import collections +from collections import OrderedDict +from contextlib import contextmanager +from datetime import timedelta import functools as ft +from io import StringIO import json import logging import os -import uuid import sys import threading - -from collections import OrderedDict -from contextlib import contextmanager -from datetime import timedelta -from io import StringIO from unittest.mock import MagicMock, Mock, patch - -import homeassistant.util.dt as date_util -import homeassistant.util.yaml.loader as yaml_loader +import uuid from homeassistant import auth, config_entries, core as ha, loader from homeassistant.auth import ( - models as auth_models, auth_store, - providers as auth_providers, + models as auth_models, permissions as auth_permissions, + providers as auth_providers, ) from homeassistant.auth.permissions import system_policies from homeassistant.components import mqtt, recorder +from homeassistant.components.device_automation import ( # noqa: F401 + _async_get_device_automation_capabilities as async_get_device_automation_capabilities, + _async_get_device_automations as async_get_device_automations, +) from homeassistant.components.mqtt.models import Message from homeassistant.config import async_process_component_config from homeassistant.const import ( @@ -38,8 +38,8 @@ EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, SERVER_PORT, - STATE_ON, STATE_OFF, + STATE_ON, ) from homeassistant.core import State from homeassistant.helpers import ( @@ -54,12 +54,10 @@ ) from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.components.device_automation import ( # noqa: F401 - _async_get_device_automations as async_get_device_automations, - _async_get_device_automation_capabilities as async_get_device_automation_capabilities, -) +import homeassistant.util.dt as date_util +from homeassistant.util.unit_system import METRIC_SYSTEM +import homeassistant.util.yaml.loader as yaml_loader _TEST_INSTANCE_PORT = SERVER_PORT _LOGGER = logging.getLogger(__name__) diff --git a/tests/conftest.py b/tests/conftest.py index 5e1bbc76fb5641..2b6a1a1673912f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,22 +9,23 @@ import requests_mock as _requests_mock from homeassistant import util -from homeassistant.util import location from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY -from homeassistant.auth.providers import legacy_api_password, homeassistant +from homeassistant.auth.providers import homeassistant, legacy_api_password +from homeassistant.util import location -pytest.register_assert_rewrite("tests.common") from tests.common import ( # noqa: E402 module level import not at top of file - async_test_home_assistant, + CLIENT_ID, INSTANCES, + MockUser, + async_test_home_assistant, mock_coro, mock_storage as mock_storage, - MockUser, - CLIENT_ID, ) -from tests.test_util.aiohttp import ( +from tests.test_util.aiohttp import ( # noqa: E402 module level import not at top of file mock_aiohttp_client, -) # noqa: E402 module level import not at top of file +) + +pytest.register_assert_rewrite("tests.common") if os.environ.get("UVLOOP") == "1": import uvloop diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 5494bc40a75cc2..de7057ae9c7a7a 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -6,8 +6,8 @@ import pytest from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE -from homeassistant.setup import async_setup_component import homeassistant.helpers.aiohttp_client as client +from homeassistant.setup import async_setup_component from homeassistant.util.async_ import run_callback_threadsafe from tests.common import get_test_home_assistant diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 48d32861a0abbd..87e4b4c4d031a4 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -6,7 +6,8 @@ from homeassistant.core import callback from homeassistant.helpers import area_registry -from tests.common import mock_area_registry, flush_store + +from tests.common import flush_store, mock_area_registry @pytest.fixture diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 0b34b263cafa23..54c835895ca2bc 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -2,11 +2,12 @@ import logging from unittest.mock import patch +from homeassistant.config import YAML_CONFIG_FILE from homeassistant.helpers.check_config import ( - async_check_ha_config_file, CheckConfigError, + async_check_ha_config_file, ) -from homeassistant.config import YAML_CONFIG_FILE + from tests.common import patch_yaml_files _LOGGER = logging.getLogger(__name__) diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 3c3d1224e1234b..1c292e5ed4811f 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -1,16 +1,17 @@ """Tests for the Config Entry Flow helper.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.helpers import config_entry_flow + from tests.common import ( MockConfigEntry, MockModule, mock_coro, - mock_integration, mock_entity_platform, + mock_integration, ) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 773dfa09375d22..366c295874d10e 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -1,15 +1,15 @@ """Tests for the Somfy config flow.""" import asyncio import logging -from unittest.mock import patch import time +from unittest.mock import patch import pytest -from homeassistant import data_entry_flow, setup, config_entries +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.helpers import config_entry_oauth2_flow -from tests.common import mock_platform, MockConfigEntry +from tests.common import MockConfigEntry, mock_platform TEST_DOMAIN = "oauth2_test" CLIENT_SECRET = "5678" diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index d0cb0eca55ab96..38410c3bf0fdf1 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -1,7 +1,7 @@ """Test deprecation helpers.""" -from homeassistant.helpers.deprecation import deprecated_substitute, get_deprecated +from unittest.mock import MagicMock, patch -from unittest.mock import patch, MagicMock +from homeassistant.helpers.deprecation import deprecated_substitute, get_deprecated class MockBaseClass: diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 1b146e9cb129c0..3846230e6d3571 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -7,7 +7,8 @@ from homeassistant.core import callback from homeassistant.helpers import device_registry -from tests.common import mock_device_registry, flush_store + +from tests.common import flush_store, mock_device_registry @pytest.fixture diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index dbd837eb5c7a0d..3b0996d676a2b6 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -9,12 +9,12 @@ from homeassistant.helpers import discovery from tests.common import ( - get_test_home_assistant, MockModule, MockPlatform, + get_test_home_assistant, mock_coro, - mock_integration, mock_entity_platform, + mock_integration, ) diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index 789cb6c1dc8b3c..4cf266e88a26b0 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -4,8 +4,8 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, - dispatcher_send, dispatcher_connect, + dispatcher_send, ) from tests.common import get_test_home_assistant diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index cd852f5bfc0688..749c11ff1a5084 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1,16 +1,16 @@ """Test the entity helper.""" # pylint: disable=protected-access import asyncio -import threading from datetime import timedelta -from unittest.mock import MagicMock, patch, PropertyMock +import threading +from unittest.mock import MagicMock, PropertyMock, patch import pytest -from homeassistant.helpers import entity, entity_registry -from homeassistant.core import Context -from homeassistant.const import ATTR_HIDDEN, ATTR_DEVICE_CLASS, STATE_UNAVAILABLE from homeassistant.config import DATA_CUSTOMIZE +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_HIDDEN, STATE_UNAVAILABLE +from homeassistant.core import Context +from homeassistant.helpers import entity, entity_registry from homeassistant.helpers.entity_values import EntityValues from tests.common import get_test_home_assistant, mock_registry diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 350aa88b6af2f3..81fbe2d6520047 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -1,30 +1,29 @@ """The tests for the Entity component helper.""" # pylint: disable=protected-access from collections import OrderedDict -import logging -from unittest.mock import patch, Mock from datetime import timedelta +import logging +from unittest.mock import Mock, patch import asynctest import pytest -import homeassistant.core as ha +from homeassistant.components import group from homeassistant.const import ENTITY_MATCH_ALL +import homeassistant.core as ha from homeassistant.exceptions import PlatformNotReady -from homeassistant.components import group +from homeassistant.helpers import discovery from homeassistant.helpers.entity_component import EntityComponent from homeassistant.setup import async_setup_component - -from homeassistant.helpers import discovery import homeassistant.util.dt as dt_util from tests.common import ( - MockPlatform, + MockConfigEntry, + MockEntity, MockModule, - mock_coro, + MockPlatform, async_fire_time_changed, - MockEntity, - MockConfigEntry, + mock_coro, mock_entity_platform, mock_integration, ) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index caf8bb702afe87..5909dfaf3aaaf8 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1,30 +1,29 @@ """Tests for the EntityPlatform helper.""" import asyncio -import logging -from unittest.mock import patch, Mock, MagicMock from datetime import timedelta +import logging +from unittest.mock import MagicMock, Mock, patch import asynctest import pytest from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import entity_platform, entity_registry from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_component import ( - EntityComponent, DEFAULT_SCAN_INTERVAL, + EntityComponent, ) -from homeassistant.helpers import entity_platform, entity_registry - import homeassistant.util.dt as dt_util from tests.common import ( - MockPlatform, - async_fire_time_changed, - mock_registry, + MockConfigEntry, MockEntity, MockEntityPlatform, - MockConfigEntry, + MockPlatform, + async_fire_time_changed, mock_entity_platform, + mock_registry, ) _LOGGER = logging.getLogger(__name__) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 9debbdbcba7cda..b07c5237116b7b 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -5,11 +5,10 @@ import asynctest import pytest -from homeassistant.core import valid_entity_id, callback +from homeassistant.core import callback, valid_entity_id from homeassistant.helpers import entity_registry -from tests.common import MockConfigEntry, mock_registry, flush_store - +from tests.common import MockConfigEntry, flush_store, mock_registry YAML__OPEN_PATH = "homeassistant.util.yaml.loader.open" diff --git a/tests/helpers/test_entity_values.py b/tests/helpers/test_entity_values.py index d9be6c52b4e112..f5ad68ee23160d 100644 --- a/tests/helpers/test_entity_values.py +++ b/tests/helpers/test_entity_values.py @@ -1,5 +1,6 @@ """Test the entity values helper.""" from collections import OrderedDict + from homeassistant.helpers.entity_values import EntityValues as EV ent = "test.test" diff --git a/tests/helpers/test_entityfilter.py b/tests/helpers/test_entityfilter.py index 8deea67ac16963..726e6bd92d0ec9 100644 --- a/tests/helpers/test_entityfilter.py +++ b/tests/helpers/test_entityfilter.py @@ -1,5 +1,5 @@ """The tests for the EntityFilter component.""" -from homeassistant.helpers.entityfilter import generate_filter, FILTER_SCHEMA +from homeassistant.helpers.entityfilter import FILTER_SCHEMA, generate_filter def test_no_filters_case_1(): diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 3d4804e5079f85..d331da5d92d26f 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -7,10 +7,10 @@ from astral import Astral import pytest -from homeassistant.core import callback -from homeassistant.setup import async_setup_component -import homeassistant.core as ha +from homeassistant.components import sun from homeassistant.const import MATCH_ALL +import homeassistant.core as ha +from homeassistant.core import callback from homeassistant.helpers.event import ( async_call_later, async_track_point_in_time, @@ -25,7 +25,7 @@ async_track_utc_time_change, ) from homeassistant.helpers.template import Template -from homeassistant.components import sun +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index 3988c86f516928..bbb6e394ee0612 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -1,11 +1,10 @@ """Tests for the intent helpers.""" -import voluptuous as vol - import pytest +import voluptuous as vol from homeassistant.core import State -from homeassistant.helpers import intent, config_validation as cv +from homeassistant.helpers import config_validation as cv, intent class MockIntentHandler(intent.IntentHandler): diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index afb9e88c5a499d..d4c5366b879482 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -1,8 +1,8 @@ """Test network helper.""" from unittest.mock import Mock, patch -from homeassistant.helpers import network from homeassistant.components import cloud +from homeassistant.helpers import network async def test_get_external_url(hass): diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 94a17697eb404e..97004362d20079 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -8,15 +8,14 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import Entity from homeassistant.helpers.restore_state import ( - RestoreStateData, - RestoreEntity, - StoredState, DATA_RESTORE_STATE_TASK, STORAGE_KEY, + RestoreEntity, + RestoreStateData, + StoredState, ) from homeassistant.util import dt as dt_util - from tests.common import mock_coro diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 4b8be715f37ca5..a7fe2c25236879 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -6,21 +6,19 @@ import asynctest import jinja2 -import voluptuous as vol import pytest +import voluptuous as vol -import homeassistant.components.scene as scene +# Otherwise can't test just this file (import order issue) from homeassistant import exceptions +import homeassistant.components.scene as scene from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON from homeassistant.core import Context, callback - -# Otherwise can't test just this file (import order issue) +from homeassistant.helpers import config_validation as cv, script import homeassistant.util.dt as dt_util -from homeassistant.helpers import script, config_validation as cv from tests.common import async_fire_time_changed - ENTITY_ID = "script.test" diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 20be7db42f5402..2697c59b787be0 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -5,28 +5,29 @@ import unittest from unittest.mock import Mock, patch -import voluptuous as vol import pytest +import voluptuous as vol # To prevent circular import when running just this file -import homeassistant.components # noqa: F401 from homeassistant import core as ha, exceptions -from homeassistant.const import STATE_ON, STATE_OFF, ATTR_ENTITY_ID, ENTITY_MATCH_ALL -from homeassistant.setup import async_setup_component -import homeassistant.helpers.config_validation as cv from homeassistant.auth.permissions import PolicyPermissions +import homeassistant.components # noqa: F401 +from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, STATE_OFF, STATE_ON from homeassistant.helpers import ( - service, - template, device_registry as dev_reg, entity_registry as ent_reg, + service, + template, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.setup import async_setup_component + from tests.common import ( get_test_home_assistant, - mock_service, mock_coro, - mock_registry, mock_device_registry, + mock_registry, + mock_service, ) diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 14bcbde50949b5..567bac65f5baed 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -5,21 +5,22 @@ import pytest -import homeassistant.core as ha -from homeassistant.const import SERVICE_TURN_ON, SERVICE_TURN_OFF -from homeassistant.util import dt as dt_util -from homeassistant.helpers import state +from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON from homeassistant.const import ( - STATE_OPEN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_CLOSED, - STATE_LOCKED, - STATE_UNLOCKED, - STATE_ON, - STATE_OFF, STATE_HOME, + STATE_LOCKED, STATE_NOT_HOME, + STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_UNLOCKED, ) -from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON +import homeassistant.core as ha +from homeassistant.helpers import state +from homeassistant.util import dt as dt_util from tests.common import async_mock_service diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 87a22fcb845b14..2fef58ad115672 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -2,7 +2,7 @@ import asyncio from datetime import timedelta import json -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest @@ -12,7 +12,6 @@ from tests.common import async_fire_time_changed, mock_coro - MOCK_VERSION = 1 MOCK_KEY = "storage-test" MOCK_DATA = {"hello": "world"} diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index 1746c7e6fc0203..b8ecd1ed86aa96 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -1,11 +1,11 @@ """The tests for the Sun helpers.""" # pylint: disable=protected-access +from datetime import datetime, timedelta from unittest.mock import patch -from datetime import timedelta, datetime from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET -import homeassistant.util.dt as dt_util import homeassistant.helpers.sun as sun +import homeassistant.util.dt as dt_util def test_next_events(hass): diff --git a/tests/helpers/test_temperature.py b/tests/helpers/test_temperature.py index 840e6fd5d9d0ac..5808b66115066c 100644 --- a/tests/helpers/test_temperature.py +++ b/tests/helpers/test_temperature.py @@ -2,11 +2,11 @@ import pytest from homeassistant.const import ( - TEMP_CELSIUS, - PRECISION_WHOLE, - TEMP_FAHRENHEIT, PRECISION_HALVES, PRECISION_TENTHS, + PRECISION_WHOLE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.helpers.temperature import display_temp diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index f463149bc2820c..cbd530d0b4c497 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1,13 +1,12 @@ """Test Home Assistant template helper methods.""" +from datetime import datetime import math import random -from datetime import datetime from unittest.mock import patch import pytest import pytz -import homeassistant.util.dt as dt_util from homeassistant.components import group from homeassistant.const import ( LENGTH_METERS, @@ -19,6 +18,7 @@ ) from homeassistant.exceptions import TemplateError from homeassistant.helpers import template +import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 1c3748250a5fdb..6b846703914e94 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -5,9 +5,10 @@ import pytest +from homeassistant.generated import config_flows import homeassistant.helpers.translation as translation from homeassistant.setup import async_setup_component -from homeassistant.generated import config_flows + from tests.common import mock_coro diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index f762630a42a964..3ab194508793e2 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -3,8 +3,8 @@ import pytest -from homeassistant.scripts import auth as script_auth from homeassistant.auth.providers import homeassistant as hass_auth +from homeassistant.scripts import auth as script_auth from tests.common import register_auth_provider diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 8e1ffe63e84533..481efc6fb30265 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -2,8 +2,9 @@ import logging from unittest.mock import patch -import homeassistant.scripts.check_config as check_config from homeassistant.config import YAML_CONFIG_FILE +import homeassistant.scripts.check_config as check_config + from tests.common import get_test_config_dir, patch_yaml_files _LOGGER = logging.getLogger(__name__) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index eaabd61af4b829..f71be8fe9b1bed 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,20 +1,20 @@ """Test the bootstrapping.""" # pylint: disable=protected-access import asyncio +import logging import os from unittest.mock import Mock, patch -import logging -import homeassistant.config as config_util from homeassistant import bootstrap +import homeassistant.config as config_util import homeassistant.util.dt as dt_util from tests.common import ( - patch_yaml_files, + MockModule, get_test_config_dir, mock_coro, mock_integration, - MockModule, + patch_yaml_files, ) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/test_config.py b/tests/test_config.py index 1c872369096c60..1abb6ec7ff1a91 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,39 +1,39 @@ """Test config utils.""" # pylint: disable=protected-access import asyncio +from collections import OrderedDict import copy import os import unittest.mock as mock -from collections import OrderedDict import asynctest import pytest -from voluptuous import MultipleInvalid, Invalid +from voluptuous import Invalid, MultipleInvalid import yaml -from homeassistant.core import SOURCE_STORAGE, HomeAssistantError import homeassistant.config as config_util -from homeassistant.loader import async_get_integration from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, - ATTR_ASSUMED_STATE, + CONF_AUTH_MFA_MODULES, + CONF_AUTH_PROVIDERS, + CONF_CUSTOMIZE, CONF_LATITUDE, CONF_LONGITUDE, - CONF_UNIT_SYSTEM, CONF_NAME, - CONF_CUSTOMIZE, - __version__, - CONF_UNIT_SYSTEM_METRIC, - CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, - CONF_AUTH_PROVIDERS, - CONF_AUTH_MFA_MODULES, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, + __version__, ) +from homeassistant.core import SOURCE_STORAGE, HomeAssistantError +import homeassistant.helpers.check_config as check_config +from homeassistant.helpers.entity import Entity +from homeassistant.loader import async_get_integration from homeassistant.util import dt as dt_util from homeassistant.util.yaml import SECRET_YAML -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.check_config as check_config from tests.common import get_test_config_dir, patch_yaml_files diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d9dd614c9a5e4a..24a0b0939bef40 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -12,14 +12,14 @@ from homeassistant.util import dt from tests.common import ( - MockModule, - mock_coro, MockConfigEntry, - async_fire_time_changed, - MockPlatform, MockEntity, - mock_integration, + MockModule, + MockPlatform, + async_fire_time_changed, + mock_coro, mock_entity_platform, + mock_integration, mock_registry, ) diff --git a/tests/test_core.py b/tests/test_core.py index 5ac13027f288a7..1999065e31dcb9 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,40 +1,40 @@ """Test to verify that Home Assistant core works.""" # pylint: disable=protected-access import asyncio +from datetime import datetime, timedelta import functools import logging import os -import unittest -from unittest.mock import patch, MagicMock -from datetime import datetime, timedelta from tempfile import TemporaryDirectory +import unittest +from unittest.mock import MagicMock, patch -import voluptuous as vol -import pytz import pytest +import pytz +import voluptuous as vol -import homeassistant.core as ha -from homeassistant.exceptions import InvalidEntityFormatError, InvalidStateError -import homeassistant.util.dt as dt_util -from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.const import ( - __version__, - EVENT_STATE_CHANGED, ATTR_FRIENDLY_NAME, - CONF_UNIT_SYSTEM, ATTR_NOW, - EVENT_TIME_CHANGED, - EVENT_TIMER_OUT_OF_SYNC, ATTR_SECONDS, - EVENT_HOMEASSISTANT_STOP, + CONF_UNIT_SYSTEM, + EVENT_CALL_SERVICE, + EVENT_CORE_CONFIG_UPDATE, EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_STOP, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, - EVENT_CALL_SERVICE, - EVENT_CORE_CONFIG_UPDATE, + EVENT_STATE_CHANGED, + EVENT_TIME_CHANGED, + EVENT_TIMER_OUT_OF_SYNC, + __version__, ) +import homeassistant.core as ha +from homeassistant.exceptions import InvalidEntityFormatError, InvalidStateError +import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.common import get_test_home_assistant, async_mock_service +from tests.common import async_mock_service, get_test_home_assistant PST = pytz.timezone("America/Los_Angeles") diff --git a/tests/test_loader.py b/tests/test_loader.py index e7011997f73626..f3e7c3bd884f20 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -2,9 +2,9 @@ from asynctest.mock import ANY, patch import pytest -import homeassistant.loader as loader from homeassistant.components import http, hue from homeassistant.components.hue import light as hue_light +import homeassistant.loader as loader from tests.common import MockModule, async_mock_service, mock_integration diff --git a/tests/test_main.py b/tests/test_main.py index 29454d269af494..5ec6460301f1c4 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,5 @@ """Test methods in __main__.""" -from unittest.mock import patch, PropertyMock +from unittest.mock import PropertyMock, patch from homeassistant import __main__ as main from homeassistant.const import REQUIRED_PYTHON_VER diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 2627a077a87c74..b807188e8a52e0 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -1,20 +1,21 @@ """Test requirements module.""" import os from pathlib import Path -from unittest.mock import patch, call +from unittest.mock import call, patch + 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, + _install, + async_get_integration_with_requirements, + async_process_requirements, ) -from tests.common import get_test_home_assistant, MockModule, mock_integration +from tests.common import MockModule, get_test_home_assistant, mock_integration def env_without_wheel_links(): diff --git a/tests/test_setup.py b/tests/test_setup.py index 8fd25091eb6801..c19d92db4b6625 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,32 +1,32 @@ """Test component/platform setup.""" # pylint: disable=protected-access import asyncio +import logging import os -from unittest import mock import threading -import logging +from unittest import mock import voluptuous as vol -from homeassistant.core import callback -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_COMPONENT_LOADED -import homeassistant.config as config_util from homeassistant import setup -import homeassistant.util.dt as dt_util +import homeassistant.config as config_util +from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START +from homeassistant.core import callback +from homeassistant.helpers import discovery from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers import discovery +import homeassistant.util.dt as dt_util from tests.common import ( - get_test_home_assistant, MockModule, MockPlatform, assert_setup_component, get_test_config_dir, - mock_integration, + get_test_home_assistant, mock_entity_platform, + mock_integration, ) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index ce13ca5a594aa5..e0e1ded32c335c 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -7,11 +7,10 @@ from urllib.parse import parse_qs from aiohttp import ClientSession +from aiohttp.client_exceptions import ClientResponseError from aiohttp.streams import StreamReader from yarl import URL -from aiohttp.client_exceptions import ClientResponseError - from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE retype = type(re.compile("")) diff --git a/tests/test_util/test_aiohttp.py b/tests/test_util/test_aiohttp.py index 0a0ead54dcaaaa..2761f5b9ea39cc 100644 --- a/tests/test_util/test_aiohttp.py +++ b/tests/test_util/test_aiohttp.py @@ -1,8 +1,8 @@ """Tests for our aiohttp mocker.""" -from .aiohttp import AiohttpClientMocker - import pytest +from .aiohttp import AiohttpClientMocker + async def test_matching_url(): """Test we can match urls.""" diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py index 1ffa52086e7bae..065ea3e3980772 100644 --- a/tests/testing_config/custom_components/test/alarm_control_panel.py +++ b/tests/testing_config/custom_components/test/alarm_control_panel.py @@ -4,6 +4,12 @@ Call init before using it in your tests to ensure clean test data. """ from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, @@ -11,12 +17,7 @@ STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.components.alarm_control_panel.const import ( - SUPPORT_ALARM_ARM_HOME, - SUPPORT_ALARM_ARM_AWAY, - SUPPORT_ALARM_ARM_NIGHT, - SUPPORT_ALARM_TRIGGER, -) + from tests.common import MockEntity ENTITIES = {} diff --git a/tests/testing_config/custom_components/test/binary_sensor.py b/tests/testing_config/custom_components/test/binary_sensor.py index 5052b8e47f10b2..bcff0adb4e4856 100644 --- a/tests/testing_config/custom_components/test/binary_sensor.py +++ b/tests/testing_config/custom_components/test/binary_sensor.py @@ -3,9 +3,9 @@ Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.binary_sensor import BinarySensorDevice, DEVICE_CLASSES -from tests.common import MockEntity +from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorDevice +from tests.common import MockEntity ENTITIES = {} diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py index d7c771e2b2821b..ce5462790bb6b9 100644 --- a/tests/testing_config/custom_components/test/cover.py +++ b/tests/testing_config/custom_components/test/cover.py @@ -4,8 +4,8 @@ Call init before using it in your tests to ensure clean test data. """ from homeassistant.components.cover import CoverDevice -from tests.common import MockEntity +from tests.common import MockEntity ENTITIES = {} diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 0a48388b718b1e..4b018adb5cbd39 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -3,9 +3,9 @@ Call init before using it in your tests to ensure clean test data. """ -from homeassistant.const import STATE_ON, STATE_OFF -from tests.common import MockToggleEntity +from homeassistant.const import STATE_OFF, STATE_ON +from tests.common import MockToggleEntity ENTITIES = [] diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py index db6ce38b097ac3..24b04903541a09 100644 --- a/tests/testing_config/custom_components/test/lock.py +++ b/tests/testing_config/custom_components/test/lock.py @@ -3,7 +3,8 @@ Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.lock import LockDevice, SUPPORT_OPEN +from homeassistant.components.lock import SUPPORT_OPEN, LockDevice + from tests.common import MockEntity ENTITIES = {} diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 651ee17bd65d46..26497b16a16536 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -4,8 +4,8 @@ Call init before using it in your tests to ensure clean test data. """ import homeassistant.components.sensor as sensor -from tests.common import MockEntity +from tests.common import MockEntity DEVICE_CLASSES = list(sensor.DEVICE_CLASSES) DEVICE_CLASSES.append("none") diff --git a/tests/testing_config/custom_components/test/switch.py b/tests/testing_config/custom_components/test/switch.py index 484c47d1190e3c..7dd1862d88f00d 100644 --- a/tests/testing_config/custom_components/test/switch.py +++ b/tests/testing_config/custom_components/test/switch.py @@ -3,9 +3,9 @@ Call init before using it in your tests to ensure clean test data. """ -from homeassistant.const import STATE_ON, STATE_OFF -from tests.common import MockToggleEntity +from homeassistant.const import STATE_OFF, STATE_ON +from tests.common import MockToggleEntity ENTITIES = [] diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index f5cd2c34edfd3d..44f62380c92c67 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -1,7 +1,6 @@ """Provide a mock package component.""" from .const import TEST # noqa: F401 - DOMAIN = "test_package" diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 9cda40c1b8b4b7..ec16fa2ae67ebd 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -1,8 +1,8 @@ """Tests for async util methods from Python source.""" import asyncio import sys -from unittest.mock import MagicMock, patch from unittest import TestCase +from unittest.mock import MagicMock, patch import pytest diff --git a/tests/util/test_distance.py b/tests/util/test_distance.py index 581257d6f49d0e..27b77a883c7801 100644 --- a/tests/util/test_distance.py +++ b/tests/util/test_distance.py @@ -2,13 +2,13 @@ import pytest -import homeassistant.util.distance as distance_util from homeassistant.const import ( + LENGTH_FEET, LENGTH_KILOMETERS, LENGTH_METERS, - LENGTH_FEET, LENGTH_MILES, ) +import homeassistant.util.distance as distance_util INVALID_SYMBOL = "bob" VALID_SYMBOL = LENGTH_KILOMETERS diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 261280cc20e22d..2ffca07082bc21 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -1,6 +1,6 @@ """Test Home Assistant util methods.""" -from unittest.mock import patch, MagicMock from datetime import datetime, timedelta +from unittest.mock import MagicMock, patch import pytest diff --git a/tests/util/test_json.py b/tests/util/test_json.py index bec3230c01eaad..26245482c2f359 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -1,16 +1,15 @@ """Test Home Assistant json utility functions.""" from json import JSONEncoder import os -import unittest -from unittest.mock import Mock import sys from tempfile import mkdtemp +import unittest +from unittest.mock import Mock import pytest -from homeassistant.util.json import SerializationError, load_json, save_json from homeassistant.exceptions import HomeAssistantError - +from homeassistant.util.json import SerializationError, load_json, save_json # Test data that can be saved as JSON TEST_JSON_A = {"a": 1, "B": "two"} diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 4908018410b504..6dd6eafca1dd9d 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,5 +1,5 @@ """Test Home Assistant location util methods.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import aiohttp import pytest diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 79676cbfcd99bc..ca4ed83734a3cd 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -2,8 +2,8 @@ import asyncio import logging import os -import sys from subprocess import PIPE +import sys from unittest.mock import MagicMock, call, patch import pkg_resources @@ -11,7 +11,6 @@ import homeassistant.util.package as package - RESOURCE_DIR = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "resources") ) diff --git a/tests/util/test_pressure.py b/tests/util/test_pressure.py index 18fa238e43b242..df65618dc48490 100644 --- a/tests/util/test_pressure.py +++ b/tests/util/test_pressure.py @@ -2,10 +2,10 @@ import pytest from homeassistant.const import ( - PRESSURE_PA, PRESSURE_HPA, - PRESSURE_MBAR, PRESSURE_INHG, + PRESSURE_MBAR, + PRESSURE_PA, PRESSURE_PSI, ) import homeassistant.util.pressure as pressure_util diff --git a/tests/util/test_ruamel_yaml.py b/tests/util/test_ruamel_yaml.py index 9ae806f31ea76c..79ed4a4f4d1165 100644 --- a/tests/util/test_ruamel_yaml.py +++ b/tests/util/test_ruamel_yaml.py @@ -1,14 +1,13 @@ """Test Home Assistant ruamel.yaml loader.""" import os from tempfile import mkdtemp -import pytest +import pytest from ruamel.yaml import YAML from homeassistant.exceptions import HomeAssistantError import homeassistant.util.ruamel_yaml as util_yaml - TEST_YAML_A = """\ title: My Awesome Home # Include external resources diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 827143bc4475a9..e0e4524a2f2e53 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -1,20 +1,20 @@ """Test the unit system helper.""" import pytest -from homeassistant.util.unit_system import UnitSystem, METRIC_SYSTEM, IMPERIAL_SYSTEM from homeassistant.const import ( - LENGTH_METERS, + LENGTH, LENGTH_KILOMETERS, + LENGTH_METERS, + MASS, MASS_GRAMS, + PRESSURE, PRESSURE_PA, - VOLUME_LITERS, TEMP_CELSIUS, - LENGTH, - MASS, - PRESSURE, TEMPERATURE, VOLUME, + VOLUME_LITERS, ) +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem SYSTEM_NAME = "TEST" INVALID_UNIT = "INVALID" diff --git a/tests/util/test_volume.py b/tests/util/test_volume.py index 4bf9b7c075d08c..9bd3e4b1a98e92 100644 --- a/tests/util/test_volume.py +++ b/tests/util/test_volume.py @@ -2,13 +2,13 @@ import pytest -import homeassistant.util.volume as volume_util from homeassistant.const import ( + VOLUME_FLUID_OUNCE, + VOLUME_GALLONS, VOLUME_LITERS, VOLUME_MILLILITERS, - VOLUME_GALLONS, - VOLUME_FLUID_OUNCE, ) +import homeassistant.util.volume as volume_util INVALID_SYMBOL = "bob" VALID_SYMBOL = VOLUME_LITERS diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 1e5797e33e7cf4..ba31ce57010da8 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -1,16 +1,17 @@ """Test Home Assistant yaml loader.""" import io +import logging import os import unittest -import logging from unittest.mock import patch import pytest +from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file from homeassistant.exceptions import HomeAssistantError -from homeassistant.util.yaml import loader as yaml_loader import homeassistant.util.yaml as yaml -from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file +from homeassistant.util.yaml import loader as yaml_loader + from tests.common import get_test_config_dir, patch_yaml_files From c804f8f9613dcb654019d76a0a42615508f1a979 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 9 Dec 2019 17:19:42 +0100 Subject: [PATCH 2342/3953] Add config flow to iCloud (#28968) * iCloud: setup ConfigFlow and prepare for more platforms - add config flow + tests - fix existing services - add play_sound & display_message services - document services - can use devices with the same name - prepare to add sensor platform * Review : not copy account conf * Review: Safer test patch * Review: remove reset_account * Review: Use executor_job while IO * Review: Use executor_job while IO 2 * Review: use hass.helpers.storage.Store() * Review: no IO in tests * Remove reset from services.yaml * Review: remove authenticate.return_value = Mock() * Review: do not initialize the api with the mocked service * isort * Review: @MartinHjelmare Test config flow with all steps * Review: Fix failed tests names * Codevov: Add one missing test --- .coveragerc | 3 +- CODEOWNERS | 1 + .../components/icloud/.translations/en.json | 38 ++ homeassistant/components/icloud/__init__.py | 607 ++++++++++++++++- .../components/icloud/config_flow.py | 230 +++++++ homeassistant/components/icloud/const.py | 89 ++- .../components/icloud/device_tracker.py | 616 +++--------------- homeassistant/components/icloud/manifest.json | 13 +- homeassistant/components/icloud/services.yaml | 70 +- homeassistant/components/icloud/strings.json | 38 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/icloud/__init__.py | 1 + tests/components/icloud/test_config_flow.py | 309 +++++++++ 14 files changed, 1463 insertions(+), 556 deletions(-) create mode 100644 homeassistant/components/icloud/.translations/en.json create mode 100644 homeassistant/components/icloud/config_flow.py create mode 100644 homeassistant/components/icloud/strings.json create mode 100644 tests/components/icloud/__init__.py create mode 100644 tests/components/icloud/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 8f872a93d8dae6..0b73599dffa7e1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -319,7 +319,8 @@ omit = homeassistant/components/iaqualink/light.py homeassistant/components/iaqualink/sensor.py homeassistant/components/iaqualink/switch.py - homeassistant/components/icloud/* + homeassistant/components/icloud/__init__.py + homeassistant/components/icloud/device_tracker.py homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py homeassistant/components/izone/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index afea92c8847b40..392c363c648c28 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -152,6 +152,7 @@ homeassistant/components/huawei_lte/* @scop homeassistant/components/huawei_router/* @abmantis homeassistant/components/hue/* @balloob homeassistant/components/iaqualink/* @flz +homeassistant/components/icloud/* @Quentame homeassistant/components/ign_sismologia/* @exxamalte homeassistant/components/incomfort/* @zxdavb homeassistant/components/influxdb/* @fabaff diff --git a/homeassistant/components/icloud/.translations/en.json b/homeassistant/components/icloud/.translations/en.json new file mode 100644 index 00000000000000..581017593566ca --- /dev/null +++ b/homeassistant/components/icloud/.translations/en.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Account already configured" + }, + "error": { + "login": "Login error: please check your email & password", + "send_verification_code": "Failed to send verification code", + "username_exists": "Account already configured", + "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Trusted device" + }, + "description": "Select your trusted device", + "title": "iCloud trusted device" + }, + "user": { + "data": { + "password": "Password", + "username": "Email" + }, + "description": "Enter your credentials", + "title": "iCloud credentials" + }, + "verification_code": { + "data": { + "verification_code": "Verification code" + }, + "description": "Please enter the verification code you just received from iCloud", + "title": "iCloud verification code" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 1169104c99d9a3..2012f69193803b 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1 +1,606 @@ -"""The icloud component.""" +"""The iCloud component.""" +from datetime import timedelta +import logging +import operator +from typing import Dict + +from pyicloud import PyiCloudService +from pyicloud.exceptions import PyiCloudFailedLoginException, PyiCloudNoDevicesException +from pyicloud.services.findmyiphone import AppleDevice +import voluptuous as vol + +from homeassistant.components.zone import async_active_zone +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType +from homeassistant.util import slugify +from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.dt import utcnow +from homeassistant.util.location import distance + +from .const import ( + CONF_ACCOUNT_NAME, + CONF_GPS_ACCURACY_THRESHOLD, + CONF_MAX_INTERVAL, + DEFAULT_GPS_ACCURACY_THRESHOLD, + DEFAULT_MAX_INTERVAL, + DEVICE_BATTERY_LEVEL, + DEVICE_BATTERY_STATUS, + DEVICE_CLASS, + DEVICE_DISPLAY_NAME, + DEVICE_ID, + DEVICE_LOCATION, + DEVICE_LOCATION_LATITUDE, + DEVICE_LOCATION_LONGITUDE, + DEVICE_LOST_MODE_CAPABLE, + DEVICE_LOW_POWER_MODE, + DEVICE_NAME, + DEVICE_PERSON_ID, + DEVICE_RAW_DEVICE_MODEL, + DEVICE_STATUS, + DEVICE_STATUS_CODES, + DEVICE_STATUS_SET, + DOMAIN, + ICLOUD_COMPONENTS, + STORAGE_KEY, + STORAGE_VERSION, + TRACKER_UPDATE, +) + +ATTRIBUTION = "Data provided by Apple iCloud" + +# entity attributes +ATTR_ACCOUNT_FETCH_INTERVAL = "account_fetch_interval" +ATTR_BATTERY = "battery" +ATTR_BATTERY_STATUS = "battery_status" +ATTR_DEVICE_NAME = "device_name" +ATTR_DEVICE_STATUS = "device_status" +ATTR_LOW_POWER_MODE = "low_power_mode" +ATTR_OWNER_NAME = "owner_fullname" + +# services +SERVICE_ICLOUD_PLAY_SOUND = "play_sound" +SERVICE_ICLOUD_DISPLAY_MESSAGE = "display_message" +SERVICE_ICLOUD_LOST_DEVICE = "lost_device" +SERVICE_ICLOUD_UPDATE = "update" +ATTR_ACCOUNT = "account" +ATTR_LOST_DEVICE_MESSAGE = "message" +ATTR_LOST_DEVICE_NUMBER = "number" +ATTR_LOST_DEVICE_SOUND = "sound" + +SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ACCOUNT): cv.string}) + +SERVICE_SCHEMA_PLAY_SOUND = vol.Schema( + {vol.Required(ATTR_ACCOUNT): cv.string, vol.Required(ATTR_DEVICE_NAME): cv.string} +) + +SERVICE_SCHEMA_DISPLAY_MESSAGE = vol.Schema( + { + vol.Required(ATTR_ACCOUNT): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_LOST_DEVICE_MESSAGE): cv.string, + vol.Optional(ATTR_LOST_DEVICE_SOUND): cv.boolean, + } +) + +SERVICE_SCHEMA_LOST_DEVICE = vol.Schema( + { + vol.Required(ATTR_ACCOUNT): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_LOST_DEVICE_NUMBER): cv.string, + vol.Required(ATTR_LOST_DEVICE_MESSAGE): cv.string, + } +) + +ACCOUNT_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_ACCOUNT_NAME): cv.string, + vol.Optional(CONF_MAX_INTERVAL, default=DEFAULT_MAX_INTERVAL): cv.positive_int, + vol.Optional( + CONF_GPS_ACCURACY_THRESHOLD, default=DEFAULT_GPS_ACCURACY_THRESHOLD + ): cv.positive_int, + } +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]))}, + extra=vol.ALLOW_EXTRA, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up iCloud from legacy config file.""" + + conf = config.get(DOMAIN) + if conf is None: + return True + + for account_conf in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=account_conf + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up an iCloud account from a config entry.""" + + hass.data.setdefault(DOMAIN, {}) + + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + account_name = entry.data.get(CONF_ACCOUNT_NAME) + max_interval = entry.data[CONF_MAX_INTERVAL] + gps_accuracy_threshold = entry.data[CONF_GPS_ACCURACY_THRESHOLD] + + icloud_dir = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + account = IcloudAccount( + hass, + username, + password, + icloud_dir, + account_name, + max_interval, + gps_accuracy_threshold, + ) + await hass.async_add_executor_job(account.setup) + hass.data[DOMAIN][username] = account + + for component in ICLOUD_COMPONENTS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + def play_sound(service: ServiceDataType) -> None: + """Play sound on the device.""" + account = service.data[ATTR_ACCOUNT] + device_name = service.data.get(ATTR_DEVICE_NAME) + device_name = slugify(device_name.replace(" ", "", 99)) + + for device in _get_account(account).get_devices_with_name(device_name): + device.play_sound() + + def display_message(service: ServiceDataType) -> None: + """Display a message on the device.""" + account = service.data[ATTR_ACCOUNT] + device_name = service.data.get(ATTR_DEVICE_NAME) + device_name = slugify(device_name.replace(" ", "", 99)) + message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) + sound = service.data.get(ATTR_LOST_DEVICE_SOUND, False) + + for device in _get_account(account).get_devices_with_name(device_name): + device.display_message(message, sound) + + def lost_device(service: ServiceDataType) -> None: + """Make the device in lost state.""" + account = service.data[ATTR_ACCOUNT] + device_name = service.data.get(ATTR_DEVICE_NAME) + device_name = slugify(device_name.replace(" ", "", 99)) + number = service.data.get(ATTR_LOST_DEVICE_NUMBER) + message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) + + for device in _get_account(account).get_devices_with_name(device_name): + device.lost_device(number, message) + + def update_account(service: ServiceDataType) -> None: + """Call the update function of an iCloud account.""" + account = service.data.get(ATTR_ACCOUNT) + + if account is None: + for account in hass.data[DOMAIN].values(): + account.keep_alive() + else: + _get_account(account).keep_alive() + + def _get_account(account_identifier: str) -> any: + if account_identifier is None: + return None + + icloud_account = hass.data[DOMAIN].get(account_identifier, None) + if icloud_account is None: + for account in hass.data[DOMAIN].values(): + if account.name == account_identifier: + icloud_account = account + + if icloud_account is None: + raise Exception( + "No iCloud account with username or name " + account_identifier + ) + return icloud_account + + hass.services.async_register( + DOMAIN, SERVICE_ICLOUD_PLAY_SOUND, play_sound, schema=SERVICE_SCHEMA_PLAY_SOUND + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ICLOUD_DISPLAY_MESSAGE, + display_message, + schema=SERVICE_SCHEMA_DISPLAY_MESSAGE, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ICLOUD_LOST_DEVICE, + lost_device, + schema=SERVICE_SCHEMA_LOST_DEVICE, + ) + + hass.services.async_register( + DOMAIN, SERVICE_ICLOUD_UPDATE, update_account, schema=SERVICE_SCHEMA + ) + + return True + + +class IcloudAccount: + """Representation of an iCloud account.""" + + def __init__( + self, + hass: HomeAssistantType, + username: str, + password: str, + icloud_dir: Store, + account_name: str, + max_interval: int, + gps_accuracy_threshold: int, + ): + """Initialize an iCloud account.""" + self.hass = hass + self._username = username + self._password = password + self._name = account_name or slugify(username.partition("@")[0]) + self._fetch_interval = max_interval + self._max_interval = max_interval + self._gps_accuracy_threshold = gps_accuracy_threshold + + self._icloud_dir = icloud_dir + + self.api = None + self._owner_fullname = None + self._family_members_fullname = {} + self._devices = {} + + self.unsub_device_tracker = None + + def setup(self): + """Set up an iCloud account.""" + try: + self.api = PyiCloudService( + self._username, self._password, self._icloud_dir.path + ) + except PyiCloudFailedLoginException as error: + self.api = None + _LOGGER.error("Error logging into iCloud Service: %s", error) + return + + user_info = None + try: + # Gets device owners infos + user_info = self.api.devices.response["userInfo"] + except PyiCloudNoDevicesException: + _LOGGER.error("No iCloud Devices found") + + self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" + + self._family_members_fullname = {} + for prs_id, member in user_info["membersInfo"].items(): + self._family_members_fullname[ + prs_id + ] = f"{member['firstName']} {member['lastName']}" + + self._devices = {} + self.update_devices() + + def update_devices(self) -> None: + """Update iCloud devices.""" + if self.api is None: + return + + api_devices = {} + try: + api_devices = self.api.devices + except PyiCloudNoDevicesException: + _LOGGER.error("No iCloud Devices found") + + # Gets devices infos + for device in api_devices: + status = device.status(DEVICE_STATUS_SET) + device_id = status[DEVICE_ID] + device_name = status[DEVICE_NAME] + + if self._devices.get(device_id, None) is not None: + # Seen device -> updating + _LOGGER.debug("Updating iCloud device: %s", device_name) + self._devices[device_id].update(status) + else: + # New device, should be unique + _LOGGER.debug( + "Adding iCloud device: %s [model: %s]", + device_name, + status[DEVICE_RAW_DEVICE_MODEL], + ) + self._devices[device_id] = IcloudDevice(self, device, status) + self._devices[device_id].update(status) + + dispatcher_send(self.hass, TRACKER_UPDATE) + self._fetch_interval = self._determine_interval() + track_point_in_utc_time( + self.hass, + self.keep_alive, + utcnow() + timedelta(minutes=self._fetch_interval), + ) + + def _determine_interval(self) -> int: + """Calculate new interval between two API fetch (in minutes).""" + intervals = {} + for device in self._devices.values(): + if device.location is None: + continue + + current_zone = run_callback_threadsafe( + self.hass.loop, + async_active_zone, + self.hass, + device.location[DEVICE_LOCATION_LATITUDE], + device.location[DEVICE_LOCATION_LONGITUDE], + ).result() + + if current_zone is not None: + intervals[device.name] = self._max_interval + continue + + zones = ( + self.hass.states.get(entity_id) + for entity_id in sorted(self.hass.states.entity_ids("zone")) + ) + + distances = [] + for zone_state in zones: + zone_state_lat = zone_state.attributes[DEVICE_LOCATION_LATITUDE] + zone_state_long = zone_state.attributes[DEVICE_LOCATION_LONGITUDE] + zone_distance = distance( + device.location[DEVICE_LOCATION_LATITUDE], + device.location[DEVICE_LOCATION_LONGITUDE], + zone_state_lat, + zone_state_long, + ) + distances.append(round(zone_distance / 1000, 1)) + + if not distances: + continue + mindistance = min(distances) + + # Calculate out how long it would take for the device to drive + # to the nearest zone at 120 km/h: + interval = round(mindistance / 2, 0) + + # Never poll more than once per minute + interval = max(interval, 1) + + if interval > 180: + # Three hour drive? + # This is far enough that they might be flying + interval = self._max_interval + + if ( + device.battery_level is not None + and device.battery_level <= 33 + and mindistance > 3 + ): + # Low battery - let's check half as often + interval = interval * 2 + + intervals[device.name] = interval + + return max( + int(min(intervals.items(), key=operator.itemgetter(1))[1]), + self._max_interval, + ) + + def keep_alive(self, now=None) -> None: + """Keep the API alive.""" + if self.api is None: + self.setup() + + if self.api is None: + return + + self.api.authenticate() + self.update_devices() + + def get_devices_with_name(self, name: str) -> [any]: + """Get devices by name.""" + result = [] + name_slug = slugify(name.replace(" ", "", 99)) + for device in self.devices.values(): + if slugify(device.name.replace(" ", "", 99)) == name_slug: + result.append(device) + if not result: + raise Exception("No device with name " + name) + return result + + @property + def name(self) -> str: + """Return the account name.""" + return self._name + + @property + def username(self) -> str: + """Return the account username.""" + return self._username + + @property + def owner_fullname(self) -> str: + """Return the account owner fullname.""" + return self._owner_fullname + + @property + def family_members_fullname(self) -> Dict[str, str]: + """Return the account family members fullname.""" + return self._family_members_fullname + + @property + def fetch_interval(self) -> int: + """Return the account fetch interval.""" + return self._fetch_interval + + @property + def devices(self) -> Dict[str, any]: + """Return the account devices.""" + return self._devices + + +class IcloudDevice: + """Representation of a iCloud device.""" + + def __init__(self, account: IcloudAccount, device: AppleDevice, status): + """Initialize the iCloud device.""" + self._account = account + account_name = account.name + + self._device = device + self._status = status + + self._name = self._status[DEVICE_NAME] + self._device_id = self._status[DEVICE_ID] + self._device_class = self._status[DEVICE_CLASS] + self._device_model = self._status[DEVICE_DISPLAY_NAME] + + if self._status[DEVICE_PERSON_ID]: + owner_fullname = account.family_members_fullname[ + self._status[DEVICE_PERSON_ID] + ] + else: + owner_fullname = account.owner_fullname + + self._battery_level = None + self._battery_status = None + self._location = None + + self._attrs = { + ATTR_ATTRIBUTION: ATTRIBUTION, + CONF_ACCOUNT_NAME: account_name, + ATTR_ACCOUNT_FETCH_INTERVAL: self._account.fetch_interval, + ATTR_DEVICE_NAME: self._device_model, + ATTR_DEVICE_STATUS: None, + ATTR_OWNER_NAME: owner_fullname, + } + + def update(self, status) -> None: + """Update the iCloud device.""" + self._status = status + + self._status[ATTR_ACCOUNT_FETCH_INTERVAL] = self._account.fetch_interval + + device_status = DEVICE_STATUS_CODES.get(self._status[DEVICE_STATUS], "error") + self._attrs[ATTR_DEVICE_STATUS] = device_status + + if self._status[DEVICE_BATTERY_STATUS] != "Unknown": + self._battery_level = int(self._status.get(DEVICE_BATTERY_LEVEL, 0) * 100) + self._battery_status = self._status[DEVICE_BATTERY_STATUS] + low_power_mode = self._status[DEVICE_LOW_POWER_MODE] + + self._attrs[ATTR_BATTERY] = self._battery_level + self._attrs[ATTR_BATTERY_STATUS] = self._battery_status + self._attrs[ATTR_LOW_POWER_MODE] = low_power_mode + + if ( + self._status[DEVICE_LOCATION] + and self._status[DEVICE_LOCATION][DEVICE_LOCATION_LATITUDE] + ): + location = self._status[DEVICE_LOCATION] + self._location = location + + def play_sound(self) -> None: + """Play sound on the device.""" + if self._account.api is None: + return + + self._account.api.authenticate() + _LOGGER.debug("Playing sound for %s", self.name) + self.device.play_sound() + + def display_message(self, message: str, sound: bool = False) -> None: + """Display a message on the device.""" + if self._account.api is None: + return + + self._account.api.authenticate() + _LOGGER.debug("Displaying message for %s", self.name) + self.device.display_message("Subject not working", message, sound) + + def lost_device(self, number: str, message: str) -> None: + """Make the device in lost state.""" + if self._account.api is None: + return + + self._account.api.authenticate() + if self._status[DEVICE_LOST_MODE_CAPABLE]: + _LOGGER.debug("Make device lost for %s", self.name) + self.device.lost_device(number, message, None) + else: + _LOGGER.error("Cannot make device lost for %s", self.name) + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device_id + + @property + def dev_id(self) -> str: + """Return the device ID.""" + return self._device_id + + @property + def name(self) -> str: + """Return the Apple device name.""" + return self._name + + @property + def device(self) -> AppleDevice: + """Return the Apple device.""" + return self._device + + @property + def device_class(self) -> str: + """Return the Apple device class.""" + return self._device_class + + @property + def device_model(self) -> str: + """Return the Apple device model.""" + return self._device_model + + @property + def battery_level(self) -> int: + """Return the Apple device battery level.""" + return self._battery_level + + @property + def battery_status(self) -> str: + """Return the Apple device battery status.""" + return self._battery_status + + @property + def location(self) -> Dict[str, any]: + """Return the Apple device location.""" + return self._location + + @property + def state_attributes(self) -> Dict[str, any]: + """Return the attributes.""" + return self._attrs diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py new file mode 100644 index 00000000000000..cf05c07e26f89d --- /dev/null +++ b/homeassistant/components/icloud/config_flow.py @@ -0,0 +1,230 @@ +"""Config flow to configure the iCloud integration.""" +import logging +import os + +from pyicloud import PyiCloudService +from pyicloud.exceptions import PyiCloudException, PyiCloudFailedLoginException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.util import slugify + +from .const import ( + CONF_ACCOUNT_NAME, + CONF_GPS_ACCURACY_THRESHOLD, + CONF_MAX_INTERVAL, + DEFAULT_GPS_ACCURACY_THRESHOLD, + DEFAULT_MAX_INTERVAL, + STORAGE_KEY, + STORAGE_VERSION, +) +from .const import DOMAIN # pylint: disable=unused-import + +CONF_TRUSTED_DEVICE = "trusted_device" +CONF_VERIFICATION_CODE = "verification_code" + +_LOGGER = logging.getLogger(__name__) + + +class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a iCloud config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize iCloud config flow.""" + self.api = None + self._username = None + self._password = None + self._account_name = None + self._max_interval = None + self._gps_accuracy_threshold = None + + self._trusted_device = None + self._verification_code = None + + def _configuration_exists(self, username: str, account_name: str) -> bool: + """Return True if username or account_name exists in configuration.""" + for entry in self._async_current_entries(): + if ( + entry.data[CONF_USERNAME] == username + or entry.data.get(CONF_ACCOUNT_NAME) == account_name + or slugify(entry.data[CONF_USERNAME].partition("@")[0]) == account_name + ): + return True + return False + + async def _show_setup_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ): str, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + } + ), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + icloud_dir = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + if not os.path.exists(icloud_dir.path): + await self.hass.async_add_executor_job(os.makedirs, icloud_dir.path) + + if user_input is None: + return await self._show_setup_form(user_input, errors) + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + self._account_name = user_input.get(CONF_ACCOUNT_NAME) + self._max_interval = user_input.get(CONF_MAX_INTERVAL, DEFAULT_MAX_INTERVAL) + self._gps_accuracy_threshold = user_input.get( + CONF_GPS_ACCURACY_THRESHOLD, DEFAULT_GPS_ACCURACY_THRESHOLD + ) + + if self._configuration_exists(self._username, self._account_name): + errors[CONF_USERNAME] = "username_exists" + return await self._show_setup_form(user_input, errors) + + try: + self.api = await self.hass.async_add_executor_job( + PyiCloudService, self._username, self._password, icloud_dir.path + ) + except PyiCloudFailedLoginException as error: + _LOGGER.error("Error logging into iCloud service: %s", error) + self.api = None + errors[CONF_USERNAME] = "login" + return await self._show_setup_form(user_input, errors) + + if self.api.requires_2fa: + return await self.async_step_trusted_device() + + return self.async_create_entry( + title=self._username, + data={ + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_ACCOUNT_NAME: self._account_name, + CONF_MAX_INTERVAL: self._max_interval, + CONF_GPS_ACCURACY_THRESHOLD: self._gps_accuracy_threshold, + }, + ) + + async def async_step_import(self, user_input): + """Import a config entry.""" + if self._configuration_exists( + user_input[CONF_USERNAME], user_input.get(CONF_ACCOUNT_NAME) + ): + return self.async_abort(reason="username_exists") + + return await self.async_step_user(user_input) + + async def async_step_trusted_device(self, user_input=None, errors=None): + """We need a trusted device.""" + if errors is None: + errors = {} + + trusted_devices = await self.hass.async_add_executor_job( + getattr, self.api, "trusted_devices" + ) + trusted_devices_for_form = {} + for i, device in enumerate(trusted_devices): + trusted_devices_for_form[i] = device.get( + "deviceName", f"SMS to {device.get('phoneNumber')}" + ) + + if user_input is None: + return await self._show_trusted_device_form( + trusted_devices_for_form, user_input, errors + ) + + self._trusted_device = trusted_devices[int(user_input[CONF_TRUSTED_DEVICE])] + + if not await self.hass.async_add_executor_job( + self.api.send_verification_code, self._trusted_device + ): + _LOGGER.error("Failed to send verification code") + self._trusted_device = None + errors[CONF_TRUSTED_DEVICE] = "send_verification_code" + + return await self._show_trusted_device_form( + trusted_devices_for_form, user_input, errors + ) + + return await self.async_step_verification_code() + + async def _show_trusted_device_form( + self, trusted_devices, user_input=None, errors=None + ): + """Show the trusted_device form to the user.""" + + return self.async_show_form( + step_id=CONF_TRUSTED_DEVICE, + data_schema=vol.Schema( + { + vol.Required(CONF_TRUSTED_DEVICE): vol.All( + vol.Coerce(int), vol.In(trusted_devices) + ) + } + ), + errors=errors or {}, + ) + + async def async_step_verification_code(self, user_input=None): + """Ask the verification code to the user.""" + errors = {} + + if user_input is None: + return await self._show_verification_code_form(user_input) + + self._verification_code = user_input[CONF_VERIFICATION_CODE] + + try: + if not await self.hass.async_add_executor_job( + self.api.validate_verification_code, + self._trusted_device, + self._verification_code, + ): + raise PyiCloudException("The code you entered is not valid.") + except PyiCloudException as error: + # Reset to the initial 2FA state to allow the user to retry + _LOGGER.error("Failed to verify verification code: %s", error) + self._trusted_device = None + self._verification_code = None + errors["base"] = "validate_verification_code" + + return await self.async_step_trusted_device(None, errors) + + return await self.async_step_user( + { + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_ACCOUNT_NAME: self._account_name, + CONF_MAX_INTERVAL: self._max_interval, + CONF_GPS_ACCURACY_THRESHOLD: self._gps_accuracy_threshold, + } + ) + + async def _show_verification_code_form(self, user_input=None): + """Show the verification_code form to the user.""" + + return self.async_show_form( + step_id=CONF_VERIFICATION_CODE, + data_schema=vol.Schema({vol.Required(CONF_VERIFICATION_CODE): str}), + errors=None, + ) diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index fe8010df703b43..4e99a378077c9a 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -1,6 +1,85 @@ -"""Constants for the iCloud component.""" +"""iCloud component constants.""" + DOMAIN = "icloud" -SERVICE_LOST_IPHONE = "lost_iphone" -SERVICE_UPDATE = "update" -SERVICE_RESET_ACCOUNT = "reset_account" -SERVICE_SET_INTERVAL = "set_interval" +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" + +CONF_ACCOUNT_NAME = "account_name" +CONF_MAX_INTERVAL = "max_interval" +CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold" + +DEFAULT_MAX_INTERVAL = 30 # min +DEFAULT_GPS_ACCURACY_THRESHOLD = 500 # meters + +# to store the cookie +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +# Next PR will add sensor +ICLOUD_COMPONENTS = ["device_tracker"] + +# pyicloud.AppleDevice status +DEVICE_BATTERY_LEVEL = "batteryLevel" +DEVICE_BATTERY_STATUS = "batteryStatus" +DEVICE_CLASS = "deviceClass" +DEVICE_DISPLAY_NAME = "deviceDisplayName" +DEVICE_ID = "id" +DEVICE_LOCATION = "location" +DEVICE_LOCATION_HORIZONTAL_ACCURACY = "horizontalAccuracy" +DEVICE_LOCATION_LATITUDE = "latitude" +DEVICE_LOCATION_LONGITUDE = "longitude" +DEVICE_LOST_MODE_CAPABLE = "lostModeCapable" +DEVICE_LOW_POWER_MODE = "lowPowerMode" +DEVICE_NAME = "name" +DEVICE_PERSON_ID = "prsId" +DEVICE_RAW_DEVICE_MODEL = "rawDeviceModel" +DEVICE_STATUS = "deviceStatus" + +DEVICE_STATUS_SET = [ + "features", + "maxMsgChar", + "darkWake", + "fmlyShare", + DEVICE_STATUS, + "remoteLock", + "activationLocked", + DEVICE_CLASS, + DEVICE_ID, + "deviceModel", + DEVICE_RAW_DEVICE_MODEL, + "passcodeLength", + "canWipeAfterLock", + "trackingInfo", + DEVICE_LOCATION, + "msg", + DEVICE_BATTERY_LEVEL, + "remoteWipe", + "thisDevice", + "snd", + DEVICE_PERSON_ID, + "wipeInProgress", + DEVICE_LOW_POWER_MODE, + "lostModeEnabled", + "isLocating", + DEVICE_LOST_MODE_CAPABLE, + "mesg", + DEVICE_NAME, + DEVICE_BATTERY_STATUS, + "lockedTimestamp", + "lostTimestamp", + "locationCapable", + DEVICE_DISPLAY_NAME, + "lostDevice", + "deviceColor", + "wipedTimestamp", + "modelDisplayName", + "locationEnabled", + "isMac", + "locFoundEnabled", +] + +DEVICE_STATUS_CODES = { + "200": "online", + "201": "offline", + "203": "pending", + "204": "unregistered", +} diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 3d9fb4715da0b6..4be34728c6dfde 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,544 +1,132 @@ -"""Platform that supports scanning iCloud.""" +"""Support for tracking for iCloud devices.""" import logging -import os -import random -from pyicloud import PyiCloudService -from pyicloud.exceptions import ( - PyiCloudException, - PyiCloudFailedLoginException, - PyiCloudNoDevicesException, -) -import voluptuous as vol - -from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.components.device_tracker.const import ( - ATTR_ATTRIBUTES, - ENTITY_ID_FORMAT, -) -from homeassistant.components.device_tracker.legacy import DeviceScanner -from homeassistant.components.zone import async_active_zone -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_utc_time_change -from homeassistant.util import slugify -from homeassistant.util.async_ import run_callback_threadsafe -import homeassistant.util.dt as dt_util -from homeassistant.util.location import distance +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.const import CONF_USERNAME +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType +from . import IcloudDevice from .const import ( + DEVICE_LOCATION_HORIZONTAL_ACCURACY, + DEVICE_LOCATION_LATITUDE, + DEVICE_LOCATION_LONGITUDE, DOMAIN, - SERVICE_LOST_IPHONE, - SERVICE_RESET_ACCOUNT, - SERVICE_SET_INTERVAL, - SERVICE_UPDATE, + TRACKER_UPDATE, ) _LOGGER = logging.getLogger(__name__) -CONF_ACCOUNTNAME = "account_name" -CONF_MAX_INTERVAL = "max_interval" -CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold" - -# entity attributes -ATTR_ACCOUNTNAME = "account_name" -ATTR_INTERVAL = "interval" -ATTR_DEVICENAME = "device_name" -ATTR_BATTERY = "battery" -ATTR_DISTANCE = "distance" -ATTR_DEVICESTATUS = "device_status" -ATTR_LOWPOWERMODE = "low_power_mode" -ATTR_BATTERYSTATUS = "battery_status" - -ICLOUDTRACKERS = {} -_CONFIGURING = {} +async def async_setup_scanner( + hass: HomeAssistantType, config, see, discovery_info=None +): + """Old way of setting up the iCloud tracker.""" + pass -DEVICESTATUSSET = [ - "features", - "maxMsgChar", - "darkWake", - "fmlyShare", - "deviceStatus", - "remoteLock", - "activationLocked", - "deviceClass", - "id", - "deviceModel", - "rawDeviceModel", - "passcodeLength", - "canWipeAfterLock", - "trackingInfo", - "location", - "msg", - "batteryLevel", - "remoteWipe", - "thisDevice", - "snd", - "prsId", - "wipeInProgress", - "lowPowerMode", - "lostModeEnabled", - "isLocating", - "lostModeCapable", - "mesg", - "name", - "batteryStatus", - "lockedTimestamp", - "lostTimestamp", - "locationCapable", - "deviceDisplayName", - "lostDevice", - "deviceColor", - "wipedTimestamp", - "modelDisplayName", - "locationEnabled", - "isMac", - "locFoundEnabled", -] - -DEVICESTATUSCODES = { - "200": "online", - "201": "offline", - "203": "pending", - "204": "unregistered", -} - -SERVICE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]), - vol.Optional(ATTR_DEVICENAME): cv.slugify, - vol.Optional(ATTR_INTERVAL): cv.positive_int, - } -) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(ATTR_ACCOUNTNAME): cv.slugify, - vol.Optional(CONF_MAX_INTERVAL, default=30): cv.positive_int, - vol.Optional(CONF_GPS_ACCURACY_THRESHOLD, default=1000): cv.positive_int, - } -) - - -def setup_scanner(hass, config: dict, see, discovery_info=None): - """Set up the iCloud Scanner.""" - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - account = config.get(CONF_ACCOUNTNAME, slugify(username.partition("@")[0])) - max_interval = config.get(CONF_MAX_INTERVAL) - gps_accuracy_threshold = config.get(CONF_GPS_ACCURACY_THRESHOLD) - - icloudaccount = Icloud( - hass, username, password, account, max_interval, gps_accuracy_threshold, see - ) - - if icloudaccount.api is not None: - ICLOUDTRACKERS[account] = icloudaccount - - else: - _LOGGER.error("No ICLOUDTRACKERS added") - return False +async def async_setup_entry(hass: HomeAssistantType, entry, async_add_entities): + """Configure a dispatcher connection based on a config entry.""" + username = entry.data[CONF_USERNAME] - def lost_iphone(call): - """Call the lost iPhone function if the device is found.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - devicename = call.data.get(ATTR_DEVICENAME) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].lost_iphone(devicename) + for device in hass.data[DOMAIN][username].devices.values(): + if device.location is None: + _LOGGER.debug("No position found for device %s", device.name) + continue - hass.services.register( - DOMAIN, SERVICE_LOST_IPHONE, lost_iphone, schema=SERVICE_SCHEMA - ) + _LOGGER.debug("Adding device_tracker for %s", device.name) - def update_icloud(call): - """Call the update function of an iCloud account.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - devicename = call.data.get(ATTR_DEVICENAME) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].update_icloud(devicename) + async_add_entities([IcloudTrackerEntity(device)]) - hass.services.register(DOMAIN, SERVICE_UPDATE, update_icloud, schema=SERVICE_SCHEMA) - def reset_account_icloud(call): - """Reset an iCloud account.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].reset_account_icloud() +class IcloudTrackerEntity(TrackerEntity): + """Represent a tracked device.""" - hass.services.register( - DOMAIN, SERVICE_RESET_ACCOUNT, reset_account_icloud, schema=SERVICE_SCHEMA - ) + def __init__(self, device: IcloudDevice): + """Set up the iCloud tracker entity.""" + self._device = device + self._unsub_dispatcher = None - def setinterval(call): - """Call the update function of an iCloud account.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - interval = call.data.get(ATTR_INTERVAL) - devicename = call.data.get(ATTR_DEVICENAME) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].setinterval(interval, devicename) + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._device.unique_id}_tracker" - hass.services.register( - DOMAIN, SERVICE_SET_INTERVAL, setinterval, schema=SERVICE_SCHEMA - ) + @property + def name(self): + """Return the name of the device.""" + return self._device.name - # Tells the bootstrapper that the component was successfully initialized - return True + @property + def location_accuracy(self): + """Return the location accuracy of the device.""" + return self._device.location[DEVICE_LOCATION_HORIZONTAL_ACCURACY] + @property + def latitude(self): + """Return latitude value of the device.""" + return self._device.location[DEVICE_LOCATION_LATITUDE] -class Icloud(DeviceScanner): - """Representation of an iCloud account.""" - - def __init__( - self, hass, username, password, name, max_interval, gps_accuracy_threshold, see - ): - """Initialize an iCloud account.""" - self.hass = hass - self.username = username - self.password = password - self.api = None - self.accountname = name - self.devices = {} - self.seen_devices = {} - self._overridestates = {} - self._intervals = {} - self._max_interval = max_interval - self._gps_accuracy_threshold = gps_accuracy_threshold - self.see = see - - self._trusted_device = None - self._verification_code = None - - self._attrs = {} - self._attrs[ATTR_ACCOUNTNAME] = name - - self.reset_account_icloud() - - randomseconds = random.randint(10, 59) - track_utc_time_change(self.hass, self.keep_alive, second=randomseconds) - - def reset_account_icloud(self): - """Reset an iCloud account.""" - icloud_dir = self.hass.config.path("icloud") - if not os.path.exists(icloud_dir): - os.makedirs(icloud_dir) - - try: - self.api = PyiCloudService( - self.username, self.password, cookie_directory=icloud_dir, verify=True - ) - except PyiCloudFailedLoginException as error: - self.api = None - _LOGGER.error("Error logging into iCloud Service: %s", error) - return - - try: - self.devices = {} - self._overridestates = {} - self._intervals = {} - for device in self.api.devices: - status = device.status(DEVICESTATUSSET) - _LOGGER.debug("Device Status is %s", status) - devicename = slugify(status["name"].replace(" ", "", 99)) - _LOGGER.info("Adding icloud device: %s", devicename) - if devicename in self.devices: - _LOGGER.error("Multiple devices with name: %s", devicename) - continue - self.devices[devicename] = device - self._intervals[devicename] = 1 - self._overridestates[devicename] = None - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found!") - - def icloud_trusted_device_callback(self, callback_data): - """Handle chosen trusted devices.""" - self._trusted_device = int(callback_data.get("trusted_device")) - self._trusted_device = self.api.trusted_devices[self._trusted_device] - - if not self.api.send_verification_code(self._trusted_device): - _LOGGER.error("Failed to send verification code") - self._trusted_device = None - return - - if self.accountname in _CONFIGURING: - request_id = _CONFIGURING.pop(self.accountname) - configurator = self.hass.components.configurator - configurator.request_done(request_id) - - # Trigger the next step immediately - self.icloud_need_verification_code() - - def icloud_need_trusted_device(self): - """We need a trusted device.""" - configurator = self.hass.components.configurator - if self.accountname in _CONFIGURING: - return - - devicesstring = "" - devices = self.api.trusted_devices - for i, device in enumerate(devices): - devicename = device.get( - "deviceName", "SMS to %s" % device.get("phoneNumber") - ) - devicesstring += f"{i}: {devicename};" - - _CONFIGURING[self.accountname] = configurator.request_config( - f"iCloud {self.accountname}", - self.icloud_trusted_device_callback, - description=( - "Please choose your trusted device by entering" - " the index from this list: " + devicesstring - ), - entity_picture="/static/images/config_icloud.png", - submit_caption="Confirm", - fields=[{"id": "trusted_device", "name": "Trusted Device"}], - ) + @property + def longitude(self): + """Return longitude value of the device.""" + return self._device.location[DEVICE_LOCATION_LONGITUDE] - def icloud_verification_callback(self, callback_data): - """Handle the chosen trusted device.""" - self._verification_code = callback_data.get("code") - - try: - if not self.api.validate_verification_code( - self._trusted_device, self._verification_code - ): - raise PyiCloudException("Unknown failure") - except PyiCloudException as error: - # Reset to the initial 2FA state to allow the user to retry - _LOGGER.error("Failed to verify verification code: %s", error) - self._trusted_device = None - self._verification_code = None - - # Trigger the next step immediately - self.icloud_need_trusted_device() - - if self.accountname in _CONFIGURING: - request_id = _CONFIGURING.pop(self.accountname) - configurator = self.hass.components.configurator - configurator.request_done(request_id) - - def icloud_need_verification_code(self): - """Return the verification code.""" - configurator = self.hass.components.configurator - if self.accountname in _CONFIGURING: - return - - _CONFIGURING[self.accountname] = configurator.request_config( - f"iCloud {self.accountname}", - self.icloud_verification_callback, - description=("Please enter the validation code:"), - entity_picture="/static/images/config_icloud.png", - submit_caption="Confirm", - fields=[{"id": "code", "name": "code"}], - ) - - def keep_alive(self, now): - """Keep the API alive.""" - if self.api is None: - self.reset_account_icloud() - - if self.api is None: - return - - if self.api.requires_2fa: - try: - if self._trusted_device is None: - self.icloud_need_trusted_device() - return - - if self._verification_code is None: - self.icloud_need_verification_code() - return - - self.api.authenticate() - if self.api.requires_2fa: - raise Exception("Unknown failure") - - self._trusted_device = None - self._verification_code = None - except PyiCloudException as error: - _LOGGER.error("Error setting up 2FA: %s", error) - else: - self.api.authenticate() - - currentminutes = dt_util.now().hour * 60 + dt_util.now().minute - try: - for devicename in self.devices: - interval = self._intervals.get(devicename, 1) - if (currentminutes % interval == 0) or ( - interval > 10 and currentminutes % interval in [2, 4] - ): - self.update_device(devicename) - except ValueError: - _LOGGER.debug("iCloud API returned an error") - - def determine_interval(self, devicename, latitude, longitude, battery): - """Calculate new interval.""" - currentzone = run_callback_threadsafe( - self.hass.loop, async_active_zone, self.hass, latitude, longitude - ).result() - - if ( - currentzone is not None - and currentzone == self._overridestates.get(devicename) - ) or (currentzone is None and self._overridestates.get(devicename) == "away"): - return + @property + def should_poll(self): + """No polling needed.""" + return False - zones = ( - self.hass.states.get(entity_id) - for entity_id in sorted(self.hass.states.entity_ids("zone")) + @property + def battery_level(self): + """Return the battery level of the device.""" + return self._device.battery_level + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + @property + def icon(self): + """Return the icon.""" + return icon_for_icloud_device(self._device) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return self._device.state_attributes + + @property + def device_info(self): + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Apple", + "model": self._device.device_model, + } + + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, TRACKER_UPDATE, self.async_write_ha_state ) - distances = [] - for zone_state in zones: - zone_state_lat = zone_state.attributes["latitude"] - zone_state_long = zone_state.attributes["longitude"] - zone_distance = distance( - latitude, longitude, zone_state_lat, zone_state_long - ) - distances.append(round(zone_distance / 1000, 1)) + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + self._unsub_dispatcher() - if distances: - mindistance = min(distances) - else: - mindistance = None - self._overridestates[devicename] = None - - if currentzone is not None: - self._intervals[devicename] = self._max_interval - return - - if mindistance is None: - return - - # Calculate out how long it would take for the device to drive to the - # nearest zone at 120 km/h: - interval = round(mindistance / 2, 0) - - # Never poll more than once per minute - interval = max(interval, 1) - - if interval > 180: - # Three hour drive? This is far enough that they might be flying - interval = 30 - - if battery is not None and battery <= 33 and mindistance > 3: - # Low battery - let's check half as often - interval = interval * 2 - - self._intervals[devicename] = interval - - def update_device(self, devicename): - """Update the device_tracker entity.""" - # An entity will not be created by see() when track=false in - # 'known_devices.yaml', but we need to see() it at least once - entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename)) - if entity is None and devicename in self.seen_devices: - return - attrs = {} - kwargs = {} - - if self.api is None: - return - - try: - for device in self.api.devices: - if str(device) != str(self.devices[devicename]): - continue - - status = device.status(DEVICESTATUSSET) - _LOGGER.debug("Device Status is %s", status) - dev_id = status["name"].replace(" ", "", 99) - dev_id = slugify(dev_id) - attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get( - status["deviceStatus"], "error" - ) - attrs[ATTR_LOWPOWERMODE] = status["lowPowerMode"] - attrs[ATTR_BATTERYSTATUS] = status["batteryStatus"] - attrs[ATTR_ACCOUNTNAME] = self.accountname - status = device.status(DEVICESTATUSSET) - battery = status.get("batteryLevel", 0) * 100 - location = status["location"] - if location and location["horizontalAccuracy"]: - horizontal_accuracy = int(location["horizontalAccuracy"]) - if horizontal_accuracy < self._gps_accuracy_threshold: - self.determine_interval( - devicename, - location["latitude"], - location["longitude"], - battery, - ) - interval = self._intervals.get(devicename, 1) - attrs[ATTR_INTERVAL] = interval - accuracy = location["horizontalAccuracy"] - kwargs["dev_id"] = dev_id - kwargs["host_name"] = status["name"] - kwargs["gps"] = (location["latitude"], location["longitude"]) - kwargs["battery"] = battery - kwargs["gps_accuracy"] = accuracy - kwargs[ATTR_ATTRIBUTES] = attrs - self.see(**kwargs) - self.seen_devices[devicename] = True - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found") - - def lost_iphone(self, devicename): - """Call the lost iPhone function if the device is found.""" - if self.api is None: - return - - self.api.authenticate() - for device in self.api.devices: - if str(device) == str(self.devices[devicename]): - _LOGGER.info("Playing Lost iPhone sound for %s", devicename) - device.play_sound() - - def update_icloud(self, devicename=None): - """Request device information from iCloud and update device_tracker.""" - if self.api is None: - return - - try: - if devicename is not None: - if devicename in self.devices: - self.update_device(devicename) - else: - _LOGGER.error( - "devicename %s unknown for account %s", - devicename, - self._attrs[ATTR_ACCOUNTNAME], - ) - else: - for device in self.devices: - self.update_device(device) - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found") +def icon_for_icloud_device(icloud_device: IcloudDevice) -> str: + """Return a battery icon valid identifier.""" + switcher = { + "iPad": "mdi:tablet-ipad", + "iPhone": "mdi:cellphone-iphone", + "iPod": "mdi:ipod", + "iMac": "mdi:desktop-mac", + "MacBookPro": "mdi:laptop-mac", + } - def setinterval(self, interval=None, devicename=None): - """Set the interval of the given devices.""" - devs = [devicename] if devicename else self.devices - for device in devs: - devid = f"{DOMAIN}.{device}" - devicestate = self.hass.states.get(devid) - if interval is not None: - if devicestate is not None: - self._overridestates[device] = run_callback_threadsafe( - self.hass.loop, - async_active_zone, - self.hass, - float(devicestate.attributes.get("latitude", 0)), - float(devicestate.attributes.get("longitude", 0)), - ).result() - if self._overridestates[device] is None: - self._overridestates[device] = "away" - self._intervals[device] = interval - else: - self._overridestates[device] = None - self.update_device(device) + return switcher.get(icloud_device.device_class, "mdi:cellphone-link") diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index d3924ee61a8b21..f7295ceae4d0cc 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -1,10 +1,13 @@ { "domain": "icloud", - "name": "Icloud", - "documentation": "https://www.home-assistant.io/integrations/icloud", + "name": "iCloud", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/icloud", "requirements": [ "pyicloud==0.9.1" ], - "dependencies": ["configurator"], - "codeowners": [] -} + "dependencies": [], + "codeowners": [ + "@Quentame" + ] +} \ No newline at end of file diff --git a/homeassistant/components/icloud/services.yaml b/homeassistant/components/icloud/services.yaml index 7b2d3b80e8435b..ce239df7564c91 100644 --- a/homeassistant/components/icloud/services.yaml +++ b/homeassistant/components/icloud/services.yaml @@ -1,39 +1,49 @@ -lost_iphone: - description: Service to play the lost iphone sound on an iDevice. +update: + description: Update iCloud devices. fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. - example: 'iphonebart' + account: + description: Your iCloud account username (email) or account name. + example: 'steve@apple.com' -set_interval: - description: Service to set the interval of an iDevice. +play_sound: + description: Play sound on an Apple device. fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' + account: + description: (required) Your iCloud account username (email) or account name. + example: 'steve@apple.com' device_name: - description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. - example: 'iphonebart' - interval: - description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. - example: 1 + description: (required) The name of the Apple device to play a sound. + example: 'stevesiphone' -update: - description: Service to ask for an update of an iDevice. +display_message: + description: Display a message on an Apple device. fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' + account: + description: (required) Your iCloud account username (email) or account name. + example: 'steve@apple.com' device_name: - description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. - example: 'iphonebart' + description: (required) The name of the Apple device to display the message. + example: 'stevesiphone' + message: + description: (required) The content of your message. + example: 'Hey Steve !' + sound: + description: To make a sound when displaying the message (boolean). + example: 'true' -reset_account: - description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. +lost_device: + description: Make an Apple device in lost state. fields: - account_name: - description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. - example: 'bart' + account: + description: (required) Your iCloud account username (email) or account name. + example: 'steve@apple.com' + device_name: + description: (required) The name of the Apple device to set lost. + example: 'stevesiphone' + number: + description: (required) The phone number to call in lost mode (must contain country code). + example: '+33450020100' + message: + description: (required) The message to display in lost mode. + example: 'Call me' + diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json new file mode 100644 index 00000000000000..343a087738f4da --- /dev/null +++ b/homeassistant/components/icloud/strings.json @@ -0,0 +1,38 @@ +{ + "config": { + "title": "Apple iCloud", + "step": { + "user": { + "title": "iCloud credentials", + "description": "Enter your credentials", + "data": { + "username": "Email", + "password": "Password" + } + }, + "trusted_device": { + "title": "iCloud trusted device", + "description": "Select your trusted device", + "data": { + "trusted_device": "Trusted device" + } + }, + "verification_code": { + "title": "iCloud verification code", + "description": "Please enter the verification code you just received from iCloud", + "data": { + "verification_code": "Verification code" + } + } + }, + "error":{ + "username_exists": "Account already configured", + "login": "Login error: please check your email & password", + "send_verification_code": "Failed to send verification code", + "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again", + }, + "abort":{ + "username_exists": "Account already configured" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cf1c4b55e1924a..2b3940000e7963 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -36,6 +36,7 @@ "huawei_lte", "hue", "iaqualink", + "icloud", "ifttt", "ios", "ipma", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4cb7ca26a87035..3812b8f0b339b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -432,6 +432,9 @@ pyheos==0.6.0 # homeassistant.components.homematic pyhomematic==0.1.62 +# homeassistant.components.icloud +pyicloud==0.9.1 + # homeassistant.components.ipma pyipma==1.2.1 diff --git a/tests/components/icloud/__init__.py b/tests/components/icloud/__init__.py new file mode 100644 index 00000000000000..b85f1017e45d6b --- /dev/null +++ b/tests/components/icloud/__init__.py @@ -0,0 +1 @@ +"""Tests for the iCloud component.""" diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py new file mode 100644 index 00000000000000..b292a9e258cf37 --- /dev/null +++ b/tests/components/icloud/test_config_flow.py @@ -0,0 +1,309 @@ +"""Tests for the iCloud config flow.""" +from unittest.mock import patch, Mock, MagicMock +import pytest + +from pyicloud.exceptions import PyiCloudFailedLoginException + +from homeassistant import data_entry_flow +from homeassistant.components.icloud import config_flow + +from homeassistant.components.icloud.config_flow import ( + CONF_TRUSTED_DEVICE, + CONF_VERIFICATION_CODE, +) +from homeassistant.components.icloud.const import ( + DOMAIN, + CONF_ACCOUNT_NAME, + CONF_GPS_ACCURACY_THRESHOLD, + CONF_MAX_INTERVAL, + DEFAULT_GPS_ACCURACY_THRESHOLD, + DEFAULT_MAX_INTERVAL, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + +USERNAME = "username@me.com" +PASSWORD = "password" +ACCOUNT_NAME = "Account name 1 2 3" +ACCOUNT_NAME_FROM_USERNAME = None +MAX_INTERVAL = 15 +GPS_ACCURACY_THRESHOLD = 250 + +TRUSTED_DEVICES = [ + {"deviceType": "SMS", "areaCode": "", "phoneNumber": "*******58", "deviceId": "1"} +] + + +@pytest.fixture(name="service") +def mock_controller_service(): + """Mock a successful service.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2fa = True + yield service_mock + + +@pytest.fixture(name="service_with_cookie") +def mock_controller_service_with_cookie(): + """Mock a successful service while already authenticate.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2fa = False + service_mock.return_value.trusted_devices = TRUSTED_DEVICES + service_mock.return_value.send_verification_code = Mock(return_value=True) + service_mock.return_value.validate_verification_code = Mock(return_value=True) + yield service_mock + + +@pytest.fixture(name="service_send_verification_code_failed") +def mock_controller_service_send_verification_code_failed(): + """Mock a failed service during sending verification code step.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2fa = False + service_mock.return_value.trusted_devices = TRUSTED_DEVICES + service_mock.return_value.send_verification_code = Mock(return_value=False) + yield service_mock + + +@pytest.fixture(name="service_validate_verification_code_failed") +def mock_controller_service_validate_verification_code_failed(): + """Mock a failed service during validation of verification code step.""" + with patch( + "homeassistant.components.icloud.config_flow.PyiCloudService" + ) as service_mock: + service_mock.return_value.requires_2fa = False + service_mock.return_value.trusted_devices = TRUSTED_DEVICES + service_mock.return_value.send_verification_code = Mock(return_value=True) + service_mock.return_value.validate_verification_code = Mock(return_value=False) + yield service_mock + + +def init_config_flow(hass: HomeAssistantType): + """Init a configuration flow.""" + flow = config_flow.IcloudFlowHandler() + flow.hass = hass + return flow + + +async def test_user(hass: HomeAssistantType, service: MagicMock): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # test with all provided + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_TRUSTED_DEVICE + + +async def test_user_with_cookie( + hass: HomeAssistantType, service_with_cookie: MagicMock +): + """Test user config with presence of a cookie.""" + flow = init_config_flow(hass) + + # test with all provided + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_ACCOUNT_NAME] == ACCOUNT_NAME_FROM_USERNAME + assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL + assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD + + +async def test_import(hass: HomeAssistantType, service: MagicMock): + """Test import step.""" + flow = init_config_flow(hass) + + # import with username and password + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "trusted_device" + + # import with all + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_ACCOUNT_NAME: ACCOUNT_NAME, + CONF_MAX_INTERVAL: MAX_INTERVAL, + CONF_GPS_ACCURACY_THRESHOLD: GPS_ACCURACY_THRESHOLD, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "trusted_device" + + +async def test_import_with_cookie( + hass: HomeAssistantType, service_with_cookie: MagicMock +): + """Test import step with presence of a cookie.""" + flow = init_config_flow(hass) + + # import with username and password + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_ACCOUNT_NAME] == ACCOUNT_NAME_FROM_USERNAME + assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL + assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD + + # import with all + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_ACCOUNT_NAME: ACCOUNT_NAME, + CONF_MAX_INTERVAL: MAX_INTERVAL, + CONF_GPS_ACCURACY_THRESHOLD: GPS_ACCURACY_THRESHOLD, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_ACCOUNT_NAME] == ACCOUNT_NAME + assert result["data"][CONF_MAX_INTERVAL] == MAX_INTERVAL + assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == GPS_ACCURACY_THRESHOLD + + +async def test_abort_if_already_setup(hass: HomeAssistantType): + """Test we abort if the account is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain=DOMAIN, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ).add_to_hass(hass) + + # Should fail, same USERNAME (import) + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "username_exists" + + # Should fail, same ACCOUNT_NAME (import) + result = await flow.async_step_import( + { + CONF_USERNAME: "other_username@icloud.com", + CONF_PASSWORD: PASSWORD, + CONF_ACCOUNT_NAME: ACCOUNT_NAME_FROM_USERNAME, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "username_exists" + + # Should fail, same USERNAME (flow) + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_USERNAME: "username_exists"} + + +async def test_login_failed(hass: HomeAssistantType): + """Test when we have errors during login.""" + flow = init_config_flow(hass) + + with patch( + "pyicloud.base.PyiCloudService.authenticate", + side_effect=PyiCloudFailedLoginException(), + ): + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_USERNAME: "login"} + + +async def test_trusted_device(hass: HomeAssistantType, service: MagicMock): + """Test trusted_device step.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_trusted_device() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_TRUSTED_DEVICE + + +async def test_trusted_device_success(hass: HomeAssistantType, service: MagicMock): + """Test trusted_device step success.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_trusted_device({CONF_TRUSTED_DEVICE: 0}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_VERIFICATION_CODE + + +async def test_send_verification_code_failed( + hass: HomeAssistantType, service_send_verification_code_failed: MagicMock +): + """Test when we have errors during send_verification_code.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_trusted_device({CONF_TRUSTED_DEVICE: 0}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_TRUSTED_DEVICE + assert result["errors"] == {CONF_TRUSTED_DEVICE: "send_verification_code"} + + +async def test_verification_code(hass: HomeAssistantType): + """Test verification_code step.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_verification_code() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_VERIFICATION_CODE + + +async def test_verification_code_success( + hass: HomeAssistantType, service_with_cookie: MagicMock +): + """Test verification_code step success.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_verification_code({CONF_VERIFICATION_CODE: 0}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_ACCOUNT_NAME] is None + assert result["data"][CONF_MAX_INTERVAL] == DEFAULT_MAX_INTERVAL + assert result["data"][CONF_GPS_ACCURACY_THRESHOLD] == DEFAULT_GPS_ACCURACY_THRESHOLD + + +async def test_validate_verification_code_failed( + hass: HomeAssistantType, service_validate_verification_code_failed: MagicMock +): + """Test when we have errors during validate_verification_code.""" + flow = init_config_flow(hass) + await flow.async_step_user({CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}) + + result = await flow.async_step_verification_code({CONF_VERIFICATION_CODE: 0}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == CONF_TRUSTED_DEVICE + assert result["errors"] == {"base": "validate_verification_code"} From 08f128e9c782d46010938dc99cea1043336c1cb8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 17:42:00 +0100 Subject: [PATCH 2343/3953] Sort imports according to PEP8 for components starting with "D" (#29764) * use isort to sort imports for components starting with 'd' * fix isort mistake --- .../components/denon/media_player.py | 2 +- .../components/deutsche_bahn/sensor.py | 3 +- .../device_sun_light_trigger/__init__.py | 10 ++--- homeassistant/components/dht/sensor.py | 4 +- .../components/digital_ocean/__init__.py | 4 +- .../components/digitalloggers/switch.py | 6 +-- homeassistant/components/discord/notify.py | 5 +-- .../components/discovery/__init__.py | 6 +-- .../dlib_face_detect/image_processing.py | 6 ++- .../dlib_face_identify/image_processing.py | 12 +++--- .../components/doods/image_processing.py | 4 +- homeassistant/components/dovado/__init__.py | 8 ++-- .../components/dublin_bus_transport/sensor.py | 8 ++-- homeassistant/components/duckdns/__init__.py | 6 +-- .../components/dwd_weather_warnings/sensor.py | 10 ++--- tests/components/datadog/test_init.py | 6 +-- tests/components/default_config/test_init.py | 4 +- .../device_sun_light_trigger/test_init.py | 9 ++-- tests/components/directv/test_media_player.py | 42 +++++++++---------- tests/components/discovery/test_init.py | 4 +- tests/components/duckdns/test_init.py | 5 ++- 21 files changed, 83 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 7bed8423e8f397..cd9d6e8feb7cbe 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -4,7 +4,7 @@ import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index ad7b40f78dbb37..204518b2ce32b5 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -2,9 +2,8 @@ from datetime import timedelta import logging -import voluptuous as vol - import schiene +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index 9a058cfacc10bb..64831cfac893bf 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -1,11 +1,9 @@ """Support to turn on lights based on the states.""" -import logging from datetime import timedelta +import logging import voluptuous as vol -from homeassistant.core import callback -import homeassistant.util.dt as dt_util from homeassistant.components.light import ( ATTR_PROFILE, ATTR_TRANSITION, @@ -20,12 +18,14 @@ SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change, ) -from homeassistant.helpers.sun import is_up, get_astral_event_next -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.sun import get_astral_event_next, is_up +import homeassistant.util.dt as dt_util DOMAIN = "device_sun_light_trigger" CONF_DEVICE_GROUP = "device_group" diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 648e0e1ed7267a..26b0493cb99055 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -1,13 +1,13 @@ """Support for Adafruit DHT temperature and humidity sensor.""" -import logging from datetime import timedelta +import logging import Adafruit_DHT # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py index bdb0c348803ce1..33663f121d1cca 100644 --- a/homeassistant/components/digital_ocean/__init__.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -1,13 +1,13 @@ """Support for Digital Ocean.""" -import logging from datetime import timedelta +import logging import digitalocean import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py index 10c8ce73a4706f..824af4416880b2 100644 --- a/homeassistant/components/digitalloggers/switch.py +++ b/homeassistant/components/digitalloggers/switch.py @@ -1,17 +1,17 @@ """Support for Digital Loggers DIN III Relays.""" -import logging from datetime import timedelta +import logging import dlipower import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index f35cf5b0ce9f93..864b7da5e55a00 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -5,15 +5,14 @@ import discord import voluptuous as vol -from homeassistant.const import CONF_TOKEN -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_TOKEN +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 884f6680d7cd3d..965782d1228fcd 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -6,19 +6,19 @@ Knows which components handle certain types, will make sure they are loaded before the EVENT_PLATFORM_DISCOVERED is fired. """ -import json from datetime import timedelta +import json import logging from netdisco.discovery import NetworkDiscovery import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import async_discover, async_load_platform from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util DOMAIN = "discovery" diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 6e5c49e7aba4bd..430878ca44f33f 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -10,10 +10,12 @@ CONF_SOURCE, ImageProcessingFaceEntity, ) +from homeassistant.core import split_entity_id # pylint: disable=unused-import -from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa: F401 -from homeassistant.core import split_entity_id +from homeassistant.components.image_processing import ( # noqa: F401, isort:skip + PLATFORM_SCHEMA, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index d5b55b6a68ce35..d6fbf106b0c96b 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -1,20 +1,20 @@ """Component that will help set the Dlib face detect processing.""" -import logging import io +import logging # pylint: disable=import-error import face_recognition import voluptuous as vol -from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, - PLATFORM_SCHEMA, - CONF_SOURCE, + CONF_CONFIDENCE, CONF_ENTITY_ID, CONF_NAME, - CONF_CONFIDENCE, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingFaceEntity, ) +from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 7f2c3fd1d4276d..5e4d211d49718d 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -3,11 +3,10 @@ import logging import time -import voluptuous as vol from PIL import Image, ImageDraw from pydoods import PyDOODS +import voluptuous as vol -from homeassistant.const import CONF_TIMEOUT from homeassistant.components.image_processing import ( CONF_CONFIDENCE, CONF_ENTITY_ID, @@ -17,6 +16,7 @@ ImageProcessingEntity, draw_box, ) +from homeassistant.const import CONF_TIMEOUT from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index 03f12314c5af18..b8d18d90833188 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -1,18 +1,18 @@ """Support for Dovado router.""" -import logging from datetime import timedelta +import logging import dovado import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, CONF_HOST, + CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, DEVICE_DEFAULT_NAME, ) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index 203cfb1e27cd8e..a5fe8fd6b30a3b 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -7,17 +7,17 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.dublin_public_transport/ """ +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://data.dublinked.ie/cgi-bin/rtpi/realtimebusinformation" diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index 171d17faff9fac..b3da1ec275253f 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -1,14 +1,14 @@ """Integrate with DuckDNS.""" -import logging from asyncio import iscoroutinefunction from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN -from homeassistant.core import callback, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 4d7ad04e38257d..01a06209d99ffc 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -12,19 +12,19 @@ Warnungen vor markantem Wetter (Stufe 2) Wetterwarnungen (Stufe 1) """ -import logging -import json from datetime import timedelta +import json +import logging import voluptuous as vol +from homeassistant.components.rest.sensor import RestData +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.util import Throttle import homeassistant.util.dt as dt_util -from homeassistant.components.rest.sensor import RestData _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index 30541c8137a89d..fdaec26204dd66 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -1,16 +1,16 @@ """The tests for the Datadog component.""" -from unittest import mock import unittest +from unittest import mock +import homeassistant.components.datadog as datadog from homeassistant.const import ( EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, ) -from homeassistant.setup import setup_component -import homeassistant.components.datadog as datadog import homeassistant.core as ha +from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 5fce4b98019538..6b9004595bb0be 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -1,10 +1,10 @@ """Test the default_config init.""" from unittest.mock import patch -from homeassistant.setup import async_setup_component - import pytest +from homeassistant.setup import async_setup_component + from tests.common import MockDependency, mock_coro diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 70681a6d1504d4..98b027c6175d2b 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -1,20 +1,21 @@ """The tests device sun light trigger component.""" # pylint: disable=protected-access from datetime import datetime + from asynctest import patch import pytest -from homeassistant.setup import async_setup_component -from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.components import ( - device_tracker, - light, device_sun_light_trigger, + device_tracker, group, + light, ) from homeassistant.components.device_tracker.const import ( ENTITY_ID_FORMAT as DT_ENTITY_ID_FORMAT, ) +from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 7f802e1a94b7f7..449147c3648bb3 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -1,40 +1,40 @@ """The tests for the DirecTV Media player platform.""" +from datetime import datetime, timedelta from unittest.mock import call, patch -from datetime import datetime, timedelta -import requests import pytest +import requests +from homeassistant.components.directv.media_player import ( + ATTR_MEDIA_CURRENTLY_RECORDING, + ATTR_MEDIA_RATING, + ATTR_MEDIA_RECORDED, + ATTR_MEDIA_START_TIME, + DEFAULT_DEVICE, + DEFAULT_PORT, +) from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - MEDIA_TYPE_TVSHOW, - ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_DURATION, - ATTR_MEDIA_TITLE, + ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_POSITION, - ATTR_MEDIA_SERIES_TITLE, - ATTR_MEDIA_CHANNEL, - ATTR_INPUT_SOURCE, ATTR_MEDIA_POSITION_UPDATED_AT, + ATTR_MEDIA_SERIES_TITLE, + ATTR_MEDIA_TITLE, DOMAIN, + MEDIA_TYPE_TVSHOW, SERVICE_PLAY_MEDIA, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, - SUPPORT_TURN_ON, - SUPPORT_TURN_OFF, + SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, - SUPPORT_STOP, - SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, - SUPPORT_PLAY, -) -from homeassistant.components.directv.media_player import ( - ATTR_MEDIA_CURRENTLY_RECORDING, - ATTR_MEDIA_RATING, - ATTR_MEDIA_RECORDED, - ATTR_MEDIA_START_TIME, - DEFAULT_DEVICE, - DEFAULT_PORT, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, ) from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 8255751517c4fc..de63a0bf495a22 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -1,6 +1,6 @@ """The tests for the discovery component.""" import asyncio -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest @@ -9,7 +9,7 @@ from homeassistant.components import discovery from homeassistant.util.dt import utcnow -from tests.common import mock_coro, async_fire_time_changed +from tests.common import async_fire_time_changed, mock_coro # One might consider to "mock" services, but it's easy enough to just use # what is already available. diff --git a/tests/components/duckdns/test_init.py b/tests/components/duckdns/test_init.py index 0213d9aefa6fd8..9bc9b3504e7146 100644 --- a/tests/components/duckdns/test_init.py +++ b/tests/components/duckdns/test_init.py @@ -1,13 +1,14 @@ """Test the DuckDNS component.""" from datetime import timedelta import logging + import pytest +from homeassistant.components import duckdns +from homeassistant.components.duckdns import async_track_time_interval_backoff from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component -from homeassistant.components import duckdns from homeassistant.util.dt import utcnow -from homeassistant.components.duckdns import async_track_time_interval_backoff from tests.common import async_fire_time_changed From 6b6570e7ca3242a53b3a6117f1d4940f778ac5ed Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 9 Dec 2019 18:46:56 +0100 Subject: [PATCH 2344/3953] Move imports to top for ness_alarm (#29518) * Move imports to top for ness_alarm * Added patch for the ArminState in alarm_control_panel.py --- homeassistant/components/ness_alarm/__init__.py | 2 +- .../components/ness_alarm/alarm_control_panel.py | 3 ++- tests/components/ness_alarm/test_init.py | 13 ++++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index cc6fad1346d9f7..7131ac505b5adf 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -3,6 +3,7 @@ import datetime import logging +from nessclient import ArmingState, Client import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES @@ -82,7 +83,6 @@ async def async_setup(hass, config): """Set up the Ness Alarm platform.""" - from nessclient import Client, ArmingState conf = config[DOMAIN] diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index d2feebfb64f4b9..f77244a584ed40 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -2,6 +2,8 @@ import logging +from nessclient import ArmingState + import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -91,7 +93,6 @@ async def async_alarm_trigger(self, code=None): @callback def _handle_arming_state_change(self, arming_state): """Handle arming state update.""" - from nessclient import ArmingState if arming_state == ArmingState.UNKNOWN: self._state = None diff --git a/tests/components/ness_alarm/test_init.py b/tests/components/ness_alarm/test_init.py index 31b173f9be665c..9da361852e9017 100644 --- a/tests/components/ness_alarm/test_init.py +++ b/tests/components/ness_alarm/test_init.py @@ -32,8 +32,6 @@ ) from homeassistant.setup import async_setup_component -from tests.common import MockDependency - VALID_CONFIG = { DOMAIN: { CONF_HOST: "alarm.local", @@ -262,7 +260,12 @@ def mock_nessclient(): _mock_factory = MagicMock() _mock_factory.return_value = _mock_instance - with MockDependency("nessclient"), patch( - "nessclient.Client", new=_mock_factory, create=True - ), patch("nessclient.ArmingState", new=MockArmingState): + with patch( + "homeassistant.components.ness_alarm.Client", new=_mock_factory, create=True + ), patch( + "homeassistant.components.ness_alarm.ArmingState", new=MockArmingState + ), patch( + "homeassistant.components.ness_alarm.alarm_control_panel.ArmingState", + new=MockArmingState, + ): yield _mock_instance From 6cc75fc6f30968fc8be5517d5fa61a51ba923b3c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 18:54:54 +0100 Subject: [PATCH 2345/3953] Sort imports according to PEP8 for components starting with "Z" (#29784) * use isort to sort imports for components starting with 'z' * add skip to end of zha/core/channels/__init__.py * put 'pylint: disable=import-error' at the right place * remove the import of config_flow in zha/__init__.py According to @balloob it is no longer needed. * revert previous commit * isort:skip homeassistant/components/zha/__init__.py completely --- homeassistant/components/updater/__init__.py | 4 +--- homeassistant/components/zamg/sensor.py | 6 ++--- homeassistant/components/zengge/light.py | 6 ++--- homeassistant/components/zeroconf/__init__.py | 9 ++++--- homeassistant/components/zestimate/sensor.py | 4 ++-- homeassistant/components/zha/__init__.py | 9 ++++--- .../components/zha/core/channels/__init__.py | 24 ++++++++++--------- homeassistant/components/zigbee/__init__.py | 14 +++++------ homeassistant/components/zigbee/switch.py | 1 - tests/components/zeroconf/test_init.py | 2 +- 10 files changed, 40 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 08f08e1bb64fb6..0ddbfd63c734c7 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -1,8 +1,6 @@ """Support to check for available updates.""" import asyncio from datetime import timedelta - -# pylint: disable=import-error from distutils.version import StrictVersion import json import logging @@ -10,7 +8,7 @@ import aiohttp import async_timeout -from distro import linux_distribution +from distro import linux_distribution # pylint: disable=import-error import voluptuous as vol from homeassistant.const import __version__ as current_version diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 9eea1f6612c8d1..c984b13ef4bab7 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -12,18 +12,18 @@ import voluptuous as vol from homeassistant.components.weather import ( + ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_WIND_SPEED, - ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, ) from homeassistant.const import ( - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, + CONF_NAME, __version__, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index d890b193d729c0..42746c6bad3722 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -1,20 +1,20 @@ """Support for Zengge lights.""" import logging -from zengge import zengge import voluptuous as vol +from zengge import zengge -from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 9f27a5fafc9aea..18ea1fdfca7b89 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,26 +1,25 @@ """Support for exposing Home Assistant via Zeroconf.""" +import ipaddress import logging import socket -import ipaddress import voluptuous as vol - from zeroconf import ( + NonUniqueNameException, ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf, - NonUniqueNameException, ) from homeassistant import util from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, __version__, ) -from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT +from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 4b8bdf5fa2e681..cdf7e6304adafb 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -3,11 +3,11 @@ import logging import requests -import xmltodict import voluptuous as vol +import xmltodict from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 07eb3c53a95d12..a3c68ae3030ad4 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -1,4 +1,8 @@ -"""Support for Zigbee Home Automation devices.""" +"""Support for Zigbee Home Automation devices. + +isort:skip_file +""" + import logging import voluptuous as vol @@ -7,7 +11,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE -# Loading the config flow file will register the flow from . import config_flow # noqa: F401 pylint: disable=unused-import from . import api from .core import ZHAGateway @@ -100,7 +103,7 @@ async def async_setup_entry(hass, config_entry): if config.get(CONF_ENABLE_QUIRKS, True): # needs to be done here so that the ZHA module is finished loading # before zhaquirks is imported - import zhaquirks # noqa: F401 pylint: disable=unused-import, import-outside-toplevel + import zhaquirks # noqa: F401 pylint: disable=unused-import, import-outside-toplevel, import-error zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 2d24c4ce045a15..715156c43eda64 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -395,14 +395,16 @@ def cluster_command(self, tsn, command_id, args): # pylint: disable=wrong-import-position, import-outside-toplevel -from . import closures # noqa: F401 -from . import general # noqa: F401 -from . import homeautomation # noqa: F401 -from . import hvac # noqa: F401 -from . import lighting # noqa: F401 -from . import lightlink # noqa: F401 -from . import manufacturerspecific # noqa: F401 -from . import measurement # noqa: F401 -from . import protocol # noqa: F401 -from . import security # noqa: F401 -from . import smartenergy # noqa: F401 +from . import ( # noqa: F401 isort:skip + closures, + general, + homeautomation, + hvac, + lighting, + lightlink, + manufacturerspecific, + measurement, + protocol, + security, + smartenergy, +) diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/zigbee/__init__.py index e74726a70f909f..d63a713045b0b1 100644 --- a/homeassistant/components/zigbee/__init__.py +++ b/homeassistant/components/zigbee/__init__.py @@ -1,24 +1,24 @@ """Support for Zigbee devices.""" -import logging from binascii import hexlify, unhexlify +import logging -import xbee_helper.const as xb_const +from serial import Serial, SerialException +import voluptuous as vol from xbee_helper import ZigBee +import xbee_helper.const as xb_const from xbee_helper.device import convert_adc from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure -from serial import Serial, SerialException -import voluptuous as vol from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, + CONF_ADDRESS, CONF_DEVICE, CONF_NAME, CONF_PIN, - CONF_ADDRESS, + EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.entity import Entity from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zigbee/switch.py b/homeassistant/components/zigbee/switch.py index 16af6ec7e3a92a..4e8d21f438a12e 100644 --- a/homeassistant/components/zigbee/switch.py +++ b/homeassistant/components/zigbee/switch.py @@ -5,7 +5,6 @@ from . import PLATFORM_SCHEMA, ZigBeeDigitalOut, ZigBeeDigitalOutConfig - CONF_ON_STATE = "on_state" DEFAULT_ON_STATE = "high" diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index f60cf1d7bbb38f..00651bcfc5d92a 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -4,9 +4,9 @@ import pytest from zeroconf import ServiceInfo, ServiceStateChange +from homeassistant.components import zeroconf from homeassistant.generated import zeroconf as zc_gen from homeassistant.setup import async_setup_component -from homeassistant.components import zeroconf @pytest.fixture From 080c702d4f3bfbb6d8cbafa7cd1d363b05c21e66 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 18:56:21 +0100 Subject: [PATCH 2346/3953] Sort imports according to PEP8 for components starting with "C" (#29763) --- homeassistant/components/calendar/__init__.py | 1 - homeassistant/components/clickatell/notify.py | 3 +-- homeassistant/components/clicksend/notify.py | 3 +-- homeassistant/components/clicksend_tts/notify.py | 3 +-- homeassistant/components/configurator/__init__.py | 8 ++++---- homeassistant/components/conversation/__init__.py | 2 +- homeassistant/components/coolmaster/config_flow.py | 2 +- homeassistant/components/cups/sensor.py | 4 ++-- homeassistant/components/currencylayer/sensor.py | 6 +++--- tests/components/canary/test_init.py | 5 +++-- tests/components/canary/test_sensor.py | 10 +++++----- tests/components/coinmarketcap/test_sensor.py | 4 ++-- tests/components/configurator/test_init.py | 2 +- tests/components/conversation/test_init.py | 4 ++-- tests/components/coolmaster/test_config_flow.py | 2 +- 15 files changed, 28 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index ca240925cf5167..35b25b86d43610 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -17,7 +17,6 @@ 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__) diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py index 26f2f30aeff1a9..d59a553a4f62fc 100644 --- a/homeassistant/components/clickatell/notify.py +++ b/homeassistant/components/clickatell/notify.py @@ -4,11 +4,10 @@ import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "clickatell" diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 87fc217ac42396..42136e9a09cb5c 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -6,6 +6,7 @@ import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -15,8 +16,6 @@ ) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) BASE_API_URL = "https://rest.clicksend.com/v3" diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index ba30c61e937f9a..400e72a7d0cb80 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -6,6 +6,7 @@ import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -14,8 +15,6 @@ ) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) BASE_API_URL = "https://rest.clicksend.com/v3" diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index f3b2a41e9177a4..78333d96355a83 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -9,14 +9,14 @@ import functools as ft import logging -from homeassistant.core import callback as async_callback from homeassistant.const import ( - EVENT_TIME_CHANGED, - ATTR_FRIENDLY_NAME, ATTR_ENTITY_PICTURE, + ATTR_FRIENDLY_NAME, + EVENT_TIME_CHANGED, ) -from homeassistant.loader import bind_hass +from homeassistant.core import callback as async_callback from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index ec5868e86feab8..158a365981b838 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -11,7 +11,7 @@ from homeassistant.loader import bind_hass from .agent import AbstractConversationAgent -from .default_agent import async_register, DefaultAgent +from .default_agent import DefaultAgent, async_register _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index fe52ea17b28bf8..e9cef562647ff2 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -3,7 +3,7 @@ from pycoolmasternet import CoolMasterNet import voluptuous as vol -from homeassistant import core, config_entries +from homeassistant import config_entries, core from homeassistant.const import CONF_HOST, CONF_PORT # pylint: disable=unused-import diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 17a246561a50fa..7581891af6aa50 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -1,14 +1,14 @@ """Details about printers which are connected to CUPS.""" +from datetime import timedelta import importlib import logging -from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index d4660d70286d8c..cbad07c0284e41 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -5,15 +5,15 @@ import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_API_KEY, - CONF_NAME, CONF_BASE, + CONF_NAME, CONF_QUOTE, - ATTR_ATTRIBUTION, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index cb671e3061875a..819d1ce0e90715 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -1,9 +1,10 @@ """The tests for the Canary component.""" import unittest -from unittest.mock import patch, MagicMock, PropertyMock +from unittest.mock import MagicMock, PropertyMock, patch -import homeassistant.components.canary as canary from homeassistant import setup +import homeassistant.components.canary as canary + from tests.common import get_test_home_assistant diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index cfb8d46141abda..7d5e829a347607 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -3,16 +3,16 @@ import unittest from unittest.mock import Mock -from homeassistant.components.canary import DATA_CANARY -from homeassistant.components.canary import sensor as canary +from homeassistant.components.canary import DATA_CANARY, sensor as canary from homeassistant.components.canary.sensor import ( - CanarySensor, - SENSOR_TYPES, ATTR_AIR_QUALITY, - STATE_AIR_QUALITY_NORMAL, + SENSOR_TYPES, STATE_AIR_QUALITY_ABNORMAL, + STATE_AIR_QUALITY_NORMAL, STATE_AIR_QUALITY_VERY_ABNORMAL, + CanarySensor, ) + from tests.common import get_test_home_assistant from tests.components.canary.test_init import mock_device, mock_location diff --git a/tests/components/coinmarketcap/test_sensor.py b/tests/components/coinmarketcap/test_sensor.py index d629bf141848fe..9d1e89fbc24370 100644 --- a/tests/components/coinmarketcap/test_sensor.py +++ b/tests/components/coinmarketcap/test_sensor.py @@ -1,12 +1,12 @@ """Tests for the CoinMarketCap sensor platform.""" import json - import unittest from unittest.mock import patch import homeassistant.components.sensor as sensor from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, load_fixture, assert_setup_component + +from tests.common import assert_setup_component, get_test_home_assistant, load_fixture VALID_CONFIG = { "platform": "coinmarketcap", diff --git a/tests/components/configurator/test_init.py b/tests/components/configurator/test_init.py index 43395c0e7e87cf..b572609c5a2790 100644 --- a/tests/components/configurator/test_init.py +++ b/tests/components/configurator/test_init.py @@ -3,7 +3,7 @@ import unittest import homeassistant.components.configurator as configurator -from homeassistant.const import EVENT_TIME_CHANGED, ATTR_FRIENDLY_NAME +from homeassistant.const import ATTR_FRIENDLY_NAME, EVENT_TIME_CHANGED from tests.common import get_test_home_assistant diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 45008ef9447600..f84d210909534e 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1,10 +1,10 @@ """The tests for the Conversation component.""" import pytest -from homeassistant.core import DOMAIN as HASS_DOMAIN, Context -from homeassistant.setup import async_setup_component from homeassistant.components import conversation +from homeassistant.core import DOMAIN as HASS_DOMAIN, Context from homeassistant.helpers import intent +from homeassistant.setup import async_setup_component from tests.common import async_mock_intent, async_mock_service diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index d0126ff2cb64ae..c71f308dece965 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant import config_entries, setup -from homeassistant.components.coolmaster.const import DOMAIN, AVAILABLE_MODES +from homeassistant.components.coolmaster.const import AVAILABLE_MODES, DOMAIN from tests.common import mock_coro From 897522a82d69df200b2684e97855e29ff143bbcb Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 19:04:38 +0100 Subject: [PATCH 2347/3953] Fix 'pytest.register_assert_rewrite("tests.common")' warning (#29797) see https://github.com/home-assistant/home-assistant/pull/29791/files/fea243c035f117275fda1895720d30e7f977aa9c#r355533273 --- tests/conftest.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2b6a1a1673912f..41b4757c92ed5c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,9 @@ from homeassistant.auth.providers import homeassistant, legacy_api_password from homeassistant.util import location -from tests.common import ( # noqa: E402 module level import not at top of file +pytest.register_assert_rewrite("tests.common") + +from tests.common import ( # noqa: E402, isort:skip CLIENT_ID, INSTANCES, MockUser, @@ -21,11 +23,7 @@ mock_coro, mock_storage as mock_storage, ) -from tests.test_util.aiohttp import ( # noqa: E402 module level import not at top of file - mock_aiohttp_client, -) - -pytest.register_assert_rewrite("tests.common") +from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:skip if os.environ.get("UVLOOP") == "1": import uvloop From 80c344d3a8aa137972e8616575c2fdcdccdd6a9e Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 19:06:25 +0100 Subject: [PATCH 2348/3953] Sort imports according to PEP8 for huawei_lte (#29664) --- homeassistant/components/huawei_lte/__init__.py | 15 +++++++++------ .../components/huawei_lte/binary_sensor.py | 2 +- .../components/huawei_lte/config_flow.py | 9 ++++----- .../components/huawei_lte/device_tracker.py | 2 +- homeassistant/components/huawei_lte/notify.py | 3 +-- homeassistant/components/huawei_lte/sensor.py | 3 +-- homeassistant/components/huawei_lte/switch.py | 2 +- tests/components/huawei_lte/test_config_flow.py | 9 ++++----- 8 files changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 30b8eb1bc6d1dd..4a5614543e9d92 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -3,12 +3,11 @@ from collections import defaultdict from datetime import timedelta from functools import partial -from urllib.parse import urlparse import ipaddress import logging from typing import Any, Callable, Dict, List, Set, Tuple +from urllib.parse import urlparse -import voluptuous as vol import attr from getmac import get_mac_address from huawei_lte_api.AuthorizedConnection import AuthorizedConnection @@ -20,13 +19,14 @@ ) from requests.exceptions import Timeout from url_normalize import url_normalize +import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_RECIPIENT, @@ -36,8 +36,11 @@ ) from homeassistant.core import CALLBACK_TYPE from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + discovery, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -46,6 +49,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType + from .const import ( ALL_KEYS, CONNECTION_TIMEOUT, @@ -64,7 +68,6 @@ UPDATE_SIGNAL, ) - _LOGGER = logging.getLogger(__name__) # dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 4fcb400c32a750..104933fe71492f 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -11,10 +11,10 @@ BinarySensorDevice, ) from homeassistant.const import CONF_URL + from . import HuaweiLteBaseEntity from .const import DOMAIN, KEY_MONITORING_STATUS - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 1bc3753bdd7df9..9151e5eb99977e 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -8,10 +8,10 @@ from huawei_lte_api.Client import Client from huawei_lte_api.Connection import Connection from huawei_lte_api.exceptions import ( - LoginErrorUsernameWrongException, LoginErrorPasswordWrongException, - LoginErrorUsernamePasswordWrongException, LoginErrorUsernamePasswordOverrunException, + LoginErrorUsernamePasswordWrongException, + LoginErrorUsernameWrongException, ResponseErrorException, ) from requests.exceptions import Timeout @@ -22,12 +22,11 @@ from homeassistant.components.ssdp import ATTR_HOST, ATTR_NAME, ATTR_PRESENTATIONURL from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME from homeassistant.core import callback -from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME -# https://github.com/PyCQA/pylint/issues/3202 +# see https://github.com/PyCQA/pylint/issues/3202 about the DOMAIN's pylint issue +from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME from .const import DOMAIN # pylint: disable=unused-import - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index f5f834fa18674a..a9c61831fdd023 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -15,10 +15,10 @@ from homeassistant.const import CONF_URL from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import HuaweiLteBaseEntity from .const import DOMAIN, KEY_WLAN_HOST_LIST, UPDATE_SIGNAL - _LOGGER = logging.getLogger(__name__) _DEVICE_SCAN = f"{DEVICE_TRACKER_DOMAIN}/device_scan" diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 4b5a63756b5eb6..494d0ec720ed6a 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -6,13 +6,12 @@ import attr from huawei_lte_api.exceptions import ResponseErrorException -from homeassistant.components.notify import BaseNotificationService, ATTR_TARGET +from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import CONF_RECIPIENT, CONF_URL from . import Router from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 3cc36b30d8ec94..3b6b75edfba68f 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -6,11 +6,11 @@ import attr -from homeassistant.const import CONF_URL, STATE_UNKNOWN from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DOMAIN as SENSOR_DOMAIN, ) +from homeassistant.const import CONF_URL, STATE_UNKNOWN from . import HuaweiLteBaseEntity from .const import ( @@ -22,7 +22,6 @@ UNIT_SECONDS, ) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index bff82227b80cc4..44d2da0c898e43 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -11,10 +11,10 @@ SwitchDevice, ) from homeassistant.const import CONF_URL + from . import HuaweiLteBaseEntity from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH - _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index a9f5034fcfe051..b14583f13cd57b 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -2,14 +2,13 @@ from huawei_lte_api.enums.client import ResponseCodeEnum from huawei_lte_api.enums.user import LoginErrorEnum, LoginStateEnum, PasswordTypeEnum -from requests_mock import ANY -from requests.exceptions import ConnectionError import pytest +from requests.exceptions import ConnectionError +from requests_mock import ANY from homeassistant import data_entry_flow -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_URL -from homeassistant.components.huawei_lte.const import DOMAIN from homeassistant.components.huawei_lte.config_flow import ConfigFlowHandler +from homeassistant.components.huawei_lte.const import DOMAIN from homeassistant.components.ssdp import ( ATTR_HOST, ATTR_MANUFACTURER, @@ -24,10 +23,10 @@ ATTR_UDN, ATTR_UPNP_DEVICE_TYPE, ) +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from tests.common import MockConfigEntry - FIXTURE_USER_INPUT = { CONF_URL: "http://192.168.1.1/", CONF_USERNAME: "admin", From e37443f10c6a3d20f4302ea093e5bec1712dcd3c Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 19:07:32 +0100 Subject: [PATCH 2349/3953] Sort imports according to PEP8 for components starting with "U" (#29779) * use isort to sort imports for components starting with 'u' * add 'pylint: disable=import-error' to the right place --- .../components/ue_smart_radio/media_player.py | 2 +- .../components/uk_transport/sensor.py | 6 +++--- .../components/universal/media_player.py | 2 +- homeassistant/components/updater/__init__.py | 5 ++--- .../components/updater/binary_sensor.py | 2 +- homeassistant/components/uscis/sensor.py | 9 ++++---- tests/components/uk_transport/test_sensor.py | 15 ++++++------- .../unifi_direct/test_device_tracker.py | 18 ++++++++-------- .../components/universal/test_media_player.py | 6 +++--- tests/components/upnp/test_init.py | 8 +++---- tests/components/uptime/test_sensor.py | 5 +++-- .../test_geo_location.py | 21 ++++++++++--------- tests/components/uvc/test_camera.py | 8 +++---- 13 files changed, 53 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/ue_smart_radio/media_player.py b/homeassistant/components/ue_smart_radio/media_player.py index ae54eb76d7215a..d25c52608e1672 100644 --- a/homeassistant/components/ue_smart_radio/media_player.py +++ b/homeassistant/components/ue_smart_radio/media_player.py @@ -5,7 +5,7 @@ import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index eb325d32212e11..e3c5440c450472 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -3,19 +3,19 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.uk_transport/ """ +from datetime import datetime, timedelta import logging import re -from datetime import datetime, timedelta import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_MODE +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 63e3ff7448d873..37d4cf138f2f5a 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -4,7 +4,7 @@ import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_APP_ID, ATTR_APP_NAME, diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 0ddbfd63c734c7..1d4e441f12e25f 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -12,11 +12,10 @@ import voluptuous as vol from homeassistant.const import __version__ as current_version -from homeassistant.helpers import event +from homeassistant.helpers import discovery, 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 +from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index cae3ae32e3c9d7..3e026a87d4dff9 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Home Assistant Updater binary sensors.""" -from homeassistant.core import callback from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DISPATCHER_REMOTE_UPDATE, Updater diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index 3f5175ad09d6ca..6f94d5c38b065c 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -1,16 +1,15 @@ """Support for USCIS Case Status.""" -import logging from datetime import timedelta +import logging import uscisstatus import voluptuous as vol -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers import config_validation as cv from homeassistant.const import CONF_FRIENDLY_NAME - +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/uk_transport/test_sensor.py b/tests/components/uk_transport/test_sensor.py index c55de0dd3890a1..ab115d0218766b 100644 --- a/tests/components/uk_transport/test_sensor.py +++ b/tests/components/uk_transport/test_sensor.py @@ -1,23 +1,24 @@ """The tests for the uk_transport platform.""" import re +import unittest import requests_mock -import unittest from homeassistant.components.uk_transport.sensor import ( - UkTransportSensor, ATTR_ATCOCODE, + ATTR_CALLING_AT, ATTR_LOCALITY, - ATTR_STOP_NAME, ATTR_NEXT_BUSES, - ATTR_STATION_CODE, - ATTR_CALLING_AT, ATTR_NEXT_TRAINS, - CONF_API_APP_KEY, + ATTR_STATION_CODE, + ATTR_STOP_NAME, CONF_API_APP_ID, + CONF_API_APP_KEY, + UkTransportSensor, ) from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant + +from tests.common import get_test_home_assistant, load_fixture BUS_ATCOCODE = "340000368SHE" BUS_DIRECTION = "Wantage" diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py index cda6dff84134fc..3d62a451af2538 100644 --- a/tests/components/unifi_direct/test_device_tracker.py +++ b/tests/components/unifi_direct/test_device_tracker.py @@ -1,29 +1,29 @@ """The tests for the Unifi direct device tracker platform.""" -import os from datetime import timedelta -from asynctest import mock, patch +import os +from asynctest import mock, patch import pytest import voluptuous as vol -from homeassistant.setup import async_setup_component -from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.components.device_tracker import ( - CONF_CONSIDER_HOME, - CONF_TRACK_NEW, CONF_AWAY_HIDE, + CONF_CONSIDER_HOME, CONF_NEW_DEVICE_DEFAULTS, + CONF_TRACK_NEW, ) +from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.components.unifi_direct.device_tracker import ( - DOMAIN, CONF_PORT, + DOMAIN, PLATFORM_SCHEMA, _response_to_json, get_scanner, ) -from homeassistant.const import CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME +from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, mock_component, load_fixture +from tests.common import assert_setup_component, load_fixture, mock_component scanner_path = ( "homeassistant.components.unifi_direct.device_tracker." + "UnifiDeviceScanner" diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 22ec96c3a21702..cf985621351511 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -5,14 +5,14 @@ from voluptuous.error import MultipleInvalid -from homeassistant.const import STATE_OFF, STATE_ON, STATE_PLAYING, STATE_PAUSED -import homeassistant.components.switch as switch import homeassistant.components.input_number as input_number import homeassistant.components.input_select as input_select import homeassistant.components.media_player as media_player +import homeassistant.components.switch as switch import homeassistant.components.universal.media_player as universal +from homeassistant.const import STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING -from tests.common import mock_service, get_test_home_assistant +from tests.common import get_test_home_assistant, mock_service def validate_config(config): diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 5e2106ff208d99..1daa60bcb369eb 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,16 +1,14 @@ """Test UPnP/IGD setup process.""" from ipaddress import ip_address -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch -from homeassistant.setup import async_setup_component from homeassistant.components import upnp from homeassistant.components.upnp.device import Device from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry -from tests.common import MockDependency -from tests.common import mock_coro +from tests.common import MockConfigEntry, MockDependency, mock_coro class MockDevice(Device): diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index b3dcddfba6a290..0a9d227681b1be 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -1,11 +1,12 @@ """The tests for the uptime sensor platform.""" import asyncio +from datetime import timedelta import unittest from unittest.mock import patch -from datetime import timedelta -from homeassistant.setup import setup_component from homeassistant.components.uptime.sensor import UptimeSensor +from homeassistant.setup import setup_component + from tests.common import get_test_home_assistant diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index 65ceec4d425efc..646878c97bdee1 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -1,37 +1,38 @@ """The tests for the USGS Earthquake Hazards Program Feed platform.""" import datetime -from unittest.mock import patch, MagicMock, call +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE from homeassistant.components.usgs_earthquakes_feed.geo_location import ( ATTR_ALERT, ATTR_EXTERNAL_ID, - SCAN_INTERVAL, - ATTR_PLACE, ATTR_MAGNITUDE, + ATTR_PLACE, ATTR_STATUS, - ATTR_TYPE, ATTR_TIME, + ATTR_TYPE, ATTR_UPDATED, CONF_FEED_TYPE, + SCAN_INTERVAL, ) from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - CONF_RADIUS, + ATTR_ATTRIBUTION, + ATTR_FRIENDLY_NAME, + ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ICON, + CONF_RADIUS, + EVENT_HOMEASSISTANT_START, ) from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component, async_fire_time_changed import homeassistant.util.dt as dt_util +from tests.common import assert_setup_component, async_fire_time_changed + CONFIG = { geo_location.DOMAIN: [ { diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index f4be8ac6a8beac..c77b5d83749324 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -3,15 +3,15 @@ import unittest from unittest import mock +import pytest import requests -from uvcclient import camera -from uvcclient import nvr +from uvcclient import camera, nvr +from homeassistant.components.uvc import camera as uvc from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import setup_component -from homeassistant.components.uvc import camera as uvc + from tests.common import get_test_home_assistant -import pytest class TestUVCSetup(unittest.TestCase): From bc3b7ed06bf92cc902b2703c32fc5148c81135fc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Dec 2019 19:18:41 +0100 Subject: [PATCH 2350/3953] Fix build, invalid JSON file in icloud component (#29798) --- homeassistant/components/icloud/strings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json index 343a087738f4da..117e26c8830685 100644 --- a/homeassistant/components/icloud/strings.json +++ b/homeassistant/components/icloud/strings.json @@ -25,14 +25,14 @@ } } }, - "error":{ + "error": { "username_exists": "Account already configured", "login": "Login error: please check your email & password", "send_verification_code": "Failed to send verification code", - "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again", + "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" }, - "abort":{ + "abort": { "username_exists": "Account already configured" } } -} +} \ No newline at end of file From 7f948594eb687294ae75ea0ee7454e2a1239df94 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 9 Dec 2019 20:00:14 +0100 Subject: [PATCH 2351/3953] Sort imports according to PEP8 for google_assistant (#29633) * sort imports and fix flake8 issue for google * add isort:skip to EVENT_SYNC_RECEIVED --- .../components/google_assistant/__init__.py | 3 +- tests/components/google_assistant/__init__.py | 1 + .../google_assistant/test_google_assistant.py | 10 ++--- .../google_assistant/test_helpers.py | 13 +++++-- .../components/google_assistant/test_http.py | 15 ++++---- .../components/google_assistant/test_init.py | 2 +- .../google_assistant/test_report_state.py | 5 +-- .../google_assistant/test_smart_home.py | 37 ++++++++++--------- .../components/google_assistant/test_trait.py | 35 ++++++++++-------- 9 files changed, 66 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index f34a8e342c4db6..107003db583a13 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -29,10 +29,11 @@ DOMAIN, SERVICE_REQUEST_SYNC, ) -from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 from .const import EVENT_QUERY_RECEIVED # noqa: F401 from .http import GoogleAssistantView, GoogleConfig +from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401, isort:skip + _LOGGER = logging.getLogger(__name__) ENTITY_SCHEMA = vol.Schema( diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 657bf930ed6007..db3b2e68f20f97 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,5 +1,6 @@ """Tests for the Google Assistant integration.""" from asynctest.mock import MagicMock + from homeassistant.components.google_assistant import helpers diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index b43e913ab2765d..d1d584228843aa 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -6,19 +6,19 @@ from aiohttp.hdrs import AUTHORIZATION import pytest -from homeassistant import core, const, setup +from homeassistant import const, core, setup from homeassistant.components import ( - fan, + alarm_control_panel, cover, + fan, + google_assistant as ga, light, - switch, lock, media_player, - alarm_control_panel, + switch, ) from homeassistant.components.climate import const as climate from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES -from homeassistant.components import google_assistant as ga from . import DEMO_DEVICES diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index eb479a3b6b52c6..9c8a868e68da8a 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -1,17 +1,22 @@ """Test Google Assistant helpers.""" -from asynctest.mock import Mock, patch, call from datetime import timedelta + +from asynctest.mock import Mock, call, patch import pytest -from homeassistant.setup import async_setup_component + from homeassistant.components.google_assistant import helpers -from homeassistant.components.google_assistant.const import EVENT_COMMAND_RECEIVED +from homeassistant.components.google_assistant.const import ( # noqa: F401 + EVENT_COMMAND_RECEIVED, +) +from homeassistant.setup import async_setup_component from homeassistant.util import dt + from . import MockConfig from tests.common import ( async_capture_events, - async_mock_service, async_fire_time_changed, + async_mock_service, ) diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 86ffcc87ac0a16..112935f0160a63 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -1,17 +1,18 @@ """Test Google http services.""" -from datetime import datetime, timezone, timedelta -from asynctest import patch, ANY +from datetime import datetime, timedelta, timezone +from asynctest import ANY, patch + +from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA +from homeassistant.components.google_assistant.const import ( + HOMEGRAPH_TOKEN_URL, + REPORT_STATE_BASE_URL, +) from homeassistant.components.google_assistant.http import ( GoogleConfig, _get_homegraph_jwt, _get_homegraph_token, ) -from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA -from homeassistant.components.google_assistant.const import ( - REPORT_STATE_BASE_URL, - HOMEGRAPH_TOKEN_URL, -) DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA( { diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index 9a8b9643cfe2a3..7c5d14ae6d7aaa 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -1,9 +1,9 @@ """The tests for google-assistant init.""" import asyncio +from homeassistant.components import google_assistant as ga from homeassistant.core import Context from homeassistant.setup import async_setup_component -from homeassistant.components import google_assistant as ga GA_API_KEY = "Agdgjsj399sdfkosd932ksd" diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index ce624c9ca95ed2..fd9cad27ffa57b 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,13 +1,12 @@ """Test Google report state.""" from unittest.mock import patch -from homeassistant.components.google_assistant import report_state, error +from homeassistant.components.google_assistant import error, report_state from homeassistant.util.dt import utcnow from . import BASIC_CONFIG - -from tests.common import mock_coro, async_fire_time_changed +from tests.common import async_fire_time_changed, mock_coro async def test_report_state(hass, caplog): diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index c144ffee6def51..7ffe9cda477da4 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,40 +1,41 @@ """Test Google Smart Home.""" -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch + import pytest -from homeassistant.core import State, EVENT_CALL_SERVICE -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__ -from homeassistant.setup import async_setup_component from homeassistant.components import camera from homeassistant.components.climate.const import ( - ATTR_MIN_TEMP, ATTR_MAX_TEMP, + ATTR_MIN_TEMP, HVAC_MODE_HEAT, ) -from homeassistant.components.google_assistant import ( - const, - trait, - smart_home as sh, - EVENT_COMMAND_RECEIVED, - EVENT_QUERY_RECEIVED, - EVENT_SYNC_RECEIVED, -) from homeassistant.components.demo.binary_sensor import DemoBinarySensor from homeassistant.components.demo.cover import DemoCover from homeassistant.components.demo.light import DemoLight from homeassistant.components.demo.media_player import AbstractDemoPlayer from homeassistant.components.demo.switch import DemoSwitch - +from homeassistant.components.google_assistant import ( + EVENT_COMMAND_RECEIVED, + EVENT_QUERY_RECEIVED, + EVENT_SYNC_RECEIVED, + const, + smart_home as sh, + trait, +) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__ +from homeassistant.core import EVENT_CALL_SERVICE, State from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from . import BASIC_CONFIG, MockConfig + from tests.common import ( - mock_device_registry, - mock_registry, mock_area_registry, mock_coro, + mock_device_registry, + mock_registry, ) -from . import BASIC_CONFIG, MockConfig - REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 6d24aa0942f8ce..8a1d7384729b04 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,13 +1,16 @@ """Tests for the Google Assistant traits.""" -from unittest.mock import patch, Mock import logging +from unittest.mock import Mock, patch + import pytest from homeassistant.components import ( + alarm_control_panel, binary_sensor, camera, cover, fan, + group, input_boolean, light, lock, @@ -17,33 +20,33 @@ sensor, switch, vacuum, - group, - alarm_control_panel, ) from homeassistant.components.climate import const as climate -from homeassistant.components.google_assistant import trait, helpers, const, error +from homeassistant.components.google_assistant import const, error, helpers, trait from homeassistant.const import ( - STATE_ON, - STATE_OFF, + ATTR_ASSUMED_STATE, + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_ALARM_ARMED_AWAY, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, - ATTR_ENTITY_ID, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_SUPPORTED_FEATURES, - ATTR_TEMPERATURE, - ATTR_DEVICE_CLASS, - ATTR_ASSUMED_STATE, - STATE_UNKNOWN, ) -from homeassistant.core import State, DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE +from homeassistant.core import DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE, State from homeassistant.util import color -from tests.common import async_mock_service, mock_coro + from . import BASIC_CONFIG, MockConfig +from tests.common import async_mock_service, mock_coro + _LOGGER = logging.getLogger(__name__) REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" From 1222aa8c56f7d7f853996fa58ab0f962ed0bb4ef Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 9 Dec 2019 14:50:04 -0500 Subject: [PATCH 2352/3953] Add ZHA group API (#29641) * add skeleton to retrieve zigbee groups * get single group * add a group * return group members with group * add comment * fix group members * add function to add device to group * add group members * add remove from group method * add api to remove members from group * add remove groups method * clean up group add and remove * fix remove group * fix remove groups * add api to get only groupable devices * change var init * add tests * address review comment --- homeassistant/components/zha/api.py | 250 ++++++++++++++++++++ homeassistant/components/zha/core/const.py | 5 + homeassistant/components/zha/core/device.py | 20 ++ tests/components/zha/common.py | 2 + tests/components/zha/conftest.py | 11 + tests/components/zha/test_api.py | 176 +++++++++++++- 6 files changed, 461 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 438b93244cf84d..e796c48c3f3326 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -23,6 +23,7 @@ ATTR_ENDPOINT_ID, ATTR_LEVEL, ATTR_MANUFACTURER, + ATTR_MEMBERS, ATTR_NAME, ATTR_VALUE, ATTR_WARNING_DEVICE_DURATION, @@ -39,6 +40,9 @@ DATA_ZHA, DATA_ZHA_GATEWAY, DOMAIN, + GROUP_ID, + GROUP_IDS, + GROUP_NAME, MFG_CLUSTER_ID_START, WARNING_DEVICE_MODE_EMERGENCY, WARNING_DEVICE_SOUND_HIGH, @@ -211,6 +215,34 @@ async def websocket_get_devices(hass, connection, msg): connection.send_result(msg[ID], devices) +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required(TYPE): "zha/devices/groupable"}) +async def websocket_get_groupable_devices(hass, connection, msg): + """Get ZHA devices that can be grouped.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + + devices = [] + for device in zha_gateway.devices.values(): + if device.is_groupable: + devices.append( + async_get_device_info( + hass, device, ha_device_registry=ha_device_registry + ) + ) + connection.send_result(msg[ID], devices) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required(TYPE): "zha/groups"}) +async def websocket_get_groups(hass, connection, msg): + """Get ZHA groups.""" + groups = await get_groups(hass) + connection.send_result(msg[ID], groups) + + @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( @@ -236,6 +268,161 @@ async def websocket_get_device(hass, connection, msg): connection.send_result(msg[ID], device) +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + {vol.Required(TYPE): "zha/group", vol.Required(GROUP_ID): cv.positive_int} +) +async def websocket_get_group(hass, connection, msg): + """Get ZHA group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + group_id = msg[GROUP_ID] + group = None + + if group_id in zha_gateway.application_controller.groups: + group = async_get_group_info( + hass, + zha_gateway, + zha_gateway.application_controller.groups[group_id], + ha_device_registry, + ) + if not group: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + ) + ) + return + connection.send_result(msg[ID], group) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/add", + vol.Required(GROUP_NAME): cv.string, + vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + } +) +async def websocket_add_group(hass, connection, msg): + """Add a new ZHA group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + group_id = len(zha_gateway.application_controller.groups) + 1 + group_name = msg[GROUP_NAME] + zigpy_group = async_get_group_by_name(zha_gateway, group_name) + ret_group = None + members = msg.get(ATTR_MEMBERS) + + # guard against group already existing + if zigpy_group is None: + zigpy_group = zha_gateway.application_controller.groups.add_group( + group_id, group_name + ) + if members is not None: + tasks = [] + for ieee in members: + tasks.append(zha_gateway.devices[ieee].async_add_to_group(group_id)) + await asyncio.gather(*tasks) + ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) + connection.send_result(msg[ID], ret_group) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/remove", + vol.Required(GROUP_IDS): vol.All(cv.ensure_list, [cv.positive_int]), + } +) +async def websocket_remove_groups(hass, connection, msg): + """Remove the specified ZHA groups.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + groups = zha_gateway.application_controller.groups + group_ids = msg[GROUP_IDS] + + if len(group_ids) > 1: + tasks = [] + for group_id in group_ids: + tasks.append(remove_group(groups[group_id], zha_gateway)) + await asyncio.gather(*tasks) + else: + await remove_group(groups[group_ids[0]], zha_gateway) + ret_groups = await get_groups(hass) + connection.send_result(msg[ID], ret_groups) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/members/add", + vol.Required(GROUP_ID): cv.positive_int, + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + } +) +async def websocket_add_group_members(hass, connection, msg): + """Add members to a ZHA group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + group_id = msg[GROUP_ID] + members = msg[ATTR_MEMBERS] + zigpy_group = None + + if group_id in zha_gateway.application_controller.groups: + zigpy_group = zha_gateway.application_controller.groups[group_id] + tasks = [] + for ieee in members: + tasks.append(zha_gateway.devices[ieee].async_add_to_group(group_id)) + await asyncio.gather(*tasks) + if not zigpy_group: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + ) + ) + return + ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) + connection.send_result(msg[ID], ret_group) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/members/remove", + vol.Required(GROUP_ID): cv.positive_int, + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + } +) +async def websocket_remove_group_members(hass, connection, msg): + """Remove members from a ZHA group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + group_id = msg[GROUP_ID] + members = msg[ATTR_MEMBERS] + zigpy_group = None + + if group_id in zha_gateway.application_controller.groups: + zigpy_group = zha_gateway.application_controller.groups[group_id] + tasks = [] + for ieee in members: + tasks.append(zha_gateway.devices[ieee].async_remove_from_group(group_id)) + await asyncio.gather(*tasks) + if not zigpy_group: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + ) + ) + return + ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) + connection.send_result(msg[ID], ret_group) + + @callback def async_get_device_info(hass, device, ha_device_registry=None): """Get ZHA device.""" @@ -261,6 +448,62 @@ def async_get_device_info(hass, device, ha_device_registry=None): return ret_device +async def get_groups(hass,): + """Get ZHA Groups.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + + groups = [] + for group in zha_gateway.application_controller.groups.values(): + groups.append( + async_get_group_info(hass, zha_gateway, group, ha_device_registry) + ) + return groups + + +async def remove_group(group, zha_gateway): + """Remove ZHA Group.""" + if group.members: + tasks = [] + for member_ieee in group.members.keys(): + if member_ieee[0] in zha_gateway.devices: + tasks.append( + zha_gateway.devices[member_ieee[0]].async_remove_from_group( + group.group_id + ) + ) + await asyncio.gather(*tasks) + else: + zha_gateway.application_controller.groups.pop(group.group_id) + + +@callback +def async_get_group_info(hass, zha_gateway, group, ha_device_registry): + """Get ZHA group.""" + ret_group = {} + ret_group["group_id"] = group.group_id + ret_group["name"] = group.name + ret_group["members"] = [ + async_get_device_info( + hass, + zha_gateway.get_device(member_ieee[0]), + ha_device_registry=ha_device_registry, + ) + for member_ieee in group.members.keys() + if member_ieee[0] in zha_gateway.devices + ] + return ret_group + + +@callback +def async_get_group_by_name(zha_gateway, group_name): + """Get ZHA group by name.""" + for group in zha_gateway.application_controller.groups.values(): + if group.name == group_name: + return group + return None + + @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( @@ -785,7 +1028,14 @@ async def warning_device_warn(service): websocket_api.async_register_command(hass, websocket_permit_devices) websocket_api.async_register_command(hass, websocket_get_devices) + websocket_api.async_register_command(hass, websocket_get_groupable_devices) + websocket_api.async_register_command(hass, websocket_get_groups) websocket_api.async_register_command(hass, websocket_get_device) + websocket_api.async_register_command(hass, websocket_get_group) + websocket_api.async_register_command(hass, websocket_add_group) + websocket_api.async_register_command(hass, websocket_remove_groups) + websocket_api.async_register_command(hass, websocket_add_group_members) + websocket_api.async_register_command(hass, websocket_remove_group_members) websocket_api.async_register_command(hass, websocket_reconfigure_node) websocket_api.async_register_command(hass, websocket_device_clusters) websocket_api.async_register_command(hass, websocket_device_cluster_attributes) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index ac83c2cdcd8f41..24c0126ba600a0 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -24,6 +24,7 @@ ATTR_LQI = "lqi" ATTR_MANUFACTURER = "manufacturer" ATTR_MANUFACTURER_CODE = "manufacturer_code" +ATTR_MEMBERS = "members" ATTR_MODEL = "model" ATTR_NAME = "name" ATTR_NWK = "nwk" @@ -105,6 +106,10 @@ DOMAIN = "zha" +GROUP_ID = "group_id" +GROUP_IDS = "group_ids" +GROUP_NAME = "group_name" + MFG_CLUSTER_ID_START = 0xFC00 POWER_MAINS_POWERED = "Mains" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 9ace477d6212e5..77e0263c06c34a 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -13,6 +13,7 @@ import zigpy.exceptions from zigpy.profiles import zha, zll import zigpy.quirks +from zigpy.zcl.clusters.general import Groups from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( @@ -179,6 +180,17 @@ def is_end_device(self): """Return true if this device is an end device.""" return self._zigpy_device.node_desc.is_end_device + @property + def is_groupable(self): + """Return true if this device has a group cluster.""" + if not self.available: + return False + clusters = self.async_get_clusters() + for cluster_map in clusters.values(): + for clusters in cluster_map.values(): + if Groups.cluster_id in clusters: + return True + @property def gateway(self): """Return the gateway for this device.""" @@ -506,6 +518,14 @@ async def issue_cluster_command( ) return response + async def async_add_to_group(self, group_id): + """Add this device to the provided zigbee group.""" + await self._zigpy_device.add_to_group(group_id) + + async def async_remove_from_group(self, group_id): + """Remove this device from the provided zigbee group.""" + await self._zigpy_device.remove_from_group(group_id) + def log(self, level, msg, *args): """Log a message.""" msg = "[%s](%s): " + msg diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 583b4e0738b94d..57fb26db7f0334 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -93,6 +93,8 @@ def __init__(self, ieee, manufacturer, model): self.manufacturer = manufacturer self.model = model self.node_desc = zigpy.zdo.types.NodeDescriptor() + self.add_to_group = CoroutineMock() + self.remove_from_group = CoroutineMock() def make_device( diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index e34ad208744c1b..cc8f9366ecb0fc 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,7 +1,10 @@ """Test configuration for the ZHA component.""" +from unittest import mock from unittest.mock import patch import pytest +import zigpy +from zigpy.application import ControllerApplication from homeassistant import config_entries from homeassistant.components.zha.core.const import COMPONENTS, DATA_ZHA, DOMAIN @@ -12,6 +15,9 @@ from .common import async_setup_entry +FIXTURE_GRP_ID = 0x1001 +FIXTURE_GRP_NAME = "fixture group" + @pytest.fixture(name="config_entry") def config_entry_fixture(hass): @@ -43,6 +49,11 @@ async def zha_gateway_fixture(hass, config_entry): gateway = ZHAGateway(hass, {}, config_entry) gateway.zha_storage = zha_storage gateway.ha_device_registry = dev_reg + gateway.application_controller = mock.MagicMock(spec_set=ControllerApplication) + groups = zigpy.group.Groups(gateway.application_controller) + groups.listener_event = mock.MagicMock() + groups.add_group(FIXTURE_GRP_ID, FIXTURE_GRP_NAME, suppress_event=True) + gateway.application_controller.groups = groups return gateway diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 3fea9dfe0880f3..f01d27eb1670c4 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,7 +1,10 @@ """Test ZHA API.""" + import pytest +import zigpy import zigpy.zcl.clusters.general as general +from homeassistant.components.light import DOMAIN as light_domain from homeassistant.components.switch import DOMAIN from homeassistant.components.websocket_api import const from homeassistant.components.zha.api import ID, TYPE, async_load_api @@ -15,9 +18,13 @@ ATTR_NAME, ATTR_QUIRK_APPLIED, CLUSTER_TYPE_IN, + GROUP_ID, + GROUP_IDS, + GROUP_NAME, ) from .common import async_init_zigpy_device +from .conftest import FIXTURE_GRP_ID, FIXTURE_GRP_NAME @pytest.fixture @@ -36,9 +43,22 @@ async def zha_client(hass, config_entry, zha_gateway, hass_ws_client): zha_gateway, ) + await async_init_zigpy_device( + hass, + [general.OnOff.cluster_id, general.Basic.cluster_id, general.Groups.cluster_id], + [], + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, + zha_gateway, + manufacturer="FakeGroupManufacturer", + model="FakeGroupModel", + ieee="01:2d:6f:00:0a:90:69:e8", + ) + # load up switch domain await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.async_block_till_done() + await hass.config_entries.async_forward_entry_setup(config_entry, light_domain) + await hass.async_block_till_done() return await hass_ws_client(hass) @@ -114,15 +134,17 @@ async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_clie async def test_list_devices(hass, config_entry, zha_gateway, zha_client): - """Test getting entity cluster commands.""" + """Test getting zha devices.""" await zha_client.send_json({ID: 5, TYPE: "zha/devices"}) msg = await zha_client.receive_json() devices = msg["result"] - assert len(devices) == 1 + assert len(devices) == 2 + msg_id = 100 for device in devices: + msg_id += 1 assert device[ATTR_IEEE] is not None assert device[ATTR_MANUFACTURER] is not None assert device[ATTR_MODEL] is not None @@ -135,7 +157,7 @@ async def test_list_devices(hass, config_entry, zha_gateway, zha_client): assert entity_reference["entity_id"] is not None await zha_client.send_json( - {ID: 6, TYPE: "zha/device", ATTR_IEEE: device[ATTR_IEEE]} + {ID: msg_id, TYPE: "zha/device", ATTR_IEEE: device[ATTR_IEEE]} ) msg = await zha_client.receive_json() device2 = msg["result"] @@ -152,3 +174,151 @@ async def test_device_not_found(hass, config_entry, zha_gateway, zha_client): assert msg["type"] == const.TYPE_RESULT assert not msg["success"] assert msg["error"]["code"] == const.ERR_NOT_FOUND + + +async def test_list_groups(hass, config_entry, zha_gateway, zha_client): + """Test getting zha zigbee groups.""" + await zha_client.send_json({ID: 7, TYPE: "zha/groups"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + + groups = msg["result"] + assert len(groups) == 1 + + for group in groups: + assert group["group_id"] == FIXTURE_GRP_ID + assert group["name"] == FIXTURE_GRP_NAME + assert group["members"] == [] + + +async def test_get_group(hass, config_entry, zha_gateway, zha_client): + """Test getting a specific zha zigbee group.""" + await zha_client.send_json({ID: 8, TYPE: "zha/group", GROUP_ID: FIXTURE_GRP_ID}) + + msg = await zha_client.receive_json() + assert msg["id"] == 8 + assert msg["type"] == const.TYPE_RESULT + + group = msg["result"] + assert group is not None + assert group["group_id"] == FIXTURE_GRP_ID + assert group["name"] == FIXTURE_GRP_NAME + assert group["members"] == [] + + +async def test_get_group_not_found(hass, config_entry, zha_gateway, zha_client): + """Test not found response from get group API.""" + await zha_client.send_json({ID: 9, TYPE: "zha/group", GROUP_ID: 1234567}) + + msg = await zha_client.receive_json() + + assert msg["id"] == 9 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_NOT_FOUND + + +async def test_list_groupable_devices(hass, config_entry, zha_gateway, zha_client): + """Test getting zha devices that have a group cluster.""" + + # Make device available + zha_gateway.devices[ + zigpy.types.EUI64.convert("01:2d:6f:00:0a:90:69:e8") + ].set_available(True) + + await zha_client.send_json({ID: 10, TYPE: "zha/devices/groupable"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 10 + assert msg["type"] == const.TYPE_RESULT + + devices = msg["result"] + assert len(devices) == 1 + + for device in devices: + assert device[ATTR_IEEE] == "01:2d:6f:00:0a:90:69:e8" + assert device[ATTR_MANUFACTURER] 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[ATTR_NAME] is not None + assert entity_reference["entity_id"] is not None + + # Make sure there are no groupable devices when the device is unavailable + # Make device unavailable + zha_gateway.devices[ + zigpy.types.EUI64.convert("01:2d:6f:00:0a:90:69:e8") + ].set_available(False) + + await zha_client.send_json({ID: 11, TYPE: "zha/devices/groupable"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 11 + assert msg["type"] == const.TYPE_RESULT + + devices = msg["result"] + assert len(devices) == 0 + + +async def test_add_group(hass, config_entry, zha_gateway, zha_client): + """Test adding and getting a new zha zigbee group.""" + await zha_client.send_json({ID: 12, TYPE: "zha/group/add", GROUP_NAME: "new_group"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 12 + assert msg["type"] == const.TYPE_RESULT + + added_group = msg["result"] + + assert added_group["name"] == "new_group" + assert added_group["members"] == [] + + await zha_client.send_json({ID: 13, TYPE: "zha/groups"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 13 + assert msg["type"] == const.TYPE_RESULT + + groups = msg["result"] + assert len(groups) == 2 + + for group in groups: + assert group["name"] == FIXTURE_GRP_NAME or group["name"] == "new_group" + + +async def test_remove_group(hass, config_entry, zha_gateway, zha_client): + """Test removing a new zha zigbee group.""" + + await zha_client.send_json({ID: 14, TYPE: "zha/groups"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 14 + assert msg["type"] == const.TYPE_RESULT + + groups = msg["result"] + assert len(groups) == 1 + + await zha_client.send_json( + {ID: 15, TYPE: "zha/group/remove", GROUP_IDS: [FIXTURE_GRP_ID]} + ) + + msg = await zha_client.receive_json() + assert msg["id"] == 15 + assert msg["type"] == const.TYPE_RESULT + + groups_remaining = msg["result"] + assert len(groups_remaining) == 0 + + await zha_client.send_json({ID: 16, TYPE: "zha/groups"}) + + msg = await zha_client.receive_json() + assert msg["id"] == 16 + assert msg["type"] == const.TYPE_RESULT + + groups = msg["result"] + assert len(groups) == 0 From 8c1cdc0cf7a2012c0e68001650054a1fc8f874eb Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 9 Dec 2019 15:15:24 -0500 Subject: [PATCH 2353/3953] Add input_text reload service. (#29644) * Add input_text reload service. * Add test. --- .../components/input_text/__init__.py | 43 +++++++++--- .../components/input_text/services.yaml | 2 + tests/components/input_text/test_init.py | 67 ++++++++++++++++++- 3 files changed, 102 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 2d5a23e2a76d8e..77f007c5ba8fed 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -9,10 +9,12 @@ CONF_ICON, CONF_MODE, CONF_NAME, + SERVICE_RELOAD, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service _LOGGER = logging.getLogger(__name__) @@ -76,12 +78,43 @@ def _cv_input_text(cfg): required=True, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input text box.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(config) + + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + component.async_register_entity_service( + SERVICE_SET_VALUE, {vol.Required(ATTR_VALUE): cv.string}, "async_set_value" + ) + + if entities: + await component.async_add_entities(entities) + return True + + +async def _async_process_config(config): + """Process config and create list of entities.""" entities = [] for object_id, cfg in config[DOMAIN].items(): @@ -102,15 +135,7 @@ async def async_setup(hass, config): ) ) - if not entities: - return False - - component.async_register_entity_service( - SERVICE_SET_VALUE, {vol.Required(ATTR_VALUE): cv.string}, "async_set_value" - ) - - await component.async_add_entities(entities) - return True + return entities class InputText(RestoreEntity): diff --git a/homeassistant/components/input_text/services.yaml b/homeassistant/components/input_text/services.yaml index 219eecf2fd6d85..e9b709b0c03cd7 100644 --- a/homeassistant/components/input_text/services.yaml +++ b/homeassistant/components/input_text/services.yaml @@ -4,3 +4,5 @@ set_value: entity_id: {description: Entity id of the input text to set the new value., example: input_text.text1} value: {description: The target value the entity should be set to., example: This is an example text} +reload: + description: Reload the input_text configuration. diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index b758b245092dba..1bcf612c39b7f6 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -1,10 +1,14 @@ """The tests for the Input text component.""" # pylint: disable=protected-access import asyncio +from unittest.mock import patch + +import pytest from homeassistant.components.input_text import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_RELOAD from homeassistant.core import Context, CoreState, State +from homeassistant.exceptions import Unauthorized from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component @@ -195,3 +199,64 @@ async def test_config_none(hass): state = hass.states.get("input_text.b1") assert state assert str(state.state) == "unknown" + + +async def test_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {"test_1": {"initial": "test 1"}, "test_2": {"initial": "test 2"}}}, + ) + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_text.test_1") + state_2 = hass.states.get("input_text.test_2") + state_3 = hass.states.get("input_text.test_3") + + assert state_1 is not None + assert state_2 is not None + assert state_3 is None + assert "test 1" == state_1.state + assert "test 2" == state_2.state + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_2": {"initial": "test reloaded"}, + "test_3": {"initial": "test 3"}, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_text.test_1") + state_2 = hass.states.get("input_text.test_2") + state_3 = hass.states.get("input_text.test_3") + + assert state_1 is None + assert state_2 is not None + assert state_3 is not None + assert "test reloaded" == state_2.state + assert "test 3" == state_3.state From 454cc684e43447153f5374e5c31cd97ca3f38bc3 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 9 Dec 2019 15:15:32 -0500 Subject: [PATCH 2354/3953] Add input_select reload service. (#29647) * Add input_select reload service. * Add test. --- .../components/input_select/__init__.py | 48 +++++++--- .../components/input_select/services.yaml | 2 + tests/components/input_select/test_init.py | 89 ++++++++++++++++++- 3 files changed, 126 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index ae609e0927153b..b2b4b2083e8452 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -3,10 +3,11 @@ import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME +from homeassistant.const import CONF_ICON, CONF_NAME, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service _LOGGER = logging.getLogger(__name__) @@ -61,23 +62,31 @@ def _cv_input_select(cfg): required=True, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input select.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - entities = [] + entities = await _async_process_config(config) - for object_id, cfg in config[DOMAIN].items(): - name = cfg.get(CONF_NAME) - options = cfg.get(CONF_OPTIONS) - initial = cfg.get(CONF_INITIAL) - icon = cfg.get(CONF_ICON) - entities.append(InputSelect(object_id, name, initial, options, icon)) - - if not entities: - return False + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) component.async_register_entity_service( SERVICE_SELECT_OPTION, @@ -103,10 +112,25 @@ async def async_setup(hass, config): "async_set_options", ) - await component.async_add_entities(entities) + if entities: + await component.async_add_entities(entities) return True +async def _async_process_config(config): + """Process config and create list of entities.""" + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + name = cfg.get(CONF_NAME) + options = cfg.get(CONF_OPTIONS) + initial = cfg.get(CONF_INITIAL) + icon = cfg.get(CONF_ICON) + entities.append(InputSelect(object_id, name, initial, options, icon)) + + return entities + + class InputSelect(RestoreEntity): """Representation of a select input.""" diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index 8084e56b731e78..2cce496d0b6b4e 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -20,3 +20,5 @@ set_options: for., example: input_select.my_select} options: {description: Options for the input select entity., example: '["Item A", "Item B", "Item C"]'} +reload: + description: Reload the input_select configuration. diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 6c5d8501239d94..51f0b24bc8a0f4 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -1,6 +1,9 @@ """The tests for the Input select component.""" # pylint: disable=protected-access import asyncio +from unittest.mock import patch + +import pytest from homeassistant.components.input_select import ( ATTR_OPTION, @@ -11,8 +14,14 @@ SERVICE_SELECT_PREVIOUS, SERVICE_SET_OPTIONS, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + SERVICE_RELOAD, +) from homeassistant.core import Context, State +from homeassistant.exceptions import Unauthorized from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component @@ -322,3 +331,81 @@ async def test_input_select_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_1": { + "options": ["first option", "middle option", "last option"], + "initial": "middle option", + }, + "test_2": { + "options": ["an option", "not an option"], + "initial": "an option", + }, + } + }, + ) + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_select.test_1") + state_2 = hass.states.get("input_select.test_2") + state_3 = hass.states.get("input_select.test_3") + + assert state_1 is not None + assert state_2 is not None + assert state_3 is None + assert "middle option" == state_1.state + assert "an option" == state_2.state + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_2": { + "options": ["an option", "reloaded option"], + "initial": "reloaded option", + }, + "test_3": { + "options": ["new option", "newer option"], + "initial": "newer option", + }, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("input_select.test_1") + state_2 = hass.states.get("input_select.test_2") + state_3 = hass.states.get("input_select.test_3") + + assert state_1 is None + assert state_2 is not None + assert state_3 is not None + assert "reloaded option" == state_2.state + assert "newer option" == state_3.state From 38a6fffecb0009bd72b44830256032ae219e1c2d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 Dec 2019 22:43:38 +0100 Subject: [PATCH 2355/3953] Add JSON files validation to hassfest (#29799) --- script/hassfest/__main__.py | 22 ++++++++++++++++++++-- script/hassfest/json.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 script/hassfest/json.py diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 78b46b8db57d15..99e32e57f43292 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -2,10 +2,28 @@ import pathlib import sys -from . import codeowners, config_flow, dependencies, manifest, services, ssdp, zeroconf +from . import ( + codeowners, + config_flow, + dependencies, + json, + manifest, + services, + ssdp, + zeroconf, +) from .model import Config, Integration -PLUGINS = [codeowners, config_flow, dependencies, manifest, services, ssdp, zeroconf] +PLUGINS = [ + json, + codeowners, + config_flow, + dependencies, + manifest, + services, + ssdp, + zeroconf, +] def get_config() -> Config: diff --git a/script/hassfest/json.py b/script/hassfest/json.py new file mode 100644 index 00000000000000..73b6c372b4f5d3 --- /dev/null +++ b/script/hassfest/json.py @@ -0,0 +1,29 @@ +"""Validate integration JSON files.""" +import json +from typing import Dict + +from .model import Integration + + +def validate_json_files(integration: Integration): + """Validate JSON files for integration.""" + for json_file in integration.path.glob("**/*.json"): + if not json_file.is_file(): + continue + + try: + json.loads(json_file.read_text()) + except json.JSONDecodeError: + relative_path = json_file.relative_to(integration.path) + integration.add_error("json", f"Invalid JSON file {relative_path}") + + return + + +def validate(integrations: Dict[str, Integration], config): + """Handle JSON files inside integrations.""" + for integration in integrations.values(): + if not integration.manifest: + continue + + validate_json_files(integration) From 12f273eb11fc4bb282f158cf7dfe721eed9eefdd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 10 Dec 2019 00:32:16 +0000 Subject: [PATCH 2356/3953] [ci skip] Translation update --- .../alarm_control_panel/.translations/es.json | 7 ++++ .../.translations/pt-BR.json | 2 + .../almond/.translations/pt-BR.json | 9 +++++ .../components/cover/.translations/ru.json | 2 + .../components/demo/.translations/pt-BR.json | 5 +++ .../components/elgato/.translations/es.json | 27 +++++++++++++ .../components/elgato/.translations/it.json | 27 +++++++++++++ .../components/elgato/.translations/no.json | 20 +++++++++- .../components/elgato/.translations/pl.json | 27 +++++++++++++ .../components/elgato/.translations/ru.json | 27 +++++++++++++ .../components/fan/.translations/pt-BR.json | 8 ++++ .../geonetnz_volcano/.translations/pt-BR.json | 14 +++++++ .../components/hangouts/.translations/ru.json | 2 + .../components/icloud/.translations/es.json | 38 +++++++++++++++++++ .../components/plex/.translations/es.json | 1 + .../starline/.translations/pt-BR.json | 31 +++++++++++++++ .../tellduslive/.translations/ru.json | 1 + .../components/unifi/.translations/ru.json | 8 ++++ .../components/upnp/.translations/ru.json | 6 +++ .../components/vacuum/.translations/es.json | 2 +- .../components/zha/.translations/ru.json | 2 +- 21 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/almond/.translations/pt-BR.json create mode 100644 homeassistant/components/demo/.translations/pt-BR.json create mode 100644 homeassistant/components/elgato/.translations/es.json create mode 100644 homeassistant/components/elgato/.translations/it.json create mode 100644 homeassistant/components/elgato/.translations/pl.json create mode 100644 homeassistant/components/elgato/.translations/ru.json create mode 100644 homeassistant/components/fan/.translations/pt-BR.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/pt-BR.json create mode 100644 homeassistant/components/icloud/.translations/es.json create mode 100644 homeassistant/components/starline/.translations/pt-BR.json diff --git a/homeassistant/components/alarm_control_panel/.translations/es.json b/homeassistant/components/alarm_control_panel/.translations/es.json index 273efeeaba52f0..8200755de0fd1c 100644 --- a/homeassistant/components/alarm_control_panel/.translations/es.json +++ b/homeassistant/components/alarm_control_panel/.translations/es.json @@ -6,6 +6,13 @@ "arm_night": "Armar {entity_name} por la noche", "disarm": "Desarmar {entity_name}", "trigger": "Lanzar {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} armado fuera", + "armed_home": "{entity_name} armado en casa", + "armed_night": "{entity_name} armado modo noche", + "disarmed": "{entity_name} desarmado", + "triggered": "{entity_name} activado" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json index f72ae0e820ed86..032756f48f2606 100644 --- a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json +++ b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json @@ -8,6 +8,8 @@ "trigger": "Disparar {entidade_nome}" }, "trigger_type": { + "armed_away": "{entity_name} armado modo longe", + "armed_home": "{entidade_nome} armadado modo casa ", "armed_night": "{entity_name} armadado para noite", "disarmed": "{entity_name} desarmado", "triggered": "{entity_name} acionado" diff --git a/homeassistant/components/almond/.translations/pt-BR.json b/homeassistant/components/almond/.translations/pt-BR.json new file mode 100644 index 00000000000000..94dfbefb86a22c --- /dev/null +++ b/homeassistant/components/almond/.translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json index 043e5a42d2a954..ebe81486cf5371 100644 --- a/homeassistant/components/cover/.translations/ru.json +++ b/homeassistant/components/cover/.translations/ru.json @@ -9,7 +9,9 @@ "is_tilt_position": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \u043d\u0430\u043a\u043b\u043e\u043d\u0430" }, "trigger_type": { + "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0442\u043e", "closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0442\u043e", "opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", "tilt_position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043d\u0430\u043a\u043b\u043e\u043d" diff --git a/homeassistant/components/demo/.translations/pt-BR.json b/homeassistant/components/demo/.translations/pt-BR.json new file mode 100644 index 00000000000000..8183f28aed3f38 --- /dev/null +++ b/homeassistant/components/demo/.translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demonstra\u00e7\u00e3o" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/es.json b/homeassistant/components/elgato/.translations/es.json new file mode 100644 index 00000000000000..2e689b5e064a0c --- /dev/null +++ b/homeassistant/components/elgato/.translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo Elgato Key Light ya est\u00e1 configurado.", + "connection_error": "No se pudo conectar al dispositivo Elgato Key Light." + }, + "error": { + "connection_error": "No se pudo conectar al dispositivo Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "port": "N\u00famero de puerto" + }, + "description": "Configura tu Elgato Key Light para integrarlo con Home Assistant.", + "title": "Conecte su Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "\u00bfDesea agregar Elgato Key Light con el n\u00famero de serie `{serial_number}` a Home Assistant?", + "title": "Descubierto dispositivo Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/it.json b/homeassistant/components/elgato/.translations/it.json new file mode 100644 index 00000000000000..81e363aa01b085 --- /dev/null +++ b/homeassistant/components/elgato/.translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Questo dispositivo Elgato Key Light \u00e8 gi\u00e0 configurato.", + "connection_error": "Impossibile connettersi al dispositivo Elgato Key Light." + }, + "error": { + "connection_error": "Impossibile connettersi al dispositivo Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Host o indirizzo IP", + "port": "Numero porta" + }, + "description": "Configura Elgato Key Light per l'integrazione con Home Assistant.", + "title": "Collega il tuo Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vuoi aggiungere il dispositivo Elgato Key Light con il numero di serie {serial_number} a Home Assistant?", + "title": "Dispositivo Elgato Key Light rilevato" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/no.json b/homeassistant/components/elgato/.translations/no.json index df7d6db26216e1..8642ae75025732 100644 --- a/homeassistant/components/elgato/.translations/no.json +++ b/homeassistant/components/elgato/.translations/no.json @@ -1,11 +1,27 @@ { "config": { + "abort": { + "already_configured": "Denne Elgato Key Light-enheten er allerede konfigurert.", + "connection_error": "Kunne ikke koble til Elgato Key Light-enheten." + }, + "error": { + "connection_error": "Kunne ikke koble til Elgato Key Light-enheten." + }, + "flow_title": "Elgato Key Light: {serial_number}", "step": { "user": { "data": { + "host": "Vert eller IP-adresse", "port": "Portnummer" - } + }, + "description": "Sett opp Elgato Key Light for \u00e5 integrere med Home Assistant.", + "title": "Linken ditt Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vil du legge Elgato Key Light med serienummer ` {serial_number} til Home Assistant?", + "title": "Oppdaget Elgato Key Light-enheten" } - } + }, + "title": "Elgato Key Light" } } \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/pl.json b/homeassistant/components/elgato/.translations/pl.json new file mode 100644 index 00000000000000..97e10b451f0428 --- /dev/null +++ b/homeassistant/components/elgato/.translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "To urz\u0105dzenie Elgato Key Light jest ju\u017c skonfigurowane.", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Elgato Key Light." + }, + "error": { + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "description": "Konfiguracja Elgato Key Light w celu integracji z Home Assistant'em.", + "title": "Po\u0142\u0105cz swoje Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Czy chcesz doda\u0107 urz\u0105dzenie Elgato Key Light o numerze seryjnym `{serial_number}` do Home Assistant'a?", + "title": "Wykryto urz\u0105dzenie Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/ru.json b/homeassistant/components/elgato/.translations/ru.json new file mode 100644 index 00000000000000..3eeeed631c69fb --- /dev/null +++ b/homeassistant/components/elgato/.translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elgato Key Light \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Elgato Key Light." + }, + "error": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Elgato Key Light \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0432\u0430\u0448 Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Elgato Key Light \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c ` {serial_number} ` \u0432 Home Assistant?", + "title": "\u041d\u0430\u0439\u0434\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elgado Key Light" + } + }, + "title": "\u041e\u0441\u0432\u0435\u0442\u0438\u0442\u0435\u043b\u044c Elgado Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/pt-BR.json b/homeassistant/components/fan/.translations/pt-BR.json new file mode 100644 index 00000000000000..86b10b0f909e8e --- /dev/null +++ b/homeassistant/components/fan/.translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condtion_type": { + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/pt-BR.json b/homeassistant/components/geonetnz_volcano/.translations/pt-BR.json new file mode 100644 index 00000000000000..b16295999265f4 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "identifier_exists": "Local j\u00e1 registrado" + }, + "step": { + "user": { + "data": { + "radius": "Raio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 15d90a672ded13..5bb98effb9f767 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -14,6 +14,7 @@ "data": { "2fa": "\u041f\u0438\u043d-\u043a\u043e\u0434 \u0434\u043b\u044f \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" }, + "description": "\u043f\u0443\u0441\u0442\u043e", "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { @@ -22,6 +23,7 @@ "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, + "description": "\u043f\u0443\u0441\u0442\u043e", "title": "Google Hangouts" } }, diff --git a/homeassistant/components/icloud/.translations/es.json b/homeassistant/components/icloud/.translations/es.json new file mode 100644 index 00000000000000..13355fa2b8e107 --- /dev/null +++ b/homeassistant/components/icloud/.translations/es.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Cuenta ya configurada" + }, + "error": { + "login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a", + "send_verification_code": "Error al enviar el c\u00f3digo de verificaci\u00f3n", + "username_exists": "Cuenta ya configurada", + "validate_verification_code": "No se pudo verificar el c\u00f3digo de verificaci\u00f3n, elegir un dispositivo de confianza e iniciar la verificaci\u00f3n de nuevo" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositivo de confianza" + }, + "description": "Seleccione su dispositivo de confianza", + "title": "Dispositivo de confianza iCloud" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "description": "Ingrese sus credenciales", + "title": "Credenciales iCloud" + }, + "verification_code": { + "data": { + "verification_code": "C\u00f3digo de verificaci\u00f3n" + }, + "description": "Por favor, introduzca el c\u00f3digo de verificaci\u00f3n que acaba de recibir de iCloud", + "title": "C\u00f3digo de verificaci\u00f3n de iCloud" + } + }, + "title": "iCloud de Apple" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json index 261ca9514905ec..53dd322828853a 100644 --- a/homeassistant/components/plex/.translations/es.json +++ b/homeassistant/components/plex/.translations/es.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex se est\u00e1 configurando", "discovery_no_file": "No se ha encontrado ning\u00fan archivo de configuraci\u00f3n antiguo", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "non-interactive": "Importaci\u00f3n no interactiva", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Fall\u00f3 por razones desconocidas" }, diff --git a/homeassistant/components/starline/.translations/pt-BR.json b/homeassistant/components/starline/.translations/pt-BR.json new file mode 100644 index 00000000000000..158c2b01cf9cf6 --- /dev/null +++ b/homeassistant/components/starline/.translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "error_auth_mfa": "C\u00f3digo incorreto", + "error_auth_user": "Usu\u00e1rio ou senha incorretos" + }, + "step": { + "auth_app": { + "title": "Credenciais do aplicativo" + }, + "auth_captcha": { + "data": { + "captcha_code": "C\u00f3digo da imagem" + }, + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "C\u00f3digo SMS" + }, + "description": "Digite o c\u00f3digo enviado para o telefone {phone_number}", + "title": "Autoriza\u00e7\u00e3o de dois fatores" + }, + "auth_user": { + "data": { + "password": "Senha" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index 41dc39146e8d1c..fa5b7e2d3192fb 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -18,6 +18,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, + "description": "\u043f\u0443\u0441\u0442\u043e", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443." } }, diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index b01cdb84fbf929..3a67d483c0ce32 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -33,6 +33,14 @@ "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" } }, + "init": { + "data": { + "few": "\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e", + "many": "\u043c\u043d\u043e\u0433\u043e", + "one": "\u043e\u0434\u043d\u0438", + "other": "\u0434\u0440\u0443\u0433\u0438\u0435" + } + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "\u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 9599832799f9f0..6dce1b3d76c905 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -8,6 +8,12 @@ "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, + "error": { + "few": "\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e", + "many": "\u043c\u043d\u043e\u0433\u043e", + "one": "\u043e\u0434\u0438\u043d", + "other": "\u0434\u0440\u0443\u0433\u0438\u0435" + }, "step": { "confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c UPnP / IGD?", diff --git a/homeassistant/components/vacuum/.translations/es.json b/homeassistant/components/vacuum/.translations/es.json index 9ecf3ade99c891..f99e94a40a3855 100644 --- a/homeassistant/components/vacuum/.translations/es.json +++ b/homeassistant/components/vacuum/.translations/es.json @@ -6,7 +6,7 @@ }, "condtion_type": { "is_cleaning": "{entity_name} est\u00e1 limpiando", - "is_docked": "{entity_name} est\u00e1 acoplado" + "is_docked": "{entity_name} en la base" }, "trigger_type": { "cleaning": "{entity_name} empez\u00f3 a limpiar", diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index c0bc7c176a2a73..27682ebfd829b5 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -19,7 +19,7 @@ }, "device_automation": { "action_type": { - "squawk": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u0438\u0440\u0435\u043d\u0443", + "squawk": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u043d\u0434\u0435\u0440", "warn": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0435" }, "trigger_subtype": { From 315d0064fe0deaa35a459979f668fee9f22b9f91 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 10 Dec 2019 00:00:04 -0500 Subject: [PATCH 2357/3953] Fix zha circular import (#29802) * Refactor zha.core.helpers. * Make zha isort-able. * Const import reorg. * Keep DATA_ZHA config key on entry unload. * Cleanup ZHA config flow. * isort. * Add test. --- homeassistant/components/zha/__init__.py | 7 +-- homeassistant/components/zha/api.py | 31 ++-------- homeassistant/components/zha/config_flow.py | 23 +++++++- homeassistant/components/zha/core/gateway.py | 2 +- homeassistant/components/zha/core/helpers.py | 62 +++++++++----------- tests/components/zha/test_config_flow.py | 61 +++++++++++++++++-- 6 files changed, 113 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index a3c68ae3030ad4..7303367d485506 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -1,7 +1,4 @@ -"""Support for Zigbee Home Automation devices. - -isort:skip_file -""" +"""Support for Zigbee Home Automation devices.""" import logging @@ -11,7 +8,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE -from . import config_flow # noqa: F401 pylint: disable=unused-import from . import api from .core import ZHAGateway from .core.const import ( @@ -147,5 +143,4 @@ async def async_unload_entry(hass, config_entry): for component in COMPONENTS: await hass.config_entries.async_forward_entry_unload(config_entry, component) - del hass.data[DATA_ZHA] return True diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index e796c48c3f3326..462afd777b9c27 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -50,7 +50,11 @@ WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) -from .core.helpers import async_is_bindable_target, get_matched_clusters +from .core.helpers import ( + async_get_device_info, + async_is_bindable_target, + get_matched_clusters, +) _LOGGER = logging.getLogger(__name__) @@ -423,31 +427,6 @@ async def websocket_remove_group_members(hass, connection, msg): connection.send_result(msg[ID], ret_group) -@callback -def async_get_device_info(hass, device, ha_device_registry=None): - """Get ZHA device.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ret_device = {} - ret_device.update(device.device_info) - ret_device["entities"] = [ - { - "entity_id": entity_ref.reference_id, - ATTR_NAME: entity_ref.device_info[ATTR_NAME], - } - for entity_ref in zha_gateway.device_registry[device.ieee] - ] - - if ha_device_registry is not None: - reg_device = ha_device_registry.async_get_device( - {(DOMAIN, str(device.ieee))}, set() - ) - if reg_device is not None: - ret_device["user_given_name"] = reg_device.name_by_user - ret_device["device_reg_id"] = reg_device.id - ret_device["area_id"] = reg_device.area_id - return ret_device - - async def get_groups(hass,): """Get ZHA Groups.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 474cb15b41a46d..5ee0d0ee9bbf21 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -1,4 +1,5 @@ """Config flow for ZHA.""" +import asyncio from collections import OrderedDict import os @@ -9,11 +10,14 @@ from .core.const import ( CONF_RADIO_TYPE, CONF_USB_PATH, + CONTROLLER, + DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DOMAIN, + ZHA_GW_RADIO, RadioType, ) -from .core.helpers import check_zigpy_connection +from .core.registries import RADIO_TYPES @config_entries.HANDLERS.register(DOMAIN) @@ -57,3 +61,20 @@ async def async_step_import(self, import_info): return self.async_create_entry( title=import_info[CONF_USB_PATH], data=import_info ) + + +async def check_zigpy_connection(usb_path, radio_type, database_path): + """Test zigpy radio connection.""" + try: + radio = RADIO_TYPES[radio_type][ZHA_GW_RADIO]() + controller_application = RADIO_TYPES[radio_type][CONTROLLER] + except KeyError: + return False + try: + await radio.connect(usb_path, DEFAULT_BAUDRATE) + controller = controller_application(radio, database_path) + await asyncio.wait_for(controller.startup(auto_form=True), timeout=30) + await controller.shutdown() + except Exception: # pylint: disable=broad-except + return False + return True diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index ef81705ce471f1..72931c665ee820 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -20,7 +20,6 @@ ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from ..api import async_get_device_info from .const import ( ATTR_IEEE, ATTR_MANUFACTURER, @@ -65,6 +64,7 @@ ) from .device import DeviceStatus, ZHADevice from .discovery import async_dispatch_discovery_info, async_process_endpoint +from .helpers import async_get_device_info from .patches import apply_application_controller_patch from .registries import RADIO_TYPES from .store import async_get_registry diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index d3f06090dae34a..981a03fe7b56fb 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -4,29 +4,20 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ -import asyncio import collections import logging -import bellows.ezsp -import bellows.zigbee.application import zigpy.types -import zigpy_deconz.api -import zigpy_deconz.zigbee.application -import zigpy_xbee.api -import zigpy_xbee.zigbee.application -import zigpy_zigate.api -import zigpy_zigate.zigbee.application from homeassistant.core import callback from .const import ( + ATTR_NAME, CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DATA_ZHA, DATA_ZHA_GATEWAY, - DEFAULT_BAUDRATE, - RadioType, + DOMAIN, ) from .registries import BINDABLE_CLUSTERS @@ -56,30 +47,6 @@ async def safe_read( return {} -async def check_zigpy_connection(usb_path, radio_type, database_path): - """Test zigpy radio connection.""" - if radio_type == RadioType.ezsp.name: - radio = bellows.ezsp.EZSP() - ControllerApplication = bellows.zigbee.application.ControllerApplication - elif radio_type == RadioType.xbee.name: - radio = zigpy_xbee.api.XBee() - ControllerApplication = zigpy_xbee.zigbee.application.ControllerApplication - elif radio_type == RadioType.deconz.name: - radio = zigpy_deconz.api.Deconz() - ControllerApplication = zigpy_deconz.zigbee.application.ControllerApplication - elif radio_type == RadioType.zigate.name: - radio = zigpy_zigate.api.ZiGate() - ControllerApplication = zigpy_zigate.zigbee.application.ControllerApplication - try: - await radio.connect(usb_path, DEFAULT_BAUDRATE) - controller = ControllerApplication(radio, database_path) - await asyncio.wait_for(controller.startup(auto_form=True), timeout=30) - await controller.shutdown() - except Exception: # pylint: disable=broad-except - return False - return True - - def get_attr_id_by_name(cluster, attr_name): """Get the attribute id for a cluster attribute by its name.""" return next( @@ -164,3 +131,28 @@ def warning(self, msg, *args): def error(self, msg, *args): """Error level log.""" return self.log(logging.ERROR, msg, *args) + + +@callback +def async_get_device_info(hass, device, ha_device_registry=None): + """Get ZHA device.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ret_device = {} + ret_device.update(device.device_info) + ret_device["entities"] = [ + { + "entity_id": entity_ref.reference_id, + ATTR_NAME: entity_ref.device_info[ATTR_NAME], + } + for entity_ref in zha_gateway.device_registry[device.ieee] + ] + + if ha_device_registry is not None: + reg_device = ha_device_registry.async_get_device( + {(DOMAIN, str(device.ieee))}, set() + ) + if reg_device is not None: + ret_device["user_given_name"] = reg_device.name_by_user + ret_device["device_reg_id"] = reg_device.id + ret_device["area_id"] = reg_device.area_id + return ret_device diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 5e6bf51afd61df..fdff064a1c5796 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,8 +1,11 @@ """Tests for ZHA config flow.""" -from asynctest import patch +from unittest import mock + +import asynctest from homeassistant.components.zha import config_flow -from homeassistant.components.zha.core.const import DOMAIN +from homeassistant.components.zha.core.const import CONTROLLER, DOMAIN, ZHA_GW_RADIO +import homeassistant.components.zha.core.registries from tests.common import MockConfigEntry @@ -12,7 +15,7 @@ async def test_user_flow(hass): flow = config_flow.ZhaFlowHandler() flow.hass = hass - with patch( + with asynctest.patch( "homeassistant.components.zha.config_flow" ".check_zigpy_connection", return_value=False, ): @@ -22,7 +25,7 @@ async def test_user_flow(hass): assert result["errors"] == {"base": "cannot_connect"} - with patch( + with asynctest.patch( "homeassistant.components.zha.config_flow" ".check_zigpy_connection", return_value=True, ): @@ -71,3 +74,53 @@ async def test_import_flow_existing_config_entry(hass): ) assert result["type"] == "abort" + + +async def test_check_zigpy_connection(): + """Test config flow validator.""" + + mock_radio = asynctest.MagicMock() + mock_radio.connect = asynctest.CoroutineMock() + radio_cls = asynctest.MagicMock(return_value=mock_radio) + + bad_radio = asynctest.MagicMock() + bad_radio.connect = asynctest.CoroutineMock(side_effect=Exception) + bad_radio_cls = asynctest.MagicMock(return_value=bad_radio) + + mock_ctrl = asynctest.MagicMock() + mock_ctrl.startup = asynctest.CoroutineMock() + mock_ctrl.shutdown = asynctest.CoroutineMock() + ctrl_cls = asynctest.MagicMock(return_value=mock_ctrl) + new_radios = { + mock.sentinel.radio: {ZHA_GW_RADIO: radio_cls, CONTROLLER: ctrl_cls}, + mock.sentinel.bad_radio: {ZHA_GW_RADIO: bad_radio_cls, CONTROLLER: ctrl_cls}, + } + + with mock.patch.dict( + homeassistant.components.zha.core.registries.RADIO_TYPES, new_radios, clear=True + ): + assert not await config_flow.check_zigpy_connection( + mock.sentinel.usb_path, mock.sentinel.unk_radio, mock.sentinel.zigbee_db + ) + assert mock_radio.connect.call_count == 0 + assert bad_radio.connect.call_count == 0 + assert mock_ctrl.startup.call_count == 0 + assert mock_ctrl.shutdown.call_count == 0 + + # unsuccessful radio connect + assert not await config_flow.check_zigpy_connection( + mock.sentinel.usb_path, mock.sentinel.bad_radio, mock.sentinel.zigbee_db + ) + assert mock_radio.connect.call_count == 0 + assert bad_radio.connect.call_count == 1 + assert mock_ctrl.startup.call_count == 0 + assert mock_ctrl.shutdown.call_count == 0 + + # successful radio connect + assert await config_flow.check_zigpy_connection( + mock.sentinel.usb_path, mock.sentinel.radio, mock.sentinel.zigbee_db + ) + assert mock_radio.connect.call_count == 1 + assert bad_radio.connect.call_count == 1 + assert mock_ctrl.startup.call_count == 1 + assert mock_ctrl.shutdown.call_count == 1 From 9049e090f96f6d18bf6a84657d0696b58045d0a0 Mon Sep 17 00:00:00 2001 From: "Brett T. Warden" Date: Tue, 10 Dec 2019 00:20:52 -0800 Subject: [PATCH 2358/3953] Bump Roku to 4.0.0 (#29809) --- 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 f2639b31d1583a..1b5f07eb87a246 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/integrations/roku", "requirements": [ - "roku==3.1" + "roku==4.0.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index e5af926a51e707..5f6e9ac78aab2c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1751,7 +1751,7 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -roku==3.1 +roku==4.0.0 # homeassistant.components.roomba roombapy==1.4.2 From 4f1f4a1b4ff056cc9595ce6f7fb801d2a7b35b00 Mon Sep 17 00:00:00 2001 From: Ian Date: Tue, 10 Dec 2019 00:22:13 -0800 Subject: [PATCH 2359/3953] Nextbus: Sort results for upcoming times (#29811) Sort upcoming times across all direction's for a particular route and stop. --- homeassistant/components/nextbus/sensor.py | 4 +++- tests/components/nextbus/test_sensor.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 983c2272adcca1..5909804ebd1ac8 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -225,7 +225,9 @@ def update(self): return # Generate list of upcoming times - self._attributes["upcoming"] = ", ".join(p["minutes"] for p in predictions) + self._attributes["upcoming"] = ", ".join( + sorted(p["minutes"] for p in predictions) + ) latest_prediction = maybe_first(predictions) self._state = utc_from_timestamp( diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index bc74ebcbe1e0aa..bee9db445e26f9 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -206,7 +206,7 @@ async def test_direction_list( }, { "title": "Outbound 2", - "prediction": {"minutes": "4", "epochTime": "1553807374000"}, + "prediction": {"minutes": "0", "epochTime": "1553807374000"}, }, ], } @@ -221,7 +221,7 @@ async def test_direction_list( assert state.attributes["route"] == VALID_ROUTE_TITLE assert state.attributes["stop"] == VALID_STOP_TITLE assert state.attributes["direction"] == "Outbound, Outbound 2" - assert state.attributes["upcoming"] == "1, 2, 3, 4" + assert state.attributes["upcoming"] == "0, 1, 2, 3" async def test_custom_name( From 0ed6a434f8a68093597dc9c3bda42400c595a754 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 Dec 2019 09:22:37 +0100 Subject: [PATCH 2360/3953] Adjusts repository README (#29805) --- README.rst | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/README.rst b/README.rst index ae9531456fdbba..0de30d43c650f0 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,7 @@ Home Assistant |Chat Status| ================================================================================= -Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control. - -To get started: - -.. code:: bash - - python3 -m pip install homeassistant - hass --open-ui +Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server. Check out `home-assistant.io `__ for `a demo `__, `installation instructions `__, From 27244e29c40bfa2093028199e7a33af4a7068053 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 10 Dec 2019 09:24:49 +0100 Subject: [PATCH 2361/3953] Install discovery requirements if used (#29795) * Install discovery requirements if used * Update loader.py * Fix types --- .../components/deconz/config_flow.py | 5 +- homeassistant/components/hue/config_flow.py | 4 +- homeassistant/loader.py | 41 +++++++++++--- homeassistant/requirements.py | 18 +++++- script/hassfest/dependencies.py | 10 +++- tests/test_requirements.py | 55 +++++++++++++++++-- 6 files changed, 108 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index b757f1f4d03045..8c8ba318e8377d 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -169,10 +170,6 @@ async def _update_entry(self, entry, host): async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" - # Import it here, because only now do we know ssdp integration loaded. - # pylint: disable=import-outside-toplevel - from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL - if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL: return self.async_abort(reason="not_deconz_bridge") diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 84b435d02eda28..0423dc6fc2bd6c 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -134,9 +135,6 @@ async def async_step_ssdp(self, discovery_info): This flow is triggered by the SSDP component. It will check if the host is already configured and delegate to the import step if not. """ - # pylint: disable=import-outside-toplevel - from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME - if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL: return self.async_abort(reason="not_hue_bridge") diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 0e1ee8ae7567e9..9e8ea9fc7caebb 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -195,22 +195,45 @@ def __init__( hass: "HomeAssistant", pkg_path: str, file_path: pathlib.Path, - manifest: Dict, + manifest: Dict[str, Any], ): """Initialize an integration.""" self.hass = hass self.pkg_path = pkg_path self.file_path = file_path - self.name: str = manifest["name"] - self.domain: str = manifest["domain"] - self.dependencies: List[str] = manifest["dependencies"] - self.after_dependencies: Optional[List[str]] = manifest.get( - "after_dependencies" - ) - self.requirements: List[str] = manifest["requirements"] - self.config_flow: bool = manifest.get("config_flow", False) + self.manifest = manifest _LOGGER.info("Loaded %s from %s", self.domain, pkg_path) + @property + def name(self) -> str: + """Return name.""" + return cast(str, self.manifest["name"]) + + @property + def domain(self) -> str: + """Return domain.""" + return cast(str, self.manifest["domain"]) + + @property + def dependencies(self) -> List[str]: + """Return dependencies.""" + return cast(List[str], self.manifest.get("dependencies", [])) + + @property + def after_dependencies(self) -> List[str]: + """Return after_dependencies.""" + return cast(List[str], self.manifest.get("after_dependencies", [])) + + @property + def requirements(self) -> List[str]: + """Return requirements.""" + return cast(List[str], self.manifest.get("requirements", [])) + + @property + def config_flow(self) -> bool: + """Return config_flow.""" + return cast(bool, self.manifest.get("config_flow", False)) + @property def is_built_in(self) -> bool: """Test if package is a built-in integration.""" diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index bd52253cdb1365..670f3af7dcc9b3 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -3,7 +3,7 @@ import logging import os from pathlib import Path -from typing import Any, Dict, List, Optional, Set +from typing import Any, Dict, List, Optional, Set, Iterable from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -15,6 +15,10 @@ CONSTRAINT_FILE = "package_constraints.txt" PROGRESS_FILE = ".pip_progress" _LOGGER = logging.getLogger(__name__) +DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = { + "ssdp": ("ssdp",), + "zeroconf": ("zeroconf", "homekit"), +} class RequirementsNotFound(HomeAssistantError): @@ -30,7 +34,7 @@ def __init__(self, domain: str, requirements: List) -> None: async def async_get_integration_with_requirements( hass: HomeAssistant, domain: str, done: Set[str] = None ) -> Integration: - """Get an integration with installed requirements. + """Get an integration with all requirements installed, including the dependencies. This can raise IntegrationNotFound if manifest or integration is invalid, RequirementNotFound if there was some type of @@ -53,10 +57,18 @@ async def async_get_integration_with_requirements( deps_to_check = [ dep - for dep in integration.dependencies + (integration.after_dependencies or []) + for dep in integration.dependencies + integration.after_dependencies if dep not in done ] + for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): + if ( + check_domain not in done + and check_domain not in deps_to_check + and any(check in integration.manifest for check in to_check) + ): + deps_to_check.append(check_domain) + if deps_to_check: await asyncio.gather( *[ diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 71936411b75d47..b6f277438b405a 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -3,6 +3,8 @@ import re from typing import Dict, Set +from homeassistant.requirements import DISCOVERY_INTEGRATIONS + from .model import Integration @@ -49,7 +51,6 @@ def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> Set[ "system_log", "person", # Discovery - "ssdp", "discovery", # Other "mjpeg", # base class, has no reqs or component to load. @@ -93,6 +94,13 @@ def validate_dependencies(integration: Integration): referenced -= set(integration.manifest["dependencies"]) referenced -= set(integration.manifest.get("after_dependencies", [])) + # Discovery requirements are ok if referenced in manifest + for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): + if check_domain in referenced and any( + check in integration.manifest for check in to_check + ): + referenced.remove(check_domain) + if referenced: for domain in sorted(referenced): if ( diff --git a/tests/test_requirements.py b/tests/test_requirements.py index b807188e8a52e0..87bbf38e4651fa 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -2,10 +2,9 @@ import os from pathlib import Path from unittest.mock import call, patch +import pytest -from pytest import raises - -from homeassistant import setup +from homeassistant import setup, loader from homeassistant.requirements import ( CONSTRAINT_FILE, PROGRESS_FILE, @@ -15,7 +14,12 @@ async_process_requirements, ) -from tests.common import MockModule, get_test_home_assistant, mock_integration +from tests.common import ( + MockModule, + get_test_home_assistant, + mock_coro, + mock_integration, +) def env_without_wheel_links(): @@ -104,7 +108,7 @@ async def test_install_missing_package(hass): with patch( "homeassistant.util.package.install_package", return_value=False ) as mock_inst: - with raises(RequirementsNotFound): + with pytest.raises(RequirementsNotFound): await async_process_requirements(hass, "test_component", ["hello==1.0.0"]) assert len(mock_inst.mock_calls) == 1 @@ -222,3 +226,44 @@ def assert_env(req, **passed_kwargs): _install(hass, "hello", kwargs) assert not progress_path.exists() + + +async def test_discovery_requirements_ssdp(hass): + """Test that we load discovery requirements.""" + hass.config.skip_pip = False + ssdp = await loader.async_get_integration(hass, "ssdp") + + mock_integration( + hass, MockModule("ssdp_comp", partial_manifest={"ssdp": [{"st": "roku:ecp"}]}) + ) + with patch( + "homeassistant.requirements.async_process_requirements", + side_effect=lambda _, _2, _3: mock_coro(), + ) as mock_process: + await async_get_integration_with_requirements(hass, "ssdp_comp") + + assert len(mock_process.mock_calls) == 1 + assert mock_process.mock_calls[0][1][2] == ssdp.requirements + + +@pytest.mark.parametrize( + "partial_manifest", + [{"zeroconf": ["_googlecast._tcp.local."]}, {"homekit": {"models": ["LIFX"]}}], +) +async def test_discovery_requirements_zeroconf(hass, partial_manifest): + """Test that we load discovery requirements.""" + hass.config.skip_pip = False + zeroconf = await loader.async_get_integration(hass, "zeroconf") + + mock_integration( + hass, MockModule("comp", partial_manifest=partial_manifest), + ) + + with patch( + "homeassistant.requirements.async_process_requirements", + side_effect=lambda _, _2, _3: mock_coro(), + ) as mock_process: + await async_get_integration_with_requirements(hass, "comp") + + assert len(mock_process.mock_calls) == 2 # zeroconf also depends on http + assert mock_process.mock_calls[0][1][2] == zeroconf.requirements From f5d4878992d63683d3da661ef02ae7b51421beb4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 Dec 2019 09:25:42 +0100 Subject: [PATCH 2362/3953] Fix isort on a small set of misc files (#29803) --- docs/source/_ext/edit_on_github.py | 1 - docs/source/conf.py | 6 +++--- homeassistant/components/xiaomi_miio/fan.py | 1 - tests/components/feedreader/test_init.py | 3 +-- tests/components/icloud/test_config_flow.py | 7 +++---- .../jewish_calendar/test_binary_sensor.py | 4 ++-- tests/components/jewish_calendar/test_sensor.py | 4 ++-- tests/components/xiaomi_miio/test_vacuum.py | 14 +++++++------- 8 files changed, 18 insertions(+), 22 deletions(-) diff --git a/docs/source/_ext/edit_on_github.py b/docs/source/_ext/edit_on_github.py index eef249a3f019f3..a31fb13ebf115d 100644 --- a/docs/source/_ext/edit_on_github.py +++ b/docs/source/_ext/edit_on_github.py @@ -8,7 +8,6 @@ import os import warnings - __licence__ = 'BSD (3 clause)' diff --git a/docs/source/conf.py b/docs/source/conf.py index b5428ede8fa10d..f36b5b8124a417 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,11 +17,11 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import sys -import os import inspect +import os +import sys -from homeassistant.const import __version__, __short_version__ +from homeassistant.const import __short_version__, __version__ PROJECT_NAME = 'Home Assistant' PROJECT_PACKAGE_NAME = 'homeassistant' diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index bf3691eb48613e..7cb45296506625 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -58,7 +58,6 @@ SERVICE_SET_VOLUME, ) - _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Xiaomi Miio Device" diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index d74bbd79d804b1..bffbe9676fa9ff 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -1,5 +1,6 @@ """The tests for the feedreader component.""" from datetime import timedelta +from genericpath import exists from logging import getLogger from os import remove import time @@ -7,8 +8,6 @@ from unittest import mock from unittest.mock import patch -from genericpath import exists - from homeassistant.components import feedreader from homeassistant.components.feedreader import ( CONF_MAX_ENTRIES, diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index b292a9e258cf37..5555150befcb2b 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -1,23 +1,22 @@ """Tests for the iCloud config flow.""" -from unittest.mock import patch, Mock, MagicMock -import pytest +from unittest.mock import MagicMock, Mock, patch from pyicloud.exceptions import PyiCloudFailedLoginException +import pytest from homeassistant import data_entry_flow from homeassistant.components.icloud import config_flow - from homeassistant.components.icloud.config_flow import ( CONF_TRUSTED_DEVICE, CONF_VERIFICATION_CODE, ) from homeassistant.components.icloud.const import ( - DOMAIN, CONF_ACCOUNT_NAME, CONF_GPS_ACCURACY_THRESHOLD, CONF_MAX_INTERVAL, DEFAULT_GPS_ACCURACY_THRESHOLD, DEFAULT_MAX_INTERVAL, + DOMAIN, ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index 0daa7c68993904..a9ea2449c6f6f6 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -8,10 +8,10 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed - from . import alter_time, make_jerusalem_test_params, make_nyc_test_params +from tests.common import async_fire_time_changed + MELACHA_PARAMS = [ make_nyc_test_params(dt(2018, 9, 1, 16, 0), STATE_ON), make_nyc_test_params(dt(2018, 9, 1, 20, 21), STATE_OFF), diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 7ed3fdccb6269c..e630702c6b2f42 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -7,10 +7,10 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed - from . import alter_time, make_jerusalem_test_params, make_nyc_test_params +from tests.common import async_fire_time_changed + async def test_jewish_calendar_min_config(hass): """Test minimum jewish calendar configuration.""" diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 5b6ce578c8b5a1..6b2eb87f1538c7 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -1,6 +1,6 @@ """The tests for the Xiaomi vacuum platform.""" import asyncio -from datetime import timedelta, time +from datetime import time, timedelta from unittest import mock import pytest @@ -22,26 +22,26 @@ ) from homeassistant.components.xiaomi_miio.vacuum import ( ATTR_CLEANED_AREA, + ATTR_CLEANED_TOTAL_AREA, + ATTR_CLEANING_COUNT, ATTR_CLEANING_TIME, + ATTR_CLEANING_TOTAL_TIME, ATTR_DO_NOT_DISTURB, - ATTR_DO_NOT_DISTURB_START, ATTR_DO_NOT_DISTURB_END, + ATTR_DO_NOT_DISTURB_START, ATTR_ERROR, + ATTR_FILTER_LEFT, ATTR_MAIN_BRUSH_LEFT, ATTR_SIDE_BRUSH_LEFT, - ATTR_FILTER_LEFT, - ATTR_CLEANING_COUNT, - ATTR_CLEANED_TOTAL_AREA, - ATTR_CLEANING_TOTAL_TIME, CONF_HOST, CONF_NAME, CONF_TOKEN, DOMAIN as XIAOMI_DOMAIN, + SERVICE_CLEAN_ZONE, SERVICE_MOVE_REMOTE_CONTROL, SERVICE_MOVE_REMOTE_CONTROL_STEP, SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL, - SERVICE_CLEAN_ZONE, ) from homeassistant.const import ( ATTR_ENTITY_ID, From db0baab692a2e4fbe7ba1f378cda3f6cd9fe42e2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 Dec 2019 11:02:26 +0100 Subject: [PATCH 2363/3953] Only update disabled_by when enabled default and entity enabled states differ (#29643) --- homeassistant/components/unifi/device_tracker.py | 10 ++++++---- homeassistant/components/unifi/sensor.py | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 0806a2c04e3f4e..159949f29853eb 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -75,6 +75,9 @@ def update_disable_on_entities(): """Update the values of the controller.""" for entity in tracked.values(): + if entity.entity_registry_enabled_default == entity.enabled: + continue + disabled_by = None if not entity.entity_registry_enabled_default and entity.enabled: disabled_by = DISABLED_CONFIG_ENTRY @@ -227,10 +230,9 @@ def __init__(self, device, controller): @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" - if not self.controller.option_track_devices: - return False - - return True + if self.controller.option_track_devices: + return True + return False async def async_added_to_hass(self): """Subscribe to device events.""" diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index e4f9b0df6c9dab..9145fd8e00f48d 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -40,6 +40,9 @@ def update_disable_on_entities(): """Update the values of the controller.""" for entity in sensors.values(): + if entity.entity_registry_enabled_default == entity.enabled: + continue + disabled_by = None if not entity.entity_registry_enabled_default and entity.enabled: disabled_by = DISABLED_CONFIG_ENTRY From 22e7ece31586ac3ec30e9a8e06de8bd1e7e41d35 Mon Sep 17 00:00:00 2001 From: David K <142583+neffs@users.noreply.github.com> Date: Fri, 6 Dec 2019 14:07:45 +0100 Subject: [PATCH 2364/3953] Limit available heat/cool modes for HomeKit thermostats (#28586) * Limit available heat/cool modes for HomeKit thermostats. The Home app only shows appropriate modes (heat/cool/auto) for the device. Depending on the climate integration, disabling the auto start might be needed. * Include improved mapping for HVAC modes in tests --- .../components/homekit/type_thermostats.py | 57 ++++++++++-- .../homekit/test_type_thermostats.py | 87 ++++++++++++++----- 2 files changed, 115 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 9adc3cc0600bf5..b6e1e75d3c6529 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -7,6 +7,7 @@ ATTR_CURRENT_TEMPERATURE, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, + ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TARGET_TEMP_HIGH, @@ -23,6 +24,8 @@ HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_FAN_ONLY, SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -60,13 +63,18 @@ _LOGGER = logging.getLogger(__name__) +HC_HOMEKIT_VALID_MODES_WATER_HEATER = { + "Heat": 1, +} UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()} HC_HASS_TO_HOMEKIT = { HVAC_MODE_OFF: 0, HVAC_MODE_HEAT: 1, HVAC_MODE_COOL: 2, + HVAC_MODE_AUTO: 3, HVAC_MODE_HEAT_COOL: 3, + HVAC_MODE_FAN_ONLY: 2, } HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} @@ -97,9 +105,9 @@ def __init__(self, *args): # Add additional characteristics if auto mode is supported self.chars = [] - features = self.hass.states.get(self.entity_id).attributes.get( - ATTR_SUPPORTED_FEATURES, 0 - ) + state = self.hass.states.get(self.entity_id) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & SUPPORT_TARGET_TEMPERATURE_RANGE: self.chars.extend( (CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) @@ -107,12 +115,44 @@ def __init__(self, *args): serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) - # Current and target mode characteristics + # Current mode characteristics self.char_current_heat_cool = serv_thermostat.configure_char( CHAR_CURRENT_HEATING_COOLING, value=0 ) + + # Target mode characteristics + hc_modes = state.attributes.get(ATTR_HVAC_MODES, None) + if hc_modes is None: + _LOGGER.error( + "%s: HVAC modes not yet available. Please disable auto start for homekit.", + self.entity_id, + ) + hc_modes = ( + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + ) + + # determine available modes for this entity, prefer AUTO over HEAT_COOL and COOL over FAN_ONLY + self.hc_homekit_to_hass = { + c: s + for s, c in HC_HASS_TO_HOMEKIT.items() + if ( + s in hc_modes + and not ( + (s == HVAC_MODE_HEAT_COOL and HVAC_MODE_AUTO in hc_modes) + or (s == HVAC_MODE_FAN_ONLY and HVAC_MODE_COOL in hc_modes) + ) + ) + } + hc_valid_values = {k: v for v, k in self.hc_homekit_to_hass.items()} + self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=0, setter_callback=self.set_heat_cool + CHAR_TARGET_HEATING_COOLING, + value=0, + setter_callback=self.set_heat_cool, + valid_values=hc_valid_values, ) # Current and target temperature characteristics @@ -185,7 +225,7 @@ def set_heat_cool(self, value): """Change operation mode to value if call came from HomeKit.""" _LOGGER.debug("%s: Set heat-cool to %d", self.entity_id, value) self._flag_heat_cool = True - hass_value = HC_HOMEKIT_TO_HASS[value] + hass_value = self.hc_homekit_to_hass[value] params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HVAC_MODE: hass_value} self.call_service( DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE_THERMOSTAT, params, hass_value @@ -318,7 +358,10 @@ def __init__(self, *args): CHAR_CURRENT_HEATING_COOLING, value=1 ) self.char_target_heat_cool = serv_thermostat.configure_char( - CHAR_TARGET_HEATING_COOLING, value=1, setter_callback=self.set_heat_cool + CHAR_TARGET_HEATING_COOLING, + value=1, + setter_callback=self.set_heat_cool, + valid_values=HC_HOMEKIT_VALID_MODES_WATER_HEATER, ) self.char_current_temp = serv_thermostat.configure_char( diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index c896ad211e8685..9f9ebcdfd32ee3 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -24,6 +24,8 @@ HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_AUTO, ) from homeassistant.components.homekit.const import ( ATTR_VALUE, @@ -64,7 +66,20 @@ async def test_thermostat(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" - hass.states.async_set(entity_id, HVAC_MODE_OFF) + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) await hass.async_add_job(acc.run) @@ -120,7 +135,7 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, { ATTR_TEMPERATURE: 20.0, ATTR_CURRENT_TEMPERATURE: 25.0, @@ -164,9 +179,8 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, @@ -183,7 +197,6 @@ async def test_thermostat(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 25.0, ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, @@ -198,9 +211,8 @@ async def test_thermostat(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL], ATTR_TEMPERATURE: 22.0, ATTR_CURRENT_TEMPERATURE: 22.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -226,14 +238,23 @@ async def test_thermostat(hass, hk_driver, cls, events): assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "19.0°C" - await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 2) await hass.async_block_till_done() assert call_set_hvac_mode assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT - assert acc.char_target_heat_cool.value == 1 + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + assert acc.char_target_heat_cool.value == 2 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == HVAC_MODE_HEAT + assert events[-1].data[ATTR_VALUE] == HVAC_MODE_COOL + + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3) + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_AUTO + assert acc.char_target_heat_cool.value == 3 + assert len(events) == 3 + assert events[-1].data[ATTR_VALUE] == HVAC_MODE_AUTO async def test_thermostat_auto(hass, hk_driver, cls, events): @@ -261,7 +282,6 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 22.0, ATTR_TARGET_TEMP_LOW: 20.0, ATTR_CURRENT_TEMPERATURE: 18.0, @@ -278,9 +298,8 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 24.0, @@ -291,15 +310,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): assert acc.char_heating_thresh_temp.value == 19.0 assert acc.char_cooling_thresh_temp.value == 23.0 assert acc.char_current_heat_cool.value == 2 - assert acc.char_target_heat_cool.value == 3 + assert acc.char_target_heat_cool.value == 2 assert acc.char_current_temp.value == 24.0 assert acc.char_display_units.value == 0 hass.states.async_set( entity_id, - HVAC_MODE_HEAT_COOL, + HVAC_MODE_AUTO, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 23.0, ATTR_TARGET_TEMP_LOW: 19.0, ATTR_CURRENT_TEMPERATURE: 21.0, @@ -346,7 +364,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): HVAC_MODE_HEAT, { ATTR_SUPPORTED_FEATURES: 4096, - ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_HEAT, @@ -364,7 +381,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): entity_id, HVAC_MODE_OFF, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -378,7 +394,6 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): entity_id, HVAC_MODE_OFF, { - ATTR_HVAC_MODE: HVAC_MODE_OFF, ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0, ATTR_HVAC_ACTION: CURRENT_HVAC_IDLE, @@ -423,7 +438,6 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): entity_id, HVAC_MODE_HEAT_COOL, { - ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL, ATTR_TARGET_TEMP_HIGH: 75.2, ATTR_TARGET_TEMP_LOW: 68.1, ATTR_TEMPERATURE: 71.6, @@ -503,6 +517,34 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): assert acc.char_target_temp.properties[PROP_MIN_STEP] == 1.0 +async def test_thermostat_hvac_modes(hass, hk_driver, cls): + """Test if unsupported HVAC modes are deactivated in HomeKit.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, HVAC_MODE_OFF, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]} + ) + + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 3) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 0 + + await hass.async_add_job(acc.char_target_heat_cool.set_value, 1) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 2) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == 1 + + async def test_water_heater(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "water_heater.test" @@ -571,7 +613,8 @@ async def test_water_heater(hass, hk_driver, cls, events): await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 - await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 3) + with pytest.raises(ValueError): + await hass.async_add_job(acc.char_target_heat_cool.set_value, 3) await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 1 From a4fc4bb2812b968fc8d0612f63bd6f5613bb8d08 Mon Sep 17 00:00:00 2001 From: tetienne Date: Thu, 5 Dec 2019 21:33:56 +0100 Subject: [PATCH 2365/3953] Increase somfy SCAN_INTERVAL (#29524) - There was too many errors 504 --- homeassistant/components/somfy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 184e32f1e6de69..1368725777bf5f 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=30) DOMAIN = "somfy" From d9280330a627736d7ec2c88ea3a365f47a59b120 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 6 Dec 2019 16:55:42 +1100 Subject: [PATCH 2366/3953] Bump georss_generic_client to 0.3 (#29532) * bump version of georss_generic_client library * updated requirements --- homeassistant/components/geo_rss_events/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index c681807ad0113d..b8949286dea6c5 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -3,7 +3,7 @@ "name": "Geo RSS events", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": [ - "georss_generic_client==0.2" + "georss_generic_client==0.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c6dad0573ca838..1fd51ad5d0019c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -563,7 +563,7 @@ geojson_client==0.4 geopy==1.19.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.2 +georss_generic_client==0.3 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 484dba217a8fa6..6b5bcf96d2d392 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -181,7 +181,7 @@ geojson_client==0.4 geopy==1.19.0 # homeassistant.components.geo_rss_events -georss_generic_client==0.2 +georss_generic_client==0.3 # homeassistant.components.ign_sismologia georss_ign_sismologia_client==0.2 From 2d6599fdd22c2096fe987a1ec259c1a81ff83af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 6 Dec 2019 22:53:26 +0200 Subject: [PATCH 2367/3953] Huawei LTE device tracker fixes (#29551) * Include MAC address in device state attributes for absent devices too * Use MAC address as default name whether device is connected or not * Fix initialization of known entities Closes https://github.com/home-assistant/home-assistant/issues/29354 --- .../components/huawei_lte/device_tracker.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 29d7f437b5f54a..f5f834fa18674a 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -2,7 +2,7 @@ import logging import re -from typing import Any, Dict, Set +from typing import Any, Dict, List, Optional, Set import attr from stringcase import snakecase @@ -40,13 +40,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Initialize already tracked entities tracked: Set[str] = set() registry = await entity_registry.async_get_registry(hass) + known_entities: List[HuaweiLteScannerEntity] = [] for entity in registry.entities.values(): if ( entity.domain == DEVICE_TRACKER_DOMAIN and entity.config_entry_id == config_entry.entry_id ): tracked.add(entity.unique_id) - async_add_new_entities(hass, router.url, async_add_entities, tracked, True) + known_entities.append( + HuaweiLteScannerEntity(router, entity.unique_id.partition("-")[2]) + ) + async_add_entities(known_entities, True) # Tell parent router to poll hosts list to gather new devices router.subscriptions[KEY_WLAN_HOST_LIST].add(_DEVICE_SCAN) @@ -66,13 +70,8 @@ async def _async_maybe_add_new_entities(url: str) -> None: async_add_new_entities(hass, router.url, async_add_entities, tracked) -def async_add_new_entities( - hass, router_url, async_add_entities, tracked, included: bool = False -): - """Add new entities. - - :param included: if True, setup only items in tracked, and vice versa - """ +def async_add_new_entities(hass, router_url, async_add_entities, tracked): + """Add new entities that are not already being tracked.""" router = hass.data[DOMAIN].routers[router_url] try: hosts = router.data[KEY_WLAN_HOST_LIST]["Hosts"]["Host"] @@ -83,8 +82,7 @@ def async_add_new_entities( new_entities = [] for host in (x for x in hosts if x.get("MacAddress")): entity = HuaweiLteScannerEntity(router, host["MacAddress"]) - tracking = entity.unique_id in tracked - if tracking != included: + if entity.unique_id in tracked: continue tracked.add(entity.unique_id) new_entities.append(entity) @@ -113,12 +111,16 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): mac: str = attr.ib() _is_connected: bool = attr.ib(init=False, default=False) - _name: str = attr.ib(init=False, default="device") + _hostname: Optional[str] = attr.ib(init=False, default=None) _device_state_attributes: Dict[str, Any] = attr.ib(init=False, factory=dict) + def __attrs_post_init__(self): + """Initialize internal state.""" + self._device_state_attributes["mac_address"] = self.mac + @property def _entity_name(self) -> str: - return self._name + return self._hostname or self.mac @property def _device_unique_id(self) -> str: @@ -145,8 +147,7 @@ async def async_update(self) -> None: host = next((x for x in hosts if x.get("MacAddress") == self.mac), None) self._is_connected = host is not None if self._is_connected: - # HostName may be present with explicit None value - self._name = host.get("HostName") or self.mac + self._hostname = host.get("HostName") self._device_state_attributes = { _better_snakecase(k): v for k, v in host.items() if k != "HostName" } From 90de5652b9621ab30d1602fdc90e83f49384f20e Mon Sep 17 00:00:00 2001 From: Santobert Date: Mon, 9 Dec 2019 10:46:25 +0100 Subject: [PATCH 2368/3953] Change source of device_info (#29570) --- homeassistant/components/neato/vacuum.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 9cac0cd24ceb79..d8a3e4ded45239 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -165,7 +165,7 @@ def update(self): _LOGGER.debug("Running Neato Vacuums update") try: if self._robot_stats is None: - self._robot_stats = self.robot.get_robot_info().json() + self._robot_stats = self.robot.get_general_info().json().get("data") except NeatoRobotException: _LOGGER.warning("Couldn't fetch robot information of %s", self._name) @@ -319,10 +319,9 @@ def device_info(self): """Device info for neato robot.""" info = {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}, "name": self._name} if self._robot_stats: - info["manufacturer"] = self._robot_stats["data"]["mfg_name"] - info["model"] = self._robot_stats["data"]["modelName"] - if self._state: - info["sw_version"] = self._state["meta"]["firmware"] + info["manufacturer"] = self._robot_stats["battery"]["vendor"] + info["model"] = self._robot_stats["model"] + info["sw_version"] = self._robot_stats["firmware"] def start(self): """Start cleaning or resume cleaning.""" From f7015703bb53bc24011e2686be67a65e93d9c189 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 Dec 2019 11:02:26 +0100 Subject: [PATCH 2369/3953] Only update disabled_by when enabled default and entity enabled states differ (#29643) --- homeassistant/components/unifi/device_tracker.py | 10 ++++++---- homeassistant/components/unifi/sensor.py | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b92211c4eae5a2..46ac3468835e80 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -76,6 +76,9 @@ def update_disable_on_entities(): """Update the values of the controller.""" for entity in tracked.values(): + if entity.entity_registry_enabled_default == entity.enabled: + continue + disabled_by = None if not entity.entity_registry_enabled_default and entity.enabled: disabled_by = DISABLED_CONFIG_ENTRY @@ -228,10 +231,9 @@ def __init__(self, device, controller): @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" - if not self.controller.option_track_devices: - return False - - return True + if self.controller.option_track_devices: + return True + return False async def async_added_to_hass(self): """Subscribe to device events.""" diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index e4f9b0df6c9dab..9145fd8e00f48d 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -40,6 +40,9 @@ def update_disable_on_entities(): """Update the values of the controller.""" for entity in sensors.values(): + if entity.entity_registry_enabled_default == entity.enabled: + continue + disabled_by = None if not entity.entity_registry_enabled_default and entity.enabled: disabled_by = DISABLED_CONFIG_ENTRY From 70fe5d323827178dfad38eb4da49855eae2ba5c3 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Mon, 9 Dec 2019 12:56:57 +0300 Subject: [PATCH 2370/3953] Fix unit_of_measurement for Starline temperature sensors (#29740) --- homeassistant/components/starline/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 2507aba4955d12..ba0cef0255d65c 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -1,5 +1,6 @@ """Reads vehicle status from StarLine API.""" from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level from .account import StarlineAccount, StarlineDevice @@ -9,8 +10,8 @@ SENSOR_TYPES = { "battery": ["Battery", None, "V", None], "balance": ["Balance", None, None, "mdi:cash-multiple"], - "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, None, None], - "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, None, None], + "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], + "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], "gsm_lvl": ["GSM Signal", None, "%", None], } From 19920e48ba18e1f08bde0ed47c3aaa72f833e186 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 9 Dec 2019 13:33:28 +0100 Subject: [PATCH 2371/3953] HomeAssistant-pyozw 0.1.7 (#29743) --- homeassistant/components/zwave/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index 78362b25462e9b..c781a493b55088 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", - "requirements": ["homeassistant-pyozw==0.1.6", "pydispatcher==2.0.5"], + "requirements": ["homeassistant-pyozw==0.1.7", "pydispatcher==2.0.5"], "dependencies": [], "codeowners": ["@home-assistant/z-wave"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1fd51ad5d0019c..8723ad4b372829 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -667,7 +667,7 @@ holidays==0.9.11 home-assistant-frontend==20191204.0 # homeassistant.components.zwave -homeassistant-pyozw==0.1.6 +homeassistant-pyozw==0.1.7 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b5bcf96d2d392..dc5a12ea9e8161 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,7 +231,7 @@ holidays==0.9.11 home-assistant-frontend==20191204.0 # homeassistant.components.zwave -homeassistant-pyozw==0.1.6 +homeassistant-pyozw==0.1.7 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From d53c6be50adace585b85627d0d18516e8cf28915 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 9 Dec 2019 15:10:03 +0100 Subject: [PATCH 2372/3953] Updated frontend to 20191204.1 (#29787) --- 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 03d2d7899b07ba..75d02baaeeb23b 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191204.0" + "home-assistant-frontend==20191204.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e0a823da44e733..90a6ac8c8a74f0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.30 -home-assistant-frontend==20191204.0 +home-assistant-frontend==20191204.1 importlib-metadata==0.23 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8723ad4b372829..7392c26562a6b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -664,7 +664,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191204.0 +home-assistant-frontend==20191204.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc5a12ea9e8161..036de45e342926 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,7 +228,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191204.0 +home-assistant-frontend==20191204.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.7 From 807badea9384bb043cd674d7c2630edb304dc0ac Mon Sep 17 00:00:00 2001 From: "Brett T. Warden" Date: Tue, 10 Dec 2019 00:20:52 -0800 Subject: [PATCH 2373/3953] Bump Roku to 4.0.0 (#29809) --- 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 f2639b31d1583a..1b5f07eb87a246 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/integrations/roku", "requirements": [ - "roku==3.1" + "roku==4.0.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 7392c26562a6b1..9e2f31443e5ebf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1742,7 +1742,7 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -roku==3.1 +roku==4.0.0 # homeassistant.components.roomba roombapy==1.4.2 From 302d5d66ab42067ba29269b36877520072eb4b95 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 10 Dec 2019 13:05:22 +0100 Subject: [PATCH 2374/3953] Bumped version to 0.103.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 07a4284c491853..35ee19da6b596f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 103 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From e28fd16c84ec04eaae3a489f2f9815f4e607a628 Mon Sep 17 00:00:00 2001 From: proferabg Date: Tue, 10 Dec 2019 13:25:22 -0500 Subject: [PATCH 2375/3953] Fix tank utility token (#29801) * Tank Utility Token Fix Fix 400 Bad Request on Invalid Token * Format Fix for Pull Request #29801 * Pylint Annotation Fix for Pull Request #29801 --- homeassistant/components/tank_utility/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 3ab4a027b04f2d..6848b263633f1e 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -116,6 +116,8 @@ def get_data(self): if ( http_error.response.status_code == requests.codes.unauthorized # pylint: disable=no-member + or http_error.response.status_code + == requests.codes.bad_request # pylint: disable=no-member ): _LOGGER.info("Getting new token") self._token = tank_utility.auth.get_token( From 47e5142ddbc78d09a767c126e13b7a157fbc03e1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 Dec 2019 20:04:48 +0100 Subject: [PATCH 2376/3953] UniFi - Handle disabled switches (#29824) --- homeassistant/components/unifi/switch.py | 33 ++++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 18bfbb4066e146..e4192d0b6c77eb 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -73,12 +73,13 @@ def update_items(controller, async_add_entities, switches, switches_off): block_client_id = f"block-{client_id}" if block_client_id in switches: - LOGGER.debug( - "Updating UniFi block switch %s (%s)", - switches[block_client_id].entity_id, - switches[block_client_id].client.mac, - ) - switches[block_client_id].async_schedule_update_ha_state() + if switches[block_client_id].enabled: + LOGGER.debug( + "Updating UniFi block switch %s (%s)", + switches[block_client_id].entity_id, + switches[block_client_id].client.mac, + ) + switches[block_client_id].async_schedule_update_ha_state() continue if client_id not in controller.api.clients_all: @@ -87,7 +88,6 @@ def update_items(controller, async_add_entities, switches, switches_off): client = controller.api.clients_all[client_id] switches[block_client_id] = UniFiBlockClientSwitch(client, controller) new_switches.append(switches[block_client_id]) - LOGGER.debug("New UniFi Block switch %s (%s)", client.hostname, client.mac) # control POE for client_id in controller.api.clients: @@ -95,12 +95,13 @@ def update_items(controller, async_add_entities, switches, switches_off): poe_client_id = f"poe-{client_id}" if poe_client_id in switches: - LOGGER.debug( - "Updating UniFi POE switch %s (%s)", - switches[poe_client_id].entity_id, - switches[poe_client_id].client.mac, - ) - switches[poe_client_id].async_schedule_update_ha_state() + if switches[poe_client_id].enabled: + LOGGER.debug( + "Updating UniFi POE switch %s (%s)", + switches[poe_client_id].entity_id, + switches[poe_client_id].client.mac, + ) + switches[poe_client_id].async_schedule_update_ha_state() continue client = controller.api.clients[client_id] @@ -138,7 +139,6 @@ def update_items(controller, async_add_entities, switches, switches_off): switches[poe_client_id] = UniFiPOEClientSwitch(client, controller) new_switches.append(switches[poe_client_id]) - LOGGER.debug("New UniFi POE switch %s (%s)", client.hostname, client.mac) if new_switches: async_add_entities(new_switches) @@ -179,6 +179,7 @@ def __init__(self, client, controller): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" + LOGGER.debug("New UniFi POE switch %s (%s)", self.name, self.client.mac) state = await self.async_get_last_state() if state is None: @@ -252,6 +253,10 @@ def port(self): class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): """Representation of a blockable client.""" + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + LOGGER.debug("New UniFi Block switch %s (%s)", self.name, self.client.mac) + @property def unique_id(self): """Return a unique identifier for this switch.""" From 99328bd4c1704819ea3bd3bed19bb69646a4e333 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 Dec 2019 20:05:18 +0100 Subject: [PATCH 2377/3953] UniFi - honor detection time when UniFi wire bug happens (#29820) --- .../components/unifi/device_tracker.py | 18 ++++++++++++------ tests/components/unifi/test_device_tracker.py | 19 ++++++++++++++++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 159949f29853eb..6c3701e883d9a5 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -127,6 +127,7 @@ def __init__(self, client, controller): self.client = client self.controller = controller self.is_wired = self.client.mac not in controller.wireless_clients + self.wired_bug = None @property def entity_registry_enabled_default(self): @@ -169,13 +170,18 @@ def is_connected(self): If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. """ - if self.is_wired == self.client.is_wired and ( - ( - dt_util.utcnow() - - dt_util.utc_from_timestamp(float(self.client.last_seen)) + if self.is_wired != self.client.is_wired: + if not self.wired_bug: + self.wired_bug = dt_util.utcnow() + since_last_seen = dt_util.utcnow() - self.wired_bug + + else: + self.wired_bug = None + since_last_seen = dt_util.utcnow() - dt_util.utc_from_timestamp( + float(self.client.last_seen) ) - < self.controller.option_detection_time - ): + + if since_last_seen < self.controller.option_detection_time: return True return False diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 634247a5283d5a..42ba83a16124ec 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -2,6 +2,8 @@ from copy import copy from datetime import timedelta +from asynctest import patch + from homeassistant import config_entries from homeassistant.components import unifi import homeassistant.components.device_tracker as device_tracker @@ -17,8 +19,6 @@ from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration -DEFAULT_DETECTION_TIME = timedelta(seconds=300) - CLIENT_1 = { "essid": "ssid", "hostname": "client_1", @@ -202,7 +202,20 @@ async def test_wireless_client_go_wired_issue(hass): await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") - assert client_1.state == "not_home" + assert client_1.state == "home" + + with patch.object( + unifi.device_tracker.dt_util, + "utcnow", + return_value=(dt_util.utcnow() + timedelta(minutes=5)), + ): + controller.mock_client_responses.append([client_1_client]) + controller.mock_device_responses.append({}) + await controller.async_update() + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" client_1_client["is_wired"] = False client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) From 899f6cf1a34d3ba274460b2612948cf5a8df7828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 10 Dec 2019 22:41:29 +0200 Subject: [PATCH 2378/3953] Re-authorize Huawei LTE on login required error (#29597) * Re-authorize Huawei LTE on login required error Closes https://github.com/home-assistant/home-assistant/issues/29531 * Eliminate one indented "else" block * Convert get_data from inline to instance method * Use .get instead of [] for checking subscriptions for clarity --- .../components/huawei_lte/__init__.py | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 4a5614543e9d92..531529b17cd45e 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -157,40 +157,53 @@ def device_connections(self) -> Set[Tuple[str, str]]: """Get router connections for device registry.""" return {(dr.CONNECTION_NETWORK_MAC, self.mac)} if self.mac else set() + def _get_data(self, key: str, func: Callable[[None], Any]) -> None: + if not self.subscriptions.get(key): + return + _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) + try: + self.data[key] = func() + except ResponseErrorNotSupportedException: + _LOGGER.info( + "%s not supported by device, excluding from future updates", key + ) + self.subscriptions.pop(key) + except ResponseErrorLoginRequiredException: + if isinstance(self.connection, AuthorizedConnection): + _LOGGER.debug("Trying to authorize again...") + if self.connection.enforce_authorized_connection(): + _LOGGER.debug( + "...success, %s will be updated by a future periodic run", key, + ) + else: + _LOGGER.debug("...failed") + return + _LOGGER.info( + "%s requires authorization, excluding from future updates", key + ) + self.subscriptions.pop(key) + finally: + _LOGGER.debug("%s=%s", key, self.data.get(key)) + def update(self) -> None: """Update router data.""" - def get_data(key: str, func: Callable[[None], Any]) -> None: - if not self.subscriptions[key]: - return - _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) - try: - self.data[key] = func() - except ResponseErrorNotSupportedException: - _LOGGER.info( - "%s not supported by device, excluding from future updates", key - ) - self.subscriptions.pop(key) - except ResponseErrorLoginRequiredException: - _LOGGER.info( - "%s requires authorization, excluding from future updates", key - ) - self.subscriptions.pop(key) - finally: - _LOGGER.debug("%s=%s", key, self.data.get(key)) - - get_data(KEY_DEVICE_INFORMATION, self.client.device.information) + self._get_data(KEY_DEVICE_INFORMATION, self.client.device.information) if self.data.get(KEY_DEVICE_INFORMATION): # Full information includes everything in basic self.subscriptions.pop(KEY_DEVICE_BASIC_INFORMATION, None) - get_data(KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information) - get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) - get_data(KEY_DIALUP_MOBILE_DATASWITCH, self.client.dial_up.mobile_dataswitch) - get_data(KEY_MONITORING_STATUS, self.client.monitoring.status) - get_data( + self._get_data( + KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information + ) + self._get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) + self._get_data( + KEY_DIALUP_MOBILE_DATASWITCH, self.client.dial_up.mobile_dataswitch + ) + self._get_data(KEY_MONITORING_STATUS, self.client.monitoring.status) + self._get_data( KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics ) - get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) + self._get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) self.signal_update() From 66d2f5f61de6a9c2cfed63efe686b3eaeef74fe9 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Tue, 10 Dec 2019 12:54:50 -0800 Subject: [PATCH 2379/3953] Fix withings wrong sleep state entry (#29651) * Fixing issue where wrong sleep state entry was being used. closes #28370,#29397 * Fixing formatting. * Sorting imports to get black checks to pass. * Using lambda for getting latest sleep serie. --- homeassistant/components/withings/__init__.py | 4 +- homeassistant/components/withings/common.py | 6 +-- homeassistant/components/withings/sensor.py | 11 ++--- tests/components/withings/test_common.py | 5 +-- tests/components/withings/test_init.py | 40 ++++++++++++++----- 5 files changed, 43 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 482c4e96e5cec5..92c3f2ae155fc9 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -7,11 +7,11 @@ from withings_api import WithingsAuth from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from . import config_flow, const -from .common import _LOGGER, get_data_manager, NotAuthenticatedError +from .common import _LOGGER, NotAuthenticatedError, get_data_manager DOMAIN = const.DOMAIN diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 655776ae00438c..5ec15eeac54fd3 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -1,4 +1,5 @@ """Common code for Withings.""" +from asyncio import run_coroutine_threadsafe import datetime from functools import partial import logging @@ -6,15 +7,14 @@ import time from typing import Any, Dict -from asyncio import run_coroutine_threadsafe import requests from withings_api import ( AbstractWithingsApi, - SleepGetResponse, MeasureGetMeasResponse, + SleepGetResponse, SleepGetSummaryResponse, ) -from withings_api.common import UnauthorizedException, AuthFailedException +from withings_api.common import AuthFailedException, UnauthorizedException from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 17eae93ec0db92..9b5ceab64bf112 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -2,21 +2,21 @@ from typing import Callable, List, Union from withings_api.common import ( - MeasureType, GetSleepSummaryField, MeasureGetMeasResponse, + MeasureGroupAttribs, + MeasureType, SleepGetResponse, SleepGetSummaryResponse, - get_measure_value, - MeasureGroupAttribs, SleepState, + get_measure_value, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from homeassistant.helpers import config_entry_oauth2_flow from . import const from .common import _LOGGER, WithingsDataManager, get_data_manager @@ -382,7 +382,8 @@ async def async_update_sleep_state(self, data: SleepGetResponse) -> None: self._state = None return - serie = data.series[len(data.series) - 1] + sorted_series = sorted(data.series, key=lambda serie: serie.startdate) + serie = sorted_series[len(sorted_series) - 1] state = None if serie.state == SleepState.AWAKE: state = const.STATE_AWAKE diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index f6075c0f734cb4..9328526d6ef1ec 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,15 +1,14 @@ """Tests for the Withings component.""" from asynctest import MagicMock - import pytest from withings_api import WithingsApi -from withings_api.common import UnauthorizedException, TimeoutException +from withings_api.common import TimeoutException, UnauthorizedException -from homeassistant.exceptions import PlatformNotReady from homeassistant.components.withings.common import ( NotAuthenticatedError, WithingsDataManager, ) +from homeassistant.exceptions import PlatformNotReady @pytest.fixture(name="withings_api") diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index bd4940d9504bef..286b28b61ff4e4 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -10,26 +10,26 @@ import homeassistant.components.http as http from homeassistant.components.withings import ( + CONFIG_SCHEMA, async_setup, async_setup_entry, const, - CONFIG_SCHEMA, ) from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from .common import ( - assert_state_equals, - configure_integration, - setup_hass, WITHINGS_GET_DEVICE_RESPONSE, WITHINGS_GET_DEVICE_RESPONSE_EMPTY, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_MEASURES_RESPONSE_EMPTY, WITHINGS_SLEEP_RESPONSE, WITHINGS_SLEEP_RESPONSE_EMPTY, WITHINGS_SLEEP_SUMMARY_RESPONSE, WITHINGS_SLEEP_SUMMARY_RESPONSE_EMPTY, - WITHINGS_MEASURES_RESPONSE, - WITHINGS_MEASURES_RESPONSE_EMPTY, + assert_state_equals, + configure_integration, + setup_hass, ) @@ -308,8 +308,13 @@ async def test_full_setup(hass: HomeAssistant, aiohttp_client, aioclient_mock) - { "startdate": "2019-02-01 00:00:00", "enddate": "2019-02-01 01:00:00", + "state": SleepState.REM.real, + }, + { + "startdate": "2019-02-01 01:00:00", + "enddate": "2019-02-01 02:00:00", "state": SleepState.AWAKE.real, - } + }, ], }, }, @@ -329,11 +334,16 @@ async def test_full_setup(hass: HomeAssistant, aiohttp_client, aioclient_mock) - "body": { "model": SleepModel.TRACKER.real, "series": [ + { + "startdate": "2019-02-01 01:00:00", + "enddate": "2019-02-01 02:00:00", + "state": SleepState.LIGHT.real, + }, { "startdate": "2019-02-01 00:00:00", "enddate": "2019-02-01 01:00:00", - "state": SleepState.LIGHT.real, - } + "state": SleepState.REM.real, + }, ], }, }, @@ -356,8 +366,18 @@ async def test_full_setup(hass: HomeAssistant, aiohttp_client, aioclient_mock) - { "startdate": "2019-02-01 00:00:00", "enddate": "2019-02-01 01:00:00", + "state": SleepState.LIGHT.real, + }, + { + "startdate": "2019-02-01 02:00:00", + "enddate": "2019-02-01 03:00:00", "state": SleepState.REM.real, - } + }, + { + "startdate": "2019-02-01 01:00:00", + "enddate": "2019-02-01 02:00:00", + "state": SleepState.AWAKE.real, + }, ], }, }, From 3ed1738f769cbaf050a132f3f16ffb95e2154ab3 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 10 Dec 2019 17:24:33 -0500 Subject: [PATCH 2380/3953] Fix input_text initialization with empty config. (#29829) --- homeassistant/components/input_text/__init__.py | 10 ++++++---- tests/components/input_text/test_init.py | 9 +++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 77f007c5ba8fed..2049de7ab27239 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -23,7 +23,9 @@ CONF_INITIAL = "initial" CONF_MIN = "min" +CONF_MIN_VALUE = 0 CONF_MAX = "max" +CONF_MAX_VALUE = 100 MODE_TEXT = "text" MODE_PASSWORD = "password" @@ -59,8 +61,8 @@ def _cv_input_text(cfg): vol.All( { vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MIN, default=0): vol.Coerce(int), - vol.Optional(CONF_MAX, default=100): vol.Coerce(int), + vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), + vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), vol.Optional(CONF_INITIAL, ""): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, @@ -121,8 +123,8 @@ async def _async_process_config(config): if cfg is None: cfg = {} name = cfg.get(CONF_NAME) - minimum = cfg.get(CONF_MIN) - maximum = cfg.get(CONF_MAX) + minimum = cfg.get(CONF_MIN, CONF_MIN_VALUE) + maximum = cfg.get(CONF_MAX, CONF_MAX_VALUE) initial = cfg.get(CONF_INITIAL) icon = cfg.get(CONF_ICON) unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index 1bcf612c39b7f6..d37fe01cd29e52 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -100,8 +100,7 @@ async def test_mode(hass): assert "password" == state.attributes["mode"] -@asyncio.coroutine -def test_restore_state(hass): +async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -110,10 +109,8 @@ def test_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( - hass, - DOMAIN, - {DOMAIN: {"b1": {"min": 0, "max": 10}, "b2": {"min": 0, "max": 10}}}, + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {"b1": None, "b2": {"min": 0, "max": 10}}}, ) state = hass.states.get("input_text.b1") From a12cf7211da9eb9e47403a09f2147f6ee5e32f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 11 Dec 2019 00:25:06 +0200 Subject: [PATCH 2381/3953] Upgrade pydocstyle to 5.0.1 (#29830) * Upgrade pydocstyle to 5.0.1 http://www.pydocstyle.org/en/5.0.1/release_notes.html * Pydocstyle and other docstring fixes --- .pre-commit-config-all.yaml | 2 +- .pre-commit-config.yaml | 2 +- .../components/bmw_connected_drive/__init__.py | 2 +- .../bmw_connected_drive/binary_sensor.py | 2 +- .../components/bmw_connected_drive/sensor.py | 2 +- homeassistant/components/dlna_dmr/media_player.py | 2 +- homeassistant/components/emulated_hue/__init__.py | 2 +- homeassistant/components/homeworks/__init__.py | 2 +- homeassistant/components/keba/__init__.py | 2 +- homeassistant/components/nissan_leaf/sensor.py | 2 +- homeassistant/components/starline/account.py | 2 +- homeassistant/components/starline/binary_sensor.py | 2 +- homeassistant/components/starline/entity.py | 2 +- homeassistant/components/starline/sensor.py | 2 +- homeassistant/components/tplink/common.py | 2 +- homeassistant/components/upnp/device.py | 4 ++-- homeassistant/components/upnp/sensor.py | 2 +- homeassistant/components/withings/common.py | 4 ++-- homeassistant/components/withings/sensor.py | 4 ++-- homeassistant/components/wunderground/sensor.py | 14 +++++++------- requirements_test_pre_commit.txt | 2 +- tests/components/mqtt/test_light_json.py | 2 +- tests/components/upnp/test_init.py | 2 +- tests/components/vera/common.py | 2 +- 24 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml index f7b29cddc4fbe1..cb01bff85cb084 100644 --- a/.pre-commit-config-all.yaml +++ b/.pre-commit-config-all.yaml @@ -24,7 +24,7 @@ repos: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - - pydocstyle==4.0.1 + - pydocstyle==5.0.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.6.2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 216bac95f2923b..e6f37ae6355929 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - - pydocstyle==4.0.1 + - pydocstyle==5.0.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.6.2 diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 455d821e6692bf..6e7723b16ec554 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -119,7 +119,7 @@ class BMWConnectedDriveAccount: def __init__( self, username: str, password: str, region_str: str, name: str, read_only ) -> None: - """Constructor.""" + """Initialize account.""" region = get_region_from_name(region_str) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 8163ae4eae30e5..d0cb4289d37db0 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -59,7 +59,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): def __init__( self, account, vehicle, attribute: str, sensor_name, device_class, icon ): - """Constructor.""" + """Initialize sensor.""" self._account = account self._vehicle = vehicle self._attribute = attribute diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index f919bba6b95fae..3c40900bed8df6 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -69,7 +69,7 @@ class BMWConnectedDriveSensor(Entity): """Representation of a BMW vehicle sensor.""" def __init__(self, account, vehicle, attribute: str, attribute_info): - """Constructor.""" + """Initialize BMW vehicle sensor.""" self._vehicle = vehicle self._account = account self._attribute = attribute diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index a9ea9b21d596c1..a6ebf95424a9a1 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -198,7 +198,7 @@ class DlnaDmrDevice(MediaPlayerDevice): """Representation of a DLNA DMR device.""" def __init__(self, dmr_device, name=None): - """Initializer.""" + """Initialize DLNA DMR device.""" self._device = dmr_device self._name = name diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index c40a22df4510c4..0a358c6e894eba 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -312,7 +312,7 @@ def is_entity_exposed(self, entity): def _load_json(filename): - """Wrapper, because we actually want to handle invalid json.""" + """Load JSON, handling invalid syntax.""" try: return load_json(filename) except HomeAssistantError: diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index 55357acdad4467..c6296d8f4c69eb 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -98,7 +98,7 @@ class HomeworksDevice: """Base class of a Homeworks device.""" def __init__(self, controller, addr, name): - """Controller, address, and name of the device.""" + """Initialize Homeworks device.""" self._addr = addr self._name = name self._controller = controller diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py index 5a9a49a005a8dc..830ebe7ffabdc2 100644 --- a/homeassistant/components/keba/__init__.py +++ b/homeassistant/components/keba/__init__.py @@ -106,7 +106,7 @@ class KebaHandler(KebaKeContact): """Representation of a KEBA charging station connection.""" def __init__(self, hass, host, rfid, refresh_interval): - """Constructor.""" + """Initialize charging station connection.""" super().__init__(host, self.hass_callback) self._update_listeners = [] diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index cd82735d207765..6e5d119c7a3c9d 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -69,7 +69,7 @@ class LeafRangeSensor(LeafEntity): """Nissan Leaf Range Sensor.""" def __init__(self, car, ac_on): - """Set-up range sensor. Store if AC on.""" + """Set up range sensor. Store if AC on.""" self._ac_on = ac_on super().__init__(car) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index aee88c0bd3f54f..8d0214d1b5c4ff 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -23,7 +23,7 @@ class StarlineAccount: """StarLine Account class.""" def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): - """Constructor.""" + """Initialize StarLine account.""" self._hass: HomeAssistant = hass self._config_entry: ConfigEntry = config_entry self._update_interval: int = DEFAULT_SCAN_INTERVAL diff --git a/homeassistant/components/starline/binary_sensor.py b/homeassistant/components/starline/binary_sensor.py index 21074069135249..f2288e9363bf69 100644 --- a/homeassistant/components/starline/binary_sensor.py +++ b/homeassistant/components/starline/binary_sensor.py @@ -44,7 +44,7 @@ def __init__( name: str, device_class: str, ): - """Constructor.""" + """Initialize sensor.""" super().__init__(account, device, key, name) self._device_class = device_class diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py index 31d1c79b9f0664..5db4d369f5e742 100644 --- a/homeassistant/components/starline/entity.py +++ b/homeassistant/components/starline/entity.py @@ -12,7 +12,7 @@ class StarlineEntity(Entity): def __init__( self, account: StarlineAccount, device: StarlineDevice, key: str, name: str ): - """Constructor.""" + """Initialize StarLine entity.""" self._account = account self._device = device self._key = key diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 874fc5a9a7eda4..0c6cd8de683285 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -42,7 +42,7 @@ def __init__( unit: str, icon: str, ): - """Constructor.""" + """Initialize StarLine sensor.""" super().__init__(account, device, key, name) self._device_class = device_class self._unit = unit diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 548edc6822c706..0e06babbd525a2 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -32,7 +32,7 @@ class SmartDevices: def __init__( self, lights: List[SmartDevice] = None, switches: List[SmartDevice] = None ): - """Constructor.""" + """Initialize device holder.""" self._lights = lights or [] self._switches = switches or [] diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index fffee57b411870..1f34e63bcdf268 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -17,13 +17,13 @@ class Device: """Hass representation of an UPnP/IGD.""" def __init__(self, igd_device): - """Initializer.""" + """Initialize UPnP/IGD device.""" self._igd_device = igd_device self._mapped_ports = [] @classmethod async def async_discover(cls, hass: HomeAssistantType): - """Discovery UPNP/IGD devices.""" + """Discover UPnP/IGD devices.""" _LOGGER.debug("Discovering UPnP/IGD devices") local_ip = None if DOMAIN in hass.data and "config" in hass.data[DOMAIN]: diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 7b5071aabd3e05..06e4a86401fcbd 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -154,7 +154,7 @@ class PerSecondUPnPIGDSensor(UpnpSensor): """Abstract representation of a X Sent/Received per second sensor.""" def __init__(self, device, direction): - """Initializer.""" + """Initialize sensor.""" super().__init__(device) self._direction = direction diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 5ec15eeac54fd3..4c3772d77e707d 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -51,7 +51,7 @@ class ThrottleData: """Throttle data.""" def __init__(self, interval: int, data: Any): - """Constructor.""" + """Initialize throttle data.""" self._time = int(time.time()) self._interval = interval self._data = data @@ -126,7 +126,7 @@ class WithingsDataManager: service_available = None def __init__(self, hass: HomeAssistant, profile: str, api: ConfigEntryWithingsApi): - """Constructor.""" + """Initialize data manager.""" self._hass = hass self._api = api self._profile = profile diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 9b5ceab64bf112..aae706dec6151b 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -55,7 +55,7 @@ def __init__( unit_of_measurement: str, icon: str, ) -> None: - """Constructor.""" + """Initialize attribute.""" self.measurement = measurement self.measure_type = measure_type self.friendly_name = friendly_name @@ -73,7 +73,7 @@ class WithingsSleepStateAttribute(WithingsAttribute): def __init__( self, measurement: str, friendly_name: str, unit_of_measurement: str, icon: str ) -> None: - """Constructor.""" + """Initialize sleep state attribute.""" super().__init__(measurement, None, friendly_name, unit_of_measurement, icon) diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 84c870af54d55e..cc71e71a1d0c05 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -66,7 +66,7 @@ def __init__( device_state_attributes=None, device_class=None, ): - """Constructor. + """Initialize sensor configuration. :param friendly_name: Friendly name :param feature: WU feature. See: @@ -98,7 +98,7 @@ def __init__( unit_of_measurement: Optional[str] = None, device_class=None, ): - """Constructor. + """Initialize current conditions sensor configuration. :param friendly_name: Friendly name of sensor :field: Field name in the "current_observation" dictionary. @@ -127,7 +127,7 @@ class WUDailyTextForecastSensorConfig(WUSensorConfig): def __init__( self, period: int, field: str, unit_of_measurement: Optional[str] = None ): - """Constructor. + """Initialize daily text forecast sensor configuration. :param period: forecast period number :param field: field name to use as value @@ -164,7 +164,7 @@ def __init__( icon=None, device_class=None, ): - """Constructor. + """Initialize daily simple forecast sensor configuration. :param friendly_name: friendly_name of the sensor :param period: forecast period number @@ -207,7 +207,7 @@ class WUHourlyForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for hourly text forecasts.""" def __init__(self, period: int, field: int): - """Constructor. + """Initialize hourly forecast sensor configuration. :param period: forecast period number :param field: field name to use as value @@ -274,7 +274,7 @@ def __init__( icon: str, device_class=None, ): - """Constructor. + """Initialize almanac sensor configuration. :param friendly_name: Friendly name :param field: value name returned in 'almanac' dict as returned by the WU API @@ -297,7 +297,7 @@ class WUAlertsSensorConfig(WUSensorConfig): """Helper for defining field configuration for alerts.""" def __init__(self, friendly_name: Union[str, Callable]): - """Constructor. + """Initialiize alerts sensor configuration. :param friendly_name: Friendly name """ diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 3f4d05a4908111..14b866c70349af 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -4,4 +4,4 @@ bandit==1.6.2 black==19.10b0 flake8-docstrings==1.5.0 flake8==3.7.9 -pydocstyle==4.0.1 +pydocstyle==5.0.1 diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index f424b36ded6a37..52adeb61514c3c 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -117,7 +117,7 @@ class JsonValidator(object): """Helper to compare JSON.""" def __init__(self, jsondata): - """Constructor.""" + """Initialize JSON validator.""" self.jsondata = jsondata def __eq__(self, other): diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 1daa60bcb369eb..86ed017ae8d192 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -15,7 +15,7 @@ class MockDevice(Device): """Mock device for Device.""" def __init__(self, udn): - """Initializer.""" + """Initialize mock device.""" device = MagicMock() device.manufacturer = "mock-manuf" device.name = "mock-name" diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index d78a536e95c645..bd87e8bc9f2f0f 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -16,7 +16,7 @@ class ComponentFactory: """Factory class.""" def __init__(self, init_controller_mock): - """Constructor.""" + """Initialize component factory.""" self.init_controller_mock = init_controller_mock async def configure_component( From c2357d843b6bcc96bb613e0b31e4e3c84fe2d04e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 11 Dec 2019 00:32:21 +0000 Subject: [PATCH 2382/3953] [ci skip] Translation update --- .../alarm_control_panel/.translations/fr.json | 13 +++++-- .../components/deconz/.translations/fr.json | 11 +++++- .../components/elgato/.translations/ca.json | 27 +++++++++++++ .../components/elgato/.translations/fr.json | 27 +++++++++++++ .../components/elgato/.translations/lb.json | 27 +++++++++++++ .../components/icloud/.translations/ca.json | 38 +++++++++++++++++++ .../components/icloud/.translations/fr.json | 38 +++++++++++++++++++ .../components/icloud/.translations/lb.json | 38 +++++++++++++++++++ .../components/icloud/.translations/no.json | 38 +++++++++++++++++++ .../components/icloud/.translations/ru.json | 38 +++++++++++++++++++ .../components/linky/.translations/fr.json | 2 +- .../components/linky/.translations/pt.json | 2 +- .../components/starline/.translations/fr.json | 3 ++ 13 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/elgato/.translations/ca.json create mode 100644 homeassistant/components/elgato/.translations/fr.json create mode 100644 homeassistant/components/elgato/.translations/lb.json create mode 100644 homeassistant/components/icloud/.translations/ca.json create mode 100644 homeassistant/components/icloud/.translations/fr.json create mode 100644 homeassistant/components/icloud/.translations/lb.json create mode 100644 homeassistant/components/icloud/.translations/no.json create mode 100644 homeassistant/components/icloud/.translations/ru.json diff --git a/homeassistant/components/alarm_control_panel/.translations/fr.json b/homeassistant/components/alarm_control_panel/.translations/fr.json index c3ba6db0c62661..fbdc6a5605f92e 100644 --- a/homeassistant/components/alarm_control_panel/.translations/fr.json +++ b/homeassistant/components/alarm_control_panel/.translations/fr.json @@ -1,11 +1,18 @@ { "device_automation": { "action_type": { - "arm_away": "Armer {entity_name} mode sortie", - "arm_home": "Armer {entity_name} mode \u00e0 la maison", - "arm_night": "Armer {entity_name} mode nuit", + "arm_away": "Armer {entity_name} en mode \"sortie\"", + "arm_home": "Armer {entity_name} en mode \"maison\"", + "arm_night": "Armer {entity_name} en mode \"nuit\"", "disarm": "D\u00e9sarmer {entity_name}", "trigger": "D\u00e9clencheur {entity_name}" + }, + "trigger_type": { + "armed_away": "Armer {entity_name} en mode \"sortie\"", + "armed_home": "Armer {entity_name} en mode \"maison\"", + "armed_night": "Armer {entity_name} en mode \"nuit\"", + "disarmed": "{entity_name} d\u00e9sarm\u00e9", + "triggered": "{entity_name} d\u00e9clench\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 4d49bd18d1e1c5..1a4232e0817f2d 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -76,7 +76,16 @@ "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", - "remote_gyro_activated": "Appareil secou\u00e9" + "remote_double_tap": "Appareil \"{subtype}\" tapot\u00e9 deux fois", + "remote_falling": "Appareil en chute libre", + "remote_gyro_activated": "Appareil secou\u00e9", + "remote_moved": "Appareil d\u00e9plac\u00e9 avec \"{subtype}\" vers le haut", + "remote_rotate_from_side_1": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 1\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_2": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 2\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_3": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 3\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_4": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 4\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_5": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 5\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_6": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 6\" \u00e0 \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/elgato/.translations/ca.json b/homeassistant/components/elgato/.translations/ca.json new file mode 100644 index 00000000000000..b717a5abadee28 --- /dev/null +++ b/homeassistant/components/elgato/.translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest dispositiu Elgato Key Light ja est\u00e0 configurat.", + "connection_error": "No s'ha pogut connectar amb el dispositiu Elgato Key Light." + }, + "error": { + "connection_error": "No s'ha pogut connectar amb el dispositiu Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 o adre\u00e7a IP", + "port": "N\u00famero de port" + }, + "description": "Configura l'Elgato Key Light per integrar-lo amb Home Assistant.", + "title": "Enlla\u00e7a Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vols afegir l'Elgato Key Light amb n\u00famero de s\u00e8rie `{serial_number}` a Home Assistant?", + "title": "S'ha descobert un dispositiu Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/fr.json b/homeassistant/components/elgato/.translations/fr.json new file mode 100644 index 00000000000000..e8465a56728d4c --- /dev/null +++ b/homeassistant/components/elgato/.translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Cet appareil Elgato Key Light est d\u00e9j\u00e0 configur\u00e9.", + "connection_error": "Impossible de se connecter au p\u00e9riph\u00e9rique Elgato Key Light." + }, + "error": { + "connection_error": "Impossible de se connecter au p\u00e9riph\u00e9rique Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "H\u00f4te ou adresse IP", + "port": "Port" + }, + "description": "Configurez votre Elgato Key Light pour l'int\u00e9grer \u00e0 Home Assistant.", + "title": "Associez votre Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter l'Elgato Key Light avec le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant?", + "title": "Appareil Elgato Key Light d\u00e9couvert" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/lb.json b/homeassistant/components/elgato/.translations/lb.json new file mode 100644 index 00000000000000..d53eea87c4cd2d --- /dev/null +++ b/homeassistant/components/elgato/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebsen Elgato Key Light Apparat ass scho konfigur\u00e9iert.", + "connection_error": "Feeler beim verbannen mam Elgato key Light Apparat." + }, + "error": { + "connection_error": "Feeler beim verbannen mam Elgato key Light Apparat." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Numm oder IP Adresse", + "port": "Port Nummer" + }, + "description": "\u00c4ren Elgator Key Light als Integratioun mam Home Assistant ariichten.", + "title": "\u00c4ren Elgato Key Light verbannen" + }, + "zeroconf_confirm": { + "description": "W\u00ebllt dir den Elgato Key Light mat der Seriennummer `{serial_number}` am 'Home Assistant dob\u00e4isetzen?", + "title": "Entdeckten Elgato Key Light Apparat" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/ca.json b/homeassistant/components/icloud/.translations/ca.json new file mode 100644 index 00000000000000..30e6c50b81be81 --- /dev/null +++ b/homeassistant/components/icloud/.translations/ca.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "El compte ja ha estat configurat" + }, + "error": { + "login": "Error d\u2019inici de sessi\u00f3: comprova el correu electr\u00f2nic i la contrasenya", + "send_verification_code": "No s'ha pogut enviar el codi de verificaci\u00f3", + "username_exists": "El compte ja ha estat configurat", + "validate_verification_code": "No s'ha pogut verificar el codi de verificaci\u00f3, tria un dispositiu de confian\u00e7a i torna a iniciar el proc\u00e9s" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositiu de confian\u00e7a" + }, + "description": "Selecciona el teu dispositiu de confian\u00e7a", + "title": "Dispositiu de confian\u00e7a iCloud" + }, + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "description": "Introdueix les teves credencials", + "title": "credencials d'iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Codi de verificaci\u00f3" + }, + "description": "Introdueix el codi de verificaci\u00f3 que rebis d'iCloud", + "title": "Codi de verificaci\u00f3 iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/fr.json b/homeassistant/components/icloud/.translations/fr.json new file mode 100644 index 00000000000000..81996d908a62d9 --- /dev/null +++ b/homeassistant/components/icloud/.translations/fr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "login": "Erreur de connexion: veuillez v\u00e9rifier votre e-mail et votre mot de passe", + "send_verification_code": "\u00c9chec de l'envoi du code de v\u00e9rification", + "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9", + "validate_verification_code": "Impossible de v\u00e9rifier votre code de v\u00e9rification, choisissez un appareil de confiance et recommencez la v\u00e9rification" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Appareil de confiance" + }, + "description": "S\u00e9lectionnez votre appareil de confiance", + "title": "Appareil de confiance iCloud" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Email" + }, + "description": "Entrez vos identifiants", + "title": "Identifiants iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Code de v\u00e9rification" + }, + "description": "Veuillez entrer le code de v\u00e9rification que vous venez de recevoir d'iCloud", + "title": "Code de v\u00e9rification iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/lb.json b/homeassistant/components/icloud/.translations/lb.json new file mode 100644 index 00000000000000..eaeb300f7a873e --- /dev/null +++ b/homeassistant/components/icloud/.translations/lb.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Kont ass scho konfigur\u00e9iert" + }, + "error": { + "login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert", + "send_verification_code": "Feeler beim sch\u00e9cken vum Verifikatiouns Code", + "username_exists": "Kont ass scho konfigur\u00e9iert", + "validate_verification_code": "Feeler beim iwwerpr\u00e9iwe vum Verifikatiouns Code, wielt ee vertrauten Apparat aus a start d'Iwwerpr\u00e9iwung nei" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Vertrauten Apparat" + }, + "description": "Wielt \u00e4ren vertrauten Apparat aus", + "title": "iCloud vertrauten Apparat" + }, + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + }, + "description": "F\u00ebllt \u00e4r Umeldungs Informatiounen aus", + "title": "iCloud Umeldungs Informatiounen" + }, + "verification_code": { + "data": { + "verification_code": "Verifikatiouns Code" + }, + "description": "Gitt de Verifikatiouns Code an deen dir elo grad vun iCloud kritt hutt", + "title": "iCloud Verifikatiouns Code" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/no.json b/homeassistant/components/icloud/.translations/no.json new file mode 100644 index 00000000000000..a582b9163102c7 --- /dev/null +++ b/homeassistant/components/icloud/.translations/no.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Kontoen er allerede konfigurert" + }, + "error": { + "login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt", + "send_verification_code": "Kunne ikke sende bekreftelseskode", + "username_exists": "Kontoen er allerede konfigurert", + "validate_verification_code": "Kunne ikke bekrefte bekreftelseskoden din, velg en tillitsenhet og start bekreftelsen p\u00e5 nytt" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "P\u00e5litelig enhet" + }, + "description": "Velg den p\u00e5litelige enheten din", + "title": "iCloud p\u00e5litelig enhet" + }, + "user": { + "data": { + "password": "Passord", + "username": "E-post" + }, + "description": "Angi legitimasjonsbeskrivelsen", + "title": "iCloud-legitimasjon" + }, + "verification_code": { + "data": { + "verification_code": "iCloud-bekreftelseskode" + }, + "description": "Vennligst skriv inn bekreftelseskoden du nettopp har f\u00e5tt fra iCloud", + "title": "iCloud-bekreftelseskode" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/ru.json b/homeassistant/components/icloud/.translations/ru.json new file mode 100644 index 00000000000000..000edd71e00a82 --- /dev/null +++ b/homeassistant/components/icloud/.translations/ru.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "send_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f.", + "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", + "validate_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043d\u0430\u0447\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u043d\u043e\u0432\u0430." + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "\u0414\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "title": "\u0414\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e iCloud" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "title": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 iCloud" + }, + "verification_code": { + "data": { + "verification_code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043e\u0442 iCloud", + "title": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/fr.json b/homeassistant/components/linky/.translations/fr.json index af12c2b654d8ff..6ff99c41a161aa 100644 --- a/homeassistant/components/linky/.translations/fr.json +++ b/homeassistant/components/linky/.translations/fr.json @@ -8,7 +8,7 @@ "enedis": "Erreur d'Enedis.fr: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", "unknown": "Erreur inconnue: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9", - "wrong_login": "Impossible de vous identifier: merci de v\u00e9rifier vos identifiants" + "wrong_login": "Erreur de connexion: veuillez v\u00e9rifier votre e-mail et votre mot de passe" }, "step": { "user": { diff --git a/homeassistant/components/linky/.translations/pt.json b/homeassistant/components/linky/.translations/pt.json index daf1ce751816e4..67e742c5813b70 100644 --- a/homeassistant/components/linky/.translations/pt.json +++ b/homeassistant/components/linky/.translations/pt.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Palavra-passe", - "username": "" + "username": "O email" } } } diff --git a/homeassistant/components/starline/.translations/fr.json b/homeassistant/components/starline/.translations/fr.json index 67a7dae4afcd02..d15f5c37edf337 100644 --- a/homeassistant/components/starline/.translations/fr.json +++ b/homeassistant/components/starline/.translations/fr.json @@ -1,6 +1,7 @@ { "config": { "error": { + "error_auth_app": "ID applicatif ou code secret incorrect", "error_auth_mfa": "code incorrect", "error_auth_user": "identifiant ou mot de passe incorrect" }, @@ -10,6 +11,7 @@ "app_id": "ID de l'application", "app_secret": "Secret" }, + "description": "ID applicatif et code secret du compte d\u00e9veloppeur StarLine", "title": "Informations d'identification de l'application" }, "auth_captcha": { @@ -23,6 +25,7 @@ "data": { "mfa_code": "Code SMS" }, + "description": "Entrez le code envoy\u00e9 au t\u00e9l\u00e9phone {phone_number}", "title": "Autorisation \u00e0 deux facteurs" }, "auth_user": { From 004af97699f892093c1c25f1bba0a6359773f61b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Wed, 11 Dec 2019 12:12:06 +0100 Subject: [PATCH 2383/3953] Sort imports for requirements.py and its test using isort (#29836) Unblocks https://github.com/home-assistant/home-assistant/pull/29739 --- homeassistant/requirements.py | 2 +- tests/test_requirements.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 670f3af7dcc9b3..7b2d3fe9bc3166 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -3,7 +3,7 @@ import logging import os from pathlib import Path -from typing import Any, Dict, List, Optional, Set, Iterable +from typing import Any, Dict, Iterable, List, Optional, Set from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 87bbf38e4651fa..e95db4e533dd8c 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -2,9 +2,10 @@ import os from pathlib import Path from unittest.mock import call, patch + import pytest -from homeassistant import setup, loader +from homeassistant import loader, setup from homeassistant.requirements import ( CONSTRAINT_FILE, PROGRESS_FILE, From 01ef44fd681b4c2bd37dd51bc794289f3a0a4dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiit=20R=C3=A4tsep?= Date: Wed, 11 Dec 2019 14:27:28 +0200 Subject: [PATCH 2384/3953] Fix Soma integration connection issue (#27692) * Added a check for Connect actually returning something before telling the user the setup succeeded * Added handling for KeyError in case API returns empty response, formatting * Trying to please the linter --- .../components/soma/.translations/en.json | 4 +++- homeassistant/components/soma/config_flow.py | 18 ++++++++++---- homeassistant/components/soma/strings.json | 4 +++- tests/components/soma/test_config_flow.py | 24 +++++++++++++++++-- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json index 42e09a8762c582..ae2f99001620ee 100644 --- a/homeassistant/components/soma/.translations/en.json +++ b/homeassistant/components/soma/.translations/en.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "You can only configure one Soma account.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Soma component is not configured. Please follow the documentation." + "missing_configuration": "The Soma component is not configured. Please follow the documentation.", + "result_error": "SOMA Connect responded with error status.", + "connection_error": "Failed to connect to SOMA Connect." }, "create_entry": { "default": "Successfully authenticated with Soma." diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py index f46066e23cacef..afb5d05b77e033 100644 --- a/homeassistant/components/soma/config_flow.py +++ b/homeassistant/components/soma/config_flow.py @@ -40,14 +40,22 @@ async def async_step_creation(self, user_input=None): """Finish config flow.""" api = SomaApi(user_input["host"], user_input["port"]) try: - await self.hass.async_add_executor_job(api.list_devices) + result = await self.hass.async_add_executor_job(api.list_devices) _LOGGER.info("Successfully set up Soma Connect") - return self.async_create_entry( - title="Soma Connect", - data={"host": user_input["host"], "port": user_input["port"]}, + if result["result"] == "success": + return self.async_create_entry( + title="Soma Connect", + data={"host": user_input["host"], "port": user_input["port"]}, + ) + _LOGGER.error( + "Connection to SOMA Connect failed (result:%s)", result["result"] ) + return self.async_abort(reason="result_error") except RequestException: - _LOGGER.error("Connection to SOMA Connect failed") + _LOGGER.error("Connection to SOMA Connect failed with RequestException") + return self.async_abort(reason="connection_error") + except KeyError: + _LOGGER.error("Connection to SOMA Connect failed with KeyError") return self.async_abort(reason="connection_error") async def async_step_import(self, user_input=None): diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json index aa2f92f0be603c..67f1f6b7d460fe 100644 --- a/homeassistant/components/soma/strings.json +++ b/homeassistant/components/soma/strings.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "You can only configure one Soma account.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Soma component is not configured. Please follow the documentation." + "missing_configuration": "The Soma component is not configured. Please follow the documentation.", + "result_error": "SOMA Connect responded with error status.", + "connection_error": "Failed to connect to SOMA Connect." }, "create_entry": { "default": "Successfully authenticated with Soma." diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 15f90516c170a9..1d00f83a608492 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -35,11 +35,31 @@ async def test_import_create(hass): """Test configuration from YAML.""" flow = config_flow.SomaFlowHandler() flow.hass = hass - with patch.object(SomaApi, "list_devices", return_value={}): + with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY +async def test_error_status(hass): + """Test Connect successfully returning error status.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + with patch.object(SomaApi, "list_devices", return_value={"result": "error"}): + result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "result_error" + + +async def test_key_error(hass): + """Test Connect returning empty string.""" + flow = config_flow.SomaFlowHandler() + flow.hass = hass + with patch.object(SomaApi, "list_devices", return_value={}): + result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_error" + + async def test_exception(hass): """Test if RequestException fires when no connection can be made.""" flow = config_flow.SomaFlowHandler() @@ -55,6 +75,6 @@ async def test_full_flow(hass): hass.data[DOMAIN] = {} flow = config_flow.SomaFlowHandler() flow.hass = hass - with patch.object(SomaApi, "list_devices", return_value={}): + with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_user({"host": MOCK_HOST, "port": MOCK_PORT}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY From 0c796fc3c3cb79d2655283f1808e1f2938c78368 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 11 Dec 2019 13:28:50 +0100 Subject: [PATCH 2385/3953] Remove uvloop event policy (#29835) * Remove uvloop event policy * Clean tests * Fix lint * Cleanup statment --- homeassistant/__main__.py | 12 +----------- tests/conftest.py | 6 ------ 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 2cecd1217f901c..bf3042c3f88c5a 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -15,12 +15,10 @@ def set_loop() -> None: - """Attempt to use uvloop.""" + """Attempt to use different loop.""" import asyncio from asyncio.events import BaseDefaultEventLoopPolicy - policy = None - if sys.platform == "win32": if hasattr(asyncio, "WindowsProactorEventLoopPolicy"): # pylint: disable=no-member @@ -33,15 +31,7 @@ class ProactorPolicy(BaseDefaultEventLoopPolicy): _loop_factory = asyncio.ProactorEventLoop policy = ProactorPolicy() - else: - try: - import uvloop - except ImportError: - pass - else: - policy = uvloop.EventLoopPolicy() - if policy is not None: asyncio.set_event_loop_policy(policy) diff --git a/tests/conftest.py b/tests/conftest.py index 41b4757c92ed5c..262acda6314f9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,6 @@ """Set up some common test helper things.""" -import asyncio import functools import logging -import os from unittest.mock import patch import pytest @@ -25,10 +23,6 @@ ) from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:skip -if os.environ.get("UVLOOP") == "1": - import uvloop - - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) logging.basicConfig(level=logging.DEBUG) logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) From 7b8dff2aa92a13cb530cd6a02a85b6147039ef25 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 11 Dec 2019 15:43:54 +0100 Subject: [PATCH 2386/3953] Add user-agent to fix dwd_weather_warnings setup error (#29596) * Added dummy user-agent to http request to fix setup error * Replace dummy user-agent with the user-agent of the global home assistant session * Adjust comment and rename user-agent constant --- homeassistant/components/dwd_weather_warnings/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 01a06209d99ffc..46b9ac560e5d6e 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -22,6 +22,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE as HA_USER_AGENT from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -184,7 +185,10 @@ def __init__(self, region_name): "jsonp=loadWarnings", ) - self._rest = RestData("GET", resource, None, None, None, True) + # a User-Agent is necessary for this rest api endpoint (#29496) + headers = {"User-Agent": HA_USER_AGENT} + + self._rest = RestData("GET", resource, None, headers, None, True) self.region_name = region_name self.region_id = None self.region_state = None From 856dd6368085e6caf1ecce64a7642f3ad9af11ff Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 11 Dec 2019 15:45:21 +0100 Subject: [PATCH 2387/3953] Add more logging to help future debug situations (#29800) --- .../components/unifi/device_tracker.py | 17 ++++++++++++----- homeassistant/components/unifi/switch.py | 11 +++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 6c3701e883d9a5..6dfa6a6ecad3fe 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,5 +1,6 @@ """Track devices using UniFi controllers.""" import logging +from pprint import pformat from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity @@ -156,14 +157,17 @@ async def async_update(self): Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired. """ - LOGGER.debug( - "Updating UniFi tracked client %s (%s)", self.entity_id, self.client.mac - ) await self.controller.request_update() if self.is_wired and self.client.mac in self.controller.wireless_clients: self.is_wired = False + LOGGER.debug( + "Updating UniFi tracked client %s\n%s", + self.entity_id, + pformat(self.client.raw), + ) + @property def is_connected(self): """Return true if the client is connected to the network. @@ -246,10 +250,13 @@ async def async_added_to_hass(self): async def async_update(self): """Synchronize state with controller.""" + await self.controller.request_update() + LOGGER.debug( - "Updating UniFi tracked device %s (%s)", self.entity_id, self.device.mac + "Updating UniFi tracked device %s\n%s", + self.entity_id, + pformat(self.device.raw), ) - await self.controller.request_update() @property def is_connected(self): diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index e4192d0b6c77eb..5b64f573ccd9f4 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,5 +1,6 @@ """Support for devices connected to UniFi POE.""" import logging +from pprint import pformat from homeassistant.components.switch import SwitchDevice from homeassistant.components.unifi.config_flow import get_controller_from_config_entry @@ -194,6 +195,16 @@ async def async_added_to_hass(self): if not self.client.sw_port: self.client.raw["sw_port"] = state.attributes["port"] + async def async_update(self): + """Log client information after update.""" + await super().async_update() + + LOGGER.debug( + "Updating UniFi POE controlled client %s\n%s", + self.entity_id, + pformat(self.client.raw), + ) + @property def unique_id(self): """Return a unique identifier for this switch.""" From 0cb9d0746aed9d80756e5c68b8e82c424ae69469 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 11 Dec 2019 15:43:54 +0100 Subject: [PATCH 2388/3953] Add user-agent to fix dwd_weather_warnings setup error (#29596) * Added dummy user-agent to http request to fix setup error * Replace dummy user-agent with the user-agent of the global home assistant session * Adjust comment and rename user-agent constant --- homeassistant/components/dwd_weather_warnings/sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 4d7ad04e38257d..2484ba70074f98 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -19,6 +19,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE as HA_USER_AGENT from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_MONITORED_CONDITIONS @@ -184,7 +185,10 @@ def __init__(self, region_name): "jsonp=loadWarnings", ) - self._rest = RestData("GET", resource, None, None, None, True) + # a User-Agent is necessary for this rest api endpoint (#29496) + headers = {"User-Agent": HA_USER_AGENT} + + self._rest = RestData("GET", resource, None, headers, None, True) self.region_name = region_name self.region_id = None self.region_state = None From 4c9c22bc3af4207df1cb49ff9862e950f23fc07a Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 11 Dec 2019 15:45:21 +0100 Subject: [PATCH 2389/3953] Add more logging to help future debug situations (#29800) --- .../components/unifi/device_tracker.py | 17 ++++++++++++----- homeassistant/components/unifi/switch.py | 11 +++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 46ac3468835e80..200258306c1ab0 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,5 +1,6 @@ """Track devices using UniFi controllers.""" import logging +from pprint import pformat from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN @@ -156,14 +157,17 @@ async def async_update(self): Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired. """ - LOGGER.debug( - "Updating UniFi tracked client %s (%s)", self.entity_id, self.client.mac - ) await self.controller.request_update() if self.is_wired and self.client.mac in self.controller.wireless_clients: self.is_wired = False + LOGGER.debug( + "Updating UniFi tracked client %s\n%s", + self.entity_id, + pformat(self.client.raw), + ) + @property def is_connected(self): """Return true if the client is connected to the network. @@ -241,10 +245,13 @@ async def async_added_to_hass(self): async def async_update(self): """Synchronize state with controller.""" + await self.controller.request_update() + LOGGER.debug( - "Updating UniFi tracked device %s (%s)", self.entity_id, self.device.mac + "Updating UniFi tracked device %s\n%s", + self.entity_id, + pformat(self.device.raw), ) - await self.controller.request_update() @property def is_connected(self): diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 82aa6f0384de00..f08c3e65630fe0 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,5 +1,6 @@ """Support for devices connected to UniFi POE.""" import logging +from pprint import pformat from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.switch import SwitchDevice @@ -193,6 +194,16 @@ async def async_added_to_hass(self): if not self.client.sw_port: self.client.raw["sw_port"] = state.attributes["port"] + async def async_update(self): + """Log client information after update.""" + await super().async_update() + + LOGGER.debug( + "Updating UniFi POE controlled client %s\n%s", + self.entity_id, + pformat(self.client.raw), + ) + @property def unique_id(self): """Return a unique identifier for this switch.""" From 4dd1c5118676f8afc631b165d09fc20db046163e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 Dec 2019 20:05:18 +0100 Subject: [PATCH 2390/3953] UniFi - honor detection time when UniFi wire bug happens (#29820) --- .../components/unifi/device_tracker.py | 18 ++++++++++++------ tests/components/unifi/test_device_tracker.py | 19 ++++++++++++++++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 200258306c1ab0..086393b85d2d68 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -129,6 +129,7 @@ def __init__(self, client, controller): self.client = client self.controller = controller self.is_wired = self.client.mac not in controller.wireless_clients + self.wired_bug = None @property def entity_registry_enabled_default(self): @@ -174,13 +175,18 @@ def is_connected(self): If is_wired and client.is_wired differ it means that the device is offline and UniFi bug shows device as wired. """ - if self.is_wired == self.client.is_wired and ( - ( - dt_util.utcnow() - - dt_util.utc_from_timestamp(float(self.client.last_seen)) + if self.is_wired != self.client.is_wired: + if not self.wired_bug: + self.wired_bug = dt_util.utcnow() + since_last_seen = dt_util.utcnow() - self.wired_bug + + else: + self.wired_bug = None + since_last_seen = dt_util.utcnow() - dt_util.utc_from_timestamp( + float(self.client.last_seen) ) - < self.controller.option_detection_time - ): + + if since_last_seen < self.controller.option_detection_time: return True return False diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 29b165537571b5..580365bb8bd4dc 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -2,6 +2,8 @@ from copy import copy from datetime import timedelta +from asynctest import patch + from homeassistant import config_entries from homeassistant.components import unifi from homeassistant.components.unifi.const import ( @@ -18,8 +20,6 @@ from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration -DEFAULT_DETECTION_TIME = timedelta(seconds=300) - CLIENT_1 = { "essid": "ssid", "hostname": "client_1", @@ -203,7 +203,20 @@ async def test_wireless_client_go_wired_issue(hass): await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") - assert client_1.state == "not_home" + assert client_1.state == "home" + + with patch.object( + unifi.device_tracker.dt_util, + "utcnow", + return_value=(dt_util.utcnow() + timedelta(minutes=5)), + ): + controller.mock_client_responses.append([client_1_client]) + controller.mock_device_responses.append({}) + await controller.async_update() + await hass.async_block_till_done() + + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" client_1_client["is_wired"] = False client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) From 2fb36d3dea735ac1b5adb3ce5e67c6a6e3ddaf51 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 Dec 2019 20:04:48 +0100 Subject: [PATCH 2391/3953] UniFi - Handle disabled switches (#29824) --- homeassistant/components/unifi/switch.py | 33 ++++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index f08c3e65630fe0..45f74f8882f4b6 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -74,12 +74,13 @@ def update_items(controller, async_add_entities, switches, switches_off): block_client_id = f"block-{client_id}" if block_client_id in switches: - LOGGER.debug( - "Updating UniFi block switch %s (%s)", - switches[block_client_id].entity_id, - switches[block_client_id].client.mac, - ) - switches[block_client_id].async_schedule_update_ha_state() + if switches[block_client_id].enabled: + LOGGER.debug( + "Updating UniFi block switch %s (%s)", + switches[block_client_id].entity_id, + switches[block_client_id].client.mac, + ) + switches[block_client_id].async_schedule_update_ha_state() continue if client_id not in controller.api.clients_all: @@ -88,7 +89,6 @@ def update_items(controller, async_add_entities, switches, switches_off): client = controller.api.clients_all[client_id] switches[block_client_id] = UniFiBlockClientSwitch(client, controller) new_switches.append(switches[block_client_id]) - LOGGER.debug("New UniFi Block switch %s (%s)", client.hostname, client.mac) # control POE for client_id in controller.api.clients: @@ -96,12 +96,13 @@ def update_items(controller, async_add_entities, switches, switches_off): poe_client_id = f"poe-{client_id}" if poe_client_id in switches: - LOGGER.debug( - "Updating UniFi POE switch %s (%s)", - switches[poe_client_id].entity_id, - switches[poe_client_id].client.mac, - ) - switches[poe_client_id].async_schedule_update_ha_state() + if switches[poe_client_id].enabled: + LOGGER.debug( + "Updating UniFi POE switch %s (%s)", + switches[poe_client_id].entity_id, + switches[poe_client_id].client.mac, + ) + switches[poe_client_id].async_schedule_update_ha_state() continue client = controller.api.clients[client_id] @@ -139,7 +140,6 @@ def update_items(controller, async_add_entities, switches, switches_off): switches[poe_client_id] = UniFiPOEClientSwitch(client, controller) new_switches.append(switches[poe_client_id]) - LOGGER.debug("New UniFi POE switch %s (%s)", client.hostname, client.mac) if new_switches: async_add_entities(new_switches) @@ -180,6 +180,7 @@ def __init__(self, client, controller): async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" + LOGGER.debug("New UniFi POE switch %s (%s)", self.name, self.client.mac) state = await self.async_get_last_state() if state is None: @@ -263,6 +264,10 @@ def port(self): class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): """Representation of a blockable client.""" + async def async_added_to_hass(self): + """Call when entity about to be added to Home Assistant.""" + LOGGER.debug("New UniFi Block switch %s (%s)", self.name, self.client.mac) + @property def unique_id(self): """Return a unique identifier for this switch.""" From ce041f131e84593d2e4f44a09cf6ece5d9be32db Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 11 Dec 2019 13:28:50 +0100 Subject: [PATCH 2392/3953] Remove uvloop event policy (#29835) * Remove uvloop event policy * Clean tests * Fix lint * Cleanup statment --- homeassistant/__main__.py | 12 +----------- tests/conftest.py | 6 ------ 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index d2903370f4992f..40336c195924d1 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -15,12 +15,10 @@ def set_loop() -> None: - """Attempt to use uvloop.""" + """Attempt to use different loop.""" import asyncio from asyncio.events import BaseDefaultEventLoopPolicy - policy = None - if sys.platform == "win32": if hasattr(asyncio, "WindowsProactorEventLoopPolicy"): # pylint: disable=no-member @@ -33,15 +31,7 @@ class ProactorPolicy(BaseDefaultEventLoopPolicy): _loop_factory = asyncio.ProactorEventLoop policy = ProactorPolicy() - else: - try: - import uvloop - except ImportError: - pass - else: - policy = uvloop.EventLoopPolicy() - if policy is not None: asyncio.set_event_loop_policy(policy) diff --git a/tests/conftest.py b/tests/conftest.py index 5e1bbc76fb5641..a6683ecad3ab48 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,6 @@ """Set up some common test helper things.""" -import asyncio import functools import logging -import os from unittest.mock import patch import pytest @@ -26,10 +24,6 @@ mock_aiohttp_client, ) # noqa: E402 module level import not at top of file -if os.environ.get("UVLOOP") == "1": - import uvloop - - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) logging.basicConfig(level=logging.DEBUG) logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) From 87164e21291472a9f5914c2af2823fe6f192084f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 11 Dec 2019 15:55:23 +0100 Subject: [PATCH 2393/3953] Bumped version to 0.103.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 35ee19da6b596f..e312bd4c2574aa 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 103 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From a08e3d73524891d6357ba1bc9dca8db2399c03f9 Mon Sep 17 00:00:00 2001 From: foxy82 Date: Wed, 11 Dec 2019 14:58:49 +0000 Subject: [PATCH 2394/3953] Update rfxtrx component so it can be run as a custom_component (#29638) * Updating rfxtrx component so it can be run as a custom_component * Fix pylint errors * Fix pylint errors * Fix pylint error on dict. * isort --- .../components/rfxtrx/binary_sensor.py | 36 ++++++++++--------- homeassistant/components/rfxtrx/cover.py | 18 ++++++---- homeassistant/components/rfxtrx/light.py | 18 ++++++---- homeassistant/components/rfxtrx/sensor.py | 20 ++++++----- homeassistant/components/rfxtrx/switch.py | 18 ++++++---- 5 files changed, 64 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 6465dc36326ae6..a88594dcceaa0b 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -4,7 +4,6 @@ import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, @@ -26,6 +25,14 @@ CONF_DEVICES, CONF_FIRE_EVENT, CONF_OFF_DELAY, + RECEIVED_EVT_SUBSCRIBERS, + RFX_DEVICES, + apply_received_command, + find_possible_pt2262_device, + get_pt2262_cmd, + get_pt2262_device, + get_pt2262_deviceid, + get_rfx_object, ) _LOGGER = logging.getLogger(__name__) @@ -58,16 +65,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for packet_id, entity in config[CONF_DEVICES].items(): - event = rfxtrx.get_rfx_object(packet_id) + event = get_rfx_object(packet_id) device_id = slugify(event.device.id_string.lower()) - if device_id in rfxtrx.RFX_DEVICES: + if device_id in RFX_DEVICES: continue if entity.get(CONF_DATA_BITS) is not None: _LOGGER.debug( "Masked device id: %s", - rfxtrx.get_pt2262_deviceid(device_id, entity.get(CONF_DATA_BITS)), + get_pt2262_deviceid(device_id, entity.get(CONF_DATA_BITS)), ) _LOGGER.debug( @@ -88,7 +95,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) device.hass = hass sensors.append(device) - rfxtrx.RFX_DEVICES[device_id] = device + RFX_DEVICES[device_id] = device add_entities(sensors) @@ -99,10 +106,7 @@ def binary_sensor_update(event): device_id = slugify(event.device.id_string.lower()) - if device_id in rfxtrx.RFX_DEVICES: - sensor = rfxtrx.RFX_DEVICES[device_id] - else: - sensor = rfxtrx.get_pt2262_device(device_id) + sensor = RFX_DEVICES.get(device_id, get_pt2262_device(device_id)) if sensor is None: # Add the entity if not exists and automatic_add is True @@ -110,7 +114,7 @@ def binary_sensor_update(event): return if event.device.packettype == 0x13: - poss_dev = rfxtrx.find_possible_pt2262_device(device_id) + poss_dev = find_possible_pt2262_device(device_id) if poss_dev is not None: poss_id = slugify(poss_dev.event.device.id_string.lower()) _LOGGER.debug("Found possible matching device ID: %s", poss_id) @@ -118,7 +122,7 @@ def binary_sensor_update(event): pkt_id = "".join(f"{x:02x}" for x in event.data) sensor = RfxtrxBinarySensor(event, pkt_id) sensor.hass = hass - rfxtrx.RFX_DEVICES[device_id] = sensor + RFX_DEVICES[device_id] = sensor add_entities([sensor]) _LOGGER.info( "Added binary sensor %s (Device ID: %s Class: %s Sub: %s)", @@ -140,12 +144,12 @@ def binary_sensor_update(event): if sensor.is_lighting4: if sensor.data_bits is not None: - cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits) + cmd = get_pt2262_cmd(device_id, sensor.data_bits) sensor.apply_cmd(int(cmd, 16)) else: sensor.update_state(True) else: - rfxtrx.apply_received_command(event) + apply_received_command(event) if ( sensor.is_on @@ -163,8 +167,8 @@ def off_delay_listener(now): ) # Subscribe to main RFXtrx events - if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update) + if binary_sensor_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update) class RfxtrxBinarySensor(BinarySensorDevice): @@ -195,7 +199,7 @@ def __init__( self._cmd_off = cmd_off if data_bits is not None: - self._masked_id = rfxtrx.get_pt2262_deviceid( + self._masked_id = get_pt2262_deviceid( event.device.id_string.lower(), data_bits ) else: diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 7aff22bd12460f..4806fd9a6b7107 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -2,7 +2,6 @@ import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv @@ -13,6 +12,11 @@ CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, + RECEIVED_EVT_SUBSCRIBERS, + RfxtrxDevice, + apply_received_command, + get_devices_from_config, + get_new_device, ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -35,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx cover.""" - covers = rfxtrx.get_devices_from_config(config, RfxtrxCover) + covers = get_devices_from_config(config, RfxtrxCover) add_entities(covers) def cover_update(event): @@ -47,18 +51,18 @@ def cover_update(event): ): return - new_device = rfxtrx.get_new_device(event, config, RfxtrxCover) + new_device = get_new_device(event, config, RfxtrxCover) if new_device: add_entities([new_device]) - rfxtrx.apply_received_command(event) + apply_received_command(event) # Subscribe to main RFXtrx events - if cover_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update) + if cover_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(cover_update) -class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice): +class RfxtrxCover(RfxtrxDevice, CoverDevice): """Representation of a RFXtrx cover.""" @property diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index a745a11388ac67..a29b8bfa6605bd 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -4,7 +4,6 @@ import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, @@ -20,6 +19,11 @@ CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, + RECEIVED_EVT_SUBSCRIBERS, + RfxtrxDevice, + apply_received_command, + get_devices_from_config, + get_new_device, ) _LOGGER = logging.getLogger(__name__) @@ -46,7 +50,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" - lights = rfxtrx.get_devices_from_config(config, RfxtrxLight) + lights = get_devices_from_config(config, RfxtrxLight) add_entities(lights) def light_update(event): @@ -57,18 +61,18 @@ def light_update(event): ): return - new_device = rfxtrx.get_new_device(event, config, RfxtrxLight) + new_device = get_new_device(event, config, RfxtrxLight) if new_device: add_entities([new_device]) - rfxtrx.apply_received_command(event) + apply_received_command(event) # Subscribe to main RFXtrx events - if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(light_update) + if light_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(light_update) -class RfxtrxLight(rfxtrx.RfxtrxDevice, Light): +class RfxtrxLight(RfxtrxDevice, Light): """Representation of a RFXtrx light.""" @property diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 5429943a7a6123..3f74ff1869569c 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -4,7 +4,6 @@ from RFXtrx import SensorEvent import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -19,6 +18,9 @@ CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES, + RECEIVED_EVT_SUBSCRIBERS, + RFX_DEVICES, + get_rfx_object, ) _LOGGER = logging.getLogger(__name__) @@ -46,9 +48,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" sensors = [] for packet_id, entity_info in config[CONF_DEVICES].items(): - event = rfxtrx.get_rfx_object(packet_id) + event = get_rfx_object(packet_id) device_id = "sensor_{}".format(slugify(event.device.id_string.lower())) - if device_id in rfxtrx.RFX_DEVICES: + if device_id in RFX_DEVICES: continue _LOGGER.info("Add %s rfxtrx.sensor", entity_info[ATTR_NAME]) @@ -66,7 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) sensors.append(new_sensor) sub_sensors[_data_type] = new_sensor - rfxtrx.RFX_DEVICES[device_id] = sub_sensors + RFX_DEVICES[device_id] = sub_sensors add_entities(sensors) def sensor_update(event): @@ -76,8 +78,8 @@ def sensor_update(event): device_id = "sensor_" + slugify(event.device.id_string.lower()) - if device_id in rfxtrx.RFX_DEVICES: - sensors = rfxtrx.RFX_DEVICES[device_id] + if device_id in RFX_DEVICES: + sensors = RFX_DEVICES[device_id] for data_type in sensors: # Some multi-sensor devices send individual messages for each # of their sensors. Update only if event contains the @@ -108,11 +110,11 @@ def sensor_update(event): new_sensor = RfxtrxSensor(event, pkt_id, data_type) sub_sensors = {} sub_sensors[new_sensor.data_type] = new_sensor - rfxtrx.RFX_DEVICES[device_id] = sub_sensors + RFX_DEVICES[device_id] = sub_sensors add_entities([new_sensor]) - if sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(sensor_update) + if sensor_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(sensor_update) class RfxtrxSensor(Entity): diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 6d91b261a4f5d7..49bcd1b3924527 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -4,7 +4,6 @@ import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME from homeassistant.helpers import config_validation as cv @@ -15,6 +14,11 @@ CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, + RECEIVED_EVT_SUBSCRIBERS, + RfxtrxDevice, + apply_received_command, + get_devices_from_config, + get_new_device, ) _LOGGER = logging.getLogger(__name__) @@ -40,7 +44,7 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the RFXtrx platform.""" # Add switch from config file - switches = rfxtrx.get_devices_from_config(config, RfxtrxSwitch) + switches = get_devices_from_config(config, RfxtrxSwitch) add_entities_callback(switches) def switch_update(event): @@ -52,18 +56,18 @@ def switch_update(event): ): return - new_device = rfxtrx.get_new_device(event, config, RfxtrxSwitch) + new_device = get_new_device(event, config, RfxtrxSwitch) if new_device: add_entities_callback([new_device]) - rfxtrx.apply_received_command(event) + apply_received_command(event) # Subscribe to main RFXtrx events - if switch_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(switch_update) + if switch_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(switch_update) -class RfxtrxSwitch(rfxtrx.RfxtrxDevice, SwitchDevice): +class RfxtrxSwitch(RfxtrxDevice, SwitchDevice): """Representation of a RFXtrx switch.""" def turn_on(self, **kwargs): From 7bd98d108244f3f3206b1ca0e83c385df3711413 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 12 Dec 2019 00:32:15 +0000 Subject: [PATCH 2395/3953] [ci skip] Translation update --- .../icloud/.translations/zh-Hant.json | 38 +++++++++++++++++++ .../components/soma/.translations/ca.json | 4 +- .../components/soma/.translations/en.json | 4 +- .../components/soma/.translations/no.json | 4 +- 4 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/icloud/.translations/zh-Hant.json diff --git a/homeassistant/components/icloud/.translations/zh-Hant.json b/homeassistant/components/icloud/.translations/zh-Hant.json new file mode 100644 index 00000000000000..80d8ba1485b699 --- /dev/null +++ b/homeassistant/components/icloud/.translations/zh-Hant.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "login": "\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u78ba\u8a8d\u96fb\u5b50\u90f5\u4ef6\u8207\u79d8\u5bc6\u6b63\u78ba\u6027", + "send_verification_code": "\u50b3\u9001\u9a57\u8b49\u78bc\u5931\u6557", + "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "validate_verification_code": "\u7121\u6cd5\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\uff0c\u9078\u64c7\u4e00\u90e8\u4fe1\u4efb\u8a2d\u5099\u3001\u7136\u5f8c\u91cd\u65b0\u57f7\u884c\u9a57\u8b49\u3002" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "\u4fe1\u4efb\u8a2d\u5099" + }, + "description": "\u9078\u64c7\u4fe1\u4efb\u8a2d\u5099", + "title": "iCloud \u4fe1\u4efb\u8a2d\u5099" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u8f38\u5165\u6191\u8b49", + "title": "iCloud \u6191\u8b49" + }, + "verification_code": { + "data": { + "verification_code": "\u9a57\u8b49\u78bc" + }, + "description": "\u8acb\u8f38\u5165\u6240\u6536\u5230\u7684 iCloud \u9a57\u8b49\u78bc", + "title": "iCloud \u9a57\u8b49\u78bc" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ca.json b/homeassistant/components/soma/.translations/ca.json index 18b33d1bc9b152..a1a5b9489faf8d 100644 --- a/homeassistant/components/soma/.translations/ca.json +++ b/homeassistant/components/soma/.translations/ca.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Nom\u00e9s pots configurar un compte de Soma.", "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "missing_configuration": "El component Soma no est\u00e0 configurat. Mira'n la documentaci\u00f3." + "connection_error": "No s'ha pogut connectar amb SOMA Connect.", + "missing_configuration": "El component Soma no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "result_error": "SOMA Connect ha respost amb un estat d'error." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Soma." diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json index ae2f99001620ee..46bfd441fc49cf 100644 --- a/homeassistant/components/soma/.translations/en.json +++ b/homeassistant/components/soma/.translations/en.json @@ -3,9 +3,9 @@ "abort": { "already_setup": "You can only configure one Soma account.", "authorize_url_timeout": "Timeout generating authorize url.", + "connection_error": "Failed to connect to SOMA Connect.", "missing_configuration": "The Soma component is not configured. Please follow the documentation.", - "result_error": "SOMA Connect responded with error status.", - "connection_error": "Failed to connect to SOMA Connect." + "result_error": "SOMA Connect responded with error status." }, "create_entry": { "default": "Successfully authenticated with Soma." diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json index b2d80208b83e70..518cbc6a37e5a3 100644 --- a/homeassistant/components/soma/.translations/no.json +++ b/homeassistant/components/soma/.translations/no.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Du kan bare konfigurere \u00e9n Soma-konto.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", - "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "connection_error": "Kunne ikke koble til SOMA Connect.", + "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "result_error": "SOMA Connect svarte med feilstatus." }, "create_entry": { "default": "Vellykket autentisering med Somfy." From b9eb831d291d41b4673325b57be410bb7efb53d5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 12 Dec 2019 06:37:55 +0100 Subject: [PATCH 2396/3953] Revert Tahoma removal (#29840) * Revert "Remove Tahoma component #29744 (#29745)" This reverts commit df74272ba6311d527fd07198929c80a45d9fed15. * Revert "Cleanup removed component (#29788)" This reverts commit 3a28361bebbe11d124793a0ec269de74ef180665. --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/tahoma/__init__.py | 140 ++++++++++ .../components/tahoma/binary_sensor.py | 95 +++++++ homeassistant/components/tahoma/cover.py | 249 ++++++++++++++++++ homeassistant/components/tahoma/manifest.json | 12 + homeassistant/components/tahoma/scene.py | 41 +++ homeassistant/components/tahoma/sensor.py | 106 ++++++++ homeassistant/components/tahoma/switch.py | 109 ++++++++ requirements_all.txt | 3 + 10 files changed, 757 insertions(+) create mode 100644 homeassistant/components/tahoma/__init__.py create mode 100644 homeassistant/components/tahoma/binary_sensor.py create mode 100644 homeassistant/components/tahoma/cover.py create mode 100644 homeassistant/components/tahoma/manifest.json create mode 100644 homeassistant/components/tahoma/scene.py create mode 100644 homeassistant/components/tahoma/sensor.py create mode 100644 homeassistant/components/tahoma/switch.py diff --git a/.coveragerc b/.coveragerc index 0b73599dffa7e1..f4794b593816f1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -676,6 +676,7 @@ omit = homeassistant/components/systemmonitor/sensor.py homeassistant/components/tado/* homeassistant/components/tado/device_tracker.py + homeassistant/components/tahoma/* homeassistant/components/tank_utility/sensor.py homeassistant/components/tapsaff/binary_sensor.py homeassistant/components/tautulli/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 392c363c648c28..4fbdca20686615 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -318,6 +318,7 @@ homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff homeassistant/components/tado/* @michaelarnauts +homeassistant/components/tahoma/* @philklei homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike homeassistant/components/template/* @PhracturedBlue diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py new file mode 100644 index 00000000000000..640cc6418d0f01 --- /dev/null +++ b/homeassistant/components/tahoma/__init__.py @@ -0,0 +1,140 @@ +"""Support for Tahoma devices.""" +from collections import defaultdict +import logging + +from requests.exceptions import RequestException +from tahoma_api import Action, TahomaApi +import voluptuous as vol + +from homeassistant.const import CONF_EXCLUDE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "tahoma" + +TAHOMA_ID_FORMAT = "{}_{}" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_EXCLUDE, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +TAHOMA_COMPONENTS = ["scene", "sensor", "cover", "switch", "binary_sensor"] + +TAHOMA_TYPES = { + "io:ExteriorVenetianBlindIOComponent": "cover", + "io:HorizontalAwningIOComponent": "cover", + "io:LightIOSystemSensor": "sensor", + "io:OnOffIOComponent": "switch", + "io:OnOffLightIOComponent": "switch", + "io:RollerShutterGenericIOComponent": "cover", + "io:RollerShutterUnoIOComponent": "cover", + "io:RollerShutterVeluxIOComponent": "cover", + "io:RollerShutterWithLowSpeedManagementIOComponent": "cover", + "io:SomfyBasicContactIOSystemSensor": "sensor", + "io:SomfyContactIOSystemSensor": "sensor", + "io:VerticalExteriorAwningIOComponent": "cover", + "io:VerticalInteriorBlindVeluxIOComponent": "cover", + "io:WindowOpenerVeluxIOComponent": "cover", + "io:GarageOpenerIOComponent": "cover", + "io:DiscreteGarageOpenerIOComponent": "cover", + "rtds:RTDSContactSensor": "sensor", + "rtds:RTDSMotionSensor": "sensor", + "rtds:RTDSSmokeSensor": "smoke", + "rts:BlindRTSComponent": "cover", + "rts:CurtainRTSComponent": "cover", + "rts:DualCurtainRTSComponent": "cover", + "rts:ExteriorVenetianBlindRTSComponent": "cover", + "rts:GarageDoor4TRTSComponent": "switch", + "rts:RollerShutterRTSComponent": "cover", + "rts:VenetianBlindRTSComponent": "cover", +} + + +def setup(hass, config): + """Activate Tahoma component.""" + + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + exclude = conf.get(CONF_EXCLUDE) + try: + api = TahomaApi(username, password) + except RequestException: + _LOGGER.exception("Error when trying to log in to the Tahoma API") + return False + + try: + api.get_setup() + devices = api.get_devices() + scenes = api.get_action_groups() + except RequestException: + _LOGGER.exception("Error when getting devices from the Tahoma API") + return False + + hass.data[DOMAIN] = {"controller": api, "devices": defaultdict(list), "scenes": []} + + for device in devices: + _device = api.get_device(device) + if all(ext not in _device.type for ext in exclude): + device_type = map_tahoma_device(_device) + if device_type is None: + _LOGGER.warning( + "Unsupported type %s for Tahoma device %s", + _device.type, + _device.label, + ) + continue + hass.data[DOMAIN]["devices"][device_type].append(_device) + + for scene in scenes: + hass.data[DOMAIN]["scenes"].append(scene) + + for component in TAHOMA_COMPONENTS: + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + + +def map_tahoma_device(tahoma_device): + """Map Tahoma device types to Home Assistant components.""" + return TAHOMA_TYPES.get(tahoma_device.type) + + +class TahomaDevice(Entity): + """Representation of a Tahoma device entity.""" + + def __init__(self, tahoma_device, controller): + """Initialize the device.""" + self.tahoma_device = tahoma_device + self.controller = controller + self._name = self.tahoma_device.label + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return {"tahoma_device_id": self.tahoma_device.url} + + def apply_action(self, cmd_name, *args): + """Apply Action to Device.""" + + action = Action(self.tahoma_device.url) + action.add_command(cmd_name, *args) + self.controller.apply_actions("HomeAssistant", [action]) diff --git a/homeassistant/components/tahoma/binary_sensor.py b/homeassistant/components/tahoma/binary_sensor.py new file mode 100644 index 00000000000000..81078ab480babd --- /dev/null +++ b/homeassistant/components/tahoma/binary_sensor.py @@ -0,0 +1,95 @@ +"""Support for Tahoma binary sensors.""" +from datetime import timedelta +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON + +from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=120) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Tahoma controller devices.""" + _LOGGER.debug("Setup Tahoma Binary sensor platform") + controller = hass.data[TAHOMA_DOMAIN]["controller"] + devices = [] + for device in hass.data[TAHOMA_DOMAIN]["devices"]["smoke"]: + devices.append(TahomaBinarySensor(device, controller)) + add_entities(devices, True) + + +class TahomaBinarySensor(TahomaDevice, BinarySensorDevice): + """Representation of a Tahoma Binary Sensor.""" + + def __init__(self, tahoma_device, controller): + """Initialize the sensor.""" + super().__init__(tahoma_device, controller) + + self._state = None + self._icon = None + self._battery = None + self._available = False + + @property + def is_on(self): + """Return the state of the sensor.""" + return bool(self._state == STATE_ON) + + @property + def device_class(self): + """Return the class of the device.""" + if self.tahoma_device.type == "rtds:RTDSSmokeSensor": + return "smoke" + return None + + @property + def icon(self): + """Icon for device by its type.""" + return self._icon + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attr = {} + super_attr = super().device_state_attributes + if super_attr is not None: + attr.update(super_attr) + + if self._battery is not None: + attr[ATTR_BATTERY_LEVEL] = self._battery + return attr + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + def update(self): + """Update the state.""" + self.controller.get_states([self.tahoma_device]) + if self.tahoma_device.type == "rtds:RTDSSmokeSensor": + if self.tahoma_device.active_states["core:SmokeState"] == "notDetected": + self._state = STATE_OFF + else: + self._state = STATE_ON + + if "core:SensorDefectState" in self.tahoma_device.active_states: + # 'lowBattery' for low battery warning. 'dead' for not available. + self._battery = self.tahoma_device.active_states["core:SensorDefectState"] + self._available = bool(self._battery != "dead") + else: + self._battery = None + self._available = True + + if self._state == STATE_ON: + self._icon = "mdi:fire" + elif self._battery == "lowBattery": + self._icon = "mdi:battery-alert" + else: + self._icon = None + + _LOGGER.debug("Update %s, state: %s", self._name, self._state) diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py new file mode 100644 index 00000000000000..e11c2f4cdf59ce --- /dev/null +++ b/homeassistant/components/tahoma/cover.py @@ -0,0 +1,249 @@ +"""Support for Tahoma cover - shutters etc.""" +from datetime import timedelta +import logging + +from homeassistant.components.cover import ( + ATTR_POSITION, + DEVICE_CLASS_AWNING, + DEVICE_CLASS_BLIND, + DEVICE_CLASS_CURTAIN, + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW, + CoverDevice, +) +from homeassistant.util.dt import utcnow + +from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice + +_LOGGER = logging.getLogger(__name__) + +ATTR_MEM_POS = "memorized_position" +ATTR_RSSI_LEVEL = "rssi_level" +ATTR_LOCK_START_TS = "lock_start_ts" +ATTR_LOCK_END_TS = "lock_end_ts" +ATTR_LOCK_LEVEL = "lock_level" +ATTR_LOCK_ORIG = "lock_originator" + +HORIZONTAL_AWNING = "io:HorizontalAwningIOComponent" + +TAHOMA_DEVICE_CLASSES = { + "io:ExteriorVenetianBlindIOComponent": DEVICE_CLASS_BLIND, + HORIZONTAL_AWNING: DEVICE_CLASS_AWNING, + "io:RollerShutterGenericIOComponent": DEVICE_CLASS_SHUTTER, + "io:RollerShutterUnoIOComponent": DEVICE_CLASS_SHUTTER, + "io:RollerShutterVeluxIOComponent": DEVICE_CLASS_SHUTTER, + "io:RollerShutterWithLowSpeedManagementIOComponent": DEVICE_CLASS_SHUTTER, + "io:VerticalExteriorAwningIOComponent": DEVICE_CLASS_AWNING, + "io:VerticalInteriorBlindVeluxIOComponent": DEVICE_CLASS_BLIND, + "io:WindowOpenerVeluxIOComponent": DEVICE_CLASS_WINDOW, + "io:GarageOpenerIOComponent": DEVICE_CLASS_GARAGE, + "io:DiscreteGarageOpenerIOComponent": DEVICE_CLASS_GARAGE, + "rts:BlindRTSComponent": DEVICE_CLASS_BLIND, + "rts:CurtainRTSComponent": DEVICE_CLASS_CURTAIN, + "rts:DualCurtainRTSComponent": DEVICE_CLASS_CURTAIN, + "rts:ExteriorVenetianBlindRTSComponent": DEVICE_CLASS_BLIND, + "rts:RollerShutterRTSComponent": DEVICE_CLASS_SHUTTER, + "rts:VenetianBlindRTSComponent": DEVICE_CLASS_BLIND, +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Tahoma covers.""" + controller = hass.data[TAHOMA_DOMAIN]["controller"] + devices = [] + for device in hass.data[TAHOMA_DOMAIN]["devices"]["cover"]: + devices.append(TahomaCover(device, controller)) + add_entities(devices, True) + + +class TahomaCover(TahomaDevice, CoverDevice): + """Representation a Tahoma Cover.""" + + def __init__(self, tahoma_device, controller): + """Initialize the device.""" + super().__init__(tahoma_device, controller) + + self._closure = 0 + # 100 equals open + self._position = 100 + self._closed = False + self._rssi_level = None + self._icon = None + # Can be 0 and bigger + self._lock_timer = 0 + self._lock_start_ts = None + self._lock_end_ts = None + # Can be 'comfortLevel1', 'comfortLevel2', 'comfortLevel3', + # 'comfortLevel4', 'environmentProtection', 'humanProtection', + # 'userLevel1', 'userLevel2' + self._lock_level = None + # Can be 'LSC', 'SAAC', 'SFC', 'UPS', 'externalGateway', 'localUser', + # 'myself', 'rain', 'security', 'temperature', 'timer', 'user', 'wind' + self._lock_originator = None + + def update(self): + """Update method.""" + self.controller.get_states([self.tahoma_device]) + + # For vertical covers + self._closure = self.tahoma_device.active_states.get("core:ClosureState") + # For horizontal covers + if self._closure is None: + self._closure = self.tahoma_device.active_states.get("core:DeploymentState") + + # For all, if available + if "core:PriorityLockTimerState" in self.tahoma_device.active_states: + old_lock_timer = self._lock_timer + self._lock_timer = self.tahoma_device.active_states[ + "core:PriorityLockTimerState" + ] + # Derive timestamps from _lock_timer, only if not already set or + # something has changed + if self._lock_timer > 0: + _LOGGER.debug("Update %s, lock_timer: %d", self._name, self._lock_timer) + if self._lock_start_ts is None: + self._lock_start_ts = utcnow() + if self._lock_end_ts is None or old_lock_timer != self._lock_timer: + self._lock_end_ts = utcnow() + timedelta(seconds=self._lock_timer) + else: + self._lock_start_ts = None + self._lock_end_ts = None + else: + self._lock_timer = 0 + self._lock_start_ts = None + self._lock_end_ts = None + + self._lock_level = self.tahoma_device.active_states.get( + "io:PriorityLockLevelState" + ) + + self._lock_originator = self.tahoma_device.active_states.get( + "io:PriorityLockOriginatorState" + ) + + self._rssi_level = self.tahoma_device.active_states.get("core:RSSILevelState") + + # Define which icon to use + if self._lock_timer > 0: + if self._lock_originator == "wind": + self._icon = "mdi:weather-windy" + else: + self._icon = "mdi:lock-alert" + else: + self._icon = None + + # Define current position. + # _position: 0 is closed, 100 is fully open. + # 'core:ClosureState': 100 is closed, 0 is fully open. + if self._closure is not None: + if self.tahoma_device.type == HORIZONTAL_AWNING: + self._position = self._closure + else: + self._position = 100 - self._closure + if self._position <= 5: + self._position = 0 + if self._position >= 95: + self._position = 100 + self._closed = self._position == 0 + else: + self._position = None + if "core:OpenClosedState" in self.tahoma_device.active_states: + self._closed = ( + self.tahoma_device.active_states["core:OpenClosedState"] == "closed" + ) + else: + self._closed = False + + _LOGGER.debug("Update %s, position: %d", self._name, self._position) + + @property + def current_cover_position(self): + """Return current position of cover.""" + return self._position + + def set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + if self.tahoma_device.type == "io:WindowOpenerVeluxIOComponent": + command = "setClosure" + else: + command = "setPosition" + + if self.tahoma_device.type == HORIZONTAL_AWNING: + self.apply_action(command, kwargs.get(ATTR_POSITION, 0)) + else: + self.apply_action(command, 100 - kwargs.get(ATTR_POSITION, 0)) + + @property + def is_closed(self): + """Return if the cover is closed.""" + return self._closed + + @property + def device_class(self): + """Return the class of the device.""" + return TAHOMA_DEVICE_CLASSES.get(self.tahoma_device.type) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attr = {} + super_attr = super().device_state_attributes + if super_attr is not None: + attr.update(super_attr) + + if "core:Memorized1PositionState" in self.tahoma_device.active_states: + attr[ATTR_MEM_POS] = self.tahoma_device.active_states[ + "core:Memorized1PositionState" + ] + if self._rssi_level is not None: + attr[ATTR_RSSI_LEVEL] = self._rssi_level + if self._lock_start_ts is not None: + attr[ATTR_LOCK_START_TS] = self._lock_start_ts.isoformat() + if self._lock_end_ts is not None: + attr[ATTR_LOCK_END_TS] = self._lock_end_ts.isoformat() + if self._lock_level is not None: + attr[ATTR_LOCK_LEVEL] = self._lock_level + if self._lock_originator is not None: + attr[ATTR_LOCK_ORIG] = self._lock_originator + return attr + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + + def open_cover(self, **kwargs): + """Open the cover.""" + self.apply_action("open") + + def close_cover(self, **kwargs): + """Close the cover.""" + self.apply_action("close") + + def stop_cover(self, **kwargs): + """Stop the cover.""" + if ( + self.tahoma_device.type + == "io:RollerShutterWithLowSpeedManagementIOComponent" + ): + self.apply_action("setPosition", "secured") + elif self.tahoma_device.type in ( + "rts:BlindRTSComponent", + "io:ExteriorVenetianBlindIOComponent", + "rts:VenetianBlindRTSComponent", + "rts:DualCurtainRTSComponent", + "rts:ExteriorVenetianBlindRTSComponent", + "rts:BlindRTSComponent", + ): + self.apply_action("my") + elif self.tahoma_device.type in ( + HORIZONTAL_AWNING, + "io:RollerShutterGenericIOComponent", + "io:VerticalExteriorAwningIOComponent", + "io:VerticalInteriorBlindVeluxIOComponent", + "io:WindowOpenerVeluxIOComponent", + ): + self.apply_action("stop") + else: + self.apply_action("stopIdentify") diff --git a/homeassistant/components/tahoma/manifest.json b/homeassistant/components/tahoma/manifest.json new file mode 100644 index 00000000000000..1e99d4b288d725 --- /dev/null +++ b/homeassistant/components/tahoma/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "tahoma", + "name": "Tahoma", + "documentation": "https://www.home-assistant.io/integrations/tahoma", + "requirements": [ + "tahoma-api==0.0.14" + ], + "dependencies": [], + "codeowners": [ + "@philklei" + ] +} diff --git a/homeassistant/components/tahoma/scene.py b/homeassistant/components/tahoma/scene.py new file mode 100644 index 00000000000000..e54ff91a0f6e90 --- /dev/null +++ b/homeassistant/components/tahoma/scene.py @@ -0,0 +1,41 @@ +"""Support for Tahoma scenes.""" +import logging + +from homeassistant.components.scene import Scene + +from . import DOMAIN as TAHOMA_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Tahoma scenes.""" + controller = hass.data[TAHOMA_DOMAIN]["controller"] + scenes = [] + for scene in hass.data[TAHOMA_DOMAIN]["scenes"]: + scenes.append(TahomaScene(scene, controller)) + add_entities(scenes, True) + + +class TahomaScene(Scene): + """Representation of a Tahoma scene entity.""" + + def __init__(self, tahoma_scene, controller): + """Initialize the scene.""" + self.tahoma_scene = tahoma_scene + self.controller = controller + self._name = self.tahoma_scene.name + + def activate(self): + """Activate the scene.""" + self.controller.launch_action_group(self.tahoma_scene.oid) + + @property + def name(self): + """Return the name of the scene.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of the scene.""" + return {"tahoma_scene_oid": self.tahoma_scene.oid} diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py new file mode 100644 index 00000000000000..5279b160d9c540 --- /dev/null +++ b/homeassistant/components/tahoma/sensor.py @@ -0,0 +1,106 @@ +"""Support for Tahoma sensors.""" +from datetime import timedelta +import logging + +from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.helpers.entity import Entity + +from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=60) + +ATTR_RSSI_LEVEL = "rssi_level" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Tahoma controller devices.""" + controller = hass.data[TAHOMA_DOMAIN]["controller"] + devices = [] + for device in hass.data[TAHOMA_DOMAIN]["devices"]["sensor"]: + devices.append(TahomaSensor(device, controller)) + add_entities(devices, True) + + +class TahomaSensor(TahomaDevice, Entity): + """Representation of a Tahoma Sensor.""" + + def __init__(self, tahoma_device, controller): + """Initialize the sensor.""" + self.current_value = None + self._available = False + super().__init__(tahoma_device, controller) + + @property + def state(self): + """Return the name of the sensor.""" + return self.current_value + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + if self.tahoma_device.type == "Temperature Sensor": + return None + if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": + return None + if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": + return None + if self.tahoma_device.type == "io:LightIOSystemSensor": + return "lx" + if self.tahoma_device.type == "Humidity Sensor": + return "%" + if self.tahoma_device.type == "rtds:RTDSContactSensor": + return None + if self.tahoma_device.type == "rtds:RTDSMotionSensor": + return None + + def update(self): + """Update the state.""" + self.controller.get_states([self.tahoma_device]) + if self.tahoma_device.type == "io:LightIOSystemSensor": + self.current_value = self.tahoma_device.active_states["core:LuminanceState"] + self._available = bool( + self.tahoma_device.active_states.get("core:StatusState") == "available" + ) + if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": + self.current_value = self.tahoma_device.active_states["core:ContactState"] + self._available = bool( + self.tahoma_device.active_states.get("core:StatusState") == "available" + ) + if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": + self.current_value = self.tahoma_device.active_states["core:ContactState"] + self._available = bool( + self.tahoma_device.active_states.get("core:StatusState") == "available" + ) + if self.tahoma_device.type == "rtds:RTDSContactSensor": + self.current_value = self.tahoma_device.active_states["core:ContactState"] + self._available = True + if self.tahoma_device.type == "rtds:RTDSMotionSensor": + self.current_value = self.tahoma_device.active_states["core:OccupancyState"] + self._available = True + + _LOGGER.debug("Update %s, value: %d", self._name, self.current_value) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attr = {} + super_attr = super().device_state_attributes + if super_attr is not None: + attr.update(super_attr) + + if "core:RSSILevelState" in self.tahoma_device.active_states: + attr[ATTR_RSSI_LEVEL] = self.tahoma_device.active_states[ + "core:RSSILevelState" + ] + if "core:SensorDefectState" in self.tahoma_device.active_states: + attr[ATTR_BATTERY_LEVEL] = self.tahoma_device.active_states[ + "core:SensorDefectState" + ] + return attr + + @property + def available(self): + """Return True if entity is available.""" + return self._available diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py new file mode 100644 index 00000000000000..a0a95ab47ce056 --- /dev/null +++ b/homeassistant/components/tahoma/switch.py @@ -0,0 +1,109 @@ +"""Support for Tahoma switches.""" +import logging + +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import STATE_OFF, STATE_ON + +from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice + +_LOGGER = logging.getLogger(__name__) + +ATTR_RSSI_LEVEL = "rssi_level" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up Tahoma switches.""" + controller = hass.data[TAHOMA_DOMAIN]["controller"] + devices = [] + for switch in hass.data[TAHOMA_DOMAIN]["devices"]["switch"]: + devices.append(TahomaSwitch(switch, controller)) + add_entities(devices, True) + + +class TahomaSwitch(TahomaDevice, SwitchDevice): + """Representation a Tahoma Switch.""" + + def __init__(self, tahoma_device, controller): + """Initialize the switch.""" + super().__init__(tahoma_device, controller) + self._state = STATE_OFF + self._skip_update = False + self._available = False + + def update(self): + """Update method.""" + # Postpone the immediate state check for changes that take time. + if self._skip_update: + self._skip_update = False + return + + self.controller.get_states([self.tahoma_device]) + + if self.tahoma_device.type == "io:OnOffLightIOComponent": + if self.tahoma_device.active_states.get("core:OnOffState") == "on": + self._state = STATE_ON + else: + self._state = STATE_OFF + + self._available = bool( + self.tahoma_device.active_states.get("core:StatusState") == "available" + ) + + _LOGGER.debug("Update %s, state: %s", self._name, self._state) + + @property + def device_class(self): + """Return the class of the device.""" + if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": + return "garage" + return None + + def turn_on(self, **kwargs): + """Send the on command.""" + _LOGGER.debug("Turn on: %s", self._name) + if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": + self.toggle() + else: + self.apply_action("on") + self._skip_update = True + self._state = STATE_ON + + def turn_off(self, **kwargs): + """Send the off command.""" + _LOGGER.debug("Turn off: %s", self._name) + if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": + return + + self.apply_action("off") + self._skip_update = True + self._state = STATE_OFF + + def toggle(self, **kwargs): + """Click the switch.""" + self.apply_action("cycle") + + @property + def is_on(self): + """Get whether the switch is in on state.""" + if self.tahoma_device.type == "rts:GarageDoor4TRTSComponent": + return False + return bool(self._state == STATE_ON) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attr = {} + super_attr = super().device_state_attributes + if super_attr is not None: + attr.update(super_attr) + + if "core:RSSILevelState" in self.tahoma_device.active_states: + attr[ATTR_RSSI_LEVEL] = self.tahoma_device.active_states[ + "core:RSSILevelState" + ] + return attr + + @property + def available(self): + """Return True if entity is available.""" + return self._available diff --git a/requirements_all.txt b/requirements_all.txt index 5f6e9ac78aab2c..c91ffeace3f629 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1906,6 +1906,9 @@ swisshydrodata==0.0.3 # homeassistant.components.synology_srm synology-srm==0.0.7 +# homeassistant.components.tahoma +tahoma-api==0.0.14 + # homeassistant.components.tank_utility tank_utility==1.4.0 From 7931a0b675d43e7f83a3f572799645bec31cc8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 12 Dec 2019 08:15:32 +0200 Subject: [PATCH 2397/3953] Use Bionic's ffmpeg on Travis, jonathonf/ffmpeg-4 is N/A at the moment (#29860) --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e35787bb1e818f..2660d805726c09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,6 @@ sudo: false dist: bionic addons: apt: - sources: - - sourceline: "ppa:jonathonf/ffmpeg-4" packages: - libudev-dev - libavformat-dev From 914b49566a812ee96646f5a42d11cc017ad24937 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 12 Dec 2019 02:24:57 -0700 Subject: [PATCH 2398/3953] Bump aioambient to 1.0.2 (#29850) --- 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 8f363ba219f99d..1e6c06f260a3c8 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/integrations/ambient_station", "requirements": [ - "aioambient==0.3.2" + "aioambient==1.0.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c91ffeace3f629..fb7b193ababc79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -133,7 +133,7 @@ aio_geojson_geonetnz_volcano==0.5 aio_geojson_nsw_rfs_incidents==0.1 # homeassistant.components.ambient_station -aioambient==0.3.2 +aioambient==1.0.2 # homeassistant.components.asuswrt aioasuswrt==1.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3812b8f0b339b8..9716cd2f950475 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -44,7 +44,7 @@ aio_geojson_geonetnz_volcano==0.5 aio_geojson_nsw_rfs_incidents==0.1 # homeassistant.components.ambient_station -aioambient==0.3.2 +aioambient==1.0.2 # homeassistant.components.asuswrt aioasuswrt==1.1.22 From 4b335787577a20c935f696c0e8673dc91eaac67b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Dec 2019 14:45:00 +0100 Subject: [PATCH 2399/3953] Fix package import sort on dwd_weather_warnings (#29874) --- homeassistant/components/dwd_weather_warnings/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 46b9ac560e5d6e..695b839d18cf89 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -21,8 +21,8 @@ from homeassistant.components.rest.sensor import RestData from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE as HA_USER_AGENT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.util.dt as dt_util From 7d68e88d31246e268db809225e3d924acb1fc352 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 12 Dec 2019 15:56:32 +0100 Subject: [PATCH 2400/3953] Sort import for tests/components/feedreader/test_init.py (#29878) This unblocks https://github.com/home-assistant/home-assistant/pull/29739 --- tests/components/feedreader/test_init.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index bffbe9676fa9ff..d74bbd79d804b1 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -1,6 +1,5 @@ """The tests for the feedreader component.""" from datetime import timedelta -from genericpath import exists from logging import getLogger from os import remove import time @@ -8,6 +7,8 @@ from unittest import mock from unittest.mock import patch +from genericpath import exists + from homeassistant.components import feedreader from homeassistant.components.feedreader import ( CONF_MAX_ENTRIES, From c58c10ab7cd75d093c4a64891c1193a2e01a3f55 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 12 Dec 2019 15:58:47 +0100 Subject: [PATCH 2401/3953] Add isort to CI and pre-commit (#29739) * add isort to CI and pre-commit * disable wrong-import-order in pylintrc * :pencil2: Tweak Co-authored-by: Franck Nijhof --- .pre-commit-config-all.yaml | 4 ++++ .pre-commit-config.yaml | 4 ++++ azure-pipelines-ci.yml | 4 ++++ pylintrc | 4 +++- requirements_test_pre_commit.txt | 1 + 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml index cb01bff85cb084..ec9492e0210d9e 100644 --- a/.pre-commit-config-all.yaml +++ b/.pre-commit-config-all.yaml @@ -35,6 +35,10 @@ repos: - --format=custom - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort # Using a local "system" mypy instead of the mypy hook, because its # results depend on what is installed. And the mypy hook runs in a # virtualenv of its own, meaning we'd need to install and maintain diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6f37ae6355929..23d1d9c73f9cf3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,3 +31,7 @@ repos: - --format=custom - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index cbad0c9af08a1c..5a289cbbc70c2a 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -54,6 +54,10 @@ stages: . venv/bin/activate pre-commit run bandit --all-files displayName: 'Run bandit' + - script: | + . venv/bin/activate + pre-commit run isort --all-files + displayName: 'Run isort' - job: 'Validate' pool: vmImage: 'ubuntu-latest' diff --git a/pylintrc b/pylintrc index 3235d583865a8b..0ffbb138f9e976 100644 --- a/pylintrc +++ b/pylintrc @@ -25,6 +25,7 @@ good-names=id,i,j,k,ex,Run,_,fp # unnecessary-pass - readability for functions which only contain pass # import-outside-toplevel - TODO # too-many-ancestors - it's too strict. +# wrong-import-order - isort guards this disable= format, abstract-class-little-used, @@ -49,7 +50,8 @@ disable= too-many-statements, too-many-boolean-expressions, unnecessary-pass, - unused-argument + unused-argument, + wrong-import-order enable= use-symbolic-message-instead diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 14b866c70349af..7a20962ff7cb07 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -4,4 +4,5 @@ bandit==1.6.2 black==19.10b0 flake8-docstrings==1.5.0 flake8==3.7.9 +isort==v4.3.21 pydocstyle==5.0.1 From 130571c478b08a06e566ab3c08965d2c784ceead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 12 Dec 2019 17:46:33 +0200 Subject: [PATCH 2402/3953] Remove no longer needed auth.util, use secrets instead (#29861) --- homeassistant/auth/models.py | 6 +++--- homeassistant/auth/util.py | 13 ------------- homeassistant/components/http/auth.py | 4 ++-- homeassistant/components/mobile_app/http_api.py | 6 +++--- homeassistant/components/owntracks/config_flow.py | 7 ++++--- homeassistant/components/rachio/__init__.py | 4 ++-- homeassistant/components/smartthings/smartapp.py | 3 ++- homeassistant/components/stream/__init__.py | 4 ++-- homeassistant/components/webhook/__init__.py | 4 ++-- homeassistant/helpers/config_entry_oauth2_flow.py | 4 ++-- tests/components/smartthings/conftest.py | 3 ++- 11 files changed, 24 insertions(+), 34 deletions(-) delete mode 100644 homeassistant/auth/util.py diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 6889d17a25fe6d..08f2f375b41161 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -1,5 +1,6 @@ """Auth models.""" from datetime import datetime, timedelta +import secrets from typing import Dict, List, NamedTuple, Optional import uuid @@ -9,7 +10,6 @@ from . import permissions as perm_mdl from .const import GROUP_ID_ADMIN -from .util import generate_secret TOKEN_TYPE_NORMAL = "normal" TOKEN_TYPE_SYSTEM = "system" @@ -96,8 +96,8 @@ class RefreshToken: ) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) created_at = attr.ib(type=datetime, factory=dt_util.utcnow) - token = attr.ib(type=str, factory=lambda: generate_secret(64)) - jwt_key = attr.ib(type=str, factory=lambda: generate_secret(64)) + token = attr.ib(type=str, factory=lambda: secrets.token_hex(64)) + jwt_key = attr.ib(type=str, factory=lambda: secrets.token_hex(64)) last_used_at = attr.ib(type=Optional[datetime], default=None) last_used_ip = attr.ib(type=Optional[str], default=None) diff --git a/homeassistant/auth/util.py b/homeassistant/auth/util.py deleted file mode 100644 index 83834fa7683968..00000000000000 --- a/homeassistant/auth/util.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Auth utils.""" -import binascii -import os - - -def generate_secret(entropy: int = 32) -> str: - """Generate a secret. - - Backport of secrets.token_hex from Python 3.6 - - Event loop friendly. - """ - return binascii.hexlify(os.urandom(entropy)).decode("ascii") diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 3866e770de0373..58814b77e2dc44 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,11 +1,11 @@ """Authentication for HTTP component.""" import logging +import secrets from aiohttp import hdrs from aiohttp.web import middleware import jwt -from homeassistant.auth.util import generate_secret from homeassistant.core import callback from homeassistant.util import dt as dt_util @@ -26,7 +26,7 @@ def async_sign_path(hass, refresh_token_id, path, expiration): secret = hass.data.get(DATA_SIGN_SECRET) if secret is None: - secret = hass.data[DATA_SIGN_SECRET] = generate_secret() + secret = hass.data[DATA_SIGN_SECRET] = secrets.token_hex() now = dt_util.utcnow() return "{}?{}={}".format( diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index ede18528f81f75..9581a3743847e6 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -1,11 +1,11 @@ """Provides an HTTP API for mobile_app.""" +import secrets from typing import Dict import uuid from aiohttp.web import Request, Response from nacl.secret import SecretBox -from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED @@ -34,7 +34,7 @@ async def post(self, request: Request, data: Dict) -> Response: """Handle the POST request for registration.""" hass = request.app["hass"] - webhook_id = generate_secret() + webhook_id = secrets.token_hex() if hass.components.cloud.async_active_subscription(): data[ @@ -46,7 +46,7 @@ async def post(self, request: Request, data: Dict) -> Response: data[CONF_WEBHOOK_ID] = webhook_id if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption(): - data[CONF_SECRET] = generate_secret(SecretBox.KEY_SIZE) + data[CONF_SECRET] = secrets.token_hex(SecretBox.KEY_SIZE) data[CONF_USER_ID] = request["hass_user"].id diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 82f89de6363742..0aba24217cc6fe 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -1,6 +1,7 @@ """Config flow for OwnTracks.""" +import secrets + from homeassistant import config_entries -from homeassistant.auth.util import generate_secret from homeassistant.const import CONF_WEBHOOK_ID from .const import DOMAIN # noqa pylint: disable=unused-import @@ -25,7 +26,7 @@ async def async_step_user(self, user_input=None): webhook_id, webhook_url, cloudhook = await self._get_webhook_id() - secret = generate_secret(16) + secret = secrets.token_hex(16) if supports_encryption(): secret_desc = f"The encryption key is {secret} (on Android under preferences -> advanced)" @@ -53,7 +54,7 @@ async def async_step_import(self, user_input): if self._async_current_entries(): return self.async_abort(reason="one_instance_allowed") webhook_id, _webhook_url, cloudhook = await self._get_webhook_id() - secret = generate_secret(16) + secret = secrets.token_hex(16) return self.async_create_entry( title="OwnTracks", data={ diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 4d67175ecd56d9..1b24f4e007169d 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -1,13 +1,13 @@ """Integration with the Rachio Iro sprinkler system controller.""" import asyncio import logging +import secrets from typing import Optional from aiohttp import web from rachiopy import Rachio import voluptuous as vol -from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API from homeassistant.helpers import config_validation as cv, discovery @@ -115,7 +115,7 @@ def setup(hass, config) -> bool: # Get the URL of this server custom_url = config[DOMAIN].get(CONF_CUSTOM_URL) hass_url = hass.config.api.base_url if custom_url is None else custom_url - rachio.webhook_auth = generate_secret() + rachio.webhook_auth = secrets.token_hex() rachio.webhook_url = hass_url + WEBHOOK_PATH # Get the API user diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 9b67df21491011..74feb93eec43c2 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -2,6 +2,7 @@ import asyncio import functools import logging +import secrets from urllib.parse import urlparse from uuid import uuid4 @@ -208,7 +209,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): # Create config config = { CONF_INSTANCE_ID: str(uuid4()), - CONF_WEBHOOK_ID: webhook.generate_secret(), + CONF_WEBHOOK_ID: secrets.token_hex(), CONF_CLOUDHOOK_URL: None, } await store.async_save(config) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 2bfd37a2641130..d88f90a83f89fc 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -1,10 +1,10 @@ """Provide functionality to stream video source.""" import logging +import secrets import threading import voluptuous as vol -from homeassistant.auth.util import generate_secret from homeassistant.const import CONF_FILENAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -72,7 +72,7 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N stream.add_provider(fmt) if not stream.access_token: - stream.access_token = generate_secret() + stream.access_token = secrets.token_hex() stream.start() return hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(stream.access_token) except Exception: diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index d51771d0b16778..217baf42f3ad49 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -1,10 +1,10 @@ """Webhooks for Home Assistant.""" import logging +import secrets from aiohttp.web import Request, Response import voluptuous as vol -from homeassistant.auth.util import generate_secret from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import callback @@ -46,7 +46,7 @@ def async_unregister(hass, webhook_id): @callback def async_generate_id(): """Generate a webhook_id.""" - return generate_secret(entropy=32) + return secrets.token_hex(32) @callback diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 4ea82f5f04749b..2fdfea8673fe7a 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -8,6 +8,7 @@ from abc import ABC, ABCMeta, abstractmethod import asyncio import logging +import secrets import time from typing import Any, Awaitable, Callable, Dict, Optional, cast @@ -18,7 +19,6 @@ from yarl import URL from homeassistant import config_entries -from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback @@ -441,7 +441,7 @@ def _encode_jwt(hass: HomeAssistant, data: dict) -> str: secret = hass.data.get(DATA_JWT_SECRET) if secret is None: - secret = hass.data[DATA_JWT_SECRET] = generate_secret() + secret = hass.data[DATA_JWT_SECRET] = secrets.token_hex() return jwt.encode(data, secret, algorithm="HS256").decode() diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index b3b172e7606a50..0dc71ea72b9feb 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -1,4 +1,5 @@ """Test configuration and mocks for the SmartThings component.""" +import secrets from uuid import uuid4 from asynctest import Mock, patch @@ -160,7 +161,7 @@ def installed_apps_fixture(installed_app, locations, app): @pytest.fixture(name="config_file") def config_file_fixture(): """Fixture representing the local config file contents.""" - return {CONF_INSTANCE_ID: str(uuid4()), CONF_WEBHOOK_ID: webhook.generate_secret()} + return {CONF_INSTANCE_ID: str(uuid4()), CONF_WEBHOOK_ID: secrets.token_hex()} @pytest.fixture(name="smartthings_mock") From 1fee400dcd94229db56a2ada5e3c335aa139146f Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 12 Dec 2019 11:10:43 -0500 Subject: [PATCH 2403/3953] =?UTF-8?q?Revert=20"Sort=20import=20for=20tests?= =?UTF-8?q?/components/feedreader/test=5Finit.=E2=80=A6=20(#29882)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 7d68e88d31246e268db809225e3d924acb1fc352. --- tests/components/feedreader/test_init.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index d74bbd79d804b1..bffbe9676fa9ff 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -1,5 +1,6 @@ """The tests for the feedreader component.""" from datetime import timedelta +from genericpath import exists from logging import getLogger from os import remove import time @@ -7,8 +8,6 @@ from unittest import mock from unittest.mock import patch -from genericpath import exists - from homeassistant.components import feedreader from homeassistant.components.feedreader import ( CONF_MAX_ENTRIES, From 327b5c3c94cb8e98bb31411edda6f60d47ee249a Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 12 Dec 2019 12:16:51 -0500 Subject: [PATCH 2404/3953] Log ZHA bind/unbind operations status (#29842) * Log bind/unbind operation result. * Use ZDO consts. * Use device logger for bind/unbind results. * Lint. --- homeassistant/components/zha/api.py | 50 ++++++++++++++++++----------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 462afd777b9c27..5f1b8e16512431 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -70,8 +70,6 @@ ATTR_IEEE = "ieee" ATTR_SOURCE_IEEE = "source_ieee" ATTR_TARGET_IEEE = "target_ieee" -BIND_REQUEST = 0x0021 -UNBIND_REQUEST = 0x0022 SERVICE_PERMIT = "permit" SERVICE_REMOVE = "remove" @@ -717,9 +715,11 @@ async def websocket_bind_devices(hass, connection, msg): zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_SOURCE_IEEE] target_ieee = msg[ATTR_TARGET_IEEE] - await async_binding_operation(zha_gateway, source_ieee, target_ieee, BIND_REQUEST) + await async_binding_operation( + zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Bind_req + ) _LOGGER.info( - "Issue bind devices: %s %s", + "Issued bind devices: %s %s", f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", f"{ATTR_TARGET_IEEE}: [{target_ieee}]", ) @@ -739,9 +739,11 @@ async def websocket_unbind_devices(hass, connection, msg): zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_SOURCE_IEEE] target_ieee = msg[ATTR_TARGET_IEEE] - await async_binding_operation(zha_gateway, source_ieee, target_ieee, UNBIND_REQUEST) + await async_binding_operation( + zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Unbind_req + ) _LOGGER.info( - "Issue unbind devices: %s %s", + "Issued unbind devices: %s %s", f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", f"{ATTR_TARGET_IEEE}: [{target_ieee}]", ) @@ -764,22 +766,34 @@ async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operati zdo = cluster_pair.source_cluster.endpoint.device.zdo - _LOGGER.debug( - "processing binding operation for: %s %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - f"{ATTR_TARGET_IEEE}: [{target_ieee}]", - "{}: {}".format("cluster", cluster_pair.source_cluster.cluster_id), + op_msg = "cluster: %s %s --> [%s]" + op_params = ( + cluster_pair.source_cluster.cluster_id, + operation.name, + target_ieee, ) + zdo.debug("processing " + op_msg, *op_params) + bind_tasks.append( - zdo.request( - operation, - source_device.ieee, - cluster_pair.source_cluster.endpoint.endpoint_id, - cluster_pair.source_cluster.cluster_id, - destination_address, + ( + zdo.request( + operation, + source_device.ieee, + cluster_pair.source_cluster.endpoint.endpoint_id, + cluster_pair.source_cluster.cluster_id, + destination_address, + ), + op_msg, + op_params, ) ) - await asyncio.gather(*bind_tasks) + res = await asyncio.gather(*(t[0] for t in bind_tasks), return_exceptions=True) + for outcome, log_msg in zip(res, bind_tasks): + if isinstance(outcome, Exception): + fmt = log_msg[1] + " failed: %s" + else: + fmt = log_msg[1] + " completed: %s" + zdo.debug(fmt, *(log_msg[2] + (outcome,))) def async_load_api(hass): From 7c42f4b45b62a0d7cbbcf97630b637047dd049ef Mon Sep 17 00:00:00 2001 From: John Luetke Date: Thu, 12 Dec 2019 10:43:49 -0800 Subject: [PATCH 2405/3953] (Re)Add support for multiple Pi-Holes (#27569) * Update configuration schema to support multiple Pi-holes * Construct sensors for each configured Pi-hole * Ensure each Pi-hole has a unique name * Update services to handle multiple Pi-holes * Update tests for multiple configurations * Refactor tests to support service testing * Fix else-raise per pyliunt * Per code review, add all entities in a single call * Per code review, add the default name as default. * Per code review, add cv.ensure_list to prevent breaking change * Per code review, move name validation to schema * Remove default name * Per code review, validate api_key in schema definition * Per code review, rename variables * Per code review, use list comprehension * Ensure unique slug names in config validation * Per code review, refactor to CoroutineMock * Fix adding sensor entities * Per code review, refactor mock function creation * Per code review, refactor mock function return values --- homeassistant/components/pi_hole/__init__.py | 212 +++++++++++++----- homeassistant/components/pi_hole/const.py | 6 +- homeassistant/components/pi_hole/sensor.py | 8 +- .../components/pi_hole/services.yaml | 11 +- tests/components/pi_hole/test_init.py | 127 ++++++++--- 5 files changed, 274 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 8ee21af7858c8f..ed6144af47e9ff 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -20,7 +20,7 @@ from .const import ( CONF_LOCATION, - DEFAULT_HOST, + CONF_SLUG, DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, @@ -29,82 +29,194 @@ MIN_TIME_BETWEEN_UPDATES, SERVICE_DISABLE, SERVICE_DISABLE_ATTR_DURATION, + SERVICE_DISABLE_ATTR_NAME, SERVICE_ENABLE, + SERVICE_ENABLE_ATTR_NAME, ) + +def ensure_unique_names_and_slugs(config): + """Ensure that each configuration dict contains a unique `name` value.""" + names = {} + slugs = {} + for conf in config: + if conf[CONF_NAME] not in names and conf[CONF_SLUG] not in slugs: + names[conf[CONF_NAME]] = conf[CONF_HOST] + slugs[conf[CONF_SLUG]] = conf[CONF_HOST] + else: + raise vol.Invalid( + "Duplicate name '{}' (or slug '{}') for '{}' (already in use by '{}'). Each configured Pi-hole must have a unique name.".format( + conf[CONF_NAME], + conf[CONF_SLUG], + conf[CONF_HOST], + names.get(conf[CONF_NAME], slugs[conf[CONF_SLUG]]), + ) + ) + return config + + +def coerce_slug(config): + """Coerce the name of the Pi-Hole into a slug.""" + config[CONF_SLUG] = cv.slugify(config[CONF_NAME]) + return config + + LOGGER = logging.getLogger(__name__) +PI_HOLE_SCHEMA = vol.Schema( + vol.All( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + }, + coerce_slug, + ) +) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - } + vol.All(cv.ensure_list, [PI_HOLE_SCHEMA], ensure_unique_names_and_slugs) ) }, extra=vol.ALLOW_EXTRA, ) -SERVICE_DISABLE_SCHEMA = vol.Schema( - { - vol.Required(SERVICE_DISABLE_ATTR_DURATION): vol.All( - cv.time_period_str, cv.positive_timedelta - ) - } -) - async def async_setup(hass, config): """Set up the pi_hole integration.""" - conf = config[DOMAIN] - name = conf[CONF_NAME] - host = conf[CONF_HOST] - use_tls = conf[CONF_SSL] - verify_tls = conf[CONF_VERIFY_SSL] - location = conf[CONF_LOCATION] - api_key = conf.get(CONF_API_KEY) - - LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) - - session = async_get_clientsession(hass, verify_tls) - pi_hole = PiHoleData( - Hole( - host, hass.loop, session, location=location, tls=use_tls, api_token=api_key - ), - name, + def get_data(): + """Retrive component data.""" + return hass.data[DOMAIN] + + def ensure_api_token(call_data): + """Ensure the Pi-Hole to be enabled/disabled has a api_token configured.""" + data = get_data() + if SERVICE_DISABLE_ATTR_NAME not in call_data: + for slug in data: + call_data[SERVICE_DISABLE_ATTR_NAME] = data[slug].name + ensure_api_token(call_data) + + call_data[SERVICE_DISABLE_ATTR_NAME] = None + else: + slug = cv.slugify(call_data[SERVICE_DISABLE_ATTR_NAME]) + + if (data[slug]).api.api_token is None: + raise vol.Invalid( + "Pi-hole '{}' must have an api_key provided in configuration to be enabled.".format( + pi_hole.name + ) + ) + + return call_data + + service_disable_schema = vol.Schema( # pylint: disable=invalid-name + vol.All( + { + vol.Required(SERVICE_DISABLE_ATTR_DURATION): vol.All( + cv.time_period_str, cv.positive_timedelta + ), + vol.Optional(SERVICE_DISABLE_ATTR_NAME): vol.In( + [conf[CONF_NAME] for conf in config[DOMAIN]], msg="Unknown Pi-Hole", + ), + }, + ensure_api_token, + ) ) - await pi_hole.async_update() - - hass.data[DOMAIN] = pi_hole - - async def handle_disable(call): - if api_key is None: - raise vol.Invalid("Pi-hole api_key must be provided in configuration") + service_enable_schema = vol.Schema( + { + vol.Optional(SERVICE_ENABLE_ATTR_NAME): vol.In( + [conf[CONF_NAME] for conf in config[DOMAIN]], msg="Unknown Pi-Hole" + ) + } + ) - duration = call.data[SERVICE_DISABLE_ATTR_DURATION].total_seconds() + hass.data[DOMAIN] = {} + + for conf in config[DOMAIN]: + name = conf[CONF_NAME] + slug = conf[CONF_SLUG] + host = conf[CONF_HOST] + use_tls = conf[CONF_SSL] + verify_tls = conf[CONF_VERIFY_SSL] + location = conf[CONF_LOCATION] + api_key = conf.get(CONF_API_KEY) + + LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) + + session = async_get_clientsession(hass, verify_tls) + pi_hole = PiHoleData( + Hole( + host, + hass.loop, + session, + location=location, + tls=use_tls, + api_token=api_key, + ), + name, + ) - LOGGER.debug("Disabling %s %s for %d seconds", DOMAIN, host, duration) - await pi_hole.api.disable(duration) + await pi_hole.async_update() - async def handle_enable(call): - if api_key is None: - raise vol.Invalid("Pi-hole api_key must be provided in configuration") + hass.data[DOMAIN][slug] = pi_hole - LOGGER.debug("Enabling %s %s", DOMAIN, host) - await pi_hole.api.enable() + async def disable_service_handler(call): + """Handle the service call to disable a single Pi-Hole or all configured Pi-Holes.""" + duration = call.data[SERVICE_DISABLE_ATTR_DURATION].total_seconds() + name = call.data.get(SERVICE_DISABLE_ATTR_NAME) + + async def do_disable(name): + """Disable the named Pi-Hole.""" + slug = cv.slugify(name) + pi_hole = hass.data[DOMAIN][slug] + + LOGGER.debug( + "Disabling Pi-hole '%s' (%s) for %d seconds", + name, + pi_hole.api.host, + duration, + ) + await pi_hole.api.disable(duration) + + if name is not None: + await do_disable(name) + else: + for pi_hole in hass.data[DOMAIN].values(): + await do_disable(pi_hole.name) + + async def enable_service_handler(call): + """Handle the service call to enable a single Pi-Hole or all configured Pi-Holes.""" + + name = call.data.get(SERVICE_ENABLE_ATTR_NAME) + + async def do_enable(name): + """Enable the named Pi-Hole.""" + slug = cv.slugify(name) + pi_hole = hass.data[DOMAIN][slug] + + LOGGER.debug("Enabling Pi-hole '%s' (%s)", name, pi_hole.api.host) + await pi_hole.api.enable() + + if name is not None: + await do_enable(name) + else: + for pi_hole in hass.data[DOMAIN].values(): + await do_enable(pi_hole.name) hass.services.async_register( - DOMAIN, SERVICE_DISABLE, handle_disable, schema=SERVICE_DISABLE_SCHEMA + DOMAIN, SERVICE_DISABLE, disable_service_handler, schema=service_disable_schema ) - hass.services.async_register(DOMAIN, SERVICE_ENABLE, handle_enable) + hass.services.async_register( + DOMAIN, SERVICE_ENABLE, enable_service_handler, schema=service_enable_schema + ) hass.async_create_task(async_load_platform(hass, SENSOR_DOMAIN, DOMAIN, {}, config)) diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 5422054795097e..0ae62b318650b6 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -4,8 +4,8 @@ DOMAIN = "pi_hole" CONF_LOCATION = "location" +CONF_SLUG = "slug" -DEFAULT_HOST = "pi.hole" DEFAULT_LOCATION = "admin" DEFAULT_METHOD = "GET" DEFAULT_NAME = "Pi-Hole" @@ -13,8 +13,10 @@ DEFAULT_VERIFY_SSL = True SERVICE_DISABLE = "disable" -SERVICE_ENABLE = "enable" SERVICE_DISABLE_ATTR_DURATION = "duration" +SERVICE_DISABLE_ATTR_NAME = "name" +SERVICE_ENABLE = "enable" +SERVICE_ENABLE_ATTR_NAME = SERVICE_DISABLE_ATTR_NAME ATTR_BLOCKED_DOMAINS = "domains_blocked" diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index f76e756aec49f3..c01a0167e53d4d 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -18,10 +18,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return - pi_hole = hass.data[PIHOLE_DOMAIN] - sensors = [] - sensors = [PiHoleSensor(pi_hole, sensor_name) for sensor_name in SENSOR_LIST] + for pi_hole in hass.data[PIHOLE_DOMAIN].values(): + for sensor in [ + PiHoleSensor(pi_hole, sensor_name) for sensor_name in SENSOR_LIST + ]: + sensors.append(sensor) async_add_entities(sensors, True) diff --git a/homeassistant/components/pi_hole/services.yaml b/homeassistant/components/pi_hole/services.yaml index b16ed21a5d3c2b..e3cc8624e368f8 100644 --- a/homeassistant/components/pi_hole/services.yaml +++ b/homeassistant/components/pi_hole/services.yaml @@ -1,8 +1,15 @@ disable: - description: Disable Pi-hole for an amount of time + description: Disable configured Pi-hole(s) for an amount of time fields: duration: description: Time that the Pi-hole should be disabled for example: "00:00:15" + name: + description: "[Optional] When multiple Pi-holes are configured, the name of the one to disable. If omitted, all configured Pi-holes will be disabled." + example: "Pi-Hole" enable: - description: Enable Pi-hole \ No newline at end of file + description: Enable configured Pi-hole(s) + fields: + name: + description: "[Optional] When multiple Pi-holes are configured, the name of the one to enable. If omitted, all configured Pi-holes will be enabled." + example: "Pi-Hole" \ No newline at end of file diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index b2f4b3f28aff18..c2d9ec77f03e1b 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -3,39 +3,34 @@ from unittest.mock import patch from asynctest import CoroutineMock -from hole import Hole from homeassistant.components import pi_hole from tests.common import async_setup_component +ZERO_DATA = { + "ads_blocked_today": 0, + "ads_percentage_today": 0, + "clients_ever_seen": 0, + "dns_queries_today": 0, + "domains_being_blocked": 0, + "queries_cached": 0, + "queries_forwarded": 0, + "status": 0, + "unique_clients": 0, + "unique_domains": 0, +} -def mock_pihole_data_call(Hole): - """Need to override so as to allow mocked data.""" - Hole.__init__ = ( - lambda self, host, loop, session, location, tls, verify_tls=True, api_token=None: None - ) - Hole.data = { - "ads_blocked_today": 0, - "ads_percentage_today": 0, - "clients_ever_seen": 0, - "dns_queries_today": 0, - "domains_being_blocked": 0, - "queries_cached": 0, - "queries_forwarded": 0, - "status": 0, - "unique_clients": 0, - "unique_domains": 0, - } - pass - - -async def test_setup_no_config(hass): - """Tests component setup with no config.""" - with patch.object( - Hole, "get_data", new=CoroutineMock(side_effect=mock_pihole_data_call(Hole)) - ): - assert await async_setup_component(hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: {}}) + +async def test_setup_minimal_config(hass): + """Tests component setup with minimal config.""" + with patch("homeassistant.components.pi_hole.Hole") as _hole: + _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.data = ZERO_DATA + + assert await async_setup_component( + hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: [{"host": "pi.hole"}]} + ) await hass.async_block_till_done() @@ -84,13 +79,16 @@ async def test_setup_no_config(hass): assert hass.states.get("sensor.pi_hole_seen_clients").state == "0" -async def test_setup_custom_config(hass): - """Tests component setup with custom config.""" - with patch.object( - Hole, "get_data", new=CoroutineMock(side_effect=mock_pihole_data_call(Hole)) - ): +async def test_setup_name_config(hass): + """Tests component setup with a custom name.""" + with patch("homeassistant.components.pi_hole.Hole") as _hole: + _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.data = ZERO_DATA + assert await async_setup_component( - hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: {"name": "Custom"}} + hass, + pi_hole.DOMAIN, + {pi_hole.DOMAIN: [{"host": "pi.hole", "name": "Custom"}]}, ) await hass.async_block_till_done() @@ -99,3 +97,66 @@ async def test_setup_custom_config(hass): hass.states.get("sensor.custom_ads_blocked_today").name == "Custom Ads Blocked Today" ) + + +async def test_disable_service_call(hass): + """Test disable service call with no Pi-hole named.""" + with patch("homeassistant.components.pi_hole.Hole") as _hole: + mock_disable = CoroutineMock(return_value=None) + _hole.return_value.disable = mock_disable + _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.data = ZERO_DATA + + assert await async_setup_component( + hass, + pi_hole.DOMAIN, + { + pi_hole.DOMAIN: [ + {"host": "pi.hole", "api_key": "1"}, + {"host": "pi.hole", "name": "Custom", "api_key": "2"}, + ] + }, + ) + + await hass.async_block_till_done() + + await hass.services.async_call( + pi_hole.DOMAIN, + pi_hole.SERVICE_DISABLE, + {pi_hole.SERVICE_DISABLE_ATTR_DURATION: "00:00:01"}, + blocking=True, + ) + + await hass.async_block_till_done() + + assert mock_disable.call_count == 2 + + +async def test_enable_service_call(hass): + """Test enable service call with no Pi-hole named.""" + with patch("homeassistant.components.pi_hole.Hole") as _hole: + mock_enable = CoroutineMock(return_value=None) + _hole.return_value.enable = mock_enable + _hole.return_value.get_data = CoroutineMock(return_value=None) + _hole.return_value.data = ZERO_DATA + + assert await async_setup_component( + hass, + pi_hole.DOMAIN, + { + pi_hole.DOMAIN: [ + {"host": "pi.hole", "api_key": "1"}, + {"host": "pi.hole", "name": "Custom", "api_key": "2"}, + ] + }, + ) + + await hass.async_block_till_done() + + await hass.services.async_call( + pi_hole.DOMAIN, pi_hole.SERVICE_ENABLE, {}, blocking=True + ) + + await hass.async_block_till_done() + + assert mock_enable.call_count == 2 From 9d662d61149f4ddac251c0ddcd97a70fab4925b4 Mon Sep 17 00:00:00 2001 From: randellhodges Date: Thu, 12 Dec 2019 12:47:38 -0600 Subject: [PATCH 2406/3953] ISY994 Node Filter Update (#28155) * Adding filters for keypad dimmers and smoke sensor * ran black --fast as suggested * Adding filters for keypad dimmers and smoke sensor * ran black --fast as suggested --- homeassistant/components/isy994/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 96796e37a6a393..6c5a668c51aeb7 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -62,7 +62,7 @@ "binary_sensor": { "uom": [], "states": [], - "node_def_id": ["BinaryAlarm"], + "node_def_id": ["BinaryAlarm", "BinaryAlarm_ADV"], "insteon_type": ["16."], # Does a startswith() match; include the dot }, "sensor": { @@ -112,6 +112,8 @@ "BallastRelayLampSwitch_ADV", "RemoteLinc2", "RemoteLinc2_ADV", + "KeypadDimmer", + "KeypadDimmer_ADV", ], "insteon_type": ["1."], }, From 7685c76b9b49408dea07c947c2eb54fe1f01f92f Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 12 Dec 2019 14:16:02 -0500 Subject: [PATCH 2407/3953] Defer log formatting. (#29888) --- homeassistant/components/zha/api.py | 185 ++++++++++++++++++---------- 1 file changed, 117 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 5f1b8e16512431..d9c1db2ae5d652 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -565,11 +565,15 @@ async def websocket_device_cluster_attributes(hass, connection, msg): {ID: attr_id, ATTR_NAME: attributes[attr_id][0]} ) _LOGGER.debug( - "Requested attributes for: %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{RESPONSE}: [{cluster_attributes}]", + "Requested attributes for: %s: %s, %s: '%s', %s: %s, %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + RESPONSE, + cluster_attributes, ) connection.send_result(msg[ID], cluster_attributes) @@ -619,11 +623,15 @@ async def websocket_device_cluster_commands(hass, connection, msg): } ) _LOGGER.debug( - "Requested commands for: %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{RESPONSE}: [{cluster_commands}]", + "Requested commands for: %s: %s, %s: '%s', %s: %s, %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + RESPONSE, + cluster_commands, ) connection.send_result(msg[ID], cluster_commands) @@ -663,14 +671,21 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): [attribute], allow_cache=False, only_cache=False, manufacturer=manufacturer ) _LOGGER.debug( - "Read attribute for: %s %s %s %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{ATTR_ATTRIBUTE}: [{attribute}]", - f"{ATTR_MANUFACTURER}: [{manufacturer}]", - "{}: [{}]".format(RESPONSE, str(success.get(attribute))), - "{}: [{}]".format("failure", failure), + "Read attribute for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s],", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + ATTR_ATTRIBUTE, + attribute, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + str(success.get(attribute)), + "failure", + failure, ) connection.send_result(msg[ID], str(success.get(attribute))) @@ -693,9 +708,11 @@ async def websocket_get_bindable_devices(hass, connection, msg): ] _LOGGER.debug( - "Get bindable devices: %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - "{}: [{}]".format("bindable devices:", devices), + "Get bindable devices: %s: [%s], %s: [%s]", + ATTR_SOURCE_IEEE, + source_ieee, + "bindable devices", + devices, ) connection.send_message(websocket_api.result_message(msg[ID], devices)) @@ -719,9 +736,11 @@ async def websocket_bind_devices(hass, connection, msg): zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Bind_req ) _LOGGER.info( - "Issued bind devices: %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - f"{ATTR_TARGET_IEEE}: [{target_ieee}]", + "Devices bound: %s: [%s] %s: [%s]", + ATTR_SOURCE_IEEE, + source_ieee, + ATTR_TARGET_IEEE, + target_ieee, ) @@ -743,9 +762,11 @@ async def websocket_unbind_devices(hass, connection, msg): zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Unbind_req ) _LOGGER.info( - "Issued unbind devices: %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - f"{ATTR_TARGET_IEEE}: [{target_ieee}]", + "Devices un-bound: %s: [%s] %s: [%s]", + ATTR_SOURCE_IEEE, + source_ieee, + ATTR_TARGET_IEEE, + target_ieee, ) @@ -848,14 +869,21 @@ async def set_zigbee_cluster_attributes(service): manufacturer=manufacturer, ) _LOGGER.debug( - "Set attribute for: %s %s %s %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{ATTR_ATTRIBUTE}: [{attribute}]", - f"{ATTR_VALUE}: [{value}]", - f"{ATTR_MANUFACTURER}: [{manufacturer}]", - f"{RESPONSE}: [{response}]", + "Set attribute for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s]", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + ATTR_ATTRIBUTE, + attribute, + ATTR_VALUE, + value, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + response, ) hass.helpers.service.async_register_admin_service( @@ -890,15 +918,23 @@ async def issue_zigbee_cluster_command(service): manufacturer=manufacturer, ) _LOGGER.debug( - "Issue command for: %s %s %s %s %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{ATTR_COMMAND}: [{command}]", - f"{ATTR_COMMAND_TYPE}: [{command_type}]", - f"{ATTR_ARGS}: [{args}]", - f"{ATTR_MANUFACTURER}: [{manufacturer}]", - f"{RESPONSE}: [{response}]", + "Issued command for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: %s %s: [%s] %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + ATTR_COMMAND, + command, + ATTR_COMMAND_TYPE, + command_type, + ATTR_ARGS, + args, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + response, ) hass.helpers.service.async_register_admin_service( @@ -925,12 +961,17 @@ async def issue_zigbee_group_command(service): command, *args, manufacturer=manufacturer, expect_reply=True ) _LOGGER.debug( - "Issue group command for: %s %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_COMMAND}: [{command}]", - f"{ATTR_ARGS}: [{args}]", - f"{ATTR_MANUFACTURER}: [{manufacturer}]", - f"{RESPONSE}: [{response}]", + "Issued group command for: %s: [%s] %s: [%s] %s: %s %s: [%s] %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_COMMAND, + command, + ATTR_ARGS, + args, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + response, ) hass.helpers.service.async_register_admin_service( @@ -954,20 +995,24 @@ async def warning_device_squawk(service): await channel.squawk(mode, strobe, level) else: _LOGGER.error( - "Squawking IASWD: %s is missing the required IASWD channel!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Squawking IASWD: %s: [%s] is missing the required IASWD channel!", + ATTR_IEEE, + str(ieee), ) else: _LOGGER.error( - "Squawking IASWD: %s could not be found!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Squawking IASWD: %s: [%s] could not be found!", ATTR_IEEE, str(ieee) ) _LOGGER.debug( - "Squawking IASWD: %s %s %s %s", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), - "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), - "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), - "{}: [{}]".format(ATTR_LEVEL, level), + "Squawking IASWD: %s: [%s] %s: [%s] %s: [%s] %s: [%s]", + ATTR_IEEE, + str(ieee), + ATTR_WARNING_DEVICE_MODE, + mode, + ATTR_WARNING_DEVICE_STROBE, + strobe, + ATTR_LEVEL, + level, ) hass.helpers.service.async_register_admin_service( @@ -996,20 +1041,24 @@ async def warning_device_warn(service): ) else: _LOGGER.error( - "Warning IASWD: %s is missing the required IASWD channel!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Warning IASWD: %s: [%s] is missing the required IASWD channel!", + ATTR_IEEE, + str(ieee), ) else: _LOGGER.error( - "Warning IASWD: %s could not be found!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Warning IASWD: %s: [%s] could not be found!", ATTR_IEEE, str(ieee) ) _LOGGER.debug( - "Warning IASWD: %s %s %s %s", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), - "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), - "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), - "{}: [{}]".format(ATTR_LEVEL, level), + "Warning IASWD: %s: [%s] %s: [%s] %s: [%s] %s: [%s]", + ATTR_IEEE, + str(ieee), + ATTR_WARNING_DEVICE_MODE, + mode, + ATTR_WARNING_DEVICE_STROBE, + strobe, + ATTR_LEVEL, + level, ) hass.helpers.service.async_register_admin_service( From c051ae0bfbce437c3e3f8ae1ae519d9343661c4f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 12 Dec 2019 22:39:11 +0100 Subject: [PATCH 2408/3953] Bump python-miio version to 0.4.8 (#29890) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 849e4573bbf85c..4f2e752feb188c 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": [ "construct==2.9.45", - "python-miio==0.4.7" + "python-miio==0.4.8" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index fb7b193ababc79..0bf32b8d23ca96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1579,7 +1579,7 @@ python-juicenet==0.1.6 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.4.7 +python-miio==0.4.8 # homeassistant.components.mpd python-mpd2==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9716cd2f950475..bb02be746dea20 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -522,7 +522,7 @@ python-forecastio==1.4.0 python-izone==1.1.1 # homeassistant.components.xiaomi_miio -python-miio==0.4.7 +python-miio==0.4.8 # homeassistant.components.nest python-nest==4.1.0 From 5b32ee566c87c2becc063e67b54daea56d992f75 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 13 Dec 2019 00:32:17 +0000 Subject: [PATCH 2409/3953] [ci skip] Translation update --- homeassistant/components/soma/.translations/fr.json | 4 +++- homeassistant/components/soma/.translations/ru.json | 6 ++++-- homeassistant/components/soma/.translations/zh-Hant.json | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json index a758ab0f615fe7..0889cdea2ec8f1 100644 --- a/homeassistant/components/soma/.translations/fr.json +++ b/homeassistant/components/soma/.translations/fr.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Soma.", "authorize_url_timeout": "D\u00e9lai d'attente g\u00e9n\u00e9rant l'autorisation de l'URL.", - "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation." + "connection_error": "Impossible de se connecter \u00e0 SOMA Connect.", + "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation.", + "result_error": "SOMA Connect a r\u00e9pondu avec l'\u00e9tat d'erreur." }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json index f28d672d3f28c3..fa581eb0821de7 100644 --- a/homeassistant/components/soma/.translations/ru.json +++ b/homeassistant/components/soma/.translations/ru.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "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.", - "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a SOMA Connect.", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", + "result_error": "SOMA Connect \u043e\u0442\u0432\u0435\u0442\u0438\u043b \u0441\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c \u043e\u0448\u0438\u0431\u043a\u0438." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." @@ -15,7 +17,7 @@ "port": "\u041f\u043e\u0440\u0442" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a SOMA Connect.", - "title": "Soma" + "title": "SOMA Connect" } }, "title": "Soma" diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json index 893abe82ee1919..73b26cb91f1ab8 100644 --- a/homeassistant/components/soma/.translations/zh-Hant.json +++ b/homeassistant/components/soma/.translations/zh-Hant.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Soma \u5e33\u865f\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", - "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "connection_error": "SOMA \u9023\u7dda\u5931\u6557\u3002", + "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "result_error": "SOMA \u9023\u7dda\u56de\u61c9\u72c0\u614b\u932f\u8aa4\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" From c59bf0bff6eb9f5edfba4272f6b5f80b1634167f Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 13 Dec 2019 09:47:09 +0100 Subject: [PATCH 2410/3953] `genericpath` is an internal Python module and shouldn't be imported according to core Python devs. (see [this](https://bugs.python.org/msg358136) comment) (#29903) For a reason unknown to me, @exxamalte introduced this in https://github.com/home-assistant/home-assistant/pull/14342. The problem is that Linux and macOS implement `os.path` differently, one imports from [`ntpath.py`](https://github.com/python/cpython/blob/master/Lib/ntpath.py) and the other one from [`posixpath.py`](https://github.com/python/cpython/blob/master/Lib/posixpath.py), and both these files use `genericpath.py`. Somehow, `isort` on macOS will see `genericpath` as a third party library and sort it accordingly. Other Unix-based OSes will correctly treat `genericpath` as an internal library. This problem led to a sorting sequence in the following commits: - ca0fad2cbb0544125d87d21ffe308cf3addcde5a - f5d4878992d63683d3da661ef02ae7b51421beb4 - 7d68e88d31246e268db809225e3d924acb1fc352 - 1fee400dcd94229db56a2ada5e3c335aa139146f This supersedes https://github.com/home-assistant/home-assistant/pull/29893. --- tests/components/feedreader/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index bffbe9676fa9ff..62412e53900974 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -1,8 +1,8 @@ """The tests for the feedreader component.""" from datetime import timedelta -from genericpath import exists from logging import getLogger from os import remove +from os.path import exists import time import unittest from unittest import mock From 9fbb6d981a6c20c47a0d47d9e5d563c6526688eb Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 13 Dec 2019 10:31:53 +0100 Subject: [PATCH 2411/3953] Fix setup for tank_utility component (#29902) --- homeassistant/components/tank_utility/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 6848b263633f1e..23446257eab143 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -4,7 +4,7 @@ import logging import requests -import tank_utility +from tank_utility import auth, device as tank_monitor import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = config.get(CONF_DEVICES) try: - token = tank_utility.auth.get_token(email, password) + token = auth.get_token(email, password) except requests.exceptions.HTTPError as http_error: if ( http_error.response.status_code @@ -111,7 +111,7 @@ def get_data(self): data = {} try: - data = tank_utility.device.get_device_data(self._token, self.device) + data = tank_monitor.get_device_data(self._token, self.device) except requests.exceptions.HTTPError as http_error: if ( http_error.response.status_code @@ -120,10 +120,8 @@ def get_data(self): == requests.codes.bad_request # pylint: disable=no-member ): _LOGGER.info("Getting new token") - self._token = tank_utility.auth.get_token( - self._email, self._password, force=True - ) - data = tank_utility.device.get_device_data(self._token, self.device) + self._token = auth.get_token(self._email, self._password, force=True) + data = tank_monitor.get_device_data(self._token, self.device) else: raise http_error data.update(data.pop("device", {})) From 4d57de335c8b088b4baf17d89fbfb2d78c9c4639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 13 Dec 2019 11:39:57 +0200 Subject: [PATCH 2412/3953] Make Python deprecation notice easier to maintain (#29900) --- homeassistant/bootstrap.py | 17 ++++++++++++----- homeassistant/const.py | 3 +++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 79de0b2545398f..12fbc6f232f0ed 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -11,7 +11,11 @@ import voluptuous as vol from homeassistant import config as conf_util, config_entries, core, loader -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE +from homeassistant.const import ( + EVENT_HOMEASSISTANT_CLOSE, + REQUIRED_NEXT_PYTHON_DATE, + REQUIRED_NEXT_PYTHON_VER, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util.logging import AsyncHandler @@ -95,11 +99,14 @@ async def async_from_config_dict( stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) - if sys.version_info[:3] < (3, 7, 0): + if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER: msg = ( - "Python 3.6 support is deprecated and will " - "be removed in the first release after December 15, 2019. Please " - "upgrade Python to 3.7.0 or higher." + "Support for the running Python version " + f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will " + f"be removed in the first release after {REQUIRED_NEXT_PYTHON_DATE}. " + "Please upgrade Python to " + f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or " + "higher." ) _LOGGER.warning(msg) hass.components.persistent_notification.async_create( diff --git a/homeassistant/const.py b/homeassistant/const.py index dc937f81482c6f..ba7e7a66126946 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,6 +5,9 @@ __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) +# Truthy date string triggers showing related deprecation warning messages. +REQUIRED_NEXT_PYTHON_VER = (3, 7, 0) +REQUIRED_NEXT_PYTHON_DATE = "December 15, 2019" # Format for platform files PLATFORM_FORMAT = "{platform}.{domain}" From 1301a4fcc671e458fb2c6c8274ac593ce079905d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 13 Dec 2019 11:15:26 +0100 Subject: [PATCH 2413/3953] Upgrade Sphinx to 2.2.2 and sphinx-autodoc-typehintsi to 1.10.3 (#29906) --- requirements_docs.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index b3dd4616f49509..55f0f2d162d5f5 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==2.1.2 -sphinx-autodoc-typehints==1.6.0 +Sphinx==2.2.2 +sphinx-autodoc-typehints==1.10.3 sphinx-autodoc-annotation==1.0.post1 \ No newline at end of file From b91a8f510cc8e7f2f5e940ce93231e3e046feee6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Dec 2019 12:29:24 +0100 Subject: [PATCH 2414/3953] Fix incorrect file format yr test fixure (#29910) --- tests/components/yr/test_sensor.py | 6 +++--- tests/fixtures/{yr.no.json => yr.no.xml} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename tests/fixtures/{yr.no.json => yr.no.xml} (100%) diff --git a/tests/components/yr/test_sensor.py b/tests/components/yr/test_sensor.py index 1e8282a58ffaae..21ce0bbe7cebfe 100644 --- a/tests/components/yr/test_sensor.py +++ b/tests/components/yr/test_sensor.py @@ -16,7 +16,7 @@ def test_default_setup(hass, aioclient_mock): """Test the default setup.""" aioclient_mock.get( "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", - text=load_fixture("yr.no.json"), + text=load_fixture("yr.no.xml"), ) config = {"platform": "yr", "elevation": 0} hass.allow_pool = True @@ -36,7 +36,7 @@ def test_custom_setup(hass, aioclient_mock): """Test a custom setup.""" aioclient_mock.get( "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", - text=load_fixture("yr.no.json"), + text=load_fixture("yr.no.xml"), ) config = { @@ -82,7 +82,7 @@ def test_forecast_setup(hass, aioclient_mock): """Test a custom setup with 24h forecast.""" aioclient_mock.get( "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", - text=load_fixture("yr.no.json"), + text=load_fixture("yr.no.xml"), ) config = { diff --git a/tests/fixtures/yr.no.json b/tests/fixtures/yr.no.xml similarity index 100% rename from tests/fixtures/yr.no.json rename to tests/fixtures/yr.no.xml From c8cc8acc81104056d0978259af9e550c06b1581f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Dec 2019 12:41:56 +0100 Subject: [PATCH 2415/3953] Fixes invalid JSON syntax in devcontainer (#29911) --- .devcontainer/devcontainer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5bfd37fab36ae4..7b56b66d0b59c9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,3 @@ -// See https://aka.ms/vscode-remote/devcontainer.json for format details. { "name": "Home Assistant Dev", "context": "..", From 6b3260357fe08d830087d5ed3691cfc4211a6ab2 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 13 Dec 2019 14:08:30 +0100 Subject: [PATCH 2416/3953] Fix setup error for logbook (#29908) * Fix setup error by moving an import back into the setup function * Revert c741664d4da76611b5bb7502dda61a24dce22c61 * Add homekit as after_dependency to logbook manifest.json --- homeassistant/components/logbook/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index 08083ea4024579..9d5c78dc34d5a4 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/logbook", "requirements": [], "dependencies": ["frontend", "http", "recorder"], + "after_dependencies": ["homekit"], "codeowners": [] } From a470a72ec592e58cdbb6b0c00cd76ce2d59371e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Dec 2019 15:38:41 +0100 Subject: [PATCH 2417/3953] Add integration platform helper (#29914) --- homeassistant/components/intent/__init__.py | 34 +++----------- homeassistant/helpers/integration_platform.py | 46 +++++++++++++++++++ tests/helpers/test_integration_platform.py | 37 +++++++++++++++ 3 files changed, 90 insertions(+), 27 deletions(-) create mode 100644 homeassistant/helpers/integration_platform.py create mode 100644 tests/helpers/test_integration_platform.py diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 53960851f6cc41..bdf612b2e831ff 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -1,16 +1,12 @@ """The Intent integration.""" -import asyncio import logging import voluptuous as vol from homeassistant.components import http from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, intent -from homeassistant.loader import IntegrationNotFound, async_get_integration -from homeassistant.setup import ATTR_COMPONENT +from homeassistant.helpers import config_validation as cv, integration_platform, intent from .const import DOMAIN @@ -22,32 +18,16 @@ async def async_setup(hass: HomeAssistant, config: dict): """Set up the Intent component.""" hass.http.register_view(IntentHandleView()) - tasks = [_async_process_intent(hass, comp) for comp in hass.config.components] - - async def async_component_loaded(event): - """Handle a new component loaded.""" - await _async_process_intent(hass, event.data[ATTR_COMPONENT]) - - hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_component_loaded) - - if tasks: - await asyncio.gather(*tasks) + await integration_platform.async_process_integration_platforms( + hass, DOMAIN, _async_process_intent + ) return True -async def _async_process_intent(hass: HomeAssistant, component_name: str): - """Process the intents of a component.""" - try: - integration = await async_get_integration(hass, component_name) - platform = integration.get_platform(DOMAIN) - except (IntegrationNotFound, ImportError): - return - - try: - await platform.async_setup_intents(hass) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error setting up intents for %s", component_name) +async def _async_process_intent(hass: HomeAssistant, domain: str, platform): + """Process the intents of an integration.""" + await platform.async_setup_intents(hass) class IntentHandleView(http.HomeAssistantView): diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py new file mode 100644 index 00000000000000..01567c72c7b63e --- /dev/null +++ b/homeassistant/helpers/integration_platform.py @@ -0,0 +1,46 @@ +"""Helpers to help with integration platforms.""" +import asyncio +import logging +from typing import Any, Awaitable, Callable + +from homeassistant.core import Event, HomeAssistant +from homeassistant.loader import IntegrationNotFound, async_get_integration, bind_hass +from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED + +_LOGGER = logging.getLogger(__name__) + + +@bind_hass +async def async_process_integration_platforms( + hass: HomeAssistant, + platform_name: str, + # Any = platform. + process_platform: Callable[[HomeAssistant, str, Any], Awaitable[None]], +) -> None: + """Process a specific platform for all current and future loaded integrations.""" + + async def _process(component_name: str) -> None: + """Process the intents of a component.""" + try: + integration = await async_get_integration(hass, component_name) + platform = integration.get_platform(platform_name) + except (IntegrationNotFound, ImportError): + return + + try: + await process_platform(hass, component_name, platform) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error processing platform %s.%s", component_name, platform_name + ) + + async def async_component_loaded(event: Event) -> None: + """Handle a new component loaded.""" + await _process(event.data[ATTR_COMPONENT]) + + hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_component_loaded) + + tasks = [_process(comp) for comp in hass.config.components] + + if tasks: + await asyncio.gather(*tasks) diff --git a/tests/helpers/test_integration_platform.py b/tests/helpers/test_integration_platform.py new file mode 100644 index 00000000000000..d6c844c0d914eb --- /dev/null +++ b/tests/helpers/test_integration_platform.py @@ -0,0 +1,37 @@ +"""Test integration platform helpers.""" +from unittest.mock import Mock + +from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED + +from tests.common import mock_platform + + +async def test_process_integration_platforms(hass): + """Test processing integrations.""" + loaded_platform = Mock() + mock_platform(hass, "loaded.platform_to_check", loaded_platform) + hass.config.components.add("loaded") + + event_platform = Mock() + mock_platform(hass, "event.platform_to_check", event_platform) + + processed = [] + + async def _process_platform(hass, domain, platform): + """Process platform.""" + processed.append((domain, platform)) + + await hass.helpers.integration_platform.async_process_integration_platforms( + "platform_to_check", _process_platform + ) + + assert len(processed) == 1 + assert processed[0][0] == "loaded" + assert processed[0][1] == loaded_platform + + hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "event"}) + await hass.async_block_till_done() + + assert len(processed) == 2 + assert processed[1][0] == "event" + assert processed[1][1] == event_platform From 8f5a00a98bddc6a42424c5a72dc40cc4f33f7e26 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 14 Dec 2019 00:32:08 +0000 Subject: [PATCH 2418/3953] [ci skip] Translation update --- .../components/icloud/.translations/it.json | 38 +++++++++++++++++++ .../components/soma/.translations/it.json | 4 +- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/icloud/.translations/it.json diff --git a/homeassistant/components/icloud/.translations/it.json b/homeassistant/components/icloud/.translations/it.json new file mode 100644 index 00000000000000..0a986f1fe7793b --- /dev/null +++ b/homeassistant/components/icloud/.translations/it.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Account gi\u00e0 configurato" + }, + "error": { + "login": "Errore di accesso: si prega di controllare la tua e-mail e la password", + "send_verification_code": "Impossibile inviare il codice di verifica", + "username_exists": "Account gi\u00e0 configurato", + "validate_verification_code": "Impossibile verificare il codice di verifica, scegliere un dispositivo attendibile e riavviare la verifica" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositivo attendibile" + }, + "description": "Selezionare il dispositivo attendibile", + "title": "Dispositivo attendibile iCloud" + }, + "user": { + "data": { + "password": "Password", + "username": "E-mail" + }, + "description": "Inserisci le tue credenziali", + "title": "Credenziali iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Codice di verifica" + }, + "description": "Inserisci il codice di verifica che hai appena ricevuto da iCloud", + "title": "Codice di verifica iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/it.json b/homeassistant/components/soma/.translations/it.json index 1398b2a66bee79..6c7d0129708fcc 100644 --- a/homeassistant/components/soma/.translations/it.json +++ b/homeassistant/components/soma/.translations/it.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "\u00c8 possibile configurare un solo account Soma.", "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", - "missing_configuration": "Il componente Soma non \u00e8 configurato. Si prega di seguire la documentazione." + "connection_error": "Impossibile connettersi a SOMA Connect.", + "missing_configuration": "Il componente Soma non \u00e8 configurato. Si prega di seguire la documentazione.", + "result_error": "SOMA Connect ha risposto con stato di errore." }, "create_entry": { "default": "Autenticato con successo con Soma." From 114390c95e2a18745659e712e9576ffdd75c84fd Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Sat, 14 Dec 2019 01:36:12 -0500 Subject: [PATCH 2419/3953] Fix mobile app device identifiers (#29920) Fix identifiers when updating device registration. --- homeassistant/components/mobile_app/webhook.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 46f17b401bcdbf..c2bc6c9411272f 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -190,10 +190,7 @@ async def handle_webhook( device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - identifiers={ - (ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]), - (CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID]), - }, + identifiers={(DOMAIN, registration[ATTR_DEVICE_ID])}, manufacturer=new_registration[ATTR_MANUFACTURER], model=new_registration[ATTR_MODEL], name=new_registration[ATTR_DEVICE_NAME], From 2cb92c66ef3c18eb5a8bfa7744263fcf68b27ad2 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 14 Dec 2019 07:36:33 +0100 Subject: [PATCH 2420/3953] Support entity_id: all in lifx.set_state (#29919) --- homeassistant/components/lifx/light.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 1fb614f856fb5f..aa63be04f0d3f0 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -35,7 +35,12 @@ Light, preprocess_turn_on_alternatives, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_MODE, + ENTITY_MATCH_ALL, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr @@ -369,6 +374,9 @@ async def start_effect(self, entities, service, **kwargs): async def async_service_to_entities(self, service): """Return the known entities that a service call mentions.""" + if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL: + return self.entities.values() + entity_ids = await async_extract_entity_ids(self.hass, service) return [ entity From 3c86825e253192bdd71871a265c6882ef611d51d Mon Sep 17 00:00:00 2001 From: DjMoren Date: Sat, 14 Dec 2019 08:00:19 +0100 Subject: [PATCH 2421/3953] Update Tahoma component's tahoma-api requirement's version (#29918) --- homeassistant/components/tahoma/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tahoma/manifest.json b/homeassistant/components/tahoma/manifest.json index 1e99d4b288d725..4efe9aed97f7f3 100644 --- a/homeassistant/components/tahoma/manifest.json +++ b/homeassistant/components/tahoma/manifest.json @@ -3,7 +3,7 @@ "name": "Tahoma", "documentation": "https://www.home-assistant.io/integrations/tahoma", "requirements": [ - "tahoma-api==0.0.14" + "tahoma-api==0.0.16" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0bf32b8d23ca96..46c1b0f2ade603 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1907,7 +1907,7 @@ swisshydrodata==0.0.3 synology-srm==0.0.7 # homeassistant.components.tahoma -tahoma-api==0.0.14 +tahoma-api==0.0.16 # homeassistant.components.tank_utility tank_utility==1.4.0 From 3db7e8f5e9218a5721e8cc46e32e8e9969b6ce2e Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Sat, 14 Dec 2019 02:47:45 -0500 Subject: [PATCH 2422/3953] Implement Alexa.EventDetectionSensor for Alexa (#28276) * Implement Alexa.EventDetectionSensor Interface * Removed references to PR #28218 not yet merged into dev. * Update tests to include Alexa Interface * Guard for `unknown` and `unavailible` states. * Fixed Unnecessary "elif" after "return" --- .../components/alexa/capabilities.py | 59 +++++++++++++- homeassistant/components/alexa/entities.py | 37 ++++++++- tests/components/alexa/__init__.py | 1 + tests/components/alexa/test_capabilities.py | 42 ++++++++++ tests/components/alexa/test_smart_home.py | 81 +++++++++++++++++++ 5 files changed, 217 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 9f217d2e9c9586..b5ffb1ef7e6442 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,7 +1,7 @@ """Alexa capabilities.""" import logging -from homeassistant.components import cover, fan, light +from homeassistant.components import cover, fan, image_processing, light from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER import homeassistant.components.climate.const as climate import homeassistant.components.media_player.const as media_player @@ -1265,3 +1265,60 @@ class AlexaSeekController(AlexaCapability): def name(self): """Return the Alexa API name of this interface.""" return "Alexa.SeekController" + + +class AlexaEventDetectionSensor(AlexaCapability): + """Implements Alexa.EventDetectionSensor. + + https://developer.amazon.com/docs/device-apis/alexa-eventdetectionsensor.html + """ + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.EventDetectionSensor" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "humanPresenceDetectionState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "humanPresenceDetectionState": + raise UnsupportedProperty(name) + + human_presence = "NOT_DETECTED" + state = self.entity.state + + # Return None for unavailable and unknown states. + # Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport. + if state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + return None + + if self.entity.domain == image_processing.DOMAIN: + if int(state): + human_presence = "DETECTED" + elif state == STATE_ON: + human_presence = "DETECTED" + + return {"value": human_presence} + + def configuration(self): + """Return supported detection types.""" + return { + "detectionMethods": ["AUDIO", "VIDEO"], + "detectionModes": { + "humanPresence": { + "featureAvailability": "ENABLED", + "supportsNotDetected": True, + } + }, + } diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 733ea73f9980a7..017686df60771a 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -9,6 +9,7 @@ cover, fan, group, + image_processing, input_boolean, light, lock, @@ -40,6 +41,7 @@ AlexaContactSensor, AlexaDoorbellEventSource, AlexaEndpointHealth, + AlexaEventDetectionSensor, AlexaInputController, AlexaLockController, AlexaModeController, @@ -522,6 +524,7 @@ class BinarySensorCapabilities(AlexaEntity): TYPE_CONTACT = "contact" TYPE_MOTION = "motion" + TYPE_PRESENCE = "presence" def default_display_categories(self): """Return the display categories for this entity.""" @@ -530,6 +533,8 @@ def default_display_categories(self): return [DisplayCategory.CONTACT_SENSOR] if sensor_type is self.TYPE_MOTION: return [DisplayCategory.MOTION_SENSOR] + if sensor_type is self.TYPE_PRESENCE: + return [DisplayCategory.CAMERA] def interfaces(self): """Yield the supported interfaces.""" @@ -538,7 +543,10 @@ def interfaces(self): yield AlexaContactSensor(self.hass, self.entity) elif sensor_type is self.TYPE_MOTION: yield AlexaMotionSensor(self.hass, self.entity) + elif sensor_type is self.TYPE_PRESENCE: + yield AlexaEventDetectionSensor(self.hass, self.entity) + # yield additional interfaces based on specified display category in config. entity_conf = self.config.entity_config.get(self.entity.entity_id, {}) if CONF_DISPLAY_CATEGORIES in entity_conf: if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL: @@ -547,6 +555,8 @@ def interfaces(self): yield AlexaContactSensor(self.hass, self.entity) elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.MOTION_SENSOR: yield AlexaMotionSensor(self.hass, self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CAMERA: + yield AlexaEventDetectionSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) @@ -554,11 +564,20 @@ def interfaces(self): def get_type(self): """Return the type of binary sensor.""" attrs = self.entity.attributes - if attrs.get(ATTR_DEVICE_CLASS) in ("door", "garage_door", "opening", "window"): + if attrs.get(ATTR_DEVICE_CLASS) in ( + binary_sensor.DEVICE_CLASS_DOOR, + binary_sensor.DEVICE_CLASS_GARAGE_DOOR, + binary_sensor.DEVICE_CLASS_OPENING, + binary_sensor.DEVICE_CLASS_WINDOW, + ): return self.TYPE_CONTACT - if attrs.get(ATTR_DEVICE_CLASS) == "motion": + + if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_MOTION: return self.TYPE_MOTION + if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_PRESENCE: + return self.TYPE_PRESENCE + @ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN) class AlarmControlPanelCapabilities(AlexaEntity): @@ -574,3 +593,17 @@ def interfaces(self): yield AlexaSecurityPanelController(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(image_processing.DOMAIN) +class ImageProcessingCapabilities(AlexaEntity): + """Class to represent image_processing capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.CAMERA] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaEventDetectionSensor(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 79fdb86c3efdcd..473a3c6e12b736 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -17,6 +17,7 @@ class MockConfig(config.AbstractConfig): "binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}, "binary_sensor.test_contact_forced": {"display_categories": "CONTACT_SENSOR"}, "binary_sensor.test_motion_forced": {"display_categories": "MOTION_SENSOR"}, + "binary_sensor.test_motion_camera_event": {"display_categories": "CAMERA"}, } @property diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 952af543aabfc4..ab9c375103a9ab 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -667,3 +667,45 @@ async def test_report_playback_state(hass): properties.assert_equal( "Alexa.PlaybackStateReporter", "playbackState", {"state": "STOPPED"} ) + + +async def test_report_image_processing(hass): + """Test EventDetectionSensor implements humanPresenceDetectionState property.""" + hass.states.async_set( + "image_processing.test_face", + 0, + { + "friendly_name": "Test face", + "device_class": "face", + "faces": [], + "total_faces": 0, + }, + ) + + properties = await reported_properties(hass, "image_processing#test_face") + properties.assert_equal( + "Alexa.EventDetectionSensor", + "humanPresenceDetectionState", + {"value": "NOT_DETECTED"}, + ) + + hass.states.async_set( + "image_processing.test_classifier", + 3, + { + "friendly_name": "Test classifier", + "device_class": "face", + "faces": [ + {"confidence": 98.34, "name": "Hans", "age": 16.0, "gender": "male"}, + {"name": "Helena", "age": 28.0, "gender": "female"}, + {"confidence": 62.53, "name": "Luna"}, + ], + "total_faces": 3, + }, + ) + properties = await reported_properties(hass, "image_processing#test_classifier") + properties.assert_equal( + "Alexa.EventDetectionSensor", + "humanPresenceDetectionState", + {"value": "DETECTED"}, + ) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index ea1e6f50fcf9a8..25c8f2a864fe22 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2386,3 +2386,84 @@ async def test_cover_position(hass): assert properties["name"] == "mode" assert properties["namespace"] == "Alexa.ModeController" assert properties["value"] == "position.open" + + +async def test_image_processing(hass): + """Test image_processing discovery as event detection.""" + device = ( + "image_processing.test_face", + 0, + { + "friendly_name": "Test face", + "device_class": "face", + "faces": [], + "total_faces": 0, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "image_processing#test_face" + assert appliance["displayCategories"][0] == "CAMERA" + assert appliance["friendlyName"] == "Test face" + + assert_endpoint_capabilities( + appliance, "Alexa.EventDetectionSensor", "Alexa.EndpointHealth" + ) + + +async def test_motion_sensor_event_detection(hass): + """Test motion sensor with EventDetectionSensor discovery.""" + device = ( + "binary_sensor.test_motion_camera_event", + "off", + {"friendly_name": "Test motion camera event", "device_class": "motion"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_motion_camera_event" + assert appliance["displayCategories"][0] == "CAMERA" + assert appliance["friendlyName"] == "Test motion camera event" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa", + "Alexa.MotionSensor", + "Alexa.EventDetectionSensor", + "Alexa.EndpointHealth", + ) + + event_detection_capability = get_capability( + capabilities, "Alexa.EventDetectionSensor" + ) + assert event_detection_capability is not None + properties = event_detection_capability["properties"] + assert properties["proactivelyReported"] is True + assert not properties["retrievable"] + assert {"name": "humanPresenceDetectionState"} in properties["supported"] + + +async def test_presence_sensor(hass): + """Test presence sensor.""" + device = ( + "binary_sensor.test_presence_sensor", + "off", + {"friendly_name": "Test presence sensor", "device_class": "presence"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_presence_sensor" + assert appliance["displayCategories"][0] == "CAMERA" + assert appliance["friendlyName"] == "Test presence sensor" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa", "Alexa.EventDetectionSensor", "Alexa.EndpointHealth" + ) + + event_detection_capability = get_capability( + capabilities, "Alexa.EventDetectionSensor" + ) + assert event_detection_capability is not None + properties = event_detection_capability["properties"] + assert properties["proactivelyReported"] is True + assert not properties["retrievable"] + assert {"name": "humanPresenceDetectionState"} in properties["supported"] From 003658a3f084c5be5e4959f69c57e526423a7ef3 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 14 Dec 2019 10:54:41 -0500 Subject: [PATCH 2423/3953] Update androidtv version to improve source selection support (#29579) * Change androidtv module versions and add support for select_source for all device types * Update and add tests * Update requirements_test_all.txt * Update requirements_all.txt * Consolidate tests * Fix typo * Remove 'self._device' --- .../components/androidtv/manifest.json | 4 +- .../components/androidtv/media_player.py | 91 ++++----- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/androidtv/patchers.py | 21 ++- .../components/androidtv/test_media_player.py | 176 +++++++++++++++--- 6 files changed, 224 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 8b68f089617b3d..47768cbd4dc4f6 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.8", - "androidtv==0.0.34", + "adb-shell==0.0.9", + "androidtv==0.0.35", "pure-python-adb==0.2.2.dev0" ], "dependencies": [], diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 8b7f18802649a7..a1fb4cea9cd8c8 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -55,6 +55,7 @@ | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + | SUPPORT_SELECT_SOURCE | SUPPORT_STOP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP @@ -199,6 +200,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): aftv, config[CONF_NAME], config[CONF_APPS], + config[CONF_GET_SOURCES], config.get(CONF_TURN_ON_COMMAND), config.get(CONF_TURN_OFF_COMMAND), ) @@ -287,7 +289,9 @@ def _adb_exception_catcher(self, *args, **kwargs): class ADBDevice(MediaPlayerDevice): """Representation of an Android TV or Fire TV device.""" - def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): + def __init__( + self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + ): """Initialize the Android TV / Fire TV device.""" self.aftv = aftv self._name = name @@ -296,6 +300,7 @@ def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): self._app_name_to_id = { value: key for key, value in self._app_id_to_name.items() } + self._get_sources = get_sources self._keys = KEYS self._device_properties = self.aftv.device_properties @@ -325,6 +330,7 @@ def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): self._adb_response = None self._available = True self._current_app = None + self._sources = None self._state = None @property @@ -357,6 +363,16 @@ def should_poll(self): """Device should be polled.""" return True + @property + def source(self): + """Return the current app.""" + return self._app_id_to_name.get(self._current_app, self._current_app) + + @property + def source_list(self): + """Return a list of running apps.""" + return self._sources + @property def state(self): """Return the state of the player.""" @@ -408,6 +424,20 @@ def media_next_track(self): """Send next track command (results in fast-forward).""" self.aftv.media_next_track() + @adb_decorator() + def select_source(self, source): + """Select input source. + + If the source starts with a '!', then it will close the app instead of + opening it. + """ + if isinstance(source, str): + if not source.startswith("!"): + self.aftv.launch_app(self._app_name_to_id.get(source, source)) + else: + source_ = source[1:].lstrip() + self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) + @adb_decorator() def adb_command(self, cmd): """Send an ADB command to an Android TV / Fire TV device.""" @@ -436,11 +466,14 @@ def adb_command(self, cmd): class AndroidTVDevice(ADBDevice): """Representation of an Android TV device.""" - def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): + def __init__( + self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + ): """Initialize the Android TV device.""" - super().__init__(aftv, name, apps, turn_on_command, turn_off_command) + super().__init__( + aftv, name, apps, get_sources, turn_on_command, turn_off_command + ) - self._device = None self._is_volume_muted = None self._volume_level = None @@ -465,25 +498,28 @@ def update(self): ( state, self._current_app, - self._device, + running_apps, + _, self._is_volume_muted, self._volume_level, - ) = self.aftv.update() + ) = self.aftv.update(self._get_sources) self._state = ANDROIDTV_STATES.get(state) if self._state is None: self._available = False + if running_apps: + self._sources = [ + self._app_id_to_name.get(app_id, app_id) for app_id in running_apps + ] + else: + self._sources = None + @property def is_volume_muted(self): """Boolean if volume is currently muted.""" return self._is_volume_muted - @property - def source(self): - """Return the current playback device.""" - return self._device - @property def supported_features(self): """Flag media player features that are supported.""" @@ -518,15 +554,6 @@ def volume_up(self): class FireTVDevice(ADBDevice): """Representation of a Fire TV device.""" - def __init__( - self, aftv, name, apps, get_sources, turn_on_command, turn_off_command - ): - """Initialize the Fire TV device.""" - super().__init__(aftv, name, apps, turn_on_command, turn_off_command) - - self._get_sources = get_sources - self._sources = None - @adb_decorator(override_available=True) def update(self): """Update the device state and, if necessary, re-connect.""" @@ -558,16 +585,6 @@ def update(self): else: self._sources = None - @property - def source(self): - """Return the current app.""" - return self._app_id_to_name.get(self._current_app, self._current_app) - - @property - def source_list(self): - """Return a list of running apps.""" - return self._sources - @property def supported_features(self): """Flag media player features that are supported.""" @@ -577,17 +594,3 @@ def supported_features(self): def media_stop(self): """Send stop (back) command.""" self.aftv.back() - - @adb_decorator() - def select_source(self, source): - """Select input source. - - If the source starts with a '!', then it will close the app instead of - opening it. - """ - if isinstance(source, str): - if not source.startswith("!"): - self.aftv.launch_app(self._app_name_to_id.get(source, source)) - else: - source_ = source[1:].lstrip() - self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) diff --git a/requirements_all.txt b/requirements_all.txt index 46c1b0f2ade603..f93db416543f68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,7 +115,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.8 +adb-shell==0.0.9 # homeassistant.components.adguard adguardhome==0.3.0 @@ -215,7 +215,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.34 +androidtv==0.0.35 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb02be746dea20..812af1a2b13061 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -29,7 +29,7 @@ YesssSMS==0.4.1 abodepy==0.16.7 # homeassistant.components.androidtv -adb-shell==0.0.8 +adb-shell==0.0.9 # homeassistant.components.adguard adguardhome==0.3.0 @@ -84,7 +84,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.34 +androidtv==0.0.35 # homeassistant.components.apns apns2==0.3.0 diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 0549ad995e18e7..bd05cab2a7484f 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -149,5 +149,22 @@ def patch_firetv_update(state, current_app, running_apps): ) -PATCH_LAUNCH_APP = patch("androidtv.firetv.FireTV.launch_app") -PATCH_STOP_APP = patch("androidtv.firetv.FireTV.stop_app") +def patch_androidtv_update( + state, current_app, running_apps, device, is_volume_muted, volume_level +): + """Patch the `AndroidTV.update()` method.""" + return patch( + "androidtv.androidtv.AndroidTV.update", + return_value=( + state, + current_app, + running_apps, + device, + is_volume_muted, + volume_level, + ), + ) + + +PATCH_LAUNCH_APP = patch("androidtv.basetv.BaseTV.launch_app") +PATCH_STOP_APP = patch("androidtv.basetv.BaseTV.stop_app") diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index d7a6a8c1ce682d..860b8738607e18 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -33,6 +33,7 @@ CONF_PLATFORM: ANDROIDTV_DOMAIN, CONF_HOST: "127.0.0.1", CONF_NAME: "Android TV", + CONF_DEVICE_CLASS: "androidtv", } } @@ -42,6 +43,7 @@ CONF_PLATFORM: ANDROIDTV_DOMAIN, CONF_HOST: "127.0.0.1", CONF_NAME: "Android TV", + CONF_DEVICE_CLASS: "androidtv", CONF_ADB_SERVER_IP: "127.0.0.1", } } @@ -284,9 +286,9 @@ async def test_setup_with_adbkey(hass): assert state.state == STATE_OFF -async def test_firetv_sources(hass): - """Test that sources (i.e., apps) are handled correctly for Fire TV devices.""" - config = CONFIG_FIRETV_ADB_SERVER.copy() +async def _test_sources(hass, config0): + """Test that sources (i.e., apps) are handled correctly for Android TV and Fire TV devices.""" + config = config0.copy() config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} patch_key, entity_id = _setup(hass, config) @@ -299,9 +301,21 @@ async def test_firetv_sources(hass): assert state is not None assert state.state == STATE_OFF - with patchers.patch_firetv_update( - "playing", "com.app.test1", ["com.app.test1", "com.app.test2"] - ): + if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + patch_update = patchers.patch_androidtv_update( + "playing", + "com.app.test1", + ["com.app.test1", "com.app.test2"], + "hdmi", + False, + 1, + ) + else: + patch_update = patchers.patch_firetv_update( + "playing", "com.app.test1", ["com.app.test1", "com.app.test2"] + ) + + with patch_update: await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -309,9 +323,21 @@ async def test_firetv_sources(hass): assert state.attributes["source"] == "TEST 1" assert state.attributes["source_list"] == ["TEST 1", "com.app.test2"] - with patchers.patch_firetv_update( - "playing", "com.app.test2", ["com.app.test2", "com.app.test1"] - ): + if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": + patch_update = patchers.patch_androidtv_update( + "playing", + "com.app.test2", + ["com.app.test2", "com.app.test1"], + "hdmi", + True, + 0, + ) + else: + patch_update = patchers.patch_firetv_update( + "playing", "com.app.test2", ["com.app.test2", "com.app.test1"] + ) + + with patch_update: await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -319,10 +345,22 @@ async def test_firetv_sources(hass): assert state.attributes["source"] == "com.app.test2" assert state.attributes["source_list"] == ["com.app.test2", "TEST 1"] + return True + -async def _test_firetv_select_source(hass, source, expected_arg, method_patch): - """Test that the `FireTV.launch_app` and `FireTV.stop_app` methods are called with the right parameter.""" - config = CONFIG_FIRETV_ADB_SERVER.copy() +async def test_androidtv_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Android TV devices.""" + assert await _test_sources(hass, CONFIG_ANDROIDTV_ADB_SERVER) + + +async def test_firetv_sources(hass): + """Test that sources (i.e., apps) are handled correctly for Fire TV devices.""" + assert await _test_sources(hass, CONFIG_FIRETV_ADB_SERVER) + + +async def _test_select_source(hass, config0, source, expected_arg, method_patch): + """Test that the methods for launching and stopping apps are called correctly when selecting a source.""" + config = config0.copy() config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} patch_key, entity_id = _setup(hass, config) @@ -347,43 +385,133 @@ async def _test_firetv_select_source(hass, source, expected_arg, method_patch): return True +async def test_androidtv_select_source_launch_app_id(hass): + """Test that an app can be launched using its app ID.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "com.app.test1", + "com.app.test1", + patchers.PATCH_LAUNCH_APP, + ) + + +async def test_androidtv_select_source_launch_app_name(hass): + """Test that an app can be launched using its friendly name.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "TEST 1", + "com.app.test1", + patchers.PATCH_LAUNCH_APP, + ) + + +async def test_androidtv_select_source_launch_app_id_no_name(hass): + """Test that an app can be launched using its app ID when it has no friendly name.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "com.app.test2", + "com.app.test2", + patchers.PATCH_LAUNCH_APP, + ) + + +async def test_androidtv_select_source_stop_app_id(hass): + """Test that an app can be stopped using its app ID.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "!com.app.test1", + "com.app.test1", + patchers.PATCH_STOP_APP, + ) + + +async def test_androidtv_select_source_stop_app_name(hass): + """Test that an app can be stopped using its friendly name.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "!TEST 1", + "com.app.test1", + patchers.PATCH_STOP_APP, + ) + + +async def test_androidtv_select_source_stop_app_id_no_name(hass): + """Test that an app can be stopped using its app ID when it has no friendly name.""" + assert await _test_select_source( + hass, + CONFIG_ANDROIDTV_ADB_SERVER, + "!com.app.test2", + "com.app.test2", + patchers.PATCH_STOP_APP, + ) + + async def test_firetv_select_source_launch_app_id(hass): """Test that an app can be launched using its app ID.""" - assert await _test_firetv_select_source( - hass, "com.app.test1", "com.app.test1", patchers.PATCH_LAUNCH_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "com.app.test1", + "com.app.test1", + patchers.PATCH_LAUNCH_APP, ) async def test_firetv_select_source_launch_app_name(hass): """Test that an app can be launched using its friendly name.""" - assert await _test_firetv_select_source( - hass, "TEST 1", "com.app.test1", patchers.PATCH_LAUNCH_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "TEST 1", + "com.app.test1", + patchers.PATCH_LAUNCH_APP, ) async def test_firetv_select_source_launch_app_id_no_name(hass): """Test that an app can be launched using its app ID when it has no friendly name.""" - assert await _test_firetv_select_source( - hass, "com.app.test2", "com.app.test2", patchers.PATCH_LAUNCH_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "com.app.test2", + "com.app.test2", + patchers.PATCH_LAUNCH_APP, ) async def test_firetv_select_source_stop_app_id(hass): """Test that an app can be stopped using its app ID.""" - assert await _test_firetv_select_source( - hass, "!com.app.test1", "com.app.test1", patchers.PATCH_STOP_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "!com.app.test1", + "com.app.test1", + patchers.PATCH_STOP_APP, ) async def test_firetv_select_source_stop_app_name(hass): """Test that an app can be stopped using its friendly name.""" - assert await _test_firetv_select_source( - hass, "!TEST 1", "com.app.test1", patchers.PATCH_STOP_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "!TEST 1", + "com.app.test1", + patchers.PATCH_STOP_APP, ) async def test_firetv_select_source_stop_app_id_no_name(hass): """Test that an app can be stopped using its app ID when it has no friendly name.""" - assert await _test_firetv_select_source( - hass, "!com.app.test2", "com.app.test2", patchers.PATCH_STOP_APP + assert await _test_select_source( + hass, + CONFIG_FIRETV_ADB_SERVER, + "!com.app.test2", + "com.app.test2", + patchers.PATCH_STOP_APP, ) From 820780996a30768c33d94d0218892ac27d222f10 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 14 Dec 2019 23:06:00 +0100 Subject: [PATCH 2424/3953] Add battery sensor to iCloud (#29818) * Add battery sensor to iCloud * Update .coveragerc * Review: @balloob & @MartinHjelmare * Review: use f string --- .coveragerc | 1 + homeassistant/components/icloud/__init__.py | 5 -- homeassistant/components/icloud/const.py | 3 +- .../components/icloud/device_tracker.py | 30 ++++--- homeassistant/components/icloud/sensor.py | 85 +++++++++++++++++++ 5 files changed, 104 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/icloud/sensor.py diff --git a/.coveragerc b/.coveragerc index f4794b593816f1..c6e7182d326d88 100644 --- a/.coveragerc +++ b/.coveragerc @@ -321,6 +321,7 @@ omit = homeassistant/components/iaqualink/switch.py homeassistant/components/icloud/__init__.py homeassistant/components/icloud/device_tracker.py + homeassistant/components/icloud/sensor.py homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py homeassistant/components/izone/__init__.py diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 2012f69193803b..c59f4098951ad3 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -560,11 +560,6 @@ def unique_id(self) -> str: """Return a unique ID.""" return self._device_id - @property - def dev_id(self) -> str: - """Return the device ID.""" - return self._device_id - @property def name(self) -> str: """Return the Apple device name.""" diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index 4e99a378077c9a..ed2fc78fe6d254 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -14,8 +14,7 @@ STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -# Next PR will add sensor -ICLOUD_COMPONENTS = ["device_tracker"] +ICLOUD_COMPONENTS = ["device_tracker", "sensor"] # pyicloud.AppleDevice status DEVICE_BATTERY_LEVEL = "batteryLevel" diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 4be34728c6dfde..511ce7f94470a2 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,8 +1,10 @@ """Support for tracking for iCloud devices.""" import logging +from typing import Dict from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_USERNAME from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType @@ -26,13 +28,15 @@ async def async_setup_scanner( pass -async def async_setup_entry(hass: HomeAssistantType, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +): """Configure a dispatcher connection based on a config entry.""" username = entry.data[CONF_USERNAME] for device in hass.data[DOMAIN][username].devices.values(): if device.location is None: - _LOGGER.debug("No position found for device %s", device.name) + _LOGGER.debug("No position found for %s", device.name) continue _LOGGER.debug("Adding device_tracker for %s", device.name) @@ -49,12 +53,12 @@ def __init__(self, device: IcloudDevice): self._unsub_dispatcher = None @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" - return f"{self._device.unique_id}_tracker" + return self._device.unique_id @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._device.name @@ -74,36 +78,36 @@ def longitude(self): return self._device.location[DEVICE_LOCATION_LONGITUDE] @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed.""" return False @property - def battery_level(self): + def battery_level(self) -> int: """Return the battery level of the device.""" return self._device.battery_level @property - def source_type(self): + def source_type(self) -> str: """Return the source type, eg gps or router, of the device.""" return SOURCE_TYPE_GPS @property - def icon(self): + def icon(self) -> str: """Return the icon.""" return icon_for_icloud_device(self._device) @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, any]: """Return the device state attributes.""" return self._device.state_attributes @property - def device_info(self): + def device_info(self) -> Dict[str, any]: """Return the device information.""" return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": self.name, + "identifiers": {(DOMAIN, self._device.unique_id)}, + "name": self._device.name, "manufacturer": "Apple", "model": self._device.device_model, } diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py new file mode 100644 index 00000000000000..4351d4ffa1983c --- /dev/null +++ b/homeassistant/components/icloud/sensor.py @@ -0,0 +1,85 @@ +"""Support for iCloud sensors.""" +import logging +from typing import Dict + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_USERNAME, DEVICE_CLASS_BATTERY +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.helpers.typing import HomeAssistantType + +from . import IcloudDevice +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up iCloud devices sensors based on a config entry.""" + username = entry.data[CONF_USERNAME] + + entities = [] + for device in hass.data[DOMAIN][username].devices.values(): + if device.battery_level is not None: + _LOGGER.debug("Adding battery sensor for %s", device.name) + entities.append(IcloudDeviceBatterySensor(device)) + + async_add_entities(entities, True) + + +class IcloudDeviceBatterySensor(Entity): + """Representation of a iCloud device battery sensor.""" + + def __init__(self, device: IcloudDevice): + """Initialize the battery sensor.""" + self._device = device + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._device.unique_id}_battery" + + @property + def name(self) -> str: + """Sensor name.""" + return f"{self._device.name} battery state" + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def state(self) -> int: + """Battery state percentage.""" + return self._device.battery_level + + @property + def unit_of_measurement(self) -> str: + """Battery state measured in percentage.""" + return "%" + + @property + def icon(self) -> str: + """Battery state icon handling.""" + return icon_for_battery_level( + battery_level=self._device.battery_level, + charging=self._device.battery_status == "Charging", + ) + + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return default attributes for the iCloud device entity.""" + return self._device.state_attributes + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._device.unique_id)}, + "name": self._device.name, + "manufacturer": "Apple", + "model": self._device.device_model, + } From 6dd496deb49b9d3a2a3cc35309479dabdf24807b Mon Sep 17 00:00:00 2001 From: Chris Mandich Date: Sat, 14 Dec 2019 18:45:29 -0800 Subject: [PATCH 2425/3953] Fix loading flume integration (#29926) * Fix https://github.com/home-assistant/home-assistant/issues/29853 * Run script.gen_requirements * Update to store Token File in config directory * Update to store Token File in config directory * Update to store Token File in config directory --- homeassistant/components/flume/manifest.json | 4 ++-- homeassistant/components/flume/sensor.py | 6 +++++- requirements_all.txt | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index 800751e80ef2ea..6a9fb7a1fd8175 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -3,9 +3,9 @@ "name": "Flume", "documentation": "https://www.home-assistant.io/integrations/flume/", "requirements": [ - "pyflume==0.2.1" + "pyflume==0.2.4" ], "dependencies": [], "codeowners": ["@ChrisMandich"] } - \ No newline at end of file + diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index 5fee408e0dcc43..e96ce0d96ef209 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -37,11 +37,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config[CONF_PASSWORD] client_id = config[CONF_CLIENT_ID] client_secret = config[CONF_CLIENT_SECRET] + flume_token_file = hass.config.path("FLUME_TOKEN_FILE") time_zone = str(hass.config.time_zone) name = config[CONF_NAME] flume_entity_list = [] - flume_devices = FlumeDeviceList(username, password, client_id, client_secret) + flume_devices = FlumeDeviceList( + username, password, client_id, client_secret, flume_token_file + ) for device in flume_devices.device_list: if device["type"] == FLUME_TYPE_SENSOR: @@ -53,6 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device["id"], time_zone, SCAN_INTERVAL, + flume_token_file, ) flume_entity_list.append(FlumeSensor(flume, f"{name} {device['id']}")) diff --git a/requirements_all.txt b/requirements_all.txt index f93db416543f68..84b67a09c18308 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1234,7 +1234,7 @@ pyflexit==0.3 pyflic-homeassistant==0.4.dev0 # homeassistant.components.flume -pyflume==0.2.1 +pyflume==0.2.4 # homeassistant.components.flunearyou pyflunearyou==1.0.3 From a28545b69bdbc91a807bf8821785b852fa0e97c7 Mon Sep 17 00:00:00 2001 From: Tyler <8901422+tyler-public@users.noreply.github.com> Date: Sun, 15 Dec 2019 07:16:20 +0000 Subject: [PATCH 2426/3953] bump venstar 0.12 (#29954) * bump venstar 0.12 * Update manifest.json --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index e8e36d04467ce9..78243844a93617 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "documentation": "https://www.home-assistant.io/integrations/venstar", "requirements": [ - "venstarcolortouch==0.9" + "venstarcolortouch==0.12" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 84b67a09c18308..2f6247ec18ec16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1988,7 +1988,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.2.0 # homeassistant.components.venstar -venstarcolortouch==0.9 +venstarcolortouch==0.12 # homeassistant.components.meteo_france vigilancemeteo==3.0.0 From 8a5bce81c8f7f3f9d0b2d67564860498bdcab14a Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 15 Dec 2019 02:31:59 -0800 Subject: [PATCH 2427/3953] Bump adb-shell to 0.1.0 and androidtv to 0.0.36 (#29938) * Bump adb-shell to 0.1.0 and androidtv to 0.0.36 * Add test for setting up two devices * Add test_setup_same_device_twice * Fix test_setup_two_devices * Fix coverage * Coverage * Fix flaky 'test_setup_two_devices' * Another stab at coverage * Rename 'address' back to 'host' --- .../components/androidtv/manifest.json | 4 +- .../components/androidtv/media_player.py | 9 ++- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/androidtv/patchers.py | 28 +++---- .../components/androidtv/test_media_player.py | 75 +++++++++++++++++-- 6 files changed, 96 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 47768cbd4dc4f6..39e5bfb2cdf53d 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.9", - "androidtv==0.0.35", + "adb-shell==0.1.0", + "androidtv==0.0.36", "pure-python-adb==0.2.2.dev0" ], "dependencies": [], diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index a1fb4cea9cd8c8..15acd594bee40e 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -146,7 +146,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" aftv = setup( - host, + config[CONF_HOST], + config[CONF_PORT], adbkey, device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], @@ -159,7 +160,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) aftv = setup( - host, + config[CONF_HOST], + config[CONF_PORT], config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], @@ -171,7 +173,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" aftv = setup( - host, + config[CONF_HOST], + config[CONF_PORT], adb_server_ip=config[CONF_ADB_SERVER_IP], adb_server_port=config[CONF_ADB_SERVER_PORT], device_class=config[CONF_DEVICE_CLASS], diff --git a/requirements_all.txt b/requirements_all.txt index 2f6247ec18ec16..2c4d1c9e5a0c7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,7 +115,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.9 +adb-shell==0.1.0 # homeassistant.components.adguard adguardhome==0.3.0 @@ -215,7 +215,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.35 +androidtv==0.0.36 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 812af1a2b13061..472ed8df9a32a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -29,7 +29,7 @@ YesssSMS==0.4.1 abodepy==0.16.7 # homeassistant.components.androidtv -adb-shell==0.0.9 +adb-shell==0.1.0 # homeassistant.components.adguard adguardhome==0.3.0 @@ -84,7 +84,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.35 +androidtv==0.0.36 # homeassistant.components.apns apns2==0.3.0 diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index bd05cab2a7484f..c49b6ad11e9c07 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -3,11 +3,11 @@ from unittest.mock import mock_open, patch -class AdbDeviceFake: - """A fake of the `adb_shell.adb_device.AdbDevice` class.""" +class AdbDeviceTcpFake: + """A fake of the `adb_shell.adb_device.AdbDeviceTcp` class.""" def __init__(self, *args, **kwargs): - """Initialize a fake `adb_shell.adb_device.AdbDevice` instance.""" + """Initialize a fake `adb_shell.adb_device.AdbDeviceTcp` instance.""" self.available = False def close(self): @@ -74,39 +74,39 @@ def shell(self, cmd): def patch_connect(success): - """Mock the `adb_shell.adb_device.AdbDevice` and `ppadb.client.Client` classes.""" + """Mock the `adb_shell.adb_device.AdbDeviceTcp` and `ppadb.client.Client` classes.""" def connect_success_python(self, *args, **kwargs): - """Mock the `AdbDeviceFake.connect` method when it succeeds.""" + """Mock the `AdbDeviceTcpFake.connect` method when it succeeds.""" self.available = True def connect_fail_python(self, *args, **kwargs): - """Mock the `AdbDeviceFake.connect` method when it fails.""" + """Mock the `AdbDeviceTcpFake.connect` method when it fails.""" raise OSError if success: return { "python": patch( - f"{__name__}.AdbDeviceFake.connect", connect_success_python + f"{__name__}.AdbDeviceTcpFake.connect", connect_success_python ), "server": patch("androidtv.adb_manager.Client", ClientFakeSuccess), } return { - "python": patch(f"{__name__}.AdbDeviceFake.connect", connect_fail_python), + "python": patch(f"{__name__}.AdbDeviceTcpFake.connect", connect_fail_python), "server": patch("androidtv.adb_manager.Client", ClientFakeFail), } def patch_shell(response=None, error=False): - """Mock the `AdbDeviceFake.shell` and `DeviceFake.shell` methods.""" + """Mock the `AdbDeviceTcpFake.shell` and `DeviceFake.shell` methods.""" def shell_success(self, cmd): - """Mock the `AdbDeviceFake.shell` and `DeviceFake.shell` methods when they are successful.""" + """Mock the `AdbDeviceTcpFake.shell` and `DeviceFake.shell` methods when they are successful.""" self.shell_cmd = cmd return response def shell_fail_python(self, cmd): - """Mock the `AdbDeviceFake.shell` method when it fails.""" + """Mock the `AdbDeviceTcpFake.shell` method when it fails.""" self.shell_cmd = cmd raise AttributeError @@ -117,16 +117,16 @@ def shell_fail_server(self, cmd): if not error: return { - "python": patch(f"{__name__}.AdbDeviceFake.shell", shell_success), + "python": patch(f"{__name__}.AdbDeviceTcpFake.shell", shell_success), "server": patch(f"{__name__}.DeviceFake.shell", shell_success), } return { - "python": patch(f"{__name__}.AdbDeviceFake.shell", shell_fail_python), + "python": patch(f"{__name__}.AdbDeviceTcpFake.shell", shell_fail_python), "server": patch(f"{__name__}.DeviceFake.shell", shell_fail_server), } -PATCH_ADB_DEVICE = patch("androidtv.adb_manager.AdbDevice", AdbDeviceFake) +PATCH_ADB_DEVICE_TCP = patch("androidtv.adb_manager.AdbDeviceTcp", AdbDeviceTcpFake) PATCH_ANDROIDTV_OPEN = patch("androidtv.adb_manager.open", mock_open()) PATCH_KEYGEN = patch("homeassistant.components.androidtv.media_player.keygen") PATCH_SIGNER = patch("androidtv.adb_manager.PythonRSASigner") diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 860b8738607e18..15c4897c136111 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -95,7 +95,7 @@ async def _test_reconnect(hass, caplog, config): """ patch_key, entity_id = _setup(hass, config) - with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell("")[ patch_key @@ -166,7 +166,7 @@ async def _test_adb_shell_returns_none(hass, config): """ patch_key, entity_id = _setup(hass, config) - with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell("")[ patch_key @@ -274,7 +274,7 @@ async def test_setup_with_adbkey(hass): config[DOMAIN][CONF_ADBKEY] = hass.config.path("user_provided_adbkey") patch_key, entity_id = _setup(hass, config) - with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell("")[ patch_key @@ -292,7 +292,7 @@ async def _test_sources(hass, config0): config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} patch_key, entity_id = _setup(hass, config) - with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) @@ -364,7 +364,7 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch) config[DOMAIN][CONF_APPS] = {"com.app.test1": "TEST 1"} patch_key, entity_id = _setup(hass, config) - with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ patch_key ], patchers.patch_shell("")[patch_key]: assert await async_setup_component(hass, DOMAIN, config) @@ -515,3 +515,68 @@ async def test_firetv_select_source_stop_app_id_no_name(hass): "com.app.test2", patchers.PATCH_STOP_APP, ) + + +async def _test_setup_fail(hass, config): + """Test that the entity is not created when the ADB connection is not established.""" + patch_key, entity_id = _setup(hass, config) + + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(False)[ + patch_key + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is None + + return True + + +async def test_setup_fail_androidtv(hass): + """Test that the Android TV entity is not created when the ADB connection is not established.""" + assert await _test_setup_fail(hass, CONFIG_ANDROIDTV_PYTHON_ADB) + + +async def test_setup_fail_firetv(hass): + """Test that the Fire TV entity is not created when the ADB connection is not established.""" + assert await _test_setup_fail(hass, CONFIG_FIRETV_PYTHON_ADB) + + +async def test_setup_two_devices(hass): + """Test that two devices can be set up.""" + config = { + DOMAIN: [ + CONFIG_ANDROIDTV_ADB_SERVER[DOMAIN], + CONFIG_FIRETV_ADB_SERVER[DOMAIN].copy(), + ] + } + config[DOMAIN][1][CONF_HOST] = "127.0.0.2" + + patch_key = "server" + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, config) + + for entity_id in ["media_player.android_tv", "media_player.fire_tv"]: + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + +async def test_setup_same_device_twice(hass): + """Test that setup succeeds with a duplicated config entry.""" + patch_key = "server" + + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) + + with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[patch_key]: + assert await async_setup_component(hass, DOMAIN, CONFIG_ANDROIDTV_ADB_SERVER) From 84a711543599ef9a162e92cf5cf74b5778231cc3 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 15 Dec 2019 13:14:50 +0100 Subject: [PATCH 2428/3953] Start of using hass state for tests rather than direct object (#29377) --- tests/components/arcam_fmj/conftest.py | 7 ++ .../components/arcam_fmj/test_media_player.py | 71 +++++++++++++------ 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index 10405dbeb11de8..6611cbaf9eb581 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -16,6 +16,7 @@ "data": {"entity_id": "switch.test"}, } MOCK_NAME = "dummy" +MOCK_ENTITY_ID = "media_player.arcam_fmj_1" MOCK_CONFIG = DEVICE_SCHEMA({CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}) @@ -41,6 +42,10 @@ def state_fixture(client): state.client = client state.zn = 1 state.get_power.return_value = True + state.get_volume.return_value = 0.0 + state.get_source_list.return_value = [] + state.get_incoming_audio_format.return_value = (0, 0) + state.get_mute.return_value = None return state @@ -48,5 +53,7 @@ def state_fixture(client): def player_fixture(hass, state): """Get standard player.""" player = ArcamFmj(state, MOCK_NAME, None) + player.entity_id = MOCK_ENTITY_ID + player.hass = hass player.async_schedule_update_ha_state = Mock() return player diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 2d2c14f8e18812..06dab76ec87c4b 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -5,18 +5,10 @@ from asynctest.mock import ANY, MagicMock, Mock, PropertyMock, patch import pytest -from homeassistant.components.arcam_fmj.const import ( - DOMAIN, - SIGNAL_CLIENT_DATA, - SIGNAL_CLIENT_STARTED, - SIGNAL_CLIENT_STOPPED, -) -from homeassistant.components.arcam_fmj.media_player import ArcamFmj from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant -from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT +from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_ENTITY_ID MOCK_TURN_ON = { "service": "switch.turn_on", @@ -24,50 +16,69 @@ } +async def update(player, force_refresh=False): + """Force a update of player and return current state data.""" + await player.async_update_ha_state(force_refresh=force_refresh) + return player.hass.states.get(player.entity_id) + + async def test_properties(player, state): """Test standard properties.""" assert player.unique_id is None assert player.device_info == { - "identifiers": {(DOMAIN, MOCK_HOST, MOCK_PORT)}, + "identifiers": {("arcam_fmj", MOCK_HOST, MOCK_PORT)}, "model": "FMJ", "manufacturer": "Arcam", } assert not player.should_poll -async def test_powered_off(player, state): +async def test_powered_off(hass, player, state): """Test properties in powered off state.""" state.get_source.return_value = None state.get_power.return_value = None - assert player.source is None - assert player.state == STATE_OFF + + data = await update(player) + assert "source" not in data.attributes + assert data.state == "off" async def test_powered_on(player, state): """Test properties in powered on state.""" state.get_source.return_value = SourceCodes.PVR state.get_power.return_value = True - assert player.source == "PVR" - assert player.state == STATE_ON + + data = await update(player) + assert data.attributes["source"] == "PVR" + assert data.state == "on" async def test_supported_features_no_service(player, state): """Test support when turn on service exist.""" state.get_power.return_value = None - assert player.supported_features == 68876 + data = await update(player) + assert data.attributes["supported_features"] == 68876 state.get_power.return_value = False - assert player.supported_features == 69004 + data = await update(player) + assert data.attributes["supported_features"] == 69004 async def test_supported_features_service(hass, state): """Test support when turn on service exist.""" + from homeassistant.components.arcam_fmj.media_player import ArcamFmj + player = ArcamFmj(state, "dummy", MOCK_TURN_ON) + player.hass = hass + player.entity_id = MOCK_ENTITY_ID + state.get_power.return_value = None - assert player.supported_features == 69004 + data = await update(player) + assert data.attributes["supported_features"] == 69004 state.get_power.return_value = False - assert player.supported_features == 69004 + data = await update(player) + assert data.attributes["supported_features"] == 69004 async def test_turn_on_without_service(player, state): @@ -83,8 +94,11 @@ async def test_turn_on_without_service(player, state): async def test_turn_on_with_service(hass, state): """Test support when turn on service exist.""" + from homeassistant.components.arcam_fmj.media_player import ArcamFmj + player = ArcamFmj(state, "dummy", MOCK_TURN_ON) player.hass = Mock(HomeAssistant) + player.entity_id = MOCK_ENTITY_ID with patch( "homeassistant.components.arcam_fmj.media_player.async_call_from_config" ) as async_call_from_config: @@ -122,7 +136,7 @@ async def test_name(player): async def test_update(player, state): """Test update.""" - await player.async_update() + await update(player, force_refresh=True) state.update.assert_called_with() @@ -157,7 +171,8 @@ async def test_select_source(player, state, source, value): async def test_source_list(player, state): """Test source list.""" state.get_source_list.return_value = [SourceCodes.BD] - assert player.source_list == ["BD"] + data = await update(player) + assert data.attributes["source_list"] == ["BD"] @pytest.mark.parametrize( @@ -317,16 +332,28 @@ async def test_media_artist(player, state, source, dls, artist): ) async def test_media_title(player, state, source, channel, title): """Test media title.""" + from homeassistant.components.arcam_fmj.media_player import ArcamFmj + state.get_source.return_value = source with patch.object( ArcamFmj, "media_channel", new_callable=PropertyMock ) as media_channel: media_channel.return_value = channel - assert player.media_title == title + data = await update(player) + if title is None: + assert "media_title" not in data.attributes + else: + assert data.attributes["media_title"] == title async def test_added_to_hass(player, state): """Test addition to hass.""" + from homeassistant.components.arcam_fmj.const import ( + SIGNAL_CLIENT_DATA, + SIGNAL_CLIENT_STARTED, + SIGNAL_CLIENT_STOPPED, + ) + connectors = {} def _connect(signal, fun): From 09f3362cc6f7a317854254ed06701ea023f0ac59 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Sun, 15 Dec 2019 10:07:57 -0500 Subject: [PATCH 2429/3953] isort fix on test_media_player (#29965) --- tests/components/arcam_fmj/test_media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 06dab76ec87c4b..8448a25a7fd323 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -8,7 +8,7 @@ from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.core import HomeAssistant -from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_ENTITY_ID +from .conftest import MOCK_ENTITY_ID, MOCK_HOST, MOCK_NAME, MOCK_PORT MOCK_TURN_ON = { "service": "switch.turn_on", From 9c0799eb0ac0e2dd6edb52199597579a45abc0aa Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 15 Dec 2019 17:41:56 +0100 Subject: [PATCH 2430/3953] Upgrade keyring to 20.0.0 and keyrings.alt to 3.4.0 (#29960) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 63b4fd1a37e736..594d897ee4c5b4 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -6,7 +6,7 @@ from homeassistant.util.yaml import _SECRET_NAMESPACE # mypy: allow-untyped-defs -REQUIREMENTS = ["keyring==19.3.0", "keyrings.alt==3.2.0"] +REQUIREMENTS = ["keyring==20.0.0", "keyrings.alt==3.4.0"] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 2c4d1c9e5a0c7a..33cc5c725a8111 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -742,10 +742,10 @@ kaiterra-async-client==0.0.2 keba-kecontact==0.2.0 # homeassistant.scripts.keyring -keyring==19.3.0 +keyring==20.0.0 # homeassistant.scripts.keyring -keyrings.alt==3.2.0 +keyrings.alt==3.4.0 # homeassistant.components.kiwi kiwiki-client==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 472ed8df9a32a2..9e80c2585e6c8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -259,10 +259,10 @@ influxdb==5.2.3 jsonpath==0.82 # homeassistant.scripts.keyring -keyring==19.3.0 +keyring==20.0.0 # homeassistant.scripts.keyring -keyrings.alt==3.2.0 +keyrings.alt==3.4.0 # homeassistant.components.dyson libpurecool==0.5.0 From 012c09ce00d1443401e22ccfae9cdd15d193053b Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Sun, 15 Dec 2019 09:47:11 -0800 Subject: [PATCH 2431/3953] Fix example value for Todoist service (#29953) --- homeassistant/components/todoist/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index c2d23cc4bec5a6..3382e27693d3d5 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -21,5 +21,5 @@ new_task: example: en due_date: description: The day this task is due, in format YYYY-MM-DD. - example: 2019-10-22 + example: "2019-10-22" From d6f317c0a988a6fdc492fbcec55c59b479533ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 15 Dec 2019 22:57:23 +0200 Subject: [PATCH 2432/3953] Remove deprecated rflink configs (#29972) They've been deprecated and automatically replaced since July 2017 already, fe6a4b8ae5953c8d565dc93acc9d34c715a80aee --- homeassistant/components/rflink/__init__.py | 19 ------------------- homeassistant/components/rflink/light.py | 13 ------------- homeassistant/components/rflink/sensor.py | 5 ----- homeassistant/components/rflink/switch.py | 13 ------------- tests/components/rflink/test_sensor.py | 6 +++--- 5 files changed, 3 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index b3e1d2b16b7617..2e5875b9d0865f 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -19,7 +19,6 @@ from homeassistant.core import CoreState, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.deprecation import get_deprecated from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -33,12 +32,9 @@ ATTR_STATE = "state" CONF_ALIASES = "aliases" -CONF_ALIASSES = "aliasses" CONF_GROUP_ALIASES = "group_aliases" -CONF_GROUP_ALIASSES = "group_aliasses" CONF_GROUP = "group" CONF_NOGROUP_ALIASES = "nogroup_aliases" -CONF_NOGROUP_ALIASSES = "nogroup_aliasses" CONF_DEVICE_DEFAULTS = "device_defaults" CONF_DEVICE_ID = "device_id" CONF_DEVICES = "devices" @@ -563,18 +559,3 @@ def async_turn_on(self, **kwargs): def async_turn_off(self, **kwargs): """Turn the device off.""" return self._async_handle_command("turn_off") - - -DEPRECATED_CONFIG_OPTIONS = [CONF_ALIASSES, CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASSES] -REPLACEMENT_CONFIG_OPTIONS = [CONF_ALIASES, CONF_GROUP_ALIASES, CONF_NOGROUP_ALIASES] - - -def remove_deprecated(config): - """Remove deprecated config options from device config.""" - for index, deprecated_option in enumerate(DEPRECATED_CONFIG_OPTIONS): - if deprecated_option in config: - replacement_option = REPLACEMENT_CONFIG_OPTIONS[index] - # generate deprecation warning - get_deprecated(config, replacement_option, deprecated_option) - # remove old config value replacing new one - config[replacement_option] = config.pop(deprecated_option) diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index 682d45f8f420b2..db616b92fc42f9 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -14,23 +14,19 @@ from . import ( CONF_ALIASES, - CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, - CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASES, - CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER, DEVICE_DEFAULTS_SCHEMA, EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, - remove_deprecated, ) _LOGGER = logging.getLogger(__name__) @@ -65,14 +61,6 @@ vol.Optional(CONF_FIRE_EVENT): cv.boolean, vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int), vol.Optional(CONF_GROUP, default=True): cv.boolean, - # deprecated config options - vol.Optional(CONF_ALIASSES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_GROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_NOGROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), } ) }, @@ -131,7 +119,6 @@ def devices_from_config(domain_config): entity_class = entity_class_for_type(entity_type) device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) - remove_deprecated(device_config) is_hybrid = entity_class is HybridRflinkLight diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index aa0ef4f9c62b6f..bc736a1ede6195 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -15,7 +15,6 @@ from . import ( CONF_ALIASES, - CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICES, DATA_DEVICE_REGISTER, @@ -27,7 +26,6 @@ SIGNAL_HANDLE_EVENT, TMP_ENTITY, RflinkDevice, - remove_deprecated, ) _LOGGER = logging.getLogger(__name__) @@ -52,8 +50,6 @@ vol.Optional(CONF_ALIASES, default=[]): vol.All( cv.ensure_list, [cv.string] ), - # deprecated config options - vol.Optional(CONF_ALIASSES): vol.All(cv.ensure_list, [cv.string]), } ) }, @@ -80,7 +76,6 @@ def devices_from_config(domain_config): config[ATTR_UNIT_OF_MEASUREMENT] = lookup_unit_for_sensor_type( config[CONF_SENSOR_TYPE] ) - remove_deprecated(config) device = RflinkSensor(device_id, **config) devices.append(device) diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py index c9173acc1a5a22..8e0ce9a0c8e68b 100644 --- a/homeassistant/components/rflink/switch.py +++ b/homeassistant/components/rflink/switch.py @@ -9,19 +9,15 @@ from . import ( CONF_ALIASES, - CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, - CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASES, - CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DEVICE_DEFAULTS_SCHEMA, SwitchableRflinkDevice, - remove_deprecated, ) _LOGGER = logging.getLogger(__name__) @@ -47,14 +43,6 @@ vol.Optional(CONF_FIRE_EVENT): cv.boolean, vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int), vol.Optional(CONF_GROUP, default=True): cv.boolean, - # deprecated config options - vol.Optional(CONF_ALIASSES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_GROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_NOGROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), } ) }, @@ -68,7 +56,6 @@ def devices_from_config(domain_config): devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) - remove_deprecated(device_config) device = RflinkSwitch(device_id, **device_config) devices.append(device) diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index 3fea3ef6ef4cda..b68e1f959f1ee4 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -115,8 +115,8 @@ async def test_entity_availability(hass, monkeypatch): assert hass.states.get("sensor.test").state == STATE_UNKNOWN -async def test_aliasses(hass, monkeypatch): - """Validate the response to sensor's alias (with aliasses).""" +async def test_aliases(hass, monkeypatch): + """Validate the response to sensor's alias (with aliases).""" config = { "rflink": {"port": "/dev/ttyABC0"}, DOMAIN: { @@ -125,7 +125,7 @@ async def test_aliasses(hass, monkeypatch): "test_02": { "name": "test_02", "sensor_type": "humidity", - "aliasses": ["test_alias_02_0"], + "aliases": ["test_alias_02_0"], } }, }, From 95a6a7502ac253a049a65380125d537dffe9e7cd Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 16 Dec 2019 00:32:25 +0000 Subject: [PATCH 2433/3953] [ci skip] Translation update --- .../components/adguard/.translations/it.json | 4 +- .../components/deconz/.translations/it.json | 4 +- .../components/demo/.translations/ja.json | 5 +++ .../components/icloud/.translations/ja.json | 27 +++++++++++++ .../components/icloud/.translations/pl.json | 38 +++++++++++++++++++ .../components/mqtt/.translations/it.json | 4 +- .../components/soma/.translations/pl.json | 4 +- .../components/zha/.translations/fr.json | 2 +- 8 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/demo/.translations/ja.json create mode 100644 homeassistant/components/icloud/.translations/ja.json create mode 100644 homeassistant/components/icloud/.translations/pl.json diff --git a/homeassistant/components/adguard/.translations/it.json b/homeassistant/components/adguard/.translations/it.json index 1b3ce014d905d1..6dc6ae18d8171a 100644 --- a/homeassistant/components/adguard/.translations/it.json +++ b/homeassistant/components/adguard/.translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo Hass.io AdGuard Home.", + "adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo AdGuard Home di Hass.io.", "adguard_home_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}.", "existing_instance_updated": "Configurazione esistente aggiornata.", "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home." @@ -11,7 +11,7 @@ }, "step": { "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi alla AdGuard Home fornita dal componente aggiuntivo di Hass.io: {addon} ?", + "description": "Vuoi configurare Home Assistant per connettersi alla AdGuard Home fornita dal componente aggiuntivo di Hass.io: {addon}?", "title": "AdGuard Home tramite il componente aggiuntivo di Hass.io" }, "user": { diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 33d49dfca464fa..99e5622129fa53 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -18,8 +18,8 @@ "allow_clip_sensor": "Consenti l'importazione di sensori virtuali", "allow_deconz_groups": "Consenti l'importazione di gruppi deCONZ" }, - "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo hass.io {addon} ?", - "title": "Gateway Zigbee deCONZ tramite l'add-on Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo di Hass.io: {addon}?", + "title": "Gateway Pigmee deCONZ tramite il componente aggiuntivo di Hass.io" }, "init": { "data": { diff --git a/homeassistant/components/demo/.translations/ja.json b/homeassistant/components/demo/.translations/ja.json new file mode 100644 index 00000000000000..529170b111d40a --- /dev/null +++ b/homeassistant/components/demo/.translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\u30c7\u30e2" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/ja.json b/homeassistant/components/icloud/.translations/ja.json new file mode 100644 index 00000000000000..f5c3be53639364 --- /dev/null +++ b/homeassistant/components/icloud/.translations/ja.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "trusted_device": { + "data": { + "trusted_device": "\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "iCloud \u306e\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9" + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb" + }, + "description": "\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "iCloud \u306e\u8cc7\u683c\u60c5\u5831" + }, + "verification_code": { + "data": { + "verification_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9" + }, + "title": "iCloud \u306e\u8a8d\u8a3c\u30b3\u30fc\u30c9" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/pl.json b/homeassistant/components/icloud/.translations/pl.json new file mode 100644 index 00000000000000..f154f77f186ac9 --- /dev/null +++ b/homeassistant/components/icloud/.translations/pl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o", + "send_verification_code": "Nie uda\u0142o si\u0119 wys\u0142a\u0107 kodu weryfikacyjnego", + "username_exists": "Konto jest ju\u017c skonfigurowane", + "validate_verification_code": "Nie uda\u0142o si\u0119 zweryfikowa\u0107 kodu weryfikacyjnego, wybierz urz\u0105dzenie zaufane i ponownie rozpocznij weryfikacj\u0119" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Zaufane urz\u0105dzenie" + }, + "description": "Wybierz zaufane urz\u0105dzenie", + "title": "Zaufane urz\u0105dzenie iCloud" + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "E-mail" + }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", + "title": "Dane uwierzytelniaj\u0105ce iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Kod weryfikacyjny" + }, + "description": "Wprowad\u017a kod weryfikacyjny, kt\u00f3ry w\u0142a\u015bnie otrzyma\u0142e\u015b z iCloud", + "title": "Kod weryfikacyjny iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/it.json b/homeassistant/components/mqtt/.translations/it.json index ed33b182a9693f..cf2b3ddf7d5bc1 100644 --- a/homeassistant/components/mqtt/.translations/it.json +++ b/homeassistant/components/mqtt/.translations/it.json @@ -22,8 +22,8 @@ "data": { "discovery": "Attiva l'individuazione" }, - "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dall'add-on di Hass.io {addon}?", - "title": "Broker MQTT tramite l'add-on di Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo di Hass.io: {addon}?", + "title": "Broker MQTT tramite il componente aggiuntivo di Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json index c71e160142e2d2..102413bf4463da 100644 --- a/homeassistant/components/soma/.translations/pl.json +++ b/homeassistant/components/soma/.translations/pl.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", - "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + "connection_error": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107 z SOMA Connect.", + "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "result_error": "SOMA Connect odpowiedzia\u0142 statusem b\u0142\u0119du." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Soma" diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index 9b1ba025d7c3d6..5d8bdfa82eba87 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -59,7 +59,7 @@ "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", "remote_button_quadruple_press": "bouton \" {subtype} \" quadruple clics", "remote_button_quintuple_press": "bouton \" {subtype} \" quintuple clics", - "remote_button_short_press": "bouton \" {subtype} \" enfonc\u00e9", + "remote_button_short_press": "bouton \"{subtype}\" est press\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" \u00e0 trois clics" } From bfafa77016cc260d53702c7d3d9323b183c29e88 Mon Sep 17 00:00:00 2001 From: Andrew Onyshchuk Date: Sun, 15 Dec 2019 19:02:18 -0600 Subject: [PATCH 2434/3953] Fix support for legacy Z-Wave thermostats (#29955) This brings back support for Z-Wave thermostats of SETPOINT_THERMOSTAT specific device class. Such devices don't have COMMAND_CLASS_THERMOSTAT_MODE and are now handled separately. --- homeassistant/components/zwave/climate.py | 91 ++++++-- .../components/zwave/discovery_schemas.py | 51 +++- tests/components/zwave/test_climate.py | 217 +++++++++++++++++- tests/components/zwave/test_init.py | 42 +++- 4 files changed, 372 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 81b37aa5cb661f..2b421db70b5ceb 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -1,7 +1,7 @@ """Support for Z-Wave climate devices.""" # Because we do not compile openzwave on CI import logging -from typing import Optional +from typing import Optional, Tuple from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -34,7 +34,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ZWaveDeviceEntity +from . import ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) @@ -147,10 +147,14 @@ def async_add_climate(climate): def get_device(hass, values, **kwargs): """Create Z-Wave entity device.""" temp_unit = hass.config.units.temperature_unit - return ZWaveClimate(values, temp_unit) + if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_SETPOINT: + return ZWaveClimateSingleSetpoint(values, temp_unit) + if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_MODE: + return ZWaveClimateMultipleSetpoint(values, temp_unit) + return None -class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): +class ZWaveClimateBase(ZWaveDeviceEntity, ClimateDevice): """Representation of a Z-Wave Climate device.""" def __init__(self, values, temp_unit): @@ -188,18 +192,21 @@ def __init__(self, values, temp_unit): self._zxt_120 = 1 self.update_properties() - def _current_mode_setpoints(self): - current_mode = str(self.values.primary.data).lower() - setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) - return tuple(getattr(self.values, name, None) for name in setpoints_names) + def _mode(self) -> None: + """Return thermostat mode Z-Wave value.""" + raise NotImplementedError() + + def _current_mode_setpoints(self) -> Tuple: + """Return a tuple of current setpoint Z-Wave value(s).""" + raise NotImplementedError() @property def supported_features(self): """Return the list of supported features.""" support = SUPPORT_TARGET_TEMPERATURE - if HVAC_MODE_HEAT_COOL in self._hvac_list: + if self._hvac_list and HVAC_MODE_HEAT_COOL in self._hvac_list: support |= SUPPORT_TARGET_TEMPERATURE_RANGE - if PRESET_AWAY in self._preset_list: + if self._preset_list and PRESET_AWAY in self._preset_list: support |= SUPPORT_TARGET_TEMPERATURE_RANGE if self.values.fan_mode: @@ -237,13 +244,13 @@ def update_properties(self): def _update_operation_mode(self): """Update hvac and preset modes.""" - if self.values.primary: + if self._mode(): self._hvac_list = [] self._hvac_mapping = {} self._preset_list = [] self._preset_mapping = {} - mode_list = self.values.primary.data_items + mode_list = self._mode().data_items if mode_list: for mode in mode_list: ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) @@ -271,7 +278,7 @@ def _update_operation_mode(self): # Presets are supported self._preset_list.append(PRESET_NONE) - current_mode = self.values.primary.data + current_mode = self._mode().data _LOGGER.debug("current_mode=%s", current_mode) _hvac_temp = next( ( @@ -424,7 +431,7 @@ def hvac_mode(self): Need to be one of HVAC_MODE_*. """ - if self.values.primary: + if self._mode(): return self._hvac_mode return self._default_hvac_mode @@ -434,7 +441,7 @@ def hvac_modes(self): Need to be a subset of HVAC_MODES. """ - if self.values.primary: + if self._mode(): return self._hvac_list return [] @@ -451,7 +458,7 @@ def is_aux_heat(self): """Return true if aux heater.""" if not self._aux_heat: return None - if self.values.primary.data == AUX_HEAT_ZWAVE_MODE: + if self._mode().data == AUX_HEAT_ZWAVE_MODE: return True return False @@ -461,7 +468,7 @@ def preset_mode(self): Need to be one of PRESET_*. """ - if self.values.primary: + if self._mode(): return self._preset_mode return PRESET_NONE @@ -471,7 +478,7 @@ def preset_modes(self): Need to be a subset of PRESET_MODES. """ - if self.values.primary: + if self._mode(): return self._preset_list return [] @@ -520,11 +527,11 @@ def set_fan_mode(self, 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.primary: + if not self._mode(): return operation_mode = self._hvac_mapping.get(hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._mode().data = operation_mode def turn_aux_heat_on(self): """Turn auxillary heater on.""" @@ -532,7 +539,7 @@ def turn_aux_heat_on(self): return operation_mode = AUX_HEAT_ZWAVE_MODE _LOGGER.debug("Aux heat on. Set operation mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._mode().data = operation_mode def turn_aux_heat_off(self): """Turn auxillary heater off.""" @@ -543,23 +550,23 @@ def turn_aux_heat_off(self): else: operation_mode = self._hvac_mapping.get(HVAC_MODE_OFF) _LOGGER.debug("Aux heat off. Set operation mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._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.primary: + if not self._mode(): return if preset_mode == PRESET_NONE: # Activate the current hvac mode self._update_operation_mode() operation_mode = self._hvac_mapping.get(self.hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._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.primary.data = operation_mode + self._mode().data = operation_mode def set_swing_mode(self, swing_mode): """Set new target swing mode.""" @@ -575,3 +582,37 @@ def device_state_attributes(self): if self._fan_action: data[ATTR_FAN_ACTION] = self._fan_action return data + + +class ZWaveClimateSingleSetpoint(ZWaveClimateBase): + """Representation of a single setpoint Z-Wave thermostat device.""" + + def __init__(self, values, temp_unit): + """Initialize the Z-Wave climate device.""" + ZWaveClimateBase.__init__(self, values, temp_unit) + + def _mode(self) -> None: + """Return thermostat mode Z-Wave value.""" + return self.values.mode + + def _current_mode_setpoints(self) -> Tuple: + """Return a tuple of current setpoint Z-Wave value(s).""" + return (self.values.primary,) + + +class ZWaveClimateMultipleSetpoint(ZWaveClimateBase): + """Representation of a multiple setpoint Z-Wave thermostat device.""" + + def __init__(self, values, temp_unit): + """Initialize the Z-Wave climate device.""" + ZWaveClimateBase.__init__(self, values, temp_unit) + + def _mode(self) -> None: + """Return thermostat mode Z-Wave value.""" + return self.values.primary + + def _current_mode_setpoints(self) -> Tuple: + """Return a tuple of current setpoint Z-Wave value(s).""" + current_mode = str(self.values.primary.data).lower() + setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) + return tuple(getattr(self.values, name, None) for name in setpoints_names) diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index 2d6f08169eafcf..5e4b83d81e1346 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -48,11 +48,60 @@ ), }, { - const.DISC_COMPONENT: "climate", + const.DISC_COMPONENT: "climate", # thermostat without COMMAND_CLASS_THERMOSTAT_MODE const.DISC_GENERIC_DEVICE_CLASS: [ const.GENERIC_TYPE_THERMOSTAT, const.GENERIC_TYPE_SENSOR_MULTILEVEL, ], + const.DISC_SPECIFIC_DEVICE_CLASS: [ + const.SPECIFIC_TYPE_THERMOSTAT_HEATING, + const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + ], + const.DISC_VALUES: dict( + DEFAULT_VALUES_SCHEMA, + **{ + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT] + }, + "temperature": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], + const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE], + const.DISC_OPTIONAL: True, + }, + "fan_mode": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE], + const.DISC_OPTIONAL: True, + }, + "operating_state": { + const.DISC_COMMAND_CLASS: [ + const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE + ], + const.DISC_OPTIONAL: True, + }, + "fan_action": { + const.DISC_COMMAND_CLASS: [ + const.COMMAND_CLASS_THERMOSTAT_FAN_ACTION + ], + const.DISC_OPTIONAL: True, + }, + "mode": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE], + const.DISC_OPTIONAL: True, + }, + }, + ), + }, + { + const.DISC_COMPONENT: "climate", # thermostat with COMMAND_CLASS_THERMOSTAT_MODE + const.DISC_GENERIC_DEVICE_CLASS: [ + const.GENERIC_TYPE_THERMOSTAT, + const.GENERIC_TYPE_SENSOR_MULTILEVEL, + ], + const.DISC_SPECIFIC_DEVICE_CLASS: [ + const.SPECIFIC_TYPE_THERMOSTAT_GENERAL, + const.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, + const.SPECIFIC_TYPE_SETBACK_THERMOSTAT, + ], const.DISC_VALUES: dict( DEFAULT_VALUES_SCHEMA, **{ diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index b820e496226162..631bf0a0ce886b 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -15,14 +15,18 @@ PRESET_BOOST, PRESET_ECO, PRESET_NONE, + SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.components.zwave import climate -from homeassistant.components.zwave.climate import DEFAULT_HVAC_MODES +from homeassistant.components.zwave import climate, const +from homeassistant.components.zwave.climate import ( + AUX_HEAT_ZWAVE_MODE, + DEFAULT_HVAC_MODES, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed @@ -34,6 +38,7 @@ def device(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -62,6 +67,7 @@ def device_zxt_120(hass, mock_openzwave): values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -90,6 +96,7 @@ def device_mapping(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data="Heat", data_items=["Off", "Cool", "Heat", "Full Power", "Auto"], node=node, @@ -112,6 +119,7 @@ def device_unknown(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data="Heat", data_items=["Off", "Cool", "Heat", "heat_cool", "Abcdefg"], node=node, @@ -134,6 +142,7 @@ def device_heat_cool(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -162,6 +171,7 @@ def device_heat_cool_range(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT_COOL, data_items=[ HVAC_MODE_OFF, @@ -189,6 +199,7 @@ def device_heat_cool_away(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT_COOL, data_items=[ HVAC_MODE_OFF, @@ -219,6 +230,7 @@ def device_heat_eco(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "heat econ"], node=node, @@ -235,6 +247,100 @@ def device_heat_eco(hass, mock_openzwave): yield device +@pytest.fixture +def device_aux_heat(hass, mock_openzwave): + """Fixture to provide a precreated climate device. aux heat.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "Aux Heat"], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_eco_heating=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_single_setpoint(hass, mock_openzwave): + """Fixture to provide a precreated climate device. + + SETPOINT_THERMOSTAT device class. + """ + + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, data=1, node=node + ), + mode=None, + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_single_setpoint_with_mode(hass, mock_openzwave): + """Fixture to provide a precreated climate device. + + SETPOINT_THERMOSTAT device class with COMMAND_CLASS_THERMOSTAT_MODE command class + """ + + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, data=1, node=node + ), + mode=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT], + node=node, + ), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +def test_get_device_detects_none(hass, mock_openzwave): + """Test get_device returns None.""" + node = MockNode() + value = MockValue(data=0, node=node) + values = MockEntityValues(primary=value) + + device = climate.get_device(hass, node=node, values=values, node_config={}) + assert device is None + + +def test_get_device_detects_multiple_setpoint_device(device): + """Test get_device returns a Z-Wave multiple setpoint device.""" + assert isinstance(device, climate.ZWaveClimateMultipleSetpoint) + + +def test_get_device_detects_single_setpoint_device(device_single_setpoint): + """Test get_device returns a Z-Wave single setpoint device.""" + assert isinstance(device_single_setpoint, climate.ZWaveClimateSingleSetpoint) + + def test_default_hvac_modes(): """Test wether all hvac modes are included in default_hvac_modes.""" for hvac_mode in HVAC_MODES: @@ -274,6 +380,18 @@ def test_supported_features_preset_mode(device_mapping): ) +def test_supported_features_preset_mode_away(device_heat_cool_away): + """Test supported features flags with swing mode.""" + device = device_heat_cool_away + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_PRESET_MODE + ) + + def test_supported_features_swing_mode(device_zxt_120): """Test supported features flags with swing mode.""" device = device_zxt_120 @@ -286,6 +404,27 @@ def test_supported_features_swing_mode(device_zxt_120): ) +def test_supported_features_aux_heat(device_aux_heat): + """Test supported features flags with aux heat.""" + device = device_aux_heat + assert ( + device.supported_features + == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_AUX_HEAT + ) + + +def test_supported_features_single_setpoint(device_single_setpoint): + """Test supported features flags for SETPOINT_THERMOSTAT.""" + device = device_single_setpoint + assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + + +def test_supported_features_single_setpoint_with_mode(device_single_setpoint_with_mode): + """Test supported features flags for SETPOINT_THERMOSTAT.""" + device = device_single_setpoint_with_mode + assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + + def test_zxt_120_swing_mode(device_zxt_120): """Test operation of the zxt 120 swing mode.""" device = device_zxt_120 @@ -331,6 +470,22 @@ def test_data_lists(device): assert device.preset_modes == [] +def test_data_lists_single_setpoint(device_single_setpoint): + """Test data lists from zwave value items.""" + device = device_single_setpoint + assert device.fan_modes == [3, 4, 5] + assert device.hvac_modes == [] + assert device.preset_modes == [] + + +def test_data_lists_single_setpoint_with_mode(device_single_setpoint_with_mode): + """Test data lists from zwave value items.""" + device = device_single_setpoint_with_mode + assert device.fan_modes == [3, 4, 5] + assert device.hvac_modes == [HVAC_MODE_OFF, HVAC_MODE_HEAT] + assert device.preset_modes == [] + + def test_data_lists_mapping(device_mapping): """Test data lists from zwave value items.""" device = device_mapping @@ -404,6 +559,14 @@ def test_target_value_set_eco(device_heat_eco): assert device.values.setpoint_eco_heating.data == 0 +def test_target_value_set_single_setpoint(device_single_setpoint): + """Test values changed for climate device.""" + device = device_single_setpoint + assert device.values.primary.data == 1 + device.set_temperature(**{ATTR_TEMPERATURE: 2}) + assert device.values.primary.data == 2 + + def test_operation_value_set(device): """Test values changed for climate device.""" assert device.values.primary.data == HVAC_MODE_HEAT @@ -546,6 +709,15 @@ def test_target_changed_with_mode(device): assert device.target_temperature_high == 10 +def test_target_value_changed_single_setpoint(device_single_setpoint): + """Test values changed for climate device.""" + device = device_single_setpoint + assert device.target_temperature == 1 + device.values.primary.data = 2 + value_changed(device.values.primary) + assert device.target_temperature == 2 + + def test_temperature_value_changed(device): """Test values changed for climate device.""" assert device.current_temperature == 5 @@ -677,3 +849,44 @@ def test_fan_action_value_changed(device): device.values.fan_action.data = 9 value_changed(device.values.fan_action) assert device.device_state_attributes[climate.ATTR_FAN_ACTION] == 9 + + +def test_aux_heat_unsupported_set(device): + """Test aux heat for climate device.""" + device = device + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_on() + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_off() + assert device.values.primary.data == HVAC_MODE_HEAT + + +def test_aux_heat_unsupported_value_changed(device): + """Test aux heat for climate device.""" + device = device + assert device.is_aux_heat is None + device.values.primary.data = HVAC_MODE_HEAT + value_changed(device.values.primary) + assert device.is_aux_heat is None + + +def test_aux_heat_set(device_aux_heat): + """Test aux heat for climate device.""" + device = device_aux_heat + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_on() + assert device.values.primary.data == AUX_HEAT_ZWAVE_MODE + device.turn_aux_heat_off() + assert device.values.primary.data == HVAC_MODE_HEAT + + +def test_aux_heat_value_changed(device_aux_heat): + """Test aux heat for climate device.""" + device = device_aux_heat + assert device.is_aux_heat is False + device.values.primary.data = AUX_HEAT_ZWAVE_MODE + value_changed(device.values.primary) + assert device.is_aux_heat is True + device.values.primary.data = HVAC_MODE_HEAT + value_changed(device.values.primary) + assert device.is_aux_heat is False diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 8f717b2903cc15..36c918232205d0 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -573,7 +573,11 @@ def mock_connect(receiver, signal, *args, **kwargs): assert len(mock_receivers) == 1 - node = MockNode(node_id=11, generic=const.GENERIC_TYPE_THERMOSTAT) + node = MockNode( + node_id=11, + generic=const.GENERIC_TYPE_THERMOSTAT, + specific=const.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, + ) thermostat_mode = MockValue( data="Heat", data_items=["Off", "Heat"], @@ -638,6 +642,42 @@ def mock_update(self): ) +async def test_value_discovery_legacy_thermostat(hass, mock_openzwave): + """Test discovery of a node. Special case for legacy thermostats.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_VALUE_ADDED: + mock_receivers.append(receiver) + + with patch("pydispatch.dispatcher.connect", new=mock_connect): + await async_setup_component(hass, "zwave", {"zwave": {}}) + await hass.async_block_till_done() + + assert len(mock_receivers) == 1 + + node = MockNode( + node_id=11, + generic=const.GENERIC_TYPE_THERMOSTAT, + specific=const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + ) + setpoint_heating = MockValue( + data=22.0, + node=node, + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, + index=1, + genre=const.GENRE_USER, + ) + + hass.async_add_job(mock_receivers[0], node, setpoint_heating) + await hass.async_block_till_done() + + assert ( + hass.states.get("climate.mock_node_mock_value").attributes["temperature"] + == 22.0 + ) + + async def test_power_schemes(hass, mock_openzwave): """Test power attribute.""" mock_receivers = [] From 445fd15f769f966cb7df0d0ed0c4cfbe59a7852e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 16 Dec 2019 08:29:19 +0200 Subject: [PATCH 2435/3953] Drop Python 3.6 support (#29978) --- .coveragerc | 1 - .readthedocs.yml | 2 +- .travis.yml | 10 ++-- azure-pipelines-ci.yml | 6 +-- homeassistant/__main__.py | 14 ++--- homeassistant/const.py | 6 +-- homeassistant/monkey_patch.py | 73 --------------------------- homeassistant/package_constraints.txt | 3 +- homeassistant/util/async_.py | 21 +------- pyproject.toml | 2 +- requirements_all.txt | 1 - script/gen_requirements_all.py | 2 +- setup.cfg | 5 +- setup.py | 1 - tests/util/test_async.py | 7 +-- 15 files changed, 19 insertions(+), 135 deletions(-) delete mode 100644 homeassistant/monkey_patch.py diff --git a/.coveragerc b/.coveragerc index c6e7182d326d88..e16a622cc61edf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,7 +5,6 @@ omit = homeassistant/__main__.py homeassistant/helpers/signal.py homeassistant/helpers/typing.py - homeassistant/monkey_patch.py homeassistant/scripts/*.py homeassistant/util/async.py diff --git a/.readthedocs.yml b/.readthedocs.yml index 923a03f03dd8a4..0303f84d51c1f1 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,7 +4,7 @@ build: image: latest python: - version: 3.6 + version: 3.7 setup_py_install: true requirements_file: requirements_docs.txt diff --git a/.travis.yml b/.travis.yml index 2660d805726c09..6add8c15bfc5a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,15 +14,13 @@ addons: matrix: fast_finish: true include: - - python: "3.6.1" + - python: "3.7.0" env: TOXENV=lint - - python: "3.6.1" + - python: "3.7.0" env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30 - - python: "3.6.1" + - python: "3.7.0" env: TOXENV=typing - - python: "3.6.1" - env: TOXENV=py36 - - python: "3.7" + - python: "3.7.0" env: TOXENV=py37 cache: diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 5a289cbbc70c2a..464b1079957c3f 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -14,8 +14,6 @@ pr: resources: containers: - - container: 36 - image: homeassistant/ci-azure:3.6 - container: 37 image: homeassistant/ci-azure:3.7 repositories: @@ -25,7 +23,7 @@ resources: endpoint: 'home-assistant' variables: - name: PythonMain - value: '36' + value: '37' - group: codecov stages: @@ -108,8 +106,6 @@ stages: strategy: maxParallel: 3 matrix: - Python36: - python.container: '36' Python37: python.container: '37' container: $[ variables['python.container'] ] diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index bf3042c3f88c5a..bcc972522556ec 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,5 +1,6 @@ """Start Home Assistant.""" import argparse +import asyncio import os import platform import subprocess @@ -7,7 +8,6 @@ import threading from typing import TYPE_CHECKING, Any, Dict, List -from homeassistant import monkey_patch from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ if TYPE_CHECKING: @@ -16,7 +16,6 @@ def set_loop() -> None: """Attempt to use different loop.""" - import asyncio from asyncio.events import BaseDefaultEventLoopPolicy if sys.platform == "win32": @@ -345,11 +344,6 @@ def main() -> int: """Start Home Assistant.""" validate_python() - monkey_patch_needed = sys.version_info[:3] < (3, 6, 3) - if monkey_patch_needed and os.environ.get("HASS_NO_MONKEY") != "1": - monkey_patch.disable_c_asyncio() - monkey_patch.patch_weakref_tasks() - set_loop() # Run a simple daemon runner process on Windows to handle restarts @@ -383,13 +377,11 @@ def main() -> int: if args.pid_file: write_pid(args.pid_file) - from homeassistant.util.async_ import asyncio_run - - exit_code = asyncio_run(setup_and_run_hass(config_dir, args)) + exit_code = asyncio.run(setup_and_run_hass(config_dir, args)) if exit_code == RESTART_EXIT_CODE and not args.runner: try_to_restart() - return exit_code # type: ignore + return exit_code if __name__ == "__main__": diff --git a/homeassistant/const.py b/homeassistant/const.py index ba7e7a66126946..15dc5a099bc0ab 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -4,10 +4,10 @@ PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) -REQUIRED_PYTHON_VER = (3, 6, 1) +REQUIRED_PYTHON_VER = (3, 7, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_VER = (3, 7, 0) -REQUIRED_NEXT_PYTHON_DATE = "December 15, 2019" +REQUIRED_NEXT_PYTHON_VER = (3, 8, 0) +REQUIRED_NEXT_PYTHON_DATE = "" # Format for platform files PLATFORM_FORMAT = "{platform}.{domain}" diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py deleted file mode 100644 index c6e2e66ab131a5..00000000000000 --- a/homeassistant/monkey_patch.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Monkey patch Python to work around issues causing segfaults. - -Under heavy threading operations that schedule calls into -the asyncio event loop, Task objects are created. Due to -a bug in Python, GC may have an issue when switching between -the threads and objects with __del__ (which various components -in HASS have). - -This monkey-patch removes the weakref.Weakset, and replaces it -with an object that ignores the only call utilizing it (the -Task.__init__ which calls _all_tasks.add(self)). It also removes -the __del__ which could trigger the future objects __del__ at -unpredictable times. - -The side-effect of this manipulation of the Task is that -Task.all_tasks() is no longer accurate, and there will be no -warning emitted if a Task is GC'd while in use. - -Related Python bugs: - - https://bugs.python.org/issue26617 -""" -import sys -from typing import Any - - -def patch_weakref_tasks() -> None: - """Replace weakref.WeakSet to address Python 3 bug.""" - # pylint: disable=no-self-use, protected-access - import asyncio.tasks - - class IgnoreCalls: - """Ignore add calls.""" - - def add(self, other: Any) -> None: - """No-op add.""" - return - - asyncio.tasks.Task._all_tasks = IgnoreCalls() # type: ignore - try: - del asyncio.tasks.Task.__del__ - except: # noqa: E722 pylint: disable=bare-except - pass - - -def disable_c_asyncio() -> None: - """Disable using C implementation of asyncio. - - Required to be able to apply the weakref monkey patch. - - Requires Python 3.6+. - """ - - class AsyncioImportFinder: - """Finder that blocks C version of asyncio being loaded.""" - - PATH_TRIGGER = "_asyncio" - - def __init__(self, path_entry: str) -> None: - if path_entry != self.PATH_TRIGGER: - raise ImportError() - - def find_module(self, fullname: str, path: Any = None) -> None: - """Find a module.""" - if fullname == self.PATH_TRIGGER: - raise ModuleNotFoundError() - - sys.path_hooks.append(AsyncioImportFinder) - sys.path.insert(0, AsyncioImportFinder.PATH_TRIGGER) - - try: - import _asyncio # noqa: F401 - except ImportError: - pass diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index afc92f92ef790f..c0b7933fcca0fe 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,6 @@ async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 certifi>=2019.11.28 -contextvars==2.4;python_version<"3.7" cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 @@ -29,7 +28,7 @@ zeroconf==0.24.0 pycryptodome>=3.6.6 -# Breaks Python 3.6 and is not needed for our supported Python versions +# Not needed for our supported Python versions enum34==1000000000.0.0 # This is a old unmaintained library and is replaced with pycryptodome diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index b1d3a7dd8e7cd1..212c2bff91064b 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -1,33 +1,14 @@ """Asyncio backports for Python 3.6 compatibility.""" -import asyncio from asyncio import coroutines, ensure_future from asyncio.events import AbstractEventLoop import concurrent.futures import logging import threading -from typing import Any, Awaitable, Callable, Coroutine, TypeVar +from typing import Any, Callable, Coroutine _LOGGER = logging.getLogger(__name__) -try: - # pylint: disable=invalid-name - asyncio_run = asyncio.run # type: ignore -except AttributeError: - _T = TypeVar("_T") - - def asyncio_run(main: Awaitable[_T], *, debug: bool = False) -> _T: - """Minimal re-implementation of asyncio.run (since 3.7).""" - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - loop.set_debug(debug) - try: - return loop.run_until_complete(main) - finally: - asyncio.set_event_loop(None) - loop.close() - - def fire_coroutine_threadsafe(coro: Coroutine, loop: AbstractEventLoop) -> None: """Submit a coroutine object to a given event loop. diff --git a/pyproject.toml b/pyproject.toml index 7a75060c8e9c2d..7c0c5eeb4337f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [tool.black] -target-version = ["py36", "py37", "py38"] +target-version = ["py37", "py38"] exclude = 'generated' diff --git a/requirements_all.txt b/requirements_all.txt index 33cc5c725a8111..f0b547b4a25b4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,6 @@ async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 certifi>=2019.11.28 -contextvars==2.4;python_version<"3.7" importlib-metadata==0.23 jinja2>=2.10.3 PyJWT==1.7.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c18b51ba5d1aae..0dfefc958c35d6 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -58,7 +58,7 @@ CONSTRAINT_BASE = """ pycryptodome>=3.6.6 -# Breaks Python 3.6 and is not needed for our supported Python versions +# Not needed for our supported Python versions enum34==1000000000.0.0 # This is a old unmaintained library and is replaced with pycryptodome diff --git a/setup.cfg b/setup.cfg index bb2b1652ffa20c..f9e9852812c8ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,6 @@ classifier = Intended Audience :: Developers License :: OSI Approved :: Apache Software License Operating System :: OS Independent - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Topic :: Home Automation @@ -57,7 +56,7 @@ forced_separate = tests combine_as_imports = true [mypy] -python_version = 3.6 +python_version = 3.7 ignore_errors = true follow_imports = silent ignore_missing_imports = true @@ -65,7 +64,7 @@ warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true -[mypy-homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.loader,homeassistant.__main__,homeassistant.monkey_patch,homeassistant.requirements,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,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.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*] +[mypy-homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,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.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*] ignore_errors = false check_untyped_defs = true disallow_incomplete_defs = true diff --git a/setup.py b/setup.py index f9cb24d1e91b8e..720406ab91b32a 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,6 @@ "attrs==19.3.0", "bcrypt==3.1.7", "certifi>=2019.11.28", - 'contextvars==2.4;python_version<"3.7"', "importlib-metadata==0.23", "jinja2>=2.10.3", "PyJWT==1.7.1", diff --git a/tests/util/test_async.py b/tests/util/test_async.py index ec16fa2ae67ebd..098b04a30486a1 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -1,6 +1,5 @@ """Tests for async util methods from Python source.""" import asyncio -import sys from unittest import TestCase from unittest.mock import MagicMock, patch @@ -112,11 +111,7 @@ def add_coroutine(self, a, b, fail, invalid, cancel): """Wait 0.05 second and return a + b.""" yield from asyncio.sleep(0.05, loop=self.loop) if cancel: - if sys.version_info[:2] >= (3, 7): - current_task = asyncio.current_task - else: - current_task = asyncio.tasks.Task.current_task - current_task(self.loop).cancel() + asyncio.current_task(self.loop).cancel() yield return self.add_callback(a, b, fail, invalid) From 039cc98278612cbfc08d2c242ba6182a0f399a4d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 16 Dec 2019 08:04:59 +0100 Subject: [PATCH 2436/3953] Support case of unknown/unavailable temperature/humidity (#29959) * Support case of unknown/unavailable temperature/humidity State is never None, just a string. * Lint suggestion --- .../components/google_assistant/trait.py | 5 +-- .../components/google_assistant/test_trait.py | 36 +++++++++++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 40def0cb464f03..d49632755cd6c0 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -44,6 +44,7 @@ STATE_LOCKED, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -666,7 +667,7 @@ def query_attributes(self): device_class = attrs.get(ATTR_DEVICE_CLASS) if device_class == sensor.DEVICE_CLASS_TEMPERATURE: current_temp = self.state.state - if current_temp is not None: + if current_temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): response["thermostatTemperatureAmbient"] = round( temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1 ) @@ -887,7 +888,7 @@ def query_attributes(self): device_class = attrs.get(ATTR_DEVICE_CLASS) if device_class == sensor.DEVICE_CLASS_HUMIDITY: current_humidity = self.state.state - if current_humidity is not None: + if current_humidity not in (STATE_UNKNOWN, STATE_UNAVAILABLE): response["humidityAmbientPercent"] = round(float(current_humidity)) return response diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 8a1d7384729b04..98e5149de1d0e3 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1615,22 +1615,37 @@ async def test_temperature_setting_sensor(hass): sensor.DOMAIN, 0, sensor.DEVICE_CLASS_TEMPERATURE ) - hass.config.units.temperature_unit = TEMP_FAHRENHEIT + +@pytest.mark.parametrize( + "unit_in,unit_out,state,ambient", + [ + (TEMP_FAHRENHEIT, "F", "70", 21.1), + (TEMP_CELSIUS, "C", "21.1", 21.1), + (TEMP_FAHRENHEIT, "F", "unavailable", None), + (TEMP_FAHRENHEIT, "F", "unknown", None), + ], +) +async def test_temperature_setting_sensor_data(hass, unit_in, unit_out, state, ambient): + """Test TemperatureSetting trait support for temperature sensor.""" + hass.config.units.temperature_unit = unit_in trt = trait.TemperatureSettingTrait( hass, State( - "sensor.test", "70", {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TEMPERATURE} + "sensor.test", state, {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TEMPERATURE} ), BASIC_CONFIG, ) assert trt.sync_attributes() == { "queryOnlyTemperatureSetting": True, - "thermostatTemperatureUnit": "F", + "thermostatTemperatureUnit": unit_out, } - assert trt.query_attributes() == {"thermostatTemperatureAmbient": 21.1} + if ambient: + assert trt.query_attributes() == {"thermostatTemperatureAmbient": ambient} + else: + assert trt.query_attributes() == {} hass.config.units.temperature_unit = TEMP_CELSIUS @@ -1646,14 +1661,23 @@ async def test_humidity_setting_sensor(hass): sensor.DOMAIN, 0, sensor.DEVICE_CLASS_HUMIDITY ) + +@pytest.mark.parametrize( + "state,ambient", [("70", 70), ("unavailable", None), ("unknown", None)] +) +async def test_humidity_setting_sensor_data(hass, state, ambient): + """Test HumiditySetting trait support for humidity sensor.""" trt = trait.HumiditySettingTrait( hass, - State("sensor.test", "70", {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_HUMIDITY}), + State("sensor.test", state, {ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_HUMIDITY}), BASIC_CONFIG, ) assert trt.sync_attributes() == {"queryOnlyHumiditySetting": True} - assert trt.query_attributes() == {"humidityAmbientPercent": 70} + if ambient: + assert trt.query_attributes() == {"humidityAmbientPercent": ambient} + else: + assert trt.query_attributes() == {} with pytest.raises(helpers.SmartHomeError) as err: await trt.execute(trait.COMMAND_ONOFF, BASIC_DATA, {"on": False}, {}) From fc01da8933dcc87a7dcb54df1e9b2cb6138d0287 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Mon, 16 Dec 2019 08:20:41 +0100 Subject: [PATCH 2437/3953] Migrate to api_key (#29966) --- .../components/here_travel_time/manifest.json | 2 +- .../components/here_travel_time/sensor.py | 11 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../here_travel_time/test_sensor.py | 113 +++++++----------- 5 files changed, 48 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index 5ef71a249e6d5c..da2c03b1ac826a 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -3,7 +3,7 @@ "name": "HERE travel time", "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "requirements": [ - "herepy==0.6.3.3" + "herepy==2.0.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 0b688a770c58f5..e482943eff3132 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -32,8 +32,7 @@ CONF_ORIGIN_LATITUDE = "origin_latitude" CONF_ORIGIN_LONGITUDE = "origin_longitude" CONF_ORIGIN_ENTITY_ID = "origin_entity_id" -CONF_APP_ID = "app_id" -CONF_APP_CODE = "app_code" +CONF_API_KEY = "api_key" CONF_TRAFFIC_MODE = "traffic_mode" CONF_ROUTE_MODE = "route_mode" @@ -97,8 +96,7 @@ cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID), PLATFORM_SCHEMA.extend( { - vol.Required(CONF_APP_ID): cv.string, - vol.Required(CONF_APP_CODE): cv.string, + vol.Required(CONF_API_KEY): cv.string, vol.Inclusive( CONF_DESTINATION_LATITUDE, "destination_coordinates" ): cv.latitude, @@ -131,9 +129,8 @@ async def async_setup_platform( ) -> None: """Set up the HERE travel time platform.""" - app_id = config[CONF_APP_ID] - app_code = config[CONF_APP_CODE] - here_client = herepy.RoutingApi(app_id, app_code) + api_key = config[CONF_API_KEY] + here_client = herepy.RoutingApi(api_key) if not await hass.async_add_executor_job( _are_valid_client_credentials, here_client diff --git a/requirements_all.txt b/requirements_all.txt index f0b547b4a25b4c..873e056eb72a0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -648,7 +648,7 @@ hdate==0.9.3 heatmiserV3==1.1.18 # homeassistant.components.here_travel_time -herepy==0.6.3.3 +herepy==2.0.0 # homeassistant.components.hikvisioncam hikvision==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e80c2585e6c8b..074d5bd6c8e7f3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ hbmqtt==0.9.5 hdate==0.9.3 # homeassistant.components.here_travel_time -herepy==0.6.3.3 +herepy==2.0.0 # homeassistant.components.pi_hole hole==0.5.0 diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 296efbdad532e0..6b9c52b1042ae2 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -48,8 +48,7 @@ PLATFORM = "here_travel_time" -APP_ID = "test" -APP_CODE = "test" +API_KEY = "test" TRUCK_ORIGIN_LATITUDE = "41.9798" TRUCK_ORIGIN_LONGITUDE = "-87.8801" @@ -67,15 +66,14 @@ CAR_DESTINATION_LONGITUDE = "-77.1" -def _build_mock_url(origin, destination, modes, app_id, app_code, departure): +def _build_mock_url(origin, destination, modes, api_key, departure): """Construct a url for HERE.""" - base_url = "https://route.cit.api.here.com/routing/7.2/calculateroute.json?" + base_url = "https://route.ls.hereapi.com/routing/7.2/calculateroute.json?" parameters = { "waypoint0": f"geo!{origin}", "waypoint1": f"geo!{destination}", "mode": ";".join(str(herepy.RouteMode[mode]) for mode in modes), - "app_id": app_id, - "app_code": app_code, + "apikey": api_key, "departure": departure, } url = base_url + urllib.parse.urlencode(parameters) @@ -118,8 +116,7 @@ def requests_mock_credentials_check(requests_mock): ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, - APP_ID, - APP_CODE, + API_KEY, "now", ) requests_mock.get( @@ -136,8 +133,7 @@ def requests_mock_truck_response(requests_mock_credentials_check): ",".join([TRUCK_ORIGIN_LATITUDE, TRUCK_ORIGIN_LONGITUDE]), ",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]), modes, - APP_ID, - APP_CODE, + API_KEY, "now", ) requests_mock_credentials_check.get( @@ -153,8 +149,7 @@ def requests_mock_car_disabled_response(requests_mock_credentials_check): ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, - APP_ID, - APP_CODE, + API_KEY, "now", ) requests_mock_credentials_check.get( @@ -172,8 +167,7 @@ async def test_car(hass, requests_mock_car_disabled_response): "origin_longitude": CAR_ORIGIN_LONGITUDE, "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -219,8 +213,7 @@ async def test_traffic_mode_enabled(hass, requests_mock_credentials_check): ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, - APP_ID, - APP_CODE, + API_KEY, "now", ) requests_mock_credentials_check.get( @@ -235,8 +228,7 @@ async def test_traffic_mode_enabled(hass, requests_mock_credentials_check): "origin_longitude": CAR_ORIGIN_LONGITUDE, "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "traffic_mode": True, } } @@ -262,8 +254,7 @@ async def test_imperial(hass, requests_mock_car_disabled_response): "origin_longitude": CAR_ORIGIN_LONGITUDE, "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "unit_system": "imperial", } } @@ -281,7 +272,7 @@ async def test_route_mode_shortest(hass, requests_mock_credentials_check): origin = "38.902981,-77.048338" destination = "39.042158,-77.119116" modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/car_shortest_response.json") ) @@ -294,8 +285,7 @@ async def test_route_mode_shortest(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "route_mode": ROUTE_MODE_SHORTEST, } } @@ -313,7 +303,7 @@ async def test_route_mode_fastest(hass, requests_mock_credentials_check): origin = "38.902981,-77.048338" destination = "39.042158,-77.119116" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_ENABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/car_enabled_response.json") ) @@ -326,8 +316,7 @@ async def test_route_mode_fastest(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "traffic_mode": True, } } @@ -350,8 +339,7 @@ async def test_truck(hass, requests_mock_truck_response): "origin_longitude": TRUCK_ORIGIN_LONGITUDE, "destination_latitude": TRUCK_DESTINATION_LATITUDE, "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -369,7 +357,7 @@ async def test_public_transport(hass, requests_mock_credentials_check): origin = "41.9798,-87.8801" destination = "41.9043,-87.9216" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/public_response.json") ) @@ -382,8 +370,7 @@ async def test_public_transport(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_PUBLIC, } } @@ -419,7 +406,7 @@ async def test_public_transport_time_table(hass, requests_mock_credentials_check origin = "41.9798,-87.8801" destination = "41.9043,-87.9216" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/public_time_table_response.json"), @@ -433,8 +420,7 @@ async def test_public_transport_time_table(hass, requests_mock_credentials_check "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, } } @@ -470,7 +456,7 @@ async def test_pedestrian(hass, requests_mock_credentials_check): origin = "41.9798,-87.8801" destination = "41.9043,-87.9216" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_PEDESTRIAN, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/pedestrian_response.json") ) @@ -483,8 +469,7 @@ async def test_pedestrian(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_PEDESTRIAN, } } @@ -523,7 +508,7 @@ async def test_bicycle(hass, requests_mock_credentials_check): origin = "41.9798,-87.8801" destination = "41.9043,-87.9216" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_BICYCLE, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/bike_response.json") ) @@ -536,8 +521,7 @@ async def test_bicycle(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_BICYCLE, } } @@ -599,8 +583,7 @@ async def test_location_zone(hass, requests_mock_truck_response): "name": "test", "origin_entity_id": "zone.origin", "destination_entity_id": "zone.destination", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -640,8 +623,7 @@ async def test_location_sensor(hass, requests_mock_truck_response): "name": "test", "origin_entity_id": "sensor.origin", "destination_entity_id": "sensor.destination", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -689,8 +671,7 @@ async def test_location_person(hass, requests_mock_truck_response): "name": "test", "origin_entity_id": "person.origin", "destination_entity_id": "person.destination", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -738,8 +719,7 @@ async def test_location_device_tracker(hass, requests_mock_truck_response): "name": "test", "origin_entity_id": "device_tracker.origin", "destination_entity_id": "device_tracker.destination", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -773,8 +753,7 @@ async def test_location_device_tracker_added_after_update( "name": "test", "origin_entity_id": "device_tracker.origin", "destination_entity_id": "device_tracker.destination", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -842,8 +821,7 @@ async def test_location_device_tracker_in_zone( "origin_entity_id": "device_tracker.origin", "destination_latitude": TRUCK_DESTINATION_LATITUDE, "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -863,7 +841,7 @@ async def test_route_not_found(hass, requests_mock_credentials_check, caplog): origin = "52.516,13.3779" destination = "47.013399,-10.171986" modes = [ROUTE_MODE_FASTEST, TRAVEL_MODE_CAR, TRAFFIC_MODE_DISABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/routing_error_no_route_found.json"), @@ -877,8 +855,7 @@ async def test_route_not_found(hass, requests_mock_credentials_check, caplog): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -901,8 +878,7 @@ async def test_pattern_origin(hass, caplog): "origin_longitude": "-77.04833", "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -921,8 +897,7 @@ async def test_pattern_destination(hass, caplog): "origin_longitude": CAR_ORIGIN_LONGITUDE, "destination_latitude": "139.0", "destination_longitude": "-77.1", - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -938,8 +913,7 @@ async def test_invalid_credentials(hass, requests_mock, caplog): ",".join([CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE]), ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, - APP_ID, - APP_CODE, + API_KEY, "now", ) requests_mock.get( @@ -955,8 +929,7 @@ async def test_invalid_credentials(hass, requests_mock, caplog): "origin_longitude": CAR_ORIGIN_LONGITUDE, "destination_latitude": CAR_DESTINATION_LATITUDE, "destination_longitude": CAR_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, } } assert await async_setup_component(hass, DOMAIN, config) @@ -969,7 +942,7 @@ async def test_attribution(hass, requests_mock_credentials_check): origin = "50.037751372637686,14.39233448220898" destination = "50.07993838201255,14.42582157361062" modes = [ROUTE_MODE_SHORTEST, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAFFIC_MODE_ENABLED] - response_url = _build_mock_url(origin, destination, modes, APP_ID, APP_CODE, "now") + response_url = _build_mock_url(origin, destination, modes, API_KEY, "now") requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/attribution_response.json") ) @@ -982,8 +955,7 @@ async def test_attribution(hass, requests_mock_credentials_check): "origin_longitude": origin.split(",")[1], "destination_latitude": destination.split(",")[0], "destination_longitude": destination.split(",")[1], - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "traffic_mode": True, "route_mode": ROUTE_MODE_SHORTEST, "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, @@ -1013,8 +985,7 @@ async def test_pattern_entity_state(hass, requests_mock_truck_response, caplog): "origin_entity_id": "sensor.origin", "destination_latitude": TRUCK_DESTINATION_LATITUDE, "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -1040,8 +1011,7 @@ async def test_pattern_entity_state_with_space(hass, requests_mock_truck_respons "origin_entity_id": "sensor.origin", "destination_latitude": TRUCK_DESTINATION_LATITUDE, "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } @@ -1059,8 +1029,7 @@ async def test_delayed_update(hass, requests_mock_truck_response, caplog): "origin_entity_id": "sensor.origin", "destination_latitude": TRUCK_DESTINATION_LATITUDE, "destination_longitude": TRUCK_DESTINATION_LONGITUDE, - "app_id": APP_ID, - "app_code": APP_CODE, + "api_key": API_KEY, "mode": TRAVEL_MODE_TRUCK, } } From 8fe17c0933a379c0466bf4ff5198bd4e878c19bd Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sun, 15 Dec 2019 23:21:29 -0800 Subject: [PATCH 2438/3953] Remove 'SUPPORT_PLAY_MEDIA' from Volumio (#29969) --- homeassistant/components/volumio/media_player.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index b25520c3f383cb..f62a74345b1dc9 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -21,7 +21,6 @@ SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, @@ -61,7 +60,6 @@ | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK - | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP From 9e51a18845bde3baa873dcd37c3e1b0a8476473e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Dec 2019 08:22:20 +0100 Subject: [PATCH 2439/3953] Make hassfest import detection better (#29932) * Make hassfest import detection better * Fix tests --- homeassistant/components/filter/manifest.json | 6 +- homeassistant/components/history/__init__.py | 4 +- script/hassfest/dependencies.py | 116 ++++++++++++----- tests/components/filter/test_sensor.py | 1 + tests/hassfest/__init__.py | 1 + tests/hassfest/test_dependencies.py | 120 ++++++++++++++++++ 6 files changed, 212 insertions(+), 36 deletions(-) create mode 100644 tests/hassfest/__init__.py create mode 100644 tests/hassfest/test_dependencies.py diff --git a/homeassistant/components/filter/manifest.json b/homeassistant/components/filter/manifest.json index f28007ba552882..f84a8b5192fc89 100644 --- a/homeassistant/components/filter/manifest.json +++ b/homeassistant/components/filter/manifest.json @@ -3,8 +3,6 @@ "name": "Filter", "documentation": "https://www.home-assistant.io/integrations/filter", "requirements": [], - "dependencies": [], - "codeowners": [ - "@dgomes" - ] + "dependencies": ["history"], + "codeowners": ["@dgomes"] } diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 133151c7f735f3..7fcbf519bf34d4 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -8,7 +8,7 @@ from sqlalchemy import and_, func import voluptuous as vol -from homeassistant.components import recorder, script +from homeassistant.components import recorder from homeassistant.components.http import HomeAssistantView from homeassistant.components.recorder.models import States from homeassistant.components.recorder.util import execute, session_scope @@ -430,4 +430,4 @@ def _is_significant(state): Will only test for things that are not filtered out in SQL. """ # scripts that are not cancellable will never change state - return state.domain != "script" or state.attributes.get(script.ATTR_CAN_CANCEL) + return state.domain != "script" or state.attributes.get("can_cancel") diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index b6f277438b405a..6ba228b5bc7a10 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -1,6 +1,5 @@ """Validate dependencies.""" -import pathlib -import re +import ast from typing import Dict, Set from homeassistant.requirements import DISCOVERY_INTEGRATIONS @@ -8,31 +7,80 @@ from .model import Integration -def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> Set[str]: - """Recursively go through a dir and it's children and find the regex.""" - pattern = re.compile(search_pattern) - found = set() +class ImportCollector(ast.NodeVisitor): + """Collect all integrations referenced.""" - for fil in path.glob(glob_pattern): - if not fil.is_file(): - continue - - for match in pattern.finditer(fil.read_text()): - integration = match.groups()[1] + def __init__(self, integration: Integration): + """Initialize the import collector.""" + self.integration = integration + self.referenced: Set[str] = set() - if ( - # If it's importing something from itself - integration == path.name - # Platform file - or (path / f"{integration}.py").exists() - # Dir for platform - or (path / integration).exists() - ): - continue - - found.add(match.groups()[1]) - - return found + def maybe_add_reference(self, reference_domain: str): + """Add a reference.""" + if ( + # If it's importing something from itself + reference_domain == self.integration.path.name + # Platform file + or (self.integration.path / f"{reference_domain}.py").exists() + # Platform dir + or (self.integration.path / reference_domain).exists() + ): + return + + self.referenced.add(reference_domain) + + def visit_ImportFrom(self, node): + """Visit ImportFrom node.""" + if node.module is None: + return + + if node.module.startswith("homeassistant.components."): + # from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME + # from homeassistant.components.logbook import bla + self.maybe_add_reference(node.module.split(".")[2]) + + elif node.module == "homeassistant.components": + # from homeassistant.components import sun + for name_node in node.names: + self.maybe_add_reference(name_node.name) + + def visit_Import(self, node): + """Visit Import node.""" + # import homeassistant.components.hue as hue + for name_node in node.names: + if name_node.name.startswith("homeassistant.components."): + self.maybe_add_reference(name_node.name.split(".")[2]) + + def visit_Attribute(self, node): + """Visit Attribute node.""" + # hass.components.hue.async_create() + # Name(id=hass) + # .Attribute(attr=hue) + # .Attribute(attr=async_create) + + # self.hass.components.hue.async_create() + # Name(id=self) + # .Attribute(attr=hass) + # .Attribute(attr=hue) + # .Attribute(attr=async_create) + if ( + isinstance(node.value, ast.Attribute) + and node.value.attr == "components" + and ( + ( + isinstance(node.value.value, ast.Name) + and node.value.value.id == "hass" + ) + or ( + isinstance(node.value.value, ast.Attribute) + and node.value.value.attr == "hass" + ) + ) + ): + self.maybe_add_reference(node.attr) + else: + # Have it visit other kids + self.generic_visit(node) ALLOWED_USED_COMPONENTS = { @@ -87,12 +135,20 @@ def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> Set[ def validate_dependencies(integration: Integration): """Validate all dependencies.""" # Find usage of hass.components - referenced = grep_dir( - integration.path, "**/*.py", r"(hass|homeassistant)\.components\.(\w+)" + collector = ImportCollector(integration) + + for fil in integration.path.glob("**/*.py"): + if not fil.is_file(): + continue + + collector.visit(ast.parse(fil.read_text())) + + referenced = ( + collector.referenced + - ALLOWED_USED_COMPONENTS + - set(integration.manifest["dependencies"]) + - set(integration.manifest.get("after_dependencies", [])) ) - referenced -= ALLOWED_USED_COMPONENTS - referenced -= set(integration.manifest["dependencies"]) - referenced -= set(integration.manifest.get("after_dependencies", [])) # Discovery requirements are ok if referenced in manifest for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 17e5bd8fd5d4f4..a5f23b464dd8ff 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -28,6 +28,7 @@ class TestFilterSensor(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.config.components.add("history") raw_values = [20, 19, 18, 21, 22, 0] self.values = [] diff --git a/tests/hassfest/__init__.py b/tests/hassfest/__init__.py new file mode 100644 index 00000000000000..1ec5a22a567c51 --- /dev/null +++ b/tests/hassfest/__init__.py @@ -0,0 +1 @@ +"""Tests for hassfest.""" diff --git a/tests/hassfest/test_dependencies.py b/tests/hassfest/test_dependencies.py new file mode 100644 index 00000000000000..0f07c45317e4fc --- /dev/null +++ b/tests/hassfest/test_dependencies.py @@ -0,0 +1,120 @@ +"""Tests for hassfest dependency finder.""" +import ast + +import pytest +from script.hassfest.dependencies import ImportCollector + + +@pytest.fixture +def mock_collector(): + """Fixture with import collector that adds all referenced nodes.""" + collector = ImportCollector(None) + collector.maybe_add_reference = collector.referenced.add + return collector + + +def test_child_import(mock_collector): + """Test detecting a child_import reference.""" + mock_collector.visit( + ast.parse( + """ +from homeassistant.components import child_import +""" + ) + ) + assert mock_collector.referenced == {"child_import"} + + +def test_subimport(mock_collector): + """Test detecting a subimport reference.""" + mock_collector.visit( + ast.parse( + """ +from homeassistant.components.subimport.smart_home import EVENT_ALEXA_SMART_HOME +""" + ) + ) + assert mock_collector.referenced == {"subimport"} + + +def test_child_import_field(mock_collector): + """Test detecting a child_import_field reference.""" + mock_collector.visit( + ast.parse( + """ +from homeassistant.components.child_import_field import bla +""" + ) + ) + assert mock_collector.referenced == {"child_import_field"} + + +def test_renamed_absolute(mock_collector): + """Test detecting a renamed_absolute reference.""" + mock_collector.visit( + ast.parse( + """ +import homeassistant.components.renamed_absolute as hue +""" + ) + ) + assert mock_collector.referenced == {"renamed_absolute"} + + +def test_hass_components_var(mock_collector): + """Test detecting a hass_components_var reference.""" + mock_collector.visit( + ast.parse( + """ +def bla(hass): + hass.components.hass_components_var.async_do_something() +""" + ) + ) + assert mock_collector.referenced == {"hass_components_var"} + + +def test_hass_components_class(mock_collector): + """Test detecting a hass_components_class reference.""" + mock_collector.visit( + ast.parse( + """ +class Hello: + def something(self): + self.hass.components.hass_components_class.async_yo() +""" + ) + ) + assert mock_collector.referenced == {"hass_components_class"} + + +def test_all_imports(mock_collector): + """Test all imports together.""" + mock_collector.visit( + ast.parse( + """ +from homeassistant.components import child_import + +from homeassistant.components.subimport.smart_home import EVENT_ALEXA_SMART_HOME + +from homeassistant.components.child_import_field import bla + +import homeassistant.components.renamed_absolute as hue + +def bla(hass): + hass.components.hass_components_var.async_do_something() + +class Hello: + def something(self): + self.hass.components.hass_components_class.async_yo() +""" + ) + ) + assert mock_collector.referenced == { + "child_import", + "subimport", + "child_import_field", + "renamed_absolute", + "hass_components_var", + "hass_components_class", + } From 3f32490ae62ba757e081e4a80d9a54a676647f62 Mon Sep 17 00:00:00 2001 From: Ryan <2199132+rsnodgrass@users.noreply.github.com> Date: Sun, 15 Dec 2019 23:41:32 -0800 Subject: [PATCH 2440/3953] Fixed "condtion_type" to "condition_type" (#29984) Fixed "condtion_type" to "condition_type" --- homeassistant/components/fan/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index 134119f41ffff4..98c3012c1232a7 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_on": "{entity_name} is on", "is_off": "{entity_name} is off" }, From b058742404a6bcac48b6a1df1336591f13206ce1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Dec 2019 09:33:11 +0100 Subject: [PATCH 2441/3953] Fix condition typo (#29989) --- homeassistant/components/climate/strings.json | 2 +- homeassistant/components/device_tracker/strings.json | 4 ++-- homeassistant/components/vacuum/strings.json | 2 +- script/scaffold/generate.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index a2ceeff2143853..ff071aed083d5d 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} is set to a specific HVAC mode", "is_preset_mode": "{entity_name} is set to a specific preset mode" }, diff --git a/homeassistant/components/device_tracker/strings.json b/homeassistant/components/device_tracker/strings.json index 7e0691654a09b9..285bac2cb4bbd0 100644 --- a/homeassistant/components/device_tracker/strings.json +++ b/homeassistant/components/device_tracker/strings.json @@ -1,8 +1,8 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} is home", "is_not_home": "{entity_name} is not home" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 0300242a506da4..4eee3f359b5d3e 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_docked": "{entity_name} is docked", "is_cleaning": "{entity_name} is cleaning" }, diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 59767249d29799..b2f669006a9032 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -92,7 +92,7 @@ def _custom_tasks(template, info) -> None: info.update_strings( device_automation={ **info.strings().get("device_automation", {}), - "condtion_type": { + "condition_type": { "is_on": "{entity_name} is on", "is_off": "{entity_name} is off", }, From 33cbb398ad9873cc3ac7fc1f5d5c5b11b0721ccd Mon Sep 17 00:00:00 2001 From: Louis-Dominique Dubeau Date: Mon, 16 Dec 2019 03:39:20 -0500 Subject: [PATCH 2442/3953] Don't use the locals parameter on exec. (#29979) Using the locals parameter makes it so that the code of a Python script runs as if it were in the body of a ``class``. One effect of this is that functions defined as part of a script cannot call one another directly. Fixes: #24704, #13653 --- .../components/python_script/__init__.py | 8 ++++--- tests/components/python_script/test_init.py | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index e36ac397c0fa7a..ddae8a81db159d 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -175,6 +175,7 @@ def protected_getattr(obj, name, default=None): builtins["sorted"] = sorted builtins["time"] = TimeWrapper() builtins["dt_util"] = dt_util + logger = logging.getLogger(f"{__name__}.{filename}") restricted_globals = { "__builtins__": builtins, "_print_": StubPrinter, @@ -184,14 +185,15 @@ def protected_getattr(obj, name, default=None): "_getitem_": default_guarded_getitem, "_iter_unpack_sequence_": guarded_iter_unpack_sequence, "_unpack_sequence_": guarded_unpack_sequence, + "hass": hass, + "data": data or {}, + "logger": logger, } - logger = logging.getLogger(f"{__name__}.{filename}") - local = {"hass": hass, "data": data or {}, "logger": logger} try: _LOGGER.info("Executing %s: %s", filename, data) # pylint: disable=exec-used - exec(compiled.code, restricted_globals, local) + exec(compiled.code, restricted_globals) except ScriptError as err: logger.error("Error executing script: %s", err) except Exception as err: # pylint: disable=broad-except diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 75616c7400a701..b5879479837588 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -258,6 +258,29 @@ def test_exposed_modules(hass, caplog): assert caplog.text == "" +@asyncio.coroutine +def test_execute_functions(hass, caplog): + """Test functions defined in script can call one another.""" + caplog.set_level(logging.ERROR) + source = """ +def a(): + hass.states.set('hello.a', 'one') + +def b(): + a() + hass.states.set('hello.b', 'two') + +b() +""" + hass.async_add_job(execute, hass, "test.py", source, {}) + yield from hass.async_block_till_done() + + assert hass.states.is_state("hello.a", "one") + assert hass.states.is_state("hello.b", "two") + # No errors logged = good + assert caplog.text == "" + + @asyncio.coroutine def test_reload(hass): """Test we can re-discover scripts.""" From be042f3d91ec3e5a0a92a607c0a4df1061058ad2 Mon Sep 17 00:00:00 2001 From: Emacee Date: Mon, 16 Dec 2019 09:42:48 +0100 Subject: [PATCH 2443/3953] Update binary_sensor.py (#29977) Change sensor type for central locking from safety into lock. --- homeassistant/components/bmw_connected_drive/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index d0cb4289d37db0..591cdadda35251 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -13,7 +13,7 @@ SENSOR_TYPES = { "lids": ["Doors", "opening", "mdi:car-door-lock"], "windows": ["Windows", "opening", "mdi:car-door"], - "door_lock_state": ["Door lock state", "safety", "mdi:car-key"], + "door_lock_state": ["Door lock state", "lock", "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"], From dd0f0034f34a01df6a156779ca29712e8d4484a3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Dec 2019 10:57:42 +0100 Subject: [PATCH 2444/3953] Bump shodan to 1.21.0 (#29991) --- homeassistant/components/shodan/manifest.json | 10 +++------- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 36af63da9f8912..0ff3e44ece5973 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,11 +2,7 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": [ - "shodan==1.20.0" - ], + "requirements": ["shodan==1.21.0"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] -} \ No newline at end of file + "codeowners": ["@fabaff"] +} diff --git a/requirements_all.txt b/requirements_all.txt index 873e056eb72a0b..a8d454aac99514 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1795,7 +1795,7 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.20.0 +shodan==1.21.0 # homeassistant.components.simplepush simplepush==1.1.4 From d1e59b20c8b779a52710b81a855cf4077134d7bd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Dec 2019 10:59:32 +0100 Subject: [PATCH 2445/3953] Bump pytest to 5.3.2 (#29990) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 70e78222644bef..328ad1d5b9a538 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,6 +14,6 @@ pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.3.1 +pytest==5.3.2 requests_mock==1.7.0 responses==0.10.6 From 87ca61ddd7e5316ba9e703e8145dfeeaf5a024fd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Dec 2019 11:06:17 +0100 Subject: [PATCH 2446/3953] Add check-json to CI and Pre-commit (#29912) * Add check-json to CI and Pre-commit * Add ignore pre-commit hooks to gen_requirements_all --- .pre-commit-config-all.yaml | 4 ++++ .pre-commit-config.yaml | 6 +++++- azure-pipelines-ci.yml | 4 ++++ script/gen_requirements_all.py | 7 +++++-- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml index ec9492e0210d9e..1eabfcb0017cde 100644 --- a/.pre-commit-config-all.yaml +++ b/.pre-commit-config-all.yaml @@ -39,6 +39,10 @@ repos: rev: v4.3.21 hooks: - id: isort +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: check-json # Using a local "system" mypy instead of the mypy hook, because its # results depend on what is installed. And the mypy hook runs in a # virtualenv of its own, meaning we'd need to install and maintain diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23d1d9c73f9cf3..226708bb94712a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: - repo: https://github.com/PyCQA/bandit rev: 1.6.2 hooks: - - id: bandit + - id: bandit args: - --quiet - --format=custom @@ -35,3 +35,7 @@ repos: rev: v4.3.21 hooks: - id: isort +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: check-json diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 464b1079957c3f..78de90c85528dc 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -56,6 +56,10 @@ stages: . venv/bin/activate pre-commit run isort --all-files displayName: 'Run isort' + - script: | + . venv/bin/activate + pre-commit run check-json --all-files + displayName: 'Run check-json' - job: 'Validate' pool: vmImage: 'ubuntu-latest' diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 0dfefc958c35d6..f40a89a9d9a278 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -65,6 +65,8 @@ pycrypto==1000000000.0.0 """ +IGNORE_PRE_COMMIT_HOOK_ID = ("check-json",) + def has_tests(module: str): """Test if a module has tests. @@ -256,8 +258,9 @@ def requirements_pre_commit_output(): reqs = [] for repo in (x for x in pre_commit_conf["repos"] if x.get("rev")): for hook in repo["hooks"]: - reqs.append(f"{hook['id']}=={repo['rev']}") - reqs.extend(x for x in hook.get("additional_dependencies", ())) + if hook["id"] not in IGNORE_PRE_COMMIT_HOOK_ID: + reqs.append(f"{hook['id']}=={repo['rev']}") + reqs.extend(x for x in hook.get("additional_dependencies", ())) output = [ f"# Automatically generated " f"from {source} by {Path(__file__).name}, do not edit", From d851cb6f9e116dddb8ebeed4dd3910eb811993bd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Dec 2019 12:27:43 +0100 Subject: [PATCH 2447/3953] Add unique ID to config entries (#29806) * Add unique ID to config entries * Unload existing entries with same unique ID if flow with unique ID is finished * Remove unused exception * Fix typing * silence pylint * Fix tests * Add unique ID to Hue * Address typing comment * Tweaks to comments * lint --- homeassistant/components/hue/__init__.py | 15 ++- homeassistant/components/hue/bridge.py | 22 ++++ homeassistant/components/hue/config_flow.py | 31 +++--- homeassistant/config_entries.py | 61 ++++++++++- homeassistant/data_entry_flow.py | 46 +++++++-- tests/common.py | 2 + tests/components/hue/test_config_flow.py | 35 ++++--- tests/components/hue/test_init.py | 16 +++ tests/test_config_entries.py | 107 ++++++++++++++++++++ tests/test_data_entry_flow.py | 16 ++- 10 files changed, 305 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index f2b9bd1a229f45..57057004479a96 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -4,11 +4,11 @@ import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, core from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.helpers import config_validation as cv, device_registry as dr -from .bridge import HueBridge +from .bridge import HueBridge, normalize_bridge_id from .config_flow import ( # Loading the config flow file will register the flow configured_hosts, ) @@ -102,7 +102,9 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): """Set up a bridge from a config entry.""" host = entry.data["host"] config = hass.data[DATA_CONFIGS].get(host) @@ -121,6 +123,13 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN][host] = bridge config = bridge.api.config + + # For backwards compat + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=normalize_bridge_id(config.bridgeid) + ) + device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 5a5e55773a5da0..0ed6e3a9911a74 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -201,3 +201,25 @@ async def get_bridge(hass, host, username=None): except aiohue.AiohueException: LOGGER.exception("Unknown Hue linking error occurred") raise AuthenticationRequired + + +def normalize_bridge_id(bridge_id: str): + """Normalize a bridge identifier. + + There are three sources where we receive bridge ID from: + - ssdp/upnp: /description.xml, field root/device/serialNumber + - nupnp: "id" field + - Hue Bridge API: config.bridgeid + + The SSDP/UPNP source does not contain the middle 4 characters compared + to the other sources. In all our tests the middle 4 characters are "fffe". + """ + if len(bridge_id) == 16: + return bridge_id[0:6] + bridge_id[-6:] + + if len(bridge_id) == 12: + return bridge_id + + LOGGER.warning("Unexpected bridge id number found: %s", bridge_id) + + return bridge_id diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 0423dc6fc2bd6c..882bf5b70db588 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .bridge import get_bridge +from .bridge import get_bridge, normalize_bridge_id from .const import DOMAIN, LOGGER from .errors import AuthenticationRequired, CannotConnect @@ -154,17 +154,15 @@ async def async_step_ssdp(self, discovery_info): if host in configured_hosts(self.hass): return self.async_abort(reason="already_configured") - # This value is based off host/description.xml and is, weirdly, missing - # 4 characters in the middle of the serial compared to results returned - # from the NUPNP API or when querying the bridge API for bridgeid. - # (on first gen Hue hub) - serial = discovery_info.get("serial") + bridge_id = discovery_info.get("serial") + + await self.async_set_unique_id(normalize_bridge_id(bridge_id)) return await self.async_step_import( { "host": host, # This format is the legacy format that Hue used for discovery - "path": f"phue-{serial}.conf", + "path": f"phue-{bridge_id}.conf", } ) @@ -180,6 +178,10 @@ async def async_step_homekit(self, homekit_info): if host in configured_hosts(self.hass): return self.async_abort(reason="already_configured") + await self.async_set_unique_id( + normalize_bridge_id(homekit_info["properties"]["id"].replace(":", "")) + ) + return await self.async_step_import({"host": host}) async def async_step_import(self, import_info): @@ -234,18 +236,9 @@ async def _entry_from_bridge(self, bridge): host = bridge.host bridge_id = bridge.config.bridgeid - same_hub_entries = [ - entry.entry_id - for entry in self.hass.config_entries.async_entries(DOMAIN) - if entry.data["bridge_id"] == bridge_id or entry.data["host"] == host - ] - - if same_hub_entries: - await asyncio.wait( - [ - self.hass.config_entries.async_remove(entry_id) - for entry_id in same_hub_entries - ] + if self.unique_id is None: + await self.async_set_unique_id( + normalize_bridge_id(bridge_id), raise_on_progress=False ) return self.async_create_entry( diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 07a287c387cb96..09ee186da0ffd6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2,7 +2,7 @@ import asyncio import functools import logging -from typing import Any, Callable, Dict, List, Optional, Set, cast +from typing import Any, Callable, Dict, List, Optional, Set, Union, cast import uuid import weakref @@ -75,6 +75,10 @@ class OperationNotAllowed(ConfigError): """Raised when a config entry operation is not allowed.""" +class UniqueIdInProgress(data_entry_flow.AbortFlow): + """Error to indicate that the unique Id is in progress.""" + + class ConfigEntry: """Hold a configuration entry.""" @@ -85,6 +89,7 @@ class ConfigEntry: "title", "data", "options", + "unique_id", "system_options", "source", "connection_class", @@ -104,6 +109,7 @@ def __init__( connection_class: str, system_options: dict, options: Optional[dict] = None, + unique_id: Optional[str] = None, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED, ) -> None: @@ -138,6 +144,9 @@ def __init__( # State of the entry (LOADED, NOT_LOADED) self.state = state + # Unique ID of this entry. + self.unique_id = unique_id + # Listeners to call on update self.update_listeners: List = [] @@ -533,11 +542,15 @@ def async_update_entry( self, entry: ConfigEntry, *, + unique_id: Union[str, dict, None] = _UNDEF, data: dict = _UNDEF, options: dict = _UNDEF, system_options: dict = _UNDEF, ) -> None: """Update a config entry.""" + if unique_id is not _UNDEF: + entry.unique_id = cast(Optional[str], unique_id) + if data is not _UNDEF: entry.data = data @@ -602,6 +615,25 @@ async def _async_finish_flow( if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: return result + # Check if config entry exists with unique ID. Unload it. + existing_entry = None + unique_id = flow.context.get("unique_id") + + if unique_id is not None: + for check_entry in self.async_entries(result["handler"]): + if check_entry.unique_id == unique_id: + existing_entry = check_entry + break + + # Unload the entry before setting up the new one. + # We will remove it only after the other one is set up, + # so that device customizations are not getting lost. + if ( + existing_entry is not None + and existing_entry.state not in UNRECOVERABLE_STATES + ): + await self.async_unload(existing_entry.entry_id) + entry = ConfigEntry( version=result["version"], domain=result["handler"], @@ -611,12 +643,16 @@ async def _async_finish_flow( system_options={}, source=flow.context["source"], connection_class=flow.CONNECTION_CLASS, + unique_id=unique_id, ) self._entries.append(entry) self._async_schedule_save() await self.async_setup(entry.entry_id) + if existing_entry is not None: + await self.async_remove(existing_entry.entry_id) + result["result"] = entry return result @@ -687,6 +723,8 @@ async def _old_conf_migrator(old_config: Dict[str, Any]) -> Dict[str, Any]: class ConfigFlow(data_entry_flow.FlowHandler): """Base class for config flows with some helpers.""" + unique_id = None + def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None: """Initialize a subclass, register if possible.""" super().__init_subclass__(**kwargs) # type: ignore @@ -701,6 +739,27 @@ def async_get_options_flow(config_entry: ConfigEntry) -> "OptionsFlow": """Get the options flow for this handler.""" raise data_entry_flow.UnknownHandler + async def async_set_unique_id( + self, unique_id: str, *, raise_on_progress: bool = True + ) -> Optional[ConfigEntry]: + """Set a unique ID for the config flow. + + Returns optionally existing config entry with same ID. + """ + if raise_on_progress: + for progress in self._async_in_progress(): + if progress["context"].get("unique_id") == unique_id: + raise UniqueIdInProgress("already_in_progress") + + # pylint: disable=no-member + self.context["unique_id"] = unique_id + + for entry in self._async_current_entries(): + if entry.unique_id == unique_id: + return entry + + return None + @callback def _async_current_entries(self) -> List[ConfigEntry]: """Return current entries.""" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index e7432cd52f75d6..7c2b4ab6ddc603 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,6 +1,6 @@ """Classes to help gather user submissions.""" import logging -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional, cast import uuid import voluptuous as vol @@ -36,6 +36,16 @@ class UnknownStep(FlowError): """Unknown step specified.""" +class AbortFlow(FlowError): + """Exception to indicate a flow needs to be aborted.""" + + def __init__(self, reason: str, description_placeholders: Optional[Dict] = None): + """Initialize an abort flow exception.""" + super().__init__(f"Flow aborted: {reason}") + self.reason = reason + self.description_placeholders = description_placeholders + + class FlowManager: """Manage all the flows that are in progress.""" @@ -131,7 +141,12 @@ async def _async_handle_step( ) ) - result: Dict = await getattr(flow, method)(user_input) + try: + result: Dict = await getattr(flow, method)(user_input) + except AbortFlow as err: + result = _create_abort_data( + flow.flow_id, flow.handler, err.reason, err.description_placeholders + ) if result["type"] not in ( RESULT_TYPE_FORM, @@ -228,13 +243,9 @@ def async_abort( self, *, reason: str, description_placeholders: Optional[Dict] = None ) -> Dict[str, Any]: """Abort the config flow.""" - return { - "type": RESULT_TYPE_ABORT, - "flow_id": self.flow_id, - "handler": self.handler, - "reason": reason, - "description_placeholders": description_placeholders, - } + return _create_abort_data( + self.flow_id, cast(str, self.handler), reason, description_placeholders + ) @callback def async_external_step( @@ -259,3 +270,20 @@ def async_external_step_done(self, *, next_step_id: str) -> Dict[str, Any]: "handler": self.handler, "step_id": next_step_id, } + + +@callback +def _create_abort_data( + flow_id: str, + handler: str, + reason: str, + description_placeholders: Optional[Dict] = None, +) -> Dict[str, Any]: + """Return the definition of an external step for the user to take.""" + return { + "type": RESULT_TYPE_ABORT, + "flow_id": flow_id, + "handler": handler, + "reason": reason, + "description_placeholders": description_placeholders, + } diff --git a/tests/common.py b/tests/common.py index a54b3899698f36..5d13da74e8800a 100644 --- a/tests/common.py +++ b/tests/common.py @@ -671,6 +671,7 @@ def __init__( options={}, system_options={}, connection_class=config_entries.CONN_CLASS_UNKNOWN, + unique_id=None, ): """Initialize a mock config entry.""" kwargs = { @@ -682,6 +683,7 @@ def __init__( "version": version, "title": title, "connection_class": connection_class, + "unique_id": unique_id, } if source is not None: kwargs["source"] = source diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index a6d221ef323446..030f6ade1fa70c 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -19,6 +19,7 @@ async def test_flow_works(hass, aioclient_mock): flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} await flow.async_step_init() with patch("aiohue.Bridge") as mock_bridge: @@ -349,28 +350,33 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): accessible via a single IP. So when we create a new entry, we'll remove all existing entries that either have same IP or same bridge_id. """ - MockConfigEntry( - domain="hue", data={"host": "0.0.0.0", "bridge_id": "id-1234"} - ).add_to_hass(hass) + orig_entry = MockConfigEntry( + domain="hue", + data={"host": "0.0.0.0", "bridge_id": "id-1234"}, + unique_id="id-1234", + ) + orig_entry.add_to_hass(hass) MockConfigEntry( - domain="hue", data={"host": "1.2.3.4", "bridge_id": "id-1234"} + domain="hue", + data={"host": "1.2.3.4", "bridge_id": "id-5678"}, + unique_id="id-5678", ).add_to_hass(hass) assert len(hass.config_entries.async_entries("hue")) == 2 - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - bridge = Mock() bridge.username = "username-abc" bridge.config.bridgeid = "id-1234" bridge.config.name = "Mock Bridge" bridge.host = "0.0.0.0" - with patch.object(config_flow, "get_bridge", return_value=mock_coro(bridge)): - result = await flow.async_step_import({"host": "0.0.0.0"}) + with patch.object( + config_flow, "_find_username_from_config", return_value="mock-user" + ), patch.object(config_flow, "get_bridge", return_value=mock_coro(bridge)): + result = await hass.config_entries.flow.async_init( + "hue", data={"host": "2.2.2.2"}, context={"source": "import"} + ) assert result["type"] == "create_entry" assert result["title"] == "Mock Bridge" @@ -379,9 +385,11 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): "bridge_id": "id-1234", "username": "username-abc", } - # We did not process the result of this entry but already removed the old - # ones. So we should have 0 entries. - assert len(hass.config_entries.async_entries("hue")) == 0 + entries = hass.config_entries.async_entries("hue") + assert len(entries) == 2 + new_entry = entries[-1] + assert orig_entry.entry_id != new_entry.entry_id + assert new_entry.unique_id == "id-1234" async def test_bridge_homekit(hass): @@ -398,6 +406,7 @@ async def test_bridge_homekit(hass): "host": "0.0.0.0", "serial": "1234", "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + "properties": {"id": "aa:bb:cc:dd:ee:ff"}, } ) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 58f004ec540225..d064ff9f340688 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -175,3 +175,19 @@ async def test_unload_entry(hass): assert await hue.async_unload_entry(hass, entry) assert len(mock_bridge.return_value.async_reset.mock_calls) == 1 assert hass.data[hue.DOMAIN] == {} + + +async def test_setting_unique_id(hass): + """Test we set unique ID if not set yet.""" + entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) + entry.add_to_hass(hass) + + with patch.object(hue, "HueBridge") as mock_bridge, patch( + "homeassistant.helpers.device_registry.async_get_registry", + return_value=mock_coro(Mock()), + ): + mock_bridge.return_value.async_setup.return_value = mock_coro(True) + mock_bridge.return_value.api.config = Mock(bridgeid="mock-id") + assert await async_setup_component(hass, hue.DOMAIN, {}) is True + + assert entry.unique_id == "mock-id" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 24a0b0939bef40..a9ae4eb59ac224 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1001,3 +1001,110 @@ async def test_reload_entry_entity_registry_works(hass): await hass.async_block_till_done() assert len(mock_unload_entry.mock_calls) == 1 + + +async def test_unqiue_id_persisted(hass, manager): + """Test that a unique ID is stored in the config entry.""" + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + + mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + return self.async_create_entry(title="mock-title", data={}) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + + assert len(mock_setup_entry.mock_calls) == 1 + p_hass, p_entry = mock_setup_entry.mock_calls[0][1] + + assert p_hass is hass + assert p_entry.unique_id == "mock-unique-id" + + +async def test_unique_id_existing_entry(hass, manager): + """Test that we remove an entry if there already is an entry with unique ID.""" + hass.config.components.add("comp") + MockConfigEntry( + domain="comp", + state=config_entries.ENTRY_STATE_LOADED, + unique_id="mock-unique-id", + ).add_to_hass(hass) + + async_setup_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) + async_unload_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) + async_remove_entry = MagicMock(side_effect=lambda _, _2: mock_coro(True)) + + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=async_setup_entry, + async_unload_entry=async_unload_entry, + async_remove_entry=async_remove_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + existing_entry = await self.async_set_unique_id("mock-unique-id") + + assert existing_entry is not None + + return self.async_create_entry(title="mock-title", data={"via": "flow"}) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entries = hass.config_entries.async_entries("comp") + assert len(entries) == 1 + assert entries[0].data == {"via": "flow"} + + assert len(async_setup_entry.mock_calls) == 1 + assert len(async_unload_entry.mock_calls) == 1 + assert len(async_remove_entry.mock_calls) == 1 + + +async def test_unique_id_in_progress(hass, manager): + """Test that we abort if there is already a flow in progress with same unique id.""" + mock_integration(hass, MockModule("comp")) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + return self.async_show_form(step_id="discovery") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + # Create one to be in progress + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + # Will be canceled + result2 = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 175efebd755480..a6bdd2b5cb6957 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -94,7 +94,7 @@ async def async_step_second(self, user_input=None): async def test_show_form(manager): - """Test that abort removes the flow from progress.""" + """Test that we can show a form.""" schema = vol.Schema({vol.Required("username"): str, vol.Required("password"): str}) @manager.mock_reg_handler("test") @@ -271,3 +271,17 @@ async def async_step_finish(self, user_input=None): result = await manager.async_configure(result["flow_id"]) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Hello" + + +async def test_abort_flow_exception(manager): + """Test that the AbortFlow exception works.""" + + @manager.mock_reg_handler("test") + class TestFlow(data_entry_flow.FlowHandler): + async def async_step_init(self, user_input=None): + raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"}) + + form = await manager.async_init("test") + assert form["type"] == "abort" + assert form["reason"] == "mock-reason" + assert form["description_placeholders"] == {"placeholder": "yo"} From 575eb48febe9a5eeff424faa82209d96bce15eef Mon Sep 17 00:00:00 2001 From: zewelor Date: Mon, 16 Dec 2019 16:23:05 +0100 Subject: [PATCH 2448/3953] Show current effect in yeelight device (#28975) * Show current effect in yeelight device * Use device_state_attributes instead of state_attributes * Add early return in set effect * Make single if elif chain * Fix if elif * Fix if elif --- homeassistant/components/yeelight/__init__.py | 11 +++ homeassistant/components/yeelight/light.py | 93 ++++++++++++------- 2 files changed, 68 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index ddd3cf2a0535da..b947f6b448c33b 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -49,6 +49,7 @@ ACTION_OFF = "off" ACTIVE_MODE_NIGHTLIGHT = "1" +ACTIVE_COLOR_FLOWING = "1" NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" @@ -124,6 +125,7 @@ "hue", "sat", "color_mode", + "flowing", "bg_power", "bg_lmode", "bg_flowing", @@ -251,10 +253,19 @@ def is_nightlight_supported(self) -> bool: return self._active_mode is not None + @property + def is_color_flow_enabled(self) -> bool: + """Return true / false if color flow is currently running.""" + return self._color_flow == ACTIVE_COLOR_FLOWING + @property def _active_mode(self): return self.bulb.last_properties.get("active_mode") + @property + def _color_flow(self): + return self.bulb.last_properties.get("flowing") + @property def type(self): """Return bulb type.""" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 3e98403587f20f..0039755a6af518 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -142,6 +142,21 @@ "ceiling4": BulbType.WhiteTempMood, } +EFFECTS_MAP = { + EFFECT_DISCO: yee_transitions.disco, + EFFECT_TEMP: yee_transitions.temp, + EFFECT_STROBE: yee_transitions.strobe, + EFFECT_STROBE_COLOR: yee_transitions.strobe_color, + EFFECT_ALARM: yee_transitions.alarm, + EFFECT_POLICE: yee_transitions.police, + EFFECT_POLICE2: yee_transitions.police2, + EFFECT_CHRISTMAS: yee_transitions.christmas, + EFFECT_RGB: yee_transitions.rgb, + EFFECT_RANDOM_LOOP: yee_transitions.randomloop, + EFFECT_LSD: yee_transitions.lsd, + EFFECT_SLOWDOWN: yee_transitions.slowdown, +} + VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100)) SERVICE_SCHEMA_SET_MODE = YEELIGHT_SERVICE_SCHEMA.extend( @@ -416,6 +431,7 @@ def __init__(self, device, custom_effects=None): self._brightness = None self._color_temp = None self._hs = None + self._effect = None model_specs = self._bulb.get_model_specs() self._min_mireds = kelvin_to_mired(model_specs["color_temp"]["max"]) @@ -516,6 +532,11 @@ def hs_color(self) -> tuple: """Return the color property.""" return self._hs + @property + def effect(self): + """Return the current effect.""" + return self._effect + # F821: https://github.com/PyCQA/pyflakes/issues/373 @property def _bulb(self) -> "Bulb": # noqa: F821 @@ -546,6 +567,16 @@ def _turn_on_power_mode(self): def _predefined_effects(self): return YEELIGHT_MONO_EFFECT_LIST + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + + attributes = {"flowing": self.device.is_color_flow_enabled} + if self.device.is_nightlight_supported: + attributes["night_light"] = self.device.is_nightlight_enabled + + return attributes + @property def device(self): """Return yeelight device.""" @@ -554,6 +585,8 @@ def device(self): def update(self): """Update light properties.""" self._hs = self._get_hs_from_properties() + if not self.device.is_color_flow_enabled: + self._effect = None def _get_hs_from_properties(self): rgb = self._get_property("rgb") @@ -658,45 +691,33 @@ def set_flash(self, flash) -> None: @_cmd def set_effect(self, effect) -> None: """Activate effect.""" - if effect: - if effect == EFFECT_STOP: - self._bulb.stop_flow(light_type=self.light_type) - return + if not effect: + return - effects_map = { - EFFECT_DISCO: yee_transitions.disco, - EFFECT_TEMP: yee_transitions.temp, - EFFECT_STROBE: yee_transitions.strobe, - EFFECT_STROBE_COLOR: yee_transitions.strobe_color, - EFFECT_ALARM: yee_transitions.alarm, - EFFECT_POLICE: yee_transitions.police, - EFFECT_POLICE2: yee_transitions.police2, - EFFECT_CHRISTMAS: yee_transitions.christmas, - EFFECT_RGB: yee_transitions.rgb, - EFFECT_RANDOM_LOOP: yee_transitions.randomloop, - EFFECT_LSD: yee_transitions.lsd, - EFFECT_SLOWDOWN: yee_transitions.slowdown, - } + if effect == EFFECT_STOP: + self._bulb.stop_flow(light_type=self.light_type) + return - if effect in self.custom_effects_names: - flow = Flow(**self.custom_effects[effect]) - elif effect in effects_map: - flow = Flow(count=0, transitions=effects_map[effect]()) - elif effect == EFFECT_FAST_RANDOM_LOOP: - flow = Flow( - count=0, transitions=yee_transitions.randomloop(duration=250) - ) - elif effect == EFFECT_WHATSAPP: - flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) - elif effect == EFFECT_FACEBOOK: - flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152)) - elif effect == EFFECT_TWITTER: - flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237)) + if effect in self.custom_effects_names: + flow = Flow(**self.custom_effects[effect]) + elif effect in EFFECTS_MAP: + flow = Flow(count=0, transitions=EFFECTS_MAP[effect]()) + elif effect == EFFECT_FAST_RANDOM_LOOP: + flow = Flow(count=0, transitions=yee_transitions.randomloop(duration=250)) + elif effect == EFFECT_WHATSAPP: + flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) + elif effect == EFFECT_FACEBOOK: + flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152)) + elif effect == EFFECT_TWITTER: + flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237)) + else: + return - try: - self._bulb.start_flow(flow, light_type=self.light_type) - except BulbException as ex: - _LOGGER.error("Unable to set effect: %s", ex) + try: + self._bulb.start_flow(flow, light_type=self.light_type) + self._effect = effect + except BulbException as ex: + _LOGGER.error("Unable to set effect: %s", ex) def turn_on(self, **kwargs) -> None: """Turn the bulb on.""" From 58b5833d64cd66401d313206595ce582125f8f9f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Dec 2019 19:45:09 +0100 Subject: [PATCH 2449/3953] Convert Hue to use unique ID (#30000) * Convert Hue to use unique ID * Fix normalization * Store/restore unique ID * Fix tests --- homeassistant/components/hue/__init__.py | 32 ++- homeassistant/components/hue/bridge.py | 43 ++-- homeassistant/components/hue/config_flow.py | 233 ++++++++----------- homeassistant/components/hue/manifest.json | 12 +- homeassistant/config_entries.py | 58 ++++- homeassistant/core.py | 3 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/test_bridge.py | 83 +++---- tests/components/hue/test_config_flow.py | 235 ++++++++------------ tests/components/hue/test_init.py | 60 ++--- tests/test_config_entries.py | 42 +++- 12 files changed, 375 insertions(+), 430 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 57057004479a96..7239efafd10a07 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -2,16 +2,14 @@ import ipaddress import logging +from aiohue.util import normalize_bridge_id import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.const import CONF_FILENAME, CONF_HOST +from homeassistant.const import CONF_HOST from homeassistant.helpers import config_validation as cv, device_registry as dr -from .bridge import HueBridge, normalize_bridge_id -from .config_flow import ( # Loading the config flow file will register the flow - configured_hosts, -) +from .bridge import HueBridge from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -32,8 +30,6 @@ { # Validate as IP address and then convert back to a string. vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), - # This is for legacy reasons and is only used for importing auth. - vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string, vol.Optional( CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE ): cv.boolean, @@ -65,7 +61,6 @@ async def async_setup(hass, config): hass.data[DOMAIN] = {} hass.data[DATA_CONFIGS] = {} - configured = configured_hosts(hass) # User has configured bridges if CONF_BRIDGES not in conf: @@ -73,29 +68,28 @@ async def async_setup(hass, config): bridges = conf[CONF_BRIDGES] + configured_hosts = set( + entry.data["host"] for entry in hass.config_entries.async_entries(DOMAIN) + ) + for bridge_conf in bridges: host = bridge_conf[CONF_HOST] # Store config in hass.data so the config entry can find it hass.data[DATA_CONFIGS][host] = bridge_conf - # If configured, the bridge will be set up during config entry phase - if host in configured: + if host in configured_hosts: continue - # No existing config entry found, try importing it or trigger link - # config flow if no existing auth. Because we're inside the setup of - # this component we'll have to use hass.async_add_job to avoid a - # deadlock: creating a config entry will set up the component but the - # setup would block till the entry is created! + # No existing config entry found, trigger link config flow. Because we're + # inside the setup of this component we'll have to use hass.async_add_job + # to avoid a deadlock: creating a config entry will set up the component + # but the setup would block till the entry is created! hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={ - "host": bridge_conf[CONF_HOST], - "path": bridge_conf[CONF_FILENAME], - }, + data={"host": bridge_conf[CONF_HOST]}, ) ) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 0ed6e3a9911a74..58a744dd5b0feb 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -6,6 +6,7 @@ import slugify as unicode_slug import voluptuous as vol +from homeassistant import core from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv @@ -45,8 +46,15 @@ async def async_setup(self, tries=0): host = self.host hass = self.hass + bridge = aiohue.Bridge( + host, + username=self.config_entry.data["username"], + websession=aiohttp_client.async_get_clientsession(hass), + ) + try: - self.api = await get_bridge(hass, host, self.config_entry.data["username"]) + await authenticate_bridge(hass, bridge) + except AuthenticationRequired: # Usernames can become invalid if hub is reset or user removed. # We are going to fail the config entry setup and initiate a new @@ -63,6 +71,8 @@ async def async_setup(self, tries=0): LOGGER.exception("Unknown error connecting with Hue bridge at %s", host) return False + self.api = bridge + hass.async_create_task( hass.config_entries.async_forward_entry_setup(self.config_entry, "light") ) @@ -175,16 +185,12 @@ async def handle_unauthorized_error(self): create_config_flow(self.hass, self.host) -async def get_bridge(hass, host, username=None): +async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge): """Create a bridge object and verify authentication.""" - bridge = aiohue.Bridge( - host, username=username, websession=aiohttp_client.async_get_clientsession(hass) - ) - try: with async_timeout.timeout(10): # Create username if we don't have one - if not username: + if not bridge.username: device_name = unicode_slug.slugify( hass.config.location_name, max_length=19 ) @@ -193,7 +199,6 @@ async def get_bridge(hass, host, username=None): # Initialize bridge (and validate our username) await bridge.initialize() - return bridge except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): raise AuthenticationRequired except (asyncio.TimeoutError, aiohue.RequestError): @@ -201,25 +206,3 @@ async def get_bridge(hass, host, username=None): except aiohue.AiohueException: LOGGER.exception("Unknown Hue linking error occurred") raise AuthenticationRequired - - -def normalize_bridge_id(bridge_id: str): - """Normalize a bridge identifier. - - There are three sources where we receive bridge ID from: - - ssdp/upnp: /description.xml, field root/device/serialNumber - - nupnp: "id" field - - Hue Bridge API: config.bridgeid - - The SSDP/UPNP source does not contain the middle 4 characters compared - to the other sources. In all our tests the middle 4 characters are "fffe". - """ - if len(bridge_id) == 16: - return bridge_id[0:6] + bridge_id[-6:] - - if len(bridge_id) == 12: - return bridge_id - - LOGGER.warning("Unexpected bridge id number found: %s", bridge_id) - - return bridge_id diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 882bf5b70db588..f2d7c6d1e8a1a7 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -1,51 +1,24 @@ """Config flow to configure Philips Hue.""" import asyncio -import json -import os +from typing import Dict, Optional -from aiohue.discovery import discover_nupnp +import aiohue +from aiohue.discovery import discover_nupnp, normalize_bridge_id import async_timeout import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, core from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME -from homeassistant.core import callback from homeassistant.helpers import aiohttp_client -from .bridge import get_bridge, normalize_bridge_id -from .const import DOMAIN, LOGGER +from .bridge import authenticate_bridge +from .const import DOMAIN, LOGGER # pylint: disable=unused-import from .errors import AuthenticationRequired, CannotConnect HUE_MANUFACTURERURL = "http://www.philips.com" HUE_IGNORED_BRIDGE_NAMES = ["HASS Bridge", "Espalexa"] -@callback -def configured_hosts(hass): - """Return a set of the configured hosts.""" - return set( - entry.data["host"] for entry in hass.config_entries.async_entries(DOMAIN) - ) - - -def _find_username_from_config(hass, filename): - """Load username from config. - - This was a legacy way of configuring Hue until Home Assistant 0.67. - """ - path = hass.config.path(filename) - - if not os.path.isfile(path): - return None - - with open(path) as inp: - try: - return list(json.load(inp).values())[0]["username"] - except ValueError: - # If we get invalid JSON - return None - - class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Hue config flow.""" @@ -56,23 +29,45 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Hue flow.""" - self.host = None + self.bridge: Optional[aiohue.Bridge] = None + self.discovered_bridges: Optional[Dict[str, aiohue.Bridge]] = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" + # This is for backwards compatibility. return await self.async_step_init(user_input) + @core.callback + def _async_get_bridge(self, host: str, bridge_id: Optional[str] = None): + """Return a bridge object.""" + if bridge_id is not None: + bridge_id = normalize_bridge_id(bridge_id) + + return aiohue.Bridge( + host, + websession=aiohttp_client.async_get_clientsession(self.hass), + bridge_id=bridge_id, + ) + async def async_step_init(self, user_input=None): """Handle a flow start.""" - if user_input is not None: - self.host = self.context["host"] = user_input["host"] - return await self.async_step_link() - - websession = aiohttp_client.async_get_clientsession(self.hass) + if ( + user_input is not None + and self.discovered_bridges is not None + # pylint: disable=unsupported-membership-test + and user_input["id"] in self.discovered_bridges + ): + # pylint: disable=unsubscriptable-object + self.bridge = self.discovered_bridges[user_input["id"]] + await self.async_set_unique_id(self.bridge.id, raise_on_progress=False) + # We pass user input to link so it will attempt to link right away + return await self.async_step_link({}) try: with async_timeout.timeout(5): - bridges = await discover_nupnp(websession=websession) + bridges = await discover_nupnp( + websession=aiohttp_client.async_get_clientsession(self.hass) + ) except asyncio.TimeoutError: return self.async_abort(reason="discover_timeout") @@ -80,20 +75,28 @@ async def async_step_init(self, user_input=None): return self.async_abort(reason="no_bridges") # Find already configured hosts - configured = configured_hosts(self.hass) - - hosts = [bridge.host for bridge in bridges if bridge.host not in configured] + already_configured = self._async_current_ids() + bridges = [bridge for bridge in bridges if bridge.id not in already_configured] - if not hosts: + if not bridges: return self.async_abort(reason="all_configured") - if len(hosts) == 1: - self.host = hosts[0] + if len(bridges) == 1: + self.bridge = bridges[0] + await self.async_set_unique_id(self.bridge.id, raise_on_progress=False) return await self.async_step_link() + self.discovered_bridges = {bridge.id: bridge for bridge in bridges} + return self.async_show_form( step_id="init", - data_schema=vol.Schema({vol.Required("host"): vol.In(hosts)}), + data_schema=vol.Schema( + { + vol.Required("id"): vol.In( + {bridge.id: bridge.host for bridge in bridges} + ) + } + ), ) async def async_step_link(self, user_input=None): @@ -102,31 +105,39 @@ async def async_step_link(self, user_input=None): Given a configured host, will ask the user to press the link button to connect to the bridge. """ + if user_input is None: + return self.async_show_form(step_id="link") + + bridge = self.bridge + assert bridge is not None errors = {} - # We will always try linking in case the user has already pressed - # the link button. try: - bridge = await get_bridge(self.hass, self.host, username=None) + await authenticate_bridge(self.hass, bridge) - return await self._entry_from_bridge(bridge) + # Can happen if we come from import. + if self.unique_id is None: + await self.async_set_unique_id( + normalize_bridge_id(bridge.id), raise_on_progress=False + ) + + return self.async_create_entry( + title=bridge.config.name, + data={"host": bridge.host, "username": bridge.username}, + ) except AuthenticationRequired: errors["base"] = "register_failed" except CannotConnect: - LOGGER.error("Error connecting to the Hue bridge at %s", self.host) + LOGGER.error("Error connecting to the Hue bridge at %s", bridge.host) errors["base"] = "linking" except Exception: # pylint: disable=broad-except LOGGER.exception( - "Unknown error connecting with Hue bridge at %s", self.host + "Unknown error connecting with Hue bridge at %s", bridge.host ) errors["base"] = "linking" - # If there was no user input, do not show the errors. - if user_input is None: - errors = {} - return self.async_show_form(step_id="link", errors=errors) async def async_step_ssdp(self, discovery_info): @@ -135,113 +146,55 @@ async def async_step_ssdp(self, discovery_info): This flow is triggered by the SSDP component. It will check if the host is already configured and delegate to the import step if not. """ + # Filter out non-Hue bridges #1 if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL: return self.async_abort(reason="not_hue_bridge") + # Filter out non-Hue bridges #2 if any( name in discovery_info.get(ATTR_NAME, "") for name in HUE_IGNORED_BRIDGE_NAMES ): return self.async_abort(reason="not_hue_bridge") - host = self.context["host"] = discovery_info.get("host") - - 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): - return self.async_abort(reason="already_configured") - - bridge_id = discovery_info.get("serial") - - await self.async_set_unique_id(normalize_bridge_id(bridge_id)) + if "host" not in discovery_info or "serial" not in discovery_info: + return self.async_abort(reason="not_hue_bridge") - return await self.async_step_import( - { - "host": host, - # This format is the legacy format that Hue used for discovery - "path": f"phue-{bridge_id}.conf", - } + bridge = self._async_get_bridge( + discovery_info["host"], discovery_info["serial"] ) + await self.async_set_unique_id(bridge.id) + self._abort_if_unique_id_configured() + self.bridge = bridge + return await self.async_step_link() + async def async_step_homekit(self, homekit_info): """Handle HomeKit discovery.""" - host = self.context["host"] = homekit_info.get("host") - - 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): - return self.async_abort(reason="already_configured") - - await self.async_set_unique_id( - normalize_bridge_id(homekit_info["properties"]["id"].replace(":", "")) + bridge = self._async_get_bridge( + homekit_info["host"], homekit_info["properties"]["id"] ) - return await self.async_step_import({"host": host}) + await self.async_set_unique_id(bridge.id) + self._abort_if_unique_id_configured() + self.bridge = bridge + return await self.async_step_link() async def async_step_import(self, import_info): """Import a new bridge as a config entry. - Will read authentication from Phue config file if available. - This flow is triggered by `async_setup` for both configured and discovered bridges. Triggered for any bridge that does not have a config entry yet (based on host). This flow is also triggered by `async_step_discovery`. - - If an existing config file is found, we will validate the credentials - and create an entry. Otherwise we will delegate to `link` step which - will ask user to link the bridge. """ - host = self.context["host"] = import_info["host"] - path = import_info.get("path") - - if path is not None: - username = await self.hass.async_add_job( - _find_username_from_config, self.hass, self.hass.config.path(path) - ) - else: - username = None - - try: - bridge = await get_bridge(self.hass, host, username) - - LOGGER.info("Imported authentication for %s from %s", host, path) - - return await self._entry_from_bridge(bridge) - except AuthenticationRequired: - self.host = host - - LOGGER.info("Invalid authentication for %s, requesting link.", host) - - return await self.async_step_link() - - except CannotConnect: - LOGGER.error("Error connecting to the Hue bridge at %s", host) - return self.async_abort(reason="cannot_connect") - - except Exception: # pylint: disable=broad-except - LOGGER.exception("Unknown error connecting with Hue bridge at %s", host) - return self.async_abort(reason="unknown") - - async def _entry_from_bridge(self, bridge): - """Return a config entry from an initialized bridge.""" - # Remove all other entries of hubs with same ID or host - host = bridge.host - bridge_id = bridge.config.bridgeid - - if self.unique_id is None: - await self.async_set_unique_id( - normalize_bridge_id(bridge_id), raise_on_progress=False - ) + # Check if host exists, abort if so. + if any( + import_info["host"] == entry.data["host"] + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") - return self.async_create_entry( - title=bridge.config.name, - data={"host": host, "bridge_id": bridge_id, "username": bridge.username}, - ) + self.bridge = self._async_get_bridge(import_info["host"]) + return await self.async_step_link() diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index c90b6181559458..75384e012e0d02 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,21 +3,15 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": [ - "aiohue==1.9.2" - ], + "requirements": ["aiohue==1.10.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" } ], "homekit": { - "models": [ - "BSB002" - ] + "models": ["BSB002"] }, "dependencies": [], - "codeowners": [ - "@balloob" - ] + "codeowners": ["@balloob"] } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 09ee186da0ffd6..d39fc4803eac77 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -75,10 +75,6 @@ class OperationNotAllowed(ConfigError): """Raised when a config entry operation is not allowed.""" -class UniqueIdInProgress(data_entry_flow.AbortFlow): - """Error to indicate that the unique Id is in progress.""" - - class ConfigEntry: """Hold a configuration entry.""" @@ -379,6 +375,7 @@ def as_dict(self) -> Dict[str, Any]: "system_options": self.system_options.as_dict(), "source": self.source, "connection_class": self.connection_class, + "unique_id": self.unique_id, } @@ -482,6 +479,8 @@ async def async_initialize(self) -> None: options=entry.get("options"), # New in 0.98 system_options=entry.get("system_options", {}), + # New in 0.104 + unique_id=entry.get("unique_id"), ) for entry in config["entries"] ] @@ -617,11 +616,20 @@ async def _async_finish_flow( # Check if config entry exists with unique ID. Unload it. existing_entry = None - unique_id = flow.context.get("unique_id") - if unique_id is not None: + if flow.unique_id is not None: + # Abort all flows in progress with same unique ID. + for progress_flow in self.flow.async_progress(): + if ( + progress_flow["handler"] == flow.handler + and progress_flow["flow_id"] != flow.flow_id + and progress_flow["context"].get("unique_id") == flow.unique_id + ): + self.flow.async_abort(progress_flow["flow_id"]) + + # Find existing entry. for check_entry in self.async_entries(result["handler"]): - if check_entry.unique_id == unique_id: + if check_entry.unique_id == flow.unique_id: existing_entry = check_entry break @@ -643,16 +651,17 @@ async def _async_finish_flow( system_options={}, source=flow.context["source"], connection_class=flow.CONNECTION_CLASS, - unique_id=unique_id, + unique_id=flow.unique_id, ) self._entries.append(entry) - self._async_schedule_save() await self.async_setup(entry.entry_id) if existing_entry is not None: await self.async_remove(existing_entry.entry_id) + self._async_schedule_save() + result["result"] = entry return result @@ -723,8 +732,6 @@ async def _old_conf_migrator(old_config: Dict[str, Any]) -> Dict[str, Any]: class ConfigFlow(data_entry_flow.FlowHandler): """Base class for config flows with some helpers.""" - unique_id = None - def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None: """Initialize a subclass, register if possible.""" super().__init_subclass__(**kwargs) # type: ignore @@ -733,12 +740,30 @@ def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None: CONNECTION_CLASS = CONN_CLASS_UNKNOWN + @property + def unique_id(self) -> Optional[str]: + """Return unique ID if available.""" + # pylint: disable=no-member + if not self.context: + return None + + return cast(Optional[str], self.context.get("unique_id")) + @staticmethod @callback def async_get_options_flow(config_entry: ConfigEntry) -> "OptionsFlow": """Get the options flow for this handler.""" raise data_entry_flow.UnknownHandler + @callback + def _abort_if_unique_id_configured(self) -> None: + """Abort if the unique ID is already configured.""" + if self.unique_id is None: + return + + if self.unique_id in self._async_current_ids(): + raise data_entry_flow.AbortFlow("already_configured") + async def async_set_unique_id( self, unique_id: str, *, raise_on_progress: bool = True ) -> Optional[ConfigEntry]: @@ -749,7 +774,7 @@ async def async_set_unique_id( if raise_on_progress: for progress in self._async_in_progress(): if progress["context"].get("unique_id") == unique_id: - raise UniqueIdInProgress("already_in_progress") + raise data_entry_flow.AbortFlow("already_in_progress") # pylint: disable=no-member self.context["unique_id"] = unique_id @@ -766,6 +791,15 @@ def _async_current_entries(self) -> List[ConfigEntry]: assert self.hass is not None return self.hass.config_entries.async_entries(self.handler) + @callback + def _async_current_ids(self) -> Set[Optional[str]]: + """Return current unique IDs.""" + assert self.hass is not None + return set( + entry.unique_id + for entry in self.hass.config_entries.async_entries(self.handler) + ) + @callback def _async_in_progress(self) -> List[Dict]: """Return other in progress flows for current domain.""" diff --git a/homeassistant/core.py b/homeassistant/core.py index 0002019fdfaaf8..e76673f572734e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1134,6 +1134,9 @@ def async_remove(self, domain: str, service: str) -> None: self._services[domain].pop(service) + if not self._services[domain]: + self._services.pop(domain) + self._hass.bus.async_fire( EVENT_SERVICE_REMOVED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} ) diff --git a/requirements_all.txt b/requirements_all.txt index a8d454aac99514..7984cd4a32a175 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -163,7 +163,7 @@ aioharmony==0.1.13 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.2 +aiohue==1.10.1 # homeassistant.components.imap aioimaplib==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 074d5bd6c8e7f3..20f8e8efc76fb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -63,7 +63,7 @@ aioesphomeapi==2.6.1 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.9.2 +aiohue==1.10.1 # homeassistant.components.notion aionotion==1.1.0 diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index b66733e7c76a91..03966560d8d0df 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -9,107 +9,110 @@ from tests.common import mock_coro -async def test_bridge_setup(): +async def test_bridge_setup(hass): """Test a successful setup.""" - hass = Mock() entry = Mock() - api = Mock() + api = Mock(initialize=mock_coro) entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object(bridge, "get_bridge", return_value=mock_coro(api)): + with patch("aiohue.Bridge", return_value=api), patch.object( + hass.config_entries, "async_forward_entry_setup" + ) as mock_forward: assert await hue_bridge.async_setup() is True assert hue_bridge.api is api - forward_entries = set( - c[1][1] for c in hass.config_entries.async_forward_entry_setup.mock_calls - ) - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3 + assert len(mock_forward.mock_calls) == 3 + forward_entries = set(c[1][1] for c in mock_forward.mock_calls) assert forward_entries == set(["light", "binary_sensor", "sensor"]) -async def test_bridge_setup_invalid_username(): +async def test_bridge_setup_invalid_username(hass): """Test we start config flow if username is no longer whitelisted.""" - hass = Mock() entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object(bridge, "get_bridge", side_effect=errors.AuthenticationRequired): + with patch.object( + bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired + ), patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: assert await hue_bridge.async_setup() is False - assert len(hass.async_create_task.mock_calls) == 1 - assert len(hass.config_entries.flow.async_init.mock_calls) == 1 - assert hass.config_entries.flow.async_init.mock_calls[0][2]["data"] == { - "host": "1.2.3.4" - } + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][2]["data"] == {"host": "1.2.3.4"} async def test_bridge_setup_timeout(hass): """Test we retry to connect if we cannot connect.""" - hass = Mock() entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) with patch.object( - bridge, "get_bridge", side_effect=errors.CannotConnect + bridge, "authenticate_bridge", side_effect=errors.CannotConnect ), pytest.raises(ConfigEntryNotReady): await hue_bridge.async_setup() -async def test_reset_if_entry_had_wrong_auth(): +async def test_reset_if_entry_had_wrong_auth(hass): """Test calling reset when the entry contained wrong auth.""" - hass = Mock() entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object(bridge, "get_bridge", side_effect=errors.AuthenticationRequired): + with patch.object( + bridge, "authenticate_bridge", side_effect=errors.AuthenticationRequired + ), patch.object(bridge, "create_config_flow") as mock_create: assert await hue_bridge.async_setup() is False - assert len(hass.async_create_task.mock_calls) == 1 + assert len(mock_create.mock_calls) == 1 assert await hue_bridge.async_reset() -async def test_reset_unloads_entry_if_setup(): +async def test_reset_unloads_entry_if_setup(hass): """Test calling reset while the entry has been setup.""" - hass = Mock() entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object(bridge, "get_bridge", return_value=mock_coro(Mock())): + with patch.object( + bridge, "authenticate_bridge", return_value=mock_coro(Mock()) + ), patch("aiohue.Bridge", return_value=Mock()), patch.object( + hass.config_entries, "async_forward_entry_setup" + ) as mock_forward: assert await hue_bridge.async_setup() is True - assert len(hass.services.async_register.mock_calls) == 1 - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3 + assert len(hass.services.async_services()) == 1 + assert len(mock_forward.mock_calls) == 3 - hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) - assert await hue_bridge.async_reset() + with patch.object( + hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) + ) as mock_forward: + assert await hue_bridge.async_reset() - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 3 - assert len(hass.services.async_remove.mock_calls) == 1 + assert len(mock_forward.mock_calls) == 3 + assert len(hass.services.async_services()) == 0 -async def test_handle_unauthorized(): +async def test_handle_unauthorized(hass): """Test handling an unauthorized error on update.""" - hass = Mock() entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) - with patch.object(bridge, "get_bridge", return_value=mock_coro(Mock())): + with patch.object( + bridge, "authenticate_bridge", return_value=mock_coro(Mock()) + ), patch("aiohue.Bridge", return_value=Mock()): assert await hue_bridge.async_setup() is True assert hue_bridge.authorized is True - await hue_bridge.handle_unauthorized_error() + with patch.object(bridge, "create_config_flow") as mock_create: + await hue_bridge.handle_unauthorized_error() assert hue_bridge.authorized is False - assert len(hass.async_create_task.mock_calls) == 4 - assert len(hass.config_entries.flow.async_init.mock_calls) == 1 - assert hass.config_entries.flow.async_init.mock_calls[0][2]["data"] == { - "host": "1.2.3.4" - } + assert len(mock_create.mock_calls) == 1 + assert mock_create.mock_calls[0][1][1] == "1.2.3.4" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 030f6ade1fa70c..fe9a1f0e32ccd4 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -6,50 +6,52 @@ import pytest import voluptuous as vol -from homeassistant.components.hue import config_flow, const, errors +from homeassistant import data_entry_flow +from homeassistant.components.hue import config_flow, const from tests.common import MockConfigEntry, mock_coro -async def test_flow_works(hass, aioclient_mock): +async def test_flow_works(hass): """Test config flow .""" - aioclient_mock.get( - const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}] - ) + mock_bridge = Mock() + mock_bridge.host = "1.2.3.4" + mock_bridge.username = None + mock_bridge.config.name = "Mock Bridge" + mock_bridge.id = "aabbccddeeff" + + async def mock_create_user(username): + mock_bridge.username = username + + mock_bridge.create_user = mock_create_user + mock_bridge.initialize.return_value = mock_coro() flow = config_flow.HueFlowHandler() flow.hass = hass flow.context = {} - await flow.async_step_init() - - with patch("aiohue.Bridge") as mock_bridge: - def mock_constructor(host, websession, username=None): - """Fake the bridge constructor.""" - mock_bridge.host = host - return mock_bridge + with patch( + "homeassistant.components.hue.config_flow.discover_nupnp", + return_value=mock_coro([mock_bridge]), + ): + result = await flow.async_step_init() - mock_bridge.side_effect = mock_constructor - mock_bridge.username = "username-abc" - mock_bridge.config.name = "Mock Bridge" - mock_bridge.config.bridgeid = "bridge-id-1234" - mock_bridge.create_user.return_value = mock_coro() - mock_bridge.initialize.return_value = mock_coro() + assert result["type"] == "form" + assert result["step_id"] == "link" - result = await flow.async_step_link(user_input={}) + assert flow.context["unique_id"] == "aabbccddeeff" - assert mock_bridge.host == "1.2.3.4" - assert len(mock_bridge.create_user.mock_calls) == 1 - assert len(mock_bridge.initialize.mock_calls) == 1 + result = await flow.async_step_link(user_input={}) assert result["type"] == "create_entry" assert result["title"] == "Mock Bridge" assert result["data"] == { "host": "1.2.3.4", - "bridge_id": "bridge-id-1234", - "username": "username-abc", + "username": "home-assistant#test-home", } + assert len(mock_bridge.initialize.mock_calls) == 1 + async def test_flow_no_discovered_bridges(hass, aioclient_mock): """Test config flow discovers no bridges.""" @@ -66,9 +68,12 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): aioclient_mock.get( const.API_NUPNP, json=[{"internalipaddress": "1.2.3.4", "id": "bla"}] ) - MockConfigEntry(domain="hue", data={"host": "1.2.3.4"}).add_to_hass(hass) + MockConfigEntry( + domain="hue", unique_id="bla", data={"host": "1.2.3.4"} + ).add_to_hass(hass) flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} result = await flow.async_step_init() assert result["type"] == "abort" @@ -81,6 +86,7 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock): ) flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} result = await flow.async_step_init() assert result["type"] == "form" @@ -104,10 +110,10 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock): assert result["step_id"] == "init" with pytest.raises(vol.Invalid): - assert result["data_schema"]({"host": "0.0.0.0"}) + assert result["data_schema"]({"id": "not-discovered"}) - result["data_schema"]({"host": "1.2.3.4"}) - result["data_schema"]({"host": "5.6.7.8"}) + result["data_schema"]({"id": "bla"}) + result["data_schema"]({"id": "beer"}) async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): @@ -119,14 +125,17 @@ async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): {"internalipaddress": "5.6.7.8", "id": "beer"}, ], ) - MockConfigEntry(domain="hue", data={"host": "1.2.3.4"}).add_to_hass(hass) + MockConfigEntry( + domain="hue", unique_id="bla", data={"host": "1.2.3.4"} + ).add_to_hass(hass) flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} result = await flow.async_step_init() assert result["type"] == "form" assert result["step_id"] == "link" - assert flow.host == "5.6.7.8" + assert flow.bridge.host == "5.6.7.8" async def test_flow_timeout_discovery(hass): @@ -147,6 +156,7 @@ async def test_flow_link_timeout(hass): """Test config flow .""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.bridge = Mock() with patch("aiohue.Bridge.create_user", side_effect=asyncio.TimeoutError): result = await flow.async_step_link({}) @@ -160,9 +170,11 @@ async def test_flow_link_button_not_pressed(hass): """Test config flow .""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.bridge = Mock( + username=None, create_user=Mock(side_effect=aiohue.LinkButtonNotPressed) + ) - with patch("aiohue.Bridge.create_user", side_effect=aiohue.LinkButtonNotPressed): - result = await flow.async_step_link({}) + result = await flow.async_step_link({}) assert result["type"] == "form" assert result["step_id"] == "link" @@ -173,6 +185,7 @@ async def test_flow_link_unknown_host(hass): """Test config flow .""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.bridge = Mock() with patch("aiohue.Bridge.create_user", side_effect=aiohue.RequestError): result = await flow.async_step_link({}) @@ -188,16 +201,13 @@ async def test_bridge_ssdp(hass): flow.hass = hass flow.context = {} - with patch.object( - config_flow, "get_bridge", side_effect=errors.AuthenticationRequired - ): - result = await flow.async_step_ssdp( - { - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, - } - ) + result = await flow.async_step_ssdp( + { + "host": "0.0.0.0", + "serial": "1234", + "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + } + ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -255,47 +265,22 @@ async def test_bridge_ssdp_espalexa(hass): async def test_bridge_ssdp_already_configured(hass): """Test if a discovered bridge has already been configured.""" - MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass) - - flow = config_flow.HueFlowHandler() - flow.hass = hass - flow.context = {} - - result = await flow.async_step_ssdp( - { - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, - } - ) - - assert result["type"] == "abort" - + MockConfigEntry( + domain="hue", unique_id="1234", data={"host": "0.0.0.0"} + ).add_to_hass(hass) -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" - bridge.config.bridgeid = "bridge-id-1234" - bridge.config.name = "Mock Bridge" - bridge.host = "0.0.0.0" - - with patch.object( - config_flow, "_find_username_from_config", return_value="mock-user" - ), patch.object(config_flow, "get_bridge", return_value=mock_coro(bridge)): - result = await flow.async_step_import({"host": "0.0.0.0", "path": "bla.conf"}) - - assert result["type"] == "create_entry" - assert result["title"] == "Mock Bridge" - assert result["data"] == { - "host": "0.0.0.0", - "bridge_id": "bridge-id-1234", - "username": "username-abc", - } + with pytest.raises(data_entry_flow.AbortFlow): + await flow.async_step_ssdp( + { + "host": "0.0.0.0", + "serial": "1234", + "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + } + ) async def test_import_with_no_config(hass): @@ -304,45 +289,12 @@ async def test_import_with_no_config(hass): flow.hass = hass flow.context = {} - with patch.object( - config_flow, "get_bridge", side_effect=errors.AuthenticationRequired - ): - result = await flow.async_step_import({"host": "0.0.0.0"}) - - assert result["type"] == "form" - assert result["step_id"] == "link" - - -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" - ), patch.object( - config_flow, "get_bridge", side_effect=errors.AuthenticationRequired - ): - result = await flow.async_step_import({"host": "0.0.0.0", "path": "bla.conf"}) + result = await flow.async_step_import({"host": "0.0.0.0"}) assert result["type"] == "form" assert result["step_id"] == "link" -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"}) - - assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" - - async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): """Test that we clean up entries for same host and bridge. @@ -351,38 +303,45 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): all existing entries that either have same IP or same bridge_id. """ orig_entry = MockConfigEntry( - domain="hue", - data={"host": "0.0.0.0", "bridge_id": "id-1234"}, - unique_id="id-1234", + domain="hue", data={"host": "0.0.0.0", "username": "aaaa"}, unique_id="id-1234", ) orig_entry.add_to_hass(hass) MockConfigEntry( - domain="hue", - data={"host": "1.2.3.4", "bridge_id": "id-5678"}, - unique_id="id-5678", + domain="hue", data={"host": "1.2.3.4", "username": "bbbb"}, unique_id="id-5678", ).add_to_hass(hass) assert len(hass.config_entries.async_entries("hue")) == 2 bridge = Mock() bridge.username = "username-abc" - bridge.config.bridgeid = "id-1234" bridge.config.name = "Mock Bridge" bridge.host = "0.0.0.0" + bridge.id = "id-1234" - with patch.object( - config_flow, "_find_username_from_config", return_value="mock-user" - ), patch.object(config_flow, "get_bridge", return_value=mock_coro(bridge)): + with patch( + "aiohue.Bridge", return_value=bridge, + ): result = await hass.config_entries.flow.async_init( "hue", data={"host": "2.2.2.2"}, context={"source": "import"} ) + assert result["type"] == "form" + assert result["step_id"] == "link" + + with patch( + "homeassistant.components.hue.config_flow.authenticate_bridge", + return_value=mock_coro(), + ), patch( + "homeassistant.components.hue.async_setup_entry", + side_effect=lambda _, _2: mock_coro(True), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == "create_entry" assert result["title"] == "Mock Bridge" assert result["data"] == { "host": "0.0.0.0", - "bridge_id": "id-1234", "username": "username-abc", } entries = hass.config_entries.async_entries("hue") @@ -398,17 +357,14 @@ async def test_bridge_homekit(hass): flow.hass = hass flow.context = {} - with patch.object( - config_flow, "get_bridge", side_effect=errors.AuthenticationRequired - ): - result = await flow.async_step_homekit( - { - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, - "properties": {"id": "aa:bb:cc:dd:ee:ff"}, - } - ) + result = await flow.async_step_homekit( + { + "host": "0.0.0.0", + "serial": "1234", + "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + "properties": {"id": "aa:bb:cc:dd:ee:ff"}, + } + ) assert result["type"] == "form" assert result["step_id"] == "link" @@ -416,12 +372,15 @@ async def test_bridge_homekit(hass): async def test_bridge_homekit_already_configured(hass): """Test if a HomeKit discovered bridge has already been configured.""" - MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass) + MockConfigEntry( + domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"} + ).add_to_hass(hass) flow = config_flow.HueFlowHandler() flow.hass = hass flow.context = {} - result = await flow.async_step_homekit({"host": "0.0.0.0"}) - - assert result["type"] == "abort" + with pytest.raises(data_entry_flow.AbortFlow): + await flow.async_step_homekit( + {"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}} + ) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index d064ff9f340688..b48d66990e8548 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -9,13 +9,10 @@ async def test_setup_with_no_config(hass): """Test that we do not discover anything or try to set up a bridge.""" - with patch.object(hass, "config_entries") as mock_config_entries, patch.object( - hue, "configured_hosts", return_value=[] - ): - assert await async_setup_component(hass, hue.DOMAIN, {}) is True + assert await async_setup_component(hass, hue.DOMAIN, {}) is True # No flows started - assert len(mock_config_entries.flow.mock_calls) == 0 + assert len(hass.config_entries.flow.async_progress()) == 0 # No configs stored assert hass.data[hue.DOMAIN] == {} @@ -23,9 +20,9 @@ async def test_setup_with_no_config(hass): async def test_setup_defined_hosts_known_auth(hass): """Test we don't initiate a config entry if config bridge is known.""" - with patch.object(hass, "config_entries") as mock_config_entries, patch.object( - hue, "configured_hosts", return_value=["0.0.0.0"] - ): + MockConfigEntry(domain="hue", data={"host": "0.0.0.0"}).add_to_hass(hass) + + with patch.object(hue, "async_setup_entry", return_value=mock_coro(True)): assert ( await async_setup_component( hass, @@ -34,7 +31,6 @@ async def test_setup_defined_hosts_known_auth(hass): hue.DOMAIN: { hue.CONF_BRIDGES: { hue.CONF_HOST: "0.0.0.0", - hue.CONF_FILENAME: "bla.conf", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, } @@ -45,13 +41,12 @@ async def test_setup_defined_hosts_known_auth(hass): ) # Flow started for discovered bridge - assert len(mock_config_entries.flow.mock_calls) == 0 + assert len(hass.config_entries.flow.async_progress()) == 0 # Config stored for domain. assert hass.data[hue.DATA_CONFIGS] == { "0.0.0.0": { hue.CONF_HOST: "0.0.0.0", - hue.CONF_FILENAME: "bla.conf", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, } @@ -60,40 +55,30 @@ async def test_setup_defined_hosts_known_auth(hass): async def test_setup_defined_hosts_no_known_auth(hass): """Test we initiate config entry if config bridge is not known.""" - with patch.object(hass, "config_entries") as mock_config_entries, patch.object( - hue, "configured_hosts", return_value=[] - ): - mock_config_entries.flow.async_init.return_value = mock_coro() - assert ( - await async_setup_component( - hass, - hue.DOMAIN, - { - hue.DOMAIN: { - hue.CONF_BRIDGES: { - hue.CONF_HOST: "0.0.0.0", - hue.CONF_FILENAME: "bla.conf", - hue.CONF_ALLOW_HUE_GROUPS: False, - hue.CONF_ALLOW_UNREACHABLE: True, - } + assert ( + await async_setup_component( + hass, + hue.DOMAIN, + { + hue.DOMAIN: { + hue.CONF_BRIDGES: { + hue.CONF_HOST: "0.0.0.0", + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True, } - }, - ) - is True + } + }, ) + is True + ) # Flow started for discovered bridge - assert len(mock_config_entries.flow.mock_calls) == 1 - assert mock_config_entries.flow.mock_calls[0][2]["data"] == { - "host": "0.0.0.0", - "path": "bla.conf", - } + assert len(hass.config_entries.flow.async_progress()) == 1 # Config stored for domain. assert hass.data[hue.DATA_CONFIGS] == { "0.0.0.0": { hue.CONF_HOST: "0.0.0.0", - hue.CONF_FILENAME: "bla.conf", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, } @@ -126,7 +111,6 @@ async def test_config_passed_to_config_entry(hass): hue.DOMAIN: { hue.CONF_BRIDGES: { hue.CONF_HOST: "0.0.0.0", - hue.CONF_FILENAME: "bla.conf", hue.CONF_ALLOW_HUE_GROUPS: False, hue.CONF_ALLOW_UNREACHABLE: True, } @@ -166,7 +150,7 @@ async def test_unload_entry(hass): return_value=mock_coro(Mock()), ): mock_bridge.return_value.async_setup.return_value = mock_coro(True) - mock_bridge.return_value.api.config = Mock() + mock_bridge.return_value.api.config = Mock(bridgeid="aabbccddeeff") assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert len(mock_bridge.return_value.mock_calls) == 1 diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index a9ae4eb59ac224..d83de45022791a 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -434,8 +434,8 @@ class TestFlow(config_entries.ConfigFlow): VERSION = 5 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("unique") return self.async_create_entry(title="Test Title", data={"token": "abcd"}) with patch.dict(config_entries.HANDLERS, {"test": TestFlow}): @@ -477,6 +477,7 @@ def async_step_user(self, user_input=None): assert orig.data == loaded.data assert orig.source == loaded.source assert orig.connection_class == loaded.connection_class + assert orig.unique_id == loaded.unique_id async def test_forward_entry_sets_up_component(hass): @@ -1108,3 +1109,40 @@ async def async_step_user(self, user_input=None): assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["reason"] == "already_in_progress" + + +async def test_finish_flow_aborts_progress(hass, manager): + """Test that when finishing a flow, we abort other flows in progress with unique ID.""" + mock_integration( + hass, + MockModule("comp", async_setup_entry=MagicMock(return_value=mock_coro(True))), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id", raise_on_progress=False) + + if user_input is None: + return self.async_show_form(step_id="discovery") + + return self.async_create_entry(title="yo", data={}) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + # Create one to be in progress + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + # Will finish and cancel other one. + result2 = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER}, data={} + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + assert len(hass.config_entries.flow.async_progress()) == 0 From c16fae2c0b7a36099d900bcdcba044bef3e427cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Mon, 16 Dec 2019 20:07:46 +0100 Subject: [PATCH 2450/3953] Fix modbus service description (#30005) --- homeassistant/components/modbus/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index 8713257b47c261..2158528814f9c3 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -1,7 +1,7 @@ write_coil: description: Write to a modbus coil. fields: - address: {description: Address of the register to read., example: 0} + address: {description: Address of the register to write to., example: 0} state: {description: State to write., example: false} unit: {description: Address of the modbus unit., example: 21} write_register: From 0439d6964c7adf56ae32e019de08d64541631b04 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 Dec 2019 20:16:23 +0100 Subject: [PATCH 2451/3953] Fix persistent setup error notification content (#29995) * Fix persistent setup error notification content * Use documentation from manifest, enriched error messages * Fix issue caught by mypy --- homeassistant/config.py | 43 ++++++++++++++++++++++++----------------- homeassistant/loader.py | 5 +++++ homeassistant/setup.py | 12 ++++++------ 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 353a717778b008..c3a97a1184cbc1 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -60,7 +60,6 @@ DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors" RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") RE_ASCII = re.compile(r"\033\[[^m]*m") -HA_COMPONENT_URL = "[{}](https://home-assistant.io/integrations/{}/)" YAML_CONFIG_FILE = "configuration.yaml" VERSION_FILE = ".HA_VERSION" CONFIG_DIR_NAME = ".homeassistant" @@ -412,19 +411,25 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: @callback def async_log_exception( - ex: Exception, domain: str, config: Dict, hass: HomeAssistant + ex: Exception, + domain: str, + config: Dict, + hass: HomeAssistant, + link: Optional[str] = None, ) -> None: """Log an error for configuration validation. This method must be run in the event loop. """ if hass is not None: - async_notify_setup_error(hass, domain, True) - _LOGGER.error(_format_config_error(ex, domain, config)) + async_notify_setup_error(hass, domain, link) + _LOGGER.error(_format_config_error(ex, domain, config, link)) @callback -def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: +def _format_config_error( + ex: Exception, domain: str, config: Dict, link: Optional[str] = None +) -> str: """Generate log exception for configuration validation. This method must be run in the event loop. @@ -455,12 +460,8 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: getattr(domain_config, "__line__", "?"), ) - if domain != CONF_CORE: - integration = domain.split(".")[-1] - message += ( - "Please check the docs at " - f"https://home-assistant.io/integrations/{integration}/" - ) + if domain != CONF_CORE and link: + message += f"Please check the docs at {link}" return message @@ -717,7 +718,7 @@ async def async_process_component_config( hass, config ) except (vol.Invalid, HomeAssistantError) as ex: - async_log_exception(ex, domain, config, hass) + async_log_exception(ex, domain, config, hass, integration.documentation) return None # No custom config validator, proceed with schema validation @@ -725,7 +726,7 @@ async def async_process_component_config( try: return component.CONFIG_SCHEMA(config) # type: ignore except vol.Invalid as ex: - async_log_exception(ex, domain, config, hass) + async_log_exception(ex, domain, config, hass, integration.documentation) return None component_platform_schema = getattr( @@ -741,7 +742,7 @@ async def async_process_component_config( try: p_validated = component_platform_schema(p_config) except vol.Invalid as ex: - async_log_exception(ex, domain, p_config, hass) + async_log_exception(ex, domain, p_config, hass, integration.documentation) continue # Not all platform components follow same pattern for platforms @@ -770,7 +771,13 @@ async def async_process_component_config( p_config ) except vol.Invalid as ex: - async_log_exception(ex, f"{domain}.{p_name}", p_config, hass) + async_log_exception( + ex, + f"{domain}.{p_name}", + p_config, + hass, + p_integration.documentation, + ) continue platforms.append(p_validated) @@ -806,7 +813,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> Optional[str]: @callback def async_notify_setup_error( - hass: HomeAssistant, component: str, display_link: bool = False + hass: HomeAssistant, component: str, display_link: Optional[str] = None ) -> None: """Print a persistent notification. @@ -821,11 +828,11 @@ def async_notify_setup_error( errors[component] = errors.get(component) or display_link - message = "The following components and platforms could not be set up:\n\n" + message = "The following integrations and platforms could not be set up:\n\n" for name, link in errors.items(): if link: - part = HA_COMPONENT_URL.format(name.replace("_", "-"), name) + part = f"[{name}]({link})" else: part = name diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 9e8ea9fc7caebb..de05c944aafb7f 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -234,6 +234,11 @@ def config_flow(self) -> bool: """Return config_flow.""" return cast(bool, self.manifest.get("config_flow", False)) + @property + def documentation(self) -> Optional[str]: + """Return documentation.""" + return cast(str, self.manifest.get("documentation")) + @property def is_built_in(self) -> bool: """Test if package is a built-in integration.""" diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 01b13629e7b60d..2424f5fc465209 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -92,7 +92,7 @@ async def _async_setup_component( This method is a coroutine. """ - def log_error(msg: str, link: bool = True) -> None: + def log_error(msg: str, link: Optional[str] = None) -> None: """Log helper.""" _LOGGER.error("Setup failed for %s: %s", domain, msg) async_notify_setup_error(hass, domain, link) @@ -100,7 +100,7 @@ def log_error(msg: str, link: bool = True) -> None: try: integration = await loader.async_get_integration(hass, domain) except loader.IntegrationNotFound: - log_error("Integration not found.", False) + log_error("Integration not found.") return False # Validate all dependencies exist and there are no circular dependencies @@ -127,7 +127,7 @@ def log_error(msg: str, link: bool = True) -> None: try: await async_process_deps_reqs(hass, config, integration) except HomeAssistantError as err: - log_error(str(err)) + log_error(str(err), integration.documentation) return False # Some integrations fail on import because they call functions incorrectly. @@ -135,7 +135,7 @@ def log_error(msg: str, link: bool = True) -> None: try: component = integration.get_component() except ImportError: - log_error("Unable to import component", False) + log_error("Unable to import component", integration.documentation) return False except Exception: # pylint: disable=broad-except _LOGGER.exception("Setup failed for %s: unknown error", domain) @@ -146,7 +146,7 @@ def log_error(msg: str, link: bool = True) -> None: ) if processed_config is None: - log_error("Invalid config.") + log_error("Invalid config.", integration.documentation) return False start = timer() @@ -178,7 +178,7 @@ def log_error(msg: str, link: bool = True) -> None: return False except Exception: # pylint: disable=broad-except _LOGGER.exception("Error during setup of component %s", domain) - async_notify_setup_error(hass, domain, True) + async_notify_setup_error(hass, domain, integration.documentation) return False finally: end = timer() From ad8278c078bc3ce7d8912bb3a280069939dafd02 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 17 Dec 2019 00:32:23 +0000 Subject: [PATCH 2452/3953] [ci skip] Translation update --- .../alarm_control_panel/.translations/hu.json | 18 +++++++++++++++++ .../components/climate/.translations/bg.json | 2 +- .../components/climate/.translations/ca.json | 2 +- .../components/climate/.translations/en.json | 2 +- .../components/climate/.translations/es.json | 2 +- .../components/climate/.translations/fr.json | 2 +- .../components/climate/.translations/it.json | 2 +- .../components/climate/.translations/lb.json | 2 +- .../components/climate/.translations/no.json | 2 +- .../components/climate/.translations/pl.json | 2 +- .../components/climate/.translations/ru.json | 2 +- .../components/climate/.translations/sl.json | 2 +- .../climate/.translations/zh-Hant.json | 2 +- .../components/cover/.translations/hu.json | 20 +++++++++++++++++++ .../components/demo/.translations/hu.json | 5 +++++ .../device_tracker/.translations/bg.json | 2 +- .../device_tracker/.translations/ca.json | 2 +- .../device_tracker/.translations/cs.json | 2 +- .../device_tracker/.translations/de.json | 2 +- .../device_tracker/.translations/en.json | 2 +- .../device_tracker/.translations/es.json | 2 +- .../device_tracker/.translations/fr.json | 2 +- .../device_tracker/.translations/it.json | 2 +- .../device_tracker/.translations/ko.json | 2 +- .../device_tracker/.translations/lb.json | 2 +- .../device_tracker/.translations/nl.json | 2 +- .../device_tracker/.translations/no.json | 2 +- .../device_tracker/.translations/pl.json | 2 +- .../device_tracker/.translations/pt.json | 2 +- .../device_tracker/.translations/ru.json | 2 +- .../device_tracker/.translations/sl.json | 2 +- .../device_tracker/.translations/zh-Hant.json | 2 +- .../components/fan/.translations/bg.json | 2 +- .../components/fan/.translations/ca.json | 2 +- .../components/fan/.translations/de.json | 2 +- .../components/fan/.translations/en.json | 2 +- .../components/fan/.translations/es.json | 2 +- .../components/fan/.translations/fr.json | 2 +- .../components/fan/.translations/it.json | 2 +- .../components/fan/.translations/lb.json | 2 +- .../components/fan/.translations/nl.json | 2 +- .../components/fan/.translations/no.json | 2 +- .../components/fan/.translations/pl.json | 2 +- .../components/fan/.translations/pt-BR.json | 2 +- .../components/fan/.translations/pt.json | 2 +- .../components/fan/.translations/ru.json | 2 +- .../components/fan/.translations/sl.json | 2 +- .../components/fan/.translations/zh-Hant.json | 2 +- .../components/neato/.translations/ko.json | 4 ++-- .../components/soma/.translations/lb.json | 4 +++- .../components/vacuum/.translations/bg.json | 2 +- .../components/vacuum/.translations/ca.json | 2 +- .../components/vacuum/.translations/de.json | 2 +- .../components/vacuum/.translations/en.json | 2 +- .../components/vacuum/.translations/es.json | 2 +- .../components/vacuum/.translations/fr.json | 2 +- .../components/vacuum/.translations/it.json | 2 +- .../components/vacuum/.translations/lb.json | 2 +- .../components/vacuum/.translations/nl.json | 2 +- .../components/vacuum/.translations/no.json | 2 +- .../components/vacuum/.translations/pl.json | 2 +- .../components/vacuum/.translations/pt.json | 2 +- .../components/vacuum/.translations/ru.json | 2 +- .../components/vacuum/.translations/sl.json | 2 +- .../vacuum/.translations/zh-Hant.json | 2 +- 65 files changed, 108 insertions(+), 63 deletions(-) create mode 100644 homeassistant/components/alarm_control_panel/.translations/hu.json create mode 100644 homeassistant/components/cover/.translations/hu.json create mode 100644 homeassistant/components/demo/.translations/hu.json diff --git a/homeassistant/components/alarm_control_panel/.translations/hu.json b/homeassistant/components/alarm_control_panel/.translations/hu.json new file mode 100644 index 00000000000000..b249a16c9f1cef --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} \u00e9les\u00edt\u00e9se t\u00e1voz\u00f3 m\u00f3dban", + "arm_home": "{entity_name} \u00e9les\u00edt\u00e9se otthon marad\u00f3 m\u00f3dban", + "arm_night": "{entity_name} \u00e9les\u00edt\u00e9se \u00e9jszakai m\u00f3dban", + "disarm": "{entity_name} hat\u00e1stalan\u00edt\u00e1sa", + "trigger": "{entity_name} riaszt\u00e1si esem\u00e9ny ind\u00edt\u00e1sa" + }, + "trigger_type": { + "armed_away": "{entity_name} t\u00e1voz\u00f3 m\u00f3dban lett \u00e9les\u00edtve", + "armed_home": "{entity_name} otthon marad\u00f3 m\u00f3dban lett \u00e9les\u00edtve", + "armed_night": "{entity_name} \u00e9jszakai m\u00f3dban lett \u00e9les\u00edtve", + "disarmed": "{entity_name} hat\u00e1stalan\u00edtva lett", + "triggered": "{entity_name} riaszt\u00e1sba ker\u00fclt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/bg.json b/homeassistant/components/climate/.translations/bg.json index d7901d2988443a..ac1b05b096a6c7 100644 --- a/homeassistant/components/climate/.translations/bg.json +++ b/homeassistant/components/climate/.translations/bg.json @@ -4,7 +4,7 @@ "set_hvac_mode": "\u041f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u043d\u0430 \u041e\u0412\u041a \u043d\u0430 {entity_name}", "set_preset_mode": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438 \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043d\u0430 {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0447\u0435\u043d \u041e\u0412\u041a \u0440\u0435\u0436\u0438\u043c", "is_preset_mode": "{entity_name} \u0435 \u0432 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u043d \u0440\u0435\u0436\u0438\u043c" }, diff --git a/homeassistant/components/climate/.translations/ca.json b/homeassistant/components/climate/.translations/ca.json index 743729041ab283..bde91c26b7ed44 100644 --- a/homeassistant/components/climate/.translations/ca.json +++ b/homeassistant/components/climate/.translations/ca.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Canvia el mode HVAC de {entity_name}", "set_preset_mode": "Canvia la configuraci\u00f3 preestablerta de {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} est\u00e0 configurat/ada en un mode HVAC espec\u00edfic", "is_preset_mode": "{entity_name} est\u00e0 configurat/ada en un mode preestablert espec\u00edfic" }, diff --git a/homeassistant/components/climate/.translations/en.json b/homeassistant/components/climate/.translations/en.json index 942d9a2761ff8d..2a56426e988645 100644 --- a/homeassistant/components/climate/.translations/en.json +++ b/homeassistant/components/climate/.translations/en.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Change HVAC mode on {entity_name}", "set_preset_mode": "Change preset on {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} is set to a specific HVAC mode", "is_preset_mode": "{entity_name} is set to a specific preset mode" }, diff --git a/homeassistant/components/climate/.translations/es.json b/homeassistant/components/climate/.translations/es.json index baae9b974367af..e873427e6948c8 100644 --- a/homeassistant/components/climate/.translations/es.json +++ b/homeassistant/components/climate/.translations/es.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Cambiar el modo HVAC de {entity_name}.", "set_preset_mode": "Cambiar la configuraci\u00f3n prefijada de {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico", "is_preset_mode": "{entity_name} se establece en un modo predeterminado espec\u00edfico" }, diff --git a/homeassistant/components/climate/.translations/fr.json b/homeassistant/components/climate/.translations/fr.json index db29f8424d5fc2..0358a60f180bb5 100644 --- a/homeassistant/components/climate/.translations/fr.json +++ b/homeassistant/components/climate/.translations/fr.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Changer le mode HVAC sur {entity_name}.", "set_preset_mode": "Changer les pr\u00e9r\u00e9glages de {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} est d\u00e9fini sur un mode HVAC sp\u00e9cifique", "is_preset_mode": "{entity_name} est d\u00e9fini sur un mode pr\u00e9d\u00e9fini sp\u00e9cifique" }, diff --git a/homeassistant/components/climate/.translations/it.json b/homeassistant/components/climate/.translations/it.json index 34ecbf5e9f2aa2..25a09b7d66da02 100644 --- a/homeassistant/components/climate/.translations/it.json +++ b/homeassistant/components/climate/.translations/it.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Cambia modalit\u00e0 HVAC su {entity_name}", "set_preset_mode": "Modifica preimpostazione su {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 HVAC specifica", "is_preset_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 preimpostata specifica" }, diff --git a/homeassistant/components/climate/.translations/lb.json b/homeassistant/components/climate/.translations/lb.json index 72ab7efc62380d..cfb49f29f05993 100644 --- a/homeassistant/components/climate/.translations/lb.json +++ b/homeassistant/components/climate/.translations/lb.json @@ -4,7 +4,7 @@ "set_hvac_mode": "HVAC Modus \u00e4nnere fir {entity_name}", "set_preset_mode": "Preset \u00e4nnere fir {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "\n{entity_name} ass op e spezifesche HVAC Modus gesat", "is_preset_mode": "{entity_name} ass op e spezifesche preset Modus gesat" }, diff --git a/homeassistant/components/climate/.translations/no.json b/homeassistant/components/climate/.translations/no.json index 2d95c63a6aef32..bc6e97b9aa50f7 100644 --- a/homeassistant/components/climate/.translations/no.json +++ b/homeassistant/components/climate/.translations/no.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Endre HVAC-modus p\u00e5 {entity_name}", "set_preset_mode": "Endre forh\u00e5ndsinnstilling p\u00e5 {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} er satt til en spesifikk HVAC-modus", "is_preset_mode": "{entity_name} er satt til en spesifikk forh\u00e5ndsinnstilt modus" }, diff --git a/homeassistant/components/climate/.translations/pl.json b/homeassistant/components/climate/.translations/pl.json index c5b0c483ca9321..f2a09eee3ef62d 100644 --- a/homeassistant/components/climate/.translations/pl.json +++ b/homeassistant/components/climate/.translations/pl.json @@ -4,7 +4,7 @@ "set_hvac_mode": "zmie\u0144 tryb HVAC na {entity_name}", "set_preset_mode": "zmie\u0144 ustawienia dla {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "na {entity_name} jest ustawiony okre\u015blony tryb HVAC", "is_preset_mode": "na {entity_name} jest okre\u015blone ustawienie" }, diff --git a/homeassistant/components/climate/.translations/ru.json b/homeassistant/components/climate/.translations/ru.json index 045f96137d2bd9..6a9c52be209b68 100644 --- a/homeassistant/components/climate/.translations/ru.json +++ b/homeassistant/components/climate/.translations/ru.json @@ -4,7 +4,7 @@ "set_hvac_mode": "\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \"{entity_name}\"", "set_preset_mode": "\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430\u0431\u043e\u0440 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \"{entity_name}\"" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043d\u043e\u043c \u0440\u0435\u0436\u0438\u043c\u0435 \u0440\u0430\u0431\u043e\u0442\u044b", "is_preset_mode": "{entity_name} \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043d\u0430\u0431\u043e\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a" }, diff --git a/homeassistant/components/climate/.translations/sl.json b/homeassistant/components/climate/.translations/sl.json index 4ba4cb02a4b590..ecaf24fed80ab5 100644 --- a/homeassistant/components/climate/.translations/sl.json +++ b/homeassistant/components/climate/.translations/sl.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Spremeni na\u010din HVAC na {entity_name}", "set_preset_mode": "Spremenite prednastavitev na {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} je nastavljen na dolo\u010den na\u010din HVAC", "is_preset_mode": "{entity_name} je nastavljen na dolo\u010den prednastavljeni na\u010din" }, diff --git a/homeassistant/components/climate/.translations/zh-Hant.json b/homeassistant/components/climate/.translations/zh-Hant.json index 1d39eecc056166..17e6c955046bfb 100644 --- a/homeassistant/components/climate/.translations/zh-Hant.json +++ b/homeassistant/components/climate/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "set_hvac_mode": "\u8b8a\u66f4 {entity_name} HVAC \u6a21\u5f0f", "set_preset_mode": "\u8b8a\u66f4 {entity_name} \u8a2d\u5b9a\u6a21\u5f0f" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a HVAC \u6a21\u5f0f", "is_preset_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a\u8a2d\u5b9a\u6a21\u5f0f" }, diff --git a/homeassistant/components/cover/.translations/hu.json b/homeassistant/components/cover/.translations/hu.json new file mode 100644 index 00000000000000..d460c53109dd17 --- /dev/null +++ b/homeassistant/components/cover/.translations/hu.json @@ -0,0 +1,20 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} z\u00e1rva van", + "is_closing": "{entity_name} z\u00e1r\u00f3dik", + "is_open": "{entity_name} nyitva van", + "is_opening": "{entity_name} ny\u00edlik", + "is_position": "{entity_name} jelenlegi poz\u00edci\u00f3ja", + "is_tilt_position": "{entity_name} jelenlegi d\u00f6nt\u00e9si poz\u00edci\u00f3ja" + }, + "trigger_type": { + "closed": "{entity_name} bez\u00e1r\u00f3dott", + "closing": "{entity_name} z\u00e1r\u00f3dik", + "opened": "{entity_name} kiny\u00edlt", + "opening": "{entity_name} ny\u00edlik", + "position": "{entity_name} poz\u00edci\u00f3ja v\u00e1ltozik", + "tilt_position": "{entity_name} d\u00f6nt\u00e9si poz\u00edci\u00f3ja v\u00e1ltozik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/hu.json b/homeassistant/components/demo/.translations/hu.json new file mode 100644 index 00000000000000..51f0cd0064250b --- /dev/null +++ b/homeassistant/components/demo/.translations/hu.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Dem\u00f3" + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/bg.json b/homeassistant/components/device_tracker/.translations/bg.json index 471cbc6a53af2b..68affa5afd0464 100644 --- a/homeassistant/components/device_tracker/.translations/bg.json +++ b/homeassistant/components/device_tracker/.translations/bg.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u0435 \u0443 \u0434\u043e\u043c\u0430", "is_not_home": "{entity_name} \u043d\u0435 \u0435 \u0443 \u0434\u043e\u043c\u0430" } diff --git a/homeassistant/components/device_tracker/.translations/ca.json b/homeassistant/components/device_tracker/.translations/ca.json index de5aed41e3c239..3a95841559b225 100644 --- a/homeassistant/components/device_tracker/.translations/ca.json +++ b/homeassistant/components/device_tracker/.translations/ca.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u00e9s a casa", "is_not_home": "{entity_name} no \u00e9s a casa" } diff --git a/homeassistant/components/device_tracker/.translations/cs.json b/homeassistant/components/device_tracker/.translations/cs.json index 778ea0208c48af..7e82f1a34f8674 100644 --- a/homeassistant/components/device_tracker/.translations/cs.json +++ b/homeassistant/components/device_tracker/.translations/cs.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} je doma", "is_not_home": "{entity_name} nen\u00ed doma" } diff --git a/homeassistant/components/device_tracker/.translations/de.json b/homeassistant/components/device_tracker/.translations/de.json index 7e72bd5595a998..90a81db6b905b3 100644 --- a/homeassistant/components/device_tracker/.translations/de.json +++ b/homeassistant/components/device_tracker/.translations/de.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} ist Zuhause", "is_not_home": "{entity_name} ist nicht zu Hause" } diff --git a/homeassistant/components/device_tracker/.translations/en.json b/homeassistant/components/device_tracker/.translations/en.json index 25045e62b15ca0..1022608477eb1b 100644 --- a/homeassistant/components/device_tracker/.translations/en.json +++ b/homeassistant/components/device_tracker/.translations/en.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} is home", "is_not_home": "{entity_name} is not home" } diff --git a/homeassistant/components/device_tracker/.translations/es.json b/homeassistant/components/device_tracker/.translations/es.json index 00bda928b56f44..cfbf7bcfe3ebac 100644 --- a/homeassistant/components/device_tracker/.translations/es.json +++ b/homeassistant/components/device_tracker/.translations/es.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} est\u00e1 en casa", "is_not_home": "{entity_name} no est\u00e1 en casa" } diff --git a/homeassistant/components/device_tracker/.translations/fr.json b/homeassistant/components/device_tracker/.translations/fr.json index bf9033170c1551..4c59d5ea1c8a9b 100644 --- a/homeassistant/components/device_tracker/.translations/fr.json +++ b/homeassistant/components/device_tracker/.translations/fr.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} est \u00e0 la maison", "is_not_home": "{entity_name} n'est pas \u00e0 la maison" } diff --git a/homeassistant/components/device_tracker/.translations/it.json b/homeassistant/components/device_tracker/.translations/it.json index e2d35296152f41..112afc6689f36e 100644 --- a/homeassistant/components/device_tracker/.translations/it.json +++ b/homeassistant/components/device_tracker/.translations/it.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u00e8 in casa", "is_not_home": "{entity_name} non \u00e8 in casa" } diff --git a/homeassistant/components/device_tracker/.translations/ko.json b/homeassistant/components/device_tracker/.translations/ko.json index d258f67db22e21..92137ea27685d1 100644 --- a/homeassistant/components/device_tracker/.translations/ko.json +++ b/homeassistant/components/device_tracker/.translations/ko.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \uc774(\uac00) \uc9d1\uc5d0 \uc788\uc2b5\ub2c8\ub2e4", "is_not_home": "{entity_name} \uc774(\uac00) \uc678\ucd9c\uc911\uc785\ub2c8\ub2e4" } diff --git a/homeassistant/components/device_tracker/.translations/lb.json b/homeassistant/components/device_tracker/.translations/lb.json index 98a066ef8e83e7..2c49f692662770 100644 --- a/homeassistant/components/device_tracker/.translations/lb.json +++ b/homeassistant/components/device_tracker/.translations/lb.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} ass doheem", "is_not_home": "{entity_name} ass net doheem" } diff --git a/homeassistant/components/device_tracker/.translations/nl.json b/homeassistant/components/device_tracker/.translations/nl.json index d4de8b1f66a010..31ab788f171fa4 100644 --- a/homeassistant/components/device_tracker/.translations/nl.json +++ b/homeassistant/components/device_tracker/.translations/nl.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} is thuis", "is_not_home": "{entity_name} is niet thuis" } diff --git a/homeassistant/components/device_tracker/.translations/no.json b/homeassistant/components/device_tracker/.translations/no.json index 7034378b0666d7..d714b5b7d31c2f 100644 --- a/homeassistant/components/device_tracker/.translations/no.json +++ b/homeassistant/components/device_tracker/.translations/no.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} er hjemme", "is_not_home": "{entity_name} er ikke hjemme" } diff --git a/homeassistant/components/device_tracker/.translations/pl.json b/homeassistant/components/device_tracker/.translations/pl.json index 8f0f7953a2d516..3930031ad38f99 100644 --- a/homeassistant/components/device_tracker/.translations/pl.json +++ b/homeassistant/components/device_tracker/.translations/pl.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "urz\u0105dzenie {entity_name} jest w domu", "is_not_home": "urz\u0105dzenie {entity_name} jest poza domem" } diff --git a/homeassistant/components/device_tracker/.translations/pt.json b/homeassistant/components/device_tracker/.translations/pt.json index 952eb4b1475dfc..8a8f662183a295 100644 --- a/homeassistant/components/device_tracker/.translations/pt.json +++ b/homeassistant/components/device_tracker/.translations/pt.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} est\u00e1 em casa", "is_not_home": "{entity_name} n\u00e3o est\u00e1 em casa" } diff --git a/homeassistant/components/device_tracker/.translations/ru.json b/homeassistant/components/device_tracker/.translations/ru.json index 50a48ce942b431..58767361fd4ccc 100644 --- a/homeassistant/components/device_tracker/.translations/ru.json +++ b/homeassistant/components/device_tracker/.translations/ru.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u0434\u043e\u043c\u0430", "is_not_home": "{entity_name} \u043d\u0435 \u0434\u043e\u043c\u0430" } diff --git a/homeassistant/components/device_tracker/.translations/sl.json b/homeassistant/components/device_tracker/.translations/sl.json index f4784fbc664eb1..11d876883d3737 100644 --- a/homeassistant/components/device_tracker/.translations/sl.json +++ b/homeassistant/components/device_tracker/.translations/sl.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} je doma", "is_not_home": "{entity_name} ni doma" } diff --git a/homeassistant/components/device_tracker/.translations/zh-Hant.json b/homeassistant/components/device_tracker/.translations/zh-Hant.json index 4092031434ce97..456e09ebf0e8ad 100644 --- a/homeassistant/components/device_tracker/.translations/zh-Hant.json +++ b/homeassistant/components/device_tracker/.translations/zh-Hant.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u5728\u5bb6", "is_not_home": "{entity_name} \u4e0d\u5728\u5bb6" } diff --git a/homeassistant/components/fan/.translations/bg.json b/homeassistant/components/fan/.translations/bg.json index 62452e67179f2a..f678c87096823c 100644 --- a/homeassistant/components/fan/.translations/bg.json +++ b/homeassistant/components/fan/.translations/bg.json @@ -4,7 +4,7 @@ "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d" }, diff --git a/homeassistant/components/fan/.translations/ca.json b/homeassistant/components/fan/.translations/ca.json index 0530ccf5a85a78..e2f3ce2b0a40c5 100644 --- a/homeassistant/components/fan/.translations/ca.json +++ b/homeassistant/components/fan/.translations/ca.json @@ -4,7 +4,7 @@ "turn_off": "Apaga {entity_name}", "turn_on": "Enc\u00e9n {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est\u00e0 apagat", "is_on": "{entity_name} est\u00e0 enc\u00e8s" }, diff --git a/homeassistant/components/fan/.translations/de.json b/homeassistant/components/fan/.translations/de.json index 9ac3d9993706b9..9c3559b7cfc961 100644 --- a/homeassistant/components/fan/.translations/de.json +++ b/homeassistant/components/fan/.translations/de.json @@ -4,7 +4,7 @@ "turn_off": "Schalte {entity_name} aus.", "turn_on": "Schalte {entity_name} ein." }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} ist ausgeschaltet", "is_on": "{entity_name} ist eingeschaltet" }, diff --git a/homeassistant/components/fan/.translations/en.json b/homeassistant/components/fan/.translations/en.json index b085e7baa45a04..c27d983ca2e90a 100644 --- a/homeassistant/components/fan/.translations/en.json +++ b/homeassistant/components/fan/.translations/en.json @@ -4,7 +4,7 @@ "turn_off": "Turn off {entity_name}", "turn_on": "Turn on {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} is off", "is_on": "{entity_name} is on" }, diff --git a/homeassistant/components/fan/.translations/es.json b/homeassistant/components/fan/.translations/es.json index d92153a63021af..4ceefe9c721286 100644 --- a/homeassistant/components/fan/.translations/es.json +++ b/homeassistant/components/fan/.translations/es.json @@ -4,7 +4,7 @@ "turn_off": "Desactivar {entity_name}", "turn_on": "Activar {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est\u00e1 desactivado", "is_on": "{entity_name} est\u00e1 activado" }, diff --git a/homeassistant/components/fan/.translations/fr.json b/homeassistant/components/fan/.translations/fr.json index 5c5a65b6bcddce..e6944dab781b50 100644 --- a/homeassistant/components/fan/.translations/fr.json +++ b/homeassistant/components/fan/.translations/fr.json @@ -4,7 +4,7 @@ "turn_off": "\u00c9teindre {entity_name}", "turn_on": "Allumer {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est d\u00e9sactiv\u00e9", "is_on": "{entity_name} est activ\u00e9" }, diff --git a/homeassistant/components/fan/.translations/it.json b/homeassistant/components/fan/.translations/it.json index b62d80c793b8fe..4fab847f1cb65f 100644 --- a/homeassistant/components/fan/.translations/it.json +++ b/homeassistant/components/fan/.translations/it.json @@ -4,7 +4,7 @@ "turn_off": "Spegnere {entity_name}", "turn_on": "Accendere {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} \u00e8 spento", "is_on": "{entity_name} \u00e8 acceso" }, diff --git a/homeassistant/components/fan/.translations/lb.json b/homeassistant/components/fan/.translations/lb.json index 316a77d471d8ef..f5170949badf4d 100644 --- a/homeassistant/components/fan/.translations/lb.json +++ b/homeassistant/components/fan/.translations/lb.json @@ -4,7 +4,7 @@ "turn_off": "{entity_name} ausschalten", "turn_on": "{entity_name} uschalten" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} ass aus", "is_on": "{entity_name} ass un" }, diff --git a/homeassistant/components/fan/.translations/nl.json b/homeassistant/components/fan/.translations/nl.json index 706c2c92b19183..4837b301ea7f6a 100644 --- a/homeassistant/components/fan/.translations/nl.json +++ b/homeassistant/components/fan/.translations/nl.json @@ -4,7 +4,7 @@ "turn_off": "Schakel {entity_name} uit", "turn_on": "Schakel {entity_name} in" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} is uitgeschakeld", "is_on": "{entity_name} is ingeschakeld" }, diff --git a/homeassistant/components/fan/.translations/no.json b/homeassistant/components/fan/.translations/no.json index 73917ac45c4e72..aa6320f0a657b6 100644 --- a/homeassistant/components/fan/.translations/no.json +++ b/homeassistant/components/fan/.translations/no.json @@ -4,7 +4,7 @@ "turn_off": "Sl\u00e5 av {entity_name}", "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} er av", "is_on": "{entity_name} er p\u00e5" }, diff --git a/homeassistant/components/fan/.translations/pl.json b/homeassistant/components/fan/.translations/pl.json index 424794a5b64a43..709a63c238996b 100644 --- a/homeassistant/components/fan/.translations/pl.json +++ b/homeassistant/components/fan/.translations/pl.json @@ -4,7 +4,7 @@ "turn_off": "wy\u0142\u0105cz {entity_name}", "turn_on": "w\u0142\u0105cz {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "wentylator (entity_name} jest wy\u0142\u0105czony", "is_on": "wentylator (entity_name} jest w\u0142\u0105czony" }, diff --git a/homeassistant/components/fan/.translations/pt-BR.json b/homeassistant/components/fan/.translations/pt-BR.json index 86b10b0f909e8e..6b95464bdbcdb2 100644 --- a/homeassistant/components/fan/.translations/pt-BR.json +++ b/homeassistant/components/fan/.translations/pt-BR.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est\u00e1 desligado", "is_on": "{entity_name} est\u00e1 ligado" } diff --git a/homeassistant/components/fan/.translations/pt.json b/homeassistant/components/fan/.translations/pt.json index a76550cbeddec2..ab78bc776bdd14 100644 --- a/homeassistant/components/fan/.translations/pt.json +++ b/homeassistant/components/fan/.translations/pt.json @@ -4,7 +4,7 @@ "turn_off": "Desligar {entity_name}", "turn_on": "Ligar {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est\u00e1 desligada", "is_on": "{entity_name} est\u00e1 ligada" }, diff --git a/homeassistant/components/fan/.translations/ru.json b/homeassistant/components/fan/.translations/ru.json index 4fd5ebe28c5e60..157c78975cb420 100644 --- a/homeassistant/components/fan/.translations/ru.json +++ b/homeassistant/components/fan/.translations/ru.json @@ -4,7 +4,7 @@ "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" }, diff --git a/homeassistant/components/fan/.translations/sl.json b/homeassistant/components/fan/.translations/sl.json index a5de109f764bc1..a2bca3352ab28d 100644 --- a/homeassistant/components/fan/.translations/sl.json +++ b/homeassistant/components/fan/.translations/sl.json @@ -4,7 +4,7 @@ "turn_off": "Izklopite {entity_name}", "turn_on": "Vklopite {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} je izklopljen", "is_on": "{entity_name} je vklopljen" }, diff --git a/homeassistant/components/fan/.translations/zh-Hant.json b/homeassistant/components/fan/.translations/zh-Hant.json index 4b34f6e0165b22..78c0d991125e46 100644 --- a/homeassistant/components/fan/.translations/zh-Hant.json +++ b/homeassistant/components/fan/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "turn_off": "\u95dc\u9589 {entity_name}", "turn_on": "\u958b\u555f {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} \u95dc\u9589", "is_on": "{entity_name} \u958b\u555f" }, diff --git a/homeassistant/components/neato/.translations/ko.json b/homeassistant/components/neato/.translations/ko.json index aeb591f7b20ccd..391d0aee191860 100644 --- a/homeassistant/components/neato/.translations/ko.json +++ b/homeassistant/components/neato/.translations/ko.json @@ -5,7 +5,7 @@ "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "create_entry": { - "default": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "[Neato \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "error": { "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", @@ -18,7 +18,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "vendor": "\uacf5\uae09 \uc5c5\uccb4" }, - "description": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "description": "[Neato \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "title": "Neato \uacc4\uc815 \uc815\ubcf4" } }, diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json index 93e9a1e66c4cff..fdf180a1a6115d 100644 --- a/homeassistant/components/soma/.translations/lb.json +++ b/homeassistant/components/soma/.translations/lb.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Soma Kont konfigur\u00e9ieren.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "missing_configuration": "D'Soma Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + "connection_error": "Feeler beim verbannen mat SOMA Connect.", + "missing_configuration": "D'Soma Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.", + "result_error": "SOMA Connect \u00e4ntwert mat engem Feeler Code." }, "create_entry": { "default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert." diff --git a/homeassistant/components/vacuum/.translations/bg.json b/homeassistant/components/vacuum/.translations/bg.json index 2d422284a38915..1ab7fce7abe2b1 100644 --- a/homeassistant/components/vacuum/.translations/bg.json +++ b/homeassistant/components/vacuum/.translations/bg.json @@ -4,7 +4,7 @@ "clean": "\u041d\u0435\u043a\u0430 {entity_name} \u043f\u043e\u0447\u0438\u0441\u0442\u0438", "dock": "\u041d\u0435\u043a\u0430 {entity_name} \u0434\u0430 \u0441\u0435 \u0432\u044a\u0440\u043d\u0435 \u0432 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} \u043f\u043e\u0447\u0438\u0441\u0442\u0432\u0430", "is_docked": "{entity_name} \u0435 \u0432 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" }, diff --git a/homeassistant/components/vacuum/.translations/ca.json b/homeassistant/components/vacuum/.translations/ca.json index ee69152ed5c548..b3cdbb2f6c724d 100644 --- a/homeassistant/components/vacuum/.translations/ca.json +++ b/homeassistant/components/vacuum/.translations/ca.json @@ -4,7 +4,7 @@ "clean": "Fes que {entity_name} netegi", "dock": "Fes que {entity_name} torni a la base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} est\u00e0 netejant", "is_docked": "{entity_name} est\u00e0 acoblada" }, diff --git a/homeassistant/components/vacuum/.translations/de.json b/homeassistant/components/vacuum/.translations/de.json index 7aed7da23e3f92..3fe2d57eb01435 100644 --- a/homeassistant/components/vacuum/.translations/de.json +++ b/homeassistant/components/vacuum/.translations/de.json @@ -4,7 +4,7 @@ "clean": "Lass {entity_name} reinigen", "dock": "Lass {entity_name} zum Dock zur\u00fcckkehren" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} reinigt", "is_docked": "{entity_name} ist angedockt" }, diff --git a/homeassistant/components/vacuum/.translations/en.json b/homeassistant/components/vacuum/.translations/en.json index 396c6a83be996f..3feb8eada729cc 100644 --- a/homeassistant/components/vacuum/.translations/en.json +++ b/homeassistant/components/vacuum/.translations/en.json @@ -4,7 +4,7 @@ "clean": "Let {entity_name} clean", "dock": "Let {entity_name} return to the dock" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} is cleaning", "is_docked": "{entity_name} is docked" }, diff --git a/homeassistant/components/vacuum/.translations/es.json b/homeassistant/components/vacuum/.translations/es.json index f99e94a40a3855..376058faafa457 100644 --- a/homeassistant/components/vacuum/.translations/es.json +++ b/homeassistant/components/vacuum/.translations/es.json @@ -4,7 +4,7 @@ "clean": "Deje que {entity_name} limpie", "dock": "Deje que {entity_name} regrese a la base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} est\u00e1 limpiando", "is_docked": "{entity_name} en la base" }, diff --git a/homeassistant/components/vacuum/.translations/fr.json b/homeassistant/components/vacuum/.translations/fr.json index 4a0ab7f8de7255..84d5e17bda15f6 100644 --- a/homeassistant/components/vacuum/.translations/fr.json +++ b/homeassistant/components/vacuum/.translations/fr.json @@ -4,7 +4,7 @@ "clean": "Laisser {entity_name} vide", "dock": "Laisser {entity_name} retourner \u00e0 la base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} nettoie", "is_docked": "{entity_name} est connect\u00e9" }, diff --git a/homeassistant/components/vacuum/.translations/it.json b/homeassistant/components/vacuum/.translations/it.json index 0b879f154fa064..32ecd1e0377c41 100644 --- a/homeassistant/components/vacuum/.translations/it.json +++ b/homeassistant/components/vacuum/.translations/it.json @@ -4,7 +4,7 @@ "clean": "Lascia pulire {entity_name}", "dock": "Lascia che {entity_name} ritorni alla base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} sta pulendo", "is_docked": "{entity_name} \u00e8 agganciato alla base" }, diff --git a/homeassistant/components/vacuum/.translations/lb.json b/homeassistant/components/vacuum/.translations/lb.json index 6d984b997fa99c..d6776ccd6192c7 100644 --- a/homeassistant/components/vacuum/.translations/lb.json +++ b/homeassistant/components/vacuum/.translations/lb.json @@ -4,7 +4,7 @@ "clean": "Looss {entity_name} botzen", "dock": "Sch\u00e9ck {entity_name} z\u00e9reck zur Statioun" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} botzt", "is_docked": "{entity_name} ass an der Statioun" }, diff --git a/homeassistant/components/vacuum/.translations/nl.json b/homeassistant/components/vacuum/.translations/nl.json index 3032fc22508f34..3e49c926d2db2e 100644 --- a/homeassistant/components/vacuum/.translations/nl.json +++ b/homeassistant/components/vacuum/.translations/nl.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} is aan het schoonmaken" }, "trigger_type": { diff --git a/homeassistant/components/vacuum/.translations/no.json b/homeassistant/components/vacuum/.translations/no.json index 7d6475f8cef298..0c34081cb2f7b5 100644 --- a/homeassistant/components/vacuum/.translations/no.json +++ b/homeassistant/components/vacuum/.translations/no.json @@ -4,7 +4,7 @@ "clean": "La {entity_name} rengj\u00f8res", "dock": "La {entity_name} tilbake til dock" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} rengj\u00f8res", "is_docked": "{entity_name} er docked" }, diff --git a/homeassistant/components/vacuum/.translations/pl.json b/homeassistant/components/vacuum/.translations/pl.json index e637c26b3edecf..09eef23ac9a217 100644 --- a/homeassistant/components/vacuum/.translations/pl.json +++ b/homeassistant/components/vacuum/.translations/pl.json @@ -4,7 +4,7 @@ "clean": "niech {entity_name} sprz\u0105ta", "dock": "niech {entity_name} wr\u00f3ci do bazy" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} sprz\u0105ta", "is_docked": "{entity_name} jest w bazie" }, diff --git a/homeassistant/components/vacuum/.translations/pt.json b/homeassistant/components/vacuum/.translations/pt.json index 42b8bdabc0f344..15b8ac3fd19884 100644 --- a/homeassistant/components/vacuum/.translations/pt.json +++ b/homeassistant/components/vacuum/.translations/pt.json @@ -3,7 +3,7 @@ "action_type": { "clean": "Deixar {entity_name} limpar" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} est\u00e1 a limpar" } } diff --git a/homeassistant/components/vacuum/.translations/ru.json b/homeassistant/components/vacuum/.translations/ru.json index c727e8f6ea3d06..c42f0310fae893 100644 --- a/homeassistant/components/vacuum/.translations/ru.json +++ b/homeassistant/components/vacuum/.translations/ru.json @@ -4,7 +4,7 @@ "clean": "\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c {entity_name} \u0434\u0435\u043b\u0430\u0442\u044c \u0443\u0431\u043e\u0440\u043a\u0443", "dock": "\u0412\u0435\u0440\u043d\u0443\u0442\u044c {entity_name} \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u044e" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} \u0434\u0435\u043b\u0430\u0435\u0442 \u0443\u0431\u043e\u0440\u043a\u0443", "is_docked": "{entity_name} \u0443 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u0438" }, diff --git a/homeassistant/components/vacuum/.translations/sl.json b/homeassistant/components/vacuum/.translations/sl.json index 25de303b157a9e..c594c4f1bdde4a 100644 --- a/homeassistant/components/vacuum/.translations/sl.json +++ b/homeassistant/components/vacuum/.translations/sl.json @@ -4,7 +4,7 @@ "clean": "Naj {entity_name} \u010disti", "dock": "Pustite, da se {entity_name} vrne na dok" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} \u010disti", "is_docked": "{entity_name} je priklju\u010den" }, diff --git a/homeassistant/components/vacuum/.translations/zh-Hant.json b/homeassistant/components/vacuum/.translations/zh-Hant.json index f0ad431afc944a..b406e1baedefba 100644 --- a/homeassistant/components/vacuum/.translations/zh-Hant.json +++ b/homeassistant/components/vacuum/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "clean": "\u555f\u52d5 {entity_name} \u6e05\u9664", "dock": "\u555f\u52d5 {entity_name} \u56de\u5230\u5145\u96fb\u7ad9" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} \u6b63\u5728\u6e05\u6383", "is_docked": "{entity_name} \u65bc\u5145\u96fb\u7ad9" }, From 78e831b08ed1eae81620cef4cb43672d8c566eb2 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Mon, 16 Dec 2019 17:24:50 -0800 Subject: [PATCH 2453/3953] Make tplink light more responsive (#28652) * Making tplink light more responsive. * Adding light platform tests. * Addressing PR feedback. * Mocking the module, not the api. * Using sync method for background update. --- .coveragerc | 1 - homeassistant/components/tplink/const.py | 2 + homeassistant/components/tplink/light.py | 27 ++- tests/components/tplink/test_light.py | 220 +++++++++++++++++++++++ 4 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 tests/components/tplink/test_light.py diff --git a/.coveragerc b/.coveragerc index e16a622cc61edf..fc7a4691ef2918 100644 --- a/.coveragerc +++ b/.coveragerc @@ -708,7 +708,6 @@ omit = homeassistant/components/totalconnect/* homeassistant/components/touchline/climate.py homeassistant/components/tplink/device_tracker.py - homeassistant/components/tplink/light.py homeassistant/components/tplink/switch.py homeassistant/components/tplink_lte/* homeassistant/components/traccar/device_tracker.py diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 583c25e285c02f..8b85b8afd74240 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -1,3 +1,5 @@ """Const for TP-Link.""" +import datetime DOMAIN = "tplink" +MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=8) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 117ebf750250f7..ec3307fc87e3f0 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -126,23 +126,27 @@ def device_state_attributes(self): def turn_on(self, **kwargs): """Turn the light on.""" + self._state = True self.smartbulb.state = SmartBulb.BULB_STATE_ON if ATTR_COLOR_TEMP in kwargs: - self.smartbulb.color_temp = mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) + self._color_temp = kwargs.get(ATTR_COLOR_TEMP) + self.smartbulb.color_temp = mired_to_kelvin(self._color_temp) - brightness = brightness_to_percentage( - kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) - ) + brightness_value = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) + brightness_pct = brightness_to_percentage(brightness_value) if ATTR_HS_COLOR in kwargs: - hue, sat = kwargs.get(ATTR_HS_COLOR) - hsv = (int(hue), int(sat), brightness) + self._hs = kwargs.get(ATTR_HS_COLOR) + hue, sat = self._hs + hsv = (int(hue), int(sat), brightness_pct) self.smartbulb.hsv = hsv elif ATTR_BRIGHTNESS in kwargs: - self.smartbulb.brightness = brightness + self._brightness = brightness_value + self.smartbulb.brightness = brightness_pct def turn_off(self, **kwargs): """Turn the light off.""" + self._state = False self.smartbulb.state = SmartBulb.BULB_STATE_OFF @property @@ -177,6 +181,15 @@ def is_on(self): def update(self): """Update the TP-Link Bulb's state.""" + if self._supported_features is None: + # First run, update by blocking. + self.do_update() + else: + # Not first run, update in the background. + self.hass.add_job(self.do_update) + + def do_update(self): + """Update states.""" try: if self._supported_features is None: self.get_features() diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py new file mode 100644 index 00000000000000..8d1d4d94738884 --- /dev/null +++ b/tests/components/tplink/test_light.py @@ -0,0 +1,220 @@ +"""Tests for light platform.""" +from unittest.mock import patch + +from pyHS100 import SmartBulb + +from homeassistant.components import tplink +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.components.tplink.common import CONF_DISCOVERY, CONF_LIGHT +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_light(hass: HomeAssistant) -> None: + """Test function.""" + sys_info = { + "sw_ver": "1.2.3", + "hw_ver": "2.3.4", + "mac": "aa:bb:cc:dd:ee:ff", + "mic_mac": "00:11:22:33:44", + "type": "light", + "hwId": "1234", + "fwId": "4567", + "oemId": "891011", + "dev_name": "light1", + "rssi": 11, + "latitude": "0", + "longitude": "0", + "is_color": True, + "is_dimmable": True, + "is_variable_color_temp": True, + "model": "LB120", + "alias": "light1", + } + + light_state = { + "on_off": SmartBulb.BULB_STATE_ON, + "dft_on_state": { + "brightness": 12, + "color_temp": 3200, + "hue": 100, + "saturation": 200, + }, + "brightness": 13, + "color_temp": 3300, + "hue": 110, + "saturation": 210, + } + + def set_light_state(state): + nonlocal light_state + light_state.update(state) + + set_light_state_patch = patch( + "homeassistant.components.tplink.common.SmartBulb.set_light_state", + side_effect=set_light_state, + ) + get_light_state_patch = patch( + "homeassistant.components.tplink.common.SmartBulb.get_light_state", + return_value=light_state, + ) + current_consumption_patch = patch( + "homeassistant.components.tplink.common.SmartDevice.current_consumption", + return_value=3.23, + ) + get_sysinfo_patch = patch( + "homeassistant.components.tplink.common.SmartDevice.get_sysinfo", + return_value=sys_info, + ) + get_emeter_daily_patch = patch( + "homeassistant.components.tplink.common.SmartDevice.get_emeter_daily", + return_value={ + 1: 1.01, + 2: 1.02, + 3: 1.03, + 4: 1.04, + 5: 1.05, + 6: 1.06, + 7: 1.07, + 8: 1.08, + 9: 1.09, + 10: 1.10, + 11: 1.11, + 12: 1.12, + }, + ) + get_emeter_monthly_patch = patch( + "homeassistant.components.tplink.common.SmartDevice.get_emeter_monthly", + return_value={ + 1: 2.01, + 2: 2.02, + 3: 2.03, + 4: 2.04, + 5: 2.05, + 6: 2.06, + 7: 2.07, + 8: 2.08, + 9: 2.09, + 10: 2.10, + 11: 2.11, + 12: 2.12, + }, + ) + + with set_light_state_patch, get_light_state_patch, current_consumption_patch, get_sysinfo_patch, get_emeter_daily_patch, get_emeter_monthly_patch: + await async_setup_component( + hass, + tplink.DOMAIN, + { + tplink.DOMAIN: { + CONF_DISCOVERY: False, + CONF_LIGHT: [{CONF_HOST: "123.123.123.123"}], + } + }, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.light1"}, + blocking=True, + ) + + assert hass.states.get("light.light1").state == "off" + assert light_state["on_off"] == 0 + + await hass.async_block_till_done() + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.light1", + ATTR_COLOR_TEMP: 312, + ATTR_BRIGHTNESS: 50, + }, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("light.light1") + assert state.state == "on" + assert state.attributes["brightness"] == 48.45 + assert state.attributes["hs_color"] == (110, 210) + assert state.attributes["color_temp"] == 312 + assert light_state["on_off"] == 1 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.light1", + ATTR_BRIGHTNESS: 55, + ATTR_HS_COLOR: (23, 27), + }, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("light.light1") + assert state.state == "on" + assert state.attributes["brightness"] == 53.55 + assert state.attributes["hs_color"] == (23, 27) + assert state.attributes["color_temp"] == 312 + assert light_state["brightness"] == 21 + assert light_state["hue"] == 23 + assert light_state["saturation"] == 27 + + light_state["on_off"] = 0 + light_state["dft_on_state"]["on_off"] = 0 + light_state["brightness"] = 66 + light_state["dft_on_state"]["brightness"] = 66 + light_state["color_temp"] = 6400 + light_state["dft_on_state"]["color_temp"] = 123 + light_state["hue"] = 77 + light_state["dft_on_state"]["hue"] = 77 + light_state["saturation"] = 78 + light_state["dft_on_state"]["saturation"] = 78 + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.light1"}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("light.light1") + assert state.state == "off" + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.light1"}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("light.light1") + assert state.attributes["brightness"] == 168.3 + assert state.attributes["hs_color"] == (77, 78) + assert state.attributes["color_temp"] == 156 + assert light_state["brightness"] == 66 + assert light_state["hue"] == 77 + assert light_state["saturation"] == 78 From 115aa2e49c97afeb9f77e7271fdd104a2261a482 Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Tue, 17 Dec 2019 15:26:19 +0100 Subject: [PATCH 2454/3953] Z-Wave: Fibaro FGR*-222: Add venetian blind support (#29701) The Fibaro FGR-222/FGRM-2222 ZWave roller shutter devices have a proprietary command class to support setting the tilt angle of venetian blinds (= type of window cover). This PR adds the support to HA for this. This allows the user to set the height of the blinds and the tilt angle separately. Original patch by @ChristianKuehnel #24405. --- homeassistant/components/zwave/__init__.py | 5 + homeassistant/components/zwave/cover.py | 144 +++++++++++++++++++++ 2 files changed, 149 insertions(+) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 6dd007fb8a8b2f..cb494b5fa6f290 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -79,12 +79,14 @@ CONF_DEVICE_CONFIG = "device_config" CONF_DEVICE_CONFIG_GLOB = "device_config_glob" CONF_DEVICE_CONFIG_DOMAIN = "device_config_domain" +CONF_TILT_OPEN_POSITION = "tilt_open_position" DEFAULT_CONF_IGNORED = False DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False DEFAULT_CONF_INVERT_PERCENT = False DEFAULT_CONF_REFRESH_VALUE = False DEFAULT_CONF_REFRESH_DELAY = 5 +DEFAULT_CONF_TILT_OPEN_POSITION = 50 SUPPORTED_PLATFORMS = [ "binary_sensor", @@ -214,6 +216,9 @@ vol.Optional( CONF_REFRESH_DELAY, default=DEFAULT_CONF_REFRESH_DELAY ): cv.positive_int, + vol.Optional( + CONF_TILT_OPEN_POSITION, default=DEFAULT_CONF_TILT_OPEN_POSITION + ): cv.positive_int, } ) diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py index 95cc994e4ff7ec..5b4fb0c9934a2a 100644 --- a/homeassistant/components/zwave/cover.py +++ b/homeassistant/components/zwave/cover.py @@ -3,6 +3,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, + ATTR_TILT_POSITION, DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, @@ -14,11 +15,13 @@ from . import ( CONF_INVERT_OPENCLOSE_BUTTONS, CONF_INVERT_PERCENT, + CONF_TILT_OPEN_POSITION, ZWaveDeviceEntity, workaround, ) from .const import ( COMMAND_CLASS_BARRIER_OPERATOR, + COMMAND_CLASS_MANUFACTURER_PROPRIETARY, COMMAND_CLASS_SWITCH_BINARY, COMMAND_CLASS_SWITCH_MULTILEVEL, DATA_NETWORK, @@ -29,6 +32,23 @@ SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE +def _to_hex_str(id_in_bytes): + """Convert a two byte value to a hex string. + + Example: 0x1234 --> '0x1234' + """ + return "0x{:04x}".format(id_in_bytes) + + +# For some reason node.manufacturer_id is of type string. So we need to convert +# the values. +FIBARO = _to_hex_str(workaround.FIBARO) +FIBARO222_SHUTTERS = [ + _to_hex_str(workaround.FGR222_SHUTTER2), + _to_hex_str(workaround.FGRM222_SHUTTER2), +] + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old method of setting up Z-Wave covers.""" pass @@ -53,6 +73,17 @@ def get_device(hass, values, node_config, **kwargs): values.primary.command_class == COMMAND_CLASS_SWITCH_MULTILEVEL and values.primary.index == 0 ): + if ( + values.primary.node.manufacturer_id == FIBARO + and values.primary.node.product_type in FIBARO222_SHUTTERS + ): + return FibaroFGRM222( + hass, + values, + invert_buttons, + invert_percent, + node_config.get(CONF_TILT_OPEN_POSITION), + ) return ZwaveRollershutter(hass, values, invert_buttons, invert_percent) if values.primary.command_class == COMMAND_CLASS_SWITCH_BINARY: return ZwaveGarageDoorSwitch(values) @@ -212,3 +243,116 @@ def close_cover(self, **kwargs): def open_cover(self, **kwargs): """Open the garage door.""" self.values.primary.data = "Opened" + + +class FibaroFGRM222(ZwaveRollershutter): + """Implementation of proprietary features for Fibaro FGR-222 / FGRM-222. + + This adds support for the tilt feature for the ventian blind mode. + To enable this you need to configure the devices to use the venetian blind + mode and to enable the proprietary command class: + * Set "3: Reports type to Blind position reports sent" + to value "the main controller using Fibaro Command Class" + * Set "10: Roller Shutter operating modes" + to value "2 - Venetian Blind Mode, with positioning" + """ + + def __init__( + self, hass, values, invert_buttons, invert_percent, open_tilt_position: int + ): + """Initialize the FGRM-222.""" + self._value_blinds = None + self._value_tilt = None + self._has_tilt_mode = False # type: bool + self._open_tilt_position = 50 # type: int + if open_tilt_position is not None: + self._open_tilt_position = open_tilt_position + super().__init__(hass, values, invert_buttons, invert_percent) + + @property + def current_cover_tilt_position(self) -> int: + """Get the tilt of the blinds. + + Saturate values <5 and >94 so that it's easier to detect the end + positions in automations. + """ + if not self._has_tilt_mode: + return None + if self._value_tilt.data <= 5: + return 0 + if self._value_tilt.data >= 95: + return 100 + return self._value_tilt.data + + def set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + if not self._has_tilt_mode: + _LOGGER.error("Can't set cover tilt as device is not yet set up.") + else: + # Limit the range to [0-99], as this what that the ZWave command + # accepts. + tilt_position = max(0, min(99, kwargs.get(ATTR_TILT_POSITION))) + _LOGGER.debug("setting tilt to %d", tilt_position) + self._value_tilt.data = tilt_position + + def open_cover_tilt(self, **kwargs): + """Set slats to horizontal position.""" + self.set_cover_tilt_position(tilt_position=self._open_tilt_position) + + def close_cover_tilt(self, **kwargs): + """Close the slats.""" + self.set_cover_tilt_position(tilt_position=0) + + def set_cover_position(self, **kwargs): + """Move the roller shutter to a specific position. + + If the venetian blinds mode is not activated, fall back to + the behavior of the parent class. + """ + if not self._has_tilt_mode: + super().set_cover_position(**kwargs) + else: + _LOGGER.debug("Setting cover position to %s", kwargs.get(ATTR_POSITION)) + self._value_blinds.data = kwargs.get(ATTR_POSITION) + + def _configure_values(self): + """Get the value objects from the node.""" + for value in self.node.get_values( + class_id=COMMAND_CLASS_MANUFACTURER_PROPRIETARY + ).values(): + if value is None: + continue + if value.index == 0: + self._value_blinds = value + elif value.index == 1: + self._value_tilt = value + else: + _LOGGER.warning( + "Undefined index %d for this command class", value.index + ) + + if self._value_tilt is not None: + # We reached here because the user has configured the Fibaro to + # report using the MANUFACTURER_PROPRIETARY command class. The only + # reason for the user to configure this way is if tilt support is + # needed (aka venetian blind mode). Therefore, turn it on. + # + # Note: This is safe to do even if the user has accidentally set + # this configuration parameter, or configuration parameter 10 to + # something other than venetian blind mode. The controller will just + # ignore potential tilt settings sent from home assistant in this + # case. + self._has_tilt_mode = True + _LOGGER.info( + "Zwave node %s is a Fibaro FGR-222/FGRM-222 with tilt support.", + self.node_id, + ) + + def update_properties(self): + """React on properties being updated.""" + if not self._has_tilt_mode: + self._configure_values() + if self._value_blinds is not None: + self._current_position = self._value_blinds.data + else: + super().update_properties() From 31cd0af47a2a6784eb197885931720ca860dfd0b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 17 Dec 2019 16:26:36 +0100 Subject: [PATCH 2455/3953] Upgrade matrix-client to 0.3.2 (#30027) --- homeassistant/components/matrix/__init__.py | 41 +++++++------------ homeassistant/components/matrix/const.py | 4 ++ homeassistant/components/matrix/manifest.json | 2 +- homeassistant/components/matrix/notify.py | 13 +++--- homeassistant/components/matrix/services.yaml | 4 +- requirements_all.txt | 2 +- 6 files changed, 30 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/matrix/const.py diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 71735bd7e51b6b..f8a57572d04a8b 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -1,4 +1,4 @@ -"""The matrix bot component.""" +"""The Matrix bot component.""" from functools import partial import logging import os @@ -19,6 +19,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json +from .const import DOMAIN, SERVICE_SEND_MESSAGE + _LOGGER = logging.getLogger(__name__) SESSION_FILE = ".matrix.conf" @@ -31,10 +33,7 @@ EVENT_MATRIX_COMMAND = "matrix_command" -DOMAIN = "matrix" - COMMAND_SCHEMA = vol.All( - # Basic Schema vol.Schema( { vol.Exclusive(CONF_WORD, "trigger"): cv.string, @@ -43,7 +42,6 @@ vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, [cv.string]), } ), - # Make sure it's either a word or an expression command cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION), ) @@ -65,7 +63,6 @@ extra=vol.ALLOW_EXTRA, ) -SERVICE_SEND_MESSAGE = "send_message" SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema( { @@ -77,7 +74,6 @@ def setup(hass, config): """Set up the Matrix bot component.""" - config = config[DOMAIN] try: @@ -138,11 +134,11 @@ def __init__( # so we only do it once per room. self._aliases_fetched_for = set() - # word commands are stored dict-of-dict: First dict indexes by room ID + # Word commands are stored dict-of-dict: First dict indexes by room ID # / alias, second dict indexes by the word self._word_commands = {} - # regular expression commands are stored as a list of commands per + # Regular expression commands are stored as a list of commands per # room, i.e., a dict-of-list self._expression_commands = {} @@ -184,7 +180,7 @@ def handle_startup(_): self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, handle_startup) def _handle_room_message(self, room_id, room, event): - """Handle a message sent to a room.""" + """Handle a message sent to a Matrix room.""" if event["content"]["msgtype"] != "m.text": return @@ -194,7 +190,7 @@ def _handle_room_message(self, room_id, room, event): _LOGGER.debug("Handling message: %s", event["content"]["body"]) if event["content"]["body"][0] == "!": - # Could trigger a single-word command. + # Could trigger a single-word command pieces = event["content"]["body"].split(" ") cmd = pieces[0][1:] @@ -248,8 +244,7 @@ def _join_or_get_room(self, room_id_or_alias): return room def _join_rooms(self): - """Join the rooms that we listen for commands in.""" - + """Join the Matrix rooms that we listen for commands in.""" for room_id in self._listening_rooms: try: room = self._join_or_get_room(room_id) @@ -285,8 +280,7 @@ def _store_auth_token(self, token): save_json(self._session_filepath, self._auth_tokens) def _login(self): - """Login to the matrix homeserver and return the client instance.""" - + """Login to the Matrix homeserver and return the client instance.""" # Attempt to generate a valid client using either of the two possible # login methods: client = None @@ -299,13 +293,12 @@ def _login(self): except MatrixRequestError as ex: _LOGGER.warning( - "Login by token failed, falling back to password. " - "login_by_token raised: (%d) %s", + "Login by token failed, falling back to password: %d, %s", ex.code, ex.content, ) - # If we still don't have a client try password. + # If we still don't have a client try password if not client: try: client = self._login_by_password() @@ -313,20 +306,17 @@ def _login(self): except MatrixRequestError as ex: _LOGGER.error( - "Login failed, both token and username/password invalid " - "login_by_password raised: (%d) %s", + "Login failed, both token and username/password invalid: %d, %s", ex.code, ex.content, ) - - # re-raise the error so _setup can catch it. + # Re-raise the error so _setup can catch it raise return client def _login_by_token(self): """Login using authentication token and return the client.""" - return MatrixClient( base_url=self._homeserver, token=self._auth_tokens[self._mx_id], @@ -336,7 +326,6 @@ def _login_by_token(self): def _login_by_password(self): """Login using password authentication and return the client.""" - _client = MatrixClient( base_url=self._homeserver, valid_cert_check=self._verify_tls ) @@ -348,7 +337,7 @@ def _login_by_password(self): return _client def _send_message(self, message, target_rooms): - """Send the message to the matrix server.""" + """Send the message to the Matrix server.""" for target_room in target_rooms: try: @@ -356,7 +345,7 @@ def _send_message(self, message, target_rooms): _LOGGER.debug(room.send_text(message)) except MatrixRequestError as ex: _LOGGER.error( - "Unable to deliver message to room '%s': (%d): %s", + "Unable to deliver message to room '%s': %d, %s", target_room, ex.code, ex.content, diff --git a/homeassistant/components/matrix/const.py b/homeassistant/components/matrix/const.py new file mode 100644 index 00000000000000..6b082bde121a98 --- /dev/null +++ b/homeassistant/components/matrix/const.py @@ -0,0 +1,4 @@ +"""Constants for the Matrix integration.""" +DOMAIN = "matrix" + +SERVICE_SEND_MESSAGE = "send_message" diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index a467518c04ef87..e7d1fab6874ce5 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -3,7 +3,7 @@ "name": "Matrix", "documentation": "https://www.home-assistant.io/integrations/matrix", "requirements": [ - "matrix-client==0.2.0" + "matrix-client==0.3.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py index 06e9712becc173..9f1f0eb992ab77 100644 --- a/homeassistant/components/matrix/notify.py +++ b/homeassistant/components/matrix/notify.py @@ -11,32 +11,33 @@ ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_SEND_MESSAGE + _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_ROOM = "default_room" -DOMAIN = "matrix" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_DEFAULT_ROOM): cv.string}) def get_service(hass, config, discovery_info=None): """Get the Matrix notification service.""" - return MatrixNotificationService(config.get(CONF_DEFAULT_ROOM)) + return MatrixNotificationService(config[CONF_DEFAULT_ROOM]) class MatrixNotificationService(BaseNotificationService): - """Send Notifications to a Matrix Room.""" + """Send notifications to a Matrix room.""" def __init__(self, default_room): - """Set up the notification service.""" + """Set up the Matrix notification service.""" self._default_room = default_room def send_message(self, message="", **kwargs): - """Send the message to the matrix server.""" + """Send the message to the Matrix server.""" target_rooms = kwargs.get(ATTR_TARGET) or [self._default_room] service_data = {ATTR_TARGET: target_rooms, ATTR_MESSAGE: message} return self.hass.services.call( - DOMAIN, "send_message", service_data=service_data + DOMAIN, SERVICE_SEND_MESSAGE, service_data=service_data ) diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index 1cf83de2c332cf..03c441a39ec22b 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -2,8 +2,8 @@ send_message: description: Send message to target room(s) fields: message: - description: The message to be sent + description: The message to be sent. example: 'This is a message I am sending to matrix' target: - description: A list of room(s) to send the message to + description: A list of room(s) to send the message to. example: '#hasstest:matrix.org' \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 7984cd4a32a175..d9e9a15998a81d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ lyft_rides==0.2 magicseaweed==1.0.3 # homeassistant.components.matrix -matrix-client==0.2.0 +matrix-client==0.3.2 # homeassistant.components.maxcube maxcube-api==0.1.0 From 876195a8a8c2e3fed27df0ffa88ef2206e272e56 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 17 Dec 2019 17:00:00 +0100 Subject: [PATCH 2456/3953] Upgrade zeroconf to 0.24.1 (#30028) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index ba764300daee2f..c02ac425445173 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -3,7 +3,7 @@ "name": "Zeroconf", "documentation": "https://www.home-assistant.io/integrations/zeroconf", "requirements": [ - "zeroconf==0.24.0" + "zeroconf==0.24.1" ], "dependencies": [ "api" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c0b7933fcca0fe..bb212b3e0b46d7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.11 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.24.0 +zeroconf==0.24.1 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index d9e9a15998a81d..8515e78c5a11e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2086,7 +2086,7 @@ youtube_dl==2019.11.28 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.24.0 +zeroconf==0.24.1 # homeassistant.components.zha zha-quirks==0.0.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20f8e8efc76fb4..07f2b446e24639 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -658,7 +658,7 @@ ya_ma==0.3.8 yahooweather==0.10 # homeassistant.components.zeroconf -zeroconf==0.24.0 +zeroconf==0.24.1 # homeassistant.components.zha zha-quirks==0.0.28 From 0135b988ff452f6ad49cc3a47b888a1ef1673efb Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 18 Dec 2019 00:32:19 +0000 Subject: [PATCH 2457/3953] [ci skip] Translation update --- .../components/elgato/.translations/sl.json | 27 +++++++++++++ .../components/icloud/.translations/sl.json | 38 +++++++++++++++++++ .../components/soma/.translations/sl.json | 4 +- 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/elgato/.translations/sl.json create mode 100644 homeassistant/components/icloud/.translations/sl.json diff --git a/homeassistant/components/elgato/.translations/sl.json b/homeassistant/components/elgato/.translations/sl.json new file mode 100644 index 00000000000000..f05b0bcbd8ffce --- /dev/null +++ b/homeassistant/components/elgato/.translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ta naprava Elgato Key Light je \u017ee nastavljena.", + "connection_error": "Povezava z napravo Elgato Key Light ni uspela." + }, + "error": { + "connection_error": "Povezava z napravo Elgato Key Light ni uspela." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Gostitelj ali IP naslov", + "port": "\u0160tevilka vrat" + }, + "description": "Nastavite svojo Elgato Key Light tako, da se bo vklju\u010dila v Home Assistant.", + "title": "Pove\u017eite svojo Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Ali \u017eelite dodati Elgato Key Light s serijsko \u0161tevilko ' {serial_number} ' v Home Assistant-a?", + "title": "Odkrita naprava Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/sl.json b/homeassistant/components/icloud/.translations/sl.json new file mode 100644 index 00000000000000..91cb4312cb3425 --- /dev/null +++ b/homeassistant/components/icloud/.translations/sl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Ra\u010dun \u017ee nastavljen" + }, + "error": { + "login": "Napaka pri prijavi: preverite svoj e-po\u0161tni naslov in geslo", + "send_verification_code": "Kode za preverjanje ni bilo mogo\u010de poslati", + "username_exists": "Ra\u010dun \u017ee nastavljen", + "validate_verification_code": "Kode za preverjanje ni bilo mogo\u010de preveriti, izberi napravo za zaupanje in znova za\u017eeni preverjanje" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Zaupanja vredna naprava" + }, + "description": "Izberite svojo zaupanja vredno napravo", + "title": "iCloud zaupanja vredna naprava" + }, + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "description": "Vnesite svoje poverilnice", + "title": "iCloud poverilnice" + }, + "verification_code": { + "data": { + "verification_code": "Koda za preverjanje" + }, + "description": "Prosimo, vnesite kodo za preverjanje, ki ste jo pravkar prejeli od iCloud", + "title": "iCloud koda za preverjanje" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/sl.json b/homeassistant/components/soma/.translations/sl.json index b3075208d2c4fa..01f7e50eb96d24 100644 --- a/homeassistant/components/soma/.translations/sl.json +++ b/homeassistant/components/soma/.translations/sl.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Nastavite lahko samo en ra\u010dun Soma.", "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", - "missing_configuration": "Komponenta Soma ni konfigurirana. Upo\u0161tevajte dokumentacijo." + "connection_error": "Povezava s SOMA Connect ni uspela.", + "missing_configuration": "Komponenta Soma ni konfigurirana. Upo\u0161tevajte dokumentacijo.", + "result_error": "SOMA Connect se je odzvala s statusom napake." }, "create_entry": { "default": "Uspe\u0161no overjen s Soma." From feb39c39a3ac713a5ba27821a6f12debb153eb9c Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Tue, 17 Dec 2019 16:51:19 -0800 Subject: [PATCH 2458/3953] Update Envoy sensor to configure credentials and grab Inverter Date from updated API (#28837) * Update manifest.json Updated sensor to use the latest version of the API to be able to use the new features. * Updated sensor to use new API features. Configurable credentials and Inverter last reported date. * EnvoyReader is passed to Envoy uses async update() * Fixed pydocstyle warnings * Fixed merge issue. Had extra variable. * Added warning message when authentication for Inverter data fails * Added continue after exception for loop * Moved if statement outside of try/except * Removed unneeded boolean attribute --- .../components/enphase_envoy/manifest.json | 2 +- .../components/enphase_envoy/sensor.py | 55 +++++++++++++++---- requirements_all.txt | 2 +- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 5b5f94f7c8cae2..21d154be23ab54 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/integrations/enphase_envoy", "requirements": [ - "envoy_reader==0.8.6" + "envoy_reader==0.10.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 3977326c06ddf9..a2b50f20eb6358 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -2,6 +2,7 @@ import logging from envoy_reader.envoy_reader import EnvoyReader +import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -9,6 +10,8 @@ CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, ENERGY_WATT_HOUR, POWER_WATT, ) @@ -42,6 +45,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_IP_ADDRESS, default=CONST_DEFAULT_HOST): cv.string, + vol.Optional(CONF_USERNAME, default="envoy"): cv.string, + vol.Optional(CONF_PASSWORD, default=""): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( cv.ensure_list, [vol.In(list(SENSORS))] ), @@ -52,30 +57,42 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Enphase Envoy sensor.""" - ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] name = config[CONF_NAME] + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + envoy_reader = EnvoyReader(ip_address, username, password) entities = [] # Iterate through the list of sensors for condition in monitored_conditions: if condition == "inverters": - inverters = await EnvoyReader(ip_address).inverters_production() + try: + inverters = await envoy_reader.inverters_production() + except requests.exceptions.HTTPError: + _LOGGER.warning( + "Authentication for Inverter data failed during setup: %s", + ip_address, + ) + continue + if isinstance(inverters, dict): for inverter in inverters: entities.append( Envoy( - ip_address, + envoy_reader, condition, f"{name}{SENSORS[condition][0]} {inverter}", SENSORS[condition][1], ) ) + else: entities.append( Envoy( - ip_address, + envoy_reader, condition, f"{name}{SENSORS[condition][0]}", SENSORS[condition][1], @@ -87,13 +104,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class Envoy(Entity): """Implementation of the Enphase Envoy sensors.""" - def __init__(self, ip_address, sensor_type, name, unit): + def __init__(self, envoy_reader, sensor_type, name, unit): """Initialize the sensor.""" - self._ip_address = ip_address + self._envoy_reader = envoy_reader + self._type = sensor_type self._name = name self._unit_of_measurement = unit - self._type = sensor_type self._state = None + self._last_reported = None @property def name(self): @@ -115,11 +133,18 @@ def icon(self): """Icon to use in the frontend, if any.""" return ICON + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._type == "inverters": + return {"last_reported": self._last_reported} + + return None + async def async_update(self): """Get the energy production data from the Enphase Envoy.""" - if self._type != "inverters": - _state = await getattr(EnvoyReader(self._ip_address), self._type)() + _state = await getattr(self._envoy_reader, self._type)() if isinstance(_state, int): self._state = _state else: @@ -127,9 +152,17 @@ async def async_update(self): self._state = None elif self._type == "inverters": - inverters = await (EnvoyReader(self._ip_address).inverters_production()) + try: + inverters = await (self._envoy_reader.inverters_production()) + except requests.exceptions.HTTPError: + _LOGGER.warning( + "Authentication for Inverter data failed during update: %s", + self._envoy_reader.host, + ) + if isinstance(inverters, dict): serial_number = self._name.split(" ")[2] - self._state = inverters[serial_number] + self._state = inverters[serial_number][0] + self._last_reported = inverters[serial_number][1] else: self._state = None diff --git a/requirements_all.txt b/requirements_all.txt index 8515e78c5a11e7..4c1674596096e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -483,7 +483,7 @@ env_canada==0.0.30 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.8.6 +envoy_reader==0.10.0 # homeassistant.components.season ephem==3.7.7.0 From 9c7caaa142962a11eb06ff0fc7110c22e0bc5ff3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Dec 2019 07:41:01 +0100 Subject: [PATCH 2459/3953] Add option to ignore flows (#30008) --- .../components/config/config_entries.py | 35 ++++++++++++++++ homeassistant/components/hue/config_flow.py | 2 +- homeassistant/config_entries.py | 12 +++++- .../components/config/test_config_entries.py | 39 ++++++++++++++++++ tests/components/hue/test_config_flow.py | 7 +++- tests/test_config_entries.py | 40 +++++++++++++++++++ 6 files changed, 132 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 81065665e34394..dbf0ee8f283de1 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -33,6 +33,7 @@ async def async_setup(hass): 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) + hass.components.websocket_api.async_register_command(ignore_config_flow) return True @@ -284,3 +285,37 @@ async def system_options_update(hass, connection, msg): hass.config_entries.async_update_entry(entry, system_options=changes) 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/ignore_flow", "flow_id": str}) +async def ignore_config_flow(hass, connection, msg): + """Ignore a config flow.""" + flow = next( + ( + flw + for flw in hass.config_entries.flow.async_progress() + if flw["flow_id"] == msg["flow_id"] + ), + None, + ) + + if flow is None: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found" + ) + return + + if "unique_id" not in flow["context"]: + connection.send_error( + msg["id"], "no_unique_id", "Specified flow has no unique ID." + ) + return + + await hass.config_entries.flow.async_init( + flow["handler"], + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": flow["context"]["unique_id"]}, + ) + connection.send_result(msg["id"]) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index f2d7c6d1e8a1a7..97cc1a8d66cc9f 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -75,7 +75,7 @@ async def async_step_init(self, user_input=None): return self.async_abort(reason="no_bridges") # Find already configured hosts - already_configured = self._async_current_ids() + already_configured = self._async_current_ids(False) bridges = [bridge for bridge in bridges if bridge.id not in already_configured] if not bridges: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d39fc4803eac77..1a010b38e7052a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -24,6 +24,7 @@ SOURCE_SSDP = "ssdp" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" +SOURCE_IGNORE = "ignore" HANDLERS = Registry() @@ -157,6 +158,9 @@ async def async_setup( tries: int = 0, ) -> None: """Set up an entry.""" + if self.source == SOURCE_IGNORE: + return + if integration is None: integration = await loader.async_get_integration(hass, self.domain) @@ -792,12 +796,13 @@ def _async_current_entries(self) -> List[ConfigEntry]: return self.hass.config_entries.async_entries(self.handler) @callback - def _async_current_ids(self) -> Set[Optional[str]]: + def _async_current_ids(self, include_ignore: bool = True) -> Set[Optional[str]]: """Return current unique IDs.""" assert self.hass is not None return set( entry.unique_id for entry in self.hass.config_entries.async_entries(self.handler) + if include_ignore or entry.source != SOURCE_IGNORE ) @callback @@ -810,6 +815,11 @@ def _async_in_progress(self) -> List[Dict]: if flw["handler"] == self.handler and flw["flow_id"] != self.flow_id ] + async def async_step_ignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: + """Ignore this config flow.""" + await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) + return self.async_create_entry(title="Ignored", data={}) + class OptionsFlowManager: """Flow to set options for a configuration entry.""" diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 6176bd73c52c64..6631bbf8fbf064 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -634,3 +634,42 @@ async def test_update_system_options(hass, hass_ws_client): assert response["success"] assert response["result"]["disable_new_entities"] assert entry.system_options.disable_new_entities + + +async def test_ignore_flow(hass, hass_ws_client): + """Test we can ignore a flow.""" + assert await async_setup_component(hass, "config", {}) + mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) + mock_entity_platform(hass, "config_flow.test", None) + + class TestFlow(core_ce.ConfigFlow): + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + return self.async_show_form(step_id="account", data_schema=vol.Schema({})) + + ws_client = await hass_ws_client(hass) + + with patch.dict(HANDLERS, {"test": TestFlow}): + result = await hass.config_entries.flow.async_init( + "test", context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/ignore_flow", + "flow_id": result["flow_id"], + } + ) + response = await ws_client.receive_json() + + assert response["success"] + + assert len(hass.config_entries.flow.async_progress()) == 0 + + entry = hass.config_entries.async_entries("test")[0] + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index fe9a1f0e32ccd4..64a9c81136a9cd 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -6,7 +6,7 @@ import pytest import voluptuous as vol -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.hue import config_flow, const from tests.common import MockConfigEntry, mock_coro @@ -95,6 +95,11 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock): async def test_flow_two_bridges_discovered(hass, aioclient_mock): """Test config flow discovers two bridges.""" + # Add ignored config entry. Should still show up as option. + MockConfigEntry( + domain="hue", source=config_entries.SOURCE_IGNORE, unique_id="bla" + ).add_to_hass(hass) + aioclient_mock.get( const.API_NUPNP, json=[ diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d83de45022791a..19f84e94570e91 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1146,3 +1146,43 @@ async def async_step_user(self, user_input=None): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 + + +async def test_unique_id_ignore(hass, manager): + """Test that we can ignore flows that are in progress and have a unique ID.""" + async_setup_entry = MagicMock(return_value=mock_coro(False)) + mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_user(self, user_input=None): + await self.async_set_unique_id("mock-unique-id") + return self.async_show_form(step_id="discovery") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + # Create one to be in progress + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result2 = await manager.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": "mock-unique-id"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + # assert len(hass.config_entries.flow.async_progress()) == 0 + + # We should never set up an ignored entry. + assert len(async_setup_entry.mock_calls) == 0 + + entry = hass.config_entries.async_entries("comp")[0] + + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id" From d16011b849088d17af6ed54657eeec3c837d4fd1 Mon Sep 17 00:00:00 2001 From: Erik Kastelec <34520112+erikkastelec@users.noreply.github.com> Date: Wed, 18 Dec 2019 14:47:01 +0100 Subject: [PATCH 2460/3953] changed Venstar component temperature to half degree accuracy (#30034) --- homeassistant/components/venstar/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index df7dfcacb20f9e..4ffe75acb9e605 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -34,7 +34,7 @@ CONF_SSL, CONF_TIMEOUT, CONF_USERNAME, - PRECISION_WHOLE, + PRECISION_HALVES, STATE_ON, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -134,9 +134,9 @@ def precision(self): """Return the precision of the system. Venstar temperature values are passed back and forth in the - API as whole degrees C or F. + API in C or F, with half-degree accuracy. """ - return PRECISION_WHOLE + return PRECISION_HALVES @property def temperature_unit(self): From bdef54de0c10e512b5375c692c7e2749c18f2ea5 Mon Sep 17 00:00:00 2001 From: omriasta Date: Wed, 18 Dec 2019 15:04:54 -0500 Subject: [PATCH 2461/3953] Patch rachio (#30031) * fix binary sensor offline/online fixed self._handle_update on line 66 to produce args, kwargs. Updated the binary sensor to check the correct index in the tuple. * Fixed Standby switch Set standby switch to poll in order to get status when homeassistant starts up. Updated the index for the switch to get the status from the tuple. --- homeassistant/components/rachio/binary_sensor.py | 6 +++--- homeassistant/components/rachio/switch.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index f74e3ca1802181..f13eba59ac9b8c 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -63,7 +63,7 @@ def _handle_any_update(self, *args, **kwargs) -> None: return # For this device - self._handle_update() + self._handle_update(args, kwargs) @abstractmethod def _poll_update(self, data=None) -> bool: @@ -119,9 +119,9 @@ def _poll_update(self, data=None) -> bool: def _handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" - if args[0][KEY_SUBTYPE] == SUBTYPE_ONLINE: + if args[0][0][KEY_SUBTYPE] == SUBTYPE_ONLINE: self._state = True - elif args[0][KEY_SUBTYPE] == SUBTYPE_OFFLINE: + elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE: self._state = False self.schedule_update_ha_state() diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 80c227a6df6977..a3a4f6bcca17b3 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -107,7 +107,7 @@ def __init__(self, hass, controller): dispatcher_connect( hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, self._handle_any_update ) - super().__init__(controller, poll=False) + super().__init__(controller, poll=True) self._poll_update(controller.init_data) @property @@ -134,9 +134,9 @@ def _poll_update(self, data=None) -> bool: def _handle_update(self, *args, **kwargs) -> None: """Update the state using webhook data.""" - if args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON: + if args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON: self._state = True - elif args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF: + elif args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF: self._state = False self.schedule_update_ha_state() From 0adb88156d166d7e60998d431d517c791c044504 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 18 Dec 2019 13:06:57 -0700 Subject: [PATCH 2462/3953] Bump simplisafe-python to 5.3.6 (#30055) --- 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 4115ce455b5d46..2df49bb520987e 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/integrations/simplisafe", "requirements": [ - "simplisafe-python==5.3.5" + "simplisafe-python==5.3.6" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4c1674596096e2..5cecb39664964d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1801,7 +1801,7 @@ shodan==1.21.0 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==5.3.5 +simplisafe-python==5.3.6 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07f2b446e24639..538262919ff530 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -567,7 +567,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.simplisafe -simplisafe-python==5.3.5 +simplisafe-python==5.3.6 # homeassistant.components.sleepiq sleepyq==0.7 From 41bef4b919cc9768b3a027811487cf1719f2ca20 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 18 Dec 2019 15:15:11 -0500 Subject: [PATCH 2463/3953] Add timer reload service. (#30015) --- homeassistant/components/timer/__init__.py | 56 +++++++---- tests/components/timer/test_init.py | 103 ++++++++++++++++++++- 2 files changed, 141 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index de60752eada9ee..0cc707f5a452bc 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -4,11 +4,12 @@ import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME +from homeassistant.const import CONF_ICON, CONF_NAME, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -54,26 +55,31 @@ extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) + async def async_setup(hass, config): """Set up a timer.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - entities = [] - - for object_id, cfg in config[DOMAIN].items(): - if not cfg: - cfg = {} - - name = cfg.get(CONF_NAME) - icon = cfg.get(CONF_ICON) - duration = cfg.get(CONF_DURATION) - - entities.append(Timer(hass, object_id, name, icon, duration)) - - if not entities: - return False + entities = await _async_process_config(hass, config) + async def reload_service_handler(service_call): + """Remove all input booleans and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(hass, conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) component.async_register_entity_service( SERVICE_START, { @@ -87,10 +93,28 @@ async def async_setup(hass, config): component.async_register_entity_service(SERVICE_CANCEL, {}, "async_cancel") component.async_register_entity_service(SERVICE_FINISH, {}, "async_finish") - await component.async_add_entities(entities) + if entities: + await component.async_add_entities(entities) return True +async def _async_process_config(hass, config): + """Process config and create list of entities.""" + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + if not cfg: + cfg = {} + + name = cfg.get(CONF_NAME) + icon = cfg.get(CONF_ICON) + duration = cfg.get(CONF_DURATION) + + entities.append(Timer(hass, object_id, name, icon, duration)) + + return entities + + class Timer(RestoreEntity): """Representation of a timer.""" diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 39648b68fd7921..547f7e4ab0538a 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -3,6 +3,9 @@ import asyncio from datetime import timedelta import logging +from unittest.mock import patch + +import pytest from homeassistant.components.timer import ( ATTR_DURATION, @@ -23,8 +26,14 @@ STATUS_IDLE, STATUS_PAUSED, ) -from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON, CONF_ENTITY_ID -from homeassistant.core import CoreState +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_ICON, + CONF_ENTITY_ID, + SERVICE_RELOAD, +) +from homeassistant.core import Context, CoreState +from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -195,3 +204,93 @@ def test_no_initial_state_and_no_restore_state(hass): state = hass.states.get("timer.test1") assert state assert state.state == STATUS_IDLE + + +async def test_config_reload(hass, hass_admin_user, hass_read_only_user): + """Test reload service.""" + count_start = len(hass.states.async_entity_ids()) + + _LOGGER.debug("ENTITIES @ start: %s", hass.states.async_entity_ids()) + + config = { + DOMAIN: { + "test_1": {}, + "test_2": { + CONF_NAME: "Hello World", + CONF_ICON: "mdi:work", + CONF_DURATION: 10, + }, + } + } + + assert await async_setup_component(hass, "timer", config) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + await hass.async_block_till_done() + + state_1 = hass.states.get("timer.test_1") + state_2 = hass.states.get("timer.test_2") + state_3 = hass.states.get("timer.test_3") + + assert state_1 is not None + assert state_2 is not None + assert state_3 is None + + assert STATUS_IDLE == state_1.state + assert ATTR_ICON not in state_1.attributes + assert ATTR_FRIENDLY_NAME not in state_1.attributes + + assert STATUS_IDLE == state_2.state + assert "Hello World" == state_2.attributes.get(ATTR_FRIENDLY_NAME) + assert "mdi:work" == state_2.attributes.get(ATTR_ICON) + assert "0:00:10" == state_2.attributes.get(ATTR_DURATION) + + with patch( + "homeassistant.config.load_yaml_config_file", + autospec=True, + return_value={ + DOMAIN: { + "test_2": { + CONF_NAME: "Hello World reloaded", + CONF_ICON: "mdi:work-reloaded", + CONF_DURATION: 20, + }, + "test_3": {}, + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + with pytest.raises(Unauthorized): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_read_only_user.id), + ) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start + 2 == len(hass.states.async_entity_ids()) + + state_1 = hass.states.get("timer.test_1") + state_2 = hass.states.get("timer.test_2") + state_3 = hass.states.get("timer.test_3") + + assert state_1 is None + assert state_2 is not None + assert state_3 is not None + + assert STATUS_IDLE == state_2.state + assert "Hello World reloaded" == state_2.attributes.get(ATTR_FRIENDLY_NAME) + assert "mdi:work-reloaded" == state_2.attributes.get(ATTR_ICON) + assert "0:00:20" == state_2.attributes.get(ATTR_DURATION) + + assert STATUS_IDLE == state_3.state + assert ATTR_ICON not in state_3.attributes + assert ATTR_FRIENDLY_NAME not in state_3.attributes From 628a148944afaafd8cdd15075867b362d5ed6982 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Dec 2019 00:28:56 -0800 Subject: [PATCH 2464/3953] Install requirements of after_dependencies when loading integrations (#29491) * Install requirements of after_dependencies when loading integrations * Fix smartthings test --- homeassistant/components/auth/manifest.json | 9 +++----- homeassistant/components/camera/manifest.json | 7 +----- homeassistant/components/cast/manifest.json | 1 + .../components/google_assistant/manifest.json | 1 + .../components/mobile_app/http_api.py | 13 +++++------ .../components/mobile_app/manifest.json | 1 + .../components/mobile_app/webhook.py | 9 ++++---- .../components/mobile_app/websocket_api.py | 2 +- .../components/onboarding/manifest.json | 9 ++------ .../components/owntracks/__init__.py | 2 +- .../components/owntracks/config_flow.py | 5 +---- .../components/owntracks/manifest.json | 12 +++------- homeassistant/components/proxy/camera.py | 22 +++++++++++-------- homeassistant/components/proxy/manifest.json | 4 +--- .../components/smartthings/manifest.json | 14 ++++-------- .../components/smartthings/smartapp.py | 18 +++------------ homeassistant/setup.py | 4 ++-- script/hassfest/dependencies.py | 13 +++++------ .../smartthings/test_config_flow.py | 9 ++++---- 19 files changed, 57 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index 1b0ab33f38155b..2f3e724b5831cd 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -3,10 +3,7 @@ "name": "Auth", "documentation": "https://www.home-assistant.io/integrations/auth", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["http"], + "after_dependencies": ["onboarding"], + "codeowners": ["@home-assistant/core"] } diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index a3395965e4f749..1bd4bb7caeb065 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,11 +3,6 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "requirements": [], - "dependencies": [ - "http" - ], - "after_dependencies": [ - "stream" - ], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index b6776a17f7cba0..8ad6f8fdb8dcc0 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==4.0.1"], "dependencies": [], + "after_dependencies": ["cloud"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": [] } diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index f97977a74001bf..94dd3b7f079047 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/google_assistant", "requirements": [], "dependencies": ["http"], + "after_dependencies": ["camera"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 11ca39e8b60014..5be2d19789e420 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -37,9 +37,7 @@ async def post(self, request: Request, data: Dict) -> Response: webhook_id = generate_secret() - cloud_loaded = "cloud" in hass.config.components - - if cloud_loaded and hass.components.cloud.async_active_subscription(): + if hass.components.cloud.async_active_subscription(): data[ CONF_CLOUDHOOK_URL ] = await hass.components.cloud.async_create_cloudhook(webhook_id) @@ -59,11 +57,10 @@ async def post(self, request: Request, data: Dict) -> Response: ) remote_ui_url = None - if cloud_loaded: - try: - remote_ui_url = hass.components.cloud.async_remote_ui_url() - except hass.components.cloud.CloudNotAvailable: - pass + try: + remote_ui_url = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass return self.json( { diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 29ee35e002c0ad..230a60fdf256ea 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -5,5 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": ["PyNaCl==1.3.0"], "dependencies": ["http", "webhook"], + "after_dependencies": ["cloud"], "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 98687e6658fdc7..46f17b401bcdbf 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -309,10 +309,9 @@ async def handle_webhook( if CONF_CLOUDHOOK_URL in registration: resp[CONF_CLOUDHOOK_URL] = registration[CONF_CLOUDHOOK_URL] - if "cloud" in hass.config.components: - try: - resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() - except hass.components.cloud.CloudNotAvailable: - pass + try: + resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass return webhook_response(resp, registration=registration, headers=headers) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index bc5305c36fa6bc..a18e5247bfa50f 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -115,7 +115,7 @@ async def websocket_delete_registration( except HomeAssistantError: return error_message(msg["id"], "internal_error", "Error deleting registration") - if CONF_CLOUDHOOK_URL in registration and "cloud" in hass.config.components: + if CONF_CLOUDHOOK_URL in registration: await hass.components.cloud.async_delete_cloudhook(webhook_id) connection.send_message(result_message(msg["id"], "ok")) diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index 2febfc481e01f9..8e525ff0baad1b 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -3,11 +3,6 @@ "name": "Onboarding", "documentation": "https://www.home-assistant.io/integrations/onboarding", "requirements": [], - "dependencies": [ - "auth", - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["auth", "http", "person"], + "codeowners": ["@home-assistant/core"] } diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index d30e667f368e68..8556e8a7556057 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -118,7 +118,7 @@ async def async_unload_entry(hass, entry): async def async_remove_entry(hass, entry): """Remove an OwnTracks config entry.""" - if not entry.data.get("cloudhook") or "cloud" not in hass.config.components: + if not entry.data.get("cloudhook"): return await hass.components.cloud.async_delete_cloudhook(entry.data[CONF_WEBHOOK_ID]) diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index ff4a649e0ce76f..1a8bb838e181f7 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -66,10 +66,7 @@ async def async_step_import(self, user_input): async def _get_webhook_id(self): """Generate webhook ID.""" webhook_id = self.hass.components.webhook.async_generate_id() - if ( - "cloud" in self.hass.config.components - and self.hass.components.cloud.async_active_subscription() - ): + if self.hass.components.cloud.async_active_subscription(): webhook_url = await self.hass.components.cloud.async_create_cloudhook( webhook_id ) diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 529d7990a8644e..63fdfb94cf7779 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -3,14 +3,8 @@ "name": "Owntracks", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/owntracks", - "requirements": [ - "PyNaCl==1.3.0" - ], - "dependencies": [ - "webhook" - ], - "after_dependencies": [ - "mqtt" - ], + "requirements": ["PyNaCl==1.3.0"], + "dependencies": ["webhook"], + "after_dependencies": ["mqtt", "cloud"], "codeowners": [] } diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 90487120ffe0a7..893fadfe178a81 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -7,7 +7,13 @@ from PIL import Image import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA, + Camera, + async_get_image, + async_get_mjpeg_stream, + async_get_still_stream, +) from homeassistant.const import CONF_ENTITY_ID, CONF_MODE, CONF_NAME from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -227,7 +233,7 @@ async def async_camera_image(self): return self._last_image self._last_image_time = now - image = await self.hass.components.camera.async_get_image(self._proxied_camera) + image = await async_get_image(self.hass, self._proxied_camera) if not image: _LOGGER.error("Error getting original camera image") return self._last_image @@ -247,12 +253,12 @@ async def async_camera_image(self): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from camera images.""" if not self._stream_opts: - return await self.hass.components.camera.async_get_mjpeg_stream( - request, self._proxied_camera + return await async_get_mjpeg_stream( + self.hass, request, self._proxied_camera ) - return await self.hass.components.camera.async_get_still_stream( - request, self._async_stream_image, self.content_type, self.frame_interval + return await async_get_still_stream( + request, self._async_stream_image, self.content_type, self.frame_interval, ) @property @@ -263,9 +269,7 @@ def name(self): async def _async_stream_image(self): """Return a still image response from the camera.""" try: - image = await self.hass.components.camera.async_get_image( - self._proxied_camera - ) + image = await async_get_image(self.hass, self._proxied_camera) if not image: return None except HomeAssistantError: diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 39f7b9064fc69a..5344ea6fe835d7 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,9 +2,7 @@ "domain": "proxy", "name": "Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": [ - "pillow==6.2.1" - ], + "requirements": ["pillow==6.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 8b5bf65afa1349..0ab71382fad590 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -3,14 +3,8 @@ "name": "Smartthings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smartthings", - "requirements": [ - "pysmartapp==0.3.2", - "pysmartthings==0.6.9" - ], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@andrewsayre" - ] + "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.6.9"], + "dependencies": ["webhook"], + "after_dependencies": ["cloud"], + "codeowners": ["@andrewsayre"] } diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 6acd29397ae6a6..9b67df21491011 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -88,10 +88,7 @@ async def validate_installed_app(api, installed_app_id: str): def validate_webhook_requirements(hass: HomeAssistantType) -> bool: """Ensure HASS is setup properly to receive webhooks.""" - if ( - "cloud" in hass.config.components - and hass.components.cloud.async_active_subscription() - ): + if hass.components.cloud.async_active_subscription(): return True if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None: return True @@ -105,11 +102,7 @@ def get_webhook_url(hass: HomeAssistantType) -> str: Return the cloudhook if available, otherwise local webhook. """ cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if ( - "cloud" in hass.config.components - and hass.components.cloud.async_active_subscription() - and cloudhook_url is not None - ): + if hass.components.cloud.async_active_subscription() and cloudhook_url is not None: return cloudhook_url return webhook.async_generate_url(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) @@ -229,7 +222,6 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): cloudhook_url = config.get(CONF_CLOUDHOOK_URL) if ( cloudhook_url is None - and "cloud" in hass.config.components and hass.components.cloud.async_active_subscription() and not hass.config_entries.async_entries(DOMAIN) ): @@ -281,11 +273,7 @@ async def unload_smartapp_endpoint(hass: HomeAssistantType): return # Remove the cloudhook if it was created cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if ( - cloudhook_url - and "cloud" in hass.config.components - and hass.components.cloud.async_is_logged_in() - ): + if cloudhook_url and hass.components.cloud.async_is_logged_in(): await hass.components.cloud.async_delete_cloudhook( hass.data[DOMAIN][CONF_WEBHOOK_ID] ) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 314938feeed42c..42296a4935d657 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -288,8 +288,8 @@ async def async_process_deps_reqs( raise HomeAssistantError("Could not set up all dependencies.") if not hass.config.skip_pip and integration.requirements: - await requirements.async_process_requirements( - hass, integration.domain, integration.requirements + await requirements.async_get_integration_with_requirements( + hass, integration.domain ) processed.add(integration.domain) diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index e9933995715425..e47deb76ad5f7e 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -43,15 +43,12 @@ def validate_dependencies(integration: Integration): if referenced: for domain in sorted(referenced): - print( - "Warning: {} references integration {} but it's not a " - "dependency".format(integration.domain, domain) + integration.add_error( + "dependencies", + "Using component {} but it's not in 'dependencies' or 'after_dependencies'".format( + domain + ), ) - # Not enforced yet. - # integration.add_error( - # 'dependencies', - # "Using component {} but it's not a dependency".format(domain) - # ) def validate(integrations: Dict[str, Integration], config): diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 521f1c6a6a8972..82a24f38287139 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -7,7 +7,6 @@ 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 from homeassistant.components.smartthings.const import ( @@ -18,7 +17,7 @@ DOMAIN, ) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_step_user(hass): @@ -211,9 +210,11 @@ async def test_cloudhook_app_created_then_show_wait_form( await smartapp.unload_smartapp_endpoint(hass) with patch.object( - cloud, "async_active_subscription", return_value=True + hass.components.cloud, "async_active_subscription", return_value=True ), patch.object( - cloud, "async_create_cloudhook", return_value="http://cloud.test" + hass.components.cloud, + "async_create_cloudhook", + return_value=mock_coro("http://cloud.test"), ) as mock_create_cloudhook: await smartapp.setup_smartapp_endpoint(hass) From a466ae02795077ae47a3dfe566ac49b6c87c6505 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 12 Dec 2019 02:24:57 -0700 Subject: [PATCH 2465/3953] Bump aioambient to 1.0.2 (#29850) --- 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 8f363ba219f99d..1e6c06f260a3c8 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/integrations/ambient_station", "requirements": [ - "aioambient==0.3.2" + "aioambient==1.0.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9e2f31443e5ebf..046ee6fc5fbcbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -133,7 +133,7 @@ aio_geojson_geonetnz_volcano==0.5 aio_geojson_nsw_rfs_incidents==0.1 # homeassistant.components.ambient_station -aioambient==0.3.2 +aioambient==1.0.2 # homeassistant.components.asuswrt aioasuswrt==1.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 036de45e342926..700bdf6a3b2bf3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -44,7 +44,7 @@ aio_geojson_geonetnz_volcano==0.5 aio_geojson_nsw_rfs_incidents==0.1 # homeassistant.components.ambient_station -aioambient==0.3.2 +aioambient==1.0.2 # homeassistant.components.asuswrt aioasuswrt==1.1.22 From 29e412a6c45c981b30dfee7a0b34d3233a493f72 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 13 Dec 2019 10:31:53 +0100 Subject: [PATCH 2466/3953] Fix setup for tank_utility component (#29902) --- homeassistant/components/tank_utility/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 3ab4a027b04f2d..4dcc880703c602 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -4,7 +4,7 @@ import logging import requests -import tank_utility +from tank_utility import auth, device as tank_monitor import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = config.get(CONF_DEVICES) try: - token = tank_utility.auth.get_token(email, password) + token = auth.get_token(email, password) except requests.exceptions.HTTPError as http_error: if ( http_error.response.status_code @@ -111,17 +111,15 @@ def get_data(self): data = {} try: - data = tank_utility.device.get_device_data(self._token, self.device) + data = tank_monitor.get_device_data(self._token, self.device) except requests.exceptions.HTTPError as http_error: if ( http_error.response.status_code == requests.codes.unauthorized # pylint: disable=no-member ): _LOGGER.info("Getting new token") - self._token = tank_utility.auth.get_token( - self._email, self._password, force=True - ) - data = tank_utility.device.get_device_data(self._token, self.device) + self._token = auth.get_token(self._email, self._password, force=True) + data = tank_monitor.get_device_data(self._token, self.device) else: raise http_error data.update(data.pop("device", {})) From 9b65d83e28bf27115a83675b3cfdafb0e8d83c29 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 13 Dec 2019 14:08:30 +0100 Subject: [PATCH 2467/3953] Fix setup error for logbook (#29908) * Fix setup error by moving an import back into the setup function * Revert c741664d4da76611b5bb7502dda61a24dce22c61 * Add homekit as after_dependency to logbook manifest.json --- homeassistant/components/logbook/manifest.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index e8e3ad8ac2e580..9d5c78dc34d5a4 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -3,9 +3,7 @@ "name": "Logbook", "documentation": "https://www.home-assistant.io/integrations/logbook", "requirements": [], - "dependencies": [ - "frontend", - "recorder" - ], + "dependencies": ["frontend", "http", "recorder"], + "after_dependencies": ["homekit"], "codeowners": [] } From 36f7096f0959b374b1c646493f22eb7aa9d0a771 Mon Sep 17 00:00:00 2001 From: Wim Haanstra Date: Wed, 18 Dec 2019 21:18:14 +0100 Subject: [PATCH 2468/3953] Fix failure in transform method (#30023) * Fix failure in transform method * Fix formatting issue --- homeassistant/components/dsmr_reader/definitions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index f4a36ebc34682c..45bebfeda92c2d 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -3,7 +3,9 @@ def dsmr_transform(value): """Transform DSMR version value to right format.""" - return float(value) / 10 + if value.isdigit(): + return float(value) / 10 + return value def tariff_transform(value): From 24c87638e6e428308e0d46275e0bf81a445c3b65 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 14 Dec 2019 07:36:33 +0100 Subject: [PATCH 2469/3953] Support entity_id: all in lifx.set_state (#29919) --- homeassistant/components/lifx/light.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 1fb614f856fb5f..aa63be04f0d3f0 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -35,7 +35,12 @@ Light, preprocess_turn_on_alternatives, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_MODE, + ENTITY_MATCH_ALL, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr @@ -369,6 +374,9 @@ async def start_effect(self, entities, service, **kwargs): async def async_service_to_entities(self, service): """Return the known entities that a service call mentions.""" + if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL: + return self.entities.values() + entity_ids = await async_extract_entity_ids(self.hass, service) return [ entity From f1d22db009200bfe8445c6029192f6957590d4f7 Mon Sep 17 00:00:00 2001 From: Justin Bassett Date: Sat, 14 Dec 2019 01:36:12 -0500 Subject: [PATCH 2470/3953] Fix mobile app device identifiers (#29920) Fix identifiers when updating device registration. --- homeassistant/components/mobile_app/webhook.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 46f17b401bcdbf..c2bc6c9411272f 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -190,10 +190,7 @@ async def handle_webhook( device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - identifiers={ - (ATTR_DEVICE_ID, registration[ATTR_DEVICE_ID]), - (CONF_WEBHOOK_ID, registration[CONF_WEBHOOK_ID]), - }, + identifiers={(DOMAIN, registration[ATTR_DEVICE_ID])}, manufacturer=new_registration[ATTR_MANUFACTURER], model=new_registration[ATTR_MODEL], name=new_registration[ATTR_DEVICE_NAME], From 9836e781206a5f1b6c66621c72998c900ccd37f5 Mon Sep 17 00:00:00 2001 From: Chris Mandich Date: Sat, 14 Dec 2019 18:45:29 -0800 Subject: [PATCH 2471/3953] Fix loading flume integration (#29926) * Fix https://github.com/home-assistant/home-assistant/issues/29853 * Run script.gen_requirements * Update to store Token File in config directory * Update to store Token File in config directory * Update to store Token File in config directory --- homeassistant/components/flume/manifest.json | 4 ++-- homeassistant/components/flume/sensor.py | 6 +++++- requirements_all.txt | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index 800751e80ef2ea..6a9fb7a1fd8175 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -3,9 +3,9 @@ "name": "Flume", "documentation": "https://www.home-assistant.io/integrations/flume/", "requirements": [ - "pyflume==0.2.1" + "pyflume==0.2.4" ], "dependencies": [], "codeowners": ["@ChrisMandich"] } - \ No newline at end of file + diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index 5fee408e0dcc43..e96ce0d96ef209 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -37,11 +37,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config[CONF_PASSWORD] client_id = config[CONF_CLIENT_ID] client_secret = config[CONF_CLIENT_SECRET] + flume_token_file = hass.config.path("FLUME_TOKEN_FILE") time_zone = str(hass.config.time_zone) name = config[CONF_NAME] flume_entity_list = [] - flume_devices = FlumeDeviceList(username, password, client_id, client_secret) + flume_devices = FlumeDeviceList( + username, password, client_id, client_secret, flume_token_file + ) for device in flume_devices.device_list: if device["type"] == FLUME_TYPE_SENSOR: @@ -53,6 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device["id"], time_zone, SCAN_INTERVAL, + flume_token_file, ) flume_entity_list.append(FlumeSensor(flume, f"{name} {device['id']}")) diff --git a/requirements_all.txt b/requirements_all.txt index 046ee6fc5fbcbb..68c772785b3e0d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1231,7 +1231,7 @@ pyflexit==0.3 pyflic-homeassistant==0.4.dev0 # homeassistant.components.flume -pyflume==0.2.1 +pyflume==0.2.4 # homeassistant.components.flunearyou pyflunearyou==1.0.3 From 6d06cec0e0b8c31269e0602b97548fef0cb4f96a Mon Sep 17 00:00:00 2001 From: Andrew Onyshchuk Date: Sun, 15 Dec 2019 19:02:18 -0600 Subject: [PATCH 2472/3953] Fix support for legacy Z-Wave thermostats (#29955) This brings back support for Z-Wave thermostats of SETPOINT_THERMOSTAT specific device class. Such devices don't have COMMAND_CLASS_THERMOSTAT_MODE and are now handled separately. --- homeassistant/components/zwave/climate.py | 103 ++++++--- .../components/zwave/discovery_schemas.py | 51 +++- tests/components/zwave/test_climate.py | 217 +++++++++++++++++- tests/components/zwave/test_init.py | 42 +++- 4 files changed, 377 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index e50908783283e3..2b421db70b5ceb 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -1,11 +1,12 @@ """Support for Z-Wave climate devices.""" # Because we do not compile openzwave on CI import logging - -from typing import Optional +from typing import Optional, Tuple from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_FAN, CURRENT_HVAC_HEAT, @@ -14,29 +15,26 @@ DOMAIN, HVAC_MODE_AUTO, HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - SUPPORT_PRESET_MODE, - ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_HIGH, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from . import ZWaveDeviceEntity +from . import ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) @@ -149,10 +147,14 @@ def async_add_climate(climate): def get_device(hass, values, **kwargs): """Create Z-Wave entity device.""" temp_unit = hass.config.units.temperature_unit - return ZWaveClimate(values, temp_unit) + if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_SETPOINT: + return ZWaveClimateSingleSetpoint(values, temp_unit) + if values.primary.command_class == const.COMMAND_CLASS_THERMOSTAT_MODE: + return ZWaveClimateMultipleSetpoint(values, temp_unit) + return None -class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): +class ZWaveClimateBase(ZWaveDeviceEntity, ClimateDevice): """Representation of a Z-Wave Climate device.""" def __init__(self, values, temp_unit): @@ -190,18 +192,21 @@ def __init__(self, values, temp_unit): self._zxt_120 = 1 self.update_properties() - def _current_mode_setpoints(self): - current_mode = str(self.values.primary.data).lower() - setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) - return tuple(getattr(self.values, name, None) for name in setpoints_names) + def _mode(self) -> None: + """Return thermostat mode Z-Wave value.""" + raise NotImplementedError() + + def _current_mode_setpoints(self) -> Tuple: + """Return a tuple of current setpoint Z-Wave value(s).""" + raise NotImplementedError() @property def supported_features(self): """Return the list of supported features.""" support = SUPPORT_TARGET_TEMPERATURE - if HVAC_MODE_HEAT_COOL in self._hvac_list: + if self._hvac_list and HVAC_MODE_HEAT_COOL in self._hvac_list: support |= SUPPORT_TARGET_TEMPERATURE_RANGE - if PRESET_AWAY in self._preset_list: + if self._preset_list and PRESET_AWAY in self._preset_list: support |= SUPPORT_TARGET_TEMPERATURE_RANGE if self.values.fan_mode: @@ -239,13 +244,13 @@ def update_properties(self): def _update_operation_mode(self): """Update hvac and preset modes.""" - if self.values.primary: + if self._mode(): self._hvac_list = [] self._hvac_mapping = {} self._preset_list = [] self._preset_mapping = {} - mode_list = self.values.primary.data_items + mode_list = self._mode().data_items if mode_list: for mode in mode_list: ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) @@ -273,7 +278,7 @@ def _update_operation_mode(self): # Presets are supported self._preset_list.append(PRESET_NONE) - current_mode = self.values.primary.data + current_mode = self._mode().data _LOGGER.debug("current_mode=%s", current_mode) _hvac_temp = next( ( @@ -426,7 +431,7 @@ def hvac_mode(self): Need to be one of HVAC_MODE_*. """ - if self.values.primary: + if self._mode(): return self._hvac_mode return self._default_hvac_mode @@ -436,7 +441,7 @@ def hvac_modes(self): Need to be a subset of HVAC_MODES. """ - if self.values.primary: + if self._mode(): return self._hvac_list return [] @@ -453,7 +458,7 @@ def is_aux_heat(self): """Return true if aux heater.""" if not self._aux_heat: return None - if self.values.primary.data == AUX_HEAT_ZWAVE_MODE: + if self._mode().data == AUX_HEAT_ZWAVE_MODE: return True return False @@ -463,7 +468,7 @@ def preset_mode(self): Need to be one of PRESET_*. """ - if self.values.primary: + if self._mode(): return self._preset_mode return PRESET_NONE @@ -473,7 +478,7 @@ def preset_modes(self): Need to be a subset of PRESET_MODES. """ - if self.values.primary: + if self._mode(): return self._preset_list return [] @@ -522,11 +527,11 @@ def set_fan_mode(self, 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.primary: + if not self._mode(): return operation_mode = self._hvac_mapping.get(hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._mode().data = operation_mode def turn_aux_heat_on(self): """Turn auxillary heater on.""" @@ -534,7 +539,7 @@ def turn_aux_heat_on(self): return operation_mode = AUX_HEAT_ZWAVE_MODE _LOGGER.debug("Aux heat on. Set operation mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._mode().data = operation_mode def turn_aux_heat_off(self): """Turn auxillary heater off.""" @@ -545,23 +550,23 @@ def turn_aux_heat_off(self): else: operation_mode = self._hvac_mapping.get(HVAC_MODE_OFF) _LOGGER.debug("Aux heat off. Set operation mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._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.primary: + if not self._mode(): return if preset_mode == PRESET_NONE: # Activate the current hvac mode self._update_operation_mode() operation_mode = self._hvac_mapping.get(self.hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) - self.values.primary.data = operation_mode + self._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.primary.data = operation_mode + self._mode().data = operation_mode def set_swing_mode(self, swing_mode): """Set new target swing mode.""" @@ -577,3 +582,37 @@ def device_state_attributes(self): if self._fan_action: data[ATTR_FAN_ACTION] = self._fan_action return data + + +class ZWaveClimateSingleSetpoint(ZWaveClimateBase): + """Representation of a single setpoint Z-Wave thermostat device.""" + + def __init__(self, values, temp_unit): + """Initialize the Z-Wave climate device.""" + ZWaveClimateBase.__init__(self, values, temp_unit) + + def _mode(self) -> None: + """Return thermostat mode Z-Wave value.""" + return self.values.mode + + def _current_mode_setpoints(self) -> Tuple: + """Return a tuple of current setpoint Z-Wave value(s).""" + return (self.values.primary,) + + +class ZWaveClimateMultipleSetpoint(ZWaveClimateBase): + """Representation of a multiple setpoint Z-Wave thermostat device.""" + + def __init__(self, values, temp_unit): + """Initialize the Z-Wave climate device.""" + ZWaveClimateBase.__init__(self, values, temp_unit) + + def _mode(self) -> None: + """Return thermostat mode Z-Wave value.""" + return self.values.primary + + def _current_mode_setpoints(self) -> Tuple: + """Return a tuple of current setpoint Z-Wave value(s).""" + current_mode = str(self.values.primary.data).lower() + setpoints_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) + return tuple(getattr(self.values, name, None) for name in setpoints_names) diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index 2d6f08169eafcf..5e4b83d81e1346 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -48,11 +48,60 @@ ), }, { - const.DISC_COMPONENT: "climate", + const.DISC_COMPONENT: "climate", # thermostat without COMMAND_CLASS_THERMOSTAT_MODE const.DISC_GENERIC_DEVICE_CLASS: [ const.GENERIC_TYPE_THERMOSTAT, const.GENERIC_TYPE_SENSOR_MULTILEVEL, ], + const.DISC_SPECIFIC_DEVICE_CLASS: [ + const.SPECIFIC_TYPE_THERMOSTAT_HEATING, + const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + ], + const.DISC_VALUES: dict( + DEFAULT_VALUES_SCHEMA, + **{ + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_SETPOINT] + }, + "temperature": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_SENSOR_MULTILEVEL], + const.DISC_INDEX: [const.INDEX_SENSOR_MULTILEVEL_TEMPERATURE], + const.DISC_OPTIONAL: True, + }, + "fan_mode": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_FAN_MODE], + const.DISC_OPTIONAL: True, + }, + "operating_state": { + const.DISC_COMMAND_CLASS: [ + const.COMMAND_CLASS_THERMOSTAT_OPERATING_STATE + ], + const.DISC_OPTIONAL: True, + }, + "fan_action": { + const.DISC_COMMAND_CLASS: [ + const.COMMAND_CLASS_THERMOSTAT_FAN_ACTION + ], + const.DISC_OPTIONAL: True, + }, + "mode": { + const.DISC_COMMAND_CLASS: [const.COMMAND_CLASS_THERMOSTAT_MODE], + const.DISC_OPTIONAL: True, + }, + }, + ), + }, + { + const.DISC_COMPONENT: "climate", # thermostat with COMMAND_CLASS_THERMOSTAT_MODE + const.DISC_GENERIC_DEVICE_CLASS: [ + const.GENERIC_TYPE_THERMOSTAT, + const.GENERIC_TYPE_SENSOR_MULTILEVEL, + ], + const.DISC_SPECIFIC_DEVICE_CLASS: [ + const.SPECIFIC_TYPE_THERMOSTAT_GENERAL, + const.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, + const.SPECIFIC_TYPE_SETBACK_THERMOSTAT, + ], const.DISC_VALUES: dict( DEFAULT_VALUES_SCHEMA, **{ diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index c9fe123af82c8a..98e7bdbcbca857 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -13,6 +13,7 @@ PRESET_BOOST, PRESET_ECO, PRESET_NONE, + SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, @@ -21,8 +22,11 @@ ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ) -from homeassistant.components.zwave import climate -from homeassistant.components.zwave.climate import DEFAULT_HVAC_MODES +from homeassistant.components.zwave import climate, const +from homeassistant.components.zwave.climate import ( + AUX_HEAT_ZWAVE_MODE, + DEFAULT_HVAC_MODES, +) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed @@ -34,6 +38,7 @@ def device(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -62,6 +67,7 @@ def device_zxt_120(hass, mock_openzwave): values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -90,6 +96,7 @@ def device_mapping(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data="Heat", data_items=["Off", "Cool", "Heat", "Full Power", "Auto"], node=node, @@ -112,6 +119,7 @@ def device_unknown(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data="Heat", data_items=["Off", "Cool", "Heat", "heat_cool", "Abcdefg"], node=node, @@ -134,6 +142,7 @@ def device_heat_cool(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[ HVAC_MODE_OFF, @@ -162,6 +171,7 @@ def device_heat_cool_range(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT_COOL, data_items=[ HVAC_MODE_OFF, @@ -189,6 +199,7 @@ def device_heat_cool_away(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT_COOL, data_items=[ HVAC_MODE_OFF, @@ -219,6 +230,7 @@ def device_heat_eco(hass, mock_openzwave): node = MockNode() values = MockEntityValues( primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, data=HVAC_MODE_HEAT, data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "heat econ"], node=node, @@ -235,6 +247,100 @@ def device_heat_eco(hass, mock_openzwave): yield device +@pytest.fixture +def device_aux_heat(hass, mock_openzwave): + """Fixture to provide a precreated climate device. aux heat.""" + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT, "Aux Heat"], + node=node, + ), + setpoint_heating=MockValue(data=2, node=node), + setpoint_eco_heating=MockValue(data=1, node=node), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data="test4", node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_single_setpoint(hass, mock_openzwave): + """Fixture to provide a precreated climate device. + + SETPOINT_THERMOSTAT device class. + """ + + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, data=1, node=node + ), + mode=None, + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +@pytest.fixture +def device_single_setpoint_with_mode(hass, mock_openzwave): + """Fixture to provide a precreated climate device. + + SETPOINT_THERMOSTAT device class with COMMAND_CLASS_THERMOSTAT_MODE command class + """ + + node = MockNode() + values = MockEntityValues( + primary=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, data=1, node=node + ), + mode=MockValue( + command_class=const.COMMAND_CLASS_THERMOSTAT_MODE, + data=HVAC_MODE_HEAT, + data_items=[HVAC_MODE_OFF, HVAC_MODE_HEAT], + node=node, + ), + temperature=MockValue(data=5, node=node, units=None), + fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), + operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), + fan_action=MockValue(data=7, node=node), + ) + device = climate.get_device(hass, node=node, values=values, node_config={}) + + yield device + + +def test_get_device_detects_none(hass, mock_openzwave): + """Test get_device returns None.""" + node = MockNode() + value = MockValue(data=0, node=node) + values = MockEntityValues(primary=value) + + device = climate.get_device(hass, node=node, values=values, node_config={}) + assert device is None + + +def test_get_device_detects_multiple_setpoint_device(device): + """Test get_device returns a Z-Wave multiple setpoint device.""" + assert isinstance(device, climate.ZWaveClimateMultipleSetpoint) + + +def test_get_device_detects_single_setpoint_device(device_single_setpoint): + """Test get_device returns a Z-Wave single setpoint device.""" + assert isinstance(device_single_setpoint, climate.ZWaveClimateSingleSetpoint) + + def test_default_hvac_modes(): """Test wether all hvac modes are included in default_hvac_modes.""" for hvac_mode in HVAC_MODES: @@ -274,6 +380,18 @@ def test_supported_features_preset_mode(device_mapping): ) +def test_supported_features_preset_mode_away(device_heat_cool_away): + """Test supported features flags with swing mode.""" + device = device_heat_cool_away + assert ( + device.supported_features + == SUPPORT_FAN_MODE + + SUPPORT_TARGET_TEMPERATURE + + SUPPORT_TARGET_TEMPERATURE_RANGE + + SUPPORT_PRESET_MODE + ) + + def test_supported_features_swing_mode(device_zxt_120): """Test supported features flags with swing mode.""" device = device_zxt_120 @@ -286,6 +404,27 @@ def test_supported_features_swing_mode(device_zxt_120): ) +def test_supported_features_aux_heat(device_aux_heat): + """Test supported features flags with aux heat.""" + device = device_aux_heat + assert ( + device.supported_features + == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + SUPPORT_AUX_HEAT + ) + + +def test_supported_features_single_setpoint(device_single_setpoint): + """Test supported features flags for SETPOINT_THERMOSTAT.""" + device = device_single_setpoint + assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + + +def test_supported_features_single_setpoint_with_mode(device_single_setpoint_with_mode): + """Test supported features flags for SETPOINT_THERMOSTAT.""" + device = device_single_setpoint_with_mode + assert device.supported_features == SUPPORT_FAN_MODE + SUPPORT_TARGET_TEMPERATURE + + def test_zxt_120_swing_mode(device_zxt_120): """Test operation of the zxt 120 swing mode.""" device = device_zxt_120 @@ -331,6 +470,22 @@ def test_data_lists(device): assert device.preset_modes == [] +def test_data_lists_single_setpoint(device_single_setpoint): + """Test data lists from zwave value items.""" + device = device_single_setpoint + assert device.fan_modes == [3, 4, 5] + assert device.hvac_modes == [] + assert device.preset_modes == [] + + +def test_data_lists_single_setpoint_with_mode(device_single_setpoint_with_mode): + """Test data lists from zwave value items.""" + device = device_single_setpoint_with_mode + assert device.fan_modes == [3, 4, 5] + assert device.hvac_modes == [HVAC_MODE_OFF, HVAC_MODE_HEAT] + assert device.preset_modes == [] + + def test_data_lists_mapping(device_mapping): """Test data lists from zwave value items.""" device = device_mapping @@ -404,6 +559,14 @@ def test_target_value_set_eco(device_heat_eco): assert device.values.setpoint_eco_heating.data == 0 +def test_target_value_set_single_setpoint(device_single_setpoint): + """Test values changed for climate device.""" + device = device_single_setpoint + assert device.values.primary.data == 1 + device.set_temperature(**{ATTR_TEMPERATURE: 2}) + assert device.values.primary.data == 2 + + def test_operation_value_set(device): """Test values changed for climate device.""" assert device.values.primary.data == HVAC_MODE_HEAT @@ -546,6 +709,15 @@ def test_target_changed_with_mode(device): assert device.target_temperature_high == 10 +def test_target_value_changed_single_setpoint(device_single_setpoint): + """Test values changed for climate device.""" + device = device_single_setpoint + assert device.target_temperature == 1 + device.values.primary.data = 2 + value_changed(device.values.primary) + assert device.target_temperature == 2 + + def test_temperature_value_changed(device): """Test values changed for climate device.""" assert device.current_temperature == 5 @@ -677,3 +849,44 @@ def test_fan_action_value_changed(device): device.values.fan_action.data = 9 value_changed(device.values.fan_action) assert device.device_state_attributes[climate.ATTR_FAN_ACTION] == 9 + + +def test_aux_heat_unsupported_set(device): + """Test aux heat for climate device.""" + device = device + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_on() + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_off() + assert device.values.primary.data == HVAC_MODE_HEAT + + +def test_aux_heat_unsupported_value_changed(device): + """Test aux heat for climate device.""" + device = device + assert device.is_aux_heat is None + device.values.primary.data = HVAC_MODE_HEAT + value_changed(device.values.primary) + assert device.is_aux_heat is None + + +def test_aux_heat_set(device_aux_heat): + """Test aux heat for climate device.""" + device = device_aux_heat + assert device.values.primary.data == HVAC_MODE_HEAT + device.turn_aux_heat_on() + assert device.values.primary.data == AUX_HEAT_ZWAVE_MODE + device.turn_aux_heat_off() + assert device.values.primary.data == HVAC_MODE_HEAT + + +def test_aux_heat_value_changed(device_aux_heat): + """Test aux heat for climate device.""" + device = device_aux_heat + assert device.is_aux_heat is False + device.values.primary.data = AUX_HEAT_ZWAVE_MODE + value_changed(device.values.primary) + assert device.is_aux_heat is True + device.values.primary.data = HVAC_MODE_HEAT + value_changed(device.values.primary) + assert device.is_aux_heat is False diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 7038d6b61147c9..faa1ed2a88c850 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -573,7 +573,11 @@ def mock_connect(receiver, signal, *args, **kwargs): assert len(mock_receivers) == 1 - node = MockNode(node_id=11, generic=const.GENERIC_TYPE_THERMOSTAT) + node = MockNode( + node_id=11, + generic=const.GENERIC_TYPE_THERMOSTAT, + specific=const.SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2, + ) thermostat_mode = MockValue( data="Heat", data_items=["Off", "Heat"], @@ -638,6 +642,42 @@ def mock_update(self): ) +async def test_value_discovery_legacy_thermostat(hass, mock_openzwave): + """Test discovery of a node. Special case for legacy thermostats.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_VALUE_ADDED: + mock_receivers.append(receiver) + + with patch("pydispatch.dispatcher.connect", new=mock_connect): + await async_setup_component(hass, "zwave", {"zwave": {}}) + await hass.async_block_till_done() + + assert len(mock_receivers) == 1 + + node = MockNode( + node_id=11, + generic=const.GENERIC_TYPE_THERMOSTAT, + specific=const.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + ) + setpoint_heating = MockValue( + data=22.0, + node=node, + command_class=const.COMMAND_CLASS_THERMOSTAT_SETPOINT, + index=1, + genre=const.GENRE_USER, + ) + + hass.async_add_job(mock_receivers[0], node, setpoint_heating) + await hass.async_block_till_done() + + assert ( + hass.states.get("climate.mock_node_mock_value").attributes["temperature"] + == 22.0 + ) + + async def test_power_schemes(hass, mock_openzwave): """Test power attribute.""" mock_receivers = [] From 9f64656603ec13466fdc5e2d6dfbaebc34f663e9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 18 Dec 2019 13:06:57 -0700 Subject: [PATCH 2473/3953] Bump simplisafe-python to 5.3.6 (#30055) --- 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 4115ce455b5d46..2df49bb520987e 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/integrations/simplisafe", "requirements": [ - "simplisafe-python==5.3.5" + "simplisafe-python==5.3.6" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 68c772785b3e0d..9c2fb16fa16645 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1793,7 +1793,7 @@ shodan==1.20.0 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==5.3.5 +simplisafe-python==5.3.6 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 700bdf6a3b2bf3..a59a9a57d547b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ rxv==0.6.0 samsungctl[websocket]==0.7.1 # homeassistant.components.simplisafe -simplisafe-python==5.3.5 +simplisafe-python==5.3.6 # homeassistant.components.sleepiq sleepyq==0.7 From 952b21faccdbbe15fa28e54dd83273b7578a27d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Dec 2019 21:24:39 +0100 Subject: [PATCH 2474/3953] Bumped version to 0.103.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e312bd4c2574aa..34e6153b966779 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 103 -PATCH_VERSION = "0" +PATCH_VERSION = "1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 5ea4ba6a2e9f53ffa761a28176b08c59e022b403 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 19 Dec 2019 00:32:16 +0000 Subject: [PATCH 2475/3953] [ci skip] Translation update --- homeassistant/components/soma/.translations/es.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json index 8692262270455c..6df113b82c9396 100644 --- a/homeassistant/components/soma/.translations/es.json +++ b/homeassistant/components/soma/.translations/es.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", - "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n." + "connection_error": "No se ha podido conectar a SOMA Connect.", + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n.", + "result_error": "SOMA Connect respondi\u00f3 con un error." }, "create_entry": { "default": "Autenticado con \u00e9xito con Soma." From 9e5de1a106b934100c98aac1764f8a7a4b713fd5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 18 Dec 2019 17:52:56 -0700 Subject: [PATCH 2476/3953] Guard against future unknown SimpliSafe entity types (#30059) * Guard against future unknown SimpliSafe entity types * Updated log message --- .../components/simplisafe/alarm_control_panel.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 9671d56c873483..05dad43955cee6 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -177,11 +177,23 @@ async def async_update(self): self._state = None last_event = self._simplisafe.last_event_data[self._system.system_id] + + try: + last_event_sensor_type = EntityTypes(last_event["sensorType"]).name + except ValueError: + _LOGGER.warning( + 'Encountered unknown entity type: %s ("%s"). Please report it at' + "https://github.com/home-assistant/home-assistant/issues.", + last_event["sensorType"], + last_event["sensorName"], + ) + last_event_sensor_type = None + self._attrs.update( { ATTR_LAST_EVENT_INFO: last_event["info"], ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"], - ATTR_LAST_EVENT_SENSOR_TYPE: EntityTypes(last_event["sensorType"]).name, + ATTR_LAST_EVENT_SENSOR_TYPE: last_event_sensor_type, ATTR_LAST_EVENT_TIMESTAMP: utc_from_timestamp( last_event["eventTimestamp"] ), From aae80dca1cb101b420e7e0c0933cdba83abb50fd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 Dec 2019 10:40:05 -0800 Subject: [PATCH 2477/3953] Fix recursion --- homeassistant/requirements.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index a0eec0f442b782..eb8aeeecfae896 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -3,7 +3,7 @@ from pathlib import Path import logging import os -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Set from homeassistant.exceptions import HomeAssistantError import homeassistant.util.package as pkg_util @@ -28,16 +28,19 @@ def __init__(self, domain: str, requirements: List) -> None: async def async_get_integration_with_requirements( - hass: HomeAssistant, domain: str + hass: HomeAssistant, domain: str, done: Set[str] = None ) -> 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. - - Does not handle circular dependencies. """ + if done is None: + done = {domain} + else: + done.add(domain) + integration = await async_get_integration(hass, domain) if hass.config.skip_pip: @@ -48,11 +51,18 @@ async def async_get_integration_with_requirements( hass, integration.domain, integration.requirements ) - deps = integration.dependencies + (integration.after_dependencies or []) + deps_to_check = [ + dep + for dep in integration.dependencies + (integration.after_dependencies or []) + if dep not in done + ] - if deps: + if deps_to_check: await asyncio.gather( - *[async_get_integration_with_requirements(hass, dep) for dep in deps] + *[ + async_get_integration_with_requirements(hass, dep, done) + for dep in deps_to_check + ] ) return integration From c9de5b9fefb02725341d8bfecffd06f82911e46b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Dec 2019 07:53:38 +0100 Subject: [PATCH 2478/3953] Bumped version to 0.103.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 34e6153b966779..01a2d609b87c0f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 103 -PATCH_VERSION = "1" +PATCH_VERSION = "2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From c3144eddbbfe1d7b0949caeb3cd7a2f61841f71a Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 19 Dec 2019 08:45:23 +0000 Subject: [PATCH 2479/3953] Set unique id on homekit_controller config entries (#30035) * Set unique id on config entries * Changes from review --- .../components/homekit_controller/__init__.py | 9 +++- .../homekit_controller/config_flow.py | 54 ++++++++++--------- .../homekit_controller/test_config_flow.py | 28 +++++++--- 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index c863da14a3a409..444f64b6f386c9 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -9,8 +9,7 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import Entity -# We need an import from .config_flow, without it .config_flow is never loaded. -from .config_flow import HomekitControllerFlowHandler # noqa: F401 +from .config_flow import normalize_hkid from .connection import HKDevice, get_accessory_information from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES from .storage import EntityMapStorage @@ -181,6 +180,12 @@ async def async_setup_entry(hass, entry): conn = HKDevice(hass, entry, entry.data) hass.data[KNOWN_DEVICES][conn.unique_id] = conn + # For backwards compat + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=normalize_hkid(conn.unique_id) + ) + if not await conn.async_setup(): del hass.data[KNOWN_DEVICES][conn.unique_id] raise ConfigEntryNotReady diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index f4eb1190727ab4..6cc724e9fe5ba2 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -46,6 +46,11 @@ def load_old_pairings(hass): return old_pairings +def normalize_hkid(hkid): + """Normalize a hkid so that it is safe to compare with other normalized hkids.""" + return hkid.lower() + + @callback def find_existing_host(hass, serial): """Return a set of the configured hosts.""" @@ -77,6 +82,9 @@ async def async_step_user(self, user_input=None): key = user_input["device"] self.hkid = self.devices[key]["id"] self.model = self.devices[key]["md"] + await self.async_set_unique_id( + normalize_hkid(self.hkid), raise_on_progress=False + ) return await self.async_step_pair() all_hosts = await self.hass.async_add_executor_job(self.controller.discover, 5) @@ -120,18 +128,6 @@ async def async_step_zeroconf(self, discovery_info): status_flags = int(properties["sf"]) paired = not status_flags & 0x01 - _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) - - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context["hkid"] = hkid - self.context["title_placeholders"] = {"name": name} - - # If multiple HomekitControllerFlowHandler end up getting created - # for the same accessory dont let duplicates hang around - active_flows = self._async_in_progress() - if any(hkid == flow["context"]["hkid"] for flow in active_flows): - return self.async_abort(reason="already_in_progress") - # The configuration number increases every time the characteristic map # needs updating. Some devices use a slightly off-spec name so handle # both cases. @@ -143,21 +139,27 @@ async def async_step_zeroconf(self, discovery_info): ) config_num = None - if paired: - if hkid in self.hass.data.get(KNOWN_DEVICES, {}): - # The device is already paired and known to us - # According to spec we should monitor c# (config_num) for - # changes. If it changes, we check for new entities - conn = self.hass.data[KNOWN_DEVICES][hkid] - if conn.config_num != config_num: - _LOGGER.debug( - "HomeKit info %s: c# incremented, refreshing entities", hkid - ) - self.hass.async_create_task( - conn.async_refresh_entity_map(config_num) - ) - return self.async_abort(reason="already_configured") + # If the device is already paired and known to us we should monitor c# + # (config_num) for changes. If it changes, we check for new entities + if paired and hkid in self.hass.data.get(KNOWN_DEVICES, {}): + conn = self.hass.data[KNOWN_DEVICES][hkid] + if conn.config_num != config_num: + _LOGGER.debug( + "HomeKit info %s: c# incremented, refreshing entities", hkid + ) + self.hass.async_create_task(conn.async_refresh_entity_map(config_num)) + return self.async_abort(reason="already_configured") + + _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) + + await self.async_set_unique_id(normalize_hkid(hkid)) + self._abort_if_unique_id_configured() + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["hkid"] = hkid + self.context["title_placeholders"] = {"name": name} + + if paired: old_pairings = await self.hass.async_add_executor_job( load_old_pairings, self.hass ) diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 22b486e1d5193f..4733581f1368e9 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -74,6 +74,7 @@ async def test_discovery_works(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -122,6 +123,7 @@ async def test_discovery_works_upper_case(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -169,6 +171,7 @@ async def test_discovery_works_missing_csharp(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -234,6 +237,7 @@ async def test_pair_already_paired_1(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -259,6 +263,7 @@ async def test_discovery_ignored_model(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -286,6 +291,7 @@ async def test_discovery_invalid_config_entry(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # Discovery of a HKID that is in a pairable state but for which there is @@ -315,10 +321,7 @@ async def test_discovery_already_configured(hass): result = await flow.async_step_zeroconf(discovery_info) assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - } + assert flow.context == {} assert conn.async_config_num_changed.call_count == 0 @@ -343,10 +346,7 @@ async def test_discovery_already_configured_config_change(hass): result = await flow.async_step_zeroconf(discovery_info) assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - } + assert flow.context == {} assert conn.async_refresh_entity_map.call_args == mock.call(2) @@ -369,6 +369,7 @@ async def test_pair_unable_to_pair(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -402,6 +403,7 @@ async def test_pair_abort_errors_on_start(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device refuses to enter pairing mode @@ -414,6 +416,7 @@ async def test_pair_abort_errors_on_start(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -436,6 +439,7 @@ async def test_pair_form_errors_on_start(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device refuses to enter pairing mode @@ -448,6 +452,7 @@ async def test_pair_form_errors_on_start(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -470,6 +475,7 @@ async def test_pair_abort_errors_on_finish(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -486,6 +492,7 @@ async def test_pair_abort_errors_on_finish(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -508,6 +515,7 @@ async def test_pair_form_errors_on_finish(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code @@ -524,6 +532,7 @@ async def test_pair_form_errors_on_finish(hass, exception, expected): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -712,6 +721,7 @@ async def test_parse_new_homekit_json(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -763,6 +773,7 @@ async def test_parse_old_homekit_json(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } @@ -823,4 +834,5 @@ async def test_parse_overlapping_homekit_json(hass): assert flow.context == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", } From e0d6810134418d8d3d7464998dcd31c355b614a0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Dec 2019 11:23:19 +0100 Subject: [PATCH 2480/3953] Remove stream from camera after deps (#30057) --- homeassistant/components/camera/manifest.json | 2 +- script/hassfest/dependencies.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index 32cd7c3fe47138..25d344d05addfa 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/camera", "requirements": [], "dependencies": ["http"], - "after_dependencies": ["stream", "media_player"], + "after_dependencies": ["media_player"], "codeowners": [] } diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 6ba228b5bc7a10..c67779d3c33ec5 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -102,6 +102,7 @@ def visit_Attribute(self, node): "discovery", # Other "mjpeg", # base class, has no reqs or component to load. + "stream", # Stream cannot install on all systems, can be imported without reqs. } IGNORE_VIOLATIONS = [ From 9804fbb527fb053296533c5cd4604afcb7043a28 Mon Sep 17 00:00:00 2001 From: Yuchen Ying Date: Thu, 19 Dec 2019 02:23:46 -0800 Subject: [PATCH 2481/3953] Add unit_of_measurement to various Transmission sensors (#30037) * Add unit_of_measurement to various Transmission sensors Without unit_of_measurement, the history graph card will not show those sensors as line chart. * Change Counts to Torrents. --- homeassistant/components/transmission/const.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 5540f718ba10f0..9a9250dbed65ba 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -2,14 +2,14 @@ DOMAIN = "transmission" SENSOR_TYPES = { - "active_torrents": ["Active Torrents", None], + "active_torrents": ["Active Torrents", "Torrents"], "current_status": ["Status", None], "download_speed": ["Down Speed", "MB/s"], - "paused_torrents": ["Paused Torrents", None], - "total_torrents": ["Total Torrents", None], + "paused_torrents": ["Paused Torrents", "Torrents"], + "total_torrents": ["Total Torrents", "Torrents"], "upload_speed": ["Up Speed", "MB/s"], - "completed_torrents": ["Completed Torrents", None], - "started_torrents": ["Started Torrents", None], + "completed_torrents": ["Completed Torrents", "Torrents"], + "started_torrents": ["Started Torrents", "Torrents"], } SWITCH_TYPES = {"on_off": "Switch", "turtle_mode": "Turtle Mode"} From 5baaa852ddb6777de839bda12ff0b7022c74beb0 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Thu, 19 Dec 2019 06:44:17 -0500 Subject: [PATCH 2482/3953] Refactor Alexa capabilityResources object into class, Implement Alexa semantics object (#29917) * Refactor capabilityResources object into class. Implement semantics object to support open, close, raise, lower utterences. Replace covers PercentageController with RangeController. Add semantics for covers. Remove PowerController for covers. Add new display categories. Add new items to Alexa Global Catalog. Implement garage door voice PIN code support though Alexa app. Fixed bug with getting property for ModeController. Fixed bug were PercentageController AdjustPercentage would exceed 100. * Comment fixes in Tests. * Reorder imports. * Added additional tests for more code coverage. * Added and additional test for more code coverage. * Explicitly return None for configuration() if not instance of AlexaCapabilityResource. --- .../components/alexa/capabilities.py | 386 ++++++++--------- homeassistant/components/alexa/const.py | 157 ------- homeassistant/components/alexa/entities.py | 76 +++- homeassistant/components/alexa/handlers.py | 195 ++++++--- homeassistant/components/alexa/resources.py | 387 ++++++++++++++++++ tests/components/alexa/test_capabilities.py | 14 +- tests/components/alexa/test_smart_home.py | 387 +++++++++++++++--- 7 files changed, 1119 insertions(+), 483 deletions(-) create mode 100644 homeassistant/components/alexa/resources.py diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index b5ffb1ef7e6442..938101a75001d6 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -13,11 +13,9 @@ STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, - STATE_CLOSED, STATE_LOCKED, STATE_OFF, STATE_ON, - STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, @@ -34,10 +32,16 @@ DATE_FORMAT, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, - Catalog, Inputs, ) from .errors import UnsupportedProperty +from .resources import ( + AlexaCapabilityResource, + AlexaGlobalCatalog, + AlexaModeResource, + AlexaPresetResource, + AlexaSemantics, +) _LOGGER = logging.getLogger(__name__) @@ -108,12 +112,15 @@ def capability_proactively_reported(): @staticmethod def capability_resources(): - """Applicable to ToggleController, RangeController, and ModeController interfaces.""" + """Return the capability object. + + Applicable to ToggleController, RangeController, and ModeController interfaces. + """ return [] @staticmethod def configuration(): - """Return the Configuration object.""" + """Return the configuration object.""" return [] @staticmethod @@ -121,6 +128,14 @@ def inputs(): """Applicable only to media players.""" return [] + @staticmethod + def semantics(): + """Return the semantics object. + + Applicable to ToggleController, RangeController, and ModeController interfaces. + """ + return [] + @staticmethod def supported_operations(): """Return the supportedOperations object.""" @@ -130,6 +145,10 @@ def serialize_discovery(self): """Serialize according to the Discovery API.""" result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} + instance = self.instance + if instance is not None: + result["instance"] = instance + properties_supported = self.properties_supported() if properties_supported: result["properties"] = { @@ -138,22 +157,19 @@ def serialize_discovery(self): "retrievable": self.properties_retrievable(), } - # pylint: disable=assignment-from-none proactively_reported = self.capability_proactively_reported() if proactively_reported is not None: result["proactivelyReported"] = proactively_reported - # pylint: disable=assignment-from-none non_controllable = self.properties_non_controllable() if non_controllable is not None: result["properties"]["nonControllable"] = non_controllable - # pylint: disable=assignment-from-none supports_deactivation = self.supports_deactivation() if supports_deactivation is not None: result["supportsDeactivation"] = supports_deactivation - capability_resources = self.serialize_capability_resources() + capability_resources = self.capability_resources() if capability_resources: result["capabilityResources"] = capability_resources @@ -161,10 +177,9 @@ def serialize_discovery(self): if configuration: result["configuration"] = configuration - # pylint: disable=assignment-from-none - instance = self.instance - if instance is not None: - result["instance"] = instance + semantics = self.semantics() + if semantics: + result["semantics"] = semantics supported_operations = self.supported_operations() if supported_operations: @@ -196,36 +211,6 @@ def serialize_properties(self): yield result - def serialize_capability_resources(self): - """Return capabilityResources friendlyNames serialized for an API response.""" - resources = self.capability_resources() - if resources: - return {"friendlyNames": self.serialize_friendly_names(resources)} - - return None - - @staticmethod - def serialize_friendly_names(resources): - """Return capabilityResources, ModeResources, or presetResources friendlyNames serialized for an API response.""" - friendly_names = [] - for resource in resources: - if resource["type"] == Catalog.LABEL_ASSET: - friendly_names.append( - { - "@type": Catalog.LABEL_ASSET, - "value": {"assetId": resource["value"]}, - } - ) - else: - friendly_names.append( - { - "@type": Catalog.LABEL_TEXT, - "value": {"text": resource["value"], "locale": "en-US"}, - } - ) - - return friendly_names - class Alexa(AlexaCapability): """Implements Alexa Interface. @@ -906,6 +891,8 @@ class AlexaModeController(AlexaCapability): def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" super().__init__(entity, instance) + self._resource = None + self._semantics = None self.properties_non_controllable = lambda: non_controllable def name(self): @@ -922,108 +909,102 @@ def properties_proactively_reported(self): def properties_retrievable(self): """Return True if properties can be retrieved.""" + return True def get_property(self, name): """Read and return a property.""" if name != "mode": raise UnsupportedProperty(name) + # Fan Direction if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - return self.entity.attributes.get(fan.ATTR_DIRECTION) + mode = self.entity.attributes.get(fan.ATTR_DIRECTION, None) + if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN): + return f"{fan.ATTR_DIRECTION}.{mode}" + # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": - return self.entity.attributes.get(cover.ATTR_POSITION) + # Return state instead of position when using ModeController. + mode = self.entity.state + if mode in ( + cover.STATE_OPEN, + cover.STATE_OPENING, + cover.STATE_CLOSED, + cover.STATE_CLOSING, + STATE_UNKNOWN, + ): + return f"{cover.ATTR_POSITION}.{mode}" return None def configuration(self): """Return configuration with modeResources.""" - return self.serialize_mode_resources() + if isinstance(self._resource, AlexaCapabilityResource): + return self._resource.serialize_configuration() + + return None def capability_resources(self): """Return capabilityResources object.""" - capability_resources = [] + # Fan Direction Resource if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - capability_resources = [ - {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION} - ] + self._resource = AlexaModeResource( + [AlexaGlobalCatalog.SETTING_DIRECTION], False + ) + self._resource.add_mode( + f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", [fan.DIRECTION_FORWARD] + ) + self._resource.add_mode( + f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", [fan.DIRECTION_REVERSE] + ) + return self._resource.serialize_capability_resources() + # Cover Position Resources if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": - capability_resources = [ - {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_MODE}, - {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_PRESET}, - ] + self._resource = AlexaModeResource( + ["Position", AlexaGlobalCatalog.SETTING_OPENING], False + ) + self._resource.add_mode( + f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", + [AlexaGlobalCatalog.VALUE_OPEN], + ) + self._resource.add_mode( + f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", + [AlexaGlobalCatalog.VALUE_CLOSE], + ) + self._resource.add_mode(f"{cover.ATTR_POSITION}.custom", ["Custom"]) + return self._resource.serialize_capability_resources() - return capability_resources + return None - def mode_resources(self): - """Return modeResources object.""" - mode_resources = None - if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - mode_resources = { - "ordered": False, - "resources": [ - { - "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", - "friendly_names": [ - {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_FORWARD} - ], - }, - { - "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", - "friendly_names": [ - {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_REVERSE} - ], - }, - ], - } + def semantics(self): + """Build and return semantics object.""" + # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": - mode_resources = { - "ordered": False, - "resources": [ - { - "value": f"{cover.ATTR_POSITION}.{STATE_OPEN}", - "friendly_names": [ - {"type": Catalog.LABEL_TEXT, "value": "open"}, - {"type": Catalog.LABEL_TEXT, "value": "opened"}, - {"type": Catalog.LABEL_TEXT, "value": "raise"}, - {"type": Catalog.LABEL_TEXT, "value": "raised"}, - ], - }, - { - "value": f"{cover.ATTR_POSITION}.{STATE_CLOSED}", - "friendly_names": [ - {"type": Catalog.LABEL_TEXT, "value": "close"}, - {"type": Catalog.LABEL_TEXT, "value": "closed"}, - {"type": Catalog.LABEL_TEXT, "value": "shut"}, - {"type": Catalog.LABEL_TEXT, "value": "lower"}, - {"type": Catalog.LABEL_TEXT, "value": "lowered"}, - ], - }, - ], - } - - return mode_resources - - def serialize_mode_resources(self): - """Return ModeResources, friendlyNames serialized for an API response.""" - mode_resources = [] - resources = self.mode_resources() - ordered = resources["ordered"] - for resource in resources["resources"]: - mode_value = resource["value"] - friendly_names = resource["friendly_names"] - result = { - "value": mode_value, - "modeResources": { - "friendlyNames": self.serialize_friendly_names(friendly_names) - }, - } - mode_resources.append(result) + self._semantics = AlexaSemantics() + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER], + "SetMode", + {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"}, + ) + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE], + "SetMode", + {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"}, + ) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_CLOSED], + f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", + ) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_OPEN], + f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", + ) + return self._semantics.serialize_semantics() - return {"ordered": ordered, "supportedModes": mode_resources} + return None class AlexaRangeController(AlexaCapability): @@ -1035,6 +1016,8 @@ class AlexaRangeController(AlexaCapability): def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" super().__init__(entity, instance) + self._resource = None + self._semantics = None self.properties_non_controllable = lambda: non_controllable def name(self): @@ -1058,88 +1041,111 @@ def get_property(self, name): if name != "rangeValue": raise UnsupportedProperty(name) + # Fan Speed if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": speed = self.entity.attributes.get(fan.ATTR_SPEED) return RANGE_FAN_MAP.get(speed, 0) + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION) + + # Cover Tilt Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION) + return None def configuration(self): """Return configuration with presetResources.""" - return self.serialize_preset_resources() + if isinstance(self._resource, AlexaCapabilityResource): + return self._resource.serialize_configuration() + + return None def capability_resources(self): """Return capabilityResources object.""" - capability_resources = [] + # Fan Speed Resources if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": - return [{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_FANSPEED}] + self._resource = AlexaPresetResource( + labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED], + min_value=1, + max_value=3, + precision=1, + ) + self._resource.add_preset( + value=1, + labels=[AlexaGlobalCatalog.VALUE_LOW, AlexaGlobalCatalog.VALUE_MINIMUM], + ) + self._resource.add_preset(value=2, labels=[AlexaGlobalCatalog.VALUE_MEDIUM]) + self._resource.add_preset( + value=3, + labels=[ + AlexaGlobalCatalog.VALUE_HIGH, + AlexaGlobalCatalog.VALUE_MAXIMUM, + ], + ) + return self._resource.serialize_capability_resources() + + # Cover Position Resources + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + self._resource = AlexaPresetResource( + ["Position", AlexaGlobalCatalog.SETTING_OPENING], + min_value=0, + max_value=100, + precision=1, + unit=AlexaGlobalCatalog.UNIT_PERCENT, + ) + return self._resource.serialize_capability_resources() + + # Cover Tilt Position Resources + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + self._resource = AlexaPresetResource( + ["Tilt Position", AlexaGlobalCatalog.SETTING_OPENING], + min_value=0, + max_value=100, + precision=1, + unit=AlexaGlobalCatalog.UNIT_PERCENT, + ) + return self._resource.serialize_capability_resources() - return capability_resources + return None - def preset_resources(self): - """Return presetResources object.""" - preset_resources = [] + def semantics(self): + """Build and return semantics object.""" - if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": - preset_resources = { - "minimumValue": 1, - "maximumValue": 3, - "precision": 1, - "presets": [ - { - "rangeValue": 1, - "names": [ - { - "type": Catalog.LABEL_ASSET, - "value": Catalog.VALUE_MINIMUM, - }, - {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_LOW}, - ], - }, - { - "rangeValue": 2, - "names": [ - {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_MEDIUM} - ], - }, - { - "rangeValue": 3, - "names": [ - { - "type": Catalog.LABEL_ASSET, - "value": Catalog.VALUE_MAXIMUM, - }, - {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_HIGH}, - ], - }, - ], - } + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + self._semantics = AlexaSemantics() + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0} + ) + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_RAISE], "SetRangeValue", {"rangeValue": 100} + ) + self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0) + self._semantics.add_states_to_range( + [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 + ) + return self._semantics.serialize_semantics() - return preset_resources - - def serialize_preset_resources(self): - """Return PresetResources, friendlyNames serialized for an API response.""" - preset_resources = [] - resources = self.preset_resources() - for preset in resources["presets"]: - preset_resources.append( - { - "rangeValue": preset["rangeValue"], - "presetResources": { - "friendlyNames": self.serialize_friendly_names(preset["names"]) - }, - } + # Cover Tilt Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + self._semantics = AlexaSemantics() + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0} + ) + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_OPEN], "SetRangeValue", {"rangeValue": 100} + ) + self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0) + self._semantics.add_states_to_range( + [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 ) + return self._semantics.serialize_semantics() - return { - "supportedRange": { - "minimumValue": resources["minimumValue"], - "maximumValue": resources["maximumValue"], - "precision": resources["precision"], - }, - "presets": preset_resources, - } + return None class AlexaToggleController(AlexaCapability): @@ -1151,6 +1157,8 @@ class AlexaToggleController(AlexaCapability): def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" super().__init__(entity, instance) + self._resource = None + self._semantics = None self.properties_non_controllable = lambda: non_controllable def name(self): @@ -1174,6 +1182,7 @@ def get_property(self, name): if name != "toggleState": raise UnsupportedProperty(name) + # Fan Oscillating if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING)) return "ON" if is_on else "OFF" @@ -1182,16 +1191,15 @@ def get_property(self, name): def capability_resources(self): """Return capabilityResources object.""" - capability_resources = [] + # Fan Oscillating Resource if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": - capability_resources = [ - {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_OSCILLATE}, - {"type": Catalog.LABEL_TEXT, "value": "Rotate"}, - {"type": Catalog.LABEL_TEXT, "value": "Rotation"}, - ] + self._resource = AlexaCapabilityResource( + [AlexaGlobalCatalog.SETTING_OSCILLATE, "Rotate", "Rotation"] + ) + return self._resource.serialize_capability_resources() - return capability_resources + return None class AlexaChannelController(AlexaCapability): diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 2c62e1a485a6a6..f1a86859da9dc3 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -117,163 +117,6 @@ class Cause: VOICE_INTERACTION = "VOICE_INTERACTION" -class Catalog: - """The Global Alexa catalog. - - https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog - - You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units. - This catalog is localized into all the languages that Alexa supports. - - You can reference the following catalog of pre-defined friendly names. - Each item in the following list is an asset identifier followed by its supported friendly names. - The first friendly name for each identifier is the one displayed in the Alexa mobile app. - """ - - LABEL_ASSET = "asset" - LABEL_TEXT = "text" - - # Shower - DEVICENAME_SHOWER = "Alexa.DeviceName.Shower" - - # Washer, Washing Machine - DEVICENAME_WASHER = "Alexa.DeviceName.Washer" - - # Router, Internet Router, Network Router, Wifi Router, Net Router - DEVICENAME_ROUTER = "Alexa.DeviceName.Router" - - # Fan, Blower - DEVICENAME_FAN = "Alexa.DeviceName.Fan" - - # Air Purifier, Air Cleaner,Clean Air Machine - DEVICENAME_AIRPURIFIER = "Alexa.DeviceName.AirPurifier" - - # Space Heater, Portable Heater - DEVICENAME_SPACEHEATER = "Alexa.DeviceName.SpaceHeater" - - # Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet - SHOWER_RAINHEAD = "Alexa.Shower.RainHead" - - # Handheld Shower, Shower Wand, Hand Shower - SHOWER_HANDHELD = "Alexa.Shower.HandHeld" - - # Water Temperature, Water Temp, Water Heat - SETTING_WATERTEMPERATURE = "Alexa.Setting.WaterTemperature" - - # Temperature, Temp - SETTING_TEMPERATURE = "Alexa.Setting.Temperature" - - # Wash Cycle, Wash Preset, Wash setting - SETTING_WASHCYCLE = "Alexa.Setting.WashCycle" - - # 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi - SETTING_2GGUESTWIFI = "Alexa.Setting.2GGuestWiFi" - - # 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi - SETTING_5GGUESTWIFI = "Alexa.Setting.5GGuestWiFi" - - # Guest Wi-fi, Guest Network, Guest Net - SETTING_GUESTWIFI = "Alexa.Setting.GuestWiFi" - - # Auto, Automatic, Automatic Mode, Auto Mode - SETTING_AUTO = "Alexa.Setting.Auto" - - # #Night, Night Mode - SETTING_NIGHT = "Alexa.Setting.Night" - - # Quiet, Quiet Mode, Noiseless, Silent - SETTING_QUIET = "Alexa.Setting.Quiet" - - # Oscillate, Swivel, Oscillation, Spin, Back and forth - SETTING_OSCILLATE = "Alexa.Setting.Oscillate" - - # Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity - SETTING_FANSPEED = "Alexa.Setting.FanSpeed" - - # Preset, Setting - SETTING_PRESET = "Alexa.Setting.Preset" - - # Mode - SETTING_MODE = "Alexa.Setting.Mode" - - # Direction - SETTING_DIRECTION = "Alexa.Setting.Direction" - - # Delicates, Delicate - VALUE_DELICATE = "Alexa.Value.Delicate" - - # Quick Wash, Fast Wash, Wash Quickly, Speed Wash - VALUE_QUICKWASH = "Alexa.Value.QuickWash" - - # Maximum, Max - VALUE_MAXIMUM = "Alexa.Value.Maximum" - - # Minimum, Min - VALUE_MINIMUM = "Alexa.Value.Minimum" - - # High - VALUE_HIGH = "Alexa.Value.High" - - # Low - VALUE_LOW = "Alexa.Value.Low" - - # Medium, Mid - VALUE_MEDIUM = "Alexa.Value.Medium" - - -class Unit: - """Alexa Units of Measure. - - https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#units-of-measure - """ - - ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees" - - ANGLE_RADIANS = "Alexa.Unit.Angle.Radians" - - DISTANCE_FEET = "Alexa.Unit.Distance.Feet" - - DISTANCE_INCHES = "Alexa.Unit.Distance.Inches" - - DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers" - - DISTANCE_METERS = "Alexa.Unit.Distance.Meters" - - DISTANCE_MILES = "Alexa.Unit.Distance.Miles" - - DISTANCE_YARDS = "Alexa.Unit.Distance.Yards" - - MASS_GRAMS = "Alexa.Unit.Mass.Grams" - - MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms" - - PERCENT = "Alexa.Unit.Percent" - - TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius" - - TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees" - - TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit" - - TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin" - - VOLUME_CUBICFEET = "Alexa.Unit.Volume.CubicFeet" - - VOLUME_CUBICMETERS = "Alexa.Unit.Volume.CubicMeters" - - VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons" - - VOLUME_LITERS = "Alexa.Unit.Volume.Liters" - - VOLUME_PINTS = "Alexa.Unit.Volume.Pints" - - VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts" - - WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" - - WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" - - class Inputs: """Valid names for the InputController. diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 017686df60771a..2a3355434a3ca9 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -83,6 +83,9 @@ class DisplayCategory: # Indicates media devices with video or photo capabilities. CAMERA = "CAMERA" + # Indicates a non-mobile computer, such as a desktop computer. + COMPUTER = "COMPUTER" + # Indicates an endpoint that detects and reports contact. CONTACT_SENSOR = "CONTACT_SENSOR" @@ -92,27 +95,60 @@ class DisplayCategory: # Indicates a doorbell. DOORBELL = "DOORBELL" + # Indicates a window covering on the outside of a structure. + EXTERIOR_BLIND = "EXTERIOR_BLIND" + # Indicates a fan. FAN = "FAN" + # Indicates a game console, such as Microsoft Xbox or Nintendo Switch + GAME_CONSOLE = "GAME_CONSOLE" + + # Indicates a garage door. Garage doors must implement the ModeController interface to open and close the door. + GARAGE_DOOR = "GARAGE_DOOR" + + # Indicates a window covering on the inside of a structure. + INTERIOR_BLIND = "INTERIOR_BLIND" + + # Indicates a laptop or other mobile computer. + LAPTOP = "LAPTOP" + # Indicates light sources or fixtures. LIGHT = "LIGHT" # Indicates a microwave oven. MICROWAVE = "MICROWAVE" + # Indicates a mobile phone. + MOBILE_PHONE = "MOBILE_PHONE" + # Indicates an endpoint that detects and reports motion. MOTION_SENSOR = "MOTION_SENSOR" + # Indicates a network-connected music system. + MUSIC_SYSTEM = "MUSIC_SYSTEM" + # An endpoint that cannot be described in on of the other categories. OTHER = "OTHER" + # Indicates a network router. + NETWORK_HARDWARE = "NETWORK_HARDWARE" + + # Indicates an oven cooking appliance. + OVEN = "OVEN" + + # Indicates a non-mobile phone, such as landline or an IP phone. + PHONE = "PHONE" + # Describes a combination of devices set to a specific state, when the # order of the state change is not important. For example a bedtime scene # might include turning off lights and lowering the thermostat, but the # order is unimportant. Applies to Scenes SCENE_TRIGGER = "SCENE_TRIGGER" + # Indicates a projector screen. + SCREEN = "SCREEN" + # Indicates a security panel. SECURITY_PANEL = "SECURITY_PANEL" @@ -126,10 +162,16 @@ class DisplayCategory: # Indicates the endpoint is a speaker or speaker system. SPEAKER = "SPEAKER" + # Indicates a streaming device such as Apple TV, Chromecast, or Roku. + STREAMING_DEVICE = "STREAMING_DEVICE" + # Indicates in-wall switches wired to the electrical system. Can control a # variety of devices. SWITCH = "SWITCH" + # Indicates a tablet computer. + TABLET = "TABLET" + # Indicates endpoints that report the temperature only. TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR" @@ -140,6 +182,9 @@ class DisplayCategory: # Indicates the endpoint is a television. TV = "TV" + # Indicates a network-connected wearable device, such as an Apple Watch, Fitbit, or Samsung Gear. + WEARABLE = "WEARABLE" + class AlexaEntity: """An adaptation of an entity, expressed in Alexa's terms. @@ -318,20 +363,40 @@ class CoverCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) - if device_class in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_DOOR): + if device_class == cover.DEVICE_CLASS_GARAGE: + return [DisplayCategory.GARAGE_DOOR] + if device_class == cover.DEVICE_CLASS_DOOR: return [DisplayCategory.DOOR] + if device_class in ( + cover.DEVICE_CLASS_BLIND, + cover.DEVICE_CLASS_SHADE, + cover.DEVICE_CLASS_CURTAIN, + ): + return [DisplayCategory.INTERIOR_BLIND] + if device_class in ( + cover.DEVICE_CLASS_WINDOW, + cover.DEVICE_CLASS_AWNING, + cover.DEVICE_CLASS_SHUTTER, + ): + return [DisplayCategory.EXTERIOR_BLIND] + return [DisplayCategory.OTHER] def interfaces(self): """Yield the supported interfaces.""" - yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & cover.SUPPORT_SET_POSITION: - yield AlexaPercentageController(self.entity) - if supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN): + yield AlexaRangeController( + self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" + ) + elif supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN): yield AlexaModeController( self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" ) + if supported & cover.SUPPORT_SET_TILT_POSITION: + yield AlexaRangeController( + self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}" + ) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) @@ -355,6 +420,7 @@ def interfaces(self): yield AlexaColorController(self.entity) if supported & light.SUPPORT_COLOR_TEMP: yield AlexaColorTemperatureController(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) @@ -370,6 +436,7 @@ def default_display_categories(self): def interfaces(self): """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & fan.SUPPORT_SET_SPEED: yield AlexaPercentageController(self.entity) @@ -377,7 +444,6 @@ def interfaces(self): yield AlexaRangeController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}" ) - if supported & fan.SUPPORT_OSCILLATE: yield AlexaToggleController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index efb4f59514dc79..b5603af7402424 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -20,6 +20,7 @@ SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, @@ -28,8 +29,6 @@ SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_ALARM_DISARMED, - STATE_CLOSED, - STATE_OPEN, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -113,9 +112,7 @@ async def async_api_turn_on(hass, config, directive, context): domain = ha.DOMAIN service = SERVICE_TURN_ON - if domain == cover.DOMAIN: - service = cover.SERVICE_OPEN_COVER - elif domain == media_player.DOMAIN: + if domain == media_player.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF if not supported & power_features: @@ -141,9 +138,7 @@ async def async_api_turn_off(hass, config, directive, context): domain = ha.DOMAIN service = SERVICE_TURN_OFF - if entity.domain == cover.DOMAIN: - service = cover.SERVICE_CLOSE_COVER - elif domain == media_player.DOMAIN: + if domain == media_player.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF if not supported & power_features: @@ -348,10 +343,6 @@ async def async_api_set_percentage(hass, config, directive, context): speed = "high" data[fan.ATTR_SPEED] = speed - elif entity.domain == cover.DOMAIN: - service = SERVICE_SET_COVER_POSITION - data[cover.ATTR_POSITION] = percentage - await hass.services.async_call( entity.domain, service, data, blocking=False, context=context ) @@ -385,13 +376,6 @@ async def async_api_adjust_percentage(hass, config, directive, context): data[fan.ATTR_SPEED] = speed - elif entity.domain == cover.DOMAIN: - service = SERVICE_SET_COVER_POSITION - - current = entity.attributes.get(cover.ATTR_POSITION) - - data[cover.ATTR_POSITION] = max(0, percentage_delta + current) - await hass.services.async_call( entity.domain, service, data, blocking=False, context=context ) @@ -960,32 +944,35 @@ async def async_api_disarm(hass, config, directive, context): @HANDLERS.register(("Alexa.ModeController", "SetMode")) async def async_api_set_mode(hass, config, directive, context): - """Process a next request.""" + """Process a SetMode directive.""" entity = directive.entity instance = directive.instance domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - capability_mode = directive.payload["mode"] - - if domain not in (fan.DOMAIN, cover.DOMAIN): - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + mode = directive.payload["mode"] + # Fan Direction if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - _, direction = capability_mode.split(".") + _, direction = mode.split(".") if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD): service = fan.SERVICE_SET_DIRECTION data[fan.ATTR_DIRECTION] = direction - if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": - _, position = capability_mode.split(".") + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + _, position = mode.split(".") - if position == STATE_CLOSED: + if position == cover.STATE_CLOSED: service = cover.SERVICE_CLOSE_COVER - - if position == STATE_OPEN: + elif position == cover.STATE_OPEN: service = cover.SERVICE_OPEN_COVER + elif position == "custom": + service = cover.SERVICE_STOP_COVER + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -997,7 +984,7 @@ async def async_api_set_mode(hass, config, directive, context): "namespace": "Alexa.ModeController", "instance": instance, "name": "mode", - "value": capability_mode, + "value": mode, } ) @@ -1008,24 +995,13 @@ async def async_api_set_mode(hass, config, directive, context): async def async_api_adjust_mode(hass, config, directive, context): """Process a AdjustMode request. - Requires modeResources to be ordered. - Only modes that are ordered support the adjustMode directive. + Requires capabilityResources supportedModes to be ordered. + Only supportedModes with ordered=True support the adjustMode directive. """ - entity = directive.entity - instance = directive.instance - domain = entity.domain - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - - if instance is None: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - - # No modeResources are currently ordered to support this request. - - return directive.response() + # Currently no supportedModes are configured with ordered=True to support this request. + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) @HANDLERS.register(("Alexa.ToggleController", "TurnOn")) @@ -1037,19 +1013,29 @@ async def async_api_toggle_on(hass, config, directive, context): service = None data = {ATTR_ENTITY_ID: entity.entity_id} - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - + # Fan Oscillating if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": service = fan.SERVICE_OSCILLATE data[fan.ATTR_OSCILLATING] = True + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ToggleController", + "instance": instance, + "name": "toggleState", + "value": "ON", + } + ) + + return response @HANDLERS.register(("Alexa.ToggleController", "TurnOff")) @@ -1061,19 +1047,29 @@ async def async_api_toggle_off(hass, config, directive, context): service = None data = {ATTR_ENTITY_ID: entity.entity_id} - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - + # Fan Oscillating if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": service = fan.SERVICE_OSCILLATE data[fan.ATTR_OSCILLATING] = False + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ToggleController", + "instance": instance, + "name": "toggleState", + "value": "OFF", + } + ) + + return response @HANDLERS.register(("Alexa.RangeController", "SetRangeValue")) @@ -1086,10 +1082,7 @@ async def async_api_set_range(hass, config, directive, context): data = {ATTR_ENTITY_ID: entity.entity_id} range_value = int(directive.payload["rangeValue"]) - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - + # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": service = fan.SERVICE_SET_SPEED speed = SPEED_FAN_MAP.get(range_value, None) @@ -1103,11 +1096,45 @@ async def async_api_set_range(hass, config, directive, context): data[fan.ATTR_SPEED] = speed + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + if range_value == 0: + service = cover.SERVICE_CLOSE_COVER + elif range_value == 100: + service = cover.SERVICE_OPEN_COVER + else: + service = cover.SERVICE_SET_COVER_POSITION + data[cover.ATTR_POSITION] = range_value + + # Cover Tilt Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + if range_value == 0: + service = cover.SERVICE_CLOSE_COVER_TILT + elif range_value == 100: + service = cover.SERVICE_OPEN_COVER_TILT + else: + service = cover.SERVICE_SET_COVER_TILT_POSITION + data[cover.ATTR_POSITION] = range_value + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.RangeController", + "instance": instance, + "name": "rangeValue", + "value": range_value, + } + ) + + return response @HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue")) @@ -1119,24 +1146,56 @@ async def async_api_adjust_range(hass, config, directive, context): service = None data = {ATTR_ENTITY_ID: entity.entity_id} range_delta = int(directive.payload["rangeValueDelta"]) + response_value = 0 + # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": service = fan.SERVICE_SET_SPEED - - # adjust range current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0) - speed = SPEED_FAN_MAP.get(max(0, range_delta + current_range), fan.SPEED_OFF) + speed = SPEED_FAN_MAP.get( + min(3, max(0, range_delta + current_range)), fan.SPEED_OFF + ) if speed == fan.SPEED_OFF: service = fan.SERVICE_TURN_OFF - data[fan.ATTR_SPEED] = speed + data[fan.ATTR_SPEED] = response_value = speed + + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + service = SERVICE_SET_COVER_POSITION + current = entity.attributes.get(cover.ATTR_POSITION) + data[cover.ATTR_POSITION] = response_value = min( + 100, max(0, range_delta + current) + ) + + # Cover Tilt Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + service = SERVICE_SET_COVER_TILT_POSITION + current = entity.attributes.get(cover.ATTR_TILT_POSITION) + data[cover.ATTR_TILT_POSITION] = response_value = min( + 100, max(0, range_delta + current) + ) + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.RangeController", + "instance": instance, + "name": "rangeValue", + "value": response_value, + } + ) + + return response @HANDLERS.register(("Alexa.ChannelController", "ChangeChannel")) diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py new file mode 100644 index 00000000000000..061005252dc739 --- /dev/null +++ b/homeassistant/components/alexa/resources.py @@ -0,0 +1,387 @@ +"""Alexa Resources and Assets.""" + + +class AlexaGlobalCatalog: + """The Global Alexa catalog. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog + + You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units. + This catalog is localized into all the languages that Alexa supports. + + You can reference the following catalog of pre-defined friendly names. + Each item in the following list is an asset identifier followed by its supported friendly names. + The first friendly name for each identifier is the one displayed in the Alexa mobile app. + """ + + # Air Purifier, Air Cleaner,Clean Air Machine + DEVICE_NAME_AIR_PURIFIER = "Alexa.DeviceName.AirPurifier" + + # Fan, Blower + DEVICE_NAME_FAN = "Alexa.DeviceName.Fan" + + # Router, Internet Router, Network Router, Wifi Router, Net Router + DEVICE_NAME_ROUTER = "Alexa.DeviceName.Router" + + # Shade, Blind, Curtain, Roller, Shutter, Drape, Awning, Window shade, Interior blind + DEVICE_NAME_SHADE = "Alexa.DeviceName.Shade" + + # Shower + DEVICE_NAME_SHOWER = "Alexa.DeviceName.Shower" + + # Space Heater, Portable Heater + DEVICE_NAME_SPACE_HEATER = "Alexa.DeviceName.SpaceHeater" + + # Washer, Washing Machine + DEVICE_NAME_WASHER = "Alexa.DeviceName.Washer" + + # 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi + SETTING_2G_GUEST_WIFI = "Alexa.Setting.2GGuestWiFi" + + # 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi + SETTING_5G_GUEST_WIFI = "Alexa.Setting.5GGuestWiFi" + + # Auto, Automatic, Automatic Mode, Auto Mode + SETTING_AUTO = "Alexa.Setting.Auto" + + # Direction + SETTING_DIRECTION = "Alexa.Setting.Direction" + + # Dry Cycle, Dry Preset, Dry Setting, Dryer Cycle, Dryer Preset, Dryer Setting + SETTING_DRY_CYCLE = "Alexa.Setting.DryCycle" + + # Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity + SETTING_FAN_SPEED = "Alexa.Setting.FanSpeed" + + # Guest Wi-fi, Guest Network, Guest Net + SETTING_GUEST_WIFI = "Alexa.Setting.GuestWiFi" + + # Heat + SETTING_HEAT = "Alexa.Setting.Heat" + + # Mode + SETTING_MODE = "Alexa.Setting.Mode" + + # Night, Night Mode + SETTING_NIGHT = "Alexa.Setting.Night" + + # Opening, Height, Lift, Width + SETTING_OPENING = "Alexa.Setting.Opening" + + # Oscillate, Swivel, Oscillation, Spin, Back and forth + SETTING_OSCILLATE = "Alexa.Setting.Oscillate" + + # Preset, Setting + SETTING_PRESET = "Alexa.Setting.Preset" + + # Quiet, Quiet Mode, Noiseless, Silent + SETTING_QUIET = "Alexa.Setting.Quiet" + + # Temperature, Temp + SETTING_TEMPERATURE = "Alexa.Setting.Temperature" + + # Wash Cycle, Wash Preset, Wash setting + SETTING_WASH_CYCLE = "Alexa.Setting.WashCycle" + + # Water Temperature, Water Temp, Water Heat + SETTING_WATER_TEMPERATURE = "Alexa.Setting.WaterTemperature" + + # Handheld Shower, Shower Wand, Hand Shower + SHOWER_HAND_HELD = "Alexa.Shower.HandHeld" + + # Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet + SHOWER_RAIN_HEAD = "Alexa.Shower.RainHead" + + # Degrees, Degree + UNIT_ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees" + + # Radians, Radian + UNIT_ANGLE_RADIANS = "Alexa.Unit.Angle.Radians" + + # Feet, Foot + UNIT_DISTANCE_FEET = "Alexa.Unit.Distance.Feet" + + # Inches, Inch + UNIT_DISTANCE_INCHES = "Alexa.Unit.Distance.Inches" + + # Kilometers + UNIT_DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers" + + # Meters, Meter, m + UNIT_DISTANCE_METERS = "Alexa.Unit.Distance.Meters" + + # Miles, Mile + UNIT_DISTANCE_MILES = "Alexa.Unit.Distance.Miles" + + # Yards, Yard + UNIT_DISTANCE_YARDS = "Alexa.Unit.Distance.Yards" + + # Grams, Gram, g + UNIT_MASS_GRAMS = "Alexa.Unit.Mass.Grams" + + # Kilograms, Kilogram, kg + UNIT_MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms" + + # Percent + UNIT_PERCENT = "Alexa.Unit.Percent" + + # Celsius, Degrees Celsius, Degrees, C, Centigrade, Degrees Centigrade + UNIT_TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius" + + # Degrees, Degree + UNIT_TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees" + + # Fahrenheit, Degrees Fahrenheit, Degrees F, Degrees, F + UNIT_TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit" + + # Kelvin, Degrees Kelvin, Degrees K, Degrees, K + UNIT_TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin" + + # Cubic Feet, Cubic Foot + UNIT_VOLUME_CUBIC_FEET = "Alexa.Unit.Volume.CubicFeet" + + # Cubic Meters, Cubic Meter, Meters Cubed + UNIT_VOLUME_CUBIC_METERS = "Alexa.Unit.Volume.CubicMeters" + + # Gallons, Gallon + UNIT_VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons" + + # Liters, Liter, L + UNIT_VOLUME_LITERS = "Alexa.Unit.Volume.Liters" + + # Pints, Pint + UNIT_VOLUME_PINTS = "Alexa.Unit.Volume.Pints" + + # Quarts, Quart + UNIT_VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts" + + # Ounces, Ounce, oz + UNIT_WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" + + # Pounds, Pound, lbs + UNIT_WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" + + # Close + VALUE_CLOSE = "Alexa.Value.Close" + + # Delicates, Delicate + VALUE_DELICATE = "Alexa.Value.Delicate" + + # High + VALUE_HIGH = "Alexa.Value.High" + + # Low + VALUE_LOW = "Alexa.Value.Low" + + # Maximum, Max + VALUE_MAXIMUM = "Alexa.Value.Maximum" + + # Medium, Mid + VALUE_MEDIUM = "Alexa.Value.Medium" + + # Minimum, Min + VALUE_MINIMUM = "Alexa.Value.Minimum" + + # Open + VALUE_OPEN = "Alexa.Value.Open" + + # Quick Wash, Fast Wash, Wash Quickly, Speed Wash + VALUE_QUICK_WASH = "Alexa.Value.QuickWash" + + +class AlexaCapabilityResource: + """Base class for Alexa capabilityResources, ModeResources, and presetResources objects. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources + """ + + def __init__(self, labels): + """Initialize an Alexa resource.""" + self._resource_labels = [] + for label in labels: + self._resource_labels.append(label) + + def serialize_capability_resources(self): + """Return capabilityResources object serialized for an API response.""" + return self.serialize_labels(self._resource_labels) + + @staticmethod + def serialize_configuration(): + """Return ModeResources, PresetResources friendlyNames serialized for an API response.""" + return [] + + @staticmethod + def serialize_labels(resources): + """Return resource label objects for friendlyNames serialized for an API response.""" + labels = [] + for label in resources: + if label in AlexaGlobalCatalog.__dict__.values(): + label = {"@type": "asset", "value": {"assetId": label}} + else: + label = {"@type": "text", "value": {"text": label, "locale": "en-US"}} + + labels.append(label) + + return {"friendlyNames": labels} + + +class AlexaModeResource(AlexaCapabilityResource): + """Implements Alexa ModeResources. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources + """ + + def __init__(self, labels, ordered=False): + """Initialize an Alexa modeResource.""" + super().__init__(labels) + self._supported_modes = [] + self._mode_ordered = ordered + + def add_mode(self, value, labels): + """Add mode to the supportedModes object.""" + self._supported_modes.append({"value": value, "labels": labels}) + + def serialize_configuration(self): + """Return configuration for ModeResources friendlyNames serialized for an API response.""" + mode_resources = [] + for mode in self._supported_modes: + result = { + "value": mode["value"], + "modeResources": self.serialize_labels(mode["labels"]), + } + mode_resources.append(result) + + return {"ordered": self._mode_ordered, "supportedModes": mode_resources} + + +class AlexaPresetResource(AlexaCapabilityResource): + """Implements Alexa PresetResources. + + Use presetResources with RangeController to provide a set of friendlyNames for each RangeController preset. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources + """ + + def __init__(self, labels, min_value, max_value, precision, unit=None): + """Initialize an Alexa presetResource.""" + super().__init__(labels) + self._presets = [] + self._minimum_value = int(min_value) + self._maximum_value = int(max_value) + self._precision = int(precision) + self._unit_of_measure = None + if unit in AlexaGlobalCatalog.__dict__.values(): + self._unit_of_measure = unit + + def add_preset(self, value, labels): + """Add preset to configuration presets array.""" + self._presets.append({"value": value, "labels": labels}) + + def serialize_configuration(self): + """Return configuration for PresetResources friendlyNames serialized for an API response.""" + configuration = { + "supportedRange": { + "minimumValue": self._minimum_value, + "maximumValue": self._maximum_value, + "precision": self._precision, + } + } + + if self._unit_of_measure: + configuration["unitOfMeasure"] = self._unit_of_measure + + if self._presets: + preset_resources = [] + for preset in self._presets: + preset_resources.append( + { + "rangeValue": preset["value"], + "presetResources": self.serialize_labels(preset["labels"]), + } + ) + configuration["presets"] = preset_resources + + return configuration + + +class AlexaSemantics: + """Class for Alexa Semantics Object. + + You can optionally enable additional utterances by using semantics. When you use semantics, + you manually map the phrases "open", "close", "raise", and "lower" to directives. + + Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController. + + https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object + """ + + MAPPINGS_ACTION = "actionMappings" + MAPPINGS_STATE = "stateMappings" + + ACTIONS_TO_DIRECTIVE = "ActionsToDirective" + STATES_TO_VALUE = "StatesToValue" + STATES_TO_RANGE = "StatesToRange" + + ACTION_CLOSE = "Alexa.Actions.Close" + ACTION_LOWER = "Alexa.Actions.Lower" + ACTION_OPEN = "Alexa.Actions.Open" + ACTION_RAISE = "Alexa.Actions.Raise" + + STATES_OPEN = "Alexa.States.Open" + STATES_CLOSED = "Alexa.States.Closed" + + DIRECTIVE_RANGE_SET_VALUE = "SetRangeValue" + DIRECTIVE_RANGE_ADJUST_VALUE = "AdjustRangeValue" + DIRECTIVE_TOGGLE_TURN_ON = "TurnOn" + DIRECTIVE_TOGGLE_TURN_OFF = "TurnOff" + DIRECTIVE_MODE_SET_MODE = "SetMode" + DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode" + + def __init__(self): + """Initialize an Alexa modeResource.""" + self._action_mappings = [] + self._state_mappings = [] + + def _add_action_mapping(self, semantics): + """Add action mapping between actions and interface directives.""" + self._action_mappings.append(semantics) + + def _add_state_mapping(self, semantics): + """Add state mapping between states and interface directives.""" + self._state_mappings.append(semantics) + + def add_states_to_value(self, states, value): + """Add StatesToValue stateMappings.""" + self._add_state_mapping( + {"@type": self.STATES_TO_VALUE, "states": states, "value": value} + ) + + def add_states_to_range(self, states, min_value, max_value): + """Add StatesToRange stateMappings.""" + self._add_state_mapping( + { + "@type": self.STATES_TO_RANGE, + "states": states, + "range": {"minimumValue": min_value, "maximumValue": max_value}, + } + ) + + def add_action_to_directive(self, actions, directive, payload): + """Add ActionsToDirective actionMappings.""" + self._add_action_mapping( + { + "@type": self.ACTIONS_TO_DIRECTIVE, + "actions": actions, + "directive": {"name": directive, "payload": payload}, + } + ) + + def serialize_semantics(self): + """Return semantics object serialized for an API response.""" + semantics = {} + if self._action_mappings: + semantics[self.MAPPINGS_ACTION] = self._action_mappings + if self._state_mappings: + semantics[self.MAPPINGS_STATE] = self._state_mappings + + return semantics diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index ab9c375103a9ab..9c086e1fc50476 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -411,14 +411,14 @@ async def test_report_fan_direction(hass): properties.assert_not_has_property("Alexa.ModeController", "mode") properties = await reported_properties(hass, "fan.reverse") - properties.assert_equal("Alexa.ModeController", "mode", "reverse") + properties.assert_equal("Alexa.ModeController", "mode", "direction.reverse") properties = await reported_properties(hass, "fan.forward") - properties.assert_equal("Alexa.ModeController", "mode", "forward") + properties.assert_equal("Alexa.ModeController", "mode", "direction.forward") -async def test_report_cover_percentage_state(hass): - """Test PercentageController reports cover percentage correctly.""" +async def test_report_cover_range_value(hass): + """Test RangeController reports cover position correctly.""" hass.states.async_set( "cover.fully_open", "open", @@ -448,13 +448,13 @@ async def test_report_cover_percentage_state(hass): ) properties = await reported_properties(hass, "cover.fully_open") - properties.assert_equal("Alexa.PercentageController", "percentage", 100) + properties.assert_equal("Alexa.RangeController", "rangeValue", 100) properties = await reported_properties(hass, "cover.half_open") - properties.assert_equal("Alexa.PercentageController", "percentage", 50) + properties.assert_equal("Alexa.RangeController", "rangeValue", 50) properties = await reported_properties(hass, "cover.closed") - properties.assert_equal("Alexa.PercentageController", "percentage", 0) + properties.assert_equal("Alexa.RangeController", "rangeValue", 0) async def test_report_climate_state(hass): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 25c8f2a864fe22..4187c4a2c4f044 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -126,10 +126,12 @@ async def discovery_test(device, hass, expected_endpoints=1): return None -def get_capability(capabilities, capability_name): +def get_capability(capabilities, capability_name, instance=None): """Search a set of capabilities for a specific one.""" for capability in capabilities: - if capability["interface"] == capability_name: + if instance and capability["instance"] == instance: + return capability + elif capability["interface"] == capability_name: return capability return None @@ -420,9 +422,29 @@ async def test_variable_fan(hass): ) assert call.data["speed"] == "medium" + call, _ = await assert_request_calls_service( + "Alexa.PercentageController", + "SetPercentage", + "fan#test_2", + "fan.set_speed", + hass, + payload={"percentage": "33"}, + ) + assert call.data["speed"] == "low" + + call, _ = await assert_request_calls_service( + "Alexa.PercentageController", + "SetPercentage", + "fan#test_2", + "fan.set_speed", + hass, + payload={"percentage": "100"}, + ) + assert call.data["speed"] == "high" + await assert_percentage_changes( hass, - [("high", "-5"), ("off", "5"), ("low", "-80")], + [("high", "-5"), ("off", "5"), ("low", "-80"), ("medium", "-34")], "Alexa.PercentageController", "AdjustPercentage", "fan#test_2", @@ -431,6 +453,16 @@ async def test_variable_fan(hass): "speed", ) + call, _ = await assert_request_calls_service( + "Alexa.PowerLevelController", + "SetPowerLevel", + "fan#test_2", + "fan.set_speed", + hass, + payload={"powerLevel": "20"}, + ) + assert call.data["speed"] == "low" + call, _ = await assert_request_calls_service( "Alexa.PowerLevelController", "SetPowerLevel", @@ -441,6 +473,16 @@ async def test_variable_fan(hass): ) assert call.data["speed"] == "medium" + call, _ = await assert_request_calls_service( + "Alexa.PowerLevelController", + "SetPowerLevel", + "fan#test_2", + "fan.set_speed", + hass, + payload={"powerLevel": "99"}, + ) + assert call.data["speed"] == "high" + await assert_percentage_changes( hass, [("high", "-5"), ("medium", "-50"), ("low", "-80")], @@ -1333,51 +1375,106 @@ async def test_group(hass): ) -async def test_cover(hass): - """Test cover discovery.""" +async def test_cover_position_range(hass): + """Test cover discovery and position using rangeController.""" device = ( - "cover.test", - "off", - {"friendly_name": "Test cover", "supported_features": 255, "position": 30}, + "cover.test_range", + "open", + { + "friendly_name": "Test cover range", + "device_class": "blind", + "supported_features": 7, + "position": 30, + }, ) appliance = await discovery_test(device, hass) - assert appliance["endpointId"] == "cover#test" - assert appliance["displayCategories"][0] == "OTHER" - assert appliance["friendlyName"] == "Test cover" + assert appliance["endpointId"] == "cover#test_range" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover range" - assert_endpoint_capabilities( - appliance, - "Alexa.ModeController", - "Alexa.PercentageController", - "Alexa.PowerController", - "Alexa.EndpointHealth", - "Alexa", + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" ) - await assert_power_controller_works( - "cover#test", "cover.open_cover", "cover.close_cover", hass - ) + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "cover.position" + + properties = range_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "rangeValue"} in properties["supported"] + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Position", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Opening"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + assert configuration["unitOfMeasure"] == "Alexa.Unit.Percent" + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == 0 + assert supported_range["maximumValue"] == 100 + assert supported_range["precision"] == 1 call, _ = await assert_request_calls_service( - "Alexa.PercentageController", - "SetPercentage", - "cover#test", + "Alexa.RangeController", + "SetRangeValue", + "cover#test_range", "cover.set_cover_position", hass, - payload={"percentage": "50"}, + payload={"rangeValue": "50"}, + instance="cover.position", ) assert call.data["position"] == 50 - await assert_percentage_changes( + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_range", + "cover.close_cover", hass, - [(25, "-5"), (35, "5"), (0, "-80")], - "Alexa.PercentageController", - "AdjustPercentage", - "cover#test", - "percentageDelta", + payload={"rangeValue": "0"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 0 + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_range", + "cover.open_cover", + hass, + payload={"rangeValue": "100"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 100 + + await assert_range_changes( + hass, + [(25, "-5"), (35, "5"), (0, "-99"), (100, "99")], + "Alexa.RangeController", + "AdjustRangeValue", + "cover#test_range", + False, "cover.set_cover_position", "position", + instance="cover.position", ) @@ -2292,26 +2389,25 @@ async def test_mode_unsupported_domain(hass): assert msg["payload"]["type"] == "INVALID_DIRECTIVE" -async def test_cover_position(hass): - """Test cover position mode discovery.""" +async def test_cover_position_mode(hass): + """Test cover discovery and position using modeController.""" device = ( - "cover.test", - "off", - {"friendly_name": "Test cover", "supported_features": 255, "position": 30}, + "cover.test_mode", + "open", + { + "friendly_name": "Test cover mode", + "device_class": "blind", + "supported_features": 3, + }, ) appliance = await discovery_test(device, hass) - assert appliance["endpointId"] == "cover#test" - assert appliance["displayCategories"][0] == "OTHER" - assert appliance["friendlyName"] == "Test cover" + assert appliance["endpointId"] == "cover#test_mode" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover mode" capabilities = assert_endpoint_capabilities( - appliance, - "Alexa", - "Alexa.ModeController", - "Alexa.PercentageController", - "Alexa.PowerController", - "Alexa.EndpointHealth", + appliance, "Alexa", "Alexa.ModeController", "Alexa.EndpointHealth" ) mode_capability = get_capability(capabilities, "Alexa.ModeController") @@ -2324,9 +2420,14 @@ async def test_cover_position(hass): capability_resources = mode_capability["capabilityResources"] assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Position", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + assert { "@type": "asset", - "value": {"assetId": "Alexa.Setting.Mode"}, + "value": {"assetId": "Alexa.Setting.Opening"}, } in capability_resources["friendlyNames"] configuration = mode_capability["configuration"] @@ -2339,10 +2440,7 @@ async def test_cover_position(hass): "value": "position.open", "modeResources": { "friendlyNames": [ - {"@type": "text", "value": {"text": "open", "locale": "en-US"}}, - {"@type": "text", "value": {"text": "opened", "locale": "en-US"}}, - {"@type": "text", "value": {"text": "raise", "locale": "en-US"}}, - {"@type": "text", "value": {"text": "raised", "locale": "en-US"}}, + {"@type": "asset", "value": {"assetId": "Alexa.Value.Open"}} ] }, } in supported_modes @@ -2350,19 +2448,24 @@ async def test_cover_position(hass): "value": "position.closed", "modeResources": { "friendlyNames": [ - {"@type": "text", "value": {"text": "close", "locale": "en-US"}}, - {"@type": "text", "value": {"text": "closed", "locale": "en-US"}}, - {"@type": "text", "value": {"text": "shut", "locale": "en-US"}}, - {"@type": "text", "value": {"text": "lower", "locale": "en-US"}}, - {"@type": "text", "value": {"text": "lowered", "locale": "en-US"}}, + {"@type": "asset", "value": {"assetId": "Alexa.Value.Close"}} ] }, } in supported_modes + semantics = mode_capability["semantics"] + assert semantics is not None + + action_mappings = semantics["actionMappings"] + assert action_mappings is not None + + state_mappings = semantics["stateMappings"] + assert state_mappings is not None + call, msg = await assert_request_calls_service( "Alexa.ModeController", "SetMode", - "cover#test", + "cover#test_mode", "cover.close_cover", hass, payload={"mode": "position.closed"}, @@ -2376,7 +2479,7 @@ async def test_cover_position(hass): call, msg = await assert_request_calls_service( "Alexa.ModeController", "SetMode", - "cover#test", + "cover#test_mode", "cover.open_cover", hass, payload={"mode": "position.open"}, @@ -2387,6 +2490,20 @@ async def test_cover_position(hass): assert properties["namespace"] == "Alexa.ModeController" assert properties["value"] == "position.open" + call, msg = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "cover#test_mode", + "cover.stop_cover", + hass, + payload={"mode": "position.custom"}, + instance="cover.position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "mode" + assert properties["namespace"] == "Alexa.ModeController" + assert properties["value"] == "position.custom" + async def test_image_processing(hass): """Test image_processing discovery as event detection.""" @@ -2467,3 +2584,159 @@ async def test_presence_sensor(hass): assert properties["proactivelyReported"] is True assert not properties["retrievable"] assert {"name": "humanPresenceDetectionState"} in properties["supported"] + + +async def test_cover_tilt_position_range(hass): + """Test cover discovery and tilt position using rangeController.""" + device = ( + "cover.test_tilt_range", + "open", + { + "friendly_name": "Test cover tilt range", + "device_class": "blind", + "supported_features": 240, + "tilt_position": 30, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_tilt_range" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover tilt range" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + ) + + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "cover.tilt_position" + + semantics = range_capability["semantics"] + assert semantics is not None + + action_mappings = semantics["actionMappings"] + assert action_mappings is not None + + state_mappings = semantics["stateMappings"] + assert state_mappings is not None + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_tilt_range", + "cover.set_cover_tilt_position", + hass, + payload={"rangeValue": "50"}, + instance="cover.tilt_position", + ) + assert call.data["position"] == 50 + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_tilt_range", + "cover.close_cover_tilt", + hass, + payload={"rangeValue": "0"}, + instance="cover.tilt_position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 0 + + call, msg = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "cover#test_tilt_range", + "cover.open_cover_tilt", + hass, + payload={"rangeValue": "100"}, + instance="cover.tilt_position", + ) + properties = msg["context"]["properties"][0] + assert properties["name"] == "rangeValue" + assert properties["namespace"] == "Alexa.RangeController" + assert properties["value"] == 100 + + await assert_range_changes( + hass, + [(25, "-5"), (35, "5"), (0, "-99"), (100, "99")], + "Alexa.RangeController", + "AdjustRangeValue", + "cover#test_tilt_range", + False, + "cover.set_cover_tilt_position", + "tilt_position", + instance="cover.tilt_position", + ) + + +async def test_cover_semantics(hass): + """Test cover discovery and semantics.""" + device = ( + "cover.test_semantics", + "open", + { + "friendly_name": "Test cover semantics", + "device_class": "blind", + "supported_features": 255, + "position": 30, + "tilt_position": 30, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_semantics" + assert appliance["displayCategories"][0] == "INTERIOR_BLIND" + assert appliance["friendlyName"] == "Test cover semantics" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + ) + + for range_instance in ("cover.position", "cover.tilt_position"): + range_capability = get_capability( + capabilities, "Alexa.RangeController", range_instance + ) + semantics = range_capability["semantics"] + assert semantics is not None + + action_mappings = semantics["actionMappings"] + assert action_mappings is not None + if range_instance == "cover.position": + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Lower"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Raise"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in action_mappings + elif range_instance == "cover.position": + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Close"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 0}}, + } in action_mappings + assert { + "@type": "ActionsToDirective", + "actions": ["Alexa.Actions.Open"], + "directive": {"name": "SetRangeValue", "payload": {"rangeValue": 100}}, + } in action_mappings + + state_mappings = semantics["stateMappings"] + assert state_mappings is not None + assert { + "@type": "StatesToValue", + "states": ["Alexa.States.Closed"], + "value": 0, + } in state_mappings + assert { + "@type": "StatesToRange", + "states": ["Alexa.States.Open"], + "range": {"minimumValue": 1, "maximumValue": 100}, + } in state_mappings From 52818bdb891387e4b9eaa30ec0540e532c7cb78e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Dec 2019 14:00:22 +0100 Subject: [PATCH 2483/3953] Make Hassfest stricter pt 2 (#30068) * Make Hassfest stricter * Fix if-condition * Small cleanup --- .../components/modbus/binary_sensor.py | 3 +- homeassistant/components/modbus/switch.py | 2 +- homeassistant/components/mqtt/climate.py | 10 +- homeassistant/components/tuya/climate.py | 6 +- homeassistant/components/zamg/sensor.py | 21 +-- script/hassfest/dependencies.py | 151 ++++++++++++------ tests/hassfest/test_dependencies.py | 17 +- 7 files changed, 130 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index cbefd1271d0c60..faf5160d7e5d79 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -3,8 +3,7 @@ import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_NAME, CONF_SLAVE from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 43ef649f788007..eba0c754f454e7 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -3,7 +3,7 @@ import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index a51590e0b59cf1..f9d7d8752f270a 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -14,6 +14,10 @@ ATTR_TARGET_TEMP_LOW, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -29,7 +33,6 @@ SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ( ATTR_TEMPERATURE, CONF_DEVICE, @@ -165,8 +168,7 @@ vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( - CONF_FAN_MODE_LIST, - default=[HVAC_MODE_AUTO, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], + CONF_FAN_MODE_LIST, default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], ): cv.ensure_list, vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -339,7 +341,7 @@ def _setup_from_config(self, config): self._target_temp_high = config[CONF_TEMP_INITIAL] if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: - self._current_fan_mode = SPEED_LOW + self._current_fan_mode = FAN_LOW if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: self._current_swing_mode = HVAC_MODE_OFF if self._topic[CONF_MODE_STATE_TOPIC] is None: diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 6450920b806427..eb0ef5eca2f299 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -1,6 +1,9 @@ """Support for the Tuya climate devices.""" from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice from homeassistant.components.climate.const import ( + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY, @@ -9,7 +12,6 @@ SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_WHOLE, @@ -30,7 +32,7 @@ TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()} -FAN_MODES = {SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH} +FAN_MODES = {FAN_LOW, FAN_MEDIUM, FAN_HIGH} def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index c984b13ef4bab7..44c216eb1be951 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -11,15 +11,8 @@ import requests import voluptuous as vol -from homeassistant.components.weather import ( - ATTR_WEATHER_ATTRIBUTION, - ATTR_WEATHER_HUMIDITY, - ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_TEMPERATURE, - ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED, -) from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, @@ -43,15 +36,15 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) SENSOR_TYPES = { - ATTR_WEATHER_PRESSURE: ("Pressure", "hPa", "LDstat hPa", float), + "pressure": ("Pressure", "hPa", "LDstat hPa", float), "pressure_sealevel": ("Pressure at Sea Level", "hPa", "LDred hPa", float), - ATTR_WEATHER_HUMIDITY: ("Humidity", "%", "RF %", int), - ATTR_WEATHER_WIND_SPEED: ("Wind Speed", "km/h", "WG km/h", float), - ATTR_WEATHER_WIND_BEARING: ("Wind Bearing", "°", "WR °", int), + "humidity": ("Humidity", "%", "RF %", int), + "wind_speed": ("Wind Speed", "km/h", "WG km/h", float), + "wind_bearing": ("Wind Bearing", "°", "WR °", int), "wind_max_speed": ("Top Wind Speed", "km/h", "WSG km/h", float), "wind_max_bearing": ("Top Wind Bearing", "°", "WSR °", int), "sun_last_hour": ("Sun Last Hour", "%", "SO %", int), - ATTR_WEATHER_TEMPERATURE: ("Temperature", "°C", "T °C", float), + "temperature": ("Temperature", "°C", "T °C", float), "precipitation": ("Precipitation", "l/m²", "N l/m²", float), "dewpoint": ("Dew Point", "°C", "TP °C", float), # The following probably not useful for general consumption, @@ -140,7 +133,7 @@ def unit_of_measurement(self): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_WEATHER_ATTRIBUTION: ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_STATION: self.probe.get_data("station_name"), ATTR_UPDATED: self.probe.last_update.isoformat(), } diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index c67779d3c33ec5..cb58de3af768d5 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -1,5 +1,6 @@ """Validate dependencies.""" import ast +from pathlib import Path from typing import Dict, Set from homeassistant.requirements import DISCOVERY_INTEGRATIONS @@ -13,21 +14,25 @@ class ImportCollector(ast.NodeVisitor): def __init__(self, integration: Integration): """Initialize the import collector.""" self.integration = integration - self.referenced: Set[str] = set() + self.referenced: Dict[Path, Set[str]] = {} - def maybe_add_reference(self, reference_domain: str): - """Add a reference.""" - if ( - # If it's importing something from itself - reference_domain == self.integration.path.name - # Platform file - or (self.integration.path / f"{reference_domain}.py").exists() - # Platform dir - or (self.integration.path / reference_domain).exists() - ): - return + # Current file or dir we're inspecting + self._cur_fil_dir = None - self.referenced.add(reference_domain) + def collect(self) -> None: + """Collect imports from a source file.""" + for fil in self.integration.path.glob("**/*.py"): + if not fil.is_file(): + continue + + self._cur_fil_dir = fil.relative_to(self.integration.path) + self.referenced[self._cur_fil_dir] = set() + self.visit(ast.parse(fil.read_text())) + self._cur_fil_dir = None + + def _add_reference(self, reference_domain: str): + """Add a reference.""" + self.referenced[self._cur_fil_dir].add(reference_domain) def visit_ImportFrom(self, node): """Visit ImportFrom node.""" @@ -37,19 +42,19 @@ def visit_ImportFrom(self, node): if node.module.startswith("homeassistant.components."): # from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME # from homeassistant.components.logbook import bla - self.maybe_add_reference(node.module.split(".")[2]) + self._add_reference(node.module.split(".")[2]) elif node.module == "homeassistant.components": # from homeassistant.components import sun for name_node in node.names: - self.maybe_add_reference(name_node.name) + self._add_reference(name_node.name) def visit_Import(self, node): """Visit Import node.""" # import homeassistant.components.hue as hue for name_node in node.names: if name_node.name.startswith("homeassistant.components."): - self.maybe_add_reference(name_node.name.split(".")[2]) + self._add_reference(name_node.name.split(".")[2]) def visit_Attribute(self, node): """Visit Attribute node.""" @@ -77,7 +82,7 @@ def visit_Attribute(self, node): ) ) ): - self.maybe_add_reference(node.attr) + self._add_reference(node.attr) else: # Have it visit other kids self.generic_visit(node) @@ -133,45 +138,93 @@ def visit_Attribute(self, node): ] -def validate_dependencies(integration: Integration): - """Validate all dependencies.""" - # Find usage of hass.components - collector = ImportCollector(integration) - - for fil in integration.path.glob("**/*.py"): - if not fil.is_file(): - continue - - collector.visit(ast.parse(fil.read_text())) - - referenced = ( - collector.referenced - - ALLOWED_USED_COMPONENTS - - set(integration.manifest["dependencies"]) - - set(integration.manifest.get("after_dependencies", [])) +def calc_allowed_references(integration: Integration) -> Set[str]: + """Return a set of allowed references.""" + allowed_references = ( + ALLOWED_USED_COMPONENTS + | set(integration.manifest["dependencies"]) + | set(integration.manifest.get("after_dependencies", [])) ) # Discovery requirements are ok if referenced in manifest for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): - if check_domain in referenced and any( - check in integration.manifest for check in to_check - ): - referenced.remove(check_domain) + if any(check in integration.manifest for check in to_check): + allowed_references.add(check_domain) + + return allowed_references + + +def find_non_referenced_integrations( + integrations: Dict[str, Integration], + integration: Integration, + references: Dict[Path, Set[str]], +): + """Find intergrations that are not allowed to be referenced.""" + allowed_references = calc_allowed_references(integration) + referenced = set() + for path, refs in references.items(): + if len(path.parts) == 1: + # climate.py is stored as climate + cur_fil_dir = path.stem + else: + # climate/__init__.py is stored as climate + cur_fil_dir = path.parts[0] + + is_platform_other_integration = cur_fil_dir in integrations - if referenced: - for domain in sorted(referenced): - if ( - integration.domain in IGNORE_VIOLATIONS - or (integration.domain, domain) in IGNORE_VIOLATIONS + for ref in refs: + # We are always allowed to import from ourselves + if ref == integration.domain: + continue + + # These references are approved based on the manifest + if ref in allowed_references: + continue + + # Some violations are whitelisted + if (integration.domain, ref) in IGNORE_VIOLATIONS: + continue + + # If it's a platform for another integration, the other integration is ok + if is_platform_other_integration and cur_fil_dir == ref: + continue + + # These have a platform specified in this integration + if not is_platform_other_integration and ( + (integration.path / f"{ref}.py").is_file() + # Platform dir + or (integration.path / ref).is_dir() ): continue - integration.add_error( - "dependencies", - "Using component {} but it's not in 'dependencies' or 'after_dependencies'".format( - domain - ), - ) + referenced.add(ref) + + return referenced + + +def validate_dependencies( + integrations: Dict[str, Integration], integration: Integration +): + """Validate all dependencies.""" + # Some integrations are allowed to have violations. + if integration.domain in IGNORE_VIOLATIONS: + return + + # Find usage of hass.components + collector = ImportCollector(integration) + collector.collect() + + for domain in sorted( + find_non_referenced_integrations( + integrations, integration, collector.referenced + ) + ): + integration.add_error( + "dependencies", + "Using component {} but it's not in 'dependencies' or 'after_dependencies'".format( + domain + ), + ) def validate(integrations: Dict[str, Integration], config): @@ -181,7 +234,7 @@ def validate(integrations: Dict[str, Integration], config): if not integration.manifest: continue - validate_dependencies(integration) + validate_dependencies(integrations, integration) # check that all referenced dependencies exist for dep in integration.manifest["dependencies"]: diff --git a/tests/hassfest/test_dependencies.py b/tests/hassfest/test_dependencies.py index 0f07c45317e4fc..b9690107619ead 100644 --- a/tests/hassfest/test_dependencies.py +++ b/tests/hassfest/test_dependencies.py @@ -9,7 +9,8 @@ def mock_collector(): """Fixture with import collector that adds all referenced nodes.""" collector = ImportCollector(None) - collector.maybe_add_reference = collector.referenced.add + collector.unfiltered_referenced = set() + collector._add_reference = collector.unfiltered_referenced.add return collector @@ -22,7 +23,7 @@ def test_child_import(mock_collector): """ ) ) - assert mock_collector.referenced == {"child_import"} + assert mock_collector.unfiltered_referenced == {"child_import"} def test_subimport(mock_collector): @@ -34,7 +35,7 @@ def test_subimport(mock_collector): """ ) ) - assert mock_collector.referenced == {"subimport"} + assert mock_collector.unfiltered_referenced == {"subimport"} def test_child_import_field(mock_collector): @@ -46,7 +47,7 @@ def test_child_import_field(mock_collector): """ ) ) - assert mock_collector.referenced == {"child_import_field"} + assert mock_collector.unfiltered_referenced == {"child_import_field"} def test_renamed_absolute(mock_collector): @@ -58,7 +59,7 @@ def test_renamed_absolute(mock_collector): """ ) ) - assert mock_collector.referenced == {"renamed_absolute"} + assert mock_collector.unfiltered_referenced == {"renamed_absolute"} def test_hass_components_var(mock_collector): @@ -71,7 +72,7 @@ def bla(hass): """ ) ) - assert mock_collector.referenced == {"hass_components_var"} + assert mock_collector.unfiltered_referenced == {"hass_components_var"} def test_hass_components_class(mock_collector): @@ -85,7 +86,7 @@ def something(self): """ ) ) - assert mock_collector.referenced == {"hass_components_class"} + assert mock_collector.unfiltered_referenced == {"hass_components_class"} def test_all_imports(mock_collector): @@ -110,7 +111,7 @@ def something(self): """ ) ) - assert mock_collector.referenced == { + assert mock_collector.unfiltered_referenced == { "child_import", "subimport", "child_import_field", From 80be3b74a98a7dd29e0204f1acc22253316c1625 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 19 Dec 2019 14:10:27 +0100 Subject: [PATCH 2484/3953] Init entities as unavailable when offline (#29738) --- homeassistant/components/airly/__init__.py | 4 ---- homeassistant/components/airly/air_quality.py | 21 +++++++++++-------- homeassistant/components/airly/sensor.py | 15 +++++++++---- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index ce165918ac244a..17e1d27e571377 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -10,7 +10,6 @@ from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import Config, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle @@ -48,9 +47,6 @@ async def async_setup_entry(hass, config_entry): await airly.async_update() - if not airly.data: - raise ConfigEntryNotReady() - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly hass.async_create_task( diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index e22fa7939c2ea1..b48a360da28864 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -5,7 +5,7 @@ ATTR_PM_10, AirQualityEntity, ) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from .const import ( ATTR_API_ADVICE, @@ -35,10 +35,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Airly air_quality entity based on a config entry.""" name = config_entry.data[CONF_NAME] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + unique_id = f"{latitude}-{longitude}" data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] - async_add_entities([AirlyAirQuality(data, name)], True) + async_add_entities([AirlyAirQuality(data, name, unique_id)], True) def round_state(func): @@ -56,11 +59,12 @@ def _decorator(self): class AirlyAirQuality(AirQualityEntity): """Define an Airly air quality.""" - def __init__(self, airly, name): + def __init__(self, airly, name, unique_id): """Initialize.""" self.airly = airly self.data = airly.data self._name = name + self._unique_id = unique_id self._pm_2_5 = None self._pm_10 = None self._aqi = None @@ -108,12 +112,12 @@ def state(self): @property def unique_id(self): """Return a unique_id for this entity.""" - return f"{self.airly.latitude}-{self.airly.longitude}" + return self._unique_id @property def available(self): """Return True if entity is available.""" - return bool(self.airly.data) + return bool(self.data) @property def device_state_attributes(self): @@ -132,7 +136,6 @@ async def async_update(self): if self.airly.data: self.data = self.airly.data - - self._pm_10 = self.data[ATTR_API_PM10] - self._pm_2_5 = self.data[ATTR_API_PM25] - self._aqi = self.data[ATTR_API_CAQI] + self._pm_10 = self.data[ATTR_API_PM10] + self._pm_2_5 = self.data[ATTR_API_PM25] + self._aqi = self.data[ATTR_API_CAQI] diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index bce32d640416cb..af0eac39cdcac6 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -2,6 +2,8 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, + CONF_LATITUDE, + CONF_LONGITUDE, CONF_NAME, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -60,12 +62,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Airly sensor entities based on a config entry.""" name = config_entry.data[CONF_NAME] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] sensors = [] for sensor in SENSOR_TYPES: - sensors.append(AirlySensor(data, name, sensor)) + unique_id = f"{latitude}-{longitude}-{sensor.lower()}" + sensors.append(AirlySensor(data, name, sensor, unique_id)) + async_add_entities(sensors, True) @@ -84,11 +90,12 @@ def _decorator(self): class AirlySensor(Entity): """Define an Airly sensor.""" - def __init__(self, airly, name, kind): + def __init__(self, airly, name, kind, unique_id): """Initialize.""" self.airly = airly self.data = airly.data self._name = name + self._unique_id = unique_id self.kind = kind self._device_class = None self._state = None @@ -130,7 +137,7 @@ def device_class(self): @property def unique_id(self): """Return a unique_id for this entity.""" - return f"{self.airly.latitude}-{self.airly.longitude}-{self.kind.lower()}" + return self._unique_id @property def unit_of_measurement(self): @@ -140,7 +147,7 @@ def unit_of_measurement(self): @property def available(self): """Return True if entity is available.""" - return bool(self.airly.data) + return bool(self.data) async def async_update(self): """Update the sensor.""" From 0cb468d7b0987733600bbc850a5e6a1538509784 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 19 Dec 2019 17:45:52 +0100 Subject: [PATCH 2485/3953] Make name of nmbs live sensor customizable via unique_id (#29662) * Make name customizable for nmbs live sensors * Change order of the live name * Added unique id to live nmbs sensor * Revert changing the name of the live sensor --- homeassistant/components/nmbs/sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 26802808c0a0f0..35c928deb37c45 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -92,7 +92,7 @@ def __init__(self, api_client, live_station): """Initialize the sensor for getting liveboard data.""" self._station = live_station self._api_client = api_client - + self._unique_id = f"nmbs_live_{self._station}" self._attrs = {} self._state = None @@ -101,6 +101,11 @@ def name(self): """Return the sensor default name.""" return "NMBS Live" + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def icon(self): """Return the default icon or an alert icon if delays.""" From d236a19139311640610d165b16ed6c9acf1e45e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 19 Dec 2019 19:28:03 +0200 Subject: [PATCH 2486/3953] Include all SSDP data in discovery info (#28197) * Include all SSDP data in discovery info * Use UPnP device description as discovery info, inject some SSDP attrs * Clean up attribute names * Adapt existing SSDP flows to changed attribute names * Prefix all SSDP UPnP attribute name constants with ATTR_UPNP, tweak a bit --- .../components/deconz/config_flow.py | 20 ++++---- homeassistant/components/heos/config_flow.py | 10 ++-- .../components/huawei_lte/config_flow.py | 8 ++-- homeassistant/components/hue/config_flow.py | 18 ++++--- homeassistant/components/ssdp/__init__.py | 47 +++++++------------ tests/components/deconz/test_config_flow.py | 21 ++++----- tests/components/deconz/test_gateway.py | 9 ++-- tests/components/heos/conftest.py | 19 ++++---- tests/components/heos/test_config_flow.py | 10 ++-- .../components/huawei_lte/test_config_flow.py | 38 +++++---------- tests/components/hue/test_config_flow.py | 31 ++++++------ tests/components/ssdp/test_init.py | 22 +++++++-- 12 files changed, 128 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 8c8ba318e8377d..05b7c18f448d0b 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 urllib.parse import urlparse import async_timeout from pydeconz.errors import RequestError, ResponseError @@ -7,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL +from homeassistant.components import ssdp from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -26,7 +27,6 @@ DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" CONF_SERIAL = "serial" -ATTR_UUID = "udn" @callback @@ -170,18 +170,20 @@ async def _update_entry(self, entry, host): async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" - if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL: + if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER_URL] != DECONZ_MANUFACTURERURL: return self.async_abort(reason="not_deconz_bridge") - uuid = discovery_info[ATTR_UUID].replace("uuid:", "") + uuid = discovery_info[ssdp.ATTR_UPNP_UDN].replace("uuid:", "") _LOGGER.debug("deCONZ gateway discovered (%s)", uuid) + parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) + for entry in self.hass.config_entries.async_entries(DOMAIN): if uuid == entry.data.get(CONF_UUID): - return await self._update_entry(entry, discovery_info[CONF_HOST]) + return await self._update_entry(entry, parsed_url.hostname) - bridgeid = discovery_info[ATTR_SERIAL] + bridgeid = discovery_info[ssdp.ATTR_UPNP_SERIAL] if any( bridgeid == flow["context"][CONF_BRIDGEID] for flow in self._async_in_progress() @@ -190,11 +192,11 @@ async def async_step_ssdp(self, discovery_info): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context[CONF_BRIDGEID] = bridgeid - self.context["title_placeholders"] = {"host": discovery_info[CONF_HOST]} + self.context["title_placeholders"] = {"host": parsed_url.hostname} self.deconz_config = { - CONF_HOST: discovery_info[CONF_HOST], - CONF_PORT: discovery_info[CONF_PORT], + CONF_HOST: parsed_url.hostname, + CONF_PORT: parsed_url.port, } return await self.async_step_link() diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 4380cb4d8ba09a..7e7fe067874d5f 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -1,9 +1,12 @@ """Config flow to configure Heos.""" +from urllib.parse import urlparse + from pyheos import Heos, HeosError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.components import ssdp +from homeassistant.const import CONF_HOST from .const import DATA_DISCOVERED_HOSTS, DOMAIN @@ -23,11 +26,12 @@ class HeosFlowHandler(config_entries.ConfigFlow): async def async_step_ssdp(self, discovery_info): """Handle a discovered Heos device.""" # Store discovered host + hostname = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname friendly_name = "{} ({})".format( - discovery_info[CONF_NAME], discovery_info[CONF_HOST] + discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME], hostname ) self.hass.data.setdefault(DATA_DISCOVERED_HOSTS, {}) - self.hass.data[DATA_DISCOVERED_HOSTS][friendly_name] = discovery_info[CONF_HOST] + self.hass.data[DATA_DISCOVERED_HOSTS][friendly_name] = hostname # Abort if other flows in progress or an entry already exists if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="already_setup") diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 9151e5eb99977e..b316472efaf488 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -3,6 +3,7 @@ from collections import OrderedDict import logging from typing import Optional +from urllib.parse import urlparse from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client @@ -19,7 +20,7 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.ssdp import ATTR_HOST, ATTR_NAME, ATTR_PRESENTATIONURL +from homeassistant.components import ssdp from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME from homeassistant.core import callback @@ -208,13 +209,14 @@ async def async_step_ssdp(self, discovery_info): """Handle SSDP initiated config flow.""" # Attempt to distinguish from other non-LTE Huawei router devices, at least # some ones we are interested in have "Mobile Wi-Fi" friendlyName. - if "mobile" not in discovery_info.get(ATTR_NAME, "").lower(): + if "mobile" not in discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "").lower(): return self.async_abort(reason="not_huawei_lte") # https://github.com/PyCQA/pylint/issues/3167 url = self.context[CONF_URL] = url_normalize( # pylint: disable=no-member discovery_info.get( - ATTR_PRESENTATIONURL, f"http://{discovery_info[ATTR_HOST]}/" + ssdp.ATTR_UPNP_PRESENTATION_URL, + f"http://{urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname}/", ) ) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 97cc1a8d66cc9f..60000a68fb7f67 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure Philips Hue.""" import asyncio from typing import Dict, Optional +from urllib.parse import urlparse import aiohue from aiohue.discovery import discover_nupnp, normalize_bridge_id @@ -8,7 +9,7 @@ import voluptuous as vol from homeassistant import config_entries, core -from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME +from homeassistant.components import ssdp from homeassistant.helpers import aiohttp_client from .bridge import authenticate_bridge @@ -147,22 +148,25 @@ async def async_step_ssdp(self, discovery_info): host is already configured and delegate to the import step if not. """ # Filter out non-Hue bridges #1 - if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL: + if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER_URL] != HUE_MANUFACTURERURL: return self.async_abort(reason="not_hue_bridge") # Filter out non-Hue bridges #2 if any( - name in discovery_info.get(ATTR_NAME, "") + name in discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") for name in HUE_IGNORED_BRIDGE_NAMES ): return self.async_abort(reason="not_hue_bridge") - if "host" not in discovery_info or "serial" not in discovery_info: + if ( + ssdp.ATTR_SSDP_LOCATION not in discovery_info + or ssdp.ATTR_UPNP_SERIAL not in discovery_info + ): return self.async_abort(reason="not_hue_bridge") - bridge = self._async_get_bridge( - discovery_info["host"], discovery_info["serial"] - ) + host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname + + bridge = self._async_get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL]) await self.async_set_unique_id(bridge.id) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 1a97b1721fcde0..94e256f05230c5 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -2,7 +2,6 @@ import asyncio from datetime import timedelta import logging -from urllib.parse import urlparse import aiohttp from defusedxml import ElementTree @@ -14,19 +13,19 @@ DOMAIN = "ssdp" SCAN_INTERVAL = timedelta(seconds=60) -ATTR_HOST = "host" -ATTR_PORT = "port" -ATTR_SSDP_DESCRIPTION = "ssdp_description" -ATTR_ST = "ssdp_st" -ATTR_NAME = "name" -ATTR_MODEL_NAME = "model_name" -ATTR_MODEL_NUMBER = "model_number" -ATTR_SERIAL = "serial_number" -ATTR_MANUFACTURER = "manufacturer" -ATTR_MANUFACTURERURL = "manufacturerURL" -ATTR_UDN = "udn" -ATTR_UPNP_DEVICE_TYPE = "upnp_device_type" -ATTR_PRESENTATIONURL = "presentation_url" +# Attributes for accessing info from SSDP response +ATTR_SSDP_LOCATION = "ssdp_location" +ATTR_SSDP_ST = "ssdp_st" +# Attributes for accessing info from retrieved UPnP device description +ATTR_UPNP_DEVICE_TYPE = "deviceType" +ATTR_UPNP_FRIENDLY_NAME = "friendlyName" +ATTR_UPNP_MANUFACTURER = "manufacturer" +ATTR_UPNP_MANUFACTURER_URL = "manufacturerURL" +ATTR_UPNP_MODEL_NAME = "modelName" +ATTR_UPNP_MODEL_NUMBER = "modelNumber" +ATTR_UPNP_PRESENTATION_URL = "presentationURL" +ATTR_UPNP_SERIAL = "serialNumber" +ATTR_UPNP_UDN = "UDN" _LOGGER = logging.getLogger(__name__) @@ -157,24 +156,12 @@ async def _fetch_description(self, xml_location): def info_from_entry(entry, device_info): - """Get most important info from an entry.""" - url = urlparse(entry.location) + """Get info from an entry.""" info = { - ATTR_HOST: url.hostname, - ATTR_PORT: url.port, - ATTR_SSDP_DESCRIPTION: entry.location, - ATTR_ST: entry.st, + ATTR_SSDP_LOCATION: entry.location, + ATTR_SSDP_ST: entry.st, } - if device_info: - info[ATTR_NAME] = device_info.get("friendlyName") - info[ATTR_MODEL_NAME] = device_info.get("modelName") - info[ATTR_MODEL_NUMBER] = device_info.get("modelNumber") - info[ATTR_SERIAL] = device_info.get("serialNumber") - info[ATTR_MANUFACTURER] = device_info.get("manufacturer") - info[ATTR_MANUFACTURERURL] = device_info.get("manufacturerURL") - info[ATTR_UDN] = device_info.get("UDN") - info[ATTR_UPNP_DEVICE_TYPE] = device_info.get("deviceType") - info[ATTR_PRESENTATIONURL] = device_info.get("presentationURL") + info.update(device_info) return info diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index b95743c95ad250..2a1fb43333f212 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -4,8 +4,8 @@ import pydeconz +from homeassistant.components import ssdp from homeassistant.components.deconz import config_flow -from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL from tests.common import MockConfigEntry @@ -213,11 +213,10 @@ async def test_bridge_ssdp_discovery(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_PORT: 80, - ATTR_SERIAL: "id", - ATTR_MANUFACTURERURL: config_flow.DECONZ_MANUFACTURERURL, - config_flow.ATTR_UUID: "uuid:1234", + ssdp.ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "id", + ssdp.ATTR_UPNP_UDN: "uuid:1234", }, context={"source": "ssdp"}, ) @@ -230,7 +229,7 @@ async def test_bridge_ssdp_discovery_not_deconz_bridge(hass): """Test a non deconz bridge being discovered over ssdp.""" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - data={ATTR_MANUFACTURERURL: "not deconz bridge"}, + data={ssdp.ATTR_UPNP_MANUFACTURER_URL: "not deconz bridge"}, context={"source": "ssdp"}, ) @@ -257,10 +256,10 @@ async def test_bridge_discovery_update_existing_entry(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ - config_flow.CONF_HOST: "mock-deconz", - ATTR_SERIAL: "123ABC", - ATTR_MANUFACTURERURL: config_flow.DECONZ_MANUFACTURERURL, - config_flow.ATTR_UUID: "uuid:456DEF", + ssdp.ATTR_SSDP_LOCATION: "http://mock-deconz/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "123ABC", + ssdp.ATTR_UPNP_UDN: "uuid:456DEF", }, context={"source": "ssdp"}, ) diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 109bb325dac432..288868f1bec215 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -145,11 +145,10 @@ async def test_update_address(hass): await hass.config_entries.flow.async_init( deconz.config_flow.DOMAIN, data={ - deconz.config_flow.CONF_HOST: "2.3.4.5", - deconz.config_flow.CONF_PORT: 80, - ssdp.ATTR_SERIAL: BRIDGEID, - ssdp.ATTR_MANUFACTURERURL: deconz.config_flow.DECONZ_MANUFACTURERURL, - deconz.config_flow.ATTR_UUID: "uuid:456DEF", + ssdp.ATTR_SSDP_LOCATION: "http://2.3.4.5:80/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: deconz.config_flow.DECONZ_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: BRIDGEID, + ssdp.ATTR_UPNP_UDN: "uuid:456DEF", }, context={"source": "ssdp"}, ) diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index c4610fdc1e7132..5201b7f7b8ada9 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -5,6 +5,7 @@ from pyheos import Dispatcher, Heos, HeosPlayer, HeosSource, InputSource, const import pytest +from homeassistant.components import ssdp from homeassistant.components.heos import DOMAIN from homeassistant.const import CONF_HOST @@ -118,16 +119,14 @@ def dispatcher_fixture() -> Dispatcher: def discovery_data_fixture() -> dict: """Return mock discovery data for testing.""" return { - "host": "127.0.0.1", - "manufacturer": "Denon", - "model_name": "HEOS Drive", - "model_number": "DWSA-10 4.0", - "name": "Office", - "port": 60006, - "serial": None, - "ssdp_description": "http://127.0.0.1:60006/upnp/desc/aios_device/aios_device.xml", - "udn": "uuid:e61de70c-2250-1c22-0080-0005cdf512be", - "upnp_device_type": "urn:schemas-denon-com:device:AiosDevice:1", + ssdp.ATTR_SSDP_LOCATION: "http://127.0.0.1:60006/upnp/desc/aios_device/aios_device.xml", + ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-denon-com:device:AiosDevice:1", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Office", + ssdp.ATTR_UPNP_MANUFACTURER: "Denon", + ssdp.ATTR_UPNP_MODEL_NAME: "HEOS Drive", + ssdp.ATTR_UPNP_MODEL_NUMBER: "DWSA-10 4.0", + ssdp.ATTR_UPNP_SERIAL: None, + ssdp.ATTR_UPNP_UDN: "uuid:e61de70c-2250-1c22-0080-0005cdf512be", } diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index df021fea55df5c..84e5dce1f1cf10 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -1,10 +1,13 @@ """Tests for the Heos config flow module.""" +from urllib.parse import urlparse + from pyheos import HeosError from homeassistant import data_entry_flow +from homeassistant.components import ssdp from homeassistant.components.heos.config_flow import HeosFlowHandler from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS, DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST async def test_flow_aborts_already_setup(hass, config_entry): @@ -79,8 +82,9 @@ async def test_discovery_shows_create_form(hass, controller, discovery_data): assert len(hass.config_entries.flow.async_progress()) == 1 assert hass.data[DATA_DISCOVERED_HOSTS] == {"Office (127.0.0.1)": "127.0.0.1"} - discovery_data[CONF_HOST] = "127.0.0.2" - discovery_data[CONF_NAME] = "Bedroom" + port = urlparse(discovery_data[ssdp.ATTR_SSDP_LOCATION]).port + discovery_data[ssdp.ATTR_SSDP_LOCATION] = f"http://127.0.0.2:{port}/" + discovery_data[ssdp.ATTR_UPNP_FRIENDLY_NAME] = "Bedroom" await hass.config_entries.flow.async_init( DOMAIN, context={"source": "ssdp"}, data=discovery_data ) diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index b14583f13cd57b..29127ed964bdb4 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -7,22 +7,9 @@ from requests_mock import ANY from homeassistant import data_entry_flow +from homeassistant.components import ssdp from homeassistant.components.huawei_lte.config_flow import ConfigFlowHandler from homeassistant.components.huawei_lte.const import DOMAIN -from homeassistant.components.ssdp import ( - ATTR_HOST, - ATTR_MANUFACTURER, - ATTR_MANUFACTURERURL, - ATTR_MODEL_NAME, - ATTR_MODEL_NUMBER, - ATTR_NAME, - ATTR_PORT, - ATTR_PRESENTATIONURL, - ATTR_SERIAL, - ATTR_ST, - ATTR_UDN, - ATTR_UPNP_DEVICE_TYPE, -) from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from tests.common import MockConfigEntry @@ -156,18 +143,17 @@ async def test_ssdp(flow): url = "http://192.168.100.1/" result = await flow.async_step_ssdp( discovery_info={ - ATTR_ST: "upnp:rootdevice", - ATTR_PORT: 60957, - ATTR_HOST: "192.168.100.1", - ATTR_MANUFACTURER: "Huawei", - ATTR_MANUFACTURERURL: "http://www.huawei.com/", - ATTR_MODEL_NAME: "Huawei router", - ATTR_MODEL_NUMBER: "12345678", - ATTR_NAME: "Mobile Wi-Fi", - ATTR_PRESENTATIONURL: url, - ATTR_SERIAL: "00000000", - ATTR_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", - ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + ssdp.ATTR_SSDP_LOCATION: "http://192.168.100.1:60957/rootDesc.xml", + ssdp.ATTR_SSDP_ST: "upnp:rootdevice", + ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi", + ssdp.ATTR_UPNP_MANUFACTURER: "Huawei", + ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.huawei.com/", + ssdp.ATTR_UPNP_MODEL_NAME: "Huawei router", + ssdp.ATTR_UPNP_MODEL_NUMBER: "12345678", + ssdp.ATTR_UPNP_PRESENTATION_URL: url, + ssdp.ATTR_UPNP_SERIAL: "00000000", + ssdp.ATTR_UPNP_UDN: "uuid:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX", } ) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 64a9c81136a9cd..2ad6474b9ac5e7 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries, data_entry_flow +from homeassistant.components import ssdp from homeassistant.components.hue import config_flow, const from tests.common import MockConfigEntry, mock_coro @@ -208,9 +209,9 @@ async def test_bridge_ssdp(hass): result = await flow.async_step_ssdp( { - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "1234", } ) @@ -224,7 +225,7 @@ async def test_bridge_ssdp_discover_other_bridge(hass): flow.hass = hass result = await flow.async_step_ssdp( - {"manufacturerURL": "http://www.notphilips.com"} + {ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.notphilips.com"} ) assert result["type"] == "abort" @@ -238,10 +239,10 @@ async def test_bridge_ssdp_emulated_hue(hass): result = await flow.async_step_ssdp( { - "name": "HASS Bridge", - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "HASS Bridge", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "1234", } ) @@ -257,10 +258,10 @@ async def test_bridge_ssdp_espalexa(hass): result = await flow.async_step_ssdp( { - "name": "Espalexa (0.0.0.0)", - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Espalexa (0.0.0.0)", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "1234", } ) @@ -281,9 +282,9 @@ async def test_bridge_ssdp_already_configured(hass): with pytest.raises(data_entry_flow.AbortFlow): await flow.async_step_ssdp( { - "host": "0.0.0.0", - "serial": "1234", - "manufacturerURL": config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_SSDP_LOCATION: "http://0.0.0.0/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "1234", } ) diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index e224842b5abc16..62b7c2bbde282e 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -27,7 +27,9 @@ async def test_scan_match_st(hass): assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} -@pytest.mark.parametrize("key", ("manufacturer", "deviceType")) +@pytest.mark.parametrize( + "key", (ssdp.ATTR_UPNP_MANUFACTURER, ssdp.ATTR_UPNP_DEVICE_TYPE) +) async def test_scan_match_upnp_devicedesc(hass, aioclient_mock, key): """Test matching based on UPnP device description data.""" aioclient_mock.get( @@ -74,7 +76,14 @@ async def test_scan_not_all_present(hass, aioclient_mock): return_value=[Mock(st="mock-st", location="http://1.1.1.1")], ), patch.dict( gn_ssdp.SSDP, - {"mock-domain": [{"deviceType": "Paulus", "manufacturer": "Paulus"}]}, + { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_MANUFACTURER: "Paulus", + } + ] + }, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: @@ -103,7 +112,14 @@ async def test_scan_not_all_match(hass, aioclient_mock): return_value=[Mock(st="mock-st", location="http://1.1.1.1")], ), patch.dict( gn_ssdp.SSDP, - {"mock-domain": [{"deviceType": "Paulus", "manufacturer": "Not-Paulus"}]}, + { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", + ssdp.ATTR_UPNP_MANUFACTURER: "Not-Paulus", + } + ] + }, ), patch.object( hass.config_entries.flow, "async_init", return_value=mock_coro() ) as mock_init: From 3a610edb78b15059b8edb663482e2087fb757507 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 20 Dec 2019 00:32:22 +0000 Subject: [PATCH 2487/3953] [ci skip] Translation update --- .../components/fan/.translations/hu.json | 16 ++++++++++++++++ .../components/lock/.translations/hu.json | 17 +++++++++++++++++ .../components/vacuum/.translations/hu.json | 16 ++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 homeassistant/components/fan/.translations/hu.json create mode 100644 homeassistant/components/lock/.translations/hu.json create mode 100644 homeassistant/components/vacuum/.translations/hu.json diff --git a/homeassistant/components/fan/.translations/hu.json b/homeassistant/components/fan/.translations/hu.json new file mode 100644 index 00000000000000..b559f29c58163c --- /dev/null +++ b/homeassistant/components/fan/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name} kikapcsol\u00e1sa", + "turn_on": "{entity_name} bekapcsol\u00e1sa" + }, + "condition_type": { + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva" + }, + "trigger_type": { + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/hu.json b/homeassistant/components/lock/.translations/hu.json new file mode 100644 index 00000000000000..20b1663558bc47 --- /dev/null +++ b/homeassistant/components/lock/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "lock": "{entity_name} z\u00e1r\u00e1sa", + "open": "{entity_name} nyit\u00e1sa", + "unlock": "{entity_name} nyit\u00e1sa" + }, + "condition_type": { + "is_locked": "{entity_name} z\u00e1rva", + "is_unlocked": "{entity_name} nyitva" + }, + "trigger_type": { + "locked": "{entity_name} be lett z\u00e1rva", + "unlocked": "{entity_name} ki lett nyitva" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/hu.json b/homeassistant/components/vacuum/.translations/hu.json new file mode 100644 index 00000000000000..81a39802c550d9 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "{entity_name} takar\u00edt\u00e1s ind\u00edt\u00e1sa", + "dock": "{entity_name} visszak\u00fcld\u00e9se a dokkol\u00f3ra" + }, + "condition_type": { + "is_cleaning": "{entity_name} takar\u00edt", + "is_docked": "{entity_name} dokkolva van" + }, + "trigger_type": { + "cleaning": "{entity_name} elkezdett takar\u00edtani", + "docked": "{entity_name} dokkolt" + } + } +} \ No newline at end of file From eb0aed36536b7ae20308382dc8b755025857d735 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Dec 2019 10:29:18 +0100 Subject: [PATCH 2488/3953] =?UTF-8?q?Fix=20update=20port=20and=20api=20key?= =?UTF-8?q?=20on=20deconz=20discovery=20config=20entry=20u=E2=80=A6=20(#30?= =?UTF-8?q?088)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix update port and api key on discovery config entry update * Remove coroutine from _update_entry --- .../components/deconz/config_flow.py | 16 ++++++++-- tests/components/deconz/test_config_flow.py | 31 ++++++++++++++++--- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 05b7c18f448d0b..f44acdbd3050ce 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -159,12 +159,17 @@ async def _create_entry(self): title="deCONZ-" + self.deconz_config[CONF_BRIDGEID], data=self.deconz_config ) - async def _update_entry(self, entry, host): + def _update_entry(self, entry, host, port, api_key=None): """Update existing entry.""" if entry.data[CONF_HOST] == host: return self.async_abort(reason="already_configured") entry.data[CONF_HOST] = host + entry.data[CONF_PORT] = port + + if api_key is not None: + entry.data[CONF_API_KEY] = api_key + self.hass.config_entries.async_update_entry(entry) return self.async_abort(reason="updated_instance") @@ -181,7 +186,7 @@ async def async_step_ssdp(self, discovery_info): for entry in self.hass.config_entries.async_entries(DOMAIN): if uuid == entry.data.get(CONF_UUID): - return await self._update_entry(entry, parsed_url.hostname) + return self._update_entry(entry, parsed_url.hostname, parsed_url.port) bridgeid = discovery_info[ssdp.ATTR_UPNP_SERIAL] if any( @@ -211,7 +216,12 @@ async def async_step_hassio(self, user_input=None): if bridgeid in gateway_entries: entry = gateway_entries[bridgeid] - return await self._update_entry(entry, user_input[CONF_HOST]) + return self._update_entry( + entry, + user_input[CONF_HOST], + user_input[CONF_PORT], + user_input[CONF_API_KEY], + ) self._hassio_discovery = user_input diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 2a1fb43333f212..ddcbc1a1d5fac4 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -322,32 +322,53 @@ async def test_hassio_update_instance(hass): """Test we can update an existing config entry.""" entry = MockConfigEntry( domain=config_flow.DOMAIN, - data={config_flow.CONF_BRIDGEID: "id", config_flow.CONF_HOST: "1.2.3.4"}, + data={ + config_flow.CONF_BRIDGEID: "id", + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_PORT: 40850, + config_flow.CONF_API_KEY: "secret", + }, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - data={config_flow.CONF_HOST: "mock-deconz", config_flow.CONF_SERIAL: "id"}, + data={ + config_flow.CONF_HOST: "mock-deconz", + config_flow.CONF_PORT: 8080, + config_flow.CONF_API_KEY: "updated", + config_flow.CONF_SERIAL: "id", + }, context={"source": "hassio"}, ) assert result["type"] == "abort" assert result["reason"] == "updated_instance" assert entry.data[config_flow.CONF_HOST] == "mock-deconz" + assert entry.data[config_flow.CONF_PORT] == 8080 + assert entry.data[config_flow.CONF_API_KEY] == "updated" async def test_hassio_dont_update_instance(hass): """Test we can update an existing config entry.""" entry = MockConfigEntry( domain=config_flow.DOMAIN, - data={config_flow.CONF_BRIDGEID: "id", config_flow.CONF_HOST: "1.2.3.4"}, + data={ + config_flow.CONF_BRIDGEID: "id", + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_PORT: 8080, + config_flow.CONF_API_KEY: "secret", + }, ) entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - data={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_SERIAL: "id"}, + data={ + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_PORT: 8080, + config_flow.CONF_API_KEY: "secret", + config_flow.CONF_SERIAL: "id", + }, context={"source": "hassio"}, ) From 92fd3e3ad5eb611c9d0dbce92ab5d5c94b3317e8 Mon Sep 17 00:00:00 2001 From: Fazli Sapuan Date: Fri, 20 Dec 2019 18:00:21 +0800 Subject: [PATCH 2489/3953] Fix homekit handling of 0 light brightness and fan speed (#29962) * Fix homekit handling of 0 light brightness and fan speed * Update homekit tests for new initial brightness/speed value --- homeassistant/components/homekit/type_fans.py | 22 +++++++++++++++++-- .../components/homekit/type_lights.py | 21 ++++++++++++++++-- tests/components/homekit/test_type_fans.py | 5 ++++- tests/components/homekit/test_type_lights.py | 4 +++- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index e3fa6c42c584d4..e6d128d1e28bfe 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -88,8 +88,11 @@ def __init__(self, *args): ) if CHAR_ROTATION_SPEED in chars: + # Initial value is set to 100 because 0 is a special value (off). 100 is + # an arbitrary non-zero value. It is updated immediately by update_state + # to set to the correct initial value. self.char_speed = serv_fan.configure_char( - CHAR_ROTATION_SPEED, value=0, setter_callback=self.set_speed + CHAR_ROTATION_SPEED, value=100, setter_callback=self.set_speed ) if CHAR_SWING_MODE in chars: @@ -156,7 +159,22 @@ def update_state(self, new_state): speed = new_state.attributes.get(ATTR_SPEED) hk_speed_value = self.speed_mapping.speed_to_homekit(speed) if hk_speed_value is not None and self.char_speed.value != hk_speed_value: - self.char_speed.set_value(hk_speed_value) + # If the homeassistant component reports its speed as the first entry + # in its speed list but is not off, the hk_speed_value is 0. But 0 + # is a special value in homekit. When you turn on a homekit accessory + # it will try to restore the last rotation speed state which will be + # the last value saved by char_speed.set_value. But if it is set to + # 0, HomeKit will update the rotation speed to 100 as it thinks 0 is + # off. + # + # Therefore, if the hk_speed_value is 0 and the device is still on, + # the rotation speed is mapped to 1 otherwise the update is ignored + # in order to avoid this incorrect behavior. + if hk_speed_value == 0: + if state == STATE_ON: + self.char_speed.set_value(1) + else: + self.char_speed.set_value(hk_speed_value) # Handle Oscillating if self.char_swing is not None: diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 8e1b07fbbfff34..7f195b276d6984 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -82,8 +82,11 @@ def __init__(self, *args): ) if CHAR_BRIGHTNESS in self.chars: + # Initial value is set to 100 because 0 is a special value (off). 100 is + # an arbitrary non-zero value. It is updated immediately by update_state + # to set to the correct initial value. self.char_brightness = serv_light.configure_char( - CHAR_BRIGHTNESS, value=0, setter_callback=self.set_brightness + CHAR_BRIGHTNESS, value=100, setter_callback=self.set_brightness ) if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id).attributes.get( @@ -183,7 +186,21 @@ def update_state(self, new_state): if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): brightness = round(brightness / 255 * 100, 0) if self.char_brightness.value != brightness: - self.char_brightness.set_value(brightness) + # The homeassistant component might report its brightness as 0 but is + # not off. But 0 is a special value in homekit. When you turn on a + # homekit accessory it will try to restore the last brightness state + # which will be the last value saved by char_brightness.set_value. + # But if it is set to 0, HomeKit will update the brightness to 100 as + # it thinks 0 is off. + # + # Therefore, if the the brighness is 0 and the device is still on, + # the brightness is mapped to 1 otherwise the update is ignored in + # order to avoid this incorrect behavior. + if brightness == 0: + if state == STATE_ON: + self.char_brightness.set_value(1) + else: + self.char_brightness.set_value(brightness) self._flag[CHAR_BRIGHTNESS] = False # Handle color temperature diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 781df6cfb7b87c..5631791d7a2e37 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -197,7 +197,10 @@ async def test_fan_speed(hass, hk_driver, cls, events): ) await hass.async_block_till_done() acc = cls.fan(hass, hk_driver, "Fan", entity_id, 2, None) - assert acc.char_speed.value == 0 + + # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the + # speed to 100 when turning on a fan on a freshly booted up server. + assert acc.char_speed.value != 0 await hass.async_add_job(acc.run) assert ( diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 3444b2778f6b57..510cfa4f6660f9 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -101,7 +101,9 @@ async def test_light_brightness(hass, hk_driver, cls, events): await hass.async_block_till_done() acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) - assert acc.char_brightness.value == 0 + # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the + # brightness to 100 when turning on a light on a freshly booted up server. + assert acc.char_brightness.value != 0 await hass.async_add_job(acc.run) await hass.async_block_till_done() From 04b5d6c6976333d88877a2760874febc2052041d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Fri, 20 Dec 2019 13:24:43 +0100 Subject: [PATCH 2490/3953] Rework tado component (#29246) * Fix imports so it works in custom_components * Rework tado component * Code cleanup * Remove water_heater * Address pylint warnings * Remove water_heater from components * Raise PlatformNotReady when we couldn't connect * Revert PlatformNotReady since we are not a platform * Add debugging information * Add fallback setting * Import with relative path * Address race condition * Cleanup * Catch 422 Errors and log the real error * Use async_schedule_update_ha_state to update the entities * Forgot the True --- homeassistant/components/tado/__init__.py | 192 ++++++----- homeassistant/components/tado/climate.py | 370 ++++++++-------------- homeassistant/components/tado/const.py | 18 ++ homeassistant/components/tado/sensor.py | 177 +++++------ 4 files changed, 341 insertions(+), 416 deletions(-) create mode 100644 homeassistant/components/tado/const.py diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 1739cbb9254f06..ebf605bdc75015 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -9,23 +9,29 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.util import Throttle +from .const import CONF_FALLBACK + _LOGGER = logging.getLogger(__name__) -DATA_TADO = "tado_data" DOMAIN = "tado" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}" TADO_COMPONENTS = ["sensor", "climate"] +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=15) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_FALLBACK, default=True): cv.boolean, } ) }, @@ -38,91 +44,106 @@ def setup(hass, config): username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] - try: - tado = Tado(username, password) - tado.setDebugging(True) - except (RuntimeError, urllib.error.HTTPError): - _LOGGER.error("Unable to connect to mytado with username and password") + tadoconnector = TadoConnector(hass, username, password) + if not tadoconnector.setup(): return False - hass.data[DATA_TADO] = TadoDataStore(tado) + hass.data[DOMAIN] = tadoconnector + # Do first update + tadoconnector.update() + + # Load components for component in TADO_COMPONENTS: - load_platform(hass, component, DOMAIN, {}, config) + load_platform( + hass, + component, + DOMAIN, + {CONF_FALLBACK: config[DOMAIN][CONF_FALLBACK]}, + config, + ) + + # Poll for updates in the background + hass.helpers.event.track_time_interval( + lambda now: tadoconnector.update(), SCAN_INTERVAL + ) return True -class TadoDataStore: +class TadoConnector: """An object to store the Tado data.""" - def __init__(self, tado): - """Initialize Tado data store.""" - self.tado = tado + def __init__(self, hass, username, password): + """Initialize Tado Connector.""" + self.hass = hass + self._username = username + self._password = password - self.sensors = {} - self.data = {} + self.tado = None + self.zones = None + self.devices = None + self.data = { + "zone": {}, + "device": {}, + } + + def setup(self): + """Connect to Tado and fetch the zones.""" + try: + self.tado = Tado(self._username, self._password) + except (RuntimeError, urllib.error.HTTPError) as exc: + _LOGGER.error("Unable to connect: %s", exc) + return False + + self.tado.setDebugging(True) + + # Load zones and devices + self.zones = self.tado.getZones() + self.devices = self.tado.getMe()["homes"] + + return True @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Update the internal data from mytado.com.""" - for data_id, sensor in list(self.sensors.items()): - data = None - - try: - if "zone" in sensor: - _LOGGER.debug( - "Querying mytado.com for zone %s %s", - sensor["id"], - sensor["name"], - ) - data = self.tado.getState(sensor["id"]) - - if "device" in sensor: - _LOGGER.debug( - "Querying mytado.com for device %s %s", - sensor["id"], - sensor["name"], - ) - data = self.tado.getDevices()[0] - - except RuntimeError: - _LOGGER.error( - "Unable to connect to myTado. %s %s", sensor["id"], sensor["id"] - ) - - self.data[data_id] = data - - def add_sensor(self, data_id, sensor): - """Add a sensor to update in _update().""" - self.sensors[data_id] = sensor - self.data[data_id] = None - - def get_data(self, data_id): - """Get the cached data.""" - data = {"error": "no data"} - - if data_id in self.data: - data = self.data[data_id] - - return data - - def get_zones(self): - """Wrap for getZones().""" - return self.tado.getZones() - - def get_capabilities(self, tado_id): - """Wrap for getCapabilities(..).""" - return self.tado.getCapabilities(tado_id) - - def get_me(self): - """Wrap for getMe().""" - return self.tado.getMe() + """Update the registered zones.""" + for zone in self.zones: + self.update_sensor("zone", zone["id"]) + for device in self.devices: + self.update_sensor("device", device["id"]) + + def update_sensor(self, sensor_type, sensor): + """Update the internal data from Tado.""" + _LOGGER.debug("Updating %s %s", sensor_type, sensor) + try: + if sensor_type == "zone": + data = self.tado.getState(sensor) + elif sensor_type == "device": + data = self.tado.getDevices()[0] + else: + _LOGGER.debug("Unknown sensor: %s", sensor_type) + return + except RuntimeError: + _LOGGER.error( + "Unable to connect to Tado while updating %s %s", sensor_type, sensor, + ) + return + + self.data[sensor_type][sensor] = data + + _LOGGER.debug("Dispatching update to %s %s: %s", sensor_type, sensor, data) + dispatcher_send( + self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format(sensor_type, sensor) + ) + + def get_capabilities(self, zone_id): + """Return the capabilities of the devices.""" + return self.tado.getCapabilities(zone_id) def reset_zone_overlay(self, zone_id): - """Wrap for resetZoneOverlay(..).""" + """Reset the zone back to the default operation.""" self.tado.resetZoneOverlay(zone_id) - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + self.update_sensor("zone", zone_id) def set_zone_overlay( self, @@ -133,13 +154,32 @@ def set_zone_overlay( device_type="HEATING", mode=None, ): - """Wrap for setZoneOverlay(..).""" - self.tado.setZoneOverlay( - zone_id, overlay_mode, temperature, duration, device_type, "ON", mode + """Set a zone overlay.""" + _LOGGER.debug( + "Set overlay for zone %s: mode=%s, temp=%s, duration=%s, type=%s, mode=%s", + zone_id, + overlay_mode, + temperature, + duration, + device_type, + mode, ) - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + try: + self.tado.setZoneOverlay( + zone_id, overlay_mode, temperature, duration, device_type, "ON", mode + ) + except urllib.error.HTTPError as exc: + _LOGGER.error("Could not set zone overlay: %s", exc.read()) + + self.update_sensor("zone", zone_id) def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): """Set a zone to off.""" - self.tado.setZoneOverlay(zone_id, overlay_mode, None, None, device_type, "OFF") - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + try: + self.tado.setZoneOverlay( + zone_id, overlay_mode, None, None, device_type, "OFF" + ) + except urllib.error.HTTPError as exc: + _LOGGER.error("Could not set zone overlay: %s", exc.read()) + + self.update_sensor("zone", zone_id) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 2baf1f380b5928..88433db09914b9 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -1,6 +1,5 @@ -"""Support for Tado to create a climate device for each zone.""" +"""Support for Tado thermostats.""" import logging -from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -23,27 +22,20 @@ SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS -from homeassistant.util.temperature import convert as convert_temperature - -from . import DATA_TADO +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import CONF_FALLBACK, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from .const import ( + CONST_MODE_OFF, + CONST_MODE_SMART_SCHEDULE, + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_MODE, + TYPE_AIR_CONDITIONING, +) _LOGGER = logging.getLogger(__name__) -CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Default mytado mode -CONST_MODE_OFF = "OFF" # Switch off heating in a zone - -# When we change the temperature setting, we need an overlay mode -# wait until tado changes the mode automatic -CONST_OVERLAY_TADO_MODE = "TADO_MODE" -# the user has change the temperature or mode manually -CONST_OVERLAY_MANUAL = "MANUAL" -# the temperature will be reset after a timespan -CONST_OVERLAY_TIMER = "TIMER" - -CONST_MODE_FAN_HIGH = "HIGH" -CONST_MODE_FAN_MIDDLE = "MIDDLE" -CONST_MODE_FAN_LOW = "LOW" - FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW} HVAC_MAP_TADO_HEAT = { @@ -78,35 +70,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tado climate platform.""" - tado = hass.data[DATA_TADO] - - try: - zones = tado.get_zones() - except RuntimeError: - _LOGGER.error("Unable to get zone info from mytado") - return + tado = hass.data[DOMAIN] - climate_devices = [] - for zone in zones: - device = create_climate_device(tado, hass, zone, zone["name"], zone["id"]) - if not device: - continue - climate_devices.append(device) + entities = [] + for zone in tado.zones: + entity = create_climate_entity( + tado, zone["name"], zone["id"], discovery_info[CONF_FALLBACK] + ) + if entity: + entities.append(entity) - if climate_devices: - add_entities(climate_devices, True) + if entities: + add_entities(entities, True) -def create_climate_device(tado, hass, zone, name, zone_id): - """Create a Tado climate device.""" +def create_climate_entity(tado, name: str, zone_id: int, fallback: bool): + """Create a Tado climate entity.""" capabilities = tado.get_capabilities(zone_id) + _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities) - unit = TEMP_CELSIUS - ac_device = capabilities["type"] == "AIR_CONDITIONING" - hot_water_device = capabilities["type"] == "HOT_WATER" - ac_support_heat = False + zone_type = capabilities["type"] - if ac_device: + ac_support_heat = False + if zone_type == TYPE_AIR_CONDITIONING: # 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 @@ -118,67 +104,56 @@ def create_climate_device(tado, hass, zone, name, zone_id): elif "temperatures" in capabilities: temperatures = capabilities["temperatures"] else: - _LOGGER.debug("Received zone %s has no temperature; not adding", name) - return + _LOGGER.debug("Not adding zone %s since it has no temperature", name) + return None min_temp = float(temperatures["celsius"]["min"]) max_temp = float(temperatures["celsius"]["max"]) step = temperatures["celsius"].get("step", PRECISION_TENTHS) - data_id = f"zone {name} {zone_id}" - device = TadoClimate( + entity = TadoClimate( tado, name, zone_id, - data_id, - hass.config.units.temperature(min_temp, unit), - hass.config.units.temperature(max_temp, unit), + zone_type, + min_temp, + max_temp, step, - ac_device, - hot_water_device, ac_support_heat, + fallback, ) - - tado.add_sensor( - data_id, {"id": zone_id, "zone": zone, "name": name, "climate": device} - ) - - return device + return entity class TadoClimate(ClimateDevice): - """Representation of a Tado climate device.""" + """Representation of a Tado climate entity.""" def __init__( self, - store, + tado, zone_name, zone_id, - data_id, + zone_type, min_temp, max_temp, step, - ac_device, - hot_water_device, ac_support_heat, - tolerance=0.3, + fallback, ): - """Initialize of Tado climate device.""" - self._store = store - self._data_id = data_id + """Initialize of Tado climate entity.""" + self._tado = tado self.zone_name = zone_name self.zone_id = zone_id + self.zone_type = zone_type - self._ac_device = ac_device - self._hot_water_device = hot_water_device + self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._ac_support_heat = ac_support_heat self._cooling = False self._active = False self._device_is_active = False - self._unit = TEMP_CELSIUS self._cur_temp = None self._cur_humidity = None self._is_away = False @@ -186,12 +161,34 @@ def __init__( self._max_temp = max_temp self._step = step self._target_temp = None - self._tolerance = tolerance + + if fallback: + _LOGGER.debug("Default overlay is set to TADO MODE") + # Fallback to Smart Schedule at next Schedule switch + self._default_overlay = CONST_OVERLAY_TADO_MODE + else: + _LOGGER.debug("Default overlay is set to MANUAL MODE") + # Don't fallback to Smart Schedule, but keep in manual mode + self._default_overlay = CONST_OVERLAY_MANUAL self._current_fan = CONST_MODE_OFF self._current_operation = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE + async def async_added_to_hass(self): + """Register for sensor updates.""" + + @callback + def async_update_callback(): + """Schedule an entity update.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), + async_update_callback, + ) + @property def supported_features(self): """Return the list of supported features.""" @@ -199,18 +196,19 @@ def supported_features(self): @property def name(self): - """Return the name of the device.""" + """Return the name of the entity.""" return self.zone_name + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + @property def current_humidity(self): """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.""" @@ -234,9 +232,9 @@ def hvac_modes(self): Need to be a subset of HVAC_MODES. """ - if self._ac_device and self._ac_support_heat: - return SUPPORT_HVAC_HEAT_COOL - if self._ac_device and not self._ac_support_heat: + if self._ac_device: + if self._ac_support_heat: + return SUPPORT_HVAC_HEAT_COOL return SUPPORT_HVAC_COOL return SUPPORT_HVAC_HEAT @@ -248,16 +246,10 @@ def hvac_action(self): """ 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._ac_device: if self._active: + if self._ac_support_heat and not self._cooling: + return CURRENT_HVAC_HEAT return CURRENT_HVAC_COOL return CURRENT_HVAC_IDLE if self._active: @@ -284,7 +276,7 @@ def set_fan_mode(self, fan_mode: str): @property def preset_mode(self): - """Return the current preset mode, e.g., home, away, temp.""" + """Return the current preset mode (home, away).""" if self._is_away: return PRESET_AWAY return PRESET_HOME @@ -301,7 +293,7 @@ def set_preset_mode(self, preset_mode): @property def temperature_unit(self): """Return the unit of measurement used by the platform.""" - return self._unit + return TEMP_CELSIUS @property def target_temperature_step(self): @@ -313,23 +305,13 @@ def target_temperature(self): """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 = self._default_overlay self._overlay_mode = None self._target_temp = temperature self._control_heating() @@ -343,50 +325,51 @@ def set_hvac_mode(self, hvac_mode): elif hvac_mode == HVAC_MODE_AUTO: mode = CONST_MODE_SMART_SCHEDULE elif hvac_mode == HVAC_MODE_HEAT: - mode = CONST_OVERLAY_TADO_MODE + mode = self._default_overlay elif hvac_mode == HVAC_MODE_COOL: - mode = CONST_OVERLAY_TADO_MODE + mode = self._default_overlay elif hvac_mode == HVAC_MODE_HEAT_COOL: - mode = CONST_OVERLAY_TADO_MODE + mode = self._default_overlay self._current_operation = mode self._overlay_mode = None - if self._target_temp is None and self._ac_device: - self._target_temp = 27 + + # Set a target temperature if we don't have any + # This can happen when we switch from Off to On + if self._target_temp is None: + if self._ac_device: + self._target_temp = self.max_temp + else: + self._target_temp = self.min_temp + self.schedule_update_ha_state() + self._control_heating() @property def min_temp(self): """Return the minimum temperature.""" - return convert_temperature( - self._min_temp, self._unit, self.hass.config.units.temperature_unit - ) + return self._min_temp @property def max_temp(self): """Return the maximum temperature.""" - return convert_temperature( - self._max_temp, self._unit, self.hass.config.units.temperature_unit - ) + return self._max_temp def update(self): - """Update the state of this climate device.""" - self._store.update() - - data = self._store.get_data(self._data_id) - - if data is None: - _LOGGER.debug("Received no data for zone %s", self.zone_name) + """Handle update callbacks.""" + _LOGGER.debug("Updating climate platform for zone %d", self.zone_id) + try: + data = self._tado.data["zone"][self.zone_id] + except KeyError: + _LOGGER.debug("No data") return if "sensorDataPoints" in data: sensor_data = data["sensorDataPoints"] - unit = TEMP_CELSIUS - if "insideTemperature" in sensor_data: temperature = float(sensor_data["insideTemperature"]["celsius"]) - self._cur_temp = self.hass.config.units.temperature(temperature, unit) + self._cur_temp = temperature if "humidity" in sensor_data: humidity = float(sensor_data["humidity"]["percentage"]) @@ -398,7 +381,7 @@ def update(self): and data["setting"]["temperature"] is not None ): setting = float(data["setting"]["temperature"]["celsius"]) - self._target_temp = self.hass.config.units.temperature(setting, unit) + self._target_temp = setting if "tadoMode" in data: mode = data["tadoMode"] @@ -468,135 +451,38 @@ def update(self): self._current_fan = fan_speed def _control_heating(self): - """Send new target temperature to mytado.""" - if None not in (self._cur_temp, self._target_temp): - _LOGGER.info( - "Obtained current (%d) and target temperature (%d). " - "Tado thermostat active", - self._cur_temp, - self._target_temp, - ) - + """Send new target temperature to Tado.""" if self._current_operation == CONST_MODE_SMART_SCHEDULE: - _LOGGER.info( - "Switching mytado.com to SCHEDULE (default) for zone %s (%d)", + _LOGGER.debug( + "Switching to SMART_SCHEDULE for zone %s (%d)", self.zone_name, self.zone_id, ) - self._store.reset_zone_overlay(self.zone_id) + self._tado.reset_zone_overlay(self.zone_id) self._overlay_mode = self._current_operation return if self._current_operation == CONST_MODE_OFF: - 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" - ) - elif self._hot_water_device: - _LOGGER.info( - "Switching mytado.com to OFF for zone %s (%d) - HOT_WATER", - self.zone_name, - self.zone_id, - ) - self._store.set_zone_off( - self.zone_id, CONST_OVERLAY_MANUAL, "HOT_WATER" - ) - 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") + _LOGGER.debug( + "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id + ) + self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, self.zone_type) self._overlay_mode = self._current_operation return - 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", - ) - elif self._hot_water_device: - _LOGGER.info( - "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - HOT_WATER", - 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, - "HOT_WATER", - ) - 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", - ) - + _LOGGER.debug( + "Switching to %s for zone %s (%d) with temperature %s °C", + self._current_operation, + self.zone_name, + self.zone_id, + self._target_temp, + ) + self._tado.set_zone_overlay( + self.zone_id, + self._current_operation, + self._target_temp, + None, + self.zone_type, + "COOL" if self._ac_device else None, + ) 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/const.py b/homeassistant/components/tado/const.py new file mode 100644 index 00000000000000..3c0232c8ba2119 --- /dev/null +++ b/homeassistant/components/tado/const.py @@ -0,0 +1,18 @@ +"""Constant values for the Tado component.""" + +# Configuration +CONF_FALLBACK = "fallback" + +# Types +TYPE_AIR_CONDITIONING = "AIR_CONDITIONING" +TYPE_HEATING = "HEATING" +TYPE_HOT_WATER = "HOT_WATER" + +# Base modes +CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Use the schedule +CONST_MODE_OFF = "OFF" # Switch off heating in a zone + +# When we change the temperature setting, we need an overlay mode +CONST_OVERLAY_TADO_MODE = "TADO_MODE" # wait until tado changes the mode automatic +CONST_OVERLAY_MANUAL = "MANUAL" # the user has change the temperature or mode manually +CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 346b27bec26be5..68b470aced706a 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -1,130 +1,109 @@ """Support for Tado sensors for each zone.""" import logging -from homeassistant.const import ATTR_ID, ATTR_NAME, TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import DATA_TADO +from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from .const import TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER _LOGGER = logging.getLogger(__name__) -ATTR_DATA_ID = "data_id" -ATTR_DEVICE = "device" -ATTR_ZONE = "zone" - -CLIMATE_HEAT_SENSOR_TYPES = [ - "temperature", - "humidity", - "power", - "link", - "heating", - "tado mode", - "overlay", - "early start", -] - -CLIMATE_COOL_SENSOR_TYPES = [ - "temperature", - "humidity", - "power", - "link", - "ac", - "tado mode", - "overlay", -] - -HOT_WATER_SENSOR_TYPES = ["power", "link", "tado mode", "overlay"] +ZONE_SENSORS = { + TYPE_HEATING: [ + "temperature", + "humidity", + "power", + "link", + "heating", + "tado mode", + "overlay", + "early start", + ], + TYPE_AIR_CONDITIONING: [ + "temperature", + "humidity", + "power", + "link", + "ac", + "tado mode", + "overlay", + ], + TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"], +} + +DEVICE_SENSORS = ["tado bridge status"] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the sensor platform.""" - tado = hass.data[DATA_TADO] - - try: - zones = tado.get_zones() - except RuntimeError: - _LOGGER.error("Unable to get zone info from mytado") - return - - sensor_items = [] - for zone in zones: - if zone["type"] == "HEATING": - for variable in CLIMATE_HEAT_SENSOR_TYPES: - sensor_items.append( - create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) - ) - elif zone["type"] == "HOT_WATER": - for variable in HOT_WATER_SENSOR_TYPES: - 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( - create_device_sensor( - tado, - me_data, - me_data["homes"][0]["name"], - me_data["homes"][0]["id"], - "tado bridge status", + tado = hass.data[DOMAIN] + + # Create zone sensors + entities = [] + for zone in tado.zones: + entities.extend( + [ + create_zone_sensor(tado, zone["name"], zone["id"], variable) + for variable in ZONE_SENSORS.get(zone["type"]) + ] ) - ) - - if sensor_items: - add_entities(sensor_items, True) + # Create device sensors + for home in tado.devices: + entities.extend( + [ + create_device_sensor(tado, home["name"], home["id"], variable) + for variable in DEVICE_SENSORS + ] + ) -def create_zone_sensor(tado, zone, name, zone_id, variable): - """Create a zone sensor.""" - data_id = f"zone {name} {zone_id}" + add_entities(entities, True) - tado.add_sensor( - data_id, - {ATTR_ZONE: zone, ATTR_NAME: name, ATTR_ID: zone_id, ATTR_DATA_ID: data_id}, - ) - return TadoSensor(tado, name, zone_id, variable, data_id) +def create_zone_sensor(tado, name, zone_id, variable): + """Create a zone sensor.""" + return TadoSensor(tado, name, "zone", zone_id, variable) -def create_device_sensor(tado, device, name, device_id, variable): +def create_device_sensor(tado, name, device_id, variable): """Create a device sensor.""" - data_id = f"device {name} {device_id}" - - tado.add_sensor( - data_id, - { - ATTR_DEVICE: device, - ATTR_NAME: name, - ATTR_ID: device_id, - ATTR_DATA_ID: data_id, - }, - ) - - return TadoSensor(tado, name, device_id, variable, data_id) + return TadoSensor(tado, name, "device", device_id, variable) class TadoSensor(Entity): """Representation of a tado Sensor.""" - def __init__(self, store, zone_name, zone_id, zone_variable, data_id): + def __init__(self, tado, zone_name, sensor_type, zone_id, zone_variable): """Initialize of the Tado Sensor.""" - self._store = store + self._tado = tado self.zone_name = zone_name self.zone_id = zone_id self.zone_variable = zone_variable + self.sensor_type = sensor_type self._unique_id = f"{zone_variable} {zone_id}" - self._data_id = data_id self._state = None self._state_attributes = None + async def async_added_to_hass(self): + """Register for sensor updates.""" + + @callback + def async_update_callback(): + """Schedule an entity update.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format(self.sensor_type, self.zone_id), + async_update_callback, + ) + @property def unique_id(self): """Return the unique id.""" @@ -165,14 +144,16 @@ def icon(self): if self.zone_variable == "humidity": return "mdi:water-percent" - def update(self): - """Update method called when should_poll is true.""" - self._store.update() - - data = self._store.get_data(self._data_id) + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False - if data is None: - _LOGGER.debug("Received no data for zone %s", self.zone_name) + def update(self): + """Handle update callbacks.""" + try: + data = self._tado.data[self.sensor_type][self.zone_id] + except KeyError: return unit = TEMP_CELSIUS From 939ca63cbc2f9072628e0f3086e594a544296027 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Dec 2019 10:29:18 +0100 Subject: [PATCH 2491/3953] =?UTF-8?q?Fix=20update=20port=20and=20api=20key?= =?UTF-8?q?=20on=20deconz=20discovery=20config=20entry=20u=E2=80=A6=20(#30?= =?UTF-8?q?088)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix update port and api key on discovery config entry update * Remove coroutine from _update_entry --- .../components/deconz/config_flow.py | 18 +++++++++-- tests/components/deconz/test_config_flow.py | 31 ++++++++++++++++--- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index b757f1f4d03045..8a34ddcbae00fe 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -158,12 +158,17 @@ async def _create_entry(self): title="deCONZ-" + self.deconz_config[CONF_BRIDGEID], data=self.deconz_config ) - async def _update_entry(self, entry, host): + def _update_entry(self, entry, host, port, api_key=None): """Update existing entry.""" if entry.data[CONF_HOST] == host: return self.async_abort(reason="already_configured") entry.data[CONF_HOST] = host + entry.data[CONF_PORT] = port + + if api_key is not None: + entry.data[CONF_API_KEY] = api_key + self.hass.config_entries.async_update_entry(entry) return self.async_abort(reason="updated_instance") @@ -182,7 +187,9 @@ async def async_step_ssdp(self, discovery_info): for entry in self.hass.config_entries.async_entries(DOMAIN): if uuid == entry.data.get(CONF_UUID): - return await self._update_entry(entry, discovery_info[CONF_HOST]) + return self._update_entry( + entry, discovery_info[CONF_HOST], discovery_info[CONF_PORT] + ) bridgeid = discovery_info[ATTR_SERIAL] if any( @@ -212,7 +219,12 @@ async def async_step_hassio(self, user_input=None): if bridgeid in gateway_entries: entry = gateway_entries[bridgeid] - return await self._update_entry(entry, user_input[CONF_HOST]) + return self._update_entry( + entry, + user_input[CONF_HOST], + user_input[CONF_PORT], + user_input[CONF_API_KEY], + ) self._hassio_discovery = user_input diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 4045201bd18084..51986a3bbc0627 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -323,32 +323,53 @@ async def test_hassio_update_instance(hass): """Test we can update an existing config entry.""" entry = MockConfigEntry( domain=config_flow.DOMAIN, - data={config_flow.CONF_BRIDGEID: "id", config_flow.CONF_HOST: "1.2.3.4"}, + data={ + config_flow.CONF_BRIDGEID: "id", + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_PORT: 40850, + config_flow.CONF_API_KEY: "secret", + }, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - data={config_flow.CONF_HOST: "mock-deconz", config_flow.CONF_SERIAL: "id"}, + data={ + config_flow.CONF_HOST: "mock-deconz", + config_flow.CONF_PORT: 8080, + config_flow.CONF_API_KEY: "updated", + config_flow.CONF_SERIAL: "id", + }, context={"source": "hassio"}, ) assert result["type"] == "abort" assert result["reason"] == "updated_instance" assert entry.data[config_flow.CONF_HOST] == "mock-deconz" + assert entry.data[config_flow.CONF_PORT] == 8080 + assert entry.data[config_flow.CONF_API_KEY] == "updated" async def test_hassio_dont_update_instance(hass): """Test we can update an existing config entry.""" entry = MockConfigEntry( domain=config_flow.DOMAIN, - data={config_flow.CONF_BRIDGEID: "id", config_flow.CONF_HOST: "1.2.3.4"}, + data={ + config_flow.CONF_BRIDGEID: "id", + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_PORT: 8080, + config_flow.CONF_API_KEY: "secret", + }, ) entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, - data={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_SERIAL: "id"}, + data={ + config_flow.CONF_HOST: "1.2.3.4", + config_flow.CONF_PORT: 8080, + config_flow.CONF_API_KEY: "secret", + config_flow.CONF_SERIAL: "id", + }, context={"source": "hassio"}, ) From e6a27983d32e0743a634397c9408cb576ada3fdd Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 20 Dec 2019 14:16:39 +0000 Subject: [PATCH 2492/3953] Bump version 0.103.3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 01a2d609b87c0f..6239be3cb61512 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 103 -PATCH_VERSION = "2" +PATCH_VERSION = "3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From e58291015c86bae2677e8a88ae66515b99fb05a8 Mon Sep 17 00:00:00 2001 From: omriasta Date: Wed, 18 Dec 2019 15:04:54 -0500 Subject: [PATCH 2493/3953] Patch rachio (#30031) * fix binary sensor offline/online fixed self._handle_update on line 66 to produce args, kwargs. Updated the binary sensor to check the correct index in the tuple. * Fixed Standby switch Set standby switch to poll in order to get status when homeassistant starts up. Updated the index for the switch to get the status from the tuple. --- homeassistant/components/rachio/binary_sensor.py | 6 +++--- homeassistant/components/rachio/switch.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index f74e3ca1802181..f13eba59ac9b8c 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -63,7 +63,7 @@ def _handle_any_update(self, *args, **kwargs) -> None: return # For this device - self._handle_update() + self._handle_update(args, kwargs) @abstractmethod def _poll_update(self, data=None) -> bool: @@ -119,9 +119,9 @@ def _poll_update(self, data=None) -> bool: def _handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" - if args[0][KEY_SUBTYPE] == SUBTYPE_ONLINE: + if args[0][0][KEY_SUBTYPE] == SUBTYPE_ONLINE: self._state = True - elif args[0][KEY_SUBTYPE] == SUBTYPE_OFFLINE: + elif args[0][0][KEY_SUBTYPE] == SUBTYPE_OFFLINE: self._state = False self.schedule_update_ha_state() diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 80c227a6df6977..a3a4f6bcca17b3 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -107,7 +107,7 @@ def __init__(self, hass, controller): dispatcher_connect( hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, self._handle_any_update ) - super().__init__(controller, poll=False) + super().__init__(controller, poll=True) self._poll_update(controller.init_data) @property @@ -134,9 +134,9 @@ def _poll_update(self, data=None) -> bool: def _handle_update(self, *args, **kwargs) -> None: """Update the state using webhook data.""" - if args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON: + if args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_ON: self._state = True - elif args[0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF: + elif args[0][0][KEY_SUBTYPE] == SUBTYPE_SLEEP_MODE_OFF: self._state = False self.schedule_update_ha_state() From 20a0557be71722ed9e2a94b055411b713ff95257 Mon Sep 17 00:00:00 2001 From: Wim Haanstra Date: Wed, 18 Dec 2019 21:18:14 +0100 Subject: [PATCH 2494/3953] Fix failure in transform method (#30023) * Fix failure in transform method * Fix formatting issue --- homeassistant/components/dsmr_reader/definitions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index f4a36ebc34682c..45bebfeda92c2d 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -3,7 +3,9 @@ def dsmr_transform(value): """Transform DSMR version value to right format.""" - return float(value) / 10 + if value.isdigit(): + return float(value) / 10 + return value def tariff_transform(value): From 488f26d55b93dc56dc484de4953d77546423f1f5 Mon Sep 17 00:00:00 2001 From: Fazli Sapuan Date: Fri, 20 Dec 2019 18:00:21 +0800 Subject: [PATCH 2495/3953] Fix homekit handling of 0 light brightness and fan speed (#29962) * Fix homekit handling of 0 light brightness and fan speed * Update homekit tests for new initial brightness/speed value --- homeassistant/components/homekit/type_fans.py | 22 +++++++++++++++++-- .../components/homekit/type_lights.py | 21 ++++++++++++++++-- tests/components/homekit/test_type_fans.py | 5 ++++- tests/components/homekit/test_type_lights.py | 4 +++- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index e3fa6c42c584d4..e6d128d1e28bfe 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -88,8 +88,11 @@ def __init__(self, *args): ) if CHAR_ROTATION_SPEED in chars: + # Initial value is set to 100 because 0 is a special value (off). 100 is + # an arbitrary non-zero value. It is updated immediately by update_state + # to set to the correct initial value. self.char_speed = serv_fan.configure_char( - CHAR_ROTATION_SPEED, value=0, setter_callback=self.set_speed + CHAR_ROTATION_SPEED, value=100, setter_callback=self.set_speed ) if CHAR_SWING_MODE in chars: @@ -156,7 +159,22 @@ def update_state(self, new_state): speed = new_state.attributes.get(ATTR_SPEED) hk_speed_value = self.speed_mapping.speed_to_homekit(speed) if hk_speed_value is not None and self.char_speed.value != hk_speed_value: - self.char_speed.set_value(hk_speed_value) + # If the homeassistant component reports its speed as the first entry + # in its speed list but is not off, the hk_speed_value is 0. But 0 + # is a special value in homekit. When you turn on a homekit accessory + # it will try to restore the last rotation speed state which will be + # the last value saved by char_speed.set_value. But if it is set to + # 0, HomeKit will update the rotation speed to 100 as it thinks 0 is + # off. + # + # Therefore, if the hk_speed_value is 0 and the device is still on, + # the rotation speed is mapped to 1 otherwise the update is ignored + # in order to avoid this incorrect behavior. + if hk_speed_value == 0: + if state == STATE_ON: + self.char_speed.set_value(1) + else: + self.char_speed.set_value(hk_speed_value) # Handle Oscillating if self.char_swing is not None: diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 8e1b07fbbfff34..7f195b276d6984 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -82,8 +82,11 @@ def __init__(self, *args): ) if CHAR_BRIGHTNESS in self.chars: + # Initial value is set to 100 because 0 is a special value (off). 100 is + # an arbitrary non-zero value. It is updated immediately by update_state + # to set to the correct initial value. self.char_brightness = serv_light.configure_char( - CHAR_BRIGHTNESS, value=0, setter_callback=self.set_brightness + CHAR_BRIGHTNESS, value=100, setter_callback=self.set_brightness ) if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id).attributes.get( @@ -183,7 +186,21 @@ def update_state(self, new_state): if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): brightness = round(brightness / 255 * 100, 0) if self.char_brightness.value != brightness: - self.char_brightness.set_value(brightness) + # The homeassistant component might report its brightness as 0 but is + # not off. But 0 is a special value in homekit. When you turn on a + # homekit accessory it will try to restore the last brightness state + # which will be the last value saved by char_brightness.set_value. + # But if it is set to 0, HomeKit will update the brightness to 100 as + # it thinks 0 is off. + # + # Therefore, if the the brighness is 0 and the device is still on, + # the brightness is mapped to 1 otherwise the update is ignored in + # order to avoid this incorrect behavior. + if brightness == 0: + if state == STATE_ON: + self.char_brightness.set_value(1) + else: + self.char_brightness.set_value(brightness) self._flag[CHAR_BRIGHTNESS] = False # Handle color temperature diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 781df6cfb7b87c..5631791d7a2e37 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -197,7 +197,10 @@ async def test_fan_speed(hass, hk_driver, cls, events): ) await hass.async_block_till_done() acc = cls.fan(hass, hk_driver, "Fan", entity_id, 2, None) - assert acc.char_speed.value == 0 + + # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the + # speed to 100 when turning on a fan on a freshly booted up server. + assert acc.char_speed.value != 0 await hass.async_add_job(acc.run) assert ( diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 784a6c82346174..996783249376c3 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -101,7 +101,9 @@ async def test_light_brightness(hass, hk_driver, cls, events): await hass.async_block_till_done() acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) - assert acc.char_brightness.value == 0 + # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the + # brightness to 100 when turning on a light on a freshly booted up server. + assert acc.char_brightness.value != 0 await hass.async_add_job(acc.run) await hass.async_block_till_done() From 12f1a322a7e5e83bc0c308252eb98b42873ab1c8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 20 Dec 2019 16:20:10 +0000 Subject: [PATCH 2496/3953] Fix test --- homeassistant/components/deconz/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 8a34ddcbae00fe..ce890cd010359f 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -188,7 +188,7 @@ async def async_step_ssdp(self, discovery_info): for entry in self.hass.config_entries.async_entries(DOMAIN): if uuid == entry.data.get(CONF_UUID): return self._update_entry( - entry, discovery_info[CONF_HOST], discovery_info[CONF_PORT] + entry, discovery_info[CONF_HOST], entry.data.get[CONF_PORT] ) bridgeid = discovery_info[ATTR_SERIAL] From 5def51835eb98ec11dc19276fcd678f6cd2bd958 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 20 Dec 2019 17:10:04 +0000 Subject: [PATCH 2497/3953] Fix v2 --- homeassistant/components/deconz/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index ce890cd010359f..c2035d61d6e0a3 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -188,7 +188,7 @@ async def async_step_ssdp(self, discovery_info): for entry in self.hass.config_entries.async_entries(DOMAIN): if uuid == entry.data.get(CONF_UUID): return self._update_entry( - entry, discovery_info[CONF_HOST], entry.data.get[CONF_PORT] + entry, discovery_info[CONF_HOST], entry.data.get(CONF_PORT) ) bridgeid = discovery_info[ATTR_SERIAL] From 84e1b3d07fb1bf3e65383286f5dd41f30498e653 Mon Sep 17 00:00:00 2001 From: gppanayotov <39181403+gppanayotov@users.noreply.github.com> Date: Fri, 20 Dec 2019 20:02:46 +0200 Subject: [PATCH 2498/3953] Add an open window sensor for heating zones (#30090) --- homeassistant/components/tado/sensor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 68b470aced706a..a928b61a50867f 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -21,6 +21,7 @@ "tado mode", "overlay", "early start", + "open window", ], TYPE_AIR_CONDITIONING: [ "temperature", @@ -240,3 +241,9 @@ def update(self): self._state = True else: self._state = False + + elif self.zone_variable == "open window": + if "openWindowDetected" in data: + self._state = data["openWindowDetected"] + else: + self._state = False From 0faca57e8bb0b5f7b53de242a9b56d54a7eb9643 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 20 Dec 2019 14:28:23 -0500 Subject: [PATCH 2499/3953] Explicitly include Alexa interface for image_processing entities. (#30101) --- homeassistant/components/alexa/entities.py | 1 + tests/components/alexa/test_smart_home.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 2a3355434a3ca9..89ca646890b6ce 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -673,3 +673,4 @@ def interfaces(self): """Yield the supported interfaces.""" yield AlexaEventDetectionSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 4187c4a2c4f044..468652bf6d28ee 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2524,7 +2524,7 @@ async def test_image_processing(hass): assert appliance["friendlyName"] == "Test face" assert_endpoint_capabilities( - appliance, "Alexa.EventDetectionSensor", "Alexa.EndpointHealth" + appliance, "Alexa.EventDetectionSensor", "Alexa.EndpointHealth", "Alexa" ) From 27f35f86ad8883d6913ef161603201c829cb69a8 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 20 Dec 2019 20:29:12 +0100 Subject: [PATCH 2500/3953] Bump starlingbank to 3.2 (#30098) --- homeassistant/components/starlingbank/manifest.json | 4 +--- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/starlingbank/manifest.json b/homeassistant/components/starlingbank/manifest.json index 33dbb40f78a17b..d68b6ea125c47a 100644 --- a/homeassistant/components/starlingbank/manifest.json +++ b/homeassistant/components/starlingbank/manifest.json @@ -2,9 +2,7 @@ "domain": "starlingbank", "name": "Starlingbank", "documentation": "https://www.home-assistant.io/integrations/starlingbank", - "requirements": [ - "starlingbank==3.1" - ], + "requirements": ["starlingbank==3.2"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 5cecb39664964d..c7d10a8fc9dcea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1876,7 +1876,7 @@ sqlalchemy==1.3.11 starline==0.1.3 # homeassistant.components.starlingbank -starlingbank==3.1 +starlingbank==3.2 # homeassistant.components.statsd statsd==3.2.1 From b3098c9f2c55f0e5080ae3bea9457faa0192f743 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Dec 2019 20:42:13 +0100 Subject: [PATCH 2501/3953] Bump ring to 0.2.5 (#30103) --- homeassistant/components/ring/manifest.json | 8 ++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 47fc2f3a6a8bbd..5c23822fef9e8e 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,11 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": [ - "ring_doorbell==0.2.3" - ], - "dependencies": [ - "ffmpeg" - ], + "requirements": ["ring_doorbell==0.2.5"], + "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index c7d10a8fc9dcea..17a948f754bd11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1738,7 +1738,7 @@ rfk101py==0.0.1 rflink==0.0.46 # homeassistant.components.ring -ring_doorbell==0.2.3 +ring_doorbell==0.2.5 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 538262919ff530..be842e8e82f2bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ restrictedpython==5.0 rflink==0.0.46 # homeassistant.components.ring -ring_doorbell==0.2.3 +ring_doorbell==0.2.5 # homeassistant.components.yamaha rxv==0.6.0 From f229cf27967a1d27432472a72bdf42425cd63fa2 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 20 Dec 2019 20:29:12 +0100 Subject: [PATCH 2502/3953] Bump starlingbank to 3.2 (#30098) --- homeassistant/components/starlingbank/manifest.json | 4 +--- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/starlingbank/manifest.json b/homeassistant/components/starlingbank/manifest.json index 33dbb40f78a17b..d68b6ea125c47a 100644 --- a/homeassistant/components/starlingbank/manifest.json +++ b/homeassistant/components/starlingbank/manifest.json @@ -2,9 +2,7 @@ "domain": "starlingbank", "name": "Starlingbank", "documentation": "https://www.home-assistant.io/integrations/starlingbank", - "requirements": [ - "starlingbank==3.1" - ], + "requirements": ["starlingbank==3.2"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 9c2fb16fa16645..3ed237919efb06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1868,7 +1868,7 @@ sqlalchemy==1.3.11 starline==0.1.3 # homeassistant.components.starlingbank -starlingbank==3.1 +starlingbank==3.2 # homeassistant.components.statsd statsd==3.2.1 From d448ed9f6d9cb515db3b2299d4a3a91bbbef462f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Dec 2019 20:42:13 +0100 Subject: [PATCH 2503/3953] Bump ring to 0.2.5 (#30103) --- homeassistant/components/ring/manifest.json | 8 ++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 47fc2f3a6a8bbd..5c23822fef9e8e 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,11 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": [ - "ring_doorbell==0.2.3" - ], - "dependencies": [ - "ffmpeg" - ], + "requirements": ["ring_doorbell==0.2.5"], + "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 3ed237919efb06..e876b9144de548 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1730,7 +1730,7 @@ rfk101py==0.0.1 rflink==0.0.46 # homeassistant.components.ring -ring_doorbell==0.2.3 +ring_doorbell==0.2.5 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a59a9a57d547b5..86c3a657988485 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -549,7 +549,7 @@ restrictedpython==5.0 rflink==0.0.46 # homeassistant.components.ring -ring_doorbell==0.2.3 +ring_doorbell==0.2.5 # homeassistant.components.yamaha rxv==0.6.0 From 4ef04840e91330ff932b051ee42ba8634ca0026b Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 20 Dec 2019 20:49:07 +0000 Subject: [PATCH 2504/3953] Don't error on removal of an ignored homekit_controller config entry (#30083) * Don't error on ignored entries * Don't ever call async_remove_entry or async_unload_entry for an unignored ignore config entry --- homeassistant/config_entries.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1a010b38e7052a..8b314547d9c67a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -246,6 +246,10 @@ async def async_unload( Returns if unload is possible and was successful. """ + if self.source == SOURCE_IGNORE: + self.state = ENTRY_STATE_NOT_LOADED + return True + if integration is None: integration = await loader.async_get_integration(hass, self.domain) @@ -292,6 +296,9 @@ async def async_unload( async def async_remove(self, hass: HomeAssistant) -> None: """Invoke remove callback on component.""" + if self.source == SOURCE_IGNORE: + return + integration = await loader.async_get_integration(hass, self.domain) component = integration.get_component() if not hasattr(component, "async_remove_entry"): From de94afd09094caaf413cb1af69ca53ec61d393a8 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 20 Dec 2019 22:00:31 +0100 Subject: [PATCH 2505/3953] add --show-diff-on-failure to pre-commit (#30097) This makes the traceback on a failing CI pipeline much more useful. --- azure-pipelines-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 78de90c85528dc..546b63950fe4d1 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -54,7 +54,7 @@ stages: displayName: 'Run bandit' - script: | . venv/bin/activate - pre-commit run isort --all-files + pre-commit run isort --all-files --show-diff-on-failure displayName: 'Run isort' - script: | . venv/bin/activate @@ -97,7 +97,7 @@ stages: pre-commit install-hooks --config .pre-commit-config-all.yaml - script: | . venv/bin/activate - pre-commit run black --all-files + pre-commit run black --all-files --show-diff-on-failure displayName: 'Check Black formatting' - stage: 'Tests' From ecdc1adf90ef1dc7033eae6362a666ec37224d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 20 Dec 2019 23:35:02 +0200 Subject: [PATCH 2506/3953] Upgrade mypy to 0.761 (#30104) https://mypy-lang.blogspot.com/2019/12/mypy-0760-released.html https://github.com/python/mypy/releases/tag/v0.761 --- homeassistant/helpers/config_validation.py | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 0f5b4e13dc21ea..5787db65102d83 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -10,7 +10,7 @@ from numbers import Number import os import re -from socket import _GLOBAL_DEFAULT_TIMEOUT +from socket import _GLOBAL_DEFAULT_TIMEOUT # type: ignore # private, not in typeshed from typing import Any, Callable, Dict, List, Optional, TypeVar, Union from urllib.parse import urlparse from uuid import UUID diff --git a/requirements_test.txt b/requirements_test.txt index 328ad1d5b9a538..37268e70726937 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ asynctest==0.13.0 codecov==2.0.15 mock-open==1.3.1 -mypy==0.750 +mypy==0.761 pre-commit==1.20.0 pylint==2.4.4 astroid==2.3.3 From 660468079310ec7432e890436b314526fdd4ab7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 21 Dec 2019 09:23:48 +0200 Subject: [PATCH 2507/3953] Helpers type hint improvements (#30106) --- homeassistant/helpers/device_registry.py | 12 ++++++------ homeassistant/helpers/discovery.py | 22 ++++++++++++++++------ homeassistant/helpers/entity_component.py | 13 +++++++------ homeassistant/helpers/entity_platform.py | 11 ++++++----- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 2ff444da89f384..4818de83cb9246 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -2,7 +2,7 @@ from asyncio import Event from collections import OrderedDict import logging -from typing import List, Optional, cast +from typing import Any, Dict, List, Optional, cast import uuid import attr @@ -48,7 +48,7 @@ class DeviceEntry: is_new = attr.ib(type=bool, default=False) -def format_mac(mac): +def format_mac(mac: str) -> str: """Format the mac address string for entry into dev reg.""" to_test = mac @@ -260,7 +260,7 @@ def _async_update_device( return new - def async_remove_device(self, device_id): + def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" del self.devices[device_id] self.hass.bus.async_fire( @@ -298,12 +298,12 @@ async def async_load(self): self.devices = devices @callback - def async_schedule_save(self): + def async_schedule_save(self) -> None: """Schedule saving the device registry.""" self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self): + def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]: """Return data of device registry to store in a file.""" data = {} @@ -327,7 +327,7 @@ def _data_to_save(self): return data @callback - def async_clear_config_entry(self, config_entry_id): + def async_clear_config_entry(self, config_entry_id: str) -> None: """Clear config entry from registry entries.""" remove = [] for dev_id, device in self.devices.items(): diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 8e4def77440a8b..a6162dbde557f7 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -5,6 +5,8 @@ - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ +from typing import Callable, Collection, Union + from homeassistant import core, setup from homeassistant.const import ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED from homeassistant.exceptions import HomeAssistantError @@ -18,7 +20,9 @@ @bind_hass -def listen(hass, service, callback): +def listen( + hass: core.HomeAssistant, service: Union[str, Collection[str]], callback: Callable +) -> None: """Set up listener for discovery of specific service. Service can be a string or a list/tuple. @@ -28,7 +32,9 @@ def listen(hass, service, callback): @core.callback @bind_hass -def async_listen(hass, service, callback): +def async_listen( + hass: core.HomeAssistant, service: Union[str, Collection[str]], callback: Callable +) -> None: """Set up listener for discovery of specific service. Service can be a string or a list/tuple. @@ -39,7 +45,7 @@ def async_listen(hass, service, callback): service = tuple(service) @core.callback - def discovery_event_listener(event): + def discovery_event_listener(event: core.Event) -> None: """Listen for discovery events.""" if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service: hass.async_add_job( @@ -73,7 +79,9 @@ async def async_discover(hass, service, discovered, component, hass_config): @bind_hass -def listen_platform(hass, component, callback): +def listen_platform( + hass: core.HomeAssistant, component: str, callback: Callable +) -> None: """Register a platform loader listener.""" run_callback_threadsafe( hass.loop, async_listen_platform, hass, component, callback @@ -81,7 +89,9 @@ def listen_platform(hass, component, callback): @bind_hass -def async_listen_platform(hass, component, callback): +def async_listen_platform( + hass: core.HomeAssistant, component: str, callback: Callable +) -> None: """Register a platform loader listener. This method must be run in the event loop. @@ -89,7 +99,7 @@ def async_listen_platform(hass, component, callback): service = EVENT_LOAD_PLATFORM.format(component) @core.callback - def discovery_platform_listener(event): + def discovery_platform_listener(event: core.Event) -> None: """Listen for platform discovery events.""" if event.data.get(ATTR_SERVICE) != service: return diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index becd96bf5f3807..84aa8becafdd06 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -5,13 +5,14 @@ import logging from homeassistant import config as conf_util +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_NAMESPACE, CONF_SCAN_INTERVAL, ENTITY_MATCH_ALL, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.config_validation import make_entity_service_schema @@ -29,7 +30,7 @@ @bind_hass -async def async_update_entity(hass, entity_id): +async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: """Trigger an update for an entity.""" domain = entity_id.split(".", 1)[0] entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain) @@ -158,7 +159,7 @@ async def async_setup_entry(self, config_entry): return await self._platforms[key].async_setup_entry(config_entry) - async def async_unload_entry(self, config_entry): + async def async_unload_entry(self, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" key = config_entry.entry_id @@ -237,7 +238,7 @@ async def async_setup_platform( await self._platforms[key].async_setup(platform_config, discovery_info) @callback - def _async_update_group(self): + def _async_update_group(self) -> None: """Set up and/or update component group. This method must be run in the event loop. @@ -265,7 +266,7 @@ def _async_update_group(self): ) ) - async def _async_reset(self): + async def _async_reset(self) -> None: """Remove entities and reset the entity component to initial values. This method must be run in the event loop. @@ -283,7 +284,7 @@ async def _async_reset(self): "group", "remove", dict(object_id=slugify(self.group_name)) ) - async def async_remove_entity(self, entity_id): + async def async_remove_entity(self, entity_id: str) -> None: """Remove an entity managed by one of the platforms.""" for platform in self._platforms.values(): if entity_id in platform.entities: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 133d1a5841f5b9..e171a4cade8754 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,6 +1,7 @@ """Class to manage the entities for a single platform.""" import asyncio from contextvars import ContextVar +from datetime import datetime from typing import Optional from homeassistant.const import DEVICE_DEFAULT_NAME @@ -64,14 +65,14 @@ def __init__( # which powers entity_component.add_entities if platform is None: self.parallel_updates = None - self.parallel_updates_semaphore = None + self.parallel_updates_semaphore: Optional[asyncio.Semaphore] = None return self.parallel_updates = getattr(platform, "PARALLEL_UPDATES", None) # semaphore will be created on demand self.parallel_updates_semaphore = None - def _get_parallel_updates_semaphore(self): + def _get_parallel_updates_semaphore(self) -> asyncio.Semaphore: """Get or create a semaphore for parallel updates.""" if self.parallel_updates_semaphore is None: self.parallel_updates_semaphore = asyncio.Semaphore( @@ -406,7 +407,7 @@ async def _async_add_entity( await entity.async_update_ha_state() - async def async_reset(self): + async def async_reset(self) -> None: """Remove all entities and reset data. This method must be run in the event loop. @@ -426,7 +427,7 @@ async def async_reset(self): self._async_unsub_polling() self._async_unsub_polling = None - async def async_remove_entity(self, entity_id): + async def async_remove_entity(self, entity_id: str) -> None: """Remove entity id from platform.""" await self.entities[entity_id].async_remove() @@ -437,7 +438,7 @@ async def async_remove_entity(self, entity_id): self._async_unsub_polling() self._async_unsub_polling = None - async def _update_entity_states(self, now): + async def _update_entity_states(self, now: datetime) -> None: """Update the states of all the polling entities. To protect from flooding the executor, we will update async entities From 0fc92928a4544a3f4d3021f7a0923e7b950c4e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Z=C3=A1hradn=C3=ADk?= Date: Sat, 21 Dec 2019 10:54:48 +0100 Subject: [PATCH 2508/3953] Add device class attribute to modbus sensors (#30030) --- .../components/modbus/binary_sensor.py | 24 +++++++++++++++---- homeassistant/components/modbus/sensor.py | 14 +++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index faf5160d7e5d79..9a431d24b0c528 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -1,10 +1,15 @@ """Support for Modbus Coil sensors.""" import logging +from typing import Optional import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from homeassistant.const import CONF_NAME, CONF_SLAVE +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorDevice, +) +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE from homeassistant.helpers import config_validation as cv from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN @@ -20,6 +25,7 @@ { vol.Required(CONF_COIL): cv.positive_int, vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_SLAVE): cv.positive_int, } @@ -35,7 +41,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hub = hass.data[MODBUS_DOMAIN][coil.get(CONF_HUB)] sensors.append( ModbusCoilSensor( - hub, coil.get(CONF_NAME), coil.get(CONF_SLAVE), coil.get(CONF_COIL) + hub, + coil.get(CONF_NAME), + coil.get(CONF_SLAVE), + coil.get(CONF_COIL), + coil.get(CONF_DEVICE_CLASS), ) ) @@ -45,12 +55,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ModbusCoilSensor(BinarySensorDevice): """Modbus coil sensor.""" - def __init__(self, hub, name, slave, coil): + def __init__(self, hub, name, slave, coil, device_class): """Initialize the Modbus coil sensor.""" self._hub = hub self._name = name self._slave = int(slave) if slave else None self._coil = int(coil) + self._device_class = device_class self._value = None @property @@ -63,6 +74,11 @@ def is_on(self): """Return the state of the sensor.""" return self._value + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class + def update(self): """Update the state of the sensor.""" result = self._hub.read_coils(self._slave, self._coil, 1) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 86f6445b8d6972..5b04a898ab92ed 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -1,12 +1,13 @@ """Support for Modbus Register sensors.""" import logging import struct -from typing import Any, Union +from typing import Any, Optional, Union import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_NAME, CONF_OFFSET, CONF_SLAVE, @@ -67,6 +68,7 @@ def number(value: Any) -> Union[int, float]: vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In( [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM] ), + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_OFFSET, default=0): number, vol.Optional(CONF_PRECISION, default=0): cv.positive_int, @@ -139,6 +141,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): register.get(CONF_OFFSET), structure, register.get(CONF_PRECISION), + register.get(CONF_DEVICE_CLASS), ) ) @@ -164,6 +167,7 @@ def __init__( offset, structure, precision, + device_class, ): """Initialize the modbus register sensor.""" self._hub = hub @@ -178,6 +182,7 @@ def __init__( self._offset = offset self._precision = precision self._structure = structure + self._device_class = device_class self._value = None async def async_added_to_hass(self): @@ -202,6 +207,11 @@ def unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class + def update(self): """Update the state of the sensor.""" if self._register_type == REGISTER_TYPE_INPUT: From 3911f24f75d324f7f091126d17ee4e7c1634447c Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sat, 21 Dec 2019 11:20:31 +0100 Subject: [PATCH 2509/3953] Upgrade python-velbus (#30110) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 1d9401f6cfe827..71a2ac2b9933aa 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -3,7 +3,7 @@ "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", "requirements": [ - "python-velbus==2.0.27" + "python-velbus==2.0.30" ], "config_flow": true, "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 17a948f754bd11..822e92ef759901 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1623,7 +1623,7 @@ python-telnet-vlc==1.0.4 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.27 +python-velbus==2.0.30 # homeassistant.components.vlc python-vlc==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index be842e8e82f2bf..a7f443e0ccfd97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -528,7 +528,7 @@ python-miio==0.4.8 python-nest==4.1.0 # homeassistant.components.velbus -python-velbus==2.0.27 +python-velbus==2.0.30 # homeassistant.components.awair python_awair==0.0.4 From b41480ae466a99c977e138d335e552107e9865ac Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 21 Dec 2019 10:22:07 +0000 Subject: [PATCH 2510/3953] Add a config entry mechanism to rediscover a discovery that was ignored (#30099) * Mechanism to rediscover a discovery that was ignored * Add core config entry tests for new rediscover step * Add tests for homekit_controller implementation of async_step_rediscover * Rename rediscover to unignore * Comment the new ignore/unignore mechanisms --- .../homekit_controller/config_flow.py | 32 +++++ homeassistant/config_entries.py | 26 ++++ .../homekit_controller/test_config_flow.py | 82 ++++++++++++ tests/test_config_entries.py | 117 ++++++++++++++++++ 4 files changed, 257 insertions(+) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 6cc724e9fe5ba2..3f230d923c7acf 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -108,6 +108,38 @@ async def async_step_user(self, user_input=None): ), ) + async def async_step_unignore(self, user_input): + """Rediscover a previously ignored discover.""" + unique_id = user_input["unique_id"] + await self.async_set_unique_id(unique_id) + + records = await self.hass.async_add_executor_job(self.controller.discover, 5) + for record in records: + if normalize_hkid(record["id"]) != unique_id: + continue + return await self.async_step_zeroconf( + { + "host": record["address"], + "port": record["port"], + "hostname": record["name"], + "type": "_hap._tcp.local.", + "name": record["name"], + "properties": { + "md": record["md"], + "pv": record["pv"], + "id": unique_id, + "c#": record["c#"], + "s#": record["s#"], + "ff": record["ff"], + "ci": record["ci"], + "sf": record["sf"], + "sh": "", + }, + } + ) + + return self.async_abort(reason="no_devices") + async def async_step_zeroconf(self, discovery_info): """Handle a discovered HomeKit accessory. diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8b314547d9c67a..942998767a1bf2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -24,8 +24,17 @@ SOURCE_SSDP = "ssdp" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" + +# If a user wants to hide a discovery from the UI they can "Ignore" it. The config_entries/ignore_flow +# websocket command creates a config entry with this source and while it exists normal discoveries +# with the same unique id are ignored. SOURCE_IGNORE = "ignore" +# This is used when a user uses the "Stop Ignoring" button in the UI (the +# config_entries/ignore_flow websocket command). It's triggered after the "ignore" config entry has +# been removed and unloaded. +SOURCE_UNIGNORE = "unignore" + HANDLERS = Registry() STORAGE_KEY = "core.config_entries" @@ -461,6 +470,19 @@ async def async_remove(self, entry_id: str) -> Dict[str, Any]: dev_reg.async_clear_config_entry(entry_id) ent_reg.async_clear_config_entry(entry_id) + # After we have fully removed an "ignore" config entry we can try and rediscover it so that a + # user is able to immediately start configuring it. We do this by starting a new flow with + # the 'unignore' step. If the integration doesn't implement async_step_unignore then + # this will be a no-op. + if entry.source == SOURCE_IGNORE: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + entry.domain, + context={"source": SOURCE_UNIGNORE}, + data={"unique_id": entry.unique_id}, + ) + ) + return {"require_restart": not unload_success} async def async_initialize(self) -> None: @@ -827,6 +849,10 @@ async def async_step_ignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) return self.async_create_entry(title="Ignored", data={}) + async def async_step_unignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: + """Rediscover a config entry by it's unique_id.""" + return self.async_abort(reason="not_implemented") + class OptionsFlowManager: """Flow to set options for a configuration entry.""" diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 4733581f1368e9..56c1c30e8f353b 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -836,3 +836,85 @@ async def test_parse_overlapping_homekit_json(hass): "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", } + + +async def test_unignore_works(hass): + """Test rediscovery triggered disovers work.""" + discovery_info = { + "name": "TestDevice", + "address": "127.0.0.1", + "port": 8080, + "md": "TestDevice", + "pv": "1.0", + "id": "00:00:00:00:00:00", + "c#": 1, + "s#": 1, + "ff": 0, + "ci": 0, + "sf": 1, + } + + pairing = mock.Mock(pairing_data={"AccessoryPairingID": "00:00:00:00:00:00"}) + pairing.list_accessories_and_characteristics.return_value = [ + { + "aid": 1, + "services": [ + { + "characteristics": [{"type": "23", "value": "Koogeek-LS1-20833F"}], + "type": "3e", + } + ], + } + ] + + flow = _setup_flow_handler(hass) + + flow.controller.pairings = {"00:00:00:00:00:00": pairing} + flow.controller.discover.return_value = [discovery_info] + + result = await flow.async_step_unignore({"unique_id": "00:00:00:00:00:00"}) + assert result["type"] == "form" + assert result["step_id"] == "pair" + assert flow.context == { + "hkid": "00:00:00:00:00:00", + "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", + } + + # User initiates pairing by clicking on 'configure' - device enters pairing mode and displays code + result = await flow.async_step_pair({}) + assert result["type"] == "form" + assert result["step_id"] == "pair" + assert flow.controller.start_pairing.call_count == 1 + + # Pairing finalized + result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + assert result["type"] == "create_entry" + assert result["title"] == "Koogeek-LS1-20833F" + assert result["data"] == pairing.pairing_data + + +async def test_unignore_ignores_missing_devices(hass): + """Test rediscovery triggered disovers handle devices that have gone away.""" + discovery_info = { + "name": "TestDevice", + "address": "127.0.0.1", + "port": 8080, + "md": "TestDevice", + "pv": "1.0", + "id": "00:00:00:00:00:00", + "c#": 1, + "s#": 1, + "ff": 0, + "ci": 0, + "sf": 1, + } + + flow = _setup_flow_handler(hass) + flow.controller.discover.return_value = [discovery_info] + + result = await flow.async_step_unignore({"unique_id": "00:00:00:00:00:01"}) + assert result["type"] == "abort" + assert flow.context == { + "unique_id": "00:00:00:00:00:01", + } diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 19f84e94570e91..5b694b2de87e50 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1186,3 +1186,120 @@ async def async_step_user(self, user_input=None): assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" + + +async def test_unignore_step_form(hass, manager): + """Test that we can ignore flows that are in progress and have a unique ID, then rediscover them.""" + async_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_unignore(self, user_input): + unique_id = user_input["unique_id"] + await self.async_set_unique_id(unique_id) + return self.async_show_form(step_id="discovery") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + result = await manager.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": "mock-unique-id"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entry = hass.config_entries.async_entries("comp")[0] + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id" + assert entry.domain == "comp" + + await manager.async_remove(entry.entry_id) + + # Right after removal there shouldn't be an entry or active flows + assert len(hass.config_entries.async_entries("comp")) == 0 + assert len(hass.config_entries.flow.async_progress()) == 0 + + # But after a 'tick' the unignore step has run and we can see an active flow again. + await hass.async_block_till_done() + assert len(hass.config_entries.flow.async_progress()) == 1 + + # and still not config entries + assert len(hass.config_entries.async_entries("comp")) == 0 + + +async def test_unignore_create_entry(hass, manager): + """Test that we can ignore flows that are in progress and have a unique ID, then rediscover them.""" + async_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + async def async_step_unignore(self, user_input): + unique_id = user_input["unique_id"] + await self.async_set_unique_id(unique_id) + return self.async_create_entry(title="yo", data={}) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + result = await manager.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": "mock-unique-id"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entry = hass.config_entries.async_entries("comp")[0] + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id" + assert entry.domain == "comp" + + await manager.async_remove(entry.entry_id) + + # Right after removal there shouldn't be an entry or flow + assert len(hass.config_entries.flow.async_progress()) == 0 + assert len(hass.config_entries.async_entries("comp")) == 0 + + # But after a 'tick' the unignore step has run and we can see a config entry. + await hass.async_block_till_done() + entry = hass.config_entries.async_entries("comp")[0] + assert entry.source == "unignore" + assert entry.unique_id == "mock-unique-id" + assert entry.title == "yo" + + # And still no active flow + assert len(hass.config_entries.flow.async_progress()) == 0 + + +async def test_unignore_default_impl(hass, manager): + """Test that resdicovery is a no-op by default.""" + async_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_integration(hass, MockModule("comp", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + + VERSION = 1 + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + result = await manager.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": "mock-unique-id"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entry = hass.config_entries.async_entries("comp")[0] + assert entry.source == "ignore" + assert entry.unique_id == "mock-unique-id" + assert entry.domain == "comp" + + await manager.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries("comp")) == 0 + assert len(hass.config_entries.flow.async_progress()) == 0 From fb3bb8220b99ad0b3a7ad23accf7c7a31478c52f Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 21 Dec 2019 16:26:58 -0500 Subject: [PATCH 2511/3953] Implement ZHA entity classes registry (#30108) * ZHA Entity registry. Match a zha_device and channels to a ZHA entity. * Refactor ZHA sensor to use registry. * Remove sensor_types registry. * Fix ZHA device tracker battery remaining. * Remove should_poll/force_update attributes. * Fix binary_sensor regression. * isort. * Pylint. * Don't access protected members. * Address comments and fix spelling. * Make pylint happy again. --- homeassistant/components/zha/binary_sensor.py | 4 +- .../components/zha/core/channels/__init__.py | 2 - homeassistant/components/zha/core/const.py | 18 +- .../components/zha/core/discovery.py | 7 - .../components/zha/core/registries.py | 118 +++++-- .../components/zha/device_tracker.py | 4 +- homeassistant/components/zha/sensor.py | 289 +++++++++--------- tests/components/zha/test_registries.py | 165 ++++++++++ 8 files changed, 415 insertions(+), 192 deletions(-) create mode 100644 tests/components/zha/test_registries.py diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 24c2b92e739277..e6176fe9da338f 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -18,7 +18,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( - CHANNEL_ATTRIBUTE, + CHANNEL_OCCUPANCY, CHANNEL_ON_OFF, CHANNEL_ZONE, DATA_ZHA, @@ -111,7 +111,7 @@ def __init__(self, **kwargs): self._device_state_attributes = {} 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._attr_channel = self.cluster_channels.get(CHANNEL_OCCUPANCY) 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 715156c43eda64..4013f05e0b67c3 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -17,7 +17,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from ..const import ( - CHANNEL_ATTRIBUTE, CHANNEL_EVENT_RELAY, CHANNEL_ZDO, REPORT_CONFIG_DEFAULT, @@ -280,7 +279,6 @@ def __getattr__(self, name): 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): diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 24c0126ba600a0..6c991a319acae0 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -50,10 +50,16 @@ CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement" CHANNEL_EVENT_RELAY = "event_relay" CHANNEL_FAN = "fan" +CHANNEL_HUMIDITY = "humidity" CHANNEL_IAS_WD = "ias_wd" +CHANNEL_ILLUMINANCE = "illuminance" CHANNEL_LEVEL = ATTR_LEVEL +CHANNEL_OCCUPANCY = "occupancy" CHANNEL_ON_OFF = "on_off" CHANNEL_POWER_CONFIGURATION = "power" +CHANNEL_PRESSURE = "pressure" +CHANNEL_SMARTENERGY_METERING = "smartenergy_metering" +CHANNEL_TEMPERATURE = "temperature" CHANNEL_ZDO = "zdo" CHANNEL_ZONE = ZONE = "ias_zone" @@ -166,15 +172,15 @@ def list(cls): SENSOR_ACCELERATION = "acceleration" SENSOR_BATTERY = "battery" -SENSOR_ELECTRICAL_MEASUREMENT = "electrical_measurement" +SENSOR_ELECTRICAL_MEASUREMENT = CHANNEL_ELECTRICAL_MEASUREMENT SENSOR_GENERIC = "generic" -SENSOR_HUMIDITY = "humidity" -SENSOR_ILLUMINANCE = "illuminance" +SENSOR_HUMIDITY = CHANNEL_HUMIDITY +SENSOR_ILLUMINANCE = CHANNEL_ILLUMINANCE SENSOR_METERING = "metering" -SENSOR_OCCUPANCY = "occupancy" +SENSOR_OCCUPANCY = CHANNEL_OCCUPANCY SENSOR_OPENING = "opening" -SENSOR_PRESSURE = "pressure" -SENSOR_TEMPERATURE = "temperature" +SENSOR_PRESSURE = CHANNEL_PRESSURE +SENSOR_TEMPERATURE = CHANNEL_TEMPERATURE SENSOR_TYPE = "sensor_type" SIGNAL_ATTR_UPDATED = "attribute_updated" diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index e23862a7d3e891..108bd841252c08 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -12,7 +12,6 @@ from homeassistant import const as ha_const 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 @@ -21,7 +20,6 @@ COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, - SENSOR_GENERIC, SENSOR_TYPE, UNKNOWN, ZHA_DISCOVERY_NEW, @@ -34,7 +32,6 @@ EVENT_RELAY_CLUSTERS, OUTPUT_CHANNEL_ONLY_CLUSTERS, REMOTE_DEVICE_TYPES, - SENSOR_TYPES, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, ZIGBEE_CHANNEL_REGISTRY, @@ -291,10 +288,6 @@ def _async_handle_single_cluster_match( "component": component, } - if component == SENSOR: - discovery_info.update( - {SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, SENSOR_GENERIC)} - ) if component == BINARY_SENSOR: discovery_info.update( {SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster.cluster_id, UNKNOWN)} diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 13688a6c4204d3..c2d3b13e375bfb 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -5,7 +5,9 @@ https://home-assistant.io/integrations/zha/ """ import collections +from typing import Callable, Set +import attr import bellows.ezsp import bellows.zigbee.application import zigpy.profiles.zha @@ -31,21 +33,14 @@ from .const import ( CONTROLLER, SENSOR_ACCELERATION, - SENSOR_BATTERY, - SENSOR_ELECTRICAL_MEASUREMENT, - SENSOR_HUMIDITY, - SENSOR_ILLUMINANCE, - SENSOR_METERING, SENSOR_OCCUPANCY, SENSOR_OPENING, - SENSOR_PRESSURE, - SENSOR_TEMPERATURE, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, ZONE, RadioType, ) -from .decorators import DictRegistry, SetRegistry +from .decorators import CALLABLE_T, DictRegistry, SetRegistry BINARY_SENSOR_CLUSTERS = SetRegistry() BINARY_SENSOR_TYPES = {} @@ -60,7 +55,6 @@ OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry() RADIO_TYPES = {} REMOTE_DEVICE_TYPES = collections.defaultdict(list) -SENSOR_TYPES = {} SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} SWITCH_CLUSTERS = SetRegistry() @@ -176,19 +170,6 @@ def establish_device_mappings(): {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, - } - ) - zha = zigpy.profiles.zha REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_CONTROLLER) REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_DIMMER_SWITCH) @@ -207,3 +188,96 @@ def establish_device_mappings(): REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROL_BRIDGE) REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROLLER) REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.SCENE_CONTROLLER) + + +@attr.s(frozen=True) +class MatchRule: + """Match a ZHA Entity to a channel name or generic id.""" + + channel_names: Set[str] = attr.ib(factory=frozenset, converter=frozenset) + generic_ids: Set[str] = attr.ib(factory=frozenset, converter=frozenset) + manufacturer: str = attr.ib(default=None) + model: str = attr.ib(default=None) + + +class ZHAEntityRegistry: + """Channel to ZHA Entity mapping.""" + + def __init__(self): + """Initialize Registry instance.""" + self._strict_registry = collections.defaultdict(dict) + self._loose_registry = collections.defaultdict(dict) + + def get_entity( + self, component: str, zha_device, chnls: list, default: CALLABLE_T = None + ) -> CALLABLE_T: + """Match a ZHA Channels to a ZHA Entity class.""" + for match in self._strict_registry[component]: + if self._strict_matched(zha_device, chnls, match): + return self._strict_registry[component][match] + + return default + + def strict_match( + self, component: str, rule: MatchRule + ) -> Callable[[CALLABLE_T], CALLABLE_T]: + """Decorate a strict match rule.""" + + def decorator(zha_ent: CALLABLE_T) -> CALLABLE_T: + """Register a strict match rule. + + All non empty fields of a match rule must match. + """ + self._strict_registry[component][rule] = zha_ent + return zha_ent + + return decorator + + def loose_match( + self, component: str, rule: MatchRule + ) -> Callable[[CALLABLE_T], CALLABLE_T]: + """Decorate a loose match rule.""" + + def decorator(zha_entity: CALLABLE_T) -> CALLABLE_T: + """Register a loose match rule. + + All non empty fields of a match rule must match. + """ + self._loose_registry[component][rule] = zha_entity + return zha_entity + + return decorator + + def _strict_matched(self, zha_device, chnls: dict, rule: MatchRule) -> bool: + """Return True if this device matches the criteria.""" + return all(self._matched(zha_device, chnls, rule)) + + def _loose_matched(self, zha_device, chnls: dict, rule: MatchRule) -> bool: + """Return True if this device matches the criteria.""" + return any(self._matched(zha_device, chnls, rule)) + + @staticmethod + def _matched(zha_device, chnls: list, rule: MatchRule) -> bool: + """Return a list of field matches.""" + if not any(attr.asdict(rule).values()): + return [False] + + matches = [] + if rule.channel_names: + channel_names = {ch.name for ch in chnls} + matches.append(rule.channel_names.issubset(channel_names)) + + if rule.generic_ids: + all_generic_ids = {ch.generic_id for ch in chnls} + matches.append(rule.generic_ids.issubset(all_generic_ids)) + + if rule.manufacturer: + matches.append(zha_device.manufacturer == rule.manufacturer) + + if rule.model: + matches.append(zha_device.model == rule.model) + + return matches + + +ZHA_ENTITIES = ZHAEntityRegistry() diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 60a1f6c3c4055b..e7663b35686f30 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -15,7 +15,7 @@ ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity -from .sensor import battery_percentage_remaining_formatter +from .sensor import Battery _LOGGER = logging.getLogger(__name__) @@ -100,7 +100,7 @@ def async_battery_percentage_remaining_updated(self, value): """Handle tracking.""" self.debug("battery_percentage_remaining updated: %s", value) self._connected = True - self._battery_level = battery_percentage_remaining_formatter(value) + self._battery_level = Battery.formatter(value) self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index b260dfc5459bc5..133e82e69140ae 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -1,4 +1,5 @@ """Sensors on Zigbee Home Automation networks.""" +import functools import logging import numbers @@ -16,25 +17,20 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( - CHANNEL_ATTRIBUTE, CHANNEL_ELECTRICAL_MEASUREMENT, + CHANNEL_HUMIDITY, + CHANNEL_ILLUMINANCE, CHANNEL_POWER_CONFIGURATION, + CHANNEL_PRESSURE, + CHANNEL_SMARTENERGY_METERING, + CHANNEL_TEMPERATURE, DATA_ZHA, DATA_ZHA_DISPATCHERS, - SENSOR_BATTERY, - SENSOR_ELECTRICAL_MEASUREMENT, - SENSOR_GENERIC, - SENSOR_HUMIDITY, - SENSOR_ILLUMINANCE, - SENSOR_METERING, - SENSOR_PRESSURE, - SENSOR_TEMPERATURE, - SENSOR_TYPE, SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR, - UNKNOWN, ZHA_DISCOVERY_NEW, ) +from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES, MatchRule from .entity import ZhaEntity PARALLEL_UPDATES = 5 @@ -56,115 +52,8 @@ 255: "Unknown", } - -# Formatter functions -def pass_through_formatter(value): - """No op update function.""" - return value - - -def illuminance_formatter(value): - """Convert Illimination data.""" - if value is None: - return None - return round(pow(10, ((value - 1) / 10000)), 1) - - -def temperature_formatter(value): - """Convert temperature data.""" - if value is None: - return None - return round(value / 100, 1) - - -def humidity_formatter(value): - """Return the state of the entity.""" - if value is None: - return None - return round(float(value) / 100, 1) - - -def active_power_formatter(value): - """Return the state of the entity.""" - if value is None: - return None - return round(float(value) / 10, 1) - - -def pressure_formatter(value): - """Return the state of the entity.""" - if value is None: - return None - - return round(float(value)) - - -def battery_percentage_remaining_formatter(value): - """Return the state of the entity.""" - # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ - if not isinstance(value, numbers.Number) or value == -1: - return value - value = value / 2 - value = int(round(value)) - return value - - -async def async_battery_device_state_attr_provider(channel): - """Return device statr attrs for battery sensors.""" - state_attrs = {} - battery_size = await channel.get_attribute_value("battery_size") - if battery_size is not None: - state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown") - battery_quantity = await channel.get_attribute_value("battery_quantity") - if battery_quantity is not None: - state_attrs["battery_quantity"] = battery_quantity - return state_attrs - - -FORMATTER_FUNC_REGISTRY = { - 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 = { - SENSOR_HUMIDITY: "%", - SENSOR_TEMPERATURE: TEMP_CELSIUS, - SENSOR_PRESSURE: "hPa", - SENSOR_ILLUMINANCE: "lx", - SENSOR_ELECTRICAL_MEASUREMENT: POWER_WATT, - SENSOR_GENERIC: None, - SENSOR_BATTERY: "%", -} - -CHANNEL_REGISTRY = { - SENSOR_ELECTRICAL_MEASUREMENT: CHANNEL_ELECTRICAL_MEASUREMENT, - SENSOR_BATTERY: CHANNEL_POWER_CONFIGURATION, -} - -POLLING_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: True} - -FORCE_UPDATE_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: False} - -DEVICE_CLASS_REGISTRY = { - UNKNOWN: None, - 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 = { - SENSOR_BATTERY: async_battery_device_state_attr_provider -} +CHANNEL_ST_HUMIDITY_CLUSTER = f"channel_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}" +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -206,43 +95,34 @@ async def _async_setup_entities( async def make_sensor(discovery_info): """Create ZHA sensors factory.""" - return Sensor(**discovery_info) + + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] + + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, Sensor) + return entity(**discovery_info) class Sensor(ZhaEntity): """Base ZHA sensor.""" + _decimals = 1 + _device_class = None + _divisor = 1 _domain = DOMAIN + _multiplier = 1 + _unit = None 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, SENSOR_GENERIC) - self._channel = self.cluster_channels.get( - CHANNEL_REGISTRY.get(self._sensor_type, CHANNEL_ATTRIBUTE) - ) - if self._sensor_type == SENSOR_METERING: - self._unit = self._channel.unit_of_measurement - self._formatter_function = self._channel.formatter_function - else: - self._unit = UNIT_REGISTRY.get(self._sensor_type) - self._formatter_function = FORMATTER_FUNC_REGISTRY.get( - self._sensor_type, pass_through_formatter - ) - self._force_update = FORCE_UPDATE_REGISTRY.get(self._sensor_type, False) - self._should_poll = POLLING_REGISTRY.get(self._sensor_type, False) - self._device_class = DEVICE_CLASS_REGISTRY.get(self._sensor_type, None) - self.state_attr_provider = DEVICE_STATE_ATTR_PROVIDER_REGISTRY.get( - self._sensor_type, None - ) + self._channel = channels[0] async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() - if self.state_attr_provider is not None: - self._device_state_attributes = await self.state_attr_provider( - self._channel - ) + self._device_state_attributes = await self.async_state_attr_provider() + await self.async_accept_signal( self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state ) @@ -271,14 +151,9 @@ def state(self) -> str: def async_set_state(self, state): """Handle state update from channel.""" - # this is necessary because HA saves the unit based on what shows in - # the UI and not based on what the sensor has configured so we need - # to flip it back after state restoration - if self._sensor_type == SENSOR_METERING: - self._unit = self._channel.unit_of_measurement - else: - self._unit = UNIT_REGISTRY.get(self._sensor_type) - self._state = self._formatter_function(state) + if state is not None: + state = self.formatter(state) + self._state = state self.async_schedule_update_ha_state() @callback @@ -286,3 +161,115 @@ def async_restore_last_state(self, last_state): """Restore previous state.""" self._state = last_state.state self._unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + @callback + async def async_state_attr_provider(self): + """Initialize device state attributes.""" + return {} + + def formatter(self, value): + """Numeric pass-through formatter.""" + if self._decimals > 0: + return round( + float(value * self._multiplier) / self._divisor, self._decimals + ) + return round(float(value * self._multiplier) / self._divisor) + + +@STRICT_MATCH(MatchRule(channel_names={CHANNEL_POWER_CONFIGURATION})) +class Battery(Sensor): + """Battery sensor of power configuration cluster.""" + + _device_class = DEVICE_CLASS_BATTERY + _unit = "%" + + @staticmethod + def formatter(value): + """Return the state of the entity.""" + # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ + if not isinstance(value, numbers.Number) or value == -1: + return value + value = round(value / 2) + return value + + async def async_state_attr_provider(self): + """Return device state attrs for battery sensors.""" + state_attrs = {} + battery_size = await self._channel.get_attribute_value("battery_size") + if battery_size is not None: + state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown") + battery_quantity = await self._channel.get_attribute_value("battery_quantity") + if battery_quantity is not None: + state_attrs["battery_quantity"] = battery_quantity + return state_attrs + + +@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ELECTRICAL_MEASUREMENT})) +class ElectricalMeasurement(Sensor): + """Active power measurement.""" + + _device_class = DEVICE_CLASS_POWER + _divisor = 10 + _unit = POWER_WATT + + @property + def should_poll(self) -> bool: + """Return True if HA needs to poll for state changes.""" + return True + + +@STRICT_MATCH(MatchRule(generic_ids={CHANNEL_ST_HUMIDITY_CLUSTER})) +@STRICT_MATCH(MatchRule(channel_names={CHANNEL_HUMIDITY})) +class Humidity(Sensor): + """Humidity sensor.""" + + _device_class = DEVICE_CLASS_HUMIDITY + _divisor = 100 + _unit = "%" + + +@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ILLUMINANCE})) +class Illuminance(Sensor): + """Illuminance Sensor.""" + + _device_class = DEVICE_CLASS_ILLUMINANCE + _unit = "lx" + + @staticmethod + def formatter(value): + """Convert illumination data.""" + return round(pow(10, ((value - 1) / 10000)), 1) + + +@STRICT_MATCH(MatchRule(channel_names={CHANNEL_SMARTENERGY_METERING})) +class SmartEnergyMetering(Sensor): + """Metering sensor.""" + + _device_class = DEVICE_CLASS_POWER + + def formatter(self, value): + """Pass through channel formatter.""" + return self._channel.formatter_function(value) + + @property + def unit_of_measurement(self): + """Return Unit of measurement.""" + return self._channel.unit_of_measurement + + +@STRICT_MATCH(MatchRule(channel_names={CHANNEL_PRESSURE})) +class Pressure(Sensor): + """Pressure sensor.""" + + _device_class = DEVICE_CLASS_PRESSURE + _decimals = 0 + _unit = "hPa" + + +@STRICT_MATCH(MatchRule(channel_names={CHANNEL_TEMPERATURE})) +class Temperature(Sensor): + """Temperature Sensor.""" + + _device_class = DEVICE_CLASS_TEMPERATURE + _divisor = 100 + _unit = TEMP_CELSIUS diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py new file mode 100644 index 00000000000000..a0eef355229e63 --- /dev/null +++ b/tests/components/zha/test_registries.py @@ -0,0 +1,165 @@ +"""Test ZHA registries.""" +from unittest import mock + +import pytest + +import homeassistant.components.zha.core.registries as registries + +MANUFACTURER = "mock manufacturer" +MODEL = "mock model" + + +@pytest.fixture +def zha_device(): + """Return a mock of ZHA device.""" + dev = mock.MagicMock() + dev.manufacturer = MANUFACTURER + dev.model = MODEL + return dev + + +@pytest.fixture +def channels(): + """Return a mock of channels.""" + + def channel(name, chan_id): + ch = mock.MagicMock() + ch.name = name + ch.generic_id = chan_id + return ch + + return [channel("level", "channel_0x0008"), channel("on_off", "channel_0x0006")] + + +@pytest.mark.parametrize( + "rule, matched", + [ + (registries.MatchRule(), False), + (registries.MatchRule(channel_names={"level"}), True), + (registries.MatchRule(channel_names={"level", "no match"}), False), + (registries.MatchRule(channel_names={"on_off"}), True), + (registries.MatchRule(channel_names={"on_off", "no match"}), False), + (registries.MatchRule(channel_names={"on_off", "level"}), True), + (registries.MatchRule(channel_names={"on_off", "level", "no match"}), False), + # test generic_id matching + (registries.MatchRule(generic_ids={"channel_0x0006"}), True), + (registries.MatchRule(generic_ids={"channel_0x0008"}), True), + (registries.MatchRule(generic_ids={"channel_0x0006", "channel_0x0008"}), True), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"} + ), + False, + ), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008"}, + channel_names={"on_off", "level"}, + ), + True, + ), + # manufacturer matching + (registries.MatchRule(manufacturer="no match"), False), + (registries.MatchRule(manufacturer=MANUFACTURER), True), + (registries.MatchRule(model=MODEL), True), + (registries.MatchRule(model="no match"), False), + # match everything + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008"}, + channel_names={"on_off", "level"}, + manufacturer=MANUFACTURER, + model=MODEL, + ), + True, + ), + ], +) +def test_registry_matching(rule, matched, zha_device, channels): + """Test empty rule matching.""" + reg = registries.ZHAEntityRegistry() + assert reg._strict_matched(zha_device, channels, rule) is matched + + +@pytest.mark.parametrize( + "rule, matched", + [ + (registries.MatchRule(), False), + (registries.MatchRule(channel_names={"level"}), True), + (registries.MatchRule(channel_names={"level", "no match"}), False), + (registries.MatchRule(channel_names={"on_off"}), True), + (registries.MatchRule(channel_names={"on_off", "no match"}), False), + (registries.MatchRule(channel_names={"on_off", "level"}), True), + (registries.MatchRule(channel_names={"on_off", "level", "no match"}), False), + ( + registries.MatchRule(channel_names={"on_off", "level"}, model="no match"), + True, + ), + ( + registries.MatchRule( + channel_names={"on_off", "level"}, + model="no match", + manufacturer="no match", + ), + True, + ), + ( + registries.MatchRule( + channel_names={"on_off", "level"}, + model="no match", + manufacturer=MANUFACTURER, + ), + True, + ), + # test generic_id matching + (registries.MatchRule(generic_ids={"channel_0x0006"}), True), + (registries.MatchRule(generic_ids={"channel_0x0008"}), True), + (registries.MatchRule(generic_ids={"channel_0x0006", "channel_0x0008"}), True), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"} + ), + False, + ), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"}, + model="mo match", + ), + False, + ), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"}, + model=MODEL, + ), + True, + ), + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008"}, + channel_names={"on_off", "level"}, + ), + True, + ), + # manufacturer matching + (registries.MatchRule(manufacturer="no match"), False), + (registries.MatchRule(manufacturer=MANUFACTURER), True), + (registries.MatchRule(model=MODEL), True), + (registries.MatchRule(model="no match"), False), + # match everything + ( + registries.MatchRule( + generic_ids={"channel_0x0006", "channel_0x0008"}, + channel_names={"on_off", "level"}, + manufacturer=MANUFACTURER, + model=MODEL, + ), + True, + ), + ], +) +def test_registry_loose_matching(rule, matched, zha_device, channels): + """Test loose rule matching.""" + reg = registries.ZHAEntityRegistry() + assert reg._loose_matched(zha_device, channels, rule) is matched From 834929a14e4c0d288891a7ad9b8da94aa78b30c3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 21 Dec 2019 22:45:06 +0100 Subject: [PATCH 2512/3953] Clean up mobile app webhooks (#30123) --- .../components/http/data_validator.py | 3 + homeassistant/components/mobile_app/const.py | 153 ------ .../components/mobile_app/device_tracker.py | 10 +- .../components/mobile_app/http_api.py | 27 +- .../components/mobile_app/webhook.py | 485 +++++++++++------- script/hassfest/dependencies.py | 1 + 6 files changed, 347 insertions(+), 332 deletions(-) diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 017644a4d3695d..51b3b5617e49cb 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -20,6 +20,9 @@ class RequestDataValidator: def __init__(self, schema, allow_empty=False): """Initialize the decorator.""" + if isinstance(schema, dict): + schema = vol.Schema(schema) + self._schema = schema self._allow_empty = allow_empty diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 318076d5fd933d..720cf7106e766c 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -1,19 +1,4 @@ """Constants for mobile_app.""" -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES as BINARY_SENSOR_CLASSES, -) -from homeassistant.components.device_tracker import ( - ATTR_BATTERY, - ATTR_GPS, - ATTR_GPS_ACCURACY, - ATTR_LOCATION_NAME, -) -from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES -from homeassistant.const import ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA -from homeassistant.helpers import config_validation as cv - DOMAIN = "mobile_app" STORAGE_KEY = DOMAIN @@ -71,100 +56,6 @@ ERR_SENSOR_NOT_REGISTERED = "not_registered" ERR_SENSOR_DUPLICATE_UNIQUE_ID = "duplicate_unique_id" -WEBHOOK_TYPE_CALL_SERVICE = "call_service" -WEBHOOK_TYPE_FIRE_EVENT = "fire_event" -WEBHOOK_TYPE_GET_CONFIG = "get_config" -WEBHOOK_TYPE_GET_ZONES = "get_zones" -WEBHOOK_TYPE_REGISTER_SENSOR = "register_sensor" -WEBHOOK_TYPE_RENDER_TEMPLATE = "render_template" -WEBHOOK_TYPE_UPDATE_LOCATION = "update_location" -WEBHOOK_TYPE_UPDATE_REGISTRATION = "update_registration" -WEBHOOK_TYPE_UPDATE_SENSOR_STATES = "update_sensor_states" - -WEBHOOK_TYPES = [ - WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, - WEBHOOK_TYPE_GET_CONFIG, - WEBHOOK_TYPE_GET_ZONES, - WEBHOOK_TYPE_REGISTER_SENSOR, - WEBHOOK_TYPE_RENDER_TEMPLATE, - WEBHOOK_TYPE_UPDATE_LOCATION, - WEBHOOK_TYPE_UPDATE_REGISTRATION, - WEBHOOK_TYPE_UPDATE_SENSOR_STATES, -] - - -REGISTRATION_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_APP_DATA, default={}): dict, - vol.Required(ATTR_APP_ID): cv.string, - vol.Required(ATTR_APP_NAME): cv.string, - vol.Required(ATTR_APP_VERSION): cv.string, - vol.Required(ATTR_DEVICE_NAME): cv.string, - vol.Required(ATTR_MANUFACTURER): cv.string, - vol.Required(ATTR_MODEL): cv.string, - vol.Required(ATTR_OS_NAME): cv.string, - vol.Optional(ATTR_OS_VERSION): cv.string, - vol.Required(ATTR_SUPPORTS_ENCRYPTION, default=False): cv.boolean, - } -) - -UPDATE_REGISTRATION_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_APP_DATA, default={}): dict, - vol.Required(ATTR_APP_VERSION): cv.string, - vol.Required(ATTR_DEVICE_NAME): cv.string, - vol.Required(ATTR_MANUFACTURER): cv.string, - vol.Required(ATTR_MODEL): cv.string, - vol.Optional(ATTR_OS_VERSION): cv.string, - } -) - -WEBHOOK_PAYLOAD_SCHEMA = vol.Schema( - { - vol.Required(ATTR_WEBHOOK_TYPE): cv.string, # vol.In(WEBHOOK_TYPES) - vol.Required(ATTR_WEBHOOK_DATA, default={}): vol.Any(dict, list), - vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean, - vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string, - } -) - -CALL_SERVICE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DOMAIN): cv.string, - vol.Required(ATTR_SERVICE): cv.string, - vol.Optional(ATTR_SERVICE_DATA, default={}): dict, - } -) - -FIRE_EVENT_SCHEMA = vol.Schema( - { - vol.Required(ATTR_EVENT_TYPE): cv.string, - vol.Optional(ATTR_EVENT_DATA, default={}): dict, - } -) - -RENDER_TEMPLATE_SCHEMA = vol.Schema( - { - str: { - vol.Required(ATTR_TEMPLATE): cv.template, - vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict, - } - } -) - -UPDATE_LOCATION_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_LOCATION_NAME): cv.string, - vol.Required(ATTR_GPS): cv.gps, - vol.Required(ATTR_GPS_ACCURACY): cv.positive_int, - vol.Optional(ATTR_BATTERY): cv.positive_int, - vol.Optional(ATTR_SPEED): cv.positive_int, - vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), - vol.Optional(ATTR_COURSE): cv.positive_int, - vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, - } -) ATTR_SENSOR_ATTRIBUTES = "attributes" ATTR_SENSOR_DEVICE_CLASS = "device_class" @@ -177,49 +68,5 @@ ATTR_SENSOR_UNIQUE_ID = "unique_id" ATTR_SENSOR_UOM = "unit_of_measurement" -SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] - -COMBINED_CLASSES = sorted(set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES)) - SIGNAL_SENSOR_UPDATE = DOMAIN + "_sensor_update" SIGNAL_LOCATION_UPDATE = DOMAIN + "_location_update_{}" - -REGISTER_SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( - vol.Lower, vol.In(COMBINED_CLASSES) - ), - vol.Required(ATTR_SENSOR_NAME): cv.string, - vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), - vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - vol.Optional(ATTR_SENSOR_UOM): cv.string, - vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - } -) - -UPDATE_SENSOR_STATE_SCHEMA = vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), - vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), - vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - } - ) - ], -) - -WEBHOOK_SCHEMAS = { - WEBHOOK_TYPE_CALL_SERVICE: CALL_SERVICE_SCHEMA, - WEBHOOK_TYPE_FIRE_EVENT: FIRE_EVENT_SCHEMA, - WEBHOOK_TYPE_REGISTER_SENSOR: REGISTER_SENSOR_SCHEMA, - WEBHOOK_TYPE_RENDER_TEMPLATE: RENDER_TEMPLATE_SCHEMA, - WEBHOOK_TYPE_UPDATE_LOCATION: UPDATE_LOCATION_SCHEMA, - WEBHOOK_TYPE_UPDATE_REGISTRATION: UPDATE_REGISTRATION_SCHEMA, - WEBHOOK_TYPE_UPDATE_SENSOR_STATES: UPDATE_SENSOR_STATE_SCHEMA, -} diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index f58f80aa5fc5e5..480bfee512fe1f 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -1,6 +1,12 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" import logging +from homeassistant.components.device_tracker import ( + ATTR_BATTERY, + ATTR_GPS, + ATTR_GPS_ACCURACY, + ATTR_LOCATION_NAME, +) from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_LATITUDE, ATTR_LONGITUDE @@ -9,13 +15,9 @@ from .const import ( ATTR_ALTITUDE, - ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, - ATTR_GPS, - ATTR_GPS_ACCURACY, - ATTR_LOCATION_NAME, ATTR_SPEED, ATTR_VERTICAL_ACCURACY, SIGNAL_LOCATION_UPDATE, diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 9581a3743847e6..717413f889adcc 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -5,20 +5,30 @@ from aiohttp.web import Request, Response from nacl.secret import SecretBox +import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED +from homeassistant.helpers import config_validation as cv from .const import ( + ATTR_APP_DATA, + ATTR_APP_ID, + ATTR_APP_NAME, + ATTR_APP_VERSION, ATTR_DEVICE_ID, + ATTR_DEVICE_NAME, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_OS_NAME, + ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION, CONF_CLOUDHOOK_URL, CONF_REMOTE_UI_URL, CONF_SECRET, CONF_USER_ID, DOMAIN, - REGISTRATION_SCHEMA, ) from .helpers import supports_encryption @@ -29,7 +39,20 @@ class RegistrationsView(HomeAssistantView): url = "/api/mobile_app/registrations" name = "api:mobile_app:register" - @RequestDataValidator(REGISTRATION_SCHEMA) + @RequestDataValidator( + { + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_ID): cv.string, + vol.Required(ATTR_APP_NAME): cv.string, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Required(ATTR_OS_NAME): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, + vol.Required(ATTR_SUPPORTS_ENCRYPTION, default=False): cv.boolean, + } + ) async def post(self, request: Request, data: Dict) -> Response: """Handle the POST request for registration.""" hass = request.app["hass"] diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index c2bc6c9411272f..3a477d899250d9 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,10 +1,21 @@ """Webhook handlers for mobile_app.""" +from functools import wraps import logging from aiohttp.web import HTTPBadRequest, Request, Response import voluptuous as vol +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES as BINARY_SENSOR_CLASSES, +) +from homeassistant.components.device_tracker import ( + ATTR_BATTERY, + ATTR_GPS, + ATTR_GPS_ACCURACY, + ATTR_LOCATION_NAME, +) from homeassistant.components.frontend import MANIFEST_JSON +from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( ATTR_DOMAIN, @@ -16,12 +27,17 @@ ) from homeassistant.core import EventOrigin from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.template import attach from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.decorator import Registry from .const import ( + ATTR_ALTITUDE, + ATTR_APP_DATA, + ATTR_APP_VERSION, + ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_EVENT_DATA, @@ -29,11 +45,21 @@ ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, + ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ICON, + ATTR_SENSOR_NAME, + ATTR_SENSOR_STATE, ATTR_SENSOR_TYPE, + ATTR_SENSOR_TYPE_BINARY_SENSOR, + ATTR_SENSOR_TYPE_SENSOR, ATTR_SENSOR_UNIQUE_ID, + ATTR_SENSOR_UOM, + ATTR_SPEED, ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE, ATTR_TEMPLATE_VARIABLES, + ATTR_VERTICAL_ACCURACY, ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED, ATTR_WEBHOOK_ENCRYPTED_DATA, @@ -50,18 +76,6 @@ ERR_SENSOR_NOT_REGISTERED, SIGNAL_LOCATION_UPDATE, SIGNAL_SENSOR_UPDATE, - WEBHOOK_PAYLOAD_SCHEMA, - WEBHOOK_SCHEMAS, - WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, - WEBHOOK_TYPE_GET_CONFIG, - WEBHOOK_TYPE_GET_ZONES, - WEBHOOK_TYPE_REGISTER_SENSOR, - WEBHOOK_TYPE_RENDER_TEMPLATE, - WEBHOOK_TYPE_UPDATE_LOCATION, - WEBHOOK_TYPE_UPDATE_REGISTRATION, - WEBHOOK_TYPE_UPDATE_SENSOR_STATES, - WEBHOOK_TYPES, ) from .helpers import ( _decrypt_payload, @@ -76,6 +90,46 @@ _LOGGER = logging.getLogger(__name__) +WEBHOOK_COMMANDS = Registry() + +COMBINED_CLASSES = set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES) +SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] + +WEBHOOK_PAYLOAD_SCHEMA = vol.Schema( + { + vol.Required(ATTR_WEBHOOK_TYPE): cv.string, + vol.Required(ATTR_WEBHOOK_DATA, default={}): vol.Any(dict, list), + vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean, + vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string, + } +) + + +def validate_schema(schema): + """Decorate a webhook function with a schema.""" + if isinstance(schema, dict): + schema = vol.Schema(schema) + + def wrapper(func): + """Wrap function so we validate schema.""" + + @wraps(func) + async def validate_and_run(hass, config_entry, data): + """Validate input and call handler.""" + try: + data = schema(data) + except vol.Invalid as ex: + err = vol.humanize.humanize_error(data, ex) + _LOGGER.error("Received invalid webhook payload: %s", err) + return empty_okay_response() + + return await func(hass, config_entry, data) + + return validate_and_run + + return wrapper + + async def handle_webhook( hass: HomeAssistantType, webhook_id: str, request: Request ) -> Response: @@ -83,12 +137,8 @@ async def handle_webhook( if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]: return Response(status=410) - headers = {} - config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] - registration = config_entry.data - try: req_data = await request.json() except ValueError: @@ -97,11 +147,11 @@ async def handle_webhook( if ( ATTR_WEBHOOK_ENCRYPTED not in req_data - and registration[ATTR_SUPPORTS_ENCRYPTION] + and config_entry.data[ATTR_SUPPORTS_ENCRYPTION] ): _LOGGER.warning( "Refusing to accept unencrypted webhook from %s", - registration[ATTR_DEVICE_NAME], + config_entry.data[ATTR_DEVICE_NAME], ) return error_response(ERR_ENCRYPTION_REQUIRED, "Encryption required") @@ -118,197 +168,286 @@ async def handle_webhook( if req_data[ATTR_WEBHOOK_ENCRYPTED]: enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] - webhook_payload = _decrypt_payload(registration[CONF_SECRET], enc_data) + webhook_payload = _decrypt_payload(config_entry.data[CONF_SECRET], enc_data) - if webhook_type not in WEBHOOK_TYPES: + if webhook_type not in WEBHOOK_COMMANDS: _LOGGER.error("Received invalid webhook type: %s", webhook_type) return empty_okay_response() - data = webhook_payload + _LOGGER.debug( + "Received webhook payload for type %s: %s", webhook_type, webhook_payload + ) - _LOGGER.debug("Received webhook payload for type %s: %s", webhook_type, data) + return await WEBHOOK_COMMANDS[webhook_type](hass, config_entry, webhook_payload) - if webhook_type in WEBHOOK_SCHEMAS: - try: - data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) - except vol.Invalid as ex: - err = vol.humanize.humanize_error(webhook_payload, ex) - _LOGGER.error("Received invalid webhook payload: %s", err) - return empty_okay_response(headers=headers) - context = registration_context(registration) +@WEBHOOK_COMMANDS.register("call_service") +@validate_schema( + { + vol.Required(ATTR_DOMAIN): cv.string, + vol.Required(ATTR_SERVICE): cv.string, + vol.Optional(ATTR_SERVICE_DATA, default={}): dict, + } +) +async def webhook_call_service(hass, config_entry, data): + """Handle a call service webhook.""" + try: + await hass.services.async_call( + data[ATTR_DOMAIN], + data[ATTR_SERVICE], + data[ATTR_SERVICE_DATA], + blocking=True, + context=registration_context(config_entry.data), + ) + except (vol.Invalid, ServiceNotFound, Exception) as ex: + _LOGGER.error( + "Error when calling service during mobile_app " + "webhook (device name: %s): %s", + config_entry.data[ATTR_DEVICE_NAME], + ex, + ) + raise HTTPBadRequest() - if webhook_type == WEBHOOK_TYPE_CALL_SERVICE: - try: - await hass.services.async_call( - data[ATTR_DOMAIN], - data[ATTR_SERVICE], - data[ATTR_SERVICE_DATA], - blocking=True, - context=context, - ) - except (vol.Invalid, ServiceNotFound, Exception) as ex: - _LOGGER.error( - "Error when calling service during mobile_app " - "webhook (device name: %s): %s", - registration[ATTR_DEVICE_NAME], - ex, - ) - raise HTTPBadRequest() + return empty_okay_response() - return empty_okay_response(headers=headers) - if webhook_type == WEBHOOK_TYPE_FIRE_EVENT: - event_type = data[ATTR_EVENT_TYPE] - hass.bus.async_fire( - event_type, data[ATTR_EVENT_DATA], EventOrigin.remote, context=context - ) - return empty_okay_response(headers=headers) +@WEBHOOK_COMMANDS.register("fire_event") +@validate_schema( + { + vol.Required(ATTR_EVENT_TYPE): cv.string, + vol.Optional(ATTR_EVENT_DATA, default={}): dict, + } +) +async def webhook_fire_event(hass, config_entry, data): + """Handle a fire event webhook.""" + event_type = data[ATTR_EVENT_TYPE] + hass.bus.async_fire( + event_type, + data[ATTR_EVENT_DATA], + EventOrigin.remote, + context=registration_context(config_entry.data), + ) + return empty_okay_response() + + +@WEBHOOK_COMMANDS.register("render_template") +@validate_schema( + { + str: { + vol.Required(ATTR_TEMPLATE): cv.template, + vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict, + } + } +) +async def webhook_render_template(hass, config_entry, data): + """Handle a render template webhook.""" + resp = {} + for key, item in data.items(): + try: + tpl = item[ATTR_TEMPLATE] + attach(hass, tpl) + resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) + except TemplateError as ex: + resp[key] = {"error": str(ex)} + + return webhook_response(resp, registration=config_entry.data) + + +@WEBHOOK_COMMANDS.register("update_location") +@validate_schema( + { + vol.Optional(ATTR_LOCATION_NAME): cv.string, + vol.Required(ATTR_GPS): cv.gps, + vol.Required(ATTR_GPS_ACCURACY): cv.positive_int, + vol.Optional(ATTR_BATTERY): cv.positive_int, + vol.Optional(ATTR_SPEED): cv.positive_int, + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), + vol.Optional(ATTR_COURSE): cv.positive_int, + vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, + } +) +async def webhook_update_location(hass, config_entry, data): + """Handle an update location webhook.""" + hass.helpers.dispatcher.async_dispatcher_send( + SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data + ) + return empty_okay_response() + + +@WEBHOOK_COMMANDS.register("update_registration") +@validate_schema( + { + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, + } +) +async def webhook_update_registration(hass, config_entry, data): + """Handle an update registration webhook.""" + new_registration = {**config_entry.data, **data} + + device_registry = await dr.async_get_registry(hass) + + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, config_entry.data[ATTR_DEVICE_ID])}, + manufacturer=new_registration[ATTR_MANUFACTURER], + model=new_registration[ATTR_MODEL], + name=new_registration[ATTR_DEVICE_NAME], + sw_version=new_registration[ATTR_OS_VERSION], + ) + + hass.config_entries.async_update_entry(config_entry, data=new_registration) + + return webhook_response( + safe_registration(new_registration), registration=new_registration, + ) + + +@WEBHOOK_COMMANDS.register("register_sensor") +@validate_schema( + { + vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, + vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( + vol.Lower, vol.In(COMBINED_CLASSES) + ), + vol.Required(ATTR_SENSOR_NAME): cv.string, + vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, + vol.Optional(ATTR_SENSOR_UOM): cv.string, + vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, + } +) +async def webhook_register_sensor(hass, config_entry, data): + """Handle a register sensor webhook.""" + entity_type = data[ATTR_SENSOR_TYPE] - if webhook_type == WEBHOOK_TYPE_RENDER_TEMPLATE: - resp = {} - for key, item in data.items(): - try: - tpl = item[ATTR_TEMPLATE] - attach(hass, tpl) - resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) - except TemplateError as ex: - resp[key] = {"error": str(ex)} + unique_id = data[ATTR_SENSOR_UNIQUE_ID] - return webhook_response(resp, registration=registration, headers=headers) + unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" - if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION: - hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data + if unique_store_key in hass.data[DOMAIN][entity_type]: + _LOGGER.error("Refusing to re-register existing sensor %s!", unique_id) + return error_response( + ERR_SENSOR_DUPLICATE_UNIQUE_ID, + f"{entity_type} {unique_id} already exists!", + status=409, ) - return empty_okay_response(headers=headers) - if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION: - new_registration = {**registration, **data} + data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] - device_registry = await dr.async_get_registry(hass) + hass.data[DOMAIN][entity_type][unique_store_key] = data - device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - identifiers={(DOMAIN, registration[ATTR_DEVICE_ID])}, - manufacturer=new_registration[ATTR_MANUFACTURER], - model=new_registration[ATTR_MODEL], - name=new_registration[ATTR_DEVICE_NAME], - sw_version=new_registration[ATTR_OS_VERSION], - ) + try: + await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass)) + except HomeAssistantError as ex: + _LOGGER.error("Error registering sensor: %s", ex) + return empty_okay_response() - hass.config_entries.async_update_entry(config_entry, data=new_registration) + register_signal = "{}_{}_register".format(DOMAIN, data[ATTR_SENSOR_TYPE]) + async_dispatcher_send(hass, register_signal, data) + + return webhook_response( + {"success": True}, registration=config_entry.data, status=HTTP_CREATED, + ) + + +@WEBHOOK_COMMANDS.register("update_sensor_states") +@validate_schema( + vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, + vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), + vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, + } + ) + ], + ) +) +async def webhook_update_sensor_states(hass, config_entry, data): + """Handle an update sensor states webhook.""" + resp = {} + for sensor in data: + entity_type = sensor[ATTR_SENSOR_TYPE] - return webhook_response( - safe_registration(new_registration), - registration=registration, - headers=headers, - ) + unique_id = sensor[ATTR_SENSOR_UNIQUE_ID] - if webhook_type == WEBHOOK_TYPE_REGISTER_SENSOR: - entity_type = data[ATTR_SENSOR_TYPE] + unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" - unique_id = data[ATTR_SENSOR_UNIQUE_ID] + if unique_store_key not in hass.data[DOMAIN][entity_type]: + _LOGGER.error( + "Refusing to update non-registered sensor: %s", unique_store_key + ) + err_msg = f"{entity_type} {unique_id} is not registered" + resp[unique_id] = { + "success": False, + "error": {"code": ERR_SENSOR_NOT_REGISTERED, "message": err_msg}, + } + continue - unique_store_key = f"{webhook_id}_{unique_id}" + entry = hass.data[DOMAIN][entity_type][unique_store_key] - if unique_store_key in hass.data[DOMAIN][entity_type]: - _LOGGER.error("Refusing to re-register existing sensor %s!", unique_id) - return error_response( - ERR_SENSOR_DUPLICATE_UNIQUE_ID, - f"{entity_type} {unique_id} already exists!", - status=409, - ) + new_state = {**entry, **sensor} - data[CONF_WEBHOOK_ID] = webhook_id + hass.data[DOMAIN][entity_type][unique_store_key] = new_state - hass.data[DOMAIN][entity_type][unique_store_key] = data + safe = savable_state(hass) try: - await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass)) + await hass.data[DOMAIN][DATA_STORE].async_save(safe) except HomeAssistantError as ex: - _LOGGER.error("Error registering sensor: %s", ex) + _LOGGER.error("Error updating mobile_app registration: %s", ex) return empty_okay_response() - register_signal = "{}_{}_register".format(DOMAIN, data[ATTR_SENSOR_TYPE]) - async_dispatcher_send(hass, register_signal, data) + async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) - return webhook_response( - {"success": True}, - registration=registration, - status=HTTP_CREATED, - headers=headers, - ) - - if webhook_type == WEBHOOK_TYPE_UPDATE_SENSOR_STATES: - resp = {} - for sensor in data: - entity_type = sensor[ATTR_SENSOR_TYPE] - - unique_id = sensor[ATTR_SENSOR_UNIQUE_ID] - - unique_store_key = f"{webhook_id}_{unique_id}" + resp[unique_id] = {"success": True} - if unique_store_key not in hass.data[DOMAIN][entity_type]: - _LOGGER.error( - "Refusing to update non-registered sensor: %s", unique_store_key - ) - err_msg = f"{entity_type} {unique_id} is not registered" - resp[unique_id] = { - "success": False, - "error": {"code": ERR_SENSOR_NOT_REGISTERED, "message": err_msg}, - } - continue - - entry = hass.data[DOMAIN][entity_type][unique_store_key] + return webhook_response(resp, registration=config_entry.data) - new_state = {**entry, **sensor} - hass.data[DOMAIN][entity_type][unique_store_key] = new_state +@WEBHOOK_COMMANDS.register("get_zones") +async def webhook_get_zones(hass, config_entry, data): + """Handle a get zones webhook.""" + zones = [ + hass.states.get(entity_id) + for entity_id in sorted(hass.states.async_entity_ids(ZONE_DOMAIN)) + ] + return webhook_response(zones, registration=config_entry.data) - safe = savable_state(hass) - try: - await hass.data[DOMAIN][DATA_STORE].async_save(safe) - except HomeAssistantError as ex: - _LOGGER.error("Error updating mobile_app registration: %s", ex) - return empty_okay_response() +@WEBHOOK_COMMANDS.register("get_config") +async def webhook_get_config(hass, config_entry, data): + """Handle a get config webhook.""" + hass_config = hass.config.as_dict() - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) + resp = { + "latitude": hass_config["latitude"], + "longitude": hass_config["longitude"], + "elevation": hass_config["elevation"], + "unit_system": hass_config["unit_system"], + "location_name": hass_config["location_name"], + "time_zone": hass_config["time_zone"], + "components": hass_config["components"], + "version": hass_config["version"], + "theme_color": MANIFEST_JSON["theme_color"], + } - resp[unique_id] = {"success": True} + if CONF_CLOUDHOOK_URL in config_entry.data: + resp[CONF_CLOUDHOOK_URL] = config_entry.data[CONF_CLOUDHOOK_URL] - return webhook_response(resp, registration=registration, headers=headers) - - if webhook_type == WEBHOOK_TYPE_GET_ZONES: - zones = ( - hass.states.get(entity_id) - for entity_id in sorted(hass.states.async_entity_ids(ZONE_DOMAIN)) - ) - return webhook_response(list(zones), registration=registration, headers=headers) - - if webhook_type == WEBHOOK_TYPE_GET_CONFIG: - - hass_config = hass.config.as_dict() - - resp = { - "latitude": hass_config["latitude"], - "longitude": hass_config["longitude"], - "elevation": hass_config["elevation"], - "unit_system": hass_config["unit_system"], - "location_name": hass_config["location_name"], - "time_zone": hass_config["time_zone"], - "components": hass_config["components"], - "version": hass_config["version"], - "theme_color": MANIFEST_JSON["theme_color"], - } - - if CONF_CLOUDHOOK_URL in registration: - resp[CONF_CLOUDHOOK_URL] = registration[CONF_CLOUDHOOK_URL] - - try: - resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() - except hass.components.cloud.CloudNotAvailable: - pass + try: + resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass - return webhook_response(resp, registration=registration, headers=headers) + return webhook_response(resp, registration=config_entry.data) diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index cb58de3af768d5..8500e9d897d3a7 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -130,6 +130,7 @@ def visit_Attribute(self, node): "prometheus", "conversation", "logbook", + "mobile_app", # These should be extracted to external package "pvoutput", "dwd_weather_warnings", From 8e3dfbd5c98fe9d9644bc82734cab4177e236578 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 21 Dec 2019 17:15:50 -0500 Subject: [PATCH 2513/3953] Refactor ZHA electrical measurement sensor. (#30130) --- .../zha/core/channels/homeautomation.py | 38 +++++++++++++++++++ homeassistant/components/zha/sensor.py | 4 ++ tests/components/zha/test_sensor.py | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index dda6c1f4c13f3b..d9d8f57eaaf5ef 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -5,6 +5,7 @@ https://home-assistant.io/integrations/zha/ """ import logging +from typing import Optional import zigpy.zcl.clusters.homeautomation as homeautomation @@ -65,6 +66,12 @@ class ElectricalMeasurementChannel(AttributeListeningChannel): REPORT_CONFIG = ({"attr": "active_power", "config": REPORT_CONFIG_DEFAULT},) + def __init__(self, cluster, device): + """Initialize Metering.""" + super().__init__(cluster, device) + self._divisor = None + self._multiplier = None + async def async_update(self): """Retrieve latest state.""" self.debug("async_update") @@ -78,8 +85,39 @@ async def async_update(self): async def async_initialize(self, from_cache): """Initialize channel.""" await self.get_attribute_value("active_power", from_cache=from_cache) + await self.fetch_config(from_cache) await super().async_initialize(from_cache) + async def fetch_config(self, from_cache): + """Fetch config from device and updates format specifier.""" + divisor = await self.get_attribute_value( + "ac_power_divisor", from_cache=from_cache + ) + if divisor is None: + divisor = await self.get_attribute_value( + "power_divisor", from_cache=from_cache + ) + self._divisor = divisor + + mult = await self.get_attribute_value( + "ac_power_multiplier", from_cache=from_cache + ) + if mult is None: + mult = await self.get_attribute_value( + "power_multiplier", from_cache=from_cache + ) + self._multiplier = mult + + @property + def divisor(self) -> Optional[int]: + """Return active power divisor.""" + return self._divisor or 1 + + @property + def multiplier(self) -> Optional[int]: + """Return active power divisor.""" + return self._multiplier or 1 + @registries.ZIGBEE_CHANNEL_REGISTRY.register( homeautomation.MeterIdentification.cluster_id diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 133e82e69140ae..e9d21be6132c58 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -217,6 +217,10 @@ def should_poll(self) -> bool: """Return True if HA needs to poll for state changes.""" return True + def formatter(self, value) -> int: + """Return 'normalized' value.""" + return round(value * self._channel.multiplier / self._channel.divisor) + @STRICT_MATCH(MatchRule(generic_ids={CHANNEL_ST_HUMIDITY_CLUSTER})) @STRICT_MATCH(MatchRule(channel_names={CHANNEL_HUMIDITY})) diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 7746c5d422eb30..4fa16f06b04e5e 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -166,7 +166,7 @@ async def async_test_metering(hass, device_info): async def async_test_electrical_measurement(hass, device_info): """Test electrical measurement sensor.""" await send_attribute_report(hass, device_info["cluster"], 1291, 100) - assert_state(hass, device_info, "10.0", "W") + assert_state(hass, device_info, "100", "W") async def send_attribute_report(hass, cluster, attrid, value): From 9c23c4adf207bd66bebce3a55e141c6821507d28 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 21 Dec 2019 18:33:00 -0500 Subject: [PATCH 2514/3953] Cleanup ZHAEntity class (#30131) Remove `_domain` attribute since we're not using it anymore. --- homeassistant/components/zha/binary_sensor.py | 1 - homeassistant/components/zha/entity.py | 2 -- homeassistant/components/zha/fan.py | 2 -- homeassistant/components/zha/light.py | 2 -- homeassistant/components/zha/lock.py | 2 -- homeassistant/components/zha/sensor.py | 1 - homeassistant/components/zha/switch.py | 2 -- 7 files changed, 12 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index e6176fe9da338f..df95d408398d8f 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -102,7 +102,6 @@ async def _async_setup_entities( class BinarySensor(ZhaEntity, BinarySensorDevice): """ZHA BinarySensor.""" - _domain = DOMAIN _device_class = None def __init__(self, **kwargs): diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 108d8e27a9f88a..102472d25b0cbc 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -30,8 +30,6 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): """A base class for ZHA entities.""" - _domain = None # Must be overridden by subclasses - def __init__(self, unique_id, zha_device, channels, skip_entity_id=False, **kwargs): """Init ZHA entity.""" self._force_update = False diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 43ad2291cb775d..bccdf260a118c0 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -87,8 +87,6 @@ async def _async_setup_entities( class ZhaFan(ZhaEntity, FanEntity): """Representation of a ZHA fan.""" - _domain = DOMAIN - def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index fb388afac0ffc9..08d74f9fdb37c5 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -80,8 +80,6 @@ async def _async_setup_entities( class Light(ZhaEntity, light.Light): """Representation of a ZHA or ZLL light.""" - _domain = light.DOMAIN - def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA light.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index a2151b4bdcb4af..2458bf4be5bf94 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -70,8 +70,6 @@ async def _async_setup_entities( class ZhaDoorLock(ZhaEntity, LockDevice): """Representation of a ZHA lock.""" - _domain = DOMAIN - def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index e9d21be6132c58..26dc25c71dcb49 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -109,7 +109,6 @@ class Sensor(ZhaEntity): _decimals = 1 _device_class = None _divisor = 1 - _domain = DOMAIN _multiplier = 1 _unit = None diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index bfe816d614a9f4..03296e8a553923 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -60,8 +60,6 @@ async def _async_setup_entities( class Switch(ZhaEntity, SwitchDevice): """ZHA switch.""" - _domain = DOMAIN - def __init__(self, **kwargs): """Initialize the ZHA switch.""" super().__init__(**kwargs) From 251808874f015bcd96e9f747313e1f7657ca79fb Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 22 Dec 2019 10:23:44 +0100 Subject: [PATCH 2515/3953] Move imports into setup function in homekit __init__.py (#30137) --- homeassistant/components/homekit/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 9a2b2d5a851ea5..ea2c466092e3b3 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -29,7 +29,6 @@ from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry -from .accessories import HomeBridge, HomeDriver from .const import ( BRIDGE_NAME, CONF_ADVERTISE_IP, @@ -303,6 +302,8 @@ def __init__( def setup(self): """Set up bridge and accessory driver.""" + # pylint: disable=import-outside-toplevel + from .accessories import HomeBridge, HomeDriver self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) From 63a843c19c9a536316d55f880225091ab54b082b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Dec 2019 10:31:23 +0100 Subject: [PATCH 2516/3953] Fix test --- tests/components/homekit/test_homekit.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index c845370e278499..de6aaf0f11eb58 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -126,7 +126,7 @@ async def test_homekit_setup(hass, hk_driver): homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, DEFAULT_SAFE_MODE) with patch( - PATH_HOMEKIT + ".HomeDriver", return_value=hk_driver + PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver ) as mock_driver, patch("homeassistant.util.get_local_ip") as mock_ip: mock_ip.return_value = IP_ADDRESS await hass.async_add_job(homekit.setup) @@ -150,7 +150,9 @@ async def test_homekit_setup_ip_address(hass, hk_driver): """Test setup with given IP address.""" homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, "172.0.0.0", {}, {}, None) - with patch(PATH_HOMEKIT + ".HomeDriver", return_value=hk_driver) as mock_driver: + with patch( + PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver + ) as mock_driver: await hass.async_add_job(homekit.setup) mock_driver.assert_called_with( hass, @@ -167,7 +169,9 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver): hass, BRIDGE_NAME, DEFAULT_PORT, "0.0.0.0", {}, {}, None, "192.168.1.100" ) - with patch(PATH_HOMEKIT + ".HomeDriver", return_value=hk_driver) as mock_driver: + with patch( + PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver + ) as mock_driver: await hass.async_add_job(homekit.setup) mock_driver.assert_called_with( hass, @@ -182,7 +186,7 @@ async def test_homekit_setup_safe_mode(hass, hk_driver): """Test if safe_mode flag is set.""" homekit = HomeKit(hass, BRIDGE_NAME, DEFAULT_PORT, None, {}, {}, True) - with patch(PATH_HOMEKIT + ".HomeDriver", return_value=hk_driver): + with patch(PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver): await hass.async_add_job(homekit.setup) assert homekit.driver.safe_mode is True From 32aae7017ecafd58808a1740abaac955b199e92d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Dec 2019 10:32:42 +0100 Subject: [PATCH 2517/3953] Remove requirement from entity integration (#30113) --- .../components/doods/image_processing.py | 2 +- homeassistant/components/doods/manifest.json | 14 +++--- .../components/image_processing/__init__.py | 42 ----------------- .../components/image_processing/manifest.json | 8 +--- .../components/seven_segments/manifest.json | 2 +- .../components/tensorflow/image_processing.py | 2 +- .../components/tensorflow/manifest.json | 7 ++- homeassistant/util/pil.py | 47 +++++++++++++++++++ requirements_all.txt | 4 +- requirements_test_all.txt | 5 -- 10 files changed, 67 insertions(+), 66 deletions(-) create mode 100644 homeassistant/util/pil.py diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 5e4d211d49718d..9525f9e8ddfd82 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -14,12 +14,12 @@ CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, - draw_box, ) from homeassistant.const import CONF_TIMEOUT from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv +from homeassistant.util.pil import draw_box _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index e0dcb48527f91b..551af839b5c475 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -1,10 +1,8 @@ { - "domain": "doods", - "name": "DOODS - Distributed Outside Object Detection Service", - "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": [ - "pydoods==1.0.2" - ], - "dependencies": [], - "codeowners": [] + "domain": "doods", + "name": "DOODS - Distributed Outside Object Detection Service", + "documentation": "https://www.home-assistant.io/integrations/doods", + "requirements": ["pydoods==1.0.2", "pillow==6.2.1"], + "dependencies": [], + "codeowners": [] } diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 78ae15eb5372b4..a8f5f0f097e79c 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -2,9 +2,7 @@ import asyncio from datetime import timedelta import logging -from typing import Tuple -from PIL import ImageDraw import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME @@ -65,46 +63,6 @@ PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) -def draw_box( - draw: ImageDraw, - box: Tuple[float, float, float, float], - img_width: int, - img_height: int, - text: str = "", - color: Tuple[int, int, int] = (255, 255, 0), -) -> None: - """ - Draw a bounding box on and image. - - The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) - where the coordinates are floats in the range [0.0, 1.0] and - relative to the width and height of the image. - - For example, if an image is 100 x 200 pixels (height x width) and the bounding - box is `(0.1, 0.2, 0.5, 0.9)`, the upper-left and bottom-right coordinates of - the bounding box will be `(40, 10)` to `(180, 50)` (in (x,y) coordinates). - """ - - line_width = 3 - font_height = 8 - y_min, x_min, y_max, x_max = box - (left, right, top, bottom) = ( - x_min * img_width, - x_max * img_width, - y_min * img_height, - y_max * img_height, - ) - draw.line( - [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], - width=line_width, - fill=color, - ) - if text: - draw.text( - (left + line_width, abs(top - line_width - font_height)), text, fill=color - ) - - async def async_setup(hass, config): """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index e986ac6f4ca193..1e9f2963a38735 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -2,11 +2,7 @@ "domain": "image_processing", "name": "Image processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", - "requirements": [ - "pillow==6.2.1" - ], - "dependencies": [ - "camera" - ], + "requirements": [], + "dependencies": ["camera"], "codeowners": [] } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 0bf49d4b9066e7..6e375da03225cf 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven segments", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": [], + "requirements": ["pillow==6.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 5f576f176e8261..dee2a0218299b0 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -15,11 +15,11 @@ CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, - draw_box, ) from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv +from homeassistant.util.pil import draw_box _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 278a92a481f9f2..627fa8d6bd6e43 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -2,7 +2,12 @@ "domain": "tensorflow", "name": "Tensorflow", "documentation": "https://www.home-assistant.io/integrations/tensorflow", - "requirements": ["tensorflow==1.13.2", "numpy==1.17.4", "protobuf==3.6.1"], + "requirements": [ + "tensorflow==1.13.2", + "numpy==1.17.4", + "protobuf==3.6.1", + "pillow==6.2.1" + ], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/util/pil.py b/homeassistant/util/pil.py new file mode 100644 index 00000000000000..80c11c9c4104c0 --- /dev/null +++ b/homeassistant/util/pil.py @@ -0,0 +1,47 @@ +"""PIL utilities. + +Can only be used by integrations that have pillow in their requirements. +""" +from typing import Tuple + +from PIL import ImageDraw + + +def draw_box( + draw: ImageDraw, + box: Tuple[float, float, float, float], + img_width: int, + img_height: int, + text: str = "", + color: Tuple[int, int, int] = (255, 255, 0), +) -> None: + """ + Draw a bounding box on and image. + + The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) + where the coordinates are floats in the range [0.0, 1.0] and + relative to the width and height of the image. + + For example, if an image is 100 x 200 pixels (height x width) and the bounding + box is `(0.1, 0.2, 0.5, 0.9)`, the upper-left and bottom-right coordinates of + the bounding box will be `(40, 10)` to `(180, 50)` (in (x,y) coordinates). + """ + + line_width = 3 + font_height = 8 + y_min, x_min, y_max, x_max = box + (left, right, top, bottom) = ( + x_min * img_width, + x_max * img_width, + y_min * img_height, + y_max * img_height, + ) + draw.line( + [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], + width=line_width, + fill=color, + ) + if text: + draw.text( + (left + line_width, abs(top - line_width - font_height)), text, fill=color + ) diff --git a/requirements_all.txt b/requirements_all.txt index 822e92ef759901..19b97e2521f34f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -982,9 +982,11 @@ piglow==1.2.4 # homeassistant.components.pilight pilight==0.1.1 -# homeassistant.components.image_processing +# homeassistant.components.doods # homeassistant.components.proxy # homeassistant.components.qrcode +# homeassistant.components.seven_segments +# homeassistant.components.tensorflow pillow==6.2.1 # homeassistant.components.dominos diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7f443e0ccfd97..ab84629f12ec0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -323,11 +323,6 @@ pexpect==4.6.0 # homeassistant.components.pilight pilight==0.1.1 -# homeassistant.components.image_processing -# homeassistant.components.proxy -# homeassistant.components.qrcode -pillow==6.2.1 - # homeassistant.components.plex plexapi==3.3.0 From ed0ee3100d56c54f4e93f94e4e3f6bdfbb62e20c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 22 Dec 2019 12:01:22 +0100 Subject: [PATCH 2518/3953] Upgrade zeroconf to 0.24.2 (#30140) --- homeassistant/components/zeroconf/__init__.py | 3 +-- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 18ea1fdfca7b89..d6be4cdf6a072a 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,5 +1,4 @@ """Support for exposing Home Assistant via Zeroconf.""" - import ipaddress import logging import socket @@ -15,6 +14,7 @@ from homeassistant import util from homeassistant.const import ( + ATTR_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, __version__, @@ -29,7 +29,6 @@ ATTR_PORT = "port" ATTR_HOSTNAME = "hostname" ATTR_TYPE = "type" -ATTR_NAME = "name" ATTR_PROPERTIES = "properties" ZEROCONF_TYPE = "_home-assistant._tcp.local." diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index c02ac425445173..1dc1618842d0fc 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -3,7 +3,7 @@ "name": "Zeroconf", "documentation": "https://www.home-assistant.io/integrations/zeroconf", "requirements": [ - "zeroconf==0.24.1" + "zeroconf==0.24.2" ], "dependencies": [ "api" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bb212b3e0b46d7..ce6ad36dbf0b4c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.11 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.24.1 +zeroconf==0.24.2 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 19b97e2521f34f..6919b1031174cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2088,7 +2088,7 @@ youtube_dl==2019.11.28 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.24.1 +zeroconf==0.24.2 # homeassistant.components.zha zha-quirks==0.0.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab84629f12ec0b..0cc01b5f47f616 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -653,7 +653,7 @@ ya_ma==0.3.8 yahooweather==0.10 # homeassistant.components.zeroconf -zeroconf==0.24.1 +zeroconf==0.24.2 # homeassistant.components.zha zha-quirks==0.0.28 From 83768be8147139b43b5f43665f17a6f1ed301a94 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 22 Dec 2019 13:24:57 -0500 Subject: [PATCH 2519/3953] Refactor ZHA binary_sensor (#30138) * Refactor ZHA binary_sensor. Use ZHA entity class registry for channel specific implementations. * Remove registries.BINARY_SENSOR_TYPES dict. * Address PR comments. --- homeassistant/components/zha/binary_sensor.py | 126 ++++++++++-------- homeassistant/components/zha/core/const.py | 1 + .../components/zha/core/discovery.py | 25 +--- .../components/zha/core/registries.py | 21 +-- 4 files changed, 70 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index df95d408398d8f..ed09a190adb48e 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensors on Zigbee Home Automation networks.""" +import functools import logging from homeassistant.components.binary_sensor import ( @@ -18,20 +19,16 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( + CHANNEL_ACCELEROMETER, CHANNEL_OCCUPANCY, CHANNEL_ON_OFF, CHANNEL_ZONE, DATA_ZHA, DATA_ZHA_DISPATCHERS, - SENSOR_ACCELERATION, - SENSOR_OCCUPANCY, - SENSOR_OPENING, - SENSOR_TYPE, SIGNAL_ATTR_UPDATED, - UNKNOWN, ZHA_DISCOVERY_NEW, - ZONE, ) +from .core.registries import ZHA_ENTITIES, MatchRule from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -46,19 +43,13 @@ 0x002D: DEVICE_CLASS_VIBRATION, } - -async def get_ias_device_class(channel): - """Get the HA device class from the channel.""" - zone_type = await channel.get_attribute_value("zone_type") - return CLASS_MAPPING.get(zone_type) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) DEVICE_CLASS_REGISTRY = { - UNKNOWN: None, - SENSOR_OPENING: DEVICE_CLASS_OPENING, - ZONE: get_ias_device_class, - SENSOR_OCCUPANCY: DEVICE_CLASS_OCCUPANCY, - SENSOR_ACCELERATION: DEVICE_CLASS_MOVING, + CHANNEL_ACCELEROMETER: DEVICE_CLASS_MOVING, + CHANNEL_OCCUPANCY: DEVICE_CLASS_OCCUPANCY, + CHANNEL_ON_OFF: DEVICE_CLASS_OPENING, } @@ -94,7 +85,12 @@ async def _async_setup_entities( """Set up the ZHA binary sensors.""" entities = [] for discovery_info in discovery_infos: - entities.append(BinarySensor(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] + + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, BinarySensor) + if entity: + entities.append(entity(**discovery_info)) async_add_entities(entities, update_before_add=True) @@ -102,43 +98,25 @@ async def _async_setup_entities( class BinarySensor(ZhaEntity, BinarySensorDevice): """ZHA BinarySensor.""" - _device_class = None + DEVICE_CLASS = None - def __init__(self, **kwargs): + def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA binary sensor.""" - super().__init__(**kwargs) - self._device_state_attributes = {} - 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_OCCUPANCY) - self._zha_sensor_type = kwargs[SENSOR_TYPE] - - async def _determine_device_class(self): - """Determine the device class for this binary sensor.""" - device_class_supplier = DEVICE_CLASS_REGISTRY.get(self._zha_sensor_type) - if callable(device_class_supplier): - channel = self.cluster_channels.get(self._zha_sensor_type) - if channel is None: - return None - return await device_class_supplier(channel) - return device_class_supplier + super().__init__(unique_id, zha_device, channels, **kwargs) + self._channel = channels[0] + self._device_class = self.DEVICE_CLASS + + async def get_device_class(self): + """Get the HA device class from the channel.""" + pass async def async_added_to_hass(self): """Run when about to be added to hass.""" - self._device_class = await self._determine_device_class() await super().async_added_to_hass() - if self._on_off_channel: - await self.async_accept_signal( - self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - if self._zone_channel: - await self.async_accept_signal( - self._zone_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - if self._attr_channel: - await self.async_accept_signal( - self._attr_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) + await self.get_device_class() + await self.async_accept_signal( + self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state + ) @callback def async_restore_last_state(self, last_state): @@ -148,7 +126,7 @@ def async_restore_last_state(self, last_state): @property def is_on(self) -> bool: - """Return if the switch is on based on the statemachine.""" + """Return True if the switch is on based on the state machine.""" if self._state is None: return False return self._state @@ -166,13 +144,43 @@ def async_set_state(self, state): async def async_update(self): """Attempt to retrieve on off state from the binary sensor.""" await super().async_update() - if self._on_off_channel: - self._state = await self._on_off_channel.get_attribute_value("on_off") - if self._zone_channel: - value = await self._zone_channel.get_attribute_value("zone_status") - if value is not None: - self._state = value & 3 - if self._attr_channel: - self._state = await self._attr_channel.get_attribute_value( - self._attr_channel.value_attribute - ) + attribute = getattr(self._channel, "value_attribute", "on_off") + self._state = await self._channel.get_attribute_value(attribute) + + +@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ACCELEROMETER})) +class Accelerometer(BinarySensor): + """ZHA BinarySensor.""" + + DEVICE_CLASS = DEVICE_CLASS_MOTION + + +@STRICT_MATCH(MatchRule(channel_names={CHANNEL_OCCUPANCY})) +class Occupancy(BinarySensor): + """ZHA BinarySensor.""" + + DEVICE_CLASS = DEVICE_CLASS_OCCUPANCY + + +@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ON_OFF})) +class Opening(BinarySensor): + """ZHA BinarySensor.""" + + DEVICE_CLASS = DEVICE_CLASS_OPENING + + +@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ZONE})) +class IASZone(BinarySensor): + """ZHA IAS BinarySensor.""" + + async def get_device_class(self) -> None: + """Get the HA device class from the channel.""" + zone_type = await self._channel.get_attribute_value("zone_type") + self._device_class = CLASS_MAPPING.get(zone_type) + + async def async_update(self): + """Attempt to retrieve on off state from the binary sensor.""" + await super().async_update() + value = await self._channel.get_attribute_value("zone_status") + if value is not None: + self._state = value & 3 diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 6c991a319acae0..c658febfd2d35c 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -43,6 +43,7 @@ BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000] +CHANNEL_ACCELEROMETER = "accelerometer" CHANNEL_ATTRIBUTE = "attribute" CHANNEL_BASIC = "basic" CHANNEL_COLOR = "light_color" diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 108bd841252c08..d128ed274c0ea6 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -11,21 +11,12 @@ from zigpy.zcl.clusters.general import OnOff, PowerConfiguration from homeassistant import const as ha_const -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from .channels import AttributeListeningChannel, EventRelayChannel, ZDOChannel -from .const import ( - COMPONENTS, - CONF_DEVICE_CONFIG, - DATA_ZHA, - SENSOR_TYPE, - UNKNOWN, - ZHA_DISCOVERY_NEW, -) +from .const import COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, ZHA_DISCOVERY_NEW from .registries import ( - BINARY_SENSOR_TYPES, CHANNEL_ONLY_CLUSTERS, COMPONENT_CLUSTERS, DEVICE_CLASS, @@ -160,15 +151,6 @@ def _async_handle_profile_match( "component": component, } - if component == BINARY_SENSOR: - discovery_info.update({SENSOR_TYPE: UNKNOWN}) - for cluster_id in profile_clusters: - if cluster_id in BINARY_SENSOR_TYPES: - discovery_info.update( - {SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster_id, UNKNOWN)} - ) - break - return discovery_info @@ -288,9 +270,4 @@ def _async_handle_single_cluster_match( "component": component, } - if component == BINARY_SENSOR: - discovery_info.update( - {SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster.cluster_id, UNKNOWN)} - ) - return discovery_info diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index c2d3b13e375bfb..f235b459b879aa 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -30,20 +30,10 @@ # importing channels updates registries from . import channels # noqa: F401 pylint: disable=unused-import -from .const import ( - CONTROLLER, - SENSOR_ACCELERATION, - SENSOR_OCCUPANCY, - SENSOR_OPENING, - ZHA_GW_RADIO, - ZHA_GW_RADIO_DESCRIPTION, - ZONE, - RadioType, -) +from .const import CONTROLLER, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, RadioType from .decorators import CALLABLE_T, DictRegistry, SetRegistry BINARY_SENSOR_CLUSTERS = SetRegistry() -BINARY_SENSOR_TYPES = {} BINDABLE_CLUSTERS = SetRegistry() CHANNEL_ONLY_CLUSTERS = SetRegistry() CLUSTER_REPORT_CONFIGS = {} @@ -104,15 +94,6 @@ def establish_device_mappings(): BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) - 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, - } - ) - DEVICE_CLASS[zigpy.profiles.zha.PROFILE_ID].update( { SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, From 70f8bfbd4f569d58b7959179a5fd3239c59f7cb9 Mon Sep 17 00:00:00 2001 From: Philipp Danner Date: Sun, 22 Dec 2019 19:46:53 +0100 Subject: [PATCH 2520/3953] Update Integration of Keba charging station (#30125) * fixed parsing of current to float in service set_current * Added optional name in the config file in order to get a better entety naming (easier to find) * fix parsing of all parameters to service calls * addressed code review comments + updated pypi dependency * config name imported from cont.py + minor naming changes to be more clear about the meaning of a sensor * removed name in config again, use product name gathered from the charging station instead * implemented suggested changes * changed variable naming as requested --- homeassistant/components/keba/__init__.py | 19 +++++--- .../components/keba/binary_sensor.py | 23 +++++---- homeassistant/components/keba/lock.py | 11 +++-- homeassistant/components/keba/manifest.json | 2 +- homeassistant/components/keba/sensor.py | 47 +++++++++++++++---- requirements_all.txt | 2 +- 6 files changed, 72 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py index 830ebe7ffabdc2..a261311be7669f 100644 --- a/homeassistant/components/keba/__init__.py +++ b/homeassistant/components/keba/__init__.py @@ -112,7 +112,8 @@ def __init__(self, hass, host, rfid, refresh_interval): self._update_listeners = [] self._hass = hass self.rfid = rfid - self.device_name = "keba_wallbox_" + self.device_name = "keba" # correct device name will be set in setup() + self.device_id = "keba_wallbox_" # correct device id will be set in setup() # Ensure at least MAX_POLLING_INTERVAL seconds delay self._refresh_interval = max(MAX_POLLING_INTERVAL, refresh_interval) @@ -147,8 +148,12 @@ async def setup(self, loop=None): # 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')}" + if ( + self.get_value("Serial") is not None + and self.get_value("Product") is not None + ): + self.device_id = f"keba_wallbox_{self.get_value('Serial')}" + self.device_name = self.get_value("Product") return True return False @@ -179,7 +184,7 @@ async def async_set_energy(self, param): """Set energy target in async way.""" try: energy = param["energy"] - await self.set_energy(energy) + await self.set_energy(float(energy)) self._set_fast_polling() except (KeyError, ValueError) as ex: _LOGGER.warning("Energy value is not correct. %s", ex) @@ -188,7 +193,7 @@ async def async_set_current(self, param): """Set current maximum in async way.""" try: current = param["current"] - await self.set_current(current) + await self.set_current(float(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) @@ -216,10 +221,10 @@ async def async_disable_ev(self, param=None): async def async_set_failsafe(self, param=None): """Set failsafe mode in async way.""" try: - timout = param[CONF_FS_TIMEOUT] + timeout = param[CONF_FS_TIMEOUT] fallback = param[CONF_FS_FALLBACK] persist = param[CONF_FS_PERSIST] - await self.set_failsafe(timout, fallback, persist) + await self.set_failsafe(int(timeout), float(fallback), bool(persist)) self._set_fast_polling() except (KeyError, ValueError) as ex: _LOGGER.warning( diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py index ac7326cc92eaf5..5cced416bc3a6a 100644 --- a/homeassistant/components/keba/binary_sensor.py +++ b/homeassistant/components/keba/binary_sensor.py @@ -22,10 +22,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= 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), + KebaBinarySensor( + keba, "Online", "Status", "device_state", DEVICE_CLASS_CONNECTIVITY + ), + KebaBinarySensor(keba, "Plug", "Plug", "plug_state", DEVICE_CLASS_PLUG), + KebaBinarySensor( + keba, "State", "Charging State", "charging_state", DEVICE_CLASS_POWER + ), + KebaBinarySensor( + keba, "Tmo FS", "Failsafe Mode", "failsafe_mode_state", DEVICE_CLASS_SAFETY + ), ] async_add_entities(sensors) @@ -33,11 +39,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KebaBinarySensor(BinarySensorDevice): """Representation of a binary sensor of a KEBA charging station.""" - def __init__(self, keba, key, sensor_name, device_class): + def __init__(self, keba, key, name, entity_type, device_class): """Initialize the KEBA Sensor.""" self._key = key self._keba = keba - self._name = sensor_name + self._name = name + self._entity_type = entity_type self._device_class = device_class self._is_on = None self._attributes = {} @@ -50,12 +57,12 @@ def should_poll(self): @property def unique_id(self): """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_name}_{self._name}" + return f"{self._keba.device_id}_{self._entity_type}" @property def name(self): """Return the name of the device.""" - return self._name + return f"{self._keba.device_name} {self._name}" @property def device_class(self): diff --git a/homeassistant/components/keba/lock.py b/homeassistant/components/keba/lock.py index 3a65e44cd6fcb0..f69fbdddf20520 100644 --- a/homeassistant/components/keba/lock.py +++ b/homeassistant/components/keba/lock.py @@ -15,17 +15,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= keba = hass.data[DOMAIN] - sensors = [KebaLock(keba, "Authentication")] + sensors = [KebaLock(keba, "Authentication", "authentication")] async_add_entities(sensors) class KebaLock(LockDevice): """The entity class for KEBA charging stations switch.""" - def __init__(self, keba, name): + def __init__(self, keba, name, entity_type): """Initialize the KEBA switch.""" self._keba = keba self._name = name + self._entity_type = entity_type self._state = True @property @@ -35,13 +36,13 @@ def should_poll(self): @property def unique_id(self): - """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_name}_{self._name}" + """Return the unique ID of the lock.""" + return f"{self._keba.device_id}_{self._entity_type}" @property def name(self): """Return the name of the device.""" - return self._name + return f"{self._keba.device_name} {self._name}" @property def is_locked(self): diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json index 422a79cd0bedd6..0f3d21fc783879 100644 --- a/homeassistant/components/keba/manifest.json +++ b/homeassistant/components/keba/manifest.json @@ -2,7 +2,7 @@ "domain": "keba", "name": "Keba Charging Station", "documentation": "https://www.home-assistant.io/integrations/keba", - "requirements": ["keba-kecontact==0.2.0"], + "requirements": ["keba-kecontact==1.0.0"], "dependencies": [], "codeowners": [ "@dannerph" diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index dfa04f95c65a3f..d9e6118ff32088 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -17,15 +17,40 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= keba = hass.data[DOMAIN] sensors = [ - KebaSensor(keba, "Curr user", "Max current", "mdi:flash", "A"), + KebaSensor(keba, "Curr user", "Max Current", "max_current", "mdi:flash", "A"), KebaSensor( - keba, "Setenergy", "Energy target", "mdi:gauge", ENERGY_KILO_WATT_HOUR + keba, + "Setenergy", + "Energy Target", + "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 + keba, + "P", + "Charging Power", + "charging_power", + "mdi:flash", + "kW", + DEVICE_CLASS_POWER, + ), + KebaSensor( + keba, + "E pres", + "Session Energy", + "session_energy", + "mdi:gauge", + ENERGY_KILO_WATT_HOUR, + ), + KebaSensor( + keba, + "E total", + "Total Energy", + "total_energy", + "mdi:gauge", + ENERGY_KILO_WATT_HOUR, ), - KebaSensor(keba, "E total", "Total Energy", "mdi:gauge", ENERGY_KILO_WATT_HOUR), ] async_add_entities(sensors) @@ -33,14 +58,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KebaSensor(Entity): """The entity class for KEBA charging stations sensors.""" - def __init__(self, keba, key, name, icon, unit, device_class=None): + def __init__(self, keba, key, name, entity_type, icon, unit, device_class=None): """Initialize the KEBA Sensor.""" - self._key = key self._keba = keba + self._key = key self._name = name - self._device_class = device_class + self._entity_type = entity_type self._icon = icon self._unit = unit + self._device_class = device_class + self._state = None self._attributes = {} @@ -52,12 +79,12 @@ def should_poll(self): @property def unique_id(self): """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_name}_{self._name}" + return f"{self._keba.device_id}_{self._entity_type}" @property def name(self): """Return the name of the device.""" - return self._name + return f"{self._keba.device_name} {self._name}" @property def device_class(self): diff --git a/requirements_all.txt b/requirements_all.txt index 6919b1031174cb..831933c70b19dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ jsonrpc-websocket==0.6 kaiterra-async-client==0.0.2 # homeassistant.components.keba -keba-kecontact==0.2.0 +keba-kecontact==1.0.0 # homeassistant.scripts.keyring keyring==20.0.0 From 868eb3c735c2136cf81a5f18a19cd08a7d0522a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 22 Dec 2019 20:51:39 +0200 Subject: [PATCH 2521/3953] More helpers type improvements (#30145) --- homeassistant/helpers/check_config.py | 26 +++-- homeassistant/helpers/config_validation.py | 115 ++++++++++++--------- homeassistant/helpers/device_registry.py | 8 +- homeassistant/helpers/entity_registry.py | 42 ++++---- homeassistant/helpers/logging.py | 23 +++-- 5 files changed, 124 insertions(+), 90 deletions(-) diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 81e654247b72aa..1b1e136ed894b3 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -1,6 +1,6 @@ """Helper to check the configuration file.""" -from collections import OrderedDict, namedtuple -from typing import List +from collections import OrderedDict +from typing import List, NamedTuple, Optional import attr import voluptuous as vol @@ -19,15 +19,20 @@ ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import ConfigType from homeassistant.requirements import ( RequirementsNotFound, async_get_integration_with_requirements, ) import homeassistant.util.yaml.loader as yaml_loader -# mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any -CheckConfigError = namedtuple("CheckConfigError", "message domain config") +class CheckConfigError(NamedTuple): + """Configuration check error.""" + + message: str + domain: Optional[str] + config: Optional[ConfigType] @attr.s @@ -36,7 +41,12 @@ class HomeAssistantConfig(OrderedDict): errors: List[CheckConfigError] = attr.ib(default=attr.Factory(list)) - def add_error(self, message, domain=None, config=None): + def add_error( + self, + message: str, + domain: Optional[str] = None, + config: Optional[ConfigType] = None, + ) -> "HomeAssistantConfig": """Add a single error.""" self.errors.append(CheckConfigError(str(message), domain, config)) return self @@ -55,7 +65,9 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig config_dir = hass.config.config_dir result = HomeAssistantConfig() - def _pack_error(package, component, config, message): + def _pack_error( + package: str, component: str, config: ConfigType, message: str + ) -> None: """Handle errors from packages: _log_pkg_error.""" message = "Package {} setup failed. Component {} {}".format( package, component, message @@ -64,7 +76,7 @@ def _pack_error(package, component, config, message): pack_config = core_config[CONF_PACKAGES].get(package, config) result.add_error(message, domain, pack_config) - def _comp_error(ex, domain, config): + def _comp_error(ex: Exception, domain: str, config: ConfigType) -> None: """Handle errors from components: async_log_exception.""" result.add_error(_format_config_error(ex, domain, config), domain, config) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 5787db65102d83..035e1f678bfbf3 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -5,13 +5,26 @@ time as time_sys, timedelta, ) +from enum import Enum import inspect import logging from numbers import Number import os import re from socket import _GLOBAL_DEFAULT_TIMEOUT # type: ignore # private, not in typeshed -from typing import Any, Callable, Dict, List, Optional, TypeVar, Union +from typing import ( + Any, + Callable, + Dict, + Hashable, + List, + Optional, + Pattern, + Type, + TypeVar, + Union, + cast, +) from urllib.parse import urlparse from uuid import UUID @@ -48,12 +61,11 @@ ) from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template as template_helper from homeassistant.helpers.logging import KeywordStyleAdapter from homeassistant.util import slugify as util_slugify import homeassistant.util.dt as dt_util -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any # pylint: disable=invalid-name TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM' or 'HH:MM:SS'" @@ -126,7 +138,7 @@ def boolean(value: Any) -> bool: raise vol.Invalid("invalid boolean value {}".format(value)) -def isdevice(value): +def isdevice(value: Any) -> str: """Validate that value is a real device.""" try: os.stat(value) @@ -135,19 +147,19 @@ def isdevice(value): raise vol.Invalid("No device at {} found".format(value)) -def matches_regex(regex): +def matches_regex(regex: str) -> Callable[[Any], str]: """Validate that the value is a string that matches a regex.""" - regex = re.compile(regex) + compiled = re.compile(regex) def validator(value: Any) -> str: """Validate that value matches the given regex.""" if not isinstance(value, str): raise vol.Invalid("not a string value: {}".format(value)) - if not regex.match(value): + if not compiled.match(value): raise vol.Invalid( "value {} does not match regular expression {}".format( - value, regex.pattern + value, compiled.pattern ) ) @@ -156,14 +168,14 @@ def validator(value: Any) -> str: return validator -def is_regex(value): +def is_regex(value: Any) -> Pattern[Any]: """Validate that a string is a valid regular expression.""" try: r = re.compile(value) return r except TypeError: raise vol.Invalid( - "value {} is of the wrong type for a regular " "expression".format(value) + "value {} is of the wrong type for a regular expression".format(value) ) except re.error: raise vol.Invalid("value {} is not a valid regular expression".format(value)) @@ -204,9 +216,9 @@ def ensure_list(value: Union[T, List[T], None]) -> List[T]: def entity_id(value: Any) -> str: """Validate Entity ID.""" - value = string(value).lower() - if valid_entity_id(value): - return value + str_value = string(value).lower() + if valid_entity_id(str_value): + return str_value raise vol.Invalid("Entity ID {} is an invalid entity id".format(value)) @@ -253,17 +265,17 @@ def validate(values: Union[str, List]) -> List[str]: return validate -def enum(enumClass): +def enum(enumClass: Type[Enum]) -> vol.All: """Create validator for specified enum.""" return vol.All(vol.In(enumClass.__members__), enumClass.__getitem__) -def icon(value): +def icon(value: Any) -> str: """Validate icon.""" - value = str(value) + str_value = str(value) - if ":" in value: - return value + if ":" in str_value: + return str_value raise vol.Invalid('Icons should be specified in the form "prefix:name"') @@ -362,7 +374,7 @@ def time_period_seconds(value: Union[int, str]) -> timedelta: time_period = vol.Any(time_period_str, time_period_seconds, timedelta, time_period_dict) -def match_all(value): +def match_all(value: T) -> T: """Validate that matches all values.""" return value @@ -382,12 +394,12 @@ def remove_falsy(value: List[T]) -> List[T]: return [v for v in value if v] -def service(value): +def service(value: Any) -> str: """Validate service.""" # Services use same format as entities so we can use same helper. - value = string(value).lower() - if valid_entity_id(value): - return value + str_value = string(value).lower() + if valid_entity_id(str_value): + return str_value raise vol.Invalid("Service {} does not match format .".format(value)) @@ -407,7 +419,7 @@ def verify(value: Dict) -> Dict: for key in value.keys(): slug(key) - return schema(value) + return cast(Dict, schema(value)) return verify @@ -416,10 +428,10 @@ def slug(value: Any) -> str: """Validate value is a valid slug.""" if value is None: raise vol.Invalid("Slug should not be None") - value = str(value) - slg = util_slugify(value) - if value == slg: - return value + str_value = str(value) + slg = util_slugify(str_value) + if str_value == slg: + return str_value raise vol.Invalid("invalid slug {} (try {})".format(value, slg)) @@ -458,42 +470,41 @@ def temperature_unit(value: Any) -> str: ) -def template(value): +def template(value: Optional[Any]) -> template_helper.Template: """Validate a jinja2 template.""" - from homeassistant.helpers import template as template_helper if value is None: raise vol.Invalid("template value is None") if isinstance(value, (list, dict, template_helper.Template)): raise vol.Invalid("template value should be a string") - value = template_helper.Template(str(value)) + template_value = template_helper.Template(str(value)) # type: ignore try: - value.ensure_valid() - return value + template_value.ensure_valid() + return cast(template_helper.Template, template_value) except TemplateError as ex: raise vol.Invalid("invalid template ({})".format(ex)) -def template_complex(value): +def template_complex(value: Any) -> Any: """Validate a complex jinja2 template.""" if isinstance(value, list): - return_value = value.copy() - for idx, element in enumerate(return_value): - return_value[idx] = template_complex(element) - return return_value + return_list = value.copy() + for idx, element in enumerate(return_list): + return_list[idx] = template_complex(element) + return return_list if isinstance(value, dict): - return_value = value.copy() - for key, element in return_value.items(): - return_value[key] = template_complex(element) - return return_value + return_dict = value.copy() + for key, element in return_dict.items(): + return_dict[key] = template_complex(element) + return return_dict if isinstance(value, str): return template(value) return value -def datetime(value): +def datetime(value: Any) -> datetime_sys: """Validate datetime.""" if isinstance(value, datetime_sys): return value @@ -509,7 +520,7 @@ def datetime(value): return date_val -def time_zone(value): +def time_zone(value: str) -> str: """Validate timezone.""" if dt_util.get_time_zone(value) is not None: return value @@ -522,7 +533,7 @@ def time_zone(value): weekdays = vol.All(ensure_list, [vol.In(WEEKDAYS)]) -def socket_timeout(value): +def socket_timeout(value: Optional[Any]) -> object: """Validate timeout float > 0.0. None coerced to socket._GLOBAL_DEFAULT_TIMEOUT bare object. @@ -544,12 +555,12 @@ def url(value: Any) -> str: url_in = str(value) if urlparse(url_in).scheme in ["http", "https"]: - return vol.Schema(vol.Url())(url_in) + return cast(str, vol.Schema(vol.Url())(url_in)) raise vol.Invalid("invalid url") -def x10_address(value): +def x10_address(value: str) -> str: """Validate an x10 address.""" regex = re.compile(r"([A-Pa-p]{1})(?:[2-9]|1[0-6]?)$") if not regex.match(value): @@ -557,7 +568,7 @@ def x10_address(value): return str(value).lower() -def uuid4_hex(value): +def uuid4_hex(value: Any) -> str: """Validate a v4 UUID in hex format.""" try: result = UUID(value, version=4) @@ -678,10 +689,12 @@ def validator(config: Dict) -> Dict: # Validator helpers -def key_dependency(key, dependency): +def key_dependency( + key: Hashable, dependency: Hashable +) -> Callable[[Dict[Hashable, Any]], Dict[Hashable, Any]]: """Validate that all dependencies exist for key.""" - def validator(value): + def validator(value: Dict[Hashable, Any]) -> Dict[Hashable, Any]: """Test dependencies.""" if not isinstance(value, dict): raise vol.Invalid("key dependencies require a dict") @@ -696,7 +709,7 @@ def validator(value): return validator -def custom_serializer(schema): +def custom_serializer(schema: Any) -> Any: """Serialize additional types for voluptuous_serialize.""" if schema is positive_time_period_dict: return {"type": "positive_time_period_dict"} diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 4818de83cb9246..512334c8d3c958 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -12,8 +12,7 @@ from .typing import HomeAssistantType -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) _UNDEF = object() @@ -71,10 +70,11 @@ def format_mac(mac: str) -> str: class DeviceRegistry: """Class to hold a registry of devices.""" - def __init__(self, hass): + devices: Dict[str, DeviceEntry] + + def __init__(self, hass: HomeAssistantType) -> None: """Initialize the device registry.""" self.hass = hass - self.devices = None self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index a5bd62d973c8ef..5eb799658802de 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -11,7 +11,7 @@ from collections import OrderedDict from itertools import chain import logging -from typing import Any, Dict, Iterable, List, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, cast import attr @@ -23,6 +23,9 @@ from .typing import HomeAssistantType +if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntry # noqa: F401 + # mypy: allow-untyped-defs, no-check-untyped-defs PATH_REGISTRY = "entity_registry.yaml" @@ -48,7 +51,7 @@ class RegistryEntry: unique_id = attr.ib(type=str) platform = attr.ib(type=str) name = attr.ib(type=str, default=None) - device_id = attr.ib(type=str, default=None) + device_id: Optional[str] = attr.ib(default=None) config_entry_id: Optional[str] = attr.ib(default=None) disabled_by = attr.ib( type=Optional[str], @@ -135,16 +138,16 @@ def async_generate_entity_id( @callback def async_get_or_create( self, - domain, - platform, - unique_id, + domain: str, + platform: str, + unique_id: str, *, - suggested_object_id=None, - config_entry=None, - device_id=None, - known_object_ids=None, - disabled_by=None, - ): + suggested_object_id: Optional[str] = None, + config_entry: Optional["ConfigEntry"] = None, + device_id: Optional[str] = None, + known_object_ids: Optional[Iterable[str]] = None, + disabled_by: Optional[str] = None, + ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None if config_entry: @@ -153,7 +156,7 @@ def async_get_or_create( entity_id = self.async_get_entity_id(domain, platform, unique_id) if entity_id: - return self._async_update_entity( + return self._async_update_entity( # type: ignore entity_id, config_entry_id=config_entry_id or _UNDEF, device_id=device_id or _UNDEF, @@ -228,12 +231,15 @@ def async_update_entity( disabled_by=_UNDEF, ): """Update properties of an entity.""" - return self._async_update_entity( - entity_id, - name=name, - new_entity_id=new_entity_id, - new_unique_id=new_unique_id, - disabled_by=disabled_by, + return cast( # cast until we have _async_update_entity type hinted + RegistryEntry, + self._async_update_entity( + entity_id, + name=name, + new_entity_id=new_entity_id, + new_unique_id=new_unique_id, + disabled_by=disabled_by, + ), ) @callback diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index 7b2507d9e05e61..0b274458045939 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -1,8 +1,7 @@ """Helpers for logging allowing more advanced logging styles to be used.""" import inspect import logging - -# mypy: allow-untyped-defs, no-check-untyped-defs +from typing import Any, Mapping, MutableMapping, Optional, Tuple class KeywordMessage: @@ -12,13 +11,13 @@ class KeywordMessage: Adapted from: https://stackoverflow.com/a/24683360/2267718 """ - def __init__(self, fmt, args, kwargs): - """Initialize a new BraceMessage object.""" + def __init__(self, fmt: Any, args: Any, kwargs: Mapping[str, Any]) -> None: + """Initialize a new KeywordMessage object.""" self._fmt = fmt self._args = args self._kwargs = kwargs - def __str__(self): + def __str__(self) -> str: """Convert the object to a string for logging.""" return str(self._fmt).format(*self._args, **self._kwargs) @@ -26,26 +25,30 @@ def __str__(self): class KeywordStyleAdapter(logging.LoggerAdapter): """Represents an adapter wrapping the logger allowing KeywordMessages.""" - def __init__(self, logger, extra=None): + def __init__( + self, logger: logging.Logger, extra: Optional[Mapping[str, Any]] = None + ) -> None: """Initialize a new StyleAdapter for the provided logger.""" super().__init__(logger, extra or {}) - def log(self, level, msg, *args, **kwargs): + def log(self, level: int, msg: Any, *args: Any, **kwargs: Any) -> None: """Log the message provided at the appropriate level.""" if self.isEnabledFor(level): msg, log_kwargs = self.process(msg, kwargs) - self.logger._log( # pylint: disable=protected-access + self.logger._log( # type: ignore # pylint: disable=protected-access level, KeywordMessage(msg, args, kwargs), (), **log_kwargs ) - def process(self, msg, kwargs): + def process( + self, msg: Any, kwargs: MutableMapping[str, Any] + ) -> Tuple[Any, MutableMapping[str, Any]]: """Process the keyward args in preparation for logging.""" return ( msg, { k: kwargs[k] for k in inspect.getfullargspec( - self.logger._log # pylint: disable=protected-access + self.logger._log # type: ignore # pylint: disable=protected-access ).args[1:] if k in kwargs }, From b1bb2298e0c57f6a40119bce88195722fb4ed0ac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Dec 2019 19:52:39 +0100 Subject: [PATCH 2522/3953] Bump sqlalchemy to 1.3.12 (#30142) --- homeassistant/components/recorder/manifest.json | 6 ++---- homeassistant/components/sql/manifest.json | 10 +++------- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 4ad000866db081..ad0ac979e03dd3 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,9 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": [ - "sqlalchemy==1.3.11" - ], + "requirements": ["sqlalchemy==1.3.12"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 39435524c20cdc..3434b154e2988a 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,11 +2,7 @@ "domain": "sql", "name": "Sql", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": [ - "sqlalchemy==1.3.11" - ], + "requirements": ["sqlalchemy==1.3.12"], "dependencies": [], - "codeowners": [ - "@dgomes" - ] -} \ No newline at end of file + "codeowners": ["@dgomes"] +} diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ce6ad36dbf0b4c..5d14a6e30d60e9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ pytz>=2019.03 pyyaml==5.2.0 requests==2.22.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.11 +sqlalchemy==1.3.12 voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.24.2 diff --git a/requirements_all.txt b/requirements_all.txt index 831933c70b19dc..dddbcf7a3fd8d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1872,7 +1872,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.11 +sqlalchemy==1.3.12 # homeassistant.components.starline starline==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0cc01b5f47f616..e5361e7464e80a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -578,7 +578,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.11 +sqlalchemy==1.3.12 # homeassistant.components.starline starline==0.1.3 From d4ff214fce5680dcf753e0d16d4f51ccb8bf9d95 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 22 Dec 2019 19:53:03 +0100 Subject: [PATCH 2523/3953] Clean up scaffold (#30135) --- .../config_flow_oauth2/tests/test_config_flow.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py index 50540594a3424b..ec332de13e213f 100644 --- a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py @@ -1,5 +1,5 @@ """Test the NEW_NAME config flow.""" -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.const import ( DOMAIN, OAUTH2_AUTHORIZE, @@ -17,11 +17,7 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): hass, "NEW_DOMAIN", { - "NEW_DOMAIN": { - "type": "oauth2", - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, - }, + "NEW_DOMAIN": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, "http": {"base_url": "https://example.com"}, }, ) @@ -31,7 +27,6 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): ) state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -56,5 +51,3 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.data["type"] == "oauth2" From fdaaabf07015a88c33203d2724e54966ddbaf0c3 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 22 Dec 2019 14:07:49 -0500 Subject: [PATCH 2524/3953] ZHA binary_sensor cleanup. (#30149) --- homeassistant/components/zha/binary_sensor.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index ed09a190adb48e..954fa8b29aa62e 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -46,13 +46,6 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) -DEVICE_CLASS_REGISTRY = { - CHANNEL_ACCELEROMETER: DEVICE_CLASS_MOVING, - CHANNEL_OCCUPANCY: DEVICE_CLASS_OCCUPANCY, - CHANNEL_ON_OFF: DEVICE_CLASS_OPENING, -} - - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up Zigbee Home Automation binary sensors.""" pass @@ -152,7 +145,7 @@ async def async_update(self): class Accelerometer(BinarySensor): """ZHA BinarySensor.""" - DEVICE_CLASS = DEVICE_CLASS_MOTION + DEVICE_CLASS = DEVICE_CLASS_MOVING @STRICT_MATCH(MatchRule(channel_names={CHANNEL_OCCUPANCY})) From d101d4449fa056548bf787645eb0b9a0aa8ee827 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Dec 2019 22:24:18 +0100 Subject: [PATCH 2525/3953] Fix deconz SSDP updating Hassio discovery (#30153) --- .../components/deconz/config_flow.py | 8 ++++- tests/components/deconz/test_config_flow.py | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index f44acdbd3050ce..c84192456d1d8f 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -161,7 +161,11 @@ async def _create_entry(self): def _update_entry(self, entry, host, port, api_key=None): """Update existing entry.""" - if entry.data[CONF_HOST] == host: + if ( + entry.data[CONF_HOST] == host + and entry.data[CONF_PORT] == port + and (api_key is None or entry.data[CONF_API_KEY] == api_key) + ): return self.async_abort(reason="already_configured") entry.data[CONF_HOST] = host @@ -186,6 +190,8 @@ async def async_step_ssdp(self, discovery_info): for entry in self.hass.config_entries.async_entries(DOMAIN): if uuid == entry.data.get(CONF_UUID): + if entry.source == "hassio": + return self.async_abort(reason="already_configured") return self._update_entry(entry, parsed_url.hostname, parsed_url.port) bridgeid = discovery_info[ssdp.ATTR_UPNP_SERIAL] diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index ddcbc1a1d5fac4..da8b0a8a7f47e8 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -269,6 +269,39 @@ async def test_bridge_discovery_update_existing_entry(hass): assert entry.data[config_flow.CONF_HOST] == "mock-deconz" +async def test_bridge_discovery_dont_update_existing_hassio_entry(hass): + """Test to ensure the SSDP discovery does not update an Hass.io entry.""" + entry = MockConfigEntry( + domain=config_flow.DOMAIN, + source="hassio", + data={ + config_flow.CONF_HOST: "core-deconz", + config_flow.CONF_BRIDGEID: "123ABC", + config_flow.CONF_UUID: "456DEF", + }, + ) + entry.add_to_hass(hass) + + gateway = Mock() + gateway.config_entry = entry + hass.data[config_flow.DOMAIN] = {"123ABC": gateway} + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + data={ + ssdp.ATTR_SSDP_LOCATION: "http://mock-deconz/", + ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL, + ssdp.ATTR_UPNP_SERIAL: "123ABC", + ssdp.ATTR_UPNP_UDN: "uuid:456DEF", + }, + context={"source": "ssdp"}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[config_flow.CONF_HOST] == "core-deconz" + + async def test_create_entry(hass, aioclient_mock): """Test that _create_entry work and that bridgeid can be requested.""" aioclient_mock.get( From 697833bc917bdfc3db56fe0559c61dbf9bb8ad1f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Dec 2019 10:32:42 +0100 Subject: [PATCH 2526/3953] Remove requirement from entity integration (#30113) --- .../components/doods/image_processing.py | 2 +- homeassistant/components/doods/manifest.json | 14 +++--- .../components/image_processing/__init__.py | 42 ----------------- .../components/image_processing/manifest.json | 8 +--- .../components/seven_segments/manifest.json | 2 +- .../components/tensorflow/image_processing.py | 2 +- .../components/tensorflow/manifest.json | 7 ++- homeassistant/util/pil.py | 47 +++++++++++++++++++ requirements_all.txt | 4 +- requirements_test_all.txt | 5 -- 10 files changed, 67 insertions(+), 66 deletions(-) create mode 100644 homeassistant/util/pil.py diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 7f2c3fd1d4276d..bd631f757d59c3 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -15,11 +15,11 @@ CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, - draw_box, ) from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv +from homeassistant.util.pil import draw_box _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index e0dcb48527f91b..551af839b5c475 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -1,10 +1,8 @@ { - "domain": "doods", - "name": "DOODS - Distributed Outside Object Detection Service", - "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": [ - "pydoods==1.0.2" - ], - "dependencies": [], - "codeowners": [] + "domain": "doods", + "name": "DOODS - Distributed Outside Object Detection Service", + "documentation": "https://www.home-assistant.io/integrations/doods", + "requirements": ["pydoods==1.0.2", "pillow==6.2.1"], + "dependencies": [], + "codeowners": [] } diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 78ae15eb5372b4..a8f5f0f097e79c 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -2,9 +2,7 @@ import asyncio from datetime import timedelta import logging -from typing import Tuple -from PIL import ImageDraw import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME @@ -65,46 +63,6 @@ PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE.extend(PLATFORM_SCHEMA.schema) -def draw_box( - draw: ImageDraw, - box: Tuple[float, float, float, float], - img_width: int, - img_height: int, - text: str = "", - color: Tuple[int, int, int] = (255, 255, 0), -) -> None: - """ - Draw a bounding box on and image. - - The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) - where the coordinates are floats in the range [0.0, 1.0] and - relative to the width and height of the image. - - For example, if an image is 100 x 200 pixels (height x width) and the bounding - box is `(0.1, 0.2, 0.5, 0.9)`, the upper-left and bottom-right coordinates of - the bounding box will be `(40, 10)` to `(180, 50)` (in (x,y) coordinates). - """ - - line_width = 3 - font_height = 8 - y_min, x_min, y_max, x_max = box - (left, right, top, bottom) = ( - x_min * img_width, - x_max * img_width, - y_min * img_height, - y_max * img_height, - ) - draw.line( - [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], - width=line_width, - fill=color, - ) - if text: - draw.text( - (left + line_width, abs(top - line_width - font_height)), text, fill=color - ) - - async def async_setup(hass, config): """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index e986ac6f4ca193..1e9f2963a38735 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -2,11 +2,7 @@ "domain": "image_processing", "name": "Image processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", - "requirements": [ - "pillow==6.2.1" - ], - "dependencies": [ - "camera" - ], + "requirements": [], + "dependencies": ["camera"], "codeowners": [] } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 0bf49d4b9066e7..6e375da03225cf 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven segments", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": [], + "requirements": ["pillow==6.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 5f576f176e8261..dee2a0218299b0 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -15,11 +15,11 @@ CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingEntity, - draw_box, ) from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv +from homeassistant.util.pil import draw_box _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 278a92a481f9f2..627fa8d6bd6e43 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -2,7 +2,12 @@ "domain": "tensorflow", "name": "Tensorflow", "documentation": "https://www.home-assistant.io/integrations/tensorflow", - "requirements": ["tensorflow==1.13.2", "numpy==1.17.4", "protobuf==3.6.1"], + "requirements": [ + "tensorflow==1.13.2", + "numpy==1.17.4", + "protobuf==3.6.1", + "pillow==6.2.1" + ], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/util/pil.py b/homeassistant/util/pil.py new file mode 100644 index 00000000000000..80c11c9c4104c0 --- /dev/null +++ b/homeassistant/util/pil.py @@ -0,0 +1,47 @@ +"""PIL utilities. + +Can only be used by integrations that have pillow in their requirements. +""" +from typing import Tuple + +from PIL import ImageDraw + + +def draw_box( + draw: ImageDraw, + box: Tuple[float, float, float, float], + img_width: int, + img_height: int, + text: str = "", + color: Tuple[int, int, int] = (255, 255, 0), +) -> None: + """ + Draw a bounding box on and image. + + The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) + where the coordinates are floats in the range [0.0, 1.0] and + relative to the width and height of the image. + + For example, if an image is 100 x 200 pixels (height x width) and the bounding + box is `(0.1, 0.2, 0.5, 0.9)`, the upper-left and bottom-right coordinates of + the bounding box will be `(40, 10)` to `(180, 50)` (in (x,y) coordinates). + """ + + line_width = 3 + font_height = 8 + y_min, x_min, y_max, x_max = box + (left, right, top, bottom) = ( + x_min * img_width, + x_max * img_width, + y_min * img_height, + y_max * img_height, + ) + draw.line( + [(left, top), (left, bottom), (right, bottom), (right, top), (left, top)], + width=line_width, + fill=color, + ) + if text: + draw.text( + (left + line_width, abs(top - line_width - font_height)), text, fill=color + ) diff --git a/requirements_all.txt b/requirements_all.txt index e876b9144de548..9ee9b9a740a2c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -980,9 +980,11 @@ piglow==1.2.4 # homeassistant.components.pilight pilight==0.1.1 -# homeassistant.components.image_processing +# homeassistant.components.doods # homeassistant.components.proxy # homeassistant.components.qrcode +# homeassistant.components.seven_segments +# homeassistant.components.tensorflow pillow==6.2.1 # homeassistant.components.dominos diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86c3a657988485..9b71d8092be11e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,11 +320,6 @@ pexpect==4.6.0 # homeassistant.components.pilight pilight==0.1.1 -# homeassistant.components.image_processing -# homeassistant.components.proxy -# homeassistant.components.qrcode -pillow==6.2.1 - # homeassistant.components.plex plexapi==3.3.0 From e74c4c5d998d5e3cdd44c204785d5db7ee4b4386 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 22 Dec 2019 10:23:44 +0100 Subject: [PATCH 2527/3953] Move imports into setup function in homekit __init__.py (#30137) --- homeassistant/components/homekit/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 525c091e177ba8..91d0719e0e72cb 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -303,6 +303,7 @@ def __init__( def setup(self): """Set up bridge and accessory driver.""" + # pylint: disable=import-outside-toplevel from .accessories import HomeBridge, HomeDriver self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop) From e9b19e0465b36c7a41d8e530f0a7b122c2483e0b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 22 Dec 2019 22:24:18 +0100 Subject: [PATCH 2528/3953] Fix deconz SSDP updating Hassio discovery (#30153) --- homeassistant/components/deconz/config_flow.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index c2035d61d6e0a3..62b8190ff63ff4 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -160,7 +160,11 @@ async def _create_entry(self): def _update_entry(self, entry, host, port, api_key=None): """Update existing entry.""" - if entry.data[CONF_HOST] == host: + if ( + entry.data[CONF_HOST] == host + and entry.data[CONF_PORT] == port + and (api_key is None or entry.data[CONF_API_KEY] == api_key) + ): return self.async_abort(reason="already_configured") entry.data[CONF_HOST] = host @@ -187,6 +191,8 @@ async def async_step_ssdp(self, discovery_info): for entry in self.hass.config_entries.async_entries(DOMAIN): if uuid == entry.data.get(CONF_UUID): + if entry.source == "hassio": + return self.async_abort(reason="already_configured") return self._update_entry( entry, discovery_info[CONF_HOST], entry.data.get(CONF_PORT) ) From 720d63f4961fdb826178e377ab5da4b75f9e0b81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Dec 2019 22:34:57 +0100 Subject: [PATCH 2529/3953] Bumped version to 0.103.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6239be3cb61512..c23ed1cdd03725 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 103 -PATCH_VERSION = "3" +PATCH_VERSION = "4" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From e9dc404de15f32eeff9a54a0e541381e6a8a57a8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 22 Dec 2019 22:58:22 +0100 Subject: [PATCH 2530/3953] Allow battery value of 0 as well as make sure to not create a battery tracker if one already exist (#30155) --- homeassistant/components/deconz/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 4c854a0ec11578..4ffaba9b499385 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -59,7 +59,7 @@ def async_add_sensor(sensors, new=True): entity_handler.add_entity(new_sensor) entities.append(new_sensor) - if sensor.battery: + if sensor.battery is not None: new_battery = DeconzBattery(sensor, gateway) if new_battery.unique_id not in batteries: batteries.add(new_battery.unique_id) @@ -225,6 +225,9 @@ def __init__(self, gateway): @callback def create_tracker(self, sensor): """Create new tracker for battery state.""" + for tracker in self._trackers: + if sensor == tracker.sensor: + return self._trackers.add(DeconzSensorStateTracker(sensor, self.gateway)) @callback From 31b17e2b95b82a5c507f26641c92b6f4e3569602 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 22 Dec 2019 22:58:22 +0100 Subject: [PATCH 2531/3953] Allow battery value of 0 as well as make sure to not create a battery tracker if one already exist (#30155) --- homeassistant/components/deconz/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 4c854a0ec11578..4ffaba9b499385 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -59,7 +59,7 @@ def async_add_sensor(sensors, new=True): entity_handler.add_entity(new_sensor) entities.append(new_sensor) - if sensor.battery: + if sensor.battery is not None: new_battery = DeconzBattery(sensor, gateway) if new_battery.unique_id not in batteries: batteries.add(new_battery.unique_id) @@ -225,6 +225,9 @@ def __init__(self, gateway): @callback def create_tracker(self, sensor): """Create new tracker for battery state.""" + for tracker in self._trackers: + if sensor == tracker.sensor: + return self._trackers.add(DeconzSensorStateTracker(sensor, self.gateway)) @callback From a2678b2affea5bc11da7f470a5b554803cc3b5ea Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Mon, 23 Dec 2019 09:17:37 -0500 Subject: [PATCH 2532/3953] Add support for input_number entities in Alexa integration (#30139) * Add support for input_number entities * Update homeassistant/components/alexa/capabilities.py Co-Authored-By: Paulus Schoutsen * Removed get methods to directly access required attributes dicts. Co-authored-by: Paulus Schoutsen --- .../components/alexa/capabilities.py | 28 ++- homeassistant/components/alexa/entities.py | 19 ++ homeassistant/components/alexa/handlers.py | 39 +++- homeassistant/components/alexa/resources.py | 6 +- tests/components/alexa/test_smart_home.py | 172 ++++++++++++++++++ 5 files changed, 256 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 938101a75001d6..513a0c157d38a8 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,7 +1,7 @@ """Alexa capabilities.""" import logging -from homeassistant.components import cover, fan, image_processing, light +from homeassistant.components import cover, fan, image_processing, input_number, light from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER import homeassistant.components.climate.const as climate import homeassistant.components.media_player.const as media_player @@ -1054,6 +1054,10 @@ def get_property(self, name): if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION) + # Input Number Value + if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + return float(self.entity.state) + return None def configuration(self): @@ -1110,6 +1114,28 @@ def capability_resources(self): ) return self._resource.serialize_capability_resources() + # Input Number Value + if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + min_value = float(self.entity.attributes[input_number.ATTR_MIN]) + max_value = float(self.entity.attributes[input_number.ATTR_MAX]) + precision = float(self.entity.attributes.get(input_number.ATTR_STEP, 1)) + unit = self.entity.attributes.get(input_number.ATTR_UNIT_OF_MEASUREMENT) + + self._resource = AlexaPresetResource( + ["Value"], + min_value=min_value, + max_value=max_value, + precision=precision, + unit=unit, + ) + self._resource.add_preset( + value=min_value, labels=[AlexaGlobalCatalog.VALUE_MINIMUM] + ) + self._resource.add_preset( + value=max_value, labels=[AlexaGlobalCatalog.VALUE_MAXIMUM] + ) + return self._resource.serialize_capability_resources() + return None def semantics(self): diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 89ca646890b6ce..ab3dc75bd2ccc2 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -11,6 +11,7 @@ group, image_processing, input_boolean, + input_number, light, lock, media_player, @@ -674,3 +675,21 @@ def interfaces(self): yield AlexaEventDetectionSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(input_number.DOMAIN) +class InputNumberCapabilities(AlexaEntity): + """Class to represent input_number capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + + yield AlexaRangeController( + self.entity, instance=f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}" + ) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index b5603af7402424..133919be84d6c9 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -3,7 +3,14 @@ import math from homeassistant import core as ha -from homeassistant.components import cover, fan, group, light, media_player +from homeassistant.components import ( + cover, + fan, + group, + input_number, + light, + media_player, +) from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_ENTITY_ID, @@ -1080,12 +1087,12 @@ async def async_api_set_range(hass, config, directive, context): domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - range_value = int(directive.payload["rangeValue"]) + range_value = directive.payload["rangeValue"] # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": service = fan.SERVICE_SET_SPEED - speed = SPEED_FAN_MAP.get(range_value, None) + speed = SPEED_FAN_MAP.get(int(range_value)) if not speed: msg = "Entity does not support value" @@ -1098,6 +1105,7 @@ async def async_api_set_range(hass, config, directive, context): # Cover Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + range_value = int(range_value) if range_value == 0: service = cover.SERVICE_CLOSE_COVER elif range_value == 100: @@ -1108,6 +1116,7 @@ async def async_api_set_range(hass, config, directive, context): # Cover Tilt Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + range_value = int(range_value) if range_value == 0: service = cover.SERVICE_CLOSE_COVER_TILT elif range_value == 100: @@ -1116,6 +1125,14 @@ async def async_api_set_range(hass, config, directive, context): service = cover.SERVICE_SET_COVER_TILT_POSITION data[cover.ATTR_POSITION] = range_value + # Input Number Value + elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + range_value = float(range_value) + service = input_number.SERVICE_SET_VALUE + min_value = float(entity.attributes[input_number.ATTR_MIN]) + max_value = float(entity.attributes[input_number.ATTR_MAX]) + data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value)) + else: msg = "Entity does not support directive" raise AlexaInvalidDirectiveError(msg) @@ -1145,11 +1162,12 @@ async def async_api_adjust_range(hass, config, directive, context): domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - range_delta = int(directive.payload["rangeValueDelta"]) + range_delta = directive.payload["rangeValueDelta"] response_value = 0 # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + range_delta = int(range_delta) service = fan.SERVICE_SET_SPEED current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0) speed = SPEED_FAN_MAP.get( @@ -1163,6 +1181,7 @@ async def async_api_adjust_range(hass, config, directive, context): # Cover Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + range_delta = int(range_delta) service = SERVICE_SET_COVER_POSITION current = entity.attributes.get(cover.ATTR_POSITION) data[cover.ATTR_POSITION] = response_value = min( @@ -1171,12 +1190,24 @@ async def async_api_adjust_range(hass, config, directive, context): # Cover Tilt Position elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + range_delta = int(range_delta) service = SERVICE_SET_COVER_TILT_POSITION current = entity.attributes.get(cover.ATTR_TILT_POSITION) data[cover.ATTR_TILT_POSITION] = response_value = min( 100, max(0, range_delta + current) ) + # Input Number Value + elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + range_delta = float(range_delta) + service = input_number.SERVICE_SET_VALUE + min_value = float(entity.attributes[input_number.ATTR_MIN]) + max_value = float(entity.attributes[input_number.ATTR_MAX]) + current = float(entity.state) + data[input_number.ATTR_VALUE] = response_value = min( + max_value, max(min_value, range_delta + current) + ) + else: msg = "Entity does not support directive" raise AlexaInvalidDirectiveError(msg) diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index 061005252dc739..09927321c3671b 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -266,9 +266,9 @@ def __init__(self, labels, min_value, max_value, precision, unit=None): """Initialize an Alexa presetResource.""" super().__init__(labels) self._presets = [] - self._minimum_value = int(min_value) - self._maximum_value = int(max_value) - self._precision = int(precision) + self._minimum_value = min_value + self._maximum_value = max_value + self._precision = precision self._unit_of_measure = None if unit in AlexaGlobalCatalog.__dict__.values(): self._unit_of_measure = unit diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 468652bf6d28ee..3d7c2b118e7cf6 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2740,3 +2740,175 @@ async def test_cover_semantics(hass): "states": ["Alexa.States.Open"], "range": {"minimumValue": 1, "maximumValue": 100}, } in state_mappings + + +async def test_input_number(hass): + """Test input_number discovery.""" + device = ( + "input_number.test_slider", + 30, + { + "initial": 30, + "min": -20, + "max": 35, + "step": 1, + "mode": "slider", + "friendly_name": "Test Slider", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "input_number#test_slider" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Test Slider" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + ) + + range_capability = get_capability( + capabilities, "Alexa.RangeController", "input_number.value" + ) + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Value", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == -20 + assert supported_range["maximumValue"] == 35 + assert supported_range["precision"] == 1 + + presets = configuration["presets"] + assert { + "rangeValue": 35, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}} + ] + }, + } in presets + + assert { + "rangeValue": -20, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Minimum"}} + ] + }, + } in presets + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "input_number#test_slider", + "input_number.set_value", + hass, + payload={"rangeValue": "10"}, + instance="input_number.value", + ) + assert call.data["value"] == 10 + + await assert_range_changes( + hass, + [(25, "-5"), (35, "5"), (-20, "-100"), (35, "100")], + "Alexa.RangeController", + "AdjustRangeValue", + "input_number#test_slider", + False, + "input_number.set_value", + "value", + instance="input_number.value", + ) + + +async def test_input_number_float(hass): + """Test input_number discovery.""" + device = ( + "input_number.test_slider_float", + 0.5, + { + "initial": 0.5, + "min": 0, + "max": 1, + "step": 0.01, + "mode": "slider", + "friendly_name": "Test Slider Float", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "input_number#test_slider_float" + assert appliance["displayCategories"][0] == "OTHER" + assert appliance["friendlyName"] == "Test Slider Float" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.RangeController", "Alexa.EndpointHealth", "Alexa" + ) + + range_capability = get_capability( + capabilities, "Alexa.RangeController", "input_number.value" + ) + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "text", + "value": {"text": "Value", "locale": "en-US"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + + supported_range = configuration["supportedRange"] + assert supported_range["minimumValue"] == 0 + assert supported_range["maximumValue"] == 1 + assert supported_range["precision"] == 0.01 + + presets = configuration["presets"] + assert { + "rangeValue": 1, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Maximum"}} + ] + }, + } in presets + + assert { + "rangeValue": 0, + "presetResources": { + "friendlyNames": [ + {"@type": "asset", "value": {"assetId": "Alexa.Value.Minimum"}} + ] + }, + } in presets + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "input_number#test_slider_float", + "input_number.set_value", + hass, + payload={"rangeValue": "0.333"}, + instance="input_number.value", + ) + assert call.data["value"] == 0.333 + + await assert_range_changes( + hass, + [(0.4, "-0.1"), (0.6, "0.1"), (0, "-100"), (1, "100"), (0.51, "0.01")], + "Alexa.RangeController", + "AdjustRangeValue", + "input_number#test_slider_float", + False, + "input_number.set_value", + "value", + instance="input_number.value", + ) From e3a396491119fc8f436415ede7ea3543e6b5e8c7 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 23 Dec 2019 17:25:57 +0100 Subject: [PATCH 2533/3953] Add icon to Plex sensor (#30172) --- homeassistant/components/plex/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 2a994b08a7bcb7..2aed57946eb722 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -86,6 +86,11 @@ def unit_of_measurement(self): """Return the unit this state is expressed in.""" return "Watching" + @property + def icon(self): + """Return the icon of the sensor.""" + return "mdi:plex" + @property def device_state_attributes(self): """Return the state attributes.""" From 4c5801ee8c856c58ac74de9634857649336f299f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Mon, 23 Dec 2019 17:34:57 +0100 Subject: [PATCH 2534/3953] Add cast to state of Dyson Air Quality Sensor (#30100) --- homeassistant/components/dyson/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index c51f46c77901c2..2fdd3cd6c1fc02 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -195,5 +195,5 @@ def __init__(self, device): def state(self): """Return Air Quality value.""" if self._device.environmental_state: - return self._device.environmental_state.volatil_organic_compounds + return int(self._device.environmental_state.volatil_organic_compounds) return None From f72ba0c716b945ad30ff80f52fb986bd60aaf3e9 Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 23 Dec 2019 17:37:41 +0100 Subject: [PATCH 2535/3953] Add icons to Freebox sensors (#30132) --- homeassistant/components/freebox/sensor.py | 24 +++++++++++++--------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index e8f9658630066f..61ec670d217ef8 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -18,6 +18,8 @@ class FbxSensor(Entity): """Representation of a freebox sensor.""" _name = "generic" + _unit = None + _icon = None def __init__(self, fbx): """Initialize the sensor.""" @@ -30,6 +32,16 @@ def name(self): """Return the name of the sensor.""" return self._name + @property + def unit_of_measurement(self): + """Return the unit of the sensor.""" + return self._unit + + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + @property def state(self): """Return the state of the sensor.""" @@ -45,11 +57,7 @@ class FbxRXSensor(FbxSensor): _name = "Freebox download speed" _unit = "KB/s" - - @property - def unit_of_measurement(self): - """Define the unit.""" - return self._unit + _icon = "mdi:download-network" async def async_update(self): """Get the value from fetched datas.""" @@ -63,11 +71,7 @@ class FbxTXSensor(FbxSensor): _name = "Freebox upload speed" _unit = "KB/s" - - @property - def unit_of_measurement(self): - """Define the unit.""" - return self._unit + _icon = "mdi:upload-network" async def async_update(self): """Get the value from fetched datas.""" From f23cc16660c27b041fa034f9e06310dfd0caa882 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Mon, 23 Dec 2019 12:35:54 -0800 Subject: [PATCH 2536/3953] Upgrade envoy_reader to 0.11.0 (#30179) --- 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 21d154be23ab54..fc79659ff76450 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/integrations/enphase_envoy", "requirements": [ - "envoy_reader==0.10.0" + "envoy_reader==0.11.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index dddbcf7a3fd8d6..252665f527bb99 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -483,7 +483,7 @@ env_canada==0.0.30 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.10.0 +envoy_reader==0.11.0 # homeassistant.components.season ephem==3.7.7.0 From 059e8722b6af0c289d5455a2a212c56de16f1409 Mon Sep 17 00:00:00 2001 From: P-Verbrugge <41943098+P-Verbrugge@users.noreply.github.com> Date: Mon, 23 Dec 2019 21:42:44 +0100 Subject: [PATCH 2537/3953] Updated formatting of total_blocks value (#30170) The number of total blocks is always a round number. There can't be .1 or .11 blocks for example. The output is now always formatted with two decimals that are always 00. --- homeassistant/components/bitcoin/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index b62bb434e85f83..bc8394d51a5806 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -148,7 +148,7 @@ def update(self): elif self.type == "total_btc": self._state = "{0:.2f}".format(stats.total_btc * 0.00000001) elif self.type == "total_blocks": - self._state = "{0:.2f}".format(stats.total_blocks) + self._state = "{0:.0f}".format(stats.total_blocks) elif self.type == "next_retarget": self._state = "{0:.2f}".format(stats.next_retarget) elif self.type == "estimated_transaction_volume_usd": From edce497a0d48ba25a8b5a5373c4eec2a85b4a4a7 Mon Sep 17 00:00:00 2001 From: Claudio Heckler Date: Mon, 23 Dec 2019 17:48:24 -0300 Subject: [PATCH 2538/3953] New date_time_utc display option added to the time_date sensor platform (#30158) --- homeassistant/components/time_date/sensor.py | 6 ++- tests/components/time_date/test_sensor.py | 49 +++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index 9edbdd3a9a1901..1deb564133edb4 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -20,7 +20,8 @@ "time": "Time", "date": "Date", "date_time": "Date & Time", - "date_time_iso": "Date & Time ISO", + "date_time_utc": "Date & Time (UTC)", + "date_time_iso": "Date & Time (ISO)", "time_date": "Time & Date", "beat": "Internet Time", "time_utc": "Time (UTC)", @@ -102,6 +103,7 @@ def _update_internal_state(self, time_date): time = dt_util.as_local(time_date).strftime(TIME_STR_FORMAT) time_utc = time_date.strftime(TIME_STR_FORMAT) date = dt_util.as_local(time_date).date().isoformat() + date_utc = time_date.date().isoformat() # Calculate Swatch Internet Time. time_bmt = time_date + timedelta(hours=1) @@ -119,6 +121,8 @@ def _update_internal_state(self, time_date): self._state = date elif self.type == "date_time": self._state = f"{date}, {time}" + elif self.type == "date_time_utc": + self._state = f"{date_utc}, {time_utc}" elif self.type == "time_date": self._state = f"{time}, {date}" elif self.type == "time_utc": diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index cdd3bd7e9f36cf..2aae99f93a5bd4 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -1,4 +1,4 @@ -"""The tests for Kira sensor platform.""" +"""The tests for time_date sensor platform.""" import unittest from unittest.mock import patch @@ -9,7 +9,7 @@ class TestTimeDateSensor(unittest.TestCase): - """Tests the Kira Sensor platform.""" + """Tests the time_date Sensor platform.""" # pylint: disable=invalid-name DEVICES = [] @@ -67,6 +67,14 @@ def test_states(self): device._update_internal_state(now) assert device.state == "00:54" + device = time_date.TimeDateSensor(self.hass, "date_time") + device._update_internal_state(now) + assert device.state == "2017-05-18, 00:54" + + device = time_date.TimeDateSensor(self.hass, "date_time_utc") + device._update_internal_state(now) + assert device.state == "2017-05-18, 00:54" + device = time_date.TimeDateSensor(self.hass, "beat") device._update_internal_state(now) assert device.state == "@079" @@ -75,6 +83,41 @@ def test_states(self): device._update_internal_state(now) assert device.state == "2017-05-18T00:54:00" + def test_states_non_default_timezone(self): + """Test states of sensors in a timezone other than UTC.""" + new_tz = dt_util.get_time_zone("America/New_York") + assert new_tz is not None + dt_util.set_default_time_zone(new_tz) + + now = dt_util.utc_from_timestamp(1495068856) + device = time_date.TimeDateSensor(self.hass, "time") + device._update_internal_state(now) + assert device.state == "20:54" + + device = time_date.TimeDateSensor(self.hass, "date") + device._update_internal_state(now) + assert device.state == "2017-05-17" + + device = time_date.TimeDateSensor(self.hass, "time_utc") + device._update_internal_state(now) + assert device.state == "00:54" + + device = time_date.TimeDateSensor(self.hass, "date_time") + device._update_internal_state(now) + assert device.state == "2017-05-17, 20:54" + + device = time_date.TimeDateSensor(self.hass, "date_time_utc") + device._update_internal_state(now) + assert device.state == "2017-05-18, 00:54" + + device = time_date.TimeDateSensor(self.hass, "beat") + device._update_internal_state(now) + assert device.state == "@079" + + device = time_date.TimeDateSensor(self.hass, "date_time_iso") + device._update_internal_state(now) + assert device.state == "2017-05-17T20:54:00" + # pylint: disable=no-member def test_timezone_intervals(self): """Test date sensor behavior in a timezone besides UTC.""" @@ -122,5 +165,7 @@ def test_icons(self): assert device.icon == "mdi:calendar" device = time_date.TimeDateSensor(self.hass, "date_time") assert device.icon == "mdi:calendar-clock" + device = time_date.TimeDateSensor(self.hass, "date_time_utc") + assert device.icon == "mdi:calendar-clock" device = time_date.TimeDateSensor(self.hass, "date_time_iso") assert device.icon == "mdi:calendar-clock" From 3aa2ae170010cb5d9bcfdbed4f68a0a4072a99ce Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 23 Dec 2019 12:54:25 -0800 Subject: [PATCH 2539/3953] Enable config flow for Tesla (#28744) * build: bump teslajsonpy to 0.2.0 * Remove tests * feat: add config flow * feat: add async * perf: convert unnecessary async calls to sync * feat: add charger voltage and current sensor * feat: add options flow * build: bump teslajsonpy to 0.2.0 * Remove icon property * Revert climate mode change * Remove charger sensor * Simplify async_setup_platform * Update homeassistant/components/tesla/sensor.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/tesla/binary_sensor.py Co-Authored-By: Paulus Schoutsen * Address requested changes * Fix pylint error * Address requested changes * Update codeowners * Fix pylint error * Address requested changes * Address requested change * Remove unnecessary check for existing config entry * Load scan_interval in async_setup_entry * Include coverage of config_flow * Add tests for full coverage * Address requested test changes * Remove unnecessary init lines * Remove unnecessary init Co-authored-by: Paulus Schoutsen --- .coveragerc | 9 +- CODEOWNERS | 2 +- .../components/tesla/.translations/en.json | 30 +++ homeassistant/components/tesla/__init__.py | 187 ++++++++++++++---- .../components/tesla/binary_sensor.py | 30 ++- homeassistant/components/tesla/climate.py | 29 ++- homeassistant/components/tesla/config_flow.py | 143 ++++++++++++++ .../components/tesla/device_tracker.py | 85 +++++--- homeassistant/components/tesla/lock.py | 23 ++- homeassistant/components/tesla/manifest.json | 3 +- homeassistant/components/tesla/sensor.py | 24 ++- homeassistant/components/tesla/strings.json | 30 +++ homeassistant/components/tesla/switch.py | 33 ++-- homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/tesla/__init__.py | 1 + tests/components/tesla/test_config_flow.py | 160 +++++++++++++++ 17 files changed, 671 insertions(+), 122 deletions(-) create mode 100644 homeassistant/components/tesla/.translations/en.json create mode 100644 homeassistant/components/tesla/config_flow.py create mode 100644 homeassistant/components/tesla/strings.json create mode 100644 tests/components/tesla/__init__.py create mode 100644 tests/components/tesla/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index fc7a4691ef2918..70d8f867e0e42d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -688,7 +688,14 @@ omit = homeassistant/components/telnet/switch.py homeassistant/components/temper/sensor.py homeassistant/components/tensorflow/image_processing.py - homeassistant/components/tesla/* + homeassistant/components/tesla/__init__.py + homeassistant/components/tesla/binary_sensor.py + homeassistant/components/tesla/climate.py + homeassistant/components/tesla/const.py + homeassistant/components/tesla/device_tracker.py + homeassistant/components/tesla/lock.py + homeassistant/components/tesla/sensor.py + homeassistant/components/tesla/switch.py homeassistant/components/tfiac/climate.py homeassistant/components/thermoworks_smoke/sensor.py homeassistant/components/thethingsnetwork/* diff --git a/CODEOWNERS b/CODEOWNERS index 4fbdca20686615..a7d1d346c5fe31 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -322,7 +322,7 @@ homeassistant/components/tahoma/* @philklei homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike homeassistant/components/template/* @PhracturedBlue -homeassistant/components/tesla/* @zabuldon +homeassistant/components/tesla/* @zabuldon @alandtse homeassistant/components/tfiac/* @fredrike @mellado homeassistant/components/thethingsnetwork/* @fabaff homeassistant/components/threshold/* @fabaff diff --git a/homeassistant/components/tesla/.translations/en.json b/homeassistant/components/tesla/.translations/en.json new file mode 100644 index 00000000000000..831406a0d63a93 --- /dev/null +++ b/homeassistant/components/tesla/.translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error connecting; check network and retry", + "identifier_exists": "Email already registered", + "invalid_credentials": "Invalid credentials", + "unknown_error": "Unknown error, please report log info" + }, + "step": { + "user": { + "data": { + "username": "Email Address", + "password": "Password" + }, + "description": "Please enter your information.", + "title": "Tesla - Configuration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconds between scans" + } + } + } + } +} diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index a3d45eed01c6b6..dbfe07271ee61b 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -1,21 +1,32 @@ """Support for Tesla cars.""" +import asyncio from collections import defaultdict import logging -from teslajsonpy import Controller as teslaAPI, TeslaException +from teslajsonpy import Controller as TeslaAPI, TeslaException import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_BATTERY_LEVEL, + CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_SCAN_INTERVAL, + CONF_TOKEN, CONF_USERNAME, ) -from homeassistant.helpers import aiohttp_client, config_validation as cv, discovery +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from .const import DOMAIN, TESLA_COMPONENTS +from .config_flow import ( + CannotConnect, + InvalidAuth, + configured_instances, + validate_input, +) +from .const import DATA_LISTENER, DOMAIN, TESLA_COMPONENTS _LOGGER = logging.getLogger(__name__) @@ -34,69 +45,144 @@ extra=vol.ALLOW_EXTRA, ) -NOTIFICATION_ID = "tesla_integration_notification" -NOTIFICATION_TITLE = "Tesla integration setup" + +@callback +def _async_save_tokens(hass, config_entry, access_token, refresh_token): + hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + CONF_ACCESS_TOKEN: access_token, + CONF_TOKEN: refresh_token, + }, + ) async def async_setup(hass, base_config): """Set up of Tesla component.""" - config = base_config.get(DOMAIN) - email = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - update_interval = config.get(CONF_SCAN_INTERVAL) - if hass.data.get(DOMAIN) is None: + def _update_entry(email, data=None, options=None): + data = data or {} + options = options or {CONF_SCAN_INTERVAL: 300} + for entry in hass.config_entries.async_entries(DOMAIN): + if email != entry.title: + continue + hass.config_entries.async_update_entry(entry, data=data, options=options) + + config = base_config.get(DOMAIN) + if not config: + return True + email = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + scan_interval = config[CONF_SCAN_INTERVAL] + if email in configured_instances(hass): try: - websession = aiohttp_client.async_get_clientsession(hass) - controller = teslaAPI( - websession, - email=email, - password=password, - update_interval=update_interval, - ) - await controller.connect(test_login=False) - hass.data[DOMAIN] = {"controller": controller, "devices": defaultdict(list)} - _LOGGER.debug("Connected to the Tesla API.") - except TeslaException as ex: - if ex.code == 401: - hass.components.persistent_notification.create( - "Error:
Please check username and password." - "You will need to restart Home Assistant after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - else: - hass.components.persistent_notification.create( - "Error:
Can't communicate with Tesla API.
" - "Error code: {} Reason: {}" - "You will need to restart Home Assistant after fixing." - "".format(ex.code, ex.message), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) + info = await validate_input(hass, config) + except (CannotConnect, InvalidAuth): return False - all_devices = controller.get_homeassistant_components() + _update_entry( + email, + data={ + CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN], + CONF_TOKEN: info[CONF_TOKEN], + }, + options={CONF_SCAN_INTERVAL: scan_interval}, + ) + else: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: email, CONF_PASSWORD: password}, + ) + ) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][email] = {CONF_SCAN_INTERVAL: scan_interval} + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Tesla as config entry.""" + + hass.data.setdefault(DOMAIN, {}) + config = config_entry.data + websession = aiohttp_client.async_get_clientsession(hass) + email = config_entry.title + if email in hass.data[DOMAIN] and CONF_SCAN_INTERVAL in hass.data[DOMAIN][email]: + scan_interval = hass.data[DOMAIN][email][CONF_SCAN_INTERVAL] + hass.config_entries.async_update_entry( + config_entry, options={CONF_SCAN_INTERVAL: scan_interval} + ) + hass.data[DOMAIN].pop(email) + try: + controller = TeslaAPI( + websession, + refresh_token=config[CONF_TOKEN], + update_interval=config_entry.options.get(CONF_SCAN_INTERVAL, 300), + ) + (refresh_token, access_token) = await controller.connect() + except TeslaException as ex: + _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) + return False + _async_save_tokens(hass, config_entry, access_token, refresh_token) + entry_data = hass.data[DOMAIN][config_entry.entry_id] = { + "controller": controller, + "devices": defaultdict(list), + DATA_LISTENER: [config_entry.add_update_listener(update_listener)], + } + _LOGGER.debug("Connected to the Tesla API.") + all_devices = entry_data["controller"].get_homeassistant_components() + if not all_devices: return False for device in all_devices: - hass.data[DOMAIN]["devices"][device.hass_type].append(device) + entry_data["devices"][device.hass_type].append(device) for component in TESLA_COMPONENTS: + _LOGGER.debug("Loading %s", component) hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN, {}, base_config) + hass.config_entries.async_forward_entry_setup(config_entry, component) ) return True +async def async_unload_entry(hass, config_entry) -> bool: + """Unload a config entry.""" + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in TESLA_COMPONENTS + ] + ) + for listener in hass.data[DOMAIN][config_entry.entry_id][DATA_LISTENER]: + listener() + username = config_entry.title + hass.data[DOMAIN].pop(config_entry.entry_id) + _LOGGER.debug("Unloaded entry for %s", username) + return True + + +async def update_listener(hass, config_entry): + """Update when config_entry options update.""" + controller = hass.data[DOMAIN][config_entry.entry_id]["controller"] + old_update_interval = controller.update_interval + controller.update_interval = config_entry.options.get(CONF_SCAN_INTERVAL) + _LOGGER.debug( + "Changing scan_interval from %s to %s", + old_update_interval, + controller.update_interval, + ) + + class TeslaDevice(Entity): """Representation of a Tesla device.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise the Tesla device.""" self.tesla_device = tesla_device self.controller = controller + self.config_entry = config_entry self._name = self.tesla_device.name self.tesla_id = slugify(self.tesla_device.uniq_name) self._attributes = {} @@ -124,6 +210,17 @@ def device_state_attributes(self): attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level() return attr + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self.tesla_device.id())}, + "name": self.tesla_device.car_name(), + "manufacturer": "Tesla", + "model": self.tesla_device.car_type, + "sw_version": self.tesla_device.car_version, + } + async def async_added_to_hass(self): """Register state update callback.""" pass @@ -134,4 +231,10 @@ async def async_will_remove_from_hass(self): async def async_update(self): """Update the state of the device.""" + if self.controller.is_token_refreshed(): + (refresh_token, access_token) = self.controller.get_tokens() + _async_save_tokens( + self.hass, self.config_entry, access_token, refresh_token + ) + _LOGGER.debug("Saving new tokens in config_entry") await self.tesla_device.async_update() diff --git a/homeassistant/components/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py index 738533a9b5685a..8f610d960b33bf 100644 --- a/homeassistant/components/tesla/binary_sensor.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -8,21 +8,35 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tesla binary sensor.""" - devices = [ - TeslaBinarySensor(device, hass.data[TESLA_DOMAIN]["controller"], "connectivity") - for device in hass.data[TESLA_DOMAIN]["devices"]["binary_sensor"] - ] - add_entities(devices, True) + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + async_add_entities( + [ + TeslaBinarySensor( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + "connectivity", + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ + "binary_sensor" + ] + ], + True, + ) class TeslaBinarySensor(TeslaDevice, BinarySensorDevice): """Implement an Tesla binary sensor for parking and charger.""" - def __init__(self, tesla_device, controller, sensor_type): + def __init__(self, tesla_device, controller, sensor_type, config_entry): """Initialise of a Tesla binary sensor.""" - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) self._state = False self._sensor_type = sensor_type diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index 85fd8a8e258dda..d3c87035c9ca9d 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -16,21 +16,34 @@ SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF] -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tesla climate platform.""" - devices = [ - TeslaThermostat(device, hass.data[TESLA_DOMAIN]["controller"]) - for device in hass.data[TESLA_DOMAIN]["devices"]["climate"] - ] - add_entities(devices, True) + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + async_add_entities( + [ + TeslaThermostat( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ + "climate" + ] + ], + True, + ) class TeslaThermostat(TeslaDevice, ClimateDevice): """Representation of a Tesla climate.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialize the Tesla device.""" - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) self._target_temperature = None self._temperature = None diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py new file mode 100644 index 00000000000000..2d2bc0158d2b88 --- /dev/null +++ b/homeassistant/components/tesla/config_flow.py @@ -0,0 +1,143 @@ +"""Tesla Config Flow.""" +import logging + +from teslajsonpy import Controller as TeslaAPI, TeslaException +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_TOKEN, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +@callback +def configured_instances(hass): + """Return a set of configured Tesla instances.""" + return set(entry.title for entry in hass.config_entries.async_entries(DOMAIN)) + + +class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Tesla.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + + if not user_input: + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={}, + description_placeholders={}, + ) + + if user_input[CONF_USERNAME] in configured_instances(self.hass): + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={CONF_USERNAME: "identifier_exists"}, + description_placeholders={}, + ) + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={"base": "connection_error"}, + description_placeholders={}, + ) + except InvalidAuth: + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={"base": "invalid_credentials"}, + description_placeholders={}, + ) + return self.async_create_entry(title=user_input[CONF_USERNAME], data=info) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for Tesla.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get(CONF_SCAN_INTERVAL, 300), + ): vol.All(cv.positive_int, vol.Clamp(min=300)) + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + config = {} + websession = aiohttp_client.async_get_clientsession(hass) + try: + controller = TeslaAPI( + websession, + email=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + update_interval=300, + ) + (config[CONF_TOKEN], config[CONF_ACCESS_TOKEN]) = await controller.connect( + test_login=True + ) + except TeslaException as ex: + if ex.code == 401: + _LOGGER.error("Invalid credentials: %s", ex) + raise InvalidAuth() + _LOGGER.error("Unable to communicate with Tesla API: %s", ex) + raise CannotConnect() + _LOGGER.debug("Credentials successfully connected to the Tesla API") + return config + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py index c205cc587eba4e..08e5d58ba6e6ef 100644 --- a/homeassistant/components/tesla/device_tracker.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -1,45 +1,70 @@ """Support for tracking Tesla cars.""" import logging -from homeassistant.helpers.event import async_track_utc_time_change -from homeassistant.util import slugify +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity -from . import DOMAIN as TESLA_DOMAIN +from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Set up the Tesla tracker.""" - tracker = TeslaDeviceTracker( - hass, config, async_see, hass.data[TESLA_DOMAIN]["devices"]["devices_tracker"] - ) - await tracker.update_info() - async_track_utc_time_change(hass, tracker.update_info, second=range(0, 60, 30)) - return True +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + entities = [ + TeslaDeviceEntity( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ + "devices_tracker" + ] + ] + async_add_entities(entities, True) -class TeslaDeviceTracker: +class TeslaDeviceEntity(TeslaDevice, TrackerEntity): """A class representing a Tesla device.""" - def __init__(self, hass, config, see, tesla_devices): + def __init__(self, tesla_device, controller, config_entry): """Initialize the Tesla device scanner.""" - self.hass = hass - self.see = see - self.devices = tesla_devices + super().__init__(tesla_device, controller, config_entry) + self._latitude = None + self._longitude = None + self._attributes = {"trackr_id": self.unique_id} + self._listener = None - async def update_info(self, now=None): + async def async_update(self): """Update the device info.""" - for device in self.devices: - await device.async_update() - name = device.name - _LOGGER.debug("Updating device position: %s", name) - dev_id = slugify(device.uniq_name) - location = device.get_location() - if location: - lat = location["latitude"] - lon = location["longitude"] - attrs = {"trackr_id": dev_id, "id": dev_id, "name": name} - await self.see( - dev_id=dev_id, host_name=name, gps=(lat, lon), attributes=attrs - ) + _LOGGER.debug("Updating device position: %s", self.name) + await super().async_update() + location = self.tesla_device.get_location() + if location: + self._latitude = location["latitude"] + self._longitude = location["longitude"] + self._attributes = { + "trackr_id": self.unique_id, + "heading": location["heading"], + "speed": location["speed"], + } + + @property + def latitude(self) -> float: + """Return latitude value of the device.""" + return self._latitude + + @property + def longitude(self) -> float: + """Return longitude value of the device.""" + return self._longitude + + @property + def should_poll(self): + """Return whether polling is needed.""" + return True + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS diff --git a/homeassistant/components/tesla/lock.py b/homeassistant/components/tesla/lock.py index 5e97602357df8f..33eed8cf7c1e23 100644 --- a/homeassistant/components/tesla/lock.py +++ b/homeassistant/components/tesla/lock.py @@ -9,22 +9,31 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tesla lock platform.""" - devices = [ - TeslaLock(device, hass.data[TESLA_DOMAIN]["controller"]) - for device in hass.data[TESLA_DOMAIN]["devices"]["lock"] + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + entities = [ + TeslaLock( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["lock"] ] - add_entities(devices, True) + async_add_entities(entities, True) class TeslaLock(TeslaDevice, LockDevice): """Representation of a Tesla door lock.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise of the lock.""" self._state = None - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) async def async_lock(self, **kwargs): """Send the lock command.""" diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index a2021092413011..4a869ab0a4138a 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -1,8 +1,9 @@ { "domain": "tesla", "name": "Tesla", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", "requirements": ["teslajsonpy==0.2.0"], "dependencies": [], - "codeowners": ["@zabuldon"] + "codeowners": ["@zabuldon", "@alandtse"] } diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index 1cce37f232a5e1..d93d3fa45d6ec1 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -14,30 +14,34 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tesla sensor platform.""" - controller = hass.data[TESLA_DOMAIN]["devices"]["controller"] - devices = [] + pass - for device in hass.data[TESLA_DOMAIN]["devices"]["sensor"]: + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] + entities = [] + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["sensor"]: if device.bin_type == 0x4: - devices.append(TeslaSensor(device, controller, "inside")) - devices.append(TeslaSensor(device, controller, "outside")) + entities.append(TeslaSensor(device, controller, config_entry, "inside")) + entities.append(TeslaSensor(device, controller, config_entry, "outside")) elif device.bin_type in [0xA, 0xB, 0x5]: - devices.append(TeslaSensor(device, controller)) - add_entities(devices, True) + entities.append(TeslaSensor(device, controller, config_entry)) + async_add_entities(entities, True) class TeslaSensor(TeslaDevice, Entity): """Representation of Tesla sensors.""" - def __init__(self, tesla_device, controller, sensor_type=None): + def __init__(self, tesla_device, controller, config_entry, sensor_type=None): """Initialize of the sensor.""" self.current_value = None self._unit = None self.last_changed_time = None self.type = sensor_type - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) if self.type: self._name = f"{self.tesla_device.name} ({self.type})" diff --git a/homeassistant/components/tesla/strings.json b/homeassistant/components/tesla/strings.json new file mode 100644 index 00000000000000..831406a0d63a93 --- /dev/null +++ b/homeassistant/components/tesla/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error connecting; check network and retry", + "identifier_exists": "Email already registered", + "invalid_credentials": "Invalid credentials", + "unknown_error": "Unknown error, please report log info" + }, + "step": { + "user": { + "data": { + "username": "Email Address", + "password": "Password" + }, + "description": "Please enter your information.", + "title": "Tesla - Configuration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconds between scans" + } + } + } + } +} diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 5f432875aebc0b..3fc424e390da3e 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -9,26 +9,31 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tesla switch platform.""" - controller = hass.data[TESLA_DOMAIN]["controller"] - devices = [] - for device in hass.data[TESLA_DOMAIN]["devices"]["switch"]: + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] + entities = [] + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["switch"]: if device.bin_type == 0x8: - devices.append(ChargerSwitch(device, controller)) - devices.append(UpdateSwitch(device, controller)) + entities.append(ChargerSwitch(device, controller, config_entry)) + entities.append(UpdateSwitch(device, controller, config_entry)) elif device.bin_type == 0x9: - devices.append(RangeSwitch(device, controller)) - add_entities(devices, True) + entities.append(RangeSwitch(device, controller, config_entry)) + async_add_entities(entities, True) class ChargerSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla charger switch.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise of the switch.""" self._state = None - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) async def async_turn_on(self, **kwargs): """Send the on command.""" @@ -55,10 +60,10 @@ async def async_update(self): class RangeSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla max range charging switch.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise the switch.""" self._state = None - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) async def async_turn_on(self, **kwargs): """Send the on command.""" @@ -85,11 +90,11 @@ async def async_update(self): class UpdateSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla update switch.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise the switch.""" self._state = None tesla_device.type = "update switch" - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) self._name = self._name.replace("charger", "update") self.tesla_id = self.tesla_id.replace("charger", "update") diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2b3940000e7963..88ff92a57b0593 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -73,6 +73,7 @@ "sonos", "starline", "tellduslive", + "tesla", "toon", "tplink", "traccar", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5361e7464e80a..2b7101dfa25820 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,6 +598,9 @@ sunwatcher==0.2.1 # homeassistant.components.tellduslive tellduslive==0.10.10 +# homeassistant.components.tesla +teslajsonpy==0.2.0 + # homeassistant.components.toon toonapilib==3.2.4 diff --git a/tests/components/tesla/__init__.py b/tests/components/tesla/__init__.py new file mode 100644 index 00000000000000..89b1e1c0c549f7 --- /dev/null +++ b/tests/components/tesla/__init__.py @@ -0,0 +1 @@ +"""Tests for the Tesla integration.""" diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py new file mode 100644 index 00000000000000..b6eeff54a50737 --- /dev/null +++ b/tests/components/tesla/test_config_flow.py @@ -0,0 +1,160 @@ +"""Test the Tesla config flow.""" +from unittest.mock import patch + +from teslajsonpy import TeslaException + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.tesla.const import DOMAIN +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_TOKEN, + CONF_USERNAME, +) + +from tests.common import MockConfigEntry, mock_coro + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.tesla.config_flow.TeslaAPI.connect", + return_value=mock_coro(("test-refresh-token", "test-access-token")), + ), patch( + "homeassistant.components.tesla.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.tesla.async_setup_entry", return_value=mock_coro(True) + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: "test", CONF_USERNAME: "test@email.com"} + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "test@email.com" + assert result2["data"] == { + "token": "test-refresh-token", + "access_token": "test-access-token", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.tesla.config_flow.TeslaAPI.connect", + side_effect=TeslaException(401), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_credentials"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.tesla.config_flow.TeslaAPI.connect", + side_effect=TeslaException(code=404), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password", CONF_USERNAME: "test-username"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_form_repeat_identifier(hass): + """Test we handle repeat identifiers.""" + entry = MockConfigEntry(domain=DOMAIN, title="test-username", data={}, options=None) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + "homeassistant.components.tesla.config_flow.TeslaAPI.connect", + return_value=mock_coro(("test-refresh-token", "test-access-token")), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {CONF_USERNAME: "identifier_exists"} + + +async def test_import(hass): + """Test import step.""" + + with patch( + "homeassistant.components.tesla.config_flow.TeslaAPI.connect", + return_value=mock_coro(("test-refresh-token", "test-access-token")), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_PASSWORD: "test-password", CONF_USERNAME: "test-username"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "test-username" + assert result["data"][CONF_ACCESS_TOKEN] == "test-access-token" + assert result["data"][CONF_TOKEN] == "test-refresh-token" + assert result["description_placeholders"] is None + + +async def test_option_flow(hass): + """Test config flow options.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options=None) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.flow.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 350} + ) + assert result["type"] == "create_entry" + assert result["data"] == {CONF_SCAN_INTERVAL: 350} + + +async def test_option_flow_input_floor(hass): + """Test config flow options.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options=None) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.flow.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], user_input={CONF_SCAN_INTERVAL: 1} + ) + assert result["type"] == "create_entry" + assert result["data"] == {CONF_SCAN_INTERVAL: 300} From 85d2ba047b9ec16fa54e0b5e84cb2a94b1f98297 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 23 Dec 2019 19:11:35 -0500 Subject: [PATCH 2540/3953] Protect against bad data stored in ZHA (#30183) --- homeassistant/components/zha/api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index d9c1db2ae5d652..1294fcaedbd518 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -449,7 +449,11 @@ async def remove_group(group, zha_gateway): group.group_id ) ) - await asyncio.gather(*tasks) + if tasks: + await asyncio.gather(*tasks) + else: + # we have members but none are tracked by ZHA for whatever reason + zha_gateway.application_controller.groups.pop(group.group_id) else: zha_gateway.application_controller.groups.pop(group.group_id) From 8add6c5f2e968b6fea2814bd5fecb1f496a67f0d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 24 Dec 2019 00:32:12 +0000 Subject: [PATCH 2541/3953] [ci skip] Translation update --- .../components/tesla/.translations/en.json | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/tesla/.translations/en.json b/homeassistant/components/tesla/.translations/en.json index 831406a0d63a93..8c43f28e04ea28 100644 --- a/homeassistant/components/tesla/.translations/en.json +++ b/homeassistant/components/tesla/.translations/en.json @@ -1,30 +1,30 @@ { - "config": { - "error": { - "connection_error": "Error connecting; check network and retry", - "identifier_exists": "Email already registered", - "invalid_credentials": "Invalid credentials", - "unknown_error": "Unknown error, please report log info" - }, - "step": { - "user": { - "data": { - "username": "Email Address", - "password": "Password" + "config": { + "error": { + "connection_error": "Error connecting; check network and retry", + "identifier_exists": "Email already registered", + "invalid_credentials": "Invalid credentials", + "unknown_error": "Unknown error, please report log info" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email Address" + }, + "description": "Please enter your information.", + "title": "Tesla - Configuration" + } }, - "description": "Please enter your information.", - "title": "Tesla - Configuration" - } + "title": "Tesla" }, - "title": "Tesla" - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Seconds between scans" + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconds between scans" + } + } } - } } - } -} +} \ No newline at end of file From 783672d305b8390d6e3ea2f49622b644de6c46ef Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 24 Dec 2019 14:11:01 +0100 Subject: [PATCH 2542/3953] Upgrade zeroconf to 0.24.3 (#30187) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 1dc1618842d0fc..720ffc3e69ee51 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -3,7 +3,7 @@ "name": "Zeroconf", "documentation": "https://www.home-assistant.io/integrations/zeroconf", "requirements": [ - "zeroconf==0.24.2" + "zeroconf==0.24.3" ], "dependencies": [ "api" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5d14a6e30d60e9..363e175ec5a0bf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.12 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.24.2 +zeroconf==0.24.3 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 252665f527bb99..49a9fa49e2ea4d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2088,7 +2088,7 @@ youtube_dl==2019.11.28 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.24.2 +zeroconf==0.24.3 # homeassistant.components.zha zha-quirks==0.0.28 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b7101dfa25820..422af61d9dfe98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -656,7 +656,7 @@ ya_ma==0.3.8 yahooweather==0.10 # homeassistant.components.zeroconf -zeroconf==0.24.2 +zeroconf==0.24.3 # homeassistant.components.zha zha-quirks==0.0.28 From 2113d65c06a1b4bdae4f7ebc80691610c998dfbd Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 24 Dec 2019 14:14:58 +0100 Subject: [PATCH 2543/3953] Fix AdGuard Home safe search sensor name (#30171) * Fix AdGuard Home safe search sensor name * Fix typo --- homeassistant/components/adguard/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index e0c86e42d26677..c818752ad2f71d 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -178,7 +178,7 @@ def __init__(self, adguard): """Initialize AdGuard Home sensor.""" super().__init__( adguard, - "Searches Safe Search Enforced", + "AdGuard Safe Searches Enforced", "mdi:shield-search", "enforced_safesearch", "requests", From f32eaa2fdd4a68fc53a0b1e30d509f09f8ae675f Mon Sep 17 00:00:00 2001 From: Tim Rightnour <6556271+garbled1@users.noreply.github.com> Date: Tue, 24 Dec 2019 06:41:29 -0700 Subject: [PATCH 2544/3953] Add onewire devices and owserver remote host support (#29948) * Add support for HobbyBoards sensors: Hub, Humidity, Moisture, PulseCounter Add support for an owserver running on a remote host * Cleanup and fixes for style/etc * Forgot to convert to f-strings. * Update with changes requested by @MartinHjelmare * Pylint * Fix to None * Reverse logic for hobbyboard test to eliminate pylint complaint * Modified manifest to list myself as codeowner * Move some functions down into OneWireProxy instead of the top OneWire * Oops, missed some decode()'s * And another! --- CODEOWNERS | 1 + .../components/onewire/manifest.json | 8 +- homeassistant/components/onewire/sensor.py | 130 +++++++++++++++++- requirements_all.txt | 3 + 4 files changed, 137 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a7d1d346c5fe31..52f13748303174 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -235,6 +235,7 @@ homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ombi/* @larssont homeassistant/components/onboarding/* @home-assistant/core +homeassistant/components/onewire/* @garbled1 homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index 2d8c6c7107107e..6687a10e3d7c50 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -2,7 +2,11 @@ "domain": "onewire", "name": "Onewire", "documentation": "https://www.home-assistant.io/integrations/onewire", - "requirements": [], + "requirements": [ + "pyownet==0.10.0.post1" + ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@garbled1" + ] } diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 6e90178c5d3537..6405cb05adc967 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -4,10 +4,11 @@ import os import time +from pyownet import protocol import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import CONF_HOST, CONF_PORT, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -30,33 +31,122 @@ "28": {"temperature": "temperature"}, "3B": {"temperature": "temperature"}, "42": {"temperature": "temperature"}, + "1D": {"counter_a": "counter.A", "counter_b": "counter.B"}, + "EF": {"HobbyBoard": "special"}, +} + +# EF sensors are usually hobbyboards specialized sensors. +# These can only be read by OWFS. Currently this driver only supports them +# via owserver (network protocol) + +HOBBYBOARD_EF = { + "HobbyBoards_EF": { + "humidity": "humidity/humidity_corrected", + "humidity_raw": "humidity/humidity_raw", + "temperature": "humidity/temperature", + }, + "HB_MOISTURE_METER": { + "moisture_0": "moisture/sensor.0", + "moisture_1": "moisture/sensor.1", + "moisture_2": "moisture/sensor.2", + "moisture_3": "moisture/sensor.3", + }, } SENSOR_TYPES = { "temperature": ["temperature", TEMP_CELSIUS], "humidity": ["humidity", "%"], + "humidity_raw": ["humidity", "%"], "pressure": ["pressure", "mb"], "illuminance": ["illuminance", "lux"], + "wetness_0": ["wetness", "%"], + "wetness_1": ["wetness", "%"], + "wetness_2": ["wetness", "%"], + "wetness_3": ["wetness", "%"], + "moisture_0": ["moisture", "cb"], + "moisture_1": ["moisture", "cb"], + "moisture_2": ["moisture", "cb"], + "moisture_3": ["moisture", "cb"], + "counter_a": ["counter", "count"], + "counter_b": ["counter", "count"], + "HobbyBoard": ["none", "none"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAMES): {cv.string: cv.string}, vol.Optional(CONF_MOUNT_DIR, default=DEFAULT_MOUNT_DIR): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=4304): cv.port, } ) +def hb_info_from_type(dev_type="std"): + """Return the proper info array for the device type.""" + if "std" in dev_type: + return DEVICE_SENSORS + if "HobbyBoard" in dev_type: + return HOBBYBOARD_EF + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the one wire Sensors.""" - base_dir = config.get(CONF_MOUNT_DIR) + base_dir = config[CONF_MOUNT_DIR] + owport = config[CONF_PORT] + owhost = config.get(CONF_HOST) devs = [] device_names = {} if "names" in config: if isinstance(config["names"], dict): device_names = config["names"] - if base_dir == DEFAULT_MOUNT_DIR: + # We have an owserver on a remote(or local) host/port + if owhost: + try: + owproxy = protocol.proxy(host=owhost, port=owport) + devices = owproxy.dir() + except protocol.Error as exc: + _LOGGER.error( + "Cannot connect to owserver on %s:%d, got: %s", owhost, owport, exc + ) + devices = [] + for device in devices: + _LOGGER.debug("found device: %s", device) + family = owproxy.read(f"{device}family").decode() + dev_type = "std" + if "EF" in family: + dev_type = "HobbyBoard" + family = owproxy.read(f"{device}type").decode() + + if family not in hb_info_from_type(dev_type): + _LOGGER.warning( + "Ignoring unknown family (%s) of sensor found for device: %s", + family, + device, + ) + continue + for sensor_key, sensor_value in hb_info_from_type(dev_type)[family].items(): + if "moisture" in sensor_key: + s_id = sensor_key.split("_")[1] + is_leaf = int( + owproxy.read(f"{device}moisture/is_leaf.{s_id}").decode() + ) + if is_leaf: + sensor_key = f"wetness_{id}" + sensor_id = os.path.split(os.path.split(device)[0])[1] + device_file = os.path.join(os.path.split(device)[0], sensor_value) + devs.append( + OneWireProxy( + device_names.get(sensor_id, sensor_id), + device_file, + sensor_key, + owproxy, + ) + ) + + # We have a raw GPIO ow sensor on a Pi + elif base_dir == DEFAULT_MOUNT_DIR: for device_family in DEVICE_SENSORS: for device_folder in glob(os.path.join(base_dir, device_family + "[.-]*")): sensor_id = os.path.split(device_folder)[1] @@ -68,10 +158,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "temperature", ) ) + + # We have an owfs mounted else: for family_file_path in glob(os.path.join(base_dir, "*", "family")): with open(family_file_path, "r") as family_file: family = family_file.read() + if "EF" in family: + continue if family in DEVICE_SENSORS: for sensor_key, sensor_value in DEVICE_SENSORS[family].items(): sensor_id = os.path.split(os.path.split(family_file_path)[0])[1] @@ -121,6 +215,8 @@ def name(self): @property def state(self): """Return the state of the sensor.""" + if "count" in self._unit_of_measurement: + return int(self._state) return self._state @property @@ -129,6 +225,34 @@ def unit_of_measurement(self): return self._unit_of_measurement +class OneWireProxy(OneWire): + """Implementation of a One wire Sensor through owserver.""" + + def __init__(self, name, device_file, sensor_type, owproxy): + """Initialize the onewire sensor via owserver.""" + super().__init__(name, device_file, sensor_type) + self._owproxy = owproxy + + def _read_value_ownet(self): + """Read a value from the owserver.""" + if self._owproxy: + return self._owproxy.read(self._device_file).decode().lstrip() + return None + + def update(self): + """Get the latest data from the device.""" + value = None + value_read = False + try: + value_read = self._read_value_ownet() + except protocol.Error as exc: + _LOGGER.error("Owserver failure in read(), got: %s", exc) + if value_read: + value = round(float(value_read), 1) + + self._state = value + + class OneWireDirect(OneWire): """Implementation of an One wire Sensor directly connected to RPI GPIO.""" diff --git a/requirements_all.txt b/requirements_all.txt index 49a9fa49e2ea4d..5a95d3f28b8b13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1429,6 +1429,9 @@ pyowlet==1.0.3 # homeassistant.components.openweathermap pyowm==2.10.0 +# homeassistant.components.onewire +pyownet==0.10.0.post1 + # homeassistant.components.elv pypca==0.0.7 From 482ff4061e043eb8134d188a2aad9a4d65b99cb0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 24 Dec 2019 20:00:05 +0100 Subject: [PATCH 2545/3953] Bump importlib-metadata to 1.3.0 (#30196) * Bump importlib-metadata to 1.3.0 * Include setup.py and package_constraints.txt --- 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 363e175ec5a0bf..4bbbad9ad727a1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.30 home-assistant-frontend==20191204.1 -importlib-metadata==0.23 +importlib-metadata==1.3.0 jinja2>=2.10.3 netdisco==2.6.0 pip>=8.0.3 diff --git a/requirements_all.txt b/requirements_all.txt index 5a95d3f28b8b13..dd1e7ea3705a26 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 certifi>=2019.11.28 -importlib-metadata==0.23 +importlib-metadata==1.3.0 jinja2>=2.10.3 PyJWT==1.7.1 cryptography==2.8 diff --git a/setup.py b/setup.py index 720406ab91b32a..cf84577b558b55 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ "attrs==19.3.0", "bcrypt==3.1.7", "certifi>=2019.11.28", - "importlib-metadata==0.23", + "importlib-metadata==1.3.0", "jinja2>=2.10.3", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. From 4ea42c2479033ec45e2239c93645dc9bd5daaca5 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 24 Dec 2019 20:00:32 +0100 Subject: [PATCH 2546/3953] Bump datapoint to 0.9.5 (#30185) --- homeassistant/components/metoffice/manifest.json | 4 +--- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index f5624e33edbefd..572b3a7bcb8df6 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -2,9 +2,7 @@ "domain": "metoffice", "name": "Metoffice", "documentation": "https://www.home-assistant.io/integrations/metoffice", - "requirements": [ - "datapoint==0.4.3" - ], + "requirements": ["datapoint==0.9.5"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index dd1e7ea3705a26..7cfaff94ef87f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -399,7 +399,7 @@ crimereports==1.0.1 datadog==0.15.0 # homeassistant.components.metoffice -datapoint==0.4.3 +datapoint==0.9.5 # homeassistant.components.decora # decora==0.6 From 25f78dd1a9d2deab026039ef35ab98e533cd70cc Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Tue, 24 Dec 2019 17:06:39 -0500 Subject: [PATCH 2547/3953] Implement EqualizerController in Alexa for media_player. (#30159) --- .../components/alexa/capabilities.py | 72 +++++++++- homeassistant/components/alexa/const.py | 8 ++ homeassistant/components/alexa/entities.py | 4 + homeassistant/components/alexa/handlers.py | 40 ++++++ tests/components/alexa/test_smart_home.py | 125 ++++++++++++++++++ 5 files changed, 248 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 513a0c157d38a8..3fdad2b3c924c0 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -120,7 +120,20 @@ def capability_resources(): @staticmethod def configuration(): - """Return the configuration object.""" + """Return the configuration object. + + Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController, + and EventDetectionSensor. + """ + return [] + + @staticmethod + def configurations(): + """Return the configurations object. + + The plural configurations object is different that the singular configuration object. + Applicable to EqualizerController interface. + """ return [] @staticmethod @@ -177,6 +190,11 @@ def serialize_discovery(self): if configuration: result["configuration"] = configuration + # The plural configurations object is different than the singular configuration object above. + configurations = self.configurations() + if configurations: + result["configurations"] = configurations + semantics = self.semantics() if semantics: result["semantics"] = semantics @@ -1356,3 +1374,55 @@ def configuration(self): } }, } + + +class AlexaEqualizerController(AlexaCapability): + """Implements Alexa.EqualizerController. + + https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-equalizercontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.EqualizerController" + + def properties_supported(self): + """Return what properties this entity supports. + + Either bands, mode or both can be specified. Only mode is supported at this time. + """ + return [{"name": "mode"}] + + def get_property(self, name): + """Read and return a property.""" + if name != "mode": + raise UnsupportedProperty(name) + + sound_mode = self.entity.attributes.get(media_player.ATTR_SOUND_MODE) + if sound_mode and sound_mode.upper() in ( + "MOVIE", + "MUSIC", + "NIGHT", + "SPORT", + "TV", + ): + return sound_mode.upper() + + return None + + def configurations(self): + """Return the sound modes supported in the configurations object. + + Valid Values for modes are: MOVIE, MUSIC, NIGHT, SPORT, TV. + """ + configurations = None + sound_mode_list = self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) + if sound_mode_list: + supported_sound_modes = [] + for sound_mode in sound_mode_list: + if sound_mode.upper() in ("MOVIE", "MUSIC", "NIGHT", "SPORT", "TV"): + supported_sound_modes.append({"name": sound_mode.upper()}) + + configurations = {"modes": {"supported": supported_sound_modes}} + + return configurations diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index f1a86859da9dc3..6968ab3a6912ba 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -196,3 +196,11 @@ class Inputs: "video3": "VIDEO 3", "xbox": "XBOX", } + + VALID_SOUND_MODE_MAP = { + "movie": "MOVIE", + "music": "MUSIC", + "night": "NIGHT", + "sport": "SPORT", + "tv": "TV", + } diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index ab3dc75bd2ccc2..4321d289cecb41 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -42,6 +42,7 @@ AlexaContactSensor, AlexaDoorbellEventSource, AlexaEndpointHealth, + AlexaEqualizerController, AlexaEventDetectionSensor, AlexaInputController, AlexaLockController, @@ -522,6 +523,9 @@ def interfaces(self): if supported & media_player.const.SUPPORT_PLAY_MEDIA: yield AlexaChannelController(self.entity) + if supported & media_player.const.SUPPORT_SELECT_SOUND_MODE: + yield AlexaEqualizerController(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 133919be84d6c9..ce6c37a2b394d4 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1352,3 +1352,43 @@ async def async_api_seek(hass, config, directive, context): return directive.response( name="StateReport", namespace="Alexa.SeekController", payload=payload ) + + +@HANDLERS.register(("Alexa.EqualizerController", "SetMode")) +async def async_api_set_eq_mode(hass, config, directive, context): + """Process a SetMode request for EqualizerController.""" + mode = directive.payload["mode"] + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST) + if sound_mode_list and mode.lower() in sound_mode_list: + data[media_player.const.ATTR_SOUND_MODE] = mode.lower() + else: + msg = "failed to map sound mode {} to a mode on {}".format( + mode, entity.entity_id + ) + raise AlexaInvalidValueError(msg) + + await hass.services.async_call( + entity.domain, + media_player.SERVICE_SELECT_SOUND_MODE, + data, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.EqualizerController", "AdjustBands")) +@HANDLERS.register(("Alexa.EqualizerController", "ResetBands")) +@HANDLERS.register(("Alexa.EqualizerController", "SetBands")) +async def async_api_bands_directive(hass, config, directive, context): + """Handle an AdjustBands, ResetBands, SetBands request. + + Only mode directives are currently supported for the EqualizerController. + """ + # Currently bands directives are not supported. + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3d7c2b118e7cf6..37301c3555e9d5 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -9,6 +9,7 @@ SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -2912,3 +2913,127 @@ async def test_input_number_float(hass): "value", instance="input_number.value", ) + + +async def test_media_player_eq_modes(hass): + """Test media player discovery with sound mode list.""" + device = ( + "media_player.test", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOUND_MODE, + "sound_mode": "tv", + "sound_mode_list": ["movie", "music", "night", "sport", "tv", "rocknroll"], + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "media_player#test" + assert appliance["friendlyName"] == "Test media player" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa", + "Alexa.EqualizerController", + "Alexa.PowerController", + "Alexa.EndpointHealth", + ) + + eq_capability = get_capability(capabilities, "Alexa.EqualizerController") + assert eq_capability is not None + assert "modes" in eq_capability["configurations"] + + eq_modes = eq_capability["configurations"]["modes"] + assert {"name": "rocknroll"} not in eq_modes["supported"] + assert {"name": "ROCKNROLL"} not in eq_modes["supported"] + + for mode in ("MOVIE", "MUSIC", "NIGHT", "SPORT", "TV"): + assert {"name": mode} in eq_modes["supported"] + + call, _ = await assert_request_calls_service( + "Alexa.EqualizerController", + "SetMode", + "media_player#test", + "media_player.select_sound_mode", + hass, + payload={"mode": mode}, + ) + assert call.data["sound_mode"] == mode.lower() + + +async def test_media_player_sound_mode_list_none(hass): + """Test EqualizerController bands directive not supported.""" + device = ( + "media_player.test", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOUND_MODE, + "sound_mode": "unknown", + "sound_mode_list": None, + }, + ) + appliance = await discovery_test(device, hass) + assert appliance["endpointId"] == "media_player#test" + assert appliance["friendlyName"] == "Test media player" + + +async def test_media_player_eq_bands_not_supported(hass): + """Test EqualizerController bands directive not supported.""" + device = ( + "media_player.test_bands", + "on", + { + "friendly_name": "Test media player", + "supported_features": SUPPORT_SELECT_SOUND_MODE, + "sound_mode": "tv", + "sound_mode_list": ["movie", "music", "night", "sport", "tv", "rocknroll"], + }, + ) + await discovery_test(device, hass) + + context = Context() + + # Test for SetBands Error + request = get_new_request( + "Alexa.EqualizerController", "SetBands", "media_player#test_bands" + ) + request["directive"]["payload"] = {"bands": [{"name": "BASS", "value": -2}]} + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + # Test for AdjustBands Error + request = get_new_request( + "Alexa.EqualizerController", "AdjustBands", "media_player#test_bands" + ) + request["directive"]["payload"] = { + "bands": [{"name": "BASS", "levelDelta": 3, "levelDirection": "UP"}] + } + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + # Test for ResetBands Error + request = get_new_request( + "Alexa.EqualizerController", "ResetBands", "media_player#test_bands" + ) + request["directive"]["payload"] = { + "bands": [{"name": "BASS", "levelDelta": 3, "levelDirection": "UP"}] + } + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" From bb35d93fde2b775564d1a054a3c45ba9785b8d4b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 25 Dec 2019 00:32:06 +0000 Subject: [PATCH 2548/3953] [ci skip] Translation update --- .../alarm_control_panel/.translations/ko.json | 7 + .../components/almond/.translations/ko.json | 5 + .../ambient_station/.translations/ko.json | 6 +- .../binary_sensor/.translations/ko.json | 172 +++++++++--------- .../components/cast/.translations/ko.json | 2 +- .../components/climate/.translations/ko.json | 17 ++ .../coolmaster/.translations/ko.json | 2 +- .../components/cover/.translations/ko.json | 24 +-- .../components/deconz/.translations/ko.json | 42 ++--- .../components/demo/.translations/ko.json | 5 + .../device_tracker/.translations/ko.json | 4 +- .../dialogflow/.translations/ko.json | 2 +- .../components/elgato/.translations/ko.json | 27 +++ .../components/esphome/.translations/ko.json | 4 +- .../components/fan/.translations/ko.json | 16 ++ .../components/geofency/.translations/ko.json | 2 +- .../geonetnz_volcano/.translations/ko.json | 16 ++ .../gpslogger/.translations/ko.json | 2 +- .../hisense_aehw4a1/.translations/ko.json | 15 ++ .../components/icloud/.translations/ko.json | 38 ++++ .../components/ifttt/.translations/ko.json | 2 +- .../components/ios/.translations/ko.json | 2 +- .../components/izone/.translations/ko.json | 2 +- .../components/light/.translations/ko.json | 8 +- .../components/locative/.translations/ko.json | 2 +- .../components/lock/.translations/ko.json | 8 +- .../lutron_caseta/.translations/ko.json | 5 + .../components/mailgun/.translations/ko.json | 2 +- .../media_player/.translations/ko.json | 10 +- .../owntracks/.translations/ko.json | 2 +- .../components/plaato/.translations/ko.json | 2 +- .../components/plex/.translations/ko.json | 1 + .../components/ps4/.translations/ko.json | 2 +- .../components/sensor/.translations/ko.json | 36 ++-- .../components/soma/.translations/ko.json | 4 +- .../components/sonos/.translations/ko.json | 2 +- .../components/starline/.translations/ko.json | 42 +++++ .../components/switch/.translations/ko.json | 12 +- .../components/tesla/.translations/fr.json | 30 +++ .../components/tesla/.translations/ko.json | 30 +++ .../components/tesla/.translations/ru.json | 30 +++ .../components/tplink/.translations/ko.json | 2 +- .../components/traccar/.translations/ko.json | 2 +- .../components/twilio/.translations/ko.json | 2 +- .../components/upnp/.translations/ko.json | 2 +- .../components/vacuum/.translations/ko.json | 16 ++ .../components/wemo/.translations/ko.json | 2 +- .../components/wled/.translations/ko.json | 11 +- .../components/zha/.translations/ko.json | 30 +-- 49 files changed, 511 insertions(+), 198 deletions(-) create mode 100644 homeassistant/components/climate/.translations/ko.json create mode 100644 homeassistant/components/demo/.translations/ko.json create mode 100644 homeassistant/components/elgato/.translations/ko.json create mode 100644 homeassistant/components/fan/.translations/ko.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/ko.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/ko.json create mode 100644 homeassistant/components/icloud/.translations/ko.json create mode 100644 homeassistant/components/lutron_caseta/.translations/ko.json create mode 100644 homeassistant/components/starline/.translations/ko.json create mode 100644 homeassistant/components/tesla/.translations/fr.json create mode 100644 homeassistant/components/tesla/.translations/ko.json create mode 100644 homeassistant/components/tesla/.translations/ru.json create mode 100644 homeassistant/components/vacuum/.translations/ko.json diff --git a/homeassistant/components/alarm_control_panel/.translations/ko.json b/homeassistant/components/alarm_control_panel/.translations/ko.json index 5d6caa5fe1245f..b70ae8dc0259b0 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ko.json +++ b/homeassistant/components/alarm_control_panel/.translations/ko.json @@ -6,6 +6,13 @@ "arm_night": "{entity_name} \uc57c\uac04\uacbd\ube44", "disarm": "{entity_name} \uacbd\ube44\ud574\uc81c", "trigger": "{entity_name} \ud2b8\ub9ac\uac70" + }, + "trigger_type": { + "armed_away": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c", + "armed_home": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c", + "armed_night": "{entity_name} \uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c", + "disarmed": "{entity_name} \uc774(\uac00) \ud574\uc81c\ub420 \ub54c", + "triggered": "{entity_name} \uc774(\uac00) \ud2b8\ub9ac\uac70\ub420 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ko.json b/homeassistant/components/almond/.translations/ko.json index 2a2346aaf501a9..9f1e71163d651a 100644 --- a/homeassistant/components/almond/.translations/ko.json +++ b/homeassistant/components/almond/.translations/ko.json @@ -5,6 +5,11 @@ "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ko.json b/homeassistant/components/ambient_station/.translations/ko.json index 541b8699dc815f..eb9209a6c3781d 100644 --- a/homeassistant/components/ambient_station/.translations/ko.json +++ b/homeassistant/components/ambient_station/.translations/ko.json @@ -1,15 +1,15 @@ { "config": { "error": { - "identifier_exists": "Application \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "invalid_key": "Application \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "identifier_exists": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_devices": "\uacc4\uc815\uc5d0 \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, "step": { "user": { "data": { "api_key": "API \ud0a4", - "app_key": "Application \ud0a4" + "app_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4" }, "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825" } diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json index 167708c2cf14bd..4c1cba2bec5eb4 100644 --- a/homeassistant/components/binary_sensor/.translations/ko.json +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -1,94 +1,94 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", - "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc2b5\ub2c8\ub2e4", - "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc2b5\ub2c8\ub2e4", - "is_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4", - "is_moist": "{entity_name} \uc774(\uac00) \uc2b5\ud569\ub2c8\ub2e4", - "is_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc600\uc2b5\ub2c8\ub2e4", - "is_no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", - "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4", - "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "is_not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud569\ub2c8\ub2e4", - "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", - "is_not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud614\uc2b5\ub2c8\ub2e4", - "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc2b5\ub2c8\ub2e4", - "is_not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud569\ub2c8\ub2e4", - "is_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4", - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", - "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", - "is_plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud614\uc2b5\ub2c8\ub2e4", - "is_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_present": "{entity_name} \uc774(\uac00) \uc788\uc2b5\ub2c8\ub2e4", - "is_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4" + "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud558\uba74", + "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6b0\uba74", + "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5b4 \uc788\uc73c\uba74", + "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uba74", + "is_hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6b0\uba74", + "is_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uba74", + "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74", + "is_moist": "{entity_name} \uc774(\uac00) \uc2b5\ud558\uba74", + "is_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uba74", + "is_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uba74", + "is_no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774\uba74", + "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\ub2e4\uba74", + "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc838 \uc788\ub2e4\uba74", + "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\ub2e4\uba74", + "is_not_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74", + "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud558\uba74", + "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc73c\uba74", + "is_not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774\uc9c0 \uc54a\uc73c\uba74", + "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74", + "is_not_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \ubf51\ud600 \uc788\uc73c\uba74", + "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc73c\uba74", + "is_not_present": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74", + "is_not_unsafe": "{entity_name} \uc774(\uac00) \uc548\uc804\ud558\uba74", + "is_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774\uba74", + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", + "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74", + "is_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud600 \uc788\uc73c\uba74", + "is_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uace0 \uc788\uc73c\uba74", + "is_present": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc911\uc774\uba74", + "is_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uba74", + "is_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uba74", + "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uba74", + "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uba74" }, "trigger_type": { - "bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871", - "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", - "cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6cc\uc9d0", - "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub428", - "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud568", - "hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6cc\uc9d0", - "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud568", - "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae40", - "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0", - "moist\u00a7": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0", - "motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud568", - "moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784", - "no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0 \ubabb\ud568", - "no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0 \ubabb\ud568", - "no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0 \ubabb\ud568", - "not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc815\uc0c1", - "not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc74c", - "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9d0", - "not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc74c", - "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub428", - "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9d0", - "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc74c", - "not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc74c", - "not_opened": "{entity_name} \uc774(\uac00) \ub2eb\ud798", - "not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud798", - "not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc74c", - "not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc74c", - "not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud574\uc9d0", - "occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911", - "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9bc", - "plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud798", - "powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub428", - "present": "{entity_name} \uc774(\uac00) \uc788\uc74c", - "problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud568", - "smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud568", - "sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud568", - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9d0", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9d0", - "unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc74c", - "vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud568" + "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud574\uc9c8 \ub54c", + "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c", + "cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6cc\uc9c8 \ub54c", + "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub420 \ub54c", + "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud560 \ub54c", + "hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6cc\uc9c8 \ub54c", + "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud560 \ub54c", + "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae38 \ub54c", + "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9c8 \ub54c", + "moist\u00a7": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9c8 \ub54c", + "motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud560 \ub54c", + "moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc77c \ub54c", + "no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_bat_low": "{entity_name} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774 \ub420 \ub54c", + "not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9c8 \ub54c", + "not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub420 \ub54c", + "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9c8 \ub54c", + "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc744 \ub54c", + "not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_opened": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c", + "not_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \ubf51\ud790 \ub54c", + "not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc744 \ub54c", + "not_present": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc0c1\ud0dc\uac00 \ub420 \ub54c", + "not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud574\uc9c8 \ub54c", + "occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774 \ub420 \ub54c", + "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9b4 \ub54c", + "plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud790 \ub54c", + "powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub420 \ub54c", + "present": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub420 \ub54c", + "problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud560 \ub54c", + "smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud560 \ub54c", + "sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud560 \ub54c", + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c", + "unsafe": "{entity_name} \uc774(\uac00) \uc548\uc804\ud558\uc9c0 \uc54a\uc744 \ub54c", + "vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud560 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json index 1374372aa24be9..f0eebf4b7b9e52 100644 --- a/homeassistant/components/cast/.translations/ko.json +++ b/homeassistant/components/cast/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Google \uce90\uc2a4\ud2b8" } }, diff --git a/homeassistant/components/climate/.translations/ko.json b/homeassistant/components/climate/.translations/ko.json new file mode 100644 index 00000000000000..299172958e8235 --- /dev/null +++ b/homeassistant/components/climate/.translations/ko.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "{entity_name} \uc758 HVAC \ubaa8\ub4dc \ubcc0\uacbd", + "set_preset_mode": "{entity_name} \uc758 \uc0ac\uc804 \uc124\uc815 \ubcc0\uacbd" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 HVAC \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74", + "is_preset_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 \uc0ac\uc804 \uc124\uc815 \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \uc774(\uac00) \uc2b5\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud560 \ub54c", + "current_temperature_changed": "{entity_name} \uc774(\uac00) \uc628\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud560 \ub54c", + "hvac_mode_changed": "{entity_name} HVAC \ubaa8\ub4dc\uac00 \ubcc0\uacbd\ub420 \ub54c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/ko.json b/homeassistant/components/coolmaster/.translations/ko.json index ff6ddf0acfe7ae..4d96e606c7b5c6 100644 --- a/homeassistant/components/coolmaster/.translations/ko.json +++ b/homeassistant/components/coolmaster/.translations/ko.json @@ -13,7 +13,7 @@ "heat": "\ub09c\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", "heat_cool": "\uc790\ub3d9 \ub0c9/\ub09c\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", "host": "\ud638\uc2a4\ud2b8", - "off": "\uc804\uc6d0\uc744 \ub04c \uc218 \uc788\uc2b4" + "off": "\uc804\uc6d0 \ub044\uae30 \ud5c8\uc6a9" }, "title": "CoolMasterNet \uc5f0\uacb0 \uc0c1\uc138\uc815\ubcf4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694." } diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json index 6a59bb9f6aea93..145938b6f24245 100644 --- a/homeassistant/components/cover/.translations/ko.json +++ b/homeassistant/components/cover/.translations/ko.json @@ -1,20 +1,20 @@ { "device_automation": { "condition_type": { - "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", - "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud799\ub2c8\ub2e4", - "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", - "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4", - "is_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uc704\uce58", - "is_tilt_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30" + "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74", + "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc774\uba74", + "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74", + "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc774\uba74", + "is_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uc704\uce58\uac00 ~ \uc774\uba74", + "is_tilt_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 ~ \uc774\uba74" }, "trigger_type": { - "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", - "closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911", - "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9bc", - "opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911", - "position": "{entity_name} \uac1c\ud3d0 \uc704\uce58 \ubcc0\ud654", - "tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30 \ubcc0\ud654" + "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c", + "closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc77c \ub54c", + "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9b4 \ub54c", + "opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc77c \ub54c", + "position": "{entity_name} \uac1c\ud3d0 \uc704\uce58\uac00 \ubcc0\ud560 \ub54c", + "tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 \ubcc0\ud560 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index fede936b964d65..2c9b864dfc11d2 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -65,27 +65,27 @@ "turn_on": "\ucf1c\uae30" }, "trigger_type": { - "remote_awakened": "\uae30\uae30 \uc808\uc804 \ubaa8\ub4dc \ud574\uc81c\ub428", - "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", - "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", - "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", - "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", - "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", - "remote_button_rotated": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804", - "remote_button_rotation_stopped": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804 \uc815\uc9c0", - "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", - "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", - "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984", - "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \ub354\ube14\ud0ed \ub428", - "remote_falling": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc9d0", - "remote_gyro_activated": "\uae30\uae30 \ud754\ub4e6", - "remote_moved": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc784", - "remote_rotate_from_side_1": "\"\uba74 1\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", - "remote_rotate_from_side_2": "\"\uba74 2\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", - "remote_rotate_from_side_3": "\"\uba74 3\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", - "remote_rotate_from_side_4": "\"\uba74 4\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", - "remote_rotate_from_side_5": "\"\uba74 5\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", - "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428" + "remote_awakened": "\uae30\uae30 \uc808\uc804 \ubaa8\ub4dc \ud574\uc81c\ub420 \ub54c", + "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c", + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", + "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_rotated": "\"{subtype}\" \ub85c \ubc84\ud2bc\uc774 \ud68c\uc804\ub420 \ub54c", + "remote_button_rotation_stopped": "\"{subtype}\" \ub85c \ubc84\ud2bc\uc774 \ud68c\uc804\uc744 \uba48\ucd9c \ub54c", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", + "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c", + "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \ub354\ube14 \ud0ed \ub420 \ub54c", + "remote_falling": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc9c8 \ub54c", + "remote_gyro_activated": "\uae30\uae30\uac00 \ud754\ub4e4\ub9b4 \ub54c", + "remote_moved": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc77c \ub54c", + "remote_rotate_from_side_1": "\"\uba74 1\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_rotate_from_side_2": "\"\uba74 2\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_rotate_from_side_3": "\"\uba74 3\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_rotate_from_side_4": "\"\uba74 4\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_rotate_from_side_5": "\"\uba74 5\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c" } }, "options": { diff --git a/homeassistant/components/demo/.translations/ko.json b/homeassistant/components/demo/.translations/ko.json new file mode 100644 index 00000000000000..d20943c7b36354 --- /dev/null +++ b/homeassistant/components/demo/.translations/ko.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\ub370\ubaa8" + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/ko.json b/homeassistant/components/device_tracker/.translations/ko.json index 92137ea27685d1..1834767222a975 100644 --- a/homeassistant/components/device_tracker/.translations/ko.json +++ b/homeassistant/components/device_tracker/.translations/ko.json @@ -1,8 +1,8 @@ { "device_automation": { "condition_type": { - "is_home": "{entity_name} \uc774(\uac00) \uc9d1\uc5d0 \uc788\uc2b5\ub2c8\ub2e4", - "is_not_home": "{entity_name} \uc774(\uac00) \uc678\ucd9c\uc911\uc785\ub2c8\ub2e4" + "is_home": "{entity_name} \uc774(\uac00) \uc9d1\uc5d0 \uc788\uc73c\uba74", + "is_not_home": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74" } } } \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/ko.json b/homeassistant/components/dialogflow/.translations/ko.json index 91f15f1fb7714d..2010495d959e8c 100644 --- a/homeassistant/components/dialogflow/.translations/ko.json +++ b/homeassistant/components/dialogflow/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Dialogflow \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Dialogflow \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Dialogflow Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/elgato/.translations/ko.json b/homeassistant/components/elgato/.translations/ko.json new file mode 100644 index 00000000000000..9d7ab4ef2b0e72 --- /dev/null +++ b/homeassistant/components/elgato/.translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Elgato Key Light \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "connection_error": "Elgato Key Light \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "Elgato Key Light \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8 \ubc88\ud638" + }, + "description": "Home Assistant \uc5d0 Elgato Key Light \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "title": "Elgato Key Light \uc5f0\uacb0" + }, + "zeroconf_confirm": { + "description": "Elgato Key Light \uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}` \uc744(\ub97c) Home Assistant \uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c Elgato Key Light \uae30\uae30" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/ko.json b/homeassistant/components/esphome/.translations/ko.json index b6bcf3cd1b3377..4d8068c801b35b 100644 --- a/homeassistant/components/esphome/.translations/ko.json +++ b/homeassistant/components/esphome/.translations/ko.json @@ -18,8 +18,8 @@ "title": "\ube44\ubc00\ubc88\ud638 \uc785\ub825" }, "discovery_confirm": { - "description": "Home Assistant \uc5d0 ESPHome node `{name}` \uc744(\ub97c) \ucd94\uac00 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "\ubc1c\uacac \ub41c ESPHome node" + "description": "Home Assistant \uc5d0 ESPHome node `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c ESPHome node" }, "user": { "data": { diff --git a/homeassistant/components/fan/.translations/ko.json b/homeassistant/components/fan/.translations/ko.json new file mode 100644 index 00000000000000..dec2a711e578e5 --- /dev/null +++ b/homeassistant/components/fan/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name} \ub044\uae30", + "turn_on": "{entity_name} \ucf1c\uae30" + }, + "condition_type": { + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" + }, + "trigger_type": { + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/ko.json b/homeassistant/components/geofency/.translations/ko.json index 42ff061a151a73..37f5ef0e76a606 100644 --- a/homeassistant/components/geofency/.translations/ko.json +++ b/homeassistant/components/geofency/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Geofency Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Geofency Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Geofency Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/geonetnz_volcano/.translations/ko.json b/homeassistant/components/geonetnz_volcano/.translations/ko.json new file mode 100644 index 00000000000000..5d393fef4c49fb --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "radius": "\ubc18\uacbd" + }, + "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/ko.json b/homeassistant/components/gpslogger/.translations/ko.json index 786a67b0b19eb3..19bfc36e424233 100644 --- a/homeassistant/components/gpslogger/.translations/ko.json +++ b/homeassistant/components/gpslogger/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "GPSLogger Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "GPSLogger Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "GPSLogger Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/hisense_aehw4a1/.translations/ko.json b/homeassistant/components/hisense_aehw4a1/.translations/ko.json new file mode 100644 index 00000000000000..6d8b6b4b44ce1d --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Hisense AEH-W4A1 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 Hisense AEH-W4A1 \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "Hisense AEH-W4A1 \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/ko.json b/homeassistant/components/icloud/.translations/ko.json new file mode 100644 index 00000000000000..a689a89527897e --- /dev/null +++ b/homeassistant/components/icloud/.translations/ko.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", + "send_verification_code": "\uc778\uc99d \ucf54\ub4dc\ub97c \ubcf4\ub0b4\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "validate_verification_code": "\uc778\uc99d \ucf54\ub4dc\ub97c \ud655\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30\ub97c \uc120\ud0dd\ud558\uace0 \uc778\uc99d\uc744 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "\uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30" + }, + "description": "\uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "title": "iCloud \uac00 \uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30" + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + }, + "description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "iCloud \uc790\uaca9 \uc99d\uba85" + }, + "verification_code": { + "data": { + "verification_code": "\uc778\uc99d \ucf54\ub4dc" + }, + "description": "iCloud \uc5d0\uc11c \ubc1b\uc740 \uc778\uc99d \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "iCloud \uc778\uc99d \ucf54\ub4dc" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/ko.json b/homeassistant/components/ifttt/.translations/ko.json index 75bdd0d99c8ec7..9c8083a1d9452d 100644 --- a/homeassistant/components/ifttt/.translations/ko.json +++ b/homeassistant/components/ifttt/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "IFTTT \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "IFTTT \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "IFTTT Webhook \uc560\ud50c\ub9bf \uc124\uc815" } }, diff --git a/homeassistant/components/ios/.translations/ko.json b/homeassistant/components/ios/.translations/ko.json index 1496dab05558cf..283594a45b51f6 100644 --- a/homeassistant/components/ios/.translations/ko.json +++ b/homeassistant/components/ios/.translations/ko.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Home Assistant iOS" } }, diff --git a/homeassistant/components/izone/.translations/ko.json b/homeassistant/components/izone/.translations/ko.json index 69b8ce8a31ea35..91593b2651127a 100644 --- a/homeassistant/components/izone/.translations/ko.json +++ b/homeassistant/components/izone/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "iZone \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "iZone \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "iZone" } }, diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json index e055f67421ef53..b923fdb210e673 100644 --- a/homeassistant/components/light/.translations/ko.json +++ b/homeassistant/components/light/.translations/ko.json @@ -6,12 +6,12 @@ "turn_on": "{entity_name} \ucf1c\uae30" }, "condition_type": { - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" }, "trigger_type": { - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/ko.json b/homeassistant/components/locative/.translations/ko.json index 0649ed557c4f10..c53f538799fea8 100644 --- a/homeassistant/components/locative/.translations/ko.json +++ b/homeassistant/components/locative/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Locative Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Locative Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Locative Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/lock/.translations/ko.json b/homeassistant/components/lock/.translations/ko.json index 6abd9cd60e6449..fb202f73b37ace 100644 --- a/homeassistant/components/lock/.translations/ko.json +++ b/homeassistant/components/lock/.translations/ko.json @@ -6,8 +6,12 @@ "unlock": "{entity_name} \uc7a0\uae08 \ud574\uc81c" }, "condition_type": { - "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4", - "is_unlocked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74", + "is_unlocked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74" + }, + "trigger_type": { + "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae38 \ub54c", + "unlocked": "{entity_name} \uc774(\uac00) \uc7a0\uae08\uc774 \ud574\uc81c\ub420 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/ko.json b/homeassistant/components/lutron_caseta/.translations/ko.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/ko.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ko.json b/homeassistant/components/mailgun/.translations/ko.json index 4ca5b155e7371c..8f1f021caf617f 100644 --- a/homeassistant/components/mailgun/.translations/ko.json +++ b/homeassistant/components/mailgun/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Mailgun \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Mailgun \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Mailgun Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/media_player/.translations/ko.json b/homeassistant/components/media_player/.translations/ko.json index 7542154448f620..49367eaf617464 100644 --- a/homeassistant/components/media_player/.translations/ko.json +++ b/homeassistant/components/media_player/.translations/ko.json @@ -1,11 +1,11 @@ { "device_automation": { "condition_type": { - "is_idle": "{entity_name} \uc774(\uac00) \uc720\ud734\uc0c1\ud0dc\uc785\ub2c8\ub2e4", - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", - "is_paused": "{entity_name} \uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_playing": "{entity_name} \uc774(\uac00) \uc7ac\uc0dd\uc911\uc785\ub2c8\ub2e4" + "is_idle": "{entity_name} \uc774(\uac00) \uc720\ud734\uc0c1\ud0dc\uc774\uba74", + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", + "is_paused": "{entity_name} \uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5b4 \uc788\uc73c\uba74", + "is_playing": "{entity_name} \uc774(\uac00) \uc7ac\uc0dd \uc911\uc774\uba74" } } } \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/ko.json b/homeassistant/components/owntracks/.translations/ko.json index d70ca8b114ec6c..ee1507d9e0aebc 100644 --- a/homeassistant/components/owntracks/.translations/ko.json +++ b/homeassistant/components/owntracks/.translations/ko.json @@ -8,7 +8,7 @@ }, "step": { "user": { - "description": "OwnTracks \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "OwnTracks \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "OwnTracks \uc124\uc815" } }, diff --git a/homeassistant/components/plaato/.translations/ko.json b/homeassistant/components/plaato/.translations/ko.json index 50a51dff873186..619fdcf736f9ff 100644 --- a/homeassistant/components/plaato/.translations/ko.json +++ b/homeassistant/components/plaato/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Plaato Airlock \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Plaato Airlock \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Plaato Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json index f8e78945802bd1..cf5a7946b9ddd1 100644 --- a/homeassistant/components/plex/.translations/ko.json +++ b/homeassistant/components/plex/.translations/ko.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", "discovery_no_file": "\ub808\uac70\uc2dc \uad6c\uc131 \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "non-interactive": "\ube44 \ub300\ud654\ud615 \uac00\uc838\uc624\uae30", "token_request_timeout": "\ud1a0\ud070 \ud68d\ub4dd \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/ps4/.translations/ko.json b/homeassistant/components/ps4/.translations/ko.json index 25f64cd21e9db2..46bbd6b309cdaf 100644 --- a/homeassistant/components/ps4/.translations/ko.json +++ b/homeassistant/components/ps4/.translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "credential_error": "\uc790\uaca9 \uc99d\uba85\uc744 \uac00\uc838\uc624\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "devices_configured": "\ubc1c\uacac \ub41c \ubaa8\ub4e0 \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "devices_configured": "\ubc1c\uacac\ub41c \ubaa8\ub4e0 \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "no_devices_found": "PlayStation 4 \uae30\uae30\ub97c \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "port_987_bind_error": "\ud3ec\ud2b8 987 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "port_997_bind_error": "\ud3ec\ud2b8 997 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." diff --git a/homeassistant/components/sensor/.translations/ko.json b/homeassistant/components/sensor/.translations/ko.json index 0e74f3f4f897d3..7716cc016c34e6 100644 --- a/homeassistant/components/sensor/.translations/ko.json +++ b/homeassistant/components/sensor/.translations/ko.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "\ud604\uc7ac {entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", - "is_humidity": "\ud604\uc7ac {entity_name} \uc2b5\ub3c4", - "is_illuminance": "\ud604\uc7ac {entity_name} \uc870\ub3c4", - "is_power": "\ud604\uc7ac {entity_name} \uc18c\ube44 \uc804\ub825", - "is_pressure": "\ud604\uc7ac {entity_name} \uc555\ub825", - "is_signal_strength": "\ud604\uc7ac {entity_name} \uc2e0\ud638 \uac15\ub3c4", - "is_temperature": "\ud604\uc7ac {entity_name} \uc628\ub3c4", - "is_timestamp": "\ud604\uc7ac {entity_name} \uc2dc\uac01", - "is_value": "\ud604\uc7ac {entity_name} \uac12" + "is_battery_level": "\ud604\uc7ac {entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 ~ \uc774\uba74", + "is_humidity": "\ud604\uc7ac {entity_name} \uc2b5\ub3c4\uac00 ~ \uc774\uba74", + "is_illuminance": "\ud604\uc7ac {entity_name} \uc870\ub3c4\uac00 ~ \uc774\uba74", + "is_power": "\ud604\uc7ac {entity_name} \uc18c\ube44 \uc804\ub825\uc774 ~ \uc774\uba74", + "is_pressure": "\ud604\uc7ac {entity_name} \uc555\ub825\uc774 ~ \uc774\uba74", + "is_signal_strength": "\ud604\uc7ac {entity_name} \uc2e0\ud638 \uac15\ub3c4\uac00 ~ \uc774\uba74", + "is_temperature": "\ud604\uc7ac {entity_name} \uc628\ub3c4\uac00 ~ \uc774\uba74", + "is_timestamp": "\ud604\uc7ac {entity_name} \uc2dc\uac01\uc774 ~ \uc774\uba74", + "is_value": "\ud604\uc7ac {entity_name} \uac12\uc774 ~ \uc774\uba74" }, "trigger_type": { - "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubcc0\ud654", - "humidity": "{entity_name} \uc2b5\ub3c4 \ubcc0\ud654", - "illuminance": "{entity_name} \uc870\ub3c4 \ubcc0\ud654", - "power": "{entity_name} \uc18c\ube44 \uc804\ub825 \ubcc0\ud654", - "pressure": "{entity_name} \uc555\ub825 \ubcc0\ud654", - "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4 \ubcc0\ud654", - "temperature": "{entity_name} \uc628\ub3c4 \ubcc0\ud654", - "timestamp": "{entity_name} \uc2dc\uac01 \ubcc0\ud654", - "value": "{entity_name} \uac12 \ubcc0\ud654" + "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubc14\ub014 \ub54c", + "humidity": "{entity_name} \uc2b5\ub3c4\uac00 \ubc14\ub014 \ub54c", + "illuminance": "{entity_name} \uc870\ub3c4\uac00 \ubc14\ub014 \ub54c", + "power": "{entity_name} \uc18c\ube44 \uc804\ub825\uc774 \ubc14\ub014 \ub54c", + "pressure": "{entity_name} \uc555\ub825\uc774 \ubc14\ub014 \ub54c", + "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4\uac00 \ubc14\ub014 \ub54c", + "temperature": "{entity_name} \uc628\ub3c4\uac00 \ubc14\ub014 \ub54c", + "timestamp": "{entity_name} \uc2dc\uac01\uc774 \ubc14\ub014 \ub54c", + "value": "{entity_name} \uac12\uc774 \ubc14\ub014 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ko.json b/homeassistant/components/soma/.translations/ko.json index 90995ebc9f2110..ae4d84671a3c5a 100644 --- a/homeassistant/components/soma/.translations/ko.json +++ b/homeassistant/components/soma/.translations/ko.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "\ud558\ub098\uc758 Soma \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "Soma \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + "connection_error": "SOMA Connect \uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "Soma \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "result_error": "SOMA Connect \uac00 \uc624\ub958 \uc0c1\ud0dc\ub85c \uc751\ub2f5\ud588\uc2b5\ub2c8\ub2e4." }, "create_entry": { "default": "Soma \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/sonos/.translations/ko.json b/homeassistant/components/sonos/.translations/ko.json index 4ca3d621599d9e..931a0beadfc9eb 100644 --- a/homeassistant/components/sonos/.translations/ko.json +++ b/homeassistant/components/sonos/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Sonos \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Sonos \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Sonos" } }, diff --git a/homeassistant/components/starline/.translations/ko.json b/homeassistant/components/starline/.translations/ko.json new file mode 100644 index 00000000000000..4d7ecf427f8657 --- /dev/null +++ b/homeassistant/components/starline/.translations/ko.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID \ud639\uc740 \ubcf4\uc548\ud0a4\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "error_auth_mfa": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "error_auth_user": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + }, + "step": { + "auth_app": { + "data": { + "app_id": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID", + "app_secret": "\ubcf4\uc548\ud0a4" + }, + "description": "StarLine \uac1c\ubc1c\uc790 \uacc4\uc815\uc758 \uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID \ubc0f \ube44\ubc00\ubc88\ud638", + "title": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \uc790\uaca9 \uc99d\uba85" + }, + "auth_captcha": { + "data": { + "captcha_code": "\uc774\ubbf8\uc9c0\uc758 \ucf54\ub4dc" + }, + "description": "{captcha_img}", + "title": "\ubcf4\uc548 \ubb38\uc790" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS \ucf54\ub4dc" + }, + "description": "{phone_number} \uc804\ud654\ub85c \uc804\uc1a1\ub41c \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "2\ub2e8\uacc4 \uc778\uc99d" + }, + "auth_user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "StarLine \uacc4\uc815 \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638", + "title": "\uc0ac\uc6a9\uc790 \uc790\uaca9 \uc99d\uba85" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ko.json b/homeassistant/components/switch/.translations/ko.json index 02c303f932987b..d3b9b1dd16904d 100644 --- a/homeassistant/components/switch/.translations/ko.json +++ b/homeassistant/components/switch/.translations/ko.json @@ -6,14 +6,14 @@ "turn_on": "{entity_name} \ucf1c\uae30" }, "condition_type": { - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", - "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", + "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" }, "trigger_type": { - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/fr.json b/homeassistant/components/tesla/.translations/fr.json new file mode 100644 index 00000000000000..69742d3370c4f2 --- /dev/null +++ b/homeassistant/components/tesla/.translations/fr.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Erreur de connexion; v\u00e9rifier le r\u00e9seau et r\u00e9essayer", + "identifier_exists": "Email d\u00e9j\u00e0 enregistr\u00e9", + "invalid_credentials": "Informations d'identification invalides", + "unknown_error": "Erreur inconnue, veuillez signaler les informations du journal" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Adresse e-mail" + }, + "description": "Veuillez saisir vos informations.", + "title": "Tesla - Configuration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Secondes entre les scans" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/ko.json b/homeassistant/components/tesla/.translations/ko.json new file mode 100644 index 00000000000000..8b7dc9ce93c199 --- /dev/null +++ b/homeassistant/components/tesla/.translations/ko.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "\uc5f0\uacb0 \uc624\ub958; \ub124\ud2b8\uc6cc\ud06c\ub97c \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", + "identifier_exists": "\uc774\uba54\uc77c \uc8fc\uc18c\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_credentials": "\uc774\uba54\uc77c \uc8fc\uc18c \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub85c\uadf8 \ub0b4\uc6a9\uc744 \uc54c\ub824\uc8fc\uc138\uc694" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + }, + "description": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "Tesla - \uad6c\uc131" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ucd08)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/ru.json b/homeassistant/components/tesla/.translations/ru.json new file mode 100644 index 00000000000000..15eeabf6136a8a --- /dev/null +++ b/homeassistant/components/tesla/.translations/ru.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u0435\u0442\u044c \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "unknown_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438.", + "title": "Tesla" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 (\u0441\u0435\u043a.)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/ko.json b/homeassistant/components/tplink/.translations/ko.json index 05bebdd14554d3..89255d785182ba 100644 --- a/homeassistant/components/tplink/.translations/ko.json +++ b/homeassistant/components/tplink/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uae30\uae30\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/traccar/.translations/ko.json b/homeassistant/components/traccar/.translations/ko.json index d9f31967e68b9e..40e1aaf4d6bf9c 100644 --- a/homeassistant/components/traccar/.translations/ko.json +++ b/homeassistant/components/traccar/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Traccar \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Traccar \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Traccar \uc124\uc815" } }, diff --git a/homeassistant/components/twilio/.translations/ko.json b/homeassistant/components/twilio/.translations/ko.json index 4e4c80801d4f3d..b8e88820590267 100644 --- a/homeassistant/components/twilio/.translations/ko.json +++ b/homeassistant/components/twilio/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Twilio \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Twilio \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Twilio Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json index d846a5e38ce342..bd6aaeef4e2945 100644 --- a/homeassistant/components/upnp/.translations/ko.json +++ b/homeassistant/components/upnp/.translations/ko.json @@ -10,7 +10,7 @@ }, "step": { "confirm": { - "description": "UPnP/IGD \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "UPnP/IGD \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "UPnP/IGD" }, "init": { diff --git a/homeassistant/components/vacuum/.translations/ko.json b/homeassistant/components/vacuum/.translations/ko.json new file mode 100644 index 00000000000000..0197329abda0b1 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "{entity_name} \uc744(\ub97c) \uccad\uc18c\uc2dc\ud0a4\uae30", + "dock": "{entity_name} \uc744(\ub97c) \ucda9\uc804\uc2a4\ud14c\uc774\uc158\uc73c\ub85c \ubcf5\uadc0\uc2dc\ud0a4\uae30" + }, + "condition_type": { + "is_cleaning": "{entity_name} \uc774(\uac00) \uccad\uc18c \uc911\uc774\uba74", + "is_docked": "{entity_name} \uc774(\uac00) \ub3c4\ud0b9\ub418\uc5b4\uc788\uc73c\uba74" + }, + "trigger_type": { + "cleaning": "{entity_name} \uc774(\uac00) \uccad\uc18c\ub97c \uc2dc\uc791\ud560 \ub54c", + "docked": "{entity_name} \uc774(\uac00) \ub3c4\ud0b9\ub420 \ub54c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/.translations/ko.json b/homeassistant/components/wemo/.translations/ko.json index 57515f2c9708f5..cc3a70a0bc60c3 100644 --- a/homeassistant/components/wemo/.translations/ko.json +++ b/homeassistant/components/wemo/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Wemo \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Wemo \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Wemo" } }, diff --git a/homeassistant/components/wled/.translations/ko.json b/homeassistant/components/wled/.translations/ko.json index bee9c2a6204be4..38496c01ee836a 100644 --- a/homeassistant/components/wled/.translations/ko.json +++ b/homeassistant/components/wled/.translations/ko.json @@ -12,8 +12,15 @@ "user": { "data": { "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c" - } + }, + "description": "Home Assistant \uc5d0 WLED \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "title": "WLED \uc5f0\uacb0" + }, + "zeroconf_confirm": { + "description": "Home Assistant \uc5d0 WLED `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c WLED \uae30\uae30" } - } + }, + "title": "WLED" } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json index 3a62f5d7ebe2f4..69b8f9ad9a4f1c 100644 --- a/homeassistant/components/zha/.translations/ko.json +++ b/homeassistant/components/zha/.translations/ko.json @@ -47,21 +47,21 @@ "turn_on": "\ucf1c\uae30" }, "trigger_type": { - "device_dropped": "\uae30\uae30\ub97c \ub5a8\uad7c", - "device_flipped": "\"{subtype}\" \uae30\uae30\ub97c \ub4a4\uc9d1\uc74c", - "device_knocked": "\"{subtype}\" \uae30\uae30\ub97c \ub450\ub4dc\ub9bc", - "device_rotated": "\"{subtype}\" \uae30\uae30\ub97c \ud68c\uc804", - "device_shaken": "\uae30\uae30\ub97c \ud754\ub4e6", - "device_slid": "\"{subtype}\" \uae30\uae30\ub97c \uc2ac\ub77c\uc774\ub4dc", - "device_tilted": "\uae30\uae30\ub97c \uae30\uc6b8\uc784", - "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", - "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", - "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", - "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", - "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", - "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", - "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", - "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984" + "device_dropped": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc84c\uc744 \ub54c", + "device_flipped": "\"{subtype}\" \uae30\uae30\uac00 \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c", + "device_knocked": "\"{subtype}\" \uae30\uae30\uac00 \ub450\ub4dc\ub824\uc9c8 \ub54c", + "device_rotated": "\"{subtype}\" \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "device_shaken": "\uae30\uae30\uac00 \ud754\ub4e4\ub9b4 \ub54c", + "device_slid": "\"{subtype}\" \uae30\uae30\uac00 \ubbf8\ub044\ub7ec\uc9c8 \ub54c", + "device_tilted": "\uae30\uae30\uac00 \uae30\uc6b8\uc5b4\uc9c8 \ub54c", + "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c", + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", + "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", + "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c" } } } \ No newline at end of file From 217b974cefaae4dbbee4f4ba916937b8a0e0dba9 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Wed, 25 Dec 2019 10:18:10 +0100 Subject: [PATCH 2549/3953] Upgrade youtube_dl to 2019.12.25 (#30203) --- 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 16f491f0caefd3..b57b43968695a0 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/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.11.28" + "youtube_dl==2019.12.25" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 7cfaff94ef87f8..fedfa2f61ec656 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2085,7 +2085,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.11.28 +youtube_dl==2019.12.25 # homeassistant.components.zengge zengge==0.2 From 5e3102b2d6c3aece0009bfbb0894ccb7b478bff5 Mon Sep 17 00:00:00 2001 From: Niall Donegan Date: Wed, 25 Dec 2019 10:09:03 +0000 Subject: [PATCH 2550/3953] Pull track position from MPD status (#28579) * Pull track position from MPD status() This allows the progress bar to work when using the media-control card in lovelace. * Actually commit flake8 fix! * Extra documentation. Mainly to trigger CI rerun. * Updated to use self._media_position --- homeassistant/components/mpd/media_player.py | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 2628815727c812..6460becbb3ed52 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -37,6 +37,7 @@ ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -98,6 +99,8 @@ def __init__(self, server, port, password, name): self._is_connected = False self._muted = False self._muted_volume = 0 + self._media_position_updated_at = None + self._media_position = None # set up MPD client self._client = mpd.MPDClient() @@ -130,6 +133,11 @@ def _fetch_status(self): self._status = self._client.status() self._currentsong = self._client.currentsong() + position = self._status["time"] + if self._media_position != position: + self._media_position_updated_at = dt_util.utcnow() + self._media_position = position + self._update_playlists() @property @@ -188,6 +196,20 @@ def media_duration(self): # Time does not exist for streams return self._currentsong.get("time") + @property + def media_position(self): + """Position of current playing media in seconds. + + This is returned as part of the mpd status rather than in the details + of the current song. + """ + return self._media_position + + @property + def media_position_updated_at(self): + """Last valid time of media position.""" + return self._media_position_updated_at + @property def media_title(self): """Return the title of current playing media.""" From 89450f405c76dd6d7fd166f59f9e30ee5a3bad17 Mon Sep 17 00:00:00 2001 From: rhadamantys <46837767+rhadamantys@users.noreply.github.com> Date: Wed, 25 Dec 2019 11:41:00 +0100 Subject: [PATCH 2551/3953] Add support for enocean window handle FA 10 00 (Hoppe) (#29968) * Added support for Somfy RTS wireless power socket and Somfy Temperature Sensore Thermos Wirefree io * Added code formatting fixes for commit 5faaf9c * added support for RollerShutterRTSComponent from Somfy * Added support for RTS roller shutter in set_cover_position * added support for enocean window handle FA 10 00 (Hoppe) * removed spaces in empty lines * removal of rawdata attribute / code style * isort fix * PyLint fixes * Improvements after review suggestions: rename device class to sensor type to avoid ambiguousness * added import for DEVICE_CLASS_POWER from const.py * removed window handle unit --- homeassistant/components/enocean/sensor.py | 66 ++++++++++++++++++---- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index cfab52b3665954..59ca10da791e94 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -10,8 +10,11 @@ CONF_ID, CONF_NAME, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, POWER_WATT, + STATE_CLOSED, + STATE_OPEN, TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv @@ -25,34 +28,44 @@ DEFAULT_NAME = "EnOcean sensor" -DEVICE_CLASS_POWER = "powersensor" +SENSOR_TYPE_HUMIDITY = "humidity" +SENSOR_TYPE_POWER = "powersensor" +SENSOR_TYPE_TEMPERATURE = "temperature" +SENSOR_TYPE_WINDOWHANDLE = "windowhandle" SENSOR_TYPES = { - DEVICE_CLASS_HUMIDITY: { + SENSOR_TYPE_HUMIDITY: { "name": "Humidity", "unit": "%", "icon": "mdi:water-percent", "class": DEVICE_CLASS_HUMIDITY, }, - DEVICE_CLASS_POWER: { + SENSOR_TYPE_POWER: { "name": "Power", "unit": POWER_WATT, "icon": "mdi:power-plug", "class": DEVICE_CLASS_POWER, }, - DEVICE_CLASS_TEMPERATURE: { + SENSOR_TYPE_TEMPERATURE: { "name": "Temperature", "unit": TEMP_CELSIUS, "icon": "mdi:thermometer", "class": DEVICE_CLASS_TEMPERATURE, }, + SENSOR_TYPE_WINDOWHANDLE: { + "name": "WindowHandle", + "unit": None, + "icon": "mdi:window", + "class": None, + }, } + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_POWER): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=SENSOR_TYPE_POWER): cv.string, vol.Optional(CONF_MAX_TEMP, default=40): vol.Coerce(int), vol.Optional(CONF_MIN_TEMP, default=0): vol.Coerce(int), vol.Optional(CONF_RANGE_FROM, default=255): cv.positive_int, @@ -65,9 +78,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up an EnOcean sensor device.""" dev_id = config.get(CONF_ID) dev_name = config.get(CONF_NAME) - dev_class = config.get(CONF_DEVICE_CLASS) + sensor_type = config.get(CONF_DEVICE_CLASS) - if dev_class == DEVICE_CLASS_TEMPERATURE: + if sensor_type == SENSOR_TYPE_TEMPERATURE: temp_min = config.get(CONF_MIN_TEMP) temp_max = config.get(CONF_MAX_TEMP) range_from = config.get(CONF_RANGE_FROM) @@ -80,12 +93,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] ) - elif dev_class == DEVICE_CLASS_HUMIDITY: + elif sensor_type == SENSOR_TYPE_HUMIDITY: add_entities([EnOceanHumiditySensor(dev_id, dev_name)]) - elif dev_class == DEVICE_CLASS_POWER: + elif sensor_type == SENSOR_TYPE_POWER: add_entities([EnOceanPowerSensor(dev_id, dev_name)]) + elif sensor_type == SENSOR_TYPE_WINDOWHANDLE: + add_entities([EnOceanWindowHandle(dev_id, dev_name)]) + class EnOceanSensor(enocean.EnOceanDevice): """Representation of an EnOcean sensor device such as a power meter.""" @@ -140,7 +156,7 @@ class EnOceanPowerSensor(EnOceanSensor): def __init__(self, dev_id, dev_name): """Initialize the EnOcean power sensor device.""" - super().__init__(dev_id, dev_name, DEVICE_CLASS_POWER) + super().__init__(dev_id, dev_name, SENSOR_TYPE_POWER) def value_changed(self, packet): """Update the internal state of the sensor.""" @@ -175,7 +191,7 @@ class EnOceanTemperatureSensor(EnOceanSensor): def __init__(self, dev_id, dev_name, scale_min, scale_max, range_from, range_to): """Initialize the EnOcean temperature sensor device.""" - super().__init__(dev_id, dev_name, DEVICE_CLASS_TEMPERATURE) + super().__init__(dev_id, dev_name, SENSOR_TYPE_TEMPERATURE) self._scale_min = scale_min self._scale_max = scale_max self.range_from = range_from @@ -205,7 +221,7 @@ class EnOceanHumiditySensor(EnOceanSensor): def __init__(self, dev_id, dev_name): """Initialize the EnOcean humidity sensor device.""" - super().__init__(dev_id, dev_name, DEVICE_CLASS_HUMIDITY) + super().__init__(dev_id, dev_name, SENSOR_TYPE_HUMIDITY) def value_changed(self, packet): """Update the internal state of the sensor.""" @@ -214,3 +230,29 @@ def value_changed(self, packet): humidity = packet.data[2] * 100 / 250 self._state = round(humidity, 1) self.schedule_update_ha_state() + + +class EnOceanWindowHandle(EnOceanSensor): + """Representation of an EnOcean window handle device. + + EEPs (EnOcean Equipment Profiles): + - F6-10-00 (Mechanical handle / Hoppe AG) + """ + + def __init__(self, dev_id, dev_name): + """Initialize the EnOcean window handle sensor device.""" + super().__init__(dev_id, dev_name, SENSOR_TYPE_WINDOWHANDLE) + + def value_changed(self, packet): + """Update the internal state of the sensor.""" + + action = (packet.data[1] & 0x70) >> 4 + + if action == 0x07: + self._state = STATE_CLOSED + if action in (0x04, 0x06): + self._state = STATE_OPEN + if action == 0x05: + self._state = "tilt" + + self.schedule_update_ha_state() From f56797e413f36b25b04d8ffb2d6d1b82ed468538 Mon Sep 17 00:00:00 2001 From: Kevin McCormack Date: Wed, 25 Dec 2019 05:45:49 -0500 Subject: [PATCH 2552/3953] Update Vivotek camera component (#30191) - Bump libpyvivotek version to 0.4.0 - Add digest authentication --- homeassistant/components/vivotek/camera.py | 7 +++++++ homeassistant/components/vivotek/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index 665db37344044f..f4a195f5b0c03b 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -7,12 +7,15 @@ from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import ( + CONF_AUTHENTICATION, CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.helpers import config_validation as cv @@ -34,6 +37,9 @@ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In( + [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] + ), vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, @@ -54,6 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): verify_ssl=config[CONF_VERIFY_SSL], usr=config[CONF_USERNAME], pwd=config[CONF_PASSWORD], + digest_auth=config[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION, sec_lvl=config[CONF_SECURITY_LEVEL], ), stream_source=f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}", diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index c97a8461da923e..afd7535aa0fdfa 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -3,7 +3,7 @@ "name": "Vivotek", "documentation": "https://www.home-assistant.io/integrations/vivotek", "requirements": [ - "libpyvivotek==0.3.1" + "libpyvivotek==0.4.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index fedfa2f61ec656..f69a9b8c080d51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -762,7 +762,7 @@ libpurecool==0.5.0 libpyfoscam==1.0 # homeassistant.components.vivotek -libpyvivotek==0.3.1 +libpyvivotek==0.4.0 # homeassistant.components.mikrotik librouteros==2.3.0 From a5c450857182595493e33dde62bfbeb376d9eca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 25 Dec 2019 13:42:28 +0200 Subject: [PATCH 2553/3953] Make Huawei LTE notify service name configurable (#30208) * Default Huawei LTE notify service name to notify.huawei_lte, make configurable Closes https://github.com/home-assistant/home-assistant/issues/29409 * Set default notify service name for uninvoked options flow --- .../huawei_lte/.translations/en.json | 1 + .../components/huawei_lte/__init__.py | 18 ++++++++++++++++-- .../components/huawei_lte/config_flow.py | 18 +++++++++++++++--- homeassistant/components/huawei_lte/const.py | 1 + .../components/huawei_lte/strings.json | 1 + 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 52aaafe595cdc9..5cee60fb727778 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Notification service name", "recipient": "SMS notification recipients", "track_new_devices": "Track new devices" } diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 531529b17cd45e..c79170b19dbbc4 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -28,6 +28,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + CONF_NAME, CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, @@ -54,6 +55,7 @@ ALL_KEYS, CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, + DEFAULT_NOTIFY_SERVICE_NAME, DOMAIN, KEY_DEVICE_BASIC_INFORMATION, KEY_DEVICE_INFORMATION, @@ -82,9 +84,10 @@ None, vol.Schema( { + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_RECIPIENT): vol.Any( None, vol.All(cv.ensure_list, [cv.string]) - ) + ), } ), ) @@ -262,6 +265,13 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) ): new_options[f"{CONF_RECIPIENT}_from_yaml"] = yaml_recipient new_options[CONF_RECIPIENT] = yaml_recipient + yaml_notify_name = yaml_config.get(NOTIFY_DOMAIN, {}).get(CONF_NAME) + if ( + yaml_notify_name is not None + and yaml_notify_name != config_entry.options.get(f"{CONF_NAME}_from_yaml") + ): + new_options[f"{CONF_NAME}_from_yaml"] = yaml_notify_name + new_options[CONF_NAME] = yaml_notify_name # Update entry if overrides were found if new_data or new_options: hass.config_entries.async_update_entry( @@ -353,7 +363,11 @@ def signal_update() -> None: hass, NOTIFY_DOMAIN, DOMAIN, - {CONF_URL: url, CONF_RECIPIENT: config_entry.options.get(CONF_RECIPIENT)}, + { + CONF_URL: url, + CONF_NAME: config_entry.options.get(CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME), + CONF_RECIPIENT: config_entry.options.get(CONF_RECIPIENT), + }, hass.data[DOMAIN].hass_config, ) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index b316472efaf488..64803d4ad450ed 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -21,11 +21,17 @@ from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME +from homeassistant.const import ( + CONF_NAME, + CONF_PASSWORD, + CONF_RECIPIENT, + CONF_URL, + CONF_USERNAME, +) from homeassistant.core import callback # see https://github.com/PyCQA/pylint/issues/3202 about the DOMAIN's pylint issue -from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME +from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DEFAULT_NOTIFY_SERVICE_NAME from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -246,10 +252,16 @@ async def async_step_init(self, user_input=None): data_schema = vol.Schema( { + vol.Optional( + CONF_NAME, + default=self.config_entry.options.get( + CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME + ), + ): str, vol.Optional( CONF_RECIPIENT, default=self.config_entry.options.get(CONF_RECIPIENT, ""), - ): str + ): str, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index c71b51435e1a4b..164d833f03aeff 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -3,6 +3,7 @@ DOMAIN = "huawei_lte" DEFAULT_DEVICE_NAME = "LTE" +DEFAULT_NOTIFY_SERVICE_NAME = DOMAIN UPDATE_SIGNAL = f"{DOMAIN}_update" UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 17684253671bfc..0d586e0d0ad2ac 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Notification service name", "recipient": "SMS notification recipients", "track_new_devices": "Track new devices" } From 50a87bbe18b9128a3e8ed861d9e2e42e9248bdea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 25 Dec 2019 13:43:51 +0200 Subject: [PATCH 2554/3953] Add Huawei LTE integration suspend and resume services (#30207) Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. --- .../components/huawei_lte/__init__.py | 46 +++++++++++++++---- homeassistant/components/huawei_lte/const.py | 9 ++++ homeassistant/components/huawei_lte/notify.py | 6 +++ .../components/huawei_lte/services.yaml | 17 +++++++ 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index c79170b19dbbc4..deb92de218c54b 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -52,6 +52,7 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ( + ADMIN_SERVICES, ALL_KEYS, CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, @@ -66,6 +67,8 @@ KEY_WLAN_HOST_LIST, SERVICE_CLEAR_TRAFFIC_STATISTICS, SERVICE_REBOOT, + SERVICE_RESUME_INTEGRATION, + SERVICE_SUSPEND_INTEGRATION, UPDATE_OPTIONS_SIGNAL, UPDATE_SIGNAL, ) @@ -137,6 +140,7 @@ class Router: ) unload_handlers: List[CALLBACK_TYPE] = attr.ib(init=False, factory=list) client: Client + suspended = attr.ib(init=False, default=False) def __attrs_post_init__(self): """Set up internal state on init.""" @@ -191,6 +195,10 @@ def _get_data(self, key: str, func: Callable[[None], Any]) -> None: def update(self) -> None: """Update router data.""" + if self.suspended: + _LOGGER.debug("Integration suspended, not updating data") + return + self._get_data(KEY_DEVICE_INFORMATION, self.client.device.information) if self.data.get(KEY_DEVICE_INFORMATION): # Full information includes everything in basic @@ -210,15 +218,8 @@ def update(self) -> None: self.signal_update() - def cleanup(self, *_) -> None: - """Clean up resources.""" - - self.subscriptions.clear() - - for handler in self.unload_handlers: - handler() - self.unload_handlers.clear() - + def logout(self) -> None: + """Log out router session.""" if not isinstance(self.connection, AuthorizedConnection): return try: @@ -230,6 +231,17 @@ def cleanup(self, *_) -> None: except Exception: # pylint: disable=broad-except _LOGGER.warning("Logout error", exc_info=True) + def cleanup(self, *_) -> None: + """Clean up resources.""" + + self.subscriptions.clear() + + for handler in self.unload_handlers: + handler() + self.unload_handlers.clear() + + self.logout() + @attr.s class HuaweiLteData: @@ -441,15 +453,29 @@ def service_handler(service) -> None: return if service.service == SERVICE_CLEAR_TRAFFIC_STATISTICS: + if router.suspended: + _LOGGER.debug("%s: ignored, integration suspended", service.service) + return result = router.client.monitoring.set_clear_traffic() _LOGGER.debug("%s: %s", service.service, result) elif service.service == SERVICE_REBOOT: + if router.suspended: + _LOGGER.debug("%s: ignored, integration suspended", service.service) + return result = router.client.device.reboot() _LOGGER.debug("%s: %s", service.service, result) + elif service.service == SERVICE_RESUME_INTEGRATION: + # Login will be handled automatically on demand + router.suspended = False + _LOGGER.debug("%s: %s", service.service, "done") + elif service.service == SERVICE_SUSPEND_INTEGRATION: + router.logout() + router.suspended = True + _LOGGER.debug("%s: %s", service.service, "done") else: _LOGGER.error("%s: unsupported service", service.service) - for service in (SERVICE_CLEAR_TRAFFIC_STATISTICS, SERVICE_REBOOT): + for service in ADMIN_SERVICES: hass.helpers.service.async_register_admin_service( DOMAIN, service, service_handler, schema=SERVICE_SCHEMA, ) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 164d833f03aeff..c6837fce06c656 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -15,6 +15,15 @@ SERVICE_CLEAR_TRAFFIC_STATISTICS = "clear_traffic_statistics" SERVICE_REBOOT = "reboot" +SERVICE_RESUME_INTEGRATION = "resume_integration" +SERVICE_SUSPEND_INTEGRATION = "suspend_integration" + +ADMIN_SERVICES = { + SERVICE_CLEAR_TRAFFIC_STATISTICS, + SERVICE_REBOOT, + SERVICE_RESUME_INTEGRATION, + SERVICE_SUSPEND_INTEGRATION, +} KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 494d0ec720ed6a..5619a5d702c41b 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -44,6 +44,12 @@ def send_message(self, message: str = "", **kwargs: Any) -> None: if not targets or not message: return + if self.router.suspended: + _LOGGER.debug( + "Integration suspended, not sending notification to %s", targets + ) + return + try: resp = self.router.client.sms.send_sms( phone_numbers=targets, message=message diff --git a/homeassistant/components/huawei_lte/services.yaml b/homeassistant/components/huawei_lte/services.yaml index 428745ee33e2ec..bcb9be33299cc4 100644 --- a/homeassistant/components/huawei_lte/services.yaml +++ b/homeassistant/components/huawei_lte/services.yaml @@ -11,3 +11,20 @@ reboot: url: description: URL of router to reboot; optional when only one is configured. example: http://192.168.100.1/ + +resume_integration: + description: Resume suspended integration. + fields: + url: + description: URL of router to resume integration for; optional when only one is configured. + example: http://192.168.100.1/ + +suspend_integration: + description: > + Suspend integration. Suspending logs the integration out from the router, and stops accessing it. + Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. + Invoke the resume_integration service to resume. + fields: + url: + description: URL of router to resume integration for; optional when only one is configured. + example: http://192.168.100.1/ From 169c4089ff7cdfebc49c5eb27a8766edaf63bd38 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 26 Dec 2019 00:32:16 +0000 Subject: [PATCH 2555/3953] [ci skip] Translation update --- .../geonetnz_quakes/.translations/hu.json | 2 +- .../huawei_lte/.translations/es.json | 1 + .../huawei_lte/.translations/ru.json | 1 + .../components/notion/.translations/hu.json | 2 +- .../components/tesla/.translations/es.json | 30 +++++++++++++++++++ .../components/tesla/.translations/hu.json | 21 +++++++++++++ 6 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/tesla/.translations/es.json create mode 100644 homeassistant/components/tesla/.translations/hu.json diff --git a/homeassistant/components/geonetnz_quakes/.translations/hu.json b/homeassistant/components/geonetnz_quakes/.translations/hu.json index 42de5a1314239b..4a163d24b75925 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/hu.json +++ b/homeassistant/components/geonetnz_quakes/.translations/hu.json @@ -5,7 +5,7 @@ "data": { "radius": "Sug\u00e1r" }, - "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." + "title": "T\u00f6ltsd ki a sz\u0171r\u0151 adatait." } } } diff --git a/homeassistant/components/huawei_lte/.translations/es.json b/homeassistant/components/huawei_lte/.translations/es.json index 92ccf8fc0480b5..c35d1eacf23397 100644 --- a/homeassistant/components/huawei_lte/.translations/es.json +++ b/homeassistant/components/huawei_lte/.translations/es.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nombre del servicio de notificaci\u00f3n", "recipient": "Destinatarios de notificaciones por SMS", "track_new_devices": "Rastrea nuevos dispositivos" } diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json index ec28325dcddfdc..6f478987f508fa 100644 --- a/homeassistant/components/huawei_lte/.translations/ru.json +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 SMS-\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" } diff --git a/homeassistant/components/notion/.translations/hu.json b/homeassistant/components/notion/.translations/hu.json index 2f7664cf74eec1..79878858ddcd79 100644 --- a/homeassistant/components/notion/.translations/hu.json +++ b/homeassistant/components/notion/.translations/hu.json @@ -11,7 +11,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v/Email C\u00edm" }, - "title": "T\u00f6ltse ki adatait" + "title": "T\u00f6ltsd ki az adataid" } } } diff --git a/homeassistant/components/tesla/.translations/es.json b/homeassistant/components/tesla/.translations/es.json new file mode 100644 index 00000000000000..64bab24ee3f774 --- /dev/null +++ b/homeassistant/components/tesla/.translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error de conexi\u00f3n; compruebe la red y vuelva a intentarlo", + "identifier_exists": "Correo electr\u00f3nico ya registrado", + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unknown_error": "Error desconocido, por favor reporte la informaci\u00f3n de registro" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "description": "Por favor, introduzca su informaci\u00f3n.", + "title": "Tesla - Configuraci\u00f3n" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Segundos entre escaneos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/hu.json b/homeassistant/components/tesla/.translations/hu.json new file mode 100644 index 00000000000000..7a9a3deff4953d --- /dev/null +++ b/homeassistant/components/tesla/.translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "connection_error": "Hiba a csatlakoz\u00e1skor; ellen\u0151rizd a h\u00e1l\u00f3zatot \u00e9s pr\u00f3b\u00e1ld \u00fajra", + "identifier_exists": "Az e-mail c\u00edm m\u00e1r regisztr\u00e1lva van", + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok", + "unknown_error": "Ismeretlen hiba, k\u00e9rlek jelentsd a napl\u00f3f\u00e1jlban l\u00e9v\u0151 adatokat" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Email c\u00edm" + }, + "description": "K\u00e9rlek, add meg az adataidat.", + "title": "Tesla - Konfigur\u00e1ci\u00f3" + } + }, + "title": "Tesla" + } +} \ No newline at end of file From e58ef36adc1b1ac9faa21856d43e8557a13153c6 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 26 Dec 2019 04:25:49 -0800 Subject: [PATCH 2556/3953] Bump teslajsonpy to 0.2.1 (#30217) Closes #29922 --- homeassistant/components/tesla/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 4a869ab0a4138a..09a579373d62f6 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -3,7 +3,7 @@ "name": "Tesla", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.2.0"], + "requirements": ["teslajsonpy==0.2.1"], "dependencies": [], "codeowners": ["@zabuldon", "@alandtse"] } diff --git a/requirements_all.txt b/requirements_all.txt index f69a9b8c080d51..ca7748f2583ade 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1938,7 +1938,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.2.0 +teslajsonpy==0.2.1 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 422af61d9dfe98..0f6586ed2bc47b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -599,7 +599,7 @@ sunwatcher==0.2.1 tellduslive==0.10.10 # homeassistant.components.tesla -teslajsonpy==0.2.0 +teslajsonpy==0.2.1 # homeassistant.components.toon toonapilib==3.2.4 From b2753b75785a090e85f4d5fd9f575511834299cf Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Thu, 26 Dec 2019 14:27:59 +0200 Subject: [PATCH 2557/3953] bump tuyaha 0.0.5 (#30213) --- 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 cf16d587e87709..6479a26694db13 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/integrations/tuya", "requirements": [ - "tuyaha==0.0.4" + "tuyaha==0.0.5" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index ca7748f2583ade..780bf419366a8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1965,7 +1965,7 @@ tp-connected==0.0.4 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.4 +tuyaha==0.0.5 # homeassistant.components.twentemilieu twentemilieu==0.1.0 From d6744fbc4eb5b3a8d7dd8f4a3cc4348f576d6878 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Thu, 26 Dec 2019 13:06:57 -0500 Subject: [PATCH 2558/3953] Fix handling of symlinked device descriptors in keyboard_remote and move remaining sync io to executor thread pool (#30206) * fix handling of symlinked device decriptors * make check for symlinked paths more efficient * make variable names pylint compliant * move sync io during setup and device connect/disconnect to executor thread pool * move remaining sync io during setup to executor thread pool * remove unnecessary lambda functions --- .../components/keyboard_remote/__init__.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index 24889a3f82095a..310bd0189bda39 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -2,6 +2,7 @@ # pylint: disable=import-error import asyncio import logging +import os import aionotify from evdev import InputDevice, categorize, ecodes, list_devices @@ -119,9 +120,11 @@ async def async_start_monitoring(self, event): # add initial devices (do this AFTER starting watcher in order to # avoid race conditions leading to missing device connections) initial_start_monitoring = set() - descriptors = list_devices(DEVINPUT) + descriptors = await self.hass.async_add_executor_job(list_devices, DEVINPUT) for descriptor in descriptors: - dev, handler = self.get_device_handler(descriptor) + dev, handler = await self.hass.async_add_executor_job( + self.get_device_handler, descriptor + ) if handler is None: continue @@ -165,6 +168,15 @@ def get_device_handler(self, descriptor): handler = self.handlers_by_descriptor[descriptor] elif dev.name in self.handlers_by_name: handler = self.handlers_by_name[dev.name] + else: + # check for symlinked paths matching descriptor + for test_descriptor, test_handler in self.handlers_by_descriptor.items(): + if test_handler.dev is not None: + fullpath = test_handler.dev.path + else: + fullpath = os.path.realpath(test_descriptor) + if fullpath == descriptor: + handler = test_handler return (dev, handler) @@ -186,7 +198,9 @@ async def async_monitor_devices(self): (event.flags & aionotify.Flags.CREATE) or (event.flags & aionotify.Flags.ATTRIB) ) and not descriptor_active: - dev, handler = self.get_device_handler(descriptor) + dev, handler = await self.hass.async_add_executor_job( + self.get_device_handler, descriptor + ) if handler is None: continue self.active_handlers_by_descriptor[descriptor] = handler @@ -242,7 +256,7 @@ async def async_stop_monitoring(self): """Stop event monitoring task and issue event.""" if self.monitor_task is not None: try: - self.dev.ungrab() + await self.hass.async_add_executor_job(self.dev.ungrab) except OSError: pass # monitoring of the device form the event loop and closing of the @@ -272,7 +286,7 @@ async def async_monitor_input(self, dev): try: _LOGGER.debug("Start device monitoring") - dev.grab() + await self.hass.async_add_executor_job(dev.grab) async for event in dev.async_read_loop(): if event.type is ecodes.EV_KEY: if event.value in self.key_values: From 05a0922dc092b7ab0eacb41c992f1ad2258c24da Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 26 Dec 2019 17:08:14 -0500 Subject: [PATCH 2559/3953] Bump ZHA dependencies (#30228) * bump dependencies * requirement files * bump quirks to 0.0.30 --- homeassistant/components/zha/manifest.json | 8 ++++---- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 8781625d326522..3beca6fd3c5cf1 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.11.0", - "zha-quirks==0.0.28", + "bellows-homeassistant==0.12.0", + "zha-quirks==0.0.30", "zigpy-deconz==0.7.0", - "zigpy-homeassistant==0.11.0", - "zigpy-xbee-homeassistant==0.7.0", + "zigpy-homeassistant==0.12.0", + "zigpy-xbee-homeassistant==0.8.0", "zigpy-zigate==0.5.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 780bf419366a8c..c99b8af492eb5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -293,7 +293,7 @@ beautifulsoup4==4.8.1 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.11.0 +bellows-homeassistant==0.12.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.6.2 @@ -2094,7 +2094,7 @@ zengge==0.2 zeroconf==0.24.3 # homeassistant.components.zha -zha-quirks==0.0.28 +zha-quirks==0.0.30 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2106,10 +2106,10 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.11.0 +zigpy-homeassistant==0.12.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.7.0 +zigpy-xbee-homeassistant==0.8.0 # homeassistant.components.zha zigpy-zigate==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f6586ed2bc47b..9c2139244e459f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.11.0 +bellows-homeassistant==0.12.0 # homeassistant.components.bom bomradarloop==0.1.3 @@ -659,16 +659,16 @@ yahooweather==0.10 zeroconf==0.24.3 # homeassistant.components.zha -zha-quirks==0.0.28 +zha-quirks==0.0.30 # homeassistant.components.zha zigpy-deconz==0.7.0 # homeassistant.components.zha -zigpy-homeassistant==0.11.0 +zigpy-homeassistant==0.12.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.7.0 +zigpy-xbee-homeassistant==0.8.0 # homeassistant.components.zha zigpy-zigate==0.5.0 From 39d38923b7d61eb1e6d8860f511305c8785a0af2 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 27 Dec 2019 00:32:13 +0000 Subject: [PATCH 2560/3953] [ci skip] Translation update --- .../huawei_lte/.translations/fr.json | 1 + .../huawei_lte/.translations/zh-Hant.json | 1 + .../tesla/.translations/zh-Hant.json | 30 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 homeassistant/components/tesla/.translations/zh-Hant.json diff --git a/homeassistant/components/huawei_lte/.translations/fr.json b/homeassistant/components/huawei_lte/.translations/fr.json index 34db4e93bc4ac1..1b2be78109dee2 100644 --- a/homeassistant/components/huawei_lte/.translations/fr.json +++ b/homeassistant/components/huawei_lte/.translations/fr.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nom du service de notification", "recipient": "Destinataires des notifications SMS", "track_new_devices": "Suivre les nouveaux appareils" } diff --git a/homeassistant/components/huawei_lte/.translations/zh-Hant.json b/homeassistant/components/huawei_lte/.translations/zh-Hant.json index 37f1111b77f6bf..7371669e15700e 100644 --- a/homeassistant/components/huawei_lte/.translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/.translations/zh-Hant.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "\u901a\u77e5\u670d\u52d9\u540d\u7a31", "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", "track_new_devices": "\u8ffd\u8e64\u65b0\u8a2d\u5099" } diff --git a/homeassistant/components/tesla/.translations/zh-Hant.json b/homeassistant/components/tesla/.translations/zh-Hant.json new file mode 100644 index 00000000000000..776a80da7fb619 --- /dev/null +++ b/homeassistant/components/tesla/.translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "\u9023\u7dda\u932f\u8aa4\uff1b\u8acb\u6aa2\u5bdf\u7db2\u8def\u5f8c\u518d\u8a66\u4e00\u6b21", + "identifier_exists": "\u90f5\u4ef6\u5df2\u8a3b\u518a", + "invalid_credentials": "\u6191\u8b49\u7121\u6548", + "unknown_error": "\u672a\u77e5\u932f\u8aa4\uff0c\u8acb\u56de\u5831\u7d00\u9304" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + }, + "description": "\u8acb\u8f38\u5165\u8cc7\u8a0a\u3002", + "title": "Tesla - \u8a2d\u5b9a" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u6383\u63cf\u9593\u9694\u79d2\u6578" + } + } + } + } +} \ No newline at end of file From 9159da6583f1405f6218eb21f41d562f7f3bc106 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 27 Dec 2019 20:35:25 +0100 Subject: [PATCH 2561/3953] Bump shodan to 1.21.1 (#30234) --- 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 0ff3e44ece5973..007f6ef1d99d4d 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,7 +2,7 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": ["shodan==1.21.0"], + "requirements": ["shodan==1.21.1"], "dependencies": [], "codeowners": ["@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index c99b8af492eb5c..a9638a1d767e75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1800,7 +1800,7 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.21.0 +shodan==1.21.1 # homeassistant.components.simplepush simplepush==1.1.4 From 9d6f3654adeaf05d9b0ee547528167e1685d9dae Mon Sep 17 00:00:00 2001 From: Kerwood Date: Fri, 27 Dec 2019 21:47:45 +0100 Subject: [PATCH 2562/3953] DECONZ - Added support for Aqara single switch WXKG03LM (#30240) --- homeassistant/components/deconz/device_trigger.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index d057de23d02b20..9c8a41453aaf60 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -216,6 +216,13 @@ (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): 3002, } +AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL = "lumi.remote.b186acn01" +AQARA_SINGLE_WALL_SWITCH_WXKG03LM = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, +} + AQARA_MINI_SWITCH_MODEL = "lumi.remote.b1acn01" AQARA_MINI_SWITCH = { (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, @@ -266,6 +273,7 @@ AQARA_CUBE_MODEL_ALT1: AQARA_CUBE, AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_WXKG02LM, + AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL: AQARA_SINGLE_WALL_SWITCH_WXKG03LM, AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH, AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, From 2e079029999ac8fb1b25376aafdf4838e4929c71 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 28 Dec 2019 00:32:22 +0000 Subject: [PATCH 2563/3953] [ci skip] Translation update --- .../huawei_lte/.translations/ca.json | 1 + .../huawei_lte/.translations/lb.json | 1 + .../components/tesla/.translations/ca.json | 30 +++++++++++++++++++ .../components/tesla/.translations/lb.json | 30 +++++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 homeassistant/components/tesla/.translations/ca.json create mode 100644 homeassistant/components/tesla/.translations/lb.json diff --git a/homeassistant/components/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json index b213da018d210c..594c2e3b16de27 100644 --- a/homeassistant/components/huawei_lte/.translations/ca.json +++ b/homeassistant/components/huawei_lte/.translations/ca.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nom del servei de notificacions", "recipient": "Destinataris de notificacions SMS", "track_new_devices": "Segueix dispositius nous" } diff --git a/homeassistant/components/huawei_lte/.translations/lb.json b/homeassistant/components/huawei_lte/.translations/lb.json index 3c8f0464a55c8e..56d383edba32b4 100644 --- a/homeassistant/components/huawei_lte/.translations/lb.json +++ b/homeassistant/components/huawei_lte/.translations/lb.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Numm vum Notifikatioun's Service", "recipient": "Empf\u00e4nger vun SMS Notifikatioune", "track_new_devices": "Nei Apparater verfollegen" } diff --git a/homeassistant/components/tesla/.translations/ca.json b/homeassistant/components/tesla/.translations/ca.json new file mode 100644 index 00000000000000..cb4840dea7af34 --- /dev/null +++ b/homeassistant/components/tesla/.translations/ca.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error de connexi\u00f3; comprova la xarxa i torna-ho a intentar", + "identifier_exists": "Correu electr\u00f2nic ja registrat", + "invalid_credentials": "Credencials inv\u00e0lides", + "unknown_error": "Error desconegut, si us plau, envia la informaci\u00f3 del registre" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "description": "Introdueix la teva informaci\u00f3.", + "title": "Configuraci\u00f3 de Tesla" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Segons entre escanejos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/lb.json b/homeassistant/components/tesla/.translations/lb.json new file mode 100644 index 00000000000000..fa63c5a289ade4 --- /dev/null +++ b/homeassistant/components/tesla/.translations/lb.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Feeler beim verbannen, Iwwerpr\u00e9ift Netzwierk a prob\u00e9iert nach emol", + "identifier_exists": "E-Mail ass scho registr\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune", + "unknown_error": "Onbekannte Feeler, mellt w.e.g. Logbuch Info" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail Adress" + }, + "description": "F\u00ebllt \u00e4r Informatiounen aus.", + "title": "Tesla - Konfiguratioun" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekonnen t\u00ebscht Scannen" + } + } + } + } +} \ No newline at end of file From cf7b70dd8c39317a700fdeb96826a503cedc2a01 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sat, 28 Dec 2019 10:09:42 +0100 Subject: [PATCH 2564/3953] Bump python-qbittorrent to 0.4.1 (#30239) --- homeassistant/components/qbittorrent/manifest.json | 4 +--- homeassistant/components/qbittorrent/sensor.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index c41d4ba46d362f..2125e8cfe7b38d 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -2,9 +2,7 @@ "domain": "qbittorrent", "name": "Qbittorrent", "documentation": "https://www.home-assistant.io/integrations/qbittorrent", - "requirements": [ - "python-qbittorrent==0.3.1" - ], + "requirements": ["python-qbittorrent==0.4.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 0299277059b484..9544d74b1cde5e 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -107,7 +107,7 @@ def unit_of_measurement(self): def update(self): """Get the latest data from qBittorrent and updates the state.""" try: - data = self.client.sync() + data = self.client.sync_main_data() self._available = True except RequestException: _LOGGER.error("Connection lost") diff --git a/requirements_all.txt b/requirements_all.txt index a9638a1d767e75..9e12355533756a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1601,7 +1601,7 @@ python-nmap==0.6.1 python-pushover==0.4 # homeassistant.components.qbittorrent -python-qbittorrent==0.3.1 +python-qbittorrent==0.4.1 # homeassistant.components.ripple python-ripple-api==0.0.3 From 59fee12b450bf01d5398848c2fe7595cb579c8d1 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sat, 28 Dec 2019 04:15:35 -0500 Subject: [PATCH 2565/3953] Bump ring to 0.2.8 to fix Oauth issues (#30245) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 5c23822fef9e8e..6fc57244deba52 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.2.5"], + "requirements": ["ring_doorbell==0.2.8"], "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 9e12355533756a..fa2c24f9a8dfd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1743,7 +1743,7 @@ rfk101py==0.0.1 rflink==0.0.46 # homeassistant.components.ring -ring_doorbell==0.2.5 +ring_doorbell==0.2.8 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c2139244e459f..f2625b96c9cfe8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ restrictedpython==5.0 rflink==0.0.46 # homeassistant.components.ring -ring_doorbell==0.2.5 +ring_doorbell==0.2.8 # homeassistant.components.yamaha rxv==0.6.0 From e4cda9ae0bcbe68449ff7cb600fe2daf80315206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 28 Dec 2019 13:55:45 +0200 Subject: [PATCH 2566/3953] Fix Huawei LTE error message on service call without URL and routers (#30250) --- homeassistant/components/huawei_lte/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index deb92de218c54b..97a57405ae07be 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -439,6 +439,9 @@ def service_handler(service) -> None: routers = hass.data[DOMAIN].routers if url: router = routers.get(url) + elif not routers: + _LOGGER.error("%s: no routers configured", service.service) + return elif len(routers) == 1: router = next(iter(routers.values())) else: From 134dc45b77519b3266b8a73e8562357bbd57e24d Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 28 Dec 2019 13:46:04 +0100 Subject: [PATCH 2567/3953] Bump dependency for HomematicIp cloud (#30237) * Bump dependency for HomematicIp cloud * Update test_data --- .../homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/fixtures/homematicip_cloud.json | 201 ++++++++++++++++-- 4 files changed, 191 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 4feef19c8dac8a..9f965bfdb6d870 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/integrations/homematicip_cloud", "requirements": [ - "homematicip==0.10.13" + "homematicip==0.10.14" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index fa2c24f9a8dfd2..ee2d1b8fc6535f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -675,7 +675,7 @@ homeassistant-pyozw==0.1.7 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.13 +homematicip==0.10.14 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2625b96c9cfe8..f8ffbead228c15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -240,7 +240,7 @@ homeassistant-pyozw==0.1.7 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.13 +homematicip==0.10.14 # homeassistant.components.google # homeassistant.components.remember_the_milk diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 8cec2462f32dda..01667f353d3c16 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -14,6 +14,68 @@ } }, "devices": { + "3014F7110000ABCDABCD0033": { + "availableFirmwareVersion": "1.0.6", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "badBatteryHealth": true, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000ABCDABCD0033", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_RECHARGEABLE_WITH_SABOTAGE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -51, + "rssiPeerValue": null, + "sabotage": false, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000ABCDABCD0033", + "functionalChannelType": "ALARM_SIREN_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000ABCDABCD0033", + "label": "Alarmsirene \u2013 au\u00dfen", + "lastStatusUpdate": 1573078567665, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 385, + "modelType": "HmIP-ASIR-O", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000ABCDABCD0033", + "type": "ALARM_SIREN_OUTDOOR", + "updateState": "UP_TO_DATE" + }, "3014F7110000000000000031": { "availableFirmwareVersion": "1.2.1", "firmwareVersion": "1.2.1", @@ -685,14 +747,14 @@ "firmwareVersionInteger": 65542, "functionalChannels": { "0": { - "coProFaulty": false, - "coProRestartNeeded": false, - "coProUpdateFailure": false, + "coProFaulty": true, + "coProRestartNeeded": true, + "coProUpdateFailure": true, "configPending": false, "deviceId": "3014F7110000000000000064", "deviceOverheated": true, - "deviceOverloaded": false, - "deviceUndervoltage": false, + "deviceOverloaded": true, + "deviceUndervoltage": true, "dutyCycle": false, "functionalChannelType": "DEVICE_SABOTAGE", "groupIndex": 0, @@ -709,15 +771,15 @@ "rssiPeerValue": null, "sabotage": false, "supportedOptionalFeatures": { - "IFeatureDeviceCoProError": false, - "IFeatureDeviceCoProRestart": false, - "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceCoProError": true, + "IFeatureDeviceCoProRestart": true, + "IFeatureDeviceCoProUpdate": true, "IFeatureDeviceOverheated": true, - "IFeatureDeviceOverloaded": false, - "IFeatureDeviceTemperatureOutOfRange": false, - "IFeatureDeviceUndervoltage": false + "IFeatureDeviceOverloaded": true, + "IFeatureDeviceTemperatureOutOfRange": true, + "IFeatureDeviceUndervoltage": true }, - "temperatureOutOfRange": false, + "temperatureOutOfRange": true, "unreach": false }, "1": { @@ -4031,6 +4093,74 @@ "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB18", "type": "OPEN_COLLECTOR_8_MODULE", "updateState": "UP_TO_DATE" + }, + "3014F0000000000000FAF9B4": { + "availableFirmwareVersion": "1.0.0", + "firmwareVersion": "1.0.0", + "firmwareVersionInteger": 65536, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F0000000000000FAF9B4", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -52, + "rssiPeerValue": -54, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F0000000000000FAF9B4", + "doorState": "CLOSED", + "functionalChannelType": "DOOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "on": false, + "processing": false, + "ventilationPositionSupported": true + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F0000000000000FAF9B4", + "label": "Garage Door Module", + "lastStatusUpdate": 1574878451970, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 376, + "modelType": "HmIP-MOD-TM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F0000000000000FAF9B4", + "type": "TORMATIC_MODULE", + "updateState": "UP_TO_DATE" } }, "groups": { @@ -4406,7 +4536,11 @@ "lowBat": null, "metaGroupId": "00000000-0000-0000-0000-000000000017", "on": true, - "processing": null, + "primaryShadingLevel": 1.0, + "primaryShadingStateType": "POSITION_USED", + "processing": false, + "secondaryShadingLevel": null, + "secondaryShadingStateType": "NOT_EXISTENT", "shutterLevel": null, "slatsLevel": null, "type": "SWITCHING", @@ -5394,6 +5528,47 @@ "type": "HUMIDITY_WARNING_RULE_GROUP", "unreach": false, "ventilationRecommended": false + }, + "00000000-0000-0000-0000-000000000050": { + "bottomShutterLevel": 1.0, + "bottomSlatsLevel": 1.0, + "channels": [], + "dutyCycle": false, + "groupVisibility": "VISIBLE", + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000050", + "label": "Rollos", + "lastStatusUpdate": 1573078054795, + "lowBat": null, + "metaGroupId": null, + "primaryShadingLevel": 1.0, + "primaryShadingStateType": "POSITION_USED", + "processing": false, + "secondaryShadingLevel": null, + "secondaryShadingStateType": "NOT_EXISTENT", + "sensorSpecificParameters": {}, + "shutterLevel": 1.0, + "slatsLevel": null, + "topShutterLevel": 0.0, + "topSlatsLevel": 0.0, + "type": "EXTENDED_LINKED_SHUTTER", + "unreach": false + }, + "00000000-0000-0000-0000-000000000067": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000067", + "label": "HOT_WATER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "onTime": 900.0, + "profileId": "00000000-0000-0000-0000-000000000068", + "profileMode": null, + "type": "HOT_WATER", + "unreach": null } }, "home": { From 0323b246bd70b3624ff6874e7516907f1a2f78dc Mon Sep 17 00:00:00 2001 From: Felix Barbalet Date: Sun, 29 Dec 2019 02:19:18 +1100 Subject: [PATCH 2568/3953] Bump libpurecool to 0.6.0 (#30247) * bump libpurecool to 0.6.0 - fixes home-assistant/home-assistant#26367 * update manifest too --- homeassistant/components/dyson/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 92940c8c1e19fa..9b561d78f95bfb 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -3,7 +3,7 @@ "name": "Dyson", "documentation": "https://www.home-assistant.io/integrations/dyson", "requirements": [ - "libpurecool==0.5.0" + "libpurecool==0.6.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index ee2d1b8fc6535f..5fb21cd09556a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -756,7 +756,7 @@ konnected==0.1.5 lakeside==0.12 # homeassistant.components.dyson -libpurecool==0.5.0 +libpurecool==0.6.0 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8ffbead228c15..590775abeb2d7e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -265,7 +265,7 @@ keyring==20.0.0 keyrings.alt==3.4.0 # homeassistant.components.dyson -libpurecool==0.5.0 +libpurecool==0.6.0 # homeassistant.components.soundtouch libsoundtouch==0.7.2 From c5a280c064220ce5ef9135700dc0fcbd384ec04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 28 Dec 2019 17:27:49 +0200 Subject: [PATCH 2569/3953] Huawei LTE: Fix YAML options overriding ones set from GUI (#30249) Closes https://github.com/home-assistant/home-assistant/issues/30221 --- homeassistant/components/huawei_lte/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 64803d4ad450ed..0dcdb6636c690d 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -248,7 +248,9 @@ def __init__(self, config_entry: config_entries.ConfigEntry): async def async_step_init(self, user_input=None): """Handle options flow.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + # Preserve existing options, for example *_from_yaml markers + data = {**self.config_entry.options, **user_input} + return self.async_create_entry(title="", data=data) data_schema = vol.Schema( { From 658ec309aa65241b564c3983f61b339151d56b45 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 28 Dec 2019 16:29:14 +0100 Subject: [PATCH 2570/3953] Add HmIP-MOD_TM to HomematicIP Cloud (#30255) --- .../components/homematicip_cloud/cover.py | 41 ++++++++++++++++- .../homematicip_cloud/test_cover.py | 46 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index ef8cbacfde2a9c..e3efe9a950870b 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -2,7 +2,12 @@ import logging from typing import Optional -from homematicip.aio.device import AsyncFullFlushBlind, AsyncFullFlushShutter +from homematicip.aio.device import ( + AsyncFullFlushBlind, + AsyncFullFlushShutter, + AsyncGarageDoorModuleTormatic, +) +from homematicip.base.enums import DoorCommand, DoorState from homeassistant.components.cover import ( ATTR_POSITION, @@ -40,6 +45,8 @@ async def async_setup_entry( entities.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): entities.append(HomematicipCoverShutter(hap, device)) + elif isinstance(device, AsyncGarageDoorModuleTormatic): + entities.append(HomematicipGarageDoorModuleTormatic(hap, device)) if entities: async_add_entities(entities) @@ -106,3 +113,35 @@ async def async_close_cover_tilt(self, **kwargs) -> None: async def async_stop_cover_tilt(self, **kwargs) -> None: """Stop the device if in motion.""" await self._device.set_shutter_stop() + + +class HomematicipGarageDoorModuleTormatic(HomematicipGenericDevice, CoverDevice): + """Representation of a HomematicIP Garage Door Module for Tormatic.""" + + @property + def current_cover_position(self) -> int: + """Return current position of cover.""" + door_state_to_position = { + DoorState.CLOSED: 0, + DoorState.OPEN: 100, + DoorState.VENTILATION_POSITION: 10, + DoorState.POSITION_UNKNOWN: None, + } + return door_state_to_position.get(self._device.doorState) + + @property + def is_closed(self) -> Optional[bool]: + """Return if the cover is closed.""" + return self._device.doorState == DoorState.CLOSED + + async def async_open_cover(self, **kwargs) -> None: + """Open the cover.""" + await self._device.send_door_command(DoorCommand.OPEN) + + async def async_close_cover(self, **kwargs) -> None: + """Close the cover.""" + await self._device.send_door_command(DoorCommand.CLOSE) + + async def async_stop_cover(self, **kwargs) -> None: + """Stop the cover.""" + await self._device.send_door_command(DoorCommand.STOP) diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 22922303f9e0d2..728d60d5501628 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -1,4 +1,6 @@ """Tests for HomematicIP Cloud cover.""" +from homematicip.base.enums import DoorCommand, DoorState + from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, @@ -153,3 +155,47 @@ async def test_hmip_cover_slats(hass, default_mock_hap): await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN + + +async def test_hmip_garage_door_tormatic(hass, default_mock_hap): + """Test HomematicipCoverShutte.""" + entity_id = "cover.garage_door_module" + entity_name = "Garage Door Module" + device_model = "HmIP-MOD-TM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "closed" + assert ha_state.attributes["current_position"] == 0 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "send_door_command" + assert hmip_device.mock_calls[-1][1] == (DoorCommand.OPEN,) + await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.OPEN) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "send_door_command" + assert hmip_device.mock_calls[-1][1] == (DoorCommand.CLOSE,) + await async_manipulate_test_data(hass, hmip_device, "doorState", DoorState.CLOSED) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 5 + assert hmip_device.mock_calls[-1][0] == "send_door_command" + assert hmip_device.mock_calls[-1][1] == (DoorCommand.STOP,) From 4e50a402c736f1102436e612b679eb5a71ac8ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 28 Dec 2019 17:31:42 +0200 Subject: [PATCH 2571/3953] Note Huawei LTE notify service change requires restart (#30223) Refs https://github.com/home-assistant/home-assistant/issues/30222 --- homeassistant/components/huawei_lte/.translations/en.json | 2 +- homeassistant/components/huawei_lte/strings.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 5cee60fb727778..c5f2b4a2a02e66 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -33,7 +33,7 @@ "step": { "init": { "data": { - "name": "Notification service name", + "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", "track_new_devices": "Track new devices" } diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 0d586e0d0ad2ac..c5f2b4a2a02e66 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -7,13 +7,13 @@ }, "error": { "connection_failed": "Connection failed", + "connection_timeout": "Connection timeout", "incorrect_password": "Incorrect password", "incorrect_username": "Incorrect username", "incorrect_username_or_password": "Incorrect username or password", "invalid_url": "Invalid URL", "login_attempts_exceeded": "Maximum login attempts exceeded, please try again later", "response_error": "Unknown error from device", - "connection_timeout": "Connection timeout", "unknown_connection_error": "Unknown error connecting to device" }, "step": { @@ -33,7 +33,7 @@ "step": { "init": { "data": { - "name": "Notification service name", + "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", "track_new_devices": "Track new devices" } From e1e8d6a562ed152a08ecb926b111a20d0be513df Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sat, 28 Dec 2019 21:16:52 +0100 Subject: [PATCH 2572/3953] Bump python-velbus library to 2.0.32 to fix problems with the glaspanels (#30257) --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 71a2ac2b9933aa..007ca4212765f6 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -3,7 +3,7 @@ "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", "requirements": [ - "python-velbus==2.0.30" + "python-velbus==2.0.32" ], "config_flow": true, "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 5fb21cd09556a8..ca438455bbacda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1628,7 +1628,7 @@ python-telnet-vlc==1.0.4 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.30 +python-velbus==2.0.32 # homeassistant.components.vlc python-vlc==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 590775abeb2d7e..5ee5e0178646ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -523,7 +523,7 @@ python-miio==0.4.8 python-nest==4.1.0 # homeassistant.components.velbus -python-velbus==2.0.30 +python-velbus==2.0.32 # homeassistant.components.awair python_awair==0.0.4 From 5a9e543087da3cab2afad754e1e603cbb448851e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Dec 2019 21:20:18 +0100 Subject: [PATCH 2573/3953] Whitelist Android/iOS auth callbacks (#30082) * Whitelist Android/iOS * Add iOS alternate flavor URLs * Update indieauth.py Co-authored-by: Robbie Trencheny --- homeassistant/components/auth/indieauth.py | 8 ++++++++ tests/components/auth/test_indieauth.py | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index c845f230bf3b56..5915a4ec3013d2 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -30,6 +30,14 @@ async def verify_redirect_uri(hass, client_id, redirect_uri): if is_valid: return True + # Whitelist the iOS and Android callbacks so that people can link apps + # without being connected to the internet. + if redirect_uri == "homeassistant://auth-callback" and client_id in ( + "https://home-assistant.io/android", + "https://home-assistant.io/iOS", + ): + return True + # IndieAuth 4.2.2 allows for redirect_uri to be on different domain # but needs to be specified in link tag when fetching `client_id`. redirect_uris = await fetch_redirect_uris(hass, client_id) diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index 8cfb573939ebc0..ce8edae1466517 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -166,3 +166,24 @@ async def test_find_link_tag_max_size(hass, mock_session): redirect_uris = await indieauth.fetch_redirect_uris(hass, "http://127.0.0.1:8000") assert redirect_uris == ["http://127.0.0.1:8000/wine"] + + +@pytest.mark.parametrize( + "client_id", ["https://home-assistant.io/android", "https://home-assistant.io/iOS"] +) +async def test_verify_redirect_uri_android_ios(client_id): + """Test that we verify redirect uri correctly for Android/iOS.""" + with patch.object( + indieauth, "fetch_redirect_uris", side_effect=lambda *_: mock_coro([]) + ): + assert await indieauth.verify_redirect_uri( + None, client_id, "homeassistant://auth-callback" + ) + + assert not await indieauth.verify_redirect_uri( + None, client_id, "homeassistant://something-else" + ) + + assert not await indieauth.verify_redirect_uri( + None, "https://incorrect.com", "homeassistant://auth-callback" + ) From fdd090bae07db357eb3e47fbd976685d102f4a18 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sat, 28 Dec 2019 04:15:35 -0500 Subject: [PATCH 2574/3953] Bump ring to 0.2.8 to fix Oauth issues (#30245) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 5c23822fef9e8e..6fc57244deba52 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,7 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.2.5"], + "requirements": ["ring_doorbell==0.2.8"], "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 9ee9b9a740a2c3..01492591988960 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1732,7 +1732,7 @@ rfk101py==0.0.1 rflink==0.0.46 # homeassistant.components.ring -ring_doorbell==0.2.5 +ring_doorbell==0.2.8 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b71d8092be11e..79691bb3cf274a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -544,7 +544,7 @@ restrictedpython==5.0 rflink==0.0.46 # homeassistant.components.ring -ring_doorbell==0.2.5 +ring_doorbell==0.2.8 # homeassistant.components.yamaha rxv==0.6.0 From 08af98965866ac92abfefff63333c9ccef560cc0 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Sat, 28 Dec 2019 12:25:37 -0800 Subject: [PATCH 2575/3953] Fixing timezone issue which caused wrong selection of data to be used. (#30011) --- homeassistant/components/withings/common.py | 4 +-- tests/components/withings/test_common.py | 35 +++++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 4c3772d77e707d..9cba055bac4cda 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -259,8 +259,8 @@ def function(): async def update_sleep(self) -> SleepGetResponse: """Update the sleep data.""" - end_date = int(time.time()) - start_date = end_date - (6 * 60 * 60) + end_date = dt.now() + start_date = end_date - datetime.timedelta(hours=2) def function(): return self._api.sleep_get(startdate=start_date, enddate=end_date) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 9328526d6ef1ec..37fcb4ce7f5f5d 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,4 +1,6 @@ """Tests for the Withings component.""" +from datetime import timedelta + from asynctest import MagicMock import pytest from withings_api import WithingsApi @@ -8,15 +10,20 @@ NotAuthenticatedError, WithingsDataManager, ) +from homeassistant.config import async_process_ha_core_config +from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady +from homeassistant.util import dt @pytest.fixture(name="withings_api") def withings_api_fixture() -> WithingsApi: """Provide withings api.""" withings_api = WithingsApi.__new__(WithingsApi) - withings_api.get_measures = MagicMock() - withings_api.get_sleep = MagicMock() + withings_api.user_get_device = MagicMock() + withings_api.measure_get_meas = MagicMock() + withings_api.sleep_get = MagicMock() + withings_api.sleep_get_summary = MagicMock() return withings_api @@ -101,3 +108,27 @@ async def test_data_manager_call_throttle_disabled( assert result == "HELLO2" assert hello_func.call_count == 2 + + +async def test_data_manager_update_sleep_date_range( + hass: HomeAssistant, data_manager: WithingsDataManager, +) -> None: + """Test method.""" + await async_process_ha_core_config( + hass=hass, config={"time_zone": "America/Los_Angeles"} + ) + + update_start_time = dt.now() + await data_manager.update_sleep() + + call_args = data_manager.api.sleep_get.call_args_list[0][1] + startdate = call_args.get("startdate") + enddate = call_args.get("enddate") + + assert startdate.tzname() == "PST" + + assert enddate.tzname() == "PST" + assert startdate.tzname() == "PST" + assert update_start_time < enddate + assert enddate < update_start_time + timedelta(seconds=1) + assert enddate > startdate From ae42736235cbd2a29024be48c6fb9b0ecca2a84f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 28 Dec 2019 20:26:06 +0000 Subject: [PATCH 2576/3953] Bump version 0.103.5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c23ed1cdd03725..306c4ded067261 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 103 -PATCH_VERSION = "4" +PATCH_VERSION = "5" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 36585558a5b37b21924c749ae1c8470de7c1c04c Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 28 Dec 2019 15:41:55 -0500 Subject: [PATCH 2577/3953] Refactor ZHA channel logging (#30259) Add channel.id property -- id unique for this the device only. --- .../components/zha/core/channels/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 4013f05e0b67c3..5a337b2a5373f9 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -89,19 +89,19 @@ def __init__(self, cluster, device): self._generic_id = f"channel_0x{cluster.cluster_id:04x}" self._cluster = cluster self._zha_device = device - 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._id = f"{cluster.endpoint.endpoint_id}:0x{cluster.cluster_id:04x}" + self._unique_id = f"{str(device.ieee)}:{self._id}" self._report_config = CLUSTER_REPORT_CONFIGS.get( self._cluster.cluster_id, self.REPORT_CONFIG ) self._status = ChannelStatus.CREATED self._cluster.add_listener(self) + @property + def id(self) -> str: + """Return channel id unique for this device only.""" + return self._id + @property def generic_id(self): """Return the generic id for this channel.""" @@ -263,8 +263,8 @@ async def get_attribute_value(self, attribute, from_cache=True): def log(self, level, msg, *args): """Log a message.""" - msg = "[%s]: " + msg - args = (self._log_id,) + args + msg = "[%s:%s]: " + msg + args = (self.device.nwk, self._id,) + args _LOGGER.log(level, msg, *args) def __getattr__(self, name): From 5d8dda4f687dcf4a072f50c423debe0fb01a910c Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 29 Dec 2019 00:32:14 +0000 Subject: [PATCH 2578/3953] [ci skip] Translation update --- .../alarm_control_panel/.translations/da.json | 7 ++++ .../components/almond/.translations/da.json | 15 +++++++ .../components/auth/.translations/da.json | 2 +- .../components/climate/.translations/da.json | 17 ++++++++ .../components/cover/.translations/da.json | 8 ++++ .../components/deconz/.translations/da.json | 20 ++++++++- .../components/demo/.translations/da.json | 5 +++ .../dialogflow/.translations/da.json | 2 +- .../components/elgato/.translations/da.json | 27 ++++++++++++ .../components/fan/.translations/da.json | 16 +++++++ .../components/geofency/.translations/da.json | 2 +- .../geonetnz_volcano/.translations/da.json | 16 +++++++ .../gpslogger/.translations/da.json | 2 +- .../hisense_aehw4a1/.translations/da.json | 15 +++++++ .../huawei_lte/.translations/da.json | 20 +++++++++ .../components/icloud/.translations/da.json | 38 +++++++++++++++++ .../components/ifttt/.translations/da.json | 2 +- .../components/lock/.translations/da.json | 4 ++ .../lutron_caseta/.translations/da.json | 5 +++ .../components/mailgun/.translations/da.json | 2 +- .../components/plaato/.translations/da.json | 2 +- .../components/plex/.translations/da.json | 1 + .../components/soma/.translations/da.json | 4 +- .../components/somfy/.translations/da.json | 5 +++ .../components/starline/.translations/da.json | 42 +++++++++++++++++++ .../components/tesla/.translations/da.json | 30 +++++++++++++ .../components/traccar/.translations/da.json | 2 +- .../components/twilio/.translations/da.json | 2 +- .../components/vacuum/.translations/da.json | 16 +++++++ .../components/wled/.translations/da.json | 26 ++++++++++++ 30 files changed, 343 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/almond/.translations/da.json create mode 100644 homeassistant/components/climate/.translations/da.json create mode 100644 homeassistant/components/demo/.translations/da.json create mode 100644 homeassistant/components/elgato/.translations/da.json create mode 100644 homeassistant/components/fan/.translations/da.json create mode 100644 homeassistant/components/geonetnz_volcano/.translations/da.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/da.json create mode 100644 homeassistant/components/huawei_lte/.translations/da.json create mode 100644 homeassistant/components/icloud/.translations/da.json create mode 100644 homeassistant/components/lutron_caseta/.translations/da.json create mode 100644 homeassistant/components/starline/.translations/da.json create mode 100644 homeassistant/components/tesla/.translations/da.json create mode 100644 homeassistant/components/vacuum/.translations/da.json create mode 100644 homeassistant/components/wled/.translations/da.json diff --git a/homeassistant/components/alarm_control_panel/.translations/da.json b/homeassistant/components/alarm_control_panel/.translations/da.json index 74e02e10de4aa1..304fa342bf2f7a 100644 --- a/homeassistant/components/alarm_control_panel/.translations/da.json +++ b/homeassistant/components/alarm_control_panel/.translations/da.json @@ -2,6 +2,13 @@ "device_automation": { "action_type": { "trigger": "Udl\u00f8s {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} tilkoblet ude", + "armed_home": "{entity_name} tilkoblet hjemme", + "armed_night": "{entity_name} tilkoblet nat", + "disarmed": "{entity_name} frakoblet", + "triggered": "{entity_name} udl\u00f8st" } } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/da.json b/homeassistant/components/almond/.translations/da.json new file mode 100644 index 00000000000000..93158cee94f28f --- /dev/null +++ b/homeassistant/components/almond/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en Almond-konto.", + "cannot_connect": "Kan ikke oprette forbindelse til Almond-serveren.", + "missing_configuration": "Tjek venligst dokumentationen om, hvordan man indstiller Almond." + }, + "step": { + "pick_implementation": { + "title": "V\u00e6lg godkendelsesmetode" + } + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/da.json b/homeassistant/components/auth/.translations/da.json index f461f376d166c2..8bf710a1932966 100644 --- a/homeassistant/components/auth/.translations/da.json +++ b/homeassistant/components/auth/.translations/da.json @@ -2,7 +2,7 @@ "mfa_setup": { "notify": { "abort": { - "no_available_service": "Ingen underretningstjenester til r\u00e5dighed." + "no_available_service": "Ingen meddelelsestjenester tilg\u00e6ngelige." }, "error": { "invalid_code": "Ugyldig kode, pr\u00f8v venligst igen." diff --git a/homeassistant/components/climate/.translations/da.json b/homeassistant/components/climate/.translations/da.json new file mode 100644 index 00000000000000..78731dd1577b1b --- /dev/null +++ b/homeassistant/components/climate/.translations/da.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Skift af klimaanl\u00e6gstilstand p\u00e5 {entity_name}", + "set_preset_mode": "Skift af forudindstilling p\u00e5 {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} er indstillet til en bestemt klimaanl\u00e6gstilstand", + "is_preset_mode": "{entity_name} er indstillet til en bestemt forudindstillet tilstand" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} m\u00e5lte luftfugtighed \u00e6ndret", + "current_temperature_changed": "{entity_name} m\u00e5lte temperatur \u00e6ndret", + "hvac_mode_changed": "{entity_name} klimaanl\u00e6gstilstand \u00e6ndret" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/da.json b/homeassistant/components/cover/.translations/da.json index e603723b56449f..a645a3cc613fe9 100644 --- a/homeassistant/components/cover/.translations/da.json +++ b/homeassistant/components/cover/.translations/da.json @@ -5,6 +5,14 @@ "is_closing": "{entity_name} lukker", "is_open": "{entity_name} er \u00e5ben", "is_opening": "{entity_name} \u00e5bnes" + }, + "trigger_type": { + "closed": "{entity_name} lukket", + "closing": "{entity_name} lukning", + "opened": "{entity_name} \u00e5bnet", + "opening": "{entity_name} \u00e5bning", + "position": "{entity_name} position \u00e6ndres", + "tilt_position": "{entity_name} vippeposition \u00e6ndres" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index ec9c4dc35b16f9..80dc9ae6d3b5e1 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -54,10 +54,26 @@ "dim_up": "D\u00e6mp op", "left": "Venstre", "open": "\u00c5ben", - "right": "H\u00f8jre" + "right": "H\u00f8jre", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6" }, "trigger_type": { - "remote_gyro_activated": "Enhed rystet" + "remote_awakened": "Enheden v\u00e6kket", + "remote_double_tap": "Enheden \"{subtype}\" dobbelttappet", + "remote_falling": "Enheden er i frit fald", + "remote_gyro_activated": "Enhed rystet", + "remote_moved": "Enheden flyttede med \"{subtype}\" op", + "remote_rotate_from_side_1": "Enhed roteret fra \"side 1\" til \"{subtype}\"", + "remote_rotate_from_side_2": "Enhed roteret fra \"side 2\" til \"{subtype}\"", + "remote_rotate_from_side_3": "Enhed roteret fra \"side 3\" til \"{subtype}\"", + "remote_rotate_from_side_4": "Enhed roteret fra \"side 4\" til \"{subtype}\"", + "remote_rotate_from_side_5": "Enhed roteret fra \"side 5\" til \"{subtype}\"", + "remote_rotate_from_side_6": "Enhed roteret fra \"side 6\" til \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/demo/.translations/da.json b/homeassistant/components/demo/.translations/da.json new file mode 100644 index 00000000000000..ef01fcb4f3c35e --- /dev/null +++ b/homeassistant/components/demo/.translations/da.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/da.json b/homeassistant/components/dialogflow/.translations/da.json index 2fb203450a5eb9..22f123a38a1281 100644 --- a/homeassistant/components/dialogflow/.translations/da.json +++ b/homeassistant/components/dialogflow/.translations/da.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere [Webhook integration med Dialogflow]({dialogflow_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere [webhook-integration med Dialogflow]({dialogflow_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/elgato/.translations/da.json b/homeassistant/components/elgato/.translations/da.json new file mode 100644 index 00000000000000..a10e4d9e89f41a --- /dev/null +++ b/homeassistant/components/elgato/.translations/da.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Elgato Key Light-enhed er allerede konfigureret.", + "connection_error": "Kunne ikke oprette forbindelse til Elgato Key Light-enheden." + }, + "error": { + "connection_error": "Kunne ikke oprette forbindelse til Elgato Key Light-enheden." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "V\u00e6rt eller IP-adresse", + "port": "Portnummer" + }, + "description": "Indstil din Elgato Key Light til at integrere med Home Assistant.", + "title": "Forbind din Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vil du tilf\u00f8je Elgato Key Light med serienummer `{serial_number}` til Home Assistant?", + "title": "Fandt Elgato Key Light-enhed" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/da.json b/homeassistant/components/fan/.translations/da.json new file mode 100644 index 00000000000000..0c9556bfedb844 --- /dev/null +++ b/homeassistant/components/fan/.translations/da.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Sluk {entity_name}", + "turn_on": "T\u00e6nd for {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er slukket", + "is_on": "{entity_name} er t\u00e6ndt" + }, + "trigger_type": { + "turned_off": "{entity_name} blev slukket", + "turned_on": "{entity_name} blev t\u00e6ndt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/.translations/da.json b/homeassistant/components/geofency/.translations/da.json index 1390dfb504a619..21ff2e9fcedfea 100644 --- a/homeassistant/components/geofency/.translations/da.json +++ b/homeassistant/components/geofency/.translations/da.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i Geofency.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i Geofency.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_volcano/.translations/da.json b/homeassistant/components/geonetnz_volcano/.translations/da.json new file mode 100644 index 00000000000000..a8c238a60b016f --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokalitet allerede registreret" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Udfyld dine filteroplysninger." + } + }, + "title": "GeoNet NZ vulkan" + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/da.json b/homeassistant/components/gpslogger/.translations/da.json index 6d5c2185718a30..4aaebb7aa82a5e 100644 --- a/homeassistant/components/gpslogger/.translations/da.json +++ b/homeassistant/components/gpslogger/.translations/da.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i GPSLogger.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i GPSLogger.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/hisense_aehw4a1/.translations/da.json b/homeassistant/components/hisense_aehw4a1/.translations/da.json new file mode 100644 index 00000000000000..3d479543231ea6 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Hisense AEH-W4A1-enheder fundet p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Kun en enkelt konfiguration af Hisense AEH-W4A1 er mulig." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/da.json b/homeassistant/components/huawei_lte/.translations/da.json new file mode 100644 index 00000000000000..b693d414ceb484 --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_in_progress": "Denne enhed er allerede ved at blive konfigureret", + "not_huawei_lte": "Ikke en Huawei LTE-enhed" + }, + "error": { + "connection_timeout": "Timeout for forbindelse" + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Navn p\u00e5 meddelelsestjeneste (\u00e6ndring kr\u00e6ver genstart)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/da.json b/homeassistant/components/icloud/.translations/da.json new file mode 100644 index 00000000000000..1a06bd8e0f2969 --- /dev/null +++ b/homeassistant/components/icloud/.translations/da.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Kontoen er allerede konfigureret" + }, + "error": { + "login": "Loginfejl: Kontroller din email og adgangskode", + "send_verification_code": "Bekr\u00e6ftelseskoden kunne ikke sendes", + "username_exists": "Kontoen er allerede konfigureret", + "validate_verification_code": "Bekr\u00e6ftelseskoden kunne ikke bekr\u00e6ftes, V\u00e6lg en betroet enhed, og start bekr\u00e6ftelsen igen" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Betroet enhed" + }, + "description": "V\u00e6lg din betroede enhed", + "title": "iCloud-enhed, der er tillid til" + }, + "user": { + "data": { + "password": "Adgangskode", + "username": "Email" + }, + "description": "Indtast dine legitimationsoplysninger", + "title": "iCloud-legitimationsoplysninger" + }, + "verification_code": { + "data": { + "verification_code": "Bekr\u00e6ftelseskode" + }, + "description": "Indtast venligst den bekr\u00e6ftelseskode, du lige har modtaget fra iCloud", + "title": "iCloud-bekr\u00e6ftelseskode" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/da.json b/homeassistant/components/ifttt/.translations/da.json index 25c502ed05efad..7e05d9389997a5 100644 --- a/homeassistant/components/ifttt/.translations/da.json +++ b/homeassistant/components/ifttt/.translations/da.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du bruge handlingen \"Foretag en web foresp\u00f8rgsel\" fra [IFTTT Webhook applet] ({applet_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du bruge handlingen \"Foretag en web-foresp\u00f8rgsel\" fra [IFTTT Webhook-applet] ({applet_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." }, "step": { "user": { diff --git a/homeassistant/components/lock/.translations/da.json b/homeassistant/components/lock/.translations/da.json index de4f603ac43f38..517c86444fd5ba 100644 --- a/homeassistant/components/lock/.translations/da.json +++ b/homeassistant/components/lock/.translations/da.json @@ -7,6 +7,10 @@ "condition_type": { "is_locked": "{entity_name} er l\u00e5st", "is_unlocked": "{entity_name} er l\u00e5st op" + }, + "trigger_type": { + "locked": "{entity_name} blev l\u00e5st", + "unlocked": "{entity_name} l\u00e5st op" } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/da.json b/homeassistant/components/lutron_caseta/.translations/da.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/da.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/da.json b/homeassistant/components/mailgun/.translations/da.json index 0e25974031d75a..475d560bad62bb 100644 --- a/homeassistant/components/mailgun/.translations/da.json +++ b/homeassistant/components/mailgun/.translations/da.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere [Webhooks med Mailgun]({mailgun_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere [Webhooks med Mailgun]({mailgun_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/da.json b/homeassistant/components/plaato/.translations/da.json index 12e95b25e0f27e..f8d59572388591 100644 --- a/homeassistant/components/plaato/.translations/da.json +++ b/homeassistant/components/plaato/.translations/da.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i Plaato Airlock.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i Plaato Airlock.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 99d5d4d1685c64..2c3a20ae94f9a2 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex konfigureres", "discovery_no_file": "Der blev ikke fundet nogen legacy konfigurationsfil", "invalid_import": "Importeret konfiguration er ugyldig", + "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Timeout ved hentning af token", "unknown": "Mislykkedes af ukendt \u00e5rsag" }, diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json index 557eeab55b1f16..49bf83148a23ad 100644 --- a/homeassistant/components/soma/.translations/da.json +++ b/homeassistant/components/soma/.translations/da.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Du kan kun konfigurere en Soma-konto.", "authorize_url_timeout": "Timeout ved generering af autoriseret url.", - "missing_configuration": "Soma-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen." + "connection_error": "Kunne ikke oprette forbindelse til SOMA Connect.", + "missing_configuration": "Soma-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen.", + "result_error": "SOMA Connect svarede med fejlstatus." }, "create_entry": { "default": "Godkendt med Soma." diff --git a/homeassistant/components/somfy/.translations/da.json b/homeassistant/components/somfy/.translations/da.json index 9d05fd65a06a25..b50c030c636548 100644 --- a/homeassistant/components/somfy/.translations/da.json +++ b/homeassistant/components/somfy/.translations/da.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Godkendt med Somfy." }, + "step": { + "pick_implementation": { + "title": "V\u00e6lg godkendelsesmetode" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/da.json b/homeassistant/components/starline/.translations/da.json new file mode 100644 index 00000000000000..2a8cbcf12700b5 --- /dev/null +++ b/homeassistant/components/starline/.translations/da.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Forkert applikations-id eller hemmelighed", + "error_auth_mfa": "Forkert kode", + "error_auth_user": "Forkert brugernavn eller adgangskode" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App-id", + "app_secret": "Hemmelighed" + }, + "description": "Applikations-id og hemmelig kode fra StarLine-udviklerkonto ", + "title": "Applikations-legitimationsoplysninger" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kode fra billede" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS-kode" + }, + "description": "Indtast koden, der er sendt til telefon {phone_number}", + "title": "Tofaktor-godkendelse" + }, + "auth_user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "description": "StarLine-konto email og adgangskode", + "title": "Brugeroplysninger" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/da.json b/homeassistant/components/tesla/.translations/da.json new file mode 100644 index 00000000000000..85091c350d8c73 --- /dev/null +++ b/homeassistant/components/tesla/.translations/da.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Fejl ved tilslutning; tjek netv\u00e6rk og pr\u00f8v igen", + "identifier_exists": "Email er allerede registreret", + "invalid_credentials": "Ugyldige legitimationsoplysninger", + "unknown_error": "Ukendt fejl, rapporter venligst loginfo" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Email-adresse" + }, + "description": "Indtast dine oplysninger.", + "title": "Tesla - Konfiguration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunder mellem scanninger" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/da.json b/homeassistant/components/traccar/.translations/da.json index af3963f8c0f7b5..2b0ec0003d6800 100644 --- a/homeassistant/components/traccar/.translations/da.json +++ b/homeassistant/components/traccar/.translations/da.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i Traccar.\n\n Brug f\u00f8lgende URL: `{webhook_url}`\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i Traccar.\n\nBrug f\u00f8lgende webadresse: `{webhook_url}`\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/twilio/.translations/da.json b/homeassistant/components/twilio/.translations/da.json index 3c1ab7c01b52bb..0bb40aae7f25f2 100644 --- a/homeassistant/components/twilio/.translations/da.json +++ b/homeassistant/components/twilio/.translations/da.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere [Webhooks med Twilio]({twilio_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/x-www-form-urlencoded\n\n Se [dokumentationen]({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere [Webhooks med Twilio]({twilio_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/x-www-form-urlencoded\n\nSe [dokumentationen]({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." }, "step": { "user": { diff --git a/homeassistant/components/vacuum/.translations/da.json b/homeassistant/components/vacuum/.translations/da.json new file mode 100644 index 00000000000000..fac748ca464a04 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/da.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Lad {entity_name} g\u00f8re rent", + "dock": "Lad {entity_name} vende tilbage til dock" + }, + "condition_type": { + "is_cleaning": "{entity_name} g\u00f8r rent", + "is_docked": "{entity_name} er i dock" + }, + "trigger_type": { + "cleaning": "{entity_name} begyndte at reng\u00f8re", + "docked": "{entity_name} er i dock" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/da.json b/homeassistant/components/wled/.translations/da.json new file mode 100644 index 00000000000000..0ab3a789b3a4d9 --- /dev/null +++ b/homeassistant/components/wled/.translations/da.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Denne WLED-enhed er allerede konfigureret.", + "connection_error": "Kunne ikke oprette forbindelse til WLED-enheden." + }, + "error": { + "connection_error": "Kunne ikke oprette forbindelse til WLED-enheden." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "V\u00e6rt eller IP-adresse" + }, + "description": "Indstil din WLED til at integrere med Home Assistant.", + "title": "Forbind din WLED" + }, + "zeroconf_confirm": { + "description": "\u00d8nsker du at tilf\u00f8je WLED-enhed med navnet `{name}' til Home Assistant?", + "title": "Fandt WLED-enhed" + } + }, + "title": "WLED" + } +} \ No newline at end of file From a91b0058229974aed578fb20e833df4454d007f9 Mon Sep 17 00:00:00 2001 From: LE LAY Olivier Date: Sun, 29 Dec 2019 08:26:43 +0100 Subject: [PATCH 2579/3953] Fix ble_tracker randomly pygatt thrown error (#28671) * fix(ble_tracker): catch randomly pygatt thrown error * fix(ble_tracker): merge except errors --- homeassistant/components/bluetooth_le_tracker/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 40f25f2fc43c55..9c64232c6e9590 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -77,7 +77,7 @@ def discover_ble_devices(): devices = {x["address"]: x["name"] for x in devs} _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) - except RuntimeError as error: + except (RuntimeError, pygatt.exceptions.BLEError) as error: _LOGGER.error("Error during Bluetooth LE scan: %s", error) return {} return devices From 85624e80312e0487f51653ce24b398320972cbec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Sun, 29 Dec 2019 17:18:23 +0100 Subject: [PATCH 2580/3953] Fix creating smappee sensors when remote is not active (#30270) --- homeassistant/components/smappee/sensor.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 28abf759d098f6..c61d28bbaacbae 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -97,19 +97,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) if smappee.is_local_active: - for location_id in smappee.locations.keys(): + if smappee.is_remote_active: + location_keys = smappee.locations.keys() + else: + location_keys = [None] + for location_id in location_keys: for sensor in SENSOR_TYPES: if "local" in SENSOR_TYPES[sensor]: - if smappee.is_remote_active: - dev.append( - SmappeeSensor( - smappee, location_id, sensor, SENSOR_TYPES[sensor] - ) - ) - else: - dev.append( - SmappeeSensor(smappee, None, sensor, SENSOR_TYPES[sensor]) + dev.append( + SmappeeSensor( + smappee, location_id, sensor, SENSOR_TYPES[sensor] ) + ) add_entities(dev, True) From 9892564ab54bb38959fcfdbe9c1d2656af9cd0c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 29 Dec 2019 21:12:49 +0200 Subject: [PATCH 2581/3953] Upgrade rflink to 0.0.50, ignore_devices now supports * and ? anywhere (#30268) https://github.com/aequitas/python-rflink/releases/tag/0.0.50 https://github.com/aequitas/python-rflink/releases/tag/0.0.49 https://github.com/aequitas/python-rflink/releases/tag/0.0.48 https://github.com/aequitas/python-rflink/releases/tag/0.0.47 --- homeassistant/components/rflink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index bda260bdff2230..8c322e5bdf562b 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -3,7 +3,7 @@ "name": "Rflink", "documentation": "https://www.home-assistant.io/integrations/rflink", "requirements": [ - "rflink==0.0.46" + "rflink==0.0.50" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index ca438455bbacda..641667d4506b6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1740,7 +1740,7 @@ restrictedpython==5.0 rfk101py==0.0.1 # homeassistant.components.rflink -rflink==0.0.46 +rflink==0.0.50 # homeassistant.components.ring ring_doorbell==0.2.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5ee5e0178646ee..6b9a7c98702772 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -550,7 +550,7 @@ regenmaschine==1.5.1 restrictedpython==5.0 # homeassistant.components.rflink -rflink==0.0.46 +rflink==0.0.50 # homeassistant.components.ring ring_doorbell==0.2.8 From 13116d8d3f6900a12a93e6eeb567263ab7349a1f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 30 Dec 2019 00:32:19 +0000 Subject: [PATCH 2582/3953] [ci skip] Translation update --- .../components/abode/.translations/da.json | 2 +- .../components/adguard/.translations/da.json | 12 +++-- .../components/airly/.translations/da.json | 4 +- .../alarm_control_panel/.translations/da.json | 4 ++ .../alarm_control_panel/.translations/nl.json | 7 +++ .../ambient_station/.translations/da.json | 2 +- .../components/auth/.translations/da.json | 8 ++-- .../components/auth/.translations/nl.json | 2 +- .../components/axis/.translations/da.json | 8 ++-- .../binary_sensor/.translations/da.json | 29 +++++++++++- .../cert_expiry/.translations/da.json | 2 +- .../components/climate/.translations/nl.json | 17 +++++++ .../coolmaster/.translations/da.json | 10 +++- .../coolmaster/.translations/nl.json | 9 +++- .../components/cover/.translations/da.json | 4 +- .../components/deconz/.translations/da.json | 46 ++++++++++++------- .../device_tracker/.translations/da.json | 8 ++++ .../dialogflow/.translations/da.json | 2 +- .../components/ecobee/.translations/da.json | 5 +- .../components/elgato/.translations/nl.json | 27 +++++++++++ .../emulated_roku/.translations/da.json | 10 ++-- .../components/esphome/.translations/da.json | 4 +- .../components/geofency/.translations/da.json | 2 +- .../geonetnz_volcano/.translations/nl.json | 3 +- .../gpslogger/.translations/da.json | 2 +- .../components/hangouts/.translations/da.json | 8 ++-- .../hisense_aehw4a1/.translations/nl.json | 15 ++++++ .../homekit_controller/.translations/da.json | 22 ++++----- .../huawei_lte/.translations/da.json | 29 ++++++++++-- .../huawei_lte/.translations/ko.json | 1 + .../huawei_lte/.translations/nl.json | 1 + .../huawei_lte/.translations/no.json | 1 + .../components/hue/.translations/da.json | 16 +++---- .../components/icloud/.translations/nl.json | 38 +++++++++++++++ .../components/ifttt/.translations/da.json | 6 +-- .../components/ipma/.translations/da.json | 2 +- .../components/life360/.translations/da.json | 2 +- .../components/lifx/.translations/da.json | 4 +- .../components/light/.translations/da.json | 13 +++++- .../components/locative/.translations/da.json | 2 +- .../components/lock/.translations/da.json | 3 +- .../logi_circle/.translations/da.json | 6 +-- .../luftdaten/.translations/da.json | 2 +- .../lutron_caseta/.translations/nl.json | 5 ++ .../components/mailgun/.translations/da.json | 2 +- .../media_player/.translations/da.json | 11 +++++ .../components/met/.translations/da.json | 2 +- .../mobile_app/.translations/da.json | 8 ++-- .../components/mqtt/.translations/da.json | 10 ++-- .../components/neato/.translations/da.json | 9 ++-- .../components/nest/.translations/da.json | 4 +- .../components/notion/.translations/da.json | 2 +- .../opentherm_gw/.translations/da.json | 11 +++-- .../components/openuv/.translations/da.json | 4 +- .../owntracks/.translations/da.json | 2 +- .../components/plaato/.translations/da.json | 4 +- .../components/plex/.translations/da.json | 10 ++-- .../components/plex/.translations/nl.json | 1 + .../components/point/.translations/da.json | 2 +- .../components/ps4/.translations/da.json | 12 ++--- .../rainmachine/.translations/da.json | 2 +- .../components/sensor/.translations/da.json | 36 +++++++-------- .../simplisafe/.translations/da.json | 2 +- .../smartthings/.translations/da.json | 4 +- .../components/smhi/.translations/da.json | 4 +- .../components/soma/.translations/nl.json | 4 +- .../components/sonos/.translations/da.json | 2 +- .../components/starline/.translations/nl.json | 42 +++++++++++++++++ .../components/switch/.translations/da.json | 19 ++++++++ .../components/tesla/.translations/nl.json | 30 ++++++++++++ .../components/tesla/.translations/no.json | 30 ++++++++++++ .../components/toon/.translations/da.json | 8 ++-- .../components/tplink/.translations/da.json | 2 +- .../components/traccar/.translations/da.json | 4 +- .../components/traccar/.translations/tr.json | 10 ++++ .../transmission/.translations/da.json | 6 +-- .../twentemilieu/.translations/tr.json | 7 +++ .../components/twilio/.translations/da.json | 2 +- .../components/unifi/.translations/da.json | 8 +++- .../components/upnp/.translations/da.json | 4 +- .../components/vacuum/.translations/nl.json | 10 +++- .../components/vesync/.translations/da.json | 2 +- .../components/wemo/.translations/da.json | 4 +- .../components/withings/.translations/da.json | 10 ++++ .../components/wled/.translations/nl.json | 1 + .../components/wwlln/.translations/da.json | 6 +-- .../components/zha/.translations/da.json | 31 +++++++++++-- .../components/zwave/.translations/da.json | 6 +-- 88 files changed, 607 insertions(+), 188 deletions(-) create mode 100644 homeassistant/components/climate/.translations/nl.json create mode 100644 homeassistant/components/device_tracker/.translations/da.json create mode 100644 homeassistant/components/elgato/.translations/nl.json create mode 100644 homeassistant/components/hisense_aehw4a1/.translations/nl.json create mode 100644 homeassistant/components/icloud/.translations/nl.json create mode 100644 homeassistant/components/lutron_caseta/.translations/nl.json create mode 100644 homeassistant/components/media_player/.translations/da.json create mode 100644 homeassistant/components/starline/.translations/nl.json create mode 100644 homeassistant/components/switch/.translations/da.json create mode 100644 homeassistant/components/tesla/.translations/nl.json create mode 100644 homeassistant/components/tesla/.translations/no.json create mode 100644 homeassistant/components/traccar/.translations/tr.json create mode 100644 homeassistant/components/twentemilieu/.translations/tr.json diff --git a/homeassistant/components/abode/.translations/da.json b/homeassistant/components/abode/.translations/da.json index 3f094cb93bd13f..4a5fa763ea16de 100644 --- a/homeassistant/components/abode/.translations/da.json +++ b/homeassistant/components/abode/.translations/da.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Adgangskode", - "username": "Email adresse" + "username": "Email-adresse" }, "title": "Udfyld dine Abode-loginoplysninger" } diff --git a/homeassistant/components/adguard/.translations/da.json b/homeassistant/components/adguard/.translations/da.json index 813405cec62471..e9e6415518d833 100644 --- a/homeassistant/components/adguard/.translations/da.json +++ b/homeassistant/components/adguard/.translations/da.json @@ -1,16 +1,18 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Denne integration kr\u00e6ver AdGuard Home {minimal_version} eller h\u00f8jere, du har {current_version}. Opdater venligst din Hass.io AdGuard Home-tilf\u00f8jelse.", + "adguard_home_outdated": "Denne integration kr\u00e6ver AdGuard Home {minimal_version} eller h\u00f8jere, du har {current_version}.", "existing_instance_updated": "Opdaterede eksisterende konfiguration.", - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af AdGuard Home." + "single_instance_allowed": "Kun en enkelt konfiguration af AdGuard Home er tilladt." }, "error": { "connection_error": "Forbindelse mislykkedes." }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home, der leveres af Hass.io add-on: {addon}?", - "title": "AdGuard Home via Hass.io add-on" + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Hass.io-tilf\u00f8jelsen: {addon}?", + "title": "AdGuard Home via Hass.io-tilf\u00f8jelse" }, "user": { "data": { @@ -21,8 +23,8 @@ "username": "Brugernavn", "verify_ssl": "AdGuard Home bruger et korrekt certifikat" }, - "description": "Konfigurer din AdGuard Home instans for at tillade overv\u00e5gning og kontrol.", - "title": "Link AdGuard Home." + "description": "Konfigurer din AdGuard Home-instans for at tillade overv\u00e5gning og kontrol.", + "title": "Forbind din AdGuard Home." } }, "title": "AdGuard Home" diff --git a/homeassistant/components/airly/.translations/da.json b/homeassistant/components/airly/.translations/da.json index 652cc46a7b3e0a..c2c14d1d101c5a 100644 --- a/homeassistant/components/airly/.translations/da.json +++ b/homeassistant/components/airly/.translations/da.json @@ -3,7 +3,7 @@ "error": { "auth": "API-n\u00f8glen er ikke korrekt.", "name_exists": "Navnet findes allerede.", - "wrong_location": "Ingen Airly m\u00e5lestationer i dette omr\u00e5de." + "wrong_location": "Ingen Airly-m\u00e5lestationer i dette omr\u00e5de." }, "step": { "user": { @@ -13,7 +13,7 @@ "longitude": "L\u00e6ngdegrad", "name": "Integrationens navn" }, - "description": "Konfigurer Airly luftkvalitet integration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", + "description": "Konfigurer Airly luftkvalitetsintegration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", "title": "Airly" } }, diff --git a/homeassistant/components/alarm_control_panel/.translations/da.json b/homeassistant/components/alarm_control_panel/.translations/da.json index 304fa342bf2f7a..220034d23e17d3 100644 --- a/homeassistant/components/alarm_control_panel/.translations/da.json +++ b/homeassistant/components/alarm_control_panel/.translations/da.json @@ -1,6 +1,10 @@ { "device_automation": { "action_type": { + "arm_away": "Tilkobl {entity_name} ude", + "arm_home": "Tilkobl {entity_name} hjemme", + "arm_night": "Tilkobl {entity_name} nat", + "disarm": "Frakobl {entity_name}", "trigger": "Udl\u00f8s {entity_name}" }, "trigger_type": { diff --git a/homeassistant/components/alarm_control_panel/.translations/nl.json b/homeassistant/components/alarm_control_panel/.translations/nl.json index 9329a089d3204b..5081ae992b4d74 100644 --- a/homeassistant/components/alarm_control_panel/.translations/nl.json +++ b/homeassistant/components/alarm_control_panel/.translations/nl.json @@ -6,6 +6,13 @@ "arm_night": "Inschakelen {entity_name} nacht", "disarm": "Uitschakelen {entity_name}", "trigger": "Trigger {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} afwezig ingeschakeld", + "armed_home": "{entity_name} thuis ingeschakeld", + "armed_night": "{entity_name} nachtstand ingeschakeld", + "disarmed": "{entity_name} uitgeschakeld", + "triggered": "{entity_name} geactiveerd" } } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/da.json b/homeassistant/components/ambient_station/.translations/da.json index ac3d86a995bd09..6cec31eca29a53 100644 --- a/homeassistant/components/ambient_station/.translations/da.json +++ b/homeassistant/components/ambient_station/.translations/da.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "api_key": "API n\u00f8gle", + "api_key": "API-n\u00f8gle", "app_key": "Applikationsn\u00f8gle" }, "title": "Udfyld dine oplysninger" diff --git a/homeassistant/components/auth/.translations/da.json b/homeassistant/components/auth/.translations/da.json index 8bf710a1932966..7877a813218b53 100644 --- a/homeassistant/components/auth/.translations/da.json +++ b/homeassistant/components/auth/.translations/da.json @@ -10,14 +10,14 @@ "step": { "init": { "description": "V\u00e6lg venligst en af meddelelsestjenesterne:", - "title": "Ops\u00e6t engangsadgangskode, der er leveret af besked komponenten" + "title": "Ops\u00e6t engangsadgangskoder leveret af notify-komponenten" }, "setup": { "description": "En engangsadgangskode er blevet sendt via **notify.{notify_service}**. Indtast den venligst nedenunder:", "title": "Bekr\u00e6ft ops\u00e6tningen" } }, - "title": "Advis\u00e9r engangskodeord" + "title": "Notify-engangsadgangskode" }, "totp": { "error": { @@ -25,8 +25,8 @@ }, "step": { "init": { - "description": "Hvis du vil aktivere tofaktorautentificering ved hj\u00e6lp af tidsbaserede engangskoder skal du scanne QR-koden med din autentificeringsapp. Hvis du ikke har en anbefaler vi enten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har scannet koden skal du indtaste den sekscifrede kode fra din app for at bekr\u00e6fte ops\u00e6tningen. Hvis du har problemer med at scanne QR-koden skal du lave en manuel ops\u00e6tning med kode **`{code}`**.", - "title": "Konfigurer to-faktors godkendelse ved hj\u00e6lp af TOTP" + "description": "Hvis du vil aktivere tofaktorgodkendelse ved hj\u00e6lp af tidsbaserede engangskoder skal du scanne QR-koden med din autentificeringsapp. Hvis du ikke har en anbefaler vi enten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har scannet koden skal du indtaste den sekscifrede kode fra din app for at bekr\u00e6fte ops\u00e6tningen. Hvis du har problemer med at scanne QR-koden skal du lave en manuel ops\u00e6tning med kode **`{code}`**.", + "title": "Konfigurer tofaktorgodkendelse ved hj\u00e6lp af TOTP" } }, "title": "TOTP" diff --git a/homeassistant/components/auth/.translations/nl.json b/homeassistant/components/auth/.translations/nl.json index 9ec8006507b824..d61613097dd7b5 100644 --- a/homeassistant/components/auth/.translations/nl.json +++ b/homeassistant/components/auth/.translations/nl.json @@ -9,7 +9,7 @@ }, "step": { "init": { - "description": "Selecteer een van de meldingsdiensten:", + "description": "Selecteer een van de meldingsservices:", "title": "Stel een \u00e9\u00e9nmalig wachtwoord in dat wordt afgegeven door een meldingscomponent" }, "setup": { diff --git a/homeassistant/components/axis/.translations/da.json b/homeassistant/components/axis/.translations/da.json index c169f85f2802d5..21f33d120f7ae3 100644 --- a/homeassistant/components/axis/.translations/da.json +++ b/homeassistant/components/axis/.translations/da.json @@ -8,11 +8,11 @@ }, "error": { "already_configured": "Enheden er allerede konfigureret", - "already_in_progress": "Enheds konfiguration er allerede i gang.", + "already_in_progress": "Enhedskonfiguration er allerede i gang.", "device_unavailable": "Enheden er ikke tilg\u00e6ngelig", "faulty_credentials": "Ugyldige legitimationsoplysninger" }, - "flow_title": "Axis enhed: {name} ({host})", + "flow_title": "Axis-enhed: {name} ({host})", "step": { "user": { "data": { @@ -21,9 +21,9 @@ "port": "Port", "username": "Brugernavn" }, - "title": "Konfigurer Axis enhed" + "title": "Indstil Axis-enhed" } }, - "title": "Axis enhed" + "title": "Axis-enhed" } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/da.json b/homeassistant/components/binary_sensor/.translations/da.json index f7bd834561c11e..19229c16cb3438 100644 --- a/homeassistant/components/binary_sensor/.translations/da.json +++ b/homeassistant/components/binary_sensor/.translations/da.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_bat_low": "{entity_name} batteri er lavt", "is_cold": "{entity_name} er kold", "is_connected": "{entity_name} er tilsluttet", "is_gas": "{entity_name} registrerer gas", @@ -17,6 +18,7 @@ "is_no_smoke": "{entity_name} registrerer ikke r\u00f8g", "is_no_sound": "{entity_name} registrerer ikke lyd", "is_no_vibration": "{entity_name} registrerer ikke vibration", + "is_not_bat_low": "{entity_name} batteri er normalt", "is_not_cold": "{entity_name} er ikke kold", "is_not_connected": "{entity_name} er afbrudt", "is_not_hot": "{entity_name} er ikke varm", @@ -25,10 +27,17 @@ "is_not_moving": "{entity_name} bev\u00e6ger sig ikke", "is_not_occupied": "{entity_name} er ikke optaget", "is_not_open": "{entity_name} er lukket", + "is_not_plugged_in": "{entity_name} er ikke tilsluttet str\u00f8m", + "is_not_powered": "{entity_name} er ikke tilsluttet str\u00f8m", "is_not_present": "{entity_name} er ikke til stede", "is_not_unsafe": "{entity_name} er sikker", "is_occupied": "{entity_name} er optaget", + "is_off": "{entity_name} er sl\u00e5et fra", + "is_on": "{entity_name} er sl\u00e5et til", "is_open": "{entity_name} er \u00e5ben", + "is_plugged_in": "{entity_name} er tilsluttet str\u00f8m", + "is_powered": "{entity_name} er tilsluttet str\u00f8m", + "is_present": "{entity_name} er til stede", "is_problem": "{entity_name} registrerer problem", "is_smoke": "{entity_name} registrerer r\u00f8g", "is_sound": "{entity_name} registrerer lyd", @@ -36,9 +45,14 @@ "is_vibration": "{entity_name} registrerer vibration" }, "trigger_type": { + "bat_low": "{entity_name} lavt batteriniveau", "closed": "{entity_name} lukket", "cold": "{entity_name} blev kold", "connected": "{entity_name} tilsluttet", + "gas": "{entity_name} begyndte at registrere gas", + "hot": "{entity_name} blev varm", + "light": "{entity_name} begyndte at registrere lys", + "locked": "{entity_name} l\u00e5st", "moist": "{entity_name} blev fugtig", "moist\u00a7": "{entity_name} blev fugtig", "motion": "{entity_name} begyndte at registrere bev\u00e6gelse", @@ -50,18 +64,31 @@ "no_smoke": "{entity_name} stoppede med at registrere r\u00f8g", "no_sound": "{entity_name} stoppede med at registrere lyd", "no_vibration": "{entity_name} stoppede med at registrere vibration", + "not_bat_low": "{entity_name} batteri normalt", + "not_cold": "{entity_name} blev ikke kold", "not_connected": "{entity_name} afbrudt", "not_hot": "{entity_name} blev ikke varm", "not_locked": "{entity_name} l\u00e5st op", "not_moist": "{entity_name} blev t\u00f8r", + "not_moving": "{entity_name} stoppede med at bev\u00e6ge sig", + "not_occupied": "{entity_name} blev ikke optaget", "not_opened": "{entity_name} lukket", + "not_plugged_in": "{entity_name} ikke tilsluttet str\u00f8m", + "not_powered": "{entity_name} ikke tilsluttet str\u00f8m", "not_present": "{entity_name} ikke til stede", "not_unsafe": "{entity_name} blev sikker", "occupied": "{entity_name} blev optaget", + "opened": "{entity_name} \u00e5bnet", + "plugged_in": "{entity_name} tilsluttet str\u00f8m", + "powered": "{entity_name} tilsluttet str\u00f8m", "present": "{entity_name} til stede", "problem": "{entity_name} begyndte at registrere problem", "smoke": "{entity_name} begyndte at registrere r\u00f8g", - "sound": "{entity_name} begyndte at registrere lyd" + "sound": "{entity_name} begyndte at registrere lyd", + "turned_off": "{entity_name} slukkede", + "turned_on": "{entity_name} t\u00e6ndte", + "unsafe": "{entity_name} blev usikker", + "vibration": "{entity_name} begyndte at registrere vibration" } } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/da.json b/homeassistant/components/cert_expiry/.translations/da.json index c95a56320c985e..26ee436860aad9 100644 --- a/homeassistant/components/cert_expiry/.translations/da.json +++ b/homeassistant/components/cert_expiry/.translations/da.json @@ -21,6 +21,6 @@ "title": "Definer certifikatet, der skal testes" } }, - "title": "Certifikat udl\u00f8b" + "title": "Certifikatets udl\u00f8bsdato" } } \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/nl.json b/homeassistant/components/climate/.translations/nl.json new file mode 100644 index 00000000000000..87e16c1c885a9f --- /dev/null +++ b/homeassistant/components/climate/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Wijzig de HVAC-modus op {entity_name}", + "set_preset_mode": "Wijzig voorinstelling op {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} is ingesteld op een specifieke HVAC-modus", + "is_preset_mode": "{entity_name} is ingesteld op een specifieke vooraf ingestelde modus" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} gemeten vochtigheid veranderd", + "current_temperature_changed": "{entity_name} gemeten temperatuur veranderd", + "hvac_mode_changed": "{entity_name} HVAC-modus gewijzigd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/da.json b/homeassistant/components/coolmaster/.translations/da.json index 8f50a0eb6ddf79..882bc5de359535 100644 --- a/homeassistant/components/coolmaster/.translations/da.json +++ b/homeassistant/components/coolmaster/.translations/da.json @@ -1,9 +1,17 @@ { "config": { + "error": { + "connection_error": "Kunne ikke oprette forbindelse til CoolMasterNet-instansen. Tjek din v\u00e6rt.", + "no_units": "Kunne ikke finde nogen klimaanl\u00e6g i CoolMasterNet-v\u00e6rt." + }, "step": { "user": { "data": { - "heat_cool": "Underst\u00f8t automatisk varm/k\u00f8l tilstand", + "cool": "Underst\u00f8tter k\u00f8lingstilstand", + "dry": "Underst\u00f8tter t\u00f8rringstilstand", + "fan_only": "Underst\u00f8tter kun-bl\u00e6ser-tilstand", + "heat": "Underst\u00f8tter varmetilstand", + "heat_cool": "Underst\u00f8tter automatisk varm/k\u00f8l-tilstand", "host": "V\u00e6rt", "off": "Kan slukkes" }, diff --git a/homeassistant/components/coolmaster/.translations/nl.json b/homeassistant/components/coolmaster/.translations/nl.json index 02b65cdfff912a..e5b1683790f299 100644 --- a/homeassistant/components/coolmaster/.translations/nl.json +++ b/homeassistant/components/coolmaster/.translations/nl.json @@ -1,11 +1,18 @@ { "config": { "error": { - "connection_error": "Kan geen verbinding maken met CoolMasterNet-instantie. Controleer uw host" + "connection_error": "Kan geen verbinding maken met CoolMasterNet-instantie. Controleer uw host", + "no_units": "Kon geen HVAC units vinden in CoolMasterNet host." }, "step": { "user": { "data": { + "cool": "Ondersteuning afkoelen modus", + "dry": "Ondersteuning droog modus", + "fan_only": "Ondersteunt alleen ventilatormodus", + "heat": "Ondersteuning warmtemodus", + "heat_cool": "Ondersteuning van automatische warmte/koelmodus", + "host": "Host", "off": "Kan uitgeschakeld worden" }, "title": "Stel uw CoolMasterNet-verbindingsgegevens in." diff --git a/homeassistant/components/cover/.translations/da.json b/homeassistant/components/cover/.translations/da.json index a645a3cc613fe9..64b89be526726c 100644 --- a/homeassistant/components/cover/.translations/da.json +++ b/homeassistant/components/cover/.translations/da.json @@ -4,7 +4,9 @@ "is_closed": "{entity_name} er lukket", "is_closing": "{entity_name} lukker", "is_open": "{entity_name} er \u00e5ben", - "is_opening": "{entity_name} \u00e5bnes" + "is_opening": "{entity_name} \u00e5bnes", + "is_position": "Aktuel {entity_name} position er", + "is_tilt_position": "Aktuel {entity_name} vippeposition er" }, "trigger_type": { "closed": "{entity_name} lukket", diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index 80dc9ae6d3b5e1..1f0828274f0d12 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Bridge er allerede konfigureret", - "already_in_progress": "Bro konfiguration er allerede i gang.", - "no_bridges": "Ingen deConz bridge fundet", - "not_deconz_bridge": "Ikke en deCONZ bro", - "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n deCONZ forekomst", - "updated_instance": "Opdaterede deCONZ instans med ny v\u00e6rtsadresse" + "already_in_progress": "Konfigurationsflow for bro er allerede i gang.", + "no_bridges": "Ingen deConz-bridge fundet", + "not_deconz_bridge": "Ikke en deCONZ-bro", + "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n deCONZ-instans", + "updated_instance": "Opdaterede deCONZ-instans med ny v\u00e6rtadresse" }, "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8gle" @@ -16,28 +16,28 @@ "hassio_confirm": { "data": { "allow_clip_sensor": "Tillad import af virtuelle sensorer", - "allow_deconz_groups": "Tillad import af deCONZ grupper" + "allow_deconz_groups": "Tillad import af deCONZ-grupper" }, - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ gateway leveret af Hass.io add-on {addon}?", - "title": "deCONZ Zigbee-gateway via Hass.io add-on" + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ-gateway'en leveret af Hass.io-tilf\u00f8jelsen {addon}?", + "title": "deCONZ Zigbee-gateway via Hass.io-tilf\u00f8jelse" }, "init": { "data": { "host": "V\u00e6rt", "port": "Port" }, - "title": "Definer deCONZ gateway" + "title": "Definer deCONZ-gateway" }, "link": { "description": "L\u00e5s din deCONZ-gateway op for at registrere dig med Home Assistant. \n\n 1. G\u00e5 til deCONZ settings -> Gateway -> Advanced\n 2. Tryk p\u00e5 knappen \"Authenticate app\"", - "title": "Link med deCONZ" + "title": "Forbind med deCONZ" }, "options": { "data": { "allow_clip_sensor": "Tillad import af virtuelle sensorer", - "allow_deconz_groups": "Tillad importering af deCONZ grupper" + "allow_deconz_groups": "Tillad import af deCONZ-grupper" }, - "title": "Ekstra konfiguration valgmuligheder for deCONZ" + "title": "Ekstra konfigurationsindstillinger for deCONZ" } }, "title": "deCONZ Zigbee gateway" @@ -60,10 +60,22 @@ "side_3": "Side 3", "side_4": "Side 4", "side_5": "Side 5", - "side_6": "Side 6" + "side_6": "Side 6", + "turn_off": "Sluk", + "turn_on": "T\u00e6nd" }, "trigger_type": { "remote_awakened": "Enheden v\u00e6kket", + "remote_button_double_press": "\"{subtype}\"-knappen er dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen trykket p\u00e5 konstant", + "remote_button_long_release": "\"{subtype}\"-knappen frigivet efter langt tryk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen firedobbelt-klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt-klikket", + "remote_button_rotated": "Knap roteret \"{subtype}\"", + "remote_button_rotation_stopped": "Knaprotation \"{subtype}\" er stoppet", + "remote_button_short_press": "\"{subtype}\"-knappen trykket p\u00e5", + "remote_button_short_release": "\"{subtype}\"-knappen frigivet", + "remote_button_triple_press": "\"{subtype}\"-knappen tredobbeltklikkes", "remote_double_tap": "Enheden \"{subtype}\" dobbelttappet", "remote_falling": "Enheden er i frit fald", "remote_gyro_activated": "Enhed rystet", @@ -80,15 +92,15 @@ "step": { "async_step_deconz_devices": { "data": { - "allow_clip_sensor": "Tillad deCONZ CLIP sensorer", - "allow_deconz_groups": "Tillad deCONZ lys grupper" + "allow_clip_sensor": "Tillad deCONZ CLIP-sensorer", + "allow_deconz_groups": "Tillad deCONZ-lysgrupper" }, "description": "Konfigurer synligheden af deCONZ-enhedstyper" }, "deconz_devices": { "data": { - "allow_clip_sensor": "Tillad deCONZ CLIP sensorer", - "allow_deconz_groups": "Tillad deCONZ lys grupper" + "allow_clip_sensor": "Tillad deCONZ CLIP-sensorer", + "allow_deconz_groups": "Tillad deCONZ-lysgrupper" }, "description": "Konfigurer synligheden af deCONZ-enhedstyper" } diff --git a/homeassistant/components/device_tracker/.translations/da.json b/homeassistant/components/device_tracker/.translations/da.json new file mode 100644 index 00000000000000..d714b5b7d31c2f --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/da.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_home": "{entity_name} er hjemme", + "is_not_home": "{entity_name} er ikke hjemme" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/.translations/da.json b/homeassistant/components/dialogflow/.translations/da.json index 22f123a38a1281..c682c07a8b9fef 100644 --- a/homeassistant/components/dialogflow/.translations/da.json +++ b/homeassistant/components/dialogflow/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Dialogflow meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Dialogflow-meddelelser", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { diff --git a/homeassistant/components/ecobee/.translations/da.json b/homeassistant/components/ecobee/.translations/da.json index 7a42a9470db52a..614811db45aee3 100644 --- a/homeassistant/components/ecobee/.translations/da.json +++ b/homeassistant/components/ecobee/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "one_instance_only": "Integrationen underst\u00f8tter kun \u00e9n ecobee forekomst" + "one_instance_only": "Denne integration underst\u00f8tter i \u00f8jeblikket kun en ecobee-instans." }, "error": { "pin_request_failed": "Fejl ved anmodning om pinkode fra ecobee. Kontroller at API-n\u00f8glen er korrekt.", @@ -9,7 +9,8 @@ }, "step": { "authorize": { - "title": "Autoriser app p\u00e5 ecobee.com" + "description": "Godkend denne app p\u00e5 https://www.ecobee.com/consumerportal/index.html med PIN-kode:\n\n{pin}\n\nTryk derefter p\u00e5 Indsend.", + "title": "Godkend app p\u00e5 ecobee.com" }, "user": { "data": { diff --git a/homeassistant/components/elgato/.translations/nl.json b/homeassistant/components/elgato/.translations/nl.json new file mode 100644 index 00000000000000..ca05983eeb57d8 --- /dev/null +++ b/homeassistant/components/elgato/.translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Dit Elgato Key Light apparaat is al geconfigureerd.", + "connection_error": "Kan geen verbinding maken met het Elgato Key Light apparaat." + }, + "error": { + "connection_error": "Kan geen verbinding maken met het Elgato Key Light apparaat." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Hostnaam of IP-adres", + "port": "Poortnummer" + }, + "description": "Stel uw Elgato Key Light in om te integreren met Home Assistant.", + "title": "Koppel uw Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Wilt u de Elgato Key Light met serienummer ` {serial_number} ` toevoegen aan Home Assistant?", + "title": "Elgato Key Light apparaat ontdekt" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/da.json b/homeassistant/components/emulated_roku/.translations/da.json index 0479dee437d6ef..0da64fac623a48 100644 --- a/homeassistant/components/emulated_roku/.translations/da.json +++ b/homeassistant/components/emulated_roku/.translations/da.json @@ -6,14 +6,14 @@ "step": { "user": { "data": { - "advertise_ip": "Adviserings IP", - "advertise_port": "Adviserings port", - "host_ip": "V\u00e6rt IP", - "listen_port": "Lytte port", + "advertise_ip": "Adviserings-IP", + "advertise_port": "Adviseringsport", + "host_ip": "V\u00e6rts-IP", + "listen_port": "Lytte-port", "name": "Navn", "upnp_bind_multicast": "Bind multicast (sand/falsk)" }, - "title": "Angiv server konfiguration" + "title": "Angiv server-konfiguration" } }, "title": "EmulatedRoku" diff --git a/homeassistant/components/esphome/.translations/da.json b/homeassistant/components/esphome/.translations/da.json index ba84ab40301a29..db4b4362a5e0f7 100644 --- a/homeassistant/components/esphome/.translations/da.json +++ b/homeassistant/components/esphome/.translations/da.json @@ -18,8 +18,8 @@ "title": "Indtast adgangskode" }, "discovery_confirm": { - "description": "Vil du tilf\u00f8je ESPHome node `{name}` til Home Assistant?", - "title": "Fandt ESPHome node" + "description": "Vil du tilf\u00f8je ESPHome-knudepunkt `{name}` til Home Assistant?", + "title": "Fandt ESPHome-knudepunkt" }, "user": { "data": { diff --git a/homeassistant/components/geofency/.translations/da.json b/homeassistant/components/geofency/.translations/da.json index 21ff2e9fcedfea..6e9443af5e898e 100644 --- a/homeassistant/components/geofency/.translations/da.json +++ b/homeassistant/components/geofency/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Geofency meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Geofency-meddelelser.", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { diff --git a/homeassistant/components/geonetnz_volcano/.translations/nl.json b/homeassistant/components/geonetnz_volcano/.translations/nl.json index 73c7c1eaab358d..44d814b9db2010 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/nl.json +++ b/homeassistant/components/geonetnz_volcano/.translations/nl.json @@ -10,6 +10,7 @@ }, "title": "Vul uw filtergegevens in." } - } + }, + "title": "GeoNet NZ Volcano" } } \ No newline at end of file diff --git a/homeassistant/components/gpslogger/.translations/da.json b/homeassistant/components/gpslogger/.translations/da.json index 4aaebb7aa82a5e..b118783cd3c1d0 100644 --- a/homeassistant/components/gpslogger/.translations/da.json +++ b/homeassistant/components/gpslogger/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage GPSLogger meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage GPSLogger-meddelelser.", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { diff --git a/homeassistant/components/hangouts/.translations/da.json b/homeassistant/components/hangouts/.translations/da.json index 4155da38f8f495..2ceb78ddde8c77 100644 --- a/homeassistant/components/hangouts/.translations/da.json +++ b/homeassistant/components/hangouts/.translations/da.json @@ -5,7 +5,7 @@ "unknown": "Ukendt fejl opstod" }, "error": { - "invalid_2fa": "Ugyldig 2-faktor godkendelse, pr\u00f8v venligst igen.", + "invalid_2fa": "Ugyldig tofaktor-godkendelse, pr\u00f8v igen.", "invalid_2fa_method": "Ugyldig 2FA-metode (Bekr\u00e6ft p\u00e5 telefon).", "invalid_login": "Ugyldig login, pr\u00f8v venligst igen." }, @@ -14,12 +14,12 @@ "data": { "2fa": "2FA pin" }, - "title": "To-faktor autentificering" + "title": "Tofaktor-godkendelse" }, "user": { "data": { - "authorization_code": "Autorisationskode (kr\u00e6ves til manuel godkendelse)", - "email": "Email adresse", + "authorization_code": "Godkendelseskode (kr\u00e6vet til manuel godkendelse)", + "email": "Emailadresse", "password": "Adgangskode" }, "title": "Google Hangouts login" diff --git a/homeassistant/components/hisense_aehw4a1/.translations/nl.json b/homeassistant/components/hisense_aehw4a1/.translations/nl.json new file mode 100644 index 00000000000000..7360908a11d432 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen Hisense AEH-W4A1-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Slechts een enkele configuratie van Hisense AEH-W4A1 is mogelijk." + }, + "step": { + "confirm": { + "description": "Wilt u Hisense AEH-W4A1 instellen?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/da.json b/homeassistant/components/homekit_controller/.translations/da.json index 2bcda4fb1ad372..20b209752eb891 100644 --- a/homeassistant/components/homekit_controller/.translations/da.json +++ b/homeassistant/components/homekit_controller/.translations/da.json @@ -3,38 +3,38 @@ "abort": { "accessory_not_found_error": "Parring kan ikke tilf\u00f8jes da enheden ikke l\u00e6ngere findes.", "already_configured": "Tilbeh\u00f8ret er allerede konfigureret med denne controller.", - "already_in_progress": "Enheds konfiguration er allerede i gang.", + "already_in_progress": "Enhedskonfiguration er allerede i gang.", "already_paired": "Dette tilbeh\u00f8r er allerede parret med en anden enhed. Nulstil tilbeh\u00f8ret og pr\u00f8v igen.", - "ignored_model": "HomeKit underst\u00f8ttelse til denne model er blokeret da en mere komplet native integration er til r\u00e5dighed.", + "ignored_model": "HomeKit-underst\u00f8ttelse af denne model er blokeret, da en mere funktionskomplet indbygget integration er tilg\u00e6ngelig.", "invalid_config_entry": "Denne enhed vises som klar til parring, men der er allerede en modstridende konfigurationspost for den i Home Assistant, som f\u00f8rst skal fjernes.", "no_devices": "Der blev ikke fundet nogen uparrede enheder" }, "error": { - "authentication_error": "Forkert HomeKit kode. Kontroller den og pr\u00f8v igen.", - "busy_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den allerede parrer med en anden controller.", - "max_peers_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den ikke har nok frit parrings lager.", - "max_tries_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den har modtaget mere end 100 mislykkede godkendelsesfors\u00f8g.", + "authentication_error": "Forkert HomeKit-kode. Kontroller den og pr\u00f8v igen.", + "busy_error": "Enheden n\u00e6gtede at parre da den allerede er parret med en anden controller.", + "max_peers_error": "Enheden n\u00e6gtede at parre da den ikke har nok frit parringslagerplads.", + "max_tries_error": "Enheden n\u00e6gtede at parre da den har modtaget mere end 100 mislykkede godkendelsesfors\u00f8g.", "pairing_failed": "En uh\u00e5ndteret fejl opstod under fors\u00f8g p\u00e5 at parre med denne enhed. Dette kan v\u00e6re en midlertidig fejl eller din enhed muligvis ikke underst\u00f8ttes i \u00f8jeblikket.", "unable_to_pair": "Kunne ikke parre, pr\u00f8v venligst igen.", "unknown_error": "Enhed rapporterede en ukendt fejl. Parring mislykkedes." }, - "flow_title": "HomeKit tilbeh\u00f8r: {name}", + "flow_title": "HomeKit-tilbeh\u00f8r: {name}", "step": { "pair": { "data": { "pairing_code": "Parringskode" }, - "description": "Indtast din HomeKit parringskode (i formatet XXX-XX-XXX) for at bruge dette tilbeh\u00f8r", - "title": "Par med HomeKit tilbeh\u00f8r" + "description": "Indtast din HomeKit-parringskode (i formatet XXX-XX-XXX) for at bruge dette tilbeh\u00f8r", + "title": "Par med HomeKit-tilbeh\u00f8r" }, "user": { "data": { "device": "Enhed" }, "description": "V\u00e6lg den enhed du vil parre med", - "title": "Par med HomeKit tilbeh\u00f8r" + "title": "Par med HomeKit-tilbeh\u00f8r" } }, - "title": "HomeKit tilbeh\u00f8r" + "title": "HomeKit-tilbeh\u00f8r" } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/da.json b/homeassistant/components/huawei_lte/.translations/da.json index b693d414ceb484..19bc69b77fd75d 100644 --- a/homeassistant/components/huawei_lte/.translations/da.json +++ b/homeassistant/components/huawei_lte/.translations/da.json @@ -1,18 +1,41 @@ { "config": { "abort": { + "already_configured": "Denne enhed er allerede konfigureret", "already_in_progress": "Denne enhed er allerede ved at blive konfigureret", "not_huawei_lte": "Ikke en Huawei LTE-enhed" }, "error": { - "connection_timeout": "Timeout for forbindelse" - } + "connection_failed": "Forbindelsen mislykkedes", + "connection_timeout": "Timeout for forbindelse", + "incorrect_password": "Forkert adgangskode", + "incorrect_username": "Forkert brugernavn", + "incorrect_username_or_password": "Forkert brugernavn eller adgangskode", + "invalid_url": "Ugyldig webadresse", + "login_attempts_exceeded": "Maksimale loginfors\u00f8g overskredet. Pr\u00f8v igen senere", + "response_error": "Ukendt fejl fra enheden", + "unknown_connection_error": "Ukendt fejl ved tilslutning til enheden" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "url": "Webadresse", + "username": "Brugernavn" + }, + "description": "Indtast oplysninger om enhedsadgang. Det er valgfrit at specificere brugernavn og adgangskode, men muligg\u00f8r underst\u00f8ttelse af flere integrationsfunktioner. P\u00e5 den anden side kan brug af en autoriseret forbindelse for\u00e5rsage problemer med at f\u00e5 adgang til enhedens webgr\u00e6nseflade uden for Home Assistant, mens integrationen er aktiv, og omvendt.", + "title": "Konfigurer Huawei LTE" + } + }, + "title": "Huawei LTE" }, "options": { "step": { "init": { "data": { - "name": "Navn p\u00e5 meddelelsestjeneste (\u00e6ndring kr\u00e6ver genstart)" + "name": "Navn p\u00e5 meddelelsestjeneste (\u00e6ndring kr\u00e6ver genstart)", + "recipient": "Modtagere af SMS-meddelelse", + "track_new_devices": "Spor nye enheder" } } } diff --git a/homeassistant/components/huawei_lte/.translations/ko.json b/homeassistant/components/huawei_lte/.translations/ko.json index a9ac8d7f62c9e6..f6b3d8556793a8 100644 --- a/homeassistant/components/huawei_lte/.translations/ko.json +++ b/homeassistant/components/huawei_lte/.translations/ko.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc774\ub984 (\ubcc0\uacbd \uc2dc \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud568)", "recipient": "SMS \uc54c\ub9bc \uc218\uc2e0\uc790", "track_new_devices": "\uc0c8\ub85c\uc6b4 \uae30\uae30 \ucd94\uc801" } diff --git a/homeassistant/components/huawei_lte/.translations/nl.json b/homeassistant/components/huawei_lte/.translations/nl.json index 6d5e5c3e957b70..297ec922abf110 100644 --- a/homeassistant/components/huawei_lte/.translations/nl.json +++ b/homeassistant/components/huawei_lte/.translations/nl.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Naam meldingsservice (wijziging vereist opnieuw opstarten)", "recipient": "Ontvangers van sms-berichten", "track_new_devices": "Volg nieuwe apparaten" } diff --git a/homeassistant/components/huawei_lte/.translations/no.json b/homeassistant/components/huawei_lte/.translations/no.json index 35a5d531c5dd20..39cb5bf87fe05f 100644 --- a/homeassistant/components/huawei_lte/.translations/no.json +++ b/homeassistant/components/huawei_lte/.translations/no.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Navn p\u00e5 varslingstjeneste (endring krever omstart)", "recipient": "Mottakere av SMS-varsling", "track_new_devices": "Spor nye enheter" } diff --git a/homeassistant/components/hue/.translations/da.json b/homeassistant/components/hue/.translations/da.json index 756f3b7e44bc5f..afcfd7071e7e77 100644 --- a/homeassistant/components/hue/.translations/da.json +++ b/homeassistant/components/hue/.translations/da.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "all_configured": "Alle Philips Hue brigdes er konfigureret", + "all_configured": "Alle Philips Hue-broer er allerede konfigureret", "already_configured": "Bridgen er allerede konfigureret", - "already_in_progress": "Bro konfiguration er allerede i gang.", + "already_in_progress": "Bro-konfiguration er allerede i gang.", "cannot_connect": "Kunne ikke oprette forbindelse til bridgen", - "discover_timeout": "Ingen Philips Hue bridge fundet", - "no_bridges": "Ingen Philips Hue bridge fundet", - "not_hue_bridge": "Ikke en Hue bro", + "discover_timeout": "Ingen Philips Hue-bro fundet", + "no_bridges": "Ingen Philips Hue-broer fundet", + "not_hue_bridge": "Ikke en Hue-bro", "unknown": "Ukendt fejl opstod" }, "error": { - "linking": "Ukendt sammenkoblings fejl opstod", + "linking": "Der opstod en ukendt linkfejl.", "register_failed": "Det lykkedes ikke at registrere, pr\u00f8v igen" }, "step": { @@ -22,8 +22,8 @@ "title": "V\u00e6lg Hue bridge" }, "link": { - "description": "Tryk p\u00e5 knappen p\u00e5 bridgen for at registrere Philips Hue med Home Assistant. \n\n ! [Placering af knap p\u00e5 bro] (/static/images/config_philips_hue.jpg)", - "title": "Link Hub" + "description": "Tryk p\u00e5 knappen p\u00e5 broen for at registrere Philips Hue med Home Assistant. \n\n ![Placering af knap p\u00e5 bro](/static/images/config_philips_hue.jpg)", + "title": "Forbind Hub" } }, "title": "Philips Hue" diff --git a/homeassistant/components/icloud/.translations/nl.json b/homeassistant/components/icloud/.translations/nl.json new file mode 100644 index 00000000000000..d35496b171b073 --- /dev/null +++ b/homeassistant/components/icloud/.translations/nl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Account reeds geconfigureerd" + }, + "error": { + "login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord", + "send_verification_code": "Kan verificatiecode niet verzenden", + "username_exists": "Account reeds geconfigureerd", + "validate_verification_code": "Kan uw verificatiecode niet verifi\u00ebren, kies een vertrouwensapparaat en start de verificatie opnieuw" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Vertrouwd apparaat" + }, + "description": "Selecteer uw vertrouwde apparaat", + "title": "iCloud vertrouwd apparaat" + }, + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mail" + }, + "description": "Voer uw gegevens in", + "title": "iCloud inloggegevens" + }, + "verification_code": { + "data": { + "verification_code": "Verificatiecode" + }, + "description": "Voer de verificatiecode in die u zojuist van iCloud hebt ontvangen", + "title": "iCloud verificatiecode" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/.translations/da.json b/homeassistant/components/ifttt/.translations/da.json index 7e05d9389997a5..0e0c735eb897e8 100644 --- a/homeassistant/components/ifttt/.translations/da.json +++ b/homeassistant/components/ifttt/.translations/da.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage IFTTT meddelelser.", - "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage IFTTT-meddelelser", + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "create_entry": { "default": "For at sende h\u00e6ndelser til Home Assistant skal du bruge handlingen \"Foretag en web-foresp\u00f8rgsel\" fra [IFTTT Webhook-applet] ({applet_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." }, "step": { "user": { - "description": "Er du sikker p\u00e5 at du vil oprette IFTTT?", + "description": "Er du sikker p\u00e5, at du vil konfigurere IFTTT?", "title": "Konfigurer IFTTT Webhook Applet" } }, diff --git a/homeassistant/components/ipma/.translations/da.json b/homeassistant/components/ipma/.translations/da.json index 080c41429ba213..017aff4d0ec34f 100644 --- a/homeassistant/components/ipma/.translations/da.json +++ b/homeassistant/components/ipma/.translations/da.json @@ -11,7 +11,7 @@ "name": "Navn" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", - "title": "Beliggenhed" + "title": "Lokalitet" } }, "title": "Portugisisk vejrservice (IPMA)" diff --git a/homeassistant/components/life360/.translations/da.json b/homeassistant/components/life360/.translations/da.json index 933fce4a4e8798..32acc488dc6921 100644 --- a/homeassistant/components/life360/.translations/da.json +++ b/homeassistant/components/life360/.translations/da.json @@ -20,7 +20,7 @@ "username": "Brugernavn" }, "description": "Hvis du vil angive avancerede indstillinger skal du se [Life360 dokumentation]({docs_url}).\nDu \u00f8nsker m\u00e5ske at g\u00f8re dette f\u00f8r du tilf\u00f8jer konti.", - "title": "Life360 kontooplysninger" + "title": "Life360-kontooplysninger" } }, "title": "Life360" diff --git a/homeassistant/components/lifx/.translations/da.json b/homeassistant/components/lifx/.translations/da.json index ffd8e20ce427b7..99143f38c98abe 100644 --- a/homeassistant/components/lifx/.translations/da.json +++ b/homeassistant/components/lifx/.translations/da.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Ingen LIFX enheder kunne findes p\u00e5 netv\u00e6rket.", - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af LIFX." + "no_devices_found": "Der blev ikke fundet nogen LIFX-enheder p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Kun en enkelt konfiguration af LIFX er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json index 14a747f6effeb8..eefa1e8bb6e197 100644 --- a/homeassistant/components/light/.translations/da.json +++ b/homeassistant/components/light/.translations/da.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "toggle": "Skift {entity_name}", + "turn_off": "Sluk {entity_name}", + "turn_on": "T\u00e6nd for {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er fra", + "is_on": "{entity_name} er til" + }, "trigger_type": { - "turned_off": "{entity_name} slukket", - "turned_on": "{entity_name} t\u00e6ndt" + "turned_off": "{entity_name} slukkede", + "turned_on": "{entity_name} t\u00e6ndte" } } } \ No newline at end of file diff --git a/homeassistant/components/locative/.translations/da.json b/homeassistant/components/locative/.translations/da.json index 8211d52fa5dea1..3752b23bbe3870 100644 --- a/homeassistant/components/locative/.translations/da.json +++ b/homeassistant/components/locative/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Geofency meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Geofency-meddelelser.", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { diff --git a/homeassistant/components/lock/.translations/da.json b/homeassistant/components/lock/.translations/da.json index 517c86444fd5ba..e2f2588349cc68 100644 --- a/homeassistant/components/lock/.translations/da.json +++ b/homeassistant/components/lock/.translations/da.json @@ -2,7 +2,8 @@ "device_automation": { "action_type": { "lock": "L\u00e5s {entity_name}", - "open": "\u00c5ben {entity_name}" + "open": "\u00c5bn {entity_name}", + "unlock": "L\u00e5s {entity_name} op" }, "condition_type": { "is_locked": "{entity_name} er l\u00e5st", diff --git a/homeassistant/components/logi_circle/.translations/da.json b/homeassistant/components/logi_circle/.translations/da.json index 9de8d707ad41b6..1f2a96fe5b448d 100644 --- a/homeassistant/components/logi_circle/.translations/da.json +++ b/homeassistant/components/logi_circle/.translations/da.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_setup": "Du kan kun konfigurere en enkelt Logi Circle konto.", - "external_error": "Der opstod en undtagelse fra et andet flow.", + "already_setup": "Du kan kun konfigurere en enkelt Logi Circle-konto.", + "external_error": "Undtagelse skete fra et andet flow.", "external_setup": "Logi Circle er konfigureret med succes fra et andet flow.", "no_flows": "Du skal konfigurere Logi Circle f\u00f8r du kan godkende med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/logi_circle/)." }, @@ -24,7 +24,7 @@ "flow_impl": "Udbyder" }, "description": "V\u00e6lg via hvilken godkendelsesudbyder du vil godkende med Logi Circle.", - "title": "Godkendelses udbyder" + "title": "Godkendelsesudbyder" } }, "title": "Logi Circle" diff --git a/homeassistant/components/luftdaten/.translations/da.json b/homeassistant/components/luftdaten/.translations/da.json index d43fc1128ae5f0..3a5f5e7b409931 100644 --- a/homeassistant/components/luftdaten/.translations/da.json +++ b/homeassistant/components/luftdaten/.translations/da.json @@ -9,7 +9,7 @@ "user": { "data": { "show_on_map": "Vis p\u00e5 kort", - "station_id": "Luftdaten Sensor ID" + "station_id": "Luftdaten sensor-id" }, "title": "Definer Luftdaten" } diff --git a/homeassistant/components/lutron_caseta/.translations/nl.json b/homeassistant/components/lutron_caseta/.translations/nl.json new file mode 100644 index 00000000000000..cfc3c290afeebd --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/nl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/da.json b/homeassistant/components/mailgun/.translations/da.json index 475d560bad62bb..f91526337066f5 100644 --- a/homeassistant/components/mailgun/.translations/da.json +++ b/homeassistant/components/mailgun/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Mailgun meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Mailgun-meddelelser", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." }, "create_entry": { diff --git a/homeassistant/components/media_player/.translations/da.json b/homeassistant/components/media_player/.translations/da.json new file mode 100644 index 00000000000000..a53bbed07d01c5 --- /dev/null +++ b/homeassistant/components/media_player/.translations/da.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} er inaktiv", + "is_off": "{entity_name} er slukket", + "is_on": "{entity_name} er t\u00e6ndt", + "is_paused": "{entity_name} er sat p\u00e5 pause", + "is_playing": "{entity_name} afspiller" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/da.json b/homeassistant/components/met/.translations/da.json index 9d44f1b2b6c4f0..e36a6511aa312a 100644 --- a/homeassistant/components/met/.translations/da.json +++ b/homeassistant/components/met/.translations/da.json @@ -12,7 +12,7 @@ "name": "Navn" }, "description": "Meteorologisk institutt", - "title": "Placering" + "title": "Lokalitet" } }, "title": "Met.no" diff --git a/homeassistant/components/mobile_app/.translations/da.json b/homeassistant/components/mobile_app/.translations/da.json index 551e9957254e97..54dc85e7255e36 100644 --- a/homeassistant/components/mobile_app/.translations/da.json +++ b/homeassistant/components/mobile_app/.translations/da.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "install_app": "\u00c5bn Mobile App for at konfigurere integrationen med Home Assistant. Se [dokumentationen]({apps_url}) for at f\u00e5 en liste over kompatible apps." + "install_app": "\u00c5bn mobilappen for at konfigurere integrationen med Home Assistant. Se [dokumentationen]({apps_url}) for at f\u00e5 vist en liste over kompatible apps." }, "step": { "confirm": { - "description": "Vil du konfigurere Mobile App komponenten?", - "title": "Mobile App" + "description": "Vil du konfigurere mobilapp-komponenten?", + "title": "Mobilapp" } }, - "title": "Mobile App" + "title": "Mobilapp" } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/da.json b/homeassistant/components/mqtt/.translations/da.json index ebe5696f514b8f..93ea57d49ea9a1 100644 --- a/homeassistant/components/mqtt/.translations/da.json +++ b/homeassistant/components/mqtt/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af MQTT" + "single_instance_allowed": "Kun en enkelt konfiguration af MQTT er tilladt." }, "error": { "cannot_connect": "Kunne ikke oprette forbindelse til broker" @@ -10,20 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Aktiv\u00e9r opdagelse", + "discovery": "Aktiv\u00e9r automatisk fund", "password": "Adgangskode", "port": "Port", "username": "Brugernavn" }, - "description": "Indtast venligst forbindelsesindstillinger for din MQTT broker.", + "description": "Indtast venligst forbindelsesindstillinger for din MQTT-broker.", "title": "MQTT" }, "hassio_confirm": { "data": { "discovery": "Aktiv\u00e9r opdagelse" }, - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til MQTT brokeren, der leveres af hass.io add-on {addon}?", - "title": "MQTT Broker via Hass.io add-on" + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til MQTT-brokeren, der leveres af hass.io-tilf\u00f8jelsen {addon}?", + "title": "MQTT-broker via Hass.io-tilf\u00f8jelse" } }, "title": "MQTT" diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json index ca180efa005966..736234e92da95f 100644 --- a/homeassistant/components/neato/.translations/da.json +++ b/homeassistant/components/neato/.translations/da.json @@ -5,7 +5,7 @@ "invalid_credentials": "Ugyldige legitimationsoplysninger" }, "create_entry": { - "default": "Se [Neato-dokumentation] ({docs_url})." + "default": "Se [Neato-dokumentation]({docs_url})." }, "error": { "invalid_credentials": "Ugyldige legitimationsoplysninger", @@ -15,10 +15,11 @@ "user": { "data": { "password": "Adgangskode", - "username": "Brugernavn" + "username": "Brugernavn", + "vendor": "Udbyder" }, - "description": "Se [Neato-dokumentation] ({docs_url}).", - "title": "Neato kontooplysninger" + "description": "Se [Neato-dokumentation]({docs_url}).", + "title": "Neato-kontooplysninger" } }, "title": "Neato" diff --git a/homeassistant/components/nest/.translations/da.json b/homeassistant/components/nest/.translations/da.json index 7dfd1c8b250f62..39b85754c18360 100644 --- a/homeassistant/components/nest/.translations/da.json +++ b/homeassistant/components/nest/.translations/da.json @@ -18,14 +18,14 @@ "flow_impl": "Udbyder" }, "description": "V\u00e6lg hvilken godkendelsesudbyder du vil godkende med Nest.", - "title": "Godkendelses udbyder" + "title": "Godkendelsesudbyder" }, "link": { "data": { "code": "PIN-kode" }, "description": "For at forbinde din Nest-konto, [godkend din konto]({url}). \n\nEfter godkendelse skal du kopiere pin koden nedenfor.", - "title": "Link Nest-konto" + "title": "Forbind Nest-konto" } }, "title": "Nest" diff --git a/homeassistant/components/notion/.translations/da.json b/homeassistant/components/notion/.translations/da.json index 2373920effea6b..bf17b41d777c44 100644 --- a/homeassistant/components/notion/.translations/da.json +++ b/homeassistant/components/notion/.translations/da.json @@ -9,7 +9,7 @@ "user": { "data": { "password": "Adgangskode", - "username": "Brugernavn/e-mail adresse" + "username": "Brugernavn/e-mailadresse" }, "title": "Udfyld dine oplysninger" } diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json index 152e38a5bba050..743adb715f68fa 100644 --- a/homeassistant/components/opentherm_gw/.translations/da.json +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -3,14 +3,17 @@ "error": { "already_configured": "Gateway allerede konfigureret", "id_exists": "Gateway-id findes allerede", - "serial_error": "Fejl ved tilslutning til enheden" + "serial_error": "Fejl ved tilslutning til enheden", + "timeout": "Forbindelsesfors\u00f8g fik timeout" }, "step": { "init": { "data": { - "device": "Sti eller URL", - "id": "ID", - "name": "Navn" + "device": "Sti eller webadresse", + "floor_temperature": "Gulvklima-temperatur", + "id": "Id", + "name": "Navn", + "precision": "Klimatemperatur-pr\u00e6cision" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/openuv/.translations/da.json b/homeassistant/components/openuv/.translations/da.json index a783c8646e0e50..eaf2e127026994 100644 --- a/homeassistant/components/openuv/.translations/da.json +++ b/homeassistant/components/openuv/.translations/da.json @@ -2,12 +2,12 @@ "config": { "error": { "identifier_exists": "Koordinater er allerede registreret", - "invalid_api_key": "Ugyldig API n\u00f8gle" + "invalid_api_key": "Ugyldig API-n\u00f8gle" }, "step": { "user": { "data": { - "api_key": "OpenUV API N\u00f8gle", + "api_key": "OpenUV API-n\u00f8gle", "elevation": "Elevation", "latitude": "Breddegrad", "longitude": "L\u00e6ngdegrad" diff --git a/homeassistant/components/owntracks/.translations/da.json b/homeassistant/components/owntracks/.translations/da.json index bc1328d57e4cde..110f60193e6d98 100644 --- a/homeassistant/components/owntracks/.translations/da.json +++ b/homeassistant/components/owntracks/.translations/da.json @@ -4,7 +4,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { - "default": "\n\nP\u00e5 Android skal du \u00e5bne [OwnTracks applikationen]({android_url}), g\u00e5 til indstillinger -> forbindelse. Skift f\u00f8lgende indstillinger: \n - Tilstand: Privat HTTP\n - V\u00e6rt: {webhook_url}\n - Identifikation:\n - Brugernavn: ` ` \n - Enheds-id: ` ` \n\nP\u00e5 iOS skal du \u00e5bne [OwnTracks applikationen]({ios_url}), tryk p\u00e5 (i) ikonet \u00f8verst til venstre -> indstillinger. Skift f\u00f8lgende indstillinger: \n - Tilstand: HTTP\n - URL: {webhook_url}\n - Aktiver godkendelse \n - Bruger ID: ` ` \n\n {secret}\n \n Se [dokumentationen]({docs_url}) for at f\u00e5 flere oplysninger." + "default": "\n\nP\u00e5 Android skal du \u00e5bne [OwnTracks-appen]({android_url}), g\u00e5 til indstillinger -> forbindelse. Skift f\u00f8lgende indstillinger: \n - Tilstand: Privat HTTP\n - V\u00e6rt: {webhook_url}\n - Identifikation:\n - Brugernavn: ` ` \n - Enheds-id: ` ` \n\nP\u00e5 iOS skal du \u00e5bne [OwnTracks-appen]({ios_url}), tryk p\u00e5 (i) ikonet \u00f8verst til venstre -> indstillinger. Skift f\u00f8lgende indstillinger: \n - Tilstand: HTTP\n - URL: {webhook_url}\n - Aktiver godkendelse \n - Bruger ID: ` ` \n\n {secret}\n \n Se [dokumentationen]({docs_url}) for at f\u00e5 flere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/da.json b/homeassistant/components/plaato/.translations/da.json index f8d59572388591..c4dc5ae178d04b 100644 --- a/homeassistant/components/plaato/.translations/da.json +++ b/homeassistant/components/plaato/.translations/da.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_internet_accessible": "Din Home Assistant instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage meddelelser fra Plaato Airlock.", - "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage meddelelser fra Plaato Airlock.", + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "create_entry": { "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i Plaato Airlock.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 2c3a20ae94f9a2..18dbbb840c3f37 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -4,7 +4,7 @@ "all_configured": "Alle linkede servere er allerede konfigureret", "already_configured": "Denne Plex-server er allerede konfigureret", "already_in_progress": "Plex konfigureres", - "discovery_no_file": "Der blev ikke fundet nogen legacy konfigurationsfil", + "discovery_no_file": "Der blev ikke fundet nogen \u00e6ldre konfigurationsfil", "invalid_import": "Importeret konfiguration er ugyldig", "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Timeout ved hentning af token", @@ -35,13 +35,13 @@ "title": "V\u00e6lg Plex-server" }, "start_website_auth": { - "description": "Forts\u00e6t for at autorisere p\u00e5 plex.tv.", - "title": "Tilslut Plex-server" + "description": "Forts\u00e6t for at godkende p\u00e5 plex.tv.", + "title": "Forbind Plex-server" }, "user": { "data": { "manual_setup": "Manuel ops\u00e6tning", - "token": "Plex token" + "token": "Plex-token" }, "description": "Indtast et Plex-token til automatisk ops\u00e6tning eller konfigurerer en server manuelt.", "title": "Tilslut Plex-server" @@ -54,7 +54,7 @@ "plex_mp_settings": { "data": { "show_all_controls": "Vis alle kontrolelementer", - "use_episode_art": "Brug episode kunst" + "use_episode_art": "Brug episodekunst" }, "description": "Indstillinger for Plex-medieafspillere" } diff --git a/homeassistant/components/plex/.translations/nl.json b/homeassistant/components/plex/.translations/nl.json index c971ebb4762c7c..515ee8798c7d85 100644 --- a/homeassistant/components/plex/.translations/nl.json +++ b/homeassistant/components/plex/.translations/nl.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex wordt geconfigureerd", "discovery_no_file": "Geen legacy configuratiebestand gevonden", "invalid_import": "Ge\u00efmporteerde configuratie is ongeldig", + "non-interactive": "Niet-interactieve import", "token_request_timeout": "Time-out verkrijgen van token", "unknown": "Mislukt om onbekende reden" }, diff --git a/homeassistant/components/point/.translations/da.json b/homeassistant/components/point/.translations/da.json index 109bcbe6c3701e..4b6017ddd0106a 100644 --- a/homeassistant/components/point/.translations/da.json +++ b/homeassistant/components/point/.translations/da.json @@ -24,7 +24,7 @@ "flow_impl": "Udbyder" }, "description": "V\u00e6lg hvilken godkendelsesudbyder du vil godkende med Point.", - "title": "Godkendelses udbyder" + "title": "Godkendelsesudbyder" } }, "title": "Minut Point" diff --git a/homeassistant/components/ps4/.translations/da.json b/homeassistant/components/ps4/.translations/da.json index e9aca23bb436b5..cef13db3150847 100644 --- a/homeassistant/components/ps4/.translations/da.json +++ b/homeassistant/components/ps4/.translations/da.json @@ -3,19 +3,19 @@ "abort": { "credential_error": "Fejl ved hentning af legitimationsoplysninger.", "devices_configured": "Alle de fundne enheder er allerede konfigureret.", - "no_devices_found": "Ingen PlayStation 4 enheder fundet p\u00e5 netv\u00e6rket.", + "no_devices_found": "Der blev ikke fundet nogen PlayStation 4-enheder p\u00e5 netv\u00e6rket.", "port_987_bind_error": "Kunne ikke binde til port 987. Se [dokumentationen](https://www.home-assistant.io/components/ps4/) for yderligere oplysninger.", "port_997_bind_error": "Kunne ikke binde til port 997. Se [dokumentationen](https://www.home-assistant.io/components/ps4/) for yderligere oplysninger." }, "error": { "credential_timeout": "Tjenesten for legitimationsoplysninger fik timeout. Tryk p\u00e5 send for at genstarte.", "login_failed": "Kunne ikke parre med PlayStation 4. Kontroller PIN er korrekt.", - "no_ipaddress": "Indtast IP adressen p\u00e5 den PlayStation 4 du gerne vil konfigurere.", + "no_ipaddress": "Indtast IP-adressen p\u00e5 den PlayStation 4, du gerne vil konfigurere.", "not_ready": "PlayStation 4 er ikke t\u00e6ndt eller tilsluttet til netv\u00e6rket." }, "step": { "creds": { - "description": "Legitimationsoplysninger er n\u00f8dvendige. Tryk p\u00e5 'Send' og derefter i PS4 2nd Screen App, v\u00e6lg opdater enheder og v\u00e6lg 'Home-Assistant' -enheden for at forts\u00e6tte.", + "description": "Der kr\u00e6ves legitimationsoplysninger. Tryk p\u00e5 'Indsend' og derefter i PS4 2. sk\u00e6rm-app, opdater enheder, og v\u00e6lg 'Home Assistant'-enhed for at forts\u00e6tte.", "title": "PlayStation 4" }, "link": { @@ -25,15 +25,15 @@ "name": "Navn", "region": "Omr\u00e5de" }, - "description": "Indtast dine PlayStation 4 oplysninger. For 'PIN' skal du navigere til 'Indstillinger' p\u00e5 din PlayStation 4 konsol. G\u00e5 derefter til 'Indstillinger for mobilapp-forbindelse' og v\u00e6lg 'Tilf\u00f8j enhed'. Indtast den PIN der vises.", + "description": "Indtast dine PlayStation 4-oplysninger. For 'PIN' skal du navigere til 'Indstillinger' p\u00e5 din PlayStation 4-konsol. Naviger derefter til 'Mobile App Connection Settings' og v\u00e6lg 'Add Device'. Indtast den pinkode, der vises. Se [dokumentation](https://www.home-assistant.io/components/ps4/) for yderligere oplysninger.", "title": "PlayStation 4" }, "mode": { "data": { - "ip_address": "IP adresse (Efterlad tom, hvis du bruger Auto Discovery).", + "ip_address": "IP-adresse (lad det v\u00e6re tomt, hvis du bruger automatisk registrering).", "mode": "Konfigurationstilstand" }, - "description": "V\u00e6lg tilstand til konfiguration. IP-adressefeltet kan st\u00e5 tomt hvis du v\u00e6lger Auto Discovery, da enheder automatisk bliver fundet.", + "description": "V\u00e6lg tilstand for konfiguration. IP-adressefeltet kan v\u00e6re tomt, hvis du v\u00e6lger automatisk registrering, da enheder automatisk bliver fundet.", "title": "PlayStation 4" } }, diff --git a/homeassistant/components/rainmachine/.translations/da.json b/homeassistant/components/rainmachine/.translations/da.json index 61d29894fe2516..34f4fff4ed07b9 100644 --- a/homeassistant/components/rainmachine/.translations/da.json +++ b/homeassistant/components/rainmachine/.translations/da.json @@ -8,7 +8,7 @@ "user": { "data": { "ip_address": "V\u00e6rtsnavn eller IP-adresse", - "password": "Password", + "password": "Adgangskode", "port": "Port" }, "title": "Udfyld dine oplysninger" diff --git a/homeassistant/components/sensor/.translations/da.json b/homeassistant/components/sensor/.translations/da.json index df9b9935dc149c..3febed8ac09bc1 100644 --- a/homeassistant/components/sensor/.translations/da.json +++ b/homeassistant/components/sensor/.translations/da.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} batteriniveau", - "is_humidity": "{entity_name} fugtighed", - "is_illuminance": "{entity_name} belysningsstyrke", - "is_power": "{entity_name} str\u00f8m", - "is_pressure": "{entity_name} tryk", - "is_signal_strength": "{entity_name} signalstyrke", - "is_temperature": "{entity_name} temperatur", - "is_timestamp": "{entity_name} tidsstempel", - "is_value": "{entity_name} v\u00e6rdi" + "is_battery_level": "Aktuelt {entity_name}-batteriniveau", + "is_humidity": "Aktuel {entity_name}-luftfugtighed", + "is_illuminance": "Aktuel {entity_name}-lysstyrke", + "is_power": "Aktuel {entity_name}-str\u00f8m", + "is_pressure": "Aktuelt {entity_name}-lufttryk", + "is_signal_strength": "Aktuel {entity_name}-signalstyrke", + "is_temperature": "Aktuel {entity_name}-temperatur", + "is_timestamp": "Aktuel {entity_name}-tidsstempel", + "is_value": "Aktuel {entity_name}-v\u00e6rdi" }, "trigger_type": { - "battery_level": "{entity_name} batteriniveau", - "humidity": "{entity_name} fugtighed", - "illuminance": "{entity_name} belysningsstyrke", - "power": "{entity_name} str\u00f8m", - "pressure": "{entity_name} tryk", - "signal_strength": "{entity_name} signalstyrke", - "temperature": "{entity_name} temperatur", - "timestamp": "{entity_name} tidsstempel", - "value": "{entity_name} v\u00e6rdi" + "battery_level": "{entity_name} batteriniveau \u00e6ndres", + "humidity": "{entity_name} luftfugtighed \u00e6ndres", + "illuminance": "{entity_name} lysstyrke \u00e6ndres", + "power": "{entity_name} str\u00f8m \u00e6ndres", + "pressure": "{entity_name} lufttryk \u00e6ndres", + "signal_strength": "{entity_name} signalstyrke \u00e6ndres", + "temperature": "{entity_name} temperatur \u00e6ndres", + "timestamp": "{entity_name} tidsstempel \u00e6ndres", + "value": "{entity_name} v\u00e6rdi \u00e6ndres" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/da.json b/homeassistant/components/simplisafe/.translations/da.json index 3ec3d7b456cf6f..0d3970eeba565b 100644 --- a/homeassistant/components/simplisafe/.translations/da.json +++ b/homeassistant/components/simplisafe/.translations/da.json @@ -9,7 +9,7 @@ "data": { "code": "Kode (til Home Assistant)", "password": "Adgangskode", - "username": "Email adresse" + "username": "Emailadresse" }, "title": "Udfyld dine oplysninger" } diff --git a/homeassistant/components/smartthings/.translations/da.json b/homeassistant/components/smartthings/.translations/da.json index 18412069394807..04fe2171f39ef6 100644 --- a/homeassistant/components/smartthings/.translations/da.json +++ b/homeassistant/components/smartthings/.translations/da.json @@ -1,9 +1,9 @@ { "config": { "error": { - "app_not_installed": "S\u00f8rg for at du har installeret og autoriseret Home Assistant SmartApp og pr\u00f8v igen.", + "app_not_installed": "S\u00f8rg for, at du har installeret og godkendt Home Assistant SmartApp, og pr\u00f8v igen.", "app_setup_error": "SmartApp kunne ikke konfigureres. Pr\u00f8v igen.", - "base_url_not_https": "`base_url` til` http` komponenten skal konfigureres og starte med `https://`.", + "base_url_not_https": "`base_url` til `http`-komponenten skal konfigureres og starte med `https://`.", "token_already_setup": "Token er allerede konfigureret.", "token_forbidden": "Adgangstoken er ikke indenfor OAuth", "token_invalid_format": "Adgangstoken skal v\u00e6re i UID/GUID format", diff --git a/homeassistant/components/smhi/.translations/da.json b/homeassistant/components/smhi/.translations/da.json index b43fef7ec45c2a..52c4f54ebd7fad 100644 --- a/homeassistant/components/smhi/.translations/da.json +++ b/homeassistant/components/smhi/.translations/da.json @@ -2,7 +2,7 @@ "config": { "error": { "name_exists": "Navnet findes allerede", - "wrong_location": "Placering kun i Sverige" + "wrong_location": "Lokalitet kun i Sverige" }, "step": { "user": { @@ -11,7 +11,7 @@ "longitude": "L\u00e6ngdegrad", "name": "Navn" }, - "title": "Placering i Sverige" + "title": "Lokalitet i Sverige" } }, "title": "Svensk vejr service (SMHI)" diff --git a/homeassistant/components/soma/.translations/nl.json b/homeassistant/components/soma/.translations/nl.json index c1188b0ac632e6..058f7222666f1a 100644 --- a/homeassistant/components/soma/.translations/nl.json +++ b/homeassistant/components/soma/.translations/nl.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "U kunt slechts \u00e9\u00e9n Soma-account configureren.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Soma-component is niet geconfigureerd. Gelieve de documentatie te volgen." + "connection_error": "Kan geen verbinding maken met SOMA Connect.", + "missing_configuration": "De Soma-component is niet geconfigureerd. Gelieve de documentatie te volgen.", + "result_error": "SOMA Connect reageerde met een foutstatus." }, "create_entry": { "default": "Succesvol geverifieerd met Soma." diff --git a/homeassistant/components/sonos/.translations/da.json b/homeassistant/components/sonos/.translations/da.json index c303bca0aa83f2..c4b1a555245ff6 100644 --- a/homeassistant/components/sonos/.translations/da.json +++ b/homeassistant/components/sonos/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Ingen Sonos-enheder kunne findes p\u00e5 netv\u00e6rket.", + "no_devices_found": "Der blev ikke fundet nogen Sonos-enheder p\u00e5 netv\u00e6rket.", "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Sonos" }, "step": { diff --git a/homeassistant/components/starline/.translations/nl.json b/homeassistant/components/starline/.translations/nl.json new file mode 100644 index 00000000000000..d79634468655bc --- /dev/null +++ b/homeassistant/components/starline/.translations/nl.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Onjuiste applicatie-ID of geheim", + "error_auth_mfa": "Ongeldige code", + "error_auth_user": "Ongeldige gebruikersnaam of wachtwoord" + }, + "step": { + "auth_app": { + "data": { + "app_id": "Toepassings-ID ", + "app_secret": "Geheime code" + }, + "description": "Toepassings-ID en de geheime code van StarLine developer account", + "title": "Inloggegevens van de applicatie" + }, + "auth_captcha": { + "data": { + "captcha_code": "Code van afbeelding" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS code" + }, + "description": "Voer de code in die wordt verzonden naar telefoon {phone_number}", + "title": "Tweestapsverificatie" + }, + "auth_user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "StarLine-account e-mailadres en wachtwoord", + "title": "Gebruikersgegevens" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/da.json b/homeassistant/components/switch/.translations/da.json new file mode 100644 index 00000000000000..2514a56a010c7d --- /dev/null +++ b/homeassistant/components/switch/.translations/da.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Skift {entity_name}", + "turn_off": "Sluk {entity_name}", + "turn_on": "T\u00e6nd for {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er fra", + "is_on": "{entity_name} er til", + "turn_off": "{entity_name} slukket", + "turn_on": "{entity_name} t\u00e6ndt" + }, + "trigger_type": { + "turned_off": "{entity_name} slukkede", + "turned_on": "{entity_name} t\u00e6ndte" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/nl.json b/homeassistant/components/tesla/.translations/nl.json new file mode 100644 index 00000000000000..5f3e83dd24846c --- /dev/null +++ b/homeassistant/components/tesla/.translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Fout bij verbinden; controleer het netwerk en probeer het opnieuw", + "identifier_exists": "E-mail al geregistreerd", + "invalid_credentials": "Ongeldige inloggegevens", + "unknown_error": "Onbekende fout, meldt u log info" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mailadres" + }, + "description": "Vul alstublieft uw gegevens in.", + "title": "Tesla - Configuratie" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconden tussen scans" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/no.json b/homeassistant/components/tesla/.translations/no.json new file mode 100644 index 00000000000000..0d73908f417337 --- /dev/null +++ b/homeassistant/components/tesla/.translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Feil ved tilkobling; sjekk nettverket og pr\u00f8v p\u00e5 nytt", + "identifier_exists": "E-post er allerede registrert", + "invalid_credentials": "Ugyldig brukerinformasjon", + "unknown_error": "Ukjent feil, Vennligst rapporter informasjon fra Loggen" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-postadresse" + }, + "description": "Vennligst skriv inn informasjonen din.", + "title": "Tesla - Konfigurasjon" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunder mellom skanninger" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/da.json b/homeassistant/components/toon/.translations/da.json index 9200f80add0ae9..e4f73bc7c6bff0 100644 --- a/homeassistant/components/toon/.translations/da.json +++ b/homeassistant/components/toon/.translations/da.json @@ -3,9 +3,9 @@ "abort": { "client_id": "Klient-id'et fra konfigurationen er ugyldigt.", "client_secret": "Klientens hemmelighed fra konfigurationen er ugyldig.", - "no_agreements": "Denne konto har ingen Toon sk\u00e6rme.", + "no_agreements": "Denne konto har ingen Toon-sk\u00e6rme.", "no_app": "Du skal konfigurere Toon f\u00f8r du kan godkende med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Der opstod en uventet fejl under autentificering." + "unknown_auth_fail": "Der opstod en uventet fejl under godkendelse." }, "error": { "credentials": "De angivne legitimationsoplysninger er ugyldige.", @@ -18,8 +18,8 @@ "tenant": "Tenant", "username": "Brugernavn" }, - "description": "Godkend med din Eneco Toon konto (ikke udviklerkontoen).", - "title": "Link din Toon konto" + "description": "Godkend med din Eneco Toon-konto (ikke udviklerkontoen).", + "title": "Forbind din Toon-konto" }, "display": { "data": { diff --git a/homeassistant/components/tplink/.translations/da.json b/homeassistant/components/tplink/.translations/da.json index cdd953ff5c33e9..5225a89fb95416 100644 --- a/homeassistant/components/tplink/.translations/da.json +++ b/homeassistant/components/tplink/.translations/da.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere TP-Link smart devices?", + "description": "Vil du konfigurere TP-Link-smartenheder?", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/traccar/.translations/da.json b/homeassistant/components/traccar/.translations/da.json index 2b0ec0003d6800..b1ab350c905870 100644 --- a/homeassistant/components/traccar/.translations/da.json +++ b/homeassistant/components/traccar/.translations/da.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Traccar meddelelser.", - "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Traccar-meddelelser.", + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "create_entry": { "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i Traccar.\n\nBrug f\u00f8lgende webadresse: `{webhook_url}`\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." diff --git a/homeassistant/components/traccar/.translations/tr.json b/homeassistant/components/traccar/.translations/tr.json new file mode 100644 index 00000000000000..22944e1c4cc631 --- /dev/null +++ b/homeassistant/components/traccar/.translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Traccar'\u0131 kur" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/da.json b/homeassistant/components/transmission/.translations/da.json index b14fca00c2cdd0..e84ec938ee2957 100644 --- a/homeassistant/components/transmission/.translations/da.json +++ b/homeassistant/components/transmission/.translations/da.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "V\u00e6rten er allerede konfigureret.", - "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "error": { "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", @@ -14,7 +14,7 @@ "data": { "scan_interval": "Opdateringsfrekvens" }, - "title": "Konfigurer indstillinger" + "title": "Konfigurationsmuligheder" }, "user": { "data": { @@ -24,7 +24,7 @@ "port": "Port", "username": "Brugernavn" }, - "title": "Konfigurer Transmission klient" + "title": "Konfigurer Transmission-klient" } }, "title": "Transmission" diff --git a/homeassistant/components/twentemilieu/.translations/tr.json b/homeassistant/components/twentemilieu/.translations/tr.json new file mode 100644 index 00000000000000..ebe13a37003c0a --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "address_exists": "Adres zaten kurulmu\u015f." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/da.json b/homeassistant/components/twilio/.translations/da.json index 0bb40aae7f25f2..d5f40d56446e36 100644 --- a/homeassistant/components/twilio/.translations/da.json +++ b/homeassistant/components/twilio/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Twilio meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Twilio-meddelelser.", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." }, "create_entry": { diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json index 0d0315e49c752d..46a94cc4047de1 100644 --- a/homeassistant/components/unifi/.translations/da.json +++ b/homeassistant/components/unifi/.translations/da.json @@ -33,9 +33,15 @@ "track_wired_clients": "Inkluder kablede netv\u00e6rksklienter" } }, + "init": { + "data": { + "one": "EN", + "other": "ANDEN" + } + }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Opret b\u00e5ndbredde sensorer for netv\u00e6rksklienter" + "allow_bandwidth_sensors": "Opret b\u00e5ndbredde-forbrugssensorer for netv\u00e6rksklienter" } } } diff --git a/homeassistant/components/upnp/.translations/da.json b/homeassistant/components/upnp/.translations/da.json index 1d0097c2f1f9ff..c41741b863547e 100644 --- a/homeassistant/components/upnp/.translations/da.json +++ b/homeassistant/components/upnp/.translations/da.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "UPnP/IGD er allerede konfigureret", "incomplete_device": "Ignorerer ufuldst\u00e6ndig UPnP-enhed", - "no_devices_discovered": "Ingen UPnP/IGD enheder fundet.", + "no_devices_discovered": "Ingen UPnP/IGD-enheder fundet.", "no_devices_found": "Ingen UPnP/IGD enheder kunne findes p\u00e5 netv\u00e6rket.", "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." @@ -23,7 +23,7 @@ "user": { "data": { "enable_port_mapping": "Aktiv\u00e9r porttilknytning til Home Assistent", - "enable_sensors": "Tilf\u00f8j trafik sensorer", + "enable_sensors": "Tilf\u00f8j trafiksensorer", "igd": "UPnP/IGD" }, "title": "Konfigurationsindstillinger for UPnP/IGD" diff --git a/homeassistant/components/vacuum/.translations/nl.json b/homeassistant/components/vacuum/.translations/nl.json index 3e49c926d2db2e..8ef0588796c47d 100644 --- a/homeassistant/components/vacuum/.translations/nl.json +++ b/homeassistant/components/vacuum/.translations/nl.json @@ -1,10 +1,16 @@ { "device_automation": { + "action_type": { + "clean": "Laat {entity_name} schoonmaken", + "dock": "Laat {entity_name} terugkeren naar het basisstation" + }, "condition_type": { - "is_cleaning": "{entity_name} is aan het schoonmaken" + "is_cleaning": "{entity_name} is aan het schoonmaken", + "is_docked": "{entity_name} is bij basisstation" }, "trigger_type": { - "cleaning": "{entity_name} begon met schoonmaken" + "cleaning": "{entity_name} begon met schoonmaken", + "docked": "{entity_name} is bij basisstation" } } } \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/da.json b/homeassistant/components/vesync/.translations/da.json index 43e56328f99785..f2be5792f33c22 100644 --- a/homeassistant/components/vesync/.translations/da.json +++ b/homeassistant/components/vesync/.translations/da.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Adgangskode", - "username": "Email adresse" + "username": "Emailadresse" }, "title": "Indtast brugernavn og adgangskode" } diff --git a/homeassistant/components/wemo/.translations/da.json b/homeassistant/components/wemo/.translations/da.json index c69547c66ab087..1da4d407849569 100644 --- a/homeassistant/components/wemo/.translations/da.json +++ b/homeassistant/components/wemo/.translations/da.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Ingen Wemo enheder fundet p\u00e5 netv\u00e6rket.", - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Wemo." + "no_devices_found": "Der blev ikke fundet nogen Wemo-enheder p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Kun en enkelt konfiguration af Wemo er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/withings/.translations/da.json b/homeassistant/components/withings/.translations/da.json index d2dddbbd204bfb..e4599fe8ec2923 100644 --- a/homeassistant/components/withings/.translations/da.json +++ b/homeassistant/components/withings/.translations/da.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "no_flows": "Du skal konfigurere Withings, f\u00f8r du kan godkende med den. L\u00e6s venligst dokumentationen." + }, "create_entry": { "default": "Godkendt med Withings for den valgte profil." }, "step": { + "profile": { + "data": { + "profile": "Profile" + }, + "description": "Hvilken profil har du valgt p\u00e5 Withings hjemmeside? Det er vigtigt, at profilerne matcher, ellers vil data blive m\u00e6rket forkert.", + "title": "Brugerprofil." + }, "user": { "data": { "profile": "Profil" diff --git a/homeassistant/components/wled/.translations/nl.json b/homeassistant/components/wled/.translations/nl.json index 1bf70b7a0952b7..266f74ce6c24f7 100644 --- a/homeassistant/components/wled/.translations/nl.json +++ b/homeassistant/components/wled/.translations/nl.json @@ -13,6 +13,7 @@ "data": { "host": "Hostnaam of IP-adres" }, + "description": "Stel uw WLED in op integratie met Home Assistant.", "title": "Koppel je WLED" }, "zeroconf_confirm": { diff --git a/homeassistant/components/wwlln/.translations/da.json b/homeassistant/components/wwlln/.translations/da.json index 7d9a676e1633f6..5d4f4c40b5d47c 100644 --- a/homeassistant/components/wwlln/.translations/da.json +++ b/homeassistant/components/wwlln/.translations/da.json @@ -1,16 +1,16 @@ { "config": { "error": { - "identifier_exists": "Placering er allerede registreret" + "identifier_exists": "Lokalitet er allerede registreret" }, "step": { "user": { "data": { "latitude": "Breddegrad", "longitude": "L\u00e6ngdegrad", - "radius": "Radius (ved hj\u00e6lp af dit basis enhedssystem)" + "radius": "Radius (ved hj\u00e6lp af dit basisenhedssystem)" }, - "title": "Udfyld dine placeringsoplysninger." + "title": "Udfyld dine lokalitetsoplysninger." } }, "title": "World Wide Lightning Location Network (WWLLN)" diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 39f254ac9af075..908d8113b2e4c6 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -9,8 +9,8 @@ "step": { "user": { "data": { - "radio_type": "Radio type", - "usb_path": "Sti til USB enhed" + "radio_type": "Radio-type", + "usb_path": "Sti til USB-enhed" }, "title": "ZHA" } @@ -33,12 +33,35 @@ "close": "Luk", "dim_down": "D\u00e6mp ned", "dim_up": "D\u00e6mp op", + "face_1": "med ansigt 1 aktiveret", + "face_2": "med ansigt 2 aktiveret", + "face_3": "med ansigt 3 aktiveret", + "face_4": "med ansigt 4 aktiveret", + "face_5": "med ansigt 5 aktiveret", + "face_6": "med ansigt 6 aktiveret", + "face_any": "Med ethvert/specificeret ansigt(er) aktiveret", "left": "Venstre", "open": "\u00c5ben", - "right": "H\u00f8jre" + "right": "H\u00f8jre", + "turn_off": "Sluk", + "turn_on": "T\u00e6nd" }, "trigger_type": { - "device_shaken": "Enhed rystet" + "device_dropped": "Enhed faldt", + "device_flipped": "Enheden blev vendt \"{subtype}\"", + "device_knocked": "Enhed banket med \"{subtype}\"", + "device_rotated": "Enhed roteret \"{subtype}\"", + "device_shaken": "Enhed rystet", + "device_slid": "Enheden gled \"{subtype}\"", + "device_tilted": "Enheden vippes", + "remote_button_double_press": "\"{subtype}\"-knappen er dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen trykket p\u00e5 konstant", + "remote_button_long_release": "\"{subtype}\"-knappen frigivet efter langt tryk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen firedobbelt-klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt-klikket", + "remote_button_short_press": "\"{subtype}\"-knappen trykket p\u00e5", + "remote_button_short_release": "\"{subtype}\"-knappen frigivet", + "remote_button_triple_press": "\"{subtype}\"-knappen tredobbeltklikkes" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/da.json b/homeassistant/components/zwave/.translations/da.json index e9049026a4fa5c..25eee9b3d91bdc 100644 --- a/homeassistant/components/zwave/.translations/da.json +++ b/homeassistant/components/zwave/.translations/da.json @@ -2,16 +2,16 @@ "config": { "abort": { "already_configured": "Z-Wave er allerede konfigureret", - "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n Z-Wave forekomst" + "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n Z-Wave-instans" }, "error": { - "option_error": "Z-Wave validering mislykkedes. Er stien til USB enhed korrekt?" + "option_error": "Z-Wave-validering mislykkedes. Er stien til USB-enhed korrekt?" }, "step": { "user": { "data": { "network_key": "Netv\u00e6rksn\u00f8gle (efterlad blank for autogenerering)", - "usb_path": "Sti til USB enhed" + "usb_path": "Sti til USB-enhed" }, "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ for oplysninger om konfigurationsvariabler", "title": "Ops\u00e6t Z-Wave" From 8a22a3835334f62d62c51d929788111c5d5402e4 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 30 Dec 2019 07:36:01 +0000 Subject: [PATCH 2583/3953] Accept homekit_controller pairing codes both with and without dashes (#30273) * Handle MalformedPinError from homekit_python * Handle both formats of pin codes --- .../homekit_controller/config_flow.py | 22 ++++++++ .../homekit_controller/test_config_flow.py | 55 ++++++++++++++++--- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 3f230d923c7acf..507a5cbb70a923 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -2,6 +2,7 @@ import json import logging import os +import re import homekit from homekit.controller.ip_implementation import IpPairing @@ -17,6 +18,8 @@ HOMEKIT_DIR = ".homekit" PAIRING_FILE = "pairing.json" +PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") + _LOGGER = logging.getLogger(__name__) @@ -59,6 +62,20 @@ def find_existing_host(hass, serial): return entry +def ensure_pin_format(pin): + """ + Ensure a pin code is correctly formatted. + + Ensures a pin code is in the format 111-11-111. Handles codes with and without dashes. + + If incorrect code is entered, an exception is raised. + """ + match = PIN_FORMAT.search(pin) + if not match: + raise homekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}") + return "{}-{}-{}".format(*match.groups()) + + @config_entries.HANDLERS.register(DOMAIN) class HomekitControllerFlowHandler(config_entries.ConfigFlow): """Handle a HomeKit config flow.""" @@ -277,6 +294,8 @@ async def async_step_pair(self, pair_info=None): if pair_info: code = pair_info["pairing_code"] try: + code = ensure_pin_format(code) + await self.hass.async_add_executor_job(self.finish_pairing, code) pairing = self.controller.pairings.get(self.hkid) @@ -284,6 +303,9 @@ async def async_step_pair(self, pair_info=None): return await self._entry_from_accessory(pairing) errors["pairing_code"] = "unable_to_pair" + except homekit.exceptions.MalformedPinError: + # Library claimed pin was invalid before even making an API call + errors["pairing_code"] = "authentication_error" except homekit.AuthenticationError: # PairSetup M4 - SRP proof failed # PairSetup M6 - Ed25519 signature verification failed diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 56c1c30e8f353b..2a7f36ba470d84 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -27,6 +27,7 @@ ] PAIRING_FINISH_FORM_ERRORS = [ + (homekit.exceptions.MalformedPinError, "authentication_error"), (homekit.MaxPeersError, "max_peers_error"), (homekit.AuthenticationError, "authentication_error"), (homekit.UnknownError, "unknown_error"), @@ -37,6 +38,27 @@ (homekit.AccessoryNotFoundError, "accessory_not_found_error") ] +INVALID_PAIRING_CODES = [ + "aaa-aa-aaa", + "aaa-11-aaa", + "111-aa-aaa", + "aaa-aa-111", + "1111-1-111", + "a111-11-111", + " 111-11-111", + "111-11-111 ", + "111-11-111a", + "1111111", +] + + +VALID_PAIRING_CODES = [ + "111-11-111", + "123-45-678", + "11111111", + "98765432", +] + def _setup_flow_handler(hass): flow = config_flow.HomekitControllerFlowHandler() @@ -56,6 +78,23 @@ async def _setup_flow_zeroconf(hass, discovery_info): return result +@pytest.mark.parametrize("pairing_code", INVALID_PAIRING_CODES) +def test_invalid_pairing_codes(pairing_code): + """Test ensure_pin_format raises for an invalid pin code.""" + with pytest.raises(homekit.exceptions.MalformedPinError): + config_flow.ensure_pin_format(pairing_code) + + +@pytest.mark.parametrize("pairing_code", VALID_PAIRING_CODES) +def test_valid_pairing_codes(pairing_code): + """Test ensure_pin_format corrects format for a valid pin in an alternative format.""" + valid_pin = config_flow.ensure_pin_format(pairing_code).split("-") + assert len(valid_pin) == 3 + assert len(valid_pin[0]) == 3 + assert len(valid_pin[1]) == 2 + assert len(valid_pin[2]) == 3 + + async def test_discovery_works(hass): """Test a device being discovered.""" discovery_info = { @@ -99,7 +138,7 @@ async def test_discovery_works(hass): # Pairing doesn't error error and pairing results flow.controller.pairings = {"00:00:00:00:00:00": pairing} - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == pairing.pairing_data @@ -147,7 +186,7 @@ async def test_discovery_works_upper_case(hass): ] flow.controller.pairings = {"00:00:00:00:00:00": pairing} - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == pairing.pairing_data @@ -196,7 +235,7 @@ async def test_discovery_works_missing_csharp(hass): flow.controller.pairings = {"00:00:00:00:00:00": pairing} - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == pairing.pairing_data @@ -379,7 +418,7 @@ async def test_pair_unable_to_pair(hass): assert flow.controller.start_pairing.call_count == 1 # Pairing doesn't error but no pairing object is generated - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "form" assert result["errors"]["pairing_code"] == "unable_to_pair" @@ -486,7 +525,7 @@ async def test_pair_abort_errors_on_finish(hass, exception, expected): # User submits code - pairing fails but can be retried flow.finish_pairing.side_effect = exception("error") - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "abort" assert result["reason"] == expected assert flow.context == { @@ -526,7 +565,7 @@ async def test_pair_form_errors_on_finish(hass, exception, expected): # User submits code - pairing fails but can be retried flow.finish_pairing.side_effect = exception("error") - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "form" assert result["errors"]["pairing_code"] == expected assert flow.context == { @@ -639,7 +678,7 @@ async def test_user_works(hass): assert result["type"] == "form" assert result["step_id"] == "pair" - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == pairing.pairing_data @@ -888,7 +927,7 @@ async def test_unignore_works(hass): assert flow.controller.start_pairing.call_count == 1 # Pairing finalized - result = await flow.async_step_pair({"pairing_code": "111-22-33"}) + result = await flow.async_step_pair({"pairing_code": "111-22-333"}) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == pairing.pairing_data From fccb13b76257cc894fcbd219cdba0885f64579d3 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 30 Dec 2019 08:05:49 +0000 Subject: [PATCH 2584/3953] Add homekit_controller service.sensor.smoke (#30269) --- .../homekit_controller/binary_sensor.py | 37 ++++++++++++++++++- .../components/homekit_controller/const.py | 1 + .../homekit_controller/test_binary_sensor.py | 27 ++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 1e1c8ef5d4450c..2998ce18641f37 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -3,7 +3,10 @@ from homekit.model.characteristics import CharacteristicsTypes -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SMOKE, + BinarySensorDevice, +) from . import KNOWN_DEVICES, HomeKitEntity @@ -57,7 +60,37 @@ def is_on(self): return self._state == 1 -ENTITY_TYPES = {"motion": HomeKitMotionSensor, "contact": HomeKitContactSensor} +class HomeKitSmokeSensor(HomeKitEntity, BinarySensorDevice): + """Representation of a Homekit smoke sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + + @property + def device_class(self) -> str: + """Return the class of this sensor.""" + return DEVICE_CLASS_SMOKE + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.SMOKE_DETECTED] + + def _update_smoke_detected(self, value): + self._state = value + + @property + def is_on(self): + """Return true if smoke is currently detected.""" + return self._state == 1 + + +ENTITY_TYPES = { + "motion": HomeKitMotionSensor, + "contact": HomeKitContactSensor, + "smoke": HomeKitSmokeSensor, +} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 09a7df2a2bfa67..204f0e07d3e41e 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -26,4 +26,5 @@ "light": "sensor", "temperature": "sensor", "battery": "sensor", + "smoke": "binary_sensor", } diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index 1b73021e44cdc9..f472ac38d1db8e 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -3,6 +3,7 @@ MOTION_DETECTED = ("motion", "motion-detected") CONTACT_STATE = ("contact", "contact-state") +SMOKE_DETECTED = ("smoke", "smoke-detected") def create_motion_sensor_service(): @@ -51,3 +52,29 @@ async def test_contact_sensor_read_state(hass, utcnow): helper.characteristics[CONTACT_STATE].value = 1 state = await helper.poll_and_get_state() assert state.state == "on" + + +def create_smoke_sensor_service(): + """Define smoke sensor characteristics.""" + service = FakeService("public.hap.service.sensor.smoke") + + cur_state = service.add_characteristic("smoke-detected") + cur_state.value = 0 + + return service + + +async def test_smoke_sensor_read_state(hass, utcnow): + """Test that we can read the state of a HomeKit contact accessory.""" + sensor = create_smoke_sensor_service() + helper = await setup_test_component(hass, [sensor]) + + helper.characteristics[SMOKE_DETECTED].value = 0 + state = await helper.poll_and_get_state() + assert state.state == "off" + + helper.characteristics[SMOKE_DETECTED].value = 1 + state = await helper.poll_and_get_state() + assert state.state == "on" + + assert state.attributes["device_class"] == "smoke" From 33738cc83aae78b99346a081c72ace4c5e5cd38f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 30 Dec 2019 17:51:11 +0100 Subject: [PATCH 2585/3953] Upgrade beautifulsoup4 to 4.8.2 (#30274) --- homeassistant/components/scrape/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 5fdcca372b91d8..6c5bf608999e9f 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/integrations/scrape", "requirements": [ - "beautifulsoup4==4.8.1" + "beautifulsoup4==4.8.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 641667d4506b6a..86fd67cc5bd510 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -287,7 +287,7 @@ batinfo==0.4.2 # beacontools[scan]==1.2.3 # homeassistant.components.scrape -beautifulsoup4==4.8.1 +beautifulsoup4==4.8.2 # homeassistant.components.beewi_smartclim beewi_smartclim==0.0.7 From d0c9a42b81db24f447c62f4bb5ac59668990b688 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 30 Dec 2019 17:51:25 +0100 Subject: [PATCH 2586/3953] Add custom validator for countries (#30280) --- CODEOWNERS | 1 + .../components/workday/binary_sensor.py | 141 ++++-------------- .../components/workday/manifest.json | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/workday/test_binary_sensor.py | 27 +++- 6 files changed, 55 insertions(+), 122 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 52f13748303174..1df0d2741cdfde 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -367,6 +367,7 @@ homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @sqldiablo homeassistant/components/withings/* @vangorra homeassistant/components/wled/* @frenck +homeassistant/components/workday/* @fabaff homeassistant/components/worldclock/* @fabaff homeassistant/components/wwlln/* @bachya homeassistant/components/xbox_live/* @MartinHjelmare diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 866d87d62401f3..0aa1f5bfc42838 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -1,6 +1,7 @@ """Sensor to indicate whether the current day is a workday.""" from datetime import datetime, timedelta import logging +from typing import Any import holidays import voluptuous as vol @@ -11,111 +12,6 @@ _LOGGER = logging.getLogger(__name__) -# List of all countries currently supported by holidays -# Source: https://github.com/dr-prodigy/python-holidays#available-countries -# There seems to be no way to get the list out at runtime -ALL_COUNTRIES = [ - "Argentina", - "AR", - "Aruba", - "AW", - "Australia", - "AU", - "Austria", - "AT", - "Brazil", - "BR", - "Belarus", - "BY", - "Belgium", - "BE", - "Bulgaria", - "BG", - "Canada", - "CA", - "Colombia", - "CO", - "Croatia", - "HR", - "Czech", - "CZ", - "Denmark", - "DK", - "England", - "Estonia", - "EE", - "EuropeanCentralBank", - "ECB", - "TAR", - "Finland", - "FI", - "France", - "FRA", - "Germany", - "DE", - "Hungary", - "HU", - "Honduras", - "HND", - "Iceland", - "IS", - "India", - "IND", - "Ireland", - "IE", - "Isle of Man", - "Italy", - "IT", - "Japan", - "JP", - "Kenya", - "KE", - "Lithuania", - "LT", - "Luxembourg", - "LU", - "Mexico", - "MX", - "Netherlands", - "NL", - "NewZealand", - "NZ", - "Northern Ireland", - "Norway", - "NO", - "Peru", - "PE", - "Poland", - "Polish", - "PL", - "Portugal", - "PT", - "PortugalExt", - "PTE", - "Russia", - "RU", - "Scotland", - "Slovenia", - "SI", - "Slovakia", - "SK", - "South Africa", - "ZA", - "Spain", - "ES", - "Sweden", - "SE", - "Switzerland", - "CH", - "Ukraine", - "UA", - "UnitedKingdom", - "UK", - "UnitedStates", - "US", - "Wales", -] - ALLOWED_DAYS = WEEKDAYS + ["holiday"] CONF_COUNTRY = "country" @@ -132,9 +28,28 @@ DEFAULT_NAME = "Workday Sensor" DEFAULT_OFFSET = 0 + +def valid_country(value: Any) -> str: + """Validate that the given country is supported.""" + value = cv.string(value) + all_supported_countries = holidays.list_supported_countries() + + try: + raw_value = value.encode("utf-8") + except UnicodeError: + raise vol.Invalid( + "The country name or the abbreviation must be a valid UTF-8 string." + ) + if not raw_value: + raise vol.Invalid("Country name or the abbreviation must not be empty.") + if value not in all_supported_countries: + raise vol.Invalid("Country is not supported.") + return value + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES), + vol.Required(CONF_COUNTRY): valid_country, vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): vol.All( cv.ensure_list, [vol.In(ALLOWED_DAYS)] ), @@ -151,13 +66,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Workday sensor.""" - sensor_name = config.get(CONF_NAME) - country = config.get(CONF_COUNTRY) - province = config.get(CONF_PROVINCE) - workdays = config.get(CONF_WORKDAYS) - excludes = config.get(CONF_EXCLUDES) - days_offset = config.get(CONF_OFFSET) add_holidays = config.get(CONF_ADD_HOLIDAYS) + country = config[CONF_COUNTRY] + days_offset = config[CONF_OFFSET] + excludes = config[CONF_EXCLUDES] + province = config.get(CONF_PROVINCE) + sensor_name = config[CONF_NAME] + workdays = config[CONF_WORKDAYS] year = (get_date(datetime.today()) + timedelta(days=days_offset)).year obj_holidays = getattr(holidays, country)(years=year) @@ -259,7 +174,7 @@ async def async_update(self): # Default is no workday self._state = False - # Get iso day of the week (1 = Monday, 7 = Sunday) + # Get ISO day of the week (1 = Monday, 7 = Sunday) date = get_date(datetime.today()) + timedelta(days=self._days_offset) day = date.isoweekday() - 1 day_of_week = day_to_string(day) diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 4b407e9523540c..ac3bee7d07c928 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -3,8 +3,8 @@ "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", "requirements": [ - "holidays==0.9.11" + "holidays==0.9.12" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@fabaff"] } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 86fd67cc5bd510..4cd122e85835fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -663,7 +663,7 @@ hlk-sw16==0.0.7 hole==0.5.0 # homeassistant.components.workday -holidays==0.9.11 +holidays==0.9.12 # homeassistant.components.frontend home-assistant-frontend==20191204.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b9a7c98702772..77936dd47291fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,7 +228,7 @@ herepy==2.0.0 hole==0.5.0 # homeassistant.components.workday -holidays==0.9.11 +holidays==0.9.12 # homeassistant.components.frontend home-assistant-frontend==20191204.1 diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 19da0cbfd87706..81ae18bfd3b287 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -2,7 +2,10 @@ from datetime import date from unittest.mock import patch -from homeassistant.components.workday.binary_sensor import day_to_string +import pytest +import voluptuous as vol + +import homeassistant.components.workday.binary_sensor as binary_sensor from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant @@ -68,6 +71,20 @@ def teardown_method(self): """Stop everything that was started.""" self.hass.stop() + def test_valid_country(self): + """Test topic name/filter validation.""" + # Invalid UTF-8, must not contain U+D800 to U+DFFF + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("\ud800") + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("\udfff") + # Country MUST NOT be empty + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("") + # Country must be supported by holidays + with pytest.raises(vol.Invalid): + binary_sensor.valid_country("HomeAssistantLand") + def test_setup_component_province(self): """Set up workday component.""" with assert_setup_component(1, "binary_sensor"): @@ -214,7 +231,7 @@ def test_yesterday(self, mock_date): def test_day_to_string(self): """Test if day_to_string is behaving correctly.""" - assert day_to_string(0) == "mon" - assert day_to_string(1) == "tue" - assert day_to_string(7) == "holiday" - assert day_to_string(8) is None + assert binary_sensor.day_to_string(0) == "mon" + assert binary_sensor.day_to_string(1) == "tue" + assert binary_sensor.day_to_string(7) == "holiday" + assert binary_sensor.day_to_string(8) is None From 40e3d6f773042e67d128694d63010232a3bce494 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 30 Dec 2019 08:52:34 -0800 Subject: [PATCH 2587/3953] Change default icons for Tesla components (#30288) --- homeassistant/components/tesla/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index dbfe07271ee61b..a034d9132f1816 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -26,7 +26,7 @@ configured_instances, validate_input, ) -from .const import DATA_LISTENER, DOMAIN, TESLA_COMPONENTS +from .const import DATA_LISTENER, DOMAIN, SENSOR_ICONS, TESLA_COMPONENTS _LOGGER = logging.getLogger(__name__) @@ -186,6 +186,11 @@ def __init__(self, tesla_device, controller, config_entry): self._name = self.tesla_device.name self.tesla_id = slugify(self.tesla_device.uniq_name) self._attributes = {} + self._icon = ( + SENSOR_ICONS[self.tesla_device.type] + if self.tesla_device.type and self.tesla_device.type in SENSOR_ICONS.keys() + else None + ) @property def name(self): @@ -197,6 +202,11 @@ def unique_id(self) -> str: """Return a unique ID.""" return self.tesla_id + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + @property def should_poll(self): """Return the polling state.""" From 3b9f48b51dab8dc85fd71dbd0d081841c2fd12b7 Mon Sep 17 00:00:00 2001 From: Andy Grunwald Date: Mon, 30 Dec 2019 17:54:16 +0100 Subject: [PATCH 2588/3953] Contributing: Add note about feature suggestions and bug tracking (#30225) --- CONTRIBUTING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbe77c7756fd89..1921e5d38dd146 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot The process is straight-forward. - - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) + - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1) - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant). - Write the code for your device, notification service, sensor, or IoT thing. - Ensure tests work. @@ -12,3 +12,7 @@ The process is straight-forward. Still interested? Then you should take a peek at the [developer documentation](https://developers.home-assistant.io/) to get more details. +## Feature suggestions + +If you want to suggest a new feature for Home Assistant (e.g., new integrations), please open a thread in our [Community Forum: Feature Requests](https://community.home-assistant.io/c/feature-requests). +We use [GitHub for tracking issues](https://github.com/home-assistant/home-assistant/issues), not for tracking feature requests. \ No newline at end of file From bad35577cb2fc47aa2dedeac4ced7985cf0accf3 Mon Sep 17 00:00:00 2001 From: Tais Hedegaard Holland <32095655+taisholland@users.noreply.github.com> Date: Mon, 30 Dec 2019 19:38:17 +0100 Subject: [PATCH 2589/3953] Bump ihcsdk to 2.4.0 (#30279) * Update requirements_all.txt update ihcsdk to version 2.4.0 * Update manifest.json upgrade to version 2.4.0 of ihcsdk --- homeassistant/components/ihc/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index a415b0e3103a90..cfc86f5e3cbd88 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/ihc", "requirements": [ "defusedxml==0.6.0", - "ihcsdk==2.3.0" + "ihcsdk==2.4.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 4cd122e85835fc..0b48df5caa5a15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -708,7 +708,7 @@ ibmiotf==0.3.4 iglo==1.2.7 # homeassistant.components.ihc -ihcsdk==2.3.0 +ihcsdk==2.4.0 # homeassistant.components.incomfort incomfort-client==0.4.0 From 41d2d1f3095eab51046aa88f977d6988b56b1a11 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 30 Dec 2019 19:40:52 +0100 Subject: [PATCH 2590/3953] Handle wired bug on restart (#30276) --- .../components/unifi/device_tracker.py | 3 +++ tests/components/unifi/test_controller.py | 6 ++++++ tests/components/unifi/test_device_tracker.py | 21 +++++++++++++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 6dfa6a6ecad3fe..8b45a0f227bc40 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -130,6 +130,9 @@ def __init__(self, client, controller): self.is_wired = self.client.mac not in controller.wireless_clients self.wired_bug = None + if self.is_wired != self.client.is_wired: + self.wired_bug = dt_util.utcnow() - self.controller.option_detection_time + @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index c86e2f11538982..ddd16f948cb43e 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -59,6 +59,7 @@ async def setup_unifi_integration( clients_response, devices_response, clients_all_response, + known_wireless_clients=None, ): """Create the UniFi controller.""" if UNIFI_CONFIG not in hass.data: @@ -76,6 +77,11 @@ async def setup_unifi_integration( entry_id=1, ) + if known_wireless_clients: + hass.data[UNIFI_WIRELESS_CLIENTS].update_data( + known_wireless_clients, config_entry + ) + mock_client_responses = deque() mock_client_responses.append(clients_response) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 42ba83a16124ec..5c1505653a3e1a 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -43,6 +43,14 @@ "last_seen": 1562600145, "mac": "00:00:00:00:00:03", } +CLIENT_4 = { + "essid": "ssid", + "hostname": "client_4", + "ip": "10.0.0.4", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:04", +} DEVICE_1 = { "board_rev": 3, @@ -102,16 +110,20 @@ async def test_no_clients(hass): async def test_tracked_devices(hass): """Test the update_items function with some clients.""" + client_4_copy = copy(CLIENT_4) + client_4_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) + controller = await setup_unifi_integration( hass, ENTRY_CONFIG, options={CONF_SSID_FILTER: ["ssid"]}, sites=SITES, - clients_response=[CLIENT_1, CLIENT_2, CLIENT_3], + clients_response=[CLIENT_1, CLIENT_2, CLIENT_3, client_4_copy], devices_response=[DEVICE_1, DEVICE_2], clients_all_response={}, + known_wireless_clients=(CLIENT_4["mac"],), ) - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 6 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -124,6 +136,11 @@ async def test_tracked_devices(hass): client_3 = hass.states.get("device_tracker.client_3") assert client_3 is None + # Wireless client with wired bug, if bug active on restart mark device away + client_4 = hass.states.get("device_tracker.client_4") + assert client_4 is not None + assert client_4.state == "not_home" + device_1 = hass.states.get("device_tracker.device_1") assert device_1 is not None assert device_1.state == "not_home" From 914aea94467c3098c233f9aca91d2dfa2037451d Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Mon, 30 Dec 2019 14:49:56 -0600 Subject: [PATCH 2591/3953] Bump pysmartthings 0.7.0 (#30302) --- homeassistant/components/smartthings/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/manifest.json b/homeassistant/components/smartthings/manifest.json index 0ab71382fad590..9fa156e0b2814f 100644 --- a/homeassistant/components/smartthings/manifest.json +++ b/homeassistant/components/smartthings/manifest.json @@ -3,7 +3,7 @@ "name": "Smartthings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smartthings", - "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.6.9"], + "requirements": ["pysmartapp==0.3.2", "pysmartthings==0.7.0"], "dependencies": ["webhook"], "after_dependencies": ["cloud"], "codeowners": ["@andrewsayre"] diff --git a/requirements_all.txt b/requirements_all.txt index 0b48df5caa5a15..ed6a433616a031 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1496,7 +1496,7 @@ pysma==0.3.4 pysmartapp==0.3.2 # homeassistant.components.smartthings -pysmartthings==0.6.9 +pysmartthings==0.7.0 # homeassistant.components.smarty pysmarty==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77936dd47291fe..cf62fd652888f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -496,7 +496,7 @@ pysma==0.3.4 pysmartapp==0.3.2 # homeassistant.components.smartthings -pysmartthings==0.6.9 +pysmartthings==0.7.0 # homeassistant.components.soma pysoma==0.0.10 From ae51331d498347dd6d73ad18fbec9d0861ed459e Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 31 Dec 2019 00:32:20 +0000 Subject: [PATCH 2592/3953] [ci skip] Translation update --- .../binary_sensor/.translations/es.json | 4 +-- .../components/deconz/.translations/es.json | 2 +- .../components/hangouts/.translations/ca.json | 2 ++ .../huawei_lte/.translations/it.json | 1 + .../huawei_lte/.translations/zh-Hant.json | 2 +- .../components/starline/.translations/ca.json | 1 + .../tellduslive/.translations/ca.json | 1 + .../components/tesla/.translations/it.json | 30 +++++++++++++++++++ .../components/wled/.translations/es.json | 2 +- .../components/zha/.translations/es.json | 16 +++++----- 10 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/tesla/.translations/it.json diff --git a/homeassistant/components/binary_sensor/.translations/es.json b/homeassistant/components/binary_sensor/.translations/es.json index 756a370ca3c870..9720fb974f6bf4 100644 --- a/homeassistant/components/binary_sensor/.translations/es.json +++ b/homeassistant/components/binary_sensor/.translations/es.json @@ -72,14 +72,14 @@ "not_moist": "{entity_name} se sec\u00f3", "not_moving": "{entity_name} dej\u00f3 de moverse", "not_occupied": "{entity_name} no est\u00e1 ocupado", - "not_opened": "{nombre_de_la_entidad} cerrado", + "not_opened": "{entity_name} cerrado", "not_plugged_in": "{entity_name} desconectado", "not_powered": "{entity_name} no est\u00e1 activado", "not_present": "{entity_name} no est\u00e1 presente", "not_unsafe": "{entity_name} se volvi\u00f3 seguro", "occupied": "{entity_name} se convirti\u00f3 en ocupado", "opened": "{entity_name} abierto", - "plugged_in": "{nombre_de_la_entidad} conectado", + "plugged_in": "{entity_name} conectado", "powered": "{entity_name} alimentado", "present": "{entity_name} presente", "problem": "{entity_name} empez\u00f3 a detectar problemas", diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 47fd99c48a25f8..adbe68153f7587 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -72,7 +72,7 @@ "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", - "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtipo}\" detenido", + "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtype}\" detenido", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", diff --git a/homeassistant/components/hangouts/.translations/ca.json b/homeassistant/components/hangouts/.translations/ca.json index ea43c804f2d1af..0dcc0f029c2967 100644 --- a/homeassistant/components/hangouts/.translations/ca.json +++ b/homeassistant/components/hangouts/.translations/ca.json @@ -14,6 +14,7 @@ "data": { "2fa": "Pin 2FA" }, + "description": "buit", "title": "Verificaci\u00f3 en dos passos" }, "user": { @@ -22,6 +23,7 @@ "email": "Correu electr\u00f2nic", "password": "Contrasenya" }, + "description": "buit", "title": "Inici de sessi\u00f3 de Google Hangouts" } }, diff --git a/homeassistant/components/huawei_lte/.translations/it.json b/homeassistant/components/huawei_lte/.translations/it.json index bcbae3b1b25b35..4ad17ecaa36c29 100644 --- a/homeassistant/components/huawei_lte/.translations/it.json +++ b/homeassistant/components/huawei_lte/.translations/it.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nome del servizio di notifica (la modifica richiede il riavvio)", "recipient": "Destinatari della notifica SMS", "track_new_devices": "Traccia nuovi dispositivi" } diff --git a/homeassistant/components/huawei_lte/.translations/zh-Hant.json b/homeassistant/components/huawei_lte/.translations/zh-Hant.json index 7371669e15700e..201e9afec4bb6e 100644 --- a/homeassistant/components/huawei_lte/.translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/.translations/zh-Hant.json @@ -33,7 +33,7 @@ "step": { "init": { "data": { - "name": "\u901a\u77e5\u670d\u52d9\u540d\u7a31", + "name": "\u901a\u77e5\u670d\u52d9\u540d\u7a31\uff08\u8b8a\u66f4\u5f8c\u9700\u91cd\u555f\uff09", "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", "track_new_devices": "\u8ffd\u8e64\u65b0\u8a2d\u5099" } diff --git a/homeassistant/components/starline/.translations/ca.json b/homeassistant/components/starline/.translations/ca.json index 04426c2acfa18e..72cf1a667606c0 100644 --- a/homeassistant/components/starline/.translations/ca.json +++ b/homeassistant/components/starline/.translations/ca.json @@ -11,6 +11,7 @@ "app_id": "ID d'aplicaci\u00f3", "app_secret": "Secret" }, + "description": "ID d'aplicaci\u00f3 i codi secret de compte de desenvolupador de StarLine", "title": "Credencials d'aplicaci\u00f3" }, "auth_captcha": { diff --git a/homeassistant/components/tellduslive/.translations/ca.json b/homeassistant/components/tellduslive/.translations/ca.json index fafa87984015ad..a337474c96bef6 100644 --- a/homeassistant/components/tellduslive/.translations/ca.json +++ b/homeassistant/components/tellduslive/.translations/ca.json @@ -18,6 +18,7 @@ "data": { "host": "Amfitri\u00f3" }, + "description": "buit", "title": "Selecci\u00f3 extrem" } }, diff --git a/homeassistant/components/tesla/.translations/it.json b/homeassistant/components/tesla/.translations/it.json new file mode 100644 index 00000000000000..0e254cf284365d --- /dev/null +++ b/homeassistant/components/tesla/.translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Errore durante la connessione; controllare la rete e riprovare", + "identifier_exists": "E-mail gi\u00e0 registrata", + "invalid_credentials": "Credenziali non valide", + "unknown_error": "Errore sconosciuto, si prega di segnalare le informazioni del registro" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Indirizzo E-Mail" + }, + "description": "Si prega di inserire le tue informazioni.", + "title": "Tesla - Configurazione" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Secondi tra le scansioni" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/es.json b/homeassistant/components/wled/.translations/es.json index 7dd388d41afc85..b7f567698ea497 100644 --- a/homeassistant/components/wled/.translations/es.json +++ b/homeassistant/components/wled/.translations/es.json @@ -7,7 +7,7 @@ "error": { "connection_error": "No se ha podido conectar al dispositivo WLED." }, - "flow_title": "WLED: {nombre}", + "flow_title": "WLED: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/zha/.translations/es.json b/homeassistant/components/zha/.translations/es.json index b8529ce9047376..fb2271b260ad03 100644 --- a/homeassistant/components/zha/.translations/es.json +++ b/homeassistant/components/zha/.translations/es.json @@ -54,14 +54,14 @@ "device_shaken": "Dispositivo agitado", "device_slid": "Dispositivo deslizado \" {subtype} \"", "device_tilted": "Dispositivo inclinado", - "remote_button_double_press": "\"{subtipo}\" bot\u00f3n de doble clic", - "remote_button_long_press": "Bot\u00f3n \"{subtipo}\" pulsado continuamente", - "remote_button_long_release": "Bot\u00f3n \"{subtipo}\" liberado despu\u00e9s de una pulsaci\u00f3n prolongada", - "remote_button_quadruple_press": "\"{subtipo}\" bot\u00f3n cu\u00e1druple pulsado", - "remote_button_quintuple_press": "\"{subtipo}\" bot\u00f3n qu\u00edntuple pulsado", - "remote_button_short_press": "Bot\u00f3n \"{subtipo}\" pulsado", - "remote_button_short_release": "Bot\u00f3n \"{subtipo}\" liberado", - "remote_button_triple_press": "\"{subtipo}\" bot\u00f3n de triple clic" + "remote_button_double_press": "\"{subtype}\" bot\u00f3n de doble clic", + "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", + "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_quadruple_press": "\"{subtype}\" bot\u00f3n cu\u00e1druple pulsado", + "remote_button_quintuple_press": "\"{subtype}\" bot\u00f3n qu\u00edntuple pulsado", + "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", + "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", + "remote_button_triple_press": "\"{subtype}\" bot\u00f3n de triple clic" } } } \ No newline at end of file From 1ee299b079b3672aad3dcdce8ce1ecf4e5238d07 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Mon, 30 Dec 2019 19:30:45 -0600 Subject: [PATCH 2593/3953] Ignore google_maps updates when last_seen goes backwards (#30178) --- .../components/google_maps/device_tracker.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 75f370e502eafb..9e33ff5f715468 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -53,6 +53,7 @@ def __init__(self, hass, config: ConfigType, see) -> None: self.username = config[CONF_USERNAME] self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=60) + self._prev_seen = {} credfile = "{}.{}".format( hass.config.path(CREDENTIALS_FILE), slugify(self.username) @@ -92,11 +93,22 @@ def _update_info(self, now=None): ) continue + last_seen = dt_util.as_utc(person.datetime) + if last_seen < self._prev_seen.get(dev_id, last_seen): + _LOGGER.warning( + "Ignoring %s update because timestamp " + "is older than last timestamp", + person.nickname, + ) + _LOGGER.debug("%s < %s", last_seen, self._prev_seen[dev_id]) + continue + self._prev_seen[dev_id] = last_seen + attrs = { ATTR_ADDRESS: person.address, ATTR_FULL_NAME: person.full_name, ATTR_ID: person.id, - ATTR_LAST_SEEN: dt_util.as_utc(person.datetime), + ATTR_LAST_SEEN: last_seen, ATTR_NICKNAME: person.nickname, ATTR_BATTERY_CHARGING: person.charging, ATTR_BATTERY_LEVEL: person.battery_level, From 2c1a7a54cdae4619ab6f194af1f5554717f6ec2b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 31 Dec 2019 13:05:31 +0100 Subject: [PATCH 2594/3953] Add GIOS integration (#28719) * Initial commit * Add gios to requirements * Add tests * Update .coveragerc file * Run gen_requirements_all.py * Change DEFAULT_SCAN_INTERVAL * Better strings * Bump library version * run script.hassfest * run isort * Add icons mapping * Remove unnecessary f-string * Remove unnecessary listener * Refactoring config_flow * Add unique_id to config entry * Change AQI states to consts in English * Remove unused init * Remove unused exception * Remove private instance attribute * Remove overwrite state property * Fix pylint error * Add SCAN_INTERVAL for air_quality entity * Add _abort_if_unique_id_configured() --- .coveragerc | 3 + CODEOWNERS | 1 + homeassistant/components/gios/__init__.py | 78 +++++++++ homeassistant/components/gios/air_quality.py | 158 +++++++++++++++++++ homeassistant/components/gios/config_flow.py | 65 ++++++++ homeassistant/components/gios/const.py | 25 +++ homeassistant/components/gios/manifest.json | 9 ++ homeassistant/components/gios/strings.json | 20 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/gios/__init__.py | 1 + tests/components/gios/test_config_flow.py | 104 ++++++++++++ 13 files changed, 471 insertions(+) create mode 100644 homeassistant/components/gios/__init__.py create mode 100644 homeassistant/components/gios/air_quality.py create mode 100644 homeassistant/components/gios/config_flow.py create mode 100644 homeassistant/components/gios/const.py create mode 100644 homeassistant/components/gios/manifest.json create mode 100644 homeassistant/components/gios/strings.json create mode 100644 tests/components/gios/__init__.py create mode 100644 tests/components/gios/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 70d8f867e0e42d..10d56c4701d09d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -258,6 +258,9 @@ omit = homeassistant/components/geniushub/* homeassistant/components/gearbest/sensor.py homeassistant/components/geizhals/sensor.py + homeassistant/components/gios/__init__.py + homeassistant/components/gios/air_quality.py + homeassistant/components/gios/consts.py homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 1df0d2741cdfde..f5357d1348c2cf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -119,6 +119,7 @@ homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/geonetnz_volcano/* @exxamalte +homeassistant/components/gios/* @bieniu homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/gntp/* @robbiet480 diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py new file mode 100644 index 00000000000000..981de6395deaed --- /dev/null +++ b/homeassistant/components/gios/__init__.py @@ -0,0 +1,78 @@ +"""The GIOS component.""" +import asyncio +import logging + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from gios import ApiError, Gios, NoStationError + +from homeassistant.core import Config, HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import Throttle + +from .const import CONF_STATION_ID, DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Set up configured GIOS.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + return True + + +async def async_setup_entry(hass, config_entry): + """Set up GIOS as config entry.""" + station_id = config_entry.data[CONF_STATION_ID] + _LOGGER.debug("Using station_id: %s", station_id) + + websession = async_get_clientsession(hass) + + gios = GiosData(websession, station_id) + + await gios.async_update() + + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = gios + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") + ) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality") + return True + + +class GiosData: + """Define an object to hold GIOS data.""" + + def __init__(self, session, station_id): + """Initialize.""" + self._gios = Gios(station_id, session) + self.station_id = station_id + self.sensors = {} + self.latitude = None + self.longitude = None + self.station_name = None + self.available = True + + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update GIOS data.""" + try: + with timeout(30): + await self._gios.update() + except asyncio.TimeoutError: + _LOGGER.error("Asyncio Timeout Error") + except (ApiError, NoStationError, ClientConnectorError) as error: + _LOGGER.error("GIOS data update failed: %s", error) + self.available = self._gios.available + self.latitude = self._gios.latitude + self.longitude = self._gios.longitude + self.station_name = self._gios.station_name + self.sensors = self._gios.data diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py new file mode 100644 index 00000000000000..f7285c8cc5a851 --- /dev/null +++ b/homeassistant/components/gios/air_quality.py @@ -0,0 +1,158 @@ +"""Support for the GIOS service.""" +from homeassistant.components.air_quality import ( + ATTR_CO, + ATTR_NO2, + ATTR_OZONE, + ATTR_PM_2_5, + ATTR_PM_10, + ATTR_SO2, + AirQualityEntity, +) +from homeassistant.const import CONF_NAME + +from .const import ATTR_STATION, DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, ICONS_MAP + +ATTRIBUTION = "Data provided by GIOŚ" +SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a GIOS entities from a config_entry.""" + name = config_entry.data[CONF_NAME] + + data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + + async_add_entities([GiosAirQuality(data, name)], True) + + +def round_state(func): + """Round state.""" + + def _decorator(self): + res = func(self) + if isinstance(res, float): + return round(res) + return res + + return _decorator + + +class GiosAirQuality(AirQualityEntity): + """Define an GIOS sensor.""" + + def __init__(self, gios, name): + """Initialize.""" + self.gios = gios + self._name = name + self._aqi = None + self._co = None + self._no2 = None + self._o3 = None + self._pm_2_5 = None + self._pm_10 = None + self._so2 = None + self._attrs = {} + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def icon(self): + """Return the icon.""" + if self._aqi in ICONS_MAP: + return ICONS_MAP[self._aqi] + return "mdi:blur" + + @property + def air_quality_index(self): + """Return the air quality index.""" + return self._aqi + + @property + @round_state + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._pm_2_5 + + @property + @round_state + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._pm_10 + + @property + @round_state + def ozone(self): + """Return the O3 (ozone) level.""" + return self._o3 + + @property + @round_state + def carbon_monoxide(self): + """Return the CO (carbon monoxide) level.""" + return self._co + + @property + @round_state + def sulphur_dioxide(self): + """Return the SO2 (sulphur dioxide) level.""" + return self._so2 + + @property + @round_state + def nitrogen_dioxide(self): + """Return the NO2 (nitrogen dioxide) level.""" + return self._no2 + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self.gios.station_id + + @property + def available(self): + """Return True if entity is available.""" + return self.gios.available + + @property + def device_state_attributes(self): + """Return the state attributes.""" + self._attrs[ATTR_STATION] = self.gios.station_name + return self._attrs + + async def async_update(self): + """Get the data from GIOS.""" + await self.gios.async_update() + + if self.gios.available: + # Different measuring stations have different sets of sensors. We don't know + # what data we will get. + if "AQI" in self.gios.sensors: + self._aqi = self.gios.sensors["AQI"]["value"] + if "CO" in self.gios.sensors: + self._co = self.gios.sensors["CO"]["value"] + self._attrs[f"{ATTR_CO}_index"] = self.gios.sensors["CO"]["index"] + if "NO2" in self.gios.sensors: + self._no2 = self.gios.sensors["NO2"]["value"] + self._attrs[f"{ATTR_NO2}_index"] = self.gios.sensors["NO2"]["index"] + if "O3" in self.gios.sensors: + self._o3 = self.gios.sensors["O3"]["value"] + self._attrs[f"{ATTR_OZONE}_index"] = self.gios.sensors["O3"]["index"] + if "PM2.5" in self.gios.sensors: + self._pm_2_5 = self.gios.sensors["PM2.5"]["value"] + self._attrs[f"{ATTR_PM_2_5}_index"] = self.gios.sensors["PM2.5"][ + "index" + ] + if "PM10" in self.gios.sensors: + self._pm_10 = self.gios.sensors["PM10"]["value"] + self._attrs[f"{ATTR_PM_10}_index"] = self.gios.sensors["PM10"]["index"] + if "SO2" in self.gios.sensors: + self._so2 = self.gios.sensors["SO2"]["value"] + self._attrs[f"{ATTR_SO2}_index"] = self.gios.sensors["SO2"]["index"] diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py new file mode 100644 index 00000000000000..368d610c22640e --- /dev/null +++ b/homeassistant/components/gios/config_flow.py @@ -0,0 +1,65 @@ +"""Adds config flow for GIOS.""" +import asyncio + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from gios import ApiError, Gios, NoStationError +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_NAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_STATION_ID, DEFAULT_NAME, DOMAIN # pylint:disable=unused-import + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_STATION_ID): int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + } +) + + +class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for GIOS.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + try: + await self.async_set_unique_id( + user_input[CONF_STATION_ID], raise_on_progress=False + ) + self._abort_if_unique_id_configured() + + websession = async_get_clientsession(self.hass) + + with timeout(30): + gios = Gios(user_input[CONF_STATION_ID], websession) + await gios.update() + + if not gios.available: + raise InvalidSensorsData() + + return self.async_create_entry( + title=user_input[CONF_STATION_ID], data=user_input, + ) + except (ApiError, ClientConnectorError, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + except NoStationError: + errors[CONF_STATION_ID] = "wrong_station_id" + except InvalidSensorsData: + errors[CONF_STATION_ID] = "invalid_sensors_data" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class InvalidSensorsData(exceptions.HomeAssistantError): + """Error to indicate invalid sensors data.""" diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py new file mode 100644 index 00000000000000..3588b5e8dfcf06 --- /dev/null +++ b/homeassistant/components/gios/const.py @@ -0,0 +1,25 @@ +"""Constants for GIOS integration.""" +from datetime import timedelta + +ATTR_NAME = "name" +ATTR_STATION = "station" +CONF_STATION_ID = "station_id" +DATA_CLIENT = "client" +DEFAULT_NAME = "GIOŚ" +# Term of service GIOŚ allow downloading data no more than twice an hour. +DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) +DOMAIN = "gios" + +AQI_GOOD = "dobry" +AQI_MODERATE = "umiarkowany" +AQI_POOR = "dostateczny" +AQI_VERY_GOOD = "bardzo dobry" +AQI_VERY_POOR = "zły" + +ICONS_MAP = { + AQI_VERY_GOOD: "mdi:emoticon-excited", + AQI_GOOD: "mdi:emoticon-happy", + AQI_MODERATE: "mdi:emoticon-neutral", + AQI_POOR: "mdi:emoticon-sad", + AQI_VERY_POOR: "mdi:emoticon-dead", +} diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json new file mode 100644 index 00000000000000..b3d125d8ab6424 --- /dev/null +++ b/homeassistant/components/gios/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "gios", + "name": "GIOŚ", + "documentation": "https://www.home-assistant.io/integrations/gios", + "dependencies": [], + "codeowners": ["@bieniu"], + "requirements": ["gios==0.0.3"], + "config_flow": true +} diff --git a/homeassistant/components/gios/strings.json b/homeassistant/components/gios/strings.json new file mode 100644 index 00000000000000..cc05a471b4a138 --- /dev/null +++ b/homeassistant/components/gios/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "title": "GIOŚ", + "step": { + "user": { + "title": "GIOŚ (Polish Chief Inspectorate Of Environmental Protection)", + "description": "Set up GIOŚ (Polish Chief Inspectorate Of Environmental Protection) air quality integration. If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/gios", + "data": { + "name": "Name of the integration", + "station_id": "ID of the measuring station" + } + } + }, + "error": { + "wrong_station_id": "ID of the measuring station is not correct.", + "invalid_sensors_data": "Invalid sensors' data for this measuring station.", + "cannot_connect": "Cannot connect to the GIOŚ server." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 88ff92a57b0593..55a4d76fdcd21a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -26,6 +26,7 @@ "geofency", "geonetnz_quakes", "geonetnz_volcano", + "gios", "glances", "gpslogger", "hangouts", diff --git a/requirements_all.txt b/requirements_all.txt index ed6a433616a031..d84815a42f312d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -578,6 +578,9 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.nmap_tracker getmac==0.8.1 +# homeassistant.components.gios +gios==0.0.3 + # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf62fd652888f6..d197ed0196afa3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -197,6 +197,9 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.nmap_tracker getmac==0.8.1 +# homeassistant.components.gios +gios==0.0.3 + # homeassistant.components.glances glances_api==0.2.0 diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py new file mode 100644 index 00000000000000..98528fda9f91b9 --- /dev/null +++ b/tests/components/gios/__init__.py @@ -0,0 +1 @@ +"""Tests for GIOS.""" diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py new file mode 100644 index 00000000000000..3a4aff6d9ad695 --- /dev/null +++ b/tests/components/gios/test_config_flow.py @@ -0,0 +1,104 @@ +"""Define tests for the GIOS config flow.""" +from asynctest import patch +from gios import ApiError + +from homeassistant import data_entry_flow +from homeassistant.components.gios import config_flow +from homeassistant.components.gios.const import CONF_STATION_ID +from homeassistant.const import CONF_NAME + +CONFIG = { + CONF_NAME: "Foo", + CONF_STATION_ID: 123, +} + +VALID_STATIONS = [ + {"id": 123, "stationName": "Test Name 1", "gegrLat": "99.99", "gegrLon": "88.88"}, + {"id": 321, "stationName": "Test Name 2", "gegrLat": "77.77", "gegrLon": "66.66"}, +] + +VALID_STATION = [ + {"id": 3764, "param": {"paramName": "particulate matter PM10", "paramCode": "PM10"}} +] + +VALID_INDEXES = { + "stIndexLevel": {"id": 1, "indexLevelName": "Good"}, + "pm10IndexLevel": {"id": 0, "indexLevelName": "Very good"}, +} + +VALID_SENSOR = {"key": "PM10", "values": [{"value": 11.11}]} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.GiosFlowHandler() + 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_invalid_station_id(hass): + """Test that errors are shown when measuring station ID is invalid.""" + with patch("gios.Gios._get_stations", return_value=VALID_STATIONS): + flow = config_flow.GiosFlowHandler() + flow.hass = hass + flow.context = {} + + result = await flow.async_step_user( + user_input={CONF_NAME: "Foo", CONF_STATION_ID: 0} + ) + + assert result["errors"] == {CONF_STATION_ID: "wrong_station_id"} + + +async def test_invalid_sensor_data(hass): + """Test that errors are shown when sensor data is invalid.""" + with patch("gios.Gios._get_stations", return_value=VALID_STATIONS), patch( + "gios.Gios._get_station", return_value=VALID_STATION + ), patch("gios.Gios._get_station", return_value=VALID_STATION), patch( + "gios.Gios._get_sensor", return_value={} + ): + flow = config_flow.GiosFlowHandler() + flow.hass = hass + flow.context = {} + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {CONF_STATION_ID: "invalid_sensors_data"} + + +async def test_cannot_connect(hass): + """Test that errors are shown when cannot connect to GIOS server.""" + with patch("gios.Gios._async_get", side_effect=ApiError("error")): + flow = config_flow.GiosFlowHandler() + flow.hass = hass + flow.context = {} + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_create_entry(hass): + """Test that the user step works.""" + with patch("gios.Gios._get_stations", return_value=VALID_STATIONS), patch( + "gios.Gios._get_station", return_value=VALID_STATION + ), patch("gios.Gios._get_station", return_value=VALID_STATION), patch( + "gios.Gios._get_sensor", return_value=VALID_SENSOR + ), patch( + "gios.Gios._get_indexes", return_value=VALID_INDEXES + ): + flow = config_flow.GiosFlowHandler() + flow.hass = hass + flow.context = {} + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONFIG[CONF_STATION_ID] + assert result["data"][CONF_STATION_ID] == CONFIG[CONF_STATION_ID] + + assert flow.context["unique_id"] == CONFIG[CONF_STATION_ID] From bb14a083f0a6004737df15162d9e922eef545e5b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Dec 2019 14:29:43 +0100 Subject: [PATCH 2595/3953] Store capabilities and supported features in entity registry, restore registered entities on startup (#30094) * Store capabilities and supported features in entity registry * Restore states at startup * Restore non-disabled entities on HA start * Fix test * Pass device class from entity platform * Clean up restored entities from state machine * Fix Z-Wave test? --- homeassistant/helpers/entity.py | 4 +- homeassistant/helpers/entity_platform.py | 17 ++- homeassistant/helpers/entity_registry.py | 91 +++++++++++++++- tests/common.py | 20 ++++ tests/components/zwave/test_init.py | 1 + tests/helpers/test_entity_component.py | 9 +- tests/helpers/test_entity_platform.py | 41 ++++++++ tests/helpers/test_entity_registry.py | 128 ++++++++++++++++++++++- 8 files changed, 293 insertions(+), 18 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 531444b9d1eb65..b1786130b582be 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -311,7 +311,9 @@ def _async_write_ha_state(self): start = timer() - attr = self.capability_attributes or {} + attr = self.capability_attributes + attr = dict(attr) if attr else {} + if not self.available: state = STATE_UNAVAILABLE else: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index e171a4cade8754..5fd88729f080b1 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -347,6 +347,9 @@ async def _async_add_entity( device_id=device_id, known_object_ids=self.entities.keys(), disabled_by=disabled_by, + capabilities=entity.capability_attributes, + supported_features=entity.supported_features, + device_class=entity.device_class, ) entity.registry_entry = entry @@ -387,10 +390,16 @@ async def _async_add_entity( # Make sure it is valid in case an entity set the value themselves if not valid_entity_id(entity.entity_id): raise HomeAssistantError(f"Invalid entity id: {entity.entity_id}") - if ( - entity.entity_id in self.entities - or entity.entity_id in self.hass.states.async_entity_ids(self.domain) - ): + + already_exists = entity.entity_id in self.entities + + if not already_exists: + existing = self.hass.states.get(entity.entity_id) + + if existing and not existing.attributes.get("restored"): + already_exists = True + + if already_exists: msg = f"Entity id already exists: {entity.entity_id}" if entity.unique_id is not None: msg += ". Platform {} does not generate unique IDs".format( diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 5eb799658802de..77d8ccc00e08e5 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -15,6 +15,12 @@ import attr +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_SUPPORTED_FEATURES, + EVENT_HOMEASSISTANT_START, + STATE_UNAVAILABLE, +) from homeassistant.core import Event, callback, split_entity_id, valid_entity_id from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.loader import bind_hass @@ -39,6 +45,8 @@ DISABLED_USER = "user" DISABLED_INTEGRATION = "integration" +ATTR_RESTORED = "restored" + STORAGE_VERSION = 1 STORAGE_KEY = "core.entity_registry" @@ -66,6 +74,9 @@ class RegistryEntry: ) ), ) + capabilities: Optional[Dict[str, Any]] = attr.ib(default=None) + supported_features: int = attr.ib(default=0) + device_class: Optional[str] = attr.ib(default=None) domain = attr.ib(type=str, init=False, repr=False) @domain.default @@ -142,11 +153,17 @@ def async_get_or_create( platform: str, unique_id: str, *, + # To influence entity ID generation suggested_object_id: Optional[str] = None, - config_entry: Optional["ConfigEntry"] = None, - device_id: Optional[str] = None, known_object_ids: Optional[Iterable[str]] = None, + # To disable an entity if it gets created disabled_by: Optional[str] = None, + # Data that we want entry to have + config_entry: Optional["ConfigEntry"] = None, + device_id: Optional[str] = None, + capabilities: Optional[Dict[str, Any]] = None, + supported_features: Optional[int] = None, + device_class: Optional[str] = None, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None @@ -160,6 +177,9 @@ def async_get_or_create( entity_id, config_entry_id=config_entry_id or _UNDEF, device_id=device_id or _UNDEF, + capabilities=capabilities or _UNDEF, + supported_features=supported_features or _UNDEF, + device_class=device_class or _UNDEF, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be @@ -187,6 +207,9 @@ def async_get_or_create( unique_id=unique_id, platform=platform, disabled_by=disabled_by, + capabilities=capabilities, + supported_features=supported_features or 0, + device_class=device_class, ) self.entities[entity_id] = entity _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -253,6 +276,9 @@ def _async_update_entity( device_id=_UNDEF, new_unique_id=_UNDEF, disabled_by=_UNDEF, + capabilities=_UNDEF, + supported_features=_UNDEF, + device_class=_UNDEF, ): """Private facing update properties method.""" old = self.entities[entity_id] @@ -264,6 +290,9 @@ def _async_update_entity( ("config_entry_id", config_entry_id), ("device_id", device_id), ("disabled_by", disabled_by), + ("capabilities", capabilities), + ("supported_features", supported_features), + ("device_class", device_class), ): if value is not _UNDEF and value != getattr(old, attr_name): changes[attr_name] = value @@ -318,6 +347,8 @@ def _async_update_entity( async def async_load(self) -> None: """Load the entity registry.""" + async_setup_entity_restore(self.hass, self) + data = await self.hass.helpers.storage.async_migrator( self.hass.config.path(PATH_REGISTRY), self._store, @@ -336,6 +367,9 @@ async def async_load(self) -> None: platform=entity["platform"], name=entity.get("name"), disabled_by=entity.get("disabled_by"), + capabilities=entity.get("capabilities") or {}, + supported_features=entity.get("supported_features", 0), + device_class=entity.get("device_class"), ) self.entities = entities @@ -359,6 +393,9 @@ def _data_to_save(self) -> Dict[str, Any]: "platform": entry.platform, "name": entry.name, "disabled_by": entry.disabled_by, + "capabilities": entry.capabilities, + "supported_features": entry.supported_features, + "device_class": entry.device_class, } for entry in self.entities.values() ] @@ -416,3 +453,53 @@ async def _async_migrate(entities: Dict[str, Any]) -> Dict[str, List[Dict[str, A {"entity_id": entity_id, **info} for entity_id, info in entities.items() ] } + + +@callback +def async_setup_entity_restore( + hass: HomeAssistantType, registry: EntityRegistry +) -> None: + """Set up the entity restore mechanism.""" + + @callback + def cleanup_restored_states(event: Event) -> None: + """Clean up restored states.""" + if event.data["action"] != "remove": + return + + state = hass.states.get(event.data["entity_id"]) + + if state is None or not state.attributes.get(ATTR_RESTORED): + return + + hass.states.async_remove(event.data["entity_id"]) + + hass.bus.async_listen(EVENT_ENTITY_REGISTRY_UPDATED, cleanup_restored_states) + + if hass.is_running: + return + + @callback + def _write_unavailable_states(_: Event) -> None: + """Make sure state machine contains entry for each registered entity.""" + states = hass.states + existing = set(states.async_entity_ids()) + + for entry in registry.entities.values(): + if entry.entity_id in existing or entry.disabled: + continue + + attrs: Dict[str, Any] = {ATTR_RESTORED: True} + + if entry.capabilities: + attrs.update(entry.capabilities) + + if entry.supported_features: + attrs[ATTR_SUPPORTED_FEATURES] = entry.supported_features + + if entry.device_class: + attrs[ATTR_DEVICE_CLASS] = entry.device_class + + states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) + + hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) diff --git a/tests/common.py b/tests/common.py index 5d13da74e8800a..e57710c46cc58d 100644 --- a/tests/common.py +++ b/tests/common.py @@ -906,6 +906,11 @@ def unique_id(self): """Return the unique ID of the entity.""" return self._handle("unique_id") + @property + def state(self): + """Return the state of the entity.""" + return self._handle("state") + @property def available(self): """Return True if entity is available.""" @@ -916,6 +921,21 @@ def device_info(self): """Info how it links to a device.""" return self._handle("device_info") + @property + def device_class(self): + """Info how device should be classified.""" + return self._handle("device_class") + + @property + def capability_attributes(self): + """Info about capabilities.""" + return self._handle("capability_attributes") + + @property + def supported_features(self): + """Info about supported features.""" + return self._handle("supported_features") + @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 36c918232205d0..b7ffaba7e420fa 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -130,6 +130,7 @@ async def test_auto_heal_midnight(hass, mock_openzwave): time = utc.localize(datetime(2017, 5, 6, 0, 0, 0)) async_fire_time_changed(hass, time) await hass.async_block_till_done() + await hass.async_block_till_done() assert network.heal.called assert len(network.heal.mock_calls) == 1 diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 81fbe2d6520047..a069c050cf4f03 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -8,7 +8,6 @@ import asynctest import pytest -from homeassistant.components import group from homeassistant.const import ENTITY_MATCH_ALL import homeassistant.core as ha from homeassistant.exceptions import PlatformNotReady @@ -285,15 +284,13 @@ async def test_extract_from_service_filter_out_non_existing_entities(hass): async def test_extract_from_service_no_group_expand(hass): """Test not expanding a group.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - test_group = await group.Group.async_create_group( - hass, "test_group", ["light.Ceiling", "light.Kitchen"] - ) - await component.async_add_entities([test_group]) + await component.async_add_entities([MockEntity(entity_id="group.test_group")]) call = ha.ServiceCall("test", "service", {"entity_id": ["group.test_group"]}) extracted = await component.async_extract_from_service(call, expand_group=False) - assert extracted == [test_group] + assert len(extracted) == 1 + assert extracted[0].entity_id == "group.test_group" async def test_setup_dependencies_platform(hass): diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 5909dfaf3aaaf8..0f73699c896138 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -793,3 +793,44 @@ async def test_entity_disabled_by_integration(hass): assert entry_default.disabled_by is None entry_disabled = registry.async_get_or_create(DOMAIN, DOMAIN, "disabled") assert entry_disabled.disabled_by == "integration" + + +async def test_entity_info_added_to_entity_registry(hass): + """Test entity info is written to entity registry.""" + component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) + + entity_default = MockEntity( + unique_id="default", + capability_attributes={"max": 100}, + supported_features=5, + device_class="mock-device-class", + ) + + await component.async_add_entities([entity_default]) + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry_default = registry.async_get_or_create(DOMAIN, DOMAIN, "default") + print(entry_default) + assert entry_default.capabilities == {"max": 100} + assert entry_default.supported_features == 5 + assert entry_default.device_class == "mock-device-class" + + +async def test_override_restored_entities(hass): + """Test that we allow overriding restored entities.""" + registry = mock_registry(hass) + registry.async_get_or_create( + "test_domain", "test_domain", "1234", suggested_object_id="world" + ) + + hass.states.async_set("test_domain.world", "unavailable", {"restored": True}) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + await component.async_add_entities( + [MockEntity(unique_id="1234", state="on", entity_id="test_domain.world")], True + ) + + state = hass.states.get("test_domain.world") + assert state.state == "on" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index b07c5237116b7b..7f45ff0d174daf 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -5,7 +5,8 @@ import asynctest import pytest -from homeassistant.core import callback, valid_entity_id +from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE +from homeassistant.core import CoreState, callback, valid_entity_id from homeassistant.helpers import entity_registry from tests.common import MockConfigEntry, flush_store, mock_registry @@ -57,6 +58,52 @@ def test_get_or_create_suggested_object_id(registry): assert entry.entity_id == "light.beer" +def test_get_or_create_updates_data(registry): + """Test that we update data in get_or_create.""" + orig_config_entry = MockConfigEntry(domain="light") + + orig_entry = registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=orig_config_entry, + device_id="mock-dev-id", + capabilities={"max": 100}, + supported_features=5, + device_class="mock-device-class", + disabled_by=entity_registry.DISABLED_HASS, + ) + + assert orig_entry.config_entry_id == orig_config_entry.entry_id + assert orig_entry.device_id == "mock-dev-id" + assert orig_entry.capabilities == {"max": 100} + assert orig_entry.supported_features == 5 + assert orig_entry.device_class == "mock-device-class" + assert orig_entry.disabled_by == entity_registry.DISABLED_HASS + + new_config_entry = MockConfigEntry(domain="light") + + new_entry = registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=new_config_entry, + device_id="new-mock-dev-id", + capabilities={"new-max": 100}, + supported_features=10, + device_class="new-mock-device-class", + disabled_by=entity_registry.DISABLED_USER, + ) + + assert new_entry.config_entry_id == new_config_entry.entry_id + assert new_entry.device_id == "new-mock-dev-id" + assert new_entry.capabilities == {"new-max": 100} + assert new_entry.supported_features == 10 + assert new_entry.device_class == "new-mock-device-class" + # Should not be updated + assert new_entry.disabled_by == entity_registry.DISABLED_HASS + + def test_get_or_create_suggested_object_id_conflict_register(registry): """Test that we don't generate an entity id that is already registered.""" entry = registry.async_get_or_create( @@ -91,7 +138,15 @@ async def test_loading_saving_data(hass, registry): orig_entry1 = registry.async_get_or_create("light", "hue", "1234") orig_entry2 = registry.async_get_or_create( - "light", "hue", "5678", config_entry=mock_config + "light", + "hue", + "5678", + device_id="mock-dev-id", + config_entry=mock_config, + capabilities={"max": 100}, + supported_features=5, + device_class="mock-device-class", + disabled_by=entity_registry.DISABLED_HASS, ) assert len(registry.entities) == 2 @@ -104,13 +159,17 @@ async def test_loading_saving_data(hass, registry): # Ensure same order 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=mock_config - ) + new_entry2 = registry.async_get_or_create("light", "hue", "5678") assert orig_entry1 == new_entry1 assert orig_entry2 == new_entry2 + assert new_entry2.device_id == "mock-dev-id" + assert new_entry2.disabled_by == entity_registry.DISABLED_HASS + assert new_entry2.capabilities == {"max": 100} + assert new_entry2.supported_features == 5 + assert new_entry2.device_class == "mock-device-class" + def test_generate_entity_considers_registered_entities(registry): """Test that we don't create entity id that are already registered.""" @@ -417,3 +476,62 @@ async def test_disabled_by_system_options(registry): "light", "hue", "BBBB", config_entry=mock_config, disabled_by="user" ) assert entry2.disabled_by == "user" + + +async def test_restore_states(hass): + """Test restoring states.""" + hass.state = CoreState.not_running + + registry = await entity_registry.async_get_registry(hass) + + registry.async_get_or_create( + "light", "hue", "1234", suggested_object_id="simple", + ) + # Should not be created + registry.async_get_or_create( + "light", + "hue", + "5678", + suggested_object_id="disabled", + disabled_by=entity_registry.DISABLED_HASS, + ) + registry.async_get_or_create( + "light", + "hue", + "9012", + suggested_object_id="all_info_set", + capabilities={"max": 100}, + supported_features=5, + device_class="mock-device-class", + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) + await hass.async_block_till_done() + + simple = hass.states.get("light.simple") + assert simple is not None + assert simple.state == STATE_UNAVAILABLE + assert simple.attributes == {"restored": True} + + disabled = hass.states.get("light.disabled") + assert disabled is None + + all_info_set = hass.states.get("light.all_info_set") + assert all_info_set is not None + assert all_info_set.state == STATE_UNAVAILABLE + assert all_info_set.attributes == { + "max": 100, + "supported_features": 5, + "device_class": "mock-device-class", + "restored": True, + } + + registry.async_remove("light.disabled") + registry.async_remove("light.simple") + registry.async_remove("light.all_info_set") + + await hass.async_block_till_done() + + assert hass.states.get("light.simple") is None + assert hass.states.get("light.disabled") is None + assert hass.states.get("light.all_info_set") is None From 5414e9d155c26963428dc3cc4b986b2238c08d1e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Dec 2019 14:30:09 +0100 Subject: [PATCH 2596/3953] Fix Withings leaking time zone change into other tests (#30320) * Fix Withings leaking time zone change in other tests * Fix spelling error in code doc --- tests/components/withings/test_common.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 37fcb4ce7f5f5d..6c8c0a4c3106e9 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -15,6 +15,13 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.util import dt +DEFAULT_TIME_ZONE = dt.DEFAULT_TIME_ZONE + + +def teardown(): + """Ensure the time zone is reverted after tests finish.""" + dt.set_default_time_zone(DEFAULT_TIME_ZONE) + @pytest.fixture(name="withings_api") def withings_api_fixture() -> WithingsApi: From 3f570245aa6ebf4c85a27a2b773b89ff4082d0af Mon Sep 17 00:00:00 2001 From: Issac Date: Tue, 31 Dec 2019 15:34:53 +0200 Subject: [PATCH 2597/3953] Add local_ip component (#29973) * Added localip component * Split config and core logic, and migrate to sensor platform (requested by @MartinHjelmare) Also allow overriding the sensor name via the config * Tweak docstring Co-Authored-By: Fabian Affolter * Initial support for config entries * Rename localip to local_ip (1/2) * Rename localip to local_ip (2/2) * Add test for config_flow * Split and rename tests * Remove unneeded code from config_flow * Implement configuration as config entry import. Other misc requested changes from code review. * Fix tests * minor code review fixes * remove unneeded code Co-authored-by: Fabian Affolter --- CODEOWNERS | 1 + homeassistant/components/local_ip/__init__.py | 42 +++++++++++++++++++ .../components/local_ip/config_flow.py | 34 +++++++++++++++ .../components/local_ip/manifest.json | 9 ++++ homeassistant/components/local_ip/sensor.py | 34 +++++++++++++++ .../components/local_ip/strings.json | 16 +++++++ homeassistant/generated/config_flows.py | 1 + tests/components/local_ip/__init__.py | 1 + tests/components/local_ip/test_config_flow.py | 19 +++++++++ tests/components/local_ip/test_init.py | 22 ++++++++++ 10 files changed, 179 insertions(+) create mode 100644 homeassistant/components/local_ip/__init__.py create mode 100644 homeassistant/components/local_ip/config_flow.py create mode 100644 homeassistant/components/local_ip/manifest.json create mode 100644 homeassistant/components/local_ip/sensor.py create mode 100644 homeassistant/components/local_ip/strings.json create mode 100644 tests/components/local_ip/__init__.py create mode 100644 tests/components/local_ip/test_config_flow.py create mode 100644 tests/components/local_ip/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index f5357d1348c2cf..23005cb5273b70 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -187,6 +187,7 @@ homeassistant/components/life360/* @pnbruckner homeassistant/components/linky/* @Quentame homeassistant/components/linux_battery/* @fabaff homeassistant/components/liveboxplaytv/* @pschmitt +homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd homeassistant/components/lovelace/* @home-assistant/frontend diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py new file mode 100644 index 00000000000000..c93b7a5a81bc7e --- /dev/null +++ b/homeassistant/components/local_ip/__init__.py @@ -0,0 +1,42 @@ +"""Get the local IP address of the Home Assistant instance.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv + +DOMAIN = "local_ip" +PLATFORM = "sensor" + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Optional(CONF_NAME, default=DOMAIN): cv.string})}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up local_ip from configuration.yaml.""" + conf = config.get(DOMAIN) + if conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, data=conf, context={"source": config_entries.SOURCE_IMPORT} + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): + """Set up local_ip from a config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, PLATFORM) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): + """Unload a config entry.""" + return await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) diff --git a/homeassistant/components/local_ip/config_flow.py b/homeassistant/components/local_ip/config_flow.py new file mode 100644 index 00000000000000..58a666a68f3484 --- /dev/null +++ b/homeassistant/components/local_ip/config_flow.py @@ -0,0 +1,34 @@ +"""Config flow for local_ip.""" +import voluptuous as vol + +from homeassistant import config_entries + +from . import DOMAIN + + +class SimpleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for local_ip.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is not None: + if any( + user_input["name"] == entry.data["name"] + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") + + return self.async_create_entry(title=user_input["name"], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required("name", default=DOMAIN): str}), + errors={}, + ) + + async def async_step_import(self, import_info): + """Handle import from config file.""" + return await self.async_step_user(import_info) diff --git a/homeassistant/components/local_ip/manifest.json b/homeassistant/components/local_ip/manifest.json new file mode 100644 index 00000000000000..4e97c32afa0533 --- /dev/null +++ b/homeassistant/components/local_ip/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "local_ip", + "name": "Local IP Address", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/local_ip", + "dependencies": [], + "codeowners": ["@issacg"], + "requirements": [] +} diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py new file mode 100644 index 00000000000000..274a11faec6e28 --- /dev/null +++ b/homeassistant/components/local_ip/sensor.py @@ -0,0 +1,34 @@ +"""Sensor platform for local_ip.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.util import get_local_ip + + +async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): + """Set up the platform from config_entry.""" + name = config_entry.data["name"] + async_add_entities([IPSensor(name)], True) + + +class IPSensor(Entity): + """A simple sensor.""" + + def __init__(self, name: str): + """Initialize the sensor.""" + self._state = None + self._name = name + + @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 + + def update(self): + """Fetch new state data for the sensor.""" + self._state = get_local_ip() diff --git a/homeassistant/components/local_ip/strings.json b/homeassistant/components/local_ip/strings.json new file mode 100644 index 00000000000000..43a88be332517a --- /dev/null +++ b/homeassistant/components/local_ip/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "title": "Local IP Address", + "step": { + "user": { + "title": "Local IP Address", + "data": { + "name": "Sensor Name" + } + } + }, + "abort": { + "already_configured": "Integration is already configured with an existing sensor with that name" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 55a4d76fdcd21a..135fab2b746b3e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -46,6 +46,7 @@ "life360", "lifx", "linky", + "local_ip", "locative", "logi_circle", "luftdaten", diff --git a/tests/components/local_ip/__init__.py b/tests/components/local_ip/__init__.py new file mode 100644 index 00000000000000..47e1f70fcf807f --- /dev/null +++ b/tests/components/local_ip/__init__.py @@ -0,0 +1 @@ +"""Tests for the local_ip integration.""" diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py new file mode 100644 index 00000000000000..f355e5c75b2db5 --- /dev/null +++ b/tests/components/local_ip/test_config_flow.py @@ -0,0 +1,19 @@ +"""Tests for the local_ip config_flow.""" +from homeassistant.components.local_ip import DOMAIN + + +async def test_config_flow(hass): + """Test we can finish a config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"name": "test"} + ) + assert result["type"] == "create_entry" + + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + assert state diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py new file mode 100644 index 00000000000000..fb43f06eea2b7b --- /dev/null +++ b/tests/components/local_ip/test_init.py @@ -0,0 +1,22 @@ +"""Tests for the local_ip component.""" +import pytest + +from homeassistant.components.local_ip import DOMAIN +from homeassistant.setup import async_setup_component +from homeassistant.util import get_local_ip + + +@pytest.fixture(name="config") +def config_fixture(): + """Create hass config fixture.""" + return {DOMAIN: {"name": "test"}} + + +async def test_basic_setup(hass, config): + """Test component setup creates entry from config.""" + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + local_ip = await hass.async_add_executor_job(get_local_ip) + state = hass.states.get("sensor.test") + assert state + assert state.state == local_ip From 72e97792bd083d905640bc5e17b1f33710c8dd88 Mon Sep 17 00:00:00 2001 From: Philipp Schmitt Date: Tue, 31 Dec 2019 14:38:46 +0100 Subject: [PATCH 2598/3953] Update liveboxplay and pyteleloisirs (#30093) --- homeassistant/components/liveboxplaytv/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/liveboxplaytv/manifest.json b/homeassistant/components/liveboxplaytv/manifest.json index bcb2b53f081af6..39692b1e282a9c 100644 --- a/homeassistant/components/liveboxplaytv/manifest.json +++ b/homeassistant/components/liveboxplaytv/manifest.json @@ -3,8 +3,8 @@ "name": "Liveboxplaytv", "documentation": "https://www.home-assistant.io/integrations/liveboxplaytv", "requirements": [ - "liveboxplaytv==2.0.2", - "pyteleloisirs==3.5" + "liveboxplaytv==2.0.3", + "pyteleloisirs==3.6" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index d84815a42f312d..55dcf41e6d4936 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -792,7 +792,7 @@ limitlessled==1.1.3 linode-api==4.1.9b1 # homeassistant.components.liveboxplaytv -liveboxplaytv==2.0.2 +liveboxplaytv==2.0.3 # homeassistant.components.lametric lmnotify==0.0.4 @@ -1532,7 +1532,7 @@ pysyncthru==0.5.0 pytautulli==0.5.0 # homeassistant.components.liveboxplaytv -pyteleloisirs==3.5 +pyteleloisirs==3.6 # homeassistant.components.tfiac pytfiac==0.4 From 4bfc7b984852398238053cde0e9dcf334bd5f4c7 Mon Sep 17 00:00:00 2001 From: Steven Barth Date: Tue, 31 Dec 2019 14:41:44 +0100 Subject: [PATCH 2599/3953] Add homematic host port config for HMIP-only CCUs (#30077) * homematic: Add host port config for HMIP-only CCUs When adding a host (CCU) to the homematic component currently the hardcoded port 2001 is used to communicate with it. However that port is only available on the target if the target supports HM (wireless) protocol which is not the case e.g. for the Hass.io Homematic CCU addon when running in HMIP-only mode with the HMIP-RFUSB stick. This allows to change the port home assistant uses to talk to the CCU in order to provide services under hte homematic homain, e.g. homematic.set_variable_value The default value for this option is the old hardcoded value this the change should be backwards compatible with existing configurations. * Change style of config retrieval --- homeassistant/components/homematic/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 828e170c67de19..d0776baa10642b 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -298,6 +298,7 @@ vol.Optional(CONF_HOSTS, default={}): { cv.match_all: { vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional( CONF_USERNAME, default=DEFAULT_USERNAME ): cv.string, @@ -392,7 +393,7 @@ def setup(hass, config): for sname, sconfig in conf[CONF_HOSTS].items(): remotes[sname] = { "ip": sconfig.get(CONF_HOST), - "port": DEFAULT_PORT, + "port": sconfig[CONF_PORT], "username": sconfig.get(CONF_USERNAME), "password": sconfig.get(CONF_PASSWORD), "connect": False, From b0a0871bed9c6ba0360e4de19d6e48fdc6b80250 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Tue, 31 Dec 2019 14:45:17 +0100 Subject: [PATCH 2600/3953] Bump dependency for HomematicIP Cloud (#30319) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 9f965bfdb6d870..14677dd71a0870 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/integrations/homematicip_cloud", "requirements": [ - "homematicip==0.10.14" + "homematicip==0.10.15" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 55dcf41e6d4936..15cc43e5356077 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -678,7 +678,7 @@ homeassistant-pyozw==0.1.7 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.14 +homematicip==0.10.15 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d197ed0196afa3..25d16e27ce82c2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -243,7 +243,7 @@ homeassistant-pyozw==0.1.7 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.14 +homematicip==0.10.15 # homeassistant.components.google # homeassistant.components.remember_the_milk From dedd1aec8c75d1c9c364bdeda3420d4b31a020db Mon Sep 17 00:00:00 2001 From: rhadamantys <46837767+rhadamantys@users.noreply.github.com> Date: Tue, 31 Dec 2019 14:48:31 +0100 Subject: [PATCH 2601/3953] Add support for Somfy RTS power socket and Somfy io Temperature sensor (#30053) * Added support for Somfy RTS wireless power socket and Somfy Temperature Sensore Thermos Wirefree io * Added code formatting fixes for commit 5faaf9c * added support for RollerShutterRTSComponent from Somfy * Added support for RTS roller shutter in set_cover_position * Add support for Somfy RTS power socket and Somfy io temperature sensor * black and isort fixes --- homeassistant/components/tahoma/__init__.py | 2 ++ homeassistant/components/tahoma/sensor.py | 11 ++++++++--- homeassistant/components/tahoma/switch.py | 11 ++++++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 640cc6418d0f01..6bb4fc200af68f 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -45,6 +45,7 @@ "io:RollerShutterWithLowSpeedManagementIOComponent": "cover", "io:SomfyBasicContactIOSystemSensor": "sensor", "io:SomfyContactIOSystemSensor": "sensor", + "io:TemperatureIOSystemSensor": "sensor", "io:VerticalExteriorAwningIOComponent": "cover", "io:VerticalInteriorBlindVeluxIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", @@ -59,6 +60,7 @@ "rts:ExteriorVenetianBlindRTSComponent": "cover", "rts:GarageDoor4TRTSComponent": "switch", "rts:RollerShutterRTSComponent": "cover", + "rts:OnOffRTSComponent": "switch", "rts:VenetianBlindRTSComponent": "cover", } diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 5279b160d9c540..85ccb55761da07 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.const import ATTR_BATTERY_LEVEL, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice @@ -40,8 +40,8 @@ def state(self): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - if self.tahoma_device.type == "Temperature Sensor": - return None + if self.tahoma_device.type == "io:TemperatureIOSystemSensor": + return TEMP_CELSIUS if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": return None if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": @@ -79,6 +79,11 @@ def update(self): if self.tahoma_device.type == "rtds:RTDSMotionSensor": self.current_value = self.tahoma_device.active_states["core:OccupancyState"] self._available = True + if self.tahoma_device.type == "io:TemperatureIOSystemSensor": + self.current_value = round( + float(self.tahoma_device.active_states["core:TemperatureState"]), 1 + ) + self._available = True _LOGGER.debug("Update %s, value: %d", self._name, self.current_value) diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py index a0a95ab47ce056..1612120f3136bf 100644 --- a/homeassistant/components/tahoma/switch.py +++ b/homeassistant/components/tahoma/switch.py @@ -45,9 +45,14 @@ def update(self): else: self._state = STATE_OFF - self._available = bool( - self.tahoma_device.active_states.get("core:StatusState") == "available" - ) + # A RTS power socket doesn't have a feedback channel, + # so we must assume the socket is available. + if self.tahoma_device.type == "rts:OnOffRTSComponent": + self._available = True + else: + self._available = bool( + self.tahoma_device.active_states.get("core:StatusState") == "available" + ) _LOGGER.debug("Update %s, state: %s", self._name, self._state) From e68cd339b962b2f43064ed32e0eb37a78da56d58 Mon Sep 17 00:00:00 2001 From: Shawn Wilsher <656602+sdwilsh@users.noreply.github.com> Date: Tue, 31 Dec 2019 05:56:23 -0800 Subject: [PATCH 2602/3953] Reduce solaredge logging severity (#30305) * [solaredge] Reduce Severity of Log Line This log error happens frequently for some sites, but it shouldn't be an error. It is expected, per the SolarEdge Monitoring API, that some sites do not support this information, and the expected result is that this would be empty (see comments on #27959). Fixes #27959 * Fix a typo Co-authored-by: Fabian Affolter --- homeassistant/components/solaredge/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index f0f1660a821a36..60cabaf38f062b 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass, entry, async_add_entities): return _LOGGER.debug("Credentials correct and site is active") except KeyError: - _LOGGER.error("Missing details data in solaredge response") + _LOGGER.error("Missing details data in SolarEdge response") return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve details from SolarEdge API") @@ -350,7 +350,9 @@ def update(self): power_to = [] if "connections" not in power_flow: - _LOGGER.error("Missing connections in power flow data") + _LOGGER.debug( + "Missing connections in power flow data. Assuming site does not have any" + ) return for connection in power_flow["connections"]: From 609bf445f0f64b9e1d3f78169b2d0e179010a740 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Tue, 31 Dec 2019 08:16:41 -0600 Subject: [PATCH 2603/3953] Remove Amcrest deprecated sensors and switches (#30308) --- homeassistant/components/amcrest/__init__.py | 93 ++++---------- homeassistant/components/amcrest/sensor.py | 8 +- homeassistant/components/amcrest/switch.py | 126 ------------------- 3 files changed, 27 insertions(+), 200 deletions(-) delete mode 100644 homeassistant/components/amcrest/switch.py diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 3420f42f9c9954..b934a7e0549827 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -11,7 +11,6 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.camera import DOMAIN as CAMERA from homeassistant.components.sensor import DOMAIN as SENSOR -from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( ATTR_ENTITY_ID, CONF_AUTHENTICATION, @@ -22,7 +21,6 @@ CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, - CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION, @@ -34,12 +32,11 @@ from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.service import async_extract_entity_ids -from .binary_sensor import BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSORS +from .binary_sensor import BINARY_SENSORS from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST from .const import CAMERAS, DATA_AMCREST, DEVICES, DOMAIN, SERVICE_UPDATE from .helpers import service_signal -from .sensor import SENSOR_MOTION_DETECTOR, SENSORS -from .switch import SWITCHES +from .sensor import SENSORS _LOGGER = logging.getLogger(__name__) @@ -65,68 +62,36 @@ AUTHENTICATION_LIST = {"basic": "basic"} -def _deprecated_sensor_values(sensors): - if SENSOR_MOTION_DETECTOR in sensors: - _LOGGER.warning( - "The '%s' option value '%s' is deprecated, " - "please remove it from your configuration and use " - "the '%s' option with value '%s' instead", - CONF_SENSORS, - SENSOR_MOTION_DETECTOR, - CONF_BINARY_SENSORS, - BINARY_SENSOR_MOTION_DETECTED, - ) - return sensors - - -def _deprecated_switches(config): - if CONF_SWITCHES in config: - _LOGGER.warning( - "The '%s' option (with value %s) is deprecated, " - "please remove it from your configuration and use " - "services and attributes instead", - CONF_SWITCHES, - config[CONF_SWITCHES], - ) - return config - - def _has_unique_names(devices): names = [device[CONF_NAME] for device in devices] vol.Schema(vol.Unique())(names) return devices -AMCREST_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_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional( - CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION - ): vol.All(vol.In(AUTHENTICATION_LIST)), - vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): vol.All( - vol.In(RESOLUTION_LIST) - ), - vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]): vol.All( - vol.In(STREAM_SOURCE_LIST) - ), - vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, - vol.Optional(CONF_BINARY_SENSORS): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSORS)] - ), - vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensor_values - ), - vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [vol.In(SWITCHES)]), - vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean, - } - ), - _deprecated_switches, +AMCREST_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.All( + vol.In(AUTHENTICATION_LIST) + ), + vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): vol.All( + vol.In(RESOLUTION_LIST) + ), + vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]): vol.All( + vol.In(STREAM_SOURCE_LIST) + ), + vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [vol.In(BINARY_SENSORS)] + ), + vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSORS)]), + vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean, + } ) CONFIG_SCHEMA = vol.Schema( @@ -216,7 +181,6 @@ def setup(hass, config): resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]] binary_sensors = device.get(CONF_BINARY_SENSORS) sensors = device.get(CONF_SENSORS) - switches = device.get(CONF_SWITCHES) stream_source = device[CONF_STREAM_SOURCE] control_light = device.get(CONF_CONTROL_LIGHT) @@ -252,11 +216,6 @@ def setup(hass, config): hass, SENSOR, DOMAIN, {CONF_NAME: name, CONF_SENSORS: sensors}, config ) - if switches: - discovery.load_platform( - hass, SWITCH, DOMAIN, {CONF_NAME: name, CONF_SWITCHES: switches}, config - ) - if not hass.data[DATA_AMCREST][DEVICES]: return False diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index b53f05273fa391..be03b3bedffa82 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -15,12 +15,10 @@ SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS) -SENSOR_MOTION_DETECTOR = "motion_detector" SENSOR_PTZ_PRESET = "ptz_preset" SENSOR_SDCARD = "sdcard" # Sensor types are defined like: Name, units, icon SENSORS = { - SENSOR_MOTION_DETECTOR: ["Motion Detected", None, "mdi:run"], SENSOR_PTZ_PRESET: ["PTZ Preset", None, "mdi:camera-iris"], SENSOR_SDCARD: ["SD Used", "%", "mdi:sd"], } @@ -94,11 +92,7 @@ def update(self): _LOGGER.debug("Updating %s sensor", self._name) try: - if self._sensor_type == SENSOR_MOTION_DETECTOR: - self._state = self._api.is_motion_detected - self._attrs["Record Mode"] = self._api.record_mode - - elif self._sensor_type == SENSOR_PTZ_PRESET: + if self._sensor_type == SENSOR_PTZ_PRESET: self._state = self._api.ptz_presets_count elif self._sensor_type == SENSOR_SDCARD: diff --git a/homeassistant/components/amcrest/switch.py b/homeassistant/components/amcrest/switch.py deleted file mode 100644 index 0c3390c16f9680..00000000000000 --- a/homeassistant/components/amcrest/switch.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Support for toggling Amcrest IP camera settings.""" -import logging - -from amcrest import AmcrestError - -from homeassistant.const import CONF_NAME, CONF_SWITCHES -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import ToggleEntity - -from .const import DATA_AMCREST, DEVICES, SERVICE_UPDATE -from .helpers import log_update_error, service_signal - -_LOGGER = logging.getLogger(__name__) - -MOTION_DETECTION = "motion_detection" -MOTION_RECORDING = "motion_recording" -# Switch types are defined like: Name, icon -SWITCHES = { - MOTION_DETECTION: ["Motion Detection", "mdi:run-fast"], - MOTION_RECORDING: ["Motion Recording", "mdi:record-rec"], -} - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the IP Amcrest camera switch platform.""" - if discovery_info is None: - return - - name = discovery_info[CONF_NAME] - device = hass.data[DATA_AMCREST][DEVICES][name] - async_add_entities( - [ - AmcrestSwitch(name, device, setting) - for setting in discovery_info[CONF_SWITCHES] - ], - True, - ) - - -class AmcrestSwitch(ToggleEntity): - """Representation of an Amcrest IP camera switch.""" - - def __init__(self, name, device, setting): - """Initialize the Amcrest switch.""" - self._name = "{} {}".format(name, SWITCHES[setting][0]) - self._signal_name = name - self._api = device.api - self._setting = setting - self._state = False - self._icon = SWITCHES[setting][1] - self._unsub_dispatcher = None - - @property - def name(self): - """Return the name of the switch if any.""" - return self._name - - @property - def is_on(self): - """Return true if switch is on.""" - return self._state - - def turn_on(self, **kwargs): - """Turn setting on.""" - if not self.available: - return - try: - if self._setting == MOTION_DETECTION: - self._api.motion_detection = "true" - elif self._setting == MOTION_RECORDING: - self._api.motion_recording = "true" - except AmcrestError as error: - log_update_error(_LOGGER, "turn on", self.name, "switch", error) - - def turn_off(self, **kwargs): - """Turn setting off.""" - if not self.available: - return - try: - if self._setting == MOTION_DETECTION: - self._api.motion_detection = "false" - elif self._setting == MOTION_RECORDING: - self._api.motion_recording = "false" - except AmcrestError as error: - log_update_error(_LOGGER, "turn off", self.name, "switch", error) - - @property - def available(self): - """Return True if entity is available.""" - return self._api.available - - def update(self): - """Update setting state.""" - if not self.available: - return - _LOGGER.debug("Updating %s switch", self._name) - - try: - if self._setting == MOTION_DETECTION: - detection = self._api.is_motion_detector_on() - elif self._setting == MOTION_RECORDING: - detection = self._api.is_record_on_motion_detection() - self._state = detection - except AmcrestError as error: - log_update_error(_LOGGER, "update", self.name, "switch", error) - - @property - def icon(self): - """Return the icon for the switch.""" - return self._icon - - async def async_on_demand_update(self): - """Update state.""" - self.async_schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Subscribe to update signal.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, - service_signal(SERVICE_UPDATE, self._signal_name), - self.async_on_demand_update, - ) - - async def async_will_remove_from_hass(self): - """Disconnect from update signal.""" - self._unsub_dispatcher() From fd0375ac204c3ebfc8304d7a57b662e257af41ec Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 31 Dec 2019 15:17:17 +0100 Subject: [PATCH 2604/3953] Add support for Velux garage doors (#30214) * Update manifest.json * Update cover.py Add GarageDoor * Update to pyvlx 0.2.12 * Sort --- homeassistant/components/velux/cover.py | 4 +++- homeassistant/components/velux/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 7d4adc7350c8d7..c9b4aa53fe5233 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -1,6 +1,6 @@ """Support for Velux covers.""" from pyvlx import OpeningDevice, Position -from pyvlx.opening_device import Awning, Blind, RollerShutter, Window +from pyvlx.opening_device import Awning, Blind, GarageDoor, RollerShutter, Window from homeassistant.components.cover import ( ATTR_POSITION, @@ -77,6 +77,8 @@ def device_class(self): return "shutter" if isinstance(self.node, Awning): return "awning" + if isinstance(self.node, GarageDoor): + return "garage" return "window" @property diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 783e23a8171800..d2fbb3b728a4e7 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -3,7 +3,7 @@ "name": "Velux", "documentation": "https://www.home-assistant.io/integrations/velux", "requirements": [ - "pyvlx==0.2.11" + "pyvlx==0.2.12" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 15cc43e5356077..0e163221d118f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1692,7 +1692,7 @@ pyvesync==1.1.0 pyvizio==0.0.7 # homeassistant.components.velux -pyvlx==0.2.11 +pyvlx==0.2.12 # homeassistant.components.html5 pywebpush==1.9.2 From 1c2618d99a22b12a73b5ea5959b66985cafc1910 Mon Sep 17 00:00:00 2001 From: Iulius Date: Tue, 31 Dec 2019 15:24:09 +0100 Subject: [PATCH 2605/3953] Add separate command and state topics for mqtt lock (#29808) * Update lock.py Allow different command and state topic + different command and state values. * Formatting updated after black run * TC updated to reflect different state & cmd values * Abbreviations for lock states added * additional non-default state test * whitespaces fixed * black formatting run --- .../components/mqtt/abbreviations.py | 2 + homeassistant/components/mqtt/lock.py | 14 ++- tests/components/mqtt/test_lock.py | 88 ++++++++++++++++++- 3 files changed, 98 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 5e995494a64bc5..6f9b172010256f 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -135,6 +135,8 @@ "stat_off": "state_off", "stat_on": "state_on", "stat_open": "state_open", + "stat_locked": "state_locked", + "stat_unlocked": "state_unlocked", "stat_t": "state_topic", "stat_tpl": "state_template", "stat_val_tpl": "state_value_template", diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index ccf8f2569fa727..6910e955288dd7 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -36,10 +36,16 @@ CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" +CONF_STATE_LOCKED = "state_locked" +CONF_STATE_UNLOCKED = "state_unlocked" + DEFAULT_NAME = "MQTT Lock" DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" +DEFAULT_STATE_LOCKED = "LOCKED" +DEFAULT_STATE_UNLOCKED = "UNLOCKED" + PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { @@ -50,6 +56,10 @@ vol.Optional( CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK ): cv.string, + vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string, + vol.Optional( + CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED + ): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -152,9 +162,9 @@ def message_received(msg): payload = msg.payload if value_template is not None: payload = value_template.async_render_with_possible_json_value(payload) - if payload == self._config[CONF_PAYLOAD_LOCK]: + if payload == self._config[CONF_STATE_LOCKED]: self._state = True - elif payload == self._config[CONF_PAYLOAD_UNLOCK]: + elif payload == self._config[CONF_STATE_UNLOCKED]: self._state = False self.async_write_ha_state() diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index fbaedd3f945f73..9b89fa7159d178 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -34,6 +34,8 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): "command_topic": "command-topic", "payload_lock": "LOCK", "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", } }, ) @@ -42,12 +44,46 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.state is STATE_UNLOCKED assert not state.attributes.get(ATTR_ASSUMED_STATE) - async_fire_mqtt_message(hass, "state-topic", "LOCK") + async_fire_mqtt_message(hass, "state-topic", "LOCKED") state = hass.states.get("lock.test") assert state.state is STATE_LOCKED - async_fire_mqtt_message(hass, "state-topic", "UNLOCK") + async_fire_mqtt_message(hass, "state-topic", "UNLOCKED") + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + + +async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component( + hass, + lock.DOMAIN, + { + lock.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "closed", + "state_unlocked": "open", + } + }, + ) + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "state-topic", "closed") + + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + + async_fire_mqtt_message(hass, "state-topic", "open") state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -66,6 +102,44 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): "command_topic": "command-topic", "payload_lock": "LOCK", "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + "value_template": "{{ value_json.val }}", + } + }, + ) + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + + async_fire_mqtt_message(hass, "state-topic", '{"val":"LOCKED"}') + + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + + async_fire_mqtt_message(hass, "state-topic", '{"val":"UNLOCKED"}') + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + + +async def test_controlling_non_default_state_via_topic_and_json_message( + hass, mqtt_mock +): + """Test the controlling state via topic and JSON message.""" + assert await async_setup_component( + hass, + lock.DOMAIN, + { + lock.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "state_locked": "closed", + "state_unlocked": "open", "value_template": "{{ value_json.val }}", } }, @@ -74,12 +148,12 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED - async_fire_mqtt_message(hass, "state-topic", '{"val":"LOCK"}') + async_fire_mqtt_message(hass, "state-topic", '{"val":"closed"}') state = hass.states.get("lock.test") assert state.state is STATE_LOCKED - async_fire_mqtt_message(hass, "state-topic", '{"val":"UNLOCK"}') + async_fire_mqtt_message(hass, "state-topic", '{"val":"open"}') state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -97,6 +171,8 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): "command_topic": "command-topic", "payload_lock": "LOCK", "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", } }, ) @@ -135,6 +211,8 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): "command_topic": "command-topic", "payload_lock": "LOCK", "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", "optimistic": True, } }, @@ -206,6 +284,8 @@ async def test_custom_availability_payload(hass, mqtt_mock): "command_topic": "command-topic", "payload_lock": "LOCK", "payload_unlock": "UNLOCK", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", "availability_topic": "availability-topic", "payload_available": "good", "payload_not_available": "nogood", From 734ef5a7a9070dd02dee1c5616188c54b6df4d36 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 31 Dec 2019 15:32:26 +0100 Subject: [PATCH 2606/3953] Upgrade Sphinx to 2.3.1 (#30310) --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index 55f0f2d162d5f5..a27f3a4a306236 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==2.2.2 +Sphinx==2.3.1 sphinx-autodoc-typehints==1.10.3 sphinx-autodoc-annotation==1.0.post1 \ No newline at end of file From 3c8ebf184459b7d647d2c163c8ed613133d3d160 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 31 Dec 2019 15:46:02 +0100 Subject: [PATCH 2607/3953] Add light support to Velbus integration (#30323) * Add light support to Velbus integration * Add Velbus light.py to .coveragerc * Applied black formatting --- .coveragerc | 1 + homeassistant/components/velbus/__init__.py | 2 +- homeassistant/components/velbus/light.py | 77 +++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/velbus/light.py diff --git a/.coveragerc b/.coveragerc index 10d56c4701d09d..e96895429a6cb1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -758,6 +758,7 @@ omit = homeassistant/components/velbus/climate.py homeassistant/components/velbus/const.py homeassistant/components/velbus/cover.py + homeassistant/components/velbus/light.py homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py homeassistant/components/velux/* diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 317c305254be96..de48f8465409e5 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -22,7 +22,7 @@ {DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA ) -COMPONENT_TYPES = ["switch", "sensor", "binary_sensor", "cover", "climate"] +COMPONENT_TYPES = ["switch", "sensor", "binary_sensor", "cover", "climate", "light"] async def async_setup(hass, config): diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py new file mode 100644 index 00000000000000..6b34182e559255 --- /dev/null +++ b/homeassistant/components/velbus/light.py @@ -0,0 +1,77 @@ +"""Support for Velbus light.""" +import logging + +from velbus.util import VelbusException + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, + SUPPORT_TRANSITION, + Light, +) + +from . import VelbusEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Velbus light based on config_entry.""" + cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] + modules_data = hass.data[DOMAIN][entry.entry_id]["light"] + entities = [] + for address, channel in modules_data: + module = cntrl.get_module(address) + entities.append(VelbusLight(module, channel)) + async_add_entities(entities) + + +class VelbusLight(VelbusEntity, Light): + """Representation of a Velbus light.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + + @property + def is_on(self): + """Return true if the light is on.""" + return self._module.is_on(self._channel) + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._module.get_dimmer_state(self._channel) + + def turn_on(self, **kwargs): + """Instruct the Velbus light to turn on.""" + try: + if ATTR_BRIGHTNESS in kwargs: + self._module.set_dimmer_state( + self._channel, + kwargs[ATTR_BRIGHTNESS], + kwargs.get(ATTR_TRANSITION, 0), + ) + else: + self._module.restore_dimmer_state( + self._channel, kwargs.get(ATTR_TRANSITION, 0), + ) + except VelbusException as err: + _LOGGER.error("A Velbus error occurred: %s", err) + + def turn_off(self, **kwargs): + """Instruct the velbus light to turn off.""" + try: + self._module.set_dimmer_state( + self._channel, 0, kwargs.get(ATTR_TRANSITION, 0), + ) + except VelbusException as err: + _LOGGER.error("A Velbus error occurred: %s", err) From 4031596aa7f16274f98622ba30127c03b7e60f5d Mon Sep 17 00:00:00 2001 From: Lars-P Date: Tue, 31 Dec 2019 15:58:54 +0100 Subject: [PATCH 2608/3953] Fix luftdaten integration by adding a sensor for pressure at sealevel (#30317) --- homeassistant/components/luftdaten/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 3dca82404c071a..d797fbbf4ba454 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -34,6 +34,7 @@ SENSOR_PM10 = "P1" SENSOR_PM2_5 = "P2" SENSOR_PRESSURE = "pressure" +SENSOR_PRESSURE_AT_SEALEVEL = "pressure_at_sealevel" SENSOR_TEMPERATURE = "temperature" TOPIC_UPDATE = f"{DOMAIN}_data_update" @@ -44,6 +45,7 @@ SENSOR_TEMPERATURE: ["Temperature", "mdi:thermometer", TEMP_CELSIUS], SENSOR_HUMIDITY: ["Humidity", "mdi:water-percent", "%"], SENSOR_PRESSURE: ["Pressure", "mdi:arrow-down-bold", "Pa"], + SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:mdi-download", "Pa"], SENSOR_PM10: ["PM10", "mdi:thought-bubble", VOLUME_MICROGRAMS_PER_CUBIC_METER], SENSOR_PM2_5: [ "PM2.5", From 5ed44297e6de8492f5c851cc5bc4e638b6c47086 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Tue, 31 Dec 2019 07:27:39 -0800 Subject: [PATCH 2609/3953] Simplify Tesla icon get code (#30301) * Simplify icon get code * Remove extraneous None --- homeassistant/components/tesla/__init__.py | 8 ++------ homeassistant/components/tesla/const.py | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index a034d9132f1816..1ae65f66821a88 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -26,7 +26,7 @@ configured_instances, validate_input, ) -from .const import DATA_LISTENER, DOMAIN, SENSOR_ICONS, TESLA_COMPONENTS +from .const import DATA_LISTENER, DOMAIN, ICONS, TESLA_COMPONENTS _LOGGER = logging.getLogger(__name__) @@ -186,11 +186,7 @@ def __init__(self, tesla_device, controller, config_entry): self._name = self.tesla_device.name self.tesla_id = slugify(self.tesla_device.uniq_name) self._attributes = {} - self._icon = ( - SENSOR_ICONS[self.tesla_device.type] - if self.tesla_device.type and self.tesla_device.type in SENSOR_ICONS.keys() - else None - ) + self._icon = ICONS.get(self.tesla_device.type) @property def name(self): diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py index 30a58b733edcb3..be460a430ac333 100644 --- a/homeassistant/components/tesla/const.py +++ b/homeassistant/components/tesla/const.py @@ -9,7 +9,7 @@ "device_tracker", "switch", ] -SENSOR_ICONS = { +ICONS = { "battery sensor": "mdi:battery", "range sensor": "mdi:gauge", "mileage sensor": "mdi:counter", From a3061bda60c9136859224d55cd194d61a0b0a37c Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 31 Dec 2019 11:09:58 -0500 Subject: [PATCH 2610/3953] Make the rest of ZHA platforms to use ZHA class registry (#30261) * Refactor ZHA component tests fixtures. * Add tests for ZHA device discovery. * Refactor ZHA registry MatchRule. Allow callables as a matching criteria. Allow sets for model & manufacturer. * Minor ZHA class registry refactoring. Less cluttered strict_matching registrations. * Add entities only if there are any. * Migrate rest of ZHA platforms to ZHA registry. * Pylint fixes. --- homeassistant/components/zha/binary_sensor.py | 13 +- .../components/zha/core/registries.py | 67 +- .../components/zha/device_tracker.py | 16 +- homeassistant/components/zha/fan.py | 14 +- homeassistant/components/zha/light.py | 15 +- homeassistant/components/zha/lock.py | 14 +- homeassistant/components/zha/sensor.py | 21 +- homeassistant/components/zha/switch.py | 14 +- tests/components/zha/common.py | 39 +- tests/components/zha/test_channels.py | 8 +- tests/components/zha/test_discover.py | 55 + tests/components/zha/test_registries.py | 84 +- tests/components/zha/zha_devices_list.py | 1874 +++++++++++++++++ 13 files changed, 2153 insertions(+), 81 deletions(-) create mode 100644 tests/components/zha/test_discover.py create mode 100644 tests/components/zha/zha_devices_list.py diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 954fa8b29aa62e..d8bc1187be8b21 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -28,7 +28,7 @@ SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) -from .core.registries import ZHA_ENTITIES, MatchRule +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -85,7 +85,8 @@ async def _async_setup_entities( if entity: entities.append(entity(**discovery_info)) - async_add_entities(entities, update_before_add=True) + if entities: + async_add_entities(entities, update_before_add=True) class BinarySensor(ZhaEntity, BinarySensorDevice): @@ -141,28 +142,28 @@ async def async_update(self): self._state = await self._channel.get_attribute_value(attribute) -@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ACCELEROMETER})) +@STRICT_MATCH(channel_names=CHANNEL_ACCELEROMETER) class Accelerometer(BinarySensor): """ZHA BinarySensor.""" DEVICE_CLASS = DEVICE_CLASS_MOVING -@STRICT_MATCH(MatchRule(channel_names={CHANNEL_OCCUPANCY})) +@STRICT_MATCH(channel_names=CHANNEL_OCCUPANCY) class Occupancy(BinarySensor): """ZHA BinarySensor.""" DEVICE_CLASS = DEVICE_CLASS_OCCUPANCY -@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ON_OFF})) +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) class Opening(BinarySensor): """ZHA BinarySensor.""" DEVICE_CLASS = DEVICE_CLASS_OPENING -@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ZONE})) +@STRICT_MATCH(channel_names=CHANNEL_ZONE) class IASZone(BinarySensor): """ZHA IAS BinarySensor.""" diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index f235b459b879aa..d2ba0243a5c97f 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -5,7 +5,7 @@ https://home-assistant.io/integrations/zha/ """ import collections -from typing import Callable, Set +from typing import Callable, Set, Union import attr import bellows.ezsp @@ -171,14 +171,33 @@ def establish_device_mappings(): REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.SCENE_CONTROLLER) +def set_or_callable(value): + """Convert single str or None to a set. Pass through callables and sets.""" + if value is None: + return frozenset() + if callable(value): + return value + if isinstance(value, (frozenset, set, list)): + return frozenset(value) + return frozenset([str(value)]) + + @attr.s(frozen=True) class MatchRule: """Match a ZHA Entity to a channel name or generic id.""" - channel_names: Set[str] = attr.ib(factory=frozenset, converter=frozenset) - generic_ids: Set[str] = attr.ib(factory=frozenset, converter=frozenset) - manufacturer: str = attr.ib(default=None) - model: str = attr.ib(default=None) + channel_names: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable + ) + generic_ids: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable + ) + manufacturers: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable + ) + models: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable + ) class ZHAEntityRegistry: @@ -190,7 +209,7 @@ def __init__(self): self._loose_registry = collections.defaultdict(dict) def get_entity( - self, component: str, zha_device, chnls: list, default: CALLABLE_T = None + self, component: str, zha_device, chnls: dict, default: CALLABLE_T = None ) -> CALLABLE_T: """Match a ZHA Channels to a ZHA Entity class.""" for match in self._strict_registry[component]: @@ -200,10 +219,17 @@ def get_entity( return default def strict_match( - self, component: str, rule: MatchRule + self, + component: str, + channel_names: Union[Callable, Set[str], str] = None, + generic_ids: Union[Callable, Set[str], str] = None, + manufacturers: Union[Callable, Set[str], str] = None, + models: Union[Callable, Set[str], str] = None, ) -> Callable[[CALLABLE_T], CALLABLE_T]: """Decorate a strict match rule.""" + rule = MatchRule(channel_names, generic_ids, manufacturers, models) + def decorator(zha_ent: CALLABLE_T) -> CALLABLE_T: """Register a strict match rule. @@ -215,10 +241,17 @@ def decorator(zha_ent: CALLABLE_T) -> CALLABLE_T: return decorator def loose_match( - self, component: str, rule: MatchRule + self, + component: str, + channel_names: Union[Callable, Set[str], str] = None, + generic_ids: Union[Callable, Set[str], str] = None, + manufacturers: Union[Callable, Set[str], str] = None, + models: Union[Callable, Set[str], str] = None, ) -> Callable[[CALLABLE_T], CALLABLE_T]: """Decorate a loose match rule.""" + rule = MatchRule(channel_names, generic_ids, manufacturers, models) + def decorator(zha_entity: CALLABLE_T) -> CALLABLE_T: """Register a loose match rule. @@ -238,7 +271,7 @@ def _loose_matched(self, zha_device, chnls: dict, rule: MatchRule) -> bool: return any(self._matched(zha_device, chnls, rule)) @staticmethod - def _matched(zha_device, chnls: list, rule: MatchRule) -> bool: + def _matched(zha_device, chnls: dict, rule: MatchRule) -> list: """Return a list of field matches.""" if not any(attr.asdict(rule).values()): return [False] @@ -252,11 +285,17 @@ def _matched(zha_device, chnls: list, rule: MatchRule) -> bool: all_generic_ids = {ch.generic_id for ch in chnls} matches.append(rule.generic_ids.issubset(all_generic_ids)) - if rule.manufacturer: - matches.append(zha_device.manufacturer == rule.manufacturer) - - if rule.model: - matches.append(zha_device.model == rule.model) + if rule.manufacturers: + if callable(rule.manufacturers): + matches.append(rule.manufacturers(zha_device.manufacturer)) + else: + matches.append(zha_device.manufacturer in rule.manufacturers) + + if rule.models: + if callable(rule.models): + matches.append(rule.models(zha_device.model)) + else: + matches.append(zha_device.model in rule.models) return matches diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index e7663b35686f30..7654893581437b 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -1,4 +1,5 @@ """Support for the ZHA platform.""" +import functools import logging import time @@ -14,9 +15,11 @@ SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity from .sensor import Battery +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -47,11 +50,20 @@ async def _async_setup_entities( """Set up the ZHA device trackers.""" entities = [] for discovery_info in discovery_infos: - entities.append(ZHADeviceScannerEntity(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity( + DOMAIN, zha_dev, channels, ZHADeviceScannerEntity + ) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) +@STRICT_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION) class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): """Represent a tracked device.""" diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index bccdf260a118c0..f489447e53019a 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,4 +1,5 @@ """Fans on Zigbee Home Automation networks.""" +import functools import logging from homeassistant.components.fan import ( @@ -20,6 +21,7 @@ SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -45,6 +47,7 @@ VALUE_TO_SPEED = dict(enumerate(SPEED_LIST)) SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -79,11 +82,18 @@ async def _async_setup_entities( """Set up the ZHA fans.""" entities = [] for discovery_info in discovery_infos: - entities.append(ZhaFan(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, ZhaFan) + if entity: + entities.append(entity(**discovery_info)) + if entities: + async_add_entities(entities, update_before_add=True) + +@STRICT_MATCH(channel_names=CHANNEL_FAN) class ZhaFan(ZhaEntity, FanEntity): """Representation of a ZHA fan.""" diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 08d74f9fdb37c5..eb7d3297b43cf2 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,5 +1,6 @@ """Lights on Zigbee Home Automation networks.""" from datetime import timedelta +import functools import logging from zigpy.zcl.foundation import Status @@ -21,6 +22,7 @@ SIGNAL_SET_LEVEL, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -36,6 +38,7 @@ UNSUPPORTED_ATTRIBUTE = 0x86 SCAN_INTERVAL = timedelta(minutes=60) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, light.DOMAIN) PARALLEL_UPDATES = 5 @@ -71,12 +74,18 @@ async def _async_setup_entities( """Set up the ZHA lights.""" entities = [] for discovery_info in discovery_infos: - zha_light = Light(**discovery_info) - entities.append(zha_light) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(light.DOMAIN, zha_dev, channels, Light) + if entity: + entities.append(entity(**discovery_info)) + if entities: + async_add_entities(entities, update_before_add=True) + +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) class Light(ZhaEntity, light.Light): """Representation of a ZHA or ZLL light.""" diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 2458bf4be5bf94..bf82252246ccf2 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -1,4 +1,5 @@ """Locks on Zigbee Home Automation networks.""" +import functools import logging from zigpy.zcl.foundation import Status @@ -19,6 +20,7 @@ SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -26,6 +28,7 @@ """ The first state is Zigbee 'Not fully locked' """ STATE_LIST = [STATE_UNLOCKED, STATE_LOCKED, STATE_UNLOCKED] +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) VALUE_TO_STATE = dict(enumerate(STATE_LIST)) @@ -62,11 +65,18 @@ async def _async_setup_entities( """Set up the ZHA locks.""" entities = [] for discovery_info in discovery_infos: - entities.append(ZhaDoorLock(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, ZhaDoorLock) + if entity: + entities.append(entity(**discovery_info)) + if entities: + async_add_entities(entities, update_before_add=True) + +@STRICT_MATCH(channel_names=CHANNEL_DOORLOCK) class ZhaDoorLock(ZhaEntity, LockDevice): """Representation of a ZHA lock.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 26dc25c71dcb49..2d39d562bf5b7f 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -30,7 +30,7 @@ SIGNAL_STATE_ATTR, ZHA_DISCOVERY_NEW, ) -from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES, MatchRule +from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES from .entity import ZhaEntity PARALLEL_UPDATES = 5 @@ -90,7 +90,8 @@ async def _async_setup_entities( for discovery_info in discovery_infos: entities.append(await make_sensor(discovery_info)) - async_add_entities(entities, update_before_add=True) + if entities: + async_add_entities(entities, update_before_add=True) async def make_sensor(discovery_info): @@ -175,7 +176,7 @@ def formatter(self, value): return round(float(value * self._multiplier) / self._divisor) -@STRICT_MATCH(MatchRule(channel_names={CHANNEL_POWER_CONFIGURATION})) +@STRICT_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION) class Battery(Sensor): """Battery sensor of power configuration cluster.""" @@ -203,7 +204,7 @@ async def async_state_attr_provider(self): return state_attrs -@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ELECTRICAL_MEASUREMENT})) +@STRICT_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) class ElectricalMeasurement(Sensor): """Active power measurement.""" @@ -221,8 +222,8 @@ def formatter(self, value) -> int: return round(value * self._channel.multiplier / self._channel.divisor) -@STRICT_MATCH(MatchRule(generic_ids={CHANNEL_ST_HUMIDITY_CLUSTER})) -@STRICT_MATCH(MatchRule(channel_names={CHANNEL_HUMIDITY})) +@STRICT_MATCH(generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER) +@STRICT_MATCH(channel_names=CHANNEL_HUMIDITY) class Humidity(Sensor): """Humidity sensor.""" @@ -231,7 +232,7 @@ class Humidity(Sensor): _unit = "%" -@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ILLUMINANCE})) +@STRICT_MATCH(channel_names=CHANNEL_ILLUMINANCE) class Illuminance(Sensor): """Illuminance Sensor.""" @@ -244,7 +245,7 @@ def formatter(value): return round(pow(10, ((value - 1) / 10000)), 1) -@STRICT_MATCH(MatchRule(channel_names={CHANNEL_SMARTENERGY_METERING})) +@STRICT_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING) class SmartEnergyMetering(Sensor): """Metering sensor.""" @@ -260,7 +261,7 @@ def unit_of_measurement(self): return self._channel.unit_of_measurement -@STRICT_MATCH(MatchRule(channel_names={CHANNEL_PRESSURE})) +@STRICT_MATCH(channel_names=CHANNEL_PRESSURE) class Pressure(Sensor): """Pressure sensor.""" @@ -269,7 +270,7 @@ class Pressure(Sensor): _unit = "hPa" -@STRICT_MATCH(MatchRule(channel_names={CHANNEL_TEMPERATURE})) +@STRICT_MATCH(channel_names=CHANNEL_TEMPERATURE) class Temperature(Sensor): """Temperature Sensor.""" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 03296e8a553923..cbd29925f625e0 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -1,4 +1,5 @@ """Switches on Zigbee Home Automation networks.""" +import functools import logging from zigpy.zcl.foundation import Status @@ -15,9 +16,11 @@ SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -52,11 +55,18 @@ async def _async_setup_entities( """Set up the ZHA switches.""" entities = [] for discovery_info in discovery_infos: - entities.append(Switch(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, Switch) + if entity: + entities.append(entity(**discovery_info)) + if entities: + async_add_entities(entities, update_before_add=True) + +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) class Switch(ZhaEntity, SwitchDevice): """ZHA switch.""" diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 57fb26db7f0334..06712e638f69f7 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -36,10 +36,10 @@ def __init__(self): class FakeEndpoint: """Fake endpoint for moking zigpy.""" - def __init__(self, manufacturer, model): + def __init__(self, manufacturer, model, epid=1): """Init fake endpoint.""" self.device = None - self.endpoint_id = 1 + self.endpoint_id = epid self.in_clusters = {} self.out_clusters = {} self._cluster_attr = {} @@ -97,21 +97,23 @@ def __init__(self, ieee, manufacturer, model): self.remove_from_group = CoroutineMock() -def make_device( - in_cluster_ids, out_cluster_ids, device_type, ieee, manufacturer, model -): +def make_device(endpoints, ieee, manufacturer, model): """Make a fake device using the specified cluster classes.""" device = FakeDevice(ieee, manufacturer, model) - endpoint = FakeEndpoint(manufacturer, model) - endpoint.device = device - device.endpoints[endpoint.endpoint_id] = endpoint - endpoint.device_type = device_type + for epid, ep in endpoints.items(): + endpoint = FakeEndpoint(manufacturer, model, epid) + endpoint.device = device + device.endpoints[epid] = endpoint + endpoint.device_type = ep["device_type"] + profile_id = ep.get("profile_id") + if profile_id: + endpoint.profile_id = profile_id - for cluster_id in in_cluster_ids: - endpoint.add_input_cluster(cluster_id) + for cluster_id in ep.get("in_clusters", []): + endpoint.add_input_cluster(cluster_id) - for cluster_id in out_cluster_ids: - endpoint.add_output_cluster(cluster_id) + for cluster_id in ep.get("out_clusters", []): + endpoint.add_output_cluster(cluster_id) return device @@ -136,7 +138,16 @@ async def async_init_zigpy_device( happens when the device is paired to the network for the first time. """ device = make_device( - in_cluster_ids, out_cluster_ids, device_type, ieee, manufacturer, model + { + 1: { + "in_clusters": in_cluster_ids, + "out_clusters": out_cluster_ids, + "device_type": device_type, + } + }, + ieee, + manufacturer, + model, ) if is_new_join: await gateway.async_device_initialized(device) diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 3be3aaf09307b2..557cc0f2c5cdce 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -67,9 +67,7 @@ def nwk(): 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, + {1: {"in_clusters": [cluster_id], "out_clusters": [], "device_type": 0x1234}}, "00:11:22:33:44:55:66:77", "test manufacturer", "test model", @@ -125,9 +123,7 @@ async def test_in_channel_config(cluster_id, bind_count, attrs, zha_gateway, has 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, + {1: {"out_clusters": [cluster_id], "in_clusters": [], "device_type": 0x1234}}, "00:11:22:33:44:55:66:77", "test manufacturer", "test model", diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py new file mode 100644 index 00000000000000..91805acc448b19 --- /dev/null +++ b/tests/components/zha/test_discover.py @@ -0,0 +1,55 @@ +"""Test zha device discovery.""" + +import asyncio +from unittest import mock + +import pytest + +from homeassistant.components.zha.core.channels import EventRelayChannel +import homeassistant.components.zha.core.const as zha_const +import homeassistant.components.zha.core.discovery as disc +import homeassistant.components.zha.core.gateway as core_zha_gw + +from .common import make_device +from .zha_devices_list import DEVICES + + +@pytest.mark.parametrize("device", DEVICES) +async def test_devices(device, zha_gateway: core_zha_gw.ZHAGateway, hass, config_entry): + """Test device discovery.""" + + zigpy_device = make_device( + device["endpoints"], + "00:11:22:33:44:55:66:77", + device["manufacturer"], + device["model"], + ) + + with mock.patch( + "homeassistant.components.zha.core.discovery._async_create_cluster_channel", + wraps=disc._async_create_cluster_channel, + ) as cr_ch: + await zha_gateway.async_device_restored(zigpy_device) + await hass.async_block_till_done() + tasks = [ + hass.config_entries.async_forward_entry_setup(config_entry, component) + for component in zha_const.COMPONENTS + ] + await asyncio.gather(*tasks) + + await hass.async_block_till_done() + + entity_ids = hass.states.async_entity_ids() + await hass.async_block_till_done() + zha_entities = { + ent for ent in entity_ids if ent.split(".")[0] in zha_const.COMPONENTS + } + + event_channels = { + arg[0].cluster_id + for arg, kwarg in cr_ch.call_args_list + if kwarg.get("channel_class") == EventRelayChannel + } + + assert zha_entities == set(device["entities"]) + assert event_channels == set(device["event_channels"]) diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index a0eef355229e63..9f77330dd55901 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -59,24 +59,68 @@ def channel(name, chan_id): True, ), # manufacturer matching - (registries.MatchRule(manufacturer="no match"), False), - (registries.MatchRule(manufacturer=MANUFACTURER), True), - (registries.MatchRule(model=MODEL), True), - (registries.MatchRule(model="no match"), False), + (registries.MatchRule(manufacturers="no match"), False), + (registries.MatchRule(manufacturers=MANUFACTURER), True), + (registries.MatchRule(models=MODEL), True), + (registries.MatchRule(models="no match"), False), # match everything ( registries.MatchRule( generic_ids={"channel_0x0006", "channel_0x0008"}, channel_names={"on_off", "level"}, - manufacturer=MANUFACTURER, - model=MODEL, + manufacturers=MANUFACTURER, + models=MODEL, ), True, ), + ( + registries.MatchRule( + channel_names="on_off", manufacturers={"random manuf", MANUFACTURER} + ), + True, + ), + ( + registries.MatchRule( + channel_names="on_off", manufacturers={"random manuf", "Another manuf"} + ), + False, + ), + ( + registries.MatchRule( + channel_names="on_off", manufacturers=lambda x: x == MANUFACTURER + ), + True, + ), + ( + registries.MatchRule( + channel_names="on_off", manufacturers=lambda x: x != MANUFACTURER + ), + False, + ), + ( + registries.MatchRule( + channel_names="on_off", models={"random model", MODEL} + ), + True, + ), + ( + registries.MatchRule( + channel_names="on_off", models={"random model", "Another model"} + ), + False, + ), + ( + registries.MatchRule(channel_names="on_off", models=lambda x: x == MODEL), + True, + ), + ( + registries.MatchRule(channel_names="on_off", models=lambda x: x != MODEL), + False, + ), ], ) def test_registry_matching(rule, matched, zha_device, channels): - """Test empty rule matching.""" + """Test strict rule matching.""" reg = registries.ZHAEntityRegistry() assert reg._strict_matched(zha_device, channels, rule) is matched @@ -92,22 +136,22 @@ def test_registry_matching(rule, matched, zha_device, channels): (registries.MatchRule(channel_names={"on_off", "level"}), True), (registries.MatchRule(channel_names={"on_off", "level", "no match"}), False), ( - registries.MatchRule(channel_names={"on_off", "level"}, model="no match"), + registries.MatchRule(channel_names={"on_off", "level"}, models="no match"), True, ), ( registries.MatchRule( channel_names={"on_off", "level"}, - model="no match", - manufacturer="no match", + models="no match", + manufacturers="no match", ), True, ), ( registries.MatchRule( channel_names={"on_off", "level"}, - model="no match", - manufacturer=MANUFACTURER, + models="no match", + manufacturers=MANUFACTURER, ), True, ), @@ -124,14 +168,14 @@ def test_registry_matching(rule, matched, zha_device, channels): ( registries.MatchRule( generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"}, - model="mo match", + models="mo match", ), False, ), ( registries.MatchRule( generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"}, - model=MODEL, + models=MODEL, ), True, ), @@ -143,17 +187,17 @@ def test_registry_matching(rule, matched, zha_device, channels): True, ), # manufacturer matching - (registries.MatchRule(manufacturer="no match"), False), - (registries.MatchRule(manufacturer=MANUFACTURER), True), - (registries.MatchRule(model=MODEL), True), - (registries.MatchRule(model="no match"), False), + (registries.MatchRule(manufacturers="no match"), False), + (registries.MatchRule(manufacturers=MANUFACTURER), True), + (registries.MatchRule(models=MODEL), True), + (registries.MatchRule(models="no match"), False), # match everything ( registries.MatchRule( generic_ids={"channel_0x0006", "channel_0x0008"}, channel_names={"on_off", "level"}, - manufacturer=MANUFACTURER, - model=MODEL, + manufacturers=MANUFACTURER, + models=MODEL, ), True, ), diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py new file mode 100644 index 00000000000000..d5875edc9e2ac6 --- /dev/null +++ b/tests/components/zha/zha_devices_list.py @@ -0,0 +1,1874 @@ +"""Example Zigbee Devices.""" + +DEVICES = [ + { + "endpoints": { + "1": { + "device_type": 2080, + "endpoint_id": 1, + "in_clusters": [0, 3, 4096, 64716], + "out_clusters": [3, 4, 6, 8, 4096, 64716], + "profile_id": 260, + } + }, + "entities": [], + "event_channels": [6, 8], + "manufacturer": "ADUROLIGHT", + "model": "Adurolight_NCC", + }, + { + "endpoints": { + "5": { + "device_type": 1026, + "endpoint_id": 5, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", + "sensor.bosch_isw_zpr1_wp13_77665544_power", + "sensor.bosch_isw_zpr1_wp13_77665544_temperature", + ], + "event_channels": [], + "manufacturer": "Bosch", + "model": "ISW-ZPR1-WP13", + }, + { + "endpoints": { + "1": { + "device_type": 1, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 2821], + "out_clusters": [3, 6, 8, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.centralite_3130_77665544_on_off", + "sensor.centralite_3130_77665544_power", + ], + "event_channels": [6, 8], + "manufacturer": "CentraLite", + "model": "3130", + }, + { + "endpoints": { + "1": { + "device_type": 81, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 1794, 2820, 2821, 64515], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.centralite_3210_l_77665544_smartenergy_metering", + "sensor.centralite_3210_l_77665544_electrical_measurement", + "switch.centralite_3210_l_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "3210-L", + }, + { + "endpoints": { + "1": { + "device_type": 770, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 2821, 64581], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.centralite_3310_s_77665544_power", + "sensor.centralite_3310_s_77665544_temperature", + "sensor.centralite_3310_s_77665544_manufacturer_specific", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "3310-S", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + }, + "2": { + "device_type": 12, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821, 64527], + "out_clusters": [3], + "profile_id": 49887, + }, + }, + "entities": [ + "binary_sensor.centralite_3315_s_77665544_ias_zone", + "sensor.centralite_3315_s_77665544_temperature", + "sensor.centralite_3315_s_77665544_power", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "3315-S", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + }, + "2": { + "device_type": 12, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821, 64527], + "out_clusters": [3], + "profile_id": 49887, + }, + }, + "entities": [ + "binary_sensor.centralite_3320_l_77665544_ias_zone", + "sensor.centralite_3320_l_77665544_temperature", + "sensor.centralite_3320_l_77665544_power", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "3320-L", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + }, + "2": { + "device_type": 263, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821, 64582], + "out_clusters": [3], + "profile_id": 49887, + }, + }, + "entities": [ + "binary_sensor.centralite_3326_l_77665544_ias_zone", + "sensor.centralite_3326_l_77665544_temperature", + "sensor.centralite_3326_l_77665544_power", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "3326-L", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + }, + "2": { + "device_type": 263, + "endpoint_id": 2, + "in_clusters": [0, 3, 1030, 2821], + "out_clusters": [3], + "profile_id": 260, + }, + }, + "entities": [ + "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", + "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", + "sensor.centralite_motion_sensor_a_77665544_temperature", + "sensor.centralite_motion_sensor_a_77665544_power", + ], + "event_channels": [], + "manufacturer": "CentraLite", + "model": "Motion Sensor-A", + }, + { + "endpoints": { + "1": { + "device_type": 81, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 1794], + "out_clusters": [0], + "profile_id": 260, + }, + "4": { + "device_type": 9, + "endpoint_id": 4, + "in_clusters": [], + "out_clusters": [25], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", + "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "ClimaxTechnology", + "model": "PSMP5_00.00.02.02TC", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 3, 1280, 1282], + "out_clusters": [0], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone" + ], + "event_channels": [], + "manufacturer": "ClimaxTechnology", + "model": "SD8SC_00.00.03.12TC", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 3, 1280], + "out_clusters": [0], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone" + ], + "event_channels": [], + "manufacturer": "ClimaxTechnology", + "model": "WS15_00.00.03.03TC", + }, + { + "endpoints": { + "11": { + "device_type": 528, + "endpoint_id": 11, + "in_clusters": [0, 3, 4, 5, 6, 8, 768], + "out_clusters": [], + "profile_id": 49246, + }, + "13": { + "device_type": 57694, + "endpoint_id": 13, + "in_clusters": [4096], + "out_clusters": [4096], + "profile_id": 49246, + }, + }, + "entities": [ + "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off" + ], + "event_channels": [], + "manufacturer": "Feibit Inc co.", + "model": "FB56-ZCW08KU1.1", + }, + { + "endpoints": { + "1": { + "device_type": 1027, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 4, 9, 1280, 1282], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.heiman_warningdevice_77665544_ias_zone", + "sensor.heiman_warningdevice_77665544_power", + ], + "event_channels": [], + "manufacturer": "Heiman", + "model": "WarningDevice", + }, + { + "endpoints": { + "6": { + "device_type": 1026, + "endpoint_id": 6, + "in_clusters": [0, 1, 3, 32, 1024, 1026, 1280], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.hivehome_com_mot003_77665544_temperature", + "sensor.hivehome_com_mot003_77665544_power", + "sensor.hivehome_com_mot003_77665544_illuminance", + "binary_sensor.hivehome_com_mot003_77665544_ias_zone", + ], + "event_channels": [], + "manufacturer": "HiveHome.com", + "model": "MOT003", + }, + { + "endpoints": { + "1": { + "device_type": 268, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 4096, 64636], + "out_clusters": [5, 25, 32, 4096], + "profile_id": 260, + }, + "242": { + "device_type": 97, + "endpoint_id": 242, + "in_clusters": [33], + "out_clusters": [33], + "profile_id": 41440, + }, + }, + "entities": [ + "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off" + ], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI bulb E12 WS opal 600lm", + }, + { + "endpoints": { + "1": { + "device_type": 512, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 4096], + "out_clusters": [5, 25, 32, 4096], + "profile_id": 49246, + } + }, + "entities": [ + "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off" + ], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI bulb E26 CWS opal 600lm", + }, + { + "endpoints": { + "1": { + "device_type": 256, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 2821, 4096], + "out_clusters": [5, 25, 32, 4096], + "profile_id": 49246, + } + }, + "entities": [ + "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off" + ], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI bulb E26 W opal 1000lm", + }, + { + "endpoints": { + "1": { + "device_type": 544, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 4096], + "out_clusters": [5, 25, 32, 4096], + "profile_id": 49246, + } + }, + "entities": [ + "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off" + ], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI bulb E26 WS opal 980lm", + }, + { + "endpoints": { + "1": { + "device_type": 256, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 2821, 4096], + "out_clusters": [5, 25, 32, 4096], + "profile_id": 260, + } + }, + "entities": [ + "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off" + ], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI bulb E26 opal 1000lm", + }, + { + "endpoints": { + "1": { + "device_type": 266, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 64636], + "out_clusters": [5, 25, 32], + "profile_id": 260, + } + }, + "entities": ["switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off"], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI control outlet", + }, + { + "endpoints": { + "1": { + "device_type": 2128, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 9, 2821, 4096], + "out_clusters": [3, 4, 6, 25, 4096], + "profile_id": 49246, + } + }, + "entities": [ + "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", + "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", + ], + "event_channels": [6], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI motion sensor", + }, + { + "endpoints": { + "1": { + "device_type": 2080, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 9, 32, 4096, 64636], + "out_clusters": [3, 4, 6, 8, 25, 258, 4096], + "profile_id": 260, + } + }, + "entities": ["sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power"], + "event_channels": [6, 8], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI on/off switch", + }, + { + "endpoints": { + "1": { + "device_type": 2096, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 9, 2821, 4096], + "out_clusters": [3, 4, 5, 6, 8, 25, 4096], + "profile_id": 49246, + } + }, + "entities": ["sensor.ikea_of_sweden_tradfri_remote_control_77665544_power"], + "event_channels": [6, 8], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI remote control", + }, + { + "endpoints": { + "1": { + "device_type": 8, + "endpoint_id": 1, + "in_clusters": [0, 3, 9, 2821, 4096, 64636], + "out_clusters": [25, 32, 4096], + "profile_id": 260, + }, + "242": { + "device_type": 97, + "endpoint_id": 242, + "in_clusters": [33], + "out_clusters": [33], + "profile_id": 41440, + }, + }, + "entities": [], + "event_channels": [], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI signal repeater", + }, + { + "endpoints": { + "1": { + "device_type": 2064, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 9, 2821, 4096], + "out_clusters": [3, 4, 6, 8, 25, 4096], + "profile_id": 260, + } + }, + "entities": ["sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power"], + "event_channels": [6, 8], + "manufacturer": "IKEA of Sweden", + "model": "TRADFRI wireless dimmer", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], + "out_clusters": [10, 25], + "profile_id": 260, + }, + "2": { + "device_type": 260, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821], + "out_clusters": [3, 6, 8], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.jasco_products_45852_77665544_smartenergy_metering", + "light.jasco_products_45852_77665544_level_on_off", + ], + "event_channels": [6, 8], + "manufacturer": "Jasco Products", + "model": "45852", + }, + { + "endpoints": { + "1": { + "device_type": 256, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 1794, 2821], + "out_clusters": [10, 25], + "profile_id": 260, + }, + "2": { + "device_type": 259, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821], + "out_clusters": [3, 6], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.jasco_products_45856_77665544_smartenergy_metering", + "switch.jasco_products_45856_77665544_on_off", + "light.jasco_products_45856_77665544_on_off", + ], + "event_channels": [6], + "manufacturer": "Jasco Products", + "model": "45856", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], + "out_clusters": [10, 25], + "profile_id": 260, + }, + "2": { + "device_type": 260, + "endpoint_id": 2, + "in_clusters": [0, 3, 2821], + "out_clusters": [3, 6, 8], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.jasco_products_45857_77665544_smartenergy_metering", + "light.jasco_products_45857_77665544_level_on_off", + ], + "event_channels": [6, 8], + "manufacturer": "Jasco Products", + "model": "45857", + }, + { + "endpoints": { + "1": { + "device_type": 3, + "endpoint_id": 1, + "in_clusters": [ + 0, + 1, + 3, + 4, + 5, + 6, + 8, + 32, + 1026, + 1027, + 2821, + 64513, + 64514, + ], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.keen_home_inc_sv02_610_mp_1_3_77665544_manufacturer_specific", + "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", + "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", + "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", + "light.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + ], + "event_channels": [], + "manufacturer": "Keen Home Inc", + "model": "SV02-610-MP-1.3", + }, + { + "endpoints": { + "1": { + "device_type": 3, + "endpoint_id": 1, + "in_clusters": [ + 0, + 1, + 3, + 4, + 5, + 6, + 8, + 32, + 1026, + 1027, + 2821, + 64513, + 64514, + ], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.keen_home_inc_sv02_612_mp_1_2_77665544_manufacturer_specific", + "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", + "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", + "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", + "light.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + ], + "event_channels": [], + "manufacturer": "Keen Home Inc", + "model": "SV02-612-MP-1.2", + }, + { + "endpoints": { + "1": { + "device_type": 3, + "endpoint_id": 1, + "in_clusters": [ + 0, + 1, + 3, + 4, + 5, + 6, + 8, + 32, + 1026, + 1027, + 2821, + 64513, + 64514, + ], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.keen_home_inc_sv02_612_mp_1_3_77665544_manufacturer_specific", + "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", + "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", + "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", + "light.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + ], + "event_channels": [], + "manufacturer": "Keen Home Inc", + "model": "SV02-612-MP-1.3", + }, + { + "endpoints": { + "1": { + "device_type": 14, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 514], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", + "switch.king_of_fans_inc_hbuniversalcfremote_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "King Of Fans, Inc.", + "model": "HBUniversalCFRemote", + }, + { + "endpoints": { + "1": { + "device_type": 258, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 64513], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": ["light.ledvance_a19_rgbw_77665544_level_light_color_on_off"], + "event_channels": [], + "manufacturer": "LEDVANCE", + "model": "A19 RGBW", + }, + { + "endpoints": { + "1": { + "device_type": 258, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 64513], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": ["light.ledvance_flex_rgbw_77665544_level_light_color_on_off"], + "event_channels": [], + "manufacturer": "LEDVANCE", + "model": "FLEX RGBW", + }, + { + "endpoints": { + "1": { + "device_type": 81, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 2821, 64513, 64520], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": ["switch.ledvance_plug_77665544_on_off"], + "event_channels": [], + "manufacturer": "LEDVANCE", + "model": "PLUG", + }, + { + "endpoints": { + "1": { + "device_type": 258, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2821, 64513], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": ["light.ledvance_rt_rgbw_77665544_level_light_color_on_off"], + "event_channels": [], + "manufacturer": "LEDVANCE", + "model": "RT RGBW", + }, + { + "endpoints": { + "1": { + "device_type": 81, + "endpoint_id": 1, + "in_clusters": [0, 1, 2, 3, 4, 5, 6, 10, 16, 2820], + "out_clusters": [10, 25], + "profile_id": 260, + }, + "100": { + "device_type": 263, + "endpoint_id": 100, + "in_clusters": [15], + "out_clusters": [4, 15], + "profile_id": 260, + }, + "2": { + "device_type": 9, + "endpoint_id": 2, + "in_clusters": [12], + "out_clusters": [4, 12], + "profile_id": 260, + }, + "3": { + "device_type": 83, + "endpoint_id": 3, + "in_clusters": [12], + "out_clusters": [12], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", + "sensor.lumi_lumi_plug_maus01_77665544_analog_input", + "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", + "sensor.lumi_lumi_plug_maus01_77665544_power", + "switch.lumi_lumi_plug_maus01_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.plug.maus01", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 1, 2, 3, 4, 5, 6, 10, 12, 16, 2820], + "out_clusters": [10, 25], + "profile_id": 260, + }, + "2": { + "device_type": 257, + "endpoint_id": 2, + "in_clusters": [4, 5, 6, 16], + "out_clusters": [], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_relay_c2acn01_77665544_analog_input", + "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", + "sensor.lumi_lumi_relay_c2acn01_77665544_power", + "light.lumi_lumi_relay_c2acn01_77665544_on_off", + "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.relay.c2acn01", + }, + { + "endpoints": { + "1": { + "device_type": 24321, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 18, 25, 65535], + "out_clusters": [0, 3, 4, 5, 18, 25, 65535], + "profile_id": 260, + }, + "2": { + "device_type": 24322, + "endpoint_id": 2, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 18], + "profile_id": 260, + }, + "3": { + "device_type": 24323, + "endpoint_id": 3, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 12, 18], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input", + "sensor.lumi_lumi_remote_b186acn01_77665544_power", + "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input_2", + "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input_3", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.remote.b186acn01", + }, + { + "endpoints": { + "1": { + "device_type": 24321, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 18, 25, 65535], + "out_clusters": [0, 3, 4, 5, 18, 25, 65535], + "profile_id": 260, + }, + "2": { + "device_type": 24322, + "endpoint_id": 2, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 18], + "profile_id": 260, + }, + "3": { + "device_type": 24323, + "endpoint_id": 3, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 12, 18], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input", + "sensor.lumi_lumi_remote_b286acn01_77665544_power", + "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input_2", + "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input_3", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.remote.b286acn01", + }, + { + "endpoints": { + "1": { + "device_type": 261, + "endpoint_id": 1, + "in_clusters": [0, 1, 3], + "out_clusters": [3, 6, 8, 768], + "profile_id": 260, + }, + "2": { + "device_type": -1, + "endpoint_id": 2, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "3": { + "device_type": -1, + "endpoint_id": 3, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "4": { + "device_type": -1, + "endpoint_id": 4, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "5": { + "device_type": -1, + "endpoint_id": 5, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "6": { + "device_type": -1, + "endpoint_id": 6, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + }, + "entities": ["sensor.lumi_lumi_remote_b286opcn01_77665544_power"], + "event_channels": [6, 8, 768], + "manufacturer": "LUMI", + "model": "lumi.remote.b286opcn01", + }, + { + "endpoints": { + "1": { + "device_type": 261, + "endpoint_id": 1, + "in_clusters": [0, 1, 3], + "out_clusters": [3, 6, 8, 768], + "profile_id": 260, + }, + "2": { + "device_type": 259, + "endpoint_id": 2, + "in_clusters": [3], + "out_clusters": [3, 6], + "profile_id": 260, + }, + "3": { + "device_type": -1, + "endpoint_id": 3, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "4": { + "device_type": -1, + "endpoint_id": 4, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "5": { + "device_type": -1, + "endpoint_id": 5, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + "6": { + "device_type": -1, + "endpoint_id": 6, + "in_clusters": [], + "out_clusters": [], + "profile_id": -1, + }, + }, + "entities": [ + "sensor.lumi_lumi_remote_b486opcn01_77665544_power", + "switch.lumi_lumi_remote_b486opcn01_77665544_on_off", + ], + "event_channels": [6, 8, 768, 6], + "manufacturer": "LUMI", + "model": "lumi.remote.b486opcn01", + }, + { + "endpoints": { + "1": { + "device_type": 261, + "endpoint_id": 1, + "in_clusters": [0, 1, 3], + "out_clusters": [3, 6, 8, 768], + "profile_id": 260, + }, + "2": { + "device_type": 259, + "endpoint_id": 2, + "in_clusters": [3], + "out_clusters": [3, 6], + "profile_id": 260, + }, + "3": { + "device_type": None, + "endpoint_id": 3, + "in_clusters": [], + "out_clusters": [], + "profile_id": None, + }, + "4": { + "device_type": None, + "endpoint_id": 4, + "in_clusters": [], + "out_clusters": [], + "profile_id": None, + }, + "5": { + "device_type": None, + "endpoint_id": 5, + "in_clusters": [], + "out_clusters": [], + "profile_id": None, + }, + "6": { + "device_type": None, + "endpoint_id": 6, + "in_clusters": [], + "out_clusters": [], + "profile_id": None, + }, + }, + "entities": [ + "sensor.lumi_lumi_remote_b686opcn01_77665544_power", + "switch.lumi_lumi_remote_b686opcn01_77665544_on_off", + ], + "event_channels": [6, 8, 768, 6], + "manufacturer": "LUMI", + "model": "lumi.remote.b686opcn01", + }, + { + "endpoints": { + "8": { + "device_type": 256, + "endpoint_id": 8, + "in_clusters": [0, 6, 11, 17], + "out_clusters": [0, 6], + "profile_id": 260, + } + }, + "entities": ["light.lumi_lumi_router_77665544_on_off_on_off"], + "event_channels": [6], + "manufacturer": "LUMI", + "model": "lumi.router", + }, + { + "endpoints": { + "1": { + "device_type": 28417, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 25], + "out_clusters": [0, 3, 4, 5, 18, 25], + "profile_id": 260, + }, + "2": { + "device_type": 28418, + "endpoint_id": 2, + "in_clusters": [3, 18], + "out_clusters": [3, 4, 5, 18], + "profile_id": 260, + }, + "3": { + "device_type": 28419, + "endpoint_id": 3, + "in_clusters": [3, 12], + "out_clusters": [3, 4, 5, 12], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_analog_input", + "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_multistate_input", + "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.sensor_cube.aqgl01", + }, + { + "endpoints": { + "1": { + "device_type": 24322, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 25, 1026, 1029, 65535], + "out_clusters": [0, 3, 4, 5, 18, 25, 65535], + "profile_id": 260, + }, + "2": { + "device_type": 24322, + "endpoint_id": 2, + "in_clusters": [3], + "out_clusters": [3, 4, 5, 18], + "profile_id": 260, + }, + "3": { + "device_type": 24323, + "endpoint_id": 3, + "in_clusters": [3], + "out_clusters": [3, 4, 5, 12], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.lumi_lumi_sensor_ht_77665544_power", + "sensor.lumi_lumi_sensor_ht_77665544_temperature", + "sensor.lumi_lumi_sensor_ht_77665544_humidity", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.sensor_ht", + }, + { + "endpoints": { + "1": { + "device_type": 2128, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 25, 65535], + "out_clusters": [0, 3, 4, 5, 6, 8, 25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.lumi_lumi_sensor_magnet_77665544_power", + "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", + ], + "event_channels": [6, 8], + "manufacturer": "LUMI", + "model": "lumi.sensor_magnet", + }, + { + "endpoints": { + "1": { + "device_type": 24321, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 65535], + "out_clusters": [0, 4, 6, 65535], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", + "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", + ], + "event_channels": [6], + "manufacturer": "LUMI", + "model": "lumi.sensor_magnet.aq2", + }, + { + "endpoints": { + "1": { + "device_type": 263, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 1024, 1030, 1280, 65535], + "out_clusters": [0, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", + "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", + "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", + "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.sensor_motion.aq2", + }, + { + "endpoints": { + "1": { + "device_type": 6, + "endpoint_id": 1, + "in_clusters": [0, 1, 3], + "out_clusters": [0, 4, 5, 6, 8, 25], + "profile_id": 260, + } + }, + "entities": ["sensor.lumi_lumi_sensor_switch_77665544_power"], + "event_channels": [6, 8], + "manufacturer": "LUMI", + "model": "lumi.sensor_switch", + }, + { + "endpoints": { + "1": { + "device_type": 6, + "endpoint_id": 1, + "in_clusters": [0, 1, 65535], + "out_clusters": [0, 4, 6, 65535], + "profile_id": 260, + } + }, + "entities": ["sensor.lumi_lumi_sensor_switch_aq2_77665544_power"], + "event_channels": [6], + "manufacturer": "LUMI", + "model": "lumi.sensor_switch.aq2", + }, + { + "endpoints": { + "1": { + "device_type": 6, + "endpoint_id": 1, + "in_clusters": [0, 1, 18], + "out_clusters": [0, 6], + "profile_id": 260, + } + }, + "entities": [ + "sensor.lumi_lumi_sensor_switch_aq3_77665544_multistate_input", + "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", + ], + "event_channels": [6], + "manufacturer": "LUMI", + "model": "lumi.sensor_switch.aq3", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 1280], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", + "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.sensor_wleak.aq1", + }, + { + "endpoints": { + "1": { + "device_type": 10, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 25, 257, 1280], + "out_clusters": [0, 3, 4, 5, 25], + "profile_id": 260, + }, + "2": { + "device_type": 24322, + "endpoint_id": 2, + "in_clusters": [3], + "out_clusters": [3, 4, 5, 18], + "profile_id": 260, + }, + }, + "entities": [ + "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", + "sensor.lumi_lumi_vibration_aq1_77665544_power", + "lock.lumi_lumi_vibration_aq1_77665544_door_lock", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.vibration.aq1", + }, + { + "endpoints": { + "1": { + "device_type": 24321, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 1026, 1027, 1029, 65535], + "out_clusters": [0, 4, 65535], + "profile_id": 260, + } + }, + "entities": [ + "sensor.lumi_lumi_weather_77665544_temperature", + "sensor.lumi_lumi_weather_77665544_power", + "sensor.lumi_lumi_weather_77665544_humidity", + "sensor.lumi_lumi_weather_77665544_pressure", + ], + "event_channels": [], + "manufacturer": "LUMI", + "model": "lumi.weather", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1280], + "out_clusters": [], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.nyce_3010_77665544_ias_zone", + "sensor.nyce_3010_77665544_power", + ], + "event_channels": [], + "manufacturer": "NYCE", + "model": "3010", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1280], + "out_clusters": [], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.nyce_3014_77665544_ias_zone", + "sensor.nyce_3014_77665544_power", + ], + "event_channels": [], + "manufacturer": "NYCE", + "model": "3014", + }, + { + "endpoints": { + "3": { + "device_type": 258, + "endpoint_id": 3, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 64527], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": ["light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off"], + "event_channels": [], + "manufacturer": "OSRAM", + "model": "LIGHTIFY A19 RGBW", + }, + { + "endpoints": { + "1": { + "device_type": 1, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 2821], + "out_clusters": [3, 6, 8, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.osram_lightify_dimming_switch_77665544_on_off", + "sensor.osram_lightify_dimming_switch_77665544_power", + ], + "event_channels": [6, 8], + "manufacturer": "OSRAM", + "model": "LIGHTIFY Dimming Switch", + }, + { + "endpoints": { + "3": { + "device_type": 258, + "endpoint_id": 3, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 64527], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off" + ], + "event_channels": [], + "manufacturer": "OSRAM", + "model": "LIGHTIFY Flex RGBW", + }, + { + "endpoints": { + "3": { + "device_type": 258, + "endpoint_id": 3, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 2820, 64527], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", + "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", + ], + "event_channels": [], + "manufacturer": "OSRAM", + "model": "LIGHTIFY RT Tunable White", + }, + { + "endpoints": { + "3": { + "device_type": 16, + "endpoint_id": 3, + "in_clusters": [0, 3, 4, 5, 6, 2820, 4096, 64527], + "out_clusters": [25], + "profile_id": 49246, + } + }, + "entities": [ + "sensor.osram_plug_01_77665544_electrical_measurement", + "switch.osram_plug_01_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "OSRAM", + "model": "Plug 01", + }, + { + "endpoints": { + "1": { + "device_type": 2064, + "endpoint_id": 1, + "in_clusters": [0, 1, 32, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 25, 768, 4096], + "profile_id": 260, + }, + "2": { + "device_type": 2064, + "endpoint_id": 2, + "in_clusters": [0, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 768, 4096], + "profile_id": 260, + }, + "3": { + "device_type": 2064, + "endpoint_id": 3, + "in_clusters": [0, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 768, 4096], + "profile_id": 260, + }, + "4": { + "device_type": 2064, + "endpoint_id": 4, + "in_clusters": [0, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 768, 4096], + "profile_id": 260, + }, + "5": { + "device_type": 2064, + "endpoint_id": 5, + "in_clusters": [0, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 768, 4096], + "profile_id": 260, + }, + "6": { + "device_type": 2064, + "endpoint_id": 6, + "in_clusters": [0, 4096, 64768], + "out_clusters": [3, 4, 5, 6, 8, 768, 4096], + "profile_id": 260, + }, + }, + "entities": ["sensor.osram_switch_4x_lightify_77665544_power"], + "event_channels": [ + 6, + 8, + 768, + 6, + 8, + 768, + 6, + 8, + 768, + 6, + 8, + 768, + 6, + 8, + 768, + 6, + 8, + 768, + ], + "manufacturer": "OSRAM", + "model": "Switch 4x-LIGHTIFY", + }, + { + "endpoints": { + "1": { + "device_type": 2096, + "endpoint_id": 1, + "in_clusters": [0], + "out_clusters": [0, 3, 4, 5, 6, 8], + "profile_id": 49246, + }, + "2": { + "device_type": 12, + "endpoint_id": 2, + "in_clusters": [0, 1, 3, 15, 64512], + "out_clusters": [25], + "profile_id": 260, + }, + }, + "entities": ["sensor.philips_rwl020_77665544_power"], + "event_channels": [6, 8], + "manufacturer": "Philips", + "model": "RWL020", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.samjin_button_77665544_ias_zone", + "sensor.samjin_button_77665544_temperature", + "sensor.samjin_button_77665544_power", + ], + "event_channels": [], + "manufacturer": "Samjin", + "model": "button", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 64514], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.samjin_multi_77665544_power", + "sensor.samjin_multi_77665544_temperature", + "binary_sensor.samjin_multi_77665544_ias_zone", + "binary_sensor.samjin_multi_77665544_manufacturer_specific", + ], + "event_channels": [], + "manufacturer": "Samjin", + "model": "multi", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.samjin_water_77665544_ias_zone", + "sensor.samjin_water_77665544_power", + "sensor.samjin_water_77665544_temperature", + ], + "event_channels": [], + "manufacturer": "Samjin", + "model": "water", + }, + { + "endpoints": { + "1": { + "device_type": 0, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 4, 5, 6, 2820, 2821], + "out_clusters": [0, 1, 3, 4, 5, 6, 25, 2820, 2821], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.securifi_ltd_unk_model_77665544_on_off", + "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", + "sensor.securifi_ltd_unk_model_77665544_power", + "switch.securifi_ltd_unk_model_77665544_on_off", + ], + "event_channels": [6], + "manufacturer": "Securifi Ltd.", + "model": None, + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", + "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", + "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", + ], + "event_channels": [], + "manufacturer": "Sercomm Corp.", + "model": "SZ-DWS04N_SF", + }, + { + "endpoints": { + "1": { + "device_type": 256, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 4, 5, 6, 1794, 2820, 2821], + "out_clusters": [3, 10, 25, 2821], + "profile_id": 260, + }, + "2": { + "device_type": 259, + "endpoint_id": 2, + "in_clusters": [0, 1, 3], + "out_clusters": [3, 6], + "profile_id": 260, + }, + }, + "entities": [ + "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", + "sensor.sercomm_corp_sz_esw01_77665544_power", + "sensor.sercomm_corp_sz_esw01_77665544_power_2", + "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", + "switch.sercomm_corp_sz_esw01_77665544_on_off", + "light.sercomm_corp_sz_esw01_77665544_on_off", + ], + "event_channels": [6], + "manufacturer": "Sercomm Corp.", + "model": "SZ-ESW01", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1024, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", + "sensor.sercomm_corp_sz_pir04_77665544_temperature", + "sensor.sercomm_corp_sz_pir04_77665544_illuminance", + "sensor.sercomm_corp_sz_pir04_77665544_power", + ], + "event_channels": [], + "manufacturer": "Sercomm Corp.", + "model": "SZ-PIR04", + }, + { + "endpoints": { + "1": { + "device_type": 2, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 2820, 2821, 65281], + "out_clusters": [3, 4, 25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", + "switch.sinope_technologies_rm3250zb_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "Sinope Technologies", + "model": "RM3250ZB", + }, + { + "endpoints": { + "1": { + "device_type": 769, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 513, 516, 1026, 2820, 2821, 65281], + "out_clusters": [25, 65281], + "profile_id": 260, + }, + "196": { + "device_type": 769, + "endpoint_id": 196, + "in_clusters": [1], + "out_clusters": [], + "profile_id": 49757, + }, + }, + "entities": [ + "sensor.sinope_technologies_th1124zb_77665544_temperature", + "sensor.sinope_technologies_th1124zb_77665544_power", + "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", + ], + "event_channels": [], + "manufacturer": "Sinope Technologies", + "model": "TH1124ZB", + }, + { + "endpoints": { + "1": { + "device_type": 2, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 9, 15, 2820], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.smartthings_outletv4_77665544_electrical_measurement", + "switch.smartthings_outletv4_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "SmartThings", + "model": "outletv4", + }, + { + "endpoints": { + "1": { + "device_type": 32768, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 15, 32], + "out_clusters": [3, 25], + "profile_id": 260, + } + }, + "entities": ["device_tracker.smartthings_tagv4_77665544_power"], + "event_channels": [], + "manufacturer": "SmartThings", + "model": "tagv4", + }, + { + "endpoints": { + "1": { + "device_type": 2, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 25], + "out_clusters": [], + "profile_id": 260, + } + }, + "entities": ["switch.third_reality_inc_3rss007z_77665544_on_off"], + "event_channels": [], + "manufacturer": "Third Reality, Inc", + "model": "3RSS007Z", + }, + { + "endpoints": { + "1": { + "device_type": 2, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 4, 5, 6, 25], + "out_clusters": [1], + "profile_id": 260, + } + }, + "entities": [ + "sensor.third_reality_inc_3rss008z_77665544_power", + "switch.third_reality_inc_3rss008z_77665544_on_off", + ], + "event_channels": [], + "manufacturer": "Third Reality, Inc", + "model": "3RSS008Z", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 32, 1026, 1280, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.visonic_mct_340_e_77665544_ias_zone", + "sensor.visonic_mct_340_e_77665544_temperature", + "sensor.visonic_mct_340_e_77665544_power", + ], + "event_channels": [], + "manufacturer": "Visonic", + "model": "MCT-340 E", + }, + { + "endpoints": { + "1": { + "device_type": 1026, + "endpoint_id": 1, + "in_clusters": [0, 1, 3, 21, 32, 1280, 2821], + "out_clusters": [], + "profile_id": 260, + } + }, + "entities": [ + "binary_sensor.netvox_z308e3ed_77665544_ias_zone", + "sensor.netvox_z308e3ed_77665544_power", + ], + "event_channels": [], + "manufacturer": "netvox", + "model": "Z308E3ED", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "light.sengled_e11_g13_77665544_level_on_off", + "sensor.sengled_e11_g13_77665544_smartenergy_metering", + ], + "event_channels": [], + "manufacturer": "sengled", + "model": "E11-G13", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 1794, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.sengled_e12_n14_77665544_smartenergy_metering", + "light.sengled_e12_n14_77665544_level_on_off", + ], + "event_channels": [], + "manufacturer": "sengled", + "model": "E12-N14", + }, + { + "endpoints": { + "1": { + "device_type": 257, + "endpoint_id": 1, + "in_clusters": [0, 3, 4, 5, 6, 8, 768, 1794, 2821], + "out_clusters": [25], + "profile_id": 260, + } + }, + "entities": [ + "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", + "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", + ], + "event_channels": [], + "manufacturer": "sengled", + "model": "Z01-A19NAE26", + }, +] From 2620a95944e2f364487caaa849cf6f3219a50a52 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Dec 2019 17:10:39 +0100 Subject: [PATCH 2611/3953] Improve Withings tests in different time zone (#30326) * Improve Withings tests in different time zone * Address code review comment * Spelling error in code doc --- tests/components/withings/test_common.py | 26 ++++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 6c8c0a4c3106e9..acb69dddf4e6d0 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,5 +1,6 @@ """Tests for the Withings component.""" from datetime import timedelta +from unittest.mock import patch from asynctest import MagicMock import pytest @@ -10,18 +11,10 @@ NotAuthenticatedError, WithingsDataManager, ) -from homeassistant.config import async_process_ha_core_config from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.util import dt -DEFAULT_TIME_ZONE = dt.DEFAULT_TIME_ZONE - - -def teardown(): - """Ensure the time zone is reverted after tests finish.""" - dt.set_default_time_zone(DEFAULT_TIME_ZONE) - @pytest.fixture(name="withings_api") def withings_api_fixture() -> WithingsApi: @@ -34,6 +27,17 @@ def withings_api_fixture() -> WithingsApi: return withings_api +@pytest.fixture +def mock_time_zone(): + """Provide an alternative time zone.""" + patch_time_zone = patch( + "homeassistant.util.dt.DEFAULT_TIME_ZONE", + new=dt.get_time_zone("America/Los_Angeles"), + ) + with patch_time_zone: + yield + + @pytest.fixture(name="data_manager") def data_manager_fixture(hass, withings_api: WithingsApi) -> WithingsDataManager: """Provide data manager.""" @@ -118,13 +122,9 @@ async def test_data_manager_call_throttle_disabled( async def test_data_manager_update_sleep_date_range( - hass: HomeAssistant, data_manager: WithingsDataManager, + hass: HomeAssistant, data_manager: WithingsDataManager, mock_time_zone ) -> None: """Test method.""" - await async_process_ha_core_config( - hass=hass, config={"time_zone": "America/Los_Angeles"} - ) - update_start_time = dt.now() await data_manager.update_sleep() From 30dbed3f983b85ff3f71118e0ccd7ebb75ce32f0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Dec 2019 21:03:21 +0100 Subject: [PATCH 2612/3953] Migrate dsmr tests from coroutine to async/await (#30333) --- tests/components/dsmr/test_sensor.py | 45 ++++++++++++---------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 195345dd489107..81249c04046e5c 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -27,8 +27,7 @@ def mock_connection_factory(monkeypatch): transport = asynctest.Mock(spec=asyncio.Transport) protocol = asynctest.Mock(spec=DSMRProtocol) - @asyncio.coroutine - def connection_factory(*args, **kwargs): + async def connection_factory(*args, **kwargs): """Return mocked out Asyncio classes.""" return (transport, protocol) @@ -46,8 +45,7 @@ def connection_factory(*args, **kwargs): return connection_factory, transport, protocol -@asyncio.coroutine -def test_default_setup(hass, mock_connection_factory): +async def test_default_setup(hass, mock_connection_factory): """Test the default setup.""" (connection_factory, transport, protocol) = mock_connection_factory @@ -67,7 +65,7 @@ def test_default_setup(hass, mock_connection_factory): } with assert_setup_component(1): - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) telegram_callback = connection_factory.call_args_list[0][0][2] @@ -80,7 +78,7 @@ def test_default_setup(hass, mock_connection_factory): telegram_callback(telegram) # after receiving telegram entities need to have the chance to update - yield from asyncio.sleep(0) + await asyncio.sleep(0) # ensure entities have new state value after incoming telegram power_consumption = hass.states.get("sensor.power_consumption") @@ -93,15 +91,14 @@ def test_default_setup(hass, mock_connection_factory): assert power_tariff.attributes.get("unit_of_measurement") == "" -@asyncio.coroutine -def test_derivative(): +async def test_derivative(): """Test calculation of derivative value.""" from dsmr_parser.objects import MBusObject config = {"platform": "dsmr"} entity = DerivativeDSMREntity("test", "1.0.0", config) - yield from entity.async_update() + await entity.async_update() assert entity.state is None, "initial state not unknown" @@ -113,7 +110,7 @@ def test_derivative(): ] ) } - yield from entity.async_update() + await entity.async_update() assert entity.state is None, "state after first update should still be unknown" @@ -125,7 +122,7 @@ def test_derivative(): ] ) } - yield from entity.async_update() + await entity.async_update() assert ( abs(entity.state - 0.033) < 0.00001 @@ -134,22 +131,20 @@ def test_derivative(): assert entity.unit_of_measurement == "m3/h" -@asyncio.coroutine -def test_tcp(hass, mock_connection_factory): +async def test_tcp(hass, mock_connection_factory): """If proper config provided TCP connection should be made.""" (connection_factory, transport, protocol) = mock_connection_factory config = {"platform": "dsmr", "host": "localhost", "port": 1234} with assert_setup_component(1): - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) assert connection_factory.call_args_list[0][0][0] == "localhost" assert connection_factory.call_args_list[0][0][1] == "1234" -@asyncio.coroutine -def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): +async def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): """Connection should be retried on error during setup.""" (connection_factory, transport, protocol) = mock_connection_factory @@ -164,15 +159,14 @@ def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): "homeassistant.components.dsmr.sensor.create_dsmr_reader", first_fail_connection_factory, ) - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) # wait for sleep to resolve - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert first_fail_connection_factory.call_count == 2, "connecting not retried" -@asyncio.coroutine -def test_reconnect(hass, monkeypatch, mock_connection_factory): +async def test_reconnect(hass, monkeypatch, mock_connection_factory): """If transport disconnects, the connection should be retried.""" (connection_factory, transport, protocol) = mock_connection_factory config = {"platform": "dsmr", "reconnect_interval": 0} @@ -182,26 +176,25 @@ def test_reconnect(hass, monkeypatch, mock_connection_factory): # Handshake so that `hass.async_block_till_done()` doesn't cycle forever closed2 = asyncio.Event() - @asyncio.coroutine - def wait_closed(): - yield from closed.wait() + async def wait_closed(): + await closed.wait() closed2.set() closed.clear() protocol.wait_closed = wait_closed - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) assert connection_factory.call_count == 1 # indicate disconnect, release wait lock and allow reconnect to happen closed.set() # wait for lock set to resolve - yield from closed2.wait() + await closed2.wait() closed2.clear() assert not closed.is_set() closed.set() - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert connection_factory.call_count >= 2, "connecting not retried" From cd8f954a4d57130921c3023a5d53f386f9309ada Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Dec 2019 21:04:24 +0100 Subject: [PATCH 2613/3953] Migrate wunderground tests from coroutine to async/await (#30330) --- tests/components/wunderground/test_sensor.py | 32 ++++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/tests/components/wunderground/test_sensor.py b/tests/components/wunderground/test_sensor.py index 70264249157040..bfe9f83fbc336c 100644 --- a/tests/components/wunderground/test_sensor.py +++ b/tests/components/wunderground/test_sensor.py @@ -1,6 +1,4 @@ """The tests for the WUnderground platform.""" -import asyncio - import aiohttp from pytest import raises @@ -59,39 +57,35 @@ ) -@asyncio.coroutine -def test_setup(hass, aioclient_mock): +async def test_setup(hass, aioclient_mock): """Test that the component is loaded.""" aioclient_mock.get(URL, text=load_fixture("wunderground-valid.json")) with assert_setup_component(1, "sensor"): - yield from async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) + await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) -@asyncio.coroutine -def test_setup_pws(hass, aioclient_mock): +async def test_setup_pws(hass, aioclient_mock): """Test that the component is loaded with PWS id.""" aioclient_mock.get(PWS_URL, text=load_fixture("wunderground-valid.json")) with assert_setup_component(1, "sensor"): - yield from async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG_PWS}) + await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG_PWS}) -@asyncio.coroutine -def test_setup_invalid(hass, aioclient_mock): +async def test_setup_invalid(hass, aioclient_mock): """Test that the component is not loaded with invalid config.""" aioclient_mock.get(INVALID_URL, text=load_fixture("wunderground-error.json")) with assert_setup_component(0, "sensor"): - yield from async_setup_component(hass, "sensor", {"sensor": INVALID_CONFIG}) + await async_setup_component(hass, "sensor", {"sensor": INVALID_CONFIG}) -@asyncio.coroutine -def test_sensor(hass, aioclient_mock): +async def test_sensor(hass, aioclient_mock): """Test the WUnderground sensor class and methods.""" aioclient_mock.get(URL, text=load_fixture("wunderground-valid.json")) - yield from async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) + await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) state = hass.states.get("sensor.pws_weather") assert state.state == "Clear" @@ -132,20 +126,18 @@ def test_sensor(hass, aioclient_mock): assert state.attributes["unit_of_measurement"] == LENGTH_INCHES -@asyncio.coroutine -def test_connect_failed(hass, aioclient_mock): +async def test_connect_failed(hass, aioclient_mock): """Test the WUnderground connection error.""" aioclient_mock.get(URL, exc=aiohttp.ClientError()) with raises(PlatformNotReady): - yield from wunderground.async_setup_platform(hass, VALID_CONFIG, lambda _: None) + await wunderground.async_setup_platform(hass, VALID_CONFIG, lambda _: None) -@asyncio.coroutine -def test_invalid_data(hass, aioclient_mock): +async def test_invalid_data(hass, aioclient_mock): """Test the WUnderground invalid data.""" aioclient_mock.get(URL, text=load_fixture("wunderground-invalid.json")) - yield from async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) + await async_setup_component(hass, "sensor", {"sensor": VALID_CONFIG}) for condition in VALID_CONFIG["monitored_conditions"]: state = hass.states.get("sensor.pws_" + condition) From d8d75d882be0b485c1f54969eaae6713ec71f01d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Dec 2019 21:52:45 +0100 Subject: [PATCH 2614/3953] Bump pyps4-2ndscreen to 1.0.4 (#30327) --- homeassistant/components/ps4/manifest.json | 8 ++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index add52231a07435..c7e6d1d9ba7baf 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -3,11 +3,7 @@ "name": "Ps4", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", - "requirements": [ - "pyps4-2ndscreen==1.0.3" - ], + "requirements": ["pyps4-2ndscreen==1.0.4"], "dependencies": [], - "codeowners": [ - "@ktnrg45" - ] + "codeowners": ["@ktnrg45"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0e163221d118f4..31c45d2e3b9f42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1448,7 +1448,7 @@ pypjlink2==1.2.0 pypoint==1.1.2 # homeassistant.components.ps4 -pyps4-2ndscreen==1.0.3 +pyps4-2ndscreen==1.0.4 # homeassistant.components.qwikswitch pyqwikswitch==0.93 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25d16e27ce82c2..f3ba482a977199 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -487,7 +487,7 @@ pyotp==2.3.0 pypoint==1.1.2 # homeassistant.components.ps4 -pyps4-2ndscreen==1.0.3 +pyps4-2ndscreen==1.0.4 # homeassistant.components.qwikswitch pyqwikswitch==0.93 From f11d39f8ebf281a2069dc9f01777869c59405be4 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 31 Dec 2019 21:54:31 +0100 Subject: [PATCH 2615/3953] Bump alarmdecoder to 1.13.9 (#30303) --- homeassistant/components/alarmdecoder/manifest.json | 4 +--- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index 5ab69d94cf2218..0f41405d431aa2 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -2,9 +2,7 @@ "domain": "alarmdecoder", "name": "Alarmdecoder", "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", - "requirements": [ - "alarmdecoder==1.13.2" - ], + "requirements": ["alarmdecoder==1.13.9"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 31c45d2e3b9f42..3d60bc22d11799 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -202,7 +202,7 @@ airly==0.0.2 aladdin_connect==0.3 # homeassistant.components.alarmdecoder -alarmdecoder==1.13.2 +alarmdecoder==1.13.9 # homeassistant.components.alpha_vantage alpha_vantage==2.1.2 From 687a052d40150c41263b8aea4ac1c8b036e350ef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Dec 2019 22:48:43 +0100 Subject: [PATCH 2616/3953] Migrate python_script tests from coroutine to async/await (#30335) --- tests/components/python_script/test_init.py | 92 ++++++++------------- 1 file changed, 36 insertions(+), 56 deletions(-) diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index b5879479837588..f61f00047231bc 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -1,5 +1,4 @@ """Test the python_script component.""" -import asyncio import logging from unittest.mock import mock_open, patch @@ -10,8 +9,7 @@ from tests.common import patch_yaml_files -@asyncio.coroutine -def test_setup(hass): +async def test_setup(hass): """Test we can discover scripts.""" scripts = [ "/some/config/dir/python_scripts/hello.py", @@ -20,7 +18,7 @@ def test_setup(hass): with patch( "homeassistant.components.python_script.os.path.isdir", return_value=True ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): - res = yield from async_setup_component(hass, "python_script", {}) + res = await async_setup_component(hass, "python_script", {}) assert res assert hass.services.has_service("python_script", "hello") @@ -31,7 +29,7 @@ def test_setup(hass): mock_open(read_data="fake source"), create=True, ), patch("homeassistant.components.python_script.execute") as mock_ex: - yield from hass.services.async_call( + await hass.services.async_call( "python_script", "hello", {"some": "data"}, blocking=True ) @@ -44,20 +42,18 @@ def test_setup(hass): assert data == {"some": "data"} -@asyncio.coroutine -def test_setup_fails_on_no_dir(hass, caplog): +async def test_setup_fails_on_no_dir(hass, caplog): """Test we fail setup when no dir found.""" with patch( "homeassistant.components.python_script.os.path.isdir", return_value=False ): - res = yield from async_setup_component(hass, "python_script", {}) + res = await async_setup_component(hass, "python_script", {}) assert not res assert "Folder python_scripts not found in configuration folder" in caplog.text -@asyncio.coroutine -def test_execute_with_data(hass, caplog): +async def test_execute_with_data(hass, caplog): """Test executing a script.""" caplog.set_level(logging.WARNING) source = """ @@ -65,7 +61,7 @@ def test_execute_with_data(hass, caplog): """ hass.async_add_job(execute, hass, "test.py", source, {"name": "paulus"}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.is_state("test.entity", "paulus") @@ -73,8 +69,7 @@ def test_execute_with_data(hass, caplog): assert caplog.text == "" -@asyncio.coroutine -def test_execute_warns_print(hass, caplog): +async def test_execute_warns_print(hass, caplog): """Test print triggers warning.""" caplog.set_level(logging.WARNING) source = """ @@ -82,13 +77,12 @@ def test_execute_warns_print(hass, caplog): """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Don't use print() inside scripts." in caplog.text -@asyncio.coroutine -def test_execute_logging(hass, caplog): +async def test_execute_logging(hass, caplog): """Test logging works.""" caplog.set_level(logging.INFO) source = """ @@ -96,13 +90,12 @@ def test_execute_logging(hass, caplog): """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Logging from inside script" in caplog.text -@asyncio.coroutine -def test_execute_compile_error(hass, caplog): +async def test_execute_compile_error(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) source = """ @@ -110,13 +103,12 @@ def test_execute_compile_error(hass, caplog): """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Error loading script test.py" in caplog.text -@asyncio.coroutine -def test_execute_runtime_error(hass, caplog): +async def test_execute_runtime_error(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) source = """ @@ -124,13 +116,12 @@ def test_execute_runtime_error(hass, caplog): """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Error executing script: boom" in caplog.text -@asyncio.coroutine -def test_accessing_async_methods(hass, caplog): +async def test_accessing_async_methods(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) source = """ @@ -138,13 +129,12 @@ def test_accessing_async_methods(hass, caplog): """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Not allowed to access async methods" in caplog.text -@asyncio.coroutine -def test_using_complex_structures(hass, caplog): +async def test_using_complex_structures(hass, caplog): """Test that dicts and lists work.""" caplog.set_level(logging.INFO) source = """ @@ -154,13 +144,12 @@ def test_using_complex_structures(hass, caplog): """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Logging from inside script: 1 3" in caplog.text -@asyncio.coroutine -def test_accessing_forbidden_methods(hass, caplog): +async def test_accessing_forbidden_methods(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) @@ -172,12 +161,11 @@ def test_accessing_forbidden_methods(hass, caplog): }.items(): caplog.records.clear() hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Not allowed to access {}".format(name) in caplog.text -@asyncio.coroutine -def test_iterating(hass): +async def test_iterating(hass): """Test compile error logs error.""" source = """ for i in [1, 2]: @@ -185,14 +173,13 @@ def test_iterating(hass): """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.is_state("hello.1", "world") assert hass.states.is_state("hello.2", "world") -@asyncio.coroutine -def test_unpacking_sequence(hass, caplog): +async def test_unpacking_sequence(hass, caplog): """Test compile error logs error.""" caplog.set_level(logging.ERROR) source = """ @@ -204,7 +191,7 @@ def test_unpacking_sequence(hass, caplog): """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.is_state("hello.a", "1") assert hass.states.is_state("hello.b", "2") @@ -214,8 +201,7 @@ def test_unpacking_sequence(hass, caplog): assert caplog.text == "" -@asyncio.coroutine -def test_execute_sorted(hass, caplog): +async def test_execute_sorted(hass, caplog): """Test sorted() function.""" caplog.set_level(logging.ERROR) source = """ @@ -226,7 +212,7 @@ def test_execute_sorted(hass, caplog): hass.states.set('hello.c', a[2]) """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.is_state("hello.a", "1") assert hass.states.is_state("hello.b", "2") @@ -235,8 +221,7 @@ def test_execute_sorted(hass, caplog): assert caplog.text == "" -@asyncio.coroutine -def test_exposed_modules(hass, caplog): +async def test_exposed_modules(hass, caplog): """Test datetime and time modules exposed.""" caplog.set_level(logging.ERROR) source = """ @@ -248,7 +233,7 @@ def test_exposed_modules(hass, caplog): """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.is_state("module.time", "1986") assert hass.states.is_state("module.time_strptime", "12:34") @@ -258,8 +243,7 @@ def test_exposed_modules(hass, caplog): assert caplog.text == "" -@asyncio.coroutine -def test_execute_functions(hass, caplog): +async def test_execute_functions(hass, caplog): """Test functions defined in script can call one another.""" caplog.set_level(logging.ERROR) source = """ @@ -273,7 +257,7 @@ def b(): b() """ hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert hass.states.is_state("hello.a", "one") assert hass.states.is_state("hello.b", "two") @@ -281,8 +265,7 @@ def b(): assert caplog.text == "" -@asyncio.coroutine -def test_reload(hass): +async def test_reload(hass): """Test we can re-discover scripts.""" scripts = [ "/some/config/dir/python_scripts/hello.py", @@ -291,7 +274,7 @@ def test_reload(hass): with patch( "homeassistant.components.python_script.os.path.isdir", return_value=True ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): - res = yield from async_setup_component(hass, "python_script", {}) + res = await async_setup_component(hass, "python_script", {}) assert res assert hass.services.has_service("python_script", "hello") @@ -305,9 +288,7 @@ def test_reload(hass): with patch( "homeassistant.components.python_script.os.path.isdir", return_value=True ), patch("homeassistant.components.python_script.glob.iglob", return_value=scripts): - yield from hass.services.async_call( - "python_script", "reload", {}, blocking=True - ) + await hass.services.async_call("python_script", "reload", {}, blocking=True) assert not hass.services.has_service("python_script", "hello") assert hass.services.has_service("python_script", "hello2") @@ -410,8 +391,7 @@ async def test_service_descriptions(hass): ) -@asyncio.coroutine -def test_sleep_warns_one(hass, caplog): +async def test_sleep_warns_one(hass, caplog): """Test time.sleep warns once.""" caplog.set_level(logging.WARNING) source = """ @@ -421,6 +401,6 @@ def test_sleep_warns_one(hass, caplog): with patch("homeassistant.components.python_script.time.sleep"): hass.async_add_job(execute, hass, "test.py", source, {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert caplog.text.count("time.sleep") == 1 From 0280862780100684d17ee86272ebb11a1d5925b8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Dec 2019 22:58:40 +0100 Subject: [PATCH 2617/3953] Migrate api tests from coroutine to async/await (#30338) --- tests/components/api/test_init.py | 157 +++++++++++++----------------- 1 file changed, 65 insertions(+), 92 deletions(-) diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index a6dbf3f1d9627f..28cad6dd04b976 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -1,6 +1,5 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access -import asyncio import json from unittest.mock import patch @@ -23,27 +22,23 @@ def mock_api_client(hass, hass_client): return hass.loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_api_list_state_entities(hass, mock_api_client): +async def test_api_list_state_entities(hass, mock_api_client): """Test if the debug interface allows us to list state entities.""" hass.states.async_set("test.entity", "hello") - resp = yield from mock_api_client.get(const.URL_API_STATES) + resp = await mock_api_client.get(const.URL_API_STATES) assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() remote_data = [ha.State.from_dict(item) for item in json] assert remote_data == hass.states.async_all() -@asyncio.coroutine -def test_api_get_state(hass, mock_api_client): +async def test_api_get_state(hass, mock_api_client): """Test if the debug interface allows us to get a state.""" hass.states.async_set("hello.world", "nice", {"attr": 1}) - resp = yield from mock_api_client.get( - const.URL_API_STATES_ENTITY.format("hello.world") - ) + resp = await mock_api_client.get(const.URL_API_STATES_ENTITY.format("hello.world")) assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() data = ha.State.from_dict(json) @@ -54,21 +49,19 @@ def test_api_get_state(hass, mock_api_client): assert data.attributes == state.attributes -@asyncio.coroutine -def test_api_get_non_existing_state(hass, mock_api_client): +async def test_api_get_non_existing_state(hass, mock_api_client): """Test if the debug interface allows us to get a state.""" - resp = yield from mock_api_client.get( + resp = await mock_api_client.get( const.URL_API_STATES_ENTITY.format("does_not_exist") ) assert resp.status == 404 -@asyncio.coroutine -def test_api_state_change(hass, mock_api_client): +async def test_api_state_change(hass, mock_api_client): """Test if we can change the state of an entity that exists.""" hass.states.async_set("test.test", "not_to_be_set") - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test.test"), json={"state": "debug_state_change2"}, ) @@ -77,12 +70,11 @@ def test_api_state_change(hass, mock_api_client): # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_state_change_of_non_existing_entity(hass, mock_api_client): +async def test_api_state_change_of_non_existing_entity(hass, mock_api_client): """Test if changing a state of a non existing entity is possible.""" new_state = "debug_state_change" - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test_entity.that_does_not_exist"), json={"state": new_state}, ) @@ -93,10 +85,9 @@ def test_api_state_change_of_non_existing_entity(hass, mock_api_client): # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_state_change_with_bad_data(hass, mock_api_client): +async def test_api_state_change_with_bad_data(hass, mock_api_client): """Test if API sends appropriate error if we omit state.""" - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test_entity.that_does_not_exist"), json={} ) @@ -104,17 +95,16 @@ def test_api_state_change_with_bad_data(hass, mock_api_client): # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_state_change_to_zero_value(hass, mock_api_client): +async def test_api_state_change_to_zero_value(hass, mock_api_client): """Test if changing a state to a zero value is possible.""" - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test_entity.with_zero_state"), json={"state": 0}, ) assert resp.status == 201 - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test_entity.with_zero_state"), json={"state": 0.0}, ) @@ -123,8 +113,7 @@ def test_api_state_change_to_zero_value(hass, mock_api_client): # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_state_change_push(hass, mock_api_client): +async def test_api_state_change_push(hass, mock_api_client): """Test if we can push a change the state of an entity.""" hass.states.async_set("test.test", "not_to_be_set") @@ -137,23 +126,22 @@ def event_listener(event): hass.bus.async_listen(const.EVENT_STATE_CHANGED, event_listener) - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test.test"), json={"state": "not_to_be_set"} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(events) == 0 - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_STATES_ENTITY.format("test.test"), json={"state": "not_to_be_set", "force_update": True}, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(events) == 1 # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_fire_event_with_no_data(hass, mock_api_client): +async def test_api_fire_event_with_no_data(hass, mock_api_client): """Test if the API allows us to fire an event.""" test_value = [] @@ -164,17 +152,14 @@ def listener(event): hass.bus.async_listen_once("test.event_no_data", listener) - yield from mock_api_client.post( - const.URL_API_EVENTS_EVENT.format("test.event_no_data") - ) - yield from hass.async_block_till_done() + await mock_api_client.post(const.URL_API_EVENTS_EVENT.format("test.event_no_data")) + await hass.async_block_till_done() assert len(test_value) == 1 # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_fire_event_with_data(hass, mock_api_client): +async def test_api_fire_event_with_data(hass, mock_api_client): """Test if the API allows us to fire an event.""" test_value = [] @@ -189,18 +174,17 @@ def listener(event): hass.bus.async_listen_once("test_event_with_data", listener) - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_EVENTS_EVENT.format("test_event_with_data"), json={"test": 1} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(test_value) == 1 # pylint: disable=invalid-name -@asyncio.coroutine -def test_api_fire_event_with_invalid_json(hass, mock_api_client): +async def test_api_fire_event_with_invalid_json(hass, mock_api_client): """Test if the API allows us to fire an event.""" test_value = [] @@ -211,33 +195,32 @@ def listener(event): hass.bus.async_listen_once("test_event_bad_data", listener) - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_EVENTS_EVENT.format("test_event_bad_data"), data=json.dumps("not an object"), ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert resp.status == 400 assert len(test_value) == 0 # Try now with valid but unusable JSON - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_EVENTS_EVENT.format("test_event_bad_data"), data=json.dumps([1, 2, 3]), ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert resp.status == 400 assert len(test_value) == 0 -@asyncio.coroutine -def test_api_get_config(hass, mock_api_client): +async def test_api_get_config(hass, mock_api_client): """Test the return of the configuration.""" - resp = yield from mock_api_client.get(const.URL_API_CONFIG) - result = yield from resp.json() + resp = await mock_api_client.get(const.URL_API_CONFIG) + result = await resp.json() if "components" in result: result["components"] = set(result["components"]) if "whitelist_external_dirs" in result: @@ -246,19 +229,17 @@ def test_api_get_config(hass, mock_api_client): assert hass.config.as_dict() == result -@asyncio.coroutine -def test_api_get_components(hass, mock_api_client): +async def test_api_get_components(hass, mock_api_client): """Test the return of the components.""" - resp = yield from mock_api_client.get(const.URL_API_COMPONENTS) - result = yield from resp.json() + resp = await mock_api_client.get(const.URL_API_COMPONENTS) + result = await resp.json() assert set(result) == hass.config.components -@asyncio.coroutine -def test_api_get_event_listeners(hass, mock_api_client): +async def test_api_get_event_listeners(hass, mock_api_client): """Test if we can get the list of events being listened for.""" - resp = yield from mock_api_client.get(const.URL_API_EVENTS) - data = yield from resp.json() + resp = await mock_api_client.get(const.URL_API_EVENTS) + data = await resp.json() local = hass.bus.async_listeners() @@ -268,11 +249,10 @@ def test_api_get_event_listeners(hass, mock_api_client): assert len(local) == 0 -@asyncio.coroutine -def test_api_get_services(hass, mock_api_client): +async def test_api_get_services(hass, mock_api_client): """Test if we can get a dict describing current services.""" - resp = yield from mock_api_client.get(const.URL_API_SERVICES) - data = yield from resp.json() + resp = await mock_api_client.get(const.URL_API_SERVICES) + data = await resp.json() local_services = hass.services.async_services() for serv_domain in data: @@ -281,8 +261,7 @@ def test_api_get_services(hass, mock_api_client): assert serv_domain["services"] == local -@asyncio.coroutine -def test_api_call_service_no_data(hass, mock_api_client): +async def test_api_call_service_no_data(hass, mock_api_client): """Test if the API allows us to call a service.""" test_value = [] @@ -293,15 +272,14 @@ def listener(service_call): hass.services.async_register("test_domain", "test_service", listener) - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_SERVICES_SERVICE.format("test_domain", "test_service") ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(test_value) == 1 -@asyncio.coroutine -def test_api_call_service_with_data(hass, mock_api_client): +async def test_api_call_service_with_data(hass, mock_api_client): """Test if the API allows us to call a service.""" test_value = [] @@ -316,88 +294,83 @@ def listener(service_call): hass.services.async_register("test_domain", "test_service", listener) - yield from mock_api_client.post( + await mock_api_client.post( const.URL_API_SERVICES_SERVICE.format("test_domain", "test_service"), json={"test": 1}, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(test_value) == 1 -@asyncio.coroutine -def test_api_template(hass, mock_api_client): +async def test_api_template(hass, mock_api_client): """Test the template API.""" hass.states.async_set("sensor.temperature", 10) - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_TEMPLATE, json={"template": "{{ states.sensor.temperature.state }}"}, ) - body = yield from resp.text() + body = await resp.text() assert body == "10" -@asyncio.coroutine -def test_api_template_error(hass, mock_api_client): +async def test_api_template_error(hass, mock_api_client): """Test the template API.""" hass.states.async_set("sensor.temperature", 10) - resp = yield from mock_api_client.post( + resp = await mock_api_client.post( const.URL_API_TEMPLATE, json={"template": "{{ states.sensor.temperature.state"} ) assert resp.status == 400 -@asyncio.coroutine -def test_stream(hass, mock_api_client): +async def test_stream(hass, mock_api_client): """Test the stream.""" listen_count = _listen_count(hass) - resp = yield from mock_api_client.get(const.URL_API_STREAM) + resp = await mock_api_client.get(const.URL_API_STREAM) assert resp.status == 200 assert listen_count + 1 == _listen_count(hass) hass.bus.async_fire("test_event") - data = yield from _stream_next_event(resp.content) + data = await _stream_next_event(resp.content) assert data["event_type"] == "test_event" -@asyncio.coroutine -def test_stream_with_restricted(hass, mock_api_client): +async def test_stream_with_restricted(hass, mock_api_client): """Test the stream with restrictions.""" listen_count = _listen_count(hass) - resp = yield from mock_api_client.get( + resp = await mock_api_client.get( "{}?restrict=test_event1,test_event3".format(const.URL_API_STREAM) ) assert resp.status == 200 assert listen_count + 1 == _listen_count(hass) hass.bus.async_fire("test_event1") - data = yield from _stream_next_event(resp.content) + data = await _stream_next_event(resp.content) assert data["event_type"] == "test_event1" hass.bus.async_fire("test_event2") hass.bus.async_fire("test_event3") - data = yield from _stream_next_event(resp.content) + data = await _stream_next_event(resp.content) assert data["event_type"] == "test_event3" -@asyncio.coroutine -def _stream_next_event(stream): +async def _stream_next_event(stream): """Read the stream for next event while ignoring ping.""" while True: last_new_line = False data = b"" while True: - dat = yield from stream.read(1) + dat = await stream.read(1) if dat == b"\n" and last_new_line: break data += dat From 2ac5537495ccdf865b33946e050664d515e75fa2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Dec 2019 23:48:17 +0100 Subject: [PATCH 2618/3953] Migrate alexa tests from coroutine to async/await (#30332) --- .../components/alexa/test_flash_briefings.py | 22 +++--- tests/components/alexa/test_intent.py | 75 ++++++++----------- 2 files changed, 41 insertions(+), 56 deletions(-) diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index 7a20c05ed86b3c..d3fe28d227d871 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -1,6 +1,5 @@ """The tests for the Alexa component.""" # pylint: disable=protected-access -import asyncio import datetime import pytest @@ -67,21 +66,19 @@ def _flash_briefing_req(client, briefing_id): return client.get("/api/alexa/flash_briefings/{}".format(briefing_id)) -@asyncio.coroutine -def test_flash_briefing_invalid_id(alexa_client): +async def test_flash_briefing_invalid_id(alexa_client): """Test an invalid Flash Briefing ID.""" - req = yield from _flash_briefing_req(alexa_client, 10000) + req = await _flash_briefing_req(alexa_client, 10000) assert req.status == 404 - text = yield from req.text() + text = await req.text() assert text == "" -@asyncio.coroutine -def test_flash_briefing_date_from_str(alexa_client): +async def test_flash_briefing_date_from_str(alexa_client): """Test the response has a valid date parsed from string.""" - req = yield from _flash_briefing_req(alexa_client, "weather") + req = await _flash_briefing_req(alexa_client, "weather") assert req.status == 200 - data = yield from req.json() + data = await req.json() assert isinstance( datetime.datetime.strptime( data[0].get(const.ATTR_UPDATE_DATE), const.DATE_FORMAT @@ -90,8 +87,7 @@ def test_flash_briefing_date_from_str(alexa_client): ) -@asyncio.coroutine -def test_flash_briefing_valid(alexa_client): +async def test_flash_briefing_valid(alexa_client): """Test the response is valid.""" data = [ { @@ -104,9 +100,9 @@ def test_flash_briefing_valid(alexa_client): } ] - req = yield from _flash_briefing_req(alexa_client, "news_audio") + req = await _flash_briefing_req(alexa_client, "news_audio") assert req.status == 200 - json = yield from req.json() + json = await req.json() assert isinstance( datetime.datetime.strptime( json[0].get(const.ATTR_UPDATE_DATE), const.DATE_FORMAT diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index ac41e6d3b4d1c2..962ba677403c56 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -1,6 +1,5 @@ """The tests for the Alexa component.""" # pylint: disable=protected-access -import asyncio import json import pytest @@ -115,8 +114,7 @@ def _intent_req(client, data=None): ) -@asyncio.coroutine -def test_intent_launch_request(alexa_client): +async def test_intent_launch_request(alexa_client): """Test the launch of a request.""" data = { "version": "1.0", @@ -133,15 +131,14 @@ def test_intent_launch_request(alexa_client): "timestamp": "2015-05-13T12:34:56Z", }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "LaunchRequest has been received." -@asyncio.coroutine -def test_intent_launch_request_not_configured(alexa_client): +async def test_intent_launch_request_not_configured(alexa_client): """Test the launch of a request.""" data = { "version": "1.0", @@ -160,15 +157,14 @@ def test_intent_launch_request_not_configured(alexa_client): "timestamp": "2015-05-13T12:34:56Z", }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "This intent is not yet configured within Home Assistant." -@asyncio.coroutine -def test_intent_request_with_slots(alexa_client): +async def test_intent_request_with_slots(alexa_client): """Test a request with slots.""" data = { "version": "1.0", @@ -195,15 +191,14 @@ def test_intent_request_with_slots(alexa_client): }, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is virgo." -@asyncio.coroutine -def test_intent_request_with_slots_and_synonym_resolution(alexa_client): +async def test_intent_request_with_slots_and_synonym_resolution(alexa_client): """Test a request with slots and a name synonym.""" data = { "version": "1.0", @@ -249,15 +244,14 @@ def test_intent_request_with_slots_and_synonym_resolution(alexa_client): }, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is Virgo." -@asyncio.coroutine -def test_intent_request_with_slots_and_multi_synonym_resolution(alexa_client): +async def test_intent_request_with_slots_and_multi_synonym_resolution(alexa_client): """Test a request with slots and multiple name synonyms.""" data = { "version": "1.0", @@ -303,15 +297,14 @@ def test_intent_request_with_slots_and_multi_synonym_resolution(alexa_client): }, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is V zodiac." -@asyncio.coroutine -def test_intent_request_with_slots_but_no_value(alexa_client): +async def test_intent_request_with_slots_but_no_value(alexa_client): """Test a request with slots but no value.""" data = { "version": "1.0", @@ -338,15 +331,14 @@ def test_intent_request_with_slots_but_no_value(alexa_client): }, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You told us your sign is ." -@asyncio.coroutine -def test_intent_request_without_slots(hass, alexa_client): +async def test_intent_request_without_slots(hass, alexa_client): """Test a request without slots.""" data = { "version": "1.0", @@ -370,9 +362,9 @@ def test_intent_request_without_slots(hass, alexa_client): "intent": {"name": "WhereAreWeIntent"}, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - json = yield from req.json() + json = await req.json() text = json.get("response", {}).get("outputSpeech", {}).get("text") assert text == "Anne Therese is at unknown and Paulus is at unknown" @@ -380,15 +372,14 @@ def test_intent_request_without_slots(hass, alexa_client): hass.states.async_set("device_tracker.paulus", "home") hass.states.async_set("device_tracker.anne_therese", "home") - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - json = yield from req.json() + json = await req.json() text = json.get("response", {}).get("outputSpeech", {}).get("text") assert text == "You are both home, you silly" -@asyncio.coroutine -def test_intent_request_calling_service(alexa_client): +async def test_intent_request_calling_service(alexa_client): """Test a request for calling a service.""" data = { "version": "1.0", @@ -410,7 +401,7 @@ def test_intent_request_calling_service(alexa_client): }, } call_count = len(calls) - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 assert call_count + 1 == len(calls) call = calls[-1] @@ -419,15 +410,14 @@ def test_intent_request_calling_service(alexa_client): assert call.data.get("entity_id") == ["switch.test"] assert call.data.get("hello") == "virgo" - data = yield from req.json() + data = await req.json() assert data["response"]["card"]["title"] == "Card title for virgo" assert data["response"]["card"]["content"] == "Card content: virgo" assert data["response"]["outputSpeech"]["type"] == "PlainText" assert data["response"]["outputSpeech"]["text"] == "Service called for virgo" -@asyncio.coroutine -def test_intent_session_ended_request(alexa_client): +async def test_intent_session_ended_request(alexa_client): """Test the request for ending the session.""" data = { "version": "1.0", @@ -452,14 +442,13 @@ def test_intent_session_ended_request(alexa_client): }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - text = yield from req.text() + text = await req.text() assert text == "" -@asyncio.coroutine -def test_intent_from_built_in_intent_library(alexa_client): +async def test_intent_from_built_in_intent_library(alexa_client): """Test intents from the Built-in Intent Library.""" data = { "request": { @@ -490,8 +479,8 @@ def test_intent_from_built_in_intent_library(alexa_client): "application": {"applicationId": APPLICATION_ID}, }, } - req = yield from _intent_req(alexa_client, data) + req = await _intent_req(alexa_client, data) assert req.status == 200 - data = yield from req.json() + data = await req.json() text = data.get("response", {}).get("outputSpeech", {}).get("text") assert text == "Playing the shins." From 194cb8dbf58c4a5e97fd62d50a5416ccb4281ea7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Dec 2019 23:49:00 +0100 Subject: [PATCH 2619/3953] Migrate xiaomi_miio tests from coroutine to async/await (#30329) --- tests/components/xiaomi_miio/test_vacuum.py | 52 ++++++++++----------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 6b2eb87f1538c7..7ebc59a964a350 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -1,5 +1,4 @@ """The tests for the Xiaomi vacuum platform.""" -import asyncio from datetime import time, timedelta from unittest import mock @@ -149,11 +148,10 @@ def mirobo_errors_fixture(): yield mock_vacuum -@asyncio.coroutine -def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors): +async def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_error" - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -165,7 +163,7 @@ def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors): } }, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Initializing with host 127.0.0.1 (token 12345...)" in caplog.text assert mock_mirobo_errors.status.call_count == 1 @@ -173,13 +171,12 @@ def test_xiaomi_exceptions(hass, caplog, mock_mirobo_errors): assert "Got OSError while fetching the state" in caplog.text -@asyncio.coroutine -def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): +async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_1" entity_id = "{}.{}".format(DOMAIN, entity_name) - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -191,7 +188,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): } }, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Initializing with host 127.0.0.1 (token 12345...)" in caplog.text @@ -223,7 +220,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): assert state.attributes.get(ATTR_CLEANING_TOTAL_TIME) == 695 # Call services - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_START, {"entity_id": entity_id}, blocking=True ) mock_mirobo_is_got_error.assert_has_calls( @@ -232,28 +229,28 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_STOP, {"entity_id": entity_id}, blocking=True ) mock_mirobo_is_got_error.assert_has_calls([mock.call.stop()], any_order=True) mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_RETURN_TO_BASE, {"entity_id": entity_id}, blocking=True ) mock_mirobo_is_got_error.assert_has_calls([mock.call.home()], any_order=True) mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_LOCATE, {"entity_id": entity_id}, blocking=True ) mock_mirobo_is_got_error.assert_has_calls([mock.call.find()], any_order=True) mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_CLEAN_SPOT, {"entity_id": entity_id}, blocking=True ) mock_mirobo_is_got_error.assert_has_calls([mock.call.spot()], any_order=True) @@ -261,7 +258,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.reset_mock() # Set speed service: - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_SPEED, {"entity_id": entity_id, "fan_speed": 60}, @@ -273,7 +270,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_SPEED, {"entity_id": entity_id, "fan_speed": "turbo"}, @@ -286,7 +283,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.reset_mock() assert "ERROR" not in caplog.text - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_SPEED, {"entity_id": entity_id, "fan_speed": "invent"}, @@ -294,7 +291,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): ) assert "ERROR" in caplog.text - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SEND_COMMAND, {"entity_id": entity_id, "command": "raw"}, @@ -306,7 +303,7 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_got_error.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SEND_COMMAND, {"entity_id": entity_id, "command": "raw", "params": {"k1": 2}}, @@ -319,13 +316,12 @@ def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): mock_mirobo_is_got_error.reset_mock() -@asyncio.coroutine -def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): +async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): """Test vacuum supported features.""" entity_name = "test_vacuum_cleaner_2" entity_id = "{}.{}".format(DOMAIN, entity_name) - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -337,7 +333,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): } }, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert "Initializing with host 192.168.1.100 (token 12345" in caplog.text @@ -366,7 +362,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): assert state.attributes.get(ATTR_CLEANING_TOTAL_TIME) == 675 # Xiaomi vacuum specific services: - yield from hass.services.async_call( + await hass.services.async_call( XIAOMI_DOMAIN, SERVICE_START_REMOTE_CONTROL, {ATTR_ENTITY_ID: entity_id}, @@ -378,7 +374,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): mock_mirobo_is_on.reset_mock() control = {"duration": 1000, "rotation": -40, "velocity": -0.1} - yield from hass.services.async_call( + await hass.services.async_call( XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL, control, blocking=True ) mock_mirobo_is_on.manual_control.assert_has_calls( @@ -387,7 +383,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): mock_mirobo_is_on.assert_has_calls(STATUS_CALLS, any_order=True) mock_mirobo_is_on.reset_mock() - yield from hass.services.async_call( + await hass.services.async_call( XIAOMI_DOMAIN, SERVICE_STOP_REMOTE_CONTROL, {}, blocking=True ) mock_mirobo_is_on.assert_has_calls([mock.call.manual_stop()], any_order=True) @@ -395,7 +391,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): mock_mirobo_is_on.reset_mock() control_once = {"duration": 2000, "rotation": 120, "velocity": 0.1} - yield from hass.services.async_call( + await hass.services.async_call( XIAOMI_DOMAIN, SERVICE_MOVE_REMOTE_CONTROL_STEP, control_once, blocking=True ) mock_mirobo_is_on.manual_control_once.assert_has_calls( @@ -405,7 +401,7 @@ def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): mock_mirobo_is_on.reset_mock() control = {"zone": [[123, 123, 123, 123]], "repeats": 2} - yield from hass.services.async_call( + await hass.services.async_call( XIAOMI_DOMAIN, SERVICE_CLEAN_ZONE, control, blocking=True ) mock_mirobo_is_on.zoned_clean.assert_has_calls( From af153521dcff04138f8e79823c5ed686b76ca4a6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Dec 2019 23:50:07 +0100 Subject: [PATCH 2620/3953] Migrate emulated_hue tests from coroutine to async/await (#30331) --- tests/components/emulated_hue/test_hue_api.py | 158 ++++++++---------- 1 file changed, 67 insertions(+), 91 deletions(-) diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 32543602f89ced..6c1a17c0538798 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -1,5 +1,4 @@ """The tests for the emulated Hue component.""" -import asyncio from datetime import timedelta from ipaddress import ip_address import json @@ -194,15 +193,14 @@ def hue_client(loop, hass_hue, aiohttp_client): return loop.run_until_complete(aiohttp_client(web_app)) -@asyncio.coroutine -def test_discover_lights(hue_client): +async def test_discover_lights(hue_client): """Test the discovery of lights.""" - result = yield from hue_client.get("/api/username/lights") + result = await hue_client.get("/api/username/lights") assert result.status == 200 assert "application/json" in result.headers["content-type"] - result_json = yield from result.json() + result_json = await result.json() devices = set(val["uniqueid"] for val in result_json.values()) @@ -223,10 +221,9 @@ def test_discover_lights(hue_client): assert "00:57:77:a1:6a:8e:ef:b3-6c" not in devices # climate.ecobee -@asyncio.coroutine -def test_light_without_brightness_supported(hass_hue, hue_client): +async 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( + light_without_brightness_json = await perform_get_light_state( hue_client, "light.no_brightness", 200 ) @@ -234,7 +231,6 @@ def test_light_without_brightness_supported(hass_hue, hue_client): assert light_without_brightness_json["type"] == "On/off light" -@asyncio.coroutine @pytest.mark.parametrize( "state,is_reachable", [ @@ -243,26 +239,25 @@ def test_light_without_brightness_supported(hass_hue, hue_client): (const.STATE_UNKNOWN, True), ], ) -def test_reachable_for_state(hass_hue, hue_client, state, is_reachable): +async def test_reachable_for_state(hass_hue, hue_client, state, is_reachable): """Test that an entity is reported as unreachable if in unavailable state.""" entity_id = "light.ceiling_lights" hass_hue.states.async_set(entity_id, state) - state_json = yield from perform_get_light_state(hue_client, entity_id, 200) + state_json = await perform_get_light_state(hue_client, entity_id, 200) assert state_json["state"]["reachable"] == is_reachable, state_json -@asyncio.coroutine -def test_discover_full_state(hue_client): +async def test_discover_full_state(hue_client): """Test the discovery of full state.""" - result = yield from hue_client.get("/api/" + HUE_API_USERNAME) + result = await hue_client.get("/api/" + HUE_API_USERNAME) assert result.status == 200 assert "application/json" in result.headers["content-type"] - result_json = yield from result.json() + result_json = await result.json() # Make sure array has correct content assert "lights" in result_json @@ -297,11 +292,10 @@ def test_discover_full_state(hue_client): assert "127.0.0.1:8300" in config_json["ipaddress"] -@asyncio.coroutine -def test_get_light_state(hass_hue, hue_client): +async def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" # Turn office light on and set to 127 brightness, and set light color - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_ON, { @@ -312,9 +306,7 @@ def test_get_light_state(hass_hue, hue_client): blocking=True, ) - office_json = yield from perform_get_light_state( - hue_client, "light.ceiling_lights", 200 - ) + office_json = await perform_get_light_state(hue_client, "light.ceiling_lights", 200) assert office_json["state"][HUE_API_STATE_ON] is True assert office_json["state"][HUE_API_STATE_BRI] == 127 @@ -322,27 +314,25 @@ def test_get_light_state(hass_hue, hue_client): assert office_json["state"][HUE_API_STATE_SAT] == 217 # Check all lights view - result = yield from hue_client.get("/api/username/lights") + result = await hue_client.get("/api/username/lights") assert result.status == 200 assert "application/json" in result.headers["content-type"] - result_json = yield from result.json() + result_json = await result.json() assert "light.ceiling_lights" in result_json assert result_json["light.ceiling_lights"]["state"][HUE_API_STATE_BRI] == 127 # Turn office light off - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_OFF, {const.ATTR_ENTITY_ID: "light.ceiling_lights"}, blocking=True, ) - office_json = yield from perform_get_light_state( - hue_client, "light.ceiling_lights", 200 - ) + office_json = await perform_get_light_state(hue_client, "light.ceiling_lights", 200) assert office_json["state"][HUE_API_STATE_ON] is False # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 @@ -350,19 +340,18 @@ def test_get_light_state(hass_hue, hue_client): assert office_json["state"][HUE_API_STATE_SAT] == 0 # Make sure bedroom light isn't accessible - yield from perform_get_light_state(hue_client, "light.bed_light", 401) + await perform_get_light_state(hue_client, "light.bed_light", 401) # Make sure kitchen light isn't accessible - yield from perform_get_light_state(hue_client, "light.kitchen_lights", 401) + await perform_get_light_state(hue_client, "light.kitchen_lights", 401) -@asyncio.coroutine -def test_put_light_state(hass_hue, hue_client): +async def test_put_light_state(hass_hue, hue_client): """Test the setting of light states.""" - yield from perform_put_test_on_ceiling_lights(hass_hue, hue_client) + await perform_put_test_on_ceiling_lights(hass_hue, hue_client) # Turn the bedroom light on first - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_ON, {const.ATTR_ENTITY_ID: "light.ceiling_lights", light.ATTR_BRIGHTNESS: 153}, @@ -374,7 +363,7 @@ def test_put_light_state(hass_hue, hue_client): assert ceiling_lights.attributes[light.ATTR_BRIGHTNESS] == 153 # update light state through api - yield from perform_put_light_state( + await perform_put_light_state( hass_hue, hue_client, "light.ceiling_lights", @@ -385,7 +374,7 @@ def test_put_light_state(hass_hue, hue_client): ) # go through api to get the state back - ceiling_json = yield from perform_get_light_state( + ceiling_json = await perform_get_light_state( hue_client, "light.ceiling_lights", 200 ) assert ceiling_json["state"][HUE_API_STATE_BRI] == 123 @@ -393,11 +382,11 @@ def test_put_light_state(hass_hue, hue_client): assert ceiling_json["state"][HUE_API_STATE_SAT] == 127 # Go through the API to turn it off - ceiling_result = yield from perform_put_light_state( + ceiling_result = await perform_put_light_state( hass_hue, hue_client, "light.ceiling_lights", False ) - ceiling_result_json = yield from ceiling_result.json() + ceiling_result_json = await ceiling_result.json() assert ceiling_result.status == 200 assert "application/json" in ceiling_result.headers["content-type"] @@ -407,7 +396,7 @@ def test_put_light_state(hass_hue, hue_client): # Check to make sure the state changed ceiling_lights = hass_hue.states.get("light.ceiling_lights") assert ceiling_lights.state == STATE_OFF - ceiling_json = yield from perform_get_light_state( + ceiling_json = await perform_get_light_state( hue_client, "light.ceiling_lights", 200 ) # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 @@ -415,23 +404,22 @@ def test_put_light_state(hass_hue, hue_client): assert ceiling_json["state"][HUE_API_STATE_SAT] == 0 # Make sure we can't change the bedroom light state - bedroom_result = yield from perform_put_light_state( + bedroom_result = await perform_put_light_state( hass_hue, hue_client, "light.bed_light", True ) assert bedroom_result.status == 401 # Make sure we can't change the kitchen light state - kitchen_result = yield from perform_put_light_state( + kitchen_result = await perform_put_light_state( hass_hue, hue_client, "light.kitchen_light", True ) assert kitchen_result.status == 404 -@asyncio.coroutine -def test_put_light_state_script(hass_hue, hue_client): +async def test_put_light_state_script(hass_hue, hue_client): """Test the setting of script variables.""" # Turn the kitchen light off first - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( light.DOMAIN, const.SERVICE_TURN_OFF, {const.ATTR_ENTITY_ID: "light.kitchen_lights"}, @@ -442,11 +430,11 @@ def test_put_light_state_script(hass_hue, hue_client): level = 23 brightness = round(level * 255 / 100) - script_result = yield from perform_put_light_state( + script_result = await perform_put_light_state( hass_hue, hue_client, "script.set_kitchen_light", True, brightness ) - script_result_json = yield from script_result.json() + script_result_json = await script_result.json() assert script_result.status == 200 assert len(script_result_json) == 2 @@ -456,17 +444,16 @@ def test_put_light_state_script(hass_hue, hue_client): assert kitchen_light.attributes[light.ATTR_BRIGHTNESS] == level -@asyncio.coroutine -def test_put_light_state_climate_set_temperature(hass_hue, hue_client): +async def test_put_light_state_climate_set_temperature(hass_hue, hue_client): """Test setting climate temperature.""" brightness = 19 temperature = round(brightness / 255 * 100) - hvac_result = yield from perform_put_light_state( + hvac_result = await perform_put_light_state( hass_hue, hue_client, "climate.hvac", True, brightness ) - hvac_result_json = yield from hvac_result.json() + hvac_result_json = await hvac_result.json() assert hvac_result.status == 200 assert len(hvac_result_json) == 2 @@ -476,17 +463,16 @@ def test_put_light_state_climate_set_temperature(hass_hue, hue_client): assert hvac.attributes[climate.ATTR_TEMPERATURE] == temperature # Make sure we can't change the ecobee temperature since it's not exposed - ecobee_result = yield from perform_put_light_state( + ecobee_result = await perform_put_light_state( hass_hue, hue_client, "climate.ecobee", True ) assert ecobee_result.status == 401 -@asyncio.coroutine -def test_put_light_state_media_player(hass_hue, hue_client): +async def test_put_light_state_media_player(hass_hue, hue_client): """Test turning on media player and setting volume.""" # Turn the music player off first - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( media_player.DOMAIN, const.SERVICE_TURN_OFF, {const.ATTR_ENTITY_ID: "media_player.walkman"}, @@ -497,11 +483,11 @@ def test_put_light_state_media_player(hass_hue, hue_client): level = 0.25 brightness = round(level * 255) - mp_result = yield from perform_put_light_state( + mp_result = await perform_put_light_state( hass_hue, hue_client, "media_player.walkman", True, brightness ) - mp_result_json = yield from mp_result.json() + mp_result_json = await mp_result.json() assert mp_result.status == 200 assert len(mp_result_json) == 2 @@ -610,11 +596,10 @@ async def test_set_position_cover(hass_hue, hue_client): assert cover_test_2.attributes.get("current_position") == level -@asyncio.coroutine -def test_put_light_state_fan(hass_hue, hue_client): +async def test_put_light_state_fan(hass_hue, hue_client): """Test turning on fan and setting speed.""" # Turn the fan off first - yield from hass_hue.services.async_call( + await hass_hue.services.async_call( fan.DOMAIN, const.SERVICE_TURN_OFF, {const.ATTR_ENTITY_ID: "fan.living_room_fan"}, @@ -625,11 +610,11 @@ def test_put_light_state_fan(hass_hue, hue_client): level = 43 brightness = round(level * 255 / 100) - fan_result = yield from perform_put_light_state( + fan_result = await perform_put_light_state( hass_hue, hue_client, "fan.living_room_fan", True, brightness ) - fan_result_json = yield from fan_result.json() + fan_result_json = await fan_result.json() assert fan_result.status == 200 assert len(fan_result_json) == 2 @@ -640,17 +625,16 @@ def test_put_light_state_fan(hass_hue, hue_client): # pylint: disable=invalid-name -@asyncio.coroutine -def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): +async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): """Test the form with urlencoded content.""" # Needed for Alexa - yield from perform_put_test_on_ceiling_lights( + await perform_put_test_on_ceiling_lights( hass_hue, hue_client, "application/x-www-form-urlencoded" ) # Make sure we fail gracefully when we can't parse the data data = {"key1": "value1", "key2": "value2"} - result = yield from hue_client.put( + result = await hue_client.put( "/api/username/lights/light.ceiling_lights/state", headers={"content-type": "application/x-www-form-urlencoded"}, data=data, @@ -659,41 +643,36 @@ def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): assert result.status == 400 -@asyncio.coroutine -def test_entity_not_found(hue_client): +async def test_entity_not_found(hue_client): """Test for entity which are not found.""" - result = yield from hue_client.get("/api/username/lights/not.existant_entity") + result = await hue_client.get("/api/username/lights/not.existant_entity") assert result.status == 404 - result = yield from hue_client.put("/api/username/lights/not.existant_entity/state") + result = await hue_client.put("/api/username/lights/not.existant_entity/state") assert result.status == 404 -@asyncio.coroutine -def test_allowed_methods(hue_client): +async def test_allowed_methods(hue_client): """Test the allowed methods.""" - result = yield from hue_client.get( - "/api/username/lights/light.ceiling_lights/state" - ) + result = await hue_client.get("/api/username/lights/light.ceiling_lights/state") assert result.status == 405 - result = yield from hue_client.put("/api/username/lights/light.ceiling_lights") + result = await hue_client.put("/api/username/lights/light.ceiling_lights") assert result.status == 405 - result = yield from hue_client.put("/api/username/lights") + result = await hue_client.put("/api/username/lights") assert result.status == 405 -@asyncio.coroutine -def test_proper_put_state_request(hue_client): +async def test_proper_put_state_request(hue_client): """Test the request to set the state.""" # Test proper on value parsing - result = yield from hue_client.put( + result = await hue_client.put( "/api/username/lights/{}/state".format("light.ceiling_lights"), data=json.dumps({HUE_API_STATE_ON: 1234}), ) @@ -701,7 +680,7 @@ def test_proper_put_state_request(hue_client): assert result.status == 400 # Test proper brightness value parsing - result = yield from hue_client.put( + result = await hue_client.put( "/api/username/lights/{}/state".format("light.ceiling_lights"), data=json.dumps({HUE_API_STATE_ON: True, HUE_API_STATE_BRI: "Hello world!"}), ) @@ -709,15 +688,14 @@ def test_proper_put_state_request(hue_client): assert result.status == 400 -@asyncio.coroutine -def test_get_empty_groups_state(hue_client): +async def test_get_empty_groups_state(hue_client): """Test the request to get groups endpoint.""" # Test proper on value parsing - result = yield from hue_client.get("/api/username/groups") + result = await hue_client.get("/api/username/groups") assert result.status == 200 - result_json = yield from result.json() + result_json = await result.json() assert result_json == {} @@ -756,23 +734,21 @@ async def perform_put_test_on_ceiling_lights( assert ceiling_lights.attributes[light.ATTR_BRIGHTNESS] == 56 -@asyncio.coroutine -def perform_get_light_state(client, entity_id, expected_status): +async def perform_get_light_state(client, entity_id, expected_status): """Test the getting of a light state.""" - result = yield from client.get("/api/username/lights/{}".format(entity_id)) + result = await client.get("/api/username/lights/{}".format(entity_id)) assert result.status == expected_status if expected_status == 200: assert "application/json" in result.headers["content-type"] - return (yield from result.json()) + return await result.json() return None -@asyncio.coroutine -def perform_put_light_state( +async def perform_put_light_state( hass_hue, client, entity_id, @@ -794,14 +770,14 @@ def perform_put_light_state( if saturation is not None: data[HUE_API_STATE_SAT] = saturation - result = yield from client.put( + result = await client.put( "/api/username/lights/{}/state".format(entity_id), headers=req_headers, data=json.dumps(data).encode(), ) # Wait until state change is complete before continuing - yield from hass_hue.async_block_till_done() + await hass_hue.async_block_till_done() return result From fc23b4f83f977f798a991662dee85f04d027d6f5 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Tue, 31 Dec 2019 18:26:35 -0500 Subject: [PATCH 2621/3953] Migrate webostv to new library and make integration async with callback state updates (#29296) * migrate webostv to new aiopylgtv version of the library and add support for generic commands, input/button commands, and callback state updates * update requirements * cleanup and bump aiopylgtv version * update webostv unit tests * make webostv unit tests work with python 3.7 * cleanup for code checks * cleanup and code review * make all client request functions coroutines * make host required for webostv configuration * remove generic command and button functionality plus related cleanup * fix previous track function * update unit tests * fix imports for unit tests * update unit test * further unit test updates * remove unnecessary setup call in unit tests * restore previous behaviour with client key config file in hass configuration directory --- homeassistant/components/webostv/__init__.py | 151 +++++++ .../components/webostv/manifest.json | 3 +- .../components/webostv/media_player.py | 382 ++++++++---------- homeassistant/components/webostv/notify.py | 63 ++- requirements_all.txt | 9 +- requirements_test_all.txt | 9 +- tests/components/webostv/test_media_player.py | 103 +++-- 7 files changed, 420 insertions(+), 300 deletions(-) diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index f0b3c2c5f7e054..b34dba3ad94a0a 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -1 +1,152 @@ """Support for WebOS TV.""" +import asyncio +import logging + +from aiopylgtv import PyLGTVCmdException, PyLGTVPairException, WebOsClient +import voluptuous as vol +from websockets.exceptions import ConnectionClosed + +from homeassistant.const import ( + CONF_CUSTOMIZE, + CONF_HOST, + CONF_ICON, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP, +) +import homeassistant.helpers.config_validation as cv + +DOMAIN = "webostv" + +CONF_SOURCES = "sources" +CONF_ON_ACTION = "turn_on_action" +CONF_STANDBY_CONNECTION = "standby_connection" +DEFAULT_NAME = "LG webOS Smart TV" +WEBOSTV_CONFIG_FILE = "webostv.conf" + +CUSTOMIZE_SCHEMA = vol.Schema( + {vol.Optional(CONF_SOURCES, default=[]): vol.All(cv.ensure_list, [cv.string])} +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional( + CONF_STANDBY_CONNECTION, default=False + ): cv.boolean, + vol.Optional(CONF_ICON): cv.string, + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the LG WebOS TV platform.""" + hass.data[DOMAIN] = {} + + tasks = [async_setup_tv(hass, config, conf) for conf in config[DOMAIN]] + if tasks: + await asyncio.gather(*tasks) + + return True + + +async def async_setup_tv(hass, config, conf): + """Set up a LG WebOS TV based on host parameter.""" + + host = conf[CONF_HOST] + config_file = hass.config.path(WEBOSTV_CONFIG_FILE) + standby_connection = conf[CONF_STANDBY_CONNECTION] + + client = WebOsClient(host, config_file, standby_connection=standby_connection) + hass.data[DOMAIN][host] = {"client": client} + + if client.is_registered(): + await async_setup_tv_finalize(hass, config, conf, client) + else: + _LOGGER.warning("LG webOS TV %s needs to be paired", host) + await async_request_configuration(hass, config, conf, client) + + +async def async_connect(client): + """Attempt a connection, but fail gracefully if tv is off for example.""" + try: + await client.connect() + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVPairException, + PyLGTVCmdException, + ): + pass + + +async def async_setup_tv_finalize(hass, config, conf, client): + """Make initial connection attempt and call platform setup.""" + + async def async_on_stop(event): + """Unregister callbacks and disconnect.""" + client.clear_state_update_callbacks() + await client.disconnect() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_on_stop) + + await async_connect(client) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("media_player", DOMAIN, conf, config) + ) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("notify", DOMAIN, conf, config) + ) + + +async def async_request_configuration(hass, config, conf, client): + """Request configuration steps from the user.""" + host = conf.get(CONF_HOST) + name = conf.get(CONF_NAME) + configurator = hass.components.configurator + + async def lgtv_configuration_callback(data): + """Handle actions when configuration callback is called.""" + try: + await client.connect() + except PyLGTVPairException: + _LOGGER.warning("Connected to LG webOS TV %s but not paired", host) + return + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVCmdException, + ): + _LOGGER.error("Unable to connect to host %s", host) + return + + await async_setup_tv_finalize(hass, config, conf, client) + configurator.async_request_done(request_id) + + request_id = configurator.async_request_config( + name, + lgtv_configuration_callback, + description="Click start and accept the pairing request on your TV.", + description_image="/static/images/config_webos.png", + submit_caption="Start pairing request", + ) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index dcf908cd6037d4..016f14f0f941d7 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -3,8 +3,7 @@ "name": "Webostv", "documentation": "https://www.home-assistant.io/integrations/webostv", "requirements": [ - "pylgtv==0.1.9", - "websockets==6.0" + "aiopylgtv==0.2.4" ], "dependencies": ["configurator"], "codeowners": [] diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 3bf0011907d079..fd47cf0a1144fa 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -1,16 +1,14 @@ """Support for interface with an LG webOS Smart TV.""" import asyncio from datetime import timedelta +from functools import wraps import logging -from typing import Dict -from urllib.parse import urlparse -from pylgtv import PyLGTVPairException, WebOsClient -import voluptuous as vol +from aiopylgtv import PyLGTVCmdException, PyLGTVPairException from websockets.exceptions import ConnectionClosed from homeassistant import util -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -27,27 +25,21 @@ ) from homeassistant.const import ( CONF_CUSTOMIZE, - CONF_FILENAME, CONF_HOST, CONF_NAME, - CONF_TIMEOUT, STATE_OFF, STATE_PAUSED, STATE_PLAYING, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -_CONFIGURING: Dict[str, str] = {} +from . import CONF_ON_ACTION, CONF_SOURCES, DOMAIN + _LOGGER = logging.getLogger(__name__) -CONF_SOURCES = "sources" -CONF_ON_ACTION = "turn_on_action" -DEFAULT_NAME = "LG webOS Smart TV" LIVETV_APP_ID = "com.webos.app.livetv" -WEBOSTV_CONFIG_FILE = "webostv.conf" SUPPORT_WEBOSTV = ( SUPPORT_TURN_OFF @@ -65,131 +57,65 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) -CUSTOMIZE_SCHEMA = vol.Schema( - {vol.Optional(CONF_SOURCES): vol.All(cv.ensure_list, [cv.string])} -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, - vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_TIMEOUT, default=8): cv.positive_int, - } -) - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the LG WebOS TV platform.""" - if discovery_info is not None: - host = urlparse(discovery_info[1]).hostname - else: - host = config.get(CONF_HOST) - - if host is None: - _LOGGER.error("No TV found in configuration file or with discovery") - return False - # Only act if we are not already configuring this host - if host in _CONFIGURING: + if discovery_info is None: return - name = config.get(CONF_NAME) - customize = config.get(CONF_CUSTOMIZE) - timeout = config.get(CONF_TIMEOUT) - turn_on_action = config.get(CONF_ON_ACTION) + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + customize = discovery_info[CONF_CUSTOMIZE] + turn_on_action = discovery_info.get(CONF_ON_ACTION) - config = hass.config.path(config.get(CONF_FILENAME)) + client = hass.data[DOMAIN][host]["client"] + on_script = Script(hass, turn_on_action) if turn_on_action else None - setup_tv(host, name, customize, config, timeout, hass, add_entities, turn_on_action) + entity = LgWebOSMediaPlayerEntity(client, name, customize, on_script) + async_add_entities([entity], update_before_add=False) -def setup_tv( - host, name, customize, config, timeout, hass, add_entities, turn_on_action -): - """Set up a LG WebOS TV based on host parameter.""" - client = WebOsClient(host, config, timeout) +def cmd(func): + """Catch command exceptions.""" - if not client.is_registered(): - if host in _CONFIGURING: - # Try to pair. - try: - client.register() - except PyLGTVPairException: - _LOGGER.warning("Connected to LG webOS TV %s but not paired", host) - return - except (OSError, ConnectionClosed, asyncio.TimeoutError): - _LOGGER.error("Unable to connect to host %s", host) - return - else: - # Not registered, request configuration. - _LOGGER.warning("LG webOS TV %s needs to be paired", host) - request_configuration( - host, - name, - customize, - config, - timeout, - hass, - add_entities, - turn_on_action, + @wraps(func) + async def wrapper(obj, *args, **kwargs): + """Wrap all command methods.""" + try: + await func(obj, *args, **kwargs) + except ( + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVCmdException, + ) as exc: + # If TV is off, we expect calls to fail. + if obj.state == STATE_OFF: + level = logging.INFO + else: + level = logging.ERROR + _LOGGER.log( + level, + "Error calling %s on entity %s: %r", + func.__name__, + obj.entity_id, + exc, ) - return - - # If we came here and configuring this host, mark as done. - if client.is_registered() and host in _CONFIGURING: - request_id = _CONFIGURING.pop(host) - configurator = hass.components.configurator - configurator.request_done(request_id) - add_entities( - [LgWebOSDevice(host, name, customize, config, timeout, hass, turn_on_action)], - True, - ) + return wrapper -def request_configuration( - host, name, customize, config, timeout, hass, add_entities, turn_on_action -): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - - # We got an error if this method is called while we are configuring - if host in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING[host], "Failed to pair, please try again." - ) - return - - def lgtv_configuration_callback(data): - """Handle actions when configuration callback is called.""" - setup_tv( - host, name, customize, config, timeout, hass, add_entities, turn_on_action - ) - - _CONFIGURING[host] = configurator.request_config( - name, - lgtv_configuration_callback, - description="Click start and accept the pairing request on your TV.", - description_image="/static/images/config_webos.png", - submit_caption="Start pairing request", - ) - - -class LgWebOSDevice(MediaPlayerDevice): +class LgWebOSMediaPlayerEntity(MediaPlayerDevice): """Representation of a LG WebOS TV.""" - def __init__(self, host, name, customize, config, timeout, hass, on_action): + def __init__(self, client, name, customize, on_script=None): """Initialize the webos device.""" - - self._client = WebOsClient(host, config, timeout) - self._on_script = Script(hass, on_action) if on_action else None + self._client = client + self._name = name self._customize = customize + self._on_script = on_script - self._name = name # Assume that the TV is not muted self._muted = False # Assume that the TV is in Play mode @@ -200,64 +126,86 @@ def __init__(self, host, name, customize, config, timeout, hass, on_action): self._state = None self._source_list = {} self._app_list = {} + self._input_list = {} self._channel = None self._last_icon = None - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - def update(self): - """Retrieve the latest data.""" + async def async_added_to_hass(self): + """Connect and subscribe to state updates.""" + await self._client.register_state_update_callback( + self.async_handle_state_update + ) - try: - current_input = self._client.get_input() - if current_input is not None: - self._current_source_id = current_input - if self._state in (None, STATE_OFF): - self._state = STATE_PLAYING - else: - self._state = STATE_OFF - self._current_source = None - self._current_source_id = None - self._channel = None - - if self._state is not STATE_OFF: - self._muted = self._client.get_muted() - self._volume = self._client.get_volume() - self._channel = self._client.get_current_channel() - - self._source_list = {} - self._app_list = {} - conf_sources = self._customize.get(CONF_SOURCES, []) - - for app in self._client.get_apps(): - self._app_list[app["id"]] = app - if app["id"] == self._current_source_id: - self._current_source = app["title"] - self._source_list[app["title"]] = app - elif ( - not conf_sources - or app["id"] in conf_sources - or any(word in app["title"] for word in conf_sources) - or any(word in app["id"] for word in conf_sources) - ): - self._source_list[app["title"]] = app - - for source in self._client.get_inputs(): - if source["id"] == self._current_source_id: - self._current_source = source["label"] - self._source_list[source["label"]] = source - elif ( - not conf_sources - or source["label"] in conf_sources - or any( - source["label"].find(word) != -1 for word in conf_sources - ) - ): - self._source_list[source["label"]] = source - except (OSError, ConnectionClosed, TypeError, asyncio.TimeoutError): + # force state update if needed + if self._state is None: + await self.async_handle_state_update() + + async def async_will_remove_from_hass(self): + """Call disconnect on removal.""" + self._client.unregister_state_update_callback(self.async_handle_state_update) + + async def async_handle_state_update(self): + """Update state from WebOsClient.""" + self._current_source_id = self._client.current_appId + self._muted = self._client.muted + self._volume = self._client.volume + self._channel = self._client.current_channel + self._app_list = self._client.apps + self._input_list = self._client.inputs + + if self._current_source_id == "": self._state = STATE_OFF - self._current_source = None - self._current_source_id = None - self._channel = None + else: + self._state = STATE_PLAYING + + self.update_sources() + + self.async_schedule_update_ha_state(False) + + def update_sources(self): + """Update list of sources from current source, apps, inputs and configured list.""" + self._source_list = {} + conf_sources = self._customize[CONF_SOURCES] + + for app in self._app_list.values(): + if app["id"] == self._current_source_id: + self._current_source = app["title"] + self._source_list[app["title"]] = app + elif ( + not conf_sources + or app["id"] in conf_sources + or any(word in app["title"] for word in conf_sources) + or any(word in app["id"] for word in conf_sources) + ): + self._source_list[app["title"]] = app + + for source in self._input_list.values(): + if source["appId"] == self._current_source_id: + self._current_source = source["label"] + self._source_list[source["label"]] = source + elif ( + not conf_sources + or source["label"] in conf_sources + or any(source["label"].find(word) != -1 for word in conf_sources) + ): + self._source_list[source["label"]] = source + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + async def async_update(self): + """Connect.""" + if not self._client.is_connected(): + try: + await self._client.connect() + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVPairException, + PyLGTVCmdException, + ): + pass @property def name(self): @@ -326,46 +274,54 @@ def supported_features(self): return SUPPORT_WEBOSTV | SUPPORT_TURN_ON return SUPPORT_WEBOSTV - def turn_off(self): + @cmd + async def async_turn_off(self): """Turn off media player.""" + await self._client.power_off() - self._state = STATE_OFF - try: - self._client.power_off() - except (OSError, ConnectionClosed, TypeError, asyncio.TimeoutError): - pass - - def turn_on(self): + async def async_turn_on(self): """Turn on the media player.""" + connected = self._client.is_connected() if self._on_script: - self._on_script.run() + await self._on_script.async_run() + + # if connection was already active + # ensure is still alive + if connected: + await self._client.get_current_app() - def volume_up(self): + @cmd + async def async_volume_up(self): """Volume up the media player.""" - self._client.volume_up() + await self._client.volume_up() - def volume_down(self): + @cmd + async def async_volume_down(self): """Volume down media player.""" - self._client.volume_down() + await self._client.volume_down() - def set_volume_level(self, volume): + @cmd + async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" tv_volume = volume * 100 - self._client.set_volume(tv_volume) + await self._client.set_volume(tv_volume) - def mute_volume(self, mute): + @cmd + async def async_mute_volume(self, mute): """Send mute command.""" self._muted = mute - self._client.set_mute(mute) + await self._client.set_mute(mute) - def media_play_pause(self): + @cmd + async def async_media_play_pause(self): """Simulate play pause media player.""" if self._playing: - self.media_pause() + await self.media_pause() else: - self.media_play() + await self.media_play() - def select_source(self, source): + @cmd + async def async_select_source(self, source): """Select input source.""" source_dict = self._source_list.get(source) if source_dict is None: @@ -374,12 +330,13 @@ def select_source(self, source): self._current_source_id = source_dict["id"] if source_dict.get("title"): self._current_source = source_dict["title"] - self._client.launch_app(source_dict["id"]) + await self._client.launch_app(source_dict["id"]) elif source_dict.get("label"): self._current_source = source_dict["label"] - self._client.set_input(source_dict["id"]) + await self._client.set_input(source_dict["id"]) - def play_media(self, media_type, media_id, **kwargs): + @cmd + async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) @@ -405,40 +362,47 @@ def play_media(self, media_type, media_id, **kwargs): "Switching to channel <%s> with perfect match", perfect_match_channel_id, ) - self._client.set_channel(perfect_match_channel_id) + await self._client.set_channel(perfect_match_channel_id) elif partial_match_channel_id is not None: _LOGGER.info( "Switching to channel <%s> with partial match", partial_match_channel_id, ) - self._client.set_channel(partial_match_channel_id) + await self._client.set_channel(partial_match_channel_id) - return - - def media_play(self): + @cmd + async def async_media_play(self): """Send play command.""" self._playing = True self._state = STATE_PLAYING - self._client.play() + await self._client.play() - def media_pause(self): + @cmd + async def async_media_pause(self): """Send media pause command to media player.""" self._playing = False self._state = STATE_PAUSED - self._client.pause() + await self._client.pause() + + @cmd + async def async_media_stop(self): + """Send stop command to media player.""" + await self._client.stop() - def media_next_track(self): + @cmd + async def async_media_next_track(self): """Send next track command.""" current_input = self._client.get_input() if current_input == LIVETV_APP_ID: - self._client.channel_up() + await self._client.channel_up() else: - self._client.fast_forward() + await self._client.fast_forward() - def media_previous_track(self): + @cmd + async def async_media_previous_track(self): """Send the previous track command.""" current_input = self._client.get_input() if current_input == LIVETV_APP_ID: - self._client.channel_down() + await self._client.channel_down() else: - self._client.rewind() + await self._client.rewind() diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index f62c41e9a95647..e75fafbfe23469 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -1,47 +1,29 @@ """Support for LG WebOS TV notification service.""" +import asyncio import logging -from pylgtv import PyLGTVPairException, WebOsClient -import voluptuous as vol +from aiopylgtv import PyLGTVCmdException, PyLGTVPairException +from websockets.exceptions import ConnectionClosed -from homeassistant.components.notify import ( - ATTR_DATA, - PLATFORM_SCHEMA, - BaseNotificationService, -) -from homeassistant.const import CONF_FILENAME, CONF_HOST, CONF_ICON -import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ATTR_DATA, BaseNotificationService +from homeassistant.const import CONF_HOST, CONF_ICON -_LOGGER = logging.getLogger(__name__) - -WEBOSTV_CONFIG_FILE = "webostv.conf" +from . import DOMAIN -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, - vol.Optional(CONF_ICON): cv.string, - } -) +_LOGGER = logging.getLogger(__name__) -def get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Return the notify service.""" - path = hass.config.path(config.get(CONF_FILENAME)) - client = WebOsClient(config.get(CONF_HOST), key_file_path=path, timeout_connect=8) + host = discovery_info.get(CONF_HOST) + icon_path = discovery_info.get(CONF_ICON) - if not client.is_registered(): - try: - client.register() - except PyLGTVPairException: - _LOGGER.error("Pairing with TV failed") - return None - except OSError: - _LOGGER.error("TV unreachable") - return None + client = hass.data[DOMAIN][host]["client"] + + svc = LgWebOSNotificationService(client, icon_path) - return LgWebOSNotificationService(client, config.get(CONF_ICON)) + return svc class LgWebOSNotificationService(BaseNotificationService): @@ -52,18 +34,27 @@ def __init__(self, client, icon_path): self._client = client self._icon_path = icon_path - def send_message(self, message="", **kwargs): + async def async_send_message(self, message="", **kwargs): """Send a message to the tv.""" - try: + if not self._client.is_connected(): + await self._client.connect() + data = kwargs.get(ATTR_DATA) icon_path = ( data.get(CONF_ICON, self._icon_path) if data else self._icon_path ) - self._client.send_message(message, icon_path=icon_path) + await self._client.send_message(message, icon_path=icon_path) except PyLGTVPairException: _LOGGER.error("Pairing with TV failed") except FileNotFoundError: _LOGGER.error("Icon %s not found", icon_path) - except OSError: + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVCmdException, + ): _LOGGER.error("TV unreachable") diff --git a/requirements_all.txt b/requirements_all.txt index 3d60bc22d11799..4d697933f66363 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,6 +186,9 @@ aionotion==1.1.0 # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 +# homeassistant.components.webostv +aiopylgtv==0.2.4 + # homeassistant.components.switcher_kis aioswitcher==2019.4.26 @@ -1325,9 +1328,6 @@ pylaunches==0.2.0 # homeassistant.components.lg_netcast pylgnetcast-homeassistant==0.2.0.dev0 -# homeassistant.components.webostv -pylgtv==0.1.9 - # homeassistant.components.linky pylinky==0.4.0 @@ -2035,9 +2035,6 @@ webexteamssdk==1.1.1 # homeassistant.components.gpmdp websocket-client==0.54.0 -# homeassistant.components.webostv -websockets==6.0 - # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3ba482a977199..83e2299c4acf7f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -68,6 +68,9 @@ aiohue==1.10.1 # homeassistant.components.notion aionotion==1.1.0 +# homeassistant.components.webostv +aiopylgtv==0.2.4 + # homeassistant.components.switcher_kis aioswitcher==2019.4.26 @@ -442,9 +445,6 @@ pyiqvia==0.2.1 # homeassistant.components.kira pykira==0.1.1 -# homeassistant.components.webostv -pylgtv==0.1.9 - # homeassistant.components.linky pylinky==0.4.0 @@ -636,9 +636,6 @@ wakeonlan==1.1.6 # homeassistant.components.folder_watcher watchdog==0.8.3 -# homeassistant.components.webostv -websockets==6.0 - # homeassistant.components.withings withings-api==2.1.3 diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index e5729a2d8d00a3..023e0e2dc070ee 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -1,56 +1,77 @@ """The tests for the LG webOS media player platform.""" -import unittest -from unittest import mock +import sys -from homeassistant.components.webostv import media_player as webostv +import pytest +from homeassistant.components import media_player +from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_VOLUME_MUTED, + SERVICE_SELECT_SOURCE, +) +from homeassistant.components.webostv import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + SERVICE_VOLUME_MUTE, +) +from homeassistant.setup import async_setup_component -class FakeLgWebOSDevice(webostv.LgWebOSDevice): - """A fake device without the client setup required for the real one.""" +if sys.version_info >= (3, 8, 0): + from unittest.mock import patch +else: + from asynctest import patch - def __init__(self, *args, **kwargs): - """Initialise parameters needed for tests with fake values.""" - self._source_list = {} - self._client = mock.MagicMock() - self._name = "fake_device" - self._current_source = None +NAME = "fake" +ENTITY_ID = f"{media_player.DOMAIN}.{NAME}" -class TestLgWebOSDevice(unittest.TestCase): - """Test the LgWebOSDevice class.""" - def setUp(self): - """Configure a fake device for each test.""" - self.device = FakeLgWebOSDevice() +@pytest.fixture(name="client") +def client_fixture(): + """Patch of client library for tests.""" + with patch( + "homeassistant.components.webostv.WebOsClient", autospec=True + ) as mock_client_class: + yield mock_client_class.return_value - def test_select_source_with_empty_source_list(self): - """Ensure we don't call client methods when we don't have sources.""" - self.device.select_source("nonexistent") - assert 0 == self.device._client.launch_app.call_count - assert 0 == self.device._client.set_input.call_count - def test_select_source_with_titled_entry(self): - """Test that a titled source is treated as an app.""" - self.device._source_list = { - "existent": {"id": "existent_id", "title": "existent_title"} - } +async def setup_webostv(hass): + """Initialize webostv and media_player for tests.""" + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake", CONF_NAME: NAME}}, + ) + await hass.async_block_till_done() - self.device.select_source("existent") - assert "existent_title" == self.device._current_source - assert [mock.call("existent_id")] == ( - self.device._client.launch_app.call_args_list - ) +async def test_mute(hass, client): + """Test simple service call.""" - def test_select_source_with_labelled_entry(self): - """Test that a labelled source is treated as an input source.""" - self.device._source_list = { - "existent": {"id": "existent_id", "label": "existent_label"} - } + await setup_webostv(hass) - self.device.select_source("existent") + data = { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MEDIA_VOLUME_MUTED: True, + } + await hass.services.async_call(media_player.DOMAIN, SERVICE_VOLUME_MUTE, data) + await hass.async_block_till_done() - assert "existent_label" == self.device._current_source - assert [mock.call("existent_id")] == ( - self.device._client.set_input.call_args_list - ) + client.set_mute.assert_called_once() + + +async def test_select_source_with_empty_source_list(hass, client): + """Ensure we don't call client methods when we don't have sources.""" + + await setup_webostv(hass) + + data = { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_INPUT_SOURCE: "nonexistent", + } + await hass.services.async_call(media_player.DOMAIN, SERVICE_SELECT_SOURCE, data) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_ID, "playing") + client.launch_app.assert_not_called() + client.set_input.assert_not_called() From 52ed9608e238fe7d8c72b7a1b4cd59cff2e31a5e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Jan 2020 01:22:44 +0100 Subject: [PATCH 2622/3953] Migrate input_* tests from coroutine to async/await (#30336) --- tests/components/input_boolean/test_init.py | 11 ++++------- tests/components/input_datetime/test_init.py | 18 +++++++----------- tests/components/input_number/test_init.py | 18 ++++++------------ tests/components/input_select/test_init.py | 15 ++++----------- tests/components/input_text/test_init.py | 13 ++++--------- 5 files changed, 25 insertions(+), 50 deletions(-) diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index ed5e927c2ca45d..2d504114c78931 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -1,6 +1,5 @@ """The tests for the input_boolean component.""" # pylint: disable=protected-access -import asyncio import logging from unittest.mock import patch @@ -96,8 +95,7 @@ async def test_config_options(hass): assert "mdi:work" == state_2.attributes.get(ATTR_ICON) -@asyncio.coroutine -def test_restore_state(hass): +async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -111,7 +109,7 @@ def test_restore_state(hass): hass.state = CoreState.starting mock_component(hass, "recorder") - yield from async_setup_component(hass, DOMAIN, {DOMAIN: {"b1": None, "b2": None}}) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"b1": None, "b2": None}}) state = hass.states.get("input_boolean.b1") assert state @@ -122,8 +120,7 @@ def test_restore_state(hass): assert state.state == "off" -@asyncio.coroutine -def test_initial_state_overrules_restore_state(hass): +async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, (State("input_boolean.b1", "on"), State("input_boolean.b2", "off")) @@ -131,7 +128,7 @@ def test_initial_state_overrules_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, {DOMAIN: {"b1": {CONF_INITIAL: False}, "b2": {CONF_INITIAL: True}}}, diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 427433e22d2ffa..6908c4fc5f1259 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -1,6 +1,5 @@ """Tests for the Input slider component.""" # pylint: disable=protected-access -import asyncio import datetime from unittest.mock import patch @@ -192,10 +191,9 @@ async def test_set_invalid_2(hass): assert state.state == initial -@asyncio.coroutine -def test_set_datetime_date(hass): +async def test_set_datetime_date(hass): """Test set_datetime method with only date.""" - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, {DOMAIN: {"test_date": {"has_time": False, "has_date": True}}} ) @@ -204,7 +202,7 @@ def test_set_datetime_date(hass): dt_obj = datetime.datetime(2017, 9, 7, 19, 46) date_portion = dt_obj.date() - yield from async_set_date_and_time(hass, entity_id, dt_obj) + await async_set_date_and_time(hass, entity_id, dt_obj) state = hass.states.get(entity_id) assert state.state == str(date_portion) @@ -215,8 +213,7 @@ def test_set_datetime_date(hass): assert state.attributes["timestamp"] == date_dt_obj.timestamp() -@asyncio.coroutine -def test_restore_state(hass): +async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -232,7 +229,7 @@ def test_restore_state(hass): initial = datetime.datetime(2017, 1, 1, 23, 42) - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -263,10 +260,9 @@ def test_restore_state(hass): assert state_bogus.state == str(initial) -@asyncio.coroutine -def test_default_value(hass): +async def test_default_value(hass): """Test default value if none has been set via inital or restore state.""" - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index a3b46212daf556..6d032b639cfe52 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -1,6 +1,5 @@ """The tests for the Input number component.""" # pylint: disable=protected-access -import asyncio from unittest.mock import patch import pytest @@ -171,8 +170,7 @@ async def test_mode(hass): assert "slider" == state.attributes["mode"] -@asyncio.coroutine -def test_restore_state(hass): +async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, (State("input_number.b1", "70"), State("input_number.b2", "200")) @@ -180,7 +178,7 @@ def test_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, {DOMAIN: {"b1": {"min": 0, "max": 100}, "b2": {"min": 10, "max": 100}}}, @@ -195,8 +193,7 @@ def test_restore_state(hass): assert float(state.state) == 10 -@asyncio.coroutine -def test_initial_state_overrules_restore_state(hass): +async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, (State("input_number.b1", "70"), State("input_number.b2", "200")) @@ -204,7 +201,7 @@ def test_initial_state_overrules_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -224,14 +221,11 @@ def test_initial_state_overrules_restore_state(hass): assert float(state.state) == 60 -@asyncio.coroutine -def test_no_initial_state_and_no_restore_state(hass): +async def test_no_initial_state_and_no_restore_state(hass): """Ensure that entity is create without initial and restore feature.""" hass.state = CoreState.starting - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"b1": {"min": 0, "max": 100}}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"b1": {"min": 0, "max": 100}}}) state = hass.states.get("input_number.b1") assert state diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 51f0b24bc8a0f4..8fda80cd3d2958 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -1,6 +1,5 @@ """The tests for the Input select component.""" # pylint: disable=protected-access -import asyncio from unittest.mock import patch import pytest @@ -249,8 +248,7 @@ async def test_set_options_service(hass): assert "test2" == state.state -@asyncio.coroutine -def test_restore_state(hass): +async def test_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -262,9 +260,7 @@ def test_restore_state(hass): options = {"options": ["first option", "middle option", "last option"]} - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"s1": options, "s2": options}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"s1": options, "s2": options}}) state = hass.states.get("input_select.s1") assert state @@ -275,8 +271,7 @@ def test_restore_state(hass): assert state.state == "first option" -@asyncio.coroutine -def test_initial_state_overrules_restore_state(hass): +async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -291,9 +286,7 @@ def test_initial_state_overrules_restore_state(hass): "initial": "middle option", } - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"s1": options, "s2": options}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"s1": options, "s2": options}}) state = hass.states.get("input_select.s1") assert state diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index d37fe01cd29e52..8835128d672f73 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -1,6 +1,5 @@ """The tests for the Input text component.""" # pylint: disable=protected-access -import asyncio from unittest.mock import patch import pytest @@ -122,8 +121,7 @@ async def test_restore_state(hass): assert str(state.state) == "unknown" -@asyncio.coroutine -def test_initial_state_overrules_restore_state(hass): +async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, @@ -132,7 +130,7 @@ def test_initial_state_overrules_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -152,14 +150,11 @@ def test_initial_state_overrules_restore_state(hass): assert str(state.state) == "test" -@asyncio.coroutine -def test_no_initial_state_and_no_restore_state(hass): +async def test_no_initial_state_and_no_restore_state(hass): """Ensure that entity is create without initial and restore feature.""" hass.state = CoreState.starting - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"b1": {"min": 0, "max": 100}}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"b1": {"min": 0, "max": 100}}}) state = hass.states.get("input_text.b1") assert state From bb2d8e3f7d90c13e148bac0d88dd3511bf6f4202 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 1 Jan 2020 00:32:22 +0000 Subject: [PATCH 2623/3953] [ci skip] Translation update --- .../components/gios/.translations/en.json | 20 +++++++++++++ .../components/gios/.translations/ru.json | 20 +++++++++++++ .../huawei_lte/.translations/pl.json | 1 + .../huawei_lte/.translations/ru.json | 2 +- .../components/local_ip/.translations/en.json | 16 ++++++++++ .../components/local_ip/.translations/ru.json | 16 ++++++++++ .../components/tesla/.translations/pl.json | 30 +++++++++++++++++++ 7 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/gios/.translations/en.json create mode 100644 homeassistant/components/gios/.translations/ru.json create mode 100644 homeassistant/components/local_ip/.translations/en.json create mode 100644 homeassistant/components/local_ip/.translations/ru.json create mode 100644 homeassistant/components/tesla/.translations/pl.json diff --git a/homeassistant/components/gios/.translations/en.json b/homeassistant/components/gios/.translations/en.json new file mode 100644 index 00000000000000..2ff0d8c60f3231 --- /dev/null +++ b/homeassistant/components/gios/.translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Cannot connect to the GIO\u015a server.", + "invalid_sensors_data": "Invalid sensors' data for this measuring station.", + "wrong_station_id": "ID of the measuring station is not correct." + }, + "step": { + "user": { + "data": { + "name": "Name of the integration", + "station_id": "ID of the measuring station" + }, + "description": "Set up GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) air quality integration. If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/ru.json b/homeassistant/components/gios/.translations/ru.json new file mode 100644 index 00000000000000..f45ad9655501d9 --- /dev/null +++ b/homeassistant/components/gios/.translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 GIO\u015a.", + "invalid_sensors_data": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438.", + "wrong_station_id": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 ID \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "station_id": "ID \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438" + }, + "description": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 \u043e\u0442 \u041f\u043e\u043b\u044c\u0441\u043a\u043e\u0439 \u0438\u043d\u0441\u043f\u0435\u043a\u0446\u0438\u0438 \u043f\u043e \u043e\u0445\u0440\u0430\u043d\u0435 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b (GIO\u015a). \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438: https://www.home-assistant.io/integrations/gios.", + "title": "GIO\u015a (\u041f\u043e\u043b\u044c\u0441\u043a\u0430\u044f \u0438\u043d\u0441\u043f\u0435\u043a\u0446\u0438\u044f \u043f\u043e \u043e\u0445\u0440\u0430\u043d\u0435 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json index 3851d0a409fb5c..a4e7d72852a31a 100644 --- a/homeassistant/components/huawei_lte/.translations/pl.json +++ b/homeassistant/components/huawei_lte/.translations/pl.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nazwa us\u0142ugi powiadomie\u0144 (zmiana wymaga ponownego uruchomienia)", "recipient": "Odbiorcy powiadomie\u0144 SMS", "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia" } diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json index 6f478987f508fa..3850b86167a956 100644 --- a/homeassistant/components/huawei_lte/.translations/ru.json +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -33,7 +33,7 @@ "step": { "init": { "data": { - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439 (\u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a)", "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 SMS-\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" } diff --git a/homeassistant/components/local_ip/.translations/en.json b/homeassistant/components/local_ip/.translations/en.json new file mode 100644 index 00000000000000..869bb5a23d5cc6 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integration is already configured with an existing sensor with that name" + }, + "step": { + "user": { + "data": { + "name": "Sensor Name" + }, + "title": "Local IP Address" + } + }, + "title": "Local IP Address" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/ru.json b/homeassistant/components/local_ip/.translations/ru.json new file mode 100644 index 00000000000000..de92b9680f07f5 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0434\u0430\u0442\u0447\u0438\u043a\u0430." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441" + } + }, + "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441" + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/pl.json b/homeassistant/components/tesla/.translations/pl.json new file mode 100644 index 00000000000000..5a8a3d2ebd3f88 --- /dev/null +++ b/homeassistant/components/tesla/.translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "B\u0142\u0105d po\u0142\u0105czenia; sprawd\u017a sie\u0107 i spr\u00f3buj ponownie", + "identifier_exists": "Adres e-mail ju\u017c zarejestrowany", + "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia", + "unknown_error": "Nieznany b\u0142\u0105d, prosz\u0119 zg\u0142osi\u0107 dane z loga" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "description": "Wprowad\u017a dane", + "title": "Tesla - konfiguracja" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + } + } + } + } +} \ No newline at end of file From 272c00e81bc155f0cadf04ae108db53ef7d3f173 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 1 Jan 2020 03:23:29 -0800 Subject: [PATCH 2624/3953] Remove use of bin_type in Tesla component (#30315) * Remove use of bin_type * Convert _unit attribute to units * Remove unnecessary variable assignment * Change to using util library convert --- homeassistant/components/tesla/sensor.py | 39 ++++++++++-------------- homeassistant/components/tesla/switch.py | 4 +-- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index d93d3fa45d6ec1..78e1106ed43b51 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -8,6 +8,7 @@ TEMP_FAHRENHEIT, ) from homeassistant.helpers.entity import Entity +from homeassistant.util.distance import convert from . import DOMAIN as TESLA_DOMAIN, TeslaDevice @@ -24,10 +25,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] entities = [] for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["sensor"]: - if device.bin_type == 0x4: + if device.type == "temperature sensor": entities.append(TeslaSensor(device, controller, config_entry, "inside")) entities.append(TeslaSensor(device, controller, config_entry, "outside")) - elif device.bin_type in [0xA, 0xB, 0x5]: + else: entities.append(TeslaSensor(device, controller, config_entry)) async_add_entities(entities, True) @@ -38,7 +39,7 @@ class TeslaSensor(TeslaDevice, Entity): def __init__(self, tesla_device, controller, config_entry, sensor_type=None): """Initialize of the sensor.""" self.current_value = None - self._unit = None + self.units = None self.last_changed_time = None self.type = sensor_type super().__init__(tesla_device, controller, config_entry) @@ -61,7 +62,7 @@ def state(self): @property def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" - return self._unit + return self.units async def async_update(self): """Update the state from the sensor.""" @@ -69,32 +70,24 @@ async def async_update(self): await super().async_update() units = self.tesla_device.measurement - if self.tesla_device.bin_type == 0x4: + if self.tesla_device.type == "temperature sensor": if self.type == "outside": self.current_value = self.tesla_device.get_outside_temp() else: self.current_value = self.tesla_device.get_inside_temp() if units == "F": - self._unit = TEMP_FAHRENHEIT + self.units = TEMP_FAHRENHEIT else: - self._unit = TEMP_CELSIUS - elif self.tesla_device.bin_type == 0xA or self.tesla_device.bin_type == 0xB: + self.units = TEMP_CELSIUS + elif self.tesla_device.type in ["range sensor", "mileage sensor"]: self.current_value = self.tesla_device.get_value() - tesla_dist_unit = self.tesla_device.measurement - if tesla_dist_unit == "LENGTH_MILES": - self._unit = LENGTH_MILES + if units == "LENGTH_MILES": + self.units = LENGTH_MILES else: - self._unit = LENGTH_KILOMETERS - self.current_value /= 0.621371 - self.current_value = round(self.current_value, 2) + self.units = LENGTH_KILOMETERS + self.current_value = round( + convert(self.current_value, LENGTH_MILES, LENGTH_KILOMETERS), 2 + ) else: self.current_value = self.tesla_device.get_value() - if self.tesla_device.bin_type == 0x5: - self._unit = units - elif self.tesla_device.bin_type in (0xA, 0xB): - if units == "LENGTH_MILES": - self._unit = LENGTH_MILES - else: - self._unit = LENGTH_KILOMETERS - self.current_value /= 0.621371 - self.current_value = round(self.current_value, 2) + self.units = units diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 3fc424e390da3e..fc9b5e1ba8846a 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -19,10 +19,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] entities = [] for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["switch"]: - if device.bin_type == 0x8: + if device.type == "charger switch": entities.append(ChargerSwitch(device, controller, config_entry)) entities.append(UpdateSwitch(device, controller, config_entry)) - elif device.bin_type == 0x9: + elif device.type == "maxrange switch": entities.append(RangeSwitch(device, controller, config_entry)) async_add_entities(entities, True) From 298d8986274baf5ac1fbf15bf1ecfa23953d419c Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Wed, 1 Jan 2020 12:36:43 +0100 Subject: [PATCH 2625/3953] Upgrade youtube_dl to version 2020.01.01 (#30341) --- 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 b57b43968695a0..0c8166865577b5 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/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.12.25" + "youtube_dl==2020.01.01" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 4d697933f66363..378af0971c5142 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2085,7 +2085,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.12.25 +youtube_dl==2020.01.01 # homeassistant.components.zengge zengge==0.2 From e099d57bdec79c3b92daf12f0a621d993ff55982 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 1 Jan 2020 14:04:37 +0100 Subject: [PATCH 2626/3953] Upgrade zeroconf to 0.24.4 (#30347) --- homeassistant/components/zeroconf/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/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 720ffc3e69ee51..ec4db5931dc092 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -3,7 +3,7 @@ "name": "Zeroconf", "documentation": "https://www.home-assistant.io/integrations/zeroconf", "requirements": [ - "zeroconf==0.24.3" + "zeroconf==0.24.4" ], "dependencies": [ "api" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4bbbad9ad727a1..8347567246e100 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -24,7 +24,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.12 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.24.3 +zeroconf==0.24.4 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 378af0971c5142..5ca35843279c8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2091,7 +2091,7 @@ youtube_dl==2020.01.01 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.24.3 +zeroconf==0.24.4 # homeassistant.components.zha zha-quirks==0.0.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83e2299c4acf7f..ff1ded4434cb5f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -656,7 +656,7 @@ ya_ma==0.3.8 yahooweather==0.10 # homeassistant.components.zeroconf -zeroconf==0.24.3 +zeroconf==0.24.4 # homeassistant.components.zha zha-quirks==0.0.30 From 9fbe6d60cb724d18aea91c6d8ffb419a6ad3e9c2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Jan 2020 16:17:35 +0100 Subject: [PATCH 2627/3953] Migrate startca tests from coroutine to async/await (#30354) --- tests/components/startca/test_sensor.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index ab043c44d11ad1..aa06e13a812351 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -1,13 +1,10 @@ """Tests for the Start.ca sensor platform.""" -import asyncio - from homeassistant.bootstrap import async_setup_component from homeassistant.components.startca.sensor import StartcaData from homeassistant.helpers.aiohttp_client import async_get_clientsession -@asyncio.coroutine -def test_capped_setup(hass, aioclient_mock): +async def test_capped_setup(hass, aioclient_mock): """Test the default setup.""" config = { "platform": "startca", @@ -51,7 +48,7 @@ def test_capped_setup(hass, aioclient_mock): "https://www.start.ca/support/usage/api?key=" "NOTAKEY", text=result ) - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.start_ca_usage_ratio") assert state.attributes.get("unit_of_measurement") == "%" @@ -102,8 +99,7 @@ def test_capped_setup(hass, aioclient_mock): assert state.state == "95.05" -@asyncio.coroutine -def test_unlimited_setup(hass, aioclient_mock): +async def test_unlimited_setup(hass, aioclient_mock): """Test the default setup.""" config = { "platform": "startca", @@ -147,7 +143,7 @@ def test_unlimited_setup(hass, aioclient_mock): "https://www.start.ca/support/usage/api?key=" "NOTAKEY", text=result ) - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.start_ca_usage_ratio") assert state.attributes.get("unit_of_measurement") == "%" @@ -198,8 +194,7 @@ def test_unlimited_setup(hass, aioclient_mock): assert state.state == "inf" -@asyncio.coroutine -def test_bad_return_code(hass, aioclient_mock): +async def test_bad_return_code(hass, aioclient_mock): """Test handling a return code that isn't HTTP OK.""" aioclient_mock.get( "https://www.start.ca/support/usage/api?key=" "NOTAKEY", status=404 @@ -207,12 +202,11 @@ def test_bad_return_code(hass, aioclient_mock): scd = StartcaData(hass.loop, async_get_clientsession(hass), "NOTAKEY", 400) - result = yield from scd.async_update() + result = await scd.async_update() assert result is False -@asyncio.coroutine -def test_bad_json_decode(hass, aioclient_mock): +async def test_bad_json_decode(hass, aioclient_mock): """Test decoding invalid json result.""" aioclient_mock.get( "https://www.start.ca/support/usage/api?key=" "NOTAKEY", text="this is not xml" @@ -220,5 +214,5 @@ def test_bad_json_decode(hass, aioclient_mock): scd = StartcaData(hass.loop, async_get_clientsession(hass), "NOTAKEY", 400) - result = yield from scd.async_update() + result = await scd.async_update() assert result is False From 9ba0daa35818b16bf7004005724957afff18994a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Jan 2020 16:18:17 +0100 Subject: [PATCH 2628/3953] Migrate teksavvy tests from coroutine to async/await (#30353) --- tests/components/teksavvy/test_sensor.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/tests/components/teksavvy/test_sensor.py b/tests/components/teksavvy/test_sensor.py index 723cf07f173d41..30bb98911f8e8c 100644 --- a/tests/components/teksavvy/test_sensor.py +++ b/tests/components/teksavvy/test_sensor.py @@ -1,13 +1,10 @@ """Tests for the TekSavvy sensor platform.""" -import asyncio - from homeassistant.bootstrap import async_setup_component from homeassistant.components.teksavvy.sensor import TekSavvyData from homeassistant.helpers.aiohttp_client import async_get_clientsession -@asyncio.coroutine -def test_capped_setup(hass, aioclient_mock): +async def test_capped_setup(hass, aioclient_mock): """Test the default setup.""" config = { "platform": "teksavvy", @@ -45,7 +42,7 @@ def test_capped_setup(hass, aioclient_mock): text=result, ) - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.teksavvy_data_limit") assert state.attributes.get("unit_of_measurement") == "GB" @@ -88,8 +85,7 @@ def test_capped_setup(hass, aioclient_mock): assert state.state == "173.25" -@asyncio.coroutine -def test_unlimited_setup(hass, aioclient_mock): +async def test_unlimited_setup(hass, aioclient_mock): """Test the default setup.""" config = { "platform": "teksavvy", @@ -127,7 +123,7 @@ def test_unlimited_setup(hass, aioclient_mock): text=result, ) - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.teksavvy_data_limit") assert state.attributes.get("unit_of_measurement") == "GB" @@ -170,8 +166,7 @@ def test_unlimited_setup(hass, aioclient_mock): assert state.state == "inf" -@asyncio.coroutine -def test_bad_return_code(hass, aioclient_mock): +async def test_bad_return_code(hass, aioclient_mock): """Test handling a return code that isn't HTTP OK.""" aioclient_mock.get( "https://api.teksavvy.com/" @@ -182,12 +177,11 @@ def test_bad_return_code(hass, aioclient_mock): tsd = TekSavvyData(hass.loop, async_get_clientsession(hass), "notakey", 400) - result = yield from tsd.async_update() + result = await tsd.async_update() assert result is False -@asyncio.coroutine -def test_bad_json_decode(hass, aioclient_mock): +async def test_bad_json_decode(hass, aioclient_mock): """Test decoding invalid json result.""" aioclient_mock.get( "https://api.teksavvy.com/" @@ -198,5 +192,5 @@ def test_bad_json_decode(hass, aioclient_mock): tsd = TekSavvyData(hass.loop, async_get_clientsession(hass), "notakey", 400) - result = yield from tsd.async_update() + result = await tsd.async_update() assert result is False From 33828ae514fed0ed70c6b474d5f410bd9b8f2961 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Jan 2020 16:20:18 +0100 Subject: [PATCH 2629/3953] Migrate timer tests from coroutine to async/await (#30352) --- tests/components/timer/test_init.py | 32 ++++++++++------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 547f7e4ab0538a..93493fc3a55176 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -1,6 +1,5 @@ """The tests for the timer component.""" # pylint: disable=protected-access -import asyncio from datetime import timedelta import logging from unittest.mock import patch @@ -89,14 +88,11 @@ async def test_config_options(hass): assert "0:00:10" == state_2.attributes.get(ATTR_DURATION) -@asyncio.coroutine -def test_methods_and_events(hass): +async def test_methods_and_events(hass): """Test methods and events.""" hass.state = CoreState.starting - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}}) state = hass.states.get("timer.test1") assert state @@ -129,10 +125,10 @@ def fake_event_listener(event): expectedEvents = 0 for step in steps: if step["call"] is not None: - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, step["call"], {CONF_ENTITY_ID: "timer.test1"} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("timer.test1") assert state @@ -145,14 +141,11 @@ def fake_event_listener(event): assert len(results) == expectedEvents -@asyncio.coroutine -def test_wait_till_timer_expires(hass): +async def test_wait_till_timer_expires(hass): """Test for a timer to end.""" hass.state = CoreState.starting - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}}) state = hass.states.get("timer.test1") assert state @@ -169,10 +162,10 @@ def fake_event_listener(event): hass.bus.async_listen(EVENT_TIMER_FINISHED, fake_event_listener) hass.bus.async_listen(EVENT_TIMER_CANCELLED, fake_event_listener) - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("timer.test1") assert state @@ -182,7 +175,7 @@ def fake_event_listener(event): assert len(results) == 1 async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() state = hass.states.get("timer.test1") assert state @@ -192,14 +185,11 @@ def fake_event_listener(event): assert len(results) == 2 -@asyncio.coroutine -def test_no_initial_state_and_no_restore_state(hass): +async def test_no_initial_state_and_no_restore_state(hass): """Ensure that entity is create without initial and restore feature.""" hass.state = CoreState.starting - yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}} - ) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}}) state = hass.states.get("timer.test1") assert state From bc6e2a06e6d7bf265db55ce44f36ac31bde5d600 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Jan 2020 16:21:37 +0100 Subject: [PATCH 2630/3953] Migrate yr tests from coroutine to async/await (#30351) --- tests/components/yr/test_sensor.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/components/yr/test_sensor.py b/tests/components/yr/test_sensor.py index 21ce0bbe7cebfe..7e2e8543f77519 100644 --- a/tests/components/yr/test_sensor.py +++ b/tests/components/yr/test_sensor.py @@ -1,5 +1,4 @@ """The tests for the Yr sensor platform.""" -import asyncio from datetime import datetime from unittest.mock import patch @@ -11,8 +10,7 @@ NOW = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) -@asyncio.coroutine -def test_default_setup(hass, aioclient_mock): +async def test_default_setup(hass, aioclient_mock): """Test the default setup.""" aioclient_mock.get( "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", @@ -23,7 +21,7 @@ def test_default_setup(hass, aioclient_mock): with patch( "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.yr_symbol") @@ -31,8 +29,7 @@ def test_default_setup(hass, aioclient_mock): assert state.attributes.get("unit_of_measurement") is None -@asyncio.coroutine -def test_custom_setup(hass, aioclient_mock): +async def test_custom_setup(hass, aioclient_mock): """Test a custom setup.""" aioclient_mock.get( "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", @@ -54,7 +51,7 @@ def test_custom_setup(hass, aioclient_mock): with patch( "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.yr_pressure") assert state.attributes.get("unit_of_measurement") == "hPa" @@ -77,8 +74,7 @@ def test_custom_setup(hass, aioclient_mock): assert state.state == "3.5" -@asyncio.coroutine -def test_forecast_setup(hass, aioclient_mock): +async def test_forecast_setup(hass, aioclient_mock): """Test a custom setup with 24h forecast.""" aioclient_mock.get( "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", @@ -101,7 +97,7 @@ def test_forecast_setup(hass, aioclient_mock): with patch( "homeassistant.components.yr.sensor.dt_util.utcnow", return_value=NOW ), assert_setup_component(1): - yield from async_setup_component(hass, "sensor", {"sensor": config}) + await async_setup_component(hass, "sensor", {"sensor": config}) state = hass.states.get("sensor.yr_pressure") assert state.attributes.get("unit_of_measurement") == "hPa" From 83e05da0b3d0bf644cfb25b52d527c7915685492 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Wed, 1 Jan 2020 10:40:11 -0800 Subject: [PATCH 2631/3953] Fix media_player example for select_source service (#30358) --- homeassistant/components/media_player/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index ad6b8a78957bd4..19ef1cb14c08ba 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -125,7 +125,7 @@ select_source: fields: entity_id: description: Name(s) of entities to change source on. - example: 'media_player.media_player.txnr535_0009b0d81f82' + example: 'media_player.txnr535_0009b0d81f82' source: description: Name of the source to switch to. Platform dependent. example: 'video1' From 0479e93de7266031ed1a7798fbad205d736719f4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 1 Jan 2020 20:06:20 +0100 Subject: [PATCH 2632/3953] Upgrade python_opendata_transport to 0.2.1 (#30348) --- homeassistant/components/swiss_public_transport/manifest.json | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/manifest.json b/homeassistant/components/swiss_public_transport/manifest.json index c91d85fecd7e11..387b3da5da4872 100644 --- a/homeassistant/components/swiss_public_transport/manifest.json +++ b/homeassistant/components/swiss_public_transport/manifest.json @@ -3,10 +3,10 @@ "name": "Swiss public transport", "documentation": "https://www.home-assistant.io/integrations/swiss_public_transport", "requirements": [ - "python_opendata_transport==0.1.4" + "python_opendata_transport==0.2.1" ], "dependencies": [], "codeowners": [ "@fabaff" ] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 5ca35843279c8e..d2ed4e0e1e26b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1646,7 +1646,7 @@ python-wink==1.10.5 python_awair==0.0.4 # homeassistant.components.swiss_public_transport -python_opendata_transport==0.1.4 +python_opendata_transport==0.2.1 # homeassistant.components.egardia pythonegardia==1.0.40 From 790b2d00c71ebfd112a078e2bd85ad6c26c6c158 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 1 Jan 2020 11:49:20 -0800 Subject: [PATCH 2633/3953] Fix HVAC mode for Tesla (#30287) * Fix HVAC mode for Tesla * Change HVAC_MODE to HEAT_COOL --- homeassistant/components/tesla/climate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index d3c87035c9ca9d..d7f21d7895f1dd 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -3,7 +3,7 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, ) @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) -SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF] +SUPPORT_HVAC = [HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF] async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -59,7 +59,7 @@ def hvac_mode(self): Need to be one of HVAC_MODE_*. """ if self.tesla_device.is_hvac_enabled(): - return HVAC_MODE_HEAT + return HVAC_MODE_HEAT_COOL return HVAC_MODE_OFF @property @@ -108,5 +108,5 @@ async def async_set_hvac_mode(self, hvac_mode): _LOGGER.debug("Setting mode for: %s", self._name) if hvac_mode == HVAC_MODE_OFF: await self.tesla_device.set_status(False) - elif hvac_mode == HVAC_MODE_HEAT: + elif hvac_mode == HVAC_MODE_HEAT_COOL: await self.tesla_device.set_status(True) From 99bc911f7ff9ec1af9036549957d38b6094a37b3 Mon Sep 17 00:00:00 2001 From: Aaron David Schneider Date: Wed, 1 Jan 2020 22:03:38 +0100 Subject: [PATCH 2634/3953] Add attributes to fritz device_tracker (#30350) * add attributes to device_tracker * fix pylint * requested changes * docstring updated --- homeassistant/components/fritz/device_tracker.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index ab4deec96f771e..d16fcbb3a1eb6d 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -79,6 +79,14 @@ def get_device_name(self, device): return None return ret + def get_extra_attributes(self, device): + """Return the attributes (ip, mac) of the given device or None if is not known.""" + ip_device = self.fritz_box.get_specific_host_entry(device).get("NewIPAddress") + + if not ip_device: + return None + return {"ip": ip_device, "mac": device} + def _update_info(self): """Retrieve latest information from the FRITZ!Box.""" if not self.success_init: From 3a4db2fae7733025f6b52f4fc5acb77676cec1d7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:13:04 +0100 Subject: [PATCH 2635/3953] Migrate mailbox tests from coroutine to async/await (#30361) --- tests/components/mailbox/test_init.py | 51 +++++++++++---------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index ec398be0d4ae89..258e0cc7ebfe3f 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -1,5 +1,4 @@ """The tests for the mailbox component.""" -import asyncio from hashlib import sha1 import pytest @@ -16,44 +15,40 @@ def mock_http_client(hass, hass_client): return hass.loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_get_platforms_from_mailbox(mock_http_client): +async def test_get_platforms_from_mailbox(mock_http_client): """Get platforms from mailbox.""" url = "/api/mailbox/platforms" - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 200 - result = yield from req.json() + result = await req.json() assert len(result) == 1 and "DemoMailbox" == result[0].get("name", None) -@asyncio.coroutine -def test_get_messages_from_mailbox(mock_http_client): +async def test_get_messages_from_mailbox(mock_http_client): """Get messages from mailbox.""" url = "/api/mailbox/messages/DemoMailbox" - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 200 - result = yield from req.json() + result = await req.json() assert len(result) == 10 -@asyncio.coroutine -def test_get_media_from_mailbox(mock_http_client): +async def test_get_media_from_mailbox(mock_http_client): """Get audio from mailbox.""" mp3sha = "3f67c4ea33b37d1710f772a26dd3fb43bb159d50" msgtxt = "Message 1. " "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " msgsha = sha1(msgtxt.encode("utf-8")).hexdigest() url = "/api/mailbox/media/DemoMailbox/%s" % (msgsha) - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 200 - data = yield from req.read() + data = await req.read() assert sha1(data).hexdigest() == mp3sha -@asyncio.coroutine -def test_delete_from_mailbox(mock_http_client): +async def test_delete_from_mailbox(mock_http_client): """Get audio from mailbox.""" msgtxt1 = "Message 1. " "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " msgtxt2 = "Message 3. " "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " @@ -62,50 +57,46 @@ def test_delete_from_mailbox(mock_http_client): for msg in [msgsha1, msgsha2]: url = "/api/mailbox/delete/DemoMailbox/%s" % (msg) - req = yield from mock_http_client.delete(url) + req = await mock_http_client.delete(url) assert req.status == 200 url = "/api/mailbox/messages/DemoMailbox" - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 200 - result = yield from req.json() + result = await req.json() assert len(result) == 8 -@asyncio.coroutine -def test_get_messages_from_invalid_mailbox(mock_http_client): +async def test_get_messages_from_invalid_mailbox(mock_http_client): """Get messages from mailbox.""" url = "/api/mailbox/messages/mailbox.invalid_mailbox" - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 404 -@asyncio.coroutine -def test_get_media_from_invalid_mailbox(mock_http_client): +async def test_get_media_from_invalid_mailbox(mock_http_client): """Get messages from mailbox.""" msgsha = "0000000000000000000000000000000000000000" url = "/api/mailbox/media/mailbox.invalid_mailbox/%s" % (msgsha) - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 404 -@asyncio.coroutine -def test_get_media_from_invalid_msgid(mock_http_client): +async def test_get_media_from_invalid_msgid(mock_http_client): """Get messages from mailbox.""" msgsha = "0000000000000000000000000000000000000000" url = "/api/mailbox/media/DemoMailbox/%s" % (msgsha) - req = yield from mock_http_client.get(url) + req = await mock_http_client.get(url) assert req.status == 500 -@asyncio.coroutine -def test_delete_from_invalid_mailbox(mock_http_client): +async def test_delete_from_invalid_mailbox(mock_http_client): """Get audio from mailbox.""" msgsha = "0000000000000000000000000000000000000000" url = "/api/mailbox/delete/mailbox.invalid_mailbox/%s" % (msgsha) - req = yield from mock_http_client.delete(url) + req = await mock_http_client.delete(url) assert req.status == 404 From 0fb5fbd85c3a0057ef537f57f5675fae29fae269 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:13:43 +0100 Subject: [PATCH 2636/3953] Migrate hassio tests from coroutine to async/await (#30363) --- tests/components/hassio/test_http.py | 40 ++++++------- tests/components/hassio/test_init.py | 86 ++++++++++++---------------- 2 files changed, 56 insertions(+), 70 deletions(-) diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 96d53f93c3abda..52cb3232ca61ad 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -5,35 +5,32 @@ import pytest -@asyncio.coroutine -def test_forward_request(hassio_client, aioclient_mock): +async def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" aioclient_mock.post("http://127.0.0.1/beer", text="response") - resp = yield from hassio_client.post("/api/hassio/beer") + resp = await hassio_client.post("/api/hassio/beer") # Check we got right response assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "response" # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 -@asyncio.coroutine @pytest.mark.parametrize( "build_type", ["supervisor/info", "homeassistant/update", "host/info"] ) -def test_auth_required_forward_request(hassio_noauth_client, build_type): +async def test_auth_required_forward_request(hassio_noauth_client, build_type): """Test auth required for normal request.""" - resp = yield from hassio_noauth_client.post("/api/hassio/{}".format(build_type)) + resp = await hassio_noauth_client.post("/api/hassio/{}".format(build_type)) # Check we got right response assert resp.status == 401 -@asyncio.coroutine @pytest.mark.parametrize( "build_type", [ @@ -45,61 +42,60 @@ def test_auth_required_forward_request(hassio_noauth_client, build_type): "app/app.js", ], ) -def test_forward_request_no_auth_for_panel(hassio_client, build_type, aioclient_mock): +async def test_forward_request_no_auth_for_panel( + hassio_client, build_type, aioclient_mock +): """Test no auth needed for .""" aioclient_mock.get("http://127.0.0.1/{}".format(build_type), text="response") - resp = yield from hassio_client.get("/api/hassio/{}".format(build_type)) + resp = await hassio_client.get("/api/hassio/{}".format(build_type)) # Check we got right response assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "response" # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 -@asyncio.coroutine -def test_forward_request_no_auth_for_logo(hassio_client, aioclient_mock): +async def test_forward_request_no_auth_for_logo(hassio_client, aioclient_mock): """Test no auth needed for .""" aioclient_mock.get("http://127.0.0.1/addons/bl_b392/logo", text="response") - resp = yield from hassio_client.get("/api/hassio/addons/bl_b392/logo") + resp = await hassio_client.get("/api/hassio/addons/bl_b392/logo") # Check we got right response assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "response" # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 -@asyncio.coroutine -def test_forward_log_request(hassio_client, aioclient_mock): +async def test_forward_log_request(hassio_client, aioclient_mock): """Test fetching normal log path doesn't remove ANSI color escape codes.""" aioclient_mock.get("http://127.0.0.1/beer/logs", text="\033[32mresponse\033[0m") - resp = yield from hassio_client.get("/api/hassio/beer/logs") + resp = await hassio_client.get("/api/hassio/beer/logs") # Check we got right response assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "\033[32mresponse\033[0m" # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 -@asyncio.coroutine -def test_bad_gateway_when_cannot_find_supervisor(hassio_client): +async def test_bad_gateway_when_cannot_find_supervisor(hassio_client): """Test we get a bad gateway error if we can't find supervisor.""" with patch( "homeassistant.components.hassio.http.async_timeout.timeout", side_effect=asyncio.TimeoutError, ): - resp = yield from hassio_client.get("/api/hassio/addons/test/info") + resp = await hassio_client.get("/api/hassio/addons/test/info") assert resp.status == 502 diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index c1e3d7ab2bf038..1e227f943edeca 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1,5 +1,4 @@ """The tests for the hassio component.""" -import asyncio import os from unittest.mock import Mock, patch @@ -30,11 +29,10 @@ def mock_all(aioclient_mock): ) -@asyncio.coroutine -def test_setup_api_ping(hass, aioclient_mock): +async def test_setup_api_ping(hass, aioclient_mock): """Test setup with API ping.""" with patch.dict(os.environ, MOCK_ENVIRON): - result = yield from async_setup_component(hass, "hassio", {}) + result = await async_setup_component(hass, "hassio", {}) assert result assert aioclient_mock.call_count == 5 @@ -68,11 +66,10 @@ async def test_setup_api_panel(hass, aioclient_mock): } -@asyncio.coroutine -def test_setup_api_push_api_data(hass, aioclient_mock): +async def test_setup_api_push_api_data(hass, aioclient_mock): """Test setup with API push.""" with patch.dict(os.environ, MOCK_ENVIRON): - result = yield from async_setup_component( + result = await async_setup_component( hass, "hassio", {"http": {"server_port": 9999}, "hassio": {}} ) assert result @@ -83,11 +80,10 @@ def test_setup_api_push_api_data(hass, aioclient_mock): assert aioclient_mock.mock_calls[1][2]["watchdog"] -@asyncio.coroutine -def test_setup_api_push_api_data_server_host(hass, aioclient_mock): +async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): """Test setup with API push with active server host.""" with patch.dict(os.environ, MOCK_ENVIRON): - result = yield from async_setup_component( + result = await async_setup_component( hass, "hassio", {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, @@ -175,45 +171,41 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][2]["timezone"] == "America/New_York" -@asyncio.coroutine -def test_setup_hassio_no_additional_data(hass, aioclient_mock): +async def test_setup_hassio_no_additional_data(hass, aioclient_mock): """Test setup with API push default data.""" with patch.dict(os.environ, MOCK_ENVIRON), patch.dict( os.environ, {"HASSIO_TOKEN": "123456"} ): - result = yield from async_setup_component(hass, "hassio", {"hassio": {}}) + result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result assert aioclient_mock.call_count == 5 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" -@asyncio.coroutine -def test_fail_setup_without_environ_var(hass): +async def test_fail_setup_without_environ_var(hass): """Fail setup if no environ variable set.""" with patch.dict(os.environ, {}, clear=True): - result = yield from async_setup_component(hass, "hassio", {}) + result = await async_setup_component(hass, "hassio", {}) assert not result -@asyncio.coroutine -def test_warn_when_cannot_connect(hass, caplog): +async def test_warn_when_cannot_connect(hass, caplog): """Fail warn when we cannot connect.""" with patch.dict(os.environ, MOCK_ENVIRON), patch( "homeassistant.components.hassio.HassIO.is_connected", Mock(return_value=mock_coro(None)), ): - result = yield from async_setup_component(hass, "hassio", {}) + result = await async_setup_component(hass, "hassio", {}) assert result assert hass.components.hassio.is_hassio() assert "Not connected with Hass.io / system to busy!" in caplog.text -@asyncio.coroutine -def test_service_register(hassio_env, hass): +async def test_service_register(hassio_env, hass): """Check if service will be setup.""" - assert (yield from async_setup_component(hass, "hassio", {})) + assert await async_setup_component(hass, "hassio", {}) assert hass.services.has_service("hassio", "addon_start") assert hass.services.has_service("hassio", "addon_stop") assert hass.services.has_service("hassio", "addon_restart") @@ -227,10 +219,9 @@ def test_service_register(hassio_env, hass): assert hass.services.has_service("hassio", "restore_partial") -@asyncio.coroutine -def test_service_calls(hassio_env, hass, aioclient_mock): +async def test_service_calls(hassio_env, hass, aioclient_mock): """Call service and check the API calls behind that.""" - assert (yield from async_setup_component(hass, "hassio", {})) + assert await async_setup_component(hass, "hassio", {}) aioclient_mock.post("http://127.0.0.1/addons/test/start", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/addons/test/stop", json={"result": "ok"}) @@ -247,30 +238,30 @@ def test_service_calls(hassio_env, hass, aioclient_mock): "http://127.0.0.1/snapshots/test/restore/partial", json={"result": "ok"} ) - yield from hass.services.async_call("hassio", "addon_start", {"addon": "test"}) - yield from hass.services.async_call("hassio", "addon_stop", {"addon": "test"}) - yield from hass.services.async_call("hassio", "addon_restart", {"addon": "test"}) - yield from hass.services.async_call( + await hass.services.async_call("hassio", "addon_start", {"addon": "test"}) + await hass.services.async_call("hassio", "addon_stop", {"addon": "test"}) + await hass.services.async_call("hassio", "addon_restart", {"addon": "test"}) + await hass.services.async_call( "hassio", "addon_stdin", {"addon": "test", "input": "test"} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 7 assert aioclient_mock.mock_calls[-1][2] == "test" - yield from hass.services.async_call("hassio", "host_shutdown", {}) - yield from hass.services.async_call("hassio", "host_reboot", {}) - yield from hass.async_block_till_done() + await hass.services.async_call("hassio", "host_shutdown", {}) + await hass.services.async_call("hassio", "host_reboot", {}) + await hass.async_block_till_done() assert aioclient_mock.call_count == 9 - yield from hass.services.async_call("hassio", "snapshot_full", {}) - yield from hass.services.async_call( + await hass.services.async_call("hassio", "snapshot_full", {}) + await hass.services.async_call( "hassio", "snapshot_partial", {"addons": ["test"], "folders": ["ssl"], "password": "123456"}, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 11 assert aioclient_mock.mock_calls[-1][2] == { @@ -279,8 +270,8 @@ def test_service_calls(hassio_env, hass, aioclient_mock): "password": "123456", } - yield from hass.services.async_call("hassio", "restore_full", {"snapshot": "test"}) - yield from hass.services.async_call( + await hass.services.async_call("hassio", "restore_full", {"snapshot": "test"}) + await hass.services.async_call( "hassio", "restore_partial", { @@ -291,7 +282,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock): "password": "123456", }, ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 13 assert aioclient_mock.mock_calls[-1][2] == { @@ -302,29 +293,28 @@ def test_service_calls(hassio_env, hass, aioclient_mock): } -@asyncio.coroutine -def test_service_calls_core(hassio_env, hass, aioclient_mock): +async def test_service_calls_core(hassio_env, hass, aioclient_mock): """Call core service and check the API calls behind that.""" - assert (yield from async_setup_component(hass, "hassio", {})) + assert await async_setup_component(hass, "hassio", {}) aioclient_mock.post("http://127.0.0.1/homeassistant/restart", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/homeassistant/stop", json={"result": "ok"}) - yield from hass.services.async_call("homeassistant", "stop") - yield from hass.async_block_till_done() + await hass.services.async_call("homeassistant", "stop") + await hass.async_block_till_done() assert aioclient_mock.call_count == 4 - yield from hass.services.async_call("homeassistant", "check_config") - yield from hass.async_block_till_done() + await hass.services.async_call("homeassistant", "check_config") + await hass.async_block_till_done() assert aioclient_mock.call_count == 4 with patch( "homeassistant.config.async_check_ha_config_file", return_value=mock_coro() ) as mock_check_config: - yield from hass.services.async_call("homeassistant", "restart") - yield from hass.async_block_till_done() + await hass.services.async_call("homeassistant", "restart") + await hass.async_block_till_done() assert mock_check_config.called assert aioclient_mock.call_count == 5 From 4e7b35355dcbf0193804a58ed28af0ec585c5088 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:15:29 +0100 Subject: [PATCH 2637/3953] Migrate websocket_api tests from coroutine to async/await (#30364) --- tests/components/websocket_api/test_init.py | 36 +++++++++------------ 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index 7cda3200e7b170..d32f55516aa9ce 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -1,5 +1,4 @@ """Tests for the Home Assistant Websocket API.""" -import asyncio from unittest.mock import Mock, patch from aiohttp import WSMsgType @@ -16,12 +15,11 @@ def mock_low_queue(): yield -@asyncio.coroutine -def test_invalid_message_format(websocket_client): +async def test_invalid_message_format(websocket_client): """Test sending invalid JSON.""" - yield from websocket_client.send_json({"type": 5}) + await websocket_client.send_json({"type": 5}) - msg = yield from websocket_client.receive_json() + msg = await websocket_client.receive_json() assert msg["type"] == const.TYPE_RESULT error = msg["error"] @@ -29,42 +27,38 @@ def test_invalid_message_format(websocket_client): assert error["message"].startswith("Message incorrectly formatted") -@asyncio.coroutine -def test_invalid_json(websocket_client): +async def test_invalid_json(websocket_client): """Test sending invalid JSON.""" - yield from websocket_client.send_str("this is not JSON") + await websocket_client.send_str("this is not JSON") - msg = yield from websocket_client.receive() + msg = await websocket_client.receive() assert msg.type == WSMsgType.close -@asyncio.coroutine -def test_quiting_hass(hass, websocket_client): +async def test_quiting_hass(hass, websocket_client): """Test sending invalid JSON.""" with patch.object(hass.loop, "stop"): - yield from hass.async_stop() + await hass.async_stop() - msg = yield from websocket_client.receive() + msg = await websocket_client.receive() assert msg.type == WSMsgType.CLOSE -@asyncio.coroutine -def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): +async def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): """Test get_panels command.""" for idx in range(10): - yield from websocket_client.send_json({"id": idx + 1, "type": "ping"}) - msg = yield from websocket_client.receive() + await websocket_client.send_json({"id": idx + 1, "type": "ping"}) + msg = await websocket_client.receive() assert msg.type == WSMsgType.close -@asyncio.coroutine -def test_unknown_command(websocket_client): +async def test_unknown_command(websocket_client): """Test get_panels command.""" - yield from websocket_client.send_json({"id": 5, "type": "unknown_command"}) + await websocket_client.send_json({"id": 5, "type": "unknown_command"}) - msg = yield from websocket_client.receive_json() + msg = await websocket_client.receive_json() assert not msg["success"] assert msg["error"]["code"] == const.ERR_UNKNOWN_COMMAND From 320dc52bb3811621cbabbc95aae34a83bac3823e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:16:27 +0100 Subject: [PATCH 2638/3953] Migrate config tests from coroutine to async/await (#30366) --- .../components/config/test_config_entries.py | 85 +++++------- tests/components/config/test_customize.py | 43 +++---- tests/components/config/test_group.py | 52 ++++---- tests/components/config/test_init.py | 20 ++- tests/components/config/test_zwave.py | 121 ++++++++---------- 5 files changed, 133 insertions(+), 188 deletions(-) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 6631bbf8fbf064..ccd41eeb3a5752 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1,6 +1,5 @@ """Test config entries API.""" -import asyncio from collections import OrderedDict from unittest.mock import patch @@ -94,16 +93,15 @@ def async_get_options_flow(config, options): ] -@asyncio.coroutine -def test_remove_entry(hass, client): +async def test_remove_entry(hass, client): """Test removing an entry via the API.""" entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) entry.add_to_hass(hass) - resp = yield from client.delete( + resp = await client.delete( "/api/config/config_entries/entry/{}".format(entry.entry_id) ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert data == {"require_restart": True} assert len(hass.config_entries.async_entries()) == 0 @@ -120,13 +118,12 @@ async def test_remove_entry_unauth(hass, client, hass_admin_user): assert len(hass.config_entries.async_entries()) == 1 -@asyncio.coroutine -def test_available_flows(hass, client): +async def test_available_flows(hass, client): """Test querying the available flows.""" with patch.object(config_flows, "FLOWS", ["hello", "world"]): - resp = yield from client.get("/api/config/config_entries/flow_handlers") + resp = await client.get("/api/config/config_entries/flow_handlers") assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert set(data) == set(["hello", "world"]) @@ -135,14 +132,12 @@ def test_available_flows(hass, client): ############################ -@asyncio.coroutine -def test_initialize_flow(hass, client): +async def test_initialize_flow(hass, client): """Test we can initialize a flow.""" mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required("username")] = str schema[vol.Required("password")] = str @@ -155,12 +150,12 @@ def async_step_user(self, user_input=None): ) with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() data.pop("flow_id") @@ -182,8 +177,7 @@ async def test_initialize_flow_unauth(hass, client, hass_admin_user): hass_admin_user.groups = [] class TestFlow(core_ce.ConfigFlow): - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required("username")] = str schema[vol.Required("password")] = str @@ -203,23 +197,21 @@ def async_step_user(self, user_input=None): assert resp.status == 401 -@asyncio.coroutine -def test_abort(hass, client): +async def test_abort(hass, client): """Test a flow that aborts.""" mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): return self.async_abort(reason="bla") with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() data.pop("flow_id") assert data == { "description_placeholders": None, @@ -229,8 +221,7 @@ def async_step_user(self, user_input=None): } -@asyncio.coroutine -def test_create_account(hass, client): +async def test_create_account(hass, client): """Test a flow that creates an account.""" mock_entity_platform(hass, "config_flow.test", None) @@ -239,14 +230,13 @@ def test_create_account(hass, client): class TestFlow(core_ce.ConfigFlow): VERSION = 1 - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): return self.async_create_entry( title="Test Entry", data={"secret": "account_token"} ) with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) @@ -255,7 +245,7 @@ def async_step_user(self, user_input=None): entries = hass.config_entries.async_entries("test") assert len(entries) == 1 - data = yield from resp.json() + data = await resp.json() data.pop("flow_id") assert data == { "handler": "test", @@ -268,8 +258,7 @@ def async_step_user(self, user_input=None): } -@asyncio.coroutine -def test_two_step_flow(hass, client): +async def test_two_step_flow(hass, client): """Test we can finish a two step flow.""" mock_integration(hass, MockModule("test", async_setup_entry=mock_coro_func(True))) mock_entity_platform(hass, "config_flow.test", None) @@ -277,24 +266,22 @@ def test_two_step_flow(hass, client): class TestFlow(core_ce.ConfigFlow): VERSION = 1 - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): return self.async_show_form( step_id="account", data_schema=vol.Schema({"user_title": str}) ) - @asyncio.coroutine - def async_step_account(self, user_input=None): + async def async_step_account(self, user_input=None): return self.async_create_entry( title=user_input["user_title"], data={"secret": "account_token"} ) with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() flow_id = data.pop("flow_id") assert data == { "type": "form", @@ -306,7 +293,7 @@ def async_step_account(self, user_input=None): } with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow/{}".format(flow_id), json={"user_title": "user-title"}, ) @@ -315,7 +302,7 @@ def async_step_account(self, user_input=None): entries = hass.config_entries.async_entries("test") assert len(entries) == 1 - data = yield from resp.json() + data = await resp.json() data.pop("flow_id") assert data == { "handler": "test", @@ -336,14 +323,12 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user): class TestFlow(core_ce.ConfigFlow): VERSION = 1 - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): return self.async_show_form( step_id="account", data_schema=vol.Schema({"user_title": str}) ) - @asyncio.coroutine - def async_step_account(self, user_input=None): + async def async_step_account(self, user_input=None): return self.async_create_entry( title=user_input["user_title"], data={"secret": "account_token"} ) @@ -415,14 +400,12 @@ async def test_get_progress_index_unauth(hass, hass_ws_client, hass_admin_user): assert response["error"]["code"] == "unauthorized" -@asyncio.coroutine -def test_get_progress_flow(hass, client): +async def test_get_progress_flow(hass, client): """Test we can query the API for same result as we get from init a flow.""" mock_entity_platform(hass, "config_flow.test", None) class TestFlow(core_ce.ConfigFlow): - @asyncio.coroutine - def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None): schema = OrderedDict() schema[vol.Required("username")] = str schema[vol.Required("password")] = str @@ -434,19 +417,19 @@ def async_step_user(self, user_input=None): ) with patch.dict(HANDLERS, {"test": TestFlow}): - resp = yield from client.post( + resp = await client.post( "/api/config/config_entries/flow", json={"handler": "test"} ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() - resp2 = yield from client.get( + resp2 = await client.get( "/api/config/config_entries/flow/{}".format(data["flow_id"]) ) assert resp2.status == 200 - data2 = yield from resp2.json() + data2 = await resp2.json() assert data == data2 diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index f6a678ce8f0fb8..45c1f40d4ad62f 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -1,5 +1,4 @@ """Test Customize config panel.""" -import asyncio import json from unittest.mock import patch @@ -8,13 +7,12 @@ from homeassistant.config import DATA_CUSTOMIZE -@asyncio.coroutine -def test_get_entity(hass, hass_client): +async def test_get_entity(hass, hass_client): """Test getting entity.""" with patch.object(config, "SECTIONS", ["customize"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() def mock_read(path): """Mock reading data.""" @@ -22,21 +20,20 @@ def mock_read(path): hass.data[DATA_CUSTOMIZE] = {"hello.beer": {"cold": "beer"}} with patch("homeassistant.components.config._read", mock_read): - resp = yield from client.get("/api/config/customize/config/hello.beer") + resp = await client.get("/api/config/customize/config/hello.beer") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"local": {"free": "beer"}, "global": {"cold": "beer"}} -@asyncio.coroutine -def test_update_entity(hass, hass_client): +async def test_update_entity(hass, hass_client): """Test updating entity.""" with patch.object(config, "SECTIONS", ["customize"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() orig_data = { "hello.beer": {"ignored": True}, @@ -57,7 +54,7 @@ def mock_write(path, data): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write ): - resp = yield from client.post( + resp = await client.post( "/api/config/customize/config/hello.world", data=json.dumps( {"name": "Beer", "entities": ["light.top", "light.bottom"]} @@ -65,7 +62,7 @@ def mock_write(path, data): ) assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"result": "ok"} state = hass.states.get("hello.world") @@ -82,31 +79,27 @@ def mock_write(path, data): assert written[0] == orig_data -@asyncio.coroutine -def test_update_entity_invalid_key(hass, hass_client): +async def test_update_entity_invalid_key(hass, hass_client): """Test updating entity.""" with patch.object(config, "SECTIONS", ["customize"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.post( + resp = await client.post( "/api/config/customize/config/not_entity", data=json.dumps({"name": "YO"}) ) assert resp.status == 400 -@asyncio.coroutine -def test_update_entity_invalid_json(hass, hass_client): +async def test_update_entity_invalid_json(hass, hass_client): """Test updating entity.""" with patch.object(config, "SECTIONS", ["customize"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.post( - "/api/config/customize/config/hello.beer", data="not json" - ) + resp = await client.post("/api/config/customize/config/hello.beer", data="not json") assert resp.status == 400 diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index 3a4a145105af65..1b79f30a5b6feb 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -1,5 +1,4 @@ """Test Group config panel.""" -import asyncio import json from unittest.mock import MagicMock, patch @@ -9,34 +8,32 @@ VIEW_NAME = "api:config:group:config" -@asyncio.coroutine -def test_get_device_config(hass, hass_client): +async def test_get_device_config(hass, hass_client): """Test getting device config.""" with patch.object(config, "SECTIONS", ["group"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() def mock_read(path): """Mock reading data.""" return {"hello.beer": {"free": "beer"}, "other.entity": {"do": "something"}} with patch("homeassistant.components.config._read", mock_read): - resp = yield from client.get("/api/config/group/config/hello.beer") + resp = await client.get("/api/config/group/config/hello.beer") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"free": "beer"} -@asyncio.coroutine -def test_update_device_config(hass, hass_client): +async def test_update_device_config(hass, hass_client): """Test updating device config.""" with patch.object(config, "SECTIONS", ["group"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() orig_data = { "hello.beer": {"ignored": True}, @@ -58,7 +55,7 @@ def mock_write(path, data): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write ), patch.object(hass.services, "async_call", mock_call): - resp = yield from client.post( + resp = await client.post( "/api/config/group/config/hello_beer", data=json.dumps( {"name": "Beer", "entities": ["light.top", "light.bottom"]} @@ -66,7 +63,7 @@ def mock_write(path, data): ) assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"result": "ok"} orig_data["hello_beer"]["name"] = "Beer" @@ -76,46 +73,41 @@ def mock_write(path, data): mock_call.assert_called_once_with("group", "reload") -@asyncio.coroutine -def test_update_device_config_invalid_key(hass, hass_client): +async def test_update_device_config_invalid_key(hass, hass_client): """Test updating device config.""" with patch.object(config, "SECTIONS", ["group"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.post( + resp = await client.post( "/api/config/group/config/not a slug", data=json.dumps({"name": "YO"}) ) assert resp.status == 400 -@asyncio.coroutine -def test_update_device_config_invalid_data(hass, hass_client): +async def test_update_device_config_invalid_data(hass, hass_client): """Test updating device config.""" with patch.object(config, "SECTIONS", ["group"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.post( + resp = await client.post( "/api/config/group/config/hello_beer", data=json.dumps({"invalid_option": 2}) ) assert resp.status == 400 -@asyncio.coroutine -def test_update_device_config_invalid_json(hass, hass_client): +async def test_update_device_config_invalid_json(hass, hass_client): """Test updating device config.""" with patch.object(config, "SECTIONS", ["group"]): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.post( - "/api/config/group/config/hello_beer", data="not json" - ) + resp = await client.post("/api/config/group/config/hello_beer", data="not json") assert resp.status == 400 diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 21af45a120387b..7f9b62d71f607b 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -1,5 +1,4 @@ """Test config init.""" -import asyncio from unittest.mock import patch from homeassistant.components import config @@ -9,15 +8,13 @@ from tests.common import mock_component, mock_coro -@asyncio.coroutine -def test_config_setup(hass, loop): +async def test_config_setup(hass, loop): """Test it sets up hassbian.""" - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) assert "config" in hass.config.components -@asyncio.coroutine -def test_load_on_demand_already_loaded(hass, aiohttp_client): +async def test_load_on_demand_already_loaded(hass, aiohttp_client): """Test getting suites.""" mock_component(hass, "zwave") @@ -26,25 +23,24 @@ def test_load_on_demand_already_loaded(hass, aiohttp_client): ), patch("homeassistant.components.config.zwave.async_setup") as stp: stp.return_value = mock_coro(True) - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert stp.called -@asyncio.coroutine -def test_load_on_demand_on_load(hass, aiohttp_client): +async def test_load_on_demand_on_load(hass, aiohttp_client): """Test getting suites.""" with patch.object(config, "SECTIONS", []), patch.object( config, "ON_DEMAND", ["zwave"] ): - yield from async_setup_component(hass, "config", {}) + await async_setup_component(hass, "config", {}) assert "config.zwave" not in hass.config.components with patch("homeassistant.components.config.zwave.async_setup") as stp: stp.return_value = mock_coro(True) hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: "zwave"}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert stp.called diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index c2490de23ead9b..267c57717f90e0 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -1,5 +1,4 @@ """Test Z-Wave config panel.""" -import asyncio import json from unittest.mock import MagicMock, patch @@ -23,8 +22,7 @@ def client(loop, hass, hass_client): return loop.run_until_complete(hass_client()) -@asyncio.coroutine -def test_get_device_config(client): +async def test_get_device_config(client): """Test getting device config.""" def mock_read(path): @@ -32,16 +30,15 @@ def mock_read(path): return {"hello.beer": {"free": "beer"}, "other.entity": {"do": "something"}} with patch("homeassistant.components.config._read", mock_read): - resp = yield from client.get("/api/config/zwave/device_config/hello.beer") + resp = await client.get("/api/config/zwave/device_config/hello.beer") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"free": "beer"} -@asyncio.coroutine -def test_update_device_config(client): +async def test_update_device_config(client): """Test updating device config.""" orig_data = { "hello.beer": {"ignored": True}, @@ -61,13 +58,13 @@ def mock_write(path, data): with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write ): - resp = yield from client.post( + resp = await client.post( "/api/config/zwave/device_config/hello.beer", data=json.dumps({"polling_intensity": 2}), ) assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"result": "ok"} orig_data["hello.beer"]["polling_intensity"] = 2 @@ -75,10 +72,9 @@ def mock_write(path, data): assert written[0] == orig_data -@asyncio.coroutine -def test_update_device_config_invalid_key(client): +async def test_update_device_config_invalid_key(client): """Test updating device config.""" - resp = yield from client.post( + resp = await client.post( "/api/config/zwave/device_config/invalid_entity", data=json.dumps({"polling_intensity": 2}), ) @@ -86,10 +82,9 @@ def test_update_device_config_invalid_key(client): assert resp.status == 400 -@asyncio.coroutine -def test_update_device_config_invalid_data(client): +async def test_update_device_config_invalid_data(client): """Test updating device config.""" - resp = yield from client.post( + resp = await client.post( "/api/config/zwave/device_config/hello.beer", data=json.dumps({"invalid_option": 2}), ) @@ -97,18 +92,16 @@ def test_update_device_config_invalid_data(client): assert resp.status == 400 -@asyncio.coroutine -def test_update_device_config_invalid_json(client): +async def test_update_device_config_invalid_json(client): """Test updating device config.""" - resp = yield from client.post( + resp = await client.post( "/api/config/zwave/device_config/hello.beer", data="not json" ) assert resp.status == 400 -@asyncio.coroutine -def test_get_values(hass, client): +async def test_get_values(hass, client): """Test getting values on node.""" node = MockNode(node_id=1) value = MockValue( @@ -125,10 +118,10 @@ def test_get_values(hass, client): values2 = MockEntityValues(primary=value2) hass.data[const.DATA_ENTITY_VALUES] = [values, values2] - resp = yield from client.get("/api/zwave/values/1") + resp = await client.get("/api/zwave/values/1") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == { "123456": { @@ -140,8 +133,7 @@ def test_get_values(hass, client): } -@asyncio.coroutine -def test_get_groups(hass, client): +async def test_get_groups(hass, client): """Test getting groupdata on node.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=2) @@ -152,10 +144,10 @@ def test_get_groups(hass, client): node.groups = {1: node.groups} network.nodes = {2: node} - resp = yield from client.get("/api/zwave/groups/2") + resp = await client.get("/api/zwave/groups/2") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == { "1": { @@ -167,38 +159,35 @@ def test_get_groups(hass, client): } -@asyncio.coroutine -def test_get_groups_nogroups(hass, client): +async def test_get_groups_nogroups(hass, client): """Test getting groupdata on node with no groups.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=2) network.nodes = {2: node} - resp = yield from client.get("/api/zwave/groups/2") + resp = await client.get("/api/zwave/groups/2") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {} -@asyncio.coroutine -def test_get_groups_nonode(hass, client): +async def test_get_groups_nonode(hass, client): """Test getting groupdata on nonexisting node.""" network = hass.data[DATA_NETWORK] = MagicMock() network.nodes = {1: 1, 5: 5} - resp = yield from client.get("/api/zwave/groups/2") + resp = await client.get("/api/zwave/groups/2") assert resp.status == 404 - result = yield from resp.json() + result = await resp.json() assert result == {"message": "Node not found"} -@asyncio.coroutine -def test_get_config(hass, client): +async def test_get_config(hass, client): """Test getting config on node.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=2) @@ -214,10 +203,10 @@ def test_get_config(hass, client): network.nodes = {2: node} node.get_values.return_value = node.values - resp = yield from client.get("/api/zwave/config/2") + resp = await client.get("/api/zwave/config/2") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == { "12": { @@ -232,8 +221,7 @@ def test_get_config(hass, client): } -@asyncio.coroutine -def test_get_config_noconfig_node(hass, client): +async def test_get_config_noconfig_node(hass, client): """Test getting config on node without config.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=2) @@ -241,44 +229,41 @@ def test_get_config_noconfig_node(hass, client): network.nodes = {2: node} node.get_values.return_value = node.values - resp = yield from client.get("/api/zwave/config/2") + resp = await client.get("/api/zwave/config/2") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {} -@asyncio.coroutine -def test_get_config_nonode(hass, client): +async def test_get_config_nonode(hass, client): """Test getting config on nonexisting node.""" network = hass.data[DATA_NETWORK] = MagicMock() network.nodes = {1: 1, 5: 5} - resp = yield from client.get("/api/zwave/config/2") + resp = await client.get("/api/zwave/config/2") assert resp.status == 404 - result = yield from resp.json() + result = await resp.json() assert result == {"message": "Node not found"} -@asyncio.coroutine -def test_get_usercodes_nonode(hass, client): +async def test_get_usercodes_nonode(hass, client): """Test getting usercodes on nonexisting node.""" network = hass.data[DATA_NETWORK] = MagicMock() network.nodes = {1: 1, 5: 5} - resp = yield from client.get("/api/zwave/usercodes/2") + resp = await client.get("/api/zwave/usercodes/2") assert resp.status == 404 - result = yield from resp.json() + result = await resp.json() assert result == {"message": "Node not found"} -@asyncio.coroutine -def test_get_usercodes(hass, client): +async def test_get_usercodes(hass, client): """Test getting usercodes on node.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_USER_CODE]) @@ -290,16 +275,15 @@ def test_get_usercodes(hass, client): network.nodes = {18: node} node.get_values.return_value = node.values - resp = yield from client.get("/api/zwave/usercodes/18") + resp = await client.get("/api/zwave/usercodes/18") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {"0": {"code": "1234", "label": "label", "length": 4}} -@asyncio.coroutine -def test_get_usercode_nousercode_node(hass, client): +async def test_get_usercode_nousercode_node(hass, client): """Test getting usercodes on node without usercodes.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=18) @@ -307,16 +291,15 @@ def test_get_usercode_nousercode_node(hass, client): network.nodes = {18: node} node.get_values.return_value = node.values - resp = yield from client.get("/api/zwave/usercodes/18") + resp = await client.get("/api/zwave/usercodes/18") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {} -@asyncio.coroutine -def test_get_usercodes_no_genreuser(hass, client): +async def test_get_usercodes_no_genreuser(hass, client): """Test getting usercodes on node missing genre user.""" network = hass.data[DATA_NETWORK] = MagicMock() node = MockNode(node_id=18, command_classes=[const.COMMAND_CLASS_USER_CODE]) @@ -328,33 +311,31 @@ def test_get_usercodes_no_genreuser(hass, client): network.nodes = {18: node} node.get_values.return_value = node.values - resp = yield from client.get("/api/zwave/usercodes/18") + resp = await client.get("/api/zwave/usercodes/18") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert result == {} -@asyncio.coroutine -def test_save_config_no_network(hass, client): +async def test_save_config_no_network(hass, client): """Test saving configuration without network data.""" - resp = yield from client.post("/api/zwave/saveconfig") + resp = await client.post("/api/zwave/saveconfig") assert resp.status == 404 - result = yield from resp.json() + result = await resp.json() assert result == {"message": "No Z-Wave network data found"} -@asyncio.coroutine -def test_save_config(hass, client): +async def test_save_config(hass, client): """Test saving configuration.""" network = hass.data[DATA_NETWORK] = MagicMock() - resp = yield from client.post("/api/zwave/saveconfig") + resp = await client.post("/api/zwave/saveconfig") assert resp.status == 200 - result = yield from resp.json() + result = await resp.json() assert network.write_config.called assert result == {"message": "Z-Wave configuration saved to file."} From 1a2a976be2d796616be09af07c5414144a5d84c7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:17:55 +0100 Subject: [PATCH 2639/3953] Migrate counter tests from coroutine to async/await (#30368) --- tests/components/counter/test_init.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 35512129aedccf..3e85a0808064a5 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -1,6 +1,5 @@ """The tests for the counter component.""" # pylint: disable=protected-access -import asyncio import logging from homeassistant.components.counter import ( @@ -143,8 +142,7 @@ async def test_methods_with_config(hass): assert 15 == int(state.state) -@asyncio.coroutine -def test_initial_state_overrules_restore_state(hass): +async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( hass, (State("counter.test1", "11"), State("counter.test2", "-22")) @@ -152,7 +150,7 @@ def test_initial_state_overrules_restore_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, { @@ -172,8 +170,7 @@ def test_initial_state_overrules_restore_state(hass): assert int(state.state) == 10 -@asyncio.coroutine -def test_restore_state_overrules_initial_state(hass): +async def test_restore_state_overrules_initial_state(hass): """Ensure states are restored on startup.""" attr = {"initial": 6, "minimum": 1, "maximum": 8, "step": 2} @@ -189,7 +186,7 @@ def test_restore_state_overrules_initial_state(hass): hass.state = CoreState.starting - yield from async_setup_component( + await async_setup_component( hass, DOMAIN, {DOMAIN: {"test1": {}, "test2": {CONF_INITIAL: 10}, "test3": {}}} ) @@ -210,12 +207,11 @@ def test_restore_state_overrules_initial_state(hass): assert state.attributes.get("step") == 2 -@asyncio.coroutine -def test_no_initial_state_and_no_restore_state(hass): +async def test_no_initial_state_and_no_restore_state(hass): """Ensure that entity is create without initial and restore feature.""" hass.state = CoreState.starting - yield from async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_STEP: 5}}}) + await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_STEP: 5}}}) state = hass.states.get("counter.test1") assert state From 47aa0043bf0246fb11db0002cd85aa86463b12a6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:18:20 +0100 Subject: [PATCH 2640/3953] Migrate owntracks tests from coroutine to async/await (#30369) --- tests/components/owntracks/test_init.py | 44 ++++++++++--------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/tests/components/owntracks/test_init.py b/tests/components/owntracks/test_init.py index 78ae687e2084ef..e60efb42ee2481 100644 --- a/tests/components/owntracks/test_init.py +++ b/tests/components/owntracks/test_init.py @@ -1,6 +1,4 @@ """Test the owntracks_http platform.""" -import asyncio - import pytest from homeassistant.components import owntracks @@ -55,10 +53,9 @@ def mock_client(hass, aiohttp_client): return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) -@asyncio.coroutine -def test_handle_valid_message(mock_client): +async def test_handle_valid_message(mock_client): """Test that we forward messages correctly to OwnTracks.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", json=LOCATION_MESSAGE, headers={"X-Limit-u": "Paulus", "X-Limit-d": "Pixel"}, @@ -66,14 +63,13 @@ def test_handle_valid_message(mock_client): assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() assert json == [] -@asyncio.coroutine -def test_handle_valid_minimal_message(mock_client): +async def test_handle_valid_minimal_message(mock_client): """Test that we forward messages correctly to OwnTracks.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", json=MINIMAL_LOCATION_MESSAGE, headers={"X-Limit-u": "Paulus", "X-Limit-d": "Pixel"}, @@ -81,14 +77,13 @@ def test_handle_valid_minimal_message(mock_client): assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() assert json == [] -@asyncio.coroutine -def test_handle_value_error(mock_client): +async def test_handle_value_error(mock_client): """Test we don't disclose that this is a valid webhook.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", json="", headers={"X-Limit-u": "Paulus", "X-Limit-d": "Pixel"}, @@ -96,14 +91,13 @@ def test_handle_value_error(mock_client): assert resp.status == 200 - json = yield from resp.text() + json = await resp.text() assert json == "" -@asyncio.coroutine -def test_returns_error_missing_username(mock_client, caplog): +async def test_returns_error_missing_username(mock_client, caplog): """Test that an error is returned when username is missing.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", json=LOCATION_MESSAGE, headers={"X-Limit-d": "Pixel"}, @@ -111,29 +105,27 @@ def test_returns_error_missing_username(mock_client, caplog): # Needs to be 200 or OwnTracks keeps retrying bad packet. assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() assert json == [] assert "No topic or user found" in caplog.text -@asyncio.coroutine -def test_returns_error_incorrect_json(mock_client, caplog): +async def test_returns_error_incorrect_json(mock_client, caplog): """Test that an error is returned when username is missing.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", data="not json", headers={"X-Limit-d": "Pixel"} ) # Needs to be 200 or OwnTracks keeps retrying bad packet. assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() assert json == [] assert "invalid JSON" in caplog.text -@asyncio.coroutine -def test_returns_error_missing_device(mock_client): +async def test_returns_error_missing_device(mock_client): """Test that an error is returned when device name is missing.""" - resp = yield from mock_client.post( + resp = await mock_client.post( "/api/webhook/owntracks_test", json=LOCATION_MESSAGE, headers={"X-Limit-u": "Paulus"}, @@ -141,7 +133,7 @@ def test_returns_error_missing_device(mock_client): assert resp.status == 200 - json = yield from resp.json() + json = await resp.json() assert json == [] From 8814e1eadcda8e7bd4a79a4ac1f34ac98aabd4b0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:18:41 +0100 Subject: [PATCH 2641/3953] Migrate no_ip tests from coroutine to async/await (#30370) --- tests/components/no_ip/test_init.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/components/no_ip/test_init.py b/tests/components/no_ip/test_init.py index 50063ab3fffb6e..c9b8d05906e126 100644 --- a/tests/components/no_ip/test_init.py +++ b/tests/components/no_ip/test_init.py @@ -1,5 +1,4 @@ """Test the NO-IP component.""" -import asyncio from datetime import timedelta import pytest @@ -39,12 +38,11 @@ def setup_no_ip(hass, aioclient_mock): ) -@asyncio.coroutine -def test_setup(hass, aioclient_mock): +async def test_setup(hass, aioclient_mock): """Test setup works if update passes.""" aioclient_mock.get(UPDATE_URL, params={"hostname": DOMAIN}, text="nochg 0.0.0.0") - result = yield from async_setup_component( + result = await async_setup_component( hass, no_ip.DOMAIN, {no_ip.DOMAIN: {"domain": DOMAIN, "username": USERNAME, "password": PASSWORD}}, @@ -53,16 +51,15 @@ def test_setup(hass, aioclient_mock): assert aioclient_mock.call_count == 1 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 2 -@asyncio.coroutine -def test_setup_fails_if_update_fails(hass, aioclient_mock): +async def test_setup_fails_if_update_fails(hass, aioclient_mock): """Test setup fails if first update fails.""" aioclient_mock.get(UPDATE_URL, params={"hostname": DOMAIN}, text="nohost") - result = yield from async_setup_component( + result = await async_setup_component( hass, no_ip.DOMAIN, {no_ip.DOMAIN: {"domain": DOMAIN, "username": USERNAME, "password": PASSWORD}}, @@ -71,12 +68,11 @@ def test_setup_fails_if_update_fails(hass, aioclient_mock): assert aioclient_mock.call_count == 1 -@asyncio.coroutine -def test_setup_fails_if_wrong_auth(hass, aioclient_mock): +async def test_setup_fails_if_wrong_auth(hass, aioclient_mock): """Test setup fails if first update fails through wrong authentication.""" aioclient_mock.get(UPDATE_URL, params={"hostname": DOMAIN}, text="badauth") - result = yield from async_setup_component( + result = await async_setup_component( hass, no_ip.DOMAIN, {no_ip.DOMAIN: {"domain": DOMAIN, "username": USERNAME, "password": PASSWORD}}, From c837f185f71c6cd86a81696591183d49fd1aa2f5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:21:21 +0100 Subject: [PATCH 2642/3953] Migrate camera tests from coroutine to async/await (#30372) --- tests/components/camera/test_init.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 89a19d0458a3f7..4bd13c35ad8be5 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -147,8 +147,7 @@ def test_get_image_fails(self): ).result() -@asyncio.coroutine -def test_snapshot_service(hass, mock_camera): +async def test_snapshot_service(hass, mock_camera): """Test snapshot service.""" mopen = mock_open() @@ -156,7 +155,7 @@ def test_snapshot_service(hass, mock_camera): "homeassistant.components.camera.open", mopen, create=True ), patch.object(hass.config, "is_allowed_path", return_value=True): common.async_snapshot(hass, "/tmp/bla") - yield from hass.async_block_till_done() + await hass.async_block_till_done() mock_write = mopen().write From c5298dc4dcdb78ef5d5116763a317c4bba2f103b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:21:42 +0100 Subject: [PATCH 2643/3953] Migrate cast tests from coroutine to async/await (#30374) --- tests/components/cast/test_media_player.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index fd565ab59d9b6c..93df75db8bab75 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -1,6 +1,5 @@ """The tests for the Cast Media player platform.""" # pylint: disable=protected-access -import asyncio from typing import Optional from unittest.mock import MagicMock, Mock, patch from uuid import UUID @@ -124,23 +123,21 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas return chromecast, entity -@asyncio.coroutine -def test_start_discovery_called_once(hass): +async def test_start_discovery_called_once(hass): """Test pychromecast.start_discovery called exactly once.""" with patch( "homeassistant.components.cast.discovery.pychromecast.start_discovery", return_value=(None, None), ) as start_discovery: - yield from async_setup_cast(hass) + await async_setup_cast(hass) assert start_discovery.call_count == 1 - yield from async_setup_cast(hass) + await async_setup_cast(hass) assert start_discovery.call_count == 1 -@asyncio.coroutine -def test_stop_discovery_called_on_stop(hass): +async def test_stop_discovery_called_on_stop(hass): """Test pychromecast.stop_discovery called on shutdown.""" browser = MagicMock(zc={}) @@ -149,7 +146,7 @@ def test_stop_discovery_called_on_stop(hass): return_value=(None, browser), ) as start_discovery: # start_discovery should be called with empty config - yield from async_setup_cast(hass, {}) + await async_setup_cast(hass, {}) assert start_discovery.call_count == 1 @@ -158,7 +155,7 @@ def test_stop_discovery_called_on_stop(hass): ) as stop_discovery: # stop discovery should be called on shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - yield from hass.async_block_till_done() + await hass.async_block_till_done() stop_discovery.assert_called_once_with(browser) @@ -167,7 +164,7 @@ def test_stop_discovery_called_on_stop(hass): return_value=(None, browser), ) as start_discovery: # start_discovery should be called again on re-startup - yield from async_setup_cast(hass) + await async_setup_cast(hass) assert start_discovery.call_count == 1 From b43b50b6d23e59541088e5dace5ac95481aa385d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:22:08 +0100 Subject: [PATCH 2644/3953] Migrate ffmpeg tests from coroutine to async/await (#30375) --- tests/components/ffmpeg/test_init.py | 57 ++++++++++++---------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index 1b21f07cd3651f..3c6a2fbb92d7c6 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -1,5 +1,4 @@ """The tests for Home Assistant ffmpeg.""" -import asyncio from unittest.mock import MagicMock import homeassistant.components.ffmpeg as ffmpeg @@ -61,14 +60,12 @@ def __init__(self, hass, initial_state=True, entity_id="test.ffmpeg_device"): self.called_restart = False self.called_entities = None - @asyncio.coroutine - def _async_start_ffmpeg(self, entity_ids): + async def _async_start_ffmpeg(self, entity_ids): """Mock start.""" self.called_start = True self.called_entities = entity_ids - @asyncio.coroutine - def _async_stop_ffmpeg(self, entity_ids): + async def _async_stop_ffmpeg(self, entity_ids): """Mock stop.""" self.called_stop = True self.called_entities = entity_ids @@ -102,91 +99,85 @@ def test_setup_component_test_service(self): assert self.hass.services.has_service(ffmpeg.DOMAIN, "restart") -@asyncio.coroutine -def test_setup_component_test_register(hass): +async def test_setup_component_test_register(hass): """Set up ffmpeg component test register.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) hass.bus.async_listen_once = MagicMock() ffmpeg_dev = MockFFmpegDev(hass) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() assert hass.bus.async_listen_once.called assert hass.bus.async_listen_once.call_count == 2 -@asyncio.coroutine -def test_setup_component_test_register_no_startup(hass): +async def test_setup_component_test_register_no_startup(hass): """Set up ffmpeg component test register without startup.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) hass.bus.async_listen_once = MagicMock() ffmpeg_dev = MockFFmpegDev(hass, False) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() assert hass.bus.async_listen_once.called assert hass.bus.async_listen_once.call_count == 1 -@asyncio.coroutine -def test_setup_component_test_service_start(hass): +async def test_setup_component_test_service_start(hass): """Set up ffmpeg component test service start.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) ffmpeg_dev = MockFFmpegDev(hass, False) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() async_start(hass) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert ffmpeg_dev.called_start -@asyncio.coroutine -def test_setup_component_test_service_stop(hass): +async def test_setup_component_test_service_stop(hass): """Set up ffmpeg component test service stop.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) ffmpeg_dev = MockFFmpegDev(hass, False) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() async_stop(hass) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert ffmpeg_dev.called_stop -@asyncio.coroutine -def test_setup_component_test_service_restart(hass): +async def test_setup_component_test_service_restart(hass): """Set up ffmpeg component test service restart.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) ffmpeg_dev = MockFFmpegDev(hass, False) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() async_restart(hass) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert ffmpeg_dev.called_stop assert ffmpeg_dev.called_start -@asyncio.coroutine -def test_setup_component_test_service_start_with_entity(hass): +async def test_setup_component_test_service_start_with_entity(hass): """Set up ffmpeg component test service start.""" with assert_setup_component(1): - yield from async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) ffmpeg_dev = MockFFmpegDev(hass, False) - yield from ffmpeg_dev.async_added_to_hass() + await ffmpeg_dev.async_added_to_hass() async_start(hass, "test.ffmpeg_device") - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert ffmpeg_dev.called_start assert ffmpeg_dev.called_entities == ["test.ffmpeg_device"] From bcb47dab454fae19d7d1c8a29f25042c9685bba0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:22:32 +0100 Subject: [PATCH 2645/3953] Migrate discovery tests from coroutine to async/await (#30376) --- tests/components/discovery/test_init.py | 28 +++++++++---------------- 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index de63a0bf495a22..1d11bba9e16e8c 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -1,5 +1,4 @@ """The tests for the discovery component.""" -import asyncio from unittest.mock import MagicMock, patch import pytest @@ -55,29 +54,27 @@ async def mock_discovery(hass, discoveries, config=BASE_CONFIG): return mock_discover, mock_platform -@asyncio.coroutine -def test_unknown_service(hass): +async def test_unknown_service(hass): """Test that unknown service is ignored.""" def discover(netdisco): """Fake discovery.""" return [("this_service_will_never_be_supported", {"info": "some"})] - mock_discover, mock_platform = yield from mock_discovery(hass, discover) + mock_discover, mock_platform = await mock_discovery(hass, discover) assert not mock_discover.called assert not mock_platform.called -@asyncio.coroutine -def test_load_platform(hass): +async def test_load_platform(hass): """Test load a platform.""" def discover(netdisco): """Fake discovery.""" return [(SERVICE, SERVICE_INFO)] - mock_discover, mock_platform = yield from mock_discovery(hass, discover) + mock_discover, mock_platform = await mock_discovery(hass, discover) assert not mock_discover.called assert mock_platform.called @@ -86,15 +83,14 @@ def discover(netdisco): ) -@asyncio.coroutine -def test_load_component(hass): +async def test_load_component(hass): """Test load a component.""" def discover(netdisco): """Fake discovery.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] - mock_discover, mock_platform = yield from mock_discovery(hass, discover) + mock_discover, mock_platform = await mock_discovery(hass, discover) assert mock_discover.called assert not mock_platform.called @@ -107,24 +103,20 @@ def discover(netdisco): ) -@asyncio.coroutine -def test_ignore_service(hass): +async def test_ignore_service(hass): """Test ignore service.""" def discover(netdisco): """Fake discovery.""" return [(SERVICE_NO_PLATFORM, SERVICE_INFO)] - mock_discover, mock_platform = yield from mock_discovery( - hass, discover, IGNORE_CONFIG - ) + mock_discover, mock_platform = await mock_discovery(hass, discover, IGNORE_CONFIG) assert not mock_discover.called assert not mock_platform.called -@asyncio.coroutine -def test_discover_duplicates(hass): +async def test_discover_duplicates(hass): """Test load a component.""" def discover(netdisco): @@ -134,7 +126,7 @@ def discover(netdisco): (SERVICE_NO_PLATFORM, SERVICE_INFO), ] - mock_discover, mock_platform = yield from mock_discovery(hass, discover) + mock_discover, mock_platform = await mock_discovery(hass, discover) assert mock_discover.called assert mock_discover.call_count == 1 From 37d1771400c4ab8c23eace4532a48030995323e5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 00:24:30 +0100 Subject: [PATCH 2646/3953] Migrate google_* tests from coroutine to async/await (#30377) --- .../google_assistant/test_google_assistant.py | 36 ++++++++----------- .../components/google_assistant/test_init.py | 9 ++--- tests/components/google_domains/test_init.py | 13 +++---- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index d1d584228843aa..3be97013e4de0d 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -1,6 +1,5 @@ """The tests for the Google Assistant component.""" # pylint: disable=protected-access -import asyncio import json from aiohttp.hdrs import AUTHORIZATION @@ -115,18 +114,17 @@ def hass_fixture(loop, hass): # pylint: disable=redefined-outer-name -@asyncio.coroutine -def test_sync_request(hass_fixture, assistant_client, auth_header): +async def test_sync_request(hass_fixture, assistant_client, auth_header): """Test a sync request.""" reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} - result = yield from assistant_client.post( + result = await assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), headers=auth_header, ) assert result.status == 200 - body = yield from result.json() + body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] assert sorted([dev["id"] for dev in devices]) == sorted( @@ -145,8 +143,7 @@ def test_sync_request(hass_fixture, assistant_client, auth_header): assert dev["type"] == demo["type"] -@asyncio.coroutine -def test_query_request(hass_fixture, assistant_client, auth_header): +async def test_query_request(hass_fixture, assistant_client, auth_header): """Test a query request.""" reqid = "5711642932632160984" data = { @@ -165,13 +162,13 @@ def test_query_request(hass_fixture, assistant_client, auth_header): } ], } - result = yield from assistant_client.post( + result = await assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), headers=auth_header, ) assert result.status == 200 - body = yield from result.json() + body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] assert len(devices) == 4 @@ -187,8 +184,7 @@ def test_query_request(hass_fixture, assistant_client, auth_header): assert devices["media_player.lounge_room"]["on"] is True -@asyncio.coroutine -def test_query_climate_request(hass_fixture, assistant_client, auth_header): +async def test_query_climate_request(hass_fixture, assistant_client, auth_header): """Test a query request.""" reqid = "5711642932632160984" data = { @@ -206,13 +202,13 @@ def test_query_climate_request(hass_fixture, assistant_client, auth_header): } ], } - result = yield from assistant_client.post( + result = await assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), headers=auth_header, ) assert result.status == 200 - body = yield from result.json() + body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] assert len(devices) == 3 @@ -238,8 +234,7 @@ def test_query_climate_request(hass_fixture, assistant_client, auth_header): } -@asyncio.coroutine -def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): +async def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): """Test a query request.""" # Mock demo devices as fahrenheit to see if we convert to celsius hass_fixture.config.units.temperature_unit = const.TEMP_FAHRENHEIT @@ -264,13 +259,13 @@ def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): } ], } - result = yield from assistant_client.post( + result = await assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), headers=auth_header, ) assert result.status == 200 - body = yield from result.json() + body = await result.json() assert body.get("requestId") == reqid devices = body["payload"]["devices"] assert len(devices) == 3 @@ -297,8 +292,7 @@ def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): hass_fixture.config.units.temperature_unit = const.TEMP_CELSIUS -@asyncio.coroutine -def test_execute_request(hass_fixture, assistant_client, auth_header): +async def test_execute_request(hass_fixture, assistant_client, auth_header): """Test an execute request.""" reqid = "5711642932632160985" data = { @@ -357,13 +351,13 @@ def test_execute_request(hass_fixture, assistant_client, auth_header): } ], } - result = yield from assistant_client.post( + result = await assistant_client.post( ga.const.GOOGLE_ASSISTANT_API_ENDPOINT, data=json.dumps(data), headers=auth_header, ) assert result.status == 200 - body = yield from result.json() + body = await result.json() assert body.get("requestId") == reqid commands = body["payload"]["commands"] assert len(commands) == 6 diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index 7c5d14ae6d7aaa..2773f3c3329677 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -1,6 +1,4 @@ """The tests for google-assistant init.""" -import asyncio - from homeassistant.components import google_assistant as ga from homeassistant.core import Context from homeassistant.setup import async_setup_component @@ -8,19 +6,18 @@ GA_API_KEY = "Agdgjsj399sdfkosd932ksd" -@asyncio.coroutine -def test_request_sync_service(aioclient_mock, hass): +async def test_request_sync_service(aioclient_mock, hass): """Test that it posts to the request_sync url.""" aioclient_mock.post(ga.const.REQUEST_SYNC_BASE_URL, status=200) - yield from async_setup_component( + await async_setup_component( hass, "google_assistant", {"google_assistant": {"project_id": "test_project", "api_key": GA_API_KEY}}, ) assert aioclient_mock.call_count == 0 - yield from hass.services.async_call( + await hass.services.async_call( ga.const.DOMAIN, ga.const.SERVICE_REQUEST_SYNC, blocking=True, diff --git a/tests/components/google_domains/test_init.py b/tests/components/google_domains/test_init.py index 80844063b00734..66e334d342fc84 100644 --- a/tests/components/google_domains/test_init.py +++ b/tests/components/google_domains/test_init.py @@ -1,5 +1,4 @@ """Test the Google Domains component.""" -import asyncio from datetime import timedelta import pytest @@ -37,12 +36,11 @@ def setup_google_domains(hass, aioclient_mock): ) -@asyncio.coroutine -def test_setup(hass, aioclient_mock): +async def test_setup(hass, aioclient_mock): """Test setup works if update passes.""" aioclient_mock.get(UPDATE_URL, params={"hostname": DOMAIN}, text="nochg 0.0.0.0") - result = yield from async_setup_component( + result = await async_setup_component( hass, google_domains.DOMAIN, { @@ -57,16 +55,15 @@ def test_setup(hass, aioclient_mock): assert aioclient_mock.call_count == 1 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 2 -@asyncio.coroutine -def test_setup_fails_if_update_fails(hass, aioclient_mock): +async def test_setup_fails_if_update_fails(hass, aioclient_mock): """Test setup fails if first update fails.""" aioclient_mock.get(UPDATE_URL, params={"hostname": DOMAIN}, text="nohost") - result = yield from async_setup_component( + result = await async_setup_component( hass, google_domains.DOMAIN, { From fc08c62a31d29a6cff73b038b03a1e2d664bcacd Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 1 Jan 2020 15:36:14 -0800 Subject: [PATCH 2647/3953] Add charging rate sensor to Tesla (#30286) * Add charging rate sensor to Tesla * Remove reference to bin_type --- homeassistant/components/tesla/sensor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index 78e1106ed43b51..a282f65f9e1e4d 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -88,6 +88,16 @@ async def async_update(self): self.current_value = round( convert(self.current_value, LENGTH_MILES, LENGTH_KILOMETERS), 2 ) + elif self.tesla_device.type == "charging rate sensor": + self.current_value = self.tesla_device.charging_rate + self.units = units + self._attributes = { + "time_left": self.tesla_device.time_left, + "added_range": self.tesla_device.added_range, + "charge_current_request": self.tesla_device.charge_current_request, + "charger_actual_current": self.tesla_device.charger_actual_current, + "charger_voltage": self.tesla_device.charger_voltage, + } else: self.current_value = self.tesla_device.get_value() self.units = units From 769cf19052f8c9ef374d8ba8ae7705ccc7bf4cf4 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 2 Jan 2020 00:32:24 +0000 Subject: [PATCH 2648/3953] [ci skip] Translation update --- .../components/gios/.translations/ca.json | 20 +++++++++++++++++++ .../components/gios/.translations/da.json | 20 +++++++++++++++++++ .../components/gios/.translations/ko.json | 20 +++++++++++++++++++ .../components/local_ip/.translations/ca.json | 16 +++++++++++++++ .../components/local_ip/.translations/da.json | 16 +++++++++++++++ .../components/local_ip/.translations/ko.json | 16 +++++++++++++++ 6 files changed, 108 insertions(+) create mode 100644 homeassistant/components/gios/.translations/ca.json create mode 100644 homeassistant/components/gios/.translations/da.json create mode 100644 homeassistant/components/gios/.translations/ko.json create mode 100644 homeassistant/components/local_ip/.translations/ca.json create mode 100644 homeassistant/components/local_ip/.translations/da.json create mode 100644 homeassistant/components/local_ip/.translations/ko.json diff --git a/homeassistant/components/gios/.translations/ca.json b/homeassistant/components/gios/.translations/ca.json new file mode 100644 index 00000000000000..80fedcafdd97d9 --- /dev/null +++ b/homeassistant/components/gios/.translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "No s'ha pogut connectar al servidor de GIO\u015a.", + "invalid_sensors_data": "Les dades dels sensors d'aquesta estaci\u00f3 de mesura s\u00f3n inv\u00e0lides.", + "wrong_station_id": "L'ID de l'estaci\u00f3 de mesura \u00e9s incorrecte." + }, + "step": { + "user": { + "data": { + "name": "Nom de la integraci\u00f3", + "station_id": "ID de l'estaci\u00f3 de mesura" + }, + "description": "Integraci\u00f3 de mesura de qualitat de l\u2019aire GIO\u015a (Polish Chief Inspectorate Of Environmental Protection). Si necessites ajuda amb la configuraci\u00f3, fes un cop d'ull a: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/da.json b/homeassistant/components/gios/.translations/da.json new file mode 100644 index 00000000000000..b4855da7951684 --- /dev/null +++ b/homeassistant/components/gios/.translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan ikke oprette forbindelse til GIO\u015a-serveren.", + "invalid_sensors_data": "Ugyldige sensordata for denne m\u00e5lestation.", + "wrong_station_id": "M\u00e5lestationens ID er ikke korrekt." + }, + "step": { + "user": { + "data": { + "name": "Navn p\u00e5 integrationen", + "station_id": "ID for m\u00e5lestationen" + }, + "description": "Ops\u00e6t GIO\u015a (polsk inspektorat for milj\u00f8beskyttelse) luftkvalitet-integration. Hvis du har brug for hj\u00e6lp med konfigurationen, kig her: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/ko.json b/homeassistant/components/gios/.translations/ko.json new file mode 100644 index 00000000000000..6fb37205502120 --- /dev/null +++ b/homeassistant/components/gios/.translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "GIO\u015a \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "invalid_sensors_data": "\uc774 \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc5d0 \ub300\ud55c \uc13c\uc11c \ub370\uc774\ud130\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "wrong_station_id": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc758 ID \uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984", + "station_id": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc758 ID" + }, + "description": "GIO\u015a (\ud3f4\ub780\ub4dc \ud658\uacbd \ubcf4\ud638\uccad) \ub300\uae30\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. \uad6c\uc131\uc5d0 \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 https://www.home-assistant.io/integrations/gios \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "title": "\ud3f4\ub780\ub4dc \ud658\uacbd\uccad (GIO\u015a)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/ca.json b/homeassistant/components/local_ip/.translations/ca.json new file mode 100644 index 00000000000000..b2b7ee89c16c66 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integraci\u00f3 ja configurada amb un sensor amb aquest nom" + }, + "step": { + "user": { + "data": { + "name": "Nom del sensor" + }, + "title": "Adre\u00e7a IP local" + } + }, + "title": "Adre\u00e7a IP local" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/da.json b/homeassistant/components/local_ip/.translations/da.json new file mode 100644 index 00000000000000..c0396ccb182fe7 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integration er allerede konfigureret med en eksisterende sensor med det navn" + }, + "step": { + "user": { + "data": { + "name": "Sensornavn" + }, + "title": "Lokal IP-adresse" + } + }, + "title": "Lokal IP-adresse" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/ko.json b/homeassistant/components/local_ip/.translations/ko.json new file mode 100644 index 00000000000000..a00a130bfcaf12 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc774\ubbf8 \ud574\ub2f9 \uc774\ub984\uc758 \uc13c\uc11c\ub85c \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "name": "\uc13c\uc11c \uc774\ub984" + }, + "title": "\ub85c\uceec IP \uc8fc\uc18c" + } + }, + "title": "\ub85c\uceec IP \uc8fc\uc18c" + } +} \ No newline at end of file From 77978a979be435539c81b591a633d3663c01b83c Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 2 Jan 2020 07:59:13 +0100 Subject: [PATCH 2649/3953] Restore state for Rfxtrx devices (#30309) * Restore state rfxtrx switch * Restore state RFXtrx lights * Restore state RFXtrx covers * Restore comment * Remove line * Remove logging * fix black * Fix typo --- homeassistant/components/rfxtrx/cover.py | 13 +++++++++-- homeassistant/components/rfxtrx/light.py | 28 +++++++++++++++++++++-- homeassistant/components/rfxtrx/switch.py | 13 +++++++++-- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 4806fd9a6b7107..e1eb6ae77f5ecd 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -3,8 +3,9 @@ import voluptuous as vol from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, STATE_OPEN from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -62,9 +63,17 @@ def cover_update(event): RECEIVED_EVT_SUBSCRIBERS.append(cover_update) -class RfxtrxCover(RfxtrxDevice, CoverDevice): +class RfxtrxCover(RfxtrxDevice, CoverDevice, RestoreEntity): """Representation of a RFXtrx cover.""" + async def async_added_to_hass(self): + """Restore RFXtrx cover device state (OPEN/CLOSE).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_OPEN + @property def should_poll(self): """Return the polling state. No polling available in RFXtrx cover.""" diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index a29b8bfa6605bd..9c0157870cbc49 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -10,8 +10,9 @@ SUPPORT_BRIGHTNESS, Light, ) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, STATE_ON from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -72,14 +73,37 @@ def light_update(event): RECEIVED_EVT_SUBSCRIBERS.append(light_update) -class RfxtrxLight(RfxtrxDevice, Light): +class RfxtrxLight(RfxtrxDevice, Light, RestoreEntity): """Representation of a RFXtrx light.""" + async def async_added_to_hass(self): + """Restore RFXtrx device state (ON/OFF).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON + + # Restore the brightness of dimmable devices + if ( + old_state is not None + and old_state.attributes.get(ATTR_BRIGHTNESS) is not None + ): + self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS]) + @property def brightness(self): """Return the brightness of this light between 0..255.""" return self._brightness + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attr = {} + if self._brightness is not None: + attr[ATTR_BRIGHTNESS] = self._brightness + return attr + @property def supported_features(self): """Flag supported features.""" diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 49bcd1b3924527..05e4a37ab4427e 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -5,8 +5,9 @@ import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, STATE_ON from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -67,9 +68,17 @@ def switch_update(event): RECEIVED_EVT_SUBSCRIBERS.append(switch_update) -class RfxtrxSwitch(RfxtrxDevice, SwitchDevice): +class RfxtrxSwitch(RfxtrxDevice, SwitchDevice, RestoreEntity): """Representation of a RFXtrx switch.""" + async def async_added_to_hass(self): + """Restore RFXtrx switch device state (ON/OFF).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON + def turn_on(self, **kwargs): """Turn the device on.""" self._send_command("turn_on") From cac750066ae5ccaf906a4fbf1c45c4756bcefca5 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Thu, 2 Jan 2020 16:44:29 +0100 Subject: [PATCH 2650/3953] Remove unnessecary rfxtrx light property def (#30397) --- homeassistant/components/rfxtrx/light.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 9c0157870cbc49..437cce89c49dba 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -96,14 +96,6 @@ def brightness(self): """Return the brightness of this light between 0..255.""" return self._brightness - @property - def device_state_attributes(self): - """Return the device state attributes.""" - attr = {} - if self._brightness is not None: - attr[ATTR_BRIGHTNESS] = self._brightness - return attr - @property def supported_features(self): """Flag supported features.""" From e6388e186ce97b48853f1f196193018cc4c4e109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 2 Jan 2020 21:17:10 +0200 Subject: [PATCH 2651/3953] Remove unnecessary string literal concatenations (#30360) --- homeassistant/__main__.py | 8 +-- homeassistant/auth/providers/command_line.py | 2 +- homeassistant/bootstrap.py | 4 +- homeassistant/components/alexa/auth.py | 2 +- .../components/arest/binary_sensor.py | 2 +- homeassistant/components/arest/sensor.py | 2 +- homeassistant/components/arest/switch.py | 2 +- homeassistant/components/august/__init__.py | 8 +-- .../components/aurora/binary_sensor.py | 2 +- homeassistant/components/auth/indieauth.py | 2 +- .../components/automatic/device_tracker.py | 2 +- .../components/automation/__init__.py | 2 +- homeassistant/components/buienradar/camera.py | 2 +- homeassistant/components/buienradar/util.py | 4 +- homeassistant/components/camera/__init__.py | 2 +- .../cisco_mobility_express/device_tracker.py | 2 +- .../components/cisco_webex_teams/notify.py | 2 +- homeassistant/components/citybikes/sensor.py | 6 +- homeassistant/components/cloud/http_api.py | 2 +- homeassistant/components/coinbase/__init__.py | 2 +- homeassistant/components/decora/light.py | 3 +- homeassistant/components/demo/media_player.py | 2 +- .../components/directv/media_player.py | 4 +- homeassistant/components/dyson/__init__.py | 2 +- .../components/ecoal_boiler/__init__.py | 2 +- homeassistant/components/ecobee/climate.py | 2 +- .../components/egardia/alarm_control_panel.py | 2 +- homeassistant/components/esphome/__init__.py | 2 +- .../components/feedreader/__init__.py | 2 +- homeassistant/components/fibaro/__init__.py | 6 +- homeassistant/components/flux/switch.py | 2 +- .../components/foursquare/__init__.py | 2 +- .../components/fritz/device_tracker.py | 2 +- .../components/generic_thermostat/climate.py | 2 +- homeassistant/components/harmony/remote.py | 2 +- .../components/haveibeenpwned/sensor.py | 2 +- .../components/history_stats/sensor.py | 6 +- homeassistant/components/homekit/__init__.py | 2 +- .../components/homekit/type_media_players.py | 2 +- homeassistant/components/homekit/util.py | 2 +- homeassistant/components/html5/notify.py | 6 +- homeassistant/components/http/ban.py | 4 +- homeassistant/components/http/view.py | 8 ++- homeassistant/components/hue/__init__.py | 4 +- homeassistant/components/hue/light.py | 2 +- homeassistant/components/imap/sensor.py | 2 +- homeassistant/components/influxdb/sensor.py | 5 +- .../components/input_datetime/__init__.py | 2 +- homeassistant/components/ios/notify.py | 6 +- .../components/isy994/binary_sensor.py | 6 +- homeassistant/components/knx/__init__.py | 2 +- homeassistant/components/kodi/media_player.py | 2 +- homeassistant/components/lametric/notify.py | 2 +- homeassistant/components/lcn/helpers.py | 2 +- homeassistant/components/lcn/services.py | 4 +- .../components/logi_circle/config_flow.py | 2 +- homeassistant/components/min_max/sensor.py | 2 +- homeassistant/components/mobile_app/notify.py | 6 +- homeassistant/components/modbus/sensor.py | 4 +- homeassistant/components/modbus/switch.py | 2 +- .../components/mold_indicator/sensor.py | 10 ++-- homeassistant/components/mqtt/__init__.py | 8 +-- homeassistant/components/nest/__init__.py | 2 +- .../components/nissan_leaf/__init__.py | 6 +- homeassistant/components/nut/sensor.py | 2 +- homeassistant/components/ohmconnect/sensor.py | 4 +- homeassistant/components/opensky/sensor.py | 2 +- .../components/openweathermap/weather.py | 2 +- homeassistant/components/owlet/__init__.py | 2 +- .../components/owntracks/__init__.py | 2 +- homeassistant/components/prowl/notify.py | 2 +- .../components/proximity/__init__.py | 2 +- .../components/python_script/__init__.py | 2 +- .../quantum_gateway/device_tracker.py | 2 +- homeassistant/components/recorder/__init__.py | 2 +- .../components/remember_the_milk/__init__.py | 10 ++-- .../components/satel_integra/__init__.py | 4 +- .../components/satel_integra/switch.py | 4 +- homeassistant/components/slide/__init__.py | 5 +- .../components/smartthings/smartapp.py | 2 +- .../components/spotify/media_player.py | 2 +- .../swiss_hydrological_data/sensor.py | 2 +- .../components/tado/device_tracker.py | 4 +- .../components/telegram_bot/__init__.py | 2 +- .../components/template/binary_sensor.py | 4 +- homeassistant/components/template/cover.py | 5 +- homeassistant/components/template/light.py | 2 +- homeassistant/components/template/sensor.py | 4 +- homeassistant/components/template/switch.py | 2 +- .../components/tomato/device_tracker.py | 2 +- .../totalconnect/alarm_control_panel.py | 2 +- homeassistant/components/uscis/sensor.py | 2 +- homeassistant/components/wink/__init__.py | 5 +- .../components/wirelesstag/__init__.py | 2 +- .../components/wunderground/sensor.py | 2 +- .../components/xfinity/device_tracker.py | 4 +- homeassistant/components/xiaomi_miio/light.py | 12 ++-- .../components/xiaomi_miio/vacuum.py | 2 +- homeassistant/components/xmpp/notify.py | 2 +- homeassistant/components/xs1/__init__.py | 3 +- homeassistant/components/yeelight/light.py | 4 +- homeassistant/components/yr/sensor.py | 2 +- homeassistant/components/zigbee/__init__.py | 2 +- homeassistant/components/zwave/__init__.py | 16 ++--- homeassistant/components/zwave/lock.py | 4 +- homeassistant/config.py | 2 +- homeassistant/helpers/condition.py | 2 +- homeassistant/helpers/config_validation.py | 2 +- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/template.py | 2 +- homeassistant/loader.py | 2 +- homeassistant/scripts/ensure_config.py | 4 +- homeassistant/setup.py | 6 +- homeassistant/util/dt.py | 2 +- homeassistant/util/yaml/loader.py | 4 +- script/hassfest/codeowners.py | 2 +- script/hassfest/ssdp.py | 2 +- script/hassfest/zeroconf.py | 2 +- tests/components/aprs/test_device_tracker.py | 2 +- tests/components/automation/test_time.py | 2 +- tests/components/binary_sensor/test_init.py | 5 +- tests/components/buienradar/test_camera.py | 2 +- tests/components/camera/test_init.py | 2 +- tests/components/cloud/test_http_api.py | 6 +- tests/components/demo/test_media_player.py | 2 +- tests/components/device_tracker/test_init.py | 4 +- tests/components/emulated_hue/test_hue_api.py | 2 +- tests/components/emulated_hue/test_init.py | 6 +- tests/components/emulated_hue/test_upnp.py | 2 +- .../facebox/test_image_processing.py | 2 +- tests/components/feedreader/test_init.py | 10 ++-- tests/components/filter/test_sensor.py | 8 +-- tests/components/hassio/conftest.py | 2 +- tests/components/hassio/test_discovery.py | 8 +-- .../here_travel_time/test_sensor.py | 2 +- tests/components/history_stats/test_sensor.py | 6 +- tests/components/homekit/test_accessories.py | 6 +- tests/components/honeywell/test_climate.py | 12 ++-- .../ign_sismologia/test_geo_location.py | 4 +- .../islamic_prayer_times/test_sensor.py | 2 +- tests/components/logentries/test_init.py | 2 +- tests/components/mailbox/test_init.py | 6 +- .../manual/test_alarm_control_panel.py | 60 +++++++++---------- tests/components/microsoft_face/test_init.py | 14 ++--- .../test_image_processing.py | 4 +- .../test_image_processing.py | 4 +- tests/components/mqtt/test_binary_sensor.py | 4 +- tests/components/mqtt/test_climate.py | 2 +- tests/components/mqtt/test_device_tracker.py | 2 +- tests/components/mqtt/test_discovery.py | 8 +-- tests/components/mqtt/test_light.py | 2 +- tests/components/mqtt/test_light_json.py | 2 +- tests/components/mqtt/test_light_template.py | 2 +- tests/components/mqtt/test_sensor.py | 4 +- tests/components/mqtt/test_switch.py | 2 +- .../mqtt_json/test_device_tracker.py | 2 +- .../components/owntracks/test_config_flow.py | 4 +- .../owntracks/test_device_tracker.py | 6 +- .../qld_bushfire/test_geo_location.py | 4 +- tests/components/radarr/test_sensor.py | 2 +- tests/components/rflink/test_binary_sensor.py | 8 +-- tests/components/shell_command/test_init.py | 2 +- tests/components/shopping_list/conftest.py | 2 +- tests/components/startca/test_sensor.py | 10 ++-- .../components/tomato/test_device_tracker.py | 6 +- tests/components/tplink/test_init.py | 2 +- tests/components/tradfri/conftest.py | 2 +- tests/components/tradfri/test_config_flow.py | 4 +- tests/components/tts/test_init.py | 20 +++---- tests/components/websocket_api/test_auth.py | 2 +- tests/components/wunderground/test_sensor.py | 6 +- tests/components/yr/test_sensor.py | 6 +- tests/components/zha/test_config_flow.py | 4 +- tests/conftest.py | 2 +- tests/helpers/test_check_config.py | 5 +- tests/helpers/test_device_registry.py | 2 +- tests/helpers/test_entity_component.py | 6 +- tests/helpers/test_entity_platform.py | 2 +- tests/helpers/test_event.py | 4 +- tests/helpers/test_service.py | 2 +- tests/helpers/test_template.py | 22 +++---- tests/scripts/test_check_config.py | 9 ++- tests/util/test_yaml.py | 8 +-- 183 files changed, 362 insertions(+), 393 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index bcc972522556ec..a0243e2dd8c8c2 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -78,11 +78,7 @@ def ensure_config_path(config_dir: str) -> None: try: os.mkdir(lib_dir) except OSError: - print( - ("Fatal Error: Unable to create library " "directory {} ").format( - lib_dir - ) - ) + print("Fatal Error: Unable to create library directory {}".format(lib_dir)) sys.exit(1) @@ -147,7 +143,7 @@ def get_arguments() -> argparse.Namespace: "--log-file", type=str, default=None, - help="Log file to write to. If not set, CONFIG/home-assistant.log " "is used", + help="Log file to write to. If not set, CONFIG/home-assistant.log is used", ) parser.add_argument( "--log-no-color", action="store_true", help="Disable color logs" diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 203bc191193d6b..12e27c015049a7 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -76,7 +76,7 @@ async def async_validate_login(self, username: str, password: str) -> None: if process.returncode != 0: _LOGGER.error( - "User %r failed to authenticate, command exited " "with code %d.", + "User %r failed to authenticate, command exited with code %d.", username, process.returncode, ) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 12fbc6f232f0ed..48ca96c7254047 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -66,7 +66,7 @@ async def async_from_config_dict( hass.config.skip_pip = skip_pip if skip_pip: _LOGGER.warning( - "Skipping pip installation of required modules. " "This may cause issues" + "Skipping pip installation of required modules. This may cause issues" ) core_config = config.get(core.DOMAIN, {}) @@ -168,7 +168,7 @@ def async_enable_logging( This method must be run in the event loop. """ - fmt = "%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s" + fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s" datefmt = "%Y-%m-%d %H:%M:%S" if not log_no_color: diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 33c25b73d7e73b..94789c33305fab 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -51,7 +51,7 @@ async def async_do_auth(self, accept_grant_code): "client_secret": self.client_secret, } _LOGGER.debug( - "Calling LWA to get the access token (first time), " "with: %s", + "Calling LWA to get the access token (first time), with: %s", json.dumps(lwa_params), ) diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index caabe3333f8994..3bd0a85c6f0110 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): response = requests.get(resource, timeout=10).json() except requests.exceptions.MissingSchema: _LOGGER.error( - "Missing resource or schema in configuration. " "Add http:// to your URL" + "Missing resource or schema in configuration. Add http:// to your URL" ) return False except requests.exceptions.ConnectionError: diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 270a3cda269f6e..2533ce3619ed2a 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): response = requests.get(resource, timeout=10).json() except requests.exceptions.MissingSchema: _LOGGER.error( - "Missing resource or schema in configuration. " "Add http:// to your URL" + "Missing resource or schema in configuration. Add http:// to your URL" ) return False except requests.exceptions.ConnectionError: diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index b3db6684cf2a12..ccc2c5d8bf58e2 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): response = requests.get(resource, timeout=10) except requests.exceptions.MissingSchema: _LOGGER.error( - "Missing resource or schema in configuration. " "Add http:// to your URL" + "Missing resource or schema in configuration. Add http:// to your URL" ) return False except requests.exceptions.ConnectionError: diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 468e6e429a7e93..8cbe41dac9e481 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -254,7 +254,7 @@ def _update_doorbells(self): ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve doorbell" " status for %s. %s", + "Request error trying to retrieve doorbell status for %s. %s", doorbell.device_name, ex, ) @@ -301,7 +301,7 @@ def _update_doors(self): ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve door" " status for %s. %s", + "Request error trying to retrieve door status for %s. %s", lock.device_name, ex, ) @@ -327,7 +327,7 @@ def _update_locks(self): ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve door" " status for %s. %s", + "Request error trying to retrieve door status for %s. %s", lock.device_name, ex, ) @@ -342,7 +342,7 @@ def _update_locks(self): ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve door" " details for %s. %s", + "Request error trying to retrieve door details for %s. %s", lock.device_name, ex, ) diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index d76884d289573f..454c3ad2405b5d 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric " "Administration" +ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric Administration" CONF_THRESHOLD = "forecast_threshold" DEFAULT_DEVICE_CLASS = "visible" diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 5915a4ec3013d2..3266ae65d7a491 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -99,7 +99,7 @@ async def fetch_redirect_uris(hass, url): pass except aiohttp.client_exceptions.ClientConnectionError: _LOGGER.error( - ("Low level connection error while looking up " "redirect_uri %s"), url + "Low level connection error while looking up redirect_uri %s", url ) pass except aiohttp.client_exceptions.ClientError: diff --git a/homeassistant/components/automatic/device_tracker.py b/homeassistant/components/automatic/device_tracker.py index bb403687963086..3c9e33cdc844ac 100644 --- a/homeassistant/components/automatic/device_tracker.py +++ b/homeassistant/components/automatic/device_tracker.py @@ -232,7 +232,7 @@ def handle_event(self, name, event): if event.created_at < self.vehicle_seen[event.vehicle.id]: # Skip events received out of order _LOGGER.debug( - "Skipping out of order event. Event Created %s. " "Last seen event: %s", + "Skipping out of order event. Event Created %s. Last seen event: %s", event.created_at, self.vehicle_seen[event.vehicle.id], ) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 4441b0285658c5..671d7bd3d5b77e 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -275,7 +275,7 @@ async def async_added_to_hass(self) -> None: else: enable_automation = DEFAULT_INITIAL_STATE _LOGGER.debug( - "Automation %s not in state storage, state %s from " "default is used.", + "Automation %s not in state storage, state %s from default is used.", self.entity_id, enable_automation, ) diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 3d30e330bc9ef4..c1ef11c1d0df48 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -16,7 +16,7 @@ CONF_DIMENSION = "dimension" CONF_DELTA = "delta" -RADAR_MAP_URL_TEMPLATE = "https://api.buienradar.nl/image/1.0/" "RadarMapNL?w={w}&h={h}" +RADAR_MAP_URL_TEMPLATE = "https://api.buienradar.nl/image/1.0/RadarMapNL?w={w}&h={h}" _LOG = logging.getLogger(__name__) diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 2ef0713713bbdf..37c518cef7a3cc 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -115,7 +115,7 @@ async def async_update(self, *_): if raincontent.get(SUCCESS) is not True: # unable to get the data _LOGGER.warning( - "Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)", + "Unable to retrieve raindata from Buienradar. (Msg: %s, status: %s)", raincontent.get(MESSAGE), raincontent.get(STATUS_CODE), ) @@ -136,7 +136,7 @@ async def async_update(self, *_): if result.get(SUCCESS) is not True: if int(datetime.now().strftime("%H")) > 0: _LOGGER.warning( - "Unable to parse data from Buienradar." "(Msg: %s)", + "Unable to parse data from Buienradar. (Msg: %s)", result.get(MESSAGE), ) await self.schedule_update(SCHEDULE_NOK) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index b3d5935784fdd2..4fe52a7d164535 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -168,7 +168,7 @@ async def async_get_still_stream(request, image_cb, content_type, interval): This method must be run in the event loop. """ response = web.StreamResponse() - response.content_type = "multipart/x-mixed-replace; " "boundary=--frameboundary" + response.content_type = "multipart/x-mixed-replace; boundary=--frameboundary" await response.prepare(request) async def write_to_mjpeg_stream(img_bytes): diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index 702ebdfa6112de..db504e3d19b60e 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -89,5 +89,5 @@ def _update_info(self): """Check the Cisco ME controller for devices.""" self.last_results = self.controller.get_associated_devices() _LOGGER.debug( - "Cisco Mobility Express controller returned:" " %s", self.last_results + "Cisco Mobility Express controller returned: %s", self.last_results ) diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index 6f80fa138d4b72..7be53d1fb6cd1c 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -54,5 +54,5 @@ def send_message(self, message="", **kwargs): self.client.messages.create(roomId=self.room, html=f"{title}{message}") except ApiError as api_error: _LOGGER.error( - "Could not send CiscoWebexTeams notification. " "Error: %s", api_error + "Could not send CiscoWebexTeams notification. Error: %s", api_error ) diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index cb2647487ea4f7..8e0b883b72693a 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -57,7 +57,7 @@ STATIONS_URI = "v2/networks/{uid}?fields=network.stations" CITYBIKES_ATTRIBUTION = ( - "Information provided by the CityBikes Project " "(https://citybik.es/#about)" + "Information provided by the CityBikes Project (https://citybik.es/#about)" ) CITYBIKES_NETWORKS = "citybikes_networks" @@ -143,9 +143,7 @@ async def async_citybikes_request(hass, uri, schema): except ValueError: _LOGGER.error("Received non-JSON data from CityBikes API endpoint") except vol.Invalid as err: - _LOGGER.error( - "Received unexpected JSON from CityBikes" " API endpoint: %s", err - ) + _LOGGER.error("Received unexpected JSON from CityBikes API endpoint: %s", err) raise CityBikesRequestError diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index c68f24172f035c..b97feb7c6f4362 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -583,7 +583,7 @@ async def alexa_sync(hass, connection, msg): connection.send_error( msg["id"], "alexa_relink", - "Please go to the Alexa app and re-link the Home Assistant " "skill.", + "Please go to the Alexa app and re-link the Home Assistant skill.", ) return diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 67869e6b88cf64..d52c0867e24cef 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -94,5 +94,5 @@ def update(self): self.exchange_rates = self.client.get_exchange_rates() except AuthenticationError as coinbase_error: _LOGGER.error( - "Authentication error connecting" " to coinbase: %s", coinbase_error + "Authentication error connecting to coinbase: %s", coinbase_error ) diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 6ca427f2476b5f..f4035352e51ceb 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -62,8 +62,7 @@ def wrapper_retry(device, *args, **kwargs): return method(device, *args, **kwargs) except (decora.decoraException, AttributeError, BTLEException): _LOGGER.warning( - "Decora connect error for device %s. " "Reconnecting...", - device.name, + "Decora connect error for device %s. Reconnecting...", device.name, ) # pylint: disable=protected-access device._switch.connect() diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 9d7c3892af8574..33fe4ee3647091 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -340,7 +340,7 @@ def media_duration(self): @property def media_image_url(self): """Return the image url of current playing media.""" - return "https://graph.facebook.com/v2.5/107771475912710/" "picture?type=large" + return "https://graph.facebook.com/v2.5/107771475912710/picture?type=large" @property def media_title(self): diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 5dd673ca93f9fe..cd4f910c707277 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -129,7 +129,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) else: _LOGGER.debug( - "Adding discovered device %s with" " client address %s", + "Adding discovered device %s with client address %s", str.title(loc["locationName"]), loc["clientAddr"], ) @@ -214,7 +214,7 @@ def update(self): except requests.RequestException as ex: _LOGGER.error( - "%s: Request error trying to update current status: " "%s", + "%s: Request error trying to update current status: %s", self.entity_id, ex, ) diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py index a5dde58d30ffc2..fbe7897e6bb20d 100644 --- a/homeassistant/components/dyson/__init__.py +++ b/homeassistant/components/dyson/__init__.py @@ -89,7 +89,7 @@ def setup(hass, config): # Not yet reliable for device in dyson_devices: _LOGGER.info( - "Trying to connect to device %s with timeout=%i " "and retry=%i", + "Trying to connect to device %s with timeout=%i and retry=%i", device, timeout, retry, diff --git a/homeassistant/components/ecoal_boiler/__init__.py b/homeassistant/components/ecoal_boiler/__init__.py index ed8e315bfedcaa..608e4a59a3fecc 100644 --- a/homeassistant/components/ecoal_boiler/__init__.py +++ b/homeassistant/components/ecoal_boiler/__init__.py @@ -91,7 +91,7 @@ def setup(hass, hass_config): if ecoal_contr.version is None: # Wrong credentials nor network config _LOGGER.error( - "Unable to read controller status from %s@%s" " (wrong host/credentials)", + "Unable to read controller status from %s@%s (wrong host/credentials)", username, host, ) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 83a1453a23ae4b..5915e64334f06b 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -550,7 +550,7 @@ def set_auto_temp_hold(self, heat_temp, cool_temp): self.hold_preference(), ) _LOGGER.debug( - "Setting ecobee hold_temp to: heat=%s, is=%s, " "cool=%s, is=%s", + "Setting ecobee hold_temp to: heat=%s, is=%s, cool=%s, is=%s", heat_temp, isinstance(heat_temp, (int, float)), cool_temp, diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index 2c18be47a1f817..7e5f88cff3e073 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -139,7 +139,7 @@ def alarm_disarm(self, code=None): self._egardiasystem.alarm_disarm() except requests.exceptions.RequestException as err: _LOGGER.error( - "Egardia device exception occurred when " "sending disarm command: %s", + "Egardia device exception occurred when sending disarm command: %s", err, ) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 2ad24e6f75ec43..cabba95ea7e48c 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -130,7 +130,7 @@ def async_on_service_call(service: HomeassistantServiceCall) -> None: # ESPHome uses servicecall packet for both events and service calls # Ensure the user can only send events of form 'esphome.xyz' if domain != "esphome": - _LOGGER.error("Can only generate events under esphome " "domain!") + _LOGGER.error("Can only generate events under esphome domain!") return hass.bus.async_fire(service.service, service_data) else: diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index bf1e55370bcaa1..2643607c3a87e2 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -131,7 +131,7 @@ def _filter_entries(self): """Filter the entries provided and return the ones to keep.""" if len(self._feed.entries) > self._max_entries: _LOGGER.debug( - "Processing only the first %s entries " "in feed %s", + "Processing only the first %s entries in feed %s", self._max_entries, self._url, ) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index d44819e758b5db..aeb7c0879e0c1c 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -134,11 +134,11 @@ def connect(self): info = self._client.info.get() self.hub_serial = slugify(info.serialNumber) except AssertionError: - _LOGGER.error("Can't connect to Fibaro HC. " "Please check URL.") + _LOGGER.error("Can't connect to Fibaro HC. Please check URL.") return False if login is None or login.status is False: _LOGGER.error( - "Invalid login for Fibaro HC. " "Please check username and password" + "Invalid login for Fibaro HC. Please check username and password" ) return False @@ -380,7 +380,7 @@ def level2(self): def dont_know_message(self, action): """Make a warning in case we don't know how to perform an action.""" _LOGGER.warning( - "Not sure how to setValue: %s " "(available actions: %s)", + "Not sure how to setValue: %s (available actions: %s)", str(self.ha_id), str(self.fibaro_device.actions), ) diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index a02b1b2504b264..f22b633591123e 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -323,7 +323,7 @@ async def async_flux_update(self, utcnow=None): elif self._mode == MODE_RGB: await async_set_lights_rgb(self.hass, self._lights, rgb, self._transition) _LOGGER.debug( - "Lights updated to rgb:%s, %s%% " "of %s cycle complete at %s", + "Lights updated to rgb:%s, %s%% of %s cycle complete at %s", rgb, round(percentage_complete * 100), time_state, diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py index 3f0578cf5b4750..af15c4e5fa8454 100644 --- a/homeassistant/components/foursquare/__init__.py +++ b/homeassistant/components/foursquare/__init__.py @@ -103,7 +103,7 @@ async def post(self, request): if self.push_secret != secret: _LOGGER.error( - "Received Foursquare push with invalid" "push secret: %s", secret + "Received Foursquare push with invalid push secret: %s", secret ) return self.json_message("Incorrect secret", HTTP_BAD_REQUEST) diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index d16fcbb3a1eb6d..e2382490cdef7d 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -60,7 +60,7 @@ def __init__(self, config): self._update_info() else: _LOGGER.error( - "Failed to establish connection to FRITZ!Box " "with IP: %s", self.host + "Failed to establish connection to FRITZ!Box with IP: %s", self.host ) def scan_devices(self): diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index cb5ae275df77cc..58514934fc7761 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -214,7 +214,7 @@ def _async_startup(event): else: self._target_temp = self.min_temp _LOGGER.warning( - "Undefined target temperature," "falling back to %s", + "Undefined target temperature, falling back to %s", self._target_temp, ) else: diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 7f4d03ccbb08a7..c48d5fb00b0642 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -369,7 +369,7 @@ async def async_send_command(self, command, **kwargs): for result in result_list: _LOGGER.error( - "Sending command %s to device %s failed with code " "%s: %s", + "Sending command %s to device %s failed with code %s: %s", result.command.command, result.command.device, result.code, diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index a0f30dd1a8b2c4..99f9449947872a 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -178,7 +178,7 @@ def update(self, **kwargs): else: _LOGGER.error( - "Failed fetching data for %s" "(HTTP Status_code = %d)", + "Failed fetching data for %s (HTTP Status_code = %d)", self._email, req.status_code, ) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 0bded03a29cb75..3eb604b3957b47 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -45,7 +45,7 @@ def exactly_two_period_keys(conf): """Ensure exactly 2 of CONF_PERIOD_KEYS are provided.""" if sum(param in conf for param in CONF_PERIOD_KEYS) != 2: raise vol.Invalid( - "You must provide exactly 2 of the following:" " start, end, duration" + "You must provide exactly 2 of the following: start, end, duration" ) return conf @@ -262,7 +262,7 @@ def update_period(self): ) except ValueError: _LOGGER.error( - "Parsing error: start must be a datetime" "or a timestamp" + "Parsing error: start must be a datetime or a timestamp" ) return @@ -281,7 +281,7 @@ def update_period(self): ) except ValueError: _LOGGER.error( - "Parsing error: end must be a datetime " "or a timestamp" + "Parsing error: end must be a datetime or a timestamp" ) return diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ea2c466092e3b3..ca5a601068a629 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -328,7 +328,7 @@ def reset_accessories(self, entity_ids): aid = generate_aid(entity_id) if aid not in self.bridge.accessories: _LOGGER.warning( - "Could not reset accessory. entity_id " "not found %s", entity_id + "Could not reset accessory. entity_id not found %s", entity_id ) continue acc = self.remove_bridge_accessory(aid) diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 450ae818ec87e0..9942c42a0de10f 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -422,7 +422,7 @@ def update_state(self, new_state): self.char_input_source.set_value(index) else: _LOGGER.warning( - "%s: Sources out of sync. " "Restart HomeAssistant", + "%s: Sources out of sync. Restart HomeAssistant", self.entity_id, ) self.char_input_source.set_value(0) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 608c9a974e57d9..0fe97cfca63a39 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -103,7 +103,7 @@ def validate_entity_config(values): if not isinstance(config, dict): raise vol.Invalid( - "The configuration for {} must be " " a dictionary.".format(entity) + "The configuration for {} must be a dictionary.".format(entity) ) if domain in ("alarm_control_panel", "lock"): diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 6d6fcd5c37793b..b966f5ae6a15bd 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -345,12 +345,12 @@ def check_authorization_header(self, request): if parts[0].lower() != "bearer": return self.json_message( - "Authorization header must " "start with Bearer", + "Authorization header must start with Bearer", status_code=HTTP_UNAUTHORIZED, ) if len(parts) != 2: return self.json_message( - "Authorization header must " "be Bearer token", + "Authorization header must be Bearer token", status_code=HTTP_UNAUTHORIZED, ) @@ -507,7 +507,7 @@ def _push_message(self, payload, **kwargs): info = REGISTER_SCHEMA(info) except vol.Invalid: _LOGGER.error( - "%s is not a valid HTML5 push notification" " target", target + "%s is not a valid HTML5 push notification target", target ) continue payload[ATTR_DATA][ATTR_JWT] = add_jwt( diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 553d3657160367..da406c071e4526 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -96,7 +96,7 @@ async def process_wrong_login(request): """ remote_addr = request[KEY_REAL_IP] - msg = "Login attempt or request with invalid authentication " "from {}".format( + msg = "Login attempt or request with invalid authentication from {}".format( remote_addr ) _LOGGER.warning(msg) @@ -150,7 +150,7 @@ async def process_success_login(request): and request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] > 0 ): _LOGGER.debug( - "Login success, reset failed login attempts counter" " from %s", remote_addr + "Login success, reset failed login attempts counter from %s", remote_addr ) request.app[KEY_FAILED_LOGIN_ATTEMPTS].pop(remote_addr) diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 31f96833667f3e..e60091684d3cd2 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -142,9 +142,11 @@ async def handle(request): elif result is None: result = b"" elif not isinstance(result, bytes): - assert False, ( - "Result should be None, string, bytes or Response. " "Got: {}" - ).format(result) + assert ( + False + ), "Result should be None, string, bytes or Response. Got: {}".format( + result + ) return web.Response(body=result, status=status_code) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 7239efafd10a07..cbcb21db7d0e1d 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -136,9 +136,7 @@ async def async_setup_entry( ) if config.swupdate2_bridge_state == "readytoinstall": - err = ( - "Please check for software updates of the bridge " "in the Philips Hue App." - ) + err = "Please check for software updates of the bridge in the Philips Hue App." _LOGGER.warning(err) return True diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index ad511639d57804..d81bbd4c4381b5 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -277,7 +277,7 @@ def __init__(self, light, request_bridge_update, bridge, is_group=False): _LOGGER.warning(err, self.name) if self.gamut: if not color.check_valid_gamut(self.gamut): - err = "Color gamut of %s: %s, not valid, " "setting gamut to None." + err = "Color gamut of %s: %s, not valid, setting gamut to None." _LOGGER.warning(err, self.name, str(self.gamut)) self.gamut_typ = GAMUT_TYPE_UNAVAILABLE self.gamut = None diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index db2f528153b734..ceef8acf7c3f27 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -162,7 +162,7 @@ async def refresh_email_count(self): self._email_count = len(lines[0].split()) else: _LOGGER.error( - "Can't parse IMAP server response to search " "'%s': %s / %s", + "Can't parse IMAP server response to search '%s': %s / %s", self._search, result, lines[0], diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index 58fbc5605db50f..4a169453e35d3d 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -205,14 +205,13 @@ def update(self): points = list(self.influx.query(self.query).get_points()) if not points: _LOGGER.warning( - "Query returned no points, sensor state set " "to UNKNOWN: %s", - self.query, + "Query returned no points, sensor state set to UNKNOWN: %s", self.query, ) self.value = None else: if len(points) > 1: _LOGGER.warning( - "Query returned multiple points, only first " "one shown: %s", + "Query returned multiple points, only first one shown: %s", self.query, ) self.value = points[0].get("value") diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 654f3547ad67f6..da684e03ddc829 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -98,7 +98,7 @@ async def async_set_datetime_service(entity, call): and not (time or dttm) ): _LOGGER.error( - "Invalid service data for %s " "input_datetime.set_datetime: %s", + "Invalid service data for %s input_datetime.set_datetime: %s", entity.entity_id, str(call.data), ) diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index 80dbad5336d478..63ed6a6ee26917 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -92,9 +92,9 @@ def send_message(self, message="", **kwargs): if req.status_code != 201: fallback_error = req.json().get("errorMessage", "Unknown error") - fallback_message = ( - "Internal server error, " "please try again later: " "{}" - ).format(fallback_error) + fallback_message = "Internal server error, please try again later: {}".format( + fallback_error + ) message = req.json().get("message", fallback_message) if req.status_code == 429: _LOGGER.warning(message) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index eed5f1a81a0bb2..9cf1332c4f4f1b 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -165,7 +165,7 @@ def _negative_node_control_handler(self, event: object) -> None: """Handle an "On" control event from the "negative" node.""" if event == "DON": _LOGGER.debug( - "Sensor %s turning Off via the Negative node " "sending a DON command", + "Sensor %s turning Off via the Negative node sending a DON command", self.name, ) self._computed_state = False @@ -181,7 +181,7 @@ def _positive_node_control_handler(self, event: object) -> None: """ if event == "DON": _LOGGER.debug( - "Sensor %s turning On via the Primary node " "sending a DON command", + "Sensor %s turning On via the Primary node sending a DON command", self.name, ) self._computed_state = True @@ -189,7 +189,7 @@ def _positive_node_control_handler(self, event: object) -> None: self._heartbeat() if event == "DOF": _LOGGER.debug( - "Sensor %s turning Off via the Primary node " "sending a DOF command", + "Sensor %s turning Off via the Primary node sending a DOF command", self.name, ) self._computed_state = False diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 00d5d18f0131bd..61a497e938a672 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -102,7 +102,7 @@ async def async_setup(hass, config): except XKNXException as ex: _LOGGER.warning("Can't connect to KNX interface: %s", ex) hass.components.persistent_notification.async_create( - "Can't connect to KNX interface:
" "{0}".format(ex), title="KNX" + "Can't connect to KNX interface:
{0}".format(ex), title="KNX" ) for component, discovery_type in ( diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 9721ea2d31f557..71418927ed21c4 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -811,7 +811,7 @@ async def async_call_method(self, method, **kwargs): except jsonrpc_base.jsonrpc.TransportError: result = None _LOGGER.warning( - "TransportError trying to run API method " "%s.%s(%s)", + "TransportError trying to run API method %s.%s(%s)", self.entity_id, method, kwargs, diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index b8dd610b1a0f8c..052eb3bceac887 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -113,7 +113,7 @@ def send_message(self, message="", **kwargs): self._devices = lmn.get_devices() except RequestsConnectionError: _LOGGER.warning( - "Problem connecting to LaMetric, " "using cached devices instead" + "Problem connecting to LaMetric, using cached devices instead" ) for dev in self._devices: if targets is None or dev["name"] in targets: diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 236035b0400eb7..f4545817c9f82f 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -9,7 +9,7 @@ # Regex for address validation PATTERN_ADDRESS = re.compile( - "^((?P\\w+)\\.)?s?(?P\\d+)" "\\.(?Pm|g)?(?P\\d+)$" + "^((?P\\w+)\\.)?s?(?P\\d+)\\.(?Pm|g)?(?P\\d+)$" ) diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index 3c775224623c9d..c35a0cc00bf95d 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -305,7 +305,7 @@ def __call__(self, call): hit = pypck.lcn_defs.SendKeyCommand.HIT if pypck.lcn_defs.SendKeyCommand[call.data[CONF_STATE]] != hit: raise ValueError( - "Only hit command is allowed when sending" " deferred keys." + "Only hit command is allowed when sending deferred keys." ) delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT]) address_connection.send_keys_hit_deferred(keys, delay_time, delay_unit) @@ -344,7 +344,7 @@ def __call__(self, call): if delay_time != 0: if table_id != 0: raise ValueError( - "Only table A is allowed when locking keys" " for a specific time." + "Only table A is allowed when locking keys for a specific time." ) delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT]) address_connection.lock_keys_tab_a_temporary(delay_time, delay_unit, states) diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index ce8460233d6148..bc585153b646c9 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -207,5 +207,5 @@ async def get(self, request): ) return self.json_message("Authorisation code saved") return self.json_message( - "Authorisation code missing " "from query string", status_code=400 + "Authorisation code missing from query string", status_code=400 ) diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 977ee51cd1c871..80beaf1f798268 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -153,7 +153,7 @@ def async_min_max_sensor_state_listener(entity, old_state, new_state): self.last = float(new_state.state) except ValueError: _LOGGER.warning( - "Unable to store state. " "Only numerical states are supported" + "Unable to store state. Only numerical states are supported" ) hass.async_add_job(self.async_update_ha_state, True) diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 8ac34c9af1dc67..b51bf235cf05e1 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -139,9 +139,9 @@ async def async_send_message(self, message="", **kwargs): return fallback_error = result.get("errorMessage", "Unknown error") - fallback_message = ( - "Internal server error, " "please try again later: " "{}" - ).format(fallback_error) + fallback_message = "Internal server error, please try again later: {}".format( + fallback_error + ) message = result.get("message", fallback_message) if response.status == 429: _LOGGER.warning(message) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 5b04a898ab92ed..484382983aca0c 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -102,7 +102,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) except KeyError: _LOGGER.error( - "Unable to detect data type for %s sensor, " "try a custom type", + "Unable to detect data type for %s sensor, try a custom type", register.get(CONF_NAME), ) continue @@ -119,7 +119,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if register.get(CONF_COUNT) * 2 != size: _LOGGER.error( - "Structure size (%d bytes) mismatch registers count " "(%d words)", + "Structure size (%d bytes) mismatch registers count (%d words)", size, register.get(CONF_COUNT), ) diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index eba0c754f454e7..0ed33dedb578ff 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -236,7 +236,7 @@ def update(self): self._is_on = False else: _LOGGER.error( - "Unexpected response from hub %s, slave %s " "register %s, got 0x%2x", + "Unexpected response from hub %s, slave %s register %s, got 0x%2x", self._hub.name, self._slave, self._verify_register, diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index 15f8b80a5ab9e8..0d6c6f552847ab 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -110,7 +110,7 @@ async def async_added_to_hass(self): def mold_indicator_sensors_state_listener(entity, old_state, new_state): """Handle for state changes for dependent sensors.""" _LOGGER.debug( - "Sensor state change for %s that had old state %s " "and new state %s", + "Sensor state change for %s that had old state %s and new state %s", entity, old_state, new_state, @@ -188,7 +188,7 @@ def _update_temp_sensor(state): # Return an error if the sensor change its state to Unknown. if state.state == STATE_UNKNOWN: _LOGGER.error( - "Unable to parse temperature sensor %s with state:" " %s", + "Unable to parse temperature sensor %s with state: %s", state.entity_id, state.state, ) @@ -199,7 +199,7 @@ def _update_temp_sensor(state): if temp is None: _LOGGER.error( - "Unable to parse temperature sensor %s with state:" " %s", + "Unable to parse temperature sensor %s with state: %s", state.entity_id, state.state, ) @@ -211,7 +211,7 @@ def _update_temp_sensor(state): if unit == TEMP_CELSIUS: return temp _LOGGER.error( - "Temp sensor %s has unsupported unit: %s (allowed: %s, " "%s)", + "Temp sensor %s has unsupported unit: %s (allowed: %s, %s)", state.entity_id, unit, TEMP_CELSIUS, @@ -306,7 +306,7 @@ def _calc_moldindicator(self): if None in (self._dewpoint, self._calib_factor) or self._calib_factor == 0: _LOGGER.debug( - "Invalid inputs - dewpoint: %s," " calibration-factor: %s", + "Invalid inputs - dewpoint: %s, calibration-factor: %s", self._dewpoint, self._calib_factor, ) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index d8dc584ae30e61..a9d5ac93ebc5ad 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -136,10 +136,10 @@ def valid_topic(value: Any) -> str: raise vol.Invalid("MQTT topic name/filter must not be empty.") if len(raw_value) > 65535: raise vol.Invalid( - "MQTT topic name/filter must not be longer than " "65535 encoded bytes." + "MQTT topic name/filter must not be longer than 65535 encoded bytes." ) if "\0" in value: - raise vol.Invalid("MQTT topic name/filter must not contain null " "character.") + raise vol.Invalid("MQTT topic name/filter must not contain null character.") return value @@ -151,7 +151,7 @@ def valid_subscribe_topic(value: Any) -> str: i < len(value) - 1 and value[i + 1] != "/" ): raise vol.Invalid( - "Single-level wildcard must occupy an entire " "level of the filter" + "Single-level wildcard must occupy an entire level of the filter" ) index = value.find("#") @@ -164,7 +164,7 @@ def valid_subscribe_topic(value: Any) -> str: ) if len(value) > 1 and value[index - 1] != "/": raise vol.Invalid( - "Multi-level wildcard must be after a topic " "level separator." + "Multi-level wildcard must be after a topic level separator." ) return value diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index e2a1479595e419..73a28aa121f289 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -216,7 +216,7 @@ def set_eta(service): structure.set_eta(trip_id, eta_begin, eta_end) else: _LOGGER.info( - "No thermostats found in structure: %s, " "unable to set ETA", + "No thermostats found in structure: %s, unable to set ETA", structure.name, ) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index e5b4f34812aafa..fba84c936f524a 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -124,9 +124,7 @@ async def async_handle_start_charge(service): # for the charging request to reach the car. result = await hass.async_add_executor_job(data_store.leaf.start_charging) if result: - _LOGGER.debug( - "Start charging sent, " "request updated data in 1 minute" - ) + _LOGGER.debug("Start charging sent, request updated data in 1 minute") check_charge_at = utcnow() + timedelta(minutes=1) data_store.next_update = check_charge_at async_track_point_in_utc_time( @@ -414,7 +412,7 @@ async def async_set_climate(self, toggle): for attempt in range(MAX_RESPONSE_ATTEMPTS): if attempt > 0: _LOGGER.debug( - "Climate data not in yet (%s) (%s). " "Waiting (%s) seconds", + "Climate data not in yet (%s) (%s). Waiting (%s) seconds", self.leaf.vin, attempt, PYCARWINGS2_SLEEP, diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 34e3bfaf08611e..bdf0eaafc99706 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -189,7 +189,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data.update(no_throttle=True) except data.pynuterror as err: _LOGGER.error( - "Failure while testing NUT status retrieval. " "Cannot continue setup: %s", + "Failure while testing NUT status retrieval. Cannot continue setup: %s", err, ) raise PlatformNotReady diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index a9606e25bad746..490ebbe75b3070 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -66,9 +66,7 @@ def device_state_attributes(self): def update(self): """Get the latest data from OhmConnect.""" try: - url = ("https://login.ohmconnect.com" "/verify-ohm-hour/{}").format( - self._ohmid - ) + url = "https://login.ohmconnect.com/verify-ohm-hour/{}".format(self._ohmid) response = requests.get(url, timeout=10) root = ET.fromstring(response.text) diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index cef99902d23821..d916d9f7f29669 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -40,7 +40,7 @@ SCAN_INTERVAL = timedelta(seconds=12) # opensky public limit is 10 seconds OPENSKY_ATTRIBUTION = ( - "Information provided by the OpenSky Network " "(https://opensky-network.org)" + "Information provided by the OpenSky Network (https://opensky-network.org)" ) OPENSKY_API_URL = "https://opensky-network.org/api/states/all" OPENSKY_API_FIELDS = [ diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 69ca965d660e24..ce8676ad440fea 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -272,7 +272,7 @@ def update_forecast(self): self.latitude, self.longitude ) except APICallError: - _LOGGER.error("Exception when calling OWM web API " "to update forecast") + _LOGGER.error("Exception when calling OWM web API to update forecast") return if fcd is None: diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py index afde50cae49a02..3882ba4bf7de5d 100644 --- a/homeassistant/components/owlet/__init__.py +++ b/homeassistant/components/owlet/__init__.py @@ -51,7 +51,7 @@ def setup(hass, config): device = PyOwlet(username, password) except KeyError: _LOGGER.error( - "Owlet authentication failed. Please verify your " "credentials are correct" + "Owlet authentication failed. Please verify your credentials are correct" ) return False diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index b75be465aa19aa..71494e9e8059ab 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -233,7 +233,7 @@ def async_valid_accuracy(self, message): if self.max_gps_accuracy is not None and acc > self.max_gps_accuracy: _LOGGER.info( - "Ignoring %s update because expected GPS " "accuracy %s is not met: %s", + "Ignoring %s update because expected GPS accuracy %s is not met: %s", message["_type"], self.max_gps_accuracy, message, diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index 9690e748887ec2..d5167ebfdc957c 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -59,7 +59,7 @@ async def async_send_message(self, message, **kwargs): if response.status != 200 or "error" in result: _LOGGER.error( - "Prowl service returned http " "status %d, response %s", + "Prowl service returned http status %d, response %s", response.status, result, ) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 45a1c19c29e3c9..7e5f643675716c 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -268,7 +268,7 @@ def check_proximity_state_change(self, entity, old_state, new_state): self.nearest = entity_name self.schedule_update_ha_state() _LOGGER.debug( - "proximity.%s update entity: distance=%s: direction=%s: " "device=%s", + "proximity.%s update entity: distance=%s: direction=%s: device=%s", self.friendly_name, round(dist_to_zone), direction_of_travel, diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index ddae8a81db159d..0c5886e177c5af 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -225,7 +225,7 @@ def sleep(self, *args, **kwargs): if not TimeWrapper.warned: TimeWrapper.warned = True _LOGGER.warning( - "Using time.sleep can reduce the performance of " "Home Assistant" + "Using time.sleep can reduce the performance of Home Assistant" ) time.sleep(*args, **kwargs) diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index 97eb8eedfd3915..58151fa02ce0e5 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -54,7 +54,7 @@ def __init__(self, config): _LOGGER.error("Unable to connect to gateway. Check host.") if not self.success_init: - _LOGGER.error("Unable to login to gateway. Check password and " "host.") + _LOGGER.error("Unable to login to gateway. Check password and host.") def scan_devices(self): """Scan for new devices and return a list of found MACs.""" diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 7ae1cb1a2204a8..ab56a5fc33b6c7 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -228,7 +228,7 @@ def run(self): _LOGGER.debug("Connected to recorder database") except Exception as err: # pylint: disable=broad-except _LOGGER.error( - "Error during connection setup: %s (retrying " "in %s seconds)", + "Error during connection setup: %s (retrying in %s seconds)", err, CONNECT_RETRY_WAIT, ) diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index fdfbdfd5cdc203..02875cb8aa996d 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -166,7 +166,7 @@ def __init__(self, hass): self._config = json.load(config_file) except ValueError: _LOGGER.error( - "Failed to load configuration file, creating a " "new one: %s", + "Failed to load configuration file, creating a new one: %s", self._config_file_path, ) self._config = dict() @@ -258,7 +258,7 @@ def _check_token(self): valid = self._rtm_api.token_valid() if not valid: _LOGGER.error( - "Token for account %s is invalid. You need to " "register again!", + "Token for account %s is invalid. You need to register again!", self.name, ) self._rtm_config.delete_token(self._name) @@ -306,14 +306,14 @@ def create_task(self, call): timeline=timeline, ) _LOGGER.debug( - "Updated task with id '%s' in account " "%s to name %s", + "Updated task with id '%s' in account %s to name %s", hass_id, self.name, task_name, ) except RtmRequestFailedException as rtm_exception: _LOGGER.error( - "Error creating new Remember The Milk task for " "account %s: %s", + "Error creating new Remember The Milk task for account %s: %s", self._name, rtm_exception, ) @@ -347,7 +347,7 @@ def complete_task(self, call): ) except RtmRequestFailedException as rtm_exception: _LOGGER.error( - "Error creating new Remember The Milk task for " "account %s: %s", + "Error creating new Remember The Milk task for account %s: %s", self._name, rtm_exception, ) diff --git a/homeassistant/components/satel_integra/__init__.py b/homeassistant/components/satel_integra/__init__.py index 1972eefd6b507a..84bb3b570d8912 100644 --- a/homeassistant/components/satel_integra/__init__.py +++ b/homeassistant/components/satel_integra/__init__.py @@ -62,9 +62,7 @@ def is_alarm_code_necessary(value): """Check if alarm code must be configured.""" if value.get(CONF_SWITCHABLE_OUTPUTS) and CONF_DEVICE_CODE not in value: - raise vol.Invalid( - "You need to specify alarm " " code to use switchable_outputs" - ) + raise vol.Invalid("You need to specify alarm code to use switchable_outputs") return value diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index c20f30cf87151d..9233b3d152d9aa 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -69,14 +69,14 @@ def _devices_updated(self, zones): async def async_turn_on(self, **kwargs): """Turn the device on.""" - _LOGGER.debug("Switch: %s status: %s," " turning on", self._name, self._state) + _LOGGER.debug("Switch: %s status: %s, turning on", self._name, self._state) await self._satel.set_output(self._code, self._device_number, True) self.async_schedule_update_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" _LOGGER.debug( - "Switch name: %s status: %s," " turning off", self._name, self._state + "Switch name: %s status: %s, turning off", self._name, self._state ) await self._satel.set_output(self._code, self._device_number, False) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/slide/__init__.py b/homeassistant/components/slide/__init__.py index 49e50e601dd50f..ccf4465577b206 100644 --- a/homeassistant/components/slide/__init__.py +++ b/homeassistant/components/slide/__init__.py @@ -60,8 +60,7 @@ async def update_slides(now=None): for slide in result: if "device_id" not in slide: _LOGGER.error( - "Found invalid Slide entry, device_id is " "missing. Entry=%s", - slide, + "Found invalid Slide entry, device_id is missing. Entry=%s", slide, ) continue @@ -104,7 +103,7 @@ async def update_slides(now=None): ) elif "code" in slide["device_info"]: _LOGGER.warning( - "Slide %s (%s) is offline with " "code=%s", + "Slide %s (%s) is offline with code=%s", slide["id"], slidenew["mac"], slide["device_info"]["code"], diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 74feb93eec43c2..d04872909262b9 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -339,7 +339,7 @@ async def delete_subscription(sub: SubscriptionEntity): ) except Exception as error: # pylint:disable=broad-except _LOGGER.error( - "Failed to remove subscription for '%s' under app " "'%s': %s", + "Failed to remove subscription for '%s' under app '%s': %s", sub.capability, installed_app_id, error, diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index ec21a5d7822e60..ba0c725eb7fb67 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -37,7 +37,7 @@ CONF_CLIENT_SECRET = "client_secret" CONFIGURATOR_DESCRIPTION = ( - "To link your Spotify account, " "click the link, login, and authorize:" + "To link your Spotify account, click the link, login, and authorize:" ) CONFIGURATOR_LINK_NAME = "Link Spotify account" CONFIGURATOR_SUBMIT_CAPTION = "I authorized successfully" diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index c8e7b9d6fc264e..d4624e82bb7dff 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by the Swiss Federal Office for the " "Environment FOEN" +ATTRIBUTION = "Data provided by the Swiss Federal Office for the Environment FOEN" ATTR_DELTA_24H = "delta-24h" ATTR_MAX_1H = "max-1h" diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index c63f5061dfa9c3..ea797754da8d38 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -60,9 +60,7 @@ def __init__(self, hass, config): if self.home_id is None: self.tadoapiurl = "https://my.tado.com/api/v2/me" else: - self.tadoapiurl = ( - "https://my.tado.com/api/v2" "/homes/{home_id}/mobileDevices" - ) + self.tadoapiurl = "https://my.tado.com/api/v2/homes/{home_id}/mobileDevices" # The API URL always needs a username and password self.tadoapiurl += "?username={username}&password={password}" diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index b91c37b35dee05..fc37121f3f9f07 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -628,7 +628,7 @@ def answer_callback_query( """Answer a callback originated with a press in an inline keyboard.""" params = self._get_msg_kwargs(kwargs) _LOGGER.debug( - "Answer callback query with callback ID %s: %s, " "alert: %s.", + "Answer callback query with callback ID %s: %s, alert: %s.", callback_query_id, message, show_alert, diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 3ca25a33d6437f..7de43ea0702beb 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -223,7 +223,7 @@ def _async_render(self): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render template %s, " "the state is unknown", self._name + "Could not render template %s, the state is unknown", self._name ) return _LOGGER.error("Could not render template %s: %s", self._name, ex) @@ -259,7 +259,7 @@ def _async_render(self): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index c55c3f97df5150..f6678067d70da3 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -437,8 +437,7 @@ async def async_update(self): if state < 0 or state > 100: self._tilt_value = None _LOGGER.error( - "Tilt value must be between 0 and 100." " Value was: %.2f", - state, + "Tilt value must be between 0 and 100. Value was: %.2f", state, ) else: self._tilt_value = state @@ -466,7 +465,7 @@ async def async_update(self): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index e18833aae3943e..f4682fa903d894 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -320,7 +320,7 @@ async def async_update(self): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a4e28265e1d92f..0ca5571515a573 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -230,7 +230,7 @@ async def async_update(self): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render template %s," " the state is unknown.", self._name + "Could not render template %s, the state is unknown.", self._name ) else: self._state = None @@ -268,7 +268,7 @@ async def async_update(self): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index f44f7256e199af..c2d8e8158c1d36 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -233,7 +233,7 @@ async def async_update(self): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index d53b5ab6cf0dd7..5a5f1b1985b123 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -124,7 +124,7 @@ def _update_tomato_info(self): # We get this if we could not connect to the router or # an invalid http_id was supplied. _LOGGER.exception( - "Failed to connect to the router or " "invalid http_id supplied" + "Failed to connect to the router or invalid http_id supplied" ) return False diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index b8b4236806f1ef..ed77fc4eea0061 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -119,7 +119,7 @@ def update(self): attr["triggered_source"] = "Carbon Monoxide" else: logging.info( - "Total Connect Client returned unknown " "status code: %s", status + "Total Connect Client returned unknown status code: %s", status ) state = None diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index 6f94d5c38b065c..12e84a9dbf8061 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if uscis.valid_case_id: add_entities([uscis]) else: - _LOGGER.error("Setup USCIS Sensor Fail" " check if your Case ID is Valid") + _LOGGER.error("Setup USCIS Sensor Fail check if your Case ID is Valid") class UscisSensor(Entity): diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index b3ae01c8f6733b..b71d44206c8bd8 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -740,7 +740,7 @@ def _pubnub_update(self, message): try: if message is None: _LOGGER.error( - "Error on pubnub update for %s " "polling API for current state", + "Error on pubnub update for %s polling API for current state", self.name, ) self.schedule_update_ha_state(True) @@ -749,8 +749,7 @@ def _pubnub_update(self, message): self.schedule_update_ha_state() except (ValueError, KeyError, AttributeError): _LOGGER.error( - "Error in pubnub JSON for %s " "polling API for current state", - self.name, + "Error in pubnub JSON for %s polling API for current state", self.name, ) self.schedule_update_ha_state(True) diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 1bc971f1372794..c0a30a8867ff7d 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -199,7 +199,7 @@ def setup(hass, config): except (ConnectTimeout, HTTPError, WirelessTagsException) as ex: _LOGGER.error("Unable to connect to wirelesstag.net service: %s", str(ex)) hass.components.persistent_notification.create( - "Error: {}
" "Please restart hass after fixing this." "".format(ex), + "Error: {}
Please restart hass after fixing this.".format(ex), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID, ) diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index cc71e71a1d0c05..5d3bf1f74b85d7 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -1012,7 +1012,7 @@ def _cfg_expand(self, what, default=None): val = val(self.rest) except (KeyError, IndexError, TypeError, ValueError) as err: _LOGGER.warning( - "Failed to expand cfg from WU API." " Condition: %s Attr: %s Error: %s", + "Failed to expand cfg from WU API. Condition: %s Attr: %s Error: %s", self._condition, what, repr(err), diff --git a/homeassistant/components/xfinity/device_tracker.py b/homeassistant/components/xfinity/device_tracker.py index 524929ae42d442..20e13682979c2f 100644 --- a/homeassistant/components/xfinity/device_tracker.py +++ b/homeassistant/components/xfinity/device_tracker.py @@ -32,7 +32,7 @@ def get_scanner(hass, config): scanner = XfinityDeviceScanner(gateway) except (RequestException, ValueError): _LOGGER.error( - "Error communicating with Xfinity Gateway. " "Check host: %s", gateway.host + "Error communicating with Xfinity Gateway. Check host: %s", gateway.host ) return scanner @@ -51,7 +51,7 @@ def scan_devices(self): try: connected_devices = self.gateway.scan_devices() except (RequestException, ValueError): - _LOGGER.error("Unable to scan devices. " "Check connection to gateway") + _LOGGER.error("Unable to scan devices. Check connection to gateway") return connected_devices def get_device_name(self, device): diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 5a7b743b3622ff..bcc83bae454994 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -467,7 +467,7 @@ async def async_turn_on(self, **kwargs): ) result = await self._try_command( - "Setting brightness and color temperature failed: " "%s bri, %s cct", + "Setting brightness and color temperature failed: %s bri, %s cct", self._light.set_brightness_and_color_temperature, percent_brightness, percent_color_temp, @@ -479,7 +479,7 @@ async def async_turn_on(self, **kwargs): elif ATTR_COLOR_TEMP in kwargs: _LOGGER.debug( - "Setting color temperature: " "%s mireds, %s%% cct", + "Setting color temperature: %s mireds, %s%% cct", color_temp, percent_color_temp, ) @@ -825,14 +825,14 @@ async def async_turn_on(self, **kwargs): if ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR in kwargs: _LOGGER.debug( - "Setting brightness and color: " "%s %s%%, %s", + "Setting brightness and color: %s %s%%, %s", brightness, percent_brightness, rgb, ) result = await self._try_command( - "Setting brightness and color failed: " "%s bri, %s color", + "Setting brightness and color failed: %s bri, %s color", self._light.set_brightness_and_rgb, percent_brightness, rgb, @@ -853,7 +853,7 @@ async def async_turn_on(self, **kwargs): ) result = await self._try_command( - "Setting brightness and color temperature failed: " "%s bri, %s cct", + "Setting brightness and color temperature failed: %s bri, %s cct", self._light.set_brightness_and_color_temperature, percent_brightness, percent_color_temp, @@ -875,7 +875,7 @@ async def async_turn_on(self, **kwargs): elif ATTR_COLOR_TEMP in kwargs: _LOGGER.debug( - "Setting color temperature: " "%s mireds, %s%% cct", + "Setting color temperature: %s mireds, %s%% cct", color_temp, percent_color_temp, ) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index bc703c769a556c..4ef34e8ff56d05 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -379,7 +379,7 @@ async def async_set_fan_speed(self, fan_speed, **kwargs): fan_speed = int(fan_speed) except ValueError as exc: _LOGGER.error( - "Fan speed step not recognized (%s). " "Valid speeds are: %s", + "Fan speed step not recognized (%s). Valid speeds are: %s", exc, self.fan_speed_list, ) diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index d26b1eed151360..28d4269865784e 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -201,7 +201,7 @@ async def send_file(self, timeout=None): except FileTooBig as ex: _LOGGER.error("File too big for server, could not upload file %s", ex) except UploadServiceNotFound as ex: - _LOGGER.error("UploadServiceNotFound: " " could not upload file %s", ex) + _LOGGER.error("UploadServiceNotFound, could not upload file %s", ex) except FileUploadError as ex: _LOGGER.error("FileUploadError, could not upload file %s", ex) except requests.exceptions.SSLError as ex: diff --git a/homeassistant/components/xs1/__init__.py b/homeassistant/components/xs1/__init__.py index aeb6204265b006..1fbcb49d0c90fa 100644 --- a/homeassistant/components/xs1/__init__.py +++ b/homeassistant/components/xs1/__init__.py @@ -63,8 +63,7 @@ def setup(hass, config): ) except ConnectionError as error: _LOGGER.error( - "Failed to create XS1 API client " "because of a connection error: %s", - error, + "Failed to create XS1 API client because of a connection error: %s", error, ) return False diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 0039755a6af518..c40ead2a892375 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -298,7 +298,7 @@ def _lights_setup_helper(klass): else: _lights_setup_helper(YeelightGenericLight) _LOGGER.warning( - "Cannot determine device type for %s, %s. " "Falling back to white only", + "Cannot determine device type for %s, %s. Falling back to white only", device.ipaddr, device.name, ) @@ -743,7 +743,7 @@ def turn_on(self, **kwargs) -> None: self.set_music_mode(self.config[CONF_MODE_MUSIC]) except BulbException as ex: _LOGGER.error( - "Unable to turn on music mode," "consider disabling it: %s", ex + "Unable to turn on music mode, consider disabling it: %s", ex ) try: diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index f8fbc97962f6f0..c9392561fc8c12 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -160,7 +160,7 @@ class YrData: def __init__(self, hass, coordinates, forecast, devices): """Initialize the data object.""" self._url = ( - "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/" + "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/" ) self._urlparams = coordinates self._forecast = forecast diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/zigbee/__init__.py index d63a713045b0b1..475d63a5c3b7f1 100644 --- a/homeassistant/components/zigbee/__init__.py +++ b/homeassistant/components/zigbee/__init__.py @@ -335,7 +335,7 @@ def update(self): pin_name = DIGITAL_PINS[self._config.pin] if pin_name not in sample: _LOGGER.warning( - "Pin %s (%s) was not in the sample provided by Zigbee device " "%s.", + "Pin %s (%s) was not in the sample provided by Zigbee device %s.", self._config.pin, pin_name, hexlify(self._config.address), diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index cb494b5fa6f290..32348a8becc85c 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -275,7 +275,7 @@ def nice_print_node(node): value_id: _obj_to_dict(value) for value_id, value in node.values.items() } - _LOGGER.info("FOUND NODE %s \n" "%s", node.product_name, node_dict) + _LOGGER.info("FOUND NODE %s \n%s", node.product_name, node_dict) def get_config_value(node, value_index, tries=5): @@ -476,7 +476,7 @@ def _on_ready(sec): @callback def _on_timeout(sec): _LOGGER.warning( - "Z-Wave node %d not ready after %d seconds, " "continuing anyway", + "Z-Wave node %d not ready after %d seconds, continuing anyway", entity.node_id, sec, ) @@ -526,7 +526,7 @@ def network_ready(): def network_complete(): """Handle the querying of all nodes on network.""" _LOGGER.info( - "Z-Wave network is complete. All nodes on the network " "have been queried" + "Z-Wave network is complete. All nodes on the network have been queried" ) hass.bus.fire(const.EVENT_NETWORK_COMPLETE) @@ -684,7 +684,7 @@ def set_config_parameter(service): if value.type == const.TYPE_BOOL: value.data = int(selection == "True") _LOGGER.info( - "Setting config parameter %s on Node %s " "with bool selection %s", + "Setting config parameter %s on Node %s with bool selection %s", param, node_id, str(selection), @@ -693,7 +693,7 @@ def set_config_parameter(service): if value.type == const.TYPE_LIST: value.data = str(selection) _LOGGER.info( - "Setting config parameter %s on Node %s " "with list selection %s", + "Setting config parameter %s on Node %s with list selection %s", param, node_id, str(selection), @@ -712,7 +712,7 @@ def set_config_parameter(service): return value.data = int(selection) _LOGGER.info( - "Setting config parameter %s on Node %s " "with selection %s", + "Setting config parameter %s on Node %s with selection %s", param, node_id, selection, @@ -720,7 +720,7 @@ def set_config_parameter(service): return node.set_config_param(param, selection, size) _LOGGER.info( - "Setting unknown config parameter %s on Node %s " "with selection %s", + "Setting unknown config parameter %s on Node %s with selection %s", param, node_id, selection, @@ -831,7 +831,7 @@ def reset_node_meters(service): ) return _LOGGER.info( - "Node %s on instance %s does not have resettable " "meters.", + "Node %s on instance %s does not have resettable meters.", node_id, instance, ) diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index 9d7b2bffddd44a..f84b1b5cfd47e2 100644 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -270,7 +270,7 @@ def __init__(self, values): workaround = DEVICE_MAPPINGS[specific_sensor_key] if workaround & WORKAROUND_V2BTZE: self._v2btze = 1 - _LOGGER.debug("Polycontrol Danalock v2 BTZE " "workaround enabled") + _LOGGER.debug("Polycontrol Danalock v2 BTZE workaround enabled") if workaround & WORKAROUND_DEVICE_STATE: self._state_workaround = True _LOGGER.debug("Notification device state workaround enabled") @@ -299,7 +299,7 @@ def update_properties(self): ): self._state = LOCK_STATUS.get(str(notification_data)) _LOGGER.debug( - "Lock state set from Access Control value and is %s, " "get=%s", + "Lock state set from Access Control value and is %s, get=%s", str(notification_data), self.state, ) diff --git a/homeassistant/config.py b/homeassistant/config.py index c3a97a1184cbc1..ee3ccc15f81935 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -209,7 +209,7 @@ def _no_duplicate_auth_mfa_module( { CONF_TYPE: vol.NotIn( ["insecure_example"], - "The insecure_example mfa module" " is for testing only.", + "The insecure_example mfa module is for testing only.", ) } ) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index c02c49ce311208..02853f7615b82b 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -192,7 +192,7 @@ def async_numeric_state( fvalue = float(value) except ValueError: _LOGGER.warning( - "Value cannot be processed as a number: %s " "(Offending entity: %s)", + "Value cannot be processed as a number: %s (Offending entity: %s)", entity, value, ) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 035e1f678bfbf3..bcf0d42df70841 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -544,7 +544,7 @@ def socket_timeout(value: Optional[Any]) -> object: float_value = float(value) if float_value > 0.0: return float_value - raise vol.Invalid("Invalid socket timeout value." " float > 0.0 required.") + raise vol.Invalid("Invalid socket timeout value. float > 0.0 required.") except Exception as _: raise vol.Invalid("Invalid socket timeout: {err}".format(err=_)) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 5fd88729f080b1..9b82eb76dec520 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -459,7 +459,7 @@ async def _update_entity_states(self, now: datetime) -> None: self._process_updates = asyncio.Lock() if self._process_updates.locked(): self.logger.warning( - "Updating %s %s took longer than the scheduled update " "interval %s", + "Updating %s %s took longer than the scheduled update interval %s", self.platform_name, self.domain, self.scan_interval, diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 4bdee7500348ed..b27120e1825f39 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -615,7 +615,7 @@ def distance(hass, *args): if latitude is None or longitude is None: _LOGGER.warning( - "Distance:Unable to process latitude and " "longitude: %s, %s", + "Distance:Unable to process latitude and longitude: %s, %s", value, value_2, ) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index de05c944aafb7f..a14a5209840cb8 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -407,7 +407,7 @@ def _load_file( if str(err) not in white_listed_errors: _LOGGER.exception( - ("Error loading %s. Make sure all " "dependencies are installed"), + ("Error loading %s. Make sure all dependencies are installed"), path, ) diff --git a/homeassistant/scripts/ensure_config.py b/homeassistant/scripts/ensure_config.py index cb2c408804972e..0b5d1104997976 100644 --- a/homeassistant/scripts/ensure_config.py +++ b/homeassistant/scripts/ensure_config.py @@ -11,9 +11,7 @@ def run(args): """Handle ensure config commandline script.""" parser = argparse.ArgumentParser( - description=( - "Ensure a Home Assistant config exists, " "creates one if necessary." - ) + description=("Ensure a Home Assistant config exists, creates one if necessary.") ) parser.add_argument( "-c", diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 2424f5fc465209..f0d1e492b99524 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -75,7 +75,7 @@ async def _async_process_dependencies( if failed: _LOGGER.error( - "Unable to set up dependencies of %s. " "Setup failed for dependencies: %s", + "Unable to set up dependencies of %s. Setup failed for dependencies: %s", name, ", ".join(failed), ) @@ -108,14 +108,14 @@ def log_error(msg: str, link: Optional[str] = None) -> None: await loader.async_component_dependencies(hass, domain) except loader.IntegrationNotFound as err: _LOGGER.error( - "Not setting up %s because we are unable to resolve " "(sub)dependency %s", + "Not setting up %s because we are unable to resolve (sub)dependency %s", domain, err.domain, ) return False except loader.CircularDependency as err: _LOGGER.error( - "Not setting up %s because it contains a circular dependency: " "%s -> %s", + "Not setting up %s because it contains a circular dependency: %s -> %s", domain, err.from_domain, err.to_domain, diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 791b36a4236df4..49f9d7d5f99cf2 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -253,7 +253,7 @@ def find_next_time_expression_time( including daylight saving time. """ if not seconds or not minutes or not hours: - raise ValueError("Cannot find a next time: Time expression never " "matches!") + raise ValueError("Cannot find a next time: Time expression never matches!") def _lower_bound(arr: List[int], cmp: int) -> Optional[int]: """Return the first value in arr greater or equal to cmp. diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 65422f231ba573..6b921ade961f0f 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -258,7 +258,7 @@ def _load_secret_yaml(secret_path: str) -> JSON_TYPE: _LOGGER.setLevel(logging.DEBUG) else: _LOGGER.error( - "secrets.yaml: 'logger: debug' expected," " but 'logger: %s' found", + "secrets.yaml: 'logger: debug' expected, but 'logger: %s' found", logger, ) del secrets["logger"] @@ -276,7 +276,7 @@ def secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: if node.value in secrets: _LOGGER.debug( - "Secret %s retrieved from secrets.yaml in " "folder %s", + "Secret %s retrieved from secrets.yaml in folder %s", node.value, secret_path, ) diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index f6970b50a3cd12..cfbd112100ab5c 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -66,7 +66,7 @@ def validate(integrations: Dict[str, Integration], config: Config): if fp.read().strip() != content: config.add_error( "codeowners", - "File CODEOWNERS is not up to date. " "Run python3 -m script.hassfest", + "File CODEOWNERS is not up to date. Run python3 -m script.hassfest", fixable=True, ) return diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py index 7578d52ed4fd57..5ee2076ecf4b26 100644 --- a/script/hassfest/ssdp.py +++ b/script/hassfest/ssdp.py @@ -68,7 +68,7 @@ def validate(integrations: Dict[str, Integration], config: Config): if fp.read().strip() != content: config.add_error( "ssdp", - "File ssdp.py is not up to date. " "Run python3 -m script.hassfest", + "File ssdp.py is not up to date. Run python3 -m script.hassfest", fixable=True, ) return diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py index f864d3e0327d17..2a1bb936871e33 100644 --- a/script/hassfest/zeroconf.py +++ b/script/hassfest/zeroconf.py @@ -122,7 +122,7 @@ def validate(integrations: Dict[str, Integration], config: Config): if current != content: config.add_error( "zeroconf", - "File zeroconf.py is not up to date. " "Run python3 -m script.hassfest", + "File zeroconf.py is not up to date. Run python3 -m script.hassfest", fixable=True, ) return diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index d02188a3079166..dc0cf09f28d2c2 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -302,7 +302,7 @@ def test_aprs_listener_rx_msg_no_position(): def test_setup_scanner(): """Test setup_scanner.""" with patch( - "homeassistant.components." "aprs.device_tracker.AprsListenerThread" + "homeassistant.components.aprs.device_tracker.AprsListenerThread" ) as listener: hass = get_test_home_assistant() hass.start() diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index e12ce6684d2387..d84fd18fb6b5d7 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -39,7 +39,7 @@ async def test_if_fires_using_at(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.platform }} - " "{{ trigger.now.hour }}" + "some": "{{ trigger.platform }} - {{ trigger.now.hour }}" }, }, } diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 9759d3281e62ac..2299ba4c9a25fd 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -14,12 +14,11 @@ def test_state(self): sensor = binary_sensor.BinarySensorDevice() assert STATE_OFF == sensor.state with mock.patch( - "homeassistant.components.binary_sensor." "BinarySensorDevice.is_on", + "homeassistant.components.binary_sensor.BinarySensorDevice.is_on", new=False, ): assert STATE_OFF == binary_sensor.BinarySensorDevice().state with mock.patch( - "homeassistant.components.binary_sensor." "BinarySensorDevice.is_on", - new=True, + "homeassistant.components.binary_sensor.BinarySensorDevice.is_on", new=True, ): assert STATE_ON == binary_sensor.BinarySensorDevice().state diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index 2dd63583741910..6faac295d54e19 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -12,7 +12,7 @@ def radar_map_url(dim: int = 512) -> str: """Build map url, defaulting to 512 wide (as in component).""" - return ("https://api.buienradar.nl/" "image/1.0/RadarMapNL?w={dim}&h={dim}").format( + return ("https://api.buienradar.nl/image/1.0/RadarMapNL?w={dim}&h={dim}").format( dim=dim ) diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 4bd13c35ad8be5..de48a1d48f394e 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -119,7 +119,7 @@ def test_get_image_from_camera(self, mock_camera): def test_get_image_without_exists_camera(self): """Try to get image without exists camera.""" with patch( - "homeassistant.helpers.entity_component.EntityComponent." "get_entity", + "homeassistant.helpers.entity_component.EntityComponent.get_entity", return_value=None, ), pytest.raises(HomeAssistantError): asyncio.run_coroutine_threadsafe( diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 515489035fb373..b82b2b5481e357 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -321,7 +321,7 @@ async def test_websocket_status( client = await hass_ws_client(hass) with patch.dict( - "homeassistant.components.google_assistant.const." "DOMAIN_TO_GOOGLE_TYPES", + "homeassistant.components.google_assistant.const.DOMAIN_TO_GOOGLE_TYPES", {"light": None}, clear=True, ), patch.dict( @@ -683,7 +683,7 @@ async def test_list_google_entities(hass, hass_ws_client, setup_api, mock_cloud_ hass, MockConfig(should_expose=lambda *_: False), State("light.kitchen", "on") ) with patch( - "homeassistant.components.google_assistant.helpers" ".async_get_entities", + "homeassistant.components.google_assistant.helpers.async_get_entities", return_value=[entity], ): await client.send_json({"id": 5, "type": "cloud/google_assistant/entities"}) @@ -779,7 +779,7 @@ async def test_list_alexa_entities(hass, hass_ws_client, setup_api, mock_cloud_l hass, MagicMock(entity_config={}), State("light.kitchen", "on") ) with patch( - "homeassistant.components.alexa.entities" ".async_get_entities", + "homeassistant.components.alexa.entities.async_get_entities", return_value=[entity], ): await client.send_json({"id": 5, "type": "cloud/alexa/entities"}) diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 60402844d24832..a70e7ea4b5d760 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -209,7 +209,7 @@ def test_play_media(self): assert "some_id" == state.attributes.get("media_content_id") @patch( - "homeassistant.components.demo.media_player.DemoYoutubePlayer." "media_seek", + "homeassistant.components.demo.media_player.DemoYoutubePlayer.media_seek", autospec=True, ) def test_seek(self, mock_seek): diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index e839a88536eeb9..c82f36f92e7833 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -77,7 +77,7 @@ async def test_reading_broken_yaml_config(hass): "badkey.yaml": "@:\n name: Device", "noname.yaml": "my_device:\n", "allok.yaml": "My Device:\n name: Device", - "oneok.yaml": ("My Device!:\n name: Device\n" "bad_device:\n nme: Device"), + "oneok.yaml": ("My Device!:\n name: Device\nbad_device:\n nme: Device"), } args = {"hass": hass, "consider_home": timedelta(seconds=60)} with patch_yaml_files(files): @@ -341,7 +341,7 @@ async def test_group_all_devices(hass, mock_device_tracker_conf): assert (entity_id,) == state.attributes.get(ATTR_ENTITY_ID) -@patch("homeassistant.components.device_tracker.legacy." "DeviceTracker.async_see") +@patch("homeassistant.components.device_tracker.legacy.DeviceTracker.async_see") async def test_see_service(mock_see, hass): """Test the see service with a unicode dev_id and NO MAC.""" with assert_setup_component(1, device_tracker.DOMAIN): diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 6c1a17c0538798..2fb5c48e768390 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -56,7 +56,7 @@ def hass_hue(loop, hass): ) ) - with patch("homeassistant.components" ".emulated_hue.UPNPResponderThread"): + with patch("homeassistant.components.emulated_hue.UPNPResponderThread"): loop.run_until_complete( setup.async_setup_component( hass, diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 09c0731e4cdc8c..6fa6d9695390ca 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -14,7 +14,7 @@ def test_config_google_home_entity_id_to_number(): "homeassistant.components.emulated_hue.load_json", return_value={"1": "light.test2"}, ) as json_loader: - with patch("homeassistant.components.emulated_hue" ".save_json") as json_saver: + with patch("homeassistant.components.emulated_hue.save_json") as json_saver: number = conf.entity_id_to_number("light.test") assert number == "2" @@ -48,7 +48,7 @@ def test_config_google_home_entity_id_to_number_altered(): "homeassistant.components.emulated_hue.load_json", return_value={"21": "light.test2"}, ) as json_loader: - with patch("homeassistant.components.emulated_hue" ".save_json") as json_saver: + with patch("homeassistant.components.emulated_hue.save_json") as json_saver: number = conf.entity_id_to_number("light.test") assert number == "22" assert json_saver.call_count == 1 @@ -80,7 +80,7 @@ def test_config_google_home_entity_id_to_number_empty(): with patch( "homeassistant.components.emulated_hue.load_json", return_value={} ) as json_loader: - with patch("homeassistant.components.emulated_hue" ".save_json") as json_saver: + with patch("homeassistant.components.emulated_hue.save_json") as json_saver: number = conf.entity_id_to_number("light.test") assert number == "1" assert json_saver.call_count == 1 diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 2fc9d903d3b605..5897b80659a195 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -32,7 +32,7 @@ def setUpClass(cls): hass, http.DOMAIN, {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}} ) - with patch("homeassistant.components" ".emulated_hue.UPNPResponderThread"): + with patch("homeassistant.components.emulated_hue.UPNPResponderThread"): setup.setup_component( hass, emulated_hue.DOMAIN, diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py index d82f70e7ca1a94..6b248ba1c3c3fd 100644 --- a/tests/components/facebox/test_image_processing.py +++ b/tests/components/facebox/test_image_processing.py @@ -81,7 +81,7 @@ def mock_healthybox(): """Mock fb.check_box_health.""" check_box_health = ( - "homeassistant.components.facebox.image_processing." "check_box_health" + "homeassistant.components.facebox.image_processing.check_box_health" ) with patch(check_box_health, return_value=MOCK_BOX_ID) as _mock_healthybox: yield _mock_healthybox diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index 62412e53900974..048be11e079c84 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -50,7 +50,7 @@ def tearDown(self): def test_setup_one_feed(self): """Test the general setup of this component.""" with patch( - "homeassistant.components.feedreader." "track_time_interval" + "homeassistant.components.feedreader.track_time_interval" ) as track_method: assert setup_component(self.hass, feedreader.DOMAIN, VALID_CONFIG_1) track_method.assert_called_once_with( @@ -60,7 +60,7 @@ def test_setup_one_feed(self): def test_setup_scan_interval(self): """Test the setup of this component with scan interval.""" with patch( - "homeassistant.components.feedreader." "track_time_interval" + "homeassistant.components.feedreader.track_time_interval" ) as track_method: assert setup_component(self.hass, feedreader.DOMAIN, VALID_CONFIG_2) track_method.assert_called_once_with( @@ -88,7 +88,7 @@ def record_event(event): data_file = self.hass.config.path("{}.pickle".format(feedreader.DOMAIN)) storage = StoredData(data_file) with patch( - "homeassistant.components.feedreader." "track_time_interval" + "homeassistant.components.feedreader.track_time_interval" ) as track_method: manager = FeedManager( feed_data, DEFAULT_SCAN_INTERVAL, max_entries, self.hass, storage @@ -131,7 +131,7 @@ def test_feed_updates(self): # Must patch 'get_timestamp' method because the timestamp is stored # with the URL which in these tests is the raw XML data. with patch( - "homeassistant.components.feedreader.StoredData." "get_timestamp", + "homeassistant.components.feedreader.StoredData.get_timestamp", return_value=time.struct_time((2018, 4, 30, 5, 10, 0, 0, 120, 0)), ): manager2, events2 = self.setup_manager(feed_data2) @@ -139,7 +139,7 @@ def test_feed_updates(self): # 3. Run feed_data3 = load_fixture("feedreader1.xml") with patch( - "homeassistant.components.feedreader.StoredData." "get_timestamp", + "homeassistant.components.feedreader.StoredData.get_timestamp", return_value=time.struct_time((2018, 4, 30, 5, 11, 0, 0, 120, 0)), ): manager3, events3 = self.setup_manager(feed_data3) diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index a5f23b464dd8ff..9ae4245ed7025c 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -117,11 +117,11 @@ def test_chain_history(self, missing=False): } with patch( - "homeassistant.components.history." "state_changes_during_period", + "homeassistant.components.history.state_changes_during_period", return_value=fake_states, ): with patch( - "homeassistant.components.history." "get_last_state_changes", + "homeassistant.components.history.get_last_state_changes", return_value=fake_states, ): with assert_setup_component(1, "sensor"): @@ -165,11 +165,11 @@ def test_history_time(self): ] } with patch( - "homeassistant.components.history." "state_changes_during_period", + "homeassistant.components.history.state_changes_during_period", return_value=fake_states, ): with patch( - "homeassistant.components.history." "get_last_state_changes", + "homeassistant.components.history.get_last_state_changes", return_value=fake_states, ): with assert_setup_component(1, "sensor"): diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index d7ef853012e4a9..091270c12c4660 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -20,7 +20,7 @@ def hassio_env(): "homeassistant.components.hassio.HassIO.is_connected", Mock(return_value=mock_coro({"result": "ok", "data": {}})), ), patch.dict(os.environ, {"HASSIO_TOKEN": "123456"}), patch( - "homeassistant.components.hassio.HassIO." "get_homeassistant_info", + "homeassistant.components.hassio.HassIO.get_homeassistant_info", Mock(side_effect=HassioAPIError()), ): yield diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 2a2fdc4deaa435..a0d6444004137e 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -40,7 +40,7 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): assert aioclient_mock.call_count == 0 with patch( - "homeassistant.components.mqtt." "config_flow.FlowHandler.async_step_hassio", + "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", Mock(return_value=mock_coro({"type": "abort"})), ) as mock_mqtt: hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -93,10 +93,10 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client "homeassistant.components.hassio.HassIO.update_hass_api", Mock(return_value=mock_coro({"result": "ok"})), ), patch( - "homeassistant.components.hassio.HassIO." "get_homeassistant_info", + "homeassistant.components.hassio.HassIO.get_homeassistant_info", Mock(side_effect=HassioAPIError()), ), patch( - "homeassistant.components.mqtt." "config_flow.FlowHandler.async_step_hassio", + "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", Mock(return_value=mock_coro({"type": "abort"})), ) as mock_mqtt: await hass.async_start() @@ -143,7 +143,7 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): ) with patch( - "homeassistant.components.mqtt." "config_flow.FlowHandler.async_step_hassio", + "homeassistant.components.mqtt.config_flow.FlowHandler.async_step_hassio", Mock(return_value=mock_coro({"type": "abort"})), ) as mock_mqtt: resp = await hassio_client.post( diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 6b9c52b1042ae2..4456b256f6ee75 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -387,7 +387,7 @@ async def test_public_transport(hass, requests_mock_credentials_check): assert sensor.attributes.get(ATTR_DURATION) == 89.16666666666667 assert sensor.attributes.get(ATTR_DISTANCE) == 22.325 assert sensor.attributes.get(ATTR_ROUTE) == ( - "332 - Palmer/Schiller; 332 - Cargo Rd./Delta Cargo; " "332 - Palmer/Schiller" + "332 - Palmer/Schiller; 332 - Cargo Rd./Delta Cargo; 332 - Palmer/Schiller" ) assert sensor.attributes.get(CONF_UNIT_SYSTEM) == "metric" assert sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == 89.16666666666667 diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 492f928c9f0bcc..588e0df81dbfda 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -50,7 +50,7 @@ def test_setup(self): assert state.state == STATE_UNKNOWN @patch( - "homeassistant.helpers.template.TemplateEnvironment." "is_safe_callable", + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, ) def test_period_parsing(self, mock): @@ -58,7 +58,7 @@ def test_period_parsing(self, mock): now = datetime(2019, 1, 1, 23, 30, 0, tzinfo=pytz.utc) with patch("homeassistant.util.dt.now", return_value=now): today = Template( - "{{ now().replace(hour=0).replace(minute=0)" ".replace(second=0) }}", + "{{ now().replace(hour=0).replace(minute=0).replace(second=0) }}", self.hass, ) duration = timedelta(hours=2, minutes=1) @@ -137,7 +137,7 @@ def test_measure(self): assert sensor4._type == "ratio" with patch( - "homeassistant.components.history." "state_changes_during_period", + "homeassistant.components.history.state_changes_during_period", return_value=fake_states, ): with patch("homeassistant.components.history.get_state", return_value=None): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 0c5810a5b10415..f67e0e2478d57a 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -97,7 +97,7 @@ async def test_home_accessory(hass, hk_driver): hass.states.async_set(entity_id, "on") await hass.async_block_till_done() with patch( - "homeassistant.components.homekit.accessories." "HomeAccessory.update_state" + "homeassistant.components.homekit.accessories.HomeAccessory.update_state" ) as mock_update_state: await hass.async_add_job(acc.run) await hass.async_block_till_done() @@ -343,7 +343,7 @@ def test_home_driver(): # pair with patch("pyhap.accessory_driver.AccessoryDriver.pair") as mock_pair, patch( - "homeassistant.components.homekit.accessories." "dismiss_setup_message" + "homeassistant.components.homekit.accessories.dismiss_setup_message" ) as mock_dissmiss_msg: driver.pair("client_uuid", "client_public") @@ -352,7 +352,7 @@ def test_home_driver(): # unpair with patch("pyhap.accessory_driver.AccessoryDriver.unpair") as mock_unpair, patch( - "homeassistant.components.homekit.accessories." "show_setup_message" + "homeassistant.components.homekit.accessories.show_setup_message" ) as mock_show_msg: driver.unpair("client_uuid") diff --git a/tests/components/honeywell/test_climate.py b/tests/components/honeywell/test_climate.py index feba4f6410e2d4..058988203e5d36 100644 --- a/tests/components/honeywell/test_climate.py +++ b/tests/components/honeywell/test_climate.py @@ -27,7 +27,7 @@ class TestHoneywell(unittest.TestCase): """A test class for Honeywell themostats.""" @mock.patch("somecomfort.SomeComfort") - @mock.patch("homeassistant.components.honeywell." "climate.HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def test_setup_us(self, mock_ht, mock_sc): """Test for the US setup.""" config = { @@ -98,7 +98,7 @@ def test_setup_us_failures(self, mock_sc): assert not add_entities.called @mock.patch("somecomfort.SomeComfort") - @mock.patch("homeassistant.components.honeywell." "climate.HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def _test_us_filtered_devices(self, mock_ht, mock_sc, loc=None, dev=None): """Test for US filtered thermostats.""" config = { @@ -157,7 +157,7 @@ def test_us_filtered_location_2(self): assert [mock.sentinel.loc2dev1] == devices @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate." "HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def test_eu_setup_full_config(self, mock_round, mock_evo): """Test the EU setup with complete configuration.""" config = { @@ -184,7 +184,7 @@ def test_eu_setup_full_config(self, mock_round, mock_evo): assert 2 == add_entities.call_count @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate." "HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def test_eu_setup_partial_config(self, mock_round, mock_evo): """Test the EU setup with partial configuration.""" config = { @@ -206,7 +206,7 @@ def test_eu_setup_partial_config(self, mock_round, mock_evo): ) @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate." "HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def test_eu_setup_bad_temp(self, mock_round, mock_evo): """Test the EU setup with invalid temperature.""" config = { @@ -219,7 +219,7 @@ def test_eu_setup_bad_temp(self, mock_round, mock_evo): honeywell.PLATFORM_SCHEMA(config) @mock.patch("evohomeclient.EvohomeClient") - @mock.patch("homeassistant.components.honeywell.climate." "HoneywellUSThermostat") + @mock.patch("homeassistant.components.honeywell.climate.HoneywellUSThermostat") def test_eu_setup_error(self, mock_round, mock_evo): """Test the EU setup with errors.""" config = { diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index 2d869c1a0622b5..0f0191f3b82878 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -94,7 +94,7 @@ async def test_setup(hass): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "georss_ign_sismologia_client." "IgnSismologiaFeed" + "georss_ign_sismologia_client.IgnSismologiaFeed" ) as mock_feed: mock_feed.return_value.update.return_value = ( "OK", @@ -199,7 +199,7 @@ async def test_setup_with_custom_location(hass): # Set up some mock feed entries for this test. mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 20.5, (38.1, -3.1)) - with patch("georss_ign_sismologia_client." "IgnSismologiaFeed") as mock_feed: + with patch("georss_ign_sismologia_client.IgnSismologiaFeed") as mock_feed: mock_feed.return_value.update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 389fa43945eea8..3151b030637b1a 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -175,7 +175,7 @@ async def test_islamic_prayer_times_sensor_update(hass): future = midnight_dt + timedelta(days=1, minutes=1) with patch( - "homeassistant.components.islamic_prayer_times.sensor" ".dt_util.utcnow", + "homeassistant.components.islamic_prayer_times.sensor.dt_util.utcnow", return_value=future, ): diff --git a/tests/components/logentries/test_init.py b/tests/components/logentries/test_init.py index 7125822e93e06b..f850a7dd62b327 100644 --- a/tests/components/logentries/test_init.py +++ b/tests/components/logentries/test_init.py @@ -70,7 +70,7 @@ def test_event_listener(self, mock_dump, mock_requests): } ] payload = { - "host": "https://webhook.logentries.com/noformat/" "logs/token", + "host": "https://webhook.logentries.com/noformat/logs/token", "event": body, } self.handler_method(event) diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index 258e0cc7ebfe3f..6536e1317fa3e7 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -38,7 +38,7 @@ async def test_get_messages_from_mailbox(mock_http_client): async def test_get_media_from_mailbox(mock_http_client): """Get audio from mailbox.""" mp3sha = "3f67c4ea33b37d1710f772a26dd3fb43bb159d50" - msgtxt = "Message 1. " "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + msgtxt = "Message 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " msgsha = sha1(msgtxt.encode("utf-8")).hexdigest() url = "/api/mailbox/media/DemoMailbox/%s" % (msgsha) @@ -50,8 +50,8 @@ async def test_get_media_from_mailbox(mock_http_client): async def test_delete_from_mailbox(mock_http_client): """Get audio from mailbox.""" - msgtxt1 = "Message 1. " "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - msgtxt2 = "Message 3. " "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + msgtxt1 = "Message 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + msgtxt2 = "Message 3. Lorem ipsum dolor sit amet, consectetur adipiscing elit. " msgsha1 = sha1(msgtxt1.encode("utf-8")).hexdigest() msgsha2 = sha1(msgtxt2.encode("utf-8")).hexdigest() diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index 1b06477750b9c9..f1596277e3c39b 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -111,7 +111,7 @@ async def test_arm_home_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -252,7 +252,7 @@ async def test_arm_away_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -367,7 +367,7 @@ async def test_arm_night_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -432,7 +432,7 @@ async def test_trigger_no_pending(hass): future = dt_util.utcnow() + timedelta(seconds=60) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -474,7 +474,7 @@ async def test_trigger_with_delay(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -563,7 +563,7 @@ async def test_trigger_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -574,7 +574,7 @@ async def test_trigger_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -618,7 +618,7 @@ async def test_trigger_with_unused_specific_delay(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -662,7 +662,7 @@ async def test_trigger_with_specific_delay(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -706,7 +706,7 @@ async def test_trigger_with_pending_and_delay(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -718,7 +718,7 @@ async def test_trigger_with_pending_and_delay(hass): future += timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -763,7 +763,7 @@ async def test_trigger_with_pending_and_specific_delay(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -775,7 +775,7 @@ async def test_trigger_with_pending_and_specific_delay(hass): future += timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -808,7 +808,7 @@ async def test_armed_home_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -840,7 +840,7 @@ async def test_armed_away_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -872,7 +872,7 @@ async def test_armed_night_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -906,7 +906,7 @@ async def test_trigger_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -916,7 +916,7 @@ async def test_trigger_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -951,7 +951,7 @@ async def test_trigger_with_disarm_after_trigger(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1013,7 +1013,7 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1048,7 +1048,7 @@ async def test_trigger_with_specific_trigger_time(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1087,7 +1087,7 @@ async def test_trigger_with_no_disarm_after_trigger(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1126,7 +1126,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1140,7 +1140,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1178,7 +1178,7 @@ async def test_disarm_while_pending_trigger(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1217,7 +1217,7 @@ async def test_disarm_during_trigger_with_invalid_code(hass): future = dt_util.utcnow() + timedelta(seconds=5) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1342,7 +1342,7 @@ async def test_arm_custom_bypass_with_pending(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1400,7 +1400,7 @@ async def test_armed_custom_bypass_with_specific_pending(hass): future = dt_util.utcnow() + timedelta(seconds=2) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1448,7 +1448,7 @@ async def test_arm_away_after_disabled_disarmed(hass): future = dt_util.utcnow() + timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) @@ -1466,7 +1466,7 @@ async def test_arm_away_after_disabled_disarmed(hass): future += timedelta(seconds=1) with patch( - ("homeassistant.components.manual.alarm_control_panel." "dt_util.utcnow"), + ("homeassistant.components.manual.alarm_control_panel.dt_util.utcnow"), return_value=future, ): async_fire_time_changed(hass, future) diff --git a/tests/components/microsoft_face/test_init.py b/tests/components/microsoft_face/test_init.py index 24d67f56fb5acc..3e2cdf0d5306c6 100644 --- a/tests/components/microsoft_face/test_init.py +++ b/tests/components/microsoft_face/test_init.py @@ -96,7 +96,7 @@ def teardown_method(self): self.hass.stop() @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_component(self, mock_update): @@ -105,7 +105,7 @@ def test_setup_component(self, mock_update): setup_component(self.hass, mf.DOMAIN, self.config) @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_component_wrong_api_key(self, mock_update): @@ -114,7 +114,7 @@ def test_setup_component_wrong_api_key(self, mock_update): setup_component(self.hass, mf.DOMAIN, {mf.DOMAIN: {}}) @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_component_test_service(self, mock_update): @@ -170,7 +170,7 @@ def test_setup_component_test_entities(self, aioclient_mock): ) @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_service_groups(self, mock_update, aioclient_mock): @@ -257,7 +257,7 @@ def test_service_person(self, aioclient_mock): assert "Hans" not in entity_group1.attributes @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_service_train(self, mock_update, aioclient_mock): @@ -317,7 +317,7 @@ def test_service_face(self, camera_mock, aioclient_mock): assert aioclient_mock.mock_calls[3][2] == b"Test" @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_service_status_400(self, mock_update, aioclient_mock): @@ -339,7 +339,7 @@ def test_service_status_400(self, mock_update, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_service_status_timeout(self, mock_update, aioclient_mock): diff --git a/tests/components/microsoft_face_detect/test_image_processing.py b/tests/components/microsoft_face_detect/test_image_processing.py index 1b01ee7434c07d..384e0ba130f272 100644 --- a/tests/components/microsoft_face_detect/test_image_processing.py +++ b/tests/components/microsoft_face_detect/test_image_processing.py @@ -28,7 +28,7 @@ def teardown_method(self): self.hass.stop() @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_platform(self, store_mock): @@ -49,7 +49,7 @@ def test_setup_platform(self, store_mock): assert self.hass.states.get("image_processing.microsoftface_demo_camera") @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_platform_name(self, store_mock): diff --git a/tests/components/microsoft_face_identify/test_image_processing.py b/tests/components/microsoft_face_identify/test_image_processing.py index 311d463bc1de5e..d1054cf8dc4514 100644 --- a/tests/components/microsoft_face_identify/test_image_processing.py +++ b/tests/components/microsoft_face_identify/test_image_processing.py @@ -28,7 +28,7 @@ def teardown_method(self): self.hass.stop() @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_platform(self, store_mock): @@ -49,7 +49,7 @@ def test_setup_platform(self, store_mock): assert self.hass.states.get("image_processing.microsoftface_demo_camera") @patch( - "homeassistant.components.microsoft_face." "MicrosoftFace.update_store", + "homeassistant.components.microsoft_face.MicrosoftFace.update_store", return_value=mock_coro(), ) def test_setup_platform_name(self, store_mock): diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 3e8f342ea9424c..3bfe32633b3438 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -78,7 +78,7 @@ async def expires_helper(hass, mqtt_mock, caplog): """Run the basic expiry code.""" now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): async_fire_time_changed(hass, now) async_fire_mqtt_message(hass, "test-topic", "ON") await hass.async_block_till_done() @@ -97,7 +97,7 @@ async def expires_helper(hass, mqtt_mock, caplog): assert state.state == STATE_ON # Next message resets timer - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): async_fire_time_changed(hass, now) async_fire_mqtt_message(hass, "test-topic", "OFF") await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 2db368d0311e16..29962287dd7bcb 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -113,7 +113,7 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): assert state.state == "off" with pytest.raises(vol.Invalid) as excinfo: await common.async_set_hvac_mode(hass, None, ENTITY_CLIMATE) - assert ("value is not allowed for dictionary value @ " "data['hvac_mode']") in str( + assert ("value is not allowed for dictionary value @ data['hvac_mode']") in str( excinfo.value ) state = hass.states.get(ENTITY_CLIMATE) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 71348fcf5cbda4..f4324bd8634161 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -27,7 +27,7 @@ async def mock_setup_scanner(hass, config, see, discovery_info=None): assert "qos" in config with patch( - "homeassistant.components.mqtt.device_tracker." "async_setup_scanner", + "homeassistant.components.mqtt.device_tracker.async_setup_scanner", autospec=True, side_effect=mock_setup_scanner, ) as mock_sp: diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 2b6c65b919e220..6320be3b7728cb 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -179,9 +179,7 @@ async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( - hass, - "homeassistant/binary_sensor/my_node_id/bla" "/config", - '{ "name": "Beer" }', + hass, "homeassistant/binary_sensor/my_node_id/bla/config", '{ "name": "Beer" }', ) await hass.async_block_till_done() @@ -212,7 +210,7 @@ async def test_non_duplicate_discovery(hass, mqtt_mock, caplog): assert state is not None assert state.name == "Beer" assert state_duplicate is None - assert "Component has already been discovered: " "binary_sensor bla" in caplog.text + assert "Component has already been discovered: binary_sensor bla" in caplog.text async def test_discovery_expansion(hass, mqtt_mock, caplog): @@ -424,7 +422,7 @@ async def test_complex_discovery_topic_prefix(hass, mqtt_mock, caplog): async_fire_mqtt_message( hass, - ("my_home/homeassistant/register" "/binary_sensor/node1/object1/config"), + ("my_home/homeassistant/register/binary_sensor/node1/object1/config"), '{ "name": "Beer" }', ) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index b03cd4b8d736d2..43ccaf6dea51c2 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -555,7 +555,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) with patch( - "homeassistant.helpers.restore_state.RestoreEntity" ".async_get_last_state", + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", return_value=mock_coro(fake_state), ): with assert_setup_component(1, light.DOMAIN): diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 52adeb61514c3c..355451f64697e8 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -299,7 +299,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ) with patch( - "homeassistant.helpers.restore_state.RestoreEntity" ".async_get_last_state", + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", return_value=mock_coro(fake_state), ): assert await async_setup_component( diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 5148f45e6e7794..1d109af5930b91 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -261,7 +261,7 @@ async def test_optimistic(hass, mqtt_mock): ) with patch( - "homeassistant.helpers.restore_state.RestoreEntity" ".async_get_last_state", + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", return_value=mock_coro(fake_state), ): with assert_setup_component(1, light.DOMAIN): diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 4e8e5f9bfd0197..66f8996bc2e162 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -63,7 +63,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): assert state.state == "unknown" now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): async_fire_time_changed(hass, now) async_fire_mqtt_message(hass, "test-topic", "100") await hass.async_block_till_done() @@ -82,7 +82,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): assert state.state == "100" # Next message resets timer - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=now): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=now): async_fire_time_changed(hass, now) async_fire_mqtt_message(hass, "test-topic", "101") await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 35cbea5a82b516..25fc3212f05b72 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -69,7 +69,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mock_publish): fake_state = ha.State("switch.test", "on") with patch( - "homeassistant.helpers.restore_state.RestoreEntity" ".async_get_last_state", + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", return_value=mock_coro(fake_state), ): assert await async_setup_component( diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index 7f3f806da5268f..5af196c5bf2ba4 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -46,7 +46,7 @@ async def mock_setup_scanner(hass, config, see, discovery_info=None): assert "qos" in config with patch( - "homeassistant.components.mqtt_json.device_tracker." "async_setup_scanner", + "homeassistant.components.mqtt_json.device_tracker.async_setup_scanner", autospec=True, side_effect=mock_setup_scanner, ) as mock_sp: diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index d48c3c43a25cdc..21bb5bcf9939fe 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -126,7 +126,7 @@ async def test_user_not_supports_encryption(hass, not_supports_encryption): async def test_unload(hass): """Test unloading a config flow.""" with patch( - "homeassistant.config_entries.ConfigEntries" ".async_forward_entry_setup" + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" ) as mock_forward: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "import"}, data={} @@ -140,7 +140,7 @@ async def test_unload(hass): assert entry.data["webhook_id"] in hass.data["webhook"] with patch( - "homeassistant.config_entries.ConfigEntries" ".async_forward_entry_unload", + "homeassistant.config_entries.ConfigEntries.async_forward_entry_unload", return_value=mock_coro(), ) as mock_unload: assert await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 3a7f3c030ed2bd..730da4bc7b2ebc 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1300,7 +1300,7 @@ async def test_single_waypoint_import(hass, context): async def test_not_implemented_message(hass, context): """Handle not implemented message type.""" patch_handler = patch( - "homeassistant.components.owntracks." "messages.async_handle_not_impl_msg", + "homeassistant.components.owntracks.messages.async_handle_not_impl_msg", return_value=mock_coro(False), ) patch_handler.start() @@ -1311,7 +1311,7 @@ async def test_not_implemented_message(hass, context): async def test_unsupported_message(hass, context): """Handle not implemented message type.""" patch_handler = patch( - "homeassistant.components.owntracks." "messages.async_handle_unsupported_msg", + "homeassistant.components.owntracks.messages.async_handle_unsupported_msg", return_value=mock_coro(False), ) patch_handler.start() @@ -1396,7 +1396,7 @@ def config_context(hass, setup_comp): patch_load.start() patch_save = patch( - "homeassistant.components.device_tracker." "DeviceTracker.async_update_config" + "homeassistant.components.device_tracker.DeviceTracker.async_update_config" ) patch_save.start() diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 86ab16c5f1bcce..ad9bdd7c536346 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -89,7 +89,7 @@ async def test_setup(hass): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "georss_qld_bushfire_alert_client." "QldBushfireAlertFeed" + "georss_qld_bushfire_alert_client.QldBushfireAlertFeed" ) as mock_feed: mock_feed.return_value.update.return_value = ( "OK", @@ -193,7 +193,7 @@ async def test_setup_with_custom_location(hass): "1234", "Title 1", 20.5, (38.1, -3.1), category="Category 1" ) - with patch("georss_qld_bushfire_alert_client." "QldBushfireAlertFeed") as mock_feed: + with patch("georss_qld_bushfire_alert_client.QldBushfireAlertFeed") as mock_feed: mock_feed.return_value.update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index ecd0c501ee8453..114daa7d2f7d98 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -182,7 +182,7 @@ def json(self): "sqliteVersion": "3.16.2", "urlBase": "", "runtimeVersion": ( - "4.6.1 " "(Stable 4.6.1.3/abb06f1 " "Mon Oct 3 07:57:59 UTC 2016)" + "4.6.1 (Stable 4.6.1.3/abb06f1 Mon Oct 3 07:57:59 UTC 2016)" ), }, 200, diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index d1fdec579c9db8..18c4f94631853b 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -126,7 +126,7 @@ def callback(event): now = dt_util.utcnow() # fake time and turn on sensor future = now + timedelta(seconds=0) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=future): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future): async_fire_time_changed(hass, future) event_callback(on_event) await hass.async_block_till_done() @@ -136,7 +136,7 @@ def callback(event): # fake time and turn on sensor again future = now + timedelta(seconds=15) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=future): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future): async_fire_time_changed(hass, future) event_callback(on_event) await hass.async_block_till_done() @@ -146,7 +146,7 @@ def callback(event): # fake time and verify sensor still on (de-bounce) future = now + timedelta(seconds=35) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=future): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() state = hass.states.get("binary_sensor.test2") @@ -155,7 +155,7 @@ def callback(event): # fake time and verify sensor is off future = now + timedelta(seconds=45) - with patch(("homeassistant.helpers.event." "dt_util.utcnow"), return_value=future): + with patch(("homeassistant.helpers.event.dt_util.utcnow"), return_value=future): async_fire_time_changed(hass, future) await hass.async_block_till_done() state = hass.states.get("binary_sensor.test2") diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index a54bd9f778795e..50c3c6bfb55955 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -111,7 +111,7 @@ def test_template_render(self, mock_call): shell_command.DOMAIN, { shell_command.DOMAIN: { - "test_service": ("ls /bin {{ states.sensor" ".test_state.state }}") + "test_service": ("ls /bin {{ states.sensor.test_state.state }}") } }, ) diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py index 5026f19302e4ec..44c8000efa2660 100644 --- a/tests/components/shopping_list/conftest.py +++ b/tests/components/shopping_list/conftest.py @@ -11,7 +11,7 @@ def mock_shopping_list_io(): """Stub out the persistence.""" with patch("homeassistant.components.shopping_list.ShoppingData.save"), patch( - "homeassistant.components.shopping_list." "ShoppingData.async_load" + "homeassistant.components.shopping_list.ShoppingData.async_load" ): yield diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index aa06e13a812351..eac75a3b4e7697 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -45,7 +45,7 @@ async def test_capped_setup(hass, aioclient_mock): "" ) aioclient_mock.get( - "https://www.start.ca/support/usage/api?key=" "NOTAKEY", text=result + "https://www.start.ca/support/usage/api?key=NOTAKEY", text=result ) await async_setup_component(hass, "sensor", {"sensor": config}) @@ -140,7 +140,7 @@ async def test_unlimited_setup(hass, aioclient_mock): "" ) aioclient_mock.get( - "https://www.start.ca/support/usage/api?key=" "NOTAKEY", text=result + "https://www.start.ca/support/usage/api?key=NOTAKEY", text=result ) await async_setup_component(hass, "sensor", {"sensor": config}) @@ -196,9 +196,7 @@ async def test_unlimited_setup(hass, aioclient_mock): async def test_bad_return_code(hass, aioclient_mock): """Test handling a return code that isn't HTTP OK.""" - aioclient_mock.get( - "https://www.start.ca/support/usage/api?key=" "NOTAKEY", status=404 - ) + aioclient_mock.get("https://www.start.ca/support/usage/api?key=NOTAKEY", status=404) scd = StartcaData(hass.loop, async_get_clientsession(hass), "NOTAKEY", 400) @@ -209,7 +207,7 @@ async def test_bad_return_code(hass, aioclient_mock): async def test_bad_json_decode(hass, aioclient_mock): """Test decoding invalid json result.""" aioclient_mock.get( - "https://www.start.ca/support/usage/api?key=" "NOTAKEY", text="this is not xml" + "https://www.start.ca/support/usage/api?key=NOTAKEY", text="this is not xml" ) scd = StartcaData(hass.loop, async_get_clientsession(hass), "NOTAKEY", 400) diff --git a/tests/components/tomato/test_device_tracker.py b/tests/components/tomato/test_device_tracker.py index 9695780546679e..cbc8316f7c8776 100644 --- a/tests/components/tomato/test_device_tracker.py +++ b/tests/components/tomato/test_device_tracker.py @@ -51,7 +51,7 @@ def __init__(self, text, status_code): def mock_exception_logger(): """Mock pyunifi.""" with mock.patch( - "homeassistant.components.tomato.device_tracker" "._LOGGER.exception" + "homeassistant.components.tomato.device_tracker._LOGGER.exception" ) as mock_exception_logger: yield mock_exception_logger @@ -312,7 +312,7 @@ def test_config_bad_credentials(hass, mock_exception_logger): assert mock_exception_logger.call_count == 1 assert mock_exception_logger.mock_calls[0] == mock.call( - "Failed to authenticate, " "please check your username and password" + "Failed to authenticate, please check your username and password" ) @@ -382,7 +382,7 @@ def test_bad_connection(hass, mock_exception_logger): tomato.get_scanner(hass, config) assert mock_exception_logger.call_count == 1 assert mock_exception_logger.mock_calls[0] == mock.call( - "Failed to connect to the router " "or invalid http_id supplied" + "Failed to connect to the router or invalid http_id supplied" ) diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index 9428bf05483da0..97512dfc9bd1df 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -242,7 +242,7 @@ async def test_unload(hass, platform): with patch( "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( - "homeassistant.components.tplink.{}" ".async_setup_entry".format(platform), + "homeassistant.components.tplink.{}.async_setup_entry".format(platform), return_value=mock_coro(True), ) as light_setup: config = { diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index 7ebd4bbcd7c25a..1c6e572b81f49b 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -8,6 +8,6 @@ def mock_gateway_info(): """Mock get_gateway_info.""" with patch( - "homeassistant.components.tradfri.config_flow." "get_gateway_info" + "homeassistant.components.tradfri.config_flow.get_gateway_info" ) as mock_gateway: yield mock_gateway diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 151607b1ed8ed1..ad7386c530f49b 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -13,7 +13,7 @@ def mock_auth(): """Mock authenticate.""" with patch( - "homeassistant.components.tradfri.config_flow." "authenticate" + "homeassistant.components.tradfri.config_flow.authenticate" ) as mock_auth: yield mock_auth @@ -21,7 +21,7 @@ def mock_auth(): @pytest.fixture def mock_entry_setup(): """Mock entry setup.""" - with patch("homeassistant.components.tradfri." "async_setup_entry") as mock_setup: + with patch("homeassistant.components.tradfri.async_setup_entry") as mock_setup: mock_setup.return_value = mock_coro(True) yield mock_setup diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 389f9478ad3fde..f8dc11069d8e89 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -99,7 +99,7 @@ def test_setup_component_and_test_service(self): assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_en_-_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( @@ -129,7 +129,7 @@ def test_setup_component_and_test_service_with_config_language(self): assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_de_-_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( @@ -169,7 +169,7 @@ def test_setup_component_and_test_service_with_service_language(self): assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_de_-_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( @@ -232,7 +232,7 @@ def test_setup_component_and_test_service_with_service_options(self): assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_de_{}_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_{}_demo.mp3".format( self.hass.config.api.base_url, opt_hash ) assert os.path.isfile( @@ -273,7 +273,7 @@ def test_setup_component_and_test_with_service_options_def(self, def_mock): assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_de_{}_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_{}_demo.mp3".format( self.hass.config.api.base_url, opt_hash ) assert os.path.isfile( @@ -449,7 +449,7 @@ def test_setup_component_and_web_view_wrong_file(self): self.hass.start() url = ( - "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_en_-_demo.mp3" + "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3" ).format(self.hass.config.api.base_url) req = requests.get(url) @@ -465,7 +465,7 @@ def test_setup_component_and_web_view_wrong_filename(self): self.hass.start() url = ( - "{}/api/tts_proxy/265944dsk32c1b2a621be5930510bb2cd" "_en_-_demo.mp3" + "{}/api/tts_proxy/265944dsk32c1b2a621be5930510bb2cd_en_-_demo.mp3" ).format(self.hass.config.api.base_url) req = requests.get(url) @@ -542,7 +542,7 @@ def test_setup_component_test_with_cache_dir(self): setup_component(self.hass, tts.DOMAIN, config) with patch( - "homeassistant.components.demo.tts.DemoProvider." "get_tts_audio", + "homeassistant.components.demo.tts.DemoProvider.get_tts_audio", return_value=(None, None), ): self.hass.services.call( @@ -555,7 +555,7 @@ def test_setup_component_test_with_cache_dir(self): assert len(calls) == 1 assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_en_-_demo.mp3".format( + ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3".format( self.hass.config.api.base_url ) @@ -601,7 +601,7 @@ def test_setup_component_load_cache_retrieve_without_mem_cache(self): self.hass.start() url = ( - "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" "_en_-_demo.mp3" + "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3" ).format(self.hass.config.api.base_url) req = requests.get(url) diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index ccc033ccc72812..2a0bc9f8c5a641 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -44,7 +44,7 @@ async def test_auth_events( async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): """Test authenticating.""" with patch( - "homeassistant.components.websocket_api.auth." "process_wrong_login", + "homeassistant.components.websocket_api.auth.process_wrong_login", return_value=mock_coro(), ) as mock_process_wrong_login: await no_auth_websocket_client.send_json( diff --git a/tests/components/wunderground/test_sensor.py b/tests/components/wunderground/test_sensor.py index bfe9f83fbc336c..5f74a837cf346d 100644 --- a/tests/components/wunderground/test_sensor.py +++ b/tests/components/wunderground/test_sensor.py @@ -49,11 +49,9 @@ "http://api.wunderground.com/api/foo/alerts/conditions/forecast/lang" ":EN/q/32.87336,-117.22743.json" ) -PWS_URL = ( - "http://api.wunderground.com/api/foo/alerts/conditions/" "lang:EN/q/pws:bar.json" -) +PWS_URL = "http://api.wunderground.com/api/foo/alerts/conditions/lang:EN/q/pws:bar.json" INVALID_URL = ( - "http://api.wunderground.com/api/BOB/alerts/conditions/" "lang:foo/q/pws:bar.json" + "http://api.wunderground.com/api/BOB/alerts/conditions/lang:foo/q/pws:bar.json" ) diff --git a/tests/components/yr/test_sensor.py b/tests/components/yr/test_sensor.py index 7e2e8543f77519..161a7cef66bc46 100644 --- a/tests/components/yr/test_sensor.py +++ b/tests/components/yr/test_sensor.py @@ -13,7 +13,7 @@ async def test_default_setup(hass, aioclient_mock): """Test the default setup.""" aioclient_mock.get( - "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", + "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/", text=load_fixture("yr.no.xml"), ) config = {"platform": "yr", "elevation": 0} @@ -32,7 +32,7 @@ async def test_default_setup(hass, aioclient_mock): async def test_custom_setup(hass, aioclient_mock): """Test a custom setup.""" aioclient_mock.get( - "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", + "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/", text=load_fixture("yr.no.xml"), ) @@ -77,7 +77,7 @@ async def test_custom_setup(hass, aioclient_mock): async def test_forecast_setup(hass, aioclient_mock): """Test a custom setup with 24h forecast.""" aioclient_mock.get( - "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/", + "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/", text=load_fixture("yr.no.xml"), ) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index fdff064a1c5796..7e0b89f8d70cda 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -16,7 +16,7 @@ async def test_user_flow(hass): flow.hass = hass with asynctest.patch( - "homeassistant.components.zha.config_flow" ".check_zigpy_connection", + "homeassistant.components.zha.config_flow.check_zigpy_connection", return_value=False, ): result = await flow.async_step_user( @@ -26,7 +26,7 @@ async def test_user_flow(hass): assert result["errors"] == {"base": "cannot_connect"} with asynctest.patch( - "homeassistant.components.zha.config_flow" ".check_zigpy_connection", + "homeassistant.components.zha.config_flow.check_zigpy_connection", return_value=True, ): result = await flow.async_step_user( diff --git a/tests/conftest.py b/tests/conftest.py index 262acda6314f9f..558da48a7c1d6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -60,7 +60,7 @@ def verify_cleanup(): for inst in INSTANCES: inst.stop() pytest.exit( - "Detected non stopped instances " "({}), aborting test run".format(count) + "Detected non stopped instances ({}), aborting test run".format(count) ) diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 54c835895ca2bc..0182d830e4c397 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -23,7 +23,7 @@ "\n\n" ) -BAD_CORE_CONFIG = "homeassistant:\n" " unit_system: bad\n" "\n\n" +BAD_CORE_CONFIG = "homeassistant:\n unit_system: bad\n\n\n" def log_ha_config(conf): @@ -106,8 +106,7 @@ async def test_component_platform_not_found_2(hass, loop): async def test_package_invalid(hass, loop): """Test a valid platform setup.""" files = { - YAML_CONFIG_FILE: BASE_CONFIG - + (" packages:\n" " p1:\n" ' group: ["a"]') + YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]') } with patch("os.path.isfile", return_value=True), patch_yaml_files(files): res = await async_check_ha_config_file(hass) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 3846230e6d3571..7f31c32cde3dee 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -340,7 +340,7 @@ async def test_no_unnecessary_changes(registry): identifiers={("hue", "456"), ("bla", "123")}, ) with patch( - "homeassistant.helpers.device_registry" ".DeviceRegistry.async_schedule_save" + "homeassistant.helpers.device_registry.DeviceRegistry.async_schedule_save" ) as mock_save: entry2 = registry.async_get_or_create( config_entry_id="1234", identifiers={("hue", "456")} diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index a069c050cf4f03..7284a5d9b67cf5 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -115,7 +115,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( @@ -137,7 +137,7 @@ async def test_setup_does_discovery(mock_setup_component, mock_setup, hass): assert ("platform_test", {}, {"msg": "discovery_info"}) == mock_setup.call_args[0] -@asynctest.patch("homeassistant.helpers.entity_platform." "async_track_time_interval") +@asynctest.patch("homeassistant.helpers.entity_platform.async_track_time_interval") async def test_set_scan_interval_via_config(mock_track, hass): """Test the setting of the scan interval via configuration.""" @@ -460,5 +460,5 @@ async def test_extract_all_use_match_all(hass, caplog): ent.entity_id for ent in await component.async_extract_from_service(call) ) assert ( - "Not passing an entity ID to a service to target all entities is " "deprecated" + "Not passing an entity ID to a service to target all entities is deprecated" ) not in caplog.text diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 0f73699c896138..592cc24df8ef1e 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -134,7 +134,7 @@ async def test_update_state_adds_entities_with_update_before_add_false(hass): assert not ent.update.called -@asynctest.patch("homeassistant.helpers.entity_platform." "async_track_time_interval") +@asynctest.patch("homeassistant.helpers.entity_platform.async_track_time_interval") async def test_set_scan_interval_via_platform(mock_track, hass): """Test the setting of the scan interval via platform.""" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index d331da5d92d26f..cef8baec70e839 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -805,7 +805,7 @@ def action(): now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) with patch( - "homeassistant.helpers.event" ".async_track_point_in_utc_time" + "homeassistant.helpers.event.async_track_point_in_utc_time" ) as mock, patch("homeassistant.util.dt.utcnow", return_value=now): async_call_later(hass, 3, action) @@ -825,7 +825,7 @@ def action(): now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) with patch( - "homeassistant.helpers.event" ".async_track_point_in_utc_time" + "homeassistant.helpers.event.async_track_point_in_utc_time" ) as mock, patch("homeassistant.util.dt.utcnow", return_value=now): remove = async_call_later(hass, 3, action) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 2697c59b787be0..b42b30a836a12a 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -459,7 +459,7 @@ async def test_call_with_match_all( mock_entities["light.living_room"], ] assert ( - "Not passing an entity ID to a service to target " "all entities is deprecated" + "Not passing an entity ID to a service to target all entities is deprecated" ) not in caplog.text diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index cbd530d0b4c497..1c3afa472f2951 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -153,7 +153,7 @@ def test_iterating_all_states(hass): def test_iterating_domain_states(hass): """Test iterating domain states.""" - tmpl_str = "{% for state in states.sensor %}" "{{ state.state }}{% endfor %}" + tmpl_str = "{% for state in states.sensor %}{{ state.state }}{% endfor %}" info = render_to_info(hass, tmpl_str) assert_result_info(info, "", domains=["sensor"]) @@ -818,7 +818,7 @@ def test_states_function(hass): @patch( - "homeassistant.helpers.template.TemplateEnvironment." "is_safe_callable", + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, ) def test_now(mock_is_safe, hass): @@ -832,7 +832,7 @@ def test_now(mock_is_safe, hass): @patch( - "homeassistant.helpers.template.TemplateEnvironment." "is_safe_callable", + "homeassistant.helpers.template.TemplateEnvironment.is_safe_callable", return_value=True, ) def test_utcnow(mock_is_safe, hass): @@ -1260,18 +1260,18 @@ async def test_expand(hass): hass.states.async_set("test.object", "happy") info = render_to_info( - hass, "{{ expand('test.object') | map(attribute='entity_id')" " | join(', ') }}" + hass, "{{ expand('test.object') | map(attribute='entity_id') | join(', ') }}" ) assert_result_info(info, "test.object", []) info = render_to_info( hass, - "{{ expand('group.new_group') | map(attribute='entity_id')" " | join(', ') }}", + "{{ expand('group.new_group') | map(attribute='entity_id') | join(', ') }}", ) assert_result_info(info, "", ["group.new_group"]) info = render_to_info( - hass, "{{ expand(states.group) | map(attribute='entity_id')" " | join(', ') }}" + hass, "{{ expand(states.group) | map(attribute='entity_id') | join(', ') }}" ) assert_result_info(info, "", [], ["group"]) @@ -1279,12 +1279,12 @@ async def test_expand(hass): info = render_to_info( hass, - "{{ expand('group.new_group') | map(attribute='entity_id')" " | join(', ') }}", + "{{ expand('group.new_group') | map(attribute='entity_id') | join(', ') }}", ) assert_result_info(info, "test.object", ["group.new_group"]) info = render_to_info( - hass, "{{ expand(states.group) | map(attribute='entity_id')" " | join(', ') }}" + hass, "{{ expand(states.group) | map(attribute='entity_id') | join(', ') }}" ) assert_result_info(info, "test.object", ["group.new_group"], ["group"]) @@ -1437,7 +1437,7 @@ def test_closest_function_to_state(hass): assert ( template.Template( - "{{ closest(states.zone.far_away, " "states.test_domain).entity_id }}", hass + "{{ closest(states.zone.far_away, states.test_domain).entity_id }}", hass ).async_render() == "test_domain.closest_zone" ) @@ -1471,7 +1471,7 @@ def test_closest_function_state_with_invalid_location(hass): assert ( template.Template( - "{{ closest(states.test_domain.closest_home, " "states) }}", hass + "{{ closest(states.test_domain.closest_home, states) }}", hass ).async_render() == "None" ) @@ -1517,7 +1517,7 @@ def test_extract_entities_none_exclude_stuff(hass): assert ( template.extract_entities( - "{{ closest(states.zone.far_away, " "states.test_domain).entity_id }}" + "{{ closest(states.zone.far_away, states.test_domain).entity_id }}" ) == MATCH_ALL ) diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 481efc6fb30265..ea7ae03b5dba5c 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -20,7 +20,7 @@ "\n\n" ) -BAD_CORE_CONFIG = "homeassistant:\n" " unit_system: bad\n" "\n\n" +BAD_CORE_CONFIG = "homeassistant:\n unit_system: bad\n\n\n" def normalize_yaml_files(check_dict): @@ -92,8 +92,8 @@ def test_secrets(isfile_patch, loop): files = { get_test_config_dir(YAML_CONFIG_FILE): BASE_CONFIG - + ("http:\n" " cors_allowed_origins: !secret http_pw"), - secrets_path: ("logger: debug\n" "http_pw: http://google.com"), + + ("http:\n cors_allowed_origins: !secret http_pw"), + secrets_path: ("logger: debug\nhttp_pw: http://google.com"), } with patch_yaml_files(files): @@ -122,8 +122,7 @@ def test_secrets(isfile_patch, loop): def test_package_invalid(isfile_patch, loop): """Test a valid platform setup.""" files = { - YAML_CONFIG_FILE: BASE_CONFIG - + (" packages:\n" " p1:\n" ' group: ["a"]') + YAML_CONFIG_FILE: BASE_CONFIG + (" packages:\n p1:\n" ' group: ["a"]') } with patch_yaml_files(files): res = check_config.check(get_test_config_dir()) diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index ba31ce57010da8..622d87d1a27cfa 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -392,7 +392,7 @@ def test_secrets_from_unrelated_fails(self): with pytest.raises(HomeAssistantError): load_yaml( os.path.join(self._sub_folder_path, "sub.yaml"), - "http:\n" " api_password: !secret test", + "http:\n api_password: !secret test", ) def test_secrets_keyring(self): @@ -431,9 +431,9 @@ def test_bad_logger_value(self, mock_error): def test_secrets_are_not_dict(self): """Did secrets handle non-dict file.""" - FILES[self._secret_path] = ( - "- http_pw: pwhttp\n" " comp1_un: un1\n" " comp1_pw: pw1\n" - ) + FILES[ + self._secret_path + ] = "- http_pw: pwhttp\n comp1_un: un1\n comp1_pw: pw1\n" yaml.clear_secret_cache() with pytest.raises(HomeAssistantError): load_yaml( From 24d5e54eedc40c08204107366c3d411590ad3ca1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 21:16:58 +0100 Subject: [PATCH 2652/3953] Bump pre-commit to 1.21.0 (#30406) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 37268e70726937..f4fb13a417c397 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,7 +7,7 @@ asynctest==0.13.0 codecov==2.0.15 mock-open==1.3.1 mypy==0.761 -pre-commit==1.20.0 +pre-commit==1.21.0 pylint==2.4.4 astroid==2.3.3 pytest-aiohttp==0.3.0 From 5edf72c9ea717e526375aa9339812187e3dc5c5e Mon Sep 17 00:00:00 2001 From: Aaron David Schneider Date: Thu, 2 Jan 2020 21:17:37 +0100 Subject: [PATCH 2653/3953] fix issuecomment-570284682 (#30405) --- 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 e2382490cdef7d..f27e409a28d2b4 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -84,7 +84,7 @@ def get_extra_attributes(self, device): ip_device = self.fritz_box.get_specific_host_entry(device).get("NewIPAddress") if not ip_device: - return None + return {} return {"ip": ip_device, "mac": device} def _update_info(self): From 7b00e941849f6ef68f994c113bbbf19a66536f9c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 21:21:54 +0100 Subject: [PATCH 2654/3953] Migrate local_file tests from coroutine to async/await (#30392) --- tests/components/local_file/test_camera.py | 40 ++++++++++------------ 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/tests/components/local_file/test_camera.py b/tests/components/local_file/test_camera.py index 042b0f76400177..92650715e5d966 100644 --- a/tests/components/local_file/test_camera.py +++ b/tests/components/local_file/test_camera.py @@ -1,5 +1,4 @@ """The tests for local file camera component.""" -import asyncio from unittest import mock from homeassistant.components.local_file.const import DOMAIN, SERVICE_UPDATE_FILE_PATH @@ -8,15 +7,14 @@ from tests.common import mock_registry -@asyncio.coroutine -def test_loading_file(hass, hass_client): +async def test_loading_file(hass, hass_client): """Test that it loads image from disk.""" mock_registry(hass) with mock.patch("os.path.isfile", mock.Mock(return_value=True)), mock.patch( "os.access", mock.Mock(return_value=True) ): - yield from async_setup_component( + await async_setup_component( hass, "camera", { @@ -28,26 +26,25 @@ def test_loading_file(hass, hass_client): }, ) - client = yield from hass_client() + client = await hass_client() m_open = mock.mock_open(read_data=b"hello") with mock.patch( "homeassistant.components.local_file.camera.open", m_open, create=True ): - resp = yield from client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "hello" -@asyncio.coroutine -def test_file_not_readable(hass, caplog): +async def test_file_not_readable(hass, caplog): """Test a warning is shown setup when file is not readable.""" with mock.patch("os.path.isfile", mock.Mock(return_value=True)), mock.patch( "os.access", mock.Mock(return_value=False) ): - yield from async_setup_component( + await async_setup_component( hass, "camera", { @@ -64,8 +61,7 @@ def test_file_not_readable(hass, caplog): assert "mock.file" in caplog.text -@asyncio.coroutine -def test_camera_content_type(hass, hass_client): +async def test_camera_content_type(hass, hass_client): """Test local_file camera content_type.""" cam_config_jpg = { "name": "test_jpg", @@ -88,43 +84,43 @@ def test_camera_content_type(hass, hass_client): "file_path": "/path/to/image", } - yield from async_setup_component( + await async_setup_component( hass, "camera", {"camera": [cam_config_jpg, cam_config_png, cam_config_svg, cam_config_noext]}, ) - client = yield from hass_client() + client = await hass_client() image = "hello" m_open = mock.mock_open(read_data=image.encode()) with mock.patch( "homeassistant.components.local_file.camera.open", m_open, create=True ): - resp_1 = yield from client.get("/api/camera_proxy/camera.test_jpg") - resp_2 = yield from client.get("/api/camera_proxy/camera.test_png") - resp_3 = yield from client.get("/api/camera_proxy/camera.test_svg") - resp_4 = yield from client.get("/api/camera_proxy/camera.test_no_ext") + resp_1 = await client.get("/api/camera_proxy/camera.test_jpg") + resp_2 = await client.get("/api/camera_proxy/camera.test_png") + resp_3 = await client.get("/api/camera_proxy/camera.test_svg") + resp_4 = await client.get("/api/camera_proxy/camera.test_no_ext") assert resp_1.status == 200 assert resp_1.content_type == "image/jpeg" - body = yield from resp_1.text() + body = await resp_1.text() assert body == image assert resp_2.status == 200 assert resp_2.content_type == "image/png" - body = yield from resp_2.text() + body = await resp_2.text() assert body == image assert resp_3.status == 200 assert resp_3.content_type == "image/svg+xml" - body = yield from resp_3.text() + body = await resp_3.text() assert body == image # default mime type assert resp_4.status == 200 assert resp_4.content_type == "image/jpeg" - body = yield from resp_4.text() + body = await resp_4.text() assert body == image From 5b2b86987b0ce114e644614a44413a2d45ad5423 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 21:22:13 +0100 Subject: [PATCH 2655/3953] Migrate fido tests from coroutine to async/await (#30391) --- tests/components/fido/test_sensor.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index 1f67a1e2e114b1..9a316a85735b52 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -1,5 +1,4 @@ """The test for the fido sensor platform.""" -import asyncio import logging import sys from unittest.mock import MagicMock, patch @@ -27,8 +26,7 @@ def get_data(self): """Return fake fido data.""" return {"balance": 160.12, "1112223344": {"data_remaining": 100.33}} - @asyncio.coroutine - def fetch_data(self): + async def fetch_data(self): """Return fake fetching data.""" pass @@ -36,8 +34,7 @@ def fetch_data(self): class FidoClientMockError(FidoClientMock): """Fake Fido client error.""" - @asyncio.coroutine - def fetch_data(self): + async def fetch_data(self): """Return fake fetching data.""" raise PyFidoErrorMock("Fake Error") @@ -63,8 +60,7 @@ def fake_async_add_entities(component, update_before_add=False): pass -@asyncio.coroutine -def test_fido_sensor(loop, hass): +async def test_fido_sensor(loop, hass): """Test the Fido number sensor.""" with patch( "homeassistant.components.fido.sensor.FidoClient", new=FidoClientMock @@ -79,7 +75,7 @@ def test_fido_sensor(loop, hass): } } with assert_setup_component(1): - yield from async_setup_component(hass, "sensor", config) + await async_setup_component(hass, "sensor", config) state = hass.states.get("sensor.fido_1112223344_balance") assert state.state == "160.12" assert state.attributes.get("number") == "1112223344" @@ -87,8 +83,7 @@ def test_fido_sensor(loop, hass): assert state.state == "100.33" -@asyncio.coroutine -def test_error(hass, caplog): +async def test_error(hass, caplog): """Test the Fido sensor errors.""" caplog.set_level(logging.ERROR) sys.modules["pyfido"] = PyFidoFakeModule() @@ -96,5 +91,5 @@ def test_error(hass, caplog): config = {} fake_async_add_entities = MagicMock() - yield from fido.async_setup_platform(hass, config, fake_async_add_entities) + await fido.async_setup_platform(hass, config, fake_async_add_entities) assert fake_async_add_entities.called is False From 332cbbd8b1be773593037d293c5dabbf6c100199 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 21:22:30 +0100 Subject: [PATCH 2656/3953] Migrate freedns tests from coroutine to async/await (#30390) --- tests/components/freedns/test_init.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/components/freedns/test_init.py b/tests/components/freedns/test_init.py index b9e59de9ff1339..1a64aa498e2ff6 100644 --- a/tests/components/freedns/test_init.py +++ b/tests/components/freedns/test_init.py @@ -1,6 +1,4 @@ """Test the FreeDNS component.""" -import asyncio - import pytest from homeassistant.components import freedns @@ -37,8 +35,7 @@ def setup_freedns(hass, aioclient_mock): ) -@asyncio.coroutine -def test_setup(hass, aioclient_mock): +async def test_setup(hass, aioclient_mock): """Test setup works if update passes.""" params = {} params[ACCESS_TOKEN] = "" @@ -46,7 +43,7 @@ def test_setup(hass, aioclient_mock): UPDATE_URL, params=params, text="ERROR: Address has not changed." ) - result = yield from async_setup_component( + result = await async_setup_component( hass, freedns.DOMAIN, { @@ -60,18 +57,17 @@ def test_setup(hass, aioclient_mock): assert aioclient_mock.call_count == 1 async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert aioclient_mock.call_count == 2 -@asyncio.coroutine -def test_setup_fails_if_wrong_token(hass, aioclient_mock): +async def test_setup_fails_if_wrong_token(hass, aioclient_mock): """Test setup fails if first update fails through wrong token.""" params = {} params[ACCESS_TOKEN] = "" aioclient_mock.get(UPDATE_URL, params=params, text="ERROR: Invalid update URL (2)") - result = yield from async_setup_component( + result = await async_setup_component( hass, freedns.DOMAIN, { From 1e3822bdd7744a0193b24fd805487319ae08989a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 21:22:49 +0100 Subject: [PATCH 2657/3953] Migrate group tests from coroutine to async/await (#30389) --- tests/components/group/test_init.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 5c826dbe85d375..ee52a551cb81ed 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1,6 +1,5 @@ """The tests for the Group components.""" # pylint: disable=protected-access -import asyncio from collections import OrderedDict import unittest from unittest.mock import patch @@ -481,25 +480,23 @@ def test_modify_group(self): assert group_state.attributes.get(ATTR_FRIENDLY_NAME) == "friendly_name" -@asyncio.coroutine -def test_service_group_services(hass): +async def test_service_group_services(hass): """Check if service are available.""" with assert_setup_component(0, "group"): - yield from async_setup_component(hass, "group", {"group": {}}) + await async_setup_component(hass, "group", {"group": {}}) assert hass.services.has_service("group", group.SERVICE_SET) assert hass.services.has_service("group", group.SERVICE_REMOVE) # pylint: disable=invalid-name -@asyncio.coroutine -def test_service_group_set_group_remove_group(hass): +async def test_service_group_set_group_remove_group(hass): """Check if service are available.""" with assert_setup_component(0, "group"): - yield from async_setup_component(hass, "group", {"group": {}}) + await async_setup_component(hass, "group", {"group": {}}) common.async_set_group(hass, "user_test_group", name="Test") - yield from hass.async_block_till_done() + await hass.async_block_till_done() group_state = hass.states.get("group.user_test_group") assert group_state @@ -513,7 +510,7 @@ def test_service_group_set_group_remove_group(hass): visible=False, entity_ids=["test.entity_bla1"], ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() group_state = hass.states.get("group.user_test_group") assert group_state @@ -531,7 +528,7 @@ def test_service_group_set_group_remove_group(hass): control="hidden", add=["test.entity_id2"], ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() group_state = hass.states.get("group.user_test_group") assert group_state @@ -546,7 +543,7 @@ def test_service_group_set_group_remove_group(hass): ) common.async_remove(hass, "user_test_group") - yield from hass.async_block_till_done() + await hass.async_block_till_done() group_state = hass.states.get("group.user_test_group") assert group_state is None From b9fcb87d2c0982d0f8c9eae986ab42634545447f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 21:23:16 +0100 Subject: [PATCH 2658/3953] Migrate generic tests from coroutine to async/await (#30388) --- tests/components/generic/test_camera.py | 73 ++++++++++++------------- 1 file changed, 34 insertions(+), 39 deletions(-) diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index c25b4ce9f3d17e..7f49d1b89790ab 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -5,12 +5,11 @@ from homeassistant.setup import async_setup_component -@asyncio.coroutine -def test_fetching_url(aioclient_mock, hass, hass_client): +async def test_fetching_url(aioclient_mock, hass, hass_client): """Test that it fetches the given url.""" aioclient_mock.get("http://example.com", text="hello world") - yield from async_setup_component( + await async_setup_component( hass, "camera", { @@ -24,25 +23,24 @@ def test_fetching_url(aioclient_mock, hass, hass_client): }, ) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == 200 assert aioclient_mock.call_count == 1 - body = yield from resp.text() + body = await resp.text() assert body == "hello world" - resp = yield from client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.config_test") assert aioclient_mock.call_count == 2 -@asyncio.coroutine -def test_fetching_without_verify_ssl(aioclient_mock, hass, hass_client): +async def test_fetching_without_verify_ssl(aioclient_mock, hass, hass_client): """Test that it fetches the given url when ssl verify is off.""" aioclient_mock.get("https://example.com", text="hello world") - yield from async_setup_component( + await async_setup_component( hass, "camera", { @@ -57,19 +55,18 @@ def test_fetching_without_verify_ssl(aioclient_mock, hass, hass_client): }, ) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == 200 -@asyncio.coroutine -def test_fetching_url_with_verify_ssl(aioclient_mock, hass, hass_client): +async def test_fetching_url_with_verify_ssl(aioclient_mock, hass, hass_client): """Test that it fetches the given url when ssl verify is explicitly on.""" aioclient_mock.get("https://example.com", text="hello world") - yield from async_setup_component( + await async_setup_component( hass, "camera", { @@ -84,22 +81,21 @@ def test_fetching_url_with_verify_ssl(aioclient_mock, hass, hass_client): }, ) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.config_test") assert resp.status == 200 -@asyncio.coroutine -def test_limit_refetch(aioclient_mock, hass, hass_client): +async def test_limit_refetch(aioclient_mock, hass, hass_client): """Test that it fetches the given url.""" aioclient_mock.get("http://example.com/5a", text="hello world") aioclient_mock.get("http://example.com/10a", text="hello world") aioclient_mock.get("http://example.com/15a", text="hello planet") aioclient_mock.get("http://example.com/20a", status=404) - yield from async_setup_component( + await async_setup_component( hass, "camera", { @@ -112,51 +108,50 @@ def test_limit_refetch(aioclient_mock, hass, hass_client): }, ) - client = yield from hass_client() + client = await hass_client() - resp = yield from client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.config_test") hass.states.async_set("sensor.temp", "5") with mock.patch("async_timeout.timeout", side_effect=asyncio.TimeoutError()): - resp = yield from client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.config_test") assert aioclient_mock.call_count == 0 assert resp.status == 500 hass.states.async_set("sensor.temp", "10") - resp = yield from client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.config_test") assert aioclient_mock.call_count == 1 assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "hello world" - resp = yield from client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.config_test") assert aioclient_mock.call_count == 1 assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "hello world" hass.states.async_set("sensor.temp", "15") # Url change = fetch new image - resp = yield from client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.config_test") assert aioclient_mock.call_count == 2 assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "hello planet" # Cause a template render error hass.states.async_remove("sensor.temp") - resp = yield from client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.config_test") assert aioclient_mock.call_count == 2 assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == "hello planet" -@asyncio.coroutine -def test_camera_content_type(aioclient_mock, hass, hass_client): +async def test_camera_content_type(aioclient_mock, hass, hass_client): """Test generic camera with custom content_type.""" svg_image = "" urlsvg = "https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg" @@ -172,22 +167,22 @@ def test_camera_content_type(aioclient_mock, hass, hass_client): cam_config_normal.pop("content_type") cam_config_normal["name"] = "config_test_jpg" - yield from async_setup_component( + await async_setup_component( hass, "camera", {"camera": [cam_config_svg, cam_config_normal]} ) - client = yield from hass_client() + client = await hass_client() - resp_1 = yield from client.get("/api/camera_proxy/camera.config_test_svg") + resp_1 = await client.get("/api/camera_proxy/camera.config_test_svg") assert aioclient_mock.call_count == 1 assert resp_1.status == 200 assert resp_1.content_type == "image/svg+xml" - body = yield from resp_1.text() + body = await resp_1.text() assert body == svg_image - resp_2 = yield from client.get("/api/camera_proxy/camera.config_test_jpg") + resp_2 = await client.get("/api/camera_proxy/camera.config_test_jpg") assert aioclient_mock.call_count == 2 assert resp_2.status == 200 assert resp_2.content_type == "image/jpeg" - body = yield from resp_2.text() + body = await resp_2.text() assert body == svg_image From 4e6d41554127f52999be99de656e5f7d24d27928 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 21:23:35 +0100 Subject: [PATCH 2659/3953] Migrate media_player tests from coroutine to async/await (#30387) --- .../media_player/test_async_helpers.py | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 2e1ded3f08492e..2cbca449ed6b43 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -44,28 +44,23 @@ def supported_features(self): | mp.const.SUPPORT_TURN_ON ) - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" self._volume = volume - @asyncio.coroutine - def async_media_play(self): + async def async_media_play(self): """Send play command.""" self._state = STATE_PLAYING - @asyncio.coroutine - def async_media_pause(self): + async def async_media_pause(self): """Send pause command.""" self._state = STATE_PAUSED - @asyncio.coroutine - def async_turn_on(self): + async def async_turn_on(self): """Turn the media player on.""" self._state = STATE_ON - @asyncio.coroutine - def async_turn_off(self): + async def async_turn_off(self): """Turn the media player off.""" self._state = STATE_OFF @@ -129,21 +124,19 @@ def toggle(self): else: self._state = STATE_OFF - @asyncio.coroutine - def async_media_play_pause(self): + async def async_media_play_pause(self): """Create a coroutine to wrap the future returned by ABC. This allows the run_coroutine_threadsafe helper to be used. """ - yield from super().async_media_play_pause() + await super().async_media_play_pause() - @asyncio.coroutine - def async_toggle(self): + async def async_toggle(self): """Create a coroutine to wrap the future returned by ABC. This allows the run_coroutine_threadsafe helper to be used. """ - yield from super().async_toggle() + await super().async_toggle() class TestAsyncMediaPlayer(unittest.TestCase): From 4f8663846b3b692ebf54f42875aa4fb218e8f5a5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 2 Jan 2020 21:23:56 +0100 Subject: [PATCH 2660/3953] Migrate frontend tests from coroutine to async/await (#30386) --- tests/components/frontend/test_init.py | 39 +++++++++++--------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index f8fd5f1d7e3e6c..56a945f62dff70 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -1,5 +1,4 @@ """The tests for Home Assistant frontend.""" -import asyncio import re from unittest.mock import patch @@ -71,53 +70,48 @@ def mock_onboarded(): yield -@asyncio.coroutine -def test_frontend_and_static(mock_http_client, mock_onboarded): +async def test_frontend_and_static(mock_http_client, mock_onboarded): """Test if we can get the frontend.""" - resp = yield from mock_http_client.get("") + resp = await mock_http_client.get("") assert resp.status == 200 assert "cache-control" not in resp.headers - text = yield from resp.text() + text = await resp.text() # Test we can retrieve frontend.js frontendjs = re.search(r"(?P\/frontend_es5\/app.[A-Za-z0-9]{8}.js)", text) assert frontendjs is not None, text - resp = yield from mock_http_client.get(frontendjs.groups(0)[0]) + resp = await mock_http_client.get(frontendjs.groups(0)[0]) assert resp.status == 200 assert "public" in resp.headers.get("cache-control") -@asyncio.coroutine -def test_dont_cache_service_worker(mock_http_client): +async def test_dont_cache_service_worker(mock_http_client): """Test that we don't cache the service worker.""" - resp = yield from mock_http_client.get("/service_worker.js") + resp = await mock_http_client.get("/service_worker.js") assert resp.status == 200 assert "cache-control" not in resp.headers -@asyncio.coroutine -def test_404(mock_http_client): +async def test_404(mock_http_client): """Test for HTTP 404 error.""" - resp = yield from mock_http_client.get("/not-existing") + resp = await mock_http_client.get("/not-existing") assert resp.status == 404 -@asyncio.coroutine -def test_we_cannot_POST_to_root(mock_http_client): +async def test_we_cannot_POST_to_root(mock_http_client): """Test that POST is not allow to root.""" - resp = yield from mock_http_client.post("/") + resp = await mock_http_client.post("/") assert resp.status == 405 -@asyncio.coroutine -def test_states_routes(mock_http_client): +async def test_states_routes(mock_http_client): """All served by index.""" - resp = yield from mock_http_client.get("/states") + resp = await mock_http_client.get("/states") assert resp.status == 200 - resp = yield from mock_http_client.get("/states/group.existing") + resp = await mock_http_client.get("/states/group.existing") assert resp.status == 200 @@ -211,12 +205,11 @@ async def test_missing_themes(hass, hass_ws_client): assert msg["result"]["themes"] == {} -@asyncio.coroutine -def test_extra_urls(mock_http_client_with_urls, mock_onboarded): +async def test_extra_urls(mock_http_client_with_urls, mock_onboarded): """Test that extra urls are loaded.""" - resp = yield from mock_http_client_with_urls.get("/states?latest") + resp = await mock_http_client_with_urls.get("/states?latest") assert resp.status == 200 - text = yield from resp.text() + text = await resp.text() assert text.find('href="https://domain.com/my_extra_url.html"') >= 0 From 8e5ccfcb8a1f8260395cf3e2d79eca66a2cc0e75 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 2 Jan 2020 22:24:29 +0100 Subject: [PATCH 2661/3953] Bump miflora and bluepy (#30411) --- homeassistant/components/decora/manifest.json | 5 +---- homeassistant/components/miflora/manifest.json | 10 ++-------- requirements_all.txt | 4 ++-- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/decora/manifest.json b/homeassistant/components/decora/manifest.json index 5142b5fb2e2cd6..270b0ca763d065 100644 --- a/homeassistant/components/decora/manifest.json +++ b/homeassistant/components/decora/manifest.json @@ -2,10 +2,7 @@ "domain": "decora", "name": "Decora", "documentation": "https://www.home-assistant.io/integrations/decora", - "requirements": [ - "bluepy==1.1.4", - "decora==0.6" - ], + "requirements": ["bluepy==1.3.0", "decora==0.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index 54fa59135b338a..0cce06935af6a9 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -2,13 +2,7 @@ "domain": "miflora", "name": "Miflora", "documentation": "https://www.home-assistant.io/integrations/miflora", - "requirements": [ - "bluepy==1.1.4", - "miflora==0.4.0" - ], + "requirements": ["bluepy==1.3.0", "miflora==0.6.0"], "dependencies": [], - "codeowners": [ - "@danielhiversen", - "@ChristianKuehnel" - ] + "codeowners": ["@danielhiversen", "@ChristianKuehnel"] } diff --git a/requirements_all.txt b/requirements_all.txt index d2ed4e0e1e26b9..c3550718fb6f9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -318,7 +318,7 @@ blockchain==1.4.4 # homeassistant.components.decora # homeassistant.components.miflora -# bluepy==1.1.4 +# bluepy==1.3.0 # homeassistant.components.bme680 # bme680==1.0.5 @@ -846,7 +846,7 @@ meteofrance==0.3.7 mficlient==0.3.0 # homeassistant.components.miflora -miflora==0.4.0 +miflora==0.6.0 # homeassistant.components.mill millheater==0.3.4 From b91cbb50c86008f1f81c8d2248c4b3d90fd77ccd Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Thu, 2 Jan 2020 16:24:43 -0500 Subject: [PATCH 2662/3953] Bump env_canada to 0.0.31 (#30409) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 8ad13b392513f5..bfe0aa5d2cb146 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/integrations/environment_canada", "requirements": [ - "env_canada==0.0.30" + "env_canada==0.0.31" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index c3550718fb6f9f..dd19553b961700 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -480,7 +480,7 @@ enocean==0.50 enturclient==0.2.1 # homeassistant.components.environment_canada -env_canada==0.0.30 +env_canada==0.0.31 # homeassistant.components.envirophat # envirophat==0.0.6 From 9064058a030efd002ac78954c09e2f3122a038d7 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Thu, 2 Jan 2020 16:30:20 -0500 Subject: [PATCH 2663/3953] Add generic command functionality to denonavr (#29295) * Add generic command functionality to denonavr * add minimal unit tests for denonavr * fix import order * simplify denonavr unit test * handle domain specific service calls with dispatcher * update unit tests * update unit tests * remove unnecessary return value * fix handling of mock instances in unit tests --- homeassistant/components/denonavr/__init__.py | 34 +++++++++++ .../components/denonavr/media_player.py | 24 ++++++++ .../components/denonavr/services.yaml | 11 ++++ requirements_test_all.txt | 3 + tests/components/denonavr/__init__.py | 1 + .../components/denonavr/test_media_player.py | 57 +++++++++++++++++++ 6 files changed, 130 insertions(+) create mode 100644 homeassistant/components/denonavr/services.yaml create mode 100644 tests/components/denonavr/__init__.py create mode 100644 tests/components/denonavr/test_media_player.py diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index dee84449d13a89..8877a7dfb3bd93 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -1 +1,35 @@ """The denonavr component.""" +import voluptuous as vol + +from homeassistant.const import ATTR_ENTITY_ID +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send + +DOMAIN = "denonavr" + +SERVICE_GET_COMMAND = "get_command" +ATTR_COMMAND = "command" + +CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) + +GET_COMMAND_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_COMMAND): cv.string}) + +SERVICE_TO_METHOD = { + SERVICE_GET_COMMAND: {"method": "get_command", "schema": GET_COMMAND_SCHEMA} +} + + +def setup(hass, config): + """Set up the denonavr platform.""" + + def service_handler(service): + method = SERVICE_TO_METHOD.get(service.service) + data = service.data.copy() + data["method"] = method["method"] + dispatcher_send(hass, DOMAIN, data) + + for service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service]["schema"] + hass.services.register(DOMAIN, service, service_handler, schema=schema) + + return True diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 1725b2d105cf8b..46d22187ce1fca 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -24,16 +24,21 @@ SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TIMEOUT, CONF_ZONE, + ENTITY_MATCH_ALL, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -190,6 +195,21 @@ def __init__(self, receiver): self._sound_mode_support and SUPPORT_SELECT_SOUND_MODE ) + async def async_added_to_hass(self): + """Register signal handler.""" + async_dispatcher_connect(self.hass, DOMAIN, self.signal_handler) + + def signal_handler(self, data): + """Handle domain-specific signal by calling appropriate method.""" + entity_ids = data[ATTR_ENTITY_ID] + if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: + params = { + key: value + for key, value in data.items() + if key not in ["entity_id", "method"] + } + getattr(self, data["method"])(**params) + def update(self): """Get the latest status information from device.""" self._receiver.update() @@ -398,3 +418,7 @@ def set_volume_level(self, volume): def mute_volume(self, mute): """Send mute command.""" return self._receiver.mute(mute) + + def get_command(self, command, **kwargs): + """Send generic command.""" + self._receiver.send_get_command(command) diff --git a/homeassistant/components/denonavr/services.yaml b/homeassistant/components/denonavr/services.yaml new file mode 100644 index 00000000000000..889adc3af058c1 --- /dev/null +++ b/homeassistant/components/denonavr/services.yaml @@ -0,0 +1,11 @@ +# Describes the format for available webostv services + +get_command: + description: 'Send a generic http get command.' + fields: + entity_id: + description: Name(s) of the denonavr entities where to run the API method. + example: 'media_player.living_room_receiver' + command: + description: Endpoint of the command, including associated parameters. + example: '/goform/formiPhoneAppDirect.xml?RCKSK0410370' diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff1ded4434cb5f..373abe92ec87d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -149,6 +149,9 @@ datadog==0.15.0 # homeassistant.components.ssdp defusedxml==0.6.0 +# homeassistant.components.denonavr +denonavr==0.7.10 + # homeassistant.components.directv directpy==0.5 diff --git a/tests/components/denonavr/__init__.py b/tests/components/denonavr/__init__.py new file mode 100644 index 00000000000000..5ad16068f2a255 --- /dev/null +++ b/tests/components/denonavr/__init__.py @@ -0,0 +1 @@ +"""Tests for the denonavr integration.""" diff --git a/tests/components/denonavr/test_media_player.py b/tests/components/denonavr/test_media_player.py new file mode 100644 index 00000000000000..91bc2abf94d7ce --- /dev/null +++ b/tests/components/denonavr/test_media_player.py @@ -0,0 +1,57 @@ +"""The tests for the denonavr media player platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components import media_player +from homeassistant.components.denonavr import ATTR_COMMAND, DOMAIN, SERVICE_GET_COMMAND +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PLATFORM +from homeassistant.setup import async_setup_component + +NAME = "fake" +ENTITY_ID = f"{media_player.DOMAIN}.{NAME}" + + +@pytest.fixture(name="client") +def client_fixture(): + """Patch of client library for tests.""" + with patch( + "homeassistant.components.denonavr.media_player.denonavr.DenonAVR", + autospec=True, + ) as mock_client_class, patch( + "homeassistant.components.denonavr.media_player.denonavr.discover" + ): + mock_client_class.return_value.name = NAME + mock_client_class.return_value.zones = {"Main": mock_client_class.return_value} + yield mock_client_class.return_value + + +async def setup_denonavr(hass): + """Initialize webostv and media_player for tests.""" + assert await async_setup_component( + hass, + media_player.DOMAIN, + { + media_player.DOMAIN: { + CONF_PLATFORM: "denonavr", + CONF_HOST: "fake", + CONF_NAME: NAME, + } + }, + ) + await hass.async_block_till_done() + + +async def test_get_command(hass, client): + """Test generic command functionality.""" + + await setup_denonavr(hass) + + data = { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_COMMAND: "test", + } + await hass.services.async_call(DOMAIN, SERVICE_GET_COMMAND, data) + await hass.async_block_till_done() + + client.send_get_command.assert_called_with("test") From c1936f6fe4b28a3899c31524027c06e7b31a050b Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Thu, 2 Jan 2020 16:32:57 -0500 Subject: [PATCH 2664/3953] Add generic command/button functionality to webostv (#30379) * add generic command/button functionality to webostv * update codeowners --- CODEOWNERS | 1 + homeassistant/components/webostv/__init__.py | 31 +++++++++++++++ .../components/webostv/manifest.json | 2 +- .../components/webostv/media_player.py | 28 ++++++++++++- .../components/webostv/services.yaml | 26 +++++++++++++ tests/components/webostv/test_media_player.py | 39 ++++++++++++++++++- 6 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/webostv/services.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 23005cb5273b70..04918e979ee624 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -365,6 +365,7 @@ homeassistant/components/waqi/* @andrey-git homeassistant/components/watson_tts/* @rutkai homeassistant/components/weather/* @fabaff homeassistant/components/weblink/* @home-assistant/core +homeassistant/components/webostv/* @bendavid homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @sqldiablo homeassistant/components/withings/* @vangorra diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index b34dba3ad94a0a..e03fea68fd7af6 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -7,6 +7,7 @@ from websockets.exceptions import ConnectionClosed from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_CUSTOMIZE, CONF_HOST, CONF_ICON, @@ -14,6 +15,7 @@ EVENT_HOMEASSISTANT_STOP, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send DOMAIN = "webostv" @@ -23,6 +25,12 @@ DEFAULT_NAME = "LG webOS Smart TV" WEBOSTV_CONFIG_FILE = "webostv.conf" +SERVICE_BUTTON = "button" +ATTR_BUTTON = "button" + +SERVICE_COMMAND = "command" +ATTR_COMMAND = "command" + CUSTOMIZE_SCHEMA = vol.Schema( {vol.Optional(CONF_SOURCES, default=[]): vol.All(cv.ensure_list, [cv.string])} ) @@ -50,6 +58,17 @@ extra=vol.ALLOW_EXTRA, ) +CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) + +BUTTON_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_BUTTON): cv.string}) + +COMMAND_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_COMMAND): cv.string}) + +SERVICE_TO_METHOD = { + SERVICE_BUTTON: {"method": "async_button", "schema": BUTTON_SCHEMA}, + SERVICE_COMMAND: {"method": "async_command", "schema": COMMAND_SCHEMA}, +} + _LOGGER = logging.getLogger(__name__) @@ -57,6 +76,18 @@ async def async_setup(hass, config): """Set up the LG WebOS TV platform.""" hass.data[DOMAIN] = {} + async def async_service_handler(service): + method = SERVICE_TO_METHOD.get(service.service) + data = service.data.copy() + data["method"] = method["method"] + async_dispatcher_send(hass, DOMAIN, data) + + for service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service]["schema"] + hass.services.async_register( + DOMAIN, service, async_service_handler, schema=schema + ) + tasks = [async_setup_tv(hass, config, conf) for conf in config[DOMAIN]] if tasks: await asyncio.gather(*tasks) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index 016f14f0f941d7..82d4942f83c52a 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -6,5 +6,5 @@ "aiopylgtv==0.2.4" ], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": ["@bendavid"] } diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index fd47cf0a1144fa..5e58cdf7a2f40a 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -24,13 +24,16 @@ SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_CUSTOMIZE, CONF_HOST, CONF_NAME, + ENTITY_MATCH_ALL, STATE_OFF, STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.script import Script from . import CONF_ON_ACTION, CONF_SOURCES, DOMAIN @@ -131,7 +134,9 @@ def __init__(self, client, name, customize, on_script=None): self._last_icon = None async def async_added_to_hass(self): - """Connect and subscribe to state updates.""" + """Connect and subscribe to dispatcher signals and state updates.""" + async_dispatcher_connect(self.hass, DOMAIN, self.async_signal_handler) + await self._client.register_state_update_callback( self.async_handle_state_update ) @@ -144,6 +149,17 @@ async def async_will_remove_from_hass(self): """Call disconnect on removal.""" self._client.unregister_state_update_callback(self.async_handle_state_update) + async def async_signal_handler(self, data): + """Handle domain-specific signal by calling appropriate method.""" + entity_ids = data[ATTR_ENTITY_ID] + if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: + params = { + key: value + for key, value in data.items() + if key not in ["entity_id", "method"] + } + await getattr(self, data["method"])(**params) + async def async_handle_state_update(self): """Update state from WebOsClient.""" self._current_source_id = self._client.current_appId @@ -406,3 +422,13 @@ async def async_media_previous_track(self): await self._client.channel_down() else: await self._client.rewind() + + @cmd + async def async_button(self, button): + """Send a button press.""" + await self._client.button(button) + + @cmd + async def async_command(self, command): + """Send a command.""" + await self._client.request(command) diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml new file mode 100644 index 00000000000000..137a6026eda648 --- /dev/null +++ b/homeassistant/components/webostv/services.yaml @@ -0,0 +1,26 @@ +# Describes the format for available webostv services + +button: + description: 'Send a button press command.' + fields: + entity_id: + description: Name(s) of the webostv entities where to run the API method. + example: 'media_player.living_room_tv' + button: + description: Name of the button to press. Known possible values are + LEFT, RIGHT, DOWN, UP, HOME, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, + MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + example: 'LEFT' + +command: + description: 'Send a command.' + fields: + entity_id: + description: Name(s) of the webostv entities where to run the API method. + example: 'media_player.living_room_tv' + command: + description: Endpoint of the command. Known valid endpoints are listed in + https://github.com/TheRealLink/pylgtv/blob/master/pylgtv/endpoints.py + example: 'media.controls/rewind' + diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 023e0e2dc070ee..4dcda9eb9085f8 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -9,7 +9,13 @@ ATTR_MEDIA_VOLUME_MUTED, SERVICE_SELECT_SOURCE, ) -from homeassistant.components.webostv import DOMAIN +from homeassistant.components.webostv import ( + ATTR_BUTTON, + ATTR_COMMAND, + DOMAIN, + SERVICE_BUTTON, + SERVICE_COMMAND, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -75,3 +81,34 @@ async def test_select_source_with_empty_source_list(hass, client): assert hass.states.is_state(ENTITY_ID, "playing") client.launch_app.assert_not_called() client.set_input.assert_not_called() + + +async def test_button(hass, client): + """Test generic button functionality.""" + + await setup_webostv(hass) + + data = { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_BUTTON: "test", + } + await hass.services.async_call(DOMAIN, SERVICE_BUTTON, data) + await hass.async_block_till_done() + + client.button.assert_called_once() + client.button.assert_called_with("test") + + +async def test_command(hass, client): + """Test generic button functionality.""" + + await setup_webostv(hass) + + data = { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_COMMAND: "test", + } + await hass.services.async_call(DOMAIN, SERVICE_COMMAND, data) + await hass.async_block_till_done() + + client.request.assert_called_with("test") From 9b961632af62fddcd1936d1ce0debda438438064 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 3 Jan 2020 00:02:59 +0100 Subject: [PATCH 2665/3953] Axis - Improve tests (#30415) --- tests/components/axis/test_binary_sensor.py | 67 +---- tests/components/axis/test_camera.py | 59 +--- tests/components/axis/test_config_flow.py | 2 +- tests/components/axis/test_device.py | 311 ++++++++++---------- tests/components/axis/test_switch.py | 68 +---- 5 files changed, 180 insertions(+), 327 deletions(-) diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index ca3e984c993d18..d70d55e0d1ed86 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -1,12 +1,11 @@ """Axis binary sensor platform tests.""" -from unittest.mock import Mock - -from homeassistant import config_entries from homeassistant.components import axis import homeassistant.components.binary_sensor as binary_sensor from homeassistant.setup import async_setup_component +from .test_device import NAME, setup_axis_integration + EVENTS = [ { "operation": "Initialized", @@ -24,52 +23,6 @@ }, ] -ENTRY_CONFIG = { - axis.CONF_DEVICE: { - axis.config_flow.CONF_HOST: "1.2.3.4", - axis.config_flow.CONF_USERNAME: "user", - axis.config_flow.CONF_PASSWORD: "pass", - axis.config_flow.CONF_PORT: 80, - }, - axis.config_flow.CONF_MAC: "1234ABCD", - axis.config_flow.CONF_MODEL: "model", - axis.config_flow.CONF_NAME: "model 0", -} - -ENTRY_OPTIONS = { - axis.CONF_CAMERA: False, - axis.CONF_EVENTS: True, - axis.CONF_TRIGGER_TIME: 0, -} - - -async def setup_device(hass): - """Load the Axis binary sensor platform.""" - from axis import AxisDevice - - loop = Mock() - - config_entry = config_entries.ConfigEntry( - 1, - axis.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=ENTRY_OPTIONS, - ) - device = axis.AxisNetworkDevice(hass, config_entry) - device.api = AxisDevice(loop=loop, **config_entry.data[axis.CONF_DEVICE]) - hass.data[axis.DOMAIN] = {device.serial: device} - device.api.enable_events(event_callback=device.async_event_callback) - - await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") - # To flush out the service call to update the group - await hass.async_block_till_done() - - return device - async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" @@ -85,25 +38,25 @@ async def test_platform_manually_configured(hass): async def test_no_binary_sensors(hass): """Test that no sensors in Axis results in no sensor entities.""" - await setup_device(hass) + await setup_axis_integration(hass) - assert len(hass.states.async_all()) == 0 + assert not hass.states.async_entity_ids("binary_sensor") async def test_binary_sensors(hass): """Test that sensors are loaded properly.""" - device = await setup_device(hass) + device = await setup_axis_integration(hass) for event in EVENTS: device.api.stream.event.manage_event(event) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_entity_ids("binary_sensor")) == 2 - pir = hass.states.get("binary_sensor.model_0_pir_0") + pir = hass.states.get(f"binary_sensor.{NAME}_pir_0") assert pir.state == "off" - assert pir.name == "model 0 PIR 0" + assert pir.name == f"{NAME} PIR 0" - vmd4 = hass.states.get("binary_sensor.model_0_vmd4_camera1profile1") + vmd4 = hass.states.get(f"binary_sensor.{NAME}_vmd4_camera1profile1") assert vmd4.state == "on" - assert vmd4.name == "model 0 VMD4 Camera1Profile1" + assert vmd4.name == f"{NAME} VMD4 Camera1Profile1" diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 67ca7e3690a137..5cbc5e993ca3f3 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -1,57 +1,10 @@ """Axis camera platform tests.""" -from unittest.mock import Mock - -from homeassistant import config_entries from homeassistant.components import axis import homeassistant.components.camera as camera from homeassistant.setup import async_setup_component -ENTRY_CONFIG = { - axis.CONF_DEVICE: { - axis.config_flow.CONF_HOST: "1.2.3.4", - axis.config_flow.CONF_USERNAME: "user", - axis.config_flow.CONF_PASSWORD: "pass", - axis.config_flow.CONF_PORT: 80, - }, - axis.config_flow.CONF_MAC: "1234ABCD", - axis.config_flow.CONF_MODEL: "model", - axis.config_flow.CONF_NAME: "model 0", -} - -ENTRY_OPTIONS = { - axis.CONF_CAMERA: False, - axis.CONF_EVENTS: True, - axis.CONF_TRIGGER_TIME: 0, -} - - -async def setup_device(hass): - """Load the Axis binary sensor platform.""" - from axis import AxisDevice - - loop = Mock() - - config_entry = config_entries.ConfigEntry( - 1, - axis.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=ENTRY_OPTIONS, - ) - device = axis.AxisNetworkDevice(hass, config_entry) - device.api = AxisDevice(loop=loop, **config_entry.data[axis.CONF_DEVICE]) - hass.data[axis.DOMAIN] = {device.serial: device} - device.api.enable_events(event_callback=device.async_event_callback) - - await hass.config_entries.async_forward_entry_setup(config_entry, "camera") - # To flush out the service call to update the group - await hass.async_block_till_done() - - return device +from .test_device import NAME, setup_axis_integration async def test_platform_manually_configured(hass): @@ -68,12 +21,10 @@ async def test_platform_manually_configured(hass): async def test_camera(hass): """Test that Axis camera platform is loaded properly.""" - await setup_device(hass) - - await hass.async_block_till_done() + await setup_axis_integration(hass) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_entity_ids("camera")) == 1 - cam = hass.states.get("camera.model_0") + cam = hass.states.get(f"camera.{NAME}") assert cam.state == "idle" - assert cam.name == "model 0" + assert cam.name == NAME diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index a29c270e0b8c46..e542ef0534fefd 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -60,7 +60,7 @@ def mock_constructor(loop, host, username, password, port, web_proto): ) assert result["type"] == "create_entry" - assert result["title"] == "{} - {}".format("prodnbr", "serialnumber") + assert result["title"] == f"prodnbr - serialnumber" assert result["data"] == { axis.CONF_DEVICE: { config_flow.CONF_HOST: "1.2.3.4", diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index a9f38cc4f3ab3b..58ebed606819d0 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -1,173 +1,180 @@ """Test Axis device.""" -from unittest.mock import Mock, patch +from copy import deepcopy +from asynctest import Mock, patch +import axis as axislib import pytest -from homeassistant.components.axis import device, errors -from homeassistant.components.axis.camera import AxisCamera +from homeassistant import config_entries +from homeassistant.components import axis -from tests.common import MockConfigEntry, mock_coro +MAC = "00408C12345" +MODEL = "model" +NAME = "name" DEVICE_DATA = { - device.CONF_HOST: "1.2.3.4", - device.CONF_USERNAME: "username", - device.CONF_PASSWORD: "password", - device.CONF_PORT: 1234, + axis.device.CONF_HOST: "1.2.3.4", + axis.device.CONF_USERNAME: "username", + axis.device.CONF_PASSWORD: "password", + axis.device.CONF_PORT: 1234, } -ENTRY_OPTIONS = {device.CONF_CAMERA: True, device.CONF_EVENTS: True} +ENTRY_OPTIONS = {axis.device.CONF_CAMERA: True, axis.device.CONF_EVENTS: True} ENTRY_CONFIG = { - device.CONF_DEVICE: DEVICE_DATA, - device.CONF_MAC: "mac", - device.CONF_MODEL: "model", - device.CONF_NAME: "name", + axis.device.CONF_DEVICE: DEVICE_DATA, + axis.device.CONF_MAC: MAC, + axis.device.CONF_MODEL: MODEL, + axis.device.CONF_NAME: NAME, } +DEFAULT_BRAND = """root.Brand.Brand=AXIS +root.Brand.ProdFullName=AXIS M1065-LW Network Camera +root.Brand.ProdNbr=M1065-LW +root.Brand.ProdShortName=AXIS M1065-LW +root.Brand.ProdType=Network Camera +root.Brand.ProdVariant= +root.Brand.WebURL=http://www.axis.com +""" + +DEFAULT_PORTS = """root.Input.NbrOfInputs=1 +root.IOPort.I0.Configurable=no +root.IOPort.I0.Direction=input +root.IOPort.I0.Input.Name=PIR sensor +root.IOPort.I0.Input.Trig=closed +root.Output.NbrOfOutputs=0 +""" + +DEFAULT_PROPERTIES = """root.Properties.API.HTTP.Version=3 +root.Properties.API.Metadata.Metadata=yes +root.Properties.API.Metadata.Version=1.0 +root.Properties.Firmware.BuildDate=Feb 15 2019 09:42 +root.Properties.Firmware.BuildNumber=26 +root.Properties.Firmware.Version=9.10.1 +root.Properties.Image.Format=jpeg,mjpeg,h264 +root.Properties.Image.NbrOfViews=2 +root.Properties.Image.Resolution=1920x1080,1280x960,1280x720,1024x768,1024x576,800x600,640x480,640x360,352x240,320x240 +root.Properties.Image.Rotation=0,180 +root.Properties.System.SerialNumber=ACCC12345678 +""" + + +async def setup_axis_integration( + hass, + config=ENTRY_CONFIG, + options=ENTRY_OPTIONS, + brand=DEFAULT_BRAND, + ports=DEFAULT_PORTS, + properties=DEFAULT_PROPERTIES, +): + """Create the Axis device.""" + config_entry = config_entries.ConfigEntry( + version=1, + domain=axis.DOMAIN, + title="Mock Title", + data=deepcopy(config), + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, + options=deepcopy(options), + entry_id="1", + ) -async def test_device_setup(): - """Successful setup.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = ENTRY_OPTIONS - api = Mock() - - axis_device = device.AxisNetworkDevice(hass, entry) - axis_device.start = Mock() - - assert axis_device.host == DEVICE_DATA[device.CONF_HOST] - assert axis_device.model == ENTRY_CONFIG[device.CONF_MODEL] - assert axis_device.name == ENTRY_CONFIG[device.CONF_NAME] - assert axis_device.serial == ENTRY_CONFIG[device.CONF_MAC] + def mock_request(self, method, path, json=None): + if method == "get": + if path == "/axis-cgi/param.cgi?action=list&group=root.Brand": + return brand + if path in [ + "/axis-cgi/param.cgi?action=list&group=root.Input", + "/axis-cgi/param.cgi?action=list&group=root.IOPort", + "/axis-cgi/param.cgi?action=list&group=root.Output", + ]: + return ports + if path == "/axis-cgi/param.cgi?action=list&group=root.Properties": + return properties + + return None + + with patch("axis.vapix.Vapix.request", new=mock_request), patch( + "axis.AxisDevice.start", return_value=True + ): + await axis.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() - with patch.object(device, "get_device", return_value=mock_coro(api)): - assert await axis_device.async_setup() is True + hass.config_entries._entries.append(config_entry) - assert axis_device.api is api - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3 - assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == ( - entry, - "camera", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == ( - entry, - "binary_sensor", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == ( - entry, - "switch", - ) + return hass.data[axis.DOMAIN].get(config[axis.CONF_MAC]) -async def test_device_signal_new_address(hass): +async def test_device_setup(hass): """Successful setup.""" - entry = MockConfigEntry( - domain=device.DOMAIN, data=ENTRY_CONFIG, options=ENTRY_OPTIONS + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ) as forward_entry_setup: + device = await setup_axis_integration(hass) + + entry = device.config_entry + + assert len(forward_entry_setup.mock_calls) == 3 + assert forward_entry_setup.mock_calls[0][1] == (entry, "camera") + assert forward_entry_setup.mock_calls[1][1] == (entry, "binary_sensor") + assert forward_entry_setup.mock_calls[2][1] == (entry, "switch") + + assert device.host == DEVICE_DATA[axis.device.CONF_HOST] + assert device.model == ENTRY_CONFIG[axis.device.CONF_MODEL] + assert device.name == ENTRY_CONFIG[axis.device.CONF_NAME] + assert device.serial == ENTRY_CONFIG[axis.device.CONF_MAC] + + +async def test_update_address(hass): + """Test update address works.""" + device = await setup_axis_integration(hass) + assert device.api.config.host == "1.2.3.4" + + await hass.config_entries.flow.async_init( + axis.DOMAIN, + data={ + "host": "2.3.4.5", + "port": 80, + "hostname": "name", + "properties": {"macaddress": MAC}, + }, + context={"source": "zeroconf"}, ) - - api = Mock() - api.vapix.get_param.return_value = "1234" - - axis_device = device.AxisNetworkDevice(hass, entry) - hass.data[device.DOMAIN] = {axis_device.serial: axis_device} - - with patch.object(device, "get_device", return_value=mock_coro(api)), patch.object( - AxisCamera, "_new_address" - ) as new_address_mock: - await axis_device.async_setup() - await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 1 - assert len(axis_device.listeners) == 2 - - entry.data[device.CONF_DEVICE][device.CONF_HOST] = "2.3.4.5" - hass.config_entries.async_update_entry(entry, data=entry.data) await hass.async_block_till_done() - assert axis_device.host == "2.3.4.5" - assert axis_device.api.config.host == "2.3.4.5" - assert len(new_address_mock.mock_calls) == 1 + assert device.api.config.host == "2.3.4.5" async def test_device_unavailable(hass): """Successful setup.""" - entry = MockConfigEntry( - domain=device.DOMAIN, data=ENTRY_CONFIG, options=ENTRY_OPTIONS - ) - - api = Mock() - api.vapix.get_param.return_value = "1234" - - axis_device = device.AxisNetworkDevice(hass, entry) - hass.data[device.DOMAIN] = {axis_device.serial: axis_device} - - with patch.object(device, "get_device", return_value=mock_coro(api)), patch.object( - device, "async_dispatcher_send" - ) as mock_dispatcher: - await axis_device.async_setup() - await hass.async_block_till_done() - - axis_device.async_connection_status_callback(status=False) - - assert not axis_device.available - assert len(mock_dispatcher.mock_calls) == 1 + device = await setup_axis_integration(hass) + device.async_connection_status_callback(status=False) + assert not device.available async def test_device_reset(hass): """Successfully reset device.""" - entry = MockConfigEntry( - domain=device.DOMAIN, data=ENTRY_CONFIG, options=ENTRY_OPTIONS - ) - - api = Mock() - api.vapix.get_param.return_value = "1234" - - axis_device = device.AxisNetworkDevice(hass, entry) - hass.data[device.DOMAIN] = {axis_device.serial: axis_device} - - with patch.object(device, "get_device", return_value=mock_coro(api)): - await axis_device.async_setup() - await hass.async_block_till_done() - - await axis_device.async_reset() + device = await setup_axis_integration(hass) + result = await device.async_reset() + assert result is True - assert len(api.stop.mock_calls) == 1 - assert len(hass.states.async_all()) == 0 - assert len(axis_device.listeners) == 0 - -async def test_device_not_accessible(): +async def test_device_not_accessible(hass): """Failed setup schedules a retry of setup.""" - hass = Mock() - hass.data = dict() - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = ENTRY_OPTIONS - - axis_device = device.AxisNetworkDevice(hass, entry) - with patch.object( - device, "get_device", side_effect=errors.CannotConnect - ), pytest.raises(device.ConfigEntryNotReady): - await axis_device.async_setup() + axis.device, "get_device", side_effect=axis.errors.CannotConnect + ), pytest.raises(axis.device.ConfigEntryNotReady): + await setup_axis_integration(hass) + assert hass.data[axis.DOMAIN] == {} - assert not hass.helpers.event.async_call_later.mock_calls - -async def test_device_unknown_error(): +async def test_device_unknown_error(hass): """Unknown errors are handled.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = ENTRY_OPTIONS - - axis_device = device.AxisNetworkDevice(hass, entry) - - with patch.object(device, "get_device", side_effect=Exception): - assert await axis_device.async_setup() is False - - assert not hass.helpers.event.async_call_later.mock_calls + with patch.object(axis.device, "get_device", side_effect=Exception): + await setup_axis_integration(hass) + assert hass.data[axis.DOMAIN] == {} async def test_new_event_sends_signal(hass): @@ -175,9 +182,9 @@ async def test_new_event_sends_signal(hass): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = device.AxisNetworkDevice(hass, entry) + axis_device = axis.device.AxisNetworkDevice(hass, entry) - with patch.object(device, "async_dispatcher_send") as mock_dispatch_send: + with patch.object(axis.device, "async_dispatcher_send") as mock_dispatch_send: axis_device.async_event_callback(action="add", event_id="event") await hass.async_block_till_done() @@ -191,7 +198,7 @@ async def test_shutdown(): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = device.AxisNetworkDevice(hass, entry) + axis_device = axis.device.AxisNetworkDevice(hass, entry) axis_device.api = Mock() axis_device.shutdown(None) @@ -199,39 +206,25 @@ async def test_shutdown(): assert len(axis_device.api.stop.mock_calls) == 1 -async def test_get_device(hass): - """Successful call.""" - with patch("axis.param_cgi.Params.update_brand", return_value=mock_coro()), patch( - "axis.param_cgi.Params.update_properties", return_value=mock_coro() - ), patch("axis.port_cgi.Ports.update", return_value=mock_coro()): - assert await device.get_device(hass, DEVICE_DATA) - - async def test_get_device_fails(hass): """Device unauthorized yields authentication required error.""" - import axis - with patch( - "axis.param_cgi.Params.update_brand", side_effect=axis.Unauthorized - ), pytest.raises(errors.AuthenticationRequired): - await device.get_device(hass, DEVICE_DATA) + "axis.param_cgi.Params.update_brand", side_effect=axislib.Unauthorized + ), pytest.raises(axis.errors.AuthenticationRequired): + await axis.device.get_device(hass, DEVICE_DATA) async def test_get_device_device_unavailable(hass): """Device unavailable yields cannot connect error.""" - import axis - with patch( - "axis.param_cgi.Params.update_brand", side_effect=axis.RequestError - ), pytest.raises(errors.CannotConnect): - await device.get_device(hass, DEVICE_DATA) + "axis.param_cgi.Params.update_brand", side_effect=axislib.RequestError + ), pytest.raises(axis.errors.CannotConnect): + await axis.device.get_device(hass, DEVICE_DATA) async def test_get_device_unknown_error(hass): """Device yield unknown error.""" - import axis - with patch( - "axis.param_cgi.Params.update_brand", side_effect=axis.AxisException - ), pytest.raises(errors.AuthenticationRequired): - await device.get_device(hass, DEVICE_DATA) + "axis.param_cgi.Params.update_brand", side_effect=axislib.AxisException + ), pytest.raises(axis.errors.AuthenticationRequired): + await axis.device.get_device(hass, DEVICE_DATA) diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 406e3170ab203c..844cfedf7fe927 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -2,11 +2,12 @@ from unittest.mock import Mock, call as mock_call -from homeassistant import config_entries from homeassistant.components import axis import homeassistant.components.switch as switch from homeassistant.setup import async_setup_component +from .test_device import NAME, setup_axis_integration + EVENTS = [ { "operation": "Initialized", @@ -26,52 +27,6 @@ }, ] -ENTRY_CONFIG = { - axis.CONF_DEVICE: { - axis.config_flow.CONF_HOST: "1.2.3.4", - axis.config_flow.CONF_USERNAME: "user", - axis.config_flow.CONF_PASSWORD: "pass", - axis.config_flow.CONF_PORT: 80, - }, - axis.config_flow.CONF_MAC: "1234ABCD", - axis.config_flow.CONF_MODEL: "model", - axis.config_flow.CONF_NAME: "model 0", -} - -ENTRY_OPTIONS = { - axis.CONF_CAMERA: False, - axis.CONF_EVENTS: True, - axis.CONF_TRIGGER_TIME: 0, -} - - -async def setup_device(hass): - """Load the Axis switch platform.""" - from axis import AxisDevice - - loop = Mock() - - config_entry = config_entries.ConfigEntry( - 1, - axis.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, - options=ENTRY_OPTIONS, - ) - device = axis.AxisNetworkDevice(hass, config_entry) - device.api = AxisDevice(loop=loop, **config_entry.data[axis.CONF_DEVICE]) - hass.data[axis.DOMAIN] = {device.serial: device} - device.api.enable_events(event_callback=device.async_event_callback) - - await hass.config_entries.async_forward_entry_setup(config_entry, "switch") - # To flush out the service call to update the group - await hass.async_block_till_done() - - return device - async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" @@ -84,14 +39,15 @@ async def test_platform_manually_configured(hass): async def test_no_switches(hass): """Test that no output events in Axis results in no switch entities.""" - await setup_device(hass) + await setup_axis_integration(hass) assert not hass.states.async_entity_ids("switch") async def test_switches(hass): """Test that switches are loaded properly.""" - device = await setup_device(hass) + device = await setup_axis_integration(hass) + device.api.vapix.ports = {"0": Mock(), "1": Mock()} device.api.vapix.ports["0"].name = "Doorbell" device.api.vapix.ports["1"].name = "" @@ -100,24 +56,24 @@ async def test_switches(hass): device.api.stream.event.manage_event(event) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_entity_ids("switch")) == 2 - relay_0 = hass.states.get("switch.model_0_doorbell") + relay_0 = hass.states.get(f"switch.{NAME}_doorbell") assert relay_0.state == "off" - assert relay_0.name == "model 0 Doorbell" + assert relay_0.name == f"{NAME} Doorbell" - relay_1 = hass.states.get("switch.model_0_relay_1") + relay_1 = hass.states.get(f"switch.{NAME}_relay_1") assert relay_1.state == "on" - assert relay_1.name == "model 0 Relay 1" + assert relay_1.name == f"{NAME} Relay 1" device.api.vapix.ports["0"].action = Mock() await hass.services.async_call( - "switch", "turn_on", {"entity_id": "switch.model_0_doorbell"}, blocking=True + "switch", "turn_on", {"entity_id": f"switch.{NAME}_doorbell"}, blocking=True ) await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.model_0_doorbell"}, blocking=True + "switch", "turn_off", {"entity_id": f"switch.{NAME}_doorbell"}, blocking=True ) assert device.api.vapix.ports["0"].action.call_args_list == [ From 4bf15a07a34c24e9c4939cd05ea2c86d8a013b2f Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 3 Jan 2020 00:32:16 +0000 Subject: [PATCH 2666/3953] [ci skip] Translation update --- .../components/gios/.translations/fr.json | 16 ++++++++++++ .../gios/.translations/zh-Hant.json | 20 ++++++++++++++ .../components/local_ip/.translations/fr.json | 16 ++++++++++++ .../components/local_ip/.translations/no.json | 16 ++++++++++++ .../local_ip/.translations/zh-Hant.json | 16 ++++++++++++ .../components/sensor/.translations/fr.json | 26 +++++++++---------- .../components/tesla/.translations/hu.json | 9 +++++++ 7 files changed, 106 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/gios/.translations/fr.json create mode 100644 homeassistant/components/gios/.translations/zh-Hant.json create mode 100644 homeassistant/components/local_ip/.translations/fr.json create mode 100644 homeassistant/components/local_ip/.translations/no.json create mode 100644 homeassistant/components/local_ip/.translations/zh-Hant.json diff --git a/homeassistant/components/gios/.translations/fr.json b/homeassistant/components/gios/.translations/fr.json new file mode 100644 index 00000000000000..f943adfe1c335d --- /dev/null +++ b/homeassistant/components/gios/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_sensors_data": "Donn\u00e9es des capteurs non valides pour cette station de mesure.", + "wrong_station_id": "L'identifiant de la station de mesure n'est pas correct." + }, + "step": { + "user": { + "data": { + "name": "Nom de l'int\u00e9gration", + "station_id": "Identifiant de la station de mesure" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/zh-Hant.json b/homeassistant/components/gios/.translations/zh-Hant.json new file mode 100644 index 00000000000000..19d13572c72b2f --- /dev/null +++ b/homeassistant/components/gios/.translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 GIO\u015a \u4f3a\u670d\u5668\u3002", + "invalid_sensors_data": "\u6b64\u76e3\u6e2c\u7ad9\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6548\u3002", + "wrong_station_id": "\u76e3\u6e2c\u7ad9 ID \u4e0d\u6b63\u78ba\u3002" + }, + "step": { + "user": { + "data": { + "name": "\u6574\u5408\u540d\u7a31", + "station_id": "\u76e3\u6e2c\u7ad9 ID" + }, + "description": "\u8a2d\u5b9a GIO\u015a\uff08\u6ce2\u862d\u7e3d\u74b0\u5883\u4fdd\u8b77\u7763\u5bdf\u8655\uff09\u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u5047\u5982\u9700\u8981\u5354\u52a9\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a\uff08\u6ce2\u862d\u7e3d\u74b0\u5883\u4fdd\u8b77\u7763\u5bdf\u8655\uff09" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/fr.json b/homeassistant/components/local_ip/.translations/fr.json new file mode 100644 index 00000000000000..0d3c61c385b8ed --- /dev/null +++ b/homeassistant/components/local_ip/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "L'int\u00e9gration est d\u00e9j\u00e0 configur\u00e9e avec un capteur existant portant ce nom" + }, + "step": { + "user": { + "data": { + "name": "Nom du capteur" + }, + "title": "Adresse IP locale" + } + }, + "title": "Adresse IP locale" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/no.json b/homeassistant/components/local_ip/.translations/no.json new file mode 100644 index 00000000000000..cb3b96a894043e --- /dev/null +++ b/homeassistant/components/local_ip/.translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integrasjonen er allerede konfigurert med en eksisterende sensor med det navnet" + }, + "step": { + "user": { + "data": { + "name": "Sensor navn" + }, + "title": "Lokal IP adresse" + } + }, + "title": "Lokal IP adresse" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/zh-Hant.json b/homeassistant/components/local_ip/.translations/zh-Hant.json new file mode 100644 index 00000000000000..ec50980b6eae09 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u6574\u5408\u5df2\u7d93\u8a2d\u5b9a\u4e26\u6709\u73fe\u6709\u50b3\u611f\u5668\u4f7f\u7528\u76f8\u540c\u540d\u7a31" + }, + "step": { + "user": { + "data": { + "name": "\u50b3\u611f\u5668\u540d\u7a31" + }, + "title": "\u672c\u5730 IP \u4f4d\u5740" + } + }, + "title": "\u672c\u5730 IP \u4f4d\u5740" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json index 1ce2592410d163..800f44a3fd6da0 100644 --- a/homeassistant/components/sensor/.translations/fr.json +++ b/homeassistant/components/sensor/.translations/fr.json @@ -4,22 +4,22 @@ "is_battery_level": "Niveau de la batterie de {entity_name}", "is_humidity": "Humidit\u00e9 de {entity_name}", "is_illuminance": "\u00c9clairement de {entity_name}", - "is_power": "{entity_name} puissance", - "is_pressure": "{entity_name} pression", - "is_signal_strength": "{entity_name} force du signal", - "is_temperature": "La temp\u00e9rature de {entity_name}", - "is_timestamp": "{entity_name} horodatage", + "is_power": "Puissance de {entity_name}", + "is_pressure": "Pression de {entity_name}", + "is_signal_strength": "Force du signal de {entity_name}", + "is_temperature": "Temp\u00e9rature de {entity_name}", + "is_timestamp": "Horodatage de {entity_name}", "is_value": "La valeur actuelle de {entity_name} " }, "trigger_type": { - "battery_level": "Le niveau de la batterie de {entity_name}", - "humidity": "L'humidit\u00e9 de {entity_name}", - "illuminance": "L'\u00e9clairement de {entity_name}", - "power": "{entity_name} puissance", - "pressure": "{entity_name} pression", - "signal_strength": "{entity_name} force du signal", - "temperature": "La temp\u00e9rature de {entity_name}", - "timestamp": "{entity_name} horodatage", + "battery_level": "{entity_name} modification du niveau de batterie", + "humidity": "{entity_name} modification de l'humidit\u00e9", + "illuminance": "{entity_name} modification de l'\u00e9clairement", + "power": "{entity_name} modification de la puissance", + "pressure": "{entity_name} modification de la pression", + "signal_strength": "{entity_name} modification de la force du signal", + "temperature": "{entity_name} modification de temp\u00e9rature", + "timestamp": "{entity_name} modification d'horodatage", "value": "Changements de valeur de {entity_name} " } } diff --git a/homeassistant/components/tesla/.translations/hu.json b/homeassistant/components/tesla/.translations/hu.json index 7a9a3deff4953d..01090bbfa9e95e 100644 --- a/homeassistant/components/tesla/.translations/hu.json +++ b/homeassistant/components/tesla/.translations/hu.json @@ -17,5 +17,14 @@ } }, "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Szkennel\u00e9sek k\u00f6z\u00f6tti m\u00e1sodpercek" + } + } + } } } \ No newline at end of file From 4c6e10a988b4059fe45f400ce246f30ff1cd20f7 Mon Sep 17 00:00:00 2001 From: Josh Bendavid Date: Thu, 2 Jan 2020 20:46:32 -0500 Subject: [PATCH 2667/3953] Cleanup of state handling in webostv (#30416) * cleanup unnecessary manipulation of state variables * update unit test --- .../components/webostv/media_player.py | 22 ++++--------------- tests/components/webostv/test_media_player.py | 3 ++- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 5e58cdf7a2f40a..4652d6385c1263 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -30,8 +30,7 @@ CONF_NAME, ENTITY_MATCH_ALL, STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, + STATE_ON, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.script import Script @@ -121,8 +120,6 @@ def __init__(self, client, name, customize, on_script=None): # Assume that the TV is not muted self._muted = False - # Assume that the TV is in Play mode - self._playing = True self._volume = 0 self._current_source = None self._current_source_id = None @@ -172,7 +169,7 @@ async def async_handle_state_update(self): if self._current_source_id == "": self._state = STATE_OFF else: - self._state = STATE_PLAYING + self._state = STATE_ON self.update_sources() @@ -325,16 +322,12 @@ async def async_set_volume_level(self, volume): @cmd async def async_mute_volume(self, mute): """Send mute command.""" - self._muted = mute await self._client.set_mute(mute) @cmd async def async_media_play_pause(self): - """Simulate play pause media player.""" - if self._playing: - await self.media_pause() - else: - await self.media_play() + """Client pause command acts as a play-pause toggle.""" + await self._client.pause() @cmd async def async_select_source(self, source): @@ -343,12 +336,9 @@ async def async_select_source(self, source): if source_dict is None: _LOGGER.warning("Source %s not found for %s", source, self.name) return - self._current_source_id = source_dict["id"] if source_dict.get("title"): - self._current_source = source_dict["title"] await self._client.launch_app(source_dict["id"]) elif source_dict.get("label"): - self._current_source = source_dict["label"] await self._client.set_input(source_dict["id"]) @cmd @@ -389,15 +379,11 @@ async def async_play_media(self, media_type, media_id, **kwargs): @cmd async def async_media_play(self): """Send play command.""" - self._playing = True - self._state = STATE_PLAYING await self._client.play() @cmd async def async_media_pause(self): """Send media pause command to media player.""" - self._playing = False - self._state = STATE_PAUSED await self._client.pause() @cmd diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 4dcda9eb9085f8..b0be238f971df4 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -21,6 +21,7 @@ CONF_HOST, CONF_NAME, SERVICE_VOLUME_MUTE, + STATE_ON, ) from homeassistant.setup import async_setup_component @@ -78,7 +79,7 @@ async def test_select_source_with_empty_source_list(hass, client): await hass.services.async_call(media_player.DOMAIN, SERVICE_SELECT_SOURCE, data) await hass.async_block_till_done() - assert hass.states.is_state(ENTITY_ID, "playing") + assert hass.states.is_state(ENTITY_ID, STATE_ON) client.launch_app.assert_not_called() client.set_input.assert_not_called() From ef8eefc7a0fe53ce3d2ceb9efd8f4dfb2380c0e2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 2 Jan 2020 21:21:09 -0500 Subject: [PATCH 2668/3953] Remove ZHA establish device mappings function (#30423) * remove establish_device_mappings * inline init --- homeassistant/components/zha/__init__.py | 2 - .../components/zha/core/registries.py | 208 ++++++++---------- tests/components/zha/conftest.py | 2 - 3 files changed, 96 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 7303367d485506..377c77bf601f83 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -27,7 +27,6 @@ DOMAIN, RadioType, ) -from .core.registries import establish_device_mappings DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(ha_const.CONF_TYPE): cv.string}) @@ -87,7 +86,6 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ - establish_device_mappings() for component in COMPONENTS: hass.data[DATA_ZHA][component] = hass.data[DATA_ZHA].get(component, {}) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index d2ba0243a5c97f..37acffd39d0b02 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -33,142 +33,126 @@ from .const import CONTROLLER, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, RadioType from .decorators import CALLABLE_T, DictRegistry, SetRegistry +SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 +SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 +SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 + +REMOTE_DEVICE_TYPES = { + zigpy.profiles.zha.PROFILE_ID: [ + zigpy.profiles.zha.DeviceType.COLOR_CONTROLLER, + zigpy.profiles.zha.DeviceType.COLOR_DIMMER_SWITCH, + zigpy.profiles.zha.DeviceType.COLOR_SCENE_CONTROLLER, + zigpy.profiles.zha.DeviceType.DIMMER_SWITCH, + zigpy.profiles.zha.DeviceType.NON_COLOR_CONTROLLER, + zigpy.profiles.zha.DeviceType.NON_COLOR_SCENE_CONTROLLER, + zigpy.profiles.zha.DeviceType.REMOTE_CONTROL, + zigpy.profiles.zha.DeviceType.SCENE_SELECTOR, + ], + zigpy.profiles.zll.PROFILE_ID: [ + zigpy.profiles.zll.DeviceType.COLOR_CONTROLLER, + zigpy.profiles.zll.DeviceType.COLOR_SCENE_CONTROLLER, + zigpy.profiles.zll.DeviceType.CONTROL_BRIDGE, + zigpy.profiles.zll.DeviceType.CONTROLLER, + zigpy.profiles.zll.DeviceType.SCENE_CONTROLLER, + ], +} + +SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { + # 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 = {zcl.clusters.general.OnOff: BINARY_SENSOR} + +SWITCH_CLUSTERS = SetRegistry() + BINARY_SENSOR_CLUSTERS = SetRegistry() +BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) + BINDABLE_CLUSTERS = SetRegistry() CHANNEL_ONLY_CLUSTERS = SetRegistry() CLUSTER_REPORT_CONFIGS = {} CUSTOM_CLUSTER_MAPPINGS = {} -DEVICE_CLASS = collections.defaultdict(dict) + +DEVICE_CLASS = { + zigpy.profiles.zha.PROFILE_ID: { + SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, + zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, + zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, + zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, + }, + zigpy.profiles.zll.PROFILE_ID: { + zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, + zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, + }, +} + DEVICE_TRACKER_CLUSTERS = SetRegistry() EVENT_RELAY_CLUSTERS = SetRegistry() LIGHT_CLUSTERS = SetRegistry() OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry() -RADIO_TYPES = {} -REMOTE_DEVICE_TYPES = collections.defaultdict(list) -SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} -SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} -SWITCH_CLUSTERS = SetRegistry() -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, -} - -ZIGBEE_CHANNEL_REGISTRY = DictRegistry() - - -def establish_device_mappings(): - """Establish mappings between ZCL objects and HA ZHA objects. - These cannot be module level, as importing bellows must be done in a - in a function. - """ - RADIO_TYPES[RadioType.ezsp.name] = { +RADIO_TYPES = { + RadioType.ezsp.name: { ZHA_GW_RADIO: bellows.ezsp.EZSP, CONTROLLER: bellows.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "EZSP", - } - - RADIO_TYPES[RadioType.deconz.name] = { + }, + RadioType.deconz.name: { ZHA_GW_RADIO: zigpy_deconz.api.Deconz, CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "Deconz", - } - - RADIO_TYPES[RadioType.xbee.name] = { + }, + RadioType.xbee.name: { ZHA_GW_RADIO: zigpy_xbee.api.XBee, CONTROLLER: zigpy_xbee.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "XBee", - } - - RADIO_TYPES[RadioType.zigate.name] = { + }, + RadioType.zigate.name: { ZHA_GW_RADIO: zigpy_zigate.api.ZiGate, CONTROLLER: zigpy_zigate.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "ZiGate", - } - - BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) - - DEVICE_CLASS[zigpy.profiles.zha.PROFILE_ID].update( - { - SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, - zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, - zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, - zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH, - zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, - zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, - zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, - } - ) - - DEVICE_CLASS[zigpy.profiles.zll.PROFILE_ID].update( - { - zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, - zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, - } - ) - - 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} - ) +COMPONENT_CLUSTERS = { + BINARY_SENSOR: BINARY_SENSOR_CLUSTERS, + DEVICE_TRACKER: DEVICE_TRACKER_CLUSTERS, + LIGHT: LIGHT_CLUSTERS, + SWITCH: SWITCH_CLUSTERS, +} - zha = zigpy.profiles.zha - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_DIMMER_SWITCH) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.DIMMER_SWITCH) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.NON_COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append( - zha.DeviceType.NON_COLOR_SCENE_CONTROLLER - ) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.REMOTE_CONTROL) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.SCENE_SELECTOR) - - zll = zigpy.profiles.zll - REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROL_BRIDGE) - REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROLLER) - REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.SCENE_CONTROLLER) +ZIGBEE_CHANNEL_REGISTRY = DictRegistry() def set_or_callable(value): diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index cc8f9366ecb0fc..d8abfb8f227d69 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -9,7 +9,6 @@ from homeassistant import config_entries from homeassistant.components.zha.core.const import COMPONENTS, DATA_ZHA, DOMAIN from homeassistant.components.zha.core.gateway import ZHAGateway -from homeassistant.components.zha.core.registries import establish_device_mappings from homeassistant.components.zha.core.store import async_get_registry from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg @@ -41,7 +40,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. """ - establish_device_mappings() for component in COMPONENTS: hass.data[DATA_ZHA][component] = hass.data[DATA_ZHA].get(component, {}) zha_storage = await async_get_registry(hass) From b097a64010a02fffbff8ca79199f0c53f8a1dc55 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 3 Jan 2020 05:06:33 +0100 Subject: [PATCH 2669/3953] Implemented media_play & media_pause / push to version 0.7.11 of denonavr (#30421) * Implement media_play & media_pause / push to version 0.7.11 of denonavr * fix denonavr version in requirements_test_all.txt --- homeassistant/components/denonavr/manifest.json | 2 +- homeassistant/components/denonavr/media_player.py | 11 +++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 9e084c78e21c92..a80c7d6ea5dd0c 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denonavr", "documentation": "https://www.home-assistant.io/integrations/denonavr", "requirements": [ - "denonavr==0.7.10" + "denonavr==0.7.11" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 46d22187ce1fca..350d065f9d9260 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -94,7 +94,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Denon platform.""" - # Initialize list with receivers to be started receivers = [] @@ -365,9 +364,17 @@ def device_state_attributes(self): return attributes def media_play_pause(self): - """Simulate play pause media player.""" + """Play or pause the media player.""" return self._receiver.toggle_play_pause() + def media_play(self): + """Send play command.""" + return self._receiver.play() + + def media_pause(self): + """Send pause command.""" + return self._receiver.pause() + def media_previous_track(self): """Send previous track command.""" return self._receiver.previous_track() diff --git a/requirements_all.txt b/requirements_all.txt index dd19553b961700..8c85b023b02e58 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -420,7 +420,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.7.10 +denonavr==0.7.11 # homeassistant.components.directv directpy==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 373abe92ec87d2..fb0a7d765138ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -150,7 +150,7 @@ datadog==0.15.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.7.10 +denonavr==0.7.11 # homeassistant.components.directv directpy==0.5 From 3a18ef219b1f4b906f9c0c18955df32bd36cd9c8 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 3 Jan 2020 05:30:26 -0500 Subject: [PATCH 2670/3953] Add RTSP stream support for UVC (Unifi Video Client) integration (#30297) * Add SUPPORT_STREAM to supported_features. * Implement stream_source with channel RTSP URIs. * Add Tests for Stream Support. * Make stream_source async. * Removed unused import. * Re-wrote test to remove warning, and lint error. --- homeassistant/components/uvc/camera.py | 23 +++++++++++++++++++++- tests/components/uvc/test_camera.py | 27 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index b9a6262cd4f3c5..cd6875cdcdcdea 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -6,7 +6,7 @@ from uvcclient import camera as uvc_camera, nvr import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import CONF_PORT, CONF_SSL from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -92,6 +92,17 @@ def name(self): """Return the name of this camera.""" return self._name + @property + def supported_features(self): + """Return supported features.""" + caminfo = self._nvr.get_camera(self._uuid) + channels = caminfo["channels"] + for channel in channels: + if channel["isRtspEnabled"]: + return SUPPORT_STREAM + + return 0 + @property def is_recording(self): """Return true if the camera is recording.""" @@ -199,3 +210,13 @@ def enable_motion_detection(self): def disable_motion_detection(self): """Disable motion detection in camera.""" self.set_motion_detection(False) + + async def stream_source(self): + """Return the source of the stream.""" + caminfo = self._nvr.get_camera(self._uuid) + channels = caminfo["channels"] + for channel in channels: + if channel["isRtspEnabled"]: + return channel["rtspUris"][0] + + return None diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index c77b5d83749324..b95d940bda4777 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -7,6 +7,7 @@ import requests from uvcclient import camera, nvr +from homeassistant.components.camera import SUPPORT_STREAM from homeassistant.components.uvc import camera as uvc from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import setup_component @@ -190,6 +191,26 @@ def setup_method(self, method): "host": "host-a", "internalHost": "host-b", "username": "admin", + "channels": [ + { + "id": "0", + "width": 1920, + "height": 1080, + "fps": 25, + "bitrate": 6000000, + "isRtspEnabled": True, + "rtspUris": ["rtsp://host-a:7447/uuid_rtspchannel_0"], + }, + { + "id": "1", + "width": 1024, + "height": 576, + "fps": 15, + "bitrate": 1200000, + "isRtspEnabled": False, + "rtspUris": ["rtsp://host-a:7447/uuid_rtspchannel_1"], + }, + ], } self.nvr.server_version = (3, 2, 0) @@ -199,6 +220,12 @@ def test_properties(self): assert self.uvc.is_recording assert "Ubiquiti" == self.uvc.brand assert "UVC Fake" == self.uvc.model + assert SUPPORT_STREAM == self.uvc.supported_features + + def test_stream(self): + """Test the RTSP stream URI.""" + stream_source = yield from self.uvc.stream_source() + assert stream_source == "rtsp://host-a:7447/uuid_rtspchannel_0" @mock.patch("uvcclient.store.get_info_store") @mock.patch("uvcclient.camera.UVCCameraClientV320") From f5aa89dd060cf804364865fd637ca7934968478e Mon Sep 17 00:00:00 2001 From: tetienne Date: Fri, 3 Jan 2020 11:31:05 +0100 Subject: [PATCH 2671/3953] Fix set tilt position (#30428) --- homeassistant/components/somfy/cover.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index 5eabe7ee07aa71..b48e326162d5ba 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -91,15 +91,15 @@ def current_cover_tilt_position(self): def set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - self.cover.orientation = kwargs[ATTR_TILT_POSITION] + self.cover.orientation = 100 - kwargs[ATTR_TILT_POSITION] def open_cover_tilt(self, **kwargs): """Open the cover tilt.""" - self.cover.orientation = 100 + self.cover.orientation = 0 def close_cover_tilt(self, **kwargs): """Close the cover tilt.""" - self.cover.orientation = 0 + self.cover.orientation = 100 def stop_cover_tilt(self, **kwargs): """Stop the cover.""" From c130e81638cc39f520fa056ea5af0997eff7586d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 3 Jan 2020 12:31:50 +0200 Subject: [PATCH 2672/3953] Fix number of times seen in debug message (#30429) --- homeassistant/components/bluetooth_le_tracker/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 9c64232c6e9590..523c40c8cfe5fe 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -46,8 +46,8 @@ def see_device(address, name, new_device=False): """Mark a device as seen.""" if new_device: if address in new_devices: - _LOGGER.debug("Seen %s %s times", address, new_devices[address]) new_devices[address] += 1 + _LOGGER.debug("Seen %s %s times", address, new_devices[address]) if new_devices[address] >= MIN_SEEN_NEW: _LOGGER.debug("Adding %s to tracked devices", address) devs_to_track.append(address) From 0a4f3ec1ec83f882c4fe7abea624b9d6db4b2007 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 3 Jan 2020 11:50:53 +0100 Subject: [PATCH 2673/3953] Use config entry unique id for deCONZ (#30122) * Use config entry unique id * Clean up * Backwards compatiblity note * Fix some of Balloobs comments * Bump dependency to v66 * Black somehow missed config flow tests... * Move set unique ID til after possibility to update existing entry --- homeassistant/components/deconz/__init__.py | 25 ++- .../components/deconz/config_flow.py | 67 ++++---- homeassistant/components/deconz/const.py | 1 - .../components/deconz/device_trigger.py | 6 +- homeassistant/components/deconz/gateway.py | 13 +- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_config_flow.py | 90 +++++------ tests/components/deconz/test_gateway.py | 7 +- tests/components/deconz/test_init.py | 146 ++++++------------ tests/components/deconz/test_services.py | 5 +- 12 files changed, 138 insertions(+), 228 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 0ea91d10b191bd..096bc6c2904c65 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -4,8 +4,8 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, CONF_UUID, DOMAIN -from .gateway import DeconzGateway, get_gateway_from_config_entry +from .const import CONF_MASTER_GATEWAY, DOMAIN +from .gateway import DeconzGateway from .services import async_setup_services, async_unload_services CONFIG_SCHEMA = vol.Schema( @@ -35,12 +35,15 @@ async def async_setup_entry(hass, config_entry): if not await gateway.async_setup(): return False - hass.data[DOMAIN][gateway.bridgeid] = gateway + # 0.104 introduced config entry unique id, this makes upgrading possible + if config_entry.unique_id is None: + hass.config_entries.async_update_entry( + config_entry, unique_id=gateway.api.config.bridgeid + ) - await gateway.async_update_device_registry() + hass.data[DOMAIN][config_entry.unique_id] = gateway - if CONF_UUID not in config_entry.data: - await async_add_uuid_to_config_entry(hass, config_entry) + await gateway.async_update_device_registry() await async_setup_services(hass) @@ -51,7 +54,7 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload deCONZ config entry.""" - gateway = hass.data[DOMAIN].pop(config_entry.data[CONF_BRIDGEID]) + gateway = hass.data[DOMAIN].pop(config_entry.unique_id) if not hass.data[DOMAIN]: await async_unload_services(hass) @@ -74,11 +77,3 @@ async def async_update_master_gateway(hass, config_entry): options = {**config_entry.options, CONF_MASTER_GATEWAY: master} hass.config_entries.async_update_entry(config_entry, options=options) - - -async def async_add_uuid_to_config_entry(hass, config_entry): - """Add UUID to config entry to help discovery identify entries.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - config = {**config_entry.data, CONF_UUID: gateway.api.config.uuid} - - hass.config_entries.async_update_entry(config_entry, data=config) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index c84192456d1d8f..0cec6add28c908 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -4,7 +4,12 @@ import async_timeout from pydeconz.errors import RequestError, ResponseError -from pydeconz.utils import async_discovery, async_get_api_key, async_get_gateway_config +from pydeconz.utils import ( + async_discovery, + async_get_api_key, + async_get_bridge_id, + normalize_bridge_id, +) import voluptuous as vol from homeassistant import config_entries @@ -14,11 +19,9 @@ from homeassistant.helpers import aiohttp_client from .const import ( - _LOGGER, CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, - CONF_UUID, DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, @@ -29,15 +32,6 @@ CONF_SERIAL = "serial" -@callback -def configured_gateways(hass): - """Return a set of all configured gateways.""" - return { - entry.data[CONF_BRIDGEID]: entry - for entry in hass.config_entries.async_entries(DOMAIN) - } - - @callback def get_master_gateway(hass): """Return the gateway which is marked as master.""" @@ -62,6 +56,7 @@ def async_get_options_flow(config_entry): def __init__(self): """Initialize the deCONZ config flow.""" + self.bridge_id = None self.bridges = [] self.deconz_config = {} @@ -79,7 +74,11 @@ async def async_step_user(self, user_input=None): if user_input is not None: for bridge in self.bridges: if bridge[CONF_HOST] == user_input[CONF_HOST]: - self.deconz_config = bridge + self.bridge_id = bridge[CONF_BRIDGEID] + self.deconz_config = { + CONF_HOST: bridge[CONF_HOST], + CONF_PORT: bridge[CONF_PORT], + } return await self.async_step_link() self.deconz_config = user_input @@ -95,8 +94,7 @@ async def async_step_user(self, user_input=None): self.bridges = [] if len(self.bridges) == 1: - self.deconz_config = self.bridges[0] - return await self.async_step_link() + return await self.async_step_user(self.bridges[0]) if len(self.bridges) > 1: hosts = [] @@ -141,23 +139,21 @@ async def async_step_link(self, user_input=None): async def _create_entry(self): """Create entry for gateway.""" - if CONF_BRIDGEID not in self.deconz_config: + if not self.bridge_id: session = aiohttp_client.async_get_clientsession(self.hass) try: with async_timeout.timeout(10): - gateway_config = await async_get_gateway_config( + self.bridge_id = await async_get_bridge_id( session, **self.deconz_config ) - self.deconz_config[CONF_BRIDGEID] = gateway_config.bridgeid - self.deconz_config[CONF_UUID] = gateway_config.uuid + await self.async_set_unique_id(self.bridge_id) + self._abort_if_unique_id_configured() except asyncio.TimeoutError: return self.async_abort(reason="no_bridges") - return self.async_create_entry( - title="deCONZ-" + self.deconz_config[CONF_BRIDGEID], data=self.deconz_config - ) + return self.async_create_entry(title=self.bridge_id, data=self.deconz_config) def _update_entry(self, entry, host, port, api_key=None): """Update existing entry.""" @@ -182,27 +178,17 @@ async def async_step_ssdp(self, discovery_info): if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER_URL] != DECONZ_MANUFACTURERURL: return self.async_abort(reason="not_deconz_bridge") - uuid = discovery_info[ssdp.ATTR_UPNP_UDN].replace("uuid:", "") - - _LOGGER.debug("deCONZ gateway discovered (%s)", uuid) - + self.bridge_id = normalize_bridge_id(discovery_info[ssdp.ATTR_UPNP_SERIAL]) parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) for entry in self.hass.config_entries.async_entries(DOMAIN): - if uuid == entry.data.get(CONF_UUID): + if self.bridge_id == entry.unique_id: if entry.source == "hassio": return self.async_abort(reason="already_configured") return self._update_entry(entry, parsed_url.hostname, parsed_url.port) - bridgeid = discovery_info[ssdp.ATTR_UPNP_SERIAL] - if any( - bridgeid == flow["context"][CONF_BRIDGEID] - for flow in self._async_in_progress() - ): - return self.async_abort(reason="already_in_progress") - + await self.async_set_unique_id(self.bridge_id) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context[CONF_BRIDGEID] = bridgeid self.context["title_placeholders"] = {"host": parsed_url.hostname} self.deconz_config = { @@ -217,18 +203,18 @@ async def async_step_hassio(self, user_input=None): This flow is triggered by the discovery component. """ - bridgeid = user_input[CONF_SERIAL] - gateway_entries = configured_gateways(self.hass) + self.bridge_id = normalize_bridge_id(user_input[CONF_SERIAL]) + gateway = self.hass.data.get(DOMAIN, {}).get(self.bridge_id) - if bridgeid in gateway_entries: - entry = gateway_entries[bridgeid] + if gateway: return self._update_entry( - entry, + gateway.config_entry, user_input[CONF_HOST], user_input[CONF_PORT], user_input[CONF_API_KEY], ) + await self.async_set_unique_id(self.bridge_id) self._hassio_discovery = user_input return await self.async_step_hassio_confirm() @@ -239,7 +225,6 @@ async def async_step_hassio_confirm(self, user_input=None): self.deconz_config = { CONF_HOST: self._hassio_discovery[CONF_HOST], CONF_PORT: self._hassio_discovery[CONF_PORT], - CONF_BRIDGEID: self._hassio_discovery[CONF_SERIAL], CONF_API_KEY: self._hassio_discovery[CONF_API_KEY], } diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index a663f99bf736f3..47975750de9950 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -6,7 +6,6 @@ DOMAIN = "deconz" CONF_BRIDGEID = "bridgeid" -CONF_UUID = "uuid" DEFAULT_PORT = 80 DEFAULT_ALLOW_CLIP_SENSOR = False diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 9c8a41453aaf60..9bdfa70b7de520 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -15,9 +15,7 @@ ) from . import DOMAIN -from .config_flow import configured_gateways from .deconz_event import CONF_DECONZ_EVENT, CONF_UNIQUE_ID -from .gateway import get_gateway_from_config_entry CONF_SUBTYPE = "subtype" @@ -287,10 +285,8 @@ def _get_deconz_event_from_device_id(hass, device_id): """Resolve deconz event from device id.""" - deconz_config_entries = configured_gateways(hass) - for config_entry in deconz_config_entries.values(): + for gateway in hass.data.get(DOMAIN): - gateway = get_gateway_from_config_entry(hass, config_entry) for deconz_event in gateway.events: if device_id == deconz_event.device_id: diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 083af2dca6fdc7..04452cc313cae1 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -22,7 +22,6 @@ _LOGGER, CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, - CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, @@ -36,7 +35,7 @@ @callback def get_gateway_from_config_entry(hass, config_entry): """Return gateway with a matching bridge id.""" - return hass.data[DOMAIN][config_entry.data[CONF_BRIDGEID]] + return hass.data[DOMAIN][config_entry.unique_id] class DeconzGateway: @@ -56,7 +55,7 @@ def __init__(self, hass, config_entry) -> None: @property def bridgeid(self) -> str: """Return the unique identifier of the gateway.""" - return self.config_entry.data[CONF_BRIDGEID] + return self.config_entry.unique_id @property def master(self) -> bool: @@ -92,11 +91,9 @@ async def async_update_device_registry(self) -> None: async def async_setup(self) -> bool: """Set up a deCONZ gateway.""" - hass = self.hass - try: self.api = await get_gateway( - hass, + self.hass, self.config_entry.data, self.async_add_device_callback, self.async_connection_status_callback, @@ -110,8 +107,8 @@ async def async_setup(self) -> bool: return False for component in SUPPORTED_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( self.config_entry, component ) ) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 30b00600331535..402244fbc13327 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==65" + "pydeconz==66" ], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 8c85b023b02e58..a6cba26235b8be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1187,7 +1187,7 @@ pydaikin==1.6.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==65 +pydeconz==66 # homeassistant.components.delijn pydelijn==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb0a7d765138ca..93eedd2ccffc69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -410,7 +410,7 @@ pycoolmasternet==0.0.4 pydaikin==1.6.1 # homeassistant.components.deconz -pydeconz==65 +pydeconz==66 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index da8b0a8a7f47e8..f8fe42d10d8833 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,11 +1,20 @@ """Tests for deCONZ config flow.""" import asyncio -from unittest.mock import Mock, patch +from copy import deepcopy +from asynctest import Mock, patch import pydeconz from homeassistant.components import ssdp from homeassistant.components.deconz import config_flow +from homeassistant.components.deconz.const import CONF_BRIDGEID + +from .test_gateway import ( + BRIDGEID, + DECONZ_WEB_REQUEST, + ENTRY_CONFIG, + setup_deconz_integration, +) from tests.common import MockConfigEntry @@ -14,7 +23,7 @@ async def test_flow_works(hass, aioclient_mock): """Test that config flow works.""" aioclient_mock.get( pydeconz.utils.URL_DISCOVER, - json=[{"id": "id", "internalipaddress": "1.2.3.4", "internalport": 80}], + json=[{"id": BRIDGEID, "internalipaddress": "1.2.3.4", "internalport": 80}], headers={"content-type": "application/json"}, ) aioclient_mock.post( @@ -35,9 +44,8 @@ async def test_flow_works(hass, aioclient_mock): ) assert result["type"] == "create_entry" - assert result["title"] == "deCONZ-id" + assert result["title"] == BRIDGEID assert result["data"] == { - config_flow.CONF_BRIDGEID: "id", config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80, config_flow.CONF_API_KEY: "1234567890ABCDEF", @@ -117,12 +125,12 @@ async def test_user_step_two_bridges_selection(hass, aioclient_mock): flow.hass = hass flow.bridges = [ { - config_flow.CONF_BRIDGEID: "id1", + CONF_BRIDGEID: "id1", config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80, }, { - config_flow.CONF_BRIDGEID: "id2", + CONF_BRIDGEID: "id2", config_flow.CONF_HOST: "5.6.7.8", config_flow.CONF_PORT: 80, }, @@ -241,25 +249,22 @@ async def test_bridge_discovery_update_existing_entry(hass): """Test if a discovered bridge has already been configured.""" entry = MockConfigEntry( domain=config_flow.DOMAIN, - data={ - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_BRIDGEID: "123ABC", - config_flow.CONF_UUID: "456DEF", - }, + source="user", + data={config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80}, + unique_id=BRIDGEID, ) entry.add_to_hass(hass) gateway = Mock() gateway.config_entry = entry - hass.data[config_flow.DOMAIN] = {"123ABC": gateway} + hass.data[config_flow.DOMAIN] = {BRIDGEID: gateway} result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ ssdp.ATTR_SSDP_LOCATION: "http://mock-deconz/", ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL, - ssdp.ATTR_UPNP_SERIAL: "123ABC", - ssdp.ATTR_UPNP_UDN: "uuid:456DEF", + ssdp.ATTR_UPNP_SERIAL: BRIDGEID, }, context={"source": "ssdp"}, ) @@ -277,8 +282,8 @@ async def test_bridge_discovery_dont_update_existing_hassio_entry(hass): data={ config_flow.CONF_HOST: "core-deconz", config_flow.CONF_BRIDGEID: "123ABC", - config_flow.CONF_UUID: "456DEF", }, + unique_id="123ABC", ) entry.add_to_hass(hass) @@ -292,7 +297,6 @@ async def test_bridge_discovery_dont_update_existing_hassio_entry(hass): ssdp.ATTR_SSDP_LOCATION: "http://mock-deconz/", ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.DECONZ_MANUFACTURERURL, ssdp.ATTR_UPNP_SERIAL: "123ABC", - ssdp.ATTR_UPNP_UDN: "uuid:456DEF", }, context={"source": "ssdp"}, ) @@ -306,11 +310,12 @@ async def test_create_entry(hass, aioclient_mock): """Test that _create_entry work and that bridgeid can be requested.""" aioclient_mock.get( "http://1.2.3.4:80/api/1234567890ABCDEF/config", - json={"bridgeid": "123ABC", "uuid": "456DEF"}, + json={"bridgeid": BRIDGEID, "uuid": "456DEF"}, headers={"content-type": "application/json"}, ) flow = config_flow.DeconzFlowHandler() + flow.context = {} flow.hass = hass flow.deconz_config = { config_flow.CONF_HOST: "1.2.3.4", @@ -321,13 +326,11 @@ async def test_create_entry(hass, aioclient_mock): result = await flow._create_entry() assert result["type"] == "create_entry" - assert result["title"] == "deCONZ-123ABC" + assert result["title"] == BRIDGEID assert result["data"] == { - config_flow.CONF_BRIDGEID: "123ABC", config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80, config_flow.CONF_API_KEY: "1234567890ABCDEF", - config_flow.CONF_UUID: "456DEF", } @@ -342,7 +345,7 @@ async def test_create_entry_timeout(hass, aioclient_mock): } with patch( - "homeassistant.components.deconz.config_flow.async_get_gateway_config", + "homeassistant.components.deconz.config_flow.async_get_bridge_id", side_effect=asyncio.TimeoutError, ): result = await flow._create_entry() @@ -353,54 +356,44 @@ async def test_create_entry_timeout(hass, aioclient_mock): async def test_hassio_update_instance(hass): """Test we can update an existing config entry.""" - entry = MockConfigEntry( - domain=config_flow.DOMAIN, - data={ - config_flow.CONF_BRIDGEID: "id", - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_PORT: 40850, - config_flow.CONF_API_KEY: "secret", - }, + data = deepcopy(DECONZ_WEB_REQUEST) + entry_config = deepcopy(ENTRY_CONFIG) + gateway = await setup_deconz_integration( + hass, entry_config, options={}, get_state_response=data ) - entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ - config_flow.CONF_HOST: "mock-deconz", + config_flow.CONF_HOST: "2.3.4.5", config_flow.CONF_PORT: 8080, config_flow.CONF_API_KEY: "updated", - config_flow.CONF_SERIAL: "id", + config_flow.CONF_SERIAL: BRIDGEID, }, context={"source": "hassio"}, ) assert result["type"] == "abort" assert result["reason"] == "updated_instance" - assert entry.data[config_flow.CONF_HOST] == "mock-deconz" - assert entry.data[config_flow.CONF_PORT] == 8080 - assert entry.data[config_flow.CONF_API_KEY] == "updated" + assert gateway.config_entry.data[config_flow.CONF_HOST] == "2.3.4.5" + assert gateway.config_entry.data[config_flow.CONF_PORT] == 8080 + assert gateway.config_entry.data[config_flow.CONF_API_KEY] == "updated" async def test_hassio_dont_update_instance(hass): """Test we can update an existing config entry.""" - entry = MockConfigEntry( - domain=config_flow.DOMAIN, - data={ - config_flow.CONF_BRIDGEID: "id", - config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_PORT: 8080, - config_flow.CONF_API_KEY: "secret", - }, + data = deepcopy(DECONZ_WEB_REQUEST) + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) - entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, data={ config_flow.CONF_HOST: "1.2.3.4", - config_flow.CONF_PORT: 8080, - config_flow.CONF_API_KEY: "secret", - config_flow.CONF_SERIAL: "id", + config_flow.CONF_PORT: 80, + config_flow.CONF_API_KEY: "ABCDEF", + config_flow.CONF_SERIAL: BRIDGEID, }, context={"source": "hassio"}, ) @@ -417,7 +410,7 @@ async def test_hassio_confirm(hass): "addon": "Mock Addon", config_flow.CONF_HOST: "mock-deconz", config_flow.CONF_PORT: 80, - config_flow.CONF_SERIAL: "id", + config_flow.CONF_SERIAL: BRIDGEID, config_flow.CONF_API_KEY: "1234567890ABCDEF", }, context={"source": "hassio"}, @@ -434,7 +427,6 @@ async def test_hassio_confirm(hass): assert result["result"].data == { config_flow.CONF_HOST: "mock-deconz", config_flow.CONF_PORT: 80, - config_flow.CONF_BRIDGEID: "id", config_flow.CONF_API_KEY: "1234567890ABCDEF", } diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 288868f1bec215..656b610f4a2698 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -10,14 +10,12 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect -BRIDGEID = "0123456789" +BRIDGEID = "01234E56789A" ENTRY_CONFIG = { deconz.config_flow.CONF_API_KEY: "ABCDEF", - deconz.config_flow.CONF_BRIDGEID: BRIDGEID, deconz.config_flow.CONF_HOST: "1.2.3.4", deconz.config_flow.CONF_PORT: 80, - deconz.config_flow.CONF_UUID: "456DEF", } DECONZ_CONFIG = { @@ -60,7 +58,8 @@ async def setup_deconz_integration(hass, config, options, get_state_response): hass.config_entries._entries.append(config_entry) - return hass.data[deconz.DOMAIN].get(config[deconz.CONF_BRIDGEID]) + bridgeid = get_state_response["config"]["bridgeid"] + return hass.data[deconz.DOMAIN].get(bridgeid) async def test_gateway_setup(hass): diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 3c6e02ab41cae8..806fd7ed4aa289 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,13 +1,14 @@ """Test deCONZ component setup process.""" import asyncio +from copy import deepcopy -from asynctest import Mock, patch +from asynctest import patch import pytest from homeassistant.components import deconz from homeassistant.exceptions import ConfigEntryNotReady -from tests.common import MockConfigEntry +from .test_gateway import DECONZ_WEB_REQUEST, ENTRY_CONFIG, setup_deconz_integration ENTRY1_HOST = "1.2.3.4" ENTRY1_PORT = 80 @@ -34,138 +35,83 @@ async def setup_entry(hass, entry): async def test_setup_entry_fails(hass): """Test setup entry fails if deCONZ is not available.""" - entry = Mock() - entry.data = { - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - } + data = deepcopy(DECONZ_WEB_REQUEST) with patch("pydeconz.DeconzSession.initialize", side_effect=Exception): - await deconz.async_setup_entry(hass, entry) + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + assert not hass.data[deconz.DOMAIN] async def test_setup_entry_no_available_bridge(hass): """Test setup entry fails if deCONZ is not available.""" - entry = Mock() - entry.data = { - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - } + data = deepcopy(DECONZ_WEB_REQUEST) with patch( "pydeconz.DeconzSession.initialize", side_effect=asyncio.TimeoutError ), pytest.raises(ConfigEntryNotReady): - await deconz.async_setup_entry(hass, entry) + await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) async def test_setup_entry_successful(hass): """Test setup entry is successful.""" - entry = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, - deconz.CONF_UUID: ENTRY1_UUID, - }, + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) - entry.add_to_hass(hass) - - await setup_entry(hass, entry) - assert ENTRY1_BRIDGEID in hass.data[deconz.DOMAIN] - assert hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].master + assert hass.data[deconz.DOMAIN] + assert gateway.bridgeid in hass.data[deconz.DOMAIN] + assert hass.data[deconz.DOMAIN][gateway.bridgeid].master async def test_setup_entry_multiple_gateways(hass): """Test setup entry is successful with multiple gateways.""" - entry = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, - deconz.CONF_UUID: ENTRY1_UUID, - }, + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) - entry.add_to_hass(hass) - - entry2 = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY2_HOST, - deconz.config_flow.CONF_PORT: ENTRY2_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY, - deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID, - deconz.CONF_UUID: ENTRY2_UUID, - }, - ) - entry2.add_to_hass(hass) - await setup_entry(hass, entry) - await setup_entry(hass, entry2) + data2 = deepcopy(DECONZ_WEB_REQUEST) + data2["config"]["bridgeid"] = "01234E56789B" + gateway2 = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data2 + ) - assert ENTRY1_BRIDGEID in hass.data[deconz.DOMAIN] - assert hass.data[deconz.DOMAIN][ENTRY1_BRIDGEID].master - assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN] - assert not hass.data[deconz.DOMAIN][ENTRY2_BRIDGEID].master + assert len(hass.data[deconz.DOMAIN]) == 2 + assert hass.data[deconz.DOMAIN][gateway.bridgeid].master + assert not hass.data[deconz.DOMAIN][gateway2.bridgeid].master async def test_unload_entry(hass): """Test being able to unload an entry.""" - entry = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, - deconz.CONF_UUID: ENTRY1_UUID, - }, + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) - entry.add_to_hass(hass) - - await setup_entry(hass, entry) - - with patch.object(deconz.DeconzGateway, "async_reset", return_value=True): - assert await deconz.async_unload_entry(hass, entry) + assert hass.data[deconz.DOMAIN] + assert await deconz.async_unload_entry(hass, gateway.config_entry) assert not hass.data[deconz.DOMAIN] async def test_unload_entry_multiple_gateways(hass): """Test being able to unload an entry and master gateway gets moved.""" - entry = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY1_HOST, - deconz.config_flow.CONF_PORT: ENTRY1_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY1_API_KEY, - deconz.CONF_BRIDGEID: ENTRY1_BRIDGEID, - deconz.CONF_UUID: ENTRY1_UUID, - }, + data = deepcopy(DECONZ_WEB_REQUEST) + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data ) - entry.add_to_hass(hass) - - entry2 = MockConfigEntry( - domain=deconz.DOMAIN, - data={ - deconz.config_flow.CONF_HOST: ENTRY2_HOST, - deconz.config_flow.CONF_PORT: ENTRY2_PORT, - deconz.config_flow.CONF_API_KEY: ENTRY2_API_KEY, - deconz.CONF_BRIDGEID: ENTRY2_BRIDGEID, - deconz.CONF_UUID: ENTRY2_UUID, - }, + + data2 = deepcopy(DECONZ_WEB_REQUEST) + data2["config"]["bridgeid"] = "01234E56789B" + gateway2 = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data2 ) - entry2.add_to_hass(hass) - await setup_entry(hass, entry) - await setup_entry(hass, entry2) + assert len(hass.data[deconz.DOMAIN]) == 2 - with patch.object(deconz.DeconzGateway, "async_reset", return_value=True): - assert await deconz.async_unload_entry(hass, entry) + assert await deconz.async_unload_entry(hass, gateway.config_entry) - assert ENTRY2_BRIDGEID in hass.data[deconz.DOMAIN] - assert hass.data[deconz.DOMAIN][ENTRY2_BRIDGEID].master + assert len(hass.data[deconz.DOMAIN]) == 1 + assert hass.data[deconz.DOMAIN][gateway2.bridgeid].master diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index fad5444aa00729..32065c60f833b0 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant.components import deconz +from homeassistant.components.deconz.const import CONF_BRIDGEID from .test_gateway import ( BRIDGEID, @@ -99,7 +100,7 @@ async def test_configure_service_with_field(hass): data = { deconz.services.SERVICE_FIELD: "/light/2", - deconz.CONF_BRIDGEID: BRIDGEID, + CONF_BRIDGEID: BRIDGEID, deconz.services.SERVICE_DATA: {"on": True, "attr1": 10, "attr2": 20}, } @@ -203,7 +204,7 @@ async def test_service_refresh_devices(hass): hass, ENTRY_CONFIG, options={}, get_state_response=data ) - data = {deconz.CONF_BRIDGEID: BRIDGEID} + data = {CONF_BRIDGEID: BRIDGEID} with patch( "pydeconz.DeconzSession.request", From fdfedd086bcc6f19bf8e3023381f5c0d12f30624 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 3 Jan 2020 10:52:01 +0000 Subject: [PATCH 2674/3953] Rework FlowManager to use inheritance (#30133) * Pull async_finish_flow/async_create_flow out of ConfigEntries * Towards refactoring * mypy fixes * Mark Flow manager with abc.* annotations * Flake8 fixes * Mypy fixes * Blacken data_entry_flow * Blacken longer signatures caused by mypy changes * test fixes * Test fixes * Fix typo * Avoid protected member lint (W0212) in config_entries * More protected member fixes * Missing await --- homeassistant/auth/__init__.py | 111 ++++--- .../components/auth/mfa_setup_flow.py | 24 +- .../components/config/config_entries.py | 8 +- homeassistant/config_entries.py | 291 +++++++++--------- homeassistant/data_entry_flow.py | 41 ++- tests/components/deconz/test_config_flow.py | 2 +- .../opentherm_gw/test_config_flow.py | 8 +- tests/components/plex/test_config_flow.py | 4 +- tests/components/tesla/test_config_flow.py | 8 +- tests/components/unifi/test_config_flow.py | 2 +- tests/test_config_entries.py | 4 +- tests/test_data_entry_flow.py | 72 +++-- 12 files changed, 315 insertions(+), 260 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index e4437bea840d37..9b3cf49fa22f16 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -67,6 +67,69 @@ async def auth_manager_from_config( return manager +class AuthManagerFlowManager(data_entry_flow.FlowManager): + """Manage authentication flows.""" + + def __init__(self, hass: HomeAssistant, auth_manager: "AuthManager"): + """Init auth manager flows.""" + super().__init__(hass) + self.auth_manager = auth_manager + + async def async_create_flow( + self, + handler_key: Any, + *, + context: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + ) -> data_entry_flow.FlowHandler: + """Create a login flow.""" + auth_provider = self.auth_manager.get_auth_provider(*handler_key) + if not auth_provider: + raise KeyError(f"Unknown auth provider {handler_key}") + return await auth_provider.async_login_flow(context) + + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: Dict[str, Any] + ) -> Dict[str, Any]: + """Return a user as result of login flow.""" + flow = cast(LoginFlow, flow) + + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return result + + # we got final result + if isinstance(result["data"], models.User): + result["result"] = result["data"] + return result + + auth_provider = self.auth_manager.get_auth_provider(*result["handler"]) + if not auth_provider: + raise KeyError(f"Unknown auth provider {result['handler']}") + + credentials = await auth_provider.async_get_or_create_credentials( + result["data"] + ) + + if flow.context.get("credential_only"): + result["result"] = credentials + return result + + # multi-factor module cannot enabled for new credential + # which has not linked to a user yet + if auth_provider.support_mfa and not credentials.is_new: + user = await self.auth_manager.async_get_user_by_credentials(credentials) + if user is not None: + modules = await self.auth_manager.async_get_enabled_mfa(user) + + if modules: + flow.user = user + flow.available_mfa_modules = modules + return await flow.async_step_select_mfa_module() + + result["result"] = await self.auth_manager.async_get_or_create_user(credentials) + return result + + class AuthManager: """Manage the authentication for Home Assistant.""" @@ -82,9 +145,7 @@ def __init__( self._store = store self._providers = providers self._mfa_modules = mfa_modules - self.login_flow = data_entry_flow.FlowManager( - hass, self._async_create_login_flow, self._async_finish_login_flow - ) + self.login_flow = AuthManagerFlowManager(hass, self) @property def auth_providers(self) -> List[AuthProvider]: @@ -417,50 +478,6 @@ async def async_validate_access_token( return refresh_token - async def _async_create_login_flow( - self, handler: _ProviderKey, *, context: Optional[Dict], data: Optional[Any] - ) -> data_entry_flow.FlowHandler: - """Create a login flow.""" - auth_provider = self._providers[handler] - - return await auth_provider.async_login_flow(context) - - async def _async_finish_login_flow( - self, flow: LoginFlow, result: Dict[str, Any] - ) -> Dict[str, Any]: - """Return a user as result of login flow.""" - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - return result - - # we got final result - if isinstance(result["data"], models.User): - result["result"] = result["data"] - return result - - auth_provider = self._providers[result["handler"]] - credentials = await auth_provider.async_get_or_create_credentials( - result["data"] - ) - - if flow.context.get("credential_only"): - result["result"] = credentials - return result - - # multi-factor module cannot enabled for new credential - # which has not linked to a user yet - if auth_provider.support_mfa and not credentials.is_new: - user = await self.async_get_user_by_credentials(credentials) - if user is not None: - modules = await self.async_get_enabled_mfa(user) - - if modules: - flow.user = user - flow.available_mfa_modules = modules - return await flow.async_step_select_mfa_module() - - result["result"] = await self.async_get_or_create_user(credentials) - return result - @callback def _async_get_auth_provider( self, credentials: models.Credentials diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 92926e2e7c5f0a..1b199551a14efe 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -28,25 +28,27 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass): - """Init mfa setup flow manager.""" +class MfaFlowManager(data_entry_flow.FlowManager): + """Manage multi factor authentication flows.""" - async def _async_create_setup_flow(handler, context, data): + async def async_create_flow(self, handler_key, *, context, data): """Create a setup flow. handler is a mfa module.""" - mfa_module = hass.auth.get_auth_mfa_module(handler) + mfa_module = self.hass.auth.get_auth_mfa_module(handler_key) if mfa_module is None: - raise ValueError(f"Mfa module {handler} is not found") + raise ValueError(f"Mfa module {handler_key} is not found") user_id = data.pop("user_id") return await mfa_module.async_setup_flow(user_id) - async def _async_finish_setup_flow(flow, flow_result): - _LOGGER.debug("flow_result: %s", flow_result) - return flow_result + async def async_finish_flow(self, flow, result): + """Complete an mfs setup flow.""" + _LOGGER.debug("flow_result: %s", result) + return result - hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager( - hass, _async_create_setup_flow, _async_finish_setup_flow - ) + +async def async_setup(hass): + """Init mfa setup flow manager.""" + hass.data[DATA_SETUP_FLOW_MGR] = MfaFlowManager(hass) hass.components.websocket_api.async_register_command( WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index dbf0ee8f283de1..22df26cce4e6c4 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -23,12 +23,8 @@ async def async_setup(hass): 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.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options)) + hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options)) hass.components.websocket_api.async_register_command(config_entries_progress) hass.components.websocket_api.async_register_command(system_options_list) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 942998767a1bf2..d1b5c927a2ba2a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -399,6 +399,137 @@ def as_dict(self) -> Dict[str, Any]: } +class ConfigEntriesFlowManager(data_entry_flow.FlowManager): + """Manage all the config entry flows that are in progress.""" + + def __init__( + self, hass: HomeAssistant, config_entries: "ConfigEntries", hass_config: dict + ): + """Initialize the config entry flow manager.""" + super().__init__(hass) + self.config_entries = config_entries + self._hass_config = hass_config + + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: Dict[str, Any] + ) -> Dict[str, Any]: + """Finish a config flow and add an entry.""" + flow = cast(ConfigFlow, flow) + + # Remove notification if no other discovery config entries in progress + if not any( + ent["context"]["source"] in DISCOVERY_SOURCES + for ent in self.hass.config_entries.flow.async_progress() + if ent["flow_id"] != flow.flow_id + ): + self.hass.components.persistent_notification.async_dismiss( + DISCOVERY_NOTIFICATION_ID + ) + + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return result + + # Check if config entry exists with unique ID. Unload it. + existing_entry = None + + if flow.unique_id is not None: + # Abort all flows in progress with same unique ID. + for progress_flow in self.async_progress(): + if ( + progress_flow["handler"] == flow.handler + and progress_flow["flow_id"] != flow.flow_id + and progress_flow["context"].get("unique_id") == flow.unique_id + ): + self.async_abort(progress_flow["flow_id"]) + + # Find existing entry. + for check_entry in self.config_entries.async_entries(result["handler"]): + if check_entry.unique_id == flow.unique_id: + existing_entry = check_entry + break + + # Unload the entry before setting up the new one. + # We will remove it only after the other one is set up, + # so that device customizations are not getting lost. + if ( + existing_entry is not None + and existing_entry.state not in UNRECOVERABLE_STATES + ): + await self.config_entries.async_unload(existing_entry.entry_id) + + entry = ConfigEntry( + version=result["version"], + domain=result["handler"], + title=result["title"], + data=result["data"], + options={}, + system_options={}, + source=flow.context["source"], + connection_class=flow.CONNECTION_CLASS, + unique_id=flow.unique_id, + ) + + await self.config_entries.async_add(entry) + + if existing_entry is not None: + await self.config_entries.async_remove(existing_entry.entry_id) + + result["result"] = entry + return result + + async def async_create_flow( + self, handler_key: Any, *, context: Optional[Dict] = None, data: Any = None + ) -> "ConfigFlow": + """Create a flow for specified handler. + + Handler key is the domain of the component that we want to set up. + """ + try: + integration = await loader.async_get_integration(self.hass, handler_key) + except loader.IntegrationNotFound: + _LOGGER.error("Cannot find integration %s", handler_key) + raise data_entry_flow.UnknownHandler + + # Make sure requirements and dependencies of component are resolved + await async_process_deps_reqs(self.hass, self._hass_config, integration) + + try: + integration.get_platform("config_flow") + except ImportError as err: + _LOGGER.error( + "Error occurred loading config flow for integration %s: %s", + handler_key, + err, + ) + raise data_entry_flow.UnknownHandler + + handler = HANDLERS.get(handler_key) + + if handler is None: + raise data_entry_flow.UnknownHandler + + if not context or "source" not in context: + raise KeyError("Context not set or doesn't have a source set") + + source = context["source"] + + # Create notification. + if source in DISCOVERY_SOURCES: + self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED) + self.hass.components.persistent_notification.async_create( + title="New devices discovered", + message=( + "We have discovered new devices on your network. " + "[Check it out](/config/integrations)" + ), + notification_id=DISCOVERY_NOTIFICATION_ID, + ) + + flow = cast(ConfigFlow, handler()) + flow.init_step = source + return flow + + class ConfigEntries: """Manage the configuration entries. @@ -408,9 +539,7 @@ class ConfigEntries: def __init__(self, hass: HomeAssistant, hass_config: dict) -> None: """Initialize the entry manager.""" self.hass = hass - self.flow = data_entry_flow.FlowManager( - hass, self._async_create_flow, self._async_finish_flow - ) + self.flow = ConfigEntriesFlowManager(hass, self, hass_config) self.options = OptionsFlowManager(hass) self._hass_config = hass_config self._entries: List[ConfigEntry] = [] @@ -445,6 +574,12 @@ def async_entries(self, domain: Optional[str] = None) -> List[ConfigEntry]: return list(self._entries) return [entry for entry in self._entries if entry.domain == domain] + async def async_add(self, entry: ConfigEntry) -> None: + """Add and setup an entry.""" + self._entries.append(entry) + await self.async_setup(entry.entry_id) + self._async_schedule_save() + async def async_remove(self, entry_id: str) -> Dict[str, Any]: """Remove an entry.""" entry = self.async_get_entry(entry_id) @@ -630,123 +765,6 @@ async def async_forward_entry_unload(self, entry: ConfigEntry, domain: str) -> b return await entry.async_unload(self.hass, integration=integration) - async def _async_finish_flow( - self, flow: "ConfigFlow", result: Dict[str, Any] - ) -> Dict[str, Any]: - """Finish a config flow and add an entry.""" - # Remove notification if no other discovery config entries in progress - if not any( - ent["context"]["source"] in DISCOVERY_SOURCES - for ent in self.hass.config_entries.flow.async_progress() - if ent["flow_id"] != flow.flow_id - ): - self.hass.components.persistent_notification.async_dismiss( - DISCOVERY_NOTIFICATION_ID - ) - - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - return result - - # Check if config entry exists with unique ID. Unload it. - existing_entry = None - - if flow.unique_id is not None: - # Abort all flows in progress with same unique ID. - for progress_flow in self.flow.async_progress(): - if ( - progress_flow["handler"] == flow.handler - and progress_flow["flow_id"] != flow.flow_id - and progress_flow["context"].get("unique_id") == flow.unique_id - ): - self.flow.async_abort(progress_flow["flow_id"]) - - # Find existing entry. - for check_entry in self.async_entries(result["handler"]): - if check_entry.unique_id == flow.unique_id: - existing_entry = check_entry - break - - # Unload the entry before setting up the new one. - # We will remove it only after the other one is set up, - # so that device customizations are not getting lost. - if ( - existing_entry is not None - and existing_entry.state not in UNRECOVERABLE_STATES - ): - await self.async_unload(existing_entry.entry_id) - - entry = ConfigEntry( - version=result["version"], - domain=result["handler"], - title=result["title"], - data=result["data"], - options={}, - system_options={}, - source=flow.context["source"], - connection_class=flow.CONNECTION_CLASS, - unique_id=flow.unique_id, - ) - self._entries.append(entry) - - await self.async_setup(entry.entry_id) - - if existing_entry is not None: - await self.async_remove(existing_entry.entry_id) - - self._async_schedule_save() - - result["result"] = entry - return result - - async def _async_create_flow( - self, handler_key: str, *, context: Dict[str, Any], data: Dict[str, Any] - ) -> "ConfigFlow": - """Create a flow for specified handler. - - Handler key is the domain of the component that we want to set up. - """ - try: - integration = await loader.async_get_integration(self.hass, handler_key) - except loader.IntegrationNotFound: - _LOGGER.error("Cannot find integration %s", handler_key) - raise data_entry_flow.UnknownHandler - - # Make sure requirements and dependencies of component are resolved - await async_process_deps_reqs(self.hass, self._hass_config, integration) - - try: - integration.get_platform("config_flow") - except ImportError as err: - _LOGGER.error( - "Error occurred loading config flow for integration %s: %s", - handler_key, - err, - ) - raise data_entry_flow.UnknownHandler - - handler = HANDLERS.get(handler_key) - - if handler is None: - raise data_entry_flow.UnknownHandler - - source = context["source"] - - # Create notification. - if source in DISCOVERY_SOURCES: - self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED) - self.hass.components.persistent_notification.async_create( - title="New devices discovered", - message=( - "We have discovered new devices on your network. " - "[Check it out](/config/integrations)" - ), - notification_id=DISCOVERY_NOTIFICATION_ID, - ) - - flow = cast(ConfigFlow, handler()) - flow.init_step = source - return flow - def _async_schedule_save(self) -> None: """Save the entity registry to a file.""" self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @@ -854,26 +872,23 @@ async def async_step_unignore(self, user_input: Dict[str, Any]) -> Dict[str, Any return self.async_abort(reason="not_implemented") -class OptionsFlowManager: +class OptionsFlowManager(data_entry_flow.FlowManager): """Flow to set options for a configuration entry.""" - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the options manager.""" - self.hass = hass - self.flow = data_entry_flow.FlowManager( - hass, self._async_create_flow, self._async_finish_flow - ) - - async def _async_create_flow( - self, entry_id: str, *, context: Dict[str, Any], data: Dict[str, Any] - ) -> Optional["OptionsFlow"]: + async def async_create_flow( + self, + handler_key: Any, + *, + context: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + ) -> "OptionsFlow": """Create an options flow for a config entry. Entry_id and flow.handler is the same thing to map entry with flow. """ - entry = self.hass.config_entries.async_get_entry(entry_id) + entry = self.hass.config_entries.async_get_entry(handler_key) if entry is None: - return None + raise UnknownEntry(handler_key) if entry.domain not in HANDLERS: raise data_entry_flow.UnknownHandler @@ -881,16 +896,18 @@ async def _async_create_flow( flow = cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry)) return flow - async def _async_finish_flow( - self, flow: "OptionsFlow", result: Dict[str, Any] - ) -> Optional[Dict[str, Any]]: + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: Dict[str, Any] + ) -> Dict[str, Any]: """Finish an options flow and update options for configuration entry. Flow.handler and entry_id is the same thing to map flow with entry. """ + flow = cast(OptionsFlow, flow) + entry = self.hass.config_entries.async_get_entry(flow.handler) if entry is None: - return None + raise UnknownEntry(flow.handler) self.hass.config_entries.async_update_entry(entry, options=result["data"]) result["result"] = True diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 7c2b4ab6ddc603..6a9f5b1dc5a78f 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,6 +1,7 @@ """Classes to help gather user submissions.""" +import abc import logging -from typing import Any, Callable, Dict, List, Optional, cast +from typing import Any, Dict, List, Optional, cast import uuid import voluptuous as vol @@ -46,20 +47,34 @@ def __init__(self, reason: str, description_placeholders: Optional[Dict] = None) self.description_placeholders = description_placeholders -class FlowManager: +class FlowManager(abc.ABC): """Manage all the flows that are in progress.""" - def __init__( - self, - hass: HomeAssistant, - async_create_flow: Callable, - async_finish_flow: Callable, - ) -> None: + def __init__(self, hass: HomeAssistant,) -> None: """Initialize the flow manager.""" self.hass = hass self._progress: Dict[str, Any] = {} - self._async_create_flow = async_create_flow - self._async_finish_flow = async_finish_flow + + @abc.abstractmethod + async def async_create_flow( + self, + handler_key: Any, + *, + context: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + ) -> "FlowHandler": + """Create a flow for specified handler. + + Handler key is the domain of the component that we want to set up. + """ + pass + + @abc.abstractmethod + async def async_finish_flow( + self, flow: "FlowHandler", result: Dict[str, Any] + ) -> Dict[str, Any]: + """Finish a config flow and add an entry.""" + pass @callback def async_progress(self) -> List[Dict]: @@ -75,7 +90,9 @@ async def async_init( """Start a configuration flow.""" if context is None: context = {} - flow = await self._async_create_flow(handler, context=context, data=data) + flow = await self.async_create_flow(handler, context=context, data=data) + if not flow: + raise UnknownFlow("Flow was not created") flow.hass = self.hass flow.handler = handler flow.flow_id = uuid.uuid4().hex @@ -168,7 +185,7 @@ async def _async_handle_step( return result # We pass a copy of the result because we're mutating our version - result = await self._async_finish_flow(flow, dict(result)) + result = await self.async_finish_flow(flow, dict(result)) # _async_finish_flow may change result type, check it again if result["type"] == RESULT_TYPE_FORM: diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index f8fe42d10d8833..3fef85611c84ee 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -436,7 +436,7 @@ async def test_option_flow(hass): entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None) hass.config_entries._entries.append(entry) - flow = await hass.config_entries.options._async_create_flow( + flow = await hass.config_entries.options.async_create_flow( entry.entry_id, context={"source": "test"}, data=None ) diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 26048543a22749..0adcdb188d08b9 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -182,13 +182,13 @@ async def test_options_form(hass): ) entry.add_to_hass(hass) - result = await hass.config_entries.options.flow.async_init( + result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" - result = await hass.config_entries.options.flow.async_configure( + result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FLOOR_TEMP: True, CONF_PRECISION: PRECISION_HALVES}, ) @@ -197,11 +197,11 @@ async def test_options_form(hass): assert result["data"][CONF_PRECISION] == PRECISION_HALVES assert result["data"][CONF_FLOOR_TEMP] is True - result = await hass.config_entries.options.flow.async_init( + result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - result = await hass.config_entries.options.flow.async_configure( + result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_PRECISION: 0} ) diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 0fb1f850809600..8f9342c4f72c2c 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -462,13 +462,13 @@ async def test_option_flow(hass): entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=DEFAULT_OPTIONS) entry.add_to_hass(hass) - result = await hass.config_entries.options.flow.async_init( + result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) assert result["type"] == "form" assert result["step_id"] == "plex_mp_settings" - result = await hass.config_entries.options.flow.async_configure( + result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ config_flow.CONF_USE_EPISODE_ART: True, diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py index b6eeff54a50737..7b7e822ce58fe2 100644 --- a/tests/components/tesla/test_config_flow.py +++ b/tests/components/tesla/test_config_flow.py @@ -131,12 +131,12 @@ async def test_option_flow(hass): entry = MockConfigEntry(domain=DOMAIN, data={}, options=None) entry.add_to_hass(hass) - result = await hass.config_entries.options.flow.async_init(entry.entry_id) + result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == "form" assert result["step_id"] == "init" - result = await hass.config_entries.options.flow.async_configure( + result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: 350} ) assert result["type"] == "create_entry" @@ -148,12 +148,12 @@ async def test_option_flow_input_floor(hass): entry = MockConfigEntry(domain=DOMAIN, data={}, options=None) entry.add_to_hass(hass) - result = await hass.config_entries.options.flow.async_init(entry.entry_id) + result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == "form" assert result["step_id"] == "init" - result = await hass.config_entries.options.flow.async_configure( + result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: 1} ) assert result["type"] == "create_entry" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 1b973aee9a507c..cc8896d55ce1ae 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -231,7 +231,7 @@ async def test_option_flow(hass): entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None) hass.config_entries._entries.append(entry) - flow = await hass.config_entries.options._async_create_flow( + flow = await hass.config_entries.options.async_create_flow( entry.entry_id, context={"source": "test"}, data=None ) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 5b694b2de87e50..c3a87bcf3a0164 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -692,13 +692,13 @@ class OptionsFlowHandler(data_entry_flow.FlowHandler): return OptionsFlowHandler() config_entries.HANDLERS["test"] = TestFlow() - flow = await manager.options._async_create_flow( + flow = await manager.options.async_create_flow( entry.entry_id, context={"source": "test"}, data=None ) flow.handler = entry.entry_id # Used to keep reference to config entry - await manager.options._async_finish_flow(flow, {"data": {"second": True}}) + await manager.options.async_finish_flow(flow, {"data": {"second": True}}) assert entry.data == {"first": True} diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index a6bdd2b5cb6957..664304c9ef69f9 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -14,27 +14,32 @@ def manager(): handlers = Registry() entries = [] - async def async_create_flow(handler_name, *, context, data): - handler = handlers.get(handler_name) + class FlowManager(data_entry_flow.FlowManager): + """Test flow manager.""" - if handler is None: - raise data_entry_flow.UnknownHandler + async def async_create_flow(self, handler_key, *, context, data): + """Test create flow.""" + handler = handlers.get(handler_key) - flow = handler() - flow.init_step = context.get("init_step", "init") - flow.source = context.get("source") - return flow + if handler is None: + raise data_entry_flow.UnknownHandler - async def async_add_entry(flow, result): - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - result["source"] = flow.context.get("source") - entries.append(result) - return result + flow = handler() + flow.init_step = context.get("init_step", "init") + flow.source = context.get("source") + return flow - manager = data_entry_flow.FlowManager(None, async_create_flow, async_add_entry) - manager.mock_created_entries = entries - manager.mock_reg_handler = handlers.register - return manager + async def async_finish_flow(self, flow, result): + """Test finish flow.""" + if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + result["source"] = flow.context.get("source") + entries.append(result) + return result + + mgr = FlowManager(None) + mgr.mock_created_entries = entries + mgr.mock_reg_handler = handlers.register + return mgr async def test_configure_reuses_handler_instance(manager): @@ -194,22 +199,23 @@ async def async_step_init(self, input): step_id="init", data_schema=vol.Schema({"count": int}) ) - async def async_create_flow(handler_name, *, context, data): - """Create a test flow.""" - return TestFlow() - - async def async_finish_flow(flow, result): - """Redirect to init form if count <= 1.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - if result["data"] is None or result["data"].get("count", 0) <= 1: - return flow.async_show_form( - step_id="init", data_schema=vol.Schema({"count": int}) - ) - else: - result["result"] = result["data"]["count"] - return result - - manager = data_entry_flow.FlowManager(hass, async_create_flow, async_finish_flow) + class FlowManager(data_entry_flow.FlowManager): + async def async_create_flow(self, handler_name, *, context, data): + """Create a test flow.""" + return TestFlow() + + async def async_finish_flow(self, flow, result): + """Redirect to init form if count <= 1.""" + if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["data"] is None or result["data"].get("count", 0) <= 1: + return flow.async_show_form( + step_id="init", data_schema=vol.Schema({"count": int}) + ) + else: + result["result"] = result["data"]["count"] + return result + + manager = FlowManager(hass) result = await manager.async_init("test") assert result["type"] == data_entry_flow.RESULT_TYPE_FORM From 3f33fc6122d6a38aabcc80cb137e7ef3ae3284d7 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Fri, 3 Jan 2020 11:54:19 +0100 Subject: [PATCH 2675/3953] convert to integer if rounding precision is zero (#30226) Convert values to integer if rounding precision is zero. With that a value which is an integer before filtering can be configured to stay integer when using precision = 0. This also aligns behavior of filters to how rounding behaves in tempaltes (homeassistant/helpers/template.py, function forgiving_round). --- homeassistant/components/filter/sensor.py | 3 ++- tests/components/filter/test_sensor.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index eeb0d32f51c745..baa4f90af3f8db 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -324,7 +324,8 @@ def __init__(self, state): def set_precision(self, precision): """Set precision of Number based states.""" if isinstance(self.state, Number): - self.state = round(float(self.state), precision) + value = round(float(self.state), precision) + self.state = int(value) if precision == 0 else value def __str__(self): """Return state as the string representation of FilterState.""" diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 9ae4245ed7025c..d46fa4eab68a4b 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -208,6 +208,13 @@ def test_initial_outlier(self): filtered = filt.filter_state(state) assert 21 == filtered.state + def test_precision_zero(self): + """Test if precision of zero returns an integer.""" + filt = LowPassFilter(window_size=10, precision=0, entity=None, time_constant=10) + for state in self.values: + filtered = filt.filter_state(state) + assert isinstance(filtered.state, int) + def test_lowpass(self): """Test if lowpass filter works.""" filt = LowPassFilter(window_size=10, precision=2, entity=None, time_constant=10) From 5580ee3fa184af736e85bd831b22119eac6e54e4 Mon Sep 17 00:00:00 2001 From: Ian Duffy <1243435+imduffy15@users.noreply.github.com> Date: Fri, 3 Jan 2020 11:08:40 +0000 Subject: [PATCH 2676/3953] Don't allow badly formed upnp devices to kill auto discovery (#30342) Prevent the following from occurring: ``` 2019-12-31 22:38:41 ERROR (MainThread) [homeassistant.core] Error doing job: Task exception was never retrieved Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/data_entry_flow.py", line 85, in async_init return await self._async_handle_step(flow, flow.init_step, data) File "/usr/src/homeassistant/homeassistant/data_entry_flow.py", line 145, in _async_handle_step result: Dict = await getattr(flow, method)(user_input) File "/usr/src/homeassistant/homeassistant/components/deconz/config_flow.py", line 182, in async_step_ssdp if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER_URL] != DECONZ_MANUFACTURERURL: KeyError: 'manufacturerURL' 2019-12-31 22:38:41 ERROR (MainThread) [homeassistant.core] Error doing job: Task exception was never retrieved Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/data_entry_flow.py", line 85, in async_init return await self._async_handle_step(flow, flow.init_step, data) File "/usr/src/homeassistant/homeassistant/data_entry_flow.py", line 145, in _async_handle_step result: Dict = await getattr(flow, method)(user_input) File "/usr/src/homeassistant/homeassistant/components/deconz/config_flow.py", line 182, in async_step_ssdp if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER_URL] != DECONZ_MANUFACTURERURL: KeyError: 'manufacturerURL' ``` --- homeassistant/components/deconz/config_flow.py | 5 ++++- homeassistant/components/hue/config_flow.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 0cec6add28c908..95b200bba13cb1 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -175,7 +175,10 @@ def _update_entry(self, entry, host, port, api_key=None): async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" - if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER_URL] != DECONZ_MANUFACTURERURL: + if ( + discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) + != DECONZ_MANUFACTURERURL + ): return self.async_abort(reason="not_deconz_bridge") self.bridge_id = normalize_bridge_id(discovery_info[ssdp.ATTR_UPNP_SERIAL]) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 60000a68fb7f67..cb3d63eec20bb1 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -148,7 +148,7 @@ async def async_step_ssdp(self, discovery_info): host is already configured and delegate to the import step if not. """ # Filter out non-Hue bridges #1 - if discovery_info[ssdp.ATTR_UPNP_MANUFACTURER_URL] != HUE_MANUFACTURERURL: + if discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) != HUE_MANUFACTURERURL: return self.async_abort(reason="not_hue_bridge") # Filter out non-Hue bridges #2 From 0944d022628fea532cc1f6dc0f2ad8d6306e22ec Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 3 Jan 2020 12:12:29 +0100 Subject: [PATCH 2677/3953] Mark API key as deprecated (#30402) Service account should be used instead --- homeassistant/components/google_assistant/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 107003db583a13..2d848101def80e 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -65,6 +65,7 @@ def _check_report_state(data): GOOGLE_ASSISTANT_SCHEMA = vol.All( cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version="0.95"), + cv.deprecated(CONF_API_KEY, invalidation_version="0.105"), vol.Schema( { vol.Required(CONF_PROJECT_ID): cv.string, From b57da2f86277ce2eb9fff7da955addc5cf1fce0d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 3 Jan 2020 13:27:14 +0100 Subject: [PATCH 2678/3953] Axis - Improve tests based on feedback (#30430) * Use MockConfigEntry, add_to_hass and hass.config_entries.async_setup * Mock method instead of handling paths --- tests/components/axis/test_device.py | 49 ++++++++++++---------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 58ebed606819d0..7bcb1350fe8d3b 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -8,6 +8,8 @@ from homeassistant import config_entries from homeassistant.components import axis +from tests.common import MockConfigEntry + MAC = "00408C12345" MODEL = "model" NAME = "name" @@ -55,7 +57,7 @@ root.Properties.Image.NbrOfViews=2 root.Properties.Image.Resolution=1920x1080,1280x960,1280x720,1024x768,1024x576,800x600,640x480,640x360,352x240,320x240 root.Properties.Image.Rotation=0,180 -root.Properties.System.SerialNumber=ACCC12345678 +root.Properties.System.SerialNumber=00408C12345 """ @@ -68,41 +70,34 @@ async def setup_axis_integration( properties=DEFAULT_PROPERTIES, ): """Create the Axis device.""" - config_entry = config_entries.ConfigEntry( - version=1, + config_entry = MockConfigEntry( domain=axis.DOMAIN, - title="Mock Title", data=deepcopy(config), - source="test", connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, options=deepcopy(options), entry_id="1", ) + config_entry.add_to_hass(hass) + + def mock_update_brand(self): + self.process_raw(brand) + + def mock_update_ports(self): + self.process_raw(ports) - def mock_request(self, method, path, json=None): - if method == "get": - if path == "/axis-cgi/param.cgi?action=list&group=root.Brand": - return brand - if path in [ - "/axis-cgi/param.cgi?action=list&group=root.Input", - "/axis-cgi/param.cgi?action=list&group=root.IOPort", - "/axis-cgi/param.cgi?action=list&group=root.Output", - ]: - return ports - if path == "/axis-cgi/param.cgi?action=list&group=root.Properties": - return properties - - return None - - with patch("axis.vapix.Vapix.request", new=mock_request), patch( + def mock_update_properties(self): + self.process_raw(properties) + + with patch("axis.param_cgi.Brand.update_brand", new=mock_update_brand), patch( + "axis.param_cgi.Ports.update_ports", new=mock_update_ports + ), patch( + "axis.param_cgi.Properties.update_properties", new=mock_update_properties + ), patch( "axis.AxisDevice.start", return_value=True ): - await axis.async_setup_entry(hass, config_entry) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - return hass.data[axis.DOMAIN].get(config[axis.CONF_MAC]) @@ -163,9 +158,7 @@ async def test_device_reset(hass): async def test_device_not_accessible(hass): """Failed setup schedules a retry of setup.""" - with patch.object( - axis.device, "get_device", side_effect=axis.errors.CannotConnect - ), pytest.raises(axis.device.ConfigEntryNotReady): + with patch.object(axis.device, "get_device", side_effect=axis.errors.CannotConnect): await setup_axis_integration(hass) assert hass.data[axis.DOMAIN] == {} From 859935e8bc1d421d6540f5dc83a032f943400e87 Mon Sep 17 00:00:00 2001 From: David K <142583+neffs@users.noreply.github.com> Date: Fri, 3 Jan 2020 14:19:03 +0100 Subject: [PATCH 2679/3953] Update HomeKit position state characteristic for covers (#27867) * HomeKit: update position state characteristic for covers position state is a mandatory characteristic for HK window coverings * Test position state characteristic --- .../components/homekit/type_covers.py | 17 ++++++++++- tests/components/homekit/test_type_covers.py | 28 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 3a33207e70e67b..8ad095faa13e33 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -18,6 +18,8 @@ SERVICE_STOP_COVER, STATE_CLOSED, STATE_OPEN, + STATE_OPENING, + STATE_CLOSING, ) from . import TYPES @@ -101,6 +103,9 @@ def __init__(self, *args): self.char_target_position = serv_cover.configure_char( CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover ) + self.char_position_state = serv_cover.configure_char( + CHAR_POSITION_STATE, value=2 + ) @debounce def move_cover(self, value): @@ -122,6 +127,12 @@ def update_state(self, new_state): ): self.char_target_position.set_value(current_position) self._homekit_target = None + if new_state.state == STATE_OPENING: + self.char_position_state.set_value(1) + elif new_state.state == STATE_CLOSING: + self.char_position_state.set_value(0) + else: + self.char_position_state.set_value(2) @TYPES.register("WindowCoveringBasic") @@ -175,7 +186,6 @@ def move_cover(self, value): # Snap the current/target position to the expected final position. self.char_current_position.set_value(position) self.char_target_position.set_value(position) - self.char_position_state.set_value(2) def update_state(self, new_state): """Update cover position after state changed.""" @@ -184,4 +194,9 @@ def update_state(self, new_state): if hk_position is not None: self.char_current_position.set_value(hk_position) self.char_target_position.set_value(hk_position) + if new_state.state == STATE_OPENING: + self.char_position_state.set_value(1) + elif new_state.state == STATE_CLOSING: + self.char_position_state.set_value(0) + else: self.char_position_state.set_value(2) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index d3cf24971ffb14..7ff65619bb6798 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -15,6 +15,8 @@ ATTR_SUPPORTED_FEATURES, STATE_CLOSED, STATE_OPEN, + STATE_OPENING, + STATE_CLOSING, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -138,11 +140,25 @@ async def test_window_set_cover_position(hass, hk_driver, cls, events): await hass.async_block_till_done() assert acc.char_current_position.value == 0 assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 + + hass.states.async_set(entity_id, STATE_OPENING, {ATTR_CURRENT_POSITION: 60}) + await hass.async_block_till_done() + assert acc.char_current_position.value == 60 + assert acc.char_target_position.value == 60 + assert acc.char_position_state.value == 1 + + hass.states.async_set(entity_id, STATE_CLOSING, {ATTR_CURRENT_POSITION: 50}) + await hass.async_block_till_done() + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 50 + assert acc.char_position_state.value == 0 hass.states.async_set(entity_id, STATE_OPEN, {ATTR_CURRENT_POSITION: 50}) await hass.async_block_till_done() assert acc.char_current_position.value == 50 assert acc.char_target_position.value == 50 + assert acc.char_position_state.value == 2 # Set from HomeKit call_set_cover_position = async_mock_service(hass, DOMAIN, "set_cover_position") @@ -189,12 +205,24 @@ async def test_window_open_close(hass, hk_driver, cls, events): assert acc.char_target_position.value == 0 assert acc.char_position_state.value == 2 + hass.states.async_set(entity_id, STATE_OPENING) + await hass.async_block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 1 + hass.states.async_set(entity_id, STATE_OPEN) await hass.async_block_till_done() assert acc.char_current_position.value == 100 assert acc.char_target_position.value == 100 assert acc.char_position_state.value == 2 + hass.states.async_set(entity_id, STATE_CLOSING) + await hass.async_block_till_done() + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 0 + hass.states.async_set(entity_id, STATE_CLOSED) await hass.async_block_till_done() assert acc.char_current_position.value == 0 From 5ad209c6fd8adb1cf4df640e361dcb7481753b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio?= Date: Fri, 3 Jan 2020 15:22:14 +0200 Subject: [PATCH 2680/3953] Handle telegram event commands with args (#30254) * Handle telegram event commands with args * Parse message regargless of command * Lint * Use multiple assignment --- homeassistant/components/telegram_bot/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index fc37121f3f9f07..9b56201f8c74b0 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -782,7 +782,13 @@ def process_message(self, data): if event_data is None: return message_ok - event_data[ATTR_DATA] = data[ATTR_DATA] + query_data = event_data[ATTR_DATA] = data[ATTR_DATA] + + if query_data[0] == "/": + pieces = query_data.split(" ") + event_data[ATTR_COMMAND] = pieces[0] + event_data[ATTR_ARGS] = pieces[1:] + event_data[ATTR_MSG] = data[ATTR_MSG] event_data[ATTR_CHAT_INSTANCE] = data[ATTR_CHAT_INSTANCE] event_data[ATTR_MSGID] = data[ATTR_MSGID] From fa4fa304617153cb7ec320662c07a7555575ff63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 3 Jan 2020 15:47:06 +0200 Subject: [PATCH 2681/3953] Various string cleanups (#30435) * Remove some unnecessary string concatenations * Replace some simple str.formats with f-strings * Replace some string concatenations with f-strings --- homeassistant/__main__.py | 14 ++--- homeassistant/components/apns/notify.py | 2 +- .../components/braviatv/media_player.py | 2 +- homeassistant/components/cloud/__init__.py | 2 +- homeassistant/components/darksky/sensor.py | 2 +- homeassistant/components/ebusd/__init__.py | 2 +- .../components/ecoal_boiler/__init__.py | 2 +- .../components/ecoal_boiler/switch.py | 2 +- homeassistant/components/egardia/__init__.py | 2 +- homeassistant/components/fibaro/__init__.py | 2 +- .../components/hangouts/hangouts_bot.py | 8 +-- homeassistant/components/hook/switch.py | 6 +- .../huawei_router/device_tracker.py | 2 +- homeassistant/components/icloud/__init__.py | 4 +- homeassistant/components/insteon/__init__.py | 2 +- .../components/itunes/media_player.py | 12 ++-- homeassistant/components/kodi/media_player.py | 2 +- .../components/konnected/__init__.py | 2 +- .../components/life360/device_tracker.py | 2 +- homeassistant/components/lifx/light.py | 2 +- .../components/netatmo/binary_sensor.py | 4 +- homeassistant/components/netatmo/camera.py | 4 +- .../components/netgear/device_tracker.py | 4 +- .../components/octoprint/__init__.py | 12 ++-- homeassistant/components/onewire/sensor.py | 2 +- homeassistant/components/plant/__init__.py | 18 +++--- homeassistant/components/plex/media_player.py | 2 +- .../components/raspihats/__init__.py | 2 +- homeassistant/components/raspihats/switch.py | 6 +- .../components/samsungtv/media_player.py | 2 +- homeassistant/components/sensibo/climate.py | 2 +- .../components/seventeentrack/sensor.py | 2 +- .../components/sky_hub/device_tracker.py | 7 +-- homeassistant/components/smappee/__init__.py | 2 +- .../components/smartthings/__init__.py | 2 +- .../components/smartthings/smartapp.py | 2 +- .../components/soundtouch/media_player.py | 2 +- homeassistant/components/startca/sensor.py | 2 +- .../components/tank_utility/sensor.py | 2 +- homeassistant/components/temper/sensor.py | 2 +- .../components/wunderground/sensor.py | 8 +-- homeassistant/components/x10/light.py | 10 ++-- homeassistant/components/yeelight/light.py | 2 +- homeassistant/components/zha/api.py | 2 +- .../components/zha/core/channels/__init__.py | 4 +- homeassistant/components/zha/core/device.py | 2 +- homeassistant/components/zha/entity.py | 2 +- .../components/zhong_hong/climate.py | 4 +- homeassistant/components/zwave/cover.py | 2 +- homeassistant/config.py | 4 +- homeassistant/const.py | 4 +- homeassistant/core.py | 16 ++---- homeassistant/data_entry_flow.py | 4 +- homeassistant/helpers/__init__.py | 2 +- homeassistant/helpers/check_config.py | 4 +- homeassistant/helpers/config_validation.py | 42 +++++++------- homeassistant/helpers/discovery.py | 4 +- homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/entity_platform.py | 12 +--- homeassistant/helpers/entity_registry.py | 5 +- homeassistant/helpers/icon.py | 2 +- homeassistant/helpers/intent.py | 2 +- homeassistant/helpers/script.py | 6 +- homeassistant/helpers/service.py | 2 +- homeassistant/helpers/template.py | 4 +- homeassistant/helpers/translation.py | 2 +- homeassistant/scripts/__init__.py | 2 +- homeassistant/scripts/check_config.py | 2 +- homeassistant/scripts/macos/__init__.py | 6 +- homeassistant/setup.py | 4 +- homeassistant/util/dt.py | 4 +- homeassistant/util/ruamel_yaml.py | 2 +- homeassistant/util/unit_system.py | 8 +-- script/gen_requirements_all.py | 6 +- script/hassfest/dependencies.py | 5 +- script/hassfest/model.py | 4 +- script/inspect_schemas.py | 5 +- script/lazytox.py | 5 +- tests/common.py | 55 ++++++------------- tests/components/hddtemp/test_sensor.py | 2 +- tests/components/light/test_init.py | 2 +- tests/components/tradfri/test_light.py | 10 ++-- tests/components/uk_transport/test_sensor.py | 4 +- .../unifi_direct/test_device_tracker.py | 4 +- .../components/vacuum/test_device_trigger.py | 8 +-- tests/components/webhook/test_init.py | 14 ++--- tests/components/withings/common.py | 9 ++- tests/components/xiaomi_miio/test_vacuum.py | 4 +- tests/components/yessssms/test_notify.py | 4 +- tests/components/yweather/test_sensor.py | 2 +- tests/components/zeroconf/test_init.py | 4 +- tests/components/zha/test_sensor.py | 6 +- tests/components/zwave/test_init.py | 18 +++--- tests/conftest.py | 7 +-- tests/helpers/test_config_entry_flow.py | 4 +- tests/helpers/test_entity_component.py | 4 +- tests/helpers/test_entity_platform.py | 4 +- tests/helpers/test_storage.py | 2 +- tests/helpers/test_temperature.py | 2 +- tests/helpers/test_template.py | 6 +- tests/test_bootstrap.py | 2 +- tests/test_config.py | 4 +- tests/test_core.py | 9 ++- tests/test_loader.py | 2 +- tests/test_setup.py | 2 +- 105 files changed, 241 insertions(+), 314 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index a0243e2dd8c8c2..5398c3d5c55973 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -55,10 +55,8 @@ def ensure_config_path(config_dir: str) -> None: if not os.path.isdir(config_dir): if config_dir != config_util.get_default_config_dir(): print( - ( - "Fatal Error: Specified configuration directory does " - "not exist {} " - ).format(config_dir) + f"Fatal Error: Specified configuration directory {config_dir} " + "does not exist" ) sys.exit(1) @@ -66,10 +64,8 @@ def ensure_config_path(config_dir: str) -> None: os.mkdir(config_dir) except OSError: print( - ( - "Fatal Error: Unable to create default configuration " - "directory {} " - ).format(config_dir) + "Fatal Error: Unable to create default configuration " + f"directory {config_dir}" ) sys.exit(1) @@ -78,7 +74,7 @@ def ensure_config_path(config_dir: str) -> None: try: os.mkdir(lib_dir) except OSError: - print("Fatal Error: Unable to create library directory {}".format(lib_dir)) + print(f"Fatal Error: Unable to create library directory {lib_dir}") sys.exit(1) diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index 990598508afddb..febe344a9c46bf 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -150,7 +150,7 @@ def __init__(self, hass, app_name, topic, sandbox, cert_file): self.app_name = app_name self.sandbox = sandbox self.certificate = cert_file - self.yaml_path = hass.config.path(app_name + "_" + APNS_DEVICES) + self.yaml_path = hass.config.path(f"{app_name}_{APNS_DEVICES}") self.devices = {} self.device_states = {} self.topic = topic diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index d0458541f7bee8..ef0640c8e87257 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -291,7 +291,7 @@ def media_title(self): if self._channel_name is not None: return_value = self._channel_name if self._program_name is not None: - return_value = return_value + ": " + self._program_name + return_value = f"{return_value}: {self._program_name}" return return_value @property diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 6d9b70051f5dab..4799a82979f3ae 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -158,7 +158,7 @@ def async_remote_ui_url(hass) -> str: if not hass.data[DOMAIN].remote.instance_domain: raise CloudNotAvailable - return "https://" + hass.data[DOMAIN].remote.instance_domain + return f"https://{hass.data[DOMAIN].remote.instance_domain}" def is_cloudhook_request(request): diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 5b6da5d11bb53d..9f99b37a2013c5 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -750,7 +750,7 @@ def get_state(self, data): for i, alert in enumerate(data): for attr in ALERTS_ATTRS: if multiple_alerts: - dkey = attr + "_" + str(i) + dkey = f"{attr}_{i!s}" else: dkey = attr alerts[dkey] = getattr(alert, attr) diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index e4d0bdbcdb1ac1..eafa42ba22aa73 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -34,7 +34,7 @@ def verify_ebusd_config(config): circuit = config[CONF_CIRCUIT] for condition in config[CONF_MONITORED_CONDITIONS]: if condition not in SENSOR_TYPES[circuit]: - raise vol.Invalid("Condition '" + condition + "' not in '" + circuit + "'.") + raise vol.Invalid(f"Condition '{condition}' not in '{circuit}'.") return config diff --git a/homeassistant/components/ecoal_boiler/__init__.py b/homeassistant/components/ecoal_boiler/__init__.py index 608e4a59a3fecc..b0ca7aec5ccde7 100644 --- a/homeassistant/components/ecoal_boiler/__init__.py +++ b/homeassistant/components/ecoal_boiler/__init__.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "ecoal_boiler" -DATA_ECOAL_BOILER = "data_" + DOMAIN +DATA_ECOAL_BOILER = f"data_{DOMAIN}" DEFAULT_USERNAME = "admin" DEFAULT_PASSWORD = "admin" diff --git a/homeassistant/components/ecoal_boiler/switch.py b/homeassistant/components/ecoal_boiler/switch.py index 9f286e625a5206..00bfd7f3e5b976 100644 --- a/homeassistant/components/ecoal_boiler/switch.py +++ b/homeassistant/components/ecoal_boiler/switch.py @@ -38,7 +38,7 @@ def __init__(self, ecoal_contr, name, state_attr): # set_() # as attribute name in status instance: # status. - self._contr_set_fun = getattr(self._ecoal_contr, "set_" + state_attr) + self._contr_set_fun = getattr(self._ecoal_contr, f"set_{state_attr}") # No value set, will be read from controller instead self._state = None diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py index efe477364791d9..770db1d236b76c 100644 --- a/homeassistant/components/egardia/__init__.py +++ b/homeassistant/components/egardia/__init__.py @@ -110,7 +110,7 @@ def setup(hass, config): bound = server.bind() if not bound: raise OSError( - "Binding error occurred while " + "starting EgardiaServer." + "Binding error occurred while starting EgardiaServer." ) hass.data[EGARDIA_SERVER] = server server.start() diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index aeb7c0879e0c1c..32d8f328ef84f1 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -268,7 +268,7 @@ def _read_devices(self): else: room_name = self._room_map[device.roomID].name device.room_name = room_name - device.friendly_name = room_name + " " + device.name + device.friendly_name = f"{room_name} {device.name}" device.ha_id = "{}_{}_{}".format( slugify(room_name), slugify(device.name), device.id ) diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 8575a547a9c8b5..fd14ec0b0949fe 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -86,15 +86,15 @@ def async_update_conversation_commands(self): conv_id = self._resolve_conversation_id(conversation) if conv_id is not None: conversations.append(conv_id) - data["_" + CONF_CONVERSATIONS] = conversations + data[f"_{CONF_CONVERSATIONS}"] = conversations elif self._default_conv_ids: - data["_" + CONF_CONVERSATIONS] = self._default_conv_ids + data[f"_{CONF_CONVERSATIONS}"] = self._default_conv_ids else: - data["_" + CONF_CONVERSATIONS] = [ + data[f"_{CONF_CONVERSATIONS}"] = [ conv.id_ for conv in self._conversation_list.get_all() ] - for conv_id in data["_" + CONF_CONVERSATIONS]: + for conv_id in data[f"_{CONF_CONVERSATIONS}"]: if conv_id not in self._conversation_intents: self._conversation_intents[conv_id] = {} diff --git a/homeassistant/components/hook/switch.py b/homeassistant/components/hook/switch.py index 14c4d4ba662991..582dc61af14fba 100644 --- a/homeassistant/components/hook/switch.py +++ b/homeassistant/components/hook/switch.py @@ -21,12 +21,10 @@ vol.Exclusive( CONF_PASSWORD, "hook_secret", - msg="hook: provide " + "username/password OR token", + msg="hook: provide username/password OR token", ): cv.string, vol.Exclusive( - CONF_TOKEN, - "hook_secret", - msg="hook: provide " + "username/password OR token", + CONF_TOKEN, "hook_secret", msg="hook: provide username/password OR token", ): cv.string, vol.Inclusive(CONF_USERNAME, "hook_auth"): cv.string, vol.Inclusive(CONF_PASSWORD, "hook_auth"): cv.string, diff --git a/homeassistant/components/huawei_router/device_tracker.py b/homeassistant/components/huawei_router/device_tracker.py index 4b52060e425ea8..be34b26be0d4b3 100644 --- a/homeassistant/components/huawei_router/device_tracker.py +++ b/homeassistant/components/huawei_router/device_tracker.py @@ -88,7 +88,7 @@ def _update_info(self): _LOGGER.debug( "Active clients: %s", - "\n".join((client.mac + " " + client.name) for client in active_clients), + "\n".join(f"{client.mac} {client.name}" for client in active_clients), ) return True diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index c59f4098951ad3..e983f5fac2215f 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -216,7 +216,7 @@ def _get_account(account_identifier: str) -> any: if icloud_account is None: raise Exception( - "No iCloud account with username or name " + account_identifier + f"No iCloud account with username or name {account_identifier}" ) return icloud_account @@ -430,7 +430,7 @@ def get_devices_with_name(self, name: str) -> [any]: if slugify(device.name.replace(" ", "", 99)) == name_slug: result.append(device) if not result: - raise Exception("No device with name " + name) + raise Exception(f"No device with name {name}") return result @property diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 11f224dbfccaec..df6fa626a4fa79 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -614,7 +614,7 @@ def name(self): # Get an extension label if there is one extension = self._get_label() if extension: - extension = " " + extension + extension = f" {extension}" name = "{:s} {:s}{:s}".format( description, self._insteon_device.address.human, extension ) diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index 112a9c609d836a..327cbf5e9ac11d 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -110,7 +110,7 @@ def _request(self, method, path, params=None): def _command(self, named_command): """Make a request for a controlling command.""" - return self._request("PUT", "/" + named_command) + return self._request("PUT", f"/{named_command}") def now_playing(self): """Return the current state.""" @@ -168,7 +168,7 @@ def play_playlist(self, playlist_id_or_name): def artwork_url(self): """Return a URL of the current track's album art.""" - return self._base_url + "/artwork" + return f"{self._base_url}/artwork" def airplay_devices(self): """Return a list of AirPlay devices.""" @@ -176,17 +176,17 @@ def airplay_devices(self): def airplay_device(self, device_id): """Return an AirPlay device.""" - return self._request("GET", "/airplay_devices/" + device_id) + return self._request("GET", f"/airplay_devices/{device_id}") def toggle_airplay_device(self, device_id, toggle): """Toggle airplay device on or off, id, toggle True or False.""" command = "on" if toggle else "off" - path = "/airplay_devices/" + device_id + "/" + command + path = f"/airplay_devices/{device_id}/{command}" return self._request("PUT", path) def set_volume_airplay_device(self, device_id, level): """Set volume, returns current state of device, id,level 0-100.""" - path = "/airplay_devices/" + device_id + "/volume" + path = f"/airplay_devices/{device_id}/volume" return self._request("PUT", path, {"level": level}) @@ -431,7 +431,7 @@ def update_state(self, state_hash): if "name" in state_hash: name = state_hash.get("name", "") - self.device_name = (name + " AirTunes Speaker").strip() + self.device_name = f"{name} AirTunes Speaker".strip() if "kind" in state_hash: self.kind = state_hash.get("kind", None) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 71418927ed21c4..13aa18d01adce6 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -963,7 +963,7 @@ async def async_find_album(self, album_name, artist_name=""): @staticmethod def _find(key_word, words): key_word = key_word.split(" ") - patt = [re.compile("(^| )" + k + "( |$)", re.IGNORECASE) for k in key_word] + patt = [re.compile(f"(^| ){k}( |$)", re.IGNORECASE) for k in key_word] out = [[i, 0] for i in range(len(words))] for i in range(len(words)): diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 624d359e154637..28e62c322ad824 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -488,7 +488,7 @@ async def get(self, request: Request, device_id) -> Response: device = data[CONF_DEVICES][device_id] if not device: return self.json_message( - "Device " + device_id + " not configured", status_code=HTTP_NOT_FOUND + f"Device {device_id} not configured", status_code=HTTP_NOT_FOUND ) try: diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index ddd562ebfac8e9..b2ba1ca31640c0 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -162,7 +162,7 @@ def _err(self, key, err_msg): msg = f"{key}: {err_msg}" if _errs >= self._error_threshold: if _errs == self._max_errs: - msg = "Suppressing further errors until OK: " + msg + msg = f"Suppressing further errors until OK: {msg}" _LOGGER.error(msg) elif _errs >= self._warning_threshold: _LOGGER.warning(msg) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index aa63be04f0d3f0..4e845a07854208 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -562,7 +562,7 @@ def effect(self): """Return the name of the currently running effect.""" effect = self.effects_conductor.effect(self.bulb) if effect: - return "lifx_effect_" + effect.name + return f"lifx_effect_{effect.name}" return None async def update_hass(self, now=None): diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 06097ed852d9fa..a449b7bb43daef 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -155,9 +155,9 @@ def __init__( else: self._name = camera_name if module_name: - self._name += " / " + module_name + self._name += f" / {module_name}" self._sensor_name = sensor - self._name += " " + sensor + self._name += f" {sensor}" self._cameratype = camera_type self._state = None diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 1713265a014c9a..546a5da3c152e8 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -106,7 +106,7 @@ def __init__(self, data, camera_name, home, camera_type, verify_ssl, quality): self._camera_name = camera_name self._home = home if home: - self._name = home + " / " + camera_name + self._name = f"{home} / {camera_name}" else: self._name = camera_name self._cameratype = camera_type @@ -383,7 +383,7 @@ def _set_light_mode(self, mode): """Set light mode ('auto', 'on', 'off').""" if self.model == "Presence": try: - config = '{"mode":"' + mode + '"}' + config = f'{{"mode":"{mode}"}}' if self._localurl: requests.get( f"{self._localurl}/command/floodlight_set_config?config={config}", diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index d556e83ca13707..3e87bcac53c3ec 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -116,7 +116,7 @@ def scan_devices(self): self.tracked_accesspoints and dev.conn_ap_mac in self.tracked_accesspoints ): - devices.append(dev.mac + "_" + dev.conn_ap_mac) + devices.append(f"{dev.mac}_{dev.conn_ap_mac}") return devices @@ -144,7 +144,7 @@ def get_device_name(self, device): ap_name = dev.name break - return name + " on " + ap_name + return f"{name} on {ap_name}" return name diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index 7564330e499ade..f73e525efe3821 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -45,7 +45,7 @@ def ensure_valid_path(value): """Validate the path, ensuring it starts and ends with a /.""" vol.Schema(cv.string)(value) if value[0] != "/": - value = "/" + value + value = f"/{value}" if value[-1] != "/": value += "/" return value @@ -189,7 +189,7 @@ def get_tools(self): tools = [] if self.number_of_tools > 0: for tool_number in range(0, self.number_of_tools): - tools.append("tool" + str(tool_number)) + tools.append(f"tool{tool_number!s}") if self.bed: tools.append("bed") if not self.bed and self.number_of_tools == 0: @@ -231,18 +231,16 @@ def get(self, endpoint): self.printer_error_logged = False return response.json() except Exception as conn_exc: # pylint: disable=broad-except - log_string = "Failed to update OctoPrint status. " + " Error: %s" % ( - conn_exc - ) + log_string = "Failed to update OctoPrint status. Error: %s" % conn_exc # Only log the first failure if endpoint == "job": - log_string = "Endpoint: job " + log_string + log_string = f"Endpoint: job {log_string}" if not self.job_error_logged: _LOGGER.error(log_string) self.job_error_logged = True self.job_available = False elif endpoint == "printer": - log_string = "Endpoint: printer " + log_string + log_string = f"Endpoint: printer {log_string}" if not self.printer_error_logged: _LOGGER.error(log_string) self.printer_error_logged = True diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 6405cb05adc967..936bf9f751ba7e 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -148,7 +148,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # We have a raw GPIO ow sensor on a Pi elif base_dir == DEFAULT_MOUNT_DIR: for device_family in DEVICE_SENSORS: - for device_folder in glob(os.path.join(base_dir, device_family + "[.-]*")): + for device_folder in glob(os.path.join(base_dir, f"{device_family}[.-]*")): sensor_id = os.path.split(device_folder)[1] device_file = os.path.join(device_folder, "w1_slave") devs.append( diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index cc405dcad1f643..1f440eb36dc41c 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -44,15 +44,15 @@ # to have a separate literal for it to avoid confusion. ATTR_DICT_OF_UNITS_OF_MEASUREMENT = "unit_of_measurement_dict" -CONF_MIN_BATTERY_LEVEL = "min_" + READING_BATTERY -CONF_MIN_TEMPERATURE = "min_" + READING_TEMPERATURE -CONF_MAX_TEMPERATURE = "max_" + READING_TEMPERATURE -CONF_MIN_MOISTURE = "min_" + READING_MOISTURE -CONF_MAX_MOISTURE = "max_" + READING_MOISTURE -CONF_MIN_CONDUCTIVITY = "min_" + READING_CONDUCTIVITY -CONF_MAX_CONDUCTIVITY = "max_" + READING_CONDUCTIVITY -CONF_MIN_BRIGHTNESS = "min_" + READING_BRIGHTNESS -CONF_MAX_BRIGHTNESS = "max_" + READING_BRIGHTNESS +CONF_MIN_BATTERY_LEVEL = f"min_{READING_BATTERY}" +CONF_MIN_TEMPERATURE = f"min_{READING_TEMPERATURE}" +CONF_MAX_TEMPERATURE = f"max_{READING_TEMPERATURE}" +CONF_MIN_MOISTURE = f"min_{READING_MOISTURE}" +CONF_MAX_MOISTURE = f"max_{READING_MOISTURE}" +CONF_MIN_CONDUCTIVITY = f"min_{READING_CONDUCTIVITY}" +CONF_MAX_CONDUCTIVITY = f"max_{READING_CONDUCTIVITY}" +CONF_MIN_BRIGHTNESS = f"min_{READING_BRIGHTNESS}" +CONF_MAX_BRIGHTNESS = f"max_{READING_BRIGHTNESS}" CONF_CHECK_DAYS = "check_days" CONF_SENSOR_BATTERY_LEVEL = READING_BATTERY diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index ad5fb2f73f10d1..46b797976abc95 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -300,7 +300,7 @@ def _set_media_type(self): elif self._session_type == "movie": self._media_content_type = MEDIA_TYPE_MOVIE if self.session.year is not None and self._media_title is not None: - self._media_title += " (" + str(self.session.year) + ")" + self._media_title += f" ({self.session.year!s})" elif self._session_type == "track": self._media_content_type = MEDIA_TYPE_MUSIC diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py index 8b7ea0a38d75a3..fb544d3ebcc5e8 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -50,7 +50,7 @@ def log_message(source, *parts): """Build log message.""" message = source.__class__.__name__ for part in parts: - message += ": " + str(part) + message += f": {part!s}" return message diff --git a/homeassistant/components/raspihats/switch.py b/homeassistant/components/raspihats/switch.py index b99a84bdae97c7..8a083dbe2c9112 100644 --- a/homeassistant/components/raspihats/switch.py +++ b/homeassistant/components/raspihats/switch.py @@ -127,7 +127,7 @@ def is_on(self): state = self.I2C_HATS_MANAGER.read_dq(self._address, self._channel) return state != self._invert_logic except I2CHatsException as ex: - _LOGGER.error(self._log_message("Is ON check failed, " + str(ex))) + _LOGGER.error(self._log_message(f"Is ON check failed, {ex!s}")) return False def turn_on(self, **kwargs): @@ -137,7 +137,7 @@ def turn_on(self, **kwargs): self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) self.schedule_update_ha_state() except I2CHatsException as ex: - _LOGGER.error(self._log_message("Turn ON failed, " + str(ex))) + _LOGGER.error(self._log_message(f"Turn ON failed, {ex!s}")) def turn_off(self, **kwargs): """Turn the device off.""" @@ -146,4 +146,4 @@ def turn_off(self, **kwargs): self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) self.schedule_update_ha_state() except I2CHatsException as ex: - _LOGGER.error(self._log_message("Turn OFF failed:, " + str(ex))) + _LOGGER.error(self._log_message(f"Turn OFF failed, {ex!s}")) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 56b947ba9adbf9..fd900fedec178b 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -344,7 +344,7 @@ async def async_play_media(self, media_type, media_id, **kwargs): return for digit in media_id: - await self.hass.async_add_job(self.send_key, "KEY_" + digit) + await self.hass.async_add_job(self.send_key, f"KEY_{digit}") await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop) await self.hass.async_add_job(self.send_key, "KEY_ENTER") diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 2431b223f09ef1..08e2212e2a2168 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -65,7 +65,7 @@ "temperatureUnit", ] ) -_INITIAL_FETCH_FIELDS = "id," + _FETCH_FIELDS +_INITIAL_FETCH_FIELDS = f"id,{_FETCH_FIELDS}" FIELD_TO_FLAG = { "fanLevel": SUPPORT_FAN_MODE, diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 167f4347c0cc61..43bc1b41c8f832 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -45,7 +45,7 @@ NOTIFICATION_DELIVERED_ID = "package_delivered_{0}" NOTIFICATION_DELIVERED_TITLE = "Package {0} delivered" NOTIFICATION_DELIVERED_MESSAGE = ( - "Package Delivered: {0}
" + "Visit 17.track for more information: " + "Package Delivered: {0}
Visit 17.track for more information: " "https://t.17track.net/track#nums={1}" ) diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index f7760a59eed596..c7dc1092b739ca 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -95,8 +95,7 @@ def _parse_skyhub_response(data_str): pattmatch = re.search("attach_dev = '(.*)'", data_str) if pattmatch is None: raise OSError( - "Error: Impossible to fetch data from" - + " Sky Hub. Try to reboot the router." + "Error: Impossible to fetch data from Sky Hub. Try to reboot the router." ) patt = pattmatch.group(1) @@ -107,8 +106,6 @@ def _parse_skyhub_response(data_str): if _MAC_REGEX.match(dvc[1]): devices[dvc[1]] = dvc[0] else: - raise RuntimeError( - "Error: MAC address " + dvc[1] + " not in correct format." - ) + raise RuntimeError(f"Error: MAC address {dvc[1]} not in correct format.") return devices diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index ecab09f6ff912e..d34653e60e7b88 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -24,7 +24,7 @@ DOMAIN = "smappee" DATA_SMAPPEE = "SMAPPEE" -_SENSOR_REGEX = re.compile(r"(?P([A-Za-z]+))\=" + r"(?P([0-9\.]+))") +_SENSOR_REGEX = re.compile(r"(?P([A-Za-z]+))\=(?P([0-9\.]+))") CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 9787fb53917c14..33f9558023db6d 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -279,7 +279,7 @@ def _assign_capabilities(self, devices: Iterable): capabilities = device.capabilities.copy() slots = {} for platform_name in SUPPORTED_PLATFORMS: - platform = importlib.import_module("." + platform_name, self.__module__) + platform = importlib.import_module(f".{platform_name}", self.__module__) if not hasattr(platform, "get_capabilities"): continue assigned = platform.get_capabilities(capabilities) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index d04872909262b9..d17f6061101ad2 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -109,7 +109,7 @@ def get_webhook_url(hass: HomeAssistantType) -> str: def _get_app_template(hass: HomeAssistantType): - endpoint = "at " + hass.config.api.base_url + endpoint = f"at {hass.config.api.base_url}" cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] if cloudhook_url is not None: endpoint = "via Nabu Casa" diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 4a0f6b55b227bc..72677995a9d6b3 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -304,7 +304,7 @@ def media_title(self): if self._status.station_name is not None: return self._status.station_name if self._status.artist is not None: - return self._status.artist + " - " + self._status.track + return f"{self._status.artist} - {self._status.track}" return None diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 55ae15cede7ea9..e07f21e5d60486 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -140,7 +140,7 @@ def bytes_to_gb(value): async def async_update(self): """Get the Start.ca bandwidth data from the web service.""" _LOGGER.debug("Updating Start.ca usage data") - url = "https://www.start.ca/support/usage/api?key=" + self.api_key + url = f"https://www.start.ca/support/usage/api?key={self.api_key}" with async_timeout.timeout(REQUEST_TIMEOUT): req = await self.websession.get(url) if req.status != 200: diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 23446257eab143..61a3d7367bfef0 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -73,7 +73,7 @@ def __init__(self, email, password, token, device): self._token = token self._device = device self._state = None - self._name = "Tank Utility " + self.device + self._name = f"Tank Utility {self.device}" self._unit_of_measurement = SENSOR_UNIT_OF_MEASUREMENT self._attributes = {} diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index 8b782ae4d797cc..fd26b1702dce98 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for idx, dev in enumerate(temper_devices): if idx != 0: - name = name + "_" + str(idx) + name = f"{name}_{idx!s}" TEMPER_SENSORS.append(TemperSensor(dev, temp_unit, name, scaling)) add_entities(TEMPER_SENSORS) diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 5d3bf1f74b85d7..0a566259a8802a 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -998,7 +998,7 @@ def __init__(self, hass: HomeAssistantType, rest, condition, unique_id_base: str self.rest.request_feature(SENSOR_TYPES[condition].feature) # This is only the suggested entity id, it might get changed by # the entity registry later. - self.entity_id = sensor.ENTITY_ID_FORMAT.format("pws_" + condition) + self.entity_id = sensor.ENTITY_ID_FORMAT.format(f"pws_{condition}") self._unique_id = f"{unique_id_base},{condition}" self._device_class = self._cfg_expand("device_class") @@ -1122,11 +1122,11 @@ def _build_url(self, baseurl=_RESOURCE): self._api_key, "/".join(sorted(self._features)), self._lang ) if self._pws_id: - url = url + f"pws:{self._pws_id}" + url = f"{url}pws:{self._pws_id}" else: - url = url + f"{self._latitude},{self._longitude}" + url = f"{url}{self._latitude},{self._longitude}" - return url + ".json" + return f"{url}.json" @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): diff --git a/homeassistant/components/x10/light.py b/homeassistant/components/x10/light.py index 1f74326d544c09..7be2f12d949b80 100644 --- a/homeassistant/components/x10/light.py +++ b/homeassistant/components/x10/light.py @@ -34,7 +34,7 @@ def x10_command(command): def get_unit_status(code): """Get on/off status for given unit.""" - output = check_output("heyu onstate " + code, shell=True) + output = check_output(f"heyu onstate {code}", shell=True) return int(output.decode("utf-8")[0]) @@ -84,18 +84,18 @@ def supported_features(self): def turn_on(self, **kwargs): """Instruct the light to turn on.""" if self._is_cm11a: - x10_command("on " + self._id) + x10_command(f"on {self._id}") else: - x10_command("fon " + self._id) + x10_command(f"fon {self._id}") self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) self._state = True def turn_off(self, **kwargs): """Instruct the light to turn off.""" if self._is_cm11a: - x10_command("off " + self._id) + x10_command(f"off {self._id}") else: - x10_command("foff " + self._id) + x10_command(f"foff {self._id}") self._state = False def update(self): diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index c40ead2a892375..61de12eafbfd1e 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -935,6 +935,6 @@ def _get_property(self, prop, default=None): bg_prop = self.PROPERTIES_MAPPING.get(prop) if not bg_prop: - bg_prop = "bg_" + prop + bg_prop = f"bg_{prop}" return super()._get_property(bg_prop, default) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 1294fcaedbd518..6228a2bc0c8a10 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -797,7 +797,7 @@ async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operati operation.name, target_ieee, ) - zdo.debug("processing " + op_msg, *op_params) + zdo.debug(f"processing {op_msg}", *op_params) bind_tasks.append( ( diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 5a337b2a5373f9..a5ecf21e0c38e1 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -263,7 +263,7 @@ async def get_attribute_value(self, attribute, from_cache=True): def log(self, level, msg, *args): """Log a message.""" - msg = "[%s:%s]: " + msg + msg = f"[%s:%s]: {msg}" args = (self.device.nwk, self._id,) + args _LOGGER.log(level, msg, *args) @@ -357,7 +357,7 @@ async def async_configure(self): def log(self, level, msg, *args): """Log a message.""" - msg = "[%s:ZDO](%s): " + msg + msg = f"[%s:ZDO](%s): {msg}" args = (self._zha_device.nwk, self._zha_device.model) + args _LOGGER.log(level, msg, *args) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 77e0263c06c34a..dbaf3fd44352b2 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -528,6 +528,6 @@ async def async_remove_from_group(self, group_id): def log(self, level, msg, *args): """Log a message.""" - msg = "[%s](%s): " + msg + msg = f"[%s](%s): {msg}" args = (self.nwk, self.model) + args _LOGGER.log(level, msg, *args) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 102472d25b0cbc..0b001bdedbc7bf 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -185,6 +185,6 @@ async def async_accept_signal(self, channel, signal, func, signal_override=False def log(self, level, msg, *args): """Log a message.""" - msg = "%s: " + msg + msg = f"%s: {msg}" args = (self.entity_id,) + args _LOGGER.log(level, msg, *args) diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index b94e19d6dbdb10..203131afdb1d28 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -161,9 +161,7 @@ def name(self): @property def unique_id(self): """Return the unique ID of the HVAC.""" - return "zhong_hong_hvac_{}_{}".format( - self._device.addr_out, self._device.addr_in - ) + return f"zhong_hong_hvac_{self._device.addr_out}_{self._device.addr_in}" @property def supported_features(self): diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py index 5b4fb0c9934a2a..724977a4220186 100644 --- a/homeassistant/components/zwave/cover.py +++ b/homeassistant/components/zwave/cover.py @@ -37,7 +37,7 @@ def _to_hex_str(id_in_bytes): Example: 0x1234 --> '0x1234' """ - return "0x{:04x}".format(id_in_bytes) + return f"0x{id_in_bytes:04x}" # For some reason node.manufacturer_id is of type string. So we need to convert diff --git a/homeassistant/config.py b/homeassistant/config.py index ee3ccc15f81935..6777c1ef5a570d 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -572,9 +572,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non def _log_pkg_error(package: str, component: str, config: Dict, message: str) -> None: """Log an error while merging packages.""" - message = "Package {} setup failed. Integration {} {}".format( - package, component, message - ) + message = f"Package {package} setup failed. Integration {component} {message}" pack_config = config[CONF_CORE][CONF_PACKAGES].get(package, config) message += " (See {}:{}). ".format( diff --git a/homeassistant/const.py b/homeassistant/const.py index 15dc5a099bc0ab..ae055c52f10249 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,8 +2,8 @@ MAJOR_VERSION = 0 MINOR_VERSION = 104 PATCH_VERSION = "0.dev0" -__short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) -__version__ = "{}.{}".format(__short_version__, PATCH_VERSION) +__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" +__version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) # Truthy date string triggers showing related deprecation warning messages. REQUIRED_NEXT_PYTHON_VER = (3, 8, 0) diff --git a/homeassistant/core.py b/homeassistant/core.py index e76673f572734e..af27110dc9d863 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -712,18 +712,14 @@ def __init__( if not valid_entity_id(entity_id) and not temp_invalid_id_bypass: raise InvalidEntityFormatError( - ( - "Invalid entity id encountered: {}. " - "Format should be ." - ).format(entity_id) + f"Invalid entity id encountered: {entity_id}. " + "Format should be ." ) if not valid_state(state): raise InvalidStateError( - ( - "Invalid state encountered for entity id: {}. " - "State max length is 255 characters." - ).format(entity_id) + f"Invalid state encountered for entity id: {entity_id}. " + "State max length is 255 characters." ) self.entity_id = entity_id.lower() @@ -1034,9 +1030,7 @@ def __repr__(self) -> str: self.domain, self.service, self.context.id, util.repr_helper(self.data) ) - return "".format( - self.domain, self.service, self.context.id - ) + return f"" class ServiceRegistry: diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 6a9f5b1dc5a78f..5e72dea9273ae2 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -153,9 +153,7 @@ async def _async_handle_step( if not hasattr(flow, method): self._progress.pop(flow.flow_id) raise UnknownStep( - "Handler {} doesn't support step {}".format( - flow.__class__.__name__, step_id - ) + f"Handler {flow.__class__.__name__} doesn't support step {step_id}" ) try: diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index 125d90e1162b96..ad97456968b864 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -36,5 +36,5 @@ def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]: Async friendly. """ - pattern = re.compile(r"^{}(| .+)$".format(domain)) + pattern = re.compile(fr"^{domain}(| .+)$") return [key for key in config.keys() if pattern.match(key)] diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 1b1e136ed894b3..6ac1326545a5ce 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -69,9 +69,7 @@ def _pack_error( package: str, component: str, config: ConfigType, message: str ) -> None: """Handle errors from packages: _log_pkg_error.""" - message = "Package {} setup failed. Component {} {}".format( - package, component, message - ) + message = f"Package {package} setup failed. Component {component} {message}" domain = f"homeassistant.packages.{package}.{component}" pack_config = core_config[CONF_PACKAGES].get(package, config) result.add_error(message, domain, pack_config) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index bcf0d42df70841..e357a2ba622cfb 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -135,7 +135,7 @@ def boolean(value: Any) -> bool: elif isinstance(value, Number): # type ignore: https://github.com/python/mypy/issues/3186 return value != 0 # type: ignore - raise vol.Invalid("invalid boolean value {}".format(value)) + raise vol.Invalid(f"invalid boolean value {value}") def isdevice(value: Any) -> str: @@ -144,7 +144,7 @@ def isdevice(value: Any) -> str: os.stat(value) return str(value) except OSError: - raise vol.Invalid("No device at {} found".format(value)) + raise vol.Invalid(f"No device at {value} found") def matches_regex(regex: str) -> Callable[[Any], str]: @@ -154,13 +154,11 @@ def matches_regex(regex: str) -> Callable[[Any], str]: def validator(value: Any) -> str: """Validate that value matches the given regex.""" if not isinstance(value, str): - raise vol.Invalid("not a string value: {}".format(value)) + raise vol.Invalid(f"not a string value: {value}") if not compiled.match(value): raise vol.Invalid( - "value {} does not match regular expression {}".format( - value, compiled.pattern - ) + f"value {value} does not match regular expression {compiled.pattern}" ) return value @@ -175,10 +173,10 @@ def is_regex(value: Any) -> Pattern[Any]: return r except TypeError: raise vol.Invalid( - "value {} is of the wrong type for a regular expression".format(value) + f"value {value} is of the wrong type for a regular expression" ) except re.error: - raise vol.Invalid("value {} is not a valid regular expression".format(value)) + raise vol.Invalid(f"value {value} is not a valid regular expression") def isfile(value: Any) -> str: @@ -220,7 +218,7 @@ def entity_id(value: Any) -> str: if valid_entity_id(str_value): return str_value - raise vol.Invalid("Entity ID {} is an invalid entity id".format(value)) + raise vol.Invalid(f"Entity ID {value} is an invalid entity id") def entity_ids(value: Union[str, List]) -> List[str]: @@ -256,9 +254,7 @@ def validate(values: Union[str, List]) -> List[str]: for ent_id in values: if split_entity_id(ent_id)[0] != domain: raise vol.Invalid( - "Entity ID '{}' does not belong to domain '{}'".format( - ent_id, domain - ) + f"Entity ID '{ent_id}' does not belong to domain '{domain}'" ) return values @@ -307,7 +303,7 @@ def time(value: Any) -> time_sys: raise vol.Invalid("Not a parseable type") if time_val is None: - raise vol.Invalid("Invalid time specified: {}".format(value)) + raise vol.Invalid(f"Invalid time specified: {value}") return time_val @@ -368,7 +364,7 @@ def time_period_seconds(value: Union[int, str]) -> timedelta: try: return timedelta(seconds=int(value)) except (ValueError, TypeError): - raise vol.Invalid("Expected seconds, got {}".format(value)) + raise vol.Invalid(f"Expected seconds, got {value}") time_period = vol.Any(time_period_str, time_period_seconds, timedelta, time_period_dict) @@ -400,7 +396,7 @@ def service(value: Any) -> str: str_value = string(value).lower() if valid_entity_id(str_value): return str_value - raise vol.Invalid("Service {} does not match format .".format(value)) + raise vol.Invalid(f"Service {value} does not match format .") def schema_with_slug_keys(value_schema: Union[T, Callable]) -> Callable: @@ -432,7 +428,7 @@ def slug(value: Any) -> str: slg = util_slugify(str_value) if str_value == slg: return str_value - raise vol.Invalid("invalid slug {} (try {})".format(value, slg)) + raise vol.Invalid(f"invalid slug {value} (try {slg})") def slugify(value: Any) -> str: @@ -442,7 +438,7 @@ def slugify(value: Any) -> str: slg = util_slugify(str(value)) if slg: return slg - raise vol.Invalid("Unable to slugify {}".format(value)) + raise vol.Invalid(f"Unable to slugify {value}") def string(value: Any) -> str: @@ -484,7 +480,7 @@ def template(value: Optional[Any]) -> template_helper.Template: template_value.ensure_valid() return cast(template_helper.Template, template_value) except TemplateError as ex: - raise vol.Invalid("invalid template ({})".format(ex)) + raise vol.Invalid(f"invalid template ({ex})") def template_complex(value: Any) -> Any: @@ -515,7 +511,7 @@ def datetime(value: Any) -> datetime_sys: date_val = None if date_val is None: - raise vol.Invalid("Invalid datetime specified: {}".format(value)) + raise vol.Invalid(f"Invalid datetime specified: {value}") return date_val @@ -545,8 +541,8 @@ def socket_timeout(value: Optional[Any]) -> object: if float_value > 0.0: return float_value raise vol.Invalid("Invalid socket timeout value. float > 0.0 required.") - except Exception as _: - raise vol.Invalid("Invalid socket timeout: {err}".format(err=_)) + except Exception as err: + raise vol.Invalid(f"Invalid socket timeout: {err}") # pylint: disable=no-value-for-parameter @@ -700,8 +696,8 @@ def validator(value: Dict[Hashable, Any]) -> Dict[Hashable, Any]: raise vol.Invalid("key dependencies require a dict") if key in value and dependency not in value: raise vol.Invalid( - 'dependency violation - key "{}" requires ' - 'key "{}" to exist'.format(key, dependency) + f'dependency violation - key "{key}" requires ' + f'key "{dependency}" to exist' ) return value diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index a6162dbde557f7..806540e57cef3a 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -65,7 +65,7 @@ def discover(hass, service, discovered, component, hass_config): async def async_discover(hass, service, discovered, component, hass_config): """Fire discovery event. Can ensure a component is loaded.""" if component in DEPENDENCY_BLACKLIST: - raise HomeAssistantError("Cannot discover the {} component.".format(component)) + raise HomeAssistantError(f"Cannot discover the {component} component.") if component is not None and component not in hass.config.components: await setup.async_setup_component(hass, component, hass_config) @@ -151,7 +151,7 @@ async def async_load_platform(hass, component, platform, discovered, hass_config assert hass_config, "You need to pass in the real hass config" if component in DEPENDENCY_BLACKLIST: - raise HomeAssistantError("Cannot discover the {} component.".format(component)) + raise HomeAssistantError(f"Cannot discover the {component} component.") setup_success = True diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index b1786130b582be..b7c806950a015f 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -562,7 +562,7 @@ def __eq__(self, other): def __repr__(self) -> str: """Return the representation.""" - return "".format(self.name, self.state) + return f"" # call an requests async def async_request_call(self, coro): diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 9b82eb76dec520..82236875ea4d10 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -304,9 +304,7 @@ async def _async_add_entity( suggested_object_id = entity.name if self.entity_namespace is not None: - suggested_object_id = "{} {}".format( - self.entity_namespace, suggested_object_id - ) + suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" if self.config_entry is not None: config_entry_id = self.config_entry.entry_id @@ -380,9 +378,7 @@ async def _async_add_entity( ) if self.entity_namespace is not None: - suggested_object_id = "{} {}".format( - self.entity_namespace, suggested_object_id - ) + suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" entity.entity_id = entity_registry.async_generate_entity_id( self.domain, suggested_object_id, self.entities.keys() ) @@ -402,9 +398,7 @@ async def _async_add_entity( if already_exists: msg = f"Entity id already exists: {entity.entity_id}" if entity.unique_id is not None: - msg += ". Platform {} does not generate unique IDs".format( - self.platform_name - ) + msg += f". Platform {self.platform_name} does not generate unique IDs" raise HomeAssistantError(msg) entity_id = entity.entity_id diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 77d8ccc00e08e5..2673162a841fdd 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -323,9 +323,8 @@ def _async_update_entity( ) if conflict: raise ValueError( - "Unique id '{}' is already in use by '{}'".format( - new_unique_id, conflict.entity_id - ) + f"Unique id '{new_unique_id}' is already in use by " + f"'{conflict.entity_id}'" ) changes["unique_id"] = new_unique_id diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index b2a1d58717bce2..dd64e9c92f11d6 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -8,7 +8,7 @@ def icon_for_battery_level( """Return a battery icon valid identifier.""" icon = "mdi:battery" if battery_level is None: - return icon + "-unknown" + return f"{icon}-unknown" if charging and battery_level > 10: icon += "-charging-{}".format(int(round(battery_level / 20 - 0.01)) * 20) elif charging: diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 181d1baebc0b09..8fdf617e3f6547 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -158,7 +158,7 @@ async def async_handle(self, intent_obj: "Intent") -> "IntentResponse": def __repr__(self) -> str: """Represent a string of an intent handler.""" - return "<{} - {}>".format(self.__class__.__name__, self.intent_type) + return f"<{self.__class__.__name__} - {self.intent_type}>" def _fuzzymatch(name: str, items: Iterable[T], key: Callable[[T], str]) -> Optional[T]: diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 8e0faa2ce4d1a7..837a561181d407 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -299,7 +299,7 @@ def async_script_delay(now): _LOGGER.error("Error rendering '%s' delay template: %s", self.name, ex) raise _StopScript - self.last_action = action.get(CONF_ALIAS, "delay {}".format(delay)) + self.last_action = action.get(CONF_ALIAS, f"delay {delay}") self._log("Executing step %s" % self.last_action) unsub = async_track_point_in_utc_time( @@ -408,7 +408,7 @@ async def _async_check_condition(self, action, variables, context): self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION]) check = config(self.hass, variables) - self._log("Test condition {}: {}".format(self.last_action, check)) + self._log(f"Test condition {self.last_action}: {check}") if not check: raise _StopScript @@ -446,6 +446,6 @@ def _async_remove_listener(self): def _log(self, msg): """Logger helper.""" if self.name is not None: - msg = "Script {}: {}".format(self.name, msg) + msg = f"Script {self.name}: {msg}" _LOGGER.info(msg) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 5381f7659934f4..16fabe251afbe1 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -240,7 +240,7 @@ def async_set_service_schema(hass, domain, service, schema): "fields": schema.get("fields") or {}, } - hass.data[SERVICE_DESCRIPTION_CACHE]["{}.{}".format(domain, service)] = description + hass.data[SERVICE_DESCRIPTION_CACHE][f"{domain}.{service}"] = description @bind_hass diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index b27120e1825f39..8565315f87f5c9 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -328,7 +328,7 @@ def __getattr__(self, name): if not valid_entity_id(name): raise TemplateError(f"Invalid entity ID '{name}'") return _get_state(self._hass, name) - if not valid_entity_id(name + ".entity"): + if not valid_entity_id(f"{name}.entity"): raise TemplateError(f"Invalid domain name '{name}'") return DomainStates(self._hass, name) @@ -451,7 +451,7 @@ def __repr__(self): """Representation of Template State.""" state = object.__getattribute__(self, "_access_state")() rep = state.__repr__() - return "